From a23fa7fc66110234d83ab0fd5882bce6fb79f024 Mon Sep 17 00:00:00 2001 From: csf Date: Mon, 23 May 2022 16:02:37 +0800 Subject: [PATCH 0001/2015] add desktop --- flutter/lib/common.dart | 1 + flutter/lib/main.dart | 13 +++++++++++-- flutter/lib/models/model.dart | 8 ++++---- flutter/lib/models/native_model.dart | 5 +++++ flutter/lib/models/web_model.dart | 2 +- flutter/lib/pages/connection_page.dart | 4 ++-- flutter/lib/pages/remote_page.dart | 8 ++++---- 7 files changed, 28 insertions(+), 13 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 3070833e4..8d432474e 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -11,6 +11,7 @@ final navigationBarKey = GlobalKey(); var isAndroid = false; var isIOS = false; var isWeb = false; +var isWebDesktop = false; var isDesktop = false; var version = ""; int androidVersion = 0; diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index a81a047b4..79e433d7f 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -1,4 +1,7 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; +import 'package:flutter_hbb/pages/desktop_home_page.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:provider/provider.dart'; import 'package:firebase_analytics/firebase_analytics.dart'; @@ -16,7 +19,9 @@ Future main() async { await a; await b; refreshCurrentUser(); - toAndroidChannelInit(); + if (Platform.isAndroid) { + toAndroidChannelInit(); + } runApp(App()); } @@ -39,7 +44,11 @@ class App extends StatelessWidget { primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, ), - home: !isAndroid ? WebHomePage() : HomePage(), + home: isDesktop + ? DesktopHomePage() + : !isAndroid + ? WebHomePage() + : HomePage(), navigatorObservers: [ FirebaseAnalyticsObserver(analytics: analytics), FlutterSmartDialog.observer diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 313ab3fc1..72c960be7 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -282,7 +282,7 @@ class ImageModel with ChangeNotifier { void update(ui.Image? image) { if (_image == null && image != null) { - if (isDesktop) { + if (isWebDesktop) { FFI.canvasModel.updateViewStyle(); } else { final size = MediaQueryData.fromWindow(ui.window).size; @@ -394,7 +394,7 @@ class CanvasModel with ChangeNotifier { } void resetOffset() { - if (isDesktop) { + if (isWebDesktop) { updateViewStyle(); } else { _x = 0; @@ -783,7 +783,7 @@ class FFI { static void close() { chatModel.close(); - if (FFI.imageModel.image != null && !isDesktop) { + if (FFI.imageModel.image != null && !isWebDesktop) { savePreference(id, cursorModel.x, cursorModel.y, canvasModel.x, canvasModel.y, canvasModel.scale, ffiModel.pi.currentDisplay); } @@ -919,7 +919,7 @@ void savePreference(String id, double xCursor, double yCursor, double xCanvas, } Future?> getPreference(String id) async { - if (!isDesktop) return null; + if (!isWebDesktop) return null; SharedPreferences prefs = await SharedPreferences.getInstance(); var p = prefs.getString('peer' + id); if (p == null) return null; diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index f6824dda8..ffbe7a2f3 100644 --- a/flutter/lib/models/native_model.dart +++ b/flutter/lib/models/native_model.dart @@ -59,6 +59,11 @@ class PlatformFFI { static Future init() async { isIOS = Platform.isIOS; isAndroid = Platform.isAndroid; + isDesktop = Platform.isWindows || Platform.isMacOS || Platform.isLinux; + if (isDesktop) { + // TODO + return; + } final dylib = Platform.isAndroid ? DynamicLibrary.open('librustdesk.so') : DynamicLibrary.process(); diff --git a/flutter/lib/models/web_model.dart b/flutter/lib/models/web_model.dart index d9668272a..13b62998f 100644 --- a/flutter/lib/models/web_model.dart +++ b/flutter/lib/models/web_model.dart @@ -20,7 +20,7 @@ class PlatformFFI { static Future init() async { isWeb = true; - isDesktop = !context.callMethod('isMobile'); + isWebDesktop = !context.callMethod('isMobile'); context.callMethod('init'); version = getByName('version'); } diff --git a/flutter/lib/pages/connection_page.dart b/flutter/lib/pages/connection_page.dart index 1fe4dc6b0..1b5268586 100644 --- a/flutter/lib/pages/connection_page.dart +++ b/flutter/lib/pages/connection_page.dart @@ -211,8 +211,8 @@ class _ConnectionPageState extends State { width: width, child: Card( child: GestureDetector( - onTap: !isDesktop ? () => connect('${p.id}') : null, - onDoubleTap: isDesktop ? () => connect('${p.id}') : null, + onTap: !isWebDesktop ? () => connect('${p.id}') : null, + onDoubleTap: isWebDesktop ? () => connect('${p.id}') : null, onLongPressStart: (details) { final x = details.globalPosition.dx; final y = details.globalPosition.dy; diff --git a/flutter/lib/pages/remote_page.dart b/flutter/lib/pages/remote_page.dart index 50e645540..1d8c02709 100644 --- a/flutter/lib/pages/remote_page.dart +++ b/flutter/lib/pages/remote_page.dart @@ -28,7 +28,7 @@ class RemotePage extends StatefulWidget { class _RemotePageState extends State { Timer? _interval; Timer? _timer; - bool _showBar = !isDesktop; + bool _showBar = !isWebDesktop; double _bottom = 0; String _value = ''; double _scale = 1; @@ -256,7 +256,7 @@ class _RemotePageState extends State { OverlayEntry(builder: (context) { return Container( color: Colors.black, - child: isDesktop + child: isWebDesktop ? getBodyForDesktopWithListener(keyboard) : SafeArea( child: Container( @@ -397,7 +397,7 @@ class _RemotePageState extends State { }, ) ] + - (isDesktop + (isWebDesktop ? [] : FFI.ffiModel.isPeerAndroid ? [ @@ -641,7 +641,7 @@ class _RemotePageState extends State { ) ])), value: 'enter_os_password')); - if (!isDesktop) { + if (!isWebDesktop) { if (perms['keyboard'] != false && perms['clipboard'] != false) { more.add(PopupMenuItem( child: Text(translate('Paste')), value: 'paste')); From 6a949b5f6acf1f4ec58f6531b718f1732295b461 Mon Sep 17 00:00:00 2001 From: csf Date: Mon, 23 May 2022 16:24:56 +0800 Subject: [PATCH 0002/2015] fix platform --- flutter/.metadata | 30 ++++++++++++++++++++++++++++-- flutter/lib/main.dart | 14 ++++++-------- flutter/pubspec.lock | 18 +++++++++--------- 3 files changed, 43 insertions(+), 19 deletions(-) diff --git a/flutter/.metadata b/flutter/.metadata index 107fcb7b5..8b4892cfb 100644 --- a/flutter/.metadata +++ b/flutter/.metadata @@ -1,10 +1,36 @@ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # -# This file should be version controlled and should not be manually edited. +# This file should be version controlled. version: - revision: 8874f21e79d7ec66d0457c7ab338348e31b17f1d + revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 channel: stable project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + - platform: linux + create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + - platform: macos + create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + - platform: windows + create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 79e433d7f..63a1c405b 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -14,21 +14,19 @@ import 'pages/settings_page.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); - var a = FFI.ffiModel.init(); - var b = Firebase.initializeApp(); - await a; - await b; - refreshCurrentUser(); - if (Platform.isAndroid) { + await FFI.ffiModel.init(); + // await Firebase.initializeApp(); + if (isAndroid) { toAndroidChannelInit(); } + refreshCurrentUser(); runApp(App()); } class App extends StatelessWidget { @override Widget build(BuildContext context) { - final analytics = FirebaseAnalytics.instance; + // final analytics = FirebaseAnalytics.instance; return MultiProvider( providers: [ ChangeNotifierProvider.value(value: FFI.ffiModel), @@ -50,7 +48,7 @@ class App extends StatelessWidget { ? WebHomePage() : HomePage(), navigatorObservers: [ - FirebaseAnalyticsObserver(analytics: analytics), + // FirebaseAnalyticsObserver(analytics: analytics), FlutterSmartDialog.observer ], builder: FlutterSmartDialog.init( diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 27c7c2e74..0f9691f3a 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -56,7 +56,7 @@ packages: name: collection url: "https://pub.dartlang.org" source: hosted - version: "1.15.0" + version: "1.16.0" cross_file: dependency: transitive description: @@ -126,7 +126,7 @@ packages: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.0" ffi: dependency: "direct main" description: @@ -318,7 +318,7 @@ packages: name: js url: "https://pub.dartlang.org" source: hosted - version: "0.6.3" + version: "0.6.4" matcher: dependency: transitive description: @@ -332,7 +332,7 @@ packages: name: material_color_utilities url: "https://pub.dartlang.org" source: hosted - version: "0.1.3" + version: "0.1.4" meta: dependency: transitive description: @@ -360,7 +360,7 @@ packages: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.0" + version: "1.8.1" path_provider: dependency: "direct main" description: @@ -540,7 +540,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.1" + version: "1.8.2" stack_trace: dependency: transitive description: @@ -575,7 +575,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.8" + version: "0.4.9" toggle_switch: dependency: "direct main" description: @@ -673,7 +673,7 @@ packages: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" + version: "2.1.2" wakelock: dependency: "direct main" description: @@ -745,5 +745,5 @@ packages: source: hosted version: "0.1.0" sdks: - dart: ">=2.16.1 <3.0.0" + dart: ">=2.17.0-0 <3.0.0" flutter: ">=2.10.0" From beb11bd31c2e82df91c8fd27e8455cd395f5b6bc Mon Sep 17 00:00:00 2001 From: csf Date: Mon, 23 May 2022 16:25:55 +0800 Subject: [PATCH 0003/2015] flutter create --platforms=windows,macos,linux --- flutter/README.md | 16 + flutter/analysis_options.yaml | 29 + flutter/linux/.gitignore | 1 + flutter/linux/CMakeLists.txt | 138 +++++ flutter/linux/flutter/CMakeLists.txt | 88 +++ .../flutter/generated_plugin_registrant.cc | 15 + .../flutter/generated_plugin_registrant.h | 15 + flutter/linux/flutter/generated_plugins.cmake | 24 + flutter/linux/main.cc | 6 + flutter/linux/my_application.cc | 104 ++++ flutter/linux/my_application.h | 18 + flutter/macos/.gitignore | 7 + flutter/macos/Flutter/Flutter-Debug.xcconfig | 1 + .../macos/Flutter/Flutter-Release.xcconfig | 1 + .../Flutter/GeneratedPluginRegistrant.swift | 24 + .../macos/Runner.xcodeproj/project.pbxproj | 572 ++++++++++++++++++ .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/xcschemes/Runner.xcscheme | 87 +++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + flutter/macos/Runner/AppDelegate.swift | 9 + .../AppIcon.appiconset/Contents.json | 68 +++ flutter/macos/Runner/Base.lproj/MainMenu.xib | 343 +++++++++++ flutter/macos/Runner/Configs/AppInfo.xcconfig | 14 + flutter/macos/Runner/Configs/Debug.xcconfig | 2 + flutter/macos/Runner/Configs/Release.xcconfig | 2 + .../macos/Runner/Configs/Warnings.xcconfig | 13 + .../macos/Runner/DebugProfile.entitlements | 12 + flutter/macos/Runner/Info.plist | 32 + flutter/macos/Runner/MainFlutterWindow.swift | 15 + flutter/macos/Runner/Release.entitlements | 8 + flutter/windows/.gitignore | 17 + flutter/windows/CMakeLists.txt | 101 ++++ flutter/windows/flutter/CMakeLists.txt | 104 ++++ .../flutter/generated_plugin_registrant.cc | 14 + .../flutter/generated_plugin_registrant.h | 15 + .../windows/flutter/generated_plugins.cmake | 24 + flutter/windows/runner/CMakeLists.txt | 32 + flutter/windows/runner/Runner.rc | 121 ++++ flutter/windows/runner/flutter_window.cpp | 61 ++ flutter/windows/runner/flutter_window.h | 33 + flutter/windows/runner/main.cpp | 43 ++ flutter/windows/runner/resource.h | 16 + flutter/windows/runner/resources/app_icon.ico | Bin 0 -> 33772 bytes flutter/windows/runner/runner.exe.manifest | 20 + flutter/windows/runner/utils.cpp | 64 ++ flutter/windows/runner/utils.h | 19 + flutter/windows/runner/win32_window.cpp | 245 ++++++++ flutter/windows/runner/win32_window.h | 98 +++ 49 files changed, 2714 insertions(+) create mode 100644 flutter/README.md create mode 100644 flutter/analysis_options.yaml create mode 100644 flutter/linux/.gitignore create mode 100644 flutter/linux/CMakeLists.txt create mode 100644 flutter/linux/flutter/CMakeLists.txt create mode 100644 flutter/linux/flutter/generated_plugin_registrant.cc create mode 100644 flutter/linux/flutter/generated_plugin_registrant.h create mode 100644 flutter/linux/flutter/generated_plugins.cmake create mode 100644 flutter/linux/main.cc create mode 100644 flutter/linux/my_application.cc create mode 100644 flutter/linux/my_application.h create mode 100644 flutter/macos/.gitignore create mode 100644 flutter/macos/Flutter/Flutter-Debug.xcconfig create mode 100644 flutter/macos/Flutter/Flutter-Release.xcconfig create mode 100644 flutter/macos/Flutter/GeneratedPluginRegistrant.swift create mode 100644 flutter/macos/Runner.xcodeproj/project.pbxproj create mode 100644 flutter/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 flutter/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme create mode 100644 flutter/macos/Runner.xcworkspace/contents.xcworkspacedata create mode 100644 flutter/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 flutter/macos/Runner/AppDelegate.swift create mode 100644 flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 flutter/macos/Runner/Base.lproj/MainMenu.xib create mode 100644 flutter/macos/Runner/Configs/AppInfo.xcconfig create mode 100644 flutter/macos/Runner/Configs/Debug.xcconfig create mode 100644 flutter/macos/Runner/Configs/Release.xcconfig create mode 100644 flutter/macos/Runner/Configs/Warnings.xcconfig create mode 100644 flutter/macos/Runner/DebugProfile.entitlements create mode 100644 flutter/macos/Runner/Info.plist create mode 100644 flutter/macos/Runner/MainFlutterWindow.swift create mode 100644 flutter/macos/Runner/Release.entitlements create mode 100644 flutter/windows/.gitignore create mode 100644 flutter/windows/CMakeLists.txt create mode 100644 flutter/windows/flutter/CMakeLists.txt create mode 100644 flutter/windows/flutter/generated_plugin_registrant.cc create mode 100644 flutter/windows/flutter/generated_plugin_registrant.h create mode 100644 flutter/windows/flutter/generated_plugins.cmake create mode 100644 flutter/windows/runner/CMakeLists.txt create mode 100644 flutter/windows/runner/Runner.rc create mode 100644 flutter/windows/runner/flutter_window.cpp create mode 100644 flutter/windows/runner/flutter_window.h create mode 100644 flutter/windows/runner/main.cpp create mode 100644 flutter/windows/runner/resource.h create mode 100644 flutter/windows/runner/resources/app_icon.ico create mode 100644 flutter/windows/runner/runner.exe.manifest create mode 100644 flutter/windows/runner/utils.cpp create mode 100644 flutter/windows/runner/utils.h create mode 100644 flutter/windows/runner/win32_window.cpp create mode 100644 flutter/windows/runner/win32_window.h diff --git a/flutter/README.md b/flutter/README.md new file mode 100644 index 000000000..ca73a12b2 --- /dev/null +++ b/flutter/README.md @@ -0,0 +1,16 @@ +# flutter_hbb + +A new Flutter project. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/flutter/analysis_options.yaml b/flutter/analysis_options.yaml new file mode 100644 index 000000000..61b6c4de1 --- /dev/null +++ b/flutter/analysis_options.yaml @@ -0,0 +1,29 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at + # https://dart-lang.github.io/linter/lints/index.html. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/flutter/linux/.gitignore b/flutter/linux/.gitignore new file mode 100644 index 000000000..d3896c984 --- /dev/null +++ b/flutter/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/flutter/linux/CMakeLists.txt b/flutter/linux/CMakeLists.txt new file mode 100644 index 000000000..1e5caff11 --- /dev/null +++ b/flutter/linux/CMakeLists.txt @@ -0,0 +1,138 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.10) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "flutter_hbb") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "com.carriez.flutter_hbb") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Define the application target. To change its name, change BINARY_NAME above, +# not the value here, or `flutter run` will no longer work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/flutter/linux/flutter/CMakeLists.txt b/flutter/linux/flutter/CMakeLists.txt new file mode 100644 index 000000000..d5bd01648 --- /dev/null +++ b/flutter/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/flutter/linux/flutter/generated_plugin_registrant.cc b/flutter/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 000000000..f6f23bfe9 --- /dev/null +++ b/flutter/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include + +void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); +} diff --git a/flutter/linux/flutter/generated_plugin_registrant.h b/flutter/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 000000000..e0f0a47bc --- /dev/null +++ b/flutter/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/flutter/linux/flutter/generated_plugins.cmake b/flutter/linux/flutter/generated_plugins.cmake new file mode 100644 index 000000000..f16b4c342 --- /dev/null +++ b/flutter/linux/flutter/generated_plugins.cmake @@ -0,0 +1,24 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + url_launcher_linux +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/flutter/linux/main.cc b/flutter/linux/main.cc new file mode 100644 index 000000000..e7c5c5437 --- /dev/null +++ b/flutter/linux/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/flutter/linux/my_application.cc b/flutter/linux/my_application.cc new file mode 100644 index 000000000..fbbf4ab0d --- /dev/null +++ b/flutter/linux/my_application.cc @@ -0,0 +1,104 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "flutter_hbb"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "flutter_hbb"); + } + + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, + "flags", G_APPLICATION_NON_UNIQUE, + nullptr)); +} diff --git a/flutter/linux/my_application.h b/flutter/linux/my_application.h new file mode 100644 index 000000000..72271d5e4 --- /dev/null +++ b/flutter/linux/my_application.h @@ -0,0 +1,18 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/flutter/macos/.gitignore b/flutter/macos/.gitignore new file mode 100644 index 000000000..746adbb6b --- /dev/null +++ b/flutter/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/flutter/macos/Flutter/Flutter-Debug.xcconfig b/flutter/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 000000000..c2efd0b60 --- /dev/null +++ b/flutter/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/flutter/macos/Flutter/Flutter-Release.xcconfig b/flutter/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 000000000..c2efd0b60 --- /dev/null +++ b/flutter/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/flutter/macos/Flutter/GeneratedPluginRegistrant.swift b/flutter/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 000000000..086b7f675 --- /dev/null +++ b/flutter/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,24 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import firebase_analytics +import firebase_core +import package_info +import path_provider_macos +import shared_preferences_macos +import url_launcher_macos +import wakelock_macos + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FLTFirebaseAnalyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAnalyticsPlugin")) + FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) + FLTPackageInfoPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) + WakelockMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockMacosPlugin")) +} diff --git a/flutter/macos/Runner.xcodeproj/project.pbxproj b/flutter/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 000000000..05460fe4b --- /dev/null +++ b/flutter/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,572 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 51; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 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 */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 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 /* flutter_hbb.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "flutter_hbb.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 = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* flutter_hbb.app */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* flutter_hbb.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/flutter/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/flutter/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/flutter/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/flutter/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/flutter/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 000000000..85831efcf --- /dev/null +++ b/flutter/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/flutter/macos/Runner.xcworkspace/contents.xcworkspacedata b/flutter/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..1d526a16e --- /dev/null +++ b/flutter/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/flutter/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/flutter/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/flutter/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/flutter/macos/Runner/AppDelegate.swift b/flutter/macos/Runner/AppDelegate.swift new file mode 100644 index 000000000..d53ef6437 --- /dev/null +++ b/flutter/macos/Runner/AppDelegate.swift @@ -0,0 +1,9 @@ +import Cocoa +import FlutterMacOS + +@NSApplicationMain +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } +} diff --git a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..a2ec33f19 --- /dev/null +++ b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/flutter/macos/Runner/Base.lproj/MainMenu.xib b/flutter/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 000000000..80e867a4e --- /dev/null +++ b/flutter/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/flutter/macos/Runner/Configs/AppInfo.xcconfig b/flutter/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 000000000..3c862dee9 --- /dev/null +++ b/flutter/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = flutter_hbb + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.carriez.flutterHbb + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2022 com.carriez. All rights reserved. diff --git a/flutter/macos/Runner/Configs/Debug.xcconfig b/flutter/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 000000000..36b0fd946 --- /dev/null +++ b/flutter/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/flutter/macos/Runner/Configs/Release.xcconfig b/flutter/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 000000000..dff4f4956 --- /dev/null +++ b/flutter/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/flutter/macos/Runner/Configs/Warnings.xcconfig b/flutter/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 000000000..42bcbf478 --- /dev/null +++ b/flutter/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/flutter/macos/Runner/DebugProfile.entitlements b/flutter/macos/Runner/DebugProfile.entitlements new file mode 100644 index 000000000..dddb8a30c --- /dev/null +++ b/flutter/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + + diff --git a/flutter/macos/Runner/Info.plist b/flutter/macos/Runner/Info.plist new file mode 100644 index 000000000..4789daa6a --- /dev/null +++ b/flutter/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/flutter/macos/Runner/MainFlutterWindow.swift b/flutter/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 000000000..2722837ec --- /dev/null +++ b/flutter/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController.init() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/flutter/macos/Runner/Release.entitlements b/flutter/macos/Runner/Release.entitlements new file mode 100644 index 000000000..852fa1a47 --- /dev/null +++ b/flutter/macos/Runner/Release.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/flutter/windows/.gitignore b/flutter/windows/.gitignore new file mode 100644 index 000000000..d492d0d98 --- /dev/null +++ b/flutter/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/flutter/windows/CMakeLists.txt b/flutter/windows/CMakeLists.txt new file mode 100644 index 000000000..3d4e30586 --- /dev/null +++ b/flutter/windows/CMakeLists.txt @@ -0,0 +1,101 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(flutter_hbb LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "flutter_hbb") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/flutter/windows/flutter/CMakeLists.txt b/flutter/windows/flutter/CMakeLists.txt new file mode 100644 index 000000000..930d2071a --- /dev/null +++ b/flutter/windows/flutter/CMakeLists.txt @@ -0,0 +1,104 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + windows-x64 $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/flutter/windows/flutter/generated_plugin_registrant.cc b/flutter/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 000000000..4f7884874 --- /dev/null +++ b/flutter/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,14 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include + +void RegisterPlugins(flutter::PluginRegistry* registry) { + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); +} diff --git a/flutter/windows/flutter/generated_plugin_registrant.h b/flutter/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 000000000..dc139d85a --- /dev/null +++ b/flutter/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/flutter/windows/flutter/generated_plugins.cmake b/flutter/windows/flutter/generated_plugins.cmake new file mode 100644 index 000000000..88b22e5c7 --- /dev/null +++ b/flutter/windows/flutter/generated_plugins.cmake @@ -0,0 +1,24 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + url_launcher_windows +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/flutter/windows/runner/CMakeLists.txt b/flutter/windows/runner/CMakeLists.txt new file mode 100644 index 000000000..b9e550fba --- /dev/null +++ b/flutter/windows/runner/CMakeLists.txt @@ -0,0 +1,32 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/flutter/windows/runner/Runner.rc b/flutter/windows/runner/Runner.rc new file mode 100644 index 000000000..d10e3f411 --- /dev/null +++ b/flutter/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#ifdef FLUTTER_BUILD_NUMBER +#define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER +#else +#define VERSION_AS_NUMBER 1,0,0 +#endif + +#ifdef FLUTTER_BUILD_NAME +#define VERSION_AS_STRING #FLUTTER_BUILD_NAME +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.carriez" "\0" + VALUE "FileDescription", "flutter_hbb" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "flutter_hbb" "\0" + VALUE "LegalCopyright", "Copyright (C) 2022 com.carriez. All rights reserved." "\0" + VALUE "OriginalFilename", "flutter_hbb.exe" "\0" + VALUE "ProductName", "flutter_hbb" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/flutter/windows/runner/flutter_window.cpp b/flutter/windows/runner/flutter_window.cpp new file mode 100644 index 000000000..b43b9095e --- /dev/null +++ b/flutter/windows/runner/flutter_window.cpp @@ -0,0 +1,61 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/flutter/windows/runner/flutter_window.h b/flutter/windows/runner/flutter_window.h new file mode 100644 index 000000000..6da0652f0 --- /dev/null +++ b/flutter/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/flutter/windows/runner/main.cpp b/flutter/windows/runner/main.cpp new file mode 100644 index 000000000..bbc7d344b --- /dev/null +++ b/flutter/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.CreateAndShow(L"flutter_hbb", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/flutter/windows/runner/resource.h b/flutter/windows/runner/resource.h new file mode 100644 index 000000000..66a65d1e4 --- /dev/null +++ b/flutter/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/flutter/windows/runner/resources/app_icon.ico b/flutter/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..c04e20caf6370ebb9253ad831cc31de4a9c965f6 GIT binary patch literal 33772 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 literal 0 HcmV?d00001 diff --git a/flutter/windows/runner/runner.exe.manifest b/flutter/windows/runner/runner.exe.manifest new file mode 100644 index 000000000..c977c4a42 --- /dev/null +++ b/flutter/windows/runner/runner.exe.manifest @@ -0,0 +1,20 @@ + + + + + PerMonitorV2 + + + + + + + + + + + + + + + diff --git a/flutter/windows/runner/utils.cpp b/flutter/windows/runner/utils.cpp new file mode 100644 index 000000000..f5bf9fa0f --- /dev/null +++ b/flutter/windows/runner/utils.cpp @@ -0,0 +1,64 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr); + std::string utf8_string; + if (target_length == 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, utf8_string.data(), + target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/flutter/windows/runner/utils.h b/flutter/windows/runner/utils.h new file mode 100644 index 000000000..3879d5475 --- /dev/null +++ b/flutter/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/flutter/windows/runner/win32_window.cpp b/flutter/windows/runner/win32_window.cpp new file mode 100644 index 000000000..c10f08dc7 --- /dev/null +++ b/flutter/windows/runner/win32_window.cpp @@ -0,0 +1,245 @@ +#include "win32_window.h" + +#include + +#include "resource.h" + +namespace { + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + FreeLibrary(user32_module); + } +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::CreateAndShow(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + return OnCreate(); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} diff --git a/flutter/windows/runner/win32_window.h b/flutter/windows/runner/win32_window.h new file mode 100644 index 000000000..17ba43112 --- /dev/null +++ b/flutter/windows/runner/win32_window.h @@ -0,0 +1,98 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates and shows a win32 window with |title| and position and size using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size to will treat the width height passed in to this function + // as logical pixels and scale to appropriate for the default monitor. Returns + // true if the window was created successfully. + bool CreateAndShow(const std::wstring& title, + const Point& origin, + const Size& size); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responsponds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ From be6f677b14888370c227f3692334b72bdd8a4b34 Mon Sep 17 00:00:00 2001 From: csf Date: Mon, 23 May 2022 16:44:23 +0800 Subject: [PATCH 0004/2015] fix .gitignore --- .gitignore | 1 - flutter/lib/pages/desktop_home_page.dart | 15 +++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 flutter/lib/pages/desktop_home_page.dart diff --git a/.gitignore b/.gitignore index 9ab24b514..d9d64935f 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,6 @@ src/version.rs *dmg *exe *tgz -*lib cert.pfx flutter_hbb *.bak diff --git a/flutter/lib/pages/desktop_home_page.dart b/flutter/lib/pages/desktop_home_page.dart new file mode 100644 index 000000000..a5d38b08b --- /dev/null +++ b/flutter/lib/pages/desktop_home_page.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; + +class DesktopHomePage extends StatefulWidget { + DesktopHomePage({Key? key}) : super(key: key); + + @override + State createState() => _DesktopHomePageState(); +} + +class _DesktopHomePageState extends State { + @override + Widget build(BuildContext context) { + return Text("Hello Desktop"); + } +} From 26281d95f67ad74704ad2c269367d0b20da6260c Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 24 May 2022 09:32:40 +0800 Subject: [PATCH 0005/2015] add: rustdesk linux flutter build cmake --- Cargo.toml | 2 +- flutter/linux/CMakeLists.txt | 22 +++++++++++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 68ba2ab3f..a3e2a66e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -99,7 +99,7 @@ async-process = "1.3" android_logger = "0.11" jni = "0.19.0" -[target.'cfg(any(target_os = "android", target_os = "ios"))'.dependencies] +[target.'cfg(any(target_os = "android", target_os = "ios", target_os = "linux"))'.dependencies] flutter_rust_bridge = "1.30.0" [workspace] diff --git a/flutter/linux/CMakeLists.txt b/flutter/linux/CMakeLists.txt index 1e5caff11..28f309c7f 100644 --- a/flutter/linux/CMakeLists.txt +++ b/flutter/linux/CMakeLists.txt @@ -1,5 +1,5 @@ # Project-level configuration. -cmake_minimum_required(VERSION 3.10) +cmake_minimum_required(VERSION 3.12) project(runner LANGUAGES CXX) # The name of the executable created for the application. Change this to change @@ -56,6 +56,24 @@ pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") +# flutter_rust_bridge +find_package(Corrosion REQUIRED) + +corrosion_import_crate(MANIFEST_PATH ../../Cargo.toml + # Equivalent to --all-features passed to cargo build +# [ALL_FEATURES] + # Equivalent to --no-default-features passed to cargo build +# [NO_DEFAULT_FEATURES] + # Disable linking of standard libraries (required for no_std crates). +# [NO_STD] + # Specify cargo build profile (e.g. release or a custom profile) +# [PROFILE ] + # Only import the specified crates from a workspace +# [CRATES ... ] + # Enable the specified features +# [FEATURES ... ] +) + # Define the application target. To change its name, change BINARY_NAME above, # not the value here, or `flutter run` will no longer work. # @@ -74,6 +92,8 @@ apply_standard_settings(${BINARY_NAME}) target_link_libraries(${BINARY_NAME} PRIVATE flutter) target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) +target_link_libraries(${BINARY_NAME} PRIVATE librustdesk) + # Run the Flutter tool portions of the build. This must not be removed. add_dependencies(${BINARY_NAME} flutter_assemble) From a81e2f9859db9f5c6e4f295b74b306a33574dc6e Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 24 May 2022 23:33:00 +0800 Subject: [PATCH 0006/2015] refactor: split desktop & mobile --- .../{ => desktop}/pages/desktop_home_page.dart | 0 flutter/lib/main.dart | 8 ++++---- flutter/lib/{ => mobile}/pages/chat_page.dart | 2 +- .../{ => mobile}/pages/connection_page.dart | 6 +++--- .../{ => mobile}/pages/file_manager_page.dart | 4 ++-- flutter/lib/{ => mobile}/pages/home_page.dart | 8 ++++---- .../lib/{ => mobile}/pages/remote_page.dart | 6 +++--- flutter/lib/{ => mobile}/pages/scan_page.dart | 4 ++-- .../lib/{ => mobile}/pages/server_page.dart | 8 ++++---- .../lib/{ => mobile}/pages/settings_page.dart | 4 ++-- flutter/lib/{ => mobile}/widgets/dialog.dart | 4 ++-- .../lib/{ => mobile}/widgets/gesture_help.dart | 2 +- flutter/lib/{ => mobile}/widgets/gestures.dart | 0 flutter/lib/{ => mobile}/widgets/overlay.dart | 2 +- flutter/lib/models/chat_model.dart | 2 +- flutter/lib/models/file_model.dart | 2 +- flutter/lib/models/model.dart | 4 ++-- flutter/lib/models/server_model.dart | 2 +- flutter/pubspec.lock | 18 +++++++++--------- 19 files changed, 43 insertions(+), 43 deletions(-) rename flutter/lib/{ => desktop}/pages/desktop_home_page.dart (100%) rename flutter/lib/{ => mobile}/pages/chat_page.dart (98%) rename flutter/lib/{ => mobile}/pages/connection_page.dart (98%) rename flutter/lib/{ => mobile}/pages/file_manager_page.dart (99%) rename flutter/lib/{ => mobile}/pages/home_page.dart (92%) rename flutter/lib/{ => mobile}/pages/remote_page.dart (99%) rename flutter/lib/{ => mobile}/pages/scan_page.dart (99%) rename flutter/lib/{ => mobile}/pages/server_page.dart (99%) rename flutter/lib/{ => mobile}/pages/settings_page.dart (99%) rename flutter/lib/{ => mobile}/widgets/dialog.dart (99%) rename flutter/lib/{ => mobile}/widgets/gesture_help.dart (99%) rename flutter/lib/{ => mobile}/widgets/gestures.dart (100%) rename flutter/lib/{ => mobile}/widgets/overlay.dart (99%) diff --git a/flutter/lib/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart similarity index 100% rename from flutter/lib/pages/desktop_home_page.dart rename to flutter/lib/desktop/pages/desktop_home_page.dart diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 63a1c405b..b12f9567c 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -1,16 +1,16 @@ import 'dart:io'; import 'package:flutter/material.dart'; -import 'package:flutter_hbb/pages/desktop_home_page.dart'; +import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:provider/provider.dart'; import 'package:firebase_analytics/firebase_analytics.dart'; import 'package:firebase_core/firebase_core.dart'; import 'common.dart'; import 'models/model.dart'; -import 'pages/home_page.dart'; -import 'pages/server_page.dart'; -import 'pages/settings_page.dart'; +import 'mobile/pages/home_page.dart'; +import 'mobile/pages/server_page.dart'; +import 'mobile/pages/settings_page.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); diff --git a/flutter/lib/pages/chat_page.dart b/flutter/lib/mobile/pages/chat_page.dart similarity index 98% rename from flutter/lib/pages/chat_page.dart rename to flutter/lib/mobile/pages/chat_page.dart index af940a29e..a4cf83ab8 100644 --- a/flutter/lib/pages/chat_page.dart +++ b/flutter/lib/mobile/pages/chat_page.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/models/chat_model.dart'; import 'package:provider/provider.dart'; -import '../models/model.dart'; +import '../../models/model.dart'; import 'home_page.dart'; ChatPage chatPage = ChatPage(); diff --git a/flutter/lib/pages/connection_page.dart b/flutter/lib/mobile/pages/connection_page.dart similarity index 98% rename from flutter/lib/pages/connection_page.dart rename to flutter/lib/mobile/pages/connection_page.dart index 1b5268586..8067ca146 100644 --- a/flutter/lib/pages/connection_page.dart +++ b/flutter/lib/mobile/pages/connection_page.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; -import 'package:flutter_hbb/pages/file_manager_page.dart'; +import 'package:flutter_hbb/mobile/pages/file_manager_page.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; import 'dart:async'; -import '../common.dart'; -import '../models/model.dart'; +import '../../common.dart'; +import '../../models/model.dart'; import 'home_page.dart'; import 'remote_page.dart'; import 'settings_page.dart'; diff --git a/flutter/lib/pages/file_manager_page.dart b/flutter/lib/mobile/pages/file_manager_page.dart similarity index 99% rename from flutter/lib/pages/file_manager_page.dart rename to flutter/lib/mobile/pages/file_manager_page.dart index 2cb980f44..0370bedff 100644 --- a/flutter/lib/pages/file_manager_page.dart +++ b/flutter/lib/mobile/pages/file_manager_page.dart @@ -7,8 +7,8 @@ import 'package:flutter_breadcrumb/flutter_breadcrumb.dart'; import 'package:wakelock/wakelock.dart'; import 'package:toggle_switch/toggle_switch.dart'; -import '../common.dart'; -import '../models/model.dart'; +import '../../common.dart'; +import '../../models/model.dart'; import '../widgets/dialog.dart'; class FileManagerPage extends StatefulWidget { diff --git a/flutter/lib/pages/home_page.dart b/flutter/lib/mobile/pages/home_page.dart similarity index 92% rename from flutter/lib/pages/home_page.dart rename to flutter/lib/mobile/pages/home_page.dart index 371aa3f64..756df7f91 100644 --- a/flutter/lib/pages/home_page.dart +++ b/flutter/lib/mobile/pages/home_page.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:flutter_hbb/pages/chat_page.dart'; -import 'package:flutter_hbb/pages/server_page.dart'; -import 'package:flutter_hbb/pages/settings_page.dart'; -import '../common.dart'; +import 'package:flutter_hbb/mobile/pages/chat_page.dart'; +import 'package:flutter_hbb/mobile/pages/server_page.dart'; +import 'package:flutter_hbb/mobile/pages/settings_page.dart'; +import '../../common.dart'; import '../widgets/overlay.dart'; import 'connection_page.dart'; diff --git a/flutter/lib/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart similarity index 99% rename from flutter/lib/pages/remote_page.dart rename to flutter/lib/mobile/pages/remote_page.dart index 1d8c02709..bf6220998 100644 --- a/flutter/lib/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -1,16 +1,16 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/models/chat_model.dart'; -import 'package:flutter_hbb/widgets/gesture_help.dart'; +import 'package:flutter_hbb/mobile/widgets/gesture_help.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:provider/provider.dart'; import 'package:flutter/services.dart'; import 'dart:ui' as ui; import 'dart:async'; import 'package:wakelock/wakelock.dart'; -import '../common.dart'; +import '../../common.dart'; import '../widgets/gestures.dart'; -import '../models/model.dart'; +import '../../models/model.dart'; import '../widgets/dialog.dart'; import '../widgets/overlay.dart'; diff --git a/flutter/lib/pages/scan_page.dart b/flutter/lib/mobile/pages/scan_page.dart similarity index 99% rename from flutter/lib/pages/scan_page.dart rename to flutter/lib/mobile/pages/scan_page.dart index 0bc6dfb21..a7d01f0b8 100644 --- a/flutter/lib/pages/scan_page.dart +++ b/flutter/lib/mobile/pages/scan_page.dart @@ -6,8 +6,8 @@ import 'package:zxing2/qrcode.dart'; import 'dart:io'; import 'dart:async'; import 'dart:convert'; -import '../common.dart'; -import '../models/model.dart'; +import '../../common.dart'; +import '../../models/model.dart'; class ScanPage extends StatefulWidget { @override diff --git a/flutter/lib/pages/server_page.dart b/flutter/lib/mobile/pages/server_page.dart similarity index 99% rename from flutter/lib/pages/server_page.dart rename to flutter/lib/mobile/pages/server_page.dart index 9377f495d..9caa327ea 100644 --- a/flutter/lib/pages/server_page.dart +++ b/flutter/lib/mobile/pages/server_page.dart @@ -1,13 +1,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_hbb/models/model.dart'; -import 'package:flutter_hbb/widgets/dialog.dart'; +import 'package:flutter_hbb/mobile/widgets/dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:provider/provider.dart'; -import '../common.dart'; -import '../models/server_model.dart'; +import '../../common.dart'; +import '../../models/server_model.dart'; import 'home_page.dart'; -import '../models/model.dart'; +import '../../models/model.dart'; class ServerPage extends StatelessWidget implements PageShape { @override diff --git a/flutter/lib/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart similarity index 99% rename from flutter/lib/pages/settings_page.dart rename to flutter/lib/mobile/pages/settings_page.dart index 90ff0d564..a1225ae85 100644 --- a/flutter/lib/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -4,9 +4,9 @@ import 'package:url_launcher/url_launcher.dart'; import 'package:provider/provider.dart'; import 'dart:convert'; import 'package:http/http.dart' as http; -import '../common.dart'; +import '../../common.dart'; import '../widgets/dialog.dart'; -import '../models/model.dart'; +import '../../models/model.dart'; import 'home_page.dart'; import 'scan_page.dart'; diff --git a/flutter/lib/widgets/dialog.dart b/flutter/lib/mobile/widgets/dialog.dart similarity index 99% rename from flutter/lib/widgets/dialog.dart rename to flutter/lib/mobile/widgets/dialog.dart index 7781cfe40..57d44e2aa 100644 --- a/flutter/lib/widgets/dialog.dart +++ b/flutter/lib/mobile/widgets/dialog.dart @@ -2,8 +2,8 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; -import '../common.dart'; -import '../models/model.dart'; +import '../../common.dart'; +import '../../models/model.dart'; void clientClose() { msgBox('', 'Close', 'Are you sure to close the connection?'); diff --git a/flutter/lib/widgets/gesture_help.dart b/flutter/lib/mobile/widgets/gesture_help.dart similarity index 99% rename from flutter/lib/widgets/gesture_help.dart rename to flutter/lib/mobile/widgets/gesture_help.dart index e907890b0..37cc77c8f 100644 --- a/flutter/lib/widgets/gesture_help.dart +++ b/flutter/lib/mobile/widgets/gesture_help.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; import 'package:toggle_switch/toggle_switch.dart'; -import '../models/model.dart'; +import '../../models/model.dart'; class GestureIcons { static const String _family = 'gestureicons'; diff --git a/flutter/lib/widgets/gestures.dart b/flutter/lib/mobile/widgets/gestures.dart similarity index 100% rename from flutter/lib/widgets/gestures.dart rename to flutter/lib/mobile/widgets/gestures.dart diff --git a/flutter/lib/widgets/overlay.dart b/flutter/lib/mobile/widgets/overlay.dart similarity index 99% rename from flutter/lib/widgets/overlay.dart rename to flutter/lib/mobile/widgets/overlay.dart index a90492f51..b2176ef0a 100644 --- a/flutter/lib/widgets/overlay.dart +++ b/flutter/lib/mobile/widgets/overlay.dart @@ -2,7 +2,7 @@ import 'package:draggable_float_widget/draggable_float_widget.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; -import '../models/model.dart'; +import '../../models/model.dart'; import '../pages/chat_page.dart'; OverlayEntry? chatIconOverlayEntry; diff --git a/flutter/lib/models/chat_model.dart b/flutter/lib/models/chat_model.dart index eaf8d2243..efef5f1e4 100644 --- a/flutter/lib/models/chat_model.dart +++ b/flutter/lib/models/chat_model.dart @@ -3,7 +3,7 @@ import 'dart:convert'; import 'package:dash_chat/dash_chat.dart'; import 'package:flutter/material.dart'; -import '../widgets/overlay.dart'; +import '../../mobile/widgets/overlay.dart'; import 'model.dart'; class MessageBody { diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index 49184cf5b..2122b146f 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -1,8 +1,8 @@ import 'dart:async'; import 'dart:convert'; import 'package:flutter_hbb/common.dart'; -import 'package:flutter_hbb/pages/file_manager_page.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_hbb/mobile/pages/file_manager_page.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:path/path.dart' as Path; diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 72c960be7..aef7a535d 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -12,8 +12,8 @@ import 'package:flutter/material.dart'; import 'package:tuple/tuple.dart'; import 'dart:async'; import '../common.dart'; -import '../widgets/dialog.dart'; -import '../widgets/overlay.dart'; +import '../mobile/widgets/dialog.dart'; +import '../mobile/widgets/overlay.dart'; import 'native_model.dart' if (dart.library.html) 'web_model.dart'; typedef HandleMsgBox = void Function(Map evt, String id); diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index f3a366cf1..a673a78a5 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -3,7 +3,7 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:wakelock/wakelock.dart'; import '../common.dart'; -import '../pages/server_page.dart'; +import '../mobile/pages/server_page.dart'; import 'model.dart'; const loginDialogTag = "LOGIN"; diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 0f9691f3a..e927ea50c 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -262,14 +262,14 @@ packages: name: http_parser url: "https://pub.dartlang.org" source: hosted - version: "4.0.0" + version: "4.0.1" image: dependency: "direct main" description: name: image url: "https://pub.dartlang.org" source: hosted - version: "3.1.3" + version: "3.2.0" image_picker: dependency: "direct main" description: @@ -297,7 +297,7 @@ packages: name: image_picker_ios url: "https://pub.dartlang.org" source: hosted - version: "0.8.5+2" + version: "0.8.5+5" image_picker_platform_interface: dependency: transitive description: @@ -423,7 +423,7 @@ packages: name: petitparser url: "https://pub.dartlang.org" source: hosted - version: "4.4.0" + version: "5.0.0" platform: dependency: transitive description: @@ -603,7 +603,7 @@ packages: name: typed_data url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.3.1" url_launcher: dependency: "direct main" description: @@ -624,7 +624,7 @@ packages: name: url_launcher_ios url: "https://pub.dartlang.org" source: hosted - version: "6.0.16" + version: "6.0.17" url_launcher_linux: dependency: transitive description: @@ -715,7 +715,7 @@ packages: name: win32 url: "https://pub.dartlang.org" source: hosted - version: "2.5.2" + version: "2.6.1" xdg_directories: dependency: transitive description: @@ -729,7 +729,7 @@ packages: name: xml url: "https://pub.dartlang.org" source: hosted - version: "5.3.1" + version: "6.0.1" yaml: dependency: transitive description: @@ -745,5 +745,5 @@ packages: source: hosted version: "0.1.0" sdks: - dart: ">=2.17.0-0 <3.0.0" + dart: ">=2.17.0 <3.0.0" flutter: ">=2.10.0" From a364e7f8082b88053ab0bf055076af1cf01ede3a Mon Sep 17 00:00:00 2001 From: Kingtous Date: Wed, 25 May 2022 00:28:59 +0800 Subject: [PATCH 0007/2015] demo: use mobile_ffi to get id for desktop version --- .../lib/desktop/pages/desktop_home_page.dart | 74 ++++++++++++++++++- flutter/lib/main.dart | 11 +-- flutter/lib/models/native_model.dart | 28 ++++--- flutter/lib/models/server_model.dart | 13 +++- .../Flutter/GeneratedPluginRegistrant.swift | 4 +- flutter/pubspec.lock | 41 +++++++++- flutter/pubspec.yaml | 2 +- libs/hbb_common/src/config.rs | 2 +- src/common.rs | 6 +- src/lib.rs | 6 +- src/mobile.rs | 6 +- src/mobile_ffi.rs | 4 + 12 files changed, 165 insertions(+), 32 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index a5d38b08b..9ed485df8 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -1,4 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/models/model.dart'; +import 'package:provider/provider.dart'; class DesktopHomePage extends StatefulWidget { DesktopHomePage({Key? key}) : super(key: key); @@ -10,6 +13,75 @@ class DesktopHomePage extends StatefulWidget { class _DesktopHomePageState extends State { @override Widget build(BuildContext context) { - return Text("Hello Desktop"); + return Scaffold( + body: Container( + child: Row( + children: [ + Flexible( + child: buildServerInfo(context), + flex: 1, + ), + Flexible( + child: buildServerBoard(context), + flex: 4, + ), + ], + ), + ), + ); + } + + buildServerInfo(BuildContext context) { + return ChangeNotifierProvider.value( + value: FFI.serverModel, + child: Column( + children: [buildIDBoard(context)], + ), + ); + } + + buildServerBoard(BuildContext context) { + return Center( + child: Text("waiting implementation"), + ); + } + + buildIDBoard(BuildContext context) { + final model = FFI.serverModel; + return Card( + elevation: 0.5, + child: Container( + margin: EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + children: [ + Container( + width: 4, + height: 70, + decoration: BoxDecoration(color: MyTheme.accent), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + translate("ID"), + style: + TextStyle(fontSize: 18, fontWeight: FontWeight.w500), + ), + TextFormField( + controller: model.serverId, + ), + ], + ), + ), + ), + ], + ), + ), + ); } } diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index b12f9567c..f69ab6465 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -1,16 +1,13 @@ -import 'dart:io'; - import 'package:flutter/material.dart'; import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:provider/provider.dart'; -import 'package:firebase_analytics/firebase_analytics.dart'; -import 'package:firebase_core/firebase_core.dart'; + import 'common.dart'; -import 'models/model.dart'; import 'mobile/pages/home_page.dart'; import 'mobile/pages/server_page.dart'; import 'mobile/pages/settings_page.dart'; +import 'models/model.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -20,6 +17,10 @@ Future main() async { toAndroidChannelInit(); } refreshCurrentUser(); + if (isDesktop) { + print("desktop mode: starting service"); + FFI.serverModel.startService(); + } runApp(App()); } diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index ffbe7a2f3..21ecd37e3 100644 --- a/flutter/lib/models/native_model.dart +++ b/flutter/lib/models/native_model.dart @@ -1,15 +1,17 @@ import 'dart:convert'; +import 'dart:ffi'; import 'dart:io'; import 'dart:typed_data'; -import 'dart:ffi'; -import 'package:ffi/ffi.dart'; -import 'package:path_provider/path_provider.dart'; + import 'package:device_info/device_info.dart'; -import 'package:package_info/package_info.dart'; import 'package:external_path/external_path.dart'; +import 'package:ffi/ffi.dart'; import 'package:flutter/services.dart'; -import '../generated_bridge.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:path_provider/path_provider.dart'; + import '../common.dart'; +import '../generated_bridge.dart'; class RgbaFrame extends Struct { @Uint32() @@ -60,13 +62,19 @@ class PlatformFFI { isIOS = Platform.isIOS; isAndroid = Platform.isAndroid; isDesktop = Platform.isWindows || Platform.isMacOS || Platform.isLinux; - if (isDesktop) { - // TODO - return; - } + // if (isDesktop) { + // // TODO + // return; + // } final dylib = Platform.isAndroid ? DynamicLibrary.open('librustdesk.so') - : DynamicLibrary.process(); + : Platform.isLinux + ? DynamicLibrary.open("/usr/lib/rustdesk/librustdesk.so") + : Platform.isWindows + ? DynamicLibrary.open("librustdesk.dll") + : Platform.isMacOS + ? DynamicLibrary.open("librustdesk.dylib") + : DynamicLibrary.process(); print('initializing FFI'); try { _getByName = dylib.lookupFunction('get_by_name'); diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index a673a78a5..681ff3c25 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -1,7 +1,10 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:wakelock/wakelock.dart'; + import '../common.dart'; import '../mobile/pages/server_page.dart'; import 'model.dart'; @@ -203,7 +206,10 @@ class ServerModel with ChangeNotifier { FFI.setByName("start_service"); getIDPasswd(); updateClientState(); - Wakelock.enable(); + if (!Platform.isLinux) { + // current linux is not supported + Wakelock.enable(); + } } Future stopService() async { @@ -212,7 +218,10 @@ class ServerModel with ChangeNotifier { await FFI.invokeMethod("stop_service"); FFI.setByName("stop_service"); notifyListeners(); - Wakelock.disable(); + if (!Platform.isLinux) { + // current linux is not supported + Wakelock.disable(); + } } Future initInput() async { diff --git a/flutter/macos/Flutter/GeneratedPluginRegistrant.swift b/flutter/macos/Flutter/GeneratedPluginRegistrant.swift index 086b7f675..a540eabec 100644 --- a/flutter/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/flutter/macos/Flutter/GeneratedPluginRegistrant.swift @@ -7,7 +7,7 @@ import Foundation import firebase_analytics import firebase_core -import package_info +import package_info_plus_macos import path_provider_macos import shared_preferences_macos import url_launcher_macos @@ -16,7 +16,7 @@ import wakelock_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FLTFirebaseAnalyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAnalyticsPlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) - FLTPackageInfoPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlugin")) + FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index e927ea50c..083c4a494 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -347,13 +347,48 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.0" - package_info: + package_info_plus: dependency: "direct main" description: - name: package_info + name: package_info_plus url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "1.4.2" + package_info_plus_linux: + dependency: transitive + description: + name: package_info_plus_linux + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.5" + package_info_plus_macos: + dependency: transitive + description: + name: package_info_plus_macos + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + package_info_plus_web: + dependency: transitive + description: + name: package_info_plus_web + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.5" + package_info_plus_windows: + dependency: transitive + description: + name: package_info_plus_windows + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.5" path: dependency: transitive description: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index eba7dfd12..c8d31e87e 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -37,7 +37,7 @@ dependencies: wakelock: ^0.5.2 device_info: ^2.0.2 firebase_analytics: ^9.1.5 - package_info: ^2.0.2 + package_info_plus: ^1.4.2 url_launcher: ^6.0.9 shared_preferences: ^2.0.6 toggle_switch: ^1.4.0 diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index ce0fc509a..e6ca46ee3 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -39,7 +39,7 @@ lazy_static::lazy_static! { pub static ref PROD_RENDEZVOUS_SERVER: Arc> = Default::default(); pub static ref APP_NAME: Arc> = Arc::new(RwLock::new("RustDesk".to_owned())); } -#[cfg(any(target_os = "android", target_os = "ios"))] +// #[cfg(any(target_os = "android", target_os = "ios"))] lazy_static::lazy_static! { pub static ref APP_DIR: Arc> = Default::default(); pub static ref APP_HOME_DIR: Arc> = Default::default(); diff --git a/src/common.rs b/src/common.rs index 2a865afbb..03e5f4f4b 100644 --- a/src/common.rs +++ b/src/common.rs @@ -12,7 +12,7 @@ use hbb_common::{ rendezvous_proto::*, sleep, socket_client, tokio, ResultType, }; -#[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))] +// #[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))] use hbb_common::{config::RENDEZVOUS_PORT, futures::future::join_all}; use std::sync::{Arc, Mutex}; @@ -336,7 +336,7 @@ pub async fn get_nat_type(ms_timeout: u64) -> i32 { crate::ipc::get_nat_type(ms_timeout).await } -#[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))] +// #[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))] #[tokio::main(flavor = "current_thread")] async fn test_rendezvous_server_() { let servers = Config::get_rendezvous_servers(); @@ -363,7 +363,7 @@ async fn test_rendezvous_server_() { join_all(futs).await; } -#[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))] +// #[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))] pub fn test_rendezvous_server() { std::thread::spawn(test_rendezvous_server_); } diff --git a/src/lib.rs b/src/lib.rs index 8dafb727e..556d22594 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,11 +21,11 @@ pub mod ipc; pub mod ui; mod version; pub use version::*; -#[cfg(any(target_os = "android", target_os = "ios"))] +// #[cfg(any(target_os = "android", target_os = "ios"))] mod bridge_generated; -#[cfg(any(target_os = "android", target_os = "ios"))] +// #[cfg(any(target_os = "android", target_os = "ios"))] pub mod mobile; -#[cfg(any(target_os = "android", target_os = "ios"))] +// #[cfg(any(target_os = "android", target_os = "ios"))] pub mod mobile_ffi; use common::*; #[cfg(feature = "cli")] diff --git a/src/mobile.rs b/src/mobile.rs index 200c0b24d..80dd1f807 100644 --- a/src/mobile.rs +++ b/src/mobile.rs @@ -1165,7 +1165,7 @@ pub fn make_fd_to_json(fd: FileDirectory) -> String { // Server Side // TODO connection_manager need use struct and trait,impl default method -#[cfg(target_os = "android")] +#[cfg(not(any(target_os = "ios")))] pub mod connection_manager { use std::{ collections::HashMap, @@ -1191,6 +1191,7 @@ pub mod connection_manager { task::spawn_blocking, }, }; + #[cfg(any(target_os = "android"))] use scrap::android::call_main_service_set_by_name; use serde_derive::Serialize; @@ -1253,6 +1254,7 @@ pub mod connection_manager { client.authorized = true; let client_json = serde_json::to_string(&client).unwrap_or("".into()); // send to Android service,active notification no matter UI is shown or not. + #[cfg(any(target_os = "android"))] if let Err(e) = call_main_service_set_by_name( "on_client_authorized", Some(&client_json), @@ -1265,6 +1267,7 @@ pub mod connection_manager { } else { let client_json = serde_json::to_string(&client).unwrap_or("".into()); // send to Android service,active notification no matter UI is shown or not. + #[cfg(any(target_os = "android"))] if let Err(e) = call_main_service_set_by_name( "try_start_without_auth", Some(&client_json), @@ -1343,6 +1346,7 @@ pub mod connection_manager { .next() .is_none() { + #[cfg(any(target_os = "android"))] if let Err(e) = call_main_service_set_by_name("stop_capture", None, None) { log::debug!("stop_capture err:{}", e); } diff --git a/src/mobile_ffi.rs b/src/mobile_ffi.rs index 2d1b90e7c..ec6ef9082 100644 --- a/src/mobile_ffi.rs +++ b/src/mobile_ffi.rs @@ -29,6 +29,7 @@ fn initialize(app_dir: &str) { use hbb_common::env_logger::*; init_from_env(Env::default().filter_or(DEFAULT_FILTER_ENV, "debug")); } + #[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))] crate::common::test_rendezvous_server(); crate::common::test_nat_type(); #[cfg(target_os = "android")] @@ -182,9 +183,11 @@ unsafe extern "C" fn set_by_name(name: *const c_char, value: *const c_char) { "init" => { initialize(value); } + #[cfg(any(target_os = "android", target_os = "ios"))] "info1" => { *crate::common::MOBILE_INFO1.lock().unwrap() = value.to_owned(); } + #[cfg(any(target_os = "android", target_os = "ios"))] "info2" => { *crate::common::MOBILE_INFO2.lock().unwrap() = value.to_owned(); } @@ -293,6 +296,7 @@ unsafe extern "C" fn set_by_name(name: *const c_char, value: *const c_char) { if name == "custom-rendezvous-server" { #[cfg(target_os = "android")] crate::rendezvous_mediator::RendezvousMediator::restart(); + #[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))] crate::common::test_rendezvous_server(); } } From a68520df08b6a6e3ae282165881579dbf8906cf0 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Wed, 25 May 2022 14:30:19 +0800 Subject: [PATCH 0008/2015] fix: bridge compilation --- build.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/build.rs b/build.rs index 0f734715a..aaf0858c1 100644 --- a/build.rs +++ b/build.rs @@ -82,11 +82,11 @@ fn main() { hbb_common::gen_version(); install_oboe(); // there is problem with cfg(target_os) in build.rs, so use our workaround - let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); - if target_os == "android" || target_os == "ios" { + // let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); + // if target_os == "android" || target_os == "ios" { gen_flutter_rust_bridge(); - return; - } + // return; + // } #[cfg(all(windows, feature = "inline"))] build_manifest(); #[cfg(windows)] From 967482aa78c7f139c30fa6731dd8b1266e18d46f Mon Sep 17 00:00:00 2001 From: Kingtous Date: Wed, 25 May 2022 14:41:37 +0800 Subject: [PATCH 0009/2015] fix: add ffigen --- .github/workflows/ci.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cd8282187..2989051df 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -78,10 +78,18 @@ jobs: shell: bash run: | case ${{ matrix.job.target }} in - x86_64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake ;; + x86_64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev;; # arm-unknown-linux-*) sudo apt-get -y update ; sudo apt-get -y install gcc-arm-linux-gnueabihf ;; # aarch64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt-get -y install gcc-aarch64-linux-gnu ;; esac + + - name: Install flutter + uses: subosito/flutter-action@v2 + with: + channel: 'stable' + - name: Install flutter rust bridge deps + run: | + dart pub global activate ffigen - name: Restore from cache and install vcpkg uses: lukka/run-vcpkg@v7 From 3081df429ed355972d60769d91c8ebea07048f4b Mon Sep 17 00:00:00 2001 From: SoLongAndThanksForAllThePizza <103753680+SoLongAndThanksForAllThePizza@users.noreply.github.com> Date: Wed, 25 May 2022 17:20:35 +0800 Subject: [PATCH 0010/2015] remove flutter generated files --- flutter/.gitignore | 11 ++++++++- .../flutter/generated_plugin_registrant.cc | 15 ------------ .../flutter/generated_plugin_registrant.h | 15 ------------ flutter/linux/flutter/generated_plugins.cmake | 24 ------------------- .../Flutter/GeneratedPluginRegistrant.swift | 24 ------------------- .../flutter/generated_plugin_registrant.cc | 14 ----------- .../flutter/generated_plugin_registrant.h | 15 ------------ .../windows/flutter/generated_plugins.cmake | 24 ------------------- 8 files changed, 10 insertions(+), 132 deletions(-) delete mode 100644 flutter/linux/flutter/generated_plugin_registrant.cc delete mode 100644 flutter/linux/flutter/generated_plugin_registrant.h delete mode 100644 flutter/linux/flutter/generated_plugins.cmake delete mode 100644 flutter/macos/Flutter/GeneratedPluginRegistrant.swift delete mode 100644 flutter/windows/flutter/generated_plugin_registrant.cc delete mode 100644 flutter/windows/flutter/generated_plugin_registrant.h delete mode 100644 flutter/windows/flutter/generated_plugins.cmake diff --git a/flutter/.gitignore b/flutter/.gitignore index ab9a85d6c..7dc95a613 100644 --- a/flutter/.gitignore +++ b/flutter/.gitignore @@ -44,4 +44,13 @@ jniLibs .vscode # flutter rust bridge -lib/generated_bridge.dart \ No newline at end of file +lib/generated_bridge.dart + +# Flutter Generated Files +linux/flutter/generated_plugin_registrant.cc +linux/flutter/generated_plugin_registrant.h +linux/flutter/generated_plugins.cmake +macos/Flutter/GeneratedPluginRegistrant.swift +windows/flutter/generated_plugin_registrant.cc +windows/flutter/generated_plugin_registrant.h +windows/flutter/generated_plugins.cmake \ No newline at end of file diff --git a/flutter/linux/flutter/generated_plugin_registrant.cc b/flutter/linux/flutter/generated_plugin_registrant.cc deleted file mode 100644 index f6f23bfe9..000000000 --- a/flutter/linux/flutter/generated_plugin_registrant.cc +++ /dev/null @@ -1,15 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#include "generated_plugin_registrant.h" - -#include - -void fl_register_plugins(FlPluginRegistry* registry) { - g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); - url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); -} diff --git a/flutter/linux/flutter/generated_plugin_registrant.h b/flutter/linux/flutter/generated_plugin_registrant.h deleted file mode 100644 index e0f0a47bc..000000000 --- a/flutter/linux/flutter/generated_plugin_registrant.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#ifndef GENERATED_PLUGIN_REGISTRANT_ -#define GENERATED_PLUGIN_REGISTRANT_ - -#include - -// Registers Flutter plugins. -void fl_register_plugins(FlPluginRegistry* registry); - -#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/flutter/linux/flutter/generated_plugins.cmake b/flutter/linux/flutter/generated_plugins.cmake deleted file mode 100644 index f16b4c342..000000000 --- a/flutter/linux/flutter/generated_plugins.cmake +++ /dev/null @@ -1,24 +0,0 @@ -# -# Generated file, do not edit. -# - -list(APPEND FLUTTER_PLUGIN_LIST - url_launcher_linux -) - -list(APPEND FLUTTER_FFI_PLUGIN_LIST -) - -set(PLUGIN_BUNDLED_LIBRARIES) - -foreach(plugin ${FLUTTER_PLUGIN_LIST}) - add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) - target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) - list(APPEND PLUGIN_BUNDLED_LIBRARIES $) - list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) -endforeach(plugin) - -foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) - add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) - list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) -endforeach(ffi_plugin) diff --git a/flutter/macos/Flutter/GeneratedPluginRegistrant.swift b/flutter/macos/Flutter/GeneratedPluginRegistrant.swift deleted file mode 100644 index a540eabec..000000000 --- a/flutter/macos/Flutter/GeneratedPluginRegistrant.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// Generated file. Do not edit. -// - -import FlutterMacOS -import Foundation - -import firebase_analytics -import firebase_core -import package_info_plus_macos -import path_provider_macos -import shared_preferences_macos -import url_launcher_macos -import wakelock_macos - -func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { - FLTFirebaseAnalyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAnalyticsPlugin")) - FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) - FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin")) - PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) - SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) - UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) - WakelockMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockMacosPlugin")) -} diff --git a/flutter/windows/flutter/generated_plugin_registrant.cc b/flutter/windows/flutter/generated_plugin_registrant.cc deleted file mode 100644 index 4f7884874..000000000 --- a/flutter/windows/flutter/generated_plugin_registrant.cc +++ /dev/null @@ -1,14 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#include "generated_plugin_registrant.h" - -#include - -void RegisterPlugins(flutter::PluginRegistry* registry) { - UrlLauncherWindowsRegisterWithRegistrar( - registry->GetRegistrarForPlugin("UrlLauncherWindows")); -} diff --git a/flutter/windows/flutter/generated_plugin_registrant.h b/flutter/windows/flutter/generated_plugin_registrant.h deleted file mode 100644 index dc139d85a..000000000 --- a/flutter/windows/flutter/generated_plugin_registrant.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#ifndef GENERATED_PLUGIN_REGISTRANT_ -#define GENERATED_PLUGIN_REGISTRANT_ - -#include - -// Registers Flutter plugins. -void RegisterPlugins(flutter::PluginRegistry* registry); - -#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/flutter/windows/flutter/generated_plugins.cmake b/flutter/windows/flutter/generated_plugins.cmake deleted file mode 100644 index 88b22e5c7..000000000 --- a/flutter/windows/flutter/generated_plugins.cmake +++ /dev/null @@ -1,24 +0,0 @@ -# -# Generated file, do not edit. -# - -list(APPEND FLUTTER_PLUGIN_LIST - url_launcher_windows -) - -list(APPEND FLUTTER_FFI_PLUGIN_LIST -) - -set(PLUGIN_BUNDLED_LIBRARIES) - -foreach(plugin ${FLUTTER_PLUGIN_LIST}) - add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) - target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) - list(APPEND PLUGIN_BUNDLED_LIBRARIES $) - list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) -endforeach(plugin) - -foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) - add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) - list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) -endforeach(ffi_plugin) From 4d324063c52684348c5d1821cafd104cb9073dec Mon Sep 17 00:00:00 2001 From: SoLongAndThanksForAllThePizza <103753680+SoLongAndThanksForAllThePizza@users.noreply.github.com> Date: Wed, 25 May 2022 20:23:02 +0800 Subject: [PATCH 0011/2015] add flutter feature and rename mobile to flutter --- Cargo.toml | 6 ++++-- src/{mobile.rs => flutter.rs} | 0 src/{mobile_ffi.rs => flutter_ffi.rs} | 0 src/lib.rs | 10 +++++----- 4 files changed, 9 insertions(+), 7 deletions(-) rename src/{mobile.rs => flutter.rs} (100%) rename src/{mobile_ffi.rs => flutter_ffi.rs} (100%) diff --git a/Cargo.toml b/Cargo.toml index a3e2a66e1..2b707a688 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,8 @@ cli = [] use_samplerate = ["samplerate"] use_rubato = ["rubato"] use_dasp = ["dasp"] -default = ["use_dasp"] +flutter = ["flutter_rust_bridge"] +default = ["use_dasp","flutter"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -52,6 +53,7 @@ rpassword = "6.0" base64 = "0.13" sysinfo = "0.23" num_cpus = "1.13" +flutter_rust_bridge = { version = "1.30.0", optional = true } [target.'cfg(not(target_os = "linux"))'.dependencies] reqwest = { version = "0.11", features = ["json", "rustls-tls"], default-features=false } @@ -99,7 +101,7 @@ async-process = "1.3" android_logger = "0.11" jni = "0.19.0" -[target.'cfg(any(target_os = "android", target_os = "ios", target_os = "linux"))'.dependencies] +[target.'cfg(any(target_os = "android", target_os = "ios"))'.dependencies] flutter_rust_bridge = "1.30.0" [workspace] diff --git a/src/mobile.rs b/src/flutter.rs similarity index 100% rename from src/mobile.rs rename to src/flutter.rs diff --git a/src/mobile_ffi.rs b/src/flutter_ffi.rs similarity index 100% rename from src/mobile_ffi.rs rename to src/flutter_ffi.rs diff --git a/src/lib.rs b/src/lib.rs index 556d22594..84d9af8e1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,12 +21,12 @@ pub mod ipc; pub mod ui; mod version; pub use version::*; -// #[cfg(any(target_os = "android", target_os = "ios"))] +#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] mod bridge_generated; -// #[cfg(any(target_os = "android", target_os = "ios"))] -pub mod mobile; -// #[cfg(any(target_os = "android", target_os = "ios"))] -pub mod mobile_ffi; +#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] +pub mod flutter; +#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] +pub mod flutter_ffi; use common::*; #[cfg(feature = "cli")] pub mod cli; From 52d4b4226ecafdeb4ea4d2f8a7a564dacc3912b6 Mon Sep 17 00:00:00 2001 From: SoLongAndThanksForAllThePizza <103753680+SoLongAndThanksForAllThePizza@users.noreply.github.com> Date: Wed, 25 May 2022 20:26:46 +0800 Subject: [PATCH 0012/2015] fix flutter compile on windows --- flutter/windows/runner/CMakeLists.txt | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/flutter/windows/runner/CMakeLists.txt b/flutter/windows/runner/CMakeLists.txt index b9e550fba..bcaa06d73 100644 --- a/flutter/windows/runner/CMakeLists.txt +++ b/flutter/windows/runner/CMakeLists.txt @@ -16,6 +16,25 @@ add_executable(${BINARY_NAME} WIN32 "runner.exe.manifest" ) +# flutter_rust_bridge with Corrosion +find_package(Corrosion REQUIRED) + +corrosion_import_crate(MANIFEST_PATH ../../../Cargo.toml + # Equivalent to --all-features passed to cargo build +# [ALL_FEATURES] + # Equivalent to --no-default-features passed to cargo build +# [NO_DEFAULT_FEATURES] + # Disable linking of standard libraries (required for no_std crates). +# [NO_STD] + # Specify cargo build profile (e.g. release or a custom profile) +# [PROFILE ] + # Only import the specified crates from a workspace +# [CRATES ... ] + # Enable the specified features +# [FEATURES ... ] +) +target_link_libraries(${BINARY_NAME} PRIVATE librustdesk) + # Apply the standard set of build settings. This can be removed for applications # that need different build settings. apply_standard_settings(${BINARY_NAME}) From bd2250b6c97f87987d6c861b2030ba3d25e94a98 Mon Sep 17 00:00:00 2001 From: SoLongAndThanksForAllThePizza <103753680+SoLongAndThanksForAllThePizza@users.noreply.github.com> Date: Wed, 25 May 2022 20:50:32 +0800 Subject: [PATCH 0013/2015] fix unchanged mobile_ffi.rs --- build.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.rs b/build.rs index aaf0858c1..bad00457f 100644 --- a/build.rs +++ b/build.rs @@ -64,11 +64,11 @@ fn install_oboe() { fn gen_flutter_rust_bridge() { // Tell Cargo that if the given file changes, to rerun this build script. - println!("cargo:rerun-if-changed=src/mobile_ffi.rs"); + println!("cargo:rerun-if-changed=src/flutter_ffi.rs"); // settings for fbr_codegen let opts = lib_flutter_rust_bridge_codegen::Opts { // Path of input Rust code - rust_input: "src/mobile_ffi.rs".to_string(), + rust_input: "src/flutter_ffi.rs".to_string(), // Path of output generated Dart code dart_output: "flutter/lib/generated_bridge.dart".to_string(), // for other options lets use default From 537918674ead1eb11a1c3234ce2ddf6003b25135 Mon Sep 17 00:00:00 2001 From: SoLongAndThanksForAllThePizza <103753680+SoLongAndThanksForAllThePizza@users.noreply.github.com> Date: Wed, 25 May 2022 20:57:25 +0800 Subject: [PATCH 0014/2015] fix unchanged mobile --- src/client/file_trait.rs | 2 +- src/flutter_ffi.rs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/client/file_trait.rs b/src/client/file_trait.rs index be790b035..5dc4cd786 100644 --- a/src/client/file_trait.rs +++ b/src/client/file_trait.rs @@ -24,7 +24,7 @@ pub trait FileManager: Interface { #[cfg(any(target_os = "android", target_os = "ios"))] fn read_dir(&self,path: &str, include_hidden: bool) -> String { - use crate::mobile::make_fd_to_json; + use crate::flutter::make_fd_to_json; match fs::read_dir(&fs::get_path(path), include_hidden){ Ok(fd) => make_fd_to_json(fd), Err(_)=>"".into() diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index ec6ef9082..d2a0eddef 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1,6 +1,6 @@ use crate::client::file_trait::FileManager; -use crate::mobile::connection_manager::{self, get_clients_length, get_clients_state}; -use crate::mobile::{self, make_fd_to_json, Session}; +use crate::flutter::connection_manager::{self, get_clients_length, get_clients_state}; +use crate::flutter::{self, make_fd_to_json, Session}; use flutter_rust_bridge::{StreamSink, ZeroCopyBuffer}; use hbb_common::ResultType; use hbb_common::{ @@ -37,12 +37,12 @@ fn initialize(app_dir: &str) { } pub fn start_event_stream(s: StreamSink) -> ResultType<()> { - let _ = mobile::EVENT_STREAM.write().unwrap().insert(s); + let _ = flutter::EVENT_STREAM.write().unwrap().insert(s); Ok(()) } pub fn start_rgba_stream(s: StreamSink>>) -> ResultType<()> { - let _ = mobile::RGBA_STREAM.write().unwrap().insert(s); + let _ = flutter::RGBA_STREAM.write().unwrap().insert(s); Ok(()) } From f4c4b0d9f372ef5dab214b49e8c45eaf8c4d55a6 Mon Sep 17 00:00:00 2001 From: csf Date: Wed, 25 May 2022 23:09:14 +0800 Subject: [PATCH 0015/2015] public ui function --- src/flutter_ffi.rs | 9 +- src/ui.rs | 1014 ++++++++++++++++++++++++++++---------------- 2 files changed, 667 insertions(+), 356 deletions(-) diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index d2a0eddef..71cedb0a8 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1,6 +1,7 @@ use crate::client::file_trait::FileManager; use crate::flutter::connection_manager::{self, get_clients_length, get_clients_state}; use crate::flutter::{self, make_fd_to_json, Session}; +use crate::ui; use flutter_rust_bridge::{StreamSink, ZeroCopyBuffer}; use hbb_common::ResultType; use hbb_common::{ @@ -115,7 +116,7 @@ unsafe extern "C" fn get_by_name(name: *const c_char, arg: *const c_char) -> *co } } "server_id" => { - res = Config::get_id(); + res = ui::get_id(); } "server_password" => { res = Config::get_password(); @@ -296,7 +297,11 @@ unsafe extern "C" fn set_by_name(name: *const c_char, value: *const c_char) { if name == "custom-rendezvous-server" { #[cfg(target_os = "android")] crate::rendezvous_mediator::RendezvousMediator::restart(); - #[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))] + #[cfg(any( + target_os = "android", + target_os = "ios", + feature = "cli" + ))] crate::common::test_rendezvous_server(); } } diff --git a/src/ui.rs b/src/ui.rs index 5e133ea79..061651750 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -23,6 +23,7 @@ use std::{ iter::FromIterator, process::Child, sync::{Arc, Mutex}, + time::SystemTime, }; type Message = RendezvousMessage; @@ -33,9 +34,579 @@ type Status = (i32, bool, i64, String); lazy_static::lazy_static! { // stupid workaround for https://sciter.com/forums/topic/crash-on-latest-tis-mac-sdk-sometimes/ static ref STUPID_VALUES: Mutex>>> = Default::default(); + pub static ref UI_DATA: Mutex = Mutex::new(UIData::new(Childs::default())); } -struct UI( +pub fn recent_sessions_updated() -> bool { + let ui_data = UI_DATA.lock().unwrap(); + let mut lock = ui_data.0.lock().unwrap(); + if lock.0 { + lock.0 = false; + true + } else { + false + } +} + +pub fn get_id() -> String { + ipc::get_id() +} + +pub fn get_password() -> String { + ipc::get_password() +} + +pub fn update_password(password: String) { + if password.is_empty() { + allow_err!(ipc::set_password(Config::get_auto_password())); + } else { + allow_err!(ipc::set_password(password)); + } +} + +pub fn get_remote_id() -> String { + LocalConfig::get_remote_id() +} + +pub fn set_remote_id(id: String) { + LocalConfig::set_remote_id(&id); +} + +pub fn goto_install() { + allow_err!(crate::run_me(vec!["--install"])); +} + +pub fn install_me(_options: String, _path: String) { + #[cfg(windows)] + std::thread::spawn(move || { + allow_err!(crate::platform::windows::install_me(&_options, _path)); + std::process::exit(0); + }); +} + +pub fn update_me(_path: String) { + #[cfg(target_os = "linux")] + { + std::process::Command::new("pkexec") + .args(&["apt", "install", "-f", &_path]) + .spawn() + .ok(); + std::fs::remove_file(&_path).ok(); + crate::run_me(Vec::<&str>::new()).ok(); + } + #[cfg(windows)] + { + let mut path = _path; + if path.is_empty() { + if let Ok(tmp) = std::env::current_exe() { + path = tmp.to_string_lossy().to_string(); + } + } + std::process::Command::new(path) + .arg("--update") + .spawn() + .ok(); + std::process::exit(0); + } +} + +pub fn run_without_install() { + crate::run_me(vec!["--noinstall"]).ok(); + std::process::exit(0); +} + +pub fn show_run_without_install() -> bool { + let mut it = std::env::args(); + if let Some(tmp) = it.next() { + if crate::is_setup(&tmp) { + return it.next() == None; + } + } + false +} + +pub fn has_rendezvous_service() -> bool { + #[cfg(all(windows, feature = "hbbs"))] + return crate::platform::is_win_server() && crate::platform::windows::get_license().is_some(); + return false; +} + +pub fn get_license() -> String { + #[cfg(windows)] + if let Some(lic) = crate::platform::windows::get_license() { + return format!( + "
Key: {}
Host: {} Api: {}", + lic.key, lic.host, lic.api + ); + } + Default::default() +} + +pub fn get_option(key: &str) -> String { + let ui_data = UI_DATA.lock().unwrap(); + let map = ui_data.2.lock().unwrap(); + if let Some(v) = map.get(key) { + v.to_owned() + } else { + "".to_owned() + } +} + +pub fn get_local_option(key: String) -> String { + LocalConfig::get_option(&key) +} + +pub fn set_local_option(key: String, value: String) { + LocalConfig::set_option(key, value); +} + +pub fn peer_has_password(id: String) -> bool { + !PeerConfig::load(&id).password.is_empty() +} + +pub fn forget_password(id: String) { + let mut c = PeerConfig::load(&id); + c.password.clear(); + c.store(&id); +} + +pub fn get_peer_option(id: String, name: String) -> String { + let c = PeerConfig::load(&id); + c.options.get(&name).unwrap_or(&"".to_owned()).to_owned() +} + +pub fn set_peer_option(id: String, name: String, value: String) { + let mut c = PeerConfig::load(&id); + if value.is_empty() { + c.options.remove(&name); + } else { + c.options.insert(name, value); + } + c.store(&id); +} + +pub fn using_public_server() -> bool { + crate::get_custom_rendezvous_server(get_option("custom-rendezvous-server")).is_empty() +} + +pub fn get_options() -> HashMap { + let ui_data = UI_DATA.lock().unwrap(); + let mut m = HashMap::new(); + for (k, v) in ui_data.2.lock().unwrap().iter() { + m.insert(k.into(), v.into()); + } + m +} + +pub fn test_if_valid_server(host: String) -> String { + hbb_common::socket_client::test_if_valid_server(&host) +} + +pub fn get_sound_inputs() -> Vec { + let mut a = Vec::new(); + #[cfg(windows)] + { + // TODO TEST + fn get_sound_inputs_() -> Vec { + let mut out = Vec::new(); + use cpal::traits::{DeviceTrait, HostTrait}; + let host = cpal::default_host(); + if let Ok(devices) = host.devices() { + for device in devices { + if device.default_input_config().is_err() { + continue; + } + if let Ok(name) = device.name() { + out.push(name); + } + } + } + out + } + + let inputs = Arc::new(Mutex::new(Vec::new())); + let cloned = inputs.clone(); + // can not call below in UI thread, because conflict with sciter sound com initialization + std::thread::spawn(move || *cloned.lock().unwrap() = get_sound_inputs_()) + .join() + .ok(); + for name in inputs.lock().unwrap().drain(..) { + a.push(name); + } + } + #[cfg(not(windows))] + { + let inputs: Vec = crate::platform::linux::get_pa_sources() + .drain(..) + .map(|x| x.1) + .collect(); + + for name in inputs { + a.push(name); + } + } + a +} + +pub fn set_options(m: HashMap) { + let ui_data = UI_DATA.lock().unwrap(); + *ui_data.2.lock().unwrap() = m.clone(); + ipc::set_options(m).ok(); +} + +pub fn set_option(key: String, value: String) { + let ui_data = UI_DATA.lock().unwrap(); + #[cfg(target_os = "macos")] + if &key == "stop-service" { + let is_stop = value == "Y"; + if is_stop && crate::platform::macos::uninstall() { + return; + } + } + let mut options = ui_data.2.lock().unwrap(); + if value.is_empty() { + options.remove(&key); + } else { + options.insert(key.clone(), value.clone()); + } + ipc::set_options(options.clone()).ok(); +} + +pub fn install_path() -> String { + #[cfg(windows)] + return crate::platform::windows::get_install_info().1; + #[cfg(not(windows))] + return "".to_owned(); +} + +pub fn get_socks() -> Vec { + let s = ipc::get_socks(); + match s { + None => Vec::new(), + Some(s) => { + let mut v = Vec::new(); + v.push(s.proxy); + v.push(s.username); + v.push(s.password); + v + } + } +} + +pub fn set_socks(proxy: String, username: String, password: String) { + ipc::set_socks(config::Socks5Server { + proxy, + username, + password, + }) + .ok(); +} + +pub fn is_installed() -> bool { + crate::platform::is_installed() +} + +pub fn is_rdp_service_open() -> bool { + #[cfg(windows)] + return self.is_installed() && crate::platform::windows::is_rdp_service_open(); + #[cfg(not(windows))] + return false; +} + +pub fn is_share_rdp() -> bool { + #[cfg(windows)] + return crate::platform::windows::is_share_rdp(); + #[cfg(not(windows))] + return false; +} + +pub fn set_share_rdp(_enable: bool) { + #[cfg(windows)] + crate::platform::windows::set_share_rdp(_enable); +} + +pub fn is_installed_lower_version() -> bool { + #[cfg(not(windows))] + return false; + #[cfg(windows)] + { + let installed_version = crate::platform::windows::get_installed_version(); + let a = hbb_common::get_version_number(crate::VERSION); + let b = hbb_common::get_version_number(&installed_version); + return a > b; + } +} + +pub fn closing(x: i32, y: i32, w: i32, h: i32) { + crate::server::input_service::fix_key_down_timeout_at_exit(); + LocalConfig::set_size(x, y, w, h); +} + +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 +} + +pub fn get_mouse_time() -> f64 { + let ui_data = UI_DATA.lock().unwrap(); + let res = ui_data.1.lock().unwrap().2 as f64; + return res; +} + +pub fn check_mouse_time() { + let ui_data = UI_DATA.lock().unwrap(); + allow_err!(ui_data.4.send(ipc::Data::MouseMoveTime(0))); +} + +pub fn get_connect_status() -> Status { + let ui_data = UI_DATA.lock().unwrap(); + let res = ui_data.1.lock().unwrap().clone(); + res +} + +pub fn get_peer(id: String) -> PeerConfig { + PeerConfig::load(&id) +} + +pub fn get_fav() -> Vec { + LocalConfig::get_fav() +} + +pub fn store_fav(fav: Vec) { + LocalConfig::set_fav(fav); +} + +pub fn get_recent_sessions() -> Vec<(String, SystemTime, PeerConfig)> { + PeerConfig::peers() +} + +pub fn get_icon() -> String { + crate::get_icon() +} + +pub fn remove_peer(id: String) { + PeerConfig::remove(&id); +} + +pub fn new_remote(id: String, remote_type: String) { + let ui_data = UI_DATA.lock().unwrap(); + let mut lock = ui_data.0.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); + } + } +} + +pub fn is_process_trusted(_prompt: bool) -> bool { + #[cfg(target_os = "macos")] + return crate::platform::macos::is_process_trusted(_prompt); + #[cfg(not(target_os = "macos"))] + return true; +} + +pub fn is_can_screen_recording(_prompt: bool) -> bool { + #[cfg(target_os = "macos")] + return crate::platform::macos::is_can_screen_recording(_prompt); + #[cfg(not(target_os = "macos"))] + return true; +} + +pub fn is_installed_daemon(_prompt: bool) -> bool { + #[cfg(target_os = "macos")] + return crate::platform::macos::is_installed_daemon(_prompt); + #[cfg(not(target_os = "macos"))] + return true; +} + +pub fn get_error() -> String { + #[cfg(target_os = "linux")] + { + let dtype = crate::platform::linux::get_display_server(); + if "wayland" == dtype { + return "".to_owned(); + } + if dtype != "x11" { + return format!( + "{} {}, {}", + t("Unsupported display server ".to_owned()), + dtype, + t("x11 expected".to_owned()), + ); + } + } + return "".to_owned(); +} + +pub fn is_login_wayland() -> bool { + #[cfg(target_os = "linux")] + return crate::platform::linux::is_login_wayland(); + #[cfg(not(target_os = "linux"))] + return false; +} + +pub fn fix_login_wayland() { + #[cfg(target_os = "linux")] + crate::platform::linux::fix_login_wayland(); +} + +pub fn current_is_wayland() -> bool { + #[cfg(target_os = "linux")] + return crate::platform::linux::current_is_wayland(); + #[cfg(not(target_os = "linux"))] + return false; +} + +pub fn modify_default_login() -> String { + #[cfg(target_os = "linux")] + return crate::platform::linux::modify_default_login(); + #[cfg(not(target_os = "linux"))] + return "".to_owned(); +} + +pub fn get_software_update_url() -> String { + SOFTWARE_UPDATE_URL.lock().unwrap().clone() +} + +pub fn get_new_version() -> String { + hbb_common::get_version_from_url(&*SOFTWARE_UPDATE_URL.lock().unwrap()) +} + +pub fn get_version() -> String { + crate::VERSION.to_owned() +} + +pub fn get_app_name() -> String { + crate::get_app_name() +} + +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() +} + +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()) +} + +pub fn create_shortcut(_id: String) { + #[cfg(windows)] + crate::platform::windows::create_shortcut(&_id).ok(); +} + +pub fn discover() { + std::thread::spawn(move || { + allow_err!(crate::rendezvous_mediator::discover()); + }); +} + +pub fn get_lan_peers() -> String { + config::LanPeers::load().peers +} + +pub fn get_uuid() -> String { + base64::encode(crate::get_uuid()) +} + +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()); +} + +pub fn change_id(id: String) { + let ui_data = UI_DATA.lock().unwrap(); + let status = ui_data.3.clone(); + *status.lock().unwrap() = " ".to_owned(); + let old_id = get_id(); + std::thread::spawn(move || { + *status.lock().unwrap() = change_id_(id, old_id).to_owned(); + }); +} + +pub fn post_request(url: String, body: String, header: String) { + let ui_data = UI_DATA.lock().unwrap(); + let status = ui_data.3.clone(); + *status.lock().unwrap() = " ".to_owned(); + std::thread::spawn(move || { + *status.lock().unwrap() = match crate::post_request_sync(url, body, &header) { + Err(err) => err.to_string(), + Ok(text) => text, + }; + }); +} + +pub fn is_ok_change_id() -> bool { + machine_uid::get().is_ok() +} + +pub fn get_async_job_status() -> String { + let ui_data = UI_DATA.lock().unwrap(); + ui_data.3.clone().lock().unwrap().clone() +} + +pub fn t(name: String) -> String { + crate::client::translate(name) +} + +pub fn is_xfce() -> bool { + crate::platform::is_xfce() +} + +pub fn get_api_server() -> String { + crate::get_api_server( + get_option("api-server"), + get_option("custom-rendezvous-server"), + ) +} + +pub struct UIData( Childs, Arc>, Arc>>, @@ -43,6 +614,13 @@ struct UI( mpsc::UnboundedSender, ); +impl UIData { + fn new(childs: Childs) -> Self { + let res = check_connect_status(true); + Self(childs, res.0, res.1, Default::default(), res.2) + } +} + struct UIHostHandler; pub fn start(args: &mut [String]) { @@ -93,16 +671,15 @@ pub fn start(args: &mut [String]) { args[1] = id; } if args.is_empty() { - let childs: Childs = Default::default(); - let cloned = childs.clone(); + let ui_data = UI_DATA.lock().unwrap(); + let cloned = ui_data.0.clone(); std::thread::spawn(move || check_zombie(cloned)); crate::common::check_software_update(); - frame.event_handler(UI::new(childs)); + frame.event_handler(UI {}); frame.sciter_handler(UIHostHandler {}); page = "index.html"; } else if args[0] == "--install" { - let childs: Childs = Default::default(); - frame.event_handler(UI::new(childs)); + frame.event_handler(UI {}); frame.sciter_handler(UIHostHandler {}); page = "install.html"; } else if args[0] == "--cm" { @@ -158,197 +735,108 @@ pub fn start(args: &mut [String]) { frame.run_app(); } -impl UI { - fn new(childs: Childs) -> Self { - let res = check_connect_status(true); - Self(childs, res.0, res.1, Default::default(), res.2) - } +struct UI {} - fn recent_sessions_updated(&mut self) -> bool { - let mut lock = self.0.lock().unwrap(); - if lock.0 { - lock.0 = false; - true - } else { - false - } +impl UI { + fn recent_sessions_updated(&self) -> bool { + recent_sessions_updated() } fn get_id(&self) -> String { - ipc::get_id() + get_id() } fn get_password(&mut self) -> String { - ipc::get_password() + get_password() } fn update_password(&mut self, password: String) { - if password.is_empty() { - allow_err!(ipc::set_password(Config::get_auto_password())); - } else { - allow_err!(ipc::set_password(password)); - } + update_password(password) } fn get_remote_id(&mut self) -> String { - LocalConfig::get_remote_id() + get_remote_id() } fn set_remote_id(&mut self, id: String) { - LocalConfig::set_remote_id(&id); + set_remote_id(id); } fn goto_install(&mut self) { - allow_err!(crate::run_me(vec!["--install"])); + goto_install(); } fn install_me(&mut self, _options: String, _path: String) { - #[cfg(windows)] - std::thread::spawn(move || { - allow_err!(crate::platform::windows::install_me(&_options, _path)); - std::process::exit(0); - }); + install_me(_options, _path); } fn update_me(&self, _path: String) { - #[cfg(target_os = "linux")] - { - std::process::Command::new("pkexec") - .args(&["apt", "install", "-f", &_path]) - .spawn() - .ok(); - std::fs::remove_file(&_path).ok(); - crate::run_me(Vec::<&str>::new()).ok(); - } - #[cfg(windows)] - { - let mut path = _path; - if path.is_empty() { - if let Ok(tmp) = std::env::current_exe() { - path = tmp.to_string_lossy().to_string(); - } - } - std::process::Command::new(path) - .arg("--update") - .spawn() - .ok(); - std::process::exit(0); - } + update_me(_path); } fn run_without_install(&self) { - crate::run_me(vec!["--noinstall"]).ok(); - std::process::exit(0); + run_without_install(); } fn show_run_without_install(&self) -> bool { - let mut it = std::env::args(); - if let Some(tmp) = it.next() { - if crate::is_setup(&tmp) { - return it.next() == None; - } - } - false + show_run_without_install() } fn has_rendezvous_service(&self) -> bool { - #[cfg(all(windows, feature = "hbbs"))] - return crate::platform::is_win_server() - && crate::platform::windows::get_license().is_some(); - return false; + has_rendezvous_service() } fn get_license(&self) -> String { - #[cfg(windows)] - if let Some(lic) = crate::platform::windows::get_license() { - return format!( - "
Key: {}
Host: {} Api: {}", - lic.key, lic.host, lic.api - ); - } - Default::default() + get_license() } fn get_option(&self, key: String) -> String { - self.get_option_(&key) - } - - fn get_option_(&self, key: &str) -> String { - if let Some(v) = self.2.lock().unwrap().get(key) { - v.to_owned() - } else { - "".to_owned() - } + get_option(&key) } fn get_local_option(&self, key: String) -> String { - LocalConfig::get_option(&key) + get_local_option(key) } fn set_local_option(&self, key: String, value: String) { - LocalConfig::set_option(key, value); + set_local_option(key, value); } fn peer_has_password(&self, id: String) -> bool { - !PeerConfig::load(&id).password.is_empty() + peer_has_password(id) } fn forget_password(&self, id: String) { - let mut c = PeerConfig::load(&id); - c.password.clear(); - c.store(&id); + forget_password(id) } fn get_peer_option(&self, id: String, name: String) -> String { - let c = PeerConfig::load(&id); - c.options.get(&name).unwrap_or(&"".to_owned()).to_owned() + get_peer_option(id, name) } fn set_peer_option(&self, id: String, name: String, value: String) { - let mut c = PeerConfig::load(&id); - if value.is_empty() { - c.options.remove(&name); - } else { - c.options.insert(name, value); - } - c.store(&id); + set_peer_option(id, name, value) } fn using_public_server(&self) -> bool { - crate::get_custom_rendezvous_server(self.get_option_("custom-rendezvous-server")).is_empty() + using_public_server() } fn get_options(&self) -> Value { + let hashmap = get_options(); let mut m = Value::map(); - for (k, v) in self.2.lock().unwrap().iter() { + for (k, v) in hashmap { m.set_item(k, v); } m } fn test_if_valid_server(&self, host: String) -> String { - hbb_common::socket_client::test_if_valid_server(&host) + test_if_valid_server(host) } fn get_sound_inputs(&self) -> Value { - let mut a = Value::array(0); - #[cfg(windows)] - { - let inputs = Arc::new(Mutex::new(Vec::new())); - let cloned = inputs.clone(); - // can not call below in UI thread, because conflict with sciter sound com initialization - std::thread::spawn(move || *cloned.lock().unwrap() = get_sound_inputs()) - .join() - .ok(); - for name in inputs.lock().unwrap().drain(..) { - a.push(name); - } - } - #[cfg(not(windows))] - for name in get_sound_inputs() { - a.push(name); - } - a + Value::from_iter(get_sound_inputs()) } fn set_options(&self, v: Value) { @@ -362,119 +850,64 @@ impl UI { } } } - - *self.2.lock().unwrap() = m.clone(); - ipc::set_options(m).ok(); + set_options(m); } fn set_option(&self, key: String, value: String) { - #[cfg(target_os = "macos")] - if &key == "stop-service" { - let is_stop = value == "Y"; - if is_stop && crate::platform::macos::uninstall() { - return; - } - } - let mut options = self.2.lock().unwrap(); - if value.is_empty() { - options.remove(&key); - } else { - options.insert(key.clone(), value.clone()); - } - ipc::set_options(options.clone()).ok(); + set_option(key, value); } fn install_path(&mut self) -> String { - #[cfg(windows)] - return crate::platform::windows::get_install_info().1; - #[cfg(not(windows))] - return "".to_owned(); + install_path() } fn get_socks(&self) -> Value { - let s = ipc::get_socks(); - match s { - None => Value::null(), - Some(s) => { - let mut v = Value::array(0); - v.push(s.proxy); - v.push(s.username); - v.push(s.password); - v - } - } + Value::from_iter(get_socks()) } fn set_socks(&self, proxy: String, username: String, password: String) { - ipc::set_socks(config::Socks5Server { - proxy, - username, - password, - }) - .ok(); + set_socks(proxy, username, password) } fn is_installed(&self) -> bool { - crate::platform::is_installed() + is_installed() } fn is_rdp_service_open(&self) -> bool { - #[cfg(windows)] - return self.is_installed() && crate::platform::windows::is_rdp_service_open(); - #[cfg(not(windows))] - return false; + is_rdp_service_open() } fn is_share_rdp(&self) -> bool { - #[cfg(windows)] - return crate::platform::windows::is_share_rdp(); - #[cfg(not(windows))] - return false; + is_share_rdp() } fn set_share_rdp(&self, _enable: bool) { - #[cfg(windows)] - crate::platform::windows::set_share_rdp(_enable); + set_share_rdp(_enable); } fn is_installed_lower_version(&self) -> bool { - #[cfg(not(windows))] - return false; - #[cfg(windows)] - { - let installed_version = crate::platform::windows::get_installed_version(); - let a = hbb_common::get_version_number(crate::VERSION); - let b = hbb_common::get_version_number(&installed_version); - return a > b; - } + is_installed_lower_version() } fn closing(&mut self, x: i32, y: i32, w: i32, h: i32) { - crate::server::input_service::fix_key_down_timeout_at_exit(); - LocalConfig::set_size(x, y, w, h); + closing(x, y, w, h) } fn get_size(&mut self) -> Value { - let s = LocalConfig::get_size(); - let mut v = Value::array(0); - v.push(s.0); - v.push(s.1); - v.push(s.2); - v.push(s.3); - v + Value::from_iter(get_size()) } fn get_mouse_time(&self) -> f64 { - self.1.lock().unwrap().2 as _ + get_mouse_time() } fn check_mouse_time(&self) { - allow_err!(self.4.send(ipc::Data::MouseMoveTime(0))); + check_mouse_time() } fn get_connect_status(&mut self) -> Value { let mut v = Value::array(0); - let x = self.1.lock().unwrap().clone(); + let x = get_connect_status(); v.push(x.0); v.push(x.1); v.push(x.3); @@ -494,12 +927,12 @@ impl UI { } fn get_peer(&self, id: String) -> Value { - let c = PeerConfig::load(&id); + let c = get_peer(id.clone()); Self::get_peer_value(id, c) } fn get_fav(&self) -> Value { - Value::from_iter(LocalConfig::get_fav()) + Value::from_iter(get_fav()) } fn store_fav(&self, fav: Value) { @@ -511,12 +944,12 @@ impl UI { } } }); - LocalConfig::set_fav(tmp); + store_fav(tmp); } fn get_recent_sessions(&mut self) -> Value { // to-do: limit number of recent sessions, and remove old peer file - let peers: Vec = PeerConfig::peers() + let peers: Vec = get_recent_sessions() .drain(..) .map(|p| Self::get_peer_value(p.0, p.2)) .collect(); @@ -524,220 +957,119 @@ impl UI { } fn get_icon(&mut self) -> String { - crate::get_icon() + get_icon() } fn remove_peer(&mut self, id: String) { - PeerConfig::remove(&id); + remove_peer(id) } fn new_remote(&mut self, id: String, remote_type: String) { - let mut lock = self.0.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); - } - } + new_remote(id, remote_type) } fn is_process_trusted(&mut self, _prompt: bool) -> bool { - #[cfg(target_os = "macos")] - return crate::platform::macos::is_process_trusted(_prompt); - #[cfg(not(target_os = "macos"))] - return true; + is_process_trusted(_prompt) } fn is_can_screen_recording(&mut self, _prompt: bool) -> bool { - #[cfg(target_os = "macos")] - return crate::platform::macos::is_can_screen_recording(_prompt); - #[cfg(not(target_os = "macos"))] - return true; + is_can_screen_recording(_prompt) } fn is_installed_daemon(&mut self, _prompt: bool) -> bool { - #[cfg(target_os = "macos")] - return crate::platform::macos::is_installed_daemon(_prompt); - #[cfg(not(target_os = "macos"))] - return true; + is_installed_daemon(_prompt) } fn get_error(&mut self) -> String { - #[cfg(target_os = "linux")] - { - let dtype = crate::platform::linux::get_display_server(); - if "wayland" == dtype { - return "".to_owned(); - } - if dtype != "x11" { - return format!( - "{} {}, {}", - self.t("Unsupported display server ".to_owned()), - dtype, - self.t("x11 expected".to_owned()), - ); - } - } - return "".to_owned(); + get_error() } fn is_login_wayland(&mut self) -> bool { - #[cfg(target_os = "linux")] - return crate::platform::linux::is_login_wayland(); - #[cfg(not(target_os = "linux"))] - return false; + is_login_wayland() } fn fix_login_wayland(&mut self) { - #[cfg(target_os = "linux")] - crate::platform::linux::fix_login_wayland(); + fix_login_wayland() } fn current_is_wayland(&mut self) -> bool { - #[cfg(target_os = "linux")] - return crate::platform::linux::current_is_wayland(); - #[cfg(not(target_os = "linux"))] - return false; + current_is_wayland() } fn modify_default_login(&mut self) -> String { - #[cfg(target_os = "linux")] - return crate::platform::linux::modify_default_login(); - #[cfg(not(target_os = "linux"))] - return "".to_owned(); + modify_default_login() } fn get_software_update_url(&self) -> String { - SOFTWARE_UPDATE_URL.lock().unwrap().clone() + get_software_update_url() } fn get_new_version(&self) -> String { - hbb_common::get_version_from_url(&*SOFTWARE_UPDATE_URL.lock().unwrap()) + get_new_version() } fn get_version(&self) -> String { - crate::VERSION.to_owned() + get_version() } fn get_app_name(&self) -> String { - crate::get_app_name() + get_app_name() } fn get_software_ext(&self) -> String { - #[cfg(windows)] - let p = "exe"; - #[cfg(target_os = "macos")] - let p = "dmg"; - #[cfg(target_os = "linux")] - let p = "deb"; - p.to_owned() + get_software_ext() } fn get_software_store_path(&self) -> 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(), self.get_software_ext()) + get_software_store_path() } fn create_shortcut(&self, _id: String) { - #[cfg(windows)] - crate::platform::windows::create_shortcut(&_id).ok(); + create_shortcut(_id) } fn discover(&self) { - std::thread::spawn(move || { - allow_err!(crate::rendezvous_mediator::discover()); - }); + discover() } fn get_lan_peers(&self) -> String { - config::LanPeers::load().peers + get_lan_peers() } fn get_uuid(&self) -> String { - base64::encode(crate::get_uuid()) + get_uuid() } fn open_url(&self, 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()); + open_url(url) } fn change_id(&self, id: String) { - let status = self.3.clone(); - *status.lock().unwrap() = " ".to_owned(); - let old_id = self.get_id(); - std::thread::spawn(move || { - *status.lock().unwrap() = change_id(id, old_id).to_owned(); - }); + change_id(id) } fn post_request(&self, url: String, body: String, header: String) { - let status = self.3.clone(); - *status.lock().unwrap() = " ".to_owned(); - std::thread::spawn(move || { - *status.lock().unwrap() = match crate::post_request_sync(url, body, &header) { - Err(err) => err.to_string(), - Ok(text) => text, - }; - }); + post_request(url, body, header) } fn is_ok_change_id(&self) -> bool { - machine_uid::get().is_ok() + is_ok_change_id() } fn get_async_job_status(&self) -> String { - self.3.clone().lock().unwrap().clone() + get_async_job_status() } fn t(&self, name: String) -> String { - crate::client::translate(name) + t(name) } fn is_xfce(&self) -> bool { - crate::platform::is_xfce() + is_xfce() } fn get_api_server(&self) -> String { - crate::get_api_server( - self.get_option_("api-server"), - self.get_option_("custom-rendezvous-server"), - ) + get_api_server() } } @@ -915,32 +1247,6 @@ async fn check_connect_status_( } } -#[cfg(not(target_os = "linux"))] -fn get_sound_inputs() -> Vec { - let mut out = Vec::new(); - use cpal::traits::{DeviceTrait, HostTrait}; - let host = cpal::default_host(); - if let Ok(devices) = host.devices() { - for device in devices { - if device.default_input_config().is_err() { - continue; - } - if let Ok(name) = device.name() { - out.push(name); - } - } - } - out -} - -#[cfg(target_os = "linux")] -fn get_sound_inputs() -> Vec { - crate::platform::linux::get_pa_sources() - .drain(..) - .map(|x| x.1) - .collect() -} - fn check_connect_status( reconnect: bool, ) -> ( @@ -961,7 +1267,7 @@ const INVALID_FORMAT: &'static str = "Invalid format"; const UNKNOWN_ERROR: &'static str = "Unknown error"; #[tokio::main(flavor = "current_thread")] -async fn change_id(id: String, old_id: String) -> &'static str { +async fn change_id_(id: String, old_id: String) -> &'static str { if !hbb_common::is_valid_custom_id(&id) { return INVALID_FORMAT; } From 8edc0d4c76260757c10aa30f1a599a2b4fe5804e Mon Sep 17 00:00:00 2001 From: csf Date: Wed, 25 May 2022 23:22:14 +0800 Subject: [PATCH 0016/2015] fix ref fun --- src/ui.rs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/ui.rs b/src/ui.rs index 061651750..a8357312f 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -142,7 +142,11 @@ pub fn get_license() -> String { Default::default() } -pub fn get_option(key: &str) -> String { +pub fn get_option(key: String) -> String { + get_option_(&key) +} + +fn get_option_(key: &str) -> String { let ui_data = UI_DATA.lock().unwrap(); let map = ui_data.2.lock().unwrap(); if let Some(v) = map.get(key) { @@ -186,7 +190,7 @@ pub fn set_peer_option(id: String, name: String, value: String) { } pub fn using_public_server() -> bool { - crate::get_custom_rendezvous_server(get_option("custom-rendezvous-server")).is_empty() + crate::get_custom_rendezvous_server(get_option_("custom-rendezvous-server")).is_empty() } pub fn get_options() -> HashMap { @@ -601,8 +605,8 @@ pub fn is_xfce() -> bool { pub fn get_api_server() -> String { crate::get_api_server( - get_option("api-server"), - get_option("custom-rendezvous-server"), + get_option_("api-server"), + get_option_("custom-rendezvous-server"), ) } @@ -791,7 +795,7 @@ impl UI { } fn get_option(&self, key: String) -> String { - get_option(&key) + get_option(key) } fn get_local_option(&self, key: String) -> String { From 79553816555c8561dde3f6466682d0620ca71efe Mon Sep 17 00:00:00 2001 From: csf Date: Thu, 26 May 2022 11:54:29 +0800 Subject: [PATCH 0017/2015] refactor ui struct -> global ref (linux) --- src/ui.rs | 134 +++++++++++++++++++++++++----------------------------- 1 file changed, 63 insertions(+), 71 deletions(-) diff --git a/src/ui.rs b/src/ui.rs index a8357312f..a8ce30af4 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -29,19 +29,30 @@ use std::{ type Message = RendezvousMessage; pub type Childs = Arc)>>; -type Status = (i32, bool, i64, String); +type Status = (i32, bool, i64, String); // (status_num, key_confirmed, mouse_time, id) lazy_static::lazy_static! { // stupid workaround for https://sciter.com/forums/topic/crash-on-latest-tis-mac-sdk-sometimes/ static ref STUPID_VALUES: Mutex>>> = Default::default(); - pub static ref UI_DATA: Mutex = Mutex::new(UIData::new(Childs::default())); + pub static ref CHILDS : Childs = Default::default(); + pub static ref UI_STATUS : Arc> = Arc::new(Mutex::new((0, false, 0, "".to_owned()))); + pub static ref OPTIONS : Arc>> = Arc::new(Mutex::new(Config::get_options())); + pub static ref ASYNC_JOB_STATUS : Arc> = Default::default(); + pub static ref SENDER : Mutex> = Mutex::new(check_connect_status(true)); } +// struct UI( +// Childs, +// Arc>, +// Arc>>, options +// Arc>, async_job_status +// mpsc::UnboundedSender, Sender +// ); + pub fn recent_sessions_updated() -> bool { - let ui_data = UI_DATA.lock().unwrap(); - let mut lock = ui_data.0.lock().unwrap(); - if lock.0 { - lock.0 = false; + let mut childs = CHILDS.lock().unwrap(); + if childs.0 { + childs.0 = false; true } else { false @@ -147,8 +158,7 @@ pub fn get_option(key: String) -> String { } fn get_option_(key: &str) -> String { - let ui_data = UI_DATA.lock().unwrap(); - let map = ui_data.2.lock().unwrap(); + let map = OPTIONS.lock().unwrap(); if let Some(v) = map.get(key) { v.to_owned() } else { @@ -194,9 +204,10 @@ pub fn using_public_server() -> bool { } pub fn get_options() -> HashMap { - let ui_data = UI_DATA.lock().unwrap(); + // TODO Vec<(String,String)> + let options = OPTIONS.lock().unwrap(); let mut m = HashMap::new(); - for (k, v) in ui_data.2.lock().unwrap().iter() { + for (k, v) in options.iter() { m.insert(k.into(), v.into()); } m @@ -253,13 +264,12 @@ pub fn get_sound_inputs() -> Vec { } pub fn set_options(m: HashMap) { - let ui_data = UI_DATA.lock().unwrap(); - *ui_data.2.lock().unwrap() = m.clone(); + *OPTIONS.lock().unwrap() = m.clone(); ipc::set_options(m).ok(); } pub fn set_option(key: String, value: String) { - let ui_data = UI_DATA.lock().unwrap(); + let mut options = OPTIONS.lock().unwrap(); #[cfg(target_os = "macos")] if &key == "stop-service" { let is_stop = value == "Y"; @@ -267,7 +277,6 @@ pub fn set_option(key: String, value: String) { return; } } - let mut options = ui_data.2.lock().unwrap(); if value.is_empty() { options.remove(&key); } else { @@ -357,19 +366,19 @@ pub fn get_size() -> Vec { } pub fn get_mouse_time() -> f64 { - let ui_data = UI_DATA.lock().unwrap(); - let res = ui_data.1.lock().unwrap().2 as f64; + let ui_status = UI_STATUS.lock().unwrap(); + let res = ui_status.2 as f64; return res; } pub fn check_mouse_time() { - let ui_data = UI_DATA.lock().unwrap(); - allow_err!(ui_data.4.send(ipc::Data::MouseMoveTime(0))); + let sender = SENDER.lock().unwrap(); + allow_err!(sender.send(ipc::Data::MouseMoveTime(0))); } pub fn get_connect_status() -> Status { - let ui_data = UI_DATA.lock().unwrap(); - let res = ui_data.1.lock().unwrap().clone(); + let ui_statue = UI_STATUS.lock().unwrap(); + let res = ui_statue.clone(); res } @@ -398,8 +407,7 @@ pub fn remove_peer(id: String) { } pub fn new_remote(id: String, remote_type: String) { - let ui_data = UI_DATA.lock().unwrap(); - let mut lock = ui_data.0.lock().unwrap(); + let mut lock = CHILDS.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) { @@ -565,21 +573,17 @@ pub fn open_url(url: String) { } pub fn change_id(id: String) { - let ui_data = UI_DATA.lock().unwrap(); - let status = ui_data.3.clone(); - *status.lock().unwrap() = " ".to_owned(); + *ASYNC_JOB_STATUS.lock().unwrap() = " ".to_owned(); let old_id = get_id(); std::thread::spawn(move || { - *status.lock().unwrap() = change_id_(id, old_id).to_owned(); + *ASYNC_JOB_STATUS.lock().unwrap() = change_id_(id, old_id).to_owned(); }); } pub fn post_request(url: String, body: String, header: String) { - let ui_data = UI_DATA.lock().unwrap(); - let status = ui_data.3.clone(); - *status.lock().unwrap() = " ".to_owned(); + *ASYNC_JOB_STATUS.lock().unwrap() = " ".to_owned(); std::thread::spawn(move || { - *status.lock().unwrap() = match crate::post_request_sync(url, body, &header) { + *ASYNC_JOB_STATUS.lock().unwrap() = match crate::post_request_sync(url, body, &header) { Err(err) => err.to_string(), Ok(text) => text, }; @@ -591,8 +595,7 @@ pub fn is_ok_change_id() -> bool { } pub fn get_async_job_status() -> String { - let ui_data = UI_DATA.lock().unwrap(); - ui_data.3.clone().lock().unwrap().clone() + ASYNC_JOB_STATUS.lock().unwrap().clone() } pub fn t(name: String) -> String { @@ -610,20 +613,25 @@ pub fn get_api_server() -> String { ) } -pub struct UIData( - Childs, - Arc>, - Arc>>, - Arc>, - mpsc::UnboundedSender, -); +// pub struct UIData( +// Status, // 1 +// HashMap, // 2 options +// String, // 3 +// mpsc::UnboundedSender, // 4 +// ); +// pub struct UIData { +// status: Status, // 1 arc +// options: HashMap, // 2 arc options +// _3: String, // 3 arc async_job_status +// _4: mpsc::UnboundedSender, // 4 +// } -impl UIData { - fn new(childs: Childs) -> Self { - let res = check_connect_status(true); - Self(childs, res.0, res.1, Default::default(), res.2) - } -} +// impl UIData { +// fn new(childs: Childs) -> Self { +// let res = check_connect_status(true); +// Self(childs, res.0, res.1, Default::default(), res.2) +// } +// } struct UIHostHandler; @@ -675,8 +683,7 @@ pub fn start(args: &mut [String]) { args[1] = id; } if args.is_empty() { - let ui_data = UI_DATA.lock().unwrap(); - let cloned = ui_data.0.clone(); + let cloned = CHILDS.clone(); std::thread::spawn(move || check_zombie(cloned)); crate::common::check_software_update(); frame.event_handler(UI {}); @@ -1185,12 +1192,7 @@ pub fn check_zombie(childs: Childs) { // notice: avoiding create ipc connecton repeatly, // because windows named pipe has serious memory leak issue. #[tokio::main(flavor = "current_thread")] -async fn check_connect_status_( - reconnect: bool, - status: Arc>, - options: Arc>>, - rx: mpsc::UnboundedReceiver, -) { +async fn check_connect_status_(reconnect: bool, rx: mpsc::UnboundedReceiver) { let mut key_confirmed = false; let mut rx = rx; let mut mouse_time = 0; @@ -1208,10 +1210,10 @@ async fn check_connect_status_( } Ok(Some(ipc::Data::MouseMoveTime(v))) => { mouse_time = v; - status.lock().unwrap().2 = v; + UI_STATUS.lock().unwrap().2 = v; } Ok(Some(ipc::Data::Options(Some(v)))) => { - *options.lock().unwrap() = v + *OPTIONS.lock().unwrap() = v } Ok(Some(ipc::Data::Config((name, Some(value))))) => { if name == "id" { @@ -1223,7 +1225,7 @@ async fn check_connect_status_( x = 1 } key_confirmed = c; - *status.lock().unwrap() = (x as _, key_confirmed, mouse_time, id.clone()); + *UI_STATUS.lock().unwrap() = (x as _, key_confirmed, mouse_time, id.clone()); } _ => {} } @@ -1240,31 +1242,21 @@ async fn check_connect_status_( } } if !reconnect { - options + OPTIONS .lock() .unwrap() .insert("ipc-closed".to_owned(), "Y".to_owned()); break; } - *status.lock().unwrap() = (-1, key_confirmed, mouse_time, id.clone()); + *UI_STATUS.lock().unwrap() = (-1, key_confirmed, mouse_time, id.clone()); sleep(1.).await; } } -fn check_connect_status( - reconnect: bool, -) -> ( - Arc>, - Arc>>, - mpsc::UnboundedSender, -) { - let status = Arc::new(Mutex::new((0, false, 0, "".to_owned()))); - let options = Arc::new(Mutex::new(Config::get_options())); - let cloned = status.clone(); - let cloned_options = options.clone(); +fn check_connect_status(reconnect: bool) -> mpsc::UnboundedSender { let (tx, rx) = mpsc::unbounded_channel::(); - std::thread::spawn(move || check_connect_status_(reconnect, cloned, cloned_options, rx)); - (status, options, tx) + std::thread::spawn(move || check_connect_status_(reconnect, rx)); + tx } const INVALID_FORMAT: &'static str = "Invalid format"; From 35e17f0ef9efcfc94e5f0b6362b88ce04fc1ec91 Mon Sep 17 00:00:00 2001 From: csf Date: Wed, 25 May 2022 21:07:24 -0700 Subject: [PATCH 0018/2015] fix windows --- src/ui.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ui.rs b/src/ui.rs index a8ce30af4..a884455cd 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -321,7 +321,7 @@ pub fn is_installed() -> bool { pub fn is_rdp_service_open() -> bool { #[cfg(windows)] - return self.is_installed() && crate::platform::windows::is_rdp_service_open(); + return is_installed() && crate::platform::windows::is_rdp_service_open(); #[cfg(not(windows))] return false; } @@ -657,7 +657,7 @@ pub fn start(args: &mut [String]) { } #[cfg(windows)] if args.len() > 0 && args[0] == "--tray" { - let options = check_connect_status(false).1; + let options = OPTIONS.clone(); crate::tray::start_tray(options); return; } From 9aa3f5c51986e510b603bbe05dd406c6c6076934 Mon Sep 17 00:00:00 2001 From: csf Date: Thu, 26 May 2022 12:19:11 +0800 Subject: [PATCH 0019/2015] del unused --- src/ui.rs | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/src/ui.rs b/src/ui.rs index a884455cd..c734d7555 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -613,26 +613,6 @@ pub fn get_api_server() -> String { ) } -// pub struct UIData( -// Status, // 1 -// HashMap, // 2 options -// String, // 3 -// mpsc::UnboundedSender, // 4 -// ); -// pub struct UIData { -// status: Status, // 1 arc -// options: HashMap, // 2 arc options -// _3: String, // 3 arc async_job_status -// _4: mpsc::UnboundedSender, // 4 -// } - -// impl UIData { -// fn new(childs: Childs) -> Self { -// let res = check_connect_status(true); -// Self(childs, res.0, res.1, Default::default(), res.2) -// } -// } - struct UIHostHandler; pub fn start(args: &mut [String]) { From 699907eebdad8ec6181b03f6c254a1d96bc76eaf Mon Sep 17 00:00:00 2001 From: csf Date: Thu, 26 May 2022 18:11:00 +0800 Subject: [PATCH 0020/2015] fix build & create ui interface --- src/flutter_ffi.rs | 4 +- src/lib.rs | 2 + src/server/connection.rs | 2 +- src/ui.rs | 786 +------------------------------------- src/ui_interface.rs | 795 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 802 insertions(+), 787 deletions(-) create mode 100644 src/ui_interface.rs diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 71cedb0a8..2e62bdf69 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1,7 +1,7 @@ use crate::client::file_trait::FileManager; use crate::flutter::connection_manager::{self, get_clients_length, get_clients_state}; use crate::flutter::{self, make_fd_to_json, Session}; -use crate::ui; +use crate::ui_interface; use flutter_rust_bridge::{StreamSink, ZeroCopyBuffer}; use hbb_common::ResultType; use hbb_common::{ @@ -116,7 +116,7 @@ unsafe extern "C" fn get_by_name(name: *const c_char, arg: *const c_char) -> *co } } "server_id" => { - res = ui::get_id(); + res = ui_interface::get_id(); } "server_password" => { res = Config::get_password(); diff --git a/src/lib.rs b/src/lib.rs index 84d9af8e1..7452bdb42 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -40,5 +40,7 @@ mod port_forward; #[cfg(windows)] mod tray; +mod ui_interface; + #[cfg(windows)] pub mod clipboard_file; diff --git a/src/server/connection.rs b/src/server/connection.rs index 3a026d924..c6313d16a 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -4,7 +4,7 @@ use crate::clipboard_file::*; #[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::common::update_clipboard; #[cfg(any(target_os = "android", target_os = "ios"))] -use crate::{common::MOBILE_INFO2, mobile::connection_manager::start_channel}; +use crate::{common::MOBILE_INFO2, flutter::connection_manager::start_channel}; use crate::{ipc, VERSION}; use hbb_common::fs::can_enable_overwrite_detection; use hbb_common::{ diff --git a/src/ui.rs b/src/ui.rs index c734d7555..92af4328a 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -4,613 +4,18 @@ mod inline; #[cfg(target_os = "macos")] mod macos; pub mod remote; -use crate::common::SOFTWARE_UPDATE_URL; -use crate::ipc; -use hbb_common::{ - allow_err, - config::{self, Config, LocalConfig, PeerConfig, RENDEZVOUS_PORT, RENDEZVOUS_TIMEOUT}, - futures::future::join_all, - log, - protobuf::Message as _, - rendezvous_proto::*, - sleep, - tcp::FramedStream, - tokio::{self, sync::mpsc, time}, -}; +use crate::ui_interface::*; +use hbb_common::{allow_err, config::PeerConfig, log}; use sciter::Value; use std::{ collections::HashMap, iter::FromIterator, - process::Child, sync::{Arc, Mutex}, - time::SystemTime, }; -type Message = RendezvousMessage; - -pub type Childs = Arc)>>; -type Status = (i32, bool, i64, String); // (status_num, key_confirmed, mouse_time, id) - lazy_static::lazy_static! { // stupid workaround for https://sciter.com/forums/topic/crash-on-latest-tis-mac-sdk-sometimes/ static ref STUPID_VALUES: Mutex>>> = Default::default(); - pub static ref CHILDS : Childs = Default::default(); - pub static ref UI_STATUS : Arc> = Arc::new(Mutex::new((0, false, 0, "".to_owned()))); - pub static ref OPTIONS : Arc>> = Arc::new(Mutex::new(Config::get_options())); - pub static ref ASYNC_JOB_STATUS : Arc> = Default::default(); - pub static ref SENDER : Mutex> = Mutex::new(check_connect_status(true)); -} - -// struct UI( -// Childs, -// Arc>, -// Arc>>, options -// Arc>, async_job_status -// mpsc::UnboundedSender, Sender -// ); - -pub fn recent_sessions_updated() -> bool { - let mut childs = CHILDS.lock().unwrap(); - if childs.0 { - childs.0 = false; - true - } else { - false - } -} - -pub fn get_id() -> String { - ipc::get_id() -} - -pub fn get_password() -> String { - ipc::get_password() -} - -pub fn update_password(password: String) { - if password.is_empty() { - allow_err!(ipc::set_password(Config::get_auto_password())); - } else { - allow_err!(ipc::set_password(password)); - } -} - -pub fn get_remote_id() -> String { - LocalConfig::get_remote_id() -} - -pub fn set_remote_id(id: String) { - LocalConfig::set_remote_id(&id); -} - -pub fn goto_install() { - allow_err!(crate::run_me(vec!["--install"])); -} - -pub fn install_me(_options: String, _path: String) { - #[cfg(windows)] - std::thread::spawn(move || { - allow_err!(crate::platform::windows::install_me(&_options, _path)); - std::process::exit(0); - }); -} - -pub fn update_me(_path: String) { - #[cfg(target_os = "linux")] - { - std::process::Command::new("pkexec") - .args(&["apt", "install", "-f", &_path]) - .spawn() - .ok(); - std::fs::remove_file(&_path).ok(); - crate::run_me(Vec::<&str>::new()).ok(); - } - #[cfg(windows)] - { - let mut path = _path; - if path.is_empty() { - if let Ok(tmp) = std::env::current_exe() { - path = tmp.to_string_lossy().to_string(); - } - } - std::process::Command::new(path) - .arg("--update") - .spawn() - .ok(); - std::process::exit(0); - } -} - -pub fn run_without_install() { - crate::run_me(vec!["--noinstall"]).ok(); - std::process::exit(0); -} - -pub fn show_run_without_install() -> bool { - let mut it = std::env::args(); - if let Some(tmp) = it.next() { - if crate::is_setup(&tmp) { - return it.next() == None; - } - } - false -} - -pub fn has_rendezvous_service() -> bool { - #[cfg(all(windows, feature = "hbbs"))] - return crate::platform::is_win_server() && crate::platform::windows::get_license().is_some(); - return false; -} - -pub fn get_license() -> String { - #[cfg(windows)] - if let Some(lic) = crate::platform::windows::get_license() { - return format!( - "
Key: {}
Host: {} Api: {}", - lic.key, lic.host, lic.api - ); - } - Default::default() -} - -pub fn get_option(key: String) -> String { - get_option_(&key) -} - -fn get_option_(key: &str) -> String { - let map = OPTIONS.lock().unwrap(); - if let Some(v) = map.get(key) { - v.to_owned() - } else { - "".to_owned() - } -} - -pub fn get_local_option(key: String) -> String { - LocalConfig::get_option(&key) -} - -pub fn set_local_option(key: String, value: String) { - LocalConfig::set_option(key, value); -} - -pub fn peer_has_password(id: String) -> bool { - !PeerConfig::load(&id).password.is_empty() -} - -pub fn forget_password(id: String) { - let mut c = PeerConfig::load(&id); - c.password.clear(); - c.store(&id); -} - -pub fn get_peer_option(id: String, name: String) -> String { - let c = PeerConfig::load(&id); - c.options.get(&name).unwrap_or(&"".to_owned()).to_owned() -} - -pub fn set_peer_option(id: String, name: String, value: String) { - let mut c = PeerConfig::load(&id); - if value.is_empty() { - c.options.remove(&name); - } else { - c.options.insert(name, value); - } - c.store(&id); -} - -pub fn using_public_server() -> bool { - crate::get_custom_rendezvous_server(get_option_("custom-rendezvous-server")).is_empty() -} - -pub fn get_options() -> HashMap { - // TODO Vec<(String,String)> - let options = OPTIONS.lock().unwrap(); - let mut m = HashMap::new(); - for (k, v) in options.iter() { - m.insert(k.into(), v.into()); - } - m -} - -pub fn test_if_valid_server(host: String) -> String { - hbb_common::socket_client::test_if_valid_server(&host) -} - -pub fn get_sound_inputs() -> Vec { - let mut a = Vec::new(); - #[cfg(windows)] - { - // TODO TEST - fn get_sound_inputs_() -> Vec { - let mut out = Vec::new(); - use cpal::traits::{DeviceTrait, HostTrait}; - let host = cpal::default_host(); - if let Ok(devices) = host.devices() { - for device in devices { - if device.default_input_config().is_err() { - continue; - } - if let Ok(name) = device.name() { - out.push(name); - } - } - } - out - } - - let inputs = Arc::new(Mutex::new(Vec::new())); - let cloned = inputs.clone(); - // can not call below in UI thread, because conflict with sciter sound com initialization - std::thread::spawn(move || *cloned.lock().unwrap() = get_sound_inputs_()) - .join() - .ok(); - for name in inputs.lock().unwrap().drain(..) { - a.push(name); - } - } - #[cfg(not(windows))] - { - let inputs: Vec = crate::platform::linux::get_pa_sources() - .drain(..) - .map(|x| x.1) - .collect(); - - for name in inputs { - a.push(name); - } - } - a -} - -pub fn set_options(m: HashMap) { - *OPTIONS.lock().unwrap() = m.clone(); - ipc::set_options(m).ok(); -} - -pub fn set_option(key: String, value: String) { - let mut options = OPTIONS.lock().unwrap(); - #[cfg(target_os = "macos")] - if &key == "stop-service" { - let is_stop = value == "Y"; - if is_stop && crate::platform::macos::uninstall() { - return; - } - } - if value.is_empty() { - options.remove(&key); - } else { - options.insert(key.clone(), value.clone()); - } - ipc::set_options(options.clone()).ok(); -} - -pub fn install_path() -> String { - #[cfg(windows)] - return crate::platform::windows::get_install_info().1; - #[cfg(not(windows))] - return "".to_owned(); -} - -pub fn get_socks() -> Vec { - let s = ipc::get_socks(); - match s { - None => Vec::new(), - Some(s) => { - let mut v = Vec::new(); - v.push(s.proxy); - v.push(s.username); - v.push(s.password); - v - } - } -} - -pub fn set_socks(proxy: String, username: String, password: String) { - ipc::set_socks(config::Socks5Server { - proxy, - username, - password, - }) - .ok(); -} - -pub fn is_installed() -> bool { - crate::platform::is_installed() -} - -pub fn is_rdp_service_open() -> bool { - #[cfg(windows)] - return is_installed() && crate::platform::windows::is_rdp_service_open(); - #[cfg(not(windows))] - return false; -} - -pub fn is_share_rdp() -> bool { - #[cfg(windows)] - return crate::platform::windows::is_share_rdp(); - #[cfg(not(windows))] - return false; -} - -pub fn set_share_rdp(_enable: bool) { - #[cfg(windows)] - crate::platform::windows::set_share_rdp(_enable); -} - -pub fn is_installed_lower_version() -> bool { - #[cfg(not(windows))] - return false; - #[cfg(windows)] - { - let installed_version = crate::platform::windows::get_installed_version(); - let a = hbb_common::get_version_number(crate::VERSION); - let b = hbb_common::get_version_number(&installed_version); - return a > b; - } -} - -pub fn closing(x: i32, y: i32, w: i32, h: i32) { - crate::server::input_service::fix_key_down_timeout_at_exit(); - LocalConfig::set_size(x, y, w, h); -} - -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 -} - -pub fn get_mouse_time() -> f64 { - let ui_status = UI_STATUS.lock().unwrap(); - let res = ui_status.2 as f64; - return res; -} - -pub fn check_mouse_time() { - let sender = SENDER.lock().unwrap(); - allow_err!(sender.send(ipc::Data::MouseMoveTime(0))); -} - -pub fn get_connect_status() -> Status { - let ui_statue = UI_STATUS.lock().unwrap(); - let res = ui_statue.clone(); - res -} - -pub fn get_peer(id: String) -> PeerConfig { - PeerConfig::load(&id) -} - -pub fn get_fav() -> Vec { - LocalConfig::get_fav() -} - -pub fn store_fav(fav: Vec) { - LocalConfig::set_fav(fav); -} - -pub fn get_recent_sessions() -> Vec<(String, SystemTime, PeerConfig)> { - PeerConfig::peers() -} - -pub fn get_icon() -> String { - crate::get_icon() -} - -pub fn remove_peer(id: String) { - PeerConfig::remove(&id); -} - -pub fn new_remote(id: String, remote_type: String) { - let mut lock = CHILDS.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); - } - } -} - -pub fn is_process_trusted(_prompt: bool) -> bool { - #[cfg(target_os = "macos")] - return crate::platform::macos::is_process_trusted(_prompt); - #[cfg(not(target_os = "macos"))] - return true; -} - -pub fn is_can_screen_recording(_prompt: bool) -> bool { - #[cfg(target_os = "macos")] - return crate::platform::macos::is_can_screen_recording(_prompt); - #[cfg(not(target_os = "macos"))] - return true; -} - -pub fn is_installed_daemon(_prompt: bool) -> bool { - #[cfg(target_os = "macos")] - return crate::platform::macos::is_installed_daemon(_prompt); - #[cfg(not(target_os = "macos"))] - return true; -} - -pub fn get_error() -> String { - #[cfg(target_os = "linux")] - { - let dtype = crate::platform::linux::get_display_server(); - if "wayland" == dtype { - return "".to_owned(); - } - if dtype != "x11" { - return format!( - "{} {}, {}", - t("Unsupported display server ".to_owned()), - dtype, - t("x11 expected".to_owned()), - ); - } - } - return "".to_owned(); -} - -pub fn is_login_wayland() -> bool { - #[cfg(target_os = "linux")] - return crate::platform::linux::is_login_wayland(); - #[cfg(not(target_os = "linux"))] - return false; -} - -pub fn fix_login_wayland() { - #[cfg(target_os = "linux")] - crate::platform::linux::fix_login_wayland(); -} - -pub fn current_is_wayland() -> bool { - #[cfg(target_os = "linux")] - return crate::platform::linux::current_is_wayland(); - #[cfg(not(target_os = "linux"))] - return false; -} - -pub fn modify_default_login() -> String { - #[cfg(target_os = "linux")] - return crate::platform::linux::modify_default_login(); - #[cfg(not(target_os = "linux"))] - return "".to_owned(); -} - -pub fn get_software_update_url() -> String { - SOFTWARE_UPDATE_URL.lock().unwrap().clone() -} - -pub fn get_new_version() -> String { - hbb_common::get_version_from_url(&*SOFTWARE_UPDATE_URL.lock().unwrap()) -} - -pub fn get_version() -> String { - crate::VERSION.to_owned() -} - -pub fn get_app_name() -> String { - crate::get_app_name() -} - -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() -} - -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()) -} - -pub fn create_shortcut(_id: String) { - #[cfg(windows)] - crate::platform::windows::create_shortcut(&_id).ok(); -} - -pub fn discover() { - std::thread::spawn(move || { - allow_err!(crate::rendezvous_mediator::discover()); - }); -} - -pub fn get_lan_peers() -> String { - config::LanPeers::load().peers -} - -pub fn get_uuid() -> String { - base64::encode(crate::get_uuid()) -} - -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()); -} - -pub fn change_id(id: String) { - *ASYNC_JOB_STATUS.lock().unwrap() = " ".to_owned(); - let old_id = get_id(); - std::thread::spawn(move || { - *ASYNC_JOB_STATUS.lock().unwrap() = change_id_(id, old_id).to_owned(); - }); -} - -pub fn post_request(url: String, body: String, header: String) { - *ASYNC_JOB_STATUS.lock().unwrap() = " ".to_owned(); - std::thread::spawn(move || { - *ASYNC_JOB_STATUS.lock().unwrap() = match crate::post_request_sync(url, body, &header) { - Err(err) => err.to_string(), - Ok(text) => text, - }; - }); -} - -pub fn is_ok_change_id() -> bool { - machine_uid::get().is_ok() -} - -pub fn get_async_job_status() -> String { - ASYNC_JOB_STATUS.lock().unwrap().clone() -} - -pub fn t(name: String) -> String { - crate::client::translate(name) -} - -pub fn is_xfce() -> bool { - crate::platform::is_xfce() -} - -pub fn get_api_server() -> String { - crate::get_api_server( - get_option_("api-server"), - get_option_("custom-rendezvous-server"), - ) } struct UIHostHandler; @@ -1147,193 +552,6 @@ impl sciter::host::HostHandler for UIHostHandler { } } -pub fn check_zombie(childs: Childs) { - let mut deads = Vec::new(); - loop { - let mut lock = childs.lock().unwrap(); - let mut n = 0; - for (id, c) in lock.1.iter_mut() { - if let Ok(Some(_)) = c.try_wait() { - deads.push(id.clone()); - n += 1; - } - } - for ref id in deads.drain(..) { - lock.1.remove(id); - } - if n > 0 { - lock.0 = true; - } - drop(lock); - std::thread::sleep(std::time::Duration::from_millis(100)); - } -} - -// notice: avoiding create ipc connecton repeatly, -// because windows named pipe has serious memory leak issue. -#[tokio::main(flavor = "current_thread")] -async fn check_connect_status_(reconnect: bool, rx: mpsc::UnboundedReceiver) { - let mut key_confirmed = false; - let mut rx = rx; - let mut mouse_time = 0; - let mut id = "".to_owned(); - loop { - if let Ok(mut c) = ipc::connect(1000, "").await { - let mut timer = time::interval(time::Duration::from_secs(1)); - loop { - tokio::select! { - res = c.next() => { - match res { - Err(err) => { - log::error!("ipc connection closed: {}", err); - break; - } - Ok(Some(ipc::Data::MouseMoveTime(v))) => { - mouse_time = v; - UI_STATUS.lock().unwrap().2 = v; - } - Ok(Some(ipc::Data::Options(Some(v)))) => { - *OPTIONS.lock().unwrap() = v - } - Ok(Some(ipc::Data::Config((name, Some(value))))) => { - if name == "id" { - id = value; - } - } - Ok(Some(ipc::Data::OnlineStatus(Some((mut x, c))))) => { - if x > 0 { - x = 1 - } - key_confirmed = c; - *UI_STATUS.lock().unwrap() = (x as _, key_confirmed, mouse_time, id.clone()); - } - _ => {} - } - } - Some(data) = rx.recv() => { - allow_err!(c.send(&data).await); - } - _ = timer.tick() => { - c.send(&ipc::Data::OnlineStatus(None)).await.ok(); - c.send(&ipc::Data::Options(None)).await.ok(); - c.send(&ipc::Data::Config(("id".to_owned(), None))).await.ok(); - } - } - } - } - if !reconnect { - OPTIONS - .lock() - .unwrap() - .insert("ipc-closed".to_owned(), "Y".to_owned()); - break; - } - *UI_STATUS.lock().unwrap() = (-1, key_confirmed, mouse_time, id.clone()); - sleep(1.).await; - } -} - -fn check_connect_status(reconnect: bool) -> mpsc::UnboundedSender { - let (tx, rx) = mpsc::unbounded_channel::(); - std::thread::spawn(move || check_connect_status_(reconnect, rx)); - tx -} - -const INVALID_FORMAT: &'static str = "Invalid format"; -const UNKNOWN_ERROR: &'static str = "Unknown error"; - -#[tokio::main(flavor = "current_thread")] -async fn change_id_(id: String, old_id: String) -> &'static str { - if !hbb_common::is_valid_custom_id(&id) { - return INVALID_FORMAT; - } - let uuid = machine_uid::get().unwrap_or("".to_owned()); - if uuid.is_empty() { - return UNKNOWN_ERROR; - } - let rendezvous_servers = crate::ipc::get_rendezvous_servers(1_000).await; - let mut futs = Vec::new(); - let err: Arc> = Default::default(); - for rendezvous_server in rendezvous_servers { - let err = err.clone(); - let id = id.to_owned(); - let uuid = uuid.clone(); - let old_id = old_id.clone(); - futs.push(tokio::spawn(async move { - let tmp = check_id(rendezvous_server, old_id, id, uuid).await; - if !tmp.is_empty() { - *err.lock().unwrap() = tmp; - } - })); - } - join_all(futs).await; - let err = *err.lock().unwrap(); - if err.is_empty() { - crate::ipc::set_config_async("id", id.to_owned()).await.ok(); - } - err -} - -async fn check_id( - rendezvous_server: String, - old_id: String, - id: String, - uuid: String, -) -> &'static str { - let any_addr = Config::get_any_listen_addr(); - if let Ok(mut socket) = FramedStream::new( - crate::check_port(rendezvous_server, RENDEZVOUS_PORT), - any_addr, - RENDEZVOUS_TIMEOUT, - ) - .await - { - let mut msg_out = Message::new(); - msg_out.set_register_pk(RegisterPk { - old_id, - id, - uuid: uuid.into(), - ..Default::default() - }); - let mut ok = false; - if socket.send(&msg_out).await.is_ok() { - if let Some(Ok(bytes)) = socket.next_timeout(3_000).await { - if let Ok(msg_in) = RendezvousMessage::parse_from_bytes(&bytes) { - match msg_in.union { - Some(rendezvous_message::Union::register_pk_response(rpr)) => { - match rpr.result.enum_value_or_default() { - register_pk_response::Result::OK => { - ok = true; - } - register_pk_response::Result::ID_EXISTS => { - return "Not available"; - } - register_pk_response::Result::TOO_FREQUENT => { - return "Too frequent"; - } - register_pk_response::Result::NOT_SUPPORT => { - return "server_not_support"; - } - register_pk_response::Result::INVALID_ID_FORMAT => { - return INVALID_FORMAT; - } - _ => {} - } - } - _ => {} - } - } - } - } - if !ok { - return UNKNOWN_ERROR; - } - } else { - return "Failed to connect to rendezvous server"; - } - "" -} - // sacrifice some memory pub fn value_crash_workaround(values: &[Value]) -> Arc> { let persist = Arc::new(values.to_vec()); diff --git a/src/ui_interface.rs b/src/ui_interface.rs new file mode 100644 index 000000000..7b5451ecf --- /dev/null +++ b/src/ui_interface.rs @@ -0,0 +1,795 @@ +#[cfg(target_os = "macos")] +mod macos; +use crate::common::SOFTWARE_UPDATE_URL; +use crate::ipc; +use hbb_common::{ + allow_err, + config::{self, Config, LocalConfig, PeerConfig, RENDEZVOUS_PORT, RENDEZVOUS_TIMEOUT}, + futures::future::join_all, + log, + protobuf::Message as _, + rendezvous_proto::*, + sleep, + tcp::FramedStream, + tokio::{self, sync::mpsc, time}, +}; +use std::{ + collections::HashMap, + process::Child, + sync::{Arc, Mutex}, + time::SystemTime, +}; + +type Message = RendezvousMessage; + +pub type Childs = Arc)>>; +type Status = (i32, bool, i64, String); // (status_num, key_confirmed, mouse_time, id) + +lazy_static::lazy_static! { + pub static ref CHILDS : Childs = Default::default(); + pub static ref UI_STATUS : Arc> = Arc::new(Mutex::new((0, false, 0, "".to_owned()))); + pub static ref OPTIONS : Arc>> = Arc::new(Mutex::new(Config::get_options())); + pub static ref ASYNC_JOB_STATUS : Arc> = Default::default(); + pub static ref SENDER : Mutex> = Mutex::new(check_connect_status(true)); +} + +pub fn recent_sessions_updated() -> bool { + let mut childs = CHILDS.lock().unwrap(); + if childs.0 { + childs.0 = false; + true + } else { + false + } +} + +pub fn get_id() -> String { + ipc::get_id() +} + +pub fn get_password() -> String { + ipc::get_password() +} + +pub fn update_password(password: String) { + if password.is_empty() { + allow_err!(ipc::set_password(Config::get_auto_password())); + } else { + allow_err!(ipc::set_password(password)); + } +} + +pub fn get_remote_id() -> String { + LocalConfig::get_remote_id() +} + +pub fn set_remote_id(id: String) { + LocalConfig::set_remote_id(&id); +} + +pub fn goto_install() { + allow_err!(crate::run_me(vec!["--install"])); +} + +pub fn install_me(_options: String, _path: String) { + #[cfg(windows)] + std::thread::spawn(move || { + allow_err!(crate::platform::windows::install_me(&_options, _path)); + std::process::exit(0); + }); +} + +pub fn update_me(_path: String) { + #[cfg(target_os = "linux")] + { + std::process::Command::new("pkexec") + .args(&["apt", "install", "-f", &_path]) + .spawn() + .ok(); + std::fs::remove_file(&_path).ok(); + crate::run_me(Vec::<&str>::new()).ok(); + } + #[cfg(windows)] + { + let mut path = _path; + if path.is_empty() { + if let Ok(tmp) = std::env::current_exe() { + path = tmp.to_string_lossy().to_string(); + } + } + std::process::Command::new(path) + .arg("--update") + .spawn() + .ok(); + std::process::exit(0); + } +} + +pub fn run_without_install() { + crate::run_me(vec!["--noinstall"]).ok(); + std::process::exit(0); +} + +pub fn show_run_without_install() -> bool { + let mut it = std::env::args(); + if let Some(tmp) = it.next() { + if crate::is_setup(&tmp) { + return it.next() == None; + } + } + false +} + +pub fn has_rendezvous_service() -> bool { + #[cfg(all(windows, feature = "hbbs"))] + return crate::platform::is_win_server() && crate::platform::windows::get_license().is_some(); + return false; +} + +pub fn get_license() -> String { + #[cfg(windows)] + if let Some(lic) = crate::platform::windows::get_license() { + return format!( + "
Key: {}
Host: {} Api: {}", + lic.key, lic.host, lic.api + ); + } + Default::default() +} + +pub fn get_option(key: String) -> String { + get_option_(&key) +} + +fn get_option_(key: &str) -> String { + let map = OPTIONS.lock().unwrap(); + if let Some(v) = map.get(key) { + v.to_owned() + } else { + "".to_owned() + } +} + +pub fn get_local_option(key: String) -> String { + LocalConfig::get_option(&key) +} + +pub fn set_local_option(key: String, value: String) { + LocalConfig::set_option(key, value); +} + +pub fn peer_has_password(id: String) -> bool { + !PeerConfig::load(&id).password.is_empty() +} + +pub fn forget_password(id: String) { + let mut c = PeerConfig::load(&id); + c.password.clear(); + c.store(&id); +} + +pub fn get_peer_option(id: String, name: String) -> String { + let c = PeerConfig::load(&id); + c.options.get(&name).unwrap_or(&"".to_owned()).to_owned() +} + +pub fn set_peer_option(id: String, name: String, value: String) { + let mut c = PeerConfig::load(&id); + if value.is_empty() { + c.options.remove(&name); + } else { + c.options.insert(name, value); + } + c.store(&id); +} + +pub fn using_public_server() -> bool { + crate::get_custom_rendezvous_server(get_option_("custom-rendezvous-server")).is_empty() +} + +pub fn get_options() -> HashMap { + // TODO Vec<(String,String)> + let options = OPTIONS.lock().unwrap(); + let mut m = HashMap::new(); + for (k, v) in options.iter() { + m.insert(k.into(), v.into()); + } + m +} + +pub fn test_if_valid_server(host: String) -> String { + hbb_common::socket_client::test_if_valid_server(&host) +} + +pub fn get_sound_inputs() -> Vec { + let mut a = Vec::new(); + #[cfg(windows)] + { + // TODO TEST + fn get_sound_inputs_() -> Vec { + let mut out = Vec::new(); + use cpal::traits::{DeviceTrait, HostTrait}; + let host = cpal::default_host(); + if let Ok(devices) = host.devices() { + for device in devices { + if device.default_input_config().is_err() { + continue; + } + if let Ok(name) = device.name() { + out.push(name); + } + } + } + out + } + + let inputs = Arc::new(Mutex::new(Vec::new())); + let cloned = inputs.clone(); + // can not call below in UI thread, because conflict with sciter sound com initialization + std::thread::spawn(move || *cloned.lock().unwrap() = get_sound_inputs_()) + .join() + .ok(); + for name in inputs.lock().unwrap().drain(..) { + a.push(name); + } + } + #[cfg(target_os = "linux")] // TODO + { + let inputs: Vec = crate::platform::linux::get_pa_sources() + .drain(..) + .map(|x| x.1) + .collect(); + + for name in inputs { + a.push(name); + } + } + a +} + +pub fn set_options(m: HashMap) { + *OPTIONS.lock().unwrap() = m.clone(); + ipc::set_options(m).ok(); +} + +pub fn set_option(key: String, value: String) { + let mut options = OPTIONS.lock().unwrap(); + #[cfg(target_os = "macos")] + if &key == "stop-service" { + let is_stop = value == "Y"; + if is_stop && crate::platform::macos::uninstall() { + return; + } + } + if value.is_empty() { + options.remove(&key); + } else { + options.insert(key.clone(), value.clone()); + } + ipc::set_options(options.clone()).ok(); +} + +pub fn install_path() -> String { + #[cfg(windows)] + return crate::platform::windows::get_install_info().1; + #[cfg(not(windows))] + return "".to_owned(); +} + +pub fn get_socks() -> Vec { + let s = ipc::get_socks(); + match s { + None => Vec::new(), + Some(s) => { + let mut v = Vec::new(); + v.push(s.proxy); + v.push(s.username); + v.push(s.password); + v + } + } +} + +pub fn set_socks(proxy: String, username: String, password: String) { + ipc::set_socks(config::Socks5Server { + proxy, + username, + password, + }) + .ok(); +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn is_installed() -> bool { + crate::platform::is_installed() +} + +pub fn is_rdp_service_open() -> bool { + #[cfg(windows)] + return is_installed() && crate::platform::windows::is_rdp_service_open(); + #[cfg(not(windows))] + return false; +} + +pub fn is_share_rdp() -> bool { + #[cfg(windows)] + return crate::platform::windows::is_share_rdp(); + #[cfg(not(windows))] + return false; +} + +pub fn set_share_rdp(_enable: bool) { + #[cfg(windows)] + crate::platform::windows::set_share_rdp(_enable); +} + +pub fn is_installed_lower_version() -> bool { + #[cfg(not(windows))] + return false; + #[cfg(windows)] + { + let installed_version = crate::platform::windows::get_installed_version(); + let a = hbb_common::get_version_number(crate::VERSION); + let b = hbb_common::get_version_number(&installed_version); + return a > b; + } +} + +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); +} + +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 +} + +pub fn get_mouse_time() -> f64 { + let ui_status = UI_STATUS.lock().unwrap(); + let res = ui_status.2 as f64; + return res; +} + +pub fn check_mouse_time() { + let sender = SENDER.lock().unwrap(); + allow_err!(sender.send(ipc::Data::MouseMoveTime(0))); +} + +pub fn get_connect_status() -> Status { + let ui_statue = UI_STATUS.lock().unwrap(); + let res = ui_statue.clone(); + res +} + +pub fn get_peer(id: String) -> PeerConfig { + PeerConfig::load(&id) +} + +pub fn get_fav() -> Vec { + LocalConfig::get_fav() +} + +pub fn store_fav(fav: Vec) { + LocalConfig::set_fav(fav); +} + +pub fn get_recent_sessions() -> Vec<(String, SystemTime, PeerConfig)> { + PeerConfig::peers() +} + +#[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] +pub fn get_icon() -> String { + crate::get_icon() +} + +pub fn remove_peer(id: String) { + PeerConfig::remove(&id); +} + +pub fn new_remote(id: String, remote_type: String) { + let mut lock = CHILDS.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); + } + } +} + +pub fn is_process_trusted(_prompt: bool) -> bool { + #[cfg(target_os = "macos")] + return crate::platform::macos::is_process_trusted(_prompt); + #[cfg(not(target_os = "macos"))] + return true; +} + +pub fn is_can_screen_recording(_prompt: bool) -> bool { + #[cfg(target_os = "macos")] + return crate::platform::macos::is_can_screen_recording(_prompt); + #[cfg(not(target_os = "macos"))] + return true; +} + +pub fn is_installed_daemon(_prompt: bool) -> bool { + #[cfg(target_os = "macos")] + return crate::platform::macos::is_installed_daemon(_prompt); + #[cfg(not(target_os = "macos"))] + return true; +} + +pub fn get_error() -> String { + #[cfg(target_os = "linux")] + { + let dtype = crate::platform::linux::get_display_server(); + if "wayland" == dtype { + return "".to_owned(); + } + if dtype != "x11" { + return format!( + "{} {}, {}", + t("Unsupported display server ".to_owned()), + dtype, + t("x11 expected".to_owned()), + ); + } + } + return "".to_owned(); +} + +pub fn is_login_wayland() -> bool { + #[cfg(target_os = "linux")] + return crate::platform::linux::is_login_wayland(); + #[cfg(not(target_os = "linux"))] + return false; +} + +pub fn fix_login_wayland() { + #[cfg(target_os = "linux")] + crate::platform::linux::fix_login_wayland(); +} + +pub fn current_is_wayland() -> bool { + #[cfg(target_os = "linux")] + return crate::platform::linux::current_is_wayland(); + #[cfg(not(target_os = "linux"))] + return false; +} + +pub fn modify_default_login() -> String { + #[cfg(target_os = "linux")] + return crate::platform::linux::modify_default_login(); + #[cfg(not(target_os = "linux"))] + return "".to_owned(); +} + +pub fn get_software_update_url() -> String { + SOFTWARE_UPDATE_URL.lock().unwrap().clone() +} + +pub fn get_new_version() -> String { + hbb_common::get_version_from_url(&*SOFTWARE_UPDATE_URL.lock().unwrap()) +} + +pub fn get_version() -> String { + crate::VERSION.to_owned() +} + +pub fn get_app_name() -> String { + crate::get_app_name() +} + +#[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() +} + +#[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()) +} + +pub fn create_shortcut(_id: String) { + #[cfg(windows)] + crate::platform::windows::create_shortcut(&_id).ok(); +} + +pub fn discover() { + std::thread::spawn(move || { + allow_err!(crate::rendezvous_mediator::discover()); + }); +} + +pub fn get_lan_peers() -> String { + config::LanPeers::load().peers +} + +pub fn get_uuid() -> String { + base64::encode(crate::get_uuid()) +} + +#[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(not(any(target_os = "android", target_os = "ios")))] +pub fn change_id(id: String) { + *ASYNC_JOB_STATUS.lock().unwrap() = " ".to_owned(); + let old_id = get_id(); + std::thread::spawn(move || { + *ASYNC_JOB_STATUS.lock().unwrap() = change_id_(id, old_id).to_owned(); + }); +} + +pub fn post_request(url: String, body: String, header: String) { + *ASYNC_JOB_STATUS.lock().unwrap() = " ".to_owned(); + std::thread::spawn(move || { + *ASYNC_JOB_STATUS.lock().unwrap() = match crate::post_request_sync(url, body, &header) { + Err(err) => err.to_string(), + Ok(text) => text, + }; + }); +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn is_ok_change_id() -> bool { + machine_uid::get().is_ok() +} + +pub fn get_async_job_status() -> String { + ASYNC_JOB_STATUS.lock().unwrap().clone() +} + +#[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] +pub fn t(name: String) -> String { + crate::client::translate(name) +} + +pub fn is_xfce() -> bool { + crate::platform::is_xfce() +} + +pub fn get_api_server() -> String { + crate::get_api_server( + get_option_("api-server"), + get_option_("custom-rendezvous-server"), + ) +} + +pub fn check_zombie(childs: Childs) { + let mut deads = Vec::new(); + loop { + let mut lock = childs.lock().unwrap(); + let mut n = 0; + for (id, c) in lock.1.iter_mut() { + if let Ok(Some(_)) = c.try_wait() { + deads.push(id.clone()); + n += 1; + } + } + for ref id in deads.drain(..) { + lock.1.remove(id); + } + if n > 0 { + lock.0 = true; + } + drop(lock); + std::thread::sleep(std::time::Duration::from_millis(100)); + } +} + +fn check_connect_status(reconnect: bool) -> mpsc::UnboundedSender { + let (tx, rx) = mpsc::unbounded_channel::(); + std::thread::spawn(move || check_connect_status_(reconnect, rx)); + tx +} + +// notice: avoiding create ipc connecton repeatly, +// because windows named pipe has serious memory leak issue. +#[tokio::main(flavor = "current_thread")] +async fn check_connect_status_(reconnect: bool, rx: mpsc::UnboundedReceiver) { + let mut key_confirmed = false; + let mut rx = rx; + let mut mouse_time = 0; + let mut id = "".to_owned(); + loop { + if let Ok(mut c) = ipc::connect(1000, "").await { + let mut timer = time::interval(time::Duration::from_secs(1)); + loop { + tokio::select! { + res = c.next() => { + match res { + Err(err) => { + log::error!("ipc connection closed: {}", err); + break; + } + Ok(Some(ipc::Data::MouseMoveTime(v))) => { + mouse_time = v; + UI_STATUS.lock().unwrap().2 = v; + } + Ok(Some(ipc::Data::Options(Some(v)))) => { + *OPTIONS.lock().unwrap() = v + } + Ok(Some(ipc::Data::Config((name, Some(value))))) => { + if name == "id" { + id = value; + } + } + Ok(Some(ipc::Data::OnlineStatus(Some((mut x, c))))) => { + if x > 0 { + x = 1 + } + key_confirmed = c; + *UI_STATUS.lock().unwrap() = (x as _, key_confirmed, mouse_time, id.clone()); + } + _ => {} + } + } + Some(data) = rx.recv() => { + allow_err!(c.send(&data).await); + } + _ = timer.tick() => { + c.send(&ipc::Data::OnlineStatus(None)).await.ok(); + c.send(&ipc::Data::Options(None)).await.ok(); + c.send(&ipc::Data::Config(("id".to_owned(), None))).await.ok(); + } + } + } + } + if !reconnect { + OPTIONS + .lock() + .unwrap() + .insert("ipc-closed".to_owned(), "Y".to_owned()); + break; + } + *UI_STATUS.lock().unwrap() = (-1, key_confirmed, mouse_time, id.clone()); + sleep(1.).await; + } +} + +const INVALID_FORMAT: &'static str = "Invalid format"; +const UNKNOWN_ERROR: &'static str = "Unknown error"; + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +#[tokio::main(flavor = "current_thread")] +async fn change_id_(id: String, old_id: String) -> &'static str { + if !hbb_common::is_valid_custom_id(&id) { + return INVALID_FORMAT; + } + let uuid = machine_uid::get().unwrap_or("".to_owned()); + if uuid.is_empty() { + return UNKNOWN_ERROR; + } + let rendezvous_servers = crate::ipc::get_rendezvous_servers(1_000).await; + let mut futs = Vec::new(); + let err: Arc> = Default::default(); + for rendezvous_server in rendezvous_servers { + let err = err.clone(); + let id = id.to_owned(); + let uuid = uuid.clone(); + let old_id = old_id.clone(); + futs.push(tokio::spawn(async move { + let tmp = check_id(rendezvous_server, old_id, id, uuid).await; + if !tmp.is_empty() { + *err.lock().unwrap() = tmp; + } + })); + } + join_all(futs).await; + let err = *err.lock().unwrap(); + if err.is_empty() { + crate::ipc::set_config_async("id", id.to_owned()).await.ok(); + } + err +} + +async fn check_id( + rendezvous_server: String, + old_id: String, + id: String, + uuid: String, +) -> &'static str { + let any_addr = Config::get_any_listen_addr(); + if let Ok(mut socket) = FramedStream::new( + crate::check_port(rendezvous_server, RENDEZVOUS_PORT), + any_addr, + RENDEZVOUS_TIMEOUT, + ) + .await + { + let mut msg_out = Message::new(); + msg_out.set_register_pk(RegisterPk { + old_id, + id, + uuid: uuid.into(), + ..Default::default() + }); + let mut ok = false; + if socket.send(&msg_out).await.is_ok() { + if let Some(Ok(bytes)) = socket.next_timeout(3_000).await { + if let Ok(msg_in) = RendezvousMessage::parse_from_bytes(&bytes) { + match msg_in.union { + Some(rendezvous_message::Union::register_pk_response(rpr)) => { + match rpr.result.enum_value_or_default() { + register_pk_response::Result::OK => { + ok = true; + } + register_pk_response::Result::ID_EXISTS => { + return "Not available"; + } + register_pk_response::Result::TOO_FREQUENT => { + return "Too frequent"; + } + register_pk_response::Result::NOT_SUPPORT => { + return "server_not_support"; + } + register_pk_response::Result::INVALID_ID_FORMAT => { + return INVALID_FORMAT; + } + _ => {} + } + } + _ => {} + } + } + } + } + if !ok { + return UNKNOWN_ERROR; + } + } else { + return "Failed to connect to rendezvous server"; + } + "" +} From fa5f48638f20460ca82204ca183fbb952f4215b2 Mon Sep 17 00:00:00 2001 From: csf Date: Thu, 26 May 2022 18:25:16 +0800 Subject: [PATCH 0021/2015] adapt to flutter 3 --- flutter/lib/common.dart | 12 ++++----- flutter/lib/mobile/pages/remote_page.dart | 2 +- flutter/lib/mobile/widgets/gestures.dart | 19 +++++++------- flutter/pubspec.lock | 32 +++++++++++------------ flutter/pubspec.yaml | 11 ++++---- 5 files changed, 38 insertions(+), 38 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 8d432474e..e66f8d79c 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -224,10 +224,10 @@ class AccessibilityListener extends StatelessWidget { Widget build(BuildContext context) { return Listener( onPointerDown: (evt) { - if (evt.size == 1 && GestureBinding.instance != null) { - GestureBinding.instance!.handlePointerEvent(PointerAddedEvent( + if (evt.size == 1) { + GestureBinding.instance.handlePointerEvent(PointerAddedEvent( pointer: evt.pointer + offset, position: evt.position)); - GestureBinding.instance!.handlePointerEvent(PointerDownEvent( + GestureBinding.instance.handlePointerEvent(PointerDownEvent( pointer: evt.pointer + offset, size: 0.1, position: evt.position)); @@ -235,17 +235,17 @@ class AccessibilityListener extends StatelessWidget { }, onPointerUp: (evt) { if (evt.size == 1 && GestureBinding.instance != null) { - GestureBinding.instance!.handlePointerEvent(PointerUpEvent( + GestureBinding.instance.handlePointerEvent(PointerUpEvent( pointer: evt.pointer + offset, size: 0.1, position: evt.position)); - GestureBinding.instance!.handlePointerEvent(PointerRemovedEvent( + GestureBinding.instance.handlePointerEvent(PointerRemovedEvent( pointer: evt.pointer + offset, position: evt.position)); } }, onPointerMove: (evt) { if (evt.size == 1 && GestureBinding.instance != null) { - GestureBinding.instance!.handlePointerEvent(PointerMoveEvent( + GestureBinding.instance.handlePointerEvent(PointerMoveEvent( pointer: evt.pointer + offset, size: 0.1, delta: evt.delta, diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index bf6220998..4ba50e8e5 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -45,7 +45,7 @@ class _RemotePageState extends State { void initState() { super.initState(); FFI.connect(widget.id); - WidgetsBinding.instance!.addPostFrameCallback((_) { + WidgetsBinding.instance.addPostFrameCallback((_) { SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); showLoading(translate('Connecting...')); _interval = diff --git a/flutter/lib/mobile/widgets/gestures.dart b/flutter/lib/mobile/widgets/gestures.dart index 8d690c734..d70fe05e6 100644 --- a/flutter/lib/mobile/widgets/gestures.dart +++ b/flutter/lib/mobile/widgets/gestures.dart @@ -213,7 +213,7 @@ class HoldTapMoveGestureRecognizer extends GestureRecognizer { _stopSecondTapDownTimer(); final _TapTracker tracker = _TapTracker( event: event, - entry: GestureBinding.instance!.gestureArena.add(event.pointer, this), + entry: GestureBinding.instance.gestureArena.add(event.pointer, this), doubleTapMinTime: kDoubleTapMinTime, gestureSettings: gestureSettings, ); @@ -318,13 +318,13 @@ class HoldTapMoveGestureRecognizer extends GestureRecognizer { final _TapTracker tracker = _firstTap!; _firstTap = null; _reject(tracker); - GestureBinding.instance!.gestureArena.release(tracker.pointer); + GestureBinding.instance.gestureArena.release(tracker.pointer); if (_secondTap != null) { final _TapTracker tracker = _secondTap!; _secondTap = null; _reject(tracker); - GestureBinding.instance!.gestureArena.release(tracker.pointer); + GestureBinding.instance.gestureArena.release(tracker.pointer); } } _firstTap = null; @@ -334,7 +334,7 @@ class HoldTapMoveGestureRecognizer extends GestureRecognizer { void _registerFirstTap(_TapTracker tracker) { _startFirstTapUpTimer(); - GestureBinding.instance!.gestureArena.hold(tracker.pointer); + GestureBinding.instance.gestureArena.hold(tracker.pointer); // Note, order is important below in order for the clear -> reject logic to // work properly. _freezeTracker(tracker); @@ -350,7 +350,7 @@ class HoldTapMoveGestureRecognizer extends GestureRecognizer { } _startSecondTapDownTimer(); - GestureBinding.instance!.gestureArena.hold(tracker.pointer); + GestureBinding.instance.gestureArena.hold(tracker.pointer); _secondTap = tracker; @@ -463,7 +463,7 @@ class DoubleFinerTapGestureRecognizer extends GestureRecognizer { void _trackTap(PointerDownEvent event) { final _TapTracker tracker = _TapTracker( event: event, - entry: GestureBinding.instance!.gestureArena.add(event.pointer, this), + entry: GestureBinding.instance.gestureArena.add(event.pointer, this), doubleTapMinTime: kDoubleTapMinTime, gestureSettings: gestureSettings, ); @@ -532,7 +532,7 @@ class DoubleFinerTapGestureRecognizer extends GestureRecognizer { } void _registerTap(_TapTracker tracker) { - GestureBinding.instance!.gestureArena.hold(tracker.pointer); + GestureBinding.instance.gestureArena.hold(tracker.pointer); // Note, order is important below in order for the clear -> reject logic to // work properly. } @@ -615,15 +615,14 @@ class _TapTracker { void startTrackingPointer(PointerRoute route, Matrix4? transform) { if (!_isTrackingPointer) { _isTrackingPointer = true; - GestureBinding.instance!.pointerRouter - .addRoute(pointer, route, transform); + GestureBinding.instance.pointerRouter.addRoute(pointer, route, transform); } } void stopTrackingPointer(PointerRoute route) { if (_isTrackingPointer) { _isTrackingPointer = false; - GestureBinding.instance!.pointerRouter.removeRoute(pointer, route); + GestureBinding.instance.pointerRouter.removeRoute(pointer, route); } } diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 083c4a494..59f8cdc3e 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -233,12 +233,10 @@ packages: flutter_smart_dialog: dependency: "direct main" description: - path: "." - ref: HEAD - resolved-ref: c89ce60664cbc206cb98c1f407e86b8a766f4c0e - url: "https://github.com/Heap-Hop/flutter_smart_dialog.git" - source: git - version: "4.0.0" + name: flutter_smart_dialog + url: "https://pub.dartlang.org" + source: hosted + version: "4.3.1" flutter_test: dependency: "direct dev" description: flutter @@ -269,7 +267,7 @@ packages: name: image url: "https://pub.dartlang.org" source: hosted - version: "3.2.0" + version: "3.1.3" image_picker: dependency: "direct main" description: @@ -458,7 +456,7 @@ packages: name: petitparser url: "https://pub.dartlang.org" source: hosted - version: "5.0.0" + version: "4.4.0" platform: dependency: transitive description: @@ -490,9 +488,11 @@ packages: qr_code_scanner: dependency: "direct main" description: - name: qr_code_scanner - url: "https://pub.dartlang.org" - source: hosted + path: "." + ref: fix_break_changes_platform + resolved-ref: "0feca6f15042c279ff575c559a3430df917b623d" + url: "https://github.com/Heap-Hop/qr_code_scanner.git" + source: git version: "0.7.0" quiver: dependency: transitive @@ -638,7 +638,7 @@ packages: name: typed_data url: "https://pub.dartlang.org" source: hosted - version: "1.3.1" + version: "1.3.0" url_launcher: dependency: "direct main" description: @@ -750,7 +750,7 @@ packages: name: win32 url: "https://pub.dartlang.org" source: hosted - version: "2.6.1" + version: "2.5.2" xdg_directories: dependency: transitive description: @@ -764,7 +764,7 @@ packages: name: xml url: "https://pub.dartlang.org" source: hosted - version: "6.0.1" + version: "5.3.1" yaml: dependency: transitive description: @@ -780,5 +780,5 @@ packages: source: hosted version: "0.1.0" sdks: - dart: ">=2.17.0 <3.0.0" - flutter: ">=2.10.0" + dart: ">=2.17.0-0 <3.0.0" + flutter: ">=3.0.0" diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index c8d31e87e..f2aa1bf44 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -19,7 +19,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev version: 1.1.10+27 environment: - sdk: ">=2.16.1 <3.0.0" + sdk: ">=2.16.1" dependencies: flutter: @@ -46,13 +46,14 @@ dependencies: settings_ui: ^2.0.2 flutter_breadcrumb: ^1.0.1 http: ^0.13.4 - qr_code_scanner: ^0.7.0 + qr_code_scanner: + git: + url: https://github.com/Heap-Hop/qr_code_scanner.git + ref: fix_break_changes_platform zxing2: ^0.1.0 image_picker: ^0.8.5 image: ^3.1.3 - flutter_smart_dialog: - git: - url: https://github.com/Heap-Hop/flutter_smart_dialog.git + flutter_smart_dialog: ^4.3.1 flutter_rust_bridge: ^1.30.0 dev_dependencies: From 9dd6e400031947bab1804932cf990e3c7d026411 Mon Sep 17 00:00:00 2001 From: SoLongAndThanksForAllThePizza <103753680+SoLongAndThanksForAllThePizza@users.noreply.github.com> Date: Sat, 28 May 2022 03:56:42 +0800 Subject: [PATCH 0022/2015] add comment --- flutter/lib/mobile/pages/connection_page.dart | 17 ++ flutter/lib/models/model.dart | 25 +++ flutter/lib/models/native_model.dart | 7 + src/client.rs | 187 +++++++++++++++++- src/client/helper.rs | 12 +- src/flutter.rs | 112 ++++++++++- src/flutter_ffi.rs | 13 ++ 7 files changed, 363 insertions(+), 10 deletions(-) diff --git a/flutter/lib/mobile/pages/connection_page.dart b/flutter/lib/mobile/pages/connection_page.dart index 8067ca146..113c41676 100644 --- a/flutter/lib/mobile/pages/connection_page.dart +++ b/flutter/lib/mobile/pages/connection_page.dart @@ -10,6 +10,7 @@ import 'remote_page.dart'; import 'settings_page.dart'; import 'scan_page.dart'; +/// Connection page for connecting to a remote peer. class ConnectionPage extends StatefulWidget implements PageShape { ConnectionPage({Key? key}) : super(key: key); @@ -26,8 +27,12 @@ class ConnectionPage extends StatefulWidget implements PageShape { _ConnectionPageState createState() => _ConnectionPageState(); } +/// State for the connection page. class _ConnectionPageState extends State { + /// Controller for the id input bar. final _idController = TextEditingController(); + + /// Update url. If it's not null, means an update is available. var _updateUrl = ''; var _menuPos; @@ -60,11 +65,15 @@ class _ConnectionPageState extends State { ); } + /// Callback for the connect button. + /// Connects to the selected peer. void onConnect() { var id = _idController.text.trim(); connect(id); } + /// Connect to a peer with [id]. + /// If [isFileTransfer], starts a session only for file transfer. void connect(String id, {bool isFileTransfer = false}) async { if (id == '') return; id = id.replaceAll(' ', ''); @@ -94,6 +103,8 @@ class _ConnectionPageState extends State { } } + /// UI for software update. + /// If [_updateUrl] is not empty, shows a button to update the software. Widget getUpdateUI() { return _updateUrl.isEmpty ? SizedBox(height: 0) @@ -114,6 +125,8 @@ class _ConnectionPageState extends State { color: Colors.white, fontWeight: FontWeight.bold)))); } + /// UI for the search bar. + /// Search for a peer and connect to it if the id exists. Widget getSearchBarUI() { var w = Padding( padding: const EdgeInsets.fromLTRB(16.0, 8.0, 16.0, 0.0), @@ -187,6 +200,7 @@ class _ConnectionPageState extends State { super.dispose(); } + /// Get the image for the current [platform]. Widget getPlatformImage(String platform) { platform = platform.toLowerCase(); if (platform == 'mac os') @@ -195,6 +209,7 @@ class _ConnectionPageState extends State { return Image.asset('assets/$platform.png', width: 24, height: 24); } + /// Get all the saved peers. Widget getPeers() { final size = MediaQuery.of(context).size; final space = 8.0; @@ -244,6 +259,8 @@ class _ConnectionPageState extends State { return Wrap(children: cards, spacing: space, runSpacing: space); } + /// Show the peer menu and handle user's choice. + /// User might remove the peer or send a file to the peer. void showPeerMenu(BuildContext context, String id) async { var value = await showMenu( context: context, diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index aef7a535d..464e171aa 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -119,6 +119,7 @@ class FfiModel with ChangeNotifier { _permissions.clear(); } + /// Bind the event listener to receive events from the Rust core. void updateEventListener(String peerId) { final void Function(Map) cb = (evt) { var name = evt['name']; @@ -179,6 +180,7 @@ class FfiModel with ChangeNotifier { notifyListeners(); } + /// Handle the message box event based on [evt] and [id]. void handleMsgBox(Map evt, String id) { var type = evt['type']; var title = evt['title']; @@ -193,6 +195,7 @@ class FfiModel with ChangeNotifier { } } + /// Show a message box with [type], [title] and [text]. void showMsgBox(String type, String title, String text, bool hasRetry) { msgBox(type, title, text); _timer?.cancel(); @@ -207,6 +210,7 @@ class FfiModel with ChangeNotifier { } } + /// Handle the peer info event based on [evt]. void handlePeerInfo(Map evt) { SmartDialog.dismiss(); _pi.version = evt['version']; @@ -649,6 +653,7 @@ class CursorModel with ChangeNotifier { } } +/// Mouse button enum. enum MouseButtons { left, right, wheel } extension ToString on MouseButtons { @@ -664,6 +669,7 @@ extension ToString on MouseButtons { } } +/// FFI class for communicating with the Rust core. class FFI { static var id = ""; static var shift = false; @@ -679,29 +685,35 @@ class FFI { static final chatModel = ChatModel(); static final fileModel = FileModel(); + /// Get the remote id for current client. static String getId() { return getByName('remote_id'); } + /// Send a mouse tap event(down and up). static void tap(MouseButtons button) { sendMouse('down', button); sendMouse('up', button); } + /// Send scroll event with scroll distance [y]. static void scroll(int y) { setByName('send_mouse', json.encode(modify({'type': 'wheel', 'y': y.toString()}))); } + /// Reconnect to the remote peer. static void reconnect() { setByName('reconnect'); FFI.ffiModel.clearPermissions(); } + /// Reset key modifiers to false, including [shift], [ctrl], [alt] and [command]. static void resetModifiers() { shift = ctrl = alt = command = false; } + /// Modify the given modifier map [evt] based on current modifier key status. static Map modify(Map evt) { if (ctrl) evt['ctrl'] = 'true'; if (shift) evt['shift'] = 'true'; @@ -710,12 +722,16 @@ class FFI { return evt; } + /// Send mouse press event. static void sendMouse(String type, MouseButtons button) { if (!ffiModel.keyboard()) return; setByName('send_mouse', json.encode(modify({'type': type, 'buttons': button.value}))); } + /// Send key stroke event. + /// [down] indicates the key's state(down or up). + /// [press] indicates a click event(down and up). static void inputKey(String name, {bool? down, bool? press}) { if (!ffiModel.keyboard()) return; setByName( @@ -727,6 +743,7 @@ class FFI { }))); } + /// Send mouse movement event with distance in [x] and [y]. static void moveMouse(double x, double y) { if (!ffiModel.keyboard()) return; var x2 = x.toInt(); @@ -734,6 +751,7 @@ class FFI { setByName('send_mouse', json.encode(modify({'x': '$x2', 'y': '$y2'}))); } + /// List the saved peers. static List peers() { try { var str = getByName('peers'); @@ -750,6 +768,7 @@ class FFI { return []; } + /// Connect with the given [id]. Only transfer file if [isFileTransfer]. static void connect(String id, {bool isFileTransfer = false}) { if (isFileTransfer) { setByName('connect_file_transfer', id); @@ -772,6 +791,7 @@ class FFI { return null; } + /// Login with [password], choose if the client should [remember] it. static void login(String password, bool remember) { setByName( 'login', @@ -781,6 +801,7 @@ class FFI { })); } + /// Close the remote session. static void close() { chatModel.close(); if (FFI.imageModel.image != null && !isWebDesktop) { @@ -796,10 +817,13 @@ class FFI { resetModifiers(); } + /// Send **get** command to the Rust core based on [name] and [arg]. + /// Return the result as a string. static String getByName(String name, [String arg = '']) { return PlatformFFI.getByName(name, arg); } + /// Send **set** command to the Rust core based on [name] and [value]. static void setByName(String name, [String value = '']) { PlatformFFI.setByName(name, value); } @@ -953,6 +977,7 @@ void initializeCursorAndCanvas() async { FFI.canvasModel.update(xCanvas, yCanvas, scale); } +/// Translate text based on the pre-defined dictionary. String translate(String name) { if (name.startsWith('Failed to') && name.contains(': ')) { return name.split(': ').map((x) => translate(x)).join(': '); diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index 21ecd37e3..f9135a06c 100644 --- a/flutter/lib/models/native_model.dart +++ b/flutter/lib/models/native_model.dart @@ -22,6 +22,8 @@ class RgbaFrame extends Struct { typedef F2 = Pointer Function(Pointer, Pointer); typedef F3 = void Function(Pointer, Pointer); +/// FFI wrapper around the native Rust core. +/// Hides the platform differences. class PlatformFFI { static Pointer? _lastRgbaFrame; static String _dir = ''; @@ -36,6 +38,8 @@ class PlatformFFI { return packageInfo.version; } + /// Send **get** command to the Rust core based on [name] and [arg]. + /// Return the result as a string. static String getByName(String name, [String arg = '']) { if (_getByName == null) return ''; var a = name.toNativeUtf8(); @@ -49,6 +53,7 @@ class PlatformFFI { return res; } + /// Send **set** command to the Rust core based on [name] and [value]. static void setByName(String name, [String value = '']) { if (_setByName == null) return; var a = name.toNativeUtf8(); @@ -58,6 +63,7 @@ class PlatformFFI { calloc.free(b); } + /// Init the FFI class, loads the native Rust core library. static Future init() async { isIOS = Platform.isIOS; isAndroid = Platform.isAndroid; @@ -112,6 +118,7 @@ class PlatformFFI { version = await getVersion(); } + /// Start listening to the Rust core's events and frames. static void _startListenEvent(RustdeskImpl rustdeskImpl) { () async { await for (final message in rustdeskImpl.startEventStream()) { diff --git a/src/client.rs b/src/client.rs index be2b788ab..236eb331d 100644 --- a/src/client.rs +++ b/src/client.rs @@ -39,6 +39,7 @@ pub mod helper; pub use helper::LatencyController; pub const SEC30: Duration = Duration::from_secs(30); +/// Client of the remote desktop. pub struct Client; #[cfg(not(any(target_os = "android", target_os = "linux")))] @@ -106,6 +107,7 @@ impl Drop for OboePlayer { } impl Client { + /// Start a new connection. pub async fn start( peer: &str, key: &str, @@ -125,6 +127,7 @@ impl Client { } } + /// Start a new connection. async fn _start( peer: &str, key: &str, @@ -259,6 +262,7 @@ impl Client { .await } + /// Connect to the peer. async fn connect( local_addr: SocketAddr, peer: SocketAddr, @@ -345,6 +349,7 @@ impl Client { Ok((conn, direct)) } + /// Establish secure connection with the server. async fn secure_connection( peer_id: &str, signed_id_pk: Vec, @@ -422,6 +427,7 @@ impl Client { Ok(()) } + /// Request a relay connection to the server. async fn request_relay( peer: &str, relay_server: String, @@ -478,6 +484,7 @@ impl Client { Self::create_relay(peer, uuid, relay_server, key, conn_type).await } + /// Create a relay connection to the server. async fn create_relay( peer: &str, uuid: String, @@ -505,6 +512,7 @@ impl Client { } } +/// Audio handler for the [`Client`]. #[derive(Default)] pub struct AudioHandler { audio_decoder: Option<(AudioDecoder, Vec)>, @@ -522,6 +530,7 @@ pub struct AudioHandler { } impl AudioHandler { + /// Create a new audio handler. pub fn new(latency_controller: Arc>) -> Self { AudioHandler { latency_controller, @@ -529,6 +538,7 @@ impl AudioHandler { } } + /// Start the audio playback. #[cfg(target_os = "linux")] fn start_audio(&mut self, format0: AudioFormat) -> ResultType<()> { use psimple::Simple; @@ -558,6 +568,7 @@ impl AudioHandler { Ok(()) } + /// Start the audio playback. #[cfg(target_os = "android")] fn start_audio(&mut self, format0: AudioFormat) -> ResultType<()> { self.oboe = Some(OboePlayer::new( @@ -568,6 +579,7 @@ impl AudioHandler { Ok(()) } + /// Start the audio playback. #[cfg(not(any(target_os = "android", target_os = "linux")))] fn start_audio(&mut self, format0: AudioFormat) -> ResultType<()> { let device = AUDIO_HOST @@ -592,6 +604,7 @@ impl AudioHandler { Ok(()) } + /// Handle audio format and create an audio decoder. pub fn handle_format(&mut self, f: AudioFormat) { match AudioDecoder::new(f.sample_rate, if f.channels > 1 { Stereo } else { Mono }) { Ok(d) => { @@ -606,6 +619,7 @@ impl AudioHandler { } } + /// Handle audio frame and play it. pub fn handle_frame(&mut self, frame: AudioFrame) { if frame.timestamp != 0 { if self @@ -673,6 +687,7 @@ impl AudioHandler { }); } + /// Build audio output stream for current device. #[cfg(not(any(target_os = "android", target_os = "linux")))] fn build_output_stream( &mut self, @@ -708,6 +723,7 @@ impl AudioHandler { } } +/// Video handler for the [`Client`]. pub struct VideoHandler { decoder: Decoder, latency_controller: Arc>, @@ -715,6 +731,7 @@ pub struct VideoHandler { } impl VideoHandler { + /// Create a new video handler. pub fn new(latency_controller: Arc>) -> Self { VideoHandler { decoder: Decoder::new(VideoCodecId::VP9, (num_cpus::get() / 2) as _).unwrap(), @@ -723,8 +740,10 @@ impl VideoHandler { } } + /// Handle a new video frame. pub fn handle_frame(&mut self, vf: VideoFrame) -> ResultType { if vf.timestamp != 0 { + // Update the lantency controller with the latest timestamp. self.latency_controller .lock() .unwrap() @@ -736,6 +755,7 @@ impl VideoHandler { } } + /// Handle a VP9S frame. pub fn handle_vp9s(&mut self, vp9s: &VP9s) -> ResultType { let mut last_frame = Image::new(); for vp9 in vp9s.frames.iter() { @@ -756,11 +776,13 @@ impl VideoHandler { } } + /// Reset the decoder. pub fn reset(&mut self) { self.decoder = Decoder::new(VideoCodecId::VP9, 1).unwrap(); } } +/// Login config handler for [`Client`]. #[derive(Default)] pub struct LoginConfigHandler { id: String, @@ -783,12 +805,24 @@ impl Deref for LoginConfigHandler { } } +/// Load [`PeerConfig`] from id. +/// +/// # Arguments +/// +/// * `id` - id of peer #[inline] pub fn load_config(id: &str) -> PeerConfig { PeerConfig::load(id) } impl LoginConfigHandler { + /// Initialize the login config handler. + /// + /// # Arguments + /// + /// * `id` - id of peer + /// * `is_file_transfer` - Whether the connection is file transfer. + /// * `is_port_forward` - Whether the connection is port forward. pub fn initialize(&mut self, id: String, is_file_transfer: bool, is_port_forward: bool) { self.id = id; self.is_file_transfer = is_file_transfer; @@ -798,6 +832,8 @@ impl LoginConfigHandler { self.config = config; } + /// Check if the client should auto login. + /// Return password if the client should auto login, otherwise return empty string. pub fn should_auto_login(&self) -> String { let l = self.lock_after_session_end; let a = !self.get_option("auto-login").is_empty(); @@ -809,27 +845,49 @@ impl LoginConfigHandler { } } + /// Load [`PeerConfig`]. fn load_config(&self) -> PeerConfig { load_config(&self.id) } + /// Save a [`PeerConfig`] into the handler. + /// + /// # Arguments + /// + /// * `config` - [`PeerConfig`] to save. pub fn save_config(&mut self, config: PeerConfig) { config.store(&self.id); self.config = config; } + /// Set an option for handler's [`PeerConfig`]. + /// + /// # Arguments + /// + /// * `k` - key of option + /// * `v` - value of option pub fn set_option(&mut self, k: String, v: String) { let mut config = self.load_config(); config.options.insert(k, v); self.save_config(config); } + /// Save view style to the current config. + /// + /// # Arguments + /// + /// * `value` - The view style to be saved. pub fn save_view_style(&mut self, value: String) { let mut config = self.load_config(); config.view_style = value; self.save_config(config); } + /// Toggle an option in the handler. + /// + /// # Arguments + /// + /// * `name` - The name of the option to toggle. pub fn toggle_option(&mut self, name: String) -> Option { let mut option = OptionMessage::default(); let mut config = self.load_config(); @@ -905,6 +963,12 @@ impl LoginConfigHandler { Some(msg_out) } + /// Get [`OptionMessage`] of the current [`LoginConfigHandler`]. + /// Return `None` if there's no option, for example, when the session is only for file transfer. + /// + /// # Arguments + /// + /// * `ignore_default` - If `true`, ignore the default value of the option. fn get_option_message(&self, ignore_default: bool) -> Option { if self.is_port_forward || self.is_file_transfer { return None; @@ -958,6 +1022,13 @@ impl LoginConfigHandler { } } + /// Parse the image quality option. + /// Return [`ImageQuality`] if the option is valid, otherwise return `None`. + /// + /// # Arguments + /// + /// * `q` - The image quality option. + /// * `ignore_default` - Ignore the default value. fn get_image_quality_enum(&self, q: &str, ignore_default: bool) -> Option { if q == "low" { Some(ImageQuality::Low) @@ -974,6 +1045,11 @@ impl LoginConfigHandler { } } + /// Get the status of a toggle option. + /// + /// # Arguments + /// + /// * `name` - The name of the toggle option. pub fn get_toggle_option(&self, name: &str) -> bool { if name == "show-remote-cursor" { self.config.show_remote_cursor @@ -992,6 +1068,7 @@ impl LoginConfigHandler { } } + /// Create a [`Message`] for refreshing video. pub fn refresh() -> Message { let mut misc = Misc::new(); misc.set_refresh_video(true); @@ -1000,6 +1077,12 @@ impl LoginConfigHandler { msg_out } + /// Create a [`Message`] for saving custom image quality. + /// + /// # Arguments + /// + /// * `bitrate` - The given bitrate. + /// * `quantizer` - The given quantizer. pub fn save_custom_image_quality(&mut self, bitrate: i32, quantizer: i32) -> Message { let mut misc = Misc::new(); misc.set_option(OptionMessage { @@ -1015,6 +1098,11 @@ impl LoginConfigHandler { msg_out } + /// Save the given image quality to the config. + /// Return a [`Message`] that contains image quality, or `None` if the image quality is not valid. + /// # Arguments + /// + /// * `value` - The image quality. pub fn save_image_quality(&mut self, value: String) -> Option { let mut res = None; if let Some(q) = self.get_image_quality_enum(&value, false) { @@ -1041,6 +1129,8 @@ impl LoginConfigHandler { } } + /// Handle login error. + /// Return true if the password is wrong, return false if there's an actual error. pub fn handle_login_error(&mut self, err: &str, interface: &impl Interface) -> bool { if err == "Wrong Password" { self.password = Default::default(); @@ -1052,6 +1142,12 @@ impl LoginConfigHandler { } } + /// Get user name. + /// Return the name of the given peer. If the peer has no name, return the name in the config. + /// + /// # Arguments + /// + /// * `pi` - peer info. pub fn get_username(&self, pi: &PeerInfo) -> String { return if pi.username.is_empty() { self.info.username.clone() @@ -1060,6 +1156,12 @@ impl LoginConfigHandler { }; } + /// Handle peer info. + /// + /// # Arguments + /// + /// * `username` - The name of the peer. + /// * `pi` - The peer info. pub fn handle_peer_info(&mut self, username: String, pi: PeerInfo) { if !pi.version.is_empty() { self.version = hbb_common::get_version_number(&pi.version); @@ -1109,6 +1211,7 @@ impl LoginConfigHandler { serde_json::to_string::>(&x).unwrap_or_default() } + /// Create a [`Message`] for login. fn create_login_msg(&self, password: Vec) -> Message { #[cfg(any(target_os = "android", target_os = "ios"))] let my_id = Config::get_id_or(crate::common::MOBILE_INFO1.lock().unwrap().clone()); @@ -1141,6 +1244,7 @@ impl LoginConfigHandler { } } +/// Media data. pub enum MediaData { VideoFrame(VideoFrame), AudioFrame(AudioFrame), @@ -1150,6 +1254,12 @@ pub enum MediaData { pub type MediaSender = mpsc::Sender; +/// Start video and audio thread. +/// Return two [`MediaSender`], they should be given to the media producer. +/// +/// # Arguments +/// +/// * `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, @@ -1204,6 +1314,12 @@ where return (video_sender, audio_sender); } +/// Handle latency test. +/// +/// # Arguments +/// +/// * `t` - The latency test message. +/// * `peer` - The peer. pub async fn handle_test_delay(t: TestDelay, peer: &mut Stream) { if !t.from_client { let mut msg_out = Message::new(); @@ -1212,9 +1328,21 @@ pub async fn handle_test_delay(t: TestDelay, peer: &mut Stream) { } } -// mask = buttons << 3 | type -// type, 1: down, 2: up, 3: wheel -// buttons, 1: left, 2: right, 4: middle +/// Send mouse data. +/// +/// # Arguments +/// +/// * `mask` - Mouse event. +/// * mask = buttons << 3 | type +/// * type, 1: down, 2: up, 3: wheel +/// * buttons, 1: left, 2: right, 4: middle +/// * `x` - X coordinate. +/// * `y` - Y coordinate. +/// * `alt` - Whether the alt key is pressed. +/// * `ctrl` - Whether the ctrl key is pressed. +/// * `shift` - Whether the shift key is pressed. +/// * `command` - Whether the command key is pressed. +/// * `interface` - The interface for sending data. #[inline] pub fn send_mouse( mask: i32, @@ -1249,6 +1377,11 @@ pub fn send_mouse( interface.send(Data::Message(msg_out)); } +/// Avtivate OS by sending mouse movement. +/// +/// # Arguments +/// +/// * `interface` - The interface for sending data. fn activate_os(interface: &impl Interface) { send_mouse(0, 0, 0, false, false, false, false, interface); std::thread::sleep(Duration::from_millis(50)); @@ -1267,12 +1400,26 @@ fn activate_os(interface: &impl Interface) { */ } +/// Input the OS's password. +/// +/// # Arguments +/// +/// * `p` - The password. +/// * `avtivate` - Whether to activate OS. +/// * `interface` - The interface for sending data. pub fn input_os_password(p: String, activate: bool, interface: impl Interface) { std::thread::spawn(move || { _input_os_password(p, activate, interface); }); } +/// Input the OS's password. +/// +/// # Arguments +/// +/// * `p` - The password. +/// * `avtivate` - Whether to activate OS. +/// * `interface` - The interface for sending data. fn _input_os_password(p: String, activate: bool, interface: impl Interface) { if activate { activate_os(&interface); @@ -1289,6 +1436,15 @@ fn _input_os_password(p: String, activate: bool, interface: impl Interface) { interface.send(Data::Message(msg_out)); } +/// Handle hash message sent by peer. +/// Hash will be used for login. +/// +/// # Arguments +/// +/// * `lc` - Login config. +/// * `hash` - Hash sent by peer. +/// * `interface` - [`Interface`] for sending data. +/// * `peer` - [`Stream`] for communicating with peer. pub async fn handle_hash( lc: Arc>, hash: Hash, @@ -1312,11 +1468,26 @@ pub async fn handle_hash( lc.write().unwrap().hash = hash; } +/// Send login message to peer. +/// +/// # Arguments +/// +/// * `lc` - Login config. +/// * `password` - Password. +/// * `peer` - [`Stream`] for communicating with peer. async fn send_login(lc: Arc>, password: Vec, peer: &mut Stream) { let msg_out = lc.read().unwrap().create_login_msg(password); allow_err!(peer.send(&msg_out).await); } +/// Handle login request made from ui. +/// +/// # Arguments +/// +/// * `lc` - Login config. +/// * `password` - Password. +/// * `remember` - Whether to remember password. +/// * `peer` - [`Stream`] for communicating with peer. pub async fn handle_login_from_ui( lc: Arc>, password: String, @@ -1335,6 +1506,7 @@ pub async fn handle_login_from_ui( send_login(lc.clone(), hasher2.finalize()[..].into(), peer).await; } +/// Interface for client to send data and commands. #[async_trait] pub trait Interface: Send + Clone + 'static + Sized { fn send(&self, data: Data); @@ -1346,6 +1518,7 @@ pub trait Interface: Send + Clone + 'static + Sized { async fn handle_test_delay(&mut self, t: TestDelay, peer: &mut Stream); } +/// Data used by the client interface. #[derive(Clone)] pub enum Data { Close, @@ -1368,6 +1541,7 @@ pub enum Data { ResumeJob((i32, bool)), } +/// Keycode for key events. #[derive(Clone)] pub enum Key { ControlKey(ControlKey), @@ -1498,6 +1672,13 @@ lazy_static::lazy_static! { ].iter().cloned().collect(); } +/// Check if the given message is an error and can be retried. +/// +/// # Arguments +/// +/// * `msgtype` - The message type. +/// * `title` - The title of the message. +/// * `text` - The text of the message. #[inline] pub fn check_if_retry(msgtype: &str, title: &str, text: &str) -> bool { msgtype == "error" diff --git a/src/client/helper.rs b/src/client/helper.rs index abd20d312..b29930e1c 100644 --- a/src/client/helper.rs +++ b/src/client/helper.rs @@ -8,8 +8,8 @@ use hbb_common::log; const MAX_LATENCY: i64 = 500; const MIN_LATENCY: i64 = 100; -// based on video frame time, fix audio latency relatively. -// only works on audio, can't fix video latency. +/// Latency controller for syncing audio with the video stream. +/// Only sync the audio to video, not the other way around. #[derive(Debug)] pub struct LatencyController { last_video_remote_ts: i64, // generated on remote deivce @@ -28,21 +28,23 @@ impl Default for LatencyController { } impl LatencyController { + /// Create a new latency controller. pub fn new() -> Arc> { Arc::new(Mutex::new(LatencyController::default())) } - // first, receive new video frame and update time + /// Update the latency controller with the latest video timestamp. pub fn update_video(&mut self, timestamp: i64) { self.last_video_remote_ts = timestamp; self.update_time = Instant::now(); } - // second, compute audio latency - // set MAX and MIN, avoid fixing too frequently. + /// Check if the audio should be played based on the current latency. 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; + // Set MAX and MIN, avoid fixing too frequently. if self.allow_audio { if latency.abs() > MAX_LATENCY { log::debug!("LATENCY > {}ms cut off, latency:{}", MAX_LATENCY, latency); diff --git a/src/flutter.rs b/src/flutter.rs index 80dd1f807..e40084450 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -4,8 +4,12 @@ use hbb_common::{ allow_err, compress::decompress, config::{Config, LocalConfig}, - fs, log, - fs::{can_enable_overwrite_detection, new_send_confirm, DigestCheckResult, get_string, transform_windows_path}, + fs, + fs::{ + can_enable_overwrite_detection, get_string, new_send_confirm, transform_windows_path, + DigestCheckResult, + }, + log, message_proto::*, protobuf::Message as _, rendezvous_proto::ConnType, @@ -36,6 +40,12 @@ pub struct Session { } impl Session { + /// Create a new remote session with the given id. + /// + /// # Arguments + /// + /// * `id` - The id of the remote session. + /// * `is_file_transfer` - If the session is used for file transfer. pub fn start(id: &str, is_file_transfer: bool) { LocalConfig::set_remote_id(id); Self::close(); @@ -52,10 +62,16 @@ impl Session { }); } + /// Get the current session instance. pub fn get() -> Arc>> { SESSION.clone() } + /// Get the option of the current session. + /// + /// # Arguments + /// + /// * `name` - The name of the option to get. Currently only `remote_dir` is supported. pub fn get_option(name: &str) -> String { if let Some(session) = SESSION.read().unwrap().as_ref() { if name == "remote_dir" { @@ -66,6 +82,12 @@ impl Session { "".to_owned() } + /// Set the option of the current session. + /// + /// # Arguments + /// + /// * `name` - The name of the option to set. Currently only `remote_dir` is supported. + /// * `value` - The value of the option to set. pub fn set_option(name: String, value: String) { if let Some(session) = SESSION.read().unwrap().as_ref() { let mut value = value; @@ -76,18 +98,25 @@ impl Session { } } + /// Input the OS password. pub fn input_os_password(pass: String, activate: bool) { if let Some(session) = SESSION.read().unwrap().as_ref() { input_os_password(pass, activate, session.clone()); } } + /// Send message to the remote session. + /// + /// # Arguments + /// + /// * `data` - The data to send. See [`Data`] for more details. fn send(data: Data) { if let Some(session) = SESSION.read().unwrap().as_ref() { session.send(data); } } + /// Pop a event from the event queue. pub fn pop_event() -> Option { if let Some(session) = SESSION.read().unwrap().as_ref() { session.events2ui.write().unwrap().pop_front() @@ -96,6 +125,7 @@ impl Session { } } + /// Toggle an option. pub fn toggle_option(name: &str) { if let Some(session) = SESSION.read().unwrap().as_ref() { let msg = session.lc.write().unwrap().toggle_option(name.to_owned()); @@ -105,10 +135,12 @@ impl Session { } } + /// Send a refresh command. pub fn refresh() { Self::send(Data::Message(LoginConfigHandler::refresh())); } + /// Get image quality. pub fn get_image_quality() -> String { if let Some(session) = SESSION.read().unwrap().as_ref() { session.lc.read().unwrap().image_quality.clone() @@ -117,6 +149,7 @@ impl Session { } } + /// Set image quality. pub fn set_image_quality(value: &str) { if let Some(session) = SESSION.read().unwrap().as_ref() { let msg = session @@ -130,6 +163,12 @@ impl Session { } } + /// Get the status of a toggle option. + /// Return `None` if the option is not found. + /// + /// # Arguments + /// + /// * `name` - The name of the option to get. pub fn get_toggle_option(name: &str) -> Option { if let Some(session) = SESSION.read().unwrap().as_ref() { Some(session.lc.write().unwrap().get_toggle_option(name)) @@ -138,15 +177,23 @@ impl Session { } } + /// Login. + /// + /// # Arguments + /// + /// * `password` - The password to login. + /// * `remember` - If the password should be remembered. pub fn login(password: &str, remember: bool) { Session::send(Data::Login((password.to_owned(), remember))); } + /// Close the session. pub fn close() { Session::send(Data::Close); SESSION.write().unwrap().take(); } + /// Reconnect to the current session. pub fn reconnect() { if let Some(session) = SESSION.read().unwrap().as_ref() { if let Some(sender) = session.sender.read().unwrap().as_ref() { @@ -159,6 +206,7 @@ impl Session { } } + /// Get `remember` flag in [`LoginConfigHandler`]. pub fn get_remember() -> bool { if let Some(session) = SESSION.read().unwrap().as_ref() { session.lc.read().unwrap().remember @@ -167,6 +215,11 @@ impl Session { } } + /// Send message over the current session. + /// + /// # Arguments + /// + /// * `msg` - The message to send. #[inline] pub fn send_msg(&self, msg: Message) { if let Some(sender) = self.sender.read().unwrap().as_ref() { @@ -174,6 +227,11 @@ impl Session { } } + /// Send chat message over the current session. + /// + /// # Arguments + /// + /// * `text` - The message to send. pub fn send_chat(text: String) { let mut misc = Misc::new(); misc.set_chat_message(ChatMessage { @@ -185,6 +243,7 @@ impl Session { Self::send_msg_static(msg_out); } + /// Send file over the current session. pub fn send_files( id: i32, path: String, @@ -198,6 +257,7 @@ impl Session { } } + /// Confirm file override. pub fn set_confirm_override_file( id: i32, file_num: i32, @@ -225,6 +285,11 @@ impl Session { } } + /// Static method to send message over the current session. + /// + /// # Arguments + /// + /// * `msg` - The message to send. #[inline] pub fn send_msg_static(msg: Message) { if let Some(session) = SESSION.read().unwrap().as_ref() { @@ -232,6 +297,13 @@ impl Session { } } + /// Push an event to the event queue. + /// An event is stored as json in the event queue. + /// + /// # Arguments + /// + /// * `name` - The name of the event. + /// * `event` - Fields of the event content. fn push_event(&self, name: &str, event: Vec<(&str, &str)>) { let mut h: HashMap<&str, &str> = event.iter().cloned().collect(); assert!(h.get("name").is_none()); @@ -242,11 +314,13 @@ impl Session { }; } + /// Get platform of peer. #[inline] fn peer_platform(&self) -> String { self.lc.read().unwrap().info.platform.clone() } + /// Quick method for sending a ctrl_alt_del command. pub fn ctrl_alt_del() { if let Some(session) = SESSION.read().unwrap().as_ref() { if session.peer_platform() == "Windows" { @@ -259,6 +333,11 @@ impl Session { } } + /// Switch the display. + /// + /// # Arguments + /// + /// * `display` - The display to switch to. pub fn switch_display(display: i32) { let mut misc = Misc::new(); misc.set_switch_display(SwitchDisplay { @@ -270,6 +349,7 @@ impl Session { Self::send_msg_static(msg_out); } + /// Send lock screen command. pub fn lock_screen() { if let Some(session) = SESSION.read().unwrap().as_ref() { let k = Key::ControlKey(ControlKey::LockScreen); @@ -277,6 +357,17 @@ impl Session { } } + /// Send key input command. + /// + /// # Arguments + /// + /// * `name` - The name of the key. + /// * `down` - Whether the key is down or up. + /// * `press` - If the key is simply being pressed(Down+Up). + /// * `alt` - If the alt key is also pressed. + /// * `ctrl` - If the ctrl key is also pressed. + /// * `shift` - If the shift key is also pressed. + /// * `command` - If the command key is also pressed. pub fn input_key( name: &str, down: bool, @@ -299,6 +390,12 @@ impl Session { } } + /// Input a string of text. + /// String is parsed into individual key presses. + /// + /// # Arguments + /// + /// * `value` - The text to input. pub fn input_string(value: &str) { let mut key_event = KeyEvent::new(); key_event.set_seq(value.to_owned()); @@ -499,6 +596,12 @@ struct Connection { } impl Connection { + /// Create a new connection. + /// + /// # Arguments + /// + /// * `session` - The session to create a new connection for. + /// * `is_file_transfer` - Whether the connection is for file transfer. #[tokio::main(flavor = "current_thread")] async fn start(session: Session, is_file_transfer: bool) { let mut last_recv_time = Instant::now(); @@ -591,6 +694,10 @@ impl Connection { } } + /// Handle message from peer. + /// Return false if the connection should be closed. + /// + /// The message is handled by [`Message`], see [`message::Union`] for possible types. async fn handle_msg_from_peer(&mut self, data: &[u8], peer: &mut Stream) -> bool { if let Ok(msg_in) = Message::parse_from_bytes(&data) { match msg_in.union { @@ -1144,6 +1251,7 @@ impl Connection { } } +/// Parse [`FileDirectory`] to json. pub fn make_fd_to_json(fd: FileDirectory) -> String { use serde_json::json; let mut fd_json = serde_json::Map::new(); diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 2e62bdf69..98fac8242 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -47,6 +47,13 @@ pub fn start_rgba_stream(s: StreamSink>>) -> ResultType<( Ok(()) } +/// FFI for **get** commands which are idempotent. +/// Return result in c string. +/// +/// # Arguments +/// +/// * `name` - name of the command +/// * `arg` - argument of the command #[no_mangle] unsafe extern "C" fn get_by_name(name: *const c_char, arg: *const c_char) -> *const c_char { let mut res = "".to_owned(); @@ -174,6 +181,12 @@ unsafe extern "C" fn get_by_name(name: *const c_char, arg: *const c_char) -> *co CString::from_vec_unchecked(res.into_bytes()).into_raw() } +/// FFI for **set** commands which are not idempotent. +/// +/// # Arguments +/// +/// * `name` - name of the command +/// * `arg` - argument of the command #[no_mangle] unsafe extern "C" fn set_by_name(name: *const c_char, value: *const c_char) { let value: &CStr = CStr::from_ptr(value); From c4639ecfcbfb7a379479fdc3224202cb3693eb19 Mon Sep 17 00:00:00 2001 From: SoLongAndThanksForAllThePizza <103753680+SoLongAndThanksForAllThePizza@users.noreply.github.com> Date: Sat, 28 May 2022 03:57:34 +0800 Subject: [PATCH 0023/2015] add connection page --- flutter/lib/desktop/pages/desktop_home_page.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 9ed485df8..00b071ec5 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/mobile/pages/connection_page.dart'; import 'package:flutter_hbb/models/model.dart'; import 'package:provider/provider.dart'; @@ -42,7 +43,7 @@ class _DesktopHomePageState extends State { buildServerBoard(BuildContext context) { return Center( - child: Text("waiting implementation"), + child: ConnectionPage(key: null), ); } From e836b7fcfb3644055425553213d7f962550af82e Mon Sep 17 00:00:00 2001 From: SoLongAndThanksForAllThePizza <103753680+SoLongAndThanksForAllThePizza@users.noreply.github.com> Date: Sun, 29 May 2022 04:39:12 +0800 Subject: [PATCH 0024/2015] implement functional draft version --- .../lib/desktop/pages/connection_page.dart | 355 ++++++++++++++++++ .../lib/desktop/pages/desktop_home_page.dart | 10 +- flutter/lib/models/native_model.dart | 2 +- flutter/lib/models/server_model.dart | 3 + src/flutter_ffi.rs | 6 +- src/server.rs | 14 + 6 files changed, 385 insertions(+), 5 deletions(-) create mode 100644 flutter/lib/desktop/pages/connection_page.dart diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart new file mode 100644 index 000000000..4fb65c2e3 --- /dev/null +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -0,0 +1,355 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/mobile/pages/file_manager_page.dart'; +import 'package:provider/provider.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'dart:async'; +import '../../common.dart'; +import '../../models/model.dart'; +import '../../mobile/pages/home_page.dart'; +import '../../mobile/pages/remote_page.dart'; +import '../../mobile/pages/settings_page.dart'; +import '../../mobile/pages/scan_page.dart'; +import '../../models/server_model.dart'; + +/// Connection page for connecting to a remote peer. +class ConnectionPage extends StatefulWidget implements PageShape { + ConnectionPage({Key? key}) : super(key: key); + + @override + final icon = Icon(Icons.connected_tv); + + @override + final title = translate("Connection"); + + @override + final appBarActions = !isAndroid ? [WebMenu()] : []; + + @override + _ConnectionPageState createState() => _ConnectionPageState(); +} + +/// State for the connection page. +class _ConnectionPageState extends State { + /// Controller for the id input bar. + final _idController = TextEditingController(); + + /// Update url. If it's not null, means an update is available. + var _updateUrl = ''; + var _menuPos; + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + Provider.of(context); + if (_idController.text.isEmpty) _idController.text = FFI.getId(); + FFI.serverModel.startService(); + return SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + getUpdateUI(), + getSearchBarUI(), + Container(height: 12), + getPeers(), + ]), + ); + } + + /// Callback for the connect button. + /// Connects to the selected peer. + void onConnect() { + var id = _idController.text.trim(); + connect(id); + } + + /// Connect to a peer with [id]. + /// If [isFileTransfer], starts a session only for file transfer. + void connect(String id, {bool isFileTransfer = false}) async { + if (id == '') return; + id = id.replaceAll(' ', ''); + if (isFileTransfer) { + if (!await PermissionManager.check("file")) { + if (!await PermissionManager.request("file")) { + return; + } + } + Navigator.push( + context, + MaterialPageRoute( + builder: (BuildContext context) => FileManagerPage(id: id), + ), + ); + } else { + Navigator.push( + context, + MaterialPageRoute( + builder: (BuildContext context) => RemotePage(id: id), + ), + ); + } + FocusScopeNode currentFocus = FocusScope.of(context); + if (!currentFocus.hasPrimaryFocus) { + currentFocus.unfocus(); + } + } + + /// UI for software update. + /// If [_updateUrl] is not empty, shows a button to update the software. + Widget getUpdateUI() { + return _updateUrl.isEmpty + ? SizedBox(height: 0) + : InkWell( + onTap: () async { + final url = _updateUrl + '.apk'; + if (await canLaunch(url)) { + await launch(url); + } + }, + child: Container( + alignment: AlignmentDirectional.center, + width: double.infinity, + color: Colors.pinkAccent, + padding: EdgeInsets.symmetric(vertical: 12), + child: Text(translate('Download new version'), + style: TextStyle( + color: Colors.white, fontWeight: FontWeight.bold)))); + } + + /// UI for the search bar. + /// Search for a peer and connect to it if the id exists. + Widget getSearchBarUI() { + var w = Padding( + padding: const EdgeInsets.fromLTRB(16.0, 8.0, 16.0, 0.0), + child: Container( + height: 84, + child: Padding( + padding: const EdgeInsets.only(top: 8, bottom: 8), + child: Ink( + decoration: BoxDecoration( + color: MyTheme.white, + borderRadius: const BorderRadius.all(Radius.circular(13)), + ), + child: Row( + children: [ + Expanded( + child: Container( + padding: const EdgeInsets.only(left: 16, right: 16), + child: TextField( + autocorrect: false, + enableSuggestions: false, + keyboardType: TextInputType.visiblePassword, + // keyboardType: TextInputType.number, + style: TextStyle( + fontFamily: 'WorkSans', + fontWeight: FontWeight.bold, + fontSize: 30, + color: MyTheme.idColor, + ), + decoration: InputDecoration( + labelText: translate('Remote ID'), + // hintText: 'Enter your remote ID', + border: InputBorder.none, + helperStyle: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + color: MyTheme.darkGray, + ), + labelStyle: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 16, + letterSpacing: 0.2, + color: MyTheme.darkGray, + ), + ), + controller: _idController, + ), + ), + ), + SizedBox( + width: 60, + height: 60, + child: IconButton( + icon: Icon(Icons.arrow_forward, + color: MyTheme.darkGray, size: 45), + onPressed: onConnect, + ), + ), + ], + ), + ), + ), + ), + ); + return Center( + child: Container(constraints: BoxConstraints(maxWidth: 600), child: w)); + } + + @override + void dispose() { + _idController.dispose(); + super.dispose(); + } + + /// Get the image for the current [platform]. + Widget getPlatformImage(String platform) { + platform = platform.toLowerCase(); + if (platform == 'mac os') + platform = 'mac'; + else if (platform != 'linux' && platform != 'android') platform = 'win'; + return Image.asset('assets/$platform.png', width: 24, height: 24); + } + + /// Get all the saved peers. + Widget getPeers() { + final size = MediaQuery.of(context).size; + final space = 8.0; + var width = size.width - 2 * space; + final minWidth = 320.0; + if (size.width > minWidth + 2 * space) { + final n = (size.width / (minWidth + 2 * space)).floor(); + width = size.width / n - 2 * space; + } + final cards = []; + var peers = FFI.peers(); + peers.forEach((p) { + cards.add(Container( + width: width, + child: Card( + child: GestureDetector( + onTap: !isWebDesktop ? () => connect('${p.id}') : null, + onDoubleTap: isWebDesktop ? () => connect('${p.id}') : null, + onLongPressStart: (details) { + final x = details.globalPosition.dx; + final y = details.globalPosition.dy; + _menuPos = RelativeRect.fromLTRB(x, y, x, y); + showPeerMenu(context, p.id); + }, + child: ListTile( + contentPadding: const EdgeInsets.only(left: 12), + subtitle: Text('${p.username}@${p.hostname}'), + title: Text('${p.id}'), + leading: Container( + padding: const EdgeInsets.all(6), + child: getPlatformImage('${p.platform}'), + color: str2color('${p.id}${p.platform}', 0x7f)), + trailing: InkWell( + child: Padding( + padding: const EdgeInsets.all(12), + child: Icon(Icons.more_vert)), + onTapDown: (e) { + final x = e.globalPosition.dx; + final y = e.globalPosition.dy; + _menuPos = RelativeRect.fromLTRB(x, y, x, y); + }, + onTap: () { + showPeerMenu(context, p.id); + }), + ))))); + }); + return Wrap(children: cards, spacing: space, runSpacing: space); + } + + /// Show the peer menu and handle user's choice. + /// User might remove the peer or send a file to the peer. + void showPeerMenu(BuildContext context, String id) async { + var value = await showMenu( + context: context, + position: this._menuPos, + items: [ + PopupMenuItem( + child: Text(translate('Remove')), value: 'remove') + ] + + (!isAndroid + ? [] + : [ + PopupMenuItem( + child: Text(translate('File transfer')), value: 'file') + ]), + elevation: 8, + ); + if (value == 'remove') { + setState(() => FFI.setByName('remove', '$id')); + () async { + removePreference(id); + }(); + } else if (value == 'file') { + connect(id, isFileTransfer: true); + } + } +} + +class WebMenu extends StatefulWidget { + @override + _WebMenuState createState() => _WebMenuState(); +} + +class _WebMenuState extends State { + @override + Widget build(BuildContext context) { + Provider.of(context); + final username = getUsername(); + return PopupMenuButton( + icon: Icon(Icons.more_vert), + itemBuilder: (context) { + return (isIOS + ? [ + PopupMenuItem( + child: Icon(Icons.qr_code_scanner, color: Colors.black), + value: "scan", + ) + ] + : >[]) + + [ + PopupMenuItem( + child: Text(translate('ID/Relay Server')), + value: "server", + ) + ] + + (getUrl().contains('admin.rustdesk.com') + ? >[] + : [ + PopupMenuItem( + child: Text(username == null + ? translate("Login") + : translate("Logout") + ' ($username)'), + value: "login", + ) + ]) + + [ + PopupMenuItem( + child: Text(translate('About') + ' RustDesk'), + value: "about", + ) + ]; + }, + onSelected: (value) { + if (value == 'server') { + showServerSettings(); + } + if (value == 'about') { + showAbout(); + } + if (value == 'login') { + if (username == null) { + showLogin(); + } else { + logout(); + } + } + if (value == 'scan') { + Navigator.push( + context, + MaterialPageRoute( + builder: (BuildContext context) => ScanPage(), + ), + ); + } + }); + } +} diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 00b071ec5..467f85cc1 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; -import 'package:flutter_hbb/mobile/pages/connection_page.dart'; +import 'package:flutter_hbb/desktop/pages/connection_page.dart'; import 'package:flutter_hbb/models/model.dart'; import 'package:provider/provider.dart'; @@ -76,6 +76,14 @@ class _DesktopHomePageState extends State { TextFormField( controller: model.serverId, ), + Text( + translate("Password"), + style: + TextStyle(fontSize: 18, fontWeight: FontWeight.w500), + ), + TextField( + controller: model.serverPasswd, + ) ], ), ), diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index f9135a06c..a8803a8f8 100644 --- a/flutter/lib/models/native_model.dart +++ b/flutter/lib/models/native_model.dart @@ -102,7 +102,7 @@ class PlatformFFI { name = '${androidInfo.brand}-${androidInfo.model}'; id = androidInfo.id.hashCode.toString(); androidVersion = androidInfo.version.sdkInt; - } else { + } else if (Platform.isIOS) { IosDeviceInfo iosInfo = await deviceInfo.iosInfo; name = iosInfo.utsname.machine; id = iosInfo.identifierForVendor.hashCode.toString(); diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index 681ff3c25..68d3d2391 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -150,6 +150,7 @@ class ServerModel with ChangeNotifier { } } + /// Toggle the screen sharing service. toggleService() async { if (_isStart) { final res = @@ -198,6 +199,7 @@ class ServerModel with ChangeNotifier { } } + /// Start the screen sharing service. Future startService() async { _isStart = true; notifyListeners(); @@ -212,6 +214,7 @@ class ServerModel with ChangeNotifier { } } + /// Stop the screen sharing service. Future stopService() async { _isStart = false; FFI.serverModel.closeAll(); diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 98fac8242..8344cae99 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1,6 +1,7 @@ use crate::client::file_trait::FileManager; use crate::flutter::connection_manager::{self, get_clients_length, get_clients_state}; use crate::flutter::{self, make_fd_to_json, Session}; +use crate::start_server; use crate::ui_interface; use flutter_rust_bridge::{StreamSink, ZeroCopyBuffer}; use hbb_common::ResultType; @@ -49,7 +50,7 @@ pub fn start_rgba_stream(s: StreamSink>>) -> ResultType<( /// FFI for **get** commands which are idempotent. /// Return result in c string. -/// +/// /// # Arguments /// /// * `name` - name of the command @@ -515,10 +516,9 @@ unsafe extern "C" fn set_by_name(name: *const c_char, value: *const c_char) { Config::set_option("stop-service".into(), "Y".into()); crate::rendezvous_mediator::RendezvousMediator::restart(); } - #[cfg(target_os = "android")] "start_service" => { Config::set_option("stop-service".into(), "".into()); - crate::rendezvous_mediator::RendezvousMediator::restart(); + start_server(false); } #[cfg(target_os = "android")] "close_conn" => { diff --git a/src/server.rs b/src/server.rs index f4758e3fb..0782c7231 100644 --- a/src/server.rs +++ b/src/server.rs @@ -287,12 +287,26 @@ pub fn check_zombie() { }); } +/// Start the host server that allows the remote peer to control the current machine. +/// +/// # Arguments +/// +/// * `is_server` - Whether the current client is definitely the server. +/// If true, the server will be started. +/// Otherwise, client will check if there's already a server and start one if not. #[cfg(any(target_os = "android", target_os = "ios"))] #[tokio::main] pub async fn start_server(is_server: bool) { crate::RendezvousMediator::start_all().await; } +/// Start the host server that allows the remote peer to control the current machine. +/// +/// # Arguments +/// +/// * `is_server` - Whether the current client is definitely the server. +/// If true, the server will be started. +/// Otherwise, client will check if there's already a server and start one if not. #[cfg(not(any(target_os = "android", target_os = "ios")))] #[tokio::main] pub async fn start_server(is_server: bool) { From 59a8600b533fca060756769a10aa019f2808405d Mon Sep 17 00:00:00 2001 From: SoLongAndThanksForAllThePizza <103753680+SoLongAndThanksForAllThePizza@users.noreply.github.com> Date: Sun, 29 May 2022 15:18:36 +0800 Subject: [PATCH 0025/2015] fix flutter ffi init for all platforms --- src/flutter_ffi.rs | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 8344cae99..398bd11c6 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -18,6 +18,14 @@ use std::{ fn initialize(app_dir: &str) { *config::APP_DIR.write().unwrap() = app_dir.to_owned(); + #[cfg(feature = "cli")] + { + #[cfg(any(target_os = "android", target_os = "ios"))] + { + crate::common::test_rendezvous_server(); + crate::common::test_nat_type(); + } + } #[cfg(target_os = "android")] { android_logger::init_once( @@ -31,11 +39,15 @@ fn initialize(app_dir: &str) { use hbb_common::env_logger::*; init_from_env(Env::default().filter_or(DEFAULT_FILTER_ENV, "debug")); } - #[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))] - crate::common::test_rendezvous_server(); - crate::common::test_nat_type(); #[cfg(target_os = "android")] - crate::common::check_software_update(); + { + crate::common::check_software_update(); + } + #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + { + use hbb_common::env_logger::*; + init_from_env(Env::default().filter_or(DEFAULT_FILTER_ENV, "debug")); + } } pub fn start_event_stream(s: StreamSink) -> ResultType<()> { From 24a6846f03eaf68dc3984442d37e9d9c3f4add6a Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sun, 29 May 2022 10:25:36 +0800 Subject: [PATCH 0026/2015] add: desktop password page --- .../lib/desktop/pages/desktop_home_page.dart | 191 +++++++++++++----- .../desktop/pages/desktop_remote_page.dart | 16 ++ 2 files changed, 156 insertions(+), 51 deletions(-) create mode 100644 flutter/lib/desktop/pages/desktop_remote_page.dart diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 467f85cc1..ad27a6d3b 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -22,6 +22,9 @@ class _DesktopHomePageState extends State { child: buildServerInfo(context), flex: 1, ), + SizedBox( + width: 16.0, + ), Flexible( child: buildServerBoard(context), flex: 4, @@ -35,62 +38,148 @@ class _DesktopHomePageState extends State { buildServerInfo(BuildContext context) { return ChangeNotifierProvider.value( value: FFI.serverModel, - child: Column( - children: [buildIDBoard(context)], - ), - ); - } - - buildServerBoard(BuildContext context) { - return Center( - child: ConnectionPage(key: null), - ); - } - - buildIDBoard(BuildContext context) { - final model = FFI.serverModel; - return Card( - elevation: 0.5, child: Container( - margin: EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.baseline, - textBaseline: TextBaseline.alphabetic, + decoration: BoxDecoration(color: MyTheme.white), + child: Column( children: [ - Container( - width: 4, - height: 70, - decoration: BoxDecoration(color: MyTheme.accent), - ), - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - translate("ID"), - style: - TextStyle(fontSize: 18, fontWeight: FontWeight.w500), - ), - TextFormField( - controller: model.serverId, - ), - Text( - translate("Password"), - style: - TextStyle(fontSize: 18, fontWeight: FontWeight.w500), - ), - TextField( - controller: model.serverPasswd, - ) - ], - ), - ), - ), + buildTip(context), + buildIDBoard(context), + buildPasswordBoard(context), ], ), ), ); } + + buildServerBoard(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + buildControlPanel(context), + buildRecentSession(context), + ConnectionPage() + ], + ); + } + + buildIDBoard(BuildContext context) { + final model = FFI.serverModel; + return Container( + margin: EdgeInsets.symmetric(vertical: 4.0, horizontal: 16.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + children: [ + Container( + width: 3, + height: 70, + decoration: BoxDecoration(color: MyTheme.accent), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + translate("ID"), + style: TextStyle(fontSize: 18, fontWeight: FontWeight.w500), + ), + TextFormField( + controller: model.serverId, + ), + ], + ), + ), + ), + ], + ), + ); + } + + buildPasswordBoard(BuildContext context) { + final model = FFI.serverModel; + return Container( + margin: EdgeInsets.symmetric(vertical: 4.0, horizontal: 16.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + children: [ + Container( + width: 3, + height: 70, + decoration: BoxDecoration(color: MyTheme.accent), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + translate("Password"), + style: TextStyle(fontSize: 18, fontWeight: FontWeight.w500), + ), + TextFormField( + controller: model.serverPasswd, + ), + ], + ), + ), + ), + ], + ), + ); + } + + buildTip(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + translate("Your Desktop"), + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20), + ), + SizedBox( + height: 8.0, + ), + Text( + translate("desk_tip"), + overflow: TextOverflow.clip, + style: TextStyle(fontSize: 14), + ) + ], + ), + ); + } + + buildControlPanel(BuildContext context) { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), color: MyTheme.white), + padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(translate("Control Remote Desktop")), + Form( + child: Column( + children: [ + TextFormField( + controller: TextEditingController(), + inputFormatters: [], + ) + ], + )) + ], + ), + ); + } + + buildRecentSession(BuildContext context) { + return Center(child: Text("waiting implementation")); + } } diff --git a/flutter/lib/desktop/pages/desktop_remote_page.dart b/flutter/lib/desktop/pages/desktop_remote_page.dart new file mode 100644 index 000000000..748e4cf3c --- /dev/null +++ b/flutter/lib/desktop/pages/desktop_remote_page.dart @@ -0,0 +1,16 @@ +import 'package:flutter/material.dart'; + +/// Remote Page, use it in multi window context +class DesktopRemotePage extends StatefulWidget { + const DesktopRemotePage({Key? key}) : super(key: key); + + @override + State createState() => _DesktopRemotePageState(); +} + +class _DesktopRemotePageState extends State { + @override + Widget build(BuildContext context) { + return Container(); + } +} From 708801bdf623f0fc1d6a69aeaa1bc0610f78835d Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sun, 29 May 2022 17:19:50 +0800 Subject: [PATCH 0027/2015] feat: add single/multi window manager wrapper & fix issue causing input twice --- .../lib/common/formatter/id_formatter.dart | 4 + .../lib/desktop/pages/connection_page.dart | 27 +- .../desktop/pages/connection_tab_page.dart | 66 + .../lib/desktop/pages/desktop_home_page.dart | 4 +- flutter/lib/desktop/pages/remote_page.dart | 1364 +++++++++++++++++ .../desktop/screen/desktop_remote_screen.dart | 46 + flutter/lib/main.dart | 63 +- flutter/lib/mobile/pages/remote_page.dart | 250 +-- flutter/lib/models/model.dart | 59 +- flutter/lib/utils/multi_window_manager.dart | 93 ++ flutter/pubspec.lock | 14 + flutter/pubspec.yaml | 2 + 12 files changed, 1817 insertions(+), 175 deletions(-) create mode 100644 flutter/lib/common/formatter/id_formatter.dart create mode 100644 flutter/lib/desktop/pages/connection_tab_page.dart create mode 100644 flutter/lib/desktop/pages/remote_page.dart create mode 100644 flutter/lib/desktop/screen/desktop_remote_screen.dart create mode 100644 flutter/lib/utils/multi_window_manager.dart diff --git a/flutter/lib/common/formatter/id_formatter.dart b/flutter/lib/common/formatter/id_formatter.dart new file mode 100644 index 000000000..29aea84ff --- /dev/null +++ b/flutter/lib/common/formatter/id_formatter.dart @@ -0,0 +1,4 @@ +import 'package:flutter/material.dart'; + +/// TODO: Divide every 3 number to display ID +class IdFormController extends TextEditingController {} diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 4fb65c2e3..6f0a8115a 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -1,15 +1,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_hbb/mobile/pages/file_manager_page.dart'; +import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; -import 'dart:async'; + import '../../common.dart'; -import '../../models/model.dart'; import '../../mobile/pages/home_page.dart'; -import '../../mobile/pages/remote_page.dart'; -import '../../mobile/pages/settings_page.dart'; import '../../mobile/pages/scan_page.dart'; -import '../../models/server_model.dart'; +import '../../mobile/pages/settings_page.dart'; +import '../../models/model.dart'; /// Connection page for connecting to a remote peer. class ConnectionPage extends StatefulWidget implements PageShape { @@ -46,7 +45,6 @@ class _ConnectionPageState extends State { Widget build(BuildContext context) { Provider.of(context); if (_idController.text.isEmpty) _idController.text = FFI.getId(); - FFI.serverModel.startService(); return SingleChildScrollView( child: Column( mainAxisAlignment: MainAxisAlignment.start, @@ -55,7 +53,7 @@ class _ConnectionPageState extends State { children: [ getUpdateUI(), getSearchBarUI(), - Container(height: 12), + SizedBox(height: 12), getPeers(), ]), ); @@ -86,12 +84,15 @@ class _ConnectionPageState extends State { ), ); } else { - Navigator.push( - context, - MaterialPageRoute( - builder: (BuildContext context) => RemotePage(id: id), - ), - ); + // single window + // Navigator.push( + // context, + // MaterialPageRoute( + // builder: (BuildContext context) => RemotePage(id: id), + // ), + // ); + // multi window + await rustDeskWinManager.new_remote_desktop(id); } FocusScopeNode currentFocus = FocusScope.of(context); if (!currentFocus.hasPrimaryFocus) { diff --git a/flutter/lib/desktop/pages/connection_tab_page.dart b/flutter/lib/desktop/pages/connection_tab_page.dart new file mode 100644 index 000000000..ca53224f1 --- /dev/null +++ b/flutter/lib/desktop/pages/connection_tab_page.dart @@ -0,0 +1,66 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/desktop/pages/remote_page.dart'; +import 'package:flutter_hbb/models/model.dart'; +import 'package:flutter_hbb/utils/multi_window_manager.dart'; + +class ConnectionTabPage extends StatefulWidget { + final Map params; + + const ConnectionTabPage({Key? key, required this.params}) : super(key: key); + + @override + State createState() => _ConnectionTabPageState(params); +} + +class _ConnectionTabPageState extends State + with SingleTickerProviderStateMixin { + // refactor List when using multi-tab + // this singleton is only for test + late String connectionId; + late TabController tabController; + + _ConnectionTabPageState(Map params) { + connectionId = params['id'] ?? ""; + } + + @override + void initState() { + super.initState(); + rustDeskWinManager.setMethodHandler((call, fromWindowId) async { + print( + "call ${call.method} with args ${call.arguments} from window ${fromWindowId}"); + // for simplify, just replace connectionId + if (call.method == "new_remote_desktop") { + setState(() { + FFI.close(); + connectionId = jsonDecode(call.arguments)["id"]; + }); + } + }); + tabController = TabController(length: 1, vsync: this); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + TabBar( + controller: tabController, + isScrollable: true, + labelColor: Colors.black87, + physics: NeverScrollableScrollPhysics(), + tabs: [ + Tab( + text: connectionId, + ), + ]), + Expanded( + child: TabBarView(controller: tabController, children: [ + RemotePage(key: ValueKey(connectionId), id: connectionId) + ])) + ], + ); + } +} diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index ad27a6d3b..90566e165 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -55,8 +55,8 @@ class _DesktopHomePageState extends State { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - buildControlPanel(context), - buildRecentSession(context), + // buildControlPanel(context), + // buildRecentSession(context), ConnectionPage() ], ); diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart new file mode 100644 index 000000000..6827bde60 --- /dev/null +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -0,0 +1,1364 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:ui' as ui; + +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hbb/mobile/widgets/gesture_help.dart'; +import 'package:flutter_hbb/models/chat_model.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:provider/provider.dart'; +import 'package:wakelock/wakelock.dart'; +import 'package:window_manager/window_manager.dart'; + +import '../../common.dart'; +import '../../mobile/widgets/dialog.dart'; +import '../../mobile/widgets/gestures.dart'; +import '../../mobile/widgets/overlay.dart'; +import '../../models/model.dart'; + +final initText = '\1' * 1024; + +class RemotePage extends StatefulWidget { + RemotePage({Key? key, required this.id}) : super(key: key); + + final String id; + + @override + _RemotePageState createState() => _RemotePageState(); +} + +class _RemotePageState extends State with WindowListener { + Timer? _interval; + Timer? _timer; + bool _showBar = !isWebDesktop; + double _bottom = 0; + String _value = ''; + double _scale = 1; + double _mouseScrollIntegral = 0; // mouse scroll speed controller + + var _more = true; + var _fn = false; + final FocusNode _mobileFocusNode = FocusNode(); + final FocusNode _physicalFocusNode = FocusNode(); + var _showEdit = false; // use soft keyboard + var _isPhysicalMouse = false; + + @override + void initState() { + super.initState(); + FFI.connect(widget.id); + WidgetsBinding.instance.addPostFrameCallback((_) { + SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); + showLoading(translate('Connecting...')); + _interval = + Timer.periodic(Duration(milliseconds: 30), (timer) => interval()); + }); + if (!Platform.isLinux) { + Wakelock.enable(); + } + _physicalFocusNode.requestFocus(); + FFI.ffiModel.updateEventListener(widget.id); + FFI.listenToMouse(true); + WindowManager.instance.addListener(this); + } + + @override + void dispose() { + print("remote page dispose"); + hideMobileActionsOverlay(); + FFI.listenToMouse(false); + FFI.invokeMethod("enable_soft_keyboard", true); + _mobileFocusNode.dispose(); + _physicalFocusNode.dispose(); + FFI.close(); + _interval?.cancel(); + _timer?.cancel(); + SmartDialog.dismiss(); + SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, + overlays: SystemUiOverlay.values); + if (!Platform.isLinux) { + Wakelock.disable(); + } + WindowManager.instance.removeListener(this); + super.dispose(); + } + + void resetTool() { + FFI.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 (chatWindowOverlayEntry == null && + FFI.ffiModel.pi.version.isNotEmpty) { + FFI.invokeMethod("enable_soft_keyboard", false); + } + } + }); + } + } + + void interval() { + try { + intervalUnsafe(); + } catch (e) {} + } + + // handle mobile virtual keyboard + void handleInput(String newValue) { + var oldValue = _value; + _value = newValue; + if (isIOS) { + var i = newValue.length - 1; + for (; i >= 0 && newValue[i] != '\1'; --i) {} + var j = oldValue.length - 1; + for (; j >= 0 && oldValue[j] != '\1'; --j) {} + if (i < j) j = i; + newValue = newValue.substring(j + 1); + oldValue = oldValue.substring(j + 1); + var common = 0; + for (; + common < oldValue.length && + common < newValue.length && + newValue[common] == oldValue[common]; + ++common); + for (i = 0; i < oldValue.length - common; ++i) { + FFI.inputKey('VK_BACK'); + } + if (newValue.length > common) { + var s = newValue.substring(common); + if (s.length > 1) { + FFI.setByName('input_string', s); + } else { + inputChar(s); + } + } + return; + } + if (oldValue.length > 0 && + newValue.length > 0 && + oldValue[0] == '\1' && + newValue[0] != '\1') { + // clipboard + oldValue = ''; + } + if (newValue.length == oldValue.length) { + // ? + } else if (newValue.length < oldValue.length) { + final char = 'VK_BACK'; + FFI.inputKey(char); + } else { + final content = newValue.substring(oldValue.length); + if (content.length > 1) { + if (oldValue != '' && + content.length == 2 && + (content == '""' || + content == '()' || + content == '[]' || + content == '<>' || + content == "{}" || + content == '”“' || + content == '《》' || + content == '()' || + content == '【】')) { + // can not only input content[0], because when input ], [ are also auo insert, which cause ] never be input + FFI.setByName('input_string', content); + openKeyboard(); + return; + } + FFI.setByName('input_string', content); + } else { + inputChar(content); + } + } + } + + void inputChar(String char) { + if (char == '\n') { + char = 'VK_RETURN'; + } else if (char == ' ') { + char = 'VK_SPACE'; + } + FFI.inputKey(char); + } + + void openKeyboard() { + FFI.invokeMethod("enable_soft_keyboard", true); + // destroy first, so that our _value trick can work + _value = initText; + setState(() => _showEdit = false); + _timer?.cancel(); + _timer = Timer(Duration(milliseconds: 30), () { + // show now, and sleep a while to requestFocus to + // make sure edit ready, so that keyboard wont show/hide/show/hide happen + setState(() => _showEdit = true); + _timer?.cancel(); + _timer = Timer(Duration(milliseconds: 30), () { + SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, + overlays: SystemUiOverlay.values); + _mobileFocusNode.requestFocus(); + }); + }); + } + + void sendRawKey(RawKeyEvent e, {bool? down, bool? press}) { + // for maximum compatibility + final label = _logicalKeyMap[e.logicalKey.keyId] ?? + _physicalKeyMap[e.physicalKey.usbHidUsage] ?? + e.logicalKey.keyLabel; + FFI.inputKey(label, down: down, press: press ?? false); + } + + @override + Widget build(BuildContext context) { + final pi = Provider.of(context).pi; + final hideKeyboard = isKeyboardShown() && _showEdit; + final showActionButton = !_showBar || hideKeyboard; + final keyboard = FFI.ffiModel.permissions['keyboard'] != false; + + return WillPopScope( + onWillPop: () async { + clientClose(); + return false; + }, + child: getRawPointerAndKeyBody( + keyboard, + Scaffold( + // resizeToAvoidBottomInset: true, + floatingActionButton: !showActionButton + ? null + : FloatingActionButton( + mini: !hideKeyboard, + child: Icon( + hideKeyboard ? Icons.expand_more : Icons.expand_less), + backgroundColor: MyTheme.accent, + onPressed: () { + setState(() { + if (hideKeyboard) { + _showEdit = false; + FFI.invokeMethod("enable_soft_keyboard", false); + _mobileFocusNode.unfocus(); + _physicalFocusNode.requestFocus(); + } else { + _showBar = !_showBar; + } + }); + }), + bottomNavigationBar: _showBar && pi.displays.length > 0 + ? getBottomAppBar(keyboard) + : null, + body: Overlay( + initialEntries: [ + OverlayEntry(builder: (context) { + return Container( + color: Colors.black, + child: isWebDesktop + ? getBodyForDesktopWithListener(keyboard) + : SafeArea( + child: Container( + color: MyTheme.canvasColor, + child: _isPhysicalMouse + ? getBodyForMobile() + : getBodyForMobileWithGesture()))); + }) + ], + ))), + ); + } + + Widget getRawPointerAndKeyBody(bool keyboard, Widget child) { + return Listener( + onPointerHover: (e) { + if (e.kind != ui.PointerDeviceKind.mouse) return; + if (!_isPhysicalMouse) { + setState(() { + _isPhysicalMouse = true; + }); + } + if (_isPhysicalMouse) { + FFI.handleMouse(getEvent(e, 'mousemove')); + } + }, + onPointerDown: (e) { + if (e.kind != ui.PointerDeviceKind.mouse) { + if (_isPhysicalMouse) { + setState(() { + _isPhysicalMouse = false; + }); + } + } + if (_isPhysicalMouse) { + FFI.handleMouse(getEvent(e, 'mousedown')); + } + }, + onPointerUp: (e) { + if (e.kind != ui.PointerDeviceKind.mouse) return; + if (_isPhysicalMouse) { + FFI.handleMouse(getEvent(e, 'mouseup')); + } + }, + onPointerMove: (e) { + if (e.kind != ui.PointerDeviceKind.mouse) return; + if (_isPhysicalMouse) { + FFI.handleMouse(getEvent(e, 'mousemove')); + } + }, + onPointerSignal: (e) { + if (e is PointerScrollEvent) { + var dx = e.scrollDelta.dx; + var dy = e.scrollDelta.dy; + if (dx > 0) + dx = -1; + else if (dx < 0) dx = 1; + if (dy > 0) + dy = -1; + else if (dy < 0) dy = 1; + FFI.setByName( + 'send_mouse', '{"type": "wheel", "x": "$dx", "y": "$dy"}'); + } + }, + child: MouseRegion( + cursor: keyboard ? SystemMouseCursors.none : MouseCursor.defer, + child: FocusScope( + autofocus: true, + child: Focus( + autofocus: true, + canRequestFocus: true, + focusNode: _physicalFocusNode, + onKey: (data, e) { + final key = e.logicalKey; + if (e is RawKeyDownEvent) { + if (e.repeat) { + sendRawKey(e, press: true); + } else { + if (e.isAltPressed && !FFI.alt) { + FFI.alt = true; + } else if (e.isControlPressed && !FFI.ctrl) { + FFI.ctrl = true; + } else if (e.isShiftPressed && !FFI.shift) { + FFI.shift = true; + } else if (e.isMetaPressed && !FFI.command) { + FFI.command = true; + } + sendRawKey(e, down: true); + } + } + // [!_showEdit] workaround for soft-keyboard's control_key like Backspace / Enter + if (!_showEdit && e is RawKeyUpEvent) { + if (key == LogicalKeyboardKey.altLeft || + key == LogicalKeyboardKey.altRight) { + FFI.alt = false; + } else if (key == LogicalKeyboardKey.controlLeft || + key == LogicalKeyboardKey.controlRight) { + FFI.ctrl = false; + } else if (key == LogicalKeyboardKey.shiftRight || + key == LogicalKeyboardKey.shiftLeft) { + FFI.shift = false; + } else if (key == LogicalKeyboardKey.metaLeft || + key == LogicalKeyboardKey.metaRight) { + FFI.command = false; + } + sendRawKey(e); + } + return KeyEventResult.handled; + }, + child: child)))); + } + + Widget getBottomAppBar(bool keyboard) { + return BottomAppBar( + elevation: 10, + color: MyTheme.accent, + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + IconButton( + color: Colors.white, + icon: Icon(Icons.clear), + onPressed: () { + clientClose(); + }, + ) + ] + + [ + IconButton( + color: Colors.white, + icon: Icon(Icons.tv), + onPressed: () { + setState(() => _showEdit = false); + showOptions(); + }, + ) + ] + + (isWebDesktop + ? [] + : FFI.ffiModel.isPeerAndroid + ? [ + IconButton( + color: Colors.white, + icon: Icon(Icons.build), + onPressed: () { + if (mobileActionsOverlayEntry == null) { + showMobileActionsOverlay(); + } else { + hideMobileActionsOverlay(); + } + }, + ) + ] + : [ + IconButton( + color: Colors.white, + icon: Icon(Icons.keyboard), + onPressed: openKeyboard), + IconButton( + color: Colors.white, + icon: Icon(FFI.ffiModel.touchMode + ? Icons.touch_app + : Icons.mouse), + onPressed: changeTouchMode, + ), + ]) + + (isWeb + ? [] + : [ + IconButton( + color: Colors.white, + icon: Icon(Icons.message), + onPressed: () { + FFI.chatModel + .changeCurrentID(ChatModel.clientModeID); + toggleChatOverlay(); + }, + ) + ]) + + [ + IconButton( + color: Colors.white, + icon: Icon(Icons.more_vert), + onPressed: () { + setState(() => _showEdit = false); + showActions(); + }, + ), + ]), + IconButton( + color: Colors.white, + icon: Icon(Icons.expand_more), + onPressed: () { + setState(() => _showBar = !_showBar); + }), + ], + ), + ); + } + + /// touchMode only: + /// LongPress -> right click + /// OneFingerPan -> start/end -> left down start/end + /// onDoubleTapDown -> move to + /// onLongPressDown => move to + /// + /// mouseMode only: + /// DoubleFiner -> right click + /// HoldDrag -> left drag + + Widget getBodyForMobileWithGesture() { + final touchMode = FFI.ffiModel.touchMode; + return getMixinGestureDetector( + child: getBodyForMobile(), + onTapUp: (d) { + if (touchMode) { + FFI.cursorModel.touch( + d.localPosition.dx, d.localPosition.dy, MouseButtons.left); + } else { + FFI.tap(MouseButtons.left); + } + }, + onDoubleTapDown: (d) { + if (touchMode) { + FFI.cursorModel.move(d.localPosition.dx, d.localPosition.dy); + } + }, + onDoubleTap: () { + FFI.tap(MouseButtons.left); + FFI.tap(MouseButtons.left); + }, + onLongPressDown: (d) { + if (touchMode) { + FFI.cursorModel.move(d.localPosition.dx, d.localPosition.dy); + } + }, + onLongPress: () { + FFI.tap(MouseButtons.right); + }, + onDoubleFinerTap: (d) { + if (!touchMode) { + FFI.tap(MouseButtons.right); + } + }, + onHoldDragStart: (d) { + if (!touchMode) { + FFI.sendMouse('down', MouseButtons.left); + } + }, + onHoldDragUpdate: (d) { + if (!touchMode) { + FFI.cursorModel.updatePan(d.delta.dx, d.delta.dy, touchMode); + } + }, + onHoldDragEnd: (_) { + if (!touchMode) { + FFI.sendMouse('up', MouseButtons.left); + } + }, + onOneFingerPanStart: (d) { + if (touchMode) { + FFI.cursorModel.move(d.localPosition.dx, d.localPosition.dy); + FFI.sendMouse('down', MouseButtons.left); + } + }, + onOneFingerPanUpdate: (d) { + FFI.cursorModel.updatePan(d.delta.dx, d.delta.dy, touchMode); + }, + onOneFingerPanEnd: (d) { + if (touchMode) { + FFI.sendMouse('up', MouseButtons.left); + } + }, + // scale + pan event + onTwoFingerScaleUpdate: (d) { + FFI.canvasModel.updateScale(d.scale / _scale); + _scale = d.scale; + FFI.canvasModel.panX(d.focalPointDelta.dx); + FFI.canvasModel.panY(d.focalPointDelta.dy); + }, + onTwoFingerScaleEnd: (d) { + _scale = 1; + FFI.setByName('peer_option', '{"name": "view-style", "value": ""}'); + }, + onThreeFingerVerticalDragUpdate: FFI.ffiModel.isPeerAndroid + ? null + : (d) { + _mouseScrollIntegral += d.delta.dy / 4; + if (_mouseScrollIntegral > 1) { + FFI.scroll(1); + _mouseScrollIntegral = 0; + } else if (_mouseScrollIntegral < -1) { + FFI.scroll(-1); + _mouseScrollIntegral = 0; + } + }); + } + + Widget getBodyForMobile() { + return Container( + color: MyTheme.canvasColor, + child: Stack(children: [ + ImagePaint(), + CursorPaint(), + getHelpTools(), + SizedBox( + width: 0, + height: 0, + child: !_showEdit + ? Container() + : TextFormField( + textInputAction: TextInputAction.newline, + autocorrect: false, + enableSuggestions: false, + autofocus: true, + focusNode: _mobileFocusNode, + maxLines: null, + initialValue: _value, + // trick way to make backspace work always + keyboardType: TextInputType.multiline, + onChanged: handleInput, + ), + ), + ])); + } + + Widget getBodyForDesktopWithListener(bool keyboard) { + var paints = [ImagePaint()]; + if (keyboard || + FFI.getByName('toggle_option', 'show-remote-cursor') == 'true') { + paints.add(CursorPaint()); + } + return Container( + color: MyTheme.canvasColor, child: Stack(children: paints)); + } + + int lastMouseDownButtons = 0; + + Map getEvent(PointerEvent evt, String type) { + final Map out = {}; + out['type'] = type; + out['x'] = evt.position.dx; + out['y'] = evt.position.dy; + if (FFI.alt) out['alt'] = 'true'; + if (FFI.shift) out['shift'] = 'true'; + if (FFI.ctrl) out['ctrl'] = 'true'; + if (FFI.command) out['command'] = 'true'; + out['buttons'] = evt + .buttons; // left button: 1, right button: 2, middle button: 4, 1 | 2 = 3 (left + right) + if (evt.buttons != 0) { + lastMouseDownButtons = evt.buttons; + } else { + out['buttons'] = lastMouseDownButtons; + } + return out; + } + + void showActions() { + final size = MediaQuery.of(context).size; + final x = 120.0; + final y = size.height; + final more = >[]; + final pi = FFI.ffiModel.pi; + final perms = FFI.ffiModel.permissions; + if (pi.version.isNotEmpty) { + more.add(PopupMenuItem( + child: Text(translate('Refresh')), value: 'refresh')); + } + more.add(PopupMenuItem( + child: Row( + children: ([ + Container(width: 100.0, child: Text(translate('OS Password'))), + TextButton( + style: flatButtonStyle, + onPressed: () { + Navigator.pop(context); + showSetOSPassword(false); + }, + child: Icon(Icons.edit, color: MyTheme.accent), + ) + ])), + value: 'enter_os_password')); + if (!isWebDesktop) { + if (perms['keyboard'] != false && perms['clipboard'] != false) { + more.add(PopupMenuItem( + child: Text(translate('Paste')), value: 'paste')); + } + more.add(PopupMenuItem( + child: Text(translate('Reset canvas')), value: 'reset_canvas')); + } + if (perms['keyboard'] != false) { + if (pi.platform == 'Linux' || pi.sasEnabled) { + more.add(PopupMenuItem( + child: Text(translate('Insert') + ' Ctrl + Alt + Del'), + value: 'cad')); + } + more.add(PopupMenuItem( + child: Text(translate('Insert Lock')), value: 'lock')); + if (pi.platform == 'Windows' && + FFI.getByName('toggle_option', 'privacy-mode') != 'true') { + more.add(PopupMenuItem( + child: Text(translate( + (FFI.ffiModel.inputBlocked ? 'Unb' : 'B') + 'lock user input')), + value: 'block-input')); + } + } + () async { + var value = await showMenu( + context: context, + position: RelativeRect.fromLTRB(x, y, x, y), + items: more, + elevation: 8, + ); + if (value == 'cad') { + FFI.setByName('ctrl_alt_del'); + } else if (value == 'lock') { + FFI.setByName('lock_screen'); + } else if (value == 'block-input') { + FFI.setByName('toggle_option', + (FFI.ffiModel.inputBlocked ? 'un' : '') + 'block-input'); + FFI.ffiModel.inputBlocked = !FFI.ffiModel.inputBlocked; + } else if (value == 'refresh') { + FFI.setByName('refresh'); + } else if (value == 'paste') { + () async { + ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain); + if (data != null && data.text != null) { + FFI.setByName('input_string', '${data.text}'); + } + }(); + } else if (value == 'enter_os_password') { + var password = FFI.getByName('peer_option', "os-password"); + if (password != "") { + FFI.setByName('input_os_password', password); + } else { + showSetOSPassword(true); + } + } else if (value == 'reset_canvas') { + FFI.cursorModel.reset(); + } + }(); + } + + 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( + padding: EdgeInsets.symmetric(vertical: 10), + child: GestureHelp( + touchMode: FFI.ffiModel.touchMode, + onTouchModeChange: (t) { + FFI.ffiModel.toggleTouchMode(); + final v = FFI.ffiModel.touchMode ? 'Y' : ''; + FFI.setByName('peer_option', + '{"name": "touch-mode", "value": "$v"}'); + })); + })); + } + + Widget getHelpTools() { + final keyboard = isKeyboardShown(); + if (!keyboard) { + return SizedBox(); + } + final size = MediaQuery.of(context).size; + var 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 = FFI.ffiModel.pi; + final isMac = pi.platform == "Mac OS"; + final modifiers = [ + wrap('Ctrl ', () { + setState(() => FFI.ctrl = !FFI.ctrl); + }, FFI.ctrl), + wrap(' Alt ', () { + setState(() => FFI.alt = !FFI.alt); + }, FFI.alt), + wrap('Shift', () { + setState(() => FFI.shift = !FFI.shift); + }, FFI.shift), + wrap(isMac ? ' Cmd ' : ' Win ', () { + setState(() => FFI.command = !FFI.command); + }, FFI.command), + ]; + final keys = [ + wrap( + ' Fn ', + () => setState( + () { + _fn = !_fn; + if (_fn) { + _more = false; + } + }, + ), + _fn), + wrap( + ' ... ', + () => setState( + () { + _more = !_more; + if (_more) { + _fn = false; + } + }, + ), + _more), + ]; + final fn = [ + SizedBox(width: 9999), + ]; + for (var i = 1; i <= 12; ++i) { + final name = 'F' + i.toString(); + fn.add(wrap(name, () { + FFI.inputKey('VK_' + name); + })); + } + final more = [ + SizedBox(width: 9999), + wrap('Esc', () { + FFI.inputKey('VK_ESCAPE'); + }), + wrap('Tab', () { + FFI.inputKey('VK_TAB'); + }), + wrap('Home', () { + FFI.inputKey('VK_HOME'); + }), + wrap('End', () { + FFI.inputKey('VK_END'); + }), + wrap('Del', () { + FFI.inputKey('VK_DELETE'); + }), + wrap('PgUp', () { + FFI.inputKey('VK_PRIOR'); + }), + wrap('PgDn', () { + FFI.inputKey('VK_NEXT'); + }), + SizedBox(width: 9999), + wrap('', () { + FFI.inputKey('VK_LEFT'); + }, false, Icons.keyboard_arrow_left), + wrap('', () { + FFI.inputKey('VK_UP'); + }, false, Icons.keyboard_arrow_up), + wrap('', () { + FFI.inputKey('VK_DOWN'); + }, false, Icons.keyboard_arrow_down), + wrap('', () { + FFI.inputKey('VK_RIGHT'); + }, false, Icons.keyboard_arrow_right), + wrap(isMac ? 'Cmd+C' : 'Ctrl+C', () { + sendPrompt(isMac, 'VK_C'); + }), + wrap(isMac ? 'Cmd+V' : 'Ctrl+V', () { + sendPrompt(isMac, 'VK_V'); + }), + wrap(isMac ? 'Cmd+S' : 'Ctrl+S', () { + sendPrompt(isMac, 'VK_S'); + }), + ]; + 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), + child: Wrap( + spacing: space, + runSpacing: space, + children: [SizedBox(width: 9999)] + + (keyboard + ? modifiers + keys + (_fn ? fn : []) + (_more ? more : []) + : modifiers), + )); + } + + @override + void onWindowEvent(String eventName) { + print("window event: $eventName"); + switch (eventName) { + case 'resize': + FFI.canvasModel.updateViewStyle(); + break; + case 'maximize': + Future.delayed(Duration(milliseconds: 100), () { + FFI.canvasModel.updateViewStyle(); + }); + break; + } + } +} + +class ImagePaint extends StatelessWidget { + @override + Widget build(BuildContext context) { + final m = Provider.of(context); + final c = Provider.of(context); + final adjust = FFI.cursorModel.adjustForKeyboard(); + var s = c.scale; + return CustomPaint( + painter: new ImagePainter( + image: m.image, x: c.x / s, y: (c.y - adjust) / s, scale: s), + ); + } +} + +class CursorPaint extends StatelessWidget { + @override + Widget build(BuildContext context) { + final m = Provider.of(context); + final c = Provider.of(context); + final adjust = FFI.cursorModel.adjustForKeyboard(); + var s = c.scale; + return CustomPaint( + painter: new ImagePainter( + image: m.image, + x: m.x * s - m.hotx + c.x, + y: m.y * s - m.hoty + c.y - adjust, + scale: 1), + ); + } +} + +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!, new Offset(x, y), new Paint()); + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) { + return oldDelegate != this; + } +} + +CheckboxListTile getToggle( + void Function(void Function()) setState, option, name) { + return CheckboxListTile( + value: FFI.getByName('toggle_option', option) == 'true', + onChanged: (v) { + setState(() { + FFI.setByName('toggle_option', option); + }); + }, + dense: true, + title: Text(translate(name))); +} + +RadioListTile getRadio(String name, String toValue, String curValue, + void Function(String?) onChange) { + return RadioListTile( + controlAffinity: ListTileControlAffinity.trailing, + title: Text(translate(name)), + value: toValue, + groupValue: curValue, + onChanged: onChange, + dense: true, + ); +} + +void showOptions() { + String quality = FFI.getByName('image_quality'); + if (quality == '') quality = 'balanced'; + String viewStyle = FFI.getByName('peer_option', 'view-style'); + var displays = []; + final pi = FFI.ffiModel.pi; + final image = FFI.ffiModel.getConnectionImage(); + if (image != null) + displays.add(Padding(padding: const EdgeInsets.only(top: 8), child: image)); + if (pi.displays.length > 1) { + final cur = pi.currentDisplay; + final children = []; + for (var i = 0; i < pi.displays.length; ++i) + children.add(InkWell( + onTap: () { + if (i == cur) return; + FFI.setByName('switch_display', i.toString()); + SmartDialog.dismiss(); + }, + child: Ink( + width: 40, + height: 40, + decoration: BoxDecoration( + border: Border.all(color: Colors.black87), + color: i == cur ? Colors.black87 : Colors.white), + child: Center( + child: Text((i + 1).toString(), + style: TextStyle( + color: i == cur ? Colors.white : Colors.black87)))))); + displays.add(Padding( + padding: const EdgeInsets.only(top: 8), + child: Wrap( + alignment: WrapAlignment.center, + spacing: 8, + children: children, + ))); + } + if (displays.isNotEmpty) { + displays.add(Divider(color: MyTheme.border)); + } + final perms = FFI.ffiModel.permissions; + + DialogManager.show((setState, close) { + final more = []; + if (perms['audio'] != false) { + more.add(getToggle(setState, 'disable-audio', 'Mute')); + } + if (perms['keyboard'] != false) { + if (perms['clipboard'] != false) + more.add(getToggle(setState, 'disable-clipboard', 'Disable clipboard')); + more.add(getToggle( + setState, 'lock-after-session-end', 'Lock after session end')); + if (pi.platform == 'Windows') { + more.add(getToggle(setState, 'privacy-mode', 'Privacy mode')); + } + } + var setQuality = (String? value) { + if (value == null) return; + setState(() { + quality = value; + FFI.setByName('image_quality', value); + }); + }; + var setViewStyle = (String? value) { + if (value == null) return; + setState(() { + viewStyle = value; + FFI.setByName( + 'peer_option', '{"name": "view-style", "value": "$value"}'); + FFI.canvasModel.updateViewStyle(); + }); + }; + return CustomAlertDialog( + title: SizedBox.shrink(), + content: Column( + mainAxisSize: MainAxisSize.min, + children: displays + + [ + getRadio('Original', 'original', viewStyle, setViewStyle), + getRadio('Shrink', 'shrink', viewStyle, setViewStyle), + getRadio('Stretch', 'stretch', viewStyle, setViewStyle), + Divider(color: MyTheme.border), + getRadio('Good image quality', 'best', quality, setQuality), + getRadio('Balanced', 'balanced', quality, setQuality), + getRadio('Optimize reaction time', 'low', quality, setQuality), + Divider(color: MyTheme.border), + getToggle(setState, 'show-remote-cursor', 'Show remote cursor'), + ] + + more), + actions: [], + contentPadding: 0, + ); + }, clickMaskDismiss: true, backDismiss: true); +} + +void showSetOSPassword(bool login) { + final controller = TextEditingController(); + var password = FFI.getByName('peer_option', "os-password"); + var autoLogin = FFI.getByName('peer_option', "auto-login") != ""; + controller.text = password; + DialogManager.show((setState, close) { + return CustomAlertDialog( + title: Text(translate('OS Password')), + content: Column(mainAxisSize: MainAxisSize.min, children: [ + PasswordWidget(controller: controller), + CheckboxListTile( + contentPadding: const EdgeInsets.all(0), + dense: true, + controlAffinity: ListTileControlAffinity.leading, + title: Text( + translate('Auto Login'), + ), + value: autoLogin, + onChanged: (v) { + if (v == null) return; + setState(() => autoLogin = v); + }, + ), + ]), + actions: [ + TextButton( + style: flatButtonStyle, + onPressed: () { + close(); + }, + child: Text(translate('Cancel')), + ), + TextButton( + style: flatButtonStyle, + onPressed: () { + var text = controller.text.trim(); + FFI.setByName( + 'peer_option', '{"name": "os-password", "value": "$text"}'); + FFI.setByName('peer_option', + '{"name": "auto-login", "value": "${autoLogin ? 'Y' : ''}"}'); + if (text != "" && login) { + FFI.setByName('input_os_password', text); + } + close(); + }, + child: Text(translate('OK')), + ), + ]); + }); +} + +void sendPrompt(bool isMac, String key) { + final old = isMac ? FFI.command : FFI.ctrl; + if (isMac) { + FFI.command = true; + } else { + FFI.ctrl = true; + } + FFI.inputKey(key); + if (isMac) { + FFI.command = old; + } else { + FFI.ctrl = old; + } +} + +/// flutter/packages/flutter/lib/src/services/keyboard_key.dart -> _keyLabels +/// see [LogicalKeyboardKey.keyLabel] +const Map _logicalKeyMap = { + 0x00000000020: 'VK_SPACE', + 0x00000000022: 'VK_QUOTE', + 0x0000000002c: 'VK_COMMA', + 0x0000000002d: 'VK_MINUS', + 0x0000000002f: 'VK_SLASH', + 0x00000000030: 'VK_0', + 0x00000000031: 'VK_1', + 0x00000000032: 'VK_2', + 0x00000000033: 'VK_3', + 0x00000000034: 'VK_4', + 0x00000000035: 'VK_5', + 0x00000000036: 'VK_6', + 0x00000000037: 'VK_7', + 0x00000000038: 'VK_8', + 0x00000000039: 'VK_9', + 0x0000000003b: 'VK_SEMICOLON', + 0x0000000003d: 'VK_PLUS', // it is = + 0x0000000005b: 'VK_LBRACKET', + 0x0000000005c: 'VK_BACKSLASH', + 0x0000000005d: 'VK_RBRACKET', + 0x00000000061: 'VK_A', + 0x00000000062: 'VK_B', + 0x00000000063: 'VK_C', + 0x00000000064: 'VK_D', + 0x00000000065: 'VK_E', + 0x00000000066: 'VK_F', + 0x00000000067: 'VK_G', + 0x00000000068: 'VK_H', + 0x00000000069: 'VK_I', + 0x0000000006a: 'VK_J', + 0x0000000006b: 'VK_K', + 0x0000000006c: 'VK_L', + 0x0000000006d: 'VK_M', + 0x0000000006e: 'VK_N', + 0x0000000006f: 'VK_O', + 0x00000000070: 'VK_P', + 0x00000000071: 'VK_Q', + 0x00000000072: 'VK_R', + 0x00000000073: 'VK_S', + 0x00000000074: 'VK_T', + 0x00000000075: 'VK_U', + 0x00000000076: 'VK_V', + 0x00000000077: 'VK_W', + 0x00000000078: 'VK_X', + 0x00000000079: 'VK_Y', + 0x0000000007a: 'VK_Z', + 0x00100000008: 'VK_BACK', + 0x00100000009: 'VK_TAB', + 0x0010000000d: 'VK_ENTER', + 0x0010000001b: 'VK_ESCAPE', + 0x0010000007f: 'VK_DELETE', + 0x00100000104: 'VK_CAPITAL', + 0x00100000301: 'VK_DOWN', + 0x00100000302: 'VK_LEFT', + 0x00100000303: 'VK_RIGHT', + 0x00100000304: 'VK_UP', + 0x00100000305: 'VK_END', + 0x00100000306: 'VK_HOME', + 0x00100000307: 'VK_NEXT', + 0x00100000308: 'VK_PRIOR', + 0x00100000401: 'VK_CLEAR', + 0x00100000407: 'VK_INSERT', + 0x00100000504: 'VK_CANCEL', + 0x00100000506: 'VK_EXECUTE', + 0x00100000508: 'VK_HELP', + 0x00100000509: 'VK_PAUSE', + 0x0010000050c: 'VK_SELECT', + 0x00100000608: 'VK_PRINT', + 0x00100000705: 'VK_CONVERT', + 0x00100000706: 'VK_FINAL', + 0x00100000711: 'VK_HANGUL', + 0x00100000712: 'VK_HANJA', + 0x00100000713: 'VK_JUNJA', + 0x00100000718: 'VK_KANA', + 0x00100000719: 'VK_KANJI', + 0x00100000801: 'VK_F1', + 0x00100000802: 'VK_F2', + 0x00100000803: 'VK_F3', + 0x00100000804: 'VK_F4', + 0x00100000805: 'VK_F5', + 0x00100000806: 'VK_F6', + 0x00100000807: 'VK_F7', + 0x00100000808: 'VK_F8', + 0x00100000809: 'VK_F9', + 0x0010000080a: 'VK_F10', + 0x0010000080b: 'VK_F11', + 0x0010000080c: 'VK_F12', + 0x00100000d2b: 'Apps', + 0x00200000002: 'VK_SLEEP', + 0x00200000100: 'VK_CONTROL', + 0x00200000101: 'RControl', + 0x00200000102: 'VK_SHIFT', + 0x00200000103: 'RShift', + 0x00200000104: 'VK_MENU', + 0x00200000105: 'RAlt', + 0x002000001f0: 'VK_CONTROL', + 0x002000001f2: 'VK_SHIFT', + 0x002000001f4: 'VK_MENU', + 0x002000001f6: 'Meta', + 0x0020000022a: 'VK_MULTIPLY', + 0x0020000022b: 'VK_ADD', + 0x0020000022d: 'VK_SUBTRACT', + 0x0020000022e: 'VK_DECIMAL', + 0x0020000022f: 'VK_DIVIDE', + 0x00200000230: 'VK_NUMPAD0', + 0x00200000231: 'VK_NUMPAD1', + 0x00200000232: 'VK_NUMPAD2', + 0x00200000233: 'VK_NUMPAD3', + 0x00200000234: 'VK_NUMPAD4', + 0x00200000235: 'VK_NUMPAD5', + 0x00200000236: 'VK_NUMPAD6', + 0x00200000237: 'VK_NUMPAD7', + 0x00200000238: 'VK_NUMPAD8', + 0x00200000239: 'VK_NUMPAD9', +}; + +/// flutter/packages/flutter/lib/src/services/keyboard_key.dart -> _debugName +/// see [PhysicalKeyboardKey.debugName] -> _debugName +const Map _physicalKeyMap = { + 0x00010082: 'VK_SLEEP', + 0x00070004: 'VK_A', + 0x00070005: 'VK_B', + 0x00070006: 'VK_C', + 0x00070007: 'VK_D', + 0x00070008: 'VK_E', + 0x00070009: 'VK_F', + 0x0007000a: 'VK_G', + 0x0007000b: 'VK_H', + 0x0007000c: 'VK_I', + 0x0007000d: 'VK_J', + 0x0007000e: 'VK_K', + 0x0007000f: 'VK_L', + 0x00070010: 'VK_M', + 0x00070011: 'VK_N', + 0x00070012: 'VK_O', + 0x00070013: 'VK_P', + 0x00070014: 'VK_Q', + 0x00070015: 'VK_R', + 0x00070016: 'VK_S', + 0x00070017: 'VK_T', + 0x00070018: 'VK_U', + 0x00070019: 'VK_V', + 0x0007001a: 'VK_W', + 0x0007001b: 'VK_X', + 0x0007001c: 'VK_Y', + 0x0007001d: 'VK_Z', + 0x0007001e: 'VK_1', + 0x0007001f: 'VK_2', + 0x00070020: 'VK_3', + 0x00070021: 'VK_4', + 0x00070022: 'VK_5', + 0x00070023: 'VK_6', + 0x00070024: 'VK_7', + 0x00070025: 'VK_8', + 0x00070026: 'VK_9', + 0x00070027: 'VK_0', + 0x00070028: 'VK_ENTER', + 0x00070029: 'VK_ESCAPE', + 0x0007002a: 'VK_BACK', + 0x0007002b: 'VK_TAB', + 0x0007002c: 'VK_SPACE', + 0x0007002d: 'VK_MINUS', + 0x0007002e: 'VK_PLUS', // it is = + 0x0007002f: 'VK_LBRACKET', + 0x00070030: 'VK_RBRACKET', + 0x00070033: 'VK_SEMICOLON', + 0x00070034: 'VK_QUOTE', + 0x00070036: 'VK_COMMA', + 0x00070038: 'VK_SLASH', + 0x00070039: 'VK_CAPITAL', + 0x0007003a: 'VK_F1', + 0x0007003b: 'VK_F2', + 0x0007003c: 'VK_F3', + 0x0007003d: 'VK_F4', + 0x0007003e: 'VK_F5', + 0x0007003f: 'VK_F6', + 0x00070040: 'VK_F7', + 0x00070041: 'VK_F8', + 0x00070042: 'VK_F9', + 0x00070043: 'VK_F10', + 0x00070044: 'VK_F11', + 0x00070045: 'VK_F12', + 0x00070049: 'VK_INSERT', + 0x0007004a: 'VK_HOME', + 0x0007004b: 'VK_PRIOR', // Page Up + 0x0007004c: 'VK_DELETE', + 0x0007004d: 'VK_END', + 0x0007004e: 'VK_NEXT', // Page Down + 0x0007004f: 'VK_RIGHT', + 0x00070050: 'VK_LEFT', + 0x00070051: 'VK_DOWN', + 0x00070052: 'VK_UP', + 0x00070053: 'Num Lock', // TODO rust not impl + 0x00070054: 'VK_DIVIDE', // numpad + 0x00070055: 'VK_MULTIPLY', + 0x00070056: 'VK_SUBTRACT', + 0x00070057: 'VK_ADD', + 0x00070058: 'VK_ENTER', // num enter + 0x00070059: 'VK_NUMPAD0', + 0x0007005a: 'VK_NUMPAD1', + 0x0007005b: 'VK_NUMPAD2', + 0x0007005c: 'VK_NUMPAD3', + 0x0007005d: 'VK_NUMPAD4', + 0x0007005e: 'VK_NUMPAD5', + 0x0007005f: 'VK_NUMPAD6', + 0x00070060: 'VK_NUMPAD7', + 0x00070061: 'VK_NUMPAD8', + 0x00070062: 'VK_NUMPAD9', + 0x00070063: 'VK_DECIMAL', + 0x00070075: 'VK_HELP', + 0x00070077: 'VK_SELECT', + 0x00070088: 'VK_KANA', + 0x0007008a: 'VK_CONVERT', + 0x000700e0: 'VK_CONTROL', + 0x000700e1: 'VK_SHIFT', + 0x000700e2: 'VK_MENU', + 0x000700e3: 'Meta', + 0x000700e4: 'RControl', + 0x000700e5: 'RShift', + 0x000700e6: 'RAlt', + 0x000700e7: 'RWin', + 0x000c00b1: 'VK_PAUSE', + 0x000c00cd: 'VK_PAUSE', + 0x000c019e: 'LOCK_SCREEN', + 0x000c0208: 'VK_PRINT', +}; diff --git a/flutter/lib/desktop/screen/desktop_remote_screen.dart b/flutter/lib/desktop/screen/desktop_remote_screen.dart new file mode 100644 index 000000000..d2a9ab952 --- /dev/null +++ b/flutter/lib/desktop/screen/desktop_remote_screen.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/desktop/pages/connection_tab_page.dart'; +import 'package:flutter_hbb/models/model.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:provider/provider.dart'; + +/// multi-tab desktop remote screen +class DesktopRemoteScreen extends StatelessWidget { + final Map params; + + const DesktopRemoteScreen({Key? key, required this.params}) : super(key: key); + + @override + Widget build(BuildContext context) { + return MultiProvider( + providers: [ + ChangeNotifierProvider.value(value: FFI.ffiModel), + ChangeNotifierProvider.value(value: FFI.imageModel), + ChangeNotifierProvider.value(value: FFI.cursorModel), + ChangeNotifierProvider.value(value: FFI.canvasModel), + ], + child: MaterialApp( + navigatorKey: globalKey, + debugShowCheckedModeBanner: false, + title: 'RustDesk - Remote Desktop', + theme: ThemeData( + primarySwatch: Colors.blue, + visualDensity: VisualDensity.adaptivePlatformDensity, + ), + home: ConnectionTabPage( + params: params, + ), + navigatorObservers: [ + // FirebaseAnalyticsObserver(analytics: analytics), + FlutterSmartDialog.observer + ], + builder: FlutterSmartDialog.init( + builder: isAndroid + ? (_, child) => AccessibilityListener( + child: child, + ) + : null)), + ); + } +} diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index f69ab6465..2ab1586f0 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -1,7 +1,12 @@ +import 'dart:convert'; + import 'package:flutter/material.dart'; import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart'; +import 'package:flutter_hbb/desktop/screen/desktop_remote_screen.dart'; +import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:provider/provider.dart'; +import 'package:window_manager/window_manager.dart'; import 'common.dart'; import 'mobile/pages/home_page.dart'; @@ -9,7 +14,9 @@ import 'mobile/pages/server_page.dart'; import 'mobile/pages/settings_page.dart'; import 'models/model.dart'; -Future main() async { +int? windowId; + +Future main(List args) async { WidgetsFlutterBinding.ensureInitialized(); await FFI.ffiModel.init(); // await Firebase.initializeApp(); @@ -17,11 +24,49 @@ Future main() async { toAndroidChannelInit(); } refreshCurrentUser(); - if (isDesktop) { - print("desktop mode: starting service"); - FFI.serverModel.startService(); + runRustDeskApp(args); +} + +void runRustDeskApp(List args) async { + if (!isDesktop) { + runApp(App()); + return; + } + if (args.isNotEmpty && args.first == 'multi_window') { + windowId = int.parse(args[1]); + final argument = args[2].isEmpty + ? Map() + : jsonDecode(args[2]) as Map; + int type = argument['type'] ?? -1; + WindowType wType = type.windowType; + switch (wType) { + case WindowType.RemoteDesktop: + runApp(DesktopRemoteScreen( + params: argument, + )); + break; + default: + break; + } + } else { + // main window + await windowManager.ensureInitialized(); + // start service + FFI.serverModel.startService(); + WindowOptions windowOptions = WindowOptions( + size: Size(1280, 720), + center: true, + backgroundColor: Colors.transparent, + skipTaskbar: false, + titleBarStyle: TitleBarStyle.normal, + ); + windowManager.waitUntilReadyToShow(windowOptions, () async { + await windowManager.show(); + await windowManager.focus(); + }); + + runApp(App()); } - runApp(App()); } class App extends StatelessWidget { @@ -46,8 +91,8 @@ class App extends StatelessWidget { home: isDesktop ? DesktopHomePage() : !isAndroid - ? WebHomePage() - : HomePage(), + ? WebHomePage() + : HomePage(), navigatorObservers: [ // FirebaseAnalyticsObserver(analytics: analytics), FlutterSmartDialog.observer @@ -55,8 +100,8 @@ class App extends StatelessWidget { builder: FlutterSmartDialog.init( builder: isAndroid ? (_, child) => AccessibilityListener( - child: child, - ) + child: child, + ) : null)), ); } diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 4ba50e8e5..6f10b234d 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -1,17 +1,19 @@ +import 'dart:async'; +import 'dart:ui' as ui; + import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_hbb/models/chat_model.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_hbb/mobile/widgets/gesture_help.dart'; +import 'package:flutter_hbb/models/chat_model.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:provider/provider.dart'; -import 'package:flutter/services.dart'; -import 'dart:ui' as ui; -import 'dart:async'; import 'package:wakelock/wakelock.dart'; + import '../../common.dart'; -import '../widgets/gestures.dart'; import '../../models/model.dart'; import '../widgets/dialog.dart'; +import '../widgets/gestures.dart'; import '../widgets/overlay.dart'; final initText = '\1' * 1024; @@ -122,10 +124,10 @@ class _RemotePageState extends State { oldValue = oldValue.substring(j + 1); var common = 0; for (; - common < oldValue.length && - common < newValue.length && - newValue[common] == oldValue[common]; - ++common); + common < oldValue.length && + common < newValue.length && + newValue[common] == oldValue[common]; + ++common); for (i = 0; i < oldValue.length - common; ++i) { FFI.inputKey('VK_BACK'); } @@ -228,26 +230,26 @@ class _RemotePageState extends State { child: getRawPointerAndKeyBody( keyboard, Scaffold( - // resizeToAvoidBottomInset: true, + // resizeToAvoidBottomInset: true, floatingActionButton: !showActionButton ? null : FloatingActionButton( - mini: !hideKeyboard, - child: Icon( - hideKeyboard ? Icons.expand_more : Icons.expand_less), - backgroundColor: MyTheme.accent, - onPressed: () { - setState(() { - if (hideKeyboard) { - _showEdit = false; - FFI.invokeMethod("enable_soft_keyboard", false); - _mobileFocusNode.unfocus(); - _physicalFocusNode.requestFocus(); - } else { - _showBar = !_showBar; - } - }); - }), + mini: !hideKeyboard, + child: Icon( + hideKeyboard ? Icons.expand_more : Icons.expand_less), + backgroundColor: MyTheme.accent, + onPressed: () { + setState(() { + if (hideKeyboard) { + _showEdit = false; + FFI.invokeMethod("enable_soft_keyboard", false); + _mobileFocusNode.unfocus(); + _physicalFocusNode.requestFocus(); + } else { + _showBar = !_showBar; + } + }); + }), bottomNavigationBar: _showBar && pi.displays.length > 0 ? getBottomAppBar(keyboard) : null, @@ -259,11 +261,11 @@ class _RemotePageState extends State { child: isWebDesktop ? getBodyForDesktopWithListener(keyboard) : SafeArea( - child: Container( - color: MyTheme.canvasColor, - child: _isPhysicalMouse - ? getBodyForMobile() - : getBodyForMobileWithGesture()))); + child: Container( + color: MyTheme.canvasColor, + child: _isPhysicalMouse + ? getBodyForMobile() + : getBodyForMobileWithGesture()))); }) ], ))), @@ -379,14 +381,14 @@ class _RemotePageState extends State { children: [ Row( children: [ - IconButton( - color: Colors.white, - icon: Icon(Icons.clear), - onPressed: () { - clientClose(); - }, - ) - ] + + IconButton( + color: Colors.white, + icon: Icon(Icons.clear), + onPressed: () { + clientClose(); + }, + ) + ] + [ IconButton( color: Colors.white, @@ -400,45 +402,45 @@ class _RemotePageState extends State { (isWebDesktop ? [] : FFI.ffiModel.isPeerAndroid - ? [ - IconButton( - color: Colors.white, - icon: Icon(Icons.build), - onPressed: () { - if (mobileActionsOverlayEntry == null) { - showMobileActionsOverlay(); - } else { - hideMobileActionsOverlay(); - } - }, - ) - ] - : [ - IconButton( - color: Colors.white, - icon: Icon(Icons.keyboard), - onPressed: openKeyboard), - IconButton( - color: Colors.white, - icon: Icon(FFI.ffiModel.touchMode - ? Icons.touch_app - : Icons.mouse), - onPressed: changeTouchMode, - ), - ]) + + ? [ + IconButton( + color: Colors.white, + icon: Icon(Icons.build), + onPressed: () { + if (mobileActionsOverlayEntry == null) { + showMobileActionsOverlay(); + } else { + hideMobileActionsOverlay(); + } + }, + ) + ] + : [ + IconButton( + color: Colors.white, + icon: Icon(Icons.keyboard), + onPressed: openKeyboard), + IconButton( + color: Colors.white, + icon: Icon(FFI.ffiModel.touchMode + ? Icons.touch_app + : Icons.mouse), + onPressed: changeTouchMode, + ), + ]) + (isWeb ? [] : [ - IconButton( - color: Colors.white, - icon: Icon(Icons.message), - onPressed: () { - FFI.chatModel - .changeCurrentID(ChatModel.clientModeID); - toggleChatOverlay(); - }, - ) - ]) + + IconButton( + color: Colors.white, + icon: Icon(Icons.message), + onPressed: () { + FFI.chatModel + .changeCurrentID(ChatModel.clientModeID); + toggleChatOverlay(); + }, + ) + ]) + [ IconButton( color: Colors.white, @@ -547,15 +549,15 @@ class _RemotePageState extends State { onThreeFingerVerticalDragUpdate: FFI.ffiModel.isPeerAndroid ? null : (d) { - _mouseScrollIntegral += d.delta.dy / 4; - if (_mouseScrollIntegral > 1) { - FFI.scroll(1); - _mouseScrollIntegral = 0; - } else if (_mouseScrollIntegral < -1) { - FFI.scroll(-1); - _mouseScrollIntegral = 0; - } - }); + _mouseScrollIntegral += d.delta.dy / 4; + if (_mouseScrollIntegral > 1) { + FFI.scroll(1); + _mouseScrollIntegral = 0; + } else if (_mouseScrollIntegral < -1) { + FFI.scroll(-1); + _mouseScrollIntegral = 0; + } + }); } Widget getBodyForMobile() { @@ -571,17 +573,17 @@ class _RemotePageState extends State { child: !_showEdit ? Container() : TextFormField( - textInputAction: TextInputAction.newline, - autocorrect: false, - enableSuggestions: false, - autofocus: true, - focusNode: _mobileFocusNode, - maxLines: null, - initialValue: _value, - // trick way to make backspace work always - keyboardType: TextInputType.multiline, - onChanged: handleInput, - ), + textInputAction: TextInputAction.newline, + autocorrect: false, + enableSuggestions: false, + autofocus: true, + focusNode: _mobileFocusNode, + maxLines: null, + initialValue: _value, + // trick way to make backspace work always + keyboardType: TextInputType.multiline, + onChanged: handleInput, + ), ), ])); } @@ -597,6 +599,7 @@ class _RemotePageState extends State { } int lastMouseDownButtons = 0; + Map getEvent(PointerEvent evt, String type) { final Map out = {}; out['type'] = type; @@ -630,16 +633,16 @@ class _RemotePageState extends State { more.add(PopupMenuItem( child: Row( children: ([ - Container(width: 100.0, child: Text(translate('OS Password'))), - TextButton( - style: flatButtonStyle, - onPressed: () { - Navigator.pop(context); - showSetOSPassword(false); - }, - child: Icon(Icons.edit, color: MyTheme.accent), - ) - ])), + Container(width: 100.0, child: Text(translate('OS Password'))), + TextButton( + style: flatButtonStyle, + onPressed: () { + Navigator.pop(context); + showSetOSPassword(false); + }, + child: Icon(Icons.edit, color: MyTheme.accent), + ) + ])), value: 'enter_os_password')); if (!isWebDesktop) { if (perms['keyboard'] != false && perms['clipboard'] != false) { @@ -665,7 +668,7 @@ class _RemotePageState extends State { value: 'block-input')); } } - () async { + () async { var value = await showMenu( context: context, position: RelativeRect.fromLTRB(x, y, x, y), @@ -683,7 +686,7 @@ class _RemotePageState extends State { } else if (value == 'refresh') { FFI.setByName('refresh'); } else if (value == 'paste') { - () async { + () async { ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain); if (data != null && data.text != null) { FFI.setByName('input_string', '${data.text}'); @@ -749,7 +752,7 @@ class _RemotePageState extends State { child: icon != null ? Icon(icon, size: 17, color: Colors.white) : Text(translate(text), - style: TextStyle(color: Colors.white, fontSize: 11)), + style: TextStyle(color: Colors.white, fontSize: 11)), onPressed: onPressed); }; final pi = FFI.ffiModel.pi; @@ -771,25 +774,25 @@ class _RemotePageState extends State { final keys = [ wrap( ' Fn ', - () => setState( + () => setState( () { - _fn = !_fn; - if (_fn) { - _more = false; - } - }, - ), + _fn = !_fn; + if (_fn) { + _more = false; + } + }, + ), _fn), wrap( ' ... ', - () => setState( + () => setState( () { - _more = !_more; - if (_more) { - _fn = false; - } - }, - ), + _more = !_more; + if (_more) { + _fn = false; + } + }, + ), _more), ]; final fn = [ @@ -920,8 +923,7 @@ class ImagePainter extends CustomPainter { } } -CheckboxListTile getToggle( - void Function(void Function()) setState, option, name) { +CheckboxListTile getToggle(void Function(void Function()) setState, option, name) { return CheckboxListTile( value: FFI.getByName('toggle_option', option) == 'true', onChanged: (v) { diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 464e171aa..d94e69341 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -1,16 +1,18 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:math'; +import 'dart:typed_data'; +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hbb/models/chat_model.dart'; import 'package:flutter_hbb/models/file_model.dart'; import 'package:flutter_hbb/models/server_model.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'dart:math'; -import 'dart:convert'; -import 'dart:typed_data'; -import 'dart:ui' as ui; -import 'package:flutter/material.dart'; import 'package:tuple/tuple.dart'; -import 'dart:async'; + import '../common.dart'; import '../mobile/widgets/dialog.dart'; import '../mobile/widgets/overlay.dart'; @@ -596,17 +598,17 @@ class CursorModel with ChangeNotifier { final rgba = Uint8List.fromList(colors.map((s) => s as int).toList()); var pid = FFI.id; ui.decodeImageFromPixels(rgba, width, height, ui.PixelFormat.rgba8888, - (image) { - if (FFI.id != pid) return; - _image = image; - _images[id] = Tuple3(image, _hotx, _hoty); - try { - // my throw exception, because the listener maybe already dispose - notifyListeners(); - } catch (e) { - print('notify cursor: $e'); - } - }); + (image) { + if (FFI.id != pid) return; + _image = image; + _images[id] = Tuple3(image, _hotx, _hoty); + try { + // my throw exception, because the listener maybe already dispose + notifyListeners(); + } catch (e) { + print('notify cursor: $e'); + } + }); } void updateCursorId(Map evt) { @@ -635,8 +637,7 @@ class CursorModel with ChangeNotifier { notifyListeners(); } - void updateDisplayOriginWithCursor( - double x, double y, double xCursor, double yCursor) { + void updateDisplayOriginWithCursor(double x, double y, double xCursor, double yCursor) { _displayOriginX = x; _displayOriginY = y; _x = xCursor; @@ -734,13 +735,17 @@ class FFI { /// [press] indicates a click event(down and up). static void inputKey(String name, {bool? down, bool? press}) { if (!ffiModel.keyboard()) return; - setByName( - 'input_key', - json.encode(modify({ - 'name': name, - 'down': (down ?? false).toString(), - 'press': (press ?? true).toString() - }))); + final Map out = Map(); + out['name'] = name; + // default: down = false + if (down == true) { + out['down'] = "true"; + } + // default: press = true + if (press != false) { + out['press'] = "true"; + } + setByName('input_key', json.encode(modify(out))); } /// Send mouse movement event with distance in [x] and [y]. @@ -760,7 +765,7 @@ class FFI { return peers .map((s) => s as List) .map((s) => - Peer.fromJson(s[0] as String, s[1] as Map)) + Peer.fromJson(s[0] as String, s[1] as Map)) .toList(); } catch (e) { print('peers(): $e'); diff --git a/flutter/lib/utils/multi_window_manager.dart b/flutter/lib/utils/multi_window_manager.dart new file mode 100644 index 000000000..81944e648 --- /dev/null +++ b/flutter/lib/utils/multi_window_manager.dart @@ -0,0 +1,93 @@ +import 'dart:convert'; +import 'dart:ui'; + +import 'package:desktop_multi_window/desktop_multi_window.dart'; +import 'package:flutter/services.dart'; + +/// must keep the order +enum WindowType { Main, RemoteDesktop, FileTransfer, PortForward, Unknown } + +extension Index on int { + WindowType get windowType { + switch (this) { + case 0: + return WindowType.Main; + case 1: + return WindowType.RemoteDesktop; + case 2: + return WindowType.FileTransfer; + case 3: + return WindowType.PortForward; + default: + return WindowType.Unknown; + } + } +} + +/// Window Manager +/// mainly use it in `Main Window` +/// use it in sub window is not recommended +class RustDeskMultiWindowManager { + RustDeskMultiWindowManager._(); + + static final instance = RustDeskMultiWindowManager._(); + + int? _remoteDesktopWindowId; + + Future new_remote_desktop(String remote_id) async { + final msg = + jsonEncode({"type": WindowType.RemoteDesktop.index, "id": remote_id}); + + try { + final ids = await DesktopMultiWindow.getAllSubWindowIds(); + if (!ids.contains(_remoteDesktopWindowId)) { + _remoteDesktopWindowId = null; + } + } on Error { + _remoteDesktopWindowId = null; + } + if (_remoteDesktopWindowId == null) { + final remoteDesktopController = + await DesktopMultiWindow.createWindow(msg); + remoteDesktopController + ..setFrame(const Offset(0, 0) & const Size(1280, 720)) + ..center() + ..setTitle("rustdesk - remote desktop") + ..show(); + _remoteDesktopWindowId = remoteDesktopController.windowId; + } else { + return call(WindowType.RemoteDesktop, "new_remote_desktop", msg); + } + } + + Future call(WindowType type, String methodName, dynamic args) async { + int? windowId = findWindowByType(type); + if (windowId == null) { + return; + } + return await DesktopMultiWindow.invokeMethod(windowId, methodName, args); + } + + int? findWindowByType(WindowType type) { + switch (type) { + case WindowType.Main: + break; + case WindowType.RemoteDesktop: + return _remoteDesktopWindowId; + case WindowType.FileTransfer: + break; + case WindowType.PortForward: + break; + case WindowType.Unknown: + break; + } + return null; + } + + void setMethodHandler( + Future Function(MethodCall call, int fromWindowId)? handler) { + DesktopMultiWindow.setMethodHandler(handler); + } +} + +final rustDeskWinManager = RustDeskMultiWindowManager.instance; diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 59f8cdc3e..2f7f30ec9 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -85,6 +85,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.16" + desktop_multi_window: + dependency: "direct main" + description: + name: desktop_multi_window + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.2" device_info: dependency: "direct main" description: @@ -751,6 +758,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.5.2" + window_manager: + dependency: "direct main" + description: + name: window_manager + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.3" xdg_directories: dependency: transitive description: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index f2aa1bf44..75ad90be9 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -55,6 +55,8 @@ dependencies: image: ^3.1.3 flutter_smart_dialog: ^4.3.1 flutter_rust_bridge: ^1.30.0 + window_manager: ^0.2.3 + desktop_multi_window: ^0.0.2 dev_dependencies: flutter_launcher_icons: ^0.9.1 From 7b3bbdf964bc974a6734b9da03fe7b36f2e2261b Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sun, 29 May 2022 19:55:50 +0800 Subject: [PATCH 0028/2015] feat: add customed titlebar --- .../lib/desktop/pages/desktop_home_page.dart | 54 +++- .../desktop/pages/desktop_remote_page.dart | 16 - .../lib/desktop/widgets/titlebar_widget.dart | 66 ++++ flutter/lib/main.dart | 34 ++- flutter/linux/my_application.cc | 5 +- flutter/macos/Runner/MainFlutterWindow.swift | 7 +- flutter/pubspec.lock | 281 ++++++++++-------- flutter/pubspec.yaml | 1 + flutter/windows/runner/main.cpp | 2 + 9 files changed, 296 insertions(+), 170 deletions(-) delete mode 100644 flutter/lib/desktop/pages/desktop_remote_page.dart create mode 100644 flutter/lib/desktop/widgets/titlebar_widget.dart diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 90566e165..1d0cd2b9d 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/desktop/pages/connection_page.dart'; +import 'package:flutter_hbb/desktop/widgets/titlebar_widget.dart'; import 'package:flutter_hbb/models/model.dart'; import 'package:provider/provider.dart'; @@ -11,26 +12,49 @@ class DesktopHomePage extends StatefulWidget { State createState() => _DesktopHomePageState(); } +const borderColor = Color(0xFF2F65BA); + class _DesktopHomePageState extends State { @override Widget build(BuildContext context) { return Scaffold( - body: Container( - child: Row( - children: [ - Flexible( - child: buildServerInfo(context), - flex: 1, + body: Column( + children: [ + Row( + children: [ + DesktopTitleBar( + child: Center( + child: Text( + "RustDesk", + style: TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.bold), + ), + ), + ) + ], + ), + Expanded( + child: Container( + child: Row( + children: [ + Flexible( + child: buildServerInfo(context), + flex: 1, + ), + SizedBox( + width: 16.0, + ), + Flexible( + child: buildServerBoard(context), + flex: 4, + ), + ], + ), ), - SizedBox( - width: 16.0, - ), - Flexible( - child: buildServerBoard(context), - flex: 4, - ), - ], - ), + ), + ], ), ); } diff --git a/flutter/lib/desktop/pages/desktop_remote_page.dart b/flutter/lib/desktop/pages/desktop_remote_page.dart deleted file mode 100644 index 748e4cf3c..000000000 --- a/flutter/lib/desktop/pages/desktop_remote_page.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:flutter/material.dart'; - -/// Remote Page, use it in multi window context -class DesktopRemotePage extends StatefulWidget { - const DesktopRemotePage({Key? key}) : super(key: key); - - @override - State createState() => _DesktopRemotePageState(); -} - -class _DesktopRemotePageState extends State { - @override - Widget build(BuildContext context) { - return Container(); - } -} diff --git a/flutter/lib/desktop/widgets/titlebar_widget.dart b/flutter/lib/desktop/widgets/titlebar_widget.dart new file mode 100644 index 000000000..f238cb4cd --- /dev/null +++ b/flutter/lib/desktop/widgets/titlebar_widget.dart @@ -0,0 +1,66 @@ +import 'package:bitsdojo_window/bitsdojo_window.dart'; +import 'package:flutter/material.dart'; + +const sidebarColor = Color(0xFF0C6AF6); +const backgroundStartColor = Color(0xFF7BBCF5); +const backgroundEndColor = Color(0xFF0CCBF6); + +class DesktopTitleBar extends StatelessWidget { + final Widget? child; + + const DesktopTitleBar({Key? key, this.child}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Expanded( + child: Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [backgroundStartColor, backgroundEndColor], + stops: [0.0, 1.0]), + ), + child: WindowTitleBarBox( + child: Row( + children: [ + Expanded( + child: MoveWindow( + child: child, + )), + 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), + MaximizeWindowButton(colors: buttonColors), + CloseWindowButton(colors: closeButtonColors), + ], + ); + } +} diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 2ab1586f0..1d8d6ab57 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -1,12 +1,12 @@ import 'dart:convert'; +import 'package:bitsdojo_window/bitsdojo_window.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart'; import 'package:flutter_hbb/desktop/screen/desktop_remote_screen.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:provider/provider.dart'; -import 'package:window_manager/window_manager.dart'; import 'common.dart'; import 'mobile/pages/home_page.dart'; @@ -50,22 +50,28 @@ void runRustDeskApp(List args) async { } } else { // main window - await windowManager.ensureInitialized(); + // await windowManager.ensureInitialized(); // start service FFI.serverModel.startService(); - WindowOptions windowOptions = WindowOptions( - size: Size(1280, 720), - center: true, - backgroundColor: Colors.transparent, - skipTaskbar: false, - titleBarStyle: TitleBarStyle.normal, - ); - windowManager.waitUntilReadyToShow(windowOptions, () async { - await windowManager.show(); - await windowManager.focus(); - }); - + // WindowOptions windowOptions = WindowOptions( + // size: Size(1280, 720), + // center: true, + // backgroundColor: Colors.transparent, + // skipTaskbar: false, + // titleBarStyle: TitleBarStyle.normal, + // ); + // windowManager.waitUntilReadyToShow(windowOptions, () async { + // await windowManager.show(); + // await windowManager.focus(); + // }); runApp(App()); + doWhenWindowReady(() { + const initialSize = Size(1280, 720); + appWindow.minSize = initialSize; + appWindow.size = initialSize; + appWindow.alignment = Alignment.center; + appWindow.show(); + }); } } diff --git a/flutter/linux/my_application.cc b/flutter/linux/my_application.cc index fbbf4ab0d..64d6e614a 100644 --- a/flutter/linux/my_application.cc +++ b/flutter/linux/my_application.cc @@ -1,6 +1,7 @@ #include "my_application.h" #include +#include #ifdef GDK_WINDOWING_X11 #include #endif @@ -47,7 +48,9 @@ static void my_application_activate(GApplication* application) { gtk_window_set_title(window, "flutter_hbb"); } - gtk_window_set_default_size(window, 1280, 720); + auto bdw = bitsdojo_window_from(window); // <--- add this line + bdw->setCustomFrame(true); // <-- add this line + //gtk_window_set_default_size(window, 1280, 720); // <-- comment this line gtk_widget_show(GTK_WIDGET(window)); g_autoptr(FlDartProject) project = fl_dart_project_new(); diff --git a/flutter/macos/Runner/MainFlutterWindow.swift b/flutter/macos/Runner/MainFlutterWindow.swift index 2722837ec..f3ed804b1 100644 --- a/flutter/macos/Runner/MainFlutterWindow.swift +++ b/flutter/macos/Runner/MainFlutterWindow.swift @@ -1,7 +1,8 @@ import Cocoa import FlutterMacOS +import bitsdojo_window_macos -class MainFlutterWindow: NSWindow { +class MainFlutterWindow: BitsdojoWindow { override func awakeFromNib() { let flutterViewController = FlutterViewController.init() let windowFrame = self.frame @@ -12,4 +13,8 @@ class MainFlutterWindow: NSWindow { super.awakeFromNib() } + + override func bitsdojo_window_configure() -> UInt { + return BDW_CUSTOM_FRAME | BDW_HIDE_ON_STARTUP + } } diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 2f7f30ec9..0c82ce182 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -5,196 +5,231 @@ packages: dependency: transitive description: name: archive - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.3.0" args: dependency: transitive description: name: args - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.3.1" async: dependency: transitive description: name: async - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.8.2" + bitsdojo_window: + dependency: "direct main" + description: + name: bitsdojo_window + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.1.2" + bitsdojo_window_linux: + dependency: transitive + description: + name: bitsdojo_window_linux + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.1.2" + bitsdojo_window_macos: + dependency: transitive + description: + name: bitsdojo_window_macos + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.1.2" + bitsdojo_window_platform_interface: + dependency: transitive + description: + name: bitsdojo_window_platform_interface + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.1.2" + bitsdojo_window_windows: + dependency: transitive + description: + name: bitsdojo_window_windows + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.1.2" boolean_selector: dependency: transitive description: name: boolean_selector - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0" characters: dependency: transitive description: name: characters - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.0" charcode: dependency: transitive description: name: charcode - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.1" clock: dependency: transitive description: name: clock - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.0" collection: dependency: transitive description: name: collection - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.16.0" cross_file: dependency: transitive description: name: cross_file - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "0.3.3" + version: "0.3.3+1" crypto: dependency: transitive description: name: crypto - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.2" cupertino_icons: dependency: "direct main" description: name: cupertino_icons - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.4" dash_chat: dependency: "direct main" description: name: dash_chat - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.16" desktop_multi_window: dependency: "direct main" description: name: desktop_multi_window - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.0.2" device_info: dependency: "direct main" description: name: device_info - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.3" device_info_platform_interface: dependency: transitive description: name: device_info_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.1" draggable_float_widget: dependency: "direct main" description: name: draggable_float_widget - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.0.2" event_bus: dependency: transitive description: name: event_bus - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.0" external_path: dependency: "direct main" description: name: external_path - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.1" fake_async: dependency: transitive description: name: fake_async - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.0" ffi: dependency: "direct main" description: name: ffi - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.1" file: dependency: transitive description: name: file - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "6.1.2" firebase_analytics: dependency: "direct main" description: name: firebase_analytics - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "9.1.8" + version: "9.1.9" firebase_analytics_platform_interface: dependency: transitive description: name: firebase_analytics_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "3.1.6" + version: "3.1.7" firebase_analytics_web: dependency: transitive description: name: firebase_analytics_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "0.4.0+13" + version: "0.4.0+14" firebase_core: dependency: transitive description: name: firebase_core - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "1.17.0" + version: "1.17.1" firebase_core_platform_interface: dependency: transitive description: name: firebase_core_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "4.4.0" firebase_core_web: dependency: transitive description: name: firebase_core_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.6.4" fixnum: dependency: transitive description: name: fixnum - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.1" flutter: @@ -206,42 +241,42 @@ packages: dependency: "direct main" description: name: flutter_breadcrumb - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.1" flutter_launcher_icons: dependency: "direct dev" description: name: flutter_launcher_icons - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.9.2" flutter_parsed_text: dependency: transitive description: name: flutter_parsed_text - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.1" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.6" flutter_rust_bridge: dependency: "direct main" description: name: flutter_rust_bridge - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "1.30.0" + version: "1.32.0" flutter_smart_dialog: dependency: "direct main" description: name: flutter_smart_dialog - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "4.3.1" flutter_test: @@ -258,238 +293,238 @@ packages: dependency: "direct main" description: name: http - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.13.4" http_parser: dependency: transitive description: name: http_parser - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "4.0.1" image: dependency: "direct main" description: name: image - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "3.1.3" + version: "3.2.0" image_picker: dependency: "direct main" description: name: image_picker - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.8.5+3" image_picker_android: dependency: transitive description: name: image_picker_android - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.8.4+13" image_picker_for_web: dependency: transitive description: name: image_picker_for_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.8" image_picker_ios: dependency: transitive description: name: image_picker_ios - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.8.5+5" image_picker_platform_interface: dependency: transitive description: name: image_picker_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.5.0" intl: dependency: transitive description: name: intl - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.17.0" js: dependency: transitive description: name: js - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.6.4" matcher: dependency: transitive description: name: matcher - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.12.11" material_color_utilities: dependency: transitive description: name: material_color_utilities - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.1.4" meta: dependency: transitive description: name: meta - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.7.0" nested: dependency: transitive description: name: nested - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.0" package_info_plus: dependency: "direct main" description: name: package_info_plus - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.4.2" package_info_plus_linux: dependency: transitive description: name: package_info_plus_linux - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.5" package_info_plus_macos: dependency: transitive description: name: package_info_plus_macos - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.0" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.2" package_info_plus_web: dependency: transitive description: name: package_info_plus_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.5" package_info_plus_windows: dependency: transitive description: name: package_info_plus_windows - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.5" path: dependency: transitive description: name: path - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.8.1" path_provider: dependency: "direct main" description: name: path_provider - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.10" path_provider_android: dependency: transitive description: name: path_provider_android - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.14" path_provider_ios: dependency: transitive description: name: path_provider_ios - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.9" path_provider_linux: dependency: transitive description: name: path_provider_linux - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "2.1.6" + version: "2.1.7" path_provider_macos: dependency: transitive description: name: path_provider_macos - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.6" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.4" path_provider_windows: dependency: transitive description: name: path_provider_windows - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "2.0.6" + version: "2.0.7" pedantic: dependency: transitive description: name: pedantic - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.11.1" petitparser: dependency: transitive description: name: petitparser - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "4.4.0" + version: "5.0.0" platform: dependency: transitive description: name: platform - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.0" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.2" process: dependency: transitive description: name: process - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "4.2.4" provider: dependency: "direct main" description: name: provider - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "5.0.0" qr_code_scanner: @@ -505,70 +540,70 @@ packages: dependency: transitive description: name: quiver - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.0" settings_ui: dependency: "direct main" description: name: settings_ui - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.2" shared_preferences: dependency: "direct main" description: name: shared_preferences - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.15" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.12" shared_preferences_ios: dependency: transitive description: name: shared_preferences_ios - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.1" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.1" shared_preferences_macos: dependency: transitive description: name: shared_preferences_macos - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.4" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.0" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.4" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.1" sky_engine: @@ -580,219 +615,219 @@ packages: dependency: transitive description: name: source_span - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.8.2" stack_trace: dependency: transitive description: name: stack_trace - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.10.0" stream_channel: dependency: transitive description: name: stream_channel - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0" string_scanner: dependency: transitive description: name: string_scanner - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.0" term_glyph: dependency: transitive description: name: term_glyph - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.0" test_api: dependency: transitive description: name: test_api - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.4.9" toggle_switch: dependency: "direct main" description: name: toggle_switch - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.4.0" transparent_image: dependency: transitive description: name: transparent_image - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.0" tuple: dependency: "direct main" description: name: tuple - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.0" typed_data: dependency: transitive description: name: typed_data - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "1.3.0" + version: "1.3.1" url_launcher: dependency: "direct main" description: name: url_launcher - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "6.1.2" url_launcher_android: dependency: transitive description: name: url_launcher_android - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "6.0.17" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "6.0.17" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.1" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.5" url_launcher_web: dependency: transitive description: name: url_launcher_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.11" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.1" uuid: dependency: transitive description: name: uuid - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.6" vector_math: dependency: transitive description: name: vector_math - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.2" wakelock: dependency: "direct main" description: name: wakelock - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.5.6" wakelock_macos: dependency: transitive description: name: wakelock_macos - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.4.0" wakelock_platform_interface: dependency: transitive description: name: wakelock_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.3.0" wakelock_web: dependency: transitive description: name: wakelock_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.4.0" wakelock_windows: dependency: transitive description: name: wakelock_windows - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.2.0" win32: dependency: transitive description: name: win32 - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "2.5.2" + version: "2.6.1" window_manager: dependency: "direct main" description: name: window_manager - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.2.3" xdg_directories: dependency: transitive description: name: xdg_directories - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.2.0+1" xml: dependency: transitive description: name: xml - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "5.3.1" + version: "6.1.0" yaml: dependency: transitive description: name: yaml - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.1" zxing2: dependency: "direct main" description: name: zxing2 - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.1.0" sdks: - dart: ">=2.17.0-0 <3.0.0" + dart: ">=2.17.0 <3.0.0" flutter: ">=3.0.0" diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 75ad90be9..e74b7fd02 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -57,6 +57,7 @@ dependencies: flutter_rust_bridge: ^1.30.0 window_manager: ^0.2.3 desktop_multi_window: ^0.0.2 + bitsdojo_window: ^0.1.2 dev_dependencies: flutter_launcher_icons: ^0.9.1 diff --git a/flutter/windows/runner/main.cpp b/flutter/windows/runner/main.cpp index bbc7d344b..a32464559 100644 --- a/flutter/windows/runner/main.cpp +++ b/flutter/windows/runner/main.cpp @@ -4,7 +4,9 @@ #include "flutter_window.h" #include "utils.h" +#include +auto bdw = bitsdojo_window_configure(BDW_CUSTOM_FRAME | BDW_HIDE_ON_STARTUP); int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, _In_ wchar_t *command_line, _In_ int show_command) { // Attach to console when present (e.g., 'flutter run') or create a From 82895e6951e5d42a27494e9584a2e6ba3721c36e Mon Sep 17 00:00:00 2001 From: kingtous Date: Mon, 30 May 2022 13:25:06 +0800 Subject: [PATCH 0029/2015] opt & fix: - main window ui: adapt pc logic - fix: platform infomation using device info plus Signed-off-by: Kingtous --- flutter/lib/common.dart | 21 +- .../lib/desktop/pages/connection_page.dart | 143 ++++++---- .../lib/desktop/pages/desktop_home_page.dart | 3 +- .../lib/desktop/widgets/titlebar_widget.dart | 4 +- flutter/lib/models/native_model.dart | 18 +- flutter/linux/my_application.cc | 4 +- flutter/pubspec.lock | 270 ++++++++++-------- flutter/pubspec.yaml | 2 +- src/flutter_ffi.rs | 4 +- 9 files changed, 278 insertions(+), 191 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index e66f8d79c..32f7c4bfa 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1,6 +1,7 @@ +import 'dart:async'; + import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'dart:async'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'models/model.dart'; @@ -35,6 +36,7 @@ class MyTheme { static const Color border = Color(0xFFCCCCCC); static const Color idColor = Color(0xFF00B6F0); static const Color darkGray = Color(0xFFB9BABC); + static const Color dark = Colors.black87; } final ButtonStyle flatButtonStyle = TextButton.styleFrom( @@ -97,9 +99,9 @@ class DialogManager { static Future show(DialogBuilder builder, {bool clickMaskDismiss = false, - bool backDismiss = false, - String? tag, - bool useAnimation = true}) async { + bool backDismiss = false, + String? tag, + bool useAnimation = true}) async { final t; if (tag != null) { t = tag; @@ -124,11 +126,10 @@ class DialogManager { } class CustomAlertDialog extends StatelessWidget { - CustomAlertDialog( - {required this.title, - required this.content, - required this.actions, - this.contentPadding}); + CustomAlertDialog({required this.title, + required this.content, + required this.actions, + this.contentPadding}); final Widget title; final Widget content; @@ -141,7 +142,7 @@ class CustomAlertDialog extends StatelessWidget { scrollable: true, title: title, contentPadding: - EdgeInsets.symmetric(horizontal: contentPadding ?? 25, vertical: 10), + EdgeInsets.symmetric(horizontal: contentPadding ?? 25, vertical: 10), content: content, actions: actions, ); diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 6f0a8115a..703d0a79a 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -52,7 +52,11 @@ class _ConnectionPageState extends State { crossAxisAlignment: CrossAxisAlignment.center, children: [ getUpdateUI(), - getSearchBarUI(), + Row( + children: [ + getSearchBarUI(), + ], + ), SizedBox(height: 12), getPeers(), ]), @@ -61,9 +65,9 @@ class _ConnectionPageState extends State { /// Callback for the connect button. /// Connects to the selected peer. - void onConnect() { + void onConnect({bool isFileTransfer = false}) { var id = _idController.text.trim(); - connect(id); + connect(id, isFileTransfer: isFileTransfer); } /// Connect to a peer with [id]. @@ -72,9 +76,11 @@ class _ConnectionPageState extends State { if (id == '') return; id = id.replaceAll(' ', ''); if (isFileTransfer) { - if (!await PermissionManager.check("file")) { - if (!await PermissionManager.request("file")) { - return; + if (!isDesktop) { + if (!await PermissionManager.check("file")) { + if (!await PermissionManager.request("file")) { + return; + } } } Navigator.push( @@ -126,61 +132,100 @@ class _ConnectionPageState extends State { /// Search for a peer and connect to it if the id exists. Widget getSearchBarUI() { var w = Padding( - padding: const EdgeInsets.fromLTRB(16.0, 8.0, 16.0, 0.0), + padding: const EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 16.0), child: Container( - height: 84, child: Padding( - padding: const EdgeInsets.only(top: 8, bottom: 8), + padding: const EdgeInsets.only(top: 16, bottom: 16), child: Ink( decoration: BoxDecoration( color: MyTheme.white, borderRadius: const BorderRadius.all(Radius.circular(13)), ), - child: Row( - children: [ - Expanded( - child: Container( - padding: const EdgeInsets.only(left: 16, right: 16), - child: TextField( - autocorrect: false, - enableSuggestions: false, - keyboardType: TextInputType.visiblePassword, - // keyboardType: TextInputType.number, - style: TextStyle( - fontFamily: 'WorkSans', - fontWeight: FontWeight.bold, - fontSize: 30, - color: MyTheme.idColor, - ), - decoration: InputDecoration( - labelText: translate('Remote ID'), - // hintText: 'Enter your remote ID', - border: InputBorder.none, - helperStyle: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - color: MyTheme.darkGray, - ), - labelStyle: TextStyle( - fontWeight: FontWeight.w600, - fontSize: 16, - letterSpacing: 0.2, - color: MyTheme.darkGray, + child: Column( + children: [ + Row( + children: [ + Expanded( + child: Container( + padding: const EdgeInsets.only(left: 16, right: 16), + child: TextField( + autocorrect: false, + enableSuggestions: false, + keyboardType: TextInputType.visiblePassword, + // keyboardType: TextInputType.number, + style: TextStyle( + fontFamily: 'WorkSans', + fontWeight: FontWeight.bold, + fontSize: 30, + // color: MyTheme.idColor, + ), + decoration: InputDecoration( + labelText: translate('Control Remote Desktop'), + // hintText: 'Enter your remote ID', + // border: InputBorder., + border: OutlineInputBorder( + borderRadius: BorderRadius.zero), + helperStyle: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + color: MyTheme.dark, + ), + labelStyle: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 26, + letterSpacing: 0.2, + color: MyTheme.dark, + ), + ), + controller: _idController, ), ), - controller: _idController, ), - ), + ], ), - SizedBox( - width: 60, - height: 60, - child: IconButton( - icon: Icon(Icons.arrow_forward, - color: MyTheme.darkGray, size: 45), - onPressed: onConnect, + Padding( + padding: const EdgeInsets.symmetric( + vertical: 16.0, horizontal: 16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + OutlinedButton( + onPressed: () { + onConnect(isFileTransfer: true); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 8.0, horizontal: 8.0), + child: Text( + translate( + "File Transfer", + ), + style: TextStyle(color: MyTheme.dark), + ), + ), + ), + SizedBox( + width: 30, + ), + OutlinedButton( + onPressed: onConnect, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 8.0, horizontal: 16.0), + child: Text( + translate( + "Connection", + ), + style: TextStyle(color: MyTheme.white), + ), + ), + style: OutlinedButton.styleFrom( + backgroundColor: Colors.blueAccent, + ), + ), + ], ), - ), + ) ], ), ), diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 1d0cd2b9d..fdffda031 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -78,10 +78,11 @@ class _DesktopHomePageState extends State { buildServerBoard(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, children: [ // buildControlPanel(context), // buildRecentSession(context), - ConnectionPage() + Expanded(child: ConnectionPage()) ], ); } diff --git a/flutter/lib/desktop/widgets/titlebar_widget.dart b/flutter/lib/desktop/widgets/titlebar_widget.dart index f238cb4cd..f98b7cc79 100644 --- a/flutter/lib/desktop/widgets/titlebar_widget.dart +++ b/flutter/lib/desktop/widgets/titlebar_widget.dart @@ -2,8 +2,8 @@ import 'package:bitsdojo_window/bitsdojo_window.dart'; import 'package:flutter/material.dart'; const sidebarColor = Color(0xFF0C6AF6); -const backgroundStartColor = Color(0xFF7BBCF5); -const backgroundEndColor = Color(0xFF0CCBF6); +const backgroundStartColor = Color(0xFF0583EA); +const backgroundEndColor = Color(0xFF0697EA); class DesktopTitleBar extends StatelessWidget { final Widget? child; diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index a8803a8f8..9527555d0 100644 --- a/flutter/lib/models/native_model.dart +++ b/flutter/lib/models/native_model.dart @@ -3,7 +3,7 @@ import 'dart:ffi'; import 'dart:io'; import 'dart:typed_data'; -import 'package:device_info/device_info.dart'; +import 'package:device_info_plus/device_info_plus.dart'; import 'package:external_path/external_path.dart'; import 'package:ffi/ffi.dart'; import 'package:flutter/services.dart'; @@ -101,11 +101,23 @@ class PlatformFFI { AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; name = '${androidInfo.brand}-${androidInfo.model}'; id = androidInfo.id.hashCode.toString(); - androidVersion = androidInfo.version.sdkInt; + androidVersion = androidInfo.version.sdkInt ?? 0; } else if (Platform.isIOS) { IosDeviceInfo iosInfo = await deviceInfo.iosInfo; - name = iosInfo.utsname.machine; + name = iosInfo.utsname.machine ?? ""; id = iosInfo.identifierForVendor.hashCode.toString(); + } else if (Platform.isLinux) { + LinuxDeviceInfo linuxInfo = await deviceInfo.linuxInfo; + name = linuxInfo.name; + id = linuxInfo.machineId ?? linuxInfo.id; + } else if (Platform.isWindows) { + WindowsDeviceInfo winInfo = await deviceInfo.windowsInfo; + name = winInfo.computerName; + id = winInfo.computerName; + } else if (Platform.isMacOS) { + MacOsDeviceInfo macOsInfo = await deviceInfo.macOsInfo; + name = macOsInfo.computerName; + id = macOsInfo.systemGUID ?? ""; } print("info1-id:$id,info2-name:$name,dir:$_dir,homeDir:$_homeDir"); setByName('info1', id); diff --git a/flutter/linux/my_application.cc b/flutter/linux/my_application.cc index 64d6e614a..f726dd76c 100644 --- a/flutter/linux/my_application.cc +++ b/flutter/linux/my_application.cc @@ -41,11 +41,11 @@ static void my_application_activate(GApplication* application) { if (use_header_bar) { GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); gtk_widget_show(GTK_WIDGET(header_bar)); - gtk_header_bar_set_title(header_bar, "flutter_hbb"); + gtk_header_bar_set_title(header_bar, "rustdesk"); gtk_header_bar_set_show_close_button(header_bar, TRUE); gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); } else { - gtk_window_set_title(window, "flutter_hbb"); + gtk_window_set_title(window, "rustdesk"); } auto bdw = bitsdojo_window_from(window); // <--- add this line diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 0c82ce182..04e88981e 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -5,231 +5,259 @@ packages: dependency: transitive description: name: archive - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.3.0" args: dependency: transitive description: name: args - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.3.1" async: dependency: transitive description: name: async - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.8.2" bitsdojo_window: dependency: "direct main" description: name: bitsdojo_window - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.1.2" bitsdojo_window_linux: dependency: transitive description: name: bitsdojo_window_linux - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.1.2" bitsdojo_window_macos: dependency: transitive description: name: bitsdojo_window_macos - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.1.2" bitsdojo_window_platform_interface: dependency: transitive description: name: bitsdojo_window_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.1.2" bitsdojo_window_windows: dependency: transitive description: name: bitsdojo_window_windows - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.1.2" boolean_selector: dependency: transitive description: name: boolean_selector - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.0" characters: dependency: transitive description: name: characters - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.2.0" charcode: dependency: transitive description: name: charcode - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.3.1" clock: dependency: transitive description: name: clock - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.1.0" collection: dependency: transitive description: name: collection - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.16.0" cross_file: dependency: transitive description: name: cross_file - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.3.3+1" crypto: dependency: transitive description: name: crypto - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.2" cupertino_icons: dependency: "direct main" description: name: cupertino_icons - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.4" dash_chat: dependency: "direct main" description: name: dash_chat - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.1.16" desktop_multi_window: dependency: "direct main" description: name: desktop_multi_window - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.0.2" - device_info: + device_info_plus: dependency: "direct main" description: - name: device_info - url: "https://pub.flutter-io.cn" + name: device_info_plus + url: "https://pub.dartlang.org" source: hosted - version: "2.0.3" - device_info_platform_interface: + version: "3.2.3" + device_info_plus_linux: dependency: transitive description: - name: device_info_platform_interface - url: "https://pub.flutter-io.cn" + name: device_info_plus_linux + url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "2.1.1" + device_info_plus_macos: + dependency: transitive + description: + name: device_info_plus_macos + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.3" + device_info_plus_platform_interface: + dependency: transitive + description: + name: device_info_plus_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.0+1" + device_info_plus_web: + dependency: transitive + description: + name: device_info_plus_web + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + device_info_plus_windows: + dependency: transitive + description: + name: device_info_plus_windows + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" draggable_float_widget: dependency: "direct main" description: name: draggable_float_widget - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.0.2" event_bus: dependency: transitive description: name: event_bus - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.0" external_path: dependency: "direct main" description: name: external_path - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.1" fake_async: dependency: transitive description: name: fake_async - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.3.0" ffi: dependency: "direct main" description: name: ffi - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.2.1" file: dependency: transitive description: name: file - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "6.1.2" firebase_analytics: dependency: "direct main" description: name: firebase_analytics - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "9.1.9" firebase_analytics_platform_interface: dependency: transitive description: name: firebase_analytics_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.1.7" firebase_analytics_web: dependency: transitive description: name: firebase_analytics_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.4.0+14" firebase_core: dependency: transitive description: name: firebase_core - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.17.1" firebase_core_platform_interface: dependency: transitive description: name: firebase_core_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "4.4.0" firebase_core_web: dependency: transitive description: name: firebase_core_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.6.4" fixnum: dependency: transitive description: name: fixnum - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.1" flutter: @@ -241,44 +269,44 @@ packages: dependency: "direct main" description: name: flutter_breadcrumb - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.1" flutter_launcher_icons: dependency: "direct dev" description: name: flutter_launcher_icons - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.9.2" flutter_parsed_text: dependency: transitive description: name: flutter_parsed_text - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.2.1" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.6" flutter_rust_bridge: dependency: "direct main" description: name: flutter_rust_bridge - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.32.0" flutter_smart_dialog: dependency: "direct main" description: name: flutter_smart_dialog - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "4.3.1" + version: "4.3.2" flutter_test: dependency: "direct dev" description: flutter @@ -293,238 +321,238 @@ packages: dependency: "direct main" description: name: http - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.13.4" http_parser: dependency: transitive description: name: http_parser - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "4.0.1" image: dependency: "direct main" description: name: image - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.2.0" image_picker: dependency: "direct main" description: name: image_picker - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.8.5+3" image_picker_android: dependency: transitive description: name: image_picker_android - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.8.4+13" image_picker_for_web: dependency: transitive description: name: image_picker_for_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.8" image_picker_ios: dependency: transitive description: name: image_picker_ios - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.8.5+5" image_picker_platform_interface: dependency: transitive description: name: image_picker_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.5.0" intl: dependency: transitive description: name: intl - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.17.0" js: dependency: transitive description: name: js - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.6.4" matcher: dependency: transitive description: name: matcher - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.12.11" material_color_utilities: dependency: transitive description: name: material_color_utilities - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.1.4" meta: dependency: transitive description: name: meta - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.7.0" nested: dependency: transitive description: name: nested - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.0" package_info_plus: dependency: "direct main" description: name: package_info_plus - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.4.2" package_info_plus_linux: dependency: transitive description: name: package_info_plus_linux - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.5" package_info_plus_macos: dependency: transitive description: name: package_info_plus_macos - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.3.0" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.2" package_info_plus_web: dependency: transitive description: name: package_info_plus_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.5" package_info_plus_windows: dependency: transitive description: name: package_info_plus_windows - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.5" path: dependency: transitive description: name: path - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.8.1" path_provider: dependency: "direct main" description: name: path_provider - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.10" path_provider_android: dependency: transitive description: name: path_provider_android - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.14" path_provider_ios: dependency: transitive description: name: path_provider_ios - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.9" path_provider_linux: dependency: transitive description: name: path_provider_linux - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.7" path_provider_macos: dependency: transitive description: name: path_provider_macos - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.6" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.4" path_provider_windows: dependency: transitive description: name: path_provider_windows - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.7" pedantic: dependency: transitive description: name: pedantic - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.11.1" petitparser: dependency: transitive description: name: petitparser - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "5.0.0" platform: dependency: transitive description: name: platform - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.1.0" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.2" process: dependency: transitive description: name: process - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "4.2.4" provider: dependency: "direct main" description: name: provider - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "5.0.0" qr_code_scanner: @@ -540,70 +568,70 @@ packages: dependency: transitive description: name: quiver - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.1.0" settings_ui: dependency: "direct main" description: name: settings_ui - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.2" shared_preferences: dependency: "direct main" description: name: shared_preferences - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.15" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.12" shared_preferences_ios: dependency: transitive description: name: shared_preferences_ios - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.1" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.1" shared_preferences_macos: dependency: transitive description: name: shared_preferences_macos - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.4" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.0" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.4" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.1" sky_engine: @@ -615,217 +643,217 @@ packages: dependency: transitive description: name: source_span - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.8.2" stack_trace: dependency: transitive description: name: stack_trace - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.10.0" stream_channel: dependency: transitive description: name: stream_channel - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.0" string_scanner: dependency: transitive description: name: string_scanner - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.1.0" term_glyph: dependency: transitive description: name: term_glyph - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.2.0" test_api: dependency: transitive description: name: test_api - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.4.9" toggle_switch: dependency: "direct main" description: name: toggle_switch - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.4.0" transparent_image: dependency: transitive description: name: transparent_image - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.0" tuple: dependency: "direct main" description: name: tuple - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.0" typed_data: dependency: transitive description: name: typed_data - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.3.1" url_launcher: dependency: "direct main" description: name: url_launcher - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "6.1.2" url_launcher_android: dependency: transitive description: name: url_launcher_android - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "6.0.17" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "6.0.17" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.1" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.5" url_launcher_web: dependency: transitive description: name: url_launcher_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.11" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.1" uuid: dependency: transitive description: name: uuid - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.6" vector_math: dependency: transitive description: name: vector_math - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.2" wakelock: dependency: "direct main" description: name: wakelock - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.5.6" wakelock_macos: dependency: transitive description: name: wakelock_macos - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.4.0" wakelock_platform_interface: dependency: transitive description: name: wakelock_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.3.0" wakelock_web: dependency: transitive description: name: wakelock_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.4.0" wakelock_windows: dependency: transitive description: name: wakelock_windows - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.2.0" win32: dependency: transitive description: name: win32 - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.6.1" window_manager: dependency: "direct main" description: name: window_manager - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.2.3" xdg_directories: dependency: transitive description: name: xdg_directories - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.2.0+1" xml: dependency: transitive description: name: xml - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "6.1.0" yaml: dependency: transitive description: name: yaml - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.1.1" zxing2: dependency: "direct main" description: name: zxing2 - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.1.0" sdks: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index e74b7fd02..72f741d37 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -35,7 +35,7 @@ dependencies: provider: ^5.0.0 tuple: ^2.0.0 wakelock: ^0.5.2 - device_info: ^2.0.2 + device_info_plus: ^3.2.3 firebase_analytics: ^9.1.5 package_info_plus: ^1.4.2 url_launcher: ^6.0.9 diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 398bd11c6..9897cd40e 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -171,11 +171,11 @@ unsafe extern "C" fn get_by_name(name: *const c_char, arg: *const c_char) -> *co } } // Server Side - #[cfg(target_os = "android")] + #[cfg(not(any(target_os = "ios")))] "clients_state" => { res = get_clients_state(); } - #[cfg(target_os = "android")] + #[cfg(not(any(target_os = "ios")))] "check_clients_length" => { if let Ok(value) = arg.to_str() { if value.parse::().unwrap_or(usize::MAX) != get_clients_length() { From 2228fba8c7f8373f8fb81269c100bde6f10dee02 Mon Sep 17 00:00:00 2001 From: SoLongAndThanksForAllThePizza <103753680+SoLongAndThanksForAllThePizza@users.noreply.github.com> Date: Mon, 30 May 2022 13:55:26 +0800 Subject: [PATCH 0030/2015] fix: make sure env_logger only init once --- src/flutter_ffi.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 9897cd40e..d21ac7996 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -46,7 +46,9 @@ fn initialize(app_dir: &str) { #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] { use hbb_common::env_logger::*; - init_from_env(Env::default().filter_or(DEFAULT_FILTER_ENV, "debug")); + if let Err(e) = try_init_from_env(Env::default().filter_or(DEFAULT_FILTER_ENV, "debug")) { + log::debug!("{}", e); + } } } From 7cd0940661b3685618ac0849f0b081ec69af5a8d Mon Sep 17 00:00:00 2001 From: SoLongAndThanksForAllThePizza <103753680+SoLongAndThanksForAllThePizza@users.noreply.github.com> Date: Mon, 30 May 2022 16:16:20 +0800 Subject: [PATCH 0031/2015] feat: insert core entry before launching flutter --- flutter/windows/runner/main.cpp | 34 +++++++++++++++++++++++++++++---- src/core_main.rs | 13 +++++++++++++ src/flutter_ffi.rs | 7 +++++++ src/lib.rs | 5 +++++ 4 files changed, 55 insertions(+), 4 deletions(-) create mode 100644 src/core_main.rs diff --git a/flutter/windows/runner/main.cpp b/flutter/windows/runner/main.cpp index a32464559..4073213e5 100644 --- a/flutter/windows/runner/main.cpp +++ b/flutter/windows/runner/main.cpp @@ -1,17 +1,41 @@ #include #include #include +#include #include "flutter_window.h" #include "utils.h" #include +typedef bool (*FUNC_RUSTDESK_CORE_MAIN)(void); + auto bdw = bitsdojo_window_configure(BDW_CUSTOM_FRAME | BDW_HIDE_ON_STARTUP); int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, - _In_ wchar_t *command_line, _In_ int show_command) { + _In_ wchar_t *command_line, _In_ int show_command) +{ + HINSTANCE hInstance = LoadLibraryA("librustdesk.dll"); + if (!hInstance) + { + std::cout << "Failed to load librustdesk.dll" << std::endl; + return EXIT_FAILURE; + } + FUNC_RUSTDESK_CORE_MAIN rustdesk_core_main = + (FUNC_RUSTDESK_CORE_MAIN)GetProcAddress(hInstance, "rustdesk_core_main"); + if (!rustdesk_core_main) + { + std::cout << "Failed to get rustdesk_core_main" << std::endl; + return EXIT_FAILURE; + } + if (!rustdesk_core_main()) + { + std::cout << "Rustdesk core returns false, exiting without launching Flutter app" << std::endl; + return EXIT_SUCCESS; + } + // Attach to console when present (e.g., 'flutter run') or create a // new console when running with a debugger. - if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) + { CreateAndAttachConsole(); } @@ -29,13 +53,15 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, FlutterWindow window(project); Win32Window::Point origin(10, 10); Win32Window::Size size(1280, 720); - if (!window.CreateAndShow(L"flutter_hbb", origin, size)) { + if (!window.CreateAndShow(L"flutter_hbb", origin, size)) + { return EXIT_FAILURE; } window.SetQuitOnClose(true); ::MSG msg; - while (::GetMessage(&msg, nullptr, 0, 0)) { + while (::GetMessage(&msg, nullptr, 0, 0)) + { ::TranslateMessage(&msg); ::DispatchMessage(&msg); } diff --git a/src/core_main.rs b/src/core_main.rs new file mode 100644 index 000000000..c50bb0835 --- /dev/null +++ b/src/core_main.rs @@ -0,0 +1,13 @@ +/// Main entry of the RustDesk Core. +/// Return true if the app should continue running with UI(possibly Flutter), false if the app should exit. +pub fn core_main() -> bool { + let args = std::env::args().collect::>(); + // TODO: implement core_main() + if args.len() > 1 { + if args[1] == "--cm" { + // For test purpose only, this should stop any new window from popping up when a new connection is established. + return false; + } + } + true +} diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index d21ac7996..a1b9b1e7b 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -52,6 +52,13 @@ fn initialize(app_dir: &str) { } } +/// FFI for rustdesk core's main entry. +/// Return true if the app should continue running with UI(possibly Flutter), false if the app should exit. +#[no_mangle] +pub extern "C" fn rustdesk_core_main() -> bool { + crate::core_main::core_main() +} + pub fn start_event_stream(s: StreamSink) -> ResultType<()> { let _ = flutter::EVENT_STREAM.write().unwrap().insert(s); Ok(()) diff --git a/src/lib.rs b/src/lib.rs index 7452bdb42..1dda032e1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -28,6 +28,11 @@ pub mod flutter; #[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] pub mod flutter_ffi; use common::*; +#[cfg(all( + not(any(target_os = "android", target_os = "ios")), + feature = "flutter" +))] +pub mod core_main; #[cfg(feature = "cli")] pub mod cli; #[cfg(all(windows, feature = "hbbs"))] From 7af663809fbdb04f9e011e353d208dd840d62fb5 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 30 May 2022 15:33:30 +0800 Subject: [PATCH 0032/2015] opt: adapt --cm Signed-off-by: Kingtous [linux] opt: add librustdesk.so filter --- flutter/linux/main.cc | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/flutter/linux/main.cc b/flutter/linux/main.cc index e7c5c5437..55fb650bc 100644 --- a/flutter/linux/main.cc +++ b/flutter/linux/main.cc @@ -1,6 +1,28 @@ +#include #include "my_application.h" +#define RUSTDESK_LIB_PATH "/usr/lib/rustdesk/librustdesk.so" +typedef bool (*RustDeskCoreMain)(); + +bool flutter_rustdesk_core_main() { + void* librustdesk = dlopen(RUSTDESK_LIB_PATH, RTLD_LAZY); + if (!librustdesk) { + fprintf(stderr,"load librustdesk.so failed\n"); + return true; + } + auto core_main = (RustDeskCoreMain) dlsym(librustdesk,"rustdesk_core_main"); + char* error; + if ((error = dlerror()) != nullptr) { + fprintf(stderr, "error finding rustdesk_core_main: %s", error); + return true; + } + return core_main(); +} + int main(int argc, char** argv) { + if (!flutter_rustdesk_core_main()) { + return 0; + } g_autoptr(MyApplication) app = my_application_new(); return g_application_run(G_APPLICATION(app), argc, argv); } From ac09c37516f5ad9bf8805aa1ba00f6946eb64d9d Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 31 May 2022 12:09:47 +0800 Subject: [PATCH 0033/2015] fix: method channel in multi window context Signed-off-by: Kingtous --- flutter/lib/main.dart | 17 +++-------------- flutter/pubspec.lock | 10 ++++++---- flutter/pubspec.yaml | 5 ++++- 3 files changed, 13 insertions(+), 19 deletions(-) diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 1d8d6ab57..336f5dda6 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -7,6 +7,7 @@ import 'package:flutter_hbb/desktop/screen/desktop_remote_screen.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:provider/provider.dart'; +import 'package:window_manager/window_manager.dart'; import 'common.dart'; import 'mobile/pages/home_page.dart'; @@ -32,6 +33,8 @@ void runRustDeskApp(List args) async { runApp(App()); return; } + // main window + await windowManager.ensureInitialized(); if (args.isNotEmpty && args.first == 'multi_window') { windowId = int.parse(args[1]); final argument = args[2].isEmpty @@ -49,21 +52,7 @@ void runRustDeskApp(List args) async { break; } } else { - // main window - // await windowManager.ensureInitialized(); - // start service FFI.serverModel.startService(); - // WindowOptions windowOptions = WindowOptions( - // size: Size(1280, 720), - // center: true, - // backgroundColor: Colors.transparent, - // skipTaskbar: false, - // titleBarStyle: TitleBarStyle.normal, - // ); - // windowManager.waitUntilReadyToShow(windowOptions, () async { - // await windowManager.show(); - // await windowManager.focus(); - // }); runApp(App()); doWhenWindowReady(() { const initialSize = Size(1280, 720); diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 04e88981e..4eaa1c877 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -123,10 +123,12 @@ packages: desktop_multi_window: dependency: "direct main" description: - name: desktop_multi_window - url: "https://pub.dartlang.org" - source: hosted - version: "0.0.2" + path: "." + ref: master + resolved-ref: "7150283dcd0c79450b98bf0a62b26df95897e53c" + url: "https://github.com/Kingtous/rustdesk_desktop_multi_window" + source: git + version: "0.0.1" device_info_plus: dependency: "direct main" description: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 72f741d37..008d4ef9d 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -56,7 +56,10 @@ dependencies: flutter_smart_dialog: ^4.3.1 flutter_rust_bridge: ^1.30.0 window_manager: ^0.2.3 - desktop_multi_window: ^0.0.2 + desktop_multi_window: + git: + url: https://github.com/Kingtous/rustdesk_desktop_multi_window + ref: master bitsdojo_window: ^0.1.2 dev_dependencies: From 18ad23435b4124ccdd8948d7d3cd3900a2852281 Mon Sep 17 00:00:00 2001 From: csf Date: Tue, 31 May 2022 14:44:06 +0800 Subject: [PATCH 0034/2015] multi remote instances --- flutter/lib/desktop/pages/remote_page.dart | 7 +- flutter/lib/models/model.dart | 36 ++- flutter/lib/models/native_model.dart | 6 +- src/flutter.rs | 311 ++++++++++----------- src/flutter_ffi.rs | 16 +- 5 files changed, 182 insertions(+), 194 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 6827bde60..b7d567482 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -404,7 +404,7 @@ class _RemotePageState extends State with WindowListener { icon: Icon(Icons.tv), onPressed: () { setState(() => _showEdit = false); - showOptions(); + showOptions(widget.id); }, ) ] + @@ -972,8 +972,9 @@ RadioListTile getRadio(String name, String toValue, String curValue, ); } -void showOptions() { - String quality = FFI.getByName('image_quality'); +void showOptions(String id) async { + // String quality = FFI.getByName('image_quality'); + String quality = await FFI.rustdeskImpl.getImageQuality(id: id) ?? 'balanced'; if (quality == '') quality = 'balanced'; String viewStyle = FFI.getByName('peer_option', 'view-style'); var displays = []; diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index d94e69341..6590dc41a 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -6,6 +6,7 @@ import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_hbb/generated_bridge.dart'; import 'package:flutter_hbb/models/chat_model.dart'; import 'package:flutter_hbb/models/file_model.dart'; import 'package:flutter_hbb/models/server_model.dart'; @@ -598,17 +599,17 @@ class CursorModel with ChangeNotifier { final rgba = Uint8List.fromList(colors.map((s) => s as int).toList()); var pid = FFI.id; ui.decodeImageFromPixels(rgba, width, height, ui.PixelFormat.rgba8888, - (image) { - if (FFI.id != pid) return; - _image = image; - _images[id] = Tuple3(image, _hotx, _hoty); - try { - // my throw exception, because the listener maybe already dispose - notifyListeners(); - } catch (e) { - print('notify cursor: $e'); - } - }); + (image) { + if (FFI.id != pid) return; + _image = image; + _images[id] = Tuple3(image, _hotx, _hoty); + try { + // my throw exception, because the listener maybe already dispose + notifyListeners(); + } catch (e) { + print('notify cursor: $e'); + } + }); } void updateCursorId(Map evt) { @@ -637,7 +638,8 @@ class CursorModel with ChangeNotifier { notifyListeners(); } - void updateDisplayOriginWithCursor(double x, double y, double xCursor, double yCursor) { + void updateDisplayOriginWithCursor( + double x, double y, double xCursor, double yCursor) { _displayOriginX = x; _displayOriginY = y; _x = xCursor; @@ -765,7 +767,7 @@ class FFI { return peers .map((s) => s as List) .map((s) => - Peer.fromJson(s[0] as String, s[1] as Map)) + Peer.fromJson(s[0] as String, s[1] as Map)) .toList(); } catch (e) { print('peers(): $e'); @@ -779,7 +781,11 @@ class FFI { setByName('connect_file_transfer', id); } else { FFI.chatModel.resetClientMode(); - setByName('connect', id); + // setByName('connect', id); + final stream = + FFI.rustdeskImpl.connect(id: id, isFileTransfer: isFileTransfer); + // listen stream ... + // every instance will bind a stream } FFI.id = id; } @@ -833,6 +839,8 @@ class FFI { PlatformFFI.setByName(name, value); } + static RustdeskImpl get rustdeskImpl => PlatformFFI.rustdeskImpl; + static handleMouse(Map evt) { var type = ''; var isMove = false; diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index 9527555d0..e1b9137b6 100644 --- a/flutter/lib/models/native_model.dart +++ b/flutter/lib/models/native_model.dart @@ -30,9 +30,12 @@ class PlatformFFI { static String _homeDir = ''; static F2? _getByName; static F3? _setByName; + static late RustdeskImpl _rustdeskImpl; static void Function(Map)? _eventCallback; static void Function(Uint8List)? _rgbaCallback; + static RustdeskImpl get rustdeskImpl => _rustdeskImpl; + static Future getVersion() async { PackageInfo packageInfo = await PackageInfo.fromPlatform(); return packageInfo.version; @@ -88,7 +91,8 @@ class PlatformFFI { dylib.lookupFunction, Pointer), F3>( 'set_by_name'); _dir = (await getApplicationDocumentsDirectory()).path; - _startListenEvent(RustdeskImpl(dylib)); + _rustdeskImpl = RustdeskImpl(dylib); + _startListenEvent(_rustdeskImpl); // global event try { _homeDir = (await ExternalPath.getExternalStorageDirectories())[0]; } catch (e) { diff --git a/src/flutter.rs b/src/flutter.rs index e40084450..c24923c72 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -26,17 +26,22 @@ use std::{ }; lazy_static::lazy_static! { - static ref SESSION: Arc>> = Default::default(); + // static ref SESSION: Arc>> = Default::default(); + static ref SESSIONS: RwLock> = Default::default(); pub static ref EVENT_STREAM: RwLock>> = Default::default(); // rust to dart event channel pub static ref RGBA_STREAM: RwLock>>>> = Default::default(); // rust to dart rgba (big u8 list) channel } -#[derive(Clone, Default)] +pub fn get_session(id: &str) -> Option<&Session> { + SESSIONS.read().unwrap().get(id) +} + +#[derive(Clone)] pub struct Session { id: String, - sender: Arc>>>, + sender: Arc>>>, // UI to rust lc: Arc>, - events2ui: Arc>>, + events2ui: Arc>>, } impl Session { @@ -46,40 +51,47 @@ impl Session { /// /// * `id` - The id of the remote session. /// * `is_file_transfer` - If the session is used for file transfer. - pub fn start(id: &str, is_file_transfer: bool) { - LocalConfig::set_remote_id(id); - Self::close(); - let mut session = Session::default(); + pub fn start(id: &str, is_file_transfer: bool, events2ui: StreamSink) { + LocalConfig::set_remote_id(&id); + // TODO check same id + // TODO close + // Self::close(); + let events2ui = Arc::new(RwLock::new(events2ui)); + let mut session = Session { + id: id.to_owned(), + sender: Default::default(), + lc: Default::default(), + events2ui, + }; session .lc .write() .unwrap() .initialize(id.to_owned(), false, false); - session.id = id.to_owned(); - *SESSION.write().unwrap() = Some(session.clone()); + SESSIONS + .write() + .unwrap() + .insert(id.to_owned(), session.clone()); std::thread::spawn(move || { Connection::start(session, is_file_transfer); }); } /// Get the current session instance. - pub fn get() -> Arc>> { - SESSION.clone() - } + // pub fn get() -> Arc>> { + // SESSION.clone() + // } /// Get the option of the current session. /// /// # Arguments /// /// * `name` - The name of the option to get. Currently only `remote_dir` is supported. - pub fn get_option(name: &str) -> String { - if let Some(session) = SESSION.read().unwrap().as_ref() { - if name == "remote_dir" { - return session.lc.read().unwrap().get_remote_dir(); - } - return session.lc.read().unwrap().get_option(name); + pub fn get_option(&self, name: &str) -> String { + if name == "remote_dir" { + return self.lc.read().unwrap().get_remote_dir(); } - "".to_owned() + self.lc.read().unwrap().get_option(name) } /// Set the option of the current session. @@ -88,78 +100,59 @@ impl Session { /// /// * `name` - The name of the option to set. Currently only `remote_dir` is supported. /// * `value` - The value of the option to set. - pub fn set_option(name: String, value: String) { - if let Some(session) = SESSION.read().unwrap().as_ref() { - let mut value = value; - if name == "remote_dir" { - value = session.lc.write().unwrap().get_all_remote_dir(value); - } - return session.lc.write().unwrap().set_option(name, value); + pub fn set_option(&self, name: String, value: String) { + let mut value = value; + let lc = self.lc.write().unwrap(); + if name == "remote_dir" { + value = lc.get_all_remote_dir(value); } + lc.set_option(name, value); } /// Input the OS password. - pub fn input_os_password(pass: String, activate: bool) { - if let Some(session) = SESSION.read().unwrap().as_ref() { - input_os_password(pass, activate, session.clone()); - } + pub fn input_os_password(&self, pass: String, activate: bool) { + input_os_password(pass, activate, self.clone()); } + // impl Interface /// Send message to the remote session. /// /// # Arguments /// /// * `data` - The data to send. See [`Data`] for more details. - fn send(data: Data) { - if let Some(session) = SESSION.read().unwrap().as_ref() { - session.send(data); - } - } - - /// Pop a event from the event queue. - pub fn pop_event() -> Option { - if let Some(session) = SESSION.read().unwrap().as_ref() { - session.events2ui.write().unwrap().pop_front() - } else { - None - } - } + // fn send(data: Data) { + // if let Some(session) = SESSION.read().unwrap().as_ref() { + // session.send(data); + // } + // } /// Toggle an option. - pub fn toggle_option(name: &str) { - if let Some(session) = SESSION.read().unwrap().as_ref() { - let msg = session.lc.write().unwrap().toggle_option(name.to_owned()); - if let Some(msg) = msg { - session.send_msg(msg); - } + pub fn toggle_option(&self, name: &str) { + let msg = self.lc.write().unwrap().toggle_option(name.to_owned()); + if let Some(msg) = msg { + self.send_msg(msg); } } /// Send a refresh command. - pub fn refresh() { - Self::send(Data::Message(LoginConfigHandler::refresh())); + pub fn refresh(&self) { + self.send(Data::Message(LoginConfigHandler::refresh())); } /// Get image quality. - pub fn get_image_quality() -> String { - if let Some(session) = SESSION.read().unwrap().as_ref() { - session.lc.read().unwrap().image_quality.clone() - } else { - "".to_owned() - } + pub fn get_image_quality(&self) -> String { + self.lc.read().unwrap().image_quality.clone() } /// Set image quality. - pub fn set_image_quality(value: &str) { - if let Some(session) = SESSION.read().unwrap().as_ref() { - let msg = session - .lc - .write() - .unwrap() - .save_image_quality(value.to_owned()); - if let Some(msg) = msg { - session.send_msg(msg); - } + pub fn set_image_quality(&self, value: &str) { + let msg = self + .lc + .write() + .unwrap() + .save_image_quality(value.to_owned()); + if let Some(msg) = msg { + self.send_msg(msg); } } @@ -169,12 +162,8 @@ impl Session { /// # Arguments /// /// * `name` - The name of the option to get. - pub fn get_toggle_option(name: &str) -> Option { - if let Some(session) = SESSION.read().unwrap().as_ref() { - Some(session.lc.write().unwrap().get_toggle_option(name)) - } else { - None - } + pub fn get_toggle_option(&self, name: &str) -> bool { + self.lc.write().unwrap().get_toggle_option(name) } /// Login. @@ -183,36 +172,28 @@ impl Session { /// /// * `password` - The password to login. /// * `remember` - If the password should be remembered. - pub fn login(password: &str, remember: bool) { - Session::send(Data::Login((password.to_owned(), remember))); + pub fn login(&self, password: &str, remember: bool) { + self.send(Data::Login((password.to_owned(), remember))); } /// Close the session. - pub fn close() { - Session::send(Data::Close); - SESSION.write().unwrap().take(); + pub fn close(&self) { + self.send(Data::Close); + let _ = SESSIONS.write().unwrap().remove(&self.id); } /// Reconnect to the current session. - pub fn reconnect() { - if let Some(session) = SESSION.read().unwrap().as_ref() { - if let Some(sender) = session.sender.read().unwrap().as_ref() { - sender.send(Data::Close).ok(); - } - let session = session.clone(); - std::thread::spawn(move || { - Connection::start(session, false); - }); - } + pub fn reconnect(&self) { + self.send(Data::Close); + let session = self.clone(); + std::thread::spawn(move || { + Connection::start(session, false); + }); } /// Get `remember` flag in [`LoginConfigHandler`]. - pub fn get_remember() -> bool { - if let Some(session) = SESSION.read().unwrap().as_ref() { - session.lc.read().unwrap().remember - } else { - false - } + pub fn get_remember(&self) -> bool { + self.lc.read().unwrap().remember } /// Send message over the current session. @@ -222,9 +203,7 @@ impl Session { /// * `msg` - The message to send. #[inline] pub fn send_msg(&self, msg: Message) { - if let Some(sender) = self.sender.read().unwrap().as_ref() { - sender.send(Data::Message(msg)).ok(); - } + self.send(Data::Message(msg)); } /// Send chat message over the current session. @@ -232,7 +211,7 @@ impl Session { /// # Arguments /// /// * `text` - The message to send. - pub fn send_chat(text: String) { + pub fn send_chat(&self, text: String) { let mut misc = Misc::new(); misc.set_chat_message(ChatMessage { text, @@ -240,49 +219,46 @@ impl Session { }); let mut msg_out = Message::new(); msg_out.set_misc(misc); - Self::send_msg_static(msg_out); + self.send_msg(msg_out); } + // file trait /// Send file over the current session. - pub fn send_files( - id: i32, - path: String, - to: String, - file_num: i32, - include_hidden: bool, - is_remote: bool, - ) { - if let Some(session) = SESSION.write().unwrap().as_mut() { - session.send_files(id, path, to, file_num, include_hidden, is_remote); - } - } + // pub fn send_files( + // id: i32, + // path: String, + // to: String, + // file_num: i32, + // include_hidden: bool, + // is_remote: bool, + // ) { + // if let Some(session) = SESSION.write().unwrap().as_mut() { + // session.send_files(id, path, to, file_num, include_hidden, is_remote); + // } + // } + // TODO into file trait /// Confirm file override. pub fn set_confirm_override_file( + &self, id: i32, file_num: i32, need_override: bool, remember: bool, is_upload: bool, ) { - if let Some(session) = SESSION.read().unwrap().as_ref() { - if let Some(sender) = session.sender.read().unwrap().as_ref() { - log::info!( - "confirm file transfer, job: {}, need_override: {}", - id, - need_override - ); - sender - .send(Data::SetConfirmOverrideFile(( - id, - file_num, - need_override, - remember, - is_upload, - ))) - .ok(); - } - } + log::info!( + "confirm file transfer, job: {}, need_override: {}", + id, + need_override + ); + self.send(Data::SetConfirmOverrideFile(( + id, + file_num, + need_override, + remember, + is_upload, + ))); } /// Static method to send message over the current session. @@ -290,12 +266,12 @@ impl Session { /// # Arguments /// /// * `msg` - The message to send. - #[inline] - pub fn send_msg_static(msg: Message) { - if let Some(session) = SESSION.read().unwrap().as_ref() { - session.send_msg(msg); - } - } + // #[inline] + // pub fn send_msg_static(msg: Message) { + // if let Some(session) = SESSION.read().unwrap().as_ref() { + // session.send_msg(msg); + // } + // } /// Push an event to the event queue. /// An event is stored as json in the event queue. @@ -309,9 +285,10 @@ impl Session { assert!(h.get("name").is_none()); h.insert("name", name); - if let Some(s) = EVENT_STREAM.read().unwrap().as_ref() { - s.add(serde_json::ser::to_string(&h).unwrap_or("".to_owned())); - }; + self.events2ui + .read() + .unwrap() + .add(serde_json::ser::to_string(&h).unwrap_or("".to_owned())); } /// Get platform of peer. @@ -321,15 +298,13 @@ impl Session { } /// Quick method for sending a ctrl_alt_del command. - pub fn ctrl_alt_del() { - if let Some(session) = SESSION.read().unwrap().as_ref() { - if session.peer_platform() == "Windows" { - let k = Key::ControlKey(ControlKey::CtrlAltDel); - session.key_down_or_up(1, k, false, false, false, false); - } else { - let k = Key::ControlKey(ControlKey::Delete); - session.key_down_or_up(3, k, true, true, false, false); - } + pub fn ctrl_alt_del(&self) { + if self.peer_platform() == "Windows" { + let k = Key::ControlKey(ControlKey::CtrlAltDel); + self.key_down_or_up(1, k, false, false, false, false); + } else { + let k = Key::ControlKey(ControlKey::Delete); + self.key_down_or_up(3, k, true, true, false, false); } } @@ -338,7 +313,7 @@ impl Session { /// # Arguments /// /// * `display` - The display to switch to. - pub fn switch_display(display: i32) { + pub fn switch_display(&self, display: i32) { let mut misc = Misc::new(); misc.set_switch_display(SwitchDisplay { display, @@ -346,15 +321,13 @@ impl Session { }); let mut msg_out = Message::new(); msg_out.set_misc(misc); - Self::send_msg_static(msg_out); + self.send_msg(msg_out); } /// Send lock screen command. - pub fn lock_screen() { - if let Some(session) = SESSION.read().unwrap().as_ref() { - let k = Key::ControlKey(ControlKey::LockScreen); - session.key_down_or_up(1, k, false, false, false, false); - } + pub fn lock_screen(&self) { + let k = Key::ControlKey(ControlKey::LockScreen); + self.key_down_or_up(1, k, false, false, false, false); } /// Send key input command. @@ -369,6 +342,7 @@ impl Session { /// * `shift` - If the shift key is also pressed. /// * `command` - If the command key is also pressed. pub fn input_key( + &self, name: &str, down: bool, press: bool, @@ -377,15 +351,13 @@ impl Session { shift: bool, command: bool, ) { - if let Some(session) = SESSION.read().unwrap().as_ref() { - let chars: Vec = name.chars().collect(); - if chars.len() == 1 { - let key = Key::_Raw(chars[0] as _); - session._input_key(key, down, press, alt, ctrl, shift, command); - } else { - if let Some(key) = KEY_MAP.get(name) { - session._input_key(key.clone(), down, press, alt, ctrl, shift, command); - } + let chars: Vec = name.chars().collect(); + if chars.len() == 1 { + let key = Key::_Raw(chars[0] as _); + self._input_key(key, down, press, alt, ctrl, shift, command); + } else { + if let Some(key) = KEY_MAP.get(name) { + self._input_key(key.clone(), down, press, alt, ctrl, shift, command); } } } @@ -396,12 +368,12 @@ impl Session { /// # Arguments /// /// * `value` - The text to input. - pub fn input_string(value: &str) { + pub fn input_string(&self, value: &str) { let mut key_event = KeyEvent::new(); key_event.set_seq(value.to_owned()); let mut msg_out = Message::new(); msg_out.set_key_event(key_event); - Self::send_msg_static(msg_out); + self.send_msg(msg_out); } fn _input_key( @@ -425,6 +397,7 @@ impl Session { } pub fn send_mouse( + &self, mask: i32, x: i32, y: i32, @@ -433,9 +406,7 @@ impl Session { shift: bool, command: bool, ) { - if let Some(session) = SESSION.read().unwrap().as_ref() { - send_mouse(mask, x, y, alt, ctrl, shift, command, session); - } + send_mouse(mask, x, y, alt, ctrl, shift, command, self); } fn key_down_or_up( diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index a1b9b1e7b..5d1ca2368 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1,6 +1,6 @@ use crate::client::file_trait::FileManager; use crate::flutter::connection_manager::{self, get_clients_length, get_clients_state}; -use crate::flutter::{self, make_fd_to_json, Session}; +use crate::flutter::{self, get_session, make_fd_to_json, Session}; use crate::start_server; use crate::ui_interface; use flutter_rust_bridge::{StreamSink, ZeroCopyBuffer}; @@ -69,6 +69,15 @@ pub fn start_rgba_stream(s: StreamSink>>) -> ResultType<( Ok(()) } +pub fn connect(id: String, is_file_transfer: bool, events2ui: StreamSink) { + Session::start(&id, is_file_transfer, events2ui); +} + +pub fn get_image_quality(id: String) -> Option { + let session = get_session(&id)?; + Some(session.get_image_quality()) +} + /// FFI for **get** commands which are idempotent. /// Return result in c string. /// @@ -100,11 +109,6 @@ unsafe extern "C" fn get_by_name(name: *const c_char, arg: *const c_char) -> *co "remember" => { res = Session::get_remember().to_string(); } - "event" => { - if let Some(e) = Session::pop_event() { - res = e; - } - } "toggle_option" => { if let Ok(arg) = arg.to_str() { if let Some(v) = Session::get_toggle_option(arg) { From 5825ae4531081cd3ac83f36ab30e52baf72e98aa Mon Sep 17 00:00:00 2001 From: SoLongAndThanksForAllThePizza <103753680+SoLongAndThanksForAllThePizza@users.noreply.github.com> Date: Tue, 31 May 2022 16:28:12 +0800 Subject: [PATCH 0035/2015] fix: compile error when using enum in flutter --- Cargo.lock | 4 +- Cargo.toml | 4 +- flutter/.gitignore | 1 + flutter/pubspec.lock | 245 ++++++++ flutter/pubspec.yaml | 164 +++--- libs/flutter_rust_bridge_codegen/.gitignore | 14 + libs/flutter_rust_bridge_codegen/Cargo.toml | 37 ++ libs/flutter_rust_bridge_codegen/README.md | 95 +++ .../src/commands.rs | 267 +++++++++ .../flutter_rust_bridge_codegen/src/config.rs | 292 +++++++++ libs/flutter_rust_bridge_codegen/src/error.rs | 32 + .../src/generator/c/mod.rs | 14 + .../src/generator/dart/mod.rs | 393 +++++++++++++ .../src/generator/dart/ty.rs | 64 ++ .../src/generator/dart/ty_boxed.rs | 45 ++ .../src/generator/dart/ty_delegate.rs | 42 ++ .../src/generator/dart/ty_enum.rs | 207 +++++++ .../src/generator/dart/ty_general_list.rs | 27 + .../src/generator/dart/ty_optional.rs | 31 + .../src/generator/dart/ty_primitive.rs | 22 + .../src/generator/dart/ty_primitive_list.rs | 30 + .../src/generator/dart/ty_struct.rs | 135 +++++ .../src/generator/mod.rs | 3 + .../src/generator/rust/mod.rs | 481 +++++++++++++++ .../src/generator/rust/ty.rs | 96 +++ .../src/generator/rust/ty_boxed.rs | 62 ++ .../src/generator/rust/ty_delegate.rs | 45 ++ .../src/generator/rust/ty_enum.rs | 343 +++++++++++ .../src/generator/rust/ty_general_list.rs | 55 ++ .../src/generator/rust/ty_optional.rs | 30 + .../src/generator/rust/ty_primitive.rs | 11 + .../src/generator/rust/ty_primitive_list.rs | 42 ++ .../src/generator/rust/ty_struct.rs | 185 ++++++ .../src/ir/annotation.rs | 7 + .../src/ir/comment.rs | 26 + .../src/ir/field.rs | 9 + .../src/ir/file.rs | 61 ++ .../src/ir/func.rs | 60 ++ .../src/ir/ident.rs | 26 + .../src/ir/import.rs | 5 + .../flutter_rust_bridge_codegen/src/ir/mod.rs | 33 ++ libs/flutter_rust_bridge_codegen/src/ir/ty.rs | 84 +++ .../src/ir/ty_boxed.rs | 56 ++ .../src/ir/ty_delegate.rs | 85 +++ .../src/ir/ty_enum.rs | 139 +++++ .../src/ir/ty_general_list.rs | 36 ++ .../src/ir/ty_optional.rs | 65 ++ .../src/ir/ty_primitive.rs | 114 ++++ .../src/ir/ty_primitive_list.rs | 50 ++ .../src/ir/ty_struct.rs | 66 +++ libs/flutter_rust_bridge_codegen/src/lib.rs | 183 ++++++ libs/flutter_rust_bridge_codegen/src/main.rs | 19 + .../src/markers.rs | 39 ++ .../flutter_rust_bridge_codegen/src/others.rs | 169 ++++++ .../src/parser/mod.rs | 353 +++++++++++ .../src/parser/ty.rs | 392 +++++++++++++ .../src/source_graph.rs | 553 ++++++++++++++++++ .../src/transformer.rs | 46 ++ libs/flutter_rust_bridge_codegen/src/utils.rs | 26 + 59 files changed, 6133 insertions(+), 87 deletions(-) create mode 100644 libs/flutter_rust_bridge_codegen/.gitignore create mode 100644 libs/flutter_rust_bridge_codegen/Cargo.toml create mode 100644 libs/flutter_rust_bridge_codegen/README.md create mode 100644 libs/flutter_rust_bridge_codegen/src/commands.rs create mode 100644 libs/flutter_rust_bridge_codegen/src/config.rs create mode 100644 libs/flutter_rust_bridge_codegen/src/error.rs create mode 100644 libs/flutter_rust_bridge_codegen/src/generator/c/mod.rs create mode 100644 libs/flutter_rust_bridge_codegen/src/generator/dart/mod.rs create mode 100644 libs/flutter_rust_bridge_codegen/src/generator/dart/ty.rs create mode 100644 libs/flutter_rust_bridge_codegen/src/generator/dart/ty_boxed.rs create mode 100644 libs/flutter_rust_bridge_codegen/src/generator/dart/ty_delegate.rs create mode 100644 libs/flutter_rust_bridge_codegen/src/generator/dart/ty_enum.rs create mode 100644 libs/flutter_rust_bridge_codegen/src/generator/dart/ty_general_list.rs create mode 100644 libs/flutter_rust_bridge_codegen/src/generator/dart/ty_optional.rs create mode 100644 libs/flutter_rust_bridge_codegen/src/generator/dart/ty_primitive.rs create mode 100644 libs/flutter_rust_bridge_codegen/src/generator/dart/ty_primitive_list.rs create mode 100644 libs/flutter_rust_bridge_codegen/src/generator/dart/ty_struct.rs create mode 100644 libs/flutter_rust_bridge_codegen/src/generator/mod.rs create mode 100644 libs/flutter_rust_bridge_codegen/src/generator/rust/mod.rs create mode 100644 libs/flutter_rust_bridge_codegen/src/generator/rust/ty.rs create mode 100644 libs/flutter_rust_bridge_codegen/src/generator/rust/ty_boxed.rs create mode 100644 libs/flutter_rust_bridge_codegen/src/generator/rust/ty_delegate.rs create mode 100644 libs/flutter_rust_bridge_codegen/src/generator/rust/ty_enum.rs create mode 100644 libs/flutter_rust_bridge_codegen/src/generator/rust/ty_general_list.rs create mode 100644 libs/flutter_rust_bridge_codegen/src/generator/rust/ty_optional.rs create mode 100644 libs/flutter_rust_bridge_codegen/src/generator/rust/ty_primitive.rs create mode 100644 libs/flutter_rust_bridge_codegen/src/generator/rust/ty_primitive_list.rs create mode 100644 libs/flutter_rust_bridge_codegen/src/generator/rust/ty_struct.rs create mode 100644 libs/flutter_rust_bridge_codegen/src/ir/annotation.rs create mode 100644 libs/flutter_rust_bridge_codegen/src/ir/comment.rs create mode 100644 libs/flutter_rust_bridge_codegen/src/ir/field.rs create mode 100644 libs/flutter_rust_bridge_codegen/src/ir/file.rs create mode 100644 libs/flutter_rust_bridge_codegen/src/ir/func.rs create mode 100644 libs/flutter_rust_bridge_codegen/src/ir/ident.rs create mode 100644 libs/flutter_rust_bridge_codegen/src/ir/import.rs create mode 100644 libs/flutter_rust_bridge_codegen/src/ir/mod.rs create mode 100644 libs/flutter_rust_bridge_codegen/src/ir/ty.rs create mode 100644 libs/flutter_rust_bridge_codegen/src/ir/ty_boxed.rs create mode 100644 libs/flutter_rust_bridge_codegen/src/ir/ty_delegate.rs create mode 100644 libs/flutter_rust_bridge_codegen/src/ir/ty_enum.rs create mode 100644 libs/flutter_rust_bridge_codegen/src/ir/ty_general_list.rs create mode 100644 libs/flutter_rust_bridge_codegen/src/ir/ty_optional.rs create mode 100644 libs/flutter_rust_bridge_codegen/src/ir/ty_primitive.rs create mode 100644 libs/flutter_rust_bridge_codegen/src/ir/ty_primitive_list.rs create mode 100644 libs/flutter_rust_bridge_codegen/src/ir/ty_struct.rs create mode 100644 libs/flutter_rust_bridge_codegen/src/lib.rs create mode 100644 libs/flutter_rust_bridge_codegen/src/main.rs create mode 100644 libs/flutter_rust_bridge_codegen/src/markers.rs create mode 100644 libs/flutter_rust_bridge_codegen/src/others.rs create mode 100644 libs/flutter_rust_bridge_codegen/src/parser/mod.rs create mode 100644 libs/flutter_rust_bridge_codegen/src/parser/ty.rs create mode 100644 libs/flutter_rust_bridge_codegen/src/source_graph.rs create mode 100644 libs/flutter_rust_bridge_codegen/src/transformer.rs create mode 100644 libs/flutter_rust_bridge_codegen/src/utils.rs diff --git a/Cargo.lock b/Cargo.lock index e6942ef72..5c4621f57 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1476,9 +1476,7 @@ dependencies = [ [[package]] name = "flutter_rust_bridge_codegen" -version = "1.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3209735fd687b06b8d770ec008874119b91f7f46b4a73d17226d5c337435bb74" +version = "1.32.0" dependencies = [ "anyhow", "cargo_metadata", diff --git a/Cargo.toml b/Cargo.toml index 2b707a688..f046df244 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -105,7 +105,7 @@ jni = "0.19.0" flutter_rust_bridge = "1.30.0" [workspace] -members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "libs/virtual_display"] +members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "libs/virtual_display", "libs/flutter_rust_bridge_codegen"] [package.metadata.winres] LegalCopyright = "Copyright © 2022 Purslane, Inc." @@ -119,7 +119,7 @@ winapi = { version = "0.3", features = [ "winnt" ] } [build-dependencies] cc = "1.0" hbb_common = { path = "libs/hbb_common" } -flutter_rust_bridge_codegen = "1.30.0" +flutter_rust_bridge_codegen = { path = "libs/flutter_rust_bridge_codegen" } [dev-dependencies] hound = "3.4" diff --git a/flutter/.gitignore b/flutter/.gitignore index 7dc95a613..c8ff34feb 100644 --- a/flutter/.gitignore +++ b/flutter/.gitignore @@ -45,6 +45,7 @@ jniLibs # flutter rust bridge lib/generated_bridge.dart +lib/generated_bridge.freezed.dart # Flutter Generated Files linux/flutter/generated_plugin_registrant.cc diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 4eaa1c877..1ba610f19 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -1,6 +1,20 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + url: "https://pub.dartlang.org" + source: hosted + version: "40.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + url: "https://pub.dartlang.org" + source: hosted + version: "4.1.0" archive: dependency: transitive description: @@ -64,6 +78,62 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.0" + build: + dependency: transitive + description: + name: build + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.0" + build_config: + dependency: transitive + description: + name: build_config + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + build_daemon: + dependency: transitive + description: + name: build_daemon + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.0" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.9" + build_runner: + dependency: "direct dev" + description: + name: build_runner + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.11" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + url: "https://pub.dartlang.org" + source: hosted + version: "7.2.3" + built_collection: + dependency: transitive + description: + name: built_collection + url: "https://pub.dartlang.org" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + url: "https://pub.dartlang.org" + source: hosted + version: "8.3.2" characters: dependency: transitive description: @@ -78,6 +148,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.3.1" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" clock: dependency: transitive description: @@ -85,6 +162,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.0" + code_builder: + dependency: transitive + description: + name: code_builder + url: "https://pub.dartlang.org" + source: hosted + version: "4.1.0" collection: dependency: transitive description: @@ -92,6 +176,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.16.0" + convert: + dependency: transitive + description: + name: convert + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.2" cross_file: dependency: transitive description: @@ -113,6 +204,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.4" + dart_style: + dependency: transitive + description: + name: dart_style + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.3" dash_chat: dependency: "direct main" description: @@ -319,6 +417,41 @@ packages: description: flutter source: sdk version: "0.0.0" + freezed: + dependency: "direct dev" + description: + name: freezed + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.3+1" + freezed_annotation: + dependency: "direct main" + description: + name: freezed_annotation + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.3" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.3" + glob: + dependency: transitive + description: + name: glob + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.2" + graphs: + dependency: transitive + description: + name: graphs + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" http: dependency: "direct main" description: @@ -326,6 +459,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.13.4" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + url: "https://pub.dartlang.org" + source: hosted + version: "3.2.0" http_parser: dependency: transitive description: @@ -382,6 +522,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.17.0" + io: + dependency: transitive + description: + name: io + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.3" js: dependency: transitive description: @@ -389,6 +536,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.6.4" + json_annotation: + dependency: transitive + description: + name: json_annotation + url: "https://pub.dartlang.org" + source: hosted + version: "4.5.0" + logging: + dependency: transitive + description: + name: logging + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" matcher: dependency: transitive description: @@ -410,6 +571,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.7.0" + mime: + dependency: transitive + description: + name: mime + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" nested: dependency: transitive description: @@ -417,6 +585,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.0" + package_config: + dependency: transitive + description: + name: package_config + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.2" package_info_plus: dependency: "direct main" description: @@ -543,6 +718,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.2" + pool: + dependency: transitive + description: + name: pool + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.0" process: dependency: transitive description: @@ -557,6 +739,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "5.0.0" + pub_semver: + dependency: transitive + description: + name: pub_semver + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" qr_code_scanner: dependency: "direct main" description: @@ -636,11 +832,32 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.1" + shelf: + dependency: transitive + description: + name: shelf + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" sky_engine: dependency: transitive description: flutter source: sdk version: "0.0.99" + source_gen: + dependency: transitive + description: + name: source_gen + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.2" source_span: dependency: transitive description: @@ -662,6 +879,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.0" + stream_transform: + dependency: transitive + description: + name: stream_transform + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" string_scanner: dependency: transitive description: @@ -683,6 +907,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.4.9" + timing: + dependency: transitive + description: + name: timing + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" toggle_switch: dependency: "direct main" description: @@ -816,6 +1047,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.2.0" + watcher: + dependency: transitive + description: + name: watcher + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.0" win32: dependency: transitive description: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 008d4ef9d..21b1857eb 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -3,7 +3,7 @@ description: Your Remote Desktop Software # The following line prevents the package from being accidentally published to # pub.dev using `pub publish`. This is preferred for private packages. -publish_to: 'none' # Remove this line if you wish to publish to pub.dev +publish_to: "none" # Remove this line if you wish to publish to pub.dev # The following defines the version and build number for your application. # A version number is three numbers separated by dots, like 1.2.43 @@ -19,102 +19,102 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev version: 1.1.10+27 environment: - sdk: ">=2.16.1" + sdk: ">=2.16.1" dependencies: - flutter: - sdk: flutter + flutter: + sdk: flutter - - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^1.0.3 - ffi: ^1.1.2 - path_provider: ^2.0.2 - external_path: ^1.0.1 - provider: ^5.0.0 - tuple: ^2.0.0 - wakelock: ^0.5.2 - device_info_plus: ^3.2.3 - firebase_analytics: ^9.1.5 - package_info_plus: ^1.4.2 - url_launcher: ^6.0.9 - shared_preferences: ^2.0.6 - toggle_switch: ^1.4.0 - dash_chat: ^1.1.16 - draggable_float_widget: ^0.0.2 - settings_ui: ^2.0.2 - flutter_breadcrumb: ^1.0.1 - http: ^0.13.4 - qr_code_scanner: - git: - url: https://github.com/Heap-Hop/qr_code_scanner.git - ref: fix_break_changes_platform - zxing2: ^0.1.0 - image_picker: ^0.8.5 - image: ^3.1.3 - flutter_smart_dialog: ^4.3.1 - flutter_rust_bridge: ^1.30.0 - window_manager: ^0.2.3 - desktop_multi_window: - git: - url: https://github.com/Kingtous/rustdesk_desktop_multi_window - ref: master - bitsdojo_window: ^0.1.2 + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.3 + ffi: ^1.1.2 + path_provider: ^2.0.2 + external_path: ^1.0.1 + provider: ^5.0.0 + tuple: ^2.0.0 + wakelock: ^0.5.2 + device_info_plus: ^3.2.3 + firebase_analytics: ^9.1.5 + package_info_plus: ^1.4.2 + url_launcher: ^6.0.9 + shared_preferences: ^2.0.6 + toggle_switch: ^1.4.0 + dash_chat: ^1.1.16 + draggable_float_widget: ^0.0.2 + settings_ui: ^2.0.2 + flutter_breadcrumb: ^1.0.1 + http: ^0.13.4 + qr_code_scanner: + git: + url: https://github.com/Heap-Hop/qr_code_scanner.git + ref: fix_break_changes_platform + zxing2: ^0.1.0 + image_picker: ^0.8.5 + image: ^3.1.3 + flutter_smart_dialog: ^4.3.1 + flutter_rust_bridge: ^1.30.0 + window_manager: ^0.2.3 + desktop_multi_window: + git: + url: https://github.com/Kingtous/rustdesk_desktop_multi_window + ref: master + bitsdojo_window: ^0.1.2 + freezed_annotation: ^2.0.3 dev_dependencies: - flutter_launcher_icons: ^0.9.1 - flutter_test: - sdk: flutter - + flutter_launcher_icons: ^0.9.1 + flutter_test: + sdk: flutter + build_runner: ^2.1.11 + freezed: ^2.0.3 # rerun: flutter pub run flutter_launcher_icons:main flutter_icons: - android: "ic_launcher" - ios: true - image_path: "../1024-rec.png" + android: "ic_launcher" + ios: true + image_path: "../1024-rec.png" # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec # The following section is specific to Flutter. flutter: + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. - uses-material-design: true + # To add assets to your application, add an assets section, like this: + assets: + - assets/ - # To add assets to your application, add an assets section, like this: - assets: - - assets/ + fonts: + - family: GestureIcons + fonts: + - asset: assets/gestures.ttf - fonts: - - family: GestureIcons - fonts: - - asset: assets/gestures.ttf + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware. - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware. + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages - # For details regarding adding assets from package dependencies, see - # https://flutter.dev/assets-and-images/#from-packages - - # To add custom fonts to your application, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.dev/custom-fonts/#from-packages + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages diff --git a/libs/flutter_rust_bridge_codegen/.gitignore b/libs/flutter_rust_bridge_codegen/.gitignore new file mode 100644 index 000000000..6985cf1bd --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/.gitignore @@ -0,0 +1,14 @@ +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb diff --git a/libs/flutter_rust_bridge_codegen/Cargo.toml b/libs/flutter_rust_bridge_codegen/Cargo.toml new file mode 100644 index 000000000..dfd1556db --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "flutter_rust_bridge_codegen" +version = "1.32.0" +edition = "2018" +description = "High-level memory-safe bindgen for Dart/Flutter <-> Rust" +license = "MIT" +repository = "https://github.com/fzyzcjy/flutter_rust_bridge" +keywords = ["flutter", "dart", "ffi", "code-generation", "bindings"] +categories = ["development-tools::ffi"] + +[lib] +name = "lib_flutter_rust_bridge_codegen" +path = "src/lib.rs" + +[[bin]] +name = "flutter_rust_bridge_codegen" +path = "src/main.rs" + +[dependencies] +syn = { version = "1.0.77", features = ["full", "extra-traits"] } +quote = "1.0" +regex = "1.5.4" +lazy_static = "1.4.0" +convert_case = "0.5.0" +tempfile = "3.2.0" +serde = { version = "1.0", features = ["derive"] } +serde_yaml = "0.8" +log = "0.4" +env_logger = "0.9.0" +structopt = "0.3" +toml = "0.5.8" +anyhow = "1.0.44" +pathdiff = "0.2.1" +cargo_metadata = "0.14.1" +enum_dispatch = "0.3.8" +thiserror = "1" +cbindgen = "0.23" \ No newline at end of file diff --git a/libs/flutter_rust_bridge_codegen/README.md b/libs/flutter_rust_bridge_codegen/README.md new file mode 100644 index 000000000..d9aa76531 --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/README.md @@ -0,0 +1,95 @@ +# [flutter_rust_bridge](https://github.com/fzyzcjy/flutter_rust_bridge): High-level memory-safe binding generator for Flutter/Dart <-> Rust + +[![Rust Package](https://img.shields.io/crates/v/flutter_rust_bridge.svg)](https://crates.io/crates/flutter_rust_bridge) +[![Flutter Package](https://img.shields.io/pub/v/flutter_rust_bridge.svg)](https://pub.dev/packages/flutter_rust_bridge) +[![Stars](https://img.shields.io/github/stars/fzyzcjy/flutter_rust_bridge)](https://github.com/fzyzcjy/flutter_rust_bridge) +[![CI](https://github.com/fzyzcjy/flutter_rust_bridge/actions/workflows/ci.yaml/badge.svg)](https://github.com/fzyzcjy/flutter_rust_bridge/actions/workflows/ci.yaml) +[![Example](https://github.com/fzyzcjy/flutter_rust_bridge/actions/workflows/post_release.yaml/badge.svg)](https://github.com/fzyzcjy/flutter_rust_bridge/actions/workflows/post_release.yaml) +[![Codacy Badge](https://api.codacy.com/project/badge/Grade/6afbdad19e7245adbf9e9771777be3d7)](https://app.codacy.com/gh/fzyzcjy/flutter_rust_bridge?utm_source=github.com&utm_medium=referral&utm_content=fzyzcjy/flutter_rust_bridge&utm_campaign=Badge_Grade_Settings) + +![Logo](https://github.com/fzyzcjy/flutter_rust_bridge/raw/master/book/logo.png) + +Want to combine the best between [Flutter](https://flutter.dev/), a cross-platform hot-reload rapid-development UI toolkit, and [Rust](https://www.rust-lang.org/), a language empowering everyone to build reliable and efficient software? Here it comes! + +## 🚀 Advantages + +* **Memory-safe**: Never need to think about malloc/free. +* **Feature-rich**: `enum`s with values, platform-optimized `Vec`, possibly recursive `struct`, zero-copy big arrays, `Stream` (iterator) abstraction, error (`Result`) handling, cancellable tasks, concurrency control, and more. See full features [here](https://fzyzcjy.github.io/flutter_rust_bridge/feature.html). +* **Async programming**: Rust code will never block the Flutter. Call Rust naturally from Flutter's main isolate (thread). +* **Lightweight**: This is not a huge framework that includes everything, so you are free to use your favorite Flutter and Rust libraries. For example, state-management with Flutter library (e.g. MobX) can be elegant and simple (contrary to implementing in Rust); implementing a photo manipulation algorithm in Rust will be fast and safe (countrary to implementing in Flutter). +* **Cross-platform**: Android, iOS, Windows, Linux, MacOS ([Web](https://github.com/fzyzcjy/flutter_rust_bridge/issues/315) coming soon) +* **Easy to code-review & convince yourself**: This package simply simulates how humans write boilerplate code. If you want to convince yourself (or your team) that it is safe, there is not much code to look at. No magic at all! ([More about](https://fzyzcjy.github.io/flutter_rust_bridge/safety.html) safety concerns.) +* **Fast**: It is only a thin (though feature-rich) wrapper, without overhead such as protobuf serialization, thus performant. (More [benchmarks](https://github.com/fzyzcjy/flutter_rust_bridge/issues/318#issuecomment-1034536815) later) (Throw away components like thread-pool to make it even faster) +* **Pure-Dart compatible:** Despite the name, this package is 100% compatible with [pure](https://github.com/fzyzcjy/flutter_rust_bridge/blob/master/frb_example/pure_dart/README.md) Dart. + +## 💡 User Guide + +Check out [the user guide](https://fzyzcjy.github.io/flutter_rust_bridge/) for [show-me-the-code](https://fzyzcjy.github.io/flutter_rust_bridge/quickstart.html), [tutorials](https://fzyzcjy.github.io/flutter_rust_bridge/tutorial_with_flutter.html), [features](https://fzyzcjy.github.io/flutter_rust_bridge/feature.html) and much more. + +## 📎 P.S. Convenient Flutter tests + +If you want to write and debug tests in Flutter conveniently, with action history, time travelling, screenshots, rapid re-execution, video recordings, interactive mode and more, here is my another open-source library: https://github.com/fzyzcjy/flutter_convenient_test. + +## ✨ Contributors + + +[![All Contributors](https://img.shields.io/badge/all_contributors-18-orange.svg?style=flat-square)](#contributors-) + + +Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key) following [all-contributors](https://github.com/all-contributors/all-contributors) specification): + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

fzyzcjy

💻 📖 💡 🤔 🚧

Viet Dinh

💻 ⚠️ 📖

Joshua Wade

💻

Marcel

💻

rustui

📖

Michael Bryan

💻

bus710

📖

Sebastian Urban

💻

Daniel

💻

Kevin Li

💻 📖

Patrick Auernig

💻

Anton Lazarev

💻

Unoqwy

💻

Febrian Setianto

📖

syndim

💻

sagu

💻 📖

Ikko Ashimine

📖

alanlzhang

💻 📖
+ + + + + + +More specifically, thanks for all these contributions: + +* [Desdaemon](https://github.com/Desdaemon): Support not only simple enums but also enums with fields which gets translated to native enum or freezed class in Dart. Support the Option type as nullable types in Dart. Support Vec of Strings type. Support comments in code. Add marker attributes for future usage. Add Linux and Windows support for with-flutter example, and make CI works for that. Avoid parameter collision. Overhaul the documentation and add several chapters to demonstrate configuring a Flutter+Rust project in all five platforms. Refactor command module. +* [SecondFlight](https://github.com/SecondFlight): Allow structs and enums to be imported from other files within the crate by creating source graph. Auto-create relavent dir. +* [Unoqwy](https://github.com/Unoqwy): Add struct mirrors, such that types in the external crates can be imported and used without redefining and copying. +* [antonok-edm](https://github.com/antonok-edm): Avoid converting syn types to strings before parsing to improve code and be more robust. +* [sagudev](https://github.com/sagudev): Make code generator a `lib`. Add error types. Depend on `cbindgen`. Fix LLVM paths. Update deps. Fix CI errors. +* [surban](https://github.com/surban): Support unit return type. Skip unresolvable modules. Ignore prefer_const_constructors. Non-final Dart fields. +* [trobanga](https://github.com/trobanga): Add support for `[T;N]` structs. Add `usize` support. Add a cmd argument. Separate dart tests. +* [AlienKevin](https://github.com/AlienKevin): Add flutter example for macOS. Add doc for Android NDK bug. +* [alanlzhang](https://github.com/alanlzhang): Add generation for Dart metadata. +* [efc-mw](https://github.com/efc-mw): Improve Windows encoding handling. +* [valeth](https://github.com/valeth): Rename callFfi's port. +* [Michael-F-Bryan](https://github.com/Michael-F-Bryan): Detect broken bindings. +* [bus710](https://github.com/bus710): Add a case in troubleshooting. +* [Syndim](https://github.com/Syndim): Add a bracket to box. +* [feber](https://github.com/feber): Fix doc link. +* [rustui](https://github.com/rustui): Fix a typo. +* [eltociear](https://github.com/eltociear): Fix a typo. + diff --git a/libs/flutter_rust_bridge_codegen/src/commands.rs b/libs/flutter_rust_bridge_codegen/src/commands.rs new file mode 100644 index 000000000..6838449d8 --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/commands.rs @@ -0,0 +1,267 @@ +use std::fmt::Write; +use std::path::Path; +use std::process::Command; +use std::process::Output; + +use crate::error::{Error, Result}; +use log::{debug, info, warn}; + +#[must_use] +fn call_shell(cmd: &str) -> Output { + #[cfg(windows)] + return execute_command("powershell", &["-noprofile", "-c", cmd], None); + + #[cfg(not(windows))] + execute_command("sh", &["-c", cmd], None) +} + +pub fn ensure_tools_available() -> Result { + let output = call_shell("dart pub global list"); + let output = String::from_utf8_lossy(&output.stdout); + if !output.contains("ffigen") { + return Err(Error::MissingExe(String::from("ffigen"))); + } + + Ok(()) +} + +pub fn bindgen_rust_to_dart( + rust_crate_dir: &str, + c_output_path: &str, + dart_output_path: &str, + dart_class_name: &str, + c_struct_names: Vec, + llvm_install_path: &[String], + llvm_compiler_opts: &str, +) -> anyhow::Result<()> { + cbindgen(rust_crate_dir, c_output_path, c_struct_names)?; + ffigen( + c_output_path, + dart_output_path, + dart_class_name, + llvm_install_path, + llvm_compiler_opts, + ) +} + +#[must_use = "Error path must be handled."] +fn execute_command(bin: &str, args: &[&str], current_dir: Option<&str>) -> Output { + let mut cmd = Command::new(bin); + cmd.args(args); + + if let Some(current_dir) = current_dir { + cmd.current_dir(current_dir); + } + + debug!( + "execute command: bin={} args={:?} current_dir={:?} cmd={:?}", + bin, args, current_dir, cmd + ); + + let result = cmd + .output() + .unwrap_or_else(|err| panic!("\"{}\" \"{}\" failed: {}", bin, args.join(" "), err)); + + let stdout = String::from_utf8_lossy(&result.stdout); + if result.status.success() { + debug!( + "command={:?} stdout={} stderr={}", + cmd, + stdout, + String::from_utf8_lossy(&result.stderr) + ); + if stdout.contains("fatal error") { + warn!("See keywords such as `error` in command output. Maybe there is a problem? command={:?} output={:?}", cmd, result); + } else if args.contains(&"ffigen") && stdout.contains("[SEVERE]") { + // HACK: If ffigen can't find a header file it will generate broken + // bindings but still exit successfully. We can detect these broken + // bindings by looking for a "[SEVERE]" log message. + // + // It may emit SEVERE log messages for non-fatal errors though, so + // we don't want to error out completely. + + warn!( + "The `ffigen` command emitted a SEVERE error. Maybe there is a problem? command={:?} output=\n{}", + cmd, String::from_utf8_lossy(&result.stdout) + ); + } + } else { + warn!( + "command={:?} stdout={} stderr={}", + cmd, + stdout, + String::from_utf8_lossy(&result.stderr) + ); + } + result +} + +fn cbindgen( + rust_crate_dir: &str, + c_output_path: &str, + c_struct_names: Vec, +) -> anyhow::Result<()> { + debug!( + "execute cbindgen rust_crate_dir={} c_output_path={}", + rust_crate_dir, c_output_path + ); + + let config = cbindgen::Config { + language: cbindgen::Language::C, + sys_includes: vec![ + "stdbool.h".to_string(), + "stdint.h".to_string(), + "stdlib.h".to_string(), + ], + no_includes: true, + export: cbindgen::ExportConfig { + include: c_struct_names + .iter() + .map(|name| format!("\"{}\"", name)) + .collect::>(), + ..Default::default() + }, + ..Default::default() + }; + + debug!("cbindgen config: {:?}", config); + + let canonical = Path::new(rust_crate_dir) + .canonicalize() + .expect("Could not canonicalize rust crate dir"); + let mut path = canonical.to_str().unwrap(); + + // on windows get rid of the UNC path + if path.starts_with(r"\\?\") { + path = &path[r"\\?\".len()..]; + } + + if cbindgen::generate_with_config(path, config)?.write_to_file(c_output_path) { + Ok(()) + } else { + Err(Error::str("cbindgen failed writing file").into()) + } +} + +fn ffigen( + c_path: &str, + dart_path: &str, + dart_class_name: &str, + llvm_path: &[String], + llvm_compiler_opts: &str, +) -> anyhow::Result<()> { + debug!( + "execute ffigen c_path={} dart_path={} llvm_path={:?}", + c_path, dart_path, llvm_path + ); + let mut config = format!( + " + output: '{}' + name: '{}' + description: 'generated by flutter_rust_bridge' + headers: + entry-points: + - '{}' + include-directives: + - '{}' + comments: false + preamble: | + // ignore_for_file: camel_case_types, non_constant_identifier_names, avoid_positional_boolean_parameters, annotate_overrides, constant_identifier_names + ", + dart_path, dart_class_name, c_path, c_path, + ); + if !llvm_path.is_empty() { + write!( + &mut config, + " + llvm-path:\n" + )?; + for path in llvm_path { + writeln!(&mut config, " - '{}'", path)?; + } + } + + if !llvm_compiler_opts.is_empty() { + config = format!( + "{} + compiler-opts: + - '{}'", + config, llvm_compiler_opts + ); + } + + debug!("ffigen config: {}", config); + + let mut config_file = tempfile::NamedTempFile::new()?; + std::io::Write::write_all(&mut config_file, config.as_bytes())?; + debug!("ffigen config_file: {:?}", config_file); + + // NOTE please install ffigen globally first: `dart pub global activate ffigen` + let res = call_shell(&format!( + "dart pub global run ffigen --config \"{}\"", + config_file.path().to_string_lossy() + )); + if !res.status.success() { + let err = String::from_utf8_lossy(&res.stderr); + let out = String::from_utf8_lossy(&res.stdout); + let pat = "Couldn't find dynamic library in default locations."; + if err.contains(pat) || out.contains(pat) { + return Err(Error::FfigenLlvm.into()); + } + return Err( + Error::string(format!("ffigen failed:\nstderr: {}\nstdout: {}", err, out)).into(), + ); + } + Ok(()) +} + +pub fn format_rust(path: &str) -> Result { + debug!("execute format_rust path={}", path); + let res = execute_command("rustfmt", &[path], None); + if !res.status.success() { + return Err(Error::Rustfmt( + String::from_utf8_lossy(&res.stderr).to_string(), + )); + } + Ok(()) +} + +pub fn format_dart(path: &str, line_length: i32) -> Result { + debug!( + "execute format_dart path={} line_length={}", + path, line_length + ); + let res = call_shell(&format!( + "dart format {} --line-length {}", + path, line_length + )); + if !res.status.success() { + return Err(Error::Dartfmt( + String::from_utf8_lossy(&res.stderr).to_string(), + )); + } + Ok(()) +} + +pub fn build_runner(dart_root: &str) -> Result { + info!("Running build_runner at {}", dart_root); + let out = if cfg!(windows) { + call_shell(&format!( + "cd \"{}\"; flutter pub run build_runner build --delete-conflicting-outputs", + dart_root + )) + } else { + call_shell(&format!( + "cd \"{}\" && flutter pub run build_runner build --delete-conflicting-outputs", + dart_root + )) + }; + if !out.status.success() { + return Err(Error::StringError(format!( + "Failed to run build_runner for {}: {}", + dart_root, + String::from_utf8_lossy(&out.stdout) + ))); + } + Ok(()) +} diff --git a/libs/flutter_rust_bridge_codegen/src/config.rs b/libs/flutter_rust_bridge_codegen/src/config.rs new file mode 100644 index 000000000..de77cd1b1 --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/config.rs @@ -0,0 +1,292 @@ +use std::env; +use std::ffi::OsString; +use std::fs; +use std::path::Path; +use std::path::PathBuf; +use std::str::FromStr; + +use anyhow::{anyhow, Result}; +use convert_case::{Case, Casing}; +use serde::Deserialize; +use structopt::clap::AppSettings; +use structopt::StructOpt; +use toml::Value; + +#[derive(StructOpt, Debug, PartialEq, Deserialize, Default)] +#[structopt(setting(AppSettings::DeriveDisplayOrder))] +pub struct RawOpts { + /// Path of input Rust code + #[structopt(short, long)] + pub rust_input: String, + /// Path of output generated Dart code + #[structopt(short, long)] + pub dart_output: String, + /// If provided, generated Dart declaration code to this separate file + #[structopt(long)] + pub dart_decl_output: Option, + + /// Path of output generated C header + #[structopt(short, long)] + pub c_output: Option>, + /// Crate directory for your Rust project + #[structopt(long)] + pub rust_crate_dir: Option, + /// Path of output generated Rust code + #[structopt(long)] + pub rust_output: Option, + /// Generated class name + #[structopt(long)] + pub class_name: Option, + /// Line length for dart formatting + #[structopt(long)] + pub dart_format_line_length: Option, + /// Skip automatically adding `mod bridge_generated;` to `lib.rs` + #[structopt(long)] + pub skip_add_mod_to_lib: bool, + /// Path to the installed LLVM + #[structopt(long)] + pub llvm_path: Option>, + /// LLVM compiler opts + #[structopt(long)] + pub llvm_compiler_opts: Option, + /// Path to root of Dart project, otherwise inferred from --dart-output + #[structopt(long)] + pub dart_root: Option, + /// Skip running build_runner even when codegen-capable code is detected + #[structopt(long)] + pub no_build_runner: bool, + /// Show debug messages. + #[structopt(short, long)] + pub verbose: bool, +} + +#[derive(Debug)] +pub struct Opts { + pub rust_input_path: String, + pub dart_output_path: String, + pub dart_decl_output_path: Option, + pub c_output_path: Vec, + pub rust_crate_dir: String, + pub rust_output_path: String, + pub class_name: String, + pub dart_format_line_length: i32, + pub skip_add_mod_to_lib: bool, + pub llvm_path: Vec, + pub llvm_compiler_opts: String, + pub manifest_path: String, + pub dart_root: Option, + pub build_runner: bool, +} + +pub fn parse(raw: RawOpts) -> Opts { + let rust_input_path = canon_path(&raw.rust_input); + + let rust_crate_dir = canon_path(&raw.rust_crate_dir.unwrap_or_else(|| { + fallback_rust_crate_dir(&rust_input_path) + .unwrap_or_else(|_| panic!("{}", format_fail_to_guess_error("rust_crate_dir"))) + })); + let manifest_path = { + let mut path = std::path::PathBuf::from_str(&rust_crate_dir).unwrap(); + path.push("Cargo.toml"); + path_to_string(path).unwrap() + }; + let rust_output_path = canon_path(&raw.rust_output.unwrap_or_else(|| { + fallback_rust_output_path(&rust_input_path) + .unwrap_or_else(|_| panic!("{}", format_fail_to_guess_error("rust_output"))) + })); + let class_name = raw.class_name.unwrap_or_else(|| { + fallback_class_name(&*rust_crate_dir) + .unwrap_or_else(|_| panic!("{}", format_fail_to_guess_error("class_name"))) + }); + let c_output_path = raw + .c_output + .map(|outputs| { + outputs + .iter() + .map(|output| canon_path(output)) + .collect::>() + }) + .unwrap_or_else(|| { + vec![fallback_c_output_path() + .unwrap_or_else(|_| panic!("{}", format_fail_to_guess_error("c_output")))] + }); + + let dart_root = { + let dart_output = &raw.dart_output; + raw.dart_root + .as_deref() + .map(canon_path) + .or_else(|| fallback_dart_root(dart_output).ok()) + }; + + Opts { + rust_input_path, + dart_output_path: canon_path(&raw.dart_output), + dart_decl_output_path: raw + .dart_decl_output + .as_ref() + .map(|s| canon_path(s.as_str())), + c_output_path, + rust_crate_dir, + rust_output_path, + class_name, + dart_format_line_length: raw.dart_format_line_length.unwrap_or(80), + skip_add_mod_to_lib: raw.skip_add_mod_to_lib, + llvm_path: raw.llvm_path.unwrap_or_else(|| { + vec![ + "/opt/homebrew/opt/llvm".to_owned(), // Homebrew root + "/usr/local/opt/llvm".to_owned(), // Homebrew x86-64 root + // Possible Linux LLVM roots + "/usr/lib/llvm-9".to_owned(), + "/usr/lib/llvm-10".to_owned(), + "/usr/lib/llvm-11".to_owned(), + "/usr/lib/llvm-12".to_owned(), + "/usr/lib/llvm-13".to_owned(), + "/usr/lib/llvm-14".to_owned(), + "/usr/lib/".to_owned(), + "/usr/lib64/".to_owned(), + "C:/Program Files/llvm".to_owned(), // Default on Windows + "C:/Program Files/LLVM".to_owned(), + "C:/msys64/mingw64".to_owned(), // https://packages.msys2.org/package/mingw-w64-x86_64-clang + ] + }), + llvm_compiler_opts: raw.llvm_compiler_opts.unwrap_or_else(|| "".to_string()), + manifest_path, + dart_root, + build_runner: !raw.no_build_runner, + } +} + +fn format_fail_to_guess_error(name: &str) -> String { + format!( + "fail to guess {}, please specify it manually in command line arguments", + name + ) +} + +fn fallback_rust_crate_dir(rust_input_path: &str) -> Result { + let mut dir_curr = Path::new(rust_input_path) + .parent() + .ok_or_else(|| anyhow!(""))?; + + loop { + let path_cargo_toml = dir_curr.join("Cargo.toml"); + + if path_cargo_toml.exists() { + return Ok(dir_curr + .as_os_str() + .to_str() + .ok_or_else(|| anyhow!(""))? + .to_string()); + } + + if let Some(next_parent) = dir_curr.parent() { + dir_curr = next_parent; + } else { + break; + } + } + Err(anyhow!( + "look at parent directories but none contains Cargo.toml" + )) +} + +fn fallback_c_output_path() -> Result { + let named_temp_file = Box::leak(Box::new(tempfile::Builder::new().suffix(".h").tempfile()?)); + Ok(named_temp_file + .path() + .to_str() + .ok_or_else(|| anyhow!(""))? + .to_string()) +} + +fn fallback_rust_output_path(rust_input_path: &str) -> Result { + Ok(Path::new(rust_input_path) + .parent() + .ok_or_else(|| anyhow!(""))? + .join("bridge_generated.rs") + .to_str() + .ok_or_else(|| anyhow!(""))? + .to_string()) +} + +fn fallback_dart_root(dart_output_path: &str) -> Result { + let mut res = canon_pathbuf(dart_output_path); + while res.pop() { + if res.join("pubspec.yaml").is_file() { + return res + .to_str() + .map(ToString::to_string) + .ok_or_else(|| anyhow!("Non-utf8 path")); + } + } + Err(anyhow!( + "Root of Dart library could not be inferred from Dart output" + )) +} + +fn fallback_class_name(rust_crate_dir: &str) -> Result { + let cargo_toml_path = Path::new(rust_crate_dir).join("Cargo.toml"); + let cargo_toml_content = fs::read_to_string(cargo_toml_path)?; + + let cargo_toml_value = cargo_toml_content.parse::()?; + let package_name = cargo_toml_value + .get("package") + .ok_or_else(|| anyhow!("no `package` in Cargo.toml"))? + .get("name") + .ok_or_else(|| anyhow!("no `name` in Cargo.toml"))? + .as_str() + .ok_or_else(|| anyhow!(""))?; + + Ok(package_name.to_case(Case::Pascal)) +} + +fn canon_path(sub_path: &str) -> String { + let path = canon_pathbuf(sub_path); + path_to_string(path).unwrap_or_else(|_| panic!("fail to parse path: {}", sub_path)) +} + +fn canon_pathbuf(sub_path: &str) -> PathBuf { + let mut path = + env::current_dir().unwrap_or_else(|_| panic!("fail to parse path: {}", sub_path)); + path.push(sub_path); + path +} + +fn path_to_string(path: PathBuf) -> Result { + path.into_os_string().into_string() +} + +impl Opts { + pub fn dart_api_class_name(&self) -> String { + self.class_name.clone() + } + + pub fn dart_api_impl_class_name(&self) -> String { + format!("{}Impl", self.class_name) + } + + pub fn dart_wire_class_name(&self) -> String { + format!("{}Wire", self.class_name) + } + + /// Returns None if the path terminates in "..", or not utf8. + pub fn dart_output_path_name(&self) -> Option<&str> { + let name = Path::new(&self.dart_output_path); + let root = name.file_name()?.to_str()?; + if let Some((name, _)) = root.rsplit_once('.') { + Some(name) + } else { + Some(root) + } + } + + pub fn dart_output_freezed_path(&self) -> Option { + Some( + Path::new(&self.dart_output_path) + .with_extension("freezed.dart") + .to_str()? + .to_owned(), + ) + } +} diff --git a/libs/flutter_rust_bridge_codegen/src/error.rs b/libs/flutter_rust_bridge_codegen/src/error.rs new file mode 100644 index 000000000..9a8607d37 --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/error.rs @@ -0,0 +1,32 @@ +use thiserror::Error; + +pub type Result = std::result::Result<(), Error>; + +#[derive(Error, Debug)] +pub enum Error { + #[error("rustfmt failed: {0}")] + Rustfmt(String), + #[error("dart fmt failed: {0}")] + Dartfmt(String), + #[error( + "ffigen could not find LLVM. + Please supply --llvm-path to flutter_rust_bridge_codegen, e.g.: + + flutter_rust_bridge_codegen .. --llvm-path " + )] + FfigenLlvm, + #[error("{0} is not a command, or not executable.")] + MissingExe(String), + #[error("{0}")] + StringError(String), +} + +impl Error { + pub fn str(msg: &str) -> Self { + Self::StringError(msg.to_owned()) + } + + pub fn string(msg: String) -> Self { + Self::StringError(msg) + } +} diff --git a/libs/flutter_rust_bridge_codegen/src/generator/c/mod.rs b/libs/flutter_rust_bridge_codegen/src/generator/c/mod.rs new file mode 100644 index 000000000..2a2410dbc --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/generator/c/mod.rs @@ -0,0 +1,14 @@ +pub fn generate_dummy(func_names: &[String]) -> String { + format!( + r#"static int64_t dummy_method_to_enforce_bundling(void) {{ + int64_t dummy_var = 0; +{} + return dummy_var; +}}"#, + func_names + .iter() + .map(|func_name| { format!(" dummy_var ^= ((int64_t) (void*) {});", func_name) }) + .collect::>() + .join("\n"), + ) +} diff --git a/libs/flutter_rust_bridge_codegen/src/generator/dart/mod.rs b/libs/flutter_rust_bridge_codegen/src/generator/dart/mod.rs new file mode 100644 index 000000000..afe35527f --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/generator/dart/mod.rs @@ -0,0 +1,393 @@ +mod ty; +mod ty_boxed; +mod ty_delegate; +mod ty_enum; +mod ty_general_list; +mod ty_optional; +mod ty_primitive; +mod ty_primitive_list; +mod ty_struct; + +use std::collections::HashSet; + +pub use ty::*; +pub use ty_boxed::*; +pub use ty_delegate::*; +pub use ty_enum::*; +pub use ty_general_list::*; +pub use ty_optional::*; +pub use ty_primitive::*; +pub use ty_primitive_list::*; +pub use ty_struct::*; + +use convert_case::{Case, Casing}; +use log::debug; + +use crate::ir::IrType::*; +use crate::ir::*; +use crate::others::*; + +pub struct Output { + pub file_prelude: DartBasicCode, + pub decl_code: DartBasicCode, + pub impl_code: DartBasicCode, +} + +pub fn generate( + ir_file: &IrFile, + dart_api_class_name: &str, + dart_api_impl_class_name: &str, + dart_wire_class_name: &str, + dart_output_file_root: &str, +) -> (Output, bool) { + let distinct_types = ir_file.distinct_types(true, true); + let distinct_input_types = ir_file.distinct_types(true, false); + let distinct_output_types = ir_file.distinct_types(false, true); + debug!("distinct_input_types={:?}", distinct_input_types); + debug!("distinct_output_types={:?}", distinct_output_types); + + let dart_func_signatures_and_implementations = ir_file + .funcs + .iter() + .map(generate_api_func) + .collect::>(); + let dart_structs = distinct_types + .iter() + .map(|ty| TypeDartGenerator::new(ty.clone(), ir_file).structs()) + .collect::>(); + let dart_api2wire_funcs = distinct_input_types + .iter() + .map(|ty| generate_api2wire_func(ty, ir_file)) + .collect::>(); + let dart_api_fill_to_wire_funcs = distinct_input_types + .iter() + .map(|ty| generate_api_fill_to_wire_func(ty, ir_file)) + .collect::>(); + let dart_wire2api_funcs = distinct_output_types + .iter() + .map(|ty| generate_wire2api_func(ty, ir_file)) + .collect::>(); + + let needs_freezed = distinct_types.iter().any(|ty| match ty { + EnumRef(e) if e.is_struct => true, + StructRef(s) if s.freezed => true, + _ => false, + }); + let freezed_header = if needs_freezed { + DartBasicCode { + import: "import 'package:freezed_annotation/freezed_annotation.dart';".to_string(), + part: format!("part '{}.freezed.dart';", dart_output_file_root), + body: "".to_string(), + } + } else { + DartBasicCode::default() + }; + + let imports = ir_file + .struct_pool + .values() + .flat_map(|s| s.dart_metadata.iter().flat_map(|it| &it.library)) + .collect::>(); + + let import_header = if !imports.is_empty() { + DartBasicCode { + import: imports + .iter() + .map(|it| match &it.alias { + Some(alias) => format!("import '{}' as {};", it.uri, alias), + _ => format!("import '{}';", it.uri), + }) + .collect::>() + .join("\n"), + part: "".to_string(), + body: "".to_string(), + } + } else { + DartBasicCode::default() + }; + + let common_header = DartBasicCode { + import: "import 'dart:convert'; + import 'dart:typed_data';" + .to_string(), + part: "".to_string(), + body: "".to_string(), + }; + + let decl_body = format!( + "abstract class {} {{ + {} + }} + + {} + ", + dart_api_class_name, + dart_func_signatures_and_implementations + .iter() + .map(|(sig, _, comm)| format!("{}{}", comm, sig)) + .collect::>() + .join("\n\n"), + dart_structs.join("\n\n"), + ); + + let impl_body = format!( + "class {dart_api_impl_class_name} extends FlutterRustBridgeBase<{dart_wire_class_name}> implements {dart_api_class_name} {{ + factory {dart_api_impl_class_name}(ffi.DynamicLibrary dylib) => {dart_api_impl_class_name}.raw({dart_wire_class_name}(dylib)); + + {dart_api_impl_class_name}.raw({dart_wire_class_name} inner) : super(inner); + + {} + + // Section: api2wire + {} + + // Section: api_fill_to_wire + {} + }} + + // Section: wire2api + {} + ", + dart_func_signatures_and_implementations + .iter() + .map(|(_, imp, _)| imp.clone()) + .collect::>() + .join("\n\n"), + dart_api2wire_funcs.join("\n\n"), + dart_api_fill_to_wire_funcs.join("\n\n"), + dart_wire2api_funcs.join("\n\n"), + dart_api_impl_class_name = dart_api_impl_class_name, + dart_wire_class_name = dart_wire_class_name, + dart_api_class_name = dart_api_class_name, + ); + + let decl_code = &common_header + + &freezed_header + + &import_header + + &DartBasicCode { + import: "".to_string(), + part: "".to_string(), + body: decl_body, + }; + + let impl_code = &common_header + + &DartBasicCode { + import: "import 'package:flutter_rust_bridge/flutter_rust_bridge.dart';".to_string(), + part: "".to_string(), + body: impl_body, + }; + + let file_prelude = DartBasicCode { + import: format!("{} + + // ignore_for_file: non_constant_identifier_names, unused_element, duplicate_ignore, directives_ordering, curly_braces_in_flow_control_structures, unnecessary_lambdas, slash_for_doc_comments, prefer_const_literals_to_create_immutables, implicit_dynamic_list_literal, duplicate_import, unused_import, prefer_single_quotes, prefer_const_constructors + ", + CODE_HEADER + ), + part: "".to_string(), + body: "".to_string(), + }; + + ( + Output { + file_prelude, + decl_code, + impl_code, + }, + needs_freezed, + ) +} + +fn generate_api_func(func: &IrFunc) -> (String, String, String) { + let raw_func_param_list = func + .inputs + .iter() + .map(|input| { + format!( + "{}{} {}", + input.ty.dart_required_modifier(), + input.ty.dart_api_type(), + input.name.dart_style() + ) + }) + .collect::>(); + + let full_func_param_list = [raw_func_param_list, vec!["dynamic hint".to_string()]].concat(); + + let wire_param_list = [ + if func.mode.has_port_argument() { + vec!["port_".to_string()] + } else { + vec![] + }, + func.inputs + .iter() + .map(|input| { + // edge case: ffigen performs its own bool-to-int conversions + if let IrType::Primitive(IrTypePrimitive::Bool) = input.ty { + input.name.dart_style() + } else { + format!( + "_api2wire_{}({})", + &input.ty.safe_ident(), + &input.name.dart_style() + ) + } + }) + .collect::>(), + ] + .concat(); + + let partial = format!( + "{} {}({{ {} }})", + func.mode.dart_return_type(&func.output.dart_api_type()), + func.name.to_case(Case::Camel), + full_func_param_list.join(","), + ); + + let execute_func_name = match func.mode { + IrFuncMode::Normal => "executeNormal", + IrFuncMode::Sync => "executeSync", + IrFuncMode::Stream => "executeStream", + }; + + let signature = format!("{};", partial); + + let comments = dart_comments(&func.comments); + + let task_common_args = format!( + " + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: \"{}\", + argNames: [{}], + ), + argValues: [{}], + hint: hint, + ", + func.name, + func.inputs + .iter() + .map(|input| format!("\"{}\"", input.name.dart_style())) + .collect::>() + .join(", "), + func.inputs + .iter() + .map(|input| input.name.dart_style()) + .collect::>() + .join(", "), + ); + + let implementation = match func.mode { + IrFuncMode::Sync => format!( + "{} => {}(FlutterRustBridgeSyncTask( + callFfi: () => inner.{}({}), + {} + ));", + partial, + execute_func_name, + func.wire_func_name(), + wire_param_list.join(", "), + task_common_args, + ), + _ => format!( + "{} => {}(FlutterRustBridgeTask( + callFfi: (port_) => inner.{}({}), + parseSuccessData: _wire2api_{}, + {} + ));", + partial, + execute_func_name, + func.wire_func_name(), + wire_param_list.join(", "), + func.output.safe_ident(), + task_common_args, + ), + }; + + (signature, implementation, comments) +} + +fn generate_api2wire_func(ty: &IrType, ir_file: &IrFile) -> String { + if let Some(body) = TypeDartGenerator::new(ty.clone(), ir_file).api2wire_body() { + format!( + "{} _api2wire_{}({} raw) {{ + {} + }} + ", + ty.dart_wire_type(), + ty.safe_ident(), + ty.dart_api_type(), + body, + ) + } else { + "".to_string() + } +} + +fn generate_api_fill_to_wire_func(ty: &IrType, ir_file: &IrFile) -> String { + if let Some(body) = TypeDartGenerator::new(ty.clone(), ir_file).api_fill_to_wire_body() { + let target_wire_type = match ty { + Optional(inner) => &inner.inner, + it => it, + }; + + format!( + "void _api_fill_to_wire_{}({} apiObj, {} wireObj) {{ + {} + }}", + ty.safe_ident(), + ty.dart_api_type(), + target_wire_type.dart_wire_type(), + body, + ) + } else { + "".to_string() + } +} + +fn generate_wire2api_func(ty: &IrType, ir_file: &IrFile) -> String { + let body = TypeDartGenerator::new(ty.clone(), ir_file).wire2api_body(); + + format!( + "{} _wire2api_{}(dynamic raw) {{ + {} + }} + ", + ty.dart_api_type(), + ty.safe_ident(), + body, + ) +} + +fn gen_wire2api_simple_type_cast(s: &str) -> String { + format!("return raw as {};", s) +} + +/// A trailing newline is included if comments is not empty. +fn dart_comments(comments: &[IrComment]) -> String { + let mut comments = comments + .iter() + .map(IrComment::comment) + .collect::>() + .join("\n"); + if !comments.is_empty() { + comments.push('\n'); + } + comments +} +fn dart_metadata(metadata: &[IrDartAnnotation]) -> String { + let mut metadata = metadata + .iter() + .map(|it| match &it.library { + Some(IrDartImport { + alias: Some(alias), .. + }) => format!("@{}.{}", alias, it.content), + _ => format!("@{}", it.content), + }) + .collect::>() + .join("\n"); + if !metadata.is_empty() { + metadata.push('\n'); + } + metadata +} diff --git a/libs/flutter_rust_bridge_codegen/src/generator/dart/ty.rs b/libs/flutter_rust_bridge_codegen/src/generator/dart/ty.rs new file mode 100644 index 000000000..dd8004ed9 --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/generator/dart/ty.rs @@ -0,0 +1,64 @@ +use crate::generator::dart::*; +use enum_dispatch::enum_dispatch; + +#[enum_dispatch] +pub trait TypeDartGeneratorTrait { + fn api2wire_body(&self) -> Option; + + fn api_fill_to_wire_body(&self) -> Option { + None + } + + fn wire2api_body(&self) -> String { + "".to_string() + } + + fn structs(&self) -> String { + "".to_string() + } +} + +#[derive(Debug, Clone)] +pub struct TypeGeneratorContext<'a> { + pub ir_file: &'a IrFile, +} + +#[macro_export] +macro_rules! type_dart_generator_struct { + ($cls:ident, $ir_cls:ty) => { + #[derive(Debug, Clone)] + pub struct $cls<'a> { + pub ir: $ir_cls, + pub context: TypeGeneratorContext<'a>, + } + }; +} + +#[enum_dispatch(TypeDartGeneratorTrait)] +#[derive(Debug, Clone)] +pub enum TypeDartGenerator<'a> { + Primitive(TypePrimitiveGenerator<'a>), + Delegate(TypeDelegateGenerator<'a>), + PrimitiveList(TypePrimitiveListGenerator<'a>), + Optional(TypeOptionalGenerator<'a>), + GeneralList(TypeGeneralListGenerator<'a>), + StructRef(TypeStructRefGenerator<'a>), + Boxed(TypeBoxedGenerator<'a>), + EnumRef(TypeEnumRefGenerator<'a>), +} + +impl<'a> TypeDartGenerator<'a> { + pub fn new(ty: IrType, ir_file: &'a IrFile) -> Self { + let context = TypeGeneratorContext { ir_file }; + match ty { + Primitive(ir) => TypePrimitiveGenerator { ir, context }.into(), + Delegate(ir) => TypeDelegateGenerator { ir, context }.into(), + PrimitiveList(ir) => TypePrimitiveListGenerator { ir, context }.into(), + Optional(ir) => TypeOptionalGenerator { ir, context }.into(), + GeneralList(ir) => TypeGeneralListGenerator { ir, context }.into(), + StructRef(ir) => TypeStructRefGenerator { ir, context }.into(), + Boxed(ir) => TypeBoxedGenerator { ir, context }.into(), + EnumRef(ir) => TypeEnumRefGenerator { ir, context }.into(), + } + } +} diff --git a/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_boxed.rs b/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_boxed.rs new file mode 100644 index 000000000..84c2b3675 --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_boxed.rs @@ -0,0 +1,45 @@ +use crate::generator::dart::gen_wire2api_simple_type_cast; +use crate::generator::dart::ty::*; +use crate::ir::IrType::{EnumRef, Primitive, StructRef}; +use crate::ir::*; +use crate::type_dart_generator_struct; + +type_dart_generator_struct!(TypeBoxedGenerator, IrTypeBoxed); + +impl TypeDartGeneratorTrait for TypeBoxedGenerator<'_> { + fn api2wire_body(&self) -> Option { + Some(match &*self.ir.inner { + Primitive(_) => { + format!("return inner.new_{}(raw);", self.ir.safe_ident()) + } + inner => { + format!( + "final ptr = inner.new_{}(); + _api_fill_to_wire_{}(raw, ptr.ref); + return ptr;", + self.ir.safe_ident(), + inner.safe_ident(), + ) + } + }) + } + + fn api_fill_to_wire_body(&self) -> Option { + if !matches!(*self.ir.inner, Primitive(_)) { + Some(format!( + " _api_fill_to_wire_{}(apiObj, wireObj.ref);", + self.ir.inner.safe_ident() + )) + } else { + None + } + } + + fn wire2api_body(&self) -> String { + match &*self.ir.inner { + StructRef(inner) => format!("return _wire2api_{}(raw);", inner.safe_ident()), + EnumRef(inner) => format!("return _wire2api_{}(raw);", inner.safe_ident()), + _ => gen_wire2api_simple_type_cast(&self.ir.dart_api_type()), + } + } +} diff --git a/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_delegate.rs b/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_delegate.rs new file mode 100644 index 000000000..b585ff3f7 --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_delegate.rs @@ -0,0 +1,42 @@ +use crate::generator::dart::gen_wire2api_simple_type_cast; +use crate::generator::dart::ty::*; +use crate::ir::*; +use crate::type_dart_generator_struct; + +type_dart_generator_struct!(TypeDelegateGenerator, IrTypeDelegate); + +impl TypeDartGeneratorTrait for TypeDelegateGenerator<'_> { + fn api2wire_body(&self) -> Option { + Some(match self.ir { + IrTypeDelegate::String => { + "return _api2wire_uint_8_list(utf8.encoder.convert(raw));".to_string() + } + IrTypeDelegate::SyncReturnVecU8 => "/*unsupported*/".to_string(), + IrTypeDelegate::ZeroCopyBufferVecPrimitive(_) => { + format!( + "return _api2wire_{}(raw);", + self.ir.get_delegate().safe_ident() + ) + } + IrTypeDelegate::StringList => "final ans = inner.new_StringList(raw.length); + for (var i = 0; i < raw.length; i++) { + ans.ref.ptr[i] = _api2wire_String(raw[i]); + } + return ans;" + .to_owned(), + }) + } + + fn wire2api_body(&self) -> String { + match &self.ir { + IrTypeDelegate::String + | IrTypeDelegate::SyncReturnVecU8 + | IrTypeDelegate::ZeroCopyBufferVecPrimitive(_) => { + gen_wire2api_simple_type_cast(&self.ir.dart_api_type()) + } + IrTypeDelegate::StringList => { + "return (raw as List).cast();".to_owned() + } + } + } +} diff --git a/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_enum.rs b/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_enum.rs new file mode 100644 index 000000000..fc361b4c8 --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_enum.rs @@ -0,0 +1,207 @@ +use crate::generator::dart::dart_comments; +use crate::generator::dart::ty::*; +use crate::ir::*; +use crate::type_dart_generator_struct; + +type_dart_generator_struct!(TypeEnumRefGenerator, IrTypeEnumRef); + +impl TypeDartGeneratorTrait for TypeEnumRefGenerator<'_> { + fn api2wire_body(&self) -> Option { + if !self.ir.is_struct { + Some("return raw.index;".to_owned()) + } else { + None + } + } + + fn api_fill_to_wire_body(&self) -> Option { + if self.ir.is_struct { + Some( + self.ir + .get(self.context.ir_file) + .variants() + .iter() + .enumerate() + .map(|(idx, variant)| { + if let IrVariantKind::Value = &variant.kind { + format!( + "if (apiObj is {}) {{ wireObj.tag = {}; return; }}", + variant.name, idx + ) + } else { + let r = format!("wireObj.kind.ref.{}.ref", variant.name); + let body: Vec<_> = match &variant.kind { + IrVariantKind::Struct(st) => st + .fields + .iter() + .map(|field| { + format!( + "{}.{} = _api2wire_{}(apiObj.{});", + r, + field.name.rust_style(), + field.ty.safe_ident(), + field.name.dart_style() + ) + }) + .collect(), + _ => unreachable!(), + }; + format!( + "if (apiObj is {0}) {{ + wireObj.tag = {1}; + wireObj.kind = inner.inflate_{2}_{0}(); + {3} + }}", + variant.name, + idx, + self.ir.name, + body.join("\n") + ) + } + }) + .collect::>() + .join("\n"), + ) + } else { + None + } + } + + fn wire2api_body(&self) -> String { + if self.ir.is_struct { + let enu = self.ir.get(self.context.ir_file); + let variants = enu + .variants() + .iter() + .enumerate() + .map(|(idx, variant)| { + let args = match &variant.kind { + IrVariantKind::Value => "".to_owned(), + IrVariantKind::Struct(st) => st + .fields + .iter() + .enumerate() + .map(|(idx, field)| { + let val = format!( + "_wire2api_{}(raw[{}]),", + field.ty.safe_ident(), + idx + 1 + ); + if st.is_fields_named { + format!("{}: {}", field.name.dart_style(), val) + } else { + val + } + }) + .collect::>() + .join(""), + }; + format!("case {}: return {}({});", idx, variant.name, args) + }) + .collect::>(); + format!( + "switch (raw[0]) {{ + {} + default: throw Exception(\"unreachable\"); + }}", + variants.join("\n"), + ) + } else { + format!("return {}.values[raw];", self.ir.name) + } + } + + fn structs(&self) -> String { + let src = self.ir.get(self.context.ir_file); + + let comments = dart_comments(&src.comments); + if src.is_struct() { + let variants = src + .variants() + .iter() + .map(|variant| { + let args = match &variant.kind { + IrVariantKind::Value => "".to_owned(), + IrVariantKind::Struct(IrStruct { + is_fields_named: false, + fields, + .. + }) => { + let types = fields.iter().map(|field| &field.ty).collect::>(); + let split = optional_boundary_index(&types); + let types = fields + .iter() + .map(|field| { + format!( + "{}{} {},", + dart_comments(&field.comments), + field.ty.dart_api_type(), + field.name.dart_style() + ) + }) + .collect::>(); + if let Some(idx) = split { + let before = &types[..idx]; + let after = &types[idx..]; + format!("{}[{}]", before.join(""), after.join("")) + } else { + types.join("") + } + } + IrVariantKind::Struct(st) => { + let fields = st + .fields + .iter() + .map(|field| { + format!( + "{}{}{} {},", + dart_comments(&field.comments), + field.ty.dart_required_modifier(), + field.ty.dart_api_type(), + field.name.dart_style() + ) + }) + .collect::>(); + format!("{{ {} }}", fields.join("")) + } + }; + format!( + "{}const factory {}.{}({}) = {};", + dart_comments(&variant.comments), + self.ir.name, + variant.name.dart_style(), + args, + variant.name.rust_style(), + ) + }) + .collect::>(); + format!( + "@freezed + class {0} with _${0} {{ + {1} + }}", + self.ir.name, + variants.join("\n") + ) + } else { + let variants = src + .variants() + .iter() + .map(|variant| { + format!( + "{}{},", + dart_comments(&variant.comments), + variant.name.rust_style() + ) + }) + .collect::>() + .join("\n"); + format!( + "{}enum {} {{ + {} + }}", + comments, self.ir.name, variants + ) + } + } +} diff --git a/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_general_list.rs b/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_general_list.rs new file mode 100644 index 000000000..000f7288f --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_general_list.rs @@ -0,0 +1,27 @@ +use crate::generator::dart::ty::*; +use crate::ir::*; +use crate::type_dart_generator_struct; + +type_dart_generator_struct!(TypeGeneralListGenerator, IrTypeGeneralList); + +impl TypeDartGeneratorTrait for TypeGeneralListGenerator<'_> { + fn api2wire_body(&self) -> Option { + // NOTE the memory strategy is same as PrimitiveList, see comments there. + Some(format!( + "final ans = inner.new_{}(raw.length); + for (var i = 0; i < raw.length; ++i) {{ + _api_fill_to_wire_{}(raw[i], ans.ref.ptr[i]); + }} + return ans;", + self.ir.safe_ident(), + self.ir.inner.safe_ident() + )) + } + + fn wire2api_body(&self) -> String { + format!( + "return (raw as List).map(_wire2api_{}).toList();", + self.ir.inner.safe_ident() + ) + } +} diff --git a/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_optional.rs b/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_optional.rs new file mode 100644 index 000000000..5b7e60d27 --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_optional.rs @@ -0,0 +1,31 @@ +use crate::generator::dart::ty::*; +use crate::ir::*; +use crate::type_dart_generator_struct; + +type_dart_generator_struct!(TypeOptionalGenerator, IrTypeOptional); + +impl TypeDartGeneratorTrait for TypeOptionalGenerator<'_> { + fn api2wire_body(&self) -> Option { + Some(format!( + "return raw == null ? ffi.nullptr : _api2wire_{}(raw);", + self.ir.inner.safe_ident() + )) + } + + fn api_fill_to_wire_body(&self) -> Option { + if !self.ir.needs_initialization() || self.ir.is_list() { + return None; + } + Some(format!( + "if (apiObj != null) _api_fill_to_wire_{}(apiObj, wireObj);", + self.ir.inner.safe_ident() + )) + } + + fn wire2api_body(&self) -> String { + format!( + "return raw == null ? null : _wire2api_{}(raw);", + self.ir.inner.safe_ident() + ) + } +} diff --git a/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_primitive.rs b/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_primitive.rs new file mode 100644 index 000000000..0ed9aa686 --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_primitive.rs @@ -0,0 +1,22 @@ +use crate::generator::dart::gen_wire2api_simple_type_cast; +use crate::generator::dart::ty::*; +use crate::ir::*; +use crate::type_dart_generator_struct; + +type_dart_generator_struct!(TypePrimitiveGenerator, IrTypePrimitive); + +impl TypeDartGeneratorTrait for TypePrimitiveGenerator<'_> { + fn api2wire_body(&self) -> Option { + Some(match self.ir { + IrTypePrimitive::Bool => "return raw ? 1 : 0;".to_owned(), + _ => "return raw;".to_string(), + }) + } + + fn wire2api_body(&self) -> String { + match self.ir { + IrTypePrimitive::Unit => "return;".to_owned(), + _ => gen_wire2api_simple_type_cast(&self.ir.dart_api_type()), + } + } +} diff --git a/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_primitive_list.rs b/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_primitive_list.rs new file mode 100644 index 000000000..d07c24d6b --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_primitive_list.rs @@ -0,0 +1,30 @@ +use crate::generator::dart::gen_wire2api_simple_type_cast; +use crate::generator::dart::ty::*; +use crate::ir::*; +use crate::type_dart_generator_struct; + +type_dart_generator_struct!(TypePrimitiveListGenerator, IrTypePrimitiveList); + +impl TypeDartGeneratorTrait for TypePrimitiveListGenerator<'_> { + fn api2wire_body(&self) -> Option { + // NOTE Dart code *only* allocates memory. It never *release* memory by itself. + // Instead, Rust receives that pointer and now it is in control of Rust. + // Therefore, *never* continue to use this pointer after you have passed the pointer + // to Rust. + // NOTE WARN: Never use the [calloc] provided by Dart FFI to allocate any memory. + // Instead, ask Rust to allocate some memory and return raw pointers. Otherwise, + // memory will be allocated in one dylib (e.g. libflutter.so), and then be released + // by another dylib (e.g. my_rust_code.so), especially in Android platform. It can be + // undefined behavior. + Some(format!( + "final ans = inner.new_{}(raw.length); + ans.ref.ptr.asTypedList(raw.length).setAll(0, raw); + return ans;", + self.ir.safe_ident(), + )) + } + + fn wire2api_body(&self) -> String { + gen_wire2api_simple_type_cast(&self.ir.dart_api_type()) + } +} diff --git a/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_struct.rs b/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_struct.rs new file mode 100644 index 000000000..fa67bd32a --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_struct.rs @@ -0,0 +1,135 @@ +use crate::generator::dart::ty::*; +use crate::generator::dart::{dart_comments, dart_metadata}; +use crate::ir::*; +use crate::type_dart_generator_struct; + +type_dart_generator_struct!(TypeStructRefGenerator, IrTypeStructRef); + +impl TypeDartGeneratorTrait for TypeStructRefGenerator<'_> { + fn api2wire_body(&self) -> Option { + None + } + + fn api_fill_to_wire_body(&self) -> Option { + let s = self.ir.get(self.context.ir_file); + Some( + s.fields + .iter() + .map(|field| { + format!( + "wireObj.{} = _api2wire_{}(apiObj.{});", + field.name.rust_style(), + field.ty.safe_ident(), + field.name.dart_style() + ) + }) + .collect::>() + .join("\n"), + ) + } + + fn wire2api_body(&self) -> String { + let s = self.ir.get(self.context.ir_file); + let inner = s + .fields + .iter() + .enumerate() + .map(|(idx, field)| { + format!( + "{}: _wire2api_{}(arr[{}]),", + field.name.dart_style(), + field.ty.safe_ident(), + idx + ) + }) + .collect::>() + .join("\n"); + + format!( + "final arr = raw as List; + if (arr.length != {}) throw Exception('unexpected arr length: expect {} but see ${{arr.length}}'); + return {}({});", + s.fields.len(), + s.fields.len(), + s.name, inner, + ) + } + + fn structs(&self) -> String { + let src = self.ir.get(self.context.ir_file); + let comments = dart_comments(&src.comments); + let metadata = dart_metadata(&src.dart_metadata); + + if src.using_freezed() { + let constructor_params = src + .fields + .iter() + .map(|f| { + format!( + "{} {} {},", + f.ty.dart_required_modifier(), + f.ty.dart_api_type(), + f.name.dart_style() + ) + }) + .collect::>() + .join(""); + + format!( + "{}{}class {} with _${} {{ + const factory {}({{{}}}) = _{}; + }}", + comments, + metadata, + self.ir.name, + self.ir.name, + self.ir.name, + constructor_params, + self.ir.name + ) + } else { + let field_declarations = src + .fields + .iter() + .map(|f| { + let comments = dart_comments(&f.comments); + format!( + "{}{} {} {};", + comments, + if f.is_final { "final" } else { "" }, + f.ty.dart_api_type(), + f.name.dart_style() + ) + }) + .collect::>() + .join("\n"); + + let constructor_params = src + .fields + .iter() + .map(|f| { + format!( + "{}this.{},", + f.ty.dart_required_modifier(), + f.name.dart_style() + ) + }) + .collect::>() + .join(""); + + format!( + "{}{}class {} {{ + {} + + {}({{{}}}); + }}", + comments, + metadata, + self.ir.name, + field_declarations, + self.ir.name, + constructor_params + ) + } + } +} diff --git a/libs/flutter_rust_bridge_codegen/src/generator/mod.rs b/libs/flutter_rust_bridge_codegen/src/generator/mod.rs new file mode 100644 index 000000000..3891c02e3 --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/generator/mod.rs @@ -0,0 +1,3 @@ +pub mod c; +pub mod dart; +pub mod rust; diff --git a/libs/flutter_rust_bridge_codegen/src/generator/rust/mod.rs b/libs/flutter_rust_bridge_codegen/src/generator/rust/mod.rs new file mode 100644 index 000000000..0b0d1df88 --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/generator/rust/mod.rs @@ -0,0 +1,481 @@ +mod ty; +mod ty_boxed; +mod ty_delegate; +mod ty_enum; +mod ty_general_list; +mod ty_optional; +mod ty_primitive; +mod ty_primitive_list; +mod ty_struct; + +pub use ty::*; +pub use ty_boxed::*; +pub use ty_delegate::*; +pub use ty_enum::*; +pub use ty_general_list::*; +pub use ty_optional::*; +pub use ty_primitive::*; +pub use ty_primitive_list::*; +pub use ty_struct::*; + +use std::collections::HashSet; + +use crate::ir::IrType::*; +use crate::ir::*; +use crate::others::*; + +pub const HANDLER_NAME: &str = "FLUTTER_RUST_BRIDGE_HANDLER"; + +pub struct Output { + pub code: String, + pub extern_func_names: Vec, +} + +pub fn generate(ir_file: &IrFile, rust_wire_mod: &str) -> Output { + let mut generator = Generator::new(); + let code = generator.generate(ir_file, rust_wire_mod); + + Output { + code, + extern_func_names: generator.extern_func_collector.names, + } +} + +struct Generator { + extern_func_collector: ExternFuncCollector, +} + +impl Generator { + fn new() -> Self { + Self { + extern_func_collector: ExternFuncCollector::new(), + } + } + + fn generate(&mut self, ir_file: &IrFile, rust_wire_mod: &str) -> String { + let mut lines: Vec = vec![]; + + let distinct_input_types = ir_file.distinct_types(true, false); + let distinct_output_types = ir_file.distinct_types(false, true); + + lines.push(r#"#![allow(non_camel_case_types, unused, clippy::redundant_closure, clippy::useless_conversion, clippy::unit_arg, clippy::double_parens, non_snake_case)]"#.to_string()); + lines.push(CODE_HEADER.to_string()); + + lines.push(String::new()); + lines.push(format!("use crate::{}::*;", rust_wire_mod)); + lines.push("use flutter_rust_bridge::*;".to_string()); + lines.push(String::new()); + + lines.push(self.section_header_comment("imports")); + lines.extend(self.generate_imports( + ir_file, + rust_wire_mod, + &distinct_input_types, + &distinct_output_types, + )); + lines.push(String::new()); + + lines.push(self.section_header_comment("wire functions")); + lines.extend( + ir_file + .funcs + .iter() + .map(|f| self.generate_wire_func(f, ir_file)), + ); + + lines.push(self.section_header_comment("wire structs")); + lines.extend( + distinct_input_types + .iter() + .map(|ty| self.generate_wire_struct(ty, ir_file)), + ); + lines.extend( + distinct_input_types + .iter() + .map(|ty| TypeRustGenerator::new(ty.clone(), ir_file).structs()), + ); + + lines.push(self.section_header_comment("wrapper structs")); + lines.extend( + distinct_output_types + .iter() + .filter_map(|ty| self.generate_wrapper_struct(ty, ir_file)), + ); + lines.push(self.section_header_comment("static checks")); + let static_checks: Vec<_> = distinct_output_types + .iter() + .filter_map(|ty| self.generate_static_checks(ty, ir_file)) + .collect(); + if !static_checks.is_empty() { + lines.push("const _: fn() = || {".to_owned()); + lines.extend(static_checks); + lines.push("};".to_owned()); + } + + lines.push(self.section_header_comment("allocate functions")); + lines.extend( + distinct_input_types + .iter() + .map(|f| self.generate_allocate_funcs(f, ir_file)), + ); + + lines.push(self.section_header_comment("impl Wire2Api")); + lines.push(self.generate_wire2api_misc().to_string()); + lines.extend( + distinct_input_types + .iter() + .map(|ty| self.generate_wire2api_func(ty, ir_file)), + ); + + lines.push(self.section_header_comment("impl NewWithNullPtr")); + lines.push(self.generate_new_with_nullptr_misc().to_string()); + lines.extend( + distinct_input_types + .iter() + .map(|ty| self.generate_new_with_nullptr_func(ty, ir_file)), + ); + + lines.push(self.section_header_comment("impl IntoDart")); + lines.extend( + distinct_output_types + .iter() + .map(|ty| self.generate_impl_intodart(ty, ir_file)), + ); + + lines.push(self.section_header_comment("executor")); + lines.push(self.generate_executor(ir_file)); + + lines.push(self.section_header_comment("sync execution mode utility")); + lines.push(self.generate_sync_execution_mode_utility()); + + lines.join("\n") + } + + fn section_header_comment(&self, section_name: &str) -> String { + format!("// Section: {}\n", section_name) + } + + fn generate_imports( + &self, + ir_file: &IrFile, + rust_wire_mod: &str, + distinct_input_types: &[IrType], + distinct_output_types: &[IrType], + ) -> impl Iterator { + let input_type_imports = distinct_input_types + .iter() + .map(|api_type| generate_import(api_type, ir_file)); + let output_type_imports = distinct_output_types + .iter() + .map(|api_type| generate_import(api_type, ir_file)); + + input_type_imports + .chain(output_type_imports) + // Filter out `None` and unwrap + .flatten() + // Don't include imports from the API file + .filter(|import| !import.starts_with(&format!("use crate::{}::", rust_wire_mod))) + // de-duplicate + .collect::>() + .into_iter() + } + + fn generate_executor(&mut self, ir_file: &IrFile) -> String { + if ir_file.has_executor { + "/* nothing since executor detected */".to_string() + } else { + format!( + "support::lazy_static! {{ + pub static ref {}: support::DefaultHandler = Default::default(); + }} + ", + HANDLER_NAME + ) + } + } + + fn generate_sync_execution_mode_utility(&mut self) -> String { + self.extern_func_collector.generate( + "free_WireSyncReturnStruct", + &["val: support::WireSyncReturnStruct"], + None, + "unsafe { let _ = support::vec_from_leak_ptr(val.ptr, val.len); }", + ) + } + + fn generate_wire_func(&mut self, func: &IrFunc, ir_file: &IrFile) -> String { + let params = [ + if func.mode.has_port_argument() { + vec!["port_: i64".to_string()] + } else { + vec![] + }, + func.inputs + .iter() + .map(|field| { + format!( + "{}: {}{}", + field.name.rust_style(), + field.ty.rust_wire_modifier(), + field.ty.rust_wire_type() + ) + }) + .collect::>(), + ] + .concat(); + + let inner_func_params = [ + match func.mode { + IrFuncMode::Normal | IrFuncMode::Sync => vec![], + IrFuncMode::Stream => vec!["task_callback.stream_sink()".to_string()], + }, + func.inputs + .iter() + .map(|field| format!("api_{}", field.name.rust_style())) + .collect::>(), + ] + .concat(); + + let wrap_info_obj = format!( + "WrapInfo{{ debug_name: \"{}\", port: {}, mode: FfiCallMode::{} }}", + func.name, + if func.mode.has_port_argument() { + "Some(port_)" + } else { + "None" + }, + func.mode.ffi_call_mode(), + ); + + let code_wire2api = func + .inputs + .iter() + .map(|field| { + format!( + "let api_{} = {}.wire2api();", + field.name.rust_style(), + field.name.rust_style() + ) + }) + .collect::>() + .join(""); + + let code_call_inner_func = TypeRustGenerator::new(func.output.clone(), ir_file) + .wrap_obj(format!("{}({})", func.name, inner_func_params.join(", "))); + let code_call_inner_func_result = if func.fallible { + code_call_inner_func + } else { + format!("Ok({})", code_call_inner_func) + }; + + let (handler_func_name, return_type, code_closure) = match func.mode { + IrFuncMode::Sync => ( + "wrap_sync", + Some("support::WireSyncReturnStruct"), + format!( + "{} + {}", + code_wire2api, code_call_inner_func_result, + ), + ), + IrFuncMode::Normal | IrFuncMode::Stream => ( + "wrap", + None, + format!( + "{} + move |task_callback| {} + ", + code_wire2api, code_call_inner_func_result, + ), + ), + }; + + self.extern_func_collector.generate( + &func.wire_func_name(), + ¶ms + .iter() + .map(std::ops::Deref::deref) + .collect::>(), + return_type, + &format!( + " + {}.{}({}, move || {{ + {} + }}) + ", + HANDLER_NAME, handler_func_name, wrap_info_obj, code_closure, + ), + ) + } + + fn generate_wire_struct(&mut self, ty: &IrType, ir_file: &IrFile) -> String { + // println!("generate_wire_struct: {:?}", ty); + if let Some(fields) = TypeRustGenerator::new(ty.clone(), ir_file).wire_struct_fields() { + format!( + r###" + #[repr(C)] + #[derive(Clone)] + pub struct {} {{ + {} + }} + "###, + ty.rust_wire_type(), + fields.join(",\n"), + ) + } else { + "".to_string() + } + } + + fn generate_allocate_funcs(&mut self, ty: &IrType, ir_file: &IrFile) -> String { + // println!("generate_allocate_funcs: {:?}", ty); + TypeRustGenerator::new(ty.clone(), ir_file).allocate_funcs(&mut self.extern_func_collector) + } + + fn generate_wire2api_misc(&self) -> &'static str { + r"pub trait Wire2Api { + fn wire2api(self) -> T; + } + + impl Wire2Api> for *mut S + where + *mut S: Wire2Api + { + fn wire2api(self) -> Option { + if self.is_null() { + None + } else { + Some(self.wire2api()) + } + } + } + " + } + + fn generate_wire2api_func(&mut self, ty: &IrType, ir_file: &IrFile) -> String { + // println!("generate_wire2api_func: {:?}", ty); + if let Some(body) = TypeRustGenerator::new(ty.clone(), ir_file).wire2api_body() { + format!( + "impl Wire2Api<{}> for {} {{ + fn wire2api(self) -> {} {{ + {} + }} + }} + ", + ty.rust_api_type(), + ty.rust_wire_modifier() + &ty.rust_wire_type(), + ty.rust_api_type(), + body, + ) + } else { + "".to_string() + } + } + + fn generate_static_checks(&mut self, ty: &IrType, ir_file: &IrFile) -> Option { + TypeRustGenerator::new(ty.clone(), ir_file).static_checks() + } + + fn generate_wrapper_struct(&mut self, ty: &IrType, ir_file: &IrFile) -> Option { + match ty { + IrType::StructRef(_) | IrType::EnumRef(_) => { + TypeRustGenerator::new(ty.clone(), ir_file) + .wrapper_struct() + .map(|wrapper| { + format!( + r###" + #[derive(Clone)] + struct {}({}); + "###, + wrapper, + ty.rust_api_type(), + ) + }) + } + _ => None, + } + } + + fn generate_new_with_nullptr_misc(&self) -> &'static str { + "pub trait NewWithNullPtr { + fn new_with_null_ptr() -> Self; + } + + impl NewWithNullPtr for *mut T { + fn new_with_null_ptr() -> Self { + std::ptr::null_mut() + } + } + " + } + + fn generate_new_with_nullptr_func(&mut self, ty: &IrType, ir_file: &IrFile) -> String { + TypeRustGenerator::new(ty.clone(), ir_file) + .new_with_nullptr(&mut self.extern_func_collector) + } + + fn generate_impl_intodart(&mut self, ty: &IrType, ir_file: &IrFile) -> String { + // println!("generate_impl_intodart: {:?}", ty); + TypeRustGenerator::new(ty.clone(), ir_file).impl_intodart() + } +} + +pub fn generate_import(api_type: &IrType, ir_file: &IrFile) -> Option { + TypeRustGenerator::new(api_type.clone(), ir_file).imports() +} + +pub fn generate_list_allocate_func( + collector: &mut ExternFuncCollector, + safe_ident: &str, + list: &impl IrTypeTrait, + inner: &IrType, +) -> String { + collector.generate( + &format!("new_{}", safe_ident), + &["len: i32"], + Some(&[ + list.rust_wire_modifier().as_str(), + list.rust_wire_type().as_str() + ].concat()), + &format!( + "let wrap = {} {{ ptr: support::new_leak_vec_ptr(<{}{}>::new_with_null_ptr(), len), len }}; + support::new_leak_box_ptr(wrap)", + list.rust_wire_type(), + inner.rust_ptr_modifier(), + inner.rust_wire_type() + ), + ) +} + +pub struct ExternFuncCollector { + names: Vec, +} + +impl ExternFuncCollector { + fn new() -> Self { + ExternFuncCollector { names: vec![] } + } + + fn generate( + &mut self, + func_name: &str, + params: &[&str], + return_type: Option<&str>, + body: &str, + ) -> String { + self.names.push(func_name.to_string()); + + format!( + r#" + #[no_mangle] + pub extern "C" fn {}({}) {} {{ + {} + }} + "#, + func_name, + params.join(", "), + return_type.map_or("".to_string(), |r| format!("-> {}", r)), + body, + ) + } +} diff --git a/libs/flutter_rust_bridge_codegen/src/generator/rust/ty.rs b/libs/flutter_rust_bridge_codegen/src/generator/rust/ty.rs new file mode 100644 index 000000000..827d6b8f1 --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/generator/rust/ty.rs @@ -0,0 +1,96 @@ +use crate::generator::rust::*; +use enum_dispatch::enum_dispatch; + +#[enum_dispatch] +pub trait TypeRustGeneratorTrait { + fn wire2api_body(&self) -> Option; + + fn wire_struct_fields(&self) -> Option> { + None + } + + fn static_checks(&self) -> Option { + None + } + + fn wrapper_struct(&self) -> Option { + None + } + + fn self_access(&self, obj: String) -> String { + obj + } + + fn wrap_obj(&self, obj: String) -> String { + obj + } + + fn convert_to_dart(&self, obj: String) -> String { + format!("{}.into_dart()", obj) + } + + fn structs(&self) -> String { + "".to_string() + } + + fn allocate_funcs(&self, _collector: &mut ExternFuncCollector) -> String { + "".to_string() + } + + fn impl_intodart(&self) -> String { + "".to_string() + } + + fn new_with_nullptr(&self, _collector: &mut ExternFuncCollector) -> String { + "".to_string() + } + + fn imports(&self) -> Option { + None + } +} + +#[derive(Debug, Clone)] +pub struct TypeGeneratorContext<'a> { + pub ir_file: &'a IrFile, +} + +#[macro_export] +macro_rules! type_rust_generator_struct { + ($cls:ident, $ir_cls:ty) => { + #[derive(Debug, Clone)] + pub struct $cls<'a> { + pub ir: $ir_cls, + pub context: TypeGeneratorContext<'a>, + } + }; +} + +#[enum_dispatch(TypeRustGeneratorTrait)] +#[derive(Debug, Clone)] +pub enum TypeRustGenerator<'a> { + Primitive(TypePrimitiveGenerator<'a>), + Delegate(TypeDelegateGenerator<'a>), + PrimitiveList(TypePrimitiveListGenerator<'a>), + Optional(TypeOptionalGenerator<'a>), + GeneralList(TypeGeneralListGenerator<'a>), + StructRef(TypeStructRefGenerator<'a>), + Boxed(TypeBoxedGenerator<'a>), + EnumRef(TypeEnumRefGenerator<'a>), +} + +impl<'a> TypeRustGenerator<'a> { + pub fn new(ty: IrType, ir_file: &'a IrFile) -> Self { + let context = TypeGeneratorContext { ir_file }; + match ty { + Primitive(ir) => TypePrimitiveGenerator { ir, context }.into(), + Delegate(ir) => TypeDelegateGenerator { ir, context }.into(), + PrimitiveList(ir) => TypePrimitiveListGenerator { ir, context }.into(), + Optional(ir) => TypeOptionalGenerator { ir, context }.into(), + GeneralList(ir) => TypeGeneralListGenerator { ir, context }.into(), + StructRef(ir) => TypeStructRefGenerator { ir, context }.into(), + Boxed(ir) => TypeBoxedGenerator { ir, context }.into(), + EnumRef(ir) => TypeEnumRefGenerator { ir, context }.into(), + } + } +} diff --git a/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_boxed.rs b/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_boxed.rs new file mode 100644 index 000000000..ab6d25d02 --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_boxed.rs @@ -0,0 +1,62 @@ +use crate::generator::rust::ty::*; +use crate::generator::rust::{generate_import, ExternFuncCollector}; +use crate::ir::IrType::Primitive; +use crate::ir::*; +use crate::type_rust_generator_struct; + +type_rust_generator_struct!(TypeBoxedGenerator, IrTypeBoxed); + +impl TypeRustGeneratorTrait for TypeBoxedGenerator<'_> { + fn wire2api_body(&self) -> Option { + let IrTypeBoxed { + inner: box_inner, + exist_in_real_api, + } = &self.ir; + Some(match (box_inner.as_ref(), exist_in_real_api) { + (IrType::Primitive(_), false) => "unsafe { *support::box_from_leak_ptr(self) }".into(), + (IrType::Primitive(_), true) => "unsafe { support::box_from_leak_ptr(self) }".into(), + _ => { + "let wrap = unsafe { support::box_from_leak_ptr(self) }; (*wrap).wire2api().into()" + .into() + } + }) + } + + fn wrapper_struct(&self) -> Option { + let src = TypeRustGenerator::new(*self.ir.inner.clone(), self.context.ir_file); + src.wrapper_struct() + } + + fn self_access(&self, obj: String) -> String { + format!("(*{})", obj) + } + + fn wrap_obj(&self, obj: String) -> String { + let src = TypeRustGenerator::new(*self.ir.inner.clone(), self.context.ir_file); + src.wrap_obj(self.self_access(obj)) + } + + fn allocate_funcs(&self, collector: &mut ExternFuncCollector) -> String { + match &*self.ir.inner { + Primitive(prim) => collector.generate( + &format!("new_{}", self.ir.safe_ident()), + &[&format!("value: {}", prim.rust_wire_type())], + Some(&format!("*mut {}", prim.rust_wire_type())), + "support::new_leak_box_ptr(value)", + ), + inner => collector.generate( + &format!("new_{}", self.ir.safe_ident()), + &[], + Some(&[self.ir.rust_wire_modifier(), self.ir.rust_wire_type()].concat()), + &format!( + "support::new_leak_box_ptr({}::new_with_null_ptr())", + inner.rust_wire_type() + ), + ), + } + } + + fn imports(&self) -> Option { + generate_import(&self.ir.inner, self.context.ir_file) + } +} diff --git a/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_delegate.rs b/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_delegate.rs new file mode 100644 index 000000000..9b67ba7dd --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_delegate.rs @@ -0,0 +1,45 @@ +use crate::generator::rust::ty::*; +use crate::generator::rust::{ + generate_list_allocate_func, ExternFuncCollector, TypeGeneralListGenerator, +}; +use crate::ir::*; +use crate::type_rust_generator_struct; + +type_rust_generator_struct!(TypeDelegateGenerator, IrTypeDelegate); + +impl TypeRustGeneratorTrait for TypeDelegateGenerator<'_> { + fn wire2api_body(&self) -> Option { + Some(match &self.ir { + IrTypeDelegate::String => "let vec: Vec = self.wire2api(); + String::from_utf8_lossy(&vec).into_owned()" + .into(), + IrTypeDelegate::SyncReturnVecU8 => "/*unsupported*/".into(), + IrTypeDelegate::ZeroCopyBufferVecPrimitive(_) => { + "ZeroCopyBuffer(self.wire2api())".into() + } + IrTypeDelegate::StringList => TypeGeneralListGenerator::WIRE2API_BODY.to_string(), + }) + } + + fn wire_struct_fields(&self) -> Option> { + match &self.ir { + ty @ IrTypeDelegate::StringList => Some(vec![ + format!("ptr: *mut *mut {}", ty.get_delegate().rust_wire_type()), + "len: i32".to_owned(), + ]), + _ => None, + } + } + + fn allocate_funcs(&self, collector: &mut ExternFuncCollector) -> String { + match &self.ir { + list @ IrTypeDelegate::StringList => generate_list_allocate_func( + collector, + &self.ir.safe_ident(), + list, + &list.get_delegate(), + ), + _ => "".to_string(), + } + } +} diff --git a/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_enum.rs b/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_enum.rs new file mode 100644 index 000000000..a0fc42ddb --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_enum.rs @@ -0,0 +1,343 @@ +use crate::generator::rust::ty::*; +use crate::generator::rust::ExternFuncCollector; +use crate::ir::*; +use crate::type_rust_generator_struct; + +type_rust_generator_struct!(TypeEnumRefGenerator, IrTypeEnumRef); + +impl TypeRustGeneratorTrait for TypeEnumRefGenerator<'_> { + fn wire2api_body(&self) -> Option { + let enu = self.ir.get(self.context.ir_file); + Some(if self.ir.is_struct { + let variants = enu + .variants() + .iter() + .enumerate() + .map(|(idx, variant)| match &variant.kind { + IrVariantKind::Value => { + format!("{} => {}::{},", idx, enu.name, variant.name) + } + IrVariantKind::Struct(st) => { + let fields: Vec<_> = st + .fields + .iter() + .map(|field| { + if st.is_fields_named { + format!("{0}: ans.{0}.wire2api()", field.name.rust_style()) + } else { + format!("ans.{}.wire2api()", field.name.rust_style()) + } + }) + .collect(); + let (left, right) = st.brackets_pair(); + format!( + "{} => unsafe {{ + let ans = support::box_from_leak_ptr(self.kind); + let ans = support::box_from_leak_ptr(ans.{2}); + {}::{2}{3}{4}{5} + }}", + idx, + enu.name, + variant.name, + left, + fields.join(","), + right + ) + } + }) + .collect::>(); + format!( + "match self.tag {{ + {} + _ => unreachable!(), + }}", + variants.join("\n"), + ) + } else { + let variants = enu + .variants() + .iter() + .enumerate() + .map(|(idx, variant)| format!("{} => {}::{},", idx, enu.name, variant.name)) + .collect::>() + .join("\n"); + format!( + "match self {{ + {} + _ => unreachable!(\"Invalid variant for {}: {{}}\", self), + }}", + variants, enu.name + ) + }) + } + + fn structs(&self) -> String { + let src = self.ir.get(self.context.ir_file); + if !src.is_struct() { + return "".to_owned(); + } + let variant_structs = src + .variants() + .iter() + .map(|variant| { + let fields = match &variant.kind { + IrVariantKind::Value => vec![], + IrVariantKind::Struct(s) => s + .fields + .iter() + .map(|field| { + format!( + "{}: {}{},", + field.name.rust_style(), + field.ty.rust_wire_modifier(), + field.ty.rust_wire_type() + ) + }) + .collect(), + }; + format!( + "#[repr(C)] + #[derive(Clone)] + pub struct {}_{} {{ {} }}", + self.ir.name, + variant.name, + fields.join("\n") + ) + }) + .collect::>(); + let union_fields = src + .variants() + .iter() + .map(|variant| format!("{0}: *mut {1}_{0},", variant.name, self.ir.name)) + .collect::>(); + format!( + "#[repr(C)] + #[derive(Clone)] + pub struct {0} {{ tag: i32, kind: *mut {1}Kind }} + + #[repr(C)] + pub union {1}Kind {{ + {2} + }} + + {3}", + self.ir.rust_wire_type(), + self.ir.name, + union_fields.join("\n"), + variant_structs.join("\n\n") + ) + } + + fn static_checks(&self) -> Option { + let src = self.ir.get(self.context.ir_file); + src.wrapper_name.as_ref()?; + + let branches: Vec<_> = src + .variants() + .iter() + .map(|variant| match &variant.kind { + IrVariantKind::Value => format!("{}::{} => {{}}", src.name, variant.name), + IrVariantKind::Struct(s) => { + let pattern = s + .fields + .iter() + .map(|field| field.name.rust_style().to_owned()) + .collect::>(); + let pattern = if s.is_fields_named { + format!("{}::{} {{ {} }}", src.name, variant.name, pattern.join(",")) + } else { + format!("{}::{}({})", src.name, variant.name, pattern.join(",")) + }; + let checks = s + .fields + .iter() + .map(|field| { + format!( + "let _: {} = {};\n", + field.ty.rust_api_type(), + field.name.rust_style(), + ) + }) + .collect::>(); + format!("{} => {{ {} }}", pattern, checks.join("")) + } + }) + .collect(); + Some(format!( + "match None::<{}>.unwrap() {{ {} }}", + src.name, + branches.join(","), + )) + } + + fn wrapper_struct(&self) -> Option { + let src = self.ir.get(self.context.ir_file); + src.wrapper_name.as_ref().cloned() + } + + fn self_access(&self, obj: String) -> String { + let src = self.ir.get(self.context.ir_file); + match &src.wrapper_name { + Some(_) => format!("{}.0", obj), + None => obj, + } + } + + fn wrap_obj(&self, obj: String) -> String { + match self.wrapper_struct() { + Some(wrapper) => format!("{}({})", wrapper, obj), + None => obj, + } + } + + fn impl_intodart(&self) -> String { + let src = self.ir.get(self.context.ir_file); + + let (name, self_path): (&str, &str) = match &src.wrapper_name { + Some(wrapper) => (wrapper, &src.name), + None => (&src.name, "Self"), + }; + let self_ref = self.self_access("self".to_owned()); + if self.ir.is_struct { + let variants = src + .variants() + .iter() + .enumerate() + .map(|(idx, variant)| { + let tag = format!("{}.into_dart()", idx); + match &variant.kind { + IrVariantKind::Value => { + format!("{}::{} => vec![{}],", self_path, variant.name, tag) + } + IrVariantKind::Struct(s) => { + let fields = Some(tag) + .into_iter() + .chain(s.fields.iter().map(|field| { + let gen = TypeRustGenerator::new( + field.ty.clone(), + self.context.ir_file, + ); + gen.convert_to_dart(field.name.rust_style().to_owned()) + })) + .collect::>(); + let pattern = s + .fields + .iter() + .map(|field| field.name.rust_style().to_owned()) + .collect::>(); + let (left, right) = s.brackets_pair(); + format!( + "{}::{}{}{}{} => vec![{}],", + self_path, + variant.name, + left, + pattern.join(","), + right, + fields.join(",") + ) + } + } + }) + .collect::>(); + format!( + "impl support::IntoDart for {} {{ + fn into_dart(self) -> support::DartCObject {{ + match {} {{ + {} + }}.into_dart() + }} + }} + impl support::IntoDartExceptPrimitive for {0} {{}} + ", + name, + self_ref, + variants.join("\n") + ) + } else { + let variants = src + .variants() + .iter() + .enumerate() + .map(|(idx, variant)| format!("{}::{} => {},", self_path, variant.name, idx)) + .collect::>() + .join("\n"); + format!( + "impl support::IntoDart for {} {{ + fn into_dart(self) -> support::DartCObject {{ + match {} {{ + {} + }}.into_dart() + }} + }} + ", + name, self_ref, variants + ) + } + } + + fn new_with_nullptr(&self, collector: &mut ExternFuncCollector) -> String { + if !self.ir.is_struct { + return "".to_string(); + } + + fn init_of(ty: &IrType) -> &str { + if ty.rust_wire_is_pointer() { + "core::ptr::null_mut()" + } else { + "Default::default()" + } + } + + let src = self.ir.get(self.context.ir_file); + + let inflators = src + .variants() + .iter() + .filter_map(|variant| { + let typ = format!("{}_{}", self.ir.name, variant.name); + let body: Vec<_> = if let IrVariantKind::Struct(st) = &variant.kind { + st.fields + .iter() + .map(|field| format!("{}: {}", field.name.rust_style(), init_of(&field.ty))) + .collect() + } else { + return None; + }; + Some(collector.generate( + &format!("inflate_{}", typ), + &[], + Some(&format!("*mut {}Kind", self.ir.name)), + &format!( + "support::new_leak_box_ptr({}Kind {{ + {}: support::new_leak_box_ptr({} {{ + {} + }}) + }})", + self.ir.name, + variant.name.rust_style(), + typ, + body.join(",") + ), + )) + }) + .collect::>(); + format!( + "impl NewWithNullPtr for {} {{ + fn new_with_null_ptr() -> Self {{ + Self {{ + tag: -1, + kind: core::ptr::null_mut(), + }} + }} + }} + {}", + self.ir.rust_wire_type(), + inflators.join("\n\n") + ) + } + + fn imports(&self) -> Option { + let api_enum = self.ir.get(self.context.ir_file); + Some(format!("use {};", api_enum.path.join("::"))) + } +} diff --git a/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_general_list.rs b/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_general_list.rs new file mode 100644 index 000000000..1e88a5867 --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_general_list.rs @@ -0,0 +1,55 @@ +use crate::generator::rust::ty::*; +use crate::generator::rust::{generate_import, generate_list_allocate_func, ExternFuncCollector}; +use crate::ir::*; +use crate::type_rust_generator_struct; + +type_rust_generator_struct!(TypeGeneralListGenerator, IrTypeGeneralList); + +impl TypeGeneralListGenerator<'_> { + pub const WIRE2API_BODY: &'static str = " + let vec = unsafe { + let wrap = support::box_from_leak_ptr(self); + support::vec_from_leak_ptr(wrap.ptr, wrap.len) + }; + vec.into_iter().map(Wire2Api::wire2api).collect()"; +} + +impl TypeRustGeneratorTrait for TypeGeneralListGenerator<'_> { + fn wire2api_body(&self) -> Option { + Some(TypeGeneralListGenerator::WIRE2API_BODY.to_string()) + } + + fn wire_struct_fields(&self) -> Option> { + Some(vec![ + format!( + "ptr: *mut {}{}", + self.ir.inner.rust_ptr_modifier(), + self.ir.inner.rust_wire_type() + ), + "len: i32".to_string(), + ]) + } + + fn wrap_obj(&self, obj: String) -> String { + let inner = TypeRustGenerator::new(*self.ir.inner.clone(), self.context.ir_file); + inner + .wrapper_struct() + .map(|wrapper| { + format!( + "{}.into_iter().map(|v| {}({})).collect::>()", + obj, + wrapper, + inner.self_access("v".to_owned()) + ) + }) + .unwrap_or(obj) + } + + fn allocate_funcs(&self, collector: &mut ExternFuncCollector) -> String { + generate_list_allocate_func(collector, &self.ir.safe_ident(), &self.ir, &self.ir.inner) + } + + fn imports(&self) -> Option { + generate_import(&self.ir.inner, self.context.ir_file) + } +} diff --git a/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_optional.rs b/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_optional.rs new file mode 100644 index 000000000..4e13ee23c --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_optional.rs @@ -0,0 +1,30 @@ +use crate::generator::rust::generate_import; +use crate::generator::rust::ty::*; +use crate::ir::*; +use crate::type_rust_generator_struct; + +type_rust_generator_struct!(TypeOptionalGenerator, IrTypeOptional); + +impl TypeRustGeneratorTrait for TypeOptionalGenerator<'_> { + fn wire2api_body(&self) -> Option { + None + } + + fn convert_to_dart(&self, obj: String) -> String { + let inner = TypeRustGenerator::new(*self.ir.inner.clone(), self.context.ir_file); + let obj = match inner.wrapper_struct() { + Some(wrapper) => format!( + "{}.map(|v| {}({}))", + obj, + wrapper, + inner.self_access("v".to_owned()) + ), + None => obj, + }; + format!("{}.into_dart()", obj) + } + + fn imports(&self) -> Option { + generate_import(&self.ir.inner, self.context.ir_file) + } +} diff --git a/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_primitive.rs b/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_primitive.rs new file mode 100644 index 000000000..5fd3bb562 --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_primitive.rs @@ -0,0 +1,11 @@ +use crate::generator::rust::ty::*; +use crate::ir::*; +use crate::type_rust_generator_struct; + +type_rust_generator_struct!(TypePrimitiveGenerator, IrTypePrimitive); + +impl TypeRustGeneratorTrait for TypePrimitiveGenerator<'_> { + fn wire2api_body(&self) -> Option { + Some("self".into()) + } +} diff --git a/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_primitive_list.rs b/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_primitive_list.rs new file mode 100644 index 000000000..3fa85f82c --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_primitive_list.rs @@ -0,0 +1,42 @@ +use crate::generator::rust::ty::*; +use crate::generator::rust::ExternFuncCollector; +use crate::ir::*; +use crate::type_rust_generator_struct; + +type_rust_generator_struct!(TypePrimitiveListGenerator, IrTypePrimitiveList); + +impl TypeRustGeneratorTrait for TypePrimitiveListGenerator<'_> { + fn wire2api_body(&self) -> Option { + Some( + "unsafe { + let wrap = support::box_from_leak_ptr(self); + support::vec_from_leak_ptr(wrap.ptr, wrap.len) + }" + .into(), + ) + } + + fn wire_struct_fields(&self) -> Option> { + Some(vec![ + format!("ptr: *mut {}", self.ir.primitive.rust_wire_type()), + "len: i32".to_string(), + ]) + } + + fn allocate_funcs(&self, collector: &mut ExternFuncCollector) -> String { + collector.generate( + &format!("new_{}", self.ir.safe_ident()), + &["len: i32"], + Some(&format!( + "{}{}", + self.ir.rust_wire_modifier(), + self.ir.rust_wire_type() + )), + &format!( + "let ans = {} {{ ptr: support::new_leak_vec_ptr(Default::default(), len), len }}; + support::new_leak_box_ptr(ans)", + self.ir.rust_wire_type(), + ), + ) + } +} diff --git a/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_struct.rs b/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_struct.rs new file mode 100644 index 000000000..021dd9f47 --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_struct.rs @@ -0,0 +1,185 @@ +use crate::generator::rust::ty::*; +use crate::generator::rust::ExternFuncCollector; +use crate::ir::*; +use crate::type_rust_generator_struct; + +type_rust_generator_struct!(TypeStructRefGenerator, IrTypeStructRef); + +impl TypeRustGeneratorTrait for TypeStructRefGenerator<'_> { + fn wire2api_body(&self) -> Option { + let api_struct = self.ir.get(self.context.ir_file); + let fields_str = &api_struct + .fields + .iter() + .map(|field| { + format!( + "{} self.{}.wire2api()", + if api_struct.is_fields_named { + field.name.rust_style().to_string() + ": " + } else { + String::new() + }, + field.name.rust_style() + ) + }) + .collect::>() + .join(","); + + let (left, right) = api_struct.brackets_pair(); + Some(format!( + "{}{}{}{}", + self.ir.rust_api_type(), + left, + fields_str, + right + )) + } + + fn wire_struct_fields(&self) -> Option> { + let s = self.ir.get(self.context.ir_file); + Some( + s.fields + .iter() + .map(|field| { + format!( + "{}: {}{}", + field.name.rust_style(), + field.ty.rust_wire_modifier(), + field.ty.rust_wire_type() + ) + }) + .collect(), + ) + } + + fn static_checks(&self) -> Option { + let src = self.ir.get(self.context.ir_file); + src.wrapper_name.as_ref()?; + + let var = if src.is_fields_named { + src.name.clone() + } else { + // let bindings cannot shadow tuple structs + format!("{}_", src.name) + }; + let checks = src + .fields + .iter() + .enumerate() + .map(|(i, field)| { + format!( + "let _: {} = {}.{};\n", + field.ty.rust_api_type(), + var, + if src.is_fields_named { + field.name.to_string() + } else { + i.to_string() + }, + ) + }) + .collect::>() + .join(""); + Some(format!( + "{{ let {} = None::<{}>.unwrap(); {} }} ", + var, src.name, checks + )) + } + + fn wrapper_struct(&self) -> Option { + let src = self.ir.get(self.context.ir_file); + src.wrapper_name.as_ref().cloned() + } + + fn wrap_obj(&self, obj: String) -> String { + match self.wrapper_struct() { + Some(wrapper) => format!("{}({})", wrapper, obj), + None => obj, + } + } + + fn impl_intodart(&self) -> String { + let src = self.ir.get(self.context.ir_file); + + let unwrap = match &src.wrapper_name { + Some(_) => ".0", + None => "", + }; + let body = src + .fields + .iter() + .enumerate() + .map(|(i, field)| { + let field_ref = if src.is_fields_named { + field.name.rust_style().to_string() + } else { + i.to_string() + }; + let gen = TypeRustGenerator::new(field.ty.clone(), self.context.ir_file); + gen.convert_to_dart(gen.wrap_obj(format!("self{}.{}", unwrap, field_ref))) + }) + .collect::>() + .join(",\n"); + + let name = match &src.wrapper_name { + Some(wrapper) => wrapper, + None => &src.name, + }; + format!( + "impl support::IntoDart for {} {{ + fn into_dart(self) -> support::DartCObject {{ + vec![ + {} + ].into_dart() + }} + }} + impl support::IntoDartExceptPrimitive for {} {{}} + ", + name, body, name, + ) + } + + fn new_with_nullptr(&self, _collector: &mut ExternFuncCollector) -> String { + let src = self.ir.get(self.context.ir_file); + + let body = { + src.fields + .iter() + .map(|field| { + format!( + "{}: {},", + field.name.rust_style(), + if field.ty.rust_wire_is_pointer() { + "core::ptr::null_mut()" + } else { + "Default::default()" + } + ) + }) + .collect::>() + .join("\n") + }; + format!( + r#"impl NewWithNullPtr for {} {{ + fn new_with_null_ptr() -> Self {{ + Self {{ {} }} + }} + }} + "#, + self.ir.rust_wire_type(), + body, + ) + } + + fn imports(&self) -> Option { + let api_struct = self.ir.get(self.context.ir_file); + if api_struct.path.is_some() { + Some(format!( + "use {};", + api_struct.path.as_ref().unwrap().join("::") + )) + } else { + None + } + } +} diff --git a/libs/flutter_rust_bridge_codegen/src/ir/annotation.rs b/libs/flutter_rust_bridge_codegen/src/ir/annotation.rs new file mode 100644 index 000000000..e67580142 --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/ir/annotation.rs @@ -0,0 +1,7 @@ +use crate::ir::*; + +#[derive(Debug, Clone)] +pub struct IrDartAnnotation { + pub content: String, + pub library: Option, +} diff --git a/libs/flutter_rust_bridge_codegen/src/ir/comment.rs b/libs/flutter_rust_bridge_codegen/src/ir/comment.rs new file mode 100644 index 000000000..e91af1602 --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/ir/comment.rs @@ -0,0 +1,26 @@ +#[derive(Debug, Clone)] +pub struct IrComment(String); + +impl IrComment { + pub fn comment(&self) -> &str { + &self.0 + } +} + +impl From<&str> for IrComment { + fn from(input: &str) -> Self { + if input.contains('\n') { + // Dart's formatter has issues with block comments + // so we convert them ahead of time. + let formatted = input + .split('\n') + .into_iter() + .map(|e| format!("///{}", e)) + .collect::>() + .join("\n"); + Self(formatted) + } else { + Self(format!("///{}", input)) + } + } +} diff --git a/libs/flutter_rust_bridge_codegen/src/ir/field.rs b/libs/flutter_rust_bridge_codegen/src/ir/field.rs new file mode 100644 index 000000000..8c54bd54e --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/ir/field.rs @@ -0,0 +1,9 @@ +use crate::ir::*; + +#[derive(Debug, Clone)] +pub struct IrField { + pub ty: IrType, + pub name: IrIdent, + pub is_final: bool, + pub comments: Vec, +} diff --git a/libs/flutter_rust_bridge_codegen/src/ir/file.rs b/libs/flutter_rust_bridge_codegen/src/ir/file.rs new file mode 100644 index 000000000..cbfec6723 --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/ir/file.rs @@ -0,0 +1,61 @@ +use crate::ir::*; +use std::collections::{HashMap, HashSet}; + +pub type IrStructPool = HashMap; +pub type IrEnumPool = HashMap; + +#[derive(Debug, Clone)] +pub struct IrFile { + pub funcs: Vec, + pub struct_pool: IrStructPool, + pub enum_pool: IrEnumPool, + pub has_executor: bool, +} + +impl IrFile { + /// [f] returns [true] if it wants to stop going to the *children* of this subtree + pub fn visit_types bool>( + &self, + f: &mut F, + include_func_inputs: bool, + include_func_output: bool, + ) { + for func in &self.funcs { + if include_func_inputs { + for field in &func.inputs { + field.ty.visit_types(f, self); + } + } + if include_func_output { + func.output.visit_types(f, self); + } + } + } + + pub fn distinct_types( + &self, + include_func_inputs: bool, + include_func_output: bool, + ) -> Vec { + let mut seen_idents = HashSet::new(); + let mut ans = Vec::new(); + self.visit_types( + &mut |ty| { + let ident = ty.safe_ident(); + let contains = seen_idents.contains(&ident); + if !contains { + seen_idents.insert(ident); + ans.push(ty.clone()); + } + contains + }, + include_func_inputs, + include_func_output, + ); + + // make the output change less when input change + ans.sort_by_key(|ty| ty.safe_ident()); + + ans + } +} diff --git a/libs/flutter_rust_bridge_codegen/src/ir/func.rs b/libs/flutter_rust_bridge_codegen/src/ir/func.rs new file mode 100644 index 000000000..4bce1c42f --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/ir/func.rs @@ -0,0 +1,60 @@ +use crate::ir::*; + +#[derive(Debug, Clone)] +pub struct IrFunc { + pub name: String, + pub inputs: Vec, + pub output: IrType, + pub fallible: bool, + pub mode: IrFuncMode, + pub comments: Vec, +} + +impl IrFunc { + pub fn wire_func_name(&self) -> String { + format!("wire_{}", self.name) + } +} + +/// Represents a function's output type +#[derive(Debug, Clone)] +pub enum IrFuncOutput { + ResultType(IrType), + Type(IrType), +} + +/// Represents the type of an argument to a function +#[derive(Debug, Clone)] +pub enum IrFuncArg { + StreamSinkType(IrType), + Type(IrType), +} + +#[derive(Debug, Clone, PartialOrd, PartialEq)] +pub enum IrFuncMode { + Normal, + Sync, + Stream, +} + +impl IrFuncMode { + pub fn dart_return_type(&self, inner: &str) -> String { + match self { + Self::Normal => format!("Future<{}>", inner), + Self::Sync => inner.to_string(), + Self::Stream => format!("Stream<{}>", inner), + } + } + + pub fn ffi_call_mode(&self) -> &'static str { + match self { + Self::Normal => "Normal", + Self::Sync => "Sync", + Self::Stream => "Stream", + } + } + + pub fn has_port_argument(&self) -> bool { + self != &Self::Sync + } +} diff --git a/libs/flutter_rust_bridge_codegen/src/ir/ident.rs b/libs/flutter_rust_bridge_codegen/src/ir/ident.rs new file mode 100644 index 000000000..c86ac25fe --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/ir/ident.rs @@ -0,0 +1,26 @@ +use convert_case::{Case, Casing}; + +#[derive(Debug, Clone)] +pub struct IrIdent { + pub raw: String, +} + +impl std::fmt::Display for IrIdent { + fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + fmt.write_str(&self.raw) + } +} + +impl IrIdent { + pub fn new(raw: String) -> IrIdent { + IrIdent { raw } + } + + pub fn rust_style(&self) -> &str { + &self.raw + } + + pub fn dart_style(&self) -> String { + self.raw.to_case(Case::Camel) + } +} diff --git a/libs/flutter_rust_bridge_codegen/src/ir/import.rs b/libs/flutter_rust_bridge_codegen/src/ir/import.rs new file mode 100644 index 000000000..072975c35 --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/ir/import.rs @@ -0,0 +1,5 @@ +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub struct IrDartImport { + pub uri: String, + pub alias: Option, +} diff --git a/libs/flutter_rust_bridge_codegen/src/ir/mod.rs b/libs/flutter_rust_bridge_codegen/src/ir/mod.rs new file mode 100644 index 000000000..eb3c73c47 --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/ir/mod.rs @@ -0,0 +1,33 @@ +mod annotation; +mod comment; +mod field; +mod file; +mod func; +mod ident; +mod import; +mod ty; +mod ty_boxed; +mod ty_delegate; +mod ty_enum; +mod ty_general_list; +mod ty_optional; +mod ty_primitive; +mod ty_primitive_list; +mod ty_struct; + +pub use annotation::*; +pub use comment::*; +pub use field::*; +pub use file::*; +pub use func::*; +pub use ident::*; +pub use import::*; +pub use ty::*; +pub use ty_boxed::*; +pub use ty_delegate::*; +pub use ty_enum::*; +pub use ty_general_list::*; +pub use ty_optional::*; +pub use ty_primitive::*; +pub use ty_primitive_list::*; +pub use ty_struct::*; diff --git a/libs/flutter_rust_bridge_codegen/src/ir/ty.rs b/libs/flutter_rust_bridge_codegen/src/ir/ty.rs new file mode 100644 index 000000000..d342c54c7 --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/ir/ty.rs @@ -0,0 +1,84 @@ +use crate::ir::*; +use enum_dispatch::enum_dispatch; +use IrType::*; + +/// Remark: "Ty" instead of "Type", since "type" is a reserved word in Rust. +#[enum_dispatch(IrTypeTrait)] +#[derive(Debug, Clone)] +pub enum IrType { + Primitive(IrTypePrimitive), + Delegate(IrTypeDelegate), + PrimitiveList(IrTypePrimitiveList), + Optional(IrTypeOptional), + GeneralList(IrTypeGeneralList), + StructRef(IrTypeStructRef), + Boxed(IrTypeBoxed), + EnumRef(IrTypeEnumRef), +} + +impl IrType { + pub fn visit_types bool>(&self, f: &mut F, ir_file: &IrFile) { + if f(self) { + return; + } + + self.visit_children_types(f, ir_file); + } + + #[inline] + pub fn dart_required_modifier(&self) -> &'static str { + match self { + Optional(_) => "", + _ => "required ", + } + } + + /// Additional indirection for types put behind a vector + #[inline] + pub fn rust_ptr_modifier(&self) -> &'static str { + match self { + Optional(_) | Delegate(IrTypeDelegate::String) => "*mut ", + _ => "", + } + } +} + +#[enum_dispatch] +pub trait IrTypeTrait { + fn visit_children_types bool>(&self, f: &mut F, ir_file: &IrFile); + + fn safe_ident(&self) -> String; + + fn dart_api_type(&self) -> String; + + fn dart_wire_type(&self) -> String; + + fn rust_api_type(&self) -> String; + + fn rust_wire_type(&self) -> String; + + fn rust_wire_modifier(&self) -> String { + if self.rust_wire_is_pointer() { + "*mut ".to_string() + } else { + "".to_string() + } + } + + fn rust_wire_is_pointer(&self) -> bool { + false + } +} + +pub fn optional_boundary_index(types: &[&IrType]) -> Option { + types + .iter() + .enumerate() + .find(|ty| matches!(ty.1, Optional(_))) + .and_then(|(idx, _)| { + (&types[idx..]) + .iter() + .all(|ty| matches!(ty, Optional(_))) + .then(|| idx) + }) +} diff --git a/libs/flutter_rust_bridge_codegen/src/ir/ty_boxed.rs b/libs/flutter_rust_bridge_codegen/src/ir/ty_boxed.rs new file mode 100644 index 000000000..0ef2cddb0 --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/ir/ty_boxed.rs @@ -0,0 +1,56 @@ +use crate::ir::IrType::Primitive; +use crate::ir::*; + +#[derive(Debug, Clone)] +pub struct IrTypeBoxed { + /// if false, means that we automatically add it when transforming it - it does not exist in real api. + pub exist_in_real_api: bool, + pub inner: Box, +} + +impl IrTypeTrait for IrTypeBoxed { + fn visit_children_types bool>(&self, f: &mut F, ir_file: &IrFile) { + self.inner.visit_types(f, ir_file); + } + + fn safe_ident(&self) -> String { + format!( + "box_{}{}", + if self.exist_in_real_api { + "" + } else { + "autoadd_" + }, + self.inner.safe_ident() + ) + } + + fn dart_api_type(&self) -> String { + self.inner.dart_api_type() + } + + fn dart_wire_type(&self) -> String { + let wire_type = if let Primitive(prim) = &*self.inner { + prim.dart_native_type().to_owned() + } else { + self.inner.dart_wire_type() + }; + format!("ffi.Pointer<{}>", wire_type) + } + + fn rust_api_type(&self) -> String { + if self.exist_in_real_api { + format!("Box<{}>", self.inner.rust_api_type()) + } else { + self.inner.rust_api_type() + } + } + + fn rust_wire_type(&self) -> String { + self.inner.rust_wire_type() + } + + fn rust_wire_is_pointer(&self) -> bool { + true + } +} diff --git a/libs/flutter_rust_bridge_codegen/src/ir/ty_delegate.rs b/libs/flutter_rust_bridge_codegen/src/ir/ty_delegate.rs new file mode 100644 index 000000000..ac2574e4f --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/ir/ty_delegate.rs @@ -0,0 +1,85 @@ +use crate::ir::*; + +/// types that delegate to another type +#[derive(Debug, Clone)] +pub enum IrTypeDelegate { + String, + StringList, + SyncReturnVecU8, + ZeroCopyBufferVecPrimitive(IrTypePrimitive), +} + +impl IrTypeDelegate { + pub fn get_delegate(&self) -> IrType { + match self { + IrTypeDelegate::String => IrType::PrimitiveList(IrTypePrimitiveList { + primitive: IrTypePrimitive::U8, + }), + IrTypeDelegate::SyncReturnVecU8 => IrType::PrimitiveList(IrTypePrimitiveList { + primitive: IrTypePrimitive::U8, + }), + IrTypeDelegate::ZeroCopyBufferVecPrimitive(primitive) => { + IrType::PrimitiveList(IrTypePrimitiveList { + primitive: primitive.clone(), + }) + } + IrTypeDelegate::StringList => IrType::Delegate(IrTypeDelegate::String), + } + } +} + +impl IrTypeTrait for IrTypeDelegate { + fn visit_children_types bool>(&self, f: &mut F, ir_file: &IrFile) { + self.get_delegate().visit_types(f, ir_file); + } + + fn safe_ident(&self) -> String { + match self { + IrTypeDelegate::String => "String".to_owned(), + IrTypeDelegate::StringList => "StringList".to_owned(), + IrTypeDelegate::SyncReturnVecU8 => "SyncReturnVecU8".to_owned(), + IrTypeDelegate::ZeroCopyBufferVecPrimitive(_) => { + "ZeroCopyBuffer_".to_owned() + &self.get_delegate().dart_api_type() + } + } + } + + fn dart_api_type(&self) -> String { + match self { + IrTypeDelegate::String => "String".to_string(), + IrTypeDelegate::StringList => "List".to_owned(), + IrTypeDelegate::SyncReturnVecU8 | IrTypeDelegate::ZeroCopyBufferVecPrimitive(_) => { + self.get_delegate().dart_api_type() + } + } + } + + fn dart_wire_type(&self) -> String { + match self { + IrTypeDelegate::StringList => "ffi.Pointer".to_owned(), + _ => self.get_delegate().dart_wire_type(), + } + } + + fn rust_api_type(&self) -> String { + match self { + IrTypeDelegate::String => "String".to_owned(), + IrTypeDelegate::SyncReturnVecU8 => "SyncReturn>".to_string(), + IrTypeDelegate::StringList => "Vec".to_owned(), + IrTypeDelegate::ZeroCopyBufferVecPrimitive(_) => { + format!("ZeroCopyBuffer<{}>", self.get_delegate().rust_api_type()) + } + } + } + + fn rust_wire_type(&self) -> String { + match self { + IrTypeDelegate::StringList => "wire_StringList".to_owned(), + _ => self.get_delegate().rust_wire_type(), + } + } + + fn rust_wire_is_pointer(&self) -> bool { + self.get_delegate().rust_wire_is_pointer() + } +} diff --git a/libs/flutter_rust_bridge_codegen/src/ir/ty_enum.rs b/libs/flutter_rust_bridge_codegen/src/ir/ty_enum.rs new file mode 100644 index 000000000..bae45a692 --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/ir/ty_enum.rs @@ -0,0 +1,139 @@ +use crate::ir::IrType::{EnumRef, StructRef}; +use crate::ir::*; +use convert_case::{Case, Casing}; + +#[derive(Debug, Clone)] +pub struct IrTypeEnumRef { + pub name: String, + pub is_struct: bool, +} + +impl IrTypeEnumRef { + pub fn get<'a>(&self, file: &'a IrFile) -> &'a IrEnum { + &file.enum_pool[&self.name] + } +} + +impl IrTypeTrait for IrTypeEnumRef { + fn visit_children_types bool>(&self, f: &mut F, ir_file: &IrFile) { + let enu = self.get(ir_file); + for variant in enu.variants() { + if let IrVariantKind::Struct(st) = &variant.kind { + st.fields + .iter() + .for_each(|field| field.ty.visit_types(f, ir_file)); + } + } + } + + fn safe_ident(&self) -> String { + self.dart_api_type().to_case(Case::Snake) + } + fn dart_api_type(&self) -> String { + self.name.to_string() + } + fn dart_wire_type(&self) -> String { + if self.is_struct { + self.rust_wire_type() + } else { + "int".to_owned() + } + } + fn rust_api_type(&self) -> String { + self.name.to_string() + } + fn rust_wire_type(&self) -> String { + if self.is_struct { + format!("wire_{}", self.name) + } else { + "i32".to_owned() + } + } +} + +#[derive(Debug, Clone)] +pub struct IrEnum { + pub name: String, + pub wrapper_name: Option, + pub path: Vec, + pub comments: Vec, + _variants: Vec, + _is_struct: bool, +} + +impl IrEnum { + pub fn new( + name: String, + wrapper_name: Option, + path: Vec, + comments: Vec, + mut variants: Vec, + ) -> Self { + fn wrap_box(ty: IrType) -> IrType { + match ty { + StructRef(_) + | EnumRef(IrTypeEnumRef { + is_struct: true, .. + }) => IrType::Boxed(IrTypeBoxed { + exist_in_real_api: false, + inner: Box::new(ty), + }), + _ => ty, + } + } + let _is_struct = variants + .iter() + .any(|variant| !matches!(variant.kind, IrVariantKind::Value)); + if _is_struct { + variants = variants + .into_iter() + .map(|variant| IrVariant { + kind: match variant.kind { + IrVariantKind::Struct(st) => IrVariantKind::Struct(IrStruct { + fields: st + .fields + .into_iter() + .map(|field| IrField { + ty: wrap_box(field.ty), + ..field + }) + .collect(), + ..st + }), + _ => variant.kind, + }, + ..variant + }) + .collect::>(); + } + Self { + name, + wrapper_name, + path, + comments, + _variants: variants, + _is_struct, + } + } + + pub fn variants(&self) -> &[IrVariant] { + &self._variants + } + + pub fn is_struct(&self) -> bool { + self._is_struct + } +} + +#[derive(Debug, Clone)] +pub struct IrVariant { + pub name: IrIdent, + pub comments: Vec, + pub kind: IrVariantKind, +} + +#[derive(Debug, Clone)] +pub enum IrVariantKind { + Value, + Struct(IrStruct), +} diff --git a/libs/flutter_rust_bridge_codegen/src/ir/ty_general_list.rs b/libs/flutter_rust_bridge_codegen/src/ir/ty_general_list.rs new file mode 100644 index 000000000..44f5fde95 --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/ir/ty_general_list.rs @@ -0,0 +1,36 @@ +use crate::ir::*; + +#[derive(Debug, Clone)] +pub struct IrTypeGeneralList { + pub inner: Box, +} + +impl IrTypeTrait for IrTypeGeneralList { + fn visit_children_types bool>(&self, f: &mut F, ir_file: &IrFile) { + self.inner.visit_types(f, ir_file); + } + + fn safe_ident(&self) -> String { + format!("list_{}", self.inner.safe_ident()) + } + + fn dart_api_type(&self) -> String { + format!("List<{}>", self.inner.dart_api_type()) + } + + fn dart_wire_type(&self) -> String { + format!("ffi.Pointer", self.safe_ident()) + } + + fn rust_api_type(&self) -> String { + format!("Vec<{}>", self.inner.rust_api_type()) + } + + fn rust_wire_type(&self) -> String { + format!("wire_{}", self.safe_ident()) + } + + fn rust_wire_is_pointer(&self) -> bool { + true + } +} diff --git a/libs/flutter_rust_bridge_codegen/src/ir/ty_optional.rs b/libs/flutter_rust_bridge_codegen/src/ir/ty_optional.rs new file mode 100644 index 000000000..580788918 --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/ir/ty_optional.rs @@ -0,0 +1,65 @@ +use crate::ir::IrType::*; +use crate::ir::*; + +#[derive(Debug, Clone)] +pub struct IrTypeOptional { + pub inner: Box, +} + +impl IrTypeOptional { + pub fn new_prim(prim: IrTypePrimitive) -> Self { + Self { + inner: Box::new(Boxed(IrTypeBoxed { + inner: Box::new(Primitive(prim)), + exist_in_real_api: false, + })), + } + } + + pub fn new_ptr(ptr: IrType) -> Self { + Self { + inner: Box::new(ptr), + } + } + + pub fn is_primitive(&self) -> bool { + matches!(&*self.inner, Boxed(boxed) if matches!(*boxed.inner, IrType::Primitive(_))) + } + + pub fn is_list(&self) -> bool { + matches!(&*self.inner, GeneralList(_) | PrimitiveList(_)) + } + + pub fn is_delegate(&self) -> bool { + matches!(&*self.inner, Delegate(_)) + } + + pub fn needs_initialization(&self) -> bool { + !(self.is_primitive() || self.is_delegate()) + } +} + +impl IrTypeTrait for IrTypeOptional { + fn safe_ident(&self) -> String { + format!("opt_{}", self.inner.safe_ident()) + } + fn rust_wire_type(&self) -> String { + self.inner.rust_wire_type() + } + fn rust_api_type(&self) -> String { + format!("Option<{}>", self.inner.rust_api_type()) + } + fn dart_wire_type(&self) -> String { + self.inner.dart_wire_type() + } + fn dart_api_type(&self) -> String { + format!("{}?", self.inner.dart_api_type()) + } + fn rust_wire_is_pointer(&self) -> bool { + true + } + + fn visit_children_types bool>(&self, f: &mut F, ir_file: &IrFile) { + self.inner.visit_types(f, ir_file); + } +} diff --git a/libs/flutter_rust_bridge_codegen/src/ir/ty_primitive.rs b/libs/flutter_rust_bridge_codegen/src/ir/ty_primitive.rs new file mode 100644 index 000000000..06cfece9d --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/ir/ty_primitive.rs @@ -0,0 +1,114 @@ +use crate::ir::*; + +#[derive(Debug, Clone)] +pub enum IrTypePrimitive { + U8, + I8, + U16, + I16, + U32, + I32, + U64, + I64, + F32, + F64, + Bool, + Unit, + Usize, +} + +impl IrTypeTrait for IrTypePrimitive { + fn visit_children_types bool>(&self, _f: &mut F, _ir_file: &IrFile) {} + + fn safe_ident(&self) -> String { + self.rust_api_type() + } + + fn dart_api_type(&self) -> String { + match self { + IrTypePrimitive::U8 + | IrTypePrimitive::I8 + | IrTypePrimitive::U16 + | IrTypePrimitive::I16 + | IrTypePrimitive::U32 + | IrTypePrimitive::I32 + | IrTypePrimitive::U64 + | IrTypePrimitive::I64 + | IrTypePrimitive::Usize => "int", + IrTypePrimitive::F32 | IrTypePrimitive::F64 => "double", + IrTypePrimitive::Bool => "bool", + IrTypePrimitive::Unit => "void", + } + .to_string() + } + + fn dart_wire_type(&self) -> String { + match self { + IrTypePrimitive::Bool => "int".to_owned(), + _ => self.dart_api_type(), + } + } + + fn rust_api_type(&self) -> String { + self.rust_wire_type() + } + + fn rust_wire_type(&self) -> String { + match self { + IrTypePrimitive::U8 => "u8", + IrTypePrimitive::I8 => "i8", + IrTypePrimitive::U16 => "u16", + IrTypePrimitive::I16 => "i16", + IrTypePrimitive::U32 => "u32", + IrTypePrimitive::I32 => "i32", + IrTypePrimitive::U64 => "u64", + IrTypePrimitive::I64 => "i64", + IrTypePrimitive::F32 => "f32", + IrTypePrimitive::F64 => "f64", + IrTypePrimitive::Bool => "bool", + IrTypePrimitive::Unit => "unit", + IrTypePrimitive::Usize => "usize", + } + .to_string() + } +} + +impl IrTypePrimitive { + /// Representations of primitives within Dart's pointers, e.g. `ffi.Pointer`. + /// This is enforced on Dart's side, and should be used instead of `dart_wire_type` + /// whenever primitives are put behind a pointer. + pub fn dart_native_type(&self) -> &'static str { + match self { + IrTypePrimitive::U8 | IrTypePrimitive::Bool => "ffi.Uint8", + IrTypePrimitive::I8 => "ffi.Int8", + IrTypePrimitive::U16 => "ffi.Uint16", + IrTypePrimitive::I16 => "ffi.Int16", + IrTypePrimitive::U32 => "ffi.Uint32", + IrTypePrimitive::I32 => "ffi.Int32", + IrTypePrimitive::U64 => "ffi.Uint64", + IrTypePrimitive::I64 => "ffi.Int64", + IrTypePrimitive::F32 => "ffi.Float", + IrTypePrimitive::F64 => "ffi.Double", + IrTypePrimitive::Unit => "ffi.Void", + IrTypePrimitive::Usize => "ffi.Usize", + } + } + pub fn try_from_rust_str(s: &str) -> Option { + match s { + "u8" => Some(IrTypePrimitive::U8), + "i8" => Some(IrTypePrimitive::I8), + "u16" => Some(IrTypePrimitive::U16), + "i16" => Some(IrTypePrimitive::I16), + "u32" => Some(IrTypePrimitive::U32), + "i32" => Some(IrTypePrimitive::I32), + "u64" => Some(IrTypePrimitive::U64), + "i64" => Some(IrTypePrimitive::I64), + "f32" => Some(IrTypePrimitive::F32), + "f64" => Some(IrTypePrimitive::F64), + "bool" => Some(IrTypePrimitive::Bool), + "()" => Some(IrTypePrimitive::Unit), + "usize" => Some(IrTypePrimitive::Usize), + _ => None, + } + } +} diff --git a/libs/flutter_rust_bridge_codegen/src/ir/ty_primitive_list.rs b/libs/flutter_rust_bridge_codegen/src/ir/ty_primitive_list.rs new file mode 100644 index 000000000..759d29d71 --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/ir/ty_primitive_list.rs @@ -0,0 +1,50 @@ +use crate::ir::*; +use convert_case::{Case, Casing}; + +#[derive(Debug, Clone)] +pub struct IrTypePrimitiveList { + pub primitive: IrTypePrimitive, +} + +impl IrTypeTrait for IrTypePrimitiveList { + fn visit_children_types bool>(&self, f: &mut F, _ir_file: &IrFile) { + f(&IrType::Primitive(self.primitive.clone())); + } + + fn safe_ident(&self) -> String { + self.dart_api_type().to_case(Case::Snake) + } + + fn dart_api_type(&self) -> String { + match &self.primitive { + IrTypePrimitive::U8 => "Uint8List", + IrTypePrimitive::I8 => "Int8List", + IrTypePrimitive::U16 => "Uint16List", + IrTypePrimitive::I16 => "Int16List", + IrTypePrimitive::U32 => "Uint32List", + IrTypePrimitive::I32 => "Int32List", + IrTypePrimitive::U64 => "Uint64List", + IrTypePrimitive::I64 => "Int64List", + IrTypePrimitive::F32 => "Float32List", + IrTypePrimitive::F64 => "Float64List", + _ => panic!("does not support {:?} yet", &self.primitive), + } + .to_string() + } + + fn dart_wire_type(&self) -> String { + format!("ffi.Pointer", self.safe_ident()) + } + + fn rust_api_type(&self) -> String { + format!("Vec<{}>", self.primitive.rust_api_type()) + } + + fn rust_wire_type(&self) -> String { + format!("wire_{}", self.safe_ident()) + } + + fn rust_wire_is_pointer(&self) -> bool { + true + } +} diff --git a/libs/flutter_rust_bridge_codegen/src/ir/ty_struct.rs b/libs/flutter_rust_bridge_codegen/src/ir/ty_struct.rs new file mode 100644 index 000000000..3cacbd626 --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/ir/ty_struct.rs @@ -0,0 +1,66 @@ +use crate::ir::*; +use convert_case::{Case, Casing}; + +#[derive(Debug, Clone)] +pub struct IrTypeStructRef { + pub name: String, + pub freezed: bool, +} + +impl IrTypeStructRef { + pub fn get<'a>(&self, f: &'a IrFile) -> &'a IrStruct { + &f.struct_pool[&self.name] + } +} + +impl IrTypeTrait for IrTypeStructRef { + fn visit_children_types bool>(&self, f: &mut F, ir_file: &IrFile) { + for field in &self.get(ir_file).fields { + field.ty.visit_types(f, ir_file); + } + } + + fn safe_ident(&self) -> String { + self.dart_api_type().to_case(Case::Snake) + } + fn dart_api_type(&self) -> String { + self.name.to_string() + } + + fn dart_wire_type(&self) -> String { + self.rust_wire_type() + } + + fn rust_api_type(&self) -> String { + self.name.to_string() + } + + fn rust_wire_type(&self) -> String { + format!("wire_{}", self.name) + } +} + +#[derive(Debug, Clone)] +pub struct IrStruct { + pub name: String, + pub wrapper_name: Option, + pub path: Option>, + pub fields: Vec, + pub is_fields_named: bool, + pub dart_metadata: Vec, + pub comments: Vec, +} + +impl IrStruct { + pub fn brackets_pair(&self) -> (char, char) { + if self.is_fields_named { + ('{', '}') + } else { + ('(', ')') + } + } + + pub fn using_freezed(&self) -> bool { + self.dart_metadata.iter().any(|it| it.content == "freezed") + } +} diff --git a/libs/flutter_rust_bridge_codegen/src/lib.rs b/libs/flutter_rust_bridge_codegen/src/lib.rs new file mode 100644 index 000000000..0eaa76529 --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/lib.rs @@ -0,0 +1,183 @@ +use std::fs; +use std::path::Path; + +use log::info; +use pathdiff::diff_paths; + +use crate::commands::ensure_tools_available; +pub use crate::config::RawOpts as Opts; +use crate::ir::*; +use crate::others::*; +use crate::utils::*; + +mod commands; +mod config; +mod error; +mod generator; +mod ir; +mod markers; +mod others; +mod parser; +mod source_graph; +mod transformer; +mod utils; +use error::*; + +pub fn frb_codegen(raw_opts: Opts) -> anyhow::Result<()> { + ensure_tools_available()?; + + let config = config::parse(raw_opts); + info!("Picked config: {:?}", &config); + + let rust_output_dir = Path::new(&config.rust_output_path).parent().unwrap(); + let dart_output_dir = Path::new(&config.dart_output_path).parent().unwrap(); + + info!("Phase: Parse source code to AST"); + let source_rust_content = fs::read_to_string(&config.rust_input_path)?; + let file_ast = syn::parse_file(&source_rust_content)?; + + info!("Phase: Parse AST to IR"); + let raw_ir_file = parser::parse(&source_rust_content, file_ast, &config.manifest_path); + + info!("Phase: Transform IR"); + let ir_file = transformer::transform(raw_ir_file); + + info!("Phase: Generate Rust code"); + let generated_rust = generator::rust::generate( + &ir_file, + &mod_from_rust_path(&config.rust_input_path, &config.rust_crate_dir), + ); + fs::create_dir_all(&rust_output_dir)?; + fs::write(&config.rust_output_path, generated_rust.code)?; + + info!("Phase: Generate Dart code"); + let (generated_dart, needs_freezed) = generator::dart::generate( + &ir_file, + &config.dart_api_class_name(), + &config.dart_api_impl_class_name(), + &config.dart_wire_class_name(), + config + .dart_output_path_name() + .ok_or_else(|| Error::str("Invalid dart_output_path_name"))?, + ); + + info!("Phase: Other things"); + + commands::format_rust(&config.rust_output_path)?; + + if !config.skip_add_mod_to_lib { + others::try_add_mod_to_lib(&config.rust_crate_dir, &config.rust_output_path); + } + + let c_struct_names = ir_file + .distinct_types(true, true) + .iter() + .filter_map(|ty| { + if let IrType::StructRef(_) = ty { + Some(ty.rust_wire_type()) + } else { + None + } + }) + .collect(); + + let temp_dart_wire_file = tempfile::NamedTempFile::new()?; + let temp_bindgen_c_output_file = tempfile::Builder::new().suffix(".h").tempfile()?; + with_changed_file( + &config.rust_output_path, + DUMMY_WIRE_CODE_FOR_BINDGEN, + || { + commands::bindgen_rust_to_dart( + &config.rust_crate_dir, + temp_bindgen_c_output_file + .path() + .as_os_str() + .to_str() + .unwrap(), + temp_dart_wire_file.path().as_os_str().to_str().unwrap(), + &config.dart_wire_class_name(), + c_struct_names, + &config.llvm_path[..], + &config.llvm_compiler_opts, + ) + }, + )?; + + let effective_func_names = [ + generated_rust.extern_func_names, + EXTRA_EXTERN_FUNC_NAMES.to_vec(), + ] + .concat(); + let c_dummy_code = generator::c::generate_dummy(&effective_func_names); + for output in &config.c_output_path { + fs::create_dir_all(Path::new(output).parent().unwrap())?; + fs::write( + &output, + fs::read_to_string(&temp_bindgen_c_output_file)? + "\n" + &c_dummy_code, + )?; + } + + fs::create_dir_all(&dart_output_dir)?; + let generated_dart_wire_code_raw = fs::read_to_string(temp_dart_wire_file)?; + let generated_dart_wire = extract_dart_wire_content(&modify_dart_wire_content( + &generated_dart_wire_code_raw, + &config.dart_wire_class_name(), + )); + + sanity_check(&generated_dart_wire.body, &config.dart_wire_class_name())?; + + let generated_dart_decl_all = generated_dart.decl_code; + let generated_dart_impl_all = &generated_dart.impl_code + &generated_dart_wire; + if let Some(dart_decl_output_path) = &config.dart_decl_output_path { + let impl_import_decl = DartBasicCode { + import: format!( + "import \"{}\";", + diff_paths(dart_decl_output_path, dart_output_dir) + .unwrap() + .to_str() + .unwrap() + ), + part: String::new(), + body: String::new(), + }; + fs::write( + &dart_decl_output_path, + (&generated_dart.file_prelude + &generated_dart_decl_all).to_text(), + )?; + fs::write( + &config.dart_output_path, + (&generated_dart.file_prelude + &impl_import_decl + &generated_dart_impl_all).to_text(), + )?; + } else { + fs::write( + &config.dart_output_path, + (&generated_dart.file_prelude + &generated_dart_decl_all + &generated_dart_impl_all) + .to_text(), + )?; + } + + let dart_root = &config.dart_root; + if needs_freezed && config.build_runner { + let dart_root = dart_root.as_ref().ok_or_else(|| { + Error::str( + "build_runner configured to run, but Dart root could not be inferred. + Please specify --dart-root, or disable build_runner with --no-build-runner.", + ) + })?; + commands::build_runner(dart_root)?; + commands::format_dart( + &config + .dart_output_freezed_path() + .ok_or_else(|| Error::str("Invalid freezed file path"))?, + config.dart_format_line_length, + )?; + } + + commands::format_dart(&config.dart_output_path, config.dart_format_line_length)?; + if let Some(dart_decl_output_path) = &config.dart_decl_output_path { + commands::format_dart(dart_decl_output_path, config.dart_format_line_length)?; + } + + info!("Success!"); + Ok(()) +} diff --git a/libs/flutter_rust_bridge_codegen/src/main.rs b/libs/flutter_rust_bridge_codegen/src/main.rs new file mode 100644 index 000000000..986ca3215 --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/main.rs @@ -0,0 +1,19 @@ +use env_logger::Env; +use log::info; +use structopt::StructOpt; + +use lib_flutter_rust_bridge_codegen::{frb_codegen, Opts}; + +fn main() { + let opts = Opts::from_args(); + env_logger::Builder::from_env(Env::default().default_filter_or(if opts.verbose { + "debug" + } else { + "info" + })) + .init(); + + frb_codegen(opts).unwrap(); + + info!("Now go and use it :)"); +} diff --git a/libs/flutter_rust_bridge_codegen/src/markers.rs b/libs/flutter_rust_bridge_codegen/src/markers.rs new file mode 100644 index 000000000..048cb77db --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/markers.rs @@ -0,0 +1,39 @@ +use syn::*; + +/// Extract a path from marker `#[frb(mirror(path), ..)]` +pub fn extract_mirror_marker(attrs: &[Attribute]) -> Option { + attrs + .iter() + .filter(|attr| attr.path.is_ident("frb")) + .find_map(|attr| match attr.parse_meta() { + Ok(Meta::List(MetaList { nested, .. })) => nested.iter().find_map(|meta| match meta { + NestedMeta::Meta(Meta::List(MetaList { + path, + nested: mirror, + .. + })) if path.is_ident("mirror") && mirror.len() == 1 => { + match mirror.first().unwrap() { + NestedMeta::Meta(Meta::Path(path)) => Some(path.clone()), + _ => None, + } + } + _ => None, + }), + _ => None, + }) +} + +/// Checks if the `#[frb(non_final)]` attribute is present. +pub fn has_non_final(attrs: &[Attribute]) -> bool { + attrs + .iter() + .filter(|attr| attr.path.is_ident("frb")) + .any(|attr| { + match attr.parse_meta() { + Ok(Meta::List(MetaList { nested, .. })) => nested.iter().any(|meta| { + matches!(meta, NestedMeta::Meta(Meta::Path(path)) if path.is_ident("non_final")) + }), + _ => false, + } + }) +} diff --git a/libs/flutter_rust_bridge_codegen/src/others.rs b/libs/flutter_rust_bridge_codegen/src/others.rs new file mode 100644 index 000000000..4a8d10c8f --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/others.rs @@ -0,0 +1,169 @@ +use std::fs; +use std::ops::Add; +use std::path::Path; + +use anyhow::{anyhow, Result}; +use lazy_static::lazy_static; +use log::{info, warn}; +use pathdiff::diff_paths; +use regex::RegexBuilder; + +// NOTE [DartPostCObjectFnType] was originally [*mut DartCObject] but I changed it to [*mut c_void] +// because cannot automatically generate things related to [DartCObject]. Anyway this works fine. +// NOTE please sync [DUMMY_WIRE_CODE_FOR_BINDGEN] and [EXTRA_EXTERN_FUNC_NAMES] +pub const DUMMY_WIRE_CODE_FOR_BINDGEN: &str = r#" + // ----------- DUMMY CODE FOR BINDGEN ---------- + + // copied from: allo-isolate + pub type DartPort = i64; + pub type DartPostCObjectFnType = unsafe extern "C" fn(port_id: DartPort, message: *mut std::ffi::c_void) -> bool; + #[no_mangle] pub unsafe extern "C" fn store_dart_post_cobject(ptr: DartPostCObjectFnType) { panic!("dummy code") } + + // copied from: frb_rust::support.rs + #[repr(C)] + pub struct WireSyncReturnStruct { + pub ptr: *mut u8, + pub len: i32, + pub success: bool, + } + + // --------------------------------------------- + "#; + +lazy_static! { + pub static ref EXTRA_EXTERN_FUNC_NAMES: Vec = + vec!["store_dart_post_cobject".to_string()]; +} + +pub const CODE_HEADER: &str = "// AUTO GENERATED FILE, DO NOT EDIT. +// Generated by `flutter_rust_bridge`."; + +pub fn modify_dart_wire_content(content_raw: &str, dart_wire_class_name: &str) -> String { + let content = content_raw.replace( + &format!("class {} {{", dart_wire_class_name), + &format!( + "class {} implements FlutterRustBridgeWireBase {{", + dart_wire_class_name + ), + ); + + let content = RegexBuilder::new("class WireSyncReturnStruct extends ffi.Struct \\{.+?\\}") + .multi_line(true) + .dot_matches_new_line(true) + .build() + .unwrap() + .replace(&content, ""); + + content.to_string() +} + +#[derive(Default)] +pub struct DartBasicCode { + pub import: String, + pub part: String, + pub body: String, +} + +impl Add for &DartBasicCode { + type Output = DartBasicCode; + + fn add(self, rhs: Self) -> Self::Output { + DartBasicCode { + import: format!("{}\n{}", self.import, rhs.import), + part: format!("{}\n{}", self.part, rhs.part), + body: format!("{}\n{}", self.body, rhs.body), + } + } +} + +impl Add<&DartBasicCode> for DartBasicCode { + type Output = DartBasicCode; + + fn add(self, rhs: &DartBasicCode) -> Self::Output { + (&self).add(rhs) + } +} + +impl DartBasicCode { + pub fn to_text(&self) -> String { + format!("{}\n{}\n{}", self.import, self.part, self.body) + } +} + +pub fn extract_dart_wire_content(content: &str) -> DartBasicCode { + let (mut imports, mut body) = (Vec::new(), Vec::new()); + for line in content.split('\n') { + (if line.starts_with("import ") { + &mut imports + } else { + &mut body + }) + .push(line); + } + DartBasicCode { + import: imports.join("\n"), + part: "".to_string(), + body: body.join("\n"), + } +} + +pub fn sanity_check( + generated_dart_wire_code: &str, + dart_wire_class_name: &str, +) -> anyhow::Result<()> { + if !generated_dart_wire_code.contains(dart_wire_class_name) { + return Err(crate::error::Error::str( + "Nothing is generated for dart wire class. \ + Maybe you forget to put code like `mod the_generated_bridge_code;` to your `lib.rs`?", + ) + .into()); + } + Ok(()) +} + +pub fn try_add_mod_to_lib(rust_crate_dir: &str, rust_output_path: &str) { + if let Err(e) = auto_add_mod_to_lib_core(rust_crate_dir, rust_output_path) { + warn!( + "auto_add_mod_to_lib fail, the generated code may or may not have problems. \ + Please ensure you have add code like `mod the_generated_bridge_code;` to your `lib.rs`. \ + Details: {}", + e + ); + } +} + +pub fn auto_add_mod_to_lib_core(rust_crate_dir: &str, rust_output_path: &str) -> Result<()> { + let path_src_folder = Path::new(rust_crate_dir).join("src"); + let rust_output_path_relative_to_src_folder = + diff_paths(rust_output_path, path_src_folder.clone()).ok_or_else(|| { + anyhow!( + "rust_output_path={} is unrelated to path_src_folder={:?}", + rust_output_path, + &path_src_folder, + ) + })?; + + let mod_name = rust_output_path_relative_to_src_folder + .file_stem() + .ok_or_else(|| anyhow!(""))? + .to_str() + .ok_or_else(|| anyhow!(""))? + .to_string() + .replace('/', "::"); + let expect_code = format!("mod {};", mod_name); + + let path_lib_rs = path_src_folder.join("lib.rs"); + + let raw_content_lib_rs = fs::read_to_string(path_lib_rs.clone())?; + if !raw_content_lib_rs.contains(&expect_code) { + info!("Inject `{}` into {:?}", &expect_code, &path_lib_rs); + + let comments = " /* AUTO INJECTED BY flutter_rust_bridge. This line may not be accurate, and you can change it according to your needs. */"; + let modified_content_lib_rs = + format!("{}{}\n{}", expect_code, comments, raw_content_lib_rs); + + fs::write(&path_lib_rs, modified_content_lib_rs).unwrap(); + } + + Ok(()) +} diff --git a/libs/flutter_rust_bridge_codegen/src/parser/mod.rs b/libs/flutter_rust_bridge_codegen/src/parser/mod.rs new file mode 100644 index 000000000..16de7dd8e --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/parser/mod.rs @@ -0,0 +1,353 @@ +mod ty; + +use std::string::String; + +use log::debug; +use quote::quote; +use syn::parse::{Parse, ParseStream}; +use syn::punctuated::Punctuated; +use syn::*; + +use crate::ir::*; + +use crate::generator::rust::HANDLER_NAME; +use crate::parser::ty::TypeParser; +use crate::source_graph::Crate; + +const STREAM_SINK_IDENT: &str = "StreamSink"; +const RESULT_IDENT: &str = "Result"; + +pub fn parse(source_rust_content: &str, file: File, manifest_path: &str) -> IrFile { + let crate_map = Crate::new(manifest_path); + + let src_fns = extract_fns_from_file(&file); + let src_structs = crate_map.root_module.collect_structs_to_vec(); + let src_enums = crate_map.root_module.collect_enums_to_vec(); + + let parser = Parser::new(TypeParser::new(src_structs, src_enums)); + parser.parse(source_rust_content, src_fns) +} + +struct Parser<'a> { + type_parser: TypeParser<'a>, +} + +impl<'a> Parser<'a> { + pub fn new(type_parser: TypeParser<'a>) -> Self { + Parser { type_parser } + } +} + +impl<'a> Parser<'a> { + fn parse(mut self, source_rust_content: &str, src_fns: Vec<&ItemFn>) -> IrFile { + let funcs = src_fns.iter().map(|f| self.parse_function(f)).collect(); + + let has_executor = source_rust_content.contains(HANDLER_NAME); + + let (struct_pool, enum_pool) = self.type_parser.consume(); + + IrFile { + funcs, + struct_pool, + enum_pool, + has_executor, + } + } + + /// Attempts to parse the type from the return part of a function signature. There is a special + /// case for top-level `Result` types. + pub fn try_parse_fn_output_type(&mut self, ty: &syn::Type) -> Option { + let inner = ty::SupportedInnerType::try_from_syn_type(ty)?; + + match inner { + ty::SupportedInnerType::Path(ty::SupportedPathType { + ident, + generic: Some(generic), + }) if ident == RESULT_IDENT => Some(IrFuncOutput::ResultType( + self.type_parser.convert_to_ir_type(*generic)?, + )), + _ => Some(IrFuncOutput::Type( + self.type_parser.convert_to_ir_type(inner)?, + )), + } + } + + /// Attempts to parse the type from an argument of a function signature. There is a special + /// case for top-level `StreamSink` types. + pub fn try_parse_fn_arg_type(&mut self, ty: &syn::Type) -> Option { + match ty { + syn::Type::Path(syn::TypePath { path, .. }) => { + let last_segment = path.segments.last().unwrap(); + if last_segment.ident == STREAM_SINK_IDENT { + match &last_segment.arguments { + syn::PathArguments::AngleBracketed( + syn::AngleBracketedGenericArguments { args, .. }, + ) if args.len() == 1 => { + // Unwrap is safe here because args.len() == 1 + match args.last().unwrap() { + syn::GenericArgument::Type(t) => { + Some(IrFuncArg::StreamSinkType(self.type_parser.parse_type(t))) + } + _ => None, + } + } + _ => None, + } + } else { + Some(IrFuncArg::Type(self.type_parser.parse_type(ty))) + } + } + _ => None, + } + } + + fn parse_function(&mut self, func: &ItemFn) -> IrFunc { + debug!("parse_function function name: {:?}", func.sig.ident); + + let sig = &func.sig; + let func_name = sig.ident.to_string(); + + let mut inputs = Vec::new(); + let mut output = None; + let mut mode = None; + let mut fallible = true; + + for sig_input in &sig.inputs { + if let FnArg::Typed(ref pat_type) = sig_input { + let name = if let Pat::Ident(ref pat_ident) = *pat_type.pat { + format!("{}", pat_ident.ident) + } else { + panic!("unexpected pat_type={:?}", pat_type) + }; + + match self.try_parse_fn_arg_type(&pat_type.ty).unwrap_or_else(|| { + panic!( + "Failed to parse function argument type `{}`", + type_to_string(&pat_type.ty) + ) + }) { + IrFuncArg::StreamSinkType(ty) => { + output = Some(ty); + mode = Some(IrFuncMode::Stream); + } + IrFuncArg::Type(ty) => { + inputs.push(IrField { + name: IrIdent::new(name), + ty, + is_final: true, + comments: extract_comments(&pat_type.attrs), + }); + } + } + } else { + panic!("unexpected sig_input={:?}", sig_input); + } + } + + if output.is_none() { + output = Some(match &sig.output { + ReturnType::Type(_, ty) => { + match self.try_parse_fn_output_type(ty).unwrap_or_else(|| { + panic!( + "Failed to parse function output type `{}`", + type_to_string(ty) + ) + }) { + IrFuncOutput::ResultType(ty) => ty, + IrFuncOutput::Type(ty) => { + fallible = false; + ty + } + } + } + ReturnType::Default => { + fallible = false; + IrType::Primitive(IrTypePrimitive::Unit) + } + }); + mode = Some( + if let Some(IrType::Delegate(IrTypeDelegate::SyncReturnVecU8)) = output { + IrFuncMode::Sync + } else { + IrFuncMode::Normal + }, + ); + } + + // let comments = func.attrs.iter().filter_map(extract_comments).collect(); + + IrFunc { + name: func_name, + inputs, + output: output.expect("unsupported output"), + fallible, + mode: mode.expect("unsupported mode"), + comments: extract_comments(&func.attrs), + } + } +} + +fn extract_fns_from_file(file: &File) -> Vec<&ItemFn> { + let mut src_fns = Vec::new(); + + for item in file.items.iter() { + if let Item::Fn(ref item_fn) = item { + if let Visibility::Public(_) = &item_fn.vis { + src_fns.push(item_fn); + } + } + } + + src_fns +} + +fn extract_comments(attrs: &[Attribute]) -> Vec { + attrs + .iter() + .filter_map(|attr| match attr.parse_meta() { + Ok(Meta::NameValue(MetaNameValue { + path, + lit: Lit::Str(lit), + .. + })) if path.is_ident("doc") => Some(IrComment::from(lit.value().as_ref())), + _ => None, + }) + .collect() +} + +pub mod frb_keyword { + syn::custom_keyword!(mirror); + syn::custom_keyword!(non_final); + syn::custom_keyword!(dart_metadata); + syn::custom_keyword!(import); +} + +#[derive(Clone, Debug)] +pub struct NamedOption { + pub name: K, + pub value: V, +} + +impl Parse for NamedOption { + fn parse(input: ParseStream<'_>) -> Result { + let name: K = input.parse()?; + let _: Token![=] = input.parse()?; + let value = input.parse()?; + Ok(Self { name, value }) + } +} + +#[derive(Clone, Debug)] +pub struct MirrorOption(Path); + +impl Parse for MirrorOption { + fn parse(input: ParseStream<'_>) -> Result { + let content; + parenthesized!(content in input); + let path: Path = content.parse()?; + Ok(Self(path)) + } +} + +#[derive(Clone, Debug)] +pub struct MetadataAnnotations(Vec); + +impl Parse for IrDartAnnotation { + fn parse(input: ParseStream<'_>) -> Result { + let annotation: LitStr = input.parse()?; + let library = if input.peek(frb_keyword::import) { + let _ = input.parse::()?; + let library: IrDartImport = input.parse()?; + Some(library) + } else { + None + }; + Ok(Self { + content: annotation.value(), + library, + }) + } +} +impl Parse for MetadataAnnotations { + fn parse(input: ParseStream<'_>) -> Result { + let content; + parenthesized!(content in input); + let annotations = + Punctuated::::parse_terminated(&content)? + .into_iter() + .collect(); + Ok(Self(annotations)) + } +} + +#[derive(Clone, Debug)] +pub struct DartImports(Vec); + +impl Parse for IrDartImport { + fn parse(input: ParseStream<'_>) -> Result { + let uri: LitStr = input.parse()?; + let alias: Option = if input.peek(token::As) { + let _ = input.parse::()?; + let alias: Ident = input.parse()?; + Some(alias.to_string()) + } else { + None + }; + Ok(Self { + uri: uri.value(), + alias, + }) + } +} +impl Parse for DartImports { + fn parse(input: ParseStream<'_>) -> Result { + let content; + parenthesized!(content in input); + let imports = Punctuated::::parse_terminated(&content)? + .into_iter() + .collect(); + Ok(Self(imports)) + } +} + +enum FrbOption { + Mirror(MirrorOption), + NonFinal, + Metadata(NamedOption), +} + +impl Parse for FrbOption { + fn parse(input: ParseStream<'_>) -> Result { + let lookahead = input.lookahead1(); + if lookahead.peek(frb_keyword::mirror) { + input.parse().map(FrbOption::Mirror) + } else if lookahead.peek(frb_keyword::non_final) { + input + .parse::() + .map(|_| FrbOption::NonFinal) + } else if lookahead.peek(frb_keyword::dart_metadata) { + input.parse().map(FrbOption::Metadata) + } else { + Err(lookahead.error()) + } + } +} +fn extract_metadata(attrs: &[Attribute]) -> Vec { + attrs + .iter() + .filter(|attr| attr.path.is_ident("frb")) + .map(|attr| attr.parse_args::()) + .flat_map(|frb_option| match frb_option { + Ok(FrbOption::Metadata(NamedOption { + name: _, + value: MetadataAnnotations(annotations), + })) => annotations, + _ => vec![], + }) + .collect() +} + +/// syn -> string https://github.com/dtolnay/syn/issues/294 +fn type_to_string(ty: &Type) -> String { + quote!(#ty).to_string().replace(' ', "") +} diff --git a/libs/flutter_rust_bridge_codegen/src/parser/ty.rs b/libs/flutter_rust_bridge_codegen/src/parser/ty.rs new file mode 100644 index 000000000..15cbbce43 --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/parser/ty.rs @@ -0,0 +1,392 @@ +use std::collections::{HashMap, HashSet}; +use std::string::String; + +use syn::*; + +use crate::ir::IrType::*; +use crate::ir::*; + +use crate::markers; + +use crate::source_graph::{Enum, Struct}; + +use crate::parser::{extract_comments, extract_metadata, type_to_string}; + +pub struct TypeParser<'a> { + src_structs: HashMap, + src_enums: HashMap, + + parsing_or_parsed_struct_names: HashSet, + struct_pool: IrStructPool, + + parsed_enums: HashSet, + enum_pool: IrEnumPool, +} + +impl<'a> TypeParser<'a> { + pub fn new( + src_structs: HashMap, + src_enums: HashMap, + ) -> Self { + TypeParser { + src_structs, + src_enums, + struct_pool: HashMap::new(), + enum_pool: HashMap::new(), + parsing_or_parsed_struct_names: HashSet::new(), + parsed_enums: HashSet::new(), + } + } + + pub fn consume(self) -> (IrStructPool, IrEnumPool) { + (self.struct_pool, self.enum_pool) + } +} + +/// Generic intermediate representation of a type that can appear inside a function signature. +#[derive(Debug)] +pub enum SupportedInnerType { + /// Path types with up to 1 generic type argument on the final segment. All segments before + /// the last segment are ignored. The generic type argument must also be a valid + /// `SupportedInnerType`. + Path(SupportedPathType), + /// Array type + Array(Box, usize), + /// The unit type `()`. + Unit, +} + +impl std::fmt::Display for SupportedInnerType { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Self::Path(p) => write!(f, "{}", p), + Self::Array(u, len) => write!(f, "[{}; {}]", u, len), + Self::Unit => write!(f, "()"), + } + } +} + +/// Represents a named type, with an optional path and up to 1 generic type argument. +#[derive(Debug)] +pub struct SupportedPathType { + pub ident: syn::Ident, + pub generic: Option>, +} + +impl std::fmt::Display for SupportedPathType { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + let ident = self.ident.to_string(); + if let Some(generic) = &self.generic { + write!(f, "{}<{}>", ident, generic) + } else { + write!(f, "{}", ident) + } + } +} + +impl SupportedInnerType { + /// Given a `syn::Type`, returns a simplified representation of the type if it's supported, + /// or `None` otherwise. + pub fn try_from_syn_type(ty: &syn::Type) -> Option { + match ty { + syn::Type::Path(syn::TypePath { path, .. }) => { + let last_segment = path.segments.last().unwrap().clone(); + match last_segment.arguments { + syn::PathArguments::None => Some(SupportedInnerType::Path(SupportedPathType { + ident: last_segment.ident, + generic: None, + })), + syn::PathArguments::AngleBracketed(a) => { + let generic = match a.args.into_iter().next() { + Some(syn::GenericArgument::Type(t)) => { + Some(Box::new(SupportedInnerType::try_from_syn_type(&t)?)) + } + _ => None, + }; + + Some(SupportedInnerType::Path(SupportedPathType { + ident: last_segment.ident, + generic, + })) + } + _ => None, + } + } + syn::Type::Array(syn::TypeArray { elem, len, .. }) => { + let len: usize = match len { + syn::Expr::Lit(lit) => match &lit.lit { + syn::Lit::Int(x) => x.base10_parse().unwrap(), + _ => panic!("Cannot parse array length"), + }, + _ => panic!("Cannot parse array length"), + }; + Some(SupportedInnerType::Array( + Box::new(SupportedInnerType::try_from_syn_type(elem)?), + len, + )) + } + syn::Type::Tuple(syn::TypeTuple { elems, .. }) if elems.is_empty() => { + Some(SupportedInnerType::Unit) + } + _ => None, + } + } +} + +impl<'a> TypeParser<'a> { + pub fn parse_type(&mut self, ty: &syn::Type) -> IrType { + let supported_type = SupportedInnerType::try_from_syn_type(ty) + .unwrap_or_else(|| panic!("Unsupported type `{}`", type_to_string(ty))); + + self.convert_to_ir_type(supported_type) + .unwrap_or_else(|| panic!("parse_type failed for ty={}", type_to_string(ty))) + } + + /// Converts an inner type into an `IrType` if possible. + pub fn convert_to_ir_type(&mut self, ty: SupportedInnerType) -> Option { + match ty { + SupportedInnerType::Path(p) => self.convert_path_to_ir_type(p), + SupportedInnerType::Array(p, len) => self.convert_array_to_ir_type(*p, len), + SupportedInnerType::Unit => Some(IrType::Primitive(IrTypePrimitive::Unit)), + } + } + + /// Converts an array type into an `IrType` if possible. + pub fn convert_array_to_ir_type( + &mut self, + generic: SupportedInnerType, + _len: usize, + ) -> Option { + self.convert_to_ir_type(generic).map(|inner| match inner { + Primitive(primitive) => PrimitiveList(IrTypePrimitiveList { primitive }), + others => GeneralList(IrTypeGeneralList { + inner: Box::new(others), + }), + }) + } + + /// Converts a path type into an `IrType` if possible. + pub fn convert_path_to_ir_type(&mut self, p: SupportedPathType) -> Option { + let p_as_str = format!("{}", &p); + let ident_string = &p.ident.to_string(); + if let Some(generic) = p.generic { + match ident_string.as_str() { + "SyncReturn" => { + // Special-case SyncReturn>. SyncReturn for any other type is not + // supported. + match *generic { + SupportedInnerType::Path(SupportedPathType { + ident, + generic: Some(generic), + }) if ident == "Vec" => match *generic { + SupportedInnerType::Path(SupportedPathType { + ident, + generic: None, + }) if ident == "u8" => { + Some(IrType::Delegate(IrTypeDelegate::SyncReturnVecU8)) + } + _ => None, + }, + _ => None, + } + } + "Vec" => { + // Special-case Vec as StringList + if matches!(*generic, SupportedInnerType::Path(SupportedPathType { ref ident, .. }) if ident == "String") + { + Some(IrType::Delegate(IrTypeDelegate::StringList)) + } else { + self.convert_to_ir_type(*generic).map(|inner| match inner { + Primitive(primitive) => { + PrimitiveList(IrTypePrimitiveList { primitive }) + } + others => GeneralList(IrTypeGeneralList { + inner: Box::new(others), + }), + }) + } + } + "ZeroCopyBuffer" => { + let inner = self.convert_to_ir_type(*generic); + if let Some(IrType::PrimitiveList(IrTypePrimitiveList { primitive })) = inner { + Some(IrType::Delegate( + IrTypeDelegate::ZeroCopyBufferVecPrimitive(primitive), + )) + } else { + None + } + } + "Box" => self.convert_to_ir_type(*generic).map(|inner| { + Boxed(IrTypeBoxed { + exist_in_real_api: true, + inner: Box::new(inner), + }) + }), + "Option" => { + // Disallow nested Option + if matches!(*generic, SupportedInnerType::Path(SupportedPathType { ref ident, .. }) if ident == "Option") + { + panic!( + "Nested optionals without indirection are not supported. (Option>)", + p_as_str + ); + } + self.convert_to_ir_type(*generic).map(|inner| match inner { + Primitive(prim) => IrType::Optional(IrTypeOptional::new_prim(prim)), + st @ StructRef(_) => { + IrType::Optional(IrTypeOptional::new_ptr(Boxed(IrTypeBoxed { + inner: Box::new(st), + exist_in_real_api: false, + }))) + } + other => IrType::Optional(IrTypeOptional::new_ptr(other)), + }) + } + _ => None, + } + } else { + IrTypePrimitive::try_from_rust_str(ident_string) + .map(Primitive) + .or_else(|| { + if ident_string == "String" { + Some(IrType::Delegate(IrTypeDelegate::String)) + } else if self.src_structs.contains_key(ident_string) { + if !self.parsing_or_parsed_struct_names.contains(ident_string) { + self.parsing_or_parsed_struct_names + .insert(ident_string.to_owned()); + let api_struct = self.parse_struct_core(&p.ident); + self.struct_pool.insert(ident_string.to_owned(), api_struct); + } + + Some(StructRef(IrTypeStructRef { + name: ident_string.to_owned(), + freezed: self + .struct_pool + .get(ident_string) + .map(IrStruct::using_freezed) + .unwrap_or(false), + })) + } else if self.src_enums.contains_key(ident_string) { + if self.parsed_enums.insert(ident_string.to_owned()) { + let enu = self.parse_enum_core(&p.ident); + self.enum_pool.insert(ident_string.to_owned(), enu); + } + + Some(EnumRef(IrTypeEnumRef { + name: ident_string.to_owned(), + is_struct: self + .enum_pool + .get(ident_string) + .map(IrEnum::is_struct) + .unwrap_or(true), + })) + } else { + None + } + }) + } + } +} + +impl<'a> TypeParser<'a> { + fn parse_enum_core(&mut self, ident: &syn::Ident) -> IrEnum { + let src_enum = self.src_enums[&ident.to_string()]; + let name = src_enum.ident.to_string(); + let wrapper_name = if src_enum.mirror { + Some(format!("mirror_{}", name)) + } else { + None + }; + let path = src_enum.path.clone(); + let comments = extract_comments(&src_enum.src.attrs); + let variants = src_enum + .src + .variants + .iter() + .map(|variant| IrVariant { + name: IrIdent::new(variant.ident.to_string()), + comments: extract_comments(&variant.attrs), + kind: match variant.fields.iter().next() { + None => IrVariantKind::Value, + Some(Field { + attrs, + ident: field_ident, + .. + }) => { + let variant_ident = variant.ident.to_string(); + IrVariantKind::Struct(IrStruct { + name: variant_ident, + wrapper_name: None, + path: None, + is_fields_named: field_ident.is_some(), + dart_metadata: extract_metadata(attrs), + comments: extract_comments(attrs), + fields: variant + .fields + .iter() + .enumerate() + .map(|(idx, field)| IrField { + name: IrIdent::new( + field + .ident + .as_ref() + .map(ToString::to_string) + .unwrap_or_else(|| format!("field{}", idx)), + ), + ty: self.parse_type(&field.ty), + is_final: true, + comments: extract_comments(&field.attrs), + }) + .collect(), + }) + } + }, + }) + .collect(); + IrEnum::new(name, wrapper_name, path, comments, variants) + } + + fn parse_struct_core(&mut self, ident: &syn::Ident) -> IrStruct { + let src_struct = self.src_structs[&ident.to_string()]; + let mut fields = Vec::new(); + + let (is_fields_named, struct_fields) = match &src_struct.src.fields { + Fields::Named(FieldsNamed { named, .. }) => (true, named), + Fields::Unnamed(FieldsUnnamed { unnamed, .. }) => (false, unnamed), + _ => panic!("unsupported type: {:?}", src_struct.src.fields), + }; + + for (idx, field) in struct_fields.iter().enumerate() { + let field_name = field + .ident + .as_ref() + .map_or(format!("field{}", idx), ToString::to_string); + let field_type = self.parse_type(&field.ty); + fields.push(IrField { + name: IrIdent::new(field_name), + ty: field_type, + is_final: !markers::has_non_final(&field.attrs), + comments: extract_comments(&field.attrs), + }); + } + + let name = src_struct.ident.to_string(); + let wrapper_name = if src_struct.mirror { + Some(format!("mirror_{}", name)) + } else { + None + }; + let path = Some(src_struct.path.clone()); + let metadata = extract_metadata(&src_struct.src.attrs); + let comments = extract_comments(&src_struct.src.attrs); + IrStruct { + name, + wrapper_name, + path, + fields, + is_fields_named, + dart_metadata: metadata, + comments, + } + } +} diff --git a/libs/flutter_rust_bridge_codegen/src/source_graph.rs b/libs/flutter_rust_bridge_codegen/src/source_graph.rs new file mode 100644 index 000000000..de9e3cbfe --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/source_graph.rs @@ -0,0 +1,553 @@ +/* + Things this doesn't currently support that it might need to later: + + - Import parsing is unfinished and so is currently disabled + - When import parsing is enabled: + - Import renames (use a::b as c) - these are silently ignored + - Imports that start with two colons (use ::a::b) - these are also silently ignored +*/ + +use std::{collections::HashMap, fmt::Debug, fs, path::PathBuf}; + +use cargo_metadata::MetadataCommand; +use log::{debug, warn}; +use syn::{Attribute, Ident, ItemEnum, ItemStruct, UseTree}; + +use crate::markers; + +/// Represents a crate, including a map of its modules, imports, structs and +/// enums. +#[derive(Debug, Clone)] +pub struct Crate { + pub name: String, + pub manifest_path: PathBuf, + pub root_src_file: PathBuf, + pub root_module: Module, +} + +impl Crate { + pub fn new(manifest_path: &str) -> Self { + let mut cmd = MetadataCommand::new(); + cmd.manifest_path(&manifest_path); + + let metadata = cmd.exec().unwrap(); + + let root_package = metadata.root_package().unwrap(); + let root_src_file = { + let lib_file = root_package + .manifest_path + .parent() + .unwrap() + .join("src/lib.rs"); + let main_file = root_package + .manifest_path + .parent() + .unwrap() + .join("src/main.rs"); + + if lib_file.exists() { + fs::canonicalize(lib_file).unwrap() + } else if main_file.exists() { + fs::canonicalize(main_file).unwrap() + } else { + panic!("No src/lib.rs or src/main.rs found for this Cargo.toml file"); + } + }; + + let source_rust_content = fs::read_to_string(&root_src_file).unwrap(); + let file_ast = syn::parse_file(&source_rust_content).unwrap(); + + let mut result = Crate { + name: root_package.name.clone(), + manifest_path: fs::canonicalize(manifest_path).unwrap(), + root_src_file: root_src_file.clone(), + root_module: Module { + visibility: Visibility::Public, + file_path: root_src_file, + module_path: vec!["crate".to_string()], + source: Some(ModuleSource::File(file_ast)), + scope: None, + }, + }; + + result.resolve(); + + result + } + + /// Create a map of the modules for this crate + pub fn resolve(&mut self) { + self.root_module.resolve(); + } +} + +/// Mirrors syn::Visibility, but can be created without a token +#[derive(Debug, Clone)] +pub enum Visibility { + Public, + Crate, + Restricted, // Not supported + Inherited, // Usually means private +} + +fn syn_vis_to_visibility(vis: &syn::Visibility) -> Visibility { + match vis { + syn::Visibility::Public(_) => Visibility::Public, + syn::Visibility::Crate(_) => Visibility::Crate, + syn::Visibility::Restricted(_) => Visibility::Restricted, + syn::Visibility::Inherited => Visibility::Inherited, + } +} + +#[derive(Debug, Clone)] +pub struct Import { + pub path: Vec, + pub visibility: Visibility, +} + +#[derive(Debug, Clone)] +pub enum ModuleSource { + File(syn::File), + ModuleInFile(Vec), +} + +#[derive(Clone)] +pub struct Struct { + pub ident: Ident, + pub src: ItemStruct, + pub visibility: Visibility, + pub path: Vec, + pub mirror: bool, +} + +impl Debug for Struct { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Struct") + .field("ident", &self.ident) + .field("src", &"omitted") + .field("visibility", &self.visibility) + .field("path", &self.path) + .field("mirror", &self.mirror) + .finish() + } +} + +#[derive(Clone)] +pub struct Enum { + pub ident: Ident, + pub src: ItemEnum, + pub visibility: Visibility, + pub path: Vec, + pub mirror: bool, +} + +impl Debug for Enum { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Enum") + .field("ident", &self.ident) + .field("src", &"omitted") + .field("visibility", &self.visibility) + .field("path", &self.path) + .field("mirror", &self.mirror) + .finish() + } +} + +#[derive(Debug, Clone)] +pub struct ModuleScope { + pub modules: Vec, + pub enums: Vec, + pub structs: Vec, + pub imports: Vec, +} + +#[derive(Clone)] +pub struct Module { + pub visibility: Visibility, + pub file_path: PathBuf, + pub module_path: Vec, + pub source: Option, + pub scope: Option, +} + +impl Debug for Module { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Module") + .field("visibility", &self.visibility) + .field("module_path", &self.module_path) + .field("file_path", &self.file_path) + .field("source", &"omitted") + .field("scope", &self.scope) + .finish() + } +} + +/// Get a struct or enum ident, possibly remapped by a mirror marker +fn get_ident(ident: &Ident, attrs: &[Attribute]) -> (Ident, bool) { + markers::extract_mirror_marker(attrs) + .and_then(|path| path.get_ident().map(|ident| (ident.clone(), true))) + .unwrap_or_else(|| (ident.clone(), false)) +} + +impl Module { + pub fn resolve(&mut self) { + self.resolve_modules(); + // self.resolve_imports(); + } + + /// Maps out modules, structs and enums within the scope of this module + fn resolve_modules(&mut self) { + let mut scope_modules = Vec::new(); + let mut scope_structs = Vec::new(); + let mut scope_enums = Vec::new(); + + let items = match self.source.as_ref().unwrap() { + ModuleSource::File(file) => &file.items, + ModuleSource::ModuleInFile(items) => items, + }; + + for item in items.iter() { + match item { + syn::Item::Struct(item_struct) => { + let (ident, mirror) = get_ident(&item_struct.ident, &item_struct.attrs); + let ident_str = ident.to_string(); + scope_structs.push(Struct { + ident, + src: item_struct.clone(), + visibility: syn_vis_to_visibility(&item_struct.vis), + path: { + let mut path = self.module_path.clone(); + path.push(ident_str); + path + }, + mirror, + }); + } + syn::Item::Enum(item_enum) => { + let (ident, mirror) = get_ident(&item_enum.ident, &item_enum.attrs); + let ident_str = ident.to_string(); + scope_enums.push(Enum { + ident, + src: item_enum.clone(), + visibility: syn_vis_to_visibility(&item_enum.vis), + path: { + let mut path = self.module_path.clone(); + path.push(ident_str); + path + }, + mirror, + }); + } + syn::Item::Mod(item_mod) => { + let ident = item_mod.ident.clone(); + + let mut module_path = self.module_path.clone(); + module_path.push(ident.to_string()); + + scope_modules.push(match &item_mod.content { + Some(content) => { + let mut child_module = Module { + visibility: syn_vis_to_visibility(&item_mod.vis), + file_path: self.file_path.clone(), + module_path, + source: Some(ModuleSource::ModuleInFile(content.1.clone())), + scope: None, + }; + + child_module.resolve(); + + child_module + } + None => { + let folder_path = + self.file_path.parent().unwrap().join(ident.to_string()); + let folder_exists = folder_path.exists(); + + let file_path = if folder_exists { + folder_path.join("mod.rs") + } else { + self.file_path + .parent() + .unwrap() + .join(ident.to_string() + ".rs") + }; + + let file_exists = file_path.exists(); + + if !file_exists { + warn!( + "Skipping unresolvable module {} (tried {})", + &ident, + file_path.to_string_lossy() + ); + continue; + } + + let source = if file_exists { + let source_rust_content = fs::read_to_string(&file_path).unwrap(); + debug!("Trying to parse {:?}", file_path); + Some(ModuleSource::File( + syn::parse_file(&source_rust_content).unwrap(), + )) + } else { + None + }; + + let mut child_module = Module { + visibility: syn_vis_to_visibility(&item_mod.vis), + file_path, + module_path, + source, + scope: None, + }; + + if file_exists { + child_module.resolve(); + } + + child_module + } + }); + } + _ => {} + } + } + + self.scope = Some(ModuleScope { + modules: scope_modules, + enums: scope_enums, + structs: scope_structs, + imports: vec![], // Will be filled in by resolve_imports() + }); + } + + #[allow(dead_code)] + fn resolve_imports(&mut self) { + let imports = &mut self.scope.as_mut().unwrap().imports; + + let items = match self.source.as_ref().unwrap() { + ModuleSource::File(file) => &file.items, + ModuleSource::ModuleInFile(items) => items, + }; + + for item in items.iter() { + if let syn::Item::Use(item_use) = item { + let flattened_imports = flatten_use_tree(&item_use.tree); + + for import in flattened_imports { + imports.push(Import { + path: import, + visibility: syn_vis_to_visibility(&item_use.vis), + }); + } + } + } + } + + pub fn collect_structs<'a>(&'a self, container: &mut HashMap) { + let scope = self.scope.as_ref().unwrap(); + for scope_struct in &scope.structs { + container.insert(scope_struct.ident.to_string(), scope_struct); + } + for scope_module in &scope.modules { + scope_module.collect_structs(container); + } + } + + pub fn collect_structs_to_vec(&self) -> HashMap { + let mut ans = HashMap::new(); + self.collect_structs(&mut ans); + ans + } + + pub fn collect_enums<'a>(&'a self, container: &mut HashMap) { + let scope = self.scope.as_ref().unwrap(); + for scope_enum in &scope.enums { + container.insert(scope_enum.ident.to_string(), scope_enum); + } + for scope_module in &scope.modules { + scope_module.collect_enums(container); + } + } + + pub fn collect_enums_to_vec(&self) -> HashMap { + let mut ans = HashMap::new(); + self.collect_enums(&mut ans); + ans + } +} + +fn flatten_use_tree_rename_abort_warning(use_tree: &UseTree) { + debug!("WARNING: flatten_use_tree() found an import rename (use a::b as c). flatten_use_tree() will now abort."); + debug!("WARNING: This happened while parsing {:?}", use_tree); + debug!("WARNING: This use statement will be ignored."); +} + +/// Takes a use tree and returns a flat list of use paths (list of string tokens) +/// +/// Example: +/// use a::{b::c, d::e}; +/// becomes +/// [ +/// ["a", "b", "c"], +/// ["a", "d", "e"] +/// ] +/// +/// Warning: As of writing, import renames (import a::b as c) are silently +/// ignored. +fn flatten_use_tree(use_tree: &UseTree) -> Vec> { + // Vec<(path, is_complete)> + let mut result = vec![(vec![], false)]; + + let mut counter: usize = 0; + + loop { + counter += 1; + + if counter > 10000 { + panic!("flatten_use_tree: Use statement complexity limit exceeded. This is probably a bug."); + } + + // If all paths are complete, break from the loop + if result.iter().all(|result_item| result_item.1) { + break; + } + + let mut items_to_push = Vec::new(); + + for path_tuple in &mut result { + let path = &mut path_tuple.0; + let is_complete = &mut path_tuple.1; + + if *is_complete { + continue; + } + + let mut tree_cursor = use_tree; + + for path_item in path.iter() { + match tree_cursor { + UseTree::Path(use_path) => { + let ident = use_path.ident.to_string(); + if *path_item != ident { + panic!("This ident did not match the one we already collected. This is a bug."); + } + tree_cursor = use_path.tree.as_ref(); + } + UseTree::Group(use_group) => { + let mut moved_tree_cursor = false; + + for tree in use_group.items.iter() { + match tree { + UseTree::Path(use_path) => { + if path_item == &use_path.ident.to_string() { + tree_cursor = use_path.tree.as_ref(); + moved_tree_cursor = true; + break; + } + } + // Since we're not matching UseTree::Group here, a::b::{{c}, {d}} might + // break. But also why would anybody do that + _ => unreachable!(), + } + } + + if !moved_tree_cursor { + unreachable!(); + } + } + _ => unreachable!(), + } + } + + match tree_cursor { + UseTree::Name(use_name) => { + path.push(use_name.ident.to_string()); + *is_complete = true; + } + UseTree::Path(use_path) => { + path.push(use_path.ident.to_string()); + } + UseTree::Glob(_) => { + path.push("*".to_string()); + *is_complete = true; + } + UseTree::Group(use_group) => { + // We'll modify the first one in-place, and make clones for + // all subsequent ones + let mut first: bool = true; + // Capture the path in this state, since we're about to + // modify it + let path_copy = path.clone(); + for tree in use_group.items.iter() { + let mut new_path_tuple = if first { + None + } else { + let new_path = path_copy.clone(); + items_to_push.push((new_path, false)); + Some(items_to_push.iter_mut().last().unwrap()) + }; + + match tree { + UseTree::Path(use_path) => { + let ident = use_path.ident.to_string(); + + if first { + path.push(ident); + } else { + new_path_tuple.unwrap().0.push(ident); + } + } + UseTree::Name(use_name) => { + let ident = use_name.ident.to_string(); + + if first { + path.push(ident); + *is_complete = true; + } else { + let path_tuple = new_path_tuple.as_mut().unwrap(); + path_tuple.0.push(ident); + path_tuple.1 = true; + } + } + UseTree::Glob(_) => { + if first { + path.push("*".to_string()); + *is_complete = true; + } else { + let path_tuple = new_path_tuple.as_mut().unwrap(); + path_tuple.0.push("*".to_string()); + path_tuple.1 = true; + } + } + UseTree::Group(_) => { + panic!( + "Directly-nested use groups ({}) are not supported by flutter_rust_bridge. Use {} instead.", + "use a::{{b}, c}", + "a::{b, c}" + ); + } + // UseTree::Group(_) => panic!(), + UseTree::Rename(_) => { + flatten_use_tree_rename_abort_warning(use_tree); + return vec![]; + } + } + + first = false; + } + } + UseTree::Rename(_) => { + flatten_use_tree_rename_abort_warning(use_tree); + return vec![]; + } + } + } + + for item in items_to_push { + result.push(item); + } + } + + result.into_iter().map(|val| val.0).collect() +} diff --git a/libs/flutter_rust_bridge_codegen/src/transformer.rs b/libs/flutter_rust_bridge_codegen/src/transformer.rs new file mode 100644 index 000000000..3ca79620e --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/transformer.rs @@ -0,0 +1,46 @@ +use log::debug; + +use crate::ir::IrType::*; +use crate::ir::*; + +pub fn transform(src: IrFile) -> IrFile { + let dst_funcs = src + .funcs + .into_iter() + .map(|src_func| IrFunc { + inputs: src_func + .inputs + .into_iter() + .map(transform_func_input_add_boxed) + .collect(), + ..src_func + }) + .collect(); + + IrFile { + funcs: dst_funcs, + ..src + } +} + +fn transform_func_input_add_boxed(input: IrField) -> IrField { + match &input.ty { + StructRef(_) + | EnumRef(IrTypeEnumRef { + is_struct: true, .. + }) => { + debug!( + "transform_func_input_add_boxed wrap Boxed to field={:?}", + input + ); + IrField { + ty: Boxed(IrTypeBoxed { + exist_in_real_api: false, // <-- + inner: Box::new(input.ty.clone()), + }), + ..input + } + } + _ => input, + } +} diff --git a/libs/flutter_rust_bridge_codegen/src/utils.rs b/libs/flutter_rust_bridge_codegen/src/utils.rs new file mode 100644 index 000000000..fa822b808 --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/utils.rs @@ -0,0 +1,26 @@ +use std::fs; +use std::path::Path; + +pub fn mod_from_rust_path(code_path: &str, crate_path: &str) -> String { + Path::new(code_path) + .strip_prefix(Path::new(crate_path).join("src")) + .unwrap() + .with_extension("") + .into_os_string() + .into_string() + .unwrap() + .replace('/', "::") +} + +pub fn with_changed_file anyhow::Result<()>>( + path: &str, + append_content: &str, + f: F, +) -> anyhow::Result<()> { + let content_original = fs::read_to_string(&path)?; + fs::write(&path, content_original.clone() + append_content)?; + + f()?; + + Ok(fs::write(&path, content_original)?) +} From 4b69ece608d6d21f94768c963409a65cba7b4878 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 31 May 2022 16:27:54 +0800 Subject: [PATCH 0036/2015] add: tab logic Signed-off-by: Kingtous --- .../lib/desktop/pages/connection_page.dart | 1 + .../desktop/pages/connection_tab_page.dart | 104 ++++++++++++++---- 2 files changed, 82 insertions(+), 23 deletions(-) diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 703d0a79a..6659986d8 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -53,6 +53,7 @@ class _ConnectionPageState extends State { children: [ getUpdateUI(), Row( + mainAxisAlignment: MainAxisAlignment.start, children: [ getSearchBarUI(), ], diff --git a/flutter/lib/desktop/pages/connection_tab_page.dart b/flutter/lib/desktop/pages/connection_tab_page.dart index ca53224f1..5ebf7b54e 100644 --- a/flutter/lib/desktop/pages/connection_tab_page.dart +++ b/flutter/lib/desktop/pages/connection_tab_page.dart @@ -1,8 +1,9 @@ import 'dart:convert'; +import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/desktop/pages/remote_page.dart'; -import 'package:flutter_hbb/models/model.dart'; +import 'package:flutter_hbb/desktop/widgets/titlebar_widget.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; class ConnectionTabPage extends StatefulWidget { @@ -18,11 +19,13 @@ class _ConnectionTabPageState extends State with SingleTickerProviderStateMixin { // refactor List when using multi-tab // this singleton is only for test - late String connectionId; - late TabController tabController; + List connectionIds = List.empty(growable: true); + var initialIndex = 0; _ConnectionTabPageState(Map params) { - connectionId = params['id'] ?? ""; + if (params['id'] != null) { + connectionIds.add(params['id']); + } } @override @@ -34,33 +37,88 @@ class _ConnectionTabPageState extends State // for simplify, just replace connectionId if (call.method == "new_remote_desktop") { setState(() { - FFI.close(); - connectionId = jsonDecode(call.arguments)["id"]; + final args = jsonDecode(call.arguments); + final id = args['id']; + final indexOf = connectionIds.indexOf(id); + if (indexOf >= 0) { + setState(() { + initialIndex = indexOf; + }); + } else { + connectionIds.add(id); + setState(() { + initialIndex = connectionIds.length - 1; + }); + } }); } }); - tabController = TabController(length: 1, vsync: this); } @override Widget build(BuildContext context) { - return Column( - children: [ - TabBar( - controller: tabController, - isScrollable: true, - labelColor: Colors.black87, - physics: NeverScrollableScrollPhysics(), - tabs: [ - Tab( - text: connectionId, + return Scaffold( + body: DefaultTabController( + initialIndex: initialIndex, + length: connectionIds.length, + animationDuration: Duration.zero, + child: Column( + children: [ + SizedBox( + height: 50, + child: DesktopTitleBar( + child: TabBar( + isScrollable: true, + labelColor: Colors.white, + physics: NeverScrollableScrollPhysics(), + indicatorColor: Colors.white, + tabs: connectionIds + .map((e) => Tab( + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(e), + SizedBox( + width: 4, + ), + InkWell( + onTap: () { + onRemoveId(e); + }, + child: Icon( + Icons.highlight_remove, + size: 20, + )) + ], + ), + )) + .toList()), ), - ]), - Expanded( - child: TabBarView(controller: tabController, children: [ - RemotePage(key: ValueKey(connectionId), id: connectionId) - ])) - ], + ), + Expanded( + child: TabBarView( + children: connectionIds + .map((e) => Container( + child: RemotePage( + key: ValueKey(e), + id: e))) //RemotePage(key: ValueKey(e), id: e)) + .toList()), + ) + ], + ), + ), ); } + + void onRemoveId(String id) { + final indexOf = connectionIds.indexOf(id); + if (indexOf == -1) { + return; + } + setState(() { + connectionIds.removeAt(indexOf); + initialIndex = max(0, initialIndex - 1); + }); + } } From e1e3491ec67e876fc2506ec8daf7e2730122a727 Mon Sep 17 00:00:00 2001 From: SoLongAndThanksForAllThePizza <103753680+SoLongAndThanksForAllThePizza@users.noreply.github.com> Date: Tue, 31 May 2022 16:57:42 +0800 Subject: [PATCH 0037/2015] fix: use forked codegen repo instead of local repo --- Cargo.lock | 1 + Cargo.toml | 4 +- libs/flutter_rust_bridge_codegen/.gitignore | 14 - libs/flutter_rust_bridge_codegen/Cargo.toml | 37 -- libs/flutter_rust_bridge_codegen/README.md | 95 --- .../src/commands.rs | 267 --------- .../flutter_rust_bridge_codegen/src/config.rs | 292 --------- libs/flutter_rust_bridge_codegen/src/error.rs | 32 - .../src/generator/c/mod.rs | 14 - .../src/generator/dart/mod.rs | 393 ------------- .../src/generator/dart/ty.rs | 64 -- .../src/generator/dart/ty_boxed.rs | 45 -- .../src/generator/dart/ty_delegate.rs | 42 -- .../src/generator/dart/ty_enum.rs | 207 ------- .../src/generator/dart/ty_general_list.rs | 27 - .../src/generator/dart/ty_optional.rs | 31 - .../src/generator/dart/ty_primitive.rs | 22 - .../src/generator/dart/ty_primitive_list.rs | 30 - .../src/generator/dart/ty_struct.rs | 135 ----- .../src/generator/mod.rs | 3 - .../src/generator/rust/mod.rs | 481 --------------- .../src/generator/rust/ty.rs | 96 --- .../src/generator/rust/ty_boxed.rs | 62 -- .../src/generator/rust/ty_delegate.rs | 45 -- .../src/generator/rust/ty_enum.rs | 343 ----------- .../src/generator/rust/ty_general_list.rs | 55 -- .../src/generator/rust/ty_optional.rs | 30 - .../src/generator/rust/ty_primitive.rs | 11 - .../src/generator/rust/ty_primitive_list.rs | 42 -- .../src/generator/rust/ty_struct.rs | 185 ------ .../src/ir/annotation.rs | 7 - .../src/ir/comment.rs | 26 - .../src/ir/field.rs | 9 - .../src/ir/file.rs | 61 -- .../src/ir/func.rs | 60 -- .../src/ir/ident.rs | 26 - .../src/ir/import.rs | 5 - .../flutter_rust_bridge_codegen/src/ir/mod.rs | 33 -- libs/flutter_rust_bridge_codegen/src/ir/ty.rs | 84 --- .../src/ir/ty_boxed.rs | 56 -- .../src/ir/ty_delegate.rs | 85 --- .../src/ir/ty_enum.rs | 139 ----- .../src/ir/ty_general_list.rs | 36 -- .../src/ir/ty_optional.rs | 65 -- .../src/ir/ty_primitive.rs | 114 ---- .../src/ir/ty_primitive_list.rs | 50 -- .../src/ir/ty_struct.rs | 66 --- libs/flutter_rust_bridge_codegen/src/lib.rs | 183 ------ libs/flutter_rust_bridge_codegen/src/main.rs | 19 - .../src/markers.rs | 39 -- .../flutter_rust_bridge_codegen/src/others.rs | 169 ------ .../src/parser/mod.rs | 353 ----------- .../src/parser/ty.rs | 392 ------------- .../src/source_graph.rs | 553 ------------------ .../src/transformer.rs | 46 -- libs/flutter_rust_bridge_codegen/src/utils.rs | 26 - 56 files changed, 3 insertions(+), 5804 deletions(-) delete mode 100644 libs/flutter_rust_bridge_codegen/.gitignore delete mode 100644 libs/flutter_rust_bridge_codegen/Cargo.toml delete mode 100644 libs/flutter_rust_bridge_codegen/README.md delete mode 100644 libs/flutter_rust_bridge_codegen/src/commands.rs delete mode 100644 libs/flutter_rust_bridge_codegen/src/config.rs delete mode 100644 libs/flutter_rust_bridge_codegen/src/error.rs delete mode 100644 libs/flutter_rust_bridge_codegen/src/generator/c/mod.rs delete mode 100644 libs/flutter_rust_bridge_codegen/src/generator/dart/mod.rs delete mode 100644 libs/flutter_rust_bridge_codegen/src/generator/dart/ty.rs delete mode 100644 libs/flutter_rust_bridge_codegen/src/generator/dart/ty_boxed.rs delete mode 100644 libs/flutter_rust_bridge_codegen/src/generator/dart/ty_delegate.rs delete mode 100644 libs/flutter_rust_bridge_codegen/src/generator/dart/ty_enum.rs delete mode 100644 libs/flutter_rust_bridge_codegen/src/generator/dart/ty_general_list.rs delete mode 100644 libs/flutter_rust_bridge_codegen/src/generator/dart/ty_optional.rs delete mode 100644 libs/flutter_rust_bridge_codegen/src/generator/dart/ty_primitive.rs delete mode 100644 libs/flutter_rust_bridge_codegen/src/generator/dart/ty_primitive_list.rs delete mode 100644 libs/flutter_rust_bridge_codegen/src/generator/dart/ty_struct.rs delete mode 100644 libs/flutter_rust_bridge_codegen/src/generator/mod.rs delete mode 100644 libs/flutter_rust_bridge_codegen/src/generator/rust/mod.rs delete mode 100644 libs/flutter_rust_bridge_codegen/src/generator/rust/ty.rs delete mode 100644 libs/flutter_rust_bridge_codegen/src/generator/rust/ty_boxed.rs delete mode 100644 libs/flutter_rust_bridge_codegen/src/generator/rust/ty_delegate.rs delete mode 100644 libs/flutter_rust_bridge_codegen/src/generator/rust/ty_enum.rs delete mode 100644 libs/flutter_rust_bridge_codegen/src/generator/rust/ty_general_list.rs delete mode 100644 libs/flutter_rust_bridge_codegen/src/generator/rust/ty_optional.rs delete mode 100644 libs/flutter_rust_bridge_codegen/src/generator/rust/ty_primitive.rs delete mode 100644 libs/flutter_rust_bridge_codegen/src/generator/rust/ty_primitive_list.rs delete mode 100644 libs/flutter_rust_bridge_codegen/src/generator/rust/ty_struct.rs delete mode 100644 libs/flutter_rust_bridge_codegen/src/ir/annotation.rs delete mode 100644 libs/flutter_rust_bridge_codegen/src/ir/comment.rs delete mode 100644 libs/flutter_rust_bridge_codegen/src/ir/field.rs delete mode 100644 libs/flutter_rust_bridge_codegen/src/ir/file.rs delete mode 100644 libs/flutter_rust_bridge_codegen/src/ir/func.rs delete mode 100644 libs/flutter_rust_bridge_codegen/src/ir/ident.rs delete mode 100644 libs/flutter_rust_bridge_codegen/src/ir/import.rs delete mode 100644 libs/flutter_rust_bridge_codegen/src/ir/mod.rs delete mode 100644 libs/flutter_rust_bridge_codegen/src/ir/ty.rs delete mode 100644 libs/flutter_rust_bridge_codegen/src/ir/ty_boxed.rs delete mode 100644 libs/flutter_rust_bridge_codegen/src/ir/ty_delegate.rs delete mode 100644 libs/flutter_rust_bridge_codegen/src/ir/ty_enum.rs delete mode 100644 libs/flutter_rust_bridge_codegen/src/ir/ty_general_list.rs delete mode 100644 libs/flutter_rust_bridge_codegen/src/ir/ty_optional.rs delete mode 100644 libs/flutter_rust_bridge_codegen/src/ir/ty_primitive.rs delete mode 100644 libs/flutter_rust_bridge_codegen/src/ir/ty_primitive_list.rs delete mode 100644 libs/flutter_rust_bridge_codegen/src/ir/ty_struct.rs delete mode 100644 libs/flutter_rust_bridge_codegen/src/lib.rs delete mode 100644 libs/flutter_rust_bridge_codegen/src/main.rs delete mode 100644 libs/flutter_rust_bridge_codegen/src/markers.rs delete mode 100644 libs/flutter_rust_bridge_codegen/src/others.rs delete mode 100644 libs/flutter_rust_bridge_codegen/src/parser/mod.rs delete mode 100644 libs/flutter_rust_bridge_codegen/src/parser/ty.rs delete mode 100644 libs/flutter_rust_bridge_codegen/src/source_graph.rs delete mode 100644 libs/flutter_rust_bridge_codegen/src/transformer.rs delete mode 100644 libs/flutter_rust_bridge_codegen/src/utils.rs diff --git a/Cargo.lock b/Cargo.lock index 5c4621f57..441b49c58 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1477,6 +1477,7 @@ dependencies = [ [[package]] name = "flutter_rust_bridge_codegen" version = "1.32.0" +source = "git+https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge#827fc60143988dfc3759f7e8ce16a20d80edd710" dependencies = [ "anyhow", "cargo_metadata", diff --git a/Cargo.toml b/Cargo.toml index f046df244..b395da582 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -105,7 +105,7 @@ jni = "0.19.0" flutter_rust_bridge = "1.30.0" [workspace] -members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "libs/virtual_display", "libs/flutter_rust_bridge_codegen"] +members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "libs/virtual_display"] [package.metadata.winres] LegalCopyright = "Copyright © 2022 Purslane, Inc." @@ -119,7 +119,7 @@ winapi = { version = "0.3", features = [ "winnt" ] } [build-dependencies] cc = "1.0" hbb_common = { path = "libs/hbb_common" } -flutter_rust_bridge_codegen = { path = "libs/flutter_rust_bridge_codegen" } +flutter_rust_bridge_codegen = { git = "https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge" } [dev-dependencies] hound = "3.4" diff --git a/libs/flutter_rust_bridge_codegen/.gitignore b/libs/flutter_rust_bridge_codegen/.gitignore deleted file mode 100644 index 6985cf1bd..000000000 --- a/libs/flutter_rust_bridge_codegen/.gitignore +++ /dev/null @@ -1,14 +0,0 @@ -# Generated by Cargo -# will have compiled files and executables -debug/ -target/ - -# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries -# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html -Cargo.lock - -# These are backup files generated by rustfmt -**/*.rs.bk - -# MSVC Windows builds of rustc generate these, which store debugging information -*.pdb diff --git a/libs/flutter_rust_bridge_codegen/Cargo.toml b/libs/flutter_rust_bridge_codegen/Cargo.toml deleted file mode 100644 index dfd1556db..000000000 --- a/libs/flutter_rust_bridge_codegen/Cargo.toml +++ /dev/null @@ -1,37 +0,0 @@ -[package] -name = "flutter_rust_bridge_codegen" -version = "1.32.0" -edition = "2018" -description = "High-level memory-safe bindgen for Dart/Flutter <-> Rust" -license = "MIT" -repository = "https://github.com/fzyzcjy/flutter_rust_bridge" -keywords = ["flutter", "dart", "ffi", "code-generation", "bindings"] -categories = ["development-tools::ffi"] - -[lib] -name = "lib_flutter_rust_bridge_codegen" -path = "src/lib.rs" - -[[bin]] -name = "flutter_rust_bridge_codegen" -path = "src/main.rs" - -[dependencies] -syn = { version = "1.0.77", features = ["full", "extra-traits"] } -quote = "1.0" -regex = "1.5.4" -lazy_static = "1.4.0" -convert_case = "0.5.0" -tempfile = "3.2.0" -serde = { version = "1.0", features = ["derive"] } -serde_yaml = "0.8" -log = "0.4" -env_logger = "0.9.0" -structopt = "0.3" -toml = "0.5.8" -anyhow = "1.0.44" -pathdiff = "0.2.1" -cargo_metadata = "0.14.1" -enum_dispatch = "0.3.8" -thiserror = "1" -cbindgen = "0.23" \ No newline at end of file diff --git a/libs/flutter_rust_bridge_codegen/README.md b/libs/flutter_rust_bridge_codegen/README.md deleted file mode 100644 index d9aa76531..000000000 --- a/libs/flutter_rust_bridge_codegen/README.md +++ /dev/null @@ -1,95 +0,0 @@ -# [flutter_rust_bridge](https://github.com/fzyzcjy/flutter_rust_bridge): High-level memory-safe binding generator for Flutter/Dart <-> Rust - -[![Rust Package](https://img.shields.io/crates/v/flutter_rust_bridge.svg)](https://crates.io/crates/flutter_rust_bridge) -[![Flutter Package](https://img.shields.io/pub/v/flutter_rust_bridge.svg)](https://pub.dev/packages/flutter_rust_bridge) -[![Stars](https://img.shields.io/github/stars/fzyzcjy/flutter_rust_bridge)](https://github.com/fzyzcjy/flutter_rust_bridge) -[![CI](https://github.com/fzyzcjy/flutter_rust_bridge/actions/workflows/ci.yaml/badge.svg)](https://github.com/fzyzcjy/flutter_rust_bridge/actions/workflows/ci.yaml) -[![Example](https://github.com/fzyzcjy/flutter_rust_bridge/actions/workflows/post_release.yaml/badge.svg)](https://github.com/fzyzcjy/flutter_rust_bridge/actions/workflows/post_release.yaml) -[![Codacy Badge](https://api.codacy.com/project/badge/Grade/6afbdad19e7245adbf9e9771777be3d7)](https://app.codacy.com/gh/fzyzcjy/flutter_rust_bridge?utm_source=github.com&utm_medium=referral&utm_content=fzyzcjy/flutter_rust_bridge&utm_campaign=Badge_Grade_Settings) - -![Logo](https://github.com/fzyzcjy/flutter_rust_bridge/raw/master/book/logo.png) - -Want to combine the best between [Flutter](https://flutter.dev/), a cross-platform hot-reload rapid-development UI toolkit, and [Rust](https://www.rust-lang.org/), a language empowering everyone to build reliable and efficient software? Here it comes! - -## 🚀 Advantages - -* **Memory-safe**: Never need to think about malloc/free. -* **Feature-rich**: `enum`s with values, platform-optimized `Vec`, possibly recursive `struct`, zero-copy big arrays, `Stream` (iterator) abstraction, error (`Result`) handling, cancellable tasks, concurrency control, and more. See full features [here](https://fzyzcjy.github.io/flutter_rust_bridge/feature.html). -* **Async programming**: Rust code will never block the Flutter. Call Rust naturally from Flutter's main isolate (thread). -* **Lightweight**: This is not a huge framework that includes everything, so you are free to use your favorite Flutter and Rust libraries. For example, state-management with Flutter library (e.g. MobX) can be elegant and simple (contrary to implementing in Rust); implementing a photo manipulation algorithm in Rust will be fast and safe (countrary to implementing in Flutter). -* **Cross-platform**: Android, iOS, Windows, Linux, MacOS ([Web](https://github.com/fzyzcjy/flutter_rust_bridge/issues/315) coming soon) -* **Easy to code-review & convince yourself**: This package simply simulates how humans write boilerplate code. If you want to convince yourself (or your team) that it is safe, there is not much code to look at. No magic at all! ([More about](https://fzyzcjy.github.io/flutter_rust_bridge/safety.html) safety concerns.) -* **Fast**: It is only a thin (though feature-rich) wrapper, without overhead such as protobuf serialization, thus performant. (More [benchmarks](https://github.com/fzyzcjy/flutter_rust_bridge/issues/318#issuecomment-1034536815) later) (Throw away components like thread-pool to make it even faster) -* **Pure-Dart compatible:** Despite the name, this package is 100% compatible with [pure](https://github.com/fzyzcjy/flutter_rust_bridge/blob/master/frb_example/pure_dart/README.md) Dart. - -## 💡 User Guide - -Check out [the user guide](https://fzyzcjy.github.io/flutter_rust_bridge/) for [show-me-the-code](https://fzyzcjy.github.io/flutter_rust_bridge/quickstart.html), [tutorials](https://fzyzcjy.github.io/flutter_rust_bridge/tutorial_with_flutter.html), [features](https://fzyzcjy.github.io/flutter_rust_bridge/feature.html) and much more. - -## 📎 P.S. Convenient Flutter tests - -If you want to write and debug tests in Flutter conveniently, with action history, time travelling, screenshots, rapid re-execution, video recordings, interactive mode and more, here is my another open-source library: https://github.com/fzyzcjy/flutter_convenient_test. - -## ✨ Contributors - - -[![All Contributors](https://img.shields.io/badge/all_contributors-18-orange.svg?style=flat-square)](#contributors-) - - -Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key) following [all-contributors](https://github.com/all-contributors/all-contributors) specification): - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

fzyzcjy

💻 📖 💡 🤔 🚧

Viet Dinh

💻 ⚠️ 📖

Joshua Wade

💻

Marcel

💻

rustui

📖

Michael Bryan

💻

bus710

📖

Sebastian Urban

💻

Daniel

💻

Kevin Li

💻 📖

Patrick Auernig

💻

Anton Lazarev

💻

Unoqwy

💻

Febrian Setianto

📖

syndim

💻

sagu

💻 📖

Ikko Ashimine

📖

alanlzhang

💻 📖
- - - - - - -More specifically, thanks for all these contributions: - -* [Desdaemon](https://github.com/Desdaemon): Support not only simple enums but also enums with fields which gets translated to native enum or freezed class in Dart. Support the Option type as nullable types in Dart. Support Vec of Strings type. Support comments in code. Add marker attributes for future usage. Add Linux and Windows support for with-flutter example, and make CI works for that. Avoid parameter collision. Overhaul the documentation and add several chapters to demonstrate configuring a Flutter+Rust project in all five platforms. Refactor command module. -* [SecondFlight](https://github.com/SecondFlight): Allow structs and enums to be imported from other files within the crate by creating source graph. Auto-create relavent dir. -* [Unoqwy](https://github.com/Unoqwy): Add struct mirrors, such that types in the external crates can be imported and used without redefining and copying. -* [antonok-edm](https://github.com/antonok-edm): Avoid converting syn types to strings before parsing to improve code and be more robust. -* [sagudev](https://github.com/sagudev): Make code generator a `lib`. Add error types. Depend on `cbindgen`. Fix LLVM paths. Update deps. Fix CI errors. -* [surban](https://github.com/surban): Support unit return type. Skip unresolvable modules. Ignore prefer_const_constructors. Non-final Dart fields. -* [trobanga](https://github.com/trobanga): Add support for `[T;N]` structs. Add `usize` support. Add a cmd argument. Separate dart tests. -* [AlienKevin](https://github.com/AlienKevin): Add flutter example for macOS. Add doc for Android NDK bug. -* [alanlzhang](https://github.com/alanlzhang): Add generation for Dart metadata. -* [efc-mw](https://github.com/efc-mw): Improve Windows encoding handling. -* [valeth](https://github.com/valeth): Rename callFfi's port. -* [Michael-F-Bryan](https://github.com/Michael-F-Bryan): Detect broken bindings. -* [bus710](https://github.com/bus710): Add a case in troubleshooting. -* [Syndim](https://github.com/Syndim): Add a bracket to box. -* [feber](https://github.com/feber): Fix doc link. -* [rustui](https://github.com/rustui): Fix a typo. -* [eltociear](https://github.com/eltociear): Fix a typo. - diff --git a/libs/flutter_rust_bridge_codegen/src/commands.rs b/libs/flutter_rust_bridge_codegen/src/commands.rs deleted file mode 100644 index 6838449d8..000000000 --- a/libs/flutter_rust_bridge_codegen/src/commands.rs +++ /dev/null @@ -1,267 +0,0 @@ -use std::fmt::Write; -use std::path::Path; -use std::process::Command; -use std::process::Output; - -use crate::error::{Error, Result}; -use log::{debug, info, warn}; - -#[must_use] -fn call_shell(cmd: &str) -> Output { - #[cfg(windows)] - return execute_command("powershell", &["-noprofile", "-c", cmd], None); - - #[cfg(not(windows))] - execute_command("sh", &["-c", cmd], None) -} - -pub fn ensure_tools_available() -> Result { - let output = call_shell("dart pub global list"); - let output = String::from_utf8_lossy(&output.stdout); - if !output.contains("ffigen") { - return Err(Error::MissingExe(String::from("ffigen"))); - } - - Ok(()) -} - -pub fn bindgen_rust_to_dart( - rust_crate_dir: &str, - c_output_path: &str, - dart_output_path: &str, - dart_class_name: &str, - c_struct_names: Vec, - llvm_install_path: &[String], - llvm_compiler_opts: &str, -) -> anyhow::Result<()> { - cbindgen(rust_crate_dir, c_output_path, c_struct_names)?; - ffigen( - c_output_path, - dart_output_path, - dart_class_name, - llvm_install_path, - llvm_compiler_opts, - ) -} - -#[must_use = "Error path must be handled."] -fn execute_command(bin: &str, args: &[&str], current_dir: Option<&str>) -> Output { - let mut cmd = Command::new(bin); - cmd.args(args); - - if let Some(current_dir) = current_dir { - cmd.current_dir(current_dir); - } - - debug!( - "execute command: bin={} args={:?} current_dir={:?} cmd={:?}", - bin, args, current_dir, cmd - ); - - let result = cmd - .output() - .unwrap_or_else(|err| panic!("\"{}\" \"{}\" failed: {}", bin, args.join(" "), err)); - - let stdout = String::from_utf8_lossy(&result.stdout); - if result.status.success() { - debug!( - "command={:?} stdout={} stderr={}", - cmd, - stdout, - String::from_utf8_lossy(&result.stderr) - ); - if stdout.contains("fatal error") { - warn!("See keywords such as `error` in command output. Maybe there is a problem? command={:?} output={:?}", cmd, result); - } else if args.contains(&"ffigen") && stdout.contains("[SEVERE]") { - // HACK: If ffigen can't find a header file it will generate broken - // bindings but still exit successfully. We can detect these broken - // bindings by looking for a "[SEVERE]" log message. - // - // It may emit SEVERE log messages for non-fatal errors though, so - // we don't want to error out completely. - - warn!( - "The `ffigen` command emitted a SEVERE error. Maybe there is a problem? command={:?} output=\n{}", - cmd, String::from_utf8_lossy(&result.stdout) - ); - } - } else { - warn!( - "command={:?} stdout={} stderr={}", - cmd, - stdout, - String::from_utf8_lossy(&result.stderr) - ); - } - result -} - -fn cbindgen( - rust_crate_dir: &str, - c_output_path: &str, - c_struct_names: Vec, -) -> anyhow::Result<()> { - debug!( - "execute cbindgen rust_crate_dir={} c_output_path={}", - rust_crate_dir, c_output_path - ); - - let config = cbindgen::Config { - language: cbindgen::Language::C, - sys_includes: vec![ - "stdbool.h".to_string(), - "stdint.h".to_string(), - "stdlib.h".to_string(), - ], - no_includes: true, - export: cbindgen::ExportConfig { - include: c_struct_names - .iter() - .map(|name| format!("\"{}\"", name)) - .collect::>(), - ..Default::default() - }, - ..Default::default() - }; - - debug!("cbindgen config: {:?}", config); - - let canonical = Path::new(rust_crate_dir) - .canonicalize() - .expect("Could not canonicalize rust crate dir"); - let mut path = canonical.to_str().unwrap(); - - // on windows get rid of the UNC path - if path.starts_with(r"\\?\") { - path = &path[r"\\?\".len()..]; - } - - if cbindgen::generate_with_config(path, config)?.write_to_file(c_output_path) { - Ok(()) - } else { - Err(Error::str("cbindgen failed writing file").into()) - } -} - -fn ffigen( - c_path: &str, - dart_path: &str, - dart_class_name: &str, - llvm_path: &[String], - llvm_compiler_opts: &str, -) -> anyhow::Result<()> { - debug!( - "execute ffigen c_path={} dart_path={} llvm_path={:?}", - c_path, dart_path, llvm_path - ); - let mut config = format!( - " - output: '{}' - name: '{}' - description: 'generated by flutter_rust_bridge' - headers: - entry-points: - - '{}' - include-directives: - - '{}' - comments: false - preamble: | - // ignore_for_file: camel_case_types, non_constant_identifier_names, avoid_positional_boolean_parameters, annotate_overrides, constant_identifier_names - ", - dart_path, dart_class_name, c_path, c_path, - ); - if !llvm_path.is_empty() { - write!( - &mut config, - " - llvm-path:\n" - )?; - for path in llvm_path { - writeln!(&mut config, " - '{}'", path)?; - } - } - - if !llvm_compiler_opts.is_empty() { - config = format!( - "{} - compiler-opts: - - '{}'", - config, llvm_compiler_opts - ); - } - - debug!("ffigen config: {}", config); - - let mut config_file = tempfile::NamedTempFile::new()?; - std::io::Write::write_all(&mut config_file, config.as_bytes())?; - debug!("ffigen config_file: {:?}", config_file); - - // NOTE please install ffigen globally first: `dart pub global activate ffigen` - let res = call_shell(&format!( - "dart pub global run ffigen --config \"{}\"", - config_file.path().to_string_lossy() - )); - if !res.status.success() { - let err = String::from_utf8_lossy(&res.stderr); - let out = String::from_utf8_lossy(&res.stdout); - let pat = "Couldn't find dynamic library in default locations."; - if err.contains(pat) || out.contains(pat) { - return Err(Error::FfigenLlvm.into()); - } - return Err( - Error::string(format!("ffigen failed:\nstderr: {}\nstdout: {}", err, out)).into(), - ); - } - Ok(()) -} - -pub fn format_rust(path: &str) -> Result { - debug!("execute format_rust path={}", path); - let res = execute_command("rustfmt", &[path], None); - if !res.status.success() { - return Err(Error::Rustfmt( - String::from_utf8_lossy(&res.stderr).to_string(), - )); - } - Ok(()) -} - -pub fn format_dart(path: &str, line_length: i32) -> Result { - debug!( - "execute format_dart path={} line_length={}", - path, line_length - ); - let res = call_shell(&format!( - "dart format {} --line-length {}", - path, line_length - )); - if !res.status.success() { - return Err(Error::Dartfmt( - String::from_utf8_lossy(&res.stderr).to_string(), - )); - } - Ok(()) -} - -pub fn build_runner(dart_root: &str) -> Result { - info!("Running build_runner at {}", dart_root); - let out = if cfg!(windows) { - call_shell(&format!( - "cd \"{}\"; flutter pub run build_runner build --delete-conflicting-outputs", - dart_root - )) - } else { - call_shell(&format!( - "cd \"{}\" && flutter pub run build_runner build --delete-conflicting-outputs", - dart_root - )) - }; - if !out.status.success() { - return Err(Error::StringError(format!( - "Failed to run build_runner for {}: {}", - dart_root, - String::from_utf8_lossy(&out.stdout) - ))); - } - Ok(()) -} diff --git a/libs/flutter_rust_bridge_codegen/src/config.rs b/libs/flutter_rust_bridge_codegen/src/config.rs deleted file mode 100644 index de77cd1b1..000000000 --- a/libs/flutter_rust_bridge_codegen/src/config.rs +++ /dev/null @@ -1,292 +0,0 @@ -use std::env; -use std::ffi::OsString; -use std::fs; -use std::path::Path; -use std::path::PathBuf; -use std::str::FromStr; - -use anyhow::{anyhow, Result}; -use convert_case::{Case, Casing}; -use serde::Deserialize; -use structopt::clap::AppSettings; -use structopt::StructOpt; -use toml::Value; - -#[derive(StructOpt, Debug, PartialEq, Deserialize, Default)] -#[structopt(setting(AppSettings::DeriveDisplayOrder))] -pub struct RawOpts { - /// Path of input Rust code - #[structopt(short, long)] - pub rust_input: String, - /// Path of output generated Dart code - #[structopt(short, long)] - pub dart_output: String, - /// If provided, generated Dart declaration code to this separate file - #[structopt(long)] - pub dart_decl_output: Option, - - /// Path of output generated C header - #[structopt(short, long)] - pub c_output: Option>, - /// Crate directory for your Rust project - #[structopt(long)] - pub rust_crate_dir: Option, - /// Path of output generated Rust code - #[structopt(long)] - pub rust_output: Option, - /// Generated class name - #[structopt(long)] - pub class_name: Option, - /// Line length for dart formatting - #[structopt(long)] - pub dart_format_line_length: Option, - /// Skip automatically adding `mod bridge_generated;` to `lib.rs` - #[structopt(long)] - pub skip_add_mod_to_lib: bool, - /// Path to the installed LLVM - #[structopt(long)] - pub llvm_path: Option>, - /// LLVM compiler opts - #[structopt(long)] - pub llvm_compiler_opts: Option, - /// Path to root of Dart project, otherwise inferred from --dart-output - #[structopt(long)] - pub dart_root: Option, - /// Skip running build_runner even when codegen-capable code is detected - #[structopt(long)] - pub no_build_runner: bool, - /// Show debug messages. - #[structopt(short, long)] - pub verbose: bool, -} - -#[derive(Debug)] -pub struct Opts { - pub rust_input_path: String, - pub dart_output_path: String, - pub dart_decl_output_path: Option, - pub c_output_path: Vec, - pub rust_crate_dir: String, - pub rust_output_path: String, - pub class_name: String, - pub dart_format_line_length: i32, - pub skip_add_mod_to_lib: bool, - pub llvm_path: Vec, - pub llvm_compiler_opts: String, - pub manifest_path: String, - pub dart_root: Option, - pub build_runner: bool, -} - -pub fn parse(raw: RawOpts) -> Opts { - let rust_input_path = canon_path(&raw.rust_input); - - let rust_crate_dir = canon_path(&raw.rust_crate_dir.unwrap_or_else(|| { - fallback_rust_crate_dir(&rust_input_path) - .unwrap_or_else(|_| panic!("{}", format_fail_to_guess_error("rust_crate_dir"))) - })); - let manifest_path = { - let mut path = std::path::PathBuf::from_str(&rust_crate_dir).unwrap(); - path.push("Cargo.toml"); - path_to_string(path).unwrap() - }; - let rust_output_path = canon_path(&raw.rust_output.unwrap_or_else(|| { - fallback_rust_output_path(&rust_input_path) - .unwrap_or_else(|_| panic!("{}", format_fail_to_guess_error("rust_output"))) - })); - let class_name = raw.class_name.unwrap_or_else(|| { - fallback_class_name(&*rust_crate_dir) - .unwrap_or_else(|_| panic!("{}", format_fail_to_guess_error("class_name"))) - }); - let c_output_path = raw - .c_output - .map(|outputs| { - outputs - .iter() - .map(|output| canon_path(output)) - .collect::>() - }) - .unwrap_or_else(|| { - vec![fallback_c_output_path() - .unwrap_or_else(|_| panic!("{}", format_fail_to_guess_error("c_output")))] - }); - - let dart_root = { - let dart_output = &raw.dart_output; - raw.dart_root - .as_deref() - .map(canon_path) - .or_else(|| fallback_dart_root(dart_output).ok()) - }; - - Opts { - rust_input_path, - dart_output_path: canon_path(&raw.dart_output), - dart_decl_output_path: raw - .dart_decl_output - .as_ref() - .map(|s| canon_path(s.as_str())), - c_output_path, - rust_crate_dir, - rust_output_path, - class_name, - dart_format_line_length: raw.dart_format_line_length.unwrap_or(80), - skip_add_mod_to_lib: raw.skip_add_mod_to_lib, - llvm_path: raw.llvm_path.unwrap_or_else(|| { - vec![ - "/opt/homebrew/opt/llvm".to_owned(), // Homebrew root - "/usr/local/opt/llvm".to_owned(), // Homebrew x86-64 root - // Possible Linux LLVM roots - "/usr/lib/llvm-9".to_owned(), - "/usr/lib/llvm-10".to_owned(), - "/usr/lib/llvm-11".to_owned(), - "/usr/lib/llvm-12".to_owned(), - "/usr/lib/llvm-13".to_owned(), - "/usr/lib/llvm-14".to_owned(), - "/usr/lib/".to_owned(), - "/usr/lib64/".to_owned(), - "C:/Program Files/llvm".to_owned(), // Default on Windows - "C:/Program Files/LLVM".to_owned(), - "C:/msys64/mingw64".to_owned(), // https://packages.msys2.org/package/mingw-w64-x86_64-clang - ] - }), - llvm_compiler_opts: raw.llvm_compiler_opts.unwrap_or_else(|| "".to_string()), - manifest_path, - dart_root, - build_runner: !raw.no_build_runner, - } -} - -fn format_fail_to_guess_error(name: &str) -> String { - format!( - "fail to guess {}, please specify it manually in command line arguments", - name - ) -} - -fn fallback_rust_crate_dir(rust_input_path: &str) -> Result { - let mut dir_curr = Path::new(rust_input_path) - .parent() - .ok_or_else(|| anyhow!(""))?; - - loop { - let path_cargo_toml = dir_curr.join("Cargo.toml"); - - if path_cargo_toml.exists() { - return Ok(dir_curr - .as_os_str() - .to_str() - .ok_or_else(|| anyhow!(""))? - .to_string()); - } - - if let Some(next_parent) = dir_curr.parent() { - dir_curr = next_parent; - } else { - break; - } - } - Err(anyhow!( - "look at parent directories but none contains Cargo.toml" - )) -} - -fn fallback_c_output_path() -> Result { - let named_temp_file = Box::leak(Box::new(tempfile::Builder::new().suffix(".h").tempfile()?)); - Ok(named_temp_file - .path() - .to_str() - .ok_or_else(|| anyhow!(""))? - .to_string()) -} - -fn fallback_rust_output_path(rust_input_path: &str) -> Result { - Ok(Path::new(rust_input_path) - .parent() - .ok_or_else(|| anyhow!(""))? - .join("bridge_generated.rs") - .to_str() - .ok_or_else(|| anyhow!(""))? - .to_string()) -} - -fn fallback_dart_root(dart_output_path: &str) -> Result { - let mut res = canon_pathbuf(dart_output_path); - while res.pop() { - if res.join("pubspec.yaml").is_file() { - return res - .to_str() - .map(ToString::to_string) - .ok_or_else(|| anyhow!("Non-utf8 path")); - } - } - Err(anyhow!( - "Root of Dart library could not be inferred from Dart output" - )) -} - -fn fallback_class_name(rust_crate_dir: &str) -> Result { - let cargo_toml_path = Path::new(rust_crate_dir).join("Cargo.toml"); - let cargo_toml_content = fs::read_to_string(cargo_toml_path)?; - - let cargo_toml_value = cargo_toml_content.parse::()?; - let package_name = cargo_toml_value - .get("package") - .ok_or_else(|| anyhow!("no `package` in Cargo.toml"))? - .get("name") - .ok_or_else(|| anyhow!("no `name` in Cargo.toml"))? - .as_str() - .ok_or_else(|| anyhow!(""))?; - - Ok(package_name.to_case(Case::Pascal)) -} - -fn canon_path(sub_path: &str) -> String { - let path = canon_pathbuf(sub_path); - path_to_string(path).unwrap_or_else(|_| panic!("fail to parse path: {}", sub_path)) -} - -fn canon_pathbuf(sub_path: &str) -> PathBuf { - let mut path = - env::current_dir().unwrap_or_else(|_| panic!("fail to parse path: {}", sub_path)); - path.push(sub_path); - path -} - -fn path_to_string(path: PathBuf) -> Result { - path.into_os_string().into_string() -} - -impl Opts { - pub fn dart_api_class_name(&self) -> String { - self.class_name.clone() - } - - pub fn dart_api_impl_class_name(&self) -> String { - format!("{}Impl", self.class_name) - } - - pub fn dart_wire_class_name(&self) -> String { - format!("{}Wire", self.class_name) - } - - /// Returns None if the path terminates in "..", or not utf8. - pub fn dart_output_path_name(&self) -> Option<&str> { - let name = Path::new(&self.dart_output_path); - let root = name.file_name()?.to_str()?; - if let Some((name, _)) = root.rsplit_once('.') { - Some(name) - } else { - Some(root) - } - } - - pub fn dart_output_freezed_path(&self) -> Option { - Some( - Path::new(&self.dart_output_path) - .with_extension("freezed.dart") - .to_str()? - .to_owned(), - ) - } -} diff --git a/libs/flutter_rust_bridge_codegen/src/error.rs b/libs/flutter_rust_bridge_codegen/src/error.rs deleted file mode 100644 index 9a8607d37..000000000 --- a/libs/flutter_rust_bridge_codegen/src/error.rs +++ /dev/null @@ -1,32 +0,0 @@ -use thiserror::Error; - -pub type Result = std::result::Result<(), Error>; - -#[derive(Error, Debug)] -pub enum Error { - #[error("rustfmt failed: {0}")] - Rustfmt(String), - #[error("dart fmt failed: {0}")] - Dartfmt(String), - #[error( - "ffigen could not find LLVM. - Please supply --llvm-path to flutter_rust_bridge_codegen, e.g.: - - flutter_rust_bridge_codegen .. --llvm-path " - )] - FfigenLlvm, - #[error("{0} is not a command, or not executable.")] - MissingExe(String), - #[error("{0}")] - StringError(String), -} - -impl Error { - pub fn str(msg: &str) -> Self { - Self::StringError(msg.to_owned()) - } - - pub fn string(msg: String) -> Self { - Self::StringError(msg) - } -} diff --git a/libs/flutter_rust_bridge_codegen/src/generator/c/mod.rs b/libs/flutter_rust_bridge_codegen/src/generator/c/mod.rs deleted file mode 100644 index 2a2410dbc..000000000 --- a/libs/flutter_rust_bridge_codegen/src/generator/c/mod.rs +++ /dev/null @@ -1,14 +0,0 @@ -pub fn generate_dummy(func_names: &[String]) -> String { - format!( - r#"static int64_t dummy_method_to_enforce_bundling(void) {{ - int64_t dummy_var = 0; -{} - return dummy_var; -}}"#, - func_names - .iter() - .map(|func_name| { format!(" dummy_var ^= ((int64_t) (void*) {});", func_name) }) - .collect::>() - .join("\n"), - ) -} diff --git a/libs/flutter_rust_bridge_codegen/src/generator/dart/mod.rs b/libs/flutter_rust_bridge_codegen/src/generator/dart/mod.rs deleted file mode 100644 index afe35527f..000000000 --- a/libs/flutter_rust_bridge_codegen/src/generator/dart/mod.rs +++ /dev/null @@ -1,393 +0,0 @@ -mod ty; -mod ty_boxed; -mod ty_delegate; -mod ty_enum; -mod ty_general_list; -mod ty_optional; -mod ty_primitive; -mod ty_primitive_list; -mod ty_struct; - -use std::collections::HashSet; - -pub use ty::*; -pub use ty_boxed::*; -pub use ty_delegate::*; -pub use ty_enum::*; -pub use ty_general_list::*; -pub use ty_optional::*; -pub use ty_primitive::*; -pub use ty_primitive_list::*; -pub use ty_struct::*; - -use convert_case::{Case, Casing}; -use log::debug; - -use crate::ir::IrType::*; -use crate::ir::*; -use crate::others::*; - -pub struct Output { - pub file_prelude: DartBasicCode, - pub decl_code: DartBasicCode, - pub impl_code: DartBasicCode, -} - -pub fn generate( - ir_file: &IrFile, - dart_api_class_name: &str, - dart_api_impl_class_name: &str, - dart_wire_class_name: &str, - dart_output_file_root: &str, -) -> (Output, bool) { - let distinct_types = ir_file.distinct_types(true, true); - let distinct_input_types = ir_file.distinct_types(true, false); - let distinct_output_types = ir_file.distinct_types(false, true); - debug!("distinct_input_types={:?}", distinct_input_types); - debug!("distinct_output_types={:?}", distinct_output_types); - - let dart_func_signatures_and_implementations = ir_file - .funcs - .iter() - .map(generate_api_func) - .collect::>(); - let dart_structs = distinct_types - .iter() - .map(|ty| TypeDartGenerator::new(ty.clone(), ir_file).structs()) - .collect::>(); - let dart_api2wire_funcs = distinct_input_types - .iter() - .map(|ty| generate_api2wire_func(ty, ir_file)) - .collect::>(); - let dart_api_fill_to_wire_funcs = distinct_input_types - .iter() - .map(|ty| generate_api_fill_to_wire_func(ty, ir_file)) - .collect::>(); - let dart_wire2api_funcs = distinct_output_types - .iter() - .map(|ty| generate_wire2api_func(ty, ir_file)) - .collect::>(); - - let needs_freezed = distinct_types.iter().any(|ty| match ty { - EnumRef(e) if e.is_struct => true, - StructRef(s) if s.freezed => true, - _ => false, - }); - let freezed_header = if needs_freezed { - DartBasicCode { - import: "import 'package:freezed_annotation/freezed_annotation.dart';".to_string(), - part: format!("part '{}.freezed.dart';", dart_output_file_root), - body: "".to_string(), - } - } else { - DartBasicCode::default() - }; - - let imports = ir_file - .struct_pool - .values() - .flat_map(|s| s.dart_metadata.iter().flat_map(|it| &it.library)) - .collect::>(); - - let import_header = if !imports.is_empty() { - DartBasicCode { - import: imports - .iter() - .map(|it| match &it.alias { - Some(alias) => format!("import '{}' as {};", it.uri, alias), - _ => format!("import '{}';", it.uri), - }) - .collect::>() - .join("\n"), - part: "".to_string(), - body: "".to_string(), - } - } else { - DartBasicCode::default() - }; - - let common_header = DartBasicCode { - import: "import 'dart:convert'; - import 'dart:typed_data';" - .to_string(), - part: "".to_string(), - body: "".to_string(), - }; - - let decl_body = format!( - "abstract class {} {{ - {} - }} - - {} - ", - dart_api_class_name, - dart_func_signatures_and_implementations - .iter() - .map(|(sig, _, comm)| format!("{}{}", comm, sig)) - .collect::>() - .join("\n\n"), - dart_structs.join("\n\n"), - ); - - let impl_body = format!( - "class {dart_api_impl_class_name} extends FlutterRustBridgeBase<{dart_wire_class_name}> implements {dart_api_class_name} {{ - factory {dart_api_impl_class_name}(ffi.DynamicLibrary dylib) => {dart_api_impl_class_name}.raw({dart_wire_class_name}(dylib)); - - {dart_api_impl_class_name}.raw({dart_wire_class_name} inner) : super(inner); - - {} - - // Section: api2wire - {} - - // Section: api_fill_to_wire - {} - }} - - // Section: wire2api - {} - ", - dart_func_signatures_and_implementations - .iter() - .map(|(_, imp, _)| imp.clone()) - .collect::>() - .join("\n\n"), - dart_api2wire_funcs.join("\n\n"), - dart_api_fill_to_wire_funcs.join("\n\n"), - dart_wire2api_funcs.join("\n\n"), - dart_api_impl_class_name = dart_api_impl_class_name, - dart_wire_class_name = dart_wire_class_name, - dart_api_class_name = dart_api_class_name, - ); - - let decl_code = &common_header - + &freezed_header - + &import_header - + &DartBasicCode { - import: "".to_string(), - part: "".to_string(), - body: decl_body, - }; - - let impl_code = &common_header - + &DartBasicCode { - import: "import 'package:flutter_rust_bridge/flutter_rust_bridge.dart';".to_string(), - part: "".to_string(), - body: impl_body, - }; - - let file_prelude = DartBasicCode { - import: format!("{} - - // ignore_for_file: non_constant_identifier_names, unused_element, duplicate_ignore, directives_ordering, curly_braces_in_flow_control_structures, unnecessary_lambdas, slash_for_doc_comments, prefer_const_literals_to_create_immutables, implicit_dynamic_list_literal, duplicate_import, unused_import, prefer_single_quotes, prefer_const_constructors - ", - CODE_HEADER - ), - part: "".to_string(), - body: "".to_string(), - }; - - ( - Output { - file_prelude, - decl_code, - impl_code, - }, - needs_freezed, - ) -} - -fn generate_api_func(func: &IrFunc) -> (String, String, String) { - let raw_func_param_list = func - .inputs - .iter() - .map(|input| { - format!( - "{}{} {}", - input.ty.dart_required_modifier(), - input.ty.dart_api_type(), - input.name.dart_style() - ) - }) - .collect::>(); - - let full_func_param_list = [raw_func_param_list, vec!["dynamic hint".to_string()]].concat(); - - let wire_param_list = [ - if func.mode.has_port_argument() { - vec!["port_".to_string()] - } else { - vec![] - }, - func.inputs - .iter() - .map(|input| { - // edge case: ffigen performs its own bool-to-int conversions - if let IrType::Primitive(IrTypePrimitive::Bool) = input.ty { - input.name.dart_style() - } else { - format!( - "_api2wire_{}({})", - &input.ty.safe_ident(), - &input.name.dart_style() - ) - } - }) - .collect::>(), - ] - .concat(); - - let partial = format!( - "{} {}({{ {} }})", - func.mode.dart_return_type(&func.output.dart_api_type()), - func.name.to_case(Case::Camel), - full_func_param_list.join(","), - ); - - let execute_func_name = match func.mode { - IrFuncMode::Normal => "executeNormal", - IrFuncMode::Sync => "executeSync", - IrFuncMode::Stream => "executeStream", - }; - - let signature = format!("{};", partial); - - let comments = dart_comments(&func.comments); - - let task_common_args = format!( - " - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: \"{}\", - argNames: [{}], - ), - argValues: [{}], - hint: hint, - ", - func.name, - func.inputs - .iter() - .map(|input| format!("\"{}\"", input.name.dart_style())) - .collect::>() - .join(", "), - func.inputs - .iter() - .map(|input| input.name.dart_style()) - .collect::>() - .join(", "), - ); - - let implementation = match func.mode { - IrFuncMode::Sync => format!( - "{} => {}(FlutterRustBridgeSyncTask( - callFfi: () => inner.{}({}), - {} - ));", - partial, - execute_func_name, - func.wire_func_name(), - wire_param_list.join(", "), - task_common_args, - ), - _ => format!( - "{} => {}(FlutterRustBridgeTask( - callFfi: (port_) => inner.{}({}), - parseSuccessData: _wire2api_{}, - {} - ));", - partial, - execute_func_name, - func.wire_func_name(), - wire_param_list.join(", "), - func.output.safe_ident(), - task_common_args, - ), - }; - - (signature, implementation, comments) -} - -fn generate_api2wire_func(ty: &IrType, ir_file: &IrFile) -> String { - if let Some(body) = TypeDartGenerator::new(ty.clone(), ir_file).api2wire_body() { - format!( - "{} _api2wire_{}({} raw) {{ - {} - }} - ", - ty.dart_wire_type(), - ty.safe_ident(), - ty.dart_api_type(), - body, - ) - } else { - "".to_string() - } -} - -fn generate_api_fill_to_wire_func(ty: &IrType, ir_file: &IrFile) -> String { - if let Some(body) = TypeDartGenerator::new(ty.clone(), ir_file).api_fill_to_wire_body() { - let target_wire_type = match ty { - Optional(inner) => &inner.inner, - it => it, - }; - - format!( - "void _api_fill_to_wire_{}({} apiObj, {} wireObj) {{ - {} - }}", - ty.safe_ident(), - ty.dart_api_type(), - target_wire_type.dart_wire_type(), - body, - ) - } else { - "".to_string() - } -} - -fn generate_wire2api_func(ty: &IrType, ir_file: &IrFile) -> String { - let body = TypeDartGenerator::new(ty.clone(), ir_file).wire2api_body(); - - format!( - "{} _wire2api_{}(dynamic raw) {{ - {} - }} - ", - ty.dart_api_type(), - ty.safe_ident(), - body, - ) -} - -fn gen_wire2api_simple_type_cast(s: &str) -> String { - format!("return raw as {};", s) -} - -/// A trailing newline is included if comments is not empty. -fn dart_comments(comments: &[IrComment]) -> String { - let mut comments = comments - .iter() - .map(IrComment::comment) - .collect::>() - .join("\n"); - if !comments.is_empty() { - comments.push('\n'); - } - comments -} -fn dart_metadata(metadata: &[IrDartAnnotation]) -> String { - let mut metadata = metadata - .iter() - .map(|it| match &it.library { - Some(IrDartImport { - alias: Some(alias), .. - }) => format!("@{}.{}", alias, it.content), - _ => format!("@{}", it.content), - }) - .collect::>() - .join("\n"); - if !metadata.is_empty() { - metadata.push('\n'); - } - metadata -} diff --git a/libs/flutter_rust_bridge_codegen/src/generator/dart/ty.rs b/libs/flutter_rust_bridge_codegen/src/generator/dart/ty.rs deleted file mode 100644 index dd8004ed9..000000000 --- a/libs/flutter_rust_bridge_codegen/src/generator/dart/ty.rs +++ /dev/null @@ -1,64 +0,0 @@ -use crate::generator::dart::*; -use enum_dispatch::enum_dispatch; - -#[enum_dispatch] -pub trait TypeDartGeneratorTrait { - fn api2wire_body(&self) -> Option; - - fn api_fill_to_wire_body(&self) -> Option { - None - } - - fn wire2api_body(&self) -> String { - "".to_string() - } - - fn structs(&self) -> String { - "".to_string() - } -} - -#[derive(Debug, Clone)] -pub struct TypeGeneratorContext<'a> { - pub ir_file: &'a IrFile, -} - -#[macro_export] -macro_rules! type_dart_generator_struct { - ($cls:ident, $ir_cls:ty) => { - #[derive(Debug, Clone)] - pub struct $cls<'a> { - pub ir: $ir_cls, - pub context: TypeGeneratorContext<'a>, - } - }; -} - -#[enum_dispatch(TypeDartGeneratorTrait)] -#[derive(Debug, Clone)] -pub enum TypeDartGenerator<'a> { - Primitive(TypePrimitiveGenerator<'a>), - Delegate(TypeDelegateGenerator<'a>), - PrimitiveList(TypePrimitiveListGenerator<'a>), - Optional(TypeOptionalGenerator<'a>), - GeneralList(TypeGeneralListGenerator<'a>), - StructRef(TypeStructRefGenerator<'a>), - Boxed(TypeBoxedGenerator<'a>), - EnumRef(TypeEnumRefGenerator<'a>), -} - -impl<'a> TypeDartGenerator<'a> { - pub fn new(ty: IrType, ir_file: &'a IrFile) -> Self { - let context = TypeGeneratorContext { ir_file }; - match ty { - Primitive(ir) => TypePrimitiveGenerator { ir, context }.into(), - Delegate(ir) => TypeDelegateGenerator { ir, context }.into(), - PrimitiveList(ir) => TypePrimitiveListGenerator { ir, context }.into(), - Optional(ir) => TypeOptionalGenerator { ir, context }.into(), - GeneralList(ir) => TypeGeneralListGenerator { ir, context }.into(), - StructRef(ir) => TypeStructRefGenerator { ir, context }.into(), - Boxed(ir) => TypeBoxedGenerator { ir, context }.into(), - EnumRef(ir) => TypeEnumRefGenerator { ir, context }.into(), - } - } -} diff --git a/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_boxed.rs b/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_boxed.rs deleted file mode 100644 index 84c2b3675..000000000 --- a/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_boxed.rs +++ /dev/null @@ -1,45 +0,0 @@ -use crate::generator::dart::gen_wire2api_simple_type_cast; -use crate::generator::dart::ty::*; -use crate::ir::IrType::{EnumRef, Primitive, StructRef}; -use crate::ir::*; -use crate::type_dart_generator_struct; - -type_dart_generator_struct!(TypeBoxedGenerator, IrTypeBoxed); - -impl TypeDartGeneratorTrait for TypeBoxedGenerator<'_> { - fn api2wire_body(&self) -> Option { - Some(match &*self.ir.inner { - Primitive(_) => { - format!("return inner.new_{}(raw);", self.ir.safe_ident()) - } - inner => { - format!( - "final ptr = inner.new_{}(); - _api_fill_to_wire_{}(raw, ptr.ref); - return ptr;", - self.ir.safe_ident(), - inner.safe_ident(), - ) - } - }) - } - - fn api_fill_to_wire_body(&self) -> Option { - if !matches!(*self.ir.inner, Primitive(_)) { - Some(format!( - " _api_fill_to_wire_{}(apiObj, wireObj.ref);", - self.ir.inner.safe_ident() - )) - } else { - None - } - } - - fn wire2api_body(&self) -> String { - match &*self.ir.inner { - StructRef(inner) => format!("return _wire2api_{}(raw);", inner.safe_ident()), - EnumRef(inner) => format!("return _wire2api_{}(raw);", inner.safe_ident()), - _ => gen_wire2api_simple_type_cast(&self.ir.dart_api_type()), - } - } -} diff --git a/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_delegate.rs b/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_delegate.rs deleted file mode 100644 index b585ff3f7..000000000 --- a/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_delegate.rs +++ /dev/null @@ -1,42 +0,0 @@ -use crate::generator::dart::gen_wire2api_simple_type_cast; -use crate::generator::dart::ty::*; -use crate::ir::*; -use crate::type_dart_generator_struct; - -type_dart_generator_struct!(TypeDelegateGenerator, IrTypeDelegate); - -impl TypeDartGeneratorTrait for TypeDelegateGenerator<'_> { - fn api2wire_body(&self) -> Option { - Some(match self.ir { - IrTypeDelegate::String => { - "return _api2wire_uint_8_list(utf8.encoder.convert(raw));".to_string() - } - IrTypeDelegate::SyncReturnVecU8 => "/*unsupported*/".to_string(), - IrTypeDelegate::ZeroCopyBufferVecPrimitive(_) => { - format!( - "return _api2wire_{}(raw);", - self.ir.get_delegate().safe_ident() - ) - } - IrTypeDelegate::StringList => "final ans = inner.new_StringList(raw.length); - for (var i = 0; i < raw.length; i++) { - ans.ref.ptr[i] = _api2wire_String(raw[i]); - } - return ans;" - .to_owned(), - }) - } - - fn wire2api_body(&self) -> String { - match &self.ir { - IrTypeDelegate::String - | IrTypeDelegate::SyncReturnVecU8 - | IrTypeDelegate::ZeroCopyBufferVecPrimitive(_) => { - gen_wire2api_simple_type_cast(&self.ir.dart_api_type()) - } - IrTypeDelegate::StringList => { - "return (raw as List).cast();".to_owned() - } - } - } -} diff --git a/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_enum.rs b/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_enum.rs deleted file mode 100644 index fc361b4c8..000000000 --- a/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_enum.rs +++ /dev/null @@ -1,207 +0,0 @@ -use crate::generator::dart::dart_comments; -use crate::generator::dart::ty::*; -use crate::ir::*; -use crate::type_dart_generator_struct; - -type_dart_generator_struct!(TypeEnumRefGenerator, IrTypeEnumRef); - -impl TypeDartGeneratorTrait for TypeEnumRefGenerator<'_> { - fn api2wire_body(&self) -> Option { - if !self.ir.is_struct { - Some("return raw.index;".to_owned()) - } else { - None - } - } - - fn api_fill_to_wire_body(&self) -> Option { - if self.ir.is_struct { - Some( - self.ir - .get(self.context.ir_file) - .variants() - .iter() - .enumerate() - .map(|(idx, variant)| { - if let IrVariantKind::Value = &variant.kind { - format!( - "if (apiObj is {}) {{ wireObj.tag = {}; return; }}", - variant.name, idx - ) - } else { - let r = format!("wireObj.kind.ref.{}.ref", variant.name); - let body: Vec<_> = match &variant.kind { - IrVariantKind::Struct(st) => st - .fields - .iter() - .map(|field| { - format!( - "{}.{} = _api2wire_{}(apiObj.{});", - r, - field.name.rust_style(), - field.ty.safe_ident(), - field.name.dart_style() - ) - }) - .collect(), - _ => unreachable!(), - }; - format!( - "if (apiObj is {0}) {{ - wireObj.tag = {1}; - wireObj.kind = inner.inflate_{2}_{0}(); - {3} - }}", - variant.name, - idx, - self.ir.name, - body.join("\n") - ) - } - }) - .collect::>() - .join("\n"), - ) - } else { - None - } - } - - fn wire2api_body(&self) -> String { - if self.ir.is_struct { - let enu = self.ir.get(self.context.ir_file); - let variants = enu - .variants() - .iter() - .enumerate() - .map(|(idx, variant)| { - let args = match &variant.kind { - IrVariantKind::Value => "".to_owned(), - IrVariantKind::Struct(st) => st - .fields - .iter() - .enumerate() - .map(|(idx, field)| { - let val = format!( - "_wire2api_{}(raw[{}]),", - field.ty.safe_ident(), - idx + 1 - ); - if st.is_fields_named { - format!("{}: {}", field.name.dart_style(), val) - } else { - val - } - }) - .collect::>() - .join(""), - }; - format!("case {}: return {}({});", idx, variant.name, args) - }) - .collect::>(); - format!( - "switch (raw[0]) {{ - {} - default: throw Exception(\"unreachable\"); - }}", - variants.join("\n"), - ) - } else { - format!("return {}.values[raw];", self.ir.name) - } - } - - fn structs(&self) -> String { - let src = self.ir.get(self.context.ir_file); - - let comments = dart_comments(&src.comments); - if src.is_struct() { - let variants = src - .variants() - .iter() - .map(|variant| { - let args = match &variant.kind { - IrVariantKind::Value => "".to_owned(), - IrVariantKind::Struct(IrStruct { - is_fields_named: false, - fields, - .. - }) => { - let types = fields.iter().map(|field| &field.ty).collect::>(); - let split = optional_boundary_index(&types); - let types = fields - .iter() - .map(|field| { - format!( - "{}{} {},", - dart_comments(&field.comments), - field.ty.dart_api_type(), - field.name.dart_style() - ) - }) - .collect::>(); - if let Some(idx) = split { - let before = &types[..idx]; - let after = &types[idx..]; - format!("{}[{}]", before.join(""), after.join("")) - } else { - types.join("") - } - } - IrVariantKind::Struct(st) => { - let fields = st - .fields - .iter() - .map(|field| { - format!( - "{}{}{} {},", - dart_comments(&field.comments), - field.ty.dart_required_modifier(), - field.ty.dart_api_type(), - field.name.dart_style() - ) - }) - .collect::>(); - format!("{{ {} }}", fields.join("")) - } - }; - format!( - "{}const factory {}.{}({}) = {};", - dart_comments(&variant.comments), - self.ir.name, - variant.name.dart_style(), - args, - variant.name.rust_style(), - ) - }) - .collect::>(); - format!( - "@freezed - class {0} with _${0} {{ - {1} - }}", - self.ir.name, - variants.join("\n") - ) - } else { - let variants = src - .variants() - .iter() - .map(|variant| { - format!( - "{}{},", - dart_comments(&variant.comments), - variant.name.rust_style() - ) - }) - .collect::>() - .join("\n"); - format!( - "{}enum {} {{ - {} - }}", - comments, self.ir.name, variants - ) - } - } -} diff --git a/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_general_list.rs b/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_general_list.rs deleted file mode 100644 index 000f7288f..000000000 --- a/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_general_list.rs +++ /dev/null @@ -1,27 +0,0 @@ -use crate::generator::dart::ty::*; -use crate::ir::*; -use crate::type_dart_generator_struct; - -type_dart_generator_struct!(TypeGeneralListGenerator, IrTypeGeneralList); - -impl TypeDartGeneratorTrait for TypeGeneralListGenerator<'_> { - fn api2wire_body(&self) -> Option { - // NOTE the memory strategy is same as PrimitiveList, see comments there. - Some(format!( - "final ans = inner.new_{}(raw.length); - for (var i = 0; i < raw.length; ++i) {{ - _api_fill_to_wire_{}(raw[i], ans.ref.ptr[i]); - }} - return ans;", - self.ir.safe_ident(), - self.ir.inner.safe_ident() - )) - } - - fn wire2api_body(&self) -> String { - format!( - "return (raw as List).map(_wire2api_{}).toList();", - self.ir.inner.safe_ident() - ) - } -} diff --git a/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_optional.rs b/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_optional.rs deleted file mode 100644 index 5b7e60d27..000000000 --- a/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_optional.rs +++ /dev/null @@ -1,31 +0,0 @@ -use crate::generator::dart::ty::*; -use crate::ir::*; -use crate::type_dart_generator_struct; - -type_dart_generator_struct!(TypeOptionalGenerator, IrTypeOptional); - -impl TypeDartGeneratorTrait for TypeOptionalGenerator<'_> { - fn api2wire_body(&self) -> Option { - Some(format!( - "return raw == null ? ffi.nullptr : _api2wire_{}(raw);", - self.ir.inner.safe_ident() - )) - } - - fn api_fill_to_wire_body(&self) -> Option { - if !self.ir.needs_initialization() || self.ir.is_list() { - return None; - } - Some(format!( - "if (apiObj != null) _api_fill_to_wire_{}(apiObj, wireObj);", - self.ir.inner.safe_ident() - )) - } - - fn wire2api_body(&self) -> String { - format!( - "return raw == null ? null : _wire2api_{}(raw);", - self.ir.inner.safe_ident() - ) - } -} diff --git a/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_primitive.rs b/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_primitive.rs deleted file mode 100644 index 0ed9aa686..000000000 --- a/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_primitive.rs +++ /dev/null @@ -1,22 +0,0 @@ -use crate::generator::dart::gen_wire2api_simple_type_cast; -use crate::generator::dart::ty::*; -use crate::ir::*; -use crate::type_dart_generator_struct; - -type_dart_generator_struct!(TypePrimitiveGenerator, IrTypePrimitive); - -impl TypeDartGeneratorTrait for TypePrimitiveGenerator<'_> { - fn api2wire_body(&self) -> Option { - Some(match self.ir { - IrTypePrimitive::Bool => "return raw ? 1 : 0;".to_owned(), - _ => "return raw;".to_string(), - }) - } - - fn wire2api_body(&self) -> String { - match self.ir { - IrTypePrimitive::Unit => "return;".to_owned(), - _ => gen_wire2api_simple_type_cast(&self.ir.dart_api_type()), - } - } -} diff --git a/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_primitive_list.rs b/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_primitive_list.rs deleted file mode 100644 index d07c24d6b..000000000 --- a/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_primitive_list.rs +++ /dev/null @@ -1,30 +0,0 @@ -use crate::generator::dart::gen_wire2api_simple_type_cast; -use crate::generator::dart::ty::*; -use crate::ir::*; -use crate::type_dart_generator_struct; - -type_dart_generator_struct!(TypePrimitiveListGenerator, IrTypePrimitiveList); - -impl TypeDartGeneratorTrait for TypePrimitiveListGenerator<'_> { - fn api2wire_body(&self) -> Option { - // NOTE Dart code *only* allocates memory. It never *release* memory by itself. - // Instead, Rust receives that pointer and now it is in control of Rust. - // Therefore, *never* continue to use this pointer after you have passed the pointer - // to Rust. - // NOTE WARN: Never use the [calloc] provided by Dart FFI to allocate any memory. - // Instead, ask Rust to allocate some memory and return raw pointers. Otherwise, - // memory will be allocated in one dylib (e.g. libflutter.so), and then be released - // by another dylib (e.g. my_rust_code.so), especially in Android platform. It can be - // undefined behavior. - Some(format!( - "final ans = inner.new_{}(raw.length); - ans.ref.ptr.asTypedList(raw.length).setAll(0, raw); - return ans;", - self.ir.safe_ident(), - )) - } - - fn wire2api_body(&self) -> String { - gen_wire2api_simple_type_cast(&self.ir.dart_api_type()) - } -} diff --git a/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_struct.rs b/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_struct.rs deleted file mode 100644 index fa67bd32a..000000000 --- a/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_struct.rs +++ /dev/null @@ -1,135 +0,0 @@ -use crate::generator::dart::ty::*; -use crate::generator::dart::{dart_comments, dart_metadata}; -use crate::ir::*; -use crate::type_dart_generator_struct; - -type_dart_generator_struct!(TypeStructRefGenerator, IrTypeStructRef); - -impl TypeDartGeneratorTrait for TypeStructRefGenerator<'_> { - fn api2wire_body(&self) -> Option { - None - } - - fn api_fill_to_wire_body(&self) -> Option { - let s = self.ir.get(self.context.ir_file); - Some( - s.fields - .iter() - .map(|field| { - format!( - "wireObj.{} = _api2wire_{}(apiObj.{});", - field.name.rust_style(), - field.ty.safe_ident(), - field.name.dart_style() - ) - }) - .collect::>() - .join("\n"), - ) - } - - fn wire2api_body(&self) -> String { - let s = self.ir.get(self.context.ir_file); - let inner = s - .fields - .iter() - .enumerate() - .map(|(idx, field)| { - format!( - "{}: _wire2api_{}(arr[{}]),", - field.name.dart_style(), - field.ty.safe_ident(), - idx - ) - }) - .collect::>() - .join("\n"); - - format!( - "final arr = raw as List; - if (arr.length != {}) throw Exception('unexpected arr length: expect {} but see ${{arr.length}}'); - return {}({});", - s.fields.len(), - s.fields.len(), - s.name, inner, - ) - } - - fn structs(&self) -> String { - let src = self.ir.get(self.context.ir_file); - let comments = dart_comments(&src.comments); - let metadata = dart_metadata(&src.dart_metadata); - - if src.using_freezed() { - let constructor_params = src - .fields - .iter() - .map(|f| { - format!( - "{} {} {},", - f.ty.dart_required_modifier(), - f.ty.dart_api_type(), - f.name.dart_style() - ) - }) - .collect::>() - .join(""); - - format!( - "{}{}class {} with _${} {{ - const factory {}({{{}}}) = _{}; - }}", - comments, - metadata, - self.ir.name, - self.ir.name, - self.ir.name, - constructor_params, - self.ir.name - ) - } else { - let field_declarations = src - .fields - .iter() - .map(|f| { - let comments = dart_comments(&f.comments); - format!( - "{}{} {} {};", - comments, - if f.is_final { "final" } else { "" }, - f.ty.dart_api_type(), - f.name.dart_style() - ) - }) - .collect::>() - .join("\n"); - - let constructor_params = src - .fields - .iter() - .map(|f| { - format!( - "{}this.{},", - f.ty.dart_required_modifier(), - f.name.dart_style() - ) - }) - .collect::>() - .join(""); - - format!( - "{}{}class {} {{ - {} - - {}({{{}}}); - }}", - comments, - metadata, - self.ir.name, - field_declarations, - self.ir.name, - constructor_params - ) - } - } -} diff --git a/libs/flutter_rust_bridge_codegen/src/generator/mod.rs b/libs/flutter_rust_bridge_codegen/src/generator/mod.rs deleted file mode 100644 index 3891c02e3..000000000 --- a/libs/flutter_rust_bridge_codegen/src/generator/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod c; -pub mod dart; -pub mod rust; diff --git a/libs/flutter_rust_bridge_codegen/src/generator/rust/mod.rs b/libs/flutter_rust_bridge_codegen/src/generator/rust/mod.rs deleted file mode 100644 index 0b0d1df88..000000000 --- a/libs/flutter_rust_bridge_codegen/src/generator/rust/mod.rs +++ /dev/null @@ -1,481 +0,0 @@ -mod ty; -mod ty_boxed; -mod ty_delegate; -mod ty_enum; -mod ty_general_list; -mod ty_optional; -mod ty_primitive; -mod ty_primitive_list; -mod ty_struct; - -pub use ty::*; -pub use ty_boxed::*; -pub use ty_delegate::*; -pub use ty_enum::*; -pub use ty_general_list::*; -pub use ty_optional::*; -pub use ty_primitive::*; -pub use ty_primitive_list::*; -pub use ty_struct::*; - -use std::collections::HashSet; - -use crate::ir::IrType::*; -use crate::ir::*; -use crate::others::*; - -pub const HANDLER_NAME: &str = "FLUTTER_RUST_BRIDGE_HANDLER"; - -pub struct Output { - pub code: String, - pub extern_func_names: Vec, -} - -pub fn generate(ir_file: &IrFile, rust_wire_mod: &str) -> Output { - let mut generator = Generator::new(); - let code = generator.generate(ir_file, rust_wire_mod); - - Output { - code, - extern_func_names: generator.extern_func_collector.names, - } -} - -struct Generator { - extern_func_collector: ExternFuncCollector, -} - -impl Generator { - fn new() -> Self { - Self { - extern_func_collector: ExternFuncCollector::new(), - } - } - - fn generate(&mut self, ir_file: &IrFile, rust_wire_mod: &str) -> String { - let mut lines: Vec = vec![]; - - let distinct_input_types = ir_file.distinct_types(true, false); - let distinct_output_types = ir_file.distinct_types(false, true); - - lines.push(r#"#![allow(non_camel_case_types, unused, clippy::redundant_closure, clippy::useless_conversion, clippy::unit_arg, clippy::double_parens, non_snake_case)]"#.to_string()); - lines.push(CODE_HEADER.to_string()); - - lines.push(String::new()); - lines.push(format!("use crate::{}::*;", rust_wire_mod)); - lines.push("use flutter_rust_bridge::*;".to_string()); - lines.push(String::new()); - - lines.push(self.section_header_comment("imports")); - lines.extend(self.generate_imports( - ir_file, - rust_wire_mod, - &distinct_input_types, - &distinct_output_types, - )); - lines.push(String::new()); - - lines.push(self.section_header_comment("wire functions")); - lines.extend( - ir_file - .funcs - .iter() - .map(|f| self.generate_wire_func(f, ir_file)), - ); - - lines.push(self.section_header_comment("wire structs")); - lines.extend( - distinct_input_types - .iter() - .map(|ty| self.generate_wire_struct(ty, ir_file)), - ); - lines.extend( - distinct_input_types - .iter() - .map(|ty| TypeRustGenerator::new(ty.clone(), ir_file).structs()), - ); - - lines.push(self.section_header_comment("wrapper structs")); - lines.extend( - distinct_output_types - .iter() - .filter_map(|ty| self.generate_wrapper_struct(ty, ir_file)), - ); - lines.push(self.section_header_comment("static checks")); - let static_checks: Vec<_> = distinct_output_types - .iter() - .filter_map(|ty| self.generate_static_checks(ty, ir_file)) - .collect(); - if !static_checks.is_empty() { - lines.push("const _: fn() = || {".to_owned()); - lines.extend(static_checks); - lines.push("};".to_owned()); - } - - lines.push(self.section_header_comment("allocate functions")); - lines.extend( - distinct_input_types - .iter() - .map(|f| self.generate_allocate_funcs(f, ir_file)), - ); - - lines.push(self.section_header_comment("impl Wire2Api")); - lines.push(self.generate_wire2api_misc().to_string()); - lines.extend( - distinct_input_types - .iter() - .map(|ty| self.generate_wire2api_func(ty, ir_file)), - ); - - lines.push(self.section_header_comment("impl NewWithNullPtr")); - lines.push(self.generate_new_with_nullptr_misc().to_string()); - lines.extend( - distinct_input_types - .iter() - .map(|ty| self.generate_new_with_nullptr_func(ty, ir_file)), - ); - - lines.push(self.section_header_comment("impl IntoDart")); - lines.extend( - distinct_output_types - .iter() - .map(|ty| self.generate_impl_intodart(ty, ir_file)), - ); - - lines.push(self.section_header_comment("executor")); - lines.push(self.generate_executor(ir_file)); - - lines.push(self.section_header_comment("sync execution mode utility")); - lines.push(self.generate_sync_execution_mode_utility()); - - lines.join("\n") - } - - fn section_header_comment(&self, section_name: &str) -> String { - format!("// Section: {}\n", section_name) - } - - fn generate_imports( - &self, - ir_file: &IrFile, - rust_wire_mod: &str, - distinct_input_types: &[IrType], - distinct_output_types: &[IrType], - ) -> impl Iterator { - let input_type_imports = distinct_input_types - .iter() - .map(|api_type| generate_import(api_type, ir_file)); - let output_type_imports = distinct_output_types - .iter() - .map(|api_type| generate_import(api_type, ir_file)); - - input_type_imports - .chain(output_type_imports) - // Filter out `None` and unwrap - .flatten() - // Don't include imports from the API file - .filter(|import| !import.starts_with(&format!("use crate::{}::", rust_wire_mod))) - // de-duplicate - .collect::>() - .into_iter() - } - - fn generate_executor(&mut self, ir_file: &IrFile) -> String { - if ir_file.has_executor { - "/* nothing since executor detected */".to_string() - } else { - format!( - "support::lazy_static! {{ - pub static ref {}: support::DefaultHandler = Default::default(); - }} - ", - HANDLER_NAME - ) - } - } - - fn generate_sync_execution_mode_utility(&mut self) -> String { - self.extern_func_collector.generate( - "free_WireSyncReturnStruct", - &["val: support::WireSyncReturnStruct"], - None, - "unsafe { let _ = support::vec_from_leak_ptr(val.ptr, val.len); }", - ) - } - - fn generate_wire_func(&mut self, func: &IrFunc, ir_file: &IrFile) -> String { - let params = [ - if func.mode.has_port_argument() { - vec!["port_: i64".to_string()] - } else { - vec![] - }, - func.inputs - .iter() - .map(|field| { - format!( - "{}: {}{}", - field.name.rust_style(), - field.ty.rust_wire_modifier(), - field.ty.rust_wire_type() - ) - }) - .collect::>(), - ] - .concat(); - - let inner_func_params = [ - match func.mode { - IrFuncMode::Normal | IrFuncMode::Sync => vec![], - IrFuncMode::Stream => vec!["task_callback.stream_sink()".to_string()], - }, - func.inputs - .iter() - .map(|field| format!("api_{}", field.name.rust_style())) - .collect::>(), - ] - .concat(); - - let wrap_info_obj = format!( - "WrapInfo{{ debug_name: \"{}\", port: {}, mode: FfiCallMode::{} }}", - func.name, - if func.mode.has_port_argument() { - "Some(port_)" - } else { - "None" - }, - func.mode.ffi_call_mode(), - ); - - let code_wire2api = func - .inputs - .iter() - .map(|field| { - format!( - "let api_{} = {}.wire2api();", - field.name.rust_style(), - field.name.rust_style() - ) - }) - .collect::>() - .join(""); - - let code_call_inner_func = TypeRustGenerator::new(func.output.clone(), ir_file) - .wrap_obj(format!("{}({})", func.name, inner_func_params.join(", "))); - let code_call_inner_func_result = if func.fallible { - code_call_inner_func - } else { - format!("Ok({})", code_call_inner_func) - }; - - let (handler_func_name, return_type, code_closure) = match func.mode { - IrFuncMode::Sync => ( - "wrap_sync", - Some("support::WireSyncReturnStruct"), - format!( - "{} - {}", - code_wire2api, code_call_inner_func_result, - ), - ), - IrFuncMode::Normal | IrFuncMode::Stream => ( - "wrap", - None, - format!( - "{} - move |task_callback| {} - ", - code_wire2api, code_call_inner_func_result, - ), - ), - }; - - self.extern_func_collector.generate( - &func.wire_func_name(), - ¶ms - .iter() - .map(std::ops::Deref::deref) - .collect::>(), - return_type, - &format!( - " - {}.{}({}, move || {{ - {} - }}) - ", - HANDLER_NAME, handler_func_name, wrap_info_obj, code_closure, - ), - ) - } - - fn generate_wire_struct(&mut self, ty: &IrType, ir_file: &IrFile) -> String { - // println!("generate_wire_struct: {:?}", ty); - if let Some(fields) = TypeRustGenerator::new(ty.clone(), ir_file).wire_struct_fields() { - format!( - r###" - #[repr(C)] - #[derive(Clone)] - pub struct {} {{ - {} - }} - "###, - ty.rust_wire_type(), - fields.join(",\n"), - ) - } else { - "".to_string() - } - } - - fn generate_allocate_funcs(&mut self, ty: &IrType, ir_file: &IrFile) -> String { - // println!("generate_allocate_funcs: {:?}", ty); - TypeRustGenerator::new(ty.clone(), ir_file).allocate_funcs(&mut self.extern_func_collector) - } - - fn generate_wire2api_misc(&self) -> &'static str { - r"pub trait Wire2Api { - fn wire2api(self) -> T; - } - - impl Wire2Api> for *mut S - where - *mut S: Wire2Api - { - fn wire2api(self) -> Option { - if self.is_null() { - None - } else { - Some(self.wire2api()) - } - } - } - " - } - - fn generate_wire2api_func(&mut self, ty: &IrType, ir_file: &IrFile) -> String { - // println!("generate_wire2api_func: {:?}", ty); - if let Some(body) = TypeRustGenerator::new(ty.clone(), ir_file).wire2api_body() { - format!( - "impl Wire2Api<{}> for {} {{ - fn wire2api(self) -> {} {{ - {} - }} - }} - ", - ty.rust_api_type(), - ty.rust_wire_modifier() + &ty.rust_wire_type(), - ty.rust_api_type(), - body, - ) - } else { - "".to_string() - } - } - - fn generate_static_checks(&mut self, ty: &IrType, ir_file: &IrFile) -> Option { - TypeRustGenerator::new(ty.clone(), ir_file).static_checks() - } - - fn generate_wrapper_struct(&mut self, ty: &IrType, ir_file: &IrFile) -> Option { - match ty { - IrType::StructRef(_) | IrType::EnumRef(_) => { - TypeRustGenerator::new(ty.clone(), ir_file) - .wrapper_struct() - .map(|wrapper| { - format!( - r###" - #[derive(Clone)] - struct {}({}); - "###, - wrapper, - ty.rust_api_type(), - ) - }) - } - _ => None, - } - } - - fn generate_new_with_nullptr_misc(&self) -> &'static str { - "pub trait NewWithNullPtr { - fn new_with_null_ptr() -> Self; - } - - impl NewWithNullPtr for *mut T { - fn new_with_null_ptr() -> Self { - std::ptr::null_mut() - } - } - " - } - - fn generate_new_with_nullptr_func(&mut self, ty: &IrType, ir_file: &IrFile) -> String { - TypeRustGenerator::new(ty.clone(), ir_file) - .new_with_nullptr(&mut self.extern_func_collector) - } - - fn generate_impl_intodart(&mut self, ty: &IrType, ir_file: &IrFile) -> String { - // println!("generate_impl_intodart: {:?}", ty); - TypeRustGenerator::new(ty.clone(), ir_file).impl_intodart() - } -} - -pub fn generate_import(api_type: &IrType, ir_file: &IrFile) -> Option { - TypeRustGenerator::new(api_type.clone(), ir_file).imports() -} - -pub fn generate_list_allocate_func( - collector: &mut ExternFuncCollector, - safe_ident: &str, - list: &impl IrTypeTrait, - inner: &IrType, -) -> String { - collector.generate( - &format!("new_{}", safe_ident), - &["len: i32"], - Some(&[ - list.rust_wire_modifier().as_str(), - list.rust_wire_type().as_str() - ].concat()), - &format!( - "let wrap = {} {{ ptr: support::new_leak_vec_ptr(<{}{}>::new_with_null_ptr(), len), len }}; - support::new_leak_box_ptr(wrap)", - list.rust_wire_type(), - inner.rust_ptr_modifier(), - inner.rust_wire_type() - ), - ) -} - -pub struct ExternFuncCollector { - names: Vec, -} - -impl ExternFuncCollector { - fn new() -> Self { - ExternFuncCollector { names: vec![] } - } - - fn generate( - &mut self, - func_name: &str, - params: &[&str], - return_type: Option<&str>, - body: &str, - ) -> String { - self.names.push(func_name.to_string()); - - format!( - r#" - #[no_mangle] - pub extern "C" fn {}({}) {} {{ - {} - }} - "#, - func_name, - params.join(", "), - return_type.map_or("".to_string(), |r| format!("-> {}", r)), - body, - ) - } -} diff --git a/libs/flutter_rust_bridge_codegen/src/generator/rust/ty.rs b/libs/flutter_rust_bridge_codegen/src/generator/rust/ty.rs deleted file mode 100644 index 827d6b8f1..000000000 --- a/libs/flutter_rust_bridge_codegen/src/generator/rust/ty.rs +++ /dev/null @@ -1,96 +0,0 @@ -use crate::generator::rust::*; -use enum_dispatch::enum_dispatch; - -#[enum_dispatch] -pub trait TypeRustGeneratorTrait { - fn wire2api_body(&self) -> Option; - - fn wire_struct_fields(&self) -> Option> { - None - } - - fn static_checks(&self) -> Option { - None - } - - fn wrapper_struct(&self) -> Option { - None - } - - fn self_access(&self, obj: String) -> String { - obj - } - - fn wrap_obj(&self, obj: String) -> String { - obj - } - - fn convert_to_dart(&self, obj: String) -> String { - format!("{}.into_dart()", obj) - } - - fn structs(&self) -> String { - "".to_string() - } - - fn allocate_funcs(&self, _collector: &mut ExternFuncCollector) -> String { - "".to_string() - } - - fn impl_intodart(&self) -> String { - "".to_string() - } - - fn new_with_nullptr(&self, _collector: &mut ExternFuncCollector) -> String { - "".to_string() - } - - fn imports(&self) -> Option { - None - } -} - -#[derive(Debug, Clone)] -pub struct TypeGeneratorContext<'a> { - pub ir_file: &'a IrFile, -} - -#[macro_export] -macro_rules! type_rust_generator_struct { - ($cls:ident, $ir_cls:ty) => { - #[derive(Debug, Clone)] - pub struct $cls<'a> { - pub ir: $ir_cls, - pub context: TypeGeneratorContext<'a>, - } - }; -} - -#[enum_dispatch(TypeRustGeneratorTrait)] -#[derive(Debug, Clone)] -pub enum TypeRustGenerator<'a> { - Primitive(TypePrimitiveGenerator<'a>), - Delegate(TypeDelegateGenerator<'a>), - PrimitiveList(TypePrimitiveListGenerator<'a>), - Optional(TypeOptionalGenerator<'a>), - GeneralList(TypeGeneralListGenerator<'a>), - StructRef(TypeStructRefGenerator<'a>), - Boxed(TypeBoxedGenerator<'a>), - EnumRef(TypeEnumRefGenerator<'a>), -} - -impl<'a> TypeRustGenerator<'a> { - pub fn new(ty: IrType, ir_file: &'a IrFile) -> Self { - let context = TypeGeneratorContext { ir_file }; - match ty { - Primitive(ir) => TypePrimitiveGenerator { ir, context }.into(), - Delegate(ir) => TypeDelegateGenerator { ir, context }.into(), - PrimitiveList(ir) => TypePrimitiveListGenerator { ir, context }.into(), - Optional(ir) => TypeOptionalGenerator { ir, context }.into(), - GeneralList(ir) => TypeGeneralListGenerator { ir, context }.into(), - StructRef(ir) => TypeStructRefGenerator { ir, context }.into(), - Boxed(ir) => TypeBoxedGenerator { ir, context }.into(), - EnumRef(ir) => TypeEnumRefGenerator { ir, context }.into(), - } - } -} diff --git a/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_boxed.rs b/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_boxed.rs deleted file mode 100644 index ab6d25d02..000000000 --- a/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_boxed.rs +++ /dev/null @@ -1,62 +0,0 @@ -use crate::generator::rust::ty::*; -use crate::generator::rust::{generate_import, ExternFuncCollector}; -use crate::ir::IrType::Primitive; -use crate::ir::*; -use crate::type_rust_generator_struct; - -type_rust_generator_struct!(TypeBoxedGenerator, IrTypeBoxed); - -impl TypeRustGeneratorTrait for TypeBoxedGenerator<'_> { - fn wire2api_body(&self) -> Option { - let IrTypeBoxed { - inner: box_inner, - exist_in_real_api, - } = &self.ir; - Some(match (box_inner.as_ref(), exist_in_real_api) { - (IrType::Primitive(_), false) => "unsafe { *support::box_from_leak_ptr(self) }".into(), - (IrType::Primitive(_), true) => "unsafe { support::box_from_leak_ptr(self) }".into(), - _ => { - "let wrap = unsafe { support::box_from_leak_ptr(self) }; (*wrap).wire2api().into()" - .into() - } - }) - } - - fn wrapper_struct(&self) -> Option { - let src = TypeRustGenerator::new(*self.ir.inner.clone(), self.context.ir_file); - src.wrapper_struct() - } - - fn self_access(&self, obj: String) -> String { - format!("(*{})", obj) - } - - fn wrap_obj(&self, obj: String) -> String { - let src = TypeRustGenerator::new(*self.ir.inner.clone(), self.context.ir_file); - src.wrap_obj(self.self_access(obj)) - } - - fn allocate_funcs(&self, collector: &mut ExternFuncCollector) -> String { - match &*self.ir.inner { - Primitive(prim) => collector.generate( - &format!("new_{}", self.ir.safe_ident()), - &[&format!("value: {}", prim.rust_wire_type())], - Some(&format!("*mut {}", prim.rust_wire_type())), - "support::new_leak_box_ptr(value)", - ), - inner => collector.generate( - &format!("new_{}", self.ir.safe_ident()), - &[], - Some(&[self.ir.rust_wire_modifier(), self.ir.rust_wire_type()].concat()), - &format!( - "support::new_leak_box_ptr({}::new_with_null_ptr())", - inner.rust_wire_type() - ), - ), - } - } - - fn imports(&self) -> Option { - generate_import(&self.ir.inner, self.context.ir_file) - } -} diff --git a/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_delegate.rs b/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_delegate.rs deleted file mode 100644 index 9b67ba7dd..000000000 --- a/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_delegate.rs +++ /dev/null @@ -1,45 +0,0 @@ -use crate::generator::rust::ty::*; -use crate::generator::rust::{ - generate_list_allocate_func, ExternFuncCollector, TypeGeneralListGenerator, -}; -use crate::ir::*; -use crate::type_rust_generator_struct; - -type_rust_generator_struct!(TypeDelegateGenerator, IrTypeDelegate); - -impl TypeRustGeneratorTrait for TypeDelegateGenerator<'_> { - fn wire2api_body(&self) -> Option { - Some(match &self.ir { - IrTypeDelegate::String => "let vec: Vec = self.wire2api(); - String::from_utf8_lossy(&vec).into_owned()" - .into(), - IrTypeDelegate::SyncReturnVecU8 => "/*unsupported*/".into(), - IrTypeDelegate::ZeroCopyBufferVecPrimitive(_) => { - "ZeroCopyBuffer(self.wire2api())".into() - } - IrTypeDelegate::StringList => TypeGeneralListGenerator::WIRE2API_BODY.to_string(), - }) - } - - fn wire_struct_fields(&self) -> Option> { - match &self.ir { - ty @ IrTypeDelegate::StringList => Some(vec![ - format!("ptr: *mut *mut {}", ty.get_delegate().rust_wire_type()), - "len: i32".to_owned(), - ]), - _ => None, - } - } - - fn allocate_funcs(&self, collector: &mut ExternFuncCollector) -> String { - match &self.ir { - list @ IrTypeDelegate::StringList => generate_list_allocate_func( - collector, - &self.ir.safe_ident(), - list, - &list.get_delegate(), - ), - _ => "".to_string(), - } - } -} diff --git a/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_enum.rs b/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_enum.rs deleted file mode 100644 index a0fc42ddb..000000000 --- a/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_enum.rs +++ /dev/null @@ -1,343 +0,0 @@ -use crate::generator::rust::ty::*; -use crate::generator::rust::ExternFuncCollector; -use crate::ir::*; -use crate::type_rust_generator_struct; - -type_rust_generator_struct!(TypeEnumRefGenerator, IrTypeEnumRef); - -impl TypeRustGeneratorTrait for TypeEnumRefGenerator<'_> { - fn wire2api_body(&self) -> Option { - let enu = self.ir.get(self.context.ir_file); - Some(if self.ir.is_struct { - let variants = enu - .variants() - .iter() - .enumerate() - .map(|(idx, variant)| match &variant.kind { - IrVariantKind::Value => { - format!("{} => {}::{},", idx, enu.name, variant.name) - } - IrVariantKind::Struct(st) => { - let fields: Vec<_> = st - .fields - .iter() - .map(|field| { - if st.is_fields_named { - format!("{0}: ans.{0}.wire2api()", field.name.rust_style()) - } else { - format!("ans.{}.wire2api()", field.name.rust_style()) - } - }) - .collect(); - let (left, right) = st.brackets_pair(); - format!( - "{} => unsafe {{ - let ans = support::box_from_leak_ptr(self.kind); - let ans = support::box_from_leak_ptr(ans.{2}); - {}::{2}{3}{4}{5} - }}", - idx, - enu.name, - variant.name, - left, - fields.join(","), - right - ) - } - }) - .collect::>(); - format!( - "match self.tag {{ - {} - _ => unreachable!(), - }}", - variants.join("\n"), - ) - } else { - let variants = enu - .variants() - .iter() - .enumerate() - .map(|(idx, variant)| format!("{} => {}::{},", idx, enu.name, variant.name)) - .collect::>() - .join("\n"); - format!( - "match self {{ - {} - _ => unreachable!(\"Invalid variant for {}: {{}}\", self), - }}", - variants, enu.name - ) - }) - } - - fn structs(&self) -> String { - let src = self.ir.get(self.context.ir_file); - if !src.is_struct() { - return "".to_owned(); - } - let variant_structs = src - .variants() - .iter() - .map(|variant| { - let fields = match &variant.kind { - IrVariantKind::Value => vec![], - IrVariantKind::Struct(s) => s - .fields - .iter() - .map(|field| { - format!( - "{}: {}{},", - field.name.rust_style(), - field.ty.rust_wire_modifier(), - field.ty.rust_wire_type() - ) - }) - .collect(), - }; - format!( - "#[repr(C)] - #[derive(Clone)] - pub struct {}_{} {{ {} }}", - self.ir.name, - variant.name, - fields.join("\n") - ) - }) - .collect::>(); - let union_fields = src - .variants() - .iter() - .map(|variant| format!("{0}: *mut {1}_{0},", variant.name, self.ir.name)) - .collect::>(); - format!( - "#[repr(C)] - #[derive(Clone)] - pub struct {0} {{ tag: i32, kind: *mut {1}Kind }} - - #[repr(C)] - pub union {1}Kind {{ - {2} - }} - - {3}", - self.ir.rust_wire_type(), - self.ir.name, - union_fields.join("\n"), - variant_structs.join("\n\n") - ) - } - - fn static_checks(&self) -> Option { - let src = self.ir.get(self.context.ir_file); - src.wrapper_name.as_ref()?; - - let branches: Vec<_> = src - .variants() - .iter() - .map(|variant| match &variant.kind { - IrVariantKind::Value => format!("{}::{} => {{}}", src.name, variant.name), - IrVariantKind::Struct(s) => { - let pattern = s - .fields - .iter() - .map(|field| field.name.rust_style().to_owned()) - .collect::>(); - let pattern = if s.is_fields_named { - format!("{}::{} {{ {} }}", src.name, variant.name, pattern.join(",")) - } else { - format!("{}::{}({})", src.name, variant.name, pattern.join(",")) - }; - let checks = s - .fields - .iter() - .map(|field| { - format!( - "let _: {} = {};\n", - field.ty.rust_api_type(), - field.name.rust_style(), - ) - }) - .collect::>(); - format!("{} => {{ {} }}", pattern, checks.join("")) - } - }) - .collect(); - Some(format!( - "match None::<{}>.unwrap() {{ {} }}", - src.name, - branches.join(","), - )) - } - - fn wrapper_struct(&self) -> Option { - let src = self.ir.get(self.context.ir_file); - src.wrapper_name.as_ref().cloned() - } - - fn self_access(&self, obj: String) -> String { - let src = self.ir.get(self.context.ir_file); - match &src.wrapper_name { - Some(_) => format!("{}.0", obj), - None => obj, - } - } - - fn wrap_obj(&self, obj: String) -> String { - match self.wrapper_struct() { - Some(wrapper) => format!("{}({})", wrapper, obj), - None => obj, - } - } - - fn impl_intodart(&self) -> String { - let src = self.ir.get(self.context.ir_file); - - let (name, self_path): (&str, &str) = match &src.wrapper_name { - Some(wrapper) => (wrapper, &src.name), - None => (&src.name, "Self"), - }; - let self_ref = self.self_access("self".to_owned()); - if self.ir.is_struct { - let variants = src - .variants() - .iter() - .enumerate() - .map(|(idx, variant)| { - let tag = format!("{}.into_dart()", idx); - match &variant.kind { - IrVariantKind::Value => { - format!("{}::{} => vec![{}],", self_path, variant.name, tag) - } - IrVariantKind::Struct(s) => { - let fields = Some(tag) - .into_iter() - .chain(s.fields.iter().map(|field| { - let gen = TypeRustGenerator::new( - field.ty.clone(), - self.context.ir_file, - ); - gen.convert_to_dart(field.name.rust_style().to_owned()) - })) - .collect::>(); - let pattern = s - .fields - .iter() - .map(|field| field.name.rust_style().to_owned()) - .collect::>(); - let (left, right) = s.brackets_pair(); - format!( - "{}::{}{}{}{} => vec![{}],", - self_path, - variant.name, - left, - pattern.join(","), - right, - fields.join(",") - ) - } - } - }) - .collect::>(); - format!( - "impl support::IntoDart for {} {{ - fn into_dart(self) -> support::DartCObject {{ - match {} {{ - {} - }}.into_dart() - }} - }} - impl support::IntoDartExceptPrimitive for {0} {{}} - ", - name, - self_ref, - variants.join("\n") - ) - } else { - let variants = src - .variants() - .iter() - .enumerate() - .map(|(idx, variant)| format!("{}::{} => {},", self_path, variant.name, idx)) - .collect::>() - .join("\n"); - format!( - "impl support::IntoDart for {} {{ - fn into_dart(self) -> support::DartCObject {{ - match {} {{ - {} - }}.into_dart() - }} - }} - ", - name, self_ref, variants - ) - } - } - - fn new_with_nullptr(&self, collector: &mut ExternFuncCollector) -> String { - if !self.ir.is_struct { - return "".to_string(); - } - - fn init_of(ty: &IrType) -> &str { - if ty.rust_wire_is_pointer() { - "core::ptr::null_mut()" - } else { - "Default::default()" - } - } - - let src = self.ir.get(self.context.ir_file); - - let inflators = src - .variants() - .iter() - .filter_map(|variant| { - let typ = format!("{}_{}", self.ir.name, variant.name); - let body: Vec<_> = if let IrVariantKind::Struct(st) = &variant.kind { - st.fields - .iter() - .map(|field| format!("{}: {}", field.name.rust_style(), init_of(&field.ty))) - .collect() - } else { - return None; - }; - Some(collector.generate( - &format!("inflate_{}", typ), - &[], - Some(&format!("*mut {}Kind", self.ir.name)), - &format!( - "support::new_leak_box_ptr({}Kind {{ - {}: support::new_leak_box_ptr({} {{ - {} - }}) - }})", - self.ir.name, - variant.name.rust_style(), - typ, - body.join(",") - ), - )) - }) - .collect::>(); - format!( - "impl NewWithNullPtr for {} {{ - fn new_with_null_ptr() -> Self {{ - Self {{ - tag: -1, - kind: core::ptr::null_mut(), - }} - }} - }} - {}", - self.ir.rust_wire_type(), - inflators.join("\n\n") - ) - } - - fn imports(&self) -> Option { - let api_enum = self.ir.get(self.context.ir_file); - Some(format!("use {};", api_enum.path.join("::"))) - } -} diff --git a/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_general_list.rs b/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_general_list.rs deleted file mode 100644 index 1e88a5867..000000000 --- a/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_general_list.rs +++ /dev/null @@ -1,55 +0,0 @@ -use crate::generator::rust::ty::*; -use crate::generator::rust::{generate_import, generate_list_allocate_func, ExternFuncCollector}; -use crate::ir::*; -use crate::type_rust_generator_struct; - -type_rust_generator_struct!(TypeGeneralListGenerator, IrTypeGeneralList); - -impl TypeGeneralListGenerator<'_> { - pub const WIRE2API_BODY: &'static str = " - let vec = unsafe { - let wrap = support::box_from_leak_ptr(self); - support::vec_from_leak_ptr(wrap.ptr, wrap.len) - }; - vec.into_iter().map(Wire2Api::wire2api).collect()"; -} - -impl TypeRustGeneratorTrait for TypeGeneralListGenerator<'_> { - fn wire2api_body(&self) -> Option { - Some(TypeGeneralListGenerator::WIRE2API_BODY.to_string()) - } - - fn wire_struct_fields(&self) -> Option> { - Some(vec![ - format!( - "ptr: *mut {}{}", - self.ir.inner.rust_ptr_modifier(), - self.ir.inner.rust_wire_type() - ), - "len: i32".to_string(), - ]) - } - - fn wrap_obj(&self, obj: String) -> String { - let inner = TypeRustGenerator::new(*self.ir.inner.clone(), self.context.ir_file); - inner - .wrapper_struct() - .map(|wrapper| { - format!( - "{}.into_iter().map(|v| {}({})).collect::>()", - obj, - wrapper, - inner.self_access("v".to_owned()) - ) - }) - .unwrap_or(obj) - } - - fn allocate_funcs(&self, collector: &mut ExternFuncCollector) -> String { - generate_list_allocate_func(collector, &self.ir.safe_ident(), &self.ir, &self.ir.inner) - } - - fn imports(&self) -> Option { - generate_import(&self.ir.inner, self.context.ir_file) - } -} diff --git a/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_optional.rs b/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_optional.rs deleted file mode 100644 index 4e13ee23c..000000000 --- a/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_optional.rs +++ /dev/null @@ -1,30 +0,0 @@ -use crate::generator::rust::generate_import; -use crate::generator::rust::ty::*; -use crate::ir::*; -use crate::type_rust_generator_struct; - -type_rust_generator_struct!(TypeOptionalGenerator, IrTypeOptional); - -impl TypeRustGeneratorTrait for TypeOptionalGenerator<'_> { - fn wire2api_body(&self) -> Option { - None - } - - fn convert_to_dart(&self, obj: String) -> String { - let inner = TypeRustGenerator::new(*self.ir.inner.clone(), self.context.ir_file); - let obj = match inner.wrapper_struct() { - Some(wrapper) => format!( - "{}.map(|v| {}({}))", - obj, - wrapper, - inner.self_access("v".to_owned()) - ), - None => obj, - }; - format!("{}.into_dart()", obj) - } - - fn imports(&self) -> Option { - generate_import(&self.ir.inner, self.context.ir_file) - } -} diff --git a/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_primitive.rs b/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_primitive.rs deleted file mode 100644 index 5fd3bb562..000000000 --- a/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_primitive.rs +++ /dev/null @@ -1,11 +0,0 @@ -use crate::generator::rust::ty::*; -use crate::ir::*; -use crate::type_rust_generator_struct; - -type_rust_generator_struct!(TypePrimitiveGenerator, IrTypePrimitive); - -impl TypeRustGeneratorTrait for TypePrimitiveGenerator<'_> { - fn wire2api_body(&self) -> Option { - Some("self".into()) - } -} diff --git a/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_primitive_list.rs b/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_primitive_list.rs deleted file mode 100644 index 3fa85f82c..000000000 --- a/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_primitive_list.rs +++ /dev/null @@ -1,42 +0,0 @@ -use crate::generator::rust::ty::*; -use crate::generator::rust::ExternFuncCollector; -use crate::ir::*; -use crate::type_rust_generator_struct; - -type_rust_generator_struct!(TypePrimitiveListGenerator, IrTypePrimitiveList); - -impl TypeRustGeneratorTrait for TypePrimitiveListGenerator<'_> { - fn wire2api_body(&self) -> Option { - Some( - "unsafe { - let wrap = support::box_from_leak_ptr(self); - support::vec_from_leak_ptr(wrap.ptr, wrap.len) - }" - .into(), - ) - } - - fn wire_struct_fields(&self) -> Option> { - Some(vec![ - format!("ptr: *mut {}", self.ir.primitive.rust_wire_type()), - "len: i32".to_string(), - ]) - } - - fn allocate_funcs(&self, collector: &mut ExternFuncCollector) -> String { - collector.generate( - &format!("new_{}", self.ir.safe_ident()), - &["len: i32"], - Some(&format!( - "{}{}", - self.ir.rust_wire_modifier(), - self.ir.rust_wire_type() - )), - &format!( - "let ans = {} {{ ptr: support::new_leak_vec_ptr(Default::default(), len), len }}; - support::new_leak_box_ptr(ans)", - self.ir.rust_wire_type(), - ), - ) - } -} diff --git a/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_struct.rs b/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_struct.rs deleted file mode 100644 index 021dd9f47..000000000 --- a/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_struct.rs +++ /dev/null @@ -1,185 +0,0 @@ -use crate::generator::rust::ty::*; -use crate::generator::rust::ExternFuncCollector; -use crate::ir::*; -use crate::type_rust_generator_struct; - -type_rust_generator_struct!(TypeStructRefGenerator, IrTypeStructRef); - -impl TypeRustGeneratorTrait for TypeStructRefGenerator<'_> { - fn wire2api_body(&self) -> Option { - let api_struct = self.ir.get(self.context.ir_file); - let fields_str = &api_struct - .fields - .iter() - .map(|field| { - format!( - "{} self.{}.wire2api()", - if api_struct.is_fields_named { - field.name.rust_style().to_string() + ": " - } else { - String::new() - }, - field.name.rust_style() - ) - }) - .collect::>() - .join(","); - - let (left, right) = api_struct.brackets_pair(); - Some(format!( - "{}{}{}{}", - self.ir.rust_api_type(), - left, - fields_str, - right - )) - } - - fn wire_struct_fields(&self) -> Option> { - let s = self.ir.get(self.context.ir_file); - Some( - s.fields - .iter() - .map(|field| { - format!( - "{}: {}{}", - field.name.rust_style(), - field.ty.rust_wire_modifier(), - field.ty.rust_wire_type() - ) - }) - .collect(), - ) - } - - fn static_checks(&self) -> Option { - let src = self.ir.get(self.context.ir_file); - src.wrapper_name.as_ref()?; - - let var = if src.is_fields_named { - src.name.clone() - } else { - // let bindings cannot shadow tuple structs - format!("{}_", src.name) - }; - let checks = src - .fields - .iter() - .enumerate() - .map(|(i, field)| { - format!( - "let _: {} = {}.{};\n", - field.ty.rust_api_type(), - var, - if src.is_fields_named { - field.name.to_string() - } else { - i.to_string() - }, - ) - }) - .collect::>() - .join(""); - Some(format!( - "{{ let {} = None::<{}>.unwrap(); {} }} ", - var, src.name, checks - )) - } - - fn wrapper_struct(&self) -> Option { - let src = self.ir.get(self.context.ir_file); - src.wrapper_name.as_ref().cloned() - } - - fn wrap_obj(&self, obj: String) -> String { - match self.wrapper_struct() { - Some(wrapper) => format!("{}({})", wrapper, obj), - None => obj, - } - } - - fn impl_intodart(&self) -> String { - let src = self.ir.get(self.context.ir_file); - - let unwrap = match &src.wrapper_name { - Some(_) => ".0", - None => "", - }; - let body = src - .fields - .iter() - .enumerate() - .map(|(i, field)| { - let field_ref = if src.is_fields_named { - field.name.rust_style().to_string() - } else { - i.to_string() - }; - let gen = TypeRustGenerator::new(field.ty.clone(), self.context.ir_file); - gen.convert_to_dart(gen.wrap_obj(format!("self{}.{}", unwrap, field_ref))) - }) - .collect::>() - .join(",\n"); - - let name = match &src.wrapper_name { - Some(wrapper) => wrapper, - None => &src.name, - }; - format!( - "impl support::IntoDart for {} {{ - fn into_dart(self) -> support::DartCObject {{ - vec![ - {} - ].into_dart() - }} - }} - impl support::IntoDartExceptPrimitive for {} {{}} - ", - name, body, name, - ) - } - - fn new_with_nullptr(&self, _collector: &mut ExternFuncCollector) -> String { - let src = self.ir.get(self.context.ir_file); - - let body = { - src.fields - .iter() - .map(|field| { - format!( - "{}: {},", - field.name.rust_style(), - if field.ty.rust_wire_is_pointer() { - "core::ptr::null_mut()" - } else { - "Default::default()" - } - ) - }) - .collect::>() - .join("\n") - }; - format!( - r#"impl NewWithNullPtr for {} {{ - fn new_with_null_ptr() -> Self {{ - Self {{ {} }} - }} - }} - "#, - self.ir.rust_wire_type(), - body, - ) - } - - fn imports(&self) -> Option { - let api_struct = self.ir.get(self.context.ir_file); - if api_struct.path.is_some() { - Some(format!( - "use {};", - api_struct.path.as_ref().unwrap().join("::") - )) - } else { - None - } - } -} diff --git a/libs/flutter_rust_bridge_codegen/src/ir/annotation.rs b/libs/flutter_rust_bridge_codegen/src/ir/annotation.rs deleted file mode 100644 index e67580142..000000000 --- a/libs/flutter_rust_bridge_codegen/src/ir/annotation.rs +++ /dev/null @@ -1,7 +0,0 @@ -use crate::ir::*; - -#[derive(Debug, Clone)] -pub struct IrDartAnnotation { - pub content: String, - pub library: Option, -} diff --git a/libs/flutter_rust_bridge_codegen/src/ir/comment.rs b/libs/flutter_rust_bridge_codegen/src/ir/comment.rs deleted file mode 100644 index e91af1602..000000000 --- a/libs/flutter_rust_bridge_codegen/src/ir/comment.rs +++ /dev/null @@ -1,26 +0,0 @@ -#[derive(Debug, Clone)] -pub struct IrComment(String); - -impl IrComment { - pub fn comment(&self) -> &str { - &self.0 - } -} - -impl From<&str> for IrComment { - fn from(input: &str) -> Self { - if input.contains('\n') { - // Dart's formatter has issues with block comments - // so we convert them ahead of time. - let formatted = input - .split('\n') - .into_iter() - .map(|e| format!("///{}", e)) - .collect::>() - .join("\n"); - Self(formatted) - } else { - Self(format!("///{}", input)) - } - } -} diff --git a/libs/flutter_rust_bridge_codegen/src/ir/field.rs b/libs/flutter_rust_bridge_codegen/src/ir/field.rs deleted file mode 100644 index 8c54bd54e..000000000 --- a/libs/flutter_rust_bridge_codegen/src/ir/field.rs +++ /dev/null @@ -1,9 +0,0 @@ -use crate::ir::*; - -#[derive(Debug, Clone)] -pub struct IrField { - pub ty: IrType, - pub name: IrIdent, - pub is_final: bool, - pub comments: Vec, -} diff --git a/libs/flutter_rust_bridge_codegen/src/ir/file.rs b/libs/flutter_rust_bridge_codegen/src/ir/file.rs deleted file mode 100644 index cbfec6723..000000000 --- a/libs/flutter_rust_bridge_codegen/src/ir/file.rs +++ /dev/null @@ -1,61 +0,0 @@ -use crate::ir::*; -use std::collections::{HashMap, HashSet}; - -pub type IrStructPool = HashMap; -pub type IrEnumPool = HashMap; - -#[derive(Debug, Clone)] -pub struct IrFile { - pub funcs: Vec, - pub struct_pool: IrStructPool, - pub enum_pool: IrEnumPool, - pub has_executor: bool, -} - -impl IrFile { - /// [f] returns [true] if it wants to stop going to the *children* of this subtree - pub fn visit_types bool>( - &self, - f: &mut F, - include_func_inputs: bool, - include_func_output: bool, - ) { - for func in &self.funcs { - if include_func_inputs { - for field in &func.inputs { - field.ty.visit_types(f, self); - } - } - if include_func_output { - func.output.visit_types(f, self); - } - } - } - - pub fn distinct_types( - &self, - include_func_inputs: bool, - include_func_output: bool, - ) -> Vec { - let mut seen_idents = HashSet::new(); - let mut ans = Vec::new(); - self.visit_types( - &mut |ty| { - let ident = ty.safe_ident(); - let contains = seen_idents.contains(&ident); - if !contains { - seen_idents.insert(ident); - ans.push(ty.clone()); - } - contains - }, - include_func_inputs, - include_func_output, - ); - - // make the output change less when input change - ans.sort_by_key(|ty| ty.safe_ident()); - - ans - } -} diff --git a/libs/flutter_rust_bridge_codegen/src/ir/func.rs b/libs/flutter_rust_bridge_codegen/src/ir/func.rs deleted file mode 100644 index 4bce1c42f..000000000 --- a/libs/flutter_rust_bridge_codegen/src/ir/func.rs +++ /dev/null @@ -1,60 +0,0 @@ -use crate::ir::*; - -#[derive(Debug, Clone)] -pub struct IrFunc { - pub name: String, - pub inputs: Vec, - pub output: IrType, - pub fallible: bool, - pub mode: IrFuncMode, - pub comments: Vec, -} - -impl IrFunc { - pub fn wire_func_name(&self) -> String { - format!("wire_{}", self.name) - } -} - -/// Represents a function's output type -#[derive(Debug, Clone)] -pub enum IrFuncOutput { - ResultType(IrType), - Type(IrType), -} - -/// Represents the type of an argument to a function -#[derive(Debug, Clone)] -pub enum IrFuncArg { - StreamSinkType(IrType), - Type(IrType), -} - -#[derive(Debug, Clone, PartialOrd, PartialEq)] -pub enum IrFuncMode { - Normal, - Sync, - Stream, -} - -impl IrFuncMode { - pub fn dart_return_type(&self, inner: &str) -> String { - match self { - Self::Normal => format!("Future<{}>", inner), - Self::Sync => inner.to_string(), - Self::Stream => format!("Stream<{}>", inner), - } - } - - pub fn ffi_call_mode(&self) -> &'static str { - match self { - Self::Normal => "Normal", - Self::Sync => "Sync", - Self::Stream => "Stream", - } - } - - pub fn has_port_argument(&self) -> bool { - self != &Self::Sync - } -} diff --git a/libs/flutter_rust_bridge_codegen/src/ir/ident.rs b/libs/flutter_rust_bridge_codegen/src/ir/ident.rs deleted file mode 100644 index c86ac25fe..000000000 --- a/libs/flutter_rust_bridge_codegen/src/ir/ident.rs +++ /dev/null @@ -1,26 +0,0 @@ -use convert_case::{Case, Casing}; - -#[derive(Debug, Clone)] -pub struct IrIdent { - pub raw: String, -} - -impl std::fmt::Display for IrIdent { - fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { - fmt.write_str(&self.raw) - } -} - -impl IrIdent { - pub fn new(raw: String) -> IrIdent { - IrIdent { raw } - } - - pub fn rust_style(&self) -> &str { - &self.raw - } - - pub fn dart_style(&self) -> String { - self.raw.to_case(Case::Camel) - } -} diff --git a/libs/flutter_rust_bridge_codegen/src/ir/import.rs b/libs/flutter_rust_bridge_codegen/src/ir/import.rs deleted file mode 100644 index 072975c35..000000000 --- a/libs/flutter_rust_bridge_codegen/src/ir/import.rs +++ /dev/null @@ -1,5 +0,0 @@ -#[derive(Debug, Clone, Hash, PartialEq, Eq)] -pub struct IrDartImport { - pub uri: String, - pub alias: Option, -} diff --git a/libs/flutter_rust_bridge_codegen/src/ir/mod.rs b/libs/flutter_rust_bridge_codegen/src/ir/mod.rs deleted file mode 100644 index eb3c73c47..000000000 --- a/libs/flutter_rust_bridge_codegen/src/ir/mod.rs +++ /dev/null @@ -1,33 +0,0 @@ -mod annotation; -mod comment; -mod field; -mod file; -mod func; -mod ident; -mod import; -mod ty; -mod ty_boxed; -mod ty_delegate; -mod ty_enum; -mod ty_general_list; -mod ty_optional; -mod ty_primitive; -mod ty_primitive_list; -mod ty_struct; - -pub use annotation::*; -pub use comment::*; -pub use field::*; -pub use file::*; -pub use func::*; -pub use ident::*; -pub use import::*; -pub use ty::*; -pub use ty_boxed::*; -pub use ty_delegate::*; -pub use ty_enum::*; -pub use ty_general_list::*; -pub use ty_optional::*; -pub use ty_primitive::*; -pub use ty_primitive_list::*; -pub use ty_struct::*; diff --git a/libs/flutter_rust_bridge_codegen/src/ir/ty.rs b/libs/flutter_rust_bridge_codegen/src/ir/ty.rs deleted file mode 100644 index d342c54c7..000000000 --- a/libs/flutter_rust_bridge_codegen/src/ir/ty.rs +++ /dev/null @@ -1,84 +0,0 @@ -use crate::ir::*; -use enum_dispatch::enum_dispatch; -use IrType::*; - -/// Remark: "Ty" instead of "Type", since "type" is a reserved word in Rust. -#[enum_dispatch(IrTypeTrait)] -#[derive(Debug, Clone)] -pub enum IrType { - Primitive(IrTypePrimitive), - Delegate(IrTypeDelegate), - PrimitiveList(IrTypePrimitiveList), - Optional(IrTypeOptional), - GeneralList(IrTypeGeneralList), - StructRef(IrTypeStructRef), - Boxed(IrTypeBoxed), - EnumRef(IrTypeEnumRef), -} - -impl IrType { - pub fn visit_types bool>(&self, f: &mut F, ir_file: &IrFile) { - if f(self) { - return; - } - - self.visit_children_types(f, ir_file); - } - - #[inline] - pub fn dart_required_modifier(&self) -> &'static str { - match self { - Optional(_) => "", - _ => "required ", - } - } - - /// Additional indirection for types put behind a vector - #[inline] - pub fn rust_ptr_modifier(&self) -> &'static str { - match self { - Optional(_) | Delegate(IrTypeDelegate::String) => "*mut ", - _ => "", - } - } -} - -#[enum_dispatch] -pub trait IrTypeTrait { - fn visit_children_types bool>(&self, f: &mut F, ir_file: &IrFile); - - fn safe_ident(&self) -> String; - - fn dart_api_type(&self) -> String; - - fn dart_wire_type(&self) -> String; - - fn rust_api_type(&self) -> String; - - fn rust_wire_type(&self) -> String; - - fn rust_wire_modifier(&self) -> String { - if self.rust_wire_is_pointer() { - "*mut ".to_string() - } else { - "".to_string() - } - } - - fn rust_wire_is_pointer(&self) -> bool { - false - } -} - -pub fn optional_boundary_index(types: &[&IrType]) -> Option { - types - .iter() - .enumerate() - .find(|ty| matches!(ty.1, Optional(_))) - .and_then(|(idx, _)| { - (&types[idx..]) - .iter() - .all(|ty| matches!(ty, Optional(_))) - .then(|| idx) - }) -} diff --git a/libs/flutter_rust_bridge_codegen/src/ir/ty_boxed.rs b/libs/flutter_rust_bridge_codegen/src/ir/ty_boxed.rs deleted file mode 100644 index 0ef2cddb0..000000000 --- a/libs/flutter_rust_bridge_codegen/src/ir/ty_boxed.rs +++ /dev/null @@ -1,56 +0,0 @@ -use crate::ir::IrType::Primitive; -use crate::ir::*; - -#[derive(Debug, Clone)] -pub struct IrTypeBoxed { - /// if false, means that we automatically add it when transforming it - it does not exist in real api. - pub exist_in_real_api: bool, - pub inner: Box, -} - -impl IrTypeTrait for IrTypeBoxed { - fn visit_children_types bool>(&self, f: &mut F, ir_file: &IrFile) { - self.inner.visit_types(f, ir_file); - } - - fn safe_ident(&self) -> String { - format!( - "box_{}{}", - if self.exist_in_real_api { - "" - } else { - "autoadd_" - }, - self.inner.safe_ident() - ) - } - - fn dart_api_type(&self) -> String { - self.inner.dart_api_type() - } - - fn dart_wire_type(&self) -> String { - let wire_type = if let Primitive(prim) = &*self.inner { - prim.dart_native_type().to_owned() - } else { - self.inner.dart_wire_type() - }; - format!("ffi.Pointer<{}>", wire_type) - } - - fn rust_api_type(&self) -> String { - if self.exist_in_real_api { - format!("Box<{}>", self.inner.rust_api_type()) - } else { - self.inner.rust_api_type() - } - } - - fn rust_wire_type(&self) -> String { - self.inner.rust_wire_type() - } - - fn rust_wire_is_pointer(&self) -> bool { - true - } -} diff --git a/libs/flutter_rust_bridge_codegen/src/ir/ty_delegate.rs b/libs/flutter_rust_bridge_codegen/src/ir/ty_delegate.rs deleted file mode 100644 index ac2574e4f..000000000 --- a/libs/flutter_rust_bridge_codegen/src/ir/ty_delegate.rs +++ /dev/null @@ -1,85 +0,0 @@ -use crate::ir::*; - -/// types that delegate to another type -#[derive(Debug, Clone)] -pub enum IrTypeDelegate { - String, - StringList, - SyncReturnVecU8, - ZeroCopyBufferVecPrimitive(IrTypePrimitive), -} - -impl IrTypeDelegate { - pub fn get_delegate(&self) -> IrType { - match self { - IrTypeDelegate::String => IrType::PrimitiveList(IrTypePrimitiveList { - primitive: IrTypePrimitive::U8, - }), - IrTypeDelegate::SyncReturnVecU8 => IrType::PrimitiveList(IrTypePrimitiveList { - primitive: IrTypePrimitive::U8, - }), - IrTypeDelegate::ZeroCopyBufferVecPrimitive(primitive) => { - IrType::PrimitiveList(IrTypePrimitiveList { - primitive: primitive.clone(), - }) - } - IrTypeDelegate::StringList => IrType::Delegate(IrTypeDelegate::String), - } - } -} - -impl IrTypeTrait for IrTypeDelegate { - fn visit_children_types bool>(&self, f: &mut F, ir_file: &IrFile) { - self.get_delegate().visit_types(f, ir_file); - } - - fn safe_ident(&self) -> String { - match self { - IrTypeDelegate::String => "String".to_owned(), - IrTypeDelegate::StringList => "StringList".to_owned(), - IrTypeDelegate::SyncReturnVecU8 => "SyncReturnVecU8".to_owned(), - IrTypeDelegate::ZeroCopyBufferVecPrimitive(_) => { - "ZeroCopyBuffer_".to_owned() + &self.get_delegate().dart_api_type() - } - } - } - - fn dart_api_type(&self) -> String { - match self { - IrTypeDelegate::String => "String".to_string(), - IrTypeDelegate::StringList => "List".to_owned(), - IrTypeDelegate::SyncReturnVecU8 | IrTypeDelegate::ZeroCopyBufferVecPrimitive(_) => { - self.get_delegate().dart_api_type() - } - } - } - - fn dart_wire_type(&self) -> String { - match self { - IrTypeDelegate::StringList => "ffi.Pointer".to_owned(), - _ => self.get_delegate().dart_wire_type(), - } - } - - fn rust_api_type(&self) -> String { - match self { - IrTypeDelegate::String => "String".to_owned(), - IrTypeDelegate::SyncReturnVecU8 => "SyncReturn>".to_string(), - IrTypeDelegate::StringList => "Vec".to_owned(), - IrTypeDelegate::ZeroCopyBufferVecPrimitive(_) => { - format!("ZeroCopyBuffer<{}>", self.get_delegate().rust_api_type()) - } - } - } - - fn rust_wire_type(&self) -> String { - match self { - IrTypeDelegate::StringList => "wire_StringList".to_owned(), - _ => self.get_delegate().rust_wire_type(), - } - } - - fn rust_wire_is_pointer(&self) -> bool { - self.get_delegate().rust_wire_is_pointer() - } -} diff --git a/libs/flutter_rust_bridge_codegen/src/ir/ty_enum.rs b/libs/flutter_rust_bridge_codegen/src/ir/ty_enum.rs deleted file mode 100644 index bae45a692..000000000 --- a/libs/flutter_rust_bridge_codegen/src/ir/ty_enum.rs +++ /dev/null @@ -1,139 +0,0 @@ -use crate::ir::IrType::{EnumRef, StructRef}; -use crate::ir::*; -use convert_case::{Case, Casing}; - -#[derive(Debug, Clone)] -pub struct IrTypeEnumRef { - pub name: String, - pub is_struct: bool, -} - -impl IrTypeEnumRef { - pub fn get<'a>(&self, file: &'a IrFile) -> &'a IrEnum { - &file.enum_pool[&self.name] - } -} - -impl IrTypeTrait for IrTypeEnumRef { - fn visit_children_types bool>(&self, f: &mut F, ir_file: &IrFile) { - let enu = self.get(ir_file); - for variant in enu.variants() { - if let IrVariantKind::Struct(st) = &variant.kind { - st.fields - .iter() - .for_each(|field| field.ty.visit_types(f, ir_file)); - } - } - } - - fn safe_ident(&self) -> String { - self.dart_api_type().to_case(Case::Snake) - } - fn dart_api_type(&self) -> String { - self.name.to_string() - } - fn dart_wire_type(&self) -> String { - if self.is_struct { - self.rust_wire_type() - } else { - "int".to_owned() - } - } - fn rust_api_type(&self) -> String { - self.name.to_string() - } - fn rust_wire_type(&self) -> String { - if self.is_struct { - format!("wire_{}", self.name) - } else { - "i32".to_owned() - } - } -} - -#[derive(Debug, Clone)] -pub struct IrEnum { - pub name: String, - pub wrapper_name: Option, - pub path: Vec, - pub comments: Vec, - _variants: Vec, - _is_struct: bool, -} - -impl IrEnum { - pub fn new( - name: String, - wrapper_name: Option, - path: Vec, - comments: Vec, - mut variants: Vec, - ) -> Self { - fn wrap_box(ty: IrType) -> IrType { - match ty { - StructRef(_) - | EnumRef(IrTypeEnumRef { - is_struct: true, .. - }) => IrType::Boxed(IrTypeBoxed { - exist_in_real_api: false, - inner: Box::new(ty), - }), - _ => ty, - } - } - let _is_struct = variants - .iter() - .any(|variant| !matches!(variant.kind, IrVariantKind::Value)); - if _is_struct { - variants = variants - .into_iter() - .map(|variant| IrVariant { - kind: match variant.kind { - IrVariantKind::Struct(st) => IrVariantKind::Struct(IrStruct { - fields: st - .fields - .into_iter() - .map(|field| IrField { - ty: wrap_box(field.ty), - ..field - }) - .collect(), - ..st - }), - _ => variant.kind, - }, - ..variant - }) - .collect::>(); - } - Self { - name, - wrapper_name, - path, - comments, - _variants: variants, - _is_struct, - } - } - - pub fn variants(&self) -> &[IrVariant] { - &self._variants - } - - pub fn is_struct(&self) -> bool { - self._is_struct - } -} - -#[derive(Debug, Clone)] -pub struct IrVariant { - pub name: IrIdent, - pub comments: Vec, - pub kind: IrVariantKind, -} - -#[derive(Debug, Clone)] -pub enum IrVariantKind { - Value, - Struct(IrStruct), -} diff --git a/libs/flutter_rust_bridge_codegen/src/ir/ty_general_list.rs b/libs/flutter_rust_bridge_codegen/src/ir/ty_general_list.rs deleted file mode 100644 index 44f5fde95..000000000 --- a/libs/flutter_rust_bridge_codegen/src/ir/ty_general_list.rs +++ /dev/null @@ -1,36 +0,0 @@ -use crate::ir::*; - -#[derive(Debug, Clone)] -pub struct IrTypeGeneralList { - pub inner: Box, -} - -impl IrTypeTrait for IrTypeGeneralList { - fn visit_children_types bool>(&self, f: &mut F, ir_file: &IrFile) { - self.inner.visit_types(f, ir_file); - } - - fn safe_ident(&self) -> String { - format!("list_{}", self.inner.safe_ident()) - } - - fn dart_api_type(&self) -> String { - format!("List<{}>", self.inner.dart_api_type()) - } - - fn dart_wire_type(&self) -> String { - format!("ffi.Pointer", self.safe_ident()) - } - - fn rust_api_type(&self) -> String { - format!("Vec<{}>", self.inner.rust_api_type()) - } - - fn rust_wire_type(&self) -> String { - format!("wire_{}", self.safe_ident()) - } - - fn rust_wire_is_pointer(&self) -> bool { - true - } -} diff --git a/libs/flutter_rust_bridge_codegen/src/ir/ty_optional.rs b/libs/flutter_rust_bridge_codegen/src/ir/ty_optional.rs deleted file mode 100644 index 580788918..000000000 --- a/libs/flutter_rust_bridge_codegen/src/ir/ty_optional.rs +++ /dev/null @@ -1,65 +0,0 @@ -use crate::ir::IrType::*; -use crate::ir::*; - -#[derive(Debug, Clone)] -pub struct IrTypeOptional { - pub inner: Box, -} - -impl IrTypeOptional { - pub fn new_prim(prim: IrTypePrimitive) -> Self { - Self { - inner: Box::new(Boxed(IrTypeBoxed { - inner: Box::new(Primitive(prim)), - exist_in_real_api: false, - })), - } - } - - pub fn new_ptr(ptr: IrType) -> Self { - Self { - inner: Box::new(ptr), - } - } - - pub fn is_primitive(&self) -> bool { - matches!(&*self.inner, Boxed(boxed) if matches!(*boxed.inner, IrType::Primitive(_))) - } - - pub fn is_list(&self) -> bool { - matches!(&*self.inner, GeneralList(_) | PrimitiveList(_)) - } - - pub fn is_delegate(&self) -> bool { - matches!(&*self.inner, Delegate(_)) - } - - pub fn needs_initialization(&self) -> bool { - !(self.is_primitive() || self.is_delegate()) - } -} - -impl IrTypeTrait for IrTypeOptional { - fn safe_ident(&self) -> String { - format!("opt_{}", self.inner.safe_ident()) - } - fn rust_wire_type(&self) -> String { - self.inner.rust_wire_type() - } - fn rust_api_type(&self) -> String { - format!("Option<{}>", self.inner.rust_api_type()) - } - fn dart_wire_type(&self) -> String { - self.inner.dart_wire_type() - } - fn dart_api_type(&self) -> String { - format!("{}?", self.inner.dart_api_type()) - } - fn rust_wire_is_pointer(&self) -> bool { - true - } - - fn visit_children_types bool>(&self, f: &mut F, ir_file: &IrFile) { - self.inner.visit_types(f, ir_file); - } -} diff --git a/libs/flutter_rust_bridge_codegen/src/ir/ty_primitive.rs b/libs/flutter_rust_bridge_codegen/src/ir/ty_primitive.rs deleted file mode 100644 index 06cfece9d..000000000 --- a/libs/flutter_rust_bridge_codegen/src/ir/ty_primitive.rs +++ /dev/null @@ -1,114 +0,0 @@ -use crate::ir::*; - -#[derive(Debug, Clone)] -pub enum IrTypePrimitive { - U8, - I8, - U16, - I16, - U32, - I32, - U64, - I64, - F32, - F64, - Bool, - Unit, - Usize, -} - -impl IrTypeTrait for IrTypePrimitive { - fn visit_children_types bool>(&self, _f: &mut F, _ir_file: &IrFile) {} - - fn safe_ident(&self) -> String { - self.rust_api_type() - } - - fn dart_api_type(&self) -> String { - match self { - IrTypePrimitive::U8 - | IrTypePrimitive::I8 - | IrTypePrimitive::U16 - | IrTypePrimitive::I16 - | IrTypePrimitive::U32 - | IrTypePrimitive::I32 - | IrTypePrimitive::U64 - | IrTypePrimitive::I64 - | IrTypePrimitive::Usize => "int", - IrTypePrimitive::F32 | IrTypePrimitive::F64 => "double", - IrTypePrimitive::Bool => "bool", - IrTypePrimitive::Unit => "void", - } - .to_string() - } - - fn dart_wire_type(&self) -> String { - match self { - IrTypePrimitive::Bool => "int".to_owned(), - _ => self.dart_api_type(), - } - } - - fn rust_api_type(&self) -> String { - self.rust_wire_type() - } - - fn rust_wire_type(&self) -> String { - match self { - IrTypePrimitive::U8 => "u8", - IrTypePrimitive::I8 => "i8", - IrTypePrimitive::U16 => "u16", - IrTypePrimitive::I16 => "i16", - IrTypePrimitive::U32 => "u32", - IrTypePrimitive::I32 => "i32", - IrTypePrimitive::U64 => "u64", - IrTypePrimitive::I64 => "i64", - IrTypePrimitive::F32 => "f32", - IrTypePrimitive::F64 => "f64", - IrTypePrimitive::Bool => "bool", - IrTypePrimitive::Unit => "unit", - IrTypePrimitive::Usize => "usize", - } - .to_string() - } -} - -impl IrTypePrimitive { - /// Representations of primitives within Dart's pointers, e.g. `ffi.Pointer`. - /// This is enforced on Dart's side, and should be used instead of `dart_wire_type` - /// whenever primitives are put behind a pointer. - pub fn dart_native_type(&self) -> &'static str { - match self { - IrTypePrimitive::U8 | IrTypePrimitive::Bool => "ffi.Uint8", - IrTypePrimitive::I8 => "ffi.Int8", - IrTypePrimitive::U16 => "ffi.Uint16", - IrTypePrimitive::I16 => "ffi.Int16", - IrTypePrimitive::U32 => "ffi.Uint32", - IrTypePrimitive::I32 => "ffi.Int32", - IrTypePrimitive::U64 => "ffi.Uint64", - IrTypePrimitive::I64 => "ffi.Int64", - IrTypePrimitive::F32 => "ffi.Float", - IrTypePrimitive::F64 => "ffi.Double", - IrTypePrimitive::Unit => "ffi.Void", - IrTypePrimitive::Usize => "ffi.Usize", - } - } - pub fn try_from_rust_str(s: &str) -> Option { - match s { - "u8" => Some(IrTypePrimitive::U8), - "i8" => Some(IrTypePrimitive::I8), - "u16" => Some(IrTypePrimitive::U16), - "i16" => Some(IrTypePrimitive::I16), - "u32" => Some(IrTypePrimitive::U32), - "i32" => Some(IrTypePrimitive::I32), - "u64" => Some(IrTypePrimitive::U64), - "i64" => Some(IrTypePrimitive::I64), - "f32" => Some(IrTypePrimitive::F32), - "f64" => Some(IrTypePrimitive::F64), - "bool" => Some(IrTypePrimitive::Bool), - "()" => Some(IrTypePrimitive::Unit), - "usize" => Some(IrTypePrimitive::Usize), - _ => None, - } - } -} diff --git a/libs/flutter_rust_bridge_codegen/src/ir/ty_primitive_list.rs b/libs/flutter_rust_bridge_codegen/src/ir/ty_primitive_list.rs deleted file mode 100644 index 759d29d71..000000000 --- a/libs/flutter_rust_bridge_codegen/src/ir/ty_primitive_list.rs +++ /dev/null @@ -1,50 +0,0 @@ -use crate::ir::*; -use convert_case::{Case, Casing}; - -#[derive(Debug, Clone)] -pub struct IrTypePrimitiveList { - pub primitive: IrTypePrimitive, -} - -impl IrTypeTrait for IrTypePrimitiveList { - fn visit_children_types bool>(&self, f: &mut F, _ir_file: &IrFile) { - f(&IrType::Primitive(self.primitive.clone())); - } - - fn safe_ident(&self) -> String { - self.dart_api_type().to_case(Case::Snake) - } - - fn dart_api_type(&self) -> String { - match &self.primitive { - IrTypePrimitive::U8 => "Uint8List", - IrTypePrimitive::I8 => "Int8List", - IrTypePrimitive::U16 => "Uint16List", - IrTypePrimitive::I16 => "Int16List", - IrTypePrimitive::U32 => "Uint32List", - IrTypePrimitive::I32 => "Int32List", - IrTypePrimitive::U64 => "Uint64List", - IrTypePrimitive::I64 => "Int64List", - IrTypePrimitive::F32 => "Float32List", - IrTypePrimitive::F64 => "Float64List", - _ => panic!("does not support {:?} yet", &self.primitive), - } - .to_string() - } - - fn dart_wire_type(&self) -> String { - format!("ffi.Pointer", self.safe_ident()) - } - - fn rust_api_type(&self) -> String { - format!("Vec<{}>", self.primitive.rust_api_type()) - } - - fn rust_wire_type(&self) -> String { - format!("wire_{}", self.safe_ident()) - } - - fn rust_wire_is_pointer(&self) -> bool { - true - } -} diff --git a/libs/flutter_rust_bridge_codegen/src/ir/ty_struct.rs b/libs/flutter_rust_bridge_codegen/src/ir/ty_struct.rs deleted file mode 100644 index 3cacbd626..000000000 --- a/libs/flutter_rust_bridge_codegen/src/ir/ty_struct.rs +++ /dev/null @@ -1,66 +0,0 @@ -use crate::ir::*; -use convert_case::{Case, Casing}; - -#[derive(Debug, Clone)] -pub struct IrTypeStructRef { - pub name: String, - pub freezed: bool, -} - -impl IrTypeStructRef { - pub fn get<'a>(&self, f: &'a IrFile) -> &'a IrStruct { - &f.struct_pool[&self.name] - } -} - -impl IrTypeTrait for IrTypeStructRef { - fn visit_children_types bool>(&self, f: &mut F, ir_file: &IrFile) { - for field in &self.get(ir_file).fields { - field.ty.visit_types(f, ir_file); - } - } - - fn safe_ident(&self) -> String { - self.dart_api_type().to_case(Case::Snake) - } - fn dart_api_type(&self) -> String { - self.name.to_string() - } - - fn dart_wire_type(&self) -> String { - self.rust_wire_type() - } - - fn rust_api_type(&self) -> String { - self.name.to_string() - } - - fn rust_wire_type(&self) -> String { - format!("wire_{}", self.name) - } -} - -#[derive(Debug, Clone)] -pub struct IrStruct { - pub name: String, - pub wrapper_name: Option, - pub path: Option>, - pub fields: Vec, - pub is_fields_named: bool, - pub dart_metadata: Vec, - pub comments: Vec, -} - -impl IrStruct { - pub fn brackets_pair(&self) -> (char, char) { - if self.is_fields_named { - ('{', '}') - } else { - ('(', ')') - } - } - - pub fn using_freezed(&self) -> bool { - self.dart_metadata.iter().any(|it| it.content == "freezed") - } -} diff --git a/libs/flutter_rust_bridge_codegen/src/lib.rs b/libs/flutter_rust_bridge_codegen/src/lib.rs deleted file mode 100644 index 0eaa76529..000000000 --- a/libs/flutter_rust_bridge_codegen/src/lib.rs +++ /dev/null @@ -1,183 +0,0 @@ -use std::fs; -use std::path::Path; - -use log::info; -use pathdiff::diff_paths; - -use crate::commands::ensure_tools_available; -pub use crate::config::RawOpts as Opts; -use crate::ir::*; -use crate::others::*; -use crate::utils::*; - -mod commands; -mod config; -mod error; -mod generator; -mod ir; -mod markers; -mod others; -mod parser; -mod source_graph; -mod transformer; -mod utils; -use error::*; - -pub fn frb_codegen(raw_opts: Opts) -> anyhow::Result<()> { - ensure_tools_available()?; - - let config = config::parse(raw_opts); - info!("Picked config: {:?}", &config); - - let rust_output_dir = Path::new(&config.rust_output_path).parent().unwrap(); - let dart_output_dir = Path::new(&config.dart_output_path).parent().unwrap(); - - info!("Phase: Parse source code to AST"); - let source_rust_content = fs::read_to_string(&config.rust_input_path)?; - let file_ast = syn::parse_file(&source_rust_content)?; - - info!("Phase: Parse AST to IR"); - let raw_ir_file = parser::parse(&source_rust_content, file_ast, &config.manifest_path); - - info!("Phase: Transform IR"); - let ir_file = transformer::transform(raw_ir_file); - - info!("Phase: Generate Rust code"); - let generated_rust = generator::rust::generate( - &ir_file, - &mod_from_rust_path(&config.rust_input_path, &config.rust_crate_dir), - ); - fs::create_dir_all(&rust_output_dir)?; - fs::write(&config.rust_output_path, generated_rust.code)?; - - info!("Phase: Generate Dart code"); - let (generated_dart, needs_freezed) = generator::dart::generate( - &ir_file, - &config.dart_api_class_name(), - &config.dart_api_impl_class_name(), - &config.dart_wire_class_name(), - config - .dart_output_path_name() - .ok_or_else(|| Error::str("Invalid dart_output_path_name"))?, - ); - - info!("Phase: Other things"); - - commands::format_rust(&config.rust_output_path)?; - - if !config.skip_add_mod_to_lib { - others::try_add_mod_to_lib(&config.rust_crate_dir, &config.rust_output_path); - } - - let c_struct_names = ir_file - .distinct_types(true, true) - .iter() - .filter_map(|ty| { - if let IrType::StructRef(_) = ty { - Some(ty.rust_wire_type()) - } else { - None - } - }) - .collect(); - - let temp_dart_wire_file = tempfile::NamedTempFile::new()?; - let temp_bindgen_c_output_file = tempfile::Builder::new().suffix(".h").tempfile()?; - with_changed_file( - &config.rust_output_path, - DUMMY_WIRE_CODE_FOR_BINDGEN, - || { - commands::bindgen_rust_to_dart( - &config.rust_crate_dir, - temp_bindgen_c_output_file - .path() - .as_os_str() - .to_str() - .unwrap(), - temp_dart_wire_file.path().as_os_str().to_str().unwrap(), - &config.dart_wire_class_name(), - c_struct_names, - &config.llvm_path[..], - &config.llvm_compiler_opts, - ) - }, - )?; - - let effective_func_names = [ - generated_rust.extern_func_names, - EXTRA_EXTERN_FUNC_NAMES.to_vec(), - ] - .concat(); - let c_dummy_code = generator::c::generate_dummy(&effective_func_names); - for output in &config.c_output_path { - fs::create_dir_all(Path::new(output).parent().unwrap())?; - fs::write( - &output, - fs::read_to_string(&temp_bindgen_c_output_file)? + "\n" + &c_dummy_code, - )?; - } - - fs::create_dir_all(&dart_output_dir)?; - let generated_dart_wire_code_raw = fs::read_to_string(temp_dart_wire_file)?; - let generated_dart_wire = extract_dart_wire_content(&modify_dart_wire_content( - &generated_dart_wire_code_raw, - &config.dart_wire_class_name(), - )); - - sanity_check(&generated_dart_wire.body, &config.dart_wire_class_name())?; - - let generated_dart_decl_all = generated_dart.decl_code; - let generated_dart_impl_all = &generated_dart.impl_code + &generated_dart_wire; - if let Some(dart_decl_output_path) = &config.dart_decl_output_path { - let impl_import_decl = DartBasicCode { - import: format!( - "import \"{}\";", - diff_paths(dart_decl_output_path, dart_output_dir) - .unwrap() - .to_str() - .unwrap() - ), - part: String::new(), - body: String::new(), - }; - fs::write( - &dart_decl_output_path, - (&generated_dart.file_prelude + &generated_dart_decl_all).to_text(), - )?; - fs::write( - &config.dart_output_path, - (&generated_dart.file_prelude + &impl_import_decl + &generated_dart_impl_all).to_text(), - )?; - } else { - fs::write( - &config.dart_output_path, - (&generated_dart.file_prelude + &generated_dart_decl_all + &generated_dart_impl_all) - .to_text(), - )?; - } - - let dart_root = &config.dart_root; - if needs_freezed && config.build_runner { - let dart_root = dart_root.as_ref().ok_or_else(|| { - Error::str( - "build_runner configured to run, but Dart root could not be inferred. - Please specify --dart-root, or disable build_runner with --no-build-runner.", - ) - })?; - commands::build_runner(dart_root)?; - commands::format_dart( - &config - .dart_output_freezed_path() - .ok_or_else(|| Error::str("Invalid freezed file path"))?, - config.dart_format_line_length, - )?; - } - - commands::format_dart(&config.dart_output_path, config.dart_format_line_length)?; - if let Some(dart_decl_output_path) = &config.dart_decl_output_path { - commands::format_dart(dart_decl_output_path, config.dart_format_line_length)?; - } - - info!("Success!"); - Ok(()) -} diff --git a/libs/flutter_rust_bridge_codegen/src/main.rs b/libs/flutter_rust_bridge_codegen/src/main.rs deleted file mode 100644 index 986ca3215..000000000 --- a/libs/flutter_rust_bridge_codegen/src/main.rs +++ /dev/null @@ -1,19 +0,0 @@ -use env_logger::Env; -use log::info; -use structopt::StructOpt; - -use lib_flutter_rust_bridge_codegen::{frb_codegen, Opts}; - -fn main() { - let opts = Opts::from_args(); - env_logger::Builder::from_env(Env::default().default_filter_or(if opts.verbose { - "debug" - } else { - "info" - })) - .init(); - - frb_codegen(opts).unwrap(); - - info!("Now go and use it :)"); -} diff --git a/libs/flutter_rust_bridge_codegen/src/markers.rs b/libs/flutter_rust_bridge_codegen/src/markers.rs deleted file mode 100644 index 048cb77db..000000000 --- a/libs/flutter_rust_bridge_codegen/src/markers.rs +++ /dev/null @@ -1,39 +0,0 @@ -use syn::*; - -/// Extract a path from marker `#[frb(mirror(path), ..)]` -pub fn extract_mirror_marker(attrs: &[Attribute]) -> Option { - attrs - .iter() - .filter(|attr| attr.path.is_ident("frb")) - .find_map(|attr| match attr.parse_meta() { - Ok(Meta::List(MetaList { nested, .. })) => nested.iter().find_map(|meta| match meta { - NestedMeta::Meta(Meta::List(MetaList { - path, - nested: mirror, - .. - })) if path.is_ident("mirror") && mirror.len() == 1 => { - match mirror.first().unwrap() { - NestedMeta::Meta(Meta::Path(path)) => Some(path.clone()), - _ => None, - } - } - _ => None, - }), - _ => None, - }) -} - -/// Checks if the `#[frb(non_final)]` attribute is present. -pub fn has_non_final(attrs: &[Attribute]) -> bool { - attrs - .iter() - .filter(|attr| attr.path.is_ident("frb")) - .any(|attr| { - match attr.parse_meta() { - Ok(Meta::List(MetaList { nested, .. })) => nested.iter().any(|meta| { - matches!(meta, NestedMeta::Meta(Meta::Path(path)) if path.is_ident("non_final")) - }), - _ => false, - } - }) -} diff --git a/libs/flutter_rust_bridge_codegen/src/others.rs b/libs/flutter_rust_bridge_codegen/src/others.rs deleted file mode 100644 index 4a8d10c8f..000000000 --- a/libs/flutter_rust_bridge_codegen/src/others.rs +++ /dev/null @@ -1,169 +0,0 @@ -use std::fs; -use std::ops::Add; -use std::path::Path; - -use anyhow::{anyhow, Result}; -use lazy_static::lazy_static; -use log::{info, warn}; -use pathdiff::diff_paths; -use regex::RegexBuilder; - -// NOTE [DartPostCObjectFnType] was originally [*mut DartCObject] but I changed it to [*mut c_void] -// because cannot automatically generate things related to [DartCObject]. Anyway this works fine. -// NOTE please sync [DUMMY_WIRE_CODE_FOR_BINDGEN] and [EXTRA_EXTERN_FUNC_NAMES] -pub const DUMMY_WIRE_CODE_FOR_BINDGEN: &str = r#" - // ----------- DUMMY CODE FOR BINDGEN ---------- - - // copied from: allo-isolate - pub type DartPort = i64; - pub type DartPostCObjectFnType = unsafe extern "C" fn(port_id: DartPort, message: *mut std::ffi::c_void) -> bool; - #[no_mangle] pub unsafe extern "C" fn store_dart_post_cobject(ptr: DartPostCObjectFnType) { panic!("dummy code") } - - // copied from: frb_rust::support.rs - #[repr(C)] - pub struct WireSyncReturnStruct { - pub ptr: *mut u8, - pub len: i32, - pub success: bool, - } - - // --------------------------------------------- - "#; - -lazy_static! { - pub static ref EXTRA_EXTERN_FUNC_NAMES: Vec = - vec!["store_dart_post_cobject".to_string()]; -} - -pub const CODE_HEADER: &str = "// AUTO GENERATED FILE, DO NOT EDIT. -// Generated by `flutter_rust_bridge`."; - -pub fn modify_dart_wire_content(content_raw: &str, dart_wire_class_name: &str) -> String { - let content = content_raw.replace( - &format!("class {} {{", dart_wire_class_name), - &format!( - "class {} implements FlutterRustBridgeWireBase {{", - dart_wire_class_name - ), - ); - - let content = RegexBuilder::new("class WireSyncReturnStruct extends ffi.Struct \\{.+?\\}") - .multi_line(true) - .dot_matches_new_line(true) - .build() - .unwrap() - .replace(&content, ""); - - content.to_string() -} - -#[derive(Default)] -pub struct DartBasicCode { - pub import: String, - pub part: String, - pub body: String, -} - -impl Add for &DartBasicCode { - type Output = DartBasicCode; - - fn add(self, rhs: Self) -> Self::Output { - DartBasicCode { - import: format!("{}\n{}", self.import, rhs.import), - part: format!("{}\n{}", self.part, rhs.part), - body: format!("{}\n{}", self.body, rhs.body), - } - } -} - -impl Add<&DartBasicCode> for DartBasicCode { - type Output = DartBasicCode; - - fn add(self, rhs: &DartBasicCode) -> Self::Output { - (&self).add(rhs) - } -} - -impl DartBasicCode { - pub fn to_text(&self) -> String { - format!("{}\n{}\n{}", self.import, self.part, self.body) - } -} - -pub fn extract_dart_wire_content(content: &str) -> DartBasicCode { - let (mut imports, mut body) = (Vec::new(), Vec::new()); - for line in content.split('\n') { - (if line.starts_with("import ") { - &mut imports - } else { - &mut body - }) - .push(line); - } - DartBasicCode { - import: imports.join("\n"), - part: "".to_string(), - body: body.join("\n"), - } -} - -pub fn sanity_check( - generated_dart_wire_code: &str, - dart_wire_class_name: &str, -) -> anyhow::Result<()> { - if !generated_dart_wire_code.contains(dart_wire_class_name) { - return Err(crate::error::Error::str( - "Nothing is generated for dart wire class. \ - Maybe you forget to put code like `mod the_generated_bridge_code;` to your `lib.rs`?", - ) - .into()); - } - Ok(()) -} - -pub fn try_add_mod_to_lib(rust_crate_dir: &str, rust_output_path: &str) { - if let Err(e) = auto_add_mod_to_lib_core(rust_crate_dir, rust_output_path) { - warn!( - "auto_add_mod_to_lib fail, the generated code may or may not have problems. \ - Please ensure you have add code like `mod the_generated_bridge_code;` to your `lib.rs`. \ - Details: {}", - e - ); - } -} - -pub fn auto_add_mod_to_lib_core(rust_crate_dir: &str, rust_output_path: &str) -> Result<()> { - let path_src_folder = Path::new(rust_crate_dir).join("src"); - let rust_output_path_relative_to_src_folder = - diff_paths(rust_output_path, path_src_folder.clone()).ok_or_else(|| { - anyhow!( - "rust_output_path={} is unrelated to path_src_folder={:?}", - rust_output_path, - &path_src_folder, - ) - })?; - - let mod_name = rust_output_path_relative_to_src_folder - .file_stem() - .ok_or_else(|| anyhow!(""))? - .to_str() - .ok_or_else(|| anyhow!(""))? - .to_string() - .replace('/', "::"); - let expect_code = format!("mod {};", mod_name); - - let path_lib_rs = path_src_folder.join("lib.rs"); - - let raw_content_lib_rs = fs::read_to_string(path_lib_rs.clone())?; - if !raw_content_lib_rs.contains(&expect_code) { - info!("Inject `{}` into {:?}", &expect_code, &path_lib_rs); - - let comments = " /* AUTO INJECTED BY flutter_rust_bridge. This line may not be accurate, and you can change it according to your needs. */"; - let modified_content_lib_rs = - format!("{}{}\n{}", expect_code, comments, raw_content_lib_rs); - - fs::write(&path_lib_rs, modified_content_lib_rs).unwrap(); - } - - Ok(()) -} diff --git a/libs/flutter_rust_bridge_codegen/src/parser/mod.rs b/libs/flutter_rust_bridge_codegen/src/parser/mod.rs deleted file mode 100644 index 16de7dd8e..000000000 --- a/libs/flutter_rust_bridge_codegen/src/parser/mod.rs +++ /dev/null @@ -1,353 +0,0 @@ -mod ty; - -use std::string::String; - -use log::debug; -use quote::quote; -use syn::parse::{Parse, ParseStream}; -use syn::punctuated::Punctuated; -use syn::*; - -use crate::ir::*; - -use crate::generator::rust::HANDLER_NAME; -use crate::parser::ty::TypeParser; -use crate::source_graph::Crate; - -const STREAM_SINK_IDENT: &str = "StreamSink"; -const RESULT_IDENT: &str = "Result"; - -pub fn parse(source_rust_content: &str, file: File, manifest_path: &str) -> IrFile { - let crate_map = Crate::new(manifest_path); - - let src_fns = extract_fns_from_file(&file); - let src_structs = crate_map.root_module.collect_structs_to_vec(); - let src_enums = crate_map.root_module.collect_enums_to_vec(); - - let parser = Parser::new(TypeParser::new(src_structs, src_enums)); - parser.parse(source_rust_content, src_fns) -} - -struct Parser<'a> { - type_parser: TypeParser<'a>, -} - -impl<'a> Parser<'a> { - pub fn new(type_parser: TypeParser<'a>) -> Self { - Parser { type_parser } - } -} - -impl<'a> Parser<'a> { - fn parse(mut self, source_rust_content: &str, src_fns: Vec<&ItemFn>) -> IrFile { - let funcs = src_fns.iter().map(|f| self.parse_function(f)).collect(); - - let has_executor = source_rust_content.contains(HANDLER_NAME); - - let (struct_pool, enum_pool) = self.type_parser.consume(); - - IrFile { - funcs, - struct_pool, - enum_pool, - has_executor, - } - } - - /// Attempts to parse the type from the return part of a function signature. There is a special - /// case for top-level `Result` types. - pub fn try_parse_fn_output_type(&mut self, ty: &syn::Type) -> Option { - let inner = ty::SupportedInnerType::try_from_syn_type(ty)?; - - match inner { - ty::SupportedInnerType::Path(ty::SupportedPathType { - ident, - generic: Some(generic), - }) if ident == RESULT_IDENT => Some(IrFuncOutput::ResultType( - self.type_parser.convert_to_ir_type(*generic)?, - )), - _ => Some(IrFuncOutput::Type( - self.type_parser.convert_to_ir_type(inner)?, - )), - } - } - - /// Attempts to parse the type from an argument of a function signature. There is a special - /// case for top-level `StreamSink` types. - pub fn try_parse_fn_arg_type(&mut self, ty: &syn::Type) -> Option { - match ty { - syn::Type::Path(syn::TypePath { path, .. }) => { - let last_segment = path.segments.last().unwrap(); - if last_segment.ident == STREAM_SINK_IDENT { - match &last_segment.arguments { - syn::PathArguments::AngleBracketed( - syn::AngleBracketedGenericArguments { args, .. }, - ) if args.len() == 1 => { - // Unwrap is safe here because args.len() == 1 - match args.last().unwrap() { - syn::GenericArgument::Type(t) => { - Some(IrFuncArg::StreamSinkType(self.type_parser.parse_type(t))) - } - _ => None, - } - } - _ => None, - } - } else { - Some(IrFuncArg::Type(self.type_parser.parse_type(ty))) - } - } - _ => None, - } - } - - fn parse_function(&mut self, func: &ItemFn) -> IrFunc { - debug!("parse_function function name: {:?}", func.sig.ident); - - let sig = &func.sig; - let func_name = sig.ident.to_string(); - - let mut inputs = Vec::new(); - let mut output = None; - let mut mode = None; - let mut fallible = true; - - for sig_input in &sig.inputs { - if let FnArg::Typed(ref pat_type) = sig_input { - let name = if let Pat::Ident(ref pat_ident) = *pat_type.pat { - format!("{}", pat_ident.ident) - } else { - panic!("unexpected pat_type={:?}", pat_type) - }; - - match self.try_parse_fn_arg_type(&pat_type.ty).unwrap_or_else(|| { - panic!( - "Failed to parse function argument type `{}`", - type_to_string(&pat_type.ty) - ) - }) { - IrFuncArg::StreamSinkType(ty) => { - output = Some(ty); - mode = Some(IrFuncMode::Stream); - } - IrFuncArg::Type(ty) => { - inputs.push(IrField { - name: IrIdent::new(name), - ty, - is_final: true, - comments: extract_comments(&pat_type.attrs), - }); - } - } - } else { - panic!("unexpected sig_input={:?}", sig_input); - } - } - - if output.is_none() { - output = Some(match &sig.output { - ReturnType::Type(_, ty) => { - match self.try_parse_fn_output_type(ty).unwrap_or_else(|| { - panic!( - "Failed to parse function output type `{}`", - type_to_string(ty) - ) - }) { - IrFuncOutput::ResultType(ty) => ty, - IrFuncOutput::Type(ty) => { - fallible = false; - ty - } - } - } - ReturnType::Default => { - fallible = false; - IrType::Primitive(IrTypePrimitive::Unit) - } - }); - mode = Some( - if let Some(IrType::Delegate(IrTypeDelegate::SyncReturnVecU8)) = output { - IrFuncMode::Sync - } else { - IrFuncMode::Normal - }, - ); - } - - // let comments = func.attrs.iter().filter_map(extract_comments).collect(); - - IrFunc { - name: func_name, - inputs, - output: output.expect("unsupported output"), - fallible, - mode: mode.expect("unsupported mode"), - comments: extract_comments(&func.attrs), - } - } -} - -fn extract_fns_from_file(file: &File) -> Vec<&ItemFn> { - let mut src_fns = Vec::new(); - - for item in file.items.iter() { - if let Item::Fn(ref item_fn) = item { - if let Visibility::Public(_) = &item_fn.vis { - src_fns.push(item_fn); - } - } - } - - src_fns -} - -fn extract_comments(attrs: &[Attribute]) -> Vec { - attrs - .iter() - .filter_map(|attr| match attr.parse_meta() { - Ok(Meta::NameValue(MetaNameValue { - path, - lit: Lit::Str(lit), - .. - })) if path.is_ident("doc") => Some(IrComment::from(lit.value().as_ref())), - _ => None, - }) - .collect() -} - -pub mod frb_keyword { - syn::custom_keyword!(mirror); - syn::custom_keyword!(non_final); - syn::custom_keyword!(dart_metadata); - syn::custom_keyword!(import); -} - -#[derive(Clone, Debug)] -pub struct NamedOption { - pub name: K, - pub value: V, -} - -impl Parse for NamedOption { - fn parse(input: ParseStream<'_>) -> Result { - let name: K = input.parse()?; - let _: Token![=] = input.parse()?; - let value = input.parse()?; - Ok(Self { name, value }) - } -} - -#[derive(Clone, Debug)] -pub struct MirrorOption(Path); - -impl Parse for MirrorOption { - fn parse(input: ParseStream<'_>) -> Result { - let content; - parenthesized!(content in input); - let path: Path = content.parse()?; - Ok(Self(path)) - } -} - -#[derive(Clone, Debug)] -pub struct MetadataAnnotations(Vec); - -impl Parse for IrDartAnnotation { - fn parse(input: ParseStream<'_>) -> Result { - let annotation: LitStr = input.parse()?; - let library = if input.peek(frb_keyword::import) { - let _ = input.parse::()?; - let library: IrDartImport = input.parse()?; - Some(library) - } else { - None - }; - Ok(Self { - content: annotation.value(), - library, - }) - } -} -impl Parse for MetadataAnnotations { - fn parse(input: ParseStream<'_>) -> Result { - let content; - parenthesized!(content in input); - let annotations = - Punctuated::::parse_terminated(&content)? - .into_iter() - .collect(); - Ok(Self(annotations)) - } -} - -#[derive(Clone, Debug)] -pub struct DartImports(Vec); - -impl Parse for IrDartImport { - fn parse(input: ParseStream<'_>) -> Result { - let uri: LitStr = input.parse()?; - let alias: Option = if input.peek(token::As) { - let _ = input.parse::()?; - let alias: Ident = input.parse()?; - Some(alias.to_string()) - } else { - None - }; - Ok(Self { - uri: uri.value(), - alias, - }) - } -} -impl Parse for DartImports { - fn parse(input: ParseStream<'_>) -> Result { - let content; - parenthesized!(content in input); - let imports = Punctuated::::parse_terminated(&content)? - .into_iter() - .collect(); - Ok(Self(imports)) - } -} - -enum FrbOption { - Mirror(MirrorOption), - NonFinal, - Metadata(NamedOption), -} - -impl Parse for FrbOption { - fn parse(input: ParseStream<'_>) -> Result { - let lookahead = input.lookahead1(); - if lookahead.peek(frb_keyword::mirror) { - input.parse().map(FrbOption::Mirror) - } else if lookahead.peek(frb_keyword::non_final) { - input - .parse::() - .map(|_| FrbOption::NonFinal) - } else if lookahead.peek(frb_keyword::dart_metadata) { - input.parse().map(FrbOption::Metadata) - } else { - Err(lookahead.error()) - } - } -} -fn extract_metadata(attrs: &[Attribute]) -> Vec { - attrs - .iter() - .filter(|attr| attr.path.is_ident("frb")) - .map(|attr| attr.parse_args::()) - .flat_map(|frb_option| match frb_option { - Ok(FrbOption::Metadata(NamedOption { - name: _, - value: MetadataAnnotations(annotations), - })) => annotations, - _ => vec![], - }) - .collect() -} - -/// syn -> string https://github.com/dtolnay/syn/issues/294 -fn type_to_string(ty: &Type) -> String { - quote!(#ty).to_string().replace(' ', "") -} diff --git a/libs/flutter_rust_bridge_codegen/src/parser/ty.rs b/libs/flutter_rust_bridge_codegen/src/parser/ty.rs deleted file mode 100644 index 15cbbce43..000000000 --- a/libs/flutter_rust_bridge_codegen/src/parser/ty.rs +++ /dev/null @@ -1,392 +0,0 @@ -use std::collections::{HashMap, HashSet}; -use std::string::String; - -use syn::*; - -use crate::ir::IrType::*; -use crate::ir::*; - -use crate::markers; - -use crate::source_graph::{Enum, Struct}; - -use crate::parser::{extract_comments, extract_metadata, type_to_string}; - -pub struct TypeParser<'a> { - src_structs: HashMap, - src_enums: HashMap, - - parsing_or_parsed_struct_names: HashSet, - struct_pool: IrStructPool, - - parsed_enums: HashSet, - enum_pool: IrEnumPool, -} - -impl<'a> TypeParser<'a> { - pub fn new( - src_structs: HashMap, - src_enums: HashMap, - ) -> Self { - TypeParser { - src_structs, - src_enums, - struct_pool: HashMap::new(), - enum_pool: HashMap::new(), - parsing_or_parsed_struct_names: HashSet::new(), - parsed_enums: HashSet::new(), - } - } - - pub fn consume(self) -> (IrStructPool, IrEnumPool) { - (self.struct_pool, self.enum_pool) - } -} - -/// Generic intermediate representation of a type that can appear inside a function signature. -#[derive(Debug)] -pub enum SupportedInnerType { - /// Path types with up to 1 generic type argument on the final segment. All segments before - /// the last segment are ignored. The generic type argument must also be a valid - /// `SupportedInnerType`. - Path(SupportedPathType), - /// Array type - Array(Box, usize), - /// The unit type `()`. - Unit, -} - -impl std::fmt::Display for SupportedInnerType { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - match self { - Self::Path(p) => write!(f, "{}", p), - Self::Array(u, len) => write!(f, "[{}; {}]", u, len), - Self::Unit => write!(f, "()"), - } - } -} - -/// Represents a named type, with an optional path and up to 1 generic type argument. -#[derive(Debug)] -pub struct SupportedPathType { - pub ident: syn::Ident, - pub generic: Option>, -} - -impl std::fmt::Display for SupportedPathType { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - let ident = self.ident.to_string(); - if let Some(generic) = &self.generic { - write!(f, "{}<{}>", ident, generic) - } else { - write!(f, "{}", ident) - } - } -} - -impl SupportedInnerType { - /// Given a `syn::Type`, returns a simplified representation of the type if it's supported, - /// or `None` otherwise. - pub fn try_from_syn_type(ty: &syn::Type) -> Option { - match ty { - syn::Type::Path(syn::TypePath { path, .. }) => { - let last_segment = path.segments.last().unwrap().clone(); - match last_segment.arguments { - syn::PathArguments::None => Some(SupportedInnerType::Path(SupportedPathType { - ident: last_segment.ident, - generic: None, - })), - syn::PathArguments::AngleBracketed(a) => { - let generic = match a.args.into_iter().next() { - Some(syn::GenericArgument::Type(t)) => { - Some(Box::new(SupportedInnerType::try_from_syn_type(&t)?)) - } - _ => None, - }; - - Some(SupportedInnerType::Path(SupportedPathType { - ident: last_segment.ident, - generic, - })) - } - _ => None, - } - } - syn::Type::Array(syn::TypeArray { elem, len, .. }) => { - let len: usize = match len { - syn::Expr::Lit(lit) => match &lit.lit { - syn::Lit::Int(x) => x.base10_parse().unwrap(), - _ => panic!("Cannot parse array length"), - }, - _ => panic!("Cannot parse array length"), - }; - Some(SupportedInnerType::Array( - Box::new(SupportedInnerType::try_from_syn_type(elem)?), - len, - )) - } - syn::Type::Tuple(syn::TypeTuple { elems, .. }) if elems.is_empty() => { - Some(SupportedInnerType::Unit) - } - _ => None, - } - } -} - -impl<'a> TypeParser<'a> { - pub fn parse_type(&mut self, ty: &syn::Type) -> IrType { - let supported_type = SupportedInnerType::try_from_syn_type(ty) - .unwrap_or_else(|| panic!("Unsupported type `{}`", type_to_string(ty))); - - self.convert_to_ir_type(supported_type) - .unwrap_or_else(|| panic!("parse_type failed for ty={}", type_to_string(ty))) - } - - /// Converts an inner type into an `IrType` if possible. - pub fn convert_to_ir_type(&mut self, ty: SupportedInnerType) -> Option { - match ty { - SupportedInnerType::Path(p) => self.convert_path_to_ir_type(p), - SupportedInnerType::Array(p, len) => self.convert_array_to_ir_type(*p, len), - SupportedInnerType::Unit => Some(IrType::Primitive(IrTypePrimitive::Unit)), - } - } - - /// Converts an array type into an `IrType` if possible. - pub fn convert_array_to_ir_type( - &mut self, - generic: SupportedInnerType, - _len: usize, - ) -> Option { - self.convert_to_ir_type(generic).map(|inner| match inner { - Primitive(primitive) => PrimitiveList(IrTypePrimitiveList { primitive }), - others => GeneralList(IrTypeGeneralList { - inner: Box::new(others), - }), - }) - } - - /// Converts a path type into an `IrType` if possible. - pub fn convert_path_to_ir_type(&mut self, p: SupportedPathType) -> Option { - let p_as_str = format!("{}", &p); - let ident_string = &p.ident.to_string(); - if let Some(generic) = p.generic { - match ident_string.as_str() { - "SyncReturn" => { - // Special-case SyncReturn>. SyncReturn for any other type is not - // supported. - match *generic { - SupportedInnerType::Path(SupportedPathType { - ident, - generic: Some(generic), - }) if ident == "Vec" => match *generic { - SupportedInnerType::Path(SupportedPathType { - ident, - generic: None, - }) if ident == "u8" => { - Some(IrType::Delegate(IrTypeDelegate::SyncReturnVecU8)) - } - _ => None, - }, - _ => None, - } - } - "Vec" => { - // Special-case Vec as StringList - if matches!(*generic, SupportedInnerType::Path(SupportedPathType { ref ident, .. }) if ident == "String") - { - Some(IrType::Delegate(IrTypeDelegate::StringList)) - } else { - self.convert_to_ir_type(*generic).map(|inner| match inner { - Primitive(primitive) => { - PrimitiveList(IrTypePrimitiveList { primitive }) - } - others => GeneralList(IrTypeGeneralList { - inner: Box::new(others), - }), - }) - } - } - "ZeroCopyBuffer" => { - let inner = self.convert_to_ir_type(*generic); - if let Some(IrType::PrimitiveList(IrTypePrimitiveList { primitive })) = inner { - Some(IrType::Delegate( - IrTypeDelegate::ZeroCopyBufferVecPrimitive(primitive), - )) - } else { - None - } - } - "Box" => self.convert_to_ir_type(*generic).map(|inner| { - Boxed(IrTypeBoxed { - exist_in_real_api: true, - inner: Box::new(inner), - }) - }), - "Option" => { - // Disallow nested Option - if matches!(*generic, SupportedInnerType::Path(SupportedPathType { ref ident, .. }) if ident == "Option") - { - panic!( - "Nested optionals without indirection are not supported. (Option>)", - p_as_str - ); - } - self.convert_to_ir_type(*generic).map(|inner| match inner { - Primitive(prim) => IrType::Optional(IrTypeOptional::new_prim(prim)), - st @ StructRef(_) => { - IrType::Optional(IrTypeOptional::new_ptr(Boxed(IrTypeBoxed { - inner: Box::new(st), - exist_in_real_api: false, - }))) - } - other => IrType::Optional(IrTypeOptional::new_ptr(other)), - }) - } - _ => None, - } - } else { - IrTypePrimitive::try_from_rust_str(ident_string) - .map(Primitive) - .or_else(|| { - if ident_string == "String" { - Some(IrType::Delegate(IrTypeDelegate::String)) - } else if self.src_structs.contains_key(ident_string) { - if !self.parsing_or_parsed_struct_names.contains(ident_string) { - self.parsing_or_parsed_struct_names - .insert(ident_string.to_owned()); - let api_struct = self.parse_struct_core(&p.ident); - self.struct_pool.insert(ident_string.to_owned(), api_struct); - } - - Some(StructRef(IrTypeStructRef { - name: ident_string.to_owned(), - freezed: self - .struct_pool - .get(ident_string) - .map(IrStruct::using_freezed) - .unwrap_or(false), - })) - } else if self.src_enums.contains_key(ident_string) { - if self.parsed_enums.insert(ident_string.to_owned()) { - let enu = self.parse_enum_core(&p.ident); - self.enum_pool.insert(ident_string.to_owned(), enu); - } - - Some(EnumRef(IrTypeEnumRef { - name: ident_string.to_owned(), - is_struct: self - .enum_pool - .get(ident_string) - .map(IrEnum::is_struct) - .unwrap_or(true), - })) - } else { - None - } - }) - } - } -} - -impl<'a> TypeParser<'a> { - fn parse_enum_core(&mut self, ident: &syn::Ident) -> IrEnum { - let src_enum = self.src_enums[&ident.to_string()]; - let name = src_enum.ident.to_string(); - let wrapper_name = if src_enum.mirror { - Some(format!("mirror_{}", name)) - } else { - None - }; - let path = src_enum.path.clone(); - let comments = extract_comments(&src_enum.src.attrs); - let variants = src_enum - .src - .variants - .iter() - .map(|variant| IrVariant { - name: IrIdent::new(variant.ident.to_string()), - comments: extract_comments(&variant.attrs), - kind: match variant.fields.iter().next() { - None => IrVariantKind::Value, - Some(Field { - attrs, - ident: field_ident, - .. - }) => { - let variant_ident = variant.ident.to_string(); - IrVariantKind::Struct(IrStruct { - name: variant_ident, - wrapper_name: None, - path: None, - is_fields_named: field_ident.is_some(), - dart_metadata: extract_metadata(attrs), - comments: extract_comments(attrs), - fields: variant - .fields - .iter() - .enumerate() - .map(|(idx, field)| IrField { - name: IrIdent::new( - field - .ident - .as_ref() - .map(ToString::to_string) - .unwrap_or_else(|| format!("field{}", idx)), - ), - ty: self.parse_type(&field.ty), - is_final: true, - comments: extract_comments(&field.attrs), - }) - .collect(), - }) - } - }, - }) - .collect(); - IrEnum::new(name, wrapper_name, path, comments, variants) - } - - fn parse_struct_core(&mut self, ident: &syn::Ident) -> IrStruct { - let src_struct = self.src_structs[&ident.to_string()]; - let mut fields = Vec::new(); - - let (is_fields_named, struct_fields) = match &src_struct.src.fields { - Fields::Named(FieldsNamed { named, .. }) => (true, named), - Fields::Unnamed(FieldsUnnamed { unnamed, .. }) => (false, unnamed), - _ => panic!("unsupported type: {:?}", src_struct.src.fields), - }; - - for (idx, field) in struct_fields.iter().enumerate() { - let field_name = field - .ident - .as_ref() - .map_or(format!("field{}", idx), ToString::to_string); - let field_type = self.parse_type(&field.ty); - fields.push(IrField { - name: IrIdent::new(field_name), - ty: field_type, - is_final: !markers::has_non_final(&field.attrs), - comments: extract_comments(&field.attrs), - }); - } - - let name = src_struct.ident.to_string(); - let wrapper_name = if src_struct.mirror { - Some(format!("mirror_{}", name)) - } else { - None - }; - let path = Some(src_struct.path.clone()); - let metadata = extract_metadata(&src_struct.src.attrs); - let comments = extract_comments(&src_struct.src.attrs); - IrStruct { - name, - wrapper_name, - path, - fields, - is_fields_named, - dart_metadata: metadata, - comments, - } - } -} diff --git a/libs/flutter_rust_bridge_codegen/src/source_graph.rs b/libs/flutter_rust_bridge_codegen/src/source_graph.rs deleted file mode 100644 index de9e3cbfe..000000000 --- a/libs/flutter_rust_bridge_codegen/src/source_graph.rs +++ /dev/null @@ -1,553 +0,0 @@ -/* - Things this doesn't currently support that it might need to later: - - - Import parsing is unfinished and so is currently disabled - - When import parsing is enabled: - - Import renames (use a::b as c) - these are silently ignored - - Imports that start with two colons (use ::a::b) - these are also silently ignored -*/ - -use std::{collections::HashMap, fmt::Debug, fs, path::PathBuf}; - -use cargo_metadata::MetadataCommand; -use log::{debug, warn}; -use syn::{Attribute, Ident, ItemEnum, ItemStruct, UseTree}; - -use crate::markers; - -/// Represents a crate, including a map of its modules, imports, structs and -/// enums. -#[derive(Debug, Clone)] -pub struct Crate { - pub name: String, - pub manifest_path: PathBuf, - pub root_src_file: PathBuf, - pub root_module: Module, -} - -impl Crate { - pub fn new(manifest_path: &str) -> Self { - let mut cmd = MetadataCommand::new(); - cmd.manifest_path(&manifest_path); - - let metadata = cmd.exec().unwrap(); - - let root_package = metadata.root_package().unwrap(); - let root_src_file = { - let lib_file = root_package - .manifest_path - .parent() - .unwrap() - .join("src/lib.rs"); - let main_file = root_package - .manifest_path - .parent() - .unwrap() - .join("src/main.rs"); - - if lib_file.exists() { - fs::canonicalize(lib_file).unwrap() - } else if main_file.exists() { - fs::canonicalize(main_file).unwrap() - } else { - panic!("No src/lib.rs or src/main.rs found for this Cargo.toml file"); - } - }; - - let source_rust_content = fs::read_to_string(&root_src_file).unwrap(); - let file_ast = syn::parse_file(&source_rust_content).unwrap(); - - let mut result = Crate { - name: root_package.name.clone(), - manifest_path: fs::canonicalize(manifest_path).unwrap(), - root_src_file: root_src_file.clone(), - root_module: Module { - visibility: Visibility::Public, - file_path: root_src_file, - module_path: vec!["crate".to_string()], - source: Some(ModuleSource::File(file_ast)), - scope: None, - }, - }; - - result.resolve(); - - result - } - - /// Create a map of the modules for this crate - pub fn resolve(&mut self) { - self.root_module.resolve(); - } -} - -/// Mirrors syn::Visibility, but can be created without a token -#[derive(Debug, Clone)] -pub enum Visibility { - Public, - Crate, - Restricted, // Not supported - Inherited, // Usually means private -} - -fn syn_vis_to_visibility(vis: &syn::Visibility) -> Visibility { - match vis { - syn::Visibility::Public(_) => Visibility::Public, - syn::Visibility::Crate(_) => Visibility::Crate, - syn::Visibility::Restricted(_) => Visibility::Restricted, - syn::Visibility::Inherited => Visibility::Inherited, - } -} - -#[derive(Debug, Clone)] -pub struct Import { - pub path: Vec, - pub visibility: Visibility, -} - -#[derive(Debug, Clone)] -pub enum ModuleSource { - File(syn::File), - ModuleInFile(Vec), -} - -#[derive(Clone)] -pub struct Struct { - pub ident: Ident, - pub src: ItemStruct, - pub visibility: Visibility, - pub path: Vec, - pub mirror: bool, -} - -impl Debug for Struct { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Struct") - .field("ident", &self.ident) - .field("src", &"omitted") - .field("visibility", &self.visibility) - .field("path", &self.path) - .field("mirror", &self.mirror) - .finish() - } -} - -#[derive(Clone)] -pub struct Enum { - pub ident: Ident, - pub src: ItemEnum, - pub visibility: Visibility, - pub path: Vec, - pub mirror: bool, -} - -impl Debug for Enum { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Enum") - .field("ident", &self.ident) - .field("src", &"omitted") - .field("visibility", &self.visibility) - .field("path", &self.path) - .field("mirror", &self.mirror) - .finish() - } -} - -#[derive(Debug, Clone)] -pub struct ModuleScope { - pub modules: Vec, - pub enums: Vec, - pub structs: Vec, - pub imports: Vec, -} - -#[derive(Clone)] -pub struct Module { - pub visibility: Visibility, - pub file_path: PathBuf, - pub module_path: Vec, - pub source: Option, - pub scope: Option, -} - -impl Debug for Module { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Module") - .field("visibility", &self.visibility) - .field("module_path", &self.module_path) - .field("file_path", &self.file_path) - .field("source", &"omitted") - .field("scope", &self.scope) - .finish() - } -} - -/// Get a struct or enum ident, possibly remapped by a mirror marker -fn get_ident(ident: &Ident, attrs: &[Attribute]) -> (Ident, bool) { - markers::extract_mirror_marker(attrs) - .and_then(|path| path.get_ident().map(|ident| (ident.clone(), true))) - .unwrap_or_else(|| (ident.clone(), false)) -} - -impl Module { - pub fn resolve(&mut self) { - self.resolve_modules(); - // self.resolve_imports(); - } - - /// Maps out modules, structs and enums within the scope of this module - fn resolve_modules(&mut self) { - let mut scope_modules = Vec::new(); - let mut scope_structs = Vec::new(); - let mut scope_enums = Vec::new(); - - let items = match self.source.as_ref().unwrap() { - ModuleSource::File(file) => &file.items, - ModuleSource::ModuleInFile(items) => items, - }; - - for item in items.iter() { - match item { - syn::Item::Struct(item_struct) => { - let (ident, mirror) = get_ident(&item_struct.ident, &item_struct.attrs); - let ident_str = ident.to_string(); - scope_structs.push(Struct { - ident, - src: item_struct.clone(), - visibility: syn_vis_to_visibility(&item_struct.vis), - path: { - let mut path = self.module_path.clone(); - path.push(ident_str); - path - }, - mirror, - }); - } - syn::Item::Enum(item_enum) => { - let (ident, mirror) = get_ident(&item_enum.ident, &item_enum.attrs); - let ident_str = ident.to_string(); - scope_enums.push(Enum { - ident, - src: item_enum.clone(), - visibility: syn_vis_to_visibility(&item_enum.vis), - path: { - let mut path = self.module_path.clone(); - path.push(ident_str); - path - }, - mirror, - }); - } - syn::Item::Mod(item_mod) => { - let ident = item_mod.ident.clone(); - - let mut module_path = self.module_path.clone(); - module_path.push(ident.to_string()); - - scope_modules.push(match &item_mod.content { - Some(content) => { - let mut child_module = Module { - visibility: syn_vis_to_visibility(&item_mod.vis), - file_path: self.file_path.clone(), - module_path, - source: Some(ModuleSource::ModuleInFile(content.1.clone())), - scope: None, - }; - - child_module.resolve(); - - child_module - } - None => { - let folder_path = - self.file_path.parent().unwrap().join(ident.to_string()); - let folder_exists = folder_path.exists(); - - let file_path = if folder_exists { - folder_path.join("mod.rs") - } else { - self.file_path - .parent() - .unwrap() - .join(ident.to_string() + ".rs") - }; - - let file_exists = file_path.exists(); - - if !file_exists { - warn!( - "Skipping unresolvable module {} (tried {})", - &ident, - file_path.to_string_lossy() - ); - continue; - } - - let source = if file_exists { - let source_rust_content = fs::read_to_string(&file_path).unwrap(); - debug!("Trying to parse {:?}", file_path); - Some(ModuleSource::File( - syn::parse_file(&source_rust_content).unwrap(), - )) - } else { - None - }; - - let mut child_module = Module { - visibility: syn_vis_to_visibility(&item_mod.vis), - file_path, - module_path, - source, - scope: None, - }; - - if file_exists { - child_module.resolve(); - } - - child_module - } - }); - } - _ => {} - } - } - - self.scope = Some(ModuleScope { - modules: scope_modules, - enums: scope_enums, - structs: scope_structs, - imports: vec![], // Will be filled in by resolve_imports() - }); - } - - #[allow(dead_code)] - fn resolve_imports(&mut self) { - let imports = &mut self.scope.as_mut().unwrap().imports; - - let items = match self.source.as_ref().unwrap() { - ModuleSource::File(file) => &file.items, - ModuleSource::ModuleInFile(items) => items, - }; - - for item in items.iter() { - if let syn::Item::Use(item_use) = item { - let flattened_imports = flatten_use_tree(&item_use.tree); - - for import in flattened_imports { - imports.push(Import { - path: import, - visibility: syn_vis_to_visibility(&item_use.vis), - }); - } - } - } - } - - pub fn collect_structs<'a>(&'a self, container: &mut HashMap) { - let scope = self.scope.as_ref().unwrap(); - for scope_struct in &scope.structs { - container.insert(scope_struct.ident.to_string(), scope_struct); - } - for scope_module in &scope.modules { - scope_module.collect_structs(container); - } - } - - pub fn collect_structs_to_vec(&self) -> HashMap { - let mut ans = HashMap::new(); - self.collect_structs(&mut ans); - ans - } - - pub fn collect_enums<'a>(&'a self, container: &mut HashMap) { - let scope = self.scope.as_ref().unwrap(); - for scope_enum in &scope.enums { - container.insert(scope_enum.ident.to_string(), scope_enum); - } - for scope_module in &scope.modules { - scope_module.collect_enums(container); - } - } - - pub fn collect_enums_to_vec(&self) -> HashMap { - let mut ans = HashMap::new(); - self.collect_enums(&mut ans); - ans - } -} - -fn flatten_use_tree_rename_abort_warning(use_tree: &UseTree) { - debug!("WARNING: flatten_use_tree() found an import rename (use a::b as c). flatten_use_tree() will now abort."); - debug!("WARNING: This happened while parsing {:?}", use_tree); - debug!("WARNING: This use statement will be ignored."); -} - -/// Takes a use tree and returns a flat list of use paths (list of string tokens) -/// -/// Example: -/// use a::{b::c, d::e}; -/// becomes -/// [ -/// ["a", "b", "c"], -/// ["a", "d", "e"] -/// ] -/// -/// Warning: As of writing, import renames (import a::b as c) are silently -/// ignored. -fn flatten_use_tree(use_tree: &UseTree) -> Vec> { - // Vec<(path, is_complete)> - let mut result = vec![(vec![], false)]; - - let mut counter: usize = 0; - - loop { - counter += 1; - - if counter > 10000 { - panic!("flatten_use_tree: Use statement complexity limit exceeded. This is probably a bug."); - } - - // If all paths are complete, break from the loop - if result.iter().all(|result_item| result_item.1) { - break; - } - - let mut items_to_push = Vec::new(); - - for path_tuple in &mut result { - let path = &mut path_tuple.0; - let is_complete = &mut path_tuple.1; - - if *is_complete { - continue; - } - - let mut tree_cursor = use_tree; - - for path_item in path.iter() { - match tree_cursor { - UseTree::Path(use_path) => { - let ident = use_path.ident.to_string(); - if *path_item != ident { - panic!("This ident did not match the one we already collected. This is a bug."); - } - tree_cursor = use_path.tree.as_ref(); - } - UseTree::Group(use_group) => { - let mut moved_tree_cursor = false; - - for tree in use_group.items.iter() { - match tree { - UseTree::Path(use_path) => { - if path_item == &use_path.ident.to_string() { - tree_cursor = use_path.tree.as_ref(); - moved_tree_cursor = true; - break; - } - } - // Since we're not matching UseTree::Group here, a::b::{{c}, {d}} might - // break. But also why would anybody do that - _ => unreachable!(), - } - } - - if !moved_tree_cursor { - unreachable!(); - } - } - _ => unreachable!(), - } - } - - match tree_cursor { - UseTree::Name(use_name) => { - path.push(use_name.ident.to_string()); - *is_complete = true; - } - UseTree::Path(use_path) => { - path.push(use_path.ident.to_string()); - } - UseTree::Glob(_) => { - path.push("*".to_string()); - *is_complete = true; - } - UseTree::Group(use_group) => { - // We'll modify the first one in-place, and make clones for - // all subsequent ones - let mut first: bool = true; - // Capture the path in this state, since we're about to - // modify it - let path_copy = path.clone(); - for tree in use_group.items.iter() { - let mut new_path_tuple = if first { - None - } else { - let new_path = path_copy.clone(); - items_to_push.push((new_path, false)); - Some(items_to_push.iter_mut().last().unwrap()) - }; - - match tree { - UseTree::Path(use_path) => { - let ident = use_path.ident.to_string(); - - if first { - path.push(ident); - } else { - new_path_tuple.unwrap().0.push(ident); - } - } - UseTree::Name(use_name) => { - let ident = use_name.ident.to_string(); - - if first { - path.push(ident); - *is_complete = true; - } else { - let path_tuple = new_path_tuple.as_mut().unwrap(); - path_tuple.0.push(ident); - path_tuple.1 = true; - } - } - UseTree::Glob(_) => { - if first { - path.push("*".to_string()); - *is_complete = true; - } else { - let path_tuple = new_path_tuple.as_mut().unwrap(); - path_tuple.0.push("*".to_string()); - path_tuple.1 = true; - } - } - UseTree::Group(_) => { - panic!( - "Directly-nested use groups ({}) are not supported by flutter_rust_bridge. Use {} instead.", - "use a::{{b}, c}", - "a::{b, c}" - ); - } - // UseTree::Group(_) => panic!(), - UseTree::Rename(_) => { - flatten_use_tree_rename_abort_warning(use_tree); - return vec![]; - } - } - - first = false; - } - } - UseTree::Rename(_) => { - flatten_use_tree_rename_abort_warning(use_tree); - return vec![]; - } - } - } - - for item in items_to_push { - result.push(item); - } - } - - result.into_iter().map(|val| val.0).collect() -} diff --git a/libs/flutter_rust_bridge_codegen/src/transformer.rs b/libs/flutter_rust_bridge_codegen/src/transformer.rs deleted file mode 100644 index 3ca79620e..000000000 --- a/libs/flutter_rust_bridge_codegen/src/transformer.rs +++ /dev/null @@ -1,46 +0,0 @@ -use log::debug; - -use crate::ir::IrType::*; -use crate::ir::*; - -pub fn transform(src: IrFile) -> IrFile { - let dst_funcs = src - .funcs - .into_iter() - .map(|src_func| IrFunc { - inputs: src_func - .inputs - .into_iter() - .map(transform_func_input_add_boxed) - .collect(), - ..src_func - }) - .collect(); - - IrFile { - funcs: dst_funcs, - ..src - } -} - -fn transform_func_input_add_boxed(input: IrField) -> IrField { - match &input.ty { - StructRef(_) - | EnumRef(IrTypeEnumRef { - is_struct: true, .. - }) => { - debug!( - "transform_func_input_add_boxed wrap Boxed to field={:?}", - input - ); - IrField { - ty: Boxed(IrTypeBoxed { - exist_in_real_api: false, // <-- - inner: Box::new(input.ty.clone()), - }), - ..input - } - } - _ => input, - } -} diff --git a/libs/flutter_rust_bridge_codegen/src/utils.rs b/libs/flutter_rust_bridge_codegen/src/utils.rs deleted file mode 100644 index fa822b808..000000000 --- a/libs/flutter_rust_bridge_codegen/src/utils.rs +++ /dev/null @@ -1,26 +0,0 @@ -use std::fs; -use std::path::Path; - -pub fn mod_from_rust_path(code_path: &str, crate_path: &str) -> String { - Path::new(code_path) - .strip_prefix(Path::new(crate_path).join("src")) - .unwrap() - .with_extension("") - .into_os_string() - .into_string() - .unwrap() - .replace('/', "::") -} - -pub fn with_changed_file anyhow::Result<()>>( - path: &str, - append_content: &str, - f: F, -) -> anyhow::Result<()> { - let content_original = fs::read_to_string(&path)?; - fs::write(&path, content_original.clone() + append_content)?; - - f()?; - - Ok(fs::write(&path, content_original)?) -} From 5274a43a34b24273ae94fedb439e9d77e6000192 Mon Sep 17 00:00:00 2001 From: csf Date: Tue, 31 May 2022 17:36:36 +0800 Subject: [PATCH 0038/2015] update sessions public function --- flutter/lib/desktop/pages/remote_page.dart | 16 +- flutter/lib/models/model.dart | 63 +- src/client/file_trait.rs | 39 +- src/flutter.rs | 12 +- src/flutter_ffi.rs | 772 +++++++++++++-------- 5 files changed, 602 insertions(+), 300 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index b7d567482..49beb7819 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -59,7 +59,7 @@ class _RemotePageState extends State with WindowListener { Wakelock.enable(); } _physicalFocusNode.requestFocus(); - FFI.ffiModel.updateEventListener(widget.id); + // FFI.ffiModel.updateEventListener(widget.id); FFI.listenToMouse(true); WindowManager.instance.addListener(this); } @@ -599,10 +599,18 @@ class _RemotePageState extends State with WindowListener { Widget getBodyForDesktopWithListener(bool keyboard) { var paints = [ImagePaint()]; + final cursor = await; if (keyboard || FFI.getByName('toggle_option', 'show-remote-cursor') == 'true') { paints.add(CursorPaint()); } + return FutureBuilder( + future: FFI.rustdeskImpl + .getSessionToggleOption(id: widget.id, arg: 'show-remote-cursor'), + builder: (ctx, snapshot) { + if(snapshot) + }, + ); return Container( color: MyTheme.canvasColor, child: Stack(children: paints)); } @@ -974,9 +982,11 @@ RadioListTile getRadio(String name, String toValue, String curValue, void showOptions(String id) async { // String quality = FFI.getByName('image_quality'); - String quality = await FFI.rustdeskImpl.getImageQuality(id: id) ?? 'balanced'; + String quality = + await FFI.rustdeskImpl.getSessionImageQuality(id: id) ?? 'balanced'; if (quality == '') quality = 'balanced'; - String viewStyle = FFI.getByName('peer_option', 'view-style'); + String viewStyle = + await FFI.rustdeskImpl.getSessionOption(id: id, arg: 'view-style') ?? ''; var displays = []; final pi = FFI.ffiModel.pi; final image = FFI.ffiModel.getConnectionImage(); diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 6590dc41a..4ced438db 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -122,6 +122,53 @@ class FfiModel with ChangeNotifier { _permissions.clear(); } + void Function(Map) startEventListener(String peerId) { + return (evt) { + var name = evt['name']; + if (name == 'msgbox') { + handleMsgBox(evt, peerId); + } else if (name == 'peer_info') { + handlePeerInfo(evt); + } else if (name == 'connection_ready') { + FFI.ffiModel.setConnectionType( + evt['secure'] == 'true', evt['direct'] == 'true'); + } else if (name == 'switch_display') { + handleSwitchDisplay(evt); + } else if (name == 'cursor_data') { + FFI.cursorModel.updateCursorData(evt); + } else if (name == 'cursor_id') { + FFI.cursorModel.updateCursorId(evt); + } else if (name == 'cursor_position') { + FFI.cursorModel.updateCursorPosition(evt); + } else if (name == 'clipboard') { + Clipboard.setData(ClipboardData(text: evt['content'])); + } else if (name == 'permission') { + FFI.ffiModel.updatePermission(evt); + } else if (name == 'chat_client_mode') { + FFI.chatModel.receive(ChatModel.clientModeID, evt['text'] ?? ""); + } else if (name == 'chat_server_mode') { + FFI.chatModel + .receive(int.parse(evt['id'] as String), evt['text'] ?? ""); + } else if (name == 'file_dir') { + FFI.fileModel.receiveFileDir(evt); + } else if (name == 'job_progress') { + FFI.fileModel.tryUpdateJobProgress(evt); + } else if (name == 'job_done') { + FFI.fileModel.jobDone(evt); + } else if (name == 'job_error') { + FFI.fileModel.jobError(evt); + } else if (name == 'override_file_confirm') { + FFI.fileModel.overrideFileConfirm(evt); + } else if (name == 'try_start_without_auth') { + FFI.serverModel.loginRequest(evt); + } else if (name == 'on_client_authorized') { + FFI.serverModel.onClientAuthorized(evt); + } else if (name == 'on_client_remove') { + FFI.serverModel.onClientRemove(evt); + } + }; + } + /// Bind the event listener to receive events from the Rust core. void updateEventListener(String peerId) { final void Function(Map) cb = (evt) { @@ -782,9 +829,19 @@ class FFI { } else { FFI.chatModel.resetClientMode(); // setByName('connect', id); - final stream = - FFI.rustdeskImpl.connect(id: id, isFileTransfer: isFileTransfer); - // listen stream ... + final event_stream = FFI.rustdeskImpl + .sessionConnect(id: id, isFileTransfer: isFileTransfer); + final cb = FFI.ffiModel.startEventListener(id); + () async { + await for (final message in event_stream) { + try { + Map event = json.decode(message); + cb(event); + } catch (e) { + print('json.decode fail(): $e'); + } + } + }(); // every instance will bind a stream } FFI.id = id; diff --git a/src/client/file_trait.rs b/src/client/file_trait.rs index 5dc4cd786..7b1da3e0a 100644 --- a/src/client/file_trait.rs +++ b/src/client/file_trait.rs @@ -1,16 +1,13 @@ use super::{Data, Interface}; -use hbb_common::{ - fs, - message_proto::*, -}; +use hbb_common::{fs, message_proto::*}; pub trait FileManager: Interface { - fn get_home_dir(&self) -> String{ + fn get_home_dir(&self) -> String { fs::get_home_as_string() } #[cfg(not(any(target_os = "android", target_os = "ios")))] - fn read_dir(&self,path: String, include_hidden: bool) -> sciter::Value { + 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(), Ok(fd) => { @@ -23,11 +20,11 @@ pub trait FileManager: Interface { } #[cfg(any(target_os = "android", target_os = "ios"))] - fn read_dir(&self,path: &str, include_hidden: bool) -> String { + fn read_dir(&self, path: &str, include_hidden: bool) -> String { use crate::flutter::make_fd_to_json; - match fs::read_dir(&fs::get_path(path), include_hidden){ + match fs::read_dir(&fs::get_path(path), include_hidden) { Ok(fd) => make_fd_to_json(fd), - Err(_)=>"".into() + Err(_) => "".into(), } } @@ -76,7 +73,7 @@ pub trait FileManager: Interface { } fn send_files( - &mut self, + &self, id: i32, path: String, to: String, @@ -84,7 +81,14 @@ pub trait FileManager: Interface { include_hidden: bool, is_remote: bool, ) { - self.send(Data::SendFiles((id, path, to, file_num, include_hidden, is_remote))); + self.send(Data::SendFiles(( + id, + path, + to, + file_num, + include_hidden, + is_remote, + ))); } fn add_job( @@ -96,10 +100,17 @@ pub trait FileManager: Interface { include_hidden: bool, is_remote: bool, ) { - self.send(Data::AddJob((id, path, to, file_num, include_hidden, is_remote))); + self.send(Data::AddJob(( + id, + path, + to, + file_num, + include_hidden, + is_remote, + ))); } - fn resume_job(&mut self, id: i32, is_remote: bool){ - self.send(Data::ResumeJob((id,is_remote))); + fn resume_job(&mut self, id: i32, is_remote: bool) { + self.send(Data::ResumeJob((id, is_remote))); } } diff --git a/src/flutter.rs b/src/flutter.rs index c24923c72..3872710c0 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -27,14 +27,14 @@ use std::{ lazy_static::lazy_static! { // static ref SESSION: Arc>> = Default::default(); - static ref SESSIONS: RwLock> = Default::default(); + pub static ref SESSIONS: RwLock> = Default::default(); pub static ref EVENT_STREAM: RwLock>> = Default::default(); // rust to dart event channel pub static ref RGBA_STREAM: RwLock>>>> = Default::default(); // rust to dart rgba (big u8 list) channel } -pub fn get_session(id: &str) -> Option<&Session> { - SESSIONS.read().unwrap().get(id) -} +// pub fn get_session<'a>(id: &str) -> Option<&'a Session> { +// SESSIONS.read().unwrap().get(id) +// } #[derive(Clone)] pub struct Session { @@ -102,7 +102,7 @@ impl Session { /// * `value` - The value of the option to set. pub fn set_option(&self, name: String, value: String) { let mut value = value; - let lc = self.lc.write().unwrap(); + let mut lc = self.lc.write().unwrap(); if name == "remote_dir" { value = lc.get_all_remote_dir(value); } @@ -367,7 +367,7 @@ impl Session { /// /// # Arguments /// - /// * `value` - The text to input. + /// * `value` - The text to input. TODO &str -> String pub fn input_string(&self, value: &str) { let mut key_event = KeyEvent::new(); key_event.set_seq(value.to_owned()); diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 5d1ca2368..10d1257db 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1,9 +1,9 @@ use crate::client::file_trait::FileManager; use crate::flutter::connection_manager::{self, get_clients_length, get_clients_state}; -use crate::flutter::{self, get_session, make_fd_to_json, Session}; +use crate::flutter::{self, make_fd_to_json, Session, SESSIONS}; use crate::start_server; use crate::ui_interface; -use flutter_rust_bridge::{StreamSink, ZeroCopyBuffer}; +use flutter_rust_bridge::{StreamSink, SyncReturn, ZeroCopyBuffer}; use hbb_common::ResultType; use hbb_common::{ config::{self, Config, LocalConfig, PeerConfig, ONLINE}, @@ -69,13 +69,236 @@ pub fn start_rgba_stream(s: StreamSink>>) -> ResultType<( Ok(()) } -pub fn connect(id: String, is_file_transfer: bool, events2ui: StreamSink) { +pub fn session_connect( + events2ui: StreamSink, + id: String, + is_file_transfer: bool, +) -> ResultType<()> { Session::start(&id, is_file_transfer, events2ui); + Ok(()) } -pub fn get_image_quality(id: String) -> Option { - let session = get_session(&id)?; - Some(session.get_image_quality()) +pub fn get_session_remember(id: String) -> Option { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + Some(session.get_remember()) + } else { + None + } +} + +pub fn get_session_toggle_option(id: String, arg: String) -> Option { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + Some(session.get_toggle_option(&arg)) + } else { + None + } +} + +pub fn get_session_image_quality(id: String) -> SyncReturn> { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + SyncReturn(Some(session.get_image_quality())) + } else { + SyncReturn(None) + } +} + +pub fn get_session_option(id: String, arg: String) -> Option { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + Some(session.get_option(&arg)) + } else { + None + } +} + +// void +pub fn session_login(id: String, password: String, remember: bool) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.login(&password, remember); + } +} + +pub fn session_close(id: String) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.close(); + } +} + +pub fn session_refresh(id: String) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.refresh(); + } +} + +pub fn session_reconnect(id: String) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.reconnect(); + } +} + +pub fn session_toggle_option(id: String, value: String) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.toggle_option(&value); + } +} + +pub fn session_set_image_quality(id: String, value: String) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.set_image_quality(&value); + } +} + +pub fn session_lock_screen(id: String) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.lock_screen(); + } +} + +pub fn session_ctrl_alt_del(id: String) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.ctrl_alt_del(); + } +} + +pub fn session_switch_display(id: String, value: i32) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.switch_display(value); + } +} + +pub fn session_input_key( + id: String, + name: String, + down: bool, + press: bool, + alt: bool, + ctrl: bool, + shift: bool, + command: bool, +) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.input_key(&name, down, press, alt, ctrl, shift, command); + } +} + +pub fn session_input_string(id: String, value: String) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.input_string(&value); + } +} + +// chat_client_mode +pub fn session_send_chat(id: String, text: String) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.send_chat(text); + } +} + +// if let Some(_type) = m.get("type") { +// mask = match _type.as_str() { +// "down" => 1, +// "up" => 2, +// "wheel" => 3, +// _ => 0, +// }; +// } +// if let Some(buttons) = m.get("buttons") { +// mask |= match buttons.as_str() { +// "left" => 1, +// "right" => 2, +// "wheel" => 4, +// _ => 0, +// } << 3; +// } +// TODO +pub fn session_send_mouse( + id: String, + mask: i32, + x: i32, + y: i32, + alt: bool, + ctrl: bool, + shift: bool, + command: bool, +) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.send_mouse(mask, x, y, alt, ctrl, shift, command); + } +} + +pub fn session_peer_option(id: String, name: String, value: String) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.set_option(name, value); + } +} + +pub fn session_input_os_password(id: String, value: String) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.input_os_password(value, true); + } +} + +// File Action +pub fn session_read_remote_dir(id: String, path: String, include_hidden: bool) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.read_remote_dir(path, include_hidden); + } +} + +pub fn session_send_files( + id: String, + act_id: i32, + path: String, + to: String, + file_num: i32, + include_hidden: bool, + is_remote: bool, +) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.send_files(act_id, path, to, file_num, include_hidden, is_remote); + } +} + +pub fn session_set_confirm_override_file( + id: String, + act_id: i32, + file_num: i32, + need_override: bool, + remember: bool, + is_upload: bool, +) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.set_confirm_override_file(act_id, file_num, need_override, remember, is_upload); + } +} + +pub fn session_remove_file(id: String, act_id: i32, path: String, file_num: i32, is_remote: bool) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.remove_file(act_id, path, file_num, is_remote); + } +} + +pub fn session_read_dir_recursive(id: String, act_id: i32, path: String, is_remote: bool) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.remove_dir_all(act_id, path, is_remote); + } +} + +pub fn session_remove_all_empty_dirs(id: String, act_id: i32, path: String, is_remote: bool) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.remove_dir(act_id, path, is_remote); + } +} + +pub fn session_cancel_job(id: String, act_id: i32) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.cancel_job(act_id); + } +} + +pub fn session_create_dir(id: String, act_id: i32, path: String, is_remote: bool) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.create_dir(act_id, path, is_remote); + } } /// FFI for **get** commands which are idempotent. @@ -106,16 +329,16 @@ unsafe extern "C" fn get_by_name(name: *const c_char, arg: *const c_char) -> *co res = LocalConfig::get_remote_id(); } } - "remember" => { - res = Session::get_remember().to_string(); - } - "toggle_option" => { - if let Ok(arg) = arg.to_str() { - if let Some(v) = Session::get_toggle_option(arg) { - res = v.to_string(); - } - } - } + // "remember" => { + // res = Session::get_remember().to_string(); + // } + // "toggle_option" => { + // if let Ok(arg) = arg.to_str() { + // if let Some(v) = Session::get_toggle_option(arg) { + // res = v.to_string(); + // } + // } + // } "test_if_valid_server" => { if let Ok(arg) = arg.to_str() { res = hbb_common::socket_client::test_if_valid_server(arg); @@ -126,9 +349,9 @@ unsafe extern "C" fn get_by_name(name: *const c_char, arg: *const c_char) -> *co res = Config::get_option(arg); } } - "image_quality" => { - res = Session::get_image_quality(); - } + // "image_quality" => { + // res = Session::get_image_quality(); + // } "software_update_url" => { res = crate::common::SOFTWARE_UPDATE_URL.lock().unwrap().clone() } @@ -143,11 +366,11 @@ unsafe extern "C" fn get_by_name(name: *const c_char, arg: *const c_char) -> *co } } } - "peer_option" => { - if let Ok(arg) = arg.to_str() { - res = Session::get_option(arg); - } - } + // "peer_option" => { + // if let Ok(arg) = arg.to_str() { + // res = Session::get_option(arg); + // } + // } "server_id" => { res = ui_interface::get_id(); } @@ -231,103 +454,103 @@ unsafe extern "C" fn set_by_name(name: *const c_char, value: *const c_char) { "info2" => { *crate::common::MOBILE_INFO2.lock().unwrap() = value.to_owned(); } - "connect" => { - Session::start(value, false); - } - "connect_file_transfer" => { - Session::start(value, true); - } - "login" => { - if let Ok(m) = serde_json::from_str::>(value) { - if let Some(password) = m.get("password") { - if let Some(remember) = m.get("remember") { - Session::login(password, remember == "true"); - } - } - } - } - "close" => { - Session::close(); - } - "refresh" => { - Session::refresh(); - } - "reconnect" => { - Session::reconnect(); - } - "toggle_option" => { - Session::toggle_option(value); - } - "image_quality" => { - Session::set_image_quality(value); - } - "lock_screen" => { - Session::lock_screen(); - } - "ctrl_alt_del" => { - Session::ctrl_alt_del(); - } - "switch_display" => { - if let Ok(v) = value.parse::() { - Session::switch_display(v); - } - } + // "connect" => { + // Session::start(value, false); + // } + // "connect_file_transfer" => { + // Session::start(value, true); + // } + // "login" => { + // if let Ok(m) = serde_json::from_str::>(value) { + // if let Some(password) = m.get("password") { + // if let Some(remember) = m.get("remember") { + // Session::login(password, remember == "true"); + // } + // } + // } + // } + // "close" => { + // Session::close(); + // } + // "refresh" => { + // Session::refresh(); + // } + // "reconnect" => { + // Session::reconnect(); + // } + // "toggle_option" => { + // Session::toggle_option(value); + // } + // "image_quality" => { + // Session::set_image_quality(value); + // } + // "lock_screen" => { + // Session::lock_screen(); + // } + // "ctrl_alt_del" => { + // Session::ctrl_alt_del(); + // } + // "switch_display" => { + // if let Ok(v) = value.parse::() { + // Session::switch_display(v); + // } + // } "remove" => { PeerConfig::remove(value); } - "input_key" => { - if let Ok(m) = serde_json::from_str::>(value) { - let alt = m.get("alt").is_some(); - let ctrl = m.get("ctrl").is_some(); - let shift = m.get("shift").is_some(); - let command = m.get("command").is_some(); - let down = m.get("down").is_some(); - let press = m.get("press").is_some(); - if let Some(name) = m.get("name") { - Session::input_key(name, down, press, alt, ctrl, shift, command); - } - } - } - "input_string" => { - Session::input_string(value); - } - "chat_client_mode" => { - Session::send_chat(value.to_owned()); - } - "send_mouse" => { - if let Ok(m) = serde_json::from_str::>(value) { - let alt = m.get("alt").is_some(); - let ctrl = m.get("ctrl").is_some(); - let shift = m.get("shift").is_some(); - let command = m.get("command").is_some(); - let x = m - .get("x") - .map(|x| x.parse::().unwrap_or(0)) - .unwrap_or(0); - let y = m - .get("y") - .map(|x| x.parse::().unwrap_or(0)) - .unwrap_or(0); - let mut mask = 0; - if let Some(_type) = m.get("type") { - mask = match _type.as_str() { - "down" => 1, - "up" => 2, - "wheel" => 3, - _ => 0, - }; - } - if let Some(buttons) = m.get("buttons") { - mask |= match buttons.as_str() { - "left" => 1, - "right" => 2, - "wheel" => 4, - _ => 0, - } << 3; - } - Session::send_mouse(mask, x, y, alt, ctrl, shift, command); - } - } + // "input_key" => { + // if let Ok(m) = serde_json::from_str::>(value) { + // let alt = m.get("alt").is_some(); + // let ctrl = m.get("ctrl").is_some(); + // let shift = m.get("shift").is_some(); + // let command = m.get("command").is_some(); + // let down = m.get("down").is_some(); + // let press = m.get("press").is_some(); + // if let Some(name) = m.get("name") { + // Session::input_key(name, down, press, alt, ctrl, shift, command); + // } + // } + // } + // "input_string" => { + // Session::input_string(value); + // } + // "chat_client_mode" => { + // Session::send_chat(value.to_owned()); + // } + // "send_mouse" => { + // if let Ok(m) = serde_json::from_str::>(value) { + // let alt = m.get("alt").is_some(); + // let ctrl = m.get("ctrl").is_some(); + // let shift = m.get("shift").is_some(); + // let command = m.get("command").is_some(); + // let x = m + // .get("x") + // .map(|x| x.parse::().unwrap_or(0)) + // .unwrap_or(0); + // let y = m + // .get("y") + // .map(|x| x.parse::().unwrap_or(0)) + // .unwrap_or(0); + // let mut mask = 0; + // if let Some(_type) = m.get("type") { + // mask = match _type.as_str() { + // "down" => 1, + // "up" => 2, + // "wheel" => 3, + // _ => 0, + // }; + // } + // if let Some(buttons) = m.get("buttons") { + // mask |= match buttons.as_str() { + // "left" => 1, + // "right" => 2, + // "wheel" => 4, + // _ => 0, + // } << 3; + // } + // Session::send_mouse(mask, x, y, alt, ctrl, shift, command); + // } + // } "option" => { if let Ok(m) = serde_json::from_str::>(value) { if let Some(name) = m.get("name") { @@ -347,162 +570,163 @@ unsafe extern "C" fn set_by_name(name: *const c_char, value: *const c_char) { } } } - "peer_option" => { - if let Ok(m) = serde_json::from_str::>(value) { - if let Some(name) = m.get("name") { - if let Some(value) = m.get("value") { - Session::set_option(name.to_owned(), value.to_owned()); - } - } - } - } - "input_os_password" => { - Session::input_os_password(value.to_owned(), true); - } - // File Action - "read_remote_dir" => { - if let Ok(m) = serde_json::from_str::>(value) { - if let (Some(path), Some(show_hidden), Some(session)) = ( - m.get("path"), - m.get("show_hidden"), - Session::get().read().unwrap().as_ref(), - ) { - session.read_remote_dir(path.to_owned(), show_hidden.eq("true")); - } - } - } - "send_files" => { - if let Ok(m) = serde_json::from_str::>(value) { - if let ( - Some(id), - Some(path), - Some(to), - Some(file_num), - Some(show_hidden), - Some(is_remote), - ) = ( - m.get("id"), - m.get("path"), - m.get("to"), - m.get("file_num"), - m.get("show_hidden"), - m.get("is_remote"), - ) { - Session::send_files( - id.parse().unwrap_or(0), - path.to_owned(), - to.to_owned(), - file_num.parse().unwrap_or(0), - show_hidden.eq("true"), - is_remote.eq("true"), - ); - } - } - } - "set_confirm_override_file" => { - if let Ok(m) = serde_json::from_str::>(value) { - if let ( - Some(id), - Some(file_num), - Some(need_override), - Some(remember), - Some(is_upload), - ) = ( - m.get("id"), - m.get("file_num"), - m.get("need_override"), - m.get("remember"), - m.get("is_upload"), - ) { - Session::set_confirm_override_file( - id.parse().unwrap_or(0), - file_num.parse().unwrap_or(0), - need_override.eq("true"), - remember.eq("true"), - is_upload.eq("true"), - ); - } - } - } - "remove_file" => { - if let Ok(m) = serde_json::from_str::>(value) { - if let ( - Some(id), - Some(path), - Some(file_num), - Some(is_remote), - Some(session), - ) = ( - m.get("id"), - m.get("path"), - m.get("file_num"), - m.get("is_remote"), - Session::get().write().unwrap().as_mut(), - ) { - session.remove_file( - id.parse().unwrap_or(0), - path.to_owned(), - file_num.parse().unwrap_or(0), - is_remote.eq("true"), - ); - } - } - } - "read_dir_recursive" => { - if let Ok(m) = serde_json::from_str::>(value) { - if let (Some(id), Some(path), Some(is_remote), Some(session)) = ( - m.get("id"), - m.get("path"), - m.get("is_remote"), - Session::get().write().unwrap().as_mut(), - ) { - session.remove_dir_all( - id.parse().unwrap_or(0), - path.to_owned(), - is_remote.eq("true"), - ); - } - } - } - "remove_all_empty_dirs" => { - if let Ok(m) = serde_json::from_str::>(value) { - if let (Some(id), Some(path), Some(is_remote), Some(session)) = ( - m.get("id"), - m.get("path"), - m.get("is_remote"), - Session::get().write().unwrap().as_mut(), - ) { - session.remove_dir( - id.parse().unwrap_or(0), - path.to_owned(), - is_remote.eq("true"), - ); - } - } - } - "cancel_job" => { - if let (Ok(id), Some(session)) = - (value.parse(), Session::get().write().unwrap().as_mut()) - { - session.cancel_job(id); - } - } - "create_dir" => { - if let Ok(m) = serde_json::from_str::>(value) { - if let (Some(id), Some(path), Some(is_remote), Some(session)) = ( - m.get("id"), - m.get("path"), - m.get("is_remote"), - Session::get().write().unwrap().as_mut(), - ) { - session.create_dir( - id.parse().unwrap_or(0), - path.to_owned(), - is_remote.eq("true"), - ); - } - } - } + // "peer_option" => { + // if let Ok(m) = serde_json::from_str::>(value) { + // if let Some(name) = m.get("name") { + // if let Some(value) = m.get("value") { + // Session::set_option(name.to_owned(), value.to_owned()); + // } + // } + // } + // } + // "input_os_password" => { + // Session::input_os_password(value.to_owned(), true); + // } + // // File Action + // "read_remote_dir" => { + // if let Ok(m) = serde_json::from_str::>(value) { + // if let (Some(path), Some(show_hidden), Some(session)) = ( + // m.get("path"), + // m.get("show_hidden"), + // Session::get().read().unwrap().as_ref(), + // ) { + // session.read_remote_dir(path.to_owned(), show_hidden.eq("true")); + // } + // } + // } + // "send_files" => { + // if let Ok(m) = serde_json::from_str::>(value) { + // if let ( + // Some(id), + // Some(path), + // Some(to), + // Some(file_num), + // Some(show_hidden), + // Some(is_remote), + // ) = ( + // m.get("id"), + // m.get("path"), + // m.get("to"), + // m.get("file_num"), + // m.get("show_hidden"), + // m.get("is_remote"), + // ) { + // Session::send_files( + // id.parse().unwrap_or(0), + // path.to_owned(), + // to.to_owned(), + // file_num.parse().unwrap_or(0), + // show_hidden.eq("true"), + // is_remote.eq("true"), + // ); + // } + // } + // } + // "set_confirm_override_file" => { + // if let Ok(m) = serde_json::from_str::>(value) { + // if let ( + // Some(id), + // Some(file_num), + // Some(need_override), + // Some(remember), + // Some(is_upload), + // ) = ( + // m.get("id"), + // m.get("file_num"), + // m.get("need_override"), + // m.get("remember"), + // m.get("is_upload"), + // ) { + // Session::set_confirm_override_file( + // id.parse().unwrap_or(0), + // file_num.parse().unwrap_or(0), + // need_override.eq("true"), + // remember.eq("true"), + // is_upload.eq("true"), + // ); + // } + // } + // } + // ** TODO ** continue + // "remove_file" => { + // if let Ok(m) = serde_json::from_str::>(value) { + // if let ( + // Some(id), + // Some(path), + // Some(file_num), + // Some(is_remote), + // Some(session), + // ) = ( + // m.get("id"), + // m.get("path"), + // m.get("file_num"), + // m.get("is_remote"), + // Session::get().write().unwrap().as_mut(), + // ) { + // session.remove_file( + // id.parse().unwrap_or(0), + // path.to_owned(), + // file_num.parse().unwrap_or(0), + // is_remote.eq("true"), + // ); + // } + // } + // } + // "read_dir_recursive" => { + // if let Ok(m) = serde_json::from_str::>(value) { + // if let (Some(id), Some(path), Some(is_remote), Some(session)) = ( + // m.get("id"), + // m.get("path"), + // m.get("is_remote"), + // Session::get().write().unwrap().as_mut(), + // ) { + // session.remove_dir_all( + // id.parse().unwrap_or(0), + // path.to_owned(), + // is_remote.eq("true"), + // ); + // } + // } + // } + // "remove_all_empty_dirs" => { + // if let Ok(m) = serde_json::from_str::>(value) { + // if let (Some(id), Some(path), Some(is_remote), Some(session)) = ( + // m.get("id"), + // m.get("path"), + // m.get("is_remote"), + // Session::get().write().unwrap().as_mut(), + // ) { + // session.remove_dir( + // id.parse().unwrap_or(0), + // path.to_owned(), + // is_remote.eq("true"), + // ); + // } + // } + // } + // "cancel_job" => { + // if let (Ok(id), Some(session)) = + // (value.parse(), Session::get().write().unwrap().as_mut()) + // { + // session.cancel_job(id); + // } + // } + // "create_dir" => { + // if let Ok(m) = serde_json::from_str::>(value) { + // if let (Some(id), Some(path), Some(is_remote), Some(session)) = ( + // m.get("id"), + // m.get("path"), + // m.get("is_remote"), + // Session::get().write().unwrap().as_mut(), + // ) { + // session.create_dir( + // id.parse().unwrap_or(0), + // path.to_owned(), + // is_remote.eq("true"), + // ); + // } + // } + // } // Server Side "update_password" => { if value.is_empty() { From 317b350d2b0b7ef07b5594f0247a654bb4b10143 Mon Sep 17 00:00:00 2001 From: csf Date: Tue, 31 May 2022 22:09:36 +0800 Subject: [PATCH 0039/2015] multi remote instances 0.5 --- Cargo.lock | 2 +- flutter/lib/desktop/pages/remote_page.dart | 112 +++++++------- flutter/lib/mobile/widgets/dialog.dart | 2 +- flutter/lib/models/model.dart | 170 +++++++++++---------- flutter/lib/models/native_model.dart | 24 +-- src/client/file_trait.rs | 14 +- src/flutter.rs | 30 ++-- src/flutter_ffi.rs | 107 +++++++------ 8 files changed, 238 insertions(+), 223 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 441b49c58..b999cb585 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1477,7 +1477,7 @@ dependencies = [ [[package]] name = "flutter_rust_bridge_codegen" version = "1.32.0" -source = "git+https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge#827fc60143988dfc3759f7e8ce16a20d80edd710" +source = "git+https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge#3cc3818d19b731d5f9893c48699182bed4d4c15e" dependencies = [ "anyhow", "cargo_metadata", diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 49beb7819..76ab4e8ab 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -143,7 +143,7 @@ class _RemotePageState extends State with WindowListener { if (newValue.length > common) { var s = newValue.substring(common); if (s.length > 1) { - FFI.setByName('input_string', s); + FFI.bind.sessionInputString(id: widget.id, value: s); } else { inputChar(s); } @@ -177,11 +177,11 @@ class _RemotePageState extends State with WindowListener { content == '()' || content == '【】')) { // can not only input content[0], because when input ], [ are also auo insert, which cause ] never be input - FFI.setByName('input_string', content); + FFI.bind.sessionInputString(id: widget.id, value: content); openKeyboard(); return; } - FFI.setByName('input_string', content); + FFI.bind.sessionInputString(id: widget.id, value: content); } else { inputChar(content); } @@ -328,8 +328,8 @@ class _RemotePageState extends State with WindowListener { if (dy > 0) dy = -1; else if (dy < 0) dy = 1; - FFI.setByName( - 'send_mouse', '{"type": "wheel", "x": "$dx", "y": "$dy"}'); + FFI.setByName('send_mouse', + '{"id": "${widget.id}", "type": "wheel", "x": "$dx", "y": "$dy"}'); } }, child: MouseRegion( @@ -456,7 +456,7 @@ class _RemotePageState extends State with WindowListener { icon: Icon(Icons.more_vert), onPressed: () { setState(() => _showEdit = false); - showActions(); + showActions(widget.id); }, ), ]), @@ -553,7 +553,8 @@ class _RemotePageState extends State with WindowListener { }, onTwoFingerScaleEnd: (d) { _scale = 1; - FFI.setByName('peer_option', '{"name": "view-style", "value": ""}'); + FFI.bind + .sessionPeerOption(id: widget.id, name: "view-style", value: ""); }, onThreeFingerVerticalDragUpdate: FFI.ffiModel.isPeerAndroid ? null @@ -599,18 +600,12 @@ class _RemotePageState extends State with WindowListener { Widget getBodyForDesktopWithListener(bool keyboard) { var paints = [ImagePaint()]; - final cursor = await; - if (keyboard || - FFI.getByName('toggle_option', 'show-remote-cursor') == 'true') { + final cursor = FFI.bind.getSessionToggleOptionSync( + id: widget.id, arg: 'show-remote-cursor')[0] == + 1; + if (keyboard || cursor) { paints.add(CursorPaint()); } - return FutureBuilder( - future: FFI.rustdeskImpl - .getSessionToggleOption(id: widget.id, arg: 'show-remote-cursor'), - builder: (ctx, snapshot) { - if(snapshot) - }, - ); return Container( color: MyTheme.canvasColor, child: Stack(children: paints)); } @@ -636,7 +631,7 @@ class _RemotePageState extends State with WindowListener { return out; } - void showActions() { + void showActions(String id) async { final size = MediaQuery.of(context).size; final x = 120.0; final y = size.height; @@ -655,7 +650,7 @@ class _RemotePageState extends State with WindowListener { style: flatButtonStyle, onPressed: () { Navigator.pop(context); - showSetOSPassword(false); + showSetOSPassword(widget.id, false); }, child: Icon(Icons.edit, color: MyTheme.accent), ) @@ -678,7 +673,8 @@ class _RemotePageState extends State with WindowListener { more.add(PopupMenuItem( child: Text(translate('Insert Lock')), value: 'lock')); if (pi.platform == 'Windows' && - FFI.getByName('toggle_option', 'privacy-mode') != 'true') { + await FFI.bind.getSessionToggleOption(id: id, arg: 'privacy-mode') != + true) { more.add(PopupMenuItem( child: Text(translate( (FFI.ffiModel.inputBlocked ? 'Unb' : 'B') + 'lock user input')), @@ -693,28 +689,30 @@ class _RemotePageState extends State with WindowListener { elevation: 8, ); if (value == 'cad') { - FFI.setByName('ctrl_alt_del'); + FFI.bind.sessionCtrlAltDel(id: widget.id); } else if (value == 'lock') { - FFI.setByName('lock_screen'); + FFI.bind.sessionLockScreen(id: widget.id); } else if (value == 'block-input') { - FFI.setByName('toggle_option', - (FFI.ffiModel.inputBlocked ? 'un' : '') + 'block-input'); + FFI.bind.sessionToggleOption( + id: widget.id, + value: (FFI.ffiModel.inputBlocked ? 'un' : '') + 'block-input'); FFI.ffiModel.inputBlocked = !FFI.ffiModel.inputBlocked; } else if (value == 'refresh') { - FFI.setByName('refresh'); + FFI.bind.sessionRefresh(id: widget.id); } else if (value == 'paste') { () async { ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain); if (data != null && data.text != null) { - FFI.setByName('input_string', '${data.text}'); + FFI.bind.sessionInputString(id: widget.id, value: data.text ?? ""); } }(); } else if (value == 'enter_os_password') { - var password = FFI.getByName('peer_option', "os-password"); - if (password != "") { - FFI.setByName('input_os_password', password); + var password = + await FFI.bind.getSessionOption(id: id, arg: "os-password"); + if (password != null) { + FFI.bind.sessionInputOsPassword(id: widget.id, value: password); } else { - showSetOSPassword(true); + showSetOSPassword(widget.id, true); } } else if (value == 'reset_canvas') { FFI.cursorModel.reset(); @@ -740,8 +738,8 @@ class _RemotePageState extends State with WindowListener { onTouchModeChange: (t) { FFI.ffiModel.toggleTouchMode(); final v = FFI.ffiModel.touchMode ? 'Y' : ''; - FFI.setByName('peer_option', - '{"name": "touch-mode", "value": "$v"}'); + FFI.bind.sessionPeerOption( + id: widget.id, name: "touch-mode", value: v); })); })); } @@ -956,12 +954,13 @@ class ImagePainter extends CustomPainter { } CheckboxListTile getToggle( - void Function(void Function()) setState, option, name) { + String id, void Function(void Function()) setState, option, name) { + final opt = FFI.bind.getSessionToggleOptionSync(id: id, arg: option)[0] == 1; return CheckboxListTile( - value: FFI.getByName('toggle_option', option) == 'true', + value: opt, onChanged: (v) { setState(() { - FFI.setByName('toggle_option', option); + FFI.bind.sessionToggleOption(id: id, value: option); }); }, dense: true, @@ -981,12 +980,10 @@ RadioListTile getRadio(String name, String toValue, String curValue, } void showOptions(String id) async { - // String quality = FFI.getByName('image_quality'); - String quality = - await FFI.rustdeskImpl.getSessionImageQuality(id: id) ?? 'balanced'; + String quality = await FFI.bind.getSessionImageQuality(id: id) ?? 'balanced'; if (quality == '') quality = 'balanced'; String viewStyle = - await FFI.rustdeskImpl.getSessionOption(id: id, arg: 'view-style') ?? ''; + await FFI.bind.getSessionOption(id: id, arg: 'view-style') ?? ''; var displays = []; final pi = FFI.ffiModel.pi; final image = FFI.ffiModel.getConnectionImage(); @@ -999,7 +996,7 @@ void showOptions(String id) async { children.add(InkWell( onTap: () { if (i == cur) return; - FFI.setByName('switch_display', i.toString()); + FFI.bind.sessionSwitchDisplay(id: id, value: i); SmartDialog.dismiss(); }, child: Ink( @@ -1028,30 +1025,30 @@ void showOptions(String id) async { DialogManager.show((setState, close) { final more = []; if (perms['audio'] != false) { - more.add(getToggle(setState, 'disable-audio', 'Mute')); + more.add(getToggle(id, setState, 'disable-audio', 'Mute')); } if (perms['keyboard'] != false) { if (perms['clipboard'] != false) - more.add(getToggle(setState, 'disable-clipboard', 'Disable clipboard')); + more.add( + getToggle(id, setState, 'disable-clipboard', 'Disable clipboard')); more.add(getToggle( - setState, 'lock-after-session-end', 'Lock after session end')); + id, setState, 'lock-after-session-end', 'Lock after session end')); if (pi.platform == 'Windows') { - more.add(getToggle(setState, 'privacy-mode', 'Privacy mode')); + more.add(getToggle(id, setState, 'privacy-mode', 'Privacy mode')); } } var setQuality = (String? value) { if (value == null) return; setState(() { quality = value; - FFI.setByName('image_quality', value); + FFI.bind.sessionSetImageQuality(id: id, value: value); }); }; var setViewStyle = (String? value) { if (value == null) return; setState(() { viewStyle = value; - FFI.setByName( - 'peer_option', '{"name": "view-style", "value": "$value"}'); + FFI.bind.sessionPeerOption(id: id, name: "view-style", value: value); FFI.canvasModel.updateViewStyle(); }); }; @@ -1069,7 +1066,8 @@ void showOptions(String id) async { getRadio('Balanced', 'balanced', quality, setQuality), getRadio('Optimize reaction time', 'low', quality, setQuality), Divider(color: MyTheme.border), - getToggle(setState, 'show-remote-cursor', 'Show remote cursor'), + getToggle( + id, setState, 'show-remote-cursor', 'Show remote cursor'), ] + more), actions: [], @@ -1078,10 +1076,12 @@ void showOptions(String id) async { }, clickMaskDismiss: true, backDismiss: true); } -void showSetOSPassword(bool login) { +void showSetOSPassword(String id, bool login) async { final controller = TextEditingController(); - var password = FFI.getByName('peer_option', "os-password"); - var autoLogin = FFI.getByName('peer_option', "auto-login") != ""; + var password = + await FFI.bind.getSessionOption(id: id, arg: "os-password") ?? ""; + var autoLogin = + await FFI.bind.getSessionOption(id: id, arg: "auto-login") != ""; controller.text = password; DialogManager.show((setState, close) { return CustomAlertDialog( @@ -1114,12 +1114,12 @@ void showSetOSPassword(bool login) { style: flatButtonStyle, onPressed: () { var text = controller.text.trim(); - FFI.setByName( - 'peer_option', '{"name": "os-password", "value": "$text"}'); - FFI.setByName('peer_option', - '{"name": "auto-login", "value": "${autoLogin ? 'Y' : ''}"}'); + FFI.bind + .sessionPeerOption(id: id, name: "os-password", value: text); + FFI.bind.sessionPeerOption( + id: id, name: "auto-login", value: autoLogin ? 'Y' : ''); if (text != "" && login) { - FFI.setByName('input_os_password', text); + FFI.bind.sessionInputOsPassword(id: id, value: text); } close(); }, diff --git a/flutter/lib/mobile/widgets/dialog.dart b/flutter/lib/mobile/widgets/dialog.dart index 57d44e2aa..54f034627 100644 --- a/flutter/lib/mobile/widgets/dialog.dart +++ b/flutter/lib/mobile/widgets/dialog.dart @@ -137,7 +137,7 @@ void enterPasswordDialog(String id) { onPressed: () { var text = controller.text.trim(); if (text == '') return; - FFI.login(text, remember); + FFI.login(id, text, remember); close(); showLoading(translate('Logging in...')); }, diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 4ced438db..b47e06c22 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -128,7 +128,7 @@ class FfiModel with ChangeNotifier { if (name == 'msgbox') { handleMsgBox(evt, peerId); } else if (name == 'peer_info') { - handlePeerInfo(evt); + handlePeerInfo(evt, peerId); } else if (name == 'connection_ready') { FFI.ffiModel.setConnectionType( evt['secure'] == 'true', evt['direct'] == 'true'); @@ -176,7 +176,7 @@ class FfiModel with ChangeNotifier { if (name == 'msgbox') { handleMsgBox(evt, peerId); } else if (name == 'peer_info') { - handlePeerInfo(evt); + handlePeerInfo(evt, peerId); } else if (name == 'connection_ready') { FFI.ffiModel.setConnectionType( evt['secure'] == 'true', evt['direct'] == 'true'); @@ -241,17 +241,19 @@ class FfiModel with ChangeNotifier { enterPasswordDialog(id); } else { var hasRetry = evt['hasRetry'] == 'true'; - showMsgBox(type, title, text, hasRetry); + showMsgBox(id, type, title, text, hasRetry); } } /// Show a message box with [type], [title] and [text]. - void showMsgBox(String type, String title, String text, bool hasRetry) { + void showMsgBox( + String id, String type, String title, String text, bool hasRetry) { msgBox(type, title, text); _timer?.cancel(); if (hasRetry) { _timer = Timer(Duration(seconds: _reconnects), () { - FFI.reconnect(); + FFI.bind.sessionReconnect(id: id); + clearPermissions(); showLoading(translate('Connecting...')); }); _reconnects *= 2; @@ -261,7 +263,7 @@ class FfiModel with ChangeNotifier { } /// Handle the peer info event based on [evt]. - void handlePeerInfo(Map evt) { + void handlePeerInfo(Map evt, String peerId) async { SmartDialog.dismiss(); _pi.version = evt['version']; _pi.username = evt['username']; @@ -276,7 +278,8 @@ class FfiModel with ChangeNotifier { Timer(Duration(milliseconds: 100), showMobileActionsOverlay); } } else { - _touchMode = FFI.getByName('peer_option', "touch-mode") != ''; + _touchMode = + await FFI.bind.getSessionOption(id: peerId, arg: "touch-mode") != ''; } if (evt['is_file_transfer'] == "true") { @@ -311,26 +314,26 @@ class ImageModel with ChangeNotifier { ui.Image? get image => _image; - ImageModel() { - PlatformFFI.setRgbaCallback((rgba) { - if (_waitForImage) { - _waitForImage = false; - SmartDialog.dismiss(); + String id = ""; // TODO multi image model + + void onRgba(Uint8List rgba) { + if (_waitForImage) { + _waitForImage = false; + SmartDialog.dismiss(); + } + final pid = FFI.id; + ui.decodeImageFromPixels( + rgba, + FFI.ffiModel.display.width, + FFI.ffiModel.display.height, + isWeb ? ui.PixelFormat.rgba8888 : ui.PixelFormat.bgra8888, (image) { + if (FFI.id != pid) return; + try { + // my throw exception, because the listener maybe already dispose + update(image); + } catch (e) { + print('update image: $e'); } - final pid = FFI.id; - ui.decodeImageFromPixels( - rgba, - FFI.ffiModel.display.width, - FFI.ffiModel.display.height, - isWeb ? ui.PixelFormat.rgba8888 : ui.PixelFormat.bgra8888, (image) { - if (FFI.id != pid) return; - try { - // my throw exception, because the listener maybe already dispose - FFI.imageModel.update(image); - } catch (e) { - print('update image: $e'); - } - }); }); } @@ -347,8 +350,8 @@ class ImageModel with ChangeNotifier { initializeCursorAndCanvas(); Future.delayed(Duration(milliseconds: 1), () { if (FFI.ffiModel.isPeerAndroid) { - FFI.setByName( - 'peer_option', '{"name": "view-style", "value": "shrink"}'); + FFI.bind + .sessionPeerOption(id: id, name: "view-style", value: "shrink"); FFI.canvasModel.updateViewStyle(); } }); @@ -378,6 +381,7 @@ class CanvasModel with ChangeNotifier { double _x = 0; double _y = 0; double _scale = 1.0; + String id = ""; // TODO multi canvas model CanvasModel(); @@ -387,8 +391,11 @@ class CanvasModel with ChangeNotifier { double get scale => _scale; - void updateViewStyle() { - final s = FFI.getByName('peer_option', 'view-style'); + void updateViewStyle() async { + final s = await FFI.bind.getSessionOption(id: id, arg: 'view-style'); + if (s == null) { + return; + } final size = MediaQueryData.fromWindow(ui.window).size; final s1 = size.width / FFI.ffiModel.display.width; final s2 = size.height / FFI.ffiModel.display.height; @@ -498,6 +505,7 @@ class CursorModel with ChangeNotifier { double _hoty = 0; double _displayOriginX = 0; double _displayOriginY = 0; + String id = ""; // TODO multi cursor model ui.Image? get image => _image; @@ -737,7 +745,7 @@ class FFI { /// Get the remote id for current client. static String getId() { - return getByName('remote_id'); + return getByName('remote_id'); // TODO } /// Send a mouse tap event(down and up). @@ -749,14 +757,14 @@ class FFI { /// Send scroll event with scroll distance [y]. static void scroll(int y) { setByName('send_mouse', - json.encode(modify({'type': 'wheel', 'y': y.toString()}))); + json.encode(modify({'id': id, 'type': 'wheel', 'y': y.toString()}))); } /// Reconnect to the remote peer. - static void reconnect() { - setByName('reconnect'); - FFI.ffiModel.clearPermissions(); - } + // static void reconnect() { + // setByName('reconnect'); + // FFI.ffiModel.clearPermissions(); + // } /// Reset key modifiers to false, including [shift], [ctrl], [alt] and [command]. static void resetModifiers() { @@ -776,7 +784,7 @@ class FFI { static void sendMouse(String type, MouseButtons button) { if (!ffiModel.keyboard()) return; setByName('send_mouse', - json.encode(modify({'type': type, 'buttons': button.value}))); + json.encode(modify({'id': id, 'type': type, 'buttons': button.value}))); } /// Send key stroke event. @@ -784,17 +792,27 @@ class FFI { /// [press] indicates a click event(down and up). static void inputKey(String name, {bool? down, bool? press}) { if (!ffiModel.keyboard()) return; - final Map out = Map(); - out['name'] = name; - // default: down = false - if (down == true) { - out['down'] = "true"; - } - // default: press = true - if (press != false) { - out['press'] = "true"; - } - setByName('input_key', json.encode(modify(out))); + // final Map out = Map(); + // out['name'] = name; + // // default: down = false + // if (down == true) { + // out['down'] = "true"; + // } + // // default: press = true + // if (press != false) { + // out['press'] = "true"; + // } + // setByName('input_key', json.encode(modify(out))); + // TODO id + FFI.bind.sessionInputKey( + id: id, + name: name, + down: down ?? false, + press: press ?? true, + alt: alt, + ctrl: ctrl, + shift: shift, + command: command); } /// Send mouse movement event with distance in [x] and [y]. @@ -802,13 +820,14 @@ class FFI { if (!ffiModel.keyboard()) return; var x2 = x.toInt(); var y2 = y.toInt(); - setByName('send_mouse', json.encode(modify({'x': '$x2', 'y': '$y2'}))); + setByName( + 'send_mouse', json.encode(modify({'id': id, 'x': '$x2', 'y': '$y2'}))); } /// List the saved peers. static List peers() { try { - var str = getByName('peers'); + var str = getByName('peers'); // TODO if (str == "") return []; List peers = json.decode(str); return peers @@ -829,16 +848,25 @@ class FFI { } else { FFI.chatModel.resetClientMode(); // setByName('connect', id); - final event_stream = FFI.rustdeskImpl - .sessionConnect(id: id, isFileTransfer: isFileTransfer); + // TODO multi model instances + FFI.canvasModel.id = id; + FFI.imageModel.id = id; + FFI.cursorModel.id = id; + final stream = + FFI.bind.sessionConnect(id: id, isFileTransfer: isFileTransfer); final cb = FFI.ffiModel.startEventListener(id); () async { - await for (final message in event_stream) { - try { - Map event = json.decode(message); - cb(event); - } catch (e) { - print('json.decode fail(): $e'); + await for (final message in stream) { + if (message is Event) { + try { + debugPrint("event:${message.field0}"); + Map event = json.decode(message.field0); + cb(event); + } catch (e) { + print('json.decode fail(): $e'); + } + } else if (message is Rgba) { + FFI.imageModel.onRgba(message.field0); } } }(); @@ -847,26 +875,9 @@ class FFI { FFI.id = id; } - static Map? popEvent() { - var s = getByName('event'); - if (s == '') return null; - try { - Map event = json.decode(s); - return event; - } catch (e) { - print('popEvent(): $e'); - } - return null; - } - /// Login with [password], choose if the client should [remember] it. - static void login(String password, bool remember) { - setByName( - 'login', - json.encode({ - 'password': password, - 'remember': remember ? 'true' : 'false', - })); + static void login(String id, String password, bool remember) { + FFI.bind.sessionLogin(id: id, password: password, remember: remember); } /// Close the remote session. @@ -876,8 +887,8 @@ class FFI { savePreference(id, cursorModel.x, cursorModel.y, canvasModel.x, canvasModel.y, canvasModel.scale, ffiModel.pi.currentDisplay); } + FFI.bind.sessionClose(id: id); id = ""; - setByName('close', ''); imageModel.update(null); cursorModel.clear(); ffiModel.clear(); @@ -896,7 +907,7 @@ class FFI { PlatformFFI.setByName(name, value); } - static RustdeskImpl get rustdeskImpl => PlatformFFI.rustdeskImpl; + static RustdeskImpl get bind => PlatformFFI.ffiBind; static handleMouse(Map evt) { var type = ''; @@ -949,6 +960,7 @@ class FFI { break; } evt['buttons'] = buttons; + evt['id'] = id; setByName('send_mouse', json.encode(evt)); } diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index e1b9137b6..a425ea810 100644 --- a/flutter/lib/models/native_model.dart +++ b/flutter/lib/models/native_model.dart @@ -30,11 +30,10 @@ class PlatformFFI { static String _homeDir = ''; static F2? _getByName; static F3? _setByName; - static late RustdeskImpl _rustdeskImpl; + static late RustdeskImpl _ffiBind; static void Function(Map)? _eventCallback; - static void Function(Uint8List)? _rgbaCallback; - static RustdeskImpl get rustdeskImpl => _rustdeskImpl; + static RustdeskImpl get ffiBind => _ffiBind; static Future getVersion() async { PackageInfo packageInfo = await PackageInfo.fromPlatform(); @@ -91,8 +90,8 @@ class PlatformFFI { dylib.lookupFunction, Pointer), F3>( 'set_by_name'); _dir = (await getApplicationDocumentsDirectory()).path; - _rustdeskImpl = RustdeskImpl(dylib); - _startListenEvent(_rustdeskImpl); // global event + _ffiBind = RustdeskImpl(dylib); + _startListenEvent(_ffiBind); // global event try { _homeDir = (await ExternalPath.getExternalStorageDirectories())[0]; } catch (e) { @@ -137,7 +136,7 @@ class PlatformFFI { /// Start listening to the Rust core's events and frames. static void _startListenEvent(RustdeskImpl rustdeskImpl) { () async { - await for (final message in rustdeskImpl.startEventStream()) { + await for (final message in rustdeskImpl.startGlobalEventStream()) { if (_eventCallback != null) { try { Map event = json.decode(message); @@ -148,24 +147,13 @@ class PlatformFFI { } } }(); - () async { - await for (final rgba in rustdeskImpl.startRgbaStream()) { - if (_rgbaCallback != null) { - _rgbaCallback!(rgba); - } else { - rgba.clear(); - } - } - }(); } static void setEventCallback(void Function(Map) fun) async { _eventCallback = fun; } - static void setRgbaCallback(void Function(Uint8List) fun) async { - _rgbaCallback = fun; - } + static void setRgbaCallback(void Function(Uint8List) fun) async {} static void startDesktopWebListener() {} diff --git a/src/client/file_trait.rs b/src/client/file_trait.rs index 7b1da3e0a..5dbc614e6 100644 --- a/src/client/file_trait.rs +++ b/src/client/file_trait.rs @@ -28,7 +28,7 @@ pub trait FileManager: Interface { } } - fn cancel_job(&mut self, id: i32) { + fn cancel_job(&self, id: i32) { self.send(Data::CancelJob(id)); } @@ -44,23 +44,23 @@ pub trait FileManager: Interface { self.send(Data::Message(msg_out)); } - fn remove_file(&mut self, id: i32, path: String, file_num: i32, is_remote: bool) { + fn remove_file(&self, id: i32, path: String, file_num: i32, is_remote: bool) { self.send(Data::RemoveFile((id, path, file_num, is_remote))); } - fn remove_dir_all(&mut self, id: i32, path: String, is_remote: bool) { + fn remove_dir_all(&self, id: i32, path: String, is_remote: bool) { self.send(Data::RemoveDirAll((id, path, is_remote))); } - fn confirm_delete_files(&mut self, id: i32, file_num: i32) { + fn confirm_delete_files(&self, id: i32, file_num: i32) { self.send(Data::ConfirmDeleteFiles((id, file_num))); } - fn set_no_confirm(&mut self, id: i32) { + fn set_no_confirm(&self, id: i32) { self.send(Data::SetNoConfirm(id)); } - fn remove_dir(&mut self, id: i32, path: String, is_remote: bool) { + fn remove_dir(&self, id: i32, path: String, is_remote: bool) { if is_remote { self.send(Data::RemoveDir((id, path))); } else { @@ -68,7 +68,7 @@ pub trait FileManager: Interface { } } - fn create_dir(&mut self, id: i32, path: String, is_remote: bool) { + fn create_dir(&self, id: i32, path: String, is_remote: bool) { self.send(Data::CreateDir((id, path, is_remote))); } diff --git a/src/flutter.rs b/src/flutter.rs index 3872710c0..7a0f378f9 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -1,4 +1,4 @@ -use crate::client::*; +use crate::{client::*, flutter_ffi::EventToUI}; use flutter_rust_bridge::{StreamSink, ZeroCopyBuffer}; use hbb_common::{ allow_err, @@ -28,8 +28,7 @@ use std::{ lazy_static::lazy_static! { // static ref SESSION: Arc>> = Default::default(); pub static ref SESSIONS: RwLock> = Default::default(); - pub static ref EVENT_STREAM: RwLock>> = Default::default(); // rust to dart event channel - pub static ref RGBA_STREAM: RwLock>>>> = Default::default(); // rust to dart rgba (big u8 list) channel + pub static ref GLOBAL_EVENT_STREAM: RwLock>> = Default::default(); // rust to dart event channel } // pub fn get_session<'a>(id: &str) -> Option<&'a Session> { @@ -41,7 +40,7 @@ pub struct Session { id: String, sender: Arc>>>, // UI to rust lc: Arc>, - events2ui: Arc>>, + events2ui: Arc>>, } impl Session { @@ -51,7 +50,7 @@ impl Session { /// /// * `id` - The id of the remote session. /// * `is_file_transfer` - If the session is used for file transfer. - pub fn start(id: &str, is_file_transfer: bool, events2ui: StreamSink) { + pub fn start(id: &str, is_file_transfer: bool, events2ui: StreamSink) { LocalConfig::set_remote_id(&id); // TODO check same id // TODO close @@ -284,11 +283,8 @@ impl Session { let mut h: HashMap<&str, &str> = event.iter().cloned().collect(); assert!(h.get("name").is_none()); h.insert("name", name); - - self.events2ui - .read() - .unwrap() - .add(serde_json::ser::to_string(&h).unwrap_or("".to_owned())); + let out = serde_json::ser::to_string(&h).unwrap_or("".to_owned()); + self.events2ui.read().unwrap().add(EventToUI::Event(out)); } /// Get platform of peer. @@ -676,11 +672,11 @@ impl Connection { if !self.first_frame { self.first_frame = true; } - if let (Ok(true), Some(s)) = ( - self.video_handler.handle_frame(vf), - RGBA_STREAM.read().unwrap().as_ref(), - ) { - s.add(ZeroCopyBuffer(self.video_handler.rgb.clone())); + if let Ok(true) = self.video_handler.handle_frame(vf) { + let stream = self.session.events2ui.read().unwrap(); + stream.add(EventToUI::Rgba(ZeroCopyBuffer( + self.video_handler.rgb.clone(), + ))); } } Some(message::Union::hash(hash)) => { @@ -1274,7 +1270,7 @@ pub mod connection_manager { use scrap::android::call_main_service_set_by_name; use serde_derive::Serialize; - use super::EVENT_STREAM; + use super::GLOBAL_EVENT_STREAM; #[derive(Debug, Serialize, Clone)] struct Client { @@ -1382,7 +1378,7 @@ pub mod connection_manager { assert!(h.get("name").is_none()); h.insert("name", name); - if let Some(s) = EVENT_STREAM.read().unwrap().as_ref() { + if let Some(s) = GLOBAL_EVENT_STREAM.read().unwrap().as_ref() { s.add(serde_json::ser::to_string(&h).unwrap_or("".to_owned())); }; } diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 10d1257db..4e6e63595 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -59,18 +59,18 @@ pub extern "C" fn rustdesk_core_main() -> bool { crate::core_main::core_main() } -pub fn start_event_stream(s: StreamSink) -> ResultType<()> { - let _ = flutter::EVENT_STREAM.write().unwrap().insert(s); - Ok(()) +pub enum EventToUI { + Event(String), + Rgba(ZeroCopyBuffer>), } -pub fn start_rgba_stream(s: StreamSink>>) -> ResultType<()> { - let _ = flutter::RGBA_STREAM.write().unwrap().insert(s); +pub fn start_global_event_stream(s: StreamSink) -> ResultType<()> { + let _ = flutter::GLOBAL_EVENT_STREAM.write().unwrap().insert(s); Ok(()) } pub fn session_connect( - events2ui: StreamSink, + events2ui: StreamSink, id: String, is_file_transfer: bool, ) -> ResultType<()> { @@ -86,6 +86,7 @@ pub fn get_session_remember(id: String) -> Option { } } +// TODO sync pub fn get_session_toggle_option(id: String, arg: String) -> Option { if let Some(session) = SESSIONS.read().unwrap().get(&id) { Some(session.get_toggle_option(&arg)) @@ -94,11 +95,20 @@ pub fn get_session_toggle_option(id: String, arg: String) -> Option { } } -pub fn get_session_image_quality(id: String) -> SyncReturn> { - if let Some(session) = SESSIONS.read().unwrap().get(&id) { - SyncReturn(Some(session.get_image_quality())) +pub fn get_session_toggle_option_sync(id: String, arg: String) -> SyncReturn> { + let res = if get_session_toggle_option(id, arg) == Some(true) { + 1 } else { - SyncReturn(None) + 0 + }; + SyncReturn(vec![res]) +} + +pub fn get_session_image_quality(id: String) -> Option { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + Some(session.get_image_quality()) + } else { + None } } @@ -517,40 +527,49 @@ unsafe extern "C" fn set_by_name(name: *const c_char, value: *const c_char) { // "chat_client_mode" => { // Session::send_chat(value.to_owned()); // } - // "send_mouse" => { - // if let Ok(m) = serde_json::from_str::>(value) { - // let alt = m.get("alt").is_some(); - // let ctrl = m.get("ctrl").is_some(); - // let shift = m.get("shift").is_some(); - // let command = m.get("command").is_some(); - // let x = m - // .get("x") - // .map(|x| x.parse::().unwrap_or(0)) - // .unwrap_or(0); - // let y = m - // .get("y") - // .map(|x| x.parse::().unwrap_or(0)) - // .unwrap_or(0); - // let mut mask = 0; - // if let Some(_type) = m.get("type") { - // mask = match _type.as_str() { - // "down" => 1, - // "up" => 2, - // "wheel" => 3, - // _ => 0, - // }; - // } - // if let Some(buttons) = m.get("buttons") { - // mask |= match buttons.as_str() { - // "left" => 1, - // "right" => 2, - // "wheel" => 4, - // _ => 0, - // } << 3; - // } - // Session::send_mouse(mask, x, y, alt, ctrl, shift, command); - // } - // } + + // TODO + "send_mouse" => { + if let Ok(m) = serde_json::from_str::>(value) { + let id = m.get("id"); + if id.is_none() { + return; + } + let id = id.unwrap(); + let alt = m.get("alt").is_some(); + let ctrl = m.get("ctrl").is_some(); + let shift = m.get("shift").is_some(); + let command = m.get("command").is_some(); + let x = m + .get("x") + .map(|x| x.parse::().unwrap_or(0)) + .unwrap_or(0); + let y = m + .get("y") + .map(|x| x.parse::().unwrap_or(0)) + .unwrap_or(0); + let mut mask = 0; + if let Some(_type) = m.get("type") { + mask = match _type.as_str() { + "down" => 1, + "up" => 2, + "wheel" => 3, + _ => 0, + }; + } + if let Some(buttons) = m.get("buttons") { + mask |= match buttons.as_str() { + "left" => 1, + "right" => 2, + "wheel" => 4, + _ => 0, + } << 3; + } + if let Some(session) = SESSIONS.read().unwrap().get(id) { + session.send_mouse(mask, x, y, alt, ctrl, shift, command); + } + } + } "option" => { if let Ok(m) = serde_json::from_str::>(value) { if let Some(name) = m.get("name") { From 1b7eb73ee8a85b10490be72e241632de7f4fe347 Mon Sep 17 00:00:00 2001 From: csf Date: Wed, 1 Jun 2022 15:09:48 +0800 Subject: [PATCH 0040/2015] SyncReturn --- Cargo.lock | 2 +- flutter/lib/desktop/pages/remote_page.dart | 7 +++---- flutter/lib/models/model.dart | 6 +++--- flutter/pubspec.lock | 8 +++++--- flutter/pubspec.yaml | 6 +++++- src/flutter_ffi.rs | 10 +++------- 6 files changed, 20 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b999cb585..9346e75b4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1477,7 +1477,7 @@ dependencies = [ [[package]] name = "flutter_rust_bridge_codegen" version = "1.32.0" -source = "git+https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge#3cc3818d19b731d5f9893c48699182bed4d4c15e" +source = "git+https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge#e5adce55eea0b74d3680e66a2c5252edf17b07e1" dependencies = [ "anyhow", "cargo_metadata", diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 76ab4e8ab..c40a7cb47 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -600,9 +600,8 @@ class _RemotePageState extends State with WindowListener { Widget getBodyForDesktopWithListener(bool keyboard) { var paints = [ImagePaint()]; - final cursor = FFI.bind.getSessionToggleOptionSync( - id: widget.id, arg: 'show-remote-cursor')[0] == - 1; + final cursor = FFI.bind + .getSessionToggleOptionSync(id: widget.id, arg: 'show-remote-cursor'); if (keyboard || cursor) { paints.add(CursorPaint()); } @@ -955,7 +954,7 @@ class ImagePainter extends CustomPainter { CheckboxListTile getToggle( String id, void Function(void Function()) setState, option, name) { - final opt = FFI.bind.getSessionToggleOptionSync(id: id, arg: option)[0] == 1; + final opt = FFI.bind.getSessionToggleOptionSync(id: id, arg: option); return CheckboxListTile( value: opt, onChanged: (v) { diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index b47e06c22..3659a85df 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -314,7 +314,7 @@ class ImageModel with ChangeNotifier { ui.Image? get image => _image; - String id = ""; // TODO multi image model + String _id = ""; void onRgba(Uint8List rgba) { if (_waitForImage) { @@ -351,7 +351,7 @@ class ImageModel with ChangeNotifier { Future.delayed(Duration(milliseconds: 1), () { if (FFI.ffiModel.isPeerAndroid) { FFI.bind - .sessionPeerOption(id: id, name: "view-style", value: "shrink"); + .sessionPeerOption(id: _id, name: "view-style", value: "shrink"); FFI.canvasModel.updateViewStyle(); } }); @@ -850,7 +850,7 @@ class FFI { // setByName('connect', id); // TODO multi model instances FFI.canvasModel.id = id; - FFI.imageModel.id = id; + FFI.imageModel._id = id; FFI.cursorModel.id = id; final stream = FFI.bind.sessionConnect(id: id, isFileTransfer: isFileTransfer); diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 1ba610f19..c5ad4c8ae 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -396,9 +396,11 @@ packages: flutter_rust_bridge: dependency: "direct main" description: - name: flutter_rust_bridge - url: "https://pub.dartlang.org" - source: hosted + path: frb_dart + ref: master + resolved-ref: e5adce55eea0b74d3680e66a2c5252edf17b07e1 + url: "https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge" + source: git version: "1.32.0" flutter_smart_dialog: dependency: "direct main" diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 21b1857eb..ab9a7d7eb 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -53,7 +53,11 @@ dependencies: image_picker: ^0.8.5 image: ^3.1.3 flutter_smart_dialog: ^4.3.1 - flutter_rust_bridge: ^1.30.0 + flutter_rust_bridge: + git: + url: https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge + ref: master + path: frb_dart window_manager: ^0.2.3 desktop_multi_window: git: diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 4e6e63595..25c37d418 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -95,13 +95,9 @@ pub fn get_session_toggle_option(id: String, arg: String) -> Option { } } -pub fn get_session_toggle_option_sync(id: String, arg: String) -> SyncReturn> { - let res = if get_session_toggle_option(id, arg) == Some(true) { - 1 - } else { - 0 - }; - SyncReturn(vec![res]) +pub fn get_session_toggle_option_sync(id: String, arg: String) -> SyncReturn { + let res = get_session_toggle_option(id, arg) == Some(true); + SyncReturn(res) } pub fn get_session_image_quality(id: String) -> Option { From 12d0380c8c7cabb1647ba99251ba04ffe6752afe Mon Sep 17 00:00:00 2001 From: kingtous Date: Thu, 2 Jun 2022 02:13:07 +0800 Subject: [PATCH 0041/2015] fix: windows compilation for multi window plugin --- flutter/pubspec.lock | 4 ++-- flutter/pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index c5ad4c8ae..03d01e2c6 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -222,8 +222,8 @@ packages: dependency: "direct main" description: path: "." - ref: master - resolved-ref: "7150283dcd0c79450b98bf0a62b26df95897e53c" + ref: "3966c7f1ed85f06861e66088bfa4c921ddaae4c5" + resolved-ref: "3966c7f1ed85f06861e66088bfa4c921ddaae4c5" url: "https://github.com/Kingtous/rustdesk_desktop_multi_window" source: git version: "0.0.1" diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index ab9a7d7eb..ba19baef6 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -62,7 +62,7 @@ dependencies: desktop_multi_window: git: url: https://github.com/Kingtous/rustdesk_desktop_multi_window - ref: master + ref: 3966c7f1ed85f06861e66088bfa4c921ddaae4c5 bitsdojo_window: ^0.1.2 freezed_annotation: ^2.0.3 From c0b9a67cdd9e28c9685b2728335ecd43e7d4de62 Mon Sep 17 00:00:00 2001 From: kingtous Date: Thu, 2 Jun 2022 02:20:17 +0800 Subject: [PATCH 0042/2015] fix: macOS compilation for multi window plugin --- flutter/pubspec.lock | 4 ++-- flutter/pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 03d01e2c6..f46c07982 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -222,8 +222,8 @@ packages: dependency: "direct main" description: path: "." - ref: "3966c7f1ed85f06861e66088bfa4c921ddaae4c5" - resolved-ref: "3966c7f1ed85f06861e66088bfa4c921ddaae4c5" + ref: "4aab101f17f02312dc45311eb3009cc0ea5357c1" + resolved-ref: "4aab101f17f02312dc45311eb3009cc0ea5357c1" url: "https://github.com/Kingtous/rustdesk_desktop_multi_window" source: git version: "0.0.1" diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index ba19baef6..a4417c25c 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -62,7 +62,7 @@ dependencies: desktop_multi_window: git: url: https://github.com/Kingtous/rustdesk_desktop_multi_window - ref: 3966c7f1ed85f06861e66088bfa4c921ddaae4c5 + ref: 4aab101f17f02312dc45311eb3009cc0ea5357c1 bitsdojo_window: ^0.1.2 freezed_annotation: ^2.0.3 From 1f9655d6322f01adc28064737e75459c1e7afd86 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Thu, 2 Jun 2022 14:51:09 +0800 Subject: [PATCH 0043/2015] opt: titlebar height autofit Signed-off-by: Kingtous --- .../desktop/pages/connection_tab_page.dart | 59 +++++++++---------- .../lib/desktop/pages/desktop_home_page.dart | 24 ++++---- .../lib/desktop/widgets/titlebar_widget.dart | 20 +++---- 3 files changed, 48 insertions(+), 55 deletions(-) diff --git a/flutter/lib/desktop/pages/connection_tab_page.dart b/flutter/lib/desktop/pages/connection_tab_page.dart index 5ebf7b54e..8d18b2f24 100644 --- a/flutter/lib/desktop/pages/connection_tab_page.dart +++ b/flutter/lib/desktop/pages/connection_tab_page.dart @@ -64,37 +64,34 @@ class _ConnectionTabPageState extends State animationDuration: Duration.zero, child: Column( children: [ - SizedBox( - height: 50, - child: DesktopTitleBar( - child: TabBar( - isScrollable: true, - labelColor: Colors.white, - physics: NeverScrollableScrollPhysics(), - indicatorColor: Colors.white, - tabs: connectionIds - .map((e) => Tab( - child: Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text(e), - SizedBox( - width: 4, - ), - InkWell( - onTap: () { - onRemoveId(e); - }, - child: Icon( - Icons.highlight_remove, - size: 20, - )) - ], - ), - )) - .toList()), - ), + DesktopTitleBar( + child: TabBar( + isScrollable: true, + labelColor: Colors.white, + physics: NeverScrollableScrollPhysics(), + indicatorColor: Colors.white, + tabs: connectionIds + .map((e) => Tab( + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(e), + SizedBox( + width: 4, + ), + InkWell( + onTap: () { + onRemoveId(e); + }, + child: Icon( + Icons.highlight_remove, + size: 20, + )) + ], + ), + )) + .toList()), ), Expanded( child: TabBarView( diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index fdffda031..c42ed1b53 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -20,20 +20,16 @@ class _DesktopHomePageState extends State { return Scaffold( body: Column( children: [ - Row( - children: [ - DesktopTitleBar( - child: Center( - child: Text( - "RustDesk", - style: TextStyle( - color: Colors.white, - fontSize: 20, - fontWeight: FontWeight.bold), - ), - ), - ) - ], + DesktopTitleBar( + child: Center( + child: Text( + "RustDesk", + style: TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.bold), + ), + ), ), Expanded( child: Container( diff --git a/flutter/lib/desktop/widgets/titlebar_widget.dart b/flutter/lib/desktop/widgets/titlebar_widget.dart index f98b7cc79..ecb68d513 100644 --- a/flutter/lib/desktop/widgets/titlebar_widget.dart +++ b/flutter/lib/desktop/widgets/titlebar_widget.dart @@ -12,16 +12,16 @@ class DesktopTitleBar extends StatelessWidget { @override Widget build(BuildContext context) { - return Expanded( - child: Container( - decoration: const BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [backgroundStartColor, backgroundEndColor], - stops: [0.0, 1.0]), - ), - child: WindowTitleBarBox( + return Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [backgroundStartColor, backgroundEndColor], + stops: [0.0, 1.0]), + ), + child: WindowTitleBarBox( + child: SizedBox( child: Row( children: [ Expanded( From d75655179148d45e5901bec85b26be72e0e28d02 Mon Sep 17 00:00:00 2001 From: SoLongAndThanksForAllThePizza <103753680+SoLongAndThanksForAllThePizza@users.noreply.github.com> Date: Thu, 2 Jun 2022 16:13:34 +0800 Subject: [PATCH 0044/2015] fix: macos compilation --- Cargo.lock | 417 +++++++++-------- Cargo.toml | 6 +- build.rs | 4 +- flutter/macos/Flutter/Flutter-Debug.xcconfig | 1 + .../macos/Flutter/Flutter-Release.xcconfig | 1 + flutter/macos/Podfile | 40 ++ flutter/macos/Podfile.lock | 196 ++++++++ .../macos/Runner.xcodeproj/project.pbxproj | 160 ++++++- .../contents.xcworkspacedata | 3 + flutter/macos/Runner/AppDelegate.swift | 1 + flutter/macos/Runner/MainFlutterWindow.swift | 32 +- flutter/macos/Runner/bridge_generated.h | 201 ++++++++ .../macos/rustdesk.xcodeproj/project.pbxproj | 439 ++++++++++++++++++ libs/scrap/build.rs | 77 ++- src/ui_interface.rs | 2 - 15 files changed, 1344 insertions(+), 236 deletions(-) create mode 100644 flutter/macos/Podfile create mode 100644 flutter/macos/Podfile.lock create mode 100644 flutter/macos/Runner/bridge_generated.h create mode 100644 flutter/macos/rustdesk.xcodeproj/project.pbxproj diff --git a/Cargo.lock b/Cargo.lock index 9346e75b4..ac6c0e979 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -129,9 +129,9 @@ checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc" [[package]] name = "arboard" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6045ca509e4abacde2b884ac4618a51d0c017b5d85a3ee84a7226eb33b3154a9" +checksum = "dc120354d1b5ec6d7aaf4876b602def75595937b5e15d356eb554ab5177e08bb" dependencies = [ "clipboard-win", "core-graphics 0.22.3", @@ -140,8 +140,7 @@ dependencies = [ "objc", "objc-foundation", "objc_id", - "once_cell", - "parking_lot 0.12.0", + "parking_lot 0.12.1", "thiserror", "winapi 0.3.9", "x11rb", @@ -160,9 +159,9 @@ dependencies = [ [[package]] name = "async-io" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a811e6a479f2439f0c04038796b5cfb3d2ad56c230e0f2d3f7b04d68cfee607b" +checksum = "e5e18f61464ae81cde0a23e713ae8fd299580c54d697a35820cfd0625b8b0e07" dependencies = [ "concurrent-queue", "futures-lite", @@ -286,7 +285,7 @@ dependencies = [ "cc", "cfg-if 1.0.0", "libc", - "miniz_oxide 0.5.1", + "miniz_oxide 0.5.3", "object", "rustc-demangle", ] @@ -421,11 +420,11 @@ dependencies = [ [[package]] name = "camino" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07fd178c5af4d59e83498ef15cf3f154e1a6f9d091270cb86283c65ef44e9ef0" +checksum = "869119e97797867fd90f5e22af7d0bd274bd4635ebb9eb68c04f3f513ae6c412" dependencies = [ - "serde 1.0.136", + "serde 1.0.137", ] [[package]] @@ -434,7 +433,7 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbdb825da8a5df079a43676dbe042702f1707b1109f713a01420fbb4cc71fa27" dependencies = [ - "serde 1.0.136", + "serde 1.0.137", ] [[package]] @@ -446,8 +445,8 @@ dependencies = [ "camino", "cargo-platform", "semver 1.0.9", - "serde 1.0.136", - "serde_json 1.0.79", + "serde 1.0.137", + "serde_json 1.0.81", ] [[package]] @@ -456,14 +455,14 @@ version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b6d248e3ca02f3fbfabcb9284464c596baec223a26d91bbf44a5a62ddb0d900" dependencies = [ - "clap 3.1.12", + "clap 3.1.18", "heck 0.4.0", "indexmap", "log", "proc-macro2", "quote", - "serde 1.0.136", - "serde_json 1.0.79", + "serde 1.0.137", + "serde_json 1.0.81", "syn", "tempfile", "toml", @@ -495,9 +494,9 @@ dependencies = [ [[package]] name = "cfg-expr" -version = "0.10.2" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e068cb2806bbc15b439846dc16c5f89f8599f2c3e4d73d4449d38f9b2f0b6c5" +checksum = "0aacacf4d96c24b2ad6eb8ee6df040e4f27b0d0b39a5710c30091baa830485db" dependencies = [ "smallvec", ] @@ -522,15 +521,15 @@ checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" dependencies = [ "libc", "num-integer", - "num-traits 0.2.14", + "num-traits 0.2.15", "winapi 0.3.9", ] [[package]] name = "clang-sys" -version = "1.3.1" +version = "1.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cc00842eed744b858222c4c9faf7243aafc6d33f92f96935263ef4d8a41ce21" +checksum = "5a050e2153c5be08febd6734e29298e844fdb0fa21aeddd63b4eb7baa106c69b" dependencies = [ "glob", "libc", @@ -554,9 +553,9 @@ dependencies = [ [[package]] name = "clap" -version = "3.1.12" +version = "3.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c167e37342afc5f33fd87bbc870cedd020d2a6dffa05d45ccd9241fbdd146db" +checksum = "d2dbdf4bdacb33466e854ce889eee8dfd5729abf7ccd7664d0a2d60cd384440b" dependencies = [ "atty", "bitflags", @@ -569,9 +568,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.1.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "189ddd3b5d32a70b35e7686054371742a937b0d99128e76dde6340210e966669" +checksum = "a37c35f1112dad5e6e0b1adaff798507497a18fceeb30cceb3bae7d1427b9213" dependencies = [ "os_str_bytes", ] @@ -583,7 +582,7 @@ dependencies = [ "cc", "hbb_common", "lazy_static", - "serde 1.0.136", + "serde 1.0.137", "serde_derive", "thiserror", ] @@ -694,7 +693,7 @@ version = "0.4.0" source = "git+https://github.com/open-trade/confy#630cc28a396cb7d01eefdd9f3824486fe4d8554b" dependencies = [ "directories-next", - "serde 1.0.136", + "serde 1.0.137", "thiserror", "toml", ] @@ -1269,7 +1268,7 @@ checksum = "7f3f119846c823f9eafcf953a8f6ffb6ed69bf6240883261a7f13b634579a51f" dependencies = [ "lazy_static", "regex", - "serde 1.0.136", + "serde 1.0.137", "strsim 0.10.0", ] @@ -1287,9 +1286,9 @@ checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0" [[package]] name = "ed25519" -version = "1.4.1" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d5c4b5e5959dc2c2b89918d8e2cc40fcdd623cef026ed09d2f0ee05199dc8e4" +checksum = "1e9c280362032ea4203659fc489832d0204ef09f247a0506f170dafcac08c369" dependencies = [ "signature", ] @@ -1318,7 +1317,7 @@ dependencies = [ "log", "objc", "pkg-config", - "serde 1.0.136", + "serde 1.0.137", "serde_derive", "unicode-segmentation", "winapi 0.3.9", @@ -1431,14 +1430,12 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39522e96686d38f4bc984b9198e3a0613264abaebaff2c5c918bfa6b6da09af" +checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6" dependencies = [ - "cfg-if 1.0.0", "crc32fast", - "libc", - "miniz_oxide 0.5.1", + "miniz_oxide 0.5.3", ] [[package]] @@ -1462,15 +1459,14 @@ dependencies = [ [[package]] name = "flutter_rust_bridge" -version = "1.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7e7e4af55d6a36aad9573737a12fba774999e4d6dd5e668e29c25bb473f85f3" +version = "1.32.0" +source = "git+https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge#e5adce55eea0b74d3680e66a2c5252edf17b07e1" dependencies = [ "allo-isolate", "anyhow", "flutter_rust_bridge_macros", "lazy_static", - "parking_lot 0.12.0", + "parking_lot 0.12.1", "threadpool", ] @@ -1490,7 +1486,7 @@ dependencies = [ "pathdiff", "quote", "regex", - "serde 1.0.136", + "serde 1.0.137", "serde_yaml", "structopt", "syn", @@ -1501,9 +1497,8 @@ dependencies = [ [[package]] name = "flutter_rust_bridge_macros" -version = "1.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69ffbd9713edad524e45f415a997dd05af6a67fd2ed3aa19fa85159835d85fbc" +version = "1.32.0" +source = "git+https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge#e5adce55eea0b74d3680e66a2c5252edf17b07e1" [[package]] name = "fnv" @@ -2118,7 +2113,7 @@ dependencies = [ "indexmap", "slab", "tokio", - "tokio-util 0.7.1", + "tokio-util 0.7.2", "tracing", ] @@ -2149,14 +2144,14 @@ dependencies = [ "quinn", "rand 0.8.5", "regex", - "serde 1.0.136", + "serde 1.0.137", "serde_derive", - "serde_json 1.0.79", + "serde_json 1.0.81", "socket2 0.3.19", "sodiumoxide", "tokio", "tokio-socks", - "tokio-util 0.6.9", + "tokio-util 0.6.10", "toml", "winapi 0.3.9", "zstd", @@ -2200,14 +2195,14 @@ checksum = "ff8670570af52249509a86f5e3e18a08c60b177071826898fde8997cf5f6bfbb" dependencies = [ "bytes", "fnv", - "itoa 1.0.1", + "itoa 1.0.2", ] [[package]] name = "http-body" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ff4f84919677303da5f147645dbea6b1881f368d03ac84e1dc09031ebd7b2c6" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" dependencies = [ "bytes", "http", @@ -2234,9 +2229,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.18" +version = "0.14.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b26ae0a80afebe130861d90abf98e3814a4f28a4c6ffeb5ab8ebb2be311e0ef2" +checksum = "42dc3c131584288d375f2d07f822b0cb012d8c6fb899a5b9fdb3cb7eb9b6004f" dependencies = [ "bytes", "futures-channel", @@ -2247,7 +2242,7 @@ dependencies = [ "http-body", "httparse", "httpdate", - "itoa 1.0.1", + "itoa 1.0.2", "pin-project-lite", "socket2 0.4.4", "tokio", @@ -2297,7 +2292,7 @@ dependencies = [ "color_quant", "num-iter", "num-rational", - "num-traits 0.2.14", + "num-traits 0.2.15", "png", "tiff", ] @@ -2323,9 +2318,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.8.1" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f647032dfaa1f8b6dc29bd3edb7bbef4861b8b8007ebb118d6db284fd59f6ee" +checksum = "e6012d540c5baa3589337a98ce73408de9b5a25ec9fc2c6fd6be8f0d39e0ca5a" dependencies = [ "autocfg 1.1.0", "hashbrown", @@ -2372,9 +2367,9 @@ checksum = "8324a32baf01e2ae060e9de58ed0bc2320c9a2833491ee36cd3b4c414de4db8c" [[package]] name = "itoa" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" +checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" [[package]] name = "jni" @@ -2457,19 +2452,20 @@ dependencies = [ [[package]] name = "libappindicator-sys" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bdcb8c5cfc11febe2ff3f18386d6cb7d29f464cbaf6b286985c3f1a501d74f" +checksum = "d83c2227727d7950ada2ae554613d35fd4e55b87f0a29b86d2368267d19b1d99" dependencies = [ "gtk-sys", - "pkg-config", + "libloading 0.7.3", + "once_cell", ] [[package]] name = "libc" -version = "0.2.124" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a41fed9d98f27ab1c6d161da622a4fa35e8a54a8adc24bbf3ddd0ef70b0e50" +checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" [[package]] name = "libdbus-sys" @@ -2510,7 +2506,7 @@ dependencies = [ "libc", "libpulse-sys", "num-derive", - "num-traits 0.2.14", + "num-traits 0.2.15", "winapi 0.3.9", ] @@ -2543,16 +2539,16 @@ checksum = "991e6bd0efe2a36e6534e136e7996925e4c1a8e35b7807fe533f2beffff27c30" dependencies = [ "libc", "num-derive", - "num-traits 0.2.14", + "num-traits 0.2.15", "pkg-config", "winapi 0.3.9", ] [[package]] name = "libsamplerate-sys" -version = "0.1.10" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "403163258e75b5780cd6245c04cddd7f3166c5f8dd2bf5462e596c9ca4eb9653" +checksum = "28853b399f78f8281cd88d333b54a63170c4275f6faea66726a2bea5cca72e0d" dependencies = [ "cmake", ] @@ -2587,9 +2583,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.16" +version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6389c490849ff5bc16be905ae24bc913a9c8892e19b2341dbc175e14c341c2b8" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" dependencies = [ "cfg-if 1.0.0", ] @@ -2625,7 +2621,7 @@ dependencies = [ [[package]] name = "magnum-opus" version = "0.4.0" -source = "git+https://github.com/open-trade/magnum-opus#3c3d0b86ae95c84930bebffe4bcb03b3bd83342b" +source = "git+https://github.com/SoLongAndThanksForAllThePizza/magnum-opus#6247071a64af7b18e2d553e235729e6865f63ece" dependencies = [ "bindgen", "target_build_utils", @@ -2648,9 +2644,9 @@ checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" [[package]] name = "memchr" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" [[package]] name = "memmap2" @@ -2703,9 +2699,9 @@ dependencies = [ [[package]] name = "miniz_oxide" -version = "0.5.1" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2b29bd4bc3f33391105ebee3589c19197c4271e3e5a9ec9bfe8127eeff8f082" +checksum = "6f5c75688da582b8ffc1f1799e9db273f32133c49e048f614d22ec3256773ccc" dependencies = [ "adler", ] @@ -2744,16 +2740,14 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52da4364ffb0e4fe33a9841a98a3f3014fb964045ce4f7a45a398243c8d6b0c9" +checksum = "713d550d9b44d89174e066b7a6217ae06234c10cb47819a88290d2b353c31799" dependencies = [ "libc", "log", - "miow 0.3.7", - "ntapi", "wasi 0.11.0+wasi-snapshot-preview1", - "winapi 0.3.9", + "windows-sys 0.36.1", ] [[package]] @@ -3010,11 +3004,11 @@ dependencies = [ [[package]] name = "num-complex" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26873667bbbb7c5182d4a37c1add32cdf09f841af72da53318fdb81543c15085" +checksum = "97fbc387afefefd5e9e39493299f3069e14a140dd34dc19b4c1c1a8fddb6a790" dependencies = [ - "num-traits 0.2.14", + "num-traits 0.2.15", ] [[package]] @@ -3030,23 +3024,23 @@ dependencies = [ [[package]] name = "num-integer" -version = "0.1.44" +version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" dependencies = [ "autocfg 1.1.0", - "num-traits 0.2.14", + "num-traits 0.2.15", ] [[package]] name = "num-iter" -version = "0.1.42" +version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2021c8337a54d21aca0d59a92577a029af9431cb59b909b03252b9c164fad59" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" dependencies = [ "autocfg 1.1.0", "num-integer", - "num-traits 0.2.14", + "num-traits 0.2.15", ] [[package]] @@ -3057,7 +3051,7 @@ checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07" dependencies = [ "autocfg 1.1.0", "num-integer", - "num-traits 0.2.14", + "num-traits 0.2.15", ] [[package]] @@ -3066,14 +3060,14 @@ version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31" dependencies = [ - "num-traits 0.2.14", + "num-traits 0.2.15", ] [[package]] name = "num-traits" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" dependencies = [ "autocfg 1.1.0", ] @@ -3111,9 +3105,9 @@ dependencies = [ [[package]] name = "num_threads" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aba1801fb138d8e85e11d0fc70baf4fe1cdfffda7c6cd34a854905df588e5ed0" +checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" dependencies = [ "libc", ] @@ -3149,24 +3143,24 @@ dependencies = [ [[package]] name = "object" -version = "0.28.3" +version = "0.28.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40bec70ba014595f99f7aa110b84331ffe1ee9aece7fe6f387cc7e3ecda4d456" +checksum = "e42c982f2d955fac81dd7e1d0e1426a7d702acd9c98d19ab01083a6a0328c424" dependencies = [ "memchr", ] [[package]] name = "oboe" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2463c8f2e19b4e0d0710a21f8e4011501ff28db1c95d7a5482a553b2100502d2" +checksum = "27f63c358b4fa0fbcfefd7c8be5cfc39c08ce2389f5325687e7762a48d30a5c1" dependencies = [ "jni", "ndk 0.6.0", - "ndk-glue 0.6.2", + "ndk-context", "num-derive", - "num-traits 0.2.14", + "num-traits 0.2.15", "oboe-sys", ] @@ -3181,9 +3175,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.10.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9" +checksum = "7709cef83f0c1f58f666e746a08b21e0085f7440fa6a29cc194d68aac97a4225" [[package]] name = "openssl-probe" @@ -3193,9 +3187,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "os_str_bytes" -version = "6.0.0" +version = "6.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" +checksum = "21326818e99cfe6ce1e524c2a805c189a99b5ae555a35d19f9a284b427d86afa" [[package]] name = "owned_ttf_parser" @@ -3271,12 +3265,12 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f5ec2493a61ac0506c0f4199f99070cbe83857b0337006a30f3e6719b8ef58" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ "lock_api", - "parking_lot_core 0.9.2", + "parking_lot_core 0.9.3", ] [[package]] @@ -3295,15 +3289,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "995f667a6c822200b0433ac218e05582f0e2efa1b922a3fd2fbaadc5f87bab37" +checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929" dependencies = [ "cfg-if 1.0.0", "libc", "redox_syscall", "smallvec", - "windows-sys 0.34.0", + "windows-sys 0.36.1", ] [[package]] @@ -3399,9 +3393,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" [[package]] name = "pin-utils" @@ -3506,11 +3500,11 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.37" +version = "1.0.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec757218438d5fda206afc041538b2f6d889286160d649a86a24d37e1235afd1" +checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f" dependencies = [ - "unicode-xid", + "unicode-ident", ] [[package]] @@ -3584,9 +3578,9 @@ dependencies = [ [[package]] name = "quinn" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d147472bc9a09f13b06c044787b6683cdffa02e2865b7f0fb53d67c49ed2988e" +checksum = "d7542006acd6e057ff632307d219954c44048f818898da03113d6c0086bfddd9" dependencies = [ "bytes", "futures-channel", @@ -3603,9 +3597,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "359c5eb33845f3ee05c229e65f87cdbc503eea394964b8f1330833d460b4ff3e" +checksum = "3a13a5c0a674c1ce7150c9df7bc4a1e46c2fbbe7c710f56c0dc78b1a810e779e" dependencies = [ "bytes", "fxhash", @@ -3623,13 +3617,12 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df185e5e5f7611fa6e628ed8f9633df10114b03bbaecab186ec55822c44ac727" +checksum = "b3149f7237331015f1a6adf065c397d1be71e032fcf110ba41da52e7926b882f" dependencies = [ "futures-util", "libc", - "mio 0.7.14", "quinn-proto", "socket2 0.4.4", "tokio", @@ -3802,9 +3795,9 @@ dependencies = [ [[package]] name = "rayon" -version = "1.5.2" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd249e82c21598a9a426a4e00dd7adc1d640b22445ec8545feef801d1a74c221" +checksum = "bd99e5772ead8baa5215278c9b15bf92087709e9c1b2d1f97cdb5a183c933a7d" dependencies = [ "autocfg 1.1.0", "crossbeam-deque", @@ -3814,9 +3807,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.9.2" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f51245e1e62e1f1629cbfec37b5793bbabcaeb90f30e94d2ba03564687353e4" +checksum = "258bcdb5ac6dad48491bb2992db6b7cf74878b0384908af124823d118c99683f" dependencies = [ "crossbeam-channel", "crossbeam-deque", @@ -3879,9 +3872,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.5.5" +version = "1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a11647b6b25ff05a515cb92c365cec08801e83423a235b51e231e1808747286" +checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1" dependencies = [ "aho-corasick", "memchr", @@ -3890,9 +3883,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.25" +version = "0.6.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" +checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64" [[package]] name = "remove_dir_all" @@ -3938,8 +3931,8 @@ dependencies = [ "pin-project-lite", "rustls", "rustls-pemfile 0.3.0", - "serde 1.0.136", - "serde_json 1.0.79", + "serde 1.0.137", + "serde_json 1.0.81", "serde_urlencoded", "tokio", "tokio-rustls", @@ -3984,8 +3977,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bf099a1888612545b683d2661a1940089f6c2e5a8e38979b2159da876bfd956" dependencies = [ "libc", - "serde 1.0.136", - "serde_json 1.0.79", + "serde 1.0.137", + "serde_json 1.0.81", "winapi 0.3.9", ] @@ -3997,7 +3990,7 @@ checksum = "cd70209c27d5b08f5528bdc779ea3ffb418954e28987f9f9775c6eac41003f9c" dependencies = [ "num-complex", "num-integer", - "num-traits 0.2.14", + "num-traits 0.2.15", "realfft", ] @@ -4051,7 +4044,7 @@ dependencies = [ "base64", "cc", "cfg-if 1.0.0", - "clap 3.1.12", + "clap 3.1.18", "clipboard", "cocoa 0.24.0", "core-foundation 0.9.3", @@ -4088,9 +4081,9 @@ dependencies = [ "samplerate", "sciter-rs", "scrap", - "serde 1.0.136", + "serde 1.0.137", "serde_derive", - "serde_json 1.0.79", + "serde_json 1.0.81", "sha2", "sys-locale", "sysinfo", @@ -4113,7 +4106,7 @@ checksum = "b1d089e5c57521629a59f5f39bca7434849ff89bd6873b521afe389c1c602543" dependencies = [ "num-complex", "num-integer", - "num-traits 0.2.14", + "num-traits 0.2.15", "primal-check", "strength_reduce", "transpose", @@ -4121,9 +4114,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.20.4" +version = "0.20.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fbfeb8d0ddb84706bc597a5574ab8912817c52a397f819e5b614e2265206921" +checksum = "5aab8ee6c7097ed6057f43c187a62418d0c05a4bd5f18b3571db50ee0f9ce033" dependencies = [ "log", "ring", @@ -4188,9 +4181,9 @@ checksum = "f2cc38e8fa666e2de3c4aba7edeb5ffc5246c1c2ed0e3d17e560aeeba736b23f" [[package]] name = "ryu" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" +checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695" [[package]] name = "same-file" @@ -4212,12 +4205,12 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.19" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" +checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2" dependencies = [ "lazy_static", - "winapi 0.3.9", + "windows-sys 0.36.1", ] [[package]] @@ -4263,7 +4256,7 @@ dependencies = [ "num_cpus", "quest", "repng", - "serde 1.0.136", + "serde 1.0.137", "target_build_utils", "tracing", "webm", @@ -4318,7 +4311,7 @@ version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8cb243bdfdb5936c8dc3c45762a19d12ab4550cdc753bc247637d4ec35a040fd" dependencies = [ - "serde 1.0.136", + "serde 1.0.137", ] [[package]] @@ -4338,18 +4331,18 @@ checksum = "34b623917345a631dc9608d5194cc206b3fe6c3554cd1c75b937e55e285254af" [[package]] name = "serde" -version = "1.0.136" +version = "1.0.137" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" +checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.136" +version = "1.0.137" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" +checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be" dependencies = [ "proc-macro2", "quote", @@ -4370,13 +4363,13 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.79" +version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95" +checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c" dependencies = [ - "itoa 1.0.1", + "itoa 1.0.2", "ryu", - "serde 1.0.136", + "serde 1.0.137", ] [[package]] @@ -4386,9 +4379,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", - "itoa 1.0.1", + "itoa 1.0.2", "ryu", - "serde 1.0.136", + "serde 1.0.137", ] [[package]] @@ -4399,7 +4392,7 @@ checksum = "707d15895415db6628332b737c838b88c598522e4dc70647e59b72312924aebc" dependencies = [ "indexmap", "ryu", - "serde 1.0.136", + "serde 1.0.137", "yaml-rust", ] @@ -4422,9 +4415,9 @@ checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" [[package]] name = "signal-hook" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "647c97df271007dcea485bb74ffdb57f2e683f1306c854f468a0c244badabf2d" +checksum = "a253b5e89e2698464fc26b545c9edceb338e18a89effeeecfea192c3025be29d" dependencies = [ "libc", "signal-hook-registry", @@ -4512,7 +4505,7 @@ dependencies = [ "ed25519", "libc", "libsodium-sys", - "serde 1.0.136", + "serde 1.0.137", ] [[package]] @@ -4529,9 +4522,9 @@ checksum = "ef5430c8e36b713e13b48a9f709cc21e046723fe44ce34587b73a830203b533e" [[package]] name = "str-buf" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d44a3643b4ff9caf57abcee9c2c621d6c03d9135e0d8b589bd9afb5992cb176a" +checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0" [[package]] name = "strength_reduce" @@ -4601,13 +4594,13 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.91" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b683b2b825c8eef438b77c36a06dc262294da3d5a5813fac20da149241dcd44d" +checksum = "fbaf6116ab8924f39d52792136fb74fd60a80194cf1b1c6ffa6453eef1c3f942" dependencies = [ "proc-macro2", "quote", - "unicode-xid", + "unicode-ident", ] [[package]] @@ -4637,9 +4630,9 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.23.10" +version = "0.23.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eea2ed6847da2e0c7289f72cb4f285f0bd704694ca067d32be811b2a45ea858" +checksum = "3977ec2e0520829be45c8a2df70db2bf364714d8a748316a10c3c35d4d2b01c9" dependencies = [ "cfg-if 1.0.0", "core-foundation-sys 0.8.3", @@ -4738,18 +4731,18 @@ checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" [[package]] name = "thiserror" -version = "1.0.30" +version = "1.0.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" +checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.30" +version = "1.0.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" +checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" dependencies = [ "proc-macro2", "quote", @@ -4782,7 +4775,7 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2702e08a7a860f005826c6815dcac101b19b5eb330c27fe4a5928fec1d20ddd" dependencies = [ - "itoa 1.0.1", + "itoa 1.0.2", "libc", "num_threads", "time-macros", @@ -4811,17 +4804,17 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.17.0" +version = "1.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2af73ac49756f3f7c01172e34a23e5d0216f6c32333757c2c61feb2bbff5a5ee" +checksum = "4903bf0427cf68dddd5aa6a93220756f8be0c34fcfa9f5e6191e103e15a31395" dependencies = [ "bytes", "libc", "memchr", - "mio 0.8.2", + "mio 0.8.3", "num_cpus", "once_cell", - "parking_lot 0.12.0", + "parking_lot 0.12.1", "pin-project-lite", "signal-hook-registry", "socket2 0.4.4", @@ -4864,14 +4857,14 @@ dependencies = [ "pin-project", "thiserror", "tokio", - "tokio-util 0.6.9", + "tokio-util 0.6.10", ] [[package]] name = "tokio-util" -version = "0.6.9" +version = "0.6.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e99e1983e5d376cd8eb4b66604d2e99e79f5bd988c3055891dcd8c9e2604cc0" +checksum = "36943ee01a6d67977dd3f84a5a1d2efeb4ada3a1ae771cadfaa535d9d9fc6507" dependencies = [ "bytes", "futures-core", @@ -4885,9 +4878,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0edfdeb067411dba2044da6d1cb2df793dd35add7888d73c16e3381ded401764" +checksum = "f988a1a1adc2fb21f9c12aa96441da33a1728193ae0b95d2be22dbd17fcb4e5c" dependencies = [ "bytes", "futures-core", @@ -4903,7 +4896,7 @@ version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" dependencies = [ - "serde 1.0.136", + "serde 1.0.137", ] [[package]] @@ -4926,9 +4919,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.20" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e65ce065b4b5c53e73bb28912318cb8c9e9ad3921f1d669eb0e68b4c8143a2b" +checksum = "cc6b8ad3567499f98a1db7a752b07a7c8c7c7c34c332ec00effb2b0027974b7c" dependencies = [ "proc-macro2", "quote", @@ -5012,6 +5005,12 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" +[[package]] +name = "unicode-ident" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee" + [[package]] name = "unicode-normalization" version = "0.1.19" @@ -5035,9 +5034,9 @@ checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" [[package]] name = "unicode-xid" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" +checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04" [[package]] name = "untrusted" @@ -5059,9 +5058,9 @@ dependencies = [ [[package]] name = "uuid" -version = "1.0.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cfcd319456c4d6ea10087ed423473267e1a071f3bc0aa89f80d60997843c6f0" +checksum = "c6d5d669b51467dcf7b2f1a796ce0f955f05f01cafda6c19d6e95f730df29238" dependencies = [ "getrandom", ] @@ -5097,7 +5096,7 @@ dependencies = [ "cc", "hbb_common", "lazy_static", - "serde 1.0.136", + "serde 1.0.137", "serde_derive", "thiserror", ] @@ -5458,15 +5457,15 @@ dependencies = [ [[package]] name = "windows-sys" -version = "0.34.0" +version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5acdd78cb4ba54c0045ac14f62d8f94a03d10047904ae2a40afa1e99d8f70825" +checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" dependencies = [ - "windows_aarch64_msvc 0.34.0", - "windows_i686_gnu 0.34.0", - "windows_i686_msvc 0.34.0", - "windows_x86_64_gnu 0.34.0", - "windows_x86_64_msvc 0.34.0", + "windows_aarch64_msvc 0.36.1", + "windows_i686_gnu 0.36.1", + "windows_i686_msvc 0.36.1", + "windows_x86_64_gnu 0.36.1", + "windows_x86_64_msvc 0.36.1", ] [[package]] @@ -5477,9 +5476,9 @@ checksum = "52695a41e536859d5308cc613b4a022261a274390b25bd29dfff4bf08505f3c2" [[package]] name = "windows_aarch64_msvc" -version = "0.34.0" +version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17cffbe740121affb56fad0fc0e421804adf0ae00891205213b5cecd30db881d" +checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" [[package]] name = "windows_i686_gnu" @@ -5489,9 +5488,9 @@ checksum = "f54725ac23affef038fecb177de6c9bf065787c2f432f79e3c373da92f3e1d8a" [[package]] name = "windows_i686_gnu" -version = "0.34.0" +version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2564fde759adb79129d9b4f54be42b32c89970c18ebf93124ca8870a498688ed" +checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" [[package]] name = "windows_i686_msvc" @@ -5501,9 +5500,9 @@ checksum = "51d5158a43cc43623c0729d1ad6647e62fa384a3d135fd15108d37c683461f64" [[package]] name = "windows_i686_msvc" -version = "0.34.0" +version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cd9d32ba70453522332c14d38814bceeb747d80b3958676007acadd7e166956" +checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" [[package]] name = "windows_x86_64_gnu" @@ -5513,9 +5512,9 @@ checksum = "bc31f409f565611535130cfe7ee8e6655d3fa99c1c61013981e491921b5ce954" [[package]] name = "windows_x86_64_gnu" -version = "0.34.0" +version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfce6deae227ee8d356d19effc141a509cc503dfd1f850622ec4b0f84428e1f4" +checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" [[package]] name = "windows_x86_64_msvc" @@ -5525,9 +5524,9 @@ checksum = "3f2b8c7cbd3bfdddd9ab98769f9746a7fad1bca236554cd032b78d768bc0e89f" [[package]] name = "windows_x86_64_msvc" -version = "0.34.0" +version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d19538ccc21819d01deaf88d6a17eae6596a12e9aafdbb97916fb49896d89de9" +checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" [[package]] name = "winit" diff --git a/Cargo.toml b/Cargo.toml index b395da582..0be7bb111 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,7 +42,7 @@ 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" -magnum-opus = { git = "https://github.com/open-trade/magnum-opus" } +magnum-opus = { git = "https://github.com/SoLongAndThanksForAllThePizza/magnum-opus" } dasp = { version = "0.11", features = ["signal", "interpolate-linear", "interpolate"], optional = true } rubato = { version = "0.12", optional = true } samplerate = { version = "0.2", optional = true } @@ -53,7 +53,7 @@ rpassword = "6.0" base64 = "0.13" sysinfo = "0.23" num_cpus = "1.13" -flutter_rust_bridge = { version = "1.30.0", optional = true } +flutter_rust_bridge = { git = "https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge", optional = true } [target.'cfg(not(target_os = "linux"))'.dependencies] reqwest = { version = "0.11", features = ["json", "rustls-tls"], default-features=false } @@ -102,7 +102,7 @@ android_logger = "0.11" jni = "0.19.0" [target.'cfg(any(target_os = "android", target_os = "ios"))'.dependencies] -flutter_rust_bridge = "1.30.0" +flutter_rust_bridge = { git = "https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge" } [workspace] members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "libs/virtual_display"] diff --git a/build.rs b/build.rs index bad00457f..4d51cd297 100644 --- a/build.rs +++ b/build.rs @@ -71,6 +71,8 @@ fn gen_flutter_rust_bridge() { rust_input: "src/flutter_ffi.rs".to_string(), // Path of output generated Dart code dart_output: "flutter/lib/generated_bridge.dart".to_string(), + // Path of output generated C header + c_output: Some(vec!["flutter/macos/Runner/bridge_generated.h".to_string()]), // for other options lets use default ..Default::default() }; @@ -84,7 +86,7 @@ fn main() { // there is problem with cfg(target_os) in build.rs, so use our workaround // let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); // if target_os == "android" || target_os == "ios" { - gen_flutter_rust_bridge(); + gen_flutter_rust_bridge(); // return; // } #[cfg(all(windows, feature = "inline"))] diff --git a/flutter/macos/Flutter/Flutter-Debug.xcconfig b/flutter/macos/Flutter/Flutter-Debug.xcconfig index c2efd0b60..4b81f9b2d 100644 --- a/flutter/macos/Flutter/Flutter-Debug.xcconfig +++ b/flutter/macos/Flutter/Flutter-Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/flutter/macos/Flutter/Flutter-Release.xcconfig b/flutter/macos/Flutter/Flutter-Release.xcconfig index c2efd0b60..5caa9d157 100644 --- a/flutter/macos/Flutter/Flutter-Release.xcconfig +++ b/flutter/macos/Flutter/Flutter-Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/flutter/macos/Podfile b/flutter/macos/Podfile new file mode 100644 index 000000000..22d9caad2 --- /dev/null +++ b/flutter/macos/Podfile @@ -0,0 +1,40 @@ +platform :osx, '10.12' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/flutter/macos/Podfile.lock b/flutter/macos/Podfile.lock new file mode 100644 index 000000000..a83616180 --- /dev/null +++ b/flutter/macos/Podfile.lock @@ -0,0 +1,196 @@ +PODS: + - bitsdojo_window_macos (0.0.1): + - FlutterMacOS + - desktop_multi_window (0.0.1): + - FlutterMacOS + - device_info_plus_macos (0.0.1): + - FlutterMacOS + - Firebase/Analytics (8.15.0): + - Firebase/Core + - Firebase/Core (8.15.0): + - Firebase/CoreOnly + - FirebaseAnalytics (~> 8.15.0) + - Firebase/CoreOnly (8.15.0): + - FirebaseCore (= 8.15.0) + - firebase_analytics (9.1.9): + - Firebase/Analytics (= 8.15.0) + - firebase_core + - FlutterMacOS + - firebase_core (1.17.1): + - Firebase/CoreOnly (~> 8.15.0) + - FlutterMacOS + - FirebaseAnalytics (8.15.0): + - FirebaseAnalytics/AdIdSupport (= 8.15.0) + - FirebaseCore (~> 8.0) + - FirebaseInstallations (~> 8.0) + - GoogleUtilities/AppDelegateSwizzler (~> 7.7) + - GoogleUtilities/MethodSwizzler (~> 7.7) + - GoogleUtilities/Network (~> 7.7) + - "GoogleUtilities/NSData+zlib (~> 7.7)" + - nanopb (~> 2.30908.0) + - FirebaseAnalytics/AdIdSupport (8.15.0): + - FirebaseCore (~> 8.0) + - FirebaseInstallations (~> 8.0) + - GoogleAppMeasurement (= 8.15.0) + - GoogleUtilities/AppDelegateSwizzler (~> 7.7) + - GoogleUtilities/MethodSwizzler (~> 7.7) + - GoogleUtilities/Network (~> 7.7) + - "GoogleUtilities/NSData+zlib (~> 7.7)" + - nanopb (~> 2.30908.0) + - FirebaseCore (8.15.0): + - FirebaseCoreDiagnostics (~> 8.0) + - GoogleUtilities/Environment (~> 7.7) + - GoogleUtilities/Logger (~> 7.7) + - FirebaseCoreDiagnostics (8.15.0): + - GoogleDataTransport (~> 9.1) + - GoogleUtilities/Environment (~> 7.7) + - GoogleUtilities/Logger (~> 7.7) + - nanopb (~> 2.30908.0) + - FirebaseInstallations (8.15.0): + - FirebaseCore (~> 8.0) + - GoogleUtilities/Environment (~> 7.7) + - GoogleUtilities/UserDefaults (~> 7.7) + - PromisesObjC (< 3.0, >= 1.2) + - FlutterMacOS (1.0.0) + - GoogleAppMeasurement (8.15.0): + - GoogleAppMeasurement/AdIdSupport (= 8.15.0) + - GoogleUtilities/AppDelegateSwizzler (~> 7.7) + - GoogleUtilities/MethodSwizzler (~> 7.7) + - GoogleUtilities/Network (~> 7.7) + - "GoogleUtilities/NSData+zlib (~> 7.7)" + - nanopb (~> 2.30908.0) + - GoogleAppMeasurement/AdIdSupport (8.15.0): + - GoogleAppMeasurement/WithoutAdIdSupport (= 8.15.0) + - GoogleUtilities/AppDelegateSwizzler (~> 7.7) + - GoogleUtilities/MethodSwizzler (~> 7.7) + - GoogleUtilities/Network (~> 7.7) + - "GoogleUtilities/NSData+zlib (~> 7.7)" + - nanopb (~> 2.30908.0) + - GoogleAppMeasurement/WithoutAdIdSupport (8.15.0): + - GoogleUtilities/AppDelegateSwizzler (~> 7.7) + - GoogleUtilities/MethodSwizzler (~> 7.7) + - GoogleUtilities/Network (~> 7.7) + - "GoogleUtilities/NSData+zlib (~> 7.7)" + - nanopb (~> 2.30908.0) + - GoogleDataTransport (9.1.4): + - GoogleUtilities/Environment (~> 7.7) + - nanopb (< 2.30910.0, >= 2.30908.0) + - PromisesObjC (< 3.0, >= 1.2) + - GoogleUtilities/AppDelegateSwizzler (7.7.0): + - GoogleUtilities/Environment + - GoogleUtilities/Logger + - GoogleUtilities/Network + - GoogleUtilities/Environment (7.7.0): + - PromisesObjC (< 3.0, >= 1.2) + - GoogleUtilities/Logger (7.7.0): + - GoogleUtilities/Environment + - GoogleUtilities/MethodSwizzler (7.7.0): + - GoogleUtilities/Logger + - GoogleUtilities/Network (7.7.0): + - GoogleUtilities/Logger + - "GoogleUtilities/NSData+zlib" + - GoogleUtilities/Reachability + - "GoogleUtilities/NSData+zlib (7.7.0)" + - GoogleUtilities/Reachability (7.7.0): + - GoogleUtilities/Logger + - GoogleUtilities/UserDefaults (7.7.0): + - GoogleUtilities/Logger + - nanopb (2.30908.0): + - nanopb/decode (= 2.30908.0) + - nanopb/encode (= 2.30908.0) + - nanopb/decode (2.30908.0) + - nanopb/encode (2.30908.0) + - package_info_plus_macos (0.0.1): + - FlutterMacOS + - path_provider_macos (0.0.1): + - FlutterMacOS + - PromisesObjC (2.1.0) + - shared_preferences_macos (0.0.1): + - FlutterMacOS + - url_launcher_macos (0.0.1): + - FlutterMacOS + - wakelock_macos (0.0.1): + - FlutterMacOS + - window_manager (0.2.0): + - FlutterMacOS + +DEPENDENCIES: + - bitsdojo_window_macos (from `Flutter/ephemeral/.symlinks/plugins/bitsdojo_window_macos/macos`) + - desktop_multi_window (from `Flutter/ephemeral/.symlinks/plugins/desktop_multi_window/macos`) + - device_info_plus_macos (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus_macos/macos`) + - firebase_analytics (from `Flutter/ephemeral/.symlinks/plugins/firebase_analytics/macos`) + - firebase_core (from `Flutter/ephemeral/.symlinks/plugins/firebase_core/macos`) + - FlutterMacOS (from `Flutter/ephemeral`) + - package_info_plus_macos (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus_macos/macos`) + - path_provider_macos (from `Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos`) + - shared_preferences_macos (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_macos/macos`) + - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) + - wakelock_macos (from `Flutter/ephemeral/.symlinks/plugins/wakelock_macos/macos`) + - window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`) + +SPEC REPOS: + trunk: + - Firebase + - FirebaseAnalytics + - FirebaseCore + - FirebaseCoreDiagnostics + - FirebaseInstallations + - GoogleAppMeasurement + - GoogleDataTransport + - GoogleUtilities + - nanopb + - PromisesObjC + +EXTERNAL SOURCES: + bitsdojo_window_macos: + :path: Flutter/ephemeral/.symlinks/plugins/bitsdojo_window_macos/macos + desktop_multi_window: + :path: Flutter/ephemeral/.symlinks/plugins/desktop_multi_window/macos + device_info_plus_macos: + :path: Flutter/ephemeral/.symlinks/plugins/device_info_plus_macos/macos + firebase_analytics: + :path: Flutter/ephemeral/.symlinks/plugins/firebase_analytics/macos + firebase_core: + :path: Flutter/ephemeral/.symlinks/plugins/firebase_core/macos + FlutterMacOS: + :path: Flutter/ephemeral + package_info_plus_macos: + :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus_macos/macos + path_provider_macos: + :path: Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos + shared_preferences_macos: + :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_macos/macos + url_launcher_macos: + :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos + wakelock_macos: + :path: Flutter/ephemeral/.symlinks/plugins/wakelock_macos/macos + window_manager: + :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos + +SPEC CHECKSUMS: + bitsdojo_window_macos: 44e3b8fe3dd463820e0321f6256c5b1c16bb6a00 + desktop_multi_window: 566489c048b501134f9d7fb6a2354c60a9126486 + device_info_plus_macos: 1ad388a1ef433505c4038e7dd9605aadd1e2e9c7 + Firebase: 5f8193dff4b5b7c5d5ef72ae54bb76c08e2b841d + firebase_analytics: d448483150504ed84f25c5437a34af2591a7929e + firebase_core: 7b87364e2d1eae70018a60698e89e7d6f5320bad + FirebaseAnalytics: 7761cbadb00a717d8d0939363eb46041526474fa + FirebaseCore: 5743c5785c074a794d35f2fff7ecc254a91e08b1 + FirebaseCoreDiagnostics: 92e07a649aeb66352b319d43bdd2ee3942af84cb + FirebaseInstallations: 40bd9054049b2eae9a2c38ef1c3dd213df3605cd + FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424 + GoogleAppMeasurement: 4c19f031220c72464d460c9daa1fb5d1acce958e + GoogleDataTransport: 5fffe35792f8b96ec8d6775f5eccd83c998d5a3b + GoogleUtilities: e0913149f6b0625b553d70dae12b49fc62914fd1 + nanopb: a0ba3315591a9ae0a16a309ee504766e90db0c96 + package_info_plus_macos: f010621b07802a241d96d01876d6705f15e77c1c + path_provider_macos: 3c0c3b4b0d4a76d2bf989a913c2de869c5641a19 + PromisesObjC: 99b6f43f9e1044bd87a95a60beff28c2c44ddb72 + shared_preferences_macos: a64dc611287ed6cbe28fd1297898db1336975727 + url_launcher_macos: 597e05b8e514239626bcf4a850fcf9ef5c856ec3 + wakelock_macos: bc3f2a9bd8d2e6c89fee1e1822e7ddac3bd004a9 + window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 + +PODFILE CHECKSUM: c7161fcf45d4fd9025dc0f48a76d6e64e52f8176 + +COCOAPODS: 1.11.3 diff --git a/flutter/macos/Runner.xcodeproj/project.pbxproj b/flutter/macos/Runner.xcodeproj/project.pbxproj index 05460fe4b..23549954b 100644 --- a/flutter/macos/Runner.xcodeproj/project.pbxproj +++ b/flutter/macos/Runner.xcodeproj/project.pbxproj @@ -26,6 +26,9 @@ 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 */; }; + C5E54335B73C89F72DB1B606 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 26C84465887F29AE938039CB /* Pods_Runner.framework */; }; + CC13D44B2847D53E00EF8B54 /* librustdesk.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = CC13D4362847C8C200EF8B54 /* librustdesk.dylib */; }; + CC13D4502847D5E800EF8B54 /* librustdesk.dylib in Bundle Framework */ = {isa = PBXBuildFile; fileRef = CC13D4362847C8C200EF8B54 /* librustdesk.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -36,6 +39,41 @@ remoteGlobalIDString = 33CC111A2044C6BA0003C045; remoteInfo = FLX; }; + CC13D4352847C8C200EF8B54 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = CC13D42E2847C8C200EF8B54 /* rustdesk.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = CA6071B5A0F5A7A3EF2297AA; + remoteInfo = "librustdesk-cdylib"; + }; + CC13D4372847C8C200EF8B54 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = CC13D42E2847C8C200EF8B54 /* rustdesk.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = CA604C7415FB2A3731F5016A; + remoteInfo = "librustdesk-staticlib"; + }; + CC13D4392847C8C200EF8B54 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = CC13D42E2847C8C200EF8B54 /* rustdesk.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = CA60D3BC5386D3D7DBD96893; + remoteInfo = "naming-bin"; + }; + CC13D43B2847C8C200EF8B54 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = CC13D42E2847C8C200EF8B54 /* rustdesk.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = CA60D3BC5386B357B2AB834F; + remoteInfo = "rustdesk-bin"; + }; + CC13D43D2847C8CB00EF8B54 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = CC13D42E2847C8C200EF8B54 /* rustdesk.xcodeproj */; + proxyType = 1; + remoteGlobalIDString = CA6071B5A0F5D6691E4C3FF1; + remoteInfo = "librustdesk-cdylib"; + }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -45,6 +83,7 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( + CC13D4502847D5E800EF8B54 /* librustdesk.dylib in Bundle Framework */, ); name = "Bundle Framework"; runOnlyForDeploymentPostprocessing = 0; @@ -52,9 +91,11 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 26C84465887F29AE938039CB /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 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 /* flutter_hbb.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "flutter_hbb.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10ED2044A3C60003C045 /* flutter_hbb.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = flutter_hbb.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 = ""; }; @@ -66,8 +107,12 @@ 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 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 = ""; }; 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 = ""; }; + CC13D42E2847C8C200EF8B54 /* rustdesk.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; path = rustdesk.xcodeproj; sourceTree = SOURCE_ROOT; }; + CCB6FE9A2848A6B800E58D48 /* bridge_generated.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = bridge_generated.h; path = Runner/bridge_generated.h; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -75,6 +120,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + CC13D44B2847D53E00EF8B54 /* librustdesk.dylib in Frameworks */, + C5E54335B73C89F72DB1B606 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -95,10 +142,12 @@ 33CC10E42044A3C60003C045 = { isa = PBXGroup; children = ( + CCB6FE9A2848A6B800E58D48 /* bridge_generated.h */, 33FAB671232836740065AC1E /* Runner */, 33CEB47122A05771004F2AC0 /* Flutter */, 33CC10EE2044A3C60003C045 /* Products */, D73912EC22F37F3D000D13A0 /* Frameworks */, + A6C450E1C32EC39A23170131 /* Pods */, ); sourceTree = ""; }; @@ -135,6 +184,7 @@ 33FAB671232836740065AC1E /* Runner */ = { isa = PBXGroup; children = ( + CC13D42E2847C8C200EF8B54 /* rustdesk.xcodeproj */, 33CC10F02044A3C60003C045 /* AppDelegate.swift */, 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, 33E51913231747F40026EE4D /* DebugProfile.entitlements */, @@ -145,9 +195,31 @@ path = Runner; sourceTree = ""; }; + A6C450E1C32EC39A23170131 /* Pods */ = { + isa = PBXGroup; + children = ( + 7436B85D94E8F7B5A9324869 /* Pods-Runner.debug.xcconfig */, + 295AD07E63F13855C270A0E0 /* Pods-Runner.release.xcconfig */, + C3BB669FF6190AE1B11BCAEA /* Pods-Runner.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + CC13D42F2847C8C200EF8B54 /* Products */ = { + isa = PBXGroup; + children = ( + CC13D4362847C8C200EF8B54 /* librustdesk.dylib */, + CC13D4382847C8C200EF8B54 /* liblibrustdesk_static.a */, + CC13D43A2847C8C200EF8B54 /* naming */, + CC13D43C2847C8C200EF8B54 /* rustdesk */, + ); + name = Products; + sourceTree = ""; + }; D73912EC22F37F3D000D13A0 /* Frameworks */ = { isa = PBXGroup; children = ( + 26C84465887F29AE938039CB /* Pods_Runner.framework */, ); name = Frameworks; sourceTree = ""; @@ -159,15 +231,18 @@ isa = PBXNativeTarget; buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + 610B125EE2B990E4D4B30D05 /* [CP] Check Pods Manifest.lock */, 33CC10E92044A3C60003C045 /* Sources */, 33CC10EA2044A3C60003C045 /* Frameworks */, 33CC10EB2044A3C60003C045 /* Resources */, 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, + 4688A20DD8E4F3E900927B2C /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); dependencies = ( + CC13D43E2847C8CB00EF8B54 /* PBXTargetDependency */, 33CC11202044C79F0003C045 /* PBXTargetDependency */, ); name = Runner; @@ -212,6 +287,12 @@ mainGroup = 33CC10E42044A3C60003C045; productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; projectDirPath = ""; + projectReferences = ( + { + ProductGroup = CC13D42F2847C8C200EF8B54 /* Products */; + ProjectRef = CC13D42E2847C8C200EF8B54 /* rustdesk.xcodeproj */; + }, + ); projectRoot = ""; targets = ( 33CC10EC2044A3C60003C045 /* Runner */, @@ -220,6 +301,37 @@ }; /* End PBXProject section */ +/* Begin PBXReferenceProxy section */ + CC13D4362847C8C200EF8B54 /* librustdesk.dylib */ = { + isa = PBXReferenceProxy; + fileType = "compiled.mach-o.dylib"; + path = librustdesk.dylib; + remoteRef = CC13D4352847C8C200EF8B54 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + CC13D4382847C8C200EF8B54 /* liblibrustdesk_static.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = liblibrustdesk_static.a; + remoteRef = CC13D4372847C8C200EF8B54 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + CC13D43A2847C8C200EF8B54 /* naming */ = { + isa = PBXReferenceProxy; + fileType = "compiled.mach-o.executable"; + path = naming; + remoteRef = CC13D4392847C8C200EF8B54 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + CC13D43C2847C8C200EF8B54 /* rustdesk */ = { + isa = PBXReferenceProxy; + fileType = "compiled.mach-o.executable"; + path = rustdesk; + remoteRef = CC13D43B2847C8C200EF8B54 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; +/* End PBXReferenceProxy section */ + /* Begin PBXResourcesBuildPhase section */ 33CC10EB2044A3C60003C045 /* Resources */ = { isa = PBXResourcesBuildPhase; @@ -270,6 +382,45 @@ shellPath = /bin/sh; shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; }; + 4688A20DD8E4F3E900927B2C /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 610B125EE2B990E4D4B30D05 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -291,6 +442,11 @@ target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; }; + CC13D43E2847C8CB00EF8B54 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = "librustdesk-cdylib"; + targetProxy = CC13D43D2847C8CB00EF8B54 /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ @@ -493,6 +649,7 @@ "@executable_path/../Frameworks", ); PROVISIONING_PROFILE_SPECIFIER = ""; + "SWIFT_OBJC_BRIDGING_HEADER[arch=*]" = Runner/bridge_generated.h; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; }; @@ -513,6 +670,7 @@ "@executable_path/../Frameworks", ); PROVISIONING_PROFILE_SPECIFIER = ""; + "SWIFT_OBJC_BRIDGING_HEADER[arch=*]" = Runner/bridge_generated.h; SWIFT_VERSION = 5.0; }; name = Release; diff --git a/flutter/macos/Runner.xcworkspace/contents.xcworkspacedata b/flutter/macos/Runner.xcworkspace/contents.xcworkspacedata index 1d526a16e..21a3cc14c 100644 --- a/flutter/macos/Runner.xcworkspace/contents.xcworkspacedata +++ b/flutter/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -4,4 +4,7 @@ + + diff --git a/flutter/macos/Runner/AppDelegate.swift b/flutter/macos/Runner/AppDelegate.swift index d53ef6437..156e0c79b 100644 --- a/flutter/macos/Runner/AppDelegate.swift +++ b/flutter/macos/Runner/AppDelegate.swift @@ -4,6 +4,7 @@ import FlutterMacOS @NSApplicationMain class AppDelegate: FlutterAppDelegate { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + dummy_method_to_enforce_bundling() return true } } diff --git a/flutter/macos/Runner/MainFlutterWindow.swift b/flutter/macos/Runner/MainFlutterWindow.swift index f3ed804b1..17f024ec5 100644 --- a/flutter/macos/Runner/MainFlutterWindow.swift +++ b/flutter/macos/Runner/MainFlutterWindow.swift @@ -3,18 +3,22 @@ import FlutterMacOS import bitsdojo_window_macos class MainFlutterWindow: BitsdojoWindow { - override func awakeFromNib() { - let flutterViewController = FlutterViewController.init() - let windowFrame = self.frame - self.contentViewController = flutterViewController - self.setFrame(windowFrame, display: true) - - RegisterGeneratedPlugins(registry: flutterViewController) - - super.awakeFromNib() - } - - override func bitsdojo_window_configure() -> UInt { - return BDW_CUSTOM_FRAME | BDW_HIDE_ON_STARTUP - } + override func awakeFromNib() { + if (!rustdesk_core_main()){ + print("Rustdesk core returns false, exiting without launching Flutter app") + NSApplication.shared.terminate(self) + } + let flutterViewController = FlutterViewController.init() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } + + override func bitsdojo_window_configure() -> UInt { + return BDW_CUSTOM_FRAME | BDW_HIDE_ON_STARTUP + } } diff --git a/flutter/macos/Runner/bridge_generated.h b/flutter/macos/Runner/bridge_generated.h new file mode 100644 index 000000000..20f318836 --- /dev/null +++ b/flutter/macos/Runner/bridge_generated.h @@ -0,0 +1,201 @@ +#include +#include +#include + +typedef struct wire_uint_8_list { + uint8_t *ptr; + int32_t len; +} wire_uint_8_list; + +typedef struct WireSyncReturnStruct { + uint8_t *ptr; + int32_t len; + bool success; +} WireSyncReturnStruct; + +typedef int64_t DartPort; + +typedef bool (*DartPostCObjectFnType)(DartPort port_id, void *message); + +void wire_rustdesk_core_main(int64_t port_); + +void wire_start_global_event_stream(int64_t port_); + +void wire_session_connect(int64_t port_, struct wire_uint_8_list *id, bool is_file_transfer); + +void wire_get_session_remember(int64_t port_, struct wire_uint_8_list *id); + +void wire_get_session_toggle_option(int64_t port_, + struct wire_uint_8_list *id, + struct wire_uint_8_list *arg); + +struct WireSyncReturnStruct wire_get_session_toggle_option_sync(struct wire_uint_8_list *id, + struct wire_uint_8_list *arg); + +void wire_get_session_image_quality(int64_t port_, struct wire_uint_8_list *id); + +void wire_get_session_option(int64_t port_, + struct wire_uint_8_list *id, + struct wire_uint_8_list *arg); + +void wire_session_login(int64_t port_, + struct wire_uint_8_list *id, + struct wire_uint_8_list *password, + bool remember); + +void wire_session_close(int64_t port_, struct wire_uint_8_list *id); + +void wire_session_refresh(int64_t port_, struct wire_uint_8_list *id); + +void wire_session_reconnect(int64_t port_, struct wire_uint_8_list *id); + +void wire_session_toggle_option(int64_t port_, + struct wire_uint_8_list *id, + struct wire_uint_8_list *value); + +void wire_session_set_image_quality(int64_t port_, + struct wire_uint_8_list *id, + struct wire_uint_8_list *value); + +void wire_session_lock_screen(int64_t port_, struct wire_uint_8_list *id); + +void wire_session_ctrl_alt_del(int64_t port_, struct wire_uint_8_list *id); + +void wire_session_switch_display(int64_t port_, struct wire_uint_8_list *id, int32_t value); + +void wire_session_input_key(int64_t port_, + struct wire_uint_8_list *id, + struct wire_uint_8_list *name, + bool down, + bool press, + bool alt, + bool ctrl, + bool shift, + bool command); + +void wire_session_input_string(int64_t port_, + struct wire_uint_8_list *id, + struct wire_uint_8_list *value); + +void wire_session_send_chat(int64_t port_, + struct wire_uint_8_list *id, + struct wire_uint_8_list *text); + +void wire_session_send_mouse(int64_t port_, + struct wire_uint_8_list *id, + int32_t mask, + int32_t x, + int32_t y, + bool alt, + bool ctrl, + bool shift, + bool command); + +void wire_session_peer_option(int64_t port_, + struct wire_uint_8_list *id, + struct wire_uint_8_list *name, + struct wire_uint_8_list *value); + +void wire_session_input_os_password(int64_t port_, + struct wire_uint_8_list *id, + struct wire_uint_8_list *value); + +void wire_session_read_remote_dir(int64_t port_, + struct wire_uint_8_list *id, + struct wire_uint_8_list *path, + bool include_hidden); + +void wire_session_send_files(int64_t port_, + struct wire_uint_8_list *id, + int32_t act_id, + struct wire_uint_8_list *path, + struct wire_uint_8_list *to, + int32_t file_num, + bool include_hidden, + bool is_remote); + +void wire_session_set_confirm_override_file(int64_t port_, + struct wire_uint_8_list *id, + int32_t act_id, + int32_t file_num, + bool need_override, + bool remember, + bool is_upload); + +void wire_session_remove_file(int64_t port_, + struct wire_uint_8_list *id, + int32_t act_id, + struct wire_uint_8_list *path, + int32_t file_num, + bool is_remote); + +void wire_session_read_dir_recursive(int64_t port_, + struct wire_uint_8_list *id, + int32_t act_id, + struct wire_uint_8_list *path, + bool is_remote); + +void wire_session_remove_all_empty_dirs(int64_t port_, + struct wire_uint_8_list *id, + int32_t act_id, + struct wire_uint_8_list *path, + bool is_remote); + +void wire_session_cancel_job(int64_t port_, struct wire_uint_8_list *id, int32_t act_id); + +void wire_session_create_dir(int64_t port_, + struct wire_uint_8_list *id, + int32_t act_id, + struct wire_uint_8_list *path, + bool is_remote); + +struct wire_uint_8_list *new_uint_8_list(int32_t len); + +void free_WireSyncReturnStruct(struct WireSyncReturnStruct val); + +void store_dart_post_cobject(DartPostCObjectFnType ptr); + +/** + * FFI for rustdesk core's main entry. + * Return true if the app should continue running with UI(possibly Flutter), false if the app should exit. + */ +bool rustdesk_core_main(void); + +static int64_t dummy_method_to_enforce_bundling(void) { + int64_t dummy_var = 0; + dummy_var ^= ((int64_t) (void*) wire_rustdesk_core_main); + dummy_var ^= ((int64_t) (void*) wire_start_global_event_stream); + dummy_var ^= ((int64_t) (void*) wire_session_connect); + dummy_var ^= ((int64_t) (void*) wire_get_session_remember); + dummy_var ^= ((int64_t) (void*) wire_get_session_toggle_option); + dummy_var ^= ((int64_t) (void*) wire_get_session_toggle_option_sync); + dummy_var ^= ((int64_t) (void*) wire_get_session_image_quality); + dummy_var ^= ((int64_t) (void*) wire_get_session_option); + dummy_var ^= ((int64_t) (void*) wire_session_login); + dummy_var ^= ((int64_t) (void*) wire_session_close); + dummy_var ^= ((int64_t) (void*) wire_session_refresh); + dummy_var ^= ((int64_t) (void*) wire_session_reconnect); + dummy_var ^= ((int64_t) (void*) wire_session_toggle_option); + dummy_var ^= ((int64_t) (void*) wire_session_set_image_quality); + dummy_var ^= ((int64_t) (void*) wire_session_lock_screen); + dummy_var ^= ((int64_t) (void*) wire_session_ctrl_alt_del); + dummy_var ^= ((int64_t) (void*) wire_session_switch_display); + dummy_var ^= ((int64_t) (void*) wire_session_input_key); + dummy_var ^= ((int64_t) (void*) wire_session_input_string); + dummy_var ^= ((int64_t) (void*) wire_session_send_chat); + dummy_var ^= ((int64_t) (void*) wire_session_send_mouse); + dummy_var ^= ((int64_t) (void*) wire_session_peer_option); + dummy_var ^= ((int64_t) (void*) wire_session_input_os_password); + dummy_var ^= ((int64_t) (void*) wire_session_read_remote_dir); + dummy_var ^= ((int64_t) (void*) wire_session_send_files); + dummy_var ^= ((int64_t) (void*) wire_session_set_confirm_override_file); + dummy_var ^= ((int64_t) (void*) wire_session_remove_file); + dummy_var ^= ((int64_t) (void*) wire_session_read_dir_recursive); + dummy_var ^= ((int64_t) (void*) wire_session_remove_all_empty_dirs); + dummy_var ^= ((int64_t) (void*) wire_session_cancel_job); + dummy_var ^= ((int64_t) (void*) wire_session_create_dir); + dummy_var ^= ((int64_t) (void*) new_uint_8_list); + dummy_var ^= ((int64_t) (void*) free_WireSyncReturnStruct); + dummy_var ^= ((int64_t) (void*) store_dart_post_cobject); + return dummy_var; +} \ No newline at end of file diff --git a/flutter/macos/rustdesk.xcodeproj/project.pbxproj b/flutter/macos/rustdesk.xcodeproj/project.pbxproj new file mode 100644 index 000000000..bed41ae67 --- /dev/null +++ b/flutter/macos/rustdesk.xcodeproj/project.pbxproj @@ -0,0 +1,439 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 53; + objects = { + +/* Begin PBXBuildFile section */ + CA6061C6409F12977AAB839F /* Cargo.toml in Sources */ = {isa = PBXBuildFile; fileRef = CA603C4309E13EF4668187A5 /* Cargo.toml */; settings = {COMPILER_FLAGS = "--lib"; }; }; + CA6061C6409FC858B7409EE3 /* Cargo.toml in Sources */ = {isa = PBXBuildFile; fileRef = CA603C4309E13EF4668187A5 /* Cargo.toml */; settings = {COMPILER_FLAGS = "--bin naming"; }; }; + CA6061C6409FC9FA710A2219 /* Cargo.toml in Sources */ = {isa = PBXBuildFile; fileRef = CA603C4309E13EF4668187A5 /* Cargo.toml */; settings = {COMPILER_FLAGS = "--bin rustdesk"; }; }; + CA6061C6409FD6691E4C3FF1 /* Cargo.toml in Sources */ = {isa = PBXBuildFile; fileRef = CA603C4309E13EF4668187A5 /* Cargo.toml */; settings = {COMPILER_FLAGS = "--lib"; }; }; +/* End PBXBuildFile section */ + +/* Begin PBXBuildRule section */ + CA603C4309E1AC6C1400ACA8 /* PBXBuildRule */ = { + isa = PBXBuildRule; + compilerSpec = com.apple.compilers.proxy.script; + dependencyFile = "$(DERIVED_FILE_DIR)/$(CARGO_XCODE_TARGET_ARCH)-$(EXECUTABLE_NAME).d"; + filePatterns = "*/Cargo.toml"; + fileType = pattern.proxy; + inputFiles = ( + ); + isEditable = 0; + name = "Cargo project build"; + outputFiles = ( + "$(OBJECT_FILE_DIR)/$(CARGO_XCODE_TARGET_ARCH)-$(EXECUTABLE_NAME)", + ); + script = "# generated with cargo-xcode 1.4.1\n\nset -eu; export PATH=$PATH:~/.cargo/bin:/usr/local/bin;\nif [ \"${IS_MACCATALYST-NO}\" = YES ]; then\n CARGO_XCODE_TARGET_TRIPLE=\"${CARGO_XCODE_TARGET_ARCH}-apple-ios-macabi\"\nelse\n CARGO_XCODE_TARGET_TRIPLE=\"${CARGO_XCODE_TARGET_ARCH}-apple-${CARGO_XCODE_TARGET_OS}\"\nfi\nif [ \"$CARGO_XCODE_TARGET_OS\" != \"darwin\" ]; then\n PATH=\"${PATH/\\/Contents\\/Developer\\/Toolchains\\/XcodeDefault.xctoolchain\\/usr\\/bin:/xcode-provided-ld-cant-link-lSystem-for-the-host-build-script:}\"\nfi\nPATH=\"$PATH:/opt/homebrew/bin\" # Rust projects often depend on extra tools like nasm, which Xcode lacks\nif [ \"$CARGO_XCODE_BUILD_MODE\" == release ]; then\n OTHER_INPUT_FILE_FLAGS=\"${OTHER_INPUT_FILE_FLAGS} --release\"\nfi\nif command -v rustup &> /dev/null; then\n if ! rustup target list --installed | egrep -q \"${CARGO_XCODE_TARGET_TRIPLE}\"; then\n echo \"warning: this build requires rustup toolchain for $CARGO_XCODE_TARGET_TRIPLE, but it isn't installed\"\n rustup target add \"${CARGO_XCODE_TARGET_TRIPLE}\" || echo >&2 \"warning: can't install $CARGO_XCODE_TARGET_TRIPLE\"\n fi\nfi\nif [ \"$ACTION\" = clean ]; then\n ( set -x; cargo clean --manifest-path=\"$SCRIPT_INPUT_FILE\" ${OTHER_INPUT_FILE_FLAGS} --target=\"${CARGO_XCODE_TARGET_TRIPLE}\"; );\nelse\n ( set -x; cargo build --manifest-path=\"$SCRIPT_INPUT_FILE\" --features=\"${CARGO_XCODE_FEATURES:-}\" ${OTHER_INPUT_FILE_FLAGS} --target=\"${CARGO_XCODE_TARGET_TRIPLE}\"; );\nfi\n# it's too hard to explain Cargo's actual exe path to Xcode build graph, so hardlink to a known-good path instead\nBUILT_SRC=\"${CARGO_TARGET_DIR}/${CARGO_XCODE_TARGET_TRIPLE}/${CARGO_XCODE_BUILD_MODE}/${CARGO_XCODE_CARGO_FILE_NAME}\"\nln -f -- \"$BUILT_SRC\" \"$SCRIPT_OUTPUT_FILE_0\"\n\n# xcode generates dep file, but for its own path, so append our rename to it\nDEP_FILE_SRC=\"${CARGO_TARGET_DIR}/${CARGO_XCODE_TARGET_TRIPLE}/${CARGO_XCODE_BUILD_MODE}/${CARGO_XCODE_CARGO_DEP_FILE_NAME}\"\nif [ -f \"$DEP_FILE_SRC\" ]; then\n DEP_FILE_DST=\"${DERIVED_FILE_DIR}/${CARGO_XCODE_TARGET_ARCH}-${EXECUTABLE_NAME}.d\"\n cp -f \"$DEP_FILE_SRC\" \"$DEP_FILE_DST\"\n echo >> \"$DEP_FILE_DST\" \"$SCRIPT_OUTPUT_FILE_0: $BUILT_SRC\"\nfi\n\n# lipo script needs to know all the platform-specific files that have been built\n# archs is in the file name, so that paths don't stay around after archs change\n# must match input for LipoScript\nFILE_LIST=\"${DERIVED_FILE_DIR}/${ARCHS}-${EXECUTABLE_NAME}.xcfilelist\"\ntouch \"$FILE_LIST\"\nif ! egrep -q \"$SCRIPT_OUTPUT_FILE_0\" \"$FILE_LIST\" ; then\n echo >> \"$FILE_LIST\" \"$SCRIPT_OUTPUT_FILE_0\"\nfi\n"; + }; +/* End PBXBuildRule section */ + +/* Begin PBXFileReference section */ + ADDEDBA66A6E1 /* libresolv.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libresolv.tbd; path = usr/lib/libresolv.tbd; sourceTree = SDKROOT; }; + CA603C4309E13EF4668187A5 /* Cargo.toml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = Cargo.toml; path = /Users/ruizruiz/Work/Code/Projects/RustDesk/rustdesk/Cargo.toml; sourceTree = ""; }; + CA604C7415FB2A3731F5016A /* liblibrustdesk_static.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = liblibrustdesk_static.a; sourceTree = BUILT_PRODUCTS_DIR; }; + CA6071B5A0F5A7A3EF2297AA /* librustdesk.dylib */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.dylib"; includeInIndex = 0; path = librustdesk.dylib; sourceTree = BUILT_PRODUCTS_DIR; }; + CA60D3BC5386B357B2AB834F /* rustdesk */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = rustdesk; sourceTree = BUILT_PRODUCTS_DIR; }; + CA60D3BC5386D3D7DBD96893 /* naming */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = naming; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXGroup section */ + ADDEDBA66A6E2 /* Required for static linking */ = { + isa = PBXGroup; + children = ( + ADDEDBA66A6E1 /* libresolv.tbd */, + ); + name = "Required for static linking"; + sourceTree = ""; + }; + CA603C4309E122869D176AE5 /* Products */ = { + isa = PBXGroup; + children = ( + CA6071B5A0F5A7A3EF2297AA /* librustdesk.dylib */, + CA604C7415FB2A3731F5016A /* liblibrustdesk_static.a */, + CA60D3BC5386D3D7DBD96893 /* naming */, + CA60D3BC5386B357B2AB834F /* rustdesk */, + ); + name = Products; + sourceTree = ""; + }; + CA603C4309E198AF0B5890DB /* Frameworks */ = { + isa = PBXGroup; + children = ( + ADDEDBA66A6E2 /* Required for static linking */, + ); + name = Frameworks; + sourceTree = ""; + }; + CA603C4309E1D65BC3C892A8 = { + isa = PBXGroup; + children = ( + CA603C4309E13EF4668187A5 /* Cargo.toml */, + CA603C4309E122869D176AE5 /* Products */, + CA603C4309E198AF0B5890DB /* Frameworks */, + ); + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + CA604C7415FB12977AAB839F /* librustdesk-staticlib */ = { + isa = PBXNativeTarget; + buildConfigurationList = CA6028B9540B12977AAB839F /* Build configuration list for PBXNativeTarget "librustdesk-staticlib" */; + buildPhases = ( + CA6033723F8212977AAB839F /* Sources */, + CA603C4309E1AF6EBB7F357C /* Universal Binary lipo */, + ); + buildRules = ( + CA603C4309E1AC6C1400ACA8 /* PBXBuildRule */, + ); + dependencies = ( + ); + name = "librustdesk-staticlib"; + productName = liblibrustdesk_static.a; + productReference = CA604C7415FB2A3731F5016A /* liblibrustdesk_static.a */; + productType = "com.apple.product-type.library.static"; + }; + CA6071B5A0F5D6691E4C3FF1 /* librustdesk-cdylib */ = { + isa = PBXNativeTarget; + buildConfigurationList = CA6028B9540BD6691E4C3FF1 /* Build configuration list for PBXNativeTarget "librustdesk-cdylib" */; + buildPhases = ( + CA6033723F82D6691E4C3FF1 /* Sources */, + CA603C4309E1AF6EBB7F357C /* Universal Binary lipo */, + ); + buildRules = ( + CA603C4309E1AC6C1400ACA8 /* PBXBuildRule */, + ); + dependencies = ( + ); + name = "librustdesk-cdylib"; + productName = librustdesk.dylib; + productReference = CA6071B5A0F5A7A3EF2297AA /* librustdesk.dylib */; + productType = "com.apple.product-type.library.dynamic"; + }; + CA60D3BC5386C858B7409EE3 /* naming-bin */ = { + isa = PBXNativeTarget; + buildConfigurationList = CA6028B9540BC858B7409EE3 /* Build configuration list for PBXNativeTarget "naming-bin" */; + buildPhases = ( + CA6033723F82C858B7409EE3 /* Sources */, + CA603C4309E1AF6EBB7F357C /* Universal Binary lipo */, + ); + buildRules = ( + CA603C4309E1AC6C1400ACA8 /* PBXBuildRule */, + ); + dependencies = ( + ); + name = "naming-bin"; + productName = naming; + productReference = CA60D3BC5386D3D7DBD96893 /* naming */; + productType = "com.apple.product-type.tool"; + }; + CA60D3BC5386C9FA710A2219 /* rustdesk-bin */ = { + isa = PBXNativeTarget; + buildConfigurationList = CA6028B9540BC9FA710A2219 /* Build configuration list for PBXNativeTarget "rustdesk-bin" */; + buildPhases = ( + CA6033723F82C9FA710A2219 /* Sources */, + CA603C4309E1AF6EBB7F357C /* Universal Binary lipo */, + ); + buildRules = ( + CA603C4309E1AC6C1400ACA8 /* PBXBuildRule */, + ); + dependencies = ( + ); + name = "rustdesk-bin"; + productName = rustdesk; + productReference = CA60D3BC5386B357B2AB834F /* rustdesk */; + productType = "com.apple.product-type.tool"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + CA603C4309E1E04653AD465F /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1300; + TargetAttributes = { + CA604C7415FB12977AAB839F = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Automatic; + }; + CA6071B5A0F5D6691E4C3FF1 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Automatic; + }; + CA60D3BC5386C858B7409EE3 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Automatic; + }; + CA60D3BC5386C9FA710A2219 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = CA603C4309E180E02D6C7F57 /* Build configuration list for PBXProject "rustdesk" */; + compatibilityVersion = "Xcode 11.4"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = CA603C4309E1D65BC3C892A8; + productRefGroup = CA603C4309E122869D176AE5 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + CA6071B5A0F5D6691E4C3FF1 /* librustdesk-cdylib */, + CA604C7415FB12977AAB839F /* librustdesk-staticlib */, + CA60D3BC5386C858B7409EE3 /* naming-bin */, + CA60D3BC5386C9FA710A2219 /* rustdesk-bin */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXShellScriptBuildPhase section */ + CA603C4309E1AF6EBB7F357C /* Universal Binary lipo */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "$(DERIVED_FILE_DIR)/$(ARCHS)-$(EXECUTABLE_NAME).xcfilelist", + ); + name = "Universal Binary lipo"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(TARGET_BUILD_DIR)/$(EXECUTABLE_PATH)", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "# generated with cargo-xcode 1.4.1\nset -eux; cat \"$DERIVED_FILE_DIR/$ARCHS-$EXECUTABLE_NAME.xcfilelist\" | tr '\\n' '\\0' | xargs -0 lipo -create -output \"$TARGET_BUILD_DIR/$EXECUTABLE_PATH\""; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + CA6033723F8212977AAB839F /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + CA6061C6409F12977AAB839F /* Cargo.toml in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + CA6033723F82C858B7409EE3 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + CA6061C6409FC858B7409EE3 /* Cargo.toml in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + CA6033723F82C9FA710A2219 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + CA6061C6409FC9FA710A2219 /* Cargo.toml in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + CA6033723F82D6691E4C3FF1 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + CA6061C6409FD6691E4C3FF1 /* Cargo.toml in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + CA604B55B26012977AAB839F /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CARGO_XCODE_CARGO_DEP_FILE_NAME = liblibrustdesk.d; + CARGO_XCODE_CARGO_FILE_NAME = liblibrustdesk.a; + INSTALL_GROUP = ""; + INSTALL_MODE_FLAG = ""; + INSTALL_OWNER = ""; + PRODUCT_NAME = librustdesk_static; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "macosx iphonesimulator iphoneos appletvsimulator appletvos"; + }; + name = Debug; + }; + CA604B55B260C858B7409EE3 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CARGO_XCODE_CARGO_DEP_FILE_NAME = naming.d; + CARGO_XCODE_CARGO_FILE_NAME = naming; + PRODUCT_NAME = naming; + SUPPORTED_PLATFORMS = macosx; + }; + name = Debug; + }; + CA604B55B260C9FA710A2219 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CARGO_XCODE_CARGO_DEP_FILE_NAME = rustdesk.d; + CARGO_XCODE_CARGO_FILE_NAME = rustdesk; + PRODUCT_NAME = rustdesk; + SUPPORTED_PLATFORMS = macosx; + }; + name = Debug; + }; + CA604B55B260D6691E4C3FF1 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CARGO_XCODE_CARGO_DEP_FILE_NAME = liblibrustdesk.d; + CARGO_XCODE_CARGO_FILE_NAME = liblibrustdesk.dylib; + PRODUCT_NAME = librustdesk; + SUPPORTED_PLATFORMS = macosx; + }; + name = Debug; + }; + CA60583BB9CE12977AAB839F /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CARGO_XCODE_CARGO_DEP_FILE_NAME = liblibrustdesk.d; + CARGO_XCODE_CARGO_FILE_NAME = liblibrustdesk.a; + INSTALL_GROUP = ""; + INSTALL_MODE_FLAG = ""; + INSTALL_OWNER = ""; + PRODUCT_NAME = librustdesk_static; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "macosx iphonesimulator iphoneos appletvsimulator appletvos"; + }; + name = Release; + }; + CA60583BB9CEC858B7409EE3 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CARGO_XCODE_CARGO_DEP_FILE_NAME = naming.d; + CARGO_XCODE_CARGO_FILE_NAME = naming; + PRODUCT_NAME = naming; + SUPPORTED_PLATFORMS = macosx; + }; + name = Release; + }; + CA60583BB9CEC9FA710A2219 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CARGO_XCODE_CARGO_DEP_FILE_NAME = rustdesk.d; + CARGO_XCODE_CARGO_FILE_NAME = rustdesk; + PRODUCT_NAME = rustdesk; + SUPPORTED_PLATFORMS = macosx; + }; + name = Release; + }; + CA60583BB9CED6691E4C3FF1 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CARGO_XCODE_CARGO_DEP_FILE_NAME = liblibrustdesk.d; + CARGO_XCODE_CARGO_FILE_NAME = liblibrustdesk.dylib; + PRODUCT_NAME = librustdesk; + SUPPORTED_PLATFORMS = macosx; + }; + name = Release; + }; + CA608F3F78EE228BE02872F8 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CARGO_TARGET_DIR = "$(PROJECT_TEMP_DIR)/cargo_target"; + CARGO_XCODE_BUILD_MODE = debug; + CARGO_XCODE_FEATURES = ""; + "CARGO_XCODE_TARGET_ARCH[arch=arm64*]" = aarch64; + "CARGO_XCODE_TARGET_ARCH[arch=i386]" = i686; + "CARGO_XCODE_TARGET_ARCH[arch=x86_64*]" = x86_64; + "CARGO_XCODE_TARGET_OS[sdk=appletvos*]" = tvos; + "CARGO_XCODE_TARGET_OS[sdk=appletvsimulator*]" = tvos; + "CARGO_XCODE_TARGET_OS[sdk=iphoneos*]" = ios; + "CARGO_XCODE_TARGET_OS[sdk=iphonesimulator*]" = "ios-sim"; + "CARGO_XCODE_TARGET_OS[sdk=macosx*]" = darwin; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_NAME = rustdesk; + SDKROOT = macosx; + SUPPORTS_MACCATALYST = YES; + }; + name = Debug; + }; + CA608F3F78EE3CC16B37690B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CARGO_TARGET_DIR = "$(PROJECT_TEMP_DIR)/cargo_target"; + CARGO_XCODE_BUILD_MODE = release; + CARGO_XCODE_FEATURES = ""; + "CARGO_XCODE_TARGET_ARCH[arch=arm64*]" = aarch64; + "CARGO_XCODE_TARGET_ARCH[arch=i386]" = i686; + "CARGO_XCODE_TARGET_ARCH[arch=x86_64*]" = x86_64; + "CARGO_XCODE_TARGET_OS[sdk=appletvos*]" = tvos; + "CARGO_XCODE_TARGET_OS[sdk=appletvsimulator*]" = tvos; + "CARGO_XCODE_TARGET_OS[sdk=iphoneos*]" = ios; + "CARGO_XCODE_TARGET_OS[sdk=iphonesimulator*]" = "ios-sim"; + "CARGO_XCODE_TARGET_OS[sdk=macosx*]" = darwin; + PRODUCT_NAME = rustdesk; + SDKROOT = macosx; + SUPPORTS_MACCATALYST = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + CA6028B9540B12977AAB839F /* Build configuration list for PBXNativeTarget "librustdesk-staticlib" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + CA60583BB9CE12977AAB839F /* Release */, + CA604B55B26012977AAB839F /* Debug */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + CA6028B9540BC858B7409EE3 /* Build configuration list for PBXNativeTarget "naming-bin" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + CA60583BB9CEC858B7409EE3 /* Release */, + CA604B55B260C858B7409EE3 /* Debug */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + CA6028B9540BC9FA710A2219 /* Build configuration list for PBXNativeTarget "rustdesk-bin" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + CA60583BB9CEC9FA710A2219 /* Release */, + CA604B55B260C9FA710A2219 /* Debug */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + CA6028B9540BD6691E4C3FF1 /* Build configuration list for PBXNativeTarget "librustdesk-cdylib" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + CA60583BB9CED6691E4C3FF1 /* Release */, + CA604B55B260D6691E4C3FF1 /* Debug */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + CA603C4309E180E02D6C7F57 /* Build configuration list for PBXProject "rustdesk" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + CA608F3F78EE3CC16B37690B /* Release */, + CA608F3F78EE228BE02872F8 /* Debug */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = CA603C4309E1E04653AD465F /* Project object */; +} diff --git a/libs/scrap/build.rs b/libs/scrap/build.rs index 93ea41ca7..b59dc03f3 100644 --- a/libs/scrap/build.rs +++ b/libs/scrap/build.rs @@ -3,9 +3,8 @@ use std::{ path::{Path, PathBuf}, }; -fn find_package(name: &str) -> Vec { - let vcpkg_root = std::env::var("VCPKG_ROOT").unwrap(); - let mut path: PathBuf = vcpkg_root.into(); +/// Link vcppkg package. +fn link_vcpkg(mut path: PathBuf, name: &str) -> PathBuf { let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); let mut target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap(); if target_arch == "x86_64" { @@ -26,8 +25,13 @@ fn find_package(name: &str) -> Vec { println!("cargo:info={}", target); path.push("installed"); path.push(target); - let lib = name.trim_start_matches("lib").to_string(); - println!("{}", format!("cargo:rustc-link-lib=static={}", lib)); + println!( + "{}", + format!( + "cargo:rustc-link-lib=static={}", + name.trim_start_matches("lib") + ) + ); println!( "{}", format!( @@ -37,7 +41,68 @@ fn find_package(name: &str) -> Vec { ); let include = path.join("include"); println!("{}", format!("cargo:include={}", include.to_str().unwrap())); - vec![include] + include +} + +/// Link homebrew package(for Mac M1). +fn link_homebrew_m1(name: &str) -> PathBuf { + let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); + let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap(); + if target_os != "macos" || target_arch != "aarch64" { + panic!("Couldn't find VCPKG_ROOT, also can't fallback to homebrew because it's only for macos aarch64."); + } + let mut path = PathBuf::from("/opt/homebrew/Cellar"); + path.push(name); + let entries = if let Ok(dir) = std::fs::read_dir(&path) { + dir + } else { + panic!("Could not find package in {}. Make sure your homebrew and package {} are all installed.", path.to_str().unwrap(),&name); + }; + let mut directories = entries + .into_iter() + .filter(|x| x.is_ok()) + .map(|x| x.unwrap().path()) + .filter(|x| x.is_dir()) + .collect::>(); + // Find the newest version. + directories.sort_unstable(); + if directories.is_empty() { + panic!( + "There's no installed version of {} in /opt/homebrew/Cellar", + name + ); + } + path.push(directories.pop().unwrap()); + // Link the library. + println!( + "{}", + format!( + "cargo:rustc-link-lib=static={}", + name.trim_start_matches("lib") + ) + ); + // Add the library path. + println!( + "{}", + format!( + "cargo:rustc-link-search={}", + path.join("lib").to_str().unwrap() + ) + ); + // Add the include path. + let include = path.join("include"); + println!("{}", format!("cargo:include={}", include.to_str().unwrap())); + include +} + +/// Find package. By default, it will try to find vcpkg first, then homebrew(currently only for Mac M1). +fn find_package(name: &str) -> Vec { + if let Ok(vcpkg_root) = std::env::var("VCPKG_ROOT") { + vec![link_vcpkg(vcpkg_root.into(), name)] + } else { + // Try using homebrew + vec![link_homebrew_m1(name)] + } } fn generate_bindings( diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 7b5451ecf..4e0a61fa0 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -1,5 +1,3 @@ -#[cfg(target_os = "macos")] -mod macos; use crate::common::SOFTWARE_UPDATE_URL; use crate::ipc; use hbb_common::{ From d81d7857221d918c8946adfe8615f56ef6874765 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Thu, 2 Jun 2022 16:23:20 +0800 Subject: [PATCH 0045/2015] feat: add tray icon to status bar Signed-off-by: Kingtous --- flutter/assets/logo.ico | Bin 0 -> 67646 bytes .../lib/desktop/pages/connection_page.dart | 7 +--- .../lib/desktop/pages/desktop_home_page.dart | 34 ++++++++++++++++- flutter/lib/main.dart | 2 + flutter/lib/utils/tray_manager.dart | 22 +++++++++++ flutter/pubspec.lock | 36 ++++++++++++++++-- flutter/pubspec.yaml | 5 ++- 7 files changed, 92 insertions(+), 14 deletions(-) create mode 100644 flutter/assets/logo.ico create mode 100644 flutter/lib/utils/tray_manager.dart diff --git a/flutter/assets/logo.ico b/flutter/assets/logo.ico new file mode 100644 index 0000000000000000000000000000000000000000..5ebc028090888a67094e0f80600ad8eccbd7945f GIT binary patch literal 67646 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 { crossAxisAlignment: CrossAxisAlignment.center, children: [ getUpdateUI(), - Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - getSearchBarUI(), - ], - ), + getSearchBarUI(), SizedBox(height: 12), getPeers(), ]), diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index c42ed1b53..97104cbf3 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -1,9 +1,13 @@ -import 'package:flutter/material.dart'; +import 'dart:io'; + +import 'package:flutter/material.dart' hide MenuItem; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/desktop/pages/connection_page.dart'; import 'package:flutter_hbb/desktop/widgets/titlebar_widget.dart'; import 'package:flutter_hbb/models/model.dart'; import 'package:provider/provider.dart'; +import 'package:tray_manager/tray_manager.dart'; +import 'package:window_manager/window_manager.dart'; class DesktopHomePage extends StatefulWidget { DesktopHomePage({Key? key}) : super(key: key); @@ -14,7 +18,7 @@ class DesktopHomePage extends StatefulWidget { const borderColor = Color(0xFF2F65BA); -class _DesktopHomePageState extends State { +class _DesktopHomePageState extends State with TrayListener { @override Widget build(BuildContext context) { return Scaffold( @@ -203,4 +207,30 @@ class _DesktopHomePageState extends State { buildRecentSession(BuildContext context) { return Center(child: Text("waiting implementation")); } + + @override + void onTrayMenuItemClick(MenuItem menuItem) { + print("click ${menuItem.key}"); + switch (menuItem.key) { + case "quit": + exit(0); + case "show": + windowManager.show(); + break; + default: + break; + } + } + + @override + void initState() { + super.initState(); + trayManager.addListener(this); + } + + @override + void dispose() { + trayManager.removeListener(this); + super.dispose(); + } } diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 336f5dda6..21dc649bd 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart'; import 'package:flutter_hbb/desktop/screen/desktop_remote_screen.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; +import 'package:flutter_hbb/utils/tray_manager.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:provider/provider.dart'; import 'package:window_manager/window_manager.dart'; @@ -52,6 +53,7 @@ void runRustDeskApp(List args) async { break; } } else { + initTray(); FFI.serverModel.startService(); runApp(App()); doWhenWindowReady(() { diff --git a/flutter/lib/utils/tray_manager.dart b/flutter/lib/utils/tray_manager.dart new file mode 100644 index 000000000..d911932e5 --- /dev/null +++ b/flutter/lib/utils/tray_manager.dart @@ -0,0 +1,22 @@ +import 'dart:io'; + +import 'package:flutter_hbb/models/model.dart'; +import 'package:tray_manager/tray_manager.dart'; + +Future initTray({List? extra_item}) async { + List items = [ + MenuItem(key: "show", label: translate("show rustdesk")), + MenuItem.separator(), + MenuItem(key: "quit", label: translate("quit rustdesk")), + ]; + if (extra_item != null) { + items.insertAll(0, extra_item); + } + await Future.wait([ + trayManager + .setIcon(Platform.isWindows ? "assets/logo.ico" : "assets/logo.png"), + trayManager.setContextMenu(Menu(items: items)), + trayManager.setToolTip("rustdesk"), + trayManager.setTitle("rustdesk") + ]); +} diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index f46c07982..b4cde8caf 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -222,8 +222,8 @@ packages: dependency: "direct main" description: path: "." - ref: "4aab101f17f02312dc45311eb3009cc0ea5357c1" - resolved-ref: "4aab101f17f02312dc45311eb3009cc0ea5357c1" + ref: "704718b2853723b615675e048f1f385cbfb209a6" + resolved-ref: "704718b2853723b615675e048f1f385cbfb209a6" url: "https://github.com/Kingtous/rustdesk_desktop_multi_window" source: git version: "0.0.1" @@ -408,7 +408,7 @@ packages: name: flutter_smart_dialog url: "https://pub.dartlang.org" source: hosted - version: "4.3.2" + version: "4.3.2+1" flutter_test: dependency: "direct dev" description: flutter @@ -566,6 +566,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.1.4" + menu_base: + dependency: transitive + description: + name: menu_base + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.1" meta: dependency: transitive description: @@ -771,6 +778,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.1.0" + screen_retriever: + dependency: transitive + description: + name: screen_retriever + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.2" settings_ui: dependency: "direct main" description: @@ -848,6 +862,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.1" + shortid: + dependency: transitive + description: + name: shortid + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.2" sky_engine: dependency: transitive description: flutter @@ -930,6 +951,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.0" + tray_manager: + dependency: "direct main" + description: + name: tray_manager + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.7" tuple: dependency: "direct main" description: @@ -1076,7 +1104,7 @@ packages: name: window_manager url: "https://pub.dartlang.org" source: hosted - version: "0.2.3" + version: "0.2.5" xdg_directories: dependency: transitive description: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index a4417c25c..5ff7cc6a0 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -58,13 +58,14 @@ dependencies: url: https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge ref: master path: frb_dart - window_manager: ^0.2.3 + window_manager: ^0.2.5 desktop_multi_window: git: url: https://github.com/Kingtous/rustdesk_desktop_multi_window - ref: 4aab101f17f02312dc45311eb3009cc0ea5357c1 + ref: 704718b2853723b615675e048f1f385cbfb209a6 bitsdojo_window: ^0.1.2 freezed_annotation: ^2.0.3 + tray_manager: 0.1.7 dev_dependencies: flutter_launcher_icons: ^0.9.1 From 8c3e77001c34ceef54751f3c832dd8fc344fc3d4 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Thu, 2 Jun 2022 16:45:04 +0800 Subject: [PATCH 0046/2015] refactor: disable tray Signed-off-by: Kingtous --- flutter/lib/main.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 21dc649bd..4b71d4a22 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -5,7 +5,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart'; import 'package:flutter_hbb/desktop/screen/desktop_remote_screen.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; -import 'package:flutter_hbb/utils/tray_manager.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:provider/provider.dart'; import 'package:window_manager/window_manager.dart'; @@ -53,7 +52,8 @@ void runRustDeskApp(List args) async { break; } } else { - initTray(); + // disable tray + // initTray(); FFI.serverModel.startService(); runApp(App()); doWhenWindowReady(() { From 985c616ca617ea54e7dc725a9ac471f6cc6ced7a Mon Sep 17 00:00:00 2001 From: kingtous Date: Mon, 13 Jun 2022 21:07:26 +0800 Subject: [PATCH 0047/2015] refactor: make multi FFI object && initial flutter multi sessions support Signed-off-by: Kingtous --- flutter/lib/common.dart | 23 +- .../lib/desktop/pages/connection_page.dart | 6 +- .../lib/desktop/pages/desktop_home_page.dart | 6 +- flutter/lib/desktop/pages/remote_page.dart | 317 ++++++++------- .../desktop/screen/desktop_remote_screen.dart | 9 +- flutter/lib/main.dart | 24 +- flutter/lib/mobile/pages/chat_page.dart | 7 +- flutter/lib/mobile/pages/connection_page.dart | 14 +- .../lib/mobile/pages/file_manager_page.dart | 15 +- flutter/lib/mobile/pages/remote_page.dart | 368 +++++++++--------- flutter/lib/mobile/pages/scan_page.dart | 34 +- flutter/lib/mobile/pages/server_page.dart | 35 +- flutter/lib/mobile/pages/settings_page.dart | 57 +-- flutter/lib/mobile/widgets/dialog.dart | 7 +- flutter/lib/mobile/widgets/overlay.dart | 10 +- flutter/lib/models/chat_model.dart | 17 +- flutter/lib/models/file_model.dart | 52 +-- flutter/lib/models/model.dart | 359 +++++++++-------- flutter/lib/models/native_model.dart | 36 +- flutter/lib/models/server_model.dart | 88 +++-- flutter/pubspec.lock | 331 ++++++++-------- flutter/pubspec.yaml | 1 + 22 files changed, 976 insertions(+), 840 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 32f7c4bfa..71d9ed9ad 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/instance_manager.dart'; import 'models/model.dart'; @@ -274,7 +275,7 @@ class PermissionManager { static Future check(String type) { if (!permissions.contains(type)) return Future.error("Wrong permission!$type"); - return FFI.invokeMethod("check_permission", type); + return gFFI.invokeMethod("check_permission", type); } static Future request(String type) { @@ -283,7 +284,7 @@ class PermissionManager { _current = type; _completer = Completer(); - FFI.invokeMethod("request_permission", type); + gFFI.invokeMethod("request_permission", type); // timeout _timer?.cancel(); @@ -307,3 +308,21 @@ class PermissionManager { _current = ""; } } + +/// find ffi, tag is Remote ID +/// for session specific usage +FFI ffi(String? tag) { + return Get.find(tag: tag); +} + +/// Global FFI object +late FFI _globalFFI; + +FFI get gFFI => _globalFFI; + +Future initGlobalFFI() async { + _globalFFI = FFI(); + // after `put`, can also be globally found by Get.find(); + Get.put(_globalFFI, permanent: true); + await _globalFFI.ffiModel.init(); +} \ No newline at end of file diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index c88c52e32..78d73daee 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -44,7 +44,7 @@ class _ConnectionPageState extends State { @override Widget build(BuildContext context) { Provider.of(context); - if (_idController.text.isEmpty) _idController.text = FFI.getId(); + if (_idController.text.isEmpty) _idController.text = gFFI.getId(); return SingleChildScrollView( child: Column( mainAxisAlignment: MainAxisAlignment.start, @@ -258,7 +258,7 @@ class _ConnectionPageState extends State { width = size.width / n - 2 * space; } final cards = []; - var peers = FFI.peers(); + var peers = gFFI.peers(); peers.forEach((p) { cards.add(Container( width: width, @@ -316,7 +316,7 @@ class _ConnectionPageState extends State { elevation: 8, ); if (value == 'remove') { - setState(() => FFI.setByName('remove', '$id')); + setState(() => gFFI.setByName('remove', '$id')); () async { removePreference(id); }(); diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 97104cbf3..bbd440712 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -61,7 +61,7 @@ class _DesktopHomePageState extends State with TrayListener { buildServerInfo(BuildContext context) { return ChangeNotifierProvider.value( - value: FFI.serverModel, + value: gFFI.serverModel, child: Container( decoration: BoxDecoration(color: MyTheme.white), child: Column( @@ -88,7 +88,7 @@ class _DesktopHomePageState extends State with TrayListener { } buildIDBoard(BuildContext context) { - final model = FFI.serverModel; + final model = gFFI.serverModel; return Container( margin: EdgeInsets.symmetric(vertical: 4.0, horizontal: 16.0), child: Row( @@ -123,7 +123,7 @@ class _DesktopHomePageState extends State with TrayListener { } buildPasswordBoard(BuildContext context) { - final model = FFI.serverModel; + final model = gFFI.serverModel; return Container( margin: EdgeInsets.symmetric(vertical: 4.0, horizontal: 16.0), child: Row( diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index c40a7cb47..5930b1f5a 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -8,6 +8,8 @@ import 'package:flutter/services.dart'; import 'package:flutter_hbb/mobile/widgets/gesture_help.dart'; import 'package:flutter_hbb/models/chat_model.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; +import 'package:get/route_manager.dart'; import 'package:provider/provider.dart'; import 'package:wakelock/wakelock.dart'; import 'package:window_manager/window_manager.dart'; @@ -45,10 +47,15 @@ class _RemotePageState extends State with WindowListener { var _showEdit = false; // use soft keyboard var _isPhysicalMouse = false; + FFI get _ffi => ffi(widget.id); + @override void initState() { super.initState(); - FFI.connect(widget.id); + final ffi = Get.put(FFI(), tag: widget.id); + // note: a little trick + ffi.ffiModel.platformFFI = gFFI.ffiModel.platformFFI; + ffi.connect(widget.id); WidgetsBinding.instance.addPostFrameCallback((_) { SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); showLoading(translate('Connecting...')); @@ -59,8 +66,8 @@ class _RemotePageState extends State with WindowListener { Wakelock.enable(); } _physicalFocusNode.requestFocus(); - // FFI.ffiModel.updateEventListener(widget.id); - FFI.listenToMouse(true); + ffi.ffiModel.updateEventListener(widget.id); + ffi.listenToMouse(true); WindowManager.instance.addListener(this); } @@ -68,11 +75,11 @@ class _RemotePageState extends State with WindowListener { void dispose() { print("remote page dispose"); hideMobileActionsOverlay(); - FFI.listenToMouse(false); - FFI.invokeMethod("enable_soft_keyboard", true); + _ffi.listenToMouse(false); + _ffi.invokeMethod("enable_soft_keyboard", true); _mobileFocusNode.dispose(); _physicalFocusNode.dispose(); - FFI.close(); + _ffi.close(); _interval?.cancel(); _timer?.cancel(); SmartDialog.dismiss(); @@ -82,11 +89,12 @@ class _RemotePageState extends State with WindowListener { Wakelock.disable(); } WindowManager.instance.removeListener(this); + Get.delete(tag: widget.id); super.dispose(); } void resetTool() { - FFI.resetModifiers(); + _ffi.resetModifiers(); } bool isKeyboardShown() { @@ -105,8 +113,8 @@ class _RemotePageState extends State with WindowListener { overlays: []); // [pi.version.isNotEmpty] -> check ready or not,avoid login without soft-keyboard if (chatWindowOverlayEntry == null && - FFI.ffiModel.pi.version.isNotEmpty) { - FFI.invokeMethod("enable_soft_keyboard", false); + _ffi.ffiModel.pi.version.isNotEmpty) { + _ffi.invokeMethod("enable_soft_keyboard", false); } } }); @@ -138,12 +146,12 @@ class _RemotePageState extends State with WindowListener { newValue[common] == oldValue[common]; ++common); for (i = 0; i < oldValue.length - common; ++i) { - FFI.inputKey('VK_BACK'); + _ffi.inputKey('VK_BACK'); } if (newValue.length > common) { var s = newValue.substring(common); if (s.length > 1) { - FFI.bind.sessionInputString(id: widget.id, value: s); + _ffi.bind.sessionInputString(id: widget.id, value: s); } else { inputChar(s); } @@ -161,7 +169,7 @@ class _RemotePageState extends State with WindowListener { // ? } else if (newValue.length < oldValue.length) { final char = 'VK_BACK'; - FFI.inputKey(char); + _ffi.inputKey(char); } else { final content = newValue.substring(oldValue.length); if (content.length > 1) { @@ -177,11 +185,11 @@ class _RemotePageState extends State with WindowListener { content == '()' || content == '【】')) { // can not only input content[0], because when input ], [ are also auo insert, which cause ] never be input - FFI.bind.sessionInputString(id: widget.id, value: content); + _ffi.bind.sessionInputString(id: widget.id, value: content); openKeyboard(); return; } - FFI.bind.sessionInputString(id: widget.id, value: content); + _ffi.bind.sessionInputString(id: widget.id, value: content); } else { inputChar(content); } @@ -194,11 +202,11 @@ class _RemotePageState extends State with WindowListener { } else if (char == ' ') { char = 'VK_SPACE'; } - FFI.inputKey(char); + _ffi.inputKey(char); } void openKeyboard() { - FFI.invokeMethod("enable_soft_keyboard", true); + _ffi.invokeMethod("enable_soft_keyboard", true); // destroy first, so that our _value trick can work _value = initText; setState(() => _showEdit = false); @@ -221,7 +229,7 @@ class _RemotePageState extends State with WindowListener { final label = _logicalKeyMap[e.logicalKey.keyId] ?? _physicalKeyMap[e.physicalKey.usbHidUsage] ?? e.logicalKey.keyLabel; - FFI.inputKey(label, down: down, press: press ?? false); + _ffi.inputKey(label, down: down, press: press ?? false); } @override @@ -229,7 +237,7 @@ class _RemotePageState extends State with WindowListener { final pi = Provider.of(context).pi; final hideKeyboard = isKeyboardShown() && _showEdit; final showActionButton = !_showBar || hideKeyboard; - final keyboard = FFI.ffiModel.permissions['keyboard'] != false; + final keyboard = _ffi.ffiModel.permissions['keyboard'] != false; return WillPopScope( onWillPop: () async { @@ -251,7 +259,7 @@ class _RemotePageState extends State with WindowListener { setState(() { if (hideKeyboard) { _showEdit = false; - FFI.invokeMethod("enable_soft_keyboard", false); + _ffi.invokeMethod("enable_soft_keyboard", false); _mobileFocusNode.unfocus(); _physicalFocusNode.requestFocus(); } else { @@ -291,7 +299,7 @@ class _RemotePageState extends State with WindowListener { }); } if (_isPhysicalMouse) { - FFI.handleMouse(getEvent(e, 'mousemove')); + _ffi.handleMouse(getEvent(e, 'mousemove')); } }, onPointerDown: (e) { @@ -303,19 +311,19 @@ class _RemotePageState extends State with WindowListener { } } if (_isPhysicalMouse) { - FFI.handleMouse(getEvent(e, 'mousedown')); + _ffi.handleMouse(getEvent(e, 'mousedown')); } }, onPointerUp: (e) { if (e.kind != ui.PointerDeviceKind.mouse) return; if (_isPhysicalMouse) { - FFI.handleMouse(getEvent(e, 'mouseup')); + _ffi.handleMouse(getEvent(e, 'mouseup')); } }, onPointerMove: (e) { if (e.kind != ui.PointerDeviceKind.mouse) return; if (_isPhysicalMouse) { - FFI.handleMouse(getEvent(e, 'mousemove')); + _ffi.handleMouse(getEvent(e, 'mousemove')); } }, onPointerSignal: (e) { @@ -328,7 +336,7 @@ class _RemotePageState extends State with WindowListener { if (dy > 0) dy = -1; else if (dy < 0) dy = 1; - FFI.setByName('send_mouse', + _ffi.setByName('send_mouse', '{"id": "${widget.id}", "type": "wheel", "x": "$dx", "y": "$dy"}'); } }, @@ -346,14 +354,14 @@ class _RemotePageState extends State with WindowListener { if (e.repeat) { sendRawKey(e, press: true); } else { - if (e.isAltPressed && !FFI.alt) { - FFI.alt = true; - } else if (e.isControlPressed && !FFI.ctrl) { - FFI.ctrl = true; - } else if (e.isShiftPressed && !FFI.shift) { - FFI.shift = true; - } else if (e.isMetaPressed && !FFI.command) { - FFI.command = true; + if (e.isAltPressed && !_ffi.alt) { + _ffi.alt = true; + } else if (e.isControlPressed && !_ffi.ctrl) { + _ffi.ctrl = true; + } else if (e.isShiftPressed && !_ffi.shift) { + _ffi.shift = true; + } else if (e.isMetaPressed && !_ffi.command) { + _ffi.command = true; } sendRawKey(e, down: true); } @@ -362,16 +370,16 @@ class _RemotePageState extends State with WindowListener { if (!_showEdit && e is RawKeyUpEvent) { if (key == LogicalKeyboardKey.altLeft || key == LogicalKeyboardKey.altRight) { - FFI.alt = false; + _ffi.alt = false; } else if (key == LogicalKeyboardKey.controlLeft || key == LogicalKeyboardKey.controlRight) { - FFI.ctrl = false; + _ffi.ctrl = false; } else if (key == LogicalKeyboardKey.shiftRight || key == LogicalKeyboardKey.shiftLeft) { - FFI.shift = false; + _ffi.shift = false; } else if (key == LogicalKeyboardKey.metaLeft || key == LogicalKeyboardKey.metaRight) { - FFI.command = false; + _ffi.command = false; } sendRawKey(e); } @@ -410,7 +418,7 @@ class _RemotePageState extends State with WindowListener { ] + (isWebDesktop ? [] - : FFI.ffiModel.isPeerAndroid + : _ffi.ffiModel.isPeerAndroid ? [ IconButton( color: Colors.white, @@ -431,7 +439,7 @@ class _RemotePageState extends State with WindowListener { onPressed: openKeyboard), IconButton( color: Colors.white, - icon: Icon(FFI.ffiModel.touchMode + icon: Icon(_ffi.ffiModel.touchMode ? Icons.touch_app : Icons.mouse), onPressed: changeTouchMode, @@ -444,7 +452,7 @@ class _RemotePageState extends State with WindowListener { color: Colors.white, icon: Icon(Icons.message), onPressed: () { - FFI.chatModel + _ffi.chatModel .changeCurrentID(ChatModel.clientModeID); toggleChatOverlay(); }, @@ -482,89 +490,89 @@ class _RemotePageState extends State with WindowListener { /// HoldDrag -> left drag Widget getBodyForMobileWithGesture() { - final touchMode = FFI.ffiModel.touchMode; + final touchMode = _ffi.ffiModel.touchMode; return getMixinGestureDetector( child: getBodyForMobile(), onTapUp: (d) { if (touchMode) { - FFI.cursorModel.touch( + _ffi.cursorModel.touch( d.localPosition.dx, d.localPosition.dy, MouseButtons.left); } else { - FFI.tap(MouseButtons.left); + _ffi.tap(MouseButtons.left); } }, onDoubleTapDown: (d) { if (touchMode) { - FFI.cursorModel.move(d.localPosition.dx, d.localPosition.dy); + _ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy); } }, onDoubleTap: () { - FFI.tap(MouseButtons.left); - FFI.tap(MouseButtons.left); + _ffi.tap(MouseButtons.left); + _ffi.tap(MouseButtons.left); }, onLongPressDown: (d) { if (touchMode) { - FFI.cursorModel.move(d.localPosition.dx, d.localPosition.dy); + _ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy); } }, onLongPress: () { - FFI.tap(MouseButtons.right); + _ffi.tap(MouseButtons.right); }, onDoubleFinerTap: (d) { if (!touchMode) { - FFI.tap(MouseButtons.right); + _ffi.tap(MouseButtons.right); } }, onHoldDragStart: (d) { if (!touchMode) { - FFI.sendMouse('down', MouseButtons.left); + _ffi.sendMouse('down', MouseButtons.left); } }, onHoldDragUpdate: (d) { if (!touchMode) { - FFI.cursorModel.updatePan(d.delta.dx, d.delta.dy, touchMode); + _ffi.cursorModel.updatePan(d.delta.dx, d.delta.dy, touchMode); } }, onHoldDragEnd: (_) { if (!touchMode) { - FFI.sendMouse('up', MouseButtons.left); + _ffi.sendMouse('up', MouseButtons.left); } }, onOneFingerPanStart: (d) { if (touchMode) { - FFI.cursorModel.move(d.localPosition.dx, d.localPosition.dy); - FFI.sendMouse('down', MouseButtons.left); + _ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy); + _ffi.sendMouse('down', MouseButtons.left); } }, onOneFingerPanUpdate: (d) { - FFI.cursorModel.updatePan(d.delta.dx, d.delta.dy, touchMode); + _ffi.cursorModel.updatePan(d.delta.dx, d.delta.dy, touchMode); }, onOneFingerPanEnd: (d) { if (touchMode) { - FFI.sendMouse('up', MouseButtons.left); + _ffi.sendMouse('up', MouseButtons.left); } }, // scale + pan event onTwoFingerScaleUpdate: (d) { - FFI.canvasModel.updateScale(d.scale / _scale); + _ffi.canvasModel.updateScale(d.scale / _scale); _scale = d.scale; - FFI.canvasModel.panX(d.focalPointDelta.dx); - FFI.canvasModel.panY(d.focalPointDelta.dy); + _ffi.canvasModel.panX(d.focalPointDelta.dx); + _ffi.canvasModel.panY(d.focalPointDelta.dy); }, onTwoFingerScaleEnd: (d) { _scale = 1; - FFI.bind + _ffi.bind .sessionPeerOption(id: widget.id, name: "view-style", value: ""); }, - onThreeFingerVerticalDragUpdate: FFI.ffiModel.isPeerAndroid + onThreeFingerVerticalDragUpdate: _ffi.ffiModel.isPeerAndroid ? null : (d) { _mouseScrollIntegral += d.delta.dy / 4; if (_mouseScrollIntegral > 1) { - FFI.scroll(1); + _ffi.scroll(1); _mouseScrollIntegral = 0; } else if (_mouseScrollIntegral < -1) { - FFI.scroll(-1); + _ffi.scroll(-1); _mouseScrollIntegral = 0; } }); @@ -574,8 +582,8 @@ class _RemotePageState extends State with WindowListener { return Container( color: MyTheme.canvasColor, child: Stack(children: [ - ImagePaint(), - CursorPaint(), + ImagePaint(id: widget.id), + CursorPaint(id: widget.id), getHelpTools(), SizedBox( width: 0, @@ -599,11 +607,17 @@ class _RemotePageState extends State with WindowListener { } Widget getBodyForDesktopWithListener(bool keyboard) { - var paints = [ImagePaint()]; - final cursor = FFI.bind + var paints = [ + ImagePaint( + id: widget.id, + ) + ]; + final cursor = _ffi.bind .getSessionToggleOptionSync(id: widget.id, arg: 'show-remote-cursor'); if (keyboard || cursor) { - paints.add(CursorPaint()); + paints.add(CursorPaint( + id: widget.id, + )); } return Container( color: MyTheme.canvasColor, child: Stack(children: paints)); @@ -616,10 +630,10 @@ class _RemotePageState extends State with WindowListener { out['type'] = type; out['x'] = evt.position.dx; out['y'] = evt.position.dy; - if (FFI.alt) out['alt'] = 'true'; - if (FFI.shift) out['shift'] = 'true'; - if (FFI.ctrl) out['ctrl'] = 'true'; - if (FFI.command) out['command'] = 'true'; + if (_ffi.alt) out['alt'] = 'true'; + if (_ffi.shift) out['shift'] = 'true'; + if (_ffi.ctrl) out['ctrl'] = 'true'; + if (_ffi.command) out['command'] = 'true'; out['buttons'] = evt .buttons; // left button: 1, right button: 2, middle button: 4, 1 | 2 = 3 (left + right) if (evt.buttons != 0) { @@ -635,8 +649,8 @@ class _RemotePageState extends State with WindowListener { final x = 120.0; final y = size.height; final more = >[]; - final pi = FFI.ffiModel.pi; - final perms = FFI.ffiModel.permissions; + final pi = _ffi.ffiModel.pi; + final perms = _ffi.ffiModel.permissions; if (pi.version.isNotEmpty) { more.add(PopupMenuItem( child: Text(translate('Refresh')), value: 'refresh')); @@ -672,11 +686,11 @@ class _RemotePageState extends State with WindowListener { more.add(PopupMenuItem( child: Text(translate('Insert Lock')), value: 'lock')); if (pi.platform == 'Windows' && - await FFI.bind.getSessionToggleOption(id: id, arg: 'privacy-mode') != + await _ffi.bind.getSessionToggleOption(id: id, arg: 'privacy-mode') != true) { more.add(PopupMenuItem( - child: Text(translate( - (FFI.ffiModel.inputBlocked ? 'Unb' : 'B') + 'lock user input')), + child: Text(translate((_ffi.ffiModel.inputBlocked ? 'Unb' : 'B') + + 'lock user input')), value: 'block-input')); } } @@ -688,33 +702,33 @@ class _RemotePageState extends State with WindowListener { elevation: 8, ); if (value == 'cad') { - FFI.bind.sessionCtrlAltDel(id: widget.id); + _ffi.bind.sessionCtrlAltDel(id: widget.id); } else if (value == 'lock') { - FFI.bind.sessionLockScreen(id: widget.id); + _ffi.bind.sessionLockScreen(id: widget.id); } else if (value == 'block-input') { - FFI.bind.sessionToggleOption( + _ffi.bind.sessionToggleOption( id: widget.id, - value: (FFI.ffiModel.inputBlocked ? 'un' : '') + 'block-input'); - FFI.ffiModel.inputBlocked = !FFI.ffiModel.inputBlocked; + value: (_ffi.ffiModel.inputBlocked ? 'un' : '') + 'block-input'); + _ffi.ffiModel.inputBlocked = !_ffi.ffiModel.inputBlocked; } else if (value == 'refresh') { - FFI.bind.sessionRefresh(id: widget.id); + _ffi.bind.sessionRefresh(id: widget.id); } else if (value == 'paste') { () async { ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain); if (data != null && data.text != null) { - FFI.bind.sessionInputString(id: widget.id, value: data.text ?? ""); + _ffi.bind.sessionInputString(id: widget.id, value: data.text ?? ""); } }(); } else if (value == 'enter_os_password') { var password = - await FFI.bind.getSessionOption(id: id, arg: "os-password"); + await _ffi.bind.getSessionOption(id: id, arg: "os-password"); if (password != null) { - FFI.bind.sessionInputOsPassword(id: widget.id, value: password); + _ffi.bind.sessionInputOsPassword(id: widget.id, value: password); } else { showSetOSPassword(widget.id, true); } } else if (value == 'reset_canvas') { - FFI.cursorModel.reset(); + _ffi.cursorModel.reset(); } }(); } @@ -733,11 +747,11 @@ class _RemotePageState extends State with WindowListener { return SingleChildScrollView( padding: EdgeInsets.symmetric(vertical: 10), child: GestureHelp( - touchMode: FFI.ffiModel.touchMode, + touchMode: _ffi.ffiModel.touchMode, onTouchModeChange: (t) { - FFI.ffiModel.toggleTouchMode(); - final v = FFI.ffiModel.touchMode ? 'Y' : ''; - FFI.bind.sessionPeerOption( + _ffi.ffiModel.toggleTouchMode(); + final v = _ffi.ffiModel.touchMode ? 'Y' : ''; + _ffi.bind.sessionPeerOption( id: widget.id, name: "touch-mode", value: v); })); })); @@ -769,21 +783,21 @@ class _RemotePageState extends State with WindowListener { style: TextStyle(color: Colors.white, fontSize: 11)), onPressed: onPressed); }; - final pi = FFI.ffiModel.pi; + final pi = _ffi.ffiModel.pi; final isMac = pi.platform == "Mac OS"; final modifiers = [ wrap('Ctrl ', () { - setState(() => FFI.ctrl = !FFI.ctrl); - }, FFI.ctrl), + setState(() => _ffi.ctrl = !_ffi.ctrl); + }, _ffi.ctrl), wrap(' Alt ', () { - setState(() => FFI.alt = !FFI.alt); - }, FFI.alt), + setState(() => _ffi.alt = !_ffi.alt); + }, _ffi.alt), wrap('Shift', () { - setState(() => FFI.shift = !FFI.shift); - }, FFI.shift), + setState(() => _ffi.shift = !_ffi.shift); + }, _ffi.shift), wrap(isMac ? ' Cmd ' : ' Win ', () { - setState(() => FFI.command = !FFI.command); - }, FFI.command), + setState(() => _ffi.command = !_ffi.command); + }, _ffi.command), ]; final keys = [ wrap( @@ -815,53 +829,53 @@ class _RemotePageState extends State with WindowListener { for (var i = 1; i <= 12; ++i) { final name = 'F' + i.toString(); fn.add(wrap(name, () { - FFI.inputKey('VK_' + name); + _ffi.inputKey('VK_' + name); })); } final more = [ SizedBox(width: 9999), wrap('Esc', () { - FFI.inputKey('VK_ESCAPE'); + _ffi.inputKey('VK_ESCAPE'); }), wrap('Tab', () { - FFI.inputKey('VK_TAB'); + _ffi.inputKey('VK_TAB'); }), wrap('Home', () { - FFI.inputKey('VK_HOME'); + _ffi.inputKey('VK_HOME'); }), wrap('End', () { - FFI.inputKey('VK_END'); + _ffi.inputKey('VK_END'); }), wrap('Del', () { - FFI.inputKey('VK_DELETE'); + _ffi.inputKey('VK_DELETE'); }), wrap('PgUp', () { - FFI.inputKey('VK_PRIOR'); + _ffi.inputKey('VK_PRIOR'); }), wrap('PgDn', () { - FFI.inputKey('VK_NEXT'); + _ffi.inputKey('VK_NEXT'); }), SizedBox(width: 9999), wrap('', () { - FFI.inputKey('VK_LEFT'); + _ffi.inputKey('VK_LEFT'); }, false, Icons.keyboard_arrow_left), wrap('', () { - FFI.inputKey('VK_UP'); + _ffi.inputKey('VK_UP'); }, false, Icons.keyboard_arrow_up), wrap('', () { - FFI.inputKey('VK_DOWN'); + _ffi.inputKey('VK_DOWN'); }, false, Icons.keyboard_arrow_down), wrap('', () { - FFI.inputKey('VK_RIGHT'); + _ffi.inputKey('VK_RIGHT'); }, false, Icons.keyboard_arrow_right), wrap(isMac ? 'Cmd+C' : 'Ctrl+C', () { - sendPrompt(isMac, 'VK_C'); + sendPrompt(widget.id, isMac, 'VK_C'); }), wrap(isMac ? 'Cmd+V' : 'Ctrl+V', () { - sendPrompt(isMac, 'VK_V'); + sendPrompt(widget.id, isMac, 'VK_V'); }), wrap(isMac ? 'Cmd+S' : 'Ctrl+S', () { - sendPrompt(isMac, 'VK_S'); + sendPrompt(widget.id, isMac, 'VK_S'); }), ]; final space = size.width > 320 ? 4.0 : 2.0; @@ -884,11 +898,11 @@ class _RemotePageState extends State with WindowListener { print("window event: $eventName"); switch (eventName) { case 'resize': - FFI.canvasModel.updateViewStyle(); + _ffi.canvasModel.updateViewStyle(); break; case 'maximize': Future.delayed(Duration(milliseconds: 100), () { - FFI.canvasModel.updateViewStyle(); + _ffi.canvasModel.updateViewStyle(); }); break; } @@ -896,11 +910,15 @@ class _RemotePageState extends State with WindowListener { } class ImagePaint extends StatelessWidget { + final String id; + + const ImagePaint({Key? key, required this.id}) : super(key: key); + @override Widget build(BuildContext context) { - final m = Provider.of(context); - final c = Provider.of(context); - final adjust = FFI.cursorModel.adjustForKeyboard(); + final m = ffi(this.id).imageModel; + final c = ffi(this.id).canvasModel; + final adjust = ffi(this.id).cursorModel.adjustForKeyboard(); var s = c.scale; return CustomPaint( painter: new ImagePainter( @@ -910,11 +928,15 @@ class ImagePaint extends StatelessWidget { } class CursorPaint extends StatelessWidget { + final String id; + + const CursorPaint({Key? key, required this.id}) : super(key: key); + @override Widget build(BuildContext context) { - final m = Provider.of(context); - final c = Provider.of(context); - final adjust = FFI.cursorModel.adjustForKeyboard(); + final m = ffi(this.id).cursorModel; + final c = ffi(this.id).canvasModel; + final adjust = ffi(this.id).cursorModel.adjustForKeyboard(); var s = c.scale; return CustomPaint( painter: new ImagePainter( @@ -954,12 +976,12 @@ class ImagePainter extends CustomPainter { CheckboxListTile getToggle( String id, void Function(void Function()) setState, option, name) { - final opt = FFI.bind.getSessionToggleOptionSync(id: id, arg: option); + final opt = ffi(id).bind.getSessionToggleOptionSync(id: id, arg: option); return CheckboxListTile( value: opt, onChanged: (v) { setState(() { - FFI.bind.sessionToggleOption(id: id, value: option); + ffi(id).bind.sessionToggleOption(id: id, value: option); }); }, dense: true, @@ -979,13 +1001,14 @@ RadioListTile getRadio(String name, String toValue, String curValue, } void showOptions(String id) async { - String quality = await FFI.bind.getSessionImageQuality(id: id) ?? 'balanced'; + String quality = + await ffi(id).bind.getSessionImageQuality(id: id) ?? 'balanced'; if (quality == '') quality = 'balanced'; String viewStyle = - await FFI.bind.getSessionOption(id: id, arg: 'view-style') ?? ''; + await ffi(id).bind.getSessionOption(id: id, arg: 'view-style') ?? ''; var displays = []; - final pi = FFI.ffiModel.pi; - final image = FFI.ffiModel.getConnectionImage(); + final pi = ffi(id).ffiModel.pi; + final image = ffi(id).ffiModel.getConnectionImage(); if (image != null) displays.add(Padding(padding: const EdgeInsets.only(top: 8), child: image)); if (pi.displays.length > 1) { @@ -995,7 +1018,7 @@ void showOptions(String id) async { children.add(InkWell( onTap: () { if (i == cur) return; - FFI.bind.sessionSwitchDisplay(id: id, value: i); + ffi(id).bind.sessionSwitchDisplay(id: id, value: i); SmartDialog.dismiss(); }, child: Ink( @@ -1019,7 +1042,7 @@ void showOptions(String id) async { if (displays.isNotEmpty) { displays.add(Divider(color: MyTheme.border)); } - final perms = FFI.ffiModel.permissions; + final perms = ffi(id).ffiModel.permissions; DialogManager.show((setState, close) { final more = []; @@ -1040,15 +1063,17 @@ void showOptions(String id) async { if (value == null) return; setState(() { quality = value; - FFI.bind.sessionSetImageQuality(id: id, value: value); + ffi(id).bind.sessionSetImageQuality(id: id, value: value); }); }; var setViewStyle = (String? value) { if (value == null) return; setState(() { viewStyle = value; - FFI.bind.sessionPeerOption(id: id, name: "view-style", value: value); - FFI.canvasModel.updateViewStyle(); + ffi(id) + .bind + .sessionPeerOption(id: id, name: "view-style", value: value); + ffi(id).canvasModel.updateViewStyle(); }); }; return CustomAlertDialog( @@ -1078,9 +1103,9 @@ void showOptions(String id) async { void showSetOSPassword(String id, bool login) async { final controller = TextEditingController(); var password = - await FFI.bind.getSessionOption(id: id, arg: "os-password") ?? ""; + await ffi(id).bind.getSessionOption(id: id, arg: "os-password") ?? ""; var autoLogin = - await FFI.bind.getSessionOption(id: id, arg: "auto-login") != ""; + await ffi(id).bind.getSessionOption(id: id, arg: "auto-login") != ""; controller.text = password; DialogManager.show((setState, close) { return CustomAlertDialog( @@ -1113,12 +1138,13 @@ void showSetOSPassword(String id, bool login) async { style: flatButtonStyle, onPressed: () { var text = controller.text.trim(); - FFI.bind + ffi(id) + .bind .sessionPeerOption(id: id, name: "os-password", value: text); - FFI.bind.sessionPeerOption( + ffi(id).bind.sessionPeerOption( id: id, name: "auto-login", value: autoLogin ? 'Y' : ''); if (text != "" && login) { - FFI.bind.sessionInputOsPassword(id: id, value: text); + ffi(id).bind.sessionInputOsPassword(id: id, value: text); } close(); }, @@ -1128,18 +1154,19 @@ void showSetOSPassword(String id, bool login) async { }); } -void sendPrompt(bool isMac, String key) { - final old = isMac ? FFI.command : FFI.ctrl; +void sendPrompt(String id, bool isMac, String key) { + FFI _ffi = ffi(id); + final old = isMac ? _ffi.command : _ffi.ctrl; if (isMac) { - FFI.command = true; + _ffi.command = true; } else { - FFI.ctrl = true; + _ffi.ctrl = true; } - FFI.inputKey(key); + _ffi.inputKey(key); if (isMac) { - FFI.command = old; + _ffi.command = old; } else { - FFI.ctrl = old; + _ffi.ctrl = old; } } diff --git a/flutter/lib/desktop/screen/desktop_remote_screen.dart b/flutter/lib/desktop/screen/desktop_remote_screen.dart index d2a9ab952..c5e5ecbfa 100644 --- a/flutter/lib/desktop/screen/desktop_remote_screen.dart +++ b/flutter/lib/desktop/screen/desktop_remote_screen.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/desktop/pages/connection_tab_page.dart'; -import 'package:flutter_hbb/models/model.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:provider/provider.dart'; @@ -15,10 +14,10 @@ class DesktopRemoteScreen extends StatelessWidget { Widget build(BuildContext context) { return MultiProvider( providers: [ - ChangeNotifierProvider.value(value: FFI.ffiModel), - ChangeNotifierProvider.value(value: FFI.imageModel), - ChangeNotifierProvider.value(value: FFI.cursorModel), - ChangeNotifierProvider.value(value: FFI.canvasModel), + ChangeNotifierProvider.value(value: gFFI.ffiModel), + ChangeNotifierProvider.value(value: gFFI.imageModel), + ChangeNotifierProvider.value(value: gFFI.cursorModel), + ChangeNotifierProvider.value(value: gFFI.canvasModel), ], child: MaterialApp( navigatorKey: globalKey, diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 4b71d4a22..2707d9535 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -6,6 +6,7 @@ import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart'; import 'package:flutter_hbb/desktop/screen/desktop_remote_screen.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/route_manager.dart'; import 'package:provider/provider.dart'; import 'package:window_manager/window_manager.dart'; @@ -13,13 +14,15 @@ import 'common.dart'; import 'mobile/pages/home_page.dart'; import 'mobile/pages/server_page.dart'; import 'mobile/pages/settings_page.dart'; -import 'models/model.dart'; int? windowId; Future main(List args) async { WidgetsFlutterBinding.ensureInitialized(); - await FFI.ffiModel.init(); + // global FFI, use this **ONLY** for global configuration + // for convenience, use global FFI on mobile platform + // focus on multi-ffi on desktop first + initGlobalFFI(); // await Firebase.initializeApp(); if (isAndroid) { toAndroidChannelInit(); @@ -54,7 +57,7 @@ void runRustDeskApp(List args) async { } else { // disable tray // initTray(); - FFI.serverModel.startService(); + gFFI.serverModel.startService(); runApp(App()); doWhenWindowReady(() { const initialSize = Size(1280, 720); @@ -72,12 +75,13 @@ class App extends StatelessWidget { // final analytics = FirebaseAnalytics.instance; return MultiProvider( providers: [ - ChangeNotifierProvider.value(value: FFI.ffiModel), - ChangeNotifierProvider.value(value: FFI.imageModel), - ChangeNotifierProvider.value(value: FFI.cursorModel), - ChangeNotifierProvider.value(value: FFI.canvasModel), + // TODO remove it, only for compile + ChangeNotifierProvider.value(value: gFFI.ffiModel), + ChangeNotifierProvider.value(value: gFFI.imageModel), + ChangeNotifierProvider.value(value: gFFI.cursorModel), + ChangeNotifierProvider.value(value: gFFI.canvasModel), ], - child: MaterialApp( + child: GetMaterialApp( navigatorKey: globalKey, debugShowCheckedModeBanner: false, title: 'RustDesk', @@ -88,8 +92,8 @@ class App extends StatelessWidget { home: isDesktop ? DesktopHomePage() : !isAndroid - ? WebHomePage() - : HomePage(), + ? WebHomePage() + : HomePage(), navigatorObservers: [ // FirebaseAnalyticsObserver(analytics: analytics), FlutterSmartDialog.observer diff --git a/flutter/lib/mobile/pages/chat_page.dart b/flutter/lib/mobile/pages/chat_page.dart index a4cf83ab8..c5beda6f1 100644 --- a/flutter/lib/mobile/pages/chat_page.dart +++ b/flutter/lib/mobile/pages/chat_page.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/models/chat_model.dart'; import 'package:provider/provider.dart'; + import '../../models/model.dart'; import 'home_page.dart'; @@ -20,7 +21,7 @@ class ChatPage extends StatelessWidget implements PageShape { PopupMenuButton( icon: Icon(Icons.group), itemBuilder: (context) { - final chatModel = FFI.chatModel; + final chatModel = gFFI.chatModel; return chatModel.messages.entries.map((entry) { final id = entry.key; final user = entry.value.chatUser; @@ -31,14 +32,14 @@ class ChatPage extends StatelessWidget implements PageShape { }).toList(); }, onSelected: (id) { - FFI.chatModel.changeCurrentID(id); + gFFI.chatModel.changeCurrentID(id); }) ]; @override Widget build(BuildContext context) { return ChangeNotifierProvider.value( - value: FFI.chatModel, + value: gFFI.chatModel, child: Container( color: MyTheme.grayBg, child: Consumer(builder: (context, chatModel, child) { diff --git a/flutter/lib/mobile/pages/connection_page.dart b/flutter/lib/mobile/pages/connection_page.dart index 113c41676..68841de89 100644 --- a/flutter/lib/mobile/pages/connection_page.dart +++ b/flutter/lib/mobile/pages/connection_page.dart @@ -1,14 +1,16 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_hbb/mobile/pages/file_manager_page.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; -import 'dart:async'; + import '../../common.dart'; import '../../models/model.dart'; import 'home_page.dart'; import 'remote_page.dart'; -import 'settings_page.dart'; import 'scan_page.dart'; +import 'settings_page.dart'; /// Connection page for connecting to a remote peer. class ConnectionPage extends StatefulWidget implements PageShape { @@ -41,7 +43,7 @@ class _ConnectionPageState extends State { super.initState(); if (isAndroid) { Timer(Duration(seconds: 5), () { - _updateUrl = FFI.getByName('software_update_url'); + _updateUrl = gFFI.getByName('software_update_url'); if (_updateUrl.isNotEmpty) setState(() {}); }); } @@ -50,7 +52,7 @@ class _ConnectionPageState extends State { @override Widget build(BuildContext context) { Provider.of(context); - if (_idController.text.isEmpty) _idController.text = FFI.getId(); + if (_idController.text.isEmpty) _idController.text = gFFI.getId(); return SingleChildScrollView( child: Column( mainAxisAlignment: MainAxisAlignment.start, @@ -220,7 +222,7 @@ class _ConnectionPageState extends State { width = size.width / n - 2 * space; } final cards = []; - var peers = FFI.peers(); + var peers = gFFI.peers(); peers.forEach((p) { cards.add(Container( width: width, @@ -278,7 +280,7 @@ class _ConnectionPageState extends State { elevation: 8, ); if (value == 'remove') { - setState(() => FFI.setByName('remove', '$id')); + setState(() => gFFI.setByName('remove', '$id')); () async { removePreference(id); }(); diff --git a/flutter/lib/mobile/pages/file_manager_page.dart b/flutter/lib/mobile/pages/file_manager_page.dart index 0370bedff..1f588a461 100644 --- a/flutter/lib/mobile/pages/file_manager_page.dart +++ b/flutter/lib/mobile/pages/file_manager_page.dart @@ -1,11 +1,12 @@ import 'dart:async'; + import 'package:flutter/material.dart'; +import 'package:flutter_breadcrumb/flutter_breadcrumb.dart'; import 'package:flutter_hbb/models/file_model.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:provider/provider.dart'; -import 'package:flutter_breadcrumb/flutter_breadcrumb.dart'; -import 'package:wakelock/wakelock.dart'; import 'package:toggle_switch/toggle_switch.dart'; +import 'package:wakelock/wakelock.dart'; import '../../common.dart'; import '../../models/model.dart'; @@ -20,22 +21,22 @@ class FileManagerPage extends StatefulWidget { } class _FileManagerPageState extends State { - final model = FFI.fileModel; + final model = gFFI.fileModel; final _selectedItems = SelectedItems(); final _breadCrumbScroller = ScrollController(); @override void initState() { super.initState(); - FFI.connect(widget.id, isFileTransfer: true); - FFI.ffiModel.updateEventListener(widget.id); + gFFI.connect(widget.id, isFileTransfer: true); + gFFI.ffiModel.updateEventListener(widget.id); Wakelock.enable(); } @override void dispose() { model.onClose(); - FFI.close(); + gFFI.close(); SmartDialog.dismiss(); Wakelock.disable(); super.dispose(); @@ -43,7 +44,7 @@ class _FileManagerPageState extends State { @override Widget build(BuildContext context) => ChangeNotifierProvider.value( - value: FFI.fileModel, + value: gFFI.fileModel, child: Consumer(builder: (_context, _model, _child) { return WillPopScope( onWillPop: () async { diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 6f10b234d..fcc5fcde8 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -46,7 +46,7 @@ class _RemotePageState extends State { @override void initState() { super.initState(); - FFI.connect(widget.id); + gFFI.connect(widget.id); WidgetsBinding.instance.addPostFrameCallback((_) { SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); showLoading(translate('Connecting...')); @@ -55,18 +55,18 @@ class _RemotePageState extends State { }); Wakelock.enable(); _physicalFocusNode.requestFocus(); - FFI.ffiModel.updateEventListener(widget.id); - FFI.listenToMouse(true); + gFFI.ffiModel.updateEventListener(widget.id); + gFFI.listenToMouse(true); } @override void dispose() { hideMobileActionsOverlay(); - FFI.listenToMouse(false); - FFI.invokeMethod("enable_soft_keyboard", true); + gFFI.listenToMouse(false); + gFFI.invokeMethod("enable_soft_keyboard", true); _mobileFocusNode.dispose(); _physicalFocusNode.dispose(); - FFI.close(); + gFFI.close(); _interval?.cancel(); _timer?.cancel(); SmartDialog.dismiss(); @@ -77,7 +77,7 @@ class _RemotePageState extends State { } void resetTool() { - FFI.resetModifiers(); + gFFI.resetModifiers(); } bool isKeyboardShown() { @@ -96,8 +96,8 @@ class _RemotePageState extends State { overlays: []); // [pi.version.isNotEmpty] -> check ready or not,avoid login without soft-keyboard if (chatWindowOverlayEntry == null && - FFI.ffiModel.pi.version.isNotEmpty) { - FFI.invokeMethod("enable_soft_keyboard", false); + gFFI.ffiModel.pi.version.isNotEmpty) { + gFFI.invokeMethod("enable_soft_keyboard", false); } } }); @@ -129,12 +129,12 @@ class _RemotePageState extends State { newValue[common] == oldValue[common]; ++common); for (i = 0; i < oldValue.length - common; ++i) { - FFI.inputKey('VK_BACK'); + gFFI.inputKey('VK_BACK'); } if (newValue.length > common) { var s = newValue.substring(common); if (s.length > 1) { - FFI.setByName('input_string', s); + gFFI.setByName('input_string', s); } else { inputChar(s); } @@ -152,7 +152,7 @@ class _RemotePageState extends State { // ? } else if (newValue.length < oldValue.length) { final char = 'VK_BACK'; - FFI.inputKey(char); + gFFI.inputKey(char); } else { final content = newValue.substring(oldValue.length); if (content.length > 1) { @@ -168,11 +168,11 @@ class _RemotePageState extends State { content == '()' || content == '【】')) { // can not only input content[0], because when input ], [ are also auo insert, which cause ] never be input - FFI.setByName('input_string', content); + gFFI.setByName('input_string', content); openKeyboard(); return; } - FFI.setByName('input_string', content); + gFFI.setByName('input_string', content); } else { inputChar(content); } @@ -185,11 +185,11 @@ class _RemotePageState extends State { } else if (char == ' ') { char = 'VK_SPACE'; } - FFI.inputKey(char); + gFFI.inputKey(char); } void openKeyboard() { - FFI.invokeMethod("enable_soft_keyboard", true); + gFFI.invokeMethod("enable_soft_keyboard", true); // destroy first, so that our _value trick can work _value = initText; setState(() => _showEdit = false); @@ -212,7 +212,7 @@ class _RemotePageState extends State { final label = _logicalKeyMap[e.logicalKey.keyId] ?? _physicalKeyMap[e.physicalKey.usbHidUsage] ?? e.logicalKey.keyLabel; - FFI.inputKey(label, down: down, press: press ?? false); + gFFI.inputKey(label, down: down, press: press ?? false); } @override @@ -220,7 +220,7 @@ class _RemotePageState extends State { final pi = Provider.of(context).pi; final hideKeyboard = isKeyboardShown() && _showEdit; final showActionButton = !_showBar || hideKeyboard; - final keyboard = FFI.ffiModel.permissions['keyboard'] != false; + final keyboard = gFFI.ffiModel.permissions['keyboard'] != false; return WillPopScope( onWillPop: () async { @@ -230,7 +230,7 @@ class _RemotePageState extends State { child: getRawPointerAndKeyBody( keyboard, Scaffold( - // resizeToAvoidBottomInset: true, + // resizeToAvoidBottomInset: true, floatingActionButton: !showActionButton ? null : FloatingActionButton( @@ -241,14 +241,14 @@ class _RemotePageState extends State { onPressed: () { setState(() { if (hideKeyboard) { - _showEdit = false; - FFI.invokeMethod("enable_soft_keyboard", false); - _mobileFocusNode.unfocus(); - _physicalFocusNode.requestFocus(); - } else { - _showBar = !_showBar; - } - }); + _showEdit = false; + gFFI.invokeMethod("enable_soft_keyboard", false); + _mobileFocusNode.unfocus(); + _physicalFocusNode.requestFocus(); + } else { + _showBar = !_showBar; + } + }); }), bottomNavigationBar: _showBar && pi.displays.length > 0 ? getBottomAppBar(keyboard) @@ -282,7 +282,7 @@ class _RemotePageState extends State { }); } if (_isPhysicalMouse) { - FFI.handleMouse(getEvent(e, 'mousemove')); + gFFI.handleMouse(getEvent(e, 'mousemove')); } }, onPointerDown: (e) { @@ -294,19 +294,19 @@ class _RemotePageState extends State { } } if (_isPhysicalMouse) { - FFI.handleMouse(getEvent(e, 'mousedown')); + gFFI.handleMouse(getEvent(e, 'mousedown')); } }, onPointerUp: (e) { if (e.kind != ui.PointerDeviceKind.mouse) return; if (_isPhysicalMouse) { - FFI.handleMouse(getEvent(e, 'mouseup')); + gFFI.handleMouse(getEvent(e, 'mouseup')); } }, onPointerMove: (e) { if (e.kind != ui.PointerDeviceKind.mouse) return; if (_isPhysicalMouse) { - FFI.handleMouse(getEvent(e, 'mousemove')); + gFFI.handleMouse(getEvent(e, 'mousemove')); } }, onPointerSignal: (e) { @@ -319,7 +319,7 @@ class _RemotePageState extends State { if (dy > 0) dy = -1; else if (dy < 0) dy = 1; - FFI.setByName( + gFFI.setByName( 'send_mouse', '{"type": "wheel", "x": "$dx", "y": "$dy"}'); } }, @@ -337,14 +337,14 @@ class _RemotePageState extends State { if (e.repeat) { sendRawKey(e, press: true); } else { - if (e.isAltPressed && !FFI.alt) { - FFI.alt = true; - } else if (e.isControlPressed && !FFI.ctrl) { - FFI.ctrl = true; - } else if (e.isShiftPressed && !FFI.shift) { - FFI.shift = true; - } else if (e.isMetaPressed && !FFI.command) { - FFI.command = true; + if (e.isAltPressed && !gFFI.alt) { + gFFI.alt = true; + } else if (e.isControlPressed && !gFFI.ctrl) { + gFFI.ctrl = true; + } else if (e.isShiftPressed && !gFFI.shift) { + gFFI.shift = true; + } else if (e.isMetaPressed && !gFFI.command) { + gFFI.command = true; } sendRawKey(e, down: true); } @@ -353,16 +353,16 @@ class _RemotePageState extends State { if (!_showEdit && e is RawKeyUpEvent) { if (key == LogicalKeyboardKey.altLeft || key == LogicalKeyboardKey.altRight) { - FFI.alt = false; + gFFI.alt = false; } else if (key == LogicalKeyboardKey.controlLeft || key == LogicalKeyboardKey.controlRight) { - FFI.ctrl = false; + gFFI.ctrl = false; } else if (key == LogicalKeyboardKey.shiftRight || key == LogicalKeyboardKey.shiftLeft) { - FFI.shift = false; + gFFI.shift = false; } else if (key == LogicalKeyboardKey.metaLeft || key == LogicalKeyboardKey.metaRight) { - FFI.command = false; + gFFI.command = false; } sendRawKey(e); } @@ -401,32 +401,32 @@ class _RemotePageState extends State { ] + (isWebDesktop ? [] - : FFI.ffiModel.isPeerAndroid - ? [ - IconButton( - color: Colors.white, - icon: Icon(Icons.build), - onPressed: () { - if (mobileActionsOverlayEntry == null) { - showMobileActionsOverlay(); - } else { - hideMobileActionsOverlay(); - } - }, - ) - ] - : [ - IconButton( - color: Colors.white, - icon: Icon(Icons.keyboard), - onPressed: openKeyboard), - IconButton( - color: Colors.white, - icon: Icon(FFI.ffiModel.touchMode - ? Icons.touch_app - : Icons.mouse), - onPressed: changeTouchMode, - ), + : gFFI.ffiModel.isPeerAndroid + ? [ + IconButton( + color: Colors.white, + icon: Icon(Icons.build), + onPressed: () { + if (mobileActionsOverlayEntry == null) { + showMobileActionsOverlay(); + } else { + hideMobileActionsOverlay(); + } + }, + ) + ] + : [ + IconButton( + color: Colors.white, + icon: Icon(Icons.keyboard), + onPressed: openKeyboard), + IconButton( + color: Colors.white, + icon: Icon(gFFI.ffiModel.touchMode + ? Icons.touch_app + : Icons.mouse), + onPressed: changeTouchMode, + ), ]) + (isWeb ? [] @@ -435,10 +435,10 @@ class _RemotePageState extends State { color: Colors.white, icon: Icon(Icons.message), onPressed: () { - FFI.chatModel - .changeCurrentID(ChatModel.clientModeID); - toggleChatOverlay(); - }, + gFFI.chatModel + .changeCurrentID(ChatModel.clientModeID); + toggleChatOverlay(); + }, ) ]) + [ @@ -473,91 +473,91 @@ class _RemotePageState extends State { /// HoldDrag -> left drag Widget getBodyForMobileWithGesture() { - final touchMode = FFI.ffiModel.touchMode; + final touchMode = gFFI.ffiModel.touchMode; return getMixinGestureDetector( child: getBodyForMobile(), onTapUp: (d) { if (touchMode) { - FFI.cursorModel.touch( + gFFI.cursorModel.touch( d.localPosition.dx, d.localPosition.dy, MouseButtons.left); } else { - FFI.tap(MouseButtons.left); + gFFI.tap(MouseButtons.left); } }, onDoubleTapDown: (d) { if (touchMode) { - FFI.cursorModel.move(d.localPosition.dx, d.localPosition.dy); + gFFI.cursorModel.move(d.localPosition.dx, d.localPosition.dy); } }, onDoubleTap: () { - FFI.tap(MouseButtons.left); - FFI.tap(MouseButtons.left); + gFFI.tap(MouseButtons.left); + gFFI.tap(MouseButtons.left); }, onLongPressDown: (d) { if (touchMode) { - FFI.cursorModel.move(d.localPosition.dx, d.localPosition.dy); + gFFI.cursorModel.move(d.localPosition.dx, d.localPosition.dy); } }, onLongPress: () { - FFI.tap(MouseButtons.right); + gFFI.tap(MouseButtons.right); }, onDoubleFinerTap: (d) { if (!touchMode) { - FFI.tap(MouseButtons.right); + gFFI.tap(MouseButtons.right); } }, onHoldDragStart: (d) { if (!touchMode) { - FFI.sendMouse('down', MouseButtons.left); + gFFI.sendMouse('down', MouseButtons.left); } }, onHoldDragUpdate: (d) { if (!touchMode) { - FFI.cursorModel.updatePan(d.delta.dx, d.delta.dy, touchMode); + gFFI.cursorModel.updatePan(d.delta.dx, d.delta.dy, touchMode); } }, onHoldDragEnd: (_) { if (!touchMode) { - FFI.sendMouse('up', MouseButtons.left); + gFFI.sendMouse('up', MouseButtons.left); } }, onOneFingerPanStart: (d) { if (touchMode) { - FFI.cursorModel.move(d.localPosition.dx, d.localPosition.dy); - FFI.sendMouse('down', MouseButtons.left); + gFFI.cursorModel.move(d.localPosition.dx, d.localPosition.dy); + gFFI.sendMouse('down', MouseButtons.left); } }, onOneFingerPanUpdate: (d) { - FFI.cursorModel.updatePan(d.delta.dx, d.delta.dy, touchMode); + gFFI.cursorModel.updatePan(d.delta.dx, d.delta.dy, touchMode); }, onOneFingerPanEnd: (d) { if (touchMode) { - FFI.sendMouse('up', MouseButtons.left); + gFFI.sendMouse('up', MouseButtons.left); } }, // scale + pan event onTwoFingerScaleUpdate: (d) { - FFI.canvasModel.updateScale(d.scale / _scale); + gFFI.canvasModel.updateScale(d.scale / _scale); _scale = d.scale; - FFI.canvasModel.panX(d.focalPointDelta.dx); - FFI.canvasModel.panY(d.focalPointDelta.dy); + gFFI.canvasModel.panX(d.focalPointDelta.dx); + gFFI.canvasModel.panY(d.focalPointDelta.dy); }, onTwoFingerScaleEnd: (d) { _scale = 1; - FFI.setByName('peer_option', '{"name": "view-style", "value": ""}'); + gFFI.setByName('peer_option', '{"name": "view-style", "value": ""}'); }, - onThreeFingerVerticalDragUpdate: FFI.ffiModel.isPeerAndroid + onThreeFingerVerticalDragUpdate: gFFI.ffiModel.isPeerAndroid ? null : (d) { - _mouseScrollIntegral += d.delta.dy / 4; - if (_mouseScrollIntegral > 1) { - FFI.scroll(1); - _mouseScrollIntegral = 0; - } else if (_mouseScrollIntegral < -1) { - FFI.scroll(-1); - _mouseScrollIntegral = 0; - } - }); + _mouseScrollIntegral += d.delta.dy / 4; + if (_mouseScrollIntegral > 1) { + gFFI.scroll(1); + _mouseScrollIntegral = 0; + } else if (_mouseScrollIntegral < -1) { + gFFI.scroll(-1); + _mouseScrollIntegral = 0; + } + }); } Widget getBodyForMobile() { @@ -591,7 +591,7 @@ class _RemotePageState extends State { Widget getBodyForDesktopWithListener(bool keyboard) { var paints = [ImagePaint()]; if (keyboard || - FFI.getByName('toggle_option', 'show-remote-cursor') == 'true') { + gFFI.getByName('toggle_option', 'show-remote-cursor') == 'true') { paints.add(CursorPaint()); } return Container( @@ -605,10 +605,10 @@ class _RemotePageState extends State { out['type'] = type; out['x'] = evt.position.dx; out['y'] = evt.position.dy; - if (FFI.alt) out['alt'] = 'true'; - if (FFI.shift) out['shift'] = 'true'; - if (FFI.ctrl) out['ctrl'] = 'true'; - if (FFI.command) out['command'] = 'true'; + if (gFFI.alt) out['alt'] = 'true'; + if (gFFI.shift) out['shift'] = 'true'; + if (gFFI.ctrl) out['ctrl'] = 'true'; + if (gFFI.command) out['command'] = 'true'; out['buttons'] = evt .buttons; // left button: 1, right button: 2, middle button: 4, 1 | 2 = 3 (left + right) if (evt.buttons != 0) { @@ -624,8 +624,8 @@ class _RemotePageState extends State { final x = 120.0; final y = size.height; final more = >[]; - final pi = FFI.ffiModel.pi; - final perms = FFI.ffiModel.permissions; + final pi = gFFI.ffiModel.pi; + final perms = gFFI.ffiModel.permissions; if (pi.version.isNotEmpty) { more.add(PopupMenuItem( child: Text(translate('Refresh')), value: 'refresh')); @@ -633,16 +633,16 @@ class _RemotePageState extends State { more.add(PopupMenuItem( child: Row( children: ([ - Container(width: 100.0, child: Text(translate('OS Password'))), - TextButton( - style: flatButtonStyle, - onPressed: () { - Navigator.pop(context); - showSetOSPassword(false); - }, - child: Icon(Icons.edit, color: MyTheme.accent), - ) - ])), + Container(width: 100.0, child: Text(translate('OS Password'))), + TextButton( + style: flatButtonStyle, + onPressed: () { + Navigator.pop(context); + showSetOSPassword(false); + }, + child: Icon(Icons.edit, color: MyTheme.accent), + ) + ])), value: 'enter_os_password')); if (!isWebDesktop) { if (perms['keyboard'] != false && perms['clipboard'] != false) { @@ -661,10 +661,10 @@ class _RemotePageState extends State { more.add(PopupMenuItem( child: Text(translate('Insert Lock')), value: 'lock')); if (pi.platform == 'Windows' && - FFI.getByName('toggle_option', 'privacy-mode') != 'true') { + gFFI.getByName('toggle_option', 'privacy-mode') != 'true') { more.add(PopupMenuItem( - child: Text(translate( - (FFI.ffiModel.inputBlocked ? 'Unb' : 'B') + 'lock user input')), + child: Text(translate((gFFI.ffiModel.inputBlocked ? 'Unb' : 'B') + + 'lock user input')), value: 'block-input')); } } @@ -676,31 +676,31 @@ class _RemotePageState extends State { elevation: 8, ); if (value == 'cad') { - FFI.setByName('ctrl_alt_del'); + gFFI.setByName('ctrl_alt_del'); } else if (value == 'lock') { - FFI.setByName('lock_screen'); + gFFI.setByName('lock_screen'); } else if (value == 'block-input') { - FFI.setByName('toggle_option', - (FFI.ffiModel.inputBlocked ? 'un' : '') + 'block-input'); - FFI.ffiModel.inputBlocked = !FFI.ffiModel.inputBlocked; + gFFI.setByName('toggle_option', + (gFFI.ffiModel.inputBlocked ? 'un' : '') + 'block-input'); + gFFI.ffiModel.inputBlocked = !gFFI.ffiModel.inputBlocked; } else if (value == 'refresh') { - FFI.setByName('refresh'); + gFFI.setByName('refresh'); } else if (value == 'paste') { () async { ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain); if (data != null && data.text != null) { - FFI.setByName('input_string', '${data.text}'); + gFFI.setByName('input_string', '${data.text}'); } }(); } else if (value == 'enter_os_password') { - var password = FFI.getByName('peer_option', "os-password"); + var password = gFFI.getByName('peer_option', "os-password"); if (password != "") { - FFI.setByName('input_os_password', password); + gFFI.setByName('input_os_password', password); } else { showSetOSPassword(true); } } else if (value == 'reset_canvas') { - FFI.cursorModel.reset(); + gFFI.cursorModel.reset(); } }(); } @@ -719,11 +719,11 @@ class _RemotePageState extends State { return SingleChildScrollView( padding: EdgeInsets.symmetric(vertical: 10), child: GestureHelp( - touchMode: FFI.ffiModel.touchMode, + touchMode: gFFI.ffiModel.touchMode, onTouchModeChange: (t) { - FFI.ffiModel.toggleTouchMode(); - final v = FFI.ffiModel.touchMode ? 'Y' : ''; - FFI.setByName('peer_option', + gFFI.ffiModel.toggleTouchMode(); + final v = gFFI.ffiModel.touchMode ? 'Y' : ''; + gFFI.setByName('peer_option', '{"name": "touch-mode", "value": "$v"}'); })); })); @@ -752,24 +752,24 @@ class _RemotePageState extends State { child: icon != null ? Icon(icon, size: 17, color: Colors.white) : Text(translate(text), - style: TextStyle(color: Colors.white, fontSize: 11)), + style: TextStyle(color: Colors.white, fontSize: 11)), onPressed: onPressed); }; - final pi = FFI.ffiModel.pi; + final pi = gFFI.ffiModel.pi; final isMac = pi.platform == "Mac OS"; final modifiers = [ wrap('Ctrl ', () { - setState(() => FFI.ctrl = !FFI.ctrl); - }, FFI.ctrl), + setState(() => gFFI.ctrl = !gFFI.ctrl); + }, gFFI.ctrl), wrap(' Alt ', () { - setState(() => FFI.alt = !FFI.alt); - }, FFI.alt), + setState(() => gFFI.alt = !gFFI.alt); + }, gFFI.alt), wrap('Shift', () { - setState(() => FFI.shift = !FFI.shift); - }, FFI.shift), + setState(() => gFFI.shift = !gFFI.shift); + }, gFFI.shift), wrap(isMac ? ' Cmd ' : ' Win ', () { - setState(() => FFI.command = !FFI.command); - }, FFI.command), + setState(() => gFFI.command = !gFFI.command); + }, gFFI.command), ]; final keys = [ wrap( @@ -801,44 +801,44 @@ class _RemotePageState extends State { for (var i = 1; i <= 12; ++i) { final name = 'F' + i.toString(); fn.add(wrap(name, () { - FFI.inputKey('VK_' + name); + gFFI.inputKey('VK_' + name); })); } final more = [ SizedBox(width: 9999), wrap('Esc', () { - FFI.inputKey('VK_ESCAPE'); + gFFI.inputKey('VK_ESCAPE'); }), wrap('Tab', () { - FFI.inputKey('VK_TAB'); + gFFI.inputKey('VK_TAB'); }), wrap('Home', () { - FFI.inputKey('VK_HOME'); + gFFI.inputKey('VK_HOME'); }), wrap('End', () { - FFI.inputKey('VK_END'); + gFFI.inputKey('VK_END'); }), wrap('Del', () { - FFI.inputKey('VK_DELETE'); + gFFI.inputKey('VK_DELETE'); }), wrap('PgUp', () { - FFI.inputKey('VK_PRIOR'); + gFFI.inputKey('VK_PRIOR'); }), wrap('PgDn', () { - FFI.inputKey('VK_NEXT'); + gFFI.inputKey('VK_NEXT'); }), SizedBox(width: 9999), wrap('', () { - FFI.inputKey('VK_LEFT'); + gFFI.inputKey('VK_LEFT'); }, false, Icons.keyboard_arrow_left), wrap('', () { - FFI.inputKey('VK_UP'); + gFFI.inputKey('VK_UP'); }, false, Icons.keyboard_arrow_up), wrap('', () { - FFI.inputKey('VK_DOWN'); + gFFI.inputKey('VK_DOWN'); }, false, Icons.keyboard_arrow_down), wrap('', () { - FFI.inputKey('VK_RIGHT'); + gFFI.inputKey('VK_RIGHT'); }, false, Icons.keyboard_arrow_right), wrap(isMac ? 'Cmd+C' : 'Ctrl+C', () { sendPrompt(isMac, 'VK_C'); @@ -871,7 +871,7 @@ class ImagePaint extends StatelessWidget { Widget build(BuildContext context) { final m = Provider.of(context); final c = Provider.of(context); - final adjust = FFI.cursorModel.adjustForKeyboard(); + final adjust = gFFI.cursorModel.adjustForKeyboard(); var s = c.scale; return CustomPaint( painter: new ImagePainter( @@ -885,7 +885,7 @@ class CursorPaint extends StatelessWidget { Widget build(BuildContext context) { final m = Provider.of(context); final c = Provider.of(context); - final adjust = FFI.cursorModel.adjustForKeyboard(); + final adjust = gFFI.cursorModel.adjustForKeyboard(); var s = c.scale; return CustomPaint( painter: new ImagePainter( @@ -925,10 +925,10 @@ class ImagePainter extends CustomPainter { CheckboxListTile getToggle(void Function(void Function()) setState, option, name) { return CheckboxListTile( - value: FFI.getByName('toggle_option', option) == 'true', + value: gFFI.getByName('toggle_option', option) == 'true', onChanged: (v) { setState(() { - FFI.setByName('toggle_option', option); + gFFI.setByName('toggle_option', option); }); }, dense: true, @@ -948,12 +948,12 @@ RadioListTile getRadio(String name, String toValue, String curValue, } void showOptions() { - String quality = FFI.getByName('image_quality'); + String quality = gFFI.getByName('image_quality'); if (quality == '') quality = 'balanced'; - String viewStyle = FFI.getByName('peer_option', 'view-style'); + String viewStyle = gFFI.getByName('peer_option', 'view-style'); var displays = []; - final pi = FFI.ffiModel.pi; - final image = FFI.ffiModel.getConnectionImage(); + final pi = gFFI.ffiModel.pi; + final image = gFFI.ffiModel.getConnectionImage(); if (image != null) displays.add(Padding(padding: const EdgeInsets.only(top: 8), child: image)); if (pi.displays.length > 1) { @@ -963,7 +963,7 @@ void showOptions() { children.add(InkWell( onTap: () { if (i == cur) return; - FFI.setByName('switch_display', i.toString()); + gFFI.setByName('switch_display', i.toString()); SmartDialog.dismiss(); }, child: Ink( @@ -987,7 +987,7 @@ void showOptions() { if (displays.isNotEmpty) { displays.add(Divider(color: MyTheme.border)); } - final perms = FFI.ffiModel.permissions; + final perms = gFFI.ffiModel.permissions; DialogManager.show((setState, close) { final more = []; @@ -1007,16 +1007,16 @@ void showOptions() { if (value == null) return; setState(() { quality = value; - FFI.setByName('image_quality', value); + gFFI.setByName('image_quality', value); }); }; var setViewStyle = (String? value) { if (value == null) return; setState(() { viewStyle = value; - FFI.setByName( + gFFI.setByName( 'peer_option', '{"name": "view-style", "value": "$value"}'); - FFI.canvasModel.updateViewStyle(); + gFFI.canvasModel.updateViewStyle(); }); }; return CustomAlertDialog( @@ -1044,8 +1044,8 @@ void showOptions() { void showSetOSPassword(bool login) { final controller = TextEditingController(); - var password = FFI.getByName('peer_option', "os-password"); - var autoLogin = FFI.getByName('peer_option', "auto-login") != ""; + var password = gFFI.getByName('peer_option', "os-password"); + var autoLogin = gFFI.getByName('peer_option', "auto-login") != ""; controller.text = password; DialogManager.show((setState, close) { return CustomAlertDialog( @@ -1078,12 +1078,12 @@ void showSetOSPassword(bool login) { style: flatButtonStyle, onPressed: () { var text = controller.text.trim(); - FFI.setByName( + gFFI.setByName( 'peer_option', '{"name": "os-password", "value": "$text"}'); - FFI.setByName('peer_option', + gFFI.setByName('peer_option', '{"name": "auto-login", "value": "${autoLogin ? 'Y' : ''}"}'); if (text != "" && login) { - FFI.setByName('input_os_password', text); + gFFI.setByName('input_os_password', text); } close(); }, @@ -1094,17 +1094,17 @@ void showSetOSPassword(bool login) { } void sendPrompt(bool isMac, String key) { - final old = isMac ? FFI.command : FFI.ctrl; + final old = isMac ? gFFI.command : gFFI.ctrl; if (isMac) { - FFI.command = true; + gFFI.command = true; } else { - FFI.ctrl = true; + gFFI.ctrl = true; } - FFI.inputKey(key); + gFFI.inputKey(key); if (isMac) { - FFI.command = old; + gFFI.command = old; } else { - FFI.ctrl = old; + gFFI.ctrl = old; } } diff --git a/flutter/lib/mobile/pages/scan_page.dart b/flutter/lib/mobile/pages/scan_page.dart index a7d01f0b8..2f5a9d991 100644 --- a/flutter/lib/mobile/pages/scan_page.dart +++ b/flutter/lib/mobile/pages/scan_page.dart @@ -1,11 +1,13 @@ -import 'package:flutter/material.dart'; -import 'package:qr_code_scanner/qr_code_scanner.dart'; -import 'package:image_picker/image_picker.dart'; -import 'package:image/image.dart' as img; -import 'package:zxing2/qrcode.dart'; -import 'dart:io'; import 'dart:async'; import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:image/image.dart' as img; +import 'package:image_picker/image_picker.dart'; +import 'package:qr_code_scanner/qr_code_scanner.dart'; +import 'package:zxing2/qrcode.dart'; + import '../../common.dart'; import '../../models/model.dart'; @@ -153,10 +155,10 @@ class _ScanPageState extends State { void showServerSettingsWithValue( String id, String relay, String key, String api) { final formKey = GlobalKey(); - final id0 = FFI.getByName('option', 'custom-rendezvous-server'); - final relay0 = FFI.getByName('option', 'relay-server'); - final api0 = FFI.getByName('option', 'api-server'); - final key0 = FFI.getByName('option', 'key'); + final id0 = gFFI.getByName('option', 'custom-rendezvous-server'); + final relay0 = gFFI.getByName('option', 'relay-server'); + final api0 = gFFI.getByName('option', 'api-server'); + final key0 = gFFI.getByName('option', 'key'); DialogManager.show((setState, close) { return CustomAlertDialog( title: Text(translate('ID/Relay Server')), @@ -227,17 +229,17 @@ void showServerSettingsWithValue( formKey.currentState!.validate()) { formKey.currentState!.save(); if (id != id0) - FFI.setByName('option', + gFFI.setByName('option', '{"name": "custom-rendezvous-server", "value": "$id"}'); if (relay != relay0) - FFI.setByName( + gFFI.setByName( 'option', '{"name": "relay-server", "value": "$relay"}'); if (key != key0) - FFI.setByName('option', '{"name": "key", "value": "$key"}'); + gFFI.setByName('option', '{"name": "key", "value": "$key"}'); if (api != api0) - FFI.setByName( + gFFI.setByName( 'option', '{"name": "api-server", "value": "$api"}'); - FFI.ffiModel.updateUser(); + gFFI.ffiModel.updateUser(); close(); } }, @@ -253,6 +255,6 @@ String? validate(value) { if (value.isEmpty) { return null; } - final res = FFI.getByName('test_if_valid_server', value); + final res = gFFI.getByName('test_if_valid_server', value); return res.isEmpty ? null : res; } diff --git a/flutter/lib/mobile/pages/server_page.dart b/flutter/lib/mobile/pages/server_page.dart index 9caa327ea..3b0332fa7 100644 --- a/flutter/lib/mobile/pages/server_page.dart +++ b/flutter/lib/mobile/pages/server_page.dart @@ -1,13 +1,13 @@ import 'package:flutter/material.dart'; -import 'package:flutter_hbb/models/model.dart'; import 'package:flutter_hbb/mobile/widgets/dialog.dart'; +import 'package:flutter_hbb/models/model.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:provider/provider.dart'; import '../../common.dart'; +import '../../models/model.dart'; import '../../models/server_model.dart'; import 'home_page.dart'; -import '../../models/model.dart'; class ServerPage extends StatelessWidget implements PageShape { @override @@ -30,12 +30,12 @@ class ServerPage extends StatelessWidget implements PageShape { PopupMenuItem( child: Text(translate("Set your own password")), value: "changePW", - enabled: FFI.serverModel.isStart, + enabled: gFFI.serverModel.isStart, ), PopupMenuItem( child: Text(translate("Refresh random password")), value: "refreshPW", - enabled: FFI.serverModel.isStart, + enabled: gFFI.serverModel.isStart, ) ]; }, @@ -47,7 +47,7 @@ class ServerPage extends StatelessWidget implements PageShape { } else if (value == "refreshPW") { () async { showLoading(translate("Waiting")); - if (await FFI.serverModel.updatePassword("")) { + if (await gFFI.serverModel.updatePassword("")) { showSuccess(); } else { showError(); @@ -62,10 +62,10 @@ class ServerPage extends StatelessWidget implements PageShape { Widget build(BuildContext context) { checkService(); return ChangeNotifierProvider.value( - value: FFI.serverModel, + value: gFFI.serverModel, child: Consumer( builder: (context, serverModel, child) => SingleChildScrollView( - controller: FFI.serverModel.controller, + controller: gFFI.serverModel.controller, child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.start, @@ -82,9 +82,9 @@ class ServerPage extends StatelessWidget implements PageShape { } void checkService() async { - FFI.invokeMethod("check_service"); // jvm + gFFI.invokeMethod("check_service"); // jvm // for Android 10/11,MANAGE_EXTERNAL_STORAGE permission from a system setting page - if (PermissionManager.isWaitingFile() && !FFI.serverModel.fileOk) { + if (PermissionManager.isWaitingFile() && !gFFI.serverModel.fileOk) { PermissionManager.complete("file", await PermissionManager.check("file")); debugPrint("file permission finished"); } @@ -96,7 +96,7 @@ class ServerInfo extends StatefulWidget { } class _ServerInfoState extends State { - final model = FFI.serverModel; + final model = gFFI.serverModel; var _passwdShow = false; @override @@ -327,7 +327,7 @@ class ConnectionManager extends StatelessWidget { ? SizedBox.shrink() : IconButton( onPressed: () { - FFI.chatModel + gFFI.chatModel .changeCurrentID(entry.value.id); final bar = navigationBarKey.currentWidget; @@ -355,8 +355,9 @@ class ConnectionManager extends StatelessWidget { MaterialStateProperty.all(Colors.red)), icon: Icon(Icons.close), onPressed: () { - FFI.setByName("close_conn", entry.key.toString()); - FFI.invokeMethod( + gFFI.setByName( + "close_conn", entry.key.toString()); + gFFI.invokeMethod( "cancel_notification", entry.key); }, label: Text(translate("Close"))) @@ -461,14 +462,14 @@ Widget clientInfo(Client client) { } void toAndroidChannelInit() { - FFI.setMethodCallHandler((method, arguments) { + gFFI.setMethodCallHandler((method, arguments) { debugPrint("flutter got android msg,$method,$arguments"); try { switch (method) { case "start_capture": { SmartDialog.dismiss(); - FFI.serverModel.updateClientState(); + gFFI.serverModel.updateClientState(); break; } case "on_state_changed": @@ -476,7 +477,7 @@ void toAndroidChannelInit() { var name = arguments["name"] as String; var value = arguments["value"] as String == "true"; debugPrint("from jvm:on_state_changed,$name:$value"); - FFI.serverModel.changeStatue(name, value); + gFFI.serverModel.changeStatue(name, value); break; } case "on_android_permission_result": @@ -488,7 +489,7 @@ void toAndroidChannelInit() { } case "on_media_projection_canceled": { - FFI.serverModel.stopService(); + gFFI.serverModel.stopService(); break; } } diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index a1225ae85..a3965c199 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -1,12 +1,14 @@ -import 'package:settings_ui/settings_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:url_launcher/url_launcher.dart'; -import 'package:provider/provider.dart'; import 'dart:convert'; + +import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; +import 'package:provider/provider.dart'; +import 'package:settings_ui/settings_ui.dart'; +import 'package:url_launcher/url_launcher.dart'; + import '../../common.dart'; -import '../widgets/dialog.dart'; import '../../models/model.dart'; +import '../widgets/dialog.dart'; import 'home_page.dart'; import 'scan_page.dart'; @@ -89,10 +91,10 @@ class _SettingsState extends State { } void showServerSettings() { - final id = FFI.getByName('option', 'custom-rendezvous-server'); - final relay = FFI.getByName('option', 'relay-server'); - final api = FFI.getByName('option', 'api-server'); - final key = FFI.getByName('option', 'key'); + final id = gFFI.getByName('option', 'custom-rendezvous-server'); + final relay = gFFI.getByName('option', 'relay-server'); + final api = gFFI.getByName('option', 'api-server'); + final key = gFFI.getByName('option', 'key'); showServerSettingsWithValue(id, relay, key, api); } @@ -145,8 +147,8 @@ fetch('http://localhost:21114/api/login', { final body = { 'username': name, 'password': pass, - 'id': FFI.getByName('server_id'), - 'uuid': FFI.getByName('uuid') + 'id': gFFI.getByName('server_id'), + 'uuid': gFFI.getByName('uuid') }; try { final response = await http.post(Uri.parse('${url}/api/login'), @@ -166,24 +168,25 @@ String parseResp(String body) { } final token = data['access_token']; if (token != null) { - FFI.setByName('option', '{"name": "access_token", "value": "$token"}'); + gFFI.setByName('option', '{"name": "access_token", "value": "$token"}'); } final info = data['user']; if (info != null) { final value = json.encode(info); - FFI.setByName('option', json.encode({"name": "user_info", "value": value})); - FFI.ffiModel.updateUser(); + gFFI.setByName( + 'option', json.encode({"name": "user_info", "value": value})); + gFFI.ffiModel.updateUser(); } return ''; } void refreshCurrentUser() async { - final token = FFI.getByName("option", "access_token"); + final token = gFFI.getByName("option", "access_token"); if (token == '') return; final url = getUrl(); final body = { - 'id': FFI.getByName('server_id'), - 'uuid': FFI.getByName('uuid') + 'id': gFFI.getByName('server_id'), + 'uuid': gFFI.getByName('uuid') }; try { final response = await http.post(Uri.parse('${url}/api/currentUser'), @@ -204,12 +207,12 @@ void refreshCurrentUser() async { } void logout() async { - final token = FFI.getByName("option", "access_token"); + final token = gFFI.getByName("option", "access_token"); if (token == '') return; final url = getUrl(); final body = { - 'id': FFI.getByName('server_id'), - 'uuid': FFI.getByName('uuid') + 'id': gFFI.getByName('server_id'), + 'uuid': gFFI.getByName('uuid') }; try { await http.post(Uri.parse('${url}/api/logout'), @@ -225,15 +228,15 @@ void logout() async { } void resetToken() { - FFI.setByName('option', '{"name": "access_token", "value": ""}'); - FFI.setByName('option', '{"name": "user_info", "value": ""}'); - FFI.ffiModel.updateUser(); + gFFI.setByName('option', '{"name": "access_token", "value": ""}'); + gFFI.setByName('option', '{"name": "user_info", "value": ""}'); + gFFI.ffiModel.updateUser(); } String getUrl() { - var url = FFI.getByName('option', 'api-server'); + var url = gFFI.getByName('option', 'api-server'); if (url == '') { - url = FFI.getByName('option', 'custom-rendezvous-server'); + url = gFFI.getByName('option', 'custom-rendezvous-server'); if (url != '') { if (url.contains(':')) { final tmp = url.split(':'); @@ -323,10 +326,10 @@ void showLogin() { } String? getUsername() { - final token = FFI.getByName("option", "access_token"); + final token = gFFI.getByName("option", "access_token"); String? username; if (token != "") { - final info = FFI.getByName("option", "user_info"); + final info = gFFI.getByName("option", "user_info"); if (info != "") { try { Map tmp = json.decode(info); diff --git a/flutter/lib/mobile/widgets/dialog.dart b/flutter/lib/mobile/widgets/dialog.dart index 54f034627..c1e8a31e5 100644 --- a/flutter/lib/mobile/widgets/dialog.dart +++ b/flutter/lib/mobile/widgets/dialog.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; + import '../../common.dart'; import '../../models/model.dart'; @@ -86,7 +87,7 @@ void updatePasswordDialog() { ? () async { close(); showLoading(translate("Waiting")); - if (await FFI.serverModel.updatePassword(p0.text)) { + if (await gFFI.serverModel.updatePassword(p0.text)) { showSuccess(); } else { showError(); @@ -102,7 +103,7 @@ void updatePasswordDialog() { void enterPasswordDialog(String id) { final controller = TextEditingController(); - var remember = FFI.getByName('remember', id) == 'true'; + var remember = gFFI.getByName('remember', id) == 'true'; DialogManager.show((setState, close) { return CustomAlertDialog( title: Text(translate('Password Required')), @@ -137,7 +138,7 @@ void enterPasswordDialog(String id) { onPressed: () { var text = controller.text.trim(); if (text == '') return; - FFI.login(id, text, remember); + gFFI.login(id, text, remember); close(); showLoading(translate('Logging in...')); }, diff --git a/flutter/lib/mobile/widgets/overlay.dart b/flutter/lib/mobile/widgets/overlay.dart index b2176ef0a..d2a1bdb57 100644 --- a/flutter/lib/mobile/widgets/overlay.dart +++ b/flutter/lib/mobile/widgets/overlay.dart @@ -157,7 +157,7 @@ hideChatWindowOverlay() { toggleChatOverlay() { if (chatIconOverlayEntry == null || chatWindowOverlayEntry == null) { - FFI.invokeMethod("enable_soft_keyboard", true); + gFFI.invokeMethod("enable_soft_keyboard", true); showChatIconOverlay(); showChatWindowOverlay(); } else { @@ -248,12 +248,12 @@ showMobileActionsOverlay() { position: Offset(left, top), width: overlayW, height: overlayH, - onBackPressed: () => FFI.tap(MouseButtons.right), - onHomePressed: () => FFI.tap(MouseButtons.wheel), + onBackPressed: () => gFFI.tap(MouseButtons.right), + onHomePressed: () => gFFI.tap(MouseButtons.wheel), onRecentPressed: () async { - FFI.sendMouse('down', MouseButtons.wheel); + gFFI.sendMouse('down', MouseButtons.wheel); await Future.delayed(Duration(milliseconds: 500)); - FFI.sendMouse('up', MouseButtons.wheel); + gFFI.sendMouse('up', MouseButtons.wheel); }, ); }); diff --git a/flutter/lib/models/chat_model.dart b/flutter/lib/models/chat_model.dart index efef5f1e4..0eb6db279 100644 --- a/flutter/lib/models/chat_model.dart +++ b/flutter/lib/models/chat_model.dart @@ -41,6 +41,11 @@ class ChatModel with ChangeNotifier { int get currentID => _currentID; + WeakReference _ffi; + + /// Constructor + ChatModel(this._ffi); + ChatUser get currentUser { final user = messages[currentID]?.chatUser; if (user == null) { @@ -56,7 +61,7 @@ class ChatModel with ChangeNotifier { _currentID = id; notifyListeners(); } else { - final client = FFI.serverModel.clients[id]; + final client = _ffi.target?.serverModel.clients[id]; if (client == null) { return debugPrint( "Failed to changeCurrentID,remote user doesn't exist"); @@ -80,11 +85,11 @@ class ChatModel with ChangeNotifier { late final chatUser; if (id == clientModeID) { chatUser = ChatUser( - name: FFI.ffiModel.pi.username, - uid: FFI.getId(), + name: _ffi.target?.ffiModel.pi.username, + uid: _ffi.target?.getId(), ); } else { - final client = FFI.serverModel.clients[id]; + final client = _ffi.target?.serverModel.clients[id]; if (client == null) { return debugPrint("Failed to receive msg,user doesn't exist"); } @@ -112,12 +117,12 @@ class ChatModel with ChangeNotifier { if (message.text != null && message.text!.isNotEmpty) { _messages[_currentID]?.add(message); if (_currentID == clientModeID) { - FFI.setByName("chat_client_mode", message.text!); + _ffi.target?.setByName("chat_client_mode", message.text!); } else { final msg = Map() ..["id"] = _currentID ..["text"] = message.text!; - FFI.setByName("chat_server_mode", jsonEncode(msg)); + _ffi.target?.setByName("chat_server_mode", jsonEncode(msg)); } } notifyListeners(); diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index 2122b146f..0f7ce0df2 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -1,7 +1,8 @@ import 'dart:async'; import 'dart:convert'; -import 'package:flutter_hbb/common.dart'; + import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/mobile/pages/file_manager_page.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:path/path.dart' as Path; @@ -69,6 +70,10 @@ class FileModel extends ChangeNotifier { final _jobResultListener = JobResultListener>(); + final WeakReference _ffi; + + FileModel(this._ffi); + toggleSelectMode() { if (jobState == JobState.inProgress) { return; @@ -162,7 +167,7 @@ class FileModel extends ChangeNotifier { // overwrite msg['need_override'] = 'true'; } - FFI.setByName("set_confirm_override_file", jsonEncode(msg)); + _ffi.target?.setByName("set_confirm_override_file", jsonEncode(msg)); } } @@ -172,20 +177,23 @@ class FileModel extends ChangeNotifier { } onReady() async { - _localOption.home = FFI.getByName("get_home_dir"); + _localOption.home = _ffi.target?.getByName("get_home_dir") ?? ""; _localOption.showHidden = - FFI.getByName("peer_option", "local_show_hidden").isNotEmpty; + _ffi.target?.getByName("peer_option", "local_show_hidden").isNotEmpty ?? + false; - _remoteOption.showHidden = - FFI.getByName("peer_option", "remote_show_hidden").isNotEmpty; - _remoteOption.isWindows = FFI.ffiModel.pi.platform == "Windows"; + _remoteOption.showHidden = _ffi.target + ?.getByName("peer_option", "remote_show_hidden") + .isNotEmpty ?? + false; + _remoteOption.isWindows = _ffi.target?.ffiModel.pi.platform == "Windows"; - debugPrint("remote platform: ${FFI.ffiModel.pi.platform}"); + debugPrint("remote platform: ${_ffi.target?.ffiModel.pi.platform}"); await Future.delayed(Duration(milliseconds: 100)); - final local = FFI.getByName("peer_option", "local_dir"); - final remote = FFI.getByName("peer_option", "remote_dir"); + final local = _ffi.target?.getByName("peer_option", "local_dir") ?? ""; + final remote = _ffi.target?.getByName("peer_option", "remote_dir") ?? ""; openDirectory(local.isEmpty ? _localOption.home : local, isLocal: true); openDirectory(remote.isEmpty ? _remoteOption.home : remote, isLocal: false); await Future.delayed(Duration(seconds: 1)); @@ -205,19 +213,19 @@ class FileModel extends ChangeNotifier { msg["name"] = "local_dir"; msg["value"] = _currentLocalDir.path; - FFI.setByName('peer_option', jsonEncode(msg)); + _ffi.target?.setByName('peer_option', jsonEncode(msg)); msg["name"] = "local_show_hidden"; msg["value"] = _localOption.showHidden ? "Y" : ""; - FFI.setByName('peer_option', jsonEncode(msg)); + _ffi.target?.setByName('peer_option', jsonEncode(msg)); msg["name"] = "remote_dir"; msg["value"] = _currentRemoteDir.path; - FFI.setByName('peer_option', jsonEncode(msg)); + _ffi.target?.setByName('peer_option', jsonEncode(msg)); msg["name"] = "remote_show_hidden"; msg["value"] = _remoteOption.showHidden ? "Y" : ""; - FFI.setByName('peer_option', jsonEncode(msg)); + _ffi.target?.setByName('peer_option', jsonEncode(msg)); _currentLocalDir.clear(); _currentRemoteDir.clear(); _localOption.clear(); @@ -279,7 +287,7 @@ class FileModel extends ChangeNotifier { "show_hidden": showHidden.toString(), "is_remote": (!(items.isLocal!)).toString() }; - FFI.setByName("send_files", jsonEncode(msg)); + _ffi.target?.setByName("send_files", jsonEncode(msg)); }); } @@ -478,7 +486,7 @@ class FileModel extends ChangeNotifier { "file_num": fileNum.toString(), "is_remote": (!(isLocal)).toString() }; - FFI.setByName("remove_file", jsonEncode(msg)); + _ffi.target?.setByName("remove_file", jsonEncode(msg)); } sendRemoveEmptyDir(String path, int fileNum, bool isLocal) { @@ -487,7 +495,7 @@ class FileModel extends ChangeNotifier { "path": path, "is_remote": (!isLocal).toString() }; - FFI.setByName("remove_all_empty_dirs", jsonEncode(msg)); + _ffi.target?.setByName("remove_all_empty_dirs", jsonEncode(msg)); } createDir(String path) { @@ -497,11 +505,11 @@ class FileModel extends ChangeNotifier { "path": path, "is_remote": (!isLocal).toString() }; - FFI.setByName("create_dir", jsonEncode(msg)); + _ffi.target?.setByName("create_dir", jsonEncode(msg)); } cancelJob(int id) { - FFI.setByName("cancel_job", id.toString()); + _ffi.target?.setByName("cancel_job", id.toString()); jobReset(); } @@ -627,11 +635,11 @@ class FileFetcher { try { final msg = {"path": path, "show_hidden": showHidden.toString()}; if (isLocal) { - final res = FFI.getByName("read_local_dir_sync", jsonEncode(msg)); + final res = gFFI.getByName("read_local_dir_sync", jsonEncode(msg)); final fd = FileDirectory.fromJson(jsonDecode(res)); return fd; } else { - FFI.setByName("read_remote_dir", jsonEncode(msg)); + gFFI.setByName("read_remote_dir", jsonEncode(msg)); return registerReadTask(isLocal, path); } } catch (e) { @@ -649,7 +657,7 @@ class FileFetcher { "show_hidden": showHidden.toString(), "is_remote": (!isLocal).toString() }; - FFI.setByName("read_dir_recursive", jsonEncode(msg)); + gFFI.setByName("read_dir_recursive", jsonEncode(msg)); return registerReadRecursiveTask(id); } catch (e) { return Future.error(e); diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 3659a85df..85bdc13b7 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -25,6 +25,8 @@ bool _waitForImage = false; class FfiModel with ChangeNotifier { PeerInfo _pi = PeerInfo(); Display _display = Display(); + PlatformFFI _platformFFI = PlatformFFI(); + var _inputBlocked = false; final _permissions = Map(); bool? _secure; @@ -32,11 +34,18 @@ class FfiModel with ChangeNotifier { bool _touchMode = false; Timer? _timer; var _reconnects = 1; + WeakReference parent; Map get permissions => _permissions; Display get display => _display; + PlatformFFI get platformFFI => _platformFFI; + + set platformFFI(PlatformFFI value) { + _platformFFI = value; + } + bool? get secure => _secure; bool? get direct => _direct; @@ -53,13 +62,13 @@ class FfiModel with ChangeNotifier { _inputBlocked = v; } - FfiModel() { + FfiModel(this.parent) { Translator.call = translate; clear(); } Future init() async { - await PlatformFFI.init(); + await _platformFFI.init(); } void toggleTouchMode() { @@ -130,41 +139,41 @@ class FfiModel with ChangeNotifier { } else if (name == 'peer_info') { handlePeerInfo(evt, peerId); } else if (name == 'connection_ready') { - FFI.ffiModel.setConnectionType( - evt['secure'] == 'true', evt['direct'] == 'true'); + setConnectionType(evt['secure'] == 'true', evt['direct'] == 'true'); } else if (name == 'switch_display') { handleSwitchDisplay(evt); } else if (name == 'cursor_data') { - FFI.cursorModel.updateCursorData(evt); + parent.target?.cursorModel.updateCursorData(evt); } else if (name == 'cursor_id') { - FFI.cursorModel.updateCursorId(evt); + parent.target?.cursorModel.updateCursorId(evt); } else if (name == 'cursor_position') { - FFI.cursorModel.updateCursorPosition(evt); + parent.target?.cursorModel.updateCursorPosition(evt); } else if (name == 'clipboard') { Clipboard.setData(ClipboardData(text: evt['content'])); } else if (name == 'permission') { - FFI.ffiModel.updatePermission(evt); + parent.target?.ffiModel.updatePermission(evt); } else if (name == 'chat_client_mode') { - FFI.chatModel.receive(ChatModel.clientModeID, evt['text'] ?? ""); + parent.target?.chatModel + .receive(ChatModel.clientModeID, evt['text'] ?? ""); } else if (name == 'chat_server_mode') { - FFI.chatModel + parent.target?.chatModel .receive(int.parse(evt['id'] as String), evt['text'] ?? ""); } else if (name == 'file_dir') { - FFI.fileModel.receiveFileDir(evt); + parent.target?.fileModel.receiveFileDir(evt); } else if (name == 'job_progress') { - FFI.fileModel.tryUpdateJobProgress(evt); + parent.target?.fileModel.tryUpdateJobProgress(evt); } else if (name == 'job_done') { - FFI.fileModel.jobDone(evt); + parent.target?.fileModel.jobDone(evt); } else if (name == 'job_error') { - FFI.fileModel.jobError(evt); + parent.target?.fileModel.jobError(evt); } else if (name == 'override_file_confirm') { - FFI.fileModel.overrideFileConfirm(evt); + parent.target?.fileModel.overrideFileConfirm(evt); } else if (name == 'try_start_without_auth') { - FFI.serverModel.loginRequest(evt); + parent.target?.serverModel.loginRequest(evt); } else if (name == 'on_client_authorized') { - FFI.serverModel.onClientAuthorized(evt); + parent.target?.serverModel.onClientAuthorized(evt); } else if (name == 'on_client_remove') { - FFI.serverModel.onClientRemove(evt); + parent.target?.serverModel.onClientRemove(evt); } }; } @@ -178,44 +187,45 @@ class FfiModel with ChangeNotifier { } else if (name == 'peer_info') { handlePeerInfo(evt, peerId); } else if (name == 'connection_ready') { - FFI.ffiModel.setConnectionType( + parent.target?.ffiModel.setConnectionType( evt['secure'] == 'true', evt['direct'] == 'true'); } else if (name == 'switch_display') { handleSwitchDisplay(evt); } else if (name == 'cursor_data') { - FFI.cursorModel.updateCursorData(evt); + parent.target?.cursorModel.updateCursorData(evt); } else if (name == 'cursor_id') { - FFI.cursorModel.updateCursorId(evt); + parent.target?.cursorModel.updateCursorId(evt); } else if (name == 'cursor_position') { - FFI.cursorModel.updateCursorPosition(evt); + parent.target?.cursorModel.updateCursorPosition(evt); } else if (name == 'clipboard') { Clipboard.setData(ClipboardData(text: evt['content'])); } else if (name == 'permission') { - FFI.ffiModel.updatePermission(evt); + parent.target?.ffiModel.updatePermission(evt); } else if (name == 'chat_client_mode') { - FFI.chatModel.receive(ChatModel.clientModeID, evt['text'] ?? ""); + parent.target?.chatModel + .receive(ChatModel.clientModeID, evt['text'] ?? ""); } else if (name == 'chat_server_mode') { - FFI.chatModel + parent.target?.chatModel .receive(int.parse(evt['id'] as String), evt['text'] ?? ""); } else if (name == 'file_dir') { - FFI.fileModel.receiveFileDir(evt); + parent.target?.fileModel.receiveFileDir(evt); } else if (name == 'job_progress') { - FFI.fileModel.tryUpdateJobProgress(evt); + parent.target?.fileModel.tryUpdateJobProgress(evt); } else if (name == 'job_done') { - FFI.fileModel.jobDone(evt); + parent.target?.fileModel.jobDone(evt); } else if (name == 'job_error') { - FFI.fileModel.jobError(evt); + parent.target?.fileModel.jobError(evt); } else if (name == 'override_file_confirm') { - FFI.fileModel.overrideFileConfirm(evt); + parent.target?.fileModel.overrideFileConfirm(evt); } else if (name == 'try_start_without_auth') { - FFI.serverModel.loginRequest(evt); + parent.target?.serverModel.loginRequest(evt); } else if (name == 'on_client_authorized') { - FFI.serverModel.onClientAuthorized(evt); + parent.target?.serverModel.onClientAuthorized(evt); } else if (name == 'on_client_remove') { - FFI.serverModel.onClientRemove(evt); + parent.target?.serverModel.onClientRemove(evt); } }; - PlatformFFI.setEventCallback(cb); + platformFFI.setEventCallback(cb); } void handleSwitchDisplay(Map evt) { @@ -226,7 +236,7 @@ class FfiModel with ChangeNotifier { _display.width = int.parse(evt['width']); _display.height = int.parse(evt['height']); if (old != _pi.currentDisplay) - FFI.cursorModel.updateDisplayOrigin(_display.x, _display.y); + parent.target?.cursorModel.updateDisplayOrigin(_display.x, _display.y); notifyListeners(); } @@ -252,7 +262,7 @@ class FfiModel with ChangeNotifier { _timer?.cancel(); if (hasRetry) { _timer = Timer(Duration(seconds: _reconnects), () { - FFI.bind.sessionReconnect(id: id); + parent.target?.bind.sessionReconnect(id: id); clearPermissions(); showLoading(translate('Connecting...')); }); @@ -274,16 +284,17 @@ class FfiModel with ChangeNotifier { if (isPeerAndroid) { _touchMode = true; - if (FFI.ffiModel.permissions['keyboard'] != false) { + if (parent.target?.ffiModel.permissions['keyboard'] != false) { Timer(Duration(milliseconds: 100), showMobileActionsOverlay); } } else { - _touchMode = - await FFI.bind.getSessionOption(id: peerId, arg: "touch-mode") != ''; + _touchMode = await parent.target?.bind + .getSessionOption(id: peerId, arg: "touch-mode") != + ''; } if (evt['is_file_transfer'] == "true") { - FFI.fileModel.onReady(); + parent.target?.fileModel.onReady(); } else { _pi.displays = []; List displays = json.decode(evt['displays']); @@ -316,18 +327,22 @@ class ImageModel with ChangeNotifier { String _id = ""; + WeakReference parent; + + ImageModel(this.parent); + void onRgba(Uint8List rgba) { if (_waitForImage) { _waitForImage = false; SmartDialog.dismiss(); } - final pid = FFI.id; + final pid = parent.target?.id; ui.decodeImageFromPixels( rgba, - FFI.ffiModel.display.width, - FFI.ffiModel.display.height, + parent.target?.ffiModel.display.width ?? 0, + parent.target?.ffiModel.display.height ?? 0, isWeb ? ui.PixelFormat.rgba8888 : ui.PixelFormat.bgra8888, (image) { - if (FFI.id != pid) return; + if (parent.target?.id != pid) return; try { // my throw exception, because the listener maybe already dispose update(image); @@ -340,19 +355,21 @@ class ImageModel with ChangeNotifier { void update(ui.Image? image) { if (_image == null && image != null) { if (isWebDesktop) { - FFI.canvasModel.updateViewStyle(); + parent.target?.canvasModel.updateViewStyle(); } else { final size = MediaQueryData.fromWindow(ui.window).size; final xscale = size.width / image.width; final yscale = size.height / image.height; - FFI.canvasModel.scale = max(xscale, yscale); + parent.target?.canvasModel.scale = max(xscale, yscale); + } + if (parent.target != null) { + initializeCursorAndCanvas(parent.target!); } - initializeCursorAndCanvas(); Future.delayed(Duration(milliseconds: 1), () { - if (FFI.ffiModel.isPeerAndroid) { - FFI.bind + if (parent.target?.ffiModel.isPeerAndroid ?? false) { + parent.target?.bind .sessionPeerOption(id: _id, name: "view-style", value: "shrink"); - FFI.canvasModel.updateViewStyle(); + parent.target?.canvasModel.updateViewStyle(); } }); } @@ -383,7 +400,9 @@ class CanvasModel with ChangeNotifier { double _scale = 1.0; String id = ""; // TODO multi canvas model - CanvasModel(); + WeakReference parent; + + CanvasModel(this.parent); double get x => _x; @@ -392,13 +411,14 @@ class CanvasModel with ChangeNotifier { double get scale => _scale; void updateViewStyle() async { - final s = await FFI.bind.getSessionOption(id: id, arg: 'view-style'); + final s = + await parent.target?.bind.getSessionOption(id: id, arg: 'view-style'); if (s == null) { return; } final size = MediaQueryData.fromWindow(ui.window).size; - final s1 = size.width / FFI.ffiModel.display.width; - final s2 = size.height / FFI.ffiModel.display.height; + final s1 = size.width / (parent.target?.ffiModel.display.width ?? 720); + final s2 = size.height / (parent.target?.ffiModel.display.height ?? 1280); if (s == 'shrink') { final s = s1 < s2 ? s1 : s2; if (s < 1) { @@ -412,8 +432,8 @@ class CanvasModel with ChangeNotifier { } else { _scale = 1; } - _x = (size.width - FFI.ffiModel.display.width * _scale) / 2; - _y = (size.height - FFI.ffiModel.display.height * _scale) / 2; + _x = (size.width - getDisplayWidth() * _scale) / 2; + _y = (size.height - getDisplayHeight() * _scale) / 2; notifyListeners(); } @@ -424,10 +444,18 @@ class CanvasModel with ChangeNotifier { notifyListeners(); } + int getDisplayWidth() { + return parent.target?.ffiModel.display.width ?? 1080; + } + + int getDisplayHeight() { + return parent.target?.ffiModel.display.height ?? 720; + } + void moveDesktopMouse(double x, double y) { final size = MediaQueryData.fromWindow(ui.window).size; - final dw = FFI.ffiModel.display.width * _scale; - final dh = FFI.ffiModel.display.height * _scale; + final dw = getDisplayWidth() * _scale; + final dh = getDisplayHeight() * _scale; var dxOffset = 0; var dyOffset = 0; if (dw > size.width) { @@ -441,7 +469,7 @@ class CanvasModel with ChangeNotifier { if (dxOffset != 0 || dyOffset != 0) { notifyListeners(); } - FFI.cursorModel.moveLocal(x, y); + parent.target?.cursorModel.moveLocal(x, y); } set scale(v) { @@ -470,17 +498,17 @@ class CanvasModel with ChangeNotifier { } void updateScale(double v) { - if (FFI.imageModel.image == null) return; - final offset = FFI.cursorModel.offset; - var r = FFI.cursorModel.getVisibleRect(); + if (parent.target?.imageModel.image == null) return; + final offset = parent.target?.cursorModel.offset ?? Offset(0, 0); + var r = parent.target?.cursorModel.getVisibleRect() ?? Rect.zero; final px0 = (offset.dx - r.left) * _scale; final py0 = (offset.dy - r.top) * _scale; _scale *= v; - final maxs = FFI.imageModel.maxScale; - final mins = FFI.imageModel.minScale; + final maxs = parent.target?.imageModel.maxScale ?? 1; + final mins = parent.target?.imageModel.minScale ?? 1; if (_scale > maxs) _scale = maxs; if (_scale < mins) _scale = mins; - r = FFI.cursorModel.getVisibleRect(); + r = parent.target?.cursorModel.getVisibleRect() ?? Rect.zero; final px1 = (offset.dx - r.left) * _scale; final py1 = (offset.dy - r.top) * _scale; _x -= px1 - px0; @@ -506,6 +534,7 @@ class CursorModel with ChangeNotifier { double _displayOriginX = 0; double _displayOriginY = 0; String id = ""; // TODO multi cursor model + WeakReference parent; ui.Image? get image => _image; @@ -519,12 +548,14 @@ class CursorModel with ChangeNotifier { double get hoty => _hoty; + CursorModel(this.parent); + // remote physical display coordinate Rect getVisibleRect() { final size = MediaQueryData.fromWindow(ui.window).size; - final xoffset = FFI.canvasModel.x; - final yoffset = FFI.canvasModel.y; - final scale = FFI.canvasModel.scale; + final xoffset = parent.target?.canvasModel.x ?? 0; + final yoffset = parent.target?.canvasModel.y ?? 0; + final scale = parent.target?.canvasModel.scale ?? 1; final x0 = _displayOriginX - xoffset / scale; final y0 = _displayOriginY - yoffset / scale; return Rect.fromLTWH(x0, y0, size.width / scale, size.height / scale); @@ -535,7 +566,7 @@ class CursorModel with ChangeNotifier { var keyboardHeight = m.viewInsets.bottom; final size = m.size; if (keyboardHeight < 100) return 0; - final s = FFI.canvasModel.scale; + final s = parent.target?.canvasModel.scale ?? 1.0; final thresh = (size.height - keyboardHeight) / 2; var h = (_y - getVisibleRect().top) * s; // local physical display height return h - thresh; @@ -543,19 +574,19 @@ class CursorModel with ChangeNotifier { void touch(double x, double y, MouseButtons button) { moveLocal(x, y); - FFI.moveMouse(_x, _y); - FFI.tap(button); + parent.target?.moveMouse(_x, _y); + parent.target?.tap(button); } void move(double x, double y) { moveLocal(x, y); - FFI.moveMouse(_x, _y); + parent.target?.moveMouse(_x, _y); } void moveLocal(double x, double y) { - final scale = FFI.canvasModel.scale; - final xoffset = FFI.canvasModel.x; - final yoffset = FFI.canvasModel.y; + final scale = parent.target?.canvasModel.scale ?? 1.0; + final xoffset = parent.target?.canvasModel.x ?? 0; + final yoffset = parent.target?.canvasModel.y ?? 0; _x = (x - xoffset) / scale + _displayOriginX; _y = (y - yoffset) / scale + _displayOriginY; notifyListeners(); @@ -564,22 +595,22 @@ class CursorModel with ChangeNotifier { void reset() { _x = _displayOriginX; _y = _displayOriginY; - FFI.moveMouse(_x, _y); - FFI.canvasModel.clear(true); + parent.target?.moveMouse(_x, _y); + parent.target?.canvasModel.clear(true); notifyListeners(); } void updatePan(double dx, double dy, bool touchMode) { - if (FFI.imageModel.image == null) return; + if (parent.target?.imageModel.image == null) return; if (touchMode) { - final scale = FFI.canvasModel.scale; + final scale = parent.target?.canvasModel.scale ?? 1.0; _x += dx / scale; _y += dy / scale; - FFI.moveMouse(_x, _y); + parent.target?.moveMouse(_x, _y); notifyListeners(); return; } - final scale = FFI.canvasModel.scale; + final scale = parent.target?.canvasModel.scale ?? 1.0; dx /= scale; dy /= scale; final r = getVisibleRect(); @@ -588,7 +619,7 @@ class CursorModel with ChangeNotifier { var tryMoveCanvasX = false; if (dx > 0) { final maxCanvasCanMove = _displayOriginX + - FFI.imageModel.image!.width - + (parent.target?.imageModel.image!.width ?? 1280) - r.right.roundToDouble(); tryMoveCanvasX = _x + dx > cx && maxCanvasCanMove > 0; if (tryMoveCanvasX) { @@ -610,7 +641,7 @@ class CursorModel with ChangeNotifier { var tryMoveCanvasY = false; if (dy > 0) { final mayCanvasCanMove = _displayOriginY + - FFI.imageModel.image!.height - + (parent.target?.imageModel.image!.height ?? 720) - r.bottom.roundToDouble(); tryMoveCanvasY = _y + dy > cy && mayCanvasCanMove > 0; if (tryMoveCanvasY) { @@ -634,13 +665,13 @@ class CursorModel with ChangeNotifier { _x += dx; _y += dy; if (tryMoveCanvasX && dx != 0) { - FFI.canvasModel.panX(-dx); + parent.target?.canvasModel.panX(-dx); } if (tryMoveCanvasY && dy != 0) { - FFI.canvasModel.panY(-dy); + parent.target?.canvasModel.panY(-dy); } - FFI.moveMouse(_x, _y); + parent.target?.moveMouse(_x, _y); notifyListeners(); } @@ -652,10 +683,10 @@ class CursorModel with ChangeNotifier { var height = int.parse(evt['height']); List colors = json.decode(evt['colors']); final rgba = Uint8List.fromList(colors.map((s) => s as int).toList()); - var pid = FFI.id; + var pid = parent.target?.id; ui.decodeImageFromPixels(rgba, width, height, ui.PixelFormat.rgba8888, (image) { - if (FFI.id != pid) return; + if (parent.target?.id != pid) return; _image = image; _images[id] = Tuple3(image, _hotx, _hoty); try { @@ -688,8 +719,8 @@ class CursorModel with ChangeNotifier { _displayOriginY = y; _x = x + 1; _y = y + 1; - FFI.moveMouse(x, y); - FFI.canvasModel.resetOffset(); + parent.target?.moveMouse(x, y); + parent.target?.canvasModel.resetOffset(); notifyListeners(); } @@ -699,7 +730,7 @@ class CursorModel with ChangeNotifier { _displayOriginY = y; _x = xCursor; _y = yCursor; - FFI.moveMouse(x, y); + parent.target?.moveMouse(x, y); notifyListeners(); } @@ -729,33 +760,43 @@ extension ToString on MouseButtons { /// FFI class for communicating with the Rust core. class FFI { - static var id = ""; - static var shift = false; - static var ctrl = false; - static var alt = false; - static var command = false; - static var version = ""; - static final imageModel = ImageModel(); - static final ffiModel = FfiModel(); - static final cursorModel = CursorModel(); - static final canvasModel = CanvasModel(); - static final serverModel = ServerModel(); - static final chatModel = ChatModel(); - static final fileModel = FileModel(); + var id = ""; + var shift = false; + var ctrl = false; + var alt = false; + var command = false; + var version = ""; + late final ImageModel imageModel; + late final FfiModel ffiModel; + late final CursorModel cursorModel; + late final CanvasModel canvasModel; + late final ServerModel serverModel; + late final ChatModel chatModel; + late final FileModel fileModel; + + FFI() { + this.imageModel = ImageModel(WeakReference(this)); + this.ffiModel = FfiModel(WeakReference(this)); + this.cursorModel = CursorModel(WeakReference(this)); + this.canvasModel = CanvasModel(WeakReference(this)); + this.serverModel = ServerModel(WeakReference(this)); // use global FFI + this.chatModel = ChatModel(WeakReference(this)); + this.fileModel = FileModel(WeakReference(this)); + } /// Get the remote id for current client. - static String getId() { + String getId() { return getByName('remote_id'); // TODO } /// Send a mouse tap event(down and up). - static void tap(MouseButtons button) { + void tap(MouseButtons button) { sendMouse('down', button); sendMouse('up', button); } /// Send scroll event with scroll distance [y]. - static void scroll(int y) { + void scroll(int y) { setByName('send_mouse', json.encode(modify({'id': id, 'type': 'wheel', 'y': y.toString()}))); } @@ -763,16 +804,16 @@ class FFI { /// Reconnect to the remote peer. // static void reconnect() { // setByName('reconnect'); - // FFI.ffiModel.clearPermissions(); + // parent.target?.ffiModel.clearPermissions(); // } /// Reset key modifiers to false, including [shift], [ctrl], [alt] and [command]. - static void resetModifiers() { + void resetModifiers() { shift = ctrl = alt = command = false; } /// Modify the given modifier map [evt] based on current modifier key status. - static Map modify(Map evt) { + Map modify(Map evt) { if (ctrl) evt['ctrl'] = 'true'; if (shift) evt['shift'] = 'true'; if (alt) evt['alt'] = 'true'; @@ -781,7 +822,7 @@ class FFI { } /// Send mouse press event. - static void sendMouse(String type, MouseButtons button) { + void sendMouse(String type, MouseButtons button) { if (!ffiModel.keyboard()) return; setByName('send_mouse', json.encode(modify({'id': id, 'type': type, 'buttons': button.value}))); @@ -790,7 +831,7 @@ class FFI { /// Send key stroke event. /// [down] indicates the key's state(down or up). /// [press] indicates a click event(down and up). - static void inputKey(String name, {bool? down, bool? press}) { + void inputKey(String name, {bool? down, bool? press}) { if (!ffiModel.keyboard()) return; // final Map out = Map(); // out['name'] = name; @@ -804,7 +845,7 @@ class FFI { // } // setByName('input_key', json.encode(modify(out))); // TODO id - FFI.bind.sessionInputKey( + bind.sessionInputKey( id: id, name: name, down: down ?? false, @@ -816,7 +857,7 @@ class FFI { } /// Send mouse movement event with distance in [x] and [y]. - static void moveMouse(double x, double y) { + void moveMouse(double x, double y) { if (!ffiModel.keyboard()) return; var x2 = x.toInt(); var y2 = y.toInt(); @@ -825,7 +866,7 @@ class FFI { } /// List the saved peers. - static List peers() { + List peers() { try { var str = getByName('peers'); // TODO if (str == "") return []; @@ -842,19 +883,19 @@ class FFI { } /// Connect with the given [id]. Only transfer file if [isFileTransfer]. - static void connect(String id, {bool isFileTransfer = false}) { + void connect(String id, {bool isFileTransfer = false}) { if (isFileTransfer) { setByName('connect_file_transfer', id); } else { - FFI.chatModel.resetClientMode(); + chatModel.resetClientMode(); // setByName('connect', id); // TODO multi model instances - FFI.canvasModel.id = id; - FFI.imageModel._id = id; - FFI.cursorModel.id = id; + canvasModel.id = id; + imageModel._id = id; + cursorModel.id = id; final stream = - FFI.bind.sessionConnect(id: id, isFileTransfer: isFileTransfer); - final cb = FFI.ffiModel.startEventListener(id); + bind.sessionConnect(id: id, isFileTransfer: isFileTransfer); + final cb = ffiModel.startEventListener(id); () async { await for (final message in stream) { if (message is Event) { @@ -866,28 +907,28 @@ class FFI { print('json.decode fail(): $e'); } } else if (message is Rgba) { - FFI.imageModel.onRgba(message.field0); + imageModel.onRgba(message.field0); } } }(); // every instance will bind a stream } - FFI.id = id; + id = id; } /// Login with [password], choose if the client should [remember] it. - static void login(String id, String password, bool remember) { - FFI.bind.sessionLogin(id: id, password: password, remember: remember); + void login(String id, String password, bool remember) { + bind.sessionLogin(id: id, password: password, remember: remember); } /// Close the remote session. - static void close() { + void close() { chatModel.close(); - if (FFI.imageModel.image != null && !isWebDesktop) { + if (imageModel.image != null && !isWebDesktop) { savePreference(id, cursorModel.x, cursorModel.y, canvasModel.x, canvasModel.y, canvasModel.scale, ffiModel.pi.currentDisplay); } - FFI.bind.sessionClose(id: id); + bind.sessionClose(id: id); id = ""; imageModel.update(null); cursorModel.clear(); @@ -898,18 +939,18 @@ class FFI { /// Send **get** command to the Rust core based on [name] and [arg]. /// Return the result as a string. - static String getByName(String name, [String arg = '']) { - return PlatformFFI.getByName(name, arg); + String getByName(String name, [String arg = '']) { + return ffiModel.platformFFI.getByName(name, arg); } /// Send **set** command to the Rust core based on [name] and [value]. - static void setByName(String name, [String value = '']) { - PlatformFFI.setByName(name, value); + void setByName(String name, [String value = '']) { + ffiModel.platformFFI.setByName(name, value); } - static RustdeskImpl get bind => PlatformFFI.ffiBind; + RustdeskImpl get bind => ffiModel.platformFFI.ffiBind; - static handleMouse(Map evt) { + handleMouse(Map evt) { var type = ''; var isMove = false; switch (evt['type']) { @@ -929,16 +970,16 @@ class FFI { var x = evt['x']; var y = evt['y']; if (isMove) { - FFI.canvasModel.moveDesktopMouse(x, y); + canvasModel.moveDesktopMouse(x, y); } - final d = FFI.ffiModel.display; - x -= FFI.canvasModel.x; - y -= FFI.canvasModel.y; + final d = ffiModel.display; + x -= canvasModel.x; + y -= canvasModel.y; if (!isMove && (x < 0 || x > d.width || y < 0 || y > d.height)) { return; } - x /= FFI.canvasModel.scale; - y /= FFI.canvasModel.scale; + x /= canvasModel.scale; + y /= canvasModel.scale; x += d.x; y += d.y; if (type != '') { @@ -964,20 +1005,20 @@ class FFI { setByName('send_mouse', json.encode(evt)); } - static listenToMouse(bool yesOrNo) { + listenToMouse(bool yesOrNo) { if (yesOrNo) { - PlatformFFI.startDesktopWebListener(); + ffiModel.platformFFI.startDesktopWebListener(); } else { - PlatformFFI.stopDesktopWebListener(); + ffiModel.platformFFI.stopDesktopWebListener(); } } - static void setMethodCallHandler(FMethod callback) { - PlatformFFI.setMethodCallHandler(callback); + void setMethodCallHandler(FMethod callback) { + ffiModel.platformFFI.setMethodCallHandler(callback); } - static Future invokeMethod(String method, [dynamic arguments]) async { - return await PlatformFFI.invokeMethod(method, arguments); + Future invokeMethod(String method, [dynamic arguments]) async { + return await ffiModel.platformFFI.invokeMethod(method, arguments); } } @@ -1038,15 +1079,15 @@ void removePreference(String id) async { prefs.remove('peer' + id); } -void initializeCursorAndCanvas() async { - var p = await getPreference(FFI.id); +void initializeCursorAndCanvas(FFI ffi) async { + var p = await getPreference(ffi.id); int currentDisplay = 0; if (p != null) { currentDisplay = p['currentDisplay']; } - if (p == null || currentDisplay != FFI.ffiModel.pi.currentDisplay) { - FFI.cursorModel - .updateDisplayOrigin(FFI.ffiModel.display.x, FFI.ffiModel.display.y); + if (p == null || currentDisplay != ffi.ffiModel.pi.currentDisplay) { + ffi.cursorModel + .updateDisplayOrigin(ffi.ffiModel.display.x, ffi.ffiModel.display.y); return; } double xCursor = p['xCursor']; @@ -1054,17 +1095,19 @@ void initializeCursorAndCanvas() async { double xCanvas = p['xCanvas']; double yCanvas = p['yCanvas']; double scale = p['scale']; - FFI.cursorModel.updateDisplayOriginWithCursor( - FFI.ffiModel.display.x, FFI.ffiModel.display.y, xCursor, yCursor); - FFI.canvasModel.update(xCanvas, yCanvas, scale); + ffi.cursorModel.updateDisplayOriginWithCursor( + ffi.ffiModel.display.x, ffi.ffiModel.display.y, xCursor, yCursor); + ffi.canvasModel.update(xCanvas, yCanvas, scale); } /// Translate text based on the pre-defined dictionary. -String translate(String name) { +/// note: params [FFI?] can be used to replace global FFI implementation +/// for example: during global initialization, gFFI not exists yet. +String translate(String name, {FFI? ffi}) { if (name.startsWith('Failed to') && name.contains(': ')) { return name.split(': ').map((x) => translate(x)).join(': '); } var a = 'translate'; var b = '{"locale": "$localeName", "text": "$name"}'; - return FFI.getByName(a, b); + return (ffi ?? gFFI).getByName(a, b); } diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index a425ea810..1ae523b8b 100644 --- a/flutter/lib/models/native_model.dart +++ b/flutter/lib/models/native_model.dart @@ -25,15 +25,15 @@ typedef F3 = void Function(Pointer, Pointer); /// FFI wrapper around the native Rust core. /// Hides the platform differences. class PlatformFFI { - static Pointer? _lastRgbaFrame; - static String _dir = ''; - static String _homeDir = ''; - static F2? _getByName; - static F3? _setByName; - static late RustdeskImpl _ffiBind; - static void Function(Map)? _eventCallback; + Pointer? _lastRgbaFrame; + String _dir = ''; + String _homeDir = ''; + F2? _getByName; + F3? _setByName; + late RustdeskImpl _ffiBind; + void Function(Map)? _eventCallback; - static RustdeskImpl get ffiBind => _ffiBind; + RustdeskImpl get ffiBind => _ffiBind; static Future getVersion() async { PackageInfo packageInfo = await PackageInfo.fromPlatform(); @@ -42,7 +42,7 @@ class PlatformFFI { /// Send **get** command to the Rust core based on [name] and [arg]. /// Return the result as a string. - static String getByName(String name, [String arg = '']) { + String getByName(String name, [String arg = '']) { if (_getByName == null) return ''; var a = name.toNativeUtf8(); var b = arg.toNativeUtf8(); @@ -56,7 +56,7 @@ class PlatformFFI { } /// Send **set** command to the Rust core based on [name] and [value]. - static void setByName(String name, [String value = '']) { + void setByName(String name, [String value = '']) { if (_setByName == null) return; var a = name.toNativeUtf8(); var b = value.toNativeUtf8(); @@ -66,7 +66,7 @@ class PlatformFFI { } /// Init the FFI class, loads the native Rust core library. - static Future init() async { + Future init() async { isIOS = Platform.isIOS; isAndroid = Platform.isAndroid; isDesktop = Platform.isWindows || Platform.isMacOS || Platform.isLinux; @@ -134,7 +134,7 @@ class PlatformFFI { } /// Start listening to the Rust core's events and frames. - static void _startListenEvent(RustdeskImpl rustdeskImpl) { + void _startListenEvent(RustdeskImpl rustdeskImpl) { () async { await for (final message in rustdeskImpl.startGlobalEventStream()) { if (_eventCallback != null) { @@ -149,24 +149,24 @@ class PlatformFFI { }(); } - static void setEventCallback(void Function(Map) fun) async { + void setEventCallback(void Function(Map) fun) async { _eventCallback = fun; } - static void setRgbaCallback(void Function(Uint8List) fun) async {} + void setRgbaCallback(void Function(Uint8List) fun) async {} - static void startDesktopWebListener() {} + void startDesktopWebListener() {} - static void stopDesktopWebListener() {} + void stopDesktopWebListener() {} - static void setMethodCallHandler(FMethod callback) { + void setMethodCallHandler(FMethod callback) { toAndroidChannel.setMethodCallHandler((call) async { callback(call.method, call.arguments); return null; }); } - static invokeMethod(String method, [dynamic arguments]) async { + invokeMethod(String method, [dynamic arguments]) async { if (!isAndroid) return Future(() => false); return await toAndroidChannel.invokeMethod(method, arguments); } diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index 68d3d2391..311fef334 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -10,7 +10,6 @@ import '../mobile/pages/server_page.dart'; import 'model.dart'; const loginDialogTag = "LOGIN"; -final _emptyIdShow = translate("Generating ..."); class ServerModel with ChangeNotifier { bool _isStart = false; // Android MainService status @@ -20,7 +19,8 @@ class ServerModel with ChangeNotifier { bool _fileOk = false; int _connectStatus = 0; // Rendezvous Server status - final _serverId = TextEditingController(text: _emptyIdShow); + late String _emptyIdShow; + late final TextEditingController _serverId; final _serverPasswd = TextEditingController(text: ""); Map _clients = {}; @@ -45,8 +45,12 @@ class ServerModel with ChangeNotifier { final controller = ScrollController(); - ServerModel() { + WeakReference parent; + + ServerModel(this.parent) { () async { + _emptyIdShow = translate("Generating ...", ffi: this.parent.target); + _serverId = TextEditingController(text: this._emptyIdShow); /** * 1. check android permission * 2. check config @@ -59,39 +63,42 @@ class ServerModel with ChangeNotifier { // audio if (androidVersion < 30 || !await PermissionManager.check("audio")) { _audioOk = false; - FFI.setByName( + parent.target?.setByName( 'option', jsonEncode(Map() ..["name"] = "enable-audio" ..["value"] = "N")); } else { - final audioOption = FFI.getByName('option', 'enable-audio'); - _audioOk = audioOption.isEmpty; + final audioOption = parent.target?.getByName('option', 'enable-audio'); + _audioOk = audioOption?.isEmpty ?? false; } // file if (!await PermissionManager.check("file")) { _fileOk = false; - FFI.setByName( + parent.target?.setByName( 'option', jsonEncode(Map() ..["name"] = "enable-file-transfer" ..["value"] = "N")); } else { - final fileOption = FFI.getByName('option', 'enable-file-transfer'); - _fileOk = fileOption.isEmpty; + final fileOption = + parent.target?.getByName('option', 'enable-file-transfer'); + _fileOk = fileOption?.isEmpty ?? false; } // input (mouse control) Map res = Map() ..["name"] = "enable-keyboard" ..["value"] = 'N'; - FFI.setByName('option', jsonEncode(res)); // input false by default + parent.target + ?.setByName('option', jsonEncode(res)); // input false by default notifyListeners(); }(); Timer.periodic(Duration(seconds: 1), (timer) { - var status = int.tryParse(FFI.getByName('connect_statue')) ?? 0; + var status = + int.tryParse(parent.target?.getByName('connect_statue') ?? "") ?? 0; if (status > 0) { status = 1; } @@ -99,8 +106,9 @@ class ServerModel with ChangeNotifier { _connectStatus = status; notifyListeners(); } - final res = - FFI.getByName('check_clients_length', _clients.length.toString()); + final res = parent.target + ?.getByName('check_clients_length', _clients.length.toString()) ?? + ""; if (res.isNotEmpty) { debugPrint("clients not match!"); updateClientState(res); @@ -121,7 +129,7 @@ class ServerModel with ChangeNotifier { Map res = Map() ..["name"] = "enable-audio" ..["value"] = _audioOk ? '' : 'N'; - FFI.setByName('option', jsonEncode(res)); + parent.target?.setByName('option', jsonEncode(res)); notifyListeners(); } @@ -138,15 +146,17 @@ class ServerModel with ChangeNotifier { Map res = Map() ..["name"] = "enable-file-transfer" ..["value"] = _fileOk ? '' : 'N'; - FFI.setByName('option', jsonEncode(res)); + parent.target?.setByName('option', jsonEncode(res)); notifyListeners(); } toggleInput() { if (_inputOk) { - FFI.invokeMethod("stop_input"); + parent.target?.invokeMethod("stop_input"); } else { - showInputWarnAlert(); + if (parent.target != null) { + showInputWarnAlert(parent.target!); + } } } @@ -203,9 +213,10 @@ class ServerModel with ChangeNotifier { Future startService() async { _isStart = true; notifyListeners(); - FFI.ffiModel.updateEventListener(""); - await FFI.invokeMethod("init_service"); - FFI.setByName("start_service"); + // TODO + parent.target?.ffiModel.updateEventListener(""); + await parent.target?.invokeMethod("init_service"); + parent.target?.setByName("start_service"); getIDPasswd(); updateClientState(); if (!Platform.isLinux) { @@ -217,9 +228,10 @@ class ServerModel with ChangeNotifier { /// Stop the screen sharing service. Future stopService() async { _isStart = false; - FFI.serverModel.closeAll(); - await FFI.invokeMethod("stop_service"); - FFI.setByName("stop_service"); + // TODO + parent.target?.serverModel.closeAll(); + await parent.target?.invokeMethod("stop_service"); + parent.target?.setByName("stop_service"); notifyListeners(); if (!Platform.isLinux) { // current linux is not supported @@ -228,12 +240,12 @@ class ServerModel with ChangeNotifier { } Future initInput() async { - await FFI.invokeMethod("init_input"); + await parent.target?.invokeMethod("init_input"); } Future updatePassword(String pw) async { final oldPasswd = _serverPasswd.text; - FFI.setByName("update_password", pw); + parent.target?.setByName("update_password", pw); await Future.delayed(Duration(milliseconds: 500)); await getIDPasswd(force: true); @@ -261,8 +273,8 @@ class ServerModel with ChangeNotifier { const maxCount = 10; while (count < maxCount) { await Future.delayed(Duration(seconds: 1)); - final id = FFI.getByName("server_id"); - final passwd = FFI.getByName("server_password"); + final id = parent.target?.getByName("server_id") ?? ""; + final passwd = parent.target?.getByName("server_password") ?? ""; if (id.isEmpty) { continue; } else { @@ -299,7 +311,7 @@ class ServerModel with ChangeNotifier { Map res = Map() ..["name"] = "enable-keyboard" ..["value"] = value ? '' : 'N'; - FFI.setByName('option', jsonEncode(res)); + parent.target?.setByName('option', jsonEncode(res)); } _inputOk = value; break; @@ -310,7 +322,7 @@ class ServerModel with ChangeNotifier { } updateClientState([String? json]) { - var res = json ?? FFI.getByName("clients_state"); + var res = json ?? parent.target?.getByName("clients_state") ?? ""; try { final List clientsJson = jsonDecode(res); for (var clientJson in clientsJson) { @@ -397,16 +409,16 @@ class ServerModel with ChangeNotifier { response["id"] = client.id; response["res"] = res; if (res) { - FFI.setByName("login_res", jsonEncode(response)); + parent.target?.setByName("login_res", jsonEncode(response)); if (!client.isFileTransfer) { - FFI.invokeMethod("start_capture"); + parent.target?.invokeMethod("start_capture"); } - FFI.invokeMethod("cancel_notification", client.id); + parent.target?.invokeMethod("cancel_notification", client.id); _clients[client.id]?.authorized = true; notifyListeners(); } else { - FFI.setByName("login_res", jsonEncode(response)); - FFI.invokeMethod("cancel_notification", client.id); + parent.target?.setByName("login_res", jsonEncode(response)); + parent.target?.invokeMethod("cancel_notification", client.id); _clients.remove(client.id); } } @@ -427,7 +439,7 @@ class ServerModel with ChangeNotifier { if (_clients.containsKey(id)) { _clients.remove(id); DialogManager.dismissByTag(getLoginDialogTag(id)); - FFI.invokeMethod("cancel_notification", id); + parent.target?.invokeMethod("cancel_notification", id); } notifyListeners(); } catch (e) { @@ -437,7 +449,7 @@ class ServerModel with ChangeNotifier { closeAll() { _clients.forEach((id, client) { - FFI.setByName("close_conn", id.toString()); + parent.target?.setByName("close_conn", id.toString()); }); _clients.clear(); } @@ -485,7 +497,7 @@ String getLoginDialogTag(int id) { return loginDialogTag + id.toString(); } -showInputWarnAlert() { +showInputWarnAlert(FFI ffi) { DialogManager.show((setState, close) => CustomAlertDialog( title: Text(translate("How to get Android input permission?")), content: Column( @@ -501,7 +513,7 @@ showInputWarnAlert() { ElevatedButton( child: Text(translate("Open System Setting")), onPressed: () { - FFI.serverModel.initInput(); + ffi.serverModel.initInput(); close(); }), ], diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index b4cde8caf..d82f4c367 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -5,217 +5,217 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "40.0.0" analyzer: dependency: transitive description: name: analyzer - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "4.1.0" archive: dependency: transitive description: name: archive - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.3.0" args: dependency: transitive description: name: args - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.3.1" async: dependency: transitive description: name: async - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.8.2" bitsdojo_window: dependency: "direct main" description: name: bitsdojo_window - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.1.2" bitsdojo_window_linux: dependency: transitive description: name: bitsdojo_window_linux - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.1.2" bitsdojo_window_macos: dependency: transitive description: name: bitsdojo_window_macos - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.1.2" bitsdojo_window_platform_interface: dependency: transitive description: name: bitsdojo_window_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.1.2" bitsdojo_window_windows: dependency: transitive description: name: bitsdojo_window_windows - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.1.2" boolean_selector: dependency: transitive description: name: boolean_selector - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0" build: dependency: transitive description: name: build - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.3.0" build_config: dependency: transitive description: name: build_config - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.0" build_daemon: dependency: transitive description: name: build_daemon - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.0" build_resolvers: dependency: transitive description: name: build_resolvers - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.9" build_runner: dependency: "direct dev" description: name: build_runner - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.11" build_runner_core: dependency: transitive description: name: build_runner_core - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "7.2.3" built_collection: dependency: transitive description: name: built_collection - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "5.1.1" built_value: dependency: transitive description: name: built_value - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "8.3.2" characters: dependency: transitive description: name: characters - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.0" charcode: dependency: transitive description: name: charcode - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.1" checked_yaml: dependency: transitive description: name: checked_yaml - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.1" clock: dependency: transitive description: name: clock - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.0" code_builder: dependency: transitive description: name: code_builder - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "4.1.0" collection: dependency: transitive description: name: collection - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.16.0" convert: dependency: transitive description: name: convert - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.2" cross_file: dependency: transitive description: name: cross_file - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.3.3+1" crypto: dependency: transitive description: name: crypto - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.2" cupertino_icons: dependency: "direct main" description: name: cupertino_icons - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "1.0.4" + version: "1.0.5" dart_style: dependency: transitive description: name: dart_style - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.3" dash_chat: dependency: "direct main" description: name: dash_chat - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.16" desktop_multi_window: @@ -231,133 +231,133 @@ packages: dependency: "direct main" description: name: device_info_plus - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "3.2.3" + version: "3.2.4" device_info_plus_linux: dependency: transitive description: name: device_info_plus_linux - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.1" device_info_plus_macos: dependency: transitive description: name: device_info_plus_macos - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.3" device_info_plus_platform_interface: dependency: transitive description: name: device_info_plus_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.3.0+1" device_info_plus_web: dependency: transitive description: name: device_info_plus_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0" device_info_plus_windows: dependency: transitive description: name: device_info_plus_windows - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.1" draggable_float_widget: dependency: "direct main" description: name: draggable_float_widget - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.0.2" event_bus: dependency: transitive description: name: event_bus - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.0" external_path: dependency: "direct main" description: name: external_path - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.1" fake_async: dependency: transitive description: name: fake_async - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.0" ffi: dependency: "direct main" description: name: ffi - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.1" file: dependency: transitive description: name: file - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "6.1.2" firebase_analytics: dependency: "direct main" description: name: firebase_analytics - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "9.1.9" firebase_analytics_platform_interface: dependency: transitive description: name: firebase_analytics_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.7" firebase_analytics_web: dependency: transitive description: name: firebase_analytics_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.4.0+14" firebase_core: dependency: transitive description: name: firebase_core - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.17.1" firebase_core_platform_interface: dependency: transitive description: name: firebase_core_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "4.4.0" firebase_core_web: dependency: transitive description: name: firebase_core_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.6.4" fixnum: dependency: transitive description: name: fixnum - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.1" flutter: @@ -369,28 +369,28 @@ packages: dependency: "direct main" description: name: flutter_breadcrumb - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.1" flutter_launcher_icons: dependency: "direct dev" description: name: flutter_launcher_icons - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "0.9.2" + version: "0.9.3" flutter_parsed_text: dependency: transitive description: name: flutter_parsed_text - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.1" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.6" flutter_rust_bridge: @@ -406,9 +406,9 @@ packages: dependency: "direct main" description: name: flutter_smart_dialog - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "4.3.2+1" + version: "4.5.3+2" flutter_test: dependency: "direct dev" description: flutter @@ -423,343 +423,350 @@ packages: dependency: "direct dev" description: name: freezed - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.3+1" freezed_annotation: dependency: "direct main" description: name: freezed_annotation - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.3" frontend_server_client: dependency: transitive description: name: frontend_server_client - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.3" + get: + dependency: "direct main" + description: + name: get + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.6.5" glob: dependency: transitive description: name: glob - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.2" graphs: dependency: transitive description: name: graphs - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0" http: dependency: "direct main" description: name: http - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.13.4" http_multi_server: dependency: transitive description: name: http_multi_server - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.2.0" http_parser: dependency: transitive description: name: http_parser - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "4.0.1" image: dependency: "direct main" description: name: image - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.2.0" image_picker: dependency: "direct main" description: name: image_picker - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.8.5+3" image_picker_android: dependency: transitive description: name: image_picker_android - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "0.8.4+13" + version: "0.8.5" image_picker_for_web: dependency: transitive description: name: image_picker_for_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.8" image_picker_ios: dependency: transitive description: name: image_picker_ios - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.8.5+5" image_picker_platform_interface: dependency: transitive description: name: image_picker_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.5.0" intl: dependency: transitive description: name: intl - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.17.0" io: dependency: transitive description: name: io - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.3" js: dependency: transitive description: name: js - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.6.4" json_annotation: dependency: transitive description: name: json_annotation - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "4.5.0" logging: dependency: transitive description: name: logging - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.2" matcher: dependency: transitive description: name: matcher - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.12.11" material_color_utilities: dependency: transitive description: name: material_color_utilities - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.1.4" menu_base: dependency: transitive description: name: menu_base - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.1.1" meta: dependency: transitive description: name: meta - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.7.0" mime: dependency: transitive description: name: mime - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.2" nested: dependency: transitive description: name: nested - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.0" package_config: dependency: transitive description: name: package_config - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.2" package_info_plus: dependency: "direct main" description: name: package_info_plus - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.4.2" package_info_plus_linux: dependency: transitive description: name: package_info_plus_linux - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.5" package_info_plus_macos: dependency: transitive description: name: package_info_plus_macos - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.0" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.2" package_info_plus_web: dependency: transitive description: name: package_info_plus_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.5" package_info_plus_windows: dependency: transitive description: name: package_info_plus_windows - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.5" path: dependency: transitive description: name: path - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.8.1" path_provider: dependency: "direct main" description: name: path_provider - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "2.0.10" + version: "2.0.11" path_provider_android: dependency: transitive description: name: path_provider_android - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.14" path_provider_ios: dependency: transitive description: name: path_provider_ios - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.9" path_provider_linux: dependency: transitive description: name: path_provider_linux - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.7" path_provider_macos: dependency: transitive description: name: path_provider_macos - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.6" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.4" path_provider_windows: dependency: transitive description: name: path_provider_windows - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.7" pedantic: dependency: transitive description: name: pedantic - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.11.1" petitparser: dependency: transitive description: name: petitparser - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "5.0.0" platform: dependency: transitive description: name: platform - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.0" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.2" pool: dependency: transitive description: name: pool - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.5.0" process: dependency: transitive description: name: process - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "4.2.4" provider: dependency: "direct main" description: name: provider - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "5.0.0" pub_semver: dependency: transitive description: name: pub_semver - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.1" pubspec_parse: dependency: transitive description: name: pubspec_parse - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.0" qr_code_scanner: @@ -775,98 +782,98 @@ packages: dependency: transitive description: name: quiver - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.0" screen_retriever: dependency: transitive description: name: screen_retriever - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.1.2" settings_ui: dependency: "direct main" description: name: settings_ui - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.2" shared_preferences: dependency: "direct main" description: name: shared_preferences - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.15" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.12" shared_preferences_ios: dependency: transitive description: name: shared_preferences_ios - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.1" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.1" shared_preferences_macos: dependency: transitive description: name: shared_preferences_macos - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.4" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.0" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.4" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.1" shelf: dependency: transitive description: name: shelf - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.0" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.1" shortid: dependency: transitive description: name: shortid - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.1.2" sky_engine: @@ -878,259 +885,259 @@ packages: dependency: transitive description: name: source_gen - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.2" source_span: dependency: transitive description: name: source_span - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.8.2" stack_trace: dependency: transitive description: name: stack_trace - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.10.0" stream_channel: dependency: transitive description: name: stream_channel - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0" stream_transform: dependency: transitive description: name: stream_transform - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.0" string_scanner: dependency: transitive description: name: string_scanner - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.0" term_glyph: dependency: transitive description: name: term_glyph - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.0" test_api: dependency: transitive description: name: test_api - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.4.9" timing: dependency: transitive description: name: timing - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.0" toggle_switch: dependency: "direct main" description: name: toggle_switch - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.4.0" transparent_image: dependency: transitive description: name: transparent_image - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.0" tray_manager: dependency: "direct main" description: name: tray_manager - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.1.7" tuple: dependency: "direct main" description: name: tuple - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.0" typed_data: dependency: transitive description: name: typed_data - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.1" url_launcher: dependency: "direct main" description: name: url_launcher - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "6.1.2" + version: "6.1.3" url_launcher_android: dependency: transitive description: name: url_launcher_android - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "6.0.17" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "6.0.17" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.1" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.5" url_launcher_web: dependency: transitive description: name: url_launcher_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.11" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.1" uuid: dependency: transitive description: name: uuid - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.6" vector_math: dependency: transitive description: name: vector_math - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.2" wakelock: dependency: "direct main" description: name: wakelock - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.5.6" wakelock_macos: dependency: transitive description: name: wakelock_macos - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.4.0" wakelock_platform_interface: dependency: transitive description: name: wakelock_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.3.0" wakelock_web: dependency: transitive description: name: wakelock_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.4.0" wakelock_windows: dependency: transitive description: name: wakelock_windows - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.2.0" watcher: dependency: transitive description: name: watcher - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.1" web_socket_channel: dependency: transitive description: name: web_socket_channel - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.0" win32: dependency: transitive description: name: win32 - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.6.1" window_manager: dependency: "direct main" description: name: window_manager - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.2.5" xdg_directories: dependency: transitive description: name: xdg_directories - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.2.0+1" xml: dependency: transitive description: name: xml - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "6.1.0" yaml: dependency: transitive description: name: yaml - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.1" zxing2: dependency: "direct main" description: name: zxing2 - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.1.0" sdks: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 5ff7cc6a0..65bd819ff 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -66,6 +66,7 @@ dependencies: bitsdojo_window: ^0.1.2 freezed_annotation: ^2.0.3 tray_manager: 0.1.7 + get: ^4.6.5 dev_dependencies: flutter_launcher_icons: ^0.9.1 From ed434fa90ef64fb07c1678abca31fb7314f147c0 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 17 Jun 2022 00:06:49 +0800 Subject: [PATCH 0048/2015] add: use multi provider for canvas Signed-off-by: Kingtous --- flutter/lib/desktop/pages/remote_page.dart | 118 +++++++++++---------- 1 file changed, 63 insertions(+), 55 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 5930b1f5a..30e647593 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -241,52 +241,61 @@ class _RemotePageState extends State with WindowListener { return WillPopScope( onWillPop: () async { - clientClose(); - return false; - }, - child: getRawPointerAndKeyBody( - keyboard, - Scaffold( - // resizeToAvoidBottomInset: true, - floatingActionButton: !showActionButton - ? null - : FloatingActionButton( - mini: !hideKeyboard, - child: Icon( - hideKeyboard ? Icons.expand_more : Icons.expand_less), - backgroundColor: MyTheme.accent, - onPressed: () { - setState(() { - if (hideKeyboard) { - _showEdit = false; - _ffi.invokeMethod("enable_soft_keyboard", false); - _mobileFocusNode.unfocus(); - _physicalFocusNode.requestFocus(); - } else { - _showBar = !_showBar; - } - }); - }), - bottomNavigationBar: _showBar && pi.displays.length > 0 - ? getBottomAppBar(keyboard) - : null, - body: Overlay( - initialEntries: [ - OverlayEntry(builder: (context) { - return Container( - color: Colors.black, - child: isWebDesktop - ? getBodyForDesktopWithListener(keyboard) - : SafeArea( - child: Container( - color: MyTheme.canvasColor, - child: _isPhysicalMouse - ? getBodyForMobile() - : getBodyForMobileWithGesture()))); - }) - ], - ))), - ); + clientClose(); + return false; + }, + child: MultiProvider( + providers: [ + ChangeNotifierProvider.value(value: _ffi.ffiModel), + ChangeNotifierProvider.value(value: _ffi.imageModel), + ChangeNotifierProvider.value(value: _ffi.cursorModel), + ChangeNotifierProvider.value(value: _ffi.canvasModel), + ], + child: getRawPointerAndKeyBody( + keyboard, + Scaffold( + // resizeToAvoidBottomInset: true, + floatingActionButton: !showActionButton + ? null + : FloatingActionButton( + mini: !hideKeyboard, + child: Icon(hideKeyboard + ? Icons.expand_more + : Icons.expand_less), + backgroundColor: MyTheme.accent, + onPressed: () { + setState(() { + if (hideKeyboard) { + _showEdit = false; + _ffi.invokeMethod( + "enable_soft_keyboard", false); + _mobileFocusNode.unfocus(); + _physicalFocusNode.requestFocus(); + } else { + _showBar = !_showBar; + } + }); + }), + bottomNavigationBar: _showBar && pi.displays.length > 0 + ? getBottomAppBar(keyboard) + : null, + body: Overlay( + initialEntries: [ + OverlayEntry(builder: (context) { + return Container( + color: Colors.black, + child: isWebDesktop + ? getBodyForDesktopWithListener(keyboard) + : SafeArea( + child: Container( + color: MyTheme.canvasColor, + child: _isPhysicalMouse + ? getBodyForMobile() + : getBodyForMobileWithGesture()))); + }) + ], + ))), + )); } Widget getRawPointerAndKeyBody(bool keyboard, Widget child) { @@ -916,13 +925,12 @@ class ImagePaint extends StatelessWidget { @override Widget build(BuildContext context) { - final m = ffi(this.id).imageModel; - final c = ffi(this.id).canvasModel; - final adjust = ffi(this.id).cursorModel.adjustForKeyboard(); + final m = Provider.of(context); + final c = Provider.of(context); var s = c.scale; return CustomPaint( - painter: new ImagePainter( - image: m.image, x: c.x / s, y: (c.y - adjust) / s, scale: s), + painter: + new ImagePainter(image: m.image, x: c.x / s, y: c.y / s, scale: s), ); } } @@ -934,15 +942,15 @@ class CursorPaint extends StatelessWidget { @override Widget build(BuildContext context) { - final m = ffi(this.id).cursorModel; - final c = ffi(this.id).canvasModel; - final adjust = ffi(this.id).cursorModel.adjustForKeyboard(); + final m = Provider.of(context); + final c = Provider.of(context); + // final adjust = m.adjustForKeyboard(); var s = c.scale; return CustomPaint( painter: new ImagePainter( image: m.image, x: m.x * s - m.hotx + c.x, - y: m.y * s - m.hoty + c.y - adjust, + y: m.y * s - m.hoty + c.y, scale: 1), ); } From 330a2ce5a51379ad5e44d86d9c1fa2f4228c3e17 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 17 Jun 2022 22:21:49 +0800 Subject: [PATCH 0049/2015] fix: FFI id assignment && keep Remote Page state for multi tabs Signed-off-by: Kingtous --- build.rs | 2 +- flutter/lib/desktop/pages/remote_page.dart | 57 ++++++++++++---------- flutter/lib/main.dart | 2 +- flutter/lib/models/model.dart | 3 +- 4 files changed, 35 insertions(+), 29 deletions(-) diff --git a/build.rs b/build.rs index 4d51cd297..176fa8779 100644 --- a/build.rs +++ b/build.rs @@ -77,7 +77,7 @@ fn gen_flutter_rust_bridge() { ..Default::default() }; // run fbr_codegen - lib_flutter_rust_bridge_codegen::frb_codegen(opts).unwrap(); + // lib_flutter_rust_bridge_codegen::frb_codegen(opts).unwrap(); } fn main() { diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 30e647593..9412e03c5 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -31,7 +31,8 @@ class RemotePage extends StatefulWidget { _RemotePageState createState() => _RemotePageState(); } -class _RemotePageState extends State with WindowListener { +class _RemotePageState extends State + with WindowListener, AutomaticKeepAliveClientMixin { Timer? _interval; Timer? _timer; bool _showBar = !isWebDesktop; @@ -234,13 +235,14 @@ class _RemotePageState extends State with WindowListener { @override Widget build(BuildContext context) { + super.build(context); final pi = Provider.of(context).pi; final hideKeyboard = isKeyboardShown() && _showEdit; final showActionButton = !_showBar || hideKeyboard; final keyboard = _ffi.ffiModel.permissions['keyboard'] != false; return WillPopScope( - onWillPop: () async { + onWillPop: () async { clientClose(); return false; }, @@ -254,28 +256,28 @@ class _RemotePageState extends State with WindowListener { child: getRawPointerAndKeyBody( keyboard, Scaffold( - // resizeToAvoidBottomInset: true, + // resizeToAvoidBottomInset: true, floatingActionButton: !showActionButton ? null : FloatingActionButton( - mini: !hideKeyboard, - child: Icon(hideKeyboard - ? Icons.expand_more - : Icons.expand_less), - backgroundColor: MyTheme.accent, - onPressed: () { - setState(() { - if (hideKeyboard) { - _showEdit = false; - _ffi.invokeMethod( - "enable_soft_keyboard", false); - _mobileFocusNode.unfocus(); - _physicalFocusNode.requestFocus(); - } else { - _showBar = !_showBar; - } - }); - }), + mini: !hideKeyboard, + child: Icon(hideKeyboard + ? Icons.expand_more + : Icons.expand_less), + backgroundColor: MyTheme.accent, + onPressed: () { + setState(() { + if (hideKeyboard) { + _showEdit = false; + _ffi.invokeMethod( + "enable_soft_keyboard", false); + _mobileFocusNode.unfocus(); + _physicalFocusNode.requestFocus(); + } else { + _showBar = !_showBar; + } + }); + }), bottomNavigationBar: _showBar && pi.displays.length > 0 ? getBottomAppBar(keyboard) : null, @@ -287,11 +289,11 @@ class _RemotePageState extends State with WindowListener { child: isWebDesktop ? getBodyForDesktopWithListener(keyboard) : SafeArea( - child: Container( - color: MyTheme.canvasColor, - child: _isPhysicalMouse - ? getBodyForMobile() - : getBodyForMobileWithGesture()))); + child: Container( + color: MyTheme.canvasColor, + child: _isPhysicalMouse + ? getBodyForMobile() + : getBodyForMobileWithGesture()))); }) ], ))), @@ -916,6 +918,9 @@ class _RemotePageState extends State with WindowListener { break; } } + + @override + bool get wantKeepAlive => true; } class ImagePaint extends StatelessWidget { diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 2707d9535..e4a75244f 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -22,7 +22,7 @@ Future main(List args) async { // global FFI, use this **ONLY** for global configuration // for convenience, use global FFI on mobile platform // focus on multi-ffi on desktop first - initGlobalFFI(); + await initGlobalFFI(); // await Firebase.initializeApp(); if (isAndroid) { toAndroidChannelInit(); diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 85bdc13b7..5c383f774 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -913,7 +913,7 @@ class FFI { }(); // every instance will bind a stream } - id = id; + this.id = id; } /// Login with [password], choose if the client should [remember] it. @@ -935,6 +935,7 @@ class FFI { ffiModel.clear(); canvasModel.clear(); resetModifiers(); + print("model closed"); } /// Send **get** command to the Rust core based on [name] and [arg]. From 77b86ddb6b5b79c1522242b2786b2711d46487bc Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 17 Jun 2022 22:57:41 +0800 Subject: [PATCH 0050/2015] add: file transfer multi tab support Signed-off-by: Kingtous --- build.rs | 2 +- .../lib/desktop/pages/connection_page.dart | 26 +- .../lib/desktop/pages/file_manager_page.dart | 572 ++++++++++++++++++ .../desktop/pages/file_manager_tab_page.dart | 122 ++++ .../screen/desktop_file_transfer_screen.dart | 46 ++ flutter/lib/main.dart | 7 +- flutter/lib/utils/multi_window_manager.dart | 26 + 7 files changed, 778 insertions(+), 23 deletions(-) create mode 100644 flutter/lib/desktop/pages/file_manager_page.dart create mode 100644 flutter/lib/desktop/pages/file_manager_tab_page.dart create mode 100644 flutter/lib/desktop/screen/desktop_file_transfer_screen.dart diff --git a/build.rs b/build.rs index 176fa8779..4d51cd297 100644 --- a/build.rs +++ b/build.rs @@ -77,7 +77,7 @@ fn gen_flutter_rust_bridge() { ..Default::default() }; // run fbr_codegen - // lib_flutter_rust_bridge_codegen::frb_codegen(opts).unwrap(); + lib_flutter_rust_bridge_codegen::frb_codegen(opts).unwrap(); } fn main() { diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 78d73daee..be415eb80 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter_hbb/mobile/pages/file_manager_page.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -79,21 +78,8 @@ class _ConnectionPageState extends State { } } } - Navigator.push( - context, - MaterialPageRoute( - builder: (BuildContext context) => FileManagerPage(id: id), - ), - ); + await rustDeskWinManager.new_file_transfer(id); } else { - // single window - // Navigator.push( - // context, - // MaterialPageRoute( - // builder: (BuildContext context) => RemotePage(id: id), - // ), - // ); - // multi window await rustDeskWinManager.new_remote_desktop(id); } FocusScopeNode currentFocus = FocusScope.of(context); @@ -307,12 +293,10 @@ class _ConnectionPageState extends State { PopupMenuItem( child: Text(translate('Remove')), value: 'remove') ] + - (!isAndroid - ? [] - : [ - PopupMenuItem( - child: Text(translate('File transfer')), value: 'file') - ]), + ([ + PopupMenuItem( + child: Text(translate('File transfer')), value: 'file') + ]), elevation: 8, ); if (value == 'remove') { diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart new file mode 100644 index 000000000..162e9d720 --- /dev/null +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -0,0 +1,572 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_breadcrumb/flutter_breadcrumb.dart'; +import 'package:flutter_hbb/mobile/pages/file_manager_page.dart'; +import 'package:flutter_hbb/models/file_model.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; +import 'package:provider/provider.dart'; +import 'package:toggle_switch/toggle_switch.dart'; +import 'package:wakelock/wakelock.dart'; + +import '../../common.dart'; +import '../../mobile/widgets/dialog.dart'; +import '../../models/model.dart'; + +class FileManagerPage extends StatefulWidget { + FileManagerPage({Key? key, required this.id}) : super(key: key); + final String id; + + @override + State createState() => _FileManagerPageState(); +} + +class _FileManagerPageState extends State + with AutomaticKeepAliveClientMixin { + final _selectedItems = SelectedItems(); + final _breadCrumbScroller = ScrollController(); + + /// FFI with name file_transfer_id + FFI get _ffi => ffi('ft_${widget.id}'); + + FileModel get model => _ffi.fileModel; + + @override + void initState() { + super.initState(); + Get.put(FFI(), tag: 'ft_${widget.id}'); + _ffi.ffiModel.platformFFI = gFFI.ffiModel.platformFFI; + + _ffi.connect(widget.id, isFileTransfer: true); + _ffi.ffiModel.updateEventListener(widget.id); + if (!Platform.isLinux) { + Wakelock.enable(); + } + } + + @override + void dispose() { + model.onClose(); + _ffi.close(); + SmartDialog.dismiss(); + if (!Platform.isLinux) { + Wakelock.disable(); + } + Get.delete(tag: 'ft_${widget.id}'); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + super.build(context); + return ChangeNotifierProvider.value( + value: _ffi.fileModel, + child: Consumer(builder: (_context, _model, _child) { + return WillPopScope( + onWillPop: () async { + if (model.selectMode) { + model.toggleSelectMode(); + } else { + goBack(); + } + return false; + }, + child: Scaffold( + backgroundColor: MyTheme.grayBg, + appBar: AppBar( + leading: Row(children: [ + IconButton(icon: Icon(Icons.close), onPressed: clientClose), + ]), + centerTitle: true, + title: ToggleSwitch( + initialLabelIndex: model.isLocal ? 0 : 1, + activeBgColor: [MyTheme.idColor], + inactiveBgColor: MyTheme.grayBg, + inactiveFgColor: Colors.black54, + totalSwitches: 2, + minWidth: 100, + fontSize: 15, + iconSize: 18, + labels: [translate("Local"), translate("Remote")], + icons: [Icons.phone_android_sharp, Icons.screen_share], + onToggle: (index) { + final current = model.isLocal ? 0 : 1; + if (index != current) { + model.togglePage(); + } + }, + ), + actions: [ + PopupMenuButton( + icon: Icon(Icons.more_vert), + itemBuilder: (context) { + return [ + PopupMenuItem( + child: Row( + children: [ + Icon(Icons.refresh, color: Colors.black), + SizedBox(width: 5), + Text(translate("Refresh File")) + ], + ), + value: "refresh", + ), + PopupMenuItem( + child: Row( + children: [ + Icon(Icons.check, color: Colors.black), + SizedBox(width: 5), + Text(translate("Multi Select")) + ], + ), + value: "select", + ), + PopupMenuItem( + child: Row( + children: [ + Icon(Icons.folder_outlined, + color: Colors.black), + SizedBox(width: 5), + Text(translate("Create Folder")) + ], + ), + value: "folder", + ), + PopupMenuItem( + child: Row( + children: [ + Icon( + model.currentShowHidden + ? Icons.check_box_outlined + : Icons.check_box_outline_blank, + color: Colors.black), + SizedBox(width: 5), + Text(translate("Show Hidden Files")) + ], + ), + value: "hidden", + ) + ]; + }, + onSelected: (v) { + if (v == "refresh") { + model.refresh(); + } else if (v == "select") { + _selectedItems.clear(); + model.toggleSelectMode(); + } else if (v == "folder") { + final name = TextEditingController(); + DialogManager.show((setState, close) => + CustomAlertDialog( + title: Text(translate("Create Folder")), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextFormField( + decoration: InputDecoration( + labelText: translate( + "Please enter the folder name"), + ), + controller: name, + ), + ], + ), + actions: [ + TextButton( + style: flatButtonStyle, + onPressed: () => close(false), + child: Text(translate("Cancel"))), + ElevatedButton( + style: flatButtonStyle, + onPressed: () { + if (name.value.text.isNotEmpty) { + model.createDir(PathUtil.join( + model.currentDir.path, + name.value.text, + model.currentIsWindows)); + close(); + } + }, + child: Text(translate("OK"))) + ])); + } else if (v == "hidden") { + model.toggleShowHidden(); + } + }), + ], + ), + body: body(), + bottomSheet: bottomSheet(), + )); + })); + } + + bool needShowCheckBox() { + if (!model.selectMode) { + return false; + } + return !_selectedItems.isOtherPage(model.isLocal); + } + + Widget body() { + final isLocal = model.isLocal; + final fd = model.currentDir; + final entries = fd.entries; + return Column(children: [ + headTools(), + Expanded( + child: ListView.builder( + itemCount: entries.length + 1, + itemBuilder: (context, index) { + if (index >= entries.length) { + return listTail(); + } + var selected = false; + if (model.selectMode) { + selected = _selectedItems.contains(entries[index]); + } + + final sizeStr = entries[index].isFile + ? readableFileSize(entries[index].size.toDouble()) + : ""; + return Card( + child: ListTile( + leading: Icon( + entries[index].isFile ? Icons.feed_outlined : Icons.folder, + size: 40), + title: Text(entries[index].name), + selected: selected, + subtitle: Text( + entries[index] + .lastModified() + .toString() + .replaceAll(".000", "") + + " " + + sizeStr, + style: TextStyle(fontSize: 12, color: MyTheme.darkGray), + ), + trailing: needShowCheckBox() + ? Checkbox( + value: selected, + onChanged: (v) { + if (v == null) return; + if (v && !selected) { + _selectedItems.add(isLocal, entries[index]); + } else if (!v && selected) { + _selectedItems.remove(entries[index]); + } + setState(() {}); + }) + : PopupMenuButton( + icon: Icon(Icons.more_vert), + itemBuilder: (context) { + return [ + PopupMenuItem( + child: Text(translate("Delete")), + value: "delete", + ), + PopupMenuItem( + child: Text(translate("Multi Select")), + value: "multi_select", + ), + PopupMenuItem( + child: Text(translate("Properties")), + value: "properties", + enabled: false, + ) + ]; + }, + onSelected: (v) { + if (v == "delete") { + final items = SelectedItems(); + items.add(isLocal, entries[index]); + model.removeAction(items); + } else if (v == "multi_select") { + _selectedItems.clear(); + model.toggleSelectMode(); + } + }), + onTap: () { + if (model.selectMode && !_selectedItems.isOtherPage(isLocal)) { + if (selected) { + _selectedItems.remove(entries[index]); + } else { + _selectedItems.add(isLocal, entries[index]); + } + setState(() {}); + return; + } + if (entries[index].isDirectory) { + model.openDirectory(entries[index].path); + breadCrumbScrollToEnd(); + } else { + // Perform file-related tasks. + } + }, + onLongPress: () { + _selectedItems.clear(); + model.toggleSelectMode(); + if (model.selectMode) { + _selectedItems.add(isLocal, entries[index]); + } + setState(() {}); + }, + ), + ); + }, + )) + ]); + } + + goBack() { + model.goToParentDirectory(); + } + + breadCrumbScrollToEnd() { + Future.delayed(Duration(milliseconds: 200), () { + _breadCrumbScroller.animateTo( + _breadCrumbScroller.position.maxScrollExtent, + duration: Duration(milliseconds: 200), + curve: Curves.fastLinearToSlowEaseIn); + }); + } + + Widget headTools() => Container( + child: Row( + children: [ + Expanded( + child: BreadCrumb( + items: getPathBreadCrumbItems(() => model.goHome(), (list) { + var path = ""; + if (model.currentHome.startsWith(list[0])) { + // absolute path + for (var item in list) { + path = PathUtil.join(path, item, model.currentIsWindows); + } + } else { + path += model.currentHome; + for (var item in list) { + path = PathUtil.join(path, item, model.currentIsWindows); + } + } + model.openDirectory(path); + }), + divider: Icon(Icons.chevron_right), + overflow: ScrollableOverflow(controller: _breadCrumbScroller), + )), + Row( + children: [ + IconButton( + icon: Icon(Icons.arrow_upward), + onPressed: goBack, + ), + PopupMenuButton( + icon: Icon(Icons.sort), + itemBuilder: (context) { + return SortBy.values + .map((e) => PopupMenuItem( + child: + Text(translate(e.toString().split(".").last)), + value: e, + )) + .toList(); + }, + onSelected: model.changeSortStyle), + ], + ) + ], + )); + + Widget listTail() { + return Container( + height: 100, + child: Column( + children: [ + Padding( + padding: EdgeInsets.fromLTRB(30, 5, 30, 0), + child: Text( + model.currentDir.path, + style: TextStyle(color: MyTheme.darkGray), + ), + ), + Padding( + padding: EdgeInsets.all(2), + child: Text( + "${translate("Total")}: ${model.currentDir.entries.length} ${translate("items")}", + style: TextStyle(color: MyTheme.darkGray), + ), + ) + ], + ), + ); + } + + Widget? bottomSheet() { + final state = model.jobState; + final isOtherPage = _selectedItems.isOtherPage(model.isLocal); + final selectedItemsLen = "${_selectedItems.length} ${translate("items")}"; + final local = _selectedItems.isLocal == null + ? "" + : " [${_selectedItems.isLocal! ? translate("Local") : translate("Remote")}]"; + + if (model.selectMode) { + if (_selectedItems.length == 0 || !isOtherPage) { + return BottomSheetBody( + leading: Icon(Icons.check), + title: translate("Selected"), + text: selectedItemsLen + local, + onCanceled: () => model.toggleSelectMode(), + actions: [ + IconButton( + icon: Icon(Icons.compare_arrows), + onPressed: model.togglePage, + ), + IconButton( + icon: Icon(Icons.delete_forever), + onPressed: () { + if (_selectedItems.length > 0) { + model.removeAction(_selectedItems); + } + }, + ) + ]); + } else { + return BottomSheetBody( + leading: Icon(Icons.input), + title: translate("Paste here?"), + text: selectedItemsLen + local, + onCanceled: () => model.toggleSelectMode(), + actions: [ + IconButton( + icon: Icon(Icons.compare_arrows), + onPressed: model.togglePage, + ), + IconButton( + icon: Icon(Icons.paste), + onPressed: () { + model.toggleSelectMode(); + model.sendFiles(_selectedItems); + }, + ) + ]); + } + } + + switch (state) { + case JobState.inProgress: + return BottomSheetBody( + leading: CircularProgressIndicator(), + title: translate("Waiting"), + text: + "${translate("Speed")}: ${readableFileSize(model.jobProgress.speed)}/s", + onCanceled: () => model.cancelJob(model.jobProgress.id), + ); + case JobState.done: + return BottomSheetBody( + leading: Icon(Icons.check), + title: "${translate("Successful")}!", + text: "", + onCanceled: () => model.jobReset(), + ); + case JobState.error: + return BottomSheetBody( + leading: Icon(Icons.error), + title: "${translate("Error")}!", + text: "", + onCanceled: () => model.jobReset(), + ); + case JobState.none: + break; + } + return null; + } + + List getPathBreadCrumbItems( + void Function() onHome, void Function(List) onPressed) { + final path = model.currentShortPath; + final list = PathUtil.split(path, model.currentIsWindows); + final breadCrumbList = [ + BreadCrumbItem( + content: IconButton( + icon: Icon(Icons.home_filled), + onPressed: onHome, + )) + ]; + breadCrumbList.addAll(list.asMap().entries.map((e) => BreadCrumbItem( + content: TextButton( + child: Text(e.value), + style: + ButtonStyle(minimumSize: MaterialStateProperty.all(Size(0, 0))), + onPressed: () => onPressed(list.sublist(0, e.key + 1)))))); + return breadCrumbList; + } + + @override + bool get wantKeepAlive => true; +} + +class BottomSheetBody extends StatelessWidget { + BottomSheetBody( + {required this.leading, + required this.title, + required this.text, + this.onCanceled, + this.actions}); + + final Widget leading; + final String title; + final String text; + final VoidCallback? onCanceled; + final List? actions; + + @override + BottomSheet build(BuildContext context) { + final _actions = actions ?? []; + return BottomSheet( + builder: (BuildContext context) { + return Container( + height: 65, + alignment: Alignment.centerLeft, + decoration: BoxDecoration( + color: MyTheme.accent50, + borderRadius: BorderRadius.vertical(top: Radius.circular(10))), + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 15), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + leading, + SizedBox(width: 16), + Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: TextStyle(fontSize: 18)), + Text(text, + style: TextStyle( + fontSize: 14, color: MyTheme.grayBg)) + ], + ) + ], + ), + Row(children: () { + _actions.add(IconButton( + icon: Icon(Icons.cancel_outlined), + onPressed: onCanceled, + )); + return _actions; + }()) + ], + ), + )); + }, + onClosing: () {}, + backgroundColor: MyTheme.grayBg, + enableDrag: false, + ); + } +} diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart new file mode 100644 index 000000000..6c945aede --- /dev/null +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -0,0 +1,122 @@ +import 'dart:convert'; +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/desktop/pages/file_manager_page.dart'; +import 'package:flutter_hbb/desktop/widgets/titlebar_widget.dart'; +import 'package:flutter_hbb/utils/multi_window_manager.dart'; + +/// File Transfer for multi tabs +class FileManagerTabPage extends StatefulWidget { + final Map params; + + const FileManagerTabPage({Key? key, required this.params}) : super(key: key); + + @override + State createState() => _FileManagerTabPageState(params); +} + +class _FileManagerTabPageState extends State + with SingleTickerProviderStateMixin { + // refactor List when using multi-tab + // this singleton is only for test + List connectionIds = List.empty(growable: true); + var initialIndex = 0; + + _FileManagerTabPageState(Map params) { + if (params['id'] != null) { + connectionIds.add(params['id']); + } + } + + @override + void initState() { + super.initState(); + rustDeskWinManager.setMethodHandler((call, fromWindowId) async { + print( + "call ${call.method} with args ${call.arguments} from window ${fromWindowId}"); + // for simplify, just replace connectionId + if (call.method == "new_file_transfer") { + setState(() { + final args = jsonDecode(call.arguments); + final id = args['id']; + final indexOf = connectionIds.indexOf(id); + if (indexOf >= 0) { + setState(() { + initialIndex = indexOf; + }); + } else { + connectionIds.add(id); + setState(() { + initialIndex = connectionIds.length - 1; + }); + } + }); + } + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: DefaultTabController( + initialIndex: initialIndex, + length: connectionIds.length, + animationDuration: Duration.zero, + child: Column( + children: [ + DesktopTitleBar( + child: TabBar( + isScrollable: true, + labelColor: Colors.white, + physics: NeverScrollableScrollPhysics(), + indicatorColor: Colors.white, + tabs: connectionIds + .map((e) => Tab( + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(e), + SizedBox( + width: 4, + ), + InkWell( + onTap: () { + onRemoveId(e); + }, + child: Icon( + Icons.highlight_remove, + size: 20, + )) + ], + ), + )) + .toList()), + ), + Expanded( + child: TabBarView( + children: connectionIds + .map((e) => Container( + child: FileManagerPage( + key: ValueKey(e), + id: e))) //RemotePage(key: ValueKey(e), id: e)) + .toList()), + ) + ], + ), + ), + ); + } + + void onRemoveId(String id) { + final indexOf = connectionIds.indexOf(id); + if (indexOf == -1) { + return; + } + setState(() { + connectionIds.removeAt(indexOf); + initialIndex = max(0, initialIndex - 1); + }); + } +} diff --git a/flutter/lib/desktop/screen/desktop_file_transfer_screen.dart b/flutter/lib/desktop/screen/desktop_file_transfer_screen.dart new file mode 100644 index 000000000..06a71981e --- /dev/null +++ b/flutter/lib/desktop/screen/desktop_file_transfer_screen.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/desktop/pages/file_manager_tab_page.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:provider/provider.dart'; + +/// multi-tab file transfer remote screen +class DesktopFileTransferScreen extends StatelessWidget { + final Map params; + + const DesktopFileTransferScreen({Key? key, required this.params}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return MultiProvider( + providers: [ + ChangeNotifierProvider.value(value: gFFI.ffiModel), + ChangeNotifierProvider.value(value: gFFI.imageModel), + ChangeNotifierProvider.value(value: gFFI.cursorModel), + ChangeNotifierProvider.value(value: gFFI.canvasModel), + ], + child: MaterialApp( + navigatorKey: globalKey, + debugShowCheckedModeBanner: false, + title: 'RustDesk - File Transfer', + theme: ThemeData( + primarySwatch: Colors.blue, + visualDensity: VisualDensity.adaptivePlatformDensity, + ), + home: FileManagerTabPage( + params: params, + ), + navigatorObservers: [ + // FirebaseAnalyticsObserver(analytics: analytics), + FlutterSmartDialog.observer + ], + builder: FlutterSmartDialog.init( + builder: isAndroid + ? (_, child) => AccessibilityListener( + child: child, + ) + : null)), + ); + } +} diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index e4a75244f..898274337 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:bitsdojo_window/bitsdojo_window.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart'; +import 'package:flutter_hbb/desktop/screen/desktop_file_transfer_screen.dart'; import 'package:flutter_hbb/desktop/screen/desktop_remote_screen.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; @@ -51,6 +52,9 @@ void runRustDeskApp(List args) async { params: argument, )); break; + case WindowType.FileTransfer: + runApp(DesktopFileTransferScreen(params: argument)); + break; default: break; } @@ -75,7 +79,8 @@ class App extends StatelessWidget { // final analytics = FirebaseAnalytics.instance; return MultiProvider( providers: [ - // TODO remove it, only for compile + // global configuration + // use session related FFI when in remote control or file transfer page ChangeNotifierProvider.value(value: gFFI.ffiModel), ChangeNotifierProvider.value(value: gFFI.imageModel), ChangeNotifierProvider.value(value: gFFI.cursorModel), diff --git a/flutter/lib/utils/multi_window_manager.dart b/flutter/lib/utils/multi_window_manager.dart index 81944e648..5c522f3a5 100644 --- a/flutter/lib/utils/multi_window_manager.dart +++ b/flutter/lib/utils/multi_window_manager.dart @@ -33,6 +33,7 @@ class RustDeskMultiWindowManager { static final instance = RustDeskMultiWindowManager._(); int? _remoteDesktopWindowId; + int? _fileTransferWindowId; Future new_remote_desktop(String remote_id) async { final msg = @@ -60,6 +61,31 @@ class RustDeskMultiWindowManager { } } + Future new_file_transfer(String remote_id) async { + final msg = + jsonEncode({"type": WindowType.FileTransfer.index, "id": remote_id}); + + try { + final ids = await DesktopMultiWindow.getAllSubWindowIds(); + if (!ids.contains(_fileTransferWindowId)) { + _fileTransferWindowId = null; + } + } on Error { + _fileTransferWindowId = null; + } + if (_fileTransferWindowId == null) { + final fileTransferController = await DesktopMultiWindow.createWindow(msg); + fileTransferController + ..setFrame(const Offset(0, 0) & const Size(1280, 720)) + ..center() + ..setTitle("rustdesk - file transfer") + ..show(); + _fileTransferWindowId = fileTransferController.windowId; + } else { + return call(WindowType.FileTransfer, "new_file_transfer", msg); + } + } + Future call(WindowType type, String methodName, dynamic args) async { int? windowId = findWindowByType(type); if (windowId == null) { From 0eacb6706a28cac89ef3d660a2adc26e8f046bdc Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 21 Jun 2022 17:58:27 +0800 Subject: [PATCH 0051/2015] feat: file transfer tab works Signed-off-by: Kingtous --- .../lib/desktop/pages/file_manager_page.dart | 9 ++-- flutter/lib/models/file_model.dart | 25 ++++++++-- flutter/lib/models/model.dart | 49 +++++++++++-------- flutter/lib/models/native_model.dart | 7 ++- flutter/macos/Runner/bridge_generated.h | 6 +++ src/common.rs | 5 +- src/flutter_ffi.rs | 45 ++++++++++------- 7 files changed, 93 insertions(+), 53 deletions(-) diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 162e9d720..0deb6741d 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -36,14 +36,13 @@ class _FileManagerPageState extends State @override void initState() { super.initState(); - Get.put(FFI(), tag: 'ft_${widget.id}'); - _ffi.ffiModel.platformFFI = gFFI.ffiModel.platformFFI; - - _ffi.connect(widget.id, isFileTransfer: true); - _ffi.ffiModel.updateEventListener(widget.id); + Get.put(FFI.newFFI()..connect(widget.id, isFileTransfer: true), + tag: 'ft_${widget.id}'); + // _ffi.ffiModel.updateEventListener(widget.id); if (!Platform.isLinux) { Wakelock.enable(); } + print("init success with id ${widget.id}"); } @override diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index 0f7ce0df2..aefdcf639 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -519,6 +519,10 @@ class FileModel extends ChangeNotifier { _currentRemoteDir.changeSortStyle(sort); notifyListeners(); } + + initFileFetcher() { + _fileFetcher.id = _ffi.target?.id; + } } class JobResultListener { @@ -566,6 +570,17 @@ class FileFetcher { Map> remoteTasks = Map(); Map> readRecursiveTasks = Map(); + String? _id; + + String? get id => _id; + + set id(String? id) { + _id = id; + } + + // if id == null, means to fetch global FFI + FFI get _ffi => ffi(_id == null ? "" : 'ft_${_id}'); + Future registerReadTask(bool isLocal, String path) { // final jobs = isLocal?localJobs:remoteJobs; // maybe we will use read local dir async later final tasks = remoteTasks; // bypass now @@ -633,13 +648,14 @@ class FileFetcher { Future fetchDirectory( String path, bool isLocal, bool showHidden) async { try { - final msg = {"path": path, "show_hidden": showHidden.toString()}; if (isLocal) { - final res = gFFI.getByName("read_local_dir_sync", jsonEncode(msg)); + final res = await _ffi.bind.sessionReadLocalDirSync( + id: id ?? "", path: path, showHidden: showHidden); final fd = FileDirectory.fromJson(jsonDecode(res)); return fd; } else { - gFFI.setByName("read_remote_dir", jsonEncode(msg)); + await _ffi.bind.sessionReadRemoteDir( + id: id ?? "", path: path, includeHidden: showHidden); return registerReadTask(isLocal, path); } } catch (e) { @@ -657,7 +673,8 @@ class FileFetcher { "show_hidden": showHidden.toString(), "is_remote": (!isLocal).toString() }; - gFFI.setByName("read_dir_recursive", jsonEncode(msg)); + // TODO + _ffi.setByName("read_dir_recursive", jsonEncode(msg)); return registerReadRecursiveTask(id); } catch (e) { return Future.error(e); diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 5c383f774..0332dc797 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -784,6 +784,13 @@ class FFI { this.fileModel = FileModel(WeakReference(this)); } + static FFI newFFI() { + final ffi = FFI(); + // keep platformFFI only once + ffi.ffiModel.platformFFI = gFFI.ffiModel.platformFFI; + return ffi; + } + /// Get the remote id for current client. String getId() { return getByName('remote_id'); // TODO @@ -888,32 +895,32 @@ class FFI { setByName('connect_file_transfer', id); } else { chatModel.resetClientMode(); - // setByName('connect', id); - // TODO multi model instances canvasModel.id = id; imageModel._id = id; cursorModel.id = id; - final stream = - bind.sessionConnect(id: id, isFileTransfer: isFileTransfer); - final cb = ffiModel.startEventListener(id); - () async { - await for (final message in stream) { - if (message is Event) { - try { - debugPrint("event:${message.field0}"); - Map event = json.decode(message.field0); - cb(event); - } catch (e) { - print('json.decode fail(): $e'); - } - } else if (message is Rgba) { - imageModel.onRgba(message.field0); - } - } - }(); - // every instance will bind a stream } + final stream = bind.sessionConnect(id: id, isFileTransfer: isFileTransfer); + final cb = ffiModel.startEventListener(id); + () async { + await for (final message in stream) { + if (message is Event) { + try { + debugPrint("event:${message.field0}"); + Map event = json.decode(message.field0); + cb(event); + } catch (e) { + print('json.decode fail(): $e'); + } + } else if (message is Rgba) { + imageModel.onRgba(message.field0); + } + } + }(); + // every instance will bind a stream this.id = id; + if (isFileTransfer) { + this.fileModel.initFileFetcher(); + } } /// Login with [password], choose if the client should [remember] it. diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index 1ae523b8b..c0fd4dfa1 100644 --- a/flutter/lib/models/native_model.dart +++ b/flutter/lib/models/native_model.dart @@ -93,7 +93,12 @@ class PlatformFFI { _ffiBind = RustdeskImpl(dylib); _startListenEvent(_ffiBind); // global event try { - _homeDir = (await ExternalPath.getExternalStorageDirectories())[0]; + if (isAndroid) { + // only support for android + _homeDir = (await ExternalPath.getExternalStorageDirectories())[0]; + } else { + _homeDir = (await getDownloadsDirectory())?.path ?? ""; + } } catch (e) { print(e); } diff --git a/flutter/macos/Runner/bridge_generated.h b/flutter/macos/Runner/bridge_generated.h index 20f318836..6eb6cbd51 100644 --- a/flutter/macos/Runner/bridge_generated.h +++ b/flutter/macos/Runner/bridge_generated.h @@ -149,6 +149,11 @@ void wire_session_create_dir(int64_t port_, struct wire_uint_8_list *path, bool is_remote); +void wire_session_read_local_dir_sync(int64_t port_, + struct wire_uint_8_list *id, + struct wire_uint_8_list *path, + bool show_hidden); + struct wire_uint_8_list *new_uint_8_list(int32_t len); void free_WireSyncReturnStruct(struct WireSyncReturnStruct val); @@ -194,6 +199,7 @@ static int64_t dummy_method_to_enforce_bundling(void) { dummy_var ^= ((int64_t) (void*) wire_session_remove_all_empty_dirs); dummy_var ^= ((int64_t) (void*) wire_session_cancel_job); dummy_var ^= ((int64_t) (void*) wire_session_create_dir); + dummy_var ^= ((int64_t) (void*) wire_session_read_local_dir_sync); dummy_var ^= ((int64_t) (void*) new_uint_8_list); dummy_var ^= ((int64_t) (void*) free_WireSyncReturnStruct); dummy_var ^= ((int64_t) (void*) store_dart_post_cobject); diff --git a/src/common.rs b/src/common.rs index 03e5f4f4b..b3141a7fe 100644 --- a/src/common.rs +++ b/src/common.rs @@ -24,10 +24,9 @@ lazy_static::lazy_static! { pub static ref SOFTWARE_UPDATE_URL: Arc> = Default::default(); } -#[cfg(any(target_os = "android", target_os = "ios"))] lazy_static::lazy_static! { - pub static ref MOBILE_INFO1: Arc> = Default::default(); - pub static ref MOBILE_INFO2: Arc> = Default::default(); + pub static ref FLUTTER_INFO1: Arc> = Default::default(); + pub static ref FLUTTER_INFO2: Arc> = Default::default(); } #[inline] diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 25c37d418..055e62721 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -307,6 +307,15 @@ pub fn session_create_dir(id: String, act_id: i32, path: String, is_remote: bool } } +pub fn session_read_local_dir_sync(id: String, path: String, show_hidden: bool) -> String { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + if let Ok(fd) = fs::read_dir(&fs::get_path(&path), show_hidden) { + return make_fd_to_json(fd); + } + } + "".to_string() +} + /// FFI for **get** commands which are idempotent. /// Return result in c string. /// @@ -397,21 +406,21 @@ unsafe extern "C" fn get_by_name(name: *const c_char, arg: *const c_char) -> *co "get_home_dir" => { res = fs::get_home_as_string(); } - "read_local_dir_sync" => { - if let Ok(value) = arg.to_str() { - if let Ok(m) = serde_json::from_str::>(value) { - if let (Some(path), Some(show_hidden)) = - (m.get("path"), m.get("show_hidden")) - { - if let Ok(fd) = - fs::read_dir(&fs::get_path(path), show_hidden.eq("true")) - { - res = make_fd_to_json(fd); - } - } - } - } - } + // "read_local_dir_sync" => { + // if let Ok(value) = arg.to_str() { + // if let Ok(m) = serde_json::from_str::>(value) { + // if let (Some(path), Some(show_hidden)) = + // (m.get("path"), m.get("show_hidden")) + // { + // if let Ok(fd) = + // fs::read_dir(&fs::get_path(path), show_hidden.eq("true")) + // { + // res = make_fd_to_json(fd); + // } + // } + // } + // } + // } // Server Side #[cfg(not(any(target_os = "ios")))] "clients_state" => { @@ -452,13 +461,11 @@ unsafe extern "C" fn set_by_name(name: *const c_char, value: *const c_char) { "init" => { initialize(value); } - #[cfg(any(target_os = "android", target_os = "ios"))] "info1" => { - *crate::common::MOBILE_INFO1.lock().unwrap() = value.to_owned(); + *crate::common::FLUTTER_INFO1.lock().unwrap() = value.to_owned(); } - #[cfg(any(target_os = "android", target_os = "ios"))] "info2" => { - *crate::common::MOBILE_INFO2.lock().unwrap() = value.to_owned(); + *crate::common::FLUTTER_INFO2.lock().unwrap() = value.to_owned(); } // "connect" => { // Session::start(value, false); From 02aa676030ef6a3637490225df0810bb54690967 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 21 Jun 2022 17:58:42 +0800 Subject: [PATCH 0052/2015] opt: add init frame size Signed-off-by: Kingtous --- flutter/linux/my_application.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/linux/my_application.cc b/flutter/linux/my_application.cc index f726dd76c..8c7a5fe05 100644 --- a/flutter/linux/my_application.cc +++ b/flutter/linux/my_application.cc @@ -50,7 +50,7 @@ static void my_application_activate(GApplication* application) { auto bdw = bitsdojo_window_from(window); // <--- add this line bdw->setCustomFrame(true); // <-- add this line - //gtk_window_set_default_size(window, 1280, 720); // <-- comment this line + gtk_window_set_default_size(window, 1280, 720); // <-- comment this line gtk_widget_show(GTK_WIDGET(window)); g_autoptr(FlDartProject) project = fl_dart_project_new(); From 5bfbb1b807d3b98892329096f3971b816a0b8a39 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 21 Jun 2022 18:14:44 +0800 Subject: [PATCH 0053/2015] opt: dual columns file-transfer in desktop version Signed-off-by: Kingtous --- .../lib/desktop/pages/file_manager_page.dart | 49 ++++++++++--------- flutter/lib/models/file_model.dart | 7 ++- 2 files changed, 32 insertions(+), 24 deletions(-) diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 0deb6741d..e9e69556c 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -8,7 +8,6 @@ import 'package:flutter_hbb/models/file_model.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; -import 'package:toggle_switch/toggle_switch.dart'; import 'package:wakelock/wakelock.dart'; import '../../common.dart'; @@ -79,24 +78,24 @@ class _FileManagerPageState extends State IconButton(icon: Icon(Icons.close), onPressed: clientClose), ]), centerTitle: true, - title: ToggleSwitch( - initialLabelIndex: model.isLocal ? 0 : 1, - activeBgColor: [MyTheme.idColor], - inactiveBgColor: MyTheme.grayBg, - inactiveFgColor: Colors.black54, - totalSwitches: 2, - minWidth: 100, - fontSize: 15, - iconSize: 18, - labels: [translate("Local"), translate("Remote")], - icons: [Icons.phone_android_sharp, Icons.screen_share], - onToggle: (index) { - final current = model.isLocal ? 0 : 1; - if (index != current) { - model.togglePage(); - } - }, - ), + // title: ToggleSwitch( + // initialLabelIndex: model.isLocal ? 0 : 1, + // activeBgColor: [MyTheme.idColor], + // inactiveBgColor: MyTheme.grayBg, + // inactiveFgColor: Colors.black54, + // totalSwitches: 2, + // minWidth: 100, + // fontSize: 15, + // iconSize: 18, + // labels: [translate("Local"), translate("Remote")], + // icons: [Icons.phone_android_sharp, Icons.screen_share], + // onToggle: (index) { + // final current = model.isLocal ? 0 : 1; + // if (index != current) { + // model.togglePage(); + // } + // }, + // ), actions: [ PopupMenuButton( icon: Icon(Icons.more_vert), @@ -196,7 +195,12 @@ class _FileManagerPageState extends State }), ], ), - body: body(), + body: Row( + children: [ + Flexible(flex: 1, child: body(isLocal: true)), + Flexible(flex: 1, child: body(isLocal: false)) + ], + ), bottomSheet: bottomSheet(), )); })); @@ -209,9 +213,8 @@ class _FileManagerPageState extends State return !_selectedItems.isOtherPage(model.isLocal); } - Widget body() { - final isLocal = model.isLocal; - final fd = model.currentDir; + Widget body({bool isLocal = false}) { + final fd = isLocal ? model.currentLocalDir : model.currentRemoteDir; final entries = fd.entries; return Column(children: [ headTools(), diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index aefdcf639..9ed8b54d7 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -233,7 +233,12 @@ class FileModel extends ChangeNotifier { } refresh() { - openDirectory(currentDir.path); + if (isDesktop) { + openDirectory(currentRemoteDir.path); + openDirectory(currentLocalDir.path); + } else { + openDirectory(currentDir.path); + } } openDirectory(String path, {bool? isLocal}) async { From f2460c26ca9861c424bcb090892c9ab82013206e Mon Sep 17 00:00:00 2001 From: kingtous Date: Mon, 27 Jun 2022 09:25:20 +0800 Subject: [PATCH 0054/2015] feat: add specific keyboard hook --- flutter/lib/desktop/pages/remote_page.dart | 13 +++++++++++-- flutter/macos/Runner/bridge_generated.h | 3 +++ src/flutter_ffi.rs | 5 +++++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 9412e03c5..f0708f909 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -630,8 +630,17 @@ class _RemotePageState extends State id: widget.id, )); } - return Container( - color: MyTheme.canvasColor, child: Stack(children: paints)); + paints.add(getHelpTools()); + return MouseRegion( + onEnter: (evt) { + _ffi.bind.hostStopSystemKeyPropagate(stopped: false); + }, + onExit: (evt) { + _ffi.bind.hostStopSystemKeyPropagate(stopped: true); + }, + child: Container( + color: MyTheme.canvasColor, child: Stack(children: paints)), + ); } int lastMouseDownButtons = 0; diff --git a/flutter/macos/Runner/bridge_generated.h b/flutter/macos/Runner/bridge_generated.h index 6eb6cbd51..215e6249f 100644 --- a/flutter/macos/Runner/bridge_generated.h +++ b/flutter/macos/Runner/bridge_generated.h @@ -21,6 +21,8 @@ void wire_rustdesk_core_main(int64_t port_); void wire_start_global_event_stream(int64_t port_); +void wire_host_stop_system_key_propagate(int64_t port_, bool stopped); + void wire_session_connect(int64_t port_, struct wire_uint_8_list *id, bool is_file_transfer); void wire_get_session_remember(int64_t port_, struct wire_uint_8_list *id); @@ -170,6 +172,7 @@ static int64_t dummy_method_to_enforce_bundling(void) { int64_t dummy_var = 0; dummy_var ^= ((int64_t) (void*) wire_rustdesk_core_main); dummy_var ^= ((int64_t) (void*) wire_start_global_event_stream); + dummy_var ^= ((int64_t) (void*) wire_host_stop_system_key_propagate); dummy_var ^= ((int64_t) (void*) wire_session_connect); dummy_var ^= ((int64_t) (void*) wire_get_session_remember); dummy_var ^= ((int64_t) (void*) wire_get_session_toggle_option); diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 055e62721..34d432dbe 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -69,6 +69,11 @@ pub fn start_global_event_stream(s: StreamSink) -> ResultType<()> { Ok(()) } +pub fn host_stop_system_key_propagate(stopped: bool) { + #[cfg(windows)] + crate::platform::windows::stop_system_key_propagate(stopped); +} + pub fn session_connect( events2ui: StreamSink, id: String, From eef20806d62f2d3ac56a848697d813a7b1b31b6e Mon Sep 17 00:00:00 2001 From: kingtous Date: Mon, 27 Jun 2022 09:48:35 +0800 Subject: [PATCH 0055/2015] fix: temporary remove collesped plugins --- .../lib/desktop/pages/desktop_home_page.dart | 3 +- flutter/lib/desktop/pages/remote_page.dart | 17 ++-- .../lib/desktop/widgets/titlebar_widget.dart | 83 ++++++++++--------- flutter/lib/main.dart | 12 +-- flutter/lib/models/model.dart | 4 +- flutter/pubspec.lock | 57 +------------ flutter/pubspec.yaml | 4 +- flutter/windows/runner/main.cpp | 4 +- 8 files changed, 59 insertions(+), 125 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index bbd440712..1e7006628 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -7,7 +7,6 @@ import 'package:flutter_hbb/desktop/widgets/titlebar_widget.dart'; import 'package:flutter_hbb/models/model.dart'; import 'package:provider/provider.dart'; import 'package:tray_manager/tray_manager.dart'; -import 'package:window_manager/window_manager.dart'; class DesktopHomePage extends StatefulWidget { DesktopHomePage({Key? key}) : super(key: key); @@ -215,7 +214,7 @@ class _DesktopHomePageState extends State with TrayListener { case "quit": exit(0); case "show": - windowManager.show(); + // windowManager.show(); break; default: break; diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index f0708f909..c938732cd 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -12,7 +12,7 @@ import 'package:get/get.dart'; import 'package:get/route_manager.dart'; import 'package:provider/provider.dart'; import 'package:wakelock/wakelock.dart'; -import 'package:window_manager/window_manager.dart'; +// import 'package:window_manager/window_manager.dart'; import '../../common.dart'; import '../../mobile/widgets/dialog.dart'; @@ -32,7 +32,7 @@ class RemotePage extends StatefulWidget { } class _RemotePageState extends State - with WindowListener, AutomaticKeepAliveClientMixin { + with AutomaticKeepAliveClientMixin { Timer? _interval; Timer? _timer; bool _showBar = !isWebDesktop; @@ -69,7 +69,7 @@ class _RemotePageState extends State _physicalFocusNode.requestFocus(); ffi.ffiModel.updateEventListener(widget.id); ffi.listenToMouse(true); - WindowManager.instance.addListener(this); + // WindowManager.instance.addListener(this); } @override @@ -89,7 +89,7 @@ class _RemotePageState extends State if (!Platform.isLinux) { Wakelock.disable(); } - WindowManager.instance.removeListener(this); + // WindowManager.instance.removeListener(this); Get.delete(tag: widget.id); super.dispose(); } @@ -286,14 +286,7 @@ class _RemotePageState extends State OverlayEntry(builder: (context) { return Container( color: Colors.black, - child: isWebDesktop - ? getBodyForDesktopWithListener(keyboard) - : SafeArea( - child: Container( - color: MyTheme.canvasColor, - child: _isPhysicalMouse - ? getBodyForMobile() - : getBodyForMobileWithGesture()))); + child: getBodyForDesktopWithListener(keyboard)); }) ], ))), diff --git a/flutter/lib/desktop/widgets/titlebar_widget.dart b/flutter/lib/desktop/widgets/titlebar_widget.dart index ecb68d513..6e9b0bf6e 100644 --- a/flutter/lib/desktop/widgets/titlebar_widget.dart +++ b/flutter/lib/desktop/widgets/titlebar_widget.dart @@ -1,4 +1,3 @@ -import 'package:bitsdojo_window/bitsdojo_window.dart'; import 'package:flutter/material.dart'; const sidebarColor = Color(0xFF0C6AF6); @@ -20,47 +19,51 @@ class DesktopTitleBar extends StatelessWidget { colors: [backgroundStartColor, backgroundEndColor], stops: [0.0, 1.0]), ), - child: WindowTitleBarBox( - child: SizedBox( - child: Row( - children: [ - Expanded( - child: MoveWindow( - child: child, - )), - const WindowButtons() - ], - ), - ), + child: Row( + children: [ + 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), - MaximizeWindowButton(colors: buttonColors), - CloseWindowButton(colors: closeButtonColors), - ], - ); - } -} +// 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(); +// },), +// ], +// ); +// } +// } diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 898274337..322d9f300 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -1,6 +1,5 @@ import 'dart:convert'; -import 'package:bitsdojo_window/bitsdojo_window.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart'; import 'package:flutter_hbb/desktop/screen/desktop_file_transfer_screen.dart'; @@ -9,7 +8,7 @@ import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/route_manager.dart'; import 'package:provider/provider.dart'; -import 'package:window_manager/window_manager.dart'; +// import 'package:window_manager/window_manager.dart'; import 'common.dart'; import 'mobile/pages/home_page.dart'; @@ -38,7 +37,6 @@ void runRustDeskApp(List args) async { return; } // main window - await windowManager.ensureInitialized(); if (args.isNotEmpty && args.first == 'multi_window') { windowId = int.parse(args[1]); final argument = args[2].isEmpty @@ -59,17 +57,11 @@ void runRustDeskApp(List args) async { break; } } else { + // await windowManager.ensureInitialized(); // disable tray // initTray(); gFFI.serverModel.startService(); runApp(App()); - doWhenWindowReady(() { - const initialSize = Size(1280, 720); - appWindow.minSize = initialSize; - appWindow.size = initialSize; - appWindow.alignment = Alignment.center; - appWindow.show(); - }); } } diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 0332dc797..dbb0ce23e 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -891,9 +891,7 @@ class FFI { /// Connect with the given [id]. Only transfer file if [isFileTransfer]. void connect(String id, {bool isFileTransfer = false}) { - if (isFileTransfer) { - setByName('connect_file_transfer', id); - } else { + if (!isFileTransfer) { chatModel.resetClientMode(); canvasModel.id = id; imageModel._id = id; diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index d82f4c367..e9fb72892 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -36,41 +36,6 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.8.2" - bitsdojo_window: - dependency: "direct main" - description: - name: bitsdojo_window - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.1.2" - bitsdojo_window_linux: - dependency: transitive - description: - name: bitsdojo_window_linux - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.1.2" - bitsdojo_window_macos: - dependency: transitive - description: - name: bitsdojo_window_macos - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.1.2" - bitsdojo_window_platform_interface: - dependency: transitive - description: - name: bitsdojo_window_platform_interface - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.1.2" - bitsdojo_window_windows: - dependency: transitive - description: - name: bitsdojo_window_windows - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.1.2" boolean_selector: dependency: transitive description: @@ -221,11 +186,9 @@ packages: desktop_multi_window: dependency: "direct main" description: - path: "." - ref: "704718b2853723b615675e048f1f385cbfb209a6" - resolved-ref: "704718b2853723b615675e048f1f385cbfb209a6" - url: "https://github.com/Kingtous/rustdesk_desktop_multi_window" - source: git + path: "../../rustdesk_desktop_multi_window" + relative: true + source: path version: "0.0.1" device_info_plus: dependency: "direct main" @@ -785,13 +748,6 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "3.1.0" - screen_retriever: - dependency: transitive - description: - name: screen_retriever - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.1.2" settings_ui: dependency: "direct main" description: @@ -1105,13 +1061,6 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.6.1" - window_manager: - dependency: "direct main" - description: - name: window_manager - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.2.5" xdg_directories: dependency: transitive description: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 65bd819ff..98d858d44 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -58,11 +58,11 @@ dependencies: url: https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge ref: master path: frb_dart - window_manager: ^0.2.5 + # window_manager: ^0.2.5 desktop_multi_window: git: url: https://github.com/Kingtous/rustdesk_desktop_multi_window - ref: 704718b2853723b615675e048f1f385cbfb209a6 + ref: c7d97cb6615f2def34f8bad4def01af9e0077beb bitsdojo_window: ^0.1.2 freezed_annotation: ^2.0.3 tray_manager: 0.1.7 diff --git a/flutter/windows/runner/main.cpp b/flutter/windows/runner/main.cpp index 4073213e5..f84fc1861 100644 --- a/flutter/windows/runner/main.cpp +++ b/flutter/windows/runner/main.cpp @@ -5,11 +5,11 @@ #include "flutter_window.h" #include "utils.h" -#include +// #include typedef bool (*FUNC_RUSTDESK_CORE_MAIN)(void); -auto bdw = bitsdojo_window_configure(BDW_CUSTOM_FRAME | BDW_HIDE_ON_STARTUP); +// auto bdw = bitsdojo_window_configure(BDW_CUSTOM_FRAME | BDW_HIDE_ON_STARTUP); int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, _In_ wchar_t *command_line, _In_ int show_command) { From d5c0bcea61cdc2265775238a24ae50f9b145b1f1 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 27 Jun 2022 10:00:51 +0800 Subject: [PATCH 0056/2015] revert: remove conflict bitsdojo_window plugin for linux & macOS Signed-off-by: Kingtous --- flutter/linux/my_application.cc | 6 +++--- flutter/macos/Runner/MainFlutterWindow.swift | 10 +++++----- flutter/pubspec.yaml | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/flutter/linux/my_application.cc b/flutter/linux/my_application.cc index 8c7a5fe05..25e9858cc 100644 --- a/flutter/linux/my_application.cc +++ b/flutter/linux/my_application.cc @@ -1,7 +1,7 @@ #include "my_application.h" #include -#include +// #include #ifdef GDK_WINDOWING_X11 #include #endif @@ -48,8 +48,8 @@ static void my_application_activate(GApplication* application) { gtk_window_set_title(window, "rustdesk"); } - auto bdw = bitsdojo_window_from(window); // <--- add this line - bdw->setCustomFrame(true); // <-- add this line + // auto bdw = bitsdojo_window_from(window); // <--- add this line + // bdw->setCustomFrame(true); // <-- add this line gtk_window_set_default_size(window, 1280, 720); // <-- comment this line gtk_widget_show(GTK_WIDGET(window)); diff --git a/flutter/macos/Runner/MainFlutterWindow.swift b/flutter/macos/Runner/MainFlutterWindow.swift index 17f024ec5..688292371 100644 --- a/flutter/macos/Runner/MainFlutterWindow.swift +++ b/flutter/macos/Runner/MainFlutterWindow.swift @@ -1,8 +1,8 @@ import Cocoa import FlutterMacOS -import bitsdojo_window_macos +// import bitsdojo_window_macos -class MainFlutterWindow: BitsdojoWindow { +class MainFlutterWindow: NSWindow { override func awakeFromNib() { if (!rustdesk_core_main()){ print("Rustdesk core returns false, exiting without launching Flutter app") @@ -18,7 +18,7 @@ class MainFlutterWindow: BitsdojoWindow { super.awakeFromNib() } - override func bitsdojo_window_configure() -> UInt { - return BDW_CUSTOM_FRAME | BDW_HIDE_ON_STARTUP - } +// override func bitsdojo_window_configure() -> UInt { +// return BDW_CUSTOM_FRAME | BDW_HIDE_ON_STARTUP +// } } diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 98d858d44..59fcbffca 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -63,7 +63,7 @@ dependencies: git: url: https://github.com/Kingtous/rustdesk_desktop_multi_window ref: c7d97cb6615f2def34f8bad4def01af9e0077beb - bitsdojo_window: ^0.1.2 + # bitsdojo_window: ^0.1.2 freezed_annotation: ^2.0.3 tray_manager: 0.1.7 get: ^4.6.5 From f5e0aef0dedfd8200abea4c35c8faf89c6232326 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 27 Jun 2022 10:34:57 +0800 Subject: [PATCH 0057/2015] opt: windowManager -> LayoutBuilder Signed-off-by: Kingtous --- flutter/lib/desktop/pages/remote_page.dart | 138 +++------------------ 1 file changed, 15 insertions(+), 123 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index c938732cd..2e5c4a243 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -12,11 +12,11 @@ import 'package:get/get.dart'; import 'package:get/route_manager.dart'; import 'package:provider/provider.dart'; import 'package:wakelock/wakelock.dart'; + // import 'package:window_manager/window_manager.dart'; import '../../common.dart'; import '../../mobile/widgets/dialog.dart'; -import '../../mobile/widgets/gestures.dart'; import '../../mobile/widgets/overlay.dart'; import '../../models/model.dart'; @@ -493,123 +493,6 @@ class _RemotePageState extends State /// DoubleFiner -> right click /// HoldDrag -> left drag - Widget getBodyForMobileWithGesture() { - final touchMode = _ffi.ffiModel.touchMode; - return getMixinGestureDetector( - child: getBodyForMobile(), - onTapUp: (d) { - if (touchMode) { - _ffi.cursorModel.touch( - d.localPosition.dx, d.localPosition.dy, MouseButtons.left); - } else { - _ffi.tap(MouseButtons.left); - } - }, - onDoubleTapDown: (d) { - if (touchMode) { - _ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy); - } - }, - onDoubleTap: () { - _ffi.tap(MouseButtons.left); - _ffi.tap(MouseButtons.left); - }, - onLongPressDown: (d) { - if (touchMode) { - _ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy); - } - }, - onLongPress: () { - _ffi.tap(MouseButtons.right); - }, - onDoubleFinerTap: (d) { - if (!touchMode) { - _ffi.tap(MouseButtons.right); - } - }, - onHoldDragStart: (d) { - if (!touchMode) { - _ffi.sendMouse('down', MouseButtons.left); - } - }, - onHoldDragUpdate: (d) { - if (!touchMode) { - _ffi.cursorModel.updatePan(d.delta.dx, d.delta.dy, touchMode); - } - }, - onHoldDragEnd: (_) { - if (!touchMode) { - _ffi.sendMouse('up', MouseButtons.left); - } - }, - onOneFingerPanStart: (d) { - if (touchMode) { - _ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy); - _ffi.sendMouse('down', MouseButtons.left); - } - }, - onOneFingerPanUpdate: (d) { - _ffi.cursorModel.updatePan(d.delta.dx, d.delta.dy, touchMode); - }, - onOneFingerPanEnd: (d) { - if (touchMode) { - _ffi.sendMouse('up', MouseButtons.left); - } - }, - // scale + pan event - onTwoFingerScaleUpdate: (d) { - _ffi.canvasModel.updateScale(d.scale / _scale); - _scale = d.scale; - _ffi.canvasModel.panX(d.focalPointDelta.dx); - _ffi.canvasModel.panY(d.focalPointDelta.dy); - }, - onTwoFingerScaleEnd: (d) { - _scale = 1; - _ffi.bind - .sessionPeerOption(id: widget.id, name: "view-style", value: ""); - }, - onThreeFingerVerticalDragUpdate: _ffi.ffiModel.isPeerAndroid - ? null - : (d) { - _mouseScrollIntegral += d.delta.dy / 4; - if (_mouseScrollIntegral > 1) { - _ffi.scroll(1); - _mouseScrollIntegral = 0; - } else if (_mouseScrollIntegral < -1) { - _ffi.scroll(-1); - _mouseScrollIntegral = 0; - } - }); - } - - Widget getBodyForMobile() { - return Container( - color: MyTheme.canvasColor, - child: Stack(children: [ - ImagePaint(id: widget.id), - CursorPaint(id: widget.id), - getHelpTools(), - SizedBox( - width: 0, - height: 0, - child: !_showEdit - ? Container() - : TextFormField( - textInputAction: TextInputAction.newline, - autocorrect: false, - enableSuggestions: false, - autofocus: true, - focusNode: _mobileFocusNode, - maxLines: null, - initialValue: _value, - // trick way to make backspace work always - keyboardType: TextInputType.multiline, - onChanged: handleInput, - ), - ), - ])); - } - Widget getBodyForDesktopWithListener(bool keyboard) { var paints = [ ImagePaint( @@ -625,15 +508,24 @@ class _RemotePageState extends State } paints.add(getHelpTools()); return MouseRegion( - onEnter: (evt) { + onEnter: (evt) { _ffi.bind.hostStopSystemKeyPropagate(stopped: false); }, - onExit: (evt) { + onExit: (evt) { _ffi.bind.hostStopSystemKeyPropagate(stopped: true); }, - child: Container( - color: MyTheme.canvasColor, child: Stack(children: paints)), - ); + child: Container( + color: MyTheme.canvasColor, + child: LayoutBuilder(builder: (context, constraints) { + Future.delayed(Duration.zero, () { + Provider.of(context, listen: false) + .updateViewStyle(); + }); + return Stack( + children: paints, + ); + }), + )); } int lastMouseDownButtons = 0; From 3f2aaae1ffd279522234d5178bbcdf78e440b193 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 27 Jun 2022 11:50:15 +0800 Subject: [PATCH 0058/2015] opt: merge addon Signed-off-by: Kingtous --- Cargo.lock | 16 ++++++++++++++- .../lib/desktop/pages/connection_page.dart | 11 ++-------- flutter/lib/mobile/pages/remote_page.dart | 2 +- flutter/macos/Runner/bridge_generated.h | 4 ++++ flutter/pubspec.lock | 8 +++++--- src/flutter.rs | 20 ------------------- src/flutter_ffi.rs | 3 ++- src/main.rs | 5 +++-- src/ui.rs | 2 +- src/ui_interface.rs | 4 ++-- 10 files changed, 35 insertions(+), 40 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ac6c0e979..1994bfd32 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4035,7 +4035,7 @@ dependencies = [ [[package]] name = "rustdesk" -version = "1.2.0" +version = "1.1.10" dependencies = [ "android_logger 0.11.0", "arboard", @@ -4085,11 +4085,13 @@ dependencies = [ "serde_derive", "serde_json 1.0.81", "sha2", + "simple_rc", "sys-locale", "sysinfo", "tray-item", "trayicon", "uuid", + "virtual_display", "whoami", "winapi 0.3.9", "windows-service", @@ -4257,6 +4259,7 @@ dependencies = [ "quest", "repng", "serde 1.0.137", + "serde_json 1.0.81", "target_build_utils", "tracing", "webm", @@ -4438,6 +4441,17 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f054c6c1a6e95179d6f23ed974060dcefb2d9388bb7256900badad682c499de4" +[[package]] +name = "simple_rc" +version = "0.1.0" +dependencies = [ + "confy", + "hbb_common", + "serde 1.0.137", + "serde_derive", + "walkdir", +] + [[package]] name = "siphasher" version = "0.2.3" diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index be415eb80..aa023c82c 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -71,13 +71,6 @@ class _ConnectionPageState extends State { if (id == '') return; id = id.replaceAll(' ', ''); if (isFileTransfer) { - if (!isDesktop) { - if (!await PermissionManager.check("file")) { - if (!await PermissionManager.request("file")) { - return; - } - } - } await rustDeskWinManager.new_file_transfer(id); } else { await rustDeskWinManager.new_remote_desktop(id); @@ -180,7 +173,7 @@ class _ConnectionPageState extends State { vertical: 8.0, horizontal: 8.0), child: Text( translate( - "File Transfer", + "Transfer File", ), style: TextStyle(color: MyTheme.dark), ), @@ -295,7 +288,7 @@ class _ConnectionPageState extends State { ] + ([ PopupMenuItem( - child: Text(translate('File transfer')), value: 'file') + child: Text(translate('Transfer File')), value: 'file') ]), elevation: 8, ); diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 25497dfbc..a0ea0f17c 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -268,7 +268,7 @@ class _RemotePageState extends State { Timer(Duration(milliseconds: 200), () { resetMobileActionsOverlay(); _currentOrientation = orientation; - FFI.canvasModel.updateViewStyle(); + gFFI.canvasModel.updateViewStyle(); }); } return Container( diff --git a/flutter/macos/Runner/bridge_generated.h b/flutter/macos/Runner/bridge_generated.h index 215e6249f..7f072e770 100644 --- a/flutter/macos/Runner/bridge_generated.h +++ b/flutter/macos/Runner/bridge_generated.h @@ -2,6 +2,10 @@ #include #include +#define GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT 2 + +#define GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS 4 + typedef struct wire_uint_8_list { uint8_t *ptr; int32_t len; diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index e9fb72892..b34076310 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -186,9 +186,11 @@ packages: desktop_multi_window: dependency: "direct main" description: - path: "../../rustdesk_desktop_multi_window" - relative: true - source: path + path: "." + ref: c7d97cb6615f2def34f8bad4def01af9e0077beb + resolved-ref: c7d97cb6615f2def34f8bad4def01af9e0077beb + url: "https://github.com/Kingtous/rustdesk_desktop_multi_window" + source: git version: "0.0.1" device_info_plus: dependency: "direct main" diff --git a/src/flutter.rs b/src/flutter.rs index 36df3972a..41e892bd2 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -1224,26 +1224,6 @@ impl Connection { } } -/// Parse [`FileDirectory`] to json. -pub fn make_fd_to_json(fd: FileDirectory) -> String { - use serde_json::json; - let mut fd_json = serde_json::Map::new(); - fd_json.insert("id".into(), json!(fd.id)); - fd_json.insert("path".into(), json!(fd.path)); - - let mut entries = vec![]; - for entry in fd.entries { - let mut entry_map = serde_json::Map::new(); - entry_map.insert("entry_type".into(), json!(entry.entry_type.value())); - entry_map.insert("name".into(), json!(entry.name)); - entry_map.insert("size".into(), json!(entry.size)); - entry_map.insert("modified_time".into(), json!(entry.modified_time)); - entries.push(entry_map); - } - fd_json.insert("entries".into(), json!(entries)); - serde_json::to_string(&fd_json).unwrap_or("".into()) -} - // Server Side // TODO connection_manager need use struct and trait,impl default method #[cfg(not(any(target_os = "ios")))] diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 34d432dbe..ee1e4086b 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1,6 +1,7 @@ use crate::client::file_trait::FileManager; use crate::flutter::connection_manager::{self, get_clients_length, get_clients_state}; -use crate::flutter::{self, make_fd_to_json, Session, SESSIONS}; +use crate::flutter::{self, Session, SESSIONS}; +use crate::common::make_fd_to_json; use crate::start_server; use crate::ui_interface; use flutter_rust_bridge::{StreamSink, SyncReturn, ZeroCopyBuffer}; diff --git a/src/main.rs b/src/main.rs index a5b1d7b04..6aee5cb89 100644 --- a/src/main.rs +++ b/src/main.rs @@ -195,7 +195,7 @@ fn main() { .about("RustDesk command line tool") .args_from_usage(&args) .get_matches(); - use hbb_common::env_logger::*; + use hbb_common::{env_logger::*, config::LocalConfig}; init_from_env(Env::default().filter_or(DEFAULT_FILTER_ENV, "info")); if let Some(p) = matches.value_of("port-forward") { let options: Vec = p.split(":").map(|x| x.to_owned()).collect(); @@ -222,6 +222,7 @@ fn main() { remote_host = options[3].clone(); } let key = matches.value_of("key").unwrap_or("").to_owned(); - cli::start_one_port_forward(options[0].clone(), port, remote_host, remote_port, key); + let token = LocalConfig::get_option("access_token"); + cli::start_one_port_forward(options[0].clone(), port, remote_host, remote_port, key, token); } } diff --git a/src/ui.rs b/src/ui.rs index 5d6a6dce3..7a6fd0219 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -171,7 +171,7 @@ impl UI { } fn install_me(&mut self, _options: String, _path: String) { - install_me(_options, _path); + install_me(_options, _path, false, false); } fn update_me(&self, _path: String) { diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 4e0a61fa0..c0b2ce478 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -69,10 +69,10 @@ pub fn goto_install() { allow_err!(crate::run_me(vec!["--install"])); } -pub fn install_me(_options: String, _path: String) { +pub fn install_me(_options: String, _path: String, silent: bool, debug: bool) { #[cfg(windows)] std::thread::spawn(move || { - allow_err!(crate::platform::windows::install_me(&_options, _path)); + allow_err!(crate::platform::windows::install_me(&_options, _path, silent, debug)); std::process::exit(0); }); } From d79bdd6afe3c3bca1203bd69542e28daf4d44079 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 27 Jun 2022 12:09:27 +0800 Subject: [PATCH 0059/2015] fix: cli feature compilation --- src/client/file_trait.rs | 2 +- src/ui_interface.rs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/client/file_trait.rs b/src/client/file_trait.rs index 45f0473ba..6666a2d91 100644 --- a/src/client/file_trait.rs +++ b/src/client/file_trait.rs @@ -21,7 +21,7 @@ pub trait FileManager: Interface { #[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))] fn read_dir(&self, path: &str, include_hidden: bool) -> String { - use crate::flutter::make_fd_to_json; + use crate::common::make_fd_to_json; match fs::read_dir(&fs::get_path(path), include_hidden) { Ok(fd) => make_fd_to_json(fd), Err(_) => "".into(), diff --git a/src/ui_interface.rs b/src/ui_interface.rs index c0b2ce478..90e39636d 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -441,6 +441,7 @@ pub fn is_installed_daemon(_prompt: bool) -> bool { } pub fn get_error() -> String { + #[cfg(not(any(feature = "cli")))] #[cfg(target_os = "linux")] { let dtype = crate::platform::linux::get_display_server(); From 2b10da167ce0a19b193e340b27a57eb3d4e2151e Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 27 Jun 2022 16:44:34 +0800 Subject: [PATCH 0060/2015] add: file transfer dual logic with bridge --- .../lib/desktop/pages/file_manager_page.dart | 265 ++++++++---------- .../desktop/pages/file_manager_tab_page.dart | 125 ++++----- flutter/lib/models/file_model.dart | 73 +++-- flutter/lib/utils/multi_window_manager.dart | 2 +- 4 files changed, 220 insertions(+), 245 deletions(-) diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index e9e69556c..ed4a32b37 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -11,7 +11,6 @@ import 'package:provider/provider.dart'; import 'package:wakelock/wakelock.dart'; import '../../common.dart'; -import '../../mobile/widgets/dialog.dart'; import '../../models/model.dart'; class FileManagerPage extends StatefulWidget { @@ -25,7 +24,8 @@ class FileManagerPage extends StatefulWidget { class _FileManagerPageState extends State with AutomaticKeepAliveClientMixin { final _selectedItems = SelectedItems(); - final _breadCrumbScroller = ScrollController(); + final _breadCrumbLocalScroller = ScrollController(); + final _breadCrumbRemoteScroller = ScrollController(); /// FFI with name file_transfer_id FFI get _ffi => ffi('ft_${widget.id}'); @@ -66,135 +66,11 @@ class _FileManagerPageState extends State onWillPop: () async { if (model.selectMode) { model.toggleSelectMode(); - } else { - goBack(); } return false; }, child: Scaffold( backgroundColor: MyTheme.grayBg, - appBar: AppBar( - leading: Row(children: [ - IconButton(icon: Icon(Icons.close), onPressed: clientClose), - ]), - centerTitle: true, - // title: ToggleSwitch( - // initialLabelIndex: model.isLocal ? 0 : 1, - // activeBgColor: [MyTheme.idColor], - // inactiveBgColor: MyTheme.grayBg, - // inactiveFgColor: Colors.black54, - // totalSwitches: 2, - // minWidth: 100, - // fontSize: 15, - // iconSize: 18, - // labels: [translate("Local"), translate("Remote")], - // icons: [Icons.phone_android_sharp, Icons.screen_share], - // onToggle: (index) { - // final current = model.isLocal ? 0 : 1; - // if (index != current) { - // model.togglePage(); - // } - // }, - // ), - actions: [ - PopupMenuButton( - icon: Icon(Icons.more_vert), - itemBuilder: (context) { - return [ - PopupMenuItem( - child: Row( - children: [ - Icon(Icons.refresh, color: Colors.black), - SizedBox(width: 5), - Text(translate("Refresh File")) - ], - ), - value: "refresh", - ), - PopupMenuItem( - child: Row( - children: [ - Icon(Icons.check, color: Colors.black), - SizedBox(width: 5), - Text(translate("Multi Select")) - ], - ), - value: "select", - ), - PopupMenuItem( - child: Row( - children: [ - Icon(Icons.folder_outlined, - color: Colors.black), - SizedBox(width: 5), - Text(translate("Create Folder")) - ], - ), - value: "folder", - ), - PopupMenuItem( - child: Row( - children: [ - Icon( - model.currentShowHidden - ? Icons.check_box_outlined - : Icons.check_box_outline_blank, - color: Colors.black), - SizedBox(width: 5), - Text(translate("Show Hidden Files")) - ], - ), - value: "hidden", - ) - ]; - }, - onSelected: (v) { - if (v == "refresh") { - model.refresh(); - } else if (v == "select") { - _selectedItems.clear(); - model.toggleSelectMode(); - } else if (v == "folder") { - final name = TextEditingController(); - DialogManager.show((setState, close) => - CustomAlertDialog( - title: Text(translate("Create Folder")), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TextFormField( - decoration: InputDecoration( - labelText: translate( - "Please enter the folder name"), - ), - controller: name, - ), - ], - ), - actions: [ - TextButton( - style: flatButtonStyle, - onPressed: () => close(false), - child: Text(translate("Cancel"))), - ElevatedButton( - style: flatButtonStyle, - onPressed: () { - if (name.value.text.isNotEmpty) { - model.createDir(PathUtil.join( - model.currentDir.path, - name.value.text, - model.currentIsWindows)); - close(); - } - }, - child: Text(translate("OK"))) - ])); - } else if (v == "hidden") { - model.toggleShowHidden(); - } - }), - ], - ), body: Row( children: [ Flexible(flex: 1, child: body(isLocal: true)), @@ -213,11 +89,110 @@ class _FileManagerPageState extends State return !_selectedItems.isOtherPage(model.isLocal); } + Widget menu({bool isLocal = false}) { + return PopupMenuButton( + icon: Icon(Icons.more_vert), + itemBuilder: (context) { + return [ + PopupMenuItem( + child: Row( + children: [ + Icon(Icons.refresh, color: Colors.black), + SizedBox(width: 5), + Text(translate("Refresh File")) + ], + ), + value: "refresh", + ), + PopupMenuItem( + child: Row( + children: [ + Icon(Icons.check, color: Colors.black), + SizedBox(width: 5), + Text(translate("Multi Select")) + ], + ), + value: "select", + ), + PopupMenuItem( + child: Row( + children: [ + Icon(Icons.folder_outlined, + color: Colors.black), + SizedBox(width: 5), + Text(translate("Create Folder")) + ], + ), + value: "folder", + ), + PopupMenuItem( + child: Row( + children: [ + Icon( + model.currentShowHidden + ? Icons.check_box_outlined + : Icons.check_box_outline_blank, + color: Colors.black), + SizedBox(width: 5), + Text(translate("Show Hidden Files")) + ], + ), + value: "hidden", + ) + ]; + }, + onSelected: (v) { + if (v == "refresh") { + model.refresh(); + } else if (v == "select") { + _selectedItems.clear(); + model.toggleSelectMode(); + } else if (v == "folder") { + final name = TextEditingController(); + DialogManager.show((setState, close) => + CustomAlertDialog( + title: Text(translate("Create Folder")), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextFormField( + decoration: InputDecoration( + labelText: translate( + "Please enter the folder name"), + ), + controller: name, + ), + ], + ), + actions: [ + TextButton( + style: flatButtonStyle, + onPressed: () => close(false), + child: Text(translate("Cancel"))), + ElevatedButton( + style: flatButtonStyle, + onPressed: () { + if (name.value.text.isNotEmpty) { + model.createDir(PathUtil.join( + model.currentDir.path, + name.value.text, + model.currentIsWindows)); + close(); + } + }, + child: Text(translate("OK"))) + ])); + } else if (v == "hidden") { + model.toggleShowHidden(local: isLocal); + } + }); + } + Widget body({bool isLocal = false}) { final fd = isLocal ? model.currentLocalDir : model.currentRemoteDir; final entries = fd.entries; return Column(children: [ - headTools(), + headTools(isLocal), Expanded( child: ListView.builder( itemCount: entries.length + 1, @@ -301,8 +276,8 @@ class _FileManagerPageState extends State return; } if (entries[index].isDirectory) { - model.openDirectory(entries[index].path); - breadCrumbScrollToEnd(); + model.openDirectory(entries[index].path, isLocal: isLocal); + breadCrumbScrollToEnd(isLocal); } else { // Perform file-related tasks. } @@ -322,20 +297,21 @@ class _FileManagerPageState extends State ]); } - goBack() { - model.goToParentDirectory(); + goBack({bool? isLocal}) { + model.goToParentDirectory(isLocal: isLocal); } - breadCrumbScrollToEnd() { + breadCrumbScrollToEnd(bool isLocal) { + final controller = isLocal ? _breadCrumbLocalScroller : _breadCrumbRemoteScroller; Future.delayed(Duration(milliseconds: 200), () { - _breadCrumbScroller.animateTo( - _breadCrumbScroller.position.maxScrollExtent, + controller.animateTo( + controller.position.maxScrollExtent, duration: Duration(milliseconds: 200), curve: Curves.fastLinearToSlowEaseIn); }); } - Widget headTools() => Container( + Widget headTools(bool isLocal) => Container( child: Row( children: [ Expanded( @@ -353,16 +329,18 @@ class _FileManagerPageState extends State path = PathUtil.join(path, item, model.currentIsWindows); } } - model.openDirectory(path); - }), + model.openDirectory(path, isLocal: isLocal); + }, isLocal), divider: Icon(Icons.chevron_right), - overflow: ScrollableOverflow(controller: _breadCrumbScroller), + overflow: ScrollableOverflow(controller: isLocal ? _breadCrumbLocalScroller : _breadCrumbRemoteScroller), )), Row( children: [ IconButton( icon: Icon(Icons.arrow_upward), - onPressed: goBack, + onPressed: () { + goBack(isLocal: isLocal); + }, ), PopupMenuButton( icon: Icon(Icons.sort), @@ -375,7 +353,10 @@ class _FileManagerPageState extends State )) .toList(); }, - onSelected: model.changeSortStyle), + onSelected: (sort) { + model.changeSortStyle(sort, isLocal: isLocal); + }), + menu(isLocal: isLocal) ], ) ], @@ -486,8 +467,8 @@ class _FileManagerPageState extends State } List getPathBreadCrumbItems( - void Function() onHome, void Function(List) onPressed) { - final path = model.currentShortPath; + void Function() onHome, void Function(List) onPressed, bool isLocal) { + final path = model.shortPath(isLocal); final list = PathUtil.split(path, model.currentIsWindows); final breadCrumbList = [ BreadCrumbItem( diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index 6c945aede..af65c86df 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hbb/desktop/pages/file_manager_page.dart'; import 'package:flutter_hbb/desktop/widgets/titlebar_widget.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; +import 'package:get/get.dart'; /// File Transfer for multi tabs class FileManagerTabPage extends StatefulWidget { @@ -21,7 +22,7 @@ class _FileManagerTabPageState extends State // refactor List when using multi-tab // this singleton is only for test List connectionIds = List.empty(growable: true); - var initialIndex = 0; + var initialIndex = 0.obs; _FileManagerTabPageState(Map params) { if (params['id'] != null) { @@ -37,21 +38,15 @@ class _FileManagerTabPageState extends State "call ${call.method} with args ${call.arguments} from window ${fromWindowId}"); // for simplify, just replace connectionId if (call.method == "new_file_transfer") { - setState(() { - final args = jsonDecode(call.arguments); - final id = args['id']; - final indexOf = connectionIds.indexOf(id); - if (indexOf >= 0) { - setState(() { - initialIndex = indexOf; - }); - } else { - connectionIds.add(id); - setState(() { - initialIndex = connectionIds.length - 1; - }); - } - }); + final args = jsonDecode(call.arguments); + final id = args['id']; + final indexOf = connectionIds.indexOf(id); + if (indexOf >= 0) { + initialIndex.value = indexOf; + } else { + connectionIds.add(id); + initialIndex.value = connectionIds.length - 1; + } } }); } @@ -59,51 +54,53 @@ class _FileManagerTabPageState extends State @override Widget build(BuildContext context) { return Scaffold( - body: DefaultTabController( - initialIndex: initialIndex, - length: connectionIds.length, - animationDuration: Duration.zero, - child: Column( - children: [ - DesktopTitleBar( - child: TabBar( - isScrollable: true, - labelColor: Colors.white, - physics: NeverScrollableScrollPhysics(), - indicatorColor: Colors.white, - tabs: connectionIds - .map((e) => Tab( - child: Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text(e), - SizedBox( - width: 4, - ), - InkWell( - onTap: () { - onRemoveId(e); - }, - child: Icon( - Icons.highlight_remove, - size: 20, - )) - ], - ), - )) - .toList()), - ), - Expanded( - child: TabBarView( - children: connectionIds - .map((e) => Container( - child: FileManagerPage( - key: ValueKey(e), - id: e))) //RemotePage(key: ValueKey(e), id: e)) - .toList()), - ) - ], + body: Obx( + ()=> DefaultTabController( + initialIndex: initialIndex.value, + length: connectionIds.length, + animationDuration: Duration.zero, + child: Column( + children: [ + DesktopTitleBar( + child: TabBar( + isScrollable: true, + labelColor: Colors.white, + physics: NeverScrollableScrollPhysics(), + indicatorColor: Colors.white, + tabs: connectionIds + .map((e) => Tab( + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(e), + SizedBox( + width: 4, + ), + InkWell( + onTap: () { + onRemoveId(e); + }, + child: Icon( + Icons.highlight_remove, + size: 20, + )) + ], + ), + )) + .toList()), + ), + Expanded( + child: TabBarView( + children: connectionIds + .map((e) => Container( + child: FileManagerPage( + key: ValueKey(e), + id: e))) //RemotePage(key: ValueKey(e), id: e)) + .toList()), + ) + ], + ), ), ), ); @@ -114,9 +111,7 @@ class _FileManagerTabPageState extends State if (indexOf == -1) { return; } - setState(() { - connectionIds.removeAt(indexOf); - initialIndex = max(0, initialIndex - 1); - }); + connectionIds.removeAt(indexOf); + initialIndex.value = max(0, initialIndex.value - 1); } } diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index 2c42d3b02..58ddd658a 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -60,6 +60,21 @@ class FileModel extends ChangeNotifier { } } + String shortPath(bool isLocal) { + final dir = isLocal ? currentLocalDir : currentRemoteDir; + if (dir.path.startsWith(currentHome)) { + var path = dir.path.replaceFirst(currentHome, ""); + if (path.length == 0) return ""; + if (path[0] == "/" || path[0] == "\\") { + // remove more '/' or '\' + path = path.replaceFirst(path[0], ""); + } + return path; + } else { + return dir.path.replaceFirst(currentHome, ""); + } + } + bool get currentShowHidden => _isLocal ? _localOption.showHidden : _remoteOption.showHidden; @@ -265,9 +280,9 @@ class FileModel extends ChangeNotifier { openDirectory(currentHome); } - goToParentDirectory() { + goToParentDirectory({bool? isLocal}) { final parent = PathUtil.dirname(currentDir.path, currentIsWindows); - openDirectory(parent); + openDirectory(parent, isLocal: isLocal); } sendFiles(SelectedItems items) { @@ -282,17 +297,10 @@ class FileModel extends ChangeNotifier { items.isLocal! ? _localOption.isWindows : _remoteOption.isWindows; final showHidden = items.isLocal! ? _localOption.showHidden : _remoteOption.showHidden; - items.items.forEach((from) { + items.items.forEach((from) async { _jobId++; - final msg = { - "id": _jobId.toString(), - "path": from.path, - "to": PathUtil.join(toPath, from.name, isWindows), - "file_num": "0", - "show_hidden": showHidden.toString(), - "is_remote": (!(items.isLocal!)).toString() - }; - _ffi.target?.setByName("send_files", jsonEncode(msg)); + await _ffi.target?.bind.sessionSendFiles(id: '${_ffi.target?.getId()}', actId: _jobId, path: from.path, to: PathUtil.join(toPath, from.name, isWindows) + ,fileNum: 0, includeHidden: showHidden, isRemote: !(items.isLocal!)); }); } @@ -485,43 +493,34 @@ class FileModel extends ChangeNotifier { } sendRemoveFile(String path, int fileNum, bool isLocal) { - final msg = { - "id": _jobId.toString(), - "path": path, - "file_num": fileNum.toString(), - "is_remote": (!(isLocal)).toString() - }; - _ffi.target?.setByName("remove_file", jsonEncode(msg)); + _ffi.target?.bind.sessionRemoveFile(id: '${_ffi.target?.getId()}', actId: _jobId, path: path, isRemote: !isLocal, fileNum: fileNum); } sendRemoveEmptyDir(String path, int fileNum, bool isLocal) { - final msg = { - "id": _jobId.toString(), - "path": path, - "is_remote": (!isLocal).toString() - }; - _ffi.target?.setByName("remove_all_empty_dirs", jsonEncode(msg)); + _ffi.target?.bind.sessionRemoveAllEmptyDirs(id: '${_ffi.target?.getId()}', actId: _jobId, path: path, isRemote: !isLocal); } - createDir(String path) { + createDir(String path) async { _jobId++; - final msg = { - "id": _jobId.toString(), - "path": path, - "is_remote": (!isLocal).toString() - }; - _ffi.target?.setByName("create_dir", jsonEncode(msg)); + _ffi.target?.bind.sessionCreateDir(id: '${_ffi.target?.getId()}', actId: _jobId, path: path, isRemote: !isLocal); } - cancelJob(int id) { - _ffi.target?.setByName("cancel_job", id.toString()); + cancelJob(int id) async { + _ffi.target?.bind.sessionCancelJob(id: '${_ffi.target?.getId()}', actId: id); jobReset(); } - changeSortStyle(SortBy sort) { + changeSortStyle(SortBy sort, {bool? isLocal}) { _sortStyle = sort; - _currentLocalDir.changeSortStyle(sort); - _currentRemoteDir.changeSortStyle(sort); + if (isLocal == null) { + // compatible for mobile logic + _currentLocalDir.changeSortStyle(sort); + _currentRemoteDir.changeSortStyle(sort); + } else if (isLocal) { + _currentLocalDir.changeSortStyle(sort); + } else { + _currentRemoteDir.changeSortStyle(sort); + } notifyListeners(); } diff --git a/flutter/lib/utils/multi_window_manager.dart b/flutter/lib/utils/multi_window_manager.dart index 5c522f3a5..979ebffd7 100644 --- a/flutter/lib/utils/multi_window_manager.dart +++ b/flutter/lib/utils/multi_window_manager.dart @@ -101,7 +101,7 @@ class RustDeskMultiWindowManager { case WindowType.RemoteDesktop: return _remoteDesktopWindowId; case WindowType.FileTransfer: - break; + return _fileTransferWindowId; case WindowType.PortForward: break; case WindowType.Unknown: From 0ce2c88c50749026ff418c99b9a5f604d32039fe Mon Sep 17 00:00:00 2001 From: SoLongAndThanksForAllThePizza <103753680+SoLongAndThanksForAllThePizza@users.noreply.github.com> Date: Mon, 27 Jun 2022 16:50:02 +0800 Subject: [PATCH 0061/2015] feat: implemented remote control on desktop --- flutter/lib/desktop/pages/remote_page.dart | 38 +++++++-------- flutter/lib/models/model.dart | 57 +++++++++++++++------- 2 files changed, 58 insertions(+), 37 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 2e5c4a243..e0a4fa563 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -256,28 +256,28 @@ class _RemotePageState extends State child: getRawPointerAndKeyBody( keyboard, Scaffold( - // resizeToAvoidBottomInset: true, + // resizeToAvoidBottomInset: true, floatingActionButton: !showActionButton ? null : FloatingActionButton( - mini: !hideKeyboard, - child: Icon(hideKeyboard - ? Icons.expand_more - : Icons.expand_less), - backgroundColor: MyTheme.accent, - onPressed: () { - setState(() { - if (hideKeyboard) { - _showEdit = false; - _ffi.invokeMethod( - "enable_soft_keyboard", false); - _mobileFocusNode.unfocus(); - _physicalFocusNode.requestFocus(); - } else { - _showBar = !_showBar; - } - }); - }), + mini: !hideKeyboard, + child: Icon(hideKeyboard + ? Icons.expand_more + : Icons.expand_less), + backgroundColor: MyTheme.accent, + onPressed: () { + setState(() { + if (hideKeyboard) { + _showEdit = false; + _ffi.invokeMethod( + "enable_soft_keyboard", false); + _mobileFocusNode.unfocus(); + _physicalFocusNode.requestFocus(); + } else { + _showBar = !_showBar; + } + }); + }), bottomNavigationBar: _showBar && pi.displays.length > 0 ? getBottomAppBar(keyboard) : null, diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index a39940e9d..8d4737c5a 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -425,18 +425,35 @@ class CanvasModel with ChangeNotifier { final size = MediaQueryData.fromWindow(ui.window).size; final s1 = size.width / (parent.target?.ffiModel.display.width ?? 720); final s2 = size.height / (parent.target?.ffiModel.display.height ?? 1280); - if (s == 'shrink') { + // Closure to perform shrink operation. + final shrinkOp = () { final s = s1 < s2 ? s1 : s2; if (s < 1) { _scale = s; } - } else if (s == 'stretch') { + }; + // Closure to perform stretch operation. + final stretchOp = () { final s = s1 > s2 ? s1 : s2; if (s > 1) { _scale = s; } + }; + // Closure to perform default operation(set the scale to 1.0). + final defaultOp = () { + _scale = 1.0; + }; + if (s == 'shrink') { + shrinkOp(); + } else if (s == 'stretch') { + stretchOp(); } else { - _scale = 1; + // On desktop, shrink is the default behavior. + if (isDesktop) { + shrinkOp(); + } else { + defaultOp(); + } } _x = (size.width - getDisplayWidth() * _scale) / 2; _y = (size.height - getDisplayHeight() * _scale) / 2; @@ -459,21 +476,24 @@ class CanvasModel with ChangeNotifier { } void moveDesktopMouse(double x, double y) { - final size = MediaQueryData.fromWindow(ui.window).size; - final dw = getDisplayWidth() * _scale; - final dh = getDisplayHeight() * _scale; - var dxOffset = 0; - var dyOffset = 0; - if (dw > size.width) { - dxOffset = (x - dw * (x / size.width) - _x).toInt(); - } - if (dh > size.height) { - dyOffset = (y - dh * (y / size.height) - _y).toInt(); - } - _x += dxOffset; - _y += dyOffset; - if (dxOffset != 0 || dyOffset != 0) { - notifyListeners(); + // On mobile platforms, move the canvas with the cursor. + if (!isDesktop) { + final size = MediaQueryData.fromWindow(ui.window).size; + final dw = getDisplayWidth() * _scale; + final dh = getDisplayHeight() * _scale; + var dxOffset = 0; + var dyOffset = 0; + if (dw > size.width) { + dxOffset = (x - dw * (x / size.width) - _x).toInt(); + } + if (dh > size.height) { + dyOffset = (y - dh * (y / size.height) - _y).toInt(); + } + _x += dxOffset; + _y += dyOffset; + if (dxOffset != 0 || dyOffset != 0) { + notifyListeners(); + } } parent.target?.cursorModel.moveLocal(x, y); } @@ -714,6 +734,7 @@ class CursorModel with ChangeNotifier { } } + /// Update the cursor position. void updateCursorPosition(Map evt) { _x = double.parse(evt['x']); _y = double.parse(evt['y']); From 60a628aefe2aa4901884846e2d25305d7069c1fe Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 28 Jun 2022 22:04:10 +0800 Subject: [PATCH 0062/2015] fix: window close hook Signed-off-by: Kingtous --- flutter/lib/desktop/pages/connection_tab_page.dart | 13 +++++++++++++ .../lib/desktop/pages/file_manager_tab_page.dart | 13 ++++++++++++- flutter/lib/models/model.dart | 8 ++++---- flutter/pubspec.yaml | 2 +- 4 files changed, 30 insertions(+), 6 deletions(-) diff --git a/flutter/lib/desktop/pages/connection_tab_page.dart b/flutter/lib/desktop/pages/connection_tab_page.dart index 8d18b2f24..69c10ebff 100644 --- a/flutter/lib/desktop/pages/connection_tab_page.dart +++ b/flutter/lib/desktop/pages/connection_tab_page.dart @@ -2,9 +2,13 @@ import 'dart:convert'; import 'dart:math'; import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/desktop/pages/remote_page.dart'; import 'package:flutter_hbb/desktop/widgets/titlebar_widget.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; +import 'package:get/get.dart'; + +import '../../models/model.dart'; class ConnectionTabPage extends StatefulWidget { final Map params; @@ -51,6 +55,15 @@ class _ConnectionTabPageState extends State }); } }); + } else if (call.method == "onDestroy") { + print("executing onDestroy hook, closing ${connectionIds}"); + connectionIds.forEach((id) { + final tag = '${id}'; + ffi(tag).close().then((_) { + Get.delete(tag: tag); + }); + }); + Get.back(); } }); } diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index af65c86df..6c9f199b7 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -2,8 +2,10 @@ import 'dart:convert'; import 'dart:math'; import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/desktop/pages/file_manager_page.dart'; import 'package:flutter_hbb/desktop/widgets/titlebar_widget.dart'; +import 'package:flutter_hbb/models/model.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:get/get.dart'; @@ -21,7 +23,7 @@ class _FileManagerTabPageState extends State with SingleTickerProviderStateMixin { // refactor List when using multi-tab // this singleton is only for test - List connectionIds = List.empty(growable: true); + var connectionIds = List.empty(growable: true).obs; var initialIndex = 0.obs; _FileManagerTabPageState(Map params) { @@ -47,6 +49,15 @@ class _FileManagerTabPageState extends State connectionIds.add(id); initialIndex.value = connectionIds.length - 1; } + } else if (call.method == "onDestroy") { + print("executing onDestroy hook, closing ${connectionIds}"); + connectionIds.forEach((id) { + final tag = 'ft_${id}'; + ffi(tag).close().then((_) { + Get.delete(tag: tag); + }); + }); + Get.back(); } }); } diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 8d4737c5a..b4b618666 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -954,10 +954,10 @@ class FFI { } /// Close the remote session. - void close() { + Future close() async { chatModel.close(); if (imageModel.image != null && !isWebDesktop) { - savePreference(id, cursorModel.x, cursorModel.y, canvasModel.x, + await savePreference(id, cursorModel.x, cursorModel.y, canvasModel.x, canvasModel.y, canvasModel.scale, ffiModel.pi.currentDisplay); } bind.sessionClose(id: id); @@ -1085,8 +1085,8 @@ class PeerInfo { List displays = []; } -void savePreference(String id, double xCursor, double yCursor, double xCanvas, - double yCanvas, double scale, int currentDisplay) async { +Future savePreference(String id, double xCursor, double yCursor, + double xCanvas, double yCanvas, double scale, int currentDisplay) async { SharedPreferences prefs = await SharedPreferences.getInstance(); final p = Map(); p['xCursor'] = xCursor; diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 3b6a51a90..4bbc2f3c5 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -62,7 +62,7 @@ dependencies: desktop_multi_window: git: url: https://github.com/Kingtous/rustdesk_desktop_multi_window - ref: c7d97cb6615f2def34f8bad4def01af9e0077beb + ref: 7b72918710921f5fe79eae2dbaa411a66f5dfb45 # bitsdojo_window: ^0.1.2 freezed_annotation: ^2.0.3 tray_manager: 0.1.7 From d0422fa87e98bd9f91ea708a4c03cc6e78590041 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 28 Jun 2022 22:05:49 +0800 Subject: [PATCH 0063/2015] fix: previous session.close read&write error Signed-off-by: Kingtous --- src/flutter.rs | 1 - src/flutter_ffi.rs | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/flutter.rs b/src/flutter.rs index 41e892bd2..1c9aa8bc9 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -179,7 +179,6 @@ impl Session { /// Close the session. pub fn close(&self) { self.send(Data::Close); - let _ = SESSIONS.write().unwrap().remove(&self.id); } /// Reconnect to the current session. diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index ee1e4086b..22243ca79 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -133,6 +133,7 @@ pub fn session_close(id: String) { if let Some(session) = SESSIONS.read().unwrap().get(&id) { session.close(); } + let _ = SESSIONS.write().unwrap().remove(&id); } pub fn session_refresh(id: String) { From e0c52b49f3cbd41f7e1565b787443650e7183fff Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 28 Jun 2022 22:15:00 +0800 Subject: [PATCH 0064/2015] opt: add prefix identifier for each session Signed-off-by: Kingtous --- flutter/lib/models/model.dart | 1 + src/flutter.rs | 24 +++++++++++++++++------- src/flutter_ffi.rs | 2 +- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index b4b618666..e5e521035 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -924,6 +924,7 @@ class FFI { imageModel._id = id; cursorModel.id = id; } + id = isFileTransfer ? 'ft_${id}' : id; final stream = bind.sessionConnect(id: id, isFileTransfer: isFileTransfer); final cb = ffiModel.startEventListener(id); () async { diff --git a/src/flutter.rs b/src/flutter.rs index 1c9aa8bc9..4854a0e42 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -1,5 +1,5 @@ +use crate::common::make_fd_to_json; use crate::{client::*, flutter_ffi::EventToUI}; -use crate::common::{make_fd_to_json}; use flutter_rust_bridge::{StreamSink, ZeroCopyBuffer}; use hbb_common::{ allow_err, @@ -49,16 +49,17 @@ impl Session { /// /// # Arguments /// - /// * `id` - The id of the remote session. + /// * `id` - The identifier of the remote session with prefix. Regex: [\w]*[\_]*[\d]+ /// * `is_file_transfer` - If the session is used for file transfer. - pub fn start(id: &str, is_file_transfer: bool, events2ui: StreamSink) { - LocalConfig::set_remote_id(&id); + pub fn start(identifier: &str, is_file_transfer: bool, events2ui: StreamSink) { + LocalConfig::set_remote_id(&identifier); // TODO check same id + let session_id = get_session_id(identifier.to_owned()); // TODO close // Self::close(); let events2ui = Arc::new(RwLock::new(events2ui)); let mut session = Session { - id: id.to_owned(), + id: session_id.clone(), sender: Default::default(), lc: Default::default(), events2ui, @@ -67,11 +68,11 @@ impl Session { .lc .write() .unwrap() - .initialize(id.to_owned(), false, false); + .initialize(session_id.clone(), is_file_transfer, false); SESSIONS .write() .unwrap() - .insert(id.to_owned(), session.clone()); + .insert(identifier.to_owned(), session.clone()); std::thread::spawn(move || { Connection::start(session, is_file_transfer); }); @@ -1658,3 +1659,12 @@ pub mod connection_manager { } } } + +#[inline] +pub fn get_session_id(id: String) -> String { + return if let Some(index) = id.find('_') { + id[index + 1..].to_string() + } else { + id + }; +} diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 22243ca79..650a7b0b0 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1,7 +1,7 @@ use crate::client::file_trait::FileManager; +use crate::common::make_fd_to_json; use crate::flutter::connection_manager::{self, get_clients_length, get_clients_state}; use crate::flutter::{self, Session, SESSIONS}; -use crate::common::make_fd_to_json; use crate::start_server; use crate::ui_interface; use flutter_rust_bridge::{StreamSink, SyncReturn, ZeroCopyBuffer}; From d49068706ecf510aefc32de380e04e81d167c383 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 1 Jul 2022 11:26:32 +0800 Subject: [PATCH 0065/2015] add: include_hidden parameters, migrate to bridge --- flutter/lib/models/file_model.dart | 68 ++++++++++++------------------ src/client.rs | 8 ++-- src/client/file_trait.rs | 7 +-- src/flutter.rs | 39 ++++++++++------- src/flutter_ffi.rs | 47 ++++++++++++++------- src/ui/file_transfer.tis | 3 +- src/ui/remote.rs | 10 ++--- 7 files changed, 98 insertions(+), 84 deletions(-) diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index 58ddd658a..adb44286d 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -170,19 +170,18 @@ class FileModel extends ChangeNotifier { if (false == resp) { cancelJob(int.tryParse(evt['id']) ?? 0); } else { - var msg = Map() - ..['id'] = evt['id'] - ..['file_num'] = evt['file_num'] - ..['is_upload'] = evt['is_upload'] - ..['remember'] = fileConfirmCheckboxRemember.toString(); + var need_override = false; if (resp == null) { // skip - msg['need_override'] = 'false'; + need_override = false; } else { // overwrite - msg['need_override'] = 'true'; + need_override = true; } - _ffi.target?.setByName("set_confirm_override_file", jsonEncode(msg)); + _ffi.target?.bind.sessionSetConfirmOverrideFile(id: _ffi.target?.id ?? "", + actId: evt['id'], fileNum: evt['file_num'], + needOverride: need_override, remember: fileConfirmCheckboxRemember, + isUpload: evt['is_upload']); } } @@ -193,22 +192,21 @@ class FileModel extends ChangeNotifier { onReady() async { _localOption.home = _ffi.target?.getByName("get_home_dir") ?? ""; - _localOption.showHidden = - _ffi.target?.getByName("peer_option", "local_show_hidden").isNotEmpty ?? - false; + _localOption.showHidden = (await _ffi.target?.bind.sessionGetPeerOption + (id: _ffi.target?.id ?? "", name: "local_show_hidden"))?.isNotEmpty ?? false; - _remoteOption.showHidden = _ffi.target - ?.getByName("peer_option", "remote_show_hidden") - .isNotEmpty ?? - false; + _remoteOption.showHidden = (await _ffi.target?.bind.sessionGetPeerOption + (id: _ffi.target?.id ?? "", name: "remote_show_hidden"))?.isNotEmpty ?? false; _remoteOption.isWindows = _ffi.target?.ffiModel.pi.platform == "Windows"; debugPrint("remote platform: ${_ffi.target?.ffiModel.pi.platform}"); await Future.delayed(Duration(milliseconds: 100)); - final local = _ffi.target?.getByName("peer_option", "local_dir") ?? ""; - final remote = _ffi.target?.getByName("peer_option", "remote_dir") ?? ""; + final local = (await _ffi.target?.bind.sessionGetPeerOption + (id: _ffi.target?.id ?? "", name: "local_dir")) ?? ""; + final remote = (await _ffi.target?.bind.sessionGetPeerOption + (id: _ffi.target?.id ?? "", name: "remote_dir")) ?? ""; openDirectory(local.isEmpty ? _localOption.home : local, isLocal: true); openDirectory(remote.isEmpty ? _remoteOption.home : remote, isLocal: false); await Future.delayed(Duration(seconds: 1)); @@ -224,23 +222,16 @@ class FileModel extends ChangeNotifier { SmartDialog.dismiss(); // save config - Map msg = Map(); + Map msgMap = Map(); - msg["name"] = "local_dir"; - msg["value"] = _currentLocalDir.path; - _ffi.target?.setByName('peer_option', jsonEncode(msg)); - - msg["name"] = "local_show_hidden"; - msg["value"] = _localOption.showHidden ? "Y" : ""; - _ffi.target?.setByName('peer_option', jsonEncode(msg)); - - msg["name"] = "remote_dir"; - msg["value"] = _currentRemoteDir.path; - _ffi.target?.setByName('peer_option', jsonEncode(msg)); - - msg["name"] = "remote_show_hidden"; - msg["value"] = _remoteOption.showHidden ? "Y" : ""; - _ffi.target?.setByName('peer_option', jsonEncode(msg)); + msgMap["local_dir"] = _currentLocalDir.path; + msgMap["local_show_hidden"] = _localOption.showHidden ? "Y" : ""; + msgMap["remote_dir"] = _currentRemoteDir.path; + msgMap["remote_show_hidden"] = _remoteOption.showHidden ? "Y" : ""; + final id = _ffi.target?.id ?? ""; + for(final msg in msgMap.entries) { + _ffi.target?.bind.sessionPeerOption(id: id, name: msg.key, value: msg.value); + } _currentLocalDir.clear(); _currentRemoteDir.clear(); _localOption.clear(); @@ -583,7 +574,7 @@ class FileFetcher { } // if id == null, means to fetch global FFI - FFI get _ffi => ffi(_id == null ? "" : 'ft_${_id}'); + FFI get _ffi => ffi(_id ?? ""); Future registerReadTask(bool isLocal, String path) { // final jobs = isLocal?localJobs:remoteJobs; // maybe we will use read local dir async later @@ -663,14 +654,7 @@ class FileFetcher { int id, String path, bool isLocal, bool showHidden) async { // TODO test Recursive is show hidden default? try { - final msg = { - "id": id.toString(), - "path": path, - "show_hidden": showHidden.toString(), - "is_remote": (!isLocal).toString() - }; - // TODO - _ffi.setByName("read_dir_recursive", jsonEncode(msg)); + await _ffi.bind.sessionReadDirRecursive(id: _ffi.id, actId: id, path: path, isRemote: !isLocal, showHidden: showHidden); return registerReadRecursiveTask(id); } catch (e) { return Future.error(e); diff --git a/src/client.rs b/src/client.rs index 247e2702c..8457fcd38 100644 --- a/src/client.rs +++ b/src/client.rs @@ -15,6 +15,7 @@ use magnum_opus::{Channels::*, Decoder as AudioDecoder}; use sha2::{Digest, Sha256}; use uuid::Uuid; +pub use file_trait::FileManager; use hbb_common::{ allow_err, anyhow::{anyhow, Context}, @@ -30,13 +31,14 @@ use hbb_common::{ tokio::time::Duration, AddrMangle, ResultType, Stream, }; +pub use helper::LatencyController; use scrap::{Decoder, Image, VideoCodecId}; pub use super::lang::*; + pub mod file_trait; -pub use file_trait::FileManager; pub mod helper; -pub use helper::LatencyController; + pub const SEC30: Duration = Duration::from_secs(30); /// Client of the remote desktop. @@ -1535,7 +1537,7 @@ pub enum Data { Login((String, bool)), Message(Message), SendFiles((i32, String, String, i32, bool, bool)), - RemoveDirAll((i32, String, bool)), + RemoveDirAll((i32, String, bool, bool)), ConfirmDeleteFiles((i32, i32)), SetNoConfirm(i32), RemoveDir((i32, String)), diff --git a/src/client/file_trait.rs b/src/client/file_trait.rs index 6666a2d91..1d5be47da 100644 --- a/src/client/file_trait.rs +++ b/src/client/file_trait.rs @@ -1,6 +1,7 @@ -use super::{Data, Interface}; use hbb_common::{fs, message_proto::*}; +use super::{Data, Interface}; + pub trait FileManager: Interface { fn get_home_dir(&self) -> String { fs::get_home_as_string() @@ -48,8 +49,8 @@ pub trait FileManager: Interface { self.send(Data::RemoveFile((id, path, file_num, is_remote))); } - fn remove_dir_all(&self, id: i32, path: String, is_remote: bool) { - self.send(Data::RemoveDirAll((id, path, is_remote))); + fn remove_dir_all(&self, id: i32, path: String, is_remote: bool, include_hidden: bool) { + self.send(Data::RemoveDirAll((id, path, is_remote, include_hidden))); } fn confirm_delete_files(&self, id: i32, file_num: i32) { diff --git a/src/flutter.rs b/src/flutter.rs index 4854a0e42..4877cce58 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -1,6 +1,10 @@ -use crate::common::make_fd_to_json; -use crate::{client::*, flutter_ffi::EventToUI}; +use std::{ + collections::{HashMap, VecDeque}, + sync::{Arc, Mutex, RwLock}, +}; + use flutter_rust_bridge::{StreamSink, ZeroCopyBuffer}; + use hbb_common::{ allow_err, compress::decompress, @@ -21,10 +25,9 @@ use hbb_common::{ }, Stream, }; -use std::{ - collections::{HashMap, VecDeque}, - sync::{Arc, Mutex, RwLock}, -}; + +use crate::common::make_fd_to_json; +use crate::{client::*, flutter_ffi::EventToUI}; lazy_static::lazy_static! { // static ref SESSION: Arc>> = Default::default(); @@ -52,9 +55,9 @@ impl Session { /// * `id` - The identifier of the remote session with prefix. Regex: [\w]*[\_]*[\d]+ /// * `is_file_transfer` - If the session is used for file transfer. pub fn start(identifier: &str, is_file_transfer: bool, events2ui: StreamSink) { - LocalConfig::set_remote_id(&identifier); // TODO check same id let session_id = get_session_id(identifier.to_owned()); + LocalConfig::set_remote_id(&session_id); // TODO close // Self::close(); let events2ui = Arc::new(RwLock::new(events2ui)); @@ -502,7 +505,11 @@ impl Interface for Session { if lc.is_file_transfer { if pi.username.is_empty() { - self.msgbox("error", "Error", "No active console user logged on, please connect and logon first."); + self.msgbox( + "error", + "Error", + "No active console user logged on, please connect and logon first.", + ); return; } } else { @@ -992,20 +999,20 @@ impl Connection { } } } - Data::RemoveDirAll((id, path, is_remote)) => { + Data::RemoveDirAll((id, path, is_remote, include_hidden)) => { if is_remote { let mut msg_out = Message::new(); let mut file_action = FileAction::new(); file_action.set_all_files(ReadAllFiles { id, path: path.clone(), - include_hidden: true, + include_hidden, ..Default::default() }); msg_out.set_file_action(file_action); allow_err!(peer.send(&msg_out).await); } else { - match fs::get_recursive_files(&path, true) { + match fs::get_recursive_files(&path, include_hidden) { Ok(entries) => { let mut fd = FileDirectory::new(); fd.id = id; @@ -1235,9 +1242,8 @@ pub mod connection_manager { sync::{Mutex, RwLock}, }; - use crate::ipc; - use crate::ipc::Data; - use crate::server::Connection as Conn; + use serde_derive::Serialize; + use hbb_common::{ allow_err, config::Config, @@ -1254,7 +1260,10 @@ pub mod connection_manager { }; #[cfg(any(target_os = "android"))] use scrap::android::call_main_service_set_by_name; - use serde_derive::Serialize; + + use crate::ipc; + use crate::ipc::Data; + use crate::server::Connection as Conn; use super::GLOBAL_EVENT_STREAM; diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 650a7b0b0..9bc533336 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1,21 +1,24 @@ +use std::{ + collections::HashMap, + ffi::{CStr, CString}, + os::raw::c_char, +}; + +use flutter_rust_bridge::{StreamSink, SyncReturn, ZeroCopyBuffer}; +use serde_json::{Number, Value}; + +use hbb_common::ResultType; +use hbb_common::{ + config::{self, Config, LocalConfig, PeerConfig, ONLINE}, + fs, log, +}; + use crate::client::file_trait::FileManager; use crate::common::make_fd_to_json; use crate::flutter::connection_manager::{self, get_clients_length, get_clients_state}; use crate::flutter::{self, Session, SESSIONS}; use crate::start_server; use crate::ui_interface; -use flutter_rust_bridge::{StreamSink, SyncReturn, ZeroCopyBuffer}; -use hbb_common::ResultType; -use hbb_common::{ - config::{self, Config, LocalConfig, PeerConfig, ONLINE}, - fs, log, -}; -use serde_json::{Number, Value}; -use std::{ - collections::HashMap, - ffi::{CStr, CString}, - os::raw::c_char, -}; fn initialize(app_dir: &str) { *config::APP_DIR.write().unwrap() = app_dir.to_owned(); @@ -244,6 +247,13 @@ pub fn session_peer_option(id: String, name: String, value: String) { } } +pub fn session_get_peer_option(id: String, name: String) -> String { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + return session.get_option(&name); + } + "".to_string() +} + pub fn session_input_os_password(id: String, value: String) { if let Some(session) = SESSIONS.read().unwrap().get(&id) { session.input_os_password(value, true); @@ -290,9 +300,15 @@ pub fn session_remove_file(id: String, act_id: i32, path: String, file_num: i32, } } -pub fn session_read_dir_recursive(id: String, act_id: i32, path: String, is_remote: bool) { +pub fn session_read_dir_recursive( + id: String, + act_id: i32, + path: String, + is_remote: bool, + show_hidden: bool, +) { if let Some(session) = SESSIONS.read().unwrap().get(&id) { - session.remove_dir_all(act_id, path, is_remote); + session.remove_dir_all(act_id, path, is_remote, show_hidden); } } @@ -814,13 +830,14 @@ unsafe extern "C" fn set_by_name(name: *const c_char, value: *const c_char) { #[cfg(target_os = "android")] pub mod server_side { - use hbb_common::{config::Config, log}; use jni::{ objects::{JClass, JString}, sys::jstring, JNIEnv, }; + use hbb_common::{config::Config, log}; + use crate::start_server; #[no_mangle] diff --git a/src/ui/file_transfer.tis b/src/ui/file_transfer.tis index 7d50bdf7a..f32540b33 100644 --- a/src/ui/file_transfer.tis +++ b/src/ui/file_transfer.tis @@ -188,7 +188,8 @@ class JobTable: Reactor.Component { job.confirmed = true; return; }else if (job.type == "del-dir"){ - handler.remove_dir_all(job.id, job.path, job.is_remote); + // TODO: include_hidden is always true + handler.remove_dir_all(job.id, job.path, job.is_remote, true); job.confirmed = true; return; } diff --git a/src/ui/remote.rs b/src/ui/remote.rs index a073b81c6..44c3e6c3f 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -201,7 +201,7 @@ impl sciter::EventHandler for Handler { fn read_remote_dir(String, bool); fn send_chat(String); fn switch_display(i32); - fn remove_dir_all(i32, String, bool); + fn remove_dir_all(i32, String, bool, bool); fn confirm_delete_files(i32, i32); fn set_no_confirm(i32); fn cancel_job(i32); @@ -1793,7 +1793,7 @@ impl Remote { } } } - Data::RemoveDirAll((id, path, is_remote)) => { + Data::RemoveDirAll((id, path, is_remote, include_hidden)) => { let sep = self.handler.get_path_sep(is_remote); if is_remote { let mut msg_out = Message::new(); @@ -1801,7 +1801,7 @@ impl Remote { file_action.set_all_files(ReadAllFiles { id, path: path.clone(), - include_hidden: true, + include_hidden, ..Default::default() }); msg_out.set_file_action(file_action); @@ -1809,7 +1809,7 @@ impl Remote { self.remove_jobs .insert(id, RemoveJob::new(Vec::new(), path, sep, is_remote)); } else { - match fs::get_recursive_files(&path, true) { + match fs::get_recursive_files(&path, include_hidden) { Ok(entries) => { let m = make_fd(id, &entries, true); self.handler.call("updateFolderFiles", &make_args!(m)); @@ -2370,7 +2370,7 @@ impl Remote { } back_notification::PrivacyModeState::OffSucceeded => { self.handler - .msgbox("custom-nocancel", "Privacy mode", "Out privacy mode"); + .msgbox("custom-nocancel", "Privacy mode", "Out privacy mode"); self.update_privacy_mode(false); } back_notification::PrivacyModeState::OffByPeer => { From 6b8fc6efe9c74ac3ebcc3fd52330610a5d612e10 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 1 Jul 2022 12:08:52 +0800 Subject: [PATCH 0066/2015] add: file transfer status list like sciter --- .../lib/desktop/pages/file_manager_page.dart | 24 ++++++--- flutter/lib/models/file_model.dart | 49 +++++++++++++------ 2 files changed, 51 insertions(+), 22 deletions(-) diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index ed4a32b37..241a416e5 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -73,8 +73,9 @@ class _FileManagerPageState extends State backgroundColor: MyTheme.grayBg, body: Row( children: [ - Flexible(flex: 1, child: body(isLocal: true)), - Flexible(flex: 1, child: body(isLocal: false)) + Flexible(flex: 3, child: body(isLocal: true)), + Flexible(flex: 3, child: body(isLocal: false)), + Flexible(flex: 2, child: statusList()) ], ), bottomSheet: bottomSheet(), @@ -198,7 +199,7 @@ class _FileManagerPageState extends State itemCount: entries.length + 1, itemBuilder: (context, index) { if (index >= entries.length) { - return listTail(); + return listTail(isLocal: isLocal); } var selected = false; if (model.selectMode) { @@ -297,6 +298,16 @@ class _FileManagerPageState extends State ]); } + /// transfer status list + /// watch transfer status + Widget statusList() { + return PreferredSize(child: Container( + decoration: BoxDecoration( + border: Border.all(color: Colors.white70) + ), + ), preferredSize: Size(200, double.infinity)); + } + goBack({bool? isLocal}) { model.goToParentDirectory(isLocal: isLocal); } @@ -362,7 +373,8 @@ class _FileManagerPageState extends State ], )); - Widget listTail() { + Widget listTail({bool isLocal = false}) { + final dir = isLocal ? model.currentLocalDir : model.currentRemoteDir; return Container( height: 100, child: Column( @@ -370,14 +382,14 @@ class _FileManagerPageState extends State Padding( padding: EdgeInsets.fromLTRB(30, 5, 30, 0), child: Text( - model.currentDir.path, + dir.path, style: TextStyle(color: MyTheme.darkGray), ), ), Padding( padding: EdgeInsets.all(2), child: Text( - "${translate("Total")}: ${model.currentDir.entries.length} ${translate("items")}", + "${translate("Total")}: ${dir.entries.length} ${translate("items")}", style: TextStyle(color: MyTheme.darkGray), ), ) diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index adb44286d..1aecb41e3 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -276,23 +276,40 @@ class FileModel extends ChangeNotifier { openDirectory(parent, isLocal: isLocal); } - sendFiles(SelectedItems items) { - if (items.isLocal == null) { - debugPrint("Failed to sendFiles ,wrong path state"); - return; + /// isRemote only for desktop now, [isRemote == true] means [remote -> local] + sendFiles(SelectedItems items, {bool isRemote = false}) { + if (isDesktop) { + // desktop sendFiles + _jobProgress.state = JobState.inProgress; + final toPath = + isRemote ? currentRemoteDir.path : currentLocalDir.path; + final isWindows = + isRemote ? _localOption.isWindows : _remoteOption.isWindows; + final showHidden = + isRemote ? _localOption.showHidden : _remoteOption.showHidden ; + items.items.forEach((from) async { + _jobId++; + await _ffi.target?.bind.sessionSendFiles(id: '${_ffi.target?.id}', actId: _jobId, path: from.path, to: PathUtil.join(toPath, from.name, isWindows) + ,fileNum: 0, includeHidden: showHidden, isRemote: isRemote); + }); + } else { + if (items.isLocal == null) { + debugPrint("Failed to sendFiles ,wrong path state"); + return; + } + _jobProgress.state = JobState.inProgress; + final toPath = + items.isLocal! ? currentRemoteDir.path : currentLocalDir.path; + final isWindows = + items.isLocal! ? _localOption.isWindows : _remoteOption.isWindows; + final showHidden = + items.isLocal! ? _localOption.showHidden : _remoteOption.showHidden; + items.items.forEach((from) async { + _jobId++; + await _ffi.target?.bind.sessionSendFiles(id: '${_ffi.target?.getId()}', actId: _jobId, path: from.path, to: PathUtil.join(toPath, from.name, isWindows) + ,fileNum: 0, includeHidden: showHidden, isRemote: !(items.isLocal!)); + }); } - _jobProgress.state = JobState.inProgress; - final toPath = - items.isLocal! ? currentRemoteDir.path : currentLocalDir.path; - final isWindows = - items.isLocal! ? _localOption.isWindows : _remoteOption.isWindows; - final showHidden = - items.isLocal! ? _localOption.showHidden : _remoteOption.showHidden; - items.items.forEach((from) async { - _jobId++; - await _ffi.target?.bind.sessionSendFiles(id: '${_ffi.target?.getId()}', actId: _jobId, path: from.path, to: PathUtil.join(toPath, from.name, isWindows) - ,fileNum: 0, includeHidden: showHidden, isRemote: !(items.isLocal!)); - }); } bool removeCheckboxRemember = false; From e7a8bbd291eb0864daa6471dce5f8ffd11855934 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 1 Jul 2022 17:17:25 +0800 Subject: [PATCH 0067/2015] add: use DataTable for desktop file transfer --- .../lib/desktop/pages/file_manager_page.dart | 316 +++++++++++------- 1 file changed, 196 insertions(+), 120 deletions(-) diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 241a416e5..fc9f0994d 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -118,8 +118,7 @@ class _FileManagerPageState extends State PopupMenuItem( child: Row( children: [ - Icon(Icons.folder_outlined, - color: Colors.black), + Icon(Icons.folder_outlined, color: Colors.black), SizedBox(width: 5), Text(translate("Create Folder")) ], @@ -150,16 +149,15 @@ class _FileManagerPageState extends State model.toggleSelectMode(); } else if (v == "folder") { final name = TextEditingController(); - DialogManager.show((setState, close) => - CustomAlertDialog( + DialogManager.show((setState, close) => CustomAlertDialog( title: Text(translate("Create Folder")), content: Column( mainAxisSize: MainAxisSize.min, children: [ TextFormField( decoration: InputDecoration( - labelText: translate( - "Please enter the folder name"), + labelText: + translate("Please enter the folder name"), ), controller: name, ), @@ -192,120 +190,195 @@ class _FileManagerPageState extends State Widget body({bool isLocal = false}) { final fd = isLocal ? model.currentLocalDir : model.currentRemoteDir; final entries = fd.entries; - return Column(children: [ - headTools(isLocal), - Expanded( - child: ListView.builder( - itemCount: entries.length + 1, - itemBuilder: (context, index) { - if (index >= entries.length) { - return listTail(isLocal: isLocal); - } - var selected = false; - if (model.selectMode) { - selected = _selectedItems.contains(entries[index]); - } - - final sizeStr = entries[index].isFile - ? readableFileSize(entries[index].size.toDouble()) - : ""; - return Card( - child: ListTile( - leading: Icon( - entries[index].isFile ? Icons.feed_outlined : Icons.folder, - size: 40), - title: Text(entries[index].name), - selected: selected, - subtitle: Text( - entries[index] - .lastModified() - .toString() - .replaceAll(".000", "") + - " " + - sizeStr, - style: TextStyle(fontSize: 12, color: MyTheme.darkGray), + return Container( + decoration: BoxDecoration( + color: Colors.white70, border: Border.all(color: Colors.grey)), + margin: const EdgeInsets.all(16.0), + padding: const EdgeInsets.all(8.0), + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + headTools(isLocal), + Expanded( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: SingleChildScrollView( + child: DataTable( + showCheckboxColumn: true, + dataRowHeight: 30, + columnSpacing: 8, + columns: [ + DataColumn(label: Text(translate(" "))), // icon + DataColumn( + label: Text( + translate("Name"), + )), + DataColumn(label: Text(translate("Modified"))), + DataColumn(label: Text(translate("Size"))), + ], + rows: entries.map((entry) { + final sizeStr = entry.isFile + ? readableFileSize(entry.size.toDouble()) + : ""; + return DataRow( + key: ValueKey(entry.name), + onSelectChanged: (s) { + // TODO + }, + cells: [ + // TODO: icon + DataCell(Icon( + entry.isFile ? Icons.feed_outlined : Icons.folder, + size: 25)), + DataCell( + ConstrainedBox( + constraints: BoxConstraints(maxWidth: 100), + child: Text(entry.name, + overflow: TextOverflow.ellipsis)), + onTap: () { + if (entry.isDirectory) { + model.openDirectory(entry.path, isLocal: isLocal); + } else { + // Perform file-related tasks. + } + }), + DataCell(Text( + entry + .lastModified() + .toString() + .replaceAll(".000", "") + + " ", + style: TextStyle( + fontSize: 12, color: MyTheme.darkGray), + )), + DataCell(Text( + sizeStr, + style: TextStyle( + fontSize: 12, color: MyTheme.darkGray), + )), + ]); + }).toList(), + ), ), - trailing: needShowCheckBox() - ? Checkbox( - value: selected, - onChanged: (v) { - if (v == null) return; - if (v && !selected) { - _selectedItems.add(isLocal, entries[index]); - } else if (!v && selected) { - _selectedItems.remove(entries[index]); - } - setState(() {}); - }) - : PopupMenuButton( - icon: Icon(Icons.more_vert), - itemBuilder: (context) { - return [ - PopupMenuItem( - child: Text(translate("Delete")), - value: "delete", - ), - PopupMenuItem( - child: Text(translate("Multi Select")), - value: "multi_select", - ), - PopupMenuItem( - child: Text(translate("Properties")), - value: "properties", - enabled: false, - ) - ]; - }, - onSelected: (v) { - if (v == "delete") { - final items = SelectedItems(); - items.add(isLocal, entries[index]); - model.removeAction(items); - } else if (v == "multi_select") { - _selectedItems.clear(); - model.toggleSelectMode(); - } - }), - onTap: () { - if (model.selectMode && !_selectedItems.isOtherPage(isLocal)) { - if (selected) { - _selectedItems.remove(entries[index]); - } else { - _selectedItems.add(isLocal, entries[index]); - } - setState(() {}); - return; - } - if (entries[index].isDirectory) { - model.openDirectory(entries[index].path, isLocal: isLocal); - breadCrumbScrollToEnd(isLocal); - } else { - // Perform file-related tasks. - } - }, - onLongPress: () { - _selectedItems.clear(); - model.toggleSelectMode(); - if (model.selectMode) { - _selectedItems.add(isLocal, entries[index]); - } - setState(() {}); - }, - ), - ); - }, - )) - ]); + ) + ], + )), + Center(child: listTail(isLocal: isLocal)), + // Expanded( + // child: ListView.builder( + // itemCount: entries.length + 1, + // itemBuilder: (context, index) { + // if (index >= entries.length) { + // return listTail(isLocal: isLocal); + // } + // var selected = false; + // if (model.selectMode) { + // selected = _selectedItems.contains(entries[index]); + // } + // + // final sizeStr = entries[index].isFile + // ? readableFileSize(entries[index].size.toDouble()) + // : ""; + // return Card( + // child: ListTile( + // leading: Icon( + // entries[index].isFile ? Icons.feed_outlined : Icons.folder, + // size: 40), + // title: Text(entries[index].name), + // selected: selected, + // subtitle: Text( + // entries[index] + // .lastModified() + // .toString() + // .replaceAll(".000", "") + + // " " + + // sizeStr, + // style: TextStyle(fontSize: 12, color: MyTheme.darkGray), + // ), + // trailing: needShowCheckBox() + // ? Checkbox( + // value: selected, + // onChanged: (v) { + // if (v == null) return; + // if (v && !selected) { + // _selectedItems.add(isLocal, entries[index]); + // } else if (!v && selected) { + // _selectedItems.remove(entries[index]); + // } + // setState(() {}); + // }) + // : PopupMenuButton( + // icon: Icon(Icons.more_vert), + // itemBuilder: (context) { + // return [ + // PopupMenuItem( + // child: Text(translate("Delete")), + // value: "delete", + // ), + // PopupMenuItem( + // child: Text(translate("Multi Select")), + // value: "multi_select", + // ), + // PopupMenuItem( + // child: Text(translate("Properties")), + // value: "properties", + // enabled: false, + // ) + // ]; + // }, + // onSelected: (v) { + // if (v == "delete") { + // final items = SelectedItems(); + // items.add(isLocal, entries[index]); + // model.removeAction(items); + // } else if (v == "multi_select") { + // _selectedItems.clear(); + // model.toggleSelectMode(); + // } + // }), + // onTap: () { + // if (model.selectMode && !_selectedItems.isOtherPage(isLocal)) { + // if (selected) { + // _selectedItems.remove(entries[index]); + // } else { + // _selectedItems.add(isLocal, entries[index]); + // } + // setState(() {}); + // return; + // } + // if (entries[index].isDirectory) { + // model.openDirectory(entries[index].path, isLocal: isLocal); + // breadCrumbScrollToEnd(isLocal); + // } else { + // // Perform file-related tasks. + // } + // }, + // onLongPress: () { + // _selectedItems.clear(); + // model.toggleSelectMode(); + // if (model.selectMode) { + // _selectedItems.add(isLocal, entries[index]); + // } + // setState(() {}); + // }, + // ), + // ); + // }, + // )) + ]), + ); } /// transfer status list /// watch transfer status Widget statusList() { - return PreferredSize(child: Container( - decoration: BoxDecoration( - border: Border.all(color: Colors.white70) - ), - ), preferredSize: Size(200, double.infinity)); + return PreferredSize( + child: Container( + margin: const EdgeInsets.only(top: 16.0,bottom: 16.0, right: 16.0), + padding: const EdgeInsets.all(8.0), + decoration: BoxDecoration(color: Colors.white70,border: Border.all(color: Colors.grey)), + ), + preferredSize: Size(200, double.infinity)); } goBack({bool? isLocal}) { @@ -313,10 +386,10 @@ class _FileManagerPageState extends State } breadCrumbScrollToEnd(bool isLocal) { - final controller = isLocal ? _breadCrumbLocalScroller : _breadCrumbRemoteScroller; + final controller = + isLocal ? _breadCrumbLocalScroller : _breadCrumbRemoteScroller; Future.delayed(Duration(milliseconds: 200), () { - controller.animateTo( - controller.position.maxScrollExtent, + controller.animateTo(controller.position.maxScrollExtent, duration: Duration(milliseconds: 200), curve: Curves.fastLinearToSlowEaseIn); }); @@ -343,7 +416,10 @@ class _FileManagerPageState extends State model.openDirectory(path, isLocal: isLocal); }, isLocal), divider: Icon(Icons.chevron_right), - overflow: ScrollableOverflow(controller: isLocal ? _breadCrumbLocalScroller : _breadCrumbRemoteScroller), + overflow: ScrollableOverflow( + controller: isLocal + ? _breadCrumbLocalScroller + : _breadCrumbRemoteScroller), )), Row( children: [ @@ -478,8 +554,8 @@ class _FileManagerPageState extends State return null; } - List getPathBreadCrumbItems( - void Function() onHome, void Function(List) onPressed, bool isLocal) { + List getPathBreadCrumbItems(void Function() onHome, + void Function(List) onPressed, bool isLocal) { final path = model.shortPath(isLocal); final list = PathUtil.split(path, model.currentIsWindows); final breadCrumbList = [ From 0e7975d39c4292f5e5bc046844a34586c1fa66a3 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 1 Jul 2022 17:33:06 +0800 Subject: [PATCH 0068/2015] fix: ci --- .github/workflows/ci.yml | 60 ++++++++++++++++++++++++++++------------ 1 file changed, 43 insertions(+), 17 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2989051df..5d21dee60 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -78,7 +78,7 @@ jobs: shell: bash run: | case ${{ matrix.job.target }} in - x86_64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev;; + x86_64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libappindicator3-dev;; # arm-unknown-linux-*) sudo apt-get -y update ; sudo apt-get -y install gcc-arm-linux-gnueabihf ;; # aarch64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt-get -y install gcc-aarch64-linux-gnu ;; esac @@ -87,9 +87,36 @@ jobs: uses: subosito/flutter-action@v2 with: channel: 'stable' + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: ${{ matrix.job.target }} + override: true + profile: minimal # minimal component installation (ie, no documentation) + - name: Install flutter rust bridge deps run: | - dart pub global activate ffigen + dart pub global activate ffigen --version 5.0.1 + # flutter_rust_bridge + pushd /tmp && git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1 && popd + pushd /tmp/flutter_rust_bridge/frb_codegen && cargo install --path . && popd + pushd flutter && flutter pub get && popd + ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart + + - name: Install corrosion + run: | + mkdir /tmp/corrosion + pushd /tmp/corrosion + git clone https://github.com/corrosion-rs/corrosion.git + # Optionally, specify -DCMAKE_INSTALL_PREFIX=. You can install Corrosion anyway + cmake -Scorrosion -Bbuild -DCMAKE_BUILD_TYPE=Release + cmake --build build --config Release + # This next step may require sudo or admin privileges if you're installing to a system location, + # which is the default. + sudo cmake --install build --config Release + popd - name: Restore from cache and install vcpkg uses: lukka/run-vcpkg@v7 @@ -100,15 +127,7 @@ jobs: - name: Install vcpkg dependencies run: | $VCPKG_ROOT/vcpkg install libvpx libyuv opus - shell: bash - - - name: Install Rust toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - target: ${{ matrix.job.target }} - override: true - profile: minimal # minimal component installation (ie, no documentation) + shell: bash - name: Show version information (Rust, cargo, GCC) shell: bash @@ -122,12 +141,19 @@ jobs: - uses: Swatinem/rust-cache@v1 - - name: Build - uses: actions-rs/cargo@v1 - with: - use-cross: ${{ matrix.job.use-cross }} - command: build - args: --locked --release --target=${{ matrix.job.target }} +# - name: Build +# uses: actions-rs/cargo@v1 +# with: +# use-cross: ${{ matrix.job.use-cross }} +# command: build +# args: --locked --release --target=${{ matrix.job.target }} --features flutter -v + + - name: Build Flutter + run: | + pushd flutter + flutter pub get + flutter build linux --release -v + popd # - name: Strip debug information from executable # id: strip From 234b8df41717b9c92fd7d16965b61edfd5c2439f Mon Sep 17 00:00:00 2001 From: Asura Date: Mon, 4 Jul 2022 08:08:43 -0700 Subject: [PATCH 0069/2015] fix(pynput): Add dead key conversion rules to support Czech keyboard --- pynput_service.py | 6 +++++- src/ui/remote.rs | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/pynput_service.py b/pynput_service.py index c51e9a524..5aca57986 100644 --- a/pynput_service.py +++ b/pynput_service.py @@ -145,7 +145,11 @@ class MyController(Controller): or (keycode_flag == False and keycode == list(keycode_set)[0] and len(keycode_set) == 1): deakkey_chr = str(key).replace("'", '') keysym = DEAD_KEYS[deakkey_chr] - keycode, shift_state = self.keyboard_mapping[keysym][0] + # shift_state = 0 + keycode, shift_state = list( + filter(lambda x: x[1] == 0, + self.keyboard_mapping[keysym]) + )[0] # If the key has a virtual key code, use that immediately with # fake_input; fake input,being an X server extension, has access to diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 5917314ca..8aa0bccb1 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -276,7 +276,7 @@ impl Handler { KeyRelease(k) => (k, 0), _ => return, }; - log::debug!("{:?}", key); + log::debug!("{:?}", key);a let alt = get_key_state(enigo::Key::Alt); #[cfg(windows)] let ctrl = { From 9237ae30dc200caededbe8e1648f3d1bdd398839 Mon Sep 17 00:00:00 2001 From: Asura Date: Mon, 4 Jul 2022 08:18:58 -0700 Subject: [PATCH 0070/2015] fix(pynput): Add dead key conversion rules to support Czech keyboard --- pynput_service.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pynput_service.py b/pynput_service.py index c51e9a524..5aca57986 100644 --- a/pynput_service.py +++ b/pynput_service.py @@ -145,7 +145,11 @@ class MyController(Controller): or (keycode_flag == False and keycode == list(keycode_set)[0] and len(keycode_set) == 1): deakkey_chr = str(key).replace("'", '') keysym = DEAD_KEYS[deakkey_chr] - keycode, shift_state = self.keyboard_mapping[keysym][0] + # shift_state = 0 + keycode, shift_state = list( + filter(lambda x: x[1] == 0, + self.keyboard_mapping[keysym]) + )[0] # If the key has a virtual key code, use that immediately with # fake_input; fake input,being an X server extension, has access to From beffe44cdb43b6d30218adc881800fb3c9238e98 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sat, 9 Jul 2022 11:27:59 +0800 Subject: [PATCH 0071/2015] fix: workaround for changing root disk on Windows --- flutter/lib/models/file_model.dart | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index 1aecb41e3..c3d44f4a9 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -253,6 +253,13 @@ class FileModel extends ChangeNotifier { isLocal ? _localOption.showHidden : _remoteOption.showHidden; final isWindows = isLocal ? _localOption.isWindows : _remoteOption.isWindows; + // process /C:\ -> C:\ on Windows + if (currentIsWindows && path.length > 1 && path[0] == '/') { + path = path.substring(1); + if (path[path.length - 1] != '\\') { + path = path + "\\"; + } + } try { final fd = await _fileFetcher.fetchDirectory(path, isLocal, showHidden); fd.format(isWindows, sort: _sortStyle); @@ -272,7 +279,13 @@ class FileModel extends ChangeNotifier { } goToParentDirectory({bool? isLocal}) { - final parent = PathUtil.dirname(currentDir.path, currentIsWindows); + final currDir = isLocal != null ? isLocal ? currentLocalDir : currentRemoteDir : currentDir; + var parent = PathUtil.dirname(currDir.path, currentIsWindows); + // specially for C:\, D:\, goto '/' + if (parent == currDir.path && currentIsWindows) { + openDirectory('/', isLocal: isLocal); + return; + } openDirectory(parent, isLocal: isLocal); } From 0598ee304c9e6a98a0f3a34b039ff26176e3a403 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sat, 9 Jul 2022 13:04:22 +0800 Subject: [PATCH 0072/2015] fix: workaround for changing root disk on Windows[2/2] --- flutter/lib/models/file_model.dart | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index c3d44f4a9..7b2456585 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -254,7 +254,7 @@ class FileModel extends ChangeNotifier { final isWindows = isLocal ? _localOption.isWindows : _remoteOption.isWindows; // process /C:\ -> C:\ on Windows - if (currentIsWindows && path.length > 1 && path[0] == '/') { + if (isLocal ? _localOption.isWindows : _remoteOption.isWindows && path.length > 1 && path[0] == '/') { path = path.substring(1); if (path[path.length - 1] != '\\') { path = path + "\\"; @@ -279,10 +279,12 @@ class FileModel extends ChangeNotifier { } goToParentDirectory({bool? isLocal}) { - final currDir = isLocal != null ? isLocal ? currentLocalDir : currentRemoteDir : currentDir; - var parent = PathUtil.dirname(currDir.path, currentIsWindows); + isLocal = isLocal ?? _isLocal; + final isWindows = isLocal ? _localOption.isWindows : _remoteOption.isWindows; + final currDir = isLocal ? currentLocalDir : currentRemoteDir; + var parent = PathUtil.dirname(currDir.path, isWindows); // specially for C:\, D:\, goto '/' - if (parent == currDir.path && currentIsWindows) { + if (parent == currDir.path && isWindows) { openDirectory('/', isLocal: isLocal); return; } From 1db7fee6fbb1feaa0106e8e26cb21155b678a586 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sat, 9 Jul 2022 19:14:40 +0800 Subject: [PATCH 0073/2015] opt: dual selected items & send/receive action icon --- .../lib/desktop/pages/file_manager_page.dart | 130 +++++++++++++----- flutter/lib/models/file_model.dart | 43 ++++-- 2 files changed, 128 insertions(+), 45 deletions(-) diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index fc9f0994d..e9f4ed29c 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:io'; +import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter_breadcrumb/flutter_breadcrumb.dart'; @@ -23,7 +24,8 @@ class FileManagerPage extends StatefulWidget { class _FileManagerPageState extends State with AutomaticKeepAliveClientMixin { - final _selectedItems = SelectedItems(); + final _localSelectedItems = SelectedItems(); + final _remoteSelectedItems = SelectedItems(); final _breadCrumbLocalScroller = ScrollController(); final _breadCrumbRemoteScroller = ScrollController(); @@ -32,6 +34,10 @@ class _FileManagerPageState extends State FileModel get model => _ffi.fileModel; + SelectedItems getSelectedItem(bool isLocal) { + return isLocal ? _localSelectedItems : _remoteSelectedItems; + } + @override void initState() { super.initState(); @@ -83,13 +89,6 @@ class _FileManagerPageState extends State })); } - bool needShowCheckBox() { - if (!model.selectMode) { - return false; - } - return !_selectedItems.isOtherPage(model.isLocal); - } - Widget menu({bool isLocal = false}) { return PopupMenuButton( icon: Icon(Icons.more_vert), @@ -145,7 +144,7 @@ class _FileManagerPageState extends State if (v == "refresh") { model.refresh(); } else if (v == "select") { - _selectedItems.clear(); + _localSelectedItems.clear(); model.toggleSelectMode(); } else if (v == "folder") { final name = TextEditingController(); @@ -223,8 +222,16 @@ class _FileManagerPageState extends State return DataRow( key: ValueKey(entry.name), onSelectChanged: (s) { - // TODO + if (s != null) { + if (s) { + getSelectedItem(isLocal).add(isLocal, entry); + } else { + getSelectedItem(isLocal).remove(entry); + } + setState((){}); + } }, + selected: getSelectedItem(isLocal).contains(entry), cells: [ // TODO: icon DataCell(Icon( @@ -240,6 +247,13 @@ class _FileManagerPageState extends State model.openDirectory(entry.path, isLocal: isLocal); } else { // Perform file-related tasks. + final _selectedItems = getSelectedItem(isLocal); + if (_selectedItems.contains(entry)) { + _selectedItems.remove(entry); + } else { + _selectedItems.add(isLocal, entry); + } + setState((){}); } }), DataCell(Text( @@ -377,6 +391,21 @@ class _FileManagerPageState extends State margin: const EdgeInsets.only(top: 16.0,bottom: 16.0, right: 16.0), padding: const EdgeInsets.all(8.0), decoration: BoxDecoration(color: Colors.white70,border: Border.all(color: Colors.grey)), + child: Obx( + () => ListView.builder( + itemExtent: 100, itemBuilder: (BuildContext context, int index) { + final item = model.jobTable[index + 1]; + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text('${item.id}'), + Icon(Icons.delete) + ], + ); + }, + itemCount: model.jobTable.length, + ), + ), ), preferredSize: Size(200, double.infinity)); } @@ -398,29 +427,46 @@ class _FileManagerPageState extends State Widget headTools(bool isLocal) => Container( child: Row( children: [ + Offstage( + offstage: isLocal, + child: TextButton.icon( + onPressed: (){}, icon: Transform.rotate( + angle: isLocal ? 0 : pi, + child: Icon( + Icons.send + ), + ), label: Text(isLocal ? translate('Send') : translate('Receive'))), + ), Expanded( - child: BreadCrumb( - items: getPathBreadCrumbItems(() => model.goHome(), (list) { - var path = ""; - if (model.currentHome.startsWith(list[0])) { - // absolute path - for (var item in list) { - path = PathUtil.join(path, item, model.currentIsWindows); + child: Container( + decoration: BoxDecoration( + border: Border.all(color: Colors.black12) + ), + child: BreadCrumb( + items: getPathBreadCrumbItems(() => model.goHome(isLocal: isLocal), (list) { + var path = ""; + final currentHome = model.getCurrentHome(isLocal); + final currentIsWindows = model.getCurrentIsWindows(isLocal); + if (currentHome.startsWith(list[0])) { + // absolute path + for (var item in list) { + path = PathUtil.join(path, item, currentIsWindows); + } + } else { + path += currentHome; + for (var item in list) { + path = PathUtil.join(path, item, currentIsWindows); + } } - } else { - path += model.currentHome; - for (var item in list) { - path = PathUtil.join(path, item, model.currentIsWindows); - } - } - model.openDirectory(path, isLocal: isLocal); + model.openDirectory(path, isLocal: isLocal); }, isLocal), divider: Icon(Icons.chevron_right), overflow: ScrollableOverflow( - controller: isLocal - ? _breadCrumbLocalScroller - : _breadCrumbRemoteScroller), - )), + controller: isLocal + ? _breadCrumbLocalScroller + : _breadCrumbRemoteScroller), + ), + )), Row( children: [ IconButton( @@ -443,8 +489,18 @@ class _FileManagerPageState extends State onSelected: (sort) { model.changeSortStyle(sort, isLocal: isLocal); }), - menu(isLocal: isLocal) + menu(isLocal: isLocal), ], + ), + Offstage( + offstage: !isLocal, + child: TextButton.icon( + onPressed: (){}, icon: Transform.rotate( + angle: isLocal ? 0 : pi, + child: Icon( + Icons.send + ), + ), label: Text(isLocal ? translate('Send') : translate('Receive'))), ) ], )); @@ -476,14 +532,14 @@ class _FileManagerPageState extends State Widget? bottomSheet() { final state = model.jobState; - final isOtherPage = _selectedItems.isOtherPage(model.isLocal); - final selectedItemsLen = "${_selectedItems.length} ${translate("items")}"; - final local = _selectedItems.isLocal == null + final isOtherPage = _localSelectedItems.isOtherPage(model.isLocal); + final selectedItemsLen = "${_localSelectedItems.length} ${translate("items")}"; + final local = _localSelectedItems.isLocal == null ? "" - : " [${_selectedItems.isLocal! ? translate("Local") : translate("Remote")}]"; + : " [${_localSelectedItems.isLocal! ? translate("Local") : translate("Remote")}]"; if (model.selectMode) { - if (_selectedItems.length == 0 || !isOtherPage) { + if (_localSelectedItems.length == 0 || !isOtherPage) { return BottomSheetBody( leading: Icon(Icons.check), title: translate("Selected"), @@ -497,8 +553,8 @@ class _FileManagerPageState extends State IconButton( icon: Icon(Icons.delete_forever), onPressed: () { - if (_selectedItems.length > 0) { - model.removeAction(_selectedItems); + if (_localSelectedItems.length > 0) { + model.removeAction(_localSelectedItems); } }, ) @@ -518,7 +574,7 @@ class _FileManagerPageState extends State icon: Icon(Icons.paste), onPressed: () { model.toggleSelectMode(); - model.sendFiles(_selectedItems); + model.sendFiles(_localSelectedItems); }, ) ]); diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index 7b2456585..af5e5db4b 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/mobile/pages/file_manager_page.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; import 'package:path/path.dart' as Path; import 'model.dart'; @@ -22,6 +23,11 @@ class FileModel extends ChangeNotifier { var _jobProgress = JobProgress(); // from rust update + /// JobTable + final _jobTable = List.empty(growable: true).obs; + + RxList get jobTable => _jobTable; + bool get isLocal => _isLocal; bool get selectMode => _selectMode; @@ -46,6 +52,10 @@ class FileModel extends ChangeNotifier { String get currentHome => _isLocal ? _localOption.home : _remoteOption.home; + String getCurrentHome(bool isLocal) { + return isLocal ? _localOption.home : _remoteOption.home; + } + String get currentShortPath { if (currentDir.path.startsWith(currentHome)) { var path = currentDir.path.replaceFirst(currentHome, ""); @@ -81,6 +91,10 @@ class FileModel extends ChangeNotifier { bool get currentIsWindows => _isLocal ? _localOption.isWindows : _remoteOption.isWindows; + bool getCurrentIsWindows(bool isLocal) { + return isLocal ? _localOption.isWindows : _remoteOption.isWindows; + } + final _fileFetcher = FileFetcher(); final _jobResultListener = JobResultListener>(); @@ -115,10 +129,20 @@ class FileModel extends ChangeNotifier { tryUpdateJobProgress(Map evt) { try { int id = int.parse(evt['id']); - _jobProgress.id = id; - _jobProgress.fileNum = int.parse(evt['file_num']); - _jobProgress.speed = double.parse(evt['speed']); - _jobProgress.finishedSize = int.parse(evt['finished_size']); + if (!isDesktop) { + _jobProgress.id = id; + _jobProgress.fileNum = int.parse(evt['file_num']); + _jobProgress.speed = double.parse(evt['speed']); + _jobProgress.finishedSize = int.parse(evt['finished_size']); + } else { + // Desktop uses jobTable + final job = _jobTable[id]; + if (job != null) { + job.fileNum = int.parse(evt['file_num']); + job.speed = double.parse(evt['speed']); + job.finishedSize = int.parse(evt['finished_size']); + } + } notifyListeners(); } catch (e) { debugPrint("Failed to tryUpdateJobProgress,evt:${evt.toString()}"); @@ -270,12 +294,12 @@ class FileModel extends ChangeNotifier { } notifyListeners(); } catch (e) { - debugPrint("Failed to openDirectory :$e"); + debugPrint("Failed to openDirectory ${path} :$e"); } } - goHome() { - openDirectory(currentHome); + goHome({bool? isLocal}) { + openDirectory(currentHome, isLocal: isLocal); } goToParentDirectory({bool? isLocal}) { @@ -303,7 +327,10 @@ class FileModel extends ChangeNotifier { final showHidden = isRemote ? _localOption.showHidden : _remoteOption.showHidden ; items.items.forEach((from) async { - _jobId++; + final jobId = ++_jobId; + _jobTable[jobId] = JobProgress() + ..state = JobState.inProgress + ..id = jobId; await _ffi.target?.bind.sessionSendFiles(id: '${_ffi.target?.id}', actId: _jobId, path: from.path, to: PathUtil.join(toPath, from.name, isWindows) ,fileNum: 0, includeHidden: showHidden, isRemote: isRemote); }); From 79217ca1d993731ddabd03fbdae33b1132cf4481 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 11 Jul 2022 10:30:45 +0800 Subject: [PATCH 0074/2015] add: send/receive file/folder --- flutter/lib/common.dart | 2 +- .../lib/desktop/pages/file_manager_page.dart | 65 ++++++-- flutter/lib/models/file_model.dart | 144 ++++++++++++++---- flutter/lib/models/model.dart | 4 + src/common.rs | 32 +++- src/flutter.rs | 5 +- 6 files changed, 210 insertions(+), 42 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index a987f54df..e1315d233 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -202,7 +202,7 @@ const G = M * K; String readableFileSize(double size) { if (size < K) { - return size.toString() + " B"; + return size.toStringAsFixed(2) + " B"; } else if (size < M) { return (size / K).toStringAsFixed(2) + " KB"; } else if (size < G) { diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index e9f4ed29c..5de1c206c 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -393,13 +393,52 @@ class _FileManagerPageState extends State decoration: BoxDecoration(color: Colors.white70,border: Border.all(color: Colors.grey)), child: Obx( () => ListView.builder( - itemExtent: 100, itemBuilder: (BuildContext context, int index) { - final item = model.jobTable[index + 1]; - return Row( - crossAxisAlignment: CrossAxisAlignment.center, + itemBuilder: (BuildContext context, int index) { + final item = model.jobTable[index]; + return Column( + mainAxisSize: MainAxisSize.min, children: [ - Text('${item.id}'), - Icon(Icons.delete) + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Transform.rotate( + angle: item.isRemote ? pi : 0, + child: Icon(Icons.send)), + SizedBox(width: 16.0,), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Tooltip( + message: item.jobName, + child: Text('${item.jobName}', + maxLines: 1, + style: TextStyle(color: Colors.black45), overflow: TextOverflow.ellipsis,)), + Wrap( + children: [ + Text('${item.state.display()} ${max(0, item.fileNum)}/${item.fileCount} '), + Text('${translate("files")} ${readableFileSize(item.totalSize.toDouble())} '), + Offstage(offstage: item.state != JobState.inProgress, child: Text('${readableFileSize(item.speed) + "/s"} ')), + Text('${(item.finishedSize.toDouble() * 100 / item.totalSize.toDouble()).toStringAsFixed(2)}%'), + ], + ), + ], + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + IconButton(icon: Icon(Icons.delete), onPressed: () { + model.jobTable.removeAt(index); + model.cancelJob(item.id); + },), + ], + ) + ], + ), + SizedBox(height: 8.0,), + Divider(height: 2.0, ) ], ); }, @@ -430,12 +469,15 @@ class _FileManagerPageState extends State Offstage( offstage: isLocal, child: TextButton.icon( - onPressed: (){}, icon: Transform.rotate( + onPressed: (){ + final items = getSelectedItem(isLocal); + model.sendFiles(items, isRemote: true); + }, icon: Transform.rotate( angle: isLocal ? 0 : pi, child: Icon( Icons.send ), - ), label: Text(isLocal ? translate('Send') : translate('Receive'))), + ), label: Text(translate('Receive'))), ), Expanded( child: Container( @@ -495,12 +537,15 @@ class _FileManagerPageState extends State Offstage( offstage: !isLocal, child: TextButton.icon( - onPressed: (){}, icon: Transform.rotate( + onPressed: (){ + final items = getSelectedItem(isLocal); + model.sendFiles(items, isRemote: !isLocal); + }, icon: Transform.rotate( angle: isLocal ? 0 : pi, child: Icon( Icons.send ), - ), label: Text(isLocal ? translate('Send') : translate('Receive'))), + ), label: Text(translate('Send'))), ) ], )); diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index af5e5db4b..996c5112c 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -56,6 +56,10 @@ class FileModel extends ChangeNotifier { return isLocal ? _localOption.home : _remoteOption.home; } + int getJob(int id) { + return jobTable.indexWhere((element) => element.id == id); + } + String get currentShortPath { if (currentDir.path.startsWith(currentHome)) { var path = currentDir.path.replaceFirst(currentHome, ""); @@ -136,11 +140,14 @@ class FileModel extends ChangeNotifier { _jobProgress.finishedSize = int.parse(evt['finished_size']); } else { // Desktop uses jobTable - final job = _jobTable[id]; - if (job != null) { + // id = index + 1 + final jobIndex = getJob(id); + if (jobIndex >= 0 && _jobTable.length > jobIndex){ + final job = _jobTable[jobIndex]; job.fileNum = int.parse(evt['file_num']); job.speed = double.parse(evt['speed']); job.finishedSize = int.parse(evt['finished_size']); + debugPrint("update job ${id} with ${evt}"); } } notifyListeners(); @@ -150,14 +157,28 @@ class FileModel extends ChangeNotifier { } receiveFileDir(Map evt) { - if (_remoteOption.home.isEmpty && evt['is_local'] == "false") { + debugPrint("recv file dir:${evt}"); + if (evt['is_local'] == "false") { // init remote home, the connection will automatic read remote home when established, try { final fd = FileDirectory.fromJson(jsonDecode(evt['value'])); fd.format(_remoteOption.isWindows, sort: _sortStyle); - _remoteOption.home = fd.path; - debugPrint("init remote home:${fd.path}"); - _currentRemoteDir = fd; + if (fd.id > 0){ + final jobIndex = getJob(fd.id); + if (jobIndex != -1){ + final job = jobTable[jobIndex]; + var totalSize = 0; + var fileCount = fd.entries.length; + fd.entries.forEach((element) {totalSize += element.size;}); + job.totalSize = totalSize; + job.fileCount = fileCount; + debugPrint("update receive details:${fd.path}"); + } + } else if (_remoteOption.home.isEmpty) { + _remoteOption.home = fd.path; + debugPrint("init remote home:${fd.path}"); + _currentRemoteDir = fd; + } notifyListeners(); return; } finally {} @@ -166,33 +187,57 @@ class FileModel extends ChangeNotifier { } jobDone(Map evt) { - if (_jobResultListener.isListening) { - _jobResultListener.complete(evt); - return; + if (!isDesktop) { + if (_jobResultListener.isListening) { + _jobResultListener.complete(evt); + return; + } + _selectMode = false; + _jobProgress.state = JobState.done; + } else { + int id = int.parse(evt['id']); + final jobIndex = getJob(id); + if (jobIndex != -1) { + final job = jobTable[jobIndex]; + job.finishedSize = job.totalSize; + job.state = JobState.done; + job.fileNum = int.parse(evt['file_num']); + } } - _selectMode = false; - _jobProgress.state = JobState.done; refresh(); } jobError(Map evt) { - if (_jobResultListener.isListening) { - _jobResultListener.complete(evt); - return; + if (!isDesktop) { + if (_jobResultListener.isListening) { + _jobResultListener.complete(evt); + return; + } + _selectMode = false; + _jobProgress.clear(); + _jobProgress.state = JobState.error; + } else { + int jobIndex = getJob(int.parse(evt['id'])); + if (jobIndex != -1) { + final job = jobTable[jobIndex]; + job.state = JobState.error; + } } - debugPrint("jobError $evt"); - _selectMode = false; - _jobProgress.clear(); - _jobProgress.state = JobState.error; notifyListeners(); } overrideFileConfirm(Map evt) async { final resp = await showFileConfirmDialog( translate("Overwrite"), "${evt['read_path']}", true); + final id = int.tryParse(evt['id']) ?? 0; if (false == resp) { - cancelJob(int.tryParse(evt['id']) ?? 0); + final jobIndex = getJob(id); + if (jobIndex != -1){ + cancelJob(id); + final job = jobTable[jobIndex]; + job.state = JobState.done; + } } else { var need_override = false; if (resp == null) { @@ -203,9 +248,9 @@ class FileModel extends ChangeNotifier { need_override = true; } _ffi.target?.bind.sessionSetConfirmOverrideFile(id: _ffi.target?.id ?? "", - actId: evt['id'], fileNum: evt['file_num'], + actId: id, fileNum: int.parse(evt['file_num']), needOverride: need_override, remember: fileConfirmCheckboxRemember, - isUpload: evt['is_upload']); + isUpload: evt['is_upload'] == "true"); } } @@ -319,7 +364,6 @@ class FileModel extends ChangeNotifier { sendFiles(SelectedItems items, {bool isRemote = false}) { if (isDesktop) { // desktop sendFiles - _jobProgress.state = JobState.inProgress; final toPath = isRemote ? currentRemoteDir.path : currentLocalDir.path; final isWindows = @@ -328,10 +372,14 @@ class FileModel extends ChangeNotifier { isRemote ? _localOption.showHidden : _remoteOption.showHidden ; items.items.forEach((from) async { final jobId = ++_jobId; - _jobTable[jobId] = JobProgress() + _jobTable.add(JobProgress() + ..jobName = from.path + ..totalSize = from.size ..state = JobState.inProgress - ..id = jobId; - await _ffi.target?.bind.sessionSendFiles(id: '${_ffi.target?.id}', actId: _jobId, path: from.path, to: PathUtil.join(toPath, from.name, isWindows) + ..id = jobId + ..isRemote = isRemote + ); + _ffi.target?.bind.sessionSendFiles(id: '${_ffi.target?.id}', actId: _jobId, path: from.path, to: PathUtil.join(toPath, from.name, isWindows) ,fileNum: 0, includeHidden: showHidden, isRemote: isRemote); }); } else { @@ -543,20 +591,20 @@ class FileModel extends ChangeNotifier { } sendRemoveFile(String path, int fileNum, bool isLocal) { - _ffi.target?.bind.sessionRemoveFile(id: '${_ffi.target?.getId()}', actId: _jobId, path: path, isRemote: !isLocal, fileNum: fileNum); + _ffi.target?.bind.sessionRemoveFile(id: '${_ffi.target?.id}', actId: _jobId, path: path, isRemote: !isLocal, fileNum: fileNum); } sendRemoveEmptyDir(String path, int fileNum, bool isLocal) { - _ffi.target?.bind.sessionRemoveAllEmptyDirs(id: '${_ffi.target?.getId()}', actId: _jobId, path: path, isRemote: !isLocal); + _ffi.target?.bind.sessionRemoveAllEmptyDirs(id: '${_ffi.target?.id}', actId: _jobId, path: path, isRemote: !isLocal); } createDir(String path) async { _jobId++; - _ffi.target?.bind.sessionCreateDir(id: '${_ffi.target?.getId()}', actId: _jobId, path: path, isRemote: !isLocal); + _ffi.target?.bind.sessionCreateDir(id: '${_ffi.target?.id}', actId: _jobId, path: path, isRemote: !isLocal); } cancelJob(int id) async { - _ffi.target?.bind.sessionCancelJob(id: '${_ffi.target?.getId()}', actId: id); + _ffi.target?.bind.sessionCancelJob(id: '${_ffi.target?.id}', actId: id); jobReset(); } @@ -577,6 +625,21 @@ class FileModel extends ChangeNotifier { initFileFetcher() { _fileFetcher.id = _ffi.target?.id; } + + void updateFolderFiles(Map evt) { + // ret: "{\"id\":1,\"num_entries\":12,\"total_size\":1264822.0}" + Map info = json.decode(evt['info']); + int id = info['id']; + int num_entries = info['num_entries']; + double total_size = info['total_size']; + final jobIndex = getJob(id); + if (jobIndex != -1) { + final job = jobTable[jobIndex]; + job.fileCount = num_entries; + job.totalSize = total_size.toInt(); + } + debugPrint("update folder files: ${info}"); + } } class JobResultListener { @@ -784,12 +847,33 @@ class Entry { enum JobState { none, inProgress, done, error } +extension JobStateDisplay on JobState { + String display() { + switch (this) { + case JobState.none: + return translate("Waiting"); + case JobState.inProgress: + return translate("Transfer File"); + case JobState.done: + return translate("Finished"); + case JobState.error: + return translate("Error"); + default: + return ""; + } + } +} + class JobProgress { JobState state = JobState.none; var id = 0; var fileNum = 0; var speed = 0.0; var finishedSize = 0; + var totalSize = 0; + var fileCount = 0; + var isRemote = false; + var jobName = ""; clear() { state = JobState.none; @@ -797,6 +881,8 @@ class JobProgress { fileNum = 0; speed = 0; finishedSize = 0; + jobName = ""; + fileCount = 0; } } diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index e5e521035..a76fe8e04 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -168,6 +168,8 @@ class FfiModel with ChangeNotifier { parent.target?.fileModel.jobError(evt); } else if (name == 'override_file_confirm') { parent.target?.fileModel.overrideFileConfirm(evt); + } else if (name == 'update_folder_files') { + parent.target?.fileModel.updateFolderFiles(evt); } else if (name == 'try_start_without_auth') { parent.target?.serverModel.loginRequest(evt); } else if (name == 'on_client_authorized') { @@ -217,6 +219,8 @@ class FfiModel with ChangeNotifier { parent.target?.fileModel.jobError(evt); } else if (name == 'override_file_confirm') { parent.target?.fileModel.overrideFileConfirm(evt); + } else if (name == 'update_folder_files') { + parent.target?.fileModel.updateFolderFiles(evt); } else if (name == 'try_start_without_auth') { parent.target?.serverModel.loginRequest(evt); } else if (name == 'on_client_authorized') { diff --git a/src/common.rs b/src/common.rs index 92ccb901e..c344b93a1 100644 --- a/src/common.rs +++ b/src/common.rs @@ -1,5 +1,9 @@ +use std::sync::{Arc, Mutex}; + #[cfg(not(any(target_os = "android", target_os = "ios")))] pub use arboard::Clipboard as ClipboardContext; +use serde_json::json; + use hbb_common::{ allow_err, anyhow::bail, @@ -14,7 +18,6 @@ use hbb_common::{ }; // #[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))] use hbb_common::{config::RENDEZVOUS_PORT, futures::future::join_all}; -use std::sync::{Arc, Mutex}; pub const CLIPBOARD_NAME: &'static str = "clipboard"; pub const CLIPBOARD_INTERVAL: u64 = 333; @@ -633,3 +636,30 @@ pub fn make_fd_to_json(fd: FileDirectory) -> String { fd_json.insert("entries".into(), json!(entries)); serde_json::to_string(&fd_json).unwrap_or("".into()) } + +pub fn make_fd_flutter(id: i32, entries: &Vec, only_count: bool) -> String { + let mut m = serde_json::Map::new(); + m.insert("id".into(), json!(id)); + let mut a = vec![]; + let mut n: u64 = 0; + for entry in entries { + n += entry.size; + if only_count { + continue; + } + let mut e = serde_json::Map::new(); + e.insert("name".into(), json!(entry.name.to_owned())); + let tmp = entry.entry_type.value(); + e.insert("type".into(), json!(if tmp == 0 { 1 } else { tmp })); + e.insert("time".into(), json!(entry.modified_time as f64)); + e.insert("size".into(), json!(entry.size as f64)); + a.push(e); + } + if only_count { + m.insert("num_entries".into(), json!(entries.len() as i32)); + } else { + m.insert("entries".into(), json!(a)); + } + m.insert("total_size".into(), json!(n as f64)); + serde_json::to_string(&m).unwrap_or("".into()) +} diff --git a/src/flutter.rs b/src/flutter.rs index 4877cce58..8514e7515 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -27,7 +27,7 @@ use hbb_common::{ }; use crate::common::make_fd_to_json; -use crate::{client::*, flutter_ffi::EventToUI}; +use crate::{client::*, flutter_ffi::EventToUI, make_fd_flutter}; lazy_static::lazy_static! { // static ref SESSION: Arc>> = Default::default(); @@ -991,6 +991,9 @@ impl Connection { to, job.files().len() ); + let m = make_fd_flutter(id, job.files(), true); + self.session + .push_event("update_folder_files", vec![("info", &m)]); let files = job.files().clone(); self.read_jobs.push(job); self.timer = time::interval(MILLI1); From 5aded67597fc497a25f08349f6546f9a408f3a17 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 11 Jul 2022 16:07:49 +0800 Subject: [PATCH 0075/2015] add: sortby, address link, platform, last jobs[1/2] --- .../lib/desktop/pages/file_manager_page.dart | 452 ++++++++++-------- flutter/lib/models/file_model.dart | 78 ++- flutter/macos/Runner/bridge_generated.h | 8 +- src/flutter.rs | 37 ++ src/flutter_ffi.rs | 13 + 5 files changed, 376 insertions(+), 212 deletions(-) diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 5de1c206c..e3ffa9d0c 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -1,4 +1,3 @@ -import 'dart:async'; import 'dart:io'; import 'dart:math'; @@ -26,8 +25,6 @@ class _FileManagerPageState extends State with AutomaticKeepAliveClientMixin { final _localSelectedItems = SelectedItems(); final _remoteSelectedItems = SelectedItems(); - final _breadCrumbLocalScroller = ScrollController(); - final _breadCrumbRemoteScroller = ScrollController(); /// FFI with name file_transfer_id FFI get _ffi => ffi('ft_${widget.id}'); @@ -94,41 +91,11 @@ class _FileManagerPageState extends State icon: Icon(Icons.more_vert), itemBuilder: (context) { return [ - PopupMenuItem( - child: Row( - children: [ - Icon(Icons.refresh, color: Colors.black), - SizedBox(width: 5), - Text(translate("Refresh File")) - ], - ), - value: "refresh", - ), - PopupMenuItem( - child: Row( - children: [ - Icon(Icons.check, color: Colors.black), - SizedBox(width: 5), - Text(translate("Multi Select")) - ], - ), - value: "select", - ), - PopupMenuItem( - child: Row( - children: [ - Icon(Icons.folder_outlined, color: Colors.black), - SizedBox(width: 5), - Text(translate("Create Folder")) - ], - ), - value: "folder", - ), PopupMenuItem( child: Row( children: [ Icon( - model.currentShowHidden + model.getCurrentShowHidden(isLocal) ? Icons.check_box_outlined : Icons.check_box_outline_blank, color: Colors.black), @@ -141,46 +108,7 @@ class _FileManagerPageState extends State ]; }, onSelected: (v) { - if (v == "refresh") { - model.refresh(); - } else if (v == "select") { - _localSelectedItems.clear(); - model.toggleSelectMode(); - } else if (v == "folder") { - final name = TextEditingController(); - DialogManager.show((setState, close) => CustomAlertDialog( - title: Text(translate("Create Folder")), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TextFormField( - decoration: InputDecoration( - labelText: - translate("Please enter the folder name"), - ), - controller: name, - ), - ], - ), - actions: [ - TextButton( - style: flatButtonStyle, - onPressed: () => close(false), - child: Text(translate("Cancel"))), - ElevatedButton( - style: flatButtonStyle, - onPressed: () { - if (name.value.text.isNotEmpty) { - model.createDir(PathUtil.join( - model.currentDir.path, - name.value.text, - model.currentIsWindows)); - close(); - } - }, - child: Text(translate("OK"))) - ])); - } else if (v == "hidden") { + if (v == "hidden") { model.toggleShowHidden(local: isLocal); } }); @@ -189,9 +117,23 @@ class _FileManagerPageState extends State Widget body({bool isLocal = false}) { final fd = isLocal ? model.currentLocalDir : model.currentRemoteDir; final entries = fd.entries; + final sortIndex = (SortBy style) { + switch (style) { + case SortBy.Name: + return 1; + case SortBy.Type: + return 0; + case SortBy.Modified: + return 2; + case SortBy.Size: + return 3; + } + }(model.getSortStyle(isLocal)); + final sortAscending = + isLocal ? model.localSortAscending : model.remoteSortAscending; return Container( decoration: BoxDecoration( - color: Colors.white70, border: Border.all(color: Colors.grey)), + color: Colors.white54, border: Border.all(color: Colors.black26)), margin: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(8.0), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -204,16 +146,36 @@ class _FileManagerPageState extends State child: SingleChildScrollView( child: DataTable( showCheckboxColumn: true, - dataRowHeight: 30, + dataRowHeight: 25, + headingRowHeight: 30, columnSpacing: 8, + showBottomBorder: true, + sortColumnIndex: sortIndex, + sortAscending: sortAscending, columns: [ DataColumn(label: Text(translate(" "))), // icon DataColumn( label: Text( - translate("Name"), - )), - DataColumn(label: Text(translate("Modified"))), - DataColumn(label: Text(translate("Size"))), + translate("Name"), + ), + 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: entries.map((entry) { final sizeStr = entry.isFile @@ -228,23 +190,29 @@ class _FileManagerPageState extends State } else { getSelectedItem(isLocal).remove(entry); } - setState((){}); + setState(() {}); } }, selected: getSelectedItem(isLocal).contains(entry), cells: [ - // TODO: icon DataCell(Icon( entry.isFile ? Icons.feed_outlined : Icons.folder, size: 25)), DataCell( ConstrainedBox( constraints: BoxConstraints(maxWidth: 100), - child: Text(entry.name, - overflow: TextOverflow.ellipsis)), - onTap: () { + child: Tooltip( + message: entry.name, + child: Text(entry.name, + overflow: TextOverflow.ellipsis), + )), onTap: () { if (entry.isDirectory) { model.openDirectory(entry.path, isLocal: isLocal); + if (isLocal) { + _localSelectedItems.clear(); + } else { + _remoteSelectedItems.clear(); + } } else { // Perform file-related tasks. final _selectedItems = getSelectedItem(isLocal); @@ -253,7 +221,7 @@ class _FileManagerPageState extends State } else { _selectedItems.add(isLocal, entry); } - setState((){}); + setState(() {}); } }), DataCell(Text( @@ -277,7 +245,7 @@ class _FileManagerPageState extends State ) ], )), - Center(child: listTail(isLocal: isLocal)), + // Center(child: listTail(isLocal: isLocal)), // Expanded( // child: ListView.builder( // itemCount: entries.length + 1, @@ -388,9 +356,10 @@ class _FileManagerPageState extends State Widget statusList() { return PreferredSize( child: Container( - margin: const EdgeInsets.only(top: 16.0,bottom: 16.0, right: 16.0), + margin: const EdgeInsets.only(top: 16.0, bottom: 16.0, right: 16.0), padding: const EdgeInsets.all(8.0), - decoration: BoxDecoration(color: Colors.white70,border: Border.all(color: Colors.grey)), + decoration: BoxDecoration( + color: Colors.white70, border: Border.all(color: Colors.grey)), child: Obx( () => ListView.builder( itemBuilder: (BuildContext context, int index) { @@ -404,7 +373,9 @@ class _FileManagerPageState extends State Transform.rotate( angle: item.isRemote ? pi : 0, child: Icon(Icons.send)), - SizedBox(width: 16.0,), + SizedBox( + width: 16.0, + ), Expanded( child: Column( mainAxisSize: MainAxisSize.min, @@ -412,15 +383,28 @@ class _FileManagerPageState extends State children: [ Tooltip( message: item.jobName, - child: Text('${item.jobName}', + child: Text( + '${item.jobName}', maxLines: 1, - style: TextStyle(color: Colors.black45), overflow: TextOverflow.ellipsis,)), + style: TextStyle(color: Colors.black45), + overflow: TextOverflow.ellipsis, + )), Wrap( children: [ - Text('${item.state.display()} ${max(0, item.fileNum)}/${item.fileCount} '), - Text('${translate("files")} ${readableFileSize(item.totalSize.toDouble())} '), - Offstage(offstage: item.state != JobState.inProgress, child: Text('${readableFileSize(item.speed) + "/s"} ')), - Text('${(item.finishedSize.toDouble() * 100 / item.totalSize.toDouble()).toStringAsFixed(2)}%'), + Text( + '${item.state.display()} ${max(0, item.fileNum)}/${item.fileCount} '), + Text( + '${translate("files")} ${readableFileSize(item.totalSize.toDouble())} '), + Offstage( + offstage: + item.state != JobState.inProgress, + child: Text( + '${readableFileSize(item.speed) + "/s"} ')), + Offstage( + offstage: item.totalSize <= 0, + child: Text( + '${(item.finishedSize.toDouble() * 100 / item.totalSize.toDouble()).toStringAsFixed(2)}%'), + ), ], ), ], @@ -429,19 +413,26 @@ class _FileManagerPageState extends State Row( mainAxisAlignment: MainAxisAlignment.end, children: [ - IconButton(icon: Icon(Icons.delete), onPressed: () { - model.jobTable.removeAt(index); - model.cancelJob(item.id); - },), + IconButton( + icon: Icon(Icons.delete), + onPressed: () { + model.jobTable.removeAt(index); + model.cancelJob(item.id); + }, + ), ], ) ], ), - SizedBox(height: 8.0,), - Divider(height: 2.0, ) + SizedBox( + height: 8.0, + ), + Divider( + height: 2.0, + ) ], ); - }, + }, itemCount: model.jobTable.length, ), ), @@ -453,100 +444,175 @@ class _FileManagerPageState extends State model.goToParentDirectory(isLocal: isLocal); } - breadCrumbScrollToEnd(bool isLocal) { - final controller = - isLocal ? _breadCrumbLocalScroller : _breadCrumbRemoteScroller; - Future.delayed(Duration(milliseconds: 200), () { - controller.animateTo(controller.position.maxScrollExtent, - duration: Duration(milliseconds: 200), - curve: Curves.fastLinearToSlowEaseIn); - }); - } - Widget headTools(bool isLocal) => Container( - child: Row( + child: Column( children: [ - Offstage( - offstage: isLocal, - child: TextButton.icon( - onPressed: (){ - final items = getSelectedItem(isLocal); - model.sendFiles(items, isRemote: true); - }, icon: Transform.rotate( - angle: isLocal ? 0 : pi, - child: Icon( - Icons.send + // symbols + PreferredSize( + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + width: 50, + height: 50, + decoration: BoxDecoration(color: Colors.blue), + padding: EdgeInsets.all(8.0), + child: FutureBuilder( + future: _ffi.bind.sessionGetPlatform( + id: _ffi.id, isRemote: !isLocal), + builder: (context, snapshot) { + if (snapshot.hasData && snapshot.data!.isNotEmpty) { + return getPlatformImage('${snapshot.data}'); + } else { + return CircularProgressIndicator(color: Colors.white,); + } + })), + Text(isLocal + ? translate("Local Computer") + : translate("Remote Computer")) + .marginOnly(left: 8.0) + ], ), - ), label: Text(translate('Receive'))), - ), - Expanded( - child: Container( - decoration: BoxDecoration( - border: Border.all(color: Colors.black12) - ), - child: BreadCrumb( - items: getPathBreadCrumbItems(() => model.goHome(isLocal: isLocal), (list) { - var path = ""; - final currentHome = model.getCurrentHome(isLocal); - final currentIsWindows = model.getCurrentIsWindows(isLocal); - if (currentHome.startsWith(list[0])) { - // absolute path - for (var item in list) { - path = PathUtil.join(path, item, currentIsWindows); - } - } else { - path += currentHome; - for (var item in list) { - path = PathUtil.join(path, item, currentIsWindows); - } - } - model.openDirectory(path, isLocal: isLocal); - }, isLocal), - divider: Icon(Icons.chevron_right), - overflow: ScrollableOverflow( - controller: isLocal - ? _breadCrumbLocalScroller - : _breadCrumbRemoteScroller), - ), - )), + preferredSize: Size(double.infinity, 70)), + // buttons Row( children: [ - IconButton( - icon: Icon(Icons.arrow_upward), - onPressed: () { - goBack(isLocal: isLocal); - }, + Row( + children: [ + IconButton( + onPressed: () { + model.goHome(isLocal: isLocal); + }, + icon: Icon(Icons.home_outlined)), + IconButton( + icon: Icon(Icons.arrow_upward), + onPressed: () { + goBack(isLocal: isLocal); + }, + ), + menu(isLocal: isLocal), + ], ), - PopupMenuButton( - icon: Icon(Icons.sort), - itemBuilder: (context) { - return SortBy.values - .map((e) => PopupMenuItem( - child: - Text(translate(e.toString().split(".").last)), - value: e, - )) - .toList(); + Expanded( + child: Container( + decoration: BoxDecoration( + border: Border.all(color: Colors.black12)), + child: TextField( + decoration: InputDecoration( + border: InputBorder.none, + isDense: true, + prefix: Padding(padding: EdgeInsets.only(left: 4.0)), + suffix: DropdownButton( + isDense: true, + underline: Offstage(), + items: [ + // TODO: favourite + DropdownMenuItem(child: Text('/'), value: '/',) + ], onChanged: (path) { + if (path is String && path.isNotEmpty){ + model.openDirectory(path, isLocal: isLocal); + } + }) + ), + controller: TextEditingController( + text: isLocal + ? model.currentLocalDir.path + : model.currentRemoteDir.path), + onSubmitted: (path) { + model.openDirectory(path, isLocal: isLocal); + }, + ))), + IconButton( + onPressed: () { + model.refresh(isLocal: isLocal); }, - onSelected: (sort) { - model.changeSortStyle(sort, isLocal: isLocal); - }), - menu(isLocal: isLocal), + icon: Icon(Icons.refresh)) ], ), - Offstage( - offstage: !isLocal, - child: TextButton.icon( - onPressed: (){ - final items = getSelectedItem(isLocal); - model.sendFiles(items, isRemote: !isLocal); - }, icon: Transform.rotate( - angle: isLocal ? 0 : pi, - child: Icon( - Icons.send + Row( + textDirection: isLocal ? TextDirection.ltr : TextDirection.rtl, + children: [ + Expanded( + child: Row( + mainAxisAlignment: + isLocal ? MainAxisAlignment.start : MainAxisAlignment.end, + children: [ + IconButton( + onPressed: () { + final name = TextEditingController(); + DialogManager.show((setState, close) => + CustomAlertDialog( + title: Text(translate("Create Folder")), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextFormField( + decoration: InputDecoration( + labelText: translate( + "Please enter the folder name"), + ), + controller: name, + ), + ], + ), + actions: [ + TextButton( + style: flatButtonStyle, + onPressed: () => close(false), + child: Text(translate("Cancel"))), + ElevatedButton( + style: flatButtonStyle, + onPressed: () { + if (name.value.text.isNotEmpty) { + model.createDir( + PathUtil.join( + model + .getCurrentDir(isLocal) + .path, + name.value.text, + model.getCurrentIsWindows( + isLocal)), + isLocal: isLocal); + close(); + } + }, + child: Text(translate("OK"))) + ])); + }, + icon: Icon(Icons.create_new_folder_outlined)), + IconButton( + onPressed: () async { + final items = isLocal + ? _localSelectedItems + : _remoteSelectedItems; + debugPrint("remove items: ${items.items}"); + await (model.removeAction(items)); + items.clear(); + }, + icon: Icon(Icons.delete_forever_outlined)), + ], + ), ), - ), label: Text(translate('Send'))), - ) + TextButton.icon( + onPressed: () { + final items = getSelectedItem(isLocal); + model.sendFiles(items, isRemote: !isLocal); + }, + icon: Transform.rotate( + angle: isLocal ? 0 : pi, + child: Icon( + Icons.send, + color: Colors.black54, + ), + ), + label: Text( + isLocal ? translate('Send') : translate('Receive'), + style: TextStyle( + color: Colors.black54, + ), + )), + ], + ).marginOnly(top: 8.0) ], )); @@ -578,7 +644,8 @@ class _FileManagerPageState extends State Widget? bottomSheet() { final state = model.jobState; final isOtherPage = _localSelectedItems.isOtherPage(model.isLocal); - final selectedItemsLen = "${_localSelectedItems.length} ${translate("items")}"; + final selectedItemsLen = + "${_localSelectedItems.length} ${translate("items")}"; final local = _localSelectedItems.isLocal == null ? "" : " [${_localSelectedItems.isLocal! ? translate("Local") : translate("Remote")}]"; @@ -677,6 +744,15 @@ class _FileManagerPageState extends State @override bool get wantKeepAlive => true; + + /// Get the image for the current [platform]. + Widget getPlatformImage(String platform) { + platform = platform.toLowerCase(); + if (platform == 'mac os') + platform = 'mac'; + else if (platform != 'linux' && platform != 'android') platform = 'win'; + return Image.asset('assets/$platform.png', width: 25, height: 25); + } } class BottomSheetBody extends StatelessWidget { diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index 996c5112c..bd71aff15 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -40,6 +40,20 @@ class FileModel extends ChangeNotifier { SortBy get sortStyle => _sortStyle; + SortBy _localSortStyle = SortBy.Name; + + bool _localSortAscending = true; + + bool _remoteSortAscending = true; + + SortBy _remoteSortStyle = SortBy.Name; + + bool get localSortAscending => _localSortAscending; + + SortBy getSortStyle(bool isLocal){ + return isLocal ? _localSortStyle : _remoteSortStyle; + } + FileDirectory _currentLocalDir = FileDirectory(); FileDirectory get currentLocalDir => _currentLocalDir; @@ -50,6 +64,10 @@ class FileModel extends ChangeNotifier { FileDirectory get currentDir => _isLocal ? currentLocalDir : currentRemoteDir; + FileDirectory getCurrentDir(bool isLocal) { + return isLocal ? currentLocalDir : currentRemoteDir; + } + String get currentHome => _isLocal ? _localOption.home : _remoteOption.home; String getCurrentHome(bool isLocal) { @@ -92,6 +110,10 @@ class FileModel extends ChangeNotifier { bool get currentShowHidden => _isLocal ? _localOption.showHidden : _remoteOption.showHidden; + bool getCurrentShowHidden(bool isLocal) { + return isLocal ? _localOption.showHidden : _remoteOption.showHidden; + } + bool get currentIsWindows => _isLocal ? _localOption.isWindows : _remoteOption.isWindows; @@ -163,13 +185,15 @@ class FileModel extends ChangeNotifier { try { final fd = FileDirectory.fromJson(jsonDecode(evt['value'])); fd.format(_remoteOption.isWindows, sort: _sortStyle); - if (fd.id > 0){ + if (fd.id > 0) { final jobIndex = getJob(fd.id); - if (jobIndex != -1){ + if (jobIndex != -1) { final job = jobTable[jobIndex]; var totalSize = 0; var fileCount = fd.entries.length; - fd.entries.forEach((element) {totalSize += element.size;}); + fd.entries.forEach((element) { + totalSize += element.size; + }); job.totalSize = totalSize; job.fileCount = fileCount; debugPrint("update receive details:${fd.path}"); @@ -179,11 +203,11 @@ class FileModel extends ChangeNotifier { debugPrint("init remote home:${fd.path}"); _currentRemoteDir = fd; } - notifyListeners(); - return; - } finally {} + } + finally {} } _fileFetcher.tryCompleteTask(evt['value'], evt['is_local']); + notifyListeners(); } jobDone(Map evt) { @@ -307,10 +331,10 @@ class FileModel extends ChangeNotifier { _remoteOption.clear(); } - refresh() { + refresh({bool? isLocal}) { if (isDesktop) { - openDirectory(currentRemoteDir.path); - openDirectory(currentLocalDir.path); + isLocal = isLocal ?? _isLocal; + isLocal ? openDirectory(currentLocalDir.path) : openDirectory(currentRemoteDir.path); } else { openDirectory(currentDir.path); } @@ -344,7 +368,8 @@ class FileModel extends ChangeNotifier { } goHome({bool? isLocal}) { - openDirectory(currentHome, isLocal: isLocal); + isLocal = isLocal ?? _isLocal; + openDirectory(getCurrentHome(isLocal), isLocal: isLocal); } goToParentDirectory({bool? isLocal}) { @@ -598,7 +623,8 @@ class FileModel extends ChangeNotifier { _ffi.target?.bind.sessionRemoveAllEmptyDirs(id: '${_ffi.target?.id}', actId: _jobId, path: path, isRemote: !isLocal); } - createDir(String path) async { + createDir(String path, {bool? isLocal}) async { + isLocal = isLocal ?? this.isLocal; _jobId++; _ffi.target?.bind.sessionCreateDir(id: '${_ffi.target?.id}', actId: _jobId, path: path, isRemote: !isLocal); } @@ -608,16 +634,20 @@ class FileModel extends ChangeNotifier { jobReset(); } - changeSortStyle(SortBy sort, {bool? isLocal}) { + changeSortStyle(SortBy sort, {bool? isLocal, bool ascending = true}) { _sortStyle = sort; if (isLocal == null) { // compatible for mobile logic - _currentLocalDir.changeSortStyle(sort); - _currentRemoteDir.changeSortStyle(sort); + _currentLocalDir.changeSortStyle(sort, ascending: ascending); + _currentRemoteDir.changeSortStyle(sort, ascending: ascending); + _localSortStyle = sort; _localSortAscending = ascending; + _remoteSortStyle = sort; _remoteSortAscending = ascending; } else if (isLocal) { - _currentLocalDir.changeSortStyle(sort); + _currentLocalDir.changeSortStyle(sort, ascending: ascending); + _localSortStyle = sort; _localSortAscending = ascending; } else { - _currentRemoteDir.changeSortStyle(sort); + _currentRemoteDir.changeSortStyle(sort, ascending: ascending); + _remoteSortStyle = sort; _remoteSortAscending = ascending; } notifyListeners(); } @@ -640,6 +670,8 @@ class FileModel extends ChangeNotifier { } debugPrint("update folder files: ${info}"); } + + bool get remoteSortAscending => _remoteSortAscending; } class JobResultListener { @@ -809,8 +841,8 @@ class FileDirectory { } } - changeSortStyle(SortBy sort) { - entries = _sortList(entries, sort); + changeSortStyle(SortBy sort, {bool ascending = true}) { + entries = _sortList(entries, sort, ascending); } clear() { @@ -929,7 +961,7 @@ class DirectoryOption { } // code from file_manager pkg after edit -List _sortList(List list, SortBy sortType) { +List _sortList(List list, SortBy sortType, bool ascending) { if (sortType == SortBy.Name) { // making list of only folders. final dirs = list.where((element) => element.isDirectory).toList(); @@ -942,7 +974,7 @@ List _sortList(List list, SortBy sortType) { files.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase())); // first folders will go to list (if available) then files will go to list. - return [...dirs, ...files]; + return ascending ? [...dirs, ...files] : [...dirs.reversed.toList(), ...files.reversed.toList()]; } else if (sortType == SortBy.Modified) { // making the list of Path & DateTime List<_PathStat> _pathStat = []; @@ -957,7 +989,7 @@ List _sortList(List list, SortBy sortType) { list.sort((a, b) => _pathStat .indexWhere((element) => element.path == a.name) .compareTo(_pathStat.indexWhere((element) => element.path == b.name))); - return list; + return ascending ? list : list.reversed.toList(); } else if (sortType == SortBy.Type) { // making list of only folders. final dirs = list.where((element) => element.isDirectory).toList(); @@ -974,7 +1006,7 @@ List _sortList(List list, SortBy sortType) { .split('.') .last .compareTo(b.name.toLowerCase().split('.').last)); - return [...dirs, ...files]; + return ascending ? [...dirs, ...files]: [...dirs.reversed.toList(), ...files.reversed.toList()]; } else if (sortType == SortBy.Size) { // create list of path and size Map _sizeMap = {}; @@ -999,7 +1031,7 @@ List _sortList(List list, SortBy sortType) { .indexWhere((element) => element.key == a.name) .compareTo( _sizeMapList.indexWhere((element) => element.key == b.name))); - return [...dirs, ...files]; + return ascending ? [...dirs, ...files]: [...dirs.reversed.toList(), ...files.reversed.toList()]; } return []; } diff --git a/flutter/macos/Runner/bridge_generated.h b/flutter/macos/Runner/bridge_generated.h index 7f072e770..163ad91cd 100644 --- a/flutter/macos/Runner/bridge_generated.h +++ b/flutter/macos/Runner/bridge_generated.h @@ -102,6 +102,10 @@ void wire_session_peer_option(int64_t port_, struct wire_uint_8_list *name, struct wire_uint_8_list *value); +void wire_session_get_peer_option(int64_t port_, + struct wire_uint_8_list *id, + struct wire_uint_8_list *name); + void wire_session_input_os_password(int64_t port_, struct wire_uint_8_list *id, struct wire_uint_8_list *value); @@ -139,7 +143,8 @@ void wire_session_read_dir_recursive(int64_t port_, struct wire_uint_8_list *id, int32_t act_id, struct wire_uint_8_list *path, - bool is_remote); + bool is_remote, + bool show_hidden); void wire_session_remove_all_empty_dirs(int64_t port_, struct wire_uint_8_list *id, @@ -197,6 +202,7 @@ static int64_t dummy_method_to_enforce_bundling(void) { dummy_var ^= ((int64_t) (void*) wire_session_send_chat); dummy_var ^= ((int64_t) (void*) wire_session_send_mouse); dummy_var ^= ((int64_t) (void*) wire_session_peer_option); + dummy_var ^= ((int64_t) (void*) wire_session_get_peer_option); dummy_var ^= ((int64_t) (void*) wire_session_input_os_password); dummy_var ^= ((int64_t) (void*) wire_session_read_remote_dir); dummy_var ^= ((int64_t) (void*) wire_session_send_files); diff --git a/src/flutter.rs b/src/flutter.rs index 8514e7515..ff278f3d0 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -5,6 +5,8 @@ use std::{ use flutter_rust_bridge::{StreamSink, ZeroCopyBuffer}; +use hbb_common::config::PeerConfig; +use hbb_common::fs::TransferJobMeta; use hbb_common::{ allow_err, compress::decompress, @@ -464,6 +466,41 @@ impl Session { log::debug!("{:?}", msg_out); self.send_msg(msg_out); } + + pub fn load_config(&self) -> PeerConfig { + load_config(&self.id) + } + + pub fn get_platform(&self, is_remote: bool) -> String { + if is_remote { + self.lc.read().unwrap().info.platform.clone() + } else { + whoami::platform().to_string() + } + } + + pub fn load_last_jobs(&self) { + let pc = self.load_config(); + if pc.transfer.write_jobs.is_empty() && pc.transfer.read_jobs.is_empty() { + // no last jobs + return; + } + let mut cnt = 1; + for job_str in pc.transfer.read_jobs.iter() { + if !job_str.is_empty() { + self.push_event("addJob", vec![("value", job_str)]); + cnt += 1; + println!("restore read_job: {:?}", job); + } + } + for job_str in pc.transfer.write_jobs.iter() { + if !job_str.is_empty() { + self.push_event("addJob", vec![("value", job_str)]); + cnt += 1; + println!("restore write_job: {:?}", job); + } + } + } } impl FileManager for Session {} diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 9bc533336..327e79ef5 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -339,6 +339,19 @@ pub fn session_read_local_dir_sync(id: String, path: String, show_hidden: bool) "".to_string() } +pub fn session_get_platform(id: String, is_remote: bool) -> String { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + return session.get_platform(is_remote); + } + "".to_string() +} + +pub fn session_load_last_transfer_jobs(id: String) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + return session.load_last_jobs(); + } +} + /// FFI for **get** commands which are idempotent. /// Return result in c string. /// From 9094999a8abdde7287b1f40aae7afc8273f8d587 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 11 Jul 2022 18:23:58 +0800 Subject: [PATCH 0076/2015] add: implement last jobs[2/2] --- .../lib/desktop/pages/file_manager_page.dart | 9 ++ flutter/lib/models/file_model.dart | 60 ++++++++- flutter/lib/models/model.dart | 4 + src/client/file_trait.rs | 4 +- src/flutter.rs | 124 ++++++++++++++++-- src/flutter_ffi.rs | 26 ++++ 6 files changed, 210 insertions(+), 17 deletions(-) diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index e3ffa9d0c..6e8dd57c8 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -413,6 +413,14 @@ class _FileManagerPageState extends State Row( mainAxisAlignment: MainAxisAlignment.end, children: [ + Offstage( + offstage: item.state != JobState.paused, + child: IconButton( + onPressed: () { + model.resumeJob(item.id); + }, + icon: Icon(Icons.restart_alt_rounded)), + ), IconButton( icon: Icon(Icons.delete), onPressed: () { @@ -597,6 +605,7 @@ class _FileManagerPageState extends State onPressed: () { final items = getSelectedItem(isLocal); model.sendFiles(items, isRemote: !isLocal); + items.clear(); }, icon: Transform.rotate( angle: isLocal ? 0 : pi, diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index bd71aff15..ba76d52ae 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -309,6 +309,8 @@ class FileModel extends ChangeNotifier { if (_currentRemoteDir.path.isEmpty) { openDirectory(_remoteOption.home, isLocal: false); } + // load last transfer jobs + await _ffi.target?.bind.sessionLoadLastTransferJobs(id: '${_ffi.target?.id}'); } onClose() { @@ -390,11 +392,11 @@ class FileModel extends ChangeNotifier { if (isDesktop) { // desktop sendFiles final toPath = - isRemote ? currentRemoteDir.path : currentLocalDir.path; + isRemote ? currentLocalDir.path : currentRemoteDir.path; final isWindows = - isRemote ? _localOption.isWindows : _remoteOption.isWindows; + isRemote ? _remoteOption.isWindows : _localOption.isWindows; final showHidden = - isRemote ? _localOption.showHidden : _remoteOption.showHidden ; + isRemote ? _remoteOption.showHidden : _localOption.showHidden; items.items.forEach((from) async { final jobId = ++_jobId; _jobTable.add(JobProgress() @@ -406,6 +408,7 @@ class FileModel extends ChangeNotifier { ); _ffi.target?.bind.sessionSendFiles(id: '${_ffi.target?.id}', actId: _jobId, path: from.path, to: PathUtil.join(toPath, from.name, isWindows) ,fileNum: 0, includeHidden: showHidden, isRemote: isRemote); + print("path:${from.path}, toPath:${toPath}, to:${PathUtil.join(toPath, from.name, isWindows)}"); }); } else { if (items.isLocal == null) { @@ -672,6 +675,50 @@ class FileModel extends ChangeNotifier { } bool get remoteSortAscending => _remoteSortAscending; + + void loadLastJob(Map evt) { + debugPrint("load last job: ${evt}"); + Map jobDetail = json.decode(evt['value']); + // int id = int.parse(jobDetail['id']); + String remote = jobDetail['remote']; + String to = jobDetail['to']; + bool showHidden = jobDetail['show_hidden']; + int fileNum = jobDetail['file_num']; + bool isRemote = jobDetail['is_remote']; + final currJobId = _jobId++; + var jobProgress = JobProgress() + ..jobName = isRemote ? remote : to + ..id = currJobId + ..isRemote = isRemote + ..fileNum = fileNum + ..remote = remote + ..to = to + ..showHidden = showHidden + ..state = JobState.paused; + jobTable.add(jobProgress); + _ffi.target?.bind.sessionAddJob(id: '${_ffi.target?.id}', + isRemote: isRemote, + includeHidden: showHidden, + actId: currJobId, + path: isRemote ? remote : to, + to: isRemote ? to: remote, + fileNum: fileNum, + ); + } + + resumeJob(int jobId) { + final jobIndex = getJob(jobId); + if (jobIndex != -1) { + final job = jobTable[jobIndex]; + _ffi.target?.bind.sessionResumeJob(id: '${_ffi.target?.id}', + actId: job.id, + isRemote: job.isRemote); + job.state = JobState.inProgress; + } else { + debugPrint("jobId ${jobId} is not exists"); + } + notifyListeners(); + } } class JobResultListener { @@ -877,7 +924,7 @@ class Entry { } } -enum JobState { none, inProgress, done, error } +enum JobState { none, inProgress, done, error, paused } extension JobStateDisplay on JobState { String display() { @@ -906,6 +953,9 @@ class JobProgress { var fileCount = 0; var isRemote = false; var jobName = ""; + var remote = ""; + var to = ""; + var showHidden = false; clear() { state = JobState.none; @@ -915,6 +965,8 @@ class JobProgress { finishedSize = 0; jobName = ""; fileCount = 0; + remote = ""; + to = ""; } } diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index a76fe8e04..45a5bc696 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -168,6 +168,8 @@ class FfiModel with ChangeNotifier { parent.target?.fileModel.jobError(evt); } else if (name == 'override_file_confirm') { parent.target?.fileModel.overrideFileConfirm(evt); + } else if (name == 'load_last_job') { + parent.target?.fileModel.loadLastJob(evt); } else if (name == 'update_folder_files') { parent.target?.fileModel.updateFolderFiles(evt); } else if (name == 'try_start_without_auth') { @@ -219,6 +221,8 @@ class FfiModel with ChangeNotifier { parent.target?.fileModel.jobError(evt); } else if (name == 'override_file_confirm') { parent.target?.fileModel.overrideFileConfirm(evt); + } else if (name == 'load_last_job') { + parent.target?.fileModel.loadLastJob(evt); } else if (name == 'update_folder_files') { parent.target?.fileModel.updateFolderFiles(evt); } else if (name == 'try_start_without_auth') { diff --git a/src/client/file_trait.rs b/src/client/file_trait.rs index 1d5be47da..cc149c53f 100644 --- a/src/client/file_trait.rs +++ b/src/client/file_trait.rs @@ -93,7 +93,7 @@ pub trait FileManager: Interface { } fn add_job( - &mut self, + &self, id: i32, path: String, to: String, @@ -111,7 +111,7 @@ pub trait FileManager: Interface { ))); } - fn resume_job(&mut self, id: i32, is_remote: bool) { + fn resume_job(&self, id: i32, is_remote: bool) { self.send(Data::ResumeJob((id, is_remote))); } } diff --git a/src/flutter.rs b/src/flutter.rs index ff278f3d0..2807d1711 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -5,8 +5,8 @@ use std::{ use flutter_rust_bridge::{StreamSink, ZeroCopyBuffer}; -use hbb_common::config::PeerConfig; -use hbb_common::fs::TransferJobMeta; +use hbb_common::config::{PeerConfig, TransferSerde}; +use hbb_common::fs::{get_job, TransferJobMeta}; use hbb_common::{ allow_err, compress::decompress, @@ -471,6 +471,10 @@ impl Session { load_config(&self.id) } + pub fn save_config(&self, config: &PeerConfig) { + config.store(&self.id); + } + pub fn get_platform(&self, is_remote: bool) -> String { if is_remote { self.lc.read().unwrap().info.platform.clone() @@ -488,16 +492,16 @@ impl Session { let mut cnt = 1; for job_str in pc.transfer.read_jobs.iter() { if !job_str.is_empty() { - self.push_event("addJob", vec![("value", job_str)]); + self.push_event("load_last_job", vec![("value", job_str)]); cnt += 1; - println!("restore read_job: {:?}", job); + println!("restore read_job: {:?}", job_str); } } for job_str in pc.transfer.write_jobs.iter() { if !job_str.is_empty() { - self.push_event("addJob", vec![("value", job_str)]); + self.push_event("load_last_job", vec![("value", job_str)]); cnt += 1; - println!("restore write_job: {:?}", job); + println!("restore write_job: {:?}", job_str); } } } @@ -978,6 +982,7 @@ impl Connection { async fn handle_msg_from_ui(&mut self, data: Data, peer: &mut Stream) -> bool { match data { Data::Close => { + self.sync_jobs_status_to_local().await; return false; } Data::Login((password, remember)) => { @@ -989,8 +994,7 @@ impl Connection { allow_err!(peer.send(&msg).await); } Data::SendFiles((id, path, to, file_num, include_hidden, is_remote)) => { - // in mobile, can_enable_override_detection is always true - let od = true; + let od = can_enable_overwrite_detection(self.session.lc.read().unwrap().version); if is_remote { log::debug!("New job {}, write to {} from remote {}", id, to, path); self.write_jobs.push(fs::TransferJob::new_write( @@ -1001,7 +1005,7 @@ impl Connection { include_hidden, is_remote, Vec::new(), - true, + od, )); allow_err!( peer.send(&fs::new_send(id, path, file_num, include_hidden)) @@ -1015,7 +1019,7 @@ impl Connection { file_num, include_hidden, is_remote, - true, + od, ) { Err(err) => { self.handle_job_status(id, -1, Some(err.to_string())); @@ -1180,6 +1184,87 @@ impl Connection { } } } + Data::AddJob((id, path, to, file_num, include_hidden, is_remote)) => { + let od = can_enable_overwrite_detection(self.session.lc.read().unwrap().version); + if is_remote { + log::debug!( + "new write waiting job {}, write to {} from remote {}", + id, + to, + path + ); + let mut job = fs::TransferJob::new_write( + id, + path.clone(), + to, + file_num, + include_hidden, + is_remote, + Vec::new(), + od, + ); + job.is_last_job = true; + self.write_jobs.push(job); + } else { + match fs::TransferJob::new_read( + id, + to.clone(), + path.clone(), + file_num, + include_hidden, + is_remote, + od, + ) { + Err(err) => { + self.handle_job_status(id, -1, Some(err.to_string())); + } + Ok(mut job) => { + log::debug!( + "new read waiting job {}, read {} to remote {}, {} files", + id, + path, + to, + job.files().len() + ); + let m = make_fd_flutter(job.id(), job.files(), true); + self.session + .push_event("update_folder_files", vec![("info", &m)]); + job.is_last_job = true; + self.read_jobs.push(job); + self.timer = time::interval(MILLI1); + } + } + } + } + Data::ResumeJob((id, is_remote)) => { + if is_remote { + if let Some(job) = get_job(id, &mut self.write_jobs) { + job.is_last_job = false; + allow_err!( + peer.send(&fs::new_send( + id, + job.remote.clone(), + job.file_num, + job.show_hidden + )) + .await + ); + } + } else { + if let Some(job) = get_job(id, &mut self.read_jobs) { + job.is_last_job = false; + allow_err!( + peer.send(&fs::new_receive( + id, + job.path.to_string_lossy().to_string(), + job.file_num, + job.files.clone() + )) + .await + ); + } + } + } _ => {} } true @@ -1269,6 +1354,24 @@ impl Connection { ], ); } + + async fn sync_jobs_status_to_local(&mut self) -> bool { + log::info!("sync transfer job status"); + let mut config: PeerConfig = self.session.load_config(); + let mut transfer_metas = TransferSerde::default(); + for job in self.read_jobs.iter() { + let json_str = serde_json::to_string(&job.gen_meta()).unwrap(); + transfer_metas.read_jobs.push(json_str); + } + for job in self.write_jobs.iter() { + let json_str = serde_json::to_string(&job.gen_meta()).unwrap(); + transfer_metas.write_jobs.push(json_str); + } + log::info!("meta: {:?}", transfer_metas); + config.transfer = transfer_metas; + self.session.save_config(&config); + true + } } // Server Side @@ -1510,7 +1613,6 @@ pub mod connection_manager { mut files, } => { // in mobile, can_enable_override_detection is always true - let od = true; WRITE_JOBS.lock().unwrap().push(fs::TransferJob::new_write( id, "".to_string(), diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 327e79ef5..f2bef5716 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -349,6 +349,32 @@ pub fn session_get_platform(id: String, is_remote: bool) -> String { pub fn session_load_last_transfer_jobs(id: String) { if let Some(session) = SESSIONS.read().unwrap().get(&id) { return session.load_last_jobs(); + } else { + // a tip for flutter dev + eprintln!( + "cannot load last transfer job from non-existed session. Please ensure session \ + is connected before calling load last transfer jobs." + ); + } +} + +pub fn session_add_job( + id: String, + act_id: i32, + path: String, + to: String, + file_num: i32, + include_hidden: bool, + is_remote: bool, +) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.add_job(act_id, path, to, file_num, include_hidden, is_remote); + } +} + +pub fn session_resume_job(id: String, act_id: i32, is_remote: bool) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.resume_job(act_id, is_remote); } } From e82e0bf69706914a57b16fba945fb29c2d17e370 Mon Sep 17 00:00:00 2001 From: Asura Date: Mon, 11 Jul 2022 03:26:12 -0700 Subject: [PATCH 0077/2015] feat: Add new simulate key method --- Cargo.lock | 19 +++++++++++- libs/enigo/Cargo.toml | 3 +- libs/enigo/src/linux.rs | 65 +++++++++++++++++++++++++++++++++++++---- 3 files changed, 80 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7e679ef91..bdf981356 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1317,6 +1317,7 @@ dependencies = [ "log", "objc", "pkg-config", + "rdev 0.5.1", "serde 1.0.137", "serde_derive", "unicode-segmentation", @@ -3836,6 +3837,22 @@ dependencies = [ "x11", ] +[[package]] +name = "rdev" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7336f02e29f34e9a7186ccf87051f6a5697536195569012e076e18a4efddcede" +dependencies = [ + "cocoa 0.22.0", + "core-foundation 0.7.0", + "core-foundation-sys 0.7.0", + "core-graphics 0.19.2", + "lazy_static", + "libc", + "winapi 0.3.9", + "x11", +] + [[package]] name = "rdrand" version = "0.4.0" @@ -4076,7 +4093,7 @@ dependencies = [ "num_cpus", "objc", "parity-tokio-ipc", - "rdev", + "rdev 0.5.0", "repng", "reqwest", "rpassword 6.0.1", diff --git a/libs/enigo/Cargo.toml b/libs/enigo/Cargo.toml index 6842dab56..29d7ee87f 100644 --- a/libs/enigo/Cargo.toml +++ b/libs/enigo/Cargo.toml @@ -21,7 +21,8 @@ appveyor = { repository = "pythoneer/enigo-85xiy" } [dependencies] serde = { version = "1.0", optional = true } serde_derive = { version = "1.0", optional = true } -log = "0.4" +log = "0.4.17" +rdev = "0.5.1" [features] with_serde = ["serde", "serde_derive"] diff --git a/libs/enigo/src/linux.rs b/libs/enigo/src/linux.rs index adfe9507c..ee27e06cf 100644 --- a/libs/enigo/src/linux.rs +++ b/libs/enigo/src/linux.rs @@ -3,8 +3,8 @@ use libc; use crate::{Key, KeyboardControllable, MouseButton, MouseControllable}; use self::libc::{c_char, c_int, c_void, useconds_t}; -use std::{borrow::Cow, ffi::CString, io::prelude::*, ptr, sync::mpsc}; - +use rdev::{simulate, EventType, EventType::*, Key as RdevKey, SimulateError}; +use std::{borrow::Cow, ffi::CString, io::prelude::*, ptr, sync::mpsc, thread, time}; const CURRENT_WINDOW: c_int = 0; const DEFAULT_DELAY: u64 = 12000; type Window = c_int; @@ -103,6 +103,30 @@ impl Enigo { pub fn reset(&mut self) { self.tx.send((PyMsg::Char('\0'), true)).ok(); } + + fn send_rdev(&mut self, key: &Key, is_press: bool) -> bool { + log::info!("{:?} {:?}", key, is_press); + + if let Key::Raw(keycode) = key { + let event_type = match is_press { + // todo: Acccodding to client type + true => Box::leak(Box::new(EventType::KeyPress(RdevKey::Unknown( + (*keycode).into(), + )))), + false => Box::leak(Box::new(EventType::KeyRelease(RdevKey::Unknown( + (*keycode).into(), + )))), + }; + + match simulate(event_type) { + Ok(()) => true, + Err(SimulateError) => false, + } + } else { + false + } + } + #[inline] fn send_pynput(&mut self, key: &Key, is_press: bool) -> bool { if unsafe { PYNPUT_EXIT || !PYNPUT_REDAY } { @@ -111,8 +135,15 @@ impl Enigo { if let Key::Layout(c) = key { return self.tx.send((PyMsg::Char(*c), is_press)).is_ok(); } - if let Key::Raw(_) = key { - return false; + if let Key::Raw(chr) = key { + fn string_to_static_str(s: String) -> &'static str { + Box::leak(s.into_boxed_str()) + } + dbg!(chr.to_string()); + return self + .tx + .send((PyMsg::Str(string_to_static_str(chr.to_string())), is_press)) + .is_ok(); } #[allow(deprecated)] let s = match key { @@ -431,6 +462,9 @@ impl KeyboardControllable for Enigo { if self.xdo.is_null() { return Ok(()); } + if self.send_rdev(&key, true) { + return Ok(()); + } if self.send_pynput(&key, true) { return Ok(()); } @@ -449,6 +483,11 @@ impl KeyboardControllable for Enigo { if self.xdo.is_null() { return; } + // todo + let keyboard_mode = 1; + if keyboard_mode == 1 && self.send_rdev(&key, false) { + return; + } if self.send_pynput(&key, false) { return; } @@ -478,6 +517,21 @@ impl KeyboardControllable for Enigo { } } } + + fn key_sequence_parse(&mut self, sequence: &str) + where + Self: Sized, + { + self.key_sequence_parse_try(sequence) + .expect("Could not parse sequence"); + } + + fn key_sequence_parse_try(&mut self, sequence: &str) -> Result<(), crate::dsl::ParseError> + where + Self: Sized, + { + crate::dsl::eval(self, sequence) + } } static mut PYNPUT_EXIT: bool = false; @@ -492,7 +546,8 @@ fn start_pynput_service(rx: mpsc::Receiver<(PyMsg, bool)>) { py = "/usr/lib/rustdesk/pynput_service.py".to_owned(); if !std::path::Path::new(&py).exists() { // enigo libs, not rustdesk root project, so skip using appimage features - py = std::env::var("APPDIR").unwrap_or("".to_string()) + "/usr/lib/rustdesk/pynput_service.py"; + py = std::env::var("APPDIR").unwrap_or("".to_string()) + + "/usr/lib/rustdesk/pynput_service.py"; if !std::path::Path::new(&py).exists() { log::error!("{} not exists", py); } From 956cef4a1c6eac44a06500e268f30224448c9833 Mon Sep 17 00:00:00 2001 From: Asura Date: Mon, 11 Jul 2022 08:14:57 -0700 Subject: [PATCH 0078/2015] refactor: Use new keyboard mode --- src/server/connection.rs | 3 ++- src/server/input_service.rs | 13 +++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/server/connection.rs b/src/server/connection.rs index 304b20655..40b541c99 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -421,7 +421,8 @@ impl Connection { msg.down = true; } handle_key(&msg); - if press { + let keyboard_mode = 1; + if press && keyboard_mode != 1{ msg.down = false; handle_key(&msg); } diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 8c5f3060b..a8fd5fbbb 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -585,6 +585,19 @@ fn handle_key_(evt: &KeyEvent) { #[cfg(windows)] crate::platform::windows::try_change_desktop(); let mut en = ENIGO.lock().unwrap(); + let keyboard_mode = 1; + if keyboard_mode == 1 { + if let Some(key_event::Union::chr(chr)) = evt.union { + if evt.down { + println!("key down: {:?}", chr); + en.key_down(Key::Raw(chr.try_into().unwrap())); + } else { + println!("key up: {:?}", chr); + en.key_up(Key::Raw(chr.try_into().unwrap())); + } + } + return; + } // disable numlock if press home etc when numlock is on, // because we will get numpad value (7,8,9 etc) if not #[cfg(windows)] From 6d61987c58ea0518dfe00e79fd8db0abc571bbc7 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 12 Jul 2022 11:51:58 +0800 Subject: [PATCH 0079/2015] fix: file transfer update issue --- .../lib/desktop/pages/file_manager_page.dart | 170 +----------------- flutter/lib/models/file_model.dart | 37 ++-- 2 files changed, 23 insertions(+), 184 deletions(-) diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 6e8dd57c8..de6d981ee 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -2,7 +2,6 @@ import 'dart:io'; import 'dart:math'; import 'package:flutter/material.dart'; -import 'package:flutter_breadcrumb/flutter_breadcrumb.dart'; import 'package:flutter_hbb/mobile/pages/file_manager_page.dart'; import 'package:flutter_hbb/models/file_model.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; @@ -81,7 +80,6 @@ class _FileManagerPageState extends State Flexible(flex: 2, child: statusList()) ], ), - bottomSheet: bottomSheet(), )); })); } @@ -593,8 +591,7 @@ class _FileManagerPageState extends State final items = isLocal ? _localSelectedItems : _remoteSelectedItems; - debugPrint("remove items: ${items.items}"); - await (model.removeAction(items)); + await (model.removeAction(items, isLocal: isLocal)); items.clear(); }, icon: Icon(Icons.delete_forever_outlined)), @@ -650,107 +647,6 @@ class _FileManagerPageState extends State ); } - Widget? bottomSheet() { - final state = model.jobState; - final isOtherPage = _localSelectedItems.isOtherPage(model.isLocal); - final selectedItemsLen = - "${_localSelectedItems.length} ${translate("items")}"; - final local = _localSelectedItems.isLocal == null - ? "" - : " [${_localSelectedItems.isLocal! ? translate("Local") : translate("Remote")}]"; - - if (model.selectMode) { - if (_localSelectedItems.length == 0 || !isOtherPage) { - return BottomSheetBody( - leading: Icon(Icons.check), - title: translate("Selected"), - text: selectedItemsLen + local, - onCanceled: () => model.toggleSelectMode(), - actions: [ - IconButton( - icon: Icon(Icons.compare_arrows), - onPressed: model.togglePage, - ), - IconButton( - icon: Icon(Icons.delete_forever), - onPressed: () { - if (_localSelectedItems.length > 0) { - model.removeAction(_localSelectedItems); - } - }, - ) - ]); - } else { - return BottomSheetBody( - leading: Icon(Icons.input), - title: translate("Paste here?"), - text: selectedItemsLen + local, - onCanceled: () => model.toggleSelectMode(), - actions: [ - IconButton( - icon: Icon(Icons.compare_arrows), - onPressed: model.togglePage, - ), - IconButton( - icon: Icon(Icons.paste), - onPressed: () { - model.toggleSelectMode(); - model.sendFiles(_localSelectedItems); - }, - ) - ]); - } - } - - switch (state) { - case JobState.inProgress: - return BottomSheetBody( - leading: CircularProgressIndicator(), - title: translate("Waiting"), - text: - "${translate("Speed")}: ${readableFileSize(model.jobProgress.speed)}/s", - onCanceled: () => model.cancelJob(model.jobProgress.id), - ); - case JobState.done: - return BottomSheetBody( - leading: Icon(Icons.check), - title: "${translate("Successful")}!", - text: "", - onCanceled: () => model.jobReset(), - ); - case JobState.error: - return BottomSheetBody( - leading: Icon(Icons.error), - title: "${translate("Error")}!", - text: "", - onCanceled: () => model.jobReset(), - ); - case JobState.none: - break; - } - return null; - } - - List getPathBreadCrumbItems(void Function() onHome, - void Function(List) onPressed, bool isLocal) { - final path = model.shortPath(isLocal); - final list = PathUtil.split(path, model.currentIsWindows); - final breadCrumbList = [ - BreadCrumbItem( - content: IconButton( - icon: Icon(Icons.home_filled), - onPressed: onHome, - )) - ]; - breadCrumbList.addAll(list.asMap().entries.map((e) => BreadCrumbItem( - content: TextButton( - child: Text(e.value), - style: - ButtonStyle(minimumSize: MaterialStateProperty.all(Size(0, 0))), - onPressed: () => onPressed(list.sublist(0, e.key + 1)))))); - return breadCrumbList; - } - @override bool get wantKeepAlive => true; @@ -763,67 +659,3 @@ class _FileManagerPageState extends State return Image.asset('assets/$platform.png', width: 25, height: 25); } } - -class BottomSheetBody extends StatelessWidget { - BottomSheetBody( - {required this.leading, - required this.title, - required this.text, - this.onCanceled, - this.actions}); - - final Widget leading; - final String title; - final String text; - final VoidCallback? onCanceled; - final List? actions; - - @override - BottomSheet build(BuildContext context) { - final _actions = actions ?? []; - return BottomSheet( - builder: (BuildContext context) { - return Container( - height: 65, - alignment: Alignment.centerLeft, - decoration: BoxDecoration( - color: MyTheme.accent50, - borderRadius: BorderRadius.vertical(top: Radius.circular(10))), - child: Padding( - padding: EdgeInsets.symmetric(horizontal: 15), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - leading, - SizedBox(width: 16), - Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(title, style: TextStyle(fontSize: 18)), - Text(text, - style: TextStyle( - fontSize: 14, color: MyTheme.grayBg)) - ], - ) - ], - ), - Row(children: () { - _actions.add(IconButton( - icon: Icon(Icons.cancel_outlined), - onPressed: onCanceled, - )); - return _actions; - }()) - ], - ), - )); - }, - onClosing: () {}, - backgroundColor: MyTheme.grayBg, - enableDrag: false, - ); - } -} diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index ba76d52ae..5bca33303 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -149,7 +149,7 @@ class FileModel extends ChangeNotifier { } else { _remoteOption.showHidden = showHidden ?? !_remoteOption.showHidden; } - refresh(); + refresh(isLocal: local); } tryUpdateJobProgress(Map evt) { @@ -210,12 +210,12 @@ class FileModel extends ChangeNotifier { notifyListeners(); } - jobDone(Map evt) { + jobDone(Map evt) async { + if (_jobResultListener.isListening) { + _jobResultListener.complete(evt); + return; + } if (!isDesktop) { - if (_jobResultListener.isListening) { - _jobResultListener.complete(evt); - return; - } _selectMode = false; _jobProgress.state = JobState.done; } else { @@ -228,7 +228,10 @@ class FileModel extends ChangeNotifier { job.fileNum = int.parse(evt['file_num']); } } - refresh(); + await Future.wait([ + refresh(isLocal: false), + refresh(isLocal: true), + ]); } jobError(Map evt) { @@ -333,12 +336,13 @@ class FileModel extends ChangeNotifier { _remoteOption.clear(); } - refresh({bool? isLocal}) { + Future refresh({bool? isLocal}) async { if (isDesktop) { isLocal = isLocal ?? _isLocal; - isLocal ? openDirectory(currentLocalDir.path) : openDirectory(currentRemoteDir.path); + await isLocal ? openDirectory(currentLocalDir.path, isLocal: isLocal) : + openDirectory(currentRemoteDir.path, isLocal: isLocal); } else { - openDirectory(currentDir.path); + await openDirectory(currentDir.path); } } @@ -394,9 +398,9 @@ class FileModel extends ChangeNotifier { final toPath = isRemote ? currentLocalDir.path : currentRemoteDir.path; final isWindows = - isRemote ? _remoteOption.isWindows : _localOption.isWindows; + isRemote ? _localOption.isWindows : _remoteOption.isWindows; final showHidden = - isRemote ? _remoteOption.showHidden : _localOption.showHidden; + isRemote ? _localOption.showHidden : _remoteOption.showHidden; items.items.forEach((from) async { final jobId = ++_jobId; _jobTable.add(JobProgress() @@ -432,7 +436,8 @@ class FileModel extends ChangeNotifier { bool removeCheckboxRemember = false; - removeAction(SelectedItems items) async { + removeAction(SelectedItems items, {bool? isLocal}) async { + isLocal = isLocal ?? _isLocal; removeCheckboxRemember = false; if (items.isLocal == null) { debugPrint("Failed to removeFile, wrong path state"); @@ -506,11 +511,13 @@ class FileModel extends ChangeNotifier { } break; } - } catch (e) {} + } catch (e) { + print("remove error: ${e}"); + } } }); _selectMode = false; - refresh(); + refresh(isLocal: isLocal); } Future showRemoveDialog( From 9837c9b89305b7ec924fd47003f29a40e906cda7 Mon Sep 17 00:00:00 2001 From: Asura Date: Tue, 12 Jul 2022 00:33:20 -0700 Subject: [PATCH 0080/2015] Use map mode when keyboard monitor --- src/ui/remote.rs | 491 +++++++++++++++++++++++++++-------------------- 1 file changed, 283 insertions(+), 208 deletions(-) diff --git a/src/ui/remote.rs b/src/ui/remote.rs index f85e37d04..0b73ee504 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -289,219 +289,294 @@ impl Handler { std::thread::spawn(move || { // This will block. std::env::set_var("KEYBOARD_ONLY", "y"); // pass to rdev - use rdev::{EventType::*, *}; - let func = move |evt: Event| { - if !IS_IN.load(Ordering::SeqCst) || !SERVER_KEYBOARD_ENABLED.load(Ordering::SeqCst) - { - return; - } - let (key, down) = match evt.event_type { - KeyPress(k) => (k, 1), - KeyRelease(k) => (k, 0), - _ => return, - }; - let alt = get_key_state(enigo::Key::Alt); - #[cfg(windows)] - let ctrl = { - let mut tmp = get_key_state(enigo::Key::Control); - unsafe { - if IS_ALT_GR { - if alt || key == Key::AltGr { - if tmp { - tmp = false; - } - } else { - IS_ALT_GR = false; - } - } - } - tmp - }; - #[cfg(not(windows))] - let ctrl = get_key_state(enigo::Key::Control); - let shift = get_key_state(enigo::Key::Shift); - #[cfg(windows)] - let command = crate::platform::windows::get_win_key_state(); - #[cfg(not(windows))] - let command = get_key_state(enigo::Key::Meta); - let control_key = match key { - Key::Alt => Some(ControlKey::Alt), - Key::AltGr => Some(ControlKey::RAlt), - Key::Backspace => Some(ControlKey::Backspace), - Key::ControlLeft => { - // when pressing AltGr, an extra VK_LCONTROL with a special - // scancode with bit 9 set is sent, let's ignore this. - #[cfg(windows)] - if evt.scan_code & 0x200 != 0 { - unsafe { - IS_ALT_GR = true; - } - return; - } - Some(ControlKey::Control) - } - Key::ControlRight => Some(ControlKey::RControl), - Key::DownArrow => Some(ControlKey::DownArrow), - Key::Escape => Some(ControlKey::Escape), - Key::F1 => Some(ControlKey::F1), - Key::F10 => Some(ControlKey::F10), - Key::F11 => Some(ControlKey::F11), - Key::F12 => Some(ControlKey::F12), - Key::F2 => Some(ControlKey::F2), - Key::F3 => Some(ControlKey::F3), - Key::F4 => Some(ControlKey::F4), - Key::F5 => Some(ControlKey::F5), - Key::F6 => Some(ControlKey::F6), - Key::F7 => Some(ControlKey::F7), - Key::F8 => Some(ControlKey::F8), - Key::F9 => Some(ControlKey::F9), - Key::LeftArrow => Some(ControlKey::LeftArrow), - Key::MetaLeft => Some(ControlKey::Meta), - Key::MetaRight => Some(ControlKey::RWin), - Key::Return => Some(ControlKey::Return), - Key::RightArrow => Some(ControlKey::RightArrow), - Key::ShiftLeft => Some(ControlKey::Shift), - Key::ShiftRight => Some(ControlKey::RShift), - Key::Space => Some(ControlKey::Space), - Key::Tab => Some(ControlKey::Tab), - Key::UpArrow => Some(ControlKey::UpArrow), - Key::Delete => { - if is_win && ctrl && alt { - me.ctrl_alt_del(); - return; - } - Some(ControlKey::Delete) - } - Key::Apps => Some(ControlKey::Apps), - Key::Cancel => Some(ControlKey::Cancel), - Key::Clear => Some(ControlKey::Clear), - Key::Kana => Some(ControlKey::Kana), - Key::Hangul => Some(ControlKey::Hangul), - Key::Junja => Some(ControlKey::Junja), - Key::Final => Some(ControlKey::Final), - Key::Hanja => Some(ControlKey::Hanja), - Key::Hanji => Some(ControlKey::Hanja), - Key::Convert => Some(ControlKey::Convert), - Key::Print => Some(ControlKey::Print), - Key::Select => Some(ControlKey::Select), - Key::Execute => Some(ControlKey::Execute), - Key::PrintScreen => Some(ControlKey::Snapshot), - Key::Help => Some(ControlKey::Help), - Key::Sleep => Some(ControlKey::Sleep), - Key::Separator => Some(ControlKey::Separator), - Key::KpReturn => Some(ControlKey::NumpadEnter), - Key::Kp0 => Some(ControlKey::Numpad0), - Key::Kp1 => Some(ControlKey::Numpad1), - Key::Kp2 => Some(ControlKey::Numpad2), - Key::Kp3 => Some(ControlKey::Numpad3), - Key::Kp4 => Some(ControlKey::Numpad4), - Key::Kp5 => Some(ControlKey::Numpad5), - Key::Kp6 => Some(ControlKey::Numpad6), - Key::Kp7 => Some(ControlKey::Numpad7), - Key::Kp8 => Some(ControlKey::Numpad8), - Key::Kp9 => Some(ControlKey::Numpad9), - Key::KpDivide => Some(ControlKey::Divide), - Key::KpMultiply => Some(ControlKey::Multiply), - Key::KpDecimal => Some(ControlKey::Decimal), - Key::KpMinus => Some(ControlKey::Subtract), - Key::KpPlus => Some(ControlKey::Add), - Key::CapsLock | Key::NumLock | Key::ScrollLock => { - return; - } - Key::Home => Some(ControlKey::Home), - Key::End => Some(ControlKey::End), - Key::Insert => Some(ControlKey::Insert), - Key::PageUp => Some(ControlKey::PageUp), - Key::PageDown => Some(ControlKey::PageDown), - Key::Pause => Some(ControlKey::Pause), - _ => None, - }; - let mut key_event = KeyEvent::new(); - if let Some(k) = control_key { - key_event.set_control_key(k); - } else { - let mut chr = match evt.name { - Some(ref s) => { - if s.len() <= 2 { - // exclude chinese characters - s.chars().next().unwrap_or('\0') - } else { - '\0' - } - } - _ => '\0', + let keyboard_mode = 1; + if keyboard_mode == 1 { + use rdev::{Event, EventType::*, Key as RdevKey}; + lazy_static::lazy_static! { + static ref MUTEX_SPECIAL_KEYS: Mutex> = { + let mut m = HashMap::new(); + // m.insert(RdevKey::PrintScreen, false); // TODO + m.insert(RdevKey::ShiftLeft, false); + m.insert(RdevKey::ShiftRight, false); + m.insert(RdevKey::ControlLeft, false); + m.insert(RdevKey::ControlRight, false); + m.insert(RdevKey::Alt, false); + m.insert(RdevKey::AltGr, false); + Mutex::new(m) }; - if chr == '·' { - // special for Chinese - chr = '`'; - } - if chr == '\0' { - chr = match key { - Key::Num1 => '1', - Key::Num2 => '2', - Key::Num3 => '3', - Key::Num4 => '4', - Key::Num5 => '5', - Key::Num6 => '6', - Key::Num7 => '7', - Key::Num8 => '8', - Key::Num9 => '9', - Key::Num0 => '0', - Key::KeyA => 'a', - Key::KeyB => 'b', - Key::KeyC => 'c', - Key::KeyD => 'd', - Key::KeyE => 'e', - Key::KeyF => 'f', - Key::KeyG => 'g', - Key::KeyH => 'h', - Key::KeyI => 'i', - Key::KeyJ => 'j', - Key::KeyK => 'k', - Key::KeyL => 'l', - Key::KeyM => 'm', - Key::KeyN => 'n', - Key::KeyO => 'o', - Key::KeyP => 'p', - Key::KeyQ => 'q', - Key::KeyR => 'r', - Key::KeyS => 's', - Key::KeyT => 't', - Key::KeyU => 'u', - Key::KeyV => 'v', - Key::KeyW => 'w', - Key::KeyX => 'x', - Key::KeyY => 'y', - Key::KeyZ => 'z', - Key::Comma => ',', - Key::Dot => '.', - Key::SemiColon => ';', - Key::Quote => '\'', - Key::LeftBracket => '[', - Key::RightBracket => ']', - Key::BackSlash => '\\', - Key::Minus => '-', - Key::Equal => '=', - Key::BackQuote => '`', - _ => '\0', - } - } - if chr != '\0' { - if chr == 'l' && is_win && command { - me.lock_screen(); - return; - } - key_event.set_chr(chr as _); - } else { - log::error!("Unknown key {:?}", evt); + } + // todo: auto change paltform + let func = move |evt: Event| { + if !IS_IN.load(Ordering::SeqCst) + || !SERVER_KEYBOARD_ENABLED.load(Ordering::SeqCst) + { return; } + let (key, down) = match evt.event_type { + KeyPress(k) => { + // keyboard long press + if MUTEX_SPECIAL_KEYS.lock().unwrap().contains_key(&k) { + if *MUTEX_SPECIAL_KEYS.lock().unwrap().get(&k).unwrap() { + return; + } + MUTEX_SPECIAL_KEYS.lock().unwrap().insert(k, true); + } + println!("keydown {:?} {:?} {:?}", k, evt.code, evt.scan_code); + (k, 1) + } + KeyRelease(k) => { + // keyboard long press + if MUTEX_SPECIAL_KEYS.lock().unwrap().contains_key(&k) { + MUTEX_SPECIAL_KEYS.lock().unwrap().insert(k, false); + } + println!("keyup {:?} {:?} {:?}", k, evt.code, evt.scan_code); + (k, 0) + } + _ => return, + }; + + // todo: clear key + #[cfg(target_os = "windows")] + let key = rdev::get_win_key(evt.code.into(), evt.scan_code); + + // todo: up down left right in numpad + // #[cfg(target_os = "linux")] + dbg!(key); + println!("--------------"); + + let mut key_event = KeyEvent::new(); + // According to peer platform. + if peer == "linux" { + let keycode: u32 = rdev::linux_keycode_from_key(key).unwrap().into(); + key_event.set_chr(keycode); + } else if peer == "Windows" { + let keycode: u32 = rdev::win_keycode_from_key(key).unwrap().into(); + key_event.set_chr(keycode); + } else if peer == "Mac OS" { + let keycode: u32 = rdev::macos_keycode_from_key(key).unwrap().into(); + key_event.set_chr(keycode); + } + me.key_down_or_up(down, key_event, false, false, false, false); + }; + if let Err(error) = rdev::listen(func) { + log::error!("rdev: {:?}", error); + } + } else { + use rdev::{EventType::*, *}; + let func = move |evt: Event| { + if !IS_IN.load(Ordering::SeqCst) + || !SERVER_KEYBOARD_ENABLED.load(Ordering::SeqCst) + { + return; + } + let (key, down) = match evt.event_type { + KeyPress(k) => (k, 1), + KeyRelease(k) => (k, 0), + _ => return, + }; + let alt = get_key_state(enigo::Key::Alt); + #[cfg(windows)] + let ctrl = { + let mut tmp = get_key_state(enigo::Key::Control); + unsafe { + if IS_ALT_GR { + if alt || key == Key::AltGr { + if tmp { + tmp = false; + } + } else { + IS_ALT_GR = false; + } + } + } + tmp + }; + #[cfg(not(windows))] + let ctrl = get_key_state(enigo::Key::Control); + let shift = get_key_state(enigo::Key::Shift); + #[cfg(windows)] + let command = crate::platform::windows::get_win_key_state(); + #[cfg(not(windows))] + let command = get_key_state(enigo::Key::Meta); + let control_key = match key { + Key::Alt => Some(ControlKey::Alt), + Key::AltGr => Some(ControlKey::RAlt), + Key::Backspace => Some(ControlKey::Backspace), + Key::ControlLeft => { + // when pressing AltGr, an extra VK_LCONTROL with a special + // scancode with bit 9 set is sent, let's ignore this. + #[cfg(windows)] + if evt.scan_code & 0x200 != 0 { + unsafe { + IS_ALT_GR = true; + } + return; + } + Some(ControlKey::Control) + } + Key::ControlRight => Some(ControlKey::RControl), + Key::DownArrow => Some(ControlKey::DownArrow), + Key::Escape => Some(ControlKey::Escape), + Key::F1 => Some(ControlKey::F1), + Key::F10 => Some(ControlKey::F10), + Key::F11 => Some(ControlKey::F11), + Key::F12 => Some(ControlKey::F12), + Key::F2 => Some(ControlKey::F2), + Key::F3 => Some(ControlKey::F3), + Key::F4 => Some(ControlKey::F4), + Key::F5 => Some(ControlKey::F5), + Key::F6 => Some(ControlKey::F6), + Key::F7 => Some(ControlKey::F7), + Key::F8 => Some(ControlKey::F8), + Key::F9 => Some(ControlKey::F9), + Key::LeftArrow => Some(ControlKey::LeftArrow), + Key::MetaLeft => Some(ControlKey::Meta), + Key::MetaRight => Some(ControlKey::RWin), + Key::Return => Some(ControlKey::Return), + Key::RightArrow => Some(ControlKey::RightArrow), + Key::ShiftLeft => Some(ControlKey::Shift), + Key::ShiftRight => Some(ControlKey::RShift), + Key::Space => Some(ControlKey::Space), + Key::Tab => Some(ControlKey::Tab), + Key::UpArrow => Some(ControlKey::UpArrow), + Key::Delete => { + if is_win && ctrl && alt { + me.ctrl_alt_del(); + return; + } + Some(ControlKey::Delete) + } + Key::Apps => Some(ControlKey::Apps), + Key::Cancel => Some(ControlKey::Cancel), + Key::Clear => Some(ControlKey::Clear), + Key::Kana => Some(ControlKey::Kana), + Key::Hangul => Some(ControlKey::Hangul), + Key::Junja => Some(ControlKey::Junja), + Key::Final => Some(ControlKey::Final), + Key::Hanja => Some(ControlKey::Hanja), + Key::Hanji => Some(ControlKey::Hanja), + Key::Convert => Some(ControlKey::Convert), + Key::Print => Some(ControlKey::Print), + Key::Select => Some(ControlKey::Select), + Key::Execute => Some(ControlKey::Execute), + Key::PrintScreen => Some(ControlKey::Snapshot), + Key::Help => Some(ControlKey::Help), + Key::Sleep => Some(ControlKey::Sleep), + Key::Separator => Some(ControlKey::Separator), + Key::KpReturn => Some(ControlKey::NumpadEnter), + Key::Kp0 => Some(ControlKey::Numpad0), + Key::Kp1 => Some(ControlKey::Numpad1), + Key::Kp2 => Some(ControlKey::Numpad2), + Key::Kp3 => Some(ControlKey::Numpad3), + Key::Kp4 => Some(ControlKey::Numpad4), + Key::Kp5 => Some(ControlKey::Numpad5), + Key::Kp6 => Some(ControlKey::Numpad6), + Key::Kp7 => Some(ControlKey::Numpad7), + Key::Kp8 => Some(ControlKey::Numpad8), + Key::Kp9 => Some(ControlKey::Numpad9), + Key::KpDivide => Some(ControlKey::Divide), + Key::KpMultiply => Some(ControlKey::Multiply), + Key::KpDecimal => Some(ControlKey::Decimal), + Key::KpMinus => Some(ControlKey::Subtract), + Key::KpPlus => Some(ControlKey::Add), + Key::CapsLock | Key::NumLock | Key::ScrollLock => { + return; + } + Key::Home => Some(ControlKey::Home), + Key::End => Some(ControlKey::End), + Key::Insert => Some(ControlKey::Insert), + Key::PageUp => Some(ControlKey::PageUp), + Key::PageDown => Some(ControlKey::PageDown), + Key::Pause => Some(ControlKey::Pause), + _ => None, + }; + let mut key_event = KeyEvent::new(); + if let Some(k) = control_key { + key_event.set_control_key(k); + } else { + let mut chr = match evt.name { + Some(ref s) => { + if s.len() <= 2 { + // exclude chinese characters + s.chars().next().unwrap_or('\0') + } else { + '\0' + } + } + _ => '\0', + }; + if chr == '·' { + // special for Chinese + chr = '`'; + } + if chr == '\0' { + chr = match key { + Key::Num1 => '1', + Key::Num2 => '2', + Key::Num3 => '3', + Key::Num4 => '4', + Key::Num5 => '5', + Key::Num6 => '6', + Key::Num7 => '7', + Key::Num8 => '8', + Key::Num9 => '9', + Key::Num0 => '0', + Key::KeyA => 'a', + Key::KeyB => 'b', + Key::KeyC => 'c', + Key::KeyD => 'd', + Key::KeyE => 'e', + Key::KeyF => 'f', + Key::KeyG => 'g', + Key::KeyH => 'h', + Key::KeyI => 'i', + Key::KeyJ => 'j', + Key::KeyK => 'k', + Key::KeyL => 'l', + Key::KeyM => 'm', + Key::KeyN => 'n', + Key::KeyO => 'o', + Key::KeyP => 'p', + Key::KeyQ => 'q', + Key::KeyR => 'r', + Key::KeyS => 's', + Key::KeyT => 't', + Key::KeyU => 'u', + Key::KeyV => 'v', + Key::KeyW => 'w', + Key::KeyX => 'x', + Key::KeyY => 'y', + Key::KeyZ => 'z', + Key::Comma => ',', + Key::Dot => '.', + Key::SemiColon => ';', + Key::Quote => '\'', + Key::LeftBracket => '[', + Key::RightBracket => ']', + Key::BackSlash => '\\', + Key::Minus => '-', + Key::Equal => '=', + Key::BackQuote => '`', + _ => '\0', + } + } + if chr != '\0' { + if chr == 'l' && is_win && command { + me.lock_screen(); + return; + } + key_event.set_chr(chr as _); + } else { + log::error!("Unknown key {:?}", evt); + return; + } + } + me.key_down_or_up(down, key_event, alt, ctrl, shift, command); + }; + if let Err(error) = rdev::listen(func) { + log::error!("rdev: {:?}", error); } - me.key_down_or_up(down, key_event, alt, ctrl, shift, command); }; - if let Err(error) = rdev::listen(func) { - log::error!("rdev: {:?}", error); - } }); } From 1cc6c7e167f2f9ce9b86879fabea47071325dd24 Mon Sep 17 00:00:00 2001 From: Asura Date: Tue, 12 Jul 2022 04:27:27 -0700 Subject: [PATCH 0081/2015] Use map mode when Windows are simulated --- libs/enigo/src/win/win_impl.rs | 42 ++++++++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/libs/enigo/src/win/win_impl.rs b/libs/enigo/src/win/win_impl.rs index ea1543faa..d0d97ecee 100644 --- a/libs/enigo/src/win/win_impl.rs +++ b/libs/enigo/src/win/win_impl.rs @@ -1,9 +1,9 @@ -use winapi; - use self::winapi::ctypes::c_int; use self::winapi::shared::{basetsd::ULONG_PTR, minwindef::*, windef::*}; use self::winapi::um::winbase::*; use self::winapi::um::winuser::*; +use rdev::{simulate, EventType, EventType::*, Key as RdevKey, SimulateError}; +use winapi; use crate::win::keycodes::*; use crate::{Key, KeyboardControllable, MouseButton, MouseControllable}; @@ -198,9 +198,14 @@ impl KeyboardControllable for Enigo { } fn key_down(&mut self, key: Key) -> crate::ResultType { + let keyboard_mode = 1; + if keyboard_mode == 1 { + self.send_rdev(&key, true); + return Ok(()); + }; let code = self.key_to_keycode(key); if code == 0 || code == 65535 { - return Err("".into()); + return Err("".into()); } let res = keybd_event(0, code, 0); if res == 0 { @@ -213,6 +218,11 @@ impl KeyboardControllable for Enigo { } fn key_up(&mut self, key: Key) { + let keyboard_mode = 1; + if keyboard_mode == 1 { + self.send_rdev(&key, false); + return; + }; keybd_event(KEYEVENTF_KEYUP, self.key_to_keycode(key), 0); } @@ -227,7 +237,8 @@ impl KeyboardControllable for Enigo { } impl Enigo { - /// Gets the (width, height) of the main display in screen coordinates (pixels). + /// Gets the (width, height) of the main display in screen coordinates + /// (pixels). /// /// # Example /// @@ -272,6 +283,29 @@ impl Enigo { keybd_event(KEYEVENTF_UNICODE | KEYEVENTF_KEYUP, 0, unicode_char); } + fn send_rdev(&mut self, key: &Key, is_press: bool) -> bool { + log::info!("{:?} {:?}", key, is_press); + + if let Key::Raw(keycode) = key { + let event_type = match is_press { + // todo: Acccodding to client type + true => Box::leak(Box::new(EventType::KeyPress(RdevKey::Unknown( + (*keycode).into(), + )))), + false => Box::leak(Box::new(EventType::KeyRelease(RdevKey::Unknown( + (*keycode).into(), + )))), + }; + + match simulate(event_type) { + Ok(()) => true, + Err(SimulateError) => false, + } + } else { + false + } + } + fn key_to_keycode(&self, key: Key) -> u16 { unsafe { LAYOUT = std::ptr::null_mut(); From a6f9c16d50eae2b3978c169ebc448181ed21aecc Mon Sep 17 00:00:00 2001 From: Asura Date: Tue, 12 Jul 2022 04:29:32 -0700 Subject: [PATCH 0082/2015] fix: Correct the string corresponding to the platform --- src/ui/remote.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 0b73ee504..5bef6579c 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -346,7 +346,7 @@ impl Handler { let mut key_event = KeyEvent::new(); // According to peer platform. - if peer == "linux" { + if peer == "Linux" { let keycode: u32 = rdev::linux_keycode_from_key(key).unwrap().into(); key_event.set_chr(keycode); } else if peer == "Windows" { From 7395f1a7557a123269b028cd7800d84069e0d693 Mon Sep 17 00:00:00 2001 From: Asura Date: Tue, 12 Jul 2022 04:40:38 -0700 Subject: [PATCH 0083/2015] ci: Change source of lib --- Cargo.lock | 30 +++++++++++++++++++++++++----- Cargo.toml | 2 +- libs/enigo/Cargo.toml | 2 +- 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 17832eb8b..6f3895c1c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1317,13 +1317,33 @@ dependencies = [ "log", "objc", "pkg-config", - "rdev 0.5.1", + "rdev 0.5.0 (git+https://github.com/asur4s/rdev)", "serde 1.0.137", "serde_derive", "unicode-segmentation", "winapi 0.3.9", ] +[[package]] +name = "enum-map" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ddfe61e8040145222887d0d32a939c70c8cae681490d72fb868305e9b40ced8" +dependencies = [ + "enum-map-derive", +] + +[[package]] +name = "enum-map-derive" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00d1c54e25a57236a790ecf051c2befbb57740c9b86c4273eac378ba84d620d6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "enum_dispatch" version = "0.3.8" @@ -3838,12 +3858,12 @@ dependencies = [ [[package]] name = "rdev" version = "0.5.0" -source = "git+https://github.com/open-trade/rdev#fbbefd0b5d87095a7349965aec9ecd33de7035ac" dependencies = [ "cocoa 0.22.0", "core-foundation 0.7.0", "core-foundation-sys 0.7.0", "core-graphics 0.19.2", + "enum-map", "lazy_static", "libc", "winapi 0.3.9", @@ -3852,14 +3872,14 @@ dependencies = [ [[package]] name = "rdev" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7336f02e29f34e9a7186ccf87051f6a5697536195569012e076e18a4efddcede" +version = "0.5.0" +source = "git+https://github.com/asur4s/rdev#83d998895677129f0ba8fc2ada4cddd1e0df418f" dependencies = [ "cocoa 0.22.0", "core-foundation 0.7.0", "core-foundation-sys 0.7.0", "core-graphics 0.19.2", + "enum-map", "lazy_static", "libc", "winapi 0.3.9", diff --git a/Cargo.toml b/Cargo.toml index f270f7b30..39789c772 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,7 +69,7 @@ sciter-rs = { git = "https://github.com/open-trade/rust-sciter", branch = "dyn" sys-locale = "0.2" enigo = { path = "libs/enigo" } clipboard = { path = "libs/clipboard" } -rdev = { git = "https://github.com/open-trade/rdev" } +rdev = { path = "../rdev" } ctrlc = "3.2" arboard = "2.0" #minreq = { version = "2.4", features = ["punycode", "https-native"] } diff --git a/libs/enigo/Cargo.toml b/libs/enigo/Cargo.toml index 29d7ee87f..b1c57ca92 100644 --- a/libs/enigo/Cargo.toml +++ b/libs/enigo/Cargo.toml @@ -22,7 +22,7 @@ appveyor = { repository = "pythoneer/enigo-85xiy" } serde = { version = "1.0", optional = true } serde_derive = { version = "1.0", optional = true } log = "0.4.17" -rdev = "0.5.1" +rdev = { git = "https://github.com/asur4s/rdev" } [features] with_serde = ["serde", "serde_derive"] From fa8595b77d5a22f3213937cb7efbad6a0c1146ce Mon Sep 17 00:00:00 2001 From: Asura Date: Tue, 12 Jul 2022 08:36:45 -0700 Subject: [PATCH 0084/2015] Supports Mac OS simulate input by scancode --- Cargo.lock | 21 +++--------------- Cargo.toml | 2 +- libs/enigo/src/macos/macos_impl.rs | 35 +++++++++++++++++++++++++++++- 3 files changed, 38 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6f3895c1c..f2db3ff15 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1317,7 +1317,7 @@ dependencies = [ "log", "objc", "pkg-config", - "rdev 0.5.0 (git+https://github.com/asur4s/rdev)", + "rdev", "serde 1.0.137", "serde_derive", "unicode-segmentation", @@ -3858,22 +3858,7 @@ dependencies = [ [[package]] name = "rdev" version = "0.5.0" -dependencies = [ - "cocoa 0.22.0", - "core-foundation 0.7.0", - "core-foundation-sys 0.7.0", - "core-graphics 0.19.2", - "enum-map", - "lazy_static", - "libc", - "winapi 0.3.9", - "x11", -] - -[[package]] -name = "rdev" -version = "0.5.0" -source = "git+https://github.com/asur4s/rdev#83d998895677129f0ba8fc2ada4cddd1e0df418f" +source = "git+https://github.com/asur4s/rdev#e0ed6e08b7fb7e8ac80b2ef6e710ba1db9fe0751" dependencies = [ "cocoa 0.22.0", "core-foundation 0.7.0", @@ -4126,7 +4111,7 @@ dependencies = [ "num_cpus", "objc", "parity-tokio-ipc", - "rdev 0.5.0", + "rdev", "repng", "reqwest", "rpassword 6.0.1", diff --git a/Cargo.toml b/Cargo.toml index 39789c772..264c82940 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,7 +69,7 @@ sciter-rs = { git = "https://github.com/open-trade/rust-sciter", branch = "dyn" sys-locale = "0.2" enigo = { path = "libs/enigo" } clipboard = { path = "libs/clipboard" } -rdev = { path = "../rdev" } +rdev = { git = "https://github.com/asur4s/rdev" } ctrlc = "3.2" arboard = "2.0" #minreq = { version = "2.4", features = ["punycode", "https-native"] } diff --git a/libs/enigo/src/macos/macos_impl.rs b/libs/enigo/src/macos/macos_impl.rs index 28c9362ed..6cae30984 100644 --- a/libs/enigo/src/macos/macos_impl.rs +++ b/libs/enigo/src/macos/macos_impl.rs @@ -1,5 +1,5 @@ use core_graphics; - +use rdev::{simulate, EventType, EventType::*, Key as RdevKey, SimulateError}; // TODO(dustin): use only the things i need use self::core_graphics::display::*; @@ -354,6 +354,11 @@ impl KeyboardControllable for Enigo { } fn key_down(&mut self, key: Key) -> crate::ResultType { + let keyboard_mode = 1; + if keyboard_mode == 1 { + self.send_rdev(&key, true); + return Ok(()); + }; let code = self.key_to_keycode(key); if code == u16::MAX { return Err("".into()); @@ -369,6 +374,11 @@ impl KeyboardControllable for Enigo { } fn key_up(&mut self, key: Key) { + let keyboard_mode = 1; + if keyboard_mode == 1 { + self.send_rdev(&key, true); + return Ok(()); + }; if let Some(src) = self.event_source.as_ref() { if let Ok(event) = CGEvent::new_keyboard_event(src.clone(), self.key_to_keycode(key), false) @@ -421,6 +431,29 @@ impl Enigo { (x, (display_height as i32) - y_inv) } + fn send_rdev(&mut self, key: &Key, is_press: bool) -> bool { + log::info!("{:?} {:?}", key, is_press); + + if let Key::Raw(keycode) = key { + let event_type = match is_press { + // todo: Acccodding to client type + true => Box::leak(Box::new(EventType::KeyPress(RdevKey::Unknown( + (*keycode).into(), + )))), + false => Box::leak(Box::new(EventType::KeyRelease(RdevKey::Unknown( + (*keycode).into(), + )))), + }; + + match simulate(event_type) { + Ok(()) => true, + Err(SimulateError) => false, + } + } else { + false + } + } + fn key_to_keycode(&mut self, key: Key) -> CGKeyCode { #[allow(deprecated)] // I mean duh, we still need to support deprecated keys until they're removed From 7b3b9007647ccd1b3ed4aefc9ca21c0368ba036d Mon Sep 17 00:00:00 2001 From: Asura Date: Tue, 12 Jul 2022 18:56:08 -0700 Subject: [PATCH 0085/2015] Update rdev dependency version --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f2db3ff15..056c14ae7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3857,8 +3857,8 @@ dependencies = [ [[package]] name = "rdev" -version = "0.5.0" -source = "git+https://github.com/asur4s/rdev#e0ed6e08b7fb7e8ac80b2ef6e710ba1db9fe0751" +version = "0.5.0-2" +source = "git+https://github.com/asur4s/rdev#95cecfd1b0f0b20c6cd728afca859107b911f3b8" dependencies = [ "cocoa 0.22.0", "core-foundation 0.7.0", From 7fe2609ffb4598d4bc666af0395fc89a7bf3eaba Mon Sep 17 00:00:00 2001 From: Asura Date: Wed, 13 Jul 2022 02:14:32 -0700 Subject: [PATCH 0086/2015] feat: Support new keyboard mode --- libs/enigo/src/linux.rs | 3 --- libs/enigo/src/macos/macos_impl.rs | 2 -- libs/enigo/src/win/win_impl.rs | 2 -- src/server/input_service.rs | 2 -- src/ui/remote.rs | 7 ------- 5 files changed, 16 deletions(-) diff --git a/libs/enigo/src/linux.rs b/libs/enigo/src/linux.rs index ee27e06cf..7f2afc3dc 100644 --- a/libs/enigo/src/linux.rs +++ b/libs/enigo/src/linux.rs @@ -105,8 +105,6 @@ impl Enigo { } fn send_rdev(&mut self, key: &Key, is_press: bool) -> bool { - log::info!("{:?} {:?}", key, is_press); - if let Key::Raw(keycode) = key { let event_type = match is_press { // todo: Acccodding to client type @@ -139,7 +137,6 @@ impl Enigo { fn string_to_static_str(s: String) -> &'static str { Box::leak(s.into_boxed_str()) } - dbg!(chr.to_string()); return self .tx .send((PyMsg::Str(string_to_static_str(chr.to_string())), is_press)) diff --git a/libs/enigo/src/macos/macos_impl.rs b/libs/enigo/src/macos/macos_impl.rs index 6cae30984..df494b9cb 100644 --- a/libs/enigo/src/macos/macos_impl.rs +++ b/libs/enigo/src/macos/macos_impl.rs @@ -432,8 +432,6 @@ impl Enigo { } fn send_rdev(&mut self, key: &Key, is_press: bool) -> bool { - log::info!("{:?} {:?}", key, is_press); - if let Key::Raw(keycode) = key { let event_type = match is_press { // todo: Acccodding to client type diff --git a/libs/enigo/src/win/win_impl.rs b/libs/enigo/src/win/win_impl.rs index d0d97ecee..0967d07c9 100644 --- a/libs/enigo/src/win/win_impl.rs +++ b/libs/enigo/src/win/win_impl.rs @@ -284,8 +284,6 @@ impl Enigo { } fn send_rdev(&mut self, key: &Key, is_press: bool) -> bool { - log::info!("{:?} {:?}", key, is_press); - if let Key::Raw(keycode) = key { let event_type = match is_press { // todo: Acccodding to client type diff --git a/src/server/input_service.rs b/src/server/input_service.rs index a8fd5fbbb..8e8e2abb4 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -589,10 +589,8 @@ fn handle_key_(evt: &KeyEvent) { if keyboard_mode == 1 { if let Some(key_event::Union::chr(chr)) = evt.union { if evt.down { - println!("key down: {:?}", chr); en.key_down(Key::Raw(chr.try_into().unwrap())); } else { - println!("key up: {:?}", chr); en.key_up(Key::Raw(chr.try_into().unwrap())); } } diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 5bef6579c..cc2ce8154 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -321,7 +321,6 @@ impl Handler { } MUTEX_SPECIAL_KEYS.lock().unwrap().insert(k, true); } - println!("keydown {:?} {:?} {:?}", k, evt.code, evt.scan_code); (k, 1) } KeyRelease(k) => { @@ -329,7 +328,6 @@ impl Handler { if MUTEX_SPECIAL_KEYS.lock().unwrap().contains_key(&k) { MUTEX_SPECIAL_KEYS.lock().unwrap().insert(k, false); } - println!("keyup {:?} {:?} {:?}", k, evt.code, evt.scan_code); (k, 0) } _ => return, @@ -339,11 +337,6 @@ impl Handler { #[cfg(target_os = "windows")] let key = rdev::get_win_key(evt.code.into(), evt.scan_code); - // todo: up down left right in numpad - // #[cfg(target_os = "linux")] - dbg!(key); - println!("--------------"); - let mut key_event = KeyEvent::new(); // According to peer platform. if peer == "Linux" { From 19c3c6034e95e707825384f470a9a8e9bc7f7d34 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Thu, 14 Jul 2022 12:32:01 +0800 Subject: [PATCH 0087/2015] feat: add local option to main window --- .../lib/desktop/pages/connection_page.dart | 197 +++++++++--------- .../lib/desktop/pages/desktop_home_page.dart | 133 ++++++++++-- flutter/lib/models/model.dart | 28 +++ src/flutter_ffi.rs | 5 + 4 files changed, 256 insertions(+), 107 deletions(-) diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index aa023c82c..70231d603 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; +import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -44,15 +45,23 @@ class _ConnectionPageState extends State { Widget build(BuildContext context) { Provider.of(context); if (_idController.text.isEmpty) _idController.text = gFFI.getId(); - return SingleChildScrollView( + return Container( + decoration: BoxDecoration( + color: MyTheme.grayBg + ), child: Column( mainAxisAlignment: MainAxisAlignment.start, mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, children: [ getUpdateUI(), - getSearchBarUI(), + Row( + children: [ + getSearchBarUI(), + ], + ).marginOnly(top: 16.0, left: 16.0), SizedBox(height: 12), + Divider(thickness: 1,), getPeers(), ]), ); @@ -106,104 +115,102 @@ class _ConnectionPageState extends State { /// UI for the search bar. /// Search for a peer and connect to it if the id exists. Widget getSearchBarUI() { - var w = Padding( - padding: const EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 16.0), - child: Container( - child: Padding( - padding: const EdgeInsets.only(top: 16, bottom: 16), - child: Ink( - decoration: BoxDecoration( - color: MyTheme.white, - borderRadius: const BorderRadius.all(Radius.circular(13)), - ), - child: Column( - children: [ - Row( - children: [ - Expanded( - child: Container( - padding: const EdgeInsets.only(left: 16, right: 16), - child: TextField( - autocorrect: false, - enableSuggestions: false, - keyboardType: TextInputType.visiblePassword, - // keyboardType: TextInputType.number, - style: TextStyle( - fontFamily: 'WorkSans', - fontWeight: FontWeight.bold, - fontSize: 30, - // color: MyTheme.idColor, - ), - decoration: InputDecoration( - labelText: translate('Control Remote Desktop'), - // hintText: 'Enter your remote ID', - // border: InputBorder., - border: OutlineInputBorder( - borderRadius: BorderRadius.zero), - helperStyle: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - color: MyTheme.dark, - ), - labelStyle: TextStyle( - fontWeight: FontWeight.w600, - fontSize: 26, - letterSpacing: 0.2, - color: MyTheme.dark, - ), - ), - controller: _idController, + var w = Container( + width: 500, + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 24), + decoration: BoxDecoration( + color: MyTheme.white, + borderRadius: const BorderRadius.all(Radius.circular(13)), + ), + child: Ink( + child: Column( + children: [ + Row( + children: [ + Expanded( + child: Container( + child: TextField( + autocorrect: false, + enableSuggestions: false, + keyboardType: TextInputType.visiblePassword, + // keyboardType: TextInputType.number, + style: TextStyle( + fontFamily: 'WorkSans', + fontWeight: FontWeight.bold, + fontSize: 30, + // color: MyTheme.idColor, + ), + decoration: InputDecoration( + labelText: translate('Control Remote Desktop'), + // hintText: 'Enter your remote ID', + // border: InputBorder., + border: OutlineInputBorder( + borderRadius: BorderRadius.zero), + helperStyle: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + color: MyTheme.dark, + ), + labelStyle: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 26, + letterSpacing: 0.2, + color: MyTheme.dark, ), ), + controller: _idController, + onSubmitted: (s) { + onConnect(); + }, ), - ], - ), - Padding( - padding: const EdgeInsets.symmetric( - vertical: 16.0, horizontal: 16.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - OutlinedButton( - onPressed: () { - onConnect(isFileTransfer: true); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 8.0, horizontal: 8.0), - child: Text( - translate( - "Transfer File", - ), - style: TextStyle(color: MyTheme.dark), - ), - ), - ), - SizedBox( - width: 30, - ), - OutlinedButton( - onPressed: onConnect, - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 8.0, horizontal: 16.0), - child: Text( - translate( - "Connection", - ), - style: TextStyle(color: MyTheme.white), - ), - ), - style: OutlinedButton.styleFrom( - backgroundColor: Colors.blueAccent, - ), - ), - ], ), - ) + ), ], ), - ), + Padding( + padding: const EdgeInsets.only( + top: 16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + OutlinedButton( + onPressed: () { + onConnect(isFileTransfer: true); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 8.0, horizontal: 8.0), + child: Text( + translate( + "Transfer File", + ), + style: TextStyle(color: MyTheme.dark), + ), + ), + ), + SizedBox( + width: 30, + ), + OutlinedButton( + onPressed: onConnect, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 8.0, horizontal: 16.0), + child: Text( + translate( + "Connection", + ), + style: TextStyle(color: MyTheme.white), + ), + ), + style: OutlinedButton.styleFrom( + backgroundColor: Colors.blueAccent, + ), + ), + ], + ), + ) + ], ), ), ); diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 1e7006628..02b87f1c7 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -1,10 +1,12 @@ import 'dart:io'; import 'package:flutter/material.dart' hide MenuItem; +import 'package:flutter/services.dart'; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/desktop/pages/connection_page.dart'; import 'package:flutter_hbb/desktop/widgets/titlebar_widget.dart'; import 'package:flutter_hbb/models/model.dart'; +import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:tray_manager/tray_manager.dart'; @@ -42,9 +44,6 @@ class _DesktopHomePageState extends State with TrayListener { child: buildServerInfo(context), flex: 1, ), - SizedBox( - width: 16.0, - ), Flexible( child: buildServerBoard(context), flex: 4, @@ -76,12 +75,8 @@ class _DesktopHomePageState extends State with TrayListener { buildServerBoard(BuildContext context) { return Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, children: [ - // buildControlPanel(context), - // buildRecentSession(context), - Expanded(child: ConnectionPage()) + Expanded(child: ConnectionPage()), ], ); } @@ -105,9 +100,35 @@ class _DesktopHomePageState extends State with TrayListener { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - translate("ID"), - style: TextStyle(fontSize: 18, fontWeight: FontWeight.w500), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + translate("ID"), + style: TextStyle(fontSize: 18, fontWeight: FontWeight.w500), + ), + PopupMenuButton( + padding: EdgeInsets.all(4.0), + itemBuilder: (context) => [ + genEnablePopupMenuItem(translate("Enable Keyboard/Mouse"), 'enable-keyboard',), + genEnablePopupMenuItem(translate("Enable Clipboard"), 'enable-clipboard',), + genEnablePopupMenuItem(translate("Enable File Transfer"), 'enable-file-transfer',), + genEnablePopupMenuItem(translate("Enable TCP Tunneling"), 'enable-tunnel',), + genAudioInputPopupMenuItem(), + // TODO: Audio Input + PopupMenuItem(child: Text(translate("ID/Relay Server")), value: 'custom-server',), + PopupMenuItem(child: Text(translate("IP Whitelisting")), value: 'whitelist',), + PopupMenuItem(child: Text(translate("Socks5 Proxy")), value: 'Socks5 Proxy',), + // sep + genEnablePopupMenuItem(translate("Enable Service"), 'stop-service',), + // TODO: direct server + genEnablePopupMenuItem(translate("Always connected via relay"),'allow-always-relay',), + genEnablePopupMenuItem(translate("Start ID/relay service"),'stop-rendezvous-service',), + PopupMenuItem(child: Text(translate("Change ID")), value: 'change-id',), + genEnablePopupMenuItem(translate("Dark Theme"), 'allow-darktheme',), + PopupMenuItem(child: Text(translate("About")), value: 'about',), + ], onSelected: onSelectMenu,) + ], ), TextFormField( controller: model.serverId, @@ -194,7 +215,9 @@ class _DesktopHomePageState extends State with TrayListener { children: [ TextFormField( controller: TextEditingController(), - inputFormatters: [], + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp(r"[0-9]")) + ], ) ], )) @@ -232,4 +255,90 @@ class _DesktopHomePageState extends State with TrayListener { trayManager.removeListener(this); super.dispose(); } + + void onSelectMenu(String value) { + if (value.startsWith('enable-')) { + final option = gFFI.getOption(value); + gFFI.setOption(value, option == "N" ? "" : "N"); + } else if (value.startsWith('allow-')) { + final option = gFFI.getOption(value); + gFFI.setOption(value, option == "Y" ? "" : "Y"); + } else if (value == "stop-service") { + final option = gFFI.getOption(value); + gFFI.setOption(value, option == "Y" ? "" : "Y"); + } + } + + PopupMenuItem genEnablePopupMenuItem(String label, String value) { + final isEnable = + label.startsWith('enable-') ? gFFI.getOption(value) != "N" : gFFI.getOption(value) != "Y"; + return PopupMenuItem(child: Row( + children: [ + Offstage(offstage: !isEnable, child: Icon(Icons.check)), + Text(label, style: genTextStyle(isEnable),), + ], + ), value: value,); + } + + TextStyle genTextStyle(bool isPositive) { + return isPositive ? TextStyle() : TextStyle( + color: Colors.redAccent, + decoration: TextDecoration.lineThrough + ); + } + + PopupMenuItem genAudioInputPopupMenuItem() { + final _enabledInput = gFFI.getOption('enable-audio'); + var defaultInput = gFFI.getDefaultAudioInput().obs; + var enabled = (_enabledInput != "N").obs; + return PopupMenuItem(child: FutureBuilder>( + future: gFFI.getAudioInputs(), + builder: (context, snapshot) { + if (snapshot.hasData) { + final inputs = snapshot.data!; + if (Platform.isWindows) { + inputs.insert(0, translate("System Sound")); + } + var inputList = inputs.map((e) => PopupMenuItem( + child: Row( + children: [ + Obx(()=> Offstage(offstage: defaultInput.value != e, child: Icon(Icons.check))), + Expanded(child: Tooltip( + message: e, + child: Text("$e",maxLines: 1, overflow: TextOverflow.ellipsis,))), + ], + ), + value: e, + )).toList(); + inputList.insert(0, PopupMenuItem( + child: Row( + children: [ + Obx(()=> Offstage(offstage: enabled.value, child: Icon(Icons.check))), + Expanded(child: Text(translate("Mute"))), + ], + ), + value: "Mute", + )); + return PopupMenuButton( + padding: EdgeInsets.zero, + child: Container( + alignment: Alignment.centerLeft, + child: Text(translate("Audio Input"))), + itemBuilder: (context) => inputList, + onSelected: (dev) { + if (dev == "Mute") { + gFFI.setOption('enable-audio', _enabledInput == 'N' ? '': 'N'); + enabled.value = gFFI.getOption('enable-audio') != 'N'; + } else if (dev != gFFI.getDefaultAudioInput()) { + gFFI.setDefaultAudioInput(dev); + defaultInput.value = dev; + } + }, + ); + } else { + return Text("..."); + } + }, + ), value: 'audio-input',); + } } diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 45a5bc696..9b0b7930a 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:io'; import 'dart:math'; import 'dart:typed_data'; import 'dart:ui' as ui; @@ -990,6 +991,17 @@ class FFI { ffiModel.platformFFI.setByName(name, value); } + String getOption(String name) { + return ffiModel.platformFFI.getByName("option", name); + } + + void setOption(String name, String value) { + Map res = Map() + ..["name"] = name + ..["value"] = value; + return ffiModel.platformFFI.setByName('option', jsonEncode(res)); + } + RustdeskImpl get bind => ffiModel.platformFFI.ffiBind; handleMouse(Map evt) { @@ -1062,6 +1074,22 @@ class FFI { Future invokeMethod(String method, [dynamic arguments]) async { return await ffiModel.platformFFI.invokeMethod(method, arguments); } + + Future> getAudioInputs() async { + return await bind.mainGetSoundInputs(); + } + + String getDefaultAudioInput() { + final input = getOption('audio-input'); + if (input.isEmpty && Platform.isWindows) { + return "System Sound"; + } + return input; + } + + void setDefaultAudioInput(String input){ + setOption('audio-input', input); + } } class Peer { diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index f2bef5716..f10fd6587 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -19,6 +19,7 @@ use crate::flutter::connection_manager::{self, get_clients_length, get_clients_s use crate::flutter::{self, Session, SESSIONS}; use crate::start_server; use crate::ui_interface; +use crate::ui_interface::get_sound_inputs; fn initialize(app_dir: &str) { *config::APP_DIR.write().unwrap() = app_dir.to_owned(); @@ -378,6 +379,10 @@ pub fn session_resume_job(id: String, act_id: i32, is_remote: bool) { } } +pub fn main_get_sound_inputs() -> Vec { + get_sound_inputs() +} + /// FFI for **get** commands which are idempotent. /// Return result in c string. /// From f4e0b6e50a2952b13b16e309276a04a803ef9eed Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 15 Jul 2022 17:00:37 +0800 Subject: [PATCH 0088/2015] add: change id on flutter --- .../lib/desktop/pages/desktop_home_page.dart | 62 +++++++++++++++++++ flutter/pubspec.lock | 4 +- src/flutter_ffi.rs | 10 ++- 3 files changed, 73 insertions(+), 3 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 02b87f1c7..305155f0e 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -266,6 +266,8 @@ class _DesktopHomePageState extends State with TrayListener { } else if (value == "stop-service") { final option = gFFI.getOption(value); gFFI.setOption(value, option == "Y" ? "" : "Y"); + } else if (value == "change-id") { + changeId(); } } @@ -341,4 +343,64 @@ class _DesktopHomePageState extends State with TrayListener { }, ), value: 'audio-input',); } + + /// change local ID + void changeId() { + var newId = ""; + var msg = ""; + var isInProgress = false; + DialogManager.show( (setState, close) { + return CustomAlertDialog( + title: Text(translate("Change ID")), + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(translate("id_change_tip")), + Offstage( + offstage: msg.isEmpty, + child: Text(msg, style: TextStyle(color: Colors.grey),)).marginOnly(bottom: 4.0), + TextField( + onChanged: (s) { + newId = s; + }, + decoration: InputDecoration( + border: OutlineInputBorder() + ), + inputFormatters: [ + LengthLimitingTextInputFormatter(16), + // FilteringTextInputFormatter(RegExp(r"[a-zA-z][a-zA-z0-9\_]*"), allow: true) + ], + maxLength: 16, + ), + SizedBox(height: 4.0,), + Offstage( + offstage: !isInProgress, + child: LinearProgressIndicator()) + ], + ), actions: [ + TextButton(onPressed: (){ + close(); + }, child: Text("取消")), + TextButton(onPressed: () async { + setState(() { + msg = ""; + isInProgress = true; + gFFI.bind.mainChangeId(newId: newId); + }); + + var status = await gFFI.bind.mainGetAsyncStatus(); + while (status == " "){ + await Future.delayed(Duration(milliseconds: 100)); + status = await gFFI.bind.mainGetAsyncStatus(); + } + setState(() { + isInProgress = false; + msg = translate(status); + }); + + }, child: Text("确定")), + ], + ); + }); + } } diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index b34076310..a798799f1 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -187,8 +187,8 @@ packages: dependency: "direct main" description: path: "." - ref: c7d97cb6615f2def34f8bad4def01af9e0077beb - resolved-ref: c7d97cb6615f2def34f8bad4def01af9e0077beb + ref: "7b72918710921f5fe79eae2dbaa411a66f5dfb45" + resolved-ref: "7b72918710921f5fe79eae2dbaa411a66f5dfb45" url: "https://github.com/Kingtous/rustdesk_desktop_multi_window" source: git version: "0.0.1" diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index f10fd6587..60d8fd4b5 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -19,7 +19,7 @@ use crate::flutter::connection_manager::{self, get_clients_length, get_clients_s use crate::flutter::{self, Session, SESSIONS}; use crate::start_server; use crate::ui_interface; -use crate::ui_interface::get_sound_inputs; +use crate::ui_interface::{change_id, get_async_job_status, get_sound_inputs, is_ok_change_id}; fn initialize(app_dir: &str) { *config::APP_DIR.write().unwrap() = app_dir.to_owned(); @@ -383,6 +383,14 @@ pub fn main_get_sound_inputs() -> Vec { get_sound_inputs() } +pub fn main_change_id(new_id: String) { + change_id(new_id) +} + +pub fn main_get_async_status() -> String { + get_async_job_status() +} + /// FFI for **get** commands which are idempotent. /// Return result in c string. /// From 7c24f6bb12964df3d152b2518fb299fe85d0233c Mon Sep 17 00:00:00 2001 From: Asura Date: Sun, 17 Jul 2022 14:14:51 -0700 Subject: [PATCH 0089/2015] Refactor listening keyboard to support switching keyboard modes --- libs/enigo/src/win/win_impl.rs | 2 +- libs/hbb_common/protos/message.proto | 1 + src/ui/remote.rs | 600 ++++++++++++++------------- 3 files changed, 306 insertions(+), 297 deletions(-) diff --git a/libs/enigo/src/win/win_impl.rs b/libs/enigo/src/win/win_impl.rs index 0967d07c9..b8f3d0fa3 100644 --- a/libs/enigo/src/win/win_impl.rs +++ b/libs/enigo/src/win/win_impl.rs @@ -2,7 +2,7 @@ use self::winapi::ctypes::c_int; use self::winapi::shared::{basetsd::ULONG_PTR, minwindef::*, windef::*}; use self::winapi::um::winbase::*; use self::winapi::um::winuser::*; -use rdev::{simulate, EventType, EventType::*, Key as RdevKey, SimulateError}; +use rdev::{simulate, EventType, Key as RdevKey, SimulateError}; use winapi; use crate::win::keycodes::*; diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index 0538f1aef..645930eb4 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -190,6 +190,7 @@ message KeyEvent { string seq = 6; } repeated ControlKey modifiers = 8; + uint32 mode = 9; } message CursorData { diff --git a/src/ui/remote.rs b/src/ui/remote.rs index cc2ce8154..8ebced2c4 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -43,6 +43,7 @@ use hbb_common::{ Stream, }; use hbb_common::{config::TransferSerde, fs::TransferJobMeta}; +use rdev::{Event, EventType::*, Key as RdevKey}; #[cfg(windows)] use crate::clipboard_file::*; @@ -288,288 +289,54 @@ impl Handler { crate::platform::windows::enable_lowlevel_keyboard(std::ptr::null_mut() as _); std::thread::spawn(move || { // This will block. - std::env::set_var("KEYBOARD_ONLY", "y"); // pass to rdev - let keyboard_mode = 1; - if keyboard_mode == 1 { - use rdev::{Event, EventType::*, Key as RdevKey}; - lazy_static::lazy_static! { - static ref MUTEX_SPECIAL_KEYS: Mutex> = { - let mut m = HashMap::new(); - // m.insert(RdevKey::PrintScreen, false); // TODO - m.insert(RdevKey::ShiftLeft, false); - m.insert(RdevKey::ShiftRight, false); - m.insert(RdevKey::ControlLeft, false); - m.insert(RdevKey::ControlRight, false); - m.insert(RdevKey::Alt, false); - m.insert(RdevKey::AltGr, false); - Mutex::new(m) - }; - } - // todo: auto change paltform - let func = move |evt: Event| { - if !IS_IN.load(Ordering::SeqCst) - || !SERVER_KEYBOARD_ENABLED.load(Ordering::SeqCst) - { - return; - } - let (key, down) = match evt.event_type { - KeyPress(k) => { - // keyboard long press - if MUTEX_SPECIAL_KEYS.lock().unwrap().contains_key(&k) { - if *MUTEX_SPECIAL_KEYS.lock().unwrap().get(&k).unwrap() { - return; - } - MUTEX_SPECIAL_KEYS.lock().unwrap().insert(k, true); - } - (k, 1) - } - KeyRelease(k) => { - // keyboard long press - if MUTEX_SPECIAL_KEYS.lock().unwrap().contains_key(&k) { - MUTEX_SPECIAL_KEYS.lock().unwrap().insert(k, false); - } - (k, 0) - } - _ => return, - }; - - // todo: clear key - #[cfg(target_os = "windows")] - let key = rdev::get_win_key(evt.code.into(), evt.scan_code); - - let mut key_event = KeyEvent::new(); - // According to peer platform. - if peer == "Linux" { - let keycode: u32 = rdev::linux_keycode_from_key(key).unwrap().into(); - key_event.set_chr(keycode); - } else if peer == "Windows" { - let keycode: u32 = rdev::win_keycode_from_key(key).unwrap().into(); - key_event.set_chr(keycode); - } else if peer == "Mac OS" { - let keycode: u32 = rdev::macos_keycode_from_key(key).unwrap().into(); - key_event.set_chr(keycode); - } - me.key_down_or_up(down, key_event, false, false, false, false); + std::env::set_var("KEYBOARD_ONLY", "y"); + lazy_static::lazy_static! { + static ref MUTEX_SPECIAL_KEYS: Mutex> = { + let mut m = HashMap::new(); + m.insert(RdevKey::ShiftLeft, false); + m.insert(RdevKey::ShiftRight, false); + m.insert(RdevKey::ControlLeft, false); + m.insert(RdevKey::ControlRight, false); + m.insert(RdevKey::Alt, false); + m.insert(RdevKey::AltGr, false); + Mutex::new(m) }; - if let Err(error) = rdev::listen(func) { - log::error!("rdev: {:?}", error); + } + + let func = move |evt: Event| { + if !IS_IN.load(Ordering::SeqCst) || !SERVER_KEYBOARD_ENABLED.load(Ordering::SeqCst) + { + return; } - } else { - use rdev::{EventType::*, *}; - let func = move |evt: Event| { - if !IS_IN.load(Ordering::SeqCst) - || !SERVER_KEYBOARD_ENABLED.load(Ordering::SeqCst) - { - return; + let (key, down) = match evt.event_type { + KeyPress(k) => { + // keyboard long press + if MUTEX_SPECIAL_KEYS.lock().unwrap().contains_key(&k) { + if *MUTEX_SPECIAL_KEYS.lock().unwrap().get(&k).unwrap() { + return; + } + MUTEX_SPECIAL_KEYS.lock().unwrap().insert(k, true); + } + (k, true) } - let (key, down) = match evt.event_type { - KeyPress(k) => (k, 1), - KeyRelease(k) => (k, 0), - _ => return, - }; - let alt = get_key_state(enigo::Key::Alt); - #[cfg(windows)] - let ctrl = { - let mut tmp = get_key_state(enigo::Key::Control); - unsafe { - if IS_ALT_GR { - if alt || key == Key::AltGr { - if tmp { - tmp = false; - } - } else { - IS_ALT_GR = false; - } - } - } - tmp - }; - #[cfg(not(windows))] - let ctrl = get_key_state(enigo::Key::Control); - let shift = get_key_state(enigo::Key::Shift); - #[cfg(windows)] - let command = crate::platform::windows::get_win_key_state(); - #[cfg(not(windows))] - let command = get_key_state(enigo::Key::Meta); - let control_key = match key { - Key::Alt => Some(ControlKey::Alt), - Key::AltGr => Some(ControlKey::RAlt), - Key::Backspace => Some(ControlKey::Backspace), - Key::ControlLeft => { - // when pressing AltGr, an extra VK_LCONTROL with a special - // scancode with bit 9 set is sent, let's ignore this. - #[cfg(windows)] - if evt.scan_code & 0x200 != 0 { - unsafe { - IS_ALT_GR = true; - } - return; - } - Some(ControlKey::Control) - } - Key::ControlRight => Some(ControlKey::RControl), - Key::DownArrow => Some(ControlKey::DownArrow), - Key::Escape => Some(ControlKey::Escape), - Key::F1 => Some(ControlKey::F1), - Key::F10 => Some(ControlKey::F10), - Key::F11 => Some(ControlKey::F11), - Key::F12 => Some(ControlKey::F12), - Key::F2 => Some(ControlKey::F2), - Key::F3 => Some(ControlKey::F3), - Key::F4 => Some(ControlKey::F4), - Key::F5 => Some(ControlKey::F5), - Key::F6 => Some(ControlKey::F6), - Key::F7 => Some(ControlKey::F7), - Key::F8 => Some(ControlKey::F8), - Key::F9 => Some(ControlKey::F9), - Key::LeftArrow => Some(ControlKey::LeftArrow), - Key::MetaLeft => Some(ControlKey::Meta), - Key::MetaRight => Some(ControlKey::RWin), - Key::Return => Some(ControlKey::Return), - Key::RightArrow => Some(ControlKey::RightArrow), - Key::ShiftLeft => Some(ControlKey::Shift), - Key::ShiftRight => Some(ControlKey::RShift), - Key::Space => Some(ControlKey::Space), - Key::Tab => Some(ControlKey::Tab), - Key::UpArrow => Some(ControlKey::UpArrow), - Key::Delete => { - if is_win && ctrl && alt { - me.ctrl_alt_del(); - return; - } - Some(ControlKey::Delete) - } - Key::Apps => Some(ControlKey::Apps), - Key::Cancel => Some(ControlKey::Cancel), - Key::Clear => Some(ControlKey::Clear), - Key::Kana => Some(ControlKey::Kana), - Key::Hangul => Some(ControlKey::Hangul), - Key::Junja => Some(ControlKey::Junja), - Key::Final => Some(ControlKey::Final), - Key::Hanja => Some(ControlKey::Hanja), - Key::Hanji => Some(ControlKey::Hanja), - Key::Convert => Some(ControlKey::Convert), - Key::Print => Some(ControlKey::Print), - Key::Select => Some(ControlKey::Select), - Key::Execute => Some(ControlKey::Execute), - Key::PrintScreen => Some(ControlKey::Snapshot), - Key::Help => Some(ControlKey::Help), - Key::Sleep => Some(ControlKey::Sleep), - Key::Separator => Some(ControlKey::Separator), - Key::KpReturn => Some(ControlKey::NumpadEnter), - Key::Kp0 => Some(ControlKey::Numpad0), - Key::Kp1 => Some(ControlKey::Numpad1), - Key::Kp2 => Some(ControlKey::Numpad2), - Key::Kp3 => Some(ControlKey::Numpad3), - Key::Kp4 => Some(ControlKey::Numpad4), - Key::Kp5 => Some(ControlKey::Numpad5), - Key::Kp6 => Some(ControlKey::Numpad6), - Key::Kp7 => Some(ControlKey::Numpad7), - Key::Kp8 => Some(ControlKey::Numpad8), - Key::Kp9 => Some(ControlKey::Numpad9), - Key::KpDivide => Some(ControlKey::Divide), - Key::KpMultiply => Some(ControlKey::Multiply), - Key::KpDecimal => Some(ControlKey::Decimal), - Key::KpMinus => Some(ControlKey::Subtract), - Key::KpPlus => Some(ControlKey::Add), - Key::CapsLock | Key::NumLock | Key::ScrollLock => { - return; - } - Key::Home => Some(ControlKey::Home), - Key::End => Some(ControlKey::End), - Key::Insert => Some(ControlKey::Insert), - Key::PageUp => Some(ControlKey::PageUp), - Key::PageDown => Some(ControlKey::PageDown), - Key::Pause => Some(ControlKey::Pause), - _ => None, - }; - let mut key_event = KeyEvent::new(); - if let Some(k) = control_key { - key_event.set_control_key(k); - } else { - let mut chr = match evt.name { - Some(ref s) => { - if s.len() <= 2 { - // exclude chinese characters - s.chars().next().unwrap_or('\0') - } else { - '\0' - } - } - _ => '\0', - }; - if chr == '·' { - // special for Chinese - chr = '`'; - } - if chr == '\0' { - chr = match key { - Key::Num1 => '1', - Key::Num2 => '2', - Key::Num3 => '3', - Key::Num4 => '4', - Key::Num5 => '5', - Key::Num6 => '6', - Key::Num7 => '7', - Key::Num8 => '8', - Key::Num9 => '9', - Key::Num0 => '0', - Key::KeyA => 'a', - Key::KeyB => 'b', - Key::KeyC => 'c', - Key::KeyD => 'd', - Key::KeyE => 'e', - Key::KeyF => 'f', - Key::KeyG => 'g', - Key::KeyH => 'h', - Key::KeyI => 'i', - Key::KeyJ => 'j', - Key::KeyK => 'k', - Key::KeyL => 'l', - Key::KeyM => 'm', - Key::KeyN => 'n', - Key::KeyO => 'o', - Key::KeyP => 'p', - Key::KeyQ => 'q', - Key::KeyR => 'r', - Key::KeyS => 's', - Key::KeyT => 't', - Key::KeyU => 'u', - Key::KeyV => 'v', - Key::KeyW => 'w', - Key::KeyX => 'x', - Key::KeyY => 'y', - Key::KeyZ => 'z', - Key::Comma => ',', - Key::Dot => '.', - Key::SemiColon => ';', - Key::Quote => '\'', - Key::LeftBracket => '[', - Key::RightBracket => ']', - Key::BackSlash => '\\', - Key::Minus => '-', - Key::Equal => '=', - Key::BackQuote => '`', - _ => '\0', - } - } - if chr != '\0' { - if chr == 'l' && is_win && command { - me.lock_screen(); - return; - } - key_event.set_chr(chr as _); - } else { - log::error!("Unknown key {:?}", evt); - return; + KeyRelease(k) => { + // keyboard long press + if MUTEX_SPECIAL_KEYS.lock().unwrap().contains_key(&k) { + MUTEX_SPECIAL_KEYS.lock().unwrap().insert(k, false); } + (k, false) } - me.key_down_or_up(down, key_event, alt, ctrl, shift, command); + _ => return, }; - if let Err(error) = rdev::listen(func) { - log::error!("rdev: {:?}", error); - } + + #[cfg(target_os = "windows")] + let key = rdev::get_win_key(evt.code.into(), evt.scan_code); + + me.key_down_or_up(down, key, evt); }; + if let Err(error) = rdev::listen(func) { + log::error!("rdev: {:?}", error); + } }); } @@ -1150,18 +917,19 @@ impl Handler { if self.peer_platform() == "Windows" { let mut key_event = KeyEvent::new(); key_event.set_control_key(ControlKey::CtrlAltDel); - self.key_down_or_up(1, key_event, false, false, false, false); + // todo + self.send_key_event(key_event, 2); } else { let mut key_event = KeyEvent::new(); key_event.set_control_key(ControlKey::Delete); - self.key_down_or_up(3, key_event, true, true, false, false); + // self.key_down_or_up(3, key_event, true, true, false, false); } } fn lock_screen(&mut self) { let mut key_event = KeyEvent::new(); key_event.set_control_key(ControlKey::LockScreen); - self.key_down_or_up(1, key_event, false, false, false, false); + // self.key_down_or_up(1, key_event, false, false, false, false); } fn transfer_file(&mut self) { @@ -1180,16 +948,246 @@ impl Handler { } } - fn key_down_or_up( - &mut self, - down_or_up: i32, - evt: KeyEvent, - alt: bool, - ctrl: bool, - shift: bool, - command: bool, - ) { - let mut key_event = evt; + fn send_key_event(&mut self, mut evt: KeyEvent, keyboard_mode: u32) { + // mode: map(1), translate(2), legacy(3), auto(4) + evt.mode = keyboard_mode; + let mut msg_out = Message::new(); + msg_out.set_key_event(evt); + log::info!("{:?}", msg_out); + self.send(Data::Message(msg_out)); + } + + fn map_keyboard_mode(&mut self, down_or_up: bool, key: RdevKey) { + // map mode(1): Send keycode according to the peer platform. + let peer = self.peer_platform(); + + let mut key_event = KeyEvent::new(); + // According to peer platform. + if peer == "Linux" { + let keycode: u32 = rdev::linux_keycode_from_key(key).unwrap_or_default().into(); + key_event.set_chr(keycode); + } else if peer == "Windows" { + let keycode: u32 = rdev::win_keycode_from_key(key).unwrap_or_default().into(); + key_event.set_chr(keycode); + } else if peer == "Mac OS" { + let keycode: u32 = rdev::macos_keycode_from_key(key).unwrap_or_default().into(); + key_event.set_chr(keycode); + } + if down_or_up == true { + key_event.down = true; + } else if down_or_up == true { + key_event.press = true; + } + self.send_key_event(key_event, 1); + } + + fn translate_keyboard_mode(&mut self, down_or_up: bool, key: RdevKey) { + // translate mode(2): locally generated characters are send to the peer. + } + + fn legacy_keyboard_mode(&mut self, down_or_up: bool, key: RdevKey, evt: Event) { + // legacy mode(3): Generate characters locally, look for keycode on other side. + println!("legacy_keyboard_mode {:?}", key); + let peer = self.peer_platform(); + let is_win = peer == "Windows"; + + let alt = get_key_state(enigo::Key::Alt); + #[cfg(windows)] + let ctrl = { + let mut tmp = get_key_state(enigo::Key::Control); + unsafe { + if IS_ALT_GR { + if alt || key == RdevKey::AltGr { + if tmp { + tmp = false; + } + } else { + IS_ALT_GR = false; + } + } + } + tmp + }; + #[cfg(not(windows))] + let ctrl = get_key_state(enigo::Key::Control); + let shift = get_key_state(enigo::Key::Shift); + #[cfg(windows)] + let command = crate::platform::windows::get_win_key_state(); + #[cfg(not(windows))] + let command = get_key_state(enigo::Key::Meta); + let control_key = match key { + RdevKey::Alt => Some(ControlKey::Alt), + RdevKey::AltGr => Some(ControlKey::RAlt), + RdevKey::Backspace => Some(ControlKey::Backspace), + RdevKey::ControlLeft => { + // when pressing AltGr, an extra VK_LCONTROL with a special + // scancode with bit 9 set is sent, let's ignore this. + #[cfg(windows)] + if evt.scan_code & 0x200 != 0 { + unsafe { + IS_ALT_GR = true; + } + return; + } + Some(ControlKey::Control) + } + RdevKey::ControlRight => Some(ControlKey::RControl), + RdevKey::DownArrow => Some(ControlKey::DownArrow), + RdevKey::Escape => Some(ControlKey::Escape), + RdevKey::F1 => Some(ControlKey::F1), + RdevKey::F10 => Some(ControlKey::F10), + RdevKey::F11 => Some(ControlKey::F11), + RdevKey::F12 => Some(ControlKey::F12), + RdevKey::F2 => Some(ControlKey::F2), + RdevKey::F3 => Some(ControlKey::F3), + RdevKey::F4 => Some(ControlKey::F4), + RdevKey::F5 => Some(ControlKey::F5), + RdevKey::F6 => Some(ControlKey::F6), + RdevKey::F7 => Some(ControlKey::F7), + RdevKey::F8 => Some(ControlKey::F8), + RdevKey::F9 => Some(ControlKey::F9), + RdevKey::LeftArrow => Some(ControlKey::LeftArrow), + RdevKey::MetaLeft => Some(ControlKey::Meta), + RdevKey::MetaRight => Some(ControlKey::RWin), + RdevKey::Return => Some(ControlKey::Return), + RdevKey::RightArrow => Some(ControlKey::RightArrow), + RdevKey::ShiftLeft => Some(ControlKey::Shift), + RdevKey::ShiftRight => Some(ControlKey::RShift), + RdevKey::Space => Some(ControlKey::Space), + RdevKey::Tab => Some(ControlKey::Tab), + RdevKey::UpArrow => Some(ControlKey::UpArrow), + RdevKey::Delete => { + if is_win && ctrl && alt { + self.ctrl_alt_del(); + return; + } + Some(ControlKey::Delete) + } + RdevKey::Apps => Some(ControlKey::Apps), + RdevKey::Cancel => Some(ControlKey::Cancel), + RdevKey::Clear => Some(ControlKey::Clear), + RdevKey::Kana => Some(ControlKey::Kana), + RdevKey::Hangul => Some(ControlKey::Hangul), + RdevKey::Junja => Some(ControlKey::Junja), + RdevKey::Final => Some(ControlKey::Final), + RdevKey::Hanja => Some(ControlKey::Hanja), + RdevKey::Hanji => Some(ControlKey::Hanja), + RdevKey::Convert => Some(ControlKey::Convert), + RdevKey::Print => Some(ControlKey::Print), + RdevKey::Select => Some(ControlKey::Select), + RdevKey::Execute => Some(ControlKey::Execute), + RdevKey::PrintScreen => Some(ControlKey::Snapshot), + RdevKey::Help => Some(ControlKey::Help), + RdevKey::Sleep => Some(ControlKey::Sleep), + RdevKey::Separator => Some(ControlKey::Separator), + RdevKey::KpReturn => Some(ControlKey::NumpadEnter), + RdevKey::Kp0 => Some(ControlKey::Numpad0), + RdevKey::Kp1 => Some(ControlKey::Numpad1), + RdevKey::Kp2 => Some(ControlKey::Numpad2), + RdevKey::Kp3 => Some(ControlKey::Numpad3), + RdevKey::Kp4 => Some(ControlKey::Numpad4), + RdevKey::Kp5 => Some(ControlKey::Numpad5), + RdevKey::Kp6 => Some(ControlKey::Numpad6), + RdevKey::Kp7 => Some(ControlKey::Numpad7), + RdevKey::Kp8 => Some(ControlKey::Numpad8), + RdevKey::Kp9 => Some(ControlKey::Numpad9), + RdevKey::KpDivide => Some(ControlKey::Divide), + RdevKey::KpMultiply => Some(ControlKey::Multiply), + RdevKey::KpDecimal => Some(ControlKey::Decimal), + RdevKey::KpMinus => Some(ControlKey::Subtract), + RdevKey::KpPlus => Some(ControlKey::Add), + RdevKey::CapsLock | RdevKey::NumLock | RdevKey::ScrollLock => { + return; + } + RdevKey::Home => Some(ControlKey::Home), + RdevKey::End => Some(ControlKey::End), + RdevKey::Insert => Some(ControlKey::Insert), + RdevKey::PageUp => Some(ControlKey::PageUp), + RdevKey::PageDown => Some(ControlKey::PageDown), + RdevKey::Pause => Some(ControlKey::Pause), + _ => None, + }; + let mut key_event = KeyEvent::new(); + if let Some(k) = control_key { + key_event.set_control_key(k); + } else { + let mut chr = match evt.name { + Some(ref s) => { + if s.len() <= 2 { + // exclude chinese characters + s.chars().next().unwrap_or('\0') + } else { + '\0' + } + } + _ => '\0', + }; + if chr == '·' { + // special for Chinese + chr = '`'; + } + if chr == '\0' { + chr = match key { + RdevKey::Num1 => '1', + RdevKey::Num2 => '2', + RdevKey::Num3 => '3', + RdevKey::Num4 => '4', + RdevKey::Num5 => '5', + RdevKey::Num6 => '6', + RdevKey::Num7 => '7', + RdevKey::Num8 => '8', + RdevKey::Num9 => '9', + RdevKey::Num0 => '0', + RdevKey::KeyA => 'a', + RdevKey::KeyB => 'b', + RdevKey::KeyC => 'c', + RdevKey::KeyD => 'd', + RdevKey::KeyE => 'e', + RdevKey::KeyF => 'f', + RdevKey::KeyG => 'g', + RdevKey::KeyH => 'h', + RdevKey::KeyI => 'i', + RdevKey::KeyJ => 'j', + RdevKey::KeyK => 'k', + RdevKey::KeyL => 'l', + RdevKey::KeyM => 'm', + RdevKey::KeyN => 'n', + RdevKey::KeyO => 'o', + RdevKey::KeyP => 'p', + RdevKey::KeyQ => 'q', + RdevKey::KeyR => 'r', + RdevKey::KeyS => 's', + RdevKey::KeyT => 't', + RdevKey::KeyU => 'u', + RdevKey::KeyV => 'v', + RdevKey::KeyW => 'w', + RdevKey::KeyX => 'x', + RdevKey::KeyY => 'y', + RdevKey::KeyZ => 'z', + RdevKey::Comma => ',', + RdevKey::Dot => '.', + RdevKey::SemiColon => ';', + RdevKey::Quote => '\'', + RdevKey::LeftBracket => '[', + RdevKey::RightBracket => ']', + RdevKey::BackSlash => '\\', + RdevKey::Minus => '-', + RdevKey::Equal => '=', + RdevKey::BackQuote => '`', + _ => '\0', + } + } + if chr != '\0' { + if chr == 'l' && is_win && command { + self.lock_screen(); + return; + } + key_event.set_chr(chr as _); + } else { + log::error!("Unknown key {:?}", evt); + return; + } + } if alt && !crate::is_control_key(&key_event, &ControlKey::Alt) @@ -1223,15 +1221,25 @@ impl Handler { key_event.modifiers.push(ControlKey::NumLock.into()); } } - if down_or_up == 1 { + if down_or_up == true { key_event.down = true; - } else if down_or_up == 3 { - key_event.press = true; } - let mut msg_out = Message::new(); - msg_out.set_key_event(key_event); - log::debug!("{:?}", msg_out); - self.send(Data::Message(msg_out)); + dbg!(&key_event); + self.send_key_event(key_event, 2) + } + + fn key_down_or_up(&mut self, down_or_up: bool, key: RdevKey, evt: Event) { + // Call different functions according to keyboard mode. + let mode = std::env::var("KEYBOARD_MOAD").unwrap_or(String::from("map")); + match mode.as_str() { + "map" => { + self.map_keyboard_mode(down_or_up, key); + } + "legacy" => self.legacy_keyboard_mode(down_or_up, key, evt), + _ => { + self.map_keyboard_mode(down_or_up, key); + } + } } #[inline] @@ -2692,4 +2700,4 @@ impl Handler { async fn send_note(url: String, id: String, conn_id: i32, note: String) { let body = serde_json::json!({ "id": id, "Id": conn_id, "note": note }); allow_err!(crate::post_request(url, body.to_string(), "").await); -} +} \ No newline at end of file From 5dab7bd9a218caf48ae2ae9f1fa63a827e19d8c1 Mon Sep 17 00:00:00 2001 From: Asura Date: Sun, 17 Jul 2022 20:34:08 -0700 Subject: [PATCH 0090/2015] Refactor simulate to support switching keyboard modes --- libs/enigo/src/linux.rs | 43 +++----------- libs/enigo/src/macos/macos_impl.rs | 32 ----------- libs/enigo/src/win/win_impl.rs | 32 ----------- src/server/connection.rs | 9 +-- src/server/input_service.rs | 90 +++++++++++++++++++++++++----- 5 files changed, 84 insertions(+), 122 deletions(-) diff --git a/libs/enigo/src/linux.rs b/libs/enigo/src/linux.rs index 7f2afc3dc..67af71a62 100644 --- a/libs/enigo/src/linux.rs +++ b/libs/enigo/src/linux.rs @@ -3,7 +3,6 @@ use libc; use crate::{Key, KeyboardControllable, MouseButton, MouseControllable}; use self::libc::{c_char, c_int, c_void, useconds_t}; -use rdev::{simulate, EventType, EventType::*, Key as RdevKey, SimulateError}; use std::{borrow::Cow, ffi::CString, io::prelude::*, ptr, sync::mpsc, thread, time}; const CURRENT_WINDOW: c_int = 0; const DEFAULT_DELAY: u64 = 12000; @@ -104,27 +103,7 @@ impl Enigo { self.tx.send((PyMsg::Char('\0'), true)).ok(); } - fn send_rdev(&mut self, key: &Key, is_press: bool) -> bool { - if let Key::Raw(keycode) = key { - let event_type = match is_press { - // todo: Acccodding to client type - true => Box::leak(Box::new(EventType::KeyPress(RdevKey::Unknown( - (*keycode).into(), - )))), - false => Box::leak(Box::new(EventType::KeyRelease(RdevKey::Unknown( - (*keycode).into(), - )))), - }; - - match simulate(event_type) { - Ok(()) => true, - Err(SimulateError) => false, - } - } else { - false - } - } - + #[inline] fn send_pynput(&mut self, key: &Key, is_press: bool) -> bool { if unsafe { PYNPUT_EXIT || !PYNPUT_REDAY } { @@ -459,12 +438,9 @@ impl KeyboardControllable for Enigo { if self.xdo.is_null() { return Ok(()); } - if self.send_rdev(&key, true) { - return Ok(()); - } - if self.send_pynput(&key, true) { - return Ok(()); - } + // if self.send_pynput(&key, true) { + // return Ok(()); + // } let string = CString::new(&*keysequence(key))?; unsafe { xdo_send_keysequence_window_down( @@ -480,14 +456,9 @@ impl KeyboardControllable for Enigo { if self.xdo.is_null() { return; } - // todo - let keyboard_mode = 1; - if keyboard_mode == 1 && self.send_rdev(&key, false) { - return; - } - if self.send_pynput(&key, false) { - return; - } + // if self.send_pynput(&key, false) { + // return; + // } if let Ok(string) = CString::new(&*keysequence(key)) { unsafe { xdo_send_keysequence_window_up( diff --git a/libs/enigo/src/macos/macos_impl.rs b/libs/enigo/src/macos/macos_impl.rs index df494b9cb..520c9dca1 100644 --- a/libs/enigo/src/macos/macos_impl.rs +++ b/libs/enigo/src/macos/macos_impl.rs @@ -1,5 +1,4 @@ use core_graphics; -use rdev::{simulate, EventType, EventType::*, Key as RdevKey, SimulateError}; // TODO(dustin): use only the things i need use self::core_graphics::display::*; @@ -354,11 +353,6 @@ impl KeyboardControllable for Enigo { } fn key_down(&mut self, key: Key) -> crate::ResultType { - let keyboard_mode = 1; - if keyboard_mode == 1 { - self.send_rdev(&key, true); - return Ok(()); - }; let code = self.key_to_keycode(key); if code == u16::MAX { return Err("".into()); @@ -374,11 +368,6 @@ impl KeyboardControllable for Enigo { } fn key_up(&mut self, key: Key) { - let keyboard_mode = 1; - if keyboard_mode == 1 { - self.send_rdev(&key, true); - return Ok(()); - }; if let Some(src) = self.event_source.as_ref() { if let Ok(event) = CGEvent::new_keyboard_event(src.clone(), self.key_to_keycode(key), false) @@ -431,27 +420,6 @@ impl Enigo { (x, (display_height as i32) - y_inv) } - fn send_rdev(&mut self, key: &Key, is_press: bool) -> bool { - if let Key::Raw(keycode) = key { - let event_type = match is_press { - // todo: Acccodding to client type - true => Box::leak(Box::new(EventType::KeyPress(RdevKey::Unknown( - (*keycode).into(), - )))), - false => Box::leak(Box::new(EventType::KeyRelease(RdevKey::Unknown( - (*keycode).into(), - )))), - }; - - match simulate(event_type) { - Ok(()) => true, - Err(SimulateError) => false, - } - } else { - false - } - } - fn key_to_keycode(&mut self, key: Key) -> CGKeyCode { #[allow(deprecated)] // I mean duh, we still need to support deprecated keys until they're removed diff --git a/libs/enigo/src/win/win_impl.rs b/libs/enigo/src/win/win_impl.rs index b8f3d0fa3..56fc4caef 100644 --- a/libs/enigo/src/win/win_impl.rs +++ b/libs/enigo/src/win/win_impl.rs @@ -2,7 +2,6 @@ use self::winapi::ctypes::c_int; use self::winapi::shared::{basetsd::ULONG_PTR, minwindef::*, windef::*}; use self::winapi::um::winbase::*; use self::winapi::um::winuser::*; -use rdev::{simulate, EventType, Key as RdevKey, SimulateError}; use winapi; use crate::win::keycodes::*; @@ -198,11 +197,6 @@ impl KeyboardControllable for Enigo { } fn key_down(&mut self, key: Key) -> crate::ResultType { - let keyboard_mode = 1; - if keyboard_mode == 1 { - self.send_rdev(&key, true); - return Ok(()); - }; let code = self.key_to_keycode(key); if code == 0 || code == 65535 { return Err("".into()); @@ -218,11 +212,6 @@ impl KeyboardControllable for Enigo { } fn key_up(&mut self, key: Key) { - let keyboard_mode = 1; - if keyboard_mode == 1 { - self.send_rdev(&key, false); - return; - }; keybd_event(KEYEVENTF_KEYUP, self.key_to_keycode(key), 0); } @@ -283,27 +272,6 @@ impl Enigo { keybd_event(KEYEVENTF_UNICODE | KEYEVENTF_KEYUP, 0, unicode_char); } - fn send_rdev(&mut self, key: &Key, is_press: bool) -> bool { - if let Key::Raw(keycode) = key { - let event_type = match is_press { - // todo: Acccodding to client type - true => Box::leak(Box::new(EventType::KeyPress(RdevKey::Unknown( - (*keycode).into(), - )))), - false => Box::leak(Box::new(EventType::KeyRelease(RdevKey::Unknown( - (*keycode).into(), - )))), - }; - - match simulate(event_type) { - Ok(()) => true, - Err(SimulateError) => false, - } - } else { - false - } - } - fn key_to_keycode(&self, key: Key) -> u16 { unsafe { LAYOUT = std::ptr::null_mut(); diff --git a/src/server/connection.rs b/src/server/connection.rs index 682c7d928..48fab0c2d 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -419,15 +419,8 @@ impl Connection { handle_mouse(&msg, id); } MessageInput::Key((mut msg, press)) => { - if press { - msg.down = true; - } + // todo: press and down have similar meanings. handle_key(&msg); - let keyboard_mode = 1; - if press && keyboard_mode != 1{ - msg.down = false; - handle_key(&msg); - } } MessageInput::BlockOn => { if crate::platform::block_input(true) { diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 8e8e2abb4..f041b7e86 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -3,6 +3,7 @@ use super::*; use dispatch::Queue; use enigo::{Enigo, Key, KeyboardControllable, MouseButton, MouseControllable}; use hbb_common::{config::COMPRESS_LEVEL, protobuf::ProtobufEnumOrUnknown}; +use rdev::{simulate, EventType, EventType::*, Key as RdevKey, SimulateError}; use std::{ convert::TryFrom, sync::atomic::{AtomicBool, Ordering}, @@ -578,24 +579,27 @@ pub fn handle_key(evt: &KeyEvent) { handle_key_(evt); } -fn handle_key_(evt: &KeyEvent) { - if EXITING.load(Ordering::SeqCst) { - return; +fn map_keyboard_map(evt: &KeyEvent) { + // map mode(1): Send keycode according to the peer platform. + let event_type = match evt.down { + true => EventType::KeyPress(RdevKey::Unknown(evt.get_chr())), + false => EventType::KeyRelease(RdevKey::Unknown(evt.get_chr())), + }; + + match simulate(&event_type) { + Ok(()) => (), + Err(_simulate_error) => { + // todo + log::error!("rdev could not send {:?}", event_type); + } } + return; +} + +fn legacy_keyboard_map(evt: &KeyEvent) { #[cfg(windows)] crate::platform::windows::try_change_desktop(); let mut en = ENIGO.lock().unwrap(); - let keyboard_mode = 1; - if keyboard_mode == 1 { - if let Some(key_event::Union::chr(chr)) = evt.union { - if evt.down { - en.key_down(Key::Raw(chr.try_into().unwrap())); - } else { - en.key_up(Key::Raw(chr.try_into().unwrap())); - } - } - return; - } // disable numlock if press home etc when numlock is on, // because we will get numpad value (7,8,9 etc) if not #[cfg(windows)] @@ -740,9 +744,67 @@ fn handle_key_(evt: &KeyEvent) { } } +fn handle_key_(evt: &KeyEvent) { + if EXITING.load(Ordering::SeqCst) { + return; + } + + match evt.mode { + 1 => { + map_keyboard_map(evt); + } + 3 => { + legacy_keyboard_map(evt); + } + _ => { + map_keyboard_map(evt); + } + } +} + #[tokio::main(flavor = "current_thread")] async fn send_sas() -> ResultType<()> { let mut stream = crate::ipc::connect(1000, crate::POSTFIX_SERVICE).await?; timeout(1000, stream.send(&crate::ipc::Data::SAS)).await??; Ok(()) } + +#[cfg(test)] +mod test { + use super::*; + use rdev::{listen, simulate, Event, EventType, Key}; + use std::sync::mpsc; + use std::thread; + + #[test] + fn test_handle_key() { + // listen + let (tx, rx) = mpsc::channel(); + std::thread::spawn(move || { + std::env::set_var("KEYBOARD_ONLY", "y"); + let func = move |event: Event| { + tx.send(event).ok(); + }; + if let Err(error) = listen(func) { + println!("Error: {:?}", error); + } + }); + // set key/char base on char + let mut evt = KeyEvent::new(); + evt.set_chr(49); + evt.mode = 3; + + // press + evt.down = true; + handle_key(&evt); + if let Ok(listen_evt) = rx.recv() { + assert_eq!(listen_evt.event_type, EventType::KeyPress(Key::Num1)) + } + // release + evt.down = false; + handle_key(&evt); + if let Ok(listen_evt) = rx.recv() { + assert_eq!(listen_evt.event_type, EventType::KeyRelease(Key::Num1)) + } + } +} From 3c61773d75375b493c10f83dd967683a84bbad45 Mon Sep 17 00:00:00 2001 From: Asura Date: Sun, 17 Jul 2022 20:59:14 -0700 Subject: [PATCH 0091/2015] Recover legacy keyboard mode when simulate --- libs/enigo/src/linux.rs | 12 ++++++------ src/server/connection.rs | 7 +++++++ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/libs/enigo/src/linux.rs b/libs/enigo/src/linux.rs index 67af71a62..6b4c99a1b 100644 --- a/libs/enigo/src/linux.rs +++ b/libs/enigo/src/linux.rs @@ -438,9 +438,9 @@ impl KeyboardControllable for Enigo { if self.xdo.is_null() { return Ok(()); } - // if self.send_pynput(&key, true) { - // return Ok(()); - // } + if self.send_pynput(&key, true) { + return Ok(()); + } let string = CString::new(&*keysequence(key))?; unsafe { xdo_send_keysequence_window_down( @@ -456,9 +456,9 @@ impl KeyboardControllable for Enigo { if self.xdo.is_null() { return; } - // if self.send_pynput(&key, false) { - // return; - // } + if self.send_pynput(&key, false) { + return; + } if let Ok(string) = CString::new(&*keysequence(key)) { unsafe { xdo_send_keysequence_window_up( diff --git a/src/server/connection.rs b/src/server/connection.rs index 48fab0c2d..dd5b32fe9 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -420,7 +420,14 @@ impl Connection { } MessageInput::Key((mut msg, press)) => { // todo: press and down have similar meanings. + if press && msg.mode == 3 { + msg.down = true; + } handle_key(&msg); + if press && msg.mode == 3 { + msg.down = false; + handle_key(&msg); + } } MessageInput::BlockOn => { if crate::platform::block_input(true) { From 7ae065739cb7dae729676466c0adf6e69c6e03d9 Mon Sep 17 00:00:00 2001 From: Asura Date: Mon, 18 Jul 2022 12:51:17 +0800 Subject: [PATCH 0092/2015] Recover legacy keyboard mode when listen --- src/ui/remote.rs | 91 +++++++++++++++++++++++++++--------------------- 1 file changed, 51 insertions(+), 40 deletions(-) diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 8ebced2c4..2175cd06b 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -918,18 +918,24 @@ impl Handler { let mut key_event = KeyEvent::new(); key_event.set_control_key(ControlKey::CtrlAltDel); // todo - self.send_key_event(key_event, 2); + key_event.down = true; + self.send_key_event(key_event, 3); } else { let mut key_event = KeyEvent::new(); key_event.set_control_key(ControlKey::Delete); - // self.key_down_or_up(3, key_event, true, true, false, false); + self.legacy_modifiers(&mut key_event, true, true, false, false); + // todo + key_event.press = true; + self.send_key_event(key_event, 3); } } fn lock_screen(&mut self) { let mut key_event = KeyEvent::new(); key_event.set_control_key(ControlKey::LockScreen); - // self.key_down_or_up(1, key_event, false, false, false, false); + // todo + key_event.down = true; + self.send_key_event(key_event, 3); } fn transfer_file(&mut self) { @@ -975,14 +981,49 @@ impl Handler { } if down_or_up == true { key_event.down = true; - } else if down_or_up == true { - key_event.press = true; + } else { + key_event.down = false; } self.send_key_event(key_event, 1); } - fn translate_keyboard_mode(&mut self, down_or_up: bool, key: RdevKey) { - // translate mode(2): locally generated characters are send to the peer. + // fn translate_keyboard_mode(&mut self, down_or_up: bool, key: RdevKey) { + // // translate mode(2): locally generated characters are send to the peer. + // } + + fn legacy_modifiers(&self, key_event: &mut KeyEvent, alt: bool, ctrl: bool, shift: bool, command: bool){ + if alt + && !crate::is_control_key(&key_event, &ControlKey::Alt) + && !crate::is_control_key(&key_event, &ControlKey::RAlt) + { + key_event.modifiers.push(ControlKey::Alt.into()); + } + if shift + && !crate::is_control_key(&key_event, &ControlKey::Shift) + && !crate::is_control_key(&key_event, &ControlKey::RShift) + { + key_event.modifiers.push(ControlKey::Shift.into()); + } + if ctrl + && !crate::is_control_key(&key_event, &ControlKey::Control) + && !crate::is_control_key(&key_event, &ControlKey::RControl) + { + key_event.modifiers.push(ControlKey::Control.into()); + } + if command + && !crate::is_control_key(&key_event, &ControlKey::Meta) + && !crate::is_control_key(&key_event, &ControlKey::RWin) + { + key_event.modifiers.push(ControlKey::Meta.into()); + } + if get_key_state(enigo::Key::CapsLock) { + key_event.modifiers.push(ControlKey::CapsLock.into()); + } + if self.peer_platform() != "Mac OS" { + if get_key_state(enigo::Key::NumLock) && common::valid_for_numlock(&key_event) { + key_event.modifiers.push(ControlKey::NumLock.into()); + } + } } fn legacy_keyboard_mode(&mut self, down_or_up: bool, key: RdevKey, evt: Event) { @@ -1189,38 +1230,8 @@ impl Handler { } } - if alt - && !crate::is_control_key(&key_event, &ControlKey::Alt) - && !crate::is_control_key(&key_event, &ControlKey::RAlt) - { - key_event.modifiers.push(ControlKey::Alt.into()); - } - if shift - && !crate::is_control_key(&key_event, &ControlKey::Shift) - && !crate::is_control_key(&key_event, &ControlKey::RShift) - { - key_event.modifiers.push(ControlKey::Shift.into()); - } - if ctrl - && !crate::is_control_key(&key_event, &ControlKey::Control) - && !crate::is_control_key(&key_event, &ControlKey::RControl) - { - key_event.modifiers.push(ControlKey::Control.into()); - } - if command - && !crate::is_control_key(&key_event, &ControlKey::Meta) - && !crate::is_control_key(&key_event, &ControlKey::RWin) - { - key_event.modifiers.push(ControlKey::Meta.into()); - } - if get_key_state(enigo::Key::CapsLock) { - key_event.modifiers.push(ControlKey::CapsLock.into()); - } - if self.peer_platform() != "Mac OS" { - if get_key_state(enigo::Key::NumLock) && common::valid_for_numlock(&key_event) { - key_event.modifiers.push(ControlKey::NumLock.into()); - } - } + self.legacy_modifiers(&mut key_event, alt, ctrl, shift, command); + if down_or_up == true { key_event.down = true; } @@ -2700,4 +2711,4 @@ impl Handler { async fn send_note(url: String, id: String, conn_id: i32, note: String) { let body = serde_json::json!({ "id": id, "Id": conn_id, "note": note }); allow_err!(crate::post_request(url, body.to_string(), "").await); -} \ No newline at end of file +} From 828795b4370937c328c3a15e464bced10685cda2 Mon Sep 17 00:00:00 2001 From: Asura Date: Mon, 18 Jul 2022 01:54:54 -0700 Subject: [PATCH 0093/2015] Sync Caps status --- src/server/input_service.rs | 48 +++++++++++++++++++++++++++++-------- src/ui/remote.rs | 7 +++++- 2 files changed, 44 insertions(+), 11 deletions(-) diff --git a/src/server/input_service.rs b/src/server/input_service.rs index f041b7e86..eac11a743 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -579,20 +579,46 @@ pub fn handle_key(evt: &KeyEvent) { handle_key_(evt); } -fn map_keyboard_map(evt: &KeyEvent) { - // map mode(1): Send keycode according to the peer platform. - let event_type = match evt.down { - true => EventType::KeyPress(RdevKey::Unknown(evt.get_chr())), - false => EventType::KeyRelease(RdevKey::Unknown(evt.get_chr())), - }; - - match simulate(&event_type) { +fn rdev_simulate(event_type: &EventType) { + let delay = std::time::Duration::from_millis(20); + match simulate(event_type) { Ok(()) => (), Err(_simulate_error) => { // todo log::error!("rdev could not send {:?}", event_type); } } + // Let ths OS catchup (at least MacOS) + std::thread::sleep(delay); +} + +fn map_keyboard_map(evt: &KeyEvent) { + // map mode(1): Send keycode according to the peer platform. + let mut en = ENIGO.lock().unwrap(); + // sync CAPS status + let caps_locking = evt + .modifiers + .iter() + .position(|&r| r == ControlKey::CapsLock.into()) + .is_some(); + println!("[*] remote, client: {:?} {:?}", caps_locking, en.get_key_state(enigo::Key::CapsLock)); + if caps_locking && !en.get_key_state(enigo::Key::CapsLock) + { + println!("[*]: Changing status"); + rdev_simulate(&EventType::KeyPress(RdevKey::CapsLock)); + rdev_simulate(&EventType::KeyRelease(RdevKey::CapsLock)); + }else if !caps_locking && en.get_key_state(enigo::Key::CapsLock){ + println!("[*]: Changing status"); + rdev_simulate(&EventType::KeyPress(RdevKey::CapsLock)); + rdev_simulate(&EventType::KeyRelease(RdevKey::CapsLock)); + }; + + let event_type = match evt.down { + true => EventType::KeyPress(RdevKey::Unknown(evt.get_chr())), + false => EventType::KeyRelease(RdevKey::Unknown(evt.get_chr())), + }; + + rdev_simulate(&event_type); return; } @@ -791,8 +817,10 @@ mod test { }); // set key/char base on char let mut evt = KeyEvent::new(); - evt.set_chr(49); - evt.mode = 3; + evt.set_chr(66); + evt.mode = 1; + + evt.modifiers.push(ControlKey::CapsLock.into()); // press evt.down = true; diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 2175cd06b..a230e50fb 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -984,6 +984,11 @@ impl Handler { } else { key_event.down = false; } + + if get_key_state(enigo::Key::CapsLock) { + key_event.modifiers.push(ControlKey::CapsLock.into()); + } + self.send_key_event(key_event, 1); } @@ -1236,7 +1241,7 @@ impl Handler { key_event.down = true; } dbg!(&key_event); - self.send_key_event(key_event, 2) + self.send_key_event(key_event, 3) } fn key_down_or_up(&mut self, down_or_up: bool, key: RdevKey, evt: Event) { From e0a7238cc1d5856cafcc0bc05c80e6ffc6cdcbd3 Mon Sep 17 00:00:00 2001 From: Asura Date: Mon, 18 Jul 2022 02:09:32 -0700 Subject: [PATCH 0094/2015] Refactor rdev simulate --- src/server/input_service.rs | 42 +++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/src/server/input_service.rs b/src/server/input_service.rs index eac11a743..2061c909d 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -579,21 +579,23 @@ pub fn handle_key(evt: &KeyEvent) { handle_key_(evt); } -fn rdev_simulate(event_type: &EventType) { +fn rdev_key_down_or_up(key: RdevKey, down_or_up: bool) { + let event_type = match down_or_up { + true => EventType::KeyPress(key), + false => EventType::KeyRelease(key), + }; let delay = std::time::Duration::from_millis(20); - match simulate(event_type) { + match simulate(&event_type) { Ok(()) => (), Err(_simulate_error) => { - // todo - log::error!("rdev could not send {:?}", event_type); + println!("We could not send {:?}", &event_type); } } // Let ths OS catchup (at least MacOS) std::thread::sleep(delay); } -fn map_keyboard_map(evt: &KeyEvent) { - // map mode(1): Send keycode according to the peer platform. +fn sync_status(evt: &KeyEvent) { let mut en = ENIGO.lock().unwrap(); // sync CAPS status let caps_locking = evt @@ -601,24 +603,24 @@ fn map_keyboard_map(evt: &KeyEvent) { .iter() .position(|&r| r == ControlKey::CapsLock.into()) .is_some(); - println!("[*] remote, client: {:?} {:?}", caps_locking, en.get_key_state(enigo::Key::CapsLock)); - if caps_locking && !en.get_key_state(enigo::Key::CapsLock) + println!( + "[*] remote, client: {:?} {:?}", + caps_locking, + en.get_key_state(enigo::Key::CapsLock) + ); + if (caps_locking && !en.get_key_state(enigo::Key::CapsLock)) + || (!caps_locking && en.get_key_state(enigo::Key::CapsLock)) { println!("[*]: Changing status"); - rdev_simulate(&EventType::KeyPress(RdevKey::CapsLock)); - rdev_simulate(&EventType::KeyRelease(RdevKey::CapsLock)); - }else if !caps_locking && en.get_key_state(enigo::Key::CapsLock){ - println!("[*]: Changing status"); - rdev_simulate(&EventType::KeyPress(RdevKey::CapsLock)); - rdev_simulate(&EventType::KeyRelease(RdevKey::CapsLock)); + rdev_key_down_or_up(RdevKey::CapsLock, true); + rdev_key_down_or_up(RdevKey::CapsLock, false); }; +} - let event_type = match evt.down { - true => EventType::KeyPress(RdevKey::Unknown(evt.get_chr())), - false => EventType::KeyRelease(RdevKey::Unknown(evt.get_chr())), - }; - - rdev_simulate(&event_type); +fn map_keyboard_map(evt: &KeyEvent) { + // map mode(1): Send keycode according to the peer platform. + sync_status(evt); + rdev_key_down_or_up(RdevKey::Unknown(evt.get_chr()), evt.down); return; } From 72273f454639ac07efaeb8a2c19cdff08389e42e Mon Sep 17 00:00:00 2001 From: Asura Date: Mon, 18 Jul 2022 02:19:33 -0700 Subject: [PATCH 0095/2015] Sync Numpad status --- src/server/input_service.rs | 22 +++++++++++++++------- src/ui/remote.rs | 12 +++++++++++- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 2061c909d..f54290d42 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -597,24 +597,32 @@ fn rdev_key_down_or_up(key: RdevKey, down_or_up: bool) { fn sync_status(evt: &KeyEvent) { let mut en = ENIGO.lock().unwrap(); - // sync CAPS status + + // remote caps status let caps_locking = evt .modifiers .iter() .position(|&r| r == ControlKey::CapsLock.into()) .is_some(); - println!( - "[*] remote, client: {:?} {:?}", - caps_locking, - en.get_key_state(enigo::Key::CapsLock) - ); + // remote numpad status + let num_locking = evt + .modifiers + .iter() + .position(|&r| r == ControlKey::NumLock.into()) + .is_some(); + if (caps_locking && !en.get_key_state(enigo::Key::CapsLock)) || (!caps_locking && en.get_key_state(enigo::Key::CapsLock)) { - println!("[*]: Changing status"); rdev_key_down_or_up(RdevKey::CapsLock, true); rdev_key_down_or_up(RdevKey::CapsLock, false); }; + if (num_locking && !en.get_key_state(enigo::Key::NumLock)) + || (!num_locking && en.get_key_state(enigo::Key::NumLock)) + { + rdev_key_down_or_up(RdevKey::NumLock, true); + rdev_key_down_or_up(RdevKey::NumLock, false); + }; } fn map_keyboard_map(evt: &KeyEvent) { diff --git a/src/ui/remote.rs b/src/ui/remote.rs index a230e50fb..c00d4579d 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -988,6 +988,9 @@ impl Handler { if get_key_state(enigo::Key::CapsLock) { key_event.modifiers.push(ControlKey::CapsLock.into()); } + if get_key_state(enigo::Key::NumLock) { + key_event.modifiers.push(ControlKey::NumLock.into()); + } self.send_key_event(key_event, 1); } @@ -996,7 +999,14 @@ impl Handler { // // translate mode(2): locally generated characters are send to the peer. // } - fn legacy_modifiers(&self, key_event: &mut KeyEvent, alt: bool, ctrl: bool, shift: bool, command: bool){ + fn legacy_modifiers( + &self, + key_event: &mut KeyEvent, + alt: bool, + ctrl: bool, + shift: bool, + command: bool, + ) { if alt && !crate::is_control_key(&key_event, &ControlKey::Alt) && !crate::is_control_key(&key_event, &ControlKey::RAlt) From 80b01a96dbe38cc8bf6d18f5bf11a7825ce6cd11 Mon Sep 17 00:00:00 2001 From: Asura Date: Mon, 18 Jul 2022 17:42:02 +0800 Subject: [PATCH 0096/2015] Refactor to remove warning --- src/server/input_service.rs | 2 +- src/ui/remote.rs | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/server/input_service.rs b/src/server/input_service.rs index f54290d42..81bfcd06f 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -3,7 +3,7 @@ use super::*; use dispatch::Queue; use enigo::{Enigo, Key, KeyboardControllable, MouseButton, MouseControllable}; use hbb_common::{config::COMPRESS_LEVEL, protobuf::ProtobufEnumOrUnknown}; -use rdev::{simulate, EventType, EventType::*, Key as RdevKey, SimulateError}; +use rdev::{simulate, EventType, Key as RdevKey}; use std::{ convert::TryFrom, sync::atomic::{AtomicBool, Ordering}, diff --git a/src/ui/remote.rs b/src/ui/remote.rs index c00d4579d..f6031a425 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -283,8 +283,6 @@ impl Handler { } log::info!("keyboard hooked"); let mut me = self.clone(); - let peer = self.peer_platform(); - let is_win = peer == "Windows"; #[cfg(windows)] crate::platform::windows::enable_lowlevel_keyboard(std::ptr::null_mut() as _); std::thread::spawn(move || { @@ -308,7 +306,7 @@ impl Handler { { return; } - let (key, down) = match evt.event_type { + let (_key, down) = match evt.event_type { KeyPress(k) => { // keyboard long press if MUTEX_SPECIAL_KEYS.lock().unwrap().contains_key(&k) { @@ -330,9 +328,9 @@ impl Handler { }; #[cfg(target_os = "windows")] - let key = rdev::get_win_key(evt.code.into(), evt.scan_code); + let _key = rdev::get_win_key(evt.code.into(), evt.scan_code); - me.key_down_or_up(down, key, evt); + me.key_down_or_up(down, _key, evt); }; if let Err(error) = rdev::listen(func) { log::error!("rdev: {:?}", error); From b1382c2d5709a5855ed535b03a5b32067075f215 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 15 Jul 2022 17:00:37 +0800 Subject: [PATCH 0097/2015] add: change id on flutter --- .../lib/desktop/pages/desktop_home_page.dart | 35 +++++++++++-------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 305155f0e..17aa597af 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -356,21 +356,28 @@ class _DesktopHomePageState extends State with TrayListener { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(translate("id_change_tip")), - Offstage( - offstage: msg.isEmpty, - child: Text(msg, style: TextStyle(color: Colors.grey),)).marginOnly(bottom: 4.0), - TextField( - onChanged: (s) { - newId = s; - }, - decoration: InputDecoration( - border: OutlineInputBorder() - ), - inputFormatters: [ - LengthLimitingTextInputFormatter(16), - // FilteringTextInputFormatter(RegExp(r"[a-zA-z][a-zA-z0-9\_]*"), allow: true) + SizedBox(height: 8.0,), + Row( + children: [ + Text("ID:").marginOnly(bottom: 16.0), + SizedBox(width: 24.0,), + Expanded( + child: TextField( + onChanged: (s) { + newId = s; + }, + decoration: InputDecoration( + border: OutlineInputBorder(), + errorText: msg.isEmpty ? null : translate(msg) + ), + inputFormatters: [ + LengthLimitingTextInputFormatter(16), + // FilteringTextInputFormatter(RegExp(r"[a-zA-z][a-zA-z0-9\_]*"), allow: true) + ], + maxLength: 16, + ), + ), ], - maxLength: 16, ), SizedBox(height: 4.0,), Offstage( From 08043732a88bc1af4469b7b67fe3fd00ad9fa938 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 18 Jul 2022 18:20:00 +0800 Subject: [PATCH 0098/2015] feat: ip whitelist, id/relay server/ socks5 proxy, about page --- .../lib/desktop/pages/desktop_home_page.dart | 803 +++++++++++++++--- src/flutter_ffi.rs | 40 +- src/ui.rs | 27 +- src/ui_interface.rs | 31 +- 4 files changed, 769 insertions(+), 132 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 17aa597af..2152a60c3 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'dart:io'; import 'package:flutter/material.dart' hide MenuItem; @@ -9,6 +10,7 @@ import 'package:flutter_hbb/models/model.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:tray_manager/tray_manager.dart'; +import 'package:url_launcher/url_launcher_string.dart'; class DesktopHomePage extends StatefulWidget { DesktopHomePage({Key? key}) : super(key: key); @@ -105,33 +107,78 @@ class _DesktopHomePageState extends State with TrayListener { children: [ Text( translate("ID"), - style: TextStyle(fontSize: 18, fontWeight: FontWeight.w500), + style: TextStyle( + fontSize: 18, fontWeight: FontWeight.w500), ), PopupMenuButton( padding: EdgeInsets.all(4.0), - itemBuilder: (context) => [ - genEnablePopupMenuItem(translate("Enable Keyboard/Mouse"), 'enable-keyboard',), - genEnablePopupMenuItem(translate("Enable Clipboard"), 'enable-clipboard',), - genEnablePopupMenuItem(translate("Enable File Transfer"), 'enable-file-transfer',), - genEnablePopupMenuItem(translate("Enable TCP Tunneling"), 'enable-tunnel',), - genAudioInputPopupMenuItem(), - // TODO: Audio Input - PopupMenuItem(child: Text(translate("ID/Relay Server")), value: 'custom-server',), - PopupMenuItem(child: Text(translate("IP Whitelisting")), value: 'whitelist',), - PopupMenuItem(child: Text(translate("Socks5 Proxy")), value: 'Socks5 Proxy',), - // sep - genEnablePopupMenuItem(translate("Enable Service"), 'stop-service',), - // TODO: direct server - genEnablePopupMenuItem(translate("Always connected via relay"),'allow-always-relay',), - genEnablePopupMenuItem(translate("Start ID/relay service"),'stop-rendezvous-service',), - PopupMenuItem(child: Text(translate("Change ID")), value: 'change-id',), - genEnablePopupMenuItem(translate("Dark Theme"), 'allow-darktheme',), - PopupMenuItem(child: Text(translate("About")), value: 'about',), - ], onSelected: onSelectMenu,) + itemBuilder: (context) => [ + genEnablePopupMenuItem( + translate("Enable Keyboard/Mouse"), + 'enable-keyboard', + ), + genEnablePopupMenuItem( + translate("Enable Clipboard"), + 'enable-clipboard', + ), + genEnablePopupMenuItem( + translate("Enable File Transfer"), + 'enable-file-transfer', + ), + genEnablePopupMenuItem( + translate("Enable TCP Tunneling"), + 'enable-tunnel', + ), + genAudioInputPopupMenuItem(), + // TODO: Audio Input + PopupMenuItem( + child: Text(translate("ID/Relay Server")), + value: 'custom-server', + ), + PopupMenuItem( + child: Text(translate("IP Whitelisting")), + value: 'whitelist', + ), + PopupMenuItem( + child: Text(translate("Socks5 Proxy")), + value: 'socks5-proxy', + ), + // sep + genEnablePopupMenuItem( + translate("Enable Service"), + 'stop-service', + ), + // TODO: direct server + genEnablePopupMenuItem( + translate("Always connected via relay"), + 'allow-always-relay', + ), + genEnablePopupMenuItem( + translate("Start ID/relay service"), + 'stop-rendezvous-service', + ), + PopupMenuItem( + child: Text(translate("Change ID")), + value: 'change-id', + ), + genEnablePopupMenuItem( + translate("Dark Theme"), + 'allow-darktheme', + ), + PopupMenuItem( + child: Text(translate("About")), + value: 'about', + ), + ], + onSelected: onSelectMenu, + ) ], ), TextFormField( controller: model.serverId, + decoration: InputDecoration( + enabled: false, + ), ), ], ), @@ -268,80 +315,111 @@ class _DesktopHomePageState extends State with TrayListener { gFFI.setOption(value, option == "Y" ? "" : "Y"); } else if (value == "change-id") { changeId(); + } else if (value == "custom-server") { + changeServer(); + } else if (value == "whitelist") { + changeWhiteList(); + } else if (value == "socks5-proxy") { + changeSocks5Proxy(); + } else if (value == "about") { + about(); } } PopupMenuItem genEnablePopupMenuItem(String label, String value) { - final isEnable = - label.startsWith('enable-') ? gFFI.getOption(value) != "N" : gFFI.getOption(value) != "Y"; - return PopupMenuItem(child: Row( - children: [ - Offstage(offstage: !isEnable, child: Icon(Icons.check)), - Text(label, style: genTextStyle(isEnable),), - ], - ), value: value,); + final isEnable = label.startsWith('enable-') + ? gFFI.getOption(value) != "N" + : gFFI.getOption(value) != "Y"; + return PopupMenuItem( + child: Row( + children: [ + Offstage(offstage: !isEnable, child: Icon(Icons.check)), + Text( + label, + style: genTextStyle(isEnable), + ), + ], + ), + value: value, + ); } TextStyle genTextStyle(bool isPositive) { - return isPositive ? TextStyle() : TextStyle( - color: Colors.redAccent, - decoration: TextDecoration.lineThrough - ); + return isPositive + ? TextStyle() + : TextStyle( + color: Colors.redAccent, decoration: TextDecoration.lineThrough); } PopupMenuItem genAudioInputPopupMenuItem() { final _enabledInput = gFFI.getOption('enable-audio'); var defaultInput = gFFI.getDefaultAudioInput().obs; var enabled = (_enabledInput != "N").obs; - return PopupMenuItem(child: FutureBuilder>( - future: gFFI.getAudioInputs(), - builder: (context, snapshot) { - if (snapshot.hasData) { - final inputs = snapshot.data!; - if (Platform.isWindows) { - inputs.insert(0, translate("System Sound")); - } - var inputList = inputs.map((e) => PopupMenuItem( - child: Row( - children: [ - Obx(()=> Offstage(offstage: defaultInput.value != e, child: Icon(Icons.check))), - Expanded(child: Tooltip( - message: e, - child: Text("$e",maxLines: 1, overflow: TextOverflow.ellipsis,))), - ], - ), - value: e, - )).toList(); - inputList.insert(0, PopupMenuItem( - child: Row( - children: [ - Obx(()=> Offstage(offstage: enabled.value, child: Icon(Icons.check))), - Expanded(child: Text(translate("Mute"))), - ], - ), - value: "Mute", - )); - return PopupMenuButton( + return PopupMenuItem( + child: FutureBuilder>( + future: gFFI.getAudioInputs(), + builder: (context, snapshot) { + if (snapshot.hasData) { + final inputs = snapshot.data!; + if (Platform.isWindows) { + inputs.insert(0, translate("System Sound")); + } + var inputList = inputs + .map((e) => PopupMenuItem( + child: Row( + children: [ + Obx(() => Offstage( + offstage: defaultInput.value != e, + child: Icon(Icons.check))), + Expanded( + child: Tooltip( + message: e, + child: Text( + "$e", + maxLines: 1, + overflow: TextOverflow.ellipsis, + ))), + ], + ), + value: e, + )) + .toList(); + inputList.insert( + 0, + PopupMenuItem( + child: Row( + children: [ + Obx(() => Offstage( + offstage: enabled.value, child: Icon(Icons.check))), + Expanded(child: Text(translate("Mute"))), + ], + ), + value: "Mute", + )); + return PopupMenuButton( padding: EdgeInsets.zero, child: Container( alignment: Alignment.centerLeft, child: Text(translate("Audio Input"))), itemBuilder: (context) => inputList, onSelected: (dev) { - if (dev == "Mute") { - gFFI.setOption('enable-audio', _enabledInput == 'N' ? '': 'N'); - enabled.value = gFFI.getOption('enable-audio') != 'N'; - } else if (dev != gFFI.getDefaultAudioInput()) { - gFFI.setDefaultAudioInput(dev); - defaultInput.value = dev; - } + if (dev == "Mute") { + gFFI.setOption( + 'enable-audio', _enabledInput == 'N' ? '' : 'N'); + enabled.value = gFFI.getOption('enable-audio') != 'N'; + } else if (dev != gFFI.getDefaultAudioInput()) { + gFFI.setDefaultAudioInput(dev); + defaultInput.value = dev; + } }, - ); - } else { - return Text("..."); - } - }, - ), value: 'audio-input',); + ); + } else { + return Text("..."); + } + }, + ), + value: 'audio-input', + ); } /// change local ID @@ -349,27 +427,30 @@ class _DesktopHomePageState extends State with TrayListener { var newId = ""; var msg = ""; var isInProgress = false; - DialogManager.show( (setState, close) { + DialogManager.show((setState, close) { return CustomAlertDialog( title: Text(translate("Change ID")), content: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(translate("id_change_tip")), - SizedBox(height: 8.0,), + SizedBox( + height: 8.0, + ), Row( children: [ Text("ID:").marginOnly(bottom: 16.0), - SizedBox(width: 24.0,), + SizedBox( + width: 24.0, + ), Expanded( child: TextField( onChanged: (s) { newId = s; }, decoration: InputDecoration( - border: OutlineInputBorder(), - errorText: msg.isEmpty ? null : translate(msg) - ), + border: OutlineInputBorder(), + errorText: msg.isEmpty ? null : translate(msg)), inputFormatters: [ LengthLimitingTextInputFormatter(16), // FilteringTextInputFormatter(RegExp(r"[a-zA-z][a-zA-z0-9\_]*"), allow: true) @@ -379,34 +460,546 @@ class _DesktopHomePageState extends State with TrayListener { ), ], ), - SizedBox(height: 4.0,), - Offstage( - offstage: !isInProgress, - child: LinearProgressIndicator()) + SizedBox( + height: 4.0, + ), + Offstage(offstage: !isInProgress, child: LinearProgressIndicator()) ], - ), actions: [ - TextButton(onPressed: (){ - close(); - }, child: Text("取消")), - TextButton(onPressed: () async { - setState(() { - msg = ""; - isInProgress = true; - gFFI.bind.mainChangeId(newId: newId); - }); + ), + actions: [ + TextButton( + onPressed: () { + close(); + }, + child: Text(translate("Cancel"))), + TextButton( + onPressed: () async { + setState(() { + msg = ""; + isInProgress = true; + gFFI.bind.mainChangeId(newId: newId); + }); - var status = await gFFI.bind.mainGetAsyncStatus(); - while (status == " "){ - await Future.delayed(Duration(milliseconds: 100)); - status = await gFFI.bind.mainGetAsyncStatus(); - } - setState(() { - isInProgress = false; - msg = translate(status); - }); + var status = await gFFI.bind.mainGetAsyncStatus(); + while (status == " ") { + await Future.delayed(Duration(milliseconds: 100)); + status = await gFFI.bind.mainGetAsyncStatus(); + } + if (status.isEmpty) { + // ok + close(); + return; + } + setState(() { + isInProgress = false; + msg = translate(status); + }); + }, + child: Text(translate("OK"))), + ], + ); + }); + } - }, child: Text("确定")), - ], + void changeServer() async { + Map oldOptions = + jsonDecode(await gFFI.bind.mainGetOptions()); + print("${oldOptions}"); + String idServer = oldOptions['custom-rendezvous-server'] ?? ""; + var idServerMsg = ""; + String relayServer = oldOptions['relay-server'] ?? ""; + var relayServerMsg = ""; + String apiServer = oldOptions['api-server'] ?? ""; + var apiServerMsg = ""; + var key = oldOptions['key'] ?? ""; + + var isInProgress = false; + DialogManager.show((setState, close) { + return CustomAlertDialog( + title: Text(translate("ID/Relay Server")), + content: ConstrainedBox( + constraints: BoxConstraints(minWidth: 500), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: 8.0, + ), + Row( + children: [ + ConstrainedBox( + constraints: BoxConstraints(minWidth: 100), + child: Text("${translate('ID Server')}:") + .marginOnly(bottom: 16.0)), + SizedBox( + width: 24.0, + ), + Expanded( + child: TextField( + onChanged: (s) { + idServer = s; + }, + decoration: InputDecoration( + border: OutlineInputBorder(), + errorText: + idServerMsg.isNotEmpty ? idServerMsg : null), + controller: TextEditingController(text: idServer), + ), + ), + ], + ), + SizedBox( + height: 8.0, + ), + Row( + children: [ + ConstrainedBox( + constraints: BoxConstraints(minWidth: 100), + child: Text("${translate('Relay Server')}:") + .marginOnly(bottom: 16.0)), + SizedBox( + width: 24.0, + ), + Expanded( + child: TextField( + onChanged: (s) { + relayServer = s; + }, + decoration: InputDecoration( + border: OutlineInputBorder(), + errorText: relayServerMsg.isNotEmpty + ? relayServerMsg + : null), + controller: TextEditingController(text: relayServer), + ), + ), + ], + ), + SizedBox( + height: 8.0, + ), + Row( + children: [ + ConstrainedBox( + constraints: BoxConstraints(minWidth: 100), + child: Text("${translate('API Server')}:") + .marginOnly(bottom: 16.0)), + SizedBox( + width: 24.0, + ), + Expanded( + child: TextField( + onChanged: (s) { + apiServer = s; + }, + decoration: InputDecoration( + border: OutlineInputBorder(), + errorText: + apiServerMsg.isNotEmpty ? apiServerMsg : null), + controller: TextEditingController(text: apiServer), + ), + ), + ], + ), + SizedBox( + height: 8.0, + ), + Row( + children: [ + ConstrainedBox( + constraints: BoxConstraints(minWidth: 100), + child: Text("${translate('Key')}:") + .marginOnly(bottom: 16.0)), + SizedBox( + width: 24.0, + ), + Expanded( + child: TextField( + onChanged: (s) { + key = s; + }, + decoration: InputDecoration( + border: OutlineInputBorder(), + ), + controller: TextEditingController(text: key), + ), + ), + ], + ), + SizedBox( + height: 4.0, + ), + Offstage( + offstage: !isInProgress, child: LinearProgressIndicator()) + ], + ), + ), + actions: [ + TextButton( + onPressed: () { + close(); + }, + child: Text(translate("Cancel"))), + TextButton( + onPressed: () async { + setState(() { + [idServerMsg, relayServerMsg, apiServerMsg] + .forEach((element) { + element = ""; + }); + isInProgress = true; + }); + final cancel = () { + setState(() { + isInProgress = false; + }); + }; + idServer = idServer.trim(); + relayServer = relayServer.trim(); + apiServer = apiServer.trim(); + key = key.trim(); + + if (idServer.isNotEmpty) { + idServerMsg = translate( + await gFFI.bind.mainTestIfValidServer(server: idServer)); + if (idServerMsg.isEmpty) { + oldOptions['custom-rendezvous-server'] = idServer; + } else { + cancel(); + return; + } + } else { + oldOptions['custom-rendezvous-server'] = ""; + } + + if (relayServer.isNotEmpty) { + relayServerMsg = translate(await gFFI.bind + .mainTestIfValidServer(server: relayServer)); + if (relayServerMsg.isEmpty) { + oldOptions['relay-server'] = relayServer; + } else { + cancel(); + return; + } + } else { + oldOptions['relay-server'] = ""; + } + + if (apiServer.isNotEmpty) { + if (apiServer.startsWith('http://') || + apiServer.startsWith("https://")) { + oldOptions['api-server'] = apiServer; + return; + } else { + apiServerMsg = translate("invalid_http"); + cancel(); + return; + } + } else { + oldOptions['api-server'] = ""; + } + // ok + oldOptions['key'] = key; + await gFFI.bind.mainSetOptions(json: jsonEncode(oldOptions)); + close(); + }, + child: Text(translate("OK"))), + ], + ); + }); + } + + void changeWhiteList() async { + Map oldOptions = + jsonDecode(await gFFI.bind.mainGetOptions()); + var newWhiteList = ((oldOptions['whitelist'] ?? "") as String).split(','); + var newWhiteListField = newWhiteList.join('\n'); + var msg = ""; + var isInProgress = false; + DialogManager.show((setState, close) { + return CustomAlertDialog( + title: Text(translate("IP Whitelisting")), + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(translate("whitelist_sep")), + SizedBox( + height: 8.0, + ), + Row( + children: [ + Expanded( + child: TextField( + onChanged: (s) { + newWhiteListField = s; + }, + maxLines: null, + decoration: InputDecoration( + border: OutlineInputBorder(), + errorText: msg.isEmpty ? null : translate(msg), + ), + controller: TextEditingController(text: newWhiteListField), + ), + ), + ], + ), + SizedBox( + height: 4.0, + ), + Offstage(offstage: !isInProgress, child: LinearProgressIndicator()) + ], + ), + actions: [ + TextButton( + onPressed: () { + close(); + }, + child: Text(translate("Cancel"))), + TextButton( + onPressed: () async { + setState(() { + msg = ""; + isInProgress = true; + }); + newWhiteListField = newWhiteListField.trim(); + var newWhiteList = ""; + if (newWhiteListField.isEmpty) { + // pass + } else { + final ips = + newWhiteListField.trim().split(RegExp(r"[\s,;\n]+")); + // test ip + final ipMatch = RegExp(r"^\d+\.\d+\.\d+\.\d+$"); + for (final ip in ips) { + if (!ipMatch.hasMatch(ip)) { + msg = translate("Invalid IP") + " $ip"; + setState(() { + isInProgress = false; + }); + return; + } + } + newWhiteList = ips.join(','); + } + oldOptions['whitelist'] = newWhiteList; + await gFFI.bind.mainSetOptions(json: jsonEncode(oldOptions)); + close(); + }, + child: Text(translate("OK"))), + ], + ); + }); + } + + void changeSocks5Proxy() async { + var socks = await gFFI.bind.mainGetSocks(); + + String proxy = ""; + String proxyMsg = ""; + String username = ""; + String password = ""; + if (socks.length == 3) { + proxy = socks[0]; + username = socks[1]; + password = socks[2]; + } + + var isInProgress = false; + DialogManager.show((setState, close) { + return CustomAlertDialog( + title: Text(translate("Socks5 Proxy")), + content: ConstrainedBox( + constraints: BoxConstraints(minWidth: 500), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: 8.0, + ), + Row( + children: [ + ConstrainedBox( + constraints: BoxConstraints(minWidth: 100), + child: Text("${translate('Hostname')}:") + .marginOnly(bottom: 16.0)), + SizedBox( + width: 24.0, + ), + Expanded( + child: TextField( + onChanged: (s) { + proxy = s; + }, + decoration: InputDecoration( + border: OutlineInputBorder(), + errorText: + proxyMsg.isNotEmpty ? proxyMsg : null), + controller: TextEditingController(text: proxy), + ), + ), + ], + ), + SizedBox( + height: 8.0, + ), + Row( + children: [ + ConstrainedBox( + constraints: BoxConstraints(minWidth: 100), + child: Text("${translate('Username')}:") + .marginOnly(bottom: 16.0)), + SizedBox( + width: 24.0, + ), + Expanded( + child: TextField( + onChanged: (s) { + username = s; + }, + decoration: InputDecoration( + border: OutlineInputBorder(), + ), + controller: TextEditingController(text: username), + ), + ), + ], + ), + SizedBox( + height: 8.0, + ), + Row( + children: [ + ConstrainedBox( + constraints: BoxConstraints(minWidth: 100), + child: Text("${translate('Password')}:") + .marginOnly(bottom: 16.0)), + SizedBox( + width: 24.0, + ), + Expanded( + child: TextField( + onChanged: (s) { + password = s; + }, + decoration: InputDecoration( + border: OutlineInputBorder(), + ), + controller: TextEditingController(text: password), + ), + ), + ], + ), + SizedBox( + height: 8.0, + ), + Offstage( + offstage: !isInProgress, child: LinearProgressIndicator()) + ], + ), + ), + actions: [ + TextButton( + onPressed: () { + close(); + }, + child: Text(translate("Cancel"))), + TextButton( + onPressed: () async { + setState(() { + proxyMsg = ""; + isInProgress = true; + }); + final cancel = () { + setState(() { + isInProgress = false; + }); + }; + proxy = proxy.trim(); + username = username.trim(); + password = password.trim(); + + if (proxy.isNotEmpty) { + proxyMsg = translate( + await gFFI.bind.mainTestIfValidServer(server: proxy)); + if (proxyMsg.isEmpty) { + // ignore + } else { + cancel(); + return; + } + } + await gFFI.bind.mainSetSocks(proxy: proxy, username: username, password: password); + close(); + }, + child: Text(translate("OK"))), + ], + ); + }); + } + + void about() async { + final appName = await gFFI.bind.mainGetAppName(); + final license = await gFFI.bind.mainGetLicense(); + final version = await gFFI.bind.mainGetVersion(); + final linkStyle = TextStyle( + decoration: TextDecoration.underline + ); + DialogManager.show((setState, close) { + return CustomAlertDialog( + title: Text("About $appName"), + content: ConstrainedBox( + constraints: BoxConstraints(minWidth: 500), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: 8.0, + ), + Text("Version: $version").marginSymmetric(vertical: 4.0), + InkWell( + onTap: () { + launchUrlString("https://rustdesk.com/privacy"); + }, + child: Text("Privacy Statement", style: linkStyle,).marginSymmetric(vertical: 4.0)), + InkWell( + onTap: () { + launchUrlString("https://rustdesk.com"); + } + ,child: Text("Website",style: linkStyle,).marginSymmetric(vertical: 4.0)), + Container( + decoration: BoxDecoration( + color: Color(0xFF2c8cff) + ), + padding: EdgeInsets.symmetric(vertical: 24, horizontal: 8), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Copyright © 2022 Purslane Ltd.\n$license", style: TextStyle( + color: Colors.white + ),), + Text("Made with heart in this chaotic world!", style: TextStyle( + fontWeight: FontWeight.w800, + color: Colors.white + ),) + ], + ), + ), + ], + ), + ).marginSymmetric(vertical: 4.0) + ], + ), + ), + actions: [ + TextButton( + onPressed: () async { + close(); + }, + child: Text(translate("OK"))), + ], ); }); } diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 60d8fd4b5..432ba3969 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -19,7 +19,10 @@ use crate::flutter::connection_manager::{self, get_clients_length, get_clients_s use crate::flutter::{self, Session, SESSIONS}; use crate::start_server; use crate::ui_interface; -use crate::ui_interface::{change_id, get_async_job_status, get_sound_inputs, is_ok_change_id}; +use crate::ui_interface::{ + change_id, get_app_name, get_async_job_status, get_license, get_options, get_socks, + get_sound_inputs, get_version, is_ok_change_id, set_options, set_socks, test_if_valid_server, +}; fn initialize(app_dir: &str) { *config::APP_DIR.write().unwrap() = app_dir.to_owned(); @@ -391,6 +394,41 @@ pub fn main_get_async_status() -> String { get_async_job_status() } +pub fn main_get_options() -> String { + get_options() +} + +pub fn main_set_options(json: String) { + let map: HashMap = serde_json::from_str(&json).unwrap_or(HashMap::new()); + if !map.is_empty() { + set_options(map) + } +} + +pub fn main_test_if_valid_server(server: String) -> String { + test_if_valid_server(server) +} + +pub fn main_set_socks(proxy: String, username: String, password: String) { + set_socks(proxy, username, password) +} + +pub fn main_get_socks() -> Vec { + get_socks() +} + +pub fn main_get_app_name() -> String { + get_app_name() +} + +pub fn main_get_license() -> String { + get_license() +} + +pub fn main_get_version() -> String { + get_version() +} + /// FFI for **get** commands which are idempotent. /// Return result in c string. /// diff --git a/src/ui.rs b/src/ui.rs index 7a6fd0219..713b57122 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,20 +1,23 @@ -mod cm; -#[cfg(feature = "inline")] -mod inline; -#[cfg(target_os = "macos")] -mod macos; -#[cfg(target_os = "windows")] -pub mod win_privacy; -pub mod remote; -use crate::ui_interface::*; -use hbb_common::{allow_err, config::PeerConfig, log}; -use sciter::Value; use std::{ collections::HashMap, iter::FromIterator, sync::{Arc, Mutex}, }; +use sciter::Value; + +use hbb_common::{allow_err, config::PeerConfig, log}; + +use crate::ui_interface::*; + +mod cm; +#[cfg(feature = "inline")] +mod inline; +#[cfg(target_os = "macos")] +mod macos; +pub mod remote; +#[cfg(target_os = "windows")] +pub mod win_privacy; lazy_static::lazy_static! { // stupid workaround for https://sciter.com/forums/topic/crash-on-latest-tis-mac-sdk-sometimes/ static ref STUPID_VALUES: Mutex>>> = Default::default(); @@ -227,7 +230,7 @@ impl UI { } fn get_options(&self) -> Value { - let hashmap = get_options(); + let hashmap: HashMap = serde_json::from_str(&get_options()).unwrap(); let mut m = Value::map(); for (k, v) in hashmap { m.set_item(k, v); diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 90e39636d..7eaf938d1 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -1,5 +1,10 @@ -use crate::common::SOFTWARE_UPDATE_URL; -use crate::ipc; +use std::{ + collections::HashMap, + process::Child, + sync::{Arc, Mutex}, + time::SystemTime, +}; + use hbb_common::{ allow_err, config::{self, Config, LocalConfig, PeerConfig, RENDEZVOUS_PORT, RENDEZVOUS_TIMEOUT}, @@ -11,12 +16,9 @@ use hbb_common::{ tcp::FramedStream, tokio::{self, sync::mpsc, time}, }; -use std::{ - collections::HashMap, - process::Child, - sync::{Arc, Mutex}, - time::SystemTime, -}; + +use crate::common::SOFTWARE_UPDATE_URL; +use crate::ipc; type Message = RendezvousMessage; @@ -72,7 +74,9 @@ pub fn goto_install() { pub fn install_me(_options: String, _path: String, silent: bool, debug: bool) { #[cfg(windows)] std::thread::spawn(move || { - allow_err!(crate::platform::windows::install_me(&_options, _path, silent, debug)); + allow_err!(crate::platform::windows::install_me( + &_options, _path, silent, debug + )); std::process::exit(0); }); } @@ -185,14 +189,13 @@ pub fn using_public_server() -> bool { crate::get_custom_rendezvous_server(get_option_("custom-rendezvous-server")).is_empty() } -pub fn get_options() -> HashMap { - // TODO Vec<(String,String)> +pub fn get_options() -> String { let options = OPTIONS.lock().unwrap(); - let mut m = HashMap::new(); + let mut m = serde_json::Map::new(); for (k, v) in options.iter() { - m.insert(k.into(), v.into()); + m.insert(k.into(), v.to_owned().into()); } - m + serde_json::to_string(&m).unwrap() } pub fn test_if_valid_server(host: String) -> String { From a2d8c31e856a54eb6ba551d3adefdbbe5135512f Mon Sep 17 00:00:00 2001 From: Asura Date: Mon, 18 Jul 2022 22:01:08 +0800 Subject: [PATCH 0099/2015] Auto release key --- src/ui/remote.rs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/ui/remote.rs b/src/ui/remote.rs index f6031a425..44e7d705f 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -1,5 +1,5 @@ use std::{ - collections::HashMap, + collections::{HashMap, HashSet}, ops::Deref, sync::{ atomic::{AtomicBool, AtomicUsize, Ordering}, @@ -57,6 +57,7 @@ type Video = AssetPtr; lazy_static::lazy_static! { static ref ENIGO: Arc> = Arc::new(Mutex::new(Enigo::new())); static ref VIDEO: Arc>> = Default::default(); + static ref TO_RELEASE: Arc>> = Arc::new(Mutex::new(HashSet::::new())); } fn get_key_state(key: enigo::Key) -> bool { @@ -732,6 +733,9 @@ impl Handler { } fn leave(&mut self) { + for key in TO_RELEASE.lock().unwrap().iter() { + self.map_keyboard_mode(false, *key) + } #[cfg(windows)] crate::platform::windows::stop_system_key_propagate(false); IS_IN.store(false, Ordering::SeqCst); @@ -986,8 +990,10 @@ impl Handler { if get_key_state(enigo::Key::CapsLock) { key_event.modifiers.push(ControlKey::CapsLock.into()); } - if get_key_state(enigo::Key::NumLock) { - key_event.modifiers.push(ControlKey::NumLock.into()); + if self.peer_platform() != "Mac OS" { + if get_key_state(enigo::Key::NumLock) && common::valid_for_numlock(&key_event) { + key_event.modifiers.push(ControlKey::NumLock.into()); + } } self.send_key_event(key_event, 1); @@ -1257,6 +1263,11 @@ impl Handler { let mode = std::env::var("KEYBOARD_MOAD").unwrap_or(String::from("map")); match mode.as_str() { "map" => { + if down_or_up == true { + TO_RELEASE.lock().unwrap().insert(key); + } else { + TO_RELEASE.lock().unwrap().remove(&key); + } self.map_keyboard_mode(down_or_up, key); } "legacy" => self.legacy_keyboard_mode(down_or_up, key, evt), From a8e4591217c478b62c2b93df4795b5edd4e2c596 Mon Sep 17 00:00:00 2001 From: Asura Date: Mon, 18 Jul 2022 07:38:05 -0700 Subject: [PATCH 0100/2015] Fix lock_screen and ctrl_alt_del --- libs/enigo/Cargo.toml | 2 +- libs/enigo/src/linux.rs | 11 ++--------- src/server/input_service.rs | 2 ++ 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/libs/enigo/Cargo.toml b/libs/enigo/Cargo.toml index b1c57ca92..e97f000a6 100644 --- a/libs/enigo/Cargo.toml +++ b/libs/enigo/Cargo.toml @@ -21,7 +21,7 @@ appveyor = { repository = "pythoneer/enigo-85xiy" } [dependencies] serde = { version = "1.0", optional = true } serde_derive = { version = "1.0", optional = true } -log = "0.4.17" +log = "0.4" rdev = { git = "https://github.com/asur4s/rdev" } [features] diff --git a/libs/enigo/src/linux.rs b/libs/enigo/src/linux.rs index 6b4c99a1b..30c49c014 100644 --- a/libs/enigo/src/linux.rs +++ b/libs/enigo/src/linux.rs @@ -103,7 +103,6 @@ impl Enigo { self.tx.send((PyMsg::Char('\0'), true)).ok(); } - #[inline] fn send_pynput(&mut self, key: &Key, is_press: bool) -> bool { if unsafe { PYNPUT_EXIT || !PYNPUT_REDAY } { @@ -112,14 +111,8 @@ impl Enigo { if let Key::Layout(c) = key { return self.tx.send((PyMsg::Char(*c), is_press)).is_ok(); } - if let Key::Raw(chr) = key { - fn string_to_static_str(s: String) -> &'static str { - Box::leak(s.into_boxed_str()) - } - return self - .tx - .send((PyMsg::Str(string_to_static_str(chr.to_string())), is_press)) - .is_ok(); + if let Key::Raw(_) = key { + return false; } #[allow(deprecated)] let s = match key { diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 81bfcd06f..aadb957a3 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -450,6 +450,7 @@ pub fn lock_screen() { key_event.down = true; key_event.set_chr('l' as _); key_event.modifiers.push(ControlKey::Meta.into()); + key_event.mode = 3; handle_key(&key_event); key_event.down = false; handle_key(&key_event); @@ -462,6 +463,7 @@ pub fn lock_screen() { key_event.set_chr('q' as _); key_event.modifiers.push(ControlKey::Meta.into()); key_event.modifiers.push(ControlKey::Control.into()); + key_event.mode = 3; handle_key(&key_event); key_event.down = false; handle_key(&key_event); From a118056c304c6919ff9aaa10769bd4d78809b7e9 Mon Sep 17 00:00:00 2001 From: Asura Date: Mon, 18 Jul 2022 23:04:04 +0800 Subject: [PATCH 0101/2015] Fix sync Numpad status --- src/server/input_service.rs | 11 +++++++++-- src/ui/remote.rs | 6 ++---- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/server/input_service.rs b/src/server/input_service.rs index aadb957a3..a116bbbfc 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -810,9 +810,8 @@ async fn send_sas() -> ResultType<()> { #[cfg(test)] mod test { use super::*; - use rdev::{listen, simulate, Event, EventType, Key}; + use rdev::{listen, Event, EventType, Key}; use std::sync::mpsc; - use std::thread; #[test] fn test_handle_key() { @@ -847,4 +846,12 @@ mod test { assert_eq!(listen_evt.event_type, EventType::KeyRelease(Key::Num1)) } } + #[test] + fn test_get_key_state() { + let mut en = ENIGO.lock().unwrap(); + println!( + "[*] test_get_key_state: {:?}", + en.get_key_state(enigo::Key::NumLock) + ); + } } diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 44e7d705f..a0aa5d381 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -990,10 +990,8 @@ impl Handler { if get_key_state(enigo::Key::CapsLock) { key_event.modifiers.push(ControlKey::CapsLock.into()); } - if self.peer_platform() != "Mac OS" { - if get_key_state(enigo::Key::NumLock) && common::valid_for_numlock(&key_event) { - key_event.modifiers.push(ControlKey::NumLock.into()); - } + if get_key_state(enigo::Key::NumLock) { + key_event.modifiers.push(ControlKey::NumLock.into()); } self.send_key_event(key_event, 1); From 48466bfe37824536c4b0b55bed9580dd999c6eb9 Mon Sep 17 00:00:00 2001 From: Asura Date: Tue, 19 Jul 2022 09:35:39 +0800 Subject: [PATCH 0102/2015] Numpad when linux -> windows --- src/ui/remote.rs | 36 ++++++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/src/ui/remote.rs b/src/ui/remote.rs index a0aa5d381..7c05c4407 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -965,22 +965,42 @@ impl Handler { self.send(Data::Message(msg_out)); } + fn convert_numpad_keys(&mut self, key: &RdevKey) -> &RdevKey { + if get_key_state(enigo::Key::NumLock) { + return; + } + match key { + &RdevKey::Num0 => &RdevKey::Insert, + &RdevKey::KpDecimal => &RdevKey::Delete, + &RdevKey::Num1 => &RdevKey::End, + &RdevKey::Num2 => &RdevKey::DownArrow, + &RdevKey::Num3 => &RdevKey::PageDown, + &RdevKey::Num4 => &RdevKey::LeftArrow, + &RdevKey::Num5 => &RdevKey::Clear, + &RdevKey::Num6 => &RdevKey::RightArrow, + &RdevKey::Num7 => &RdevKey::Home, + &RdevKey::Num8 => &RdevKey::UpArrow, + &RdevKey::Num9 => &RdevKey::PageUp, + } + } + fn map_keyboard_mode(&mut self, down_or_up: bool, key: RdevKey) { // map mode(1): Send keycode according to the peer platform. let peer = self.peer_platform(); let mut key_event = KeyEvent::new(); // According to peer platform. - if peer == "Linux" { - let keycode: u32 = rdev::linux_keycode_from_key(key).unwrap_or_default().into(); - key_event.set_chr(keycode); + let keycode: u32 = if peer == "Linux" { + rdev::linux_keycode_from_key(key).unwrap_or_default().into() } else if peer == "Windows" { - let keycode: u32 = rdev::win_keycode_from_key(key).unwrap_or_default().into(); - key_event.set_chr(keycode); + #[cfg(not(windows))] + self.convert_numpad_keys(&key); + rdev::win_keycode_from_key(key).unwrap_or_default().into() } else if peer == "Mac OS" { - let keycode: u32 = rdev::macos_keycode_from_key(key).unwrap_or_default().into(); - key_event.set_chr(keycode); - } + rdev::macos_keycode_from_key(key).unwrap_or_default().into() + }; + key_event.set_chr(keycode); + if down_or_up == true { key_event.down = true; } else { From a77d64d18184bd7287475f7558121c6092c5e551 Mon Sep 17 00:00:00 2001 From: Asura Date: Mon, 18 Jul 2022 18:47:38 -0700 Subject: [PATCH 0103/2015] Remove log --- libs/enigo/src/linux.rs | 2 +- src/ui/remote.rs | 32 ++++++++++++++++---------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/libs/enigo/src/linux.rs b/libs/enigo/src/linux.rs index 30c49c014..de06923f3 100644 --- a/libs/enigo/src/linux.rs +++ b/libs/enigo/src/linux.rs @@ -3,7 +3,7 @@ use libc; use crate::{Key, KeyboardControllable, MouseButton, MouseControllable}; use self::libc::{c_char, c_int, c_void, useconds_t}; -use std::{borrow::Cow, ffi::CString, io::prelude::*, ptr, sync::mpsc, thread, time}; +use std::{borrow::Cow, ffi::CString, io::prelude::*, ptr, sync::mpsc}; const CURRENT_WINDOW: c_int = 0; const DEFAULT_DELAY: u64 = 12000; type Window = c_int; diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 7c05c4407..f051663f6 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -965,22 +965,23 @@ impl Handler { self.send(Data::Message(msg_out)); } - fn convert_numpad_keys(&mut self, key: &RdevKey) -> &RdevKey { + fn convert_numpad_keys(&mut self, key: RdevKey) -> RdevKey { if get_key_state(enigo::Key::NumLock) { - return; + return key; } match key { - &RdevKey::Num0 => &RdevKey::Insert, - &RdevKey::KpDecimal => &RdevKey::Delete, - &RdevKey::Num1 => &RdevKey::End, - &RdevKey::Num2 => &RdevKey::DownArrow, - &RdevKey::Num3 => &RdevKey::PageDown, - &RdevKey::Num4 => &RdevKey::LeftArrow, - &RdevKey::Num5 => &RdevKey::Clear, - &RdevKey::Num6 => &RdevKey::RightArrow, - &RdevKey::Num7 => &RdevKey::Home, - &RdevKey::Num8 => &RdevKey::UpArrow, - &RdevKey::Num9 => &RdevKey::PageUp, + RdevKey::Kp0 => RdevKey::Insert, + RdevKey::KpDecimal => RdevKey::Delete, + RdevKey::Kp1 => RdevKey::End, + RdevKey::Kp2 => RdevKey::DownArrow, + RdevKey::Kp3 => RdevKey::PageDown, + RdevKey::Kp4 => RdevKey::LeftArrow, + RdevKey::Kp5 => RdevKey::Clear, + RdevKey::Kp6 => RdevKey::RightArrow, + RdevKey::Kp7 => RdevKey::Home, + RdevKey::Kp8 => RdevKey::UpArrow, + RdevKey::Kp9 => RdevKey::PageUp, + _ => key, } } @@ -994,9 +995,9 @@ impl Handler { rdev::linux_keycode_from_key(key).unwrap_or_default().into() } else if peer == "Windows" { #[cfg(not(windows))] - self.convert_numpad_keys(&key); + let key = self.convert_numpad_keys(key); rdev::win_keycode_from_key(key).unwrap_or_default().into() - } else if peer == "Mac OS" { + } else { rdev::macos_keycode_from_key(key).unwrap_or_default().into() }; key_event.set_chr(keycode); @@ -1065,7 +1066,6 @@ impl Handler { fn legacy_keyboard_mode(&mut self, down_or_up: bool, key: RdevKey, evt: Event) { // legacy mode(3): Generate characters locally, look for keycode on other side. - println!("legacy_keyboard_mode {:?}", key); let peer = self.peer_platform(); let is_win = peer == "Windows"; From 19ebbb145a753e9090b387258dd4746627f4ff05 Mon Sep 17 00:00:00 2001 From: Asura Date: Mon, 18 Jul 2022 23:45:54 -0700 Subject: [PATCH 0104/2015] Compatible with legacy mode in remote --- src/server/connection.rs | 4 ++-- src/server/input_service.rs | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/server/connection.rs b/src/server/connection.rs index dd5b32fe9..854191c95 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -420,11 +420,11 @@ impl Connection { } MessageInput::Key((mut msg, press)) => { // todo: press and down have similar meanings. - if press && msg.mode == 3 { + if press && msg.mode == 0 { msg.down = true; } handle_key(&msg); - if press && msg.mode == 3 { + if press && msg.mode == 0 { msg.down = false; handle_key(&msg); } diff --git a/src/server/input_service.rs b/src/server/input_service.rs index a116bbbfc..4563ef465 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -450,7 +450,7 @@ pub fn lock_screen() { key_event.down = true; key_event.set_chr('l' as _); key_event.modifiers.push(ControlKey::Meta.into()); - key_event.mode = 3; + key_event.mode = 0; handle_key(&key_event); key_event.down = false; handle_key(&key_event); @@ -463,7 +463,7 @@ pub fn lock_screen() { key_event.set_chr('q' as _); key_event.modifiers.push(ControlKey::Meta.into()); key_event.modifiers.push(ControlKey::Control.into()); - key_event.mode = 3; + key_event.mode = 0; handle_key(&key_event); key_event.down = false; handle_key(&key_event); @@ -788,14 +788,14 @@ fn handle_key_(evt: &KeyEvent) { } match evt.mode { + 0 => { + legacy_keyboard_map(evt); + } 1 => { map_keyboard_map(evt); } - 3 => { - legacy_keyboard_map(evt); - } _ => { - map_keyboard_map(evt); + legacy_keyboard_map(evt); } } } From 0bacc1c250192ccd4ec424f04b97b1c6feb44c63 Mon Sep 17 00:00:00 2001 From: Asura Date: Tue, 19 Jul 2022 15:09:45 +0800 Subject: [PATCH 0105/2015] Compatible with legacy mode in client --- src/ui/remote.rs | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/src/ui/remote.rs b/src/ui/remote.rs index f051663f6..0b96b3c26 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -921,14 +921,14 @@ impl Handler { key_event.set_control_key(ControlKey::CtrlAltDel); // todo key_event.down = true; - self.send_key_event(key_event, 3); + self.send_key_event(key_event, 0); } else { let mut key_event = KeyEvent::new(); key_event.set_control_key(ControlKey::Delete); self.legacy_modifiers(&mut key_event, true, true, false, false); // todo key_event.press = true; - self.send_key_event(key_event, 3); + self.send_key_event(key_event, 0); } } @@ -937,7 +937,7 @@ impl Handler { key_event.set_control_key(ControlKey::LockScreen); // todo key_event.down = true; - self.send_key_event(key_event, 3); + self.send_key_event(key_event, 0); } fn transfer_file(&mut self) { @@ -965,6 +965,7 @@ impl Handler { self.send(Data::Message(msg_out)); } + #[allow(dead_code)] fn convert_numpad_keys(&mut self, key: RdevKey) -> RdevKey { if get_key_state(enigo::Key::NumLock) { return key; @@ -1000,13 +1001,9 @@ impl Handler { } else { rdev::macos_keycode_from_key(key).unwrap_or_default().into() }; - key_event.set_chr(keycode); - if down_or_up == true { - key_event.down = true; - } else { - key_event.down = false; - } + key_event.set_chr(keycode); + key_event.down = down_or_up; if get_key_state(enigo::Key::CapsLock) { key_event.modifiers.push(ControlKey::CapsLock.into()); @@ -1065,7 +1062,7 @@ impl Handler { } fn legacy_keyboard_mode(&mut self, down_or_up: bool, key: RdevKey, evt: Event) { - // legacy mode(3): Generate characters locally, look for keycode on other side. + // legacy mode(0): Generate characters locally, look for keycode on other side. let peer = self.peer_platform(); let is_win = peer == "Windows"; @@ -1273,12 +1270,12 @@ impl Handler { key_event.down = true; } dbg!(&key_event); - self.send_key_event(key_event, 3) + self.send_key_event(key_event, 0) } fn key_down_or_up(&mut self, down_or_up: bool, key: RdevKey, evt: Event) { // Call different functions according to keyboard mode. - let mode = std::env::var("KEYBOARD_MOAD").unwrap_or(String::from("map")); + let mode = std::env::var("KEYBOARD_MOAD").unwrap_or(String::from("legacy")); match mode.as_str() { "map" => { if down_or_up == true { @@ -1289,9 +1286,7 @@ impl Handler { self.map_keyboard_mode(down_or_up, key); } "legacy" => self.legacy_keyboard_mode(down_or_up, key, evt), - _ => { - self.map_keyboard_mode(down_or_up, key); - } + _ => self.legacy_keyboard_mode(down_or_up, key, evt), } } From 02b4d7f1d9dc755d4e55f403479dbb76b4a82f5c Mon Sep 17 00:00:00 2001 From: Asura Date: Tue, 19 Jul 2022 15:43:13 +0800 Subject: [PATCH 0106/2015] Refactor keyboard mode by enum --- libs/hbb_common/protos/message.proto | 9 +++++++- src/server/connection.rs | 4 ++-- src/server/input_service.rs | 10 ++++---- src/ui/remote.rs | 34 +++++++++++++++++----------- 4 files changed, 36 insertions(+), 21 deletions(-) diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index 645930eb4..2282bf43a 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -97,6 +97,13 @@ message MouseEvent { repeated ControlKey modifiers = 4; } +enum KeyboardMode{ + Legacy = 0; + Map = 1; + Translate = 2; + Auto = 3; +} + enum ControlKey { Unknown = 0; Alt = 1; @@ -190,7 +197,7 @@ message KeyEvent { string seq = 6; } repeated ControlKey modifiers = 8; - uint32 mode = 9; + KeyboardMode mode = 9; } message CursorData { diff --git a/src/server/connection.rs b/src/server/connection.rs index 854191c95..fdeaac106 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -420,11 +420,11 @@ impl Connection { } MessageInput::Key((mut msg, press)) => { // todo: press and down have similar meanings. - if press && msg.mode == 0 { + if press && msg.mode.unwrap() == KeyboardMode::Legacy { msg.down = true; } handle_key(&msg); - if press && msg.mode == 0 { + if press && msg.mode.unwrap() == KeyboardMode::Legacy { msg.down = false; handle_key(&msg); } diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 4563ef465..02f50abff 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -450,7 +450,7 @@ pub fn lock_screen() { key_event.down = true; key_event.set_chr('l' as _); key_event.modifiers.push(ControlKey::Meta.into()); - key_event.mode = 0; + key_event.mode = KeyboardMode::Legacy; handle_key(&key_event); key_event.down = false; handle_key(&key_event); @@ -463,7 +463,7 @@ pub fn lock_screen() { key_event.set_chr('q' as _); key_event.modifiers.push(ControlKey::Meta.into()); key_event.modifiers.push(ControlKey::Control.into()); - key_event.mode = 0; + key_event.mode = KeyboardMode::Legacy; handle_key(&key_event); key_event.down = false; handle_key(&key_event); @@ -787,11 +787,11 @@ fn handle_key_(evt: &KeyEvent) { return; } - match evt.mode { - 0 => { + match evt.mode.unwrap() { + KeyboardMode::Legacy => { legacy_keyboard_map(evt); } - 1 => { + KeyboardMode::Map => { map_keyboard_map(evt); } _ => { diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 0b96b3c26..4deaf05d8 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -41,6 +41,7 @@ use hbb_common::{ time::{self, Duration, Instant, Interval}, }, Stream, + protobuf::ProtobufEnumOrUnknown, }; use hbb_common::{config::TransferSerde, fs::TransferJobMeta}; use rdev::{Event, EventType::*, Key as RdevKey}; @@ -921,14 +922,14 @@ impl Handler { key_event.set_control_key(ControlKey::CtrlAltDel); // todo key_event.down = true; - self.send_key_event(key_event, 0); + self.send_key_event(key_event, KeyboardMode::Legacy); } else { let mut key_event = KeyEvent::new(); key_event.set_control_key(ControlKey::Delete); self.legacy_modifiers(&mut key_event, true, true, false, false); // todo key_event.press = true; - self.send_key_event(key_event, 0); + self.send_key_event(key_event, KeyboardMode::Legacy); } } @@ -937,7 +938,7 @@ impl Handler { key_event.set_control_key(ControlKey::LockScreen); // todo key_event.down = true; - self.send_key_event(key_event, 0); + self.send_key_event(key_event, KeyboardMode::Legacy); } fn transfer_file(&mut self) { @@ -956,9 +957,9 @@ impl Handler { } } - fn send_key_event(&mut self, mut evt: KeyEvent, keyboard_mode: u32) { - // mode: map(1), translate(2), legacy(3), auto(4) - evt.mode = keyboard_mode; + fn send_key_event(&mut self, mut evt: KeyEvent, keyboard_mode: KeyboardMode) { + // mode: legacy(0), map(1), translate(2), auto(3) + evt.mode = ProtobufEnumOrUnknown::new(keyboard_mode); let mut msg_out = Message::new(); msg_out.set_key_event(evt); log::info!("{:?}", msg_out); @@ -1012,7 +1013,7 @@ impl Handler { key_event.modifiers.push(ControlKey::NumLock.into()); } - self.send_key_event(key_event, 1); + self.send_key_event(key_event, KeyboardMode::Map); } // fn translate_keyboard_mode(&mut self, down_or_up: bool, key: RdevKey) { @@ -1269,15 +1270,22 @@ impl Handler { if down_or_up == true { key_event.down = true; } - dbg!(&key_event); - self.send_key_event(key_event, 0) + self.send_key_event(key_event, KeyboardMode::Legacy) } fn key_down_or_up(&mut self, down_or_up: bool, key: RdevKey, evt: Event) { // Call different functions according to keyboard mode. - let mode = std::env::var("KEYBOARD_MOAD").unwrap_or(String::from("legacy")); - match mode.as_str() { - "map" => { + let mode = match std::env::var("KEYBOARD_MOAD") + .unwrap_or(String::from("legacy")) + .as_str() + { + "map" => KeyboardMode::Map, + "legacy" => KeyboardMode::Legacy, + _ => KeyboardMode::Legacy, + }; + + match mode { + KeyboardMode::Map => { if down_or_up == true { TO_RELEASE.lock().unwrap().insert(key); } else { @@ -1285,7 +1293,7 @@ impl Handler { } self.map_keyboard_mode(down_or_up, key); } - "legacy" => self.legacy_keyboard_mode(down_or_up, key, evt), + KeyboardMode::Legacy => self.legacy_keyboard_mode(down_or_up, key, evt), _ => self.legacy_keyboard_mode(down_or_up, key, evt), } } From cbdc28ee2055453ea35fdc52661ef9fc06b0d17f Mon Sep 17 00:00:00 2001 From: Asura Date: Tue, 19 Jul 2022 01:04:23 -0700 Subject: [PATCH 0107/2015] Fix compiler error --- src/server/input_service.rs | 15 ++++++++++----- src/ui/remote.rs | 3 +-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 02f50abff..b20cad35e 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -447,11 +447,14 @@ pub fn lock_screen() { // loginctl lock-session also not work, they both work run rustdesk from cmd std::thread::spawn(|| { let mut key_event = KeyEvent::new(); - key_event.down = true; + key_event.set_chr('l' as _); key_event.modifiers.push(ControlKey::Meta.into()); - key_event.mode = KeyboardMode::Legacy; + key_event.mode = KeyboardMode::Legacy.into(); + + key_event.down = true; handle_key(&key_event); + key_event.down = false; handle_key(&key_event); }); @@ -459,11 +462,13 @@ pub fn lock_screen() { // CGSession -suspend not real lock screen, it is user switch std::thread::spawn(|| { let mut key_event = KeyEvent::new(); - key_event.down = true; + key_event.set_chr('q' as _); key_event.modifiers.push(ControlKey::Meta.into()); key_event.modifiers.push(ControlKey::Control.into()); - key_event.mode = KeyboardMode::Legacy; + key_event.mode = KeyboardMode::Legacy.into(); + + key_event.down = true; handle_key(&key_event); key_event.down = false; handle_key(&key_event); @@ -829,7 +834,7 @@ mod test { // set key/char base on char let mut evt = KeyEvent::new(); evt.set_chr(66); - evt.mode = 1; + evt.mode = ProtobufEnum::new(KeyboardMode::Legacy); evt.modifiers.push(ControlKey::CapsLock.into()); diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 4deaf05d8..4a33be574 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -41,7 +41,6 @@ use hbb_common::{ time::{self, Duration, Instant, Interval}, }, Stream, - protobuf::ProtobufEnumOrUnknown, }; use hbb_common::{config::TransferSerde, fs::TransferJobMeta}; use rdev::{Event, EventType::*, Key as RdevKey}; @@ -959,7 +958,7 @@ impl Handler { fn send_key_event(&mut self, mut evt: KeyEvent, keyboard_mode: KeyboardMode) { // mode: legacy(0), map(1), translate(2), auto(3) - evt.mode = ProtobufEnumOrUnknown::new(keyboard_mode); + evt.mode = keyboard_mode.into(); let mut msg_out = Message::new(); msg_out.set_key_event(evt); log::info!("{:?}", msg_out); From d07ef7af8af6bf88d6b740bce1b778f5448a3ebc Mon Sep 17 00:00:00 2001 From: Asura Date: Tue, 19 Jul 2022 16:15:57 +0800 Subject: [PATCH 0108/2015] Fix compile error of testcase --- src/server/input_service.rs | 2 +- src/ui/remote.rs | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/server/input_service.rs b/src/server/input_service.rs index b20cad35e..252139adb 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -834,7 +834,7 @@ mod test { // set key/char base on char let mut evt = KeyEvent::new(); evt.set_chr(66); - evt.mode = ProtobufEnum::new(KeyboardMode::Legacy); + evt.mode = KeyboardMode::Legacy.into(); evt.modifiers.push(ControlKey::CapsLock.into()); diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 4a33be574..3688a7d75 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -961,7 +961,6 @@ impl Handler { evt.mode = keyboard_mode.into(); let mut msg_out = Message::new(); msg_out.set_key_event(evt); - log::info!("{:?}", msg_out); self.send(Data::Message(msg_out)); } @@ -1274,7 +1273,7 @@ impl Handler { fn key_down_or_up(&mut self, down_or_up: bool, key: RdevKey, evt: Event) { // Call different functions according to keyboard mode. - let mode = match std::env::var("KEYBOARD_MOAD") + let mode = match std::env::var("KEYBOARD_MODE") .unwrap_or(String::from("legacy")) .as_str() { From 669e8b98b233b9918e1e88a05b01941c5dcc8da1 Mon Sep 17 00:00:00 2001 From: Asura Date: Tue, 19 Jul 2022 16:24:19 +0800 Subject: [PATCH 0109/2015] Update Cargo.toml about rdev --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 056c14ae7..6a32a7a89 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3858,7 +3858,7 @@ dependencies = [ [[package]] name = "rdev" version = "0.5.0-2" -source = "git+https://github.com/asur4s/rdev#95cecfd1b0f0b20c6cd728afca859107b911f3b8" +source = "git+https://github.com/asur4s/rdev#548d1194dd6863ab004d59299b995eb64cf21c3d" dependencies = [ "cocoa 0.22.0", "core-foundation 0.7.0", From fe9923109092827f543560a7af42dff6c3135117 Mon Sep 17 00:00:00 2001 From: Asura Date: Tue, 19 Jul 2022 16:49:05 +0800 Subject: [PATCH 0110/2015] Make case insensitive of keyboard_mode --- src/ui/remote.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 3688a7d75..c8ce5d724 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -1274,7 +1274,7 @@ impl Handler { fn key_down_or_up(&mut self, down_or_up: bool, key: RdevKey, evt: Event) { // Call different functions according to keyboard mode. let mode = match std::env::var("KEYBOARD_MODE") - .unwrap_or(String::from("legacy")) + .unwrap_or(String::from("legacy")).to_lowercase() .as_str() { "map" => KeyboardMode::Map, From 3a0c10bdb183e4ac6298239f0797902664171d36 Mon Sep 17 00:00:00 2001 From: Asura Date: Wed, 20 Jul 2022 20:31:17 -0700 Subject: [PATCH 0111/2015] Refactor for compiler in linux --- libs/enigo/src/lib.rs | 6 +++--- libs/enigo/src/linux/xdo.rs | 2 +- src/server/input_service.rs | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/libs/enigo/src/lib.rs b/libs/enigo/src/lib.rs index 164fb1c17..01e9b67d2 100644 --- a/libs/enigo/src/lib.rs +++ b/libs/enigo/src/lib.rs @@ -257,7 +257,7 @@ pub enum Key { Backspace, /// caps lock key CapsLock, - #[deprecated(since = "0.0.12", note = "now renamed to Meta")] + // #[deprecated(since = "0.0.12", note = "now renamed to Meta")] /// command key on macOS (super key on Linux, windows key on Windows) Command, /// control key @@ -314,14 +314,14 @@ pub enum Key { Shift, /// space key Space, - #[deprecated(since = "0.0.12", note = "now renamed to Meta")] + // #[deprecated(since = "0.0.12", note = "now renamed to Meta")] /// super key on linux (command key on macOS, windows key on Windows) Super, /// tab key (tabulator) Tab, /// up arrow key UpArrow, - #[deprecated(since = "0.0.12", note = "now renamed to Meta")] + // #[deprecated(since = "0.0.12", note = "now renamed to Meta")] /// windows key on Windows (super key on Linux, command key on macOS) Windows, /// diff --git a/libs/enigo/src/linux/xdo.rs b/libs/enigo/src/linux/xdo.rs index 541dbe81f..ff687eee2 100644 --- a/libs/enigo/src/linux/xdo.rs +++ b/libs/enigo/src/linux/xdo.rs @@ -3,7 +3,7 @@ use libc; use crate::{Key, KeyboardControllable, MouseButton, MouseControllable}; use self::libc::{c_char, c_int, c_void, useconds_t}; -use std::{borrow::Cow, ffi::CString, io::prelude::*, ptr, sync::mpsc}; +use std::{borrow::Cow, ffi::CString, ptr}; const CURRENT_WINDOW: c_int = 0; const DEFAULT_DELAY: u64 = 12000; diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 6103274ee..5532967ca 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -2,7 +2,7 @@ use super::*; #[cfg(target_os = "macos")] use dispatch::Queue; use enigo::{Enigo, Key, KeyboardControllable, MouseButton, MouseControllable}; -use hbb_common::{config::COMPRESS_LEVEL, protobuf::ProtobufEnumOrUnknown, protobuf::EnumOrUnknown}; +use hbb_common::{config::COMPRESS_LEVEL, protobuf::EnumOrUnknown}; use rdev::{simulate, EventType, Key as RdevKey}; use std::{ convert::TryFrom, @@ -654,7 +654,7 @@ fn sync_status(evt: &KeyEvent) { fn map_keyboard_map(evt: &KeyEvent) { // map mode(1): Send keycode according to the peer platform. sync_status(evt); - rdev_key_down_or_up(RdevKey::Unknown(evt.get_chr()), evt.down); + rdev_key_down_or_up(RdevKey::Unknown(evt.chr()), evt.down); return; } From 25c7bbd96fb55a8a31b3ee3f22786b555440db19 Mon Sep 17 00:00:00 2001 From: Asura Date: Thu, 21 Jul 2022 12:46:19 +0800 Subject: [PATCH 0112/2015] Fix numpad error --- libs/enigo/src/linux/pynput.rs | 20 ++++++++++---------- rust-toolchain.toml | 2 -- 2 files changed, 10 insertions(+), 12 deletions(-) delete mode 100644 rust-toolchain.toml diff --git a/libs/enigo/src/linux/pynput.rs b/libs/enigo/src/linux/pynput.rs index 748b30105..7868b7f6f 100644 --- a/libs/enigo/src/linux/pynput.rs +++ b/libs/enigo/src/linux/pynput.rs @@ -67,16 +67,16 @@ impl EnigoPynput { Key::Space => "space", Key::Tab => "Tab", Key::UpArrow => "Up", - Key::Numpad0 => "0", - Key::Numpad1 => "1", - Key::Numpad2 => "2", - Key::Numpad3 => "3", - Key::Numpad4 => "4", - Key::Numpad5 => "5", - Key::Numpad6 => "6", - Key::Numpad7 => "7", - Key::Numpad8 => "8", - Key::Numpad9 => "9", + Key::Numpad0 => "KP_0", + Key::Numpad1 => "KP_1", + Key::Numpad2 => "KP_2", + Key::Numpad3 => "KP_3", + Key::Numpad4 => "KP_4", + Key::Numpad5 => "KP_5", + Key::Numpad6 => "KP_6", + Key::Numpad7 => "KP_7", + Key::Numpad8 => "KP_8", + Key::Numpad9 => "KP_9", Key::Decimal => "KP_Decimal", Key::Cancel => "Cancel", Key::Clear => "Clear", diff --git a/rust-toolchain.toml b/rust-toolchain.toml deleted file mode 100644 index 05dfa3270..000000000 --- a/rust-toolchain.toml +++ /dev/null @@ -1,2 +0,0 @@ -[toolchain] -channel = "1.62.0" From 5946f6e47dda9c2df58323c52e48a83533ea3e80 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 22 Jul 2022 23:12:31 +0800 Subject: [PATCH 0113/2015] opt: recent&fav cards --- .../lib/desktop/pages/connection_page.dart | 170 ++++++++++++++---- src/flutter_ffi.rs | 22 ++- 2 files changed, 158 insertions(+), 34 deletions(-) diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 70231d603..1e2939284 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:flutter/material.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:get/get.dart'; @@ -10,6 +12,12 @@ import '../../mobile/pages/scan_page.dart'; import '../../mobile/pages/settings_page.dart'; import '../../models/model.dart'; +enum RemoteType { + recently, + favorite, + discovered +} + /// Connection page for connecting to a remote peer. class ConnectionPage extends StatefulWidget implements PageShape { ConnectionPage({Key? key}) : super(key: key); @@ -62,7 +70,45 @@ class _ConnectionPageState extends State { ).marginOnly(top: 16.0, left: 16.0), SizedBox(height: 12), Divider(thickness: 1,), - getPeers(), + Expanded( + child: DefaultTabController( + length: 4, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TabBar( + labelColor: Colors.black87, + isScrollable: true, + indicatorSize: TabBarIndicatorSize.label, + tabs: [ + Tab(child: Text(translate("Recent Sessions")),), + Tab(child: Text(translate("Favorites")),), + Tab(child: Text(translate("Discovered")),), + Tab(child: Text(translate("Address Book")),), + ]), + Expanded(child: TabBarView(children: [ + FutureBuilder(future: getPeers(rType: RemoteType.recently), + builder: (context, snapshot){ + if (snapshot.hasData) { + return snapshot.data!; + } else { + return Offstage(); + } + }), + FutureBuilder(future: getPeers(rType: RemoteType.favorite), + builder: (context, snapshot){ + if (snapshot.hasData) { + return snapshot.data!; + } else { + return Offstage(); + } + }), + Container(), + Container(), + ]).paddingSymmetric(horizontal: 12.0,vertical: 4.0)) + ], + )), + ), ]), ); } @@ -230,25 +276,47 @@ class _ConnectionPageState extends State { if (platform == 'mac os') platform = 'mac'; else if (platform != 'linux' && platform != 'android') platform = 'win'; - return Image.asset('assets/$platform.png', width: 24, height: 24); + return Image.asset('assets/$platform.png', height: 50); } /// Get all the saved peers. - Widget getPeers() { + Future getPeers({RemoteType rType = RemoteType.recently}) async { final size = MediaQuery.of(context).size; final space = 8.0; - var width = size.width - 2 * space; - final minWidth = 320.0; - if (size.width > minWidth + 2 * space) { - final n = (size.width / (minWidth + 2 * space)).floor(); - width = size.width / n - 2 * space; - } final cards = []; - var peers = gFFI.peers(); + var peers; + switch (rType) { + case RemoteType.recently: + peers = gFFI.peers(); + break; + case RemoteType.favorite: + peers = await gFFI.bind.mainGetFav().then((peers) async { + final peersEntities = await Future.wait(peers.map((id) => gFFI.bind.mainGetPeers(id: id)).toList(growable: false)) + .then((peers_str){ + final len = peers_str.length; + final ps = List.empty(growable: true); + for(var i = 0; i< len ; i++){ + print("${peers[i]}: ${peers_str[i]}"); + ps.add(Peer.fromJson(peers[i], jsonDecode(peers_str[i])['info'])); + } + return ps; + }); + return peersEntities; + }); + break; + case RemoteType.discovered: + // TODO: Handle this case. + peers = await gFFI.bind.mainGetLanPeers().then((peers_string){ + + }); + break; + } peers.forEach((p) { cards.add(Container( - width: width, + width: 250, + height: 150, child: Card( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), child: GestureDetector( onTap: !isWebDesktop ? () => connect('${p.id}') : null, onDoubleTap: isWebDesktop ? () => connect('${p.id}') : null, @@ -258,29 +326,67 @@ class _ConnectionPageState extends State { _menuPos = RelativeRect.fromLTRB(x, y, x, y); showPeerMenu(context, p.id); }, - child: ListTile( - contentPadding: const EdgeInsets.only(left: 12), - subtitle: Text('${p.username}@${p.hostname}'), - title: Text('${p.id}'), - leading: Container( - padding: const EdgeInsets.all(6), - child: getPlatformImage('${p.platform}'), - color: str2color('${p.id}${p.platform}', 0x7f)), - trailing: InkWell( - child: Padding( - padding: const EdgeInsets.all(12), - child: Icon(Icons.more_vert)), - onTapDown: (e) { - final x = e.globalPosition.dx; - final y = e.globalPosition.dy; - _menuPos = RelativeRect.fromLTRB(x, y, x, y); - }, - onTap: () { - showPeerMenu(context, p.id); - }), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: Container( + decoration: BoxDecoration( + color: str2color('${p.id}${p.platform}', 0x7f), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ) + ), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.all(6), + child: getPlatformImage('${p.platform}'),), + Row( + children: [ + Expanded( + child: Text('${p.username}@${p.hostname}', style: TextStyle( + color: Colors.white70, + fontSize: 12 + ),textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ], + ).paddingAll(4.0), + ), + ], + ), + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("${p.id}"), + InkWell( + child: Icon(Icons.more_vert), + onTapDown: (e) { + final x = e.globalPosition.dx; + final y = e.globalPosition.dy; + _menuPos = RelativeRect.fromLTRB(x, y, x, y); + }, + onTap: () { + showPeerMenu(context, p.id); + }), + ], + ).paddingSymmetric(vertical: 8.0,horizontal: 12.0) + ], ))))); }); - return Wrap(children: cards, spacing: space, runSpacing: space); + return SingleChildScrollView(child: Wrap(children: cards, spacing: space, runSpacing: space)); } /// Show the peer menu and handle user's choice. diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 432ba3969..edd368509 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -20,8 +20,9 @@ use crate::flutter::{self, Session, SESSIONS}; use crate::start_server; use crate::ui_interface; use crate::ui_interface::{ - change_id, get_app_name, get_async_job_status, get_license, get_options, get_socks, - get_sound_inputs, get_version, is_ok_change_id, set_options, set_socks, test_if_valid_server, + change_id, get_app_name, get_async_job_status, get_fav, get_lan_peers, get_license, + get_options, get_peer, get_socks, get_sound_inputs, get_version, is_ok_change_id, set_options, + set_socks, store_fav, test_if_valid_server, }; fn initialize(app_dir: &str) { @@ -429,6 +430,23 @@ pub fn main_get_version() -> String { get_version() } +pub fn main_get_fav() -> Vec { + get_fav() +} + +pub fn main_store_fav(favs: Vec) { + store_fav(favs) +} + +pub fn main_get_peers(id: String) -> String { + let conf = get_peer(id); + serde_json::to_string(&conf).unwrap_or("".to_string()) +} + +pub fn main_get_lan_peers() -> String { + get_lan_peers() +} + /// FFI for **get** commands which are idempotent. /// Return result in c string. /// From 1b0fb5132c2dc00a38ce43efe19029301756f575 Mon Sep 17 00:00:00 2001 From: Asura Date: Sat, 23 Jul 2022 20:51:01 +0800 Subject: [PATCH 0114/2015] Support switching keyboard mode by UI --- src/lang/cn.rs | 2 ++ src/ui/common.tis | 90 +++++++++++++++++++++++++++++++++++++++++++++++ src/ui/header.tis | 31 ++++++++++++++++ src/ui/remote.rs | 17 ++++++--- 4 files changed, 136 insertions(+), 4 deletions(-) diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 6e3d4d067..c1ee5d305 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -300,5 +300,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Activate onetime password", "激活一次性访问功能"), ("Set security password", "设置安全密码"), ("Connection not allowed", "对方不允许连接"), + ("Legacy mode", "传统模式"), + ("Map mode", "1:1传输"), ].iter().cloned().collect(); } diff --git a/src/ui/common.tis b/src/ui/common.tis index aae950c2d..8a0beef98 100644 --- a/src/ui/common.tis +++ b/src/ui/common.tis @@ -155,6 +155,96 @@ var svg_send = var svg_chat = ; +var svg_keyboard = + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +; function scrollToBottom(el) { var y = el.box(#height, #content) - el.box(#height, #client); diff --git a/src/ui/header.tis b/src/ui/header.tis index 35a132c90..8cce3e18a 100644 --- a/src/ui/header.tis +++ b/src/ui/header.tis @@ -139,11 +139,22 @@ class Header: Reactor.Component { {svg_chat} {svg_action} {svg_display} + {svg_keyboard} + {this.renderKeyboardPop()} {this.renderDisplayPop()} {this.renderActionPop()} ; } + function renderKeyboardPop(){ + return + +
  • {svg_checkmark}{translate('Legacy mode')}
  • +
  • {svg_checkmark}{translate('Map mode')}
  • +
    + ; + } + function renderDisplayPop() { return @@ -252,6 +263,11 @@ class Header: Reactor.Component { me.popup(menu); } + event click $(#keyboard) (_, me) { + var menu = $(menu#keyboard-options); + me.popup(menu); + } + event click $(#screen) (_, me) { if (pi.current_display == me.index) return; handler.switch_display(me.index); @@ -332,6 +348,16 @@ class Header: Reactor.Component { toggleMenuState(); } } + + event click $(menu#keyboard-options>li) (_, me) { + stdout.println(me.id); + if (me.id == "legacy") { + handler.save_keyboard_mode("legacy"); + } else if (me.id == "map") { + handler.save_keyboard_mode("map"); + } + toggleMenuState() + } } function handle_custom_image_quality() { @@ -355,9 +381,14 @@ function toggleMenuState() { var s = handler.get_view_style(); if (!s) s = "original"; values.push(s); + var k = handler.get_keyboard_mode(); + values.push(k); for (var el in $$(menu#display-options>li)) { el.attributes.toggleClass("selected", values.indexOf(el.id) >= 0); } + for (var el in $$(menu#keyboard-options>li)) { + el.attributes.toggleClass("selected", values.indexOf(el.id) >= 0); + } for (var id in ["show-remote-cursor", "show-quality-monitor", "disable-audio", "enable-file-transfer", "disable-clipboard", "lock-after-session-end"]) { var el = self.select('#' + id); if (el) { diff --git a/src/ui/remote.rs b/src/ui/remote.rs index ef14ef42e..f68d76e1b 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -233,6 +233,8 @@ impl sciter::EventHandler for Handler { fn get_remember(); fn peer_platform(); fn set_write_override(i32, i32, bool, bool, bool); + fn get_keyboard_mode(); + fn save_keyboard_mode(String); } } @@ -347,6 +349,16 @@ impl Handler { return self.lc.read().unwrap().image_quality.clone(); } + fn get_keyboard_mode(&mut self) -> String { + return std::env::var("KEYBOARD_MODE") + .unwrap_or(String::from("legacy")) + .to_lowercase(); + } + + fn save_keyboard_mode(&mut self, value: String) { + std::env::set_var("KEYBOARD_MODE", value); + } + fn get_custom_image_quality(&mut self) -> Value { let mut v = Value::array(0); for x in self.lc.read().unwrap().custom_image_quality.iter() { @@ -1273,10 +1285,7 @@ impl Handler { fn key_down_or_up(&mut self, down_or_up: bool, key: RdevKey, evt: Event) { // Call different functions according to keyboard mode. - let mode = match std::env::var("KEYBOARD_MODE") - .unwrap_or(String::from("legacy")).to_lowercase() - .as_str() - { + let mode = match self.get_keyboard_mode().as_str() { "map" => KeyboardMode::Map, "legacy" => KeyboardMode::Legacy, _ => KeyboardMode::Legacy, From b3b97ee69a62ca4471699f387a89550d3359e73d Mon Sep 17 00:00:00 2001 From: Asura Date: Sat, 23 Jul 2022 21:45:00 +0800 Subject: [PATCH 0115/2015] Remove log info --- libs/enigo/src/linux/pynput.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/libs/enigo/src/linux/pynput.rs b/libs/enigo/src/linux/pynput.rs index 7868b7f6f..836c645fe 100644 --- a/libs/enigo/src/linux/pynput.rs +++ b/libs/enigo/src/linux/pynput.rs @@ -110,7 +110,6 @@ impl EnigoPynput { return true; } }; - log::info!("send pynput: {:?}", &s); return self.tx.send((PyMsg::Str(s), is_press)).is_ok(); } } From 1caee4e30678ba6d784689920ecd83ed2af3293a Mon Sep 17 00:00:00 2001 From: Asura Date: Sat, 23 Jul 2022 08:20:39 -0700 Subject: [PATCH 0116/2015] Sync CapsLock and NumLock status in legacy mode. --- libs/enigo/src/linux/nix_impl.rs | 10 ++----- src/server/input_service.rs | 47 ++++++++++++++++++++------------ src/ui/header.tis | 1 - src/ui/remote.rs | 2 +- 4 files changed, 34 insertions(+), 26 deletions(-) diff --git a/libs/enigo/src/linux/nix_impl.rs b/libs/enigo/src/linux/nix_impl.rs index 840290b2b..c660bd08c 100644 --- a/libs/enigo/src/linux/nix_impl.rs +++ b/libs/enigo/src/linux/nix_impl.rs @@ -156,6 +156,7 @@ impl KeyboardControllable for Enigo { } fn key_up(&mut self, key: Key) { if self.is_x11 { + dbg!(key); if self.pynput.send_pynput(&key, false) { return; } @@ -167,12 +168,7 @@ impl KeyboardControllable for Enigo { } } fn key_click(&mut self, key: Key) { - if self.is_x11 { - self.xdo.key_click(key) - } else { - if let Some(keyboard) = &mut self.uinput_keyboard { - keyboard.key_click(key) - } - } + self.key_down(key).ok(); + self.key_up(key); } } diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 01b049ddb..d8d4287fc 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -146,7 +146,8 @@ fn run_cursor(sp: MouseCursorService, state: &mut StateCursor) -> ResultType<()> msg = cached.clone(); } else { let mut data = crate::get_cursor_data(hcursor)?; - data.colors = hbb_common::compress::compress(&data.colors[..], COMPRESS_LEVEL).into(); + data.colors = + hbb_common::compress::compress(&data.colors[..], COMPRESS_LEVEL).into(); let mut tmp = Message::new(); tmp.set_cursor_data(data); msg = Arc::new(tmp); @@ -467,7 +468,7 @@ pub async fn lock_screen() { // loginctl lock-session also not work, they both work run rustdesk from cmd std::thread::spawn(|| { let mut key_event = KeyEvent::new(); - + key_event.set_chr('l' as _); key_event.modifiers.push(ControlKey::Meta.into()); key_event.mode = KeyboardMode::Legacy.into(); @@ -482,7 +483,7 @@ pub async fn lock_screen() { // CGSession -suspend not real lock screen, it is user switch std::thread::spawn(|| { let mut key_event = KeyEvent::new(); - + key_event.set_chr('q' as _); key_event.modifiers.push(ControlKey::Meta.into()); key_event.modifiers.push(ControlKey::Control.into()); @@ -621,7 +622,12 @@ fn rdev_key_down_or_up(key: RdevKey, down_or_up: bool) { std::thread::sleep(delay); } -fn sync_status(evt: &KeyEvent) { +fn rdev_key_click(key: RdevKey) { + rdev_key_down_or_up(key, true); + rdev_key_down_or_up(key, false); +} + +fn sync_status(evt: &KeyEvent) -> (bool, bool) { let mut en = ENIGO.lock().unwrap(); // remote caps status @@ -637,31 +643,38 @@ fn sync_status(evt: &KeyEvent) { .position(|&r| r == ControlKey::NumLock.into()) .is_some(); - if (caps_locking && !en.get_key_state(enigo::Key::CapsLock)) - || (!caps_locking && en.get_key_state(enigo::Key::CapsLock)) - { - rdev_key_down_or_up(RdevKey::CapsLock, true); - rdev_key_down_or_up(RdevKey::CapsLock, false); - }; - if (num_locking && !en.get_key_state(enigo::Key::NumLock)) - || (!num_locking && en.get_key_state(enigo::Key::NumLock)) - { - rdev_key_down_or_up(RdevKey::NumLock, true); - rdev_key_down_or_up(RdevKey::NumLock, false); - }; + let click_capslock = (caps_locking && !en.get_key_state(enigo::Key::CapsLock)) + || (!caps_locking && en.get_key_state(enigo::Key::CapsLock)); + let click_numlock = (num_locking && !en.get_key_state(enigo::Key::NumLock)) + || (!num_locking && en.get_key_state(enigo::Key::NumLock)); + return (click_capslock, click_numlock); } fn map_keyboard_map(evt: &KeyEvent) { // map mode(1): Send keycode according to the peer platform. - sync_status(evt); + let (click_capslock, click_numlock) = sync_status(evt); + if click_capslock { + rdev_key_click(RdevKey::CapsLock); + } + if click_numlock { + rdev_key_click(RdevKey::NumLock); + } rdev_key_down_or_up(RdevKey::Unknown(evt.chr()), evt.down); return; } fn legacy_keyboard_map(evt: &KeyEvent) { + let (click_capslock, click_numlock) = sync_status(evt); + #[cfg(windows)] crate::platform::windows::try_change_desktop(); let mut en = ENIGO.lock().unwrap(); + if click_capslock { + en.key_click(Key::CapsLock); + } + if click_numlock { + en.key_click(Key::NumLock); + } // disable numlock if press home etc when numlock is on, // because we will get numpad value (7,8,9 etc) if not #[cfg(windows)] diff --git a/src/ui/header.tis b/src/ui/header.tis index 0ef473124..def2ab9f7 100644 --- a/src/ui/header.tis +++ b/src/ui/header.tis @@ -363,7 +363,6 @@ class Header: Reactor.Component { } event click $(menu#keyboard-options>li) (_, me) { - stdout.println(me.id); if (me.id == "legacy") { handler.save_keyboard_mode("legacy"); } else if (me.id == "map") { diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 69153bedc..9338b90d4 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -1105,7 +1105,7 @@ impl Handler { key_event.modifiers.push(ControlKey::CapsLock.into()); } if self.peer_platform() != "Mac OS" { - if get_key_state(enigo::Key::NumLock) && common::valid_for_numlock(&key_event) { + if get_key_state(enigo::Key::NumLock) { key_event.modifiers.push(ControlKey::NumLock.into()); } } From 4cfa84082223df106e0b137dba86be93fa140b6b Mon Sep 17 00:00:00 2001 From: kingtous Date: Mon, 25 Jul 2022 16:23:45 +0800 Subject: [PATCH 0117/2015] add: address book ui&getAb Signed-off-by: Kingtous --- flutter/lib/common.dart | 2 + .../lib/desktop/pages/connection_page.dart | 524 +++++++++++++----- flutter/lib/models/model.dart | 19 +- src/flutter_ffi.rs | 49 +- src/ipc.rs | 21 +- src/ui_interface.rs | 2 +- 6 files changed, 467 insertions(+), 150 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index e1315d233..b896fdf9f 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -325,4 +325,6 @@ Future initGlobalFFI() async { // after `put`, can also be globally found by Get.find(); Get.put(_globalFFI, permanent: true); await _globalFFI.ffiModel.init(); + // trigger connection status updater + await _globalFFI.bind.mainCheckConnectStatus(); } \ No newline at end of file diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 1e2939284..e29ab9b5f 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:convert'; import 'package:flutter/material.dart'; @@ -5,6 +6,7 @@ import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'package:url_launcher/url_launcher_string.dart'; import '../../common.dart'; import '../../mobile/pages/home_page.dart'; @@ -12,11 +14,7 @@ import '../../mobile/pages/scan_page.dart'; import '../../mobile/pages/settings_page.dart'; import '../../models/model.dart'; -enum RemoteType { - recently, - favorite, - discovered -} +enum RemoteType { recently, favorite, discovered, addressBook } /// Connection page for connecting to a remote peer. class ConnectionPage extends StatefulWidget implements PageShape { @@ -44,18 +42,22 @@ class _ConnectionPageState extends State { var _updateUrl = ''; var _menuPos; + Timer? _updateTimer; + @override void initState() { super.initState(); + _updateTimer = Timer.periodic(Duration(seconds: 1), (timer) { + updateStatus(); + }); } @override Widget build(BuildContext context) { - Provider.of(context); if (_idController.text.isEmpty) _idController.text = gFFI.getId(); return Container( decoration: BoxDecoration( - color: MyTheme.grayBg + color: MyTheme.grayBg ), child: Column( mainAxisAlignment: MainAxisAlignment.start, @@ -77,25 +79,17 @@ class _ConnectionPageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ TabBar( - labelColor: Colors.black87, - isScrollable: true, + labelColor: Colors.black87, + isScrollable: true, indicatorSize: TabBarIndicatorSize.label, tabs: [ - Tab(child: Text(translate("Recent Sessions")),), - Tab(child: Text(translate("Favorites")),), - Tab(child: Text(translate("Discovered")),), - Tab(child: Text(translate("Address Book")),), - ]), + Tab(child: Text(translate("Recent Sessions")),), + Tab(child: Text(translate("Favorites")),), + Tab(child: Text(translate("Discovered")),), + Tab(child: Text(translate("Address Book")),), + ]), Expanded(child: TabBarView(children: [ FutureBuilder(future: getPeers(rType: RemoteType.recently), - builder: (context, snapshot){ - if (snapshot.hasData) { - return snapshot.data!; - } else { - return Offstage(); - } - }), - FutureBuilder(future: getPeers(rType: RemoteType.favorite), builder: (context, snapshot){ if (snapshot.hasData) { return snapshot.data!; @@ -103,12 +97,40 @@ class _ConnectionPageState extends State { return Offstage(); } }), - Container(), - Container(), - ]).paddingSymmetric(horizontal: 12.0,vertical: 4.0)) + FutureBuilder( + future: getPeers(rType: RemoteType.favorite), + builder: (context, snapshot) { + if (snapshot.hasData) { + return snapshot.data!; + } else { + return Offstage(); + } + }), + FutureBuilder( + future: getPeers(rType: RemoteType.discovered), + builder: (context, snapshot) { + if (snapshot.hasData) { + return snapshot.data!; + } else { + return Offstage(); + } + }), + FutureBuilder( + future: buildAddressBook(context), + builder: (context, snapshot) { + if (snapshot.hasData) { + return snapshot.data!; + } else { + return Offstage(); + } + }), + ]).paddingSymmetric(horizontal: 12.0, vertical: 4.0)) ], )), ), + Divider(), + SizedBox(height: 50, child: Obx(() => buildStatus())) + .paddingSymmetric(horizontal: 12.0) ]), ); } @@ -142,20 +164,20 @@ class _ConnectionPageState extends State { return _updateUrl.isEmpty ? SizedBox(height: 0) : InkWell( - onTap: () async { - final url = _updateUrl + '.apk'; - if (await canLaunch(url)) { - await launch(url); - } - }, - child: Container( - alignment: AlignmentDirectional.center, - width: double.infinity, - color: Colors.pinkAccent, - padding: EdgeInsets.symmetric(vertical: 12), - child: Text(translate('Download new version'), - style: TextStyle( - color: Colors.white, fontWeight: FontWeight.bold)))); + onTap: () async { + final url = _updateUrl + '.apk'; + if (await canLaunch(url)) { + await launch(url); + } + }, + child: Container( + alignment: AlignmentDirectional.center, + width: double.infinity, + color: Colors.pinkAccent, + padding: EdgeInsets.symmetric(vertical: 12), + child: Text(translate('Download new version'), + style: TextStyle( + color: Colors.white, fontWeight: FontWeight.bold)))); } /// UI for the search bar. @@ -267,6 +289,7 @@ class _ConnectionPageState extends State { @override void dispose() { _idController.dispose(); + _updateTimer?.cancel(); super.dispose(); } @@ -281,7 +304,6 @@ class _ConnectionPageState extends State { /// Get all the saved peers. Future getPeers({RemoteType rType = RemoteType.recently}) async { - final size = MediaQuery.of(context).size; final space = 8.0; final cards = []; var peers; @@ -305,104 +327,145 @@ class _ConnectionPageState extends State { }); break; case RemoteType.discovered: - // TODO: Handle this case. - peers = await gFFI.bind.mainGetLanPeers().then((peers_string){ - + peers = await gFFI.bind.mainGetLanPeers().then((peers_string) { + print(peers_string); + return []; }); break; + case RemoteType.addressBook: + await gFFI.abModel.getAb(); + peers = gFFI.abModel.peers.map((e) { + return Peer.fromJson(e['id'], e); + }).toList(); + break; } peers.forEach((p) { + var deco = Rx(BoxDecoration( + border: Border.all(color: Colors.transparent, width: 1.0), + borderRadius: BorderRadius.circular(20))); cards.add(Container( - width: 250, + width: 225, height: 150, child: Card( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), - child: GestureDetector( - onTap: !isWebDesktop ? () => connect('${p.id}') : null, - onDoubleTap: isWebDesktop ? () => connect('${p.id}') : null, - onLongPressStart: (details) { - final x = details.globalPosition.dx; - final y = details.globalPosition.dy; - _menuPos = RelativeRect.fromLTRB(x, y, x, y); - showPeerMenu(context, p.id); - }, - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Expanded( - child: Container( - decoration: BoxDecoration( - color: str2color('${p.id}${p.platform}', 0x7f), - borderRadius: BorderRadius.only( - topLeft: Radius.circular(20), - topRight: Radius.circular(20), - ) - ), - child: Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Container( - padding: const EdgeInsets.all(6), - child: getPlatformImage('${p.platform}'),), - Row( - children: [ - Expanded( - child: Text('${p.username}@${p.hostname}', style: TextStyle( - color: Colors.white70, - fontSize: 12 - ),textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - ], - ).paddingAll(4.0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20)), + child: MouseRegion( + onEnter: (evt) { + deco.value = BoxDecoration( + border: Border.all(color: Colors.blue, width: 1.0), + borderRadius: BorderRadius.circular(20)); + }, + onExit: (evt) { + deco.value = BoxDecoration( + border: Border.all(color: Colors.transparent, width: 1.0), + borderRadius: BorderRadius.circular(20)); + }, + child: Obx( + () => Container( + decoration: deco.value, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: Container( + decoration: BoxDecoration( + color: str2color('${p.id}${p.platform}', 0x7f), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), ), - ], + ), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.all(6), + child: + getPlatformImage('${p.platform}'), + ), + Row( + children: [ + Expanded( + child: Tooltip( + message: + '${p.username}@${p.hostname}', + child: Text( + '${p.username}@${p.hostname}', + style: TextStyle( + color: Colors.white70, + fontSize: 12), + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + ), + ), + ), + ], + ), + ], + ).paddingAll(4.0), + ), + ], + ), ), ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text("${p.id}"), - InkWell( - child: Icon(Icons.more_vert), - onTapDown: (e) { - final x = e.globalPosition.dx; - final y = e.globalPosition.dy; - _menuPos = RelativeRect.fromLTRB(x, y, x, y); - }, - onTap: () { - showPeerMenu(context, p.id); - }), - ], - ).paddingSymmetric(vertical: 8.0,horizontal: 12.0) - ], - ))))); + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("${p.id}"), + InkWell( + child: Icon(Icons.more_vert), + onTapDown: (e) { + final x = e.globalPosition.dx; + final y = e.globalPosition.dy; + _menuPos = RelativeRect.fromLTRB(x, y, x, y); + }, + onTap: () { + showPeerMenu(context, p.id, rType); + }), + ], + ).paddingSymmetric(vertical: 8.0, horizontal: 12.0) + ], + ), + ), + ), + )))); }); - return SingleChildScrollView(child: Wrap(children: cards, spacing: space, runSpacing: space)); + return SingleChildScrollView( + child: Wrap(children: cards, spacing: space, runSpacing: space)); } /// Show the peer menu and handle user's choice. /// User might remove the peer or send a file to the peer. - void showPeerMenu(BuildContext context, String id) async { + void showPeerMenu(BuildContext context, String id, RemoteType rType) async { + var items = [ + PopupMenuItem( + child: Text(translate('Connect')), value: 'connect'), + PopupMenuItem( + child: Text(translate('Transfer File')), value: 'file'), + PopupMenuItem( + child: Text(translate('TCP Tunneling')), value: 'tcp-tunnel'), + PopupMenuItem(child: Text(translate('Rename')), value: 'rename'), + PopupMenuItem(child: Text(translate('Remove')), value: 'remove'), + PopupMenuItem( + child: Text(translate('Unremember Password')), + value: 'unremember-password'), + ]; + if (rType == RemoteType.favorite) { + items.add(PopupMenuItem( + child: Text(translate('Remove from Favorites')), + value: 'remove-fav')); + } else + items.add(PopupMenuItem( + child: Text(translate('Add to Favorites')), value: 'add-fav')); var value = await showMenu( context: context, position: this._menuPos, - items: [ - PopupMenuItem( - child: Text(translate('Remove')), value: 'remove') - ] + - ([ - PopupMenuItem( - child: Text(translate('Transfer File')), value: 'file') - ]), + items: items, elevation: 8, ); if (value == 'remove') { @@ -412,7 +475,200 @@ class _ConnectionPageState extends State { }(); } else if (value == 'file') { connect(id, isFileTransfer: true); + } else if (value == 'add-fav') {} + } + + var svcStopped = false.obs; + var svcStatusCode = 0.obs; + var svcIsUsingPublicServer = true.obs; + + Widget buildStatus() { + final light = Container( + height: 8, + width: 8, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + color: Colors.green, + ), + ).paddingSymmetric(horizontal: 8.0); + if (svcStopped.value) { + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [light, Text(translate("Service is not running"))], + ); + } else { + if (svcStatusCode.value == 0) { + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [light, Text(translate("connecting_status"))], + ); + } else if (svcStatusCode.value == -1) { + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [light, Text(translate("not_ready_status"))], + ); + } } + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + light, + Text("${translate('Ready')}"), + svcIsUsingPublicServer.value + ? InkWell( + onTap: onUsePublicServerGuide, + child: Text( + ', ${translate('setup_server_tip')}', + style: TextStyle(decoration: TextDecoration.underline), + ), + ) + : Offstage() + ], + ); + } + + void onUsePublicServerGuide() { + final url = "https://rustdesk.com/blog/id-relay-set/"; + canLaunchUrlString(url).then((can) { + if (can) { + launchUrlString(url); + } + }); + } + + updateStatus() async { + svcStopped.value = gFFI.getOption("stop-service") == "Y"; + final status = jsonDecode(await gFFI.bind.mainGetConnectStatus()) + as Map; + svcStatusCode.value = status["status_num"]; + svcIsUsingPublicServer.value = await gFFI.bind.mainIsUsingPublicServer(); + } + + handleLogin() {} + + Future buildAddressBook(BuildContext context) async { + final token = await gFFI.getLocalOption('access_token'); + if (token.trim().isEmpty) { + return Center( + child: InkWell( + onTap: handleLogin, + child: Text( + translate("Login"), + style: TextStyle(decoration: TextDecoration.underline), + ), + ), + ); + } + final model = gFFI.abModel; + return FutureBuilder( + future: model.getAb(), + builder: (context, snapshot) { + if (snapshot.hasData) { + return _buildAddressBook(context); + } else { + if (model.abLoading) { + return Center( + child: CircularProgressIndicator(), + ); + } else if (model.abError.isNotEmpty) { + return Center( + child: CircularProgressIndicator(), + ); + } else { + return Offstage(); + } + } + }); + } + + Widget _buildAddressBook(BuildContext context) { + return Row( + children: [ + Card( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + side: BorderSide(color: MyTheme.grayBg)), + color: Colors.white, + child: Container( + width: 200, + height: double.infinity, + padding: EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(translate('Tags')), + InkWell( + child: PopupMenuButton( + itemBuilder: (context) => [], + child: Icon(Icons.more_vert_outlined)), + ) + ], + ), + Expanded( + child: Container( + width: double.infinity, + height: double.infinity, + decoration: BoxDecoration( + border: Border.all(color: MyTheme.darkGray)), + child: Wrap( + children: + gFFI.abModel.tags.map((e) => buildTag(e)).toList(), + ), + ).marginSymmetric(vertical: 8.0), + ) + ], + ), + ), + ).marginOnly(right: 8.0), + Column( + children: [ + FutureBuilder( + future: getPeers(rType: RemoteType.addressBook), + builder: (context, snapshot) { + if (snapshot.hasData) { + return snapshot.data!; + } else { + return Center(child: CircularProgressIndicator()); + } + }), + ], + ) + ], + ); + } + + Widget buildTag(String tagName) { + return Container( + decoration: BoxDecoration( + border: Border.all(color: MyTheme.darkGray), + borderRadius: BorderRadius.circular(10)), + margin: EdgeInsets.symmetric(horizontal: 4.0, vertical: 8.0), + padding: EdgeInsets.symmetric(vertical: 2.0, horizontal: 8.0), + child: Text(tagName), + ); + } +} + +class AddressBookPage extends StatefulWidget { + const AddressBookPage({Key? key}) : super(key: key); + + @override + State createState() => _AddressBookPageState(); +} + +class _AddressBookPageState extends State { + @override + void initState() { + // TODO: implement initState + final ab = gFFI.abModel.getAb(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Container(); } } @@ -430,13 +686,13 @@ class _WebMenuState extends State { icon: Icon(Icons.more_vert), itemBuilder: (context) { return (isIOS - ? [ - PopupMenuItem( - child: Icon(Icons.qr_code_scanner, color: Colors.black), - value: "scan", - ) - ] - : >[]) + + ? [ + PopupMenuItem( + child: Icon(Icons.qr_code_scanner, color: Colors.black), + value: "scan", + ) + ] + : >[]) + [ PopupMenuItem( child: Text(translate('ID/Relay Server')), @@ -446,13 +702,13 @@ class _WebMenuState extends State { (getUrl().contains('admin.rustdesk.com') ? >[] : [ - PopupMenuItem( - child: Text(username == null - ? translate("Login") - : translate("Logout") + ' ($username)'), - value: "login", - ) - ]) + + PopupMenuItem( + child: Text(username == null + ? translate("Login") + : translate("Logout") + ' ($username)'), + value: "login", + ) + ]) + [ PopupMenuItem( child: Text(translate('About') + ' RustDesk'), diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 9b0b7930a..c326dbc30 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -8,6 +8,7 @@ import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hbb/generated_bridge.dart'; +import 'package:flutter_hbb/models/ab_model.dart'; import 'package:flutter_hbb/models/chat_model.dart'; import 'package:flutter_hbb/models/file_model.dart'; import 'package:flutter_hbb/models/server_model.dart'; @@ -809,6 +810,7 @@ class FFI { late final ServerModel serverModel; late final ChatModel chatModel; late final FileModel fileModel; + late final AbModel abModel; FFI() { this.imageModel = ImageModel(WeakReference(this)); @@ -818,6 +820,7 @@ class FFI { this.serverModel = ServerModel(WeakReference(this)); // use global FFI this.chatModel = ChatModel(WeakReference(this)); this.fileModel = FileModel(WeakReference(this)); + this.abModel = AbModel(WeakReference(this)); } static FFI newFFI() { @@ -995,9 +998,17 @@ class FFI { return ffiModel.platformFFI.getByName("option", name); } + Future getLocalOption(String name) { + return bind.mainGetLocalOption(key: name); + } + + Future setLocalOption(String key, String value) { + return bind.mainSetLocalOption(key: key, value: value); + } + void setOption(String name, String value) { Map res = Map() - ..["name"] = name + ..["name"] = name ..["value"] = value; return ffiModel.platformFFI.setByName('option', jsonEncode(res)); } @@ -1087,9 +1098,13 @@ class FFI { return input; } - void setDefaultAudioInput(String input){ + void setDefaultAudioInput(String input) { setOption('audio-input', input); } + + Future> getHttpHeaders() async { + return {"Authorization": "Bearer " + await getLocalOption("access_token")}; + } } class Peer { diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index edd368509..d38dd9529 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -5,7 +5,7 @@ use std::{ }; use flutter_rust_bridge::{StreamSink, SyncReturn, ZeroCopyBuffer}; -use serde_json::{Number, Value}; +use serde_json::{json, Number, Value}; use hbb_common::ResultType; use hbb_common::{ @@ -20,9 +20,11 @@ use crate::flutter::{self, Session, SESSIONS}; use crate::start_server; use crate::ui_interface; use crate::ui_interface::{ - change_id, get_app_name, get_async_job_status, get_fav, get_lan_peers, get_license, - get_options, get_peer, get_socks, get_sound_inputs, get_version, is_ok_change_id, set_options, - set_socks, store_fav, test_if_valid_server, + change_id, check_connect_status, get_api_server, get_app_name, get_async_job_status, + get_connect_status, get_fav, get_lan_peers, get_license, get_local_option, get_options, + get_peer, get_socks, get_sound_inputs, get_version, has_rendezvous_service, is_ok_change_id, + post_request, set_local_option, set_options, set_socks, store_fav, test_if_valid_server, + using_public_server, }; fn initialize(app_dir: &str) { @@ -447,6 +449,45 @@ pub fn main_get_lan_peers() -> String { get_lan_peers() } +pub fn main_get_connect_status() -> String { + let status = get_connect_status(); + // (status_num, key_confirmed, mouse_time, id) + let mut m = serde_json::Map::new(); + m.insert("status_num".to_string(), json!(status.0)); + m.insert("key_confirmed".to_string(), json!(status.1)); + m.insert("mouse_time".to_string(), json!(status.2)); + m.insert("id".to_string(), json!(status.3)); + serde_json::to_string(&m).unwrap_or("".to_string()) +} + +pub fn main_check_connect_status() { + check_connect_status(true); +} + +pub fn main_is_using_public_server() -> bool { + using_public_server() +} + +pub fn main_has_rendezvous_service() -> bool { + has_rendezvous_service() +} + +pub fn main_get_api_server() -> String { + get_api_server() +} + +pub fn main_post_request(url: String, body: String, header: String) { + post_request(url, body, header) +} + +pub fn main_get_local_option(key: String) -> String { + get_local_option(key) +} + +pub fn main_set_local_option(key: String, value: String) { + set_local_option(key, value) +} + /// FFI for **get** commands which are idempotent. /// Return result in c string. /// diff --git a/src/ipc.rs b/src/ipc.rs index 5eabbab66..c20864700 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -1,4 +1,12 @@ -use crate::rendezvous_mediator::RendezvousMediator; +use std::{collections::HashMap, sync::atomic::Ordering}; +#[cfg(not(windows))] +use std::{fs::File, io::prelude::*}; + +use parity_tokio_ipc::{ + Connection as Conn, ConnectionClient as ConnClient, Endpoint, Incoming, SecurityAttributes, +}; +use serde_derive::{Deserialize, Serialize}; + #[cfg(not(any(target_os = "android", target_os = "ios")))] pub use clipboard::ClipbaordFile; use hbb_common::{ @@ -12,13 +20,8 @@ use hbb_common::{ tokio_util::codec::Framed, ResultType, }; -use parity_tokio_ipc::{ - Connection as Conn, ConnectionClient as ConnClient, Endpoint, Incoming, SecurityAttributes, -}; -use serde_derive::{Deserialize, Serialize}; -use std::{collections::HashMap, sync::atomic::Ordering}; -#[cfg(not(windows))] -use std::{fs::File, io::prelude::*}; + +use crate::rendezvous_mediator::RendezvousMediator; // State with timestamp, because std::time::Instant cannot be serialized #[derive(Debug, Serialize, Deserialize, Copy, Clone)] @@ -73,7 +76,7 @@ pub enum FS { WriteOffset { id: i32, file_num: i32, - offset_blk: u32 + offset_blk: u32, }, CheckDigest { id: i32, diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 7eaf938d1..86b4e9e9a 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -630,7 +630,7 @@ pub fn check_zombie(childs: Childs) { } } -fn check_connect_status(reconnect: bool) -> mpsc::UnboundedSender { +pub(crate) fn check_connect_status(reconnect: bool) -> mpsc::UnboundedSender { let (tx, rx) = mpsc::unbounded_channel::(); std::thread::spawn(move || check_connect_status_(reconnect, rx)); tx From 1eaa9ae125cd9cbf7a965dd4618ddda906fda432 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 25 Jul 2022 16:26:51 +0800 Subject: [PATCH 0118/2015] add: abModel Signed-off-by: Kingtous --- flutter/lib/models/ab_model.dart | 56 ++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 flutter/lib/models/ab_model.dart diff --git a/flutter/lib/models/ab_model.dart b/flutter/lib/models/ab_model.dart new file mode 100644 index 000000000..44357079c --- /dev/null +++ b/flutter/lib/models/ab_model.dart @@ -0,0 +1,56 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/models/model.dart'; +import 'package:http/http.dart' as http; + +class AbModel with ChangeNotifier { + var abLoading = false; + var abError = ""; + var tags = []; + var peers = []; + + WeakReference parent; + + AbModel(this.parent); + + FFI? get _ffi => parent.target; + + Future getAb() async { + abLoading = true; + notifyListeners(); + // request + final api = "${await getApiServer()}/api/ab/get"; + debugPrint("request $api with post ${await _getHeaders()}"); + final resp = await http.post(Uri.parse(api), headers: await _getHeaders()); + abLoading = false; + Map json = jsonDecode(resp.body); + if (json.containsKey('error')) { + abError = json['error']; + } else if (json.containsKey('data')) { + // {"tags":["aaa","bbb"], + // "peers":[{"id":"aa1234","username":"selfd", + // "hostname":"PC","platform":"Windows","tags":["aaa"]}]} + final data = jsonDecode(json['data']); + tags = data['tags']; + peers = data['peers']; + } + print(json); + notifyListeners(); + return resp.body; + } + + Future getApiServer() async { + return await _ffi?.bind.mainGetApiServer() ?? ""; + } + + void reset() { + tags.clear(); + peers.clear(); + notifyListeners(); + } + + Future>? _getHeaders() { + return _ffi?.getHttpHeaders(); + } +} From 6a3d527f93af7ff5173de9e3fdd9b1ee9556d80d Mon Sep 17 00:00:00 2001 From: Asura Date: Mon, 25 Jul 2022 19:30:26 -0700 Subject: [PATCH 0119/2015] Refactor: function name --- src/server/input_service.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/server/input_service.rs b/src/server/input_service.rs index d8d4287fc..c350e8d04 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -615,7 +615,7 @@ fn rdev_key_down_or_up(key: RdevKey, down_or_up: bool) { match simulate(&event_type) { Ok(()) => (), Err(_simulate_error) => { - println!("We could not send {:?}", &event_type); + log::error!("Could not send {:?}", &event_type); } } // Let ths OS catchup (at least MacOS) @@ -650,7 +650,7 @@ fn sync_status(evt: &KeyEvent) -> (bool, bool) { return (click_capslock, click_numlock); } -fn map_keyboard_map(evt: &KeyEvent) { +fn map_keyboard_mode(evt: &KeyEvent) { // map mode(1): Send keycode according to the peer platform. let (click_capslock, click_numlock) = sync_status(evt); if click_capslock { @@ -663,7 +663,7 @@ fn map_keyboard_map(evt: &KeyEvent) { return; } -fn legacy_keyboard_map(evt: &KeyEvent) { +fn legacy_keyboard_mode(evt: &KeyEvent) { let (click_capslock, click_numlock) = sync_status(evt); #[cfg(windows)] @@ -819,6 +819,7 @@ fn legacy_keyboard_map(evt: &KeyEvent) { } } + fn handle_key_(evt: &KeyEvent) { if EXITING.load(Ordering::SeqCst) { return; @@ -826,13 +827,13 @@ fn handle_key_(evt: &KeyEvent) { match evt.mode.unwrap() { KeyboardMode::Legacy => { - legacy_keyboard_map(evt); + legacy_keyboard_mode(evt); } KeyboardMode::Map => { - map_keyboard_map(evt); + map_keyboard_mode(evt); } _ => { - legacy_keyboard_map(evt); + legacy_keyboard_mode(evt); } } } From d0e55f6f814f066297dd51900794962630c95511 Mon Sep 17 00:00:00 2001 From: kingtous Date: Tue, 26 Jul 2022 17:03:19 +0800 Subject: [PATCH 0120/2015] feat: all address book logic Signed-off-by: Kingtous --- .../lib/desktop/pages/connection_page.dart | 534 ++++++++++++++---- flutter/lib/models/ab_model.dart | 94 ++- flutter/lib/models/model.dart | 8 +- flutter/pubspec.lock | 81 ++- flutter/pubspec.yaml | 1 + 5 files changed, 564 insertions(+), 154 deletions(-) diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index e29ab9b5f..aa9a71d35 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:convert'; +import 'package:contextmenu/contextmenu.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:get/get.dart'; @@ -302,22 +303,39 @@ class _ConnectionPageState extends State { return Image.asset('assets/$platform.png', height: 50); } + bool hitTag(List selectedTags, List idents) { + if (selectedTags.isEmpty) { + return true; + } + if (idents.isEmpty) { + return false; + } + for (final tag in selectedTags) { + if (!idents.contains(tag)) { + return false; + } + } + return true; + } + /// Get all the saved peers. Future getPeers({RemoteType rType = RemoteType.recently}) async { final space = 8.0; final cards = []; - var peers; + List peers; switch (rType) { case RemoteType.recently: peers = gFFI.peers(); break; case RemoteType.favorite: peers = await gFFI.bind.mainGetFav().then((peers) async { - final peersEntities = await Future.wait(peers.map((id) => gFFI.bind.mainGetPeers(id: id)).toList(growable: false)) - .then((peers_str){ + final peersEntities = await Future.wait(peers + .map((id) => gFFI.bind.mainGetPeers(id: id)) + .toList(growable: false)) + .then((peers_str) { final len = peers_str.length; final ps = List.empty(growable: true); - for(var i = 0; i< len ; i++){ + for (var i = 0; i < len; i++) { print("${peers[i]}: ${peers_str[i]}"); ps.add(Peer.fromJson(peers[i], jsonDecode(peers_str[i])['info'])); } @@ -333,7 +351,6 @@ class _ConnectionPageState extends State { }); break; case RemoteType.addressBook: - await gFFI.abModel.getAb(); peers = gFFI.abModel.peers.map((e) { return Peer.fromJson(e['id'], e); }).toList(); @@ -343,97 +360,107 @@ class _ConnectionPageState extends State { var deco = Rx(BoxDecoration( border: Border.all(color: Colors.transparent, width: 1.0), borderRadius: BorderRadius.circular(20))); - cards.add(Container( - width: 225, - height: 150, - child: Card( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20)), - child: MouseRegion( - onEnter: (evt) { - deco.value = BoxDecoration( - border: Border.all(color: Colors.blue, width: 1.0), - borderRadius: BorderRadius.circular(20)); - }, - onExit: (evt) { - deco.value = BoxDecoration( - border: Border.all(color: Colors.transparent, width: 1.0), - borderRadius: BorderRadius.circular(20)); - }, - child: Obx( - () => Container( - decoration: deco.value, - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Expanded( - child: Container( - decoration: BoxDecoration( - color: str2color('${p.id}${p.platform}', 0x7f), - borderRadius: BorderRadius.only( - topLeft: Radius.circular(20), - topRight: Radius.circular(20), - ), - ), - child: Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.center, - children: [ - Container( - padding: const EdgeInsets.all(6), - child: - getPlatformImage('${p.platform}'), - ), - Row( + cards.add(Obx( + () => Offstage( + offstage: !hitTag(gFFI.abModel.selectedTags, p.tags) && + rType == RemoteType.addressBook, + child: Container( + width: 225, + height: 150, + child: Card( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20)), + child: MouseRegion( + onEnter: (evt) { + deco.value = BoxDecoration( + border: Border.all(color: Colors.blue, width: 1.0), + borderRadius: BorderRadius.circular(20)); + }, + onExit: (evt) { + deco.value = BoxDecoration( + border: + Border.all(color: Colors.transparent, width: 1.0), + borderRadius: BorderRadius.circular(20)); + }, + child: Obx( + () => Container( + decoration: deco.value, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: Container( + decoration: BoxDecoration( + color: + str2color('${p.id}${p.platform}', 0x7f), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + ), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.center, children: [ - Expanded( - child: Tooltip( - message: - '${p.username}@${p.hostname}', - child: Text( - '${p.username}@${p.hostname}', - style: TextStyle( - color: Colors.white70, - fontSize: 12), - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, + Container( + padding: const EdgeInsets.all(6), + child: getPlatformImage( + '${p.platform}'), + ), + Row( + children: [ + Expanded( + child: Tooltip( + message: + '${p.username}@${p.hostname}', + child: Text( + '${p.username}@${p.hostname}', + style: TextStyle( + color: Colors.white70, + fontSize: 12), + textAlign: TextAlign.center, + overflow: + TextOverflow.ellipsis, + ), + ), ), - ), + ], ), ], - ), - ], - ).paddingAll(4.0), + ).paddingAll(4.0), + ), + ], ), - ], + ), ), - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text("${p.id}"), - InkWell( - child: Icon(Icons.more_vert), - onTapDown: (e) { - final x = e.globalPosition.dx; - final y = e.globalPosition.dy; - _menuPos = RelativeRect.fromLTRB(x, y, x, y); - }, - onTap: () { - showPeerMenu(context, p.id, rType); - }), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("${p.id}"), + InkWell( + child: Icon(Icons.more_vert), + onTapDown: (e) { + final x = e.globalPosition.dx; + final y = e.globalPosition.dy; + _menuPos = + RelativeRect.fromLTRB(x, y, x, y); + }, + onTap: () { + showPeerMenu(context, p.id, rType); + }), + ], + ).paddingSymmetric(vertical: 8.0, horizontal: 12.0) ], - ).paddingSymmetric(vertical: 8.0, horizontal: 12.0) - ], + ), + ), ), - ), - ), - )))); + ))), + ), + )); }); return SingleChildScrollView( child: Wrap(children: cards, spacing: space, runSpacing: space)); @@ -450,7 +477,11 @@ class _ConnectionPageState extends State { PopupMenuItem( child: Text(translate('TCP Tunneling')), value: 'tcp-tunnel'), PopupMenuItem(child: Text(translate('Rename')), value: 'rename'), - PopupMenuItem(child: Text(translate('Remove')), value: 'remove'), + rType == RemoteType.addressBook + ? PopupMenuItem( + child: Text(translate('Remove')), value: 'ab-delete') + : PopupMenuItem( + child: Text(translate('Remove')), value: 'remove'), PopupMenuItem( child: Text(translate('Unremember Password')), value: 'unremember-password'), @@ -459,9 +490,13 @@ class _ConnectionPageState extends State { items.add(PopupMenuItem( child: Text(translate('Remove from Favorites')), value: 'remove-fav')); - } else + } else if (rType != RemoteType.addressBook) { items.add(PopupMenuItem( child: Text(translate('Add to Favorites')), value: 'add-fav')); + } else { + items.add(PopupMenuItem( + child: Text(translate('Edit Tag')), value: 'ab-edit-tag')); + } var value = await showMenu( context: context, position: this._menuPos, @@ -475,7 +510,16 @@ class _ConnectionPageState extends State { }(); } else if (value == 'file') { connect(id, isFileTransfer: true); - } else if (value == 'add-fav') {} + } else if (value == 'add-fav') { + } else if (value == 'connect') { + connect(id, isFileTransfer: false); + } else if (value == 'ab-delete') { + gFFI.abModel.deletePeer(id); + await gFFI.abModel.updateAb(); + setState(() {}); + } else if (value == 'ab-edit-tag') { + abEditTag(id); + } } var svcStopped = false.obs; @@ -572,7 +616,7 @@ class _ConnectionPageState extends State { ); } else if (model.abError.isNotEmpty) { return Center( - child: CircularProgressIndicator(), + child: Text(translate("${model.abError}")), ); } else { return Offstage(); @@ -601,7 +645,21 @@ class _ConnectionPageState extends State { Text(translate('Tags')), InkWell( child: PopupMenuButton( - itemBuilder: (context) => [], + itemBuilder: (context) => [ + PopupMenuItem( + child: Text(translate("Add ID")), + value: 'add-id', + ), + PopupMenuItem( + child: Text(translate("Add Tag")), + value: 'add-tag', + ), + PopupMenuItem( + child: Text(translate("Unselect all tags")), + value: 'unset-all-tag', + ), + ], + onSelected: handleAbOp, child: Icon(Icons.more_vert_outlined)), ) ], @@ -612,9 +670,20 @@ class _ConnectionPageState extends State { height: double.infinity, decoration: BoxDecoration( border: Border.all(color: MyTheme.darkGray)), - child: Wrap( - children: - gFFI.abModel.tags.map((e) => buildTag(e)).toList(), + child: Obx( + () => Wrap( + children: gFFI.abModel.tags + .map((e) => buildTag(e, gFFI.abModel.selectedTags, + onTap: () { + // + if (gFFI.abModel.selectedTags.contains(e)) { + gFFI.abModel.selectedTags.remove(e); + } else { + gFFI.abModel.selectedTags.add(e); + } + })) + .toList(), + ), ), ).marginSymmetric(vertical: 8.0), ) @@ -622,33 +691,266 @@ class _ConnectionPageState extends State { ), ), ).marginOnly(right: 8.0), - Column( - children: [ - FutureBuilder( - future: getPeers(rType: RemoteType.addressBook), - builder: (context, snapshot) { - if (snapshot.hasData) { - return snapshot.data!; - } else { - return Center(child: CircularProgressIndicator()); - } - }), - ], + Expanded( + child: FutureBuilder( + future: getPeers(rType: RemoteType.addressBook), + builder: (context, snapshot) { + if (snapshot.hasData) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [Expanded(child: snapshot.data!)], + ); + } else if (snapshot.hasError) { + return Container( + alignment: Alignment.center, + child: Text('${snapshot.error}')); + } else { + return Container( + alignment: Alignment.center, + child: CircularProgressIndicator()); + } + }), ) ], ); } - Widget buildTag(String tagName) { - return Container( - decoration: BoxDecoration( - border: Border.all(color: MyTheme.darkGray), - borderRadius: BorderRadius.circular(10)), - margin: EdgeInsets.symmetric(horizontal: 4.0, vertical: 8.0), - padding: EdgeInsets.symmetric(vertical: 2.0, horizontal: 8.0), - child: Text(tagName), + Widget buildTag(String tagName, RxList rxTags, {Function()? onTap}) { + return ContextMenuArea( + width: 100, + builder: (context) => [ + ListTile( + title: Text(translate("Delete")), + onTap: () { + gFFI.abModel.deleteTag(tagName); + gFFI.abModel.updateAb(); + Future.delayed(Duration.zero, () => Get.back()); + }, + ) + ], + child: GestureDetector( + onTap: onTap, + child: Obx( + () => Container( + decoration: BoxDecoration( + color: rxTags.contains(tagName) ? Colors.blue : null, + border: Border.all(color: MyTheme.darkGray), + borderRadius: BorderRadius.circular(10)), + margin: EdgeInsets.symmetric(horizontal: 4.0, vertical: 8.0), + padding: EdgeInsets.symmetric(vertical: 2.0, horizontal: 8.0), + child: Text( + tagName, + style: TextStyle( + color: rxTags.contains(tagName) ? MyTheme.white : null), + ), + ), + ), + ), ); } + + /// tag operation + void handleAbOp(String value) { + if (value == 'add-id') { + abAddId(); + } else if (value == 'add-tag') { + abAddTag(); + } else if (value == 'unset-all-tag') { + gFFI.abModel.unsetSelectedTags(); + } + } + + void abAddId() async { + var field = ""; + var msg = ""; + var isInProgress = false; + DialogManager.show((setState, close) { + return CustomAlertDialog( + title: Text(translate("Add ID")), + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(translate("whitelist_sep")), + SizedBox( + height: 8.0, + ), + Row( + children: [ + Expanded( + child: TextField( + onChanged: (s) { + field = s; + }, + maxLines: null, + decoration: InputDecoration( + border: OutlineInputBorder(), + errorText: msg.isEmpty ? null : translate(msg), + ), + controller: TextEditingController(text: field), + ), + ), + ], + ), + SizedBox( + height: 4.0, + ), + Offstage(offstage: !isInProgress, child: LinearProgressIndicator()) + ], + ), + actions: [ + TextButton( + onPressed: () { + close(); + }, + child: Text(translate("Cancel"))), + TextButton( + onPressed: () async { + setState(() { + msg = ""; + isInProgress = true; + }); + field = field.trim(); + if (field.isEmpty) { + // pass + } else { + final ids = field.trim().split(RegExp(r"[\s,;\n]+")); + field = ids.join(','); + for (final newId in ids) { + if (gFFI.abModel.idContainBy(newId)) { + continue; + } + gFFI.abModel.addId(newId); + } + await gFFI.abModel.updateAb(); + this.setState(() {}); + // final currentPeers + } + close(); + }, + child: Text(translate("OK"))), + ], + ); + }); + } + + void abAddTag() async { + var field = ""; + var msg = ""; + var isInProgress = false; + DialogManager.show((setState, close) { + return CustomAlertDialog( + title: Text(translate("Add Tag")), + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(translate("whitelist_sep")), + SizedBox( + height: 8.0, + ), + Row( + children: [ + Expanded( + child: TextField( + onChanged: (s) { + field = s; + }, + maxLines: null, + decoration: InputDecoration( + border: OutlineInputBorder(), + errorText: msg.isEmpty ? null : translate(msg), + ), + controller: TextEditingController(text: field), + ), + ), + ], + ), + SizedBox( + height: 4.0, + ), + Offstage(offstage: !isInProgress, child: LinearProgressIndicator()) + ], + ), + actions: [ + TextButton( + onPressed: () { + close(); + }, + child: Text(translate("Cancel"))), + TextButton( + onPressed: () async { + setState(() { + msg = ""; + isInProgress = true; + }); + field = field.trim(); + if (field.isEmpty) { + // pass + } else { + final tags = field.trim().split(RegExp(r"[\s,;\n]+")); + field = tags.join(','); + for (final tag in tags) { + gFFI.abModel.addTag(tag); + } + await gFFI.abModel.updateAb(); + // final currentPeers + } + close(); + }, + child: Text(translate("OK"))), + ], + ); + }); + } + + void abEditTag(String id) { + var isInProgress = false; + + final tags = List.of(gFFI.abModel.tags); + var selectedTag = gFFI.abModel.getPeerTags(id).obs; + + DialogManager.show((setState, close) { + return CustomAlertDialog( + title: Text(translate("Edit Tag")), + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + child: Wrap( + children: tags + .map((e) => buildTag(e, selectedTag, onTap: () { + if (selectedTag.contains(e)) { + selectedTag.remove(e); + } else { + selectedTag.add(e); + } + })) + .toList(growable: false), + ), + ), + Offstage(offstage: !isInProgress, child: LinearProgressIndicator()) + ], + ), + actions: [ + TextButton( + onPressed: () { + close(); + }, + child: Text(translate("Cancel"))), + TextButton( + onPressed: () async { + setState(() { + isInProgress = true; + }); + gFFI.abModel.changeTagForPeer(id, selectedTag); + await gFFI.abModel.updateAb(); + close(); + }, + child: Text(translate("OK"))), + ], + ); + }); + } } class AddressBookPage extends StatefulWidget { diff --git a/flutter/lib/models/ab_model.dart b/flutter/lib/models/ab_model.dart index 44357079c..12d48bbb1 100644 --- a/flutter/lib/models/ab_model.dart +++ b/flutter/lib/models/ab_model.dart @@ -2,13 +2,16 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/models/model.dart'; +import 'package:get/get.dart'; import 'package:http/http.dart' as http; class AbModel with ChangeNotifier { var abLoading = false; var abError = ""; - var tags = []; - var peers = []; + var tags = [].obs; + var peers = [].obs; + + var selectedTags = List.empty(growable: true).obs; WeakReference parent; @@ -21,7 +24,6 @@ class AbModel with ChangeNotifier { notifyListeners(); // request final api = "${await getApiServer()}/api/ab/get"; - debugPrint("request $api with post ${await _getHeaders()}"); final resp = await http.post(Uri.parse(api), headers: await _getHeaders()); abLoading = false; Map json = jsonDecode(resp.body); @@ -32,8 +34,8 @@ class AbModel with ChangeNotifier { // "peers":[{"id":"aa1234","username":"selfd", // "hostname":"PC","platform":"Windows","tags":["aaa"]}]} final data = jsonDecode(json['data']); - tags = data['tags']; - peers = data['peers']; + tags.value = data['tags']; + peers.value = data['peers']; } print(json); notifyListeners(); @@ -53,4 +55,86 @@ class AbModel with ChangeNotifier { Future>? _getHeaders() { return _ffi?.getHttpHeaders(); } + + /// + void addId(String id) async { + if (idContainBy(id)) { + return; + } + peers.add({"id": id}); + notifyListeners(); + } + + void addTag(String tag) async { + if (tagContainBy(tag)) { + return; + } + tags.add(tag); + notifyListeners(); + } + + void changeTagForPeer(String id, List tags) { + final it = peers.where((element) => element['id'] == id); + if (it.isEmpty) { + return; + } + it.first['tags'] = tags; + } + + Future updateAb() async { + abLoading = true; + notifyListeners(); + final api = "${await getApiServer()}/api/ab"; + var authHeaders = await _getHeaders() ?? Map(); + authHeaders['Content-Type'] = "application/json"; + final body = jsonEncode({ + "data": jsonEncode({"tags": tags, "peers": peers}) + }); + final resp = + await http.post(Uri.parse(api), headers: authHeaders, body: body); + abLoading = false; + await getAb(); + notifyListeners(); + debugPrint("resp: ${resp.body}"); + } + + bool idContainBy(String id) { + return peers.where((element) => element['id'] == id).isNotEmpty; + } + + bool tagContainBy(String tag) { + return tags.where((element) => element == tag).isNotEmpty; + } + + void deletePeer(String id) { + peers.removeWhere((element) => element['id'] == id); + notifyListeners(); + } + + void deleteTag(String tag) { + tags.removeWhere((element) => element == tag); + for (var peer in peers) { + if (peer['tags'] == null) { + continue; + } + if (((peer['tags']) as List).contains(tag)) { + ((peer['tags']) as List).remove(tag); + } + } + notifyListeners(); + } + + void unsetSelectedTags() { + selectedTags.clear(); + notifyListeners(); + } + + List getPeerTags(String id) { + final it = peers.where((p0) => p0['id'] == id); + if (it.isEmpty) { + return []; + } else { + return it.first['tags'] ?? []; + } + } } diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index c326dbc30..f401cf422 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -1112,12 +1112,14 @@ class Peer { final String username; final String hostname; final String platform; + final List tags; Peer.fromJson(String id, Map json) : id = id, - username = json['username'], - hostname = json['hostname'], - platform = json['platform']; + username = json['username'] ?? '', + hostname = json['hostname'] ?? '', + platform = json['platform'] ?? '', + tags = json['tags'] ?? []; } class Display { diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index a798799f1..364bad74d 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -7,21 +7,35 @@ packages: name: _fe_analyzer_shared url: "https://pub.flutter-io.cn" source: hosted - version: "40.0.0" + version: "43.0.0" + after_layout: + dependency: transitive + description: + name: after_layout + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.0" analyzer: dependency: transitive description: name: analyzer url: "https://pub.flutter-io.cn" source: hosted - version: "4.1.0" + version: "4.3.1" + animations: + dependency: transitive + description: + name: animations + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.3" archive: dependency: transitive description: name: archive url: "https://pub.flutter-io.cn" source: hosted - version: "3.3.0" + version: "3.3.1" args: dependency: transitive description: @@ -56,7 +70,7 @@ packages: name: build_config url: "https://pub.flutter-io.cn" source: hosted - version: "1.0.0" + version: "1.1.0" build_daemon: dependency: transitive description: @@ -77,7 +91,7 @@ packages: name: build_runner url: "https://pub.flutter-io.cn" source: hosted - version: "2.1.11" + version: "2.2.0" build_runner_core: dependency: transitive description: @@ -98,7 +112,7 @@ packages: name: built_value url: "https://pub.flutter-io.cn" source: hosted - version: "8.3.2" + version: "8.4.0" characters: dependency: transitive description: @@ -141,6 +155,13 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.16.0" + contextmenu: + dependency: "direct main" + description: + name: contextmenu + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.0" convert: dependency: transitive description: @@ -282,42 +303,42 @@ packages: name: firebase_analytics url: "https://pub.flutter-io.cn" source: hosted - version: "9.1.9" + version: "9.3.0" firebase_analytics_platform_interface: dependency: transitive description: name: firebase_analytics_platform_interface url: "https://pub.flutter-io.cn" source: hosted - version: "3.1.7" + version: "3.3.0" firebase_analytics_web: dependency: transitive description: name: firebase_analytics_web url: "https://pub.flutter-io.cn" source: hosted - version: "0.4.0+14" + version: "0.4.2" firebase_core: dependency: transitive description: name: firebase_core url: "https://pub.flutter-io.cn" source: hosted - version: "1.17.1" + version: "1.20.0" firebase_core_platform_interface: dependency: transitive description: name: firebase_core_platform_interface url: "https://pub.flutter-io.cn" source: hosted - version: "4.4.0" + version: "4.5.0" firebase_core_web: dependency: transitive description: name: firebase_core_web url: "https://pub.flutter-io.cn" source: hosted - version: "1.6.4" + version: "1.7.1" fixnum: dependency: transitive description: @@ -357,7 +378,7 @@ packages: name: flutter_plugin_android_lifecycle url: "https://pub.flutter-io.cn" source: hosted - version: "2.0.6" + version: "2.0.7" flutter_rust_bridge: dependency: "direct main" description: @@ -373,7 +394,7 @@ packages: name: flutter_smart_dialog url: "https://pub.flutter-io.cn" source: hosted - version: "4.5.3+2" + version: "4.5.3+7" flutter_test: dependency: "direct dev" description: flutter @@ -390,14 +411,14 @@ packages: name: freezed url: "https://pub.flutter-io.cn" source: hosted - version: "2.0.3+1" + version: "2.1.0+1" freezed_annotation: dependency: "direct main" description: name: freezed_annotation url: "https://pub.flutter-io.cn" source: hosted - version: "2.0.3" + version: "2.1.0" frontend_server_client: dependency: transitive description: @@ -418,7 +439,7 @@ packages: name: glob url: "https://pub.flutter-io.cn" source: hosted - version: "2.0.2" + version: "2.1.0" graphs: dependency: transitive description: @@ -439,7 +460,7 @@ packages: name: http_multi_server url: "https://pub.flutter-io.cn" source: hosted - version: "3.2.0" + version: "3.2.1" http_parser: dependency: transitive description: @@ -467,7 +488,7 @@ packages: name: image_picker_android url: "https://pub.flutter-io.cn" source: hosted - version: "0.8.5" + version: "0.8.5+1" image_picker_for_web: dependency: transitive description: @@ -481,7 +502,7 @@ packages: name: image_picker_ios url: "https://pub.flutter-io.cn" source: hosted - version: "0.8.5+5" + version: "0.8.5+6" image_picker_platform_interface: dependency: transitive description: @@ -516,7 +537,7 @@ packages: name: json_annotation url: "https://pub.flutter-io.cn" source: hosted - version: "4.5.0" + version: "4.6.0" logging: dependency: transitive description: @@ -572,7 +593,7 @@ packages: name: package_config url: "https://pub.flutter-io.cn" source: hosted - version: "2.0.2" + version: "2.1.0" package_info_plus: dependency: "direct main" description: @@ -635,14 +656,14 @@ packages: name: path_provider_android url: "https://pub.flutter-io.cn" source: hosted - version: "2.0.14" + version: "2.0.16" path_provider_ios: dependency: transitive description: name: path_provider_ios url: "https://pub.flutter-io.cn" source: hosted - version: "2.0.9" + version: "2.0.10" path_provider_linux: dependency: transitive description: @@ -705,7 +726,7 @@ packages: name: pool url: "https://pub.flutter-io.cn" source: hosted - version: "1.5.0" + version: "1.5.1" process: dependency: transitive description: @@ -819,14 +840,14 @@ packages: name: shelf url: "https://pub.flutter-io.cn" source: hosted - version: "1.3.0" + version: "1.3.1" shelf_web_socket: dependency: transitive description: name: shelf_web_socket url: "https://pub.flutter-io.cn" source: hosted - version: "1.0.1" + version: "1.0.2" shortid: dependency: transitive description: @@ -943,7 +964,7 @@ packages: name: url_launcher url: "https://pub.flutter-io.cn" source: hosted - version: "6.1.3" + version: "6.1.5" url_launcher_android: dependency: transitive description: @@ -978,14 +999,14 @@ packages: name: url_launcher_platform_interface url: "https://pub.flutter-io.cn" source: hosted - version: "2.0.5" + version: "2.1.0" url_launcher_web: dependency: transitive description: name: url_launcher_web url: "https://pub.flutter-io.cn" source: hosted - version: "2.0.11" + version: "2.0.12" url_launcher_windows: dependency: transitive description: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 4bbc2f3c5..4a2b64043 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -67,6 +67,7 @@ dependencies: freezed_annotation: ^2.0.3 tray_manager: 0.1.7 get: ^4.6.5 + contextmenu: ^3.0.0 dev_dependencies: flutter_launcher_icons: ^0.9.1 From 8a3da4eb417ecfea3f177303b6eebc284291e45f Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 26 Jul 2022 17:14:52 +0800 Subject: [PATCH 0121/2015] feat: retry logic Signed-off-by: Kingtous --- .../lib/desktop/pages/connection_page.dart | 24 +++++++++++++- flutter/lib/models/ab_model.dart | 31 ++++++++++--------- 2 files changed, 40 insertions(+), 15 deletions(-) diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index aa9a71d35..56523bee1 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -609,6 +609,18 @@ class _ConnectionPageState extends State { builder: (context, snapshot) { if (snapshot.hasData) { return _buildAddressBook(context); + } else if (snapshot.hasError) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(translate("${snapshot.error}")), + TextButton( + onPressed: () { + setState(() {}); + }, + child: Text(translate("Retry"))) + ], + ); } else { if (model.abLoading) { return Center( @@ -616,7 +628,17 @@ class _ConnectionPageState extends State { ); } else if (model.abError.isNotEmpty) { return Center( - child: Text(translate("${model.abError}")), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(translate("${model.abError}")), + TextButton( + onPressed: () { + setState(() {}); + }, + child: Text(translate("Retry"))) + ], + ), ); } else { return Offstage(); diff --git a/flutter/lib/models/ab_model.dart b/flutter/lib/models/ab_model.dart index 12d48bbb1..4350b6b05 100644 --- a/flutter/lib/models/ab_model.dart +++ b/flutter/lib/models/ab_model.dart @@ -24,22 +24,25 @@ class AbModel with ChangeNotifier { notifyListeners(); // request final api = "${await getApiServer()}/api/ab/get"; - final resp = await http.post(Uri.parse(api), headers: await _getHeaders()); - abLoading = false; - Map json = jsonDecode(resp.body); - if (json.containsKey('error')) { - abError = json['error']; - } else if (json.containsKey('data')) { - // {"tags":["aaa","bbb"], - // "peers":[{"id":"aa1234","username":"selfd", - // "hostname":"PC","platform":"Windows","tags":["aaa"]}]} - final data = jsonDecode(json['data']); - tags.value = data['tags']; - peers.value = data['peers']; + try { + final resp = + await http.post(Uri.parse(api), headers: await _getHeaders()); + Map json = jsonDecode(resp.body); + if (json.containsKey('error')) { + abError = json['error']; + } else if (json.containsKey('data')) { + final data = jsonDecode(json['data']); + tags.value = data['tags']; + peers.value = data['peers']; + } + return resp.body; + } catch (err) { + abError = err.toString(); + } finally { + abLoading = false; } - print(json); notifyListeners(); - return resp.body; + return null; } Future getApiServer() async { From 06cb05f7963c0f9d1686a8ee4e13ab3cd6d5b5f1 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Wed, 27 Jul 2022 14:29:47 +0800 Subject: [PATCH 0122/2015] feat: user login/logout with UserModel Signed-off-by: Kingtous --- .../lib/desktop/pages/connection_page.dart | 30 +- .../lib/desktop/pages/desktop_home_page.dart | 427 +++++++++++++----- flutter/lib/main.dart | 3 + flutter/lib/models/ab_model.dart | 6 + flutter/lib/models/model.dart | 3 + flutter/lib/models/user_model.dart | 83 ++++ src/flutter_ffi.rs | 16 +- 7 files changed, 424 insertions(+), 144 deletions(-) create mode 100644 flutter/lib/models/user_model.dart diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 56523bee1..14a1cc4e7 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:contextmenu/contextmenu.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; @@ -588,7 +589,13 @@ class _ConnectionPageState extends State { svcIsUsingPublicServer.value = await gFFI.bind.mainIsUsingPublicServer(); } - handleLogin() {} + handleLogin() { + loginDialog().then((success) { + if (success) { + setState(() {}); + } + }); + } Future buildAddressBook(BuildContext context) async { final token = await gFFI.getLocalOption('access_token'); @@ -975,27 +982,6 @@ class _ConnectionPageState extends State { } } -class AddressBookPage extends StatefulWidget { - const AddressBookPage({Key? key}) : super(key: key); - - @override - State createState() => _AddressBookPageState(); -} - -class _AddressBookPageState extends State { - @override - void initState() { - // TODO: implement initState - final ab = gFFI.abModel.getAb(); - super.initState(); - } - - @override - Widget build(BuildContext context) { - return Container(); - } -} - class WebMenu extends StatefulWidget { @override _WebMenuState createState() => _WebMenuState(); diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 2152a60c3..47c066c9c 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:convert'; import 'dart:io'; @@ -110,68 +111,18 @@ class _DesktopHomePageState extends State with TrayListener { style: TextStyle( fontSize: 18, fontWeight: FontWeight.w500), ), - PopupMenuButton( - padding: EdgeInsets.all(4.0), - itemBuilder: (context) => [ - genEnablePopupMenuItem( - translate("Enable Keyboard/Mouse"), - 'enable-keyboard', - ), - genEnablePopupMenuItem( - translate("Enable Clipboard"), - 'enable-clipboard', - ), - genEnablePopupMenuItem( - translate("Enable File Transfer"), - 'enable-file-transfer', - ), - genEnablePopupMenuItem( - translate("Enable TCP Tunneling"), - 'enable-tunnel', - ), - genAudioInputPopupMenuItem(), - // TODO: Audio Input - PopupMenuItem( - child: Text(translate("ID/Relay Server")), - value: 'custom-server', - ), - PopupMenuItem( - child: Text(translate("IP Whitelisting")), - value: 'whitelist', - ), - PopupMenuItem( - child: Text(translate("Socks5 Proxy")), - value: 'socks5-proxy', - ), - // sep - genEnablePopupMenuItem( - translate("Enable Service"), - 'stop-service', - ), - // TODO: direct server - genEnablePopupMenuItem( - translate("Always connected via relay"), - 'allow-always-relay', - ), - genEnablePopupMenuItem( - translate("Start ID/relay service"), - 'stop-rendezvous-service', - ), - PopupMenuItem( - child: Text(translate("Change ID")), - value: 'change-id', - ), - genEnablePopupMenuItem( - translate("Dark Theme"), - 'allow-darktheme', - ), - PopupMenuItem( - child: Text(translate("About")), - value: 'about', - ), - ], - onSelected: onSelectMenu, - ) + FutureBuilder( + future: buildPopupMenu(context), + builder: (context, snapshot) { + if (snapshot.hasError) { + print("${snapshot.error}"); + } + if (snapshot.hasData) { + return snapshot.data!; + } else { + return Offstage(); + } + }) ], ), TextFormField( @@ -189,6 +140,91 @@ class _DesktopHomePageState extends State with TrayListener { ); } + Future buildPopupMenu(BuildContext context) async { + var position; + return GestureDetector( + onTapDown: (detail) { + final x = detail.globalPosition.dx; + final y = detail.globalPosition.dy; + position = RelativeRect.fromLTRB(x, y, x, y); + }, + onTap: () async { + final userName = await gFFI.userModel.getUserName(); + var menu = [ + genEnablePopupMenuItem( + translate("Enable Keyboard/Mouse"), + 'enable-keyboard', + ), + genEnablePopupMenuItem( + translate("Enable Clipboard"), + 'enable-clipboard', + ), + genEnablePopupMenuItem( + translate("Enable File Transfer"), + 'enable-file-transfer', + ), + genEnablePopupMenuItem( + translate("Enable TCP Tunneling"), + 'enable-tunnel', + ), + genAudioInputPopupMenuItem(), + PopupMenuItem( + child: Text(translate("ID/Relay Server")), + value: 'custom-server', + ), + PopupMenuItem( + child: Text(translate("IP Whitelisting")), + value: 'whitelist', + ), + PopupMenuItem( + child: Text(translate("Socks5 Proxy")), + value: 'socks5-proxy', + ), + // sep + genEnablePopupMenuItem( + translate("Enable Service"), + 'stop-service', + ), + // TODO: direct server + genEnablePopupMenuItem( + translate("Always connected via relay"), + 'allow-always-relay', + ), + genEnablePopupMenuItem( + translate("Start ID/relay service"), + 'stop-rendezvous-service', + ), + userName.isEmpty + ? PopupMenuItem( + child: Text(translate("Login")), + value: 'login', + ) + : PopupMenuItem( + child: Text("${translate("Logout")} $userName"), + value: 'logout', + ), + PopupMenuItem( + child: Text(translate("Change ID")), + value: 'change-id', + ), + genEnablePopupMenuItem( + translate("Dark Theme"), + 'allow-darktheme', + ), + PopupMenuItem( + child: Text(translate("About")), + value: 'about', + ), + ]; + final v = + await showMenu(context: context, position: position, items: menu); + if (v != null) { + onSelectMenu(v); + } + }, + child: Icon(Icons.more_vert_outlined)); + } + buildPasswordBoard(BuildContext context) { final model = gFFI.serverModel; return Container( @@ -259,15 +295,15 @@ class _DesktopHomePageState extends State with TrayListener { Text(translate("Control Remote Desktop")), Form( child: Column( - children: [ - TextFormField( - controller: TextEditingController(), - inputFormatters: [ - FilteringTextInputFormatter.allow(RegExp(r"[0-9]")) + children: [ + TextFormField( + controller: TextEditingController(), + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp(r"[0-9]")) + ], + ) ], - ) - ], - )) + )) ], ), ); @@ -284,7 +320,7 @@ class _DesktopHomePageState extends State with TrayListener { case "quit": exit(0); case "show": - // windowManager.show(); + // windowManager.show(); break; default: break; @@ -323,6 +359,10 @@ class _DesktopHomePageState extends State with TrayListener { changeSocks5Proxy(); } else if (value == "about") { about(); + } else if (value == "logout") { + logOut(); + } else if (value == "login") { + login(); } } @@ -348,7 +388,7 @@ class _DesktopHomePageState extends State with TrayListener { return isPositive ? TextStyle() : TextStyle( - color: Colors.redAccent, decoration: TextDecoration.lineThrough); + color: Colors.redAccent, decoration: TextDecoration.lineThrough); } PopupMenuItem genAudioInputPopupMenuItem() { @@ -366,23 +406,23 @@ class _DesktopHomePageState extends State with TrayListener { } var inputList = inputs .map((e) => PopupMenuItem( - child: Row( - children: [ - Obx(() => Offstage( - offstage: defaultInput.value != e, - child: Icon(Icons.check))), - Expanded( - child: Tooltip( - message: e, - child: Text( - "$e", - maxLines: 1, - overflow: TextOverflow.ellipsis, - ))), - ], - ), - value: e, - )) + child: Row( + children: [ + Obx(() => Offstage( + offstage: defaultInput.value != e, + child: Icon(Icons.check))), + Expanded( + child: Tooltip( + message: e, + child: Text( + "$e", + maxLines: 1, + overflow: TextOverflow.ellipsis, + ))), + ], + ), + value: e, + )) .toList(); inputList.insert( 0, @@ -503,7 +543,7 @@ class _DesktopHomePageState extends State with TrayListener { void changeServer() async { Map oldOptions = - jsonDecode(await gFFI.bind.mainGetOptions()); + jsonDecode(await gFFI.bind.mainGetOptions()); print("${oldOptions}"); String idServer = oldOptions['custom-rendezvous-server'] ?? ""; var idServerMsg = ""; @@ -542,7 +582,7 @@ class _DesktopHomePageState extends State with TrayListener { decoration: InputDecoration( border: OutlineInputBorder(), errorText: - idServerMsg.isNotEmpty ? idServerMsg : null), + idServerMsg.isNotEmpty ? idServerMsg : null), controller: TextEditingController(text: idServer), ), ), @@ -595,7 +635,7 @@ class _DesktopHomePageState extends State with TrayListener { decoration: InputDecoration( border: OutlineInputBorder(), errorText: - apiServerMsg.isNotEmpty ? apiServerMsg : null), + apiServerMsg.isNotEmpty ? apiServerMsg : null), controller: TextEditingController(text: apiServer), ), ), @@ -711,7 +751,7 @@ class _DesktopHomePageState extends State with TrayListener { void changeWhiteList() async { Map oldOptions = - jsonDecode(await gFFI.bind.mainGetOptions()); + jsonDecode(await gFFI.bind.mainGetOptions()); var newWhiteList = ((oldOptions['whitelist'] ?? "") as String).split(','); var newWhiteListField = newWhiteList.join('\n'); var msg = ""; @@ -767,7 +807,7 @@ class _DesktopHomePageState extends State with TrayListener { // pass } else { final ips = - newWhiteListField.trim().split(RegExp(r"[\s,;\n]+")); + newWhiteListField.trim().split(RegExp(r"[\s,;\n]+")); // test ip final ipMatch = RegExp(r"^\d+\.\d+\.\d+\.\d+$"); for (final ip in ips) { @@ -832,8 +872,7 @@ class _DesktopHomePageState extends State with TrayListener { }, decoration: InputDecoration( border: OutlineInputBorder(), - errorText: - proxyMsg.isNotEmpty ? proxyMsg : null), + errorText: proxyMsg.isNotEmpty ? proxyMsg : null), controller: TextEditingController(text: proxy), ), ), @@ -857,8 +896,8 @@ class _DesktopHomePageState extends State with TrayListener { username = s; }, decoration: InputDecoration( - border: OutlineInputBorder(), - ), + border: OutlineInputBorder(), + ), controller: TextEditingController(text: username), ), ), @@ -882,8 +921,8 @@ class _DesktopHomePageState extends State with TrayListener { password = s; }, decoration: InputDecoration( - border: OutlineInputBorder(), - ), + border: OutlineInputBorder(), + ), controller: TextEditingController(text: password), ), ), @@ -941,9 +980,7 @@ class _DesktopHomePageState extends State with TrayListener { final appName = await gFFI.bind.mainGetAppName(); final license = await gFFI.bind.mainGetLicense(); final version = await gFFI.bind.mainGetVersion(); - final linkStyle = TextStyle( - decoration: TextDecoration.underline - ); + final linkStyle = TextStyle(decoration: TextDecoration.underline); DialogManager.show((setState, close) { return CustomAlertDialog( title: Text("About $appName"), @@ -960,16 +997,20 @@ class _DesktopHomePageState extends State with TrayListener { onTap: () { launchUrlString("https://rustdesk.com/privacy"); }, - child: Text("Privacy Statement", style: linkStyle,).marginSymmetric(vertical: 4.0)), + child: Text( + "Privacy Statement", + style: linkStyle, + ).marginSymmetric(vertical: 4.0)), InkWell( - onTap: () { - launchUrlString("https://rustdesk.com"); - } - ,child: Text("Website",style: linkStyle,).marginSymmetric(vertical: 4.0)), + onTap: () { + launchUrlString("https://rustdesk.com"); + }, + child: Text( + "Website", + style: linkStyle, + ).marginSymmetric(vertical: 4.0)), Container( - decoration: BoxDecoration( - color: Color(0xFF2c8cff) - ), + decoration: BoxDecoration(color: Color(0xFF2c8cff)), padding: EdgeInsets.symmetric(vertical: 24, horizontal: 8), child: Row( children: [ @@ -977,13 +1018,16 @@ class _DesktopHomePageState extends State with TrayListener { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text("Copyright © 2022 Purslane Ltd.\n$license", style: TextStyle( - color: Colors.white - ),), - Text("Made with heart in this chaotic world!", style: TextStyle( - fontWeight: FontWeight.w800, - color: Colors.white - ),) + Text( + "Copyright © 2022 Purslane Ltd.\n$license", + style: TextStyle(color: Colors.white), + ), + Text( + "Made with heart in this chaotic world!", + style: TextStyle( + fontWeight: FontWeight.w800, + color: Colors.white), + ) ], ), ), @@ -1003,4 +1047,151 @@ class _DesktopHomePageState extends State with TrayListener { ); }); } + + void login() { + loginDialog().then((success) { + if (success) { + // refresh frame + setState(() {}); + } + }); + } + + void logOut() { + gFFI.userModel.logOut().then((_) => {setState(() {})}); + } } + +/// common login dialog for desktop +/// call this directly +Future loginDialog() async { + String userName = ""; + var userNameMsg = ""; + String pass = ""; + var passMsg = ""; + + var isInProgress = false; + var completer = Completer(); + DialogManager.show((setState, close) { + return CustomAlertDialog( + title: Text(translate("Login")), + content: ConstrainedBox( + constraints: BoxConstraints(minWidth: 500), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: 8.0, + ), + Row( + children: [ + ConstrainedBox( + constraints: BoxConstraints(minWidth: 100), + child: Text( + "${translate('Username')}:", + textAlign: TextAlign.start, + ).marginOnly(bottom: 16.0)), + SizedBox( + width: 24.0, + ), + Expanded( + child: TextField( + onChanged: (s) { + userName = s; + }, + decoration: InputDecoration( + border: OutlineInputBorder(), + errorText: userNameMsg.isNotEmpty ? userNameMsg : null), + controller: TextEditingController(text: userName), + ), + ), + ], + ), + SizedBox( + height: 8.0, + ), + Row( + children: [ + ConstrainedBox( + constraints: BoxConstraints(minWidth: 100), + child: Text("${translate('Password')}:") + .marginOnly(bottom: 16.0)), + SizedBox( + width: 24.0, + ), + Expanded( + child: TextField( + obscureText: true, + onChanged: (s) { + pass = s; + }, + decoration: InputDecoration( + border: OutlineInputBorder(), + errorText: passMsg.isNotEmpty ? passMsg : null), + controller: TextEditingController(text: pass), + ), + ), + ], + ), + SizedBox( + height: 4.0, + ), + Offstage(offstage: !isInProgress, child: LinearProgressIndicator()) + ], + ), + ), + actions: [ + TextButton( + onPressed: () { + completer.complete(false); + close(); + }, + child: Text(translate("Cancel"))), + TextButton( + onPressed: () async { + setState(() { + userNameMsg = ""; + passMsg = ""; + isInProgress = true; + }); + final cancel = () { + setState(() { + isInProgress = false; + }); + }; + userName = userName; + pass = pass; + if (userName.isEmpty) { + userNameMsg = translate("Username missed"); + cancel(); + return; + } + if (pass.isEmpty) { + passMsg = translate("Password missed"); + cancel(); + return; + } + try { + final resp = await gFFI.userModel.login(userName, pass); + if (resp.containsKey('error')) { + passMsg = resp['error']; + cancel(); + return; + } + // {access_token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJndWlkIjoiMDFkZjQ2ZjgtZjg3OS00MDE0LTk5Y2QtMGMwYzM2MmViZGJlIiwiZXhwIjoxNjYxNDg2NzYwfQ.GZpe1oI8TfM5yTYNrpcwbI599P4Z_-b2GmnwNl2Lr-w, + // token_type: Bearer, user: {id: , name: admin, email: null, note: null, status: null, grp: null, is_admin: true}} + debugPrint("$resp"); + completer.complete(true); + } catch (err) { + print(err.toString()); + cancel(); + return; + } + close(); + }, + child: Text(translate("OK"))), + ], + ); + }); + return completer.future; +} \ No newline at end of file diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 322d9f300..bb6684438 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -8,6 +8,7 @@ import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/route_manager.dart'; import 'package:provider/provider.dart'; + // import 'package:window_manager/window_manager.dart'; import 'common.dart'; @@ -77,6 +78,8 @@ class App extends StatelessWidget { ChangeNotifierProvider.value(value: gFFI.imageModel), ChangeNotifierProvider.value(value: gFFI.cursorModel), ChangeNotifierProvider.value(value: gFFI.canvasModel), + ChangeNotifierProvider.value(value: gFFI.abModel), + ChangeNotifierProvider.value(value: gFFI.userModel), ], child: GetMaterialApp( navigatorKey: globalKey, diff --git a/flutter/lib/models/ab_model.dart b/flutter/lib/models/ab_model.dart index 4350b6b05..165e3d8d1 100644 --- a/flutter/lib/models/ab_model.dart +++ b/flutter/lib/models/ab_model.dart @@ -140,4 +140,10 @@ class AbModel with ChangeNotifier { return it.first['tags'] ?? []; } } + + void clear() { + peers.clear(); + tags.clear(); + notifyListeners(); + } } diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index f401cf422..fa8210618 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -12,6 +12,7 @@ import 'package:flutter_hbb/models/ab_model.dart'; import 'package:flutter_hbb/models/chat_model.dart'; import 'package:flutter_hbb/models/file_model.dart'; import 'package:flutter_hbb/models/server_model.dart'; +import 'package:flutter_hbb/models/user_model.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:tuple/tuple.dart'; @@ -811,6 +812,7 @@ class FFI { late final ChatModel chatModel; late final FileModel fileModel; late final AbModel abModel; + late final UserModel userModel; FFI() { this.imageModel = ImageModel(WeakReference(this)); @@ -821,6 +823,7 @@ class FFI { this.chatModel = ChatModel(WeakReference(this)); this.fileModel = FileModel(WeakReference(this)); this.abModel = AbModel(WeakReference(this)); + this.userModel = UserModel(WeakReference(this)); } static FFI newFFI() { diff --git a/flutter/lib/models/user_model.dart b/flutter/lib/models/user_model.dart new file mode 100644 index 000000000..a842ec36e --- /dev/null +++ b/flutter/lib/models/user_model.dart @@ -0,0 +1,83 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:http/http.dart' as http; + +import 'model.dart'; + +class UserModel extends ChangeNotifier { + var userName = "".obs; + WeakReference parent; + + UserModel(this.parent); + + Future getUserName() async { + if (userName.isNotEmpty) { + return userName.value; + } + final userInfo = + await parent.target?.bind.mainGetLocalOption(key: 'user_info') ?? "{}"; + if (userInfo.trim().isEmpty) { + return ""; + } + final m = jsonDecode(userInfo); + userName.value = m['name'] ?? ''; + return userName.value; + } + + Future logOut() async { + debugPrint("start logout"); + final bind = parent.target?.bind; + if (bind == null) { + return; + } + final url = await bind.mainGetApiServer(); + final _ = await http.post(Uri.parse("$url/api/logout"), + body: { + "id": await bind.mainGetMyId(), + "uuid": await bind.mainGetUuid(), + }, + headers: await _getHeaders()); + await Future.wait([ + bind.mainSetLocalOption(key: 'access_token', value: ''), + bind.mainSetLocalOption(key: 'user_info', value: ''), + bind.mainSetLocalOption(key: 'selected-tags', value: ''), + ]); + parent.target?.abModel.clear(); + userName.value = ""; + notifyListeners(); + } + + Future>? _getHeaders() { + return parent.target?.getHttpHeaders(); + } + + Future> login(String userName, String pass) async { + final bind = parent.target?.bind; + if (bind == null) { + return {"error": "no context"}; + } + final url = await bind.mainGetApiServer(); + try { + final resp = await http.post(Uri.parse("$url/api/login"), + headers: {"Content-Type": "application/json"}, + body: jsonEncode({ + "username": userName, + "password": pass, + "id": await bind.mainGetMyId(), + "uuid": await bind.mainGetUuid() + })); + final body = jsonDecode(resp.body); + bind.mainSetLocalOption( + key: "access_token", value: body['access_token'] ?? ""); + bind.mainSetLocalOption( + key: "user_info", value: jsonEncode(body['user'])); + this.userName.value = body['user']?['name'] ?? ""; + return body; + } catch (err) { + return {"error": "$err"}; + } + } +} diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index d38dd9529..3d94f6cc7 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -21,10 +21,10 @@ use crate::start_server; use crate::ui_interface; use crate::ui_interface::{ change_id, check_connect_status, get_api_server, get_app_name, get_async_job_status, - get_connect_status, get_fav, get_lan_peers, get_license, get_local_option, get_options, - get_peer, get_socks, get_sound_inputs, get_version, has_rendezvous_service, is_ok_change_id, - post_request, set_local_option, set_options, set_socks, store_fav, test_if_valid_server, - using_public_server, + get_connect_status, get_fav, get_id, get_lan_peers, get_license, get_local_option, get_options, + get_peer, get_socks, get_sound_inputs, get_uuid, get_version, has_rendezvous_service, + is_ok_change_id, post_request, set_local_option, set_options, set_socks, store_fav, + test_if_valid_server, using_public_server, }; fn initialize(app_dir: &str) { @@ -488,6 +488,14 @@ pub fn main_set_local_option(key: String, value: String) { set_local_option(key, value) } +pub fn main_get_my_id() -> String { + get_id() +} + +pub fn main_get_uuid() -> String { + get_uuid() +} + /// FFI for **get** commands which are idempotent. /// Return result in c string. /// From d08931c3171f5695e3f58d18eafc15260df1e1d5 Mon Sep 17 00:00:00 2001 From: Asura Date: Wed, 27 Jul 2022 07:36:50 -0700 Subject: [PATCH 0123/2015] Doc: update lang for keyboard mode --- 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/fr.rs | 2 ++ src/lang/hu.rs | 2 ++ src/lang/id.rs | 2 ++ src/lang/it.rs | 2 ++ src/lang/ptbr.rs | 2 ++ src/lang/ru.rs | 2 ++ src/lang/sk.rs | 2 ++ src/lang/template.rs | 2 ++ src/lang/tr.rs | 2 ++ src/lang/tw.rs | 2 ++ 15 files changed, 30 insertions(+) diff --git a/src/lang/cs.rs b/src/lang/cs.rs index d9ff10416..bb5304aa6 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -300,5 +300,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Activate onetime password", ""), ("Set security password", ""), ("Connection not allowed", ""), + ("Legacy mode", ""), + ("Map mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index f9776e687..4a4310ae0 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -300,5 +300,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Activate onetime password", ""), ("Set security password", ""), ("Connection not allowed", ""), + ("Legacy mode", ""), + ("Map mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 671bcae98..c64bdec3d 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -300,5 +300,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Activate onetime password", "Einmal-Passwort aktivieren"), ("Set security password", "Sicheres Passwort setzen"), ("Connection not allowed", "Verbindung abgelehnt"), + ("Legacy mode", ""), + ("Map mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index a21833559..bd4d4bdc0 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -300,5 +300,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Activate onetime password", ""), ("Set security password", ""), ("Connection not allowed", ""), + ("Legacy mode", ""), + ("Map mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 14ba0ab57..fe627713c 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -300,5 +300,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Activate onetime password", ""), ("Set security password", ""), ("Connection not allowed", ""), + ("Legacy mode", ""), + ("Map mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index f387b550b..1142f85a8 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -300,5 +300,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Activate onetime password", ""), ("Set security password", ""), ("Connection not allowed", ""), + ("Legacy mode", ""), + ("Map mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index e35ab8195..b95dce235 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -300,5 +300,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Activate onetime password", ""), ("Set security password", ""), ("Connection not allowed", ""), + ("Legacy mode", ""), + ("Map mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 6bf69e476..e6fefe77d 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -300,5 +300,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Activate onetime password", ""), ("Set security password", ""), ("Connection not allowed", ""), + ("Legacy mode", ""), + ("Map mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 778313d5b..8fa3b77bb 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -300,5 +300,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Activate onetime password", ""), ("Set security password", ""), ("Connection not allowed", ""), + ("Legacy mode", ""), + ("Map mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 176833501..5d653550b 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -300,5 +300,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Activate onetime password", ""), ("Set security password", ""), ("Connection not allowed", ""), + ("Legacy mode", ""), + ("Map mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 55d33c7b3..edb31a7d1 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -300,5 +300,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Activate onetime password", "Активировать одноразовый пароль"), ("Set security password", "Задать пароль безопасности"), ("Connection not allowed", "Подключение не разрешено"), + ("Legacy mode", ""), + ("Map mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 332336007..f31c31d27 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -300,5 +300,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Activate onetime password", ""), ("Set security password", ""), ("Connection not allowed", ""), + ("Legacy mode", ""), + ("Map mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 7b39a2876..a211ef398 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -300,5 +300,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Activate onetime password", ""), ("Set security password", ""), ("Connection not allowed", ""), + ("Legacy mode", ""), + ("Map mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 02468201e..f45622c1a 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -300,5 +300,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Activate onetime password", ""), ("Set security password", ""), ("Connection not allowed", ""), + ("Legacy mode", ""), + ("Map mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index ea94d159c..0e7d0d573 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -300,5 +300,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Activate onetime password", "激活一次性訪問功能"), ("Set security password", "設置安全密碼"), ("Connection not allowed", "對方不允許連接"), + ("Legacy mode", "傳統模式"), + ("Map mode", "1:1傳輸"), ].iter().cloned().collect(); } From d5ac305e5717cf2144228ca1962b23d35cab14f2 Mon Sep 17 00:00:00 2001 From: Asura Date: Thu, 28 Jul 2022 11:00:01 +0800 Subject: [PATCH 0124/2015] Add translate mode in remote --- Cargo.lock | 110 +++++++++++++++++++++++++++++++---------------- src/ui/remote.rs | 41 ++++++++++++++++-- 2 files changed, 112 insertions(+), 39 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ecbf1646f..833cbe12a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -638,21 +638,6 @@ dependencies = [ "cc", ] -[[package]] -name = "cocoa" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "667fdc068627a2816b9ff831201dd9864249d6ee8d190b9532357f1fc0f61ea7" -dependencies = [ - "bitflags", - "block", - "core-foundation 0.9.3", - "core-graphics 0.21.0", - "foreign-types", - "libc", - "objc", -] - [[package]] name = "cocoa" version = "0.24.0" @@ -770,18 +755,6 @@ dependencies = [ "libc", ] -[[package]] -name = "core-graphics" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52a67c4378cf203eace8fb6567847eb641fd6ff933c1145a115c6ee820ebb978" -dependencies = [ - "bitflags", - "core-foundation 0.9.3", - "foreign-types", - "libc", -] - [[package]] name = "core-graphics" version = "0.22.3" @@ -1336,6 +1309,16 @@ dependencies = [ "termcolor", ] +[[package]] +name = "epoll" +version = "4.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20df693c700404f7e19d4d6fae6b15215d2913c27955d2b9d6f2c0f537511cd0" +dependencies = [ + "bitflags", + "libc", +] + [[package]] name = "err-derive" version = "0.3.1" @@ -1370,6 +1353,29 @@ dependencies = [ "nix 0.23.1", ] +[[package]] +name = "evdev-rs" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7db51abf6b3205a6e6e8dd68d7a5414d7c50d61736a6f4c9b97df86ef5567cf" +dependencies = [ + "bitflags", + "evdev-sys", + "libc", + "log", +] + +[[package]] +name = "evdev-sys" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14ead42b547b15d47089c1243d907bcf0eb94e457046d3b315a26ac9c9e9ea6d" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "event-listener" version = "2.5.2" @@ -2342,6 +2348,28 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "inotify" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abf888f9575c290197b2c948dc9e9ff10bd1a39ad1ea8585f734585fa6b9d3f9" +dependencies = [ + "bitflags", + "futures-core", + "inotify-sys", + "libc", + "tokio", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + [[package]] name = "instant" version = "0.1.12" @@ -3767,15 +3795,19 @@ dependencies = [ [[package]] name = "rdev" version = "0.5.0-2" -source = "git+https://github.com/asur4s/rdev#548d1194dd6863ab004d59299b995eb64cf21c3d" +source = "git+https://github.com/asur4s/rdev#c3a896bcb4a10d171dee1aa685c90abadf8946d2" dependencies = [ - "cocoa 0.22.0", - "core-foundation 0.7.0", - "core-foundation-sys 0.7.0", - "core-graphics 0.19.2", + "cocoa", + "core-foundation 0.9.3", + "core-foundation-sys 0.8.3", + "core-graphics 0.22.3", "enum-map", + "epoll", + "evdev-rs", + "inotify", "lazy_static", "libc", + "widestring 1.0.2", "winapi 0.3.9", "x11", ] @@ -3994,7 +4026,7 @@ dependencies = [ "cfg-if 1.0.0", "clap 3.2.12", "clipboard", - "cocoa 0.24.0", + "cocoa", "core-foundation 0.9.3", "core-graphics 0.22.3", "cpal", @@ -4936,7 +4968,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76863575f7842ed64fda361f417a787efa82811b4617267709066969cd4ccf3b" dependencies = [ - "cocoa 0.24.0", + "cocoa", "core-graphics 0.22.3", "gtk", "libappindicator", @@ -5348,6 +5380,12 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c168940144dd21fd8046987c16a46a33d5fc84eec29ef9dcddc2ac9e31526b7c" +[[package]] +name = "widestring" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "653f141f39ec16bba3c5abe400a0c60da7468261cc2cbf36805022876bc721a8" + [[package]] name = "winapi" version = "0.2.8" @@ -5421,7 +5459,7 @@ checksum = "0c643e10139d127d30d6d753398c8a6f0a43532e8370f6c9d29ebbff29b984ab" dependencies = [ "bitflags", "err-derive", - "widestring", + "widestring 0.4.3", "winapi 0.3.9", ] @@ -5548,7 +5586,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b43cc931d58b99461188607efd7acb2a093e65fc621f54cad78517a6063e73a" dependencies = [ "bitflags", - "cocoa 0.24.0", + "cocoa", "core-foundation 0.9.3", "core-graphics 0.22.3", "core-video-sys", diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 9338b90d4..1b5331609 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -1065,9 +1065,40 @@ impl Handler { self.send_key_event(key_event, KeyboardMode::Map); } - // fn translate_keyboard_mode(&mut self, down_or_up: bool, key: RdevKey) { - // // translate mode(2): locally generated characters are send to the peer. - // } + fn translate_keyboard_mode(&mut self, down_or_up: bool, key: RdevKey, evt: Event) { + // translate mode(2): locally generated characters are send to the peer. + let string = evt.name.unwrap_or_default(); + + let chars = if string == "" { + None + } else { + let chars: Vec = string.chars().collect(); + Some(chars) + }; + + if let Some(chars) = chars { + for chr in chars { + dbg!(chr); + + let mut key_event = KeyEvent::new(); + key_event.set_chr(chr as _); + key_event.down = true; + self.send_key_event(key_event, KeyboardMode::Translate); + + let mut key_event = KeyEvent::new(); + key_event.set_chr(chr as _); + key_event.down = false; + self.send_key_event(key_event, KeyboardMode::Translate); + } + } else { + if down_or_up == true { + TO_RELEASE.lock().unwrap().insert(key); + } else { + TO_RELEASE.lock().unwrap().remove(&key); + } + self.map_keyboard_mode(down_or_up, key); + } + } fn legacy_modifiers( &self, @@ -1327,6 +1358,7 @@ impl Handler { let mode = match self.get_keyboard_mode().as_str() { "map" => KeyboardMode::Map, "legacy" => KeyboardMode::Legacy, + "translate" => KeyboardMode::Translate, _ => KeyboardMode::Legacy, }; @@ -1340,6 +1372,9 @@ impl Handler { self.map_keyboard_mode(down_or_up, key); } KeyboardMode::Legacy => self.legacy_keyboard_mode(down_or_up, key, evt), + KeyboardMode::Translate => { + self.translate_keyboard_mode(down_or_up, key, evt); + } _ => self.legacy_keyboard_mode(down_or_up, key, evt), } } From 25525cda3f2ee4a05e63f41c602a41c41092bddc Mon Sep 17 00:00:00 2001 From: Asura Date: Wed, 27 Jul 2022 20:01:42 -0700 Subject: [PATCH 0125/2015] Add translte mode in input_service --- Cargo.lock | 110 +++++++++++++++++++++---------- libs/enigo/src/linux/nix_impl.rs | 1 - src/server/input_service.rs | 8 +++ 3 files changed, 82 insertions(+), 37 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ecbf1646f..833cbe12a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -638,21 +638,6 @@ dependencies = [ "cc", ] -[[package]] -name = "cocoa" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "667fdc068627a2816b9ff831201dd9864249d6ee8d190b9532357f1fc0f61ea7" -dependencies = [ - "bitflags", - "block", - "core-foundation 0.9.3", - "core-graphics 0.21.0", - "foreign-types", - "libc", - "objc", -] - [[package]] name = "cocoa" version = "0.24.0" @@ -770,18 +755,6 @@ dependencies = [ "libc", ] -[[package]] -name = "core-graphics" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52a67c4378cf203eace8fb6567847eb641fd6ff933c1145a115c6ee820ebb978" -dependencies = [ - "bitflags", - "core-foundation 0.9.3", - "foreign-types", - "libc", -] - [[package]] name = "core-graphics" version = "0.22.3" @@ -1336,6 +1309,16 @@ dependencies = [ "termcolor", ] +[[package]] +name = "epoll" +version = "4.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20df693c700404f7e19d4d6fae6b15215d2913c27955d2b9d6f2c0f537511cd0" +dependencies = [ + "bitflags", + "libc", +] + [[package]] name = "err-derive" version = "0.3.1" @@ -1370,6 +1353,29 @@ dependencies = [ "nix 0.23.1", ] +[[package]] +name = "evdev-rs" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7db51abf6b3205a6e6e8dd68d7a5414d7c50d61736a6f4c9b97df86ef5567cf" +dependencies = [ + "bitflags", + "evdev-sys", + "libc", + "log", +] + +[[package]] +name = "evdev-sys" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14ead42b547b15d47089c1243d907bcf0eb94e457046d3b315a26ac9c9e9ea6d" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "event-listener" version = "2.5.2" @@ -2342,6 +2348,28 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "inotify" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abf888f9575c290197b2c948dc9e9ff10bd1a39ad1ea8585f734585fa6b9d3f9" +dependencies = [ + "bitflags", + "futures-core", + "inotify-sys", + "libc", + "tokio", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + [[package]] name = "instant" version = "0.1.12" @@ -3767,15 +3795,19 @@ dependencies = [ [[package]] name = "rdev" version = "0.5.0-2" -source = "git+https://github.com/asur4s/rdev#548d1194dd6863ab004d59299b995eb64cf21c3d" +source = "git+https://github.com/asur4s/rdev#c3a896bcb4a10d171dee1aa685c90abadf8946d2" dependencies = [ - "cocoa 0.22.0", - "core-foundation 0.7.0", - "core-foundation-sys 0.7.0", - "core-graphics 0.19.2", + "cocoa", + "core-foundation 0.9.3", + "core-foundation-sys 0.8.3", + "core-graphics 0.22.3", "enum-map", + "epoll", + "evdev-rs", + "inotify", "lazy_static", "libc", + "widestring 1.0.2", "winapi 0.3.9", "x11", ] @@ -3994,7 +4026,7 @@ dependencies = [ "cfg-if 1.0.0", "clap 3.2.12", "clipboard", - "cocoa 0.24.0", + "cocoa", "core-foundation 0.9.3", "core-graphics 0.22.3", "cpal", @@ -4936,7 +4968,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76863575f7842ed64fda361f417a787efa82811b4617267709066969cd4ccf3b" dependencies = [ - "cocoa 0.24.0", + "cocoa", "core-graphics 0.22.3", "gtk", "libappindicator", @@ -5348,6 +5380,12 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c168940144dd21fd8046987c16a46a33d5fc84eec29ef9dcddc2ac9e31526b7c" +[[package]] +name = "widestring" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "653f141f39ec16bba3c5abe400a0c60da7468261cc2cbf36805022876bc721a8" + [[package]] name = "winapi" version = "0.2.8" @@ -5421,7 +5459,7 @@ checksum = "0c643e10139d127d30d6d753398c8a6f0a43532e8370f6c9d29ebbff29b984ab" dependencies = [ "bitflags", "err-derive", - "widestring", + "widestring 0.4.3", "winapi 0.3.9", ] @@ -5548,7 +5586,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b43cc931d58b99461188607efd7acb2a093e65fc621f54cad78517a6063e73a" dependencies = [ "bitflags", - "cocoa 0.24.0", + "cocoa", "core-foundation 0.9.3", "core-graphics 0.22.3", "core-video-sys", diff --git a/libs/enigo/src/linux/nix_impl.rs b/libs/enigo/src/linux/nix_impl.rs index c660bd08c..7a8f6668e 100644 --- a/libs/enigo/src/linux/nix_impl.rs +++ b/libs/enigo/src/linux/nix_impl.rs @@ -156,7 +156,6 @@ impl KeyboardControllable for Enigo { } fn key_up(&mut self, key: Key) { if self.is_x11 { - dbg!(key); if self.pynput.send_pynput(&key, false) { return; } diff --git a/src/server/input_service.rs b/src/server/input_service.rs index c350e8d04..629ed17e4 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -819,6 +819,11 @@ fn legacy_keyboard_mode(evt: &KeyEvent) { } } +fn translate_keyboard_mode(evt: &KeyEvent) { + dbg!(evt.chr()); + let chr = char::from_u32(evt.chr()).unwrap_or_default(); + rdev::simulate_char(chr, evt.down); +} fn handle_key_(evt: &KeyEvent) { if EXITING.load(Ordering::SeqCst) { @@ -832,6 +837,9 @@ fn handle_key_(evt: &KeyEvent) { KeyboardMode::Map => { map_keyboard_mode(evt); } + KeyboardMode::Translate => { + translate_keyboard_mode(evt); + } _ => { legacy_keyboard_mode(evt); } From 0ba8b4079b90924ec82b46acc6396d3b7e901d94 Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 27 Jul 2022 22:56:28 +0800 Subject: [PATCH 0126/2015] flutter_desktop_online_state: refactor connection page Signed-off-by: fufesou --- .gitignore | 1 + build.rs | 5 + flutter/.gitignore | 12 +- .../lib/desktop/pages/connection_page.dart | 167 ++++---- flutter/lib/desktop/widgets/peer_widget.dart | 244 ++++++++++++ .../lib/desktop/widgets/peercard_widget.dart | 371 ++++++++++++++++++ flutter/lib/models/model.dart | 18 +- flutter/lib/models/native_model.dart | 49 ++- flutter/lib/models/peer_model.dart | 89 +++++ flutter/macos/Runner/bridge_generated.h | 110 ++++++ flutter/pubspec.lock | 21 + flutter/pubspec.yaml | 3 +- libs/hbb_common/protos/rendezvous.proto | 11 + src/flutter_ffi.rs | 15 + src/rendezvous_mediator.rs | 137 +++++++ 15 files changed, 1152 insertions(+), 101 deletions(-) create mode 100644 flutter/lib/desktop/widgets/peer_widget.dart create mode 100644 flutter/lib/desktop/widgets/peercard_widget.dart create mode 100644 flutter/lib/models/peer_model.dart diff --git a/.gitignore b/.gitignore index 5b26711c5..9d152ac1d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +/build /target .vscode .idea diff --git a/build.rs b/build.rs index 7d6aac441..860ebae77 100644 --- a/build.rs +++ b/build.rs @@ -77,6 +77,10 @@ fn install_oboe() { } fn gen_flutter_rust_bridge() { + let llvm_path = match std::env::var("LLVM_HOME") { + Ok(path) => Some(vec![path]), + Err(_) => None, + }; // Tell Cargo that if the given file changes, to rerun this build script. println!("cargo:rerun-if-changed=src/flutter_ffi.rs"); // settings for fbr_codegen @@ -88,6 +92,7 @@ fn gen_flutter_rust_bridge() { // Path of output generated C header c_output: Some(vec!["flutter/macos/Runner/bridge_generated.h".to_string()]), // for other options lets use default + llvm_path, ..Default::default() }; // run fbr_codegen diff --git a/flutter/.gitignore b/flutter/.gitignore index ede37092d..e5db34d22 100644 --- a/flutter/.gitignore +++ b/flutter/.gitignore @@ -48,13 +48,11 @@ lib/generated_bridge.dart lib/generated_bridge.freezed.dart # Flutter Generated Files -linux/flutter/generated_plugin_registrant.cc -linux/flutter/generated_plugin_registrant.h -linux/flutter/generated_plugins.cmake -macos/Flutter/GeneratedPluginRegistrant.swift -windows/flutter/generated_plugin_registrant.cc -windows/flutter/generated_plugin_registrant.h -windows/flutter/generated_plugins.cmake +**/flutter/GeneratedPluginRegistrant.swift +**/flutter/generated_plugin_registrant.cc +**/flutter/generated_plugin_registrant.h +**/flutter/generated_plugins.cmake +**/Runner/bridge_generated.h flutter_export_environment.sh Flutter-Generated.xcconfig key.jks diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 14a1cc4e7..42c41f8b9 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -4,6 +4,7 @@ import 'dart:convert'; import 'package:contextmenu/contextmenu.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart'; +import 'package:flutter_hbb/desktop/widgets/peer_widget.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; @@ -15,6 +16,7 @@ import '../../mobile/pages/home_page.dart'; import '../../mobile/pages/scan_page.dart'; import '../../mobile/pages/settings_page.dart'; import '../../models/model.dart'; +import '../../models/peer_model.dart'; enum RemoteType { recently, favorite, discovered, addressBook } @@ -58,9 +60,7 @@ class _ConnectionPageState extends State { Widget build(BuildContext context) { if (_idController.text.isEmpty) _idController.text = gFFI.getId(); return Container( - decoration: BoxDecoration( - color: MyTheme.grayBg - ), + decoration: BoxDecoration(color: MyTheme.grayBg), child: Column( mainAxisAlignment: MainAxisAlignment.start, mainAxisSize: MainAxisSize.max, @@ -73,7 +73,9 @@ class _ConnectionPageState extends State { ], ).marginOnly(top: 16.0, left: 16.0), SizedBox(height: 12), - Divider(thickness: 1,), + Divider( + thickness: 1, + ), Expanded( child: DefaultTabController( length: 4, @@ -85,47 +87,61 @@ class _ConnectionPageState extends State { isScrollable: true, indicatorSize: TabBarIndicatorSize.label, tabs: [ - Tab(child: Text(translate("Recent Sessions")),), - Tab(child: Text(translate("Favorites")),), - Tab(child: Text(translate("Discovered")),), - Tab(child: Text(translate("Address Book")),), + Tab( + child: Text(translate("Recent Sessions")), + ), + Tab( + child: Text(translate("Favorites")), + ), + Tab( + child: Text(translate("Discovered")), + ), + Tab( + child: Text(translate("Address Book")), + ), ]), - Expanded(child: TabBarView(children: [ - FutureBuilder(future: getPeers(rType: RemoteType.recently), - builder: (context, snapshot){ - if (snapshot.hasData) { - return snapshot.data!; - } else { - return Offstage(); - } - }), - FutureBuilder( - future: getPeers(rType: RemoteType.favorite), - builder: (context, snapshot) { - if (snapshot.hasData) { - return snapshot.data!; - } else { - return Offstage(); - } - }), - FutureBuilder( - future: getPeers(rType: RemoteType.discovered), - builder: (context, snapshot) { - if (snapshot.hasData) { - return snapshot.data!; - } else { - return Offstage(); - } - }), - FutureBuilder( - future: buildAddressBook(context), - builder: (context, snapshot) { - if (snapshot.hasData) { - return snapshot.data!; - } else { - return Offstage(); - } - }), + Expanded( + child: TabBarView(children: [ + RecentPeerWidget(), + FavoritePeerWidget(), + DiscoveredPeerWidget(), + AddressBookPeerWidget(), + // FutureBuilder( + // future: getPeers(rType: RemoteType.recently), + // builder: (context, snapshot) { + // if (snapshot.hasData) { + // return snapshot.data!; + // } else { + // return Offstage(); + // } + // }), + // FutureBuilder( + // future: getPeers(rType: RemoteType.favorite), + // builder: (context, snapshot) { + // if (snapshot.hasData) { + // return snapshot.data!; + // } else { + // return Offstage(); + // } + // }), + // FutureBuilder( + // future: getPeers(rType: RemoteType.discovered), + // builder: (context, snapshot) { + // if (snapshot.hasData) { + // return snapshot.data!; + // } else { + // return Offstage(); + // } + // }), + // FutureBuilder( + // future: buildAddressBook(context), + // builder: (context, snapshot) { + // if (snapshot.hasData) { + // return snapshot.data!; + // } else { + // return Offstage(); + // } + // }), ]).paddingSymmetric(horizontal: 12.0, vertical: 4.0)) ], )), @@ -166,20 +182,20 @@ class _ConnectionPageState extends State { return _updateUrl.isEmpty ? SizedBox(height: 0) : InkWell( - onTap: () async { - final url = _updateUrl + '.apk'; - if (await canLaunch(url)) { - await launch(url); - } - }, - child: Container( - alignment: AlignmentDirectional.center, - width: double.infinity, - color: Colors.pinkAccent, - padding: EdgeInsets.symmetric(vertical: 12), - child: Text(translate('Download new version'), - style: TextStyle( - color: Colors.white, fontWeight: FontWeight.bold)))); + onTap: () async { + final url = _updateUrl + '.apk'; + if (await canLaunch(url)) { + await launch(url); + } + }, + child: Container( + alignment: AlignmentDirectional.center, + width: double.infinity, + color: Colors.pinkAccent, + padding: EdgeInsets.symmetric(vertical: 12), + child: Text(translate('Download new version'), + style: TextStyle( + color: Colors.white, fontWeight: FontWeight.bold)))); } /// UI for the search bar. @@ -214,8 +230,8 @@ class _ConnectionPageState extends State { labelText: translate('Control Remote Desktop'), // hintText: 'Enter your remote ID', // border: InputBorder., - border: OutlineInputBorder( - borderRadius: BorderRadius.zero), + border: + OutlineInputBorder(borderRadius: BorderRadius.zero), helperStyle: TextStyle( fontWeight: FontWeight.bold, fontSize: 16, @@ -238,8 +254,7 @@ class _ConnectionPageState extends State { ], ), Padding( - padding: const EdgeInsets.only( - top: 16.0), + padding: const EdgeInsets.only(top: 16.0), child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ @@ -996,13 +1011,13 @@ class _WebMenuState extends State { icon: Icon(Icons.more_vert), itemBuilder: (context) { return (isIOS - ? [ - PopupMenuItem( - child: Icon(Icons.qr_code_scanner, color: Colors.black), - value: "scan", - ) - ] - : >[]) + + ? [ + PopupMenuItem( + child: Icon(Icons.qr_code_scanner, color: Colors.black), + value: "scan", + ) + ] + : >[]) + [ PopupMenuItem( child: Text(translate('ID/Relay Server')), @@ -1012,13 +1027,13 @@ class _WebMenuState extends State { (getUrl().contains('admin.rustdesk.com') ? >[] : [ - PopupMenuItem( - child: Text(username == null - ? translate("Login") - : translate("Logout") + ' ($username)'), - value: "login", - ) - ]) + + PopupMenuItem( + child: Text(username == null + ? translate("Login") + : translate("Logout") + ' ($username)'), + value: "login", + ) + ]) + [ PopupMenuItem( child: Text(translate('About') + ' RustDesk'), diff --git a/flutter/lib/desktop/widgets/peer_widget.dart b/flutter/lib/desktop/widgets/peer_widget.dart new file mode 100644 index 000000000..e0c82bb30 --- /dev/null +++ b/flutter/lib/desktop/widgets/peer_widget.dart @@ -0,0 +1,244 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart'; +import 'package:provider/provider.dart'; +import 'package:visibility_detector/visibility_detector.dart'; +import 'package:window_manager/window_manager.dart'; + +import '../../models/peer_model.dart'; +import '../../common.dart'; +import 'peercard_widget.dart'; + +typedef OffstageFunc = bool Function(Peer peer); +typedef PeerCardWidgetFunc = Widget Function(Peer peer); + +class _PeerWidget extends StatefulWidget { + late final _name; + late final _peers; + late final OffstageFunc _offstageFunc; + late final PeerCardWidgetFunc _peerCardWidgetFunc; + _PeerWidget(String name, List peers, OffstageFunc offstageFunc, + PeerCardWidgetFunc peerCardWidgetFunc, + {Key? key}) + : super(key: key) { + _name = name; + _peers = peers; + _offstageFunc = offstageFunc; + _peerCardWidgetFunc = peerCardWidgetFunc; + } + + @override + _PeerWidgetState createState() => _PeerWidgetState(); +} + +/// State for the peer widget. +class _PeerWidgetState extends State<_PeerWidget> with WindowListener { + static const int _maxQueryCount = 3; + + var _curPeers = Set(); + var _lastChangeTime = DateTime.now(); + var _lastQueryPeers = Set(); + var _lastQueryTime = DateTime.now().subtract(Duration(hours: 1)); + var _queryCoun = 0; + var _exit = false; + + _PeerWidgetState() { + _startCheckOnlines(); + } + + @override + void initState() { + windowManager.addListener(this); + super.initState(); + } + + @override + void dispose() { + windowManager.removeListener(this); + _exit = true; + super.dispose(); + } + + @override + void onWindowFocus() { + _queryCoun = 0; + } + + @override + Widget build(BuildContext context) { + final space = 8.0; + return ChangeNotifierProvider( + create: (context) => Peers(super.widget._name, super.widget._peers), + child: SingleChildScrollView( + child: Consumer( + builder: (context, peers, child) => Wrap( + children: () { + final cards = []; + peers.peers.forEach((peer) { + cards.add(Offstage( + offstage: super.widget._offstageFunc(peer), + child: Container( + width: 225, + height: 150, + child: VisibilityDetector( + key: Key('${peer.id}'), + onVisibilityChanged: (info) { + final peerId = (info.key as ValueKey).value; + if (info.visibleFraction > 0.00001) { + _curPeers.add(peerId); + } else { + _curPeers.remove(peerId); + } + _lastChangeTime = DateTime.now(); + }, + child: super.widget._peerCardWidgetFunc(peer), + ), + ))); + }); + return cards; + }(), + spacing: space, + runSpacing: space))), + ); + } + + // ignore: todo + // TODO: variables walk through async tasks? + void _startCheckOnlines() { + () async { + while (!_exit) { + final now = DateTime.now(); + if (!setEquals(_curPeers, _lastQueryPeers)) { + if (now.difference(_lastChangeTime) > Duration(seconds: 1)) { + gFFI.ffiModel.platformFFI.ffiBind + .queryOnlines(ids: _curPeers.toList(growable: false)); + _lastQueryPeers = {..._curPeers}; + _lastQueryTime = DateTime.now(); + _queryCoun = 0; + } + } else { + if (_queryCoun < _maxQueryCount) { + if (now.difference(_lastQueryTime) > Duration(seconds: 20)) { + gFFI.ffiModel.platformFFI.ffiBind + .queryOnlines(ids: _curPeers.toList(growable: false)); + _lastQueryTime = DateTime.now(); + _queryCoun += 1; + } + } + } + await Future.delayed(Duration(milliseconds: 300)); + } + }(); + } +} + +abstract class BasePeerWidget extends StatelessWidget { + late final _name; + late final OffstageFunc _offstageFunc; + late final PeerCardWidgetFunc _peerCardWidgetFunc; + + BasePeerWidget({Key? key}) : super(key: key) {} + + @override + Widget build(BuildContext context) { + return FutureBuilder(future: () async { + return _PeerWidget( + _name, await _loadPeers(), _offstageFunc, _peerCardWidgetFunc); + }(), builder: (context, snapshot) { + if (snapshot.hasData) { + return snapshot.data!; + } else { + return Offstage(); + } + }); + } + + @protected + Future> _loadPeers(); +} + +class RecentPeerWidget extends BasePeerWidget { + RecentPeerWidget({Key? key}) : super(key: key) { + super._name = "recent peer"; + super._offstageFunc = (Peer _peer) => false; + super._peerCardWidgetFunc = (Peer peer) => RecentPeerCard(peer: peer); + } + + Future> _loadPeers() async { + return gFFI.peers(); + } +} + +class FavoritePeerWidget extends BasePeerWidget { + FavoritePeerWidget({Key? key}) : super(key: key) { + super._name = "favorite peer"; + super._offstageFunc = (Peer _peer) => false; + super._peerCardWidgetFunc = (Peer peer) => FavoritePeerCard(peer: peer); + } + + @override + Future> _loadPeers() async { + return await gFFI.bind.mainGetFav().then((peers) async { + final peersEntities = await Future.wait(peers + .map((id) => gFFI.bind.mainGetPeers(id: id)) + .toList(growable: false)) + .then((peers_str) { + final len = peers_str.length; + final ps = List.empty(growable: true); + for (var i = 0; i < len; i++) { + print("${peers[i]}: ${peers_str[i]}"); + ps.add(Peer.fromJson(peers[i], jsonDecode(peers_str[i])['info'])); + } + return ps; + }); + return peersEntities; + }); + } +} + +class DiscoveredPeerWidget extends BasePeerWidget { + DiscoveredPeerWidget({Key? key}) : super(key: key) { + super._name = "discovered peer"; + super._offstageFunc = (Peer _peer) => false; + super._peerCardWidgetFunc = (Peer peer) => DiscoveredPeerCard(peer: peer); + } + + Future> _loadPeers() async { + return await gFFI.bind.mainGetLanPeers().then((peers_string) { + debugPrint(peers_string); + return []; + }); + } +} + +class AddressBookPeerWidget extends BasePeerWidget { + AddressBookPeerWidget({Key? key}) : super(key: key) { + super._name = "address book peer"; + super._offstageFunc = + (Peer peer) => !_hitTag(gFFI.abModel.selectedTags, peer.tags); + super._peerCardWidgetFunc = (Peer peer) => AddressBookPeerCard(peer: peer); + } + + Future> _loadPeers() async { + return gFFI.abModel.peers.map((e) { + return Peer.fromJson(e['id'], e); + }).toList(); + } + + bool _hitTag(List selectedTags, List idents) { + if (selectedTags.isEmpty) { + return true; + } + if (idents.isEmpty) { + return false; + } + for (final tag in selectedTags) { + if (!idents.contains(tag)) { + return false; + } + } + return true; + } +} diff --git a/flutter/lib/desktop/widgets/peercard_widget.dart b/flutter/lib/desktop/widgets/peercard_widget.dart new file mode 100644 index 000000000..b8c6d54de --- /dev/null +++ b/flutter/lib/desktop/widgets/peercard_widget.dart @@ -0,0 +1,371 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/utils/multi_window_manager.dart'; +import 'package:get/get.dart'; +import 'package:contextmenu/contextmenu.dart'; + +import '../../common.dart'; +import '../../models/model.dart'; +import '../../models/peer_model.dart'; + +class _PeerCard extends StatefulWidget { + final Peer peer; + final List> popupMenuItems; + + _PeerCard({required this.peer, required this.popupMenuItems, Key? key}) + : super(key: key); + + @override + _PeerCardState createState() => _PeerCardState(); +} + +/// State for the connection page. +class _PeerCardState extends State<_PeerCard> { + var _menuPos; + + @override + Widget build(BuildContext context) { + final peer = super.widget.peer; + var deco = Rx(BoxDecoration( + border: Border.all(color: Colors.transparent, width: 1.0), + borderRadius: BorderRadius.circular(20))); + return Card( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + child: MouseRegion( + onEnter: (evt) { + deco.value = BoxDecoration( + border: Border.all(color: Colors.blue, width: 1.0), + borderRadius: BorderRadius.circular(20)); + }, + onExit: (evt) { + deco.value = BoxDecoration( + border: Border.all(color: Colors.transparent, width: 1.0), + borderRadius: BorderRadius.circular(20)); + }, + child: _buildPeerTile(context, peer, deco), + )); + } + + Widget _buildPeerTile( + BuildContext context, Peer peer, Rx deco) { + return Obx( + () => Container( + decoration: deco.value, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: Container( + decoration: BoxDecoration( + color: str2color('${peer.id}${peer.platform}', 0x7f), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + ), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.all(6), + child: _getPlatformImage('${peer.platform}'), + ), + Row( + children: [ + Expanded( + child: Tooltip( + message: '${peer.username}@${peer.hostname}', + child: Text( + '${peer.username}@${peer.hostname}', + style: TextStyle( + color: Colors.white70, fontSize: 12), + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + ), + ), + ), + ], + ), + ], + ).paddingAll(4.0), + ), + ], + ), + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row(children: [ + Padding( + padding: EdgeInsets.fromLTRB(0, 4, 8, 4), + child: CircleAvatar( + radius: 5, + backgroundColor: + peer.online ? Colors.green : Colors.yellow)), + Text('${peer.id}') + ]), + InkWell( + child: Icon(Icons.more_vert), + onTapDown: (e) { + final x = e.globalPosition.dx; + final y = e.globalPosition.dy; + _menuPos = RelativeRect.fromLTRB(x, y, x, y); + }, + onTap: () { + _showPeerMenu(context, peer.id); + }), + ], + ).paddingSymmetric(vertical: 8.0, horizontal: 12.0) + ], + ), + ), + ); + } + + /// Connect to a peer with [id]. + /// If [isFileTransfer], starts a session only for file transfer. + void _connect(String id, {bool isFileTransfer = false}) async { + if (id == '') return; + id = id.replaceAll(' ', ''); + if (isFileTransfer) { + await rustDeskWinManager.new_file_transfer(id); + } else { + await rustDeskWinManager.new_remote_desktop(id); + } + FocusScopeNode currentFocus = FocusScope.of(context); + if (!currentFocus.hasPrimaryFocus) { + currentFocus.unfocus(); + } + } + + /// Show the peer menu and handle user's choice. + /// User might remove the peer or send a file to the peer. + void _showPeerMenu(BuildContext context, String id) async { + var value = await showMenu( + context: context, + position: this._menuPos, + items: super.widget.popupMenuItems, + elevation: 8, + ); + if (value == 'remove') { + setState(() => gFFI.setByName('remove', '$id')); + () async { + removePreference(id); + }(); + } else if (value == 'file') { + _connect(id, isFileTransfer: true); + } else if (value == 'add-fav') { + } else if (value == 'connect') { + _connect(id, isFileTransfer: false); + } else if (value == 'ab-delete') { + gFFI.abModel.deletePeer(id); + await gFFI.abModel.updateAb(); + setState(() {}); + } else if (value == 'ab-edit-tag') { + _abEditTag(id); + } + } + + Widget _buildTag(String tagName, RxList rxTags, + {Function()? onTap}) { + return ContextMenuArea( + width: 100, + builder: (context) => [ + ListTile( + title: Text(translate("Delete")), + onTap: () { + gFFI.abModel.deleteTag(tagName); + gFFI.abModel.updateAb(); + Future.delayed(Duration.zero, () => Get.back()); + }, + ) + ], + child: GestureDetector( + onTap: onTap, + child: Obx( + () => Container( + decoration: BoxDecoration( + color: rxTags.contains(tagName) ? Colors.blue : null, + border: Border.all(color: MyTheme.darkGray), + borderRadius: BorderRadius.circular(10)), + margin: EdgeInsets.symmetric(horizontal: 4.0, vertical: 8.0), + padding: EdgeInsets.symmetric(vertical: 2.0, horizontal: 8.0), + child: Text( + tagName, + style: TextStyle( + color: rxTags.contains(tagName) ? MyTheme.white : null), + ), + ), + ), + ), + ); + } + + /// Get the image for the current [platform]. + Widget _getPlatformImage(String platform) { + platform = platform.toLowerCase(); + if (platform == 'mac os') + platform = 'mac'; + else if (platform != 'linux' && platform != 'android') platform = 'win'; + return Image.asset('assets/$platform.png', height: 50); + } + + void _abEditTag(String id) { + var isInProgress = false; + + final tags = List.of(gFFI.abModel.tags); + var selectedTag = gFFI.abModel.getPeerTags(id).obs; + + DialogManager.show((setState, close) { + return CustomAlertDialog( + title: Text(translate("Edit Tag")), + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + child: Wrap( + children: tags + .map((e) => _buildTag(e, selectedTag, onTap: () { + if (selectedTag.contains(e)) { + selectedTag.remove(e); + } else { + selectedTag.add(e); + } + })) + .toList(growable: false), + ), + ), + Offstage(offstage: !isInProgress, child: LinearProgressIndicator()) + ], + ), + actions: [ + TextButton( + onPressed: () { + close(); + }, + child: Text(translate("Cancel"))), + TextButton( + onPressed: () async { + setState(() { + isInProgress = true; + }); + gFFI.abModel.changeTagForPeer(id, selectedTag); + await gFFI.abModel.updateAb(); + close(); + }, + child: Text(translate("OK"))), + ], + ); + }); + } +} + +abstract class BasePeerCard extends StatelessWidget { + final Peer peer; + BasePeerCard({required this.peer, Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return _PeerCard(peer: peer, popupMenuItems: _getPopupMenuItems()); + } + + @protected + List> _getPopupMenuItems(); +} + +class RecentPeerCard extends BasePeerCard { + RecentPeerCard({required Peer peer, Key? key}) : super(peer: peer, key: key); + + List> _getPopupMenuItems() { + return [ + PopupMenuItem( + child: Text(translate('Connect')), value: 'connect'), + PopupMenuItem( + child: Text(translate('Transfer File')), value: 'file'), + PopupMenuItem( + child: Text(translate('TCP Tunneling')), value: 'tcp-tunnel'), + PopupMenuItem(child: Text(translate('Rename')), value: 'rename'), + PopupMenuItem(child: Text(translate('Remove')), value: 'remove'), + PopupMenuItem( + child: Text(translate('Unremember Password')), + value: 'unremember-password'), + PopupMenuItem( + child: Text(translate('Edit Tag')), value: 'ab-edit-tag'), + ]; + } +} + +class FavoritePeerCard extends BasePeerCard { + FavoritePeerCard({required Peer peer, Key? key}) + : super(peer: peer, key: key); + + List> _getPopupMenuItems() { + return [ + PopupMenuItem( + child: Text(translate('Connect')), value: 'connect'), + PopupMenuItem( + child: Text(translate('Transfer File')), value: 'file'), + PopupMenuItem( + child: Text(translate('TCP Tunneling')), value: 'tcp-tunnel'), + PopupMenuItem(child: Text(translate('Rename')), value: 'rename'), + PopupMenuItem(child: Text(translate('Remove')), value: 'remove'), + PopupMenuItem( + child: Text(translate('Unremember Password')), + value: 'unremember-password'), + PopupMenuItem( + child: Text(translate('Remove from Favorites')), value: 'remove-fav'), + ]; + } +} + +class DiscoveredPeerCard extends BasePeerCard { + DiscoveredPeerCard({required Peer peer, Key? key}) + : super(peer: peer, key: key); + + List> _getPopupMenuItems() { + return [ + PopupMenuItem( + child: Text(translate('Connect')), value: 'connect'), + PopupMenuItem( + child: Text(translate('Transfer File')), value: 'file'), + PopupMenuItem( + child: Text(translate('TCP Tunneling')), value: 'tcp-tunnel'), + PopupMenuItem(child: Text(translate('Rename')), value: 'rename'), + PopupMenuItem(child: Text(translate('Remove')), value: 'remove'), + PopupMenuItem( + child: Text(translate('Unremember Password')), + value: 'unremember-password'), + PopupMenuItem( + child: Text(translate('Edit Tag')), value: 'ab-edit-tag'), + ]; + } +} + +class AddressBookPeerCard extends BasePeerCard { + AddressBookPeerCard({required Peer peer, Key? key}) + : super(peer: peer, key: key); + + List> _getPopupMenuItems() { + return [ + PopupMenuItem( + child: Text(translate('Connect')), value: 'connect'), + PopupMenuItem( + child: Text(translate('Transfer File')), value: 'file'), + PopupMenuItem( + child: Text(translate('TCP Tunneling')), value: 'tcp-tunnel'), + PopupMenuItem(child: Text(translate('Rename')), value: 'rename'), + PopupMenuItem( + child: Text(translate('Remove')), value: 'ab-delete'), + PopupMenuItem( + child: Text(translate('Unremember Password')), + value: 'unremember-password'), + PopupMenuItem( + child: Text(translate('Add to Favorites')), value: 'add-fav'), + ]; + } +} diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index fa8210618..bc64ff6f5 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -21,6 +21,7 @@ import '../common.dart'; import '../mobile/widgets/dialog.dart'; import '../mobile/widgets/overlay.dart'; import 'native_model.dart' if (dart.library.html) 'web_model.dart'; +import 'peer_model.dart'; typedef HandleMsgBox = void Function(Map evt, String id); bool _waitForImage = false; @@ -1092,7 +1093,7 @@ class FFI { Future> getAudioInputs() async { return await bind.mainGetSoundInputs(); } - + String getDefaultAudioInput() { final input = getOption('audio-input'); if (input.isEmpty && Platform.isWindows) { @@ -1110,21 +1111,6 @@ class FFI { } } -class Peer { - final String id; - final String username; - final String hostname; - final String platform; - final List tags; - - Peer.fromJson(String id, Map json) - : id = id, - username = json['username'] ?? '', - hostname = json['hostname'] ?? '', - platform = json['platform'] ?? '', - tags = json['tags'] ?? []; -} - class Display { double x = 0; double y = 0; diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index c0fd4dfa1..511aa5ffe 100644 --- a/flutter/lib/models/native_model.dart +++ b/flutter/lib/models/native_model.dart @@ -6,6 +6,7 @@ import 'dart:typed_data'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:external_path/external_path.dart'; import 'package:ffi/ffi.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:path_provider/path_provider.dart'; @@ -21,6 +22,7 @@ class RgbaFrame extends Struct { typedef F2 = Pointer Function(Pointer, Pointer); typedef F3 = void Function(Pointer, Pointer); +typedef HandleEvent = void Function(Map evt); /// FFI wrapper around the native Rust core. /// Hides the platform differences. @@ -30,6 +32,7 @@ class PlatformFFI { String _homeDir = ''; F2? _getByName; F3? _setByName; + var _eventHandlers = Map>(); late RustdeskImpl _ffiBind; void Function(Map)? _eventCallback; @@ -40,6 +43,31 @@ class PlatformFFI { return packageInfo.version; } + bool registerEventHandler( + String event_name, String handler_name, HandleEvent handler) { + debugPrint('registerEventHandler $event_name $handler_name'); + var handlers = _eventHandlers[event_name]; + if (handlers == null) { + _eventHandlers[event_name] = {handler_name: handler}; + return true; + } else { + if (handlers.containsKey(handler_name)) { + return false; + } else { + handlers[handler_name] = handler; + return true; + } + } + } + + void unregisterEventHandler(String event_name, String handler_name) { + debugPrint('unregisterEventHandler $event_name $handler_name'); + var handlers = _eventHandlers[event_name]; + if (handlers != null) { + handlers.remove(handler_name); + } + } + /// Send **get** command to the Rust core based on [name] and [arg]. /// Return the result as a string. String getByName(String name, [String arg = '']) { @@ -138,6 +166,22 @@ class PlatformFFI { version = await getVersion(); } + bool _tryHandle(Map evt) { + final name = evt['name']; + if (name != null) { + final handlers = _eventHandlers[name]; + if (handlers != null) { + if (handlers.isNotEmpty) { + handlers.values.forEach((handler) { + handler(evt); + }); + return true; + } + } + } + return false; + } + /// Start listening to the Rust core's events and frames. void _startListenEvent(RustdeskImpl rustdeskImpl) { () async { @@ -145,7 +189,10 @@ class PlatformFFI { if (_eventCallback != null) { try { Map event = json.decode(message); - _eventCallback!(event); + // _tryHandle here may be more flexible than _eventCallback + if (!_tryHandle(event)) { + _eventCallback!(event); + } } catch (e) { print('json.decode fail(): $e'); } diff --git a/flutter/lib/models/peer_model.dart b/flutter/lib/models/peer_model.dart new file mode 100644 index 000000000..939d16ede --- /dev/null +++ b/flutter/lib/models/peer_model.dart @@ -0,0 +1,89 @@ +import 'package:flutter/foundation.dart'; +import '../../common.dart'; + +class Peer { + final String id; + final String username; + final String hostname; + final String platform; + final List tags; + bool online = false; + + Peer.fromJson(String id, Map json) + : id = id, + username = json['username'] ?? '', + hostname = json['hostname'] ?? '', + platform = json['platform'] ?? '', + tags = json['tags'] ?? []; + + Peer({ + required this.id, + required this.username, + required this.hostname, + required this.platform, + required this.tags, + }); + + Peer.loading() + : this( + id: '...', + username: '...', + hostname: '...', + platform: '...', + tags: []); +} + +class Peers extends ChangeNotifier { + late String _name; + late var _peers; + static const cbQueryOnlines = 'callback_query_onlines'; + + Peers(String name, List peers) { + _name = name; + _peers = peers; + gFFI.ffiModel.platformFFI.registerEventHandler(cbQueryOnlines, _name, + (evt) { + _updateOnlineState(evt); + }); + } + + List get peers => _peers; + + @override + void dispose() { + gFFI.ffiModel.platformFFI.unregisterEventHandler(cbQueryOnlines, _name); + super.dispose(); + } + + Peer getByIndex(int index) { + if (index < _peers.length) { + return _peers[index]; + } else { + return Peer.loading(); + } + } + + int getPeersCount() { + return _peers.length; + } + + void _updateOnlineState(Map evt) { + evt['onlines'].split(',').forEach((online) { + for (var i = 0; i < _peers.length; i++) { + if (_peers[i].id == online) { + _peers[i].online = true; + } + } + }); + + evt['offlines'].split(',').forEach((offline) { + for (var i = 0; i < _peers.length; i++) { + if (_peers[i].id == offline) { + _peers[i].online = false; + } + } + }); + + notifyListeners(); + } +} diff --git a/flutter/macos/Runner/bridge_generated.h b/flutter/macos/Runner/bridge_generated.h index 163ad91cd..2d14efe93 100644 --- a/flutter/macos/Runner/bridge_generated.h +++ b/flutter/macos/Runner/bridge_generated.h @@ -17,6 +17,11 @@ typedef struct WireSyncReturnStruct { bool success; } WireSyncReturnStruct; +typedef struct wire_StringList { + struct wire_uint_8_list **ptr; + int32_t len; +} wire_StringList; + typedef int64_t DartPort; typedef bool (*DartPostCObjectFnType)(DartPort port_id, void *message); @@ -165,6 +170,82 @@ void wire_session_read_local_dir_sync(int64_t port_, struct wire_uint_8_list *path, bool show_hidden); +void wire_session_get_platform(int64_t port_, struct wire_uint_8_list *id, bool is_remote); + +void wire_session_load_last_transfer_jobs(int64_t port_, struct wire_uint_8_list *id); + +void wire_session_add_job(int64_t port_, + struct wire_uint_8_list *id, + int32_t act_id, + struct wire_uint_8_list *path, + struct wire_uint_8_list *to, + int32_t file_num, + bool include_hidden, + bool is_remote); + +void wire_session_resume_job(int64_t port_, + struct wire_uint_8_list *id, + int32_t act_id, + bool is_remote); + +void wire_main_get_sound_inputs(int64_t port_); + +void wire_main_change_id(int64_t port_, struct wire_uint_8_list *new_id); + +void wire_main_get_async_status(int64_t port_); + +void wire_main_get_options(int64_t port_); + +void wire_main_set_options(int64_t port_, struct wire_uint_8_list *json); + +void wire_main_test_if_valid_server(int64_t port_, struct wire_uint_8_list *server); + +void wire_main_set_socks(int64_t port_, + struct wire_uint_8_list *proxy, + struct wire_uint_8_list *username, + struct wire_uint_8_list *password); + +void wire_main_get_socks(int64_t port_); + +void wire_main_get_app_name(int64_t port_); + +void wire_main_get_license(int64_t port_); + +void wire_main_get_version(int64_t port_); + +void wire_main_get_fav(int64_t port_); + +void wire_main_store_fav(int64_t port_, struct wire_StringList *favs); + +void wire_main_get_peers(int64_t port_, struct wire_uint_8_list *id); + +void wire_main_get_lan_peers(int64_t port_); + +void wire_main_get_connect_status(int64_t port_); + +void wire_main_check_connect_status(int64_t port_); + +void wire_main_is_using_public_server(int64_t port_); + +void wire_main_has_rendezvous_service(int64_t port_); + +void wire_main_get_api_server(int64_t port_); + +void wire_main_post_request(int64_t port_, + struct wire_uint_8_list *url, + struct wire_uint_8_list *body, + struct wire_uint_8_list *header); + +void wire_main_get_local_option(int64_t port_, struct wire_uint_8_list *key); + +void wire_main_set_local_option(int64_t port_, + struct wire_uint_8_list *key, + struct wire_uint_8_list *value); + +void wire_query_onlines(int64_t port_, struct wire_StringList *ids); + +struct wire_StringList *new_StringList(int32_t len); + struct wire_uint_8_list *new_uint_8_list(int32_t len); void free_WireSyncReturnStruct(struct WireSyncReturnStruct val); @@ -213,6 +294,35 @@ static int64_t dummy_method_to_enforce_bundling(void) { dummy_var ^= ((int64_t) (void*) wire_session_cancel_job); dummy_var ^= ((int64_t) (void*) wire_session_create_dir); dummy_var ^= ((int64_t) (void*) wire_session_read_local_dir_sync); + dummy_var ^= ((int64_t) (void*) wire_session_get_platform); + dummy_var ^= ((int64_t) (void*) wire_session_load_last_transfer_jobs); + dummy_var ^= ((int64_t) (void*) wire_session_add_job); + dummy_var ^= ((int64_t) (void*) wire_session_resume_job); + dummy_var ^= ((int64_t) (void*) wire_main_get_sound_inputs); + dummy_var ^= ((int64_t) (void*) wire_main_change_id); + dummy_var ^= ((int64_t) (void*) wire_main_get_async_status); + dummy_var ^= ((int64_t) (void*) wire_main_get_options); + dummy_var ^= ((int64_t) (void*) wire_main_set_options); + dummy_var ^= ((int64_t) (void*) wire_main_test_if_valid_server); + dummy_var ^= ((int64_t) (void*) wire_main_set_socks); + dummy_var ^= ((int64_t) (void*) wire_main_get_socks); + dummy_var ^= ((int64_t) (void*) wire_main_get_app_name); + dummy_var ^= ((int64_t) (void*) wire_main_get_license); + dummy_var ^= ((int64_t) (void*) wire_main_get_version); + dummy_var ^= ((int64_t) (void*) wire_main_get_fav); + dummy_var ^= ((int64_t) (void*) wire_main_store_fav); + dummy_var ^= ((int64_t) (void*) wire_main_get_peers); + dummy_var ^= ((int64_t) (void*) wire_main_get_lan_peers); + dummy_var ^= ((int64_t) (void*) wire_main_get_connect_status); + dummy_var ^= ((int64_t) (void*) wire_main_check_connect_status); + dummy_var ^= ((int64_t) (void*) wire_main_is_using_public_server); + dummy_var ^= ((int64_t) (void*) wire_main_has_rendezvous_service); + dummy_var ^= ((int64_t) (void*) wire_main_get_api_server); + dummy_var ^= ((int64_t) (void*) wire_main_post_request); + dummy_var ^= ((int64_t) (void*) wire_main_get_local_option); + dummy_var ^= ((int64_t) (void*) wire_main_set_local_option); + dummy_var ^= ((int64_t) (void*) wire_query_onlines); + dummy_var ^= ((int64_t) (void*) new_StringList); dummy_var ^= ((int64_t) (void*) new_uint_8_list); dummy_var ^= ((int64_t) (void*) free_WireSyncReturnStruct); dummy_var ^= ((int64_t) (void*) store_dart_post_cobject); diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 364bad74d..127dcd523 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -771,6 +771,13 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "3.1.0" + screen_retriever: + dependency: transitive + description: + name: screen_retriever + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.1.2" settings_ui: dependency: "direct main" description: @@ -1028,6 +1035,13 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.1.2" + visibility_detector: + dependency: "direct main" + description: + name: visibility_detector + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.3.3" wakelock: dependency: "direct main" description: @@ -1084,6 +1098,13 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.6.1" + window_manager: + dependency: "direct main" + description: + name: window_manager + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.2.5" xdg_directories: dependency: transitive description: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 4a2b64043..76c2f7e12 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -58,7 +58,7 @@ dependencies: url: https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge ref: master path: frb_dart - # window_manager: ^0.2.5 + window_manager: ^0.2.5 desktop_multi_window: git: url: https://github.com/Kingtous/rustdesk_desktop_multi_window @@ -67,6 +67,7 @@ dependencies: freezed_annotation: ^2.0.3 tray_manager: 0.1.7 get: ^4.6.5 + visibility_detector: ^0.3.3 contextmenu: ^3.0.0 dev_dependencies: diff --git a/libs/hbb_common/protos/rendezvous.proto b/libs/hbb_common/protos/rendezvous.proto index 2c5f1b3ba..1ac60f3f3 100644 --- a/libs/hbb_common/protos/rendezvous.proto +++ b/libs/hbb_common/protos/rendezvous.proto @@ -148,6 +148,15 @@ message PeerDiscovery { string misc = 7; } +message OnlineRequest { + string id = 1; + repeated string peers = 2; +} + +message OnlineResponse { + bytes states = 1; +} + message RendezvousMessage { oneof union { RegisterPeer register_peer = 6; @@ -167,5 +176,7 @@ message RendezvousMessage { TestNatRequest test_nat_request = 20; TestNatResponse test_nat_response = 21; PeerDiscovery peer_discovery = 22; + OnlineRequest online_request = 23; + OnlineResponse online_response = 24; } } diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 3d94f6cc7..57e7db87d 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -985,6 +985,21 @@ unsafe extern "C" fn set_by_name(name: *const c_char, value: *const c_char) { } } +fn handle_query_onlines(onlines: Vec, offlines: Vec) { + if let Some(s) = flutter::GLOBAL_EVENT_STREAM.read().unwrap().as_ref() { + let data = HashMap::from([ + ("name", "callback_query_onlines".to_owned()), + ("onlines", onlines.join(",")), + ("offlines", offlines.join(",")), + ]); + s.add(serde_json::ser::to_string(&data).unwrap_or("".to_owned())); + }; +} + +pub fn query_onlines(ids: Vec) { + crate::rendezvous_mediator::query_online_states(ids, handle_query_onlines) +} + #[cfg(target_os = "android")] pub mod server_side { use jni::{ diff --git a/src/rendezvous_mediator.rs b/src/rendezvous_mediator.rs index a7f90b977..09500804b 100644 --- a/src/rendezvous_mediator.rs +++ b/src/rendezvous_mediator.rs @@ -8,6 +8,7 @@ use hbb_common::{ protobuf::Message as _, rendezvous_proto::*, sleep, socket_client, + tcp::FramedStream, tokio::{ self, select, time::{interval, Duration}, @@ -637,3 +638,139 @@ pub fn discover() -> ResultType<()> { config::LanPeers::store(serde_json::to_string(&peers)?); Ok(()) } + +#[tokio::main(flavor = "current_thread")] +pub async fn query_online_states, Vec)>(ids: Vec, f: F) { + let test = false; + if test { + sleep(1.5).await; + let mut onlines = ids; + let offlines = onlines.drain((onlines.len() / 2)..).collect(); + f(onlines, offlines) + } else { + let query_begin = Instant::now(); + let query_timeout = std::time::Duration::from_millis(3_000); + loop { + if SHOULD_EXIT.load(Ordering::SeqCst) { + break; + } + match query_online_states_(&ids, query_timeout).await { + Ok((onlines, offlines)) => { + f(onlines, offlines); + break; + } + Err(e) => { + log::debug!("{}", &e); + } + } + + if query_begin.elapsed() > query_timeout { + log::debug!("query onlines timeout {:?}", query_timeout); + break; + } + + sleep(1.5).await; + } + } +} + +async fn create_online_stream() -> ResultType { + let rendezvous_server = crate::get_rendezvous_server(1_000).await; + let tmp: Vec<&str> = rendezvous_server.split(":").collect(); + if tmp.len() != 2 { + bail!("Invalid server address: {}", rendezvous_server); + } + let port: u16 = tmp[1].parse()?; + if port == 0 { + bail!("Invalid server address: {}", rendezvous_server); + } + let online_server = format!("{}:{}", tmp[0], port - 1); + let server_addr = socket_client::get_target_addr(&online_server)?; + socket_client::connect_tcp( + server_addr, + Config::get_any_listen_addr(), + RENDEZVOUS_TIMEOUT, + ) + .await +} + +async fn query_online_states_( + ids: &Vec, + timeout: std::time::Duration, +) -> ResultType<(Vec, Vec)> { + let query_begin = Instant::now(); + + let mut msg_out = RendezvousMessage::new(); + msg_out.set_online_request(OnlineRequest { + id: Config::get_id(), + peers: ids.clone(), + ..Default::default() + }); + + loop { + if SHOULD_EXIT.load(Ordering::SeqCst) { + // No need to care about onlines + return Ok((Vec::new(), Vec::new())); + } + + let mut socket = create_online_stream().await?; + socket.send(&msg_out).await?; + match socket.next_timeout(RENDEZVOUS_TIMEOUT).await { + Some(Ok(bytes)) => { + if let Ok(msg_in) = RendezvousMessage::parse_from_bytes(&bytes) { + match msg_in.union { + Some(rendezvous_message::Union::online_response(online_response)) => { + let states = online_response.states; + let mut onlines = Vec::new(); + let mut offlines = Vec::new(); + for i in 0..ids.len() { + // bytes index from left to right + let bit_value = 0x01 << (7 - i % 8); + if (states[i / 8] & bit_value) == bit_value { + onlines.push(ids[i].clone()); + } else { + offlines.push(ids[i].clone()); + } + } + return Ok((onlines, offlines)); + } + _ => { + // ignore + } + } + } + } + Some(Err(e)) => { + log::error!("Failed to receive {e}"); + } + None => { + // TODO: Make sure socket closed? + bail!("Online stream receives None"); + } + } + + if query_begin.elapsed() > timeout { + bail!("Try query onlines timeout {:?}", &timeout); + } + + sleep(300.0).await; + } +} + +#[cfg(test)] +mod tests { + #[test] + fn test_query_onlines() { + super::query_online_states( + vec![ + "152183996".to_owned(), + "165782066".to_owned(), + "155323351".to_owned(), + "460952777".to_owned(), + ], + |onlines: Vec, offlines: Vec| { + println!("onlines: {:?}, offlines: {:?}", &onlines, &offlines); + }, + ); + } +} From aa48711f05142730c2b26cf4a3a778311e16fb4a Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 28 Jul 2022 11:25:22 +0800 Subject: [PATCH 0127/2015] flutter_desktop_online_state: debug online states Signed-off-by: fufesou --- flutter/macos/Runner/bridge_generated.h | 330 ------------------------ 1 file changed, 330 deletions(-) delete mode 100644 flutter/macos/Runner/bridge_generated.h diff --git a/flutter/macos/Runner/bridge_generated.h b/flutter/macos/Runner/bridge_generated.h deleted file mode 100644 index 2d14efe93..000000000 --- a/flutter/macos/Runner/bridge_generated.h +++ /dev/null @@ -1,330 +0,0 @@ -#include -#include -#include - -#define GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT 2 - -#define GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS 4 - -typedef struct wire_uint_8_list { - uint8_t *ptr; - int32_t len; -} wire_uint_8_list; - -typedef struct WireSyncReturnStruct { - uint8_t *ptr; - int32_t len; - bool success; -} WireSyncReturnStruct; - -typedef struct wire_StringList { - struct wire_uint_8_list **ptr; - int32_t len; -} wire_StringList; - -typedef int64_t DartPort; - -typedef bool (*DartPostCObjectFnType)(DartPort port_id, void *message); - -void wire_rustdesk_core_main(int64_t port_); - -void wire_start_global_event_stream(int64_t port_); - -void wire_host_stop_system_key_propagate(int64_t port_, bool stopped); - -void wire_session_connect(int64_t port_, struct wire_uint_8_list *id, bool is_file_transfer); - -void wire_get_session_remember(int64_t port_, struct wire_uint_8_list *id); - -void wire_get_session_toggle_option(int64_t port_, - struct wire_uint_8_list *id, - struct wire_uint_8_list *arg); - -struct WireSyncReturnStruct wire_get_session_toggle_option_sync(struct wire_uint_8_list *id, - struct wire_uint_8_list *arg); - -void wire_get_session_image_quality(int64_t port_, struct wire_uint_8_list *id); - -void wire_get_session_option(int64_t port_, - struct wire_uint_8_list *id, - struct wire_uint_8_list *arg); - -void wire_session_login(int64_t port_, - struct wire_uint_8_list *id, - struct wire_uint_8_list *password, - bool remember); - -void wire_session_close(int64_t port_, struct wire_uint_8_list *id); - -void wire_session_refresh(int64_t port_, struct wire_uint_8_list *id); - -void wire_session_reconnect(int64_t port_, struct wire_uint_8_list *id); - -void wire_session_toggle_option(int64_t port_, - struct wire_uint_8_list *id, - struct wire_uint_8_list *value); - -void wire_session_set_image_quality(int64_t port_, - struct wire_uint_8_list *id, - struct wire_uint_8_list *value); - -void wire_session_lock_screen(int64_t port_, struct wire_uint_8_list *id); - -void wire_session_ctrl_alt_del(int64_t port_, struct wire_uint_8_list *id); - -void wire_session_switch_display(int64_t port_, struct wire_uint_8_list *id, int32_t value); - -void wire_session_input_key(int64_t port_, - struct wire_uint_8_list *id, - struct wire_uint_8_list *name, - bool down, - bool press, - bool alt, - bool ctrl, - bool shift, - bool command); - -void wire_session_input_string(int64_t port_, - struct wire_uint_8_list *id, - struct wire_uint_8_list *value); - -void wire_session_send_chat(int64_t port_, - struct wire_uint_8_list *id, - struct wire_uint_8_list *text); - -void wire_session_send_mouse(int64_t port_, - struct wire_uint_8_list *id, - int32_t mask, - int32_t x, - int32_t y, - bool alt, - bool ctrl, - bool shift, - bool command); - -void wire_session_peer_option(int64_t port_, - struct wire_uint_8_list *id, - struct wire_uint_8_list *name, - struct wire_uint_8_list *value); - -void wire_session_get_peer_option(int64_t port_, - struct wire_uint_8_list *id, - struct wire_uint_8_list *name); - -void wire_session_input_os_password(int64_t port_, - struct wire_uint_8_list *id, - struct wire_uint_8_list *value); - -void wire_session_read_remote_dir(int64_t port_, - struct wire_uint_8_list *id, - struct wire_uint_8_list *path, - bool include_hidden); - -void wire_session_send_files(int64_t port_, - struct wire_uint_8_list *id, - int32_t act_id, - struct wire_uint_8_list *path, - struct wire_uint_8_list *to, - int32_t file_num, - bool include_hidden, - bool is_remote); - -void wire_session_set_confirm_override_file(int64_t port_, - struct wire_uint_8_list *id, - int32_t act_id, - int32_t file_num, - bool need_override, - bool remember, - bool is_upload); - -void wire_session_remove_file(int64_t port_, - struct wire_uint_8_list *id, - int32_t act_id, - struct wire_uint_8_list *path, - int32_t file_num, - bool is_remote); - -void wire_session_read_dir_recursive(int64_t port_, - struct wire_uint_8_list *id, - int32_t act_id, - struct wire_uint_8_list *path, - bool is_remote, - bool show_hidden); - -void wire_session_remove_all_empty_dirs(int64_t port_, - struct wire_uint_8_list *id, - int32_t act_id, - struct wire_uint_8_list *path, - bool is_remote); - -void wire_session_cancel_job(int64_t port_, struct wire_uint_8_list *id, int32_t act_id); - -void wire_session_create_dir(int64_t port_, - struct wire_uint_8_list *id, - int32_t act_id, - struct wire_uint_8_list *path, - bool is_remote); - -void wire_session_read_local_dir_sync(int64_t port_, - struct wire_uint_8_list *id, - struct wire_uint_8_list *path, - bool show_hidden); - -void wire_session_get_platform(int64_t port_, struct wire_uint_8_list *id, bool is_remote); - -void wire_session_load_last_transfer_jobs(int64_t port_, struct wire_uint_8_list *id); - -void wire_session_add_job(int64_t port_, - struct wire_uint_8_list *id, - int32_t act_id, - struct wire_uint_8_list *path, - struct wire_uint_8_list *to, - int32_t file_num, - bool include_hidden, - bool is_remote); - -void wire_session_resume_job(int64_t port_, - struct wire_uint_8_list *id, - int32_t act_id, - bool is_remote); - -void wire_main_get_sound_inputs(int64_t port_); - -void wire_main_change_id(int64_t port_, struct wire_uint_8_list *new_id); - -void wire_main_get_async_status(int64_t port_); - -void wire_main_get_options(int64_t port_); - -void wire_main_set_options(int64_t port_, struct wire_uint_8_list *json); - -void wire_main_test_if_valid_server(int64_t port_, struct wire_uint_8_list *server); - -void wire_main_set_socks(int64_t port_, - struct wire_uint_8_list *proxy, - struct wire_uint_8_list *username, - struct wire_uint_8_list *password); - -void wire_main_get_socks(int64_t port_); - -void wire_main_get_app_name(int64_t port_); - -void wire_main_get_license(int64_t port_); - -void wire_main_get_version(int64_t port_); - -void wire_main_get_fav(int64_t port_); - -void wire_main_store_fav(int64_t port_, struct wire_StringList *favs); - -void wire_main_get_peers(int64_t port_, struct wire_uint_8_list *id); - -void wire_main_get_lan_peers(int64_t port_); - -void wire_main_get_connect_status(int64_t port_); - -void wire_main_check_connect_status(int64_t port_); - -void wire_main_is_using_public_server(int64_t port_); - -void wire_main_has_rendezvous_service(int64_t port_); - -void wire_main_get_api_server(int64_t port_); - -void wire_main_post_request(int64_t port_, - struct wire_uint_8_list *url, - struct wire_uint_8_list *body, - struct wire_uint_8_list *header); - -void wire_main_get_local_option(int64_t port_, struct wire_uint_8_list *key); - -void wire_main_set_local_option(int64_t port_, - struct wire_uint_8_list *key, - struct wire_uint_8_list *value); - -void wire_query_onlines(int64_t port_, struct wire_StringList *ids); - -struct wire_StringList *new_StringList(int32_t len); - -struct wire_uint_8_list *new_uint_8_list(int32_t len); - -void free_WireSyncReturnStruct(struct WireSyncReturnStruct val); - -void store_dart_post_cobject(DartPostCObjectFnType ptr); - -/** - * FFI for rustdesk core's main entry. - * Return true if the app should continue running with UI(possibly Flutter), false if the app should exit. - */ -bool rustdesk_core_main(void); - -static int64_t dummy_method_to_enforce_bundling(void) { - int64_t dummy_var = 0; - dummy_var ^= ((int64_t) (void*) wire_rustdesk_core_main); - dummy_var ^= ((int64_t) (void*) wire_start_global_event_stream); - dummy_var ^= ((int64_t) (void*) wire_host_stop_system_key_propagate); - dummy_var ^= ((int64_t) (void*) wire_session_connect); - dummy_var ^= ((int64_t) (void*) wire_get_session_remember); - dummy_var ^= ((int64_t) (void*) wire_get_session_toggle_option); - dummy_var ^= ((int64_t) (void*) wire_get_session_toggle_option_sync); - dummy_var ^= ((int64_t) (void*) wire_get_session_image_quality); - dummy_var ^= ((int64_t) (void*) wire_get_session_option); - dummy_var ^= ((int64_t) (void*) wire_session_login); - dummy_var ^= ((int64_t) (void*) wire_session_close); - dummy_var ^= ((int64_t) (void*) wire_session_refresh); - dummy_var ^= ((int64_t) (void*) wire_session_reconnect); - dummy_var ^= ((int64_t) (void*) wire_session_toggle_option); - dummy_var ^= ((int64_t) (void*) wire_session_set_image_quality); - dummy_var ^= ((int64_t) (void*) wire_session_lock_screen); - dummy_var ^= ((int64_t) (void*) wire_session_ctrl_alt_del); - dummy_var ^= ((int64_t) (void*) wire_session_switch_display); - dummy_var ^= ((int64_t) (void*) wire_session_input_key); - dummy_var ^= ((int64_t) (void*) wire_session_input_string); - dummy_var ^= ((int64_t) (void*) wire_session_send_chat); - dummy_var ^= ((int64_t) (void*) wire_session_send_mouse); - dummy_var ^= ((int64_t) (void*) wire_session_peer_option); - dummy_var ^= ((int64_t) (void*) wire_session_get_peer_option); - dummy_var ^= ((int64_t) (void*) wire_session_input_os_password); - dummy_var ^= ((int64_t) (void*) wire_session_read_remote_dir); - dummy_var ^= ((int64_t) (void*) wire_session_send_files); - dummy_var ^= ((int64_t) (void*) wire_session_set_confirm_override_file); - dummy_var ^= ((int64_t) (void*) wire_session_remove_file); - dummy_var ^= ((int64_t) (void*) wire_session_read_dir_recursive); - dummy_var ^= ((int64_t) (void*) wire_session_remove_all_empty_dirs); - dummy_var ^= ((int64_t) (void*) wire_session_cancel_job); - dummy_var ^= ((int64_t) (void*) wire_session_create_dir); - dummy_var ^= ((int64_t) (void*) wire_session_read_local_dir_sync); - dummy_var ^= ((int64_t) (void*) wire_session_get_platform); - dummy_var ^= ((int64_t) (void*) wire_session_load_last_transfer_jobs); - dummy_var ^= ((int64_t) (void*) wire_session_add_job); - dummy_var ^= ((int64_t) (void*) wire_session_resume_job); - dummy_var ^= ((int64_t) (void*) wire_main_get_sound_inputs); - dummy_var ^= ((int64_t) (void*) wire_main_change_id); - dummy_var ^= ((int64_t) (void*) wire_main_get_async_status); - dummy_var ^= ((int64_t) (void*) wire_main_get_options); - dummy_var ^= ((int64_t) (void*) wire_main_set_options); - dummy_var ^= ((int64_t) (void*) wire_main_test_if_valid_server); - dummy_var ^= ((int64_t) (void*) wire_main_set_socks); - dummy_var ^= ((int64_t) (void*) wire_main_get_socks); - dummy_var ^= ((int64_t) (void*) wire_main_get_app_name); - dummy_var ^= ((int64_t) (void*) wire_main_get_license); - dummy_var ^= ((int64_t) (void*) wire_main_get_version); - dummy_var ^= ((int64_t) (void*) wire_main_get_fav); - dummy_var ^= ((int64_t) (void*) wire_main_store_fav); - dummy_var ^= ((int64_t) (void*) wire_main_get_peers); - dummy_var ^= ((int64_t) (void*) wire_main_get_lan_peers); - dummy_var ^= ((int64_t) (void*) wire_main_get_connect_status); - dummy_var ^= ((int64_t) (void*) wire_main_check_connect_status); - dummy_var ^= ((int64_t) (void*) wire_main_is_using_public_server); - dummy_var ^= ((int64_t) (void*) wire_main_has_rendezvous_service); - dummy_var ^= ((int64_t) (void*) wire_main_get_api_server); - dummy_var ^= ((int64_t) (void*) wire_main_post_request); - dummy_var ^= ((int64_t) (void*) wire_main_get_local_option); - dummy_var ^= ((int64_t) (void*) wire_main_set_local_option); - dummy_var ^= ((int64_t) (void*) wire_query_onlines); - dummy_var ^= ((int64_t) (void*) new_StringList); - dummy_var ^= ((int64_t) (void*) new_uint_8_list); - dummy_var ^= ((int64_t) (void*) free_WireSyncReturnStruct); - dummy_var ^= ((int64_t) (void*) store_dart_post_cobject); - return dummy_var; -} \ No newline at end of file From dab8fc6cc9c8b13e7afc52ac6aa6f35d9f9143f1 Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 28 Jul 2022 14:06:02 +0800 Subject: [PATCH 0128/2015] flutter_desktop: load popup menu items onTap Signed-off-by: fufesou --- flutter/lib/desktop/widgets/peer_widget.dart | 4 ++++ .../lib/desktop/widgets/peercard_widget.dart | 24 ++++++++++++------- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/flutter/lib/desktop/widgets/peer_widget.dart b/flutter/lib/desktop/widgets/peer_widget.dart index e0c82bb30..42cb8eb1d 100644 --- a/flutter/lib/desktop/widgets/peer_widget.dart +++ b/flutter/lib/desktop/widgets/peer_widget.dart @@ -167,6 +167,7 @@ class RecentPeerWidget extends BasePeerWidget { } Future> _loadPeers() async { + debugPrint("call RecentPeerWidget _loadPeers"); return gFFI.peers(); } } @@ -180,6 +181,7 @@ class FavoritePeerWidget extends BasePeerWidget { @override Future> _loadPeers() async { + debugPrint("call FavoritePeerWidget _loadPeers"); return await gFFI.bind.mainGetFav().then((peers) async { final peersEntities = await Future.wait(peers .map((id) => gFFI.bind.mainGetPeers(id: id)) @@ -206,6 +208,7 @@ class DiscoveredPeerWidget extends BasePeerWidget { } Future> _loadPeers() async { + debugPrint("call DiscoveredPeerWidget _loadPeers"); return await gFFI.bind.mainGetLanPeers().then((peers_string) { debugPrint(peers_string); return []; @@ -222,6 +225,7 @@ class AddressBookPeerWidget extends BasePeerWidget { } Future> _loadPeers() async { + debugPrint("call AddressBookPeerWidget _loadPeers"); return gFFI.abModel.peers.map((e) { return Peer.fromJson(e['id'], e); }).toList(); diff --git a/flutter/lib/desktop/widgets/peercard_widget.dart b/flutter/lib/desktop/widgets/peercard_widget.dart index b8c6d54de..02b146457 100644 --- a/flutter/lib/desktop/widgets/peercard_widget.dart +++ b/flutter/lib/desktop/widgets/peercard_widget.dart @@ -7,11 +7,13 @@ import '../../common.dart'; import '../../models/model.dart'; import '../../models/peer_model.dart'; +typedef PopupMenuItemsFunc = Future>> Function(); + class _PeerCard extends StatefulWidget { final Peer peer; - final List> popupMenuItems; + final PopupMenuItemsFunc popupMenuItemsFunc; - _PeerCard({required this.peer, required this.popupMenuItems, Key? key}) + _PeerCard({required this.peer, required this.popupMenuItemsFunc, Key? key}) : super(key: key); @override @@ -148,7 +150,7 @@ class _PeerCardState extends State<_PeerCard> { var value = await showMenu( context: context, position: this._menuPos, - items: super.widget.popupMenuItems, + items: await super.widget.popupMenuItemsFunc(), elevation: 8, ); if (value == 'remove') { @@ -271,17 +273,18 @@ abstract class BasePeerCard extends StatelessWidget { @override Widget build(BuildContext context) { - return _PeerCard(peer: peer, popupMenuItems: _getPopupMenuItems()); + return _PeerCard(peer: peer, popupMenuItemsFunc: _getPopupMenuItems); } @protected - List> _getPopupMenuItems(); + Future>> _getPopupMenuItems(); } class RecentPeerCard extends BasePeerCard { RecentPeerCard({required Peer peer, Key? key}) : super(peer: peer, key: key); - List> _getPopupMenuItems() { + Future>> _getPopupMenuItems() async { + debugPrint("call RecentPeerCard _getPopupMenuItems"); return [ PopupMenuItem( child: Text(translate('Connect')), value: 'connect'), @@ -304,7 +307,8 @@ class FavoritePeerCard extends BasePeerCard { FavoritePeerCard({required Peer peer, Key? key}) : super(peer: peer, key: key); - List> _getPopupMenuItems() { + Future>> _getPopupMenuItems() async { + debugPrint("call FavoritePeerCard _getPopupMenuItems"); return [ PopupMenuItem( child: Text(translate('Connect')), value: 'connect'), @@ -327,7 +331,8 @@ class DiscoveredPeerCard extends BasePeerCard { DiscoveredPeerCard({required Peer peer, Key? key}) : super(peer: peer, key: key); - List> _getPopupMenuItems() { + Future>> _getPopupMenuItems() async { + debugPrint("call DiscoveredPeerCard _getPopupMenuItems"); return [ PopupMenuItem( child: Text(translate('Connect')), value: 'connect'), @@ -350,7 +355,8 @@ class AddressBookPeerCard extends BasePeerCard { AddressBookPeerCard({required Peer peer, Key? key}) : super(peer: peer, key: key); - List> _getPopupMenuItems() { + Future>> _getPopupMenuItems() async { + debugPrint("call AddressBookPeerCard _getPopupMenuItems"); return [ PopupMenuItem( child: Text(translate('Connect')), value: 'connect'), From 6b99d4d82eeb0513cc7130a80ee2d2d6198d625e Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 29 Jul 2022 12:03:24 +0800 Subject: [PATCH 0129/2015] add: peer rename Signed-off-by: Kingtous --- .../lib/desktop/pages/connection_page.dart | 188 ++---------------- .../lib/desktop/widgets/peercard_widget.dart | 147 ++++++++++++-- flutter/lib/models/ab_model.dart | 10 + flutter/lib/models/model.dart | 8 + src/flutter_ffi.rs | 14 +- 5 files changed, 168 insertions(+), 199 deletions(-) diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 42c41f8b9..7a5c47c06 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -105,7 +105,7 @@ class _ConnectionPageState extends State { RecentPeerWidget(), FavoritePeerWidget(), DiscoveredPeerWidget(), - AddressBookPeerWidget(), + // AddressBookPeerWidget(), // FutureBuilder( // future: getPeers(rType: RemoteType.recently), // builder: (context, snapshot) { @@ -133,15 +133,15 @@ class _ConnectionPageState extends State { // return Offstage(); // } // }), - // FutureBuilder( - // future: buildAddressBook(context), - // builder: (context, snapshot) { - // if (snapshot.hasData) { - // return snapshot.data!; - // } else { - // return Offstage(); - // } - // }), + FutureBuilder( + future: buildAddressBook(context), + builder: (context, snapshot) { + if (snapshot.hasData) { + return snapshot.data!; + } else { + return Offstage(); + } + }), ]).paddingSymmetric(horizontal: 12.0, vertical: 4.0)) ], )), @@ -334,154 +334,6 @@ class _ConnectionPageState extends State { return true; } - /// Get all the saved peers. - Future getPeers({RemoteType rType = RemoteType.recently}) async { - final space = 8.0; - final cards = []; - List peers; - switch (rType) { - case RemoteType.recently: - peers = gFFI.peers(); - break; - case RemoteType.favorite: - peers = await gFFI.bind.mainGetFav().then((peers) async { - final peersEntities = await Future.wait(peers - .map((id) => gFFI.bind.mainGetPeers(id: id)) - .toList(growable: false)) - .then((peers_str) { - final len = peers_str.length; - final ps = List.empty(growable: true); - for (var i = 0; i < len; i++) { - print("${peers[i]}: ${peers_str[i]}"); - ps.add(Peer.fromJson(peers[i], jsonDecode(peers_str[i])['info'])); - } - return ps; - }); - return peersEntities; - }); - break; - case RemoteType.discovered: - peers = await gFFI.bind.mainGetLanPeers().then((peers_string) { - print(peers_string); - return []; - }); - break; - case RemoteType.addressBook: - peers = gFFI.abModel.peers.map((e) { - return Peer.fromJson(e['id'], e); - }).toList(); - break; - } - peers.forEach((p) { - var deco = Rx(BoxDecoration( - border: Border.all(color: Colors.transparent, width: 1.0), - borderRadius: BorderRadius.circular(20))); - cards.add(Obx( - () => Offstage( - offstage: !hitTag(gFFI.abModel.selectedTags, p.tags) && - rType == RemoteType.addressBook, - child: Container( - width: 225, - height: 150, - child: Card( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20)), - child: MouseRegion( - onEnter: (evt) { - deco.value = BoxDecoration( - border: Border.all(color: Colors.blue, width: 1.0), - borderRadius: BorderRadius.circular(20)); - }, - onExit: (evt) { - deco.value = BoxDecoration( - border: - Border.all(color: Colors.transparent, width: 1.0), - borderRadius: BorderRadius.circular(20)); - }, - child: Obx( - () => Container( - decoration: deco.value, - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Expanded( - child: Container( - decoration: BoxDecoration( - color: - str2color('${p.id}${p.platform}', 0x7f), - borderRadius: BorderRadius.only( - topLeft: Radius.circular(20), - topRight: Radius.circular(20), - ), - ), - child: Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.center, - children: [ - Container( - padding: const EdgeInsets.all(6), - child: getPlatformImage( - '${p.platform}'), - ), - Row( - children: [ - Expanded( - child: Tooltip( - message: - '${p.username}@${p.hostname}', - child: Text( - '${p.username}@${p.hostname}', - style: TextStyle( - color: Colors.white70, - fontSize: 12), - textAlign: TextAlign.center, - overflow: - TextOverflow.ellipsis, - ), - ), - ), - ], - ), - ], - ).paddingAll(4.0), - ), - ], - ), - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text("${p.id}"), - InkWell( - child: Icon(Icons.more_vert), - onTapDown: (e) { - final x = e.globalPosition.dx; - final y = e.globalPosition.dy; - _menuPos = - RelativeRect.fromLTRB(x, y, x, y); - }, - onTap: () { - showPeerMenu(context, p.id, rType); - }), - ], - ).paddingSymmetric(vertical: 8.0, horizontal: 12.0) - ], - ), - ), - ), - ))), - ), - )); - }); - return SingleChildScrollView( - child: Wrap(children: cards, spacing: space, runSpacing: space)); - } - /// Show the peer menu and handle user's choice. /// User might remove the peer or send a file to the peer. void showPeerMenu(BuildContext context, String id, RemoteType rType) async { @@ -736,24 +588,8 @@ class _ConnectionPageState extends State { ), ).marginOnly(right: 8.0), Expanded( - child: FutureBuilder( - future: getPeers(rType: RemoteType.addressBook), - builder: (context, snapshot) { - if (snapshot.hasData) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [Expanded(child: snapshot.data!)], - ); - } else if (snapshot.hasError) { - return Container( - alignment: Alignment.center, - child: Text('${snapshot.error}')); - } else { - return Container( - alignment: Alignment.center, - child: CircularProgressIndicator()); - } - }), + child: Align( + alignment: Alignment.topLeft, child: AddressBookPeerWidget()), ) ], ); diff --git a/flutter/lib/desktop/widgets/peercard_widget.dart b/flutter/lib/desktop/widgets/peercard_widget.dart index 02b146457..f5a4156c3 100644 --- a/flutter/lib/desktop/widgets/peercard_widget.dart +++ b/flutter/lib/desktop/widgets/peercard_widget.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:get/get.dart'; import 'package:contextmenu/contextmenu.dart'; @@ -8,12 +9,18 @@ import '../../models/model.dart'; import '../../models/peer_model.dart'; typedef PopupMenuItemsFunc = Future>> Function(); +enum PeerType { recent, fav, discovered, ab } class _PeerCard extends StatefulWidget { final Peer peer; final PopupMenuItemsFunc popupMenuItemsFunc; + final PeerType type; - _PeerCard({required this.peer, required this.popupMenuItemsFunc, Key? key}) + _PeerCard( + {required this.peer, + required this.popupMenuItemsFunc, + Key? key, + required this.type}) : super(key: key); @override @@ -78,15 +85,28 @@ class _PeerCardState extends State<_PeerCard> { Row( children: [ Expanded( - child: Tooltip( - message: '${peer.username}@${peer.hostname}', - child: Text( - '${peer.username}@${peer.hostname}', - style: TextStyle( - color: Colors.white70, fontSize: 12), - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - ), + child: FutureBuilder( + future: gFFI.getPeerOption(peer.id, 'alias'), + builder: (_, snapshot) { + if (snapshot.hasData) { + final name = snapshot.data!.isEmpty + ? '${peer.username}@${peer.hostname}' + : snapshot.data!; + return Tooltip( + message: name, + child: Text( + name, + style: TextStyle( + color: Colors.white70, + fontSize: 12), + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + ), + ); + } else { + return Text(translate("Loading")); + } + }, ), ), ], @@ -169,6 +189,8 @@ class _PeerCardState extends State<_PeerCard> { setState(() {}); } else if (value == 'ab-edit-tag') { _abEditTag(id); + } else if (value == 'rename') { + _rename(id); } } @@ -265,15 +287,101 @@ class _PeerCardState extends State<_PeerCard> { ); }); } + + void _rename(String id) async { + var isInProgress = false; + var name = await gFFI.getPeerOption(id, 'alias'); + if (widget.type == PeerType.ab) { + final peer = gFFI.abModel.peers.firstWhere((p) => id == p['id']); + if (peer == null) { + // this should not happen + } else { + name = peer['alias'] ?? ""; + } + } + final k = GlobalKey(); + DialogManager.show((setState, close) { + return CustomAlertDialog( + title: Text(translate("Rename")), + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + child: Form( + key: k, + child: TextFormField( + controller: TextEditingController(text: name), + decoration: InputDecoration(border: OutlineInputBorder()), + onChanged: (newStr) { + name = newStr; + }, + validator: (s) { + if (s == null || s.isEmpty) { + return translate("Empty"); + } + return null; + }, + onSaved: (s) { + name = s ?? "unnamed"; + }, + ), + ), + ), + Offstage(offstage: !isInProgress, child: LinearProgressIndicator()) + ], + ), + actions: [ + TextButton( + onPressed: () { + close(); + }, + child: Text(translate("Cancel"))), + TextButton( + onPressed: () async { + setState(() { + isInProgress = true; + }); + if (k.currentState != null) { + if (k.currentState!.validate()) { + k.currentState!.save(); + await gFFI.setPeerOption(id, 'alias', name); + if (widget.type == PeerType.ab) { + gFFI.abModel.setPeerOption(id, 'alias', name); + await gFFI.abModel.updateAb(); + } else { + Future.delayed(Duration.zero, () { + this.setState(() {}); + }); + } + close(); + } + } + setState(() { + isInProgress = false; + }); + }, + child: Text(translate("OK"))), + ], + ); + }); + } } abstract class BasePeerCard extends StatelessWidget { final Peer peer; - BasePeerCard({required this.peer, Key? key}) : super(key: key); + final PeerType type; + + BasePeerCard({required this.peer, required this.type, Key? key}) + : super(key: key); @override Widget build(BuildContext context) { - return _PeerCard(peer: peer, popupMenuItemsFunc: _getPopupMenuItems); + return _PeerCard( + peer: peer, + popupMenuItemsFunc: _getPopupMenuItems, + type: type, + ); } @protected @@ -281,7 +389,8 @@ abstract class BasePeerCard extends StatelessWidget { } class RecentPeerCard extends BasePeerCard { - RecentPeerCard({required Peer peer, Key? key}) : super(peer: peer, key: key); + RecentPeerCard({required Peer peer, Key? key}) + : super(peer: peer, key: key, type: PeerType.recent); Future>> _getPopupMenuItems() async { debugPrint("call RecentPeerCard _getPopupMenuItems"); @@ -297,15 +406,13 @@ class RecentPeerCard extends BasePeerCard { PopupMenuItem( child: Text(translate('Unremember Password')), value: 'unremember-password'), - PopupMenuItem( - child: Text(translate('Edit Tag')), value: 'ab-edit-tag'), ]; } } class FavoritePeerCard extends BasePeerCard { FavoritePeerCard({required Peer peer, Key? key}) - : super(peer: peer, key: key); + : super(peer: peer, key: key, type: PeerType.fav); Future>> _getPopupMenuItems() async { debugPrint("call FavoritePeerCard _getPopupMenuItems"); @@ -329,7 +436,7 @@ class FavoritePeerCard extends BasePeerCard { class DiscoveredPeerCard extends BasePeerCard { DiscoveredPeerCard({required Peer peer, Key? key}) - : super(peer: peer, key: key); + : super(peer: peer, key: key, type: PeerType.discovered); Future>> _getPopupMenuItems() async { debugPrint("call DiscoveredPeerCard _getPopupMenuItems"); @@ -345,15 +452,13 @@ class DiscoveredPeerCard extends BasePeerCard { PopupMenuItem( child: Text(translate('Unremember Password')), value: 'unremember-password'), - PopupMenuItem( - child: Text(translate('Edit Tag')), value: 'ab-edit-tag'), ]; } } class AddressBookPeerCard extends BasePeerCard { AddressBookPeerCard({required Peer peer, Key? key}) - : super(peer: peer, key: key); + : super(peer: peer, key: key, type: PeerType.ab); Future>> _getPopupMenuItems() async { debugPrint("call AddressBookPeerCard _getPopupMenuItems"); @@ -372,6 +477,8 @@ class AddressBookPeerCard extends BasePeerCard { value: 'unremember-password'), PopupMenuItem( child: Text(translate('Add to Favorites')), value: 'add-fav'), + PopupMenuItem( + child: Text(translate('Edit Tag')), value: 'ab-edit-tag'), ]; } } diff --git a/flutter/lib/models/ab_model.dart b/flutter/lib/models/ab_model.dart index 165e3d8d1..bfdb6fa1a 100644 --- a/flutter/lib/models/ab_model.dart +++ b/flutter/lib/models/ab_model.dart @@ -141,6 +141,16 @@ class AbModel with ChangeNotifier { } } + void setPeerOption(String id, String key, String value) { + final it = peers.where((p0) => p0['id'] == id); + if (it.isEmpty) { + debugPrint("${id} is not exists"); + return; + } else { + it.first[key] = value; + } + } + void clear() { peers.clear(); tags.clear(); diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index bc64ff6f5..67313623c 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -1010,6 +1010,14 @@ class FFI { return bind.mainSetLocalOption(key: key, value: value); } + Future getPeerOption(String id, String key) { + return bind.mainGetPeerOption(id: id, key: key); + } + + Future setPeerOption(String id, String key, String value) { + return bind.mainSetPeerOption(id: id, key: key, value: value); + } + void setOption(String name, String value) { Map res = Map() ..["name"] = name diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 57e7db87d..30c9c7591 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -22,9 +22,9 @@ use crate::ui_interface; use crate::ui_interface::{ change_id, check_connect_status, get_api_server, get_app_name, get_async_job_status, get_connect_status, get_fav, get_id, get_lan_peers, get_license, get_local_option, get_options, - get_peer, get_socks, get_sound_inputs, get_uuid, get_version, has_rendezvous_service, - is_ok_change_id, post_request, set_local_option, set_options, set_socks, store_fav, - test_if_valid_server, using_public_server, + get_peer, get_peer_option, get_socks, get_sound_inputs, get_uuid, get_version, + has_rendezvous_service, is_ok_change_id, post_request, set_local_option, set_options, + set_peer_option, set_socks, store_fav, test_if_valid_server, using_public_server, }; fn initialize(app_dir: &str) { @@ -496,6 +496,14 @@ pub fn main_get_uuid() -> String { get_uuid() } +pub fn main_get_peer_option(id: String, key: String) -> String { + get_peer_option(id, key) +} + +pub fn main_set_peer_option(id: String, key: String, value: String) { + set_peer_option(id, key, value) +} + /// FFI for **get** commands which are idempotent. /// Return result in c string. /// From 608f02ea219888729797d60a626fb31ae9682f67 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 29 Jul 2022 16:47:24 +0800 Subject: [PATCH 0130/2015] feat: dark theme Signed-off-by: Kingtous --- flutter/lib/common.dart | 21 +++++++ .../lib/desktop/pages/connection_page.dart | 10 +--- .../lib/desktop/pages/desktop_home_page.dart | 20 +++++-- .../lib/desktop/pages/file_manager_page.dart | 2 +- .../lib/desktop/widgets/peercard_widget.dart | 58 +++++++++++-------- flutter/lib/main.dart | 21 ++++--- src/flutter_ffi.rs | 14 +++-- 7 files changed, 97 insertions(+), 49 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index b896fdf9f..47a663768 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -4,6 +4,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/instance_manager.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'models/model.dart'; @@ -38,6 +39,24 @@ class MyTheme { static const Color idColor = Color(0xFF00B6F0); static const Color darkGray = Color(0xFFB9BABC); static const Color dark = Colors.black87; + + static ThemeData lightTheme = ThemeData( + brightness: Brightness.light, + primarySwatch: Colors.blue, + visualDensity: VisualDensity.adaptivePlatformDensity, + tabBarTheme: TabBarTheme(labelColor: Colors.black87), + ); + static ThemeData darkTheme = ThemeData( + brightness: Brightness.dark, + primarySwatch: Colors.blue, + visualDensity: VisualDensity.adaptivePlatformDensity, + tabBarTheme: TabBarTheme(labelColor: Colors.white70)); +} + +bool isDarkTheme() { + final isDark = "Y" == Get.find().getString("darkTheme"); + debugPrint("current is dark theme: $isDark"); + return isDark; } final ButtonStyle flatButtonStyle = TextButton.styleFrom( @@ -327,4 +346,6 @@ Future initGlobalFFI() async { await _globalFFI.ffiModel.init(); // trigger connection status updater await _globalFFI.bind.mainCheckConnectStatus(); + // global shared preference + await Get.putAsync(() => SharedPreferences.getInstance()); } \ No newline at end of file diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 7a5c47c06..fe11857f3 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -16,7 +16,6 @@ import '../../mobile/pages/home_page.dart'; import '../../mobile/pages/scan_page.dart'; import '../../mobile/pages/settings_page.dart'; import '../../models/model.dart'; -import '../../models/peer_model.dart'; enum RemoteType { recently, favorite, discovered, addressBook } @@ -60,7 +59,7 @@ class _ConnectionPageState extends State { Widget build(BuildContext context) { if (_idController.text.isEmpty) _idController.text = gFFI.getId(); return Container( - decoration: BoxDecoration(color: MyTheme.grayBg), + decoration: BoxDecoration(color: isDarkTheme() ? null : MyTheme.grayBg), child: Column( mainAxisAlignment: MainAxisAlignment.start, mainAxisSize: MainAxisSize.max, @@ -83,7 +82,6 @@ class _ConnectionPageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ TabBar( - labelColor: Colors.black87, isScrollable: true, indicatorSize: TabBarIndicatorSize.label, tabs: [ @@ -205,7 +203,7 @@ class _ConnectionPageState extends State { width: 500, padding: EdgeInsets.symmetric(horizontal: 16, vertical: 24), decoration: BoxDecoration( - color: MyTheme.white, + color: isDarkTheme() ? null : MyTheme.white, borderRadius: const BorderRadius.all(Radius.circular(13)), ), child: Ink( @@ -235,13 +233,11 @@ class _ConnectionPageState extends State { helperStyle: TextStyle( fontWeight: FontWeight.bold, fontSize: 16, - color: MyTheme.dark, ), labelStyle: TextStyle( fontWeight: FontWeight.w600, fontSize: 26, letterSpacing: 0.2, - color: MyTheme.dark, ), ), controller: _idController, @@ -269,7 +265,6 @@ class _ConnectionPageState extends State { translate( "Transfer File", ), - style: TextStyle(color: MyTheme.dark), ), ), ), @@ -528,7 +523,6 @@ class _ConnectionPageState extends State { shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), side: BorderSide(color: MyTheme.grayBg)), - color: Colors.white, child: Container( width: 200, height: double.infinity, diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 47c066c9c..a162c3535 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -10,6 +10,7 @@ import 'package:flutter_hbb/desktop/widgets/titlebar_widget.dart'; import 'package:flutter_hbb/models/model.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:tray_manager/tray_manager.dart'; import 'package:url_launcher/url_launcher_string.dart'; @@ -64,7 +65,6 @@ class _DesktopHomePageState extends State with TrayListener { return ChangeNotifierProvider.value( value: gFFI.serverModel, child: Container( - decoration: BoxDecoration(color: MyTheme.white), child: Column( children: [ buildTip(context), @@ -339,13 +339,24 @@ class _DesktopHomePageState extends State with TrayListener { super.dispose(); } + void changeTheme(String choice) async { + if (choice == "Y") { + Get.changeTheme(MyTheme.darkTheme); + } else { + Get.changeTheme(MyTheme.lightTheme); + } + Get.find().setString("darkTheme", choice); + } + void onSelectMenu(String value) { if (value.startsWith('enable-')) { final option = gFFI.getOption(value); gFFI.setOption(value, option == "N" ? "" : "N"); } else if (value.startsWith('allow-')) { final option = gFFI.getOption(value); - gFFI.setOption(value, option == "Y" ? "" : "Y"); + final choice = option == "Y" ? "" : "Y"; + gFFI.setOption(value, choice); + changeTheme(choice); } else if (value == "stop-service") { final option = gFFI.getOption(value); gFFI.setOption(value, option == "Y" ? "" : "Y"); @@ -367,9 +378,8 @@ class _DesktopHomePageState extends State with TrayListener { } PopupMenuItem genEnablePopupMenuItem(String label, String value) { - final isEnable = label.startsWith('enable-') - ? gFFI.getOption(value) != "N" - : gFFI.getOption(value) != "Y"; + final v = gFFI.getOption(value); + final isEnable = value.startsWith('enable-') ? v != "N" : v == "Y"; return PopupMenuItem( child: Row( children: [ diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index de6d981ee..e37f56404 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -72,7 +72,7 @@ class _FileManagerPageState extends State return false; }, child: Scaffold( - backgroundColor: MyTheme.grayBg, + backgroundColor: isDarkTheme() ? MyTheme.dark : MyTheme.grayBg, body: Row( children: [ Flexible(flex: 3, child: body(isLocal: true)), diff --git a/flutter/lib/desktop/widgets/peercard_widget.dart b/flutter/lib/desktop/widgets/peercard_widget.dart index f5a4156c3..39acd0bf2 100644 --- a/flutter/lib/desktop/widgets/peercard_widget.dart +++ b/flutter/lib/desktop/widgets/peercard_widget.dart @@ -1,8 +1,7 @@ +import 'package:contextmenu/contextmenu.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:get/get.dart'; -import 'package:contextmenu/contextmenu.dart'; import '../../common.dart'; import '../../models/model.dart'; @@ -16,11 +15,10 @@ class _PeerCard extends StatefulWidget { final PopupMenuItemsFunc popupMenuItemsFunc; final PeerType type; - _PeerCard( - {required this.peer, - required this.popupMenuItemsFunc, - Key? key, - required this.type}) + _PeerCard({required this.peer, + required this.popupMenuItemsFunc, + Key? key, + required this.type}) : super(key: key); @override @@ -28,11 +26,13 @@ class _PeerCard extends StatefulWidget { } /// State for the connection page. -class _PeerCardState extends State<_PeerCard> { +class _PeerCardState extends State<_PeerCard> + with AutomaticKeepAliveClientMixin { var _menuPos; @override Widget build(BuildContext context) { + super.build(context); final peer = super.widget.peer; var deco = Rx(BoxDecoration( border: Border.all(color: Colors.transparent, width: 1.0), @@ -54,10 +54,9 @@ class _PeerCardState extends State<_PeerCard> { )); } - Widget _buildPeerTile( - BuildContext context, Peer peer, Rx deco) { + Widget _buildPeerTile(BuildContext context, Peer peer, Rx deco) { return Obx( - () => Container( + () => Container( decoration: deco.value, child: Column( mainAxisSize: MainAxisSize.min, @@ -104,7 +103,16 @@ class _PeerCardState extends State<_PeerCard> { ), ); } else { - return Text(translate("Loading")); + // alias has not arrived + return Center( + child: Text( + '${peer.username}@${peer.hostname}', + style: TextStyle( + color: Colors.white70, + fontSize: 12), + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + )); } }, ), @@ -127,7 +135,7 @@ class _PeerCardState extends State<_PeerCard> { child: CircleAvatar( radius: 5, backgroundColor: - peer.online ? Colors.green : Colors.yellow)), + peer.online ? Colors.green : Colors.yellow)), Text('${peer.id}') ]), InkWell( @@ -175,13 +183,12 @@ class _PeerCardState extends State<_PeerCard> { ); if (value == 'remove') { setState(() => gFFI.setByName('remove', '$id')); - () async { + () async { removePreference(id); }(); } else if (value == 'file') { _connect(id, isFileTransfer: true); - } else if (value == 'add-fav') { - } else if (value == 'connect') { + } else if (value == 'add-fav') {} else if (value == 'connect') { _connect(id, isFileTransfer: false); } else if (value == 'ab-delete') { gFFI.abModel.deletePeer(id); @@ -191,6 +198,8 @@ class _PeerCardState extends State<_PeerCard> { _abEditTag(id); } else if (value == 'rename') { _rename(id); + } else if (value == 'unremember-password') { + await gFFI.bind.mainForgetPassword(id: id); } } @@ -211,7 +220,7 @@ class _PeerCardState extends State<_PeerCard> { child: GestureDetector( onTap: onTap, child: Obx( - () => Container( + () => Container( decoration: BoxDecoration( color: rxTags.contains(tagName) ? Colors.blue : null, border: Border.all(color: MyTheme.darkGray), @@ -255,12 +264,12 @@ class _PeerCardState extends State<_PeerCard> { child: Wrap( children: tags .map((e) => _buildTag(e, selectedTag, onTap: () { - if (selectedTag.contains(e)) { - selectedTag.remove(e); - } else { - selectedTag.add(e); - } - })) + if (selectedTag.contains(e)) { + selectedTag.remove(e); + } else { + selectedTag.add(e); + } + })) .toList(growable: false), ), ), @@ -366,6 +375,9 @@ class _PeerCardState extends State<_PeerCard> { ); }); } + + @override + bool get wantKeepAlive => true; } abstract class BasePeerCard extends StatelessWidget { diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index bb6684438..f2ebb3134 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -6,6 +6,7 @@ import 'package:flutter_hbb/desktop/screen/desktop_file_transfer_screen.dart'; import 'package:flutter_hbb/desktop/screen/desktop_remote_screen.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; import 'package:get/route_manager.dart'; import 'package:provider/provider.dart'; @@ -32,6 +33,10 @@ Future main(List args) async { runRustDeskApp(args); } +ThemeData getCurrentTheme() { + return isDarkTheme() ? MyTheme.darkTheme : MyTheme.darkTheme; +} + void runRustDeskApp(List args) async { if (!isDesktop) { runApp(App()); @@ -47,12 +52,17 @@ void runRustDeskApp(List args) async { WindowType wType = type.windowType; switch (wType) { case WindowType.RemoteDesktop: - runApp(DesktopRemoteScreen( - params: argument, + runApp(GetMaterialApp( + theme: getCurrentTheme(), + home: DesktopRemoteScreen( + params: argument, + ), )); break; case WindowType.FileTransfer: - runApp(DesktopFileTransferScreen(params: argument)); + runApp(GetMaterialApp( + theme: getCurrentTheme(), + home: DesktopFileTransferScreen(params: argument))); break; default: break; @@ -85,10 +95,7 @@ class App extends StatelessWidget { navigatorKey: globalKey, debugShowCheckedModeBanner: false, title: 'RustDesk', - theme: ThemeData( - primarySwatch: Colors.blue, - visualDensity: VisualDensity.adaptivePlatformDensity, - ), + theme: getCurrentTheme(), home: isDesktop ? DesktopHomePage() : !isAndroid diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 30c9c7591..afbe35ec8 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -20,11 +20,11 @@ use crate::flutter::{self, Session, SESSIONS}; use crate::start_server; use crate::ui_interface; use crate::ui_interface::{ - change_id, check_connect_status, get_api_server, get_app_name, get_async_job_status, - get_connect_status, get_fav, get_id, get_lan_peers, get_license, get_local_option, get_options, - get_peer, get_peer_option, get_socks, get_sound_inputs, get_uuid, get_version, - has_rendezvous_service, is_ok_change_id, post_request, set_local_option, set_options, - set_peer_option, set_socks, store_fav, test_if_valid_server, using_public_server, + change_id, check_connect_status, forget_password, get_api_server, get_app_name, + get_async_job_status, get_connect_status, get_fav, get_id, get_lan_peers, get_license, + get_local_option, get_options, get_peer, get_peer_option, get_socks, get_sound_inputs, + get_uuid, get_version, has_rendezvous_service, is_ok_change_id, post_request, set_local_option, + set_options, set_peer_option, set_socks, store_fav, test_if_valid_server, using_public_server, }; fn initialize(app_dir: &str) { @@ -504,6 +504,10 @@ pub fn main_set_peer_option(id: String, key: String, value: String) { set_peer_option(id, key, value) } +pub fn main_forget_password(id: String) { + forget_password(id) +} + /// FFI for **get** commands which are idempotent. /// Return result in c string. /// From 92c4ee15608d2b6eea0e7c6b1159ddee2f124989 Mon Sep 17 00:00:00 2001 From: Asura Date: Mon, 1 Aug 2022 00:52:07 +0800 Subject: [PATCH 0131/2015] Fix character generation, altgr only takes effect locally --- Cargo.lock | 2 +- src/ui/remote.rs | 41 ++++++++++++++++++++++++++++++++--------- 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 833cbe12a..9d807f12e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3795,7 +3795,7 @@ dependencies = [ [[package]] name = "rdev" version = "0.5.0-2" -source = "git+https://github.com/asur4s/rdev#c3a896bcb4a10d171dee1aa685c90abadf8946d2" +source = "git+https://github.com/asur4s/rdev#d009906ba983f26c7b6f6f1a5e3c76bf43164294" dependencies = [ "cocoa", "core-foundation 0.9.3", diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 1b5331609..213158bfc 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -43,7 +43,7 @@ use hbb_common::{ Stream, }; use hbb_common::{config::TransferSerde, fs::TransferJobMeta}; -use rdev::{Event, EventType::*, Key as RdevKey}; +use rdev::{Event, EventType::*, Key as RdevKey, Keyboard as RdevKeyboard, KeyboardState}; #[cfg(windows)] use crate::clipboard_file::*; @@ -58,6 +58,7 @@ lazy_static::lazy_static! { static ref ENIGO: Arc> = Arc::new(Mutex::new(Enigo::new())); static ref VIDEO: Arc>> = Default::default(); static ref TO_RELEASE: Arc>> = Arc::new(Mutex::new(HashSet::::new())); + static ref KEYBOARD: Arc> = Arc::new(Mutex::new(RdevKeyboard::new().unwrap())); } fn get_key_state(key: enigo::Key) -> bool { @@ -333,9 +334,6 @@ impl Handler { _ => return, }; - #[cfg(target_os = "windows")] - let _key = rdev::get_win_key(evt.code.into(), evt.scan_code); - me.key_down_or_up(down, _key, evt); }; if let Err(error) = rdev::listen(func) { @@ -785,7 +783,7 @@ impl Handler { fn leave(&mut self) { for key in TO_RELEASE.lock().unwrap().iter() { - self.map_keyboard_mode(false, *key) + self.map_keyboard_mode(false, *key, None) } #[cfg(windows)] crate::platform::windows::stop_system_key_propagate(false); @@ -1036,8 +1034,15 @@ impl Handler { } } - fn map_keyboard_mode(&mut self, down_or_up: bool, key: RdevKey) { + fn map_keyboard_mode(&mut self, down_or_up: bool, key: RdevKey, evt: Option) { // map mode(1): Send keycode according to the peer platform. + #[cfg(target_os = "windows")] + let key = if let Some(e) = evt { + rdev::get_win_key(e.code.into(), e.scan_code) + } else { + key + }; + let peer = self.peer_platform(); let mut key_event = KeyEvent::new(); @@ -1067,8 +1072,20 @@ impl Handler { fn translate_keyboard_mode(&mut self, down_or_up: bool, key: RdevKey, evt: Event) { // translate mode(2): locally generated characters are send to the peer. - let string = evt.name.unwrap_or_default(); + // get char + let string = match KEYBOARD.lock() { + Ok(mut keyboard) => { + let string = keyboard.add(&evt.event_type).unwrap_or_default(); + if keyboard.last_is_dead && string == "" { + return; + } + string + } + Err(_) => "".to_owned(), + }; + + // maybe two string let chars = if string == "" { None } else { @@ -1096,7 +1113,13 @@ impl Handler { } else { TO_RELEASE.lock().unwrap().remove(&key); } - self.map_keyboard_mode(down_or_up, key); + // algr without action + // Control left + if key == RdevKey::AltGr || evt.scan_code == 541 { + return; + } + dbg!(key); + self.map_keyboard_mode(down_or_up, key, None); } } @@ -1369,7 +1392,7 @@ impl Handler { } else { TO_RELEASE.lock().unwrap().remove(&key); } - self.map_keyboard_mode(down_or_up, key); + self.map_keyboard_mode(down_or_up, key, Some(evt)); } KeyboardMode::Legacy => self.legacy_keyboard_mode(down_or_up, key, evt), KeyboardMode::Translate => { From c4451b3cc7d8e77ad8ff9de95bdca713c0cf0434 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 1 Aug 2022 14:33:08 +0800 Subject: [PATCH 0132/2015] fix: merge conflict --- flutter/lib/common.dart | 2 +- flutter/lib/mobile/pages/remote_page.dart | 12 +-- flutter/lib/mobile/pages/settings_page.dart | 9 +-- flutter/lib/mobile/widgets/dialog.dart | 8 +- flutter/lib/models/server_model.dart | 7 +- libs/hbb_common/src/config.rs | 36 ++++----- src/client.rs | 51 ++++++------ src/common.rs | 4 +- src/flutter_ffi.rs | 25 +++--- src/ipc.rs | 3 +- src/rendezvous_mediator.rs | 76 ++++++++++++------ src/ui.rs | 87 +++++++++------------ src/ui_interface.rs | 33 ++++---- 13 files changed, 178 insertions(+), 175 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 74ed28d01..eda1ed4e7 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -306,7 +306,7 @@ class PermissionManager { if (!permissions.contains(type)) return Future.error("Wrong permission!$type"); - FFI.invokeMethod("request_permission", type); + gFFI.invokeMethod("request_permission", type); if (type == "ignore_battery_optimizations") { return Future.value(false); } diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 203f76e3f..23900ef07 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -516,10 +516,10 @@ class _RemotePageState extends State { }, onLongPress: () { if (touchMode) { - FFI.cursorModel + gFFI.cursorModel .move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy); } - FFI.tap(MouseButtons.right); + gFFI.tap(MouseButtons.right); }, onDoubleFinerTap: (d) { if (!touchMode) { @@ -546,13 +546,13 @@ class _RemotePageState extends State { gFFI.cursorModel.move(d.localPosition.dx, d.localPosition.dy); gFFI.sendMouse('down', MouseButtons.left); } else { - final cursorX = FFI.cursorModel.x; - final cursorY = FFI.cursorModel.y; + final cursorX = gFFI.cursorModel.x; + final cursorY = gFFI.cursorModel.y; final visible = - FFI.cursorModel.getVisibleRect().inflate(1); // extend edges + gFFI.cursorModel.getVisibleRect().inflate(1); // extend edges final size = MediaQueryData.fromWindow(ui.window).size; if (!visible.contains(Offset(cursorX, cursorY))) { - FFI.cursorModel.move(size.width / 2, size.height / 2); + gFFI.cursorModel.move(size.width / 2, size.height / 2); } } }, diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index 477659a58..53583479f 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -1,9 +1,4 @@ import 'dart:async'; - -import 'package:settings_ui/settings_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:url_launcher/url_launcher.dart'; -import 'package:provider/provider.dart'; import 'dart:convert'; import 'package:flutter/material.dart'; @@ -75,7 +70,7 @@ class _SettingsState extends State with WidgetsBindingObserver { Widget build(BuildContext context) { Provider.of(context); final username = getUsername(); - final enableAbr = FFI.getByName("option", "enable-abr") != 'N'; + final enableAbr = gFFI.getByName("option", "enable-abr") != 'N'; final enhancementsTiles = [ SettingsTile.switchTile( title: Text(translate('Adaptive Bitrate') + '(beta)'), @@ -87,7 +82,7 @@ class _SettingsState extends State with WidgetsBindingObserver { if (!v) { msg["value"] = "N"; } - FFI.setByName("option", json.encode(msg)); + gFFI.setByName("option", json.encode(msg)); setState(() {}); }, ) diff --git a/flutter/lib/mobile/widgets/dialog.dart b/flutter/lib/mobile/widgets/dialog.dart index 7d1711390..3ab0489a9 100644 --- a/flutter/lib/mobile/widgets/dialog.dart +++ b/flutter/lib/mobile/widgets/dialog.dart @@ -23,7 +23,7 @@ void showError({Duration duration = SEC1}) { } void setPermanentPasswordDialog() { - final pw = FFI.getByName("permanent_password"); + final pw = gFFI.getByName("permanent_password"); final p0 = TextEditingController(text: pw); final p1 = TextEditingController(text: pw); var validateLength = false; @@ -105,7 +105,7 @@ void setPermanentPasswordDialog() { void setTemporaryPasswordLengthDialog() { List lengths = ['6', '8', '10']; - String length = FFI.getByName('option', 'temporary-password-length'); + String length = gFFI.getByName('option', 'temporary-password-length'); var index = lengths.indexOf(length); if (index < 0) index = 0; length = lengths[index]; @@ -119,8 +119,8 @@ void setTemporaryPasswordLengthDialog() { Map msg = Map() ..["name"] = "temporary-password-length" ..["value"] = newValue; - FFI.setByName("option", jsonEncode(msg)); - FFI.setByName("temporary_password"); + gFFI.setByName("option", jsonEncode(msg)); + gFFI.setByName("temporary_password"); Future.delayed(Duration(milliseconds: 200), () { close(); showSuccess(); diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index e1703f51f..9b5909a90 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -126,8 +126,9 @@ class ServerModel with ChangeNotifier { updatePasswordModel() { var update = false; - final temporaryPassword = FFI.getByName("temporary_password"); - final verificationMethod = FFI.getByName("option", "verification-method"); + final temporaryPassword = gFFI.getByName("temporary_password"); + print("tempo passwd: ${temporaryPassword}"); + final verificationMethod = gFFI.getByName("option", "verification-method"); if (_serverPasswd.text != temporaryPassword) { _serverPasswd.text = temporaryPassword; update = true; @@ -286,7 +287,7 @@ class ServerModel with ChangeNotifier { const maxCount = 10; while (count < maxCount) { await Future.delayed(Duration(seconds: 1)); - final id = parent.target?.getByName("server_id"); + final id = parent.target?.getByName("server_id") ?? ""; if (id.isEmpty) { continue; } else { diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 7349e2873..33c46e7ae 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -1,15 +1,3 @@ -use crate::{ - log, - password_security::{ - decrypt_str_or_original, decrypt_vec_or_original, encrypt_str_or_original, - encrypt_vec_or_original, - }, -}; -use anyhow::Result; -use directories_next::ProjectDirs; -use rand::Rng; -use serde_derive::{Deserialize, Serialize}; -use sodiumoxide::crypto::sign; use std::{ collections::HashMap, fs, @@ -19,6 +7,20 @@ use std::{ time::SystemTime, }; +use anyhow::Result; +use directories_next::ProjectDirs; +use rand::Rng; +use serde_derive::{Deserialize, Serialize}; +use sodiumoxide::crypto::sign; + +use crate::{ + log, + password_security::{ + decrypt_str_or_original, decrypt_vec_or_original, encrypt_str_or_original, + encrypt_vec_or_original, + }, +}; + pub const RENDEZVOUS_TIMEOUT: u64 = 12_000; pub const CONNECT_TIMEOUT: u64 = 18_000; pub const REG_INTERVAL: i64 = 12_000; @@ -48,16 +50,10 @@ lazy_static::lazy_static! { pub static ref APP_NAME: Arc> = Arc::new(RwLock::new("RustDesk".to_owned())); static ref KEY_PAIR: Arc, Vec)>>> = Default::default(); } -#[cfg(target_os = "android")] -lazy_static::lazy_static! { - pub static ref APP_DIR: Arc> = Arc::new(RwLock::new("/data/user/0/com.carriez.flutter_hbb/app_flutter".to_owned())); -} -#[cfg(target_os = "ios")] -lazy_static::lazy_static! { - pub static ref APP_DIR: Arc> = Default::default(); -} + // #[cfg(any(target_os = "android", target_os = "ios"))] lazy_static::lazy_static! { + pub static ref APP_DIR: Arc> = Default::default(); pub static ref APP_HOME_DIR: Arc> = Default::default(); } const CHARS: &'static [char] = &[ diff --git a/src/client.rs b/src/client.rs index 5d117e709..478d81ce8 100644 --- a/src/client.rs +++ b/src/client.rs @@ -12,11 +12,6 @@ use cpal::{ Device, Host, StreamConfig, }; use magnum_opus::{Channels::*, Decoder as AudioDecoder}; -use scrap::{ - codec::{Decoder, DecoderCfg}, - VpxDecoderConfig, VpxVideoCodecId, -}; - use sha2::{Digest, Sha256}; use uuid::Uuid; @@ -38,14 +33,18 @@ use hbb_common::{ AddrMangle, ResultType, Stream, }; pub use helper::LatencyController; -use scrap::{Decoder, Image, VideoCodecId}; +pub use helper::*; +use scrap::Image; +use scrap::{ + codec::{Decoder, DecoderCfg}, + VpxDecoderConfig, VpxVideoCodecId, +}; pub use super::lang::*; pub mod file_trait; pub mod helper; -pub use helper::*; pub const SEC30: Duration = Duration::from_secs(30); /// Client of the remote desktop. @@ -784,25 +783,25 @@ impl VideoHandler { } /// Handle a VP9S frame. - pub fn handle_vp9s(&mut self, vp9s: &VP9s) -> ResultType { - let mut last_frame = Image::new(); - for vp9 in vp9s.frames.iter() { - for frame in self.decoder.decode(&vp9.data)? { - drop(last_frame); - last_frame = frame; - } - } - for frame in self.decoder.flush()? { - drop(last_frame); - last_frame = frame; - } - if last_frame.is_null() { - Ok(false) - } else { - last_frame.rgb(1, true, &mut self.rgb); - Ok(true) - } - } + // pub fn handle_vp9s(&mut self, vp9s: &VP9s) -> ResultType { + // let mut last_frame = Image::new(); + // for vp9 in vp9s.frames.iter() { + // for frame in self.decoder.decode(&vp9.data)? { + // drop(last_frame); + // last_frame = frame; + // } + // } + // for frame in self.decoder.flush()? { + // drop(last_frame); + // last_frame = frame; + // } + // if last_frame.is_null() { + // Ok(false) + // } else { + // last_frame.rgb(1, true, &mut self.rgb); + // Ok(true) + // } + // } /// Reset the decoder. pub fn reset(&mut self) { diff --git a/src/common.rs b/src/common.rs index f375ac46c..d2d1922ec 100644 --- a/src/common.rs +++ b/src/common.rs @@ -11,8 +11,8 @@ use hbb_common::{ config::{self, Config, COMPRESS_LEVEL, RENDEZVOUS_TIMEOUT}, get_version_number, log, message_proto::*, - protobuf::Message as _, protobuf::Enum, + protobuf::Message as _, rendezvous_proto::*, sleep, socket_client, tokio, ResultType, }; @@ -51,7 +51,7 @@ pub fn create_clipboard_msg(content: String) -> Message { let mut msg = Message::new(); msg.set_clipboard(Clipboard { compress, - content:content.into(), + content: content.into(), ..Default::default() }); msg diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index afbe35ec8..f1aeabfcc 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -7,7 +7,7 @@ use std::{ use flutter_rust_bridge::{StreamSink, SyncReturn, ZeroCopyBuffer}; use serde_json::{json, Number, Value}; -use hbb_common::ResultType; +use hbb_common::{ResultType, password_security}; use hbb_common::{ config::{self, Config, LocalConfig, PeerConfig, ONLINE}, fs, log, @@ -24,7 +24,8 @@ use crate::ui_interface::{ get_async_job_status, get_connect_status, get_fav, get_id, get_lan_peers, get_license, get_local_option, get_options, get_peer, get_peer_option, get_socks, get_sound_inputs, get_uuid, get_version, has_rendezvous_service, is_ok_change_id, post_request, set_local_option, - set_options, set_peer_option, set_socks, store_fav, test_if_valid_server, using_public_server, + set_options, set_peer_option, set_socks, store_fav, temporary_password, test_if_valid_server, + using_public_server, }; fn initialize(app_dir: &str) { @@ -581,8 +582,8 @@ unsafe extern "C" fn get_by_name(name: *const c_char, arg: *const c_char) -> *co "server_id" => { res = ui_interface::get_id(); } - "server_password" => { - res = Config::get_password(); + "temporary_password" => { + res = password_security::temporary_password(); } "connect_statue" => { res = ONLINE @@ -627,7 +628,7 @@ unsafe extern "C" fn get_by_name(name: *const c_char, arg: *const c_char) -> *co } } "uuid" => { - res = base64::encode(crate::get_uuid()); + res = base64::encode(get_uuid()); } _ => { log::error!("Unknown name of get_by_name: {}", name); @@ -942,13 +943,13 @@ unsafe extern "C" fn set_by_name(name: *const c_char, value: *const c_char) { // } // } // Server Side - "update_password" => { - if value.is_empty() { - Config::set_password(&Config::get_auto_password()); - } else { - Config::set_password(value); - } - } + // "update_password" => { + // if value.is_empty() { + // Config::set_password(&Config::get_auto_password()); + // } else { + // Config::set_password(value); + // } + // } #[cfg(target_os = "android")] "chat_server_mode" => { if let Ok(m) = serde_json::from_str::>(value) { diff --git a/src/ipc.rs b/src/ipc.rs index c95a045fe..99670890e 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -1,9 +1,8 @@ -use crate::rendezvous_mediator::RendezvousMediator; -use bytes::Bytes; use std::{collections::HashMap, sync::atomic::Ordering}; #[cfg(not(windows))] use std::{fs::File, io::prelude::*}; +use bytes::Bytes; use parity_tokio_ipc::{ Connection as Conn, ConnectionClient as ConnClient, Endpoint, Incoming, SecurityAttributes, }; diff --git a/src/rendezvous_mediator.rs b/src/rendezvous_mediator.rs index 9f3674d36..6e38bff21 100644 --- a/src/rendezvous_mediator.rs +++ b/src/rendezvous_mediator.rs @@ -1,21 +1,4 @@ -use crate::server::{check_zombie, new as new_server, ServerPtr}; -use hbb_common::{ - allow_err, - anyhow::bail, - config::{Config, REG_INTERVAL, RENDEZVOUS_PORT, RENDEZVOUS_TIMEOUT}, - futures::future::join_all, - log, - protobuf::Message as _, - rendezvous_proto::*, - sleep, socket_client, - tcp::FramedStream, - tokio::{ - self, select, - time::{interval, Duration}, - }, - udp::FramedSocket, - AddrMangle, IntoTargetAddr, ResultType, TargetAddr, -}; +use std::collections::HashMap; use std::{ net::SocketAddr, sync::{ @@ -24,8 +7,31 @@ use std::{ }, time::Instant, }; + use uuid::Uuid; +use hbb_common::config::DiscoveryPeer; +use hbb_common::tcp::FramedStream; +use hbb_common::{ + allow_err, + anyhow::bail, + config, + config::{Config, REG_INTERVAL, RENDEZVOUS_PORT, RENDEZVOUS_TIMEOUT}, + futures::future::join_all, + log, + protobuf::Message as _, + rendezvous_proto::*, + sleep, socket_client, + tokio::{ + self, select, + time::{interval, Duration}, + }, + udp::FramedSocket, + AddrMangle, IntoTargetAddr, ResultType, TargetAddr, +}; + +use crate::server::{check_zombie, new as new_server, ServerPtr}; + type Message = RendezvousMessage; lazy_static::lazy_static! { @@ -354,7 +360,14 @@ impl RendezvousMediator { { let uuid = Uuid::new_v4().to_string(); return self - .create_relay(ph.socket_addr.into(), relay_server, uuid, server, true, true) + .create_relay( + ph.socket_addr.into(), + relay_server, + uuid, + server, + true, + true, + ) .await; } let peer_addr = AddrMangle::decode(&ph.socket_addr); @@ -568,7 +581,7 @@ fn lan_discovery() -> ResultType<()> { if let Ok((len, addr)) = socket.recv_from(&mut buf) { if let Ok(msg_in) = Message::parse_from_bytes(&buf[0..len]) { match msg_in.union { - Some(rendezvous_message::Union::peer_discovery(p)) => { + Some(rendezvous_message::Union::PeerDiscovery(p)) => { if p.cmd == "ping" { let mut msg_out = Message::new(); let peer = PeerDiscovery { @@ -616,11 +629,22 @@ pub fn discover() -> ResultType<()> { if let Ok((len, _)) = socket.recv_from(&mut buf) { if let Ok(msg_in) = Message::parse_from_bytes(&buf[0..len]) { match msg_in.union { - Some(rendezvous_message::Union::peer_discovery(p)) => { + Some(rendezvous_message::Union::PeerDiscovery(p)) => { last_recv_time = Instant::now(); if p.cmd == "pong" { if p.mac != mac { - peers.push((p.id, p.username, p.hostname, p.platform)); + let dp = DiscoveryPeer { + id: "".to_string(), + ip_mac: HashMap::from([ + // TODO: addr ip + (addr.ip().to_string(), p.mac.clone()), + ]), + username: p.username, + hostname: p.hostname, + platform: p.platform, + online: true, + }; + peers.push(dp); } } } @@ -629,7 +653,7 @@ pub fn discover() -> ResultType<()> { } } if last_write_time.elapsed().as_millis() > 300 && last_write_n != peers.len() { - config::LanPeers::store(serde_json::to_string(&peers)?); + config::LanPeers::store(&peers); last_write_time = Instant::now(); last_write_n = peers.len(); } @@ -638,7 +662,7 @@ pub fn discover() -> ResultType<()> { } } log::info!("discover ping done"); - config::LanPeers::store(serde_json::to_string(&peers)?); + config::LanPeers::store(&peers); Ok(()) } @@ -678,7 +702,7 @@ pub async fn query_online_states, Vec)>(ids: Vec ResultType { - let rendezvous_server = crate::get_rendezvous_server(1_000).await; + let (mut rendezvous_server, servers, contained) = crate::get_rendezvous_server(1_000).await; let tmp: Vec<&str> = rendezvous_server.split(":").collect(); if tmp.len() != 2 { bail!("Invalid server address: {}", rendezvous_server); @@ -722,7 +746,7 @@ async fn query_online_states_( Some(Ok(bytes)) => { if let Ok(msg_in) = RendezvousMessage::parse_from_bytes(&bytes) { match msg_in.union { - Some(rendezvous_message::Union::online_response(online_response)) => { + Some(rendezvous_message::Union::OnlineResponse(online_response)) => { let states = online_response.states; let mut onlines = Vec::new(); let mut offlines = Vec::new(); diff --git a/src/ui.rs b/src/ui.rs index 52e605bc8..c2bc8cbc3 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,25 +1,12 @@ use std::{ collections::HashMap, iter::FromIterator, + process::Child, sync::{Arc, Mutex}, }; use sciter::Value; -use hbb_common::{allow_err, config::PeerConfig, log}; - -use crate::ui_interface::*; - -mod cm; -#[cfg(feature = "inline")] -mod inline; -#[cfg(target_os = "macos")] -mod macos; -pub mod remote; -#[cfg(target_os = "windows")] -pub mod win_privacy; -use crate::common::SOFTWARE_UPDATE_URL; -use crate::ipc; use hbb_common::{ allow_err, config::{self, Config, LocalConfig, PeerConfig, RENDEZVOUS_PORT, RENDEZVOUS_TIMEOUT}, @@ -31,13 +18,33 @@ use hbb_common::{ tcp::FramedStream, tokio::{self, sync::mpsc, time}, }; -use sciter::Value; -use std::{ - collections::HashMap, - iter::FromIterator, - process::Child, - sync::{Arc, Mutex}, + +use crate::common::{get_app_name, SOFTWARE_UPDATE_URL}; +use crate::ui_interface::{ + check_mouse_time, closing, create_shortcut, current_is_wayland, fix_login_wayland, + forget_password, get_api_server, get_async_job_status, get_connect_status, get_error, get_fav, + get_icon, get_lan_peers, get_license, get_local_option, get_mouse_time, get_new_version, + get_option, get_options, get_peer, get_peer_option, get_recent_sessions, get_remote_id, + get_size, get_socks, get_software_ext, get_software_store_path, get_software_update_url, + get_uuid, get_version, goto_install, has_rendezvous_service, install_me, install_path, + is_can_screen_recording, is_installed, is_installed_daemon, is_installed_lower_version, + is_login_wayland, is_ok_change_id, is_process_trusted, is_rdp_service_open, is_share_rdp, + is_xfce, modify_default_login, new_remote, open_url, peer_has_password, permanent_password, + post_request, recent_sessions_updated, remove_peer, run_without_install, set_local_option, + set_option, set_options, set_peer_option, set_remote_id, set_share_rdp, set_socks, + show_run_without_install, store_fav, t, temporary_password, test_if_valid_server, update_me, + update_temporary_password, using_public_server, }; +use crate::{discover, ipc}; + +mod cm; +#[cfg(feature = "inline")] +mod inline; +#[cfg(target_os = "macos")] +mod macos; +pub mod remote; +#[cfg(target_os = "windows")] +pub mod win_privacy; type Message = RendezvousMessage; @@ -79,7 +86,7 @@ pub fn start(args: &mut [String]) { } #[cfg(windows)] if args.len() > 0 && args[0] == "--tray" { - let options = OPTIONS.clone(); + let options = check_connect_status(false).1; crate::tray::start_tray(options); return; } @@ -105,8 +112,8 @@ pub fn start(args: &mut [String]) { args[1] = id; } if args.is_empty() { - let cloned = CHILDS.clone(); - std::thread::spawn(move || check_zombie(cloned)); + let child: Childs = Default::default(); + std::thread::spawn(move || check_zombie(child)); crate::common::check_software_update(); frame.event_handler(UI {}); frame.sciter_handler(UIHostHandler {}); @@ -177,45 +184,24 @@ pub fn start(args: &mut [String]) { struct UI {} impl UI { - fn new(childs: Childs) -> Self { - let res = check_connect_status(true); - Self(childs, res.0, res.1, Default::default(), res.2, res.3) - } - - fn recent_sessions_updated(&mut self) -> bool { - let mut lock = self.0.lock().unwrap(); - if lock.0 { - lock.0 = false; - true - } else { - false - } fn recent_sessions_updated(&self) -> bool { recent_sessions_updated() } fn get_id(&self) -> String { - get_id() - } - - fn get_password(&mut self) -> String { - get_password() + ipc::get_id() } fn temporary_password(&mut self) -> String { - self.5.lock().unwrap().clone() + temporary_password() } fn update_temporary_password(&self) { - allow_err!(ipc::update_temporary_password()); - } - - fn update_password(&mut self, password: String) { - update_password(password) + update_temporary_password() } fn permanent_password(&self) -> String { - ipc::get_permanent_password() + permanent_password() } fn set_permanent_password(&self, password: String) { @@ -507,7 +493,7 @@ impl UI { } fn discover(&self) { - discover() + discover(); } fn get_lan_peers(&self) -> String { @@ -523,7 +509,8 @@ impl UI { } fn change_id(&self, id: String) { - change_id(id) + let old_id = self.get_id(); + change_id(id, old_id); } fn post_request(&self, url: String, body: String, header: String) { diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 86b4e9e9a..a3643d5c9 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -14,7 +14,7 @@ use hbb_common::{ rendezvous_proto::*, sleep, tcp::FramedStream, - tokio::{self, sync::mpsc, time}, + tokio::{self, sync::mpsc, time}, password_security, }; use crate::common::SOFTWARE_UPDATE_URL; @@ -31,6 +31,7 @@ lazy_static::lazy_static! { pub static ref OPTIONS : Arc>> = Arc::new(Mutex::new(Config::get_options())); pub static ref ASYNC_JOB_STATUS : Arc> = Default::default(); pub static ref SENDER : Mutex> = Mutex::new(check_connect_status(true)); + pub static ref TEMPORARY_PASSWD : Arc> = Arc::new(Mutex::new("".to_owned())); } pub fn recent_sessions_updated() -> bool { @@ -47,18 +48,6 @@ pub fn get_id() -> String { ipc::get_id() } -pub fn get_password() -> String { - ipc::get_password() -} - -pub fn update_password(password: String) { - if password.is_empty() { - allow_err!(ipc::set_password(Config::get_auto_password())); - } else { - allow_err!(ipc::set_password(password)); - } -} - pub fn get_remote_id() -> String { LocalConfig::get_remote_id() } @@ -369,6 +358,18 @@ pub fn get_connect_status() -> Status { res } +pub fn update_temporary_password() { + allow_err!(ipc::update_temporary_password()); +} + +pub fn permanent_password() -> String { + ipc::get_permanent_password() +} + +pub fn temporary_password() -> String { + password_security::temporary_password() +} + pub fn get_peer(id: String) -> PeerConfig { PeerConfig::load(&id) } @@ -542,11 +543,11 @@ pub fn discover() { } pub fn get_lan_peers() -> String { - config::LanPeers::load().peers + serde_json::to_string(&config::LanPeers::load().peers).unwrap_or_default() } pub fn get_uuid() -> String { - base64::encode(crate::get_uuid()) + base64::encode(hbb_common::get_uuid()) } #[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] @@ -762,7 +763,7 @@ async fn check_id( if let Some(Ok(bytes)) = socket.next_timeout(3_000).await { if let Ok(msg_in) = RendezvousMessage::parse_from_bytes(&bytes) { match msg_in.union { - Some(rendezvous_message::Union::register_pk_response(rpr)) => { + Some(rendezvous_message::Union::RegisterPkResponse(rpr)) => { match rpr.result.enum_value_or_default() { register_pk_response::Result::OK => { ok = true; From 74b830159bfdde8753bd8d835dc3af9e9e731a3c Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 1 Aug 2022 14:56:13 +0800 Subject: [PATCH 0133/2015] add: ci dependencies --- .github/workflows/ci.yml | 2 +- flutter/lib/models/server_model.dart | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5d21dee60..39fca8c5d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -78,7 +78,7 @@ jobs: shell: bash run: | case ${{ matrix.job.target }} in - x86_64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libappindicator3-dev;; + x86_64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev;; # arm-unknown-linux-*) sudo apt-get -y update ; sudo apt-get -y install gcc-arm-linux-gnueabihf ;; # aarch64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt-get -y install gcc-aarch64-linux-gnu ;; esac diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index 9b5909a90..8ea9e1c93 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -127,7 +127,6 @@ class ServerModel with ChangeNotifier { updatePasswordModel() { var update = false; final temporaryPassword = gFFI.getByName("temporary_password"); - print("tempo passwd: ${temporaryPassword}"); final verificationMethod = gFFI.getByName("option", "verification-method"); if (_serverPasswd.text != temporaryPassword) { _serverPasswd.text = temporaryPassword; From 53f496c0e4eb8d8b0a3e101faf438501aa20a501 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sat, 30 Jul 2022 07:09:31 +0800 Subject: [PATCH 0134/2015] avoid changing id manually Signed-off-by: 21pages --- libs/hbb_common/src/config.rs | 32 +++++++++++++++++++++++- libs/hbb_common/src/lib.rs | 18 +++++++++++++ libs/hbb_common/src/password_security.rs | 2 +- src/main.rs | 6 +++-- 4 files changed, 54 insertions(+), 4 deletions(-) diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index f47be3da3..0782a0fde 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -298,8 +298,37 @@ impl Config { fn load() -> Config { let mut config = Config::load_::(""); - let (password, _, store) = decrypt_str_or_original(&config.password, PASSWORD_ENC_VERSION); + let mut store = false; + let (password, _, store1) = decrypt_str_or_original(&config.password, PASSWORD_ENC_VERSION); config.password = password; + store |= store1; + let mut id_valid = false; + let (id, encrypted, store2) = decrypt_str_or_original(&config.id, PASSWORD_ENC_VERSION); + if encrypted { + config.id = id; + id_valid = true; + store |= store2; + } else { + if crate::get_modified_time(&Self::file_("")) + .checked_sub(std::time::Duration::from_secs(30)) // allow modification during installation + .unwrap_or(crate::get_exe_time()) + < crate::get_exe_time() + { + id_valid = true; + store = true; + } + } + if !id_valid { + for _ in 0..3 { + if let Some(id) = Config::get_auto_id() { + config.id = id; + store = true; + break; + } else { + log::error!("Failed to generate new id"); + } + } + } if store { config.store(); } @@ -309,6 +338,7 @@ impl Config { fn store(&self) { let mut config = self.clone(); config.password = encrypt_str_or_original(&config.password, PASSWORD_ENC_VERSION); + config.id = encrypt_str_or_original(&config.id, PASSWORD_ENC_VERSION); Config::store_(&config, ""); } diff --git a/libs/hbb_common/src/lib.rs b/libs/hbb_common/src/lib.rs index 2fdd74cd5..48fbfe23c 100644 --- a/libs/hbb_common/src/lib.rs +++ b/libs/hbb_common/src/lib.rs @@ -202,6 +202,24 @@ pub fn get_modified_time(path: &std::path::Path) -> SystemTime { .unwrap_or(UNIX_EPOCH) } +pub fn get_created_time(path: &std::path::Path) -> SystemTime { + std::fs::metadata(&path) + .map(|m| m.created().unwrap_or(UNIX_EPOCH)) + .unwrap_or(UNIX_EPOCH) +} + +pub fn get_exe_time() -> SystemTime { + std::env::current_exe().map_or(UNIX_EPOCH, |path| { + let m = get_modified_time(&path); + let c = get_created_time(&path); + if m > c { + m + } else { + c + } + }) +} + pub fn get_uuid() -> Vec { #[cfg(not(any(target_os = "android", target_os = "ios")))] if let Ok(id) = machine_uid::get() { diff --git a/libs/hbb_common/src/password_security.rs b/libs/hbb_common/src/password_security.rs index ba57c11c4..55a6825fa 100644 --- a/libs/hbb_common/src/password_security.rs +++ b/libs/hbb_common/src/password_security.rs @@ -108,7 +108,7 @@ pub fn encrypt_vec_or_original(v: &[u8], version: &str) -> Vec { v.to_owned() } -// String: password +// Vec: password // bool: whether decryption is successful // bool: whether should store to re-encrypt when load pub fn decrypt_vec_or_original(v: &[u8], current_version: &str) -> (Vec, bool, bool) { diff --git a/src/main.rs b/src/main.rs index 7dcd962bf..aecc2ec4a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -166,7 +166,7 @@ fn main() { } fn import_config(path: &str) { - use hbb_common::{config::*, get_modified_time}; + use hbb_common::{config::*, get_exe_time, get_modified_time}; let path2 = path.replace(".toml", "2.toml"); let path2 = std::path::Path::new(&path2); let path = std::path::Path::new(path); @@ -176,7 +176,9 @@ fn import_config(path: &str) { log::info!("Empty source config, skipped"); return; } - if get_modified_time(&path) > get_modified_time(&Config::file()) { + if get_modified_time(&path) > get_modified_time(&Config::file()) + && get_modified_time(&path) < get_exe_time() + { if store_path(Config::file(), config).is_err() { log::info!("config written"); } From 74a2929bc9241599266fa6bcaad1c0d523c324ab Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 2 Aug 2022 13:10:09 +0800 Subject: [PATCH 0135/2015] flutter_desktop_connection_2: debug lan Signed-off-by: fufesou --- Cargo.lock | 624 +++++++++--------- .../lib/desktop/pages/connection_page.dart | 314 +++++---- flutter/lib/desktop/pages/remote_page.dart | 5 + flutter/lib/desktop/widgets/peer_widget.dart | 74 +-- flutter/lib/mobile/pages/remote_page.dart | 121 ++-- flutter/lib/models/model.dart | 3 +- flutter/lib/models/peer_model.dart | 52 +- src/flutter_ffi.rs | 63 +- src/lan.rs | 3 + src/rendezvous_mediator.rs | 62 -- src/ui.rs | 6 +- src/ui_interface.rs | 22 +- 12 files changed, 740 insertions(+), 609 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1994bfd32..a037b4e33 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,12 +2,6 @@ # It is not intended for manual editing. version = 3 -[[package]] -name = "ab_glyph_rasterizer" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a13739d7177fbd22bb0ed28badfff9f372f8bef46c863db4e1c6248f6b223b6e" - [[package]] name = "addr2line" version = "0.17.0" @@ -69,19 +63,6 @@ dependencies = [ "pkg-config", ] -[[package]] -name = "andrew" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c4afb09dd642feec8408e33f92f3ffc4052946f6b20f32fb99c1f58cd4fa7cf" -dependencies = [ - "bitflags", - "rusttype", - "walkdir", - "xdg", - "xml-rs", -] - [[package]] name = "android_log-sys" version = "0.2.0" @@ -325,6 +306,18 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "block" version = "0.1.6" @@ -374,9 +367,12 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" -version = "1.1.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" +checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db" +dependencies = [ + "serde 1.0.137", +] [[package]] name = "cache-padded" @@ -410,12 +406,12 @@ dependencies = [ [[package]] name = "calloop" -version = "0.6.5" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b036167e76041694579972c28cf4877b4f92da222560ddb49008937b6a6727c" +checksum = "bf2eec61efe56aa1e813f5126959296933cf0700030e4314786c48779a66ab82" dependencies = [ "log", - "nix 0.18.0", + "nix 0.22.3", ] [[package]] @@ -533,7 +529,7 @@ checksum = "5a050e2153c5be08febd6734e29298e844fdb0fa21aeddd63b4eb7baa106c69b" dependencies = [ "glob", "libc", - "libloading 0.7.3", + "libloading", ] [[package]] @@ -559,13 +555,28 @@ checksum = "d2dbdf4bdacb33466e854ce889eee8dfd5729abf7ccd7664d0a2d60cd384440b" dependencies = [ "atty", "bitflags", + "clap_derive", "clap_lex", "indexmap", + "lazy_static", "strsim 0.10.0", "termcolor", "textwrap 0.15.0", ] +[[package]] +name = "clap_derive" +version = "3.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25320346e922cffe59c0bbc5410c8d8784509efb321488971081313cb1e1a33c" +dependencies = [ + "heck 0.4.0", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "clap_lex" version = "0.2.0" @@ -965,38 +976,14 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35" -[[package]] -name = "darling" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858" -dependencies = [ - "darling_core 0.10.2", - "darling_macro 0.10.2", -] - [[package]] name = "darling" version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" dependencies = [ - "darling_core 0.13.4", - "darling_macro 0.13.4", -] - -[[package]] -name = "darling_core" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim 0.9.3", - "syn", + "darling_core", + "darling_macro", ] [[package]] @@ -1013,24 +1000,13 @@ dependencies = [ "syn", ] -[[package]] -name = "darling_macro" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72" -dependencies = [ - "darling_core 0.10.2", - "quote", - "syn", -] - [[package]] name = "darling_macro" version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" dependencies = [ - "darling_core 0.13.4", + "darling_core", "quote", "syn", ] @@ -1165,6 +1141,18 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "default-net" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05e70d471b0ba4e722c85651b3bb04b6880dfdb1224a43ade80c1295314db646" +dependencies = [ + "libc", + "memalloc", + "system-configuration", + "windows", +] + [[package]] name = "deflate" version = "0.8.6" @@ -1195,15 +1183,6 @@ dependencies = [ "dirs-sys-next", ] -[[package]] -name = "dirs" -version = "4.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" -dependencies = [ - "dirs-sys", -] - [[package]] name = "dirs-next" version = "2.0.0" @@ -1214,17 +1193,6 @@ dependencies = [ "dirs-sys-next", ] -[[package]] -name = "dirs-sys" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" -dependencies = [ - "libc", - "redox_users", - "winapi 0.3.9", -] - [[package]] name = "dirs-sys-next" version = "0.1.2" @@ -1242,22 +1210,13 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" -[[package]] -name = "dlib" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b11f15d1e3268f140f68d390637d5e76d849782d971ae7063e0da69fe9709a76" -dependencies = [ - "libloading 0.6.7", -] - [[package]] name = "dlib" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac1b7517328c04c2aa68422fc60a41b92208182142ed04a25879c26c8f878794" dependencies = [ - "libloading 0.7.3", + "libloading", ] [[package]] @@ -1313,6 +1272,7 @@ name = "enigo" version = "0.0.14" dependencies = [ "core-graphics 0.22.3", + "hbb_common", "libc", "log", "objc", @@ -1382,6 +1342,16 @@ dependencies = [ "str-buf", ] +[[package]] +name = "evdev" +version = "0.11.5" +source = "git+https://github.com/fufesou/evdev#cec616e37790293d2cd2aa54a96601ed6b1b35a9" +dependencies = [ + "bitvec", + "libc", + "nix 0.23.1", +] + [[package]] name = "event-listener" version = "2.5.2" @@ -1553,6 +1523,12 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures" version = "0.3.21" @@ -2139,19 +2115,21 @@ dependencies = [ "lazy_static", "log", "mac_address", + "machine-uid", "protobuf", - "protobuf-codegen-pure", + "protobuf-codegen", "quinn", "rand 0.8.5", "regex", "serde 1.0.137", "serde_derive", "serde_json 1.0.81", + "serde_with", "socket2 0.3.19", "sodiumoxide", "tokio", "tokio-socks", - "tokio-util 0.6.10", + "tokio-util 0.7.2", "toml", "winapi 0.3.9", "zstd", @@ -2227,6 +2205,19 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +[[package]] +name = "hwcodec" +version = "0.1.0" +source = "git+https://github.com/21pages/hwcodec#890204e0703a3d361fc7a45f035fe75c0575bb1d" +dependencies = [ + "bindgen", + "cc", + "log", + "serde 1.0.137", + "serde_derive", + "serde_json 1.0.81", +] + [[package]] name = "hyper" version = "0.14.19" @@ -2333,6 +2324,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" dependencies = [ "cfg-if 1.0.0", + "js-sys", + "wasm-bindgen", + "web-sys", ] [[package]] @@ -2457,7 +2451,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d83c2227727d7950ada2ae554613d35fd4e55b87f0a29b86d2368267d19b1d99" dependencies = [ "gtk-sys", - "libloading 0.7.3", + "libloading", "once_cell", ] @@ -2476,16 +2470,6 @@ dependencies = [ "pkg-config", ] -[[package]] -name = "libloading" -version = "0.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "351a32417a12d5f7e82c368a66781e307834dae04c6ce0cd4456d52989229883" -dependencies = [ - "cfg-if 1.0.0", - "winapi 0.3.9", -] - [[package]] name = "libloading" version = "0.7.3" @@ -2642,6 +2626,12 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" +[[package]] +name = "memalloc" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df39d232f5c40b0891c10216992c2f250c054105cb1e56f0fc9032db6203ecc1" + [[package]] name = "memchr" version = "2.5.0" @@ -2650,9 +2640,9 @@ checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" [[package]] name = "memmap2" -version = "0.1.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b70ca2a6103ac8b665dc150b142ef0e4e89df640c9e6cf295d189c3caebe5a" +checksum = "00b6c2ebff6180198788f5db08d7ce3bc1d0b617176678831a7510825973e357" dependencies = [ "libc", ] @@ -2725,19 +2715,6 @@ dependencies = [ "winapi 0.2.8", ] -[[package]] -name = "mio" -version = "0.7.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc" -dependencies = [ - "libc", - "log", - "miow 0.3.7", - "ntapi", - "winapi 0.3.9", -] - [[package]] name = "mio" version = "0.8.3" @@ -2750,18 +2727,6 @@ dependencies = [ "windows-sys 0.36.1", ] -[[package]] -name = "mio-misc" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b47412f3a52115b936ff2a229b803498c7b4d332adeb87c2f1498c9da54c398c" -dependencies = [ - "crossbeam", - "crossbeam-queue", - "log", - "mio 0.7.14", -] - [[package]] name = "mio-named-pipes" version = "0.1.7" @@ -2804,6 +2769,15 @@ dependencies = [ "windows-sys 0.28.0", ] +[[package]] +name = "mouce" +version = "0.2.1" +source = "git+https://github.com/fufesou/mouce.git#26da8d4b0009b7f96996799c2a5c0990a8dbf08b" +dependencies = [ + "glob", + "libc", +] + [[package]] name = "muldiv" version = "0.2.1" @@ -2812,10 +2786,11 @@ checksum = "0419348c027fa7be448d2ae7ea0e4e04c2334c31dc4e74ab29f00a2a7ca69204" [[package]] name = "ndk" -version = "0.3.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8794322172319b972f528bf90c6b467be0079f1fa82780ffb431088e741a73ab" +checksum = "96d868f654c72e75f8687572699cdabe755f03effbb62542768e995d5b8d699d" dependencies = [ + "bitflags", "jni-sys", "ndk-sys 0.2.2", "num_enum", @@ -2843,15 +2818,16 @@ checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" [[package]] name = "ndk-glue" -version = "0.3.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5caf0c24d51ac1c905c27d4eda4fa0635bbe0de596b8f79235e0b17a4d29385" +checksum = "c71bee8ea72d685477e28bd004cfe1bf99c754d688cd78cad139eae4089484d4" dependencies = [ "lazy_static", "libc", "log", - "ndk 0.3.0", - "ndk-macro 0.2.0", + "ndk 0.5.0", + "ndk-context", + "ndk-macro", "ndk-sys 0.2.2", ] @@ -2866,30 +2842,17 @@ dependencies = [ "log", "ndk 0.6.0", "ndk-context", - "ndk-macro 0.3.0", + "ndk-macro", "ndk-sys 0.3.0", ] -[[package]] -name = "ndk-macro" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05d1c6307dc424d0f65b9b06e94f88248e6305726b14729fd67a5e47b2dc481d" -dependencies = [ - "darling 0.10.2", - "proc-macro-crate 0.1.5", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "ndk-macro" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0df7ac00c4672f9d5aece54ee3347520b7e20f158656c7db2e6de01902eb7a6c" dependencies = [ - "darling 0.13.4", + "darling", "proc-macro-crate 1.1.3", "proc-macro2", "quote", @@ -2922,30 +2885,6 @@ dependencies = [ "winapi 0.3.9", ] -[[package]] -name = "nix" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83450fe6a6142ddd95fb064b746083fc4ef1705fe81f64a64e1d4b39f54a1055" -dependencies = [ - "bitflags", - "cc", - "cfg-if 0.1.10", - "libc", -] - -[[package]] -name = "nix" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa9b4819da1bc61c0ea48b63b7bc8604064dd43013e7cc325df098d49cd7c18a" -dependencies = [ - "bitflags", - "cc", - "cfg-if 1.0.0", - "libc", -] - [[package]] name = "nix" version = "0.22.3" @@ -3191,15 +3130,6 @@ version = "6.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21326818e99cfe6ce1e524c2a805c189a99b5ae555a35d19f9a284b427d86afa" -[[package]] -name = "owned_ttf_parser" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f923fb806c46266c02ab4a5b239735c144bdeda724a50ed058e5226f594cde3" -dependencies = [ - "ttf-parser", -] - [[package]] name = "padlock" version = "0.2.0" @@ -3509,60 +3439,56 @@ dependencies = [ [[package]] name = "protobuf" -version = "3.0.0-alpha.2" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d5ef59c35c7472ce5e1b6c5924b87585143d1fc2cf39eae0009bba6c4df62f1" +checksum = "4ee4a7d8b91800c8f167a6268d1a1026607368e1adc84e98fe044aeb905302f7" +dependencies = [ + "bytes", + "once_cell", + "protobuf-support", + "thiserror", +] [[package]] name = "protobuf-codegen" -version = "3.0.0-alpha.2" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89100ee819f69b77a4cab389fec9dd155a305af4c615e6413ec1ef9341f333ef" +checksum = "07b893e5e7d3395545d5244f8c0d33674025bd566b26c03bfda49b82c6dec45e" dependencies = [ "anyhow", + "once_cell", "protobuf", "protobuf-parse", - "thiserror", -] - -[[package]] -name = "protobuf-codegen-pure" -version = "3.0.0-alpha.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79453e74d08190551e821533ee42c447f9e21ca26f83520e120e6e8af27f6879" -dependencies = [ - "anyhow", - "protobuf", - "protobuf-codegen", - "protobuf-parse", - "thiserror", -] - -[[package]] -name = "protobuf-parse" -version = "3.0.0-alpha.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c265ffc69976efc3056955b881641add3186ad0be893ef10622482d80d1d2b68" -dependencies = [ - "anyhow", - "protobuf", - "protoc", + "regex", "tempfile", "thiserror", ] [[package]] -name = "protoc" -version = "3.0.0-alpha.2" +name = "protobuf-parse" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f1f8b318a54d18fbe542513331e058f4f8ce6502e542e057c50c7e5e803fdab" +checksum = "9b1447dd751c434cc1b415579837ebd0411ed7d67d465f38010da5d7cd33af4d" dependencies = [ "anyhow", + "indexmap", "log", + "protobuf", + "protobuf-support", + "tempfile", "thiserror", "which 4.2.5", ] +[[package]] +name = "protobuf-support" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca157fe12fc7ee2e315f2f735e27df41b3d97cdd70ea112824dac1ffb08ee1c" +dependencies = [ + "thiserror", +] + [[package]] name = "quest" version = "0.3.0" @@ -3638,6 +3564,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.6.5" @@ -3774,16 +3706,6 @@ dependencies = [ "rand_core 0.3.1", ] -[[package]] -name = "raw-window-handle" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e28f55143d0548dad60bb4fbdc835a3d7ac6acc3324506450c5fdd6e42903a76" -dependencies = [ - "libc", - "raw-window-handle 0.4.3", -] - [[package]] name = "raw-window-handle" version = "0.4.3" @@ -3972,13 +3894,11 @@ dependencies = [ [[package]] name = "rpassword" -version = "6.0.1" +version = "7.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bf099a1888612545b683d2661a1940089f6c2e5a8e38979b2159da876bfd956" +checksum = "26b763cb66df1c928432cc35053f8bd4cec3335d8559fc16010017d16b3c1680" dependencies = [ "libc", - "serde 1.0.137", - "serde_json 1.0.81", "winapi 0.3.9", ] @@ -4042,6 +3962,7 @@ dependencies = [ "async-process", "async-trait", "base64", + "bytes", "cc", "cfg-if 1.0.0", "clap 3.1.18", @@ -4052,8 +3973,10 @@ dependencies = [ "cpal", "ctrlc", "dasp", + "default-net", "dispatch", "enigo", + "evdev", "flexi_logger", "flutter_rust_bridge", "flutter_rust_bridge_codegen", @@ -4068,13 +3991,14 @@ dependencies = [ "mac_address", "machine-uid", "magnum-opus", + "mouce", "num_cpus", "objc", "parity-tokio-ipc", "rdev", "repng", "reqwest", - "rpassword 6.0.1", + "rpassword 7.0.0", "rubato", "runas", "rust-pulsectl", @@ -4088,6 +4012,7 @@ dependencies = [ "simple_rc", "sys-locale", "sysinfo", + "system_shutdown", "tray-item", "trayicon", "uuid", @@ -4098,6 +4023,7 @@ dependencies = [ "winit", "winreg 0.10.1", "winres", + "wol-rs", ] [[package]] @@ -4165,16 +4091,6 @@ dependencies = [ "base64", ] -[[package]] -name = "rusttype" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc7c727aded0be18c5b80c1640eae0ac8e396abf6fa8477d96cb37d18ee5ec59" -dependencies = [ - "ab_glyph_rasterizer", - "owned_ttf_parser", -] - [[package]] name = "rustversion" version = "1.0.6" @@ -4251,6 +4167,8 @@ dependencies = [ "gstreamer", "gstreamer-app", "gstreamer-video", + "hbb_common", + "hwcodec", "jni", "lazy_static", "libc", @@ -4387,6 +4305,28 @@ dependencies = [ "serde 1.0.137", ] +[[package]] +name = "serde_with" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678b5a069e50bf00ecd22d0cd8ddf7c236f68581b03db652061ed5eb13a312ff" +dependencies = [ + "serde 1.0.137", + "serde_with_macros", +] + +[[package]] +name = "serde_with_macros" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "serde_yaml" version = "0.8.24" @@ -4472,18 +4412,18 @@ checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" [[package]] name = "smithay-client-toolkit" -version = "0.12.3" +version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4750c76fd5d3ac95fa3ed80fe667d6a3d8590a960e5b575b98eea93339a80b80" +checksum = "8a28f16a97fa0e8ce563b2774d1e732dd5d4025d2772c5dba0a41a0f90a29da3" dependencies = [ - "andrew", "bitflags", "calloop", - "dlib 0.4.2", + "dlib", "lazy_static", "log", "memmap2", - "nix 0.18.0", + "nix 0.22.3", + "pkg-config", "wayland-client", "wayland-cursor", "wayland-protocols", @@ -4552,12 +4492,6 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" -[[package]] -name = "strsim" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" - [[package]] name = "strsim" version = "0.10.0" @@ -4644,9 +4578,9 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.23.13" +version = "0.24.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3977ec2e0520829be45c8a2df70db2bf364714d8a748316a10c3c35d4d2b01c9" +checksum = "54cb4ebf3d49308b99e6e9dc95e989e2fdbdc210e4f67c39db0bb89ba927001c" dependencies = [ "cfg-if 1.0.0", "core-foundation-sys 0.8.3", @@ -4657,6 +4591,27 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "system-configuration" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75182f12f490e953596550b65ee31bda7c8e043d9386174b353bda50838c3fd" +dependencies = [ + "bitflags", + "core-foundation 0.9.3", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys 0.8.3", + "libc", +] + [[package]] name = "system-deps" version = "1.3.2" @@ -4685,6 +4640,21 @@ dependencies = [ "version-compare 0.1.0", ] +[[package]] +name = "system_shutdown" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "035e081d603551d8d78db27d2232913269c749ea67648c369100049820406a14" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "target_build_utils" version = "0.3.1" @@ -4818,10 +4788,11 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.18.2" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4903bf0427cf68dddd5aa6a93220756f8be0c34fcfa9f5e6191e103e15a31395" +checksum = "7a8325f63a7d4774dd041e363b2409ed1c5cbbd0f867795e661df066b2b0a581" dependencies = [ + "autocfg 1.1.0", "bytes", "libc", "memchr", @@ -4882,11 +4853,9 @@ checksum = "36943ee01a6d67977dd3f84a5a1d2efeb4ada3a1ae771cadfaa535d9d9fc6507" dependencies = [ "bytes", "futures-core", - "futures-io", "futures-sink", "log", "pin-project-lite", - "slab", "tokio", ] @@ -4898,8 +4867,11 @@ checksum = "f988a1a1adc2fb21f9c12aa96441da33a1728193ae0b95d2be22dbd17fcb4e5c" dependencies = [ "bytes", "futures-core", + "futures-io", "futures-sink", + "futures-util", "pin-project-lite", + "slab", "tokio", "tracing", ] @@ -4981,9 +4953,8 @@ dependencies = [ [[package]] name = "trayicon" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c367fd7cdcdf19234aa104f7e03abe1be526018e4282af9f275bf436b9c9ad23" +version = "0.1.3-1" +source = "git+https://github.com/open-trade/trayicon-rs#8d9c4489287752cc5be4a35c103198f7111112f9" dependencies = [ "winapi 0.3.9", "winit", @@ -4995,12 +4966,6 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" -[[package]] -name = "ttf-parser" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e5d7cd7ab3e47dda6e56542f4bbf3824c15234958c6e1bd6aaa347e93499fdc" - [[package]] name = "typenum" version = "1.15.0" @@ -5222,14 +5187,14 @@ checksum = "d554b7f530dee5964d9a9468d95c1f8b8acae4f282807e7d27d4b03099a46744" [[package]] name = "wayland-client" -version = "0.28.6" +version = "0.29.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ab332350e502f159382201394a78e3cc12d0f04db863429260164ea40e0355" +checksum = "91223460e73257f697d9e23d401279123d36039a3f7a449e983f123292d4458f" dependencies = [ "bitflags", "downcast-rs", "libc", - "nix 0.20.0", + "nix 0.22.3", "scoped-tls", "wayland-commons", "wayland-scanner", @@ -5238,11 +5203,11 @@ dependencies = [ [[package]] name = "wayland-commons" -version = "0.28.6" +version = "0.29.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21817947c7011bbd0a27e11b17b337bfd022e8544b071a2641232047966fbda" +checksum = "94f6e5e340d7c13490eca867898c4cec5af56c27a5ffe5c80c6fc4708e22d33e" dependencies = [ - "nix 0.20.0", + "nix 0.22.3", "once_cell", "smallvec", "wayland-sys", @@ -5250,20 +5215,20 @@ dependencies = [ [[package]] name = "wayland-cursor" -version = "0.28.6" +version = "0.29.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be610084edd1586d45e7bdd275fe345c7c1873598caa464c4fb835dee70fa65a" +checksum = "c52758f13d5e7861fc83d942d3d99bf270c83269575e52ac29e5b73cb956a6bd" dependencies = [ - "nix 0.20.0", + "nix 0.22.3", "wayland-client", "xcursor", ] [[package]] name = "wayland-protocols" -version = "0.28.6" +version = "0.29.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "286620ea4d803bacf61fa087a4242ee316693099ee5a140796aaba02b29f861f" +checksum = "60147ae23303402e41fe034f74fb2c35ad0780ee88a1c40ac09a3be1e7465741" dependencies = [ "bitflags", "wayland-client", @@ -5273,9 +5238,9 @@ dependencies = [ [[package]] name = "wayland-scanner" -version = "0.28.6" +version = "0.29.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce923eb2deb61de332d1f356ec7b6bf37094dc5573952e1c8936db03b54c03f1" +checksum = "39a1ed3143f7a143187156a2ab52742e89dac33245ba505c17224df48939f9e0" dependencies = [ "proc-macro2", "quote", @@ -5284,11 +5249,11 @@ dependencies = [ [[package]] name = "wayland-sys" -version = "0.28.6" +version = "0.29.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d841fca9aed7febf9bed2e9796c49bf58d4152ceda8ac949ebe00868d8f0feb8" +checksum = "d9341df79a8975679188e37dab3889bfa57c44ac2cb6da166f519a81cbe452d4" dependencies = [ - "dlib 0.5.0", + "dlib", "lazy_static", "pkg-config", ] @@ -5444,6 +5409,19 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b749ebd2304aa012c5992d11a25d07b406bdbe5f79d371cb7a918ce501a19eb0" +dependencies = [ + "windows_aarch64_msvc 0.30.0", + "windows_i686_gnu 0.30.0", + "windows_i686_msvc 0.30.0", + "windows_x86_64_gnu 0.30.0", + "windows_x86_64_msvc 0.30.0", +] + [[package]] name = "windows-service" version = "0.4.0" @@ -5488,6 +5466,12 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52695a41e536859d5308cc613b4a022261a274390b25bd29dfff4bf08505f3c2" +[[package]] +name = "windows_aarch64_msvc" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29277a4435d642f775f63c7d1faeb927adba532886ce0287bd985bffb16b6bca" + [[package]] name = "windows_aarch64_msvc" version = "0.36.1" @@ -5500,6 +5484,12 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f54725ac23affef038fecb177de6c9bf065787c2f432f79e3c373da92f3e1d8a" +[[package]] +name = "windows_i686_gnu" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1145e1989da93956c68d1864f32fb97c8f561a8f89a5125f6a2b7ea75524e4b8" + [[package]] name = "windows_i686_gnu" version = "0.36.1" @@ -5512,6 +5502,12 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d5158a43cc43623c0729d1ad6647e62fa384a3d135fd15108d37c683461f64" +[[package]] +name = "windows_i686_msvc" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a09e3a0d4753b73019db171c1339cd4362c8c44baf1bcea336235e955954a6" + [[package]] name = "windows_i686_msvc" version = "0.36.1" @@ -5524,6 +5520,12 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc31f409f565611535130cfe7ee8e6655d3fa99c1c61013981e491921b5ce954" +[[package]] +name = "windows_x86_64_gnu" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca64fcb0220d58db4c119e050e7af03c69e6f4f415ef69ec1773d9aab422d5a" + [[package]] name = "windows_x86_64_gnu" version = "0.36.1" @@ -5536,6 +5538,12 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f2b8c7cbd3bfdddd9ab98769f9746a7fad1bca236554cd032b78d768bc0e89f" +[[package]] +name = "windows_x86_64_msvc" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08cabc9f0066848fef4bc6a1c1668e6efce38b661d2aeec75d18d8617eebb5f1" + [[package]] name = "windows_x86_64_msvc" version = "0.36.1" @@ -5544,9 +5552,9 @@ checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" [[package]] name = "winit" -version = "0.25.0" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79610794594d5e86be473ef7763f604f2159cbac8c94debd00df8fb41e86c2f8" +checksum = "9b43cc931d58b99461188607efd7acb2a093e65fc621f54cad78517a6063e73a" dependencies = [ "bitflags", "cocoa 0.24.0", @@ -5558,18 +5566,19 @@ dependencies = [ "lazy_static", "libc", "log", - "mio 0.7.14", - "mio-misc", - "ndk 0.3.0", - "ndk-glue 0.3.0", + "mio 0.8.3", + "ndk 0.5.0", + "ndk-glue 0.5.2", "ndk-sys 0.2.2", "objc", "parking_lot 0.11.2", "percent-encoding", - "raw-window-handle 0.3.4", - "scopeguard", + "raw-window-handle", "smithay-client-toolkit", + "wasm-bindgen", "wayland-client", + "wayland-protocols", + "web-sys", "winapi 0.3.9", "x11-dl", ] @@ -5601,6 +5610,15 @@ dependencies = [ "toml", ] +[[package]] +name = "wol-rs" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7f97e69b28b256ccfb02472c25057132e234aa8368fea3bb0268def564ce1f2" +dependencies = [ + "clap 3.1.18", +] + [[package]] name = "ws2_32-sys" version = "0.2.1" @@ -5611,6 +5629,15 @@ dependencies = [ "winapi-build", ] +[[package]] +name = "wyz" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b31594f29d27036c383b53b59ed3476874d518f0efb151b27a4c275141390e" +dependencies = [ + "tap", +] + [[package]] name = "x11" version = "2.19.1" @@ -5653,15 +5680,6 @@ dependencies = [ "nom", ] -[[package]] -name = "xdg" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c4583db5cbd4c4c0303df2d15af80f0539db703fa1c68802d4cbbd2dd0f88f6" -dependencies = [ - "dirs", -] - [[package]] name = "xml-rs" version = "0.8.4" diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index fe11857f3..628f962a2 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -17,7 +17,7 @@ import '../../mobile/pages/scan_page.dart'; import '../../mobile/pages/settings_page.dart'; import '../../models/model.dart'; -enum RemoteType { recently, favorite, discovered, addressBook } +// enum RemoteType { recently, favorite, discovered, addressBook } /// Connection page for connecting to a remote peer. class ConnectionPage extends StatefulWidget implements PageShape { @@ -76,74 +76,57 @@ class _ConnectionPageState extends State { thickness: 1, ), Expanded( - child: DefaultTabController( - length: 4, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - TabBar( - isScrollable: true, - indicatorSize: TabBarIndicatorSize.label, - tabs: [ - Tab( - child: Text(translate("Recent Sessions")), - ), - Tab( - child: Text(translate("Favorites")), - ), - Tab( - child: Text(translate("Discovered")), - ), - Tab( - child: Text(translate("Address Book")), - ), - ]), - Expanded( - child: TabBarView(children: [ - RecentPeerWidget(), - FavoritePeerWidget(), - DiscoveredPeerWidget(), - // AddressBookPeerWidget(), - // FutureBuilder( - // future: getPeers(rType: RemoteType.recently), - // builder: (context, snapshot) { - // if (snapshot.hasData) { - // return snapshot.data!; - // } else { - // return Offstage(); - // } - // }), - // FutureBuilder( - // future: getPeers(rType: RemoteType.favorite), - // builder: (context, snapshot) { - // if (snapshot.hasData) { - // return snapshot.data!; - // } else { - // return Offstage(); - // } - // }), - // FutureBuilder( - // future: getPeers(rType: RemoteType.discovered), - // builder: (context, snapshot) { - // if (snapshot.hasData) { - // return snapshot.data!; - // } else { - // return Offstage(); - // } - // }), - FutureBuilder( - future: buildAddressBook(context), - builder: (context, snapshot) { - if (snapshot.hasData) { - return snapshot.data!; - } else { - return Offstage(); - } - }), - ]).paddingSymmetric(horizontal: 12.0, vertical: 4.0)) - ], - )), - ), + // TODO: move all tab info into _PeerTabbedPage + child: _PeerTabbedPage( + tabs: [ + translate('Recent Sessions'), + translate('Favorites'), + translate('Discovered'), + translate('Address Book') + ], + children: [ + RecentPeerWidget(), + FavoritePeerWidget(), + DiscoveredPeerWidget(), + // AddressBookPeerWidget(), + // FutureBuilder( + // future: getPeers(rType: RemoteType.recently), + // builder: (context, snapshot) { + // if (snapshot.hasData) { + // return snapshot.data!; + // } else { + // return Offstage(); + // } + // }), + // FutureBuilder( + // future: getPeers(rType: RemoteType.favorite), + // builder: (context, snapshot) { + // if (snapshot.hasData) { + // return snapshot.data!; + // } else { + // return Offstage(); + // } + // }), + // FutureBuilder( + // future: getPeers(rType: RemoteType.discovered), + // builder: (context, snapshot) { + // if (snapshot.hasData) { + // return snapshot.data!; + // } else { + // return Offstage(); + // } + // }), + FutureBuilder( + future: buildAddressBook(context), + builder: (context, snapshot) { + if (snapshot.hasData) { + return snapshot.data!; + } else { + return Offstage(); + } + }), + ], + )), Divider(), SizedBox(height: 50, child: Obx(() => buildStatus())) .paddingSymmetric(horizontal: 12.0) @@ -329,61 +312,61 @@ class _ConnectionPageState extends State { return true; } - /// Show the peer menu and handle user's choice. - /// User might remove the peer or send a file to the peer. - void showPeerMenu(BuildContext context, String id, RemoteType rType) async { - var items = [ - PopupMenuItem( - child: Text(translate('Connect')), value: 'connect'), - PopupMenuItem( - child: Text(translate('Transfer File')), value: 'file'), - PopupMenuItem( - child: Text(translate('TCP Tunneling')), value: 'tcp-tunnel'), - PopupMenuItem(child: Text(translate('Rename')), value: 'rename'), - rType == RemoteType.addressBook - ? PopupMenuItem( - child: Text(translate('Remove')), value: 'ab-delete') - : PopupMenuItem( - child: Text(translate('Remove')), value: 'remove'), - PopupMenuItem( - child: Text(translate('Unremember Password')), - value: 'unremember-password'), - ]; - if (rType == RemoteType.favorite) { - items.add(PopupMenuItem( - child: Text(translate('Remove from Favorites')), - value: 'remove-fav')); - } else if (rType != RemoteType.addressBook) { - items.add(PopupMenuItem( - child: Text(translate('Add to Favorites')), value: 'add-fav')); - } else { - items.add(PopupMenuItem( - child: Text(translate('Edit Tag')), value: 'ab-edit-tag')); - } - var value = await showMenu( - context: context, - position: this._menuPos, - items: items, - elevation: 8, - ); - if (value == 'remove') { - setState(() => gFFI.setByName('remove', '$id')); - () async { - removePreference(id); - }(); - } else if (value == 'file') { - connect(id, isFileTransfer: true); - } else if (value == 'add-fav') { - } else if (value == 'connect') { - connect(id, isFileTransfer: false); - } else if (value == 'ab-delete') { - gFFI.abModel.deletePeer(id); - await gFFI.abModel.updateAb(); - setState(() {}); - } else if (value == 'ab-edit-tag') { - abEditTag(id); - } - } + // /// Show the peer menu and handle user's choice. + // /// User might remove the peer or send a file to the peer. + // void showPeerMenu(BuildContext context, String id, RemoteType rType) async { + // var items = [ + // PopupMenuItem( + // child: Text(translate('Connect')), value: 'connect'), + // PopupMenuItem( + // child: Text(translate('Transfer File')), value: 'file'), + // PopupMenuItem( + // child: Text(translate('TCP Tunneling')), value: 'tcp-tunnel'), + // PopupMenuItem(child: Text(translate('Rename')), value: 'rename'), + // rType == RemoteType.addressBook + // ? PopupMenuItem( + // child: Text(translate('Remove')), value: 'ab-delete') + // : PopupMenuItem( + // child: Text(translate('Remove')), value: 'remove'), + // PopupMenuItem( + // child: Text(translate('Unremember Password')), + // value: 'unremember-password'), + // ]; + // if (rType == RemoteType.favorite) { + // items.add(PopupMenuItem( + // child: Text(translate('Remove from Favorites')), + // value: 'remove-fav')); + // } else if (rType != RemoteType.addressBook) { + // items.add(PopupMenuItem( + // child: Text(translate('Add to Favorites')), value: 'add-fav')); + // } else { + // items.add(PopupMenuItem( + // child: Text(translate('Edit Tag')), value: 'ab-edit-tag')); + // } + // var value = await showMenu( + // context: context, + // position: this._menuPos, + // items: items, + // elevation: 8, + // ); + // if (value == 'remove') { + // setState(() => gFFI.setByName('remove', '$id')); + // () async { + // removePreference(id); + // }(); + // } else if (value == 'file') { + // connect(id, isFileTransfer: true); + // } else if (value == 'add-fav') { + // } else if (value == 'connect') { + // connect(id, isFileTransfer: false); + // } else if (value == 'ab-delete') { + // gFFI.abModel.deletePeer(id); + // await gFFI.abModel.updateAb(); + // setState(() {}); + // } else if (value == 'ab-edit-tag') { + // abEditTag(id); + // } + // } var svcStopped = false.obs; var svcStatusCode = 0.obs; @@ -896,3 +879,86 @@ class _WebMenuState extends State { }); } } + +class _PeerTabbedPage extends StatefulWidget { + final List tabs; + final List children; + const _PeerTabbedPage({required this.tabs, required this.children, Key? key}) + : super(key: key); + @override + _PeerTabbedPageState createState() => _PeerTabbedPageState(); +} + +class _PeerTabbedPageState extends State<_PeerTabbedPage> + with SingleTickerProviderStateMixin { + late TabController _tabController; + + @override + void initState() { + super.initState(); + _tabController = + TabController(vsync: this, length: super.widget.tabs.length); + _tabController.addListener(_handleTabSelection); + } + + // hard code for now + void _handleTabSelection() { + if (_tabController.indexIsChanging) { + switch (_tabController.index) { + case 0: + break; + case 1: + break; + case 2: + gFFI.bind.mainDiscover(); + break; + case 3: + break; + } + } + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + // return DefaultTabController( + // length: 4, + // child: Column( + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // _createTabBar(), + // _createTabBarView(), + // ], + // )); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _createTabBar(), + _createTabBarView(), + ], + ); + } + + Widget _createTabBar() { + return TabBar( + isScrollable: true, + indicatorSize: TabBarIndicatorSize.label, + controller: _tabController, + tabs: super.widget.tabs.map((t) { + return Tab(child: Text(t)); + }).toList()); + } + + Widget _createTabBarView() { + return Expanded( + child: TabBarView( + controller: _tabController, children: super.widget.children) + .paddingSymmetric(horizontal: 12.0, vertical: 4.0)); + } +} diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index e0a4fa563..3e4810c0e 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -296,6 +296,7 @@ class _RemotePageState extends State Widget getRawPointerAndKeyBody(bool keyboard, Widget child) { return Listener( onPointerHover: (e) { + debugPrint("onPointerHover ${e}"); if (e.kind != ui.PointerDeviceKind.mouse) return; if (!_isPhysicalMouse) { setState(() { @@ -307,6 +308,7 @@ class _RemotePageState extends State } }, onPointerDown: (e) { + debugPrint("onPointerDown ${e}"); if (e.kind != ui.PointerDeviceKind.mouse) { if (_isPhysicalMouse) { setState(() { @@ -319,18 +321,21 @@ class _RemotePageState extends State } }, onPointerUp: (e) { + debugPrint("onPointerUp ${e}"); if (e.kind != ui.PointerDeviceKind.mouse) return; if (_isPhysicalMouse) { _ffi.handleMouse(getEvent(e, 'mouseup')); } }, onPointerMove: (e) { + debugPrint("onPointerMove ${e}"); if (e.kind != ui.PointerDeviceKind.mouse) return; if (_isPhysicalMouse) { _ffi.handleMouse(getEvent(e, 'mousemove')); } }, onPointerSignal: (e) { + debugPrint("onPointerSignal ${e}"); if (e is PointerScrollEvent) { var dx = e.scrollDelta.dx; var dy = e.scrollDelta.dy; diff --git a/flutter/lib/desktop/widgets/peer_widget.dart b/flutter/lib/desktop/widgets/peer_widget.dart index 42cb8eb1d..45e2953eb 100644 --- a/flutter/lib/desktop/widgets/peer_widget.dart +++ b/flutter/lib/desktop/widgets/peer_widget.dart @@ -15,15 +15,13 @@ typedef OffstageFunc = bool Function(Peer peer); typedef PeerCardWidgetFunc = Widget Function(Peer peer); class _PeerWidget extends StatefulWidget { - late final _name; late final _peers; late final OffstageFunc _offstageFunc; late final PeerCardWidgetFunc _peerCardWidgetFunc; - _PeerWidget(String name, List peers, OffstageFunc offstageFunc, + _PeerWidget(Peers peers, OffstageFunc offstageFunc, PeerCardWidgetFunc peerCardWidgetFunc, {Key? key}) : super(key: key) { - _name = name; _peers = peers; _offstageFunc = offstageFunc; _peerCardWidgetFunc = peerCardWidgetFunc; @@ -70,7 +68,7 @@ class _PeerWidgetState extends State<_PeerWidget> with WindowListener { Widget build(BuildContext context) { final space = 8.0; return ChangeNotifierProvider( - create: (context) => Peers(super.widget._name, super.widget._peers), + create: (context) => super.widget._peers, child: SingleChildScrollView( child: Consumer( builder: (context, peers, child) => Wrap( @@ -136,83 +134,69 @@ class _PeerWidgetState extends State<_PeerWidget> with WindowListener { abstract class BasePeerWidget extends StatelessWidget { late final _name; + late final _loadEvent; late final OffstageFunc _offstageFunc; late final PeerCardWidgetFunc _peerCardWidgetFunc; + late final List _initPeers; BasePeerWidget({Key? key}) : super(key: key) {} @override Widget build(BuildContext context) { - return FutureBuilder(future: () async { - return _PeerWidget( - _name, await _loadPeers(), _offstageFunc, _peerCardWidgetFunc); - }(), builder: (context, snapshot) { - if (snapshot.hasData) { - return snapshot.data!; - } else { - return Offstage(); - } - }); + return _PeerWidget(Peers(_name, _loadEvent, _initPeers), _offstageFunc, + _peerCardWidgetFunc); } - - @protected - Future> _loadPeers(); } class RecentPeerWidget extends BasePeerWidget { RecentPeerWidget({Key? key}) : super(key: key) { super._name = "recent peer"; + super._loadEvent = "load_recent_peers"; super._offstageFunc = (Peer _peer) => false; super._peerCardWidgetFunc = (Peer peer) => RecentPeerCard(peer: peer); + super._initPeers = []; } - Future> _loadPeers() async { - debugPrint("call RecentPeerWidget _loadPeers"); - return gFFI.peers(); + @override + Widget build(BuildContext context) { + final widget = super.build(context); + gFFI.bind.mainLoadRecentPeers(); + return widget; } } class FavoritePeerWidget extends BasePeerWidget { FavoritePeerWidget({Key? key}) : super(key: key) { super._name = "favorite peer"; + super._loadEvent = "load_fav_peers"; super._offstageFunc = (Peer _peer) => false; super._peerCardWidgetFunc = (Peer peer) => FavoritePeerCard(peer: peer); + super._initPeers = []; } @override - Future> _loadPeers() async { - debugPrint("call FavoritePeerWidget _loadPeers"); - return await gFFI.bind.mainGetFav().then((peers) async { - final peersEntities = await Future.wait(peers - .map((id) => gFFI.bind.mainGetPeers(id: id)) - .toList(growable: false)) - .then((peers_str) { - final len = peers_str.length; - final ps = List.empty(growable: true); - for (var i = 0; i < len; i++) { - print("${peers[i]}: ${peers_str[i]}"); - ps.add(Peer.fromJson(peers[i], jsonDecode(peers_str[i])['info'])); - } - return ps; - }); - return peersEntities; - }); + Widget build(BuildContext context) { + final widget = super.build(context); + gFFI.bind.mainLoadFavPeers(); + return widget; } } class DiscoveredPeerWidget extends BasePeerWidget { DiscoveredPeerWidget({Key? key}) : super(key: key) { super._name = "discovered peer"; + super._loadEvent = "load_lan_peers"; super._offstageFunc = (Peer _peer) => false; super._peerCardWidgetFunc = (Peer peer) => DiscoveredPeerCard(peer: peer); + super._initPeers = []; } - Future> _loadPeers() async { - debugPrint("call DiscoveredPeerWidget _loadPeers"); - return await gFFI.bind.mainGetLanPeers().then((peers_string) { - debugPrint(peers_string); - return []; - }); + @override + Widget build(BuildContext context) { + debugPrint("DiscoveredPeerWidget build"); + final widget = super.build(context); + gFFI.bind.mainLoadLanPeers(); + return widget; } } @@ -222,10 +206,10 @@ class AddressBookPeerWidget extends BasePeerWidget { super._offstageFunc = (Peer peer) => !_hitTag(gFFI.abModel.selectedTags, peer.tags); super._peerCardWidgetFunc = (Peer peer) => AddressBookPeerCard(peer: peer); + super._initPeers = _loadPeers(); } - Future> _loadPeers() async { - debugPrint("call AddressBookPeerWidget _loadPeers"); + List _loadPeers() { return gFFI.abModel.peers.map((e) { return Peer.fromJson(e['id'], e); }).toList(); diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 23900ef07..df1a91a79 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -125,10 +125,10 @@ class _RemotePageState extends State { oldValue = oldValue.substring(j + 1); var common = 0; for (; - common < oldValue.length && - common < newValue.length && - newValue[common] == oldValue[common]; - ++common) {} + common < oldValue.length && + common < newValue.length && + newValue[common] == oldValue[common]; + ++common) {} for (i = 0; i < oldValue.length - common; ++i) { gFFI.inputKey('VK_BACK'); } @@ -235,13 +235,13 @@ class _RemotePageState extends State { floatingActionButton: !showActionButton ? null : FloatingActionButton( - mini: !hideKeyboard, - child: Icon( - hideKeyboard ? Icons.expand_more : Icons.expand_less), - backgroundColor: MyTheme.accent, - onPressed: () { - setState(() { - if (hideKeyboard) { + mini: !hideKeyboard, + child: Icon( + hideKeyboard ? Icons.expand_more : Icons.expand_less), + backgroundColor: MyTheme.accent, + onPressed: () { + setState(() { + if (hideKeyboard) { _showEdit = false; gFFI.invokeMethod("enable_soft_keyboard", false); _mobileFocusNode.unfocus(); @@ -250,7 +250,7 @@ class _RemotePageState extends State { _showBar = !_showBar; } }); - }), + }), bottomNavigationBar: _showBar && pi.displays.length > 0 ? getBottomAppBar(keyboard) : null, @@ -262,7 +262,7 @@ class _RemotePageState extends State { child: isWebDesktop ? getBodyForDesktopWithListener(keyboard) : SafeArea(child: - OrientationBuilder(builder: (ctx, orientation) { + OrientationBuilder(builder: (ctx, orientation) { if (_currentOrientation != orientation) { Timer(Duration(milliseconds: 200), () { resetMobileActionsOverlay(); @@ -271,10 +271,10 @@ class _RemotePageState extends State { }); } return Container( - color: MyTheme.canvasColor, - child: _isPhysicalMouse - ? getBodyForMobile() - : getBodyForMobileWithGesture()); + color: MyTheme.canvasColor, + child: _isPhysicalMouse + ? getBodyForMobile() + : getBodyForMobileWithGesture()); }))); }) ], @@ -395,14 +395,14 @@ class _RemotePageState extends State { children: [ Row( children: [ - IconButton( - color: Colors.white, - icon: Icon(Icons.clear), - onPressed: () { - clientClose(); - }, - ) - ] + + IconButton( + color: Colors.white, + icon: Icon(Icons.clear), + onPressed: () { + clientClose(); + }, + ) + ] + [ IconButton( color: Colors.white, @@ -441,20 +441,20 @@ class _RemotePageState extends State { : Icons.mouse), onPressed: changeTouchMode, ), - ]) + + ]) + (isWeb ? [] : [ - IconButton( - color: Colors.white, - icon: Icon(Icons.message), - onPressed: () { + IconButton( + color: Colors.white, + icon: Icon(Icons.message), + onPressed: () { gFFI.chatModel .changeCurrentID(ChatModel.clientModeID); toggleChatOverlay(); }, - ) - ]) + + ) + ]) + [ IconButton( color: Colors.white, @@ -602,17 +602,17 @@ class _RemotePageState extends State { child: !_showEdit ? Container() : TextFormField( - textInputAction: TextInputAction.newline, - autocorrect: false, - enableSuggestions: false, - autofocus: true, - focusNode: _mobileFocusNode, - maxLines: null, - initialValue: _value, - // trick way to make backspace work always - keyboardType: TextInputType.multiline, - onChanged: handleInput, - ), + textInputAction: TextInputAction.newline, + autocorrect: false, + enableSuggestions: false, + autofocus: true, + focusNode: _mobileFocusNode, + maxLines: null, + initialValue: _value, + // trick way to make backspace work always + keyboardType: TextInputType.multiline, + onChanged: handleInput, + ), ), ])); } @@ -697,7 +697,7 @@ class _RemotePageState extends State { value: 'block-input')); } } - () async { + () async { var value = await showMenu( context: context, position: RelativeRect.fromLTRB(x, y, x, y), @@ -715,7 +715,7 @@ class _RemotePageState extends State { } else if (value == 'refresh') { gFFI.setByName('refresh'); } else if (value == 'paste') { - () async { + () async { ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain); if (data != null && data.text != null) { gFFI.setByName('input_string', '${data.text}'); @@ -803,25 +803,25 @@ class _RemotePageState extends State { final keys = [ wrap( ' Fn ', - () => setState( + () => setState( () { - _fn = !_fn; - if (_fn) { - _more = false; - } - }, - ), + _fn = !_fn; + if (_fn) { + _more = false; + } + }, + ), _fn), wrap( ' ... ', - () => setState( + () => setState( () { - _more = !_more; - if (_more) { - _fn = false; - } - }, - ), + _more = !_more; + if (_more) { + _fn = false; + } + }, + ), _more), ]; final fn = [ @@ -952,7 +952,8 @@ class ImagePainter extends CustomPainter { } } -CheckboxListTile getToggle(void Function(void Function()) setState, option, name) { +CheckboxListTile getToggle( + void Function(void Function()) setState, option, name) { return CheckboxListTile( value: gFFI.getByName('toggle_option', option) == 'true', onChanged: (v) { diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 743712324..e9367bd4c 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -1028,6 +1028,7 @@ class FFI { RustdeskImpl get bind => ffiModel.platformFFI.ffiBind; handleMouse(Map evt) { + debugPrint("mouse ${evt.toString()}"); var type = ''; var isMove = false; switch (evt['type']) { @@ -1045,7 +1046,7 @@ class FFI { } evt['type'] = type; var x = evt['x']; - var y = evt['y']; + var y = max(0.0, (evt['y'] as double) - 50.0); if (isMove) { canvasModel.moveDesktopMouse(x, y); } diff --git a/flutter/lib/models/peer_model.dart b/flutter/lib/models/peer_model.dart index 939d16ede..eb520f015 100644 --- a/flutter/lib/models/peer_model.dart +++ b/flutter/lib/models/peer_model.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'package:flutter/foundation.dart'; import '../../common.dart'; @@ -35,23 +36,29 @@ class Peer { class Peers extends ChangeNotifier { late String _name; - late var _peers; - static const cbQueryOnlines = 'callback_query_onlines'; + late List _peers; + late final _loadEvent; + static const _cbQueryOnlines = 'callback_query_onlines'; - Peers(String name, List peers) { + Peers(String name, String loadEvent, List _initPeers) { _name = name; - _peers = peers; - gFFI.ffiModel.platformFFI.registerEventHandler(cbQueryOnlines, _name, + _loadEvent = loadEvent; + _peers = _initPeers; + gFFI.ffiModel.platformFFI.registerEventHandler(_cbQueryOnlines, _name, (evt) { _updateOnlineState(evt); }); + gFFI.ffiModel.platformFFI.registerEventHandler(_loadEvent, _name, (evt) { + _updatePeers(evt); + }); } List get peers => _peers; @override void dispose() { - gFFI.ffiModel.platformFFI.unregisterEventHandler(cbQueryOnlines, _name); + gFFI.ffiModel.platformFFI.unregisterEventHandler(_cbQueryOnlines, _name); + gFFI.ffiModel.platformFFI.unregisterEventHandler(_loadEvent, _name); super.dispose(); } @@ -86,4 +93,37 @@ class Peers extends ChangeNotifier { notifyListeners(); } + + void _updatePeers(Map evt) { + final onlineStates = _getOnlineStates(); + _peers = _decodePeers(evt['peers']); + _peers.forEach((peer) { + final state = onlineStates[peer.id]; + peer.online = state != null && state != false; + }); + notifyListeners(); + } + + Map _getOnlineStates() { + var onlineStates = new Map(); + _peers.forEach((peer) { + onlineStates[peer.id] = peer.online; + }); + return onlineStates; + } + + List _decodePeers(String peersStr) { + try { + if (peersStr == "") return []; + List peers = json.decode(peersStr); + return peers + .map((s) => s as List) + .map((s) => + Peer.fromJson(s[0] as String, s[1] as Map)) + .toList(); + } catch (e) { + print('peers(): $e'); + } + return []; + } } diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index f1aeabfcc..9f40b69d5 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -7,11 +7,11 @@ use std::{ use flutter_rust_bridge::{StreamSink, SyncReturn, ZeroCopyBuffer}; use serde_json::{json, Number, Value}; -use hbb_common::{ResultType, password_security}; use hbb_common::{ config::{self, Config, LocalConfig, PeerConfig, ONLINE}, fs, log, }; +use hbb_common::{password_security, ResultType}; use crate::client::file_trait::FileManager; use crate::common::make_fd_to_json; @@ -20,7 +20,7 @@ use crate::flutter::{self, Session, SESSIONS}; use crate::start_server; use crate::ui_interface; use crate::ui_interface::{ - change_id, check_connect_status, forget_password, get_api_server, get_app_name, + change_id, check_connect_status, discover, forget_password, get_api_server, get_app_name, get_async_job_status, get_connect_status, get_fav, get_id, get_lan_peers, get_license, get_local_option, get_options, get_peer, get_peer_option, get_socks, get_sound_inputs, get_uuid, get_version, has_rendezvous_service, is_ok_change_id, post_request, set_local_option, @@ -469,6 +469,10 @@ pub fn main_is_using_public_server() -> bool { using_public_server() } +pub fn main_discover() { + discover(); +} + pub fn main_has_rendezvous_service() -> bool { has_rendezvous_service() } @@ -509,6 +513,61 @@ pub fn main_forget_password(id: String) { forget_password(id) } +pub fn main_load_recent_peers() { + if !config::APP_DIR.read().unwrap().is_empty() { + let peers: Vec<(String, config::PeerInfoSerde)> = PeerConfig::peers() + .drain(..) + .map(|(id, _, p)| (id, p.info)) + .collect(); + if let Some(s) = flutter::GLOBAL_EVENT_STREAM.read().unwrap().as_ref() { + let data = HashMap::from([ + ("name", "load_recent_peers".to_owned()), + ( + "peers", + serde_json::ser::to_string(&peers).unwrap_or("".to_owned()), + ), + ]); + s.add(serde_json::ser::to_string(&data).unwrap_or("".to_owned())); + }; + } +} + +pub fn main_load_fav_peers() { + if !config::APP_DIR.read().unwrap().is_empty() { + let favs = get_fav(); + let peers: Vec<(String, config::PeerInfoSerde)> = PeerConfig::peers() + .into_iter() + .filter_map(|(id, _, peer)| { + if favs.contains(&id) { + Some((id, peer.info)) + } else { + None + } + }) + .collect(); + if let Some(s) = flutter::GLOBAL_EVENT_STREAM.read().unwrap().as_ref() { + let data = HashMap::from([ + ("name", "load_fav_peers".to_owned()), + ( + "peers", + serde_json::ser::to_string(&peers).unwrap_or("".to_owned()), + ), + ]); + s.add(serde_json::ser::to_string(&data).unwrap_or("".to_owned())); + }; + } +} + +pub fn main_load_lan_peers() { + if let Some(s) = flutter::GLOBAL_EVENT_STREAM.read().unwrap().as_ref() { + let data = HashMap::from([ + ("name", "load_lan_peers".to_owned()), + ("peers", get_lan_peers()), + ]); + s.add(serde_json::ser::to_string(&data).unwrap_or("".to_owned())); + }; +} + /// FFI for **get** commands which are idempotent. /// Return result in c string. /// diff --git a/src/lan.rs b/src/lan.rs index 733e271a9..f74492b8e 100644 --- a/src/lan.rs +++ b/src/lan.rs @@ -277,6 +277,9 @@ async fn handle_received_peers(mut rx: UnboundedReceiver) if last_write_time.elapsed().as_millis() > 300 { config::LanPeers::store(&peers); last_write_time = Instant::now(); + + #[cfg(feature = "flutter")] + crate::flutter_ffi::main_load_lan_peers(); } } None => { diff --git a/src/rendezvous_mediator.rs b/src/rendezvous_mediator.rs index 6e38bff21..08a1316f0 100644 --- a/src/rendezvous_mediator.rs +++ b/src/rendezvous_mediator.rs @@ -604,68 +604,6 @@ fn lan_discovery() -> ResultType<()> { } } -pub fn discover() -> ResultType<()> { - let addr = SocketAddr::from(([0, 0, 0, 0], 0)); - let socket = std::net::UdpSocket::bind(addr)?; - socket.set_broadcast(true)?; - let mut msg_out = Message::new(); - let peer = PeerDiscovery { - cmd: "ping".to_owned(), - ..Default::default() - }; - msg_out.set_peer_discovery(peer); - let maddr = SocketAddr::from(([255, 255, 255, 255], get_broadcast_port())); - socket.send_to(&msg_out.write_to_bytes()?, maddr)?; - log::info!("discover ping sent"); - let mut last_recv_time = Instant::now(); - let mut last_write_time = Instant::now(); - let mut last_write_n = 0; - // to-do: load saved peers, and update incrementally (then we can see offline) - let mut peers = Vec::new(); - let mac = get_mac(); - socket.set_read_timeout(Some(std::time::Duration::from_millis(10)))?; - loop { - let mut buf = [0; 2048]; - if let Ok((len, _)) = socket.recv_from(&mut buf) { - if let Ok(msg_in) = Message::parse_from_bytes(&buf[0..len]) { - match msg_in.union { - Some(rendezvous_message::Union::PeerDiscovery(p)) => { - last_recv_time = Instant::now(); - if p.cmd == "pong" { - if p.mac != mac { - let dp = DiscoveryPeer { - id: "".to_string(), - ip_mac: HashMap::from([ - // TODO: addr ip - (addr.ip().to_string(), p.mac.clone()), - ]), - username: p.username, - hostname: p.hostname, - platform: p.platform, - online: true, - }; - peers.push(dp); - } - } - } - _ => {} - } - } - } - if last_write_time.elapsed().as_millis() > 300 && last_write_n != peers.len() { - config::LanPeers::store(&peers); - last_write_time = Instant::now(); - last_write_n = peers.len(); - } - if last_recv_time.elapsed().as_millis() > 3_000 { - break; - } - } - log::info!("discover ping done"); - config::LanPeers::store(&peers); - Ok(()) -} - #[tokio::main(flavor = "current_thread")] pub async fn query_online_states, Vec)>(ids: Vec, f: F) { let test = false; diff --git a/src/ui.rs b/src/ui.rs index c2bc8cbc3..f51b3e7c9 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -20,6 +20,7 @@ use hbb_common::{ }; use crate::common::{get_app_name, SOFTWARE_UPDATE_URL}; +use crate::ipc; use crate::ui_interface::{ check_mouse_time, closing, create_shortcut, current_is_wayland, fix_login_wayland, forget_password, get_api_server, get_async_job_status, get_connect_status, get_error, get_fav, @@ -35,7 +36,6 @@ use crate::ui_interface::{ show_run_without_install, store_fav, t, temporary_password, test_if_valid_server, update_me, update_temporary_password, using_public_server, }; -use crate::{discover, ipc}; mod cm; #[cfg(feature = "inline")] @@ -493,7 +493,9 @@ impl UI { } fn discover(&self) { - discover(); + std::thread::spawn(move || { + allow_err!(crate::lan::discover()); + }); } fn get_lan_peers(&self) -> String { diff --git a/src/ui_interface.rs b/src/ui_interface.rs index a3643d5c9..5b4850271 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -9,12 +9,12 @@ use hbb_common::{ allow_err, config::{self, Config, LocalConfig, PeerConfig, RENDEZVOUS_PORT, RENDEZVOUS_TIMEOUT}, futures::future::join_all, - log, + log, password_security, protobuf::Message as _, rendezvous_proto::*, sleep, tcp::FramedStream, - tokio::{self, sync::mpsc, time}, password_security, + tokio::{self, sync::mpsc, time}, }; use crate::common::SOFTWARE_UPDATE_URL; @@ -538,12 +538,26 @@ pub fn create_shortcut(_id: String) { pub fn discover() { std::thread::spawn(move || { - allow_err!(crate::rendezvous_mediator::discover()); + allow_err!(crate::lan::discover()); }); } pub fn get_lan_peers() -> String { - serde_json::to_string(&config::LanPeers::load().peers).unwrap_or_default() + let peers: Vec<(String, config::PeerInfoSerde)> = config::LanPeers::load() + .peers + .iter() + .map(|peer| { + ( + peer.id.clone(), + config::PeerInfoSerde { + username: peer.username.clone(), + hostname: peer.hostname.clone(), + platform: peer.platform.clone(), + }, + ) + }) + .collect(); + serde_json::to_string(&peers).unwrap_or_default() } pub fn get_uuid() -> String { From 7775a14c9e9ad7e231b317372f6830e20b714902 Mon Sep 17 00:00:00 2001 From: Asura Date: Tue, 2 Aug 2022 03:47:29 -0700 Subject: [PATCH 0136/2015] Use keycode mapping table --- Cargo.lock | 11 +++++++++++ Cargo.toml | 3 +++ src/server/input_service.rs | 9 +++++++-- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 833cbe12a..e341ff459 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4053,6 +4053,7 @@ dependencies = [ "mouce", "num_cpus", "objc", + "once_cell", "parity-tokio-ipc", "rdev", "repng", @@ -4071,6 +4072,7 @@ dependencies = [ "simple_rc", "sys-locale", "sysinfo", + "tfc", "tray-item", "trayicon", "uuid", @@ -4755,6 +4757,15 @@ version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" +[[package]] +name = "tfc" +version = "0.6.1" +source = "git+https://github.com/asur4s/The-Fat-Controller#35ed0bc8dd8516bdb99e45ebfc94409637a92c6b" +dependencies = [ + "core-graphics 0.22.3", + "unicode-segmentation", +] + [[package]] name = "thiserror" version = "1.0.31" diff --git a/Cargo.toml b/Cargo.toml index 825f0d739..20ec129ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -73,6 +73,8 @@ sys-locale = "0.2" enigo = { path = "libs/enigo", features = [ "with_serde" ] } clipboard = { path = "libs/clipboard" } rdev = { git = "https://github.com/asur4s/rdev" } +tfc = { git = "https://github.com/asur4s/The-Fat-Controller" } +once_cell = "1.13.0" ctrlc = "3.2" arboard = "2.0" #minreq = { version = "2.4", features = ["punycode", "https-native"] } @@ -103,6 +105,7 @@ async-process = "1.3" mouce = { git="https://github.com/fufesou/mouce.git" } evdev = { git="https://github.com/fufesou/evdev" } + [target.'cfg(target_os = "android")'.dependencies] android_logger = "0.11" jni = "0.19" diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 629ed17e4..e3ac41a59 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -9,6 +9,7 @@ use std::{ sync::atomic::{AtomicBool, Ordering}, time::Instant, }; +use tfc::{traits::*, Context}; #[derive(Default)] struct StateCursor { @@ -179,6 +180,7 @@ lazy_static::lazy_static! { }; static ref KEYS_DOWN: Arc>> = Default::default(); static ref LATEST_INPUT: Arc> = Default::default(); + static ref KBD_CONTEXT: Mutex = Mutex::new(Context::new().expect("kbd context error")); } static EXITING: AtomicBool = AtomicBool::new(false); @@ -820,9 +822,12 @@ fn legacy_keyboard_mode(evt: &KeyEvent) { } fn translate_keyboard_mode(evt: &KeyEvent) { - dbg!(evt.chr()); let chr = char::from_u32(evt.chr()).unwrap_or_default(); - rdev::simulate_char(chr, evt.down); + if evt.down { + KBD_CONTEXT.lock().unwrap().unicode_char_down(chr).expect("unicode_char_down error"); + } else { + KBD_CONTEXT.lock().unwrap().unicode_char_up(chr).expect("unicode_char_up error"); + } } fn handle_key_(evt: &KeyEvent) { From 5dfc41a7b8c462f0d1e952098ef37e0186990bfd Mon Sep 17 00:00:00 2001 From: Asura Date: Tue, 2 Aug 2022 06:07:44 -0700 Subject: [PATCH 0137/2015] Ignore dead keys in Linux --- Cargo.lock | 2 +- src/ui/remote.rs | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7d4131d41..24418f0a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3795,7 +3795,7 @@ dependencies = [ [[package]] name = "rdev" version = "0.5.0-2" -source = "git+https://github.com/asur4s/rdev#d009906ba983f26c7b6f6f1a5e3c76bf43164294" +source = "git+https://github.com/asur4s/rdev#c1175a394d811473e87ea79cb9a511a0f9b71764" dependencies = [ "cocoa", "core-foundation 0.9.3", diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 213158bfc..203a20614 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -1077,7 +1077,17 @@ impl Handler { let string = match KEYBOARD.lock() { Ok(mut keyboard) => { let string = keyboard.add(&evt.event_type).unwrap_or_default(); - if keyboard.last_is_dead && string == "" { + #[cfg(target_os = "windows")] + let is_dead = keyboard.last_is_dead; + #[cfg(target_os = "linux")] + let is_dead = unsafe { + CStr::from_ptr(XKeysymToString(*keyboard.keysym)) + .to_str() + .unwrap_or_default() + .to_owned() + .starts_with("dead") + }; + if is_dead && string == "" { return; } string From ffbab698b790dff3ea354588466c8f33de400f05 Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 1 Aug 2022 20:42:30 +0800 Subject: [PATCH 0138/2015] password Signed-off-by: 21pages --- .../lib/desktop/pages/desktop_home_page.dart | 287 +++++++++++++++--- flutter/lib/models/server_model.dart | 47 ++- src/flutter_ffi.rs | 18 +- src/ui.rs | 8 +- src/ui_interface.rs | 63 ++-- 5 files changed, 353 insertions(+), 70 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index a162c3535..54a52b774 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -8,6 +8,7 @@ import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/desktop/pages/connection_page.dart'; import 'package:flutter_hbb/desktop/widgets/titlebar_widget.dart'; import 'package:flutter_hbb/models/model.dart'; +import 'package:flutter_hbb/models/server_model.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -127,9 +128,8 @@ class _DesktopHomePageState extends State with TrayListener { ), TextFormField( controller: model.serverId, - decoration: InputDecoration( - enabled: false, - ), + enableInteractiveSelection: true, + readOnly: true, ), ], ), @@ -248,8 +248,34 @@ class _DesktopHomePageState extends State with TrayListener { translate("Password"), style: TextStyle(fontSize: 18, fontWeight: FontWeight.w500), ), - TextFormField( - controller: model.serverPasswd, + Row( + children: [ + Expanded( + child: TextFormField( + controller: model.serverPasswd, + enableInteractiveSelection: true, + readOnly: true, + ), + ), + IconButton( + icon: Icon(Icons.refresh), + onPressed: () { + gFFI.setByName("temporary_password"); + }, + ), + FutureBuilder( + future: buildPasswordPopupMenu(context), + builder: (context, snapshot) { + if (snapshot.hasError) { + print("${snapshot.error}"); + } + if (snapshot.hasData) { + return snapshot.data!; + } else { + return Offstage(); + } + }) + ], ), ], ), @@ -260,6 +286,83 @@ class _DesktopHomePageState extends State with TrayListener { ); } + Future buildPasswordPopupMenu(BuildContext context) async { + var position; + return GestureDetector( + onTapDown: (detail) { + final x = detail.globalPosition.dx; + final y = detail.globalPosition.dy; + position = RelativeRect.fromLTRB(x, y, x, y); + }, + onTap: () async { + var method = (String text, String value) => PopupMenuItem( + child: Row( + children: [ + Offstage( + offstage: gFFI.serverModel.verificationMethod != value, + child: Icon(Icons.check)), + Text( + text, + ), + ], + ), + value: value, + onTap: () => gFFI.serverModel.verificationMethod = value, + ); + final temporary_enabled = + gFFI.serverModel.verificationMethod != kUsePermanentPassword; + var menu = [ + method(translate("Use temporary password"), kUseTemporaryPassword), + method(translate("Use permanent password"), kUsePermanentPassword), + method(translate("Use both passwords"), kUseBothPasswords), + PopupMenuItem( + child: Text(translate("Set permanent password")), + value: 'set-permanent-password', + enabled: gFFI.serverModel.verificationMethod != + kUseTemporaryPassword), + PopupMenuItem( + child: PopupMenuButton( + child: Text("Set temporary password length"), + itemBuilder: (context) => ["6", "8", "10"] + .map((e) => PopupMenuItem( + child: Row( + children: [ + Offstage( + offstage: gFFI.serverModel + .temporaryPasswordLength != + e, + child: Icon(Icons.check)), + Text( + e, + ), + ], + ), + value: e, + onTap: () { + if (gFFI.serverModel.temporaryPasswordLength != + e) { + gFFI.serverModel.temporaryPasswordLength = e; + gFFI.setByName("temporary_password"); + } + }, + )) + .toList(), + enabled: temporary_enabled, + ), + value: 'set-temporary-password-length', + enabled: temporary_enabled), + ]; + final v = + await showMenu(context: context, position: position, items: menu); + if (v != null) { + if (v == "set-permanent-password") { + setPasswordDialog(); + } + } + }, + child: Icon(Icons.edit)); + } + buildTip(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0), @@ -295,15 +398,15 @@ class _DesktopHomePageState extends State with TrayListener { Text(translate("Control Remote Desktop")), Form( child: Column( - children: [ - TextFormField( - controller: TextEditingController(), - inputFormatters: [ - FilteringTextInputFormatter.allow(RegExp(r"[0-9]")) - ], - ) + children: [ + TextFormField( + controller: TextEditingController(), + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp(r"[0-9]")) ], - )) + ) + ], + )) ], ), ); @@ -320,7 +423,7 @@ class _DesktopHomePageState extends State with TrayListener { case "quit": exit(0); case "show": - // windowManager.show(); + // windowManager.show(); break; default: break; @@ -398,7 +501,7 @@ class _DesktopHomePageState extends State with TrayListener { return isPositive ? TextStyle() : TextStyle( - color: Colors.redAccent, decoration: TextDecoration.lineThrough); + color: Colors.redAccent, decoration: TextDecoration.lineThrough); } PopupMenuItem genAudioInputPopupMenuItem() { @@ -410,29 +513,29 @@ class _DesktopHomePageState extends State with TrayListener { future: gFFI.getAudioInputs(), builder: (context, snapshot) { if (snapshot.hasData) { - final inputs = snapshot.data!; + final inputs = snapshot.data!.toList(); if (Platform.isWindows) { inputs.insert(0, translate("System Sound")); } var inputList = inputs .map((e) => PopupMenuItem( - child: Row( - children: [ - Obx(() => Offstage( - offstage: defaultInput.value != e, - child: Icon(Icons.check))), - Expanded( - child: Tooltip( - message: e, - child: Text( - "$e", - maxLines: 1, - overflow: TextOverflow.ellipsis, - ))), - ], - ), - value: e, - )) + child: Row( + children: [ + Obx(() => Offstage( + offstage: defaultInput.value != e, + child: Icon(Icons.check))), + Expanded( + child: Tooltip( + message: e, + child: Text( + "$e", + maxLines: 1, + overflow: TextOverflow.ellipsis, + ))), + ], + ), + value: e, + )) .toList(); inputList.insert( 0, @@ -553,7 +656,7 @@ class _DesktopHomePageState extends State with TrayListener { void changeServer() async { Map oldOptions = - jsonDecode(await gFFI.bind.mainGetOptions()); + jsonDecode(await gFFI.bind.mainGetOptions()); print("${oldOptions}"); String idServer = oldOptions['custom-rendezvous-server'] ?? ""; var idServerMsg = ""; @@ -592,7 +695,7 @@ class _DesktopHomePageState extends State with TrayListener { decoration: InputDecoration( border: OutlineInputBorder(), errorText: - idServerMsg.isNotEmpty ? idServerMsg : null), + idServerMsg.isNotEmpty ? idServerMsg : null), controller: TextEditingController(text: idServer), ), ), @@ -645,7 +748,7 @@ class _DesktopHomePageState extends State with TrayListener { decoration: InputDecoration( border: OutlineInputBorder(), errorText: - apiServerMsg.isNotEmpty ? apiServerMsg : null), + apiServerMsg.isNotEmpty ? apiServerMsg : null), controller: TextEditingController(text: apiServer), ), ), @@ -761,7 +864,7 @@ class _DesktopHomePageState extends State with TrayListener { void changeWhiteList() async { Map oldOptions = - jsonDecode(await gFFI.bind.mainGetOptions()); + jsonDecode(await gFFI.bind.mainGetOptions()); var newWhiteList = ((oldOptions['whitelist'] ?? "") as String).split(','); var newWhiteListField = newWhiteList.join('\n'); var msg = ""; @@ -817,7 +920,7 @@ class _DesktopHomePageState extends State with TrayListener { // pass } else { final ips = - newWhiteListField.trim().split(RegExp(r"[\s,;\n]+")); + newWhiteListField.trim().split(RegExp(r"[\s,;\n]+")); // test ip final ipMatch = RegExp(r"^\d+\.\d+\.\d+\.\d+$"); for (final ip in ips) { @@ -977,7 +1080,8 @@ class _DesktopHomePageState extends State with TrayListener { return; } } - await gFFI.bind.mainSetSocks(proxy: proxy, username: username, password: password); + await gFFI.bind.mainSetSocks( + proxy: proxy, username: username, password: password); close(); }, child: Text(translate("OK"))), @@ -1204,4 +1308,107 @@ Future loginDialog() async { ); }); return completer.future; -} \ No newline at end of file +} + +void setPasswordDialog() { + final pw = gFFI.getByName("permanent_password"); + final p0 = TextEditingController(text: pw); + final p1 = TextEditingController(text: pw); + var errMsg0 = ""; + var errMsg1 = ""; + + DialogManager.show((setState, close) { + return CustomAlertDialog( + title: Text(translate("Set Password")), + content: ConstrainedBox( + constraints: BoxConstraints(minWidth: 500), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: 8.0, + ), + Row( + children: [ + ConstrainedBox( + constraints: BoxConstraints(minWidth: 100), + child: Text( + "${translate('Password')}:", + textAlign: TextAlign.start, + ).marginOnly(bottom: 16.0)), + SizedBox( + width: 24.0, + ), + Expanded( + child: TextField( + obscureText: true, + decoration: InputDecoration( + border: OutlineInputBorder(), + errorText: errMsg0.isNotEmpty ? errMsg0 : null), + controller: p0, + ), + ), + ], + ), + SizedBox( + height: 8.0, + ), + Row( + children: [ + ConstrainedBox( + constraints: BoxConstraints(minWidth: 100), + child: Text("${translate('Confirmation')}:") + .marginOnly(bottom: 16.0)), + SizedBox( + width: 24.0, + ), + Expanded( + child: TextField( + obscureText: true, + decoration: InputDecoration( + border: OutlineInputBorder(), + errorText: errMsg1.isNotEmpty ? errMsg1 : null), + controller: p1, + ), + ), + ], + ), + SizedBox( + height: 4.0, + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () { + close(); + }, + child: Text(translate("Cancel"))), + TextButton( + onPressed: () { + setState(() { + errMsg0 = ""; + errMsg1 = ""; + }); + final pass = p0.text.trim(); + if (pass.length < 6) { + setState(() { + errMsg0 = translate("Too short, at least 6 characters."); + }); + return; + } + if (p1.text.trim() != pass) { + setState(() { + errMsg1 = translate("The confirmation is not identical."); + }); + return; + } + gFFI.setByName("permanent_password", pass); + close(); + }, + child: Text(translate("OK"))), + ], + ); + }); +} diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index 8ea9e1c93..c9147441e 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -23,6 +23,7 @@ class ServerModel with ChangeNotifier { bool _fileOk = false; int _connectStatus = 0; // Rendezvous Server status String _verificationMethod = ""; + String _temporaryPasswordLength = ""; late String _emptyIdShow; late final TextEditingController _serverId; @@ -42,7 +43,35 @@ class ServerModel with ChangeNotifier { int get connectStatus => _connectStatus; - String get verificationMethod => _verificationMethod; + String get verificationMethod { + final index = [ + kUseTemporaryPassword, + kUsePermanentPassword, + kUseBothPasswords + ].indexOf(_verificationMethod); + if (index < 0) { + _verificationMethod = kUseBothPasswords; + } + return _verificationMethod; + } + + set verificationMethod(String method) { + _verificationMethod = method; + gFFI.setOption("verification-method", method); + } + + String get temporaryPasswordLength { + final lengthIndex = ["6", "8", "10"].indexOf(_temporaryPasswordLength); + if (lengthIndex < 0) { + _temporaryPasswordLength = "6"; + } + return _temporaryPasswordLength; + } + + set temporaryPasswordLength(String length) { + _temporaryPasswordLength = length; + gFFI.setOption("temporary-password-length", length); + } TextEditingController get serverId => _serverId; @@ -127,16 +156,26 @@ class ServerModel with ChangeNotifier { updatePasswordModel() { var update = false; final temporaryPassword = gFFI.getByName("temporary_password"); - final verificationMethod = gFFI.getByName("option", "verification-method"); + final verificationMethod = gFFI.getOption("verification-method"); + final temporaryPasswordLength = gFFI.getOption("temporary-password-length"); + final oldPwdText = _serverPasswd.text; if (_serverPasswd.text != temporaryPassword) { _serverPasswd.text = temporaryPassword; + } + if (verificationMethod == kUsePermanentPassword) { + _serverPasswd.text = '-'; + } + if (oldPwdText != _serverPasswd.text) { update = true; } - if (_verificationMethod != verificationMethod) { _verificationMethod = verificationMethod; update = true; } + if (_temporaryPasswordLength != temporaryPasswordLength) { + _temporaryPasswordLength = temporaryPasswordLength; + update = true; + } if (update) { notifyListeners(); } @@ -272,7 +311,7 @@ class ServerModel with ChangeNotifier { Future setPermanentPassword(String newPW) async { parent.target?.setByName("permanent_password", newPW); await Future.delayed(Duration(milliseconds: 500)); - final pw = parent.target?.getByName("permanent_password", newPW); + final pw = parent.target?.getByName("permanent_password"); if (newPW == pw) { return true; } else { diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 9f40b69d5..413e3cde3 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -24,8 +24,7 @@ use crate::ui_interface::{ get_async_job_status, get_connect_status, get_fav, get_id, get_lan_peers, get_license, get_local_option, get_options, get_peer, get_peer_option, get_socks, get_sound_inputs, get_uuid, get_version, has_rendezvous_service, is_ok_change_id, post_request, set_local_option, - set_options, set_peer_option, set_socks, store_fav, temporary_password, test_if_valid_server, - using_public_server, + set_options, set_peer_option, set_socks, store_fav, test_if_valid_server, using_public_server, }; fn initialize(app_dir: &str) { @@ -613,7 +612,7 @@ unsafe extern "C" fn get_by_name(name: *const c_char, arg: *const c_char) -> *co } "option" => { if let Ok(arg) = arg.to_str() { - res = Config::get_option(arg); + res = ui_interface::get_option(arg.to_owned()); } } // "image_quality" => { @@ -642,7 +641,10 @@ unsafe extern "C" fn get_by_name(name: *const c_char, arg: *const c_char) -> *co res = ui_interface::get_id(); } "temporary_password" => { - res = password_security::temporary_password(); + res = ui_interface::temporary_password(); + } + "permanent_password" => { + res = ui_interface::permanent_password(); } "connect_statue" => { res = ONLINE @@ -829,7 +831,7 @@ unsafe extern "C" fn set_by_name(name: *const c_char, value: *const c_char) { if let Ok(m) = serde_json::from_str::>(value) { if let Some(name) = m.get("name") { if let Some(value) = m.get("value") { - Config::set_option(name.to_owned(), value.to_owned()); + ui_interface::set_option(name.to_owned(), value.to_owned()); if name == "custom-rendezvous-server" { #[cfg(target_os = "android")] crate::rendezvous_mediator::RendezvousMediator::restart(); @@ -1049,6 +1051,12 @@ unsafe extern "C" fn set_by_name(name: *const c_char, value: *const c_char) { connection_manager::close_conn(id); }; } + "temporary_password" => { + ui_interface::update_temporary_password(); + } + "permanent_password" => { + ui_interface::set_permanent_password(value.to_owned()); + } _ => { log::error!("Unknown name of set_by_name: {}", name); } diff --git a/src/ui.rs b/src/ui.rs index f51b3e7c9..284c3c55d 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -32,9 +32,9 @@ use crate::ui_interface::{ is_login_wayland, is_ok_change_id, is_process_trusted, is_rdp_service_open, is_share_rdp, is_xfce, modify_default_login, new_remote, open_url, peer_has_password, permanent_password, post_request, recent_sessions_updated, remove_peer, run_without_install, set_local_option, - set_option, set_options, set_peer_option, set_remote_id, set_share_rdp, set_socks, - show_run_without_install, store_fav, t, temporary_password, test_if_valid_server, update_me, - update_temporary_password, using_public_server, + set_option, set_options, set_peer_option, set_permanent_password, set_remote_id, set_share_rdp, + set_socks, show_run_without_install, store_fav, t, temporary_password, test_if_valid_server, + update_me, update_temporary_password, using_public_server, }; mod cm; @@ -205,7 +205,7 @@ impl UI { } fn set_permanent_password(&self, password: String) { - allow_err!(ipc::set_permanent_password(password)); + set_permanent_password(password); } fn get_remote_id(&mut self) -> String { diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 5b4850271..7e08f9855 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -5,11 +5,13 @@ use std::{ time::SystemTime, }; +#[cfg(any(target_os = "android", target_os = "ios"))] +use hbb_common::password_security; use hbb_common::{ allow_err, config::{self, Config, LocalConfig, PeerConfig, RENDEZVOUS_PORT, RENDEZVOUS_TIMEOUT}, futures::future::join_all, - log, password_security, + log, protobuf::Message as _, rendezvous_proto::*, sleep, @@ -129,7 +131,10 @@ pub fn get_license() -> String { } pub fn get_option(key: String) -> String { - get_option_(&key) + #[cfg(any(target_os = "android", target_os = "ios"))] + return Config::get_option(arg); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + return get_option_(&key); } fn get_option_(key: &str) -> String { @@ -243,20 +248,25 @@ pub fn set_options(m: HashMap) { } pub fn set_option(key: String, value: String) { - let mut options = OPTIONS.lock().unwrap(); - #[cfg(target_os = "macos")] - if &key == "stop-service" { - let is_stop = value == "Y"; - if is_stop && crate::platform::macos::uninstall() { - return; + #[cfg(any(target_os = "android", target_os = "ios"))] + Config::set_option(name.to_owned(), value.to_owned()); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + let mut options = OPTIONS.lock().unwrap(); + #[cfg(target_os = "macos")] + if &key == "stop-service" { + let is_stop = value == "Y"; + if is_stop && crate::platform::macos::uninstall() { + return; + } } + if value.is_empty() { + options.remove(&key); + } else { + options.insert(key.clone(), value.clone()); + } + ipc::set_options(options.clone()).ok(); } - if value.is_empty() { - options.remove(&key); - } else { - options.insert(key.clone(), value.clone()); - } - ipc::set_options(options.clone()).ok(); } pub fn install_path() -> String { @@ -358,16 +368,32 @@ pub fn get_connect_status() -> Status { res } +pub fn temporary_password() -> String { + #[cfg(any(target_os = "android", target_os = "ios"))] + return password_security::temporary_password(); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + return TEMPORARY_PASSWD.lock().unwrap().clone(); +} + pub fn update_temporary_password() { + #[cfg(any(target_os = "android", target_os = "ios"))] + password_security::update_temporary_password(); + #[cfg(not(any(target_os = "android", target_os = "ios")))] allow_err!(ipc::update_temporary_password()); } pub fn permanent_password() -> String { - ipc::get_permanent_password() + #[cfg(any(target_os = "android", target_os = "ios"))] + return Config::get_permanent_password(); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + return ipc::get_permanent_password(); } -pub fn temporary_password() -> String { - password_security::temporary_password() +pub fn set_permanent_password(password: String) { + #[cfg(any(target_os = "android", target_os = "ios"))] + Config::set_permanent_password(&password); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + allow_err!(ipc::set_permanent_password(password)); } pub fn get_peer(id: String) -> PeerConfig { @@ -680,6 +706,8 @@ async fn check_connect_status_(reconnect: bool, rx: mpsc::UnboundedReceiver { if name == "id" { id = value; + } else if name == "temporary-password" { + *TEMPORARY_PASSWD.lock().unwrap() = value; } } Ok(Some(ipc::Data::OnlineStatus(Some((mut x, c))))) => { @@ -699,6 +727,7 @@ async fn check_connect_status_(reconnect: bool, rx: mpsc::UnboundedReceiver Date: Tue, 2 Aug 2022 10:48:56 -0700 Subject: [PATCH 0139/2015] Listening for char in Linux --- Cargo.lock | 5 +++-- src/ui/remote.rs | 12 +++--------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 24418f0a5..af872e57d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3795,7 +3795,7 @@ dependencies = [ [[package]] name = "rdev" version = "0.5.0-2" -source = "git+https://github.com/asur4s/rdev#c1175a394d811473e87ea79cb9a511a0f9b71764" +source = "git+https://github.com/asur4s/rdev#d6499d2e582bf3549aa4ba33cfd3fbbdfce10947" dependencies = [ "cocoa", "core-foundation 0.9.3", @@ -4760,10 +4760,11 @@ checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" [[package]] name = "tfc" version = "0.6.1" -source = "git+https://github.com/asur4s/The-Fat-Controller#35ed0bc8dd8516bdb99e45ebfc94409637a92c6b" +source = "git+https://github.com/asur4s/The-Fat-Controller#6587681075fa312a0d69587721d1ff84d8fa2970" dependencies = [ "core-graphics 0.22.3", "unicode-segmentation", + "winapi 0.3.9", ] [[package]] diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 203a20614..994cd04b2 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -1034,10 +1034,10 @@ impl Handler { } } - fn map_keyboard_mode(&mut self, down_or_up: bool, key: RdevKey, evt: Option) { + fn map_keyboard_mode(&mut self, down_or_up: bool, key: RdevKey, _evt: Option) { // map mode(1): Send keycode according to the peer platform. #[cfg(target_os = "windows")] - let key = if let Some(e) = evt { + let key = if let Some(e) = _evt { rdev::get_win_key(e.code.into(), e.scan_code) } else { key @@ -1080,13 +1080,7 @@ impl Handler { #[cfg(target_os = "windows")] let is_dead = keyboard.last_is_dead; #[cfg(target_os = "linux")] - let is_dead = unsafe { - CStr::from_ptr(XKeysymToString(*keyboard.keysym)) - .to_str() - .unwrap_or_default() - .to_owned() - .starts_with("dead") - }; + let is_dead = keyboard.is_dead(); if is_dead && string == "" { return; } From 0695d50b9f0317a314942d8e2593152b284a2b38 Mon Sep 17 00:00:00 2001 From: Asura Date: Tue, 2 Aug 2022 11:04:36 -0700 Subject: [PATCH 0140/2015] Fix Shift release failed --- src/ui/remote.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 994cd04b2..dad3e9ae0 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -1081,7 +1081,7 @@ impl Handler { let is_dead = keyboard.last_is_dead; #[cfg(target_os = "linux")] let is_dead = keyboard.is_dead(); - if is_dead && string == "" { + if is_dead && string == "" && down_or_up == true { return; } string From d0702ddfd90ce467fd12fee1a9fceafe50e9a7d5 Mon Sep 17 00:00:00 2001 From: Asura Date: Wed, 3 Aug 2022 14:34:05 +0800 Subject: [PATCH 0141/2015] Add translate mode in UI --- src/ui/header.tis | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/ui/header.tis b/src/ui/header.tis index def2ab9f7..3d8510f80 100644 --- a/src/ui/header.tis +++ b/src/ui/header.tis @@ -151,6 +151,7 @@ class Header: Reactor.Component {
  • {svg_checkmark}{translate('Legacy mode')}
  • {svg_checkmark}{translate('Map mode')}
  • +
  • {svg_checkmark}{translate('Translate mode')}
  • ; } @@ -367,6 +368,8 @@ class Header: Reactor.Component { handler.save_keyboard_mode("legacy"); } else if (me.id == "map") { handler.save_keyboard_mode("map"); + } else if (me.id == "translate") { + handler.save_keyboard_mode("translate"); } toggleMenuState() } From b3b50829f53c4bb33a0cffacc08102224958549e Mon Sep 17 00:00:00 2001 From: Asura Date: Wed, 3 Aug 2022 14:52:08 +0800 Subject: [PATCH 0142/2015] Fix sycn of CapsLock --- src/server/input_service.rs | 5 +++++ src/ui/remote.rs | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/src/server/input_service.rs b/src/server/input_service.rs index e3ac41a59..0154da085 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -822,6 +822,11 @@ fn legacy_keyboard_mode(evt: &KeyEvent) { } fn translate_keyboard_mode(evt: &KeyEvent) { + // Caps affects the keycode map of the peer system(Linux). + let mut en = ENIGO.lock().unwrap(); + if en.get_key_state(Key::CapsLock){ + rdev_key_click(RdevKey::CapsLock); + } let chr = char::from_u32(evt.chr()).unwrap_or_default(); if evt.down { KBD_CONTEXT.lock().unwrap().unicode_char_down(chr).expect("unicode_char_down error"); diff --git a/src/ui/remote.rs b/src/ui/remote.rs index dad3e9ae0..6625c83fa 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -1122,6 +1122,10 @@ impl Handler { if key == RdevKey::AltGr || evt.scan_code == 541 { return; } + // Caps affects the keycode map of the peer system(Linux). + if key == RdevKey::CapsLock { + return; + } dbg!(key); self.map_keyboard_mode(down_or_up, key, None); } From 12129ebf3e5128838c4bc7e2c91429920ca171f9 Mon Sep 17 00:00:00 2001 From: Asura Date: Wed, 3 Aug 2022 15:33:16 +0800 Subject: [PATCH 0143/2015] Update dependencies --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index af872e57d..98623611b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4760,7 +4760,7 @@ checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" [[package]] name = "tfc" version = "0.6.1" -source = "git+https://github.com/asur4s/The-Fat-Controller#6587681075fa312a0d69587721d1ff84d8fa2970" +source = "git+https://github.com/asur4s/The-Fat-Controller#14d49063c8fc9a02c68c0dc842e8d6bb6c5e7713" dependencies = [ "core-graphics 0.22.3", "unicode-segmentation", From d4c735bc3a2674160c801c6a730c91bbbe2ecf82 Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 3 Aug 2022 15:31:19 +0800 Subject: [PATCH 0144/2015] flutter_desktop: fix canvas height - tabBarHeight Signed-off-by: fufesou --- flutter/lib/consts.dart | 1 + .../lib/desktop/pages/connection_page.dart | 2 + .../desktop/pages/connection_tab_page.dart | 76 ++++++++++--------- flutter/lib/desktop/pages/remote_page.dart | 36 +++++---- flutter/lib/desktop/widgets/peer_widget.dart | 28 ++++--- .../lib/desktop/widgets/peercard_widget.dart | 4 - .../lib/desktop/widgets/titlebar_widget.dart | 3 +- flutter/lib/mobile/pages/connection_page.dart | 10 +-- flutter/lib/models/model.dart | 61 +++++++++------ src/lan.rs | 5 +- 10 files changed, 130 insertions(+), 96 deletions(-) create mode 100644 flutter/lib/consts.dart diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart new file mode 100644 index 000000000..8f647837f --- /dev/null +++ b/flutter/lib/consts.dart @@ -0,0 +1 @@ +double kDesktopRemoteTabBarHeight = 48.0; diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 628f962a2..b6a89a48c 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -906,8 +906,10 @@ class _PeerTabbedPageState extends State<_PeerTabbedPage> if (_tabController.indexIsChanging) { switch (_tabController.index) { case 0: + gFFI.bind.mainLoadRecentPeers(); break; case 1: + gFFI.bind.mainLoadFavPeers(); break; case 2: gFFI.bind.mainDiscover(); diff --git a/flutter/lib/desktop/pages/connection_tab_page.dart b/flutter/lib/desktop/pages/connection_tab_page.dart index 69c10ebff..9632fc1f0 100644 --- a/flutter/lib/desktop/pages/connection_tab_page.dart +++ b/flutter/lib/desktop/pages/connection_tab_page.dart @@ -3,9 +3,11 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/desktop/pages/remote_page.dart'; import 'package:flutter_hbb/desktop/widgets/titlebar_widget.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; +import 'package:provider/provider.dart'; import 'package:get/get.dart'; import '../../models/model.dart'; @@ -70,6 +72,42 @@ class _ConnectionTabPageState extends State @override Widget build(BuildContext context) { + final tabBar = TabBar( + isScrollable: true, + labelColor: Colors.white, + physics: NeverScrollableScrollPhysics(), + indicatorColor: Colors.white, + tabs: connectionIds + .map((e) => Tab( + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(e), + SizedBox( + width: 4, + ), + InkWell( + onTap: () { + onRemoveId(e); + }, + child: Icon( + Icons.highlight_remove, + size: 20, + )) + ], + ), + )) + .toList()); + final tabBarView = TabBarView( + children: connectionIds + .map((e) => Container( + child: RemotePage( + key: ValueKey(e), + id: e, + tabBarHeight: kDesktopRemoteTabBarHeight, + ))) //RemotePage(key: ValueKey(e), id: e)) + .toList()); return Scaffold( body: DefaultTabController( initialIndex: initialIndex, @@ -78,43 +116,9 @@ class _ConnectionTabPageState extends State child: Column( children: [ DesktopTitleBar( - child: TabBar( - isScrollable: true, - labelColor: Colors.white, - physics: NeverScrollableScrollPhysics(), - indicatorColor: Colors.white, - tabs: connectionIds - .map((e) => Tab( - child: Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text(e), - SizedBox( - width: 4, - ), - InkWell( - onTap: () { - onRemoveId(e); - }, - child: Icon( - Icons.highlight_remove, - size: 20, - )) - ], - ), - )) - .toList()), + child: Container(height: kDesktopRemoteTabBarHeight, child: tabBar), ), - Expanded( - child: TabBarView( - children: connectionIds - .map((e) => Container( - child: RemotePage( - key: ValueKey(e), - id: e))) //RemotePage(key: ValueKey(e), id: e)) - .toList()), - ) + Expanded(child: tabBarView), ], ), ), diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 3e4810c0e..ef5fb1c0b 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -16,6 +16,7 @@ import 'package:wakelock/wakelock.dart'; // import 'package:window_manager/window_manager.dart'; import '../../common.dart'; +import '../../consts.dart'; import '../../mobile/widgets/dialog.dart'; import '../../mobile/widgets/overlay.dart'; import '../../models/model.dart'; @@ -23,9 +24,11 @@ import '../../models/model.dart'; final initText = '\1' * 1024; class RemotePage extends StatefulWidget { - RemotePage({Key? key, required this.id}) : super(key: key); + RemotePage({Key? key, required this.id, required this.tabBarHeight}) + : super(key: key); final String id; + final double tabBarHeight; @override _RemotePageState createState() => _RemotePageState(); @@ -53,10 +56,12 @@ class _RemotePageState extends State @override void initState() { super.initState(); - final ffi = Get.put(FFI(), tag: widget.id); + var ffitmp = FFI(); + ffitmp.canvasModel.tabBarHeight = super.widget.tabBarHeight; + final ffi = Get.put(ffitmp, tag: widget.id); // note: a little trick ffi.ffiModel.platformFFI = gFFI.ffiModel.platformFFI; - ffi.connect(widget.id); + ffi.connect(widget.id, tabBarHeight: super.widget.tabBarHeight); WidgetsBinding.instance.addPostFrameCallback((_) { SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); showLoading(translate('Connecting...')); @@ -236,11 +241,12 @@ class _RemotePageState extends State @override Widget build(BuildContext context) { super.build(context); + Provider.of(context, listen: false).tabBarHeight = + super.widget.tabBarHeight; final pi = Provider.of(context).pi; final hideKeyboard = isKeyboardShown() && _showEdit; final showActionButton = !_showBar || hideKeyboard; final keyboard = _ffi.ffiModel.permissions['keyboard'] != false; - return WillPopScope( onWillPop: () async { clientClose(); @@ -296,7 +302,6 @@ class _RemotePageState extends State Widget getRawPointerAndKeyBody(bool keyboard, Widget child) { return Listener( onPointerHover: (e) { - debugPrint("onPointerHover ${e}"); if (e.kind != ui.PointerDeviceKind.mouse) return; if (!_isPhysicalMouse) { setState(() { @@ -304,11 +309,11 @@ class _RemotePageState extends State }); } if (_isPhysicalMouse) { - _ffi.handleMouse(getEvent(e, 'mousemove')); + _ffi.handleMouse(getEvent(e, 'mousemove'), + tabBarHeight: super.widget.tabBarHeight); } }, onPointerDown: (e) { - debugPrint("onPointerDown ${e}"); if (e.kind != ui.PointerDeviceKind.mouse) { if (_isPhysicalMouse) { setState(() { @@ -317,25 +322,25 @@ class _RemotePageState extends State } } if (_isPhysicalMouse) { - _ffi.handleMouse(getEvent(e, 'mousedown')); + _ffi.handleMouse(getEvent(e, 'mousedown'), + tabBarHeight: super.widget.tabBarHeight); } }, onPointerUp: (e) { - debugPrint("onPointerUp ${e}"); if (e.kind != ui.PointerDeviceKind.mouse) return; if (_isPhysicalMouse) { - _ffi.handleMouse(getEvent(e, 'mouseup')); + _ffi.handleMouse(getEvent(e, 'mouseup'), + tabBarHeight: super.widget.tabBarHeight); } }, onPointerMove: (e) { - debugPrint("onPointerMove ${e}"); if (e.kind != ui.PointerDeviceKind.mouse) return; if (_isPhysicalMouse) { - _ffi.handleMouse(getEvent(e, 'mousemove')); + _ffi.handleMouse(getEvent(e, 'mousemove'), + tabBarHeight: super.widget.tabBarHeight); } }, onPointerSignal: (e) { - debugPrint("onPointerSignal ${e}"); if (e is PointerScrollEvent) { var dx = e.scrollDelta.dx; var dy = e.scrollDelta.dy; @@ -557,7 +562,7 @@ class _RemotePageState extends State void showActions(String id) async { final size = MediaQuery.of(context).size; final x = 120.0; - final y = size.height; + final y = size.height - super.widget.tabBarHeight; final more = >[]; final pi = _ffi.ffiModel.pi; final perms = _ffi.ffiModel.permissions; @@ -672,7 +677,6 @@ class _RemotePageState extends State if (!keyboard) { return SizedBox(); } - final size = MediaQuery.of(context).size; var wrap = (String text, void Function() onPressed, [bool? active, IconData? icon]) { return TextButton( @@ -788,7 +792,7 @@ class _RemotePageState extends State sendPrompt(widget.id, isMac, 'VK_S'); }), ]; - final space = size.width > 320 ? 4.0 : 2.0; + final space = MediaQuery.of(context).size.width > 320 ? 4.0 : 2.0; return Container( color: Color(0xAA000000), padding: EdgeInsets.only( diff --git a/flutter/lib/desktop/widgets/peer_widget.dart b/flutter/lib/desktop/widgets/peer_widget.dart index 45e2953eb..4705516f5 100644 --- a/flutter/lib/desktop/widgets/peer_widget.dart +++ b/flutter/lib/desktop/widgets/peer_widget.dart @@ -64,6 +64,11 @@ class _PeerWidgetState extends State<_PeerWidget> with WindowListener { _queryCoun = 0; } + @override + void onWindowMinimize() { + _queryCoun = _maxQueryCount; + } + @override Widget build(BuildContext context) { final space = 8.0; @@ -110,19 +115,23 @@ class _PeerWidgetState extends State<_PeerWidget> with WindowListener { final now = DateTime.now(); if (!setEquals(_curPeers, _lastQueryPeers)) { if (now.difference(_lastChangeTime) > Duration(seconds: 1)) { - gFFI.ffiModel.platformFFI.ffiBind - .queryOnlines(ids: _curPeers.toList(growable: false)); - _lastQueryPeers = {..._curPeers}; - _lastQueryTime = DateTime.now(); - _queryCoun = 0; + if (_curPeers.length > 0) { + gFFI.ffiModel.platformFFI.ffiBind + .queryOnlines(ids: _curPeers.toList(growable: false)); + _lastQueryPeers = {..._curPeers}; + _lastQueryTime = DateTime.now(); + _queryCoun = 0; + } } } else { if (_queryCoun < _maxQueryCount) { if (now.difference(_lastQueryTime) > Duration(seconds: 20)) { - gFFI.ffiModel.platformFFI.ffiBind - .queryOnlines(ids: _curPeers.toList(growable: false)); - _lastQueryTime = DateTime.now(); - _queryCoun += 1; + if (_curPeers.length > 0) { + gFFI.ffiModel.platformFFI.ffiBind + .queryOnlines(ids: _curPeers.toList(growable: false)); + _lastQueryTime = DateTime.now(); + _queryCoun += 1; + } } } } @@ -193,7 +202,6 @@ class DiscoveredPeerWidget extends BasePeerWidget { @override Widget build(BuildContext context) { - debugPrint("DiscoveredPeerWidget build"); final widget = super.build(context); gFFI.bind.mainLoadLanPeers(); return widget; diff --git a/flutter/lib/desktop/widgets/peercard_widget.dart b/flutter/lib/desktop/widgets/peercard_widget.dart index 39acd0bf2..3a4dbfada 100644 --- a/flutter/lib/desktop/widgets/peercard_widget.dart +++ b/flutter/lib/desktop/widgets/peercard_widget.dart @@ -405,7 +405,6 @@ class RecentPeerCard extends BasePeerCard { : super(peer: peer, key: key, type: PeerType.recent); Future>> _getPopupMenuItems() async { - debugPrint("call RecentPeerCard _getPopupMenuItems"); return [ PopupMenuItem( child: Text(translate('Connect')), value: 'connect'), @@ -427,7 +426,6 @@ class FavoritePeerCard extends BasePeerCard { : super(peer: peer, key: key, type: PeerType.fav); Future>> _getPopupMenuItems() async { - debugPrint("call FavoritePeerCard _getPopupMenuItems"); return [ PopupMenuItem( child: Text(translate('Connect')), value: 'connect'), @@ -451,7 +449,6 @@ class DiscoveredPeerCard extends BasePeerCard { : super(peer: peer, key: key, type: PeerType.discovered); Future>> _getPopupMenuItems() async { - debugPrint("call DiscoveredPeerCard _getPopupMenuItems"); return [ PopupMenuItem( child: Text(translate('Connect')), value: 'connect'), @@ -473,7 +470,6 @@ class AddressBookPeerCard extends BasePeerCard { : super(peer: peer, key: key, type: PeerType.ab); Future>> _getPopupMenuItems() async { - debugPrint("call AddressBookPeerCard _getPopupMenuItems"); return [ PopupMenuItem( child: Text(translate('Connect')), value: 'connect'), diff --git a/flutter/lib/desktop/widgets/titlebar_widget.dart b/flutter/lib/desktop/widgets/titlebar_widget.dart index 6e9b0bf6e..475b4cb86 100644 --- a/flutter/lib/desktop/widgets/titlebar_widget.dart +++ b/flutter/lib/desktop/widgets/titlebar_widget.dart @@ -22,7 +22,8 @@ class DesktopTitleBar extends StatelessWidget { child: Row( children: [ Expanded( - child: child ?? Offstage(),) + child: child ?? Offstage(), + ) // const WindowButtons() ], ), diff --git a/flutter/lib/mobile/pages/connection_page.dart b/flutter/lib/mobile/pages/connection_page.dart index 9ae0f3766..9722b1a47 100644 --- a/flutter/lib/mobile/pages/connection_page.dart +++ b/flutter/lib/mobile/pages/connection_page.dart @@ -213,13 +213,13 @@ class _ConnectionPageState extends State { /// Get all the saved peers. Widget getPeers() { - final size = MediaQuery.of(context).size; + final windowWidth = MediaQuery.of(context).size.width; final space = 8.0; - var width = size.width - 2 * space; + var width = windowWidth - 2 * space; final minWidth = 320.0; - if (size.width > minWidth + 2 * space) { - final n = (size.width / (minWidth + 2 * space)).floor(); - width = size.width / n - 2 * space; + if (windowWidth > minWidth + 2 * space) { + final n = (windowWidth / (minWidth + 2 * space)).floor(); + width = windowWidth / n - 2 * space; } final cards = []; var peers = gFFI.peers(); diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index e9367bd4c..11415eeef 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -349,7 +349,7 @@ class ImageModel with ChangeNotifier { ImageModel(this.parent); - void onRgba(Uint8List rgba) { + void onRgba(Uint8List rgba, double tabBarHeight) { if (_waitForImage) { _waitForImage = false; SmartDialog.dismiss(); @@ -363,22 +363,24 @@ class ImageModel with ChangeNotifier { if (parent.target?.id != pid) return; try { // my throw exception, because the listener maybe already dispose - update(image); + update(image, tabBarHeight); } catch (e) { print('update image: $e'); } }); } - void update(ui.Image? image) { + void update(ui.Image? image, double tabBarHeight) { if (_image == null && image != null) { if (isWebDesktop) { parent.target?.canvasModel.updateViewStyle(); } else { final size = MediaQueryData.fromWindow(ui.window).size; - final xscale = size.width / image.width; - final yscale = size.height / image.height; - parent.target?.canvasModel.scale = max(xscale, yscale); + final canvasWidth = size.width; + final canvasHeight = size.height - tabBarHeight; + final xscale = canvasWidth / image.width; + final yscale = canvasHeight / image.height; + parent.target?.canvasModel.scale = min(xscale, yscale); } if (parent.target != null) { initializeCursorAndCanvas(parent.target!); @@ -395,6 +397,8 @@ class ImageModel with ChangeNotifier { if (image != null) notifyListeners(); } + // mobile only + // for desktop, height should minus tabbar height double get maxScale { if (_image == null) return 1.5; final size = MediaQueryData.fromWindow(ui.window).size; @@ -403,6 +407,8 @@ class ImageModel with ChangeNotifier { return max(1.5, max(xscale, yscale)); } + // mobile only + // for desktop, height should minus tabbar height double get minScale { if (_image == null) return 1.5; final size = MediaQueryData.fromWindow(ui.window).size; @@ -416,6 +422,7 @@ class CanvasModel with ChangeNotifier { double _x = 0; double _y = 0; double _scale = 1.0; + double _tabBarHeight = 0.0; String id = ""; // TODO multi canvas model WeakReference parent; @@ -428,6 +435,9 @@ class CanvasModel with ChangeNotifier { double get scale => _scale; + set tabBarHeight(double h) => _tabBarHeight = h; + double get tabBarHeight => _tabBarHeight; + void updateViewStyle() async { final s = await parent.target?.bind.getSessionOption(id: id, arg: 'view-style'); @@ -435,8 +445,10 @@ class CanvasModel with ChangeNotifier { return; } final size = MediaQueryData.fromWindow(ui.window).size; - final s1 = size.width / (parent.target?.ffiModel.display.width ?? 720); - final s2 = size.height / (parent.target?.ffiModel.display.height ?? 1280); + final canvasWidth = size.width; + final canvasHeight = size.height - _tabBarHeight; + final s1 = canvasWidth / (parent.target?.ffiModel.display.width ?? 720); + final s2 = canvasHeight / (parent.target?.ffiModel.display.height ?? 1280); // Closure to perform shrink operation. final shrinkOp = () { final s = s1 < s2 ? s1 : s2; @@ -467,8 +479,8 @@ class CanvasModel with ChangeNotifier { defaultOp(); } } - _x = (size.width - getDisplayWidth() * _scale) / 2; - _y = (size.height - getDisplayHeight() * _scale) / 2; + _x = (canvasWidth - getDisplayWidth() * _scale) / 2; + _y = (canvasHeight - getDisplayHeight() * _scale) / 2; notifyListeners(); } @@ -491,15 +503,17 @@ class CanvasModel with ChangeNotifier { // On mobile platforms, move the canvas with the cursor. if (!isDesktop) { final size = MediaQueryData.fromWindow(ui.window).size; + final canvasWidth = size.width; + final canvasHeight = size.height - _tabBarHeight; final dw = getDisplayWidth() * _scale; final dh = getDisplayHeight() * _scale; var dxOffset = 0; var dyOffset = 0; - if (dw > size.width) { - dxOffset = (x - dw * (x / size.width) - _x).toInt(); + if (dw > canvasWidth) { + dxOffset = (x - dw * (x / canvasWidth) - _x).toInt(); } - if (dh > size.height) { - dyOffset = (y - dh * (y / size.height) - _y).toInt(); + if (dh > canvasHeight) { + dyOffset = (y - dh * (y / canvasHeight) - _y).toInt(); } _x += dxOffset; _y += dyOffset; @@ -524,8 +538,11 @@ class CanvasModel with ChangeNotifier { if (isWebDesktop) { updateViewStyle(); } else { - _x = 0; - _y = 0; + final size = MediaQueryData.fromWindow(ui.window).size; + final canvasWidth = size.width; + final canvasHeight = size.height - _tabBarHeight; + _x = (canvasWidth - getDisplayWidth() * _scale) / 2; + _y = (canvasHeight - getDisplayHeight() * _scale) / 2; } notifyListeners(); } @@ -933,7 +950,8 @@ class FFI { } /// Connect with the given [id]. Only transfer file if [isFileTransfer]. - void connect(String id, {bool isFileTransfer = false}) { + void connect(String id, + {bool isFileTransfer = false, double tabBarHeight = 0.0}) { if (!isFileTransfer) { chatModel.resetClientMode(); canvasModel.id = id; @@ -954,7 +972,7 @@ class FFI { print('json.decode fail(): $e'); } } else if (message is Rgba) { - imageModel.onRgba(message.field0); + imageModel.onRgba(message.field0, tabBarHeight); } } }(); @@ -979,7 +997,7 @@ class FFI { } bind.sessionClose(id: id); id = ""; - imageModel.update(null); + imageModel.update(null, 0.0); cursorModel.clear(); ffiModel.clear(); canvasModel.clear(); @@ -1027,8 +1045,7 @@ class FFI { RustdeskImpl get bind => ffiModel.platformFFI.ffiBind; - handleMouse(Map evt) { - debugPrint("mouse ${evt.toString()}"); + handleMouse(Map evt, {double tabBarHeight = 0.0}) { var type = ''; var isMove = false; switch (evt['type']) { @@ -1046,7 +1063,7 @@ class FFI { } evt['type'] = type; var x = evt['x']; - var y = max(0.0, (evt['y'] as double) - 50.0); + var y = max(0.0, (evt['y'] as double) - tabBarHeight); if (isMove) { canvasModel.moveDesktopMouse(x, y); } diff --git a/src/lan.rs b/src/lan.rs index f74492b8e..30af1de6b 100644 --- a/src/lan.rs +++ b/src/lan.rs @@ -276,10 +276,9 @@ async fn handle_received_peers(mut rx: UnboundedReceiver) peers.insert(0, peer); if last_write_time.elapsed().as_millis() > 300 { config::LanPeers::store(&peers); - last_write_time = Instant::now(); - #[cfg(feature = "flutter")] crate::flutter_ffi::main_load_lan_peers(); + last_write_time = Instant::now(); } } None => { @@ -290,5 +289,7 @@ async fn handle_received_peers(mut rx: UnboundedReceiver) } config::LanPeers::store(&peers); + #[cfg(feature = "flutter")] + crate::flutter_ffi::main_load_lan_peers(); Ok(()) } From 07debe836329a0baf5b9b54530f0fab18e661c29 Mon Sep 17 00:00:00 2001 From: csf Date: Wed, 3 Aug 2022 21:51:35 +0800 Subject: [PATCH 0145/2015] fix android build --- src/client.rs | 2 +- src/common.rs | 2 +- src/flutter_ffi.rs | 28 +++++++++++++++++------- src/server/connection.rs | 4 ++-- src/ui_interface.rs | 47 +++++++++++++++++++++++++++------------- 5 files changed, 56 insertions(+), 27 deletions(-) diff --git a/src/client.rs b/src/client.rs index 478d81ce8..a05826c36 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1267,7 +1267,7 @@ impl LoginConfigHandler { /// Create a [`Message`] for login. fn create_login_msg(&self, password: Vec) -> Message { #[cfg(any(target_os = "android", target_os = "ios"))] - let my_id = Config::get_id_or(crate::common::MOBILE_INFO1.lock().unwrap().clone()); + let my_id = Config::get_id_or(crate::common::FLUTTER_INFO1.lock().unwrap().clone()); #[cfg(not(any(target_os = "android", target_os = "ios")))] let my_id = Config::get_id(); let mut lr = LoginRequest { diff --git a/src/common.rs b/src/common.rs index d2d1922ec..5af811c05 100644 --- a/src/common.rs +++ b/src/common.rs @@ -441,7 +441,7 @@ pub fn username() -> String { #[cfg(not(any(target_os = "android", target_os = "ios")))] return whoami::username().trim_end_matches('\0').to_owned(); #[cfg(any(target_os = "android", target_os = "ios"))] - return MOBILE_INFO2.lock().unwrap().clone(); + return FLUTTER_INFO2.lock().unwrap().clone(); } #[inline] diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 413e3cde3..84cc20e0a 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -19,12 +19,14 @@ use crate::flutter::connection_manager::{self, get_clients_length, get_clients_s use crate::flutter::{self, Session, SESSIONS}; use crate::start_server; use crate::ui_interface; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use crate::ui_interface::{change_id, check_connect_status, is_ok_change_id}; use crate::ui_interface::{ - change_id, check_connect_status, discover, forget_password, get_api_server, get_app_name, - get_async_job_status, get_connect_status, get_fav, get_id, get_lan_peers, get_license, - get_local_option, get_options, get_peer, get_peer_option, get_socks, get_sound_inputs, - get_uuid, get_version, has_rendezvous_service, is_ok_change_id, post_request, set_local_option, - set_options, set_peer_option, set_socks, store_fav, test_if_valid_server, using_public_server, + discover, forget_password, get_api_server, get_app_name, get_async_job_status, + get_connect_status, get_fav, get_id, get_lan_peers, get_license, get_local_option, get_options, + get_peer, get_peer_option, get_socks, get_sound_inputs, get_uuid, get_version, + has_rendezvous_service, post_request, set_local_option, set_options, set_peer_option, + set_socks, store_fav, test_if_valid_server, using_public_server, }; fn initialize(app_dir: &str) { @@ -67,7 +69,10 @@ fn initialize(app_dir: &str) { /// Return true if the app should continue running with UI(possibly Flutter), false if the app should exit. #[no_mangle] pub extern "C" fn rustdesk_core_main() -> bool { - crate::core_main::core_main() + #[cfg(not(any(target_os = "android", target_os = "ios")))] + return crate::core_main::core_main(); + #[cfg(any(target_os = "android", target_os = "ios"))] + false } pub enum EventToUI { @@ -390,6 +395,7 @@ pub fn main_get_sound_inputs() -> Vec { } pub fn main_change_id(new_id: String) { + #[cfg(not(any(target_os = "android", target_os = "ios")))] change_id(new_id) } @@ -461,6 +467,7 @@ pub fn main_get_connect_status() -> String { } pub fn main_check_connect_status() { + #[cfg(not(any(target_os = "android", target_os = "ios")))] check_connect_status(true); } @@ -1042,8 +1049,13 @@ unsafe extern "C" fn set_by_name(name: *const c_char, value: *const c_char) { crate::rendezvous_mediator::RendezvousMediator::restart(); } "start_service" => { - Config::set_option("stop-service".into(), "".into()); - start_server(false); + #[cfg(target_os = "android")] + { + Config::set_option("stop-service".into(), "".into()); + crate::rendezvous_mediator::RendezvousMediator::restart(); + } + #[cfg(not(target_os = "android"))] + std::thread::spawn(move || start_server(true)); } #[cfg(target_os = "android")] "close_conn" => { diff --git a/src/server/connection.rs b/src/server/connection.rs index fc38ec77f..383c5782b 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -5,7 +5,7 @@ use crate::clipboard_file::*; use crate::common::update_clipboard; use crate::video_service; #[cfg(any(target_os = "android", target_os = "ios"))] -use crate::{common::MOBILE_INFO2, flutter::connection_manager::start_channel}; +use crate::{common::FLUTTER_INFO2, flutter::connection_manager::start_channel}; use crate::{ipc, VERSION}; use hbb_common::{ config::Config, @@ -643,7 +643,7 @@ impl Connection { } #[cfg(target_os = "android")] { - pi.hostname = MOBILE_INFO2.lock().unwrap().clone(); + pi.hostname = FLUTTER_INFO2.lock().unwrap().clone(); pi.platform = "Android".into(); } #[cfg(feature = "hwcodec")] diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 7e08f9855..2aa4f36ec 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -32,10 +32,14 @@ lazy_static::lazy_static! { pub static ref UI_STATUS : Arc> = Arc::new(Mutex::new((0, false, 0, "".to_owned()))); pub static ref OPTIONS : Arc>> = Arc::new(Mutex::new(Config::get_options())); pub static ref ASYNC_JOB_STATUS : Arc> = Default::default(); - pub static ref SENDER : Mutex> = Mutex::new(check_connect_status(true)); pub static ref TEMPORARY_PASSWD : Arc> = Arc::new(Mutex::new("".to_owned())); } +#[cfg(not(any(target_os = "android", target_os = "ios")))] +lazy_static::lazy_static! { + pub static ref SENDER : Mutex> = Mutex::new(check_connect_status(true)); +} + pub fn recent_sessions_updated() -> bool { let mut childs = CHILDS.lock().unwrap(); if childs.0 { @@ -47,7 +51,10 @@ pub fn recent_sessions_updated() -> bool { } pub fn get_id() -> String { - ipc::get_id() + #[cfg(any(target_os = "android", target_os = "ios"))] + return Config::get_id(); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + return ipc::get_id(); } pub fn get_remote_id() -> String { @@ -132,7 +139,7 @@ pub fn get_license() -> String { pub fn get_option(key: String) -> String { #[cfg(any(target_os = "android", target_os = "ios"))] - return Config::get_option(arg); + return Config::get_option(&key); #[cfg(not(any(target_os = "android", target_os = "ios")))] return get_option_(&key); } @@ -243,13 +250,16 @@ pub fn get_sound_inputs() -> Vec { } pub fn set_options(m: HashMap) { - *OPTIONS.lock().unwrap() = m.clone(); - ipc::set_options(m).ok(); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + *OPTIONS.lock().unwrap() = m.clone(); + ipc::set_options(m).ok(); + } } pub fn set_option(key: String, value: String) { #[cfg(any(target_os = "android", target_os = "ios"))] - Config::set_option(name.to_owned(), value.to_owned()); + Config::set_option(key, value); #[cfg(not(any(target_os = "android", target_os = "ios")))] { let mut options = OPTIONS.lock().unwrap(); @@ -277,20 +287,26 @@ pub fn install_path() -> String { } pub fn get_socks() -> Vec { - let s = ipc::get_socks(); - match s { - None => Vec::new(), - Some(s) => { - let mut v = Vec::new(); - v.push(s.proxy); - v.push(s.username); - v.push(s.password); - v + #[cfg(any(target_os = "android", target_os = "ios"))] + return Vec::new(); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + let s = ipc::get_socks(); + match s { + None => Vec::new(), + Some(s) => { + let mut v = Vec::new(); + v.push(s.proxy); + v.push(s.username); + v.push(s.password); + v + } } } } pub fn set_socks(proxy: String, username: String, password: String) { + #[cfg(not(any(target_os = "android", target_os = "ios")))] ipc::set_socks(config::Socks5Server { proxy, username, @@ -357,6 +373,7 @@ pub fn get_mouse_time() -> f64 { return res; } +#[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn check_mouse_time() { let sender = SENDER.lock().unwrap(); allow_err!(sender.send(ipc::Data::MouseMoveTime(0))); From 7a2de5d280005410c71fcf9445bd0bd21dfcf9ff Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 3 Aug 2022 22:03:31 +0800 Subject: [PATCH 0146/2015] flutter_desktop: fix global envet stream shading && refactor platform ffi Signed-off-by: fufesou --- flutter/lib/common.dart | 30 +-- flutter/lib/consts.dart | 3 + .../lib/desktop/pages/connection_page.dart | 13 +- .../lib/desktop/pages/desktop_home_page.dart | 35 ++- .../lib/desktop/pages/file_manager_page.dart | 43 ++-- flutter/lib/desktop/pages/remote_page.dart | 68 +++--- flutter/lib/desktop/widgets/peer_widget.dart | 12 +- .../lib/desktop/widgets/peercard_widget.dart | 39 ++-- flutter/lib/main.dart | 78 ++++--- flutter/lib/models/ab_model.dart | 3 +- flutter/lib/models/file_model.dart | 178 ++++++++++----- flutter/lib/models/model.dart | 51 ++--- flutter/lib/models/native_model.dart | 44 ++-- flutter/lib/models/peer_model.dart | 11 +- flutter/lib/models/platform_model.dart | 7 + flutter/lib/models/user_model.dart | 12 +- flutter/lib/models/web_model.dart | 9 +- flutter/linux/CMakeLists.txt | 4 +- src/flutter.rs | 216 +++++++++--------- src/flutter_ffi.rs | 18 +- 20 files changed, 476 insertions(+), 398 deletions(-) create mode 100644 flutter/lib/models/platform_model.dart diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index eda1ed4e7..3ced905fc 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; @@ -7,15 +8,16 @@ import 'package:get/instance_manager.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'models/model.dart'; +import 'models/platform_model.dart'; final globalKey = GlobalKey(); final navigationBarKey = GlobalKey(); -var isAndroid = false; -var isIOS = false; +var isAndroid = Platform.isAndroid; +var isIOS = Platform.isIOS; var isWeb = false; var isWebDesktop = false; -var isDesktop = false; +var isDesktop = Platform.isWindows || Platform.isMacOS || Platform.isLinux; var version = ""; int androidVersion = 0; @@ -119,9 +121,9 @@ class DialogManager { static Future show(DialogBuilder builder, {bool clickMaskDismiss = false, - bool backDismiss = false, - String? tag, - bool useAnimation = true}) async { + bool backDismiss = false, + String? tag, + bool useAnimation = true}) async { final t; if (tag != null) { t = tag; @@ -146,10 +148,11 @@ class DialogManager { } class CustomAlertDialog extends StatelessWidget { - CustomAlertDialog({required this.title, - required this.content, - required this.actions, - this.contentPadding}); + CustomAlertDialog( + {required this.title, + required this.content, + required this.actions, + this.contentPadding}); final Widget title; final Widget content; @@ -162,7 +165,7 @@ class CustomAlertDialog extends StatelessWidget { scrollable: true, title: title, contentPadding: - EdgeInsets.symmetric(horizontal: contentPadding ?? 25, vertical: 10), + EdgeInsets.symmetric(horizontal: contentPadding ?? 25, vertical: 10), content: content, actions: actions, ); @@ -364,9 +367,8 @@ Future initGlobalFFI() async { _globalFFI = FFI(); // after `put`, can also be globally found by Get.find(); Get.put(_globalFFI, permanent: true); - await _globalFFI.ffiModel.init(); // trigger connection status updater - await _globalFFI.bind.mainCheckConnectStatus(); + await bind.mainCheckConnectStatus(); // global shared preference await Get.putAsync(() => SharedPreferences.getInstance()); -} \ No newline at end of file +} diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 8f647837f..eea49cf86 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -1 +1,4 @@ double kDesktopRemoteTabBarHeight = 48.0; +String kAppTypeMain = "main"; +String kAppTypeDesktopRemote = "remote"; +String kAppTypeDesktopFileTransfer = "file transfer"; diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index b6a89a48c..e32275373 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -16,6 +16,7 @@ import '../../mobile/pages/home_page.dart'; import '../../mobile/pages/scan_page.dart'; import '../../mobile/pages/settings_page.dart'; import '../../models/model.dart'; +import '../../models/platform_model.dart'; // enum RemoteType { recently, favorite, discovered, addressBook } @@ -428,10 +429,10 @@ class _ConnectionPageState extends State { updateStatus() async { svcStopped.value = gFFI.getOption("stop-service") == "Y"; - final status = jsonDecode(await gFFI.bind.mainGetConnectStatus()) - as Map; + final status = + jsonDecode(await bind.mainGetConnectStatus()) as Map; svcStatusCode.value = status["status_num"]; - svcIsUsingPublicServer.value = await gFFI.bind.mainIsUsingPublicServer(); + svcIsUsingPublicServer.value = await bind.mainIsUsingPublicServer(); } handleLogin() { @@ -906,13 +907,13 @@ class _PeerTabbedPageState extends State<_PeerTabbedPage> if (_tabController.indexIsChanging) { switch (_tabController.index) { case 0: - gFFI.bind.mainLoadRecentPeers(); + bind.mainLoadRecentPeers(); break; case 1: - gFFI.bind.mainLoadFavPeers(); + bind.mainLoadFavPeers(); break; case 2: - gFFI.bind.mainDiscover(); + bind.mainDiscover(); break; case 3: break; diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 54a52b774..0a86350d8 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -8,6 +8,7 @@ import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/desktop/pages/connection_page.dart'; import 'package:flutter_hbb/desktop/widgets/titlebar_widget.dart'; import 'package:flutter_hbb/models/model.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/server_model.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; @@ -630,13 +631,13 @@ class _DesktopHomePageState extends State with TrayListener { setState(() { msg = ""; isInProgress = true; - gFFI.bind.mainChangeId(newId: newId); + bind.mainChangeId(newId: newId); }); - var status = await gFFI.bind.mainGetAsyncStatus(); + var status = await bind.mainGetAsyncStatus(); while (status == " ") { await Future.delayed(Duration(milliseconds: 100)); - status = await gFFI.bind.mainGetAsyncStatus(); + status = await bind.mainGetAsyncStatus(); } if (status.isEmpty) { // ok @@ -655,8 +656,7 @@ class _DesktopHomePageState extends State with TrayListener { } void changeServer() async { - Map oldOptions = - jsonDecode(await gFFI.bind.mainGetOptions()); + Map oldOptions = jsonDecode(await bind.mainGetOptions()); print("${oldOptions}"); String idServer = oldOptions['custom-rendezvous-server'] ?? ""; var idServerMsg = ""; @@ -814,7 +814,7 @@ class _DesktopHomePageState extends State with TrayListener { if (idServer.isNotEmpty) { idServerMsg = translate( - await gFFI.bind.mainTestIfValidServer(server: idServer)); + await bind.mainTestIfValidServer(server: idServer)); if (idServerMsg.isEmpty) { oldOptions['custom-rendezvous-server'] = idServer; } else { @@ -826,8 +826,8 @@ class _DesktopHomePageState extends State with TrayListener { } if (relayServer.isNotEmpty) { - relayServerMsg = translate(await gFFI.bind - .mainTestIfValidServer(server: relayServer)); + relayServerMsg = translate( + await bind.mainTestIfValidServer(server: relayServer)); if (relayServerMsg.isEmpty) { oldOptions['relay-server'] = relayServer; } else { @@ -853,7 +853,7 @@ class _DesktopHomePageState extends State with TrayListener { } // ok oldOptions['key'] = key; - await gFFI.bind.mainSetOptions(json: jsonEncode(oldOptions)); + await bind.mainSetOptions(json: jsonEncode(oldOptions)); close(); }, child: Text(translate("OK"))), @@ -863,8 +863,7 @@ class _DesktopHomePageState extends State with TrayListener { } void changeWhiteList() async { - Map oldOptions = - jsonDecode(await gFFI.bind.mainGetOptions()); + Map oldOptions = jsonDecode(await bind.mainGetOptions()); var newWhiteList = ((oldOptions['whitelist'] ?? "") as String).split(','); var newWhiteListField = newWhiteList.join('\n'); var msg = ""; @@ -935,7 +934,7 @@ class _DesktopHomePageState extends State with TrayListener { newWhiteList = ips.join(','); } oldOptions['whitelist'] = newWhiteList; - await gFFI.bind.mainSetOptions(json: jsonEncode(oldOptions)); + await bind.mainSetOptions(json: jsonEncode(oldOptions)); close(); }, child: Text(translate("OK"))), @@ -945,7 +944,7 @@ class _DesktopHomePageState extends State with TrayListener { } void changeSocks5Proxy() async { - var socks = await gFFI.bind.mainGetSocks(); + var socks = await bind.mainGetSocks(); String proxy = ""; String proxyMsg = ""; @@ -1072,7 +1071,7 @@ class _DesktopHomePageState extends State with TrayListener { if (proxy.isNotEmpty) { proxyMsg = translate( - await gFFI.bind.mainTestIfValidServer(server: proxy)); + await bind.mainTestIfValidServer(server: proxy)); if (proxyMsg.isEmpty) { // ignore } else { @@ -1080,7 +1079,7 @@ class _DesktopHomePageState extends State with TrayListener { return; } } - await gFFI.bind.mainSetSocks( + await bind.mainSetSocks( proxy: proxy, username: username, password: password); close(); }, @@ -1091,9 +1090,9 @@ class _DesktopHomePageState extends State with TrayListener { } void about() async { - final appName = await gFFI.bind.mainGetAppName(); - final license = await gFFI.bind.mainGetLicense(); - final version = await gFFI.bind.mainGetVersion(); + final appName = await bind.mainGetAppName(); + final license = await bind.mainGetLicense(); + final version = await bind.mainGetVersion(); final linkStyle = TextStyle(decoration: TextDecoration.underline); DialogManager.show((setState, close) { return CustomAlertDialog( diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index e37f56404..581a38a3a 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -11,6 +11,7 @@ import 'package:wakelock/wakelock.dart'; import '../../common.dart'; import '../../models/model.dart'; +import '../../models/platform_model.dart'; class FileManagerPage extends StatefulWidget { FileManagerPage({Key? key, required this.id}) : super(key: key); @@ -37,7 +38,7 @@ class _FileManagerPageState extends State @override void initState() { super.initState(); - Get.put(FFI.newFFI()..connect(widget.id, isFileTransfer: true), + Get.put(FFI()..connect(widget.id, isFileTransfer: true), tag: 'ft_${widget.id}'); // _ffi.ffiModel.updateEventListener(widget.id); if (!Platform.isLinux) { @@ -464,13 +465,15 @@ class _FileManagerPageState extends State decoration: BoxDecoration(color: Colors.blue), padding: EdgeInsets.all(8.0), child: FutureBuilder( - future: _ffi.bind.sessionGetPlatform( + future: bind.sessionGetPlatform( id: _ffi.id, isRemote: !isLocal), builder: (context, snapshot) { if (snapshot.hasData && snapshot.data!.isNotEmpty) { return getPlatformImage('${snapshot.data}'); } else { - return CircularProgressIndicator(color: Colors.white,); + return CircularProgressIndicator( + color: Colors.white, + ); } })), Text(isLocal @@ -505,21 +508,25 @@ class _FileManagerPageState extends State border: Border.all(color: Colors.black12)), child: TextField( decoration: InputDecoration( - border: InputBorder.none, - isDense: true, - prefix: Padding(padding: EdgeInsets.only(left: 4.0)), - suffix: DropdownButton( - isDense: true, - underline: Offstage(), - items: [ - // TODO: favourite - DropdownMenuItem(child: Text('/'), value: '/',) - ], onChanged: (path) { - if (path is String && path.isNotEmpty){ - model.openDirectory(path, isLocal: isLocal); - } - }) - ), + border: InputBorder.none, + isDense: true, + prefix: + Padding(padding: EdgeInsets.only(left: 4.0)), + suffix: DropdownButton( + isDense: true, + underline: Offstage(), + items: [ + // TODO: favourite + DropdownMenuItem( + child: Text('/'), + value: '/', + ) + ], + onChanged: (path) { + if (path is String && path.isNotEmpty) { + model.openDirectory(path, isLocal: isLocal); + } + })), controller: TextEditingController( text: isLocal ? model.currentLocalDir.path diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index ef5fb1c0b..0a1979540 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -16,10 +16,10 @@ import 'package:wakelock/wakelock.dart'; // import 'package:window_manager/window_manager.dart'; import '../../common.dart'; -import '../../consts.dart'; import '../../mobile/widgets/dialog.dart'; import '../../mobile/widgets/overlay.dart'; import '../../models/model.dart'; +import '../../models/platform_model.dart'; final initText = '\1' * 1024; @@ -59,8 +59,6 @@ class _RemotePageState extends State var ffitmp = FFI(); ffitmp.canvasModel.tabBarHeight = super.widget.tabBarHeight; final ffi = Get.put(ffitmp, tag: widget.id); - // note: a little trick - ffi.ffiModel.platformFFI = gFFI.ffiModel.platformFFI; ffi.connect(widget.id, tabBarHeight: super.widget.tabBarHeight); WidgetsBinding.instance.addPostFrameCallback((_) { SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); @@ -157,7 +155,7 @@ class _RemotePageState extends State if (newValue.length > common) { var s = newValue.substring(common); if (s.length > 1) { - _ffi.bind.sessionInputString(id: widget.id, value: s); + bind.sessionInputString(id: widget.id, value: s); } else { inputChar(s); } @@ -191,11 +189,11 @@ class _RemotePageState extends State content == '()' || content == '【】')) { // can not only input content[0], because when input ], [ are also auo insert, which cause ] never be input - _ffi.bind.sessionInputString(id: widget.id, value: content); + bind.sessionInputString(id: widget.id, value: content); openKeyboard(); return; } - _ffi.bind.sessionInputString(id: widget.id, value: content); + bind.sessionInputString(id: widget.id, value: content); } else { inputChar(content); } @@ -509,8 +507,8 @@ class _RemotePageState extends State id: widget.id, ) ]; - final cursor = _ffi.bind - .getSessionToggleOptionSync(id: widget.id, arg: 'show-remote-cursor'); + final cursor = bind.getSessionToggleOptionSync( + id: widget.id, arg: 'show-remote-cursor'); if (keyboard || cursor) { paints.add(CursorPaint( id: widget.id, @@ -519,10 +517,10 @@ class _RemotePageState extends State paints.add(getHelpTools()); return MouseRegion( onEnter: (evt) { - _ffi.bind.hostStopSystemKeyPropagate(stopped: false); + bind.hostStopSystemKeyPropagate(stopped: false); }, onExit: (evt) { - _ffi.bind.hostStopSystemKeyPropagate(stopped: true); + bind.hostStopSystemKeyPropagate(stopped: true); }, child: Container( color: MyTheme.canvasColor, @@ -601,7 +599,7 @@ class _RemotePageState extends State more.add(PopupMenuItem( child: Text(translate('Insert Lock')), value: 'lock')); if (pi.platform == 'Windows' && - await _ffi.bind.getSessionToggleOption(id: id, arg: 'privacy-mode') != + await bind.getSessionToggleOption(id: id, arg: 'privacy-mode') != true) { more.add(PopupMenuItem( child: Text(translate((_ffi.ffiModel.inputBlocked ? 'Unb' : 'B') + @@ -617,28 +615,27 @@ class _RemotePageState extends State elevation: 8, ); if (value == 'cad') { - _ffi.bind.sessionCtrlAltDel(id: widget.id); + bind.sessionCtrlAltDel(id: widget.id); } else if (value == 'lock') { - _ffi.bind.sessionLockScreen(id: widget.id); + bind.sessionLockScreen(id: widget.id); } else if (value == 'block-input') { - _ffi.bind.sessionToggleOption( + bind.sessionToggleOption( id: widget.id, value: (_ffi.ffiModel.inputBlocked ? 'un' : '') + 'block-input'); _ffi.ffiModel.inputBlocked = !_ffi.ffiModel.inputBlocked; } else if (value == 'refresh') { - _ffi.bind.sessionRefresh(id: widget.id); + bind.sessionRefresh(id: widget.id); } else if (value == 'paste') { () async { ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain); if (data != null && data.text != null) { - _ffi.bind.sessionInputString(id: widget.id, value: data.text ?? ""); + bind.sessionInputString(id: widget.id, value: data.text ?? ""); } }(); } else if (value == 'enter_os_password') { - var password = - await _ffi.bind.getSessionOption(id: id, arg: "os-password"); + var password = await bind.getSessionOption(id: id, arg: "os-password"); if (password != null) { - _ffi.bind.sessionInputOsPassword(id: widget.id, value: password); + bind.sessionInputOsPassword(id: widget.id, value: password); } else { showSetOSPassword(widget.id, true); } @@ -666,7 +663,7 @@ class _RemotePageState extends State onTouchModeChange: (t) { _ffi.ffiModel.toggleTouchMode(); final v = _ffi.ffiModel.touchMode ? 'Y' : ''; - _ffi.bind.sessionPeerOption( + bind.sessionPeerOption( id: widget.id, name: "touch-mode", value: v); })); })); @@ -892,12 +889,12 @@ class ImagePainter extends CustomPainter { CheckboxListTile getToggle( String id, void Function(void Function()) setState, option, name) { - final opt = ffi(id).bind.getSessionToggleOptionSync(id: id, arg: option); + final opt = bind.getSessionToggleOptionSync(id: id, arg: option); return CheckboxListTile( value: opt, onChanged: (v) { setState(() { - ffi(id).bind.sessionToggleOption(id: id, value: option); + bind.sessionToggleOption(id: id, value: option); }); }, dense: true, @@ -917,11 +914,10 @@ RadioListTile getRadio(String name, String toValue, String curValue, } void showOptions(String id) async { - String quality = - await ffi(id).bind.getSessionImageQuality(id: id) ?? 'balanced'; + String quality = await bind.getSessionImageQuality(id: id) ?? 'balanced'; if (quality == '') quality = 'balanced'; String viewStyle = - await ffi(id).bind.getSessionOption(id: id, arg: 'view-style') ?? ''; + await bind.getSessionOption(id: id, arg: 'view-style') ?? ''; var displays = []; final pi = ffi(id).ffiModel.pi; final image = ffi(id).ffiModel.getConnectionImage(); @@ -934,7 +930,7 @@ void showOptions(String id) async { children.add(InkWell( onTap: () { if (i == cur) return; - ffi(id).bind.sessionSwitchDisplay(id: id, value: i); + bind.sessionSwitchDisplay(id: id, value: i); SmartDialog.dismiss(); }, child: Ink( @@ -979,16 +975,14 @@ void showOptions(String id) async { if (value == null) return; setState(() { quality = value; - ffi(id).bind.sessionSetImageQuality(id: id, value: value); + bind.sessionSetImageQuality(id: id, value: value); }); }; var setViewStyle = (String? value) { if (value == null) return; setState(() { viewStyle = value; - ffi(id) - .bind - .sessionPeerOption(id: id, name: "view-style", value: value); + bind.sessionPeerOption(id: id, name: "view-style", value: value); ffi(id).canvasModel.updateViewStyle(); }); }; @@ -1018,10 +1012,8 @@ void showOptions(String id) async { void showSetOSPassword(String id, bool login) async { final controller = TextEditingController(); - var password = - await ffi(id).bind.getSessionOption(id: id, arg: "os-password") ?? ""; - var autoLogin = - await ffi(id).bind.getSessionOption(id: id, arg: "auto-login") != ""; + var password = await bind.getSessionOption(id: id, arg: "os-password") ?? ""; + var autoLogin = await bind.getSessionOption(id: id, arg: "auto-login") != ""; controller.text = password; DialogManager.show((setState, close) { return CustomAlertDialog( @@ -1054,13 +1046,11 @@ void showSetOSPassword(String id, bool login) async { style: flatButtonStyle, onPressed: () { var text = controller.text.trim(); - ffi(id) - .bind - .sessionPeerOption(id: id, name: "os-password", value: text); - ffi(id).bind.sessionPeerOption( + bind.sessionPeerOption(id: id, name: "os-password", value: text); + bind.sessionPeerOption( id: id, name: "auto-login", value: autoLogin ? 'Y' : ''); if (text != "" && login) { - ffi(id).bind.sessionInputOsPassword(id: id, value: text); + bind.sessionInputOsPassword(id: id, value: text); } close(); }, diff --git a/flutter/lib/desktop/widgets/peer_widget.dart b/flutter/lib/desktop/widgets/peer_widget.dart index 4705516f5..1a66f3a06 100644 --- a/flutter/lib/desktop/widgets/peer_widget.dart +++ b/flutter/lib/desktop/widgets/peer_widget.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart'; @@ -8,6 +7,7 @@ import 'package:visibility_detector/visibility_detector.dart'; import 'package:window_manager/window_manager.dart'; import '../../models/peer_model.dart'; +import '../../models/platform_model.dart'; import '../../common.dart'; import 'peercard_widget.dart'; @@ -116,7 +116,7 @@ class _PeerWidgetState extends State<_PeerWidget> with WindowListener { if (!setEquals(_curPeers, _lastQueryPeers)) { if (now.difference(_lastChangeTime) > Duration(seconds: 1)) { if (_curPeers.length > 0) { - gFFI.ffiModel.platformFFI.ffiBind + platformFFI.ffiBind .queryOnlines(ids: _curPeers.toList(growable: false)); _lastQueryPeers = {..._curPeers}; _lastQueryTime = DateTime.now(); @@ -127,7 +127,7 @@ class _PeerWidgetState extends State<_PeerWidget> with WindowListener { if (_queryCoun < _maxQueryCount) { if (now.difference(_lastQueryTime) > Duration(seconds: 20)) { if (_curPeers.length > 0) { - gFFI.ffiModel.platformFFI.ffiBind + platformFFI.ffiBind .queryOnlines(ids: _curPeers.toList(growable: false)); _lastQueryTime = DateTime.now(); _queryCoun += 1; @@ -169,7 +169,7 @@ class RecentPeerWidget extends BasePeerWidget { @override Widget build(BuildContext context) { final widget = super.build(context); - gFFI.bind.mainLoadRecentPeers(); + bind.mainLoadRecentPeers(); return widget; } } @@ -186,7 +186,7 @@ class FavoritePeerWidget extends BasePeerWidget { @override Widget build(BuildContext context) { final widget = super.build(context); - gFFI.bind.mainLoadFavPeers(); + bind.mainLoadFavPeers(); return widget; } } @@ -203,7 +203,7 @@ class DiscoveredPeerWidget extends BasePeerWidget { @override Widget build(BuildContext context) { final widget = super.build(context); - gFFI.bind.mainLoadLanPeers(); + bind.mainLoadLanPeers(); return widget; } } diff --git a/flutter/lib/desktop/widgets/peercard_widget.dart b/flutter/lib/desktop/widgets/peercard_widget.dart index 3a4dbfada..0782e8426 100644 --- a/flutter/lib/desktop/widgets/peercard_widget.dart +++ b/flutter/lib/desktop/widgets/peercard_widget.dart @@ -5,9 +5,11 @@ import 'package:get/get.dart'; import '../../common.dart'; import '../../models/model.dart'; +import '../../models/platform_model.dart'; import '../../models/peer_model.dart'; typedef PopupMenuItemsFunc = Future>> Function(); + enum PeerType { recent, fav, discovered, ab } class _PeerCard extends StatefulWidget { @@ -15,10 +17,11 @@ class _PeerCard extends StatefulWidget { final PopupMenuItemsFunc popupMenuItemsFunc; final PeerType type; - _PeerCard({required this.peer, - required this.popupMenuItemsFunc, - Key? key, - required this.type}) + _PeerCard( + {required this.peer, + required this.popupMenuItemsFunc, + Key? key, + required this.type}) : super(key: key); @override @@ -54,9 +57,10 @@ class _PeerCardState extends State<_PeerCard> )); } - Widget _buildPeerTile(BuildContext context, Peer peer, Rx deco) { + Widget _buildPeerTile( + BuildContext context, Peer peer, Rx deco) { return Obx( - () => Container( + () => Container( decoration: deco.value, child: Column( mainAxisSize: MainAxisSize.min, @@ -135,7 +139,7 @@ class _PeerCardState extends State<_PeerCard> child: CircleAvatar( radius: 5, backgroundColor: - peer.online ? Colors.green : Colors.yellow)), + peer.online ? Colors.green : Colors.yellow)), Text('${peer.id}') ]), InkWell( @@ -183,12 +187,13 @@ class _PeerCardState extends State<_PeerCard> ); if (value == 'remove') { setState(() => gFFI.setByName('remove', '$id')); - () async { + () async { removePreference(id); }(); } else if (value == 'file') { _connect(id, isFileTransfer: true); - } else if (value == 'add-fav') {} else if (value == 'connect') { + } else if (value == 'add-fav') { + } else if (value == 'connect') { _connect(id, isFileTransfer: false); } else if (value == 'ab-delete') { gFFI.abModel.deletePeer(id); @@ -199,7 +204,7 @@ class _PeerCardState extends State<_PeerCard> } else if (value == 'rename') { _rename(id); } else if (value == 'unremember-password') { - await gFFI.bind.mainForgetPassword(id: id); + await bind.mainForgetPassword(id: id); } } @@ -220,7 +225,7 @@ class _PeerCardState extends State<_PeerCard> child: GestureDetector( onTap: onTap, child: Obx( - () => Container( + () => Container( decoration: BoxDecoration( color: rxTags.contains(tagName) ? Colors.blue : null, border: Border.all(color: MyTheme.darkGray), @@ -264,12 +269,12 @@ class _PeerCardState extends State<_PeerCard> child: Wrap( children: tags .map((e) => _buildTag(e, selectedTag, onTap: () { - if (selectedTag.contains(e)) { - selectedTag.remove(e); - } else { - selectedTag.add(e); - } - })) + if (selectedTag.contains(e)) { + selectedTag.remove(e); + } else { + selectedTag.add(e); + } + })) .toList(growable: false), ), ), diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index f2ebb3134..bceb8fa8a 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -13,6 +13,8 @@ import 'package:provider/provider.dart'; // import 'package:window_manager/window_manager.dart'; import 'common.dart'; +import 'consts.dart'; +import 'models/platform_model.dart'; import 'mobile/pages/home_page.dart'; import 'mobile/pages/server_page.dart'; import 'mobile/pages/settings_page.dart'; @@ -21,25 +23,9 @@ int? windowId; Future main(List args) async { WidgetsFlutterBinding.ensureInitialized(); - // global FFI, use this **ONLY** for global configuration - // for convenience, use global FFI on mobile platform - // focus on multi-ffi on desktop first - await initGlobalFFI(); - // await Firebase.initializeApp(); - if (isAndroid) { - toAndroidChannelInit(); - } - refreshCurrentUser(); - runRustDeskApp(args); -} -ThemeData getCurrentTheme() { - return isDarkTheme() ? MyTheme.darkTheme : MyTheme.darkTheme; -} - -void runRustDeskApp(List args) async { if (!isDesktop) { - runApp(App()); + runMainApp(false); return; } // main window @@ -52,28 +38,62 @@ void runRustDeskApp(List args) async { WindowType wType = type.windowType; switch (wType) { case WindowType.RemoteDesktop: - runApp(GetMaterialApp( - theme: getCurrentTheme(), - home: DesktopRemoteScreen( - params: argument, - ), - )); + runRemoteScreen(argument); break; case WindowType.FileTransfer: - runApp(GetMaterialApp( - theme: getCurrentTheme(), - home: DesktopFileTransferScreen(params: argument))); + runFileTransferScreen(argument); break; default: break; } } else { + runMainApp(true); + } +} + +ThemeData getCurrentTheme() { + return isDarkTheme() ? MyTheme.darkTheme : MyTheme.darkTheme; +} + +Future initEnv(String appType) async { + await platformFFI.init(appType); + // global FFI, use this **ONLY** for global configuration + // for convenience, use global FFI on mobile platform + // focus on multi-ffi on desktop first + await initGlobalFFI(); + // await Firebase.initializeApp(); + if (isAndroid) { + toAndroidChannelInit(); + } + refreshCurrentUser(); +} + +void runMainApp(bool startService) async { + await initEnv(kAppTypeMain); + if (startService) { // await windowManager.ensureInitialized(); // disable tray // initTray(); gFFI.serverModel.startService(); - runApp(App()); } + runApp(App()); +} + +void runRemoteScreen(Map argument) async { + await initEnv(kAppTypeDesktopRemote); + runApp(GetMaterialApp( + theme: getCurrentTheme(), + home: DesktopRemoteScreen( + params: argument, + ), + )); +} + +void runFileTransferScreen(Map argument) async { + await initEnv(kAppTypeDesktopFileTransfer); + runApp(GetMaterialApp( + theme: getCurrentTheme(), + home: DesktopFileTransferScreen(params: argument))); } class App extends StatelessWidget { @@ -108,8 +128,8 @@ class App extends StatelessWidget { builder: FlutterSmartDialog.init( builder: isAndroid ? (_, child) => AccessibilityListener( - child: child, - ) + child: child, + ) : null)), ); } diff --git a/flutter/lib/models/ab_model.dart b/flutter/lib/models/ab_model.dart index bfdb6fa1a..b9740ed8f 100644 --- a/flutter/lib/models/ab_model.dart +++ b/flutter/lib/models/ab_model.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/models/model.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; import 'package:get/get.dart'; import 'package:http/http.dart' as http; @@ -46,7 +47,7 @@ class AbModel with ChangeNotifier { } Future getApiServer() async { - return await _ffi?.bind.mainGetApiServer() ?? ""; + return await bind.mainGetApiServer() ?? ""; } void reset() { diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index 5bca33303..e86ac1de2 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -9,6 +9,7 @@ import 'package:get/get.dart'; import 'package:path/path.dart' as Path; import 'model.dart'; +import 'platform_model.dart'; enum SortBy { Name, Type, Modified, Size } @@ -50,7 +51,7 @@ class FileModel extends ChangeNotifier { bool get localSortAscending => _localSortAscending; - SortBy getSortStyle(bool isLocal){ + SortBy getSortStyle(bool isLocal) { return isLocal ? _localSortStyle : _remoteSortStyle; } @@ -164,7 +165,7 @@ class FileModel extends ChangeNotifier { // Desktop uses jobTable // id = index + 1 final jobIndex = getJob(id); - if (jobIndex >= 0 && _jobTable.length > jobIndex){ + if (jobIndex >= 0 && _jobTable.length > jobIndex) { final job = _jobTable[jobIndex]; job.fileNum = int.parse(evt['file_num']); job.speed = double.parse(evt['speed']); @@ -203,8 +204,7 @@ class FileModel extends ChangeNotifier { debugPrint("init remote home:${fd.path}"); _currentRemoteDir = fd; } - } - finally {} + } finally {} } _fileFetcher.tryCompleteTask(evt['value'], evt['is_local']); notifyListeners(); @@ -260,7 +260,7 @@ class FileModel extends ChangeNotifier { final id = int.tryParse(evt['id']) ?? 0; if (false == resp) { final jobIndex = getJob(id); - if (jobIndex != -1){ + if (jobIndex != -1) { cancelJob(id); final job = jobTable[jobIndex]; job.state = JobState.done; @@ -274,9 +274,12 @@ class FileModel extends ChangeNotifier { // overwrite need_override = true; } - _ffi.target?.bind.sessionSetConfirmOverrideFile(id: _ffi.target?.id ?? "", - actId: id, fileNum: int.parse(evt['file_num']), - needOverride: need_override, remember: fileConfirmCheckboxRemember, + bind.sessionSetConfirmOverrideFile( + id: _ffi.target?.id ?? "", + actId: id, + fileNum: int.parse(evt['file_num']), + needOverride: need_override, + remember: fileConfirmCheckboxRemember, isUpload: evt['is_upload'] == "true"); } } @@ -288,21 +291,27 @@ class FileModel extends ChangeNotifier { onReady() async { _localOption.home = _ffi.target?.getByName("get_home_dir") ?? ""; - _localOption.showHidden = (await _ffi.target?.bind.sessionGetPeerOption - (id: _ffi.target?.id ?? "", name: "local_show_hidden"))?.isNotEmpty ?? false; + _localOption.showHidden = (await bind.sessionGetPeerOption( + id: _ffi.target?.id ?? "", name: "local_show_hidden")) + ?.isNotEmpty ?? + false; - _remoteOption.showHidden = (await _ffi.target?.bind.sessionGetPeerOption - (id: _ffi.target?.id ?? "", name: "remote_show_hidden"))?.isNotEmpty ?? false; + _remoteOption.showHidden = (await bind.sessionGetPeerOption( + id: _ffi.target?.id ?? "", name: "remote_show_hidden")) + ?.isNotEmpty ?? + false; _remoteOption.isWindows = _ffi.target?.ffiModel.pi.platform == "Windows"; debugPrint("remote platform: ${_ffi.target?.ffiModel.pi.platform}"); await Future.delayed(Duration(milliseconds: 100)); - final local = (await _ffi.target?.bind.sessionGetPeerOption - (id: _ffi.target?.id ?? "", name: "local_dir")) ?? ""; - final remote = (await _ffi.target?.bind.sessionGetPeerOption - (id: _ffi.target?.id ?? "", name: "remote_dir")) ?? ""; + final local = (await bind.sessionGetPeerOption( + id: _ffi.target?.id ?? "", name: "local_dir")) ?? + ""; + final remote = (await bind.sessionGetPeerOption( + id: _ffi.target?.id ?? "", name: "remote_dir")) ?? + ""; openDirectory(local.isEmpty ? _localOption.home : local, isLocal: true); openDirectory(remote.isEmpty ? _remoteOption.home : remote, isLocal: false); await Future.delayed(Duration(seconds: 1)); @@ -313,7 +322,7 @@ class FileModel extends ChangeNotifier { openDirectory(_remoteOption.home, isLocal: false); } // load last transfer jobs - await _ffi.target?.bind.sessionLoadLastTransferJobs(id: '${_ffi.target?.id}'); + await bind.sessionLoadLastTransferJobs(id: '${_ffi.target?.id}'); } onClose() { @@ -327,8 +336,8 @@ class FileModel extends ChangeNotifier { msgMap["remote_dir"] = _currentRemoteDir.path; msgMap["remote_show_hidden"] = _remoteOption.showHidden ? "Y" : ""; final id = _ffi.target?.id ?? ""; - for(final msg in msgMap.entries) { - _ffi.target?.bind.sessionPeerOption(id: id, name: msg.key, value: msg.value); + for (final msg in msgMap.entries) { + bind.sessionPeerOption(id: id, name: msg.key, value: msg.value); } _currentLocalDir.clear(); _currentRemoteDir.clear(); @@ -339,8 +348,9 @@ class FileModel extends ChangeNotifier { Future refresh({bool? isLocal}) async { if (isDesktop) { isLocal = isLocal ?? _isLocal; - await isLocal ? openDirectory(currentLocalDir.path, isLocal: isLocal) : - openDirectory(currentRemoteDir.path, isLocal: isLocal); + await isLocal + ? openDirectory(currentLocalDir.path, isLocal: isLocal) + : openDirectory(currentRemoteDir.path, isLocal: isLocal); } else { await openDirectory(currentDir.path); } @@ -353,7 +363,9 @@ class FileModel extends ChangeNotifier { final isWindows = isLocal ? _localOption.isWindows : _remoteOption.isWindows; // process /C:\ -> C:\ on Windows - if (isLocal ? _localOption.isWindows : _remoteOption.isWindows && path.length > 1 && path[0] == '/') { + if (isLocal + ? _localOption.isWindows + : _remoteOption.isWindows && path.length > 1 && path[0] == '/') { path = path.substring(1); if (path[path.length - 1] != '\\') { path = path + "\\"; @@ -380,7 +392,8 @@ class FileModel extends ChangeNotifier { goToParentDirectory({bool? isLocal}) { isLocal = isLocal ?? _isLocal; - final isWindows = isLocal ? _localOption.isWindows : _remoteOption.isWindows; + final isWindows = + isLocal ? _localOption.isWindows : _remoteOption.isWindows; final currDir = isLocal ? currentLocalDir : currentRemoteDir; var parent = PathUtil.dirname(currDir.path, isWindows); // specially for C:\, D:\, goto '/' @@ -395,12 +408,11 @@ class FileModel extends ChangeNotifier { sendFiles(SelectedItems items, {bool isRemote = false}) { if (isDesktop) { // desktop sendFiles - final toPath = - isRemote ? currentLocalDir.path : currentRemoteDir.path; + final toPath = isRemote ? currentLocalDir.path : currentRemoteDir.path; final isWindows = - isRemote ? _localOption.isWindows : _remoteOption.isWindows; + isRemote ? _localOption.isWindows : _remoteOption.isWindows; final showHidden = - isRemote ? _localOption.showHidden : _remoteOption.showHidden; + isRemote ? _localOption.showHidden : _remoteOption.showHidden; items.items.forEach((from) async { final jobId = ++_jobId; _jobTable.add(JobProgress() @@ -408,11 +420,17 @@ class FileModel extends ChangeNotifier { ..totalSize = from.size ..state = JobState.inProgress ..id = jobId - ..isRemote = isRemote - ); - _ffi.target?.bind.sessionSendFiles(id: '${_ffi.target?.id}', actId: _jobId, path: from.path, to: PathUtil.join(toPath, from.name, isWindows) - ,fileNum: 0, includeHidden: showHidden, isRemote: isRemote); - print("path:${from.path}, toPath:${toPath}, to:${PathUtil.join(toPath, from.name, isWindows)}"); + ..isRemote = isRemote); + bind.sessionSendFiles( + id: '${_ffi.target?.id}', + actId: _jobId, + path: from.path, + to: PathUtil.join(toPath, from.name, isWindows), + fileNum: 0, + includeHidden: showHidden, + isRemote: isRemote); + print( + "path:${from.path}, toPath:${toPath}, to:${PathUtil.join(toPath, from.name, isWindows)}"); }); } else { if (items.isLocal == null) { @@ -421,15 +439,21 @@ class FileModel extends ChangeNotifier { } _jobProgress.state = JobState.inProgress; final toPath = - items.isLocal! ? currentRemoteDir.path : currentLocalDir.path; + items.isLocal! ? currentRemoteDir.path : currentLocalDir.path; final isWindows = - items.isLocal! ? _localOption.isWindows : _remoteOption.isWindows; + items.isLocal! ? _localOption.isWindows : _remoteOption.isWindows; final showHidden = - items.isLocal! ? _localOption.showHidden : _remoteOption.showHidden; + items.isLocal! ? _localOption.showHidden : _remoteOption.showHidden; items.items.forEach((from) async { _jobId++; - await _ffi.target?.bind.sessionSendFiles(id: '${_ffi.target?.getId()}', actId: _jobId, path: from.path, to: PathUtil.join(toPath, from.name, isWindows) - ,fileNum: 0, includeHidden: showHidden, isRemote: !(items.isLocal!)); + await bind.sessionSendFiles( + id: '${_ffi.target?.getId()}', + actId: _jobId, + path: from.path, + to: PathUtil.join(toPath, from.name, isWindows), + fileNum: 0, + includeHidden: showHidden, + isRemote: !(items.isLocal!)); }); } } @@ -626,21 +650,34 @@ class FileModel extends ChangeNotifier { } sendRemoveFile(String path, int fileNum, bool isLocal) { - _ffi.target?.bind.sessionRemoveFile(id: '${_ffi.target?.id}', actId: _jobId, path: path, isRemote: !isLocal, fileNum: fileNum); + bind.sessionRemoveFile( + id: '${_ffi.target?.id}', + actId: _jobId, + path: path, + isRemote: !isLocal, + fileNum: fileNum); } sendRemoveEmptyDir(String path, int fileNum, bool isLocal) { - _ffi.target?.bind.sessionRemoveAllEmptyDirs(id: '${_ffi.target?.id}', actId: _jobId, path: path, isRemote: !isLocal); + bind.sessionRemoveAllEmptyDirs( + id: '${_ffi.target?.id}', + actId: _jobId, + path: path, + isRemote: !isLocal); } createDir(String path, {bool? isLocal}) async { isLocal = isLocal ?? this.isLocal; _jobId++; - _ffi.target?.bind.sessionCreateDir(id: '${_ffi.target?.id}', actId: _jobId, path: path, isRemote: !isLocal); + bind.sessionCreateDir( + id: '${_ffi.target?.id}', + actId: _jobId, + path: path, + isRemote: !isLocal); } cancelJob(int id) async { - _ffi.target?.bind.sessionCancelJob(id: '${_ffi.target?.id}', actId: id); + bind.sessionCancelJob(id: '${_ffi.target?.id}', actId: id); jobReset(); } @@ -650,14 +687,18 @@ class FileModel extends ChangeNotifier { // compatible for mobile logic _currentLocalDir.changeSortStyle(sort, ascending: ascending); _currentRemoteDir.changeSortStyle(sort, ascending: ascending); - _localSortStyle = sort; _localSortAscending = ascending; - _remoteSortStyle = sort; _remoteSortAscending = ascending; + _localSortStyle = sort; + _localSortAscending = ascending; + _remoteSortStyle = sort; + _remoteSortAscending = ascending; } else if (isLocal) { _currentLocalDir.changeSortStyle(sort, ascending: ascending); - _localSortStyle = sort; _localSortAscending = ascending; + _localSortStyle = sort; + _localSortAscending = ascending; } else { _currentRemoteDir.changeSortStyle(sort, ascending: ascending); - _remoteSortStyle = sort; _remoteSortAscending = ascending; + _remoteSortStyle = sort; + _remoteSortAscending = ascending; } notifyListeners(); } @@ -668,7 +709,7 @@ class FileModel extends ChangeNotifier { void updateFolderFiles(Map evt) { // ret: "{\"id\":1,\"num_entries\":12,\"total_size\":1264822.0}" - Map info = json.decode(evt['info']); + Map info = json.decode(evt['info']); int id = info['id']; int num_entries = info['num_entries']; double total_size = info['total_size']; @@ -685,7 +726,7 @@ class FileModel extends ChangeNotifier { void loadLastJob(Map evt) { debugPrint("load last job: ${evt}"); - Map jobDetail = json.decode(evt['value']); + Map jobDetail = json.decode(evt['value']); // int id = int.parse(jobDetail['id']); String remote = jobDetail['remote']; String to = jobDetail['to']; @@ -703,13 +744,14 @@ class FileModel extends ChangeNotifier { ..showHidden = showHidden ..state = JobState.paused; jobTable.add(jobProgress); - _ffi.target?.bind.sessionAddJob(id: '${_ffi.target?.id}', - isRemote: isRemote, - includeHidden: showHidden, - actId: currJobId, - path: isRemote ? remote : to, - to: isRemote ? to: remote, - fileNum: fileNum, + bind.sessionAddJob( + id: '${_ffi.target?.id}', + isRemote: isRemote, + includeHidden: showHidden, + actId: currJobId, + path: isRemote ? remote : to, + to: isRemote ? to : remote, + fileNum: fileNum, ); } @@ -717,9 +759,8 @@ class FileModel extends ChangeNotifier { final jobIndex = getJob(jobId); if (jobIndex != -1) { final job = jobTable[jobIndex]; - _ffi.target?.bind.sessionResumeJob(id: '${_ffi.target?.id}', - actId: job.id, - isRemote: job.isRemote); + bind.sessionResumeJob( + id: '${_ffi.target?.id}', actId: job.id, isRemote: job.isRemote); job.state = JobState.inProgress; } else { debugPrint("jobId ${jobId} is not exists"); @@ -844,12 +885,12 @@ class FileFetcher { String path, bool isLocal, bool showHidden) async { try { if (isLocal) { - final res = await _ffi.bind.sessionReadLocalDirSync( + final res = await bind.sessionReadLocalDirSync( id: id ?? "", path: path, showHidden: showHidden); final fd = FileDirectory.fromJson(jsonDecode(res)); return fd; } else { - await _ffi.bind.sessionReadRemoteDir( + await bind.sessionReadRemoteDir( id: id ?? "", path: path, includeHidden: showHidden); return registerReadTask(isLocal, path); } @@ -862,7 +903,12 @@ class FileFetcher { int id, String path, bool isLocal, bool showHidden) async { // TODO test Recursive is show hidden default? try { - await _ffi.bind.sessionReadDirRecursive(id: _ffi.id, actId: id, path: path, isRemote: !isLocal, showHidden: showHidden); + await bind.sessionReadDirRecursive( + id: _ffi.id, + actId: id, + path: path, + isRemote: !isLocal, + showHidden: showHidden); return registerReadRecursiveTask(id); } catch (e) { return Future.error(e); @@ -1033,7 +1079,9 @@ List _sortList(List list, SortBy sortType, bool ascending) { files.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase())); // first folders will go to list (if available) then files will go to list. - return ascending ? [...dirs, ...files] : [...dirs.reversed.toList(), ...files.reversed.toList()]; + return ascending + ? [...dirs, ...files] + : [...dirs.reversed.toList(), ...files.reversed.toList()]; } else if (sortType == SortBy.Modified) { // making the list of Path & DateTime List<_PathStat> _pathStat = []; @@ -1065,7 +1113,9 @@ List _sortList(List list, SortBy sortType, bool ascending) { .split('.') .last .compareTo(b.name.toLowerCase().split('.').last)); - return ascending ? [...dirs, ...files]: [...dirs.reversed.toList(), ...files.reversed.toList()]; + return ascending + ? [...dirs, ...files] + : [...dirs.reversed.toList(), ...files.reversed.toList()]; } else if (sortType == SortBy.Size) { // create list of path and size Map _sizeMap = {}; @@ -1090,7 +1140,9 @@ List _sortList(List list, SortBy sortType, bool ascending) { .indexWhere((element) => element.key == a.name) .compareTo( _sizeMapList.indexWhere((element) => element.key == b.name))); - return ascending ? [...dirs, ...files]: [...dirs.reversed.toList(), ...files.reversed.toList()]; + return ascending + ? [...dirs, ...files] + : [...dirs.reversed.toList(), ...files.reversed.toList()]; } return []; } diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 11415eeef..5c83a124c 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -20,8 +20,8 @@ import 'package:tuple/tuple.dart'; import '../common.dart'; import '../mobile/widgets/dialog.dart'; import '../mobile/widgets/overlay.dart'; -import 'native_model.dart' if (dart.library.html) 'web_model.dart'; import 'peer_model.dart'; +import 'platform_model.dart'; typedef HandleMsgBox = void Function(Map evt, String id); bool _waitForImage = false; @@ -29,7 +29,6 @@ bool _waitForImage = false; class FfiModel with ChangeNotifier { PeerInfo _pi = PeerInfo(); Display _display = Display(); - PlatformFFI _platformFFI = PlatformFFI(); var _inputBlocked = false; final _permissions = Map(); @@ -44,12 +43,6 @@ class FfiModel with ChangeNotifier { Display get display => _display; - PlatformFFI get platformFFI => _platformFFI; - - set platformFFI(PlatformFFI value) { - _platformFFI = value; - } - bool? get secure => _secure; bool? get direct => _direct; @@ -71,10 +64,6 @@ class FfiModel with ChangeNotifier { clear(); } - Future init() async { - await _platformFFI.init(); - } - void toggleTouchMode() { if (!isPeerAndroid) { _touchMode = !_touchMode; @@ -280,7 +269,7 @@ class FfiModel with ChangeNotifier { _timer?.cancel(); if (hasRetry) { _timer = Timer(Duration(seconds: _reconnects), () { - parent.target?.bind.sessionReconnect(id: id); + bind.sessionReconnect(id: id); clearPermissions(); showLoading(translate('Connecting...')); }); @@ -306,9 +295,8 @@ class FfiModel with ChangeNotifier { Timer(Duration(milliseconds: 100), showMobileActionsOverlay); } } else { - _touchMode = await parent.target?.bind - .getSessionOption(id: peerId, arg: "touch-mode") != - ''; + _touchMode = + await bind.getSessionOption(id: peerId, arg: "touch-mode") != ''; } if (evt['is_file_transfer'] == "true") { @@ -387,8 +375,7 @@ class ImageModel with ChangeNotifier { } Future.delayed(Duration(milliseconds: 1), () { if (parent.target?.ffiModel.isPeerAndroid ?? false) { - parent.target?.bind - .sessionPeerOption(id: _id, name: "view-style", value: "shrink"); + bind.sessionPeerOption(id: _id, name: "view-style", value: "shrink"); parent.target?.canvasModel.updateViewStyle(); } }); @@ -439,8 +426,7 @@ class CanvasModel with ChangeNotifier { double get tabBarHeight => _tabBarHeight; void updateViewStyle() async { - final s = - await parent.target?.bind.getSessionOption(id: id, arg: 'view-style'); + final s = await bind.getSessionOption(id: id, arg: 'view-style'); if (s == null) { return; } @@ -844,13 +830,6 @@ class FFI { this.userModel = UserModel(WeakReference(this)); } - static FFI newFFI() { - final ffi = FFI(); - // keep platformFFI only once - ffi.ffiModel.platformFFI = gFFI.ffiModel.platformFFI; - return ffi; - } - /// Get the remote id for current client. String getId() { return getByName('remote_id'); // TODO @@ -1008,16 +987,16 @@ class FFI { /// Send **get** command to the Rust core based on [name] and [arg]. /// Return the result as a string. String getByName(String name, [String arg = '']) { - return ffiModel.platformFFI.getByName(name, arg); + return platformFFI.getByName(name, arg); } /// Send **set** command to the Rust core based on [name] and [value]. void setByName(String name, [String value = '']) { - ffiModel.platformFFI.setByName(name, value); + platformFFI.setByName(name, value); } String getOption(String name) { - return ffiModel.platformFFI.getByName("option", name); + return platformFFI.getByName("option", name); } Future getLocalOption(String name) { @@ -1040,11 +1019,9 @@ class FFI { Map res = Map() ..["name"] = name ..["value"] = value; - return ffiModel.platformFFI.setByName('option', jsonEncode(res)); + return platformFFI.setByName('option', jsonEncode(res)); } - RustdeskImpl get bind => ffiModel.platformFFI.ffiBind; - handleMouse(Map evt, {double tabBarHeight = 0.0}) { var type = ''; var isMove = false; @@ -1102,18 +1079,18 @@ class FFI { listenToMouse(bool yesOrNo) { if (yesOrNo) { - ffiModel.platformFFI.startDesktopWebListener(); + platformFFI.startDesktopWebListener(); } else { - ffiModel.platformFFI.stopDesktopWebListener(); + platformFFI.stopDesktopWebListener(); } } void setMethodCallHandler(FMethod callback) { - ffiModel.platformFFI.setMethodCallHandler(callback); + platformFFI.setMethodCallHandler(callback); } Future invokeMethod(String method, [dynamic arguments]) async { - return await ffiModel.platformFFI.invokeMethod(method, arguments); + return await platformFFI.invokeMethod(method, arguments); } Future> getAudioInputs() async { diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index 511aa5ffe..784ffe6c8 100644 --- a/flutter/lib/models/native_model.dart +++ b/flutter/lib/models/native_model.dart @@ -27,17 +27,24 @@ typedef HandleEvent = void Function(Map evt); /// FFI wrapper around the native Rust core. /// Hides the platform differences. class PlatformFFI { - Pointer? _lastRgbaFrame; String _dir = ''; String _homeDir = ''; F2? _getByName; F3? _setByName; var _eventHandlers = Map>(); late RustdeskImpl _ffiBind; + late String _appType; void Function(Map)? _eventCallback; + PlatformFFI._(); + + static final PlatformFFI instance = PlatformFFI._(); + final _toAndroidChannel = MethodChannel("mChannel"); + RustdeskImpl get ffiBind => _ffiBind; + static get localeName => Platform.localeName; + static Future getVersion() async { PackageInfo packageInfo = await PackageInfo.fromPlatform(); return packageInfo.version; @@ -94,10 +101,8 @@ class PlatformFFI { } /// Init the FFI class, loads the native Rust core library. - Future init() async { - isIOS = Platform.isIOS; - isAndroid = Platform.isAndroid; - isDesktop = Platform.isWindows || Platform.isMacOS || Platform.isLinux; + Future init(String appType) async { + _appType = appType; // if (isDesktop) { // // TODO // return; @@ -111,7 +116,7 @@ class PlatformFFI { : Platform.isMacOS ? DynamicLibrary.open("librustdesk.dylib") : DynamicLibrary.process(); - print('initializing FFI'); + debugPrint('initializing FFI ${_appType}'); try { _getByName = dylib.lookupFunction('get_by_name'); _setByName = @@ -155,7 +160,8 @@ class PlatformFFI { name = macOsInfo.computerName; id = macOsInfo.systemGUID ?? ""; } - print("info1-id:$id,info2-name:$name,dir:$_dir,homeDir:$_homeDir"); + print( + "_appType:$_appType,info1-id:$id,info2-name:$name,dir:$_dir,homeDir:$_homeDir"); setByName('info1', id); setByName('info2', name); setByName('home_dir', _homeDir); @@ -185,17 +191,18 @@ class PlatformFFI { /// Start listening to the Rust core's events and frames. void _startListenEvent(RustdeskImpl rustdeskImpl) { () async { - await for (final message in rustdeskImpl.startGlobalEventStream()) { - if (_eventCallback != null) { - try { - Map event = json.decode(message); - // _tryHandle here may be more flexible than _eventCallback - if (!_tryHandle(event)) { + await for (final message + in rustdeskImpl.startGlobalEventStream(appType: _appType)) { + try { + Map event = json.decode(message); + // _tryHandle here may be more flexible than _eventCallback + if (!_tryHandle(event)) { + if (_eventCallback != null) { _eventCallback!(event); } - } catch (e) { - print('json.decode fail(): $e'); } + } catch (e) { + print('json.decode fail(): $e'); } } }(); @@ -212,7 +219,7 @@ class PlatformFFI { void stopDesktopWebListener() {} void setMethodCallHandler(FMethod callback) { - toAndroidChannel.setMethodCallHandler((call) async { + _toAndroidChannel.setMethodCallHandler((call) async { callback(call.method, call.arguments); return null; }); @@ -220,9 +227,6 @@ class PlatformFFI { invokeMethod(String method, [dynamic arguments]) async { if (!isAndroid) return Future(() => false); - return await toAndroidChannel.invokeMethod(method, arguments); + return await _toAndroidChannel.invokeMethod(method, arguments); } } - -final localeName = Platform.localeName; -final toAndroidChannel = MethodChannel("mChannel"); diff --git a/flutter/lib/models/peer_model.dart b/flutter/lib/models/peer_model.dart index eb520f015..5c889e60f 100644 --- a/flutter/lib/models/peer_model.dart +++ b/flutter/lib/models/peer_model.dart @@ -1,6 +1,6 @@ import 'dart:convert'; import 'package:flutter/foundation.dart'; -import '../../common.dart'; +import 'platform_model.dart'; class Peer { final String id; @@ -44,11 +44,10 @@ class Peers extends ChangeNotifier { _name = name; _loadEvent = loadEvent; _peers = _initPeers; - gFFI.ffiModel.platformFFI.registerEventHandler(_cbQueryOnlines, _name, - (evt) { + platformFFI.registerEventHandler(_cbQueryOnlines, _name, (evt) { _updateOnlineState(evt); }); - gFFI.ffiModel.platformFFI.registerEventHandler(_loadEvent, _name, (evt) { + platformFFI.registerEventHandler(_loadEvent, _name, (evt) { _updatePeers(evt); }); } @@ -57,8 +56,8 @@ class Peers extends ChangeNotifier { @override void dispose() { - gFFI.ffiModel.platformFFI.unregisterEventHandler(_cbQueryOnlines, _name); - gFFI.ffiModel.platformFFI.unregisterEventHandler(_loadEvent, _name); + platformFFI.unregisterEventHandler(_cbQueryOnlines, _name); + platformFFI.unregisterEventHandler(_loadEvent, _name); super.dispose(); } diff --git a/flutter/lib/models/platform_model.dart b/flutter/lib/models/platform_model.dart new file mode 100644 index 000000000..d2b8fa765 --- /dev/null +++ b/flutter/lib/models/platform_model.dart @@ -0,0 +1,7 @@ +import 'package:flutter_hbb/generated_bridge.dart'; +import 'native_model.dart' if (dart.library.html) 'web_model.dart'; + +final platformFFI = PlatformFFI.instance; +final localeName = PlatformFFI.localeName; + +RustdeskImpl get bind => platformFFI.ffiBind; diff --git a/flutter/lib/models/user_model.dart b/flutter/lib/models/user_model.dart index a842ec36e..539211664 100644 --- a/flutter/lib/models/user_model.dart +++ b/flutter/lib/models/user_model.dart @@ -6,6 +6,7 @@ import 'package:get/get.dart'; import 'package:http/http.dart' as http; import 'model.dart'; +import 'platform_model.dart'; class UserModel extends ChangeNotifier { var userName = "".obs; @@ -17,8 +18,7 @@ class UserModel extends ChangeNotifier { if (userName.isNotEmpty) { return userName.value; } - final userInfo = - await parent.target?.bind.mainGetLocalOption(key: 'user_info') ?? "{}"; + final userInfo = await bind.mainGetLocalOption(key: 'user_info') ?? "{}"; if (userInfo.trim().isEmpty) { return ""; } @@ -29,10 +29,6 @@ class UserModel extends ChangeNotifier { Future logOut() async { debugPrint("start logout"); - final bind = parent.target?.bind; - if (bind == null) { - return; - } final url = await bind.mainGetApiServer(); final _ = await http.post(Uri.parse("$url/api/logout"), body: { @@ -55,10 +51,6 @@ class UserModel extends ChangeNotifier { } Future> login(String userName, String pass) async { - final bind = parent.target?.bind; - if (bind == null) { - return {"error": "no context"}; - } final url = await bind.mainGetApiServer(); try { final resp = await http.post(Uri.parse("$url/api/login"), diff --git a/flutter/lib/models/web_model.dart b/flutter/lib/models/web_model.dart index 59a0e610e..d3f1bacad 100644 --- a/flutter/lib/models/web_model.dart +++ b/flutter/lib/models/web_model.dart @@ -20,7 +20,12 @@ class PlatformFFI { context.callMethod('setByName', [name, value]); } - static Future init() async { + PlatformFFI._(); + static final PlatformFFI instance = PlatformFFI._(); + + static get localeName => window.navigator.language; + + static Future init(String _appType) async { isWeb = true; isWebDesktop = !context.callMethod('isMobile'); context.callMethod('init'); @@ -68,5 +73,3 @@ class PlatformFFI { return true; } } - -final localeName = window.navigator.language; diff --git a/flutter/linux/CMakeLists.txt b/flutter/linux/CMakeLists.txt index 28f309c7f..9f6d0ce52 100644 --- a/flutter/linux/CMakeLists.txt +++ b/flutter/linux/CMakeLists.txt @@ -115,9 +115,9 @@ include(flutter/generated_plugins.cmake) # By default, "installing" just makes a relocatable bundle in the build # directory. set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") -if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) +#if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) -endif() +#endif() # Start with a clean build bundle directory every time. install(CODE " diff --git a/src/flutter.rs b/src/flutter.rs index edd972f68..a8e0224eb 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -31,10 +31,14 @@ use hbb_common::{ use crate::common::make_fd_to_json; use crate::{client::*, flutter_ffi::EventToUI, make_fd_flutter}; +pub(super) const APP_TYPE_MAIN: &str = "main"; +pub(super) const APP_TYPE_DESKTOP_REMOTE: &str = "remote"; +pub(super) const APP_TYPE_DESKTOP_FILE_TRANSFER: &str = "file transfer"; + lazy_static::lazy_static! { // static ref SESSION: Arc>> = Default::default(); pub static ref SESSIONS: RwLock> = Default::default(); - pub static ref GLOBAL_EVENT_STREAM: RwLock>> = Default::default(); // rust to dart event channel + pub static ref GLOBAL_EVENT_STREAM: RwLock>> = Default::default(); // rust to dart event channel } // pub fn get_session<'a>(id: &str) -> Option<&'a Session> { @@ -786,113 +790,114 @@ impl Connection { vec![("x", &cp.x.to_string()), ("y", &cp.y.to_string())], ); } - Some(message::Union::FileResponse(fr)) => match fr.union { - Some(file_response::Union::Dir(fd)) => { - let mut entries = fd.entries.to_vec(); - if self.session.peer_platform() == "Windows" { - fs::transform_windows_path(&mut entries); - } - let id = fd.id; - self.session.push_event( - "file_dir", - vec![("value", &make_fd_to_json(fd)), ("is_local", "false")], - ); - if let Some(job) = fs::get_job(id, &mut self.write_jobs) { - job.set_files(entries); - } - } - Some(file_response::Union::Block(block)) => { - if let Some(job) = fs::get_job(block.id, &mut self.write_jobs) { - if let Err(_err) = job.write(block, None).await { - // to-do: add "skip" for writing job + Some(message::Union::FileResponse(fr)) => { + match fr.union { + Some(file_response::Union::Dir(fd)) => { + let mut entries = fd.entries.to_vec(); + if self.session.peer_platform() == "Windows" { + fs::transform_windows_path(&mut entries); + } + let id = fd.id; + self.session.push_event( + "file_dir", + vec![("value", &make_fd_to_json(fd)), ("is_local", "false")], + ); + if let Some(job) = fs::get_job(id, &mut self.write_jobs) { + job.set_files(entries); } - self.update_jobs_status(); } - } - Some(file_response::Union::Done(d)) => { - if let Some(job) = fs::get_job(d.id, &mut self.write_jobs) { - job.modify_time(); - fs::remove_job(d.id, &mut self.write_jobs); + Some(file_response::Union::Block(block)) => { + if let Some(job) = fs::get_job(block.id, &mut self.write_jobs) { + if let Err(_err) = job.write(block, None).await { + // to-do: add "skip" for writing job + } + self.update_jobs_status(); + } } - self.handle_job_status(d.id, d.file_num, None); - } - Some(file_response::Union::Error(e)) => { - self.handle_job_status(e.id, e.file_num, Some(e.error)); - } - Some(file_response::Union::Digest(digest)) => { - if digest.is_upload { - if let Some(job) = fs::get_job(digest.id, &mut self.read_jobs) { - if let Some(file) = job.files().get(digest.file_num as usize) { - let read_path = get_string(&job.join(&file.name)); - let overwrite_strategy = job.default_overwrite_strategy(); - if let Some(overwrite) = overwrite_strategy { - let req = FileTransferSendConfirmRequest { - id: digest.id, - file_num: digest.file_num, - union: Some(if overwrite { - file_transfer_send_confirm_request::Union::OffsetBlk(0) - } else { - file_transfer_send_confirm_request::Union::Skip( - true, - ) - }), - ..Default::default() - }; - job.confirm(&req); - let msg = new_send_confirm(req); - allow_err!(peer.send(&msg).await); - } else { - self.handle_override_file_confirm( - digest.id, - digest.file_num, - read_path, - true, - ); + Some(file_response::Union::Done(d)) => { + if let Some(job) = fs::get_job(d.id, &mut self.write_jobs) { + job.modify_time(); + fs::remove_job(d.id, &mut self.write_jobs); + } + self.handle_job_status(d.id, d.file_num, None); + } + Some(file_response::Union::Error(e)) => { + self.handle_job_status(e.id, e.file_num, Some(e.error)); + } + Some(file_response::Union::Digest(digest)) => { + if digest.is_upload { + if let Some(job) = fs::get_job(digest.id, &mut self.read_jobs) { + if let Some(file) = job.files().get(digest.file_num as usize) { + let read_path = get_string(&job.join(&file.name)); + let overwrite_strategy = job.default_overwrite_strategy(); + if let Some(overwrite) = overwrite_strategy { + let req = FileTransferSendConfirmRequest { + id: digest.id, + file_num: digest.file_num, + union: Some(if overwrite { + file_transfer_send_confirm_request::Union::OffsetBlk(0) + } else { + file_transfer_send_confirm_request::Union::Skip( + true, + ) + }), + ..Default::default() + }; + job.confirm(&req); + let msg = new_send_confirm(req); + allow_err!(peer.send(&msg).await); + } else { + self.handle_override_file_confirm( + digest.id, + digest.file_num, + read_path, + true, + ); + } } } - } - } else { - if let Some(job) = fs::get_job(digest.id, &mut self.write_jobs) { - if let Some(file) = job.files().get(digest.file_num as usize) { - let write_path = get_string(&job.join(&file.name)); - let overwrite_strategy = job.default_overwrite_strategy(); - match fs::is_write_need_confirmation(&write_path, &digest) { - Ok(res) => match res { - DigestCheckResult::IsSame => { - let msg= new_send_confirm(FileTransferSendConfirmRequest { + } else { + if let Some(job) = fs::get_job(digest.id, &mut self.write_jobs) { + if let Some(file) = job.files().get(digest.file_num as usize) { + let write_path = get_string(&job.join(&file.name)); + let overwrite_strategy = job.default_overwrite_strategy(); + match fs::is_write_need_confirmation(&write_path, &digest) { + Ok(res) => match res { + DigestCheckResult::IsSame => { + let msg= new_send_confirm(FileTransferSendConfirmRequest { id: digest.id, file_num: digest.file_num, union: Some(file_transfer_send_confirm_request::Union::Skip(true)), ..Default::default() }); - self.session.send_msg(msg); - } - DigestCheckResult::NeedConfirm(digest) => { - if let Some(overwrite) = overwrite_strategy { - let msg = new_send_confirm( - FileTransferSendConfirmRequest { - id: digest.id, - file_num: digest.file_num, - union: Some(if overwrite { - file_transfer_send_confirm_request::Union::OffsetBlk(0) - } else { - file_transfer_send_confirm_request::Union::Skip(true) - }), - ..Default::default() - }, - ); self.session.send_msg(msg); - } else { - self.handle_override_file_confirm( - digest.id, - digest.file_num, - write_path.to_string(), - false, - ); } - } - DigestCheckResult::NoSuchFile => { - let msg = new_send_confirm( + DigestCheckResult::NeedConfirm(digest) => { + if let Some(overwrite) = overwrite_strategy { + let msg = new_send_confirm( + FileTransferSendConfirmRequest { + id: digest.id, + file_num: digest.file_num, + union: Some(if overwrite { + file_transfer_send_confirm_request::Union::OffsetBlk(0) + } else { + file_transfer_send_confirm_request::Union::Skip(true) + }), + ..Default::default() + }, + ); + self.session.send_msg(msg); + } else { + self.handle_override_file_confirm( + digest.id, + digest.file_num, + write_path.to_string(), + false, + ); + } + } + DigestCheckResult::NoSuchFile => { + let msg = new_send_confirm( FileTransferSendConfirmRequest { id: digest.id, file_num: digest.file_num, @@ -900,19 +905,20 @@ impl Connection { ..Default::default() }, ); - self.session.send_msg(msg); + self.session.send_msg(msg); + } + }, + Err(err) => { + println!("error recving digest: {}", err); } - }, - Err(err) => { - println!("error recving digest: {}", err); } } } } } + _ => {} } - _ => {} - }, + } Some(message::Union::Misc(misc)) => match misc.union { Some(misc::Union::AudioFormat(f)) => { self.audio_handler.handle_format(f); // @@ -1513,7 +1519,11 @@ pub mod connection_manager { assert!(h.get("name").is_none()); h.insert("name", name); - if let Some(s) = GLOBAL_EVENT_STREAM.read().unwrap().as_ref() { + if let Some(s) = GLOBAL_EVENT_STREAM + .read() + .unwrap() + .get(super::APP_TYPE_MAIN) + { s.add(serde_json::ser::to_string(&h).unwrap_or("".to_owned())); }; } diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 413e3cde3..2d7f4be65 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -75,11 +75,17 @@ pub enum EventToUI { Rgba(ZeroCopyBuffer>), } -pub fn start_global_event_stream(s: StreamSink) -> ResultType<()> { - let _ = flutter::GLOBAL_EVENT_STREAM.write().unwrap().insert(s); +pub fn start_global_event_stream(s: StreamSink, app_type: String) -> ResultType<()> { + if let Some(_) = flutter::GLOBAL_EVENT_STREAM.write().unwrap().insert(app_type.clone(), s) { + log::warn!("Global event stream of type {} is started before, but now removed", app_type); + } Ok(()) } +pub fn stop_global_event_stream(app_type: String) { + let _ = flutter::GLOBAL_EVENT_STREAM.write().unwrap().remove(&app_type); +} + pub fn host_stop_system_key_propagate(stopped: bool) { #[cfg(windows)] crate::platform::windows::stop_system_key_propagate(stopped); @@ -518,7 +524,7 @@ pub fn main_load_recent_peers() { .drain(..) .map(|(id, _, p)| (id, p.info)) .collect(); - if let Some(s) = flutter::GLOBAL_EVENT_STREAM.read().unwrap().as_ref() { + if let Some(s) = flutter::GLOBAL_EVENT_STREAM.read().unwrap().get(flutter::APP_TYPE_MAIN) { let data = HashMap::from([ ("name", "load_recent_peers".to_owned()), ( @@ -544,7 +550,7 @@ pub fn main_load_fav_peers() { } }) .collect(); - if let Some(s) = flutter::GLOBAL_EVENT_STREAM.read().unwrap().as_ref() { + if let Some(s) = flutter::GLOBAL_EVENT_STREAM.read().unwrap().get(flutter::APP_TYPE_MAIN) { let data = HashMap::from([ ("name", "load_fav_peers".to_owned()), ( @@ -558,7 +564,7 @@ pub fn main_load_fav_peers() { } pub fn main_load_lan_peers() { - if let Some(s) = flutter::GLOBAL_EVENT_STREAM.read().unwrap().as_ref() { + if let Some(s) = flutter::GLOBAL_EVENT_STREAM.read().unwrap().get(flutter::APP_TYPE_MAIN) { let data = HashMap::from([ ("name", "load_lan_peers".to_owned()), ("peers", get_lan_peers()), @@ -1066,7 +1072,7 @@ unsafe extern "C" fn set_by_name(name: *const c_char, value: *const c_char) { } fn handle_query_onlines(onlines: Vec, offlines: Vec) { - if let Some(s) = flutter::GLOBAL_EVENT_STREAM.read().unwrap().as_ref() { + if let Some(s) = flutter::GLOBAL_EVENT_STREAM.read().unwrap().get(flutter::APP_TYPE_MAIN) { let data = HashMap::from([ ("name", "callback_query_onlines".to_owned()), ("onlines", onlines.join(",")), From 0488eb31f5338c679aee9f52bc3ed06cd89a2317 Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 3 Aug 2022 22:13:40 +0800 Subject: [PATCH 0147/2015] flutter_desktop: remove unnecessary control flow Signed-off-by: fufesou --- flutter/lib/models/ab_model.dart | 2 +- flutter/lib/models/file_model.dart | 16 ++++++---------- flutter/lib/models/user_model.dart | 2 +- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/flutter/lib/models/ab_model.dart b/flutter/lib/models/ab_model.dart index b9740ed8f..18bb73c3f 100644 --- a/flutter/lib/models/ab_model.dart +++ b/flutter/lib/models/ab_model.dart @@ -47,7 +47,7 @@ class AbModel with ChangeNotifier { } Future getApiServer() async { - return await bind.mainGetApiServer() ?? ""; + return await bind.mainGetApiServer(); } void reset() { diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index e86ac1de2..35deabac5 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -292,14 +292,12 @@ class FileModel extends ChangeNotifier { onReady() async { _localOption.home = _ffi.target?.getByName("get_home_dir") ?? ""; _localOption.showHidden = (await bind.sessionGetPeerOption( - id: _ffi.target?.id ?? "", name: "local_show_hidden")) - ?.isNotEmpty ?? - false; + id: _ffi.target?.id ?? "", name: "local_show_hidden")) + .isNotEmpty; _remoteOption.showHidden = (await bind.sessionGetPeerOption( - id: _ffi.target?.id ?? "", name: "remote_show_hidden")) - ?.isNotEmpty ?? - false; + id: _ffi.target?.id ?? "", name: "remote_show_hidden")) + .isNotEmpty; _remoteOption.isWindows = _ffi.target?.ffiModel.pi.platform == "Windows"; debugPrint("remote platform: ${_ffi.target?.ffiModel.pi.platform}"); @@ -307,11 +305,9 @@ class FileModel extends ChangeNotifier { await Future.delayed(Duration(milliseconds: 100)); final local = (await bind.sessionGetPeerOption( - id: _ffi.target?.id ?? "", name: "local_dir")) ?? - ""; + id: _ffi.target?.id ?? "", name: "local_dir")); final remote = (await bind.sessionGetPeerOption( - id: _ffi.target?.id ?? "", name: "remote_dir")) ?? - ""; + id: _ffi.target?.id ?? "", name: "remote_dir")); openDirectory(local.isEmpty ? _localOption.home : local, isLocal: true); openDirectory(remote.isEmpty ? _remoteOption.home : remote, isLocal: false); await Future.delayed(Duration(seconds: 1)); diff --git a/flutter/lib/models/user_model.dart b/flutter/lib/models/user_model.dart index 539211664..b43b4510b 100644 --- a/flutter/lib/models/user_model.dart +++ b/flutter/lib/models/user_model.dart @@ -18,7 +18,7 @@ class UserModel extends ChangeNotifier { if (userName.isNotEmpty) { return userName.value; } - final userInfo = await bind.mainGetLocalOption(key: 'user_info') ?? "{}"; + final userInfo = await bind.mainGetLocalOption(key: 'user_info'); if (userInfo.trim().isEmpty) { return ""; } From 3ff2f60fb7b088963b996431f6fb75ddb1df23b3 Mon Sep 17 00:00:00 2001 From: csf Date: Thu, 4 Aug 2022 17:24:02 +0800 Subject: [PATCH 0148/2015] Merge master --- README-FI.md | 14 +- flutter/android/app/build.gradle | 2 +- flutter/lib/common.dart | 21 +- flutter/lib/mobile/pages/chat_page.dart | 27 +- .../lib/mobile/pages/file_manager_page.dart | 1 + flutter/lib/mobile/pages/home_page.dart | 15 +- flutter/lib/mobile/pages/remote_page.dart | 77 ++++- flutter/lib/mobile/pages/server_page.dart | 46 +-- flutter/lib/mobile/pages/settings_page.dart | 50 ++- flutter/lib/mobile/widgets/overlay.dart | 4 +- flutter/lib/models/chat_model.dart | 45 +-- flutter/lib/models/file_model.dart | 1 + flutter/lib/models/model.dart | 54 +++- flutter/pubspec.lock | 158 +++++++-- flutter/pubspec.yaml | 9 +- libs/hbb_common/protos/message.proto | 1 + libs/hbb_common/src/fs.rs | 8 +- src/client.rs | 22 +- src/client/helper.rs | 14 +- src/flutter.rs | 283 +++++++++------- src/flutter_ffi.rs | 59 ++-- src/ipc.rs | 1 + src/lang.rs | 21 +- src/lang/ja.rs | 303 ++++++++++++++++++ src/server/connection.rs | 11 +- src/ui/cm.rs | 4 +- src/ui/remote.rs | 45 ++- 27 files changed, 1015 insertions(+), 281 deletions(-) create mode 100644 src/lang/ja.rs diff --git a/README-FI.md b/README-FI.md index 1258bb550..5f38d2e42 100644 --- a/README-FI.md +++ b/README-FI.md @@ -13,7 +13,7 @@ Juttele meidän kanssa: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](htt [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) -Vielä yksi etätyöpöytäohjelmisto, ohjelmoitu Rust-kielellä. Toimii suoraan pakkauksesta, ei tarvitse asetuksia. Hallitset täysin tietojasi, ei tarvitse murehtia turvallisuutta. Voit käyttää meidän rendezvous/relay-palvelinta, [aseta omasi](https://rustdesk.com/server), tai [kirjoita oma rendezvous/relay-palvelin](https://github.com/rustdesk/rustdesk-server-demo). +Vielä yksi etätyöpöytäohjelmisto, ohjelmoitu Rust-kielellä. Toimii suoraan pakkauksesta, ei tarvitse asetusta. Hallitset täysin tietojasi, ei tarvitse murehtia turvallisuutta. Voit käyttää meidän rendezvous/relay-palvelinta, [aseta omasi](https://rustdesk.com/server), tai [kirjoittaa oma rendezvous/relay-palvelin](https://github.com/rustdesk/rustdesk-server-demo). RustDesk toivottaa avustukset tervetulleiksi kaikilta. Katso lisätietoja [`CONTRIBUTING.md`](CONTRIBUTING.md) avun saamiseksi. @@ -45,9 +45,9 @@ Desktop-versiot käyttävät [sciter](https://sciter.com/) graafisena käyttöli - Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static - Linux/MacOS: vcpkg install libvpx libyuv opus -- aja `cargo run` +- suorita `cargo run` -## Kuinka rakentaa Linuxissa +## Kuinka rakentaa Linux:issa ### Ubuntu 18 (Debian 10) @@ -79,7 +79,7 @@ export VCPKG_ROOT=$HOME/vcpkg vcpkg/vcpkg install libvpx libyuv opus ``` -### Korjaa libvpx (Fedora-linux-versiota varten) +### Korjaa libvpx (Fedora) ```sh cd vcpkg/buildtrees/libvpx/src @@ -107,7 +107,7 @@ VCPKG_ROOT=$HOME/vcpkg cargo run ### Vaihda Wayland-ympäristö X11 (Xorg)-ympäristöön -RustDesk ei tue Waylandia. Tarkista [tämä](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) asettamaan Xorg oletus GNOME-istuntona. +RustDesk ei tue Waylandia. Tarkista [tämä](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) asettamalla Xorg oletus GNOME-istuntoon. ## Kuinka rakennetaan Dockerin kanssa @@ -119,13 +119,13 @@ cd rustdesk docker build -t "rustdesk-builder" . ``` -Sitten, joka kerta kun sinun on rakennettava sovellus, aja seuraava komento: +Sitten, joka kerta kun sinun on rakennettava sovellus, suorita seuraava komento: ```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 ``` -Huomaa, että ensimmäinen rakentaminen saattaa kestää pitempään ennen kuin riippuvuudet on siirretty välimuistiin, seuraavat rakentamiset ovat nopeampia. Lisäksi, jos sinun on määritettävä eri argumentteja rakentamiskomennolle, saatat tehdä sen niin, että komennon lopussa `-kohdassa. Esimerkiksi, jos haluat rakentaa optimoidun julkaisuversion, sinun on ajettava komento yllä siten, että sitä seuraa argumentti`--release`. Suoritettava tiedosto on saatavilla järjestelmäsi kohdehakemistossa, ja se voidaan suorittaa seuraavan kera: +Huomaa, että ensimmäinen rakentaminen saattaa kestää pitempään ennen kuin riippuvuudet on siirretty välimuistiin, seuraavat rakentamiset ovat nopeampia. Lisäksi, jos sinun on määritettävä eri väittämiä rakentamiskomennolle, saatat tehdä sen niin, että komennon lopussa `-kohdassa. Esimerkiksi, jos haluat rakentaa optimoidun julkaisuversion, sinun on ajettava komento yllä siten, että sitä seuraa väittämä`--release`. Suoritettava tiedosto on saatavilla järjestelmäsi kohdehakemistossa, ja se voidaan suorittaa seuraavan kera: ```sh target/debug/rustdesk diff --git a/flutter/android/app/build.gradle b/flutter/android/app/build.gradle index 79bf6426a..a2a1a02a3 100644 --- a/flutter/android/app/build.gradle +++ b/flutter/android/app/build.gradle @@ -32,7 +32,7 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 31 + compileSdkVersion 32 sourceSets { main.java.srcDirs += 'src/main/kotlin' } diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index eda1ed4e7..af87520c9 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -119,9 +119,9 @@ class DialogManager { static Future show(DialogBuilder builder, {bool clickMaskDismiss = false, - bool backDismiss = false, - String? tag, - bool useAnimation = true}) async { + bool backDismiss = false, + String? tag, + bool useAnimation = true}) async { final t; if (tag != null) { t = tag; @@ -146,10 +146,11 @@ class DialogManager { } class CustomAlertDialog extends StatelessWidget { - CustomAlertDialog({required this.title, - required this.content, - required this.actions, - this.contentPadding}); + CustomAlertDialog( + {required this.title, + required this.content, + required this.actions, + this.contentPadding}); final Widget title; final Widget content; @@ -162,7 +163,7 @@ class CustomAlertDialog extends StatelessWidget { scrollable: true, title: title, contentPadding: - EdgeInsets.symmetric(horizontal: contentPadding ?? 25, vertical: 10), + EdgeInsets.symmetric(horizontal: contentPadding ?? 25, vertical: 10), content: content, actions: actions, ); @@ -361,7 +362,9 @@ late FFI _globalFFI; FFI get gFFI => _globalFFI; Future initGlobalFFI() async { + debugPrint("_globalFFI init"); _globalFFI = FFI(); + debugPrint("_globalFFI init end"); // after `put`, can also be globally found by Get.find(); Get.put(_globalFFI, permanent: true); await _globalFFI.ffiModel.init(); @@ -369,4 +372,4 @@ Future initGlobalFFI() async { await _globalFFI.bind.mainCheckConnectStatus(); // global shared preference await Get.putAsync(() => SharedPreferences.getInstance()); -} \ No newline at end of file +} diff --git a/flutter/lib/mobile/pages/chat_page.dart b/flutter/lib/mobile/pages/chat_page.dart index c5beda6f1..a49a02bb4 100644 --- a/flutter/lib/mobile/pages/chat_page.dart +++ b/flutter/lib/mobile/pages/chat_page.dart @@ -1,4 +1,4 @@ -import 'package:dash_chat/dash_chat.dart'; +import 'package:dash_chat_2/dash_chat_2.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/models/chat_model.dart'; @@ -7,8 +7,6 @@ import 'package:provider/provider.dart'; import '../../models/model.dart'; import 'home_page.dart'; -ChatPage chatPage = ChatPage(); - class ChatPage extends StatelessWidget implements PageShape { @override final title = translate("Chat"); @@ -26,7 +24,7 @@ class ChatPage extends StatelessWidget implements PageShape { final id = entry.key; final user = entry.value.chatUser; return PopupMenuItem( - child: Text("${user.name} ${user.uid}"), + child: Text("${user.firstName} ${user.id}"), value: id, ); }).toList(); @@ -47,19 +45,24 @@ class ChatPage extends StatelessWidget implements PageShape { return Stack( children: [ DashChat( - inputContainerStyle: BoxDecoration(color: Colors.white70), - sendOnEnter: false, - // if true,reload keyboard everytime,need fix onSend: (chatMsg) { chatModel.send(chatMsg); }, - user: chatModel.me, + currentUser: chatModel.me, messages: chatModel.messages[chatModel.currentID]?.chatMessages ?? [], - // default scrollToBottom has bug https://github.com/fayeed/dash_chat/issues/53 - scrollToBottom: false, - scrollController: chatModel.scroller, + messageOptions: MessageOptions( + showOtherUsersAvatar: false, + showTime: true, + messageDecorationBuilder: (_, __, ___) => + defaultMessageDecoration( + color: MyTheme.accent80, + borderTopLeft: 8, + borderTopRight: 8, + borderBottomRight: 8, + borderBottomLeft: 8, + )), ), chatModel.currentID == ChatModel.clientModeID ? SizedBox.shrink() @@ -71,7 +74,7 @@ class ChatPage extends StatelessWidget implements PageShape { color: MyTheme.accent80), SizedBox(width: 5), Text( - "${currentUser.name ?? ""} ${currentUser.uid ?? ""}", + "${currentUser.firstName} ${currentUser.id}", style: TextStyle(color: MyTheme.accent50), ), ], diff --git a/flutter/lib/mobile/pages/file_manager_page.dart b/flutter/lib/mobile/pages/file_manager_page.dart index 1f588a461..9a8d0088a 100644 --- a/flutter/lib/mobile/pages/file_manager_page.dart +++ b/flutter/lib/mobile/pages/file_manager_page.dart @@ -29,6 +29,7 @@ class _FileManagerPageState extends State { void initState() { super.initState(); gFFI.connect(widget.id, isFileTransfer: true); + showLoading(translate('Connecting...')); gFFI.ffiModel.updateEventListener(widget.id); Wakelock.enable(); } diff --git a/flutter/lib/mobile/pages/home_page.dart b/flutter/lib/mobile/pages/home_page.dart index 756df7f91..e56434487 100644 --- a/flutter/lib/mobile/pages/home_page.dart +++ b/flutter/lib/mobile/pages/home_page.dart @@ -12,6 +12,8 @@ abstract class PageShape extends Widget { final List appBarActions = []; } +final homeKey = GlobalKey<_HomePageState>(); + class HomePage extends StatefulWidget { HomePage({Key? key}) : super(key: key); @@ -23,12 +25,23 @@ class _HomePageState extends State { var _selectedIndex = 0; final List _pages = []; + void refreshPages() { + setState(() { + initPages(); + }); + } + @override void initState() { super.initState(); + initPages(); + } + + void initPages() { + _pages.clear(); _pages.add(ConnectionPage()); if (isAndroid) { - _pages.addAll([chatPage, ServerPage()]); + _pages.addAll([ChatPage(), ServerPage()]); } _pages.add(SettingsPage()); } diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index df1a91a79..980f665e7 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -595,6 +595,7 @@ class _RemotePageState extends State { child: Stack(children: [ ImagePaint(), CursorPaint(), + QualityMonitor(), getHelpTools(), SizedBox( width: 0, @@ -662,7 +663,7 @@ class _RemotePageState extends State { more.add(PopupMenuItem( child: Row( children: ([ - Container(width: 100.0, child: Text(translate('OS Password'))), + Text(translate('OS Password')), TextButton( style: flatButtonStyle, onPressed: () { @@ -697,6 +698,13 @@ class _RemotePageState extends State { value: 'block-input')); } } + if (gFFI.ffiModel.permissions["restart"] != false && + (pi.platform == "Linux" || + pi.platform == "Windows" || + pi.platform == "Mac OS")) { + more.add(PopupMenuItem( + child: Text(translate('Restart Remote Device')), value: 'restart')); + } () async { var value = await showMenu( context: context, @@ -730,6 +738,8 @@ class _RemotePageState extends State { } } else if (value == 'reset_canvas') { gFFI.cursorModel.reset(); + } else if (value == 'restart') { + showRestartRemoteDevice(pi, widget.id); } }(); } @@ -952,6 +962,47 @@ class ImagePainter extends CustomPainter { } } +class QualityMonitor extends StatelessWidget { + @override + Widget build(BuildContext context) => ChangeNotifierProvider.value( + value: gFFI.qualityMonitorModel, + child: Consumer( + builder: (context, qualityMonitorModel, child) => Positioned( + top: 10, + right: 10, + child: qualityMonitorModel.show + ? Container( + padding: EdgeInsets.all(8), + color: MyTheme.canvasColor.withAlpha(120), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Speed: ${qualityMonitorModel.data.speed}", + style: TextStyle(color: MyTheme.grayBg), + ), + Text( + "FPS: ${qualityMonitorModel.data.fps}", + style: TextStyle(color: MyTheme.grayBg), + ), + Text( + "Delay: ${qualityMonitorModel.data.delay} ms", + style: TextStyle(color: MyTheme.grayBg), + ), + Text( + "Target Bitrate: ${qualityMonitorModel.data.targetBitrate}kb", + style: TextStyle(color: MyTheme.grayBg), + ), + Text( + "Codec: ${qualityMonitorModel.data.codecFormat}", + style: TextStyle(color: MyTheme.grayBg), + ), + ], + ), + ) + : SizedBox.shrink()))); +} + CheckboxListTile getToggle( void Function(void Function()) setState, option, name) { return CheckboxListTile( @@ -960,6 +1011,9 @@ CheckboxListTile getToggle( setState(() { gFFI.setByName('toggle_option', option); }); + if (option == "show-quality-monitor") { + gFFI.qualityMonitorModel.checkShowQualityMonitor(); + } }, dense: true, title: Text(translate(name))); @@ -1062,6 +1116,27 @@ void showOptions() { }, clickMaskDismiss: true, backDismiss: true); } +void showRestartRemoteDevice(PeerInfo pi, String id) async { + final res = + await DialogManager.show((setState, close) => CustomAlertDialog( + title: Row(children: [ + Icon(Icons.warning_amber_sharp, + color: Colors.redAccent, size: 28), + SizedBox(width: 10), + Text(translate("Restart Remote Device")), + ]), + content: Text( + "${translate('Are you sure you want to restart')} \n${pi.username}@${pi.hostname}($id) ?"), + actions: [ + TextButton( + onPressed: () => close(), child: Text(translate("Cancel"))), + ElevatedButton( + onPressed: () => close(true), child: Text(translate("OK"))), + ], + )); + if (res == true) gFFI.setByName('restart_remote_device'); +} + void showSetOSPassword(bool login) { final controller = TextEditingController(); var password = gFFI.getByName('peer_option', "os-password"); diff --git a/flutter/lib/mobile/pages/server_page.dart b/flutter/lib/mobile/pages/server_page.dart index b5a6dd1c9..19753bcac 100644 --- a/flutter/lib/mobile/pages/server_page.dart +++ b/flutter/lib/mobile/pages/server_page.dart @@ -200,7 +200,8 @@ class ServerInfo extends StatelessWidget { Icon(Icons.warning_amber_sharp, color: Colors.redAccent, size: 24), SizedBox(width: 10), - Text( + Expanded( + child: Text( translate("Service is not running"), style: TextStyle( fontFamily: 'WorkSans', @@ -208,7 +209,7 @@ class ServerInfo extends StatelessWidget { fontSize: 18, color: MyTheme.accent80, ), - ) + )) ], )), SizedBox(height: 5), @@ -316,30 +317,35 @@ class PermissionRow extends StatelessWidget { @override Widget build(BuildContext context) { return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row( - children: [ - SizedBox( - width: 140, + Expanded( + flex: 5, + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: Alignment.centerLeft, child: Text(name, - style: TextStyle(fontSize: 16.0, color: MyTheme.accent50))), - SizedBox( - width: 50, + style: + TextStyle(fontSize: 16.0, color: MyTheme.accent50)))), + Expanded( + flex: 2, + child: FittedBox( + fit: BoxFit.scaleDown, child: Text(isOk ? translate("ON") : translate("OFF"), style: TextStyle( fontSize: 16.0, - color: isOk ? Colors.green : Colors.grey)), - ) - ], + color: isOk ? Colors.green : Colors.grey))), ), - TextButton( - onPressed: onPressed, - child: Text( - translate(isOk ? "CLOSE" : "OPEN"), - style: TextStyle(fontWeight: FontWeight.bold), - )), - const Divider(height: 0) + Expanded( + flex: 3, + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: Alignment.centerRight, + child: TextButton( + onPressed: onPressed, + child: Text( + translate(isOk ? "CLOSE" : "OPEN"), + style: TextStyle(fontWeight: FontWeight.bold), + )))), ], ); } diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index 53583479f..ab7b2584d 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -27,11 +27,11 @@ class SettingsPage extends StatefulWidget implements PageShape { _SettingsState createState() => _SettingsState(); } -class _SettingsState extends State with WidgetsBindingObserver { - static const url = 'https://rustdesk.com/'; - final _hasIgnoreBattery = androidVersion >= 26; - var _ignoreBatteryOpt = false; +const url = 'https://rustdesk.com/'; +final _hasIgnoreBattery = androidVersion >= 26; +var _ignoreBatteryOpt = false; +class _SettingsState extends State with WidgetsBindingObserver { @override void initState() { super.initState(); @@ -147,6 +147,12 @@ class _SettingsState extends State with WidgetsBindingObserver { leading: Icon(Icons.cloud), onPressed: (context) { showServerSettings(); + }), + SettingsTile.navigation( + title: Text(translate('Language')), + leading: Icon(Icons.translate), + onPressed: (context) { + showLanguageSettings(); }) ]), SettingsSection( @@ -186,6 +192,42 @@ void showServerSettings() { showServerSettingsWithValue(id, relay, key, api); } +void showLanguageSettings() { + try { + final langs = json.decode(gFFI.getByName('langs')) as List; + var lang = gFFI.getByName('local_option', 'lang'); + DialogManager.show((setState, close) { + final setLang = (v) { + if (lang != v) { + setState(() { + lang = v; + }); + final msg = Map() + ..['name'] = 'lang' + ..['value'] = v; + gFFI.setByName('local_option', json.encode(msg)); + homeKey.currentState?.refreshPages(); + Future.delayed(Duration(milliseconds: 200), close); + } + }; + return CustomAlertDialog( + title: SizedBox.shrink(), + content: Column( + children: [ + getRadio('Default', '', lang, setLang), + Divider(color: MyTheme.border), + ] + + langs.map((e) { + final key = e[0] as String; + final name = e[1] as String; + return getRadio(name, key, lang, setLang); + }).toList(), + ), + actions: []); + }, backDismiss: true, clickMaskDismiss: true); + } catch (_e) {} +} + void showAbout() { DialogManager.show((setState, close) { return CustomAlertDialog( diff --git a/flutter/lib/mobile/widgets/overlay.dart b/flutter/lib/mobile/widgets/overlay.dart index 5b44d445b..362f62974 100644 --- a/flutter/lib/mobile/widgets/overlay.dart +++ b/flutter/lib/mobile/widgets/overlay.dart @@ -27,7 +27,7 @@ class DraggableChatWindow extends StatelessWidget { height: height, builder: (_, onPanUpdate) { return isIOS - ? chatPage + ? ChatPage() : Scaffold( resizeToAvoidBottomInset: false, appBar: CustomAppBar( @@ -68,7 +68,7 @@ class DraggableChatWindow extends StatelessWidget { ), ), ), - body: chatPage, + body: ChatPage(), ); }); } diff --git a/flutter/lib/models/chat_model.dart b/flutter/lib/models/chat_model.dart index 0eb6db279..ad572c164 100644 --- a/flutter/lib/models/chat_model.dart +++ b/flutter/lib/models/chat_model.dart @@ -1,6 +1,6 @@ import 'dart:convert'; -import 'package:dash_chat/dash_chat.dart'; +import 'package:dash_chat_2/dash_chat_2.dart'; import 'package:flutter/material.dart'; import '../../mobile/widgets/overlay.dart'; @@ -11,8 +11,8 @@ class MessageBody { List chatMessages; MessageBody(this.chatUser, this.chatMessages); - void add(ChatMessage cm) { - this.chatMessages.add(cm); + void insert(ChatMessage cm) { + this.chatMessages.insert(0, cm); } void clear() { @@ -24,19 +24,15 @@ class ChatModel with ChangeNotifier { static final clientModeID = -1; final ChatUser me = ChatUser( - uid: "", - name: "Me", + id: "", + firstName: "Me", ); late final Map _messages = Map() ..[clientModeID] = MessageBody(me, []); - final _scroller = ScrollController(); - var _currentID = clientModeID; - ScrollController get scroller => _scroller; - Map get messages => _messages; int get currentID => _currentID; @@ -67,8 +63,8 @@ class ChatModel with ChangeNotifier { "Failed to changeCurrentID,remote user doesn't exist"); } final chatUser = ChatUser( - uid: client.peerId, - name: client.name, + id: client.peerId, + firstName: client.name, ); _messages[id] = MessageBody(chatUser, []); _currentID = id; @@ -85,48 +81,39 @@ class ChatModel with ChangeNotifier { late final chatUser; if (id == clientModeID) { chatUser = ChatUser( - name: _ffi.target?.ffiModel.pi.username, - uid: _ffi.target?.getId(), + firstName: _ffi.target?.ffiModel.pi.username, + id: _ffi.target?.getId() ?? "", ); } else { final client = _ffi.target?.serverModel.clients[id]; if (client == null) { return debugPrint("Failed to receive msg,user doesn't exist"); } - chatUser = ChatUser(uid: client.peerId, name: client.name); + chatUser = ChatUser(id: client.peerId, firstName: client.name); } if (!_messages.containsKey(id)) { _messages[id] = MessageBody(chatUser, []); } - _messages[id]!.add(ChatMessage(text: text, user: chatUser)); + _messages[id]!.insert( + ChatMessage(text: text, user: chatUser, createdAt: DateTime.now())); _currentID = id; notifyListeners(); - scrollToBottom(); - } - - scrollToBottom() { - Future.delayed(Duration(milliseconds: 500), () { - _scroller.animateTo(_scroller.position.maxScrollExtent, - duration: Duration(milliseconds: 200), - curve: Curves.fastLinearToSlowEaseIn); - }); } send(ChatMessage message) { - if (message.text != null && message.text!.isNotEmpty) { - _messages[_currentID]?.add(message); + if (message.text.isNotEmpty) { + _messages[_currentID]?.insert(message); if (_currentID == clientModeID) { - _ffi.target?.setByName("chat_client_mode", message.text!); + _ffi.target?.setByName("chat_client_mode", message.text); } else { final msg = Map() ..["id"] = _currentID - ..["text"] = message.text!; + ..["text"] = message.text; _ffi.target?.setByName("chat_server_mode", jsonEncode(msg)); } } notifyListeners(); - scrollToBottom(); } close() { diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index 5bca33303..9433b3566 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -318,6 +318,7 @@ class FileModel extends ChangeNotifier { onClose() { SmartDialog.dismiss(); + jobReset(); // save config Map msgMap = Map(); diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 11415eeef..76fa72687 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -84,7 +84,7 @@ class FfiModel with ChangeNotifier { void updatePermission(Map evt) { evt.forEach((k, v) { - if (k == 'name') return; + if (k == 'name' || k.isEmpty) return; _permissions[k] = v == 'true'; }); print('$_permissions'); @@ -235,6 +235,8 @@ class FfiModel with ChangeNotifier { parent.target?.serverModel.onClientAuthorized(evt); } else if (name == 'on_client_remove') { parent.target?.serverModel.onClientRemove(evt); + } else if (name == 'update_quality_status') { + parent.target?.qualityMonitorModel.updateQualityStatus(evt); } }; platformFFI.setEventCallback(cb); @@ -267,6 +269,8 @@ class FfiModel with ChangeNotifier { wrongPasswordDialog(id); } else if (type == 'input-password') { enterPasswordDialog(id); + } else if (type == 'restarting') { + showMsgBox(id, type, title, text, false, hasCancel: false); } else { var hasRetry = evt['hasRetry'] == 'true'; showMsgBox(id, type, title, text, hasRetry); @@ -275,8 +279,9 @@ class FfiModel with ChangeNotifier { /// Show a message box with [type], [title] and [text]. void showMsgBox( - String id, String type, String title, String text, bool hasRetry) { - msgBox(type, title, text); + String id, String type, String title, String text, bool hasRetry, + {bool? hasCancel}) { + msgBox(type, title, text, hasCancel: hasCancel); _timer?.cancel(); if (hasRetry) { _timer = Timer(Duration(seconds: _reconnects), () { @@ -798,6 +803,47 @@ class CursorModel with ChangeNotifier { } } +class QualityMonitorData { + String? speed; + String? fps; + String? delay; + String? targetBitrate; + String? codecFormat; +} + +class QualityMonitorModel with ChangeNotifier { + WeakReference parent; + + QualityMonitorModel(this.parent); + var _show = false; + final _data = QualityMonitorData(); + + bool get show => _show; + QualityMonitorData get data => _data; + + checkShowQualityMonitor() { + final show = + gFFI.getByName('toggle_option', 'show-quality-monitor') == 'true'; + if (_show != show) { + _show = show; + notifyListeners(); + } + } + + updateQualityStatus(Map evt) { + try { + if ((evt["speed"] as String).isNotEmpty) _data.speed = evt["speed"]; + if ((evt["fps"] as String).isNotEmpty) _data.fps = evt["fps"]; + if ((evt["delay"] as String).isNotEmpty) _data.delay = evt["delay"]; + if ((evt["target_bitrate"] as String).isNotEmpty) + _data.targetBitrate = evt["target_bitrate"]; + if ((evt["codec_format"] as String).isNotEmpty) + _data.codecFormat = evt["codec_format"]; + notifyListeners(); + } catch (e) {} + } +} + /// Mouse button enum. enum MouseButtons { left, right, wheel } @@ -831,6 +877,7 @@ class FFI { late final FileModel fileModel; late final AbModel abModel; late final UserModel userModel; + late final QualityMonitorModel qualityMonitorModel; FFI() { this.imageModel = ImageModel(WeakReference(this)); @@ -842,6 +889,7 @@ class FFI { this.fileModel = FileModel(WeakReference(this)); this.abModel = AbModel(WeakReference(this)); this.userModel = UserModel(WeakReference(this)); + this.qualityMonitorModel = QualityMonitorModel(WeakReference(this)); } static FFI newFFI() { diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 127dcd523..19527c230 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -113,6 +113,27 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "8.4.0" + cached_network_image: + dependency: transitive + description: + name: cached_network_image + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.2.1" + cached_network_image_platform_interface: + dependency: transitive + description: + name: cached_network_image_platform_interface + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.0" + cached_network_image_web: + dependency: transitive + description: + name: cached_network_image_web + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.1" characters: dependency: transitive description: @@ -147,7 +168,7 @@ packages: name: code_builder url: "https://pub.flutter-io.cn" source: hosted - version: "4.1.0" + version: "4.2.0" collection: dependency: transitive description: @@ -183,6 +204,13 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "3.0.2" + csslib: + dependency: transitive + description: + name: csslib + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.17.2" cupertino_icons: dependency: "direct main" description: @@ -197,13 +225,13 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.2.3" - dash_chat: + dash_chat_2: dependency: "direct main" description: - name: dash_chat + name: dash_chat_2 url: "https://pub.flutter-io.cn" source: hosted - version: "1.1.16" + version: "0.0.12" desktop_multi_window: dependency: "direct main" description: @@ -351,6 +379,13 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_blurhash: + dependency: transitive + description: + name: flutter_blurhash + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.7.0" flutter_breadcrumb: dependency: "direct main" description: @@ -358,6 +393,13 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.0.1" + flutter_cache_manager: + dependency: transitive + description: + name: flutter_cache_manager + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.3.0" flutter_launcher_icons: dependency: "direct dev" description: @@ -394,7 +436,7 @@ packages: name: flutter_smart_dialog url: "https://pub.flutter-io.cn" source: hosted - version: "4.5.3+7" + version: "4.5.3+8" flutter_test: dependency: "direct dev" description: flutter @@ -447,13 +489,20 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0" + html: + dependency: transitive + description: + name: html + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.15.0" http: dependency: "direct main" description: name: http url: "https://pub.flutter-io.cn" source: hosted - version: "0.13.4" + version: "0.13.5" http_multi_server: dependency: transitive description: @@ -509,7 +558,7 @@ packages: name: image_picker_platform_interface url: "https://pub.flutter-io.cn" source: hosted - version: "2.5.0" + version: "2.6.1" intl: dependency: transitive description: @@ -587,6 +636,13 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.0.0" + octo_image: + dependency: transitive + description: + name: octo_image + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.2" package_config: dependency: transitive description: @@ -656,14 +712,14 @@ packages: name: path_provider_android url: "https://pub.flutter-io.cn" source: hosted - version: "2.0.16" + version: "2.0.17" path_provider_ios: dependency: transitive description: name: path_provider_ios url: "https://pub.flutter-io.cn" source: hosted - version: "2.0.10" + version: "2.0.11" path_provider_linux: dependency: transitive description: @@ -740,7 +796,7 @@ packages: name: provider url: "https://pub.flutter-io.cn" source: hosted - version: "5.0.0" + version: "6.0.3" pub_semver: dependency: transitive description: @@ -758,12 +814,10 @@ packages: qr_code_scanner: dependency: "direct main" description: - path: "." - ref: fix_break_changes_platform - resolved-ref: "0feca6f15042c279ff575c559a3430df917b623d" - url: "https://github.com/Heap-Hop/qr_code_scanner.git" - source: git - version: "0.7.0" + name: qr_code_scanner + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.0" quiver: dependency: transitive description: @@ -771,6 +825,13 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "3.1.0" + rxdart: + dependency: transitive + description: + name: rxdart + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.27.5" screen_retriever: dependency: transitive description: @@ -847,7 +908,7 @@ packages: name: shelf url: "https://pub.flutter-io.cn" source: hosted - version: "1.3.1" + version: "1.3.2" shelf_web_socket: dependency: transitive description: @@ -881,6 +942,20 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.8.2" + sqflite: + dependency: transitive + description: + name: sqflite + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.3" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.1+1" stack_trace: dependency: transitive description: @@ -909,6 +984,13 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.1.0" + synchronized: + dependency: transitive + description: + name: synchronized + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.0+2" term_glyph: dependency: transitive description: @@ -937,13 +1019,6 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.4.0" - transparent_image: - dependency: transitive - description: - name: transparent_image - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.0.0" tray_manager: dependency: "direct main" description: @@ -1035,6 +1110,41 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.1.2" + video_player: + dependency: transitive + description: + name: video_player + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.5" + video_player_android: + dependency: transitive + description: + name: video_player_android + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.8" + video_player_avfoundation: + dependency: transitive + description: + name: video_player_avfoundation + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.5" + video_player_platform_interface: + dependency: transitive + description: + name: video_player_platform_interface + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.1.3" + video_player_web: + dependency: transitive + description: + name: video_player_web + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.12" visibility_detector: dependency: "direct main" description: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 76c2f7e12..4f996c0ca 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -31,7 +31,7 @@ dependencies: ffi: ^1.1.2 path_provider: ^2.0.2 external_path: ^1.0.1 - provider: ^5.0.0 + provider: ^6.0.3 tuple: ^2.0.0 wakelock: ^0.5.2 device_info_plus: ^3.2.3 @@ -40,15 +40,12 @@ dependencies: url_launcher: ^6.0.9 shared_preferences: ^2.0.6 toggle_switch: ^1.4.0 - dash_chat: ^1.1.16 + dash_chat_2: ^0.0.12 draggable_float_widget: ^0.0.2 settings_ui: ^2.0.2 flutter_breadcrumb: ^1.0.1 http: ^0.13.4 - qr_code_scanner: - git: - url: https://github.com/Heap-Hop/qr_code_scanner.git - ref: fix_break_changes_platform + qr_code_scanner: ^1.0.0 zxing2: ^0.1.0 image_picker: ^0.8.5 image: ^3.1.3 diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index 5069fa2b0..dec00f21e 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -64,6 +64,7 @@ message LoginRequest { } bool video_ack_required = 9; uint64 session_id = 10; + string version = 11; } message ChatMessage { string text = 1; } diff --git a/libs/hbb_common/src/fs.rs b/libs/hbb_common/src/fs.rs index 4880b4622..6cc795a0d 100644 --- a/libs/hbb_common/src/fs.rs +++ b/libs/hbb_common/src/fs.rs @@ -276,7 +276,7 @@ impl TransferJob { show_hidden: bool, is_remote: bool, files: Vec, - enable_override_detection: bool, + enable_overwrite_detection: bool, ) -> Self { log::info!("new write {}", path); let total_size = files.iter().map(|x| x.size as u64).sum(); @@ -289,7 +289,7 @@ impl TransferJob { is_remote, files, total_size, - enable_overwrite_detection: enable_override_detection, + enable_overwrite_detection, ..Default::default() } } @@ -301,7 +301,7 @@ impl TransferJob { file_num: i32, show_hidden: bool, is_remote: bool, - enable_override_detection: bool, + enable_overwrite_detection: bool, ) -> ResultType { log::info!("new read {}", path); let files = get_recursive_files(&path, show_hidden)?; @@ -315,7 +315,7 @@ impl TransferJob { is_remote, files, total_size, - enable_overwrite_detection: enable_override_detection, + enable_overwrite_detection, ..Default::default() }) } diff --git a/src/client.rs b/src/client.rs index a05826c36..7ddfe0969 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1033,10 +1033,6 @@ impl LoginConfigHandler { msg.lock_after_session_end = BoolOption::Yes.into(); n += 1; } - if self.get_toggle_option("privacy-mode") { - msg.privacy_mode = BoolOption::Yes.into(); - n += 1; - } if self.get_toggle_option("disable-audio") { msg.disable_audio = BoolOption::Yes.into(); n += 1; @@ -1060,6 +1056,23 @@ impl LoginConfigHandler { } } + pub fn get_option_message_after_login(&self) -> Option { + if self.is_port_forward || self.is_file_transfer { + return None; + } + let mut n = 0; + let mut msg = OptionMessage::new(); + if self.get_toggle_option("privacy-mode") { + msg.privacy_mode = BoolOption::Yes.into(); + n += 1; + } + if n > 0 { + Some(msg) + } else { + None + } + } + /// Parse the image quality option. /// Return [`ImageQuality`] if the option is valid, otherwise return `None`. /// @@ -1277,6 +1290,7 @@ impl LoginConfigHandler { my_name: crate::username(), option: self.get_option_message(true).into(), session_id: self.session_id, + version: crate::VERSION.to_string(), ..Default::default() }; if self.is_file_transfer { diff --git a/src/client/helper.rs b/src/client/helper.rs index b3ab6cb48..d38fbf223 100644 --- a/src/client/helper.rs +++ b/src/client/helper.rs @@ -3,7 +3,10 @@ use std::{ time::Instant, }; -use hbb_common::{log, message_proto::{VideoFrame, video_frame}}; +use hbb_common::{ + log, + message_proto::{video_frame, VideoFrame}, +}; const MAX_LATENCY: i64 = 500; const MIN_LATENCY: i64 = 100; @@ -89,3 +92,12 @@ impl ToString for CodecFormat { } } } + +#[derive(Debug, Default)] +pub struct QualityStatus { + pub speed: Option, + pub fps: Option, + pub delay: Option, + pub target_bitrate: Option, + pub codec_format: Option, +} diff --git a/src/flutter.rs b/src/flutter.rs index edd972f68..c52c6b735 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -1,6 +1,9 @@ use std::{ collections::{HashMap, VecDeque}, - sync::{Arc, Mutex, RwLock}, + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, Mutex, RwLock, + }, }; use flutter_rust_bridge::{StreamSink, ZeroCopyBuffer}; @@ -114,6 +117,9 @@ impl Session { } lc.set_option(name, value); } + // TODO + // input_os_password + // restart_remote_device /// Input the OS password. pub fn input_os_password(&self, pass: String, activate: bool) { @@ -505,6 +511,26 @@ impl Session { } } } + + fn update_quality_status(&self, status: QualityStatus) { + const NULL: String = String::new(); + self.push_event( + "update_quality_status", + vec![ + ("speed", &status.speed.map_or(NULL, |it| it)), + ("fps", &status.fps.map_or(NULL, |it| it.to_string())), + ("delay", &status.delay.map_or(NULL, |it| it.to_string())), + ( + "target_bitrate", + &status.target_bitrate.map_or(NULL, |it| it.to_string()), + ), + ( + "codec_format", + &status.codec_format.map_or(NULL, |it| it.to_string()), + ), + ], + ); + } } impl FileManager for Session {} @@ -599,7 +625,14 @@ impl Interface for Session { } async fn handle_test_delay(&mut self, t: TestDelay, peer: &mut Stream) { - handle_test_delay(t, peer).await; + if !t.from_client { + self.update_quality_status(QualityStatus { + delay: Some(t.last_delay as _), + target_bitrate: Some(t.target_bitrate as _), + ..Default::default() + }); + handle_test_delay(t, peer).await; + } } } @@ -614,6 +647,9 @@ struct Connection { write_jobs: Vec, timer: Interval, last_update_jobs_status: (Instant, HashMap), + data_count: Arc, + frame_count: Arc, + video_format: CodecFormat, } impl Connection { @@ -646,6 +682,9 @@ impl Connection { write_jobs: Vec::new(), timer: time::interval(SEC30), last_update_jobs_status: (Instant::now(), Default::default()), + data_count: Arc::new(AtomicUsize::new(0)), + frame_count: Arc::new(AtomicUsize::new(0)), + video_format: CodecFormat::Unknown, }; let key = Config::get_option("key"); let token = Config::get_option("access_token"); @@ -659,6 +698,9 @@ impl Connection { ("direct", &direct.to_string()), ], ); + + let mut status_timer = time::interval(Duration::new(1, 0)); + loop { tokio::select! { res = peer.next() => { @@ -671,14 +713,20 @@ impl Connection { } Ok(ref bytes) => { last_recv_time = Instant::now(); + conn.data_count.fetch_add(bytes.len(), Ordering::Relaxed); if !conn.handle_msg_from_peer(bytes, &mut peer).await { break } } } } else { - log::info!("Reset by the peer"); - session.msgbox("error", "Connection Error", "Reset by the peer"); + if session.lc.read().unwrap().restarting_remote_device { + log::info!("Restart remote device"); + session.msgbox("restarting", "Restarting Remote Device", "remote_restarting_tip"); + } else { + log::info!("Reset by the peer"); + session.msgbox("error", "Connection Error", "Reset by the peer"); + } break; } } @@ -704,6 +752,16 @@ impl Connection { conn.timer = time::interval_at(Instant::now() + SEC30, SEC30); } } + _ = status_timer.tick() => { + let speed = conn.data_count.swap(0, Ordering::Relaxed); + let speed = format!("{:.2}kB/s", speed as f32 / 1024 as f32); + let fps = conn.frame_count.swap(0, Ordering::Relaxed) as _; + conn.session.update_quality_status(QualityStatus { + speed:Some(speed), + fps:Some(fps), + ..Default::default() + }); + } } } log::debug!("Exit io_loop of id={}", session.id); @@ -725,6 +783,14 @@ impl Connection { if !self.first_frame { self.first_frame = true; } + let incomming_format = CodecFormat::from(&vf); + if self.video_format != incomming_format { + self.video_format = incomming_format.clone(); + self.session.update_quality_status(QualityStatus { + codec_format: Some(incomming_format), + ..Default::default() + }) + }; if let Ok(true) = self.video_handler.handle_frame(vf) { let stream = self.session.events2ui.read().unwrap(); stream.add(EventToUI::Rgba(ZeroCopyBuffer( @@ -786,113 +852,114 @@ impl Connection { vec![("x", &cp.x.to_string()), ("y", &cp.y.to_string())], ); } - Some(message::Union::FileResponse(fr)) => match fr.union { - Some(file_response::Union::Dir(fd)) => { - let mut entries = fd.entries.to_vec(); - if self.session.peer_platform() == "Windows" { - fs::transform_windows_path(&mut entries); - } - let id = fd.id; - self.session.push_event( - "file_dir", - vec![("value", &make_fd_to_json(fd)), ("is_local", "false")], - ); - if let Some(job) = fs::get_job(id, &mut self.write_jobs) { - job.set_files(entries); - } - } - Some(file_response::Union::Block(block)) => { - if let Some(job) = fs::get_job(block.id, &mut self.write_jobs) { - if let Err(_err) = job.write(block, None).await { - // to-do: add "skip" for writing job + Some(message::Union::FileResponse(fr)) => { + match fr.union { + Some(file_response::Union::Dir(fd)) => { + let mut entries = fd.entries.to_vec(); + if self.session.peer_platform() == "Windows" { + fs::transform_windows_path(&mut entries); + } + let id = fd.id; + self.session.push_event( + "file_dir", + vec![("value", &make_fd_to_json(fd)), ("is_local", "false")], + ); + if let Some(job) = fs::get_job(id, &mut self.write_jobs) { + job.set_files(entries); } - self.update_jobs_status(); } - } - Some(file_response::Union::Done(d)) => { - if let Some(job) = fs::get_job(d.id, &mut self.write_jobs) { - job.modify_time(); - fs::remove_job(d.id, &mut self.write_jobs); + Some(file_response::Union::Block(block)) => { + if let Some(job) = fs::get_job(block.id, &mut self.write_jobs) { + if let Err(_err) = job.write(block, None).await { + // to-do: add "skip" for writing job + } + self.update_jobs_status(); + } } - self.handle_job_status(d.id, d.file_num, None); - } - Some(file_response::Union::Error(e)) => { - self.handle_job_status(e.id, e.file_num, Some(e.error)); - } - Some(file_response::Union::Digest(digest)) => { - if digest.is_upload { - if let Some(job) = fs::get_job(digest.id, &mut self.read_jobs) { - if let Some(file) = job.files().get(digest.file_num as usize) { - let read_path = get_string(&job.join(&file.name)); - let overwrite_strategy = job.default_overwrite_strategy(); - if let Some(overwrite) = overwrite_strategy { - let req = FileTransferSendConfirmRequest { - id: digest.id, - file_num: digest.file_num, - union: Some(if overwrite { - file_transfer_send_confirm_request::Union::OffsetBlk(0) - } else { - file_transfer_send_confirm_request::Union::Skip( - true, - ) - }), - ..Default::default() - }; - job.confirm(&req); - let msg = new_send_confirm(req); - allow_err!(peer.send(&msg).await); - } else { - self.handle_override_file_confirm( - digest.id, - digest.file_num, - read_path, - true, - ); + Some(file_response::Union::Done(d)) => { + if let Some(job) = fs::get_job(d.id, &mut self.write_jobs) { + job.modify_time(); + fs::remove_job(d.id, &mut self.write_jobs); + } + self.handle_job_status(d.id, d.file_num, None); + } + Some(file_response::Union::Error(e)) => { + self.handle_job_status(e.id, e.file_num, Some(e.error)); + } + Some(file_response::Union::Digest(digest)) => { + if digest.is_upload { + if let Some(job) = fs::get_job(digest.id, &mut self.read_jobs) { + if let Some(file) = job.files().get(digest.file_num as usize) { + let read_path = get_string(&job.join(&file.name)); + let overwrite_strategy = job.default_overwrite_strategy(); + if let Some(overwrite) = overwrite_strategy { + let req = FileTransferSendConfirmRequest { + id: digest.id, + file_num: digest.file_num, + union: Some(if overwrite { + file_transfer_send_confirm_request::Union::OffsetBlk(0) + } else { + file_transfer_send_confirm_request::Union::Skip( + true, + ) + }), + ..Default::default() + }; + job.confirm(&req); + let msg = new_send_confirm(req); + allow_err!(peer.send(&msg).await); + } else { + self.handle_override_file_confirm( + digest.id, + digest.file_num, + read_path, + true, + ); + } } } - } - } else { - if let Some(job) = fs::get_job(digest.id, &mut self.write_jobs) { - if let Some(file) = job.files().get(digest.file_num as usize) { - let write_path = get_string(&job.join(&file.name)); - let overwrite_strategy = job.default_overwrite_strategy(); - match fs::is_write_need_confirmation(&write_path, &digest) { - Ok(res) => match res { - DigestCheckResult::IsSame => { - let msg= new_send_confirm(FileTransferSendConfirmRequest { + } else { + if let Some(job) = fs::get_job(digest.id, &mut self.write_jobs) { + if let Some(file) = job.files().get(digest.file_num as usize) { + let write_path = get_string(&job.join(&file.name)); + let overwrite_strategy = job.default_overwrite_strategy(); + match fs::is_write_need_confirmation(&write_path, &digest) { + Ok(res) => match res { + DigestCheckResult::IsSame => { + let msg= new_send_confirm(FileTransferSendConfirmRequest { id: digest.id, file_num: digest.file_num, union: Some(file_transfer_send_confirm_request::Union::Skip(true)), ..Default::default() }); - self.session.send_msg(msg); - } - DigestCheckResult::NeedConfirm(digest) => { - if let Some(overwrite) = overwrite_strategy { - let msg = new_send_confirm( - FileTransferSendConfirmRequest { - id: digest.id, - file_num: digest.file_num, - union: Some(if overwrite { - file_transfer_send_confirm_request::Union::OffsetBlk(0) - } else { - file_transfer_send_confirm_request::Union::Skip(true) - }), - ..Default::default() - }, - ); self.session.send_msg(msg); - } else { - self.handle_override_file_confirm( - digest.id, - digest.file_num, - write_path.to_string(), - false, - ); } - } - DigestCheckResult::NoSuchFile => { - let msg = new_send_confirm( + DigestCheckResult::NeedConfirm(digest) => { + if let Some(overwrite) = overwrite_strategy { + let msg = new_send_confirm( + FileTransferSendConfirmRequest { + id: digest.id, + file_num: digest.file_num, + union: Some(if overwrite { + file_transfer_send_confirm_request::Union::OffsetBlk(0) + } else { + file_transfer_send_confirm_request::Union::Skip(true) + }), + ..Default::default() + }, + ); + self.session.send_msg(msg); + } else { + self.handle_override_file_confirm( + digest.id, + digest.file_num, + write_path.to_string(), + false, + ); + } + } + DigestCheckResult::NoSuchFile => { + let msg = new_send_confirm( FileTransferSendConfirmRequest { id: digest.id, file_num: digest.file_num, @@ -900,19 +967,20 @@ impl Connection { ..Default::default() }, ); - self.session.send_msg(msg); + self.session.send_msg(msg); + } + }, + Err(err) => { + println!("error recving digest: {}", err); } - }, - Err(err) => { - println!("error recving digest: {}", err); } } } } } + _ => {} } - _ => {} - }, + } Some(message::Union::Misc(misc)) => match misc.union { Some(misc::Union::AudioFormat(f)) => { self.audio_handler.handle_format(f); // @@ -931,6 +999,7 @@ impl Connection { Permission::Keyboard => "keyboard", Permission::Clipboard => "clipboard", Permission::Audio => "audio", + Permission::Restart => "restart", _ => "", }, &p.enabled.to_string(), @@ -1608,8 +1677,8 @@ pub mod connection_manager { id, file_num, mut files, + overwrite_detection, } => { - // in mobile, can_enable_override_detection is always true WRITE_JOBS.lock().unwrap().push(fs::TransferJob::new_write( id, "".to_string(), @@ -1625,7 +1694,7 @@ pub mod connection_manager { ..Default::default() }) .collect(), - true, + overwrite_detection, )); } ipc::FS::CancelWrite { id } => { diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 84cc20e0a..dd274ebc6 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -644,25 +644,6 @@ unsafe extern "C" fn get_by_name(name: *const c_char, arg: *const c_char) -> *co // res = Session::get_option(arg); // } // } - "server_id" => { - res = ui_interface::get_id(); - } - "temporary_password" => { - res = ui_interface::temporary_password(); - } - "permanent_password" => { - res = ui_interface::permanent_password(); - } - "connect_statue" => { - res = ONLINE - .lock() - .unwrap() - .values() - .max() - .unwrap_or(&0) - .clone() - .to_string(); - } // File Action "get_home_dir" => { res = fs::get_home_as_string(); @@ -683,6 +664,33 @@ unsafe extern "C" fn get_by_name(name: *const c_char, arg: *const c_char) -> *co // } // } // Server Side + "local_option" => { + if let Ok(arg) = arg.to_str() { + res = LocalConfig::get_option(arg); + } + } + "langs" => { + res = crate::lang::LANGS.to_string(); + } + "server_id" => { + res = ui_interface::get_id(); + } + "temporary_password" => { + res = ui_interface::temporary_password(); + } + "permanent_password" => { + res = ui_interface::permanent_password(); + } + "connect_statue" => { + res = ONLINE + .lock() + .unwrap() + .values() + .max() + .unwrap_or(&0) + .clone() + .to_string(); + } #[cfg(not(any(target_os = "ios")))] "clients_state" => { res = get_clients_state(); @@ -862,9 +870,22 @@ unsafe extern "C" fn set_by_name(name: *const c_char, value: *const c_char) { // } // } // } + "local_option" => { + if let Ok(m) = serde_json::from_str::>(value) { + if let Some(name) = m.get("name") { + if let Some(value) = m.get("value") { + LocalConfig::set_option(name.to_owned(), value.to_owned()); + } + } + } + } // "input_os_password" => { // Session::input_os_password(value.to_owned(), true); // } + "restart_remote_device" => { + // TODO + // Session::restart_remote_device(); + } // // File Action // "read_remote_dir" => { // if let Ok(m) = serde_json::from_str::>(value) { diff --git a/src/ipc.rs b/src/ipc.rs index 99670890e..b85a35bd5 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -60,6 +60,7 @@ pub enum FS { id: i32, file_num: i32, files: Vec<(String, u64)>, + overwrite_detection: bool, }, CancelWrite { id: i32, diff --git a/src/lang.rs b/src/lang.rs index ec0f3c187..400c4dd95 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -8,17 +8,18 @@ mod de; mod en; mod eo; mod es; -mod hu; mod fr; +mod hu; mod id; mod it; +mod ja; +mod pl; mod ptbr; mod ru; mod sk; mod tr; mod tw; mod vn; -mod pl; lazy_static::lazy_static! { pub static ref LANGS: Value = @@ -41,6 +42,7 @@ lazy_static::lazy_static! { ("tr", "Türkçe"), ("vn", "Tiếng Việt"), ("pl", "Polski"), + ("ja", "日本語"), ]); } @@ -87,16 +89,19 @@ pub fn translate_locale(name: String, locale: &str) -> String { "sk" => sk::T.deref(), "vn" => vn::T.deref(), "pl" => pl::T.deref(), + "ja" => ja::T.deref(), _ => en::T.deref(), }; if let Some(v) = m.get(&name as &str) { - v.to_string() - } else { - if lang != "en" { - if let Some(v) = en::T.get(&name as &str) { - return v.to_string(); + if v.is_empty() { + if lang != "en" { + if let Some(v) = en::T.get(&name as &str) { + return v.to_string(); + } } + } else { + return v.to_string(); } - name } + name } diff --git a/src/lang/ja.rs b/src/lang/ja.rs new file mode 100644 index 000000000..5c6ba1da7 --- /dev/null +++ b/src/lang/ja.rs @@ -0,0 +1,303 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "状態"), + ("Your Desktop", "デスクトップ"), + ("desk_tip", "このIDとパスワードであなたのデスクトップにアクセスできます。"), + ("Password", "パスワード"), + ("Ready", "準備完了"), + ("Established", "接続完了"), + ("connecting_status", "RuskDeskネットワークに接続中..."), + ("Enable Service", "サービスを有効化"), + ("Start Service", "サービスを開始"), + ("Service is running", "サービスは動作中"), + ("Service is not running", "サービスは動作していません"), + ("not_ready_status", "準備できていません。接続を確認してください。"), + ("Control Remote Desktop", "リモートのデスクトップを操作する"), + ("Transfer File", "ファイルを転送"), + ("Connect", "接続"), + ("Recent Sessions", "最近のセッション"), + ("Address Book", "アドレス帳"), + ("Confirmation", "確認用"), + ("TCP Tunneling", "TCPトンネリング"), + ("Remove", "削除"), + ("Refresh random password", "ランダムパスワードを再生成"), + ("Set your own password", "自分のパスワードを設定"), + ("Enable Keyboard/Mouse", "キーボード・マウスを有効化"), + ("Enable Clipboard", "クリップボードを有効化"), + ("Enable File Transfer", "ファイル転送を有効化"), + ("Enable TCP Tunneling", "TCPトンネリングを有効化"), + ("IP Whitelisting", "IPホワイトリスト"), + ("ID/Relay Server", "認証・中継サーバー"), + ("Stop service", "サービスを停止"), + ("Change ID", "IDを変更"), + ("Website", "公式サイト"), + ("About", "情報"), + ("Mute", "ミュート"), + ("Audio Input", "音声入力デバイス"), + ("Enhancements", "追加機能"), + ("Hardware Codec", "ハードウェア コーデック"), + ("Adaptive Bitrate", "アダプティブビットレート"), + ("ID Server", "認証サーバー"), + ("Relay Server", "中継サーバー"), + ("API Server", "APIサーバー"), + ("invalid_http", "http:// もしくは https:// から入力してください"), + ("Invalid IP", "無効なIP"), + ("id_change_tip", "使用できるのは大文字・小文字のアルファベット、数字、アンダースコア(_)のみです。初めの文字はアルファベットにする必要があります。6文字から16文字までです。"), + ("Invalid format", "無効な形式"), + ("server_not_support", "サーバー側でまだサポートされていません"), + ("Not available", "利用不可"), + ("Too frequent", "使用量が多すぎです"), + ("Cancel", "キャンセル"), + ("Skip", "スキップ"), + ("Close", "閉じる"), + ("Retry", "再試行"), + ("OK", "OK"), + ("Password Required", "パスワードが必要"), + ("Please enter your password", "パスワードを入力してください"), + ("Remember password", "パスワードを記憶する"), + ("Wrong Password", "パスワードが間違っています"), + ("Do you want to enter again?", "もう一度入力しますか?"), + ("Connection Error", "接続エラー"), + ("Error", "エラー"), + ("Reset by the peer", "相手がリセットしました"), + ("Connecting...", "接続中..."), + ("Connection in progress. Please wait.", "接続中です。しばらくお待ちください。"), + ("Please try 1 minute later", "1分後にもう一度お試しください"), + ("Login Error", "ログインエラー"), + ("Successful", "成功"), + ("Connected, waiting for image...", "接続完了、画像を取得中..."), + ("Name", "名前"), + ("Type", "種類"), + ("Modified", "最終更新"), + ("Size", "サイズ"), + ("Show Hidden Files", "隠しファイルを表示"), + ("Receive", "受信"), + ("Send", "送信"), + ("Refresh File", "ファイルを更新"), + ("Local", "ローカル"), + ("Remote", "リモート"), + ("Remote Computer", "リモート側コンピューター"), + ("Local Computer", "ローカル側コンピューター"), + ("Confirm Delete", "削除の確認"), + ("Delete", "削除"), + ("Properties", "プロパティ"), + ("Multi Select", "複数選択"), + ("Empty Directory", "空のディレクトリ"), + ("Not an empty directory", "空ではないディレクトリ"), + ("Are you sure you want to delete this file?", "本当にこのファイルを削除しますか?"), + ("Are you sure you want to delete this empty directory?", "本当にこの空のディレクトリを削除しますか?"), + ("Are you sure you want to delete the file of this directory?", "本当にこのディレクトリ内のファイルを削除しますか?"), + ("Do this for all conflicts", "他のすべてにも適用する"), + ("This is irreversible!", "この操作は元に戻せません!"), + ("Deleting", "削除中"), + ("files", "ファイル"), + ("Waiting", "待機中"), + ("Finished", "完了"), + ("Speed", "速度"), + ("Custom Image Quality", "画質を調整"), + ("Privacy mode", "プライバシーモード"), + ("Block user input", "ユーザーの入力をブロック"), + ("Unblock user input", "ユーザーの入力を許可"), + ("Adjust Window", "ウィンドウを調整"), + ("Original", "オリジナル"), + ("Shrink", "縮小"), + ("Stretch", "伸縮"), + ("Good image quality", "画質優先"), + ("Balanced", "バランス"), + ("Optimize reaction time", "速度優先"), + ("Custom", "カスタム"), + ("Show remote cursor", "リモート側のカーソルを表示"), + ("Show quality monitor", "品質モニターを表示"), + ("Disable clipboard", "クリップボードを無効化"), + ("Lock after session end", "セッション終了後にロックする"), + ("Insert", "送信"), + ("Insert Lock", "ロック命令を送信"), + ("Refresh", "更新"), + ("ID does not exist", "IDが存在しません"), + ("Failed to connect to rendezvous server", "ランデブーサーバーに接続できませんでした"), + ("Please try later", "後でもう一度お試しください"), + ("Remote desktop is offline", "リモート側デスクトップがオフラインです"), + ("Key mismatch", "キーが一致しません"), + ("Timeout", "タイムアウト"), + ("Failed to connect to relay server", "中継サーバーに接続できませんでした"), + ("Failed to connect via rendezvous server", "ランデブーサーバー経由で接続できませんでした"), + ("Failed to connect via relay server", "中継サーバー経由で接続できませんでした"), + ("Failed to make direct connection to remote desktop", "リモート側デスクトップと直接接続できませんでした"), + ("Set Password", "パスワードを設定"), + ("OS Password", "OSのパスワード"), + ("install_tip", "RustDeskがUACの影響によりリモート側で正常に動作しない場合があります。UACを回避するには、下のボタンをクリックしてシステムにRustDeskをインストールしてください。"), + ("Click to upgrade", "アップグレード"), + ("Click to download", "ダウンロード"), + ("Click to update", "アップデート"), + ("Configure", "設定"), + ("config_acc", "リモートからあなたのデスクトップを操作するには、RustDeskに「アクセシビリティ」権限を与える必要があります。"), + ("config_screen", "リモートからあなたのデスクトップにアクセスするには、RustDeskに「画面収録」権限を与える必要があります。"), + ("Installing ...", "インストール中..."), + ("Install", "インストール"), + ("Installation", "インストール"), + ("Installation Path", "インストール先のパス"), + ("Create start menu shortcuts", "スタートメニューにショートカットを作成する"), + ("Create desktop icon", "デスクトップにアイコンを作成する"), + ("agreement_tip", "インストールを開始することで、ライセンス条項に同意したとみなされます。"), + ("Accept and Install", "同意してインストール"), + ("End-user license agreement", "エンドユーザー ライセンス条項"), + ("Generating ...", "生成中 ..."), + ("Your installation is lower version.", "インストール済みのバージョンが古いです。"), + ("not_close_tcp_tip", "トンネルを使用中はこのウィンドウを閉じないでください"), + ("Listening ...", "リッスン中 ..."), + ("Remote Host", "リモートのホスト"), + ("Remote Port", "リモートのポート"), + ("Action", "操作"), + ("Add", "追加"), + ("Local Port", "ローカルのポート"), + ("setup_server_tip", "接続をより速くするには、自分のサーバーをセットアップしてください"), + ("Too short, at least 6 characters.", "短すぎます。最低6文字です。"), + ("The confirmation is not identical.", "確認用と一致しません。"), + ("Permissions", "権限"), + ("Accept", "承諾"), + ("Dismiss", "無視"), + ("Disconnect", "切断"), + ("Allow using keyboard and mouse", "キーボード・マウスの使用を許可"), + ("Allow using clipboard", "クリップボードの使用を許可"), + ("Allow hearing sound", "サウンドの受信を許可"), + ("Allow file copy and paste", "ファイルのコピーアンドペーストを許可"), + ("Connected", "接続済み"), + ("Direct and encrypted connection", "接続は暗号化され、直接つながっている"), + ("Relayed and encrypted connection", "接続は暗号化され、中継されている"), + ("Direct and unencrypted connection", "接続は暗号化されてなく、直接つながっている"), + ("Relayed and unencrypted connection", "接続は暗号化されてなく、中継されている"), + ("Enter Remote ID", "リモートのIDを入力"), + ("Enter your password", "パスワードを入力"), + ("Logging in...", "ログイン中..."), + ("Enable RDP session sharing", "RDPセッション共有を有効化"), + ("Auto Login", "自動ログイン"), + ("Enable Direct IP Access", "直接IPアクセスを有効化"), + ("Rename", "名前の変更"), + ("Space", "スペース"), + ("Create Desktop Shortcut", "デスクトップにショートカットを作成する"), + ("Change Path", "パスを変更"), + ("Create Folder", "フォルダを作成"), + ("Please enter the folder name", "フォルダ名を入力してください"), + ("Fix it", "修復"), + ("Warning", "注意"), + ("Login screen using Wayland is not supported", "Waylandを使用したログインスクリーンはサポートされていません"), + ("Reboot required", "再起動が必要"), + ("Unsupported display server ", "サポートされていないディスプレイサーバー"), + ("x11 expected", "X11 が必要です"), + ("Port", "ポート"), + ("Settings", "設定"), + ("Username", "ユーザー名"), + ("Invalid port", "無効なポート"), + ("Closed manually by the peer", "相手が手動で切断しました"), + ("Enable remote configuration modification", "リモート設定変更を有効化"), + ("Run without install", "インストールせずに実行"), + ("Always connected via relay", "常に中継サーバー経由で接続"), + ("Always connect via relay", "常に中継サーバー経由で接続"), + ("whitelist_tip", "ホワイトリストに登録されたIPからのみ接続を許可します"), + ("Login", "ログイン"), + ("Logout", "ログアウト"), + ("Tags", "タグ"), + ("Search ID", "IDを検索"), + ("Current Wayland display server is not supported", "現在のWaylandディスプレイサーバーはサポートされていません"), + ("whitelist_sep", "カンマやセミコロン、空白、改行で区切ってください"), + ("Add ID", "IDを追加"), + ("Add Tag", "タグを追加"), + ("Unselect all tags", "全てのタグを選択解除"), + ("Network error", "ネットワークエラー"), + ("Username missed", "ユーザー名がありません"), + ("Password missed", "パスワードがありません"), + ("Wrong credentials", "資格情報が間違っています"), + ("Edit Tag", "タグを編集"), + ("Unremember Password", "パスワードの記憶を解除"), + ("Favorites", "お気に入り"), + ("Add to Favorites", "お気に入りに追加"), + ("Remove from Favorites", "お気に入りから削除"), + ("Empty", "空"), + ("Invalid folder name", "無効なフォルダ名"), + ("Socks5 Proxy", "SOCKS5プロキシ"), + ("Hostname", "ホスト名"), + ("Discovered", "探知済み"), + ("install_daemon_tip", "起動時に開始するには、システムサービスをインストールする必要があります。"), + ("Remote ID", "リモートのID"), + ("Paste", "ペースト"), + ("Paste here?", "ここにペースト?"), + ("Are you sure to close the connection?", "本当に切断しますか?"), + ("Download new version", "新しいバージョンをダウンロード"), + ("Touch mode", "タッチモード"), + ("Mouse mode", "マウスモード"), + ("One-Finger Tap", "1本指でタップ"), + ("Left Mouse", "マウス左クリック"), + ("One-Long Tap", "1本指でロングタップ"), + ("Two-Finger Tap", "2本指でタップ"), + ("Right Mouse", "マウス右クリック"), + ("One-Finger Move", "1本指でドラッグ"), + ("Double Tap & Move", "2本指でタップ&ドラッグ"), + ("Mouse Drag", "マウスドラッグ"), + ("Three-Finger vertically", "3本指で縦方向"), + ("Mouse Wheel", "マウスホイール"), + ("Two-Finger Move", "2本指でドラッグ"), + ("Canvas Move", "キャンバスの移動"), + ("Pinch to Zoom", "ピンチしてズーム"), + ("Canvas Zoom", "キャンバスのズーム"), + ("Reset canvas", "キャンバスのリセット"), + ("No permission of file transfer", "ファイル転送の権限がありません"), + ("Note", "ノート"), + ("Connection", "接続"), + ("Share Screen", "画面を共有"), + ("CLOSE", "閉じる"), + ("OPEN", "開く"), + ("Chat", "チャット"), + ("Total", "計"), + ("items", "個のアイテム"), + ("Selected", "選択済み"), + ("Screen Capture", "画面キャプチャ"), + ("Input Control", "入力操作"), + ("Audio Capture", "音声キャプチャ"), + ("File Connection", "ファイルの接続"), + ("Screen Connection", "画面の接続"), + ("Do you accept?", "承諾しますか?"), + ("Open System Setting", "端末設定を開く"), + ("How to get Android input permission?", "Androidの入力権限を取得するには?"), + ("android_input_permission_tip1", "このAndroid端末をリモートの端末からマウスやタッチで操作するには、RustDeskに「アクセシビリティ」サービスの使用を許可する必要があります。"), + ("android_input_permission_tip2", "次の端末設定ページに進み、「インストール済みアプリ」から「RestDesk Input」をオンにしてください。"), + ("android_new_connection_tip", "新しい操作リクエストが届きました。この端末を操作しようとしています。"), + ("android_service_will_start_tip", "「画面キャプチャ」をオンにするとサービスが自動的に開始され、他の端末がこの端末への接続をリクエストできるようになります。"), + ("android_stop_service_tip", "サービスを停止すると、現在確立されている接続が全て自動的に閉じられます。"), + ("android_version_audio_tip", "現在のAndroidバージョンでは音声キャプチャはサポートされていません。Android 10以降にアップグレードしてください。"), + ("android_start_service_tip", "「サービスを開始」をタップするか「画面キャプチャ」を開くと、画面共有サービスが開始されます。"), + ("Account", "アカウント"), + ("Overwrite", "上書き"), + ("This file exists, skip or overwrite this file?", "このファイルは存在しています。スキップするか上書きしますか?"), + ("Quit", "終了"), + ("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"), // @TODO: Update url when someone translates the document + ("Help", "ヘルプ"), + ("Failed", "失敗"), + ("Succeeded", "成功"), + ("Someone turns on privacy mode, exit", "プライバシーモードがオンになりました。終了します。"), + ("Unsupported", "サポートされていません"), + ("Peer denied", "相手が拒否しました"), + ("Please install plugins", "プラグインをインストールしてください"), + ("Peer exit", "相手が終了しました"), + ("Failed to turn off", "オフにできませんでした"), + ("Turned off", "オフになりました"), + ("In privacy mode", "プライバシーモード開始"), + ("Out privacy mode", "プライバシーモード終了"), + ("Language", "言語"), + ("Keep RustDesk background service", "RustDesk バックグラウンドサービスを維持"), + ("Ignore Battery Optimizations", "バッテリーの最適化を無効にする"), + ("android_open_battery_optimizations_tip", "この機能を使わない場合は、次のRestDeskアプリ設定ページから「バッテリー」に進み、「制限なし」の選択を外してください"), + ("Connection not allowed", "接続が許可されていません"), + ("Use temporary password", "使い捨てのパスワードを使用"), + ("Use permanent password", "固定のパスワードを使用"), + ("Use both passwords", "どちらのパスワードも使用"), + ("Set permanent password", "固定のパスワードを設定"), + ("Set temporary password length", "使い捨てのパスワードの長さを設定"), + ("Enable Remote Restart", "リモートからの再起動を有効化"), + ("Allow remote restart", "リモートからの再起動を許可"), + ("Restart Remote Device", "リモートの端末を再起動"), + ("Are you sure you want to restart", "本当に再起動しますか"), + ("Restarting Remote Device", "リモート端末を再起動中"), + ("remote_restarting_tip", "リモート端末は再起動中です。このメッセージボックスを閉じて、しばらくした後に固定のパスワードを使用して再接続してください。"), + ].iter().cloned().collect(); +} diff --git a/src/server/connection.rs b/src/server/connection.rs index 383c5782b..7d12dce45 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1099,8 +1099,9 @@ impl Connection { } Some(file_action::Union::Send(s)) => { let id = s.id; - let od = - can_enable_overwrite_detection(get_version_number(VERSION)); + let od = can_enable_overwrite_detection(get_version_number( + &self.lr.version, + )); let path = s.path.clone(); match fs::TransferJob::new_read( id, @@ -1123,6 +1124,11 @@ impl Connection { } } Some(file_action::Union::Receive(r)) => { + // note: 1.1.10 introduced identical file detection, which breaks original logic of send/recv files + // whenever got send/recv request, check peer version to ensure old version of rustdesk + let od = can_enable_overwrite_detection(get_version_number( + &self.lr.version, + )); self.send_fs(ipc::FS::NewWrite { path: r.path, id: r.id, @@ -1133,6 +1139,7 @@ impl Connection { .drain(..) .map(|f| (f.name, f.modified_time)) .collect(), + overwrite_detection: od, }); } Some(file_action::Union::RemoveDir(d)) => { diff --git a/src/ui/cm.rs b/src/ui/cm.rs index 45038d753..38bfc9359 100644 --- a/src/ui/cm.rs +++ b/src/ui/cm.rs @@ -160,8 +160,8 @@ impl ConnectionManager { id, file_num, mut files, + overwrite_detection } => { - let od = can_enable_overwrite_detection(get_version_number(VERSION)); // cm has no show_hidden context // dummy remote, show_hidden, is_remote write_jobs.push(fs::TransferJob::new_write( @@ -179,7 +179,7 @@ impl ConnectionManager { ..Default::default() }) .collect(), - od, + overwrite_detection, )); } ipc::FS::CancelWrite { id } => { diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 30d9335c4..5d036dee2 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -23,10 +23,6 @@ use clipboard::{ get_rx_clip_client, server_clip_file, }; use enigo::{self, Enigo, KeyboardControllable}; -use hbb_common::fs::{ - can_enable_overwrite_detection, get_job, get_string, new_send_confirm, DigestCheckResult, - RemoveJobMeta, -}; use hbb_common::{ allow_err, config::{Config, LocalConfig, PeerConfig}, @@ -43,6 +39,13 @@ use hbb_common::{ Stream, }; use hbb_common::{config::TransferSerde, fs::TransferJobMeta}; +use hbb_common::{ + fs::{ + can_enable_overwrite_detection, get_job, get_string, new_send_confirm, DigestCheckResult, + RemoveJobMeta, + }, + get_version_number, +}; #[cfg(windows)] use crate::clipboard_file::*; @@ -239,15 +242,6 @@ impl sciter::EventHandler for Handler { } } -#[derive(Debug, Default)] -struct QualityStatus { - speed: Option, - fps: Option, - delay: Option, - target_bitrate: Option, - codec_format: Option, -} - impl Handler { pub fn new(cmd: String, id: String, password: String, args: Vec) -> Self { let me = Self { @@ -638,8 +632,9 @@ impl Handler { } fn restart_remote_device(&mut self) { - self.lc.write().unwrap().restarting_remote_device = true; - let msg = self.lc.write().unwrap().restart_remote_device(); + let mut lc = self.lc.write().unwrap(); + lc.restarting_remote_device = true; + let msg = lc.restart_remote_device(); self.send(Data::Message(msg)); } @@ -2076,6 +2071,22 @@ impl Remote { true } + async fn send_opts_after_login(&self, peer: &mut Stream) { + if let Some(opts) = self + .handler + .lc + .read() + .unwrap() + .get_option_message_after_login() + { + let mut misc = Misc::new(); + misc.set_option(opts); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + allow_err!(peer.send(&msg_out).await); + } + } + async fn handle_msg_from_peer(&mut self, data: &[u8], peer: &mut Stream) -> bool { if let Ok(msg_in) = Message::parse_from_bytes(&data) { match msg_in.union { @@ -2084,6 +2095,7 @@ impl Remote { self.first_frame = true; self.handler.call2("closeSuccess", &make_args!()); self.handler.call("adaptSize", &make_args!()); + self.send_opts_after_login(peer).await; } let incomming_format = CodecFormat::from(&vf); if self.video_format != incomming_format { @@ -2595,6 +2607,9 @@ impl Interface for Handler { 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); + if get_version_number(&pi.version) < get_version_number("1.1.10") { + self.call2("setPermission", &make_args!("restart", false)); + } if self.is_file_transfer() { if pi.username.is_empty() { self.on_error("No active console user logged on, please connect and logon first."); From 05b157af455f09175f8fe2acf2aa7e222ff536bf Mon Sep 17 00:00:00 2001 From: Asura Date: Thu, 4 Aug 2022 17:26:06 +0800 Subject: [PATCH 0149/2015] Fix right ctrl #1166 --- src/server/connection.rs | 2 +- src/ui/cm.rs | 1 - src/ui/remote.rs | 7 ++++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/server/connection.rs b/src/server/connection.rs index f68c13895..90548afc7 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -6,7 +6,7 @@ use crate::common::update_clipboard; use crate::video_service; #[cfg(any(target_os = "android", target_os = "ios"))] use crate::{common::MOBILE_INFO2, mobile::connection_manager::start_channel}; -use crate::{ipc, VERSION}; +use crate::{ipc}; use hbb_common::{ config::Config, fs, diff --git a/src/ui/cm.rs b/src/ui/cm.rs index 38bfc9359..e2d912c63 100644 --- a/src/ui/cm.rs +++ b/src/ui/cm.rs @@ -1,5 +1,4 @@ use crate::ipc::{self, new_listener, Connection, Data}; -use crate::VERSION; #[cfg(windows)] use clipboard::{ create_cliprdr_context, empty_clipboard, get_rx_clip_client, server_clip_file, set_conn_enabled, diff --git a/src/ui/remote.rs b/src/ui/remote.rs index bccd6ad79..9bf125cc0 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -1189,7 +1189,7 @@ impl Handler { let alt = get_key_state(enigo::Key::Alt); #[cfg(windows)] let ctrl = { - let mut tmp = get_key_state(enigo::Key::Control); + let mut tmp = get_key_state(enigo::Key::Control) || get_key_state(enigo::Key::RightControl); unsafe { if IS_ALT_GR { if alt || key == RdevKey::AltGr { @@ -1204,8 +1204,8 @@ impl Handler { tmp }; #[cfg(not(windows))] - let ctrl = get_key_state(enigo::Key::Control); - let shift = get_key_state(enigo::Key::Shift); + let ctrl = get_key_state(enigo::Key::Control) || get_key_state(enigo::Key::RightControl); + let shift = get_key_state(enigo::Key::Shift) || get_key_state(enigo::Key::RightShift); #[cfg(windows)] let command = crate::platform::windows::get_win_key_state(); #[cfg(not(windows))] @@ -1389,6 +1389,7 @@ impl Handler { if down_or_up == true { key_event.down = true; } + dbg!(&key_event); self.send_key_event(key_event, KeyboardMode::Legacy) } From 2d0cdd83a27a83d47f57d2537e6b8a7b9bce0ac4 Mon Sep 17 00:00:00 2001 From: Asura Date: Thu, 4 Aug 2022 21:02:00 +0800 Subject: [PATCH 0150/2015] Update lang for keyboard mode --- 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/fr.rs | 1 + src/lang/hu.rs | 1 + src/lang/id.rs | 1 + src/lang/it.rs | 1 + src/lang/ja.rs | 3 +++ src/lang/ptbr.rs | 1 + src/lang/ru.rs | 1 + src/lang/sk.rs | 1 + src/lang/template.rs | 1 + src/lang/tr.rs | 1 + src/lang/tw.rs | 1 + src/lang/vn.rs | 3 +++ 18 files changed, 22 insertions(+) diff --git a/src/lang/cn.rs b/src/lang/cn.rs index f433ab4d0..9a5a76250 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -290,6 +290,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Connection not allowed", "对方不允许连接"), ("Legacy mode", "传统模式"), ("Map mode", "1:1传输"), + ("Translate mode", "翻译模式"), ("Use temporary password", "使用临时密码"), ("Use permanent password", "使用固定密码"), ("Use both passwords", "同时使用两种密码"), diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 6007c5de9..aa03e1d23 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -290,6 +290,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Connection not allowed", ""), ("Legacy mode", ""), ("Map mode", ""), + ("Translate mode", ""), ("Use temporary password", ""), ("Use permanent password", ""), ("Use both passwords", ""), diff --git a/src/lang/da.rs b/src/lang/da.rs index 0981d432e..364e552a3 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -290,6 +290,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Connection not allowed", ""), ("Legacy mode", ""), ("Map mode", ""), + ("Translate mode", ""), ("Use temporary password", ""), ("Use permanent password", ""), ("Use both passwords", ""), diff --git a/src/lang/de.rs b/src/lang/de.rs index f5c8bb2dd..f9558bed9 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -290,6 +290,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Connection not allowed", "Verbindung abgelehnt"), ("Legacy mode", ""), ("Map mode", ""), + ("Translate mode", ""), ("Use temporary password", "Temporäres Passwort verwenden"), ("Use permanent password", "Dauerhaftes Passwort verwenden"), ("Use both passwords", "Beide Passwörter verwenden"), diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 67d08c5c2..ad4a2f93a 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -290,6 +290,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Connection not allowed", ""), ("Legacy mode", ""), ("Map mode", ""), + ("Translate mode", ""), ("Use temporary password", ""), ("Use permanent password", ""), ("Use both passwords", ""), diff --git a/src/lang/es.rs b/src/lang/es.rs index e34757b8c..73a38dacd 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -302,6 +302,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Connection not allowed", ""), ("Legacy mode", ""), ("Map mode", ""), + ("Translate mode", ""), ("Connection not allowed", "Conexión no disponible"), ("Use temporary password", "Usar contraseña temporal"), ("Use permanent password", "Usar contraseña permamente"), diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 81c039c0a..3553a5b17 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -290,6 +290,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Connection not allowed", ""), ("Legacy mode", ""), ("Map mode", ""), + ("Translate mode", ""), ("Use temporary password", ""), ("Use permanent password", ""), ("Use both passwords", ""), diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 432b88115..8a57378ea 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -290,6 +290,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Connection not allowed", ""), ("Legacy mode", ""), ("Map mode", ""), + ("Translate mode", ""), ("Use temporary password", ""), ("Use permanent password", ""), ("Use both passwords", ""), diff --git a/src/lang/id.rs b/src/lang/id.rs index 4b1de4330..422851c77 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -302,6 +302,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Connection not allowed", ""), ("Legacy mode", ""), ("Map mode", ""), + ("Translate mode", ""), ("Connection not allowed", "Koneksi tidak dijinkan"), ("Use temporary password", "Gunakan kata sandi sementara"), ("Use permanent password", "Gunakan kata sandi permanaen"), diff --git a/src/lang/it.rs b/src/lang/it.rs index 661feec02..c98c53100 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -290,6 +290,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Connection not allowed", ""), ("Legacy mode", ""), ("Map mode", ""), + ("Translate mode", ""), ("Use temporary password", ""), ("Use permanent password", ""), ("Use both passwords", ""), diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 5c6ba1da7..82d91993e 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -288,6 +288,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Ignore Battery Optimizations", "バッテリーの最適化を無効にする"), ("android_open_battery_optimizations_tip", "この機能を使わない場合は、次のRestDeskアプリ設定ページから「バッテリー」に進み、「制限なし」の選択を外してください"), ("Connection not allowed", "接続が許可されていません"), + ("Legacy mode", ""), + ("Map mode", ""), + ("Translate mode", ""), ("Use temporary password", "使い捨てのパスワードを使用"), ("Use permanent password", "固定のパスワードを使用"), ("Use both passwords", "どちらのパスワードも使用"), diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 821429389..dea8197b2 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -290,6 +290,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Connection not allowed", ""), ("Legacy mode", ""), ("Map mode", ""), + ("Translate mode", ""), ("Use temporary password", ""), ("Use permanent password", ""), ("Use both passwords", ""), diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 74536cd23..490fb32e0 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -290,6 +290,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Connection not allowed", "Подключение не разрешено"), ("Legacy mode", ""), ("Map mode", ""), + ("Translate mode", ""), ("Use temporary password", "Использовать временный пароль"), ("Use permanent password", "Использовать постоянный пароль"), ("Use both passwords", "Использовать оба пароля"), diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 23b5bacf7..7a78e2090 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -290,6 +290,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Connection not allowed", ""), ("Legacy mode", ""), ("Map mode", ""), + ("Translate mode", ""), ("Use temporary password", ""), ("Use permanent password", ""), ("Use both passwords", ""), diff --git a/src/lang/template.rs b/src/lang/template.rs index 9a24d6d42..f37d84b60 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -290,6 +290,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Connection not allowed", ""), ("Legacy mode", ""), ("Map mode", ""), + ("Translate mode", ""), ("Use temporary password", ""), ("Use permanent password", ""), ("Use both passwords", ""), diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 85f7b13fc..4b0d6ccb3 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -302,6 +302,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Connection not allowed", ""), ("Legacy mode", ""), ("Map mode", ""), + ("Translate mode", ""), ("Connection not allowed", "bağlantıya izin verilmedi"), ("Use temporary password", "Geçici şifre kullan"), ("Use permanent password", "Kalıcı şifre kullan"), diff --git a/src/lang/tw.rs b/src/lang/tw.rs index bcf4e2800..a294acac2 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -290,6 +290,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Connection not allowed", "對方不允許連接"), ("Legacy mode", "傳統模式"), ("Map mode", "1:1傳輸"), + ("Translate mode", "翻譯模式"), ("Use temporary password", "使用臨時密碼"), ("Use permanent password", "使用固定密碼"), ("Use both passwords", "同時使用兩種密碼"), diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 014dcb20e..55570dc40 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -288,6 +288,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Ignore Battery Optimizations", "Bỏ qua các tối ưu pin"), ("android_open_battery_optimizations_tip", "Nếu bạn muốn tắt tính năng này, vui lòng chuyển đến trang cài đặt ứng dụng RustDesk tiếp theo, tìm và nhập [Pin], Bỏ chọn [Không hạn chế]"), ("Connection not allowed", "Kết nối không đuợc phép"), + ("Legacy mode", ""), + ("Map mode", ""), + ("Translate mode", ""), ("Use temporary password", "Sử dụng mật khẩu tạm thời"), ("Use permanent password", "Sử dụng mật khẩu vĩnh viễn"), ("Use both passwords", "Sử dụng cả hai mật khẩu"), From 1977ee951e8ff1064b453dc33b386e588f92170f Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 5 Aug 2022 10:27:06 +0800 Subject: [PATCH 0151/2015] fix: tabbar rebuild issue Signed-off-by: Kingtous --- .../desktop/pages/connection_tab_page.dart | 140 +++++++++--------- .../desktop/pages/file_manager_tab_page.dart | 113 +++++++------- flutter/lib/desktop/pages/remote_page.dart | 2 +- 3 files changed, 132 insertions(+), 123 deletions(-) diff --git a/flutter/lib/desktop/pages/connection_tab_page.dart b/flutter/lib/desktop/pages/connection_tab_page.dart index 9632fc1f0..b87a876a3 100644 --- a/flutter/lib/desktop/pages/connection_tab_page.dart +++ b/flutter/lib/desktop/pages/connection_tab_page.dart @@ -7,7 +7,6 @@ import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/desktop/pages/remote_page.dart'; import 'package:flutter_hbb/desktop/widgets/titlebar_widget.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; -import 'package:provider/provider.dart'; import 'package:get/get.dart'; import '../../models/model.dart'; @@ -22,11 +21,14 @@ class ConnectionTabPage extends StatefulWidget { } class _ConnectionTabPageState extends State - with SingleTickerProviderStateMixin { + with TickerProviderStateMixin { // refactor List when using multi-tab // this singleton is only for test - List connectionIds = List.empty(growable: true); + var connectionIds = RxList.empty(growable: true); var initialIndex = 0; + late Rx tabController; + + var connectionMap = RxList.empty(growable: true); _ConnectionTabPageState(Map params) { if (params['id'] != null) { @@ -37,26 +39,27 @@ class _ConnectionTabPageState extends State @override void initState() { super.initState(); + tabController = + TabController(length: connectionIds.length, vsync: this).obs; rustDeskWinManager.setMethodHandler((call, fromWindowId) async { print( "call ${call.method} with args ${call.arguments} from window ${fromWindowId}"); // for simplify, just replace connectionId if (call.method == "new_remote_desktop") { - setState(() { - final args = jsonDecode(call.arguments); - final id = args['id']; - final indexOf = connectionIds.indexOf(id); - if (indexOf >= 0) { - setState(() { - initialIndex = indexOf; - }); - } else { - connectionIds.add(id); - setState(() { - initialIndex = connectionIds.length - 1; - }); - } - }); + final args = jsonDecode(call.arguments); + final id = args['id']; + final indexOf = connectionIds.indexOf(id); + if (indexOf >= 0) { + initialIndex = indexOf; + tabController.value.animateTo(initialIndex, duration: Duration.zero); + } else { + connectionIds.add(id); + initialIndex = connectionIds.length - 1; + tabController.value = TabController( + length: connectionIds.length, + vsync: this, + initialIndex: initialIndex); + } } else if (call.method == "onDestroy") { print("executing onDestroy hook, closing ${connectionIds}"); connectionIds.forEach((id) { @@ -72,55 +75,52 @@ class _ConnectionTabPageState extends State @override Widget build(BuildContext context) { - final tabBar = TabBar( - isScrollable: true, - labelColor: Colors.white, - physics: NeverScrollableScrollPhysics(), - indicatorColor: Colors.white, - tabs: connectionIds - .map((e) => Tab( - child: Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text(e), - SizedBox( - width: 4, - ), - InkWell( - onTap: () { - onRemoveId(e); - }, - child: Icon( - Icons.highlight_remove, - size: 20, - )) - ], - ), - )) - .toList()); - final tabBarView = TabBarView( - children: connectionIds - .map((e) => Container( - child: RemotePage( - key: ValueKey(e), - id: e, - tabBarHeight: kDesktopRemoteTabBarHeight, - ))) //RemotePage(key: ValueKey(e), id: e)) - .toList()); return Scaffold( - body: DefaultTabController( - initialIndex: initialIndex, - length: connectionIds.length, - animationDuration: Duration.zero, - child: Column( - children: [ - DesktopTitleBar( - child: Container(height: kDesktopRemoteTabBarHeight, child: tabBar), - ), - Expanded(child: tabBarView), - ], - ), + body: Column( + children: [ + DesktopTitleBar( + child: Container( + height: kDesktopRemoteTabBarHeight, + child: Obx(() => TabBar( + isScrollable: true, + labelColor: Colors.white, + physics: NeverScrollableScrollPhysics(), + indicatorColor: Colors.white, + controller: tabController.value, + tabs: connectionIds + .map((e) => Tab( + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(e), + SizedBox( + width: 4, + ), + InkWell( + onTap: () { + onRemoveId(e); + }, + child: Icon( + Icons.highlight_remove, + size: 20, + )) + ], + ), + )) + .toList()))), + ), + Expanded( + child: Obx(() => TabBarView( + controller: tabController.value, + children: connectionIds + .map((e) => RemotePage( + key: ValueKey(e), + id: e, + tabBarHeight: kDesktopRemoteTabBarHeight, + )) //RemotePage(key: ValueKey(e), id: e)) + .toList()))), + ], ), ); } @@ -130,9 +130,9 @@ class _ConnectionTabPageState extends State if (indexOf == -1) { return; } - setState(() { - connectionIds.removeAt(indexOf); - initialIndex = max(0, initialIndex - 1); - }); + connectionIds.removeAt(indexOf); + initialIndex = max(0, initialIndex - 1); + tabController.value = TabController( + length: connectionIds.length, vsync: this, initialIndex: initialIndex); } } diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index 6c9f199b7..5e3337475 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -20,11 +20,12 @@ class FileManagerTabPage extends StatefulWidget { } class _FileManagerTabPageState extends State - with SingleTickerProviderStateMixin { + with TickerProviderStateMixin { // refactor List when using multi-tab // this singleton is only for test var connectionIds = List.empty(growable: true).obs; - var initialIndex = 0.obs; + var initialIndex = 0; + late Rx tabController; _FileManagerTabPageState(Map params) { if (params['id'] != null) { @@ -35,6 +36,8 @@ class _FileManagerTabPageState extends State @override void initState() { super.initState(); + tabController = + TabController(length: connectionIds.length, vsync: this).obs; rustDeskWinManager.setMethodHandler((call, fromWindowId) async { print( "call ${call.method} with args ${call.arguments} from window ${fromWindowId}"); @@ -44,10 +47,15 @@ class _FileManagerTabPageState extends State final id = args['id']; final indexOf = connectionIds.indexOf(id); if (indexOf >= 0) { - initialIndex.value = indexOf; + initialIndex = indexOf; + tabController.value.animateTo(initialIndex, duration: Duration.zero); } else { connectionIds.add(id); - initialIndex.value = connectionIds.length - 1; + initialIndex = connectionIds.length - 1; + tabController.value = TabController( + length: connectionIds.length, + initialIndex: initialIndex, + vsync: this); } } else if (call.method == "onDestroy") { print("executing onDestroy hook, closing ${connectionIds}"); @@ -65,54 +73,53 @@ class _FileManagerTabPageState extends State @override Widget build(BuildContext context) { return Scaffold( - body: Obx( - ()=> DefaultTabController( - initialIndex: initialIndex.value, - length: connectionIds.length, - animationDuration: Duration.zero, - child: Column( - children: [ - DesktopTitleBar( - child: TabBar( - isScrollable: true, - labelColor: Colors.white, - physics: NeverScrollableScrollPhysics(), - indicatorColor: Colors.white, - tabs: connectionIds - .map((e) => Tab( - child: Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text(e), - SizedBox( - width: 4, - ), - InkWell( - onTap: () { - onRemoveId(e); - }, - child: Icon( - Icons.highlight_remove, - size: 20, - )) - ], - ), - )) - .toList()), - ), - Expanded( - child: TabBarView( - children: connectionIds - .map((e) => Container( - child: FileManagerPage( - key: ValueKey(e), - id: e))) //RemotePage(key: ValueKey(e), id: e)) - .toList()), - ) - ], + body: Column( + children: [ + DesktopTitleBar( + child: Obx( + () => TabBar( + controller: tabController.value, + isScrollable: true, + labelColor: Colors.white, + physics: NeverScrollableScrollPhysics(), + indicatorColor: Colors.white, + tabs: connectionIds + .map((e) => Tab( + key: Key('T$e'), + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(e), + SizedBox( + width: 4, + ), + InkWell( + onTap: () { + onRemoveId(e); + }, + child: Icon( + Icons.highlight_remove, + size: 20, + )) + ], + ), + )) + .toList()), + ), ), - ), + Expanded( + child: Obx( + () => TabBarView( + controller: tabController.value, + children: connectionIds + .map((e) => FileManagerPage( + key: ValueKey(e), + id: e)) //RemotePage(key: ValueKey(e), id: e)) + .toList()), + ), + ) + ], ), ); } @@ -123,6 +130,8 @@ class _FileManagerTabPageState extends State return; } connectionIds.removeAt(indexOf); - initialIndex.value = max(0, initialIndex.value - 1); + initialIndex = max(0, initialIndex - 1); + tabController.value = TabController( + length: connectionIds.length, initialIndex: initialIndex, vsync: this); } } diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 0a1979540..ed62e5067 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -77,7 +77,7 @@ class _RemotePageState extends State @override void dispose() { - print("remote page dispose"); + print("REMOTE PAGE dispose ${widget.id}"); hideMobileActionsOverlay(); _ffi.listenToMouse(false); _ffi.invokeMethod("enable_soft_keyboard", true); From 8f8d5e1efbc8f2dd79bf1681088b9dab3b2c694d Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 5 Aug 2022 10:49:02 +0800 Subject: [PATCH 0152/2015] update: sync desktop_multi_window to 0.1.0 Signed-off-by: Kingtous --- flutter/pubspec.lock | 350 +++++++++++++++++++++---------------------- flutter/pubspec.yaml | 2 +- 2 files changed, 176 insertions(+), 176 deletions(-) diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 19527c230..217051a60 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -5,373 +5,373 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "43.0.0" after_layout: dependency: transitive description: name: after_layout - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.2.0" analyzer: dependency: transitive description: name: analyzer - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "4.3.1" animations: dependency: transitive description: name: animations - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.3" archive: dependency: transitive description: name: archive - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.3.1" args: dependency: transitive description: name: args - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.3.1" async: dependency: transitive description: name: async - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.8.2" boolean_selector: dependency: transitive description: name: boolean_selector - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.0" build: dependency: transitive description: name: build - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.3.0" build_config: dependency: transitive description: name: build_config - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.1.0" build_daemon: dependency: transitive description: name: build_daemon - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.1.0" build_resolvers: dependency: transitive description: name: build_resolvers - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.9" build_runner: dependency: "direct dev" description: name: build_runner - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.2.0" build_runner_core: dependency: transitive description: name: build_runner_core - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "7.2.3" built_collection: dependency: transitive description: name: built_collection - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "5.1.1" built_value: dependency: transitive description: name: built_value - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "8.4.0" cached_network_image: dependency: transitive description: name: cached_network_image - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.2.1" cached_network_image_platform_interface: dependency: transitive description: name: cached_network_image_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.0" cached_network_image_web: dependency: transitive description: name: cached_network_image_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.1" characters: dependency: transitive description: name: characters - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.2.0" charcode: dependency: transitive description: name: charcode - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.3.1" checked_yaml: dependency: transitive description: name: checked_yaml - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.1" clock: dependency: transitive description: name: clock - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.1.0" code_builder: dependency: transitive description: name: code_builder - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "4.2.0" collection: dependency: transitive description: name: collection - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.16.0" contextmenu: dependency: "direct main" description: name: contextmenu - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.0" convert: dependency: transitive description: name: convert - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.2" cross_file: dependency: transitive description: name: cross_file - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.3.3+1" crypto: dependency: transitive description: name: crypto - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.2" csslib: dependency: transitive description: name: csslib - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.17.2" cupertino_icons: dependency: "direct main" description: name: cupertino_icons - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.5" dart_style: dependency: transitive description: name: dart_style - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.2.3" dash_chat_2: dependency: "direct main" description: name: dash_chat_2 - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.0.12" desktop_multi_window: dependency: "direct main" description: path: "." - ref: "7b72918710921f5fe79eae2dbaa411a66f5dfb45" - resolved-ref: "7b72918710921f5fe79eae2dbaa411a66f5dfb45" + ref: "832c263998275f8e6d3ea196931bc59a54ba9c79" + resolved-ref: "832c263998275f8e6d3ea196931bc59a54ba9c79" url: "https://github.com/Kingtous/rustdesk_desktop_multi_window" source: git - version: "0.0.1" + version: "0.1.0" device_info_plus: dependency: "direct main" description: name: device_info_plus - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.2.4" device_info_plus_linux: dependency: transitive description: name: device_info_plus_linux - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.1" device_info_plus_macos: dependency: transitive description: name: device_info_plus_macos - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.2.3" device_info_plus_platform_interface: dependency: transitive description: name: device_info_plus_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.3.0+1" device_info_plus_web: dependency: transitive description: name: device_info_plus_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.0" device_info_plus_windows: dependency: transitive description: name: device_info_plus_windows - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.1" draggable_float_widget: dependency: "direct main" description: name: draggable_float_widget - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.0.2" event_bus: dependency: transitive description: name: event_bus - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.0" external_path: dependency: "direct main" description: name: external_path - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.1" fake_async: dependency: transitive description: name: fake_async - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.3.0" ffi: dependency: "direct main" description: name: ffi - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.2.1" file: dependency: transitive description: name: file - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "6.1.2" firebase_analytics: dependency: "direct main" description: name: firebase_analytics - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "9.3.0" firebase_analytics_platform_interface: dependency: transitive description: name: firebase_analytics_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.3.0" firebase_analytics_web: dependency: transitive description: name: firebase_analytics_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.4.2" firebase_core: dependency: transitive description: name: firebase_core - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.20.0" firebase_core_platform_interface: dependency: transitive description: name: firebase_core_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "4.5.0" firebase_core_web: dependency: transitive description: name: firebase_core_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.7.1" fixnum: dependency: transitive description: name: fixnum - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.1" flutter: @@ -383,42 +383,42 @@ packages: dependency: transitive description: name: flutter_blurhash - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.7.0" flutter_breadcrumb: dependency: "direct main" description: name: flutter_breadcrumb - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.1" flutter_cache_manager: dependency: transitive description: name: flutter_cache_manager - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.3.0" flutter_launcher_icons: dependency: "direct dev" description: name: flutter_launcher_icons - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.9.3" flutter_parsed_text: dependency: transitive description: name: flutter_parsed_text - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.2.1" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.7" flutter_rust_bridge: @@ -434,7 +434,7 @@ packages: dependency: "direct main" description: name: flutter_smart_dialog - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "4.5.3+8" flutter_test: @@ -451,476 +451,476 @@ packages: dependency: "direct dev" description: name: freezed - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.0+1" freezed_annotation: dependency: "direct main" description: name: freezed_annotation - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.0" frontend_server_client: dependency: transitive description: name: frontend_server_client - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.3" get: dependency: "direct main" description: name: get - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "4.6.5" glob: dependency: transitive description: name: glob - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.0" graphs: dependency: transitive description: name: graphs - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.0" html: dependency: transitive description: name: html - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.15.0" http: dependency: "direct main" description: name: http - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.13.5" http_multi_server: dependency: transitive description: name: http_multi_server - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.2.1" http_parser: dependency: transitive description: name: http_parser - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "4.0.1" image: dependency: "direct main" description: name: image - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.2.0" image_picker: dependency: "direct main" description: name: image_picker - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.8.5+3" image_picker_android: dependency: transitive description: name: image_picker_android - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.8.5+1" image_picker_for_web: dependency: transitive description: name: image_picker_for_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.8" image_picker_ios: dependency: transitive description: name: image_picker_ios - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.8.5+6" image_picker_platform_interface: dependency: transitive description: name: image_picker_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.6.1" intl: dependency: transitive description: name: intl - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.17.0" io: dependency: transitive description: name: io - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.3" js: dependency: transitive description: name: js - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.6.4" json_annotation: dependency: transitive description: name: json_annotation - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "4.6.0" logging: dependency: transitive description: name: logging - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.2" matcher: dependency: transitive description: name: matcher - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.12.11" material_color_utilities: dependency: transitive description: name: material_color_utilities - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.1.4" menu_base: dependency: transitive description: name: menu_base - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.1.1" meta: dependency: transitive description: name: meta - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.7.0" mime: dependency: transitive description: name: mime - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.2" nested: dependency: transitive description: name: nested - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.0" octo_image: dependency: transitive description: name: octo_image - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.2" package_config: dependency: transitive description: name: package_config - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.0" package_info_plus: dependency: "direct main" description: name: package_info_plus - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.4.2" package_info_plus_linux: dependency: transitive description: name: package_info_plus_linux - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.5" package_info_plus_macos: dependency: transitive description: name: package_info_plus_macos - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.3.0" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.2" package_info_plus_web: dependency: transitive description: name: package_info_plus_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.5" package_info_plus_windows: dependency: transitive description: name: package_info_plus_windows - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.5" path: dependency: transitive description: name: path - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.8.1" path_provider: dependency: "direct main" description: name: path_provider - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.11" path_provider_android: dependency: transitive description: name: path_provider_android - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.17" path_provider_ios: dependency: transitive description: name: path_provider_ios - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.11" path_provider_linux: dependency: transitive description: name: path_provider_linux - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.7" path_provider_macos: dependency: transitive description: name: path_provider_macos - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.6" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.4" path_provider_windows: dependency: transitive description: name: path_provider_windows - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.7" pedantic: dependency: transitive description: name: pedantic - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.11.1" petitparser: dependency: transitive description: name: petitparser - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "5.0.0" platform: dependency: transitive description: name: platform - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.1.0" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.2" pool: dependency: transitive description: name: pool - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.5.1" process: dependency: transitive description: name: process - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "4.2.4" provider: dependency: "direct main" description: name: provider - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "6.0.3" pub_semver: dependency: transitive description: name: pub_semver - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.1" pubspec_parse: dependency: transitive description: name: pubspec_parse - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.2.0" qr_code_scanner: dependency: "direct main" description: name: qr_code_scanner - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.0" quiver: dependency: transitive description: name: quiver - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.1.0" rxdart: dependency: transitive description: name: rxdart - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.27.5" screen_retriever: dependency: transitive description: name: screen_retriever - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.1.2" settings_ui: dependency: "direct main" description: name: settings_ui - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.2" shared_preferences: dependency: "direct main" description: name: shared_preferences - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.15" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.12" shared_preferences_ios: dependency: transitive description: name: shared_preferences_ios - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.1" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.1" shared_preferences_macos: dependency: transitive description: name: shared_preferences_macos - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.4" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.0" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.4" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.1" shelf: dependency: transitive description: name: shelf - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.3.2" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.2" shortid: dependency: transitive description: name: shortid - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.1.2" sky_engine: @@ -932,315 +932,315 @@ packages: dependency: transitive description: name: source_gen - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.2.2" source_span: dependency: transitive description: name: source_span - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.8.2" sqflite: dependency: transitive description: name: sqflite - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.3" sqflite_common: dependency: transitive description: name: sqflite_common - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.2.1+1" stack_trace: dependency: transitive description: name: stack_trace - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.10.0" stream_channel: dependency: transitive description: name: stream_channel - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.0" stream_transform: dependency: transitive description: name: stream_transform - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.0" string_scanner: dependency: transitive description: name: string_scanner - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.1.0" synchronized: dependency: transitive description: name: synchronized - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.0+2" term_glyph: dependency: transitive description: name: term_glyph - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.2.0" test_api: dependency: transitive description: name: test_api - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.4.9" timing: dependency: transitive description: name: timing - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.0" toggle_switch: dependency: "direct main" description: name: toggle_switch - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.4.0" tray_manager: dependency: "direct main" description: name: tray_manager - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.1.7" tuple: dependency: "direct main" description: name: tuple - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.0" typed_data: dependency: transitive description: name: typed_data - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.3.1" url_launcher: dependency: "direct main" description: name: url_launcher - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "6.1.5" url_launcher_android: dependency: transitive description: name: url_launcher_android - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "6.0.17" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "6.0.17" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.1" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.0" url_launcher_web: dependency: transitive description: name: url_launcher_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.12" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.1" uuid: dependency: transitive description: name: uuid - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.6" vector_math: dependency: transitive description: name: vector_math - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.2" video_player: dependency: transitive description: name: video_player - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.4.5" video_player_android: dependency: transitive description: name: video_player_android - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.3.8" video_player_avfoundation: dependency: transitive description: name: video_player_avfoundation - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.3.5" video_player_platform_interface: dependency: transitive description: name: video_player_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "5.1.3" video_player_web: dependency: transitive description: name: video_player_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.12" visibility_detector: dependency: "direct main" description: name: visibility_detector - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.3.3" wakelock: dependency: "direct main" description: name: wakelock - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.5.6" wakelock_macos: dependency: transitive description: name: wakelock_macos - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.4.0" wakelock_platform_interface: dependency: transitive description: name: wakelock_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.3.0" wakelock_web: dependency: transitive description: name: wakelock_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.4.0" wakelock_windows: dependency: transitive description: name: wakelock_windows - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.2.0" watcher: dependency: transitive description: name: watcher - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.1" web_socket_channel: dependency: transitive description: name: web_socket_channel - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.2.0" win32: dependency: transitive description: name: win32 - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.6.1" window_manager: dependency: "direct main" description: name: window_manager - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.2.5" xdg_directories: dependency: transitive description: name: xdg_directories - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.2.0+1" xml: dependency: transitive description: name: xml - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "6.1.0" yaml: dependency: transitive description: name: yaml - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.1.1" zxing2: dependency: "direct main" description: name: zxing2 - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.1.0" sdks: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 4f996c0ca..f4d18c2b2 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -59,7 +59,7 @@ dependencies: desktop_multi_window: git: url: https://github.com/Kingtous/rustdesk_desktop_multi_window - ref: 7b72918710921f5fe79eae2dbaa411a66f5dfb45 + ref: 832c263998275f8e6d3ea196931bc59a54ba9c79 # bitsdojo_window: ^0.1.2 freezed_annotation: ^2.0.3 tray_manager: 0.1.7 From 2a2017df675ef7bf567bfa02696c3319f37d7d4e Mon Sep 17 00:00:00 2001 From: 21pages Date: Wed, 3 Aug 2022 09:08:10 +0800 Subject: [PATCH 0153/2015] copy id/password on double tap, some menu divider Signed-off-by: 21pages --- .../lib/desktop/pages/desktop_home_page.dart | 56 +++++++++++-------- 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/fr.rs | 1 + src/lang/hu.rs | 1 + src/lang/id.rs | 1 + src/lang/it.rs | 1 + src/lang/pl.rs | 1 + src/lang/ptbr.rs | 1 + src/lang/ru.rs | 1 + src/lang/sk.rs | 1 + src/lang/template.rs | 1 + src/lang/tr.rs | 1 + src/lang/tw.rs | 1 + src/lang/vn.rs | 1 + 19 files changed, 51 insertions(+), 23 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 0a86350d8..6dc5e8f2f 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -127,11 +127,16 @@ class _DesktopHomePageState extends State with TrayListener { }) ], ), - TextFormField( - controller: model.serverId, - enableInteractiveSelection: true, - readOnly: true, - ), + GestureDetector( + onDoubleTap: () { + Clipboard.setData( + ClipboardData(text: model.serverId.text)); + showToast(translate("Copied")); + }, + child: TextFormField( + controller: model.serverId, + readOnly: true, + )), ], ), ), @@ -151,7 +156,7 @@ class _DesktopHomePageState extends State with TrayListener { }, onTap: () async { final userName = await gFFI.userModel.getUserName(); - var menu = [ + var menu = [ genEnablePopupMenuItem( translate("Enable Keyboard/Mouse"), 'enable-keyboard', @@ -169,6 +174,7 @@ class _DesktopHomePageState extends State with TrayListener { 'enable-tunnel', ), genAudioInputPopupMenuItem(), + PopupMenuDivider(), PopupMenuItem( child: Text(translate("ID/Relay Server")), value: 'custom-server', @@ -181,7 +187,7 @@ class _DesktopHomePageState extends State with TrayListener { child: Text(translate("Socks5 Proxy")), value: 'socks5-proxy', ), - // sep + PopupMenuDivider(), genEnablePopupMenuItem( translate("Enable Service"), 'stop-service', @@ -195,6 +201,7 @@ class _DesktopHomePageState extends State with TrayListener { translate("Start ID/relay service"), 'stop-rendezvous-service', ), + PopupMenuDivider(), userName.isEmpty ? PopupMenuItem( child: Text(translate("Login")), @@ -208,6 +215,7 @@ class _DesktopHomePageState extends State with TrayListener { child: Text(translate("Change ID")), value: 'change-id', ), + PopupMenuDivider(), genEnablePopupMenuItem( translate("Dark Theme"), 'allow-darktheme', @@ -252,10 +260,16 @@ class _DesktopHomePageState extends State with TrayListener { Row( children: [ Expanded( - child: TextFormField( - controller: model.serverPasswd, - enableInteractiveSelection: true, - readOnly: true, + child: GestureDetector( + onDoubleTap: () { + Clipboard.setData( + ClipboardData(text: model.serverPasswd.text)); + showToast(translate("Copied")); + }, + child: TextFormField( + controller: model.serverPasswd, + readOnly: true, + ), ), ), IconButton( @@ -307,15 +321,15 @@ class _DesktopHomePageState extends State with TrayListener { ), ], ), - value: value, onTap: () => gFFI.serverModel.verificationMethod = value, ); final temporary_enabled = gFFI.serverModel.verificationMethod != kUsePermanentPassword; - var menu = [ + var menu = [ method(translate("Use temporary password"), kUseTemporaryPassword), method(translate("Use permanent password"), kUsePermanentPassword), method(translate("Use both passwords"), kUseBothPasswords), + PopupMenuDivider(), PopupMenuItem( child: Text(translate("Set permanent password")), value: 'set-permanent-password', @@ -323,7 +337,10 @@ class _DesktopHomePageState extends State with TrayListener { kUseTemporaryPassword), PopupMenuItem( child: PopupMenuButton( - child: Text("Set temporary password length"), + padding: EdgeInsets.zero, + child: Text( + translate("Set temporary password length"), + ), itemBuilder: (context) => ["6", "8", "10"] .map((e) => PopupMenuItem( child: Row( @@ -338,7 +355,6 @@ class _DesktopHomePageState extends State with TrayListener { ), ], ), - value: e, onTap: () { if (gFFI.serverModel.temporaryPasswordLength != e) { @@ -350,15 +366,12 @@ class _DesktopHomePageState extends State with TrayListener { .toList(), enabled: temporary_enabled, ), - value: 'set-temporary-password-length', enabled: temporary_enabled), ]; final v = await showMenu(context: context, position: position, items: menu); - if (v != null) { - if (v == "set-permanent-password") { - setPasswordDialog(); - } + if (v == "set-permanent-password") { + setPasswordDialog(); } }, child: Icon(Icons.edit)); @@ -1372,9 +1385,6 @@ void setPasswordDialog() { ), ], ), - SizedBox( - height: 4.0, - ), ], ), ), diff --git a/src/lang/cn.rs b/src/lang/cn.rs index bc06828a5..df5cfdfd7 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -299,5 +299,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Are you sure you want to restart", "确定要重启"), ("Restarting Remote Device", "正在重启远程设备"), ("remote_restarting_tip", "远程设备正在重启, 请关闭当前提示框, 并在一段时间后使用永久密码重新连接"), + ("Copied", "已复制"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 91437b2af..86aada74f 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -299,5 +299,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Are you sure you want to restart", ""), ("Restarting Remote Device", ""), ("remote_restarting_tip", ""), + ("Copied", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 87e687936..8f4861c2a 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -299,5 +299,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Are you sure you want to restart", ""), ("Restarting Remote Device", ""), ("remote_restarting_tip", ""), + ("Copied", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 8acc30991..6af6841b6 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -299,5 +299,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Are you sure you want to restart", "Möchten Sie das entfernte Gerät wirklich neu starten?"), ("Restarting Remote Device", "Entferntes Gerät wird neu gestartet"), ("remote_restarting_tip", "Entferntes Gerät startet neu, bitte schließen Sie diese Meldung und verbinden Sie sich mit dem dauerhaften Passwort erneut."), + ("Copied", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 1be29dd07..0c68bd569 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -299,5 +299,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Are you sure you want to restart", ""), ("Restarting Remote Device", ""), ("remote_restarting_tip", ""), + ("Copied", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 8ae2feecd..9eef5a5a8 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -299,5 +299,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Are you sure you want to restart", "Esta Seguro que desea reiniciar?"), ("Restarting Remote Device", "Reiniciando dispositivo remoto"), ("remote_restarting_tip", "Dispositivo remoto reiniciando, favor de cerrar este mensaje y reconectarse con la contraseña permamente despues de un momento."), + ("Copied", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 7dd0fb9a9..51d079b03 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -299,5 +299,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Are you sure you want to restart", ""), ("Restarting Remote Device", ""), ("remote_restarting_tip", ""), + ("Copied", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index de9f8922b..5590e0ec9 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -299,5 +299,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Are you sure you want to restart", ""), ("Restarting Remote Device", ""), ("remote_restarting_tip", ""), + ("Copied", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 62cb7b6b5..ef1078175 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -299,5 +299,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Are you sure you want to restart", "Apakah Anda yakin untuk memulai ulang"), ("Restarting Remote Device", "Memulai Ulang Perangkat Jarak Jauh"), ("remote_restarting_tip", ""), + ("Copied", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 8058806c2..2834644eb 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -299,5 +299,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Are you sure you want to restart", ""), ("Restarting Remote Device", ""), ("remote_restarting_tip", ""), + ("Copied", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 71b753ac3..8602d0647 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -300,5 +300,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Activate onetime password", "Aktywuj hasło jednorazowe"), ("Set security password", "Ustaw hasło zabezpieczające"), ("Connection not allowed", "Połączenie niedozwolone"), + ("Copied", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 0aac8bc8a..75d3af784 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -299,5 +299,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Are you sure you want to restart", ""), ("Restarting Remote Device", ""), ("remote_restarting_tip", ""), + ("Copied", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 0b18683a2..d44751cd8 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -299,5 +299,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Are you sure you want to restart", "Вы уверены, что хотите выполнить перезапуск?"), ("Restarting Remote Device", "Перезагрузка удаленного устройства"), ("remote_restarting_tip", "Удаленное устройство перезапускается. Пожалуйста, закройте это сообщение и через некоторое время переподключитесь, используя постоянный пароль."), + ("Copied", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 7f07657eb..f94db252b 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -299,5 +299,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Are you sure you want to restart", ""), ("Restarting Remote Device", ""), ("remote_restarting_tip", ""), + ("Copied", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index e6b2bd01d..ca64b2ac7 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -299,5 +299,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Are you sure you want to restart", ""), ("Restarting Remote Device", ""), ("remote_restarting_tip", ""), + ("Copied", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index b06c0e7f3..cff01dcc8 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -299,5 +299,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Are you sure you want to restart", "Yeniden başlatmak istediğinize emin misin?"), ("Restarting Remote Device", "Uzaktan yeniden başlatılıyor"), ("remote_restarting_tip", ""), + ("Copied", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index ce7804a65..79435a69c 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -299,5 +299,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Are you sure you want to restart", "确定要重启"), ("Restarting Remote Device", "正在重啓遠程設備"), ("remote_restarting_tip", "遠程設備正在重啓,請關閉當前提示框,並在一段時間後使用永久密碼重新連接"), + ("Copied", "已複製"), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 014dcb20e..65ffcb61c 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -299,5 +299,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Are you sure you want to restart", "Bạn có chắc bạn muốn khởi động lại không"), ("Restarting Remote Device", "Đang khởi động lại thiết bị từ xa"), ("remote_restarting_tip", "Thiết bị từ xa đang khởi động lại, hãy đóng cửa sổ tin nhắn này và kết nối lại với mật khẩu vĩnh viễn sau một khoảng thời gian"), + ("Copied", ""), ].iter().cloned().collect(); } From b0b6db6160bb3833ed6cb308ed5fc293106cd04e Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 5 Aug 2022 11:07:24 +0800 Subject: [PATCH 0154/2015] flutter_desktop: fix remote menu control and image scaling Signed-off-by: fufesou --- flutter/lib/desktop/pages/remote_page.dart | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index ed62e5067..4672afe62 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -241,7 +241,7 @@ class _RemotePageState extends State super.build(context); Provider.of(context, listen: false).tabBarHeight = super.widget.tabBarHeight; - final pi = Provider.of(context).pi; + final hasDisplays = _ffi.ffiModel.pi.displays.length > 0; final hideKeyboard = isKeyboardShown() && _showEdit; final showActionButton = !_showBar || hideKeyboard; final keyboard = _ffi.ffiModel.permissions['keyboard'] != false; @@ -282,7 +282,7 @@ class _RemotePageState extends State } }); }), - bottomNavigationBar: _showBar && pi.displays.length > 0 + bottomNavigationBar: _showBar && hasDisplays ? getBottomAppBar(keyboard) : null, body: Overlay( @@ -878,7 +878,14 @@ class ImagePainter extends CustomPainter { void paint(Canvas canvas, Size size) { if (image == null) return; canvas.scale(scale, scale); - canvas.drawImage(image!, new Offset(x, y), new Paint()); + // https://github.com/flutter/flutter/issues/76187#issuecomment-784628161 + var paint = new Paint(); + if (scale > 1.00001) { + paint.filterQuality = FilterQuality.high; + } else if (scale < 0.99999) { + paint.filterQuality = FilterQuality.medium; + } + canvas.drawImage(image!, new Offset(x, y), paint); } @override From 0ef1659b8784608e4a56552514d5ca158015cbf1 Mon Sep 17 00:00:00 2001 From: csf Date: Fri, 5 Aug 2022 20:29:43 +0800 Subject: [PATCH 0155/2015] fix mobile features --- flutter/lib/common.dart | 17 +++ flutter/lib/desktop/pages/remote_page.dart | 26 ----- flutter/lib/mobile/pages/home_page.dart | 6 +- flutter/lib/mobile/pages/remote_page.dart | 119 +++++++++----------- flutter/lib/mobile/pages/settings_page.dart | 26 ++--- flutter/lib/models/model.dart | 9 +- 6 files changed, 89 insertions(+), 114 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index f49440655..16e8a172e 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -352,6 +352,23 @@ RadioListTile getRadio( ); } +CheckboxListTile getToggle( + String id, void Function(void Function()) setState, option, name) { + final opt = bind.getSessionToggleOptionSync(id: id, arg: option); + return CheckboxListTile( + value: opt, + onChanged: (v) { + setState(() { + bind.sessionToggleOption(id: id, value: option); + }); + if (option == "show-quality-monitor") { + gFFI.qualityMonitorModel.checkShowQualityMonitor(id); + } + }, + dense: true, + title: Text(translate(name))); +} + /// find ffi, tag is Remote ID /// for session specific usage FFI ffi(String? tag) { diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index ed62e5067..bfc193a89 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -887,32 +887,6 @@ class ImagePainter extends CustomPainter { } } -CheckboxListTile getToggle( - String id, void Function(void Function()) setState, option, name) { - final opt = bind.getSessionToggleOptionSync(id: id, arg: option); - return CheckboxListTile( - value: opt, - onChanged: (v) { - setState(() { - bind.sessionToggleOption(id: id, value: option); - }); - }, - dense: true, - title: Text(translate(name))); -} - -RadioListTile getRadio(String name, String toValue, String curValue, - void Function(String?) onChange) { - return RadioListTile( - controlAffinity: ListTileControlAffinity.trailing, - title: Text(translate(name)), - value: toValue, - groupValue: curValue, - onChanged: onChange, - dense: true, - ); -} - void showOptions(String id) async { String quality = await bind.getSessionImageQuality(id: id) ?? 'balanced'; if (quality == '') quality = 'balanced'; diff --git a/flutter/lib/mobile/pages/home_page.dart b/flutter/lib/mobile/pages/home_page.dart index e56434487..6bf0be2c7 100644 --- a/flutter/lib/mobile/pages/home_page.dart +++ b/flutter/lib/mobile/pages/home_page.dart @@ -12,10 +12,10 @@ abstract class PageShape extends Widget { final List appBarActions = []; } -final homeKey = GlobalKey<_HomePageState>(); - class HomePage extends StatefulWidget { - HomePage({Key? key}) : super(key: key); + static final homeKey = GlobalKey<_HomePageState>(); + + HomePage() : super(key: homeKey); @override _HomePageState createState() => _HomePageState(); diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 980f665e7..6ea4ca2e6 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -12,6 +12,7 @@ import 'package:wakelock/wakelock.dart'; import '../../common.dart'; import '../../models/model.dart'; +import '../../models/platform_model.dart'; import '../widgets/dialog.dart'; import '../widgets/gestures.dart'; import '../widgets/overlay.dart'; @@ -135,7 +136,7 @@ class _RemotePageState extends State { if (newValue.length > common) { var s = newValue.substring(common); if (s.length > 1) { - gFFI.setByName('input_string', s); + bind.sessionInputString(id: widget.id, value: s); } else { inputChar(s); } @@ -169,11 +170,11 @@ class _RemotePageState extends State { content == '()' || content == '【】')) { // can not only input content[0], because when input ], [ are also auo insert, which cause ] never be input - gFFI.setByName('input_string', content); + bind.sessionInputString(id: widget.id, value: content); openKeyboard(); return; } - gFFI.setByName('input_string', content); + bind.sessionInputString(id: widget.id, value: content); } else { inputChar(content); } @@ -409,7 +410,7 @@ class _RemotePageState extends State { icon: Icon(Icons.tv), onPressed: () { setState(() => _showEdit = false); - showOptions(); + showOptions(widget.id); }, ) ] + @@ -461,7 +462,7 @@ class _RemotePageState extends State { icon: Icon(Icons.more_vert), onPressed: () { setState(() => _showEdit = false); - showActions(); + showActions(widget.id); }, ), ]), @@ -573,7 +574,7 @@ class _RemotePageState extends State { }, onTwoFingerScaleEnd: (d) { _scale = 1; - gFFI.setByName('peer_option', '{"name": "view-style", "value": ""}'); + bind.sessionPeerOption(id: widget.id, name: "view-style", value: ""); }, onThreeFingerVerticalDragUpdate: gFFI.ffiModel.isPeerAndroid ? null @@ -620,8 +621,9 @@ class _RemotePageState extends State { Widget getBodyForDesktopWithListener(bool keyboard) { var paints = [ImagePaint()]; - if (keyboard || - gFFI.getByName('toggle_option', 'show-remote-cursor') == 'true') { + final cursor = bind.getSessionToggleOptionSync( + id: widget.id, arg: 'show-remote-cursor'); + if (keyboard || cursor) { paints.add(CursorPaint()); } return Container( @@ -649,7 +651,7 @@ class _RemotePageState extends State { return out; } - void showActions() { + void showActions(String id) async { final size = MediaQuery.of(context).size; final x = 120.0; final y = size.height; @@ -668,7 +670,7 @@ class _RemotePageState extends State { style: flatButtonStyle, onPressed: () { Navigator.pop(context); - showSetOSPassword(false); + showSetOSPassword(id, false); }, child: Icon(Icons.edit, color: MyTheme.accent), ) @@ -691,7 +693,8 @@ class _RemotePageState extends State { more.add(PopupMenuItem( child: Text(translate('Insert Lock')), value: 'lock')); if (pi.platform == 'Windows' && - gFFI.getByName('toggle_option', 'privacy-mode') != 'true') { + await bind.getSessionToggleOption(id: id, arg: 'privacy-mode') != + true) { more.add(PopupMenuItem( child: Text(translate((gFFI.ffiModel.inputBlocked ? 'Unb' : 'B') + 'lock user input')), @@ -713,28 +716,29 @@ class _RemotePageState extends State { elevation: 8, ); if (value == 'cad') { - gFFI.setByName('ctrl_alt_del'); + bind.sessionCtrlAltDel(id: widget.id); } else if (value == 'lock') { - gFFI.setByName('lock_screen'); + bind.sessionLockScreen(id: widget.id); } else if (value == 'block-input') { - gFFI.setByName('toggle_option', - (gFFI.ffiModel.inputBlocked ? 'un' : '') + 'block-input'); + bind.sessionToggleOption( + id: widget.id, + value: (gFFI.ffiModel.inputBlocked ? 'un' : '') + 'block-input'); gFFI.ffiModel.inputBlocked = !gFFI.ffiModel.inputBlocked; } else if (value == 'refresh') { - gFFI.setByName('refresh'); + bind.sessionRefresh(id: widget.id); } else if (value == 'paste') { () async { ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain); if (data != null && data.text != null) { - gFFI.setByName('input_string', '${data.text}'); + bind.sessionInputString(id: widget.id, value: data.text ?? ""); } }(); } else if (value == 'enter_os_password') { - var password = gFFI.getByName('peer_option', "os-password"); - if (password != "") { - gFFI.setByName('input_os_password', password); + var password = await bind.getSessionOption(id: id, arg: "os-password"); + if (password != null) { + bind.sessionInputOsPassword(id: widget.id, value: password); } else { - showSetOSPassword(true); + showSetOSPassword(id, true); } } else if (value == 'reset_canvas') { gFFI.cursorModel.reset(); @@ -762,8 +766,8 @@ class _RemotePageState extends State { onTouchModeChange: (t) { gFFI.ffiModel.toggleTouchMode(); final v = gFFI.ffiModel.touchMode ? 'Y' : ''; - gFFI.setByName('peer_option', - '{"name": "touch-mode", "value": "$v"}'); + bind.sessionPeerOption( + id: widget.id, name: "touch", value: v); })); })); } @@ -978,23 +982,23 @@ class QualityMonitor extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "Speed: ${qualityMonitorModel.data.speed}", + "Speed: ${qualityMonitorModel.data.speed ?? ''}", style: TextStyle(color: MyTheme.grayBg), ), Text( - "FPS: ${qualityMonitorModel.data.fps}", + "FPS: ${qualityMonitorModel.data.fps ?? ''}", style: TextStyle(color: MyTheme.grayBg), ), Text( - "Delay: ${qualityMonitorModel.data.delay} ms", + "Delay: ${qualityMonitorModel.data.delay ?? ''} ms", style: TextStyle(color: MyTheme.grayBg), ), Text( - "Target Bitrate: ${qualityMonitorModel.data.targetBitrate}kb", + "Target Bitrate: ${qualityMonitorModel.data.targetBitrate ?? ''}kb", style: TextStyle(color: MyTheme.grayBg), ), Text( - "Codec: ${qualityMonitorModel.data.codecFormat}", + "Codec: ${qualityMonitorModel.data.codecFormat ?? ''}", style: TextStyle(color: MyTheme.grayBg), ), ], @@ -1003,26 +1007,11 @@ class QualityMonitor extends StatelessWidget { : SizedBox.shrink()))); } -CheckboxListTile getToggle( - void Function(void Function()) setState, option, name) { - return CheckboxListTile( - value: gFFI.getByName('toggle_option', option) == 'true', - onChanged: (v) { - setState(() { - gFFI.setByName('toggle_option', option); - }); - if (option == "show-quality-monitor") { - gFFI.qualityMonitorModel.checkShowQualityMonitor(); - } - }, - dense: true, - title: Text(translate(name))); -} - -void showOptions() { - String quality = gFFI.getByName('image_quality'); +void showOptions(String id) async { + String quality = await bind.getSessionImageQuality(id: id) ?? 'balanced'; if (quality == '') quality = 'balanced'; - String viewStyle = gFFI.getByName('peer_option', 'view-style'); + String viewStyle = + await bind.getSessionOption(id: id, arg: 'view-style') ?? ''; var displays = []; final pi = gFFI.ffiModel.pi; final image = gFFI.ffiModel.getConnectionImage(); @@ -1035,7 +1024,7 @@ void showOptions() { children.add(InkWell( onTap: () { if (i == cur) return; - gFFI.setByName('switch_display', i.toString()); + bind.sessionSwitchDisplay(id: id, value: i); SmartDialog.dismiss(); }, child: Ink( @@ -1064,30 +1053,30 @@ void showOptions() { DialogManager.show((setState, close) { final more = []; if (perms['audio'] != false) { - more.add(getToggle(setState, 'disable-audio', 'Mute')); + more.add(getToggle(id, setState, 'disable-audio', 'Mute')); } if (perms['keyboard'] != false) { if (perms['clipboard'] != false) - more.add(getToggle(setState, 'disable-clipboard', 'Disable clipboard')); + more.add( + getToggle(id, setState, 'disable-clipboard', 'Disable clipboard')); more.add(getToggle( - setState, 'lock-after-session-end', 'Lock after session end')); + id, setState, 'lock-after-session-end', 'Lock after session end')); if (pi.platform == 'Windows') { - more.add(getToggle(setState, 'privacy-mode', 'Privacy mode')); + more.add(getToggle(id, setState, 'privacy-mode', 'Privacy mode')); } } var setQuality = (String? value) { if (value == null) return; setState(() { quality = value; - gFFI.setByName('image_quality', value); + bind.sessionSetImageQuality(id: id, value: value); }); }; var setViewStyle = (String? value) { if (value == null) return; setState(() { viewStyle = value; - gFFI.setByName( - 'peer_option', '{"name": "view-style", "value": "$value"}'); + bind.sessionPeerOption(id: id, name: "view-style", value: value); gFFI.canvasModel.updateViewStyle(); }); }; @@ -1105,9 +1094,10 @@ void showOptions() { getRadio('Balanced', 'balanced', quality, setQuality), getRadio('Optimize reaction time', 'low', quality, setQuality), Divider(color: MyTheme.border), - getToggle(setState, 'show-remote-cursor', 'Show remote cursor'), getToggle( - setState, 'show-quality-monitor', 'Show quality monitor'), + id, setState, 'show-remote-cursor', 'Show remote cursor'), + getToggle(id, setState, 'show-quality-monitor', + 'Show quality monitor'), ] + more), actions: [], @@ -1137,10 +1127,10 @@ void showRestartRemoteDevice(PeerInfo pi, String id) async { if (res == true) gFFI.setByName('restart_remote_device'); } -void showSetOSPassword(bool login) { +void showSetOSPassword(String id, bool login) async { final controller = TextEditingController(); - var password = gFFI.getByName('peer_option', "os-password"); - var autoLogin = gFFI.getByName('peer_option', "auto-login") != ""; + var password = await bind.getSessionOption(id: id, arg: "os-password") ?? ""; + var autoLogin = await bind.getSessionOption(id: id, arg: "auto-login") != ""; controller.text = password; DialogManager.show((setState, close) { return CustomAlertDialog( @@ -1173,12 +1163,11 @@ void showSetOSPassword(bool login) { style: flatButtonStyle, onPressed: () { var text = controller.text.trim(); - gFFI.setByName( - 'peer_option', '{"name": "os-password", "value": "$text"}'); - gFFI.setByName('peer_option', - '{"name": "auto-login", "value": "${autoLogin ? 'Y' : ''}"}'); + bind.sessionPeerOption(id: id, name: "os-password", value: text); + bind.sessionPeerOption( + id: id, name: "auto-login", value: autoLogin ? 'Y' : ''); if (text != "" && login) { - gFFI.setByName('input_os_password', text); + bind.sessionInputOsPassword(id: id, value: text); } close(); }, diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index ab7b2584d..01cf4ae5d 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -9,6 +9,7 @@ import 'package:url_launcher/url_launcher.dart'; import '../../common.dart'; import '../../models/model.dart'; +import '../../models/platform_model.dart'; import '../widgets/dialog.dart'; import 'home_page.dart'; import 'scan_page.dart'; @@ -192,21 +193,18 @@ void showServerSettings() { showServerSettingsWithValue(id, relay, key, api); } -void showLanguageSettings() { +void showLanguageSettings() async { try { final langs = json.decode(gFFI.getByName('langs')) as List; - var lang = gFFI.getByName('local_option', 'lang'); + var lang = await bind.mainGetLocalOption(key: "lang"); DialogManager.show((setState, close) { final setLang = (v) { if (lang != v) { setState(() { lang = v; }); - final msg = Map() - ..['name'] = 'lang' - ..['value'] = v; - gFFI.setByName('local_option', json.encode(msg)); - homeKey.currentState?.refreshPages(); + bind.mainSetLocalOption(key: "lang", value: v); + HomePage.homeKey.currentState?.refreshPages(); Future.delayed(Duration(milliseconds: 200), close); } }; @@ -277,8 +275,8 @@ fetch('http://localhost:21114/api/login', { final body = { 'username': name, 'password': pass, - 'id': gFFI.getByName('server_id'), - 'uuid': gFFI.getByName('uuid') + 'id': bind.mainGetMyId(), + 'uuid': bind.mainGetUuid() }; try { final response = await http.post(Uri.parse('$url/api/login'), @@ -314,10 +312,7 @@ void refreshCurrentUser() async { final token = gFFI.getByName("option", "access_token"); if (token == '') return; final url = getUrl(); - final body = { - 'id': gFFI.getByName('server_id'), - 'uuid': gFFI.getByName('uuid') - }; + final body = {'id': bind.mainGetMyId(), 'uuid': bind.mainGetUuid()}; try { final response = await http.post(Uri.parse('$url/api/currentUser'), headers: { @@ -340,10 +335,7 @@ void logout() async { final token = gFFI.getByName("option", "access_token"); if (token == '') return; final url = getUrl(); - final body = { - 'id': gFFI.getByName('server_id'), - 'uuid': gFFI.getByName('uuid') - }; + final body = {'id': bind.mainGetMyId(), 'uuid': bind.mainGetUuid()}; try { await http.post(Uri.parse('$url/api/logout'), headers: { diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 612dd04ac..f67d0d5fa 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -171,6 +171,8 @@ class FfiModel with ChangeNotifier { parent.target?.serverModel.onClientAuthorized(evt); } else if (name == 'on_client_remove') { parent.target?.serverModel.onClientRemove(evt); + } else if (name == 'update_quality_status') { + parent.target?.qualityMonitorModel.updateQualityStatus(evt); } }; } @@ -807,9 +809,10 @@ class QualityMonitorModel with ChangeNotifier { bool get show => _show; QualityMonitorData get data => _data; - checkShowQualityMonitor() { - final show = - gFFI.getByName('toggle_option', 'show-quality-monitor') == 'true'; + checkShowQualityMonitor(String id) async { + final show = await bind.getSessionToggleOption( + id: id, arg: 'show-quality-monitor') == + true; if (_show != show) { _show = show; notifyListeners(); From e5e57943cb31ad6937d8b9d153e1ee73c6f6458a Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 5 Aug 2022 23:32:51 +0800 Subject: [PATCH 0156/2015] revert nl lang, because it screw up lang.rs, and nl.rs is not valid utf-8 file, can not be compiled --- src/lang.rs | 23 ++-- src/lang/nl.rs | 303 ------------------------------------------------- 2 files changed, 13 insertions(+), 313 deletions(-) delete mode 100644 src/lang/nl.rs diff --git a/src/lang.rs b/src/lang.rs index 38b97a41a..400c4dd95 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -8,18 +8,18 @@ mod de; mod en; mod eo; mod es; -mod hu; mod fr; +mod hu; mod id; mod it; -mod nl; +mod ja; +mod pl; mod ptbr; mod ru; mod sk; mod tr; mod tw; mod vn; -mod pl; lazy_static::lazy_static! { pub static ref LANGS: Value = @@ -28,7 +28,6 @@ lazy_static::lazy_static! { ("it", "Italiano"), ("fr", "Français"), ("de", "Deutsch"), - ("nl","Nederlands"), ("cn", "简体中文"), ("tw", "繁體中文"), ("pt", "Português"), @@ -43,6 +42,7 @@ lazy_static::lazy_static! { ("tr", "Türkçe"), ("vn", "Tiếng Việt"), ("pl", "Polski"), + ("ja", "日本語"), ]); } @@ -89,16 +89,19 @@ pub fn translate_locale(name: String, locale: &str) -> String { "sk" => sk::T.deref(), "vn" => vn::T.deref(), "pl" => pl::T.deref(), + "ja" => ja::T.deref(), _ => en::T.deref(), }; if let Some(v) = m.get(&name as &str) { - v.to_string() - } else { - if lang != "en" { - if let Some(v) = en::T.get(&name as &str) { - return v.to_string(); + if v.is_empty() { + if lang != "en" { + if let Some(v) = en::T.get(&name as &str) { + return v.to_string(); + } } + } else { + return v.to_string(); } - name } + name } diff --git a/src/lang/nl.rs b/src/lang/nl.rs deleted file mode 100644 index 5b2838bc1..000000000 --- a/src/lang/nl.rs +++ /dev/null @@ -1,303 +0,0 @@ -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", "Bevestigd"), - ("connecting_status", "Verbinding maken met het RustDesk netwerk..."), - ("Enable Service", "Service Inschakelen"), - ("Start Service", "Start Service"), - ("Service is running", "De service draait."), - ("Service is not running", "De service draait 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"), - ("Stop service", "Stop service"), - ("Change ID", "Wijzig ID"), - ("Website", "Website"), - ("About", "Over"), - ("Mute", "Geluid uit"), - ("Audio Input", "Audio Ingang"), - ("Enhancements", "Verbeteringen"), - ("Hardware Codec", "Hardware Codec"), - ("Adaptive Bitrate", "Aangepaste Bitsnelheid"), - ("ID Server", "ID Server"), - ("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", "Voer uw wachtwoord in"), - ("Remember password", "Wachtwoord onthouden"), - ("Wrong Password", "Verkeerd wachtwoord"), - ("Do you want to enter again?", "Wil je opnieuw invoeren?"), - ("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", "Verborgen bestanden tonen"), - ("Receive", "Ontvangen"), - ("Send", "Verzenden"), - ("Refresh File", "Bestand Verversen"), - ("Local", "Lokaal"), - ("Remote", "Op afstand"), - ("Remote Computer", "Externe Computer"), - ("Local Computer", "Locale Computer"), - ("Confirm Delete", "Bevestig Verwijderen"), - ("Delete", "Verwijder"), - ("Properties", "Eigenschappen"), - ("Multi Select", "Meervoudig selecteren"), - ("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"), - ("Good image quality", "Goede beeldkwaliteit"), - ("Balanced", "Gebalanceerd"), - ("Optimize reaction time", "Optimaliseer reactietijd"), - ("Custom", "Aangepast"), - ("Show remote cursor", "Toon cursor op 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 genstalleerde 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"), - ("setup_server_tip", "Als u een snellere verbindingssnelheid nodig heeft, kunt u ervoor kiezen om uw eigen server te creren"), - ("Too short, at least 6 characters.", "Te 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", "Kopiren 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", "Port"), - ("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"), - ("Logout", "Log Uit"), - ("Tags", "Labels"), - ("Search ID", "Zoek ID"), - ("Current Wayland display server is not supported", "Huidige Wayland weergaveserver wordt niet ondersteund"), - ("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", "En-Vinger Tik"), - ("Left Mouse", "Linkermuis"), - ("One-Long Tap", "n-Vinger-Lange-Tik"), - ("Two-Finger Tap", "Twee-Vingers-Tik"), - ("Right Mouse", "Rechter muis"), - ("One-Finger Move", "En-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 [Genstalleerde 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"), - ("Use temporary password", "Tijdelijk wachtwoord gebruiken"), - ("Use permanent password", "Gebruik permanent wachtwoord"), - ("Use both passwords", "Gebruik beide wachtwoorden"), - ("Set permanent password", "Stel permanent wachtwoord in"), - ("Set temporary password length", "Lengte tijdelijk wachtwoord instellen"), - ("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."), - ].iter().cloned().collect(); -} From 511f3c022f6598874b7e1694131e6eca4e7c0e36 Mon Sep 17 00:00:00 2001 From: fufesou Date: Sat, 6 Aug 2022 18:48:07 +0800 Subject: [PATCH 0157/2015] flutter_desktop: fix ffi model provider Signed-off-by: fufesou --- .../lib/desktop/pages/connection_page.dart | 2 + flutter/lib/desktop/pages/remote_page.dart | 190 +++++++++--------- 2 files changed, 98 insertions(+), 94 deletions(-) diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index e32275373..2552a1425 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -58,6 +58,8 @@ class _ConnectionPageState extends State { @override Widget build(BuildContext context) { + Provider.of(context); + if (_idController.text.isEmpty) _idController.text = gFFI.getId(); return Container( decoration: BoxDecoration(color: isDarkTheme() ? null : MyTheme.grayBg), diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 4672afe62..d79e6992c 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -236,68 +236,66 @@ class _RemotePageState extends State _ffi.inputKey(label, down: down, press: press ?? false); } + Widget buildBody(FfiModel ffiModel) { + final hasDisplays = ffiModel.pi.displays.length > 0; + final hideKeyboard = isKeyboardShown() && _showEdit; + final showActionButton = !_showBar || hideKeyboard; + final keyboard = ffiModel.permissions['keyboard'] != false; + return Scaffold( + // resizeToAvoidBottomInset: true, + floatingActionButton: !showActionButton + ? null + : FloatingActionButton( + mini: !hideKeyboard, + child: + Icon(hideKeyboard ? Icons.expand_more : Icons.expand_less), + backgroundColor: MyTheme.accent, + onPressed: () { + setState(() { + if (hideKeyboard) { + _showEdit = false; + _ffi.invokeMethod("enable_soft_keyboard", false); + _mobileFocusNode.unfocus(); + _physicalFocusNode.requestFocus(); + } else { + _showBar = !_showBar; + } + }); + }), + bottomNavigationBar: _showBar && hasDisplays ? getBottomAppBar() : null, + body: Overlay( + initialEntries: [ + OverlayEntry(builder: (context) { + return Container( + color: Colors.black, + child: getBodyForDesktopWithListener(keyboard)); + }) + ], + )); + } + @override Widget build(BuildContext context) { super.build(context); Provider.of(context, listen: false).tabBarHeight = super.widget.tabBarHeight; - final hasDisplays = _ffi.ffiModel.pi.displays.length > 0; - final hideKeyboard = isKeyboardShown() && _showEdit; - final showActionButton = !_showBar || hideKeyboard; - final keyboard = _ffi.ffiModel.permissions['keyboard'] != false; return WillPopScope( onWillPop: () async { clientClose(); return false; }, child: MultiProvider( - providers: [ - ChangeNotifierProvider.value(value: _ffi.ffiModel), - ChangeNotifierProvider.value(value: _ffi.imageModel), - ChangeNotifierProvider.value(value: _ffi.cursorModel), - ChangeNotifierProvider.value(value: _ffi.canvasModel), - ], - child: getRawPointerAndKeyBody( - keyboard, - Scaffold( - // resizeToAvoidBottomInset: true, - floatingActionButton: !showActionButton - ? null - : FloatingActionButton( - mini: !hideKeyboard, - child: Icon(hideKeyboard - ? Icons.expand_more - : Icons.expand_less), - backgroundColor: MyTheme.accent, - onPressed: () { - setState(() { - if (hideKeyboard) { - _showEdit = false; - _ffi.invokeMethod( - "enable_soft_keyboard", false); - _mobileFocusNode.unfocus(); - _physicalFocusNode.requestFocus(); - } else { - _showBar = !_showBar; - } - }); - }), - bottomNavigationBar: _showBar && hasDisplays - ? getBottomAppBar(keyboard) - : null, - body: Overlay( - initialEntries: [ - OverlayEntry(builder: (context) { - return Container( - color: Colors.black, - child: getBodyForDesktopWithListener(keyboard)); - }) - ], - ))), - )); + providers: [ + ChangeNotifierProvider.value(value: _ffi.ffiModel), + ChangeNotifierProvider.value(value: _ffi.imageModel), + ChangeNotifierProvider.value(value: _ffi.cursorModel), + ChangeNotifierProvider.value(value: _ffi.canvasModel), + ], + child: getRawPointerAndKeyBody(Consumer( + builder: (context, ffiModel, _child) => buildBody(ffiModel))))); } - Widget getRawPointerAndKeyBody(bool keyboard, Widget child) { + Widget getRawPointerAndKeyBody(Widget child) { return Listener( onPointerHover: (e) { if (e.kind != ui.PointerDeviceKind.mouse) return; @@ -352,55 +350,58 @@ class _RemotePageState extends State '{"id": "${widget.id}", "type": "wheel", "x": "$dx", "y": "$dy"}'); } }, - child: MouseRegion( - cursor: keyboard ? SystemMouseCursors.none : MouseCursor.defer, - child: FocusScope( - autofocus: true, - child: Focus( + child: Consumer( + builder: (context, FfiModel, _child) => MouseRegion( + cursor: FfiModel.permissions['keyboard'] != false + ? SystemMouseCursors.none + : MouseCursor.defer, + child: FocusScope( autofocus: true, - canRequestFocus: true, - focusNode: _physicalFocusNode, - onKey: (data, e) { - final key = e.logicalKey; - if (e is RawKeyDownEvent) { - if (e.repeat) { - sendRawKey(e, press: true); - } else { - if (e.isAltPressed && !_ffi.alt) { - _ffi.alt = true; - } else if (e.isControlPressed && !_ffi.ctrl) { - _ffi.ctrl = true; - } else if (e.isShiftPressed && !_ffi.shift) { - _ffi.shift = true; - } else if (e.isMetaPressed && !_ffi.command) { - _ffi.command = true; + child: Focus( + autofocus: true, + canRequestFocus: true, + focusNode: _physicalFocusNode, + onKey: (data, e) { + final key = e.logicalKey; + if (e is RawKeyDownEvent) { + if (e.repeat) { + sendRawKey(e, press: true); + } else { + if (e.isAltPressed && !_ffi.alt) { + _ffi.alt = true; + } else if (e.isControlPressed && !_ffi.ctrl) { + _ffi.ctrl = true; + } else if (e.isShiftPressed && !_ffi.shift) { + _ffi.shift = true; + } else if (e.isMetaPressed && !_ffi.command) { + _ffi.command = true; + } + sendRawKey(e, down: true); + } } - sendRawKey(e, down: true); - } - } - // [!_showEdit] workaround for soft-keyboard's control_key like Backspace / Enter - if (!_showEdit && e is RawKeyUpEvent) { - if (key == LogicalKeyboardKey.altLeft || - key == LogicalKeyboardKey.altRight) { - _ffi.alt = false; - } else if (key == LogicalKeyboardKey.controlLeft || - key == LogicalKeyboardKey.controlRight) { - _ffi.ctrl = false; - } else if (key == LogicalKeyboardKey.shiftRight || - key == LogicalKeyboardKey.shiftLeft) { - _ffi.shift = false; - } else if (key == LogicalKeyboardKey.metaLeft || - key == LogicalKeyboardKey.metaRight) { - _ffi.command = false; - } - sendRawKey(e); - } - return KeyEventResult.handled; - }, - child: child)))); + // [!_showEdit] workaround for soft-keyboard's control_key like Backspace / Enter + if (!_showEdit && e is RawKeyUpEvent) { + if (key == LogicalKeyboardKey.altLeft || + key == LogicalKeyboardKey.altRight) { + _ffi.alt = false; + } else if (key == LogicalKeyboardKey.controlLeft || + key == LogicalKeyboardKey.controlRight) { + _ffi.ctrl = false; + } else if (key == LogicalKeyboardKey.shiftRight || + key == LogicalKeyboardKey.shiftLeft) { + _ffi.shift = false; + } else if (key == LogicalKeyboardKey.metaLeft || + key == LogicalKeyboardKey.metaRight) { + _ffi.command = false; + } + sendRawKey(e); + } + return KeyEventResult.handled; + }, + child: child))))); } - Widget getBottomAppBar(bool keyboard) { + Widget? getBottomAppBar() { return BottomAppBar( elevation: 10, color: MyTheme.accent, @@ -515,6 +516,7 @@ class _RemotePageState extends State )); } paints.add(getHelpTools()); + return MouseRegion( onEnter: (evt) { bind.hostStopSystemKeyPropagate(stopped: false); From 0e012894b5567009a5b4bc1346486ad6341e6adc Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 5 Aug 2022 11:07:24 +0800 Subject: [PATCH 0158/2015] flutter_desktop: fix remote menu control and image scaling Signed-off-by: fufesou --- flutter/lib/desktop/pages/remote_page.dart | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index ed62e5067..4672afe62 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -241,7 +241,7 @@ class _RemotePageState extends State super.build(context); Provider.of(context, listen: false).tabBarHeight = super.widget.tabBarHeight; - final pi = Provider.of(context).pi; + final hasDisplays = _ffi.ffiModel.pi.displays.length > 0; final hideKeyboard = isKeyboardShown() && _showEdit; final showActionButton = !_showBar || hideKeyboard; final keyboard = _ffi.ffiModel.permissions['keyboard'] != false; @@ -282,7 +282,7 @@ class _RemotePageState extends State } }); }), - bottomNavigationBar: _showBar && pi.displays.length > 0 + bottomNavigationBar: _showBar && hasDisplays ? getBottomAppBar(keyboard) : null, body: Overlay( @@ -878,7 +878,14 @@ class ImagePainter extends CustomPainter { void paint(Canvas canvas, Size size) { if (image == null) return; canvas.scale(scale, scale); - canvas.drawImage(image!, new Offset(x, y), new Paint()); + // https://github.com/flutter/flutter/issues/76187#issuecomment-784628161 + var paint = new Paint(); + if (scale > 1.00001) { + paint.filterQuality = FilterQuality.high; + } else if (scale < 0.99999) { + paint.filterQuality = FilterQuality.medium; + } + canvas.drawImage(image!, new Offset(x, y), paint); } @override From 917830fb69a7752b55355fa0bc5b26ce7465525f Mon Sep 17 00:00:00 2001 From: fufesou Date: Sat, 6 Aug 2022 18:48:07 +0800 Subject: [PATCH 0159/2015] flutter_desktop: fix ffi model provider Signed-off-by: fufesou --- .../lib/desktop/pages/connection_page.dart | 2 + flutter/lib/desktop/pages/remote_page.dart | 190 +++++++++--------- 2 files changed, 98 insertions(+), 94 deletions(-) diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index e32275373..2552a1425 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -58,6 +58,8 @@ class _ConnectionPageState extends State { @override Widget build(BuildContext context) { + Provider.of(context); + if (_idController.text.isEmpty) _idController.text = gFFI.getId(); return Container( decoration: BoxDecoration(color: isDarkTheme() ? null : MyTheme.grayBg), diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 4672afe62..d79e6992c 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -236,68 +236,66 @@ class _RemotePageState extends State _ffi.inputKey(label, down: down, press: press ?? false); } + Widget buildBody(FfiModel ffiModel) { + final hasDisplays = ffiModel.pi.displays.length > 0; + final hideKeyboard = isKeyboardShown() && _showEdit; + final showActionButton = !_showBar || hideKeyboard; + final keyboard = ffiModel.permissions['keyboard'] != false; + return Scaffold( + // resizeToAvoidBottomInset: true, + floatingActionButton: !showActionButton + ? null + : FloatingActionButton( + mini: !hideKeyboard, + child: + Icon(hideKeyboard ? Icons.expand_more : Icons.expand_less), + backgroundColor: MyTheme.accent, + onPressed: () { + setState(() { + if (hideKeyboard) { + _showEdit = false; + _ffi.invokeMethod("enable_soft_keyboard", false); + _mobileFocusNode.unfocus(); + _physicalFocusNode.requestFocus(); + } else { + _showBar = !_showBar; + } + }); + }), + bottomNavigationBar: _showBar && hasDisplays ? getBottomAppBar() : null, + body: Overlay( + initialEntries: [ + OverlayEntry(builder: (context) { + return Container( + color: Colors.black, + child: getBodyForDesktopWithListener(keyboard)); + }) + ], + )); + } + @override Widget build(BuildContext context) { super.build(context); Provider.of(context, listen: false).tabBarHeight = super.widget.tabBarHeight; - final hasDisplays = _ffi.ffiModel.pi.displays.length > 0; - final hideKeyboard = isKeyboardShown() && _showEdit; - final showActionButton = !_showBar || hideKeyboard; - final keyboard = _ffi.ffiModel.permissions['keyboard'] != false; return WillPopScope( onWillPop: () async { clientClose(); return false; }, child: MultiProvider( - providers: [ - ChangeNotifierProvider.value(value: _ffi.ffiModel), - ChangeNotifierProvider.value(value: _ffi.imageModel), - ChangeNotifierProvider.value(value: _ffi.cursorModel), - ChangeNotifierProvider.value(value: _ffi.canvasModel), - ], - child: getRawPointerAndKeyBody( - keyboard, - Scaffold( - // resizeToAvoidBottomInset: true, - floatingActionButton: !showActionButton - ? null - : FloatingActionButton( - mini: !hideKeyboard, - child: Icon(hideKeyboard - ? Icons.expand_more - : Icons.expand_less), - backgroundColor: MyTheme.accent, - onPressed: () { - setState(() { - if (hideKeyboard) { - _showEdit = false; - _ffi.invokeMethod( - "enable_soft_keyboard", false); - _mobileFocusNode.unfocus(); - _physicalFocusNode.requestFocus(); - } else { - _showBar = !_showBar; - } - }); - }), - bottomNavigationBar: _showBar && hasDisplays - ? getBottomAppBar(keyboard) - : null, - body: Overlay( - initialEntries: [ - OverlayEntry(builder: (context) { - return Container( - color: Colors.black, - child: getBodyForDesktopWithListener(keyboard)); - }) - ], - ))), - )); + providers: [ + ChangeNotifierProvider.value(value: _ffi.ffiModel), + ChangeNotifierProvider.value(value: _ffi.imageModel), + ChangeNotifierProvider.value(value: _ffi.cursorModel), + ChangeNotifierProvider.value(value: _ffi.canvasModel), + ], + child: getRawPointerAndKeyBody(Consumer( + builder: (context, ffiModel, _child) => buildBody(ffiModel))))); } - Widget getRawPointerAndKeyBody(bool keyboard, Widget child) { + Widget getRawPointerAndKeyBody(Widget child) { return Listener( onPointerHover: (e) { if (e.kind != ui.PointerDeviceKind.mouse) return; @@ -352,55 +350,58 @@ class _RemotePageState extends State '{"id": "${widget.id}", "type": "wheel", "x": "$dx", "y": "$dy"}'); } }, - child: MouseRegion( - cursor: keyboard ? SystemMouseCursors.none : MouseCursor.defer, - child: FocusScope( - autofocus: true, - child: Focus( + child: Consumer( + builder: (context, FfiModel, _child) => MouseRegion( + cursor: FfiModel.permissions['keyboard'] != false + ? SystemMouseCursors.none + : MouseCursor.defer, + child: FocusScope( autofocus: true, - canRequestFocus: true, - focusNode: _physicalFocusNode, - onKey: (data, e) { - final key = e.logicalKey; - if (e is RawKeyDownEvent) { - if (e.repeat) { - sendRawKey(e, press: true); - } else { - if (e.isAltPressed && !_ffi.alt) { - _ffi.alt = true; - } else if (e.isControlPressed && !_ffi.ctrl) { - _ffi.ctrl = true; - } else if (e.isShiftPressed && !_ffi.shift) { - _ffi.shift = true; - } else if (e.isMetaPressed && !_ffi.command) { - _ffi.command = true; + child: Focus( + autofocus: true, + canRequestFocus: true, + focusNode: _physicalFocusNode, + onKey: (data, e) { + final key = e.logicalKey; + if (e is RawKeyDownEvent) { + if (e.repeat) { + sendRawKey(e, press: true); + } else { + if (e.isAltPressed && !_ffi.alt) { + _ffi.alt = true; + } else if (e.isControlPressed && !_ffi.ctrl) { + _ffi.ctrl = true; + } else if (e.isShiftPressed && !_ffi.shift) { + _ffi.shift = true; + } else if (e.isMetaPressed && !_ffi.command) { + _ffi.command = true; + } + sendRawKey(e, down: true); + } } - sendRawKey(e, down: true); - } - } - // [!_showEdit] workaround for soft-keyboard's control_key like Backspace / Enter - if (!_showEdit && e is RawKeyUpEvent) { - if (key == LogicalKeyboardKey.altLeft || - key == LogicalKeyboardKey.altRight) { - _ffi.alt = false; - } else if (key == LogicalKeyboardKey.controlLeft || - key == LogicalKeyboardKey.controlRight) { - _ffi.ctrl = false; - } else if (key == LogicalKeyboardKey.shiftRight || - key == LogicalKeyboardKey.shiftLeft) { - _ffi.shift = false; - } else if (key == LogicalKeyboardKey.metaLeft || - key == LogicalKeyboardKey.metaRight) { - _ffi.command = false; - } - sendRawKey(e); - } - return KeyEventResult.handled; - }, - child: child)))); + // [!_showEdit] workaround for soft-keyboard's control_key like Backspace / Enter + if (!_showEdit && e is RawKeyUpEvent) { + if (key == LogicalKeyboardKey.altLeft || + key == LogicalKeyboardKey.altRight) { + _ffi.alt = false; + } else if (key == LogicalKeyboardKey.controlLeft || + key == LogicalKeyboardKey.controlRight) { + _ffi.ctrl = false; + } else if (key == LogicalKeyboardKey.shiftRight || + key == LogicalKeyboardKey.shiftLeft) { + _ffi.shift = false; + } else if (key == LogicalKeyboardKey.metaLeft || + key == LogicalKeyboardKey.metaRight) { + _ffi.command = false; + } + sendRawKey(e); + } + return KeyEventResult.handled; + }, + child: child))))); } - Widget getBottomAppBar(bool keyboard) { + Widget? getBottomAppBar() { return BottomAppBar( elevation: 10, color: MyTheme.accent, @@ -515,6 +516,7 @@ class _RemotePageState extends State )); } paints.add(getHelpTools()); + return MouseRegion( onEnter: (evt) { bind.hostStopSystemKeyPropagate(stopped: false); From b2cf11f2df89a7a0a6b38d29f91cc3f5b494a826 Mon Sep 17 00:00:00 2001 From: Asura Date: Sun, 7 Aug 2022 10:50:31 +0800 Subject: [PATCH 0160/2015] Send caps lock key --- src/ui/remote.rs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 9bf125cc0..3a694dd50 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -1125,16 +1125,10 @@ impl Handler { } else { TO_RELEASE.lock().unwrap().remove(&key); } - // algr without action - // Control left + // AltGr && LeftControl(SpecialKey) without action if key == RdevKey::AltGr || evt.scan_code == 541 { return; } - // Caps affects the keycode map of the peer system(Linux). - if key == RdevKey::CapsLock { - return; - } - dbg!(key); self.map_keyboard_mode(down_or_up, key, None); } } From dde6df82e8b67f1c3a74104d3185f320a702e947 Mon Sep 17 00:00:00 2001 From: Asura Date: Sun, 7 Aug 2022 18:52:32 +0800 Subject: [PATCH 0161/2015] Refector: handline dead keys in translation mode --- Cargo.lock | 60 +----------------------------------------------- src/ui/remote.rs | 6 +---- 2 files changed, 2 insertions(+), 64 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ca3c29392..09ac0686d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1309,16 +1309,6 @@ dependencies = [ "termcolor", ] -[[package]] -name = "epoll" -version = "4.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20df693c700404f7e19d4d6fae6b15215d2913c27955d2b9d6f2c0f537511cd0" -dependencies = [ - "bitflags", - "libc", -] - [[package]] name = "err-derive" version = "0.3.1" @@ -1353,29 +1343,6 @@ dependencies = [ "nix 0.23.1", ] -[[package]] -name = "evdev-rs" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7db51abf6b3205a6e6e8dd68d7a5414d7c50d61736a6f4c9b97df86ef5567cf" -dependencies = [ - "bitflags", - "evdev-sys", - "libc", - "log", -] - -[[package]] -name = "evdev-sys" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14ead42b547b15d47089c1243d907bcf0eb94e457046d3b315a26ac9c9e9ea6d" -dependencies = [ - "cc", - "libc", - "pkg-config", -] - [[package]] name = "event-listener" version = "2.5.2" @@ -2348,28 +2315,6 @@ dependencies = [ "hashbrown", ] -[[package]] -name = "inotify" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abf888f9575c290197b2c948dc9e9ff10bd1a39ad1ea8585f734585fa6b9d3f9" -dependencies = [ - "bitflags", - "futures-core", - "inotify-sys", - "libc", - "tokio", -] - -[[package]] -name = "inotify-sys" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" -dependencies = [ - "libc", -] - [[package]] name = "instant" version = "0.1.12" @@ -3795,16 +3740,13 @@ dependencies = [ [[package]] name = "rdev" version = "0.5.0-2" -source = "git+https://github.com/asur4s/rdev#d6499d2e582bf3549aa4ba33cfd3fbbdfce10947" +source = "git+https://github.com/asur4s/rdev#3b440f7ff9d622b08eb83146ea3e5e529769a6c2" dependencies = [ "cocoa", "core-foundation 0.9.3", "core-foundation-sys 0.8.3", "core-graphics 0.22.3", "enum-map", - "epoll", - "evdev-rs", - "inotify", "lazy_static", "libc", "widestring 1.0.2", diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 3a694dd50..e6c4109a6 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -1085,11 +1085,7 @@ impl Handler { let string = match KEYBOARD.lock() { Ok(mut keyboard) => { let string = keyboard.add(&evt.event_type).unwrap_or_default(); - #[cfg(target_os = "windows")] - let is_dead = keyboard.last_is_dead; - #[cfg(target_os = "linux")] - let is_dead = keyboard.is_dead(); - if is_dead && string == "" && down_or_up == true { + if keyboard.is_dead() && string == "" && down_or_up == true { return; } string From 073e087a4810922ef32aaf10471cb294f8cb886d Mon Sep 17 00:00:00 2001 From: 21pages Date: Sat, 6 Aug 2022 17:08:48 +0800 Subject: [PATCH 0162/2015] custom tabbar Signed-off-by: 21pages --- flutter/lib/consts.dart | 8 +- .../desktop/pages/connection_tab_page.dart | 45 +-- .../desktop/pages/file_manager_tab_page.dart | 42 +-- .../lib/desktop/widgets/tabbar_widget.dart | 278 ++++++++++++++++++ 4 files changed, 303 insertions(+), 70 deletions(-) create mode 100644 flutter/lib/desktop/widgets/tabbar_widget.dart diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index eea49cf86..66653a746 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -1,4 +1,4 @@ -double kDesktopRemoteTabBarHeight = 48.0; -String kAppTypeMain = "main"; -String kAppTypeDesktopRemote = "remote"; -String kAppTypeDesktopFileTransfer = "file transfer"; +const double kDesktopRemoteTabBarHeight = 48.0; +const String kAppTypeMain = "main"; +const String kAppTypeDesktopRemote = "remote"; +const String kAppTypeDesktopFileTransfer = "file transfer"; diff --git a/flutter/lib/desktop/pages/connection_tab_page.dart b/flutter/lib/desktop/pages/connection_tab_page.dart index b87a876a3..f98c7d720 100644 --- a/flutter/lib/desktop/pages/connection_tab_page.dart +++ b/flutter/lib/desktop/pages/connection_tab_page.dart @@ -5,7 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/desktop/pages/remote_page.dart'; -import 'package:flutter_hbb/desktop/widgets/titlebar_widget.dart'; +import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:get/get.dart'; @@ -24,9 +24,10 @@ class _ConnectionTabPageState extends State with TickerProviderStateMixin { // refactor List when using multi-tab // this singleton is only for test - var connectionIds = RxList.empty(growable: true); + var connectionIds = RxList.empty(growable: true); var initialIndex = 0; late Rx tabController; + static final Rx _selected = 0.obs; var connectionMap = RxList.empty(growable: true); @@ -60,6 +61,7 @@ class _ConnectionTabPageState extends State vsync: this, initialIndex: initialIndex); } + _selected.value = initialIndex; } else if (call.method == "onDestroy") { print("executing onDestroy hook, closing ${connectionIds}"); connectionIds.forEach((id) { @@ -78,38 +80,13 @@ class _ConnectionTabPageState extends State return Scaffold( body: Column( children: [ - DesktopTitleBar( - child: Container( - height: kDesktopRemoteTabBarHeight, - child: Obx(() => TabBar( - isScrollable: true, - labelColor: Colors.white, - physics: NeverScrollableScrollPhysics(), - indicatorColor: Colors.white, - controller: tabController.value, - tabs: connectionIds - .map((e) => Tab( - child: Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text(e), - SizedBox( - width: 4, - ), - InkWell( - onTap: () { - onRemoveId(e); - }, - child: Icon( - Icons.highlight_remove, - size: 20, - )) - ], - ), - )) - .toList()))), - ), + Obx(() => DesktopTabBar( + controller: tabController, + tabs: connectionIds.toList(), + onTabClose: onRemoveId, + tabIcon: Icons.desktop_windows_sharp, + selected: _selected, + )), Expanded( child: Obx(() => TabBarView( controller: tabController.value, diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index 5e3337475..d06ed7444 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -4,7 +4,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/desktop/pages/file_manager_page.dart'; -import 'package:flutter_hbb/desktop/widgets/titlebar_widget.dart'; +import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; import 'package:flutter_hbb/models/model.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:get/get.dart'; @@ -26,6 +26,7 @@ class _FileManagerTabPageState extends State var connectionIds = List.empty(growable: true).obs; var initialIndex = 0; late Rx tabController; + static final Rx _selected = 0.obs; _FileManagerTabPageState(Map params) { if (params['id'] != null) { @@ -57,6 +58,7 @@ class _FileManagerTabPageState extends State initialIndex: initialIndex, vsync: this); } + _selected.value = initialIndex; } else if (call.method == "onDestroy") { print("executing onDestroy hook, closing ${connectionIds}"); connectionIds.forEach((id) { @@ -75,37 +77,13 @@ class _FileManagerTabPageState extends State return Scaffold( body: Column( children: [ - DesktopTitleBar( - child: Obx( - () => TabBar( - controller: tabController.value, - isScrollable: true, - labelColor: Colors.white, - physics: NeverScrollableScrollPhysics(), - indicatorColor: Colors.white, - tabs: connectionIds - .map((e) => Tab( - key: Key('T$e'), - child: Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text(e), - SizedBox( - width: 4, - ), - InkWell( - onTap: () { - onRemoveId(e); - }, - child: Icon( - Icons.highlight_remove, - size: 20, - )) - ], - ), - )) - .toList()), + Obx( + () => DesktopTabBar( + controller: tabController, + tabs: connectionIds.toList(), + onTabClose: onRemoveId, + tabIcon: Icons.file_copy_sharp, + selected: _selected, ), ), Expanded( diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart new file mode 100644 index 000000000..e57334be3 --- /dev/null +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -0,0 +1,278 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/consts.dart'; +import 'package:get/get.dart'; + +const Color _bgColor = Color.fromARGB(255, 231, 234, 237); +const Color _tabUnselectedColor = Color.fromARGB(255, 240, 240, 240); +const Color _tabHoverColor = Color.fromARGB(255, 245, 245, 245); +const Color _tabSelectedColor = Color.fromARGB(255, 255, 255, 255); +const Color _tabIconColor = MyTheme.accent50; +const Color _tabindicatorColor = _tabIconColor; +const Color _textColor = Color.fromARGB(255, 108, 111, 145); +const Color _iconColor = Color.fromARGB(255, 102, 106, 109); +const Color _iconHoverColor = Colors.black12; +const Color _iconPressedColor = Colors.black26; +const Color _dividerColor = Colors.black12; + +const double _kTabBarHeight = kDesktopRemoteTabBarHeight; +const double _kTabFixedWidth = 150; +const double _kIconSize = 18; +const double _kDividerIndent = 10; +const double _kAddIconSize = _kTabBarHeight - 15; + +class DesktopTabBar extends StatelessWidget { + late final Rx controller; + late final List tabs; + late final Function(String) onTabClose; + late final IconData tabIcon; + late final Rx selected; + + DesktopTabBar( + {Key? key, + required this.controller, + required this.tabs, + required this.onTabClose, + required this.tabIcon, + required this.selected}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + color: _bgColor, + height: _kTabBarHeight, + child: Row( + children: [ + Flexible( + child: Obx(() => TabBar( + indicatorColor: _tabindicatorColor, + indicatorSize: TabBarIndicatorSize.tab, + indicatorWeight: 4, + labelPadding: + const EdgeInsets.symmetric(vertical: 0, horizontal: 0), + indicatorPadding: EdgeInsets.zero, + isScrollable: true, + physics: BouncingScrollPhysics(), + controller: controller.value, + tabs: tabs + .asMap() + .entries + .map((e) => _Tab( + index: e.key, + text: e.value, + icon: tabIcon, + selected: selected.value, + onClose: () { + onTabClose(e.value); + // TODO + if (e.key <= selected.value) { + selected.value = max(0, selected.value - 1); + } + controller.value.animateTo(selected.value); + }, + onSelected: () { + selected.value = e.key; + controller.value.animateTo(e.key); + }, + )) + .toList())), + ), + Padding( + padding: EdgeInsets.only(left: 10), + child: _AddButton(), + ), + ], + ), + ); + } +} + +class _Tab extends StatelessWidget { + late final int index; + late final String text; + late final IconData icon; + late final int selected; + late final Function() onClose; + late final Function() onSelected; + final RxBool _hover = false.obs; + + _Tab({ + Key? key, + required this.index, + required this.text, + required this.icon, + required this.selected, + required this.onClose, + required this.onSelected, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + bool is_selected = index == selected; + bool show_divider = index != selected - 1 && index != selected; + return Obx( + (() => _Hoverable( + onHover: (hover) => _hover.value = hover, + onTapUp: () => onSelected(), + child: Container( + width: _kTabFixedWidth, + decoration: BoxDecoration( + color: is_selected + ? _tabSelectedColor + : _hover.value + ? _tabHoverColor + : _tabUnselectedColor, + ), + child: Row( + children: [ + Expanded( + child: Tab( + key: this.key, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Padding( + padding: EdgeInsets.symmetric(horizontal: 5), + child: Icon( + icon, + size: _kIconSize, + color: _tabIconColor, + ), + ), + Expanded( + child: Text( + text, + style: const TextStyle(color: _textColor), + ), + ), + _CloseButton( + tabHovered: _hover.value, + onClose: () => onClose(), + ), + ])), + ), + show_divider + ? VerticalDivider( + width: 1, + indent: _kDividerIndent, + endIndent: _kDividerIndent, + color: _dividerColor, + thickness: 1, + ) + : Container(), + ], + ), + ), + )), + ); + } +} + +class _AddButton extends StatelessWidget { + final RxBool _hover = false.obs; + final RxBool _pressed = false.obs; + + _AddButton({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return _Hoverable( + onHover: (hover) => _hover.value = hover, + onPressed: (pressed) => _pressed.value = pressed, + onTapUp: () => debugPrint('+'), // TODO + child: Obx((() => Container( + height: _kTabBarHeight, + decoration: ShapeDecoration( + shape: const CircleBorder(), + color: _pressed.value + ? _iconPressedColor + : _hover.value + ? _iconHoverColor + : Colors.transparent, + ), + child: const Icon( + Icons.add_sharp, + color: _iconColor, + size: _kAddIconSize, + ), + ))), + ); + } +} + +class _CloseButton extends StatelessWidget { + final bool tabHovered; + final Function onClose; + final RxBool _hover = false.obs; + final RxBool _pressed = false.obs; + + _CloseButton({ + Key? key, + required this.tabHovered, + required this.onClose, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 5), + child: SizedBox( + width: _kIconSize, + child: tabHovered + ? Obx((() => _Hoverable( + onHover: (hover) => _hover.value = hover, + onPressed: (pressed) => _pressed.value = pressed, + onTapUp: () => onClose(), + child: Container( + color: _pressed.value + ? _iconPressedColor + : _hover.value + ? _iconHoverColor + : Colors.transparent, + child: const Icon( + Icons.close, + size: _kIconSize, + color: _iconColor, + )), + ))) + : Container(), + )); + } +} + +class _Hoverable extends StatelessWidget { + final Widget child; + final Function(bool hover) onHover; + final Function(bool pressed)? onPressed; + final Function()? onTapUp; + + const _Hoverable( + {Key? key, + required this.child, + required this.onHover, + this.onPressed, + this.onTapUp}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return MouseRegion( + onEnter: (_) => onHover(true), + onExit: (_) => onHover(false), + child: onPressed == null && onTapUp == null + ? child + : GestureDetector( + onTapDown: (details) => onPressed?.call(true), + onTapUp: (details) { + onPressed?.call(false); + onTapUp?.call(); + }, + child: child, + )); + } +} From 7ea2b2735295c5d94ac30f0b577ff12bd7dd3697 Mon Sep 17 00:00:00 2001 From: kingtous Date: Mon, 8 Aug 2022 15:26:07 +0800 Subject: [PATCH 0163/2015] fix: windows onDestroy callback --- flutter/pubspec.lock | 16 ++++++++-------- flutter/pubspec.yaml | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 217051a60..7133ae132 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -7,7 +7,7 @@ packages: name: _fe_analyzer_shared url: "https://pub.dartlang.org" source: hosted - version: "43.0.0" + version: "44.0.0" after_layout: dependency: transitive description: @@ -21,7 +21,7 @@ packages: name: analyzer url: "https://pub.dartlang.org" source: hosted - version: "4.3.1" + version: "4.4.0" animations: dependency: transitive description: @@ -236,8 +236,8 @@ packages: dependency: "direct main" description: path: "." - ref: "832c263998275f8e6d3ea196931bc59a54ba9c79" - resolved-ref: "832c263998275f8e6d3ea196931bc59a54ba9c79" + ref: "7cd2d885e58397766f3f03a1e632299944580aac" + resolved-ref: "7cd2d885e58397766f3f03a1e632299944580aac" url: "https://github.com/Kingtous/rustdesk_desktop_multi_window" source: git version: "0.1.0" @@ -436,7 +436,7 @@ packages: name: flutter_smart_dialog url: "https://pub.dartlang.org" source: hosted - version: "4.5.3+8" + version: "4.5.4+1" flutter_test: dependency: "direct dev" description: flutter @@ -537,7 +537,7 @@ packages: name: image_picker_android url: "https://pub.dartlang.org" source: hosted - version: "0.8.5+1" + version: "0.8.5+2" image_picker_for_web: dependency: transitive description: @@ -948,7 +948,7 @@ packages: name: sqflite url: "https://pub.dartlang.org" source: hosted - version: "2.0.3" + version: "2.0.3+1" sqflite_common: dependency: transitive description: @@ -1088,7 +1088,7 @@ packages: name: url_launcher_web url: "https://pub.dartlang.org" source: hosted - version: "2.0.12" + version: "2.0.13" url_launcher_windows: dependency: transitive description: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index f4d18c2b2..591b59d29 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -59,7 +59,7 @@ dependencies: desktop_multi_window: git: url: https://github.com/Kingtous/rustdesk_desktop_multi_window - ref: 832c263998275f8e6d3ea196931bc59a54ba9c79 + ref: 7cd2d885e58397766f3f03a1e632299944580aac # bitsdojo_window: ^0.1.2 freezed_annotation: ^2.0.3 tray_manager: 0.1.7 From 577cce549f93d8174ef8a920738f26e0c4243c67 Mon Sep 17 00:00:00 2001 From: Asura Date: Mon, 8 Aug 2022 15:48:11 +0800 Subject: [PATCH 0164/2015] Update Cargo.lock --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 09ac0686d..6d1878ddb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4712,7 +4712,7 @@ checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" [[package]] name = "tfc" version = "0.6.1" -source = "git+https://github.com/asur4s/The-Fat-Controller#14d49063c8fc9a02c68c0dc842e8d6bb6c5e7713" +source = "git+https://github.com/asur4s/The-Fat-Controller#34ee2472e6a88dd8f0e28113d50130d93cf8a572" dependencies = [ "core-graphics 0.22.3", "unicode-segmentation", From c5d062829191809978a5880af10c3f6d729ff7b5 Mon Sep 17 00:00:00 2001 From: csf Date: Mon, 8 Aug 2022 17:53:51 +0800 Subject: [PATCH 0165/2015] refactor set/getByName "peers" "option" --- flutter/lib/common.dart | 7 + .../lib/desktop/pages/connection_page.dart | 16 +- .../lib/desktop/pages/desktop_home_page.dart | 96 ++-- .../lib/desktop/widgets/peercard_widget.dart | 8 +- flutter/lib/mobile/pages/connection_page.dart | 97 ++-- flutter/lib/mobile/pages/scan_page.dart | 118 +++-- flutter/lib/mobile/pages/settings_page.dart | 11 +- flutter/lib/models/chat_model.dart | 5 +- flutter/lib/models/file_model.dart | 2 +- flutter/lib/models/model.dart | 64 +-- flutter/lib/models/native_model.dart | 15 + flutter/lib/models/server_model.dart | 17 +- flutter/lib/utils/tray_manager.dart | 3 +- src/flutter_ffi.rs | 459 +++++------------- src/ui_interface.rs | 49 +- 15 files changed, 414 insertions(+), 553 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 16e8a172e..861b6b645 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -391,3 +391,10 @@ Future initGlobalFFI() async { // global shared preference await Get.putAsync(() => SharedPreferences.getInstance()); } + +String translate(String name) { + if (name.startsWith('Failed to') && name.contains(': ')) { + return name.split(': ').map((x) => translate(x)).join(': '); + } + return platformFFI.translate(name, localeName); +} diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index e32275373..d992c6c62 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -44,13 +44,22 @@ class _ConnectionPageState extends State { /// Update url. If it's not null, means an update is available. var _updateUrl = ''; - var _menuPos; Timer? _updateTimer; @override void initState() { super.initState(); + if (_idController.text.isEmpty) { + () async { + final lastRemoteId = await bind.mainGetLastRemoteId(); + if (lastRemoteId != _idController.text) { + setState(() { + _idController.text = lastRemoteId; + }); + } + }(); + } _updateTimer = Timer.periodic(Duration(seconds: 1), (timer) { updateStatus(); }); @@ -58,7 +67,6 @@ class _ConnectionPageState extends State { @override Widget build(BuildContext context) { - if (_idController.text.isEmpty) _idController.text = gFFI.getId(); return Container( decoration: BoxDecoration(color: isDarkTheme() ? null : MyTheme.grayBg), child: Column( @@ -428,7 +436,7 @@ class _ConnectionPageState extends State { } updateStatus() async { - svcStopped.value = gFFI.getOption("stop-service") == "Y"; + svcStopped.value = bind.mainGetOption(key: "stop-service") == "Y"; final status = jsonDecode(await bind.mainGetConnectStatus()) as Map; svcStatusCode.value = status["status_num"]; @@ -444,7 +452,7 @@ class _ConnectionPageState extends State { } Future buildAddressBook(BuildContext context) async { - final token = await gFFI.getLocalOption('access_token'); + final token = await bind.mainGetLocalOption(key: 'access_token'); if (token.trim().isEmpty) { return Center( child: InkWell( diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 6dc5e8f2f..3f908c3ce 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -7,7 +7,6 @@ import 'package:flutter/services.dart'; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/desktop/pages/connection_page.dart'; import 'package:flutter_hbb/desktop/widgets/titlebar_widget.dart'; -import 'package:flutter_hbb/models/model.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/server_model.dart'; import 'package:get/get.dart'; @@ -156,6 +155,8 @@ class _DesktopHomePageState extends State with TrayListener { }, onTap: () async { final userName = await gFFI.userModel.getUserName(); + final enabledInput = await bind.mainGetOption(key: 'enable-audio'); + final defaultInput = await gFFI.getDefaultAudioInput(); var menu = [ genEnablePopupMenuItem( translate("Enable Keyboard/Mouse"), @@ -173,7 +174,7 @@ class _DesktopHomePageState extends State with TrayListener { translate("Enable TCP Tunneling"), 'enable-tunnel', ), - genAudioInputPopupMenuItem(), + genAudioInputPopupMenuItem(enabledInput != "N", defaultInput), PopupMenuDivider(), PopupMenuItem( child: Text(translate("ID/Relay Server")), @@ -465,49 +466,60 @@ class _DesktopHomePageState extends State with TrayListener { Get.find().setString("darkTheme", choice); } - void onSelectMenu(String value) { - if (value.startsWith('enable-')) { - final option = gFFI.getOption(value); - gFFI.setOption(value, option == "N" ? "" : "N"); - } else if (value.startsWith('allow-')) { - final option = gFFI.getOption(value); + void onSelectMenu(String key) async { + if (key.startsWith('enable-')) { + final option = await bind.mainGetOption(key: key); + bind.mainSetOption(key: key, value: option == "N" ? "" : "N"); + } else if (key.startsWith('allow-')) { + final option = await bind.mainGetOption(key: key); final choice = option == "Y" ? "" : "Y"; - gFFI.setOption(value, choice); + bind.mainSetOption(key: key, value: choice); changeTheme(choice); - } else if (value == "stop-service") { - final option = gFFI.getOption(value); - gFFI.setOption(value, option == "Y" ? "" : "Y"); - } else if (value == "change-id") { + } else if (key == "stop-service") { + final option = await bind.mainGetOption(key: key); + bind.mainSetOption(key: key, value: option == "Y" ? "" : "Y"); + } else if (key == "change-id") { changeId(); - } else if (value == "custom-server") { + } else if (key == "custom-server") { changeServer(); - } else if (value == "whitelist") { + } else if (key == "whitelist") { changeWhiteList(); - } else if (value == "socks5-proxy") { + } else if (key == "socks5-proxy") { changeSocks5Proxy(); - } else if (value == "about") { + } else if (key == "about") { about(); - } else if (value == "logout") { + } else if (key == "logout") { logOut(); - } else if (value == "login") { + } else if (key == "login") { login(); } } - PopupMenuItem genEnablePopupMenuItem(String label, String value) { - final v = gFFI.getOption(value); - final isEnable = value.startsWith('enable-') ? v != "N" : v == "Y"; + PopupMenuItem genEnablePopupMenuItem(String label, String key) { + Future getOptionEnable(String key) async { + final v = await bind.mainGetOption(key: key); + return key.startsWith('enable-') ? v != "N" : v == "Y"; + } + return PopupMenuItem( - child: Row( - children: [ - Offstage(offstage: !isEnable, child: Icon(Icons.check)), - Text( - label, - style: genTextStyle(isEnable), - ), - ], - ), - value: value, + child: FutureBuilder( + future: getOptionEnable(key), + builder: (context, snapshot) { + var enable = false; + if (snapshot.hasData && snapshot.data!) { + enable = true; + } + return Row( + children: [ + Offstage(offstage: !enable, child: Icon(Icons.check)), + Text( + label, + style: genTextStyle(enable), + ), + ], + ); + }), + value: key, ); } @@ -518,10 +530,11 @@ class _DesktopHomePageState extends State with TrayListener { color: Colors.redAccent, decoration: TextDecoration.lineThrough); } - PopupMenuItem genAudioInputPopupMenuItem() { - final _enabledInput = gFFI.getOption('enable-audio'); - var defaultInput = gFFI.getDefaultAudioInput().obs; - var enabled = (_enabledInput != "N").obs; + PopupMenuItem genAudioInputPopupMenuItem( + bool enableInput, String defaultAudioInput) { + final defaultInput = defaultAudioInput.obs; + final enabled = enableInput.obs; + return PopupMenuItem( child: FutureBuilder>( future: gFFI.getAudioInputs(), @@ -569,12 +582,13 @@ class _DesktopHomePageState extends State with TrayListener { alignment: Alignment.centerLeft, child: Text(translate("Audio Input"))), itemBuilder: (context) => inputList, - onSelected: (dev) { + onSelected: (dev) async { if (dev == "Mute") { - gFFI.setOption( - 'enable-audio', _enabledInput == 'N' ? '' : 'N'); - enabled.value = gFFI.getOption('enable-audio') != 'N'; - } else if (dev != gFFI.getDefaultAudioInput()) { + await bind.mainSetOption( + key: 'enable-audio', value: enabled.value ? '' : 'N'); + enabled.value = + await bind.mainGetOption(key: 'enable-audio') != 'N'; + } else if (dev != await gFFI.getDefaultAudioInput()) { gFFI.setDefaultAudioInput(dev); defaultInput.value = dev; } diff --git a/flutter/lib/desktop/widgets/peercard_widget.dart b/flutter/lib/desktop/widgets/peercard_widget.dart index 0782e8426..949c46234 100644 --- a/flutter/lib/desktop/widgets/peercard_widget.dart +++ b/flutter/lib/desktop/widgets/peercard_widget.dart @@ -89,7 +89,8 @@ class _PeerCardState extends State<_PeerCard> children: [ Expanded( child: FutureBuilder( - future: gFFI.getPeerOption(peer.id, 'alias'), + future: bind.mainGetPeerOption( + id: peer.id, key: 'alias'), builder: (_, snapshot) { if (snapshot.hasData) { final name = snapshot.data!.isEmpty @@ -304,7 +305,7 @@ class _PeerCardState extends State<_PeerCard> void _rename(String id) async { var isInProgress = false; - var name = await gFFI.getPeerOption(id, 'alias'); + var name = await bind.mainGetPeerOption(id: id, key: 'alias'); if (widget.type == PeerType.ab) { final peer = gFFI.abModel.peers.firstWhere((p) => id == p['id']); if (peer == null) { @@ -359,7 +360,8 @@ class _PeerCardState extends State<_PeerCard> if (k.currentState != null) { if (k.currentState!.validate()) { k.currentState!.save(); - await gFFI.setPeerOption(id, 'alias', name); + await bind.mainSetPeerOption( + id: id, key: 'alias', value: name); if (widget.type == PeerType.ab) { gFFI.abModel.setPeerOption(id, 'alias', name); await gFFI.abModel.updateAb(); diff --git a/flutter/lib/mobile/pages/connection_page.dart b/flutter/lib/mobile/pages/connection_page.dart index 9722b1a47..69e7b9433 100644 --- a/flutter/lib/mobile/pages/connection_page.dart +++ b/flutter/lib/mobile/pages/connection_page.dart @@ -7,6 +7,8 @@ import 'package:url_launcher/url_launcher.dart'; import '../../common.dart'; import '../../models/model.dart'; +import '../../models/peer_model.dart'; +import '../../models/platform_model.dart'; import 'home_page.dart'; import 'remote_page.dart'; import 'scan_page.dart'; @@ -41,6 +43,16 @@ class _ConnectionPageState extends State { @override void initState() { super.initState(); + if (_idController.text.isEmpty) { + () async { + final lastRemoteId = await bind.mainGetLastRemoteId(); + if (lastRemoteId != _idController.text) { + setState(() { + _idController.text = lastRemoteId; + }); + } + }(); + } if (isAndroid) { Timer(Duration(seconds: 5), () { _updateUrl = gFFI.getByName('software_update_url'); @@ -52,7 +64,6 @@ class _ConnectionPageState extends State { @override Widget build(BuildContext context) { Provider.of(context); - if (_idController.text.isEmpty) _idController.text = gFFI.getId(); return SingleChildScrollView( child: Column( mainAxisAlignment: MainAxisAlignment.start, @@ -221,44 +232,52 @@ class _ConnectionPageState extends State { final n = (windowWidth / (minWidth + 2 * space)).floor(); width = windowWidth / n - 2 * space; } - final cards = []; - var peers = gFFI.peers(); - peers.forEach((p) { - cards.add(Container( - width: width, - child: Card( - child: GestureDetector( - onTap: !isWebDesktop ? () => connect('${p.id}') : null, - onDoubleTap: isWebDesktop ? () => connect('${p.id}') : null, - onLongPressStart: (details) { - final x = details.globalPosition.dx; - final y = details.globalPosition.dy; - _menuPos = RelativeRect.fromLTRB(x, y, x, y); - showPeerMenu(context, p.id); - }, - child: ListTile( - contentPadding: const EdgeInsets.only(left: 12), - subtitle: Text('${p.username}@${p.hostname}'), - title: Text('${p.id}'), - leading: Container( - padding: const EdgeInsets.all(6), - child: getPlatformImage('${p.platform}'), - color: str2color('${p.id}${p.platform}', 0x7f)), - trailing: InkWell( - child: Padding( - padding: const EdgeInsets.all(12), - child: Icon(Icons.more_vert)), - onTapDown: (e) { - final x = e.globalPosition.dx; - final y = e.globalPosition.dy; - _menuPos = RelativeRect.fromLTRB(x, y, x, y); - }, - onTap: () { - showPeerMenu(context, p.id); - }), - ))))); - }); - return Wrap(children: cards, spacing: space, runSpacing: space); + return FutureBuilder>( + future: gFFI.peers(), + builder: (context, snapshot) { + final cards = []; + if (snapshot.hasData) { + final peers = snapshot.data!; + peers.forEach((p) { + cards.add(Container( + width: width, + child: Card( + child: GestureDetector( + onTap: + !isWebDesktop ? () => connect('${p.id}') : null, + onDoubleTap: + isWebDesktop ? () => connect('${p.id}') : null, + onLongPressStart: (details) { + final x = details.globalPosition.dx; + final y = details.globalPosition.dy; + _menuPos = RelativeRect.fromLTRB(x, y, x, y); + showPeerMenu(context, p.id); + }, + child: ListTile( + contentPadding: const EdgeInsets.only(left: 12), + subtitle: Text('${p.username}@${p.hostname}'), + title: Text('${p.id}'), + leading: Container( + padding: const EdgeInsets.all(6), + child: getPlatformImage('${p.platform}'), + color: str2color('${p.id}${p.platform}', 0x7f)), + trailing: InkWell( + child: Padding( + padding: const EdgeInsets.all(12), + child: Icon(Icons.more_vert)), + onTapDown: (e) { + final x = e.globalPosition.dx; + final y = e.globalPosition.dy; + _menuPos = RelativeRect.fromLTRB(x, y, x, y); + }, + onTap: () { + showPeerMenu(context, p.id); + }), + ))))); + }); + } + return Wrap(children: cards, spacing: space, runSpacing: space); + }); } /// Show the peer menu and handle user's choice. diff --git a/flutter/lib/mobile/pages/scan_page.dart b/flutter/lib/mobile/pages/scan_page.dart index 2f5a9d991..54ba44892 100644 --- a/flutter/lib/mobile/pages/scan_page.dart +++ b/flutter/lib/mobile/pages/scan_page.dart @@ -9,7 +9,7 @@ import 'package:qr_code_scanner/qr_code_scanner.dart'; import 'package:zxing2/qrcode.dart'; import '../../common.dart'; -import '../../models/model.dart'; +import '../../models/platform_model.dart'; class ScanPage extends StatefulWidget { @override @@ -153,54 +153,80 @@ class _ScanPageState extends State { } void showServerSettingsWithValue( - String id, String relay, String key, String api) { - final formKey = GlobalKey(); - final id0 = gFFI.getByName('option', 'custom-rendezvous-server'); - final relay0 = gFFI.getByName('option', 'relay-server'); - final api0 = gFFI.getByName('option', 'api-server'); - final key0 = gFFI.getByName('option', 'key'); + String id, String relay, String key, String api) async { + Map oldOptions = jsonDecode(await bind.mainGetOptions()); + String id0 = oldOptions['custom-rendezvous-server'] ?? ""; + String relay0 = oldOptions['relay-server'] ?? ""; + String api0 = oldOptions['api-server'] ?? ""; + String key0 = oldOptions['key'] ?? ""; + var isInProgress = false; + final idController = TextEditingController(text: id); + final relayController = TextEditingController(text: relay); + final apiController = TextEditingController(text: api); + + String? idServerMsg; + String? relayServerMsg; + String? apiServerMsg; + DialogManager.show((setState, close) { + Future validate() async { + if (idController.text != id) { + final res = await validateAsync(idController.text); + setState(() => idServerMsg = res); + if (idServerMsg != null) return false; + id = idController.text; + } + if (relayController.text != relay) { + relayServerMsg = await validateAsync(relayController.text); + if (relayServerMsg != null) return false; + relay = relayController.text; + } + if (apiController.text != relay) { + apiServerMsg = await validateAsync(apiController.text); + if (apiServerMsg != null) return false; + api = apiController.text; + } + return true; + } + return CustomAlertDialog( title: Text(translate('ID/Relay Server')), content: Form( - key: formKey, child: Column( mainAxisSize: MainAxisSize.min, children: [ TextFormField( - initialValue: id, + controller: idController, decoration: InputDecoration( - labelText: translate('ID Server'), - ), - validator: validate, - onSaved: (String? value) { - if (value != null) id = value.trim(); - }, + labelText: translate('ID Server'), + errorText: idServerMsg), ) ] + (isAndroid ? [ TextFormField( - initialValue: relay, + controller: relayController, decoration: InputDecoration( - labelText: translate('Relay Server'), - ), - validator: validate, - onSaved: (String? value) { - if (value != null) relay = value.trim(); - }, + labelText: translate('Relay Server'), + errorText: relayServerMsg), ) ] : []) + [ TextFormField( - initialValue: api, + controller: apiController, decoration: InputDecoration( labelText: translate('API Server'), ), - validator: validate, - onSaved: (String? value) { - if (value != null) api = value.trim(); + autovalidateMode: AutovalidateMode.onUserInteraction, + validator: (v) { + if (v != null && v.length > 0) { + if (!(v.startsWith('http://') || + v.startsWith("https://"))) { + return translate("invalid_http"); + } + } + return apiServerMsg; }, ), TextFormField( @@ -208,11 +234,13 @@ void showServerSettingsWithValue( decoration: InputDecoration( labelText: 'Key', ), - validator: null, - onSaved: (String? value) { + onChanged: (String? value) { if (value != null) key = value.trim(); }, ), + Offstage( + offstage: !isInProgress, + child: LinearProgressIndicator()) ])), actions: [ TextButton( @@ -224,24 +252,28 @@ void showServerSettingsWithValue( ), TextButton( style: flatButtonStyle, - onPressed: () { - if (formKey.currentState != null && - formKey.currentState!.validate()) { - formKey.currentState!.save(); - if (id != id0) - gFFI.setByName('option', - '{"name": "custom-rendezvous-server", "value": "$id"}'); + onPressed: () async { + setState(() { + idServerMsg = null; + relayServerMsg = null; + apiServerMsg = null; + isInProgress = true; + }); + if (await validate()) { + if (id != id0) { + bind.mainSetOption(key: "custom-rendezvous-server", value: id); + } if (relay != relay0) - gFFI.setByName( - 'option', '{"name": "relay-server", "value": "$relay"}'); - if (key != key0) - gFFI.setByName('option', '{"name": "key", "value": "$key"}'); + bind.mainSetOption(key: "relay-server", value: relay); + if (key != key0) bind.mainSetOption(key: "key", value: key); if (api != api0) - gFFI.setByName( - 'option', '{"name": "api-server", "value": "$api"}'); + bind.mainSetOption(key: "api-server", value: api); gFFI.ffiModel.updateUser(); close(); } + setState(() { + isInProgress = false; + }); }, child: Text(translate('OK')), ), @@ -250,11 +282,11 @@ void showServerSettingsWithValue( }); } -String? validate(value) { +Future validateAsync(String value) async { value = value.trim(); if (value.isEmpty) { return null; } - final res = gFFI.getByName('test_if_valid_server', value); + final res = await bind.mainTestIfValidServer(server: value); return res.isEmpty ? null : res; } diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index 01cf4ae5d..3646b59e9 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -185,11 +185,12 @@ class _SettingsState extends State with WidgetsBindingObserver { } } -void showServerSettings() { - final id = gFFI.getByName('option', 'custom-rendezvous-server'); - final relay = gFFI.getByName('option', 'relay-server'); - final api = gFFI.getByName('option', 'api-server'); - final key = gFFI.getByName('option', 'key'); +void showServerSettings() async { + Map options = jsonDecode(await bind.mainGetOptions()); + String id = options['custom-rendezvous-server'] ?? ""; + String relay = options['relay-server'] ?? ""; + String api = options['api-server'] ?? ""; + String key = options['key'] ?? ""; showServerSettingsWithValue(id, relay, key, api); } diff --git a/flutter/lib/models/chat_model.dart b/flutter/lib/models/chat_model.dart index ad572c164..28ffa65e2 100644 --- a/flutter/lib/models/chat_model.dart +++ b/flutter/lib/models/chat_model.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:dash_chat_2/dash_chat_2.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; import '../../mobile/widgets/overlay.dart'; import 'model.dart'; @@ -72,7 +73,7 @@ class ChatModel with ChangeNotifier { } } - receive(int id, String text) { + receive(int id, String text) async { if (text.isEmpty) return; // first message show overlay icon if (chatIconOverlayEntry == null) { @@ -82,7 +83,7 @@ class ChatModel with ChangeNotifier { if (id == clientModeID) { chatUser = ChatUser( firstName: _ffi.target?.ffiModel.pi.username, - id: _ffi.target?.getId() ?? "", + id: await bind.mainGetLastRemoteId(), ); } else { final client = _ffi.target?.serverModel.clients[id]; diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index 459e8c448..45f5ec970 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -444,7 +444,7 @@ class FileModel extends ChangeNotifier { items.items.forEach((from) async { _jobId++; await bind.sessionSendFiles( - id: '${_ffi.target?.getId()}', + id: await bind.mainGetLastRemoteId(), actId: _jobId, path: from.path, to: PathUtil.join(toPath, from.name, isWindows), diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index f67d0d5fa..7ca77f6cd 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -881,11 +881,6 @@ class FFI { this.qualityMonitorModel = QualityMonitorModel(WeakReference(this)); } - /// Get the remote id for current client. - String getId() { - return getByName('remote_id'); // TODO - } - /// Send a mouse tap event(down and up). void tap(MouseButtons button) { sendMouse('down', button); @@ -963,9 +958,9 @@ class FFI { } /// List the saved peers. - List peers() { + Future> peers() async { try { - var str = getByName('peers'); // TODO + var str = await bind.mainGetRecentPeers(); if (str == "") return []; List peers = json.decode(str); return peers @@ -1046,33 +1041,6 @@ class FFI { platformFFI.setByName(name, value); } - String getOption(String name) { - return platformFFI.getByName("option", name); - } - - Future getLocalOption(String name) { - return bind.mainGetLocalOption(key: name); - } - - Future setLocalOption(String key, String value) { - return bind.mainSetLocalOption(key: key, value: value); - } - - Future getPeerOption(String id, String key) { - return bind.mainGetPeerOption(id: id, key: key); - } - - Future setPeerOption(String id, String key, String value) { - return bind.mainSetPeerOption(id: id, key: key, value: value); - } - - void setOption(String name, String value) { - Map res = Map() - ..["name"] = name - ..["value"] = value; - return platformFFI.setByName('option', jsonEncode(res)); - } - handleMouse(Map evt, {double tabBarHeight = 0.0}) { var type = ''; var isMove = false; @@ -1148,8 +1116,8 @@ class FFI { return await bind.mainGetSoundInputs(); } - String getDefaultAudioInput() { - final input = getOption('audio-input'); + Future getDefaultAudioInput() async { + final input = await bind.mainGetOption(key: 'audio-input'); if (input.isEmpty && Platform.isWindows) { return "System Sound"; } @@ -1157,11 +1125,14 @@ class FFI { } void setDefaultAudioInput(String input) { - setOption('audio-input', input); + bind.mainSetOption(key: 'audio-input', value: input); } Future> getHttpHeaders() async { - return {"Authorization": "Bearer " + await getLocalOption("access_token")}; + return { + "Authorization": + "Bearer " + await bind.mainGetLocalOption(key: "access_token") + }; } } @@ -1233,11 +1204,12 @@ void initializeCursorAndCanvas(FFI ffi) async { /// Translate text based on the pre-defined dictionary. /// note: params [FFI?] can be used to replace global FFI implementation /// for example: during global initialization, gFFI not exists yet. -String translate(String name, {FFI? ffi}) { - if (name.startsWith('Failed to') && name.contains(': ')) { - return name.split(': ').map((x) => translate(x)).join(': '); - } - var a = 'translate'; - var b = '{"locale": "$localeName", "text": "$name"}'; - return (ffi ?? gFFI).getByName(a, b); -} +// String translate(String name, {FFI? ffi}) { +// if (name.startsWith('Failed to') && name.contains(': ')) { +// return name.split(': ').map((x) => translate(x)).join(': '); +// } +// var a = 'translate'; +// var b = '{"locale": "$localeName", "text": "$name"}'; +// +// return (ffi ?? gFFI).getByName(a, b); +// } diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index 784ffe6c8..c58577945 100644 --- a/flutter/lib/models/native_model.dart +++ b/flutter/lib/models/native_model.dart @@ -29,6 +29,7 @@ typedef HandleEvent = void Function(Map evt); class PlatformFFI { String _dir = ''; String _homeDir = ''; + F2? _translate; F2? _getByName; F3? _setByName; var _eventHandlers = Map>(); @@ -75,6 +76,19 @@ class PlatformFFI { } } + String translate(String name, String locale) { + if (_translate == null) return ''; + var a = name.toNativeUtf8(); + var b = locale.toNativeUtf8(); + var p = _translate!(a, b); + assert(p != nullptr); + final res = p.toDartString(); + calloc.free(p); + calloc.free(a); + calloc.free(b); + return res; + } + /// Send **get** command to the Rust core based on [name] and [arg]. /// Return the result as a string. String getByName(String name, [String arg = '']) { @@ -118,6 +132,7 @@ class PlatformFFI { : DynamicLibrary.process(); debugPrint('initializing FFI ${_appType}'); try { + _translate = dylib.lookupFunction('translate'); _getByName = dylib.lookupFunction('get_by_name'); _setByName = dylib.lookupFunction, Pointer), F3>( diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index c9147441e..362e47a78 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; import 'package:wakelock/wakelock.dart'; import '../common.dart'; @@ -57,7 +58,7 @@ class ServerModel with ChangeNotifier { set verificationMethod(String method) { _verificationMethod = method; - gFFI.setOption("verification-method", method); + bind.mainSetOption(key: "verification-method", value: method); } String get temporaryPasswordLength { @@ -70,7 +71,7 @@ class ServerModel with ChangeNotifier { set temporaryPasswordLength(String length) { _temporaryPasswordLength = length; - gFFI.setOption("temporary-password-length", length); + bind.mainSetOption(key: "temporary-password-length", value: length); } TextEditingController get serverId => _serverId; @@ -85,7 +86,7 @@ class ServerModel with ChangeNotifier { ServerModel(this.parent) { () async { - _emptyIdShow = translate("Generating ...", ffi: this.parent.target); + _emptyIdShow = translate("Generating ..."); _serverId = TextEditingController(text: this._emptyIdShow); /** * 1. check android permission @@ -153,11 +154,13 @@ class ServerModel with ChangeNotifier { }); } - updatePasswordModel() { + updatePasswordModel() async { var update = false; final temporaryPassword = gFFI.getByName("temporary_password"); - final verificationMethod = gFFI.getOption("verification-method"); - final temporaryPasswordLength = gFFI.getOption("temporary-password-length"); + final verificationMethod = + await bind.mainGetOption(key: "verification-method"); + final temporaryPasswordLength = + await bind.mainGetOption(key: "temporary-password-length"); final oldPwdText = _serverPasswd.text; if (_serverPasswd.text != temporaryPassword) { _serverPasswd.text = temporaryPassword; @@ -325,7 +328,7 @@ class ServerModel with ChangeNotifier { const maxCount = 10; while (count < maxCount) { await Future.delayed(Duration(seconds: 1)); - final id = parent.target?.getByName("server_id") ?? ""; + final id = await bind.mainGetMyId(); if (id.isEmpty) { continue; } else { diff --git a/flutter/lib/utils/tray_manager.dart b/flutter/lib/utils/tray_manager.dart index d911932e5..f0422f554 100644 --- a/flutter/lib/utils/tray_manager.dart +++ b/flutter/lib/utils/tray_manager.dart @@ -1,8 +1,9 @@ import 'dart:io'; -import 'package:flutter_hbb/models/model.dart'; import 'package:tray_manager/tray_manager.dart'; +import '../common.dart'; + Future initTray({List? extra_item}) async { List items = [ MenuItem(key: "show", label: translate("show rustdesk")), diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 904912715..40f72444a 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -23,10 +23,10 @@ use crate::ui_interface; use crate::ui_interface::{change_id, check_connect_status, is_ok_change_id}; use crate::ui_interface::{ discover, forget_password, get_api_server, get_app_name, get_async_job_status, - get_connect_status, get_fav, get_id, get_lan_peers, get_license, get_local_option, get_options, - get_peer, get_peer_option, get_socks, get_sound_inputs, get_uuid, get_version, - has_rendezvous_service, post_request, set_local_option, set_options, set_peer_option, - set_socks, store_fav, test_if_valid_server, using_public_server, + get_connect_status, get_fav, get_id, get_lan_peers, get_license, get_local_option, get_option, + get_options, get_peer, get_peer_option, get_socks, get_sound_inputs, get_uuid, get_version, + has_rendezvous_service, post_request, set_local_option, set_option, set_options, + set_peer_option, set_socks, store_fav, test_if_valid_server, using_public_server, }; fn initialize(app_dir: &str) { @@ -81,14 +81,24 @@ pub enum EventToUI { } pub fn start_global_event_stream(s: StreamSink, app_type: String) -> ResultType<()> { - if let Some(_) = flutter::GLOBAL_EVENT_STREAM.write().unwrap().insert(app_type.clone(), s) { - log::warn!("Global event stream of type {} is started before, but now removed", app_type); + if let Some(_) = flutter::GLOBAL_EVENT_STREAM + .write() + .unwrap() + .insert(app_type.clone(), s) + { + log::warn!( + "Global event stream of type {} is started before, but now removed", + app_type + ); } Ok(()) } pub fn stop_global_event_stream(app_type: String) { - let _ = flutter::GLOBAL_EVENT_STREAM.write().unwrap().remove(&app_type); + let _ = flutter::GLOBAL_EVENT_STREAM + .write() + .unwrap() + .remove(&app_type); } pub fn host_stop_system_key_propagate(stopped: bool) { @@ -113,7 +123,6 @@ pub fn get_session_remember(id: String) -> Option { } } -// TODO sync pub fn get_session_toggle_option(id: String, arg: String) -> Option { if let Some(session) = SESSIONS.read().unwrap().get(&id) { Some(session.get_toggle_option(&arg)) @@ -143,7 +152,6 @@ pub fn get_session_option(id: String, arg: String) -> Option { } } -// void pub fn session_login(id: String, password: String, remember: bool) { if let Some(session) = SESSIONS.read().unwrap().get(&id) { session.login(&password, remember); @@ -409,6 +417,26 @@ pub fn main_get_async_status() -> String { get_async_job_status() } +pub fn main_get_option(key: String) -> String { + get_option(key) +} + +pub fn main_set_option(key: String, value: String) { + if key.eq("custom-rendezvous-server") { + set_option(key, value); + #[cfg(target_os = "android")] + crate::rendezvous_mediator::RendezvousMediator::restart(); + #[cfg(any( + target_os = "android", + target_os = "ios", + feature = "cli" + ))] + crate::common::test_rendezvous_server(); + } else { + set_option(key, value); + } +} + pub fn main_get_options() -> String { get_options() } @@ -452,7 +480,7 @@ pub fn main_store_fav(favs: Vec) { store_fav(favs) } -pub fn main_get_peers(id: String) -> String { +pub fn main_get_peer(id: String) -> String { let conf = get_peer(id); serde_json::to_string(&conf).unwrap_or("".to_string()) } @@ -525,13 +553,30 @@ pub fn main_forget_password(id: String) { forget_password(id) } +// TODO APP_DIR & ui_interface +pub fn main_get_recent_peers() -> String { + if !config::APP_DIR.read().unwrap().is_empty() { + let peers: Vec<(String, config::PeerInfoSerde)> = PeerConfig::peers() + .drain(..) + .map(|(id, _, p)| (id, p.info)) + .collect(); + serde_json::ser::to_string(&peers).unwrap_or("".to_owned()) + } else { + String::new() + } +} + pub fn main_load_recent_peers() { if !config::APP_DIR.read().unwrap().is_empty() { let peers: Vec<(String, config::PeerInfoSerde)> = PeerConfig::peers() .drain(..) .map(|(id, _, p)| (id, p.info)) .collect(); - if let Some(s) = flutter::GLOBAL_EVENT_STREAM.read().unwrap().get(flutter::APP_TYPE_MAIN) { + if let Some(s) = flutter::GLOBAL_EVENT_STREAM + .read() + .unwrap() + .get(flutter::APP_TYPE_MAIN) + { let data = HashMap::from([ ("name", "load_recent_peers".to_owned()), ( @@ -557,7 +602,11 @@ pub fn main_load_fav_peers() { } }) .collect(); - if let Some(s) = flutter::GLOBAL_EVENT_STREAM.read().unwrap().get(flutter::APP_TYPE_MAIN) { + if let Some(s) = flutter::GLOBAL_EVENT_STREAM + .read() + .unwrap() + .get(flutter::APP_TYPE_MAIN) + { let data = HashMap::from([ ("name", "load_fav_peers".to_owned()), ( @@ -571,7 +620,11 @@ pub fn main_load_fav_peers() { } pub fn main_load_lan_peers() { - if let Some(s) = flutter::GLOBAL_EVENT_STREAM.read().unwrap().get(flutter::APP_TYPE_MAIN) { + if let Some(s) = flutter::GLOBAL_EVENT_STREAM + .read() + .unwrap() + .get(flutter::APP_TYPE_MAIN) + { let data = HashMap::from([ ("name", "load_lan_peers".to_owned()), ("peers", get_lan_peers()), @@ -580,6 +633,25 @@ pub fn main_load_lan_peers() { }; } +pub fn main_get_last_remote_id() -> String { + // if !config::APP_DIR.read().unwrap().is_empty() { + // res = LocalConfig::get_remote_id(); + // } + LocalConfig::get_remote_id() +} + +#[no_mangle] +unsafe extern "C" fn translate(name: *const c_char, locale: *const c_char) -> *const c_char { + let name = CStr::from_ptr(name); + let locale = CStr::from_ptr(locale); + let res = if let (Ok(name), Ok(locale)) = (name.to_str(), locale.to_str()) { + crate::client::translate_locale(name.to_owned(), locale) + } else { + String::new() + }; + CString::from_vec_unchecked(res.into_bytes()).into_raw() +} + /// FFI for **get** commands which are idempotent. /// Return result in c string. /// @@ -594,93 +666,41 @@ unsafe extern "C" fn get_by_name(name: *const c_char, arg: *const c_char) -> *co let name: &CStr = CStr::from_ptr(name); if let Ok(name) = name.to_str() { match name { - "peers" => { - if !config::APP_DIR.read().unwrap().is_empty() { - let peers: Vec<(String, config::PeerInfoSerde)> = PeerConfig::peers() - .drain(..) - .map(|(id, _, p)| (id, p.info)) - .collect(); - res = serde_json::ser::to_string(&peers).unwrap_or("".to_owned()); - } - } - "remote_id" => { - if !config::APP_DIR.read().unwrap().is_empty() { - res = LocalConfig::get_remote_id(); - } - } - // "remember" => { - // res = Session::get_remember().to_string(); - // } - // "toggle_option" => { - // if let Ok(arg) = arg.to_str() { - // if let Some(v) = Session::get_toggle_option(arg) { - // res = v.to_string(); - // } + // "peers" => { + // if !config::APP_DIR.read().unwrap().is_empty() { + // let peers: Vec<(String, config::PeerInfoSerde)> = PeerConfig::peers() + // .drain(..) + // .map(|(id, _, p)| (id, p.info)) + // .collect(); + // res = serde_json::ser::to_string(&peers).unwrap_or("".to_owned()); // } // } - "test_if_valid_server" => { - if let Ok(arg) = arg.to_str() { - res = hbb_common::socket_client::test_if_valid_server(arg); - } - } - "option" => { - if let Ok(arg) = arg.to_str() { - res = ui_interface::get_option(arg.to_owned()); - } - } - // "image_quality" => { - // res = Session::get_image_quality(); + // "remote_id" => { + // if !config::APP_DIR.read().unwrap().is_empty() { + // res = LocalConfig::get_remote_id(); + // } + // } + // "test_if_valid_server" => { + // if let Ok(arg) = arg.to_str() { + // res = hbb_common::socket_client::test_if_valid_server(arg); + // } + // } + // "option" => { + // if let Ok(arg) = arg.to_str() { + // res = ui_interface::get_option(arg.to_owned()); + // } // } "software_update_url" => { res = crate::common::SOFTWARE_UPDATE_URL.lock().unwrap().clone() } - "translate" => { - if let Ok(arg) = arg.to_str() { - if let Ok(m) = serde_json::from_str::>(arg) { - if let Some(locale) = m.get("locale") { - if let Some(text) = m.get("text") { - res = crate::client::translate_locale(text.to_owned(), locale); - } - } - } - } - } - // "peer_option" => { - // if let Ok(arg) = arg.to_str() { - // res = Session::get_option(arg); - // } - // } // File Action "get_home_dir" => { res = fs::get_home_as_string(); } - // "read_local_dir_sync" => { - // if let Ok(value) = arg.to_str() { - // if let Ok(m) = serde_json::from_str::>(value) { - // if let (Some(path), Some(show_hidden)) = - // (m.get("path"), m.get("show_hidden")) - // { - // if let Ok(fd) = - // fs::read_dir(&fs::get_path(path), show_hidden.eq("true")) - // { - // res = make_fd_to_json(fd); - // } - // } - // } - // } - // } // Server Side - "local_option" => { - if let Ok(arg) = arg.to_str() { - res = LocalConfig::get_option(arg); - } - } "langs" => { res = crate::lang::LANGS.to_string(); } - "server_id" => { - res = ui_interface::get_id(); - } "temporary_password" => { res = ui_interface::temporary_password(); } @@ -709,9 +729,6 @@ unsafe extern "C" fn get_by_name(name: *const c_char, arg: *const c_char) -> *co } } } - "uuid" => { - res = base64::encode(get_uuid()); - } _ => { log::error!("Unknown name of get_by_name: {}", name); } @@ -742,69 +759,9 @@ unsafe extern "C" fn set_by_name(name: *const c_char, value: *const c_char) { "info2" => { *crate::common::FLUTTER_INFO2.lock().unwrap() = value.to_owned(); } - // "connect" => { - // Session::start(value, false); - // } - // "connect_file_transfer" => { - // Session::start(value, true); - // } - // "login" => { - // if let Ok(m) = serde_json::from_str::>(value) { - // if let Some(password) = m.get("password") { - // if let Some(remember) = m.get("remember") { - // Session::login(password, remember == "true"); - // } - // } - // } - // } - // "close" => { - // Session::close(); - // } - // "refresh" => { - // Session::refresh(); - // } - // "reconnect" => { - // Session::reconnect(); - // } - // "toggle_option" => { - // Session::toggle_option(value); - // } - // "image_quality" => { - // Session::set_image_quality(value); - // } - // "lock_screen" => { - // Session::lock_screen(); - // } - // "ctrl_alt_del" => { - // Session::ctrl_alt_del(); - // } - // "switch_display" => { - // if let Ok(v) = value.parse::() { - // Session::switch_display(v); - // } - // } "remove" => { PeerConfig::remove(value); } - // "input_key" => { - // if let Ok(m) = serde_json::from_str::>(value) { - // let alt = m.get("alt").is_some(); - // let ctrl = m.get("ctrl").is_some(); - // let shift = m.get("shift").is_some(); - // let command = m.get("command").is_some(); - // let down = m.get("down").is_some(); - // let press = m.get("press").is_some(); - // if let Some(name) = m.get("name") { - // Session::input_key(name, down, press, alt, ctrl, shift, command); - // } - // } - // } - // "input_string" => { - // Session::input_string(value); - // } - // "chat_client_mode" => { - // Session::send_chat(value.to_owned()); - // } // TODO "send_mouse" => { @@ -848,203 +805,29 @@ unsafe extern "C" fn set_by_name(name: *const c_char, value: *const c_char) { } } } - "option" => { - if let Ok(m) = serde_json::from_str::>(value) { - if let Some(name) = m.get("name") { - if let Some(value) = m.get("value") { - ui_interface::set_option(name.to_owned(), value.to_owned()); - if name == "custom-rendezvous-server" { - #[cfg(target_os = "android")] - crate::rendezvous_mediator::RendezvousMediator::restart(); - #[cfg(any( - target_os = "android", - target_os = "ios", - feature = "cli" - ))] - crate::common::test_rendezvous_server(); - } - } - } - } - } - // "peer_option" => { + // "option" => { // if let Ok(m) = serde_json::from_str::>(value) { // if let Some(name) = m.get("name") { // if let Some(value) = m.get("value") { - // Session::set_option(name.to_owned(), value.to_owned()); + // ui_interface::set_option(name.to_owned(), value.to_owned()); + // if name == "custom-rendezvous-server" { + // #[cfg(target_os = "android")] + // crate::rendezvous_mediator::RendezvousMediator::restart(); + // #[cfg(any( + // target_os = "android", + // target_os = "ios", + // feature = "cli" + // ))] + // crate::common::test_rendezvous_server(); + // } // } // } // } // } - "local_option" => { - if let Ok(m) = serde_json::from_str::>(value) { - if let Some(name) = m.get("name") { - if let Some(value) = m.get("value") { - LocalConfig::set_option(name.to_owned(), value.to_owned()); - } - } - } - } - // "input_os_password" => { - // Session::input_os_password(value.to_owned(), true); - // } "restart_remote_device" => { // TODO // Session::restart_remote_device(); } - // // File Action - // "read_remote_dir" => { - // if let Ok(m) = serde_json::from_str::>(value) { - // if let (Some(path), Some(show_hidden), Some(session)) = ( - // m.get("path"), - // m.get("show_hidden"), - // Session::get().read().unwrap().as_ref(), - // ) { - // session.read_remote_dir(path.to_owned(), show_hidden.eq("true")); - // } - // } - // } - // "send_files" => { - // if let Ok(m) = serde_json::from_str::>(value) { - // if let ( - // Some(id), - // Some(path), - // Some(to), - // Some(file_num), - // Some(show_hidden), - // Some(is_remote), - // ) = ( - // m.get("id"), - // m.get("path"), - // m.get("to"), - // m.get("file_num"), - // m.get("show_hidden"), - // m.get("is_remote"), - // ) { - // Session::send_files( - // id.parse().unwrap_or(0), - // path.to_owned(), - // to.to_owned(), - // file_num.parse().unwrap_or(0), - // show_hidden.eq("true"), - // is_remote.eq("true"), - // ); - // } - // } - // } - // "set_confirm_override_file" => { - // if let Ok(m) = serde_json::from_str::>(value) { - // if let ( - // Some(id), - // Some(file_num), - // Some(need_override), - // Some(remember), - // Some(is_upload), - // ) = ( - // m.get("id"), - // m.get("file_num"), - // m.get("need_override"), - // m.get("remember"), - // m.get("is_upload"), - // ) { - // Session::set_confirm_override_file( - // id.parse().unwrap_or(0), - // file_num.parse().unwrap_or(0), - // need_override.eq("true"), - // remember.eq("true"), - // is_upload.eq("true"), - // ); - // } - // } - // } - // ** TODO ** continue - // "remove_file" => { - // if let Ok(m) = serde_json::from_str::>(value) { - // if let ( - // Some(id), - // Some(path), - // Some(file_num), - // Some(is_remote), - // Some(session), - // ) = ( - // m.get("id"), - // m.get("path"), - // m.get("file_num"), - // m.get("is_remote"), - // Session::get().write().unwrap().as_mut(), - // ) { - // session.remove_file( - // id.parse().unwrap_or(0), - // path.to_owned(), - // file_num.parse().unwrap_or(0), - // is_remote.eq("true"), - // ); - // } - // } - // } - // "read_dir_recursive" => { - // if let Ok(m) = serde_json::from_str::>(value) { - // if let (Some(id), Some(path), Some(is_remote), Some(session)) = ( - // m.get("id"), - // m.get("path"), - // m.get("is_remote"), - // Session::get().write().unwrap().as_mut(), - // ) { - // session.remove_dir_all( - // id.parse().unwrap_or(0), - // path.to_owned(), - // is_remote.eq("true"), - // ); - // } - // } - // } - // "remove_all_empty_dirs" => { - // if let Ok(m) = serde_json::from_str::>(value) { - // if let (Some(id), Some(path), Some(is_remote), Some(session)) = ( - // m.get("id"), - // m.get("path"), - // m.get("is_remote"), - // Session::get().write().unwrap().as_mut(), - // ) { - // session.remove_dir( - // id.parse().unwrap_or(0), - // path.to_owned(), - // is_remote.eq("true"), - // ); - // } - // } - // } - // "cancel_job" => { - // if let (Ok(id), Some(session)) = - // (value.parse(), Session::get().write().unwrap().as_mut()) - // { - // session.cancel_job(id); - // } - // } - // "create_dir" => { - // if let Ok(m) = serde_json::from_str::>(value) { - // if let (Some(id), Some(path), Some(is_remote), Some(session)) = ( - // m.get("id"), - // m.get("path"), - // m.get("is_remote"), - // Session::get().write().unwrap().as_mut(), - // ) { - // session.create_dir( - // id.parse().unwrap_or(0), - // path.to_owned(), - // is_remote.eq("true"), - // ); - // } - // } - // } - // Server Side - // "update_password" => { - // if value.is_empty() { - // Config::set_password(&Config::get_auto_password()); - // } else { - // Config::set_password(value); - // } - // } #[cfg(target_os = "android")] "chat_server_mode" => { if let Ok(m) = serde_json::from_str::>(value) { @@ -1105,7 +888,11 @@ unsafe extern "C" fn set_by_name(name: *const c_char, value: *const c_char) { } fn handle_query_onlines(onlines: Vec, offlines: Vec) { - if let Some(s) = flutter::GLOBAL_EVENT_STREAM.read().unwrap().get(flutter::APP_TYPE_MAIN) { + if let Some(s) = flutter::GLOBAL_EVENT_STREAM + .read() + .unwrap() + .get(flutter::APP_TYPE_MAIN) + { let data = HashMap::from([ ("name", "callback_query_onlines".to_owned()), ("onlines", onlines.join(",")), diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 2aa4f36ec..cdfd0edce 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -138,10 +138,11 @@ pub fn get_license() -> String { } pub fn get_option(key: String) -> String { - #[cfg(any(target_os = "android", target_os = "ios"))] - return Config::get_option(&key); - #[cfg(not(any(target_os = "android", target_os = "ios")))] - return get_option_(&key); + get_option_(&key) + // #[cfg(any(target_os = "android", target_os = "ios"))] + // return Config::get_option(&key); + // #[cfg(not(any(target_os = "android", target_os = "ios")))] + // return get_option_(&key); } fn get_option_(key: &str) -> String { @@ -250,33 +251,31 @@ pub fn get_sound_inputs() -> Vec { } pub fn set_options(m: HashMap) { + *OPTIONS.lock().unwrap() = m.clone(); #[cfg(not(any(target_os = "android", target_os = "ios")))] - { - *OPTIONS.lock().unwrap() = m.clone(); - ipc::set_options(m).ok(); - } + ipc::set_options(m).ok(); + #[cfg(any(target_os = "android", target_os = "ios"))] + Config::set_options(m); } pub fn set_option(key: String, value: String) { + let mut options = OPTIONS.lock().unwrap(); + #[cfg(target_os = "macos")] + if &key == "stop-service" { + let is_stop = value == "Y"; + if is_stop && crate::platform::macos::uninstall() { + return; + } + } + if value.is_empty() { + options.remove(&key); + } else { + options.insert(key.clone(), value.clone()); + } + #[cfg(not(any(target_os = "android", target_os = "ios")))] + ipc::set_options(options.clone()).ok(); #[cfg(any(target_os = "android", target_os = "ios"))] Config::set_option(key, value); - #[cfg(not(any(target_os = "android", target_os = "ios")))] - { - let mut options = OPTIONS.lock().unwrap(); - #[cfg(target_os = "macos")] - if &key == "stop-service" { - let is_stop = value == "Y"; - if is_stop && crate::platform::macos::uninstall() { - return; - } - } - if value.is_empty() { - options.remove(&key); - } else { - options.insert(key.clone(), value.clone()); - } - ipc::set_options(options.clone()).ok(); - } } pub fn install_path() -> String { From 2f8b300518f6c887c6f5f88c814c123b928e0cca Mon Sep 17 00:00:00 2001 From: Crashys <99598990+crashys@users.noreply.github.com> Date: Mon, 8 Aug 2022 14:08:58 +0200 Subject: [PATCH 0166/2015] Create pt_PT New language translation (Portuguese / Portugal) --- src/lang/pt_PT | 303 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 303 insertions(+) create mode 100644 src/lang/pt_PT diff --git a/src/lang/pt_PT b/src/lang/pt_PT new file mode 100644 index 000000000..e6e282575 --- /dev/null +++ b/src/lang/pt_PT @@ -0,0 +1,303 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "Estado"), + ("Your Desktop", "Ambiente de Trabalho"), + ("desk_tip", "O seu Ambiente de Trabalho pode ser acedido com este ID e palavra-passe."), + ("Password", "Senha"), + ("Ready", "Pronto"), + ("Established", "Estabelecido"), + ("connecting_status", "A ligar à rede do RustDesk..."), + ("Enable Service", "Activar Serviço"), + ("Start Service", "Iniciar Serviço"), + ("Service is running", "Serviço está activo"), + ("Service is not running", "Serviço não está activo"), + ("not_ready_status", "Indisponível. Por favor verifique a sua ligação"), + ("Control Remote Desktop", "Controle o Ambiente de Trabalho à distância"), + ("Transfer File", "Transferir Ficheiro"), + ("Connect", "Ligar"), + ("Recent Sessions", "Sessões recentes"), + ("Address Book", "Lista de Endereços"), + ("Confirmation", "Confirmação"), + ("TCP Tunneling", "Túnel TCP"), + ("Remove", "Remover"), + ("Refresh random password", "Actualizar palavra-chave"), + ("Set your own password", "Configure a sua palavra-passe"), + ("Enable Keyboard/Mouse", "Activar Teclado/Rato"), + ("Enable Clipboard", "Activar Área de Transferência"), + ("Enable File Transfer", "Activar Transferência de Ficheiros"), + ("Enable TCP Tunneling", "Activar Túnel TCP"), + ("IP Whitelisting", "Whitelist de IP"), + ("ID/Relay Server", "Servidor ID/Relay"), + ("Stop service", "Parar serviço"), + ("Change ID", "Alterar ID"), + ("Website", "Website"), + ("About", "Sobre"), + ("Mute", "Emudecer"), + ("Audio Input", "Entrada de Áudio"), + ("Enhancements", "Melhorias"), + ("Hardware Codec", ""), + ("Adaptive Bitrate", ""), + ("ID Server", "Servidor de ID"), + ("Relay Server", "Servidor de Relay"), + ("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"), + ("Too frequent", "Muito frequente"), + ("Cancel", "Cancelar"), + ("Skip", "Passar"), + ("Close", "Fechar"), + ("Retry", "Tentar novamente"), + ("OK", "Confirmar"), + ("Password Required", "Palavra-chave Necessária"), + ("Please enter your password", "Por favor introduza a sua palavra-chave"), + ("Remember password", "Memorizar palavra-chave"), + ("Wrong Password", "Palavra-chave inválida"), + ("Do you want to enter again?", "Deseja tentar novamente??"), + ("Connection Error", "Erro de Ligação"), + ("Error", "Erro"), + ("Reset by the peer", "Reiniciado pelo destino"), + ("Connecting...", "A Ligar..."), + ("Connection in progress. Please wait.", "Ligação em progresso. Aguarde por favor."), + ("Please try 1 minute later", "Por favor tente após 1 minuto"), + ("Login Error", "Erro de Login"), + ("Successful", "Sucesso"), + ("Connected, waiting for image...", "Ligado. A aguardar pela imagem..."), + ("Name", "Nome"), + ("Type", "Tipo"), + ("Modified", "Modificado"), + ("Size", "Tamanho"), + ("Show Hidden Files", "Mostrar Ficheiros Ocultos"), + ("Receive", "Receber"), + ("Send", "Enviar"), + ("Refresh File", "Actualizar Ficheiro"), + ("Local", "Local"), + ("Remote", "Remoto"), + ("Remote Computer", "Computador Remoto"), + ("Local Computer", "Computador Local"), + ("Confirm Delete", "Confirmar Apagar"), + ("Delete", "Apagar"), + ("Properties", "Propriedades"), + ("Multi Select", "Selecção Múltipla"), + ("Empty Directory", "Directório Vazio"), + ("Not an empty directory", "Directório não está vazio"), + ("Are you sure you want to delete this file?", "Tem certeza que deseja apagar este ficheiro?"), + ("Are you sure you want to delete this empty directory?", "Tem certeza que deseja apagar este directório vazio?"), + ("Are you sure you want to delete the file of this directory?", "Tem certeza que deseja apagar este ficheiro deste directório?"), + ("Do this for all conflicts", "Fazer isto para todos os conflictos"), + ("This is irreversible!", "Isto é irreversível!"), + ("Deleting", "A apagar"), + ("files", "ficheiros"), + ("Waiting", "A aguardar"), + ("Finished", "Completo"), + ("Speed", "Velocidade"), + ("Custom Image Quality", "Qualidade Visual Personalizada"), + ("Privacy mode", "Modo privado"), + ("Block user input", "Bloquear entrada de utilizador"), + ("Unblock user input", "Desbloquear entrada de utilizador"), + ("Adjust Window", "Ajustar Janela"), + ("Original", "Original"), + ("Shrink", "Reduzir"), + ("Stretch", "Aumentar"), + ("Good image quality", "Qualidade visual boa"), + ("Balanced", "Equilibrada"), + ("Optimize reaction time", "Optimizar tempo de reacção"), + ("Custom", "Personalizado"), + ("Show remote cursor", "Mostrar cursor remoto"), + ("Show quality monitor", ""), + ("Disable clipboard", "Desabilitar área de transferência"), + ("Lock after session end", "Bloquear após o fim da sessão"), + ("Insert", "Inserir"), + ("Insert Lock", "Bloquear Inserir"), + ("Refresh", "Actualizar"), + ("ID does not exist", "ID não existente"), + ("Failed to connect to rendezvous server", "Falha ao ligar ao servidor de rendezvous"), + ("Please try later", "Por favor tente mais tarde"), + ("Remote desktop is offline", "Ambiente de trabalho remoto está desligado"), + ("Key mismatch", "Chaves incompatíveis"), + ("Timeout", "Tempo esgotado"), + ("Failed to connect to relay server", "Falha ao ligar ao servidor de relay"), + ("Failed to connect via rendezvous server", "Falha ao ligar ao servidor de rendezvous"), + ("Failed to connect via relay server", "Falha ao ligar através do servidor de relay"), + ("Failed to make direct connection to remote desktop", "Falha ao fazer ligação directa ao desktop remoto"), + ("Set Password", "Definir palavra-chave"), + ("OS Password", "Senha do SO"), + ("install_tip", "Devido ao UAC, o RustDesk não funciona correctamente em alguns casos. Para evitar o UAC, por favor clique no botão abaixo para instalar o RustDesk no sistema."), + ("Click to update", "Clique para fazer a actualização"), + ("Click to download", "Clique para carregar"), + ("Click to update", "Clique para fazer a actualização"), + ("Configure", "Configurar"), + ("config_acc", "Para controlar o seu Ambiente de Trabalho remotamente, é preciso conceder ao RustDesk permissões de \"Acessibilidade\"."), + ("config_screen", "Para aceder ao seu Ambiente de Trabalho remotamente, é preciso conceder ao RustDesk permissões de \"Gravar a Tela\"/"), + ("Installing ...", "A Instalar ..."), + ("Install", "Instalar"), + ("Installation", "Instalação"), + ("Installation Path", "Caminho da Instalação"), + ("Create start menu shortcuts", "Criar atalhos no menu iniciar"), + ("Create desktop icon", "Criar ícone no ambiente de trabalho"), + ("agreement_tip", "Ao iniciar a instalação, você concorda com o acordo de licença."), + ("Accept and Install", "Aceitar e Instalar"), + ("End-user license agreement", "Acordo de licença do utilizador final"), + ("Generating ...", "A Gerar ..."), + ("Your installation is lower version.", "A sua instalação é de uma versão anterior."), + ("not_close_tcp_tip", "Não feche esta janela enquanto estiver a utilizar o túnel"), + ("Listening ...", "A escuta ..."), + ("Remote Host", "Host Remoto"), + ("Remote Port", "Porta Remota"), + ("Action", "Acção"), + ("Add", "Adicionar"), + ("Local Port", "Porta Local"), + ("setup_server_tip", "Para uma ligação mais rápida, por favor configure seu próprio servidor"), + ("Too short, at least 6 characters.", "Muito curto, pelo menos 6 caracteres."), + ("The confirmation is not identical.", "A confirmação não é idêntica."), + ("Permissions", "Permissões"), + ("Accept", "Aceitar"), + ("Dismiss", "Dispensar"), + ("Disconnect", "Desconectar"), + ("Allow using keyboard and mouse", "Permitir o uso de teclado e rato"), + ("Allow using clipboard", "Permitir o uso da área de transferência"), + ("Allow hearing sound", "Permitir ouvir som"), + ("Allow file copy and paste", "Permitir copiar e mover ficheiros"), + ("Connected", "Ligado"), + ("Direct and encrypted connection", "Ligação directa e encriptada"), + ("Relayed and encrypted connection", "Ligação via relay e encriptada"), + ("Direct and unencrypted connection", "Ligação direta e não encriptada"), + ("Relayed and unencrypted connection", "Ligação via relay e não encriptada"), + ("Enter Remote ID", "Introduza o ID Remoto"), + ("Enter your password", "Introduza a sua palavra-chave"), + ("Logging in...", "A efectuar Login..."), + ("Enable RDP session sharing", "Activar partilha de sessão RDP"), + ("Auto Login", "Login Automático (Somente válido se você activou \"Bloquear após o fim da sessão\")"), + ("Enable Direct IP Access", "Activar Acesso IP Directo"), + ("Rename", "Renomear"), + ("Space", "Espaço"), + ("Create Desktop Shortcut", "Criar Atalho no Ambiente de Trabalho"), + ("Change Path", "Alterar Caminho"), + ("Create Folder", "Criar Diretório"), + ("Please enter the folder name", "Por favor introduza o nome do diretório"), + ("Fix it", "Reparar"), + ("Warning", "Aviso"), + ("Login screen using Wayland is not supported", "Tela de Login com Wayland não é suportada"), + ("Reboot required", "Reinicialização necessária"), + ("Unsupported display server ", "Servidor de display não suportado"), + ("x11 expected", "x11 em falha"), + ("Port", "Porta"), + ("Settings", "Configurações"), + ("Username", "Nome de utilizador"), + ("Invalid port", "Porta inválida"), + ("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"), + ("Always connect via relay", "Sempre conectar via relay"), + ("whitelist_tip", "Somente IPs na whitelist podem me acessar"), + ("Login", "Login"), + ("Logout", "Sair"), + ("Tags", "Tags"), + ("Search ID", "Procurar ID"), + ("Current Wayland display server is not supported", "Servidor de display Wayland atual não é suportado"), + ("whitelist_sep", "Separado por vírcula, ponto-e-vírgula, espaços ou nova linha"), + ("Add ID", "Adicionar ID"), + ("Add Tag", "Adicionar Tag"), + ("Unselect all tags", "Desselecionar todas as tags"), + ("Network error", "Erro de rede"), + ("Username missed", "Nome de utilizador em falta"), + ("Password missed", "Palavra-chave em falta"), + ("Wrong credentials", "Nome de utilizador ou palavra-chave incorrectos"), + ("Edit Tag", "Editar Tag"), + ("Unremember Password", "Esquecer Palavra-chave"), + ("Favorites", "Favoritos"), + ("Add to Favorites", "Adicionar aos Favoritos"), + ("Remove from Favorites", "Remover dos Favoritos"), + ("Empty", "Vazio"), + ("Invalid folder name", "Nome de diretório inválido"), + ("Socks5 Proxy", "Proxy Socks5"), + ("Hostname", "Nome de anfitrião"), + ("Discovered", "Descoberto"), + ("install_daemon_tip", "Para inicialização junto do sistema, deve instalar o serviço de sistema."), + ("Remote ID", "ID Remoto"), + ("Paste", "Colar"), + ("Paste here?", "Colar aqui?"), + ("Are you sure to close the connection?", "Tem certeza que deseja fechar a ligação?"), + ("Download new version", "Transferir nova versão"), + ("Touch mode", "Modo toque"), + ("Mouse mode", "Modo rato"), + ("One-Finger Tap", "Toque com um dedo"), + ("Left Mouse", "Botão esquerdo do rato"), + ("One-Long Tap", "Um toque longo"), + ("Two-Finger Tap", "Toque com dois dedos"), + ("Right Mouse", "Botão direito do rato"), + ("One-Finger Move", "Mover com um dedo"), + ("Double Tap & Move", "Toque duplo & mover"), + ("Mouse Drag", "Arrastar com o rato"), + ("Three-Finger vertically", "Três dedos verticalmente"), + ("Mouse Wheel", "Roda do rato"), + ("Two-Finger Move", "Mover com dois dedos"), + ("Canvas Move", "Mover Tela"), + ("Pinch to Zoom", "Beliscar para Zoom"), + ("Canvas Zoom", "Zoom na Tela"), + ("Reset canvas", "Reiniciar tela"), + ("No permission of file transfer", "Sem permissões de transferência de ficheiro"), + ("Note", "Nota"), + ("Connection", "Ligação"), + ("Share Screen", "Partilhar ecran"), + ("CLOSE", "FECHAR"), + ("OPEN", "ABRIR"), + ("Chat", "Conversar"), + ("Total", "Total"), + ("items", "itens"), + ("Selected", "Seleccionado"), + ("Screen Capture", "Captura de Ecran"), + ("Input Control", "Controle de Entrada"), + ("Audio Capture", "Captura de Áudio"), + ("File Connection", "Ligação de Arquivo"), + ("Screen Connection", "Ligação de Ecran"), + ("Do you accept?", "Aceita?"), + ("Open System Setting", "Abrir Configurações do Sistema"), + ("How to get Android input permission?", "Como activar a permissão de entrada do Android?"), + ("android_input_permission_tip1", "Para que um dispositivo remoto controle o seu dispositivo Android via rato ou toque, você precisa permitir que o RustDesk use o serviço \"Acessibilidade\"."), + ("android_input_permission_tip2", "Por favor vá para a próxima página de configuração do sistema, encontre e entre [Serviços Instalados], ACTIVE o serviço [RustDesk Input]."), + ("android_new_connection_tip", "Nova requisição de controle recebida, solicita o controle do seu dispositivo atual."), + ("android_service_will_start_tip", "Activar a Captura de Ecran irá automaticamente inicializar o serviço, permitindo que outros dispositivos solicitem uma ligação deste dispositivo."), + ("android_stop_service_tip", "Fechar o serviço irá automaticamente fechar todas as ligações estabelecidas."), + ("android_version_audio_tip", "A versão atual do Android não suporta captura de áudio, por favor actualize para o Android 10 ou maior."), + ("android_start_service_tip", "Toque [Iniciar Serviço] ou abra a permissão [Captura de Ecran] para iniciar o serviço de partilha de ecran."), + ("Account", "Conta"), + ("Overwrite", "Substituir"), + ("This file exists, skip or overwrite this file?", "Este ficheiro já existe, ignorar ou substituir este ficheiro?"), + ("Quit", "Saída"), + ("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"), + ("Help", "Ajuda"), + ("Failed", "Falhou"), + ("Succeeded", "Conseguiu"), + ("Someone turns on privacy mode, exit", "Alguém activou o modo de privacidade, desligue"), + ("Unsupported", "Sem suporte"), + ("Peer denied", "Remoto negado"), + ("Please install plugins", "Por favor instale plugins"), + ("Peer exit", "Saída do Remoto"), + ("Failed to turn off", "Falha ao desligar"), + ("Turned off", "Desligado"), + ("In privacy mode", "Em modo de privacidade"), + ("Out privacy mode", "Sair do modo de privacidade"), + ("Language", "Linguagem"), + ("Keep RustDesk background service", "Manter o serviço RustDesk em funcionamento"), + ("Ignore Battery Optimizations", "Ignorar optimizações de Bateria"), + ("android_open_battery_optimizations_tip", ""), + ("Connection not allowed", "Ligação não autorizada"), + ("Use temporary password", "Utilizar palavra-chave temporária"), + ("Use permanent password", "Utilizar palavra-chave permanente"), + ("Use both passwords", "Utilizar ambas as palavras-chave"), + ("Set permanent password", "Definir palavra-chave permanente"), + ("Set temporary password length", "Definir tamanho de palavra-chave temporária"), + ("Enable Remote Restart", "Activar reiniciar remoto"), + ("Allow remote restart", "Permitir reiniciar remoto"), + ("Restart Remote Device", "Reiniciar Dispositivo Remoto"), + ("Are you sure you want to restart", "Tem a certeza que pretende reiniciar"), + ("Restarting Remote Device", "A reiniciar sistema remoto"), + ("remote_restarting_tip", ""), + ].iter().cloned().collect(); +} From e420178750574de079ee604e1e5ff9f46dad1cfe Mon Sep 17 00:00:00 2001 From: csf Date: Mon, 8 Aug 2022 22:27:27 +0800 Subject: [PATCH 0167/2015] refactor all [setByName] [getByName] to async bridge function --- .../lib/desktop/pages/connection_page.dart | 28 +- .../lib/desktop/pages/desktop_home_page.dart | 12 +- flutter/lib/desktop/pages/remote_page.dart | 5 +- .../lib/desktop/widgets/peercard_widget.dart | 2 +- flutter/lib/mobile/pages/connection_page.dart | 35 +- flutter/lib/mobile/pages/remote_page.dart | 7 +- flutter/lib/mobile/pages/server_page.dart | 18 +- flutter/lib/mobile/pages/settings_page.dart | 86 ++-- flutter/lib/mobile/widgets/dialog.dart | 23 +- flutter/lib/models/chat_model.dart | 11 +- flutter/lib/models/file_model.dart | 2 +- flutter/lib/models/model.dart | 30 +- flutter/lib/models/native_model.dart | 39 +- flutter/lib/models/server_model.dart | 79 ++-- src/client.rs | 2 +- src/common.rs | 6 +- src/flutter_ffi.rs | 413 ++++++------------ src/server/connection.rs | 4 +- 18 files changed, 332 insertions(+), 470 deletions(-) diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index d992c6c62..d1080dbd3 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -825,10 +825,34 @@ class WebMenu extends StatefulWidget { } class _WebMenuState extends State { + String? username; + String url = ""; + + @override + void initState() { + super.initState(); + () async { + final usernameRes = await getUsername(); + final urlRes = await getUrl(); + var update = false; + if (usernameRes != username) { + username = usernameRes; + update = true; + } + if (urlRes != url) { + url = urlRes; + update = true; + } + + if (update) { + setState(() {}); + } + }(); + } + @override Widget build(BuildContext context) { Provider.of(context); - final username = getUsername(); return PopupMenuButton( icon: Icon(Icons.more_vert), itemBuilder: (context) { @@ -846,7 +870,7 @@ class _WebMenuState extends State { value: "server", ) ] + - (getUrl().contains('admin.rustdesk.com') + (url.contains('admin.rustdesk.com') ? >[] : [ PopupMenuItem( diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 3f908c3ce..f02e0fdfd 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -275,9 +275,7 @@ class _DesktopHomePageState extends State with TrayListener { ), IconButton( icon: Icon(Icons.refresh), - onPressed: () { - gFFI.setByName("temporary_password"); - }, + onPressed: () => bind.mainUpdateTemporaryPassword(), ), FutureBuilder( future: buildPasswordPopupMenu(context), @@ -360,7 +358,7 @@ class _DesktopHomePageState extends State with TrayListener { if (gFFI.serverModel.temporaryPasswordLength != e) { gFFI.serverModel.temporaryPasswordLength = e; - gFFI.setByName("temporary_password"); + bind.mainUpdateTemporaryPassword(); } }, )) @@ -1336,8 +1334,8 @@ Future loginDialog() async { return completer.future; } -void setPasswordDialog() { - final pw = gFFI.getByName("permanent_password"); +void setPasswordDialog() async { + final pw = await bind.mainGetPermanentPassword(); final p0 = TextEditingController(text: pw); final p1 = TextEditingController(text: pw); var errMsg0 = ""; @@ -1427,7 +1425,7 @@ void setPasswordDialog() { }); return; } - gFFI.setByName("permanent_password", pass); + bind.mainSetPermanentPassword(password: pass); close(); }, child: Text(translate("OK"))), diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index bfc193a89..a945e3ae3 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -348,8 +348,9 @@ class _RemotePageState extends State if (dy > 0) dy = -1; else if (dy < 0) dy = 1; - _ffi.setByName('send_mouse', - '{"id": "${widget.id}", "type": "wheel", "x": "$dx", "y": "$dy"}'); + bind.sessionSendMouse( + id: widget.id, + msg: '{"type": "wheel", "x": "$dx", "y": "$dy"}'); } }, child: MouseRegion( diff --git a/flutter/lib/desktop/widgets/peercard_widget.dart b/flutter/lib/desktop/widgets/peercard_widget.dart index 949c46234..f4743a7b5 100644 --- a/flutter/lib/desktop/widgets/peercard_widget.dart +++ b/flutter/lib/desktop/widgets/peercard_widget.dart @@ -187,7 +187,7 @@ class _PeerCardState extends State<_PeerCard> elevation: 8, ); if (value == 'remove') { - setState(() => gFFI.setByName('remove', '$id')); + setState(() => bind.mainRemovePeer(id: id)); () async { removePreference(id); }(); diff --git a/flutter/lib/mobile/pages/connection_page.dart b/flutter/lib/mobile/pages/connection_page.dart index 69e7b9433..227bfb630 100644 --- a/flutter/lib/mobile/pages/connection_page.dart +++ b/flutter/lib/mobile/pages/connection_page.dart @@ -54,8 +54,9 @@ class _ConnectionPageState extends State { }(); } if (isAndroid) { - Timer(Duration(seconds: 5), () { - _updateUrl = gFFI.getByName('software_update_url'); + Timer(Duration(seconds: 5), () async { + _updateUrl = await bind.mainGetSoftwareUpdateUrl(); + ; if (_updateUrl.isNotEmpty) setState(() {}); }); } @@ -299,7 +300,7 @@ class _ConnectionPageState extends State { elevation: 8, ); if (value == 'remove') { - setState(() => gFFI.setByName('remove', '$id')); + setState(() => bind.mainRemovePeer(id: id)); () async { removePreference(id); }(); @@ -315,10 +316,34 @@ class WebMenu extends StatefulWidget { } class _WebMenuState extends State { + String? username; + String url = ""; + + @override + void initState() { + super.initState(); + () async { + final usernameRes = await getUsername(); + final urlRes = await getUrl(); + var update = false; + if (usernameRes != username) { + username = usernameRes; + update = true; + } + if (urlRes != url) { + url = urlRes; + update = true; + } + + if (update) { + setState(() {}); + } + }(); + } + @override Widget build(BuildContext context) { Provider.of(context); - final username = getUsername(); return PopupMenuButton( icon: Icon(Icons.more_vert), itemBuilder: (context) { @@ -336,7 +361,7 @@ class _WebMenuState extends State { value: "server", ) ] + - (getUrl().contains('admin.rustdesk.com') + (url.contains('admin.rustdesk.com') ? >[] : [ PopupMenuItem( diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 6ea4ca2e6..9b938a1ce 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -330,8 +330,9 @@ class _RemotePageState extends State { if (dy > 0) dy = -1; else if (dy < 0) dy = 1; - gFFI.setByName( - 'send_mouse', '{"type": "wheel", "x": "$dx", "y": "$dy"}'); + bind.sessionSendMouse( + id: widget.id, + msg: '{"type": "wheel", "x": "$dx", "y": "$dy"}'); } }, child: MouseRegion( @@ -1124,7 +1125,7 @@ void showRestartRemoteDevice(PeerInfo pi, String id) async { onPressed: () => close(true), child: Text(translate("OK"))), ], )); - if (res == true) gFFI.setByName('restart_remote_device'); + if (res == true) bind.sessionRestartRemoteDevice(id: id); } void showSetOSPassword(String id, bool login) async { diff --git a/flutter/lib/mobile/pages/server_page.dart b/flutter/lib/mobile/pages/server_page.dart index 19753bcac..3abcd70da 100644 --- a/flutter/lib/mobile/pages/server_page.dart +++ b/flutter/lib/mobile/pages/server_page.dart @@ -1,13 +1,10 @@ -import 'dart:convert'; - import 'package:flutter/material.dart'; import 'package:flutter_hbb/mobile/widgets/dialog.dart'; -import 'package:flutter_hbb/models/model.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:provider/provider.dart'; import '../../common.dart'; -import '../../models/model.dart'; +import '../../models/platform_model.dart'; import '../../models/server_model.dart'; import 'home_page.dart'; @@ -99,10 +96,7 @@ class ServerPage extends StatelessWidget implements PageShape { } else if (value == kUsePermanentPassword || value == kUseTemporaryPassword || value == kUseBothPasswords) { - Map msg = Map() - ..["name"] = "verification-method" - ..["value"] = value; - gFFI.setByName('option', jsonEncode(msg)); + bind.mainSetOption(key: "verification-method", value: value); gFFI.serverModel.updatePasswordModel(); } }) @@ -183,9 +177,8 @@ class ServerInfo extends StatelessWidget { ? null : IconButton( icon: const Icon(Icons.refresh), - onPressed: () { - gFFI.setByName("temporary_password"); - })), + onPressed: () => + bind.mainUpdateTemporaryPassword())), onSaved: (String? value) {}, ), ], @@ -406,8 +399,7 @@ class ConnectionManager extends StatelessWidget { MaterialStateProperty.all(Colors.red)), icon: Icon(Icons.close), onPressed: () { - gFFI.setByName( - "close_conn", entry.key.toString()); + bind.serverCloseConnection(connId: entry.key); gFFI.invokeMethod( "cancel_notification", entry.key); }, diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index 3646b59e9..4b8760413 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -31,15 +31,38 @@ class SettingsPage extends StatefulWidget implements PageShape { const url = 'https://rustdesk.com/'; final _hasIgnoreBattery = androidVersion >= 26; var _ignoreBatteryOpt = false; +var _enableAbr = false; class _SettingsState extends State with WidgetsBindingObserver { + String? username; + @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); - if (_hasIgnoreBattery) { - updateIgnoreBatteryStatus(); - } + + () async { + var update = false; + if (_hasIgnoreBattery) { + update = await updateIgnoreBatteryStatus(); + } + + final usernameRes = await getUsername(); + if (usernameRes != username) { + update = true; + username = usernameRes; + } + + final enableAbrRes = await bind.mainGetOption(key: "enable-abr") != "N"; + if (enableAbrRes != _enableAbr) { + update = true; + _enableAbr = enableAbrRes; + } + + if (update) { + setState(() {}); + } + }(); } @override @@ -51,16 +74,18 @@ class _SettingsState extends State with WidgetsBindingObserver { @override void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.resumed) { - updateIgnoreBatteryStatus(); + () async { + if (await updateIgnoreBatteryStatus()) { + setState(() {}); + } + }(); } } Future updateIgnoreBatteryStatus() async { final res = await PermissionManager.check("ignore_battery_optimizations"); if (_ignoreBatteryOpt != res) { - setState(() { - _ignoreBatteryOpt = res; - }); + _ignoreBatteryOpt = res; return true; } else { return false; @@ -70,21 +95,15 @@ class _SettingsState extends State with WidgetsBindingObserver { @override Widget build(BuildContext context) { Provider.of(context); - final username = getUsername(); - final enableAbr = gFFI.getByName("option", "enable-abr") != 'N'; final enhancementsTiles = [ SettingsTile.switchTile( - title: Text(translate('Adaptive Bitrate') + '(beta)'), - initialValue: enableAbr, + title: Text(translate('Adaptive Bitrate') + ' (beta)'), + initialValue: _enableAbr, onToggle: (v) { - final msg = Map() - ..["name"] = "enable-abr" - ..["value"] = ""; - if (!v) { - msg["value"] = "N"; - } - gFFI.setByName("option", json.encode(msg)); - setState(() {}); + bind.mainSetOption(key: "enable-abr", value: v ? "" : "N"); + setState(() { + _enableAbr = !_enableAbr; + }); }, ) ]; @@ -196,7 +215,7 @@ void showServerSettings() async { void showLanguageSettings() async { try { - final langs = json.decode(gFFI.getByName('langs')) as List; + final langs = json.decode(await bind.mainGetLangs()) as List; var lang = await bind.mainGetLocalOption(key: "lang"); DialogManager.show((setState, close) { final setLang = (v) { @@ -297,20 +316,19 @@ String parseResp(String body) { } final token = data['access_token']; if (token != null) { - gFFI.setByName('option', '{"name": "access_token", "value": "$token"}'); + bind.mainSetOption(key: "access_token", value: token); } final info = data['user']; if (info != null) { final value = json.encode(info); - gFFI.setByName( - 'option', json.encode({"name": "user_info", "value": value})); + bind.mainSetOption(key: "user_info", value: value); gFFI.ffiModel.updateUser(); } return ''; } void refreshCurrentUser() async { - final token = gFFI.getByName("option", "access_token"); + final token = await bind.mainGetOption(key: "access_token"); if (token == '') return; final url = getUrl(); final body = {'id': bind.mainGetMyId(), 'uuid': bind.mainGetUuid()}; @@ -333,7 +351,7 @@ void refreshCurrentUser() async { } void logout() async { - final token = gFFI.getByName("option", "access_token"); + final token = await bind.mainGetOption(key: "access_token"); if (token == '') return; final url = getUrl(); final body = {'id': bind.mainGetMyId(), 'uuid': bind.mainGetUuid()}; @@ -350,16 +368,16 @@ void logout() async { resetToken(); } -void resetToken() { - gFFI.setByName('option', '{"name": "access_token", "value": ""}'); - gFFI.setByName('option', '{"name": "user_info", "value": ""}'); +void resetToken() async { + await bind.mainSetOption(key: "access_token", value: ""); + await bind.mainSetOption(key: "user_info", value: ""); gFFI.ffiModel.updateUser(); } -String getUrl() { - var url = gFFI.getByName('option', 'api-server'); +Future getUrl() async { + var url = await bind.mainGetOption(key: "api-server"); if (url == '') { - url = gFFI.getByName('option', 'custom-rendezvous-server'); + url = await bind.mainGetOption(key: "custom-rendezvous-server"); if (url != '') { if (url.contains(':')) { final tmp = url.split(':'); @@ -448,11 +466,11 @@ void showLogin() { }); } -String? getUsername() { - final token = gFFI.getByName("option", "access_token"); +Future getUsername() async { + final token = await bind.mainGetOption(key: "access_token"); String? username; if (token != "") { - final info = gFFI.getByName("option", "user_info"); + final info = await bind.mainGetOption(key: "user_info"); if (info != "") { try { Map tmp = json.decode(info); diff --git a/flutter/lib/mobile/widgets/dialog.dart b/flutter/lib/mobile/widgets/dialog.dart index 3ab0489a9..ddd6816fb 100644 --- a/flutter/lib/mobile/widgets/dialog.dart +++ b/flutter/lib/mobile/widgets/dialog.dart @@ -1,11 +1,9 @@ import 'dart:async'; -import 'dart:convert'; - import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import '../../common.dart'; -import '../../models/model.dart'; +import '../../models/platform_model.dart'; void clientClose() { msgBox('', 'Close', 'Are you sure to close the connection?'); @@ -22,8 +20,8 @@ void showError({Duration duration = SEC1}) { showToast(translate("Error"), duration: SEC1); } -void setPermanentPasswordDialog() { - final pw = gFFI.getByName("permanent_password"); +void setPermanentPasswordDialog() async { + final pw = await bind.mainGetPermanentPassword(); final p0 = TextEditingController(text: pw); final p1 = TextEditingController(text: pw); var validateLength = false; @@ -103,9 +101,9 @@ void setPermanentPasswordDialog() { }); } -void setTemporaryPasswordLengthDialog() { +void setTemporaryPasswordLengthDialog() async { List lengths = ['6', '8', '10']; - String length = gFFI.getByName('option', 'temporary-password-length'); + String length = await bind.mainGetOption(key: "temporary-password-length"); var index = lengths.indexOf(length); if (index < 0) index = 0; length = lengths[index]; @@ -116,11 +114,8 @@ void setTemporaryPasswordLengthDialog() { setState(() { length = newValue; }); - Map msg = Map() - ..["name"] = "temporary-password-length" - ..["value"] = newValue; - gFFI.setByName("option", jsonEncode(msg)); - gFFI.setByName("temporary_password"); + bind.mainSetOption(key: "temporary-password-length", value: newValue); + bind.mainUpdateTemporaryPassword(); Future.delayed(Duration(milliseconds: 200), () { close(); showSuccess(); @@ -138,9 +133,9 @@ void setTemporaryPasswordLengthDialog() { }, backDismiss: true, clickMaskDismiss: true); } -void enterPasswordDialog(String id) { +void enterPasswordDialog(String id) async { final controller = TextEditingController(); - var remember = gFFI.getByName('remember', id) == 'true'; + var remember = await bind.getSessionRemember(id: id) ?? false; DialogManager.show((setState, close) { return CustomAlertDialog( title: Text(translate('Password Required')), diff --git a/flutter/lib/models/chat_model.dart b/flutter/lib/models/chat_model.dart index 28ffa65e2..52f00aa01 100644 --- a/flutter/lib/models/chat_model.dart +++ b/flutter/lib/models/chat_model.dart @@ -1,5 +1,3 @@ -import 'dart:convert'; - import 'package:dash_chat_2/dash_chat_2.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/models/platform_model.dart'; @@ -106,12 +104,11 @@ class ChatModel with ChangeNotifier { if (message.text.isNotEmpty) { _messages[_currentID]?.insert(message); if (_currentID == clientModeID) { - _ffi.target?.setByName("chat_client_mode", message.text); + if (_ffi.target != null) { + bind.sessionSendChat(id: _ffi.target!.id, text: message.text); + } } else { - final msg = Map() - ..["id"] = _currentID - ..["text"] = message.text; - _ffi.target?.setByName("chat_server_mode", jsonEncode(msg)); + bind.serverSendChat(connId: _currentID, msg: message.text); } } notifyListeners(); diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index 45f5ec970..75f3f8045 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -290,7 +290,7 @@ class FileModel extends ChangeNotifier { } onReady() async { - _localOption.home = _ffi.target?.getByName("get_home_dir") ?? ""; + _localOption.home = await bind.mainGetHomeDir(); _localOption.showHidden = (await bind.sessionGetPeerOption( id: _ffi.target?.id ?? "", name: "local_show_hidden")) .isNotEmpty; diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 7ca77f6cd..c7295f57e 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -889,8 +889,10 @@ class FFI { /// Send scroll event with scroll distance [y]. void scroll(int y) { - setByName('send_mouse', - json.encode(modify({'id': id, 'type': 'wheel', 'y': y.toString()}))); + bind.sessionSendMouse( + id: id, + msg: json + .encode(modify({'id': id, 'type': 'wheel', 'y': y.toString()}))); } /// Reconnect to the remote peer. @@ -916,8 +918,9 @@ class FFI { /// Send mouse press event. void sendMouse(String type, MouseButtons button) { if (!ffiModel.keyboard()) return; - setByName('send_mouse', - json.encode(modify({'id': id, 'type': type, 'buttons': button.value}))); + bind.sessionSendMouse( + id: id, + msg: json.encode(modify({'type': type, 'buttons': button.value}))); } /// Send key stroke event. @@ -953,8 +956,8 @@ class FFI { if (!ffiModel.keyboard()) return; var x2 = x.toInt(); var y2 = y.toInt(); - setByName( - 'send_mouse', json.encode(modify({'id': id, 'x': '$x2', 'y': '$y2'}))); + bind.sessionSendMouse( + id: id, msg: json.encode(modify({'x': '$x2', 'y': '$y2'}))); } /// List the saved peers. @@ -1032,14 +1035,14 @@ class FFI { /// Send **get** command to the Rust core based on [name] and [arg]. /// Return the result as a string. - String getByName(String name, [String arg = '']) { - return platformFFI.getByName(name, arg); - } + // String getByName(String name, [String arg = '']) { + // return platformFFI.getByName(name, arg); + // } /// Send **set** command to the Rust core based on [name] and [value]. - void setByName(String name, [String value = '']) { - platformFFI.setByName(name, value); - } + // void setByName(String name, [String value = '']) { + // platformFFI.setByName(name, value); + // } handleMouse(Map evt, {double tabBarHeight = 0.0}) { var type = ''; @@ -1092,8 +1095,7 @@ class FFI { break; } evt['buttons'] = buttons; - evt['id'] = id; - setByName('send_mouse', json.encode(evt)); + bind.sessionSendMouse(id: id, msg: json.encode(evt)); } listenToMouse(bool yesOrNo) { diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index c58577945..a55ed1d29 100644 --- a/flutter/lib/models/native_model.dart +++ b/flutter/lib/models/native_model.dart @@ -30,8 +30,6 @@ class PlatformFFI { String _dir = ''; String _homeDir = ''; F2? _translate; - F2? _getByName; - F3? _setByName; var _eventHandlers = Map>(); late RustdeskImpl _ffiBind; late String _appType; @@ -89,31 +87,6 @@ class PlatformFFI { return res; } - /// Send **get** command to the Rust core based on [name] and [arg]. - /// Return the result as a string. - String getByName(String name, [String arg = '']) { - if (_getByName == null) return ''; - var a = name.toNativeUtf8(); - var b = arg.toNativeUtf8(); - var p = _getByName!(a, b); - assert(p != nullptr); - var res = p.toDartString(); - calloc.free(p); - calloc.free(a); - calloc.free(b); - return res; - } - - /// Send **set** command to the Rust core based on [name] and [value]. - void setByName(String name, [String value = '']) { - if (_setByName == null) return; - var a = name.toNativeUtf8(); - var b = value.toNativeUtf8(); - _setByName!(a, b); - calloc.free(a); - calloc.free(b); - } - /// Init the FFI class, loads the native Rust core library. Future init(String appType) async { _appType = appType; @@ -133,10 +106,6 @@ class PlatformFFI { debugPrint('initializing FFI ${_appType}'); try { _translate = dylib.lookupFunction('translate'); - _getByName = dylib.lookupFunction('get_by_name'); - _setByName = - dylib.lookupFunction, Pointer), F3>( - 'set_by_name'); _dir = (await getApplicationDocumentsDirectory()).path; _ffiBind = RustdeskImpl(dylib); _startListenEvent(_ffiBind); // global event @@ -177,10 +146,10 @@ class PlatformFFI { } print( "_appType:$_appType,info1-id:$id,info2-name:$name,dir:$_dir,homeDir:$_homeDir"); - setByName('info1', id); - setByName('info2', name); - setByName('home_dir', _homeDir); - setByName('init', _dir); + await _ffiBind.mainDeviceId(id: id); + await _ffiBind.mainDeviceName(name: name); + await _ffiBind.mainSetHomeDir(home: _homeDir); + await _ffiBind.mainInit(appDir: _dir); } catch (e) { print(e); } diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index 362e47a78..6aa7016b2 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -99,42 +99,29 @@ class ServerModel with ChangeNotifier { // audio if (androidVersion < 30 || !await PermissionManager.check("audio")) { _audioOk = false; - parent.target?.setByName( - 'option', - jsonEncode(Map() - ..["name"] = "enable-audio" - ..["value"] = "N")); + bind.mainSetOption(key: "enable-audio", value: "N"); } else { - final audioOption = parent.target?.getByName('option', 'enable-audio'); - _audioOk = audioOption?.isEmpty ?? false; + final audioOption = await bind.mainGetOption(key: 'enable-audio'); + _audioOk = audioOption.isEmpty; } // file if (!await PermissionManager.check("file")) { _fileOk = false; - parent.target?.setByName( - 'option', - jsonEncode(Map() - ..["name"] = "enable-file-transfer" - ..["value"] = "N")); + bind.mainSetOption(key: "enable-file-transfer", value: "N"); } else { final fileOption = - parent.target?.getByName('option', 'enable-file-transfer'); - _fileOk = fileOption?.isEmpty ?? false; + await bind.mainGetOption(key: 'enable-file-transfer'); + _fileOk = fileOption.isEmpty; } - // input (mouse control) - Map res = Map() - ..["name"] = "enable-keyboard" - ..["value"] = 'N'; - parent.target - ?.setByName('option', jsonEncode(res)); // input false by default + // input (mouse control) false by default + bind.mainSetOption(key: "enable-keyboard", value: "N"); notifyListeners(); }(); - Timer.periodic(Duration(seconds: 1), (timer) { - var status = - int.tryParse(parent.target?.getByName('connect_statue') ?? "") ?? 0; + Timer.periodic(Duration(seconds: 1), (timer) async { + var status = await bind.mainGetOnlineStatue(); if (status > 0) { status = 1; } @@ -142,10 +129,8 @@ class ServerModel with ChangeNotifier { _connectStatus = status; notifyListeners(); } - final res = parent.target - ?.getByName('check_clients_length', _clients.length.toString()) ?? - ""; - if (res.isNotEmpty) { + final res = await bind.mainCheckClientsLength(length: _clients.length); + if (res != null) { debugPrint("clients not match!"); updateClientState(res); } @@ -156,7 +141,7 @@ class ServerModel with ChangeNotifier { updatePasswordModel() async { var update = false; - final temporaryPassword = gFFI.getByName("temporary_password"); + final temporaryPassword = await bind.mainGetTemporaryPassword(); final verificationMethod = await bind.mainGetOption(key: "verification-method"); final temporaryPasswordLength = @@ -194,10 +179,7 @@ class ServerModel with ChangeNotifier { } _audioOk = !_audioOk; - Map res = Map() - ..["name"] = "enable-audio" - ..["value"] = _audioOk ? '' : 'N'; - parent.target?.setByName('option', jsonEncode(res)); + bind.mainSetOption(key: "enable-audio", value: _audioOk ? '' : 'N'); notifyListeners(); } @@ -211,10 +193,7 @@ class ServerModel with ChangeNotifier { } _fileOk = !_fileOk; - Map res = Map() - ..["name"] = "enable-file-transfer" - ..["value"] = _fileOk ? '' : 'N'; - parent.target?.setByName('option', jsonEncode(res)); + bind.mainSetOption(key: "enable-file-transfer", value: _fileOk ? '' : 'N'); notifyListeners(); } @@ -284,7 +263,7 @@ class ServerModel with ChangeNotifier { // TODO parent.target?.ffiModel.updateEventListener(""); await parent.target?.invokeMethod("init_service"); - parent.target?.setByName("start_service"); + await bind.mainStartService(); _fetchID(); updateClientState(); if (!Platform.isLinux) { @@ -299,7 +278,7 @@ class ServerModel with ChangeNotifier { // TODO parent.target?.serverModel.closeAll(); await parent.target?.invokeMethod("stop_service"); - parent.target?.setByName("stop_service"); + await bind.mainStopService(); notifyListeners(); if (!Platform.isLinux) { // current linux is not supported @@ -312,9 +291,9 @@ class ServerModel with ChangeNotifier { } Future setPermanentPassword(String newPW) async { - parent.target?.setByName("permanent_password", newPW); + await bind.mainSetPermanentPassword(password: newPW); await Future.delayed(Duration(milliseconds: 500)); - final pw = parent.target?.getByName("permanent_password"); + final pw = await bind.mainGetPermanentPassword(); if (newPW == pw) { return true; } else { @@ -355,10 +334,7 @@ class ServerModel with ChangeNotifier { break; case "input": if (_inputOk != value) { - Map res = Map() - ..["name"] = "enable-keyboard" - ..["value"] = value ? '' : 'N'; - parent.target?.setByName('option', jsonEncode(res)); + bind.mainSetOption(key: "enable-keyboard", value: value ? '' : 'N'); } _inputOk = value; break; @@ -368,8 +344,8 @@ class ServerModel with ChangeNotifier { notifyListeners(); } - updateClientState([String? json]) { - var res = json ?? parent.target?.getByName("clients_state") ?? ""; + updateClientState([String? json]) async { + var res = await bind.mainGetClientsState(); try { final List clientsJson = jsonDecode(res); for (var clientJson in clientsJson) { @@ -451,12 +427,9 @@ class ServerModel with ChangeNotifier { }); } - void sendLoginResponse(Client client, bool res) { - final Map response = Map(); - response["id"] = client.id; - response["res"] = res; + void sendLoginResponse(Client client, bool res) async { if (res) { - parent.target?.setByName("login_res", jsonEncode(response)); + bind.serverLoginRes(connId: client.id, res: res); if (!client.isFileTransfer) { parent.target?.invokeMethod("start_capture"); } @@ -464,7 +437,7 @@ class ServerModel with ChangeNotifier { _clients[client.id]?.authorized = true; notifyListeners(); } else { - parent.target?.setByName("login_res", jsonEncode(response)); + bind.serverLoginRes(connId: client.id, res: res); parent.target?.invokeMethod("cancel_notification", client.id); _clients.remove(client.id); } @@ -496,7 +469,7 @@ class ServerModel with ChangeNotifier { closeAll() { _clients.forEach((id, client) { - parent.target?.setByName("close_conn", id.toString()); + bind.serverCloseConnection(connId: id); }); _clients.clear(); } diff --git a/src/client.rs b/src/client.rs index 7ddfe0969..89d66c6ca 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1280,7 +1280,7 @@ impl LoginConfigHandler { /// Create a [`Message`] for login. fn create_login_msg(&self, password: Vec) -> Message { #[cfg(any(target_os = "android", target_os = "ios"))] - let my_id = Config::get_id_or(crate::common::FLUTTER_INFO1.lock().unwrap().clone()); + let my_id = Config::get_id_or(crate::common::DEVICE_ID.lock().unwrap().clone()); #[cfg(not(any(target_os = "android", target_os = "ios")))] let my_id = Config::get_id(); let mut lr = LoginRequest { diff --git a/src/common.rs b/src/common.rs index 5af811c05..605435956 100644 --- a/src/common.rs +++ b/src/common.rs @@ -28,8 +28,8 @@ lazy_static::lazy_static! { } lazy_static::lazy_static! { - pub static ref FLUTTER_INFO1: Arc> = Default::default(); - pub static ref FLUTTER_INFO2: Arc> = Default::default(); + pub static ref DEVICE_ID: Arc> = Default::default(); + pub static ref DEVICE_NAME: Arc> = Default::default(); } #[inline] @@ -441,7 +441,7 @@ pub fn username() -> String { #[cfg(not(any(target_os = "android", target_os = "ios")))] return whoami::username().trim_end_matches('\0').to_owned(); #[cfg(any(target_os = "android", target_os = "ios"))] - return FLUTTER_INFO2.lock().unwrap().clone(); + return DEVICE_NAME.lock().unwrap().clone(); } #[inline] diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 40f72444a..95cd1abd3 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -26,7 +26,8 @@ use crate::ui_interface::{ get_connect_status, get_fav, get_id, get_lan_peers, get_license, get_local_option, get_option, get_options, get_peer, get_peer_option, get_socks, get_sound_inputs, get_uuid, get_version, has_rendezvous_service, post_request, set_local_option, set_option, set_options, - set_peer_option, set_socks, store_fav, test_if_valid_server, using_public_server, + set_peer_option, set_permanent_password, set_socks, store_fav, test_if_valid_server, + update_temporary_password, using_public_server, }; fn initialize(app_dir: &str) { @@ -235,38 +236,6 @@ pub fn session_send_chat(id: String, text: String) { } } -// if let Some(_type) = m.get("type") { -// mask = match _type.as_str() { -// "down" => 1, -// "up" => 2, -// "wheel" => 3, -// _ => 0, -// }; -// } -// if let Some(buttons) = m.get("buttons") { -// mask |= match buttons.as_str() { -// "left" => 1, -// "right" => 2, -// "wheel" => 4, -// _ => 0, -// } << 3; -// } -// TODO -pub fn session_send_mouse( - id: String, - mask: i32, - x: i32, - y: i32, - alt: bool, - ctrl: bool, - shift: bool, - command: bool, -) { - if let Some(session) = SESSIONS.read().unwrap().get(&id) { - session.send_mouse(mask, x, y, alt, ctrl, shift, command); - } -} - pub fn session_peer_option(id: String, name: String, value: String) { if let Some(session) = SESSIONS.read().unwrap().get(&id) { session.set_option(name, value); @@ -426,11 +395,7 @@ pub fn main_set_option(key: String, value: String) { set_option(key, value); #[cfg(target_os = "android")] crate::rendezvous_mediator::RendezvousMediator::restart(); - #[cfg(any( - target_os = "android", - target_os = "ios", - feature = "cli" - ))] + #[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))] crate::common::test_rendezvous_server(); } else { set_option(key, value); @@ -640,6 +605,143 @@ pub fn main_get_last_remote_id() -> String { LocalConfig::get_remote_id() } +pub fn main_get_software_update_url() -> String { + crate::common::SOFTWARE_UPDATE_URL.lock().unwrap().clone() +} + +pub fn main_get_home_dir() -> String { + fs::get_home_as_string() +} + +pub fn main_get_langs() -> String { + crate::lang::LANGS.to_string() +} + +pub fn main_get_temporary_password() -> String { + ui_interface::temporary_password() +} + +pub fn main_get_permanent_password() -> String { + ui_interface::permanent_password() +} + +pub fn main_get_online_statue() -> i64 { + ONLINE.lock().unwrap().values().max().unwrap_or(&0).clone() +} + +pub fn main_get_clients_state() -> String { + get_clients_state() +} + +pub fn main_check_clients_length(length: usize) -> Option { + if length != get_clients_length() { + Some(get_clients_state()) + } else { + None + } +} + +pub fn main_init(app_dir: String) { + initialize(&app_dir); +} + +pub fn main_device_id(id: String) { + *crate::common::DEVICE_ID.lock().unwrap() = id; +} + +pub fn main_device_name(name: String) { + *crate::common::DEVICE_NAME.lock().unwrap() = name; +} + +pub fn main_remove_peer(id: String) { + PeerConfig::remove(&id); +} + +// TODO +pub fn session_send_mouse(id: String, msg: String) { + if let Ok(m) = serde_json::from_str::>(&msg) { + let alt = m.get("alt").is_some(); + let ctrl = m.get("ctrl").is_some(); + let shift = m.get("shift").is_some(); + let command = m.get("command").is_some(); + let x = m + .get("x") + .map(|x| x.parse::().unwrap_or(0)) + .unwrap_or(0); + let y = m + .get("y") + .map(|x| x.parse::().unwrap_or(0)) + .unwrap_or(0); + let mut mask = 0; + if let Some(_type) = m.get("type") { + mask = match _type.as_str() { + "down" => 1, + "up" => 2, + "wheel" => 3, + _ => 0, + }; + } + if let Some(buttons) = m.get("buttons") { + mask |= match buttons.as_str() { + "left" => 1, + "right" => 2, + "wheel" => 4, + _ => 0, + } << 3; + } + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.send_mouse(mask, x, y, alt, ctrl, shift, command); + } + } +} + +pub fn session_restart_remote_device(id: String) { + // TODO + // Session::restart_remote_device(); +} + +pub fn main_set_home_dir(home: String) { + *config::APP_HOME_DIR.write().unwrap() = home; +} + +pub fn main_stop_service() { + #[cfg(target_os = "android")] + { + Config::set_option("stop-service".into(), "Y".into()); + crate::rendezvous_mediator::RendezvousMediator::restart(); + } +} + +pub fn main_start_service() { + #[cfg(target_os = "android")] + { + Config::set_option("stop-service".into(), "".into()); + crate::rendezvous_mediator::RendezvousMediator::restart(); + } + #[cfg(not(target_os = "android"))] + std::thread::spawn(move || start_server(true)); +} + +pub fn main_update_temporary_password() { + update_temporary_password(); +} + +pub fn main_set_permanent_password(password: String) { + set_permanent_password(password); +} + +pub fn server_send_chat(conn_id: i32, msg: String) { + connection_manager::send_chat(conn_id, msg); +} + +pub fn server_login_res(conn_id: i32, res: bool) { + connection_manager::on_login_res(conn_id, res); +} + +pub fn server_close_connection(conn_id: i32) { + connection_manager::close_conn(conn_id); +} + #[no_mangle] unsafe extern "C" fn translate(name: *const c_char, locale: *const c_char) -> *const c_char { let name = CStr::from_ptr(name); @@ -652,241 +754,6 @@ unsafe extern "C" fn translate(name: *const c_char, locale: *const c_char) -> *c CString::from_vec_unchecked(res.into_bytes()).into_raw() } -/// FFI for **get** commands which are idempotent. -/// Return result in c string. -/// -/// # Arguments -/// -/// * `name` - name of the command -/// * `arg` - argument of the command -#[no_mangle] -unsafe extern "C" fn get_by_name(name: *const c_char, arg: *const c_char) -> *const c_char { - let mut res = "".to_owned(); - let arg: &CStr = CStr::from_ptr(arg); - let name: &CStr = CStr::from_ptr(name); - if let Ok(name) = name.to_str() { - match name { - // "peers" => { - // if !config::APP_DIR.read().unwrap().is_empty() { - // let peers: Vec<(String, config::PeerInfoSerde)> = PeerConfig::peers() - // .drain(..) - // .map(|(id, _, p)| (id, p.info)) - // .collect(); - // res = serde_json::ser::to_string(&peers).unwrap_or("".to_owned()); - // } - // } - // "remote_id" => { - // if !config::APP_DIR.read().unwrap().is_empty() { - // res = LocalConfig::get_remote_id(); - // } - // } - // "test_if_valid_server" => { - // if let Ok(arg) = arg.to_str() { - // res = hbb_common::socket_client::test_if_valid_server(arg); - // } - // } - // "option" => { - // if let Ok(arg) = arg.to_str() { - // res = ui_interface::get_option(arg.to_owned()); - // } - // } - "software_update_url" => { - res = crate::common::SOFTWARE_UPDATE_URL.lock().unwrap().clone() - } - // File Action - "get_home_dir" => { - res = fs::get_home_as_string(); - } - // Server Side - "langs" => { - res = crate::lang::LANGS.to_string(); - } - "temporary_password" => { - res = ui_interface::temporary_password(); - } - "permanent_password" => { - res = ui_interface::permanent_password(); - } - "connect_statue" => { - res = ONLINE - .lock() - .unwrap() - .values() - .max() - .unwrap_or(&0) - .clone() - .to_string(); - } - #[cfg(not(any(target_os = "ios")))] - "clients_state" => { - res = get_clients_state(); - } - #[cfg(not(any(target_os = "ios")))] - "check_clients_length" => { - if let Ok(value) = arg.to_str() { - if value.parse::().unwrap_or(usize::MAX) != get_clients_length() { - res = get_clients_state() - } - } - } - _ => { - log::error!("Unknown name of get_by_name: {}", name); - } - } - } - CString::from_vec_unchecked(res.into_bytes()).into_raw() -} - -/// FFI for **set** commands which are not idempotent. -/// -/// # Arguments -/// -/// * `name` - name of the command -/// * `arg` - argument of the command -#[no_mangle] -unsafe extern "C" fn set_by_name(name: *const c_char, value: *const c_char) { - let value: &CStr = CStr::from_ptr(value); - if let Ok(value) = value.to_str() { - let name: &CStr = CStr::from_ptr(name); - if let Ok(name) = name.to_str() { - match name { - "init" => { - initialize(value); - } - "info1" => { - *crate::common::FLUTTER_INFO1.lock().unwrap() = value.to_owned(); - } - "info2" => { - *crate::common::FLUTTER_INFO2.lock().unwrap() = value.to_owned(); - } - "remove" => { - PeerConfig::remove(value); - } - - // TODO - "send_mouse" => { - if let Ok(m) = serde_json::from_str::>(value) { - let id = m.get("id"); - if id.is_none() { - return; - } - let id = id.unwrap(); - let alt = m.get("alt").is_some(); - let ctrl = m.get("ctrl").is_some(); - let shift = m.get("shift").is_some(); - let command = m.get("command").is_some(); - let x = m - .get("x") - .map(|x| x.parse::().unwrap_or(0)) - .unwrap_or(0); - let y = m - .get("y") - .map(|x| x.parse::().unwrap_or(0)) - .unwrap_or(0); - let mut mask = 0; - if let Some(_type) = m.get("type") { - mask = match _type.as_str() { - "down" => 1, - "up" => 2, - "wheel" => 3, - _ => 0, - }; - } - if let Some(buttons) = m.get("buttons") { - mask |= match buttons.as_str() { - "left" => 1, - "right" => 2, - "wheel" => 4, - _ => 0, - } << 3; - } - if let Some(session) = SESSIONS.read().unwrap().get(id) { - session.send_mouse(mask, x, y, alt, ctrl, shift, command); - } - } - } - // "option" => { - // if let Ok(m) = serde_json::from_str::>(value) { - // if let Some(name) = m.get("name") { - // if let Some(value) = m.get("value") { - // ui_interface::set_option(name.to_owned(), value.to_owned()); - // if name == "custom-rendezvous-server" { - // #[cfg(target_os = "android")] - // crate::rendezvous_mediator::RendezvousMediator::restart(); - // #[cfg(any( - // target_os = "android", - // target_os = "ios", - // feature = "cli" - // ))] - // crate::common::test_rendezvous_server(); - // } - // } - // } - // } - // } - "restart_remote_device" => { - // TODO - // Session::restart_remote_device(); - } - #[cfg(target_os = "android")] - "chat_server_mode" => { - if let Ok(m) = serde_json::from_str::>(value) { - if let (Some(Value::Number(id)), Some(Value::String(text))) = - (m.get("id"), m.get("text")) - { - let id = id.as_i64().unwrap_or(0); - connection_manager::send_chat(id as i32, text.to_owned()); - } - } - } - "home_dir" => { - *config::APP_HOME_DIR.write().unwrap() = value.to_owned(); - } - #[cfg(target_os = "android")] - "login_res" => { - if let Ok(m) = serde_json::from_str::>(value) { - if let (Some(Value::Number(id)), Some(Value::Bool(res))) = - (m.get("id"), m.get("res")) - { - let id = id.as_i64().unwrap_or(0); - connection_manager::on_login_res(id as i32, *res); - } - } - } - #[cfg(target_os = "android")] - "stop_service" => { - Config::set_option("stop-service".into(), "Y".into()); - crate::rendezvous_mediator::RendezvousMediator::restart(); - } - "start_service" => { - #[cfg(target_os = "android")] - { - Config::set_option("stop-service".into(), "".into()); - crate::rendezvous_mediator::RendezvousMediator::restart(); - } - #[cfg(not(target_os = "android"))] - std::thread::spawn(move || start_server(true)); - } - #[cfg(target_os = "android")] - "close_conn" => { - if let Ok(id) = value.parse::() { - connection_manager::close_conn(id); - }; - } - "temporary_password" => { - ui_interface::update_temporary_password(); - } - "permanent_password" => { - ui_interface::set_permanent_password(value.to_owned()); - } - _ => { - log::error!("Unknown name of set_by_name: {}", name); - } - } - } - } -} - fn handle_query_onlines(onlines: Vec, offlines: Vec) { if let Some(s) = flutter::GLOBAL_EVENT_STREAM .read() diff --git a/src/server/connection.rs b/src/server/connection.rs index 7d12dce45..346477851 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -5,7 +5,7 @@ use crate::clipboard_file::*; use crate::common::update_clipboard; use crate::video_service; #[cfg(any(target_os = "android", target_os = "ios"))] -use crate::{common::FLUTTER_INFO2, flutter::connection_manager::start_channel}; +use crate::{common::DEVICE_NAME, flutter::connection_manager::start_channel}; use crate::{ipc, VERSION}; use hbb_common::{ config::Config, @@ -643,7 +643,7 @@ impl Connection { } #[cfg(target_os = "android")] { - pi.hostname = FLUTTER_INFO2.lock().unwrap().clone(); + pi.hostname = DEVICE_NAME.lock().unwrap().clone(); pi.platform = "Android".into(); } #[cfg(feature = "hwcodec")] From 28b75fa9f7370a244cff707fdc0e5785c91a523c Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 9 Aug 2022 09:01:06 +0800 Subject: [PATCH 0168/2015] switch window, close subwindow Signed-off-by: 21pages --- flutter/lib/common.dart | 7 +++++++ flutter/lib/desktop/pages/connection_tab_page.dart | 5 +++++ flutter/lib/desktop/pages/desktop_home_page.dart | 8 ++++++++ flutter/lib/desktop/pages/file_manager_tab_page.dart | 5 +++++ flutter/lib/desktop/widgets/tabbar_widget.dart | 4 +++- flutter/lib/utils/multi_window_manager.dart | 3 +-- 6 files changed, 29 insertions(+), 3 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 861b6b645..ef53b2c41 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/instance_manager.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:window_manager/window_manager.dart'; import 'models/model.dart'; import 'models/platform_model.dart'; @@ -109,6 +110,12 @@ backToHome() { Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/")); } +void window_on_top() { + windowManager.restore(); + windowManager.show(); + windowManager.focus(); +} + typedef DialogBuilder = CustomAlertDialog Function( StateSetter setState, void Function([dynamic]) close); diff --git a/flutter/lib/desktop/pages/connection_tab_page.dart b/flutter/lib/desktop/pages/connection_tab_page.dart index f98c7d720..92a3938f5 100644 --- a/flutter/lib/desktop/pages/connection_tab_page.dart +++ b/flutter/lib/desktop/pages/connection_tab_page.dart @@ -8,6 +8,7 @@ import 'package:flutter_hbb/desktop/pages/remote_page.dart'; import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:get/get.dart'; +import 'package:window_manager/window_manager.dart'; import '../../models/model.dart'; @@ -47,6 +48,7 @@ class _ConnectionTabPageState extends State "call ${call.method} with args ${call.arguments} from window ${fromWindowId}"); // for simplify, just replace connectionId if (call.method == "new_remote_desktop") { + window_on_top(); final args = jsonDecode(call.arguments); final id = args['id']; final indexOf = connectionIds.indexOf(id); @@ -111,5 +113,8 @@ class _ConnectionTabPageState extends State initialIndex = max(0, initialIndex - 1); tabController.value = TabController( length: connectionIds.length, vsync: this, initialIndex: initialIndex); + if (connectionIds.length == 0) { + windowManager.close(); + } } } diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index f02e0fdfd..6854027ee 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -9,6 +9,7 @@ import 'package:flutter_hbb/desktop/pages/connection_page.dart'; import 'package:flutter_hbb/desktop/widgets/titlebar_widget.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/server_model.dart'; +import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -447,6 +448,13 @@ class _DesktopHomePageState extends State with TrayListener { void initState() { super.initState(); trayManager.addListener(this); + rustDeskWinManager.setMethodHandler((call, fromWindowId) async { + print( + "call ${call.method} with args ${call.arguments} from window ${fromWindowId}"); + if (call.method == "main_window_on_top") { + window_on_top(); + } + }); } @override diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index d06ed7444..c4888f37b 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -8,6 +8,7 @@ import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; import 'package:flutter_hbb/models/model.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:get/get.dart'; +import 'package:window_manager/window_manager.dart'; /// File Transfer for multi tabs class FileManagerTabPage extends StatefulWidget { @@ -44,6 +45,7 @@ class _FileManagerTabPageState extends State "call ${call.method} with args ${call.arguments} from window ${fromWindowId}"); // for simplify, just replace connectionId if (call.method == "new_file_transfer") { + window_on_top(); final args = jsonDecode(call.arguments); final id = args['id']; final indexOf = connectionIds.indexOf(id); @@ -111,5 +113,8 @@ class _FileManagerTabPageState extends State initialIndex = max(0, initialIndex - 1); tabController.value = TabController( length: connectionIds.length, initialIndex: initialIndex, vsync: this); + if (connectionIds.length == 0) { + windowManager.close(); + } } } diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index e57334be3..1f6d1863c 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -3,6 +3,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/consts.dart'; +import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:get/get.dart'; const Color _bgColor = Color.fromARGB(255, 231, 234, 237); @@ -184,7 +185,8 @@ class _AddButton extends StatelessWidget { return _Hoverable( onHover: (hover) => _hover.value = hover, onPressed: (pressed) => _pressed.value = pressed, - onTapUp: () => debugPrint('+'), // TODO + onTapUp: () => + rustDeskWinManager.call(WindowType.Main, "main_window_on_top", ""), child: Obx((() => Container( height: _kTabBarHeight, decoration: ShapeDecoration( diff --git a/flutter/lib/utils/multi_window_manager.dart b/flutter/lib/utils/multi_window_manager.dart index 979ebffd7..bea110dab 100644 --- a/flutter/lib/utils/multi_window_manager.dart +++ b/flutter/lib/utils/multi_window_manager.dart @@ -1,5 +1,4 @@ import 'dart:convert'; -import 'dart:ui'; import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/services.dart'; @@ -97,7 +96,7 @@ class RustDeskMultiWindowManager { int? findWindowByType(WindowType type) { switch (type) { case WindowType.Main: - break; + return 0; case WindowType.RemoteDesktop: return _remoteDesktopWindowId; case WindowType.FileTransfer: From 96cb8c3d9c2d315a22fbfaac724f6002f85f6803 Mon Sep 17 00:00:00 2001 From: fufesou Date: Mon, 8 Aug 2022 09:41:24 +0800 Subject: [PATCH 0169/2015] flutter_desktop: fix image scale quanlity Signed-off-by: fufesou --- flutter/lib/desktop/pages/remote_page.dart | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 2ad9dd53b..da5ad1455 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -277,8 +277,7 @@ class _RemotePageState extends State @override Widget build(BuildContext context) { super.build(context); - Provider.of(context, listen: false).tabBarHeight = - super.widget.tabBarHeight; + _ffi.canvasModel.tabBarHeight = super.widget.tabBarHeight; return WillPopScope( onWillPop: () async { clientClose(); @@ -882,11 +881,11 @@ class ImagePainter extends CustomPainter { if (image == null) 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 = new Paint(); - if (scale > 1.00001) { + paint.filterQuality = FilterQuality.medium; + if (scale > 10.00000) { paint.filterQuality = FilterQuality.high; - } else if (scale < 0.99999) { - paint.filterQuality = FilterQuality.medium; } canvas.drawImage(image!, new Offset(x, y), paint); } From e553756ad824140f770eb3ca8c441d486ae7c5bc Mon Sep 17 00:00:00 2001 From: fufesou Date: Mon, 8 Aug 2022 17:03:28 +0800 Subject: [PATCH 0170/2015] flutter_desktop: fix clipboard Signed-off-by: fufesou --- src/flutter.rs | 60 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 58 insertions(+), 2 deletions(-) diff --git a/src/flutter.rs b/src/flutter.rs index 6c2e66656..d83e37b4e 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -1,7 +1,7 @@ use std::{ collections::{HashMap, VecDeque}, sync::{ - atomic::{AtomicUsize, Ordering}, + atomic::{AtomicBool, AtomicUsize, Ordering}, Arc, Mutex, RwLock, }, }; @@ -31,7 +31,10 @@ use hbb_common::{ Stream, }; -use crate::common::make_fd_to_json; +use crate::common::{ + self, check_clipboard, make_fd_to_json, update_clipboard, ClipboardContext, CLIPBOARD_INTERVAL, +}; + use crate::{client::*, flutter_ffi::EventToUI, make_fd_flutter}; pub(super) const APP_TYPE_MAIN: &str = "main"; @@ -44,6 +47,9 @@ lazy_static::lazy_static! { pub static ref GLOBAL_EVENT_STREAM: RwLock>> = Default::default(); // rust to dart event channel } +static SERVER_CLIPBOARD_ENABLED: AtomicBool = AtomicBool::new(true); +static SERVER_KEYBOARD_ENABLED: AtomicBool = AtomicBool::new(true); + // pub fn get_session<'a>(id: &str) -> Option<&'a Session> { // SESSIONS.read().unwrap().get(id) // } @@ -657,6 +663,43 @@ struct Connection { } impl Connection { + fn start_clipboard( + tx_protobuf: mpsc::UnboundedSender, + lc: Arc>, + ) -> Option> { + let (tx, rx) = std::sync::mpsc::channel(); + match ClipboardContext::new() { + Ok(mut ctx) => { + let old_clipboard: Arc> = Default::default(); + // 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 + { + 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) + } + /// Create a new connection. /// /// # Arguments @@ -667,6 +710,10 @@ impl Connection { async fn start(session: Session, is_file_transfer: bool) { let mut last_recv_time = Instant::now(); let (sender, mut receiver) = mpsc::unbounded_channel::(); + let mut stop_clipboard = None; + if !is_file_transfer { + stop_clipboard = Self::start_clipboard(sender.clone(), session.lc.clone()); + } *session.sender.write().unwrap() = Some(sender); let conn_type = if is_file_transfer { session.lc.write().unwrap().is_file_transfer = true; @@ -695,6 +742,9 @@ impl Connection { match Client::start(&session.id, &key, &token, conn_type).await { Ok((mut peer, direct)) => { + SERVER_KEYBOARD_ENABLED.store(true, Ordering::SeqCst); + SERVER_CLIPBOARD_ENABLED.store(true, Ordering::SeqCst); + session.push_event( "connection_ready", vec![ @@ -774,6 +824,12 @@ impl Connection { session.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); } /// Handle message from peer. From b2ffe9dee4a7e8385efe93fc87532f04492e5165 Mon Sep 17 00:00:00 2001 From: fufesou Date: Mon, 8 Aug 2022 22:00:01 +0800 Subject: [PATCH 0171/2015] flutter_desktop: handle privacy mode back notifications Signed-off-by: fufesou --- flutter/lib/common.dart | 28 +++- flutter/lib/desktop/pages/remote_page.dart | 14 +- flutter/lib/models/model.dart | 17 +++ flutter/lib/utils/multi_window_manager.dart | 1 + src/client.rs | 7 + src/common.rs | 13 ++ src/flutter.rs | 134 +++++++++++++++++++- src/ui/remote.rs | 34 +---- 8 files changed, 210 insertions(+), 38 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index ef53b2c41..1a0d59e16 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -194,14 +194,20 @@ void msgBox(String type, String title, String text, {bool? hasCancel}) { style: TextStyle(color: MyTheme.accent)))); SmartDialog.dismiss(); - final buttons = [ - wrap(Translator.call('OK'), () { - SmartDialog.dismiss(); - backToHome(); - }) - ]; + List buttons = []; + if (type != "connecting" && type != "success" && type.indexOf("nook") < 0) { + buttons.insert( + 0, + wrap(Translator.call('OK'), () { + SmartDialog.dismiss(); + backToHome(); + })); + } if (hasCancel == null) { - hasCancel = type != 'error'; + // hasCancel = type != 'error'; + hasCancel = type.indexOf("error") < 0 && + type.indexOf("nocancel") < 0 && + type != "restarting"; } if (hasCancel) { buttons.insert( @@ -210,6 +216,14 @@ void msgBox(String type, String title, String text, {bool? hasCancel}) { SmartDialog.dismiss(); })); } + // TODO: test this button + if (type.indexOf("hasclose") >= 0) { + buttons.insert( + 0, + wrap(Translator.call('Close'), () { + SmartDialog.dismiss(); + })); + } DialogManager.show((setState, close) => CustomAlertDialog( title: Text(translate(title), style: TextStyle(fontSize: 21)), content: Text(Translator.call(text), style: TextStyle(fontSize: 15)), diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index da5ad1455..da7a317a8 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -604,8 +604,12 @@ class _RemotePageState extends State await bind.getSessionToggleOption(id: id, arg: 'privacy-mode') != true) { more.add(PopupMenuItem( - child: Text(translate((_ffi.ffiModel.inputBlocked ? 'Unb' : 'B') + - 'lock user input')), + child: Consumer( + builder: (_context, ffiModel, _child) => () { + return Text(translate( + (ffiModel.inputBlocked ? 'Unb' : 'B') + + 'lock user input')); + }()), value: 'block-input')); } } @@ -951,7 +955,11 @@ void showOptions(String id) async { more.add(getToggle( id, setState, 'lock-after-session-end', 'Lock after session end')); if (pi.platform == 'Windows') { - more.add(getToggle(id, setState, 'privacy-mode', 'Privacy mode')); + more.add(Consumer( + builder: (_context, _ffiModel, _child) => () { + return getToggle( + id, setState, 'privacy-mode', 'Privacy mode'); + }())); } } var setQuality = (String? value) { diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index c7295f57e..4f295e377 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -173,6 +173,10 @@ class FfiModel with ChangeNotifier { parent.target?.serverModel.onClientRemove(evt); } else if (name == 'update_quality_status') { parent.target?.qualityMonitorModel.updateQualityStatus(evt); + } else if (name == 'update_block_input_state') { + updateBlockInputState(evt); + } else if (name == 'update_privacy_mode') { + updatePrivacyMode(evt); } }; } @@ -228,6 +232,10 @@ class FfiModel with ChangeNotifier { parent.target?.serverModel.onClientRemove(evt); } else if (name == 'update_quality_status') { parent.target?.qualityMonitorModel.updateQualityStatus(evt); + } else if (name == 'update_block_input_state') { + updateBlockInputState(evt); + } else if (name == 'update_privacy_mode') { + updatePrivacyMode(evt); } }; platformFFI.setEventCallback(cb); @@ -331,6 +339,15 @@ class FfiModel with ChangeNotifier { } notifyListeners(); } + + updateBlockInputState(Map evt) { + _inputBlocked = evt['input_state'] == 'on'; + notifyListeners(); + } + + updatePrivacyMode(Map evt) { + notifyListeners(); + } } class ImageModel with ChangeNotifier { diff --git a/flutter/lib/utils/multi_window_manager.dart b/flutter/lib/utils/multi_window_manager.dart index bea110dab..4da0dca7f 100644 --- a/flutter/lib/utils/multi_window_manager.dart +++ b/flutter/lib/utils/multi_window_manager.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:ui'; import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/services.dart'; diff --git a/src/client.rs b/src/client.rs index 89d66c6ca..3c1e5c3c3 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1004,6 +1004,13 @@ impl LoginConfigHandler { Some(msg_out) } + /// Get [`PeerConfig`] of the current [`LoginConfigHandler`]. + /// + /// # Arguments + pub fn get_config(&mut self) -> &mut PeerConfig { + &mut self.config + } + /// Get [`OptionMessage`] of the current [`LoginConfigHandler`]. /// Return `None` if there's no option, for example, when the session is only for file transfer. /// diff --git a/src/common.rs b/src/common.rs index 605435956..5c387c07e 100644 --- a/src/common.rs +++ b/src/common.rs @@ -104,6 +104,19 @@ pub fn update_clipboard(clipboard: Clipboard, old: Option<&Arc>>) } } +pub async fn send_opts_after_login( + config: &crate::client::LoginConfigHandler, + peer: &mut hbb_common::tcp::FramedStream, +) { + if let Some(opts) = config.get_option_message_after_login() { + let mut misc = Misc::new(); + misc.set_option(opts); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + allow_err!(peer.send(&msg_out).await); + } +} + #[cfg(feature = "use_rubato")] pub fn resample_channels( data: &[f32], diff --git a/src/flutter.rs b/src/flutter.rs index d83e37b4e..bb8881c58 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -76,7 +76,7 @@ impl Session { // TODO close // Self::close(); let events2ui = Arc::new(RwLock::new(events2ui)); - let mut session = Session { + let session = Session { id: session_id.clone(), sender: Default::default(), lc: Default::default(), @@ -663,6 +663,8 @@ struct Connection { } impl Connection { + // TODO: Similar to remote::start_clipboard + // merge the code fn start_clipboard( tx_protobuf: mpsc::UnboundedSender, lc: Arc>, @@ -842,6 +844,7 @@ impl Connection { Some(message::Union::VideoFrame(vf)) => { if !self.first_frame { self.first_frame = true; + common::send_opts_after_login(&self.session.lc.read().unwrap(), peer).await; } let incomming_format = CodecFormat::from(&vf); if self.video_format != incomming_format { @@ -1083,6 +1086,11 @@ impl Connection { self.session.msgbox("error", "Connection Error", &c); return false; } + Some(misc::Union::BackNotification(notification)) => { + if !self.handle_back_notification(notification).await { + return false; + } + } _ => {} }, Some(message::Union::TestDelay(t)) => { @@ -1107,6 +1115,130 @@ impl Connection { true } + async fn handle_back_notification(&mut self, notification: BackNotification) -> bool { + match notification.union { + Some(back_notification::Union::BlockInputState(state)) => { + self.handle_back_msg_block_input( + state.enum_value_or(back_notification::BlockInputState::BlkStateUnknown), + ) + .await; + } + Some(back_notification::Union::PrivacyModeState(state)) => { + if !self + .handle_back_msg_privacy_mode( + state.enum_value_or(back_notification::PrivacyModeState::PrvStateUnknown), + ) + .await + { + return false; + } + } + _ => {} + } + true + } + + #[inline(always)] + fn update_block_input_state(&mut self, on: bool) { + self.session.push_event( + "update_block_input_state", + [("input_state", if on { "on" } else { "off" })].into(), + ); + } + + async fn handle_back_msg_block_input(&mut self, state: back_notification::BlockInputState) { + match state { + back_notification::BlockInputState::BlkOnSucceeded => { + self.update_block_input_state(true); + } + back_notification::BlockInputState::BlkOnFailed => { + self.session + .msgbox("custom-error", "Block user input", "Failed"); + self.update_block_input_state(false); + } + back_notification::BlockInputState::BlkOffSucceeded => { + self.update_block_input_state(false); + } + back_notification::BlockInputState::BlkOffFailed => { + self.session + .msgbox("custom-error", "Unblock user input", "Failed"); + } + _ => {} + } + } + + #[inline(always)] + fn update_privacy_mode(&mut self, on: bool) { + let mut config = self.session.load_config(); + config.privacy_mode = on; + self.session.save_config(&config); + self.session.lc.write().unwrap().get_config().privacy_mode = on; + self.session.push_event("update_privacy_mode", [].into()); + } + + async fn handle_back_msg_privacy_mode( + &mut self, + state: back_notification::PrivacyModeState, + ) -> bool { + match state { + back_notification::PrivacyModeState::PrvOnByOther => { + self.session.msgbox( + "error", + "Connecting...", + "Someone turns on privacy mode, exit", + ); + return false; + } + back_notification::PrivacyModeState::PrvNotSupported => { + self.session + .msgbox("custom-error", "Privacy mode", "Unsupported"); + self.update_privacy_mode(false); + } + back_notification::PrivacyModeState::PrvOnSucceeded => { + self.session + .msgbox("custom-nocancel", "Privacy mode", "In privacy mode"); + self.update_privacy_mode(true); + } + back_notification::PrivacyModeState::PrvOnFailedDenied => { + self.session + .msgbox("custom-error", "Privacy mode", "Peer denied"); + self.update_privacy_mode(false); + } + back_notification::PrivacyModeState::PrvOnFailedPlugin => { + self.session + .msgbox("custom-error", "Privacy mode", "Please install plugins"); + self.update_privacy_mode(false); + } + back_notification::PrivacyModeState::PrvOnFailed => { + self.session + .msgbox("custom-error", "Privacy mode", "Failed"); + self.update_privacy_mode(false); + } + back_notification::PrivacyModeState::PrvOffSucceeded => { + self.session + .msgbox("custom-nocancel", "Privacy mode", "Out privacy mode"); + self.update_privacy_mode(false); + } + back_notification::PrivacyModeState::PrvOffByPeer => { + self.session + .msgbox("custom-error", "Privacy mode", "Peer exit"); + self.update_privacy_mode(false); + } + back_notification::PrivacyModeState::PrvOffFailed => { + self.session + .msgbox("custom-error", "Privacy mode", "Failed to turn off"); + } + back_notification::PrivacyModeState::PrvOffUnknown => { + self.session + .msgbox("custom-error", "Privacy mode", "Turned off"); + // log::error!("Privacy mode is turned off with unknown reason"); + self.update_privacy_mode(false); + } + _ => {} + } + true + } + async fn handle_msg_from_ui(&mut self, data: Data, peer: &mut Stream) -> bool { match data { Data::Close => { diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 5d036dee2..060aa59db 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -25,8 +25,12 @@ use clipboard::{ use enigo::{self, Enigo, KeyboardControllable}; use hbb_common::{ allow_err, - config::{Config, LocalConfig, PeerConfig}, - fs, log, + config::{Config, LocalConfig, PeerConfig, TransferSerde}, + fs::{ + self, can_enable_overwrite_detection, get_job, get_string, new_send_confirm, + DigestCheckResult, RemoveJobMeta, TransferJobMeta, + }, + get_version_number, log, message_proto::{permission_info::Permission, *}, protobuf::Message as _, rendezvous_proto::ConnType, @@ -38,14 +42,6 @@ use hbb_common::{ }, Stream, }; -use hbb_common::{config::TransferSerde, fs::TransferJobMeta}; -use hbb_common::{ - fs::{ - can_enable_overwrite_detection, get_job, get_string, new_send_confirm, DigestCheckResult, - RemoveJobMeta, - }, - get_version_number, -}; #[cfg(windows)] use crate::clipboard_file::*; @@ -2071,22 +2067,6 @@ impl Remote { true } - async fn send_opts_after_login(&self, peer: &mut Stream) { - if let Some(opts) = self - .handler - .lc - .read() - .unwrap() - .get_option_message_after_login() - { - let mut misc = Misc::new(); - misc.set_option(opts); - let mut msg_out = Message::new(); - msg_out.set_misc(misc); - allow_err!(peer.send(&msg_out).await); - } - } - async fn handle_msg_from_peer(&mut self, data: &[u8], peer: &mut Stream) -> bool { if let Ok(msg_in) = Message::parse_from_bytes(&data) { match msg_in.union { @@ -2095,7 +2075,7 @@ impl Remote { self.first_frame = true; self.handler.call2("closeSuccess", &make_args!()); self.handler.call("adaptSize", &make_args!()); - self.send_opts_after_login(peer).await; + common::send_opts_after_login(&self.handler.lc.read().unwrap(), peer).await; } let incomming_format = CodecFormat::from(&vf); if self.video_format != incomming_format { From 4963b519204849bba39a368ca642e6efd303c16a Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 9 Aug 2022 11:05:36 +0800 Subject: [PATCH 0172/2015] fix ci build error warn unused, but needed. Signed-off-by: 21pages --- flutter/lib/utils/multi_window_manager.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/flutter/lib/utils/multi_window_manager.dart b/flutter/lib/utils/multi_window_manager.dart index bea110dab..4da0dca7f 100644 --- a/flutter/lib/utils/multi_window_manager.dart +++ b/flutter/lib/utils/multi_window_manager.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:ui'; import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/services.dart'; From 5a953cc8df41d975e3510f24fc9cbd770a96f0ba Mon Sep 17 00:00:00 2001 From: kingtous Date: Tue, 9 Aug 2022 13:39:30 +0800 Subject: [PATCH 0173/2015] fix: multi window close issue --- .../lib/desktop/pages/desktop_home_page.dart | 23 +++++++++++++++- flutter/lib/main.dart | 3 +++ flutter/lib/utils/multi_window_manager.dart | 26 +++++++++++++++++++ 3 files changed, 51 insertions(+), 1 deletion(-) diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 6854027ee..a8f2e51af 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/material.dart' hide MenuItem; import 'package:flutter/services.dart'; import 'package:flutter_hbb/common.dart'; @@ -15,6 +16,7 @@ import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:tray_manager/tray_manager.dart'; import 'package:url_launcher/url_launcher_string.dart'; +import 'package:window_manager/window_manager.dart'; class DesktopHomePage extends StatefulWidget { DesktopHomePage({Key? key}) : super(key: key); @@ -25,7 +27,24 @@ class DesktopHomePage extends StatefulWidget { const borderColor = Color(0xFF2F65BA); -class _DesktopHomePageState extends State with TrayListener { +class _DesktopHomePageState extends State with TrayListener, WindowListener { + + @override + void onWindowClose() async { + super.onWindowClose(); + // close all sub windows + if (await windowManager.isPreventClose()) { + try { + await rustDeskWinManager.closeAllSubWindows(); + } catch (err) { + debugPrint("$err"); + } finally { + await windowManager.setPreventClose(false); + await windowManager.close(); + } + } + } + @override Widget build(BuildContext context) { return Scaffold( @@ -448,6 +467,7 @@ class _DesktopHomePageState extends State with TrayListener { void initState() { super.initState(); trayManager.addListener(this); + windowManager.addListener(this); rustDeskWinManager.setMethodHandler((call, fromWindowId) async { print( "call ${call.method} with args ${call.arguments} from window ${fromWindowId}"); @@ -460,6 +480,7 @@ class _DesktopHomePageState extends State with TrayListener { @override void dispose() { trayManager.removeListener(this); + windowManager.removeListener(this); super.dispose(); } diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index bceb8fa8a..b11ccb628 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -9,6 +9,7 @@ import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:get/route_manager.dart'; import 'package:provider/provider.dart'; +import 'package:window_manager/window_manager.dart'; // import 'package:window_manager/window_manager.dart'; @@ -47,6 +48,8 @@ Future main(List args) async { break; } } else { + await windowManager.ensureInitialized(); + windowManager.setPreventClose(true); runMainApp(true); } } diff --git a/flutter/lib/utils/multi_window_manager.dart b/flutter/lib/utils/multi_window_manager.dart index 4da0dca7f..9b26870c0 100644 --- a/flutter/lib/utils/multi_window_manager.dart +++ b/flutter/lib/utils/multi_window_manager.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'dart:ui'; import 'package:desktop_multi_window/desktop_multi_window.dart'; +import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; /// must keep the order @@ -114,6 +115,31 @@ class RustDeskMultiWindowManager { Future Function(MethodCall call, int fromWindowId)? handler) { DesktopMultiWindow.setMethodHandler(handler); } + + Future closeAllSubWindows() async { + await Future.wait(WindowType.values.map((e) => closeWindows(e))); + } + + Future closeWindows(WindowType type) async { + if (type == WindowType.Main) { + // skip main window, use window manager instead + return; + } + int? wId = findWindowByType(type); + if (wId != null) { + debugPrint("closing multi window: ${type.toString()}"); + try { + final ids = await DesktopMultiWindow.getAllSubWindowIds(); + if (!ids.contains(wId)) { + // no such window already + return; + } + await WindowController.fromWindowId(wId).close(); + } on Error { + return; + } + } + } } final rustDeskWinManager = RustDeskMultiWindowManager.instance; From fa8514aefeb7a8ac5bc6b70803907a9025c03368 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 9 Aug 2022 13:50:26 +0800 Subject: [PATCH 0174/2015] fix: currentTheme Signed-off-by: Kingtous --- flutter/lib/main.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index b11ccb628..168d9e1e3 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -15,10 +15,10 @@ import 'package:window_manager/window_manager.dart'; import 'common.dart'; import 'consts.dart'; -import 'models/platform_model.dart'; import 'mobile/pages/home_page.dart'; import 'mobile/pages/server_page.dart'; import 'mobile/pages/settings_page.dart'; +import 'models/platform_model.dart'; int? windowId; @@ -55,7 +55,7 @@ Future main(List args) async { } ThemeData getCurrentTheme() { - return isDarkTheme() ? MyTheme.darkTheme : MyTheme.darkTheme; + return isDarkTheme() ? MyTheme.darkTheme : MyTheme.lightTheme; } Future initEnv(String appType) async { From d76782a0fcf551a9e1f3433397ec43f7fd8a6c31 Mon Sep 17 00:00:00 2001 From: kingtous Date: Tue, 9 Aug 2022 16:37:11 +0800 Subject: [PATCH 0175/2015] fix: use multi window controller to close window --- flutter/lib/desktop/pages/connection_tab_page.dart | 7 ++++++- flutter/lib/desktop/pages/file_manager_tab_page.dart | 7 ++++++- flutter/lib/main.dart | 1 + 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/flutter/lib/desktop/pages/connection_tab_page.dart b/flutter/lib/desktop/pages/connection_tab_page.dart index 92a3938f5..8de2d84d0 100644 --- a/flutter/lib/desktop/pages/connection_tab_page.dart +++ b/flutter/lib/desktop/pages/connection_tab_page.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:math'; +import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/consts.dart'; @@ -114,7 +115,11 @@ class _ConnectionTabPageState extends State tabController.value = TabController( length: connectionIds.length, vsync: this, initialIndex: initialIndex); if (connectionIds.length == 0) { - windowManager.close(); + WindowController.fromWindowId(windowId()).close(); } } + + int windowId() { + return widget.params["windowId"]; + } } diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index c4888f37b..723975d62 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:math'; +import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/desktop/pages/file_manager_page.dart'; @@ -114,7 +115,11 @@ class _FileManagerTabPageState extends State tabController.value = TabController( length: connectionIds.length, initialIndex: initialIndex, vsync: this); if (connectionIds.length == 0) { - windowManager.close(); + WindowController.fromWindowId(windowId()).close(); } } + + int windowId() { + return widget.params["windowId"]; + } } diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index b11ccb628..932da4f30 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -36,6 +36,7 @@ Future main(List args) async { ? Map() : jsonDecode(args[2]) as Map; int type = argument['type'] ?? -1; + argument['windowId'] = windowId; WindowType wType = type.windowType; switch (wType) { case WindowType.RemoteDesktop: From a10020d1f1b1790fe530cefce8eaead6e19fd37a Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 9 Aug 2022 18:03:33 +0800 Subject: [PATCH 0176/2015] fix: fix window manager re-register issue Signed-off-by: Kingtous --- flutter/pubspec.lock | 12 +++++++----- flutter/pubspec.yaml | 7 +++++-- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 7133ae132..96458745c 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -236,8 +236,8 @@ packages: dependency: "direct main" description: path: "." - ref: "7cd2d885e58397766f3f03a1e632299944580aac" - resolved-ref: "7cd2d885e58397766f3f03a1e632299944580aac" + ref: ce9e333d822fe6cbf91c8634bae023bf78700d94 + resolved-ref: ce9e333d822fe6cbf91c8634bae023bf78700d94 url: "https://github.com/Kingtous/rustdesk_desktop_multi_window" source: git version: "0.1.0" @@ -1211,9 +1211,11 @@ packages: window_manager: dependency: "direct main" description: - name: window_manager - url: "https://pub.dartlang.org" - source: hosted + path: "." + ref: "1871cf2" + resolved-ref: "1871cf2857925d28db64b2151bc10b8dac714846" + url: "https://github.com/Kingtous/rustdesk_window_manager" + source: git version: "0.2.5" xdg_directories: dependency: transitive diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 591b59d29..ba400a102 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -55,11 +55,14 @@ dependencies: url: https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge ref: master path: frb_dart - window_manager: ^0.2.5 + window_manager: + git: + url: https://github.com/Kingtous/rustdesk_window_manager + ref: 1871cf2 desktop_multi_window: git: url: https://github.com/Kingtous/rustdesk_desktop_multi_window - ref: 7cd2d885e58397766f3f03a1e632299944580aac + ref: ce9e333d822fe6cbf91c8634bae023bf78700d94 # bitsdojo_window: ^0.1.2 freezed_annotation: ^2.0.3 tray_manager: 0.1.7 From 8a113caf2e64784292096f0063d0d12891844f95 Mon Sep 17 00:00:00 2001 From: kingtous Date: Tue, 9 Aug 2022 18:12:47 +0800 Subject: [PATCH 0177/2015] update: deps --- flutter/pubspec.lock | 366 +++++++++++++++++++++---------------------- flutter/pubspec.yaml | 6 +- 2 files changed, 186 insertions(+), 186 deletions(-) diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 96458745c..2c402951f 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -5,231 +5,231 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "44.0.0" after_layout: dependency: transitive description: name: after_layout - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.0" analyzer: dependency: transitive description: name: analyzer - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "4.4.0" animations: dependency: transitive description: name: animations - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.3" archive: dependency: transitive description: name: archive - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.3.1" args: dependency: transitive description: name: args - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.3.1" async: dependency: transitive description: name: async - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.8.2" boolean_selector: dependency: transitive description: name: boolean_selector - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0" build: dependency: transitive description: name: build - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.3.0" build_config: dependency: transitive description: name: build_config - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.0" build_daemon: dependency: transitive description: name: build_daemon - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.0" build_resolvers: dependency: transitive description: name: build_resolvers - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.9" build_runner: dependency: "direct dev" description: name: build_runner - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.0" build_runner_core: dependency: transitive description: name: build_runner_core - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "7.2.3" built_collection: dependency: transitive description: name: built_collection - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "5.1.1" built_value: dependency: transitive description: name: built_value - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "8.4.0" cached_network_image: dependency: transitive description: name: cached_network_image - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.2.1" cached_network_image_platform_interface: dependency: transitive description: name: cached_network_image_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.0" cached_network_image_web: dependency: transitive description: name: cached_network_image_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.1" characters: dependency: transitive description: name: characters - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.0" charcode: dependency: transitive description: name: charcode - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.1" checked_yaml: dependency: transitive description: name: checked_yaml - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.1" clock: dependency: transitive description: name: clock - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.0" code_builder: dependency: transitive description: name: code_builder - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "4.2.0" collection: dependency: transitive description: name: collection - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.16.0" contextmenu: dependency: "direct main" description: name: contextmenu - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.0" convert: dependency: transitive description: name: convert - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.2" cross_file: dependency: transitive description: name: cross_file - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.3.3+1" crypto: dependency: transitive description: name: crypto - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.2" csslib: dependency: transitive description: name: csslib - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.17.2" cupertino_icons: dependency: "direct main" description: name: cupertino_icons - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.5" dart_style: dependency: transitive description: name: dart_style - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.3" dash_chat_2: dependency: "direct main" description: name: dash_chat_2 - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.0.12" desktop_multi_window: @@ -245,133 +245,133 @@ packages: dependency: "direct main" description: name: device_info_plus - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "3.2.4" + version: "4.0.2" device_info_plus_linux: dependency: transitive description: name: device_info_plus_linux - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.1" device_info_plus_macos: dependency: transitive description: name: device_info_plus_macos - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.3" device_info_plus_platform_interface: dependency: transitive description: name: device_info_plus_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "2.3.0+1" + version: "2.4.0" device_info_plus_web: dependency: transitive description: name: device_info_plus_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0" device_info_plus_windows: dependency: transitive description: name: device_info_plus_windows - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "2.1.1" + version: "3.0.2" draggable_float_widget: dependency: "direct main" description: name: draggable_float_widget - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.0.2" event_bus: dependency: transitive description: name: event_bus - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.0" external_path: dependency: "direct main" description: name: external_path - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.1" fake_async: dependency: transitive description: name: fake_async - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.0" ffi: dependency: "direct main" description: name: ffi - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "1.2.1" + version: "2.0.1" file: dependency: transitive description: name: file - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "6.1.2" firebase_analytics: dependency: "direct main" description: name: firebase_analytics - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "9.3.0" firebase_analytics_platform_interface: dependency: transitive description: name: firebase_analytics_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.3.0" firebase_analytics_web: dependency: transitive description: name: firebase_analytics_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.4.2" firebase_core: dependency: transitive description: name: firebase_core - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.20.0" firebase_core_platform_interface: dependency: transitive description: name: firebase_core_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "4.5.0" firebase_core_web: dependency: transitive description: name: firebase_core_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.7.1" fixnum: dependency: transitive description: name: fixnum - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.1" flutter: @@ -383,42 +383,42 @@ packages: dependency: transitive description: name: flutter_blurhash - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.7.0" flutter_breadcrumb: dependency: "direct main" description: name: flutter_breadcrumb - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.1" flutter_cache_manager: dependency: transitive description: name: flutter_cache_manager - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.3.0" flutter_launcher_icons: dependency: "direct dev" description: name: flutter_launcher_icons - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.9.3" flutter_parsed_text: dependency: transitive description: name: flutter_parsed_text - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.1" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.7" flutter_rust_bridge: @@ -434,7 +434,7 @@ packages: dependency: "direct main" description: name: flutter_smart_dialog - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "4.5.4+1" flutter_test: @@ -451,476 +451,476 @@ packages: dependency: "direct dev" description: name: freezed - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0+1" freezed_annotation: dependency: "direct main" description: name: freezed_annotation - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0" frontend_server_client: dependency: transitive description: name: frontend_server_client - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.3" get: dependency: "direct main" description: name: get - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "4.6.5" glob: dependency: transitive description: name: glob - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0" graphs: dependency: transitive description: name: graphs - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0" html: dependency: transitive description: name: html - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.15.0" http: dependency: "direct main" description: name: http - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.13.5" http_multi_server: dependency: transitive description: name: http_multi_server - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.2.1" http_parser: dependency: transitive description: name: http_parser - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "4.0.1" image: dependency: "direct main" description: name: image - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.2.0" image_picker: dependency: "direct main" description: name: image_picker - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.8.5+3" image_picker_android: dependency: transitive description: name: image_picker_android - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.8.5+2" image_picker_for_web: dependency: transitive description: name: image_picker_for_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.8" image_picker_ios: dependency: transitive description: name: image_picker_ios - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.8.5+6" image_picker_platform_interface: dependency: transitive description: name: image_picker_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.6.1" intl: dependency: transitive description: name: intl - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.17.0" io: dependency: transitive description: name: io - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.3" js: dependency: transitive description: name: js - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.6.4" json_annotation: dependency: transitive description: name: json_annotation - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "4.6.0" logging: dependency: transitive description: name: logging - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.2" matcher: dependency: transitive description: name: matcher - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.12.11" material_color_utilities: dependency: transitive description: name: material_color_utilities - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.1.4" menu_base: dependency: transitive description: name: menu_base - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.1.1" meta: dependency: transitive description: name: meta - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.7.0" mime: dependency: transitive description: name: mime - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.2" nested: dependency: transitive description: name: nested - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.0" octo_image: dependency: transitive description: name: octo_image - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.2" package_config: dependency: transitive description: name: package_config - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0" package_info_plus: dependency: "direct main" description: name: package_info_plus - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "1.4.2" + version: "1.4.3" package_info_plus_linux: dependency: transitive description: name: package_info_plus_linux - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.5" package_info_plus_macos: dependency: transitive description: name: package_info_plus_macos - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.0" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.2" package_info_plus_web: dependency: transitive description: name: package_info_plus_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.5" package_info_plus_windows: dependency: transitive description: name: package_info_plus_windows - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "1.0.5" + version: "2.0.0" path: dependency: transitive description: name: path - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.8.1" path_provider: dependency: "direct main" description: name: path_provider - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.11" path_provider_android: dependency: transitive description: name: path_provider_android - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.17" path_provider_ios: dependency: transitive description: name: path_provider_ios - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.11" path_provider_linux: dependency: transitive description: name: path_provider_linux - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.7" path_provider_macos: dependency: transitive description: name: path_provider_macos - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.6" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.4" path_provider_windows: dependency: transitive description: name: path_provider_windows - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "2.0.7" + version: "2.1.2" pedantic: dependency: transitive description: name: pedantic - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.11.1" petitparser: dependency: transitive description: name: petitparser - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "5.0.0" platform: dependency: transitive description: name: platform - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.0" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.2" pool: dependency: transitive description: name: pool - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.5.1" process: dependency: transitive description: name: process - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "4.2.4" provider: dependency: "direct main" description: name: provider - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "6.0.3" pub_semver: dependency: transitive description: name: pub_semver - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.1" pubspec_parse: dependency: transitive description: name: pubspec_parse - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.0" qr_code_scanner: dependency: "direct main" description: name: qr_code_scanner - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.0" quiver: dependency: transitive description: name: quiver - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.0" rxdart: dependency: transitive description: name: rxdart - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.27.5" screen_retriever: dependency: transitive description: name: screen_retriever - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.1.2" settings_ui: dependency: "direct main" description: name: settings_ui - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.2" shared_preferences: dependency: "direct main" description: name: shared_preferences - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.15" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.12" shared_preferences_ios: dependency: transitive description: name: shared_preferences_ios - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.1" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.1" shared_preferences_macos: dependency: transitive description: name: shared_preferences_macos - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.4" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.0" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.4" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.1" shelf: dependency: transitive description: name: shelf - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.2" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.2" shortid: dependency: transitive description: name: shortid - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.1.2" sky_engine: @@ -932,288 +932,288 @@ packages: dependency: transitive description: name: source_gen - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.2" source_span: dependency: transitive description: name: source_span - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.8.2" sqflite: dependency: transitive description: name: sqflite - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.3+1" sqflite_common: dependency: transitive description: name: sqflite_common - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.1+1" stack_trace: dependency: transitive description: name: stack_trace - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.10.0" stream_channel: dependency: transitive description: name: stream_channel - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0" stream_transform: dependency: transitive description: name: stream_transform - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.0" string_scanner: dependency: transitive description: name: string_scanner - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.0" synchronized: dependency: transitive description: name: synchronized - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.0+2" term_glyph: dependency: transitive description: name: term_glyph - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.0" test_api: dependency: transitive description: name: test_api - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.4.9" timing: dependency: transitive description: name: timing - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.0" toggle_switch: dependency: "direct main" description: name: toggle_switch - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.4.0" tray_manager: dependency: "direct main" description: name: tray_manager - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.1.7" tuple: dependency: "direct main" description: name: tuple - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.0" typed_data: dependency: transitive description: name: typed_data - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.1" url_launcher: dependency: "direct main" description: name: url_launcher - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "6.1.5" url_launcher_android: dependency: transitive description: name: url_launcher_android - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "6.0.17" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "6.0.17" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.1" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0" url_launcher_web: dependency: transitive description: name: url_launcher_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.13" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.1" uuid: dependency: transitive description: name: uuid - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.6" vector_math: dependency: transitive description: name: vector_math - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.2" video_player: dependency: transitive description: name: video_player - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "2.4.5" + version: "2.4.6" video_player_android: dependency: transitive description: name: video_player_android - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.3.8" video_player_avfoundation: dependency: transitive description: name: video_player_avfoundation - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.3.5" video_player_platform_interface: dependency: transitive description: name: video_player_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "5.1.3" + version: "5.1.4" video_player_web: dependency: transitive description: name: video_player_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.12" visibility_detector: dependency: "direct main" description: name: visibility_detector - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.3.3" wakelock: dependency: "direct main" description: name: wakelock - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.5.6" wakelock_macos: dependency: transitive description: name: wakelock_macos - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.4.0" wakelock_platform_interface: dependency: transitive description: name: wakelock_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.3.0" wakelock_web: dependency: transitive description: name: wakelock_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.4.0" wakelock_windows: dependency: transitive description: name: wakelock_windows - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.2.0" watcher: dependency: transitive description: name: watcher - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.1" web_socket_channel: dependency: transitive description: name: web_socket_channel - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.0" win32: dependency: transitive description: name: win32 - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "2.6.1" + version: "2.7.0" window_manager: dependency: "direct main" description: path: "." - ref: "1871cf2" - resolved-ref: "1871cf2857925d28db64b2151bc10b8dac714846" + ref: "028a7f6" + resolved-ref: "028a7f63490a1c2aac3318493b3c1ac1a7299912" url: "https://github.com/Kingtous/rustdesk_window_manager" source: git version: "0.2.5" @@ -1221,28 +1221,28 @@ packages: dependency: transitive description: name: xdg_directories - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.2.0+1" xml: dependency: transitive description: name: xml - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "6.1.0" yaml: dependency: transitive description: name: yaml - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.1" zxing2: dependency: "direct main" description: name: zxing2 - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.1.0" sdks: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index ba400a102..02d1b42fb 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -28,13 +28,13 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.3 - ffi: ^1.1.2 + ffi: ^2.0.1 path_provider: ^2.0.2 external_path: ^1.0.1 provider: ^6.0.3 tuple: ^2.0.0 wakelock: ^0.5.2 - device_info_plus: ^3.2.3 + device_info_plus: ^4.0.2 firebase_analytics: ^9.1.5 package_info_plus: ^1.4.2 url_launcher: ^6.0.9 @@ -58,7 +58,7 @@ dependencies: window_manager: git: url: https://github.com/Kingtous/rustdesk_window_manager - ref: 1871cf2 + ref: 028a7f6 desktop_multi_window: git: url: https://github.com/Kingtous/rustdesk_desktop_multi_window From 560d623e84e85b496f6e454472c25230b6fb27e1 Mon Sep 17 00:00:00 2001 From: rklein Date: Tue, 9 Aug 2022 12:19:03 +0200 Subject: [PATCH 0178/2015] fix #1226: add missing apt dependencies to Dockerfile Signed-off-by: rklein --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index c08563614..5d15ff723 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ FROM debian WORKDIR / -RUN apt update -y && 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 unzip zip sudo +RUN apt update -y && 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 unzip zip sudo libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev RUN git clone https://github.com/microsoft/vcpkg && cd vcpkg && git checkout 134505003bb46e20fbace51ccfb69243fbbc5f82 RUN /vcpkg/bootstrap-vcpkg.sh -disableMetrics From ec3f7a8e91b24030bb4130ab2a104f007965b8c3 Mon Sep 17 00:00:00 2001 From: kingtous Date: Tue, 9 Aug 2022 19:32:19 +0800 Subject: [PATCH 0179/2015] add: multi window focus --- flutter/lib/common.dart | 14 ++++++++++---- flutter/lib/desktop/pages/connection_tab_page.dart | 2 +- flutter/lib/desktop/pages/desktop_home_page.dart | 2 +- .../lib/desktop/pages/file_manager_tab_page.dart | 2 +- 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 1a0d59e16..26398b7e4 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:io'; +import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; @@ -110,10 +111,15 @@ backToHome() { Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/")); } -void window_on_top() { - windowManager.restore(); - windowManager.show(); - windowManager.focus(); +void window_on_top(int? id) { + if (id == null) { + // main window + windowManager.restore(); + windowManager.show(); + windowManager.focus(); + } else { + WindowController.fromWindowId(id)..focus()..show(); + } } typedef DialogBuilder = CustomAlertDialog Function( diff --git a/flutter/lib/desktop/pages/connection_tab_page.dart b/flutter/lib/desktop/pages/connection_tab_page.dart index 8de2d84d0..ffe984943 100644 --- a/flutter/lib/desktop/pages/connection_tab_page.dart +++ b/flutter/lib/desktop/pages/connection_tab_page.dart @@ -49,9 +49,9 @@ class _ConnectionTabPageState extends State "call ${call.method} with args ${call.arguments} from window ${fromWindowId}"); // for simplify, just replace connectionId if (call.method == "new_remote_desktop") { - window_on_top(); final args = jsonDecode(call.arguments); final id = args['id']; + window_on_top(windowId()); final indexOf = connectionIds.indexOf(id); if (indexOf >= 0) { initialIndex = indexOf; diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index a8f2e51af..e8cd7eff6 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -472,7 +472,7 @@ class _DesktopHomePageState extends State with TrayListener, Wi print( "call ${call.method} with args ${call.arguments} from window ${fromWindowId}"); if (call.method == "main_window_on_top") { - window_on_top(); + window_on_top(null); } }); } diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index 723975d62..c06dd331d 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -46,9 +46,9 @@ class _FileManagerTabPageState extends State "call ${call.method} with args ${call.arguments} from window ${fromWindowId}"); // for simplify, just replace connectionId if (call.method == "new_file_transfer") { - window_on_top(); final args = jsonDecode(call.arguments); final id = args['id']; + window_on_top(windowId()); final indexOf = connectionIds.indexOf(id); if (indexOf >= 0) { initialIndex = indexOf; From eab7ffba7dfaf681c8d44fcac0d84da70844c47a Mon Sep 17 00:00:00 2001 From: kingtous Date: Tue, 9 Aug 2022 19:39:33 +0800 Subject: [PATCH 0180/2015] feat: focus with restore --- flutter/pubspec.lock | 4 ++-- flutter/pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 2c402951f..6bcf5a159 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -236,8 +236,8 @@ packages: dependency: "direct main" description: path: "." - ref: ce9e333d822fe6cbf91c8634bae023bf78700d94 - resolved-ref: ce9e333d822fe6cbf91c8634bae023bf78700d94 + ref: bbe24b8af079a756f2d39158dd2034127f0e1c73 + resolved-ref: bbe24b8af079a756f2d39158dd2034127f0e1c73 url: "https://github.com/Kingtous/rustdesk_desktop_multi_window" source: git version: "0.1.0" diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 02d1b42fb..c6d878dee 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -62,7 +62,7 @@ dependencies: desktop_multi_window: git: url: https://github.com/Kingtous/rustdesk_desktop_multi_window - ref: ce9e333d822fe6cbf91c8634bae023bf78700d94 + ref: bbe24b8af079a756f2d39158dd2034127f0e1c73 # bitsdojo_window: ^0.1.2 freezed_annotation: ^2.0.3 tray_manager: 0.1.7 From 0dd91acf0df59193362c2c00ce61485bf38f5ef9 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 9 Aug 2022 19:49:18 +0800 Subject: [PATCH 0181/2015] feat: add focus with restore Signed-off-by: Kingtous --- flutter/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index c6d878dee..4ecce228a 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -62,7 +62,7 @@ dependencies: desktop_multi_window: git: url: https://github.com/Kingtous/rustdesk_desktop_multi_window - ref: bbe24b8af079a756f2d39158dd2034127f0e1c73 + ref: c53879e9ce4ed038af393a02bf2c7084ad4b53aa # bitsdojo_window: ^0.1.2 freezed_annotation: ^2.0.3 tray_manager: 0.1.7 From cb88a3abb678f5c76bf70c275c2a11740efd123a Mon Sep 17 00:00:00 2001 From: csf Date: Tue, 9 Aug 2022 20:36:52 +0800 Subject: [PATCH 0182/2015] fix desktop init file / input permission bug --- .../lib/desktop/pages/connection_page.dart | 17 +---- .../lib/desktop/pages/desktop_home_page.dart | 72 +++++++------------ flutter/lib/mobile/pages/server_page.dart | 13 +++- flutter/lib/models/server_model.dart | 64 ++++++++--------- 4 files changed, 71 insertions(+), 95 deletions(-) diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index d1080dbd3..cb203f3f8 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -8,11 +8,9 @@ import 'package:flutter_hbb/desktop/widgets/peer_widget.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; -import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher_string.dart'; import '../../common.dart'; -import '../../mobile/pages/home_page.dart'; import '../../mobile/pages/scan_page.dart'; import '../../mobile/pages/settings_page.dart'; import '../../models/model.dart'; @@ -21,18 +19,9 @@ import '../../models/platform_model.dart'; // enum RemoteType { recently, favorite, discovered, addressBook } /// Connection page for connecting to a remote peer. -class ConnectionPage extends StatefulWidget implements PageShape { +class ConnectionPage extends StatefulWidget { ConnectionPage({Key? key}) : super(key: key); - @override - final icon = Icon(Icons.connected_tv); - - @override - final title = translate("Connection"); - - @override - final appBarActions = !isAndroid ? [WebMenu()] : []; - @override _ConnectionPageState createState() => _ConnectionPageState(); } @@ -174,8 +163,8 @@ class _ConnectionPageState extends State { : InkWell( onTap: () async { final url = _updateUrl + '.apk'; - if (await canLaunch(url)) { - await launch(url); + if (await canLaunchUrlString(url)) { + await launchUrlString(url); } }, child: Container( diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index e8cd7eff6..86dd2ccfe 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; -import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/material.dart' hide MenuItem; import 'package:flutter/services.dart'; import 'package:flutter_hbb/common.dart'; @@ -27,8 +26,8 @@ class DesktopHomePage extends StatefulWidget { const borderColor = Color(0xFF2F65BA); -class _DesktopHomePageState extends State with TrayListener, WindowListener { - +class _DesktopHomePageState extends State + with TrayListener, WindowListener { @override void onWindowClose() async { super.onWindowClose(); @@ -132,18 +131,7 @@ class _DesktopHomePageState extends State with TrayListener, Wi style: TextStyle( fontSize: 18, fontWeight: FontWeight.w500), ), - FutureBuilder( - future: buildPopupMenu(context), - builder: (context, snapshot) { - if (snapshot.hasError) { - print("${snapshot.error}"); - } - if (snapshot.hasData) { - return snapshot.data!; - } else { - return Offstage(); - } - }) + buildPopupMenu(context) ], ), GestureDetector( @@ -165,7 +153,7 @@ class _DesktopHomePageState extends State with TrayListener, Wi ); } - Future buildPopupMenu(BuildContext context) async { + Widget buildPopupMenu(BuildContext context) { var position; return GestureDetector( onTapDown: (detail) { @@ -178,19 +166,19 @@ class _DesktopHomePageState extends State with TrayListener, Wi final enabledInput = await bind.mainGetOption(key: 'enable-audio'); final defaultInput = await gFFI.getDefaultAudioInput(); var menu = [ - genEnablePopupMenuItem( + await genEnablePopupMenuItem( translate("Enable Keyboard/Mouse"), 'enable-keyboard', ), - genEnablePopupMenuItem( + await genEnablePopupMenuItem( translate("Enable Clipboard"), 'enable-clipboard', ), - genEnablePopupMenuItem( + await genEnablePopupMenuItem( translate("Enable File Transfer"), 'enable-file-transfer', ), - genEnablePopupMenuItem( + await genEnablePopupMenuItem( translate("Enable TCP Tunneling"), 'enable-tunnel', ), @@ -209,16 +197,16 @@ class _DesktopHomePageState extends State with TrayListener, Wi value: 'socks5-proxy', ), PopupMenuDivider(), - genEnablePopupMenuItem( + await genEnablePopupMenuItem( translate("Enable Service"), 'stop-service', ), // TODO: direct server - genEnablePopupMenuItem( + await genEnablePopupMenuItem( translate("Always connected via relay"), 'allow-always-relay', ), - genEnablePopupMenuItem( + await genEnablePopupMenuItem( translate("Start ID/relay service"), 'stop-rendezvous-service', ), @@ -237,7 +225,7 @@ class _DesktopHomePageState extends State with TrayListener, Wi value: 'change-id', ), PopupMenuDivider(), - genEnablePopupMenuItem( + await genEnablePopupMenuItem( translate("Dark Theme"), 'allow-darktheme', ), @@ -522,30 +510,22 @@ class _DesktopHomePageState extends State with TrayListener, Wi } } - PopupMenuItem genEnablePopupMenuItem(String label, String key) { - Future getOptionEnable(String key) async { - final v = await bind.mainGetOption(key: key); - return key.startsWith('enable-') ? v != "N" : v == "Y"; - } + Future> genEnablePopupMenuItem( + String label, String key) async { + final v = await bind.mainGetOption(key: key); + bool enable = v != "N"; return PopupMenuItem( - child: FutureBuilder( - future: getOptionEnable(key), - builder: (context, snapshot) { - var enable = false; - if (snapshot.hasData && snapshot.data!) { - enable = true; - } - return Row( - children: [ - Offstage(offstage: !enable, child: Icon(Icons.check)), - Text( - label, - style: genTextStyle(enable), - ), - ], - ); - }), + child: Row( + children: [ + Icon(Icons.check, + color: enable ? null : MyTheme.accent.withAlpha(00)), + Text( + label, + style: genTextStyle(enable), + ), + ], + ), value: key, ); } diff --git a/flutter/lib/mobile/pages/server_page.dart b/flutter/lib/mobile/pages/server_page.dart index 3abcd70da..d3dc4109d 100644 --- a/flutter/lib/mobile/pages/server_page.dart +++ b/flutter/lib/mobile/pages/server_page.dart @@ -8,7 +8,7 @@ import '../../models/platform_model.dart'; import '../../models/server_model.dart'; import 'home_page.dart'; -class ServerPage extends StatelessWidget implements PageShape { +class ServerPage extends StatefulWidget implements PageShape { @override final title = translate("Share Screen"); @@ -102,6 +102,17 @@ class ServerPage extends StatelessWidget implements PageShape { }) ]; + @override + State createState() => _ServerPageState(); +} + +class _ServerPageState extends State { + @override + void initState() { + super.initState(); + gFFI.serverModel.checkAndroidPermission(); + } + @override Widget build(BuildContext context) { checkService(); diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index 6aa7016b2..d59ad49c2 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -85,40 +85,8 @@ class ServerModel with ChangeNotifier { WeakReference parent; ServerModel(this.parent) { - () async { - _emptyIdShow = translate("Generating ..."); - _serverId = TextEditingController(text: this._emptyIdShow); - /** - * 1. check android permission - * 2. check config - * audio true by default (if permission on) (false default < Android 10) - * file true by default (if permission on) - */ - await Future.delayed(Duration(seconds: 1)); - - // audio - if (androidVersion < 30 || !await PermissionManager.check("audio")) { - _audioOk = false; - bind.mainSetOption(key: "enable-audio", value: "N"); - } else { - final audioOption = await bind.mainGetOption(key: 'enable-audio'); - _audioOk = audioOption.isEmpty; - } - - // file - if (!await PermissionManager.check("file")) { - _fileOk = false; - bind.mainSetOption(key: "enable-file-transfer", value: "N"); - } else { - final fileOption = - await bind.mainGetOption(key: 'enable-file-transfer'); - _fileOk = fileOption.isEmpty; - } - - // input (mouse control) false by default - bind.mainSetOption(key: "enable-keyboard", value: "N"); - notifyListeners(); - }(); + _emptyIdShow = translate("Generating ..."); + _serverId = TextEditingController(text: this._emptyIdShow); Timer.periodic(Duration(seconds: 1), (timer) async { var status = await bind.mainGetOnlineStatue(); @@ -139,6 +107,34 @@ class ServerModel with ChangeNotifier { }); } + /// 1. check android permission + /// 2. check config + /// audio true by default (if permission on) (false default < Android 10) + /// file true by default (if permission on) + checkAndroidPermission() async { + // audio + if (androidVersion < 30 || !await PermissionManager.check("audio")) { + _audioOk = false; + bind.mainSetOption(key: "enable-audio", value: "N"); + } else { + final audioOption = await bind.mainGetOption(key: 'enable-audio'); + _audioOk = audioOption.isEmpty; + } + + // file + if (!await PermissionManager.check("file")) { + _fileOk = false; + bind.mainSetOption(key: "enable-file-transfer", value: "N"); + } else { + final fileOption = await bind.mainGetOption(key: 'enable-file-transfer'); + _fileOk = fileOption.isEmpty; + } + + // input (mouse control) false by default + bind.mainSetOption(key: "enable-keyboard", value: "N"); + notifyListeners(); + } + updatePasswordModel() async { var update = false; final temporaryPassword = await bind.mainGetTemporaryPassword(); From 42f27922bfa5a3ff724f47a72fe3b49079a3742b Mon Sep 17 00:00:00 2001 From: csf Date: Tue, 9 Aug 2022 20:50:45 +0800 Subject: [PATCH 0183/2015] fix desktop stop-service --- flutter/lib/desktop/pages/connection_page.dart | 15 +++++++++++---- flutter/lib/desktop/pages/desktop_home_page.dart | 7 ++++++- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index cb203f3f8..7a80a64a1 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -376,13 +376,20 @@ class _ConnectionPageState extends State { width: 8, decoration: BoxDecoration( borderRadius: BorderRadius.circular(20), - color: Colors.green, + color: svcStopped.value ? Colors.redAccent : Colors.green, ), - ).paddingSymmetric(horizontal: 8.0); + ).paddingSymmetric(horizontal: 10.0); if (svcStopped.value) { return Row( crossAxisAlignment: CrossAxisAlignment.center, - children: [light, Text(translate("Service is not running"))], + children: [ + light, + Text(translate("Service is not running")), + TextButton( + onPressed: () => + bind.mainSetOption(key: "stop-service", value: ""), + child: Text(translate("Start Service"))) + ], ); } else { if (svcStatusCode.value == 0) { @@ -425,7 +432,7 @@ class _ConnectionPageState extends State { } updateStatus() async { - svcStopped.value = bind.mainGetOption(key: "stop-service") == "Y"; + svcStopped.value = await bind.mainGetOption(key: "stop-service") == "Y"; final status = jsonDecode(await bind.mainGetConnectStatus()) as Map; svcStatusCode.value = status["status_num"]; diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 86dd2ccfe..1d15a30a9 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -513,7 +513,12 @@ class _DesktopHomePageState extends State Future> genEnablePopupMenuItem( String label, String key) async { final v = await bind.mainGetOption(key: key); - bool enable = v != "N"; + bool enable; + if (key == "stop-service") { + enable = v != "Y"; + } else { + enable = v != "N"; + } return PopupMenuItem( child: Row( From dd8812dd88c9c697804ec076c93c212e8bdde228 Mon Sep 17 00:00:00 2001 From: csf Date: Tue, 9 Aug 2022 21:12:55 +0800 Subject: [PATCH 0184/2015] fix desktop dark mode --- flutter/lib/common.dart | 4 +++- flutter/lib/desktop/pages/desktop_home_page.dart | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 26398b7e4..3b026141d 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -118,7 +118,9 @@ void window_on_top(int? id) { windowManager.show(); windowManager.focus(); } else { - WindowController.fromWindowId(id)..focus()..show(); + WindowController.fromWindowId(id) + ..focus() + ..show(); } } diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 1d15a30a9..8496b0eda 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -479,6 +479,7 @@ class _DesktopHomePageState extends State Get.changeTheme(MyTheme.lightTheme); } Get.find().setString("darkTheme", choice); + Get.forceAppUpdate(); } void onSelectMenu(String key) async { @@ -489,7 +490,7 @@ class _DesktopHomePageState extends State final option = await bind.mainGetOption(key: key); final choice = option == "Y" ? "" : "Y"; bind.mainSetOption(key: key, value: choice); - changeTheme(choice); + if (key == "allow-darktheme") changeTheme(choice); } else if (key == "stop-service") { final option = await bind.mainGetOption(key: key); bind.mainSetOption(key: key, value: option == "Y" ? "" : "Y"); @@ -516,6 +517,8 @@ class _DesktopHomePageState extends State bool enable; if (key == "stop-service") { enable = v != "Y"; + } else if (key.startsWith("allow-")) { + enable = v == "Y"; } else { enable = v != "N"; } From f96c652ee42d014ba81a2f745d0ec049612c68f9 Mon Sep 17 00:00:00 2001 From: csf Date: Wed, 10 Aug 2022 10:42:59 +0800 Subject: [PATCH 0185/2015] refresh peers state workaround --- flutter/lib/desktop/widgets/peercard_widget.dart | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/flutter/lib/desktop/widgets/peercard_widget.dart b/flutter/lib/desktop/widgets/peercard_widget.dart index f4743a7b5..87cfa2a59 100644 --- a/flutter/lib/desktop/widgets/peercard_widget.dart +++ b/flutter/lib/desktop/widgets/peercard_widget.dart @@ -187,10 +187,9 @@ class _PeerCardState extends State<_PeerCard> elevation: 8, ); if (value == 'remove') { - setState(() => bind.mainRemovePeer(id: id)); - () async { - removePreference(id); - }(); + await bind.mainRemovePeer(id: id); + removePreference(id); + Get.forceAppUpdate(); // TODO use inner model / state } else if (value == 'file') { _connect(id, isFileTransfer: true); } else if (value == 'add-fav') { From 780b42d0ba726d114431971c4f871388098978e6 Mon Sep 17 00:00:00 2001 From: kingtous Date: Wed, 10 Aug 2022 11:33:50 +0800 Subject: [PATCH 0186/2015] feat: adapt macos dark mode --- Cargo.lock | 334 +++++++++++++++++++++++++++++- Cargo.toml | 3 +- flutter/pubspec.lock | 308 +++++++++++++-------------- mac-tray.png => mac-tray-dark.png | Bin mac-tray-light.png | Bin 0 -> 475 bytes src/ui/macos.rs | 13 +- 6 files changed, 499 insertions(+), 159 deletions(-) rename mac-tray.png => mac-tray-dark.png (100%) create mode 100644 mac-tray-light.png diff --git a/Cargo.lock b/Cargo.lock index 2d9ac2cac..a6ed80add 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,6 +23,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" +[[package]] +name = "ahash" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "739f4a8db6605981345c5654f3a85b056ce52f37a39d34da03f25bf2151ea16e" + [[package]] name = "ahash" version = "0.7.6" @@ -138,6 +144,17 @@ dependencies = [ "x11rb", ] +[[package]] +name = "async-broadcast" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d26004fe83b2d1cd3a97609b21e39f9a31535822210fe83205d2ce48866ea61" +dependencies = [ + "event-listener", + "futures-core", + "parking_lot 0.12.1", +] + [[package]] name = "async-channel" version = "1.6.1" @@ -149,6 +166,20 @@ dependencies = [ "futures-core", ] +[[package]] +name = "async-executor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "871f9bb5e0a22eeb7e8cf16641feb87c9dc67032ccf8ff49e772eb9941d3a965" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "once_cell", + "slab", +] + [[package]] name = "async-io" version = "1.7.0" @@ -168,6 +199,15 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "async-lock" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e97a171d191782fba31bb902b14ad94e24a68145032b7eedf871ab0bc0d077b6" +dependencies = [ + "event-listener", +] + [[package]] name = "async-process" version = "1.4.0" @@ -185,6 +225,17 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "async-recursion" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7d78656ba01f1b93024b7c3a0467f1608e4be67d725749fdcd7d2c7678fd7a2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "async-task" version = "4.3.0" @@ -973,6 +1024,23 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35" +[[package]] +name = "dark-light" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b83576e2eee2d9cdaa8d08812ae59cbfe1b5ac7ac5ac4b8400303c6148a88c1" +dependencies = [ + "dconf_rs", + "detect-desktop-environment", + "dirs", + "objc", + "rust-ini", + "web-sys", + "winreg 0.8.0", + "zbus", + "zvariant", +] + [[package]] name = "darling" version = "0.13.4" @@ -1138,6 +1206,12 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "dconf_rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7046468a81e6a002061c01e6a7c83139daf91b11c30e66795b13217c2d885c8b" + [[package]] name = "default-net" version = "0.11.0" @@ -1160,6 +1234,23 @@ dependencies = [ "byteorder", ] +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "detect-desktop-environment" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21d8ad60dd5b13a4ee6bd8fa2d5d88965c597c67bce32b5fc49c94f55cb50810" + [[package]] name = "digest" version = "0.10.3" @@ -1180,6 +1271,15 @@ dependencies = [ "dirs-sys-next", ] +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys", +] + [[package]] name = "dirs-next" version = "2.0.0" @@ -1190,6 +1290,17 @@ dependencies = [ "dirs-sys-next", ] +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi 0.3.9", +] + [[package]] name = "dirs-sys-next" version = "0.1.2" @@ -1216,6 +1327,15 @@ dependencies = [ "libloading", ] +[[package]] +name = "dlv-list" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68df3f2b690c1b86e65ef7830956aededf3cb0a16f898f79b9a6f421a7b6211b" +dependencies = [ + "rand 0.8.5", +] + [[package]] name = "docopt" version = "1.1.1" @@ -1292,6 +1412,27 @@ dependencies = [ "syn", ] +[[package]] +name = "enumflags2" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e75d4cd21b95383444831539909fbb14b9dc3fdceb2a6f5d36577329a1f55ccb" +dependencies = [ + "enumflags2_derive", + "serde 1.0.139", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f58dc3c5e468259f19f2d46304a6b28f1c3d034442e14b322d2b850e36f6d5ae" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "env_logger" version = "0.8.4" @@ -2094,13 +2235,22 @@ dependencies = [ "tracing", ] +[[package]] +name = "hashbrown" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" +dependencies = [ + "ahash 0.4.7", +] + [[package]] name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" dependencies = [ - "ahash", + "ahash 0.7.6", ] [[package]] @@ -2163,6 +2313,12 @@ dependencies = [ "libc", ] +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "hound" version = "3.4.0" @@ -2318,7 +2474,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" dependencies = [ "autocfg 1.1.0", - "hashbrown", + "hashbrown 0.12.3", ] [[package]] @@ -3128,6 +3284,26 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +[[package]] +name = "ordered-multimap" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c672c7ad9ec066e428c00eb917124a06f08db19e2584de982cc34b1f4c12485" +dependencies = [ + "dlv-list", + "hashbrown 0.9.1", +] + +[[package]] +name = "ordered-stream" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44630c059eacfd6e08bdaa51b1db2ce33119caa4ddc1235e923109aa5f25ccb1" +dependencies = [ + "futures-core", + "pin-project-lite", +] + [[package]] name = "os_str_bytes" version = "6.2.0" @@ -3929,6 +4105,16 @@ dependencies = [ "which 3.1.1", ] +[[package]] +name = "rust-ini" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63471c4aa97a1cf8332a5f97709a79a4234698de6a1f5087faf66f2dae810e22" +dependencies = [ + "cfg-if 1.0.0", + "ordered-multimap", +] + [[package]] name = "rust-pulsectl" version = "0.2.12" @@ -3977,6 +4163,7 @@ dependencies = [ "core-graphics 0.22.3", "cpal", "ctrlc", + "dark-light", "dasp", "default-net", "dispatch", @@ -4289,6 +4476,17 @@ dependencies = [ "serde 1.0.139", ] +[[package]] +name = "serde_repr" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fe39d9fbb0ebf5eb2c7cb7e2a47e4f462fad1379f1166b8ae49ad9eae89a7ca" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -4335,6 +4533,21 @@ dependencies = [ "yaml-rust", ] +[[package]] +name = "sha1" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1da05c97445caa12d05e848c4a4fcbbea29e748ac28f7e80e9b010392063770" +dependencies = [ + "sha1_smol", +] + +[[package]] +name = "sha1_smol" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" + [[package]] name = "sha2" version = "0.10.2" @@ -4464,6 +4677,12 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "stdweb" version = "0.1.3" @@ -4854,7 +5073,7 @@ dependencies = [ "futures-io", "futures-sink", "futures-util", - "hashbrown", + "hashbrown 0.12.3", "pin-project-lite", "slab", "tokio", @@ -4963,6 +5182,16 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89570599c4fe5585de2b388aab47e99f7fa4e9238a1399f707a02e356058141c" +[[package]] +name = "uds_windows" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce65604324d3cce9b966701489fbd0cf318cb1f7bd9dd07ac9a4ee6fb791930d" +dependencies = [ + "tempfile", + "winapi 0.3.9", +] + [[package]] name = "unicode-bidi" version = "0.3.8" @@ -5571,6 +5800,15 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "winreg" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d107f8c6e916235c4c01cabb3e8acf7bea8ef6a63ca2e7fa0527c049badfc48c" +dependencies = [ + "winapi 0.3.9", +] + [[package]] name = "winreg" version = "0.10.1" @@ -5674,6 +5912,70 @@ dependencies = [ "linked-hash-map", ] +[[package]] +name = "zbus" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d8f1a037b2c4a67d9654dc7bdfa8ff2e80555bbefdd3c1833c1d1b27c963a6b" +dependencies = [ + "async-broadcast", + "async-channel", + "async-executor", + "async-io", + "async-lock", + "async-recursion", + "async-task", + "async-trait", + "byteorder", + "derivative", + "dirs", + "enumflags2", + "event-listener", + "futures-core", + "futures-sink", + "futures-util", + "hex", + "lazy_static", + "nix 0.23.1", + "once_cell", + "ordered-stream", + "rand 0.8.5", + "serde 1.0.139", + "serde_repr", + "sha1", + "static_assertions", + "tracing", + "uds_windows", + "winapi 0.3.9", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f8fb5186d1c87ae88cf234974c240671238b4a679158ad3b94ec465237349a6" +dependencies = [ + "proc-macro-crate 1.1.3", + "proc-macro2", + "quote", + "regex", + "syn", +] + +[[package]] +name = "zbus_names" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a408fd8a352695690f53906dc7fd036be924ec51ea5e05666ff42685ed0af5" +dependencies = [ + "serde 1.0.139", + "static_assertions", + "zvariant", +] + [[package]] name = "zstd" version = "0.9.2+zstd.1.5.1" @@ -5702,3 +6004,29 @@ dependencies = [ "cc", "libc", ] + +[[package]] +name = "zvariant" +version = "3.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd68e4e6432ef19df47d7e90e2e72b5e7e3d778e0ae3baddf12b951265cc758" +dependencies = [ + "byteorder", + "enumflags2", + "libc", + "serde 1.0.139", + "static_assertions", + "zvariant_derive", +] + +[[package]] +name = "zvariant_derive" +version = "3.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08e977eaa3af652f63d479ce50d924254ad76722a6289ec1a1eac3231ca30430" +dependencies = [ + "proc-macro-crate 1.1.3", + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index 7f185db6b..f48a47d9b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -95,6 +95,7 @@ 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" [target.'cfg(target_os = "linux")'.dependencies] psimple = { package = "libpulse-simple-binding", version = "2.25" } @@ -138,7 +139,7 @@ identifier = "com.carriez.rustdesk" icon = ["32x32.png", "128x128.png", "128x128@2x.png"] deb_depends = ["libgtk-3-0", "libxcb-randr0", "libxdo3", "libxfixes3", "libxcb-shape0", "libxcb-xfixes0", "libasound2", "libsystemd0", "pulseaudio", "python3-pip", "curl"] osx_minimum_system_version = "10.14" -resources = ["mac-tray.png"] +resources = ["mac-tray-light.png","mac-tray-dark.png"] #https://github.com/johnthagen/min-sized-rust [profile.release] diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index ec709b959..8af54209e 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -5,217 +5,217 @@ packages: dependency: transitive description: name: archive - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted - version: "3.3.0" + version: "3.3.1" args: dependency: transitive description: name: args - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.3.1" async: dependency: transitive description: name: async - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.8.2" boolean_selector: dependency: transitive description: name: boolean_selector - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.1.0" cached_network_image: dependency: transitive description: name: cached_network_image - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "3.2.1" cached_network_image_platform_interface: dependency: transitive description: name: cached_network_image_platform_interface - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.0.0" cached_network_image_web: dependency: transitive description: name: cached_network_image_web - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.0.1" characters: dependency: transitive description: name: characters - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.2.0" charcode: dependency: transitive description: name: charcode - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.3.1" clock: dependency: transitive description: name: clock - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.1.0" collection: dependency: transitive description: name: collection - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.16.0" cross_file: dependency: transitive description: name: cross_file - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted - version: "0.3.3" + version: "0.3.3+1" crypto: dependency: transitive description: name: crypto - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "3.0.2" csslib: dependency: transitive description: name: csslib - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.17.2" cupertino_icons: dependency: "direct main" description: name: cupertino_icons - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted - version: "1.0.4" + version: "1.0.5" dash_chat_2: dependency: "direct main" description: name: dash_chat_2 - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.0.12" device_info: dependency: "direct main" description: name: device_info - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.0.3" device_info_platform_interface: dependency: transitive description: name: device_info_platform_interface - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.0.1" draggable_float_widget: dependency: "direct main" description: name: draggable_float_widget - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.0.2" event_bus: dependency: transitive description: name: event_bus - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.0.0" external_path: dependency: "direct main" description: name: external_path - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.0.1" fake_async: dependency: transitive description: name: fake_async - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.3.0" ffi: dependency: "direct main" description: name: ffi - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.2.1" file: dependency: transitive description: name: file - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "6.1.2" firebase_analytics: dependency: "direct main" description: name: firebase_analytics - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted - version: "9.1.8" + version: "9.3.0" firebase_analytics_platform_interface: dependency: transitive description: name: firebase_analytics_platform_interface - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted - version: "3.1.6" + version: "3.3.0" firebase_analytics_web: dependency: transitive description: name: firebase_analytics_web - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted - version: "0.4.0+13" + version: "0.4.2" firebase_core: dependency: transitive description: name: firebase_core - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted - version: "1.17.0" + version: "1.20.0" firebase_core_platform_interface: dependency: transitive description: name: firebase_core_platform_interface - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted - version: "4.4.0" + version: "4.5.0" firebase_core_web: dependency: transitive description: name: firebase_core_web - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted - version: "1.6.4" + version: "1.7.1" fixnum: dependency: transitive description: name: fixnum - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.0.1" flutter: @@ -227,58 +227,58 @@ packages: dependency: transitive description: name: flutter_blurhash - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.7.0" flutter_breadcrumb: dependency: "direct main" description: name: flutter_breadcrumb - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.0.1" flutter_cache_manager: dependency: transitive description: name: flutter_cache_manager - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "3.3.0" flutter_launcher_icons: dependency: "direct dev" description: name: flutter_launcher_icons - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted - version: "0.9.2" + version: "0.9.3" flutter_parsed_text: dependency: transitive description: name: flutter_parsed_text - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.2.1" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted - version: "2.0.6" + version: "2.0.7" flutter_rust_bridge: dependency: "direct main" description: name: flutter_rust_bridge - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted - version: "1.30.0" + version: "1.41.0" flutter_smart_dialog: dependency: "direct main" description: name: flutter_smart_dialog - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted - version: "4.3.1" + version: "4.5.4+1" flutter_test: dependency: "direct dev" description: flutter @@ -293,301 +293,301 @@ packages: dependency: transitive description: name: html - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.15.0" http: dependency: "direct main" description: name: http - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted - version: "0.13.4" + version: "0.13.5" http_parser: dependency: transitive description: name: http_parser - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted - version: "4.0.0" + version: "4.0.1" image: dependency: "direct main" description: name: image - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted - version: "3.1.3" + version: "3.2.0" image_picker: dependency: "direct main" description: name: image_picker - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.8.5+3" image_picker_android: dependency: transitive description: name: image_picker_android - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted - version: "0.8.4+13" + version: "0.8.5+2" image_picker_for_web: dependency: transitive description: name: image_picker_for_web - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.1.8" image_picker_ios: dependency: transitive description: name: image_picker_ios - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted - version: "0.8.5+2" + version: "0.8.5+6" image_picker_platform_interface: dependency: transitive description: name: image_picker_platform_interface - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted - version: "2.5.0" + version: "2.6.1" intl: dependency: transitive description: name: intl - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.17.0" js: dependency: transitive description: name: js - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.6.4" matcher: dependency: transitive description: name: matcher - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.12.11" material_color_utilities: dependency: transitive description: name: material_color_utilities - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.1.4" meta: dependency: transitive description: name: meta - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.7.0" nested: dependency: transitive description: name: nested - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.0.0" octo_image: dependency: transitive description: name: octo_image - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.0.2" package_info: dependency: "direct main" description: name: package_info - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.0.2" path: dependency: transitive description: name: path - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.8.1" path_provider: dependency: "direct main" description: name: path_provider - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted - version: "2.0.10" + version: "2.0.11" path_provider_android: dependency: transitive description: name: path_provider_android - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted - version: "2.0.14" + version: "2.0.17" path_provider_ios: dependency: transitive description: name: path_provider_ios - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted - version: "2.0.9" + version: "2.0.11" path_provider_linux: dependency: transitive description: name: path_provider_linux - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted - version: "2.1.6" + version: "2.1.7" path_provider_macos: dependency: transitive description: name: path_provider_macos - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.0.6" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.0.4" path_provider_windows: dependency: transitive description: name: path_provider_windows - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted - version: "2.0.6" + version: "2.0.7" pedantic: dependency: transitive description: name: pedantic - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.11.1" petitparser: dependency: transitive description: name: petitparser - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted - version: "4.4.0" + version: "5.0.0" platform: dependency: transitive description: name: platform - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "3.1.0" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.1.2" process: dependency: transitive description: name: process - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "4.2.4" provider: dependency: "direct main" description: name: provider - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "6.0.3" qr_code_scanner: dependency: "direct main" description: name: qr_code_scanner - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.0.0" quiver: dependency: transitive description: name: quiver - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "3.1.0" rxdart: dependency: transitive description: name: rxdart - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.27.5" settings_ui: dependency: "direct main" description: name: settings_ui - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.0.2" shared_preferences: dependency: "direct main" description: name: shared_preferences - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.0.15" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.0.12" shared_preferences_ios: dependency: transitive description: name: shared_preferences_ios - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.1.1" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.1.1" shared_preferences_macos: dependency: transitive description: name: shared_preferences_macos - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.0.4" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.0.0" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.0.4" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.1.1" sky_engine: @@ -599,259 +599,259 @@ packages: dependency: transitive description: name: source_span - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.8.2" sqflite: dependency: transitive description: name: sqflite - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted - version: "2.0.3" + version: "2.0.3+1" sqflite_common: dependency: transitive description: name: sqflite_common - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.2.1+1" stack_trace: dependency: transitive description: name: stack_trace - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.10.0" stream_channel: dependency: transitive description: name: stream_channel - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.1.0" string_scanner: dependency: transitive description: name: string_scanner - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.1.0" synchronized: dependency: transitive description: name: synchronized - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "3.0.0+2" term_glyph: dependency: transitive description: name: term_glyph - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.2.0" test_api: dependency: transitive description: name: test_api - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.4.9" toggle_switch: dependency: "direct main" description: name: toggle_switch - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.4.0" tuple: dependency: "direct main" description: name: tuple - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.0.0" typed_data: dependency: transitive description: name: typed_data - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted - version: "1.3.0" + version: "1.3.1" url_launcher: dependency: "direct main" description: name: url_launcher - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted - version: "6.1.2" + version: "6.1.5" url_launcher_android: dependency: transitive description: name: url_launcher_android - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "6.0.17" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted - version: "6.0.16" + version: "6.0.17" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "3.0.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "3.0.1" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted - version: "2.0.5" + version: "2.1.0" url_launcher_web: dependency: transitive description: name: url_launcher_web - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted - version: "2.0.11" + version: "2.0.13" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "3.0.1" uuid: dependency: transitive description: name: uuid - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "3.0.6" vector_math: dependency: transitive description: name: vector_math - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.1.2" video_player: dependency: transitive description: name: video_player - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted - version: "2.4.5" + version: "2.4.6" video_player_android: dependency: transitive description: name: video_player_android - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.3.8" video_player_avfoundation: dependency: transitive description: name: video_player_avfoundation - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.3.5" video_player_platform_interface: dependency: transitive description: name: video_player_platform_interface - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted - version: "5.1.3" + version: "5.1.4" video_player_web: dependency: transitive description: name: video_player_web - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.0.12" wakelock: dependency: "direct main" description: name: wakelock - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.5.6" wakelock_macos: dependency: transitive description: name: wakelock_macos - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.4.0" wakelock_platform_interface: dependency: transitive description: name: wakelock_platform_interface - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.3.0" wakelock_web: dependency: transitive description: name: wakelock_web - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.4.0" wakelock_windows: dependency: transitive description: name: wakelock_windows - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.2.0" win32: dependency: transitive description: name: win32 - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted - version: "2.5.2" + version: "2.6.1" xdg_directories: dependency: transitive description: name: xdg_directories - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.2.0+1" xml: dependency: transitive description: name: xml - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted - version: "5.3.1" + version: "6.1.0" yaml: dependency: transitive description: name: yaml - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "3.1.1" zxing2: dependency: "direct main" description: name: zxing2 - url: "https://pub.dartlang.org" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "0.1.0" sdks: diff --git a/mac-tray.png b/mac-tray-dark.png similarity index 100% rename from mac-tray.png rename to mac-tray-dark.png diff --git a/mac-tray-light.png b/mac-tray-light.png new file mode 100644 index 0000000000000000000000000000000000000000..c3e107410ce32019410c055f63885874e97747cd GIT binary patch literal 475 zcmV<10VMv3P)Px$l}SWFR5(wC)61)mQ544U&l5%D4=@oEM^Ypc9fL!T7$Em3*C}N}Mh42vgc4Cw zl#p9;NiuNBz(^eiC_@HJC>bbQTl>}9$?3NzZ|}37_1$Z)y>^USrIZmIK>aSq6gIN6I0SHKAzYT^@&CaLBB zca>5eVs#6iseoGM!X|7^a&w~ORz*F=wdNVCfFGFFgrOww2LKkJHnFa674Q@F7V4Ut zg@-C#rWT&RFG=#S^FB$p?+ z#yz;$i;Z@JRHtxd3;f1eoa*{fN*Tgo?CYi%_mZsb05kCtvwQRkTG~eWMK7^h>P?L# z`P_d@vs!bO^dPk#({ZJzJ;jbBpQ`o`(1xVGyq(yMMOcMj?V-KGC0y@M>K_$GlQWUr RfZ_lE002ovPDHLkV1i>W*+&2X literal 0 HcmV?d00001 diff --git a/src/ui/macos.rs b/src/ui/macos.rs index 188fbb603..3c7a7dcd0 100644 --- a/src/ui/macos.rs +++ b/src/ui/macos.rs @@ -13,6 +13,7 @@ use objc::{ }; use sciter::{make_args, Host}; use std::{ffi::c_void, rc::Rc}; +use dark_light; static APP_HANDLER_IVAR: &str = "GoDeskAppHandler"; @@ -233,7 +234,17 @@ pub fn make_tray() { set_delegate(None); } use tray_item::TrayItem; - if let Ok(mut tray) = TrayItem::new(&crate::get_app_name(), "mac-tray.png") { + let mode = dark_light::detect(); + let mut icon_path = ""; + match mode { + dark_light::Mode::Dark => { + icon_path = "mac-tray-light.png"; + }, + dark_light::Mode::Light => { + icon_path = "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(), From 09c80bc585d7138eac86051d71f3938aa013c180 Mon Sep 17 00:00:00 2001 From: csf Date: Thu, 11 Aug 2022 10:19:12 +0800 Subject: [PATCH 0187/2015] update desktop and mobile chat message --- flutter/lib/desktop/pages/remote_page.dart | 61 ++++++------- flutter/lib/mobile/pages/chat_page.dart | 10 ++- flutter/lib/mobile/pages/home_page.dart | 5 +- flutter/lib/mobile/pages/remote_page.dart | 6 +- flutter/lib/mobile/widgets/overlay.dart | 96 +++------------------ flutter/lib/models/chat_model.dart | 99 ++++++++++++++++++++++ 6 files changed, 151 insertions(+), 126 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index da7a317a8..fc94d5be8 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -115,11 +115,6 @@ class _RemotePageState extends State if (v < 100) { SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); - // [pi.version.isNotEmpty] -> check ready or not,avoid login without soft-keyboard - if (chatWindowOverlayEntry == null && - _ffi.ffiModel.pi.version.isNotEmpty) { - _ffi.invokeMethod("enable_soft_keyboard", false); - } } }); } @@ -266,9 +261,10 @@ class _RemotePageState extends State body: Overlay( initialEntries: [ OverlayEntry(builder: (context) { + _ffi.chatModel.setOverlayState(Overlay.of(context)); return Container( color: Colors.black, - child: getBodyForDesktopWithListener(keyboard)); + child: getRawPointerAndKeyBody(getBodyForDesktop(keyboard))); }) ], )); @@ -290,8 +286,8 @@ class _RemotePageState extends State ChangeNotifierProvider.value(value: _ffi.cursorModel), ChangeNotifierProvider.value(value: _ffi.canvasModel), ], - child: getRawPointerAndKeyBody(Consumer( - builder: (context, ffiModel, _child) => buildBody(ffiModel))))); + child: Consumer( + builder: (context, ffiModel, _child) => buildBody(ffiModel)))); } Widget getRawPointerAndKeyBody(Widget child) { @@ -467,7 +463,7 @@ class _RemotePageState extends State onPressed: () { _ffi.chatModel .changeCurrentID(ChatModel.clientModeID); - toggleChatOverlay(); + _ffi.chatModel.toggleChatOverlay(); }, ) ]) + @@ -502,11 +498,27 @@ class _RemotePageState extends State /// DoubleFiner -> right click /// HoldDrag -> left drag - Widget getBodyForDesktopWithListener(bool keyboard) { + Widget getBodyForDesktop(bool keyboard) { var paints = [ - ImagePaint( - id: widget.id, - ) + MouseRegion( + onEnter: (evt) { + bind.hostStopSystemKeyPropagate(stopped: false); + }, + onExit: (evt) { + bind.hostStopSystemKeyPropagate(stopped: true); + }, + child: Container( + color: MyTheme.canvasColor, + child: LayoutBuilder(builder: (context, constraints) { + Future.delayed(Duration.zero, () { + Provider.of(context, listen: false) + .updateViewStyle(); + }); + return ImagePaint( + id: widget.id, + ); + }), + )) ]; final cursor = bind.getSessionToggleOptionSync( id: widget.id, arg: 'show-remote-cursor'); @@ -516,26 +528,9 @@ class _RemotePageState extends State )); } paints.add(getHelpTools()); - - return MouseRegion( - onEnter: (evt) { - bind.hostStopSystemKeyPropagate(stopped: false); - }, - onExit: (evt) { - bind.hostStopSystemKeyPropagate(stopped: true); - }, - child: Container( - color: MyTheme.canvasColor, - child: LayoutBuilder(builder: (context, constraints) { - Future.delayed(Duration.zero, () { - Provider.of(context, listen: false) - .updateViewStyle(); - }); - return Stack( - children: paints, - ); - }), - )); + return Stack( + children: paints, + ); } int lastMouseDownButtons = 0; diff --git a/flutter/lib/mobile/pages/chat_page.dart b/flutter/lib/mobile/pages/chat_page.dart index a49a02bb4..0bc4c2a25 100644 --- a/flutter/lib/mobile/pages/chat_page.dart +++ b/flutter/lib/mobile/pages/chat_page.dart @@ -4,10 +4,15 @@ import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/models/chat_model.dart'; import 'package:provider/provider.dart'; -import '../../models/model.dart'; import 'home_page.dart'; class ChatPage extends StatelessWidget implements PageShape { + late final ChatModel chatModel; + + ChatPage({ChatModel? chatModel}) { + this.chatModel = chatModel ?? gFFI.chatModel; + } + @override final title = translate("Chat"); @@ -19,6 +24,7 @@ class ChatPage extends StatelessWidget implements PageShape { PopupMenuButton( icon: Icon(Icons.group), itemBuilder: (context) { + // only mobile need [appBarActions], just bind gFFI.chatModel final chatModel = gFFI.chatModel; return chatModel.messages.entries.map((entry) { final id = entry.key; @@ -37,7 +43,7 @@ class ChatPage extends StatelessWidget implements PageShape { @override Widget build(BuildContext context) { return ChangeNotifierProvider.value( - value: gFFI.chatModel, + value: chatModel, child: Container( color: MyTheme.grayBg, child: Consumer(builder: (context, chatModel, child) { diff --git a/flutter/lib/mobile/pages/home_page.dart b/flutter/lib/mobile/pages/home_page.dart index 6bf0be2c7..05a6d6b51 100644 --- a/flutter/lib/mobile/pages/home_page.dart +++ b/flutter/lib/mobile/pages/home_page.dart @@ -3,7 +3,6 @@ import 'package:flutter_hbb/mobile/pages/chat_page.dart'; import 'package:flutter_hbb/mobile/pages/server_page.dart'; import 'package:flutter_hbb/mobile/pages/settings_page.dart'; import '../../common.dart'; -import '../widgets/overlay.dart'; import 'connection_page.dart'; abstract class PageShape extends Widget { @@ -79,8 +78,8 @@ class _HomePageState extends State { onTap: (index) => setState(() { // close chat overlay when go chat page if (index == 1 && _selectedIndex != index) { - hideChatIconOverlay(); - hideChatWindowOverlay(); + gFFI.chatModel.hideChatIconOverlay(); + gFFI.chatModel.hideChatWindowOverlay(); } _selectedIndex = index; }), diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 9b938a1ce..69bf11de0 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -96,8 +96,8 @@ class _RemotePageState extends State { if (v < 100) { SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); - // [pi.version.isNotEmpty] -> check ready or not,avoid login without soft-keyboard - if (chatWindowOverlayEntry == null && + // [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); } @@ -453,7 +453,7 @@ class _RemotePageState extends State { onPressed: () { gFFI.chatModel .changeCurrentID(ChatModel.clientModeID); - toggleChatOverlay(); + gFFI.chatModel.toggleChatOverlay(); }, ) ]) + diff --git a/flutter/lib/mobile/widgets/overlay.dart b/flutter/lib/mobile/widgets/overlay.dart index 362f62974..976d9bb73 100644 --- a/flutter/lib/mobile/widgets/overlay.dart +++ b/flutter/lib/mobile/widgets/overlay.dart @@ -1,22 +1,23 @@ -import 'package:draggable_float_widget/draggable_float_widget.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; +import '../../models/chat_model.dart'; import '../../models/model.dart'; import '../pages/chat_page.dart'; -OverlayEntry? chatIconOverlayEntry; -OverlayEntry? chatWindowOverlayEntry; - OverlayEntry? mobileActionsOverlayEntry; class DraggableChatWindow extends StatelessWidget { DraggableChatWindow( - {this.position = Offset.zero, required this.width, required this.height}); + {this.position = Offset.zero, + required this.width, + required this.height, + required this.chatModel}); final Offset position; final double width; final double height; + final ChatModel chatModel; @override Widget build(BuildContext context) { @@ -27,7 +28,7 @@ class DraggableChatWindow extends StatelessWidget { height: height, builder: (_, onPanUpdate) { return isIOS - ? ChatPage() + ? ChatPage(chatModel: chatModel) : Scaffold( resizeToAvoidBottomInset: false, appBar: CustomAppBar( @@ -53,13 +54,13 @@ class DraggableChatWindow extends StatelessWidget { children: [ IconButton( onPressed: () { - hideChatWindowOverlay(); + chatModel.hideChatWindowOverlay(); }, icon: Icon(Icons.keyboard_arrow_down)), IconButton( onPressed: () { - hideChatWindowOverlay(); - hideChatIconOverlay(); + chatModel.hideChatWindowOverlay(); + chatModel.hideChatIconOverlay(); }, icon: Icon(Icons.close)) ], @@ -68,7 +69,7 @@ class DraggableChatWindow extends StatelessWidget { ), ), ), - body: ChatPage(), + body: ChatPage(chatModel: chatModel), ); }); } @@ -91,81 +92,6 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { Size get preferredSize => new Size.fromHeight(kToolbarHeight); } -showChatIconOverlay({Offset offset = const Offset(200, 50)}) { - if (chatIconOverlayEntry != null) { - chatIconOverlayEntry!.remove(); - } - if (globalKey.currentState == null || globalKey.currentState!.overlay == null) - return; - final bar = navigationBarKey.currentWidget; - if (bar != null) { - if ((bar as BottomNavigationBar).currentIndex == 1) { - return; - } - } - final globalOverlayState = globalKey.currentState!.overlay!; - - final overlay = OverlayEntry(builder: (context) { - return DraggableFloatWidget( - config: DraggableFloatWidgetBaseConfig( - initPositionYInTop: false, - initPositionYMarginBorder: 100, - borderTopContainTopBar: true, - ), - child: FloatingActionButton( - onPressed: () { - if (chatWindowOverlayEntry == null) { - showChatWindowOverlay(); - } else { - hideChatWindowOverlay(); - } - }, - child: Icon(Icons.message))); - }); - globalOverlayState.insert(overlay); - chatIconOverlayEntry = overlay; -} - -hideChatIconOverlay() { - if (chatIconOverlayEntry != null) { - chatIconOverlayEntry!.remove(); - chatIconOverlayEntry = null; - } -} - -showChatWindowOverlay() { - if (chatWindowOverlayEntry != null) return; - if (globalKey.currentState == null || globalKey.currentState!.overlay == null) - return; - final globalOverlayState = globalKey.currentState!.overlay!; - - final overlay = OverlayEntry(builder: (context) { - return DraggableChatWindow( - position: Offset(20, 80), width: 250, height: 350); - }); - globalOverlayState.insert(overlay); - chatWindowOverlayEntry = overlay; -} - -hideChatWindowOverlay() { - if (chatWindowOverlayEntry != null) { - chatWindowOverlayEntry!.remove(); - chatWindowOverlayEntry = null; - return; - } -} - -toggleChatOverlay() { - if (chatIconOverlayEntry == null || chatWindowOverlayEntry == null) { - gFFI.invokeMethod("enable_soft_keyboard", true); - showChatIconOverlay(); - showChatWindowOverlay(); - } else { - hideChatIconOverlay(); - hideChatWindowOverlay(); - } -} - /// floating buttons of back/home/recent actions for android class DraggableMobileActions extends StatelessWidget { DraggableMobileActions( diff --git a/flutter/lib/models/chat_model.dart b/flutter/lib/models/chat_model.dart index 52f00aa01..9b9f70756 100644 --- a/flutter/lib/models/chat_model.dart +++ b/flutter/lib/models/chat_model.dart @@ -1,8 +1,10 @@ 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 '../../mobile/widgets/overlay.dart'; +import '../common.dart'; import 'model.dart'; class MessageBody { @@ -22,6 +24,14 @@ 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; + final ChatUser me = ChatUser( id: "", firstName: "Me", @@ -51,6 +61,94 @@ 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(); + } + // mobile check navigationBar + final bar = navigationBarKey.currentWidget; + if (bar != null) { + if ((bar as BottomNavigationBar).currentIndex == 1) { + return; + } + } + + final overlayState = _getOverlayState(); + if (overlayState == null) return; + + final overlay = OverlayEntry(builder: (context) { + return DraggableFloatWidget( + config: DraggableFloatWidgetBaseConfig( + initPositionYInTop: false, + initPositionYMarginBorder: 100, + borderTopContainTopBar: true, + ), + child: FloatingActionButton( + onPressed: () { + if (chatWindowOverlayEntry == null) { + showChatWindowOverlay(); + } else { + hideChatWindowOverlay(); + } + }, + child: Icon(Icons.message))); + }); + overlayState.insert(overlay); + chatIconOverlayEntry = overlay; + } + + hideChatIconOverlay() { + if (chatIconOverlayEntry != null) { + chatIconOverlayEntry!.remove(); + chatIconOverlayEntry = null; + } + } + + showChatWindowOverlay() { + if (chatWindowOverlayEntry != null) return; + final overlayState = _getOverlayState(); + if (overlayState == null) return; + final overlay = OverlayEntry(builder: (context) { + return DraggableChatWindow( + position: Offset(20, 80), width: 250, height: 350, chatModel: this); + }); + overlayState.insert(overlay); + chatWindowOverlayEntry = overlay; + } + + hideChatWindowOverlay() { + if (chatWindowOverlayEntry != null) { + chatWindowOverlayEntry!.remove(); + chatWindowOverlayEntry = null; + return; + } + } + + toggleChatOverlay() { + if (chatIconOverlayEntry == null || chatWindowOverlayEntry == null) { + gFFI.invokeMethod("enable_soft_keyboard", true); + showChatIconOverlay(); + showChatWindowOverlay(); + } else { + hideChatIconOverlay(); + hideChatWindowOverlay(); + } + } + changeCurrentID(int id) { if (_messages.containsKey(id)) { _currentID = id; @@ -117,6 +215,7 @@ class ChatModel with ChangeNotifier { close() { hideChatIconOverlay(); hideChatWindowOverlay(); + _overlayState = null; notifyListeners(); } From f62f32788312db2304c60649c84e48f7b6c14bc0 Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 9 Aug 2022 22:35:29 +0800 Subject: [PATCH 0188/2015] tabbar theme Signed-off-by: 21pages --- .../desktop/pages/connection_tab_page.dart | 2 +- .../desktop/pages/file_manager_tab_page.dart | 2 +- .../lib/desktop/widgets/tabbar_widget.dart | 176 ++++++++++++------ 3 files changed, 120 insertions(+), 60 deletions(-) diff --git a/flutter/lib/desktop/pages/connection_tab_page.dart b/flutter/lib/desktop/pages/connection_tab_page.dart index ffe984943..dcbb0ef3e 100644 --- a/flutter/lib/desktop/pages/connection_tab_page.dart +++ b/flutter/lib/desktop/pages/connection_tab_page.dart @@ -9,7 +9,6 @@ import 'package:flutter_hbb/desktop/pages/remote_page.dart'; import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:get/get.dart'; -import 'package:window_manager/window_manager.dart'; import '../../models/model.dart'; @@ -89,6 +88,7 @@ class _ConnectionTabPageState extends State onTabClose: onRemoveId, tabIcon: Icons.desktop_windows_sharp, selected: _selected, + dark: isDarkTheme(), )), Expanded( child: Obx(() => TabBarView( diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index c06dd331d..791d3c068 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -9,7 +9,6 @@ import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; import 'package:flutter_hbb/models/model.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:get/get.dart'; -import 'package:window_manager/window_manager.dart'; /// File Transfer for multi tabs class FileManagerTabPage extends StatefulWidget { @@ -87,6 +86,7 @@ class _FileManagerTabPageState extends State onTabClose: onRemoveId, tabIcon: Icons.file_copy_sharp, selected: _selected, + dark: isDarkTheme(), ), ), Expanded( diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 1f6d1863c..2eafeed85 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -6,18 +6,6 @@ import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:get/get.dart'; -const Color _bgColor = Color.fromARGB(255, 231, 234, 237); -const Color _tabUnselectedColor = Color.fromARGB(255, 240, 240, 240); -const Color _tabHoverColor = Color.fromARGB(255, 245, 245, 245); -const Color _tabSelectedColor = Color.fromARGB(255, 255, 255, 255); -const Color _tabIconColor = MyTheme.accent50; -const Color _tabindicatorColor = _tabIconColor; -const Color _textColor = Color.fromARGB(255, 108, 111, 145); -const Color _iconColor = Color.fromARGB(255, 102, 106, 109); -const Color _iconHoverColor = Colors.black12; -const Color _iconPressedColor = Colors.black26; -const Color _dividerColor = Colors.black12; - const double _kTabBarHeight = kDesktopRemoteTabBarHeight; const double _kTabFixedWidth = 150; const double _kIconSize = 18; @@ -30,6 +18,8 @@ class DesktopTabBar extends StatelessWidget { late final Function(String) onTabClose; late final IconData tabIcon; late final Rx selected; + late final bool dark; + late final _Theme _theme; DesktopTabBar( {Key? key, @@ -37,21 +27,23 @@ class DesktopTabBar extends StatelessWidget { required this.tabs, required this.onTabClose, required this.tabIcon, - required this.selected}) - : super(key: key); + required this.selected, + required this.dark}) + : _theme = dark ? _Theme.dark() : _Theme.light(), + super(key: key); @override Widget build(BuildContext context) { return Container( - color: _bgColor, + color: _theme.bgColor, height: _kTabBarHeight, child: Row( children: [ Flexible( child: Obx(() => TabBar( - indicatorColor: _tabindicatorColor, + indicatorColor: _theme.tabindicatorColor, indicatorSize: TabBarIndicatorSize.tab, - indicatorWeight: 4, + indicatorWeight: 1, labelPadding: const EdgeInsets.symmetric(vertical: 0, horizontal: 0), indicatorPadding: EdgeInsets.zero, @@ -68,22 +60,26 @@ class DesktopTabBar extends StatelessWidget { selected: selected.value, onClose: () { onTabClose(e.value); - // TODO if (e.key <= selected.value) { selected.value = max(0, selected.value - 1); } - controller.value.animateTo(selected.value); + controller.value.animateTo(selected.value, + duration: Duration.zero); }, onSelected: () { selected.value = e.key; - controller.value.animateTo(e.key); + controller.value + .animateTo(e.key, duration: Duration.zero); }, + theme: _theme, )) .toList())), ), Padding( padding: EdgeInsets.only(left: 10), - child: _AddButton(), + child: _AddButton( + theme: _theme, + ), ), ], ), @@ -99,16 +95,18 @@ class _Tab extends StatelessWidget { late final Function() onClose; late final Function() onSelected; final RxBool _hover = false.obs; + late final _Theme theme; - _Tab({ - Key? key, - required this.index, - required this.text, - required this.icon, - required this.selected, - required this.onClose, - required this.onSelected, - }) : super(key: key); + _Tab( + {Key? key, + required this.index, + required this.text, + required this.icon, + required this.selected, + required this.onClose, + required this.onSelected, + required this.theme}) + : super(key: key); @override Widget build(BuildContext context) { @@ -122,10 +120,10 @@ class _Tab extends StatelessWidget { width: _kTabFixedWidth, decoration: BoxDecoration( color: is_selected - ? _tabSelectedColor + ? theme.tabSelectedColor : _hover.value - ? _tabHoverColor - : _tabUnselectedColor, + ? theme.tabHoverColor + : theme.tabUnselectedColor, ), child: Row( children: [ @@ -140,30 +138,36 @@ class _Tab extends StatelessWidget { child: Icon( icon, size: _kIconSize, - color: _tabIconColor, + color: theme.tabIconColor, ), ), Expanded( child: Text( text, - style: const TextStyle(color: _textColor), + style: TextStyle( + color: is_selected + ? theme.selectedTextColor + : theme.unSelectedTextColor), ), ), _CloseButton( tabHovered: _hover.value, + tabSelected: is_selected, onClose: () => onClose(), + theme: theme, ), ])), ), - show_divider - ? VerticalDivider( - width: 1, - indent: _kDividerIndent, - endIndent: _kDividerIndent, - color: _dividerColor, - thickness: 1, - ) - : Container(), + Offstage( + offstage: !show_divider, + child: VerticalDivider( + width: 1, + indent: _kDividerIndent, + endIndent: _kDividerIndent, + color: theme.dividerColor, + thickness: 1, + ), + ) ], ), ), @@ -175,9 +179,11 @@ class _Tab extends StatelessWidget { class _AddButton extends StatelessWidget { final RxBool _hover = false.obs; final RxBool _pressed = false.obs; + late final _Theme theme; _AddButton({ Key? key, + required this.theme, }) : super(key: key); @override @@ -192,14 +198,14 @@ class _AddButton extends StatelessWidget { decoration: ShapeDecoration( shape: const CircleBorder(), color: _pressed.value - ? _iconPressedColor + ? theme.iconPressedBgColor : _hover.value - ? _iconHoverColor + ? theme.iconHoverBgColor : Colors.transparent, ), - child: const Icon( + child: Icon( Icons.add_sharp, - color: _iconColor, + color: theme.unSelectedIconColor, size: _kAddIconSize, ), ))), @@ -209,14 +215,18 @@ class _AddButton extends StatelessWidget { class _CloseButton extends StatelessWidget { final bool tabHovered; + final bool tabSelected; final Function onClose; final RxBool _hover = false.obs; final RxBool _pressed = false.obs; + late final _Theme theme; _CloseButton({ Key? key, required this.tabHovered, + required this.tabSelected, required this.onClose, + required this.theme, }) : super(key: key); @override @@ -224,26 +234,28 @@ class _CloseButton extends StatelessWidget { return Padding( padding: const EdgeInsets.symmetric(horizontal: 5), child: SizedBox( - width: _kIconSize, - child: tabHovered - ? Obx((() => _Hoverable( + width: _kIconSize, + child: Offstage( + offstage: !tabHovered, + child: Obx((() => _Hoverable( onHover: (hover) => _hover.value = hover, onPressed: (pressed) => _pressed.value = pressed, onTapUp: () => onClose(), child: Container( color: _pressed.value - ? _iconPressedColor + ? theme.iconPressedBgColor : _hover.value - ? _iconHoverColor + ? theme.iconHoverBgColor : Colors.transparent, - child: const Icon( + child: Icon( Icons.close, size: _kIconSize, - color: _iconColor, + color: tabSelected + ? theme.selectedIconColor + : theme.unSelectedIconColor, )), - ))) - : Container(), - )); + ))), + ))); } } @@ -278,3 +290,51 @@ class _Hoverable extends StatelessWidget { )); } } + +class _Theme { + late Color bgColor; + late Color tabUnselectedColor; + late Color tabHoverColor; + late Color tabSelectedColor; + late Color tabIconColor; + late Color tabindicatorColor; + late Color selectedTextColor; + late Color unSelectedTextColor; + late Color selectedIconColor; + late Color unSelectedIconColor; + late Color iconHoverBgColor; + late Color iconPressedBgColor; + late Color dividerColor; + + _Theme.light() { + bgColor = Color.fromARGB(255, 253, 253, 253); + tabUnselectedColor = Color.fromARGB(255, 253, 253, 253); + tabHoverColor = Color.fromARGB(255, 245, 245, 245); + tabSelectedColor = MyTheme.grayBg; + tabIconColor = MyTheme.accent50; + tabindicatorColor = MyTheme.grayBg; + selectedTextColor = Color.fromARGB(255, 26, 26, 26); + unSelectedTextColor = Color.fromARGB(255, 96, 96, 96); + selectedIconColor = Color.fromARGB(255, 26, 26, 26); + unSelectedIconColor = Color.fromARGB(255, 96, 96, 96); + iconHoverBgColor = Color.fromARGB(255, 224, 224, 224); + iconPressedBgColor = Color.fromARGB(255, 215, 215, 215); + dividerColor = Color.fromARGB(255, 238, 238, 238); + } + + _Theme.dark() { + bgColor = Color.fromARGB(255, 50, 50, 50); + tabUnselectedColor = Color.fromARGB(255, 50, 50, 50); + tabHoverColor = Color.fromARGB(255, 59, 59, 59); + tabSelectedColor = MyTheme.canvasColor; + tabIconColor = Color.fromARGB(255, 84, 197, 248); + tabindicatorColor = MyTheme.canvasColor; + selectedTextColor = Color.fromARGB(255, 255, 255, 255); + unSelectedTextColor = Color.fromARGB(255, 207, 207, 207); + selectedIconColor = Color.fromARGB(255, 215, 215, 215); + unSelectedIconColor = Color.fromARGB(255, 255, 255, 255); + iconHoverBgColor = Color.fromARGB(255, 67, 67, 67); + iconPressedBgColor = Color.fromARGB(255, 73, 73, 73); + dividerColor = Color.fromARGB(255, 64, 64, 64); + } +} From 1440d263760cc597614902da05176b1b722e98ed Mon Sep 17 00:00:00 2001 From: 21pages Date: Wed, 10 Aug 2022 16:40:04 +0800 Subject: [PATCH 0189/2015] tabbar: material style Signed-off-by: 21pages --- .../lib/desktop/widgets/tabbar_widget.dart | 313 +++++++----------- 1 file changed, 119 insertions(+), 194 deletions(-) diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 2eafeed85..420267b44 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -35,53 +35,53 @@ class DesktopTabBar extends StatelessWidget { @override Widget build(BuildContext context) { return Container( - color: _theme.bgColor, height: _kTabBarHeight, - child: Row( - children: [ - Flexible( - child: Obx(() => TabBar( - indicatorColor: _theme.tabindicatorColor, - indicatorSize: TabBarIndicatorSize.tab, - indicatorWeight: 1, - labelPadding: - const EdgeInsets.symmetric(vertical: 0, horizontal: 0), - indicatorPadding: EdgeInsets.zero, - isScrollable: true, - physics: BouncingScrollPhysics(), - controller: controller.value, - tabs: tabs - .asMap() - .entries - .map((e) => _Tab( - index: e.key, - text: e.value, - icon: tabIcon, - selected: selected.value, - onClose: () { - onTabClose(e.value); - if (e.key <= selected.value) { - selected.value = max(0, selected.value - 1); - } - controller.value.animateTo(selected.value, - duration: Duration.zero); - }, - onSelected: () { - selected.value = e.key; - controller.value - .animateTo(e.key, duration: Duration.zero); - }, - theme: _theme, - )) - .toList())), - ), - Padding( - padding: EdgeInsets.only(left: 10), - child: _AddButton( - theme: _theme, + child: Scaffold( + backgroundColor: _theme.bgColor, + body: Row( + children: [ + Flexible( + child: Obx(() => TabBar( + indicator: BoxDecoration(), + indicatorColor: Colors.transparent, + labelPadding: + const EdgeInsets.symmetric(vertical: 0, horizontal: 0), + isScrollable: true, + physics: BouncingScrollPhysics(), + controller: controller.value, + tabs: tabs + .asMap() + .entries + .map((e) => _Tab( + index: e.key, + text: e.value, + icon: tabIcon, + selected: selected.value, + onClose: () { + onTabClose(e.value); + if (e.key <= selected.value) { + selected.value = max(0, selected.value - 1); + } + controller.value.animateTo(selected.value, + duration: Duration.zero); + }, + onSelected: () { + selected.value = e.key; + controller.value + .animateTo(e.key, duration: Duration.zero); + }, + theme: _theme, + )) + .toList())), ), - ), - ], + Padding( + padding: EdgeInsets.only(left: 10), + child: _AddButton( + theme: _theme, + ), + ), + ], + ), ), ); } @@ -112,73 +112,63 @@ class _Tab extends StatelessWidget { Widget build(BuildContext context) { bool is_selected = index == selected; bool show_divider = index != selected - 1 && index != selected; - return Obx( - (() => _Hoverable( - onHover: (hover) => _hover.value = hover, - onTapUp: () => onSelected(), - child: Container( - width: _kTabFixedWidth, - decoration: BoxDecoration( - color: is_selected - ? theme.tabSelectedColor - : _hover.value - ? theme.tabHoverColor - : theme.tabUnselectedColor, - ), - child: Row( - children: [ - Expanded( - child: Tab( - key: this.key, - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Padding( - padding: EdgeInsets.symmetric(horizontal: 5), - child: Icon( - icon, - size: _kIconSize, - color: theme.tabIconColor, - ), - ), - Expanded( - child: Text( - text, - style: TextStyle( - color: is_selected - ? theme.selectedTextColor - : theme.unSelectedTextColor), - ), - ), - _CloseButton( - tabHovered: _hover.value, - tabSelected: is_selected, - onClose: () => onClose(), - theme: theme, - ), - ])), - ), - Offstage( - offstage: !show_divider, - child: VerticalDivider( - width: 1, - indent: _kDividerIndent, - endIndent: _kDividerIndent, - color: theme.dividerColor, - thickness: 1, - ), - ) - ], - ), + return Ink( + width: _kTabFixedWidth, + color: is_selected ? theme.tabSelectedColor : null, + child: InkWell( + onHover: (hover) => _hover.value = hover, + onTap: () => onSelected(), + child: Row( + children: [ + Expanded( + child: Tab( + key: this.key, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Padding( + padding: EdgeInsets.symmetric(horizontal: 5), + child: Icon( + icon, + size: _kIconSize, + color: theme.tabIconColor, + ), + ), + Expanded( + child: Text( + text, + style: TextStyle( + color: is_selected + ? theme.selectedTextColor + : theme.unSelectedTextColor), + ), + ), + Obx((() => _CloseButton( + tabHovered: _hover.value, + tabSelected: is_selected, + onClose: () => onClose(), + theme: theme, + ))), + ])), ), - )), + Offstage( + offstage: !show_divider, + child: VerticalDivider( + width: 1, + indent: _kDividerIndent, + endIndent: _kDividerIndent, + color: theme.dividerColor, + thickness: 1, + ), + ) + ], + ), + ), ); } } class _AddButton extends StatelessWidget { - final RxBool _hover = false.obs; - final RxBool _pressed = false.obs; late final _Theme theme; _AddButton({ @@ -188,27 +178,18 @@ class _AddButton extends StatelessWidget { @override Widget build(BuildContext context) { - return _Hoverable( - onHover: (hover) => _hover.value = hover, - onPressed: (pressed) => _pressed.value = pressed, - onTapUp: () => - rustDeskWinManager.call(WindowType.Main, "main_window_on_top", ""), - child: Obx((() => Container( - height: _kTabBarHeight, - decoration: ShapeDecoration( - shape: const CircleBorder(), - color: _pressed.value - ? theme.iconPressedBgColor - : _hover.value - ? theme.iconHoverBgColor - : Colors.transparent, - ), - child: Icon( - Icons.add_sharp, - color: theme.unSelectedIconColor, - size: _kAddIconSize, - ), - ))), + return Ink( + height: _kTabBarHeight, + child: InkWell( + customBorder: const CircleBorder(), + onTap: () => + rustDeskWinManager.call(WindowType.Main, "main_window_on_top", ""), + child: Icon( + Icons.add_sharp, + size: _kAddIconSize, + color: theme.unSelectedIconColor, + ), + ), ); } } @@ -217,8 +198,6 @@ class _CloseButton extends StatelessWidget { final bool tabHovered; final bool tabSelected; final Function onClose; - final RxBool _hover = false.obs; - final RxBool _pressed = false.obs; late final _Theme theme; _CloseButton({ @@ -237,104 +216,50 @@ class _CloseButton extends StatelessWidget { width: _kIconSize, child: Offstage( offstage: !tabHovered, - child: Obx((() => _Hoverable( - onHover: (hover) => _hover.value = hover, - onPressed: (pressed) => _pressed.value = pressed, - onTapUp: () => onClose(), - child: Container( - color: _pressed.value - ? theme.iconPressedBgColor - : _hover.value - ? theme.iconHoverBgColor - : Colors.transparent, - child: Icon( - Icons.close, - size: _kIconSize, - color: tabSelected - ? theme.selectedIconColor - : theme.unSelectedIconColor, - )), - ))), + child: InkWell( + customBorder: RoundedRectangleBorder(), + onTap: () => onClose(), + child: Icon( + Icons.close, + size: _kIconSize, + color: tabSelected + ? theme.selectedIconColor + : theme.unSelectedIconColor, + ), + ), ))); } } -class _Hoverable extends StatelessWidget { - final Widget child; - final Function(bool hover) onHover; - final Function(bool pressed)? onPressed; - final Function()? onTapUp; - - const _Hoverable( - {Key? key, - required this.child, - required this.onHover, - this.onPressed, - this.onTapUp}) - : super(key: key); - - @override - Widget build(BuildContext context) { - return MouseRegion( - onEnter: (_) => onHover(true), - onExit: (_) => onHover(false), - child: onPressed == null && onTapUp == null - ? child - : GestureDetector( - onTapDown: (details) => onPressed?.call(true), - onTapUp: (details) { - onPressed?.call(false); - onTapUp?.call(); - }, - child: child, - )); - } -} - class _Theme { late Color bgColor; - late Color tabUnselectedColor; - late Color tabHoverColor; late Color tabSelectedColor; late Color tabIconColor; - late Color tabindicatorColor; late Color selectedTextColor; late Color unSelectedTextColor; late Color selectedIconColor; late Color unSelectedIconColor; - late Color iconHoverBgColor; - late Color iconPressedBgColor; late Color dividerColor; _Theme.light() { bgColor = Color.fromARGB(255, 253, 253, 253); - tabUnselectedColor = Color.fromARGB(255, 253, 253, 253); - tabHoverColor = Color.fromARGB(255, 245, 245, 245); tabSelectedColor = MyTheme.grayBg; tabIconColor = MyTheme.accent50; - tabindicatorColor = MyTheme.grayBg; selectedTextColor = Color.fromARGB(255, 26, 26, 26); unSelectedTextColor = Color.fromARGB(255, 96, 96, 96); selectedIconColor = Color.fromARGB(255, 26, 26, 26); unSelectedIconColor = Color.fromARGB(255, 96, 96, 96); - iconHoverBgColor = Color.fromARGB(255, 224, 224, 224); - iconPressedBgColor = Color.fromARGB(255, 215, 215, 215); dividerColor = Color.fromARGB(255, 238, 238, 238); } _Theme.dark() { bgColor = Color.fromARGB(255, 50, 50, 50); - tabUnselectedColor = Color.fromARGB(255, 50, 50, 50); - tabHoverColor = Color.fromARGB(255, 59, 59, 59); tabSelectedColor = MyTheme.canvasColor; tabIconColor = Color.fromARGB(255, 84, 197, 248); - tabindicatorColor = MyTheme.canvasColor; selectedTextColor = Color.fromARGB(255, 255, 255, 255); unSelectedTextColor = Color.fromARGB(255, 207, 207, 207); selectedIconColor = Color.fromARGB(255, 215, 215, 215); unSelectedIconColor = Color.fromARGB(255, 255, 255, 255); - iconHoverBgColor = Color.fromARGB(255, 67, 67, 67); - iconPressedBgColor = Color.fromARGB(255, 73, 73, 73); dividerColor = Color.fromARGB(255, 64, 64, 64); } } From c799fb18577b1e92b44e39fdabb77b5e2d67f71c Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 11 Aug 2022 16:03:04 +0800 Subject: [PATCH 0190/2015] refactor tabbar: Homepage adaptation 1. remove redundant MaterialApp in GetMaterialApp 2. unified background color Signed-off-by: 21pages --- flutter/lib/consts.dart | 2 + .../lib/desktop/pages/connection_page.dart | 1 - .../desktop/pages/connection_tab_page.dart | 7 +- .../lib/desktop/pages/desktop_home_page.dart | 44 +--- .../desktop/pages/desktop_setting_page.dart | 15 ++ .../lib/desktop/pages/desktop_tab_page.dart | 88 +++++++ .../lib/desktop/pages/file_manager_page.dart | 1 - .../desktop/pages/file_manager_tab_page.dart | 6 +- flutter/lib/desktop/pages/remote_page.dart | 34 ++- .../screen/desktop_file_transfer_screen.dart | 26 +- .../desktop/screen/desktop_remote_screen.dart | 35 +-- .../lib/desktop/widgets/tabbar_widget.dart | 222 ++++++++++-------- flutter/lib/main.dart | 32 ++- 13 files changed, 307 insertions(+), 206 deletions(-) create mode 100644 flutter/lib/desktop/pages/desktop_setting_page.dart create mode 100644 flutter/lib/desktop/pages/desktop_tab_page.dart diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 66653a746..662f7cbd2 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -2,3 +2,5 @@ const double kDesktopRemoteTabBarHeight = 48.0; const String kAppTypeMain = "main"; const String kAppTypeDesktopRemote = "remote"; const String kAppTypeDesktopFileTransfer = "file transfer"; +const String kTabLabelHomePage = "Home"; +const String kTabLabelSettingPage = "Settings"; diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 7a80a64a1..182f1d0b4 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -57,7 +57,6 @@ class _ConnectionPageState extends State { @override Widget build(BuildContext context) { return Container( - decoration: BoxDecoration(color: isDarkTheme() ? null : MyTheme.grayBg), child: Column( mainAxisAlignment: MainAxisAlignment.start, mainAxisSize: MainAxisSize.max, diff --git a/flutter/lib/desktop/pages/connection_tab_page.dart b/flutter/lib/desktop/pages/connection_tab_page.dart index dcbb0ef3e..5bd7e2469 100644 --- a/flutter/lib/desktop/pages/connection_tab_page.dart +++ b/flutter/lib/desktop/pages/connection_tab_page.dart @@ -84,11 +84,14 @@ class _ConnectionTabPageState extends State children: [ Obx(() => DesktopTabBar( controller: tabController, - tabs: connectionIds.toList(), + tabs: connectionIds + .map((e) => + TabInfo(label: e, icon: Icons.desktop_windows_sharp)) + .toList(), onTabClose: onRemoveId, - tabIcon: Icons.desktop_windows_sharp, selected: _selected, dark: isDarkTheme(), + mainTab: false, )), Expanded( child: Obx(() => TabBarView( diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 8496b0eda..770841f09 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -6,7 +6,6 @@ import 'package:flutter/material.dart' hide MenuItem; import 'package:flutter/services.dart'; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/desktop/pages/connection_page.dart'; -import 'package:flutter_hbb/desktop/widgets/titlebar_widget.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/server_model.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; @@ -46,38 +45,17 @@ class _DesktopHomePageState extends State @override Widget build(BuildContext context) { - return Scaffold( - body: Column( - children: [ - DesktopTitleBar( - child: Center( - child: Text( - "RustDesk", - style: TextStyle( - color: Colors.white, - fontSize: 20, - fontWeight: FontWeight.bold), - ), - ), - ), - Expanded( - child: Container( - child: Row( - children: [ - Flexible( - child: buildServerInfo(context), - flex: 1, - ), - Flexible( - child: buildServerBoard(context), - flex: 4, - ), - ], - ), - ), - ), - ], - ), + return Row( + children: [ + Flexible( + child: buildServerInfo(context), + flex: 1, + ), + Flexible( + child: buildServerBoard(context), + flex: 4, + ), + ], ); } diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart new file mode 100644 index 000000000..4d9a58f3b --- /dev/null +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -0,0 +1,15 @@ +import 'package:flutter/cupertino.dart'; + +class DesktopSettingPage extends StatefulWidget { + DesktopSettingPage({Key? key}) : super(key: key); + + @override + State createState() => _DesktopSettingPageState(); +} + +class _DesktopSettingPageState extends State { + @override + Widget build(BuildContext context) { + return Text("Settings"); + } +} diff --git a/flutter/lib/desktop/pages/desktop_tab_page.dart b/flutter/lib/desktop/pages/desktop_tab_page.dart new file mode 100644 index 000000000..02ef0fea3 --- /dev/null +++ b/flutter/lib/desktop/pages/desktop_tab_page.dart @@ -0,0 +1,88 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/consts.dart'; +import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart'; +import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart'; +import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; +import 'package:get/get.dart'; + +class DesktopTabPage extends StatefulWidget { + const DesktopTabPage({Key? key}) : super(key: key); + + @override + State createState() => _DesktopTabPageState(); +} + +class _DesktopTabPageState extends State + with TickerProviderStateMixin { + late Rx tabController; + late RxList tabs; + static final Rx _selected = 0.obs; + + @override + void initState() { + super.initState(); + tabs = RxList.from([ + TabInfo(label: kTabLabelHomePage, icon: Icons.home_sharp, closable: false) + ], growable: true); + tabController = + TabController(length: tabs.length, vsync: this, initialIndex: 0).obs; + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Column( + children: [ + Obx((() => DesktopTabBar( + controller: tabController, + tabs: tabs.toList(), + onTabClose: onTabClose, + selected: _selected, + dark: isDarkTheme(), + mainTab: true, + onMenu: onTabbarMenu, + ))), + Obx((() => Expanded( + child: TabBarView( + controller: tabController.value, + children: tabs.map((tab) { + switch (tab.label) { + case kTabLabelHomePage: + return DesktopHomePage(key: ValueKey(tab.label)); + case kTabLabelSettingPage: + return DesktopSettingPage(key: ValueKey(tab.label)); + default: + return Container(); + } + }).toList()), + ))), + ], + ), + ); + } + + void onTabClose(String label) { + tabs.removeWhere((tab) => tab.label == label); + tabController.value = TabController( + length: tabs.length, + vsync: this, + initialIndex: max(0, tabs.length - 1)); + } + + void onTabbarMenu() { + int index = tabs.indexWhere((tab) => tab.label == kTabLabelSettingPage); + if (index >= 0) { + tabController.value.animateTo(index, duration: Duration.zero); + _selected.value = index; + } else { + tabs.add(TabInfo(label: kTabLabelSettingPage, icon: Icons.settings)); + tabController.value = TabController( + length: tabs.length, vsync: this, initialIndex: tabs.length - 1); + tabController.value.animateTo(tabs.length - 1, duration: Duration.zero); + _selected.value = tabs.length - 1; + } + } +} diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 581a38a3a..22d46c146 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -73,7 +73,6 @@ class _FileManagerPageState extends State return false; }, child: Scaffold( - backgroundColor: isDarkTheme() ? MyTheme.dark : MyTheme.grayBg, body: Row( children: [ Flexible(flex: 3, child: body(isLocal: true)), diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index 791d3c068..c4348fad0 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -82,11 +82,13 @@ class _FileManagerTabPageState extends State Obx( () => DesktopTabBar( controller: tabController, - tabs: connectionIds.toList(), + tabs: connectionIds + .map((e) => TabInfo(label: e, icon: Icons.file_copy_sharp)) + .toList(), onTabClose: onRemoveId, - tabIcon: Icons.file_copy_sharp, selected: _selected, dark: isDarkTheme(), + mainTab: false, ), ), Expanded( diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index fc94d5be8..01744e8e7 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -263,7 +263,6 @@ class _RemotePageState extends State OverlayEntry(builder: (context) { _ffi.chatModel.setOverlayState(Overlay.of(context)); return Container( - color: Colors.black, child: getRawPointerAndKeyBody(getBodyForDesktop(keyboard))); }) ], @@ -500,25 +499,20 @@ class _RemotePageState extends State Widget getBodyForDesktop(bool keyboard) { var paints = [ - MouseRegion( - onEnter: (evt) { - bind.hostStopSystemKeyPropagate(stopped: false); - }, - onExit: (evt) { - bind.hostStopSystemKeyPropagate(stopped: true); - }, - child: Container( - color: MyTheme.canvasColor, - child: LayoutBuilder(builder: (context, constraints) { - Future.delayed(Duration.zero, () { - Provider.of(context, listen: false) - .updateViewStyle(); - }); - return ImagePaint( - id: widget.id, - ); - }), - )) + MouseRegion(onEnter: (evt) { + bind.hostStopSystemKeyPropagate(stopped: false); + }, onExit: (evt) { + bind.hostStopSystemKeyPropagate(stopped: true); + }, child: Container( + child: LayoutBuilder(builder: (context, constraints) { + Future.delayed(Duration.zero, () { + Provider.of(context, listen: false).updateViewStyle(); + }); + return ImagePaint( + id: widget.id, + ); + }), + )) ]; final cursor = bind.getSessionToggleOptionSync( id: widget.id, arg: 'show-remote-cursor'); diff --git a/flutter/lib/desktop/screen/desktop_file_transfer_screen.dart b/flutter/lib/desktop/screen/desktop_file_transfer_screen.dart index 06a71981e..03230b0b0 100644 --- a/flutter/lib/desktop/screen/desktop_file_transfer_screen.dart +++ b/flutter/lib/desktop/screen/desktop_file_transfer_screen.dart @@ -20,27 +20,11 @@ class DesktopFileTransferScreen extends StatelessWidget { ChangeNotifierProvider.value(value: gFFI.cursorModel), ChangeNotifierProvider.value(value: gFFI.canvasModel), ], - child: MaterialApp( - navigatorKey: globalKey, - debugShowCheckedModeBanner: false, - title: 'RustDesk - File Transfer', - theme: ThemeData( - primarySwatch: Colors.blue, - visualDensity: VisualDensity.adaptivePlatformDensity, - ), - home: FileManagerTabPage( - params: params, - ), - navigatorObservers: [ - // FirebaseAnalyticsObserver(analytics: analytics), - FlutterSmartDialog.observer - ], - builder: FlutterSmartDialog.init( - builder: isAndroid - ? (_, child) => AccessibilityListener( - child: child, - ) - : null)), + child: Scaffold( + body: FileManagerTabPage( + params: params, + ), + ), ); } } diff --git a/flutter/lib/desktop/screen/desktop_remote_screen.dart b/flutter/lib/desktop/screen/desktop_remote_screen.dart index c5e5ecbfa..95f6abed5 100644 --- a/flutter/lib/desktop/screen/desktop_remote_screen.dart +++ b/flutter/lib/desktop/screen/desktop_remote_screen.dart @@ -13,33 +13,16 @@ class DesktopRemoteScreen extends StatelessWidget { @override Widget build(BuildContext context) { return MultiProvider( - providers: [ - ChangeNotifierProvider.value(value: gFFI.ffiModel), - ChangeNotifierProvider.value(value: gFFI.imageModel), - ChangeNotifierProvider.value(value: gFFI.cursorModel), - ChangeNotifierProvider.value(value: gFFI.canvasModel), - ], - child: MaterialApp( - navigatorKey: globalKey, - debugShowCheckedModeBanner: false, - title: 'RustDesk - Remote Desktop', - theme: ThemeData( - primarySwatch: Colors.blue, - visualDensity: VisualDensity.adaptivePlatformDensity, - ), - home: ConnectionTabPage( + providers: [ + ChangeNotifierProvider.value(value: gFFI.ffiModel), + ChangeNotifierProvider.value(value: gFFI.imageModel), + ChangeNotifierProvider.value(value: gFFI.cursorModel), + ChangeNotifierProvider.value(value: gFFI.canvasModel), + ], + child: Scaffold( + body: ConnectionTabPage( params: params, ), - navigatorObservers: [ - // FirebaseAnalyticsObserver(analytics: analytics), - FlutterSmartDialog.observer - ], - builder: FlutterSmartDialog.init( - builder: isAndroid - ? (_, child) => AccessibilityListener( - child: child, - ) - : null)), - ); + )); } } diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 420267b44..41dca26c2 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -12,76 +12,109 @@ const double _kIconSize = 18; const double _kDividerIndent = 10; const double _kAddIconSize = _kTabBarHeight - 15; +class TabInfo { + late final String label; + late final IconData icon; + late final bool closable; + + TabInfo({required this.label, required this.icon, this.closable = true}); +} + class DesktopTabBar extends StatelessWidget { late final Rx controller; - late final List tabs; + late final List tabs; late final Function(String) onTabClose; - late final IconData tabIcon; late final Rx selected; late final bool dark; late final _Theme _theme; + late final bool mainTab; + late final Function()? onMenu; - DesktopTabBar( - {Key? key, - required this.controller, - required this.tabs, - required this.onTabClose, - required this.tabIcon, - required this.selected, - required this.dark}) - : _theme = dark ? _Theme.dark() : _Theme.light(), + DesktopTabBar({ + Key? key, + required this.controller, + required this.tabs, + required this.onTabClose, + required this.selected, + required this.dark, + required this.mainTab, + this.onMenu, + }) : _theme = dark ? _Theme.dark() : _Theme.light(), super(key: key); @override Widget build(BuildContext context) { return Container( height: _kTabBarHeight, - child: Scaffold( - backgroundColor: _theme.bgColor, - body: Row( - children: [ - Flexible( - child: Obx(() => TabBar( - indicator: BoxDecoration(), - indicatorColor: Colors.transparent, - labelPadding: - const EdgeInsets.symmetric(vertical: 0, horizontal: 0), - isScrollable: true, - physics: BouncingScrollPhysics(), - controller: controller.value, - tabs: tabs - .asMap() - .entries - .map((e) => _Tab( - index: e.key, - text: e.value, - icon: tabIcon, - selected: selected.value, - onClose: () { - onTabClose(e.value); - if (e.key <= selected.value) { - selected.value = max(0, selected.value - 1); - } - controller.value.animateTo(selected.value, - duration: Duration.zero); - }, - onSelected: () { - selected.value = e.key; - controller.value - .animateTo(e.key, duration: Duration.zero); - }, - theme: _theme, - )) - .toList())), + child: Row( + children: [ + Expanded( + child: Row( + children: [ + Offstage( + offstage: !mainTab, + child: Row(children: [ + Image.asset('assets/logo.ico'), + Text("RustDesk"), + ]).paddingSymmetric(horizontal: 12, vertical: 5), + ), + Flexible( + child: Obx(() => TabBar( + indicator: BoxDecoration(), + indicatorColor: Colors.transparent, + labelPadding: const EdgeInsets.symmetric( + vertical: 0, horizontal: 0), + isScrollable: true, + indicatorWeight: 0.1, + physics: BouncingScrollPhysics(), + controller: controller.value, + tabs: tabs.asMap().entries.map((e) { + int index = e.key; + String label = e.value.label; + + return _Tab( + index: index, + label: label, + icon: e.value.icon, + closable: e.value.closable, + selected: selected.value, + onClose: () { + onTabClose(label); + if (index <= selected.value) { + selected.value = max(0, selected.value - 1); + } + controller.value.animateTo(selected.value, + duration: Duration.zero); + }, + onSelected: () { + selected.value = index; + controller.value + .animateTo(index, duration: Duration.zero); + }, + theme: _theme, + ); + }).toList())), + ), + Offstage( + offstage: mainTab, + child: _AddButton( + theme: _theme, + ).paddingOnly(left: 10), + ) + ], ), - Padding( - padding: EdgeInsets.only(left: 10), - child: _AddButton( - theme: _theme, + ), + Offstage( + offstage: onMenu == null, + child: InkWell( + child: Icon( + Icons.menu, + color: _theme.unSelectedIconColor, ), - ), - ], - ), + onTap: () => onMenu?.call(), + ).paddingOnly(right: 10), + ) + ], ), ); } @@ -89,8 +122,9 @@ class DesktopTabBar extends StatelessWidget { class _Tab extends StatelessWidget { late final int index; - late final String text; + late final String label; late final IconData icon; + late final bool closable; late final int selected; late final Function() onClose; late final Function() onSelected; @@ -100,8 +134,9 @@ class _Tab extends StatelessWidget { _Tab( {Key? key, required this.index, - required this.text, + required this.label, required this.icon, + required this.closable, required this.selected, required this.onClose, required this.onSelected, @@ -114,7 +149,6 @@ class _Tab extends StatelessWidget { bool show_divider = index != selected - 1 && index != selected; return Ink( width: _kTabFixedWidth, - color: is_selected ? theme.tabSelectedColor : null, child: InkWell( onHover: (hover) => _hover.value = hover, onTap: () => onSelected(), @@ -126,17 +160,16 @@ class _Tab extends StatelessWidget { child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - Padding( - padding: EdgeInsets.symmetric(horizontal: 5), - child: Icon( - icon, - size: _kIconSize, - color: theme.tabIconColor, - ), - ), + Icon( + icon, + size: _kIconSize, + color: is_selected + ? theme.selectedtabIconColor + : theme.unSelectedtabIconColor, + ).paddingSymmetric(horizontal: 5), Expanded( child: Text( - text, + label, style: TextStyle( color: is_selected ? theme.selectedTextColor @@ -144,7 +177,7 @@ class _Tab extends StatelessWidget { ), ), Obx((() => _CloseButton( - tabHovered: _hover.value, + visiable: _hover.value && closable, tabSelected: is_selected, onClose: () => onClose(), theme: theme, @@ -195,14 +228,14 @@ class _AddButton extends StatelessWidget { } class _CloseButton extends StatelessWidget { - final bool tabHovered; + final bool visiable; final bool tabSelected; final Function onClose; late final _Theme theme; _CloseButton({ Key? key, - required this.tabHovered, + required this.visiable, required this.tabSelected, required this.onClose, required this.theme, @@ -210,31 +243,28 @@ class _CloseButton extends StatelessWidget { @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 5), - child: SizedBox( - width: _kIconSize, - child: Offstage( - offstage: !tabHovered, - child: InkWell( - customBorder: RoundedRectangleBorder(), - onTap: () => onClose(), - child: Icon( - Icons.close, - size: _kIconSize, - color: tabSelected - ? theme.selectedIconColor - : theme.unSelectedIconColor, - ), - ), - ))); + return SizedBox( + width: _kIconSize, + child: Offstage( + offstage: !visiable, + child: InkWell( + customBorder: RoundedRectangleBorder(), + onTap: () => onClose(), + child: Icon( + Icons.close, + size: _kIconSize, + color: tabSelected + ? theme.selectedIconColor + : theme.unSelectedIconColor, + ), + ), + )).paddingSymmetric(horizontal: 5); } } class _Theme { - late Color bgColor; - late Color tabSelectedColor; - late Color tabIconColor; + late Color unSelectedtabIconColor; + late Color selectedtabIconColor; late Color selectedTextColor; late Color unSelectedTextColor; late Color selectedIconColor; @@ -242,9 +272,8 @@ class _Theme { late Color dividerColor; _Theme.light() { - bgColor = Color.fromARGB(255, 253, 253, 253); - tabSelectedColor = MyTheme.grayBg; - tabIconColor = MyTheme.accent50; + unSelectedtabIconColor = Color.fromARGB(255, 162, 203, 241); + selectedtabIconColor = MyTheme.accent; selectedTextColor = Color.fromARGB(255, 26, 26, 26); unSelectedTextColor = Color.fromARGB(255, 96, 96, 96); selectedIconColor = Color.fromARGB(255, 26, 26, 26); @@ -253,9 +282,8 @@ class _Theme { } _Theme.dark() { - bgColor = Color.fromARGB(255, 50, 50, 50); - tabSelectedColor = MyTheme.canvasColor; - tabIconColor = Color.fromARGB(255, 84, 197, 248); + unSelectedtabIconColor = Color.fromARGB(255, 30, 65, 98); + selectedtabIconColor = MyTheme.accent; selectedTextColor = Color.fromARGB(255, 255, 255, 255); unSelectedTextColor = Color.fromARGB(255, 207, 207, 207); selectedIconColor = Color.fromARGB(255, 215, 215, 215); diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index bfae7e097..000202f65 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -1,7 +1,7 @@ import 'dart:convert'; import 'package:flutter/material.dart'; -import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart'; +import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart'; import 'package:flutter_hbb/desktop/screen/desktop_file_transfer_screen.dart'; import 'package:flutter_hbb/desktop/screen/desktop_remote_screen.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; @@ -86,18 +86,44 @@ void runMainApp(bool startService) async { void runRemoteScreen(Map argument) async { await initEnv(kAppTypeDesktopRemote); runApp(GetMaterialApp( + navigatorKey: globalKey, + debugShowCheckedModeBanner: false, + title: 'RustDesk - Remote Desktop', theme: getCurrentTheme(), home: DesktopRemoteScreen( params: argument, ), + navigatorObservers: [ + // FirebaseAnalyticsObserver(analytics: analytics), + FlutterSmartDialog.observer + ], + builder: FlutterSmartDialog.init( + builder: isAndroid + ? (_, child) => AccessibilityListener( + child: child, + ) + : null), )); } void runFileTransferScreen(Map argument) async { await initEnv(kAppTypeDesktopFileTransfer); runApp(GetMaterialApp( + navigatorKey: globalKey, + debugShowCheckedModeBanner: false, + title: 'RustDesk - File Transfer', theme: getCurrentTheme(), - home: DesktopFileTransferScreen(params: argument))); + home: DesktopFileTransferScreen(params: argument), + navigatorObservers: [ + // FirebaseAnalyticsObserver(analytics: analytics), + FlutterSmartDialog.observer + ], + builder: FlutterSmartDialog.init( + builder: isAndroid + ? (_, child) => AccessibilityListener( + child: child, + ) + : null))); } class App extends StatelessWidget { @@ -121,7 +147,7 @@ class App extends StatelessWidget { title: 'RustDesk', theme: getCurrentTheme(), home: isDesktop - ? DesktopHomePage() + ? DesktopTabPage() : !isAndroid ? WebHomePage() : HomePage(), From 94353cf90b44e34518cd1dcb343dbfab626391a2 Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 11 Aug 2022 18:08:35 +0800 Subject: [PATCH 0191/2015] unify tab logic Signed-off-by: 21pages --- .../desktop/pages/connection_tab_page.dart | 70 +++++++------------ .../lib/desktop/pages/desktop_tab_page.dart | 41 ++++------- .../desktop/pages/file_manager_tab_page.dart | 69 ++++++------------ .../lib/desktop/widgets/tabbar_widget.dart | 55 +++++++++++---- 4 files changed, 103 insertions(+), 132 deletions(-) diff --git a/flutter/lib/desktop/pages/connection_tab_page.dart b/flutter/lib/desktop/pages/connection_tab_page.dart index 5bd7e2469..a86afb683 100644 --- a/flutter/lib/desktop/pages/connection_tab_page.dart +++ b/flutter/lib/desktop/pages/connection_tab_page.dart @@ -1,5 +1,4 @@ import 'dart:convert'; -import 'dart:math'; import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/material.dart'; @@ -25,24 +24,23 @@ class _ConnectionTabPageState extends State with TickerProviderStateMixin { // refactor List when using multi-tab // this singleton is only for test - var connectionIds = RxList.empty(growable: true); - var initialIndex = 0; + RxList tabs = RxList.empty(growable: true); late Rx tabController; static final Rx _selected = 0.obs; + IconData icon = Icons.desktop_windows_sharp; var connectionMap = RxList.empty(growable: true); _ConnectionTabPageState(Map params) { if (params['id'] != null) { - connectionIds.add(params['id']); + tabs.add(TabInfo(label: params['id'], icon: icon)); } } @override void initState() { super.initState(); - tabController = - TabController(length: connectionIds.length, vsync: this).obs; + tabController = TabController(length: tabs.length, vsync: this).obs; rustDeskWinManager.setMethodHandler((call, fromWindowId) async { print( "call ${call.method} with args ${call.arguments} from window ${fromWindowId}"); @@ -51,23 +49,13 @@ class _ConnectionTabPageState extends State final args = jsonDecode(call.arguments); final id = args['id']; window_on_top(windowId()); - final indexOf = connectionIds.indexOf(id); - if (indexOf >= 0) { - initialIndex = indexOf; - tabController.value.animateTo(initialIndex, duration: Duration.zero); - } else { - connectionIds.add(id); - initialIndex = connectionIds.length - 1; - tabController.value = TabController( - length: connectionIds.length, - vsync: this, - initialIndex: initialIndex); - } - _selected.value = initialIndex; + DesktopTabBar.onAdd(this, tabController, tabs, _selected, + TabInfo(label: id, icon: icon)); } else if (call.method == "onDestroy") { - print("executing onDestroy hook, closing ${connectionIds}"); - connectionIds.forEach((id) { - final tag = '${id}'; + print( + "executing onDestroy hook, closing ${tabs.map((tab) => tab.label).toList()}"); + tabs.forEach((tab) { + final tag = '${tab.label}'; ffi(tag).close().then((_) { Get.delete(tag: tag); }); @@ -82,24 +70,21 @@ class _ConnectionTabPageState extends State return Scaffold( body: Column( children: [ - Obx(() => DesktopTabBar( - controller: tabController, - tabs: connectionIds - .map((e) => - TabInfo(label: e, icon: Icons.desktop_windows_sharp)) - .toList(), - onTabClose: onRemoveId, - selected: _selected, - dark: isDarkTheme(), - mainTab: false, - )), + DesktopTabBar( + controller: tabController, + tabs: tabs, + onTabClose: onRemoveId, + selected: _selected, + dark: isDarkTheme(), + mainTab: false, + ), Expanded( child: Obx(() => TabBarView( controller: tabController.value, - children: connectionIds - .map((e) => RemotePage( - key: ValueKey(e), - id: e, + children: tabs + .map((tab) => RemotePage( + key: ValueKey(tab.label), + id: tab.label, tabBarHeight: kDesktopRemoteTabBarHeight, )) //RemotePage(key: ValueKey(e), id: e)) .toList()))), @@ -109,15 +94,8 @@ class _ConnectionTabPageState extends State } void onRemoveId(String id) { - final indexOf = connectionIds.indexOf(id); - if (indexOf == -1) { - return; - } - connectionIds.removeAt(indexOf); - initialIndex = max(0, initialIndex - 1); - tabController.value = TabController( - length: connectionIds.length, vsync: this, initialIndex: initialIndex); - if (connectionIds.length == 0) { + DesktopTabBar.onClose(this, tabController, tabs, id); + if (tabs.length == 0) { WindowController.fromWindowId(windowId()).close(); } } diff --git a/flutter/lib/desktop/pages/desktop_tab_page.dart b/flutter/lib/desktop/pages/desktop_tab_page.dart index 02ef0fea3..24611e439 100644 --- a/flutter/lib/desktop/pages/desktop_tab_page.dart +++ b/flutter/lib/desktop/pages/desktop_tab_page.dart @@ -1,5 +1,3 @@ -import 'dart:math'; - import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/consts.dart'; @@ -36,15 +34,15 @@ class _DesktopTabPageState extends State return Scaffold( body: Column( children: [ - Obx((() => DesktopTabBar( - controller: tabController, - tabs: tabs.toList(), - onTabClose: onTabClose, - selected: _selected, - dark: isDarkTheme(), - mainTab: true, - onMenu: onTabbarMenu, - ))), + DesktopTabBar( + controller: tabController, + tabs: tabs, + onTabClose: onTabClose, + selected: _selected, + dark: isDarkTheme(), + mainTab: true, + onAddSetting: onAddSetting, + ), Obx((() => Expanded( child: TabBarView( controller: tabController.value, @@ -65,24 +63,11 @@ class _DesktopTabPageState extends State } void onTabClose(String label) { - tabs.removeWhere((tab) => tab.label == label); - tabController.value = TabController( - length: tabs.length, - vsync: this, - initialIndex: max(0, tabs.length - 1)); + DesktopTabBar.onClose(this, tabController, tabs, label); } - void onTabbarMenu() { - int index = tabs.indexWhere((tab) => tab.label == kTabLabelSettingPage); - if (index >= 0) { - tabController.value.animateTo(index, duration: Duration.zero); - _selected.value = index; - } else { - tabs.add(TabInfo(label: kTabLabelSettingPage, icon: Icons.settings)); - tabController.value = TabController( - length: tabs.length, vsync: this, initialIndex: tabs.length - 1); - tabController.value.animateTo(tabs.length - 1, duration: Duration.zero); - _selected.value = tabs.length - 1; - } + void onAddSetting() { + DesktopTabBar.onAdd(this, tabController, tabs, _selected, + TabInfo(label: kTabLabelSettingPage, icon: Icons.settings)); } } diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index c4348fad0..4c2dc3c5e 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -1,5 +1,4 @@ import 'dart:convert'; -import 'dart:math'; import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/material.dart'; @@ -24,22 +23,21 @@ class _FileManagerTabPageState extends State with TickerProviderStateMixin { // refactor List when using multi-tab // this singleton is only for test - var connectionIds = List.empty(growable: true).obs; - var initialIndex = 0; + RxList tabs = List.empty(growable: true).obs; late Rx tabController; static final Rx _selected = 0.obs; + IconData icon = Icons.file_copy_sharp; _FileManagerTabPageState(Map params) { if (params['id'] != null) { - connectionIds.add(params['id']); + tabs.add(TabInfo(label: params['id'], icon: icon)); } } @override void initState() { super.initState(); - tabController = - TabController(length: connectionIds.length, vsync: this).obs; + tabController = TabController(length: tabs.length, vsync: this).obs; rustDeskWinManager.setMethodHandler((call, fromWindowId) async { print( "call ${call.method} with args ${call.arguments} from window ${fromWindowId}"); @@ -48,23 +46,13 @@ class _FileManagerTabPageState extends State final args = jsonDecode(call.arguments); final id = args['id']; window_on_top(windowId()); - final indexOf = connectionIds.indexOf(id); - if (indexOf >= 0) { - initialIndex = indexOf; - tabController.value.animateTo(initialIndex, duration: Duration.zero); - } else { - connectionIds.add(id); - initialIndex = connectionIds.length - 1; - tabController.value = TabController( - length: connectionIds.length, - initialIndex: initialIndex, - vsync: this); - } - _selected.value = initialIndex; + DesktopTabBar.onAdd(this, tabController, tabs, _selected, + TabInfo(label: id, icon: icon)); } else if (call.method == "onDestroy") { - print("executing onDestroy hook, closing ${connectionIds}"); - connectionIds.forEach((id) { - final tag = 'ft_${id}'; + print( + "executing onDestroy hook, closing ${tabs.map((tab) => tab.label).toList()}"); + tabs.forEach((tab) { + final tag = 'ft_${tab.label}'; ffi(tag).close().then((_) { Get.delete(tag: tag); }); @@ -79,26 +67,22 @@ class _FileManagerTabPageState extends State return Scaffold( body: Column( children: [ - Obx( - () => DesktopTabBar( - controller: tabController, - tabs: connectionIds - .map((e) => TabInfo(label: e, icon: Icons.file_copy_sharp)) - .toList(), - onTabClose: onRemoveId, - selected: _selected, - dark: isDarkTheme(), - mainTab: false, - ), + DesktopTabBar( + controller: tabController, + tabs: tabs, + onTabClose: onRemoveId, + selected: _selected, + dark: isDarkTheme(), + mainTab: false, ), Expanded( child: Obx( () => TabBarView( controller: tabController.value, - children: connectionIds - .map((e) => FileManagerPage( - key: ValueKey(e), - id: e)) //RemotePage(key: ValueKey(e), id: e)) + children: tabs + .map((tab) => FileManagerPage( + key: ValueKey(tab.label), + id: tab.label)) //RemotePage(key: ValueKey(e), id: e)) .toList()), ), ) @@ -108,15 +92,8 @@ class _FileManagerTabPageState extends State } void onRemoveId(String id) { - final indexOf = connectionIds.indexOf(id); - if (indexOf == -1) { - return; - } - connectionIds.removeAt(indexOf); - initialIndex = max(0, initialIndex - 1); - tabController.value = TabController( - length: connectionIds.length, initialIndex: initialIndex, vsync: this); - if (connectionIds.length == 0) { + DesktopTabBar.onClose(this, tabController, tabs, id); + if (tabs.length == 0) { WindowController.fromWindowId(windowId()).close(); } } diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 41dca26c2..7a4f1fc79 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -22,13 +22,13 @@ class TabInfo { class DesktopTabBar extends StatelessWidget { late final Rx controller; - late final List tabs; + late final RxList tabs; late final Function(String) onTabClose; late final Rx selected; late final bool dark; late final _Theme _theme; late final bool mainTab; - late final Function()? onMenu; + late final Function()? onAddSetting; DesktopTabBar({ Key? key, @@ -38,7 +38,7 @@ class DesktopTabBar extends StatelessWidget { required this.selected, required this.dark, required this.mainTab, - this.onMenu, + this.onAddSetting, }) : _theme = dark ? _Theme.dark() : _Theme.light(), super(key: key); @@ -105,19 +105,50 @@ class DesktopTabBar extends StatelessWidget { ), ), Offstage( - offstage: onMenu == null, - child: InkWell( - child: Icon( - Icons.menu, - color: _theme.unSelectedIconColor, - ), - onTap: () => onMenu?.call(), - ).paddingOnly(right: 10), + offstage: onAddSetting == null, + child: Tooltip( + message: translate("Settings"), + child: InkWell( + child: Icon( + Icons.menu, + color: _theme.unSelectedIconColor, + ), + onTap: () => onAddSetting?.call(), + ).paddingOnly(right: 10), + ), ) ], ), ); } + + static onClose( + TickerProvider vsync, + Rx controller, + RxList tabs, + String label, + ) { + tabs.removeWhere((tab) => tab.label == label); + controller.value = TabController( + length: tabs.length, + vsync: vsync, + initialIndex: max(0, tabs.length - 1)); + } + + static onAdd(TickerProvider vsync, Rx controller, + RxList tabs, Rx selected, TabInfo tab) { + int index = tabs.indexWhere((e) => e.label == tab.label); + if (index >= 0) { + controller.value.animateTo(index, duration: Duration.zero); + selected.value = index; + } else { + tabs.add(tab); + controller.value = TabController( + length: tabs.length, vsync: vsync, initialIndex: tabs.length - 1); + controller.value.animateTo(tabs.length - 1, duration: Duration.zero); + selected.value = tabs.length - 1; + } + } } class _Tab extends StatelessWidget { @@ -169,7 +200,7 @@ class _Tab extends StatelessWidget { ).paddingSymmetric(horizontal: 5), Expanded( child: Text( - label, + translate(label), style: TextStyle( color: is_selected ? theme.selectedTextColor From 91fd3c5442bf77dc70dd8b7b63a2217e9b9ff120 Mon Sep 17 00:00:00 2001 From: jkhsjdhjs Date: Thu, 11 Aug 2022 17:28:19 +0200 Subject: [PATCH 0192/2015] fix desktop entry categories The category "Other" isn't a valid category [1] and causes unwanted behavior on some DE's [2]. Thus I remove this category and add the main category "Network" instead. I also add the additional categories "RemoteAccess", since rustdesk is a tool to remotely access computers, and "GTK", because it's based on GTK libraries. [1] https://specifications.freedesktop.org/menu-spec/latest/apa.html [2] https://aur.archlinux.org/packages/rustdesk-bin#comment-877405 --- rustdesk.desktop | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rustdesk.desktop b/rustdesk.desktop index 11c7daad0..c9cf1f254 100644 --- a/rustdesk.desktop +++ b/rustdesk.desktop @@ -8,7 +8,7 @@ Icon=/usr/share/rustdesk/files/rustdesk.png Terminal=false Type=Application StartupNotify=true -Categories=Other; +Categories=Network;RemoteAccess;GTK; Keywords=internet; Actions=new-window; From 327a712c363c3ea545d622b5366fe2abaadbbca0 Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 11 Aug 2022 21:29:43 +0800 Subject: [PATCH 0193/2015] optimize ui Signed-off-by: 21pages --- .../lib/desktop/pages/desktop_home_page.dart | 9 ++- .../lib/desktop/widgets/tabbar_widget.dart | 78 ++++++++++--------- 2 files changed, 47 insertions(+), 40 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 770841f09..9c4e84391 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -249,9 +249,12 @@ class _DesktopHomePageState extends State Expanded( child: GestureDetector( onDoubleTap: () { - Clipboard.setData( - ClipboardData(text: model.serverPasswd.text)); - showToast(translate("Copied")); + if (model.verificationMethod != + kUsePermanentPassword) { + Clipboard.setData( + ClipboardData(text: model.serverPasswd.text)); + showToast(translate("Copied")); + } }, child: TextFormField( controller: model.serverPasswd, diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 7a4f1fc79..3398ab33d 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -7,7 +7,6 @@ import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:get/get.dart'; const double _kTabBarHeight = kDesktopRemoteTabBarHeight; -const double _kTabFixedWidth = 150; const double _kIconSize = 18; const double _kDividerIndent = 10; const double _kAddIconSize = _kTabBarHeight - 15; @@ -55,17 +54,16 @@ class DesktopTabBar extends StatelessWidget { offstage: !mainTab, child: Row(children: [ Image.asset('assets/logo.ico'), - Text("RustDesk"), + Text("RustDesk").paddingOnly(left: 5), ]).paddingSymmetric(horizontal: 12, vertical: 5), ), Flexible( child: Obx(() => TabBar( - indicator: BoxDecoration(), - indicatorColor: Colors.transparent, + indicatorColor: _theme.indicatorColor, labelPadding: const EdgeInsets.symmetric( vertical: 0, horizontal: 0), isScrollable: true, - indicatorWeight: 0.1, + indicatorPadding: EdgeInsets.only(bottom: 2), physics: BouncingScrollPhysics(), controller: controller.value, tabs: tabs.asMap().entries.map((e) { @@ -179,42 +177,45 @@ class _Tab extends StatelessWidget { bool is_selected = index == selected; bool show_divider = index != selected - 1 && index != selected; return Ink( - width: _kTabFixedWidth, child: InkWell( onHover: (hover) => _hover.value = hover, onTap: () => onSelected(), child: Row( children: [ - Expanded( - child: Tab( - key: this.key, - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Icon( - icon, - size: _kIconSize, - color: is_selected - ? theme.selectedtabIconColor - : theme.unSelectedtabIconColor, - ).paddingSymmetric(horizontal: 5), - Expanded( - child: Text( - translate(label), - style: TextStyle( - color: is_selected - ? theme.selectedTextColor - : theme.unSelectedTextColor), - ), - ), - Obx((() => _CloseButton( - visiable: _hover.value && closable, - tabSelected: is_selected, - onClose: () => onClose(), - theme: theme, - ))), - ])), - ), + Tab( + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + icon, + size: _kIconSize, + color: is_selected + ? theme.selectedtabIconColor + : theme.unSelectedtabIconColor, + ).paddingOnly(right: 5), + Text( + translate(label), + textAlign: TextAlign.center, + style: TextStyle( + color: is_selected + ? theme.selectedTextColor + : theme.unSelectedTextColor), + ), + ], + ), + Offstage( + offstage: !closable, + child: Obx((() => _CloseButton( + visiable: _hover.value, + tabSelected: is_selected, + onClose: () => onClose(), + theme: theme, + ))), + ) + ])).paddingSymmetric(horizontal: 10), Offstage( offstage: !show_divider, child: VerticalDivider( @@ -289,7 +290,7 @@ class _CloseButton extends StatelessWidget { : theme.unSelectedIconColor, ), ), - )).paddingSymmetric(horizontal: 5); + )).paddingOnly(left: 5); } } @@ -301,6 +302,7 @@ class _Theme { late Color selectedIconColor; late Color unSelectedIconColor; late Color dividerColor; + late Color indicatorColor; _Theme.light() { unSelectedtabIconColor = Color.fromARGB(255, 162, 203, 241); @@ -310,6 +312,7 @@ class _Theme { selectedIconColor = Color.fromARGB(255, 26, 26, 26); unSelectedIconColor = Color.fromARGB(255, 96, 96, 96); dividerColor = Color.fromARGB(255, 238, 238, 238); + indicatorColor = MyTheme.accent; } _Theme.dark() { @@ -320,5 +323,6 @@ class _Theme { selectedIconColor = Color.fromARGB(255, 215, 215, 215); unSelectedIconColor = Color.fromARGB(255, 255, 255, 255); dividerColor = Color.fromARGB(255, 64, 64, 64); + indicatorColor = MyTheme.accent; } } From b916ef36599f15248133bb30596d2777cfd5f8cd Mon Sep 17 00:00:00 2001 From: Asura Date: Thu, 11 Aug 2022 23:59:18 -0700 Subject: [PATCH 0194/2015] Refactor translate mode --- Cargo.lock | 4 ++-- src/server/input_service.rs | 12 +++--------- src/ui/remote.rs | 7 ++----- 3 files changed, 7 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6d1878ddb..8ad3970d3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3740,7 +3740,7 @@ dependencies = [ [[package]] name = "rdev" version = "0.5.0-2" -source = "git+https://github.com/asur4s/rdev#3b440f7ff9d622b08eb83146ea3e5e529769a6c2" +source = "git+https://github.com/asur4s/rdev#895c8fb1a6106714793e8877d35d2b7a1c57ce9c" dependencies = [ "cocoa", "core-foundation 0.9.3", @@ -4712,7 +4712,7 @@ checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" [[package]] name = "tfc" version = "0.6.1" -source = "git+https://github.com/asur4s/The-Fat-Controller#34ee2472e6a88dd8f0e28113d50130d93cf8a572" +source = "git+https://github.com/asur4s/The-Fat-Controller#25bfa7ef1cb0bd0b522cc4155dea6b99673bcfd4" dependencies = [ "core-graphics 0.22.3", "unicode-segmentation", diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 0154da085..2093395c8 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -822,16 +822,10 @@ fn legacy_keyboard_mode(evt: &KeyEvent) { } fn translate_keyboard_mode(evt: &KeyEvent) { - // Caps affects the keycode map of the peer system(Linux). - let mut en = ENIGO.lock().unwrap(); - if en.get_key_state(Key::CapsLock){ - rdev_key_click(RdevKey::CapsLock); - } let chr = char::from_u32(evt.chr()).unwrap_or_default(); - if evt.down { - KBD_CONTEXT.lock().unwrap().unicode_char_down(chr).expect("unicode_char_down error"); - } else { - KBD_CONTEXT.lock().unwrap().unicode_char_up(chr).expect("unicode_char_up error"); + // down(true)->press && press(false)-> release + if evt.down && !evt.press { + KBD_CONTEXT.lock().unwrap().unicode_char(chr).expect("unicode_char_down error"); } } diff --git a/src/ui/remote.rs b/src/ui/remote.rs index e6c4109a6..f26150f31 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -1108,11 +1108,8 @@ impl Handler { let mut key_event = KeyEvent::new(); key_event.set_chr(chr as _); key_event.down = true; - self.send_key_event(key_event, KeyboardMode::Translate); - - let mut key_event = KeyEvent::new(); - key_event.set_chr(chr as _); - key_event.down = false; + key_event.press = false; + self.send_key_event(key_event, KeyboardMode::Translate); } } else { From 8310f38c15bdfcb9c8a8af94cfa84819ed07f2ee Mon Sep 17 00:00:00 2001 From: Asura Date: Fri, 12 Aug 2022 00:05:31 -0700 Subject: [PATCH 0195/2015] Fix repeatedly releasing keys without char --- src/ui/remote.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/ui/remote.rs b/src/ui/remote.rs index f26150f31..3d90ac73f 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -1113,16 +1113,19 @@ impl Handler { self.send_key_event(key_event, KeyboardMode::Translate); } } else { - if down_or_up == true { - TO_RELEASE.lock().unwrap().insert(key); + let success = if down_or_up == true { + TO_RELEASE.lock().unwrap().insert(key) } else { - TO_RELEASE.lock().unwrap().remove(&key); - } + TO_RELEASE.lock().unwrap().remove(&key) + }; + // AltGr && LeftControl(SpecialKey) without action if key == RdevKey::AltGr || evt.scan_code == 541 { return; } - self.map_keyboard_mode(down_or_up, key, None); + if success{ + self.map_keyboard_mode(down_or_up, key, None); + } } } From 3a5efb575ee33947f00f993d076f9a2056d11604 Mon Sep 17 00:00:00 2001 From: cooperbang <75366896+cooperbang@users.noreply.github.com> Date: Fri, 12 Aug 2022 12:44:25 +0200 Subject: [PATCH 0196/2015] Update AppImageBuilder.yml --- appimage/AppImageBuilder.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/appimage/AppImageBuilder.yml b/appimage/AppImageBuilder.yml index 0ca62e97c..08a4f0786 100644 --- a/appimage/AppImageBuilder.yml +++ b/appimage/AppImageBuilder.yml @@ -40,12 +40,12 @@ AppDir: - sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-updates universe - sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic multiverse - sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-updates multiverse - - sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-backports main restricted - universe multiverse + - sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-backports main restricted universe multiverse - sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-security main restricted - sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-security universe - sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-security multiverse include: + - libc6:amd64 - libgcc1:amd64 - libgcrypt20:amd64 - libgtk-3-0:amd64 @@ -95,4 +95,4 @@ AppDir: command: ./AppRun AppImage: arch: x86_64 - update-information: guess \ No newline at end of file + update-information: guess From e6329dc7eb70766ac8bd6aaa25c2493be03d2c65 Mon Sep 17 00:00:00 2001 From: csf Date: Fri, 12 Aug 2022 18:42:02 +0800 Subject: [PATCH 0197/2015] new dialog impl based on Overlay --- flutter/lib/common.dart | 225 ++++++----- .../lib/desktop/pages/connection_page.dart | 14 +- .../lib/desktop/pages/desktop_home_page.dart | 18 +- .../lib/desktop/pages/file_manager_page.dart | 13 +- flutter/lib/desktop/pages/remote_page.dart | 49 +-- .../screen/desktop_file_transfer_screen.dart | 1 - .../desktop/screen/desktop_remote_screen.dart | 1 - .../lib/desktop/widgets/peercard_widget.dart | 4 +- flutter/lib/main.dart | 29 +- flutter/lib/mobile/pages/connection_page.dart | 8 +- .../lib/mobile/pages/file_manager_page.dart | 17 +- flutter/lib/mobile/pages/remote_page.dart | 34 +- flutter/lib/mobile/pages/scan_page.dart | 18 +- flutter/lib/mobile/pages/server_page.dart | 7 +- flutter/lib/mobile/pages/settings_page.dart | 32 +- flutter/lib/mobile/widgets/dialog.dart | 46 +-- flutter/lib/models/file_model.dart | 49 ++- flutter/lib/models/model.dart | 53 +-- flutter/lib/models/native_model.dart | 2 +- flutter/lib/models/server_model.dart | 22 +- flutter/pubspec.lock | 358 +++++++++--------- flutter/pubspec.yaml | 2 +- 22 files changed, 526 insertions(+), 476 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 3b026141d..cabd91b9e 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -4,10 +4,10 @@ import 'dart:io'; import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/instance_manager.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:window_manager/window_manager.dart'; +import 'package:back_button_interceptor/back_button_interceptor.dart'; import 'models/model.dart'; import 'models/platform_model.dart'; @@ -26,10 +26,6 @@ int androidVersion = 0; typedef F = String Function(String); typedef FMethod = String Function(String, dynamic); -class Translator { - static late F call; -} - class MyTheme { MyTheme._(); @@ -71,44 +67,12 @@ final ButtonStyle flatButtonStyle = TextButton.styleFrom( ), ); -void showToast(String text, {Duration? duration}) { - SmartDialog.showToast(text, displayTime: duration); -} - -void showLoading(String text, {bool clickMaskDismiss = false}) { - SmartDialog.dismiss(); - SmartDialog.showLoading( - clickMaskDismiss: false, - builder: (context) { - return Container( - color: MyTheme.white, - constraints: BoxConstraints(maxWidth: 240), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: 30), - Center(child: CircularProgressIndicator()), - SizedBox(height: 20), - Center( - child: Text(Translator.call(text), - style: TextStyle(fontSize: 15))), - SizedBox(height: 20), - Center( - child: TextButton( - style: flatButtonStyle, - onPressed: () { - SmartDialog.dismiss(); - backToHome(); - }, - child: Text(Translator.call('Cancel'), - style: TextStyle(color: MyTheme.accent)))) - ])); - }); -} - -backToHome() { - Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/")); +backToHomePage() { + if (isAndroid || isIOS) { + Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/")); + } else { + // TODO desktop + } } void window_on_top(int? id) { @@ -127,51 +91,140 @@ void window_on_top(int? id) { typedef DialogBuilder = CustomAlertDialog Function( StateSetter setState, void Function([dynamic]) close); -class DialogManager { - static int _tag = 0; +class Dialog { + OverlayEntry? entry; + Completer completer = Completer(); - static dismissByTag(String tag, [result]) { - SmartDialog.dismiss(tag: tag, result: result); + Dialog(); + + void complete(T? res) { + try { + if (!completer.isCompleted) { + completer.complete(res); + } + entry?.remove(); + } catch (e) { + debugPrint("Dialog complete catch error: $e"); + } + } +} + +class OverlayDialogManager { + OverlayState? _overlayState; + Map _dialogs = Map(); + int _tagCount = 0; + + /// By default OverlayDialogManager use global overlay + OverlayDialogManager() { + _overlayState = globalKey.currentState?.overlay; } - static Future show(DialogBuilder builder, + void setOverlayState(OverlayState? overlayState) { + _overlayState = overlayState; + } + + void dismissAll() { + _dialogs.forEach((key, value) { + value.complete(null); + BackButtonInterceptor.removeByName(key); + }); + _dialogs.clear(); + } + + void dismissByTag(String tag) { + _dialogs[tag]?.complete(null); + _dialogs.remove(tag); + BackButtonInterceptor.removeByName(tag); + } + + // TODO clickMaskDismiss + Future show(DialogBuilder builder, {bool clickMaskDismiss = false, bool backDismiss = false, String? tag, - bool useAnimation = true}) async { - final t; - if (tag != null) { - t = tag; - } else { - _tag += 1; - t = _tag.toString(); + bool useAnimation = true, + bool forceGlobal = false}) { + final overlayState = + forceGlobal ? globalKey.currentState?.overlay : _overlayState; + + if (overlayState == null) { + return Future.error( + "[OverlayDialogManager] Failed to show dialog, _overlayState is null, call [setOverlayState] first"); } - SmartDialog.dismiss(status: SmartStatus.allToast); - SmartDialog.dismiss(status: SmartStatus.loading); + + final _tag; + if (tag != null) { + _tag = tag; + } else { + _tag = _tagCount.toString(); + _tagCount++; + } + + final dialog = Dialog(); + _dialogs[_tag] = dialog; + final close = ([res]) { - SmartDialog.dismiss(tag: t, result: res); + _dialogs.remove(_tag); + dialog.complete(res); + BackButtonInterceptor.removeByName(_tag); }; - final res = await SmartDialog.show( - tag: t, - clickMaskDismiss: clickMaskDismiss, - backDismiss: backDismiss, - useAnimation: useAnimation, - builder: (_) => StatefulBuilder( - builder: (_, setState) => builder(setState, close))); - return res; + dialog.entry = OverlayEntry(builder: (_) { + return Container( + color: Colors.transparent, + child: StatefulBuilder( + builder: (_, setState) => builder(setState, close))); + }); + overlayState.insert(dialog.entry!); + BackButtonInterceptor.add((stopDefaultButtonEvent, routeInfo) { + if (backDismiss) { + close(); + } + return true; + }, name: _tag); + return dialog.completer.future; + } + + void showLoading(String text, + {bool clickMaskDismiss = false, bool cancelToClose = false}) { + show((setState, close) => CustomAlertDialog( + content: Container( + color: MyTheme.white, + constraints: BoxConstraints(maxWidth: 240), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 30), + Center(child: CircularProgressIndicator()), + SizedBox(height: 20), + Center( + child: Text(translate(text), + style: TextStyle(fontSize: 15))), + SizedBox(height: 20), + Center( + child: TextButton( + style: flatButtonStyle, + onPressed: () { + dismissAll(); + if (cancelToClose) backToHomePage(); + }, + child: Text(translate('Cancel'), + style: TextStyle(color: MyTheme.accent)))) + ])))); + } + + void showToast(String text) { + // TODO } } class CustomAlertDialog extends StatelessWidget { CustomAlertDialog( - {required this.title, - required this.content, - required this.actions, - this.contentPadding}); + {this.title, required this.content, this.actions, this.contentPadding}); - final Widget title; + final Widget? title; final Widget content; - final List actions; + final List? actions; final double? contentPadding; @override @@ -187,7 +240,9 @@ class CustomAlertDialog extends StatelessWidget { } } -void msgBox(String type, String title, String text, {bool? hasCancel}) { +void msgBox( + String type, String title, String text, OverlayDialogManager dialogManager, + {bool? hasCancel}) { var wrap = (String text, void Function() onPressed) => ButtonTheme( padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10), materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, @@ -198,17 +253,17 @@ void msgBox(String type, String title, String text, {bool? hasCancel}) { child: TextButton( style: flatButtonStyle, onPressed: onPressed, - child: Text(Translator.call(text), - style: TextStyle(color: MyTheme.accent)))); + child: + Text(translate(text), style: TextStyle(color: MyTheme.accent)))); - SmartDialog.dismiss(); + dialogManager.dismissAll(); List buttons = []; if (type != "connecting" && type != "success" && type.indexOf("nook") < 0) { buttons.insert( 0, - wrap(Translator.call('OK'), () { - SmartDialog.dismiss(); - backToHome(); + wrap(translate('OK'), () { + dialogManager.dismissAll(); + backToHomePage(); })); } if (hasCancel == null) { @@ -220,21 +275,21 @@ void msgBox(String type, String title, String text, {bool? hasCancel}) { if (hasCancel) { buttons.insert( 0, - wrap(Translator.call('Cancel'), () { - SmartDialog.dismiss(); + wrap(translate('Cancel'), () { + dialogManager.dismissAll(); })); } // TODO: test this button if (type.indexOf("hasclose") >= 0) { buttons.insert( 0, - wrap(Translator.call('Close'), () { - SmartDialog.dismiss(); + wrap(translate('Close'), () { + dialogManager.dismissAll(); })); } - DialogManager.show((setState, close) => CustomAlertDialog( + dialogManager.show((setState, close) => CustomAlertDialog( title: Text(translate(title), style: TextStyle(fontSize: 21)), - content: Text(Translator.call(text), style: TextStyle(fontSize: 15)), + content: Text(translate(text), style: TextStyle(fontSize: 15)), actions: buttons)); } diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 182f1d0b4..c07df87e9 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -625,7 +625,7 @@ class _ConnectionPageState extends State { var field = ""; var msg = ""; var isInProgress = false; - DialogManager.show((setState, close) { + gFFI.dialogManager.show((setState, close) { return CustomAlertDialog( title: Text(translate("Add ID")), content: Column( @@ -698,7 +698,7 @@ class _ConnectionPageState extends State { var field = ""; var msg = ""; var isInProgress = false; - DialogManager.show((setState, close) { + gFFI.dialogManager.show((setState, close) { return CustomAlertDialog( title: Text(translate("Add Tag")), content: Column( @@ -769,7 +769,7 @@ class _ConnectionPageState extends State { final tags = List.of(gFFI.abModel.tags); var selectedTag = gFFI.abModel.getPeerTags(id).obs; - DialogManager.show((setState, close) { + gFFI.dialogManager.show((setState, close) { return CustomAlertDialog( title: Text(translate("Edit Tag")), content: Column( @@ -884,16 +884,16 @@ class _WebMenuState extends State { }, onSelected: (value) { if (value == 'server') { - showServerSettings(); + showServerSettings(gFFI.dialogManager); } if (value == 'about') { - showAbout(); + showAbout(gFFI.dialogManager); } if (value == 'login') { if (username == null) { - showLogin(); + showLogin(gFFI.dialogManager); } else { - logout(); + logout(gFFI.dialogManager); } } if (value == 'scan') { diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 9c4e84391..f68cdac94 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -116,7 +116,7 @@ class _DesktopHomePageState extends State onDoubleTap: () { Clipboard.setData( ClipboardData(text: model.serverId.text)); - showToast(translate("Copied")); + gFFI.dialogManager.showToast(translate("Copied")); }, child: TextFormField( controller: model.serverId, @@ -253,7 +253,7 @@ class _DesktopHomePageState extends State kUsePermanentPassword) { Clipboard.setData( ClipboardData(text: model.serverPasswd.text)); - showToast(translate("Copied")); + gFFI.dialogManager.showToast(translate("Copied")); } }, child: TextFormField( @@ -604,7 +604,7 @@ class _DesktopHomePageState extends State var newId = ""; var msg = ""; var isInProgress = false; - DialogManager.show((setState, close) { + gFFI.dialogManager.show((setState, close) { return CustomAlertDialog( title: Text(translate("Change ID")), content: Column( @@ -690,7 +690,7 @@ class _DesktopHomePageState extends State var key = oldOptions['key'] ?? ""; var isInProgress = false; - DialogManager.show((setState, close) { + gFFI.dialogManager.show((setState, close) { return CustomAlertDialog( title: Text(translate("ID/Relay Server")), content: ConstrainedBox( @@ -891,7 +891,7 @@ class _DesktopHomePageState extends State var newWhiteListField = newWhiteList.join('\n'); var msg = ""; var isInProgress = false; - DialogManager.show((setState, close) { + gFFI.dialogManager.show((setState, close) { return CustomAlertDialog( title: Text(translate("IP Whitelisting")), content: Column( @@ -980,7 +980,7 @@ class _DesktopHomePageState extends State } var isInProgress = false; - DialogManager.show((setState, close) { + gFFI.dialogManager.show((setState, close) { return CustomAlertDialog( title: Text(translate("Socks5 Proxy")), content: ConstrainedBox( @@ -1117,7 +1117,7 @@ class _DesktopHomePageState extends State final license = await bind.mainGetLicense(); final version = await bind.mainGetVersion(); final linkStyle = TextStyle(decoration: TextDecoration.underline); - DialogManager.show((setState, close) { + gFFI.dialogManager.show((setState, close) { return CustomAlertDialog( title: Text("About $appName"), content: ConstrainedBox( @@ -1208,7 +1208,7 @@ Future loginDialog() async { var isInProgress = false; var completer = Completer(); - DialogManager.show((setState, close) { + gFFI.dialogManager.show((setState, close) { return CustomAlertDialog( title: Text(translate("Login")), content: ConstrainedBox( @@ -1339,7 +1339,7 @@ void setPasswordDialog() async { var errMsg0 = ""; var errMsg1 = ""; - DialogManager.show((setState, close) { + gFFI.dialogManager.show((setState, close) { return CustomAlertDialog( title: Text(translate("Set Password")), content: ConstrainedBox( diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 22d46c146..e5279a7e2 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -4,7 +4,6 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/mobile/pages/file_manager_page.dart'; import 'package:flutter_hbb/models/file_model.dart'; -import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:wakelock/wakelock.dart'; @@ -26,8 +25,7 @@ class _FileManagerPageState extends State final _localSelectedItems = SelectedItems(); final _remoteSelectedItems = SelectedItems(); - /// FFI with name file_transfer_id - FFI get _ffi => ffi('ft_${widget.id}'); + late FFI _ffi; FileModel get model => _ffi.fileModel; @@ -38,8 +36,9 @@ class _FileManagerPageState extends State @override void initState() { super.initState(); - Get.put(FFI()..connect(widget.id, isFileTransfer: true), - tag: 'ft_${widget.id}'); + _ffi = FFI(); + _ffi.connect(widget.id, isFileTransfer: true); + Get.put(_ffi, tag: 'ft_${widget.id}'); // _ffi.ffiModel.updateEventListener(widget.id); if (!Platform.isLinux) { Wakelock.enable(); @@ -51,7 +50,7 @@ class _FileManagerPageState extends State void dispose() { model.onClose(); _ffi.close(); - SmartDialog.dismiss(); + _ffi.dialogManager.dismissAll(); if (!Platform.isLinux) { Wakelock.disable(); } @@ -552,7 +551,7 @@ class _FileManagerPageState extends State IconButton( onPressed: () { final name = TextEditingController(); - DialogManager.show((setState, close) => + _ffi.dialogManager.show((setState, close) => CustomAlertDialog( title: Text(translate("Create Folder")), content: Column( diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 01744e8e7..d81adb3d9 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -7,9 +7,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hbb/mobile/widgets/gesture_help.dart'; import 'package:flutter_hbb/models/chat_model.dart'; -import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; -import 'package:get/route_manager.dart'; import 'package:provider/provider.dart'; import 'package:wakelock/wakelock.dart'; @@ -51,18 +49,19 @@ class _RemotePageState extends State var _showEdit = false; // use soft keyboard var _isPhysicalMouse = false; - FFI get _ffi => ffi(widget.id); + late FFI _ffi; @override void initState() { super.initState(); - var ffitmp = FFI(); - ffitmp.canvasModel.tabBarHeight = super.widget.tabBarHeight; - final ffi = Get.put(ffitmp, tag: widget.id); - ffi.connect(widget.id, tabBarHeight: super.widget.tabBarHeight); + _ffi = FFI(); + _ffi.canvasModel.tabBarHeight = super.widget.tabBarHeight; + Get.put(_ffi, tag: widget.id); + _ffi.connect(widget.id, tabBarHeight: super.widget.tabBarHeight); WidgetsBinding.instance.addPostFrameCallback((_) { SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); - showLoading(translate('Connecting...')); + _ffi.dialogManager + .showLoading(translate('Connecting...'), cancelToClose: true); _interval = Timer.periodic(Duration(milliseconds: 30), (timer) => interval()); }); @@ -70,8 +69,8 @@ class _RemotePageState extends State Wakelock.enable(); } _physicalFocusNode.requestFocus(); - ffi.ffiModel.updateEventListener(widget.id); - ffi.listenToMouse(true); + _ffi.ffiModel.updateEventListener(widget.id); + _ffi.listenToMouse(true); // WindowManager.instance.addListener(this); } @@ -86,7 +85,7 @@ class _RemotePageState extends State _ffi.close(); _interval?.cancel(); _timer?.cancel(); - SmartDialog.dismiss(); + _ffi.dialogManager.dismissAll(); SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: SystemUiOverlay.values); if (!Platform.isLinux) { @@ -262,8 +261,11 @@ class _RemotePageState extends State initialEntries: [ OverlayEntry(builder: (context) { _ffi.chatModel.setOverlayState(Overlay.of(context)); + _ffi.dialogManager.setOverlayState(Overlay.of(context)); return Container( - child: getRawPointerAndKeyBody(getBodyForDesktop(keyboard))); + color: Colors.black, + child: getRawPointerAndKeyBody( + getBodyForDesktop(context, keyboard))); }) ], )); @@ -275,7 +277,7 @@ class _RemotePageState extends State _ffi.canvasModel.tabBarHeight = super.widget.tabBarHeight; return WillPopScope( onWillPop: () async { - clientClose(); + clientClose(_ffi.dialogManager); return false; }, child: MultiProvider( @@ -410,7 +412,7 @@ class _RemotePageState extends State color: Colors.white, icon: Icon(Icons.clear), onPressed: () { - clientClose(); + clientClose(_ffi.dialogManager); }, ) ] + @@ -420,7 +422,7 @@ class _RemotePageState extends State icon: Icon(Icons.tv), onPressed: () { setState(() => _showEdit = false); - showOptions(widget.id); + showOptions(widget.id, _ffi.dialogManager); }, ) ] + @@ -497,7 +499,7 @@ class _RemotePageState extends State /// DoubleFiner -> right click /// HoldDrag -> left drag - Widget getBodyForDesktop(bool keyboard) { + Widget getBodyForDesktop(BuildContext context, bool keyboard) { var paints = [ MouseRegion(onEnter: (evt) { bind.hostStopSystemKeyPropagate(stopped: false); @@ -567,7 +569,7 @@ class _RemotePageState extends State style: flatButtonStyle, onPressed: () { Navigator.pop(context); - showSetOSPassword(widget.id, false); + showSetOSPassword(widget.id, false, _ffi.dialogManager); }, child: Icon(Icons.edit, color: MyTheme.accent), ) @@ -632,7 +634,7 @@ class _RemotePageState extends State if (password != null) { bind.sessionInputOsPassword(id: widget.id, value: password); } else { - showSetOSPassword(widget.id, true); + showSetOSPassword(widget.id, true, _ffi.dialogManager); } } else if (value == 'reset_canvas') { _ffi.cursorModel.reset(); @@ -889,7 +891,7 @@ class ImagePainter extends CustomPainter { } } -void showOptions(String id) async { +void showOptions(String id, OverlayDialogManager dialogManager) async { String quality = await bind.getSessionImageQuality(id: id) ?? 'balanced'; if (quality == '') quality = 'balanced'; String viewStyle = @@ -907,7 +909,7 @@ void showOptions(String id) async { onTap: () { if (i == cur) return; bind.sessionSwitchDisplay(id: id, value: i); - SmartDialog.dismiss(); + dialogManager.dismissAll(); }, child: Ink( width: 40, @@ -932,7 +934,7 @@ void showOptions(String id) async { } final perms = ffi(id).ffiModel.permissions; - DialogManager.show((setState, close) { + dialogManager.show((setState, close) { final more = []; if (perms['audio'] != false) { more.add(getToggle(id, setState, 'disable-audio', 'Mute')); @@ -990,12 +992,13 @@ void showOptions(String id) async { }, clickMaskDismiss: true, backDismiss: true); } -void showSetOSPassword(String id, bool login) async { +void showSetOSPassword( + String id, bool login, OverlayDialogManager dialogManager) async { final controller = TextEditingController(); var password = await bind.getSessionOption(id: id, arg: "os-password") ?? ""; var autoLogin = await bind.getSessionOption(id: id, arg: "auto-login") != ""; controller.text = password; - DialogManager.show((setState, close) { + dialogManager.show((setState, close) { return CustomAlertDialog( title: Text(translate('OS Password')), content: Column(mainAxisSize: MainAxisSize.min, children: [ diff --git a/flutter/lib/desktop/screen/desktop_file_transfer_screen.dart b/flutter/lib/desktop/screen/desktop_file_transfer_screen.dart index 03230b0b0..694f18ace 100644 --- a/flutter/lib/desktop/screen/desktop_file_transfer_screen.dart +++ b/flutter/lib/desktop/screen/desktop_file_transfer_screen.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/desktop/pages/file_manager_tab_page.dart'; -import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:provider/provider.dart'; /// multi-tab file transfer remote screen diff --git a/flutter/lib/desktop/screen/desktop_remote_screen.dart b/flutter/lib/desktop/screen/desktop_remote_screen.dart index 95f6abed5..4e941ed7c 100644 --- a/flutter/lib/desktop/screen/desktop_remote_screen.dart +++ b/flutter/lib/desktop/screen/desktop_remote_screen.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/desktop/pages/connection_tab_page.dart'; -import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:provider/provider.dart'; /// multi-tab desktop remote screen diff --git a/flutter/lib/desktop/widgets/peercard_widget.dart b/flutter/lib/desktop/widgets/peercard_widget.dart index 87cfa2a59..85e6e20e6 100644 --- a/flutter/lib/desktop/widgets/peercard_widget.dart +++ b/flutter/lib/desktop/widgets/peercard_widget.dart @@ -258,7 +258,7 @@ class _PeerCardState extends State<_PeerCard> final tags = List.of(gFFI.abModel.tags); var selectedTag = gFFI.abModel.getPeerTags(id).obs; - DialogManager.show((setState, close) { + gFFI.dialogManager.show((setState, close) { return CustomAlertDialog( title: Text(translate("Edit Tag")), content: Column( @@ -314,7 +314,7 @@ class _PeerCardState extends State<_PeerCard> } } final k = GlobalKey(); - DialogManager.show((setState, close) { + gFFI.dialogManager.show((setState, close) { return CustomAlertDialog( title: Text(translate("Rename")), content: Column( diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 000202f65..dd6ccd31d 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -5,7 +5,6 @@ import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart'; import 'package:flutter_hbb/desktop/screen/desktop_file_transfer_screen.dart'; import 'package:flutter_hbb/desktop/screen/desktop_remote_screen.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; -import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:get/route_manager.dart'; import 'package:provider/provider.dart'; @@ -95,14 +94,7 @@ void runRemoteScreen(Map argument) async { ), navigatorObservers: [ // FirebaseAnalyticsObserver(analytics: analytics), - FlutterSmartDialog.observer ], - builder: FlutterSmartDialog.init( - builder: isAndroid - ? (_, child) => AccessibilityListener( - child: child, - ) - : null), )); } @@ -116,14 +108,7 @@ void runFileTransferScreen(Map argument) async { home: DesktopFileTransferScreen(params: argument), navigatorObservers: [ // FirebaseAnalyticsObserver(analytics: analytics), - FlutterSmartDialog.observer - ], - builder: FlutterSmartDialog.init( - builder: isAndroid - ? (_, child) => AccessibilityListener( - child: child, - ) - : null))); + ])); } class App extends StatelessWidget { @@ -153,14 +138,12 @@ class App extends StatelessWidget { : HomePage(), navigatorObservers: [ // FirebaseAnalyticsObserver(analytics: analytics), - FlutterSmartDialog.observer ], - builder: FlutterSmartDialog.init( - builder: isAndroid - ? (_, child) => AccessibilityListener( - child: child, - ) - : null)), + builder: isAndroid + ? (_, child) => AccessibilityListener( + child: child, + ) + : null), ); } } diff --git a/flutter/lib/mobile/pages/connection_page.dart b/flutter/lib/mobile/pages/connection_page.dart index 227bfb630..ba34b31e8 100644 --- a/flutter/lib/mobile/pages/connection_page.dart +++ b/flutter/lib/mobile/pages/connection_page.dart @@ -380,16 +380,16 @@ class _WebMenuState extends State { }, onSelected: (value) { if (value == 'server') { - showServerSettings(); + showServerSettings(gFFI.dialogManager); } if (value == 'about') { - showAbout(); + showAbout(gFFI.dialogManager); } if (value == 'login') { if (username == null) { - showLogin(); + showLogin(gFFI.dialogManager); } else { - logout(); + logout(gFFI.dialogManager); } } if (value == 'scan') { diff --git a/flutter/lib/mobile/pages/file_manager_page.dart b/flutter/lib/mobile/pages/file_manager_page.dart index 9a8d0088a..9c8fd92c4 100644 --- a/flutter/lib/mobile/pages/file_manager_page.dart +++ b/flutter/lib/mobile/pages/file_manager_page.dart @@ -3,13 +3,11 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_breadcrumb/flutter_breadcrumb.dart'; import 'package:flutter_hbb/models/file_model.dart'; -import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:provider/provider.dart'; import 'package:toggle_switch/toggle_switch.dart'; import 'package:wakelock/wakelock.dart'; import '../../common.dart'; -import '../../models/model.dart'; import '../widgets/dialog.dart'; class FileManagerPage extends StatefulWidget { @@ -29,7 +27,10 @@ class _FileManagerPageState extends State { void initState() { super.initState(); gFFI.connect(widget.id, isFileTransfer: true); - showLoading(translate('Connecting...')); + WidgetsBinding.instance.addPostFrameCallback((_) { + gFFI.dialogManager + .showLoading(translate('Connecting...'), cancelToClose: true); + }); gFFI.ffiModel.updateEventListener(widget.id); Wakelock.enable(); } @@ -38,7 +39,7 @@ class _FileManagerPageState extends State { void dispose() { model.onClose(); gFFI.close(); - SmartDialog.dismiss(); + gFFI.dialogManager.dismissAll(); Wakelock.disable(); super.dispose(); } @@ -60,7 +61,9 @@ class _FileManagerPageState extends State { backgroundColor: MyTheme.grayBg, appBar: AppBar( leading: Row(children: [ - IconButton(icon: Icon(Icons.close), onPressed: clientClose), + IconButton( + icon: Icon(Icons.close), + onPressed: () => clientClose(gFFI.dialogManager)), ]), centerTitle: true, title: ToggleSwitch( @@ -141,8 +144,8 @@ class _FileManagerPageState extends State { model.toggleSelectMode(); } else if (v == "folder") { final name = TextEditingController(); - DialogManager.show( - (setState, close) => CustomAlertDialog( + gFFI.dialogManager + .show((setState, close) => CustomAlertDialog( title: Text(translate("Create Folder")), content: Column( mainAxisSize: MainAxisSize.min, diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 69bf11de0..14bdfa833 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -6,7 +6,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hbb/mobile/widgets/gesture_help.dart'; import 'package:flutter_hbb/models/chat_model.dart'; -import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:provider/provider.dart'; import 'package:wakelock/wakelock.dart'; @@ -51,7 +50,8 @@ class _RemotePageState extends State { gFFI.connect(widget.id); WidgetsBinding.instance.addPostFrameCallback((_) { SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); - showLoading(translate('Connecting...')); + gFFI.dialogManager + .showLoading(translate('Connecting...'), cancelToClose: true); _interval = Timer.periodic(Duration(milliseconds: 30), (timer) => interval()); }); @@ -71,7 +71,7 @@ class _RemotePageState extends State { gFFI.close(); _interval?.cancel(); _timer?.cancel(); - SmartDialog.dismiss(); + gFFI.dialogManager.dismissAll(); SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: SystemUiOverlay.values); Wakelock.disable(); @@ -226,7 +226,7 @@ class _RemotePageState extends State { return WillPopScope( onWillPop: () async { - clientClose(); + clientClose(gFFI.dialogManager); return false; }, child: getRawPointerAndKeyBody( @@ -401,7 +401,7 @@ class _RemotePageState extends State { color: Colors.white, icon: Icon(Icons.clear), onPressed: () { - clientClose(); + clientClose(gFFI.dialogManager); }, ) ] + @@ -411,7 +411,7 @@ class _RemotePageState extends State { icon: Icon(Icons.tv), onPressed: () { setState(() => _showEdit = false); - showOptions(widget.id); + showOptions(widget.id, gFFI.dialogManager); }, ) ] + @@ -671,7 +671,7 @@ class _RemotePageState extends State { style: flatButtonStyle, onPressed: () { Navigator.pop(context); - showSetOSPassword(id, false); + showSetOSPassword(id, false, gFFI.dialogManager); }, child: Icon(Icons.edit, color: MyTheme.accent), ) @@ -739,12 +739,12 @@ class _RemotePageState extends State { if (password != null) { bind.sessionInputOsPassword(id: widget.id, value: password); } else { - showSetOSPassword(id, true); + showSetOSPassword(id, true, gFFI.dialogManager); } } else if (value == 'reset_canvas') { gFFI.cursorModel.reset(); } else if (value == 'restart') { - showRestartRemoteDevice(pi, widget.id); + showRestartRemoteDevice(pi, widget.id, gFFI.dialogManager); } }(); } @@ -1008,7 +1008,7 @@ class QualityMonitor extends StatelessWidget { : SizedBox.shrink()))); } -void showOptions(String id) async { +void showOptions(String id, OverlayDialogManager dialogManager) async { String quality = await bind.getSessionImageQuality(id: id) ?? 'balanced'; if (quality == '') quality = 'balanced'; String viewStyle = @@ -1026,7 +1026,7 @@ void showOptions(String id) async { onTap: () { if (i == cur) return; bind.sessionSwitchDisplay(id: id, value: i); - SmartDialog.dismiss(); + gFFI.dialogManager.dismissAll(); }, child: Ink( width: 40, @@ -1051,7 +1051,7 @@ void showOptions(String id) async { } final perms = gFFI.ffiModel.permissions; - DialogManager.show((setState, close) { + dialogManager.show((setState, close) { final more = []; if (perms['audio'] != false) { more.add(getToggle(id, setState, 'disable-audio', 'Mute')); @@ -1107,9 +1107,10 @@ void showOptions(String id) async { }, clickMaskDismiss: true, backDismiss: true); } -void showRestartRemoteDevice(PeerInfo pi, String id) async { +void showRestartRemoteDevice( + PeerInfo pi, String id, OverlayDialogManager dialogManager) async { final res = - await DialogManager.show((setState, close) => CustomAlertDialog( + await dialogManager.show((setState, close) => CustomAlertDialog( title: Row(children: [ Icon(Icons.warning_amber_sharp, color: Colors.redAccent, size: 28), @@ -1128,12 +1129,13 @@ void showRestartRemoteDevice(PeerInfo pi, String id) async { if (res == true) bind.sessionRestartRemoteDevice(id: id); } -void showSetOSPassword(String id, bool login) async { +void showSetOSPassword( + String id, bool login, OverlayDialogManager dialogManager) async { final controller = TextEditingController(); var password = await bind.getSessionOption(id: id, arg: "os-password") ?? ""; var autoLogin = await bind.getSessionOption(id: id, arg: "auto-login") != ""; controller.text = password; - DialogManager.show((setState, close) { + dialogManager.show((setState, close) { return CustomAlertDialog( title: Text(translate('OS Password')), content: Column(mainAxisSize: MainAxisSize.min, children: [ diff --git a/flutter/lib/mobile/pages/scan_page.dart b/flutter/lib/mobile/pages/scan_page.dart index 54ba44892..4325d0570 100644 --- a/flutter/lib/mobile/pages/scan_page.dart +++ b/flutter/lib/mobile/pages/scan_page.dart @@ -63,7 +63,7 @@ class _ScanPageState extends State { var result = reader.decode(bitmap); showServerSettingFromQr(result.text); } catch (e) { - showToast('No QR code found'); + gFFI.dialogManager.showToast('No QR code found'); } } }), @@ -121,7 +121,7 @@ class _ScanPageState extends State { void _onPermissionSet(BuildContext context, QRViewController ctrl, bool p) { if (!p) { - showToast('No permisssion'); + gFFI.dialogManager.showToast('No permisssion'); } } @@ -132,10 +132,10 @@ class _ScanPageState extends State { } void showServerSettingFromQr(String data) async { - backToHome(); + backToHomePage(); await controller?.pauseCamera(); if (!data.startsWith('config=')) { - showToast('Invalid QR code'); + gFFI.dialogManager.showToast('Invalid QR code'); return; } try { @@ -144,16 +144,16 @@ class _ScanPageState extends State { var key = values['key'] != null ? values['key'] as String : ''; var api = values['api'] != null ? values['api'] as String : ''; Timer(Duration(milliseconds: 60), () { - showServerSettingsWithValue(host, '', key, api); + showServerSettingsWithValue(host, '', key, api, gFFI.dialogManager); }); } catch (e) { - showToast('Invalid QR code'); + gFFI.dialogManager.showToast('Invalid QR code'); } } } -void showServerSettingsWithValue( - String id, String relay, String key, String api) async { +void showServerSettingsWithValue(String id, String relay, String key, + String api, OverlayDialogManager dialogManager) async { Map oldOptions = jsonDecode(await bind.mainGetOptions()); String id0 = oldOptions['custom-rendezvous-server'] ?? ""; String relay0 = oldOptions['relay-server'] ?? ""; @@ -168,7 +168,7 @@ void showServerSettingsWithValue( String? relayServerMsg; String? apiServerMsg; - DialogManager.show((setState, close) { + dialogManager.show((setState, close) { Future validate() async { if (idController.text != id) { final res = await validateAsync(idController.text); diff --git a/flutter/lib/mobile/pages/server_page.dart b/flutter/lib/mobile/pages/server_page.dart index d3dc4109d..f19a011b6 100644 --- a/flutter/lib/mobile/pages/server_page.dart +++ b/flutter/lib/mobile/pages/server_page.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:flutter_hbb/mobile/widgets/dialog.dart'; -import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:provider/provider.dart'; import '../../common.dart'; @@ -90,9 +89,9 @@ class ServerPage extends StatefulWidget implements PageShape { if (value == "changeID") { // TODO } else if (value == "setPermanentPassword") { - setPermanentPasswordDialog(); + setPermanentPasswordDialog(gFFI.dialogManager); } else if (value == "setTemporaryPasswordLength") { - setTemporaryPasswordLengthDialog(); + setTemporaryPasswordLengthDialog(gFFI.dialogManager); } else if (value == kUsePermanentPassword || value == kUseTemporaryPassword || value == kUseBothPasswords) { @@ -522,7 +521,7 @@ void toAndroidChannelInit() { switch (method) { case "start_capture": { - SmartDialog.dismiss(); + gFFI.dialogManager.dismissAll(); gFFI.serverModel.updateClientState(); break; } diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index 4b8760413..3a1f8b352 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -119,8 +119,8 @@ class _SettingsState extends State with WidgetsBindingObserver { if (v) { PermissionManager.request("ignore_battery_optimizations"); } else { - final res = await DialogManager.show( - (setState, close) => CustomAlertDialog( + final res = await gFFI.dialogManager + .show((setState, close) => CustomAlertDialog( title: Text(translate("Open System Setting")), content: Text(translate( "android_open_battery_optimizations_tip")), @@ -153,9 +153,9 @@ class _SettingsState extends State with WidgetsBindingObserver { leading: Icon(Icons.person), onPressed: (context) { if (username == null) { - showLogin(); + showLogin(gFFI.dialogManager); } else { - logout(); + logout(gFFI.dialogManager); } }, ), @@ -166,13 +166,13 @@ class _SettingsState extends State with WidgetsBindingObserver { title: Text(translate('ID/Relay Server')), leading: Icon(Icons.cloud), onPressed: (context) { - showServerSettings(); + showServerSettings(gFFI.dialogManager); }), SettingsTile.navigation( title: Text(translate('Language')), leading: Icon(Icons.translate), onPressed: (context) { - showLanguageSettings(); + showLanguageSettings(gFFI.dialogManager); }) ]), SettingsSection( @@ -204,20 +204,20 @@ class _SettingsState extends State with WidgetsBindingObserver { } } -void showServerSettings() async { +void showServerSettings(OverlayDialogManager dialogManager) async { Map options = jsonDecode(await bind.mainGetOptions()); String id = options['custom-rendezvous-server'] ?? ""; String relay = options['relay-server'] ?? ""; String api = options['api-server'] ?? ""; String key = options['key'] ?? ""; - showServerSettingsWithValue(id, relay, key, api); + showServerSettingsWithValue(id, relay, key, api, dialogManager); } -void showLanguageSettings() async { +void showLanguageSettings(OverlayDialogManager dialogManager) async { try { final langs = json.decode(await bind.mainGetLangs()) as List; var lang = await bind.mainGetLocalOption(key: "lang"); - DialogManager.show((setState, close) { + dialogManager.show((setState, close) { final setLang = (v) { if (lang != v) { setState(() { @@ -246,8 +246,8 @@ void showLanguageSettings() async { } catch (_e) {} } -void showAbout() { - DialogManager.show((setState, close) { +void showAbout(OverlayDialogManager dialogManager) { + dialogManager.show((setState, close) { return CustomAlertDialog( title: Text(translate('About') + ' RustDesk'), content: Wrap(direction: Axis.vertical, spacing: 12, children: [ @@ -350,7 +350,7 @@ void refreshCurrentUser() async { } } -void logout() async { +void logout(OverlayDialogManager dialogManager) async { final token = await bind.mainGetOption(key: "access_token"); if (token == '') return; final url = getUrl(); @@ -363,7 +363,7 @@ void logout() async { }, body: json.encode(body)); } catch (e) { - showToast('Failed to access $url'); + dialogManager.showToast('Failed to access $url'); } resetToken(); } @@ -396,12 +396,12 @@ Future getUrl() async { return url; } -void showLogin() { +void showLogin(OverlayDialogManager dialogManager) { final passwordController = TextEditingController(); final nameController = TextEditingController(); var loading = false; var error = ''; - DialogManager.show((setState, close) { + dialogManager.show((setState, close) { return CustomAlertDialog( title: Text(translate('Login')), content: Column(mainAxisSize: MainAxisSize.min, children: [ diff --git a/flutter/lib/mobile/widgets/dialog.dart b/flutter/lib/mobile/widgets/dialog.dart index ddd6816fb..075fa5bd9 100644 --- a/flutter/lib/mobile/widgets/dialog.dart +++ b/flutter/lib/mobile/widgets/dialog.dart @@ -1,32 +1,31 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import '../../common.dart'; import '../../models/platform_model.dart'; -void clientClose() { - msgBox('', 'Close', 'Are you sure to close the connection?'); +void clientClose(OverlayDialogManager dialogManager) { + msgBox('', 'Close', 'Are you sure to close the connection?', dialogManager); } const SEC1 = Duration(seconds: 1); void showSuccess({Duration duration = SEC1}) { - SmartDialog.dismiss(); - showToast(translate("Successful"), duration: SEC1); + // TODO + // showToast(translate("Successful"), duration: SEC1); } void showError({Duration duration = SEC1}) { - SmartDialog.dismiss(); - showToast(translate("Error"), duration: SEC1); + // TODO + // showToast(translate("Error"), duration: SEC1); } -void setPermanentPasswordDialog() async { +void setPermanentPasswordDialog(OverlayDialogManager dialogManager) async { final pw = await bind.mainGetPermanentPassword(); final p0 = TextEditingController(text: pw); final p1 = TextEditingController(text: pw); var validateLength = false; var validateSame = false; - DialogManager.show((setState, close) { + dialogManager.show((setState, close) { return CustomAlertDialog( title: Text(translate('Set your own password')), content: Form( @@ -86,7 +85,7 @@ void setPermanentPasswordDialog() async { onPressed: (validateLength && validateSame) ? () async { close(); - showLoading(translate("Waiting")); + dialogManager.showLoading(translate("Waiting")); if (await gFFI.serverModel.setPermanentPassword(p0.text)) { showSuccess(); } else { @@ -101,13 +100,14 @@ void setPermanentPasswordDialog() async { }); } -void setTemporaryPasswordLengthDialog() async { +void setTemporaryPasswordLengthDialog( + OverlayDialogManager dialogManager) async { List lengths = ['6', '8', '10']; String length = await bind.mainGetOption(key: "temporary-password-length"); var index = lengths.indexOf(length); if (index < 0) index = 0; length = lengths[index]; - DialogManager.show((setState, close) { + dialogManager.show((setState, close) { final setLength = (newValue) { final oldValue = length; if (oldValue == newValue) return; @@ -133,10 +133,11 @@ void setTemporaryPasswordLengthDialog() async { }, backDismiss: true, clickMaskDismiss: true); } -void enterPasswordDialog(String id) async { +void enterPasswordDialog(String id, OverlayDialogManager dialogManager) async { final controller = TextEditingController(); var remember = await bind.getSessionRemember(id: id) ?? false; - DialogManager.show((setState, close) { + dialogManager.dismissAll(); + dialogManager.show((setState, close) { return CustomAlertDialog( title: Text(translate('Password Required')), content: Column(mainAxisSize: MainAxisSize.min, children: [ @@ -161,7 +162,7 @@ void enterPasswordDialog(String id) async { style: flatButtonStyle, onPressed: () { close(); - backToHome(); + backToHomePage(); }, child: Text(translate('Cancel')), ), @@ -172,7 +173,8 @@ void enterPasswordDialog(String id) async { if (text == '') return; gFFI.login(id, text, remember); close(); - showLoading(translate('Logging in...')); + dialogManager.showLoading(translate('Logging in...'), + cancelToClose: true); }, child: Text(translate('OK')), ), @@ -181,8 +183,8 @@ void enterPasswordDialog(String id) async { }); } -void wrongPasswordDialog(String id) { - DialogManager.show((setState, close) => CustomAlertDialog( +void wrongPasswordDialog(String id, OverlayDialogManager dialogManager) { + dialogManager.show((setState, close) => CustomAlertDialog( title: Text(translate('Wrong Password')), content: Text(translate('Do you want to enter again?')), actions: [ @@ -190,14 +192,14 @@ void wrongPasswordDialog(String id) { style: flatButtonStyle, onPressed: () { close(); - backToHome(); + backToHomePage(); }, child: Text(translate('Cancel')), ), TextButton( style: flatButtonStyle, onPressed: () { - enterPasswordDialog(id); + enterPasswordDialog(id, dialogManager); }, child: Text(translate('Retry')), ), @@ -239,8 +241,8 @@ class _PasswordWidgetState extends State { //This will obscure text dynamically keyboardType: TextInputType.visiblePassword, decoration: InputDecoration( - labelText: Translator.call('Password'), - hintText: Translator.call('Enter your password'), + labelText: translate('Password'), + hintText: translate('Enter your password'), // Here is key idea suffixIcon: IconButton( icon: Icon( diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index 75f3f8045..74be258a0 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -4,7 +4,6 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/mobile/pages/file_manager_page.dart'; -import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:path/path.dart' as Path; @@ -126,9 +125,9 @@ class FileModel extends ChangeNotifier { final _jobResultListener = JobResultListener>(); - final WeakReference _ffi; + final WeakReference parent; - FileModel(this._ffi); + FileModel(this.parent); toggleSelectMode() { if (jobState == JobState.inProgress) { @@ -275,7 +274,7 @@ class FileModel extends ChangeNotifier { need_override = true; } bind.sessionSetConfirmOverrideFile( - id: _ffi.target?.id ?? "", + id: parent.target?.id ?? "", actId: id, fileNum: int.parse(evt['file_num']), needOverride: need_override, @@ -292,22 +291,22 @@ class FileModel extends ChangeNotifier { onReady() async { _localOption.home = await bind.mainGetHomeDir(); _localOption.showHidden = (await bind.sessionGetPeerOption( - id: _ffi.target?.id ?? "", name: "local_show_hidden")) + id: parent.target?.id ?? "", name: "local_show_hidden")) .isNotEmpty; _remoteOption.showHidden = (await bind.sessionGetPeerOption( - id: _ffi.target?.id ?? "", name: "remote_show_hidden")) + id: parent.target?.id ?? "", name: "remote_show_hidden")) .isNotEmpty; - _remoteOption.isWindows = _ffi.target?.ffiModel.pi.platform == "Windows"; + _remoteOption.isWindows = parent.target?.ffiModel.pi.platform == "Windows"; - debugPrint("remote platform: ${_ffi.target?.ffiModel.pi.platform}"); + debugPrint("remote platform: ${parent.target?.ffiModel.pi.platform}"); await Future.delayed(Duration(milliseconds: 100)); final local = (await bind.sessionGetPeerOption( - id: _ffi.target?.id ?? "", name: "local_dir")); + id: parent.target?.id ?? "", name: "local_dir")); final remote = (await bind.sessionGetPeerOption( - id: _ffi.target?.id ?? "", name: "remote_dir")); + id: parent.target?.id ?? "", name: "remote_dir")); openDirectory(local.isEmpty ? _localOption.home : local, isLocal: true); openDirectory(remote.isEmpty ? _remoteOption.home : remote, isLocal: false); await Future.delayed(Duration(seconds: 1)); @@ -318,11 +317,11 @@ class FileModel extends ChangeNotifier { openDirectory(_remoteOption.home, isLocal: false); } // load last transfer jobs - await bind.sessionLoadLastTransferJobs(id: '${_ffi.target?.id}'); + await bind.sessionLoadLastTransferJobs(id: '${parent.target?.id}'); } onClose() { - SmartDialog.dismiss(); + parent.target?.dialogManager.dismissAll(); jobReset(); // save config @@ -332,7 +331,7 @@ class FileModel extends ChangeNotifier { msgMap["local_show_hidden"] = _localOption.showHidden ? "Y" : ""; msgMap["remote_dir"] = _currentRemoteDir.path; msgMap["remote_show_hidden"] = _remoteOption.showHidden ? "Y" : ""; - final id = _ffi.target?.id ?? ""; + final id = parent.target?.id ?? ""; for (final msg in msgMap.entries) { bind.sessionPeerOption(id: id, name: msg.key, value: msg.value); } @@ -419,7 +418,7 @@ class FileModel extends ChangeNotifier { ..id = jobId ..isRemote = isRemote); bind.sessionSendFiles( - id: '${_ffi.target?.id}', + id: '${parent.target?.id}', actId: _jobId, path: from.path, to: PathUtil.join(toPath, from.name, isWindows), @@ -477,14 +476,14 @@ class FileModel extends ChangeNotifier { entries = [item]; } else if (item.isDirectory) { title = translate("Not an empty directory"); - showLoading(translate("Waiting")); + parent.target?.dialogManager.showLoading(translate("Waiting")); final fd = await _fileFetcher.fetchDirectoryRecursive( _jobId, item.path, items.isLocal!, true); if (fd.path.isEmpty) { fd.path = item.path; } fd.format(isWindows); - SmartDialog.dismiss(); + parent.target?.dialogManager.dismissAll(); if (fd.entries.isEmpty) { final confirm = await showRemoveDialog( translate( @@ -543,7 +542,7 @@ class FileModel extends ChangeNotifier { Future showRemoveDialog( String title, String content, bool showCheckbox) async { - return await DialogManager.show( + return await parent.target?.dialogManager.show( (setState, Function(bool v) close) => CustomAlertDialog( title: Row( children: [ @@ -594,7 +593,7 @@ class FileModel extends ChangeNotifier { Future showFileConfirmDialog( String title, String content, bool showCheckbox) async { fileConfirmCheckboxRemember = false; - return await DialogManager.show( + return await parent.target?.dialogManager.show( (setState, Function(bool? v) close) => CustomAlertDialog( title: Row( children: [ @@ -648,7 +647,7 @@ class FileModel extends ChangeNotifier { sendRemoveFile(String path, int fileNum, bool isLocal) { bind.sessionRemoveFile( - id: '${_ffi.target?.id}', + id: '${parent.target?.id}', actId: _jobId, path: path, isRemote: !isLocal, @@ -657,7 +656,7 @@ class FileModel extends ChangeNotifier { sendRemoveEmptyDir(String path, int fileNum, bool isLocal) { bind.sessionRemoveAllEmptyDirs( - id: '${_ffi.target?.id}', + id: '${parent.target?.id}', actId: _jobId, path: path, isRemote: !isLocal); @@ -667,14 +666,14 @@ class FileModel extends ChangeNotifier { isLocal = isLocal ?? this.isLocal; _jobId++; bind.sessionCreateDir( - id: '${_ffi.target?.id}', + id: '${parent.target?.id}', actId: _jobId, path: path, isRemote: !isLocal); } cancelJob(int id) async { - bind.sessionCancelJob(id: '${_ffi.target?.id}', actId: id); + bind.sessionCancelJob(id: '${parent.target?.id}', actId: id); jobReset(); } @@ -701,7 +700,7 @@ class FileModel extends ChangeNotifier { } initFileFetcher() { - _fileFetcher.id = _ffi.target?.id; + _fileFetcher.id = parent.target?.id; } void updateFolderFiles(Map evt) { @@ -742,7 +741,7 @@ class FileModel extends ChangeNotifier { ..state = JobState.paused; jobTable.add(jobProgress); bind.sessionAddJob( - id: '${_ffi.target?.id}', + id: '${parent.target?.id}', isRemote: isRemote, includeHidden: showHidden, actId: currJobId, @@ -757,7 +756,7 @@ class FileModel extends ChangeNotifier { if (jobIndex != -1) { final job = jobTable[jobIndex]; bind.sessionResumeJob( - id: '${_ffi.target?.id}', actId: job.id, isRemote: job.isRemote); + id: '${parent.target?.id}', actId: job.id, isRemote: job.isRemote); job.state = JobState.inProgress; } else { debugPrint("jobId ${jobId} is not exists"); diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 4f295e377..a52947c74 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -13,7 +13,6 @@ import 'package:flutter_hbb/models/chat_model.dart'; import 'package:flutter_hbb/models/file_model.dart'; import 'package:flutter_hbb/models/server_model.dart'; import 'package:flutter_hbb/models/user_model.dart'; -import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:tuple/tuple.dart'; @@ -60,7 +59,6 @@ class FfiModel with ChangeNotifier { } FfiModel(this.parent) { - Translator.call = translate; clear(); } @@ -261,32 +259,35 @@ class FfiModel with ChangeNotifier { /// Handle the message box event based on [evt] and [id]. void handleMsgBox(Map evt, String id) { + if (parent.target == null) return; + final dialogManager = parent.target!.dialogManager; var type = evt['type']; var title = evt['title']; var text = evt['text']; if (type == 're-input-password') { - wrongPasswordDialog(id); + wrongPasswordDialog(id, dialogManager); } else if (type == 'input-password') { - enterPasswordDialog(id); + enterPasswordDialog(id, dialogManager); } else if (type == 'restarting') { - showMsgBox(id, type, title, text, false, hasCancel: false); + showMsgBox(id, type, title, text, false, dialogManager, hasCancel: false); } else { var hasRetry = evt['hasRetry'] == 'true'; - showMsgBox(id, type, title, text, hasRetry); + showMsgBox(id, type, title, text, hasRetry, dialogManager); } } /// Show a message box with [type], [title] and [text]. - void showMsgBox( - String id, String type, String title, String text, bool hasRetry, + void showMsgBox(String id, String type, String title, String text, + bool hasRetry, OverlayDialogManager dialogManager, {bool? hasCancel}) { - msgBox(type, title, text, hasCancel: hasCancel); + msgBox(type, title, text, dialogManager, hasCancel: hasCancel); _timer?.cancel(); if (hasRetry) { _timer = Timer(Duration(seconds: _reconnects), () { bind.sessionReconnect(id: id); clearPermissions(); - showLoading(translate('Connecting...')); + dialogManager.showLoading(translate('Connecting...'), + cancelToClose: true); }); _reconnects *= 2; } else { @@ -296,7 +297,7 @@ class FfiModel with ChangeNotifier { /// Handle the peer info event based on [evt]. void handlePeerInfo(Map evt, String peerId) async { - SmartDialog.dismiss(); + parent.target?.dialogManager.dismissAll(); _pi.version = evt['version']; _pi.username = evt['username']; _pi.hostname = evt['hostname']; @@ -332,7 +333,9 @@ class FfiModel with ChangeNotifier { _display = _pi.displays[_pi.currentDisplay]; } if (displays.length > 0) { - showLoading(translate('Connected, waiting for image...')); + parent.target?.dialogManager.showLoading( + translate('Connected, waiting for image...'), + cancelToClose: true); _waitForImage = true; _reconnects = 1; } @@ -364,7 +367,7 @@ class ImageModel with ChangeNotifier { void onRgba(Uint8List rgba, double tabBarHeight) { if (_waitForImage) { _waitForImage = false; - SmartDialog.dismiss(); + parent.target?.dialogManager.dismissAll(); } final pid = parent.target?.id; ui.decodeImageFromPixels( @@ -874,16 +877,20 @@ class FFI { var alt = false; var command = false; var version = ""; - late final ImageModel imageModel; - late final FfiModel ffiModel; - late final CursorModel cursorModel; - late final CanvasModel canvasModel; - late final ServerModel serverModel; - late final ChatModel chatModel; - late final FileModel fileModel; - late final AbModel abModel; - late final UserModel userModel; - late final QualityMonitorModel qualityMonitorModel; + + /// dialogManager use late to ensure init after main page binding [globalKey] + late final dialogManager = OverlayDialogManager(); + + late final ImageModel imageModel; // session + late final FfiModel ffiModel; // session + late final CursorModel cursorModel; // session + late final CanvasModel canvasModel; // session + late final ServerModel serverModel; // global + late final ChatModel chatModel; // session + late final FileModel fileModel; // session + late final AbModel abModel; // global + late final UserModel userModel; // global + late final QualityMonitorModel qualityMonitorModel; // session FFI() { this.imageModel = ImageModel(WeakReference(this)); diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index a55ed1d29..55f2d0e79 100644 --- a/flutter/lib/models/native_model.dart +++ b/flutter/lib/models/native_model.dart @@ -75,7 +75,7 @@ class PlatformFFI { } String translate(String name, String locale) { - if (_translate == null) return ''; + if (_translate == null) return name; var a = name.toNativeUtf8(); var b = locale.toNativeUtf8(); var p = _translate!(a, b); diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index d59ad49c2..6ed048dd4 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -10,7 +10,7 @@ import '../common.dart'; import '../mobile/pages/server_page.dart'; import 'model.dart'; -const loginDialogTag = "LOGIN"; +const KLoginDialogTag = "LOGIN"; const kUseTemporaryPassword = "use-temporary-password"; const kUsePermanentPassword = "use-permanent-password"; @@ -206,8 +206,8 @@ class ServerModel with ChangeNotifier { /// Toggle the screen sharing service. toggleService() async { if (_isStart) { - final res = - await DialogManager.show((setState, close) => CustomAlertDialog( + final res = await parent.target?.dialogManager + .show((setState, close) => CustomAlertDialog( title: Row(children: [ Icon(Icons.warning_amber_sharp, color: Colors.redAccent, size: 28), @@ -228,8 +228,8 @@ class ServerModel with ChangeNotifier { stopService(); } } else { - final res = - await DialogManager.show((setState, close) => CustomAlertDialog( + final res = await parent.target?.dialogManager + .show((setState, close) => CustomAlertDialog( title: Row(children: [ Icon(Icons.warning_amber_sharp, color: Colors.redAccent, size: 28), @@ -272,7 +272,7 @@ class ServerModel with ChangeNotifier { Future stopService() async { _isStart = false; // TODO - parent.target?.serverModel.closeAll(); + closeAll(); await parent.target?.invokeMethod("stop_service"); await bind.mainStopService(); notifyListeners(); @@ -370,7 +370,7 @@ class ServerModel with ChangeNotifier { } void showLoginDialog(Client client) { - DialogManager.show( + parent.target?.dialogManager.show( (setState, close) => CustomAlertDialog( title: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -442,7 +442,7 @@ class ServerModel with ChangeNotifier { void onClientAuthorized(Map evt) { try { final client = Client.fromJson(jsonDecode(evt['client'])); - DialogManager.dismissByTag(getLoginDialogTag(client.id)); + parent.target?.dialogManager.dismissByTag(getLoginDialogTag(client.id)); _clients[client.id] = client; scrollToBottom(); notifyListeners(); @@ -454,7 +454,7 @@ class ServerModel with ChangeNotifier { final id = int.parse(evt['id'] as String); if (_clients.containsKey(id)) { _clients.remove(id); - DialogManager.dismissByTag(getLoginDialogTag(id)); + parent.target?.dialogManager.dismissByTag(getLoginDialogTag(id)); parent.target?.invokeMethod("cancel_notification", id); } notifyListeners(); @@ -510,11 +510,11 @@ class Client { } String getLoginDialogTag(int id) { - return loginDialogTag + id.toString(); + return KLoginDialogTag + id.toString(); } showInputWarnAlert(FFI ffi) { - DialogManager.show((setState, close) => CustomAlertDialog( + ffi.dialogManager.show((setState, close) => CustomAlertDialog( title: Text(translate("How to get Android input permission?")), content: Column( mainAxisSize: MainAxisSize.min, diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 6bcf5a159..e0a7aa8eb 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -5,239 +5,246 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "44.0.0" after_layout: dependency: transitive description: name: after_layout - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.2.0" analyzer: dependency: transitive description: name: analyzer - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "4.4.0" animations: dependency: transitive description: name: animations - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.3" archive: dependency: transitive description: name: archive - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.3.1" args: dependency: transitive description: name: args - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.3.1" async: dependency: transitive description: name: async - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.8.2" + back_button_interceptor: + dependency: "direct main" + description: + name: back_button_interceptor + url: "https://pub.dartlang.org" + source: hosted + version: "6.0.1" boolean_selector: dependency: transitive description: name: boolean_selector - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.0" build: dependency: transitive description: name: build - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.3.0" build_config: dependency: transitive description: name: build_config - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.1.0" build_daemon: dependency: transitive description: name: build_daemon - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.1.0" build_resolvers: dependency: transitive description: name: build_resolvers - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.9" build_runner: dependency: "direct dev" description: name: build_runner - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.2.0" build_runner_core: dependency: transitive description: name: build_runner_core - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "7.2.3" built_collection: dependency: transitive description: name: built_collection - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "5.1.1" built_value: dependency: transitive description: name: built_value - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "8.4.0" cached_network_image: dependency: transitive description: name: cached_network_image - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.2.1" cached_network_image_platform_interface: dependency: transitive description: name: cached_network_image_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.0" cached_network_image_web: dependency: transitive description: name: cached_network_image_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.1" characters: dependency: transitive description: name: characters - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.2.0" charcode: dependency: transitive description: name: charcode - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.3.1" checked_yaml: dependency: transitive description: name: checked_yaml - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.1" clock: dependency: transitive description: name: clock - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.1.0" code_builder: dependency: transitive description: name: code_builder - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "4.2.0" collection: dependency: transitive description: name: collection - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.16.0" contextmenu: dependency: "direct main" description: name: contextmenu - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.0" convert: dependency: transitive description: name: convert - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.2" cross_file: dependency: transitive description: name: cross_file - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.3.3+1" crypto: dependency: transitive description: name: crypto - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.2" csslib: dependency: transitive description: name: csslib - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.17.2" cupertino_icons: dependency: "direct main" description: name: cupertino_icons - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.5" dart_style: dependency: transitive description: name: dart_style - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.2.3" dash_chat_2: dependency: "direct main" description: name: dash_chat_2 - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.0.12" desktop_multi_window: dependency: "direct main" description: path: "." - ref: bbe24b8af079a756f2d39158dd2034127f0e1c73 - resolved-ref: bbe24b8af079a756f2d39158dd2034127f0e1c73 + ref: c53879e9ce4ed038af393a02bf2c7084ad4b53aa + resolved-ref: c53879e9ce4ed038af393a02bf2c7084ad4b53aa url: "https://github.com/Kingtous/rustdesk_desktop_multi_window" source: git version: "0.1.0" @@ -245,133 +252,133 @@ packages: dependency: "direct main" description: name: device_info_plus - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "4.0.2" device_info_plus_linux: dependency: transitive description: name: device_info_plus_linux - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.1" device_info_plus_macos: dependency: transitive description: name: device_info_plus_macos - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.2.3" device_info_plus_platform_interface: dependency: transitive description: name: device_info_plus_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.4.0" device_info_plus_web: dependency: transitive description: name: device_info_plus_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.0" device_info_plus_windows: dependency: transitive description: name: device_info_plus_windows - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.2" draggable_float_widget: dependency: "direct main" description: name: draggable_float_widget - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.0.2" event_bus: dependency: transitive description: name: event_bus - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.0" external_path: dependency: "direct main" description: name: external_path - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.1" fake_async: dependency: transitive description: name: fake_async - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.3.0" ffi: dependency: "direct main" description: name: ffi - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.1" file: dependency: transitive description: name: file - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "6.1.2" firebase_analytics: dependency: "direct main" description: name: firebase_analytics - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "9.3.0" firebase_analytics_platform_interface: dependency: transitive description: name: firebase_analytics_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.3.0" firebase_analytics_web: dependency: transitive description: name: firebase_analytics_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.4.2" firebase_core: dependency: transitive description: name: firebase_core - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.20.0" firebase_core_platform_interface: dependency: transitive description: name: firebase_core_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "4.5.0" firebase_core_web: dependency: transitive description: name: firebase_core_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.7.1" fixnum: dependency: transitive description: name: fixnum - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.1" flutter: @@ -383,42 +390,42 @@ packages: dependency: transitive description: name: flutter_blurhash - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.7.0" flutter_breadcrumb: dependency: "direct main" description: name: flutter_breadcrumb - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.1" flutter_cache_manager: dependency: transitive description: name: flutter_cache_manager - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.3.0" flutter_launcher_icons: dependency: "direct dev" description: name: flutter_launcher_icons - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.9.3" flutter_parsed_text: dependency: transitive description: name: flutter_parsed_text - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.2.1" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.7" flutter_rust_bridge: @@ -430,13 +437,6 @@ packages: url: "https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge" source: git version: "1.32.0" - flutter_smart_dialog: - dependency: "direct main" - description: - name: flutter_smart_dialog - url: "https://pub.flutter-io.cn" - source: hosted - version: "4.5.4+1" flutter_test: dependency: "direct dev" description: flutter @@ -451,476 +451,476 @@ packages: dependency: "direct dev" description: name: freezed - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.0+1" freezed_annotation: dependency: "direct main" description: name: freezed_annotation - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.0" frontend_server_client: dependency: transitive description: name: frontend_server_client - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.3" get: dependency: "direct main" description: name: get - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "4.6.5" glob: dependency: transitive description: name: glob - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.0" graphs: dependency: transitive description: name: graphs - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.0" html: dependency: transitive description: name: html - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.15.0" http: dependency: "direct main" description: name: http - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.13.5" http_multi_server: dependency: transitive description: name: http_multi_server - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.2.1" http_parser: dependency: transitive description: name: http_parser - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "4.0.1" image: dependency: "direct main" description: name: image - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.2.0" image_picker: dependency: "direct main" description: name: image_picker - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.8.5+3" image_picker_android: dependency: transitive description: name: image_picker_android - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.8.5+2" image_picker_for_web: dependency: transitive description: name: image_picker_for_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.8" image_picker_ios: dependency: transitive description: name: image_picker_ios - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.8.5+6" image_picker_platform_interface: dependency: transitive description: name: image_picker_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.6.1" intl: dependency: transitive description: name: intl - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.17.0" io: dependency: transitive description: name: io - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.3" js: dependency: transitive description: name: js - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.6.4" json_annotation: dependency: transitive description: name: json_annotation - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "4.6.0" logging: dependency: transitive description: name: logging - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.2" matcher: dependency: transitive description: name: matcher - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.12.11" material_color_utilities: dependency: transitive description: name: material_color_utilities - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.1.4" menu_base: dependency: transitive description: name: menu_base - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.1.1" meta: dependency: transitive description: name: meta - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.7.0" mime: dependency: transitive description: name: mime - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.2" nested: dependency: transitive description: name: nested - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.0" octo_image: dependency: transitive description: name: octo_image - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.2" package_config: dependency: transitive description: name: package_config - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.0" package_info_plus: dependency: "direct main" description: name: package_info_plus - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.4.3" package_info_plus_linux: dependency: transitive description: name: package_info_plus_linux - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.5" package_info_plus_macos: dependency: transitive description: name: package_info_plus_macos - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.3.0" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.2" package_info_plus_web: dependency: transitive description: name: package_info_plus_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.5" package_info_plus_windows: dependency: transitive description: name: package_info_plus_windows - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.0" path: dependency: transitive description: name: path - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.8.1" path_provider: dependency: "direct main" description: name: path_provider - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.11" path_provider_android: dependency: transitive description: name: path_provider_android - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.17" path_provider_ios: dependency: transitive description: name: path_provider_ios - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.11" path_provider_linux: dependency: transitive description: name: path_provider_linux - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.7" path_provider_macos: dependency: transitive description: name: path_provider_macos - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.6" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.4" path_provider_windows: dependency: transitive description: name: path_provider_windows - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.2" pedantic: dependency: transitive description: name: pedantic - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.11.1" petitparser: dependency: transitive description: name: petitparser - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "5.0.0" platform: dependency: transitive description: name: platform - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.1.0" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.2" pool: dependency: transitive description: name: pool - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.5.1" process: dependency: transitive description: name: process - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "4.2.4" provider: dependency: "direct main" description: name: provider - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "6.0.3" pub_semver: dependency: transitive description: name: pub_semver - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.1" pubspec_parse: dependency: transitive description: name: pubspec_parse - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.2.0" qr_code_scanner: dependency: "direct main" description: name: qr_code_scanner - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.0" quiver: dependency: transitive description: name: quiver - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.1.0" rxdart: dependency: transitive description: name: rxdart - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.27.5" screen_retriever: dependency: transitive description: name: screen_retriever - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.1.2" settings_ui: dependency: "direct main" description: name: settings_ui - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.2" shared_preferences: dependency: "direct main" description: name: shared_preferences - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.15" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.12" shared_preferences_ios: dependency: transitive description: name: shared_preferences_ios - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.1" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.1" shared_preferences_macos: dependency: transitive description: name: shared_preferences_macos - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.4" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.0" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.4" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.1" shelf: dependency: transitive description: name: shelf - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.3.2" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.2" shortid: dependency: transitive description: name: shortid - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.1.2" sky_engine: @@ -932,280 +932,280 @@ packages: dependency: transitive description: name: source_gen - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.2.2" source_span: dependency: transitive description: name: source_span - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.8.2" sqflite: dependency: transitive description: name: sqflite - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.3+1" sqflite_common: dependency: transitive description: name: sqflite_common - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.2.1+1" stack_trace: dependency: transitive description: name: stack_trace - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.10.0" stream_channel: dependency: transitive description: name: stream_channel - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.0" stream_transform: dependency: transitive description: name: stream_transform - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.0" string_scanner: dependency: transitive description: name: string_scanner - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.1.0" synchronized: dependency: transitive description: name: synchronized - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.0+2" term_glyph: dependency: transitive description: name: term_glyph - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.2.0" test_api: dependency: transitive description: name: test_api - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.4.9" timing: dependency: transitive description: name: timing - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.0" toggle_switch: dependency: "direct main" description: name: toggle_switch - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.4.0" tray_manager: dependency: "direct main" description: name: tray_manager - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.1.7" tuple: dependency: "direct main" description: name: tuple - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.0" typed_data: dependency: transitive description: name: typed_data - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.3.1" url_launcher: dependency: "direct main" description: name: url_launcher - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "6.1.5" url_launcher_android: dependency: transitive description: name: url_launcher_android - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "6.0.17" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "6.0.17" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.1" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.0" url_launcher_web: dependency: transitive description: name: url_launcher_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.13" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.1" uuid: dependency: transitive description: name: uuid - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.6" vector_math: dependency: transitive description: name: vector_math - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.2" video_player: dependency: transitive description: name: video_player - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.4.6" video_player_android: dependency: transitive description: name: video_player_android - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.3.8" video_player_avfoundation: dependency: transitive description: name: video_player_avfoundation - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.3.5" video_player_platform_interface: dependency: transitive description: name: video_player_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "5.1.4" video_player_web: dependency: transitive description: name: video_player_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.12" visibility_detector: dependency: "direct main" description: name: visibility_detector - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.3.3" wakelock: dependency: "direct main" description: name: wakelock - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.5.6" wakelock_macos: dependency: transitive description: name: wakelock_macos - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.4.0" wakelock_platform_interface: dependency: transitive description: name: wakelock_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.3.0" wakelock_web: dependency: transitive description: name: wakelock_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.4.0" wakelock_windows: dependency: transitive description: name: wakelock_windows - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.2.0" watcher: dependency: transitive description: name: watcher - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.1" web_socket_channel: dependency: transitive description: name: web_socket_channel - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.2.0" win32: dependency: transitive description: name: win32 - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.7.0" window_manager: @@ -1221,28 +1221,28 @@ packages: dependency: transitive description: name: xdg_directories - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.2.0+1" xml: dependency: transitive description: name: xml - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "6.1.0" yaml: dependency: transitive description: name: yaml - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.1.1" zxing2: dependency: "direct main" description: name: zxing2 - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.1.0" sdks: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 4ecce228a..b8b9580fb 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -49,7 +49,7 @@ dependencies: zxing2: ^0.1.0 image_picker: ^0.8.5 image: ^3.1.3 - flutter_smart_dialog: ^4.3.1 + back_button_interceptor: ^6.0.1 flutter_rust_bridge: git: url: https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge From e7e846cd42a1297f5ecd31ba1b4167f17e9dbfc7 Mon Sep 17 00:00:00 2001 From: Asura Date: Fri, 12 Aug 2022 19:32:42 +0800 Subject: [PATCH 0198/2015] Fix mouse input error #1032 --- src/ui/remote.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 3d90ac73f..bcb79c522 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -301,6 +301,8 @@ impl Handler { m.insert(RdevKey::ControlRight, false); m.insert(RdevKey::Alt, false); m.insert(RdevKey::AltGr, false); + m.insert(RdevKey::MetaLeft, false); + m.insert(RdevKey::MetaRight, false); Mutex::new(m) }; } @@ -816,7 +818,7 @@ impl Handler { command = true; } } - + send_mouse(mask, x, y, alt, ctrl, shift, command, self); // on macos, ctrl + left button down = right button down, up won't emit, so we need to // emit up myself if peer is not macos @@ -1379,7 +1381,6 @@ impl Handler { if down_or_up == true { key_event.down = true; } - dbg!(&key_event); self.send_key_event(key_event, KeyboardMode::Legacy) } From 48ab5e502402ff83e6882ada59bb2324993405ee Mon Sep 17 00:00:00 2001 From: Asura Date: Sat, 13 Aug 2022 08:12:45 +0800 Subject: [PATCH 0199/2015] Fix command+tab #1032 --- src/server/input_service.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 2093395c8..c01212184 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -683,6 +683,14 @@ fn legacy_keyboard_mode(evt: &KeyEvent) { let mut disable_numlock = false; #[cfg(target_os = "macos")] en.reset_flag(); + // When long-pressed the command key, then press and release + // the Tab key, there should be CGEventFlagCommand in the flag. + #[cfg(target_os = "macos")] + for ck in evt.modifiers.iter(){ + if let Some(key) = KEY_MAP.get(&ck.value()){ + en.add_flag(key); + } + } #[cfg(not(target_os = "macos"))] let mut to_release = Vec::new(); #[cfg(not(target_os = "macos"))] @@ -710,8 +718,6 @@ fn legacy_keyboard_mode(evt: &KeyEvent) { continue; } } - #[cfg(target_os = "macos")] - en.add_flag(key); #[cfg(not(target_os = "macos"))] { if key == &Key::CapsLock { From 4e4f83716029778b619cb418594b4c3280d8e784 Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 11 Aug 2022 00:12:47 +0800 Subject: [PATCH 0200/2015] flutter_desktop: scroll, mid commit Signed-off-by: fufesou --- flutter/lib/consts.dart | 3 + flutter/lib/desktop/pages/remote_page.dart | 65 +++- flutter/lib/models/model.dart | 19 +- flutter/pubspec.lock | 364 ++++++++++----------- 4 files changed, 262 insertions(+), 189 deletions(-) diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 662f7cbd2..466b4b74a 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -4,3 +4,6 @@ const String kAppTypeDesktopRemote = "remote"; const String kAppTypeDesktopFileTransfer = "file transfer"; const String kTabLabelHomePage = "Home"; const String kTabLabelSettingPage = "Settings"; + +const int kDefaultDisplayWidth = 1280; +const int kDefaultDisplayHeight = 720; diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index d81adb3d9..b1dcb1620 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -14,6 +14,7 @@ import 'package:wakelock/wakelock.dart'; // import 'package:window_manager/window_manager.dart'; import '../../common.dart'; +import '../../consts.dart'; import '../../mobile/widgets/dialog.dart'; import '../../mobile/widgets/overlay.dart'; import '../../models/model.dart'; @@ -822,18 +823,58 @@ class _RemotePageState extends State class ImagePaint extends StatelessWidget { final String id; + final ScrollController _horizontal = ScrollController(); + final ScrollController _vertical = ScrollController(); - const ImagePaint({Key? key, required this.id}) : super(key: key); + ImagePaint({Key? key, required this.id}) : super(key: key); @override Widget build(BuildContext context) { final m = Provider.of(context); final c = Provider.of(context); var s = c.scale; - return CustomPaint( - painter: - new ImagePainter(image: m.image, x: c.x / s, y: c.y / s, scale: s), - ); + final paintChild = SizedBox( + width: (m.image?.width ?? kDefaultDisplayWidth) * s, + height: (m.image?.height ?? kDefaultDisplayHeight) * s, + child: CustomPaint( + painter: new ImagePainter( + // image: m.image, x: c.x / s, y: c.y / s, scale: s), + image: m.image, + x: 0, + y: 0, + scale: s), + )); + + if (c.scrollStyle == ScrollStyle.scrollbar) { + return Center( + child: Scrollbar( + controller: _vertical, + thumbVisibility: true, + trackVisibility: true, + child: Scrollbar( + controller: _horizontal, + thumbVisibility: true, + trackVisibility: true, + notificationPredicate: (notif) => notif.depth == 1, + child: SingleChildScrollView( + controller: _vertical, + child: SingleChildScrollView( + controller: _horizontal, + scrollDirection: Axis.horizontal, + child: paintChild), + ), + ), + )); + } else { + return Center( + child: InteractiveViewer( + // boundaryMargin: const EdgeInsets.all(20.0), + // minScale: 0.1, + // maxScale: 1.6, + scaleEnabled: false, + child: paintChild, + )); + } } } @@ -896,6 +937,8 @@ void showOptions(String id, OverlayDialogManager dialogManager) async { if (quality == '') quality = 'balanced'; String viewStyle = await bind.getSessionOption(id: id, arg: 'view-style') ?? ''; + String scrollStyle = + await bind.getSessionOption(id: id, arg: 'scroll-style') ?? ''; var displays = []; final pi = ffi(id).ffiModel.pi; final image = ffi(id).ffiModel.getConnectionImage(); @@ -968,6 +1011,14 @@ void showOptions(String id, OverlayDialogManager dialogManager) async { ffi(id).canvasModel.updateViewStyle(); }); }; + var setScrollStyle = (String? value) { + if (value == null) return; + setState(() { + scrollStyle = value; + bind.sessionPeerOption(id: id, name: "scroll-style", value: value); + ffi(id).canvasModel.updateScrollStyle(); + }); + }; return CustomAlertDialog( title: SizedBox.shrink(), content: Column( @@ -978,6 +1029,10 @@ void showOptions(String id, OverlayDialogManager dialogManager) async { getRadio('Shrink', 'shrink', viewStyle, setViewStyle), getRadio('Stretch', 'stretch', viewStyle, setViewStyle), Divider(color: MyTheme.border), + getRadio('Scrollbar', 'scrollbar', scrollStyle, setScrollStyle), + getRadio( + 'ScrollMouse', 'scrollmouse', scrollStyle, setScrollStyle), + Divider(color: MyTheme.border), getRadio('Good image quality', 'best', quality, setQuality), getRadio('Balanced', 'balanced', quality, setQuality), getRadio('Optimize reaction time', 'low', quality, setQuality), diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index a52947c74..c98bfb334 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -432,22 +432,27 @@ class ImageModel with ChangeNotifier { } } +enum ScrollStyle { + scrollbar, + scrollmouse, +} + class CanvasModel with ChangeNotifier { double _x = 0; double _y = 0; double _scale = 1.0; double _tabBarHeight = 0.0; String id = ""; // TODO multi canvas model + ScrollStyle _scrollStyle = ScrollStyle.scrollbar; WeakReference parent; CanvasModel(this.parent); double get x => _x; - double get y => _y; - double get scale => _scale; + ScrollStyle get scrollStyle => _scrollStyle; set tabBarHeight(double h) => _tabBarHeight = h; double get tabBarHeight => _tabBarHeight; @@ -497,6 +502,16 @@ class CanvasModel with ChangeNotifier { notifyListeners(); } + void updateScrollStyle() async { + final s = await bind.getSessionOption(id: id, arg: 'scroll-style'); + if (s == 'scrollmouse') { + _scrollStyle = ScrollStyle.scrollmouse; + } else { + _scrollStyle = ScrollStyle.scrollbar; + } + notifyListeners(); + } + void update(double x, double y, double scale) { _x = x; _y = y; diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index e0a7aa8eb..fcefcca82 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -5,238 +5,238 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "44.0.0" + version: "46.0.0" after_layout: dependency: transitive description: name: after_layout - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.0" analyzer: dependency: transitive description: name: analyzer - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "4.4.0" + version: "4.6.0" animations: dependency: transitive description: name: animations - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.3" archive: dependency: transitive description: name: archive - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.3.1" args: dependency: transitive description: name: args - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.3.1" async: dependency: transitive description: name: async - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.8.2" back_button_interceptor: dependency: "direct main" description: name: back_button_interceptor - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "6.0.1" boolean_selector: dependency: transitive description: name: boolean_selector - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0" build: dependency: transitive description: name: build - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.3.0" build_config: dependency: transitive description: name: build_config - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.0" build_daemon: dependency: transitive description: name: build_daemon - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.0" build_resolvers: dependency: transitive description: name: build_resolvers - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.9" build_runner: dependency: "direct dev" description: name: build_runner - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.0" build_runner_core: dependency: transitive description: name: build_runner_core - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "7.2.3" built_collection: dependency: transitive description: name: built_collection - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "5.1.1" built_value: dependency: transitive description: name: built_value - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "8.4.0" cached_network_image: dependency: transitive description: name: cached_network_image - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.2.1" cached_network_image_platform_interface: dependency: transitive description: name: cached_network_image_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.0" cached_network_image_web: dependency: transitive description: name: cached_network_image_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.1" characters: dependency: transitive description: name: characters - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.0" charcode: dependency: transitive description: name: charcode - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.1" checked_yaml: dependency: transitive description: name: checked_yaml - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.1" clock: dependency: transitive description: name: clock - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.0" code_builder: dependency: transitive description: name: code_builder - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "4.2.0" collection: dependency: transitive description: name: collection - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.16.0" contextmenu: dependency: "direct main" description: name: contextmenu - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.0" convert: dependency: transitive description: name: convert - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.2" cross_file: dependency: transitive description: name: cross_file - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.3.3+1" crypto: dependency: transitive description: name: crypto - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.2" csslib: dependency: transitive description: name: csslib - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.17.2" cupertino_icons: dependency: "direct main" description: name: cupertino_icons - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.5" dart_style: dependency: transitive description: name: dart_style - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.3" dash_chat_2: dependency: "direct main" description: name: dash_chat_2 - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.0.12" desktop_multi_window: @@ -252,133 +252,133 @@ packages: dependency: "direct main" description: name: device_info_plus - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "4.0.2" + version: "4.1.0" device_info_plus_linux: dependency: transitive description: name: device_info_plus_linux - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.1" device_info_plus_macos: dependency: transitive description: name: device_info_plus_macos - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.3" device_info_plus_platform_interface: dependency: transitive description: name: device_info_plus_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "2.4.0" + version: "2.6.0" device_info_plus_web: dependency: transitive description: name: device_info_plus_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0" device_info_plus_windows: dependency: transitive description: name: device_info_plus_windows - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "3.0.2" + version: "3.0.3" draggable_float_widget: dependency: "direct main" description: name: draggable_float_widget - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.0.2" event_bus: dependency: transitive description: name: event_bus - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.0" external_path: dependency: "direct main" description: name: external_path - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.1" fake_async: dependency: transitive description: name: fake_async - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.0" ffi: dependency: "direct main" description: name: ffi - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.1" file: dependency: transitive description: name: file - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "6.1.2" firebase_analytics: dependency: "direct main" description: name: firebase_analytics - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "9.3.0" + version: "9.3.1" firebase_analytics_platform_interface: dependency: transitive description: name: firebase_analytics_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "3.3.0" + version: "3.3.1" firebase_analytics_web: dependency: transitive description: name: firebase_analytics_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "0.4.2" + version: "0.4.2+1" firebase_core: dependency: transitive description: name: firebase_core - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "1.20.0" + version: "1.20.1" firebase_core_platform_interface: dependency: transitive description: name: firebase_core_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "4.5.0" firebase_core_web: dependency: transitive description: name: firebase_core_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.7.1" fixnum: dependency: transitive description: name: fixnum - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.1" flutter: @@ -390,42 +390,42 @@ packages: dependency: transitive description: name: flutter_blurhash - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.7.0" flutter_breadcrumb: dependency: "direct main" description: name: flutter_breadcrumb - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.1" flutter_cache_manager: dependency: transitive description: name: flutter_cache_manager - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.3.0" flutter_launcher_icons: dependency: "direct dev" description: name: flutter_launcher_icons - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.9.3" flutter_parsed_text: dependency: transitive description: name: flutter_parsed_text - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.1" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.7" flutter_rust_bridge: @@ -451,476 +451,476 @@ packages: dependency: "direct dev" description: name: freezed - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0+1" freezed_annotation: dependency: "direct main" description: name: freezed_annotation - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0" frontend_server_client: dependency: transitive description: name: frontend_server_client - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.3" get: dependency: "direct main" description: name: get - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "4.6.5" glob: dependency: transitive description: name: glob - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0" graphs: dependency: transitive description: name: graphs - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0" html: dependency: transitive description: name: html - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.15.0" http: dependency: "direct main" description: name: http - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.13.5" http_multi_server: dependency: transitive description: name: http_multi_server - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.2.1" http_parser: dependency: transitive description: name: http_parser - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "4.0.1" image: dependency: "direct main" description: name: image - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.2.0" image_picker: dependency: "direct main" description: name: image_picker - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.8.5+3" image_picker_android: dependency: transitive description: name: image_picker_android - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.8.5+2" image_picker_for_web: dependency: transitive description: name: image_picker_for_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.8" image_picker_ios: dependency: transitive description: name: image_picker_ios - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.8.5+6" image_picker_platform_interface: dependency: transitive description: name: image_picker_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.6.1" intl: dependency: transitive description: name: intl - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.17.0" io: dependency: transitive description: name: io - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.3" js: dependency: transitive description: name: js - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.6.4" json_annotation: dependency: transitive description: name: json_annotation - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "4.6.0" logging: dependency: transitive description: name: logging - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.2" matcher: dependency: transitive description: name: matcher - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.12.11" material_color_utilities: dependency: transitive description: name: material_color_utilities - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.1.4" menu_base: dependency: transitive description: name: menu_base - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.1.1" meta: dependency: transitive description: name: meta - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.7.0" mime: dependency: transitive description: name: mime - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.2" nested: dependency: transitive description: name: nested - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.0" octo_image: dependency: transitive description: name: octo_image - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.2" package_config: dependency: transitive description: name: package_config - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0" package_info_plus: dependency: "direct main" description: name: package_info_plus - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "1.4.3" + version: "1.4.3+1" package_info_plus_linux: dependency: transitive description: name: package_info_plus_linux - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.5" package_info_plus_macos: dependency: transitive description: name: package_info_plus_macos - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.0" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.2" package_info_plus_web: dependency: transitive description: name: package_info_plus_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.5" package_info_plus_windows: dependency: transitive description: name: package_info_plus_windows - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.0" path: dependency: transitive description: name: path - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.8.1" path_provider: dependency: "direct main" description: name: path_provider - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.11" path_provider_android: dependency: transitive description: name: path_provider_android - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "2.0.17" + version: "2.0.19" path_provider_ios: dependency: transitive description: name: path_provider_ios - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.11" path_provider_linux: dependency: transitive description: name: path_provider_linux - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.7" path_provider_macos: dependency: transitive description: name: path_provider_macos - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.6" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.4" path_provider_windows: dependency: transitive description: name: path_provider_windows - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.2" pedantic: dependency: transitive description: name: pedantic - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.11.1" petitparser: dependency: transitive description: name: petitparser - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "5.0.0" platform: dependency: transitive description: name: platform - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.0" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.2" pool: dependency: transitive description: name: pool - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.5.1" process: dependency: transitive description: name: process - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "4.2.4" provider: dependency: "direct main" description: name: provider - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "6.0.3" pub_semver: dependency: transitive description: name: pub_semver - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.1" pubspec_parse: dependency: transitive description: name: pubspec_parse - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.0" qr_code_scanner: dependency: "direct main" description: name: qr_code_scanner - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.0" quiver: dependency: transitive description: name: quiver - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.0" rxdart: dependency: transitive description: name: rxdart - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.27.5" screen_retriever: dependency: transitive description: name: screen_retriever - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.1.2" settings_ui: dependency: "direct main" description: name: settings_ui - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.2" shared_preferences: dependency: "direct main" description: name: shared_preferences - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.15" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.12" shared_preferences_ios: dependency: transitive description: name: shared_preferences_ios - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.1" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.1" shared_preferences_macos: dependency: transitive description: name: shared_preferences_macos - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.4" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.0" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.4" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.1" shelf: dependency: transitive description: name: shelf - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.2" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.2" shortid: dependency: transitive description: name: shortid - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.1.2" sky_engine: @@ -932,280 +932,280 @@ packages: dependency: transitive description: name: source_gen - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.2" source_span: dependency: transitive description: name: source_span - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.8.2" sqflite: dependency: transitive description: name: sqflite - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.3+1" sqflite_common: dependency: transitive description: name: sqflite_common - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.1+1" stack_trace: dependency: transitive description: name: stack_trace - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.10.0" stream_channel: dependency: transitive description: name: stream_channel - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0" stream_transform: dependency: transitive description: name: stream_transform - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.0" string_scanner: dependency: transitive description: name: string_scanner - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.0" synchronized: dependency: transitive description: name: synchronized - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.0+2" term_glyph: dependency: transitive description: name: term_glyph - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.0" test_api: dependency: transitive description: name: test_api - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.4.9" timing: dependency: transitive description: name: timing - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.0" toggle_switch: dependency: "direct main" description: name: toggle_switch - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.4.0" tray_manager: dependency: "direct main" description: name: tray_manager - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.1.7" tuple: dependency: "direct main" description: name: tuple - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.0" typed_data: dependency: transitive description: name: typed_data - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.1" url_launcher: dependency: "direct main" description: name: url_launcher - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "6.1.5" url_launcher_android: dependency: transitive description: name: url_launcher_android - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "6.0.17" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "6.0.17" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.1" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0" url_launcher_web: dependency: transitive description: name: url_launcher_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.13" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.1" uuid: dependency: transitive description: name: uuid - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.6" vector_math: dependency: transitive description: name: vector_math - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.2" video_player: dependency: transitive description: name: video_player - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.4.6" video_player_android: dependency: transitive description: name: video_player_android - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.3.8" video_player_avfoundation: dependency: transitive description: name: video_player_avfoundation - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.3.5" video_player_platform_interface: dependency: transitive description: name: video_player_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "5.1.4" video_player_web: dependency: transitive description: name: video_player_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.12" visibility_detector: dependency: "direct main" description: name: visibility_detector - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.3.3" wakelock: dependency: "direct main" description: name: wakelock - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.5.6" wakelock_macos: dependency: transitive description: name: wakelock_macos - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.4.0" wakelock_platform_interface: dependency: transitive description: name: wakelock_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.3.0" wakelock_web: dependency: transitive description: name: wakelock_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.4.0" wakelock_windows: dependency: transitive description: name: wakelock_windows - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.2.0" watcher: dependency: transitive description: name: watcher - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.1" web_socket_channel: dependency: transitive description: name: web_socket_channel - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.0" win32: dependency: transitive description: name: win32 - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.7.0" window_manager: @@ -1221,28 +1221,28 @@ packages: dependency: transitive description: name: xdg_directories - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.2.0+1" xml: dependency: transitive description: name: xml - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "6.1.0" yaml: dependency: transitive description: name: yaml - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.1" zxing2: dependency: "direct main" description: name: zxing2 - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.1.0" sdks: From c38c9d275bb1ff2fb0ab3858661e041a7837bc24 Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 12 Aug 2022 20:14:53 +0800 Subject: [PATCH 0201/2015] flutter_desktop: try mouse handler Signed-off-by: fufesou --- flutter/lib/desktop/pages/remote_page.dart | 81 ++++++++------- flutter/lib/models/model.dart | 111 +++++++++++++-------- 2 files changed, 113 insertions(+), 79 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index b1dcb1620..46605821f 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -831,49 +831,52 @@ class ImagePaint extends StatelessWidget { @override Widget build(BuildContext context) { final m = Provider.of(context); - final c = Provider.of(context); - var s = c.scale; - final paintChild = SizedBox( - width: (m.image?.width ?? kDefaultDisplayWidth) * s, - height: (m.image?.height ?? kDefaultDisplayHeight) * s, - child: CustomPaint( - painter: new ImagePainter( - // image: m.image, x: c.x / s, y: c.y / s, scale: s), - image: m.image, - x: 0, - y: 0, - scale: s), - )); - + var c = Provider.of(context); + final s = c.scale; if (c.scrollStyle == ScrollStyle.scrollbar) { return Center( - child: Scrollbar( - controller: _vertical, - thumbVisibility: true, - trackVisibility: true, + child: NotificationListener( + onNotification: (_notification) { + final percentX = _horizontal.position.extentBefore / + (_horizontal.position.extentBefore + + _horizontal.position.extentInside + + _horizontal.position.extentAfter); + final percentY = _vertical.position.extentBefore / + (_vertical.position.extentBefore + + _vertical.position.extentInside + + _vertical.position.extentAfter); + c.setScrollPercent(percentX, percentY); + return false; + }, child: Scrollbar( - controller: _horizontal, - thumbVisibility: true, - trackVisibility: true, - notificationPredicate: (notif) => notif.depth == 1, - child: SingleChildScrollView( controller: _vertical, - child: SingleChildScrollView( - controller: _horizontal, - scrollDirection: Axis.horizontal, - child: paintChild), - ), - ), + thumbVisibility: true, + trackVisibility: true, + child: Scrollbar( + controller: _horizontal, + thumbVisibility: true, + trackVisibility: true, + notificationPredicate: (notif) => notif.depth == 1, + child: SingleChildScrollView( + controller: _vertical, + child: SingleChildScrollView( + controller: _horizontal, + scrollDirection: Axis.horizontal, + child: SizedBox( + width: c.getDisplayWidth() * s, + height: c.getDisplayHeight() * s, + child: CustomPaint( + painter: new ImagePainter( + image: m.image, x: 0, y: 0, scale: s), + ))), + ), + )), )); } else { - return Center( - child: InteractiveViewer( - // boundaryMargin: const EdgeInsets.all(20.0), - // minScale: 0.1, - // maxScale: 1.6, - scaleEnabled: false, - child: paintChild, - )); + return CustomPaint( + painter: + new ImagePainter(image: m.image, x: c.x / s, y: c.y / s, scale: s), + ); } } } @@ -939,6 +942,8 @@ void showOptions(String id, OverlayDialogManager dialogManager) async { await bind.getSessionOption(id: id, arg: 'view-style') ?? ''; String scrollStyle = await bind.getSessionOption(id: id, arg: 'scroll-style') ?? ''; + ffi(id).canvasModel.setScrollStyle(scrollStyle); + var displays = []; final pi = ffi(id).ffiModel.pi; final image = ffi(id).ffiModel.getConnectionImage(); @@ -1031,7 +1036,7 @@ void showOptions(String id, OverlayDialogManager dialogManager) async { Divider(color: MyTheme.border), getRadio('Scrollbar', 'scrollbar', scrollStyle, setScrollStyle), getRadio( - 'ScrollMouse', 'scrollmouse', scrollStyle, setScrollStyle), + 'ScrollAuto', 'scrollauto', scrollStyle, setScrollStyle), Divider(color: MyTheme.border), getRadio('Good image quality', 'best', quality, setQuality), getRadio('Balanced', 'balanced', quality, setQuality), diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index c98bfb334..016bc370c 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -434,10 +434,14 @@ class ImageModel with ChangeNotifier { enum ScrollStyle { scrollbar, - scrollmouse, + scrollauto, } class CanvasModel with ChangeNotifier { + // scroll offset x percent + double _scrollX = 0.0; + // scroll offset y percent + double _scrollY = 0.0; double _x = 0; double _y = 0; double _scale = 1.0; @@ -454,6 +458,14 @@ class CanvasModel with ChangeNotifier { double get scale => _scale; ScrollStyle get scrollStyle => _scrollStyle; + setScrollPercent(double x, double y) { + _scrollX = x; + _scrollY = y; + } + + double get scrollX => _scrollX; + double get scrollY => _scrollY; + set tabBarHeight(double h) => _tabBarHeight = h; double get tabBarHeight => _tabBarHeight; @@ -462,11 +474,8 @@ class CanvasModel with ChangeNotifier { if (s == null) { return; } - final size = MediaQueryData.fromWindow(ui.window).size; - final canvasWidth = size.width; - final canvasHeight = size.height - _tabBarHeight; - final s1 = canvasWidth / (parent.target?.ffiModel.display.width ?? 720); - final s2 = canvasHeight / (parent.target?.ffiModel.display.height ?? 1280); + final s1 = size.width / (parent.target?.ffiModel.display.width ?? 720); + final s2 = size.height / (parent.target?.ffiModel.display.height ?? 1280); // Closure to perform shrink operation. final shrinkOp = () { final s = s1 < s2 ? s1 : s2; @@ -476,7 +485,7 @@ class CanvasModel with ChangeNotifier { }; // Closure to perform stretch operation. final stretchOp = () { - final s = s1 > s2 ? s1 : s2; + final s = s1 < s2 ? s1 : s2; if (s > 1) { _scale = s; } @@ -485,31 +494,39 @@ class CanvasModel with ChangeNotifier { final defaultOp = () { _scale = 1.0; }; + + // // On desktop, shrink is the default behavior. + // if (isDesktop) { + // shrinkOp(); + // } else { + defaultOp(); + // } + if (s == 'shrink') { shrinkOp(); } else if (s == 'stretch') { stretchOp(); - } else { - // On desktop, shrink is the default behavior. - if (isDesktop) { - shrinkOp(); - } else { - defaultOp(); - } } - _x = (canvasWidth - getDisplayWidth() * _scale) / 2; - _y = (canvasHeight - getDisplayHeight() * _scale) / 2; + + _x = (size.width - getDisplayWidth() * _scale) / 2; + _y = (size.height - getDisplayHeight() * _scale) / 2; notifyListeners(); } - void updateScrollStyle() async { + updateScrollStyle() async { final s = await bind.getSessionOption(id: id, arg: 'scroll-style'); - if (s == 'scrollmouse') { - _scrollStyle = ScrollStyle.scrollmouse; + setScrollStyle(s); + notifyListeners(); + } + + setScrollStyle(String? style) { + if (style == 'scrollauto') { + _scrollStyle = ScrollStyle.scrollauto; } else { _scrollStyle = ScrollStyle.scrollbar; + _scrollX = 0.0; + _scrollY = 0.0; } - notifyListeners(); } void update(double x, double y, double scale) { @@ -527,28 +544,30 @@ class CanvasModel with ChangeNotifier { return parent.target?.ffiModel.display.height ?? 720; } + Size get size { + final size = MediaQueryData.fromWindow(ui.window).size; + return Size(size.width, size.height - _tabBarHeight); + } + void moveDesktopMouse(double x, double y) { // On mobile platforms, move the canvas with the cursor. - if (!isDesktop) { - final size = MediaQueryData.fromWindow(ui.window).size; - final canvasWidth = size.width; - final canvasHeight = size.height - _tabBarHeight; - final dw = getDisplayWidth() * _scale; - final dh = getDisplayHeight() * _scale; - var dxOffset = 0; - var dyOffset = 0; - if (dw > canvasWidth) { - dxOffset = (x - dw * (x / canvasWidth) - _x).toInt(); - } - if (dh > canvasHeight) { - dyOffset = (y - dh * (y / canvasHeight) - _y).toInt(); - } - _x += dxOffset; - _y += dyOffset; - if (dxOffset != 0 || dyOffset != 0) { - notifyListeners(); - } + //if (!isDesktop) { + final dw = getDisplayWidth() * _scale; + final dh = getDisplayHeight() * _scale; + var dxOffset = 0; + var dyOffset = 0; + if (dw > size.width) { + dxOffset = (x - dw * (x / size.width) - _x).toInt(); } + if (dh > size.height) { + dyOffset = (y - dh * (y / size.height) - _y).toInt(); + } + _x += dxOffset; + _y += dyOffset; + if (dxOffset != 0 || dyOffset != 0) { + notifyListeners(); + } + //} parent.target?.cursorModel.moveLocal(x, y); } @@ -1106,13 +1125,23 @@ class FFI { canvasModel.moveDesktopMouse(x, y); } final d = ffiModel.display; - x -= canvasModel.x; - y -= canvasModel.y; + if (canvasModel.scrollStyle == ScrollStyle.scrollbar) { + final imageWidth = d.width * canvasModel.scale; + final imageHeight = d.height * canvasModel.scale; + x += imageWidth * canvasModel.scrollX; + y += imageHeight * canvasModel.scrollY; + } else { + x -= canvasModel.x; + y -= canvasModel.y; + } + if (!isMove && (x < 0 || x > d.width || y < 0 || y > d.height)) { return; } + x /= canvasModel.scale; y /= canvasModel.scale; + x += d.x; y += d.y; if (type != '') { From af2e555e41a71e830b4a9928e0aab66a62e5325f Mon Sep 17 00:00:00 2001 From: fufesou Date: Sat, 13 Aug 2022 15:08:17 +0800 Subject: [PATCH 0202/2015] flutter_desktop: remote window mid commit Signed-off-by: fufesou --- flutter/lib/desktop/pages/remote_page.dart | 40 +++++++++++++++++----- flutter/lib/mobile/pages/remote_page.dart | 3 ++ flutter/lib/models/model.dart | 39 ++++++++++++--------- 3 files changed, 57 insertions(+), 25 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 46605821f..5eb6993f9 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -14,7 +14,6 @@ import 'package:wakelock/wakelock.dart'; // import 'package:window_manager/window_manager.dart'; import '../../common.dart'; -import '../../consts.dart'; import '../../mobile/widgets/dialog.dart'; import '../../mobile/widgets/overlay.dart'; import '../../models/model.dart'; @@ -275,7 +274,6 @@ class _RemotePageState extends State @override Widget build(BuildContext context) { super.build(context); - _ffi.canvasModel.tabBarHeight = super.widget.tabBarHeight; return WillPopScope( onWillPop: () async { clientClose(_ffi.dialogManager); @@ -631,6 +629,9 @@ class _RemotePageState extends State } }(); } else if (value == 'enter_os_password') { + // FIXME: + // null means no session of id + // empty string means no password var password = await bind.getSessionOption(id: id, arg: "os-password"); if (password != null) { bind.sessionInputOsPassword(id: widget.id, value: password); @@ -862,23 +863,48 @@ class ImagePaint extends StatelessWidget { child: SingleChildScrollView( controller: _horizontal, scrollDirection: Axis.horizontal, - child: SizedBox( + child: buildListener(SizedBox( width: c.getDisplayWidth() * s, height: c.getDisplayHeight() * s, child: CustomPaint( painter: new ImagePainter( image: m.image, x: 0, y: 0, scale: s), - ))), + )))), ), )), )); } else { - return CustomPaint( + return buildListener(CustomPaint( painter: new ImagePainter(image: m.image, x: c.x / s, y: c.y / s, scale: s), - ); + )); } } + + Widget buildListener(Widget child) { + return Listener( + onPointerHover: (e) { + debugPrint( + 'REMOVE ME ======================== 4444 onPointerHover ${e.position}'); + }, + onPointerDown: (e) { + debugPrint( + 'REMOVE ME ======================== 4444 onPointerDown ${e.position}'); + }, + onPointerUp: (e) { + debugPrint( + 'REMOVE ME ======================== 4444 onPointerUp ${e.position}'); + }, + onPointerMove: (e) { + debugPrint( + 'REMOVE ME ======================== 4444 onPointerMove ${e.position}'); + }, + onPointerSignal: (e) { + debugPrint( + 'REMOVE ME ======================== 3333 onPointerSignal ${e.position}'); + }, + child: child); + } } class CursorPaint extends StatelessWidget { @@ -942,8 +968,6 @@ void showOptions(String id, OverlayDialogManager dialogManager) async { await bind.getSessionOption(id: id, arg: 'view-style') ?? ''; String scrollStyle = await bind.getSessionOption(id: id, arg: 'scroll-style') ?? ''; - ffi(id).canvasModel.setScrollStyle(scrollStyle); - var displays = []; final pi = ffi(id).ffiModel.pi; final image = ffi(id).ffiModel.getConnectionImage(); diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 14bdfa833..3e826705f 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -735,6 +735,9 @@ class _RemotePageState extends State { } }(); } else if (value == 'enter_os_password') { + // FIXME: + // null means no session of id + // empty string means no password var password = await bind.getSessionOption(id: id, arg: "os-password"); if (password != null) { bind.sessionInputOsPassword(id: widget.id, value: password); diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 016bc370c..c297141de 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -387,8 +387,9 @@ class ImageModel with ChangeNotifier { void update(ui.Image? image, double tabBarHeight) { if (_image == null && image != null) { - if (isWebDesktop) { + if (isWebDesktop || isDesktop) { parent.target?.canvasModel.updateViewStyle(); + parent.target?.canvasModel.updateScrollStyle(); } else { final size = MediaQueryData.fromWindow(ui.window).size; final canvasWidth = size.width; @@ -447,7 +448,7 @@ class CanvasModel with ChangeNotifier { double _scale = 1.0; double _tabBarHeight = 0.0; String id = ""; // TODO multi canvas model - ScrollStyle _scrollStyle = ScrollStyle.scrollbar; + ScrollStyle _scrollStyle = ScrollStyle.scrollauto; WeakReference parent; @@ -470,12 +471,14 @@ class CanvasModel with ChangeNotifier { double get tabBarHeight => _tabBarHeight; void updateViewStyle() async { - final s = await bind.getSessionOption(id: id, arg: 'view-style'); - if (s == null) { + final style = await bind.getSessionOption(id: id, arg: 'view-style'); + if (style == null) { return; } + final s1 = size.width / (parent.target?.ffiModel.display.width ?? 720); final s2 = size.height / (parent.target?.ffiModel.display.height ?? 1280); + // Closure to perform shrink operation. final shrinkOp = () { final s = s1 < s2 ? s1 : s2; @@ -502,9 +505,9 @@ class CanvasModel with ChangeNotifier { defaultOp(); // } - if (s == 'shrink') { + if (style == 'shrink') { shrinkOp(); - } else if (s == 'stretch') { + } else if (style == 'stretch') { stretchOp(); } @@ -514,19 +517,15 @@ class CanvasModel with ChangeNotifier { } updateScrollStyle() async { - final s = await bind.getSessionOption(id: id, arg: 'scroll-style'); - setScrollStyle(s); - notifyListeners(); - } - - setScrollStyle(String? style) { - if (style == 'scrollauto') { - _scrollStyle = ScrollStyle.scrollauto; - } else { + final style = await bind.getSessionOption(id: id, arg: 'scroll-style'); + if (style == 'scrollbar') { _scrollStyle = ScrollStyle.scrollbar; _scrollX = 0.0; _scrollY = 0.0; + } else { + _scrollStyle = ScrollStyle.scrollauto; } + notifyListeners(); } void update(double x, double y, double scale) { @@ -1130,6 +1129,14 @@ class FFI { final imageHeight = d.height * canvasModel.scale; x += imageWidth * canvasModel.scrollX; y += imageHeight * canvasModel.scrollY; + + // boxed size is a center widget + if (canvasModel.size.width > imageWidth) { + x -= ((canvasModel.size.width - imageWidth) / 2); + } + if (canvasModel.size.height > imageHeight) { + y -= ((canvasModel.size.height - imageHeight) / 2); + } } else { x -= canvasModel.x; y -= canvasModel.y; @@ -1138,10 +1145,8 @@ class FFI { if (!isMove && (x < 0 || x > d.width || y < 0 || y > d.height)) { return; } - x /= canvasModel.scale; y /= canvasModel.scale; - x += d.x; y += d.y; if (type != '') { From fd8c83497dda54b6965c7e46b814c6927085e33a Mon Sep 17 00:00:00 2001 From: fufesou Date: Sat, 13 Aug 2022 17:58:24 +0800 Subject: [PATCH 0203/2015] flutter_desktop: remote window cursor debug Signed-off-by: fufesou --- flutter/lib/desktop/pages/remote_page.dart | 396 +++++++++++---------- 1 file changed, 211 insertions(+), 185 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 5eb6993f9..093f4c6de 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -41,6 +41,7 @@ class _RemotePageState extends State String _value = ''; double _scale = 1; double _mouseScrollIntegral = 0; // mouse scroll speed controller + var _cursorOverImage = false.obs; var _more = true; var _fn = false; @@ -291,114 +292,61 @@ class _RemotePageState extends State } Widget getRawPointerAndKeyBody(Widget child) { - return Listener( - onPointerHover: (e) { - if (e.kind != ui.PointerDeviceKind.mouse) return; - if (!_isPhysicalMouse) { - setState(() { - _isPhysicalMouse = true; - }); - } - if (_isPhysicalMouse) { - _ffi.handleMouse(getEvent(e, 'mousemove'), - tabBarHeight: super.widget.tabBarHeight); - } - }, - onPointerDown: (e) { - if (e.kind != ui.PointerDeviceKind.mouse) { - if (_isPhysicalMouse) { - setState(() { - _isPhysicalMouse = false; - }); - } - } - if (_isPhysicalMouse) { - _ffi.handleMouse(getEvent(e, 'mousedown'), - tabBarHeight: super.widget.tabBarHeight); - } - }, - onPointerUp: (e) { - if (e.kind != ui.PointerDeviceKind.mouse) return; - if (_isPhysicalMouse) { - _ffi.handleMouse(getEvent(e, 'mouseup'), - tabBarHeight: super.widget.tabBarHeight); - } - }, - onPointerMove: (e) { - if (e.kind != ui.PointerDeviceKind.mouse) return; - if (_isPhysicalMouse) { - _ffi.handleMouse(getEvent(e, 'mousemove'), - tabBarHeight: super.widget.tabBarHeight); - } - }, - onPointerSignal: (e) { - if (e is PointerScrollEvent) { - var dx = e.scrollDelta.dx; - var dy = e.scrollDelta.dy; - if (dx > 0) - dx = -1; - else if (dx < 0) dx = 1; - if (dy > 0) - dy = -1; - else if (dy < 0) dy = 1; - bind.sessionSendMouse( - id: widget.id, - msg: '{"type": "wheel", "x": "$dx", "y": "$dy"}'); - } - }, - child: Consumer( - builder: (context, FfiModel, _child) => MouseRegion( - cursor: FfiModel.permissions['keyboard'] != false - ? SystemMouseCursors.none - : MouseCursor.defer, - child: FocusScope( + return Consumer( + builder: (context, FfiModel, _child) => MouseRegion( + cursor: FfiModel.permissions['keyboard'] != false + ? SystemMouseCursors.none + : MouseCursor.defer, + child: FocusScope( + autofocus: true, + child: Focus( autofocus: true, - child: Focus( - autofocus: true, - canRequestFocus: true, - focusNode: _physicalFocusNode, - onKey: (data, e) { - final key = e.logicalKey; - if (e is RawKeyDownEvent) { - if (e.repeat) { - sendRawKey(e, press: true); - } else { - if (e.isAltPressed && !_ffi.alt) { - _ffi.alt = true; - } else if (e.isControlPressed && !_ffi.ctrl) { - _ffi.ctrl = true; - } else if (e.isShiftPressed && !_ffi.shift) { - _ffi.shift = true; - } else if (e.isMetaPressed && !_ffi.command) { - _ffi.command = true; - } - sendRawKey(e, down: true); - } + canRequestFocus: true, + focusNode: _physicalFocusNode, + onKey: (data, e) { + final key = e.logicalKey; + if (e is RawKeyDownEvent) { + if (e.repeat) { + sendRawKey(e, press: true); + } else { + if (e.isAltPressed && !_ffi.alt) { + _ffi.alt = true; + } else if (e.isControlPressed && !_ffi.ctrl) { + _ffi.ctrl = true; + } else if (e.isShiftPressed && !_ffi.shift) { + _ffi.shift = true; + } else if (e.isMetaPressed && !_ffi.command) { + _ffi.command = true; } - // [!_showEdit] workaround for soft-keyboard's control_key like Backspace / Enter - if (!_showEdit && e is RawKeyUpEvent) { - if (key == LogicalKeyboardKey.altLeft || - key == LogicalKeyboardKey.altRight) { - _ffi.alt = false; - } else if (key == LogicalKeyboardKey.controlLeft || - key == LogicalKeyboardKey.controlRight) { - _ffi.ctrl = false; - } else if (key == LogicalKeyboardKey.shiftRight || - key == LogicalKeyboardKey.shiftLeft) { - _ffi.shift = false; - } else if (key == LogicalKeyboardKey.metaLeft || - key == LogicalKeyboardKey.metaRight) { - _ffi.command = false; - } - sendRawKey(e); - } - return KeyEventResult.handled; - }, - child: child))))); + sendRawKey(e, down: true); + } + } + // [!_showEdit] workaround for soft-keyboard's control_key like Backspace / Enter + if (!_showEdit && e is RawKeyUpEvent) { + if (key == LogicalKeyboardKey.altLeft || + key == LogicalKeyboardKey.altRight) { + _ffi.alt = false; + } else if (key == LogicalKeyboardKey.controlLeft || + key == LogicalKeyboardKey.controlRight) { + _ffi.ctrl = false; + } else if (key == LogicalKeyboardKey.shiftRight || + key == LogicalKeyboardKey.shiftLeft) { + _ffi.shift = false; + } else if (key == LogicalKeyboardKey.metaLeft || + key == LogicalKeyboardKey.metaRight) { + _ffi.command = false; + } + sendRawKey(e); + } + return KeyEventResult.handled; + }, + child: child)))); } Widget? getBottomAppBar() { - return BottomAppBar( + return MouseRegion( + cursor: SystemMouseCursors.basic, + child: BottomAppBar( elevation: 10, color: MyTheme.accent, child: Row( @@ -429,40 +377,40 @@ class _RemotePageState extends State ? [] : _ffi.ffiModel.isPeerAndroid ? [ + IconButton( + color: Colors.white, + icon: Icon(Icons.build), + onPressed: () { + if (mobileActionsOverlayEntry == null) { + showMobileActionsOverlay(); + } else { + hideMobileActionsOverlay(); + } + }, + ) + ] + : [ + IconButton( + color: Colors.white, + icon: Icon(Icons.keyboard), + onPressed: openKeyboard), + IconButton( + color: Colors.white, + icon: Icon(_ffi.ffiModel.touchMode + ? Icons.touch_app + : Icons.mouse), + onPressed: changeTouchMode, + ), + ]) + + (isWeb + ? [] + : [ IconButton( color: Colors.white, - icon: Icon(Icons.build), + icon: Icon(Icons.message), onPressed: () { - if (mobileActionsOverlayEntry == null) { - showMobileActionsOverlay(); - } else { - hideMobileActionsOverlay(); - } - }, - ) - ] - : [ - IconButton( - color: Colors.white, - icon: Icon(Icons.keyboard), - onPressed: openKeyboard), - IconButton( - color: Colors.white, - icon: Icon(_ffi.ffiModel.touchMode - ? Icons.touch_app - : Icons.mouse), - onPressed: changeTouchMode, - ), - ]) + - (isWeb - ? [] - : [ - IconButton( - color: Colors.white, - icon: Icon(Icons.message), - onPressed: () { - _ffi.chatModel - .changeCurrentID(ChatModel.clientModeID); + _ffi.chatModel + .changeCurrentID(ChatModel.clientModeID); _ffi.chatModel.toggleChatOverlay(); }, ) @@ -498,6 +446,81 @@ class _RemotePageState extends State /// DoubleFiner -> right click /// HoldDrag -> left drag + void _onPointHoverImage(PointerHoverEvent e) { + if (e.kind != ui.PointerDeviceKind.mouse) return; + if (!_isPhysicalMouse) { + setState(() { + _isPhysicalMouse = true; + }); + } + if (_isPhysicalMouse) { + _ffi.handleMouse(getEvent(e, 'mousemove'), + tabBarHeight: super.widget.tabBarHeight); + } + } + + void _onPointDownImage(PointerDownEvent e) { + if (e.kind != ui.PointerDeviceKind.mouse) { + if (_isPhysicalMouse) { + setState(() { + _isPhysicalMouse = false; + }); + } + } + if (_isPhysicalMouse) { + _ffi.handleMouse(getEvent(e, 'mousedown'), + tabBarHeight: super.widget.tabBarHeight); + } + } + + void _onPointUpImage(PointerUpEvent e) { + if (e.kind != ui.PointerDeviceKind.mouse) return; + if (_isPhysicalMouse) { + _ffi.handleMouse(getEvent(e, 'mouseup'), + tabBarHeight: super.widget.tabBarHeight); + } + } + + void _onPointMoveImage(PointerMoveEvent e) { + if (e.kind != ui.PointerDeviceKind.mouse) return; + if (_isPhysicalMouse) { + _ffi.handleMouse(getEvent(e, 'mousemove'), + tabBarHeight: super.widget.tabBarHeight); + } + } + + void _onPointerSignalImage(PointerSignalEvent e) { + if (e is PointerScrollEvent) { + var dx = e.scrollDelta.dx; + var dy = e.scrollDelta.dy; + if (dx > 0) + dx = -1; + else if (dx < 0) dx = 1; + if (dy > 0) + dy = -1; + else if (dy < 0) dy = 1; + bind.sessionSendMouse( + id: widget.id, msg: '{"type": "wheel", "x": "$dx", "y": "$dy"}'); + } + } + + Widget _buildImageListener(Widget child) { + return Listener( + onPointerHover: _onPointHoverImage, + onPointerDown: _onPointDownImage, + onPointerUp: _onPointUpImage, + onPointerMove: _onPointMoveImage, + onPointerSignal: _onPointerSignalImage, + child: MouseRegion( + onEnter: (evt) { + _cursorOverImage.value = true; + }, + onExit: (evt) { + _cursorOverImage.value = false; + }, + child: child)); + } + Widget getBodyForDesktop(BuildContext context, bool keyboard) { var paints = [ MouseRegion(onEnter: (evt) { @@ -824,10 +847,17 @@ class _RemotePageState extends State class ImagePaint extends StatelessWidget { final String id; + final Rx cursorOverImage; + final Widget Function(Widget)? listenerBuilder; final ScrollController _horizontal = ScrollController(); final ScrollController _vertical = ScrollController(); - ImagePaint({Key? key, required this.id}) : super(key: key); + ImagePaint( + {Key? key, + required this.id, + required this.cursorOverImage, + this.listenerBuilder = null}) + : super(key: key); @override Widget build(BuildContext context) { @@ -835,6 +865,12 @@ class ImagePaint extends StatelessWidget { var c = Provider.of(context); final s = c.scale; if (c.scrollStyle == ScrollStyle.scrollbar) { + final imageWidget = SizedBox( + width: c.getDisplayWidth() * s, + height: c.getDisplayHeight() * s, + child: CustomPaint( + painter: new ImagePainter(image: m.image, x: 0, y: 0, scale: s), + )); return Center( child: NotificationListener( onNotification: (_notification) { @@ -849,61 +885,51 @@ class ImagePaint extends StatelessWidget { c.setScrollPercent(percentX, percentY); return false; }, - child: Scrollbar( - controller: _vertical, - thumbVisibility: true, - trackVisibility: true, - child: Scrollbar( - controller: _horizontal, - thumbVisibility: true, - trackVisibility: true, - notificationPredicate: (notif) => notif.depth == 1, - child: SingleChildScrollView( - controller: _vertical, - child: SingleChildScrollView( - controller: _horizontal, - scrollDirection: Axis.horizontal, - child: buildListener(SizedBox( - width: c.getDisplayWidth() * s, - height: c.getDisplayHeight() * s, - child: CustomPaint( - painter: new ImagePainter( - image: m.image, x: 0, y: 0, scale: s), - )))), - ), - )), + child: Obx(() => MouseRegion( + cursor: cursorOverImage.value + ? SystemMouseCursors.none + : SystemMouseCursors.basic, + child: _buildCrossScrollbar(_buildListener(imageWidget)))), )); } else { - return buildListener(CustomPaint( - painter: - new ImagePainter(image: m.image, x: c.x / s, y: c.y / s, scale: s), - )); + final imageWidget = SizedBox( + width: c.size.width, + height: c.size.height, + child: CustomPaint( + painter: new ImagePainter( + image: m.image, x: c.x / s, y: c.y / s, scale: s), + )); + return _buildListener(imageWidget); } } - Widget buildListener(Widget child) { - return Listener( - onPointerHover: (e) { - debugPrint( - 'REMOVE ME ======================== 4444 onPointerHover ${e.position}'); - }, - onPointerDown: (e) { - debugPrint( - 'REMOVE ME ======================== 4444 onPointerDown ${e.position}'); - }, - onPointerUp: (e) { - debugPrint( - 'REMOVE ME ======================== 4444 onPointerUp ${e.position}'); - }, - onPointerMove: (e) { - debugPrint( - 'REMOVE ME ======================== 4444 onPointerMove ${e.position}'); - }, - onPointerSignal: (e) { - debugPrint( - 'REMOVE ME ======================== 3333 onPointerSignal ${e.position}'); - }, - child: child); + Widget _buildCrossScrollbar(Widget child) { + return Scrollbar( + controller: _vertical, + thumbVisibility: true, + trackVisibility: true, + child: Scrollbar( + controller: _horizontal, + thumbVisibility: true, + trackVisibility: true, + notificationPredicate: (notif) => notif.depth == 1, + child: SingleChildScrollView( + controller: _vertical, + child: SingleChildScrollView( + controller: _horizontal, + scrollDirection: Axis.horizontal, + child: child, + ), + ), + )); + } + + Widget _buildListener(Widget child) { + if (listenerBuilder != null) { + return listenerBuilder!(child); + } else { + return child; + } } } From 47b7e84acaaa17d35304f3dbce0239e4a092d22c Mon Sep 17 00:00:00 2001 From: fufesou Date: Sat, 13 Aug 2022 18:10:04 +0800 Subject: [PATCH 0204/2015] flutter_desktop: remote window cursor debug (getx) Signed-off-by: fufesou --- flutter/lib/desktop/pages/remote_page.dart | 112 +++++++++++---------- 1 file changed, 57 insertions(+), 55 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 093f4c6de..4885d3e43 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -347,36 +347,36 @@ class _RemotePageState extends State return MouseRegion( cursor: SystemMouseCursors.basic, child: BottomAppBar( - elevation: 10, - color: MyTheme.accent, - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - IconButton( - color: Colors.white, - icon: Icon(Icons.clear), - onPressed: () { - clientClose(_ffi.dialogManager); - }, - ) - ] + - [ - IconButton( - color: Colors.white, - icon: Icon(Icons.tv), - onPressed: () { - setState(() => _showEdit = false); - showOptions(widget.id, _ffi.dialogManager); - }, - ) - ] + - (isWebDesktop - ? [] - : _ffi.ffiModel.isPeerAndroid - ? [ + elevation: 10, + color: MyTheme.accent, + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + IconButton( + color: Colors.white, + icon: Icon(Icons.clear), + onPressed: () { + clientClose(_ffi.dialogManager); + }, + ) + ] + + [ + IconButton( + color: Colors.white, + icon: Icon(Icons.tv), + onPressed: () { + setState(() => _showEdit = false); + showOptions(widget.id, _ffi.dialogManager); + }, + ) + ] + + (isWebDesktop + ? [] + : _ffi.ffiModel.isPeerAndroid + ? [ IconButton( color: Colors.white, icon: Icon(Icons.build), @@ -411,29 +411,29 @@ class _RemotePageState extends State onPressed: () { _ffi.chatModel .changeCurrentID(ChatModel.clientModeID); - _ffi.chatModel.toggleChatOverlay(); - }, - ) - ]) + - [ - IconButton( - color: Colors.white, - icon: Icon(Icons.more_vert), - onPressed: () { - setState(() => _showEdit = false); - showActions(widget.id); - }, - ), - ]), - IconButton( - color: Colors.white, - icon: Icon(Icons.expand_more), - onPressed: () { - setState(() => _showBar = !_showBar); - }), - ], - ), - ); + _ffi.chatModel.toggleChatOverlay(); + }, + ) + ]) + + [ + IconButton( + color: Colors.white, + icon: Icon(Icons.more_vert), + onPressed: () { + setState(() => _showEdit = false); + showActions(widget.id); + }, + ), + ]), + IconButton( + color: Colors.white, + icon: Icon(Icons.expand_more), + onPressed: () { + setState(() => _showBar = !_showBar); + }), + ], + ), + )); } /// touchMode only: @@ -533,8 +533,10 @@ class _RemotePageState extends State Provider.of(context, listen: false).updateViewStyle(); }); return ImagePaint( - id: widget.id, - ); + id: widget.id, + cursorOverImage: _cursorOverImage, + listenerBuilder: _buildImageListener, + ); }), )) ]; From 4fecbba87ee7f1ba271d48894f813acad9e24c96 Mon Sep 17 00:00:00 2001 From: fufesou Date: Sat, 13 Aug 2022 22:29:08 +0800 Subject: [PATCH 0205/2015] flutter_desktop: remote scroll choice translation Signed-off-by: fufesou --- flutter/lib/desktop/pages/remote_page.dart | 10 +++++----- 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/fr.rs | 2 ++ src/lang/hu.rs | 2 ++ src/lang/id.rs | 2 ++ src/lang/it.rs | 2 ++ src/lang/ja.rs | 2 ++ src/lang/pl.rs | 2 ++ src/lang/ptbr.rs | 2 ++ src/lang/ru.rs | 2 ++ src/lang/sk.rs | 2 ++ src/lang/template.rs | 2 ++ src/lang/tr.rs | 2 ++ src/lang/tw.rs | 2 ++ src/lang/vn.rs | 2 ++ 20 files changed, 43 insertions(+), 5 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 4885d3e43..62ad5ee0b 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -533,10 +533,10 @@ class _RemotePageState extends State Provider.of(context, listen: false).updateViewStyle(); }); return ImagePaint( - id: widget.id, - cursorOverImage: _cursorOverImage, - listenerBuilder: _buildImageListener, - ); + id: widget.id, + cursorOverImage: _cursorOverImage, + listenerBuilder: _buildImageListener, + ); }), )) ]; @@ -1086,9 +1086,9 @@ void showOptions(String id, OverlayDialogManager dialogManager) async { getRadio('Shrink', 'shrink', viewStyle, setViewStyle), getRadio('Stretch', 'stretch', viewStyle, setViewStyle), Divider(color: MyTheme.border), - getRadio('Scrollbar', 'scrollbar', scrollStyle, setScrollStyle), getRadio( 'ScrollAuto', 'scrollauto', scrollStyle, setScrollStyle), + getRadio('Scrollbar', 'scrollbar', scrollStyle, setScrollStyle), Divider(color: MyTheme.border), getRadio('Good image quality', 'best', quality, setQuality), getRadio('Balanced', 'balanced', quality, setQuality), diff --git a/src/lang/cn.rs b/src/lang/cn.rs index df5cfdfd7..730c8be94 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -103,6 +103,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Original", "原始比例"), ("Shrink", "收缩"), ("Stretch", "伸展"), + ("Scrollbar", "滚动条"), + ("ScrollAuto", "自动滚动"), ("Good image quality", "好画质"), ("Balanced", "一般画质"), ("Optimize reaction time", "优化反应时间"), diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 86aada74f..f174850fc 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -103,6 +103,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Original", "Původní"), ("Shrink", "Oříznout"), ("Stretch", "Roztáhnout"), + ("Scrollbar", "Posuvník"), + ("ScrollAuto", "Rolovať Auto"), ("Good image quality", "Dobrá kvalita obrazu"), ("Balanced", "Vyvážené"), ("Optimize reaction time", "Optimalizovat pro co nejnižší prodlevu odezvy"), diff --git a/src/lang/da.rs b/src/lang/da.rs index 8f4861c2a..60ebeafbb 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -103,6 +103,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Original", "Original"), ("Shrink", "Krymp"), ("Stretch", "Strak"), + ("Scrollbar", "Rullebar"), + ("ScrollAuto", "Rul Auto"), ("Good image quality", "God billedkvalitet"), ("Balanced", "Afbalanceret"), ("Optimize reaction time", "Optimeret responstid"), diff --git a/src/lang/de.rs b/src/lang/de.rs index 6af6841b6..02c73d095 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -103,6 +103,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Original", "Original"), ("Shrink", "Verkleinern"), ("Stretch", "Strecken"), + ("Scrollbar", "Scrollleiste"), + ("ScrollAuto", "Automatisch scrollen"), ("Good image quality", "Schöner"), ("Balanced", "Ausgeglichen"), ("Optimize reaction time", "Schneller"), diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 0c68bd569..3ce5c24f9 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -103,6 +103,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Original", "Originala rilatumo"), ("Shrink", "Ŝrumpi"), ("Stretch", "Streĉi"), + ("Scrollbar", "Rulumbreto"), + ("ScrollAuto", "Rulumu Aŭtomate"), ("Good image quality", "Bona bilda kvalito"), ("Balanced", "Normala bilda kvalito"), ("Optimize reaction time", "Optimigi reakcia tempo"), diff --git a/src/lang/es.rs b/src/lang/es.rs index 9eef5a5a8..2fa92bac8 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -103,6 +103,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Original", "Original"), ("Shrink", "Encogerse"), ("Stretch", "Estirar"), + ("Scrollbar", "Barra de desplazamiento"), + ("ScrollAuto", "Desplazamiento automático"), ("Good image quality", "Buena calidad de imagen"), ("Balanced", "Equilibrado"), ("Optimize reaction time", "Optimizar el tiempo de reacción"), diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 51d079b03..4efc804e1 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -103,6 +103,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Original", "Ratio d'origine"), ("Shrink", "Rétrécir"), ("Stretch", "Étirer"), + ("Scrollbar", "Barre de défilement"), + ("ScrollAuto", "Défilement automatique"), ("Good image quality", "Bonne qualité d'image"), ("Balanced", "Qualité d'image normale"), ("Optimize reaction time", "Optimiser le temps de réaction"), diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 5590e0ec9..fee1fc450 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -103,6 +103,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Original", "Eredeti"), ("Shrink", "Zsugorított"), ("Stretch", "Nyújtott"), + ("Scrollbar", "Görgetősáv"), + ("ScrollAuto", "Görgessen Auto"), ("Good image quality", "Jó képminőség"), ("Balanced", "Balanszolt"), ("Optimize reaction time", "Válaszidő optimializálása"), diff --git a/src/lang/id.rs b/src/lang/id.rs index ef1078175..b6d9dbd0d 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -103,6 +103,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Original", "Original"), ("Shrink", "Susutkan"), ("Stretch", "Regangkan"), + ("Scrollbar", "Scroll bar"), + ("ScrollAuto", "Gulir Otomatis"), ("Good image quality", "Kualitas Gambar Baik"), ("Balanced", "Seimbang"), ("Optimize reaction time", "Optimalkan waktu reaksi"), diff --git a/src/lang/it.rs b/src/lang/it.rs index 2834644eb..f4e2d0bce 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -103,6 +103,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Original", "Originale"), ("Shrink", "Restringi"), ("Stretch", "Allarga"), + ("Scrollbar", "Barra di scorrimento"), + ("ScrollAuto", "Scorri automaticamente"), ("Good image quality", "Buona qualità immagine"), ("Balanced", "Bilanciato"), ("Optimize reaction time", "Ottimizza il tempo di reazione"), diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 5c6ba1da7..f4c991690 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -103,6 +103,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Original", "オリジナル"), ("Shrink", "縮小"), ("Stretch", "伸縮"), + ("Scrollbar", "スクロール・バー"), + ("ScrollAuto", "自動スクロール"), ("Good image quality", "画質優先"), ("Balanced", "バランス"), ("Optimize reaction time", "速度優先"), diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 8602d0647..81eaddfaf 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -103,6 +103,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Original", "Oryginał"), ("Shrink", "Zmniejsz"), ("Stretch", "Zwiększ"), + ("Scrollbar", "Pasek przewijania"), + ("ScrollAuto", "Przewijanie automatyczne"), ("Good image quality", "Dobra jakość obrazu"), ("Balanced", "Zrównoważony"), ("Optimize reaction time", "Zoptymalizuj czas reakcji"), diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 75d3af784..8d1ffbbcf 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -103,6 +103,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Original", "Original"), ("Shrink", "Reduzir"), ("Stretch", "Aumentar"), + ("Scrollbar", "Barra de rolagem"), + ("ScrollAuto", "Rolagem automática"), ("Good image quality", "Qualidade visual boa"), ("Balanced", "Balanceada"), ("Optimize reaction time", "Otimizar tempo de reação"), diff --git a/src/lang/ru.rs b/src/lang/ru.rs index d44751cd8..ade2c7806 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -103,6 +103,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Original", "Оригинал"), ("Shrink", "Уменьшить"), ("Stretch", "Растянуть"), + ("Scrollbar", "Полоса прокрутки"), + ("ScrollAuto", "Прокрутка Авто"), ("Good image quality", "Хорошее качество изображения"), ("Balanced", "Сбалансированный"), ("Optimize reaction time", "Оптимизировать время реакции"), diff --git a/src/lang/sk.rs b/src/lang/sk.rs index f94db252b..a887cc34a 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -103,6 +103,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Original", "Pôvodný"), ("Shrink", "Zmenšené"), ("Stretch", "Roztiahnuté"), + ("Scrollbar", "Posuvník"), + ("ScrollAuto", "Rolovať Auto"), ("Good image quality", "Dobrá kvalita obrazu"), ("Balanced", "Vyvážené"), ("Optimize reaction time", "Optimalizované pre čas odozvy"), diff --git a/src/lang/template.rs b/src/lang/template.rs index ca64b2ac7..e0b64cdfa 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -103,6 +103,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Original", ""), ("Shrink", ""), ("Stretch", ""), + ("Scrollbar", ""), + ("ScrollAuto", ""), ("Good image quality", ""), ("Balanced", ""), ("Optimize reaction time", ""), diff --git a/src/lang/tr.rs b/src/lang/tr.rs index cff01dcc8..410d918eb 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -103,6 +103,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Original", "Orjinal"), ("Shrink", "Küçült"), ("Stretch", "Uzat"), + ("Scrollbar", "Kaydırma çubuğu"), + ("ScrollAuto", "Otomatik Kaydır"), ("Good image quality", "İyi görüntü kalitesi"), ("Balanced", "Dengelenmiş"), ("Optimize reaction time", "Tepki süresini optimize et"), diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 79435a69c..5f0acdd06 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -103,6 +103,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Original", "原始"), ("Shrink", "縮減"), ("Stretch", "延展"), + ("Scrollbar", "滾動條"), + ("ScrollAuto", "自動滾動"), ("Good image quality", "畫面品質良好"), ("Balanced", "平衡"), ("Optimize reaction time", "回應速度最佳化"), diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 65ffcb61c..5704bf9ee 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -103,6 +103,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Original", "Gốc"), ("Shrink", "Thu nhỏ"), ("Stretch", "Kéo dãn"), + ("Scrollbar", "Thanh cuộn"), + ("ScrollAuto", "Tự động cuộn"), ("Good image quality", "Chất lượng hình ảnh tốt"), ("Balanced", "Cân bằng"), ("Optimize reaction time", "Thời gian phản ứng tối ưu"), From 98d66ed43cf0f3006aff97d66b23731d495b6882 Mon Sep 17 00:00:00 2001 From: fufesou Date: Sun, 14 Aug 2022 11:20:52 +0800 Subject: [PATCH 0206/2015] flutter_desktop: fix scroll event to rust Signed-off-by: fufesou --- flutter/lib/desktop/pages/remote_page.dart | 62 ++++++++++++++++------ 1 file changed, 46 insertions(+), 16 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 62ad5ee0b..b410969ca 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -491,8 +491,8 @@ class _RemotePageState extends State void _onPointerSignalImage(PointerSignalEvent e) { if (e is PointerScrollEvent) { - var dx = e.scrollDelta.dx; - var dy = e.scrollDelta.dy; + var dx = e.scrollDelta.dx.toInt(); + var dy = e.scrollDelta.dy.toInt(); if (dx > 0) dx = -1; else if (dx < 0) dx = 1; @@ -906,24 +906,54 @@ class ImagePaint extends StatelessWidget { } Widget _buildCrossScrollbar(Widget child) { - return Scrollbar( - controller: _vertical, - thumbVisibility: true, - trackVisibility: true, - child: Scrollbar( - controller: _horizontal, + debugPrint( + 'REMOVE ME ==================================== _buildCrossScrollbar ${cursorOverImage.value}'); + // final physicsVertical = + // cursorOverImage.value ? const NeverScrollableScrollPhysics() : null; + // final physicsHorizontal = + // cursorOverImage.value ? const NeverScrollableScrollPhysics() : null; + + if (cursorOverImage.value) { + return Scrollbar( + controller: _vertical, thumbVisibility: true, trackVisibility: true, - notificationPredicate: (notif) => notif.depth == 1, - child: SingleChildScrollView( - controller: _vertical, + child: Scrollbar( + controller: _horizontal, + thumbVisibility: true, + trackVisibility: true, + notificationPredicate: (notif) => notif.depth == 1, child: SingleChildScrollView( - controller: _horizontal, - scrollDirection: Axis.horizontal, - child: child, + controller: _vertical, + physics: const NeverScrollableScrollPhysics(), + child: SingleChildScrollView( + controller: _horizontal, + scrollDirection: Axis.horizontal, + physics: const NeverScrollableScrollPhysics(), + child: child, + ), ), - ), - )); + )); + } else { + return Scrollbar( + controller: _vertical, + thumbVisibility: true, + trackVisibility: true, + child: Scrollbar( + controller: _horizontal, + thumbVisibility: true, + trackVisibility: true, + notificationPredicate: (notif) => notif.depth == 1, + child: SingleChildScrollView( + controller: _vertical, + child: SingleChildScrollView( + controller: _horizontal, + scrollDirection: Axis.horizontal, + child: child, + ), + ), + )); + } } Widget _buildListener(Widget child) { From b731d8e38ac91f583c454bc7544519bc76efeafa Mon Sep 17 00:00:00 2001 From: fufesou Date: Sun, 14 Aug 2022 12:48:04 +0800 Subject: [PATCH 0207/2015] flutter_desktop: disable scroll wheel event Signed-off-by: fufesou --- flutter/lib/desktop/pages/remote_page.dart | 64 +++++++--------------- 1 file changed, 20 insertions(+), 44 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index b410969ca..79f98f029 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -906,54 +906,30 @@ class ImagePaint extends StatelessWidget { } Widget _buildCrossScrollbar(Widget child) { - debugPrint( - 'REMOVE ME ==================================== _buildCrossScrollbar ${cursorOverImage.value}'); - // final physicsVertical = - // cursorOverImage.value ? const NeverScrollableScrollPhysics() : null; - // final physicsHorizontal = - // cursorOverImage.value ? const NeverScrollableScrollPhysics() : null; - - if (cursorOverImage.value) { - return Scrollbar( - controller: _vertical, + final physicsVertical = + cursorOverImage.value ? const NeverScrollableScrollPhysics() : null; + final physicsHorizontal = + cursorOverImage.value ? const NeverScrollableScrollPhysics() : null; + return Scrollbar( + controller: _vertical, + thumbVisibility: true, + trackVisibility: true, + child: Scrollbar( + controller: _horizontal, thumbVisibility: true, trackVisibility: true, - child: Scrollbar( - controller: _horizontal, - thumbVisibility: true, - trackVisibility: true, - notificationPredicate: (notif) => notif.depth == 1, + notificationPredicate: (notif) => notif.depth == 1, + child: SingleChildScrollView( + controller: _vertical, + physics: physicsVertical, child: SingleChildScrollView( - controller: _vertical, - physics: const NeverScrollableScrollPhysics(), - child: SingleChildScrollView( - controller: _horizontal, - scrollDirection: Axis.horizontal, - physics: const NeverScrollableScrollPhysics(), - child: child, - ), + controller: _horizontal, + scrollDirection: Axis.horizontal, + physics: physicsHorizontal, + child: child, ), - )); - } else { - return Scrollbar( - controller: _vertical, - thumbVisibility: true, - trackVisibility: true, - child: Scrollbar( - controller: _horizontal, - thumbVisibility: true, - trackVisibility: true, - notificationPredicate: (notif) => notif.depth == 1, - child: SingleChildScrollView( - controller: _vertical, - child: SingleChildScrollView( - controller: _horizontal, - scrollDirection: Axis.horizontal, - child: child, - ), - ), - )); - } + ), + )); } Widget _buildListener(Widget child) { From 163645ef8654cdae9e9ad3274d913181583c3bc2 Mon Sep 17 00:00:00 2001 From: fufesou Date: Sun, 14 Aug 2022 12:57:30 +0800 Subject: [PATCH 0208/2015] flutter_desktop: fix block user input action Signed-off-by: fufesou --- flutter/lib/desktop/pages/remote_page.dart | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 79f98f029..ceeb96049 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -257,7 +257,8 @@ class _RemotePageState extends State } }); }), - bottomNavigationBar: _showBar && hasDisplays ? getBottomAppBar() : null, + bottomNavigationBar: + _showBar && hasDisplays ? getBottomAppBar(ffiModel) : null, body: Overlay( initialEntries: [ OverlayEntry(builder: (context) { @@ -343,7 +344,7 @@ class _RemotePageState extends State child: child)))); } - Widget? getBottomAppBar() { + Widget? getBottomAppBar(FfiModel ffiModel) { return MouseRegion( cursor: SystemMouseCursors.basic, child: BottomAppBar( @@ -421,7 +422,7 @@ class _RemotePageState extends State icon: Icon(Icons.more_vert), onPressed: () { setState(() => _showEdit = false); - showActions(widget.id); + showActions(widget.id, ffiModel); }, ), ]), @@ -574,7 +575,7 @@ class _RemotePageState extends State return out; } - void showActions(String id) async { + void showActions(String id, FfiModel ffiModel) async { final size = MediaQuery.of(context).size; final x = 120.0; final y = size.height - super.widget.tabBarHeight; @@ -619,12 +620,8 @@ class _RemotePageState extends State await bind.getSessionToggleOption(id: id, arg: 'privacy-mode') != true) { more.add(PopupMenuItem( - child: Consumer( - builder: (_context, ffiModel, _child) => () { - return Text(translate( - (ffiModel.inputBlocked ? 'Unb' : 'B') + - 'lock user input')); - }()), + child: Text(translate( + (ffiModel.inputBlocked ? 'Unb' : 'B') + 'lock user input')), value: 'block-input')); } } From 9fbb114301d729102534a3010b6455a7c8b55c78 Mon Sep 17 00:00:00 2001 From: kordood Date: Sun, 14 Aug 2022 14:03:17 +0900 Subject: [PATCH 0209/2015] Create ko.rs Signed-off-by: kordood --- src/lang/ko.rs | 303 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 303 insertions(+) create mode 100644 src/lang/ko.rs diff --git a/src/lang/ko.rs b/src/lang/ko.rs new file mode 100644 index 000000000..a0292adcb --- /dev/null +++ b/src/lang/ko.rs @@ -0,0 +1,303 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "상태"), + ("Your Desktop", "당신의 데스크탑"), + ("desk_tip", "아래 ID와 비밀번호를 통해 당신의 데스크탑으로 접속할 수 있습니다."), + ("Password", "비밀번호"), + ("Ready", "준비"), + ("Established", "연결됨"), + ("connecting_status", "RustDesk 네트워크로 연결중입니다..."), + ("Enable Service", "서비스 활성화"), + ("Start Service", "서비스 시작"), + ("Service is running", "서비스 동작중"), + ("Service is not running", "서비스가 동작하고 있지 않습니다"), + ("not_ready_status", "준비되지 않음. 연결을 확인해주시길 바랍니다."), + ("Control Remote Desktop", "원격 데스크탑 제어"), + ("Transfer File", "파일 전송"), + ("Connect", "접속하기"), + ("Recent Sessions", "최근 세션"), + ("Address Book", "세션 주소록"), + ("Confirmation", "확인"), + ("TCP Tunneling", "TCP 터널링"), + ("Remove", "삭제"), + ("Refresh random password", "랜덤 비밀번호 새로고침"), + ("Set your own password", "개인 비밀번호 설정"), + ("Enable Keyboard/Mouse", "키보드/마우스 활성화"), + ("Enable Clipboard", "클립보드 활성화"), + ("Enable File Transfer", "파일 전송 활성화"), + ("Enable TCP Tunneling", "TCP 터널링 활성화"), + ("IP Whitelisting", "IP 화이트리스트"), + ("ID/Relay Server", "ID/Relay 서버"), + ("Stop service", "서비스 중단"), + ("Change ID", "ID 변경"), + ("Website", "웹사이트"), + ("About", "정보"), + ("Mute", "음소거"), + ("Audio Input", "오디오 입력"), + ("Enhancements", ""), + ("Hardware Codec", "하드웨어 코덱"), + ("Adaptive Bitrate", "가변 비트레이트"), + ("ID Server", "ID 서버"), + ("Relay Server", "Relay 서버"), + ("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", "불가능"), + ("Too frequent", "너무 잦은 시도"), + ("Cancel", "취소"), + ("Skip", "넘기기"), + ("Close", "닫기"), + ("Retry", "재시도"), + ("OK", "확인"), + ("Password Required", "비밀번호 입력"), + ("Please enter your password", "비밀번호를 입력해주세요"), + ("Remember password", "이 비밀번호 기억하기"), + ("Wrong Password", "틀린 비밀번호"), + ("Do you want to enter again?", "다시 접속하시겠습니까?"), + ("Connection Error", "연결 에러"), + ("Error", "에러"), + ("Reset by the peer", "다른 접속자에 의해 초기화됨"), + ("Connecting...", "연결중..."), + ("Connection in progress. Please wait.", "연결중입니다. 잠시만 기다려주세요."), + ("Please try 1 minute later", "1분 뒤 다시 시도해주세요"), + ("Login Error", "로그인 에러"), + ("Successful", "성공"), + ("Connected, waiting for image...", "연결됨. 이미지를 기다리는중..."), + ("Name", "이름"), + ("Type", "유형"), + ("Modified", "수정됨"), + ("Size", "크기"), + ("Show Hidden Files", "숨김 파일 보기"), + ("Receive", "받기"), + ("Send", "보내기"), + ("Refresh File", "파일 새로고침"), + ("Local", "로컬"), + ("Remote", "원격"), + ("Remote Computer", "원격 컴퓨터"), + ("Local Computer", "로컬 컴퓨터"), + ("Confirm Delete", "삭제 재확인"), + ("Delete", "삭제"), + ("Properties", "속성"), + ("Multi Select", "다중 선택"), + ("Empty Directory", "빈 디렉터리"), + ("Not an empty directory", "디렉터리가 비어있지 않습니다"), + ("Are you sure you want to delete this file?", "정말로 해당 파일을 삭제하시겠습니까?"), + ("Are you sure you want to delete this empty directory?", "정말로 비어있는 해당 디렉터리를 삭제하시겠습니까?"), + ("Are you sure you want to delete the file of this directory?", "정말로 해당 파일 혹은 디렉터리를 삭제하시겠습니까?"), + ("Do this for all conflicts", "모든 충돌에 대해 해당 작업 수행"), + ("This is irreversible!", "해당 결정은 돌이킬 수 없습니다!"), + ("Deleting", "삭제중"), + ("files", "파일"), + ("Waiting", "대기중"), + ("Finished", "완료됨"), + ("Speed", "속도"), + ("Custom Image Quality", "이미지 품질 조정"), + ("Privacy mode", "개인정보 보호 모드"), + ("Block user input", "사용자 입력 차단"), + ("Unblock user input", "사용자 입력 차단 해제"), + ("Adjust Window", "화면 조정"), + ("Original", "원본"), + ("Shrink", "축소"), + ("Stretch", "확대"), + ("Good image quality", "최적 이미지 품질"), + ("Balanced", "균형"), + ("Optimize reaction time", "반응 시간 최적화"), + ("Custom", "커스텀"), + ("Show remote cursor", "원격 커서 보이기"), + ("Show quality monitor", "품질 모니터 띄우기"), + ("Disable clipboard", "클립보드 비활성화"), + ("Lock after session end", "세션 종료 후 화면 잠금"), + ("Insert", "입력"), + ("Insert Lock", "입력 잠금"), + ("Refresh", "새로고침"), + ("ID does not exist", "ID가 존재하지 않습니다"), + ("Failed to connect to rendezvous server", "rendezvous 서버에 접속을 실패하였습니다"), + ("Please try later", "다시 시도해주세요"), + ("Remote desktop is offline", "원격 데스크탑이 연결되어 있지 않습니다"), + ("Key mismatch", "키가 일치하지 않습니다."), + ("Timeout", "시간 초과"), + ("Failed to connect to relay server", "relay 서버에 접속을 실패하였습니다"), + ("Failed to connect via rendezvous server", "rendezvous 서버를 통한 접속에 실패하였습니다"), + ("Failed to connect via relay server", "relay 서버를 통한 접속에 실패하였습니다"), + ("Failed to make direct connection to remote desktop", "원격 데스크탑으로의 직접 연결 생성에 실패하였습니다"), + ("Set Password", "비밀번호 설정"), + ("OS Password", "OS 비밀번호"), + ("install_tip", "UAC로 인해, RustDesk가 원격지일 때 일부 기능이 동작하지 않을 수 있습니다. UAC 문제를 방지하려면, 아래 버튼을 클릭하여 RustDesk를 시스템에 설치해주세요."), + ("Click to upgrade", "클릭하여 업그레이드"), + ("Click to download", "클릭하여 다운로드"), + ("Click to update", "클릭하여 업데이트"), + ("Configure", "구성"), + ("config_acc", "당신의 데스크탑을 원격으로 제어하기 전에, RustDesk에게 \"Accessibility (접근성)\" 권한을 부여해야 합니다."), + ("config_screen", "당신의 데스크탑을 원격으로 제어하기 전에, RustDesk에게 \"Screen Recording (화면 녹화)\" 권한을 부여해야 합니다."), + ("Installing ...", "설치중 ..."), + ("Install", "설치하기"), + ("Installation", "설치"), + ("Installation Path", "설치 경로"), + ("Create start menu shortcuts", "시작 메뉴에 바로가기 생성"), + ("Create desktop icon", "데스크탑 아이콘 생성"), + ("agreement_tip", "설치를 시작하기 전에, 라이선스 약관에 동의를 해야합니다."), + ("Accept and Install", "동의 및 설치"), + ("End-user license agreement", "최종 사용자 라이선스 약관 동의"), + ("Generating ...", "생성중 ..."), + ("Your installation is lower version.", "설치 버전이 최신 버전이 아닙니다."), + ("not_close_tcp_tip", "연결을 사용하는 동안 이 창을 끄지 마세요"), + ("Listening ...", "연결 대기중 ..."), + ("Remote Host", "원격 호스트"), + ("Remote Port", "원격 포트"), + ("Action", "액션"), + ("Add", "추가"), + ("Local Port", "로컬 포트"), + ("setup_server_tip", "빠른 접속을 위해, 당신의 서버를 설정하세요"), + ("Too short, at least 6 characters.", "너무 짧습니다, 최소 6글자 이상 입력해주세요."), + ("The confirmation is not identical.", "확인용 입력이 일치하지 않습니다."), + ("Permissions", "권한"), + ("Accept", "수락"), + ("Dismiss", "거부"), + ("Disconnect", "연결 종료"), + ("Allow using keyboard and mouse", "키보드와 마우스 허용"), + ("Allow using clipboard", "클립보드 허용"), + ("Allow hearing sound", "소리 듣기 허용"), + ("Allow file copy and paste", "파일 복사 및 붙여넣기 허용"), + ("Connected", "연결됨"), + ("Direct and encrypted connection", "암호화된 직접 연결"), + ("Relayed and encrypted connection", "암호화된 릴레이 연결"), + ("Direct and unencrypted connection", "암호화되지 않은 직접 연결"), + ("Relayed and unencrypted connection", "암호화되지 않은 릴레이 연결"), + ("Enter Remote ID", "원격지 ID를 입력하세요"), + ("Enter your password", "비밀번호를 입력하세요"), + ("Logging in...", "로그인 중..."), + ("Enable RDP session sharing", "RDP 세션 공유를 활성화하세요"), + ("Auto Login", "자동 로그인"), + ("Enable Direct IP Access", "IP 직접 접근 활성화하세요"), + ("Rename", "이름 변경"), + ("Space", "공간"), + ("Create Desktop Shortcut", "데스크탑 바로가기 생성"), + ("Change Path", "경로 변경"), + ("Create Folder", "폴더 생성"), + ("Please enter the folder name", "폴더명을 입력해주세요"), + ("Fix it", "문제 해결"), + ("Warning", "경고"), + ("Login screen using Wayland is not supported", "Wayland를 사용한 로그인 화면이 지원되지 않습니다"), + ("Reboot required", "재부팅이 필요합니다"), + ("Unsupported display server ", "지원하지 않는 디스플레이 서버"), + ("x11 expected", "x11 예상됨"), + ("Port", "포트"), + ("Settings", "설정"), + ("Username", "사용자명"), + ("Invalid port", "유효하지 않은 포트"), + ("Closed manually by the peer", "다른 사용자에 의해 종료됨"), + ("Enable remote configuration modification", "원격 구성 변경 활성화"), + ("Run without install", "설치 없이 실행"), + ("Always connected via relay", "항상 relay를 통해 접속됨"), + ("Always connect via relay", "항상 relay를 통해 접속하기"), + ("whitelist_tip", "화이트리스트에 있는 IP만 현 데스크탑에 접속 가능합니다"), + ("Login", "로그인"), + ("Logout", "로그아웃"), + ("Tags", "태그"), + ("Search ID", "ID 검색"), + ("Current Wayland display server is not supported", "현재 Wayland 디스플레이 서버가 지원되지 않습니다"), + ("whitelist_sep", "다음 글자로 구분합니다. ',(콤마) ;(세미콜론) 띄어쓰기 혹은 줄바꿈'"), + ("Add ID", "ID 추가"), + ("Add Tag", "태그 추가"), + ("Unselect all tags", "모든 태그 선택 해제"), + ("Network error", "네트워크 에러"), + ("Username missed", "사용자명 누락"), + ("Password missed", "비밀번호 누락"), + ("Wrong credentials", "틀린 인증 정보"), + ("Edit Tag", "태그 수정"), + ("Unremember Password", "패스워드 기억하지 않기"), + ("Favorites", "즐겨찾기"), + ("Add to Favorites", "즐겨찾기에 추가"), + ("Remove from Favorites", "즐겨찾기에서 삭제"), + ("Empty", "비어 있음"), + ("Invalid folder name", "유효하지 않은 폴더명"), + ("Socks5 Proxy", "Socks5 프록시"), + ("Hostname", "호스트명"), + ("Discovered", "찾음"), + ("install_daemon_tip", "부팅된 이후 시스템 서비스에 설치해야 합니다."), + ("Remote ID", "원격지 ID"), + ("Paste", "붙여넣기"), + ("Paste here?", "여기에 붙여넣겠습니까?"), + ("Are you sure to close the connection?", "정말로 연결을 종료하시겠습니까?"), + ("Download new version", "최신 버전 다운로드"), + ("Touch mode", "터치 모드"), + ("Mouse mode", "마우스 모드"), + ("One-Finger Tap", "한 손가락 탭"), + ("Left Mouse", "왼쪽 마우스"), + ("One-Long Tap", "길게 누르기"), + ("Two-Finger Tap", "두 손가락 탭"), + ("Right Mouse", "오른쪽 마우스"), + ("One-Finger Move", "한 손가락 이동"), + ("Double Tap & Move", "두 번 탭 하고 이동"), + ("Mouse Drag", "마우스 드래그"), + ("Three-Finger vertically", "세 손가락 세로로"), + ("Mouse Wheel", "마우스 휠"), + ("Two-Finger Move", "두 손가락 이동"), + ("Canvas Move", "캔버스 이동"), + ("Pinch to Zoom", "확대/축소"), + ("Canvas Zoom", "캔버스 확대"), + ("Reset canvas", "캔버스 초기화"), + ("No permission of file transfer", "파일 전송 권한이 없습니다"), + ("Note", "노트"), + ("Connection", "연결"), + ("Share Screen", "화면 공유"), + ("CLOSE", "종료"), + ("OPEN", "열기"), + ("Chat", "채팅"), + ("Total", "총합"), + ("items", "개체"), + ("Selected", "선택됨"), + ("Screen Capture", "화면 캡처"), + ("Input Control", "입력 제어"), + ("Audio Capture", "오디오 캡처"), + ("File Connection", "파일 전송"), + ("Screen Connection", "화면 전송"), + ("Do you accept?", "동의하십니까?"), + ("Open System Setting", "시스템 설정 열기"), + ("How to get Android input permission?", "안드로이드 입력 권한에 어떻게 접근합니까?"), + ("android_input_permission_tip1", "원격지로서 마우스나 터치를 통해 Android 장치를 제어하려면 RustDesk에서 \"Accessibility (접근성)\" 서비스 사용을 허용해야 합니다."), + ("android_input_permission_tip2", "시스템 설정 페이지로 이동하여 [설치된 서비스]에서 [RustDesk Input] 서비스를 켜십시오."), + ("android_new_connection_tip", "현재 장치의 새로운 제어 요청이 수신되었습니다."), + ("android_service_will_start_tip", "\"화면 캡처\"를 켜면 서비스가 자동으로 시작되어 다른 장치에서 사용자 장치에 대한 연결을 요청할 수 있습니다."), + ("android_stop_service_tip", "서비스를 종료하면 모든 연결이 자동으로 닫힙니다."), + ("android_version_audio_tip", "현재 Android 버전은 오디오 캡처를 지원하지 않습니다. Android 10 이상으로 업그레이드하십시오."), + ("android_start_service_tip", "[서비스 시작] 또는 [화면 캡처] 권한을 눌러 화면 공유 서비스를 시작합니다."), + ("Account", "계정"), + ("Overwrite", "덮어쓰기"), + ("This file exists, skip or overwrite this file?", "해당 파일이 이미 존재합니다, 넘어가거나 덮어쓰시겠습니까?"), + ("Quit", "종료"), + ("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"), + ("Help", "지원"), + ("Failed", "실패"), + ("Succeeded", "성공"), + ("Someone turns on privacy mode, exit", "누군가가 개인정보 보호 모드를 활성화하여 종료됩니다"), + ("Unsupported", "지원되지 않음"), + ("Peer denied", "다른 사용자에 의해 거부됨"), + ("Please install plugins", "플러그인을 설치해주세요"), + ("Peer exit", "다른 사용자가 나감"), + ("Failed to turn off", "종료에 실패함"), + ("Turned off", "종료됨"), + ("In privacy mode", "개인정보 보호 모드 진입"), + ("Out privacy mode", "개인정보 보호 모드 나감"), + ("Language", "언어"), + ("Keep RustDesk background service", "RustDesk 백그라운드 서비스로 유지하기"), + ("Ignore Battery Optimizations", "배터리 최적화 무시하기"), + ("android_open_battery_optimizations_tip", "해당 기능을 비활성화하려면 RustDesk 응용 프로그램 설정 페이지로 이동하여 [배터리]에서 [제한 없음] 선택을 해제하십시오."), + ("Connection not allowed", "연결이 허용되지 않음"), + ("Use temporary password", "임시 비밀번호 사용"), + ("Use permanent password", "영구 비밀번호 사용"), + ("Use both passwords", "두 비밀번호 (임시/영구) 사용"), + ("Set permanent password", "영구 비밀번호 설정"), + ("Set temporary password length", "임시 비밀번호 길이 설정"), + ("Enable Remote Restart", "원격지 재시작 활성화"), + ("Allow remote restart", "원격지 재시작 허용"), + ("Restart Remote Device", "원격 기기 재시작"), + ("Are you sure you want to restart", "정말로 재시작 하시겠습니까"), + ("Restarting Remote Device", "원격 기기를 다시 시작하는중"), + ("remote_restarting_tip", "원격 장치를 다시 시작하는 중입니다. 이 메시지 상자를 닫고 잠시 후 영구 비밀번호로 다시 연결하십시오."), + ].iter().cloned().collect(); +} \ No newline at end of file From 7cb9540c3fb4630cb7a2c9d38a7190b775202e67 Mon Sep 17 00:00:00 2001 From: Software Magic <16600519+SoftwareMagicIT@users.noreply.github.com> Date: Sun, 14 Aug 2022 10:17:53 +0200 Subject: [PATCH 0210/2015] Added missing translation and changed some mistakes My first contribute. I added missing translations and changed some mistakes --- src/lang/it.rs | 48 ++++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/src/lang/it.rs b/src/lang/it.rs index 8058806c2..f608753e1 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -35,9 +35,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "Informazioni"), ("Mute", "Silenzia"), ("Audio Input", "Input audio"), - ("Enhancements", ""), - ("Hardware Codec", ""), - ("Adaptive Bitrate", ""), + ("Enhancements", "Miglioramenti"), + ("Hardware Codec", "Codifica Hardware"), + ("Adaptive Bitrate", "Bitrate Adattivo"), ("ID Server", "ID server"), ("Relay Server", "Server relay"), ("API Server", "Server API"), @@ -53,10 +53,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Close", "Chiudi"), ("Retry", "Riprova"), ("OK", "OK"), - ("Password Required", "Password richiesta"), + ("Password Required", "Password Richiesta"), ("Please enter your password", "Inserisci la tua password"), ("Remember password", "Ricorda password"), - ("Wrong Password", "Password errata"), + ("Wrong Password", "Password Errata"), ("Do you want to enter again?", "Vuoi riprovare?"), ("Connection Error", "Errore di connessione"), ("Error", "Errore"), @@ -64,7 +64,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Connecting...", "Connessione..."), ("Connection in progress. Please wait.", "Connessione in corso. Attendi."), ("Please try 1 minute later", "Per favore riprova fra 1 minuto"), - ("Login Error", "Errore di login"), + ("Login Error", "Errore Login"), ("Successful", "Successo"), ("Connected, waiting for image...", "Connesso, in attesa dell'immagine..."), ("Name", "Nome"), @@ -101,8 +101,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Unblock user input", "Sbloccare l'input dell'utente"), ("Adjust Window", "Adatta la finestra"), ("Original", "Originale"), - ("Shrink", "Restringi"), - ("Stretch", "Allarga"), + ("Shrink", "Scala"), + ("Stretch", "Adatta"), ("Good image quality", "Buona qualità immagine"), ("Balanced", "Bilanciato"), ("Optimize reaction time", "Ottimizza il tempo di reazione"), @@ -220,7 +220,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Discovered", "Rilevati"), ("install_daemon_tip", "Per avviarsi all'accensione, è necessario installare il servizio di sistema."), ("Remote ID", "ID remoto"), - ("Paste", "Impasto"), + ("Paste", "Incolla"), ("Paste here?", "Incolla qui?"), ("Are you sure to close the connection?", "Sei sicuro di voler chiudere la connessione?"), ("Download new version", "Scarica nuova versione"), @@ -239,8 +239,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Two-Finger Move", "Movimento con due dita"), ("Canvas Move", "Sposta tela"), ("Pinch to Zoom", "Pizzica per zoomare"), - ("Canvas Zoom", "Zoom tela"), - ("Reset canvas", "Ripristina tela"), + ("Canvas Zoom", "Zoom canvas"), + ("Reset canvas", "Ripristina canvas"), ("No permission of file transfer", "Nessun permesso di trasferimento di file"), ("Note", "Nota"), ("Connection", "Connessione"), @@ -276,7 +276,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Succeeded", "Successo"), ("Someone turns on privacy mode, exit", "Qualcuno attiva la modalità privacy, esci"), ("Unsupported", "Non supportato"), - ("Peer denied", "Pari negato"), + ("Peer denied", "Peer negato"), ("Please install plugins", "Si prega di installare i plugin"), ("Peer exit", "Uscita tra pari"), ("Failed to turn off", "Impossibile spegnere"), @@ -287,17 +287,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", ""), ("Ignore Battery Optimizations", ""), ("android_open_battery_optimizations_tip", ""), - ("Connection not allowed", ""), - ("Use temporary password", ""), - ("Use permanent password", ""), - ("Use both passwords", ""), - ("Set permanent password", ""), - ("Set temporary password length", ""), - ("Enable Remote Restart", ""), - ("Allow remote restart", ""), - ("Restart Remote Device", ""), - ("Are you sure you want to restart", ""), - ("Restarting Remote Device", ""), - ("remote_restarting_tip", ""), + ("Connection not allowed", "Connessione non consentita"), + ("Use temporary password", "Usa password temporanea"), + ("Use permanent password", "Usa password permanente"), + ("Use both passwords", "Usa entrambe le password"), + ("Set permanent password", "Imposta password permanente"), + ("Set temporary password length", "Imposta lunghezza passwod temporanea"), + ("Enable Remote Restart", "Abilita riavvio da remoto"), + ("Allow remote restart", "Consenti riavvio da remoto"), + ("Restart Remote Device", "Riavvia dispositivo remoto"), + ("Are you sure you want to restart", "Sei sicuro di voler riavviare?"), + ("Restarting Remote Device", "Il dispositivo remoto si sta riavviando"), + ("remote_restarting_tip", "Riavviare il dispositivo remoto"), ].iter().cloned().collect(); } From 5887334c2e5118bbcfabe45ea093ca0ca70a99cb Mon Sep 17 00:00:00 2001 From: 21pages Date: Sat, 13 Aug 2022 12:43:35 +0800 Subject: [PATCH 0211/2015] add setting page Signed-off-by: 21pages --- flutter/lib/common.dart | 33 +- .../lib/desktop/pages/desktop_home_page.dart | 440 +------ .../desktop/pages/desktop_setting_page.dart | 1008 ++++++++++++++++- .../lib/desktop/pages/desktop_tab_page.dart | 2 +- flutter/lib/models/server_model.dart | 6 +- src/flutter_ffi.rs | 8 +- src/ui.rs | 25 +- src/ui_interface.rs | 4 + 8 files changed, 1066 insertions(+), 460 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index cabd91b9e..aa5666e86 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -55,7 +55,6 @@ class MyTheme { bool isDarkTheme() { final isDark = "Y" == Get.find().getString("darkTheme"); - debugPrint("current is dark theme: $isDark"); return isDark; } @@ -482,3 +481,35 @@ String translate(String name) { } return platformFFI.translate(name, localeName); } + +bool option2bool(String key, String value) { + bool res; + if (key.startsWith("enable-")) { + res = value != "N"; + } else if (key.startsWith("allow-") || + key == "stop-service" || + key == "direct-server" || + key == "stop-rendezvous-service") { + res = value == "Y"; + } else { + assert(false); + res = value != "N"; + } + return res; +} + +String bool2option(String key, bool option) { + String res; + if (key.startsWith('enable-')) { + res = option ? '' : 'N'; + } else if (key.startsWith('allow-') || + key == "stop-service" || + key == "direct-server" || + key == "stop-rendezvous-service") { + res = option ? 'Y' : ''; + } else { + assert(false); + res = option ? 'Y' : 'N'; + } + return res; +} diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index f68cdac94..627c5b2e4 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart' hide MenuItem; import 'package:flutter/services.dart'; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/desktop/pages/connection_page.dart'; +import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/server_model.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; @@ -26,7 +27,10 @@ class DesktopHomePage extends StatefulWidget { const borderColor = Color(0xFF2F65BA); class _DesktopHomePageState extends State - with TrayListener, WindowListener { + with TrayListener, WindowListener, AutomaticKeepAliveClientMixin { + @override + bool get wantKeepAlive => true; + @override void onWindowClose() async { super.onWindowClose(); @@ -678,440 +682,6 @@ class _DesktopHomePageState extends State }); } - void changeServer() async { - Map oldOptions = jsonDecode(await bind.mainGetOptions()); - print("${oldOptions}"); - String idServer = oldOptions['custom-rendezvous-server'] ?? ""; - var idServerMsg = ""; - String relayServer = oldOptions['relay-server'] ?? ""; - var relayServerMsg = ""; - String apiServer = oldOptions['api-server'] ?? ""; - var apiServerMsg = ""; - var key = oldOptions['key'] ?? ""; - - var isInProgress = false; - gFFI.dialogManager.show((setState, close) { - return CustomAlertDialog( - title: Text(translate("ID/Relay Server")), - content: ConstrainedBox( - constraints: BoxConstraints(minWidth: 500), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - height: 8.0, - ), - Row( - children: [ - ConstrainedBox( - constraints: BoxConstraints(minWidth: 100), - child: Text("${translate('ID Server')}:") - .marginOnly(bottom: 16.0)), - SizedBox( - width: 24.0, - ), - Expanded( - child: TextField( - onChanged: (s) { - idServer = s; - }, - decoration: InputDecoration( - border: OutlineInputBorder(), - errorText: - idServerMsg.isNotEmpty ? idServerMsg : null), - controller: TextEditingController(text: idServer), - ), - ), - ], - ), - SizedBox( - height: 8.0, - ), - Row( - children: [ - ConstrainedBox( - constraints: BoxConstraints(minWidth: 100), - child: Text("${translate('Relay Server')}:") - .marginOnly(bottom: 16.0)), - SizedBox( - width: 24.0, - ), - Expanded( - child: TextField( - onChanged: (s) { - relayServer = s; - }, - decoration: InputDecoration( - border: OutlineInputBorder(), - errorText: relayServerMsg.isNotEmpty - ? relayServerMsg - : null), - controller: TextEditingController(text: relayServer), - ), - ), - ], - ), - SizedBox( - height: 8.0, - ), - Row( - children: [ - ConstrainedBox( - constraints: BoxConstraints(minWidth: 100), - child: Text("${translate('API Server')}:") - .marginOnly(bottom: 16.0)), - SizedBox( - width: 24.0, - ), - Expanded( - child: TextField( - onChanged: (s) { - apiServer = s; - }, - decoration: InputDecoration( - border: OutlineInputBorder(), - errorText: - apiServerMsg.isNotEmpty ? apiServerMsg : null), - controller: TextEditingController(text: apiServer), - ), - ), - ], - ), - SizedBox( - height: 8.0, - ), - Row( - children: [ - ConstrainedBox( - constraints: BoxConstraints(minWidth: 100), - child: Text("${translate('Key')}:") - .marginOnly(bottom: 16.0)), - SizedBox( - width: 24.0, - ), - Expanded( - child: TextField( - onChanged: (s) { - key = s; - }, - decoration: InputDecoration( - border: OutlineInputBorder(), - ), - controller: TextEditingController(text: key), - ), - ), - ], - ), - SizedBox( - height: 4.0, - ), - Offstage( - offstage: !isInProgress, child: LinearProgressIndicator()) - ], - ), - ), - actions: [ - TextButton( - onPressed: () { - close(); - }, - child: Text(translate("Cancel"))), - TextButton( - onPressed: () async { - setState(() { - [idServerMsg, relayServerMsg, apiServerMsg] - .forEach((element) { - element = ""; - }); - isInProgress = true; - }); - final cancel = () { - setState(() { - isInProgress = false; - }); - }; - idServer = idServer.trim(); - relayServer = relayServer.trim(); - apiServer = apiServer.trim(); - key = key.trim(); - - if (idServer.isNotEmpty) { - idServerMsg = translate( - await bind.mainTestIfValidServer(server: idServer)); - if (idServerMsg.isEmpty) { - oldOptions['custom-rendezvous-server'] = idServer; - } else { - cancel(); - return; - } - } else { - oldOptions['custom-rendezvous-server'] = ""; - } - - if (relayServer.isNotEmpty) { - relayServerMsg = translate( - await bind.mainTestIfValidServer(server: relayServer)); - if (relayServerMsg.isEmpty) { - oldOptions['relay-server'] = relayServer; - } else { - cancel(); - return; - } - } else { - oldOptions['relay-server'] = ""; - } - - if (apiServer.isNotEmpty) { - if (apiServer.startsWith('http://') || - apiServer.startsWith("https://")) { - oldOptions['api-server'] = apiServer; - return; - } else { - apiServerMsg = translate("invalid_http"); - cancel(); - return; - } - } else { - oldOptions['api-server'] = ""; - } - // ok - oldOptions['key'] = key; - await bind.mainSetOptions(json: jsonEncode(oldOptions)); - close(); - }, - child: Text(translate("OK"))), - ], - ); - }); - } - - void changeWhiteList() async { - Map oldOptions = jsonDecode(await bind.mainGetOptions()); - var newWhiteList = ((oldOptions['whitelist'] ?? "") as String).split(','); - var newWhiteListField = newWhiteList.join('\n'); - var msg = ""; - var isInProgress = false; - gFFI.dialogManager.show((setState, close) { - return CustomAlertDialog( - title: Text(translate("IP Whitelisting")), - content: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(translate("whitelist_sep")), - SizedBox( - height: 8.0, - ), - Row( - children: [ - Expanded( - child: TextField( - onChanged: (s) { - newWhiteListField = s; - }, - maxLines: null, - decoration: InputDecoration( - border: OutlineInputBorder(), - errorText: msg.isEmpty ? null : translate(msg), - ), - controller: TextEditingController(text: newWhiteListField), - ), - ), - ], - ), - SizedBox( - height: 4.0, - ), - Offstage(offstage: !isInProgress, child: LinearProgressIndicator()) - ], - ), - actions: [ - TextButton( - onPressed: () { - close(); - }, - child: Text(translate("Cancel"))), - TextButton( - onPressed: () async { - setState(() { - msg = ""; - isInProgress = true; - }); - newWhiteListField = newWhiteListField.trim(); - var newWhiteList = ""; - if (newWhiteListField.isEmpty) { - // pass - } else { - final ips = - newWhiteListField.trim().split(RegExp(r"[\s,;\n]+")); - // test ip - final ipMatch = RegExp(r"^\d+\.\d+\.\d+\.\d+$"); - for (final ip in ips) { - if (!ipMatch.hasMatch(ip)) { - msg = translate("Invalid IP") + " $ip"; - setState(() { - isInProgress = false; - }); - return; - } - } - newWhiteList = ips.join(','); - } - oldOptions['whitelist'] = newWhiteList; - await bind.mainSetOptions(json: jsonEncode(oldOptions)); - close(); - }, - child: Text(translate("OK"))), - ], - ); - }); - } - - void changeSocks5Proxy() async { - var socks = await bind.mainGetSocks(); - - String proxy = ""; - String proxyMsg = ""; - String username = ""; - String password = ""; - if (socks.length == 3) { - proxy = socks[0]; - username = socks[1]; - password = socks[2]; - } - - var isInProgress = false; - gFFI.dialogManager.show((setState, close) { - return CustomAlertDialog( - title: Text(translate("Socks5 Proxy")), - content: ConstrainedBox( - constraints: BoxConstraints(minWidth: 500), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - height: 8.0, - ), - Row( - children: [ - ConstrainedBox( - constraints: BoxConstraints(minWidth: 100), - child: Text("${translate('Hostname')}:") - .marginOnly(bottom: 16.0)), - SizedBox( - width: 24.0, - ), - Expanded( - child: TextField( - onChanged: (s) { - proxy = s; - }, - decoration: InputDecoration( - border: OutlineInputBorder(), - errorText: proxyMsg.isNotEmpty ? proxyMsg : null), - controller: TextEditingController(text: proxy), - ), - ), - ], - ), - SizedBox( - height: 8.0, - ), - Row( - children: [ - ConstrainedBox( - constraints: BoxConstraints(minWidth: 100), - child: Text("${translate('Username')}:") - .marginOnly(bottom: 16.0)), - SizedBox( - width: 24.0, - ), - Expanded( - child: TextField( - onChanged: (s) { - username = s; - }, - decoration: InputDecoration( - border: OutlineInputBorder(), - ), - controller: TextEditingController(text: username), - ), - ), - ], - ), - SizedBox( - height: 8.0, - ), - Row( - children: [ - ConstrainedBox( - constraints: BoxConstraints(minWidth: 100), - child: Text("${translate('Password')}:") - .marginOnly(bottom: 16.0)), - SizedBox( - width: 24.0, - ), - Expanded( - child: TextField( - onChanged: (s) { - password = s; - }, - decoration: InputDecoration( - border: OutlineInputBorder(), - ), - controller: TextEditingController(text: password), - ), - ), - ], - ), - SizedBox( - height: 8.0, - ), - Offstage( - offstage: !isInProgress, child: LinearProgressIndicator()) - ], - ), - ), - actions: [ - TextButton( - onPressed: () { - close(); - }, - child: Text(translate("Cancel"))), - TextButton( - onPressed: () async { - setState(() { - proxyMsg = ""; - isInProgress = true; - }); - final cancel = () { - setState(() { - isInProgress = false; - }); - }; - proxy = proxy.trim(); - username = username.trim(); - password = password.trim(); - - if (proxy.isNotEmpty) { - proxyMsg = translate( - await bind.mainTestIfValidServer(server: proxy)); - if (proxyMsg.isEmpty) { - // ignore - } else { - cancel(); - return; - } - } - await bind.mainSetSocks( - proxy: proxy, username: username, password: password); - close(); - }, - child: Text(translate("OK"))), - ], - ); - }); - } - void about() async { final appName = await bind.mainGetAppName(); final license = await bind.mainGetLicense(); diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 4d9a58f3b..0da3dcc50 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -1,4 +1,20 @@ -import 'package:flutter/cupertino.dart'; +import 'dart:convert'; +import 'dart:io' show Platform; +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; +import 'package:flutter_hbb/models/server_model.dart'; +import 'package:get/get.dart'; +import 'package:provider/provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +const double _kCardFixedWidth = 600; +const double _kCardLeftPadding = 20; +const double _kContentLeftPadding = 30; +const double _kListViewBottomPadding = 30; class DesktopSettingPage extends StatefulWidget { DesktopSettingPage({Key? key}) : super(key: key); @@ -7,9 +23,995 @@ class DesktopSettingPage extends StatefulWidget { State createState() => _DesktopSettingPageState(); } -class _DesktopSettingPageState extends State { +class _DesktopSettingPageState extends State + with TickerProviderStateMixin, AutomaticKeepAliveClientMixin { + final List _destinations = + [ + _destination('Display', Icons.palette_outlined, Icons.palette), + _destination( + 'Security', Icons.health_and_safety_outlined, Icons.health_and_safety), + _destination( + 'Connection', Icons.settings_remote_outlined, Icons.settings_remote), + _destination('Video', Icons.videocam_outlined, Icons.videocam), + _destination('Audio', Icons.volume_up_outlined, Icons.volume_up), + ]; + + late TabController controller; + int _selectedIndex = 0; + + @override + bool get wantKeepAlive => true; + + @override + void initState() { + super.initState(); + controller = TabController(length: _destinations.length, vsync: this); + } + @override Widget build(BuildContext context) { - return Text("Settings"); + super.build(context); + return Scaffold( + body: Row( + children: [ + NavigationRail( + selectedIndex: _selectedIndex, + onDestinationSelected: (int index) { + setState(() { + _selectedIndex = index; + }); + controller.animateTo(index); + }, + labelType: NavigationRailLabelType.all, + destinations: _destinations, + ), + const VerticalDivider(thickness: 1, width: 1), + Expanded( + child: TabBarView( + controller: controller, + children: [ + _Display(), + _Safety(), + _Connection(), + _Video(), + _Audio(), + ], + ), + ) + ], + ), + ); + } + + static NavigationRailDestination _destination( + String label, IconData selected, IconData unSelected) { + return NavigationRailDestination( + icon: Icon(unSelected), + selectedIcon: Icon(selected), + label: Text(translate(label)), + ); } } + +//#region pages + +class _Display extends StatefulWidget { + _Display({Key? key}) : super(key: key); + + @override + State<_Display> createState() => _DisplayState(); +} + +class _DisplayState extends State<_Display> with AutomaticKeepAliveClientMixin { + @override + bool get wantKeepAlive => true; + + @override + Widget build(BuildContext context) { + super.build(context); + return ListView( + children: [ + _Card(title: translate('Display'), children: [language(), theme()]), + ], + ).paddingOnly(bottom: _kListViewBottomPadding); + } + + Widget language() { + return _futureBuilder(future: () async { + String langs = await bind.mainGetLangs(); + String lang = await bind.mainGetLocalOption(key: "lang"); + return {"langs": langs, "lang": lang}; + }(), hasData: (res) { + Map data = res as Map; + List langsList = jsonDecode(data["langs"]!); + Map langsMap = {for (var v in langsList) v[0]: v[1]}; + List keys = langsMap.keys.toList(); + List values = langsMap.values.toList(); + keys.insert(0, "default"); + values.insert(0, "Default"); + String currentKey = data["lang"]!; + if (!keys.contains(currentKey)) { + currentKey = "default"; + } + return _row( + 'Language', + _ComboBox( + keys: keys, + values: values, + initialKey: currentKey, + onChanged: (key) async { + await bind.mainSetLocalOption(key: "lang", value: key); + Get.forceAppUpdate(); + }, + )); + }); + } + + Widget theme() { + return _row( + 'Dark Theme', + Switch( + value: isDarkTheme(), + onChanged: ((dark) async { + Get.changeTheme(dark ? MyTheme.darkTheme : MyTheme.lightTheme); + Get.find() + .setString("darkTheme", dark ? "Y" : ""); + Get.forceAppUpdate(); + }))); + } +} + +class _Safety extends StatefulWidget { + const _Safety({Key? key}) : super(key: key); + + @override + State<_Safety> createState() => _SafetyState(); +} + +class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { + @override + bool get wantKeepAlive => true; + + @override + Widget build(BuildContext context) { + super.build(context); + return ListView( + children: [ + permissions(), + password(), + whitelist(), + ], + ).paddingOnly(bottom: _kListViewBottomPadding); + } + + Widget permissions() { + return _Card(title: 'Permissions', children: [ + _option_check('Enable Keyboard/Mouse', 'enable-keyboard'), + _option_check('Enable Clipboard', 'enable-clipboard'), + _option_check('Enable File Transfer', 'enable-file-transfer'), + _option_check('Enable Audio', 'enable-audio'), + _option_check('Enable Remote Restart', 'enable-remote-restart'), + _option_check('Enable remote configuration modification', + 'allow-remote-config-modification'), + ]); + } + + Widget password() { + return ChangeNotifierProvider.value( + value: gFFI.serverModel, + child: Consumer( + builder: ((context, model, child) => + _Card(title: 'Password', children: [ + _row( + 'Verification Method', + _ComboBox( + keys: [ + kUseTemporaryPassword, + kUsePermanentPassword, + kUseBothPasswords, + ], + values: [ + translate("Use temporary password"), + translate("Use permanent password"), + translate("Use both passwords"), + ], + initialKey: model.verificationMethod, + onChanged: (key) => model.verificationMethod = key)), + _row( + 'Temporary Password Length', + _ComboBox( + keys: ['6', '8', '10'], + values: ['6', '8', '10'], + initialKey: model.temporaryPasswordLength, + onChanged: (key) => model.temporaryPasswordLength = key, + enabled: + model.verificationMethod != kUsePermanentPassword, + )), + _button( + 'permanent_password_tip', + 'Set permanent password', + setPasswordDialog, + model.verificationMethod != kUseTemporaryPassword) + ])))); + } + + Widget whitelist() { + return _Card(title: 'IP Whitelisting', children: [ + _button('whitelist_tip', 'IP Whitelisting', changeWhiteList) + ]); + } +} + +class _Connection extends StatefulWidget { + const _Connection({Key? key}) : super(key: key); + + @override + State<_Connection> createState() => _ConnectionState(); +} + +class _ConnectionState extends State<_Connection> + with AutomaticKeepAliveClientMixin { + final TextEditingController controller = TextEditingController(); + + @override + bool get wantKeepAlive => true; + + @override + Widget build(BuildContext context) { + super.build(context); + return ListView( + children: [ + _Card(title: 'Server', children: [ + _button('self-hosting_tip', 'ID/Relay Server', changeServer), + ]), + _Card(title: 'Service', children: [ + _option_check('Enable Service', 'stop-service', reverse: true), + // TODO: Not implemented + // _option_check('Always connected via relay', 'allow-always-relay'), + // _option_check('Start ID/relay service', 'stop-rendezvous-service', + // reverse: true), + ]), + _Card(title: 'TCP Tunneling', children: [ + _option_check('Enable TCP Tunneling', 'enable-tunnel'), + ]), + direct_ip(), + _Card(title: 'Proxy', children: [ + _button('socks5_proxy_tip', 'Socks5 Proxy', changeSocks5Proxy), + ]), + ], + ).paddingOnly(bottom: _kListViewBottomPadding); + } + + Widget direct_ip() { + var update = () => setState(() {}); + return _Card(title: 'Direct IP Access', children: [ + _option_check('Enable Direct IP Access', 'direct-server', update: update), + _row( + 'Port', + _futureBuilder( + future: () async { + String enabled = await bind.mainGetOption(key: 'direct-server'); + String port = await bind.mainGetOption(key: 'direct-access-port'); + return {'enabled': enabled, 'port': port}; + }(), + hasData: (data) { + bool enabled = + option2bool('direct-server', data['enabled'].toString()); + String port = data['port'].toString(); + int? iport = int.tryParse(port); + if (iport == null || iport < 1 || iport > 65535) { + port = ''; + } + controller.text = port; + return TextField( + controller: controller, + enabled: enabled, + onChanged: (value) async { + await bind.mainSetOption( + key: 'direct-access-port', value: controller.text); + }, + decoration: InputDecoration( + hintText: '21118', + ), + ); + }, + ), + ), + ]); + } +} + +class _Video extends StatefulWidget { + const _Video({Key? key}) : super(key: key); + + @override + State<_Video> createState() => _VideoState(); +} + +class _VideoState extends State<_Video> with AutomaticKeepAliveClientMixin { + @override + bool get wantKeepAlive => true; + + @override + Widget build(BuildContext context) { + super.build(context); + return ListView( + children: [ + _Card(title: 'Adaptive Bitrate', children: [ + _option_check('Adaptive Bitrate', 'enable-abr'), + ]), + ], + ).paddingOnly(bottom: _kListViewBottomPadding); + } +} + +class _Audio extends StatefulWidget { + const _Audio({Key? key}) : super(key: key); + + @override + State<_Audio> createState() => _AudioState(); +} + +class _AudioState extends State<_Audio> with AutomaticKeepAliveClientMixin { + @override + bool get wantKeepAlive => true; + + @override + Widget build(BuildContext context) { + super.build(context); + var update = () => setState(() {}); + return ListView(children: [ + _Card( + title: 'Audio Input', + children: [ + _option_check('Mute', 'enable-audio', reverse: true, update: update), + _row( + 'Audio device', + _futureBuilder(future: () async { + List all = await bind.mainGetSoundInputs(); + String current = await bind.mainGetOption(key: 'audio-input'); + String enabled = await bind.mainGetOption(key: 'enable-audio'); + return {'all': all, 'current': current, 'enabled': enabled}; + }(), hasData: (data) { + List keys = (data['all'] as List).toList(); + List values = keys.toList(); + if (Platform.isWindows) { + keys.insert(0, ''); + values.insert(0, 'System Sound'); + } else { + keys.insert(0, ''); // TODO + values.insert(0, 'None'); + } + String initialKey = data['current']; + if (!keys.contains(initialKey)) { + initialKey = ''; + } + return _ComboBox( + keys: keys, + values: values, + initialKey: initialKey, + onChanged: (key) { + bind.mainSetOption(key: 'audio-input', value: key); + }, + enabled: + option2bool('enable-audio', data['enabled'].toString()), + ); + })), + ], + ) + ]).paddingOnly(bottom: _kListViewBottomPadding); + } +} + +//#endregion + +//#region components + +Widget _Card({required String title, required List children}) { + return Row( + children: [ + Container( + width: _kCardFixedWidth, + child: Card( + child: Column( + children: [ + Row( + children: [ + Text( + translate(title), + textAlign: TextAlign.start, + style: TextStyle( + fontSize: 25, + ), + ), + Spacer(), + ], + ).paddingOnly(left: _kContentLeftPadding, top: 10, bottom: 20), + ...children.map((e) => e.paddingOnly(top: 2)), + ], + ).paddingOnly(bottom: 10), + ).paddingOnly(left: _kCardLeftPadding, top: 20), + ), + ], + ); +} + +Widget _option_switch(String label, String key, + {Function()? update = null, bool reverse = false}) { + return _row( + label, + _futureBuilder( + future: bind.mainGetOption(key: key), + hasData: (data) { + bool value = option2bool(key, data.toString()); + if (reverse) value = !value; + var ref = value.obs; + return Obx((() => Switch( + value: ref.value, + onChanged: ((option) async { + ref.value = option; + if (reverse) option = !option; + String value = bool2option(key, option); + bind.mainSetOption(key: key, value: value); + update?.call(); + })))); + })); +} + +Widget _option_check(String label, String key, + {Function()? update = null, bool reverse = false}) { + return Row(children: [ + _futureBuilder( + future: bind.mainGetOption(key: key), + hasData: (data) { + bool value = option2bool(key, data.toString()); + if (reverse) value = !value; + var ref = value.obs; + return Obx((() => Checkbox( + value: ref.value, + onChanged: ((option) async { + if (option != null) { + ref.value = option; + if (reverse) option = !option; + String value = bool2option(key, option); + bind.mainSetOption(key: key, value: value); + update?.call(); + } + })))); + }).paddingOnly(right: 10), + Text(translate(label)), + ]).paddingOnly(left: _kContentLeftPadding); +} + +Widget _button(String tip, String label, Function() onPressed, + [bool enabled = true]) { + return _row( + translate(tip), + OutlinedButton( + onPressed: enabled ? onPressed : null, + child: Text( + translate(label), + ))); +} + +Widget _row(String label, Widget widget) { + return Row( + children: [ + Expanded( + child: Text( + translate(label), + )), + SizedBox( + width: 40, + ), + Expanded(child: widget), + ], + ).paddingSymmetric(horizontal: _kContentLeftPadding); +} + +Widget _futureBuilder( + {required Future? future, required Widget Function(dynamic data) hasData}) { + return FutureBuilder( + future: future, + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.hasData) { + return hasData(snapshot.data!); + } else { + if (snapshot.hasError) { + print(snapshot.error.toString()); + } + return Container(); + } + }); +} + +class _ComboBox extends StatelessWidget { + late final List keys; + late final List values; + late final String initialKey; + late final Function(String key) onChanged; + late final bool enabled; + + _ComboBox({ + Key? key, + required this.keys, + required this.values, + required this.initialKey, + required this.onChanged, + this.enabled = true, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + var index = keys.indexOf(initialKey); + if (index < 0) { + assert(false); + index = 0; + } + var ref = values[index].obs; + return Container( + child: SizedBox( + child: Obx((() => DropdownButton( + isExpanded: true, + value: ref.value, + elevation: 16, + underline: Container( + height: 40, + ), + icon: Icon( + Icons.arrow_drop_down_sharp, + size: 35, + ), + onChanged: enabled + ? (String? newValue) { + if (newValue != null && newValue != ref.value) { + ref.value = newValue; + onChanged(keys[values.indexOf(newValue)]); + } + } + : null, + items: values.map>((String value) { + return DropdownMenuItem( + value: value, + child: Text(value), + ); + }).toList(), + )))), + ); + } +} + +//#endregion + +//#region dialogs + +void changeServer() async { + Map oldOptions = jsonDecode(await bind.mainGetOptions()); + print("${oldOptions}"); + String idServer = oldOptions['custom-rendezvous-server'] ?? ""; + var idServerMsg = ""; + String relayServer = oldOptions['relay-server'] ?? ""; + var relayServerMsg = ""; + String apiServer = oldOptions['api-server'] ?? ""; + var apiServerMsg = ""; + var key = oldOptions['key'] ?? ""; + + var isInProgress = false; + gFFI.dialogManager.show((setState, close) { + return CustomAlertDialog( + title: Text(translate("ID/Relay Server")), + content: ConstrainedBox( + constraints: BoxConstraints(minWidth: 500), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: 8.0, + ), + Row( + children: [ + ConstrainedBox( + constraints: BoxConstraints(minWidth: 100), + child: Text("${translate('ID Server')}:") + .marginOnly(bottom: 16.0)), + SizedBox( + width: 24.0, + ), + Expanded( + child: TextField( + onChanged: (s) { + idServer = s; + }, + decoration: InputDecoration( + border: OutlineInputBorder(), + errorText: idServerMsg.isNotEmpty ? idServerMsg : null), + controller: TextEditingController(text: idServer), + ), + ), + ], + ), + SizedBox( + height: 8.0, + ), + Row( + children: [ + ConstrainedBox( + constraints: BoxConstraints(minWidth: 100), + child: Text("${translate('Relay Server')}:") + .marginOnly(bottom: 16.0)), + SizedBox( + width: 24.0, + ), + Expanded( + child: TextField( + onChanged: (s) { + relayServer = s; + }, + decoration: InputDecoration( + border: OutlineInputBorder(), + errorText: + relayServerMsg.isNotEmpty ? relayServerMsg : null), + controller: TextEditingController(text: relayServer), + ), + ), + ], + ), + SizedBox( + height: 8.0, + ), + Row( + children: [ + ConstrainedBox( + constraints: BoxConstraints(minWidth: 100), + child: Text("${translate('API Server')}:") + .marginOnly(bottom: 16.0)), + SizedBox( + width: 24.0, + ), + Expanded( + child: TextField( + onChanged: (s) { + apiServer = s; + }, + decoration: InputDecoration( + border: OutlineInputBorder(), + errorText: + apiServerMsg.isNotEmpty ? apiServerMsg : null), + controller: TextEditingController(text: apiServer), + ), + ), + ], + ), + SizedBox( + height: 8.0, + ), + Row( + children: [ + ConstrainedBox( + constraints: BoxConstraints(minWidth: 100), + child: + Text("${translate('Key')}:").marginOnly(bottom: 16.0)), + SizedBox( + width: 24.0, + ), + Expanded( + child: TextField( + onChanged: (s) { + key = s; + }, + decoration: InputDecoration( + border: OutlineInputBorder(), + ), + controller: TextEditingController(text: key), + ), + ), + ], + ), + SizedBox( + height: 4.0, + ), + Offstage(offstage: !isInProgress, child: LinearProgressIndicator()) + ], + ), + ), + actions: [ + TextButton( + onPressed: () { + close(); + }, + child: Text(translate("Cancel"))), + TextButton( + onPressed: () async { + setState(() { + [idServerMsg, relayServerMsg, apiServerMsg].forEach((element) { + element = ""; + }); + isInProgress = true; + }); + final cancel = () { + setState(() { + isInProgress = false; + }); + }; + idServer = idServer.trim(); + relayServer = relayServer.trim(); + apiServer = apiServer.trim(); + key = key.trim(); + + if (idServer.isNotEmpty) { + idServerMsg = translate( + await bind.mainTestIfValidServer(server: idServer)); + if (idServerMsg.isEmpty) { + oldOptions['custom-rendezvous-server'] = idServer; + } else { + cancel(); + return; + } + } else { + oldOptions['custom-rendezvous-server'] = ""; + } + + if (relayServer.isNotEmpty) { + relayServerMsg = translate( + await bind.mainTestIfValidServer(server: relayServer)); + if (relayServerMsg.isEmpty) { + oldOptions['relay-server'] = relayServer; + } else { + cancel(); + return; + } + } else { + oldOptions['relay-server'] = ""; + } + + if (apiServer.isNotEmpty) { + if (apiServer.startsWith('http://') || + apiServer.startsWith("https://")) { + oldOptions['api-server'] = apiServer; + return; + } else { + apiServerMsg = translate("invalid_http"); + cancel(); + return; + } + } else { + oldOptions['api-server'] = ""; + } + // ok + oldOptions['key'] = key; + await bind.mainSetOptions(json: jsonEncode(oldOptions)); + close(); + }, + child: Text(translate("OK"))), + ], + ); + }); +} + +void changeWhiteList() async { + Map oldOptions = jsonDecode(await bind.mainGetOptions()); + var newWhiteList = ((oldOptions['whitelist'] ?? "") as String).split(','); + var newWhiteListField = newWhiteList.join('\n'); + var msg = ""; + var isInProgress = false; + gFFI.dialogManager.show((setState, close) { + return CustomAlertDialog( + title: Text(translate("IP Whitelisting")), + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(translate("whitelist_sep")), + SizedBox( + height: 8.0, + ), + Row( + children: [ + Expanded( + child: TextField( + onChanged: (s) { + newWhiteListField = s; + }, + maxLines: null, + decoration: InputDecoration( + border: OutlineInputBorder(), + errorText: msg.isEmpty ? null : translate(msg), + ), + controller: TextEditingController(text: newWhiteListField), + ), + ), + ], + ), + SizedBox( + height: 4.0, + ), + Offstage(offstage: !isInProgress, child: LinearProgressIndicator()) + ], + ), + actions: [ + TextButton( + onPressed: () { + close(); + }, + child: Text(translate("Cancel"))), + TextButton( + onPressed: () async { + setState(() { + msg = ""; + isInProgress = true; + }); + newWhiteListField = newWhiteListField.trim(); + var newWhiteList = ""; + if (newWhiteListField.isEmpty) { + // pass + } else { + final ips = + newWhiteListField.trim().split(RegExp(r"[\s,;\n]+")); + // test ip + final ipMatch = RegExp(r"^\d+\.\d+\.\d+\.\d+$"); + for (final ip in ips) { + if (!ipMatch.hasMatch(ip)) { + msg = translate("Invalid IP") + " $ip"; + setState(() { + isInProgress = false; + }); + return; + } + } + newWhiteList = ips.join(','); + } + oldOptions['whitelist'] = newWhiteList; + await bind.mainSetOptions(json: jsonEncode(oldOptions)); + close(); + }, + child: Text(translate("OK"))), + ], + ); + }); +} + +void changeSocks5Proxy() async { + var socks = await bind.mainGetSocks(); + + String proxy = ""; + String proxyMsg = ""; + String username = ""; + String password = ""; + if (socks.length == 3) { + proxy = socks[0]; + username = socks[1]; + password = socks[2]; + } + + var isInProgress = false; + gFFI.dialogManager.show((setState, close) { + return CustomAlertDialog( + title: Text(translate("Socks5 Proxy")), + content: ConstrainedBox( + constraints: BoxConstraints(minWidth: 500), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: 8.0, + ), + Row( + children: [ + ConstrainedBox( + constraints: BoxConstraints(minWidth: 100), + child: Text("${translate('Hostname')}:") + .marginOnly(bottom: 16.0)), + SizedBox( + width: 24.0, + ), + Expanded( + child: TextField( + onChanged: (s) { + proxy = s; + }, + decoration: InputDecoration( + border: OutlineInputBorder(), + errorText: proxyMsg.isNotEmpty ? proxyMsg : null), + controller: TextEditingController(text: proxy), + ), + ), + ], + ), + SizedBox( + height: 8.0, + ), + Row( + children: [ + ConstrainedBox( + constraints: BoxConstraints(minWidth: 100), + child: Text("${translate('Username')}:") + .marginOnly(bottom: 16.0)), + SizedBox( + width: 24.0, + ), + Expanded( + child: TextField( + onChanged: (s) { + username = s; + }, + decoration: InputDecoration( + border: OutlineInputBorder(), + ), + controller: TextEditingController(text: username), + ), + ), + ], + ), + SizedBox( + height: 8.0, + ), + Row( + children: [ + ConstrainedBox( + constraints: BoxConstraints(minWidth: 100), + child: Text("${translate('Password')}:") + .marginOnly(bottom: 16.0)), + SizedBox( + width: 24.0, + ), + Expanded( + child: TextField( + onChanged: (s) { + password = s; + }, + decoration: InputDecoration( + border: OutlineInputBorder(), + ), + controller: TextEditingController(text: password), + ), + ), + ], + ), + SizedBox( + height: 8.0, + ), + Offstage(offstage: !isInProgress, child: LinearProgressIndicator()) + ], + ), + ), + actions: [ + TextButton( + onPressed: () { + close(); + }, + child: Text(translate("Cancel"))), + TextButton( + onPressed: () async { + setState(() { + proxyMsg = ""; + isInProgress = true; + }); + final cancel = () { + setState(() { + isInProgress = false; + }); + }; + proxy = proxy.trim(); + username = username.trim(); + password = password.trim(); + + if (proxy.isNotEmpty) { + proxyMsg = + translate(await bind.mainTestIfValidServer(server: proxy)); + if (proxyMsg.isEmpty) { + // ignore + } else { + cancel(); + return; + } + } + await bind.mainSetSocks( + proxy: proxy, username: username, password: password); + close(); + }, + child: Text(translate("OK"))), + ], + ); + }); +} + +//#endregion diff --git a/flutter/lib/desktop/pages/desktop_tab_page.dart b/flutter/lib/desktop/pages/desktop_tab_page.dart index 24611e439..65ba37e45 100644 --- a/flutter/lib/desktop/pages/desktop_tab_page.dart +++ b/flutter/lib/desktop/pages/desktop_tab_page.dart @@ -68,6 +68,6 @@ class _DesktopTabPageState extends State void onAddSetting() { DesktopTabBar.onAdd(this, tabController, tabs, _selected, - TabInfo(label: kTabLabelSettingPage, icon: Icons.settings)); + TabInfo(label: kTabLabelSettingPage, icon: Icons.build)); } } diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index 6ed048dd4..3da823c09 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -51,26 +51,24 @@ class ServerModel with ChangeNotifier { kUseBothPasswords ].indexOf(_verificationMethod); if (index < 0) { - _verificationMethod = kUseBothPasswords; + return kUseBothPasswords; } return _verificationMethod; } set verificationMethod(String method) { - _verificationMethod = method; bind.mainSetOption(key: "verification-method", value: method); } String get temporaryPasswordLength { final lengthIndex = ["6", "8", "10"].indexOf(_temporaryPasswordLength); if (lengthIndex < 0) { - _temporaryPasswordLength = "6"; + return "6"; } return _temporaryPasswordLength; } set temporaryPasswordLength(String length) { - _temporaryPasswordLength = length; bind.mainSetOption(key: "temporary-password-length", value: length); } diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 95cd1abd3..4d062ab11 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -23,9 +23,9 @@ use crate::ui_interface; use crate::ui_interface::{change_id, check_connect_status, is_ok_change_id}; use crate::ui_interface::{ discover, forget_password, get_api_server, get_app_name, get_async_job_status, - get_connect_status, get_fav, get_id, get_lan_peers, get_license, get_local_option, get_option, - get_options, get_peer, get_peer_option, get_socks, get_sound_inputs, get_uuid, get_version, - has_rendezvous_service, post_request, set_local_option, set_option, set_options, + get_connect_status, get_fav, get_id, get_lan_peers, get_langs, get_license, get_local_option, + get_option, get_options, get_peer, get_peer_option, get_socks, get_sound_inputs, get_uuid, + get_version, has_rendezvous_service, post_request, set_local_option, set_option, set_options, set_peer_option, set_permanent_password, set_socks, store_fav, test_if_valid_server, update_temporary_password, using_public_server, }; @@ -614,7 +614,7 @@ pub fn main_get_home_dir() -> String { } pub fn main_get_langs() -> String { - crate::lang::LANGS.to_string() + get_langs() } pub fn main_get_temporary_password() -> String { diff --git a/src/ui.rs b/src/ui.rs index 284c3c55d..6484abbe5 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -24,17 +24,18 @@ use crate::ipc; use crate::ui_interface::{ check_mouse_time, closing, create_shortcut, current_is_wayland, fix_login_wayland, forget_password, get_api_server, get_async_job_status, get_connect_status, get_error, get_fav, - get_icon, get_lan_peers, get_license, get_local_option, get_mouse_time, get_new_version, - get_option, get_options, get_peer, get_peer_option, get_recent_sessions, get_remote_id, - get_size, get_socks, get_software_ext, get_software_store_path, get_software_update_url, - get_uuid, get_version, goto_install, has_rendezvous_service, install_me, install_path, - is_can_screen_recording, is_installed, is_installed_daemon, is_installed_lower_version, - is_login_wayland, is_ok_change_id, is_process_trusted, is_rdp_service_open, is_share_rdp, - is_xfce, modify_default_login, new_remote, open_url, peer_has_password, permanent_password, - post_request, recent_sessions_updated, remove_peer, run_without_install, set_local_option, - set_option, set_options, set_peer_option, set_permanent_password, set_remote_id, set_share_rdp, - set_socks, show_run_without_install, store_fav, t, temporary_password, test_if_valid_server, - update_me, update_temporary_password, using_public_server, + get_icon, get_lan_peers, get_langs, get_license, get_local_option, get_mouse_time, + get_new_version, get_option, get_options, get_peer, get_peer_option, get_recent_sessions, + get_remote_id, get_size, get_socks, get_software_ext, get_software_store_path, + get_software_update_url, get_uuid, get_version, goto_install, has_rendezvous_service, + install_me, install_path, is_can_screen_recording, is_installed, is_installed_daemon, + is_installed_lower_version, is_login_wayland, is_ok_change_id, is_process_trusted, + is_rdp_service_open, is_share_rdp, is_xfce, modify_default_login, new_remote, open_url, + peer_has_password, permanent_password, post_request, recent_sessions_updated, remove_peer, + run_without_install, set_local_option, set_option, set_options, set_peer_option, + set_permanent_password, set_remote_id, set_share_rdp, set_socks, show_run_without_install, + store_fav, t, temporary_password, test_if_valid_server, update_me, update_temporary_password, + using_public_server, }; mod cm; @@ -547,7 +548,7 @@ impl UI { } fn get_langs(&self) -> String { - crate::lang::LANGS.to_string() + get_langs() } } diff --git a/src/ui_interface.rs b/src/ui_interface.rs index cdfd0edce..b882507c9 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -654,6 +654,10 @@ pub fn t(name: String) -> String { crate::client::translate(name) } +pub fn get_langs() -> String { + crate::lang::LANGS.to_string() +} + pub fn is_xfce() -> bool { crate::platform::is_xfce() } From ce86d5a5d42afcdd247eb18a95fe1f1c45528398 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Thu, 11 Aug 2022 18:59:26 +0800 Subject: [PATCH 0212/2015] add: cm page Signed-off-by: Kingtous --- flutter/lib/consts.dart | 1 + flutter/lib/desktop/pages/server_page.dart | 555 +++++++++++++++++++++ flutter/lib/main.dart | 16 +- src/core_main.rs | 12 +- 4 files changed, 582 insertions(+), 2 deletions(-) create mode 100644 flutter/lib/desktop/pages/server_page.dart diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 466b4b74a..7b61c5b48 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -2,6 +2,7 @@ const double kDesktopRemoteTabBarHeight = 48.0; const String kAppTypeMain = "main"; const String kAppTypeDesktopRemote = "remote"; const String kAppTypeDesktopFileTransfer = "file transfer"; +const String kAppTypeConnectionManager = "connection manager"; const String kTabLabelHomePage = "Home"; const String kTabLabelSettingPage = "Settings"; diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart new file mode 100644 index 000000000..7024e7258 --- /dev/null +++ b/flutter/lib/desktop/pages/server_page.dart @@ -0,0 +1,555 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/mobile/widgets/dialog.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:provider/provider.dart'; + +import '../../common.dart'; +import '../../mobile/pages/home_page.dart'; +import '../../models/platform_model.dart'; +import '../../models/server_model.dart'; + +class DesktopServerPage extends StatefulWidget implements PageShape { + @override + final title = translate("Share Screen"); + + @override + final icon = Icon(Icons.mobile_screen_share); + + @override + final appBarActions = [ + PopupMenuButton( + icon: Icon(Icons.more_vert), + itemBuilder: (context) { + return [ + PopupMenuItem( + child: Text(translate("Change ID")), + padding: EdgeInsets.symmetric(horizontal: 16.0), + value: "changeID", + enabled: false, + ), + PopupMenuItem( + child: Text(translate("Set permanent password")), + padding: EdgeInsets.symmetric(horizontal: 16.0), + value: "setPermanentPassword", + enabled: + gFFI.serverModel.verificationMethod != kUseTemporaryPassword, + ), + PopupMenuItem( + child: Text(translate("Set temporary password length")), + padding: EdgeInsets.symmetric(horizontal: 16.0), + value: "setTemporaryPasswordLength", + enabled: + gFFI.serverModel.verificationMethod != kUsePermanentPassword, + ), + const PopupMenuDivider(), + PopupMenuItem( + padding: EdgeInsets.symmetric(horizontal: 0.0), + value: kUseTemporaryPassword, + child: Container( + child: ListTile( + title: Text(translate("Use temporary password")), + trailing: Icon( + Icons.check, + color: gFFI.serverModel.verificationMethod == + kUseTemporaryPassword + ? null + : Color(0xFFFFFFFF), + ))), + ), + PopupMenuItem( + padding: EdgeInsets.symmetric(horizontal: 0.0), + value: kUsePermanentPassword, + child: ListTile( + title: Text(translate("Use permanent password")), + trailing: Icon( + Icons.check, + color: gFFI.serverModel.verificationMethod == + kUsePermanentPassword + ? null + : Color(0xFFFFFFFF), + )), + ), + PopupMenuItem( + padding: EdgeInsets.symmetric(horizontal: 0.0), + value: kUseBothPasswords, + child: ListTile( + title: Text(translate("Use both passwords")), + trailing: Icon( + Icons.check, + color: gFFI.serverModel.verificationMethod != + kUseTemporaryPassword && + gFFI.serverModel.verificationMethod != + kUsePermanentPassword + ? null + : Color(0xFFFFFFFF), + )), + ), + ]; + }, + onSelected: (value) { + if (value == "changeID") { + // TODO + } else if (value == "setPermanentPassword") { + setPermanentPasswordDialog(); + } else if (value == "setTemporaryPasswordLength") { + setTemporaryPasswordLengthDialog(); + } else if (value == kUsePermanentPassword || + value == kUseTemporaryPassword || + value == kUseBothPasswords) { + bind.mainSetOption(key: "verification-method", value: value); + gFFI.serverModel.updatePasswordModel(); + } + }) + ]; + + @override + State createState() => _DesktopServerPageState(); +} + +class _DesktopServerPageState extends State { + @override + void initState() { + super.initState(); + gFFI.serverModel.checkAndroidPermission(); + } + + @override + Widget build(BuildContext context) { + checkService(); + return ChangeNotifierProvider.value( + value: gFFI.serverModel, + child: Consumer( + builder: (context, serverModel, child) => SingleChildScrollView( + controller: gFFI.serverModel.controller, + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + ServerInfo(), + PermissionChecker(), + ConnectionManager(), + SizedBox.fromSize(size: Size(0, 15.0)), + ], + ), + ), + ))); + } +} + +void checkService() async { + gFFI.invokeMethod("check_service"); // jvm + // for Android 10/11,MANAGE_EXTERNAL_STORAGE permission from a system setting page + if (PermissionManager.isWaitingFile() && !gFFI.serverModel.fileOk) { + PermissionManager.complete("file", await PermissionManager.check("file")); + debugPrint("file permission finished"); + } +} + +class ServerInfo extends StatelessWidget { + final model = gFFI.serverModel; + final emptyController = TextEditingController(text: "-"); + + @override + Widget build(BuildContext context) { + final isPermanent = model.verificationMethod == kUsePermanentPassword; + return model.isStart + ? PaddingCard( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextFormField( + readOnly: true, + style: TextStyle( + fontSize: 25.0, + fontWeight: FontWeight.bold, + color: MyTheme.accent), + controller: model.serverId, + decoration: InputDecoration( + icon: const Icon(Icons.perm_identity), + labelText: translate("ID"), + labelStyle: TextStyle( + fontWeight: FontWeight.bold, color: MyTheme.accent50), + ), + onSaved: (String? value) {}, + ), + TextFormField( + readOnly: true, + style: TextStyle( + fontSize: 25.0, + fontWeight: FontWeight.bold, + color: MyTheme.accent), + controller: isPermanent ? emptyController : model.serverPasswd, + decoration: InputDecoration( + icon: const Icon(Icons.lock), + labelText: translate("Password"), + labelStyle: TextStyle( + fontWeight: FontWeight.bold, color: MyTheme.accent50), + suffix: isPermanent + ? null + : IconButton( + icon: const Icon(Icons.refresh), + onPressed: () => + bind.mainUpdateTemporaryPassword())), + onSaved: (String? value) {}, + ), + ], + )) + : PaddingCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Center( + child: Row( + children: [ + Icon(Icons.warning_amber_sharp, + color: Colors.redAccent, size: 24), + SizedBox(width: 10), + Expanded( + child: Text( + translate("Service is not running"), + style: TextStyle( + fontFamily: 'WorkSans', + fontWeight: FontWeight.bold, + fontSize: 18, + color: MyTheme.accent80, + ), + )) + ], + )), + SizedBox(height: 5), + Center( + child: Text( + translate("android_start_service_tip"), + style: TextStyle(fontSize: 12, color: MyTheme.darkGray), + )) + ], + )); + } +} + +class PermissionChecker extends StatefulWidget { + @override + _PermissionCheckerState createState() => _PermissionCheckerState(); +} + +class _PermissionCheckerState extends State { + @override + Widget build(BuildContext context) { + final serverModel = Provider.of(context); + final hasAudioPermission = androidVersion >= 30; + final status; + if (serverModel.connectStatus == -1) { + status = 'not_ready_status'; + } else if (serverModel.connectStatus == 0) { + status = 'connecting_status'; + } else { + status = 'Ready'; + } + return PaddingCard( + title: translate("Permissions"), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + PermissionRow(translate("Screen Capture"), serverModel.mediaOk, + serverModel.toggleService), + PermissionRow(translate("Input Control"), serverModel.inputOk, + serverModel.toggleInput), + PermissionRow(translate("Transfer File"), serverModel.fileOk, + serverModel.toggleFile), + hasAudioPermission + ? PermissionRow(translate("Audio Capture"), serverModel.audioOk, + serverModel.toggleAudio) + : Text( + "* ${translate("android_version_audio_tip")}", + style: TextStyle(color: MyTheme.darkGray), + ), + SizedBox(height: 8), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + flex: 0, + child: serverModel.mediaOk + ? ElevatedButton.icon( + style: ButtonStyle( + backgroundColor: + MaterialStateProperty.all(Colors.red)), + icon: Icon(Icons.stop), + onPressed: serverModel.toggleService, + label: Text(translate("Stop service"))) + : ElevatedButton.icon( + icon: Icon(Icons.play_arrow), + onPressed: serverModel.toggleService, + label: Text(translate("Start Service")))), + Expanded( + child: serverModel.mediaOk + ? Row( + children: [ + Expanded( + flex: 0, + child: Padding( + padding: + EdgeInsets.only(left: 20, right: 5), + child: Icon(Icons.circle, + color: serverModel.connectStatus > 0 + ? Colors.greenAccent + : Colors.deepOrangeAccent, + size: 10))), + Expanded( + child: Text(translate(status), + softWrap: true, + style: TextStyle( + fontSize: 14.0, + color: MyTheme.accent50))) + ], + ) + : SizedBox.shrink()) + ], + ), + ], + )); + } +} + +class PermissionRow extends StatelessWidget { + PermissionRow(this.name, this.isOk, this.onPressed); + + final String name; + final bool isOk; + final VoidCallback onPressed; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Expanded( + flex: 5, + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: Alignment.centerLeft, + child: Text(name, + style: + TextStyle(fontSize: 16.0, color: MyTheme.accent50)))), + Expanded( + flex: 2, + child: FittedBox( + fit: BoxFit.scaleDown, + child: Text(isOk ? translate("ON") : translate("OFF"), + style: TextStyle( + fontSize: 16.0, + color: isOk ? Colors.green : Colors.grey))), + ), + Expanded( + flex: 3, + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: Alignment.centerRight, + child: TextButton( + onPressed: onPressed, + child: Text( + translate(isOk ? "CLOSE" : "OPEN"), + style: TextStyle(fontWeight: FontWeight.bold), + )))), + ], + ); + } +} + +class ConnectionManager extends StatelessWidget { + @override + Widget build(BuildContext context) { + final serverModel = Provider.of(context); + return Column( + children: serverModel.clients.entries + .map((entry) => PaddingCard( + title: translate(entry.value.isFileTransfer + ? "File Connection" + : "Screen Connection"), + titleIcon: entry.value.isFileTransfer + ? Icons.folder_outlined + : Icons.mobile_screen_share, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded(child: clientInfo(entry.value)), + Expanded( + flex: -1, + child: entry.value.isFileTransfer || + !entry.value.authorized + ? SizedBox.shrink() + : IconButton( + onPressed: () { + gFFI.chatModel + .changeCurrentID(entry.value.id); + final bar = + navigationBarKey.currentWidget; + if (bar != null) { + bar as BottomNavigationBar; + bar.onTap!(1); + } + }, + icon: Icon( + Icons.chat, + color: MyTheme.accent80, + ))) + ], + ), + entry.value.authorized + ? SizedBox.shrink() + : Text( + translate("android_new_connection_tip"), + style: TextStyle(color: Colors.black54), + ), + entry.value.authorized + ? ElevatedButton.icon( + style: ButtonStyle( + backgroundColor: + MaterialStateProperty.all(Colors.red)), + icon: Icon(Icons.close), + onPressed: () { + bind.serverCloseConnection(connId: entry.key); + gFFI.invokeMethod( + "cancel_notification", entry.key); + }, + label: Text(translate("Close"))) + : Row(children: [ + TextButton( + child: Text(translate("Dismiss")), + onPressed: () { + serverModel.sendLoginResponse( + entry.value, false); + }), + SizedBox(width: 20), + ElevatedButton( + child: Text(translate("Accept")), + onPressed: () { + serverModel.sendLoginResponse( + entry.value, true); + }), + ]), + ], + ))) + .toList()); + } +} + +class PaddingCard extends StatelessWidget { + PaddingCard({required this.child, this.title, this.titleIcon}); + + final String? title; + final IconData? titleIcon; + final Widget child; + + @override + Widget build(BuildContext context) { + final children = [child]; + if (title != null) { + children.insert( + 0, + Padding( + padding: EdgeInsets.symmetric(vertical: 5.0), + child: Row( + children: [ + titleIcon != null + ? Padding( + padding: EdgeInsets.only(right: 10), + child: Icon(titleIcon, + color: MyTheme.accent80, size: 30)) + : SizedBox.shrink(), + Text( + title!, + style: TextStyle( + fontFamily: 'WorkSans', + fontWeight: FontWeight.bold, + fontSize: 20, + color: MyTheme.accent80, + ), + ) + ], + ))); + } + return Container( + width: double.maxFinite, + child: Card( + margin: EdgeInsets.fromLTRB(15.0, 15.0, 15.0, 0), + child: Padding( + padding: EdgeInsets.symmetric(vertical: 15.0, horizontal: 30.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: children, + ), + ), + )); + } +} + +Widget clientInfo(Client client) { + return Padding( + padding: EdgeInsets.symmetric(vertical: 8), + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Row( + children: [ + Expanded( + flex: -1, + child: Padding( + padding: EdgeInsets.only(right: 12), + child: CircleAvatar( + child: Text(client.name[0]), + backgroundColor: MyTheme.border))), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(client.name, + style: TextStyle(color: MyTheme.idColor, fontSize: 18)), + SizedBox(width: 8), + Text(client.peerId, + style: TextStyle(color: MyTheme.idColor, fontSize: 10)) + ])) + ], + ), + ])); +} + +void toAndroidChannelInit() { + gFFI.setMethodCallHandler((method, arguments) { + debugPrint("flutter got android msg,$method,$arguments"); + try { + switch (method) { + case "start_capture": + { + SmartDialog.dismiss(); + gFFI.serverModel.updateClientState(); + break; + } + case "on_state_changed": + { + var name = arguments["name"] as String; + var value = arguments["value"] as String == "true"; + debugPrint("from jvm:on_state_changed,$name:$value"); + gFFI.serverModel.changeStatue(name, value); + break; + } + case "on_android_permission_result": + { + var type = arguments["type"] as String; + var result = arguments["result"] as bool; + PermissionManager.complete(type, result); + break; + } + case "on_media_projection_canceled": + { + gFFI.serverModel.stopService(); + break; + } + } + } catch (e) { + debugPrint("MethodCallHandler err:$e"); + } + return ""; + }); +} diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index dd6ccd31d..2d738a383 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -1,12 +1,12 @@ import 'dart:convert'; import 'package:flutter/material.dart'; +import 'package:flutter_hbb/desktop/pages/cm.dart'; import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart'; import 'package:flutter_hbb/desktop/screen/desktop_file_transfer_screen.dart'; import 'package:flutter_hbb/desktop/screen/desktop_remote_screen.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:get/get.dart'; -import 'package:get/route_manager.dart'; import 'package:provider/provider.dart'; import 'package:window_manager/window_manager.dart'; @@ -23,6 +23,7 @@ int? windowId; Future main(List args) async { WidgetsFlutterBinding.ensureInitialized(); + print("launch args: $args"); if (!isDesktop) { runMainApp(false); @@ -47,6 +48,9 @@ Future main(List args) async { default: break; } + } else if (args.isNotEmpty && args.first == '--cm') { + await windowManager.ensureInitialized(); + runConnectionManagerScreen(); } else { await windowManager.ensureInitialized(); windowManager.setPreventClose(true); @@ -111,6 +115,16 @@ void runFileTransferScreen(Map argument) async { ])); } +void runConnectionManagerScreen() async { + await initEnv(kAppTypeConnectionManager); + windowManager.setAlwaysOnTop(true); + windowManager.setSize(Size(400, 600)).then((_) { + windowManager.setAlignment(Alignment.topRight); + }); + runApp( + GetMaterialApp(theme: getCurrentTheme(), home: ConnectionManagerPage())); +} + class App extends StatelessWidget { @override Widget build(BuildContext context) { diff --git a/src/core_main.rs b/src/core_main.rs index c50bb0835..4e95f70ae 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -1,3 +1,7 @@ +use hbb_common::log; + +use crate::start_os_service; + /// Main entry of the RustDesk Core. /// Return true if the app should continue running with UI(possibly Flutter), false if the app should exit. pub fn core_main() -> bool { @@ -5,7 +9,13 @@ pub fn core_main() -> bool { // TODO: implement core_main() if args.len() > 1 { if args[1] == "--cm" { - // For test purpose only, this should stop any new window from popping up when a new connection is established. + // call connection manager to establish connections + // meanwhile, return true to call flutter window to show control panel + return true; + } + if args[1] == "--service" { + log::info!("start --service"); + start_os_service(); return false; } } From 07e54a0614c8ad3f419b67237a4b41c042bf1f0b Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 15 Aug 2022 12:35:10 +0800 Subject: [PATCH 0213/2015] add: connection manager page Signed-off-by: Kingtous --- flutter/lib/desktop/pages/server_page.dart | 273 +-------------------- flutter/lib/main.dart | 12 +- 2 files changed, 8 insertions(+), 277 deletions(-) diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index 7024e7258..bf80bfbe7 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:flutter_hbb/mobile/widgets/dialog.dart'; -import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +// import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:provider/provider.dart'; import '../../common.dart'; @@ -90,9 +89,9 @@ class DesktopServerPage extends StatefulWidget implements PageShape { if (value == "changeID") { // TODO } else if (value == "setPermanentPassword") { - setPermanentPasswordDialog(); + // setPermanentPasswordDialog(); } else if (value == "setTemporaryPasswordLength") { - setTemporaryPasswordLengthDialog(); + // setTemporaryPasswordLengthDialog(); } else if (value == kUsePermanentPassword || value == kUseTemporaryPassword || value == kUseBothPasswords) { @@ -107,15 +106,9 @@ class DesktopServerPage extends StatefulWidget implements PageShape { } class _DesktopServerPageState extends State { - @override - void initState() { - super.initState(); - gFFI.serverModel.checkAndroidPermission(); - } @override Widget build(BuildContext context) { - checkService(); return ChangeNotifierProvider.value( value: gFFI.serverModel, child: Consumer( @@ -125,8 +118,6 @@ class _DesktopServerPageState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.start, children: [ - ServerInfo(), - PermissionChecker(), ConnectionManager(), SizedBox.fromSize(size: Size(0, 15.0)), ], @@ -136,225 +127,6 @@ class _DesktopServerPageState extends State { } } -void checkService() async { - gFFI.invokeMethod("check_service"); // jvm - // for Android 10/11,MANAGE_EXTERNAL_STORAGE permission from a system setting page - if (PermissionManager.isWaitingFile() && !gFFI.serverModel.fileOk) { - PermissionManager.complete("file", await PermissionManager.check("file")); - debugPrint("file permission finished"); - } -} - -class ServerInfo extends StatelessWidget { - final model = gFFI.serverModel; - final emptyController = TextEditingController(text: "-"); - - @override - Widget build(BuildContext context) { - final isPermanent = model.verificationMethod == kUsePermanentPassword; - return model.isStart - ? PaddingCard( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TextFormField( - readOnly: true, - style: TextStyle( - fontSize: 25.0, - fontWeight: FontWeight.bold, - color: MyTheme.accent), - controller: model.serverId, - decoration: InputDecoration( - icon: const Icon(Icons.perm_identity), - labelText: translate("ID"), - labelStyle: TextStyle( - fontWeight: FontWeight.bold, color: MyTheme.accent50), - ), - onSaved: (String? value) {}, - ), - TextFormField( - readOnly: true, - style: TextStyle( - fontSize: 25.0, - fontWeight: FontWeight.bold, - color: MyTheme.accent), - controller: isPermanent ? emptyController : model.serverPasswd, - decoration: InputDecoration( - icon: const Icon(Icons.lock), - labelText: translate("Password"), - labelStyle: TextStyle( - fontWeight: FontWeight.bold, color: MyTheme.accent50), - suffix: isPermanent - ? null - : IconButton( - icon: const Icon(Icons.refresh), - onPressed: () => - bind.mainUpdateTemporaryPassword())), - onSaved: (String? value) {}, - ), - ], - )) - : PaddingCard( - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Center( - child: Row( - children: [ - Icon(Icons.warning_amber_sharp, - color: Colors.redAccent, size: 24), - SizedBox(width: 10), - Expanded( - child: Text( - translate("Service is not running"), - style: TextStyle( - fontFamily: 'WorkSans', - fontWeight: FontWeight.bold, - fontSize: 18, - color: MyTheme.accent80, - ), - )) - ], - )), - SizedBox(height: 5), - Center( - child: Text( - translate("android_start_service_tip"), - style: TextStyle(fontSize: 12, color: MyTheme.darkGray), - )) - ], - )); - } -} - -class PermissionChecker extends StatefulWidget { - @override - _PermissionCheckerState createState() => _PermissionCheckerState(); -} - -class _PermissionCheckerState extends State { - @override - Widget build(BuildContext context) { - final serverModel = Provider.of(context); - final hasAudioPermission = androidVersion >= 30; - final status; - if (serverModel.connectStatus == -1) { - status = 'not_ready_status'; - } else if (serverModel.connectStatus == 0) { - status = 'connecting_status'; - } else { - status = 'Ready'; - } - return PaddingCard( - title: translate("Permissions"), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - PermissionRow(translate("Screen Capture"), serverModel.mediaOk, - serverModel.toggleService), - PermissionRow(translate("Input Control"), serverModel.inputOk, - serverModel.toggleInput), - PermissionRow(translate("Transfer File"), serverModel.fileOk, - serverModel.toggleFile), - hasAudioPermission - ? PermissionRow(translate("Audio Capture"), serverModel.audioOk, - serverModel.toggleAudio) - : Text( - "* ${translate("android_version_audio_tip")}", - style: TextStyle(color: MyTheme.darkGray), - ), - SizedBox(height: 8), - Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - flex: 0, - child: serverModel.mediaOk - ? ElevatedButton.icon( - style: ButtonStyle( - backgroundColor: - MaterialStateProperty.all(Colors.red)), - icon: Icon(Icons.stop), - onPressed: serverModel.toggleService, - label: Text(translate("Stop service"))) - : ElevatedButton.icon( - icon: Icon(Icons.play_arrow), - onPressed: serverModel.toggleService, - label: Text(translate("Start Service")))), - Expanded( - child: serverModel.mediaOk - ? Row( - children: [ - Expanded( - flex: 0, - child: Padding( - padding: - EdgeInsets.only(left: 20, right: 5), - child: Icon(Icons.circle, - color: serverModel.connectStatus > 0 - ? Colors.greenAccent - : Colors.deepOrangeAccent, - size: 10))), - Expanded( - child: Text(translate(status), - softWrap: true, - style: TextStyle( - fontSize: 14.0, - color: MyTheme.accent50))) - ], - ) - : SizedBox.shrink()) - ], - ), - ], - )); - } -} - -class PermissionRow extends StatelessWidget { - PermissionRow(this.name, this.isOk, this.onPressed); - - final String name; - final bool isOk; - final VoidCallback onPressed; - - @override - Widget build(BuildContext context) { - return Row( - children: [ - Expanded( - flex: 5, - child: FittedBox( - fit: BoxFit.scaleDown, - alignment: Alignment.centerLeft, - child: Text(name, - style: - TextStyle(fontSize: 16.0, color: MyTheme.accent50)))), - Expanded( - flex: 2, - child: FittedBox( - fit: BoxFit.scaleDown, - child: Text(isOk ? translate("ON") : translate("OFF"), - style: TextStyle( - fontSize: 16.0, - color: isOk ? Colors.green : Colors.grey))), - ), - Expanded( - flex: 3, - child: FittedBox( - fit: BoxFit.scaleDown, - alignment: Alignment.centerRight, - child: TextButton( - onPressed: onPressed, - child: Text( - translate(isOk ? "CLOSE" : "OPEN"), - style: TextStyle(fontWeight: FontWeight.bold), - )))), - ], - ); - } -} - class ConnectionManager extends StatelessWidget { @override Widget build(BuildContext context) { @@ -514,42 +286,3 @@ Widget clientInfo(Client client) { ), ])); } - -void toAndroidChannelInit() { - gFFI.setMethodCallHandler((method, arguments) { - debugPrint("flutter got android msg,$method,$arguments"); - try { - switch (method) { - case "start_capture": - { - SmartDialog.dismiss(); - gFFI.serverModel.updateClientState(); - break; - } - case "on_state_changed": - { - var name = arguments["name"] as String; - var value = arguments["value"] as String == "true"; - debugPrint("from jvm:on_state_changed,$name:$value"); - gFFI.serverModel.changeStatue(name, value); - break; - } - case "on_android_permission_result": - { - var type = arguments["type"] as String; - var result = arguments["result"] as bool; - PermissionManager.complete(type, result); - break; - } - case "on_media_projection_canceled": - { - gFFI.serverModel.stopService(); - break; - } - } - } catch (e) { - debugPrint("MethodCallHandler err:$e"); - } - return ""; - }); -} diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 2d738a383..d8586baad 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -1,8 +1,8 @@ import 'dart:convert'; import 'package:flutter/material.dart'; -import 'package:flutter_hbb/desktop/pages/cm.dart'; import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart'; +import 'package:flutter_hbb/desktop/pages/server_page.dart'; import 'package:flutter_hbb/desktop/screen/desktop_file_transfer_screen.dart'; import 'package:flutter_hbb/desktop/screen/desktop_remote_screen.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; @@ -117,12 +117,10 @@ void runFileTransferScreen(Map argument) async { void runConnectionManagerScreen() async { await initEnv(kAppTypeConnectionManager); - windowManager.setAlwaysOnTop(true); - windowManager.setSize(Size(400, 600)).then((_) { - windowManager.setAlignment(Alignment.topRight); - }); - runApp( - GetMaterialApp(theme: getCurrentTheme(), home: ConnectionManagerPage())); + await windowManager.setAlwaysOnTop(true); + await windowManager.setSize(Size(400, 600)); + await windowManager.setAlignment(Alignment.topRight); + runApp(GetMaterialApp(theme: getCurrentTheme(), home: DesktopServerPage())); } class App extends StatelessWidget { From a6e2ad86397397b90700f014c18c61dcc2dcc880 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 15 Aug 2022 14:04:08 +0800 Subject: [PATCH 0214/2015] add: fullscreen for sub windows Signed-off-by: Kingtous --- flutter/pubspec.lock | 4 ++-- flutter/pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index fcefcca82..695443f21 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -243,8 +243,8 @@ packages: dependency: "direct main" description: path: "." - ref: c53879e9ce4ed038af393a02bf2c7084ad4b53aa - resolved-ref: c53879e9ce4ed038af393a02bf2c7084ad4b53aa + ref: "2b1176d53f195cc55e8d37151bb3d9f6bd52fad3" + resolved-ref: "2b1176d53f195cc55e8d37151bb3d9f6bd52fad3" url: "https://github.com/Kingtous/rustdesk_desktop_multi_window" source: git version: "0.1.0" diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index b8b9580fb..a911903f8 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -62,7 +62,7 @@ dependencies: desktop_multi_window: git: url: https://github.com/Kingtous/rustdesk_desktop_multi_window - ref: c53879e9ce4ed038af393a02bf2c7084ad4b53aa + ref: 2b1176d53f195cc55e8d37151bb3d9f6bd52fad3 # bitsdojo_window: ^0.1.2 freezed_annotation: ^2.0.3 tray_manager: 0.1.7 From da4c218ea3233190a0e7a18086f87df72b957c81 Mon Sep 17 00:00:00 2001 From: csf Date: Mon, 15 Aug 2022 14:39:31 +0800 Subject: [PATCH 0215/2015] add showToast & dialog clickMaskDismiss --- flutter/lib/common.dart | 80 ++++++++++++++----- .../lib/desktop/pages/desktop_home_page.dart | 5 +- flutter/lib/desktop/pages/remote_page.dart | 2 +- .../lib/mobile/pages/file_manager_page.dart | 2 +- flutter/lib/mobile/pages/remote_page.dart | 2 +- flutter/lib/mobile/pages/scan_page.dart | 8 +- flutter/lib/mobile/pages/settings_page.dart | 2 +- flutter/lib/mobile/widgets/dialog.dart | 13 ++- flutter/lib/models/model.dart | 4 +- 9 files changed, 79 insertions(+), 39 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index aa5666e86..dd48cefea 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -136,7 +136,6 @@ class OverlayDialogManager { BackButtonInterceptor.removeByName(tag); } - // TODO clickMaskDismiss Future show(DialogBuilder builder, {bool clickMaskDismiss = false, bool backDismiss = false, @@ -168,10 +167,22 @@ class OverlayDialogManager { BackButtonInterceptor.removeByName(_tag); }; dialog.entry = OverlayEntry(builder: (_) { - return Container( - color: Colors.transparent, - child: StatefulBuilder( - builder: (_, setState) => builder(setState, close))); + bool innerClicked = false; + return Listener( + onPointerUp: (_) { + if (!innerClicked && clickMaskDismiss) { + close(); + } + innerClicked = false; + }, + child: Container( + color: Colors.black12, + child: StatefulBuilder(builder: (context, setState) { + return Listener( + onPointerUp: (_) => innerClicked = true, + child: builder(setState, close), + ); + }))); }); overlayState.insert(dialog.entry!); BackButtonInterceptor.add((stopDefaultButtonEvent, routeInfo) { @@ -184,7 +195,9 @@ class OverlayDialogManager { } void showLoading(String text, - {bool clickMaskDismiss = false, bool cancelToClose = false}) { + {bool clickMaskDismiss = false, + bool showCancel = true, + VoidCallback? onCancel}) { show((setState, close) => CustomAlertDialog( content: Container( color: MyTheme.white, @@ -200,21 +213,52 @@ class OverlayDialogManager { child: Text(translate(text), style: TextStyle(fontSize: 15))), SizedBox(height: 20), - Center( - child: TextButton( - style: flatButtonStyle, - onPressed: () { - dismissAll(); - if (cancelToClose) backToHomePage(); - }, - child: Text(translate('Cancel'), - style: TextStyle(color: MyTheme.accent)))) + Offstage( + offstage: !showCancel, + child: Center( + child: TextButton( + style: flatButtonStyle, + onPressed: () { + dismissAll(); + if (onCancel != null) { + onCancel(); + } + }, + child: Text(translate('Cancel'), + style: TextStyle(color: MyTheme.accent))))) ])))); } +} - void showToast(String text) { - // TODO - } +void showToast(String text, {Duration timeout = const Duration(seconds: 2)}) { + final overlayState = globalKey.currentState?.overlay; + if (overlayState == null) return; + final entry = OverlayEntry(builder: (_) { + return IgnorePointer( + child: Align( + alignment: Alignment(0.0, 0.8), + child: Container( + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.6), + borderRadius: BorderRadius.all( + Radius.circular(20), + ), + ), + padding: EdgeInsets.symmetric(horizontal: 20, vertical: 5), + child: Text( + text, + style: TextStyle( + decoration: TextDecoration.none, + fontWeight: FontWeight.w300, + fontSize: 18, + color: Colors.white), + ), + ))); + }); + overlayState.insert(entry); + Future.delayed(timeout, () { + entry.remove(); + }); } class CustomAlertDialog extends StatelessWidget { diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 627c5b2e4..407d38958 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:convert'; import 'dart:io'; import 'package:flutter/material.dart' hide MenuItem; @@ -120,7 +119,7 @@ class _DesktopHomePageState extends State onDoubleTap: () { Clipboard.setData( ClipboardData(text: model.serverId.text)); - gFFI.dialogManager.showToast(translate("Copied")); + showToast(translate("Copied")); }, child: TextFormField( controller: model.serverId, @@ -257,7 +256,7 @@ class _DesktopHomePageState extends State kUsePermanentPassword) { Clipboard.setData( ClipboardData(text: model.serverPasswd.text)); - gFFI.dialogManager.showToast(translate("Copied")); + showToast(translate("Copied")); } }, child: TextFormField( diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index ceeb96049..02060dee5 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 WidgetsBinding.instance.addPostFrameCallback((_) { SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); _ffi.dialogManager - .showLoading(translate('Connecting...'), cancelToClose: true); + .showLoading(translate('Connecting...'), onCancel: backToHomePage); _interval = Timer.periodic(Duration(milliseconds: 30), (timer) => interval()); }); diff --git a/flutter/lib/mobile/pages/file_manager_page.dart b/flutter/lib/mobile/pages/file_manager_page.dart index 9c8fd92c4..c361e7b7c 100644 --- a/flutter/lib/mobile/pages/file_manager_page.dart +++ b/flutter/lib/mobile/pages/file_manager_page.dart @@ -29,7 +29,7 @@ class _FileManagerPageState extends State { gFFI.connect(widget.id, isFileTransfer: true); WidgetsBinding.instance.addPostFrameCallback((_) { gFFI.dialogManager - .showLoading(translate('Connecting...'), cancelToClose: true); + .showLoading(translate('Connecting...'), onCancel: backToHomePage); }); gFFI.ffiModel.updateEventListener(widget.id); Wakelock.enable(); diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 3e826705f..d64c83707 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -51,7 +51,7 @@ class _RemotePageState extends State { WidgetsBinding.instance.addPostFrameCallback((_) { SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); gFFI.dialogManager - .showLoading(translate('Connecting...'), cancelToClose: true); + .showLoading(translate('Connecting...'), onCancel: backToHomePage); _interval = Timer.periodic(Duration(milliseconds: 30), (timer) => interval()); }); diff --git a/flutter/lib/mobile/pages/scan_page.dart b/flutter/lib/mobile/pages/scan_page.dart index 4325d0570..9f6c36ca8 100644 --- a/flutter/lib/mobile/pages/scan_page.dart +++ b/flutter/lib/mobile/pages/scan_page.dart @@ -63,7 +63,7 @@ class _ScanPageState extends State { var result = reader.decode(bitmap); showServerSettingFromQr(result.text); } catch (e) { - gFFI.dialogManager.showToast('No QR code found'); + showToast('No QR code found'); } } }), @@ -121,7 +121,7 @@ class _ScanPageState extends State { void _onPermissionSet(BuildContext context, QRViewController ctrl, bool p) { if (!p) { - gFFI.dialogManager.showToast('No permisssion'); + showToast('No permission'); } } @@ -135,7 +135,7 @@ class _ScanPageState extends State { backToHomePage(); await controller?.pauseCamera(); if (!data.startsWith('config=')) { - gFFI.dialogManager.showToast('Invalid QR code'); + showToast('Invalid QR code'); return; } try { @@ -147,7 +147,7 @@ class _ScanPageState extends State { showServerSettingsWithValue(host, '', key, api, gFFI.dialogManager); }); } catch (e) { - gFFI.dialogManager.showToast('Invalid QR code'); + showToast('Invalid QR code'); } } } diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index 3a1f8b352..be8403427 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -363,7 +363,7 @@ void logout(OverlayDialogManager dialogManager) async { }, body: json.encode(body)); } catch (e) { - dialogManager.showToast('Failed to access $url'); + showToast('Failed to access $url'); } resetToken(); } diff --git a/flutter/lib/mobile/widgets/dialog.dart b/flutter/lib/mobile/widgets/dialog.dart index 075fa5bd9..6f3428805 100644 --- a/flutter/lib/mobile/widgets/dialog.dart +++ b/flutter/lib/mobile/widgets/dialog.dart @@ -8,15 +8,12 @@ void clientClose(OverlayDialogManager dialogManager) { msgBox('', 'Close', 'Are you sure to close the connection?', dialogManager); } -const SEC1 = Duration(seconds: 1); -void showSuccess({Duration duration = SEC1}) { - // TODO - // showToast(translate("Successful"), duration: SEC1); +void showSuccess() { + showToast(translate("Successful")); } -void showError({Duration duration = SEC1}) { - // TODO - // showToast(translate("Error"), duration: SEC1); +void showError() { + showToast(translate("Error")); } void setPermanentPasswordDialog(OverlayDialogManager dialogManager) async { @@ -174,7 +171,7 @@ void enterPasswordDialog(String id, OverlayDialogManager dialogManager) async { gFFI.login(id, text, remember); close(); dialogManager.showLoading(translate('Logging in...'), - cancelToClose: true); + onCancel: backToHomePage); }, child: Text(translate('OK')), ), diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index c297141de..c4b10b377 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -287,7 +287,7 @@ class FfiModel with ChangeNotifier { bind.sessionReconnect(id: id); clearPermissions(); dialogManager.showLoading(translate('Connecting...'), - cancelToClose: true); + onCancel: backToHomePage); }); _reconnects *= 2; } else { @@ -335,7 +335,7 @@ class FfiModel with ChangeNotifier { if (displays.length > 0) { parent.target?.dialogManager.showLoading( translate('Connected, waiting for image...'), - cancelToClose: true); + onCancel: backToHomePage); _waitForImage = true; _reconnects = 1; } From 5b3ef29d757a5bc3484af84ecbe8b59af969237e Mon Sep 17 00:00:00 2001 From: csf Date: Mon, 15 Aug 2022 14:43:08 +0800 Subject: [PATCH 0216/2015] fix mobile showSuccess & update pubspec.lock --- flutter/lib/mobile/widgets/dialog.dart | 2 + flutter/pubspec.lock | 354 ++++++++++++------------- 2 files changed, 179 insertions(+), 177 deletions(-) diff --git a/flutter/lib/mobile/widgets/dialog.dart b/flutter/lib/mobile/widgets/dialog.dart index 6f3428805..098f8d912 100644 --- a/flutter/lib/mobile/widgets/dialog.dart +++ b/flutter/lib/mobile/widgets/dialog.dart @@ -84,8 +84,10 @@ void setPermanentPasswordDialog(OverlayDialogManager dialogManager) async { close(); dialogManager.showLoading(translate("Waiting")); if (await gFFI.serverModel.setPermanentPassword(p0.text)) { + dialogManager.dismissAll(); showSuccess(); } else { + dialogManager.dismissAll(); showError(); } } diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 695443f21..fe7359bf5 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -5,238 +5,238 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "46.0.0" after_layout: dependency: transitive description: name: after_layout - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.2.0" analyzer: dependency: transitive description: name: analyzer - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "4.6.0" animations: dependency: transitive description: name: animations - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.3" archive: dependency: transitive description: name: archive - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.3.1" args: dependency: transitive description: name: args - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.3.1" async: dependency: transitive description: name: async - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.8.2" back_button_interceptor: dependency: "direct main" description: name: back_button_interceptor - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "6.0.1" boolean_selector: dependency: transitive description: name: boolean_selector - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.0" build: dependency: transitive description: name: build - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.3.0" build_config: dependency: transitive description: name: build_config - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.1.0" build_daemon: dependency: transitive description: name: build_daemon - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.1.0" build_resolvers: dependency: transitive description: name: build_resolvers - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.9" build_runner: dependency: "direct dev" description: name: build_runner - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.2.0" build_runner_core: dependency: transitive description: name: build_runner_core - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "7.2.3" built_collection: dependency: transitive description: name: built_collection - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "5.1.1" built_value: dependency: transitive description: name: built_value - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "8.4.0" cached_network_image: dependency: transitive description: name: cached_network_image - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.2.1" cached_network_image_platform_interface: dependency: transitive description: name: cached_network_image_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.0" cached_network_image_web: dependency: transitive description: name: cached_network_image_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.1" characters: dependency: transitive description: name: characters - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.2.0" charcode: dependency: transitive description: name: charcode - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.3.1" checked_yaml: dependency: transitive description: name: checked_yaml - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.1" clock: dependency: transitive description: name: clock - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.1.0" code_builder: dependency: transitive description: name: code_builder - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "4.2.0" collection: dependency: transitive description: name: collection - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.16.0" contextmenu: dependency: "direct main" description: name: contextmenu - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.0" convert: dependency: transitive description: name: convert - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.2" cross_file: dependency: transitive description: name: cross_file - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.3.3+1" crypto: dependency: transitive description: name: crypto - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.2" csslib: dependency: transitive description: name: csslib - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.17.2" cupertino_icons: dependency: "direct main" description: name: cupertino_icons - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.5" dart_style: dependency: transitive description: name: dart_style - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.2.3" dash_chat_2: dependency: "direct main" description: name: dash_chat_2 - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.0.12" desktop_multi_window: @@ -252,133 +252,133 @@ packages: dependency: "direct main" description: name: device_info_plus - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "4.1.0" + version: "4.1.2" device_info_plus_linux: dependency: transitive description: name: device_info_plus_linux - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" + version: "3.0.0" device_info_plus_macos: dependency: transitive description: name: device_info_plus_macos - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "2.2.3" + version: "3.0.0" device_info_plus_platform_interface: dependency: transitive description: name: device_info_plus_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "2.6.0" + version: "3.0.0" device_info_plus_web: dependency: transitive description: name: device_info_plus_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "3.0.0" device_info_plus_windows: dependency: transitive description: name: device_info_plus_windows - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "3.0.3" + version: "4.0.0" draggable_float_widget: dependency: "direct main" description: name: draggable_float_widget - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.0.2" event_bus: dependency: transitive description: name: event_bus - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.0" external_path: dependency: "direct main" description: name: external_path - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.1" fake_async: dependency: transitive description: name: fake_async - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.3.0" ffi: dependency: "direct main" description: name: ffi - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.1" file: dependency: transitive description: name: file - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "6.1.2" firebase_analytics: dependency: "direct main" description: name: firebase_analytics - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "9.3.1" firebase_analytics_platform_interface: dependency: transitive description: name: firebase_analytics_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.3.1" firebase_analytics_web: dependency: transitive description: name: firebase_analytics_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.4.2+1" firebase_core: dependency: transitive description: name: firebase_core - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.20.1" firebase_core_platform_interface: dependency: transitive description: name: firebase_core_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "4.5.0" firebase_core_web: dependency: transitive description: name: firebase_core_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.7.1" fixnum: dependency: transitive description: name: fixnum - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.1" flutter: @@ -390,42 +390,42 @@ packages: dependency: transitive description: name: flutter_blurhash - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.7.0" flutter_breadcrumb: dependency: "direct main" description: name: flutter_breadcrumb - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.1" flutter_cache_manager: dependency: transitive description: name: flutter_cache_manager - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.3.0" flutter_launcher_icons: dependency: "direct dev" description: name: flutter_launcher_icons - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.9.3" flutter_parsed_text: dependency: transitive description: name: flutter_parsed_text - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.2.1" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.7" flutter_rust_bridge: @@ -451,476 +451,476 @@ packages: dependency: "direct dev" description: name: freezed - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.0+1" freezed_annotation: dependency: "direct main" description: name: freezed_annotation - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.0" frontend_server_client: dependency: transitive description: name: frontend_server_client - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.3" get: dependency: "direct main" description: name: get - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "4.6.5" glob: dependency: transitive description: name: glob - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.0" graphs: dependency: transitive description: name: graphs - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.0" html: dependency: transitive description: name: html - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.15.0" http: dependency: "direct main" description: name: http - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.13.5" http_multi_server: dependency: transitive description: name: http_multi_server - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.2.1" http_parser: dependency: transitive description: name: http_parser - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "4.0.1" image: dependency: "direct main" description: name: image - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.2.0" image_picker: dependency: "direct main" description: name: image_picker - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.8.5+3" image_picker_android: dependency: transitive description: name: image_picker_android - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.8.5+2" image_picker_for_web: dependency: transitive description: name: image_picker_for_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.8" image_picker_ios: dependency: transitive description: name: image_picker_ios - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.8.5+6" image_picker_platform_interface: dependency: transitive description: name: image_picker_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.6.1" intl: dependency: transitive description: name: intl - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.17.0" io: dependency: transitive description: name: io - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.3" js: dependency: transitive description: name: js - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.6.4" json_annotation: dependency: transitive description: name: json_annotation - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "4.6.0" logging: dependency: transitive description: name: logging - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.2" matcher: dependency: transitive description: name: matcher - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.12.11" material_color_utilities: dependency: transitive description: name: material_color_utilities - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.1.4" menu_base: dependency: transitive description: name: menu_base - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.1.1" meta: dependency: transitive description: name: meta - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.7.0" mime: dependency: transitive description: name: mime - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.2" nested: dependency: transitive description: name: nested - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.0" octo_image: dependency: transitive description: name: octo_image - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.2" package_config: dependency: transitive description: name: package_config - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.0" package_info_plus: dependency: "direct main" description: name: package_info_plus - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.4.3+1" package_info_plus_linux: dependency: transitive description: name: package_info_plus_linux - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.5" package_info_plus_macos: dependency: transitive description: name: package_info_plus_macos - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.3.0" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.2" package_info_plus_web: dependency: transitive description: name: package_info_plus_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.5" package_info_plus_windows: dependency: transitive description: name: package_info_plus_windows - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.0" path: dependency: transitive description: name: path - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.8.1" path_provider: dependency: "direct main" description: name: path_provider - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.11" path_provider_android: dependency: transitive description: name: path_provider_android - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.19" path_provider_ios: dependency: transitive description: name: path_provider_ios - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.11" path_provider_linux: dependency: transitive description: name: path_provider_linux - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.7" path_provider_macos: dependency: transitive description: name: path_provider_macos - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.6" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.4" path_provider_windows: dependency: transitive description: name: path_provider_windows - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.2" pedantic: dependency: transitive description: name: pedantic - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.11.1" petitparser: dependency: transitive description: name: petitparser - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "5.0.0" platform: dependency: transitive description: name: platform - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.1.0" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.2" pool: dependency: transitive description: name: pool - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.5.1" process: dependency: transitive description: name: process - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "4.2.4" provider: dependency: "direct main" description: name: provider - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "6.0.3" pub_semver: dependency: transitive description: name: pub_semver - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.1" pubspec_parse: dependency: transitive description: name: pubspec_parse - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.2.0" qr_code_scanner: dependency: "direct main" description: name: qr_code_scanner - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.0" quiver: dependency: transitive description: name: quiver - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.1.0" rxdart: dependency: transitive description: name: rxdart - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.27.5" screen_retriever: dependency: transitive description: name: screen_retriever - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.1.2" settings_ui: dependency: "direct main" description: name: settings_ui - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.2" shared_preferences: dependency: "direct main" description: name: shared_preferences - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.15" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.12" shared_preferences_ios: dependency: transitive description: name: shared_preferences_ios - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.1" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.1" shared_preferences_macos: dependency: transitive description: name: shared_preferences_macos - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.4" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.0" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.4" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.1" shelf: dependency: transitive description: name: shelf - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.3.2" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.2" shortid: dependency: transitive description: name: shortid - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.1.2" sky_engine: @@ -932,280 +932,280 @@ packages: dependency: transitive description: name: source_gen - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.2.2" source_span: dependency: transitive description: name: source_span - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.8.2" sqflite: dependency: transitive description: name: sqflite - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.3+1" sqflite_common: dependency: transitive description: name: sqflite_common - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.2.1+1" stack_trace: dependency: transitive description: name: stack_trace - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.10.0" stream_channel: dependency: transitive description: name: stream_channel - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.0" stream_transform: dependency: transitive description: name: stream_transform - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.0" string_scanner: dependency: transitive description: name: string_scanner - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.1.0" synchronized: dependency: transitive description: name: synchronized - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.0+2" term_glyph: dependency: transitive description: name: term_glyph - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.2.0" test_api: dependency: transitive description: name: test_api - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.4.9" timing: dependency: transitive description: name: timing - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.0" toggle_switch: dependency: "direct main" description: name: toggle_switch - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.4.0" tray_manager: dependency: "direct main" description: name: tray_manager - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.1.7" tuple: dependency: "direct main" description: name: tuple - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.0" typed_data: dependency: transitive description: name: typed_data - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.3.1" url_launcher: dependency: "direct main" description: name: url_launcher - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "6.1.5" url_launcher_android: dependency: transitive description: name: url_launcher_android - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "6.0.17" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "6.0.17" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.1" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.0" url_launcher_web: dependency: transitive description: name: url_launcher_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.13" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.1" uuid: dependency: transitive description: name: uuid - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.6" vector_math: dependency: transitive description: name: vector_math - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.2" video_player: dependency: transitive description: name: video_player - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.4.6" video_player_android: dependency: transitive description: name: video_player_android - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.3.8" video_player_avfoundation: dependency: transitive description: name: video_player_avfoundation - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.3.5" video_player_platform_interface: dependency: transitive description: name: video_player_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "5.1.4" video_player_web: dependency: transitive description: name: video_player_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.12" visibility_detector: dependency: "direct main" description: name: visibility_detector - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.3.3" wakelock: dependency: "direct main" description: name: wakelock - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.5.6" wakelock_macos: dependency: transitive description: name: wakelock_macos - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.4.0" wakelock_platform_interface: dependency: transitive description: name: wakelock_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.3.0" wakelock_web: dependency: transitive description: name: wakelock_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.4.0" wakelock_windows: dependency: transitive description: name: wakelock_windows - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.2.0" watcher: dependency: transitive description: name: watcher - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.1" web_socket_channel: dependency: transitive description: name: web_socket_channel - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.2.0" win32: dependency: transitive description: name: win32 - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.7.0" window_manager: @@ -1221,28 +1221,28 @@ packages: dependency: transitive description: name: xdg_directories - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.2.0+1" xml: dependency: transitive description: name: xml - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "6.1.0" yaml: dependency: transitive description: name: yaml - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.1.1" zxing2: dependency: "direct main" description: name: zxing2 - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.1.0" sdks: From 3e702c834a90164ce85f6e2e82f90af74ffd8c7a Mon Sep 17 00:00:00 2001 From: csf Date: Mon, 15 Aug 2022 16:51:33 +0800 Subject: [PATCH 0217/2015] fix showLoading dark theme & add doubleTap to connect --- flutter/lib/common.dart | 1 - flutter/lib/desktop/widgets/peercard_widget.dart | 4 +++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index dd48cefea..fb36d3aae 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -200,7 +200,6 @@ class OverlayDialogManager { VoidCallback? onCancel}) { show((setState, close) => CustomAlertDialog( content: Container( - color: MyTheme.white, constraints: BoxConstraints(maxWidth: 240), child: Column( mainAxisSize: MainAxisSize.min, diff --git a/flutter/lib/desktop/widgets/peercard_widget.dart b/flutter/lib/desktop/widgets/peercard_widget.dart index 85e6e20e6..5a5780431 100644 --- a/flutter/lib/desktop/widgets/peercard_widget.dart +++ b/flutter/lib/desktop/widgets/peercard_widget.dart @@ -53,7 +53,9 @@ class _PeerCardState extends State<_PeerCard> border: Border.all(color: Colors.transparent, width: 1.0), borderRadius: BorderRadius.circular(20)); }, - child: _buildPeerTile(context, peer, deco), + child: GestureDetector( + onDoubleTap: () => _connect(peer.id), + child: _buildPeerTile(context, peer, deco)), )); } From f99ab7d0a73db34379f3707e13d664c3d164ad04 Mon Sep 17 00:00:00 2001 From: csf Date: Mon, 15 Aug 2022 19:31:58 +0800 Subject: [PATCH 0218/2015] fix dialog res bug ; add desktop restart remote device --- flutter/lib/common.dart | 5 ++-- flutter/lib/desktop/pages/remote_page.dart | 13 ++++++++-- flutter/lib/mobile/pages/remote_page.dart | 23 ------------------ flutter/lib/mobile/widgets/dialog.dart | 23 ++++++++++++++++++ flutter/lib/models/model.dart | 1 - src/flutter.rs | 28 +++++++++------------- src/flutter_ffi.rs | 5 ++-- 7 files changed, 51 insertions(+), 47 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index fb36d3aae..d115156de 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -92,7 +92,7 @@ typedef DialogBuilder = CustomAlertDialog Function( class Dialog { OverlayEntry? entry; - Completer completer = Completer(); + Completer completer = Completer(); Dialog(); @@ -101,9 +101,10 @@ class Dialog { if (!completer.isCompleted) { completer.complete(res); } - entry?.remove(); } catch (e) { debugPrint("Dialog complete catch error: $e"); + } finally { + entry?.remove(); } } } diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 02060dee5..6be097854 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -589,11 +589,10 @@ class _RemotePageState extends State more.add(PopupMenuItem( child: Row( children: ([ - Container(width: 100.0, child: Text(translate('OS Password'))), + Text(translate('OS Password')), TextButton( style: flatButtonStyle, onPressed: () { - Navigator.pop(context); showSetOSPassword(widget.id, false, _ffi.dialogManager); }, child: Icon(Icons.edit, color: MyTheme.accent), @@ -625,6 +624,13 @@ class _RemotePageState extends State value: 'block-input')); } } + if (gFFI.ffiModel.permissions["restart"] != false && + (pi.platform == "Linux" || + pi.platform == "Windows" || + pi.platform == "Mac OS")) { + more.add(PopupMenuItem( + child: Text(translate('Restart Remote Device')), value: 'restart')); + } () async { var value = await showMenu( context: context, @@ -652,6 +658,7 @@ class _RemotePageState extends State }(); } else if (value == 'enter_os_password') { // FIXME: + // TODO icon diff // null means no session of id // empty string means no password var password = await bind.getSessionOption(id: id, arg: "os-password"); @@ -662,6 +669,8 @@ class _RemotePageState extends State } } else if (value == 'reset_canvas') { _ffi.cursorModel.reset(); + } else if (value == 'restart') { + showRestartRemoteDevice(pi, widget.id, gFFI.dialogManager); } }(); } diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index d64c83707..c7d4202f2 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -670,7 +670,6 @@ class _RemotePageState extends State { TextButton( style: flatButtonStyle, onPressed: () { - Navigator.pop(context); showSetOSPassword(id, false, gFFI.dialogManager); }, child: Icon(Icons.edit, color: MyTheme.accent), @@ -1110,28 +1109,6 @@ void showOptions(String id, OverlayDialogManager dialogManager) async { }, clickMaskDismiss: true, backDismiss: true); } -void showRestartRemoteDevice( - PeerInfo pi, String id, OverlayDialogManager dialogManager) async { - final res = - await dialogManager.show((setState, close) => CustomAlertDialog( - title: Row(children: [ - Icon(Icons.warning_amber_sharp, - color: Colors.redAccent, size: 28), - SizedBox(width: 10), - Text(translate("Restart Remote Device")), - ]), - content: Text( - "${translate('Are you sure you want to restart')} \n${pi.username}@${pi.hostname}($id) ?"), - actions: [ - TextButton( - onPressed: () => close(), child: Text(translate("Cancel"))), - ElevatedButton( - onPressed: () => close(true), child: Text(translate("OK"))), - ], - )); - if (res == true) bind.sessionRestartRemoteDevice(id: id); -} - void showSetOSPassword( String id, bool login, OverlayDialogManager dialogManager) async { final controller = TextEditingController(); diff --git a/flutter/lib/mobile/widgets/dialog.dart b/flutter/lib/mobile/widgets/dialog.dart index 098f8d912..e0f98443b 100644 --- a/flutter/lib/mobile/widgets/dialog.dart +++ b/flutter/lib/mobile/widgets/dialog.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import '../../common.dart'; +import '../../models/model.dart'; import '../../models/platform_model.dart'; void clientClose(OverlayDialogManager dialogManager) { @@ -16,6 +17,28 @@ void showError() { showToast(translate("Error")); } +void showRestartRemoteDevice( + PeerInfo pi, String id, OverlayDialogManager dialogManager) async { + final res = + await dialogManager.show((setState, close) => CustomAlertDialog( + title: Row(children: [ + Icon(Icons.warning_amber_sharp, + color: Colors.redAccent, size: 28), + SizedBox(width: 10), + Text(translate("Restart Remote Device")), + ]), + content: Text( + "${translate('Are you sure you want to restart')} \n${pi.username}@${pi.hostname}($id) ?"), + actions: [ + TextButton( + onPressed: () => close(), child: Text(translate("Cancel"))), + ElevatedButton( + onPressed: () => close(true), child: Text(translate("OK"))), + ], + )); + if (res == true) bind.sessionRestartRemoteDevice(id: id); +} + void setPermanentPasswordDialog(OverlayDialogManager dialogManager) async { final pw = await bind.mainGetPermanentPassword(); final p0 = TextEditingController(text: pw); diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index c4b10b377..18fe6a7f9 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -1050,7 +1050,6 @@ class FFI { await for (final message in stream) { if (message is Event) { try { - debugPrint("event:${message.field0}"); Map event = json.decode(message.field0); cb(event); } catch (e) { diff --git a/src/flutter.rs b/src/flutter.rs index bb8881c58..418abc8af 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -31,9 +31,10 @@ use hbb_common::{ Stream, }; -use crate::common::{ - self, check_clipboard, make_fd_to_json, update_clipboard, ClipboardContext, CLIPBOARD_INTERVAL, -}; +use crate::common::{self, make_fd_to_json, CLIPBOARD_INTERVAL}; + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use crate::common::{check_clipboard, update_clipboard, ClipboardContext}; use crate::{client::*, flutter_ffi::EventToUI, make_fd_flutter}; @@ -127,26 +128,18 @@ impl Session { } lc.set_option(name, value); } - // TODO - // input_os_password - // restart_remote_device /// Input the OS password. pub fn input_os_password(&self, pass: String, activate: bool) { input_os_password(pass, activate, self.clone()); } - // impl Interface - /// Send message to the remote session. - /// - /// # Arguments - /// - /// * `data` - The data to send. See [`Data`] for more details. - // fn send(data: Data) { - // if let Some(session) = SESSION.read().unwrap().as_ref() { - // session.send(data); - // } - // } + pub fn restart_remote_device(&self) { + let mut lc = self.lc.write().unwrap(); + lc.restarting_remote_device = true; + let msg = lc.restart_remote_device(); + self.send_msg(msg); + } /// Toggle an option. pub fn toggle_option(&self, name: &str) { @@ -670,6 +663,7 @@ impl Connection { lc: Arc>, ) -> Option> { let (tx, rx) = std::sync::mpsc::channel(); + #[cfg(not(any(target_os = "android", target_os = "ios")))] match ClipboardContext::new() { Ok(mut ctx) => { let old_clipboard: Arc> = Default::default(); diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 4d062ab11..686111715 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -696,8 +696,9 @@ pub fn session_send_mouse(id: String, msg: String) { } pub fn session_restart_remote_device(id: String) { - // TODO - // Session::restart_remote_device(); + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.restart_remote_device(); + } } pub fn main_set_home_dir(home: String) { From 710ffcd0c7310ac54b88ea0238a46da398d08c47 Mon Sep 17 00:00:00 2001 From: csf Date: Mon, 15 Aug 2022 20:26:20 +0800 Subject: [PATCH 0219/2015] update quality monitor & remove remote_page.dart desktop unused code --- flutter/lib/common.dart | 5 +- flutter/lib/desktop/pages/remote_page.dart | 323 +++++---------------- flutter/lib/mobile/pages/remote_page.dart | 1 + src/flutter.rs | 1 + 4 files changed, 71 insertions(+), 259 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index d115156de..93c151bee 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -480,7 +480,8 @@ RadioListTile getRadio( } CheckboxListTile getToggle( - String id, void Function(void Function()) setState, option, name) { + String id, void Function(void Function()) setState, option, name, + {FFI? ffi}) { final opt = bind.getSessionToggleOptionSync(id: id, arg: option); return CheckboxListTile( value: opt, @@ -489,7 +490,7 @@ CheckboxListTile getToggle( bind.sessionToggleOption(id: id, value: option); }); if (option == "show-quality-monitor") { - gFFI.qualityMonitorModel.checkShowQualityMonitor(id); + (ffi ?? gFFI).qualityMonitorModel.checkShowQualityMonitor(id); } }, dense: true, diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 6be097854..e64d7a59a 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -5,7 +5,6 @@ import 'dart:ui' as ui; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_hbb/mobile/widgets/gesture_help.dart'; import 'package:flutter_hbb/models/chat_model.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; @@ -34,20 +33,13 @@ class RemotePage extends StatefulWidget { class _RemotePageState extends State with AutomaticKeepAliveClientMixin { - Timer? _interval; Timer? _timer; bool _showBar = !isWebDesktop; - double _bottom = 0; String _value = ''; - double _scale = 1; - double _mouseScrollIntegral = 0; // mouse scroll speed controller var _cursorOverImage = false.obs; - var _more = true; - var _fn = false; final FocusNode _mobileFocusNode = FocusNode(); final FocusNode _physicalFocusNode = FocusNode(); - var _showEdit = false; // use soft keyboard var _isPhysicalMouse = false; late FFI _ffi; @@ -63,8 +55,6 @@ class _RemotePageState extends State SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); _ffi.dialogManager .showLoading(translate('Connecting...'), onCancel: backToHomePage); - _interval = - Timer.periodic(Duration(milliseconds: 30), (timer) => interval()); }); if (!Platform.isLinux) { Wakelock.enable(); @@ -72,6 +62,7 @@ class _RemotePageState extends State _physicalFocusNode.requestFocus(); _ffi.ffiModel.updateEventListener(widget.id); _ffi.listenToMouse(true); + _ffi.qualityMonitorModel.checkShowQualityMonitor(widget.id); // WindowManager.instance.addListener(this); } @@ -80,11 +71,9 @@ class _RemotePageState extends State print("REMOTE PAGE dispose ${widget.id}"); hideMobileActionsOverlay(); _ffi.listenToMouse(false); - _ffi.invokeMethod("enable_soft_keyboard", true); _mobileFocusNode.dispose(); _physicalFocusNode.dispose(); _ffi.close(); - _interval?.cancel(); _timer?.cancel(); _ffi.dialogManager.dismissAll(); SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, @@ -101,31 +90,6 @@ class _RemotePageState extends State _ffi.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: []); - } - }); - } - } - - void interval() { - try { - intervalUnsafe(); - } catch (e) {} - } - // handle mobile virtual keyboard void handleInput(String newValue) { var oldValue = _value; @@ -185,7 +149,6 @@ class _RemotePageState extends State content == '【】')) { // can not only input content[0], because when input ], [ are also auo insert, which cause ] never be input bind.sessionInputString(id: widget.id, value: content); - openKeyboard(); return; } bind.sessionInputString(id: widget.id, value: content); @@ -204,25 +167,6 @@ class _RemotePageState extends State _ffi.inputKey(char); } - void openKeyboard() { - _ffi.invokeMethod("enable_soft_keyboard", true); - // destroy first, so that our _value trick can work - _value = initText; - setState(() => _showEdit = false); - _timer?.cancel(); - _timer = Timer(Duration(milliseconds: 30), () { - // show now, and sleep a while to requestFocus to - // make sure edit ready, so that keyboard wont show/hide/show/hide happen - setState(() => _showEdit = true); - _timer?.cancel(); - _timer = Timer(Duration(milliseconds: 30), () { - SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, - overlays: SystemUiOverlay.values); - _mobileFocusNode.requestFocus(); - }); - }); - } - void sendRawKey(RawKeyEvent e, {bool? down, bool? press}) { // for maximum compatibility final label = _logicalKeyMap[e.logicalKey.keyId] ?? @@ -233,28 +177,18 @@ class _RemotePageState extends State Widget buildBody(FfiModel ffiModel) { final hasDisplays = ffiModel.pi.displays.length > 0; - final hideKeyboard = isKeyboardShown() && _showEdit; - final showActionButton = !_showBar || hideKeyboard; final keyboard = ffiModel.permissions['keyboard'] != false; return Scaffold( // resizeToAvoidBottomInset: true, - floatingActionButton: !showActionButton + floatingActionButton: _showBar ? null : FloatingActionButton( - mini: !hideKeyboard, - child: - Icon(hideKeyboard ? Icons.expand_more : Icons.expand_less), + mini: true, + child: Icon(Icons.expand_less), backgroundColor: MyTheme.accent, onPressed: () { setState(() { - if (hideKeyboard) { - _showEdit = false; - _ffi.invokeMethod("enable_soft_keyboard", false); - _mobileFocusNode.unfocus(); - _physicalFocusNode.requestFocus(); - } else { - _showBar = !_showBar; - } + _showBar = !_showBar; }); }), bottomNavigationBar: @@ -322,8 +256,7 @@ class _RemotePageState extends State sendRawKey(e, down: true); } } - // [!_showEdit] workaround for soft-keyboard's control_key like Backspace / Enter - if (!_showEdit && e is RawKeyUpEvent) { + if (e is RawKeyUpEvent) { if (key == LogicalKeyboardKey.altLeft || key == LogicalKeyboardKey.altRight) { _ffi.alt = false; @@ -369,8 +302,8 @@ class _RemotePageState extends State color: Colors.white, icon: Icon(Icons.tv), onPressed: () { - setState(() => _showEdit = false); - showOptions(widget.id, _ffi.dialogManager); + _ffi.dialogManager.dismissAll(); + showOptions(widget.id); }, ) ] + @@ -390,19 +323,7 @@ class _RemotePageState extends State }, ) ] - : [ - IconButton( - color: Colors.white, - icon: Icon(Icons.keyboard), - onPressed: openKeyboard), - IconButton( - color: Colors.white, - icon: Icon(_ffi.ffiModel.touchMode - ? Icons.touch_app - : Icons.mouse), - onPressed: changeTouchMode, - ), - ]) + + : []) + (isWeb ? [] : [ @@ -421,7 +342,6 @@ class _RemotePageState extends State color: Colors.white, icon: Icon(Icons.more_vert), onPressed: () { - setState(() => _showEdit = false); showActions(widget.id, ffiModel); }, ), @@ -548,7 +468,7 @@ class _RemotePageState extends State id: widget.id, )); } - paints.add(getHelpTools()); + paints.add(QualityMonitor(_ffi.qualityMonitorModel)); return Stack( children: paints, ); @@ -675,165 +595,6 @@ 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( - padding: EdgeInsets.symmetric(vertical: 10), - child: GestureHelp( - touchMode: _ffi.ffiModel.touchMode, - onTouchModeChange: (t) { - _ffi.ffiModel.toggleTouchMode(); - final v = _ffi.ffiModel.touchMode ? 'Y' : ''; - bind.sessionPeerOption( - id: widget.id, name: "touch-mode", value: v); - })); - })); - } - - Widget getHelpTools() { - final keyboard = isKeyboardShown(); - if (!keyboard) { - return SizedBox(); - } - var 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 = _ffi.ffiModel.pi; - final isMac = pi.platform == "Mac OS"; - final modifiers = [ - wrap('Ctrl ', () { - setState(() => _ffi.ctrl = !_ffi.ctrl); - }, _ffi.ctrl), - wrap(' Alt ', () { - setState(() => _ffi.alt = !_ffi.alt); - }, _ffi.alt), - wrap('Shift', () { - setState(() => _ffi.shift = !_ffi.shift); - }, _ffi.shift), - wrap(isMac ? ' Cmd ' : ' Win ', () { - setState(() => _ffi.command = !_ffi.command); - }, _ffi.command), - ]; - final keys = [ - wrap( - ' Fn ', - () => setState( - () { - _fn = !_fn; - if (_fn) { - _more = false; - } - }, - ), - _fn), - wrap( - ' ... ', - () => setState( - () { - _more = !_more; - if (_more) { - _fn = false; - } - }, - ), - _more), - ]; - final fn = [ - SizedBox(width: 9999), - ]; - for (var i = 1; i <= 12; ++i) { - final name = 'F' + i.toString(); - fn.add(wrap(name, () { - _ffi.inputKey('VK_' + name); - })); - } - final more = [ - SizedBox(width: 9999), - wrap('Esc', () { - _ffi.inputKey('VK_ESCAPE'); - }), - wrap('Tab', () { - _ffi.inputKey('VK_TAB'); - }), - wrap('Home', () { - _ffi.inputKey('VK_HOME'); - }), - wrap('End', () { - _ffi.inputKey('VK_END'); - }), - wrap('Del', () { - _ffi.inputKey('VK_DELETE'); - }), - wrap('PgUp', () { - _ffi.inputKey('VK_PRIOR'); - }), - wrap('PgDn', () { - _ffi.inputKey('VK_NEXT'); - }), - SizedBox(width: 9999), - wrap('', () { - _ffi.inputKey('VK_LEFT'); - }, false, Icons.keyboard_arrow_left), - wrap('', () { - _ffi.inputKey('VK_UP'); - }, false, Icons.keyboard_arrow_up), - wrap('', () { - _ffi.inputKey('VK_DOWN'); - }, false, Icons.keyboard_arrow_down), - wrap('', () { - _ffi.inputKey('VK_RIGHT'); - }, false, Icons.keyboard_arrow_right), - wrap(isMac ? 'Cmd+C' : 'Ctrl+C', () { - sendPrompt(widget.id, isMac, 'VK_C'); - }), - wrap(isMac ? 'Cmd+V' : 'Ctrl+V', () { - sendPrompt(widget.id, isMac, 'VK_V'); - }), - wrap(isMac ? 'Cmd+S' : 'Ctrl+S', () { - sendPrompt(widget.id, isMac, 'VK_S'); - }), - ]; - final space = MediaQuery.of(context).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), - child: Wrap( - spacing: space, - runSpacing: space, - children: [SizedBox(width: 9999)] + - (keyboard - ? modifiers + keys + (_fn ? fn : []) + (_more ? more : []) - : modifiers), - )); - } - @override void onWindowEvent(String eventName) { print("window event: $eventName"); @@ -1001,7 +762,52 @@ class ImagePainter extends CustomPainter { } } -void showOptions(String id, OverlayDialogManager dialogManager) async { +class QualityMonitor extends StatelessWidget { + final QualityMonitorModel qualityMonitorModel; + QualityMonitor(this.qualityMonitorModel); + + @override + Widget build(BuildContext context) => ChangeNotifierProvider.value( + value: qualityMonitorModel, + child: Consumer( + builder: (context, qualityMonitorModel, child) => Positioned( + top: 10, + right: 10, + child: qualityMonitorModel.show + ? Container( + padding: EdgeInsets.all(8), + color: MyTheme.canvasColor.withAlpha(120), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Speed: ${qualityMonitorModel.data.speed ?? ''}", + style: TextStyle(color: MyTheme.grayBg), + ), + Text( + "FPS: ${qualityMonitorModel.data.fps ?? ''}", + style: TextStyle(color: MyTheme.grayBg), + ), + Text( + "Delay: ${qualityMonitorModel.data.delay ?? ''} ms", + style: TextStyle(color: MyTheme.grayBg), + ), + Text( + "Target Bitrate: ${qualityMonitorModel.data.targetBitrate ?? ''}kb", + style: TextStyle(color: MyTheme.grayBg), + ), + Text( + "Codec: ${qualityMonitorModel.data.codecFormat ?? ''}", + style: TextStyle(color: MyTheme.grayBg), + ), + ], + ), + ) + : SizedBox.shrink()))); +} + +void showOptions(String id) async { + final _ffi = ffi(id); String quality = await bind.getSessionImageQuality(id: id) ?? 'balanced'; if (quality == '') quality = 'balanced'; String viewStyle = @@ -1009,8 +815,8 @@ void showOptions(String id, OverlayDialogManager dialogManager) async { String scrollStyle = await bind.getSessionOption(id: id, arg: 'scroll-style') ?? ''; var displays = []; - final pi = ffi(id).ffiModel.pi; - final image = ffi(id).ffiModel.getConnectionImage(); + final pi = _ffi.ffiModel.pi; + final image = _ffi.ffiModel.getConnectionImage(); if (image != null) displays.add(Padding(padding: const EdgeInsets.only(top: 8), child: image)); if (pi.displays.length > 1) { @@ -1021,7 +827,7 @@ void showOptions(String id, OverlayDialogManager dialogManager) async { onTap: () { if (i == cur) return; bind.sessionSwitchDisplay(id: id, value: i); - dialogManager.dismissAll(); + _ffi.dialogManager.dismissAll(); }, child: Ink( width: 40, @@ -1044,9 +850,9 @@ void showOptions(String id, OverlayDialogManager dialogManager) async { if (displays.isNotEmpty) { displays.add(Divider(color: MyTheme.border)); } - final perms = ffi(id).ffiModel.permissions; + final perms = _ffi.ffiModel.permissions; - dialogManager.show((setState, close) { + _ffi.dialogManager.show((setState, close) { final more = []; if (perms['audio'] != false) { more.add(getToggle(id, setState, 'disable-audio', 'Mute')); @@ -1077,7 +883,7 @@ void showOptions(String id, OverlayDialogManager dialogManager) async { setState(() { viewStyle = value; bind.sessionPeerOption(id: id, name: "view-style", value: value); - ffi(id).canvasModel.updateViewStyle(); + _ffi.canvasModel.updateViewStyle(); }); }; var setScrollStyle = (String? value) { @@ -1085,7 +891,7 @@ void showOptions(String id, OverlayDialogManager dialogManager) async { setState(() { scrollStyle = value; bind.sessionPeerOption(id: id, name: "scroll-style", value: value); - ffi(id).canvasModel.updateScrollStyle(); + _ffi.canvasModel.updateScrollStyle(); }); }; return CustomAlertDialog( @@ -1108,6 +914,9 @@ void showOptions(String id, OverlayDialogManager dialogManager) async { Divider(color: MyTheme.border), getToggle( id, setState, 'show-remote-cursor', 'Show remote cursor'), + getToggle(id, setState, 'show-quality-monitor', + 'Show quality monitor', + ffi: _ffi), ] + more), actions: [], diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index c7d4202f2..6a5be8b8d 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -59,6 +59,7 @@ class _RemotePageState extends State { _physicalFocusNode.requestFocus(); gFFI.ffiModel.updateEventListener(widget.id); gFFI.listenToMouse(true); + gFFI.qualityMonitorModel.checkShowQualityMonitor(widget.id); } @override diff --git a/src/flutter.rs b/src/flutter.rs index 418abc8af..b5553e475 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -850,6 +850,7 @@ impl Connection { }; if let Ok(true) = self.video_handler.handle_frame(vf) { let stream = self.session.events2ui.read().unwrap(); + self.frame_count.fetch_add(1, Ordering::Relaxed); stream.add(EventToUI::Rgba(ZeroCopyBuffer( self.video_handler.rgb.clone(), ))); From bb99dcab6b5dd2c792ce21c1d0d7f9790d01a069 Mon Sep 17 00:00:00 2001 From: kordood Date: Tue, 16 Aug 2022 10:55:24 +0900 Subject: [PATCH 0220/2015] Update lang.rs to add Korean language Signed-off-by: kordood --- src/lang.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/lang.rs b/src/lang.rs index 400c4dd95..22085a34d 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -13,6 +13,7 @@ mod hu; mod id; mod it; mod ja; +mod ko; mod pl; mod ptbr; mod ru; @@ -43,6 +44,7 @@ lazy_static::lazy_static! { ("vn", "Tiếng Việt"), ("pl", "Polski"), ("ja", "日本語"), + ("ko", "한국어"), ]); } @@ -90,6 +92,7 @@ pub fn translate_locale(name: String, locale: &str) -> String { "vn" => vn::T.deref(), "pl" => pl::T.deref(), "ja" => ja::T.deref(), + "ja" => ko::T.deref(), _ => en::T.deref(), }; if let Some(v) = m.get(&name as &str) { From d9c93655204665b6da79b182de6170aa0a88e4da Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 16 Aug 2022 11:46:51 +0800 Subject: [PATCH 0221/2015] feat: switch breadcrumb&path with focus node Signed-off-by: Kingtous --- .../lib/desktop/pages/file_manager_page.dart | 456 +++++++++++------- flutter/lib/models/file_model.dart | 16 + 2 files changed, 297 insertions(+), 175 deletions(-) diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index e5279a7e2..9febc462b 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'dart:math'; import 'package:flutter/material.dart'; +import 'package:flutter_breadcrumb/flutter_breadcrumb.dart'; import 'package:flutter_hbb/mobile/pages/file_manager_page.dart'; import 'package:flutter_hbb/models/file_model.dart'; import 'package:get/get.dart'; @@ -12,6 +13,8 @@ import '../../common.dart'; import '../../models/model.dart'; import '../../models/platform_model.dart'; +enum LocationStatus { bread, textField } + class FileManagerPage extends StatefulWidget { FileManagerPage({Key? key, required this.id}) : super(key: key); final String id; @@ -25,6 +28,17 @@ class _FileManagerPageState extends State final _localSelectedItems = SelectedItems(); final _remoteSelectedItems = SelectedItems(); + final _locationStatusLocal = LocationStatus.bread.obs; + final _locationStatusRemote = LocationStatus.bread.obs; + final FocusNode _locationNodeLocal = + FocusNode(debugLabel: "locationNodeLocal"); + final FocusNode _locationNodeRemote = + FocusNode(debugLabel: "locationNodeRemote"); + final FocusNode _locationSearchLocal = + FocusNode(debugLabel: "locationSearchLocal"); + final FocusNode _locationSearchRemote = + FocusNode(debugLabel: "locationSearchRemote"); + late FFI _ffi; FileModel get model => _ffi.fileModel; @@ -44,6 +58,9 @@ class _FileManagerPageState extends State Wakelock.enable(); } print("init success with id ${widget.id}"); + // register location listener + _locationNodeLocal.addListener(onLocalLocationFocusChanged); + _locationNodeRemote.addListener(onRemoteLocationFocusChanged); } @override @@ -55,6 +72,8 @@ class _FileManagerPageState extends State Wakelock.disable(); } Get.delete(tag: 'ft_${widget.id}'); + _locationNodeLocal.removeListener(onLocalLocationFocusChanged); + _locationNodeRemote.removeListener(onRemoteLocationFocusChanged); super.dispose(); } @@ -129,8 +148,7 @@ class _FileManagerPageState extends State final sortAscending = isLocal ? model.localSortAscending : model.remoteSortAscending; return Container( - decoration: BoxDecoration( - color: Colors.white54, border: Border.all(color: Colors.black26)), + decoration: BoxDecoration(border: Border.all(color: Colors.black26)), margin: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(8.0), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -142,6 +160,7 @@ class _FileManagerPageState extends State Expanded( child: SingleChildScrollView( child: DataTable( + key: ValueKey(isLocal ? 0 : 1), showCheckboxColumn: true, dataRowHeight: 25, headingRowHeight: 30, @@ -223,9 +242,9 @@ class _FileManagerPageState extends State }), DataCell(Text( entry - .lastModified() - .toString() - .replaceAll(".000", "") + + .lastModified() + .toString() + .replaceAll(".000", "") + " ", style: TextStyle( fontSize: 12, color: MyTheme.darkGray), @@ -355,8 +374,7 @@ class _FileManagerPageState extends State child: Container( margin: const EdgeInsets.only(top: 16.0, bottom: 16.0, right: 16.0), padding: const EdgeInsets.all(8.0), - decoration: BoxDecoration( - color: Colors.white70, border: Border.all(color: Colors.grey)), + decoration: BoxDecoration(border: Border.all(color: Colors.grey)), child: Obx( () => ListView.builder( itemBuilder: (BuildContext context, int index) { @@ -449,183 +467,206 @@ class _FileManagerPageState extends State model.goToParentDirectory(isLocal: isLocal); } - Widget headTools(bool isLocal) => Container( - child: Column( - children: [ - // symbols - PreferredSize( - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Container( - width: 50, - height: 50, - decoration: BoxDecoration(color: Colors.blue), - padding: EdgeInsets.all(8.0), - child: FutureBuilder( - future: bind.sessionGetPlatform( - id: _ffi.id, isRemote: !isLocal), - builder: (context, snapshot) { - if (snapshot.hasData && snapshot.data!.isNotEmpty) { - return getPlatformImage('${snapshot.data}'); - } else { - return CircularProgressIndicator( - color: Colors.white, - ); + Widget headTools(bool isLocal) { + final _locationStatus = + isLocal ? _locationStatusLocal : _locationStatusRemote; + final _locationFocus = isLocal ? _locationNodeLocal : _locationNodeRemote; + return Container( + child: Column( + children: [ + // symbols + PreferredSize( + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + width: 50, + height: 50, + decoration: BoxDecoration(color: Colors.blue), + padding: EdgeInsets.all(8.0), + child: FutureBuilder( + future: bind.sessionGetPlatform( + id: _ffi.id, isRemote: !isLocal), + builder: (context, snapshot) { + if (snapshot.hasData && snapshot.data!.isNotEmpty) { + return getPlatformImage('${snapshot.data}'); + } else { + return CircularProgressIndicator( + color: Colors.white, + ); + } + })), + Text(isLocal + ? translate("Local Computer") + : translate("Remote Computer")) + .marginOnly(left: 8.0) + ], + ), + preferredSize: Size(double.infinity, 70)), + // buttons + Row( + children: [ + Row( + children: [ + IconButton( + onPressed: () { + model.goHome(isLocal: isLocal); + }, + icon: Icon(Icons.home_outlined)), + IconButton( + icon: Icon(Icons.arrow_upward), + onPressed: () { + goBack(isLocal: isLocal); + }, + ), + menu(isLocal: isLocal), + ], + ), + Expanded( + child: GestureDetector( + onTap: () { + _locationStatus.value = + _locationStatus.value == LocationStatus.bread + ? LocationStatus.textField + : LocationStatus.bread; + Future.delayed(Duration.zero, () { + if (_locationStatus.value == LocationStatus.textField) { + _locationFocus.requestFocus(); + } + }); + }, + child: Container( + decoration: + BoxDecoration(border: Border.all(color: Colors.black12)), + child: Row( + children: [ + Expanded( + child: Obx(() => + _locationStatus.value == LocationStatus.bread + ? buildBread(isLocal) + : buildPathLocation(isLocal))), + DropdownButton( + isDense: true, + underline: Offstage(), + items: [ + // TODO: favourite + DropdownMenuItem( + child: Text('/'), + value: '/', + ) + ], + onChanged: (path) { + if (path is String && path.isNotEmpty) { + model.openDirectory(path, isLocal: isLocal); } - })), - Text(isLocal - ? translate("Local Computer") - : translate("Remote Computer")) - .marginOnly(left: 8.0) - ], - ), - preferredSize: Size(double.infinity, 70)), - // buttons - Row( - children: [ - Row( + }) + ], + )), + )), + PopupMenuButton( + itemBuilder: (context) => [ + PopupMenuItem( + enabled: false, + child: ConstrainedBox( + constraints: BoxConstraints(minWidth: 200), + child: TextField( + decoration: InputDecoration(), + ), + )) + ], + child: Icon(Icons.search), + ), + IconButton( + onPressed: () { + model.refresh(isLocal: isLocal); + }, + icon: Icon(Icons.refresh)), + ], + ), + Row( + textDirection: isLocal ? TextDirection.ltr : TextDirection.rtl, + children: [ + Expanded( + child: Row( + mainAxisAlignment: + isLocal ? MainAxisAlignment.start : MainAxisAlignment.end, children: [ IconButton( onPressed: () { - model.goHome(isLocal: isLocal); + final name = TextEditingController(); + _ffi.dialogManager + .show((setState, close) => CustomAlertDialog( + title: Text(translate("Create Folder")), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextFormField( + decoration: InputDecoration( + labelText: translate( + "Please enter the folder name"), + ), + controller: name, + ), + ], + ), + actions: [ + TextButton( + style: flatButtonStyle, + onPressed: () => close(false), + child: Text(translate("Cancel"))), + ElevatedButton( + style: flatButtonStyle, + onPressed: () { + if (name.value.text.isNotEmpty) { + model.createDir( + PathUtil.join( + model + .getCurrentDir( + isLocal) + .path, + name.value.text, + model.getCurrentIsWindows( + isLocal)), + isLocal: isLocal); + close(); + } + }, + child: Text(translate("OK"))) + ])); }, - icon: Icon(Icons.home_outlined)), + icon: Icon(Icons.create_new_folder_outlined)), IconButton( - icon: Icon(Icons.arrow_upward), - onPressed: () { - goBack(isLocal: isLocal); - }, - ), - menu(isLocal: isLocal), + onPressed: () async { + final items = isLocal + ? _localSelectedItems + : _remoteSelectedItems; + await (model.removeAction(items, isLocal: isLocal)); + items.clear(); + }, + icon: Icon(Icons.delete_forever_outlined)), ], ), - Expanded( - child: Container( - decoration: BoxDecoration( - border: Border.all(color: Colors.black12)), - child: TextField( - decoration: InputDecoration( - border: InputBorder.none, - isDense: true, - prefix: - Padding(padding: EdgeInsets.only(left: 4.0)), - suffix: DropdownButton( - isDense: true, - underline: Offstage(), - items: [ - // TODO: favourite - DropdownMenuItem( - child: Text('/'), - value: '/', - ) - ], - onChanged: (path) { - if (path is String && path.isNotEmpty) { - model.openDirectory(path, isLocal: isLocal); - } - })), - controller: TextEditingController( - text: isLocal - ? model.currentLocalDir.path - : model.currentRemoteDir.path), - onSubmitted: (path) { - model.openDirectory(path, isLocal: isLocal); - }, - ))), - IconButton( - onPressed: () { - model.refresh(isLocal: isLocal); - }, - icon: Icon(Icons.refresh)) - ], - ), - Row( - textDirection: isLocal ? TextDirection.ltr : TextDirection.rtl, - children: [ - Expanded( - child: Row( - mainAxisAlignment: - isLocal ? MainAxisAlignment.start : MainAxisAlignment.end, - children: [ - IconButton( - onPressed: () { - final name = TextEditingController(); - _ffi.dialogManager.show((setState, close) => - CustomAlertDialog( - title: Text(translate("Create Folder")), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TextFormField( - decoration: InputDecoration( - labelText: translate( - "Please enter the folder name"), - ), - controller: name, - ), - ], - ), - actions: [ - TextButton( - style: flatButtonStyle, - onPressed: () => close(false), - child: Text(translate("Cancel"))), - ElevatedButton( - style: flatButtonStyle, - onPressed: () { - if (name.value.text.isNotEmpty) { - model.createDir( - PathUtil.join( - model - .getCurrentDir(isLocal) - .path, - name.value.text, - model.getCurrentIsWindows( - isLocal)), - isLocal: isLocal); - close(); - } - }, - child: Text(translate("OK"))) - ])); - }, - icon: Icon(Icons.create_new_folder_outlined)), - IconButton( - onPressed: () async { - final items = isLocal - ? _localSelectedItems - : _remoteSelectedItems; - await (model.removeAction(items, isLocal: isLocal)); - items.clear(); - }, - icon: Icon(Icons.delete_forever_outlined)), - ], - ), - ), - TextButton.icon( - onPressed: () { - final items = getSelectedItem(isLocal); - model.sendFiles(items, isRemote: !isLocal); - items.clear(); - }, - icon: Transform.rotate( - angle: isLocal ? 0 : pi, - child: Icon( - Icons.send, - color: Colors.black54, - ), + ), + TextButton.icon( + onPressed: () { + final items = getSelectedItem(isLocal); + model.sendFiles(items, isRemote: !isLocal); + items.clear(); + }, + icon: Transform.rotate( + angle: isLocal ? 0 : pi, + child: Icon( + Icons.send, ), - label: Text( - isLocal ? translate('Send') : translate('Receive'), - style: TextStyle( - color: Colors.black54, - ), - )), - ], - ).marginOnly(top: 8.0) - ], - )); + ), + label: Text( + isLocal ? translate('Send') : translate('Receive'), + )), + ], + ).marginOnly(top: 8.0) + ], + )); + } Widget listTail({bool isLocal = false}) { final dir = isLocal ? model.currentLocalDir : model.currentRemoteDir; @@ -663,4 +704,69 @@ class _FileManagerPageState extends State else if (platform != 'linux' && platform != 'android') platform = 'win'; return Image.asset('assets/$platform.png', width: 25, height: 25); } + + void onLocalLocationFocusChanged() { + debugPrint("focus changed on local"); + if (_locationNodeLocal.hasFocus) { + // ignore + } else { + // lost focus, change to bread + _locationStatusLocal.value = LocationStatus.bread; + } + } + + void onRemoteLocationFocusChanged() { + debugPrint("focus changed on remote"); + if (_locationNodeRemote.hasFocus) { + // ignore + } else { + // lost focus, change to bread + _locationStatusRemote.value = LocationStatus.bread; + } + } + + Widget buildBread(bool isLocal) { + final directory = model.getCurrentDir(isLocal); + print(directory.path); + return BreadCrumb( + items: getPathBreadCrumbItems(isLocal, (list) { + var path = ""; + for (var item in list) { + path = PathUtil.join(path, item, model.getCurrentIsWindows(isLocal)); + } + model.openDirectory(path, isLocal: isLocal); + }), + divider: Text("/").paddingSymmetric(horizontal: 4.0), + ); + } + + List getPathBreadCrumbItems( + bool isLocal, void Function(List) onPressed) { + final path = model.getCurrentDir(isLocal).path; + final list = PathUtil.split(path, model.getCurrentIsWindows(isLocal)); + final breadCrumbList = List.empty(growable: true); + breadCrumbList.addAll(list.asMap().entries.map((e) => BreadCrumbItem( + content: TextButton( + child: Text(e.value), + style: + ButtonStyle(minimumSize: MaterialStateProperty.all(Size(0, 0))), + onPressed: () => onPressed(list.sublist(0, e.key + 1)))))); + return breadCrumbList; + } + + Widget buildPathLocation(bool isLocal) { + return TextField( + focusNode: isLocal ? _locationNodeLocal : _locationNodeRemote, + decoration: InputDecoration( + border: InputBorder.none, + isDense: true, + prefix: Padding(padding: EdgeInsets.only(left: 4.0)), + ), + controller: + TextEditingController(text: model.getCurrentDir(isLocal).path), + onSubmitted: (path) { + model.openDirectory(path, isLocal: isLocal); + }, + ); + } } diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index 74be258a0..1c3960b50 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -68,6 +68,22 @@ class FileModel extends ChangeNotifier { return isLocal ? currentLocalDir : currentRemoteDir; } + String getCurrentShortPath(bool isLocal) { + final currentDir = getCurrentDir(isLocal); + final currentHome = getCurrentHome(isLocal); + if (currentDir.path.startsWith(currentHome)) { + var path = currentDir.path.replaceFirst(currentHome, ""); + if (path.length == 0) return ""; + if (path[0] == "/" || path[0] == "\\") { + // remove more '/' or '\' + path = path.replaceFirst(path[0], ""); + } + return path; + } else { + return currentDir.path.replaceFirst(currentHome, ""); + } + } + String get currentHome => _isLocal ? _localOption.home : _remoteOption.home; String getCurrentHome(bool isLocal) { From 2017a0f02b8ad41345ee6634f6ff611730370f22 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 16 Aug 2022 12:06:54 +0800 Subject: [PATCH 0222/2015] feat: file transfer searchbar Signed-off-by: Kingtous --- .../lib/desktop/pages/file_manager_page.dart | 242 ++++++++++-------- 1 file changed, 139 insertions(+), 103 deletions(-) diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 9febc462b..f2c752f1e 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -34,10 +34,8 @@ class _FileManagerPageState extends State FocusNode(debugLabel: "locationNodeLocal"); final FocusNode _locationNodeRemote = FocusNode(debugLabel: "locationNodeRemote"); - final FocusNode _locationSearchLocal = - FocusNode(debugLabel: "locationSearchLocal"); - final FocusNode _locationSearchRemote = - FocusNode(debugLabel: "locationSearchRemote"); + final searchTextLocal = "".obs; + final searchTextRemote = "".obs; late FFI _ffi; @@ -131,7 +129,7 @@ class _FileManagerPageState extends State } Widget body({bool isLocal = false}) { - final fd = isLocal ? model.currentLocalDir : model.currentRemoteDir; + final fd = model.getCurrentDir(isLocal); final entries = fd.entries; final sortIndex = (SortBy style) { switch (style) { @@ -159,103 +157,127 @@ class _FileManagerPageState extends State children: [ Expanded( child: SingleChildScrollView( - child: DataTable( - key: ValueKey(isLocal ? 0 : 1), - showCheckboxColumn: true, - dataRowHeight: 25, - headingRowHeight: 30, - columnSpacing: 8, - showBottomBorder: true, - sortColumnIndex: sortIndex, - sortAscending: sortAscending, - columns: [ - DataColumn(label: Text(translate(" "))), // icon - DataColumn( - label: Text( - translate("Name"), - ), - 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: entries.map((entry) { - final sizeStr = entry.isFile - ? readableFileSize(entry.size.toDouble()) - : ""; - return DataRow( - key: ValueKey(entry.name), - onSelectChanged: (s) { - if (s != null) { - if (s) { - getSelectedItem(isLocal).add(isLocal, entry); - } else { - getSelectedItem(isLocal).remove(entry); - } - setState(() {}); - } - }, - selected: getSelectedItem(isLocal).contains(entry), - cells: [ - DataCell(Icon( - entry.isFile ? Icons.feed_outlined : Icons.folder, - size: 25)), - DataCell( - ConstrainedBox( - constraints: BoxConstraints(maxWidth: 100), - child: Tooltip( - message: entry.name, - child: Text(entry.name, - overflow: TextOverflow.ellipsis), - )), onTap: () { - if (entry.isDirectory) { - model.openDirectory(entry.path, isLocal: isLocal); - if (isLocal) { - _localSelectedItems.clear(); - } else { - _remoteSelectedItems.clear(); + child: Obx( + () { + final filteredEntries = entries.where((element) { + if (isLocal) { + if (searchTextLocal.isEmpty) { + return true; + } else { + return element.name.contains(searchTextLocal.value); + } + } else { + if (searchTextRemote.isEmpty) { + return true; + } else { + return element.name.contains(searchTextRemote.value); + } + } + }).toList(growable: false); + return DataTable( + key: ValueKey(isLocal ? 0 : 1), + showCheckboxColumn: true, + dataRowHeight: 25, + headingRowHeight: 30, + columnSpacing: 8, + showBottomBorder: true, + sortColumnIndex: sortIndex, + sortAscending: sortAscending, + columns: [ + DataColumn(label: Text(translate(" "))), // icon + DataColumn( + label: Text( + translate("Name"), + ), + 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 sizeStr = entry.isFile + ? readableFileSize(entry.size.toDouble()) + : ""; + return DataRow( + key: ValueKey(entry.name), + onSelectChanged: (s) { + if (s != null) { + if (s) { + getSelectedItem(isLocal).add(isLocal, entry); + } else { + getSelectedItem(isLocal).remove(entry); + } + setState(() {}); } - } else { - // Perform file-related tasks. - final _selectedItems = getSelectedItem(isLocal); - if (_selectedItems.contains(entry)) { - _selectedItems.remove(entry); - } else { - _selectedItems.add(isLocal, entry); - } - setState(() {}); - } - }), - DataCell(Text( - entry - .lastModified() - .toString() - .replaceAll(".000", "") + - " ", - style: TextStyle( - fontSize: 12, color: MyTheme.darkGray), - )), - DataCell(Text( - sizeStr, - style: TextStyle( - fontSize: 12, color: MyTheme.darkGray), - )), - ]); - }).toList(), + }, + selected: getSelectedItem(isLocal).contains(entry), + cells: [ + DataCell(Icon( + entry.isFile + ? Icons.feed_outlined + : Icons.folder, + size: 25)), + DataCell( + ConstrainedBox( + constraints: + BoxConstraints(maxWidth: 100), + child: Tooltip( + message: entry.name, + child: Text(entry.name, + overflow: TextOverflow.ellipsis), + )), onTap: () { + if (entry.isDirectory) { + model.openDirectory(entry.path, + isLocal: isLocal); + if (isLocal) { + _localSelectedItems.clear(); + } else { + _remoteSelectedItems.clear(); + } + } else { + // Perform file-related tasks. + final _selectedItems = + getSelectedItem(isLocal); + if (_selectedItems.contains(entry)) { + _selectedItems.remove(entry); + } else { + _selectedItems.add(isLocal, entry); + } + setState(() {}); + } + }), + DataCell(Text( + entry + .lastModified() + .toString() + .replaceAll(".000", "") + + " ", + style: TextStyle( + fontSize: 12, color: MyTheme.darkGray), + )), + DataCell(Text( + sizeStr, + style: TextStyle( + fontSize: 12, color: MyTheme.darkGray), + )), + ]); + }).toList(), + ); + }, ), ), ) @@ -401,7 +423,6 @@ class _FileManagerPageState extends State child: Text( '${item.jobName}', maxLines: 1, - style: TextStyle(color: Colors.black45), overflow: TextOverflow.ellipsis, )), Wrap( @@ -471,6 +492,7 @@ class _FileManagerPageState extends State final _locationStatus = isLocal ? _locationStatusLocal : _locationStatusRemote; final _locationFocus = isLocal ? _locationNodeLocal : _locationNodeRemote; + final _searchTextObs = isLocal ? searchTextLocal : searchTextRemote; return Container( child: Column( children: [ @@ -570,7 +592,13 @@ class _FileManagerPageState extends State child: ConstrainedBox( constraints: BoxConstraints(minWidth: 200), child: TextField( - decoration: InputDecoration(), + controller: + TextEditingController(text: _searchTextObs.value), + autofocus: true, + decoration: + InputDecoration(prefixIcon: Icon(Icons.search)), + onChanged: (searchText) => + onSearchText(searchText, isLocal), ), )) ], @@ -769,4 +797,12 @@ class _FileManagerPageState extends State }, ); } + + onSearchText(String searchText, bool isLocal) { + if (isLocal) { + searchTextLocal.value = searchText; + } else { + searchTextRemote.value = searchText; + } + } } From eea62352d21e7240520b6b8e3ac474bffbb4b2d1 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 16 Aug 2022 12:28:12 +0800 Subject: [PATCH 0223/2015] feat: file transfer path scrollable Signed-off-by: Kingtous --- .../lib/desktop/pages/file_manager_page.dart | 72 ++++++++++++------- 1 file changed, 45 insertions(+), 27 deletions(-) diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index f2c752f1e..a35ce4378 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -34,8 +34,14 @@ class _FileManagerPageState extends State FocusNode(debugLabel: "locationNodeLocal"); final FocusNode _locationNodeRemote = FocusNode(debugLabel: "locationNodeRemote"); - final searchTextLocal = "".obs; - final searchTextRemote = "".obs; + final _searchTextLocal = "".obs; + final _searchTextRemote = "".obs; + final _breadCrumbScrollerLocal = ScrollController(); + final _breadCrumbScrollerRemote = ScrollController(); + + ScrollController getBreadCrumbScrollController(bool isLocal) { + return isLocal ? _breadCrumbScrollerLocal : _breadCrumbScrollerRemote; + } late FFI _ffi; @@ -159,19 +165,13 @@ class _FileManagerPageState extends State child: SingleChildScrollView( child: Obx( () { + final searchText = + isLocal ? _searchTextLocal : _searchTextRemote; final filteredEntries = entries.where((element) { - if (isLocal) { - if (searchTextLocal.isEmpty) { - return true; - } else { - return element.name.contains(searchTextLocal.value); - } + if (searchText.isEmpty) { + return true; } else { - if (searchTextRemote.isEmpty) { - return true; - } else { - return element.name.contains(searchTextRemote.value); - } + return element.name.contains(searchText.value); } }).toList(growable: false); return DataTable( @@ -234,15 +234,14 @@ class _FileManagerPageState extends State DataCell( ConstrainedBox( constraints: - BoxConstraints(maxWidth: 100), + BoxConstraints(maxWidth: 100), child: Tooltip( message: entry.name, child: Text(entry.name, overflow: TextOverflow.ellipsis), )), onTap: () { if (entry.isDirectory) { - model.openDirectory(entry.path, - isLocal: isLocal); + openDirectory(entry.path, isLocal: isLocal); if (isLocal) { _localSelectedItems.clear(); } else { @@ -251,7 +250,7 @@ class _FileManagerPageState extends State } else { // Perform file-related tasks. final _selectedItems = - getSelectedItem(isLocal); + getSelectedItem(isLocal); if (_selectedItems.contains(entry)) { _selectedItems.remove(entry); } else { @@ -262,9 +261,9 @@ class _FileManagerPageState extends State }), DataCell(Text( entry - .lastModified() - .toString() - .replaceAll(".000", "") + + .lastModified() + .toString() + .replaceAll(".000", "") + " ", style: TextStyle( fontSize: 12, color: MyTheme.darkGray), @@ -367,7 +366,7 @@ class _FileManagerPageState extends State // return; // } // if (entries[index].isDirectory) { - // model.openDirectory(entries[index].path, isLocal: isLocal); + // openDirectory(entries[index].path, isLocal: isLocal); // breadCrumbScrollToEnd(isLocal); // } else { // // Perform file-related tasks. @@ -492,7 +491,7 @@ class _FileManagerPageState extends State final _locationStatus = isLocal ? _locationStatusLocal : _locationStatusRemote; final _locationFocus = isLocal ? _locationNodeLocal : _locationNodeRemote; - final _searchTextObs = isLocal ? searchTextLocal : searchTextRemote; + final _searchTextObs = isLocal ? _searchTextLocal : _searchTextRemote; return Container( child: Column( children: [ @@ -579,7 +578,7 @@ class _FileManagerPageState extends State ], onChanged: (path) { if (path is String && path.isNotEmpty) { - model.openDirectory(path, isLocal: isLocal); + openDirectory(path, isLocal: isLocal); } }) ], @@ -762,9 +761,11 @@ class _FileManagerPageState extends State for (var item in list) { path = PathUtil.join(path, item, model.getCurrentIsWindows(isLocal)); } - model.openDirectory(path, isLocal: isLocal); + openDirectory(path, isLocal: isLocal); }), divider: Text("/").paddingSymmetric(horizontal: 4.0), + overflow: ScrollableOverflow( + controller: getBreadCrumbScrollController(isLocal)), ); } @@ -782,6 +783,16 @@ class _FileManagerPageState extends State return breadCrumbList; } + breadCrumbScrollToEnd(bool isLocal) { + Future.delayed(Duration(milliseconds: 200), () { + final _breadCrumbScroller = getBreadCrumbScrollController(isLocal); + _breadCrumbScroller.animateTo( + _breadCrumbScroller.position.maxScrollExtent, + duration: Duration(milliseconds: 200), + curve: Curves.fastLinearToSlowEaseIn); + }); + } + Widget buildPathLocation(bool isLocal) { return TextField( focusNode: isLocal ? _locationNodeLocal : _locationNodeRemote, @@ -793,16 +804,23 @@ class _FileManagerPageState extends State controller: TextEditingController(text: model.getCurrentDir(isLocal).path), onSubmitted: (path) { - model.openDirectory(path, isLocal: isLocal); + openDirectory(path, isLocal: isLocal); }, ); } onSearchText(String searchText, bool isLocal) { if (isLocal) { - searchTextLocal.value = searchText; + _searchTextLocal.value = searchText; } else { - searchTextRemote.value = searchText; + _searchTextRemote.value = searchText; } } + + openDirectory(String path, {bool isLocal = false}) { + model.openDirectory(path, isLocal: isLocal).then((_) { + print("scroll"); + breadCrumbScrollToEnd(isLocal); + }); + } } From 4bd5fe1509d6eebecc18425836b5fe9a847a0544 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 16 Aug 2022 12:50:08 +0800 Subject: [PATCH 0224/2015] opt: entries empty fallback Signed-off-by: Kingtous --- .../lib/desktop/pages/file_manager_page.dart | 104 ++++++++++-------- 1 file changed, 56 insertions(+), 48 deletions(-) diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index a35ce4378..fc20d5277 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -163,28 +163,33 @@ class _FileManagerPageState extends State children: [ Expanded( child: SingleChildScrollView( - child: Obx( - () { - final searchText = - isLocal ? _searchTextLocal : _searchTextRemote; - final filteredEntries = entries.where((element) { - if (searchText.isEmpty) { - return true; - } else { - return element.name.contains(searchText.value); - } - }).toList(growable: false); - return DataTable( - key: ValueKey(isLocal ? 0 : 1), - showCheckboxColumn: true, - dataRowHeight: 25, - headingRowHeight: 30, - columnSpacing: 8, - showBottomBorder: true, - sortColumnIndex: sortIndex, - sortAscending: sortAscending, - columns: [ - DataColumn(label: Text(translate(" "))), // icon + child: entries.isEmpty + ? Offstage() + : Obx( + () { + final searchText = + isLocal ? _searchTextLocal : _searchTextRemote; + final filteredEntries = searchText.isEmpty + ? entries.where((element) { + if (searchText.isEmpty) { + return true; + } else { + return element.name + .contains(searchText.value); + } + }).toList(growable: false) + : entries; + return DataTable( + key: ValueKey(isLocal ? 0 : 1), + showCheckboxColumn: true, + dataRowHeight: 25, + headingRowHeight: 30, + columnSpacing: 8, + showBottomBorder: true, + sortColumnIndex: sortIndex, + sortAscending: sortAscending, + columns: [ + DataColumn(label: Text(translate(" "))), // icon DataColumn( label: Text( translate("Name"), @@ -264,18 +269,20 @@ class _FileManagerPageState extends State .lastModified() .toString() .replaceAll(".000", "") + - " ", - style: TextStyle( - fontSize: 12, color: MyTheme.darkGray), - )), - DataCell(Text( - sizeStr, - style: TextStyle( - fontSize: 12, color: MyTheme.darkGray), - )), - ]); - }).toList(), - ); + " ", + style: TextStyle( + fontSize: 12, + color: MyTheme.darkGray), + )), + DataCell(Text( + sizeStr, + style: TextStyle( + fontSize: 12, + color: MyTheme.darkGray), + )), + ]); + }).toList(growable: false), + ); }, ), ), @@ -753,20 +760,21 @@ class _FileManagerPageState extends State } Widget buildBread(bool isLocal) { - final directory = model.getCurrentDir(isLocal); - print(directory.path); - return BreadCrumb( - items: getPathBreadCrumbItems(isLocal, (list) { - var path = ""; - for (var item in list) { - path = PathUtil.join(path, item, model.getCurrentIsWindows(isLocal)); - } - openDirectory(path, isLocal: isLocal); - }), - divider: Text("/").paddingSymmetric(horizontal: 4.0), - overflow: ScrollableOverflow( - controller: getBreadCrumbScrollController(isLocal)), - ); + final items = getPathBreadCrumbItems(isLocal, (list) { + var path = ""; + for (var item in list) { + path = PathUtil.join(path, item, model.getCurrentIsWindows(isLocal)); + } + openDirectory(path, isLocal: isLocal); + }); + return items.isEmpty + ? Offstage() + : BreadCrumb( + items: items, + divider: Text("/").paddingSymmetric(horizontal: 4.0), + overflow: ScrollableOverflow( + controller: getBreadCrumbScrollController(isLocal)), + ); } List getPathBreadCrumbItems( From a001b15335e53a8ea9ef7aa2c44c44c6975f8778 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 16 Aug 2022 13:28:48 +0800 Subject: [PATCH 0225/2015] feat: drop to send files to remote Signed-off-by: Kingtous --- .../lib/desktop/pages/file_manager_page.dart | 489 +++++++++--------- flutter/lib/models/file_model.dart | 1 + flutter/pubspec.lock | 9 +- flutter/pubspec.yaml | 1 + 4 files changed, 268 insertions(+), 232 deletions(-) diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index fc20d5277..0111e5f90 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'dart:math'; +import 'package:desktop_drop/desktop_drop.dart'; import 'package:flutter/material.dart'; import 'package:flutter_breadcrumb/flutter_breadcrumb.dart'; import 'package:flutter_hbb/mobile/pages/file_manager_page.dart'; @@ -39,6 +40,8 @@ class _FileManagerPageState extends State final _breadCrumbScrollerLocal = ScrollController(); final _breadCrumbScrollerRemote = ScrollController(); + final _dropMaskVisible = false.obs; + ScrollController getBreadCrumbScrollController(bool isLocal) { return isLocal ? _breadCrumbScrollerLocal : _breadCrumbScrollerRemote; } @@ -155,243 +158,248 @@ class _FileManagerPageState extends State decoration: BoxDecoration(border: Border.all(color: Colors.black26)), margin: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(8.0), - child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - headTools(isLocal), - Expanded( - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: SingleChildScrollView( - child: entries.isEmpty - ? Offstage() - : Obx( - () { - final searchText = - isLocal ? _searchTextLocal : _searchTextRemote; - final filteredEntries = searchText.isEmpty - ? entries.where((element) { - if (searchText.isEmpty) { - return true; - } else { - return element.name - .contains(searchText.value); - } - }).toList(growable: false) - : entries; - return DataTable( - key: ValueKey(isLocal ? 0 : 1), - showCheckboxColumn: true, - dataRowHeight: 25, - headingRowHeight: 30, - columnSpacing: 8, - showBottomBorder: true, - sortColumnIndex: sortIndex, - sortAscending: sortAscending, - columns: [ - DataColumn(label: Text(translate(" "))), // icon - DataColumn( - label: Text( - translate("Name"), - ), - 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 sizeStr = entry.isFile - ? readableFileSize(entry.size.toDouble()) - : ""; - return DataRow( - key: ValueKey(entry.name), - onSelectChanged: (s) { - if (s != null) { - if (s) { - getSelectedItem(isLocal).add(isLocal, entry); - } else { - getSelectedItem(isLocal).remove(entry); - } - setState(() {}); + child: DropTarget( + onDragDone: (detail) => handleDragDone(detail, isLocal), + onDragEntered: (enter) { + _dropMaskVisible.value = true; + }, + onDragExited: (exit) { + _dropMaskVisible.value = false; + }, + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + headTools(isLocal), + Expanded( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: SingleChildScrollView( + child: ObxValue( + (searchText) { + final filteredEntries = searchText.isEmpty + ? entries.where((element) { + if (searchText.isEmpty) { + return true; + } else { + return element.name.contains(searchText.value); } - }, - selected: getSelectedItem(isLocal).contains(entry), - cells: [ - DataCell(Icon( - entry.isFile - ? Icons.feed_outlined - : Icons.folder, - size: 25)), - DataCell( - ConstrainedBox( - constraints: - BoxConstraints(maxWidth: 100), - child: Tooltip( - message: entry.name, - child: Text(entry.name, - overflow: TextOverflow.ellipsis), - )), onTap: () { - if (entry.isDirectory) { - openDirectory(entry.path, isLocal: isLocal); - if (isLocal) { - _localSelectedItems.clear(); + }).toList(growable: false) + : entries; + return DataTable( + key: ValueKey(isLocal ? 0 : 1), + showCheckboxColumn: true, + dataRowHeight: 25, + headingRowHeight: 30, + columnSpacing: 8, + showBottomBorder: true, + sortColumnIndex: sortIndex, + sortAscending: sortAscending, + columns: [ + DataColumn(label: Text(translate(" "))), // icon + DataColumn( + label: Text( + translate("Name"), + ), + 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 sizeStr = entry.isFile + ? readableFileSize(entry.size.toDouble()) + : ""; + return DataRow( + key: ValueKey(entry.name), + onSelectChanged: (s) { + if (s != null) { + if (s) { + getSelectedItem(isLocal) + .add(isLocal, entry); } else { - _remoteSelectedItems.clear(); - } - } else { - // Perform file-related tasks. - final _selectedItems = - getSelectedItem(isLocal); - if (_selectedItems.contains(entry)) { - _selectedItems.remove(entry); - } else { - _selectedItems.add(isLocal, entry); + getSelectedItem(isLocal).remove(entry); } setState(() {}); } - }), - DataCell(Text( - entry - .lastModified() - .toString() - .replaceAll(".000", "") + - " ", - style: TextStyle( - fontSize: 12, - color: MyTheme.darkGray), - )), - DataCell(Text( - sizeStr, - style: TextStyle( - fontSize: 12, - color: MyTheme.darkGray), - )), - ]); - }).toList(growable: false), - ); - }, + }, + selected: + getSelectedItem(isLocal).contains(entry), + cells: [ + DataCell(Icon( + entry.isFile + ? Icons.feed_outlined + : Icons.folder, + size: 25)), + DataCell( + ConstrainedBox( + constraints: + BoxConstraints(maxWidth: 100), + child: Tooltip( + message: entry.name, + child: Text(entry.name, + overflow: TextOverflow.ellipsis), + )), onTap: () { + if (entry.isDirectory) { + openDirectory(entry.path, isLocal: isLocal); + if (isLocal) { + _localSelectedItems.clear(); + } else { + _remoteSelectedItems.clear(); + } + } else { + // Perform file-related tasks. + final _selectedItems = + getSelectedItem(isLocal); + if (_selectedItems.contains(entry)) { + _selectedItems.remove(entry); + } else { + _selectedItems.add(isLocal, entry); + } + setState(() {}); + } + }), + DataCell(Text( + entry + .lastModified() + .toString() + .replaceAll(".000", "") + + " ", + style: TextStyle( + fontSize: 12, color: MyTheme.darkGray), + )), + DataCell(Text( + sizeStr, + style: TextStyle( + fontSize: 12, color: MyTheme.darkGray), + )), + ]); + }).toList(growable: false), + ); + }, + isLocal ? _searchTextLocal : _searchTextRemote, + ), ), - ), - ) - ], - )), - // Center(child: listTail(isLocal: isLocal)), - // Expanded( - // child: ListView.builder( - // itemCount: entries.length + 1, - // itemBuilder: (context, index) { - // if (index >= entries.length) { - // return listTail(isLocal: isLocal); - // } - // var selected = false; - // if (model.selectMode) { - // selected = _selectedItems.contains(entries[index]); - // } - // - // final sizeStr = entries[index].isFile - // ? readableFileSize(entries[index].size.toDouble()) - // : ""; - // return Card( - // child: ListTile( - // leading: Icon( - // entries[index].isFile ? Icons.feed_outlined : Icons.folder, - // size: 40), - // title: Text(entries[index].name), - // selected: selected, - // subtitle: Text( - // entries[index] - // .lastModified() - // .toString() - // .replaceAll(".000", "") + - // " " + - // sizeStr, - // style: TextStyle(fontSize: 12, color: MyTheme.darkGray), - // ), - // trailing: needShowCheckBox() - // ? Checkbox( - // value: selected, - // onChanged: (v) { - // if (v == null) return; - // if (v && !selected) { - // _selectedItems.add(isLocal, entries[index]); - // } else if (!v && selected) { - // _selectedItems.remove(entries[index]); - // } - // setState(() {}); - // }) - // : PopupMenuButton( - // icon: Icon(Icons.more_vert), - // itemBuilder: (context) { - // return [ - // PopupMenuItem( - // child: Text(translate("Delete")), - // value: "delete", - // ), - // PopupMenuItem( - // child: Text(translate("Multi Select")), - // value: "multi_select", - // ), - // PopupMenuItem( - // child: Text(translate("Properties")), - // value: "properties", - // enabled: false, - // ) - // ]; - // }, - // onSelected: (v) { - // if (v == "delete") { - // final items = SelectedItems(); - // items.add(isLocal, entries[index]); - // model.removeAction(items); - // } else if (v == "multi_select") { - // _selectedItems.clear(); - // model.toggleSelectMode(); - // } - // }), - // onTap: () { - // if (model.selectMode && !_selectedItems.isOtherPage(isLocal)) { - // if (selected) { - // _selectedItems.remove(entries[index]); - // } else { - // _selectedItems.add(isLocal, entries[index]); - // } - // setState(() {}); - // return; - // } - // if (entries[index].isDirectory) { - // openDirectory(entries[index].path, isLocal: isLocal); - // breadCrumbScrollToEnd(isLocal); - // } else { - // // Perform file-related tasks. - // } - // }, - // onLongPress: () { - // _selectedItems.clear(); - // model.toggleSelectMode(); - // if (model.selectMode) { - // _selectedItems.add(isLocal, entries[index]); - // } - // setState(() {}); - // }, - // ), - // ); - // }, - // )) - ]), + ) + ], + )), + // Center(child: listTail(isLocal: isLocal)), + // Expanded( + // child: ListView.builder( + // itemCount: entries.length + 1, + // itemBuilder: (context, index) { + // if (index >= entries.length) { + // return listTail(isLocal: isLocal); + // } + // var selected = false; + // if (model.selectMode) { + // selected = _selectedItems.contains(entries[index]); + // } + // + // final sizeStr = entries[index].isFile + // ? readableFileSize(entries[index].size.toDouble()) + // : ""; + // return Card( + // child: ListTile( + // leading: Icon( + // entries[index].isFile ? Icons.feed_outlined : Icons.folder, + // size: 40), + // title: Text(entries[index].name), + // selected: selected, + // subtitle: Text( + // entries[index] + // .lastModified() + // .toString() + // .replaceAll(".000", "") + + // " " + + // sizeStr, + // style: TextStyle(fontSize: 12, color: MyTheme.darkGray), + // ), + // trailing: needShowCheckBox() + // ? Checkbox( + // value: selected, + // onChanged: (v) { + // if (v == null) return; + // if (v && !selected) { + // _selectedItems.add(isLocal, entries[index]); + // } else if (!v && selected) { + // _selectedItems.remove(entries[index]); + // } + // setState(() {}); + // }) + // : PopupMenuButton( + // icon: Icon(Icons.more_vert), + // itemBuilder: (context) { + // return [ + // PopupMenuItem( + // child: Text(translate("Delete")), + // value: "delete", + // ), + // PopupMenuItem( + // child: Text(translate("Multi Select")), + // value: "multi_select", + // ), + // PopupMenuItem( + // child: Text(translate("Properties")), + // value: "properties", + // enabled: false, + // ) + // ]; + // }, + // onSelected: (v) { + // if (v == "delete") { + // final items = SelectedItems(); + // items.add(isLocal, entries[index]); + // model.removeAction(items); + // } else if (v == "multi_select") { + // _selectedItems.clear(); + // model.toggleSelectMode(); + // } + // }), + // onTap: () { + // if (model.selectMode && !_selectedItems.isOtherPage(isLocal)) { + // if (selected) { + // _selectedItems.remove(entries[index]); + // } else { + // _selectedItems.add(isLocal, entries[index]); + // } + // setState(() {}); + // return; + // } + // if (entries[index].isDirectory) { + // openDirectory(entries[index].path, isLocal: isLocal); + // breadCrumbScrollToEnd(isLocal); + // } else { + // // Perform file-related tasks. + // } + // }, + // onLongPress: () { + // _selectedItems.clear(); + // model.toggleSelectMode(); + // if (model.selectMode) { + // _selectedItems.add(isLocal, entries[index]); + // } + // setState(() {}); + // }, + // ), + // ); + // }, + // )) + ]), + ), ); } @@ -831,4 +839,23 @@ class _FileManagerPageState extends State breadCrumbScrollToEnd(isLocal); }); } + + void handleDragDone(DropDoneDetails details, bool isLocal) { + if (isLocal) { + // ignore local + return; + } + var items = SelectedItems(); + details.files.forEach((file) { + final f = File(file.path); + items.add( + true, + Entry() + ..path = file.path + ..name = file.name + ..size = + FileSystemEntity.isDirectorySync(f.path) ? 0 : f.lengthSync()); + }); + model.sendFiles(items, isRemote: false); + } } diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index 1c3960b50..74c2cd515 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -732,6 +732,7 @@ class FileModel extends ChangeNotifier { job.totalSize = total_size.toInt(); } debugPrint("update folder files: ${info}"); + notifyListeners(); } bool get remoteSortAscending => _remoteSortAscending; diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index fe7359bf5..6ccfe72ac 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -239,6 +239,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.0.12" + desktop_drop: + dependency: "direct main" + description: + name: desktop_drop + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.3" desktop_multi_window: dependency: "direct main" description: @@ -817,7 +824,7 @@ packages: name: qr_code_scanner url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "1.0.1" quiver: dependency: transitive description: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index a911903f8..40aa1ca43 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -69,6 +69,7 @@ dependencies: get: ^4.6.5 visibility_detector: ^0.3.3 contextmenu: ^3.0.0 + desktop_drop: ^0.3.3 dev_dependencies: flutter_launcher_icons: ^0.9.1 From 1f0ba830dfb6f3e5458866f1b29dd353fa89b31d Mon Sep 17 00:00:00 2001 From: kordood Date: Tue, 16 Aug 2022 14:51:10 +0900 Subject: [PATCH 0226/2015] Fix a typo of locale name Signed-off-by: kordood --- src/lang.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang.rs b/src/lang.rs index 22085a34d..7157f9b42 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -92,7 +92,7 @@ pub fn translate_locale(name: String, locale: &str) -> String { "vn" => vn::T.deref(), "pl" => pl::T.deref(), "ja" => ja::T.deref(), - "ja" => ko::T.deref(), + "ko" => ko::T.deref(), _ => en::T.deref(), }; if let Some(v) = m.get(&name as &str) { From 82b72e5fdde6ce6a8190baee7b7cec2ff335d04c Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 16 Aug 2022 20:48:36 +0800 Subject: [PATCH 0227/2015] flutter_desktop: fullscreen ok Signed-off-by: fufesou --- .../desktop/pages/connection_tab_page.dart | 45 ++- flutter/lib/desktop/pages/remote_page.dart | 33 +- flutter/pubspec.lock | 342 +++++++++--------- 3 files changed, 230 insertions(+), 190 deletions(-) diff --git a/flutter/lib/desktop/pages/connection_tab_page.dart b/flutter/lib/desktop/pages/connection_tab_page.dart index a86afb683..2a831785e 100644 --- a/flutter/lib/desktop/pages/connection_tab_page.dart +++ b/flutter/lib/desktop/pages/connection_tab_page.dart @@ -27,6 +27,7 @@ class _ConnectionTabPageState extends State RxList tabs = RxList.empty(growable: true); late Rx tabController; static final Rx _selected = 0.obs; + static final Rx _fullscreenID = "".obs; IconData icon = Icons.desktop_windows_sharp; var connectionMap = RxList.empty(growable: true); @@ -70,24 +71,32 @@ class _ConnectionTabPageState extends State return Scaffold( body: Column( children: [ - DesktopTabBar( - controller: tabController, - tabs: tabs, - onTabClose: onRemoveId, - selected: _selected, - dark: isDarkTheme(), - mainTab: false, - ), - Expanded( - child: Obx(() => TabBarView( - controller: tabController.value, - children: tabs - .map((tab) => RemotePage( - key: ValueKey(tab.label), - id: tab.label, - tabBarHeight: kDesktopRemoteTabBarHeight, - )) //RemotePage(key: ValueKey(e), id: e)) - .toList()))), + Obx(() => Visibility( + visible: _fullscreenID.value.isEmpty, + child: DesktopTabBar( + controller: tabController, + tabs: tabs, + onTabClose: onRemoveId, + selected: _selected, + dark: isDarkTheme(), + mainTab: false, + ))), + Expanded(child: Obx(() { + WindowController.fromWindowId(windowId()) + .setFullscreen(_fullscreenID.value.isNotEmpty); + return TabBarView( + controller: tabController.value, + children: tabs + .map((tab) => RemotePage( + key: ValueKey(tab.label), + id: tab.label, + tabBarHeight: _fullscreenID.value.isNotEmpty + ? 0 + : kDesktopRemoteTabBarHeight, + fullscreenID: _fullscreenID, + )) //RemotePage(key: ValueKey(e), id: e)) + .toList()); + })), ], ), ); diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index e64d7a59a..eda38307f 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -9,6 +9,7 @@ import 'package:flutter_hbb/models/chat_model.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:wakelock/wakelock.dart'; +import 'package:desktop_multi_window/desktop_multi_window.dart'; // import 'package:window_manager/window_manager.dart'; @@ -21,11 +22,16 @@ import '../../models/platform_model.dart'; final initText = '\1' * 1024; class RemotePage extends StatefulWidget { - RemotePage({Key? key, required this.id, required this.tabBarHeight}) + RemotePage( + {Key? key, + required this.id, + required this.tabBarHeight, + required this.fullscreenID}) : super(key: key); final String id; final double tabBarHeight; + final Rx fullscreenID; @override _RemotePageState createState() => _RemotePageState(); @@ -41,6 +47,7 @@ class _RemotePageState extends State final FocusNode _mobileFocusNode = FocusNode(); final FocusNode _physicalFocusNode = FocusNode(); var _isPhysicalMouse = false; + var _imageFocused = false; late FFI _ffi; @@ -238,6 +245,9 @@ class _RemotePageState extends State autofocus: true, canRequestFocus: true, focusNode: _physicalFocusNode, + onFocusChange: (bool v) { + _imageFocused = v; + }, onKey: (data, e) { final key = e.logicalKey; if (e is RawKeyDownEvent) { @@ -307,6 +317,24 @@ class _RemotePageState extends State }, ) ] + + (isWebDesktop + ? [] + : [ + IconButton( + color: Colors.white, + icon: Icon(widget.fullscreenID.value.isEmpty + ? Icons.fullscreen + : Icons.close_fullscreen), + onPressed: () { + setState(() => _showEdit = false); + if (widget.fullscreenID.value.isEmpty) { + widget.fullscreenID.value = widget.id; + } else { + widget.fullscreenID.value = ""; + } + }, + ) + ]) + (isWebDesktop ? [] : _ffi.ffiModel.isPeerAndroid @@ -434,6 +462,9 @@ class _RemotePageState extends State onPointerSignal: _onPointerSignalImage, child: MouseRegion( onEnter: (evt) { + if (!_imageFocused) { + _physicalFocusNode.requestFocus(); + } _cursorOverImage.value = true; }, onExit: (evt) { diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 6ccfe72ac..f16f9516b 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -5,238 +5,238 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "46.0.0" after_layout: dependency: transitive description: name: after_layout - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.0" analyzer: dependency: transitive description: name: analyzer - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "4.6.0" animations: dependency: transitive description: name: animations - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.3" archive: dependency: transitive description: name: archive - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.3.1" args: dependency: transitive description: name: args - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.3.1" async: dependency: transitive description: name: async - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.8.2" back_button_interceptor: dependency: "direct main" description: name: back_button_interceptor - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "6.0.1" boolean_selector: dependency: transitive description: name: boolean_selector - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0" build: dependency: transitive description: name: build - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.3.0" build_config: dependency: transitive description: name: build_config - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.0" build_daemon: dependency: transitive description: name: build_daemon - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.0" build_resolvers: dependency: transitive description: name: build_resolvers - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.9" build_runner: dependency: "direct dev" description: name: build_runner - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.0" build_runner_core: dependency: transitive description: name: build_runner_core - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "7.2.3" built_collection: dependency: transitive description: name: built_collection - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "5.1.1" built_value: dependency: transitive description: name: built_value - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "8.4.0" cached_network_image: dependency: transitive description: name: cached_network_image - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.2.1" cached_network_image_platform_interface: dependency: transitive description: name: cached_network_image_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.0" cached_network_image_web: dependency: transitive description: name: cached_network_image_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.1" characters: dependency: transitive description: name: characters - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.0" charcode: dependency: transitive description: name: charcode - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.1" checked_yaml: dependency: transitive description: name: checked_yaml - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.1" clock: dependency: transitive description: name: clock - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.0" code_builder: dependency: transitive description: name: code_builder - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "4.2.0" collection: dependency: transitive description: name: collection - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.16.0" contextmenu: dependency: "direct main" description: name: contextmenu - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.0" convert: dependency: transitive description: name: convert - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.2" cross_file: dependency: transitive description: name: cross_file - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.3.3+1" crypto: dependency: transitive description: name: crypto - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.2" csslib: dependency: transitive description: name: csslib - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.17.2" cupertino_icons: dependency: "direct main" description: name: cupertino_icons - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.5" dart_style: dependency: transitive description: name: dart_style - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.3" dash_chat_2: dependency: "direct main" description: name: dash_chat_2 - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.0.12" desktop_drop: @@ -259,133 +259,133 @@ packages: dependency: "direct main" description: name: device_info_plus - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "4.1.2" device_info_plus_linux: dependency: transitive description: name: device_info_plus_linux - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.0" device_info_plus_macos: dependency: transitive description: name: device_info_plus_macos - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.0" device_info_plus_platform_interface: dependency: transitive description: name: device_info_plus_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.0" device_info_plus_web: dependency: transitive description: name: device_info_plus_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.0" device_info_plus_windows: dependency: transitive description: name: device_info_plus_windows - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "4.0.0" draggable_float_widget: dependency: "direct main" description: name: draggable_float_widget - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.0.2" event_bus: dependency: transitive description: name: event_bus - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.0" external_path: dependency: "direct main" description: name: external_path - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.1" fake_async: dependency: transitive description: name: fake_async - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.0" ffi: dependency: "direct main" description: name: ffi - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.1" file: dependency: transitive description: name: file - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "6.1.2" firebase_analytics: dependency: "direct main" description: name: firebase_analytics - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "9.3.1" firebase_analytics_platform_interface: dependency: transitive description: name: firebase_analytics_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.3.1" firebase_analytics_web: dependency: transitive description: name: firebase_analytics_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.4.2+1" firebase_core: dependency: transitive description: name: firebase_core - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.20.1" firebase_core_platform_interface: dependency: transitive description: name: firebase_core_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "4.5.0" firebase_core_web: dependency: transitive description: name: firebase_core_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.7.1" fixnum: dependency: transitive description: name: fixnum - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.1" flutter: @@ -397,42 +397,42 @@ packages: dependency: transitive description: name: flutter_blurhash - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.7.0" flutter_breadcrumb: dependency: "direct main" description: name: flutter_breadcrumb - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.1" flutter_cache_manager: dependency: transitive description: name: flutter_cache_manager - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.3.0" flutter_launcher_icons: dependency: "direct dev" description: name: flutter_launcher_icons - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.9.3" flutter_parsed_text: dependency: transitive description: name: flutter_parsed_text - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.1" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.7" flutter_rust_bridge: @@ -458,476 +458,476 @@ packages: dependency: "direct dev" description: name: freezed - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0+1" freezed_annotation: dependency: "direct main" description: name: freezed_annotation - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0" frontend_server_client: dependency: transitive description: name: frontend_server_client - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.3" get: dependency: "direct main" description: name: get - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "4.6.5" glob: dependency: transitive description: name: glob - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0" graphs: dependency: transitive description: name: graphs - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0" html: dependency: transitive description: name: html - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.15.0" http: dependency: "direct main" description: name: http - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.13.5" http_multi_server: dependency: transitive description: name: http_multi_server - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.2.1" http_parser: dependency: transitive description: name: http_parser - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "4.0.1" image: dependency: "direct main" description: name: image - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.2.0" image_picker: dependency: "direct main" description: name: image_picker - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.8.5+3" image_picker_android: dependency: transitive description: name: image_picker_android - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.8.5+2" image_picker_for_web: dependency: transitive description: name: image_picker_for_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.8" image_picker_ios: dependency: transitive description: name: image_picker_ios - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.8.5+6" image_picker_platform_interface: dependency: transitive description: name: image_picker_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.6.1" intl: dependency: transitive description: name: intl - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.17.0" io: dependency: transitive description: name: io - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.3" js: dependency: transitive description: name: js - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.6.4" json_annotation: dependency: transitive description: name: json_annotation - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "4.6.0" logging: dependency: transitive description: name: logging - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.2" matcher: dependency: transitive description: name: matcher - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.12.11" material_color_utilities: dependency: transitive description: name: material_color_utilities - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.1.4" menu_base: dependency: transitive description: name: menu_base - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.1.1" meta: dependency: transitive description: name: meta - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.7.0" mime: dependency: transitive description: name: mime - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.2" nested: dependency: transitive description: name: nested - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.0" octo_image: dependency: transitive description: name: octo_image - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.2" package_config: dependency: transitive description: name: package_config - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0" package_info_plus: dependency: "direct main" description: name: package_info_plus - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.4.3+1" package_info_plus_linux: dependency: transitive description: name: package_info_plus_linux - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.5" package_info_plus_macos: dependency: transitive description: name: package_info_plus_macos - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.0" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.2" package_info_plus_web: dependency: transitive description: name: package_info_plus_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.5" package_info_plus_windows: dependency: transitive description: name: package_info_plus_windows - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.0" path: dependency: transitive description: name: path - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.8.1" path_provider: dependency: "direct main" description: name: path_provider - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.11" path_provider_android: dependency: transitive description: name: path_provider_android - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.19" path_provider_ios: dependency: transitive description: name: path_provider_ios - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.11" path_provider_linux: dependency: transitive description: name: path_provider_linux - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.7" path_provider_macos: dependency: transitive description: name: path_provider_macos - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.6" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.4" path_provider_windows: dependency: transitive description: name: path_provider_windows - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.2" pedantic: dependency: transitive description: name: pedantic - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.11.1" petitparser: dependency: transitive description: name: petitparser - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "5.0.0" platform: dependency: transitive description: name: platform - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.0" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.2" pool: dependency: transitive description: name: pool - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.5.1" process: dependency: transitive description: name: process - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "4.2.4" provider: dependency: "direct main" description: name: provider - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "6.0.3" pub_semver: dependency: transitive description: name: pub_semver - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.1" pubspec_parse: dependency: transitive description: name: pubspec_parse - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.0" qr_code_scanner: dependency: "direct main" description: name: qr_code_scanner - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.1" quiver: dependency: transitive description: name: quiver - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.0" rxdart: dependency: transitive description: name: rxdart - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.27.5" screen_retriever: dependency: transitive description: name: screen_retriever - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.1.2" settings_ui: dependency: "direct main" description: name: settings_ui - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.2" shared_preferences: dependency: "direct main" description: name: shared_preferences - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.15" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.12" shared_preferences_ios: dependency: transitive description: name: shared_preferences_ios - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.1" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.1" shared_preferences_macos: dependency: transitive description: name: shared_preferences_macos - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.4" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.0" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.4" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.1" shelf: dependency: transitive description: name: shelf - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.2" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.2" shortid: dependency: transitive description: name: shortid - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.1.2" sky_engine: @@ -939,280 +939,280 @@ packages: dependency: transitive description: name: source_gen - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.2" source_span: dependency: transitive description: name: source_span - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.8.2" sqflite: dependency: transitive description: name: sqflite - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.3+1" sqflite_common: dependency: transitive description: name: sqflite_common - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.1+1" stack_trace: dependency: transitive description: name: stack_trace - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.10.0" stream_channel: dependency: transitive description: name: stream_channel - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0" stream_transform: dependency: transitive description: name: stream_transform - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.0" string_scanner: dependency: transitive description: name: string_scanner - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.0" synchronized: dependency: transitive description: name: synchronized - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.0+2" term_glyph: dependency: transitive description: name: term_glyph - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.0" test_api: dependency: transitive description: name: test_api - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.4.9" timing: dependency: transitive description: name: timing - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.0" toggle_switch: dependency: "direct main" description: name: toggle_switch - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.4.0" tray_manager: dependency: "direct main" description: name: tray_manager - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.1.7" tuple: dependency: "direct main" description: name: tuple - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.0" typed_data: dependency: transitive description: name: typed_data - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.1" url_launcher: dependency: "direct main" description: name: url_launcher - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "6.1.5" url_launcher_android: dependency: transitive description: name: url_launcher_android - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "6.0.17" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "6.0.17" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.1" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0" url_launcher_web: dependency: transitive description: name: url_launcher_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.13" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.1" uuid: dependency: transitive description: name: uuid - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.6" vector_math: dependency: transitive description: name: vector_math - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.2" video_player: dependency: transitive description: name: video_player - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.4.6" video_player_android: dependency: transitive description: name: video_player_android - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.3.8" video_player_avfoundation: dependency: transitive description: name: video_player_avfoundation - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.3.5" video_player_platform_interface: dependency: transitive description: name: video_player_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "5.1.4" video_player_web: dependency: transitive description: name: video_player_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.12" visibility_detector: dependency: "direct main" description: name: visibility_detector - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.3.3" wakelock: dependency: "direct main" description: name: wakelock - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.5.6" wakelock_macos: dependency: transitive description: name: wakelock_macos - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.4.0" wakelock_platform_interface: dependency: transitive description: name: wakelock_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.3.0" wakelock_web: dependency: transitive description: name: wakelock_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.4.0" wakelock_windows: dependency: transitive description: name: wakelock_windows - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.2.0" watcher: dependency: transitive description: name: watcher - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.1" web_socket_channel: dependency: transitive description: name: web_socket_channel - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.0" win32: dependency: transitive description: name: win32 - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.7.0" window_manager: @@ -1228,28 +1228,28 @@ packages: dependency: transitive description: name: xdg_directories - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.2.0+1" xml: dependency: transitive description: name: xml - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "6.1.0" yaml: dependency: transitive description: name: yaml - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.1" zxing2: dependency: "direct main" description: name: zxing2 - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.1.0" sdks: From 213e22e019f54f5217dd3e4e049152174302b633 Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 16 Aug 2022 23:07:22 +0800 Subject: [PATCH 0228/2015] flutter_desktop: fix chat message overflow Signed-off-by: fufesou --- flutter/lib/desktop/pages/remote_page.dart | 1 - flutter/lib/mobile/pages/chat_page.dart | 43 ++++++++++++---------- flutter/pubspec.yaml | 5 ++- 3 files changed, 27 insertions(+), 22 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index eda38307f..5b30668e8 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -9,7 +9,6 @@ import 'package:flutter_hbb/models/chat_model.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:wakelock/wakelock.dart'; -import 'package:desktop_multi_window/desktop_multi_window.dart'; // import 'package:window_manager/window_manager.dart'; diff --git a/flutter/lib/mobile/pages/chat_page.dart b/flutter/lib/mobile/pages/chat_page.dart index 0bc4c2a25..738f34e89 100644 --- a/flutter/lib/mobile/pages/chat_page.dart +++ b/flutter/lib/mobile/pages/chat_page.dart @@ -50,26 +50,29 @@ class ChatPage extends StatelessWidget implements PageShape { final currentUser = chatModel.currentUser; return Stack( children: [ - DashChat( - onSend: (chatMsg) { - chatModel.send(chatMsg); - }, - currentUser: chatModel.me, - messages: - chatModel.messages[chatModel.currentID]?.chatMessages ?? - [], - messageOptions: MessageOptions( - showOtherUsersAvatar: false, - showTime: true, - messageDecorationBuilder: (_, __, ___) => - defaultMessageDecoration( - color: MyTheme.accent80, - borderTopLeft: 8, - borderTopRight: 8, - borderBottomRight: 8, - borderBottomLeft: 8, - )), - ), + LayoutBuilder(builder: (context, constraints) { + return DashChat( + onSend: (chatMsg) { + chatModel.send(chatMsg); + }, + currentUser: chatModel.me, + messages: chatModel + .messages[chatModel.currentID]?.chatMessages ?? + [], + messageOptions: MessageOptions( + showOtherUsersAvatar: false, + showTime: true, + maxWidth: constraints.maxWidth * 0.7, + messageDecorationBuilder: (_, __, ___) => + defaultMessageDecoration( + color: MyTheme.accent80, + borderTopLeft: 8, + borderTopRight: 8, + borderBottomRight: 8, + borderBottomLeft: 8, + )), + ); + }), chatModel.currentID == ChatModel.clientModeID ? SizedBox.shrink() : Padding( diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 40aa1ca43..fcc7b5f49 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -40,7 +40,10 @@ dependencies: url_launcher: ^6.0.9 shared_preferences: ^2.0.6 toggle_switch: ^1.4.0 - dash_chat_2: ^0.0.12 + dash_chat_2: + git: + url: https://github.com/fufesou/Dash-Chat-2 + ref: feat_maxWidth draggable_float_widget: ^0.0.2 settings_ui: ^2.0.2 flutter_breadcrumb: ^1.0.1 From ddd6e302267b45b67d8a74b48382cdedd778c420 Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 16 Aug 2022 23:45:17 +0800 Subject: [PATCH 0229/2015] flutter_desktop: remove _showEdit Signed-off-by: fufesou --- flutter/lib/desktop/pages/remote_page.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 5b30668e8..f3996b31b 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -325,7 +325,6 @@ class _RemotePageState extends State ? Icons.fullscreen : Icons.close_fullscreen), onPressed: () { - setState(() => _showEdit = false); if (widget.fullscreenID.value.isEmpty) { widget.fullscreenID.value = widget.id; } else { From 53b69b59a8188d91b00d1a1a0451cd525d720265 Mon Sep 17 00:00:00 2001 From: csf Date: Tue, 16 Aug 2022 15:22:57 +0800 Subject: [PATCH 0230/2015] rename get_session -> session_get --- flutter/lib/common.dart | 2 +- flutter/lib/desktop/pages/remote_page.dart | 16 ++++++++-------- flutter/lib/mobile/pages/remote_page.dart | 14 +++++++------- flutter/lib/mobile/widgets/dialog.dart | 2 +- flutter/lib/models/model.dart | 8 ++++---- src/flutter_ffi.rs | 12 ++++++------ 6 files changed, 27 insertions(+), 27 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 93c151bee..0070d5294 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -482,7 +482,7 @@ RadioListTile getRadio( CheckboxListTile getToggle( String id, void Function(void Function()) setState, option, name, {FFI? ffi}) { - final opt = bind.getSessionToggleOptionSync(id: id, arg: option); + final opt = bind.sessionGetToggleOptionSync(id: id, arg: option); return CheckboxListTile( value: opt, onChanged: (v) { diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index f3996b31b..aa839fd01 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -490,7 +490,7 @@ class _RemotePageState extends State }), )) ]; - final cursor = bind.getSessionToggleOptionSync( + final cursor = bind.sessionGetToggleOptionSync( id: widget.id, arg: 'show-remote-cursor'); if (keyboard || cursor) { paints.add(CursorPaint( @@ -565,7 +565,7 @@ class _RemotePageState extends State more.add(PopupMenuItem( child: Text(translate('Insert Lock')), value: 'lock')); if (pi.platform == 'Windows' && - await bind.getSessionToggleOption(id: id, arg: 'privacy-mode') != + await bind.sessionGetToggleOption(id: id, arg: 'privacy-mode') != true) { more.add(PopupMenuItem( child: Text(translate( @@ -610,7 +610,7 @@ class _RemotePageState extends State // TODO icon diff // null means no session of id // empty string means no password - var password = await bind.getSessionOption(id: id, arg: "os-password"); + var password = await bind.sessionGetOption(id: id, arg: "os-password"); if (password != null) { bind.sessionInputOsPassword(id: widget.id, value: password); } else { @@ -837,12 +837,12 @@ class QualityMonitor extends StatelessWidget { void showOptions(String id) async { final _ffi = ffi(id); - String quality = await bind.getSessionImageQuality(id: id) ?? 'balanced'; + String quality = await bind.sessionGetImageQuality(id: id) ?? 'balanced'; if (quality == '') quality = 'balanced'; String viewStyle = - await bind.getSessionOption(id: id, arg: 'view-style') ?? ''; + await bind.sessionGetOption(id: id, arg: 'view-style') ?? ''; String scrollStyle = - await bind.getSessionOption(id: id, arg: 'scroll-style') ?? ''; + await bind.sessionGetOption(id: id, arg: 'scroll-style') ?? ''; var displays = []; final pi = _ffi.ffiModel.pi; final image = _ffi.ffiModel.getConnectionImage(); @@ -957,8 +957,8 @@ void showOptions(String id) async { void showSetOSPassword( String id, bool login, OverlayDialogManager dialogManager) async { final controller = TextEditingController(); - var password = await bind.getSessionOption(id: id, arg: "os-password") ?? ""; - var autoLogin = await bind.getSessionOption(id: id, arg: "auto-login") != ""; + var password = await bind.sessionGetOption(id: id, arg: "os-password") ?? ""; + var autoLogin = await bind.sessionGetOption(id: id, arg: "auto-login") != ""; controller.text = password; dialogManager.show((setState, close) { return CustomAlertDialog( diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 6a5be8b8d..8a4df6a9d 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -623,7 +623,7 @@ class _RemotePageState extends State { Widget getBodyForDesktopWithListener(bool keyboard) { var paints = [ImagePaint()]; - final cursor = bind.getSessionToggleOptionSync( + final cursor = bind.sessionGetToggleOptionSync( id: widget.id, arg: 'show-remote-cursor'); if (keyboard || cursor) { paints.add(CursorPaint()); @@ -694,7 +694,7 @@ class _RemotePageState extends State { more.add(PopupMenuItem( child: Text(translate('Insert Lock')), value: 'lock')); if (pi.platform == 'Windows' && - await bind.getSessionToggleOption(id: id, arg: 'privacy-mode') != + await bind.sessionGetToggleOption(id: id, arg: 'privacy-mode') != true) { more.add(PopupMenuItem( child: Text(translate((gFFI.ffiModel.inputBlocked ? 'Unb' : 'B') + @@ -738,7 +738,7 @@ class _RemotePageState extends State { // FIXME: // null means no session of id // empty string means no password - var password = await bind.getSessionOption(id: id, arg: "os-password"); + var password = await bind.sessionGetOption(id: id, arg: "os-password"); if (password != null) { bind.sessionInputOsPassword(id: widget.id, value: password); } else { @@ -1012,10 +1012,10 @@ class QualityMonitor extends StatelessWidget { } void showOptions(String id, OverlayDialogManager dialogManager) async { - String quality = await bind.getSessionImageQuality(id: id) ?? 'balanced'; + String quality = await bind.sessionGetImageQuality(id: id) ?? 'balanced'; if (quality == '') quality = 'balanced'; String viewStyle = - await bind.getSessionOption(id: id, arg: 'view-style') ?? ''; + await bind.sessionGetOption(id: id, arg: 'view-style') ?? ''; var displays = []; final pi = gFFI.ffiModel.pi; final image = gFFI.ffiModel.getConnectionImage(); @@ -1113,8 +1113,8 @@ void showOptions(String id, OverlayDialogManager dialogManager) async { void showSetOSPassword( String id, bool login, OverlayDialogManager dialogManager) async { final controller = TextEditingController(); - var password = await bind.getSessionOption(id: id, arg: "os-password") ?? ""; - var autoLogin = await bind.getSessionOption(id: id, arg: "auto-login") != ""; + var password = await bind.sessionGetOption(id: id, arg: "os-password") ?? ""; + var autoLogin = await bind.sessionGetOption(id: id, arg: "auto-login") != ""; controller.text = password; dialogManager.show((setState, close) { return CustomAlertDialog( diff --git a/flutter/lib/mobile/widgets/dialog.dart b/flutter/lib/mobile/widgets/dialog.dart index e0f98443b..82aed42a1 100644 --- a/flutter/lib/mobile/widgets/dialog.dart +++ b/flutter/lib/mobile/widgets/dialog.dart @@ -157,7 +157,7 @@ void setTemporaryPasswordLengthDialog( void enterPasswordDialog(String id, OverlayDialogManager dialogManager) async { final controller = TextEditingController(); - var remember = await bind.getSessionRemember(id: id) ?? false; + var remember = await bind.sessionGetRemember(id: id) ?? false; dialogManager.dismissAll(); dialogManager.show((setState, close) { return CustomAlertDialog( diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 18fe6a7f9..f3e47ff7f 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -312,7 +312,7 @@ class FfiModel with ChangeNotifier { } } else { _touchMode = - await bind.getSessionOption(id: peerId, arg: "touch-mode") != ''; + await bind.sessionGetOption(id: peerId, arg: "touch-mode") != ''; } if (evt['is_file_transfer'] == "true") { @@ -471,7 +471,7 @@ class CanvasModel with ChangeNotifier { double get tabBarHeight => _tabBarHeight; void updateViewStyle() async { - final style = await bind.getSessionOption(id: id, arg: 'view-style'); + final style = await bind.sessionGetOption(id: id, arg: 'view-style'); if (style == null) { return; } @@ -517,7 +517,7 @@ class CanvasModel with ChangeNotifier { } updateScrollStyle() async { - final style = await bind.getSessionOption(id: id, arg: 'scroll-style'); + final style = await bind.sessionGetOption(id: id, arg: 'scroll-style'); if (style == 'scrollbar') { _scrollStyle = ScrollStyle.scrollbar; _scrollX = 0.0; @@ -863,7 +863,7 @@ class QualityMonitorModel with ChangeNotifier { QualityMonitorData get data => _data; checkShowQualityMonitor(String id) async { - final show = await bind.getSessionToggleOption( + final show = await bind.sessionGetToggleOption( id: id, arg: 'show-quality-monitor') == true; if (_show != show) { diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 686111715..44d48ca8c 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -116,7 +116,7 @@ pub fn session_connect( Ok(()) } -pub fn get_session_remember(id: String) -> Option { +pub fn session_get_remember(id: String) -> Option { if let Some(session) = SESSIONS.read().unwrap().get(&id) { Some(session.get_remember()) } else { @@ -124,7 +124,7 @@ pub fn get_session_remember(id: String) -> Option { } } -pub fn get_session_toggle_option(id: String, arg: String) -> Option { +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)) } else { @@ -132,12 +132,12 @@ pub fn get_session_toggle_option(id: String, arg: String) -> Option { } } -pub fn get_session_toggle_option_sync(id: String, arg: String) -> SyncReturn { - let res = get_session_toggle_option(id, arg) == Some(true); +pub fn session_get_toggle_option_sync(id: String, arg: String) -> SyncReturn { + let res = session_get_toggle_option(id, arg) == Some(true); SyncReturn(res) } -pub fn get_session_image_quality(id: String) -> Option { +pub fn session_get_image_quality(id: String) -> Option { if let Some(session) = SESSIONS.read().unwrap().get(&id) { Some(session.get_image_quality()) } else { @@ -145,7 +145,7 @@ pub fn get_session_image_quality(id: String) -> Option { } } -pub fn get_session_option(id: String, arg: String) -> Option { +pub fn session_get_option(id: String, arg: String) -> Option { if let Some(session) = SESSIONS.read().unwrap().get(&id) { Some(session.get_option(&arg)) } else { From c9c40508e7c8770d81fb09e733b01876ef394f0d Mon Sep 17 00:00:00 2001 From: csf Date: Tue, 16 Aug 2022 17:14:59 +0800 Subject: [PATCH 0231/2015] add / remove favorite --- flutter/lib/desktop/widgets/peercard_widget.dart | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/flutter/lib/desktop/widgets/peercard_widget.dart b/flutter/lib/desktop/widgets/peercard_widget.dart index 5a5780431..e260ef391 100644 --- a/flutter/lib/desktop/widgets/peercard_widget.dart +++ b/flutter/lib/desktop/widgets/peercard_widget.dart @@ -195,6 +195,17 @@ class _PeerCardState extends State<_PeerCard> } else if (value == 'file') { _connect(id, isFileTransfer: true); } else if (value == 'add-fav') { + final favs = (await bind.mainGetFav()).toList(); + if (favs.indexOf(id) < 0) { + favs.add(id); + bind.mainStoreFav(favs: favs); + } + } else if (value == 'remove-fav') { + final favs = (await bind.mainGetFav()).toList(); + if (favs.remove(id)) { + bind.mainStoreFav(favs: favs); + Get.forceAppUpdate(); // TODO use inner model / state + } } else if (value == 'connect') { _connect(id, isFileTransfer: false); } else if (value == 'ab-delete') { @@ -425,6 +436,8 @@ class RecentPeerCard extends BasePeerCard { PopupMenuItem( child: Text(translate('Unremember Password')), value: 'unremember-password'), + PopupMenuItem( + child: Text(translate('Add to Favorites')), value: 'add-fav'), ]; } } @@ -469,6 +482,8 @@ class DiscoveredPeerCard extends BasePeerCard { PopupMenuItem( child: Text(translate('Unremember Password')), value: 'unremember-password'), + PopupMenuItem( + child: Text(translate('Add to Favorites')), value: 'add-fav'), ]; } } From ce050e250d9e2d3c1dec467209fa7844e522020f Mon Sep 17 00:00:00 2001 From: csf Date: Tue, 16 Aug 2022 21:27:21 +0800 Subject: [PATCH 0232/2015] desktop close connection tab (remote page) --- flutter/lib/common.dart | 7 ++++--- flutter/lib/desktop/pages/remote_page.dart | 2 +- .../lib/desktop/widgets/tabbar_widget.dart | 20 +++++++++++++++++++ .../lib/mobile/pages/file_manager_page.dart | 2 +- flutter/lib/mobile/pages/remote_page.dart | 2 +- flutter/lib/mobile/pages/scan_page.dart | 2 +- flutter/lib/mobile/widgets/dialog.dart | 6 +++--- flutter/lib/models/model.dart | 4 ++-- 8 files changed, 33 insertions(+), 12 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 0070d5294..43b925904 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; import 'package:get/instance_manager.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:window_manager/window_manager.dart'; @@ -66,11 +67,11 @@ final ButtonStyle flatButtonStyle = TextButton.styleFrom( ), ); -backToHomePage() { +closeConnection({String? id}) { if (isAndroid || isIOS) { Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/")); } else { - // TODO desktop + closeTab(id); } } @@ -306,7 +307,7 @@ void msgBox( 0, wrap(translate('OK'), () { dialogManager.dismissAll(); - backToHomePage(); + closeConnection(); })); } if (hasCancel == null) { diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index aa839fd01..8aba86d0f 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -60,7 +60,7 @@ class _RemotePageState extends State WidgetsBinding.instance.addPostFrameCallback((_) { SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); _ffi.dialogManager - .showLoading(translate('Connecting...'), onCancel: backToHomePage); + .showLoading(translate('Connecting...'), onCancel: closeConnection); }); if (!Platform.isLinux) { Wakelock.enable(); diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 3398ab33d..f8da7b429 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -10,6 +10,25 @@ const double _kTabBarHeight = kDesktopRemoteTabBarHeight; const double _kIconSize = 18; const double _kDividerIndent = 10; const double _kAddIconSize = _kTabBarHeight - 15; +final tabBarKey = GlobalKey(); + +void closeTab(String? id) { + final tabBar = tabBarKey.currentWidget as TabBar?; + if (tabBar == null) return; + final tabs = tabBar.tabs as List<_Tab>; + if (id == null) { + final current = tabBar.controller?.index; + if (current == null) return; + tabs[current].onClose(); + } else { + for (final tab in tabs) { + if (tab.label == id) { + tab.onClose(); + break; + } + } + } +} class TabInfo { late final String label; @@ -59,6 +78,7 @@ class DesktopTabBar extends StatelessWidget { ), Flexible( child: Obx(() => TabBar( + key: tabBarKey, indicatorColor: _theme.indicatorColor, labelPadding: const EdgeInsets.symmetric( vertical: 0, horizontal: 0), diff --git a/flutter/lib/mobile/pages/file_manager_page.dart b/flutter/lib/mobile/pages/file_manager_page.dart index c361e7b7c..87169b987 100644 --- a/flutter/lib/mobile/pages/file_manager_page.dart +++ b/flutter/lib/mobile/pages/file_manager_page.dart @@ -29,7 +29,7 @@ class _FileManagerPageState extends State { gFFI.connect(widget.id, isFileTransfer: true); WidgetsBinding.instance.addPostFrameCallback((_) { gFFI.dialogManager - .showLoading(translate('Connecting...'), onCancel: backToHomePage); + .showLoading(translate('Connecting...'), onCancel: closeConnection); }); gFFI.ffiModel.updateEventListener(widget.id); Wakelock.enable(); diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 8a4df6a9d..ceb3df0ff 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -51,7 +51,7 @@ class _RemotePageState extends State { WidgetsBinding.instance.addPostFrameCallback((_) { SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); gFFI.dialogManager - .showLoading(translate('Connecting...'), onCancel: backToHomePage); + .showLoading(translate('Connecting...'), onCancel: closeConnection); _interval = Timer.periodic(Duration(milliseconds: 30), (timer) => interval()); }); diff --git a/flutter/lib/mobile/pages/scan_page.dart b/flutter/lib/mobile/pages/scan_page.dart index 9f6c36ca8..2487c0f58 100644 --- a/flutter/lib/mobile/pages/scan_page.dart +++ b/flutter/lib/mobile/pages/scan_page.dart @@ -132,7 +132,7 @@ class _ScanPageState extends State { } void showServerSettingFromQr(String data) async { - backToHomePage(); + closeConnection(); await controller?.pauseCamera(); if (!data.startsWith('config=')) { showToast('Invalid QR code'); diff --git a/flutter/lib/mobile/widgets/dialog.dart b/flutter/lib/mobile/widgets/dialog.dart index 82aed42a1..d648cd497 100644 --- a/flutter/lib/mobile/widgets/dialog.dart +++ b/flutter/lib/mobile/widgets/dialog.dart @@ -184,7 +184,7 @@ void enterPasswordDialog(String id, OverlayDialogManager dialogManager) async { style: flatButtonStyle, onPressed: () { close(); - backToHomePage(); + closeConnection(); }, child: Text(translate('Cancel')), ), @@ -196,7 +196,7 @@ void enterPasswordDialog(String id, OverlayDialogManager dialogManager) async { gFFI.login(id, text, remember); close(); dialogManager.showLoading(translate('Logging in...'), - onCancel: backToHomePage); + onCancel: closeConnection); }, child: Text(translate('OK')), ), @@ -214,7 +214,7 @@ void wrongPasswordDialog(String id, OverlayDialogManager dialogManager) { style: flatButtonStyle, onPressed: () { close(); - backToHomePage(); + closeConnection(); }, child: Text(translate('Cancel')), ), diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index f3e47ff7f..dda22a779 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -287,7 +287,7 @@ class FfiModel with ChangeNotifier { bind.sessionReconnect(id: id); clearPermissions(); dialogManager.showLoading(translate('Connecting...'), - onCancel: backToHomePage); + onCancel: closeConnection); }); _reconnects *= 2; } else { @@ -335,7 +335,7 @@ class FfiModel with ChangeNotifier { if (displays.length > 0) { parent.target?.dialogManager.showLoading( translate('Connected, waiting for image...'), - onCancel: backToHomePage); + onCancel: closeConnection); _waitForImage = true; _reconnects = 1; } From 97614b3930596d928ceea77c2906e927c5b923a7 Mon Sep 17 00:00:00 2001 From: csf Date: Tue, 16 Aug 2022 22:15:45 +0800 Subject: [PATCH 0233/2015] ensure connection close --- flutter/lib/desktop/pages/connection_tab_page.dart | 1 + flutter/lib/desktop/pages/file_manager_tab_page.dart | 1 + 2 files changed, 2 insertions(+) diff --git a/flutter/lib/desktop/pages/connection_tab_page.dart b/flutter/lib/desktop/pages/connection_tab_page.dart index 2a831785e..eb8614dd4 100644 --- a/flutter/lib/desktop/pages/connection_tab_page.dart +++ b/flutter/lib/desktop/pages/connection_tab_page.dart @@ -104,6 +104,7 @@ class _ConnectionTabPageState extends State void onRemoveId(String id) { DesktopTabBar.onClose(this, tabController, tabs, id); + ffi(id).close(); if (tabs.length == 0) { WindowController.fromWindowId(windowId()).close(); } diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index 4c2dc3c5e..aa8c60afc 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -93,6 +93,7 @@ class _FileManagerTabPageState extends State void onRemoveId(String id) { DesktopTabBar.onClose(this, tabController, tabs, id); + ffi(id).close(); if (tabs.length == 0) { WindowController.fromWindowId(windowId()).close(); } From 845a524b827b06d8c3df7bb049a488f1108bda7f Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 15 Aug 2022 11:08:42 +0800 Subject: [PATCH 0234/2015] optimize settings ui Signed-off-by: 21pages --- flutter/lib/common.dart | 30 +- .../desktop/pages/desktop_setting_page.dart | 887 ++++++++++++------ .../lib/desktop/widgets/tabbar_widget.dart | 2 +- src/flutter_ffi.rs | 10 +- src/ui.rs | 23 +- src/ui_interface.rs | 7 + 6 files changed, 651 insertions(+), 308 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 93c151bee..cc75fafd5 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -527,14 +527,14 @@ String translate(String name) { return platformFFI.translate(name, localeName); } -bool option2bool(String key, String value) { +bool option2bool(String option, String value) { bool res; - if (key.startsWith("enable-")) { + if (option.startsWith("enable-")) { res = value != "N"; - } else if (key.startsWith("allow-") || - key == "stop-service" || - key == "direct-server" || - key == "stop-rendezvous-service") { + } else if (option.startsWith("allow-") || + option == "stop-service" || + option == "direct-server" || + option == "stop-rendezvous-service") { res = value == "Y"; } else { assert(false); @@ -543,18 +543,18 @@ bool option2bool(String key, String value) { return res; } -String bool2option(String key, bool option) { +String bool2option(String option, bool b) { String res; - if (key.startsWith('enable-')) { - res = option ? '' : 'N'; - } else if (key.startsWith('allow-') || - key == "stop-service" || - key == "direct-server" || - key == "stop-rendezvous-service") { - res = option ? 'Y' : ''; + if (option.startsWith('enable-')) { + res = b ? '' : 'N'; + } else if (option.startsWith('allow-') || + option == "stop-service" || + option == "direct-server" || + option == "stop-rendezvous-service") { + res = b ? 'Y' : ''; } else { assert(false); - res = option ? 'Y' : 'N'; + res = b ? 'Y' : 'N'; } return res; } diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 0da3dcc50..65c7ae819 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -1,8 +1,8 @@ import 'dart:convert'; -import 'dart:io' show Platform; import 'dart:ui'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart'; import 'package:flutter_hbb/models/platform_model.dart'; @@ -10,11 +10,28 @@ import 'package:flutter_hbb/models/server_model.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:url_launcher/url_launcher_string.dart'; -const double _kCardFixedWidth = 600; -const double _kCardLeftPadding = 20; -const double _kContentLeftPadding = 30; -const double _kListViewBottomPadding = 30; +const double _kTabWidth = 235; +const double _kTabHeight = 42; +const double _kCardFixedWidth = 560; +const double _kCardLeftMargin = 15; +const double _kContentHMargin = 15; +const double _kContentHSubMargin = _kContentHMargin + 33; +const double _kCheckBoxLeftMargin = 10; +const double _kRadioLeftMargin = 10; +const double _kListViewBottomMargin = 15; +const double _kTitleFontSize = 20; +const double _kContentFontSize = 15; +const Color _accentColor = MyTheme.accent; + +class _TabInfo { + late final int index; + late final String label; + late final IconData unselected; + late final IconData selected; + _TabInfo(this.index, this.label, this.unselected, this.selected); +} class DesktopSettingPage extends StatefulWidget { DesktopSettingPage({Key? key}) : super(key: key); @@ -25,19 +42,21 @@ class DesktopSettingPage extends StatefulWidget { class _DesktopSettingPageState extends State with TickerProviderStateMixin, AutomaticKeepAliveClientMixin { - final List _destinations = - [ - _destination('Display', Icons.palette_outlined, Icons.palette), - _destination( - 'Security', Icons.health_and_safety_outlined, Icons.health_and_safety), - _destination( - 'Connection', Icons.settings_remote_outlined, Icons.settings_remote), - _destination('Video', Icons.videocam_outlined, Icons.videocam), - _destination('Audio', Icons.volume_up_outlined, Icons.volume_up), + final List<_TabInfo> _setting_tabs = <_TabInfo>[ + _TabInfo( + 0, 'User Interface', Icons.language_outlined, Icons.language_sharp), + _TabInfo(1, 'Security', Icons.enhanced_encryption_outlined, + Icons.enhanced_encryption_sharp), + _TabInfo(2, 'Display', Icons.desktop_windows_outlined, + Icons.desktop_windows_sharp), + _TabInfo(3, 'Audio', Icons.volume_up_outlined, Icons.volume_up_sharp), + _TabInfo(4, 'Connection', Icons.link_outlined, Icons.link_sharp), ]; + final _TabInfo _about_tab = + _TabInfo(5, 'About RustDesk', Icons.info_outline, Icons.info_sharp); - late TabController controller; - int _selectedIndex = 0; + late PageController controller; + RxInt _selectedIndex = 0.obs; @override bool get wantKeepAlive => true; @@ -45,7 +64,7 @@ class _DesktopSettingPageState extends State @override void initState() { super.initState(); - controller = TabController(length: _destinations.length, vsync: this); + controller = PageController(); } @override @@ -54,27 +73,30 @@ class _DesktopSettingPageState extends State return Scaffold( body: Row( children: [ - NavigationRail( - selectedIndex: _selectedIndex, - onDestinationSelected: (int index) { - setState(() { - _selectedIndex = index; - }); - controller.animateTo(index); - }, - labelType: NavigationRailLabelType.all, - destinations: _destinations, + Container( + width: _kTabWidth, + child: Column( + children: [ + _header(), + Flexible(child: _listView(tabs: _setting_tabs)), + _listItem(tab: _about_tab), + SizedBox( + height: 120, + ) + ], + ), ), const VerticalDivider(thickness: 1, width: 1), Expanded( - child: TabBarView( + child: PageView( controller: controller, children: [ - _Display(), + _UserInterface(), _Safety(), - _Connection(), - _Video(), + _Display(), _Audio(), + _Connection(), + _About(), ], ), ) @@ -83,26 +105,81 @@ class _DesktopSettingPageState extends State ); } - static NavigationRailDestination _destination( - String label, IconData selected, IconData unSelected) { - return NavigationRailDestination( - icon: Icon(unSelected), - selectedIcon: Icon(selected), - label: Text(translate(label)), + Widget _header() { + return Row( + children: [ + SizedBox( + height: 62, + child: Text( + translate('Settings'), + textAlign: TextAlign.left, + style: TextStyle( + color: _accentColor, + fontSize: _kTitleFontSize, + fontWeight: FontWeight.w400, + ), + ), + ).marginOnly(left: 20, top: 10), + Spacer(), + ], ); } + + Widget _listView({required List<_TabInfo> tabs}) { + return ListView( + children: tabs.map((tab) => _listItem(tab: tab)).toList(), + ); + } + + Widget _listItem({required _TabInfo tab}) { + return Obx(() { + bool selected = tab.index == _selectedIndex.value; + return Container( + width: _kTabWidth, + height: _kTabHeight, + child: InkWell( + onTap: () { + if (_selectedIndex.value != tab.index) { + controller.jumpToPage(tab.index); + } + _selectedIndex.value = tab.index; + }, + child: Row(children: [ + Container( + width: 4, + height: _kTabHeight * 0.7, + color: selected ? _accentColor : null, + ), + Icon( + selected ? tab.selected : tab.unselected, + color: selected ? _accentColor : null, + size: 20, + ).marginOnly(left: 13, right: 10), + Text( + translate(tab.label), + style: TextStyle( + color: selected ? _accentColor : null, + fontWeight: FontWeight.w400, + fontSize: _kContentFontSize), + ), + ]), + ), + ); + }); + } } //#region pages -class _Display extends StatefulWidget { - _Display({Key? key}) : super(key: key); +class _UserInterface extends StatefulWidget { + _UserInterface({Key? key}) : super(key: key); @override - State<_Display> createState() => _DisplayState(); + State<_UserInterface> createState() => _UserInterfaceState(); } -class _DisplayState extends State<_Display> with AutomaticKeepAliveClientMixin { +class _UserInterfaceState extends State<_UserInterface> + with AutomaticKeepAliveClientMixin { @override bool get wantKeepAlive => true; @@ -111,9 +188,10 @@ class _DisplayState extends State<_Display> with AutomaticKeepAliveClientMixin { super.build(context); return ListView( children: [ - _Card(title: translate('Display'), children: [language(), theme()]), + _Card(title: 'Language', children: [language()]), + _Card(title: 'Theme', children: [theme()]), ], - ).paddingOnly(bottom: _kListViewBottomPadding); + ).marginOnly(bottom: _kListViewBottomMargin); } Widget language() { @@ -133,31 +211,35 @@ class _DisplayState extends State<_Display> with AutomaticKeepAliveClientMixin { if (!keys.contains(currentKey)) { currentKey = "default"; } - return _row( - 'Language', - _ComboBox( - keys: keys, - values: values, - initialKey: currentKey, - onChanged: (key) async { - await bind.mainSetLocalOption(key: "lang", value: key); - Get.forceAppUpdate(); - }, - )); + return _ComboBox( + keys: keys, + values: values, + initialKey: currentKey, + onChanged: (key) async { + await bind.mainSetLocalOption(key: "lang", value: key); + Get.forceAppUpdate(); + }, + ).marginOnly(left: _kContentHMargin); }); } Widget theme() { - return _row( - 'Dark Theme', - Switch( - value: isDarkTheme(), - onChanged: ((dark) async { - Get.changeTheme(dark ? MyTheme.darkTheme : MyTheme.lightTheme); - Get.find() - .setString("darkTheme", dark ? "Y" : ""); - Get.forceAppUpdate(); - }))); + var change = () { + bool dark = !isDarkTheme(); + Get.changeTheme(dark ? MyTheme.darkTheme : MyTheme.lightTheme); + Get.find().setString("darkTheme", dark ? "Y" : ""); + Get.forceAppUpdate(); + }; + + return GestureDetector( + child: Row( + children: [ + Checkbox(value: isDarkTheme(), onChanged: (_) => change()), + Expanded(child: Text(translate('Dark Theme'))), + ], + ).marginOnly(left: _kCheckBoxLeftMargin), + onTap: change, + ); } } @@ -181,17 +263,17 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { password(), whitelist(), ], - ).paddingOnly(bottom: _kListViewBottomPadding); + ).marginOnly(bottom: _kListViewBottomMargin); } Widget permissions() { return _Card(title: 'Permissions', children: [ - _option_check('Enable Keyboard/Mouse', 'enable-keyboard'), - _option_check('Enable Clipboard', 'enable-clipboard'), - _option_check('Enable File Transfer', 'enable-file-transfer'), - _option_check('Enable Audio', 'enable-audio'), - _option_check('Enable Remote Restart', 'enable-remote-restart'), - _option_check('Enable remote configuration modification', + _OptionCheckBox('Enable Keyboard/Mouse', 'enable-keyboard'), + _OptionCheckBox('Enable Clipboard', 'enable-clipboard'), + _OptionCheckBox('Enable File Transfer', 'enable-file-transfer'), + _OptionCheckBox('Enable Audio', 'enable-audio'), + _OptionCheckBox('Enable Remote Restart', 'enable-remote-restart'), + _OptionCheckBox('Enable remote configuration modification', 'allow-remote-config-modification'), ]); } @@ -199,45 +281,72 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { Widget password() { return ChangeNotifierProvider.value( value: gFFI.serverModel, - child: Consumer( - builder: ((context, model, child) => - _Card(title: 'Password', children: [ - _row( - 'Verification Method', - _ComboBox( - keys: [ - kUseTemporaryPassword, - kUsePermanentPassword, - kUseBothPasswords, - ], - values: [ - translate("Use temporary password"), - translate("Use permanent password"), - translate("Use both passwords"), - ], - initialKey: model.verificationMethod, - onChanged: (key) => model.verificationMethod = key)), - _row( - 'Temporary Password Length', - _ComboBox( - keys: ['6', '8', '10'], - values: ['6', '8', '10'], - initialKey: model.temporaryPasswordLength, - onChanged: (key) => model.temporaryPasswordLength = key, - enabled: - model.verificationMethod != kUsePermanentPassword, - )), - _button( - 'permanent_password_tip', - 'Set permanent password', - setPasswordDialog, - model.verificationMethod != kUseTemporaryPassword) - ])))); + child: Consumer(builder: ((context, model, child) { + List keys = [ + kUseTemporaryPassword, + kUsePermanentPassword, + kUseBothPasswords, + ]; + List values = [ + translate("Use temporary password"), + translate("Use permanent password"), + translate("Use both passwords"), + ]; + bool tmp_enabled = model.verificationMethod != kUsePermanentPassword; + bool perm_enabled = model.verificationMethod != kUseTemporaryPassword; + String currentValue = values[keys.indexOf(model.verificationMethod)]; + List radios = values + .map((value) => _Radio( + value: value, + groupValue: currentValue, + label: value, + onChanged: ((value) { + model.verificationMethod = keys[values.indexOf(value)]; + }))) + .toList(); + + var onChanged = tmp_enabled + ? (value) { + if (value != null) + model.temporaryPasswordLength = value.toString(); + } + : null; + List lengthRadios = ['6', '8', '10'] + .map((value) => GestureDetector( + child: Row( + children: [ + Radio( + value: value, + groupValue: model.temporaryPasswordLength, + onChanged: onChanged), + Text(value), + ], + ).paddingSymmetric(horizontal: 10), + onTap: () => onChanged?.call(value), + )) + .toList(); + + return _Card(title: 'Password', children: [ + radios[0], + _SubLabeledWidget( + 'Temporary Password Length', + Row( + children: [ + ...lengthRadios, + ], + ), + enabled: tmp_enabled), + radios[1], + _SubButton( + 'Set permanent password', setPasswordDialog, perm_enabled), + radios[2], + ]); + }))); } Widget whitelist() { return _Card(title: 'IP Whitelisting', children: [ - _button('whitelist_tip', 'IP Whitelisting', changeWhiteList) + _Button('IP Whitelisting', changeWhiteList, tip: 'whitelist_tip') ]); } } @@ -251,8 +360,6 @@ class _Connection extends StatefulWidget { class _ConnectionState extends State<_Connection> with AutomaticKeepAliveClientMixin { - final TextEditingController controller = TextEditingController(); - @override bool get wantKeepAlive => true; @@ -262,73 +369,94 @@ class _ConnectionState extends State<_Connection> return ListView( children: [ _Card(title: 'Server', children: [ - _button('self-hosting_tip', 'ID/Relay Server', changeServer), + _Button('ID/Relay Server', changeServer), ]), _Card(title: 'Service', children: [ - _option_check('Enable Service', 'stop-service', reverse: true), + _OptionCheckBox('Enable Service', 'stop-service', reverse: true), // TODO: Not implemented // _option_check('Always connected via relay', 'allow-always-relay'), // _option_check('Start ID/relay service', 'stop-rendezvous-service', // reverse: true), ]), _Card(title: 'TCP Tunneling', children: [ - _option_check('Enable TCP Tunneling', 'enable-tunnel'), + _OptionCheckBox('Enable TCP Tunneling', 'enable-tunnel'), ]), direct_ip(), _Card(title: 'Proxy', children: [ - _button('socks5_proxy_tip', 'Socks5 Proxy', changeSocks5Proxy), + _Button('Socks5 Proxy', changeSocks5Proxy), ]), ], - ).paddingOnly(bottom: _kListViewBottomPadding); + ).marginOnly(bottom: _kListViewBottomMargin); } Widget direct_ip() { + TextEditingController controller = TextEditingController(); var update = () => setState(() {}); + RxBool apply_enabled = false.obs; return _Card(title: 'Direct IP Access', children: [ - _option_check('Enable Direct IP Access', 'direct-server', update: update), - _row( - 'Port', - _futureBuilder( - future: () async { - String enabled = await bind.mainGetOption(key: 'direct-server'); - String port = await bind.mainGetOption(key: 'direct-access-port'); - return {'enabled': enabled, 'port': port}; - }(), - hasData: (data) { - bool enabled = - option2bool('direct-server', data['enabled'].toString()); - String port = data['port'].toString(); - int? iport = int.tryParse(port); - if (iport == null || iport < 1 || iport > 65535) { - port = ''; - } - controller.text = port; - return TextField( - controller: controller, - enabled: enabled, - onChanged: (value) async { - await bind.mainSetOption( - key: 'direct-access-port', value: controller.text); - }, - decoration: InputDecoration( - hintText: '21118', + _OptionCheckBox('Enable Direct IP Access', 'direct-server', + update: update), + _futureBuilder( + future: () async { + String enabled = await bind.mainGetOption(key: 'direct-server'); + String port = await bind.mainGetOption(key: 'direct-access-port'); + return {'enabled': enabled, 'port': port}; + }(), + hasData: (data) { + bool enabled = + option2bool('direct-server', data['enabled'].toString()); + if (!enabled) apply_enabled.value = false; + controller.text = data['port'].toString(); + return Row(children: [ + _SubLabeledWidget( + 'Port', + Container( + width: 80, + child: TextField( + controller: controller, + enabled: enabled, + onChanged: (_) => apply_enabled.value = true, + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp( + '\^([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])\$')), + ], + textAlign: TextAlign.end, + decoration: InputDecoration( + hintText: '21118', + border: InputBorder.none, + contentPadding: EdgeInsets.only(right: 5), + isCollapsed: true, + ), + ), ), - ); - }, - ), + enabled: enabled, + ), + Obx(() => ElevatedButton( + onPressed: apply_enabled.value && enabled + ? () async { + apply_enabled.value = false; + await bind.mainSetOption( + key: 'direct-access-port', + value: controller.text); + } + : null, + child: Text(translate('Apply')), + ).marginOnly(left: 20)) + ]); + }, ), ]); } } -class _Video extends StatefulWidget { - const _Video({Key? key}) : super(key: key); +class _Display extends StatefulWidget { + const _Display({Key? key}) : super(key: key); @override - State<_Video> createState() => _VideoState(); + State<_Display> createState() => _DisplayState(); } -class _VideoState extends State<_Video> with AutomaticKeepAliveClientMixin { +class _DisplayState extends State<_Display> with AutomaticKeepAliveClientMixin { @override bool get wantKeepAlive => true; @@ -338,10 +466,24 @@ class _VideoState extends State<_Video> with AutomaticKeepAliveClientMixin { return ListView( children: [ _Card(title: 'Adaptive Bitrate', children: [ - _option_check('Adaptive Bitrate', 'enable-abr'), + _OptionCheckBox('Adaptive Bitrate', 'enable-abr'), ]), + hwcodec(), ], - ).paddingOnly(bottom: _kListViewBottomPadding); + ).marginOnly(bottom: _kListViewBottomMargin); + } + + Widget hwcodec() { + return _futureBuilder( + future: bind.mainHasHwcodec(), + hasData: (data) { + return Offstage( + offstage: !(data as bool), + child: _Card(title: 'Hardware Codec', children: [ + _OptionCheckBox('Enable hardware codec', 'enable-hwcodec'), + ]), + ); + }); } } @@ -352,6 +494,12 @@ class _Audio extends StatefulWidget { State<_Audio> createState() => _AudioState(); } +enum _AudioInputType { + Mute, + Standard, + Specify, +} + class _AudioState extends State<_Audio> with AutomaticKeepAliveClientMixin { @override bool get wantKeepAlive => true; @@ -360,46 +508,161 @@ class _AudioState extends State<_Audio> with AutomaticKeepAliveClientMixin { Widget build(BuildContext context) { super.build(context); var update = () => setState(() {}); + var set_enabled = (bool enabled) => bind.mainSetOption( + key: 'enable-audio', value: bool2option('enable-audio', enabled)); + var set_device = (String device) => + bind.mainSetOption(key: 'audio-input', value: device); return ListView(children: [ _Card( title: 'Audio Input', children: [ - _option_check('Mute', 'enable-audio', reverse: true, update: update), - _row( - 'Audio device', - _futureBuilder(future: () async { - List all = await bind.mainGetSoundInputs(); - String current = await bind.mainGetOption(key: 'audio-input'); - String enabled = await bind.mainGetOption(key: 'enable-audio'); - return {'all': all, 'current': current, 'enabled': enabled}; - }(), hasData: (data) { - List keys = (data['all'] as List).toList(); - List values = keys.toList(); - if (Platform.isWindows) { - keys.insert(0, ''); - values.insert(0, 'System Sound'); - } else { - keys.insert(0, ''); // TODO - values.insert(0, 'None'); - } - String initialKey = data['current']; - if (!keys.contains(initialKey)) { - initialKey = ''; - } - return _ComboBox( - keys: keys, - values: values, - initialKey: initialKey, - onChanged: (key) { - bind.mainSetOption(key: 'audio-input', value: key); + _futureBuilder(future: () async { + List devices = await bind.mainGetSoundInputs(); + String current = await bind.mainGetOption(key: 'audio-input'); + String enabled = await bind.mainGetOption(key: 'enable-audio'); + return {'devices': devices, 'current': current, 'enabled': enabled}; + }(), hasData: (data) { + bool mute = + !option2bool('enable-audio', data['enabled'].toString()); + String currentDevice = data['current']; + List devices = (data['devices'] as List).toList(); + _AudioInputType groupValue; + if (mute) { + groupValue = _AudioInputType.Mute; + } else if (devices.contains(currentDevice)) { + groupValue = _AudioInputType.Specify; + } else { + groupValue = _AudioInputType.Standard; + } + List deviceWidget = [].toList(); + if (devices.isNotEmpty) { + var combo = _ComboBox( + keys: devices, + values: devices, + initialKey: devices.contains(currentDevice) + ? currentDevice + : devices[0], + onChanged: (key) { + set_device(key); + }, + enabled: groupValue == _AudioInputType.Specify, + ); + deviceWidget.addAll([ + _Radio<_AudioInputType>( + value: _AudioInputType.Specify, + groupValue: groupValue, + label: 'Specify device', + onChanged: (value) { + set_device(combo.current); + set_enabled(true); + update(); }, - enabled: - option2bool('enable-audio', data['enabled'].toString()), - ); - })), + ), + combo.marginOnly(left: _kContentHSubMargin, top: 5), + ]); + } + return Column(children: [ + _Radio<_AudioInputType>( + value: _AudioInputType.Mute, + groupValue: groupValue, + label: 'Mute', + onChanged: (value) { + set_enabled(false); + update(); + }, + ), + _Radio( + value: _AudioInputType.Standard, + groupValue: groupValue, + label: 'Use standard device', + onChanged: (value) { + set_device(''); + set_enabled(true); + update(); + }, + ), + ...deviceWidget, + ]); + }), ], ) - ]).paddingOnly(bottom: _kListViewBottomPadding); + ]).marginOnly(bottom: _kListViewBottomMargin); + } +} + +class _About extends StatefulWidget { + const _About({Key? key}) : super(key: key); + + @override + State<_About> createState() => _AboutState(); +} + +class _AboutState extends State<_About> { + @override + Widget build(BuildContext context) { + return _futureBuilder(future: () async { + final license = await bind.mainGetLicense(); + final version = await bind.mainGetVersion(); + return {'license': license, 'version': version}; + }(), hasData: (data) { + final license = data['license'].toString(); + final version = data['version'].toString(); + final linkStyle = TextStyle(decoration: TextDecoration.underline); + return ListView(children: [ + _Card(title: "About Rustdesk", children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: 8.0, + ), + Text("Version: $version").marginSymmetric(vertical: 4.0), + InkWell( + onTap: () { + launchUrlString("https://rustdesk.com/privacy"); + }, + child: Text( + "Privacy Statement", + style: linkStyle, + ).marginSymmetric(vertical: 4.0)), + InkWell( + onTap: () { + launchUrlString("https://rustdesk.com"); + }, + child: Text( + "Website", + style: linkStyle, + ).marginSymmetric(vertical: 4.0)), + Container( + decoration: BoxDecoration(color: Color(0xFF2c8cff)), + padding: EdgeInsets.symmetric(vertical: 24, horizontal: 8), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Copyright © 2022 Purslane Ltd.\n$license", + style: TextStyle(color: Colors.white), + ), + Text( + "Made with heart in this chaotic world!", + style: TextStyle( + fontWeight: FontWeight.w800, + color: Colors.white), + ) + ], + ), + ), + ], + ), + ).marginSymmetric(vertical: 4.0) + ], + ).marginOnly(left: _kContentHMargin) + ]), + ]).marginOnly(left: _kCardLeftMargin); + }); } } @@ -421,92 +684,155 @@ Widget _Card({required String title, required List children}) { translate(title), textAlign: TextAlign.start, style: TextStyle( - fontSize: 25, + fontSize: _kTitleFontSize, ), ), Spacer(), ], - ).paddingOnly(left: _kContentLeftPadding, top: 10, bottom: 20), - ...children.map((e) => e.paddingOnly(top: 2)), + ).marginOnly(left: _kContentHMargin, top: 10, bottom: 10), + ...children + .map((e) => e.marginOnly(top: 4, right: _kContentHMargin)), ], - ).paddingOnly(bottom: 10), - ).paddingOnly(left: _kCardLeftPadding, top: 20), + ).marginOnly(bottom: 10), + ).marginOnly(left: _kCardLeftMargin, top: 15), ), ], ); } -Widget _option_switch(String label, String key, +Widget _OptionCheckBox(String label, String key, {Function()? update = null, bool reverse = false}) { - return _row( - label, - _futureBuilder( - future: bind.mainGetOption(key: key), - hasData: (data) { - bool value = option2bool(key, data.toString()); - if (reverse) value = !value; - var ref = value.obs; - return Obx((() => Switch( - value: ref.value, - onChanged: ((option) async { - ref.value = option; - if (reverse) option = !option; - String value = bool2option(key, option); - bind.mainSetOption(key: key, value: value); - update?.call(); - })))); - })); + return _futureBuilder( + future: bind.mainGetOption(key: key), + hasData: (data) { + bool value = option2bool(key, data.toString()); + if (reverse) value = !value; + var ref = value.obs; + var onChanged = (option) async { + if (option != null) { + ref.value = option; + if (reverse) option = !option; + String value = bool2option(key, option); + bind.mainSetOption(key: key, value: value); + update?.call(); + } + }; + return GestureDetector( + child: Obx( + () => Row( + children: [ + Checkbox(value: ref.value, onChanged: onChanged) + .marginOnly(right: 10), + Expanded(child: Text(translate(label))) + ], + ), + ).marginOnly(left: _kCheckBoxLeftMargin), + onTap: () { + onChanged(!ref.value); + }, + ); + }); } -Widget _option_check(String label, String key, - {Function()? update = null, bool reverse = false}) { +Widget _Radio({ + required T value, + required T groupValue, + required String label, + required Function(T value) onChanged, +}) { + var on_change = (T? value) { + if (value != null) { + onChanged(value); + } + }; + return GestureDetector( + child: Row( + children: [ + Radio(value: value, groupValue: groupValue, onChanged: on_change), + Expanded( + child: Text(translate(label), + style: TextStyle(fontSize: _kContentFontSize)) + .marginOnly(left: 5), + ), + ], + ).marginOnly(left: _kRadioLeftMargin), + onTap: () => on_change(value), + ); +} + +Widget _Button(String label, Function() onPressed, + {bool enabled = true, String? tip}) { + var button = ElevatedButton( + onPressed: enabled ? onPressed : null, + child: Container( + child: Text( + translate(label), + ).marginSymmetric(horizontal: 15), + )); + var child; + if (tip == null) { + child = button; + } else { + child = Tooltip(message: translate(tip), child: button); + } return Row(children: [ - _futureBuilder( - future: bind.mainGetOption(key: key), - hasData: (data) { - bool value = option2bool(key, data.toString()); - if (reverse) value = !value; - var ref = value.obs; - return Obx((() => Checkbox( - value: ref.value, - onChanged: ((option) async { - if (option != null) { - ref.value = option; - if (reverse) option = !option; - String value = bool2option(key, option); - bind.mainSetOption(key: key, value: value); - update?.call(); - } - })))); - }).paddingOnly(right: 10), - Text(translate(label)), - ]).paddingOnly(left: _kContentLeftPadding); + child, + ]).marginOnly(left: _kContentHMargin); } -Widget _button(String tip, String label, Function() onPressed, - [bool enabled = true]) { - return _row( - translate(tip), - OutlinedButton( - onPressed: enabled ? onPressed : null, - child: Text( - translate(label), - ))); -} - -Widget _row(String label, Widget widget) { +Widget _SubButton(String label, Function() onPressed, [bool enabled = true]) { return Row( children: [ - Expanded( - child: Text( - translate(label), - )), - SizedBox( - width: 40, - ), - Expanded(child: widget), + ElevatedButton( + onPressed: enabled ? onPressed : null, + child: Container( + child: Text( + translate(label), + ).marginSymmetric(horizontal: 15), + )), ], - ).paddingSymmetric(horizontal: _kContentLeftPadding); + ).marginOnly(left: _kContentHSubMargin); +} + +Widget _SubLabeledWidget(String label, Widget child, {bool enabled = true}) { + RxBool hover = false.obs; + 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 + ? Colors.grey.withOpacity(0.8) + : Colors.grey.withOpacity(0.5), + width: hover.value && enabled ? 2 : 1)), + child: Row( + children: [ + Container( + height: 28, + color: (hover.value && enabled) + ? Colors.grey.withOpacity(0.8) + : Colors.grey.withOpacity(0.5), + child: Text( + label + ': ', + style: TextStyle(), + ), + alignment: Alignment.center, + padding: + EdgeInsets.symmetric(horizontal: 5, vertical: 2), + ).paddingAll(2), + child, + ], + )); + }, + )), + ], + ).marginOnly(left: _kContentHSubMargin); } Widget _futureBuilder( @@ -525,12 +851,14 @@ Widget _futureBuilder( }); } +// ignore: must_be_immutable class _ComboBox extends StatelessWidget { late final List keys; late final List values; late final String initialKey; late final Function(String key) onChanged; late final bool enabled; + late String current; _ComboBox({ Key? key, @@ -549,34 +877,41 @@ class _ComboBox extends StatelessWidget { index = 0; } var ref = values[index].obs; + current = keys[index]; return Container( - child: SizedBox( - child: Obx((() => DropdownButton( - isExpanded: true, - value: ref.value, - elevation: 16, - underline: Container( - height: 40, - ), - icon: Icon( - Icons.arrow_drop_down_sharp, - size: 35, - ), - onChanged: enabled - ? (String? newValue) { - if (newValue != null && newValue != ref.value) { - ref.value = newValue; - onChanged(keys[values.indexOf(newValue)]); - } - } - : null, - items: values.map>((String value) { - return DropdownMenuItem( - value: value, - child: Text(value), - ); - }).toList(), - )))), + decoration: BoxDecoration(border: Border.all(color: MyTheme.border)), + height: 30, + child: Obx(() => DropdownButton( + isExpanded: true, + value: ref.value, + elevation: 16, + underline: Container( + height: 25, + ), + icon: Icon( + Icons.expand_more_sharp, + size: 20, + ), + onChanged: enabled + ? (String? newValue) { + if (newValue != null && newValue != ref.value) { + ref.value = newValue; + current = newValue; + onChanged(keys[values.indexOf(newValue)]); + } + } + : null, + items: values.map>((String value) { + return DropdownMenuItem( + value: value, + child: Text( + value, + style: TextStyle(fontSize: _kContentFontSize), + overflow: TextOverflow.ellipsis, + ).marginOnly(left: 5), + ); + }).toList(), + )), ); } } diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 3398ab33d..0606271ab 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -63,7 +63,7 @@ class DesktopTabBar extends StatelessWidget { labelPadding: const EdgeInsets.symmetric( vertical: 0, horizontal: 0), isScrollable: true, - indicatorPadding: EdgeInsets.only(bottom: 2), + indicatorPadding: EdgeInsets.zero, physics: BouncingScrollPhysics(), controller: controller.value, tabs: tabs.asMap().entries.map((e) { diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 686111715..124429ee1 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -25,9 +25,9 @@ use crate::ui_interface::{ discover, forget_password, get_api_server, get_app_name, get_async_job_status, get_connect_status, get_fav, get_id, get_lan_peers, get_langs, get_license, get_local_option, get_option, get_options, get_peer, get_peer_option, get_socks, get_sound_inputs, get_uuid, - get_version, has_rendezvous_service, post_request, set_local_option, set_option, set_options, - set_peer_option, set_permanent_password, set_socks, store_fav, test_if_valid_server, - update_temporary_password, using_public_server, + get_version, has_hwcodec, has_rendezvous_service, post_request, set_local_option, set_option, + set_options, set_peer_option, set_permanent_password, set_socks, store_fav, + test_if_valid_server, update_temporary_password, using_public_server, }; fn initialize(app_dir: &str) { @@ -657,6 +657,10 @@ pub fn main_remove_peer(id: String) { PeerConfig::remove(&id); } +pub fn main_has_hwcodec() -> bool { + has_hwcodec() +} + // TODO pub fn session_send_mouse(id: String, msg: String) { if let Ok(m) = serde_json::from_str::>(&msg) { diff --git a/src/ui.rs b/src/ui.rs index 6484abbe5..1adc7c5ee 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -27,15 +27,15 @@ use crate::ui_interface::{ get_icon, get_lan_peers, get_langs, get_license, get_local_option, get_mouse_time, get_new_version, get_option, get_options, get_peer, get_peer_option, get_recent_sessions, get_remote_id, get_size, get_socks, get_software_ext, get_software_store_path, - get_software_update_url, get_uuid, get_version, goto_install, has_rendezvous_service, - install_me, install_path, is_can_screen_recording, is_installed, is_installed_daemon, - is_installed_lower_version, is_login_wayland, is_ok_change_id, is_process_trusted, - is_rdp_service_open, is_share_rdp, is_xfce, modify_default_login, new_remote, open_url, - peer_has_password, permanent_password, post_request, recent_sessions_updated, remove_peer, - run_without_install, set_local_option, set_option, set_options, set_peer_option, - set_permanent_password, set_remote_id, set_share_rdp, set_socks, show_run_without_install, - store_fav, t, temporary_password, test_if_valid_server, update_me, update_temporary_password, - using_public_server, + get_software_update_url, get_uuid, get_version, goto_install, has_hwcodec, + has_rendezvous_service, install_me, install_path, is_can_screen_recording, is_installed, + is_installed_daemon, is_installed_lower_version, is_login_wayland, is_ok_change_id, + is_process_trusted, is_rdp_service_open, is_share_rdp, is_xfce, modify_default_login, + new_remote, open_url, peer_has_password, permanent_password, post_request, + recent_sessions_updated, remove_peer, run_without_install, set_local_option, set_option, + set_options, set_peer_option, set_permanent_password, set_remote_id, set_share_rdp, set_socks, + show_run_without_install, store_fav, t, temporary_password, test_if_valid_server, update_me, + update_temporary_password, using_public_server, }; mod cm; @@ -541,10 +541,7 @@ impl UI { } fn has_hwcodec(&self) -> bool { - #[cfg(not(feature = "hwcodec"))] - return false; - #[cfg(feature = "hwcodec")] - return true; + has_hwcodec() } fn get_langs(&self) -> String { diff --git a/src/ui_interface.rs b/src/ui_interface.rs index b882507c9..d45b83b75 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -669,6 +669,13 @@ pub fn get_api_server() -> String { ) } +pub fn has_hwcodec() -> bool { + #[cfg(not(feature = "hwcodec"))] + return false; + #[cfg(feature = "hwcodec")] + return true; +} + pub fn check_zombie(childs: Childs) { let mut deads = Vec::new(); loop { From 3063adc2fde07c0317a2684a48b84f0d4d42b88e Mon Sep 17 00:00:00 2001 From: csf Date: Wed, 17 Aug 2022 17:23:55 +0800 Subject: [PATCH 0235/2015] add desktop cm backend --- flutter/lib/desktop/pages/server_page.dart | 3 +- flutter/lib/mobile/pages/server_page.dart | 2 +- flutter/lib/models/chat_model.dart | 2 +- flutter/lib/models/server_model.dart | 6 +- src/core_main.rs | 3 +- src/flutter.rs | 293 +++++++++++++++++---- src/flutter_ffi.rs | 25 +- src/ipc.rs | 155 +++++++++++ src/ui/cm.rs | 158 +---------- 9 files changed, 425 insertions(+), 222 deletions(-) diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index bf80bfbe7..e6c7d76bf 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -106,7 +106,6 @@ class DesktopServerPage extends StatefulWidget implements PageShape { } class _DesktopServerPageState extends State { - @override Widget build(BuildContext context) { return ChangeNotifierProvider.value( @@ -182,7 +181,7 @@ class ConnectionManager extends StatelessWidget { MaterialStateProperty.all(Colors.red)), icon: Icon(Icons.close), onPressed: () { - bind.serverCloseConnection(connId: entry.key); + bind.cmCloseConnection(connId: entry.key); gFFI.invokeMethod( "cancel_notification", entry.key); }, diff --git a/flutter/lib/mobile/pages/server_page.dart b/flutter/lib/mobile/pages/server_page.dart index f19a011b6..74e436ebb 100644 --- a/flutter/lib/mobile/pages/server_page.dart +++ b/flutter/lib/mobile/pages/server_page.dart @@ -409,7 +409,7 @@ class ConnectionManager extends StatelessWidget { MaterialStateProperty.all(Colors.red)), icon: Icon(Icons.close), onPressed: () { - bind.serverCloseConnection(connId: entry.key); + bind.cmCloseConnection(connId: entry.key); gFFI.invokeMethod( "cancel_notification", entry.key); }, diff --git a/flutter/lib/models/chat_model.dart b/flutter/lib/models/chat_model.dart index 9b9f70756..524701297 100644 --- a/flutter/lib/models/chat_model.dart +++ b/flutter/lib/models/chat_model.dart @@ -206,7 +206,7 @@ class ChatModel with ChangeNotifier { bind.sessionSendChat(id: _ffi.target!.id, text: message.text); } } else { - bind.serverSendChat(connId: _currentID, msg: message.text); + bind.cmSendChat(connId: _currentID, msg: message.text); } } notifyListeners(); diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index 3da823c09..e03d0f9d6 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -423,7 +423,7 @@ class ServerModel with ChangeNotifier { void sendLoginResponse(Client client, bool res) async { if (res) { - bind.serverLoginRes(connId: client.id, res: res); + bind.cmLoginRes(connId: client.id, res: res); if (!client.isFileTransfer) { parent.target?.invokeMethod("start_capture"); } @@ -431,7 +431,7 @@ class ServerModel with ChangeNotifier { _clients[client.id]?.authorized = true; notifyListeners(); } else { - bind.serverLoginRes(connId: client.id, res: res); + bind.cmLoginRes(connId: client.id, res: res); parent.target?.invokeMethod("cancel_notification", client.id); _clients.remove(client.id); } @@ -463,7 +463,7 @@ class ServerModel with ChangeNotifier { closeAll() { _clients.forEach((id, client) { - bind.serverCloseConnection(connId: id); + bind.cmCloseConnection(connId: id); }); _clients.clear(); } diff --git a/src/core_main.rs b/src/core_main.rs index 4e95f70ae..2603e000e 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -1,6 +1,6 @@ use hbb_common::log; -use crate::start_os_service; +use crate::{start_os_service, flutter::connection_manager}; /// Main entry of the RustDesk Core. /// Return true if the app should continue running with UI(possibly Flutter), false if the app should exit. @@ -11,6 +11,7 @@ pub fn core_main() -> bool { if args[1] == "--cm" { // call connection manager to establish connections // meanwhile, return true to call flutter window to show control panel + connection_manager::start_listen_ipc_thread(); return true; } if args[1] == "--service" { diff --git a/src/flutter.rs b/src/flutter.rs index b5553e475..928be607d 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -1,5 +1,5 @@ use std::{ - collections::{HashMap, VecDeque}, + collections::HashMap, sync::{ atomic::{AtomicBool, AtomicUsize, Ordering}, Arc, Mutex, RwLock, @@ -9,7 +9,7 @@ use std::{ use flutter_rust_bridge::{StreamSink, ZeroCopyBuffer}; use hbb_common::config::{PeerConfig, TransferSerde}; -use hbb_common::fs::{get_job, TransferJobMeta}; +use hbb_common::fs::get_job; use hbb_common::{ allow_err, compress::decompress, @@ -451,7 +451,6 @@ impl Session { key_event.set_chr(raw); } } - _ => {} } if alt { key_event.modifiers.push(ControlKey::Alt.into()); @@ -794,7 +793,7 @@ impl Connection { } if !conn.read_jobs.is_empty() { if let Err(err) = fs::handle_read_jobs(&mut conn.read_jobs, &mut peer).await { - log::debug!("Connection Error"); + log::debug!("Connection Error: {}", err); break; } conn.update_jobs_status(); @@ -915,7 +914,7 @@ impl Connection { Some(file_response::Union::Dir(fd)) => { let mut entries = fd.entries.to_vec(); if self.session.peer_platform() == "Windows" { - fs::transform_windows_path(&mut entries); + transform_windows_path(&mut entries); } let id = fd.id; self.session.push_event( @@ -1636,8 +1635,10 @@ pub mod connection_manager { use std::{ collections::HashMap, iter::FromIterator, - rc::{Rc, Weak}, - sync::{Mutex, RwLock}, + sync::{ + atomic::{AtomicI64, Ordering}, + RwLock, + }, }; use serde_derive::Serialize; @@ -1652,16 +1653,18 @@ pub mod connection_manager { protobuf::Message as _, tokio::{ self, - sync::mpsc::{UnboundedReceiver, UnboundedSender}, + sync::mpsc::{self, UnboundedReceiver, UnboundedSender}, task::spawn_blocking, }, }; #[cfg(any(target_os = "android"))] use scrap::android::call_main_service_set_by_name; - use crate::ipc; + #[cfg(windows)] + use crate::ipc::start_clipboard_file; + use crate::ipc::Data; - use crate::server::Connection as Conn; + use crate::ipc::{self, new_listener, Connection}; use super::GLOBAL_EVENT_STREAM; @@ -1681,76 +1684,184 @@ pub mod connection_manager { lazy_static::lazy_static! { static ref CLIENTS: RwLock> = Default::default(); - static ref WRITE_JOBS: Mutex> = Mutex::new(Vec::new()); } + static CLICK_TIME: AtomicI64 = AtomicI64::new(0); + + enum ClipboardFileData { + #[cfg(windows)] + Clip((i32, ipc::ClipbaordFile)), + Enable((i32, bool)), + } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + pub fn start_listen_ipc_thread() { + std::thread::spawn(move || start_ipc()); + } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + #[tokio::main(flavor = "current_thread")] + async fn start_ipc() { + let (tx_file, _rx_file) = mpsc::unbounded_channel::(); + #[cfg(windows)] + let cm_clip = cm.clone(); + #[cfg(windows)] + std::thread::spawn(move || start_clipboard_file(cm_clip, _rx_file)); + + #[cfg(windows)] + std::thread::spawn(move || { + log::info!("try create privacy mode window"); + #[cfg(windows)] + { + if let Err(e) = crate::platform::windows::check_update_broker_process() { + log::warn!( + "Failed to check update broker process. Privacy mode may not work properly. {}", + e + ); + } + } + allow_err!(crate::ui::win_privacy::start()); + }); + + match new_listener("_cm").await { + Ok(mut incoming) => { + while let Some(result) = incoming.next().await { + match result { + Ok(stream) => { + log::debug!("Got new connection"); + let mut stream = Connection::new(stream); + let tx_file = tx_file.clone(); + tokio::spawn(async move { + // for tmp use, without real conn id + let conn_id_tmp = -1; + let mut conn_id: i32 = 0; + let (tx, mut rx) = mpsc::unbounded_channel::(); + let mut write_jobs: Vec = Vec::new(); + loop { + tokio::select! { + res = stream.next() => { + match res { + Err(err) => { + log::info!("cm ipc connection closed: {}", err); + break; + } + Ok(Some(data)) => { + match data { + Data::Login{id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, file_transfer_enabled, restart} => { + log::debug!("conn_id: {}", id); + conn_id = id; + tx_file.send(ClipboardFileData::Enable((id, file_transfer_enabled))).ok(); + on_login(id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart, tx.clone()); + } + Data::Close => { + tx_file.send(ClipboardFileData::Enable((conn_id, false))).ok(); + log::info!("cm ipc connection closed from connection request"); + break; + } + Data::PrivacyModeState((_, _)) => { + conn_id = conn_id_tmp; + allow_err!(tx.send(data)); + } + Data::ClickTime(ms) => { + CLICK_TIME.store(ms, Ordering::SeqCst); + } + Data::ChatMessage { text } => { + handle_chat(conn_id, text); + } + Data::FS(fs) => { + handle_fs(fs, &mut write_jobs, &tx).await; + } + #[cfg(windows)] + Data::ClipbaordFile(_clip) => { + tx_file + .send(ClipboardFileData::Clip((id, _clip))) + .ok(); + } + #[cfg(windows)] + Data::ClipboardFileEnabled(enabled) => { + tx_file + .send(ClipboardFileData::Enable((id, enabled))) + .ok(); + } + _ => {} + } + } + _ => {} + } + } + Some(data) = rx.recv() => { + if stream.send(&data).await.is_err() { + break; + } + } + } + } + if conn_id != conn_id_tmp { + remove_connection(conn_id); + } + }); + } + Err(err) => { + log::error!("Couldn't get cm client: {:?}", err); + } + } + } + } + Err(err) => { + log::error!("Failed to start cm ipc server: {}", err); + } + } + // crate::platform::quit_gui(); + // TODO flutter quit_gui + } + + #[cfg(target_os = "android")] pub fn start_channel(rx: UnboundedReceiver, tx: UnboundedSender) { std::thread::spawn(move || start_listen(rx, tx)); } + #[cfg(target_os = "android")] #[tokio::main(flavor = "current_thread")] async fn start_listen(mut rx: UnboundedReceiver, tx: UnboundedSender) { let mut current_id = 0; + let mut write_jobs: Vec = Vec::new(); loop { match rx.recv().await { Some(Data::Login { id, is_file_transfer, + port_forward, peer_id, name, authorized, keyboard, clipboard, audio, + file, + restart, .. }) => { current_id = id; - let mut client = Client { + on_login( id, - authorized, is_file_transfer, - name: name.clone(), - peer_id: peer_id.clone(), + port_forward, + peer_id, + name, + authorized, keyboard, clipboard, audio, - tx: tx.clone(), - }; - if authorized { - client.authorized = true; - let client_json = serde_json::to_string(&client).unwrap_or("".into()); - // send to Android service,active notification no matter UI is shown or not. - #[cfg(any(target_os = "android"))] - if let Err(e) = call_main_service_set_by_name( - "on_client_authorized", - Some(&client_json), - None, - ) { - log::debug!("call_service_set_by_name fail,{}", e); - } - // send to UI,refresh widget - push_event("on_client_authorized", vec![("client", &client_json)]); - } else { - let client_json = serde_json::to_string(&client).unwrap_or("".into()); - // send to Android service,active notification no matter UI is shown or not. - #[cfg(any(target_os = "android"))] - if let Err(e) = call_main_service_set_by_name( - "try_start_without_auth", - Some(&client_json), - None, - ) { - log::debug!("call_service_set_by_name fail,{}", e); - } - // send to UI,refresh widget - push_event("try_start_without_auth", vec![("client", &client_json)]); - } - CLIENTS.write().unwrap().insert(id, client); + file, + restart, + tx.clone(), + ); } Some(Data::ChatMessage { text }) => { handle_chat(current_id, text); } Some(Data::FS(fs)) => { - handle_fs(fs, &tx).await; + handle_fs(fs, &mut write_jobs, &tx).await; } Some(Data::Close) => { break; @@ -1764,6 +1875,58 @@ pub mod connection_manager { remove_connection(current_id); } + fn on_login( + id: i32, + is_file_transfer: bool, + _port_forward: String, + peer_id: String, + name: String, + authorized: bool, + keyboard: bool, + clipboard: bool, + audio: bool, + _file: bool, + _restart: bool, + tx: mpsc::UnboundedSender, + ) { + let mut client = Client { + id, + authorized, + is_file_transfer, + name: name.clone(), + peer_id: peer_id.clone(), + keyboard, + clipboard, + audio, + tx, + }; + if authorized { + client.authorized = true; + let client_json = serde_json::to_string(&client).unwrap_or("".into()); + // send to Android service, active notification no matter UI is shown or not. + #[cfg(any(target_os = "android"))] + if let Err(e) = + call_main_service_set_by_name("on_client_authorized", Some(&client_json), None) + { + log::debug!("call_service_set_by_name fail,{}", e); + } + // send to UI, refresh widget + push_event("on_client_authorized", vec![("client", &client_json)]); + } else { + let client_json = serde_json::to_string(&client).unwrap_or("".into()); + // send to Android service, active notification no matter UI is shown or not. + #[cfg(any(target_os = "android"))] + if let Err(e) = + call_main_service_set_by_name("try_start_without_auth", Some(&client_json), None) + { + log::debug!("call_service_set_by_name fail,{}", e); + } + // send to UI, refresh widget + push_event("try_start_without_auth", vec![("client", &client_json)]); + } + CLIENTS.write().unwrap().insert(id, client); + } + fn push_event(name: &str, event: Vec<(&str, &str)>) { let mut h: HashMap<&str, &str> = event.iter().cloned().collect(); assert!(h.get("name").is_none()); @@ -1778,6 +1941,22 @@ pub mod connection_manager { }; } + pub fn get_click_time() -> i64 { + CLICK_TIME.load(Ordering::SeqCst) + } + + pub fn check_click_time(id: i32) { + if let Some(client) = CLIENTS.read().unwrap().get(&id) { + allow_err!(client.tx.send(Data::ClickTime(0))); + }; + } + + pub fn switch_permission(id: i32, name: String, enabled: bool) { + if let Some(client) = CLIENTS.read().unwrap().get(&id) { + allow_err!(client.tx.send(Data::SwitchPermission { name, enabled })); + }; + } + pub fn get_clients_state() -> String { let clients = CLIENTS.read().unwrap(); let res = Vec::from_iter(clients.values().cloned()); @@ -1790,7 +1969,7 @@ pub mod connection_manager { } pub fn close_conn(id: i32) { - if let Some(client) = CLIENTS.write().unwrap().get(&id) { + if let Some(client) = CLIENTS.read().unwrap().get(&id) { allow_err!(client.tx.send(Data::Close)); }; } @@ -1812,7 +1991,7 @@ pub mod connection_manager { if clients .iter() - .filter(|(k, v)| !v.is_file_transfer) + .filter(|(_k, v)| !v.is_file_transfer) .next() .is_none() { @@ -1835,14 +2014,18 @@ pub mod connection_manager { // server mode send chat to peer pub fn send_chat(id: i32, text: String) { - let mut clients = CLIENTS.read().unwrap(); + let clients = CLIENTS.read().unwrap(); if let Some(client) = clients.get(&id) { allow_err!(client.tx.send(Data::ChatMessage { text })); } } // handle FS server - async fn handle_fs(fs: ipc::FS, tx: &UnboundedSender) { + async fn handle_fs( + fs: ipc::FS, + write_jobs: &mut Vec, + tx: &UnboundedSender, + ) { match fs { ipc::FS::ReadDir { dir, @@ -1870,7 +2053,7 @@ pub mod connection_manager { mut files, overwrite_detection, } => { - WRITE_JOBS.lock().unwrap().push(fs::TransferJob::new_write( + write_jobs.push(fs::TransferJob::new_write( id, "".to_string(), path, @@ -1889,14 +2072,12 @@ pub mod connection_manager { )); } ipc::FS::CancelWrite { id } => { - let write_jobs = &mut *WRITE_JOBS.lock().unwrap(); if let Some(job) = fs::get_job(id, write_jobs) { job.remove_download_file(); fs::remove_job(id, write_jobs); } } ipc::FS::WriteDone { id, file_num } => { - let write_jobs = &mut *WRITE_JOBS.lock().unwrap(); if let Some(job) = fs::get_job(id, write_jobs) { job.modify_time(); send_raw(fs::new_done(id, file_num), tx); @@ -1909,7 +2090,7 @@ pub mod connection_manager { data, compressed, } => { - if let Some(job) = fs::get_job(id, &mut *WRITE_JOBS.lock().unwrap()) { + if let Some(job) = fs::get_job(id, write_jobs) { if let Err(err) = job .write( FileTransferBlock { @@ -1934,7 +2115,7 @@ pub mod connection_manager { last_modified, is_upload, } => { - if let Some(job) = fs::get_job(id, &mut *WRITE_JOBS.lock().unwrap()) { + if let Some(job) = fs::get_job(id, write_jobs) { let mut req = FileTransferSendConfirmRequest { id, file_num, diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 4557953f8..d3560ba4a 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -735,18 +735,37 @@ pub fn main_set_permanent_password(password: String) { set_permanent_password(password); } -pub fn server_send_chat(conn_id: i32, msg: String) { +pub fn cm_send_chat(conn_id: i32, msg: String) { connection_manager::send_chat(conn_id, msg); } -pub fn server_login_res(conn_id: i32, res: bool) { +pub fn cm_login_res(conn_id: i32, res: bool) { connection_manager::on_login_res(conn_id, res); } -pub fn server_close_connection(conn_id: i32) { +pub fn cm_close_connection(conn_id: i32) { connection_manager::close_conn(conn_id); } +pub fn cm_check_click_time(conn_id: i32) { + connection_manager::check_click_time(conn_id) +} + +pub fn cm_get_click_time() -> f64 { + connection_manager::get_click_time() as _ +} + +pub fn cm_switch_permission(conn_id: i32, name: String, enabled: bool) { + connection_manager::switch_permission(conn_id, name, enabled) +} + +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(); +} + #[no_mangle] unsafe extern "C" fn translate(name: *const c_char, locale: *const c_char) -> *const c_char { let name = CStr::from_ptr(name); diff --git a/src/ipc.rs b/src/ipc.rs index b85a35bd5..b98b0ad77 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -1,3 +1,7 @@ +#[cfg(windows)] +use clipboard::{ + create_cliprdr_context, empty_clipboard, get_rx_clip_client, server_clip_file, set_conn_enabled, +}; use std::{collections::HashMap, sync::atomic::Ordering}; #[cfg(not(windows))] use std::{fs::File, io::prelude::*}; @@ -413,6 +417,157 @@ pub async fn connect(ms_timeout: u64, postfix: &str) -> ResultType { + loop { + if let Some(result) = incoming.next().await { + match result { + Ok(stream) => { + let mut stream = Connection::new(stream); + let mut device: String = "".to_owned(); + if let Some(Ok(Some(Data::Config((_, Some(x)))))) = + stream.next_timeout2(1000).await + { + device = x; + } + if !device.is_empty() { + device = crate::platform::linux::get_pa_source_name(&device); + } + if device.is_empty() { + device = crate::platform::linux::get_pa_monitor(); + } + if device.is_empty() { + continue; + } + let spec = pulse::sample::Spec { + format: pulse::sample::Format::F32le, + channels: 2, + rate: crate::platform::PA_SAMPLE_RATE, + }; + log::info!("pa monitor: {:?}", device); + // systemctl --user status pulseaudio.service + let mut buf: Vec = vec![0; AUDIO_DATA_SIZE_U8]; + match psimple::Simple::new( + None, // Use the default server + &crate::get_app_name(), // Our application’s name + pulse::stream::Direction::Record, // We want a record stream + Some(&device), // Use the default device + "record", // Description of our stream + &spec, // Our sample format + None, // Use default channel map + None, // Use default buffering attributes + ) { + Ok(s) => loop { + if let Ok(_) = s.read(&mut buf) { + let out = + if buf.iter().filter(|x| **x != 0).next().is_none() { + vec![] + } else { + buf.clone() + }; + if let Err(err) = stream.send_raw(out.into()).await { + log::error!("Failed to send audio data:{}", err); + break; + } + } + }, + Err(err) => { + log::error!("Could not create simple pulse: {}", err); + } + } + } + Err(err) => { + log::error!("Couldn't get pa client: {:?}", err); + } + } + } + } + } + Err(err) => { + log::error!("Failed to start pa ipc server: {}", err); + } + } +} + +#[cfg(windows)] +#[tokio::main(flavor = "current_thread")] +pub async fn start_clipboard_file( + cm: ConnectionManager, + mut rx: mpsc::UnboundedReceiver, +) { + let mut cliprdr_context = None; + let mut rx_clip_client = get_rx_clip_client().lock().await; + + loop { + tokio::select! { + clip_file = rx_clip_client.recv() => match clip_file { + Some((conn_id, clip)) => { + cmd_inner_send( + &cm, + conn_id, + Data::ClipbaordFile(clip) + ); + } + None => { + // + } + }, + server_msg = rx.recv() => match server_msg { + Some(ClipboardFileData::Clip((conn_id, clip))) => { + if let Some(ctx) = cliprdr_context.as_mut() { + server_clip_file(ctx, conn_id, clip); + } + } + Some(ClipboardFileData::Enable((id, enabled))) => { + if enabled && cliprdr_context.is_none() { + cliprdr_context = Some(match create_cliprdr_context(true, false) { + Ok(context) => { + log::info!("clipboard context for file transfer created."); + context + } + Err(err) => { + log::error!( + "Create clipboard context for file transfer: {}", + err.to_string() + ); + return; + } + }); + } + set_conn_enabled(id, enabled); + if !enabled { + if let Some(ctx) = cliprdr_context.as_mut() { + empty_clipboard(ctx, id); + } + } + } + None => { + break + } + } + } + } +} + +#[cfg(windows)] +fn cmd_inner_send(cm: &ConnectionManager, id: i32, data: Data) { + let lock = cm.read().unwrap(); + if id != 0 { + if let Some(s) = lock.senders.get(&id) { + allow_err!(s.send(data)); + } + } else { + for s in lock.senders.values() { + allow_err!(s.send(data.clone())); + } + } +} + #[inline] #[cfg(not(windows))] fn get_pid_file(postfix: &str) -> String { diff --git a/src/ui/cm.rs b/src/ui/cm.rs index 38bfc9359..f1b4eaf72 100644 --- a/src/ui/cm.rs +++ b/src/ui/cm.rs @@ -1,9 +1,7 @@ -use crate::ipc::{self, new_listener, Connection, Data}; -use crate::VERSION; +use crate::ipc::{self, new_listener, Connection, Data, start_pa}; #[cfg(windows)] -use clipboard::{ - create_cliprdr_context, empty_clipboard, get_rx_clip_client, server_clip_file, set_conn_enabled, -}; +use crate::ipc::start_clipboard_file; +use crate::VERSION; use hbb_common::fs::{ can_enable_overwrite_detection, get_string, is_write_need_confirmation, new_send_confirm, DigestCheckResult, @@ -539,153 +537,3 @@ async fn start_ipc(cm: ConnectionManager) { crate::platform::quit_gui(); } -#[cfg(target_os = "linux")] -#[tokio::main(flavor = "current_thread")] -async fn start_pa() { - use crate::audio_service::AUDIO_DATA_SIZE_U8; - - match new_listener("_pa").await { - Ok(mut incoming) => { - loop { - if let Some(result) = incoming.next().await { - match result { - Ok(stream) => { - let mut stream = Connection::new(stream); - let mut device: String = "".to_owned(); - if let Some(Ok(Some(Data::Config((_, Some(x)))))) = - stream.next_timeout2(1000).await - { - device = x; - } - if !device.is_empty() { - device = crate::platform::linux::get_pa_source_name(&device); - } - if device.is_empty() { - device = crate::platform::linux::get_pa_monitor(); - } - if device.is_empty() { - continue; - } - let spec = pulse::sample::Spec { - format: pulse::sample::Format::F32le, - channels: 2, - rate: crate::platform::PA_SAMPLE_RATE, - }; - log::info!("pa monitor: {:?}", device); - // systemctl --user status pulseaudio.service - let mut buf: Vec = vec![0; AUDIO_DATA_SIZE_U8]; - match psimple::Simple::new( - None, // Use the default server - &crate::get_app_name(), // Our application’s name - pulse::stream::Direction::Record, // We want a record stream - Some(&device), // Use the default device - "record", // Description of our stream - &spec, // Our sample format - None, // Use default channel map - None, // Use default buffering attributes - ) { - Ok(s) => loop { - if let Ok(_) = s.read(&mut buf) { - let out = - if buf.iter().filter(|x| **x != 0).next().is_none() { - vec![] - } else { - buf.clone() - }; - if let Err(err) = stream.send_raw(out.into()).await { - log::error!("Failed to send audio data:{}", err); - break; - } - } - }, - Err(err) => { - log::error!("Could not create simple pulse: {}", err); - } - } - } - Err(err) => { - log::error!("Couldn't get pa client: {:?}", err); - } - } - } - } - } - Err(err) => { - log::error!("Failed to start pa ipc server: {}", err); - } - } -} - -#[cfg(windows)] -#[tokio::main(flavor = "current_thread")] -async fn start_clipboard_file( - cm: ConnectionManager, - mut rx: mpsc::UnboundedReceiver, -) { - let mut cliprdr_context = None; - let mut rx_clip_client = get_rx_clip_client().lock().await; - - loop { - tokio::select! { - clip_file = rx_clip_client.recv() => match clip_file { - Some((conn_id, clip)) => { - cmd_inner_send( - &cm, - conn_id, - Data::ClipbaordFile(clip) - ); - } - None => { - // - } - }, - server_msg = rx.recv() => match server_msg { - Some(ClipboardFileData::Clip((conn_id, clip))) => { - if let Some(ctx) = cliprdr_context.as_mut() { - server_clip_file(ctx, conn_id, clip); - } - } - Some(ClipboardFileData::Enable((id, enabled))) => { - if enabled && cliprdr_context.is_none() { - cliprdr_context = Some(match create_cliprdr_context(true, false) { - Ok(context) => { - log::info!("clipboard context for file transfer created."); - context - } - Err(err) => { - log::error!( - "Create clipboard context for file transfer: {}", - err.to_string() - ); - return; - } - }); - } - set_conn_enabled(id, enabled); - if !enabled { - if let Some(ctx) = cliprdr_context.as_mut() { - empty_clipboard(ctx, id); - } - } - } - None => { - break - } - } - } - } -} - -#[cfg(windows)] -fn cmd_inner_send(cm: &ConnectionManager, id: i32, data: Data) { - let lock = cm.read().unwrap(); - if id != 0 { - if let Some(s) = lock.senders.get(&id) { - allow_err!(s.send(data)); - } - } else { - for s in lock.senders.values() { - allow_err!(s.send(data.clone())); - } - } -} From dcab45d8ab7123755124596887ba2c8285082a61 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Wed, 17 Aug 2022 21:28:36 +0800 Subject: [PATCH 0236/2015] feat: cm ui Signed-off-by: Kingtous --- flutter/lib/cm_main.dart | 17 + flutter/lib/common.dart | 14 +- flutter/lib/desktop/pages/server_page.dart | 350 ++++++++++++++++----- flutter/lib/main.dart | 2 +- flutter/lib/models/server_model.dart | 6 +- 5 files changed, 308 insertions(+), 81 deletions(-) create mode 100644 flutter/lib/cm_main.dart diff --git a/flutter/lib/cm_main.dart b/flutter/lib/cm_main.dart new file mode 100644 index 000000000..584d74869 --- /dev/null +++ b/flutter/lib/cm_main.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/consts.dart'; +import 'package:flutter_hbb/main.dart'; +import 'package:get/get.dart'; +import 'package:window_manager/window_manager.dart'; + +import 'desktop/pages/server_page.dart'; + +/// -t lib/cm_main.dart to test cm +void main(List args) async { + WidgetsFlutterBinding.ensureInitialized(); + await windowManager.ensureInitialized(); + await initEnv(kAppTypeConnectionManager); + runApp(GetMaterialApp(theme: getCurrentTheme(), home: DesktopServerPage())); + await windowManager.setSize(Size(400, 600)); + await windowManager.setAlignment(Alignment.topRight); +} diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 7d3406aa1..20167aeb0 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1,6 +1,9 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:io'; +import 'dart:typed_data'; +import 'package:back_button_interceptor/back_button_interceptor.dart'; import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; @@ -8,7 +11,6 @@ import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; import 'package:get/instance_manager.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:window_manager/window_manager.dart'; -import 'package:back_button_interceptor/back_button_interceptor.dart'; import 'models/model.dart'; import 'models/platform_model.dart'; @@ -27,6 +29,15 @@ int androidVersion = 0; typedef F = String Function(String); typedef FMethod = String Function(String, dynamic); +final iconKeyboard = MemoryImage(Uint8List.fromList(base64Decode( + "iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAgVBMVEUAAAD///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////9d3yJTAAAAKnRSTlMA0Gd/0y8ILZgbJffDPUwV2nvzt+TMqZxyU7CMb1pYQyzsvKunkXE4AwJnNC24AAAA+0lEQVQ4y83O2U7DMBCF4ZMxk9rZk26kpQs7nPd/QJy4EiLbLf01N5Y/2YP/qxDFQvGB5NPC/ZpVnfJx4b5xyGfF95rkHvNCWH1u+N6J6T0sC7gqRy8uGPfBLEbozPXUjlkQKwGaFPNizwQbwkx0TDvhCii34ExZCSQVBdzIOEOyeclSHgBGXkpeygXSQgStACtWx4Z8rr8COHOvfEP/IbbsQAToFUAAV1M408IIjIGYAPoCSNRP7DQutfQTqxuAiH7UUg1FaJR2AGrrx52sK2ye28LZ0wBAEyR6y8X+NADhm1B4fgiiHXbRrTrxpwEY9RdM9wsepnvFHfUDwYEeiwAJr/gAAAAASUVORK5CYII="))); +final iconClipboard = MemoryImage(Uint8List.fromList(base64Decode( + 'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAjVBMVEUAAAD///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8DizOFAAAALnRSTlMAnIsyZy8YZF3NSAuabRL34cq6trCScyZ4qI9CQDwV+fPl2tnTwzkeB+m/pIFK/Xx0ewAAAQlJREFUOMudktduhDAQRWep69iY3tle0+7/f16Qg7MsJUQ5Dwh8jzRzhemJPIaf3GiW7eFQfOwDPp1ek/iMnKgBi5PrhJAhZAa1lCxE9pw5KWMswOMAQXuQOvqTB7tLFJ36wimKLrufZTzUaoRtdthqRA2vEwS+tR4qguiElRKk1YMrYfUQRkwLmwVBYDMvJKF8R0o3V2MOhNrfo+hXSYYjPn1L/S+n438t8gWh+q1F+cYFBMm1Jh8Ia7y2OWXQxMMRLqr2eTc1crSD84cWfEGwYM4LlaACEee2ZjsQXJxR3qmYb+GpC8ZfNM5oh3yxxbxgQE7lEkb3ZvvH1BiRHn1bu02ICcKGWr4AudUkyYxmvywAAAAASUVORK5CYII='))); +final iconAudio = MemoryImage(Uint8List.fromList(base64Decode( + 'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAk1BMVEUAAAD////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////ROyVeAAAAMHRSTlMAgfz08DDqCAThvraZjEcoGA751JxzbGdfTRP25NrIpaGTcEM+HAvMuKinhXhWNx9Yzm/gAAABFUlEQVQ4y82S2XLCMAxFheMsQNghCQFalkL39vz/11V4GpNk0r629+Va1pmxPFfyh1ravOP2Y1ydJmBO0lYP3r+PyQ62s2Y7fgF6VRXOYdToT++ogIuoVhCUtX7YpwJG3F8f6V8rr3WABwwUahlEvr8y3IBniGKdKYBQ5OGQpukQakBpIVcfwptIhJcf8hWGakdndAAhBInIGHbdQGJg6jjbDUgEE5EpmB+AAM4uj6gb+AQT6wdhITLvAHJ4VCtgoAlG1tpNA0gWON/f4ioHdSADc1bfgt+PZFkDlD6ojWF+kVoaHlhvFjPHuVRrefohY1GdcFm1N8JvwEyrJ/X2Th2rIoVgIi3Fo6Xf0z5k8psKu5f/oi+nHjjI92o36AAAAABJRU5ErkJggg=='))); +final iconFile = MemoryImage(Uint8List.fromList(base64Decode( + 'iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAAUVBMVEUAAAD///////////////////////////////////////////////////////////////////////////////////////////////////////8IN+deAAAAGnRSTlMAH+CAESEN8jyZkcIb5N/ONy3vmHhmiGjUm7UwS+YAAAHZSURBVGje7dnbboMwDIBhBwgQoFAO7Ta//4NOqCAXYZQstatq4r+r5ubrgQSpg8iyC4ZURa+PlIpQYGiwrzyeHtYZjAL8T05O4H8BbbKvFgRa4NoBU8pXeYEkDDgaaLQBcwJrmeErJQB/7wes3QBWGnCIX0+AQycL1PO6BMwPa0nA4ZxbgTvOjUYMGPHRnZkQAY4mxPZBjmy53E7ukSkFKYB/D4XsWZQx64sCeYebOogGsoOBYvv6/UCb8F0IOBZ0TlP6lEYdANY350AJqB9/qPVuOI5evw4A1hgLigAlepnyxW80bcCcwN++A2s82Vcu02ta+ceq9BoL5KGTTRwQPlpqA3gCnwWU2kCDgeWRQPj2jAPCDxgCMjhI6uZnToDpvd/BJeFrJQB/fsAa02gCt3mi1wNuy8GgBNDZlysBNNSrADVSjcJl6vCpUn6jOdx0kz0q6PMhQRa4465SFKhx35cgUCBTwj2/NHwZAb71qR8GEP2H1XcmAtBPTEO67GP6FUUAIKGABbDLQ0EArhN2sAIGesRO+iyy+RMAjckVTlMCKFVAbh/4Af9OPgG61SkDVco3BQGT3GXaDAnTIAcYZDuBTwGsAGDxuBFeAQqIqwoFMlAVLrHr/wId5MPt0nilGgAAAABJRU5ErkJggg=='))); + class MyTheme { MyTheme._(); @@ -39,6 +50,7 @@ class MyTheme { static const Color border = Color(0xFFCCCCCC); static const Color idColor = Color(0xFF00B6F0); static const Color darkGray = Color(0xFFB9BABC); + static const Color cmIdColor = Color(0xFF21790B); static const Color dark = Colors.black87; static ThemeData lightTheme = ThemeData( diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index e6c7d76bf..e399effc2 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:get/get.dart'; // import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:provider/provider.dart'; @@ -111,13 +112,12 @@ class _DesktopServerPageState extends State { return ChangeNotifierProvider.value( value: gFFI.serverModel, child: Consumer( - builder: (context, serverModel, child) => SingleChildScrollView( - controller: gFFI.serverModel.controller, + builder: (context, serverModel, child) => Material( child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.start, children: [ - ConnectionManager(), + Expanded(child: ConnectionManager()), SizedBox.fromSize(size: Size(0, 15.0)), ], ), @@ -130,81 +130,277 @@ class ConnectionManager extends StatelessWidget { @override Widget build(BuildContext context) { final serverModel = Provider.of(context); - return Column( - children: serverModel.clients.entries - .map((entry) => PaddingCard( - title: translate(entry.value.isFileTransfer - ? "File Connection" - : "Screen Connection"), - titleIcon: entry.value.isFileTransfer - ? Icons.folder_outlined - : Icons.mobile_screen_share, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded(child: clientInfo(entry.value)), - Expanded( - flex: -1, - child: entry.value.isFileTransfer || - !entry.value.authorized - ? SizedBox.shrink() - : IconButton( - onPressed: () { - gFFI.chatModel - .changeCurrentID(entry.value.id); - final bar = - navigationBarKey.currentWidget; - if (bar != null) { - bar as BottomNavigationBar; - bar.onTap!(1); - } - }, - icon: Icon( - Icons.chat, - color: MyTheme.accent80, - ))) - ], - ), - entry.value.authorized - ? SizedBox.shrink() - : Text( - translate("android_new_connection_tip"), - style: TextStyle(color: Colors.black54), - ), - entry.value.authorized - ? ElevatedButton.icon( - style: ButtonStyle( - backgroundColor: - MaterialStateProperty.all(Colors.red)), - icon: Icon(Icons.close), - onPressed: () { - bind.cmCloseConnection(connId: entry.key); - gFFI.invokeMethod( - "cancel_notification", entry.key); - }, - label: Text(translate("Close"))) - : Row(children: [ - TextButton( - child: Text(translate("Dismiss")), - onPressed: () { - serverModel.sendLoginResponse( - entry.value, false); - }), - SizedBox(width: 20), - ElevatedButton( - child: Text(translate("Accept")), - onPressed: () { - serverModel.sendLoginResponse( - entry.value, true); - }), - ]), - ], - ))) - .toList()); + // test case: + // serverModel.clients.clear(); + // serverModel.clients[0] = Client(false, false, "Readmi-M21sdfsdf", "123123123", true, false, false); + return DefaultTabController( + length: serverModel.clients.length, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: kTextTabBarHeight, + child: TabBar( + isScrollable: true, + tabs: serverModel.clients.entries + .map((entry) => buildTab(entry)) + .toList(growable: false)), + ), + Expanded( + child: TabBarView( + children: serverModel.clients.entries + .map((entry) => buildConnectionCard(entry)) + .toList(growable: false)), + ) + ], + ), + ); } + + Widget buildConnectionCard(MapEntry entry) { + final client = entry.value; + return Column( + children: [ + _CmHeader(client: client), + _PrivilegeBoard(client: client), + Expanded( + child: Align( + alignment: Alignment.bottomCenter, + child: _CmControlPanel(client: client), + )) + ], + ).paddingSymmetric(vertical: 8.0, horizontal: 8.0); + } + + Widget buildTab(MapEntry entry) { + return Tab( + child: Row( + children: [ + SizedBox( + width: 80, + child: Text( + "${entry.value.name}", + maxLines: 1, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + )), + ], + ), + ); + } +} + +class _CmHeader extends StatelessWidget { + final Client client; + + const _CmHeader({Key? key, required this.client}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // icon + Container( + width: 100, + height: 100, + alignment: Alignment.center, + decoration: BoxDecoration(color: str2color(client.name)), + child: Text( + "${client.name[0]}", + style: TextStyle( + fontWeight: FontWeight.bold, color: Colors.white, fontSize: 75), + ), + ).marginOnly(left: 4.0, right: 8.0), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "${client.name}", + style: TextStyle( + color: MyTheme.cmIdColor, + fontWeight: FontWeight.bold, + fontSize: 20, + overflow: TextOverflow.ellipsis, + ), + maxLines: 1, + ), + Text("(${client.peerId})", + style: TextStyle(color: MyTheme.cmIdColor, fontSize: 14)), + SizedBox( + height: 16.0, + ), + Offstage( + offstage: !client.authorized, + child: Row( + children: [ + Text("${translate("Connected")}"), + ], + )) + ], + ), + ), + Offstage( + offstage: client.isFileTransfer, + child: IconButton( + onPressed: handleSendMsg, + icon: Icon(Icons.message_outlined), + ), + ) + ], + ); + } + + void handleSendMsg() {} +} + +class _PrivilegeBoard extends StatelessWidget { + final Client client; + + const _PrivilegeBoard({Key? key, required this.client}) : super(key: key); + + Widget buildPermissionIcon(bool enabled, ImageProvider icon, + Function(bool)? onTap, String? tooltip) { + return Tooltip( + message: tooltip ?? "", + child: Ink( + decoration: + BoxDecoration(color: enabled ? MyTheme.accent80 : Colors.grey), + padding: EdgeInsets.all(4.0), + child: InkWell( + onTap: () => onTap?.call(!enabled), + child: Image( + image: icon, + width: 50, + height: 50, + fit: BoxFit.scaleDown, + ), + ), + ).marginSymmetric(horizontal: 4.0), + ); + } + + @override + Widget build(BuildContext context) { + return Container( + margin: EdgeInsets.only(top: 16.0, bottom: 8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + translate("Permissions"), + style: TextStyle(fontSize: 16), + ).marginOnly(left: 4.0), + SizedBox( + height: 8.0, + ), + Row( + children: [ + buildPermissionIcon( + client.keyboard, iconKeyboard, (enable) => null, null), + buildPermissionIcon( + client.clipboard, iconClipboard, (enable) => null, null), + buildPermissionIcon( + client.audio, iconAudio, (enable) => null, null), + // TODO: file transfer + buildPermissionIcon(false, iconFile, (enable) => null, null), + ], + ), + ], + ), + ); + } +} + +class _CmControlPanel extends StatelessWidget { + final Client client; + + const _CmControlPanel({Key? key, required this.client}) : super(key: key); + + @override + Widget build(BuildContext context) { + return client.authorized ? buildAuthorized() : buildUnAuthorized(); + } + + buildAuthorized() { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Ink( + width: 200, + height: 40, + decoration: BoxDecoration( + color: Colors.redAccent, borderRadius: BorderRadius.circular(10)), + child: InkWell( + onTap: handleDisconnect, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + translate("Disconnect"), + style: TextStyle(color: Colors.white), + ), + ], + )), + ) + ], + ); + } + + buildUnAuthorized() { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Ink( + width: 100, + height: 40, + decoration: BoxDecoration( + color: MyTheme.accent, borderRadius: BorderRadius.circular(10)), + child: InkWell( + onTap: handleAccept, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + translate("Accept"), + style: TextStyle(color: Colors.white), + ), + ], + )), + ), + SizedBox( + width: 30, + ), + Ink( + width: 100, + height: 40, + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular(10), + border: Border.all(color: Colors.grey)), + child: InkWell( + onTap: handleCancel, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + translate("Cancel"), + style: TextStyle(), + ), + ], + )), + ) + ], + ); + } + + void handleDisconnect() {} + + void handleCancel() {} + + void handleAccept() {} } class PaddingCard extends StatelessWidget { diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index d8586baad..7f3acc79f 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -49,6 +49,7 @@ Future main(List args) async { break; } } else if (args.isNotEmpty && args.first == '--cm') { + print("--cm started"); await windowManager.ensureInitialized(); runConnectionManagerScreen(); } else { @@ -117,7 +118,6 @@ void runFileTransferScreen(Map argument) async { void runConnectionManagerScreen() async { await initEnv(kAppTypeConnectionManager); - await windowManager.setAlwaysOnTop(true); await windowManager.setSize(Size(400, 600)); await windowManager.setAlignment(Alignment.topRight); runApp(GetMaterialApp(theme: getCurrentTheme(), home: DesktopServerPage())); diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index e03d0f9d6..e5465e1e3 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -97,8 +97,9 @@ class ServerModel with ChangeNotifier { } final res = await bind.mainCheckClientsLength(length: _clients.length); if (res != null) { - debugPrint("clients not match!"); - updateClientState(res); + // for test + // debugPrint("clients not match!"); + // updateClientState(res); } updatePasswordModel(); @@ -342,6 +343,7 @@ class ServerModel with ChangeNotifier { var res = await bind.mainGetClientsState(); try { final List clientsJson = jsonDecode(res); + _clients.clear(); for (var clientJson in clientsJson) { final client = Client.fromJson(clientJson); _clients[client.id] = client; From a580b984722f2e222619b15fab4e2c925582c73c Mon Sep 17 00:00:00 2001 From: Kingtous Date: Wed, 17 Aug 2022 21:46:56 +0800 Subject: [PATCH 0237/2015] feat: accpet/disconnect Signed-off-by: Kingtous --- flutter/lib/desktop/pages/server_page.dart | 65 +++++++++++++--------- flutter/lib/models/server_model.dart | 5 +- 2 files changed, 42 insertions(+), 28 deletions(-) diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index e399effc2..34a8f94c4 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -106,9 +106,11 @@ class DesktopServerPage extends StatefulWidget implements PageShape { State createState() => _DesktopServerPageState(); } -class _DesktopServerPageState extends State { +class _DesktopServerPageState extends State + with AutomaticKeepAliveClientMixin { @override Widget build(BuildContext context) { + super.build(context); return ChangeNotifierProvider.value( value: gFFI.serverModel, child: Consumer( @@ -124,6 +126,9 @@ class _DesktopServerPageState extends State { ), ))); } + + @override + bool get wantKeepAlive => true; } class ConnectionManager extends StatelessWidget { @@ -131,20 +136,25 @@ class ConnectionManager extends StatelessWidget { Widget build(BuildContext context) { final serverModel = Provider.of(context); // test case: - // serverModel.clients.clear(); - // serverModel.clients[0] = Client(false, false, "Readmi-M21sdfsdf", "123123123", true, false, false); - return DefaultTabController( - length: serverModel.clients.length, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - height: kTextTabBarHeight, - child: TabBar( - isScrollable: true, - tabs: serverModel.clients.entries - .map((entry) => buildTab(entry)) - .toList(growable: false)), + serverModel.clients.clear(); + serverModel.clients[0] = Client( + false, false, "Readmi-M21sdfsdf", "123123123", true, false, false); + return serverModel.clients.isEmpty + ? Center( + child: Text(translate("Waiting")), + ) + : DefaultTabController( + length: serverModel.clients.length, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: kTextTabBarHeight, + child: TabBar( + isScrollable: true, + tabs: serverModel.clients.entries + .map((entry) => buildTab(entry)) + .toList(growable: false)), ), Expanded( child: TabBarView( @@ -321,10 +331,12 @@ class _CmControlPanel extends StatelessWidget { @override Widget build(BuildContext context) { - return client.authorized ? buildAuthorized() : buildUnAuthorized(); + return client.authorized + ? buildAuthorized(context) + : buildUnAuthorized(context); } - buildAuthorized() { + buildAuthorized(BuildContext context) { return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -334,7 +346,7 @@ class _CmControlPanel extends StatelessWidget { decoration: BoxDecoration( color: Colors.redAccent, borderRadius: BorderRadius.circular(10)), child: InkWell( - onTap: handleDisconnect, + onTap: () => handleDisconnect(context), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -349,7 +361,7 @@ class _CmControlPanel extends StatelessWidget { ); } - buildUnAuthorized() { + buildUnAuthorized(BuildContext context) { return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -359,7 +371,7 @@ class _CmControlPanel extends StatelessWidget { decoration: BoxDecoration( color: MyTheme.accent, borderRadius: BorderRadius.circular(10)), child: InkWell( - onTap: handleAccept, + onTap: () => handleAccept(context), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -381,7 +393,7 @@ class _CmControlPanel extends StatelessWidget { borderRadius: BorderRadius.circular(10), border: Border.all(color: Colors.grey)), child: InkWell( - onTap: handleCancel, + onTap: () => handleDisconnect(context), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -396,11 +408,14 @@ class _CmControlPanel extends StatelessWidget { ); } - void handleDisconnect() {} + void handleDisconnect(BuildContext context) { + bind.cmCloseConnection(connId: client.id); + } - void handleCancel() {} - - void handleAccept() {} + void handleAccept(BuildContext context) { + final model = Provider.of(context, listen: false); + model.sendLoginResponse(client, true); + } } class PaddingCard extends StatelessWidget { diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index e5465e1e3..9ba85eba1 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -97,9 +97,8 @@ class ServerModel with ChangeNotifier { } final res = await bind.mainCheckClientsLength(length: _clients.length); if (res != null) { - // for test - // debugPrint("clients not match!"); - // updateClientState(res); + debugPrint("clients not match!"); + updateClientState(res); } updatePasswordModel(); From eed87808e5c8f256efd459f6aa1d92e664a67175 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Thu, 18 Aug 2022 00:34:04 +0800 Subject: [PATCH 0238/2015] opt: optimize cm ui & timer & auto close Signed-off-by: Kingtous --- flutter/lib/cm_main.dart | 4 +- flutter/lib/common.dart | 15 +++ flutter/lib/desktop/pages/server_page.dart | 121 +++++++++++++-------- flutter/lib/main.dart | 9 +- flutter/lib/models/server_model.dart | 4 + 5 files changed, 104 insertions(+), 49 deletions(-) diff --git a/flutter/lib/cm_main.dart b/flutter/lib/cm_main.dart index 584d74869..99db02232 100644 --- a/flutter/lib/cm_main.dart +++ b/flutter/lib/cm_main.dart @@ -10,8 +10,8 @@ import 'desktop/pages/server_page.dart'; void main(List args) async { WidgetsFlutterBinding.ensureInitialized(); await windowManager.ensureInitialized(); - await initEnv(kAppTypeConnectionManager); - runApp(GetMaterialApp(theme: getCurrentTheme(), home: DesktopServerPage())); await windowManager.setSize(Size(400, 600)); await windowManager.setAlignment(Alignment.topRight); + await initEnv(kAppTypeConnectionManager); + runApp(GetMaterialApp(theme: getCurrentTheme(), home: DesktopServerPage())); } diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 20167aeb0..63be444e1 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -79,6 +79,15 @@ final ButtonStyle flatButtonStyle = TextButton.styleFrom( ), ); +String formatDurationToTime(Duration duration) { + var totalTime = duration.inSeconds; + final secs = totalTime % 60; + totalTime = (totalTime - secs) ~/ 60; + final mins = totalTime % 60; + totalTime = (totalTime - mins) ~/ 60; + return "${totalTime.toString().padLeft(2, "0")}:${mins.toString().padLeft(2, "0")}:${secs.toString().padLeft(2, "0")}"; +} + closeConnection({String? id}) { if (isAndroid || isIOS) { Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/")); @@ -440,12 +449,18 @@ class PermissionManager { } static Future check(String type) { + if (isDesktop) { + return Future.value(true); + } if (!permissions.contains(type)) return Future.error("Wrong permission!$type"); return gFFI.invokeMethod("check_permission", type); } static Future request(String type) { + if (isDesktop) { + return Future.value(true); + } if (!permissions.contains(type)) return Future.error("Wrong permission!$type"); diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index 34a8f94c4..c78552143 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -1,5 +1,8 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:get/get.dart'; + // import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:provider/provider.dart'; @@ -32,14 +35,14 @@ class DesktopServerPage extends StatefulWidget implements PageShape { padding: EdgeInsets.symmetric(horizontal: 16.0), value: "setPermanentPassword", enabled: - gFFI.serverModel.verificationMethod != kUseTemporaryPassword, + gFFI.serverModel.verificationMethod != kUseTemporaryPassword, ), PopupMenuItem( child: Text(translate("Set temporary password length")), padding: EdgeInsets.symmetric(horizontal: 16.0), value: "setTemporaryPasswordLength", enabled: - gFFI.serverModel.verificationMethod != kUsePermanentPassword, + gFFI.serverModel.verificationMethod != kUsePermanentPassword, ), const PopupMenuDivider(), PopupMenuItem( @@ -51,7 +54,7 @@ class DesktopServerPage extends StatefulWidget implements PageShape { trailing: Icon( Icons.check, color: gFFI.serverModel.verificationMethod == - kUseTemporaryPassword + kUseTemporaryPassword ? null : Color(0xFFFFFFFF), ))), @@ -64,7 +67,7 @@ class DesktopServerPage extends StatefulWidget implements PageShape { trailing: Icon( Icons.check, color: gFFI.serverModel.verificationMethod == - kUsePermanentPassword + kUsePermanentPassword ? null : Color(0xFFFFFFFF), )), @@ -77,9 +80,9 @@ class DesktopServerPage extends StatefulWidget implements PageShape { trailing: Icon( Icons.check, color: gFFI.serverModel.verificationMethod != - kUseTemporaryPassword && - gFFI.serverModel.verificationMethod != - kUsePermanentPassword + kUseTemporaryPassword && + gFFI.serverModel.verificationMethod != + kUsePermanentPassword ? null : Color(0xFFFFFFFF), )), @@ -115,16 +118,16 @@ class _DesktopServerPageState extends State value: gFFI.serverModel, child: Consumer( builder: (context, serverModel, child) => Material( - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Expanded(child: ConnectionManager()), - SizedBox.fromSize(size: Size(0, 15.0)), - ], - ), - ), - ))); + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Expanded(child: ConnectionManager()), + SizedBox.fromSize(size: Size(0, 15.0)), + ], + ), + ), + ))); } @override @@ -136,9 +139,9 @@ class ConnectionManager extends StatelessWidget { Widget build(BuildContext context) { final serverModel = Provider.of(context); // test case: - serverModel.clients.clear(); - serverModel.clients[0] = Client( - false, false, "Readmi-M21sdfsdf", "123123123", true, false, false); + // serverModel.clients.clear(); + // serverModel.clients[0] = Client( + // false, false, "Readmi-M21sdfsdf", "123123123", true, false, false); return serverModel.clients.isEmpty ? Center( child: Text(translate("Waiting")), @@ -150,11 +153,11 @@ class ConnectionManager extends StatelessWidget { children: [ SizedBox( height: kTextTabBarHeight, - child: TabBar( - isScrollable: true, - tabs: serverModel.clients.entries - .map((entry) => buildTab(entry)) - .toList(growable: false)), + child: TabBar( + isScrollable: true, + tabs: serverModel.clients.entries + .map((entry) => buildTab(entry)) + .toList(growable: false)), ), Expanded( child: TabBarView( @@ -170,9 +173,10 @@ class ConnectionManager extends StatelessWidget { Widget buildConnectionCard(MapEntry entry) { final client = entry.value; return Column( + key: ValueKey(entry.key), children: [ _CmHeader(client: client), - _PrivilegeBoard(client: client), + client.isFileTransfer ? Offstage() : _PrivilegeBoard(client: client), Expanded( child: Align( alignment: Alignment.bottomCenter, @@ -200,13 +204,39 @@ class ConnectionManager extends StatelessWidget { } } -class _CmHeader extends StatelessWidget { +class _CmHeader extends StatefulWidget { final Client client; const _CmHeader({Key? key, required this.client}) : super(key: key); + @override + State<_CmHeader> createState() => _CmHeaderState(); +} + +class _CmHeaderState extends State<_CmHeader> + with AutomaticKeepAliveClientMixin { + Client get client => widget.client; + + var _time = 0.obs; + Timer? _timer; + + @override + void initState() { + super.initState(); + _timer = Timer.periodic(Duration(seconds: 1), (_) { + _time.value = _time.value + 1; + }); + } + + @override + void dispose() { + _timer?.cancel(); + super.dispose(); + } + @override Widget build(BuildContext context) { + super.build(context); return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -242,13 +272,13 @@ class _CmHeader extends StatelessWidget { SizedBox( height: 16.0, ), - Offstage( - offstage: !client.authorized, - child: Row( - children: [ - Text("${translate("Connected")}"), - ], - )) + Row( + children: [ + Text("${translate("Connected")}").marginOnly(right: 8.0), + Obx(() => Text( + "${formatDurationToTime(Duration(seconds: _time.value))}")) + ], + ) ], ), ), @@ -264,6 +294,9 @@ class _CmHeader extends StatelessWidget { } void handleSendMsg() {} + + @override + bool get wantKeepAlive => true; } class _PrivilegeBoard extends StatelessWidget { @@ -277,7 +310,7 @@ class _PrivilegeBoard extends StatelessWidget { message: tooltip ?? "", child: Ink( decoration: - BoxDecoration(color: enabled ? MyTheme.accent80 : Colors.grey), + BoxDecoration(color: enabled ? MyTheme.accent80 : Colors.grey), padding: EdgeInsets.all(4.0), child: InkWell( onTap: () => onTap?.call(!enabled), @@ -437,9 +470,9 @@ class PaddingCard extends StatelessWidget { children: [ titleIcon != null ? Padding( - padding: EdgeInsets.only(right: 10), - child: Icon(titleIcon, - color: MyTheme.accent80, size: 30)) + padding: EdgeInsets.only(right: 10), + child: Icon(titleIcon, + color: MyTheme.accent80, size: 30)) : SizedBox.shrink(), Text( title!, @@ -486,12 +519,12 @@ Widget clientInfo(Client client) { crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [ - Text(client.name, - style: TextStyle(color: MyTheme.idColor, fontSize: 18)), - SizedBox(width: 8), - Text(client.peerId, - style: TextStyle(color: MyTheme.idColor, fontSize: 10)) - ])) + Text(client.name, + style: TextStyle(color: MyTheme.idColor, fontSize: 18)), + SizedBox(width: 8), + Text(client.peerId, + style: TextStyle(color: MyTheme.idColor, fontSize: 10)) + ])) ], ), ])); diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 7f3acc79f..c767014ea 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -117,9 +117,12 @@ void runFileTransferScreen(Map argument) async { } void runConnectionManagerScreen() async { - await initEnv(kAppTypeConnectionManager); - await windowManager.setSize(Size(400, 600)); - await windowManager.setAlignment(Alignment.topRight); + await Future.wait([ + initEnv(kAppTypeConnectionManager), + windowManager + .setSize(Size(300, 400)) + .then((value) => windowManager.setAlignment(Alignment.topRight)) + ]); runApp(GetMaterialApp(theme: getCurrentTheme(), home: DesktopServerPage())); } diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index 9ba85eba1..0bbb0c13e 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -342,6 +342,10 @@ class ServerModel with ChangeNotifier { var res = await bind.mainGetClientsState(); try { final List clientsJson = jsonDecode(res); + if (isDesktop && clientsJson.isEmpty && _clients.isNotEmpty) { + // exit cm when >1 peers to no peers + exit(0); + } _clients.clear(); for (var clientJson in clientsJson) { final client = Client.fromJson(clientJson); From 9fee1f41e76f3ae6e6f7107d80aa7d8723e21795 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Thu, 18 Aug 2022 09:51:19 +0800 Subject: [PATCH 0239/2015] opt: use WindowOption to initialize screen Signed-off-by: Kingtous --- flutter/lib/main.dart | 17 ++++++++++++++--- flutter/linux/my_application.cc | 5 ++--- flutter/pubspec.yaml | 1 - 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index c767014ea..ef4ec81c8 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -117,12 +117,23 @@ void runFileTransferScreen(Map argument) async { } void runConnectionManagerScreen() async { + // initialize window + WindowOptions windowOptions = WindowOptions( + size: Size(300, 400), + center: true, + backgroundColor: Colors.transparent, + skipTaskbar: false, + titleBarStyle: TitleBarStyle.normal, + ); await Future.wait([ initEnv(kAppTypeConnectionManager), - windowManager - .setSize(Size(300, 400)) - .then((value) => windowManager.setAlignment(Alignment.topRight)) + windowManager.waitUntilReadyToShow(windowOptions, () async { + await windowManager.setAlignment(Alignment.topRight); + await windowManager.show(); + await windowManager.focus(); + }) ]); + ; runApp(GetMaterialApp(theme: getCurrentTheme(), home: DesktopServerPage())); } diff --git a/flutter/linux/my_application.cc b/flutter/linux/my_application.cc index 25e9858cc..20513032d 100644 --- a/flutter/linux/my_application.cc +++ b/flutter/linux/my_application.cc @@ -1,7 +1,6 @@ #include "my_application.h" #include -// #include #ifdef GDK_WINDOWING_X11 #include #endif @@ -48,8 +47,8 @@ static void my_application_activate(GApplication* application) { gtk_window_set_title(window, "rustdesk"); } - // auto bdw = bitsdojo_window_from(window); // <--- add this line - // bdw->setCustomFrame(true); // <-- add this line + // auto bdw = bitsdojo_window_from(window); // <--- add this line + // bdw->setCustomFrame(true); // <-- add this line gtk_window_set_default_size(window, 1280, 720); // <-- comment this line gtk_widget_show(GTK_WIDGET(window)); diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index fcc7b5f49..caa12313d 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -66,7 +66,6 @@ dependencies: git: url: https://github.com/Kingtous/rustdesk_desktop_multi_window ref: 2b1176d53f195cc55e8d37151bb3d9f6bd52fad3 - # bitsdojo_window: ^0.1.2 freezed_annotation: ^2.0.3 tray_manager: 0.1.7 get: ^4.6.5 From b8f7e85c0bd1f9f9d05ee9c9366acd2c7defd497 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Thu, 18 Aug 2022 11:07:53 +0800 Subject: [PATCH 0240/2015] feat: main window custom bar & drag Signed-off-by: Kingtous --- flutter/lib/desktop/pages/server_page.dart | 88 ++++++++-- .../lib/desktop/widgets/tabbar_widget.dart | 157 ++++++++++++++---- flutter/lib/main.dart | 32 +++- flutter/pubspec.yaml | 5 +- 4 files changed, 223 insertions(+), 59 deletions(-) diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index c78552143..32130ad2e 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -2,9 +2,9 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; - // import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:provider/provider.dart'; +import 'package:window_manager/window_manager.dart'; import '../../common.dart'; import '../../mobile/pages/home_page.dart'; @@ -143,8 +143,15 @@ class ConnectionManager extends StatelessWidget { // serverModel.clients[0] = Client( // false, false, "Readmi-M21sdfsdf", "123123123", true, false, false); return serverModel.clients.isEmpty - ? Center( - child: Text(translate("Waiting")), + ? Column( + children: [ + buildTitleBar(Offstage()), + Expanded( + child: Center( + child: Text(translate("Waiting")), + ), + ), + ], ) : DefaultTabController( length: serverModel.clients.length, @@ -153,18 +160,37 @@ class ConnectionManager extends StatelessWidget { children: [ SizedBox( height: kTextTabBarHeight, - child: TabBar( - isScrollable: true, - tabs: serverModel.clients.entries - .map((entry) => buildTab(entry)) - .toList(growable: false)), + child: buildTitleBar(TabBar( + isScrollable: true, + tabs: serverModel.clients.entries + .map((entry) => buildTab(entry)) + .toList(growable: false))), + ), + Expanded( + child: TabBarView( + children: serverModel.clients.entries + .map((entry) => buildConnectionCard(entry)) + .toList(growable: false)), + ) + ], + ), + ); + } + + Widget buildTitleBar(Widget middle) { + return GestureDetector( + onPanDown: (d) { + windowManager.startDragging(); + }, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + _AppIcon(), + Expanded(child: middle), + const SizedBox( + width: 4.0, ), - Expanded( - child: TabBarView( - children: serverModel.clients.entries - .map((entry) => buildConnectionCard(entry)) - .toList(growable: false)), - ) + _CloseButton() ], ), ); @@ -204,6 +230,40 @@ class ConnectionManager extends StatelessWidget { } } +class _AppIcon extends StatelessWidget { + const _AppIcon({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + margin: EdgeInsets.symmetric(horizontal: 4.0), + child: Image.asset( + 'assets/logo.ico', + width: 30, + height: 30, + ), + ); + } +} + +class _CloseButton extends StatelessWidget { + const _CloseButton({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Ink( + child: InkWell( + onTap: () { + windowManager.close(); + }, + child: Icon( + Icons.close, + size: 30, + )), + ); + } +} + class _CmHeader extends StatefulWidget { final Client client; diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index b7a96271f..32504f6a8 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -1,10 +1,13 @@ import 'dart:math'; +import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/consts.dart'; +import 'package:flutter_hbb/main.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:get/get.dart'; +import 'package:window_manager/window_manager.dart'; const double _kTabBarHeight = kDesktopRemoteTabBarHeight; const double _kIconSize = 18; @@ -76,42 +79,49 @@ class DesktopTabBar extends StatelessWidget { Text("RustDesk").paddingOnly(left: 5), ]).paddingSymmetric(horizontal: 12, vertical: 5), ), - Flexible( - child: Obx(() => TabBar( - key: tabBarKey, - indicatorColor: _theme.indicatorColor, - labelPadding: const EdgeInsets.symmetric( - vertical: 0, horizontal: 0), - isScrollable: true, - indicatorPadding: EdgeInsets.zero, - physics: BouncingScrollPhysics(), - controller: controller.value, - tabs: tabs.asMap().entries.map((e) { - int index = e.key; - String label = e.value.label; + Expanded( + child: GestureDetector( + onPanStart: (_) { + if (mainTab) { + windowManager.startDragging(); + } + }, + child: Obx(() => TabBar( + key: tabBarKey, + indicatorColor: _theme.indicatorColor, + labelPadding: const EdgeInsets.symmetric( + vertical: 0, horizontal: 0), + isScrollable: true, + indicatorPadding: EdgeInsets.zero, + physics: BouncingScrollPhysics(), + controller: controller.value, + tabs: tabs.asMap().entries.map((e) { + int index = e.key; + String label = e.value.label; - return _Tab( - index: index, - label: label, - icon: e.value.icon, - closable: e.value.closable, - selected: selected.value, - onClose: () { - onTabClose(label); - if (index <= selected.value) { - selected.value = max(0, selected.value - 1); - } - controller.value.animateTo(selected.value, - duration: Duration.zero); - }, - onSelected: () { - selected.value = index; - controller.value - .animateTo(index, duration: Duration.zero); - }, - theme: _theme, - ); - }).toList())), + return _Tab( + index: index, + label: label, + icon: e.value.icon, + closable: e.value.closable, + selected: selected.value, + onClose: () { + onTabClose(label); + if (index <= selected.value) { + selected.value = max(0, selected.value - 1); + } + controller.value.animateTo(selected.value, + duration: Duration.zero); + }, + onSelected: () { + selected.value = index; + controller.value + .animateTo(index, duration: Duration.zero); + }, + theme: _theme, + ); + }).toList())), + ), ), Offstage( offstage: mainTab, @@ -134,6 +144,10 @@ class DesktopTabBar extends StatelessWidget { onTap: () => onAddSetting?.call(), ).paddingOnly(right: 10), ), + ), + WindowActionPanel( + mainTab: mainTab, + color: _theme.unSelectedIconColor, ) ], ), @@ -169,6 +183,79 @@ class DesktopTabBar extends StatelessWidget { } } +class WindowActionPanel extends StatelessWidget { + final bool mainTab; + final Color color; + + const WindowActionPanel( + {Key? key, required this.mainTab, required this.color}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Tooltip( + message: translate("Minimize"), + child: InkWell( + child: Icon( + Icons.minimize, + color: color, + ), + onTap: () { + if (mainTab) { + windowManager.minimize(); + } else { + // TODO + // WindowController.fromWindowId(windowId!).close(); + } + }, + ).paddingOnly(right: 10), + ), + Tooltip( + message: translate("Maximize"), + child: InkWell( + child: Icon( + Icons.rectangle_outlined, + color: color, + ), + onTap: () { + if (mainTab) { + windowManager.isMaximized().then((maximized) { + if (maximized) { + windowManager.unmaximize(); + } else { + windowManager.maximize(); + } + }); + } else { + // TODO + // WindowController.fromWindowId(windowId!).(); + } + }, + ).paddingOnly(right: 10), + ), + Tooltip( + message: translate("Close"), + child: InkWell( + child: Icon( + Icons.close, + color: color, + ), + onTap: () { + if (mainTab) { + windowManager.close(); + } else { + WindowController.fromWindowId(windowId!).close(); + } + }, + ).paddingOnly(right: 10), + ) + ], + ); + } +} + class _Tab extends StatelessWidget { late final int index; late final String label; diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index ef4ec81c8..d41e89116 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -77,7 +77,14 @@ Future initEnv(String appType) async { } void runMainApp(bool startService) async { - await initEnv(kAppTypeMain); + WindowOptions windowOptions = getHiddenTitleBarWindowOptions(Size(1280, 720)); + await Future.wait([ + initEnv(kAppTypeMain), + windowManager.waitUntilReadyToShow(windowOptions, () async { + await windowManager.show(); + await windowManager.focus(); + }) + ]); if (startService) { // await windowManager.ensureInitialized(); // disable tray @@ -118,13 +125,7 @@ void runFileTransferScreen(Map argument) async { void runConnectionManagerScreen() async { // initialize window - WindowOptions windowOptions = WindowOptions( - size: Size(300, 400), - center: true, - backgroundColor: Colors.transparent, - skipTaskbar: false, - titleBarStyle: TitleBarStyle.normal, - ); + WindowOptions windowOptions = getHiddenTitleBarWindowOptions(Size(300, 400)); await Future.wait([ initEnv(kAppTypeConnectionManager), windowManager.waitUntilReadyToShow(windowOptions, () async { @@ -134,7 +135,20 @@ void runConnectionManagerScreen() async { }) ]); ; - runApp(GetMaterialApp(theme: getCurrentTheme(), home: DesktopServerPage())); + runApp(GetMaterialApp( + debugShowCheckedModeBanner: false, + theme: getCurrentTheme(), + home: DesktopServerPage())); +} + +WindowOptions getHiddenTitleBarWindowOptions(Size size) { + return WindowOptions( + size: size, + center: true, + backgroundColor: Colors.transparent, + skipTaskbar: false, + titleBarStyle: TitleBarStyle.hidden, + ); } class App extends StatelessWidget { diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index caa12313d..d5856167a 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -67,7 +67,10 @@ dependencies: url: https://github.com/Kingtous/rustdesk_desktop_multi_window ref: 2b1176d53f195cc55e8d37151bb3d9f6bd52fad3 freezed_annotation: ^2.0.3 - tray_manager: 0.1.7 + tray_manager: + git: + url: https://github.com/Kingtous/rustdesk_tray_manager + ref: 3aa37c86e47ea748e7b5507cbe59f2c54ebdb23a get: ^4.6.5 visibility_detector: ^0.3.3 contextmenu: ^3.0.0 From 3cc67bf581c098cc0822c19f1c9105c254097755 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Thu, 18 Aug 2022 17:25:47 +0800 Subject: [PATCH 0241/2015] feat: sub window custom title bar & functions Signed-off-by: Kingtous --- .../lib/desktop/widgets/tabbar_widget.dart | 29 ++++++++++++------- flutter/lib/main.dart | 3 +- flutter/pubspec.lock | 4 +-- flutter/pubspec.yaml | 3 +- 4 files changed, 25 insertions(+), 14 deletions(-) diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 32504f6a8..4a2581705 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -84,6 +84,9 @@ class DesktopTabBar extends StatelessWidget { onPanStart: (_) { if (mainTab) { windowManager.startDragging(); + } else { + WindowController.fromWindowId(windowId!) + .startDragging(); } }, child: Obx(() => TabBar( @@ -201,16 +204,15 @@ class WindowActionPanel extends StatelessWidget { child: Icon( Icons.minimize, color: color, - ), + ).paddingSymmetric(horizontal: 5), onTap: () { if (mainTab) { windowManager.minimize(); } else { - // TODO - // WindowController.fromWindowId(windowId!).close(); + WindowController.fromWindowId(windowId!).minimize(); } }, - ).paddingOnly(right: 10), + ), ), Tooltip( message: translate("Maximize"), @@ -218,7 +220,8 @@ class WindowActionPanel extends StatelessWidget { child: Icon( Icons.rectangle_outlined, color: color, - ), + size: 20, + ).paddingSymmetric(horizontal: 5), onTap: () { if (mainTab) { windowManager.isMaximized().then((maximized) { @@ -229,11 +232,17 @@ class WindowActionPanel extends StatelessWidget { } }); } else { - // TODO - // WindowController.fromWindowId(windowId!).(); + final wc = WindowController.fromWindowId(windowId!); + wc.isMaximized().then((maximized) { + if (maximized) { + wc.unmaximize(); + } else { + wc.maximize(); + } + }); } }, - ).paddingOnly(right: 10), + ), ), Tooltip( message: translate("Close"), @@ -241,7 +250,7 @@ class WindowActionPanel extends StatelessWidget { child: Icon( Icons.close, color: color, - ), + ).paddingSymmetric(horizontal: 5), onTap: () { if (mainTab) { windowManager.close(); @@ -249,7 +258,7 @@ class WindowActionPanel extends StatelessWidget { WindowController.fromWindowId(windowId!).close(); } }, - ).paddingOnly(right: 10), + ), ) ], ); diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index d41e89116..960bfb667 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart'; import 'package:flutter_hbb/desktop/pages/server_page.dart'; @@ -32,6 +33,7 @@ Future main(List args) async { // main window if (args.isNotEmpty && args.first == 'multi_window') { windowId = int.parse(args[1]); + WindowController.fromWindowId(windowId!).showTitleBar(false); final argument = args[2].isEmpty ? Map() : jsonDecode(args[2]) as Map; @@ -134,7 +136,6 @@ void runConnectionManagerScreen() async { await windowManager.focus(); }) ]); - ; runApp(GetMaterialApp( debugShowCheckedModeBanner: false, theme: getCurrentTheme(), diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index f16f9516b..679322df3 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -250,8 +250,8 @@ packages: dependency: "direct main" description: path: "." - ref: "2b1176d53f195cc55e8d37151bb3d9f6bd52fad3" - resolved-ref: "2b1176d53f195cc55e8d37151bb3d9f6bd52fad3" + ref: bf670217de03f4866177a9793284f4db99271c51 + resolved-ref: bf670217de03f4866177a9793284f4db99271c51 url: "https://github.com/Kingtous/rustdesk_desktop_multi_window" source: git version: "0.1.0" diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index d5856167a..f25d5e341 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -63,9 +63,10 @@ dependencies: url: https://github.com/Kingtous/rustdesk_window_manager ref: 028a7f6 desktop_multi_window: + # path: ../../rustdesk_desktop_multi_window git: url: https://github.com/Kingtous/rustdesk_desktop_multi_window - ref: 2b1176d53f195cc55e8d37151bb3d9f6bd52fad3 + ref: bf670217de03f4866177a9793284f4db99271c51 freezed_annotation: ^2.0.3 tray_manager: git: From 41e5f6d0de311d270fd7a6a8913be9bb7ee0480f Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 18 Aug 2022 10:54:09 +0800 Subject: [PATCH 0242/2015] replace tabview with pageview to remove animation Signed-off-by: 21pages --- .../desktop/pages/connection_tab_page.dart | 29 +- .../lib/desktop/pages/desktop_tab_page.dart | 32 +- .../desktop/pages/file_manager_tab_page.dart | 31 +- .../lib/desktop/widgets/tabbar_widget.dart | 315 ++++++++++-------- flutter/pubspec.lock | 51 +-- flutter/pubspec.yaml | 1 + 6 files changed, 260 insertions(+), 199 deletions(-) diff --git a/flutter/lib/desktop/pages/connection_tab_page.dart b/flutter/lib/desktop/pages/connection_tab_page.dart index eb8614dd4..5dd4a829a 100644 --- a/flutter/lib/desktop/pages/connection_tab_page.dart +++ b/flutter/lib/desktop/pages/connection_tab_page.dart @@ -20,28 +20,28 @@ class ConnectionTabPage extends StatefulWidget { State createState() => _ConnectionTabPageState(params); } -class _ConnectionTabPageState extends State - with TickerProviderStateMixin { +class _ConnectionTabPageState extends State { // refactor List when using multi-tab // this singleton is only for test RxList tabs = RxList.empty(growable: true); - late Rx tabController; - static final Rx _selected = 0.obs; static final Rx _fullscreenID = "".obs; - IconData icon = Icons.desktop_windows_sharp; + final IconData selectedIcon = Icons.desktop_windows_sharp; + final IconData unselectedIcon = Icons.desktop_windows_outlined; var connectionMap = RxList.empty(growable: true); _ConnectionTabPageState(Map params) { if (params['id'] != null) { - tabs.add(TabInfo(label: params['id'], icon: icon)); + tabs.add(TabInfo( + label: params['id'], + selectedIcon: selectedIcon, + unselectedIcon: unselectedIcon)); } } @override void initState() { super.initState(); - tabController = TabController(length: tabs.length, vsync: this).obs; rustDeskWinManager.setMethodHandler((call, fromWindowId) async { print( "call ${call.method} with args ${call.arguments} from window ${fromWindowId}"); @@ -50,8 +50,12 @@ class _ConnectionTabPageState extends State final args = jsonDecode(call.arguments); final id = args['id']; window_on_top(windowId()); - DesktopTabBar.onAdd(this, tabController, tabs, _selected, - TabInfo(label: id, icon: icon)); + DesktopTabBar.onAdd( + tabs, + TabInfo( + label: id, + selectedIcon: selectedIcon, + unselectedIcon: unselectedIcon)); } else if (call.method == "onDestroy") { print( "executing onDestroy hook, closing ${tabs.map((tab) => tab.label).toList()}"); @@ -74,18 +78,16 @@ class _ConnectionTabPageState extends State Obx(() => Visibility( visible: _fullscreenID.value.isEmpty, child: DesktopTabBar( - controller: tabController, tabs: tabs, onTabClose: onRemoveId, - selected: _selected, dark: isDarkTheme(), mainTab: false, ))), Expanded(child: Obx(() { WindowController.fromWindowId(windowId()) .setFullscreen(_fullscreenID.value.isNotEmpty); - return TabBarView( - controller: tabController.value, + return PageView( + controller: DesktopTabBar.controller.value, children: tabs .map((tab) => RemotePage( key: ValueKey(tab.label), @@ -103,7 +105,6 @@ class _ConnectionTabPageState extends State } void onRemoveId(String id) { - DesktopTabBar.onClose(this, tabController, tabs, id); ffi(id).close(); if (tabs.length == 0) { WindowController.fromWindowId(windowId()).close(); diff --git a/flutter/lib/desktop/pages/desktop_tab_page.dart b/flutter/lib/desktop/pages/desktop_tab_page.dart index 65ba37e45..5cbc7aece 100644 --- a/flutter/lib/desktop/pages/desktop_tab_page.dart +++ b/flutter/lib/desktop/pages/desktop_tab_page.dart @@ -13,20 +13,19 @@ class DesktopTabPage extends StatefulWidget { State createState() => _DesktopTabPageState(); } -class _DesktopTabPageState extends State - with TickerProviderStateMixin { - late Rx tabController; +class _DesktopTabPageState extends State { late RxList tabs; - static final Rx _selected = 0.obs; @override void initState() { super.initState(); tabs = RxList.from([ - TabInfo(label: kTabLabelHomePage, icon: Icons.home_sharp, closable: false) + TabInfo( + label: kTabLabelHomePage, + selectedIcon: Icons.home_sharp, + unselectedIcon: Icons.home_outlined, + closable: false) ], growable: true); - tabController = - TabController(length: tabs.length, vsync: this, initialIndex: 0).obs; } @override @@ -35,17 +34,14 @@ class _DesktopTabPageState extends State body: Column( children: [ DesktopTabBar( - controller: tabController, tabs: tabs, - onTabClose: onTabClose, - selected: _selected, dark: isDarkTheme(), mainTab: true, onAddSetting: onAddSetting, ), Obx((() => Expanded( - child: TabBarView( - controller: tabController.value, + child: PageView( + controller: DesktopTabBar.controller.value, children: tabs.map((tab) { switch (tab.label) { case kTabLabelHomePage: @@ -62,12 +58,12 @@ class _DesktopTabPageState extends State ); } - void onTabClose(String label) { - DesktopTabBar.onClose(this, tabController, tabs, label); - } - void onAddSetting() { - DesktopTabBar.onAdd(this, tabController, tabs, _selected, - TabInfo(label: kTabLabelSettingPage, icon: Icons.build)); + DesktopTabBar.onAdd( + tabs, + TabInfo( + label: kTabLabelSettingPage, + selectedIcon: Icons.build_sharp, + unselectedIcon: Icons.build_outlined)); } } diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index aa8c60afc..5f12c873a 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -19,25 +19,25 @@ class FileManagerTabPage extends StatefulWidget { State createState() => _FileManagerTabPageState(params); } -class _FileManagerTabPageState extends State - with TickerProviderStateMixin { +class _FileManagerTabPageState extends State { // refactor List when using multi-tab // this singleton is only for test RxList tabs = List.empty(growable: true).obs; - late Rx tabController; - static final Rx _selected = 0.obs; - IconData icon = Icons.file_copy_sharp; + final IconData selectedIcon = Icons.file_copy_sharp; + final IconData unselectedIcon = Icons.file_copy_outlined; _FileManagerTabPageState(Map params) { if (params['id'] != null) { - tabs.add(TabInfo(label: params['id'], icon: icon)); + tabs.add(TabInfo( + label: params['id'], + selectedIcon: selectedIcon, + unselectedIcon: unselectedIcon)); } } @override void initState() { super.initState(); - tabController = TabController(length: tabs.length, vsync: this).obs; rustDeskWinManager.setMethodHandler((call, fromWindowId) async { print( "call ${call.method} with args ${call.arguments} from window ${fromWindowId}"); @@ -46,8 +46,12 @@ class _FileManagerTabPageState extends State final args = jsonDecode(call.arguments); final id = args['id']; window_on_top(windowId()); - DesktopTabBar.onAdd(this, tabController, tabs, _selected, - TabInfo(label: id, icon: icon)); + DesktopTabBar.onAdd( + tabs, + TabInfo( + label: id, + selectedIcon: selectedIcon, + unselectedIcon: unselectedIcon)); } else if (call.method == "onDestroy") { print( "executing onDestroy hook, closing ${tabs.map((tab) => tab.label).toList()}"); @@ -68,17 +72,15 @@ class _FileManagerTabPageState extends State body: Column( children: [ DesktopTabBar( - controller: tabController, tabs: tabs, onTabClose: onRemoveId, - selected: _selected, dark: isDarkTheme(), mainTab: false, ), Expanded( child: Obx( - () => TabBarView( - controller: tabController.value, + () => PageView( + controller: DesktopTabBar.controller.value, children: tabs .map((tab) => FileManagerPage( key: ValueKey(tab.label), @@ -92,8 +94,7 @@ class _FileManagerTabPageState extends State } void onRemoveId(String id) { - DesktopTabBar.onClose(this, tabController, tabs, id); - ffi(id).close(); + ffi("ft_$id").close(); if (tabs.length == 0) { WindowController.fromWindowId(windowId()).close(); } diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 4a2581705..d2acb87ad 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -8,21 +8,22 @@ import 'package:flutter_hbb/main.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:get/get.dart'; import 'package:window_manager/window_manager.dart'; +import 'package:scroll_pos/scroll_pos.dart'; const double _kTabBarHeight = kDesktopRemoteTabBarHeight; const double _kIconSize = 18; const double _kDividerIndent = 10; const double _kAddIconSize = _kTabBarHeight - 15; -final tabBarKey = GlobalKey(); +final _tabBarKey = GlobalKey(); void closeTab(String? id) { - final tabBar = tabBarKey.currentWidget as TabBar?; + final tabBar = _tabBarKey.currentWidget as _ListView?; if (tabBar == null) return; - final tabs = tabBar.tabs as List<_Tab>; + final tabs = tabBar.tabs; if (id == null) { - final current = tabBar.controller?.index; - if (current == null) return; - tabs[current].onClose(); + if (tabBar.selected.value < tabs.length) { + tabs[tabBar.selected.value].onClose(); + } } else { for (final tab in tabs) { if (tab.label == id) { @@ -35,33 +36,45 @@ void closeTab(String? id) { class TabInfo { late final String label; - late final IconData icon; + late final IconData selectedIcon; + late final IconData unselectedIcon; late final bool closable; - TabInfo({required this.label, required this.icon, this.closable = true}); + TabInfo( + {required this.label, + required this.selectedIcon, + required this.unselectedIcon, + this.closable = true}); } class DesktopTabBar extends StatelessWidget { - late final Rx controller; late final RxList tabs; - late final Function(String) onTabClose; - late final Rx selected; + late final Function(String)? onTabClose; late final bool dark; late final _Theme _theme; late final bool mainTab; late final Function()? onAddSetting; + final ScrollPosController scrollController = + ScrollPosController(itemCount: 0); + static final Rx controller = PageController().obs; + static final Rx selected = 0.obs; DesktopTabBar({ Key? key, - required this.controller, required this.tabs, - required this.onTabClose, - required this.selected, + this.onTabClose, required this.dark, required this.mainTab, this.onAddSetting, }) : _theme = dark ? _Theme.dark() : _Theme.light(), - super(key: key); + super(key: key) { + scrollController.itemCount = tabs.length; + WidgetsBinding.instance.addPostFrameCallback((_) { + debugPrint("callback"); + scrollController.scrollToItem(selected.value, + center: true, animate: true); + }); + } @override Widget build(BuildContext context) { @@ -81,57 +94,29 @@ class DesktopTabBar extends StatelessWidget { ), Expanded( child: GestureDetector( - onPanStart: (_) { - if (mainTab) { - windowManager.startDragging(); - } else { - WindowController.fromWindowId(windowId!) - .startDragging(); - } - }, - child: Obx(() => TabBar( - key: tabBarKey, - indicatorColor: _theme.indicatorColor, - labelPadding: const EdgeInsets.symmetric( - vertical: 0, horizontal: 0), - isScrollable: true, - indicatorPadding: EdgeInsets.zero, - physics: BouncingScrollPhysics(), - controller: controller.value, - tabs: tabs.asMap().entries.map((e) { - int index = e.key; - String label = e.value.label; - - return _Tab( - index: index, - label: label, - icon: e.value.icon, - closable: e.value.closable, - selected: selected.value, - onClose: () { - onTabClose(label); - if (index <= selected.value) { - selected.value = max(0, selected.value - 1); - } - controller.value.animateTo(selected.value, - duration: Duration.zero); - }, - onSelected: () { - selected.value = index; - controller.value - .animateTo(index, duration: Duration.zero); - }, - theme: _theme, - ); - }).toList())), - ), + onPanStart: (_) { + if (mainTab) { + windowManager.startDragging(); + } else { + WindowController.fromWindowId(windowId!) + .startDragging(); + } + }, + child: _ListView( + key: _tabBarKey, + controller: controller, + scrollController: scrollController, + tabInfos: tabs, + selected: selected, + onTabClose: onTabClose, + theme: _theme)), ), Offstage( offstage: mainTab, child: _AddButton( theme: _theme, ).paddingOnly(left: 10), - ) + ), ], ), ), @@ -157,32 +142,16 @@ class DesktopTabBar extends StatelessWidget { ); } - static onClose( - TickerProvider vsync, - Rx controller, - RxList tabs, - String label, - ) { - tabs.removeWhere((tab) => tab.label == label); - controller.value = TabController( - length: tabs.length, - vsync: vsync, - initialIndex: max(0, tabs.length - 1)); - } - - static onAdd(TickerProvider vsync, Rx controller, - RxList tabs, Rx selected, TabInfo tab) { + static onAdd(RxList tabs, TabInfo tab) { int index = tabs.indexWhere((e) => e.label == tab.label); if (index >= 0) { - controller.value.animateTo(index, duration: Duration.zero); selected.value = index; } else { tabs.add(tab); - controller.value = TabController( - length: tabs.length, vsync: vsync, initialIndex: tabs.length - 1); - controller.value.animateTo(tabs.length - 1, duration: Duration.zero); selected.value = tabs.length - 1; + assert(selected.value >= 0); } + controller.value.jumpToPage(selected.value); } } @@ -265,10 +234,76 @@ class WindowActionPanel extends StatelessWidget { } } +class _ListView extends StatelessWidget { + late Rx controller; + final ScrollPosController scrollController; + final RxList tabInfos; + final Rx selected; + final Function(String label)? onTabClose; + final _Theme _theme; + late List<_Tab> tabs; + + _ListView({ + Key? key, + required this.controller, + required this.scrollController, + required this.tabInfos, + required this.selected, + required this.onTabClose, + required _Theme theme, + }) : _theme = theme, + super(key: key); + + @override + Widget build(BuildContext context) { + return Obx(() { + tabs = tabInfos.asMap().entries.map((e) { + int index = e.key; + String label = e.value.label; + return _Tab( + index: index, + label: label, + selectedIcon: e.value.selectedIcon, + unselectedIcon: e.value.unselectedIcon, + closable: e.value.closable, + selected: selected.value, + onClose: () { + tabInfos.removeWhere((tab) => tab.label == label); + onTabClose?.call(label); + if (index <= selected.value) { + selected.value = max(0, selected.value - 1); + } + assert(tabInfos.length == 0 || selected.value < tabInfos.length); + scrollController.itemCount = tabInfos.length; + if (tabInfos.length > 0) { + scrollController.scrollToItem(selected.value, + center: true, animate: true); + controller.value.jumpToPage(selected.value); + } + }, + onSelected: () { + selected.value = index; + scrollController.scrollToItem(index, center: true, animate: true); + controller.value.jumpToPage(index); + }, + theme: _theme, + ); + }).toList(); + return ListView( + controller: scrollController, + scrollDirection: Axis.horizontal, + shrinkWrap: true, + physics: BouncingScrollPhysics(), + children: tabs); + }); + } +} + class _Tab extends StatelessWidget { late final int index; late final String label; - late final IconData icon; + late final IconData selectedIcon; + late final IconData unselectedIcon; late final bool closable; late final int selected; late final Function() onClose; @@ -280,7 +315,8 @@ class _Tab extends StatelessWidget { {Key? key, required this.index, required this.label, - required this.icon, + required this.selectedIcon, + required this.unselectedIcon, required this.closable, required this.selected, required this.onClose, @@ -292,59 +328,74 @@ class _Tab extends StatelessWidget { Widget build(BuildContext context) { bool is_selected = index == selected; bool show_divider = index != selected - 1 && index != selected; - return Ink( - child: InkWell( - onHover: (hover) => _hover.value = hover, - onTap: () => onSelected(), - child: Row( - children: [ - Tab( - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - icon, - size: _kIconSize, - color: is_selected - ? theme.selectedtabIconColor - : theme.unSelectedtabIconColor, - ).paddingOnly(right: 5), - Text( - translate(label), - textAlign: TextAlign.center, - style: TextStyle( - color: is_selected - ? theme.selectedTextColor - : theme.unSelectedTextColor), - ), - ], + return Stack( + children: [ + Ink( + child: InkWell( + onHover: (hover) => _hover.value = hover, + onTap: () => onSelected(), + child: Row( + children: [ + Container( + height: _kTabBarHeight, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + is_selected ? selectedIcon : unselectedIcon, + size: _kIconSize, + color: is_selected + ? theme.selectedtabIconColor + : theme.unSelectedtabIconColor, + ).paddingOnly(right: 5), + Text( + translate(label), + textAlign: TextAlign.center, + style: TextStyle( + color: is_selected + ? theme.selectedTextColor + : theme.unSelectedTextColor), + ), + ], + ), + Offstage( + offstage: !closable, + child: Obx((() => _CloseButton( + visiable: _hover.value, + tabSelected: is_selected, + onClose: () => onClose(), + theme: theme, + ))), + ) + ])).paddingSymmetric(horizontal: 10), + Offstage( + offstage: !show_divider, + child: VerticalDivider( + width: 1, + indent: _kDividerIndent, + endIndent: _kDividerIndent, + color: theme.dividerColor, + thickness: 1, ), - Offstage( - offstage: !closable, - child: Obx((() => _CloseButton( - visiable: _hover.value, - tabSelected: is_selected, - onClose: () => onClose(), - theme: theme, - ))), - ) - ])).paddingSymmetric(horizontal: 10), - Offstage( - offstage: !show_divider, - child: VerticalDivider( - width: 1, - indent: _kDividerIndent, - endIndent: _kDividerIndent, - color: theme.dividerColor, - thickness: 1, - ), - ) - ], + ) + ], + ), + ), ), - ), + Positioned( + height: 2, + left: 0, + right: 0, + bottom: 0, + child: Center( + child: Container( + color: + is_selected ? theme.indicatorColor : Colors.transparent), + )) + ], ); } } diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 679322df3..c27406913 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -49,7 +49,7 @@ packages: name: async url: "https://pub.flutter-io.cn" source: hosted - version: "2.8.2" + version: "2.9.0" back_button_interceptor: dependency: "direct main" description: @@ -147,7 +147,7 @@ packages: name: characters url: "https://pub.flutter-io.cn" source: hosted - version: "1.2.0" + version: "1.2.1" charcode: dependency: transitive description: @@ -168,7 +168,7 @@ packages: name: clock url: "https://pub.flutter-io.cn" source: hosted - version: "1.1.0" + version: "1.1.1" code_builder: dependency: transitive description: @@ -235,15 +235,17 @@ packages: dash_chat_2: dependency: "direct main" description: - name: dash_chat_2 - url: "https://pub.flutter-io.cn" - source: hosted + path: "." + ref: feat_maxWidth + resolved-ref: "3946ecf86d3600b54632fd80d0eb0ef0e74f2d6a" + url: "https://github.com/fufesou/Dash-Chat-2" + source: git version: "0.0.12" desktop_drop: dependency: "direct main" description: name: desktop_drop - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.3.3" desktop_multi_window: @@ -324,7 +326,7 @@ packages: name: fake_async url: "https://pub.flutter-io.cn" source: hosted - version: "1.3.0" + version: "1.3.1" ffi: dependency: "direct main" description: @@ -607,14 +609,14 @@ packages: name: matcher url: "https://pub.flutter-io.cn" source: hosted - version: "0.12.11" + version: "0.12.12" material_color_utilities: dependency: transitive description: name: material_color_utilities url: "https://pub.flutter-io.cn" source: hosted - version: "0.1.4" + version: "0.1.5" menu_base: dependency: transitive description: @@ -628,7 +630,7 @@ packages: name: meta url: "https://pub.flutter-io.cn" source: hosted - version: "1.7.0" + version: "1.8.0" mime: dependency: transitive description: @@ -705,7 +707,7 @@ packages: name: path url: "https://pub.flutter-io.cn" source: hosted - version: "1.8.1" + version: "1.8.2" path_provider: dependency: "direct main" description: @@ -846,6 +848,13 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "0.1.2" + scroll_pos: + dependency: "direct main" + description: + name: scroll_pos + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.3.0" settings_ui: dependency: "direct main" description: @@ -948,7 +957,7 @@ packages: name: source_span url: "https://pub.flutter-io.cn" source: hosted - version: "1.8.2" + version: "1.9.0" sqflite: dependency: transitive description: @@ -990,7 +999,7 @@ packages: name: string_scanner url: "https://pub.flutter-io.cn" source: hosted - version: "1.1.0" + version: "1.1.1" synchronized: dependency: transitive description: @@ -1004,14 +1013,14 @@ packages: name: term_glyph url: "https://pub.flutter-io.cn" source: hosted - version: "1.2.0" + version: "1.2.1" test_api: dependency: transitive description: name: test_api url: "https://pub.flutter-io.cn" source: hosted - version: "0.4.9" + version: "0.4.12" timing: dependency: transitive description: @@ -1029,10 +1038,12 @@ packages: tray_manager: dependency: "direct main" description: - name: tray_manager - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.1.7" + path: "." + ref: "3aa37c86e47ea748e7b5507cbe59f2c54ebdb23a" + resolved-ref: "3aa37c86e47ea748e7b5507cbe59f2c54ebdb23a" + url: "https://github.com/Kingtous/rustdesk_tray_manager" + source: git + version: "0.1.8" tuple: dependency: "direct main" description: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index f25d5e341..f616e887a 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -76,6 +76,7 @@ dependencies: visibility_detector: ^0.3.3 contextmenu: ^3.0.0 desktop_drop: ^0.3.3 + scroll_pos: ^0.3.0 dev_dependencies: flutter_launcher_icons: ^0.9.1 From 9c01870d9bab87163c4cf47dbef1aa88d9c0f993 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 19 Aug 2022 12:27:29 +0800 Subject: [PATCH 0243/2015] fix: multi window linux drag issue Signed-off-by: Kingtous --- flutter/pubspec.lock | 4 ++-- flutter/pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index c27406913..ff478900e 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -252,8 +252,8 @@ packages: dependency: "direct main" description: path: "." - ref: bf670217de03f4866177a9793284f4db99271c51 - resolved-ref: bf670217de03f4866177a9793284f4db99271c51 + ref: e8f98ce382e802464947385bed7c1b3eb2497fe1 + resolved-ref: e8f98ce382e802464947385bed7c1b3eb2497fe1 url: "https://github.com/Kingtous/rustdesk_desktop_multi_window" source: git version: "0.1.0" diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index f616e887a..1221d73bc 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -66,7 +66,7 @@ dependencies: # path: ../../rustdesk_desktop_multi_window git: url: https://github.com/Kingtous/rustdesk_desktop_multi_window - ref: bf670217de03f4866177a9793284f4db99271c51 + ref: e8f98ce382e802464947385bed7c1b3eb2497fe1 freezed_annotation: ^2.0.3 tray_manager: git: From 49b6cf198ccff53edbf416b098a5938772b3b784 Mon Sep 17 00:00:00 2001 From: Daniel Ehrhardt Date: Fri, 19 Aug 2022 07:18:47 +0200 Subject: [PATCH 0244/2015] Added new Free Public Server to Readme --- README-CS.md | 3 ++- README-DE.md | 2 ++ README-ES.md | 2 ++ README-FA.md | 2 ++ README-FI.md | 2 ++ README-FR.md | 2 ++ README-HU.md | 2 ++ README-ID.md | 4 +++- README-IT.md | 4 +++- README-JP.md | 4 +++- README-KR.md | 4 +++- README-ML.md | 4 +++- README-NL.md | 4 +++- README-PL.md | 4 +++- README-PTBR.md | 2 ++ README-RU.md | 4 +++- README-VN.md | 4 +++- README-ZH.md | 2 ++ README.md | 3 ++- 19 files changed, 47 insertions(+), 11 deletions(-) diff --git a/README-CS.md b/README-CS.md index f6fa2fbf0..4f2c0e80f 100644 --- a/README-CS.md +++ b/README-CS.md @@ -29,7 +29,8 @@ Níže jsou uvedeny servery zdarma k vašemu použití (údaje se mohou v čase | --------- | ------------- | ------------------ | | Soul | AWS lightsail | 1 VCPU / 0,5GB RAM | | Singapur | Vultr | 1 VCPU / 1GB RAM | -| Dallas | Vultr | 1 VCPU / 1GB RAM | | +| Germany | Hetzner | 2 VCPU / 4GB RAM | +| Germany | Codext | 4 VCPU / 8GB RAM | ## Softwarové součásti, na kterých závisí diff --git a/README-DE.md b/README-DE.md index 4e9929997..eb468d569 100644 --- a/README-DE.md +++ b/README-DE.md @@ -28,6 +28,8 @@ Hier sind die Server, die du kostenlos nutzen kannst, es kann sein das sich dies | Seoul | AWS lightsail | 1 VCPU / 0.5GB RAM | | | Singapore | Vultr | 1 VCPU / 1GB RAM | | | Dallas | Vultr | 1 VCPU / 1GB RAM | | +| Germany | Codext | 2 VCPU / 4GB RAM | +| Germany | Hetzner | 4 VCPU / 8GB RAM | ## Abhängigkeiten diff --git a/README-ES.md b/README-ES.md index 1aab59213..7ceed149a 100644 --- a/README-ES.md +++ b/README-ES.md @@ -28,6 +28,8 @@ A continuación se muestran los servidores que está utilizando de forma gratuit | Seoul | AWS lightsail | 1 VCPU / 0.5GB RAM | | Singapore | Vultr | 1 VCPU / 1GB RAM | | Dallas | Vultr | 1 VCPU / 1GB RAM | | +| Germany | Hetzner | 2 VCPU / 4GB RAM | +| Germany | Codext | 4 VCPU / 8GB RAM | ## Dependencies diff --git a/README-FA.md b/README-FA.md index 818e62fa8..5fd3c0d03 100644 --- a/README-FA.md +++ b/README-FA.md @@ -32,6 +32,8 @@ | Seoul | AWS lightsail | 1 VCPU / 0.5GB RAM | | Singapore | Vultr | 1 VCPU / 1GB RAM | | Dallas | Vultr | 1 VCPU / 1GB RAM | | +| Germany | Hetzner | 2 VCPU / 4GB RAM | +| Germany | Codext | 4 VCPU / 8GB RAM | ## وابستگی ها diff --git a/README-FI.md b/README-FI.md index 5f38d2e42..2e4c99ba6 100644 --- a/README-FI.md +++ b/README-FI.md @@ -27,6 +27,8 @@ Alla on palvelimia, joita voit käyttää ilmaiseksi, ne saattavat muuttua ajan | Seoul | AWS lightsail | 1 VCPU / 0.5GB RAM | | Singapore | Vultr | 1 VCPU / 1GB RAM | | Dallas | Vultr | 1 VCPU / 1GB RAM | | +| Germany | Hetzner | 2 VCPU / 4GB RAM | +| Germany | Codext | 4 VCPU / 8GB RAM | ## Riippuvuudet diff --git a/README-FR.md b/README-FR.md index 9a303e6e6..3e33cb322 100644 --- a/README-FR.md +++ b/README-FR.md @@ -26,6 +26,8 @@ Ci-dessous se trouvent les serveurs que vous utilisez gratuitement, cela peut ch - Séoul, AWS lightsail, 1 VCPU/0.5G RAM - Singapour, Vultr, 1 VCPU/1G RAM - Dallas, Vultr, 1 VCPU/1G RAM +- Germany, Codext, 2 VCPU / 4GB RAM +- Germany, Hetzner, 4 VCPU / 8GB RAM ## Dépendances diff --git a/README-HU.md b/README-HU.md index 7055ed446..3960d8b40 100644 --- a/README-HU.md +++ b/README-HU.md @@ -35,6 +35,8 @@ Ezalatt az üzenet alatt találhatóak azok a publikus szerverek, amelyeket ingy | Seoul | AWS lightsail | 1 VCPU / 0.5GB RAM | | Singapore | Vultr | 1 VCPU / 1GB RAM | | Dallas | Vultr | 1 VCPU / 1GB RAM | | +| Germany | Hetzner | 2 VCPU / 4GB RAM | +| Germany | Codext | 4 VCPU / 8GB RAM | ## Dependencies diff --git a/README-ID.md b/README-ID.md index 363d4263b..3ea9bc454 100644 --- a/README-ID.md +++ b/README-ID.md @@ -26,7 +26,9 @@ Di bawah ini adalah server yang bisa Anda gunakan secara gratis, dapat berubah s | --------- | ------------- | ------------------ | | Seoul | AWS lightsail | 1 VCPU / 0.5GB RAM | | Singapore | Vultr | 1 VCPU / 1GB RAM | -| Dallas | Vultr | 1 VCPU / 1GB RAM | | +| Dallas | Vultr | 1 VCPU / 1GB RAM | +| Germany | Hetzner | 2 VCPU / 4GB RAM | +| Germany | Codext | 4 VCPU / 8GB RAM | ## Dependencies diff --git a/README-IT.md b/README-IT.md index a3f36af55..a79c28153 100644 --- a/README-IT.md +++ b/README-IT.md @@ -26,7 +26,9 @@ Qui sotto trovate i server che possono essere usati gratuitamente, la lista potr | --------- | ------------- | ------------------ | | Seoul | AWS lightsail | 1 VCPU / 0.5GB RAM | | Singapore | Vultr | 1 VCPU / 1GB RAM | -| Dallas | Vultr | 1 VCPU / 1GB RAM | | +| Dallas | Vultr | 1 VCPU / 1GB RAM | +| Germany | Hetzner | 2 VCPU / 4GB RAM | +| Germany | Codext | 4 VCPU / 8GB RAM | ## Dipendenze diff --git a/README-JP.md b/README-JP.md index fb55d0ced..c1722a90f 100644 --- a/README-JP.md +++ b/README-JP.md @@ -31,7 +31,9 @@ RustDeskは誰からの貢献も歓迎します。 貢献するには [`CONTRIBU | --------- | ------------- | ------------------ | | Seoul | AWS lightsail | 1 VCPU / 0.5GB RAM | | Singapore | Vultr | 1 VCPU / 1GB RAM | -| Dallas | Vultr | 1 VCPU / 1GB RAM | | +| Dallas | Vultr | 1 VCPU / 1GB RAM | +| Germany | Hetzner | 2 VCPU / 4GB RAM | +| Germany | Codext | 4 VCPU / 8GB RAM | ## 依存関係 diff --git a/README-KR.md b/README-KR.md index 00564e298..c7cf423da 100644 --- a/README-KR.md +++ b/README-KR.md @@ -31,7 +31,9 @@ RustDesk는 모든 기여를 환영합니다. 기여하고자 한다면 [`CONTRI | --------- | ------------- | ------------------ | | Seoul | AWS lightsail | 1 VCPU / 0.5GB RAM | | Singapore | Vultr | 1 VCPU / 1GB RAM | -| Dallas | Vultr | 1 VCPU / 1GB RAM | | +| Dallas | Vultr | 1 VCPU / 1GB RAM | +| Germany | Hetzner | 2 VCPU / 4GB RAM | +| Germany | Codext | 4 VCPU / 8GB RAM | ## 의존관계 diff --git a/README-ML.md b/README-ML.md index d2931a2c7..45496b129 100644 --- a/README-ML.md +++ b/README-ML.md @@ -26,7 +26,9 @@ | --------- | ------------- | ------------------ | | Seoul | AWS lightsail | 1 VCPU / 0.5GB RAM | | Singapore | Vultr | 1 VCPU / 1GB RAM | -| Dallas | Vultr | 1 VCPU / 1GB RAM | | +| Dallas | Vultr | 1 VCPU / 1GB RAM | +| Germany | Hetzner | 2 VCPU / 4GB RAM | +| Germany | Codext | 4 VCPU / 8GB RAM | ## ഡിപെൻഡൻസികൾ diff --git a/README-NL.md b/README-NL.md index 5db299e7c..8a4a119fc 100644 --- a/README-NL.md +++ b/README-NL.md @@ -26,7 +26,9 @@ Onderstaande servers zijn de servers die je gratis kunt gebruiken, ze kunnen op | --------- | ------------- | ------------------ | | Seoul | AWS lightsail | 1 VCPU / 0.5GB RAM | | Singapore | Vultr | 1 VCPU / 1GB RAM | -| Dallas | Vultr | 1 VCPU / 1GB RAM | | +| Dallas | Vultr | 1 VCPU / 1GB RAM | +| Germany | Hetzner | 2 VCPU / 4GB RAM | +| Germany | Codext | 4 VCPU / 8GB RAM | ## Afhankelijkheden diff --git a/README-PL.md b/README-PL.md index 119af95cf..d3b298d5a 100644 --- a/README-PL.md +++ b/README-PL.md @@ -26,7 +26,9 @@ Poniżej znajdują się serwery, z których można korzystać za darmo, może si | --------- | ------------- | ------------------ | | Seul | AWS lightsail | 1 VCPU / 0.5GB RAM | | Singapur | Vultr | 1 VCPU / 1GB RAM | -| Dallas | Vultr | 1 VCPU / 1GB RAM | | +| Dallas | Vultr | 1 VCPU / 1GB RAM | +| Germany | Hetzner | 2 VCPU / 4GB RAM | +| Germany | Codext | 4 VCPU / 8GB RAM | ## Zależności diff --git a/README-PTBR.md b/README-PTBR.md index 955456256..020941831 100644 --- a/README-PTBR.md +++ b/README-PTBR.md @@ -28,6 +28,8 @@ Abaixo estão os servidores que você está utilizando de graça, ele pode mudar | Seul | AWS lightsail | 1 VCPU / 0.5GB RAM | | Singapura | Vultr | 1 VCPU / 1GB RAM | | Dallas | Vultr | 1 VCPU / 1GB RAM | +| Germany | Hetzner | 2 VCPU / 4GB RAM | +| Germany | Codext | 4 VCPU / 8GB RAM | ## Dependências diff --git a/README-RU.md b/README-RU.md index a9d81152c..d610f2dc5 100644 --- a/README-RU.md +++ b/README-RU.md @@ -32,7 +32,9 @@ RustDesk приветствует вклад каждого. Смотрите [` | --------- | ------------- | ------------------ | | Сеул | AWS lightsail | 1 VCPU / 0.5GB RAM | | Сингапур | Vultr | 1 VCPU / 1GB RAM | -| Даллас | Vultr | 1 VCPU / 1GB RAM | | +| Даллас | Vultr | 1 VCPU / 1GB RAM | +| Germany | Hetzner | 2 VCPU / 4GB RAM | +| Germany | Codext | 4 VCPU / 8GB RAM | ## Зависимости diff --git a/README-VN.md b/README-VN.md index b39005a31..641b80ebd 100644 --- a/README-VN.md +++ b/README-VN.md @@ -35,7 +35,9 @@ Dưới đây là những máy chủ mà bạn có thể sử dụng mà không | --------- | ------------- | ------------------ | | Seoul | AWS lightsail | 1 VCPU / 0.5GB RAM | | Singapore | Vultr | 1 VCPU / 1GB RAM | -| Dallas | Vultr | 1 VCPU / 1GB RAM | | +| Dallas | Vultr | 1 VCPU / 1GB RAM | +| Germany | Hetzner | 2 VCPU / 4GB RAM | +| Germany | Codext | 4 VCPU / 8GB RAM | ## Dependencies diff --git a/README-ZH.md b/README-ZH.md index 0c3e7d5c1..e17254670 100644 --- a/README-ZH.md +++ b/README-ZH.md @@ -27,6 +27,8 @@ Chat with us: [知乎](https://www.zhihu.com/people/rustdesk) | [Discord](https: - 首尔, AWS lightsail, 1 VCPU/0.5G RAM - 新加坡, Vultr, 1 VCPU/1G RAM - 达拉斯, Vultr, 1 VCPU/1G RAM +- Germany, Codext, 2 VCPU / 4GB RAM +- Germany, Hetzner, 4 VCPU / 8GB RAM ## 依赖 diff --git a/README.md b/README.md index 346600f61..79a4b18d3 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,8 @@ Below are the servers you are using for free, it may change along the time. If y | --------- | ------------- | ------------------ | | Seoul | AWS lightsail | 1 VCPU / 0.5GB RAM | | Singapore | Vultr | 1 VCPU / 1GB RAM | -| Dallas | Vultr | 1 VCPU / 1GB RAM | | +| Germany | Hetzner | 2 VCPU / 4GB RAM | +| Germany | Codext | 4 VCPU / 8GB RAM | ## Dependencies From f4d94498c0c0b0e5e69ae5893b0b6cfe12041786 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 19 Aug 2022 14:22:48 +0800 Subject: [PATCH 0245/2015] fix: window manager start drag Signed-off-by: Kingtous --- flutter/pubspec.lock | 4 ++-- flutter/pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index ff478900e..0ec3c9523 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -1230,8 +1230,8 @@ packages: dependency: "direct main" description: path: "." - ref: "028a7f6" - resolved-ref: "028a7f63490a1c2aac3318493b3c1ac1a7299912" + ref: "75a6c813babca461f359a586785d797f7806e390" + resolved-ref: "75a6c813babca461f359a586785d797f7806e390" url: "https://github.com/Kingtous/rustdesk_window_manager" source: git version: "0.2.5" diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 1221d73bc..ddbbb32b7 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -61,7 +61,7 @@ dependencies: window_manager: git: url: https://github.com/Kingtous/rustdesk_window_manager - ref: 028a7f6 + ref: 75a6c813babca461f359a586785d797f7806e390 desktop_multi_window: # path: ../../rustdesk_desktop_multi_window git: From 3172ed63f361d00b6c5582dd2dd02652f2cdcfa0 Mon Sep 17 00:00:00 2001 From: Asura Date: Fri, 19 Aug 2022 14:50:32 +0800 Subject: [PATCH 0246/2015] Fix compile error on MacOS --- src/ui/cm.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ui/cm.rs b/src/ui/cm.rs index f1b4eaf72..f722f8372 100644 --- a/src/ui/cm.rs +++ b/src/ui/cm.rs @@ -1,4 +1,6 @@ -use crate::ipc::{self, new_listener, Connection, Data, start_pa}; +use crate::ipc::{self, new_listener, Connection, Data}; +#[cfg(windows)] +use crate::ipc::{start_pa}; #[cfg(windows)] use crate::ipc::start_clipboard_file; use crate::VERSION; From 4faf0a3d35874cce32283ab71566ce07b47a98a0 Mon Sep 17 00:00:00 2001 From: 21pages Date: Fri, 19 Aug 2022 15:44:19 +0800 Subject: [PATCH 0247/2015] check super permission: win && linux Signed-off-by: 21pages --- flutter/lib/common.dart | 2 + .../desktop/pages/desktop_setting_page.dart | 235 +++++++++++++----- .../lib/desktop/widgets/tabbar_widget.dart | 1 - src/flutter_ffi.rs | 16 +- src/platform/linux.rs | 6 + src/platform/windows.rs | 19 +- src/ui_interface.rs | 7 + 7 files changed, 208 insertions(+), 78 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 63be444e1..6e3ec7020 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -52,6 +52,8 @@ class MyTheme { static const Color darkGray = Color(0xFFB9BABC); static const Color cmIdColor = Color(0xFF21790B); static const Color dark = Colors.black87; + static const Color disabledTextLight = Color(0xFF888888); + static const Color disabledTextDark = Color(0xFF777777); static ThemeData lightTheme = ThemeData( brightness: Brightness.light, diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 65c7ae819..9be269370 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -253,28 +253,47 @@ class _Safety extends StatefulWidget { class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { @override bool get wantKeepAlive => true; + bool locked = true; @override Widget build(BuildContext context) { super.build(context); return ListView( children: [ - permissions(), - password(), - whitelist(), + Column( + children: [ + _lock(locked, 'Unlock Security Settings', () { + locked = false; + setState(() => {}); + }), + AbsorbPointer( + absorbing: locked, + child: Column(children: [ + permissions(), + password(), + whitelist(), + ]), + ), + ], + ) ], ).marginOnly(bottom: _kListViewBottomMargin); } Widget permissions() { + bool enabled = !locked; return _Card(title: 'Permissions', children: [ - _OptionCheckBox('Enable Keyboard/Mouse', 'enable-keyboard'), - _OptionCheckBox('Enable Clipboard', 'enable-clipboard'), - _OptionCheckBox('Enable File Transfer', 'enable-file-transfer'), - _OptionCheckBox('Enable Audio', 'enable-audio'), - _OptionCheckBox('Enable Remote Restart', 'enable-remote-restart'), + _OptionCheckBox('Enable Keyboard/Mouse', 'enable-keyboard', + enabled: enabled), + _OptionCheckBox('Enable Clipboard', 'enable-clipboard', enabled: enabled), + _OptionCheckBox('Enable File Transfer', 'enable-file-transfer', + enabled: enabled), + _OptionCheckBox('Enable Audio', 'enable-audio', enabled: enabled), + _OptionCheckBox('Enable Remote Restart', 'enable-remote-restart', + enabled: enabled), _OptionCheckBox('Enable remote configuration modification', - 'allow-remote-config-modification'), + 'allow-remote-config-modification', + enabled: enabled), ]); } @@ -297,15 +316,17 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { String currentValue = values[keys.indexOf(model.verificationMethod)]; List radios = values .map((value) => _Radio( - value: value, - groupValue: currentValue, - label: value, - onChanged: ((value) { - model.verificationMethod = keys[values.indexOf(value)]; - }))) + value: value, + groupValue: currentValue, + label: value, + onChanged: ((value) { + model.verificationMethod = keys[values.indexOf(value)]; + }), + enabled: !locked, + )) .toList(); - var onChanged = tmp_enabled + var onChanged = tmp_enabled && !locked ? (value) { if (value != null) model.temporaryPasswordLength = value.toString(); @@ -319,7 +340,11 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { value: value, groupValue: model.temporaryPasswordLength, onChanged: onChanged), - Text(value), + Text( + value, + style: TextStyle( + color: _disabledTextColor(onChanged != null)), + ), ], ).paddingSymmetric(horizontal: 10), onTap: () => onChanged?.call(value), @@ -335,10 +360,10 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { ...lengthRadios, ], ), - enabled: tmp_enabled), + enabled: tmp_enabled && !locked), radios[1], - _SubButton( - 'Set permanent password', setPasswordDialog, perm_enabled), + _SubButton('Set permanent password', setPasswordDialog, + perm_enabled && !locked), radios[2], ]); }))); @@ -346,7 +371,8 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { Widget whitelist() { return _Card(title: 'IP Whitelisting', children: [ - _Button('IP Whitelisting', changeWhiteList, tip: 'whitelist_tip') + _Button('IP Whitelisting', changeWhiteList, + tip: 'whitelist_tip', enabled: !locked) ]); } } @@ -362,31 +388,46 @@ class _ConnectionState extends State<_Connection> with AutomaticKeepAliveClientMixin { @override bool get wantKeepAlive => true; + bool locked = true; @override Widget build(BuildContext context) { super.build(context); - return ListView( - children: [ - _Card(title: 'Server', children: [ - _Button('ID/Relay Server', changeServer), - ]), - _Card(title: 'Service', children: [ - _OptionCheckBox('Enable Service', 'stop-service', reverse: true), - // TODO: Not implemented - // _option_check('Always connected via relay', 'allow-always-relay'), - // _option_check('Start ID/relay service', 'stop-rendezvous-service', - // reverse: true), - ]), - _Card(title: 'TCP Tunneling', children: [ - _OptionCheckBox('Enable TCP Tunneling', 'enable-tunnel'), - ]), - direct_ip(), - _Card(title: 'Proxy', children: [ - _Button('Socks5 Proxy', changeSocks5Proxy), - ]), - ], - ).marginOnly(bottom: _kListViewBottomMargin); + bool enabled = !locked; + return ListView(children: [ + Column( + children: [ + _lock(locked, 'Unlock Connection Settings', () { + locked = false; + setState(() => {}); + }), + AbsorbPointer( + absorbing: locked, + child: Column(children: [ + _Card(title: 'Server', children: [ + _Button('ID/Relay Server', changeServer, enabled: enabled), + ]), + _Card(title: 'Service', children: [ + _OptionCheckBox('Enable Service', 'stop-service', + reverse: true, enabled: enabled), + // TODO: Not implemented + // _option_check('Always connected via relay', 'allow-always-relay', enabled: enabled), + // _option_check('Start ID/relay service', 'stop-rendezvous-service', + // reverse: true, enabled: enabled), + ]), + _Card(title: 'TCP Tunneling', children: [ + _OptionCheckBox('Enable TCP Tunneling', 'enable-tunnel', + enabled: enabled), + ]), + direct_ip(), + _Card(title: 'Proxy', children: [ + _Button('Socks5 Proxy', changeSocks5Proxy, enabled: enabled), + ]), + ]), + ), + ], + ) + ]).marginOnly(bottom: _kListViewBottomMargin); } Widget direct_ip() { @@ -395,7 +436,7 @@ class _ConnectionState extends State<_Connection> RxBool apply_enabled = false.obs; return _Card(title: 'Direct IP Access', children: [ _OptionCheckBox('Enable Direct IP Access', 'direct-server', - update: update), + update: update, enabled: !locked), _futureBuilder( future: () async { String enabled = await bind.mainGetOption(key: 'direct-server'); @@ -414,7 +455,7 @@ class _ConnectionState extends State<_Connection> width: 80, child: TextField( controller: controller, - enabled: enabled, + enabled: enabled && !locked, onChanged: (_) => apply_enabled.value = true, inputFormatters: [ FilteringTextInputFormatter.allow(RegExp( @@ -429,10 +470,10 @@ class _ConnectionState extends State<_Connection> ), ), ), - enabled: enabled, - ), + enabled: enabled && !locked, + ).marginOnly(left: 5), Obx(() => ElevatedButton( - onPressed: apply_enabled.value && enabled + onPressed: apply_enabled.value && enabled && !locked ? () async { apply_enabled.value = false; await bind.mainSetOption( @@ -440,7 +481,9 @@ class _ConnectionState extends State<_Connection> value: controller.text); } : null, - child: Text(translate('Apply')), + child: Text( + translate('Apply'), + ), ).marginOnly(left: 20)) ]); }, @@ -700,8 +743,16 @@ Widget _Card({required String title, required List children}) { ); } +Color? _disabledTextColor(bool enabled) { + return enabled + ? null + : isDarkTheme() + ? MyTheme.disabledTextDark + : MyTheme.disabledTextLight; +} + Widget _OptionCheckBox(String label, String key, - {Function()? update = null, bool reverse = false}) { + {Function()? update = null, bool reverse = false, bool enabled = true}) { return _futureBuilder( future: bind.mainGetOption(key: key), hasData: (data) { @@ -721,9 +772,14 @@ Widget _OptionCheckBox(String label, String key, child: Obx( () => Row( children: [ - Checkbox(value: ref.value, onChanged: onChanged) + Checkbox( + value: ref.value, onChanged: enabled ? onChanged : null) .marginOnly(right: 10), - Expanded(child: Text(translate(label))) + Expanded( + child: Text( + translate(label), + style: TextStyle(color: _disabledTextColor(enabled)), + )) ], ), ).marginOnly(left: _kCheckBoxLeftMargin), @@ -734,29 +790,33 @@ Widget _OptionCheckBox(String label, String key, }); } -Widget _Radio({ - required T value, - required T groupValue, - required String label, - required Function(T value) onChanged, -}) { - var on_change = (T? value) { - if (value != null) { - onChanged(value); - } - }; +Widget _Radio( + {required T value, + required T groupValue, + required String label, + required Function(T value) onChanged, + bool enabled = true}) { + var on_change = enabled + ? (T? value) { + if (value != null) { + onChanged(value); + } + } + : null; return GestureDetector( child: Row( children: [ Radio(value: value, groupValue: groupValue, onChanged: on_change), Expanded( child: Text(translate(label), - style: TextStyle(fontSize: _kContentFontSize)) + style: TextStyle( + fontSize: _kContentFontSize, + color: _disabledTextColor(enabled))) .marginOnly(left: 5), ), ], ).marginOnly(left: _kRadioLeftMargin), - onTap: () => on_change(value), + onTap: () => on_change?.call(value), ); } @@ -808,19 +868,19 @@ Widget _SubLabeledWidget(String label, Widget child, {bool enabled = true}) { decoration: BoxDecoration( border: Border.all( color: hover.value && enabled - ? Colors.grey.withOpacity(0.8) - : Colors.grey.withOpacity(0.5), + ? Color(0xFFD7D7D7) + : Color(0xFFCBCBCB), width: hover.value && enabled ? 2 : 1)), child: Row( children: [ Container( height: 28, color: (hover.value && enabled) - ? Colors.grey.withOpacity(0.8) - : Colors.grey.withOpacity(0.5), + ? Color(0xFFD7D7D7) + : Color(0xFFCBCBCB), child: Text( label + ': ', - style: TextStyle(), + style: TextStyle(fontWeight: FontWeight.w300), ), alignment: Alignment.center, padding: @@ -851,6 +911,43 @@ Widget _futureBuilder( }); } +Widget _lock( + bool locked, + String label, + Function() onUnlock, +) { + return Offstage( + offstage: !locked, + child: Row( + children: [ + Container( + width: _kCardFixedWidth, + child: Card( + child: ElevatedButton( + child: Container( + height: 25, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.security_sharp, + size: 20, + ), + Text(translate(label)).marginOnly(left: 5), + ]).marginSymmetric(vertical: 2)), + onPressed: () async { + bool checked = await bind.mainCheckSuperUserPermission(); + if (checked) { + onUnlock(); + } + }, + ).marginSymmetric(horizontal: 2, vertical: 4), + ).marginOnly(left: _kCardLeftMargin), + ).marginOnly(top: 10), + ], + )); +} + // ignore: must_be_immutable class _ComboBox extends StatelessWidget { late final List keys; diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index d2acb87ad..094659251 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -70,7 +70,6 @@ class DesktopTabBar extends StatelessWidget { super(key: key) { scrollController.itemCount = tabs.length; WidgetsBinding.instance.addPostFrameCallback((_) { - debugPrint("callback"); scrollController.scrollToItem(selected.value, center: true, animate: true); }); diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index d3560ba4a..53e3f1ff8 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -22,12 +22,12 @@ use crate::ui_interface; #[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::ui_interface::{change_id, check_connect_status, is_ok_change_id}; use crate::ui_interface::{ - discover, forget_password, get_api_server, get_app_name, get_async_job_status, - get_connect_status, get_fav, get_id, get_lan_peers, get_langs, get_license, get_local_option, - get_option, get_options, get_peer, get_peer_option, get_socks, get_sound_inputs, get_uuid, - get_version, has_hwcodec, has_rendezvous_service, post_request, set_local_option, set_option, - set_options, set_peer_option, set_permanent_password, set_socks, store_fav, - test_if_valid_server, update_temporary_password, using_public_server, + check_super_user_permission, discover, forget_password, get_api_server, get_app_name, + get_async_job_status, get_connect_status, get_fav, get_id, get_lan_peers, get_langs, + get_license, get_local_option, get_option, get_options, get_peer, get_peer_option, get_socks, + get_sound_inputs, get_uuid, get_version, has_hwcodec, has_rendezvous_service, post_request, + set_local_option, set_option, set_options, set_peer_option, set_permanent_password, set_socks, + store_fav, test_if_valid_server, update_temporary_password, using_public_server, }; fn initialize(app_dir: &str) { @@ -735,6 +735,10 @@ pub fn main_set_permanent_password(password: String) { set_permanent_password(password); } +pub fn main_check_super_user_permission() -> bool { + check_super_user_permission() +} + pub fn cm_send_chat(conn_id: i32, msg: String) { connection_manager::send_chat(conn_id, msg); } diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 85947a143..0ead52f31 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -629,3 +629,9 @@ extern "C" { pub fn quit_gui() { unsafe { gtk_main_quit() }; } + +pub fn check_super_user_permission() -> ResultType { + // TODO: replace echo with a rustdesk's program, which is location-fixed and non-gui. + let status = std::process::Command::new("pkexec").arg("echo").status()?; + Ok(status.success() && status.code() == Some(0)) +} diff --git a/src/platform/windows.rs b/src/platform/windows.rs index cb0fd778f..fa9fb5b10 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -8,7 +8,7 @@ use hbb_common::{ }; use std::io::prelude::*; use std::{ - ffi::OsString, + ffi::{CString, OsString}, fs, io, mem, sync::{Arc, Mutex}, time::{Duration, Instant}, @@ -17,7 +17,8 @@ use winapi::{ shared::{minwindef::*, ntdef::NULL, windef::*}, um::{ errhandlingapi::GetLastError, handleapi::CloseHandle, minwinbase::STILL_ACTIVE, - processthreadsapi::GetExitCodeProcess, winbase::*, wingdi::*, winnt::HANDLE, winuser::*, + processthreadsapi::GetExitCodeProcess, shellapi::ShellExecuteA, winbase::*, wingdi::*, + winnt::HANDLE, winuser::*, }, }; use windows_service::{ @@ -1418,3 +1419,17 @@ pub fn get_user_token(session_id: u32, as_user: bool) -> HANDLE { } } } + +pub fn check_super_user_permission() -> ResultType { + unsafe { + let ret = ShellExecuteA( + NULL as _, + CString::new("runas")?.as_ptr() as _, + CString::new("cmd")?.as_ptr() as _, + CString::new("/c /q")?.as_ptr() as _, + NULL as _, + SW_SHOWNORMAL, + ); + return Ok(ret as i32 > 32); + } +} diff --git a/src/ui_interface.rs b/src/ui_interface.rs index d45b83b75..f59f96090 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -676,6 +676,13 @@ pub fn has_hwcodec() -> bool { return true; } +pub fn check_super_user_permission() -> bool { + #[cfg(any(windows, target_os = "linux"))] + return crate::platform::check_super_user_permission().unwrap_or(false); + #[cfg(not(any(windows, target_os = "linux")))] + true +} + pub fn check_zombie(childs: Childs) { let mut deads = Vec::new(); loop { From 10eb1003c1dd00caaa8194f66d3d6ddbab6350c3 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 22 Aug 2022 09:39:15 +0800 Subject: [PATCH 0248/2015] fix: multi window macos compile Signed-off-by: Kingtous --- flutter/pubspec.lock | 4 ++-- flutter/pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 0ec3c9523..34b39cb56 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -252,8 +252,8 @@ packages: dependency: "direct main" description: path: "." - ref: e8f98ce382e802464947385bed7c1b3eb2497fe1 - resolved-ref: e8f98ce382e802464947385bed7c1b3eb2497fe1 + ref: "6e6b6f557f655e9c985007d754b6282a0e524932" + resolved-ref: "6e6b6f557f655e9c985007d754b6282a0e524932" url: "https://github.com/Kingtous/rustdesk_desktop_multi_window" source: git version: "0.1.0" diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index ddbbb32b7..e22ff944f 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -66,7 +66,7 @@ dependencies: # path: ../../rustdesk_desktop_multi_window git: url: https://github.com/Kingtous/rustdesk_desktop_multi_window - ref: e8f98ce382e802464947385bed7c1b3eb2497fe1 + ref: 6e6b6f557f655e9c985007d754b6282a0e524932 freezed_annotation: ^2.0.3 tray_manager: git: From a10487c8401c4592650e2bd22e8b7821a3a04603 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sat, 20 Aug 2022 19:57:16 +0800 Subject: [PATCH 0249/2015] native style Signed-off-by: 21pages --- flutter/assets/tabbar.ttf | Bin 0 -> 2288 bytes flutter/lib/common.dart | 115 ++++- flutter/lib/consts.dart | 2 +- .../lib/desktop/pages/desktop_home_page.dart | 315 ++++++++------ .../desktop/pages/desktop_setting_page.dart | 85 ++-- .../lib/desktop/pages/desktop_tab_page.dart | 53 +-- .../lib/desktop/pages/file_manager_page.dart | 1 + flutter/lib/desktop/pages/remote_page.dart | 6 +- .../lib/desktop/widgets/tabbar_widget.dart | 399 ++++++++++-------- flutter/pubspec.yaml | 3 + 10 files changed, 603 insertions(+), 376 deletions(-) create mode 100644 flutter/assets/tabbar.ttf diff --git a/flutter/assets/tabbar.ttf b/flutter/assets/tabbar.ttf new file mode 100644 index 0000000000000000000000000000000000000000..a9220f348fb303a4c064717c2b0543a5a05a44ae GIT binary patch literal 2288 zcmd^BOK%%h6h3#xk9gcXoN?U5DPP zz2AA<^SbjeF(UF)l|&kwzqE8#xz>M`h&=`A;tQ8X6VtsHfZ6-tUrQHLr3*j3vq2=Z zK(?JNY_3$V&4!4?KOl?c)Kn&--S`u>4?t5n2)y^jcY(hK#d1Ys;}`(;{*LKbK~JaH zkHQ)3e*nItnA#|@AWcBu4PMbwMYZF`^lc(f5dO0zy<&WN^Vcj<%Nx+YPRL!XHmdZp z3oH?4Ud{gDwIq-}85@7))%NMX$Zy;JGWf+m+|XVd_go|ngKZ(f+0H$AX%KzkgG(h` zL=DbjaJ@uhbcTdy=N1;Jt&lP_3iBka!S(wwE9A2&__ud5=(v_a!pP%DT`poDk{jXP z6TXA&1AApVlwjB?klW4%s)xsmE9WzG$Ml#vs~;r(OjTI$9QKC510F_DA+A;JN!yIQ z>FnQtlzTdf6&--wMGN4MSlx1&UejluFf(SZf!-j8eH^Up!C>9yTs#RkYGx=1^)O!j z|9u>>8t{_S26V)9h##2gwTYjQ>9dL7Gv*1K`01D#oA{k-=4`S~le;`kIr@aYVZ=IZ znfS5r7M}W}6h;3deFMp$^U=hd=x1ZDU8gE{4#>E`$z50{y6>3nid`C?Yu!9T%5t zi^A>NLU%OUEqvJ%jrIuJwI$)!OtfcDXgz6-d*Us;bwrYB;PKv0!TS?zVlonlc1_p; zggdouVYv2vxW7Lvj9~tV;~8OS=4?0?6K0?tn&}Vk8GIfK&puPzh79I0r(fG_3vV}) zaq7`XU-CI|Rbi2TuSeL{>@4qUC*8L#fn|`@Wy~$$Y%E}ZuY*O%`yA{79&)f7_gKQg z9>|||utdXj-obu&ETIR?g*zL4c-z5@I_NV83wU#Ut{KhsqMBVRq{>aUxh|{aN?zBL@%UI%G_PuE zx#^~|o=q6Win3DHi^?4QsfB{Bl*;<5nl|D&!zi5|9p!PjJf7BzbdJiTQi=@B8B*vn zZDP(-nsm}=#hQ&r_=~X4(i#=8v;2tm;O=Eum#G3f?o*+0isN&2&}tr5nq_$~n+mNX zS^_y5$VdT|;i(8T=j5j%lLD}U1LV3@_({V$j@J#}Rl { + const ColorThemeExtension({ + required this.bg, + required this.grayBg, + required this.text, + required this.lightText, + required this.lighterText, + required this.border, + }); + + final Color? bg; + final Color? grayBg; + final Color? text; + final Color? lightText; + final Color? lighterText; + final Color? border; + + static const light = ColorThemeExtension( + bg: Color(0xFFFFFFFF), + grayBg: Color(0xFFEEEEEE), + text: Color(0xFF222222), + lightText: Color(0xFF666666), + lighterText: Color(0xFF888888), + border: Color(0xFFCCCCCC), + ); + + static const dark = ColorThemeExtension( + bg: Color(0xFF252525), + grayBg: Color(0xFF141414), + text: Color(0xFFFFFFFF), + lightText: Color(0xFF999999), + lighterText: Color(0xFF777777), + border: Color(0xFF555555), + ); + + @override + ThemeExtension copyWith( + {Color? bg, + Color? grayBg, + Color? text, + Color? lightText, + Color? lighterText, + Color? border}) { + return ColorThemeExtension( + bg: bg ?? this.bg, + grayBg: grayBg ?? this.grayBg, + text: text ?? this.text, + lightText: lightText ?? this.lightText, + lighterText: lighterText ?? this.lighterText, + border: border ?? this.border, + ); + } + + @override + ThemeExtension lerp( + ThemeExtension? other, double t) { + if (other is! ColorThemeExtension) { + return this; + } + return ColorThemeExtension( + bg: Color.lerp(bg, other.bg, t), + grayBg: Color.lerp(grayBg, other.grayBg, t), + text: Color.lerp(text, other.text, t), + lightText: Color.lerp(lightText, other.lightText, t), + lighterText: Color.lerp(lighterText, other.lighterText, t), + border: Color.lerp(border, other.border, t), + ); + } +} + class MyTheme { MyTheme._(); @@ -52,20 +134,37 @@ class MyTheme { static const Color darkGray = Color(0xFFB9BABC); static const Color cmIdColor = Color(0xFF21790B); static const Color dark = Colors.black87; - static const Color disabledTextLight = Color(0xFF888888); - static const Color disabledTextDark = Color(0xFF777777); static ThemeData lightTheme = ThemeData( brightness: Brightness.light, primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, - tabBarTheme: TabBarTheme(labelColor: Colors.black87), + tabBarTheme: TabBarTheme( + labelColor: Colors.black87, + ), + // backgroundColor: Color(0xFFFFFFFF), + ).copyWith( + extensions: >[ + ColorThemeExtension.light, + ], ); static ThemeData darkTheme = ThemeData( - brightness: Brightness.dark, - primarySwatch: Colors.blue, - visualDensity: VisualDensity.adaptivePlatformDensity, - tabBarTheme: TabBarTheme(labelColor: Colors.white70)); + brightness: Brightness.dark, + primarySwatch: Colors.blue, + visualDensity: VisualDensity.adaptivePlatformDensity, + tabBarTheme: TabBarTheme( + labelColor: Colors.white70, + ), + // backgroundColor: Color(0xFF252525) + ).copyWith( + extensions: >[ + ColorThemeExtension.dark, + ], + ); + + static ColorThemeExtension color(BuildContext context) { + return Theme.of(context).extension()!; + } } bool isDarkTheme() { diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 7b61c5b48..09e80b482 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -1,4 +1,4 @@ -const double kDesktopRemoteTabBarHeight = 48.0; +const double kDesktopRemoteTabBarHeight = 28.0; const String kAppTypeMain = "main"; const String kAppTypeDesktopRemote = "remote"; const String kAppTypeDesktopFileTransfer = "file transfer"; diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 407d38958..30fec849b 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -48,15 +48,16 @@ class _DesktopHomePageState extends State @override Widget build(BuildContext context) { + super.build(context); return Row( children: [ - Flexible( - child: buildServerInfo(context), - flex: 1, + buildServerInfo(context), + VerticalDivider( + width: 1, + thickness: 1, ), - Flexible( + Expanded( child: buildServerBoard(context), - flex: 4, ), ], ); @@ -66,6 +67,8 @@ class _DesktopHomePageState extends State return ChangeNotifierProvider.value( value: gFFI.serverModel, child: Container( + width: 200, + color: MyTheme.color(context).bg, child: Column( children: [ buildTip(context), @@ -78,44 +81,48 @@ class _DesktopHomePageState extends State } buildServerBoard(BuildContext context) { - return Column( - children: [ - Expanded(child: ConnectionPage()), - ], + return Container( + color: MyTheme.color(context).grayBg, + child: ConnectionPage(), ); } buildIDBoard(BuildContext context) { final model = gFFI.serverModel; return Container( - margin: EdgeInsets.symmetric(vertical: 4.0, horizontal: 16.0), + margin: EdgeInsets.symmetric(horizontal: 16), + height: 52, child: Row( crossAxisAlignment: CrossAxisAlignment.baseline, textBaseline: TextBaseline.alphabetic, children: [ Container( - width: 3, - height: 70, + width: 2, decoration: BoxDecoration(color: MyTheme.accent), ), Expanded( child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), + padding: const EdgeInsets.only(left: 8.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - translate("ID"), - style: TextStyle( - fontSize: 18, fontWeight: FontWeight.w500), - ), - buildPopupMenu(context) - ], + Container( + height: 15, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + translate("ID"), + style: TextStyle( + fontSize: 14, + color: MyTheme.color(context).lightText), + ), + buildPopupMenu(context) + ], + ), ), - GestureDetector( + Flexible( + child: GestureDetector( onDoubleTap: () { Clipboard.setData( ClipboardData(text: model.serverId.text)); @@ -124,7 +131,15 @@ class _DesktopHomePageState extends State child: TextFormField( controller: model.serverId, readOnly: true, - )), + decoration: InputDecoration( + border: InputBorder.none, + ), + style: TextStyle( + fontSize: 22, + ), + ).marginOnly(bottom: 5), + ), + ) ], ), ), @@ -136,116 +151,143 @@ class _DesktopHomePageState extends State Widget buildPopupMenu(BuildContext context) { var position; - return GestureDetector( - onTapDown: (detail) { - final x = detail.globalPosition.dx; - final y = detail.globalPosition.dy; - position = RelativeRect.fromLTRB(x, y, x, y); - }, - onTap: () async { - final userName = await gFFI.userModel.getUserName(); - final enabledInput = await bind.mainGetOption(key: 'enable-audio'); - final defaultInput = await gFFI.getDefaultAudioInput(); - var menu = [ - await genEnablePopupMenuItem( - translate("Enable Keyboard/Mouse"), - 'enable-keyboard', + RxBool hover = false.obs; + return InkWell( + onTapDown: (detail) { + final x = detail.globalPosition.dx; + final y = detail.globalPosition.dy; + position = RelativeRect.fromLTRB(x, y, x, y); + }, + onTap: () async { + final userName = await gFFI.userModel.getUserName(); + final enabledInput = await bind.mainGetOption(key: 'enable-audio'); + final defaultInput = await gFFI.getDefaultAudioInput(); + var menu = [ + await genEnablePopupMenuItem( + translate("Enable Keyboard/Mouse"), + 'enable-keyboard', + ), + await genEnablePopupMenuItem( + translate("Enable Clipboard"), + 'enable-clipboard', + ), + await genEnablePopupMenuItem( + translate("Enable File Transfer"), + 'enable-file-transfer', + ), + await genEnablePopupMenuItem( + translate("Enable TCP Tunneling"), + 'enable-tunnel', + ), + genAudioInputPopupMenuItem(enabledInput != "N", defaultInput), + PopupMenuDivider(), + PopupMenuItem( + child: Text(translate("ID/Relay Server")), + value: 'custom-server', + ), + PopupMenuItem( + child: Text(translate("IP Whitelisting")), + value: 'whitelist', + ), + PopupMenuItem( + child: Text(translate("Socks5 Proxy")), + value: 'socks5-proxy', + ), + PopupMenuDivider(), + await genEnablePopupMenuItem( + translate("Enable Service"), + 'stop-service', + ), + // TODO: direct server + await genEnablePopupMenuItem( + translate("Always connected via relay"), + 'allow-always-relay', + ), + await genEnablePopupMenuItem( + translate("Start ID/relay service"), + 'stop-rendezvous-service', + ), + PopupMenuDivider(), + userName.isEmpty + ? PopupMenuItem( + child: Text(translate("Login")), + value: 'login', + ) + : PopupMenuItem( + child: Text("${translate("Logout")} $userName"), + value: 'logout', + ), + PopupMenuItem( + child: Text(translate("Change ID")), + value: 'change-id', + ), + PopupMenuDivider(), + await genEnablePopupMenuItem( + translate("Dark Theme"), + 'allow-darktheme', + ), + PopupMenuItem( + child: Text(translate("About")), + value: 'about', + ), + ]; + final v = + await showMenu(context: context, position: position, items: menu); + if (v != null) { + onSelectMenu(v); + } + }, + child: Obx( + () => Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(90), + boxShadow: [ + BoxShadow( + color: hover.value + ? MyTheme.color(context).grayBg! + : MyTheme.color(context).bg!, + spreadRadius: 2) + ], + ), + child: Center( + child: Icon( + Icons.more_vert_outlined, + size: 20, + color: hover.value + ? MyTheme.color(context).text + : MyTheme.color(context).lightText, ), - await genEnablePopupMenuItem( - translate("Enable Clipboard"), - 'enable-clipboard', - ), - await genEnablePopupMenuItem( - translate("Enable File Transfer"), - 'enable-file-transfer', - ), - await genEnablePopupMenuItem( - translate("Enable TCP Tunneling"), - 'enable-tunnel', - ), - genAudioInputPopupMenuItem(enabledInput != "N", defaultInput), - PopupMenuDivider(), - PopupMenuItem( - child: Text(translate("ID/Relay Server")), - value: 'custom-server', - ), - PopupMenuItem( - child: Text(translate("IP Whitelisting")), - value: 'whitelist', - ), - PopupMenuItem( - child: Text(translate("Socks5 Proxy")), - value: 'socks5-proxy', - ), - PopupMenuDivider(), - await genEnablePopupMenuItem( - translate("Enable Service"), - 'stop-service', - ), - // TODO: direct server - await genEnablePopupMenuItem( - translate("Always connected via relay"), - 'allow-always-relay', - ), - await genEnablePopupMenuItem( - translate("Start ID/relay service"), - 'stop-rendezvous-service', - ), - PopupMenuDivider(), - userName.isEmpty - ? PopupMenuItem( - child: Text(translate("Login")), - value: 'login', - ) - : PopupMenuItem( - child: Text("${translate("Logout")} $userName"), - value: 'logout', - ), - PopupMenuItem( - child: Text(translate("Change ID")), - value: 'change-id', - ), - PopupMenuDivider(), - await genEnablePopupMenuItem( - translate("Dark Theme"), - 'allow-darktheme', - ), - PopupMenuItem( - child: Text(translate("About")), - value: 'about', - ), - ]; - final v = - await showMenu(context: context, position: position, items: menu); - if (v != null) { - onSelectMenu(v); - } - }, - child: Icon(Icons.more_vert_outlined)); + ), + ), + ), + onHover: (value) => hover.value = value, + ); } buildPasswordBoard(BuildContext context) { final model = gFFI.serverModel; + RxBool refreshHover = false.obs; return Container( - margin: EdgeInsets.symmetric(vertical: 4.0, horizontal: 16.0), + margin: EdgeInsets.symmetric(vertical: 12, horizontal: 16.0), child: Row( crossAxisAlignment: CrossAxisAlignment.baseline, textBaseline: TextBaseline.alphabetic, children: [ Container( - width: 3, - height: 70, + width: 2, + height: 52, decoration: BoxDecoration(color: MyTheme.accent), ), Expanded( child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), + padding: const EdgeInsets.only(left: 8.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( translate("Password"), - style: TextStyle(fontSize: 18, fontWeight: FontWeight.w500), + style: TextStyle( + fontSize: 14, color: MyTheme.color(context).lightText), ), Row( children: [ @@ -262,12 +304,25 @@ class _DesktopHomePageState extends State child: TextFormField( controller: model.serverPasswd, readOnly: true, + decoration: InputDecoration( + border: InputBorder.none, + ), + style: TextStyle(fontSize: 15), ), ), ), - IconButton( - icon: Icon(Icons.refresh), - onPressed: () => bind.mainUpdateTemporaryPassword(), + InkWell( + child: Obx( + () => Icon( + Icons.refresh, + color: refreshHover.value + ? MyTheme.color(context).text + : Color(0xFFDDDDDD), + size: 22, + ).marginOnly(right: 5), + ), + onTap: () => bind.mainUpdateTemporaryPassword(), + onHover: (value) => refreshHover.value = value, ), FutureBuilder( future: buildPasswordPopupMenu(context), @@ -282,7 +337,7 @@ class _DesktopHomePageState extends State } }) ], - ), + ).marginOnly(bottom: 20), ], ), ), @@ -294,7 +349,8 @@ class _DesktopHomePageState extends State Future buildPasswordPopupMenu(BuildContext context) async { var position; - return GestureDetector( + RxBool editHover = false.obs; + return InkWell( onTapDown: (detail) { final x = detail.globalPosition.dx; final y = detail.globalPosition.dy; @@ -365,7 +421,12 @@ class _DesktopHomePageState extends State setPasswordDialog(); } }, - child: Icon(Icons.edit)); + onHover: (value) => editHover.value = value, + child: Obx(() => Icon(Icons.edit, + size: 22, + color: editHover.value + ? MyTheme.color(context).text + : Color(0xFFDDDDDD)))); } buildTip(BuildContext context) { @@ -377,7 +438,7 @@ class _DesktopHomePageState extends State children: [ Text( translate("Your Desktop"), - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20), + style: TextStyle(fontWeight: FontWeight.normal, fontSize: 19), ), SizedBox( height: 8.0, @@ -385,7 +446,8 @@ class _DesktopHomePageState extends State Text( translate("desk_tip"), overflow: TextOverflow.clip, - style: TextStyle(fontSize: 14), + style: TextStyle( + fontSize: 12, color: MyTheme.color(context).lighterText), ) ], ), @@ -394,13 +456,17 @@ class _DesktopHomePageState extends State buildControlPanel(BuildContext context) { return Container( + width: 320, decoration: BoxDecoration( borderRadius: BorderRadius.circular(10), color: MyTheme.white), padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(translate("Control Remote Desktop")), + Text( + translate("Control Remote Desktop"), + style: TextStyle(fontWeight: FontWeight.normal, fontSize: 19), + ), Form( child: Column( children: [ @@ -409,6 +475,7 @@ class _DesktopHomePageState extends State inputFormatters: [ FilteringTextInputFormatter.allow(RegExp(r"[0-9]")) ], + style: TextStyle(fontSize: 22, fontWeight: FontWeight.w400), ) ], )) diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 9be269370..7c87d7cb0 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -71,6 +71,7 @@ class _DesktopSettingPageState extends State Widget build(BuildContext context) { super.build(context); return Scaffold( + backgroundColor: MyTheme.color(context).bg, body: Row( children: [ Container( @@ -88,16 +89,19 @@ class _DesktopSettingPageState extends State ), const VerticalDivider(thickness: 1, width: 1), Expanded( - child: PageView( - controller: controller, - children: [ - _UserInterface(), - _Safety(), - _Display(), - _Audio(), - _Connection(), - _About(), - ], + child: Container( + color: MyTheme.color(context).grayBg, + child: PageView( + controller: controller, + children: [ + _UserInterface(), + _Safety(), + _Display(), + _Audio(), + _Connection(), + _About(), + ], + ), ), ) ], @@ -269,8 +273,8 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { AbsorbPointer( absorbing: locked, child: Column(children: [ - permissions(), - password(), + permissions(context), + password(context), whitelist(), ]), ), @@ -280,24 +284,26 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { ).marginOnly(bottom: _kListViewBottomMargin); } - Widget permissions() { + Widget permissions(context) { bool enabled = !locked; return _Card(title: 'Permissions', children: [ - _OptionCheckBox('Enable Keyboard/Mouse', 'enable-keyboard', + _OptionCheckBox(context, 'Enable Keyboard/Mouse', 'enable-keyboard', enabled: enabled), - _OptionCheckBox('Enable Clipboard', 'enable-clipboard', enabled: enabled), - _OptionCheckBox('Enable File Transfer', 'enable-file-transfer', + _OptionCheckBox(context, 'Enable Clipboard', 'enable-clipboard', enabled: enabled), - _OptionCheckBox('Enable Audio', 'enable-audio', enabled: enabled), - _OptionCheckBox('Enable Remote Restart', 'enable-remote-restart', + _OptionCheckBox(context, 'Enable File Transfer', 'enable-file-transfer', enabled: enabled), - _OptionCheckBox('Enable remote configuration modification', + _OptionCheckBox(context, 'Enable Audio', 'enable-audio', + enabled: enabled), + _OptionCheckBox(context, 'Enable Remote Restart', 'enable-remote-restart', + enabled: enabled), + _OptionCheckBox(context, 'Enable remote configuration modification', 'allow-remote-config-modification', enabled: enabled), ]); } - Widget password() { + Widget password(BuildContext context) { return ChangeNotifierProvider.value( value: gFFI.serverModel, child: Consumer(builder: ((context, model, child) { @@ -316,6 +322,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { String currentValue = values[keys.indexOf(model.verificationMethod)]; List radios = values .map((value) => _Radio( + context, value: value, groupValue: currentValue, label: value, @@ -343,7 +350,8 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { Text( value, style: TextStyle( - color: _disabledTextColor(onChanged != null)), + color: _disabledTextColor( + context, onChanged != null)), ), ], ).paddingSymmetric(horizontal: 10), @@ -408,7 +416,7 @@ class _ConnectionState extends State<_Connection> _Button('ID/Relay Server', changeServer, enabled: enabled), ]), _Card(title: 'Service', children: [ - _OptionCheckBox('Enable Service', 'stop-service', + _OptionCheckBox(context, 'Enable Service', 'stop-service', reverse: true, enabled: enabled), // TODO: Not implemented // _option_check('Always connected via relay', 'allow-always-relay', enabled: enabled), @@ -416,10 +424,11 @@ class _ConnectionState extends State<_Connection> // reverse: true, enabled: enabled), ]), _Card(title: 'TCP Tunneling', children: [ - _OptionCheckBox('Enable TCP Tunneling', 'enable-tunnel', + _OptionCheckBox( + context, 'Enable TCP Tunneling', 'enable-tunnel', enabled: enabled), ]), - direct_ip(), + direct_ip(context), _Card(title: 'Proxy', children: [ _Button('Socks5 Proxy', changeSocks5Proxy, enabled: enabled), ]), @@ -430,12 +439,12 @@ class _ConnectionState extends State<_Connection> ]).marginOnly(bottom: _kListViewBottomMargin); } - Widget direct_ip() { + Widget direct_ip(BuildContext context) { TextEditingController controller = TextEditingController(); var update = () => setState(() {}); RxBool apply_enabled = false.obs; return _Card(title: 'Direct IP Access', children: [ - _OptionCheckBox('Enable Direct IP Access', 'direct-server', + _OptionCheckBox(context, 'Enable Direct IP Access', 'direct-server', update: update, enabled: !locked), _futureBuilder( future: () async { @@ -509,7 +518,7 @@ class _DisplayState extends State<_Display> with AutomaticKeepAliveClientMixin { return ListView( children: [ _Card(title: 'Adaptive Bitrate', children: [ - _OptionCheckBox('Adaptive Bitrate', 'enable-abr'), + _OptionCheckBox(context, 'Adaptive Bitrate', 'enable-abr'), ]), hwcodec(), ], @@ -523,7 +532,8 @@ class _DisplayState extends State<_Display> with AutomaticKeepAliveClientMixin { return Offstage( offstage: !(data as bool), child: _Card(title: 'Hardware Codec', children: [ - _OptionCheckBox('Enable hardware codec', 'enable-hwcodec'), + _OptionCheckBox( + context, 'Enable hardware codec', 'enable-hwcodec'), ]), ); }); @@ -592,6 +602,7 @@ class _AudioState extends State<_Audio> with AutomaticKeepAliveClientMixin { ); deviceWidget.addAll([ _Radio<_AudioInputType>( + context, value: _AudioInputType.Specify, groupValue: groupValue, label: 'Specify device', @@ -606,6 +617,7 @@ class _AudioState extends State<_Audio> with AutomaticKeepAliveClientMixin { } return Column(children: [ _Radio<_AudioInputType>( + context, value: _AudioInputType.Mute, groupValue: groupValue, label: 'Mute', @@ -615,6 +627,7 @@ class _AudioState extends State<_Audio> with AutomaticKeepAliveClientMixin { }, ), _Radio( + context, value: _AudioInputType.Standard, groupValue: groupValue, label: 'Use standard device', @@ -743,15 +756,11 @@ Widget _Card({required String title, required List children}) { ); } -Color? _disabledTextColor(bool enabled) { - return enabled - ? null - : isDarkTheme() - ? MyTheme.disabledTextDark - : MyTheme.disabledTextLight; +Color? _disabledTextColor(BuildContext context, bool enabled) { + return enabled ? null : MyTheme.color(context).lighterText; } -Widget _OptionCheckBox(String label, String key, +Widget _OptionCheckBox(BuildContext context, String label, String key, {Function()? update = null, bool reverse = false, bool enabled = true}) { return _futureBuilder( future: bind.mainGetOption(key: key), @@ -778,7 +787,7 @@ Widget _OptionCheckBox(String label, String key, Expanded( child: Text( translate(label), - style: TextStyle(color: _disabledTextColor(enabled)), + style: TextStyle(color: _disabledTextColor(context, enabled)), )) ], ), @@ -790,7 +799,7 @@ Widget _OptionCheckBox(String label, String key, }); } -Widget _Radio( +Widget _Radio(BuildContext context, {required T value, required T groupValue, required String label, @@ -811,7 +820,7 @@ Widget _Radio( child: Text(translate(label), style: TextStyle( fontSize: _kContentFontSize, - color: _disabledTextColor(enabled))) + color: _disabledTextColor(context, enabled))) .marginOnly(left: 5), ), ], diff --git a/flutter/lib/desktop/pages/desktop_tab_page.dart b/flutter/lib/desktop/pages/desktop_tab_page.dart index 5cbc7aece..45722174e 100644 --- a/flutter/lib/desktop/pages/desktop_tab_page.dart +++ b/flutter/lib/desktop/pages/desktop_tab_page.dart @@ -30,30 +30,35 @@ class _DesktopTabPageState extends State { @override Widget build(BuildContext context) { - return Scaffold( - body: Column( - children: [ - DesktopTabBar( - tabs: tabs, - dark: isDarkTheme(), - mainTab: true, - onAddSetting: onAddSetting, - ), - Obx((() => Expanded( - child: PageView( - controller: DesktopTabBar.controller.value, - children: tabs.map((tab) { - switch (tab.label) { - case kTabLabelHomePage: - return DesktopHomePage(key: ValueKey(tab.label)); - case kTabLabelSettingPage: - return DesktopSettingPage(key: ValueKey(tab.label)); - default: - return Container(); - } - }).toList()), - ))), - ], + return Container( + decoration: BoxDecoration( + border: Border.all(color: MyTheme.color(context).border!)), + child: Scaffold( + backgroundColor: MyTheme.color(context).bg, + body: Column( + children: [ + DesktopTabBar( + tabs: tabs, + dark: isDarkTheme(), + mainTab: true, + onAddSetting: onAddSetting, + ), + Obx((() => Expanded( + child: PageView( + controller: DesktopTabBar.controller.value, + children: tabs.map((tab) { + switch (tab.label) { + case kTabLabelHomePage: + return DesktopHomePage(key: ValueKey(tab.label)); + case kTabLabelSettingPage: + return DesktopSettingPage(key: ValueKey(tab.label)); + default: + return Container(); + } + }).toList()), + ))), + ], + ), ), ); } diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 0111e5f90..2868d2d3b 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -98,6 +98,7 @@ class _FileManagerPageState extends State return false; }, child: Scaffold( + backgroundColor: MyTheme.color(context).bg, body: Row( children: [ Flexible(flex: 3, child: body(isLocal: true)), diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 8aba86d0f..025db279f 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -181,10 +181,11 @@ class _RemotePageState extends State _ffi.inputKey(label, down: down, press: press ?? false); } - Widget buildBody(FfiModel ffiModel) { + Widget buildBody(BuildContext context, FfiModel ffiModel) { final hasDisplays = ffiModel.pi.displays.length > 0; final keyboard = ffiModel.permissions['keyboard'] != false; return Scaffold( + backgroundColor: MyTheme.color(context).bg, // resizeToAvoidBottomInset: true, floatingActionButton: _showBar ? null @@ -229,7 +230,8 @@ class _RemotePageState extends State ChangeNotifierProvider.value(value: _ffi.canvasModel), ], child: Consumer( - builder: (context, ffiModel, _child) => buildBody(ffiModel)))); + builder: (context, ffiModel, _child) => + buildBody(context, ffiModel)))); } Widget getRawPointerAndKeyBody(Widget child) { diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 094659251..bf39f4dc6 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -13,7 +13,7 @@ import 'package:scroll_pos/scroll_pos.dart'; const double _kTabBarHeight = kDesktopRemoteTabBarHeight; const double _kIconSize = 18; const double _kDividerIndent = 10; -const double _kAddIconSize = _kTabBarHeight - 15; +const double _kActionIconSize = 12; final _tabBarKey = GlobalKey(); void closeTab(String? id) { @@ -79,63 +79,81 @@ class DesktopTabBar extends StatelessWidget { Widget build(BuildContext context) { return Container( height: _kTabBarHeight, - child: Row( + child: Column( children: [ - Expanded( + Container( + height: _kTabBarHeight - 1, child: Row( children: [ - Offstage( - offstage: !mainTab, - child: Row(children: [ - Image.asset('assets/logo.ico'), - Text("RustDesk").paddingOnly(left: 5), - ]).paddingSymmetric(horizontal: 12, vertical: 5), - ), Expanded( - child: GestureDetector( - onPanStart: (_) { - if (mainTab) { - windowManager.startDragging(); - } else { - WindowController.fromWindowId(windowId!) - .startDragging(); - } - }, - child: _ListView( - key: _tabBarKey, - controller: controller, - scrollController: scrollController, - tabInfos: tabs, - selected: selected, - onTabClose: onTabClose, - theme: _theme)), + child: Row( + children: [ + Offstage( + offstage: !mainTab, + child: Row(children: [ + Image.asset( + 'assets/logo.ico', + width: 20, + height: 20, + ), + Text( + "RustDesk", + style: TextStyle(fontSize: 13), + ).marginOnly(left: 2), + ]).marginOnly( + left: 5, + right: 10, + ), + ), + Expanded( + child: GestureDetector( + onPanStart: (_) { + if (mainTab) { + windowManager.startDragging(); + } else { + WindowController.fromWindowId(windowId!) + .startDragging(); + } + }, + child: _ListView( + key: _tabBarKey, + controller: controller, + scrollController: scrollController, + tabInfos: tabs, + selected: selected, + onTabClose: onTabClose, + theme: _theme)), + ), + Offstage( + offstage: mainTab, + child: _AddButton( + theme: _theme, + ).paddingOnly(left: 10), + ), + ], + ), ), Offstage( - offstage: mainTab, - child: _AddButton( + offstage: onAddSetting == null, + child: _ActionIcon( + message: 'Settings', + icon: IconFont.menu, theme: _theme, - ).paddingOnly(left: 10), + onTap: () => onAddSetting?.call(), + is_close: false, + ), ), + WindowActionPanel( + mainTab: mainTab, + theme: _theme, + ) ], ), ), - Offstage( - offstage: onAddSetting == null, - child: Tooltip( - message: translate("Settings"), - child: InkWell( - child: Icon( - Icons.menu, - color: _theme.unSelectedIconColor, - ), - onTap: () => onAddSetting?.call(), - ).paddingOnly(right: 10), - ), + Divider( + height: 1, + thickness: 1, ), - WindowActionPanel( - mainTab: mainTab, - color: _theme.unSelectedIconColor, - ) ], ), ); @@ -156,85 +174,88 @@ class DesktopTabBar extends StatelessWidget { class WindowActionPanel extends StatelessWidget { final bool mainTab; - final Color color; + final _Theme theme; const WindowActionPanel( - {Key? key, required this.mainTab, required this.color}) + {Key? key, required this.mainTab, required this.theme}) : super(key: key); @override Widget build(BuildContext context) { return Row( children: [ - Tooltip( - message: translate("Minimize"), - child: InkWell( - child: Icon( - Icons.minimize, - color: color, - ).paddingSymmetric(horizontal: 5), - onTap: () { - if (mainTab) { - windowManager.minimize(); - } else { - WindowController.fromWindowId(windowId!).minimize(); - } - }, - ), + _ActionIcon( + message: 'Minimize', + icon: IconFont.min, + theme: theme, + onTap: () { + if (mainTab) { + windowManager.minimize(); + } else { + WindowController.fromWindowId(windowId!).minimize(); + } + }, + is_close: false, ), - Tooltip( - message: translate("Maximize"), - child: InkWell( - child: Icon( - Icons.rectangle_outlined, - color: color, - size: 20, - ).paddingSymmetric(horizontal: 5), - onTap: () { - if (mainTab) { - windowManager.isMaximized().then((maximized) { - if (maximized) { + FutureBuilder(builder: (context, snapshot) { + RxBool is_maximized = false.obs; + if (mainTab) { + windowManager.isMaximized().then((maximized) { + is_maximized.value = maximized; + }); + } else { + final wc = WindowController.fromWindowId(windowId!); + wc.isMaximized().then((maximized) { + is_maximized.value = maximized; + }); + } + return Obx( + () => _ActionIcon( + message: is_maximized.value ? "Restore" : "Maximize", + icon: is_maximized.value ? IconFont.restore : IconFont.max, + theme: theme, + onTap: () { + if (mainTab) { + if (is_maximized.value) { windowManager.unmaximize(); } else { windowManager.maximize(); } - }); - } else { - final wc = WindowController.fromWindowId(windowId!); - wc.isMaximized().then((maximized) { - if (maximized) { + } else { + final wc = WindowController.fromWindowId(windowId!); + if (is_maximized.value) { wc.unmaximize(); } else { wc.maximize(); } - }); - } - }, - ), + } + is_maximized.value = !is_maximized.value; + }, + is_close: false, + ), + ); + }), + _ActionIcon( + message: 'Close', + icon: IconFont.close, + theme: theme, + onTap: () { + if (mainTab) { + windowManager.close(); + } else { + WindowController.fromWindowId(windowId!).close(); + } + }, + is_close: true, ), - Tooltip( - message: translate("Close"), - child: InkWell( - child: Icon( - Icons.close, - color: color, - ).paddingSymmetric(horizontal: 5), - onTap: () { - if (mainTab) { - windowManager.close(); - } else { - WindowController.fromWindowId(windowId!).close(); - } - }, - ), - ) ], ); } } +// ignore: must_be_immutable class _ListView extends StatelessWidget { - late Rx controller; + final Rx controller; final ScrollPosController scrollController; final RxList tabInfos; final Rx selected; @@ -327,74 +348,60 @@ class _Tab extends StatelessWidget { Widget build(BuildContext context) { bool is_selected = index == selected; bool show_divider = index != selected - 1 && index != selected; - return Stack( - children: [ - Ink( - child: InkWell( - onHover: (hover) => _hover.value = hover, - onTap: () => onSelected(), - child: Row( - children: [ - Container( - height: _kTabBarHeight, - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, + return Ink( + child: InkWell( + onHover: (hover) => _hover.value = hover, + onTap: () => onSelected(), + child: Row( + children: [ + Container( + height: _kTabBarHeight, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - is_selected ? selectedIcon : unselectedIcon, - size: _kIconSize, + Icon( + is_selected ? selectedIcon : unselectedIcon, + size: _kIconSize, + color: is_selected + ? theme.selectedtabIconColor + : theme.unSelectedtabIconColor, + ).paddingOnly(right: 5), + Text( + translate(label), + textAlign: TextAlign.center, + style: TextStyle( color: is_selected - ? theme.selectedtabIconColor - : theme.unSelectedtabIconColor, - ).paddingOnly(right: 5), - Text( - translate(label), - textAlign: TextAlign.center, - style: TextStyle( - color: is_selected - ? theme.selectedTextColor - : theme.unSelectedTextColor), - ), - ], + ? theme.selectedTextColor + : theme.unSelectedTextColor), ), - Offstage( - offstage: !closable, - child: Obx((() => _CloseButton( - visiable: _hover.value, - tabSelected: is_selected, - onClose: () => onClose(), - theme: theme, - ))), - ) - ])).paddingSymmetric(horizontal: 10), - Offstage( - offstage: !show_divider, - child: VerticalDivider( - width: 1, - indent: _kDividerIndent, - endIndent: _kDividerIndent, - color: theme.dividerColor, - thickness: 1, - ), - ) - ], - ), - ), + ], + ), + Offstage( + offstage: !closable, + child: Obx((() => _CloseButton( + visiable: _hover.value, + tabSelected: is_selected, + onClose: () => onClose(), + theme: theme, + ))), + ) + ])).paddingSymmetric(horizontal: 10), + Offstage( + offstage: !show_divider, + child: VerticalDivider( + width: 1, + indent: _kDividerIndent, + endIndent: _kDividerIndent, + color: theme.dividerColor, + thickness: 1, + ), + ) + ], ), - Positioned( - height: 2, - left: 0, - right: 0, - bottom: 0, - child: Center( - child: Container( - color: - is_selected ? theme.indicatorColor : Colors.transparent), - )) - ], + ), ); } } @@ -409,19 +416,13 @@ class _AddButton extends StatelessWidget { @override Widget build(BuildContext context) { - return Ink( - height: _kTabBarHeight, - child: InkWell( - customBorder: const CircleBorder(), + return _ActionIcon( + message: 'New Connection', + icon: IconFont.add, + theme: theme, onTap: () => rustDeskWinManager.call(WindowType.Main, "main_window_on_top", ""), - child: Icon( - Icons.add_sharp, - size: _kAddIconSize, - color: theme.unSelectedIconColor, - ), - ), - ); + is_close: false); } } @@ -460,6 +461,46 @@ class _CloseButton extends StatelessWidget { } } +class _ActionIcon extends StatelessWidget { + final String message; + final IconData icon; + final _Theme theme; + final Function() onTap; + final bool is_close; + const _ActionIcon({ + Key? key, + required this.message, + required this.icon, + required this.theme, + required this.onTap, + required this.is_close, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + RxBool hover = false.obs; + return Obx(() => Tooltip( + message: translate(message), + child: InkWell( + hoverColor: is_close ? Colors.red : theme.hoverColor, + onHover: (value) => hover.value = value, + child: Container( + height: _kTabBarHeight - 1, + width: _kTabBarHeight - 1, + child: Icon( + icon, + color: hover.value && is_close + ? Colors.white + : theme.unSelectedIconColor, + size: _kActionIconSize, + ), + ), + onTap: onTap, + ), + )); + } +} + class _Theme { late Color unSelectedtabIconColor; late Color selectedtabIconColor; @@ -468,7 +509,7 @@ class _Theme { late Color selectedIconColor; late Color unSelectedIconColor; late Color dividerColor; - late Color indicatorColor; + late Color hoverColor; _Theme.light() { unSelectedtabIconColor = Color.fromARGB(255, 162, 203, 241); @@ -478,7 +519,7 @@ class _Theme { selectedIconColor = Color.fromARGB(255, 26, 26, 26); unSelectedIconColor = Color.fromARGB(255, 96, 96, 96); dividerColor = Color.fromARGB(255, 238, 238, 238); - indicatorColor = MyTheme.accent; + hoverColor = Colors.grey.withOpacity(0.2); } _Theme.dark() { @@ -489,6 +530,6 @@ class _Theme { selectedIconColor = Color.fromARGB(255, 215, 215, 215); unSelectedIconColor = Color.fromARGB(255, 255, 255, 255); dividerColor = Color.fromARGB(255, 64, 64, 64); - indicatorColor = MyTheme.accent; + hoverColor = Colors.black26; } } diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index ddbbb32b7..5a5e55a05 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -108,6 +108,9 @@ flutter: - family: GestureIcons fonts: - asset: assets/gestures.ttf + - family: IconFont + fonts: + - asset: assets/tabbar.ttf # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware. From 05771e65e2377c5bd668c20756b768c98ffea58f Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 22 Aug 2022 13:51:05 +0800 Subject: [PATCH 0250/2015] feat: can resize window when without title bar Signed-off-by: Kingtous --- .../desktop/pages/connection_tab_page.dart | 59 ++++++++++--------- .../lib/desktop/pages/desktop_tab_page.dart | 59 ++++++++++--------- .../desktop/pages/file_manager_tab_page.dart | 44 +++++++------- flutter/pubspec.lock | 4 +- flutter/pubspec.yaml | 3 +- 5 files changed, 89 insertions(+), 80 deletions(-) diff --git a/flutter/lib/desktop/pages/connection_tab_page.dart b/flutter/lib/desktop/pages/connection_tab_page.dart index 5dd4a829a..be5ec82af 100644 --- a/flutter/lib/desktop/pages/connection_tab_page.dart +++ b/flutter/lib/desktop/pages/connection_tab_page.dart @@ -72,34 +72,37 @@ class _ConnectionTabPageState extends State { @override Widget build(BuildContext context) { - return Scaffold( - body: Column( - children: [ - Obx(() => Visibility( - visible: _fullscreenID.value.isEmpty, - child: DesktopTabBar( - tabs: tabs, - onTabClose: onRemoveId, - dark: isDarkTheme(), - mainTab: false, - ))), - Expanded(child: Obx(() { - WindowController.fromWindowId(windowId()) - .setFullscreen(_fullscreenID.value.isNotEmpty); - return PageView( - controller: DesktopTabBar.controller.value, - children: tabs - .map((tab) => RemotePage( - key: ValueKey(tab.label), - id: tab.label, - tabBarHeight: _fullscreenID.value.isNotEmpty - ? 0 - : kDesktopRemoteTabBarHeight, - fullscreenID: _fullscreenID, - )) //RemotePage(key: ValueKey(e), id: e)) - .toList()); - })), - ], + return SubWindowDragToResizeArea( + windowId: windowId(), + child: Scaffold( + body: Column( + children: [ + Obx(() => Visibility( + visible: _fullscreenID.value.isEmpty, + child: DesktopTabBar( + tabs: tabs, + onTabClose: onRemoveId, + dark: isDarkTheme(), + mainTab: false, + ))), + Expanded(child: Obx(() { + WindowController.fromWindowId(windowId()) + .setFullscreen(_fullscreenID.value.isNotEmpty); + return PageView( + controller: DesktopTabBar.controller.value, + children: tabs + .map((tab) => RemotePage( + key: ValueKey(tab.label), + id: tab.label, + tabBarHeight: _fullscreenID.value.isNotEmpty + ? 0 + : kDesktopRemoteTabBarHeight, + fullscreenID: _fullscreenID, + )) //RemotePage(key: ValueKey(e), id: e)) + .toList()); + })), + ], + ), ), ); } diff --git a/flutter/lib/desktop/pages/desktop_tab_page.dart b/flutter/lib/desktop/pages/desktop_tab_page.dart index 45722174e..5c108f39f 100644 --- a/flutter/lib/desktop/pages/desktop_tab_page.dart +++ b/flutter/lib/desktop/pages/desktop_tab_page.dart @@ -5,6 +5,7 @@ import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart'; import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart'; import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; import 'package:get/get.dart'; +import 'package:window_manager/window_manager.dart'; class DesktopTabPage extends StatefulWidget { const DesktopTabPage({Key? key}) : super(key: key); @@ -30,34 +31,36 @@ class _DesktopTabPageState extends State { @override Widget build(BuildContext context) { - return Container( - decoration: BoxDecoration( - border: Border.all(color: MyTheme.color(context).border!)), - child: Scaffold( - backgroundColor: MyTheme.color(context).bg, - body: Column( - children: [ - DesktopTabBar( - tabs: tabs, - dark: isDarkTheme(), - mainTab: true, - onAddSetting: onAddSetting, - ), - Obx((() => Expanded( - child: PageView( - controller: DesktopTabBar.controller.value, - children: tabs.map((tab) { - switch (tab.label) { - case kTabLabelHomePage: - return DesktopHomePage(key: ValueKey(tab.label)); - case kTabLabelSettingPage: - return DesktopSettingPage(key: ValueKey(tab.label)); - default: - return Container(); - } - }).toList()), - ))), - ], + return DragToResizeArea( + child: Container( + decoration: BoxDecoration( + border: Border.all(color: MyTheme.color(context).border!)), + child: Scaffold( + backgroundColor: MyTheme.color(context).bg, + body: Column( + children: [ + DesktopTabBar( + tabs: tabs, + dark: isDarkTheme(), + mainTab: true, + onAddSetting: onAddSetting, + ), + Obx((() => Expanded( + child: PageView( + controller: DesktopTabBar.controller.value, + children: tabs.map((tab) { + switch (tab.label) { + case kTabLabelHomePage: + return DesktopHomePage(key: ValueKey(tab.label)); + case kTabLabelSettingPage: + return DesktopSettingPage(key: ValueKey(tab.label)); + default: + return Container(); + } + }).toList()), + ))), + ], + ), ), ), ); diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index 5f12c873a..12b5b20ff 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -68,27 +68,31 @@ class _FileManagerTabPageState extends State { @override Widget build(BuildContext context) { - return Scaffold( - body: Column( - children: [ - DesktopTabBar( - tabs: tabs, - onTabClose: onRemoveId, - dark: isDarkTheme(), - mainTab: false, - ), - Expanded( - child: Obx( - () => PageView( - controller: DesktopTabBar.controller.value, - children: tabs - .map((tab) => FileManagerPage( - key: ValueKey(tab.label), - id: tab.label)) //RemotePage(key: ValueKey(e), id: e)) - .toList()), + return SubWindowDragToResizeArea( + windowId: windowId(), + child: Scaffold( + body: Column( + children: [ + DesktopTabBar( + tabs: tabs, + onTabClose: onRemoveId, + dark: isDarkTheme(), + mainTab: false, ), - ) - ], + Expanded( + child: Obx( + () => PageView( + controller: DesktopTabBar.controller.value, + children: tabs + .map((tab) => FileManagerPage( + key: ValueKey(tab.label), + id: tab + .label)) //RemotePage(key: ValueKey(e), id: e)) + .toList()), + ), + ) + ], + ), ), ); } diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 34b39cb56..078451a50 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -252,8 +252,8 @@ packages: dependency: "direct main" description: path: "." - ref: "6e6b6f557f655e9c985007d754b6282a0e524932" - resolved-ref: "6e6b6f557f655e9c985007d754b6282a0e524932" + ref: "56c4ca21d0319597f6c19f56b34f1cae6bfc78b9" + resolved-ref: "56c4ca21d0319597f6c19f56b34f1cae6bfc78b9" url: "https://github.com/Kingtous/rustdesk_desktop_multi_window" source: git version: "0.1.0" diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 13fd96d97..c00e4fce6 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -63,10 +63,9 @@ dependencies: url: https://github.com/Kingtous/rustdesk_window_manager ref: 75a6c813babca461f359a586785d797f7806e390 desktop_multi_window: - # path: ../../rustdesk_desktop_multi_window git: url: https://github.com/Kingtous/rustdesk_desktop_multi_window - ref: 6e6b6f557f655e9c985007d754b6282a0e524932 + ref: 56c4ca21d0319597f6c19f56b34f1cae6bfc78b9 freezed_annotation: ^2.0.3 tray_manager: git: From 48e25accae3229115b27a55bd1322da62ba51308 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 22 Aug 2022 14:21:38 +0800 Subject: [PATCH 0251/2015] fix: resize issue found in window manager Signed-off-by: Kingtous --- flutter/pubspec.lock | 8 ++++---- flutter/pubspec.yaml | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 078451a50..fef32af73 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -252,8 +252,8 @@ packages: dependency: "direct main" description: path: "." - ref: "56c4ca21d0319597f6c19f56b34f1cae6bfc78b9" - resolved-ref: "56c4ca21d0319597f6c19f56b34f1cae6bfc78b9" + ref: e013c81d75320bbf28adddeaadf462264ee6039d + resolved-ref: e013c81d75320bbf28adddeaadf462264ee6039d url: "https://github.com/Kingtous/rustdesk_desktop_multi_window" source: git version: "0.1.0" @@ -1230,8 +1230,8 @@ packages: dependency: "direct main" description: path: "." - ref: "75a6c813babca461f359a586785d797f7806e390" - resolved-ref: "75a6c813babca461f359a586785d797f7806e390" + ref: f1d69e5d0531af947373ec26ae22808f08b1aac6 + resolved-ref: f1d69e5d0531af947373ec26ae22808f08b1aac6 url: "https://github.com/Kingtous/rustdesk_window_manager" source: git version: "0.2.5" diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index c00e4fce6..0f3677402 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -61,11 +61,11 @@ dependencies: window_manager: git: url: https://github.com/Kingtous/rustdesk_window_manager - ref: 75a6c813babca461f359a586785d797f7806e390 + ref: f1d69e5d0531af947373ec26ae22808f08b1aac6 desktop_multi_window: git: url: https://github.com/Kingtous/rustdesk_desktop_multi_window - ref: 56c4ca21d0319597f6c19f56b34f1cae6bfc78b9 + ref: e013c81d75320bbf28adddeaadf462264ee6039d freezed_annotation: ^2.0.3 tray_manager: git: From 2c7f0d7588d403fc7c7e1a26850a6b69cd4fee05 Mon Sep 17 00:00:00 2001 From: csf Date: Thu, 18 Aug 2022 19:49:41 +0800 Subject: [PATCH 0252/2015] fix cm event listener & switch permission --- flutter/lib/common.dart | 10 +- flutter/lib/desktop/pages/server_page.dart | 193 ++++++++------------- flutter/lib/models/server_model.dart | 6 +- src/flutter.rs | 11 +- 4 files changed, 87 insertions(+), 133 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 8570e5b7e..e963993f7 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -29,14 +29,16 @@ int androidVersion = 0; typedef F = String Function(String); typedef FMethod = String Function(String, dynamic); -final iconKeyboard = MemoryImage(Uint8List.fromList(base64Decode( +late final iconKeyboard = MemoryImage(Uint8List.fromList(base64Decode( "iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAgVBMVEUAAAD///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////9d3yJTAAAAKnRSTlMA0Gd/0y8ILZgbJffDPUwV2nvzt+TMqZxyU7CMb1pYQyzsvKunkXE4AwJnNC24AAAA+0lEQVQ4y83O2U7DMBCF4ZMxk9rZk26kpQs7nPd/QJy4EiLbLf01N5Y/2YP/qxDFQvGB5NPC/ZpVnfJx4b5xyGfF95rkHvNCWH1u+N6J6T0sC7gqRy8uGPfBLEbozPXUjlkQKwGaFPNizwQbwkx0TDvhCii34ExZCSQVBdzIOEOyeclSHgBGXkpeygXSQgStACtWx4Z8rr8COHOvfEP/IbbsQAToFUAAV1M408IIjIGYAPoCSNRP7DQutfQTqxuAiH7UUg1FaJR2AGrrx52sK2ye28LZ0wBAEyR6y8X+NADhm1B4fgiiHXbRrTrxpwEY9RdM9wsepnvFHfUDwYEeiwAJr/gAAAAASUVORK5CYII="))); -final iconClipboard = MemoryImage(Uint8List.fromList(base64Decode( +late final iconClipboard = MemoryImage(Uint8List.fromList(base64Decode( 'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAjVBMVEUAAAD///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8DizOFAAAALnRSTlMAnIsyZy8YZF3NSAuabRL34cq6trCScyZ4qI9CQDwV+fPl2tnTwzkeB+m/pIFK/Xx0ewAAAQlJREFUOMudktduhDAQRWep69iY3tle0+7/f16Qg7MsJUQ5Dwh8jzRzhemJPIaf3GiW7eFQfOwDPp1ek/iMnKgBi5PrhJAhZAa1lCxE9pw5KWMswOMAQXuQOvqTB7tLFJ36wimKLrufZTzUaoRtdthqRA2vEwS+tR4qguiElRKk1YMrYfUQRkwLmwVBYDMvJKF8R0o3V2MOhNrfo+hXSYYjPn1L/S+n438t8gWh+q1F+cYFBMm1Jh8Ia7y2OWXQxMMRLqr2eTc1crSD84cWfEGwYM4LlaACEee2ZjsQXJxR3qmYb+GpC8ZfNM5oh3yxxbxgQE7lEkb3ZvvH1BiRHn1bu02ICcKGWr4AudUkyYxmvywAAAAASUVORK5CYII='))); -final iconAudio = MemoryImage(Uint8List.fromList(base64Decode( +late final iconAudio = MemoryImage(Uint8List.fromList(base64Decode( 'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAk1BMVEUAAAD////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////ROyVeAAAAMHRSTlMAgfz08DDqCAThvraZjEcoGA751JxzbGdfTRP25NrIpaGTcEM+HAvMuKinhXhWNx9Yzm/gAAABFUlEQVQ4y82S2XLCMAxFheMsQNghCQFalkL39vz/11V4GpNk0r629+Va1pmxPFfyh1ravOP2Y1ydJmBO0lYP3r+PyQ62s2Y7fgF6VRXOYdToT++ogIuoVhCUtX7YpwJG3F8f6V8rr3WABwwUahlEvr8y3IBniGKdKYBQ5OGQpukQakBpIVcfwptIhJcf8hWGakdndAAhBInIGHbdQGJg6jjbDUgEE5EpmB+AAM4uj6gb+AQT6wdhITLvAHJ4VCtgoAlG1tpNA0gWON/f4ioHdSADc1bfgt+PZFkDlD6ojWF+kVoaHlhvFjPHuVRrefohY1GdcFm1N8JvwEyrJ/X2Th2rIoVgIi3Fo6Xf0z5k8psKu5f/oi+nHjjI92o36AAAAABJRU5ErkJggg=='))); -final iconFile = MemoryImage(Uint8List.fromList(base64Decode( +late final iconFile = MemoryImage(Uint8List.fromList(base64Decode( 'iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAAUVBMVEUAAAD///////////////////////////////////////////////////////////////////////////////////////////////////////8IN+deAAAAGnRSTlMAH+CAESEN8jyZkcIb5N/ONy3vmHhmiGjUm7UwS+YAAAHZSURBVGje7dnbboMwDIBhBwgQoFAO7Ta//4NOqCAXYZQstatq4r+r5ubrgQSpg8iyC4ZURa+PlIpQYGiwrzyeHtYZjAL8T05O4H8BbbKvFgRa4NoBU8pXeYEkDDgaaLQBcwJrmeErJQB/7wes3QBWGnCIX0+AQycL1PO6BMwPa0nA4ZxbgTvOjUYMGPHRnZkQAY4mxPZBjmy53E7ukSkFKYB/D4XsWZQx64sCeYebOogGsoOBYvv6/UCb8F0IOBZ0TlP6lEYdANY350AJqB9/qPVuOI5evw4A1hgLigAlepnyxW80bcCcwN++A2s82Vcu02ta+ceq9BoL5KGTTRwQPlpqA3gCnwWU2kCDgeWRQPj2jAPCDxgCMjhI6uZnToDpvd/BJeFrJQB/fsAa02gCt3mi1wNuy8GgBNDZlysBNNSrADVSjcJl6vCpUn6jOdx0kz0q6PMhQRa4465SFKhx35cgUCBTwj2/NHwZAb71qR8GEP2H1XcmAtBPTEO67GP6FUUAIKGABbDLQ0EArhN2sAIGesRO+iyy+RMAjckVTlMCKFVAbh/4Af9OPgG61SkDVco3BQGT3GXaDAnTIAcYZDuBTwGsAGDxuBFeAQqIqwoFMlAVLrHr/wId5MPt0nilGgAAAABJRU5ErkJggg=='))); +late final iconRestart = MemoryImage(Uint8List.fromList(base64Decode( + 'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAB7BAAAewQHDaVRTAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAbhJREFUWIXVlrFqFGEUhb+7UYxaWCQKlrKKxaZSQVGDJih2tj6MD2DnMwiWvoAIRnENIpZiYxEro6IooiS7SPwsMgNLkk3mjmYmnmb45/73nMNwz/x/qH3gMu2gH6rAU+Blw+Lngau4jpmGxVF7qp1iPWjaQKnZ2WnXbuP/NqAeUPc3ZkA9XDwvqc+BVWCgPlJ7tRwUKThZce819b46VH+pfXVRXVO/q2cSul3VOgZUl0ejq86r39TXI8mqZKDuDEwCw3IREQvAbWAGmMsQZQ0sAl3gHPB1Q+0e8BuYzRDuy2yOiFVgaUxtRf0ETGc4syk4rc6PqU0Cx9j8Zf6dAeAK8Fi9sUXtFjABvEgxJlNwRP2svlNPjbw/q35U36oTFbnyMSwabxb/gB/qA3VBHagrauV7RW0DRfP1IvMlXqkXkhz1DYyQTKtHa/Z2VVMx3IiI+PI3/bCHjuOpFrSnAMpL6QfgTcMGesDx0kBr2BMzsNyi/vtQu8CJlgwsRbZDnWP90NkKaxHxJMOXMqAeAn5u0ydwMCKGY+qbkB3C2W3EKWoXk5zVoHbUZ+6Mh7tl4G4F8RJ3qvL+AfV3r5Vdpj70AAAAAElFTkSuQmCC'))); class IconFont { static const _family = 'iconfont'; diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index 32130ad2e..beaf9c1eb 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -2,109 +2,14 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -// import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:provider/provider.dart'; import 'package:window_manager/window_manager.dart'; import '../../common.dart'; -import '../../mobile/pages/home_page.dart'; import '../../models/platform_model.dart'; import '../../models/server_model.dart'; -class DesktopServerPage extends StatefulWidget implements PageShape { - @override - final title = translate("Share Screen"); - - @override - final icon = Icon(Icons.mobile_screen_share); - - @override - final appBarActions = [ - PopupMenuButton( - icon: Icon(Icons.more_vert), - itemBuilder: (context) { - return [ - PopupMenuItem( - child: Text(translate("Change ID")), - padding: EdgeInsets.symmetric(horizontal: 16.0), - value: "changeID", - enabled: false, - ), - PopupMenuItem( - child: Text(translate("Set permanent password")), - padding: EdgeInsets.symmetric(horizontal: 16.0), - value: "setPermanentPassword", - enabled: - gFFI.serverModel.verificationMethod != kUseTemporaryPassword, - ), - PopupMenuItem( - child: Text(translate("Set temporary password length")), - padding: EdgeInsets.symmetric(horizontal: 16.0), - value: "setTemporaryPasswordLength", - enabled: - gFFI.serverModel.verificationMethod != kUsePermanentPassword, - ), - const PopupMenuDivider(), - PopupMenuItem( - padding: EdgeInsets.symmetric(horizontal: 0.0), - value: kUseTemporaryPassword, - child: Container( - child: ListTile( - title: Text(translate("Use temporary password")), - trailing: Icon( - Icons.check, - color: gFFI.serverModel.verificationMethod == - kUseTemporaryPassword - ? null - : Color(0xFFFFFFFF), - ))), - ), - PopupMenuItem( - padding: EdgeInsets.symmetric(horizontal: 0.0), - value: kUsePermanentPassword, - child: ListTile( - title: Text(translate("Use permanent password")), - trailing: Icon( - Icons.check, - color: gFFI.serverModel.verificationMethod == - kUsePermanentPassword - ? null - : Color(0xFFFFFFFF), - )), - ), - PopupMenuItem( - padding: EdgeInsets.symmetric(horizontal: 0.0), - value: kUseBothPasswords, - child: ListTile( - title: Text(translate("Use both passwords")), - trailing: Icon( - Icons.check, - color: gFFI.serverModel.verificationMethod != - kUseTemporaryPassword && - gFFI.serverModel.verificationMethod != - kUsePermanentPassword - ? null - : Color(0xFFFFFFFF), - )), - ), - ]; - }, - onSelected: (value) { - if (value == "changeID") { - // TODO - } else if (value == "setPermanentPassword") { - // setPermanentPasswordDialog(); - } else if (value == "setTemporaryPasswordLength") { - // setTemporaryPasswordLengthDialog(); - } else if (value == kUsePermanentPassword || - value == kUseTemporaryPassword || - value == kUseBothPasswords) { - bind.mainSetOption(key: "verification-method", value: value); - gFFI.serverModel.updatePasswordModel(); - } - }) - ]; - +class DesktopServerPage extends StatefulWidget { @override State createState() => _DesktopServerPageState(); } @@ -112,22 +17,27 @@ class DesktopServerPage extends StatefulWidget implements PageShape { class _DesktopServerPageState extends State with AutomaticKeepAliveClientMixin { @override + void initState() { + gFFI.ffiModel.updateEventListener(""); + super.initState(); + } + Widget build(BuildContext context) { super.build(context); return ChangeNotifierProvider.value( value: gFFI.serverModel, child: Consumer( builder: (context, serverModel, child) => Material( - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Expanded(child: ConnectionManager()), - SizedBox.fromSize(size: Size(0, 15.0)), - ], - ), - ), - ))); + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Expanded(child: ConnectionManager()), + SizedBox.fromSize(size: Size(0, 15.0)), + ], + ), + ), + ))); } @override @@ -359,18 +269,24 @@ class _CmHeaderState extends State<_CmHeader> bool get wantKeepAlive => true; } -class _PrivilegeBoard extends StatelessWidget { +class _PrivilegeBoard extends StatefulWidget { final Client client; const _PrivilegeBoard({Key? key, required this.client}) : super(key: key); + @override + State createState() => _PrivilegeBoardState(); +} + +class _PrivilegeBoardState extends State<_PrivilegeBoard> { + late final client = widget.client; Widget buildPermissionIcon(bool enabled, ImageProvider icon, Function(bool)? onTap, String? tooltip) { return Tooltip( message: tooltip ?? "", child: Ink( decoration: - BoxDecoration(color: enabled ? MyTheme.accent80 : Colors.grey), + BoxDecoration(color: enabled ? MyTheme.accent80 : Colors.grey), padding: EdgeInsets.all(4.0), child: InkWell( onTap: () => onTap?.call(!enabled), @@ -401,14 +317,41 @@ class _PrivilegeBoard extends StatelessWidget { ), Row( children: [ - buildPermissionIcon( - client.keyboard, iconKeyboard, (enable) => null, null), - buildPermissionIcon( - client.clipboard, iconClipboard, (enable) => null, null), - buildPermissionIcon( - client.audio, iconAudio, (enable) => null, null), - // TODO: file transfer - buildPermissionIcon(false, iconFile, (enable) => null, null), + buildPermissionIcon(client.keyboard, iconKeyboard, (enabled) { + bind.cmSwitchPermission( + connId: client.id, name: "keyboard", enabled: enabled); + setState(() { + client.keyboard = enabled; + }); + }, null), + buildPermissionIcon(client.clipboard, iconClipboard, (enabled) { + bind.cmSwitchPermission( + connId: client.id, name: "clipboard", enabled: enabled); + setState(() { + client.clipboard = enabled; + }); + }, null), + buildPermissionIcon(client.audio, iconAudio, (enabled) { + bind.cmSwitchPermission( + connId: client.id, name: "audio", enabled: enabled); + setState(() { + client.audio = enabled; + }); + }, null), + buildPermissionIcon(client.file, iconFile, (enabled) { + bind.cmSwitchPermission( + connId: client.id, name: "file", enabled: enabled); + setState(() { + client.file = enabled; + }); + }, null), + buildPermissionIcon(client.restart, iconRestart, (enabled) { + bind.cmSwitchPermission( + connId: client.id, name: "restart", enabled: enabled); + setState(() { + client.restart = enabled; + }); + }, null), ], ), ], @@ -530,9 +473,9 @@ class PaddingCard extends StatelessWidget { children: [ titleIcon != null ? Padding( - padding: EdgeInsets.only(right: 10), - child: Icon(titleIcon, - color: MyTheme.accent80, size: 30)) + padding: EdgeInsets.only(right: 10), + child: Icon(titleIcon, + color: MyTheme.accent80, size: 30)) : SizedBox.shrink(), Text( title!, @@ -579,12 +522,12 @@ Widget clientInfo(Client client) { crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [ - Text(client.name, - style: TextStyle(color: MyTheme.idColor, fontSize: 18)), - SizedBox(width: 8), - Text(client.peerId, - style: TextStyle(color: MyTheme.idColor, fontSize: 10)) - ])) + Text(client.name, + style: TextStyle(color: MyTheme.idColor, fontSize: 18)), + SizedBox(width: 8), + Text(client.peerId, + style: TextStyle(color: MyTheme.idColor, fontSize: 10)) + ])) ], ), ])); diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index 0bbb0c13e..9f69dd04a 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -366,7 +366,7 @@ class ServerModel with ChangeNotifier { _clients[client.id] = client; scrollToBottom(); notifyListeners(); - showLoginDialog(client); + if (isAndroid) showLoginDialog(client); } catch (e) { debugPrint("Failed to call loginRequest,error:$e"); } @@ -483,6 +483,8 @@ class Client { bool keyboard = false; bool clipboard = false; bool audio = false; + bool file = false; + bool restart = false; Client(this.authorized, this.isFileTransfer, this.name, this.peerId, this.keyboard, this.clipboard, this.audio); @@ -496,6 +498,8 @@ class Client { keyboard = json['keyboard']; clipboard = json['clipboard']; audio = json['audio']; + file = json['file']; + restart = json['restart']; } Map toJson() { diff --git a/src/flutter.rs b/src/flutter.rs index 928be607d..dfdef8c5a 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -41,6 +41,7 @@ use crate::{client::*, flutter_ffi::EventToUI, make_fd_flutter}; pub(super) const APP_TYPE_MAIN: &str = "main"; pub(super) const APP_TYPE_DESKTOP_REMOTE: &str = "remote"; pub(super) const APP_TYPE_DESKTOP_FILE_TRANSFER: &str = "file transfer"; +pub(super) const APP_TYPE_DESKTOP_CONNECTION_MANAGER: &str = "connection manager"; lazy_static::lazy_static! { // static ref SESSION: Arc>> = Default::default(); @@ -1678,6 +1679,8 @@ pub mod connection_manager { keyboard: bool, clipboard: bool, audio: bool, + file: bool, + restart: bool, #[serde(skip)] tx: UnboundedSender, } @@ -1885,8 +1888,8 @@ pub mod connection_manager { keyboard: bool, clipboard: bool, audio: bool, - _file: bool, - _restart: bool, + file: bool, + restart: bool, tx: mpsc::UnboundedSender, ) { let mut client = Client { @@ -1898,6 +1901,8 @@ pub mod connection_manager { keyboard, clipboard, audio, + file, + restart, tx, }; if authorized { @@ -1935,7 +1940,7 @@ pub mod connection_manager { if let Some(s) = GLOBAL_EVENT_STREAM .read() .unwrap() - .get(super::APP_TYPE_MAIN) + .get(super::APP_TYPE_DESKTOP_CONNECTION_MANAGER) { s.add(serde_json::ser::to_string(&h).unwrap_or("".to_owned())); }; From b9d1eb0dd15a97fe8db9ef93d0af9f2f0f82941d Mon Sep 17 00:00:00 2001 From: csf Date: Fri, 19 Aug 2022 12:44:35 +0800 Subject: [PATCH 0253/2015] add file manager overlay dialog --- .../lib/desktop/pages/file_manager_page.dart | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 2868d2d3b..e07fadf28 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -108,6 +108,31 @@ class _FileManagerPageState extends State ), )); })); + return Overlay(initialEntries: [ + OverlayEntry(builder: (context) { + _ffi.dialogManager.setOverlayState(Overlay.of(context)); + return ChangeNotifierProvider.value( + value: _ffi.fileModel, + child: Consumer(builder: (_context, _model, _child) { + return WillPopScope( + onWillPop: () async { + if (model.selectMode) { + model.toggleSelectMode(); + } + return false; + }, + child: Scaffold( + body: Row( + children: [ + Flexible(flex: 3, child: body(isLocal: true)), + Flexible(flex: 3, child: body(isLocal: false)), + Flexible(flex: 2, child: statusList()) + ], + ), + )); + })); + }) + ]); } Widget menu({bool isLocal = false}) { From 72655b528a738ed18d37c262f23425db7cac4135 Mon Sep 17 00:00:00 2001 From: csf Date: Fri, 19 Aug 2022 19:17:45 +0800 Subject: [PATCH 0254/2015] opt cm FittedBox --- flutter/lib/desktop/pages/server_page.dart | 29 ++++++++++++++-------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index beaf9c1eb..b08fcae6e 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -109,6 +109,8 @@ class ConnectionManager extends StatelessWidget { Widget buildConnectionCard(MapEntry entry) { final client = entry.value; return Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, key: ValueKey(entry.key), children: [ _CmHeader(client: client), @@ -212,14 +214,14 @@ class _CmHeaderState extends State<_CmHeader> children: [ // icon Container( - width: 100, - height: 100, + width: 90, + height: 90, alignment: Alignment.center, decoration: BoxDecoration(color: str2color(client.name)), child: Text( "${client.name[0]}", style: TextStyle( - fontWeight: FontWeight.bold, color: Colors.white, fontSize: 75), + fontWeight: FontWeight.bold, color: Colors.white, fontSize: 65), ), ).marginOnly(left: 4.0, right: 8.0), Expanded( @@ -227,7 +229,8 @@ class _CmHeaderState extends State<_CmHeader> mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( + FittedBox( + child: Text( "${client.name}", style: TextStyle( color: MyTheme.cmIdColor, @@ -236,19 +239,22 @@ class _CmHeaderState extends State<_CmHeader> overflow: TextOverflow.ellipsis, ), maxLines: 1, - ), - Text("(${client.peerId})", - style: TextStyle(color: MyTheme.cmIdColor, fontSize: 14)), + )), + FittedBox( + child: Text("(${client.peerId})", + style: + TextStyle(color: MyTheme.cmIdColor, fontSize: 14))), SizedBox( height: 16.0, ), - Row( + FittedBox( + child: Row( children: [ Text("${translate("Connected")}").marginOnly(right: 8.0), Obx(() => Text( "${formatDurationToTime(Duration(seconds: _time.value))}")) ], - ) + )) ], ), ), @@ -315,7 +321,8 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> { SizedBox( height: 8.0, ), - Row( + FittedBox( + child: Row( children: [ buildPermissionIcon(client.keyboard, iconKeyboard, (enabled) { bind.cmSwitchPermission( @@ -353,7 +360,7 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> { }); }, null), ], - ), + )), ], ), ); From f88bbb059556c1c8637ecae8defda125b5a3d843 Mon Sep 17 00:00:00 2001 From: csf Date: Mon, 22 Aug 2022 20:12:05 +0800 Subject: [PATCH 0255/2015] update test cm_main.dart --- flutter/lib/cm_main.dart | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/flutter/lib/cm_main.dart b/flutter/lib/cm_main.dart index 99db02232..1f71b9e93 100644 --- a/flutter/lib/cm_main.dart +++ b/flutter/lib/cm_main.dart @@ -4,7 +4,9 @@ import 'package:flutter_hbb/main.dart'; import 'package:get/get.dart'; import 'package:window_manager/window_manager.dart'; +import 'common.dart'; import 'desktop/pages/server_page.dart'; +import 'models/server_model.dart'; /// -t lib/cm_main.dart to test cm void main(List args) async { @@ -13,5 +15,16 @@ void main(List args) async { await windowManager.setSize(Size(400, 600)); await windowManager.setAlignment(Alignment.topRight); await initEnv(kAppTypeConnectionManager); - runApp(GetMaterialApp(theme: getCurrentTheme(), home: DesktopServerPage())); + gFFI.serverModel.clients + .add(Client(0, false, false, "UserA", "123123123", true, false, false)); + gFFI.serverModel.clients + .add(Client(1, false, false, "UserB", "221123123", true, false, false)); + gFFI.serverModel.clients + .add(Client(2, false, false, "UserC", "331123123", true, false, false)); + gFFI.serverModel.clients + .add(Client(3, false, false, "UserD", "441123123", true, false, false)); + runApp(GetMaterialApp( + debugShowCheckedModeBanner: false, + theme: getCurrentTheme(), + home: DesktopServerPage())); } From b33d1f216f759c92ce6bc34131a86e642f4a41a0 Mon Sep 17 00:00:00 2001 From: csf Date: Mon, 22 Aug 2022 20:12:58 +0800 Subject: [PATCH 0256/2015] update chat_model for desktop cm --- flutter/lib/models/chat_model.dart | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/flutter/lib/models/chat_model.dart b/flutter/lib/models/chat_model.dart index 524701297..a42b10ee2 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:window_manager/window_manager.dart'; import '../../mobile/widgets/overlay.dart'; import '../common.dart'; @@ -41,11 +42,14 @@ class ChatModel with ChangeNotifier { ..[clientModeID] = MessageBody(me, []); var _currentID = clientModeID; + late bool _isShowChatPage = false; Map get messages => _messages; int get currentID => _currentID; + bool get isShowChatPage => _isShowChatPage; + WeakReference _ffi; /// Constructor @@ -149,12 +153,29 @@ class ChatModel with ChangeNotifier { } } + toggleCMChatPage(int id) async { + if (gFFI.chatModel.currentID != id) { + gFFI.chatModel.changeCurrentID(id); + } + if (_isShowChatPage) { + _isShowChatPage = !_isShowChatPage; + notifyListeners(); + await windowManager.setSize(Size(400, 600)); + } else { + await windowManager.setSize(Size(800, 600)); + await Future.delayed(Duration(milliseconds: 100)); + _isShowChatPage = !_isShowChatPage; + notifyListeners(); + } + } + changeCurrentID(int id) { if (_messages.containsKey(id)) { _currentID = id; notifyListeners(); } else { - final client = _ffi.target?.serverModel.clients[id]; + final client = _ffi.target?.serverModel.clients + .firstWhere((client) => client.id == id); if (client == null) { return debugPrint( "Failed to changeCurrentID,remote user doesn't exist"); @@ -171,10 +192,15 @@ class ChatModel with ChangeNotifier { receive(int id, String text) async { if (text.isEmpty) return; - // first message show overlay icon + // mobile: first message show overlay icon if (chatIconOverlayEntry == null) { showChatIconOverlay(); } + // desktop: show chat page + if (!_isShowChatPage) { + toggleCMChatPage(id); + } + late final chatUser; if (id == clientModeID) { chatUser = ChatUser( From 14b8140e4556cb10b58d6c20324b67183fa06521 Mon Sep 17 00:00:00 2001 From: csf Date: Mon, 22 Aug 2022 20:18:31 +0800 Subject: [PATCH 0257/2015] 1. update DesktopTabBar for cm. 2. refactor server_model clients map -> list. 3. update tab changing events. --- flutter/lib/common.dart | 2 +- .../desktop/pages/connection_tab_page.dart | 2 + .../lib/desktop/pages/desktop_tab_page.dart | 2 + .../desktop/pages/file_manager_tab_page.dart | 2 + flutter/lib/desktop/pages/server_page.dart | 99 +++-- .../lib/desktop/widgets/tabbar_widget.dart | 338 +++++++++++------- flutter/lib/mobile/pages/server_page.dart | 30 +- flutter/lib/models/server_model.dart | 52 ++- 8 files changed, 325 insertions(+), 202 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index e963993f7..8c4216020 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -195,7 +195,7 @@ closeConnection({String? id}) { if (isAndroid || isIOS) { Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/")); } else { - closeTab(id); + DesktopTabBar.close(id); } } diff --git a/flutter/lib/desktop/pages/connection_tab_page.dart b/flutter/lib/desktop/pages/connection_tab_page.dart index be5ec82af..ece7df5ca 100644 --- a/flutter/lib/desktop/pages/connection_tab_page.dart +++ b/flutter/lib/desktop/pages/connection_tab_page.dart @@ -33,6 +33,7 @@ class _ConnectionTabPageState extends State { _ConnectionTabPageState(Map params) { if (params['id'] != null) { tabs.add(TabInfo( + key: params['id'], label: params['id'], selectedIcon: selectedIcon, unselectedIcon: unselectedIcon)); @@ -53,6 +54,7 @@ class _ConnectionTabPageState extends State { DesktopTabBar.onAdd( tabs, TabInfo( + key: id, label: id, selectedIcon: selectedIcon, unselectedIcon: unselectedIcon)); diff --git a/flutter/lib/desktop/pages/desktop_tab_page.dart b/flutter/lib/desktop/pages/desktop_tab_page.dart index 5c108f39f..141b7ca0e 100644 --- a/flutter/lib/desktop/pages/desktop_tab_page.dart +++ b/flutter/lib/desktop/pages/desktop_tab_page.dart @@ -22,6 +22,7 @@ class _DesktopTabPageState extends State { super.initState(); tabs = RxList.from([ TabInfo( + key: kTabLabelHomePage, label: kTabLabelHomePage, selectedIcon: Icons.home_sharp, unselectedIcon: Icons.home_outlined, @@ -70,6 +71,7 @@ class _DesktopTabPageState extends State { DesktopTabBar.onAdd( tabs, TabInfo( + key: kTabLabelSettingPage, label: kTabLabelSettingPage, selectedIcon: Icons.build_sharp, unselectedIcon: Icons.build_outlined)); diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index 12b5b20ff..7e94724bb 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -29,6 +29,7 @@ class _FileManagerTabPageState extends State { _FileManagerTabPageState(Map params) { if (params['id'] != null) { tabs.add(TabInfo( + key: params['id'], label: params['id'], selectedIcon: selectedIcon, unselectedIcon: unselectedIcon)); @@ -49,6 +50,7 @@ class _FileManagerTabPageState extends State { DesktopTabBar.onAdd( tabs, TabInfo( + key: id, label: id, selectedIcon: selectedIcon, unselectedIcon: unselectedIcon)); diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index b08fcae6e..e8f9ec26b 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -1,6 +1,9 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; +import 'package:flutter_hbb/mobile/pages/chat_page.dart'; +import 'package:flutter_hbb/models/chat_model.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:window_manager/window_manager.dart'; @@ -24,8 +27,11 @@ class _DesktopServerPageState extends State Widget build(BuildContext context) { super.build(context); - return ChangeNotifierProvider.value( - value: gFFI.serverModel, + return MultiProvider( + providers: [ + ChangeNotifierProvider.value(value: gFFI.serverModel), + ChangeNotifierProvider.value(value: gFFI.chatModel), + ], child: Consumer( builder: (context, serverModel, child) => Material( child: Center( @@ -44,14 +50,28 @@ class _DesktopServerPageState extends State bool get wantKeepAlive => true; } -class ConnectionManager extends StatelessWidget { +class ConnectionManager extends StatefulWidget { + @override + State createState() => ConnectionManagerState(); +} + +class ConnectionManagerState extends State { + @override + void initState() { + gFFI.serverModel.updateClientState(); + // test + // gFFI.serverModel.clients.forEach((client) { + // DesktopTabBar.onAdd( + // gFFI.serverModel.tabs, + // TabInfo( + // key: client.id.toString(), label: client.name, closable: false)); + // }); + super.initState(); + } + @override Widget build(BuildContext context) { final serverModel = Provider.of(context); - // test case: - // serverModel.clients.clear(); - // serverModel.clients[0] = Client( - // false, false, "Readmi-M21sdfsdf", "123123123", true, false, false); return serverModel.clients.isEmpty ? Column( children: [ @@ -63,27 +83,37 @@ class ConnectionManager extends StatelessWidget { ), ], ) - : DefaultTabController( - length: serverModel.clients.length, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - height: kTextTabBarHeight, - child: buildTitleBar(TabBar( - isScrollable: true, - tabs: serverModel.clients.entries - .map((entry) => buildTab(entry)) - .toList(growable: false))), - ), - Expanded( - child: TabBarView( - children: serverModel.clients.entries - .map((entry) => buildConnectionCard(entry)) - .toList(growable: false)), - ) - ], - ), + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: kTextTabBarHeight, + child: Obx(() => DesktopTabBar( + dark: isDarkTheme(), + mainTab: true, + tabs: serverModel.tabs, + showTitle: false, + showMaximize: false, + showMinimize: false, + onSelected: (index) => gFFI.chatModel + .changeCurrentID(serverModel.clients[index].id), + )), + ), + Expanded( + child: Row(children: [ + Expanded( + child: PageView( + controller: DesktopTabBar.controller.value, + children: serverModel.clients + .map((client) => buildConnectionCard(client)) + .toList(growable: false))), + Consumer( + builder: (_, model, child) => model.isShowChatPage + ? Expanded(child: Scaffold(body: ChatPage())) + : Offstage()) + ]), + ) + ], ); } @@ -106,12 +136,11 @@ class ConnectionManager extends StatelessWidget { ); } - Widget buildConnectionCard(MapEntry entry) { - final client = entry.value; + Widget buildConnectionCard(Client client) { return Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, - key: ValueKey(entry.key), + key: ValueKey(client.id), children: [ _CmHeader(client: client), client.isFileTransfer ? Offstage() : _PrivilegeBoard(client: client), @@ -124,14 +153,14 @@ class ConnectionManager extends StatelessWidget { ).paddingSymmetric(vertical: 8.0, horizontal: 8.0); } - Widget buildTab(MapEntry entry) { + Widget buildTab(Client client) { return Tab( child: Row( children: [ SizedBox( width: 80, child: Text( - "${entry.value.name}", + "${client.name}", maxLines: 1, overflow: TextOverflow.ellipsis, textAlign: TextAlign.center, @@ -261,7 +290,7 @@ class _CmHeaderState extends State<_CmHeader> Offstage( offstage: client.isFileTransfer, child: IconButton( - onPressed: handleSendMsg, + onPressed: () => gFFI.chatModel.toggleCMChatPage(client.id), icon: Icon(Icons.message_outlined), ), ) @@ -269,8 +298,6 @@ class _CmHeaderState extends State<_CmHeader> ); } - void handleSendMsg() {} - @override bool get wantKeepAlive => true; } diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index bf39f4dc6..74019c815 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -14,36 +14,19 @@ const double _kTabBarHeight = kDesktopRemoteTabBarHeight; const double _kIconSize = 18; const double _kDividerIndent = 10; const double _kActionIconSize = 12; -final _tabBarKey = GlobalKey(); - -void closeTab(String? id) { - final tabBar = _tabBarKey.currentWidget as _ListView?; - if (tabBar == null) return; - final tabs = tabBar.tabs; - if (id == null) { - if (tabBar.selected.value < tabs.length) { - tabs[tabBar.selected.value].onClose(); - } - } else { - for (final tab in tabs) { - if (tab.label == id) { - tab.onClose(); - break; - } - } - } -} class TabInfo { + late final String key; late final String label; - late final IconData selectedIcon; - late final IconData unselectedIcon; + late final IconData? selectedIcon; + late final IconData? unselectedIcon; late final bool closable; TabInfo( - {required this.label, - required this.selectedIcon, - required this.unselectedIcon, + {required this.key, + required this.label, + this.selectedIcon, + this.unselectedIcon, this.closable = true}); } @@ -53,20 +36,33 @@ class DesktopTabBar extends StatelessWidget { late final bool dark; late final _Theme _theme; late final bool mainTab; - late final Function()? onAddSetting; + late final bool showLogo; + late final bool showTitle; + late final bool showMinimize; + late final bool showMaximize; + late final bool showClose; + late final void Function()? onAddSetting; + late final void Function(int)? onSelected; final ScrollPosController scrollController = ScrollPosController(itemCount: 0); static final Rx controller = PageController().obs; static final Rx selected = 0.obs; + static final _tabBarListViewKey = GlobalKey(); - DesktopTabBar({ - Key? key, - required this.tabs, - this.onTabClose, - required this.dark, - required this.mainTab, - this.onAddSetting, - }) : _theme = dark ? _Theme.dark() : _Theme.light(), + DesktopTabBar( + {Key? key, + required this.tabs, + this.onTabClose, + required this.dark, + required this.mainTab, + this.onAddSetting, + this.onSelected, + this.showLogo = true, + this.showTitle = true, + this.showMinimize = true, + this.showMaximize = true, + this.showClose = true}) + : _theme = dark ? _Theme.dark() : _Theme.light(), super(key: key) { scrollController.itemCount = tabs.length; WidgetsBinding.instance.addPostFrameCallback((_) { @@ -88,22 +84,23 @@ class DesktopTabBar extends StatelessWidget { Expanded( child: Row( children: [ - Offstage( - offstage: !mainTab, - child: Row(children: [ - Image.asset( - 'assets/logo.ico', - width: 20, - height: 20, - ), - Text( - "RustDesk", - style: TextStyle(fontSize: 13), - ).marginOnly(left: 2), - ]).marginOnly( - left: 5, - right: 10, - ), + Row(children: [ + Offstage( + offstage: !showLogo, + child: Image.asset( + 'assets/logo.ico', + width: 20, + height: 20, + )), + Offstage( + offstage: !showTitle, + child: Text( + "RustDesk", + style: TextStyle(fontSize: 13), + ).marginOnly(left: 2)) + ]).marginOnly( + left: 5, + right: 10, ), Expanded( child: GestureDetector( @@ -116,13 +113,14 @@ class DesktopTabBar extends StatelessWidget { } }, child: _ListView( - key: _tabBarKey, + key: _tabBarListViewKey, controller: controller, scrollController: scrollController, tabInfos: tabs, selected: selected, onTabClose: onTabClose, - theme: _theme)), + theme: _theme, + onSelected: onSelected)), ), Offstage( offstage: mainTab, @@ -146,6 +144,9 @@ class DesktopTabBar extends StatelessWidget { WindowActionPanel( mainTab: mainTab, theme: _theme, + showMinimize: showMinimize, + showMaximize: showMaximize, + showClose: showClose, ) ], ), @@ -160,7 +161,7 @@ class DesktopTabBar extends StatelessWidget { } static onAdd(RxList tabs, TabInfo tab) { - int index = tabs.indexWhere((e) => e.label == tab.label); + int index = tabs.indexWhere((e) => e.key == tab.key); if (index >= 0) { selected.value = index; } else { @@ -168,86 +169,148 @@ class DesktopTabBar extends StatelessWidget { selected.value = tabs.length - 1; assert(selected.value >= 0); } + try { + controller.value.jumpToPage(selected.value); + } catch (e) { + // call before binding controller will throw + debugPrint("Failed to jumpToPage: $e"); + } + } + + static remove(RxList tabs, int index) { + if (index < 0) return; + if (index == tabs.length - 1) { + selected.value = max(0, selected.value - 1); + } else if (index < tabs.length - 1 && index < selected.value) { + selected.value = max(0, selected.value - 1); + } + tabs.removeAt(index); controller.value.jumpToPage(selected.value); } + + static void jumpTo(RxList tabs, int index) { + if (index < 0 || index >= tabs.length) return; + selected.value = index; + controller.value.jumpToPage(selected.value); + } + + static void close(String? key) { + final tabBar = _tabBarListViewKey.currentWidget as _ListView?; + if (tabBar == null) return; + final tabs = tabBar.tabs; + if (key == null) { + if (tabBar.selected.value < tabs.length) { + tabs[tabBar.selected.value].onClose(); + } + } else { + for (final tab in tabs) { + if (tab.key == key) { + tab.onClose(); + break; + } + } + } + } } class WindowActionPanel extends StatelessWidget { final bool mainTab; final _Theme theme; + final bool showMinimize; + final bool showMaximize; + final bool showClose; + const WindowActionPanel( - {Key? key, required this.mainTab, required this.theme}) + {Key? key, + required this.mainTab, + required this.theme, + this.showMinimize = true, + this.showMaximize = true, + this.showClose = true}) : super(key: key); @override Widget build(BuildContext context) { return Row( children: [ - _ActionIcon( - message: 'Minimize', - icon: IconFont.min, - theme: theme, - onTap: () { - if (mainTab) { - windowManager.minimize(); - } else { - WindowController.fromWindowId(windowId!).minimize(); - } - }, - is_close: false, - ), - FutureBuilder(builder: (context, snapshot) { - RxBool is_maximized = false.obs; - if (mainTab) { - windowManager.isMaximized().then((maximized) { - is_maximized.value = maximized; - }); - } else { - final wc = WindowController.fromWindowId(windowId!); - wc.isMaximized().then((maximized) { - is_maximized.value = maximized; - }); - } - return Obx( - () => _ActionIcon( - message: is_maximized.value ? "Restore" : "Maximize", - icon: is_maximized.value ? IconFont.restore : IconFont.max, + Offstage( + offstage: !showMinimize, + child: _ActionIcon( + message: 'Minimize', + icon: IconFont.min, theme: theme, onTap: () { if (mainTab) { - if (is_maximized.value) { - windowManager.unmaximize(); - } else { - windowManager.maximize(); - } + windowManager.minimize(); } else { - final wc = WindowController.fromWindowId(windowId!); - if (is_maximized.value) { - wc.unmaximize(); - } else { - wc.maximize(); - } + WindowController.fromWindowId(windowId!).minimize(); } - is_maximized.value = !is_maximized.value; }, is_close: false, - ), - ); - }), - _ActionIcon( - message: 'Close', - icon: IconFont.close, - theme: theme, - onTap: () { - if (mainTab) { - windowManager.close(); - } else { - WindowController.fromWindowId(windowId!).close(); - } - }, - is_close: true, - ), + )), + Offstage( + offstage: !showMaximize, + child: FutureBuilder(builder: (context, snapshot) { + RxBool is_maximized = false.obs; + if (mainTab) { + windowManager.isMaximized().then((maximized) { + is_maximized.value = maximized; + }); + } else { + final wc = WindowController.fromWindowId(windowId!); + wc.isMaximized().then((maximized) { + is_maximized.value = maximized; + }); + } + return Obx( + () => _ActionIcon( + message: is_maximized.value ? "Restore" : "Maximize", + icon: is_maximized.value ? IconFont.restore : IconFont.max, + theme: theme, + onTap: () { + if (mainTab) { + if (is_maximized.value) { + windowManager.unmaximize(); + } else { + WindowController.fromWindowId(windowId!).minimize(); + } + } else { + final wc = WindowController.fromWindowId(windowId!); + if (is_maximized.value) { + wc.unmaximize(); + } else { + final wc = WindowController.fromWindowId(windowId!); + wc.isMaximized().then((maximized) { + if (maximized) { + wc.unmaximize(); + } else { + wc.maximize(); + } + }); + } + } + is_maximized.value = !is_maximized.value; + }, + is_close: false, + ), + ); + })), + Offstage( + offstage: !showClose, + child: _ActionIcon( + message: 'Close', + icon: IconFont.close, + theme: theme, + onTap: () { + if (mainTab) { + windowManager.close(); + } else { + WindowController.fromWindowId(windowId!).close(); + } + }, + is_close: true, + )), ], ); } @@ -259,19 +322,21 @@ class _ListView extends StatelessWidget { final ScrollPosController scrollController; final RxList tabInfos; final Rx selected; - final Function(String label)? onTabClose; + final Function(String key)? onTabClose; final _Theme _theme; late List<_Tab> tabs; + late final void Function(int)? onSelected; - _ListView({ - Key? key, - required this.controller, - required this.scrollController, - required this.tabInfos, - required this.selected, - required this.onTabClose, - required _Theme theme, - }) : _theme = theme, + _ListView( + {Key? key, + required this.controller, + required this.scrollController, + required this.tabInfos, + required this.selected, + required this.onTabClose, + required _Theme theme, + this.onSelected}) + : _theme = theme, super(key: key); @override @@ -279,17 +344,16 @@ class _ListView extends StatelessWidget { return Obx(() { tabs = tabInfos.asMap().entries.map((e) { int index = e.key; - String label = e.value.label; return _Tab( index: index, - label: label, + label: e.value.label, selectedIcon: e.value.selectedIcon, unselectedIcon: e.value.unselectedIcon, closable: e.value.closable, selected: selected.value, onClose: () { - tabInfos.removeWhere((tab) => tab.label == label); - onTabClose?.call(label); + tabInfos.removeWhere((tab) => tab.key == e.value.key); + onTabClose?.call(e.value.key); if (index <= selected.value) { selected.value = max(0, selected.value - 1); } @@ -305,6 +369,7 @@ class _ListView extends StatelessWidget { selected.value = index; scrollController.scrollToItem(index, center: true, animate: true); controller.value.jumpToPage(index); + onSelected?.call(selected.value); }, theme: _theme, ); @@ -322,8 +387,8 @@ class _ListView extends StatelessWidget { class _Tab extends StatelessWidget { late final int index; late final String label; - late final IconData selectedIcon; - late final IconData unselectedIcon; + late final IconData? selectedIcon; + late final IconData? unselectedIcon; late final bool closable; late final int selected; late final Function() onClose; @@ -335,8 +400,8 @@ class _Tab extends StatelessWidget { {Key? key, required this.index, required this.label, - required this.selectedIcon, - required this.unselectedIcon, + this.selectedIcon, + this.unselectedIcon, required this.closable, required this.selected, required this.onClose, @@ -346,6 +411,7 @@ class _Tab extends StatelessWidget { @override Widget build(BuildContext context) { + bool show_icon = selectedIcon != null && unselectedIcon != null; bool is_selected = index == selected; bool show_divider = index != selected - 1 && index != selected; return Ink( @@ -362,13 +428,15 @@ class _Tab extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( - is_selected ? selectedIcon : unselectedIcon, - size: _kIconSize, - color: is_selected - ? theme.selectedtabIconColor - : theme.unSelectedtabIconColor, - ).paddingOnly(right: 5), + Offstage( + offstage: !show_icon, + child: Icon( + is_selected ? selectedIcon : unselectedIcon, + size: _kIconSize, + color: is_selected + ? theme.selectedtabIconColor + : theme.unSelectedtabIconColor, + ).paddingOnly(right: 5)), Text( translate(label), textAlign: TextAlign.center, diff --git a/flutter/lib/mobile/pages/server_page.dart b/flutter/lib/mobile/pages/server_page.dart index 74e436ebb..abbc5aadc 100644 --- a/flutter/lib/mobile/pages/server_page.dart +++ b/flutter/lib/mobile/pages/server_page.dart @@ -359,12 +359,12 @@ class ConnectionManager extends StatelessWidget { Widget build(BuildContext context) { final serverModel = Provider.of(context); return Column( - children: serverModel.clients.entries - .map((entry) => PaddingCard( - title: translate(entry.value.isFileTransfer + children: serverModel.clients + .map((client) => PaddingCard( + title: translate(client.isFileTransfer ? "File Connection" : "Screen Connection"), - titleIcon: entry.value.isFileTransfer + titleIcon: client.isFileTransfer ? Icons.folder_outlined : Icons.mobile_screen_share, child: Column( @@ -373,16 +373,14 @@ class ConnectionManager extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Expanded(child: clientInfo(entry.value)), + Expanded(child: clientInfo(client)), Expanded( flex: -1, - child: entry.value.isFileTransfer || - !entry.value.authorized + child: client.isFileTransfer || !client.authorized ? SizedBox.shrink() : IconButton( onPressed: () { - gFFI.chatModel - .changeCurrentID(entry.value.id); + gFFI.chatModel.changeCurrentID(client.id); final bar = navigationBarKey.currentWidget; if (bar != null) { @@ -396,37 +394,35 @@ class ConnectionManager extends StatelessWidget { ))) ], ), - entry.value.authorized + client.authorized ? SizedBox.shrink() : Text( translate("android_new_connection_tip"), style: TextStyle(color: Colors.black54), ), - entry.value.authorized + client.authorized ? ElevatedButton.icon( style: ButtonStyle( backgroundColor: MaterialStateProperty.all(Colors.red)), icon: Icon(Icons.close), onPressed: () { - bind.cmCloseConnection(connId: entry.key); + bind.cmCloseConnection(connId: client.id); gFFI.invokeMethod( - "cancel_notification", entry.key); + "cancel_notification", client.id); }, label: Text(translate("Close"))) : Row(children: [ TextButton( child: Text(translate("Dismiss")), onPressed: () { - serverModel.sendLoginResponse( - entry.value, false); + serverModel.sendLoginResponse(client, false); }), SizedBox(width: 20), ElevatedButton( child: Text(translate("Accept")), onPressed: () { - serverModel.sendLoginResponse( - entry.value, true); + serverModel.sendLoginResponse(client, true); }), ]), ], diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index 9f69dd04a..527cea689 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -4,9 +4,11 @@ import 'dart:io'; 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:wakelock/wakelock.dart'; import '../common.dart'; +import '../desktop/widgets/tabbar_widget.dart'; import '../mobile/pages/server_page.dart'; import 'model.dart'; @@ -30,7 +32,9 @@ class ServerModel with ChangeNotifier { late final TextEditingController _serverId; final _serverPasswd = TextEditingController(text: ""); - Map _clients = {}; + RxList tabs = RxList.empty(growable: true); + + List _clients = []; bool get isStart => _isStart; @@ -76,7 +80,7 @@ class ServerModel with ChangeNotifier { TextEditingController get serverPasswd => _serverPasswd; - Map get clients => _clients; + List get clients => _clients; final controller = ScrollController(); @@ -338,6 +342,7 @@ class ServerModel with ChangeNotifier { notifyListeners(); } + // force updateClientState([String? json]) async { var res = await bind.mainGetClientsState(); try { @@ -347,9 +352,16 @@ class ServerModel with ChangeNotifier { exit(0); } _clients.clear(); + tabs.clear(); for (var clientJson in clientsJson) { final client = Client.fromJson(clientJson); - _clients[client.id] = client; + _clients.add(client); + DesktopTabBar.onAdd( + tabs, + TabInfo( + key: client.id.toString(), + label: client.name, + closable: false)); } notifyListeners(); } catch (e) { @@ -360,10 +372,14 @@ class ServerModel with ChangeNotifier { void loginRequest(Map evt) { try { final client = Client.fromJson(jsonDecode(evt["client"])); - if (_clients.containsKey(client.id)) { + if (_clients.any((c) => c.id == client.id)) { return; } - _clients[client.id] = client; + _clients.add(client); + DesktopTabBar.onAdd( + tabs, + TabInfo( + key: client.id.toString(), label: client.name, closable: false)); scrollToBottom(); notifyListeners(); if (isAndroid) showLoginDialog(client); @@ -419,6 +435,7 @@ class ServerModel with ChangeNotifier { } scrollToBottom() { + if (isDesktop) return; Future.delayed(Duration(milliseconds: 200), () { controller.animateTo(controller.position.maxScrollExtent, duration: Duration(milliseconds: 200), @@ -433,12 +450,14 @@ class ServerModel with ChangeNotifier { parent.target?.invokeMethod("start_capture"); } parent.target?.invokeMethod("cancel_notification", client.id); - _clients[client.id]?.authorized = true; + client.authorized = true; notifyListeners(); } else { bind.cmLoginRes(connId: client.id, res: res); parent.target?.invokeMethod("cancel_notification", client.id); - _clients.remove(client.id); + final index = _clients.indexOf(client); + DesktopTabBar.remove(tabs, index); + _clients.remove(client); } } @@ -446,7 +465,11 @@ class ServerModel with ChangeNotifier { try { final client = Client.fromJson(jsonDecode(evt['client'])); parent.target?.dialogManager.dismissByTag(getLoginDialogTag(client.id)); - _clients[client.id] = client; + _clients.add(client); + DesktopTabBar.onAdd( + tabs, + TabInfo( + key: client.id.toString(), label: client.name, closable: false)); scrollToBottom(); notifyListeners(); } catch (e) {} @@ -455,8 +478,10 @@ class ServerModel with ChangeNotifier { void onClientRemove(Map evt) { try { final id = int.parse(evt['id'] as String); - if (_clients.containsKey(id)) { - _clients.remove(id); + if (_clients.any((c) => c.id == id)) { + final index = _clients.indexWhere((client) => client.id == id); + _clients.removeAt(index); + DesktopTabBar.remove(tabs, index); parent.target?.dialogManager.dismissByTag(getLoginDialogTag(id)); parent.target?.invokeMethod("cancel_notification", id); } @@ -467,10 +492,11 @@ class ServerModel with ChangeNotifier { } closeAll() { - _clients.forEach((id, client) { - bind.cmCloseConnection(connId: id); + _clients.forEach((client) { + bind.cmCloseConnection(connId: client.id); }); _clients.clear(); + tabs.clear(); } } @@ -486,7 +512,7 @@ class Client { bool file = false; bool restart = false; - Client(this.authorized, this.isFileTransfer, this.name, this.peerId, + Client(this.id, this.authorized, this.isFileTransfer, this.name, this.peerId, this.keyboard, this.clipboard, this.audio); Client.fromJson(Map json) { From b5ebb5de3713caaae53893aefa5d7f6089ea4ded Mon Sep 17 00:00:00 2001 From: csf Date: Mon, 22 Aug 2022 06:50:51 -0700 Subject: [PATCH 0258/2015] flutter_desktop_cm fix Windows build & TODO clipboard_file --- src/flutter.rs | 56 ++++++++++++++++---------------- src/ipc.rs | 78 -------------------------------------------- src/ui/cm.rs | 87 +++++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 110 insertions(+), 111 deletions(-) diff --git a/src/flutter.rs b/src/flutter.rs index 928be607d..5cd43b8b2 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -1660,9 +1660,6 @@ pub mod connection_manager { #[cfg(any(target_os = "android"))] use scrap::android::call_main_service_set_by_name; - #[cfg(windows)] - use crate::ipc::start_clipboard_file; - use crate::ipc::Data; use crate::ipc::{self, new_listener, Connection}; @@ -1688,11 +1685,12 @@ pub mod connection_manager { static CLICK_TIME: AtomicI64 = AtomicI64::new(0); - enum ClipboardFileData { - #[cfg(windows)] - Clip((i32, ipc::ClipbaordFile)), - Enable((i32, bool)), - } + // // TODO clipboard_file + // enum ClipboardFileData { + // #[cfg(windows)] + // Clip((i32, ipc::ClipbaordFile)), + // Enable((i32, bool)), + // } #[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn start_listen_ipc_thread() { @@ -1702,11 +1700,12 @@ pub mod connection_manager { #[cfg(not(any(target_os = "android", target_os = "ios")))] #[tokio::main(flavor = "current_thread")] async fn start_ipc() { - let (tx_file, _rx_file) = mpsc::unbounded_channel::(); - #[cfg(windows)] - let cm_clip = cm.clone(); - #[cfg(windows)] - std::thread::spawn(move || start_clipboard_file(cm_clip, _rx_file)); + // TODO clipboard_file + // let (tx_file, _rx_file) = mpsc::unbounded_channel::(); + // #[cfg(windows)] + // let cm_clip = cm.clone(); + // #[cfg(windows)] + // std::thread::spawn(move || start_clipboard_file(cm_clip, _rx_file)); #[cfg(windows)] std::thread::spawn(move || { @@ -1730,7 +1729,7 @@ pub mod connection_manager { Ok(stream) => { log::debug!("Got new connection"); let mut stream = Connection::new(stream); - let tx_file = tx_file.clone(); + // let tx_file = tx_file.clone(); tokio::spawn(async move { // for tmp use, without real conn id let conn_id_tmp = -1; @@ -1750,11 +1749,11 @@ pub mod connection_manager { Data::Login{id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, file_transfer_enabled, restart} => { log::debug!("conn_id: {}", id); conn_id = id; - tx_file.send(ClipboardFileData::Enable((id, file_transfer_enabled))).ok(); + // tx_file.send(ClipboardFileData::Enable((id, file_transfer_enabled))).ok(); on_login(id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart, tx.clone()); } Data::Close => { - tx_file.send(ClipboardFileData::Enable((conn_id, false))).ok(); + // tx_file.send(ClipboardFileData::Enable((conn_id, false))).ok(); log::info!("cm ipc connection closed from connection request"); break; } @@ -1771,18 +1770,19 @@ pub mod connection_manager { Data::FS(fs) => { handle_fs(fs, &mut write_jobs, &tx).await; } - #[cfg(windows)] - Data::ClipbaordFile(_clip) => { - tx_file - .send(ClipboardFileData::Clip((id, _clip))) - .ok(); - } - #[cfg(windows)] - Data::ClipboardFileEnabled(enabled) => { - tx_file - .send(ClipboardFileData::Enable((id, enabled))) - .ok(); - } + // TODO ClipbaordFile + // #[cfg(windows)] + // Data::ClipbaordFile(_clip) => { + // tx_file + // .send(ClipboardFileData::Clip((id, _clip))) + // .ok(); + // } + // #[cfg(windows)] + // Data::ClipboardFileEnabled(enabled) => { + // tx_file + // .send(ClipboardFileData::Enable((id, enabled))) + // .ok(); + // } _ => {} } } diff --git a/src/ipc.rs b/src/ipc.rs index b98b0ad77..0bdc3f43b 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -1,7 +1,3 @@ -#[cfg(windows)] -use clipboard::{ - create_cliprdr_context, empty_clipboard, get_rx_clip_client, server_clip_file, set_conn_enabled, -}; use std::{collections::HashMap, sync::atomic::Ordering}; #[cfg(not(windows))] use std::{fs::File, io::prelude::*}; @@ -494,80 +490,6 @@ pub async fn start_pa() { } } -#[cfg(windows)] -#[tokio::main(flavor = "current_thread")] -pub async fn start_clipboard_file( - cm: ConnectionManager, - mut rx: mpsc::UnboundedReceiver, -) { - let mut cliprdr_context = None; - let mut rx_clip_client = get_rx_clip_client().lock().await; - - loop { - tokio::select! { - clip_file = rx_clip_client.recv() => match clip_file { - Some((conn_id, clip)) => { - cmd_inner_send( - &cm, - conn_id, - Data::ClipbaordFile(clip) - ); - } - None => { - // - } - }, - server_msg = rx.recv() => match server_msg { - Some(ClipboardFileData::Clip((conn_id, clip))) => { - if let Some(ctx) = cliprdr_context.as_mut() { - server_clip_file(ctx, conn_id, clip); - } - } - Some(ClipboardFileData::Enable((id, enabled))) => { - if enabled && cliprdr_context.is_none() { - cliprdr_context = Some(match create_cliprdr_context(true, false) { - Ok(context) => { - log::info!("clipboard context for file transfer created."); - context - } - Err(err) => { - log::error!( - "Create clipboard context for file transfer: {}", - err.to_string() - ); - return; - } - }); - } - set_conn_enabled(id, enabled); - if !enabled { - if let Some(ctx) = cliprdr_context.as_mut() { - empty_clipboard(ctx, id); - } - } - } - None => { - break - } - } - } - } -} - -#[cfg(windows)] -fn cmd_inner_send(cm: &ConnectionManager, id: i32, data: Data) { - let lock = cm.read().unwrap(); - if id != 0 { - if let Some(s) = lock.senders.get(&id) { - allow_err!(s.send(data)); - } - } else { - for s in lock.senders.values() { - allow_err!(s.send(data.clone())); - } - } -} - #[inline] #[cfg(not(windows))] fn get_pid_file(postfix: &str) -> String { diff --git a/src/ui/cm.rs b/src/ui/cm.rs index f1b4eaf72..222b9b5c9 100644 --- a/src/ui/cm.rs +++ b/src/ui/cm.rs @@ -1,7 +1,11 @@ -use crate::ipc::{self, new_listener, Connection, Data, start_pa}; -#[cfg(windows)] -use crate::ipc::start_clipboard_file; +#[cfg(target_os = "linux")] +use crate::ipc::start_pa; +use crate::ipc::{self, new_listener, Connection, Data}; use crate::VERSION; +#[cfg(windows)] +use clipboard::{ + create_cliprdr_context, empty_clipboard, get_rx_clip_client, server_clip_file, set_conn_enabled, +}; use hbb_common::fs::{ can_enable_overwrite_detection, get_string, is_write_need_confirmation, new_send_confirm, DigestCheckResult, @@ -158,7 +162,7 @@ impl ConnectionManager { id, file_num, mut files, - overwrite_detection + overwrite_detection, } => { // cm has no show_hidden context // dummy remote, show_hidden, is_remote @@ -435,7 +439,7 @@ impl sciter::EventHandler for ConnectionManager { } } -enum ClipboardFileData { +pub enum ClipboardFileData { #[cfg(windows)] Clip((i32, ipc::ClipbaordFile)), Enable((i32, bool)), @@ -537,3 +541,76 @@ async fn start_ipc(cm: ConnectionManager) { crate::platform::quit_gui(); } +#[cfg(windows)] +#[tokio::main(flavor = "current_thread")] +pub async fn start_clipboard_file( + cm: ConnectionManager, + mut rx: mpsc::UnboundedReceiver, +) { + let mut cliprdr_context = None; + let mut rx_clip_client = get_rx_clip_client().lock().await; + + loop { + tokio::select! { + clip_file = rx_clip_client.recv() => match clip_file { + Some((conn_id, clip)) => { + cmd_inner_send( + &cm, + conn_id, + Data::ClipbaordFile(clip) + ); + } + None => { + // + } + }, + server_msg = rx.recv() => match server_msg { + Some(ClipboardFileData::Clip((conn_id, clip))) => { + if let Some(ctx) = cliprdr_context.as_mut() { + server_clip_file(ctx, conn_id, clip); + } + } + Some(ClipboardFileData::Enable((id, enabled))) => { + if enabled && cliprdr_context.is_none() { + cliprdr_context = Some(match create_cliprdr_context(true, false) { + Ok(context) => { + log::info!("clipboard context for file transfer created."); + context + } + Err(err) => { + log::error!( + "Create clipboard context for file transfer: {}", + err.to_string() + ); + return; + } + }); + } + set_conn_enabled(id, enabled); + if !enabled { + if let Some(ctx) = cliprdr_context.as_mut() { + empty_clipboard(ctx, id); + } + } + } + None => { + break + } + } + } + } +} + +#[cfg(windows)] +fn cmd_inner_send(cm: &ConnectionManager, id: i32, data: Data) { + let lock = cm.read().unwrap(); + if id != 0 { + if let Some(s) = lock.senders.get(&id) { + allow_err!(s.send(data)); + } + } else { + for s in lock.senders.values() { + allow_err!(s.send(data.clone())); + } + } +} From 930bf72c91d3129ceeda2d18865459e6c34f197e Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 22 Aug 2022 17:58:48 +0800 Subject: [PATCH 0259/2015] optimize ui style Signed-off-by: 21pages --- flutter/lib/common.dart | 15 +- .../lib/desktop/pages/connection_page.dart | 153 ++++++++++++------ .../desktop/pages/connection_tab_page.dart | 61 +++---- .../lib/desktop/pages/desktop_home_page.dart | 67 +++----- .../desktop/pages/file_manager_tab_page.dart | 47 +++--- flutter/lib/desktop/pages/server_page.dart | 22 ++- .../lib/desktop/widgets/tabbar_widget.dart | 13 +- 7 files changed, 218 insertions(+), 160 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 8c4216020..c33e2b291 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -59,6 +59,7 @@ class ColorThemeExtension extends ThemeExtension { required this.text, required this.lightText, required this.lighterText, + required this.placeholder, required this.border, }); @@ -67,6 +68,7 @@ class ColorThemeExtension extends ThemeExtension { final Color? text; final Color? lightText; final Color? lighterText; + final Color? placeholder; final Color? border; static const light = ColorThemeExtension( @@ -75,6 +77,7 @@ class ColorThemeExtension extends ThemeExtension { text: Color(0xFF222222), lightText: Color(0xFF666666), lighterText: Color(0xFF888888), + placeholder: Color(0xFFAAAAAA), border: Color(0xFFCCCCCC), ); @@ -84,6 +87,7 @@ class ColorThemeExtension extends ThemeExtension { text: Color(0xFFFFFFFF), lightText: Color(0xFF999999), lighterText: Color(0xFF777777), + placeholder: Color(0xFF555555), border: Color(0xFF555555), ); @@ -94,6 +98,7 @@ class ColorThemeExtension extends ThemeExtension { Color? text, Color? lightText, Color? lighterText, + Color? placeholder, Color? border}) { return ColorThemeExtension( bg: bg ?? this.bg, @@ -101,6 +106,7 @@ class ColorThemeExtension extends ThemeExtension { text: text ?? this.text, lightText: lightText ?? this.lightText, lighterText: lighterText ?? this.lighterText, + placeholder: placeholder ?? this.placeholder, border: border ?? this.border, ); } @@ -117,6 +123,7 @@ class ColorThemeExtension extends ThemeExtension { text: Color.lerp(text, other.text, t), lightText: Color.lerp(lightText, other.lightText, t), lighterText: Color.lerp(lighterText, other.lighterText, t), + placeholder: Color.lerp(placeholder, other.placeholder, t), border: Color.lerp(border, other.border, t), ); } @@ -136,6 +143,8 @@ class MyTheme { static const Color darkGray = Color(0xFFB9BABC); static const Color cmIdColor = Color(0xFF21790B); static const Color dark = Colors.black87; + static const Color button = Color(0xFF2C8CFF); + static const Color hoverBorder = Color(0xFF999999); static ThemeData lightTheme = ThemeData( brightness: Brightness.light, @@ -144,7 +153,8 @@ class MyTheme { tabBarTheme: TabBarTheme( labelColor: Colors.black87, ), - // backgroundColor: Color(0xFFFFFFFF), + splashColor: Colors.transparent, + highlightColor: Colors.transparent, ).copyWith( extensions: >[ ColorThemeExtension.light, @@ -157,7 +167,8 @@ class MyTheme { tabBarTheme: TabBarTheme( labelColor: Colors.white70, ), - // backgroundColor: Color(0xFF252525) + splashColor: Colors.transparent, + highlightColor: Colors.transparent, ).copyWith( extensions: >[ ColorThemeExtension.dark, diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index c07df87e9..c021217e0 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -65,12 +65,14 @@ class _ConnectionPageState extends State { getUpdateUI(), Row( children: [ - getSearchBarUI(), + getSearchBarUI(context), ], - ).marginOnly(top: 16.0, left: 16.0), + ).marginOnly(top: 22, left: 22), SizedBox(height: 12), Divider( thickness: 1, + indent: 22, + endIndent: 22, ), Expanded( // TODO: move all tab info into _PeerTabbedPage @@ -123,7 +125,7 @@ class _ConnectionPageState extends State { } }), ], - )), + ).marginSymmetric(horizontal: 6)), Divider(), SizedBox(height: 50, child: Obx(() => buildStatus())) .paddingSymmetric(horizontal: 12.0) @@ -178,12 +180,16 @@ class _ConnectionPageState extends State { /// UI for the search bar. /// Search for a peer and connect to it if the id exists. - Widget getSearchBarUI() { + Widget getSearchBarUI(BuildContext context) { + RxBool ftHover = false.obs; + RxBool ftPressed = false.obs; + RxBool connHover = false.obs; + RxBool connPressed = false.obs; var w = Container( - width: 500, - padding: EdgeInsets.symmetric(horizontal: 16, vertical: 24), + width: 320 + 20 * 2, + padding: EdgeInsets.only(left: 20, right: 20, bottom: 22, top: 30), decoration: BoxDecoration( - color: isDarkTheme() ? null : MyTheme.white, + color: MyTheme.color(context).bg, borderRadius: const BorderRadius.all(Radius.circular(13)), ), child: Ink( @@ -197,17 +203,12 @@ class _ConnectionPageState extends State { autocorrect: false, enableSuggestions: false, keyboardType: TextInputType.visiblePassword, - // keyboardType: TextInputType.number, style: TextStyle( fontFamily: 'WorkSans', - fontWeight: FontWeight.bold, - fontSize: 30, - // color: MyTheme.idColor, + fontSize: 22, ), decoration: InputDecoration( labelText: translate('Control Remote Desktop'), - // hintText: 'Enter your remote ID', - // border: InputBorder., border: OutlineInputBorder(borderRadius: BorderRadius.zero), helperStyle: TextStyle( @@ -215,7 +216,7 @@ class _ConnectionPageState extends State { fontSize: 16, ), labelStyle: TextStyle( - fontWeight: FontWeight.w600, + fontWeight: FontWeight.w500, fontSize: 26, letterSpacing: 0.2, ), @@ -230,42 +231,84 @@ class _ConnectionPageState extends State { ], ), Padding( - padding: const EdgeInsets.only(top: 16.0), + padding: const EdgeInsets.only(top: 13.0), child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ - OutlinedButton( - onPressed: () { - onConnect(isFileTransfer: true); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 8.0, horizontal: 8.0), - child: Text( - translate( - "Transfer File", + Obx(() => InkWell( + onTapDown: (_) => ftPressed.value = true, + onTapUp: (_) => ftPressed.value = false, + onTapCancel: () => ftPressed.value = false, + onHover: (value) => ftHover.value = value, + onTap: () { + onConnect(isFileTransfer: true); + }, + child: Container( + height: 24, + width: 72, + alignment: Alignment.center, + decoration: BoxDecoration( + color: ftPressed.value + ? MyTheme.accent + : Colors.transparent, + border: Border.all( + color: ftPressed.value + ? MyTheme.accent + : ftHover.value + ? MyTheme.hoverBorder + : MyTheme.border, + ), + borderRadius: BorderRadius.circular(5), + ), + child: Text( + translate( + "Transfer File", + ), + style: TextStyle( + fontSize: 12, + color: ftPressed.value + ? MyTheme.color(context).bg + : MyTheme.color(context).text), + ), ), - ), - ), - ), + )), SizedBox( - width: 30, + width: 17, ), - OutlinedButton( - onPressed: onConnect, - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 8.0, horizontal: 16.0), - child: Text( - translate( - "Connection", + Obx( + () => InkWell( + onTapDown: (_) => connPressed.value = true, + onTapUp: (_) => connPressed.value = false, + onTapCancel: () => connPressed.value = false, + onHover: (value) => connHover.value = value, + onTap: onConnect, + child: Container( + height: 24, + width: 65, + decoration: BoxDecoration( + color: connPressed.value + ? MyTheme.accent + : MyTheme.button, + border: Border.all( + color: connPressed.value + ? MyTheme.accent + : connHover.value + ? MyTheme.hoverBorder + : MyTheme.button, + ), + borderRadius: BorderRadius.circular(5), + ), + child: Center( + child: Text( + translate( + "Connection", + ), + style: TextStyle( + fontSize: 12, color: MyTheme.color(context).bg), + ), ), - style: TextStyle(color: MyTheme.white), ), ), - style: OutlinedButton.styleFrom( - backgroundColor: Colors.blueAccent, - ), ), ], ), @@ -920,6 +963,7 @@ class _PeerTabbedPage extends StatefulWidget { class _PeerTabbedPageState extends State<_PeerTabbedPage> with SingleTickerProviderStateMixin { late TabController _tabController; + RxInt _tabIndex = 0.obs; @override void initState() { @@ -932,6 +976,7 @@ class _PeerTabbedPageState extends State<_PeerTabbedPage> // hard code for now void _handleTabSelection() { if (_tabController.indexIsChanging) { + _tabIndex.value = _tabController.index; switch (_tabController.index) { case 0: bind.mainLoadRecentPeers(); @@ -969,19 +1014,37 @@ class _PeerTabbedPageState extends State<_PeerTabbedPage> return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _createTabBar(), + _createTabBar(context), _createTabBarView(), ], ); } - Widget _createTabBar() { + Widget _createTabBar(BuildContext context) { return TabBar( isScrollable: true, indicatorSize: TabBarIndicatorSize.label, + indicatorColor: Colors.transparent, + indicatorWeight: 0.1, controller: _tabController, - tabs: super.widget.tabs.map((t) { - return Tab(child: Text(t)); + labelPadding: EdgeInsets.zero, + padding: EdgeInsets.only(left: 16), + tabs: super.widget.tabs.asMap().entries.map((t) { + return Obx(() => Container( + padding: EdgeInsets.symmetric(horizontal: 8, vertical: 6), + decoration: BoxDecoration( + color: + _tabIndex.value == t.key ? MyTheme.color(context).bg : null, + borderRadius: BorderRadius.circular(2), + ), + child: Text( + t.value, + style: TextStyle( + height: 1, + color: _tabIndex.value == t.key + ? MyTheme.color(context).text + : MyTheme.color(context).lightText), + ))); }).toList()); } diff --git a/flutter/lib/desktop/pages/connection_tab_page.dart b/flutter/lib/desktop/pages/connection_tab_page.dart index ece7df5ca..407feddea 100644 --- a/flutter/lib/desktop/pages/connection_tab_page.dart +++ b/flutter/lib/desktop/pages/connection_tab_page.dart @@ -76,34 +76,39 @@ class _ConnectionTabPageState extends State { Widget build(BuildContext context) { return SubWindowDragToResizeArea( windowId: windowId(), - child: Scaffold( - body: Column( - children: [ - Obx(() => Visibility( - visible: _fullscreenID.value.isEmpty, - child: DesktopTabBar( - tabs: tabs, - onTabClose: onRemoveId, - dark: isDarkTheme(), - mainTab: false, - ))), - Expanded(child: Obx(() { - WindowController.fromWindowId(windowId()) - .setFullscreen(_fullscreenID.value.isNotEmpty); - return PageView( - controller: DesktopTabBar.controller.value, - children: tabs - .map((tab) => RemotePage( - key: ValueKey(tab.label), - id: tab.label, - tabBarHeight: _fullscreenID.value.isNotEmpty - ? 0 - : kDesktopRemoteTabBarHeight, - fullscreenID: _fullscreenID, - )) //RemotePage(key: ValueKey(e), id: e)) - .toList()); - })), - ], + child: Container( + decoration: BoxDecoration( + border: Border.all(color: MyTheme.color(context).border!)), + child: Scaffold( + backgroundColor: MyTheme.color(context).bg, + body: Column( + children: [ + Obx(() => Visibility( + visible: _fullscreenID.value.isEmpty, + child: DesktopTabBar( + tabs: tabs, + onTabClose: onRemoveId, + dark: isDarkTheme(), + mainTab: false, + ))), + Expanded(child: Obx(() { + WindowController.fromWindowId(windowId()) + .setFullscreen(_fullscreenID.value.isNotEmpty); + return PageView( + controller: DesktopTabBar.controller.value, + children: tabs + .map((tab) => RemotePage( + key: ValueKey(tab.label), + id: tab.label, + tabBarHeight: _fullscreenID.value.isNotEmpty + ? 0 + : kDesktopRemoteTabBarHeight, + fullscreenID: _fullscreenID, + )) //RemotePage(key: ValueKey(e), id: e)) + .toList()); + })), + ], + ), ), ), ); diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 30fec849b..f85cf5b86 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -90,7 +90,7 @@ class _DesktopHomePageState extends State buildIDBoard(BuildContext context) { final model = gFFI.serverModel; return Container( - margin: EdgeInsets.symmetric(horizontal: 16), + margin: EdgeInsets.only(left: 20, right: 16), height: 52, child: Row( crossAxisAlignment: CrossAxisAlignment.baseline, @@ -133,11 +133,12 @@ class _DesktopHomePageState extends State readOnly: true, decoration: InputDecoration( border: InputBorder.none, + contentPadding: EdgeInsets.only(bottom: 8), ), style: TextStyle( fontSize: 22, ), - ).marginOnly(bottom: 5), + ), ), ) ], @@ -240,7 +241,8 @@ class _DesktopHomePageState extends State child: Obx( () => Container( decoration: BoxDecoration( - borderRadius: BorderRadius.circular(90), + // borderRadius: BorderRadius.circular(10), + shape: BoxShape.circle, boxShadow: [ BoxShadow( color: hover.value @@ -268,7 +270,7 @@ class _DesktopHomePageState extends State final model = gFFI.serverModel; RxBool refreshHover = false.obs; return Container( - margin: EdgeInsets.symmetric(vertical: 12, horizontal: 16.0), + margin: EdgeInsets.only(left: 20.0, right: 16, top: 13, bottom: 13), child: Row( crossAxisAlignment: CrossAxisAlignment.baseline, textBaseline: TextBaseline.alphabetic, @@ -306,6 +308,7 @@ class _DesktopHomePageState extends State readOnly: true, decoration: InputDecoration( border: InputBorder.none, + contentPadding: EdgeInsets.only(bottom: 8), ), style: TextStyle(fontSize: 15), ), @@ -319,7 +322,7 @@ class _DesktopHomePageState extends State ? MyTheme.color(context).text : Color(0xFFDDDDDD), size: 22, - ).marginOnly(right: 5), + ).marginOnly(right: 10, bottom: 8), ), onTap: () => bind.mainUpdateTemporaryPassword(), onHover: (value) => refreshHover.value = value, @@ -337,7 +340,7 @@ class _DesktopHomePageState extends State } }) ], - ).marginOnly(bottom: 20), + ), ], ), ), @@ -423,15 +426,17 @@ class _DesktopHomePageState extends State }, onHover: (value) => editHover.value = value, child: Obx(() => Icon(Icons.edit, - size: 22, - color: editHover.value - ? MyTheme.color(context).text - : Color(0xFFDDDDDD)))); + size: 22, + color: editHover.value + ? MyTheme.color(context).text + : Color(0xFFDDDDDD)) + .marginOnly(bottom: 8))); } buildTip(BuildContext context) { return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0), + padding: + const EdgeInsets.only(left: 20.0, right: 16, top: 16.0, bottom: 14), child: Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, @@ -441,53 +446,21 @@ class _DesktopHomePageState extends State style: TextStyle(fontWeight: FontWeight.normal, fontSize: 19), ), SizedBox( - height: 8.0, + height: 10.0, ), Text( translate("desk_tip"), overflow: TextOverflow.clip, style: TextStyle( - fontSize: 12, color: MyTheme.color(context).lighterText), + fontSize: 12, + color: MyTheme.color(context).lighterText, + height: 1.25), ) ], ), ); } - buildControlPanel(BuildContext context) { - return Container( - width: 320, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), color: MyTheme.white), - padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - translate("Control Remote Desktop"), - style: TextStyle(fontWeight: FontWeight.normal, fontSize: 19), - ), - Form( - child: Column( - children: [ - TextFormField( - controller: TextEditingController(), - inputFormatters: [ - FilteringTextInputFormatter.allow(RegExp(r"[0-9]")) - ], - style: TextStyle(fontSize: 22, fontWeight: FontWeight.w400), - ) - ], - )) - ], - ), - ); - } - - buildRecentSession(BuildContext context) { - return Center(child: Text("waiting implementation")); - } - @override void onTrayMenuItemClick(MenuItem menuItem) { print("click ${menuItem.key}"); diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index 7e94724bb..78f0842ad 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -72,28 +72,33 @@ class _FileManagerTabPageState extends State { Widget build(BuildContext context) { return SubWindowDragToResizeArea( windowId: windowId(), - child: Scaffold( - body: Column( - children: [ - DesktopTabBar( - tabs: tabs, - onTabClose: onRemoveId, - dark: isDarkTheme(), - mainTab: false, - ), - Expanded( - child: Obx( - () => PageView( - controller: DesktopTabBar.controller.value, - children: tabs - .map((tab) => FileManagerPage( - key: ValueKey(tab.label), - id: tab - .label)) //RemotePage(key: ValueKey(e), id: e)) - .toList()), + child: Container( + decoration: BoxDecoration( + border: Border.all(color: MyTheme.color(context).border!)), + child: Scaffold( + backgroundColor: MyTheme.color(context).bg, + body: Column( + children: [ + DesktopTabBar( + tabs: tabs, + onTabClose: onRemoveId, + dark: isDarkTheme(), + mainTab: false, ), - ) - ], + Expanded( + child: Obx( + () => PageView( + controller: DesktopTabBar.controller.value, + children: tabs + .map((tab) => FileManagerPage( + key: ValueKey(tab.label), + id: tab + .label)) //RemotePage(key: ValueKey(e), id: e)) + .toList()), + ), + ) + ], + ), ), ), ); diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index e8f9ec26b..0023158ca 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -33,14 +33,20 @@ class _DesktopServerPageState extends State ChangeNotifierProvider.value(value: gFFI.chatModel), ], child: Consumer( - builder: (context, serverModel, child) => Material( - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Expanded(child: ConnectionManager()), - SizedBox.fromSize(size: Size(0, 15.0)), - ], + builder: (context, serverModel, child) => Container( + decoration: BoxDecoration( + border: + Border.all(color: MyTheme.color(context).border!)), + child: Scaffold( + backgroundColor: MyTheme.color(context).bg, + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Expanded(child: ConnectionManager()), + SizedBox.fromSize(size: Size(0, 15.0)), + ], + ), ), ), ))); diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 74019c815..530696a48 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -249,6 +249,7 @@ class WindowActionPanel extends StatelessWidget { }, is_close: false, )), + // TODO: drag makes window restore Offstage( offstage: !showMaximize, child: FutureBuilder(builder: (context, snapshot) { @@ -273,21 +274,15 @@ class WindowActionPanel extends StatelessWidget { if (is_maximized.value) { windowManager.unmaximize(); } else { - WindowController.fromWindowId(windowId!).minimize(); + windowManager.maximize(); } } else { + // TODO: subwindow is maximized but first query result is not maximized. final wc = WindowController.fromWindowId(windowId!); if (is_maximized.value) { wc.unmaximize(); } else { - final wc = WindowController.fromWindowId(windowId!); - wc.isMaximized().then((maximized) { - if (maximized) { - wc.unmaximize(); - } else { - wc.maximize(); - } - }); + wc.maximize(); } } is_maximized.value = !is_maximized.value; From 8a825a734533719f0d205c6754553546cd7dc6e0 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 23 Aug 2022 17:21:32 +0800 Subject: [PATCH 0260/2015] fix: macos window manager compile Signed-off-by: Kingtous --- flutter/pubspec.lock | 4 ++-- flutter/pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index fef32af73..b8f1421e3 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -1230,8 +1230,8 @@ packages: dependency: "direct main" description: path: "." - ref: f1d69e5d0531af947373ec26ae22808f08b1aac6 - resolved-ref: f1d69e5d0531af947373ec26ae22808f08b1aac6 + ref: "799ef079e87938c3f4340591b4330c2598f38bb9" + resolved-ref: "799ef079e87938c3f4340591b4330c2598f38bb9" url: "https://github.com/Kingtous/rustdesk_window_manager" source: git version: "0.2.5" diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 0f3677402..da6a3cd3e 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -61,7 +61,7 @@ dependencies: window_manager: git: url: https://github.com/Kingtous/rustdesk_window_manager - ref: f1d69e5d0531af947373ec26ae22808f08b1aac6 + ref: 799ef079e87938c3f4340591b4330c2598f38bb9 desktop_multi_window: git: url: https://github.com/Kingtous/rustdesk_desktop_multi_window From 4f859d3c9df5320a15bd18982f0394f9aa4d6b04 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 23 Aug 2022 17:21:50 +0800 Subject: [PATCH 0261/2015] feat: peer card type Signed-off-by: Kingtous --- .../lib/desktop/pages/connection_page.dart | 51 ++- flutter/lib/desktop/widgets/peer_widget.dart | 43 ++- .../lib/desktop/widgets/peercard_widget.dart | 309 ++++++++++++------ 3 files changed, 285 insertions(+), 118 deletions(-) diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index c021217e0..4e2a5639f 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -5,6 +5,7 @@ import 'package:contextmenu/contextmenu.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart'; import 'package:flutter_hbb/desktop/widgets/peer_widget.dart'; +import 'package:flutter_hbb/desktop/widgets/peercard_widget.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; @@ -1014,7 +1015,13 @@ class _PeerTabbedPageState extends State<_PeerTabbedPage> return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _createTabBar(context), + Row( + children: [ + Expanded(child: _createTabBar(context)), + _createSearchBar(context), + _createPeerViewTypeSwitch(context), + ], + ), _createTabBarView(), ], ); @@ -1054,4 +1061,46 @@ class _PeerTabbedPageState extends State<_PeerTabbedPage> controller: _tabController, children: super.widget.children) .paddingSymmetric(horizontal: 12.0, vertical: 4.0)); } + + _createSearchBar(BuildContext context) { + return Offstage(); + } + + _createPeerViewTypeSwitch(BuildContext context) { + final activeDeco = BoxDecoration(color: Colors.white); + return Row( + children: [ + Obx( + () => Container( + padding: EdgeInsets.all(4.0), + decoration: + peerCardUiType.value == PeerUiType.grid ? activeDeco : null, + child: InkWell( + onTap: () { + peerCardUiType.value = PeerUiType.grid; + }, + child: Icon( + Icons.grid_view_rounded, + size: 20, + )), + ), + ), + Obx( + () => Container( + padding: EdgeInsets.all(4.0), + decoration: + peerCardUiType.value == PeerUiType.list ? activeDeco : null, + child: InkWell( + onTap: () { + peerCardUiType.value = PeerUiType.list; + }, + child: Icon( + Icons.list, + size: 20, + )), + ), + ), + ], + ); + } } diff --git a/flutter/lib/desktop/widgets/peer_widget.dart b/flutter/lib/desktop/widgets/peer_widget.dart index 1a66f3a06..9014cb608 100644 --- a/flutter/lib/desktop/widgets/peer_widget.dart +++ b/flutter/lib/desktop/widgets/peer_widget.dart @@ -1,14 +1,15 @@ import 'dart:async'; -import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:visibility_detector/visibility_detector.dart'; import 'package:window_manager/window_manager.dart'; +import '../../common.dart'; import '../../models/peer_model.dart'; import '../../models/platform_model.dart'; -import '../../common.dart'; import 'peercard_widget.dart'; typedef OffstageFunc = bool Function(Peer peer); @@ -82,21 +83,25 @@ class _PeerWidgetState extends State<_PeerWidget> with WindowListener { peers.peers.forEach((peer) { cards.add(Offstage( offstage: super.widget._offstageFunc(peer), - child: Container( - width: 225, - height: 150, - child: VisibilityDetector( - key: Key('${peer.id}'), - onVisibilityChanged: (info) { - final peerId = (info.key as ValueKey).value; - if (info.visibleFraction > 0.00001) { - _curPeers.add(peerId); - } else { - _curPeers.remove(peerId); - } - _lastChangeTime = DateTime.now(); - }, - child: super.widget._peerCardWidgetFunc(peer), + child: Obx( + () => Container( + width: 225, + height: peerCardUiType.value == PeerUiType.grid + ? 150 + : 50, + child: VisibilityDetector( + key: Key('${peer.id}'), + onVisibilityChanged: (info) { + final peerId = (info.key as ValueKey).value; + if (info.visibleFraction > 0.00001) { + _curPeers.add(peerId); + } else { + _curPeers.remove(peerId); + } + _lastChangeTime = DateTime.now(); + }, + child: super.widget._peerCardWidgetFunc(peer), + ), ), ))); }); @@ -162,7 +167,9 @@ class RecentPeerWidget extends BasePeerWidget { super._name = "recent peer"; super._loadEvent = "load_recent_peers"; super._offstageFunc = (Peer _peer) => false; - super._peerCardWidgetFunc = (Peer peer) => RecentPeerCard(peer: peer); + super._peerCardWidgetFunc = (Peer peer) => RecentPeerCard( + peer: peer, + ); super._initPeers = []; } diff --git a/flutter/lib/desktop/widgets/peercard_widget.dart b/flutter/lib/desktop/widgets/peercard_widget.dart index e260ef391..3ec149d60 100644 --- a/flutter/lib/desktop/widgets/peercard_widget.dart +++ b/flutter/lib/desktop/widgets/peercard_widget.dart @@ -5,13 +5,17 @@ import 'package:get/get.dart'; import '../../common.dart'; import '../../models/model.dart'; -import '../../models/platform_model.dart'; import '../../models/peer_model.dart'; +import '../../models/platform_model.dart'; typedef PopupMenuItemsFunc = Future>> Function(); enum PeerType { recent, fav, discovered, ab } +enum PeerUiType { grid, list } + +final peerCardUiType = PeerUiType.grid.obs; + class _PeerCard extends StatefulWidget { final Peer peer; final PopupMenuItemsFunc popupMenuItemsFunc; @@ -39,130 +43,237 @@ class _PeerCardState extends State<_PeerCard> final peer = super.widget.peer; var deco = Rx(BoxDecoration( border: Border.all(color: Colors.transparent, width: 1.0), - borderRadius: BorderRadius.circular(20))); - return Card( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), - child: MouseRegion( - onEnter: (evt) { - deco.value = BoxDecoration( - border: Border.all(color: Colors.blue, width: 1.0), - borderRadius: BorderRadius.circular(20)); - }, - onExit: (evt) { - deco.value = BoxDecoration( - border: Border.all(color: Colors.transparent, width: 1.0), - borderRadius: BorderRadius.circular(20)); - }, - child: GestureDetector( - onDoubleTap: () => _connect(peer.id), - child: _buildPeerTile(context, peer, deco)), - )); + borderRadius: peerCardUiType.value == PeerUiType.grid + ? BorderRadius.circular(20) + : null)); + return MouseRegion( + onEnter: (evt) { + deco.value = BoxDecoration( + border: Border.all(color: Colors.blue, width: 1.0), + borderRadius: peerCardUiType.value == PeerUiType.grid + ? BorderRadius.circular(20) + : null); + }, + onExit: (evt) { + deco.value = BoxDecoration( + border: Border.all(color: Colors.transparent, width: 1.0), + borderRadius: peerCardUiType.value == PeerUiType.grid + ? BorderRadius.circular(20) + : null); + }, + child: GestureDetector( + onDoubleTap: () => _connect(peer.id), + child: Obx(() => peerCardUiType.value == PeerUiType.grid + ? _buildPeerCard(context, peer, deco) + : _buildPeerTile(context, peer, deco))), + ); } Widget _buildPeerTile( BuildContext context, Peer peer, Rx deco) { + final greyStyle = TextStyle(fontSize: 12, color: Colors.grey); return Obx( () => Container( decoration: deco.value, - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, + child: Row( + mainAxisSize: MainAxisSize.max, children: [ + Container( + height: 50, + width: 50, + decoration: BoxDecoration( + color: str2color('${peer.id}${peer.platform}', 0x7f), + ), + alignment: Alignment.center, + child: _getPlatformImage('${peer.platform}').paddingAll(8.0), + ), Expanded( child: Container( - decoration: BoxDecoration( - color: str2color('${peer.id}${peer.platform}', 0x7f), - borderRadius: BorderRadius.only( - topLeft: Radius.circular(20), - topRight: Radius.circular(20), - ), - ), + decoration: BoxDecoration(color: Colors.white), child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Column( - crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ - Container( - padding: const EdgeInsets.all(6), - child: _getPlatformImage('${peer.platform}'), - ), - Row( - children: [ - Expanded( - child: FutureBuilder( - future: bind.mainGetPeerOption( - id: peer.id, key: 'alias'), - builder: (_, snapshot) { - if (snapshot.hasData) { - final name = snapshot.data!.isEmpty - ? '${peer.username}@${peer.hostname}' - : snapshot.data!; - return Tooltip( - message: name, - child: Text( - name, - style: TextStyle( - color: Colors.white70, - fontSize: 12), - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - ), - ); - } else { - // alias has not arrived - return Center( - child: Text( - '${peer.username}@${peer.hostname}', - style: TextStyle( - color: Colors.white70, - fontSize: 12), - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - )); - } - }, - ), - ), - ], + Row(children: [ + Text( + '${peer.id}', + style: TextStyle(fontWeight: FontWeight.w400), + ), + Padding( + padding: EdgeInsets.fromLTRB(4, 4, 8, 4), + child: CircleAvatar( + radius: 5, + backgroundColor: peer.online + ? Colors.green + : Colors.yellow)), + ]), + Align( + alignment: Alignment.centerLeft, + child: FutureBuilder( + future: bind.mainGetPeerOption( + id: peer.id, key: 'alias'), + builder: (_, snapshot) { + if (snapshot.hasData) { + final name = snapshot.data!.isEmpty + ? '${peer.username}@${peer.hostname}' + : snapshot.data!; + return Tooltip( + message: name, + child: Text( + name, + style: greyStyle, + textAlign: TextAlign.start, + overflow: TextOverflow.ellipsis, + ), + ); + } else { + // alias has not arrived + return Text( + '${peer.username}@${peer.hostname}', + style: greyStyle, + textAlign: TextAlign.start, + overflow: TextOverflow.ellipsis, + ); + } + }, + ), ), ], - ).paddingAll(4.0), + ), ), + InkWell( + child: Icon(Icons.more_vert), + onTapDown: (e) { + final x = e.globalPosition.dx; + final y = e.globalPosition.dy; + _menuPos = RelativeRect.fromLTRB(x, y, x, y); + }, + onTap: () { + _showPeerMenu(context, peer.id); + }), ], - ), + ).paddingSymmetric(horizontal: 8.0), ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row(children: [ - Padding( - padding: EdgeInsets.fromLTRB(0, 4, 8, 4), - child: CircleAvatar( - radius: 5, - backgroundColor: - peer.online ? Colors.green : Colors.yellow)), - Text('${peer.id}') - ]), - InkWell( - child: Icon(Icons.more_vert), - onTapDown: (e) { - final x = e.globalPosition.dx; - final y = e.globalPosition.dy; - _menuPos = RelativeRect.fromLTRB(x, y, x, y); - }, - onTap: () { - _showPeerMenu(context, peer.id); - }), - ], - ).paddingSymmetric(vertical: 8.0, horizontal: 12.0) + ) ], ), ), ); } + Widget _buildPeerCard( + BuildContext context, Peer peer, Rx deco) { + return Card( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + child: GestureDetector( + onDoubleTap: () => _connect(peer.id), + child: Obx( + () => Container( + decoration: deco.value, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: Container( + decoration: BoxDecoration( + color: str2color('${peer.id}${peer.platform}', 0x7f), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + ), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.all(6), + child: _getPlatformImage('${peer.platform}'), + ), + Row( + children: [ + Expanded( + child: FutureBuilder( + future: bind.mainGetPeerOption( + id: peer.id, key: 'alias'), + builder: (_, snapshot) { + if (snapshot.hasData) { + final name = snapshot.data!.isEmpty + ? '${peer.username}@${peer.hostname}' + : snapshot.data!; + return Tooltip( + message: name, + child: Text( + name, + style: TextStyle( + color: Colors.white70, + fontSize: 12), + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + ), + ); + } else { + // alias has not arrived + return Center( + child: Text( + '${peer.username}@${peer.hostname}', + style: TextStyle( + color: Colors.white70, + fontSize: 12), + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + )); + } + }, + ), + ), + ], + ), + ], + ).paddingAll(4.0), + ), + ], + ), + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row(children: [ + Padding( + padding: EdgeInsets.fromLTRB(0, 4, 8, 4), + child: CircleAvatar( + radius: 5, + backgroundColor: peer.online + ? Colors.green + : Colors.yellow)), + Text('${peer.id}') + ]), + InkWell( + child: Icon(Icons.more_vert), + onTapDown: (e) { + final x = e.globalPosition.dx; + final y = e.globalPosition.dy; + _menuPos = RelativeRect.fromLTRB(x, y, x, y); + }, + onTap: () { + _showPeerMenu(context, peer.id); + }), + ], + ).paddingSymmetric(vertical: 8.0, horizontal: 12.0) + ], + ), + ), + )), + ); + } + /// Connect to a peer with [id]. /// If [isFileTransfer], starts a session only for file transfer. void _connect(String id, {bool isFileTransfer = false}) async { From 0eed72a60d7a93bfcf9f1426c7fad33fcf755a30 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 23 Aug 2022 17:52:53 +0800 Subject: [PATCH 0262/2015] feat: find ID Signed-off-by: Kingtous --- .../lib/desktop/pages/connection_page.dart | 35 +++++++++++++++++-- flutter/lib/desktop/widgets/peer_widget.dart | 35 +++++++++++++------ 2 files changed, 57 insertions(+), 13 deletions(-) diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 4e2a5639f..08f334c4d 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -977,6 +977,9 @@ class _PeerTabbedPageState extends State<_PeerTabbedPage> // hard code for now void _handleTabSelection() { if (_tabController.indexIsChanging) { + // reset search text + peerSearchText.value = ""; + peerSearchTextController.clear(); _tabIndex.value = _tabController.index; switch (_tabController.index) { case 0: @@ -1063,7 +1066,31 @@ class _PeerTabbedPageState extends State<_PeerTabbedPage> } _createSearchBar(BuildContext context) { - return Offstage(); + return Container( + width: 175, + height: 30, + margin: EdgeInsets.only(right: 16), + decoration: BoxDecoration(color: Colors.white), + child: Obx( + () => TextField( + controller: peerSearchTextController, + onChanged: (searchText) { + peerSearchText.value = searchText; + }, + decoration: InputDecoration( + prefixIcon: Icon( + Icons.search, + size: 20, + ), + contentPadding: EdgeInsets.zero, + hintText: translate("Search ID"), + hintStyle: TextStyle(fontSize: 14), + border: OutlineInputBorder(), + isDense: true, + ), + ), + ), + ); } _createPeerViewTypeSwitch(BuildContext context) { @@ -1082,6 +1109,7 @@ class _PeerTabbedPageState extends State<_PeerTabbedPage> child: Icon( Icons.grid_view_rounded, size: 20, + color: Colors.black54, )), ), ), @@ -1096,11 +1124,12 @@ class _PeerTabbedPageState extends State<_PeerTabbedPage> }, child: Icon( Icons.list, - size: 20, + size: 24, + color: Colors.black54, )), ), ), ], - ); + ).paddingOnly(right: 16.0); } } diff --git a/flutter/lib/desktop/widgets/peer_widget.dart b/flutter/lib/desktop/widgets/peer_widget.dart index 9014cb608..70df44ab5 100644 --- a/flutter/lib/desktop/widgets/peer_widget.dart +++ b/flutter/lib/desktop/widgets/peer_widget.dart @@ -15,10 +15,16 @@ import 'peercard_widget.dart'; typedef OffstageFunc = bool Function(Peer peer); typedef PeerCardWidgetFunc = Widget Function(Peer peer); +/// for peer search text, global obs value +final peerSearchText = "".obs; +final peerSearchTextController = + TextEditingController(text: peerSearchText.value); + class _PeerWidget extends StatefulWidget { late final _peers; late final OffstageFunc _offstageFunc; late final PeerCardWidgetFunc _peerCardWidgetFunc; + _PeerWidget(Peers peers, OffstageFunc offstageFunc, PeerCardWidgetFunc peerCardWidgetFunc, {Key? key}) @@ -72,15 +78,24 @@ class _PeerWidgetState extends State<_PeerWidget> with WindowListener { @override Widget build(BuildContext context) { - final space = 8.0; + final space = 12.0; return ChangeNotifierProvider( create: (context) => super.widget._peers, - child: SingleChildScrollView( - child: Consumer( - builder: (context, peers, child) => Wrap( - children: () { + child: Consumer( + builder: (context, peers, child) => peers.peers.isEmpty + ? Center( + child: Text(translate("Empty")), + ) + : SingleChildScrollView( + child: ObxValue((searchText) { final cards = []; - peers.peers.forEach((peer) { + peers.peers.where((peer) { + if (searchText.isEmpty) { + return true; + } else { + return peer.id.contains(peerSearchText.value); + } + }).forEach((peer) { cards.add(Offstage( offstage: super.widget._offstageFunc(peer), child: Obx( @@ -105,10 +120,10 @@ class _PeerWidgetState extends State<_PeerWidget> with WindowListener { ), ))); }); - return cards; - }(), - spacing: space, - runSpacing: space))), + return Wrap( + children: cards, spacing: space, runSpacing: space); + }, peerSearchText), + )), ); } From 91f2106037e2da417602a9b41686d67c4d7da3bf Mon Sep 17 00:00:00 2001 From: csf Date: Tue, 23 Aug 2022 14:12:30 +0800 Subject: [PATCH 0263/2015] fix mobile build --- flutter/lib/main.dart | 11 +++++++---- flutter/lib/mobile/pages/server_page.dart | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 960bfb667..3e507fd68 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -27,7 +27,7 @@ Future main(List args) async { print("launch args: $args"); if (!isDesktop) { - runMainApp(false); + runMobileApp(); return; } // main window @@ -72,9 +72,6 @@ Future initEnv(String appType) async { // focus on multi-ffi on desktop first await initGlobalFFI(); // await Firebase.initializeApp(); - if (isAndroid) { - toAndroidChannelInit(); - } refreshCurrentUser(); } @@ -96,6 +93,12 @@ void runMainApp(bool startService) async { runApp(App()); } +void runMobileApp() async { + await initEnv(kAppTypeMain); + if (isAndroid) androidChannelInit(); + runApp(App()); +} + void runRemoteScreen(Map argument) async { await initEnv(kAppTypeDesktopRemote); runApp(GetMaterialApp( diff --git a/flutter/lib/mobile/pages/server_page.dart b/flutter/lib/mobile/pages/server_page.dart index abbc5aadc..00c433fd8 100644 --- a/flutter/lib/mobile/pages/server_page.dart +++ b/flutter/lib/mobile/pages/server_page.dart @@ -510,7 +510,7 @@ Widget clientInfo(Client client) { ])); } -void toAndroidChannelInit() { +void androidChannelInit() { gFFI.setMethodCallHandler((method, arguments) { debugPrint("flutter got android msg,$method,$arguments"); try { From 5326e32128141512bc39ab9afb9cd3abbbdc4448 Mon Sep 17 00:00:00 2001 From: csf Date: Tue, 23 Aug 2022 15:24:04 +0800 Subject: [PATCH 0264/2015] fix app type event name for mobile and cm --- flutter/lib/cm_main.dart | 2 +- flutter/lib/consts.dart | 3 ++- flutter/lib/main.dart | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/flutter/lib/cm_main.dart b/flutter/lib/cm_main.dart index 1f71b9e93..bf72849e8 100644 --- a/flutter/lib/cm_main.dart +++ b/flutter/lib/cm_main.dart @@ -14,7 +14,7 @@ void main(List args) async { await windowManager.ensureInitialized(); await windowManager.setSize(Size(400, 600)); await windowManager.setAlignment(Alignment.topRight); - await initEnv(kAppTypeConnectionManager); + await initEnv(kAppTypeMain); gFFI.serverModel.clients .add(Client(0, false, false, "UserA", "123123123", true, false, false)); gFFI.serverModel.clients diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 09e80b482..000a1cb54 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -1,8 +1,9 @@ const double kDesktopRemoteTabBarHeight = 28.0; + +/// [kAppTypeMain] used by 'Desktop Main Page' , 'Mobile (Client and Server)' , 'Desktop CM Page' const String kAppTypeMain = "main"; const String kAppTypeDesktopRemote = "remote"; const String kAppTypeDesktopFileTransfer = "file transfer"; -const String kAppTypeConnectionManager = "connection manager"; const String kTabLabelHomePage = "Home"; const String kTabLabelSettingPage = "Settings"; diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 3e507fd68..a1bebbea7 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -132,7 +132,7 @@ void runConnectionManagerScreen() async { // initialize window WindowOptions windowOptions = getHiddenTitleBarWindowOptions(Size(300, 400)); await Future.wait([ - initEnv(kAppTypeConnectionManager), + initEnv(kAppTypeMain), windowManager.waitUntilReadyToShow(windowOptions, () async { await windowManager.setAlignment(Alignment.topRight); await windowManager.show(); From befb6ffe8f0c869098fac53a29c7026d6b85a892 Mon Sep 17 00:00:00 2001 From: csf Date: Tue, 23 Aug 2022 15:25:18 +0800 Subject: [PATCH 0265/2015] fix cm client authorized --- flutter/lib/common.dart | 6 +++--- flutter/lib/desktop/widgets/tabbar_widget.dart | 4 ++++ flutter/lib/models/server_model.dart | 7 ++++++- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index c33e2b291..ece2ec797 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -18,11 +18,11 @@ import 'models/platform_model.dart'; final globalKey = GlobalKey(); final navigationBarKey = GlobalKey(); -var isAndroid = Platform.isAndroid; -var isIOS = Platform.isIOS; +final isAndroid = Platform.isAndroid; +final isIOS = Platform.isIOS; +final isDesktop = Platform.isWindows || Platform.isMacOS || Platform.isLinux; var isWeb = false; var isWebDesktop = false; -var isDesktop = Platform.isWindows || Platform.isMacOS || Platform.isLinux; var version = ""; int androidVersion = 0; diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 530696a48..7198a1c3c 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -161,6 +161,7 @@ class DesktopTabBar extends StatelessWidget { } static onAdd(RxList tabs, TabInfo tab) { + if (!isDesktop) return; int index = tabs.indexWhere((e) => e.key == tab.key); if (index >= 0) { selected.value = index; @@ -178,6 +179,7 @@ class DesktopTabBar extends StatelessWidget { } static remove(RxList tabs, int index) { + if (!isDesktop) return; if (index < 0) return; if (index == tabs.length - 1) { selected.value = max(0, selected.value - 1); @@ -189,12 +191,14 @@ class DesktopTabBar extends StatelessWidget { } static void jumpTo(RxList tabs, int index) { + if (!isDesktop) return; if (index < 0 || index >= tabs.length) return; selected.value = index; controller.value.jumpToPage(selected.value); } static void close(String? key) { + if (!isDesktop) return; final tabBar = _tabBarListViewKey.currentWidget as _ListView?; if (tabBar == null) return; final tabs = tabBar.tabs; diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index 527cea689..dec13f245 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -465,7 +465,12 @@ class ServerModel with ChangeNotifier { try { final client = Client.fromJson(jsonDecode(evt['client'])); parent.target?.dialogManager.dismissByTag(getLoginDialogTag(client.id)); - _clients.add(client); + final index = _clients.indexWhere((c) => c.id == client.id); + if (index < 0) { + _clients.add(client); + } else { + _clients[index].authorized = true; + } DesktopTabBar.onAdd( tabs, TabInfo( From b71593a25c1a111b284fd1b1d670cd4a4e89c171 Mon Sep 17 00:00:00 2001 From: csf Date: Tue, 23 Aug 2022 15:26:21 +0800 Subject: [PATCH 0266/2015] fix mobile app type event name flutter.rs --- src/flutter.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/flutter.rs b/src/flutter.rs index 5e935642a..3af096494 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -41,7 +41,6 @@ use crate::{client::*, flutter_ffi::EventToUI, make_fd_flutter}; pub(super) const APP_TYPE_MAIN: &str = "main"; pub(super) const APP_TYPE_DESKTOP_REMOTE: &str = "remote"; pub(super) const APP_TYPE_DESKTOP_FILE_TRANSFER: &str = "file transfer"; -pub(super) const APP_TYPE_DESKTOP_CONNECTION_MANAGER: &str = "connection manager"; lazy_static::lazy_static! { // static ref SESSION: Arc>> = Default::default(); @@ -1940,7 +1939,7 @@ pub mod connection_manager { if let Some(s) = GLOBAL_EVENT_STREAM .read() .unwrap() - .get(super::APP_TYPE_DESKTOP_CONNECTION_MANAGER) + .get(super::APP_TYPE_MAIN) { s.add(serde_json::ser::to_string(&h).unwrap_or("".to_owned())); }; From 3b63dea6fe7a1a7df639dc72cbe3ab14b04f8827 Mon Sep 17 00:00:00 2001 From: csf Date: Tue, 23 Aug 2022 15:33:18 +0800 Subject: [PATCH 0267/2015] add port forward closeSuccess --- src/ui/remote.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 060aa59db..aa0282bc2 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -2639,7 +2639,7 @@ impl Interface for Handler { self.lc.write().unwrap().handle_peer_info(username, pi); self.call("updatePrivacyMode", &[]); self.call("updatePi", &make_args!(pi_sciter)); - if self.is_file_transfer() { + if self.is_file_transfer() || self.is_port_forward() { self.call2("closeSuccess", &make_args!()); } else if !self.is_port_forward() { self.msgbox("success", "Successful", "Connected, waiting for image..."); From 3155d40f80c839b4387b9ba6178688040d954ce0 Mon Sep 17 00:00:00 2001 From: csf Date: Tue, 23 Aug 2022 20:22:31 +0800 Subject: [PATCH 0268/2015] fix file_manager_page.dart conflict --- .../lib/desktop/pages/file_manager_page.dart | 22 +------------------ 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index e07fadf28..4a2f11553 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -87,27 +87,6 @@ class _FileManagerPageState extends State @override Widget build(BuildContext context) { super.build(context); - return ChangeNotifierProvider.value( - value: _ffi.fileModel, - child: Consumer(builder: (_context, _model, _child) { - return WillPopScope( - onWillPop: () async { - if (model.selectMode) { - model.toggleSelectMode(); - } - return false; - }, - child: Scaffold( - backgroundColor: MyTheme.color(context).bg, - body: Row( - children: [ - Flexible(flex: 3, child: body(isLocal: true)), - Flexible(flex: 3, child: body(isLocal: false)), - Flexible(flex: 2, child: statusList()) - ], - ), - )); - })); return Overlay(initialEntries: [ OverlayEntry(builder: (context) { _ffi.dialogManager.setOverlayState(Overlay.of(context)); @@ -122,6 +101,7 @@ class _FileManagerPageState extends State return false; }, child: Scaffold( + backgroundColor: MyTheme.color(context).bg, body: Row( children: [ Flexible(flex: 3, child: body(isLocal: true)), From f4745ded232f72122a34dabc9bbdddb2caee341d Mon Sep 17 00:00:00 2001 From: csf Date: Tue, 23 Aug 2022 21:28:44 +0800 Subject: [PATCH 0269/2015] add desktop cm closeAll clients --- flutter/lib/desktop/pages/server_page.dart | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index 0023158ca..bfcc28382 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -18,13 +18,27 @@ class DesktopServerPage extends StatefulWidget { } class _DesktopServerPageState extends State - with AutomaticKeepAliveClientMixin { + with WindowListener, AutomaticKeepAliveClientMixin { @override void initState() { gFFI.ffiModel.updateEventListener(""); + windowManager.addListener(this); super.initState(); } + @override + void dispose() { + windowManager.removeListener(this); + super.dispose(); + } + + @override + void onWindowClose() { + gFFI.serverModel.closeAll(); + gFFI.close(); + super.onWindowClose(); + } + Widget build(BuildContext context) { super.build(context); return MultiProvider( From 5f68c099dd71fd7a4b05a29eeb21d65884b38eaa Mon Sep 17 00:00:00 2001 From: csf Date: Wed, 24 Aug 2022 14:57:41 +0800 Subject: [PATCH 0270/2015] prevent delay by using onDoubleTapDown instead of onDoubleTap --- .../lib/desktop/widgets/peercard_widget.dart | 195 +++++++++--------- 1 file changed, 96 insertions(+), 99 deletions(-) diff --git a/flutter/lib/desktop/widgets/peercard_widget.dart b/flutter/lib/desktop/widgets/peercard_widget.dart index 3ec149d60..e8f4d6801 100644 --- a/flutter/lib/desktop/widgets/peercard_widget.dart +++ b/flutter/lib/desktop/widgets/peercard_widget.dart @@ -62,7 +62,7 @@ class _PeerCardState extends State<_PeerCard> : null); }, child: GestureDetector( - onDoubleTap: () => _connect(peer.id), + onDoubleTapDown: (_) => _connect(peer.id), child: Obx(() => peerCardUiType.value == PeerUiType.grid ? _buildPeerCard(context, peer, deco) : _buildPeerTile(context, peer, deco))), @@ -168,109 +168,106 @@ class _PeerCardState extends State<_PeerCard> BuildContext context, Peer peer, Rx deco) { return Card( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), - child: GestureDetector( - onDoubleTap: () => _connect(peer.id), - child: Obx( - () => Container( - decoration: deco.value, - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Expanded( - child: Container( - decoration: BoxDecoration( - color: str2color('${peer.id}${peer.platform}', 0x7f), - borderRadius: BorderRadius.only( - topLeft: Radius.circular(20), - topRight: Radius.circular(20), - ), - ), - child: Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Container( - padding: const EdgeInsets.all(6), - child: _getPlatformImage('${peer.platform}'), - ), - Row( - children: [ - Expanded( - child: FutureBuilder( - future: bind.mainGetPeerOption( - id: peer.id, key: 'alias'), - builder: (_, snapshot) { - if (snapshot.hasData) { - final name = snapshot.data!.isEmpty - ? '${peer.username}@${peer.hostname}' - : snapshot.data!; - return Tooltip( - message: name, - child: Text( - name, - style: TextStyle( - color: Colors.white70, - fontSize: 12), - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - ), - ); - } else { - // alias has not arrived - return Center( - child: Text( - '${peer.username}@${peer.hostname}', - style: TextStyle( - color: Colors.white70, - fontSize: 12), - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - )); - } - }, - ), - ), - ], - ), - ], - ).paddingAll(4.0), - ), - ], - ), + child: Obx( + () => Container( + decoration: deco.value, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: Container( + decoration: BoxDecoration( + color: str2color('${peer.id}${peer.platform}', 0x7f), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), ), ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + child: Row( children: [ - Row(children: [ - Padding( - padding: EdgeInsets.fromLTRB(0, 4, 8, 4), - child: CircleAvatar( - radius: 5, - backgroundColor: peer.online - ? Colors.green - : Colors.yellow)), - Text('${peer.id}') - ]), - InkWell( - child: Icon(Icons.more_vert), - onTapDown: (e) { - final x = e.globalPosition.dx; - final y = e.globalPosition.dy; - _menuPos = RelativeRect.fromLTRB(x, y, x, y); - }, - onTap: () { - _showPeerMenu(context, peer.id); - }), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.all(6), + child: _getPlatformImage('${peer.platform}'), + ), + Row( + children: [ + Expanded( + child: FutureBuilder( + future: bind.mainGetPeerOption( + id: peer.id, key: 'alias'), + builder: (_, snapshot) { + if (snapshot.hasData) { + final name = snapshot.data!.isEmpty + ? '${peer.username}@${peer.hostname}' + : snapshot.data!; + return Tooltip( + message: name, + child: Text( + name, + style: TextStyle( + color: Colors.white70, + fontSize: 12), + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + ), + ); + } else { + // alias has not arrived + return Center( + child: Text( + '${peer.username}@${peer.hostname}', + style: TextStyle( + color: Colors.white70, + fontSize: 12), + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + )); + } + }, + ), + ), + ], + ), + ], + ).paddingAll(4.0), + ), ], - ).paddingSymmetric(vertical: 8.0, horizontal: 12.0) - ], + ), + ), ), - ), - )), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row(children: [ + Padding( + padding: EdgeInsets.fromLTRB(0, 4, 8, 4), + child: CircleAvatar( + radius: 5, + backgroundColor: + peer.online ? Colors.green : Colors.yellow)), + Text('${peer.id}') + ]), + InkWell( + child: Icon(Icons.more_vert), + onTapDown: (e) { + final x = e.globalPosition.dx; + final y = e.globalPosition.dy; + _menuPos = RelativeRect.fromLTRB(x, y, x, y); + }, + onTap: () { + _showPeerMenu(context, peer.id); + }), + ], + ).paddingSymmetric(vertical: 8.0, horizontal: 12.0) + ], + ), + ), + ), ); } From 0649a49d1746bb288d0ae009f95e753582b1c3d3 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sun, 31 Jul 2022 19:06:49 +0800 Subject: [PATCH 0271/2015] fix 10054: change direct to relay when RST Signed-off-by: 21pages --- Cargo.lock | 22 ++++++++++++ Cargo.toml | 1 + libs/hbb_common/src/config.rs | 1 + src/client.rs | 68 ++++++++++++++++++++++++++--------- src/port_forward.rs | 50 ++++++++++++++++++++++++-- src/ui/remote.rs | 31 +++++++++++++++- 6 files changed, 152 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a6ed80add..89d31556b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1470,6 +1470,27 @@ dependencies = [ "synstructure", ] +[[package]] +name = "errno" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +dependencies = [ + "errno-dragonfly", + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "error-code" version = "2.3.1" @@ -4168,6 +4189,7 @@ dependencies = [ "default-net", "dispatch", "enigo", + "errno", "evdev", "flexi_logger", "flutter_rust_bridge", diff --git a/Cargo.toml b/Cargo.toml index f48a47d9b..aaa01e3ce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,6 +58,7 @@ num_cpus = "1.13" bytes = { version = "1.2", features = ["serde"] } default-net = "0.11.0" wol-rs = "0.9.1" +errno = "0.2.8" [target.'cfg(not(target_os = "linux"))'.dependencies] reqwest = { version = "0.11", features = ["json", "rustls-tls"], default-features=false } diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 26871a958..d7cdb82ce 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -21,6 +21,7 @@ use std::{ pub const RENDEZVOUS_TIMEOUT: u64 = 12_000; pub const CONNECT_TIMEOUT: u64 = 18_000; +pub const READ_TIMEOUT: u64 = 30_000; pub const REG_INTERVAL: i64 = 12_000; pub const COMPRESS_LEVEL: i32 = 3; const SERIAL: i32 = 3; diff --git a/src/client.rs b/src/client.rs index 6a5db19e2..a73d4b60e 100644 --- a/src/client.rs +++ b/src/client.rs @@ -24,7 +24,10 @@ use hbb_common::{ allow_err, anyhow::{anyhow, Context}, bail, - config::{Config, PeerConfig, PeerInfoSerde, CONNECT_TIMEOUT, RELAY_PORT, RENDEZVOUS_TIMEOUT}, + config::{ + Config, PeerConfig, PeerInfoSerde, CONNECT_TIMEOUT, READ_TIMEOUT, RELAY_PORT, + RENDEZVOUS_TIMEOUT, + }, log, message_proto::{option_message::BoolOption, *}, protobuf::Message as _, @@ -116,8 +119,9 @@ impl Client { key: &str, token: &str, conn_type: ConnType, + interface: impl Interface, ) -> ResultType<(Stream, bool)> { - match Self::_start(peer, key, token, conn_type).await { + match Self::_start(peer, key, token, conn_type, interface).await { Err(err) => { let err_str = err.to_string(); if err_str.starts_with("Failed") { @@ -135,6 +139,7 @@ impl Client { key: &str, token: &str, conn_type: ConnType, + interface: impl Interface, ) -> ResultType<(Stream, bool)> { // to-do: remember the port for each peer, so that we can retry easier let any_addr = Config::get_any_listen_addr(); @@ -181,7 +186,11 @@ impl Client { log::info!("#{} punch attempt with {}, id: {}", i, my_addr, peer); let mut msg_out = RendezvousMessage::new(); use hbb_common::protobuf::Enum; - let nat_type = NatType::from_i32(my_nat_type).unwrap_or(NatType::UNKNOWN_NAT); + let nat_type = if interface.is_force_relay() { + NatType::SYMMETRIC + } else { + NatType::from_i32(my_nat_type).unwrap_or(NatType::UNKNOWN_NAT) + }; msg_out.set_punch_hole_request(PunchHoleRequest { id: peer.to_owned(), token: token.to_owned(), @@ -233,7 +242,15 @@ impl Client { let mut conn = Self::create_relay(peer, rr.uuid, rr.relay_server, key, conn_type) .await?; - Self::secure_connection(peer, signed_id_pk, key, &mut conn).await?; + Self::secure_connection( + peer, + signed_id_pk, + key, + &mut conn, + false, + interface, + ) + .await?; return Ok((conn, false)); } _ => { @@ -274,6 +291,7 @@ impl Client { key, token, conn_type, + interface, ) .await } @@ -292,6 +310,7 @@ impl Client { key: &str, token: &str, conn_type: ConnType, + interface: impl Interface, ) -> ResultType<(Stream, bool)> { let direct_failures = PeerConfig::load(peer_id).direct_failures; let mut connect_timeout = 0; @@ -329,8 +348,8 @@ impl Client { let start = std::time::Instant::now(); // NOTICE: Socks5 is be used event in intranet. Which may be not a good way. let mut conn = socket_client::connect_tcp(peer, local_addr, connect_timeout).await; - let direct = !conn.is_err(); - if conn.is_err() { + let mut direct = !conn.is_err(); + if interface.is_force_relay() || conn.is_err() { if !relay_server.is_empty() { conn = Self::request_relay( peer_id, @@ -348,6 +367,7 @@ impl Client { conn.err().unwrap() ); } + direct = false; } else { bail!("Failed to make direct connection to remote desktop"); } @@ -360,7 +380,7 @@ impl Client { } let mut conn = conn?; log::info!("{:?} used to establish connection", start.elapsed()); - Self::secure_connection(peer_id, signed_id_pk, key, &mut conn).await?; + Self::secure_connection(peer_id, signed_id_pk, key, &mut conn, direct, interface).await?; Ok((conn, direct)) } @@ -369,6 +389,8 @@ impl Client { signed_id_pk: Vec, key: &str, conn: &mut Stream, + direct: bool, + mut interface: impl Interface, ) -> ResultType<()> { let rs_pk = get_rs_pk(if key.is_empty() { hbb_common::config::RS_PUB_KEY @@ -394,9 +416,15 @@ impl Client { return Ok(()); } }; - match timeout(CONNECT_TIMEOUT, conn.next()).await? { + match timeout(READ_TIMEOUT, conn.next()).await? { Some(res) => { - let bytes = res?; + let bytes = match res { + Ok(bytes) => bytes, + Err(err) => { + interface.set_force_relay(direct, false); + bail!("{}", err); + } + }; if let Ok(msg_in) = Message::parse_from_bytes(&bytes) { if let Some(message::Union::SignedId(si)) = msg_in.union { if let Ok((id, their_pk_b)) = decode_id_pk(&si.id, &sign_pk) { @@ -786,6 +814,7 @@ pub struct LoginConfigHandler { session_id: u64, pub supported_encoding: Option<(bool, bool)>, pub restarting_remote_device: bool, + pub force_relay: bool, } impl Deref for LoginConfigHandler { @@ -812,6 +841,7 @@ impl LoginConfigHandler { self.session_id = rand::random(); self.supported_encoding = None; self.restarting_remote_device = false; + self.force_relay = false; } pub fn should_auto_login(&self) -> String { @@ -1418,6 +1448,8 @@ pub trait Interface: Send + Clone + 'static + Sized { fn msgbox(&self, msgtype: &str, title: &str, text: &str); fn handle_login_error(&mut self, err: &str) -> bool; fn handle_peer_info(&mut self, pi: PeerInfo); + fn set_force_relay(&mut self, direct: bool, received: bool); + fn is_force_relay(&self) -> bool; async fn handle_hash(&mut self, pass: &str, hash: Hash, peer: &mut Stream); async fn handle_login_from_ui(&mut self, password: String, remember: bool, peer: &mut Stream); async fn handle_test_delay(&mut self, t: TestDelay, peer: &mut Stream); @@ -1579,14 +1611,16 @@ lazy_static::lazy_static! { pub fn check_if_retry(msgtype: &str, title: &str, text: &str) -> bool { msgtype == "error" && title == "Connection Error" - && !text.to_lowercase().contains("offline") - && !text.to_lowercase().contains("exist") - && !text.to_lowercase().contains("handshake") - && !text.to_lowercase().contains("failed") - && !text.to_lowercase().contains("resolve") - && !text.to_lowercase().contains("mismatch") - && !text.to_lowercase().contains("manually") - && !text.to_lowercase().contains("not allowed") + && (text.contains("10054") + || text.contains("104") + || (!text.to_lowercase().contains("offline") + && !text.to_lowercase().contains("exist") + && !text.to_lowercase().contains("handshake") + && !text.to_lowercase().contains("failed") + && !text.to_lowercase().contains("resolve") + && !text.to_lowercase().contains("mismatch") + && !text.to_lowercase().contains("manually") + && !text.to_lowercase().contains("not allowed"))) } #[inline] diff --git a/src/port_forward.rs b/src/port_forward.rs index a17ee8259..9a697da42 100644 --- a/src/port_forward.rs +++ b/src/port_forward.rs @@ -1,7 +1,7 @@ use crate::client::*; use hbb_common::{ allow_err, bail, - config::CONNECT_TIMEOUT, + config::READ_TIMEOUT, futures::{SinkExt, StreamExt}, log, message_proto::*, @@ -105,22 +105,61 @@ async fn connect_and_login( key: &str, token: &str, is_rdp: bool, +) -> ResultType> { + let mut res = connect_and_login_2( + id, + password, + ui_receiver, + interface.clone(), + forward, + key, + token, + is_rdp, + ) + .await; + if res.is_err() && interface.is_force_relay() { + res = connect_and_login_2( + id, + password, + ui_receiver, + interface, + forward, + key, + token, + is_rdp, + ) + .await; + } + res +} + +async fn connect_and_login_2( + id: &str, + password: &str, + ui_receiver: &mut mpsc::UnboundedReceiver, + interface: impl Interface, + forward: &mut Framed, + key: &str, + token: &str, + is_rdp: bool, ) -> ResultType> { let conn_type = if is_rdp { ConnType::RDP } else { ConnType::PORT_FORWARD }; - let (mut stream, _) = Client::start(id, key, token, conn_type).await?; + let (mut stream, direct) = Client::start(id, key, token, conn_type, interface.clone()).await?; let mut interface = interface; let mut buffer = Vec::new(); + let mut received = false; loop { tokio::select! { - res = timeout(CONNECT_TIMEOUT, stream.next()) => match res { + res = timeout(READ_TIMEOUT, stream.next()) => match res { Err(_) => { bail!("Timeout"); } Ok(Some(Ok(bytes))) => { + received = true; let msg_in = Message::parse_from_bytes(&bytes)?; match msg_in.union { Some(message::Union::Hash(hash)) => { @@ -143,6 +182,11 @@ async fn connect_and_login( _ => {} } } + Ok(Some(Err(err))) => { + log::error!("Connection closed: {}", err); + interface.set_force_relay(direct, received); + bail!("Connection closed: {}", err); + } _ => { bail!("Reset by the peer"); } diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 1a446317d..c9dd45888 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -53,6 +53,7 @@ use crate::{ client::*, common::{self, check_clipboard, update_clipboard, ClipboardContext, CLIPBOARD_INTERVAL}, }; +use errno; type Video = AssetPtr; @@ -1456,12 +1457,21 @@ impl Remote { 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() { ConnType::FILE_TRANSFER } else { ConnType::default() }; - match Client::start(&self.handler.id, key, token, conn_type).await { + match Client::start( + &self.handler.id, + key, + token, + conn_type, + self.handler.clone(), + ) + .await + { Ok((mut peer, direct)) => { SERVER_KEYBOARD_ENABLED.store(true, Ordering::SeqCst); SERVER_CLIPBOARD_ENABLED.store(true, Ordering::SeqCst); @@ -1484,11 +1494,13 @@ impl Remote { match res { Err(err) => { log::error!("Connection closed: {}", err); + self.handler.set_force_relay(direct, received); self.handler.msgbox("error", "Connection Error", &err.to_string()); break; } Ok(ref bytes) => { last_recv_time = Instant::now(); + received = true; self.data_count.fetch_add(bytes.len(), Ordering::Relaxed); if !self.handle_msg_from_peer(bytes, &mut peer).await { break @@ -2695,6 +2707,23 @@ impl Interface for Handler { handle_test_delay(t, peer).await; } } + + fn set_force_relay(&mut self, direct: bool, received: bool) { + let mut lc = self.lc.write().unwrap(); + lc.force_relay = false; + if direct && !received { + let errno = errno::errno().0; + log::info!("errno is {}", errno); + // TODO + if cfg!(windows) && errno == 10054 || !cfg!(windows) && errno == 104 { + lc.force_relay = true; + } + } + } + + fn is_force_relay(&self) -> bool { + self.lc.read().unwrap().force_relay + } } impl Handler { From a7c87a5f573def7620507f94b375996b9ff6f14c Mon Sep 17 00:00:00 2001 From: 21pages Date: Wed, 24 Aug 2022 16:23:36 +0800 Subject: [PATCH 0272/2015] option to enable force-always-relay Signed-off-by: 21pages --- src/client.rs | 2 +- src/ui/ab.tis | 3 +-- src/ui/remote.rs | 27 ++++++++++++++------------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/client.rs b/src/client.rs index a73d4b60e..9f5338a6c 100644 --- a/src/client.rs +++ b/src/client.rs @@ -841,7 +841,7 @@ impl LoginConfigHandler { self.session_id = rand::random(); self.supported_encoding = None; self.restarting_remote_device = false; - self.force_relay = false; + self.force_relay = !self.get_option("force-always-relay").is_empty(); } pub fn should_auto_login(&self) -> String { diff --git a/src/ui/ab.tis b/src/ui/ab.tis index 28fa62352..658783623 100644 --- a/src/ui/ab.tis +++ b/src/ui/ab.tis @@ -316,7 +316,7 @@ class SessionList: Reactor.Component {
  • {translate('Connect')}
  • {translate('Transfer File')}
  • {translate('TCP Tunneling')}
  • - {false && !handler.using_public_server() &&
  • {svg_checkmark}{translate('Always connect via relay')}
  • } +
  • {svg_checkmark}{translate('Always connect via relay')}
  • RDP
  • {translate('WOL')}
  • @@ -396,7 +396,6 @@ class SessionList: Reactor.Component { if (el) { var force = handler.get_peer_option(id, "force-always-relay"); el.attributes.toggleClass("selected", force == "Y"); - el.attributes.toggleClass("line-through", force != "Y"); } var conn = this.$(menu #connect); if (conn) { diff --git a/src/ui/remote.rs b/src/ui/remote.rs index c9dd45888..25aacd26d 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -2085,18 +2085,18 @@ impl Remote { async fn send_opts_after_login(&self, peer: &mut Stream) { if let Some(opts) = self - .handler - .lc - .read() - .unwrap() - .get_option_message_after_login() - { - let mut misc = Misc::new(); - misc.set_option(opts); - let mut msg_out = Message::new(); - msg_out.set_misc(misc); - allow_err!(peer.send(&msg_out).await); - } + .handler + .lc + .read() + .unwrap() + .get_option_message_after_login() + { + let mut misc = Misc::new(); + misc.set_option(opts); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + allow_err!(peer.send(&msg_out).await); + } } async fn handle_msg_from_peer(&mut self, data: &[u8], peer: &mut Stream) -> bool { @@ -2714,9 +2714,10 @@ impl Interface for Handler { if direct && !received { let errno = errno::errno().0; log::info!("errno is {}", errno); - // TODO + // TODO: check mac and ios if cfg!(windows) && errno == 10054 || !cfg!(windows) && errno == 104 { lc.force_relay = true; + lc.set_option("force-always-relay".to_owned(), "Y".to_owned()); } } } From 78c79a0e8d3cab86c7d30a6ec5927e58e861ca59 Mon Sep 17 00:00:00 2001 From: csf Date: Wed, 24 Aug 2022 20:12:04 +0800 Subject: [PATCH 0273/2015] refactor tabbar_widget.dart and impl for desktop_tab_page.dart --- .../lib/desktop/pages/desktop_tab_page.dart | 69 +-- .../lib/desktop/widgets/tabbar_widget.dart | 519 +++++++++--------- 2 files changed, 273 insertions(+), 315 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_tab_page.dart b/flutter/lib/desktop/pages/desktop_tab_page.dart index 141b7ca0e..5cc86f0ca 100644 --- a/flutter/lib/desktop/pages/desktop_tab_page.dart +++ b/flutter/lib/desktop/pages/desktop_tab_page.dart @@ -4,7 +4,6 @@ import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart'; import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart'; import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; -import 'package:get/get.dart'; import 'package:window_manager/window_manager.dart'; class DesktopTabPage extends StatefulWidget { @@ -15,65 +14,51 @@ class DesktopTabPage extends StatefulWidget { } class _DesktopTabPageState extends State { - late RxList tabs; + final tabBarController = DesktopTabBarController(); @override void initState() { super.initState(); - tabs = RxList.from([ - TabInfo( - key: kTabLabelHomePage, - label: kTabLabelHomePage, - selectedIcon: Icons.home_sharp, - unselectedIcon: Icons.home_outlined, - closable: false) - ], growable: true); + tabBarController.state.value.tabs.add(TabInfo( + key: kTabLabelHomePage, + label: kTabLabelHomePage, + selectedIcon: Icons.home_sharp, + unselectedIcon: Icons.home_outlined, + closable: false, + page: DesktopHomePage())); } @override Widget build(BuildContext context) { + final dark = isDarkTheme(); return DragToResizeArea( child: Container( decoration: BoxDecoration( border: Border.all(color: MyTheme.color(context).border!)), child: Scaffold( - backgroundColor: MyTheme.color(context).bg, - body: Column( - children: [ - DesktopTabBar( - tabs: tabs, - dark: isDarkTheme(), - mainTab: true, - onAddSetting: onAddSetting, + backgroundColor: MyTheme.color(context).bg, + body: DesktopTab( + controller: tabBarController, + theme: dark ? TarBarTheme.dark() : TarBarTheme.light(), + isMainWindow: true, + tail: ActionIcon( + message: 'Settings', + icon: IconFont.menu, + theme: dark ? TarBarTheme.dark() : TarBarTheme.light(), + onTap: onAddSetting, + is_close: false, ), - Obx((() => Expanded( - child: PageView( - controller: DesktopTabBar.controller.value, - children: tabs.map((tab) { - switch (tab.label) { - case kTabLabelHomePage: - return DesktopHomePage(key: ValueKey(tab.label)); - case kTabLabelSettingPage: - return DesktopSettingPage(key: ValueKey(tab.label)); - default: - return Container(); - } - }).toList()), - ))), - ], - ), - ), + )), ), ); } void onAddSetting() { - DesktopTabBar.onAdd( - tabs, - TabInfo( - key: kTabLabelSettingPage, - label: kTabLabelSettingPage, - selectedIcon: Icons.build_sharp, - unselectedIcon: Icons.build_outlined)); + tabBarController.add(TabInfo( + key: kTabLabelSettingPage, + label: kTabLabelSettingPage, + selectedIcon: Icons.build_sharp, + unselectedIcon: Icons.build_outlined, + page: DesktopSettingPage())); } } diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 7198a1c3c..7544c6ef0 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -16,210 +16,216 @@ const double _kDividerIndent = 10; const double _kActionIconSize = 12; class TabInfo { - late final String key; - late final String label; - late final IconData? selectedIcon; - late final IconData? unselectedIcon; - late final bool closable; + final String key; + final String label; + final IconData? selectedIcon; + final IconData? unselectedIcon; + final bool closable; + final Widget page; TabInfo( {required this.key, required this.label, this.selectedIcon, this.unselectedIcon, - this.closable = true}); + this.closable = true, + required this.page}); } -class DesktopTabBar extends StatelessWidget { - late final RxList tabs; - late final Function(String)? onTabClose; - late final bool dark; - late final _Theme _theme; - late final bool mainTab; - late final bool showLogo; - late final bool showTitle; - late final bool showMinimize; - late final bool showMaximize; - late final bool showClose; - late final void Function()? onAddSetting; - late final void Function(int)? onSelected; +class DesktopTabBarState { + final List tabs = []; final ScrollPosController scrollController = ScrollPosController(itemCount: 0); - static final Rx controller = PageController().obs; - static final Rx selected = 0.obs; - static final _tabBarListViewKey = GlobalKey(); + final PageController pageController = PageController(); + int selected = 0; - DesktopTabBar( - {Key? key, - required this.tabs, + DesktopTabBarState() { + scrollController.itemCount = tabs.length; + // TODO test + // WidgetsBinding.instance.addPostFrameCallback((_) { + // scrollController.scrollToItem(selected, + // center: true, animate: true); + // }); + } +} + +class DesktopTabBarController { + final state = DesktopTabBarState().obs; + + void add(TabInfo tab) { + if (!isDesktop) return; + final index = state.value.tabs.indexWhere((e) => e.key == tab.key); + int toIndex; + if (index >= 0) { + toIndex = index; + } else { + state.update((val) { + val!.tabs.add(tab); + }); + toIndex = state.value.tabs.length - 1; + assert(toIndex >= 0); + } + try { + jumpTo(toIndex); + } catch (e) { + // call before binding controller will throw + debugPrint("Failed to jumpTo: $e"); + } + } + + void remove(int index) { + if (!isDesktop) return; + if (index < 0) return; + final len = state.value.tabs.length; + final currentSelected = state.value.selected; + int toIndex = 0; + if (index == len - 1) { + toIndex = max(0, currentSelected - 1); + } else if (index < len - 1 && index < currentSelected) { + toIndex = max(0, currentSelected - 1); + } + state.value.tabs.removeAt(index); + state.value.scrollController.itemCount = state.value.tabs.length; + jumpTo(toIndex); + } + + void jumpTo(int index) { + state.update((val) { + val!.selected = index; + val.pageController.jumpToPage(index); + val.scrollController.scrollToItem(index, center: true, animate: true); + }); + + // onSelected callback + } +} + +class DesktopTab extends StatelessWidget { + final Function(String)? onTabClose; + final TarBarTheme theme; + final bool isMainWindow; + final bool showLogo; + final bool showTitle; + final bool showMinimize; + final bool showMaximize; + final bool showClose; + final Widget Function(Widget pageView)? pageViewBuilder; + final Widget? tail; + + final DesktopTabBarController controller; + late final state = controller.state; + + DesktopTab( + {required this.controller, + required this.isMainWindow, + this.theme = const TarBarTheme.light(), this.onTabClose, - required this.dark, - required this.mainTab, - this.onAddSetting, - this.onSelected, this.showLogo = true, this.showTitle = true, this.showMinimize = true, this.showMaximize = true, - this.showClose = true}) - : _theme = dark ? _Theme.dark() : _Theme.light(), - super(key: key) { - scrollController.itemCount = tabs.length; - WidgetsBinding.instance.addPostFrameCallback((_) { - scrollController.scrollToItem(selected.value, - center: true, animate: true); - }); - } + this.showClose = true, + this.pageViewBuilder, + this.tail}); @override Widget build(BuildContext context) { - return Container( - height: _kTabBarHeight, - child: Column( - children: [ - Container( - height: _kTabBarHeight - 1, - child: Row( - children: [ - Expanded( - child: Row( - children: [ - Row(children: [ - Offstage( - offstage: !showLogo, - child: Image.asset( - 'assets/logo.ico', - width: 20, - height: 20, - )), - Offstage( - offstage: !showTitle, - child: Text( - "RustDesk", - style: TextStyle(fontSize: 13), - ).marginOnly(left: 2)) - ]).marginOnly( - left: 5, - right: 10, - ), - Expanded( - child: GestureDetector( - onPanStart: (_) { - if (mainTab) { - windowManager.startDragging(); - } else { - WindowController.fromWindowId(windowId!) - .startDragging(); - } - }, - child: _ListView( - key: _tabBarListViewKey, - controller: controller, - scrollController: scrollController, - tabInfos: tabs, - selected: selected, - onTabClose: onTabClose, - theme: _theme, - onSelected: onSelected)), - ), - Offstage( - offstage: mainTab, - child: _AddButton( - theme: _theme, - ).paddingOnly(left: 10), - ), - ], - ), - ), - Offstage( - offstage: onAddSetting == null, - child: _ActionIcon( - message: 'Settings', - icon: IconFont.menu, - theme: _theme, - onTap: () => onAddSetting?.call(), - is_close: false, - ), - ), - WindowActionPanel( - mainTab: mainTab, - theme: _theme, - showMinimize: showMinimize, - showMaximize: showMaximize, - showClose: showClose, - ) - ], + return Column(children: [ + Container( + height: _kTabBarHeight, + child: Column( + children: [ + Container( + height: _kTabBarHeight - 1, + child: _buildBar(), ), - ), - Divider( - height: 1, - thickness: 1, - ), - ], + Divider( + height: 1, + thickness: 1, + ), + ], + ), ), + Expanded( + child: pageViewBuilder != null + ? pageViewBuilder!(_buildPageView()) + : _buildPageView()) + ]); + } + + Widget _buildPageView() { + debugPrint("_buildPageView: ${state.value.tabs.length}"); + return Obx(() => PageView( + controller: state.value.pageController, + children: + state.value.tabs.map((tab) => tab.page).toList(growable: false))); + } + + Widget _buildBar() { + return Row( + children: [ + Expanded( + child: Row( + children: [ + Row(children: [ + Offstage( + offstage: !showLogo, + child: Image.asset( + 'assets/logo.ico', + width: 20, + height: 20, + )), + Offstage( + offstage: !showTitle, + child: Text( + "RustDesk", + style: TextStyle(fontSize: 13), + ).marginOnly(left: 2)) + ]).marginOnly( + left: 5, + right: 10, + ), + Expanded( + child: GestureDetector( + onPanStart: (_) { + if (isMainWindow) { + windowManager.startDragging(); + } else { + WindowController.fromWindowId(windowId!) + .startDragging(); + } + }, + child: _ListView( + controller: controller, + onTabClose: onTabClose, + theme: theme, + )), + ), + Offstage( + offstage: isMainWindow, + child: _AddButton( + theme: theme, + ).paddingOnly(left: 10), + ), + ], + ), + ), + Offstage(offstage: tail == null, child: tail), + WindowActionPanel( + mainTab: isMainWindow, + theme: theme, + showMinimize: showMinimize, + showMaximize: showMaximize, + showClose: showClose, + ) + ], ); } - - static onAdd(RxList tabs, TabInfo tab) { - if (!isDesktop) return; - int index = tabs.indexWhere((e) => e.key == tab.key); - if (index >= 0) { - selected.value = index; - } else { - tabs.add(tab); - selected.value = tabs.length - 1; - assert(selected.value >= 0); - } - try { - controller.value.jumpToPage(selected.value); - } catch (e) { - // call before binding controller will throw - debugPrint("Failed to jumpToPage: $e"); - } - } - - static remove(RxList tabs, int index) { - if (!isDesktop) return; - if (index < 0) return; - if (index == tabs.length - 1) { - selected.value = max(0, selected.value - 1); - } else if (index < tabs.length - 1 && index < selected.value) { - selected.value = max(0, selected.value - 1); - } - tabs.removeAt(index); - controller.value.jumpToPage(selected.value); - } - - static void jumpTo(RxList tabs, int index) { - if (!isDesktop) return; - if (index < 0 || index >= tabs.length) return; - selected.value = index; - controller.value.jumpToPage(selected.value); - } - - static void close(String? key) { - if (!isDesktop) return; - final tabBar = _tabBarListViewKey.currentWidget as _ListView?; - if (tabBar == null) return; - final tabs = tabBar.tabs; - if (key == null) { - if (tabBar.selected.value < tabs.length) { - tabs[tabBar.selected.value].onClose(); - } - } else { - for (final tab in tabs) { - if (tab.key == key) { - tab.onClose(); - break; - } - } - } - } } class WindowActionPanel extends StatelessWidget { final bool mainTab; - final _Theme theme; + final TarBarTheme theme; final bool showMinimize; final bool showMaximize; @@ -240,7 +246,7 @@ class WindowActionPanel extends StatelessWidget { children: [ Offstage( offstage: !showMinimize, - child: _ActionIcon( + child: ActionIcon( message: 'Minimize', icon: IconFont.min, theme: theme, @@ -269,7 +275,7 @@ class WindowActionPanel extends StatelessWidget { }); } return Obx( - () => _ActionIcon( + () => ActionIcon( message: is_maximized.value ? "Restore" : "Maximize", icon: is_maximized.value ? IconFont.restore : IconFont.max, theme: theme, @@ -297,7 +303,7 @@ class WindowActionPanel extends StatelessWidget { })), Offstage( offstage: !showClose, - child: _ActionIcon( + child: ActionIcon( message: 'Close', icon: IconFont.close, theme: theme, @@ -317,69 +323,37 @@ class WindowActionPanel extends StatelessWidget { // ignore: must_be_immutable class _ListView extends StatelessWidget { - final Rx controller; - final ScrollPosController scrollController; - final RxList tabInfos; - final Rx selected; + final DesktopTabBarController controller; + late final Rx state; final Function(String key)? onTabClose; - final _Theme _theme; - late List<_Tab> tabs; - late final void Function(int)? onSelected; + final TarBarTheme theme; _ListView( - {Key? key, - required this.controller, - required this.scrollController, - required this.tabInfos, - required this.selected, - required this.onTabClose, - required _Theme theme, - this.onSelected}) - : _theme = theme, - super(key: key); + {required this.controller, required this.onTabClose, required this.theme}) + : this.state = controller.state; @override Widget build(BuildContext context) { - return Obx(() { - tabs = tabInfos.asMap().entries.map((e) { - int index = e.key; - return _Tab( - index: index, - label: e.value.label, - selectedIcon: e.value.selectedIcon, - unselectedIcon: e.value.unselectedIcon, - closable: e.value.closable, - selected: selected.value, - onClose: () { - tabInfos.removeWhere((tab) => tab.key == e.value.key); - onTabClose?.call(e.value.key); - if (index <= selected.value) { - selected.value = max(0, selected.value - 1); - } - assert(tabInfos.length == 0 || selected.value < tabInfos.length); - scrollController.itemCount = tabInfos.length; - if (tabInfos.length > 0) { - scrollController.scrollToItem(selected.value, - center: true, animate: true); - controller.value.jumpToPage(selected.value); - } - }, - onSelected: () { - selected.value = index; - scrollController.scrollToItem(index, center: true, animate: true); - controller.value.jumpToPage(index); - onSelected?.call(selected.value); - }, - theme: _theme, - ); - }).toList(); - return ListView( - controller: scrollController, - scrollDirection: Axis.horizontal, - shrinkWrap: true, - physics: BouncingScrollPhysics(), - children: tabs); - }); + return Obx(() => ListView( + controller: state.value.scrollController, + scrollDirection: Axis.horizontal, + shrinkWrap: true, + physics: BouncingScrollPhysics(), + children: state.value.tabs.asMap().entries.map((e) { + final index = e.key; + final tab = e.value; + return _Tab( + index: index, + label: tab.label, + selectedIcon: tab.selectedIcon, + unselectedIcon: tab.unselectedIcon, + closable: tab.closable, + selected: state.value.selected, + onClose: () => controller.remove(index), + onSelected: () => controller.jumpTo(index), + theme: theme, + ); + }).toList())); } } @@ -393,7 +367,7 @@ class _Tab extends StatelessWidget { late final Function() onClose; late final Function() onSelected; final RxBool _hover = false.obs; - late final _Theme theme; + late final TarBarTheme theme; _Tab( {Key? key, @@ -474,7 +448,7 @@ class _Tab extends StatelessWidget { } class _AddButton extends StatelessWidget { - late final _Theme theme; + late final TarBarTheme theme; _AddButton({ Key? key, @@ -483,7 +457,7 @@ class _AddButton extends StatelessWidget { @override Widget build(BuildContext context) { - return _ActionIcon( + return ActionIcon( message: 'New Connection', icon: IconFont.add, theme: theme, @@ -497,7 +471,7 @@ class _CloseButton extends StatelessWidget { final bool visiable; final bool tabSelected; final Function onClose; - late final _Theme theme; + late final TarBarTheme theme; _CloseButton({ Key? key, @@ -528,13 +502,13 @@ class _CloseButton extends StatelessWidget { } } -class _ActionIcon extends StatelessWidget { +class ActionIcon extends StatelessWidget { final String message; final IconData icon; - final _Theme theme; + final TarBarTheme theme; final Function() onTap; final bool is_close; - const _ActionIcon({ + const ActionIcon({ Key? key, required this.message, required this.icon, @@ -568,35 +542,34 @@ class _ActionIcon extends StatelessWidget { } } -class _Theme { - late Color unSelectedtabIconColor; - late Color selectedtabIconColor; - late Color selectedTextColor; - late Color unSelectedTextColor; - late Color selectedIconColor; - late Color unSelectedIconColor; - late Color dividerColor; - late Color hoverColor; +class TarBarTheme { + final Color unSelectedtabIconColor; + final Color selectedtabIconColor; + final Color selectedTextColor; + final Color unSelectedTextColor; + final Color selectedIconColor; + final Color unSelectedIconColor; + final Color dividerColor; + final Color hoverColor; - _Theme.light() { - unSelectedtabIconColor = Color.fromARGB(255, 162, 203, 241); - selectedtabIconColor = MyTheme.accent; - selectedTextColor = Color.fromARGB(255, 26, 26, 26); - unSelectedTextColor = Color.fromARGB(255, 96, 96, 96); - selectedIconColor = Color.fromARGB(255, 26, 26, 26); - unSelectedIconColor = Color.fromARGB(255, 96, 96, 96); - dividerColor = Color.fromARGB(255, 238, 238, 238); - hoverColor = Colors.grey.withOpacity(0.2); - } + const TarBarTheme.light() + : unSelectedtabIconColor = const Color.fromARGB(255, 162, 203, 241), + selectedtabIconColor = MyTheme.accent, + selectedTextColor = const Color.fromARGB(255, 26, 26, 26), + unSelectedTextColor = const Color.fromARGB(255, 96, 96, 96), + selectedIconColor = const Color.fromARGB(255, 26, 26, 26), + unSelectedIconColor = const Color.fromARGB(255, 96, 96, 96), + dividerColor = const Color.fromARGB(255, 238, 238, 238), + hoverColor = const Color.fromARGB( + 51, 158, 158, 158); // Colors.grey; //0xFF9E9E9E - _Theme.dark() { - unSelectedtabIconColor = Color.fromARGB(255, 30, 65, 98); - selectedtabIconColor = MyTheme.accent; - selectedTextColor = Color.fromARGB(255, 255, 255, 255); - unSelectedTextColor = Color.fromARGB(255, 207, 207, 207); - selectedIconColor = Color.fromARGB(255, 215, 215, 215); - unSelectedIconColor = Color.fromARGB(255, 255, 255, 255); - dividerColor = Color.fromARGB(255, 64, 64, 64); - hoverColor = Colors.black26; - } + const TarBarTheme.dark() + : unSelectedtabIconColor = const Color.fromARGB(255, 30, 65, 98), + selectedtabIconColor = MyTheme.accent, + selectedTextColor = const Color.fromARGB(255, 255, 255, 255), + unSelectedTextColor = const Color.fromARGB(255, 207, 207, 207), + selectedIconColor = const Color.fromARGB(255, 215, 215, 215), + unSelectedIconColor = const Color.fromARGB(255, 255, 255, 255), + dividerColor = const Color.fromARGB(255, 64, 64, 64), + hoverColor = Colors.black26; } From 66b145912684f86ab08f6f2e4a5095357c2d9ecf Mon Sep 17 00:00:00 2001 From: csf Date: Wed, 24 Aug 2022 20:17:51 +0800 Subject: [PATCH 0274/2015] rename tabbar -> tab --- flutter/lib/desktop/pages/desktop_tab_page.dart | 8 ++++---- flutter/lib/desktop/widgets/tabbar_widget.dart | 14 +++++++------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_tab_page.dart b/flutter/lib/desktop/pages/desktop_tab_page.dart index 5cc86f0ca..2504c699f 100644 --- a/flutter/lib/desktop/pages/desktop_tab_page.dart +++ b/flutter/lib/desktop/pages/desktop_tab_page.dart @@ -14,12 +14,12 @@ class DesktopTabPage extends StatefulWidget { } class _DesktopTabPageState extends State { - final tabBarController = DesktopTabBarController(); + final tabController = DesktopTabController(); @override void initState() { super.initState(); - tabBarController.state.value.tabs.add(TabInfo( + tabController.state.value.tabs.add(TabInfo( key: kTabLabelHomePage, label: kTabLabelHomePage, selectedIcon: Icons.home_sharp, @@ -38,7 +38,7 @@ class _DesktopTabPageState extends State { child: Scaffold( backgroundColor: MyTheme.color(context).bg, body: DesktopTab( - controller: tabBarController, + controller: tabController, theme: dark ? TarBarTheme.dark() : TarBarTheme.light(), isMainWindow: true, tail: ActionIcon( @@ -54,7 +54,7 @@ class _DesktopTabPageState extends State { } void onAddSetting() { - tabBarController.add(TabInfo( + tabController.add(TabInfo( key: kTabLabelSettingPage, label: kTabLabelSettingPage, selectedIcon: Icons.build_sharp, diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 7544c6ef0..77757dd04 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -32,14 +32,14 @@ class TabInfo { required this.page}); } -class DesktopTabBarState { +class DesktopTabState { final List tabs = []; final ScrollPosController scrollController = ScrollPosController(itemCount: 0); final PageController pageController = PageController(); int selected = 0; - DesktopTabBarState() { + DesktopTabState() { scrollController.itemCount = tabs.length; // TODO test // WidgetsBinding.instance.addPostFrameCallback((_) { @@ -49,8 +49,8 @@ class DesktopTabBarState { } } -class DesktopTabBarController { - final state = DesktopTabBarState().obs; +class DesktopTabController { + final state = DesktopTabState().obs; void add(TabInfo tab) { if (!isDesktop) return; @@ -112,7 +112,7 @@ class DesktopTab extends StatelessWidget { final Widget Function(Widget pageView)? pageViewBuilder; final Widget? tail; - final DesktopTabBarController controller; + final DesktopTabController controller; late final state = controller.state; DesktopTab( @@ -323,8 +323,8 @@ class WindowActionPanel extends StatelessWidget { // ignore: must_be_immutable class _ListView extends StatelessWidget { - final DesktopTabBarController controller; - late final Rx state; + final DesktopTabController controller; + late final Rx state; final Function(String key)? onTabClose; final TarBarTheme theme; From cc3c725f389a786ee79a66de50455e3997840f17 Mon Sep 17 00:00:00 2001 From: csf Date: Wed, 24 Aug 2022 20:56:42 +0800 Subject: [PATCH 0275/2015] refactor DesktopTab impl for connection_tab_page.dart --- flutter/lib/common.dart | 3 +- .../desktop/pages/connection_tab_page.dart | 111 ++++++++++-------- .../lib/desktop/widgets/tabbar_widget.dart | 47 +++----- 3 files changed, 83 insertions(+), 78 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index ece2ec797..9944d6884 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -206,7 +206,8 @@ closeConnection({String? id}) { if (isAndroid || isIOS) { Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/")); } else { - DesktopTabBar.close(id); + final controller = Get.find(); + controller.closeBy(id); } } diff --git a/flutter/lib/desktop/pages/connection_tab_page.dart b/flutter/lib/desktop/pages/connection_tab_page.dart index 407feddea..8f9d4f349 100644 --- a/flutter/lib/desktop/pages/connection_tab_page.dart +++ b/flutter/lib/desktop/pages/connection_tab_page.dart @@ -21,28 +21,36 @@ class ConnectionTabPage extends StatefulWidget { } class _ConnectionTabPageState extends State { - // refactor List when using multi-tab - // this singleton is only for test - RxList tabs = RxList.empty(growable: true); + final tabController = Get.put(DesktopTabController()); static final Rx _fullscreenID = "".obs; - final IconData selectedIcon = Icons.desktop_windows_sharp; - final IconData unselectedIcon = Icons.desktop_windows_outlined; + static final IconData selectedIcon = Icons.desktop_windows_sharp; + static final IconData unselectedIcon = Icons.desktop_windows_outlined; var connectionMap = RxList.empty(growable: true); _ConnectionTabPageState(Map params) { if (params['id'] != null) { - tabs.add(TabInfo( + tabController.state.value.tabs.add(TabInfo( key: params['id'], label: params['id'], selectedIcon: selectedIcon, - unselectedIcon: unselectedIcon)); + unselectedIcon: unselectedIcon, + closable: false, + page: RemotePage( + id: params['id'], + tabBarHeight: + _fullscreenID.value.isNotEmpty ? 0 : kDesktopRemoteTabBarHeight, + fullscreenID: _fullscreenID, + ))); } } @override void initState() { super.initState(); + + tabController.onRemove = (_, id) => onRemoveId(id); + rustDeskWinManager.setMethodHandler((call, fromWindowId) async { print( "call ${call.method} with args ${call.arguments} from window ${fromWindowId}"); @@ -51,18 +59,23 @@ class _ConnectionTabPageState extends State { final args = jsonDecode(call.arguments); final id = args['id']; window_on_top(windowId()); - DesktopTabBar.onAdd( - tabs, - TabInfo( - key: id, - label: id, - selectedIcon: selectedIcon, - unselectedIcon: unselectedIcon)); + tabController.add(TabInfo( + key: id, + label: id, + selectedIcon: selectedIcon, + unselectedIcon: unselectedIcon, + closable: false, + page: RemotePage( + id: id, + tabBarHeight: _fullscreenID.value.isNotEmpty + ? 0 + : kDesktopRemoteTabBarHeight, + fullscreenID: _fullscreenID, + ))); } else if (call.method == "onDestroy") { - print( - "executing onDestroy hook, closing ${tabs.map((tab) => tab.label).toList()}"); - tabs.forEach((tab) { - final tag = '${tab.label}'; + tabController.state.value.tabs.forEach((tab) { + print("executing onDestroy hook, closing ${tab.label}}"); + final tag = tab.label; ffi(tag).close().then((_) { Get.delete(tag: tag); }); @@ -74,49 +87,29 @@ class _ConnectionTabPageState extends State { @override Widget build(BuildContext context) { + final theme = isDarkTheme() ? TarBarTheme.dark() : TarBarTheme.light(); return SubWindowDragToResizeArea( windowId: windowId(), child: Container( decoration: BoxDecoration( border: Border.all(color: MyTheme.color(context).border!)), child: Scaffold( - backgroundColor: MyTheme.color(context).bg, - body: Column( - children: [ - Obx(() => Visibility( - visible: _fullscreenID.value.isEmpty, - child: DesktopTabBar( - tabs: tabs, - onTabClose: onRemoveId, - dark: isDarkTheme(), - mainTab: false, - ))), - Expanded(child: Obx(() { - WindowController.fromWindowId(windowId()) - .setFullscreen(_fullscreenID.value.isNotEmpty); - return PageView( - controller: DesktopTabBar.controller.value, - children: tabs - .map((tab) => RemotePage( - key: ValueKey(tab.label), - id: tab.label, - tabBarHeight: _fullscreenID.value.isNotEmpty - ? 0 - : kDesktopRemoteTabBarHeight, - fullscreenID: _fullscreenID, - )) //RemotePage(key: ValueKey(e), id: e)) - .toList()); - })), - ], - ), - ), + backgroundColor: MyTheme.color(context).bg, + body: DesktopTab( + controller: tabController, + theme: theme, + isMainWindow: false, + tail: AddButton( + theme: theme, + ).paddingOnly(left: 10), + )), ), ); } void onRemoveId(String id) { ffi(id).close(); - if (tabs.length == 0) { + if (tabController.state.value.tabs.length == 0) { WindowController.fromWindowId(windowId()).close(); } } @@ -125,3 +118,23 @@ class _ConnectionTabPageState extends State { return widget.params["windowId"]; } } + +class AddButton extends StatelessWidget { + late final TarBarTheme theme; + + AddButton({ + Key? key, + required this.theme, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return ActionIcon( + message: 'New Connection', + icon: IconFont.add, + theme: theme, + onTap: () => + rustDeskWinManager.call(WindowType.Main, "main_window_on_top", ""), + is_close: false); + } +} diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 77757dd04..48116b374 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -5,7 +5,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/main.dart'; -import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:get/get.dart'; import 'package:window_manager/window_manager.dart'; import 'package:scroll_pos/scroll_pos.dart'; @@ -52,6 +51,9 @@ class DesktopTabState { class DesktopTabController { final state = DesktopTabState().obs; + /// index, key + Function(int, String)? onRemove; + void add(TabInfo tab) { if (!isDesktop) return; final index = state.value.tabs.indexWhere((e) => e.key == tab.key); @@ -75,8 +77,9 @@ class DesktopTabController { void remove(int index) { if (!isDesktop) return; - if (index < 0) return; final len = state.value.tabs.length; + if (index < 0 || index > len - 1) return; + final key = state.value.tabs[index].key; final currentSelected = state.value.selected; int toIndex = 0; if (index == len - 1) { @@ -87,6 +90,7 @@ class DesktopTabController { state.value.tabs.removeAt(index); state.value.scrollController.itemCount = state.value.tabs.length; jumpTo(toIndex); + onRemove?.call(index, key); } void jumpTo(int index) { @@ -98,6 +102,19 @@ class DesktopTabController { // onSelected callback } + + void closeBy(String? key) { + if (!isDesktop) return; + assert(onRemove != null); + if (key == null) { + if (state.value.selected < state.value.tabs.length) { + remove(state.value.selected); + } + } else { + state.value.tabs.indexWhere((tab) => tab.key == key); + remove(state.value.selected); + } + } } class DesktopTab extends StatelessWidget { @@ -201,12 +218,6 @@ class DesktopTab extends StatelessWidget { theme: theme, )), ), - Offstage( - offstage: isMainWindow, - child: _AddButton( - theme: theme, - ).paddingOnly(left: 10), - ), ], ), ), @@ -447,26 +458,6 @@ class _Tab extends StatelessWidget { } } -class _AddButton extends StatelessWidget { - late final TarBarTheme theme; - - _AddButton({ - Key? key, - required this.theme, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return ActionIcon( - message: 'New Connection', - icon: IconFont.add, - theme: theme, - onTap: () => - rustDeskWinManager.call(WindowType.Main, "main_window_on_top", ""), - is_close: false); - } -} - class _CloseButton extends StatelessWidget { final bool visiable; final bool tabSelected; From 4f4ac672287f34253b4e88a0c465bb24e98e3d19 Mon Sep 17 00:00:00 2001 From: csf Date: Wed, 24 Aug 2022 21:09:18 +0800 Subject: [PATCH 0276/2015] refactor DesktopTab impl for file_manager_tab_page.dart --- .../desktop/pages/connection_tab_page.dart | 21 ----- .../desktop/pages/file_manager_tab_page.dart | 81 ++++++++----------- .../lib/desktop/widgets/tabbar_widget.dart | 27 +++++-- 3 files changed, 55 insertions(+), 74 deletions(-) diff --git a/flutter/lib/desktop/pages/connection_tab_page.dart b/flutter/lib/desktop/pages/connection_tab_page.dart index 8f9d4f349..cf221c4d0 100644 --- a/flutter/lib/desktop/pages/connection_tab_page.dart +++ b/flutter/lib/desktop/pages/connection_tab_page.dart @@ -35,7 +35,6 @@ class _ConnectionTabPageState extends State { label: params['id'], selectedIcon: selectedIcon, unselectedIcon: unselectedIcon, - closable: false, page: RemotePage( id: params['id'], tabBarHeight: @@ -118,23 +117,3 @@ class _ConnectionTabPageState extends State { return widget.params["windowId"]; } } - -class AddButton extends StatelessWidget { - late final TarBarTheme theme; - - AddButton({ - Key? key, - required this.theme, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return ActionIcon( - message: 'New Connection', - icon: IconFont.add, - theme: theme, - onTap: () => - rustDeskWinManager.call(WindowType.Main, "main_window_on_top", ""), - is_close: false); - } -} diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index 78f0842ad..7ae8e36b3 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -20,25 +20,26 @@ class FileManagerTabPage extends StatefulWidget { } class _FileManagerTabPageState extends State { - // refactor List when using multi-tab - // this singleton is only for test - RxList tabs = List.empty(growable: true).obs; - final IconData selectedIcon = Icons.file_copy_sharp; - final IconData unselectedIcon = Icons.file_copy_outlined; + final tabController = Get.put(DesktopTabController()); + + static final IconData selectedIcon = Icons.file_copy_sharp; + static final IconData unselectedIcon = Icons.file_copy_outlined; _FileManagerTabPageState(Map params) { - if (params['id'] != null) { - tabs.add(TabInfo( - key: params['id'], - label: params['id'], - selectedIcon: selectedIcon, - unselectedIcon: unselectedIcon)); - } + tabController.state.value.tabs.add(TabInfo( + key: params['id'], + label: params['id'], + selectedIcon: selectedIcon, + unselectedIcon: unselectedIcon, + page: FileManagerPage(id: params['id']))); } @override void initState() { super.initState(); + + tabController.onRemove = (_, id) => onRemoveId(id); + rustDeskWinManager.setMethodHandler((call, fromWindowId) async { print( "call ${call.method} with args ${call.arguments} from window ${fromWindowId}"); @@ -47,18 +48,16 @@ class _FileManagerTabPageState extends State { final args = jsonDecode(call.arguments); final id = args['id']; window_on_top(windowId()); - DesktopTabBar.onAdd( - tabs, - TabInfo( - key: id, - label: id, - selectedIcon: selectedIcon, - unselectedIcon: unselectedIcon)); + tabController.add(TabInfo( + key: id, + label: id, + selectedIcon: selectedIcon, + unselectedIcon: unselectedIcon, + page: FileManagerPage(id: id))); } else if (call.method == "onDestroy") { - print( - "executing onDestroy hook, closing ${tabs.map((tab) => tab.label).toList()}"); - tabs.forEach((tab) { - final tag = 'ft_${tab.label}'; + tabController.state.value.tabs.forEach((tab) { + print("executing onDestroy hook, closing ${tab.label}}"); + final tag = tab.label; ffi(tag).close().then((_) { Get.delete(tag: tag); }); @@ -70,43 +69,29 @@ class _FileManagerTabPageState extends State { @override Widget build(BuildContext context) { + final theme = isDarkTheme() ? TarBarTheme.dark() : TarBarTheme.light(); return SubWindowDragToResizeArea( windowId: windowId(), child: Container( decoration: BoxDecoration( border: Border.all(color: MyTheme.color(context).border!)), child: Scaffold( - backgroundColor: MyTheme.color(context).bg, - body: Column( - children: [ - DesktopTabBar( - tabs: tabs, - onTabClose: onRemoveId, - dark: isDarkTheme(), - mainTab: false, - ), - Expanded( - child: Obx( - () => PageView( - controller: DesktopTabBar.controller.value, - children: tabs - .map((tab) => FileManagerPage( - key: ValueKey(tab.label), - id: tab - .label)) //RemotePage(key: ValueKey(e), id: e)) - .toList()), - ), - ) - ], - ), - ), + backgroundColor: MyTheme.color(context).bg, + body: DesktopTab( + controller: tabController, + theme: theme, + isMainWindow: false, + tail: AddButton( + theme: theme, + ).paddingOnly(left: 10), + )), ), ); } void onRemoveId(String id) { ffi("ft_$id").close(); - if (tabs.length == 0) { + if (tabController.state.value.tabs.length == 0) { WindowController.fromWindowId(windowId()).close(); } } diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 48116b374..8aa8377c6 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -9,6 +9,8 @@ import 'package:get/get.dart'; import 'package:window_manager/window_manager.dart'; import 'package:scroll_pos/scroll_pos.dart'; +import '../../utils/multi_window_manager.dart'; + const double _kTabBarHeight = kDesktopRemoteTabBarHeight; const double _kIconSize = 18; const double _kDividerIndent = 10; @@ -40,11 +42,6 @@ class DesktopTabState { DesktopTabState() { scrollController.itemCount = tabs.length; - // TODO test - // WidgetsBinding.instance.addPostFrameCallback((_) { - // scrollController.scrollToItem(selected, - // center: true, animate: true); - // }); } } @@ -533,6 +530,26 @@ class ActionIcon extends StatelessWidget { } } +class AddButton extends StatelessWidget { + late final TarBarTheme theme; + + AddButton({ + Key? key, + required this.theme, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return ActionIcon( + message: 'New Connection', + icon: IconFont.add, + theme: theme, + onTap: () => + rustDeskWinManager.call(WindowType.Main, "main_window_on_top", ""), + is_close: false); + } +} + class TarBarTheme { final Color unSelectedtabIconColor; final Color selectedtabIconColor; From 67b40b2cc7bc97be82034cfc81454a05fa300d75 Mon Sep 17 00:00:00 2001 From: csf Date: Wed, 24 Aug 2022 21:20:50 +0800 Subject: [PATCH 0277/2015] fix full screen --- .../desktop/pages/connection_tab_page.dart | 22 ++++++++----- .../lib/desktop/widgets/tabbar_widget.dart | 32 +++++++++++-------- 2 files changed, 32 insertions(+), 22 deletions(-) diff --git a/flutter/lib/desktop/pages/connection_tab_page.dart b/flutter/lib/desktop/pages/connection_tab_page.dart index cf221c4d0..66f342919 100644 --- a/flutter/lib/desktop/pages/connection_tab_page.dart +++ b/flutter/lib/desktop/pages/connection_tab_page.dart @@ -94,14 +94,20 @@ class _ConnectionTabPageState extends State { border: Border.all(color: MyTheme.color(context).border!)), child: Scaffold( backgroundColor: MyTheme.color(context).bg, - body: DesktopTab( - controller: tabController, - theme: theme, - isMainWindow: false, - tail: AddButton( - theme: theme, - ).paddingOnly(left: 10), - )), + body: Obx(() => DesktopTab( + controller: tabController, + theme: theme, + isMainWindow: false, + showTabBar: _fullscreenID.value.isEmpty, + tail: AddButton( + theme: theme, + ).paddingOnly(left: 10), + pageViewBuilder: (pageView) { + WindowController.fromWindowId(windowId()) + .setFullscreen(_fullscreenID.value.isNotEmpty); + return pageView; + }, + ))), ), ); } diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 8aa8377c6..afac932ec 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -118,6 +118,7 @@ class DesktopTab extends StatelessWidget { final Function(String)? onTabClose; final TarBarTheme theme; final bool isMainWindow; + final bool showTabBar; final bool showLogo; final bool showTitle; final bool showMinimize; @@ -134,6 +135,7 @@ class DesktopTab extends StatelessWidget { required this.isMainWindow, this.theme = const TarBarTheme.light(), this.onTabClose, + this.showTabBar = true, this.showLogo = true, this.showTitle = true, this.showMinimize = true, @@ -145,21 +147,23 @@ class DesktopTab extends StatelessWidget { @override Widget build(BuildContext context) { return Column(children: [ - Container( - height: _kTabBarHeight, - child: Column( - children: [ - Container( - height: _kTabBarHeight - 1, - child: _buildBar(), + Offstage( + offstage: !showTabBar, + child: Container( + height: _kTabBarHeight, + child: Column( + children: [ + Container( + height: _kTabBarHeight - 1, + child: _buildBar(), + ), + Divider( + height: 1, + thickness: 1, + ), + ], ), - Divider( - height: 1, - thickness: 1, - ), - ], - ), - ), + )), Expanded( child: pageViewBuilder != null ? pageViewBuilder!(_buildPageView()) From e78d44935a35643495f1b21db13cc7b27d7eacfc Mon Sep 17 00:00:00 2001 From: csf Date: Wed, 24 Aug 2022 21:52:21 +0800 Subject: [PATCH 0278/2015] refactor DesktopTab impl for cm --- flutter/lib/desktop/pages/server_page.dart | 82 ++++++++----------- .../lib/desktop/widgets/tabbar_widget.dart | 6 +- flutter/lib/models/chat_model.dart | 1 + flutter/lib/models/server_model.dart | 46 ++++++----- 4 files changed, 64 insertions(+), 71 deletions(-) diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index bfcc28382..d96efc710 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -79,6 +79,8 @@ class ConnectionManagerState extends State { @override void initState() { gFFI.serverModel.updateClientState(); + gFFI.serverModel.tabController.onSelected = (index) => + gFFI.chatModel.changeCurrentID(gFFI.serverModel.clients[index].id); // test // gFFI.serverModel.clients.forEach((client) { // DesktopTabBar.onAdd( @@ -103,38 +105,20 @@ class ConnectionManagerState extends State { ), ], ) - : Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - height: kTextTabBarHeight, - child: Obx(() => DesktopTabBar( - dark: isDarkTheme(), - mainTab: true, - tabs: serverModel.tabs, - showTitle: false, - showMaximize: false, - showMinimize: false, - onSelected: (index) => gFFI.chatModel - .changeCurrentID(serverModel.clients[index].id), - )), - ), - Expanded( - child: Row(children: [ - Expanded( - child: PageView( - controller: DesktopTabBar.controller.value, - children: serverModel.clients - .map((client) => buildConnectionCard(client)) - .toList(growable: false))), + : DesktopTab( + theme: isDarkTheme() ? TarBarTheme.dark() : TarBarTheme.light(), + showTitle: false, + showMaximize: false, + showMinimize: false, + controller: serverModel.tabController, + isMainWindow: true, + pageViewBuilder: (pageView) => Row(children: [ + Expanded(child: pageView), Consumer( builder: (_, model, child) => model.isShowChatPage ? Expanded(child: Scaffold(body: ChatPage())) : Offstage()) - ]), - ) - ], - ); + ])); } Widget buildTitleBar(Widget middle) { @@ -156,23 +140,6 @@ class ConnectionManagerState extends State { ); } - Widget buildConnectionCard(Client client) { - return Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - key: ValueKey(client.id), - children: [ - _CmHeader(client: client), - client.isFileTransfer ? Offstage() : _PrivilegeBoard(client: client), - Expanded( - child: Align( - alignment: Alignment.bottomCenter, - child: _CmControlPanel(client: client), - )) - ], - ).paddingSymmetric(vertical: 8.0, horizontal: 8.0); - } - Widget buildTab(Client client) { return Tab( child: Row( @@ -191,6 +158,23 @@ class ConnectionManagerState extends State { } } +Widget buildConnectionCard(Client client) { + return Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + key: ValueKey(client.id), + children: [ + _CmHeader(client: client), + client.isFileTransfer ? Offstage() : _PrivilegeBoard(client: client), + Expanded( + child: Align( + alignment: Alignment.bottomCenter, + child: _CmControlPanel(client: client), + )) + ], + ).paddingSymmetric(vertical: 8.0, horizontal: 8.0); +} + class _AppIcon extends StatelessWidget { const _AppIcon({Key? key}) : super(key: key); @@ -421,9 +405,11 @@ class _CmControlPanel extends StatelessWidget { @override Widget build(BuildContext context) { - return client.authorized - ? buildAuthorized(context) - : buildUnAuthorized(context); + return Consumer(builder: (_, model, child) { + return client.authorized + ? buildAuthorized(context) + : buildUnAuthorized(context); + }); } buildAuthorized(BuildContext context) { diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index afac932ec..3b88deae6 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -51,6 +51,8 @@ class DesktopTabController { /// index, key Function(int, String)? onRemove; + Function(int)? onSelected; + void add(TabInfo tab) { if (!isDesktop) return; final index = state.value.tabs.indexWhere((e) => e.key == tab.key); @@ -96,8 +98,7 @@ class DesktopTabController { val.pageController.jumpToPage(index); val.scrollController.scrollToItem(index, center: true, animate: true); }); - - // onSelected callback + onSelected?.call(index); } void closeBy(String? key) { @@ -172,7 +173,6 @@ class DesktopTab extends StatelessWidget { } Widget _buildPageView() { - debugPrint("_buildPageView: ${state.value.tabs.length}"); return Obx(() => PageView( controller: state.value.pageController, children: diff --git a/flutter/lib/models/chat_model.dart b/flutter/lib/models/chat_model.dart index a42b10ee2..de949c782 100644 --- a/flutter/lib/models/chat_model.dart +++ b/flutter/lib/models/chat_model.dart @@ -200,6 +200,7 @@ class ChatModel with ChangeNotifier { if (!_isShowChatPage) { toggleCMChatPage(id); } + _ffi.target?.serverModel.jumpTo(id); late final chatUser; if (id == clientModeID) { diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index dec13f245..fa7f15e54 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -4,10 +4,10 @@ import 'dart:io'; 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:wakelock/wakelock.dart'; import '../common.dart'; +import '../desktop/pages/server_page.dart' as Desktop; import '../desktop/widgets/tabbar_widget.dart'; import '../mobile/pages/server_page.dart'; import 'model.dart'; @@ -32,7 +32,7 @@ class ServerModel with ChangeNotifier { late final TextEditingController _serverId; final _serverPasswd = TextEditingController(text: ""); - RxList tabs = RxList.empty(growable: true); + final tabController = DesktopTabController(); List _clients = []; @@ -352,16 +352,15 @@ class ServerModel with ChangeNotifier { exit(0); } _clients.clear(); - tabs.clear(); + tabController.state.value.tabs.clear(); for (var clientJson in clientsJson) { final client = Client.fromJson(clientJson); _clients.add(client); - DesktopTabBar.onAdd( - tabs, - TabInfo( - key: client.id.toString(), - label: client.name, - closable: false)); + tabController.add(TabInfo( + key: client.id.toString(), + label: client.name, + closable: false, + page: Desktop.buildConnectionCard(client))); } notifyListeners(); } catch (e) { @@ -376,10 +375,11 @@ class ServerModel with ChangeNotifier { return; } _clients.add(client); - DesktopTabBar.onAdd( - tabs, - TabInfo( - key: client.id.toString(), label: client.name, closable: false)); + tabController.add(TabInfo( + key: client.id.toString(), + label: client.name, + closable: false, + page: Desktop.buildConnectionCard(client))); scrollToBottom(); notifyListeners(); if (isAndroid) showLoginDialog(client); @@ -456,7 +456,7 @@ class ServerModel with ChangeNotifier { bind.cmLoginRes(connId: client.id, res: res); parent.target?.invokeMethod("cancel_notification", client.id); final index = _clients.indexOf(client); - DesktopTabBar.remove(tabs, index); + tabController.remove(index); _clients.remove(client); } } @@ -471,10 +471,11 @@ class ServerModel with ChangeNotifier { } else { _clients[index].authorized = true; } - DesktopTabBar.onAdd( - tabs, - TabInfo( - key: client.id.toString(), label: client.name, closable: false)); + tabController.add(TabInfo( + key: client.id.toString(), + label: client.name, + closable: false, + page: Desktop.buildConnectionCard(client))); scrollToBottom(); notifyListeners(); } catch (e) {} @@ -486,7 +487,7 @@ class ServerModel with ChangeNotifier { if (_clients.any((c) => c.id == id)) { final index = _clients.indexWhere((client) => client.id == id); _clients.removeAt(index); - DesktopTabBar.remove(tabs, index); + tabController.remove(index); parent.target?.dialogManager.dismissByTag(getLoginDialogTag(id)); parent.target?.invokeMethod("cancel_notification", id); } @@ -501,7 +502,12 @@ class ServerModel with ChangeNotifier { bind.cmCloseConnection(connId: client.id); }); _clients.clear(); - tabs.clear(); + tabController.state.value.tabs.clear(); + } + + void jumpTo(int id) { + final index = _clients.indexWhere((client) => client.id == id); + tabController.jumpTo(index); } } From 5497a5982385eed1cd577d1311b02efa3b10169a Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 23 Aug 2022 19:47:56 +0800 Subject: [PATCH 0279/2015] keep text scale factor (except android) Signed-off-by: 21pages --- flutter/lib/main.dart | 58 ++++++++++++++++++++++++++++--------------- 1 file changed, 38 insertions(+), 20 deletions(-) diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index a1bebbea7..9682f19d1 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -112,12 +112,14 @@ void runRemoteScreen(Map argument) async { navigatorObservers: [ // FirebaseAnalyticsObserver(analytics: analytics), ], + builder: _keepScaleBuilder(), )); } void runFileTransferScreen(Map argument) async { await initEnv(kAppTypeDesktopFileTransfer); - runApp(GetMaterialApp( + runApp( + GetMaterialApp( navigatorKey: globalKey, debugShowCheckedModeBanner: false, title: 'RustDesk - File Transfer', @@ -125,7 +127,10 @@ void runFileTransferScreen(Map argument) async { home: DesktopFileTransferScreen(params: argument), navigatorObservers: [ // FirebaseAnalyticsObserver(analytics: analytics), - ])); + ], + builder: _keepScaleBuilder(), + ), + ); } void runConnectionManagerScreen() async { @@ -142,7 +147,8 @@ void runConnectionManagerScreen() async { runApp(GetMaterialApp( debugShowCheckedModeBanner: false, theme: getCurrentTheme(), - home: DesktopServerPage())); + home: DesktopServerPage(), + builder: _keepScaleBuilder())); } WindowOptions getHiddenTitleBarWindowOptions(Size size) { @@ -171,23 +177,35 @@ class App extends StatelessWidget { ChangeNotifierProvider.value(value: gFFI.userModel), ], child: GetMaterialApp( - navigatorKey: globalKey, - debugShowCheckedModeBanner: false, - title: 'RustDesk', - theme: getCurrentTheme(), - home: isDesktop - ? DesktopTabPage() - : !isAndroid - ? WebHomePage() - : HomePage(), - navigatorObservers: [ - // FirebaseAnalyticsObserver(analytics: analytics), - ], - builder: isAndroid - ? (_, child) => AccessibilityListener( - child: child, - ) - : null), + navigatorKey: globalKey, + debugShowCheckedModeBanner: false, + title: 'RustDesk', + theme: getCurrentTheme(), + home: isDesktop + ? DesktopTabPage() + : !isAndroid + ? WebHomePage() + : HomePage(), + navigatorObservers: [ + // FirebaseAnalyticsObserver(analytics: analytics), + ], + builder: isAndroid + ? (_, child) => AccessibilityListener( + child: child, + ) + : _keepScaleBuilder(), + ), ); } } + +_keepScaleBuilder() { + return (BuildContext context, Widget? child) { + return MediaQuery( + data: MediaQuery.of(context).copyWith( + textScaleFactor: 1.0, + ), + child: child ?? Container(), + ); + }; +} From 16c1813df1a019b9928bb2d44828145b30d039b5 Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 23 Aug 2022 19:49:11 +0800 Subject: [PATCH 0280/2015] adjust about setting tab position Signed-off-by: 21pages --- .../desktop/pages/desktop_setting_page.dart | 41 +++++++++---------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 7c87d7cb0..4f86974f1 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -26,11 +26,10 @@ const double _kContentFontSize = 15; const Color _accentColor = MyTheme.accent; class _TabInfo { - late final int index; late final String label; late final IconData unselected; late final IconData selected; - _TabInfo(this.index, this.label, this.unselected, this.selected); + _TabInfo(this.label, this.unselected, this.selected); } class DesktopSettingPage extends StatefulWidget { @@ -43,17 +42,15 @@ class DesktopSettingPage extends StatefulWidget { class _DesktopSettingPageState extends State with TickerProviderStateMixin, AutomaticKeepAliveClientMixin { final List<_TabInfo> _setting_tabs = <_TabInfo>[ - _TabInfo( - 0, 'User Interface', Icons.language_outlined, Icons.language_sharp), - _TabInfo(1, 'Security', Icons.enhanced_encryption_outlined, + _TabInfo('User Interface', Icons.language_outlined, Icons.language_sharp), + _TabInfo('Security', Icons.enhanced_encryption_outlined, Icons.enhanced_encryption_sharp), - _TabInfo(2, 'Display', Icons.desktop_windows_outlined, - Icons.desktop_windows_sharp), - _TabInfo(3, 'Audio', Icons.volume_up_outlined, Icons.volume_up_sharp), - _TabInfo(4, 'Connection', Icons.link_outlined, Icons.link_sharp), + _TabInfo( + 'Display', Icons.desktop_windows_outlined, Icons.desktop_windows_sharp), + _TabInfo('Audio', Icons.volume_up_outlined, Icons.volume_up_sharp), + _TabInfo('Connection', Icons.link_outlined, Icons.link_sharp), + _TabInfo('About RustDesk', Icons.info_outline, Icons.info_sharp) ]; - final _TabInfo _about_tab = - _TabInfo(5, 'About RustDesk', Icons.info_outline, Icons.info_sharp); late PageController controller; RxInt _selectedIndex = 0.obs; @@ -80,10 +77,6 @@ class _DesktopSettingPageState extends State children: [ _header(), Flexible(child: _listView(tabs: _setting_tabs)), - _listItem(tab: _about_tab), - SizedBox( - height: 120, - ) ], ), ), @@ -131,22 +124,26 @@ class _DesktopSettingPageState extends State Widget _listView({required List<_TabInfo> tabs}) { return ListView( - children: tabs.map((tab) => _listItem(tab: tab)).toList(), + children: tabs + .asMap() + .entries + .map((tab) => _listItem(tab: tab.value, index: tab.key)) + .toList(), ); } - Widget _listItem({required _TabInfo tab}) { + Widget _listItem({required _TabInfo tab, required int index}) { return Obx(() { - bool selected = tab.index == _selectedIndex.value; + bool selected = index == _selectedIndex.value; return Container( width: _kTabWidth, height: _kTabHeight, child: InkWell( onTap: () { - if (_selectedIndex.value != tab.index) { - controller.jumpToPage(tab.index); + if (_selectedIndex.value != index) { + controller.jumpToPage(index); } - _selectedIndex.value = tab.index; + _selectedIndex.value = index; }, child: Row(children: [ Container( @@ -665,7 +662,7 @@ class _AboutState extends State<_About> { final version = data['version'].toString(); final linkStyle = TextStyle(decoration: TextDecoration.underline); return ListView(children: [ - _Card(title: "About Rustdesk", children: [ + _Card(title: "About RustDesk", children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ From 7c9f799f05e4668b91d0453cdaea8bb491a4a2bd Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 23 Aug 2022 19:55:58 +0800 Subject: [PATCH 0281/2015] optimize id input Signed-off-by: 21pages --- .../lib/desktop/pages/connection_page.dart | 72 +++++++++++-------- 1 file changed, 43 insertions(+), 29 deletions(-) diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 08f334c4d..0b407d227 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -186,9 +186,14 @@ class _ConnectionPageState extends State { RxBool ftPressed = false.obs; RxBool connHover = false.obs; RxBool connPressed = false.obs; + RxBool inputFocused = false.obs; + FocusNode focusNode = FocusNode(); + focusNode.addListener(() { + inputFocused.value = focusNode.hasFocus; + }); var w = Container( width: 320 + 20 * 2, - padding: EdgeInsets.only(left: 20, right: 20, bottom: 22, top: 30), + padding: EdgeInsets.only(left: 20, right: 20, bottom: 22, top: 24), decoration: BoxDecoration( color: MyTheme.color(context).bg, borderRadius: const BorderRadius.all(Radius.circular(13)), @@ -197,36 +202,45 @@ class _ConnectionPageState extends State { child: Column( children: [ Row( - children: [ + children: [ + Text( + translate('Control Remote Desktop'), + style: TextStyle(fontSize: 19, height: 1), + ), + ], + ).marginOnly(bottom: 15), + Row( + children: [ Expanded( - child: Container( - child: TextField( - autocorrect: false, - enableSuggestions: false, - keyboardType: TextInputType.visiblePassword, - style: TextStyle( - fontFamily: 'WorkSans', - fontSize: 22, - ), - decoration: InputDecoration( - labelText: translate('Control Remote Desktop'), - border: - OutlineInputBorder(borderRadius: BorderRadius.zero), - helperStyle: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - ), - labelStyle: TextStyle( - fontWeight: FontWeight.w500, - fontSize: 26, - letterSpacing: 0.2, - ), - ), - controller: _idController, - onSubmitted: (s) { - onConnect(); - }, + child: TextField( + autocorrect: false, + enableSuggestions: false, + keyboardType: TextInputType.visiblePassword, + style: TextStyle( + fontFamily: 'WorkSans', + fontSize: 22, + height: 1, ), + decoration: InputDecoration( + hintText: translate('Enter Remote ID'), + hintStyle: TextStyle( + color: MyTheme.color(context).placeholder), + border: OutlineInputBorder( + borderRadius: BorderRadius.zero, + borderSide: BorderSide( + color: MyTheme.color(context).placeholder!)), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.zero, + borderSide: + BorderSide(color: MyTheme.button, width: 3), + ), + isDense: true, + contentPadding: + EdgeInsets.symmetric(horizontal: 10, vertical: 12)), + controller: _idController, + onSubmitted: (s) { + onConnect(); + }, ), ), ], From 92f1f17ca2b95fe11b9e1788333cab6de821884b Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 24 Aug 2022 23:22:50 +0800 Subject: [PATCH 0282/2015] flutter_desktop: fix sciter lan peers Signed-off-by: fufesou --- src/flutter_ffi.rs | 4 ++-- src/ui.rs | 6 +++++- src/ui_interface.rs | 7 +++---- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 53e3f1ff8..aa46e4faf 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -451,7 +451,7 @@ pub fn main_get_peer(id: String) -> String { } pub fn main_get_lan_peers() -> String { - get_lan_peers() + serde_json::to_string(&get_lan_peers()).unwrap_or_default() } pub fn main_get_connect_status() -> String { @@ -592,7 +592,7 @@ pub fn main_load_lan_peers() { { let data = HashMap::from([ ("name", "load_lan_peers".to_owned()), - ("peers", get_lan_peers()), + ("peers", serde_json::to_string(&get_lan_peers()).unwrap_or_default()), ]); s.add(serde_json::ser::to_string(&data).unwrap_or("".to_owned())); }; diff --git a/src/ui.rs b/src/ui.rs index 1adc7c5ee..78654e9ec 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -500,7 +500,11 @@ impl UI { } fn get_lan_peers(&self) -> String { - get_lan_peers() + let peers = get_lan_peers() + .into_iter() + .map(|(id, peer)| (id, peer.username, peer.hostname, peer.platform)) + .collect::>(); + serde_json::to_string(&peers).unwrap_or_default() } fn get_uuid(&self) -> String { diff --git a/src/ui_interface.rs b/src/ui_interface.rs index f59f96090..a8e3be980 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -584,8 +584,8 @@ pub fn discover() { }); } -pub fn get_lan_peers() -> String { - let peers: Vec<(String, config::PeerInfoSerde)> = config::LanPeers::load() +pub fn get_lan_peers() -> Vec<(String, config::PeerInfoSerde)> { + config::LanPeers::load() .peers .iter() .map(|peer| { @@ -598,8 +598,7 @@ pub fn get_lan_peers() -> String { }, ) }) - .collect(); - serde_json::to_string(&peers).unwrap_or_default() + .collect() } pub fn get_uuid() -> String { From b38c3299d840a91fceace6c832d87e975e960c29 Mon Sep 17 00:00:00 2001 From: Asura Date: Wed, 24 Aug 2022 17:10:34 -0700 Subject: [PATCH 0283/2015] fix: can't find rustdesk.so --- flutter/lib/models/native_model.dart | 2 +- flutter/linux/CMakeLists.txt | 8 +- flutter/linux/main.cc | 3 +- .../Flutter/GeneratedPluginRegistrant.swift | 38 ++ flutter/pubspec.lock | 384 +++++++++--------- src/flutter.rs | 2 +- 6 files changed, 240 insertions(+), 197 deletions(-) create mode 100644 flutter/macos/Flutter/GeneratedPluginRegistrant.swift diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index 55f2d0e79..3359e1c4f 100644 --- a/flutter/lib/models/native_model.dart +++ b/flutter/lib/models/native_model.dart @@ -97,7 +97,7 @@ class PlatformFFI { final dylib = Platform.isAndroid ? DynamicLibrary.open('librustdesk.so') : Platform.isLinux - ? DynamicLibrary.open("/usr/lib/rustdesk/librustdesk.so") + ? DynamicLibrary.open("librustdesk.so") : Platform.isWindows ? DynamicLibrary.open("librustdesk.dll") : Platform.isMacOS diff --git a/flutter/linux/CMakeLists.txt b/flutter/linux/CMakeLists.txt index 9f6d0ce52..8484ca5b6 100644 --- a/flutter/linux/CMakeLists.txt +++ b/flutter/linux/CMakeLists.txt @@ -74,6 +74,8 @@ corrosion_import_crate(MANIFEST_PATH ../../Cargo.toml # [FEATURES ... ] ) +set(BASE_RUSTDESK "librustdesk") + # Define the application target. To change its name, change BINARY_NAME above, # not the value here, or `flutter run` will no longer work. # @@ -91,8 +93,8 @@ apply_standard_settings(${BINARY_NAME}) # Add dependency libraries. Add any application-specific dependencies here. target_link_libraries(${BINARY_NAME} PRIVATE flutter) target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) - -target_link_libraries(${BINARY_NAME} PRIVATE librustdesk) +target_link_libraries(${BINARY_NAME} PRIVATE ${BASE_RUSTDESK}) +# target_link_libraries(${BINARY_NAME} PRIVATE librustdesk) # Run the Flutter tool portions of the build. This must not be removed. add_dependencies(${BINARY_NAME} flutter_assemble) @@ -142,6 +144,8 @@ foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) COMPONENT Runtime) endforeach(bundled_library) +install(FILES $ DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime RENAME librustdesk.so) + # Fully re-copy the assets directory on each build to avoid having stale files # from a previous install. set(FLUTTER_ASSET_DIR_NAME "flutter_assets") diff --git a/flutter/linux/main.cc b/flutter/linux/main.cc index 55fb650bc..e2ad70957 100644 --- a/flutter/linux/main.cc +++ b/flutter/linux/main.cc @@ -1,7 +1,8 @@ #include #include "my_application.h" -#define RUSTDESK_LIB_PATH "/usr/lib/rustdesk/librustdesk.so" +#define RUSTDESK_LIB_PATH "ibrustdesk.so" +// #define RUSTDESK_LIB_PATH "/usr/lib/rustdesk/librustdesk.so" typedef bool (*RustDeskCoreMain)(); bool flutter_rustdesk_core_main() { diff --git a/flutter/macos/Flutter/GeneratedPluginRegistrant.swift b/flutter/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 000000000..fcbc0f6bf --- /dev/null +++ b/flutter/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,38 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import desktop_drop +import desktop_multi_window +import device_info_plus_macos +import firebase_analytics +import firebase_core +import package_info_plus_macos +import path_provider_macos +import screen_retriever +import shared_preferences_macos +import sqflite +import tray_manager +import url_launcher_macos +import wakelock_macos +import window_manager + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + DesktopDropPlugin.register(with: registry.registrar(forPlugin: "DesktopDropPlugin")) + FlutterMultiWindowPlugin.register(with: registry.registrar(forPlugin: "FlutterMultiWindowPlugin")) + DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) + FLTFirebaseAnalyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAnalyticsPlugin")) + FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) + FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin")) + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) + TrayManagerPlugin.register(with: registry.registrar(forPlugin: "TrayManagerPlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) + WakelockMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockMacosPlugin")) + WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) +} diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index fef32af73..dffb58bf8 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -5,231 +5,231 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "46.0.0" after_layout: dependency: transitive description: name: after_layout - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.2.0" analyzer: dependency: transitive description: name: analyzer - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "4.6.0" animations: dependency: transitive description: name: animations - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.3" archive: dependency: transitive description: name: archive - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.3.1" args: dependency: transitive description: name: args - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.3.1" async: dependency: transitive description: name: async - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "2.9.0" + version: "2.8.2" back_button_interceptor: dependency: "direct main" description: name: back_button_interceptor - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "6.0.1" boolean_selector: dependency: transitive description: name: boolean_selector - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.0" build: dependency: transitive description: name: build - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.3.0" build_config: dependency: transitive description: name: build_config - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.1.0" build_daemon: dependency: transitive description: name: build_daemon - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.1.0" build_resolvers: dependency: transitive description: name: build_resolvers - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.9" build_runner: dependency: "direct dev" description: name: build_runner - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.2.0" build_runner_core: dependency: transitive description: name: build_runner_core - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "7.2.3" built_collection: dependency: transitive description: name: built_collection - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "5.1.1" built_value: dependency: transitive description: name: built_value - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "8.4.0" + version: "8.4.1" cached_network_image: dependency: transitive description: name: cached_network_image - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.2.1" cached_network_image_platform_interface: dependency: transitive description: name: cached_network_image_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.0" cached_network_image_web: dependency: transitive description: name: cached_network_image_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.1" characters: dependency: transitive description: name: characters - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "1.2.1" + version: "1.2.0" charcode: dependency: transitive description: name: charcode - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.3.1" checked_yaml: dependency: transitive description: name: checked_yaml - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.1" clock: dependency: transitive description: name: clock - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "1.1.1" + version: "1.1.0" code_builder: dependency: transitive description: name: code_builder - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "4.2.0" collection: dependency: transitive description: name: collection - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.16.0" contextmenu: dependency: "direct main" description: name: contextmenu - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.0" convert: dependency: transitive description: name: convert - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.2" cross_file: dependency: transitive description: name: cross_file - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.3.3+1" crypto: dependency: transitive description: name: crypto - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.2" csslib: dependency: transitive description: name: csslib - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.17.2" cupertino_icons: dependency: "direct main" description: name: cupertino_icons - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.5" dart_style: dependency: transitive description: name: dart_style - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.2.3" dash_chat_2: @@ -245,7 +245,7 @@ packages: dependency: "direct main" description: name: desktop_drop - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.3.3" desktop_multi_window: @@ -261,133 +261,133 @@ packages: dependency: "direct main" description: name: device_info_plus - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "4.1.2" device_info_plus_linux: dependency: transitive description: name: device_info_plus_linux - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.0" device_info_plus_macos: dependency: transitive description: name: device_info_plus_macos - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.0" device_info_plus_platform_interface: dependency: transitive description: name: device_info_plus_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.0" device_info_plus_web: dependency: transitive description: name: device_info_plus_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.0" device_info_plus_windows: dependency: transitive description: name: device_info_plus_windows - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "4.0.0" draggable_float_widget: dependency: "direct main" description: name: draggable_float_widget - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.0.2" event_bus: dependency: transitive description: name: event_bus - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.0" external_path: dependency: "direct main" description: name: external_path - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.1" fake_async: dependency: transitive description: name: fake_async - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "1.3.1" + version: "1.3.0" ffi: dependency: "direct main" description: name: ffi - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.1" file: dependency: transitive description: name: file - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "6.1.2" + version: "6.1.4" firebase_analytics: dependency: "direct main" description: name: firebase_analytics - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "9.3.1" + version: "9.3.2" firebase_analytics_platform_interface: dependency: transitive description: name: firebase_analytics_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "3.3.1" + version: "3.3.2" firebase_analytics_web: dependency: transitive description: name: firebase_analytics_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "0.4.2+1" + version: "0.4.2+2" firebase_core: dependency: transitive description: name: firebase_core - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "1.20.1" + version: "1.21.0" firebase_core_platform_interface: dependency: transitive description: name: firebase_core_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "4.5.0" firebase_core_web: dependency: transitive description: name: firebase_core_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.7.1" fixnum: dependency: transitive description: name: fixnum - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.1" flutter: @@ -399,42 +399,42 @@ packages: dependency: transitive description: name: flutter_blurhash - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.7.0" flutter_breadcrumb: dependency: "direct main" description: name: flutter_breadcrumb - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.1" flutter_cache_manager: dependency: transitive description: name: flutter_cache_manager - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.3.0" flutter_launcher_icons: dependency: "direct dev" description: name: flutter_launcher_icons - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.9.3" flutter_parsed_text: dependency: transitive description: name: flutter_parsed_text - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.2.1" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.7" flutter_rust_bridge: @@ -460,483 +460,483 @@ packages: dependency: "direct dev" description: name: freezed - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.0+1" freezed_annotation: dependency: "direct main" description: name: freezed_annotation - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.0" frontend_server_client: dependency: transitive description: name: frontend_server_client - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.3" get: dependency: "direct main" description: name: get - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "4.6.5" glob: dependency: transitive description: name: glob - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.0" graphs: dependency: transitive description: name: graphs - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.0" html: dependency: transitive description: name: html - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.15.0" http: dependency: "direct main" description: name: http - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.13.5" http_multi_server: dependency: transitive description: name: http_multi_server - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.2.1" http_parser: dependency: transitive description: name: http_parser - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "4.0.1" image: dependency: "direct main" description: name: image - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.2.0" image_picker: dependency: "direct main" description: name: image_picker - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.8.5+3" image_picker_android: dependency: transitive description: name: image_picker_android - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.8.5+2" image_picker_for_web: dependency: transitive description: name: image_picker_for_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.8" image_picker_ios: dependency: transitive description: name: image_picker_ios - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.8.5+6" image_picker_platform_interface: dependency: transitive description: name: image_picker_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.6.1" intl: dependency: transitive description: name: intl - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.17.0" io: dependency: transitive description: name: io - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.3" js: dependency: transitive description: name: js - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.6.4" json_annotation: dependency: transitive description: name: json_annotation - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "4.6.0" logging: dependency: transitive description: name: logging - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.2" matcher: dependency: transitive description: name: matcher - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "0.12.12" + version: "0.12.11" material_color_utilities: dependency: transitive description: name: material_color_utilities - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "0.1.5" + version: "0.1.4" menu_base: dependency: transitive description: name: menu_base - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.1.1" meta: dependency: transitive description: name: meta - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "1.8.0" + version: "1.7.0" mime: dependency: transitive description: name: mime - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.2" nested: dependency: transitive description: name: nested - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.0" octo_image: dependency: transitive description: name: octo_image - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.2" package_config: dependency: transitive description: name: package_config - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.0" package_info_plus: dependency: "direct main" description: name: package_info_plus - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.4.3+1" package_info_plus_linux: dependency: transitive description: name: package_info_plus_linux - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.5" package_info_plus_macos: dependency: transitive description: name: package_info_plus_macos - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.3.0" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.2" package_info_plus_web: dependency: transitive description: name: package_info_plus_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.5" package_info_plus_windows: dependency: transitive description: name: package_info_plus_windows - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.0" path: dependency: transitive description: name: path - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "1.8.2" + version: "1.8.1" path_provider: dependency: "direct main" description: name: path_provider - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.11" path_provider_android: dependency: transitive description: name: path_provider_android - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.19" path_provider_ios: dependency: transitive description: name: path_provider_ios - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.11" path_provider_linux: dependency: transitive description: name: path_provider_linux - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.7" path_provider_macos: dependency: transitive description: name: path_provider_macos - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.6" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.4" path_provider_windows: dependency: transitive description: name: path_provider_windows - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.2" pedantic: dependency: transitive description: name: pedantic - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.11.1" petitparser: dependency: transitive description: name: petitparser - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "5.0.0" platform: dependency: transitive description: name: platform - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.1.0" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.2" pool: dependency: transitive description: name: pool - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.5.1" process: dependency: transitive description: name: process - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "4.2.4" provider: dependency: "direct main" description: name: provider - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "6.0.3" pub_semver: dependency: transitive description: name: pub_semver - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.1" pubspec_parse: dependency: transitive description: name: pubspec_parse - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.2.1" qr_code_scanner: dependency: "direct main" description: name: qr_code_scanner - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.1" quiver: dependency: transitive description: name: quiver - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.1.0" rxdart: dependency: transitive description: name: rxdart - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.27.5" screen_retriever: dependency: transitive description: name: screen_retriever - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.1.2" scroll_pos: dependency: "direct main" description: name: scroll_pos - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.3.0" settings_ui: dependency: "direct main" description: name: settings_ui - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.2" shared_preferences: dependency: "direct main" description: name: shared_preferences - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.15" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.12" shared_preferences_ios: dependency: transitive description: name: shared_preferences_ios - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.1" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.1" shared_preferences_macos: dependency: transitive description: name: shared_preferences_macos - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.4" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.0" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.4" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.1" shelf: dependency: transitive description: name: shelf - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.3.2" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.2" shortid: dependency: transitive description: name: shortid - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.1.2" sky_engine: @@ -948,91 +948,91 @@ packages: dependency: transitive description: name: source_gen - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.2.2" source_span: dependency: transitive description: name: source_span - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "1.9.0" + version: "1.8.2" sqflite: dependency: transitive description: name: sqflite - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.3+1" sqflite_common: dependency: transitive description: name: sqflite_common - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.2.1+1" stack_trace: dependency: transitive description: name: stack_trace - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.10.0" stream_channel: dependency: transitive description: name: stream_channel - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.0" stream_transform: dependency: transitive description: name: stream_transform - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.0" string_scanner: dependency: transitive description: name: string_scanner - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "1.1.1" + version: "1.1.0" synchronized: dependency: transitive description: name: synchronized - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.0+2" term_glyph: dependency: transitive description: name: term_glyph - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "1.2.1" + version: "1.2.0" test_api: dependency: transitive description: name: test_api - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "0.4.12" + version: "0.4.9" timing: dependency: transitive description: name: timing - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.0" toggle_switch: dependency: "direct main" description: name: toggle_switch - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.4.0" tray_manager: @@ -1048,182 +1048,182 @@ packages: dependency: "direct main" description: name: tuple - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.0" typed_data: dependency: transitive description: name: typed_data - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.3.1" url_launcher: dependency: "direct main" description: name: url_launcher - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "6.1.5" url_launcher_android: dependency: transitive description: name: url_launcher_android - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "6.0.17" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "6.0.17" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.1" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.0" url_launcher_web: dependency: transitive description: name: url_launcher_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.13" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.1" uuid: dependency: transitive description: name: uuid - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.6" vector_math: dependency: transitive description: name: vector_math - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.2" video_player: dependency: transitive description: name: video_player - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.4.6" video_player_android: dependency: transitive description: name: video_player_android - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "2.3.8" + version: "2.3.9" video_player_avfoundation: dependency: transitive description: name: video_player_avfoundation - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.3.5" video_player_platform_interface: dependency: transitive description: name: video_player_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "5.1.4" video_player_web: dependency: transitive description: name: video_player_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.12" visibility_detector: dependency: "direct main" description: name: visibility_detector - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.3.3" wakelock: dependency: "direct main" description: name: wakelock - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.5.6" wakelock_macos: dependency: transitive description: name: wakelock_macos - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.4.0" wakelock_platform_interface: dependency: transitive description: name: wakelock_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.3.0" wakelock_web: dependency: transitive description: name: wakelock_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.4.0" wakelock_windows: dependency: transitive description: name: wakelock_windows - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.2.0" watcher: dependency: transitive description: name: watcher - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.1" web_socket_channel: dependency: transitive description: name: web_socket_channel - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.2.0" win32: dependency: transitive description: name: win32 - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.7.0" window_manager: @@ -1234,33 +1234,33 @@ packages: resolved-ref: f1d69e5d0531af947373ec26ae22808f08b1aac6 url: "https://github.com/Kingtous/rustdesk_window_manager" source: git - version: "0.2.5" + version: "0.2.6" xdg_directories: dependency: transitive description: name: xdg_directories - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.2.0+1" xml: dependency: transitive description: name: xml - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "6.1.0" yaml: dependency: transitive description: name: yaml - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.1.1" zxing2: dependency: "direct main" description: name: zxing2 - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.1.0" sdks: diff --git a/src/flutter.rs b/src/flutter.rs index 928be607d..d64540cda 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -471,7 +471,7 @@ impl Session { } let mut msg_out = Message::new(); msg_out.set_key_event(key_event); - log::debug!("{:?}", msg_out); + // log::debug!("{:?}", msg_out); self.send_msg(msg_out); } From bb64690ac97ca3360c3c134718f95f53c6257a8d Mon Sep 17 00:00:00 2001 From: 21pages Date: Wed, 24 Aug 2022 11:01:58 +0800 Subject: [PATCH 0284/2015] optimize style of peer card Signed-off-by: 21pages --- flutter/assets/peer_searchbar.ttf | Bin 0 -> 1940 bytes flutter/lib/common.dart | 17 +- .../lib/desktop/pages/connection_page.dart | 338 +++++++++--------- .../lib/desktop/pages/desktop_home_page.dart | 36 +- flutter/lib/desktop/widgets/peer_widget.dart | 6 +- .../lib/desktop/widgets/peercard_widget.dart | 265 ++++++++------ .../lib/desktop/widgets/tabbar_widget.dart | 4 +- flutter/pubspec.yaml | 7 +- 8 files changed, 363 insertions(+), 310 deletions(-) create mode 100644 flutter/assets/peer_searchbar.ttf diff --git a/flutter/assets/peer_searchbar.ttf b/flutter/assets/peer_searchbar.ttf new file mode 100644 index 0000000000000000000000000000000000000000..7f87e48ce40bbffe890bb21bcbbcab31e0004f82 GIT binary patch literal 1940 zcmd^A&2Jk;6o0e3UI*Ja33Z&dNX}vr8>N+#SgsSb!3|Af%N61y4G|I%&c7T$@GxK}D z_j_;N%+9Wf5s{Z#B+^Lc!b|6r4+dW+V&h=vp1&|0pSYU+3igMv7xOi>QAvN_2m3Gd zcS_aO;tzcp=*4$o7s{Gicw*(7Poceyo-BiK?}%>$UqgSgTr)5E_#$tAVyoSv1=Cx?qy&Y$qp+?#6Bh$mU~b57Tibn z%zmdhL!>}1yBfIu+g>Qn6zImm*1;X?JV^YRT2SyB?>B)jXmy*%I@az^+BAFB(Z39q zD^6m@j=@X7gep-U34E0{PN6fy!*{^%g7bBwXDUkHSRcuGh_SU4JqMiOJmv1GhI*dGkIWw$JjB@>Zw zH;YEXa$tbDSP+wMO9J>SgQQX;Xm_WqihNT_Q^w@1%pH?kR9hY%Yc8`^ev z2Ubt1&~@ae_(?xTJO7^g5!n2m^RbJLI9MPzJ?>x;eAvMf@Q{OD_+rK#EQ7bcM9AZz z6Ex%SUSw?WH%WQ~o(R3^V20;=*}($&>8gW8@LxMv0{+p#E(+3b4wk{+b+Cu#*`p5j zQiSEF3{5q)g0i%#RPuVgsMpQTJgb$KtE$m4JM+9|G%I>t8I6r}sEk(Ej82;7N-1ud zMWtxyH6@LFTD7Vx4MV@E<;_^xG#jUfhxt03kLC3mO_4zwsbpd-kU~qeim^g@(y5MX z&PVehdlu>vEmIYd#zWr2(eqFn)P#;_Q)rZ8xMYV_8K~-(@^CdxTES{@)M26`1>HcV z8c^D)PeUbDURX5$xTf)@?}f-58n?m_y7O^ literal 0 HcmV?d00001 diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 9944d6884..643705d69 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -41,15 +41,18 @@ late final iconRestart = MemoryImage(Uint8List.fromList(base64Decode( 'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAB7BAAAewQHDaVRTAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAbhJREFUWIXVlrFqFGEUhb+7UYxaWCQKlrKKxaZSQVGDJih2tj6MD2DnMwiWvoAIRnENIpZiYxEro6IooiS7SPwsMgNLkk3mjmYmnmb45/73nMNwz/x/qH3gMu2gH6rAU+Blw+Lngau4jpmGxVF7qp1iPWjaQKnZ2WnXbuP/NqAeUPc3ZkA9XDwvqc+BVWCgPlJ7tRwUKThZce819b46VH+pfXVRXVO/q2cSul3VOgZUl0ejq86r39TXI8mqZKDuDEwCw3IREQvAbWAGmMsQZQ0sAl3gHPB1Q+0e8BuYzRDuy2yOiFVgaUxtRf0ETGc4syk4rc6PqU0Cx9j8Zf6dAeAK8Fi9sUXtFjABvEgxJlNwRP2svlNPjbw/q35U36oTFbnyMSwabxb/gB/qA3VBHagrauV7RW0DRfP1IvMlXqkXkhz1DYyQTKtHa/Z2VVMx3IiI+PI3/bCHjuOpFrSnAMpL6QfgTcMGesDx0kBr2BMzsNyi/vtQu8CJlgwsRbZDnWP90NkKaxHxJMOXMqAeAn5u0ydwMCKGY+qbkB3C2W3EKWoXk5zVoHbUZ+6Mh7tl4G4F8RJ3qvL+AfV3r5Vdpj70AAAAAElFTkSuQmCC'))); class IconFont { - static const _family = 'iconfont'; + static const _family1 = 'Tabbar'; + static const _family2 = 'PeerSearchbar'; IconFont._(); - static const IconData max = IconData(0xe606, fontFamily: _family); - static const IconData restore = IconData(0xe607, fontFamily: _family); - static const IconData close = IconData(0xe668, fontFamily: _family); - static const IconData min = IconData(0xe609, fontFamily: _family); - static const IconData add = IconData(0xe664, fontFamily: _family); - static const IconData menu = IconData(0xe628, fontFamily: _family); + static const IconData max = IconData(0xe606, fontFamily: _family1); + static const IconData restore = IconData(0xe607, fontFamily: _family1); + static const IconData close = IconData(0xe668, fontFamily: _family1); + static const IconData min = IconData(0xe609, fontFamily: _family1); + static const IconData add = IconData(0xe664, fontFamily: _family1); + static const IconData menu = IconData(0xe628, fontFamily: _family1); + static const IconData search = IconData(0xe6a4, fontFamily: _family2); + static const IconData round_close = IconData(0xe6ed, fontFamily: _family2); } class ColorThemeExtension extends ThemeExtension { diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 0b407d227..29219df2a 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -63,70 +63,43 @@ class _ConnectionPageState extends State { mainAxisSize: MainAxisSize.max, crossAxisAlignment: CrossAxisAlignment.start, children: [ - getUpdateUI(), - Row( - children: [ - getSearchBarUI(context), - ], - ).marginOnly(top: 22, left: 22), - SizedBox(height: 12), - Divider( - thickness: 1, - indent: 22, - endIndent: 22, - ), Expanded( - // TODO: move all tab info into _PeerTabbedPage - child: _PeerTabbedPage( - tabs: [ - translate('Recent Sessions'), - translate('Favorites'), - translate('Discovered'), - translate('Address Book') - ], - children: [ - RecentPeerWidget(), - FavoritePeerWidget(), - DiscoveredPeerWidget(), - // AddressBookPeerWidget(), - // FutureBuilder( - // future: getPeers(rType: RemoteType.recently), - // builder: (context, snapshot) { - // if (snapshot.hasData) { - // return snapshot.data!; - // } else { - // return Offstage(); - // } - // }), - // FutureBuilder( - // future: getPeers(rType: RemoteType.favorite), - // builder: (context, snapshot) { - // if (snapshot.hasData) { - // return snapshot.data!; - // } else { - // return Offstage(); - // } - // }), - // FutureBuilder( - // future: getPeers(rType: RemoteType.discovered), - // builder: (context, snapshot) { - // if (snapshot.hasData) { - // return snapshot.data!; - // } else { - // return Offstage(); - // } - // }), - FutureBuilder( - future: buildAddressBook(context), - builder: (context, snapshot) { - if (snapshot.hasData) { - return snapshot.data!; - } else { - return Offstage(); - } - }), - ], - ).marginSymmetric(horizontal: 6)), + child: Column( + children: [ + getUpdateUI(), + Row( + children: [ + getSearchBarUI(context), + ], + ).marginOnly(top: 22), + SizedBox(height: 12), + Divider(), + Expanded( + child: _PeerTabbedPage( + tabs: [ + translate('Recent Sessions'), + translate('Favorites'), + translate('Discovered'), + translate('Address Book') + ], + children: [ + RecentPeerWidget(), + FavoritePeerWidget(), + DiscoveredPeerWidget(), + FutureBuilder( + future: buildAddressBook(context), + builder: (context, snapshot) { + if (snapshot.hasData) { + return snapshot.data!; + } else { + return Offstage(); + } + }), + ], + )), + ], + ).marginSymmetric(horizontal: 22), + ), Divider(), SizedBox(height: 50, child: Obx(() => buildStatus())) .paddingSymmetric(horizontal: 12.0) @@ -193,7 +166,7 @@ class _ConnectionPageState extends State { }); var w = Container( width: 320 + 20 * 2, - padding: EdgeInsets.only(left: 20, right: 20, bottom: 22, top: 24), + padding: EdgeInsets.fromLTRB(20, 24, 20, 22), decoration: BoxDecoration( color: MyTheme.color(context).bg, borderRadius: const BorderRadius.all(Radius.circular(13)), @@ -977,67 +950,57 @@ class _PeerTabbedPage extends StatefulWidget { class _PeerTabbedPageState extends State<_PeerTabbedPage> with SingleTickerProviderStateMixin { - late TabController _tabController; + late PageController _pageController = PageController(); RxInt _tabIndex = 0.obs; @override void initState() { super.initState(); - _tabController = - TabController(vsync: this, length: super.widget.tabs.length); - _tabController.addListener(_handleTabSelection); } // hard code for now - void _handleTabSelection() { - if (_tabController.indexIsChanging) { - // reset search text - peerSearchText.value = ""; - peerSearchTextController.clear(); - _tabIndex.value = _tabController.index; - switch (_tabController.index) { - case 0: - bind.mainLoadRecentPeers(); - break; - case 1: - bind.mainLoadFavPeers(); - break; - case 2: - bind.mainDiscover(); - break; - case 3: - break; - } + void _handleTabSelection(int index) { + // reset search text + peerSearchText.value = ""; + peerSearchTextController.clear(); + _tabIndex.value = index; + _pageController.jumpToPage(index); + switch (index) { + case 0: + bind.mainLoadRecentPeers(); + break; + case 1: + bind.mainLoadFavPeers(); + break; + case 2: + bind.mainDiscover(); + break; + case 3: + break; } } @override void dispose() { - _tabController.dispose(); + _pageController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { - // return DefaultTabController( - // length: 4, - // child: Column( - // crossAxisAlignment: CrossAxisAlignment.start, - // children: [ - // _createTabBar(), - // _createTabBarView(), - // ], - // )); - return Column( + textBaseline: TextBaseline.ideographic, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - children: [ - Expanded(child: _createTabBar(context)), - _createSearchBar(context), - _createPeerViewTypeSwitch(context), - ], + Container( + height: 28, + child: Row( + children: [ + Expanded(child: _createTabBar(context)), + _createSearchBar(context), + _createPeerViewTypeSwitch(context), + ], + ), ), _createTabBarView(), ], @@ -1045,70 +1008,121 @@ class _PeerTabbedPageState extends State<_PeerTabbedPage> } Widget _createTabBar(BuildContext context) { - return TabBar( - isScrollable: true, - indicatorSize: TabBarIndicatorSize.label, - indicatorColor: Colors.transparent, - indicatorWeight: 0.1, - controller: _tabController, - labelPadding: EdgeInsets.zero, - padding: EdgeInsets.only(left: 16), - tabs: super.widget.tabs.asMap().entries.map((t) { - return Obx(() => Container( - padding: EdgeInsets.symmetric(horizontal: 8, vertical: 6), - decoration: BoxDecoration( - color: - _tabIndex.value == t.key ? MyTheme.color(context).bg : null, - borderRadius: BorderRadius.circular(2), - ), - child: Text( - t.value, - style: TextStyle( - height: 1, - color: _tabIndex.value == t.key - ? MyTheme.color(context).text - : MyTheme.color(context).lightText), - ))); + return ListView( + scrollDirection: Axis.horizontal, + shrinkWrap: true, + children: super.widget.tabs.asMap().entries.map((t) { + return Obx(() => GestureDetector( + child: Container( + padding: EdgeInsets.symmetric(horizontal: 8), + decoration: BoxDecoration( + color: _tabIndex.value == t.key + ? MyTheme.color(context).bg + : null, + borderRadius: BorderRadius.circular(2), + ), + child: Align( + alignment: Alignment.center, + child: Text( + t.value, + textAlign: TextAlign.center, + style: TextStyle( + height: 1, + fontSize: 14, + color: _tabIndex.value == t.key + ? MyTheme.color(context).text + : MyTheme.color(context).lightText), + ), + )), + onTap: () => _handleTabSelection(t.key), + )); }).toList()); } Widget _createTabBarView() { return Expanded( - child: TabBarView( - controller: _tabController, children: super.widget.children) - .paddingSymmetric(horizontal: 12.0, vertical: 4.0)); + child: PageView( + controller: _pageController, children: super.widget.children) + .marginSymmetric(vertical: 12)); } _createSearchBar(BuildContext context) { + RxBool focused = false.obs; + FocusNode focusNode = FocusNode(); + focusNode.addListener(() => focused.value = focusNode.hasFocus); + RxBool rowHover = false.obs; + RxBool clearHover = false.obs; return Container( - width: 175, - height: 30, - margin: EdgeInsets.only(right: 16), - decoration: BoxDecoration(color: Colors.white), - child: Obx( - () => TextField( - controller: peerSearchTextController, - onChanged: (searchText) { - peerSearchText.value = searchText; - }, - decoration: InputDecoration( - prefixIcon: Icon( - Icons.search, - size: 20, - ), - contentPadding: EdgeInsets.zero, - hintText: translate("Search ID"), - hintStyle: TextStyle(fontSize: 14), - border: OutlineInputBorder(), - isDense: true, - ), - ), - ), + width: 120, + height: 25, + margin: EdgeInsets.only(right: 13), + decoration: BoxDecoration(color: MyTheme.color(context).bg), + child: Obx(() => Row( + children: [ + Expanded( + child: MouseRegion( + onEnter: (_) => rowHover.value = true, + onExit: (_) => rowHover.value = false, + child: Row( + children: [ + Icon( + IconFont.search, + size: 16, + color: MyTheme.color(context).placeholder, + ).marginSymmetric(horizontal: 4), + Expanded( + child: TextField( + controller: peerSearchTextController, + onChanged: (searchText) { + peerSearchText.value = searchText; + }, + focusNode: focusNode, + textAlign: TextAlign.start, + maxLines: 1, + cursorColor: MyTheme.color(context).lightText, + cursorHeight: 18, + cursorWidth: 1, + style: TextStyle(fontSize: 14), + decoration: InputDecoration( + contentPadding: EdgeInsets.symmetric(vertical: 6), + hintText: + focused.value ? null : translate("Search ID"), + hintStyle: TextStyle( + fontSize: 14, + color: MyTheme.color(context).placeholder), + border: InputBorder.none, + isDense: true, + ), + ), + ), + ], + ), + ), + ), + Offstage( + offstage: !(peerSearchText.value.isNotEmpty && + (rowHover.value || clearHover.value)), + child: InkWell( + onHover: (value) => clearHover.value = value, + child: Icon( + IconFont.round_close, + size: 16, + color: clearHover.value + ? MyTheme.color(context).text + : MyTheme.color(context).placeholder, + ).marginSymmetric(horizontal: 4), + onTap: () { + peerSearchTextController.clear(); + peerSearchText.value = ""; + }), + ) + ], + )), ); } _createPeerViewTypeSwitch(BuildContext context) { - final activeDeco = BoxDecoration(color: Colors.white); + final activeDeco = BoxDecoration(color: MyTheme.color(context).bg); return Row( children: [ Obx( @@ -1122,8 +1136,10 @@ class _PeerTabbedPageState extends State<_PeerTabbedPage> }, child: Icon( Icons.grid_view_rounded, - size: 20, - color: Colors.black54, + size: 18, + color: peerCardUiType.value == PeerUiType.grid + ? MyTheme.color(context).text + : MyTheme.color(context).lightText, )), ), ), @@ -1138,12 +1154,14 @@ class _PeerTabbedPageState extends State<_PeerTabbedPage> }, child: Icon( Icons.list, - size: 24, - color: Colors.black54, + size: 18, + color: peerCardUiType.value == PeerUiType.list + ? MyTheme.color(context).text + : MyTheme.color(context).lightText, )), ), ), ], - ).paddingOnly(right: 16.0); + ); } } diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index f85cf5b86..12f17c95e 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -107,9 +107,10 @@ class _DesktopHomePageState extends State crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( - height: 15, + height: 25, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( translate("ID"), @@ -133,7 +134,7 @@ class _DesktopHomePageState extends State readOnly: true, decoration: InputDecoration( border: InputBorder.none, - contentPadding: EdgeInsets.only(bottom: 8), + contentPadding: EdgeInsets.only(bottom: 18), ), style: TextStyle( fontSize: 22, @@ -239,26 +240,17 @@ class _DesktopHomePageState extends State } }, child: Obx( - () => Container( - decoration: BoxDecoration( - // borderRadius: BorderRadius.circular(10), - shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - color: hover.value - ? MyTheme.color(context).grayBg! - : MyTheme.color(context).bg!, - spreadRadius: 2) - ], - ), - child: Center( - child: Icon( - Icons.more_vert_outlined, - size: 20, - color: hover.value - ? MyTheme.color(context).text - : MyTheme.color(context).lightText, - ), + () => CircleAvatar( + radius: 12, + backgroundColor: hover.value + ? MyTheme.color(context).grayBg! + : MyTheme.color(context).bg!, + child: Icon( + Icons.more_vert_outlined, + size: 20, + color: hover.value + ? MyTheme.color(context).text + : MyTheme.color(context).lightText, ), ), ), diff --git a/flutter/lib/desktop/widgets/peer_widget.dart b/flutter/lib/desktop/widgets/peer_widget.dart index 70df44ab5..fa79db624 100644 --- a/flutter/lib/desktop/widgets/peer_widget.dart +++ b/flutter/lib/desktop/widgets/peer_widget.dart @@ -100,10 +100,10 @@ class _PeerWidgetState extends State<_PeerWidget> with WindowListener { offstage: super.widget._offstageFunc(peer), child: Obx( () => Container( - width: 225, + width: 220, height: peerCardUiType.value == PeerUiType.grid - ? 150 - : 50, + ? 140 + : 42, child: VisibilityDetector( key: Key('${peer.id}'), onVisibilityChanged: (info) { diff --git a/flutter/lib/desktop/widgets/peercard_widget.dart b/flutter/lib/desktop/widgets/peercard_widget.dart index e8f4d6801..f76336cda 100644 --- a/flutter/lib/desktop/widgets/peercard_widget.dart +++ b/flutter/lib/desktop/widgets/peercard_widget.dart @@ -36,29 +36,31 @@ class _PeerCard extends StatefulWidget { class _PeerCardState extends State<_PeerCard> with AutomaticKeepAliveClientMixin { var _menuPos; + final double _cardRadis = 20; + final double _borderWidth = 2; @override Widget build(BuildContext context) { super.build(context); final peer = super.widget.peer; var deco = Rx(BoxDecoration( - border: Border.all(color: Colors.transparent, width: 1.0), + border: Border.all(color: Colors.transparent, width: _borderWidth), borderRadius: peerCardUiType.value == PeerUiType.grid - ? BorderRadius.circular(20) + ? BorderRadius.circular(_cardRadis) : null)); return MouseRegion( onEnter: (evt) { deco.value = BoxDecoration( - border: Border.all(color: Colors.blue, width: 1.0), + border: Border.all(color: MyTheme.button, width: _borderWidth), borderRadius: peerCardUiType.value == PeerUiType.grid - ? BorderRadius.circular(20) + ? BorderRadius.circular(_cardRadis) : null); }, onExit: (evt) { deco.value = BoxDecoration( - border: Border.all(color: Colors.transparent, width: 1.0), + border: Border.all(color: Colors.transparent, width: _borderWidth), borderRadius: peerCardUiType.value == PeerUiType.grid - ? BorderRadius.circular(20) + ? BorderRadius.circular(_cardRadis) : null); }, child: GestureDetector( @@ -71,25 +73,25 @@ class _PeerCardState extends State<_PeerCard> Widget _buildPeerTile( BuildContext context, Peer peer, Rx deco) { - final greyStyle = TextStyle(fontSize: 12, color: Colors.grey); + final greyStyle = + TextStyle(fontSize: 12, color: MyTheme.color(context).lighterText); + RxBool iconHover = false.obs; return Obx( () => Container( - decoration: deco.value, + foregroundDecoration: deco.value, child: Row( mainAxisSize: MainAxisSize.max, children: [ Container( - height: 50, - width: 50, decoration: BoxDecoration( color: str2color('${peer.id}${peer.platform}', 0x7f), ), alignment: Alignment.center, - child: _getPlatformImage('${peer.platform}').paddingAll(8.0), + child: _getPlatformImage('${peer.platform}', 30).paddingAll(6), ), Expanded( child: Container( - decoration: BoxDecoration(color: Colors.white), + decoration: BoxDecoration(color: MyTheme.color(context).bg), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -98,17 +100,17 @@ class _PeerCardState extends State<_PeerCard> mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ Row(children: [ - Text( - '${peer.id}', - style: TextStyle(fontWeight: FontWeight.w400), - ), Padding( - padding: EdgeInsets.fromLTRB(4, 4, 8, 4), + padding: EdgeInsets.fromLTRB(0, 4, 4, 4), child: CircleAvatar( radius: 5, backgroundColor: peer.online ? Colors.green : Colors.yellow)), + Text( + '${peer.id}', + style: TextStyle(fontWeight: FontWeight.w400), + ), ]), Align( alignment: Alignment.centerLeft, @@ -122,6 +124,7 @@ class _PeerCardState extends State<_PeerCard> : snapshot.data!; return Tooltip( message: name, + waitDuration: Duration(seconds: 1), child: Text( name, style: greyStyle, @@ -145,17 +148,31 @@ class _PeerCardState extends State<_PeerCard> ), ), InkWell( - child: Icon(Icons.more_vert), - onTapDown: (e) { - final x = e.globalPosition.dx; - final y = e.globalPosition.dy; - _menuPos = RelativeRect.fromLTRB(x, y, x, y); - }, - onTap: () { - _showPeerMenu(context, peer.id); - }), + child: CircleAvatar( + radius: 12, + backgroundColor: iconHover.value + ? MyTheme.color(context).grayBg! + : MyTheme.color(context).bg!, + child: Icon( + Icons.more_vert, + size: 18, + color: iconHover.value + ? MyTheme.color(context).text + : MyTheme.color(context).lightText, + ), + ), + onTapDown: (e) { + final x = e.globalPosition.dx; + final y = e.globalPosition.dy; + _menuPos = RelativeRect.fromLTRB(x, y, x, y); + }, + onTap: () { + _showPeerMenu(context, peer.id); + }, + onHover: (value) => iconHover.value = value, + ), ], - ).paddingSymmetric(horizontal: 8.0), + ).paddingSymmetric(horizontal: 4.0), ), ) ], @@ -166,105 +183,121 @@ class _PeerCardState extends State<_PeerCard> Widget _buildPeerCard( BuildContext context, Peer peer, Rx deco) { + RxBool iconHover = false.obs; return Card( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + color: Colors.transparent, + elevation: 0, + margin: EdgeInsets.zero, child: Obx( () => Container( - decoration: deco.value, - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Expanded( - child: Container( - decoration: BoxDecoration( + foregroundDecoration: deco.value, + child: ClipRRect( + borderRadius: BorderRadius.circular(_cardRadis - _borderWidth), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: Container( color: str2color('${peer.id}${peer.platform}', 0x7f), - borderRadius: BorderRadius.only( - topLeft: Radius.circular(20), - topRight: Radius.circular(20), - ), - ), - child: Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Container( - padding: const EdgeInsets.all(6), - child: _getPlatformImage('${peer.platform}'), - ), - Row( - children: [ - Expanded( - child: FutureBuilder( - future: bind.mainGetPeerOption( - id: peer.id, key: 'alias'), - builder: (_, snapshot) { - if (snapshot.hasData) { - final name = snapshot.data!.isEmpty - ? '${peer.username}@${peer.hostname}' - : snapshot.data!; - return Tooltip( - message: name, - child: Text( - name, + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.all(6), + child: + _getPlatformImage('${peer.platform}', 60), + ), + Row( + children: [ + Expanded( + child: FutureBuilder( + future: bind.mainGetPeerOption( + id: peer.id, key: 'alias'), + builder: (_, snapshot) { + if (snapshot.hasData) { + final name = snapshot.data!.isEmpty + ? '${peer.username}@${peer.hostname}' + : snapshot.data!; + return Tooltip( + message: name, + waitDuration: Duration(seconds: 1), + child: Text( + name, + style: TextStyle( + color: Colors.white70, + fontSize: 12), + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + ), + ); + } else { + // alias has not arrived + return Center( + child: Text( + '${peer.username}@${peer.hostname}', style: TextStyle( color: Colors.white70, fontSize: 12), textAlign: TextAlign.center, overflow: TextOverflow.ellipsis, - ), - ); - } else { - // alias has not arrived - return Center( - child: Text( - '${peer.username}@${peer.hostname}', - style: TextStyle( - color: Colors.white70, - fontSize: 12), - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - )); - } - }, + )); + } + }, + ), ), - ), - ], - ), - ], - ).paddingAll(4.0), - ), - ], + ], + ), + ], + ).paddingAll(4.0), + ), + ], + ), ), ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row(children: [ - Padding( - padding: EdgeInsets.fromLTRB(0, 4, 8, 4), - child: CircleAvatar( - radius: 5, - backgroundColor: - peer.online ? Colors.green : Colors.yellow)), - Text('${peer.id}') - ]), - InkWell( - child: Icon(Icons.more_vert), - onTapDown: (e) { - final x = e.globalPosition.dx; - final y = e.globalPosition.dy; - _menuPos = RelativeRect.fromLTRB(x, y, x, y); - }, - onTap: () { - _showPeerMenu(context, peer.id); - }), - ], - ).paddingSymmetric(vertical: 8.0, horizontal: 12.0) - ], + Container( + color: MyTheme.color(context).bg, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row(children: [ + Padding( + padding: EdgeInsets.fromLTRB(0, 4, 8, 4), + child: CircleAvatar( + radius: 5, + backgroundColor: peer.online + ? Colors.green + : Colors.yellow)), + Text('${peer.id}') + ]), + InkWell( + child: CircleAvatar( + radius: 12, + backgroundColor: iconHover.value + ? MyTheme.color(context).grayBg! + : MyTheme.color(context).bg!, + child: Icon(Icons.more_vert, + size: 18, + color: iconHover.value + ? MyTheme.color(context).text + : MyTheme.color(context).lightText)), + onTapDown: (e) { + final x = e.globalPosition.dx; + final y = e.globalPosition.dy; + _menuPos = RelativeRect.fromLTRB(x, y, x, y); + }, + onTap: () { + _showPeerMenu(context, peer.id); + }, + onHover: (value) => iconHover.value = value), + ], + ).paddingSymmetric(vertical: 8.0, horizontal: 12.0), + ) + ], + ), ), ), ), @@ -365,12 +398,12 @@ class _PeerCardState extends State<_PeerCard> } /// Get the image for the current [platform]. - Widget _getPlatformImage(String platform) { + Widget _getPlatformImage(String platform, double size) { platform = platform.toLowerCase(); if (platform == 'mac os') platform = 'mac'; else if (platform != 'linux' && platform != 'android') platform = 'win'; - return Image.asset('assets/$platform.png', height: 50); + return Image.asset('assets/$platform.png', height: size, width: size); } void _abEditTag(String id) { diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 3b88deae6..09f1ee4b5 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -514,8 +514,10 @@ class ActionIcon extends StatelessWidget { RxBool hover = false.obs; return Obx(() => Tooltip( message: translate(message), + waitDuration: Duration(seconds: 1), child: InkWell( - hoverColor: is_close ? Colors.red : theme.hoverColor, + hoverColor: + is_close ? Color.fromARGB(255, 196, 43, 28) : theme.hoverColor, onHover: (value) => hover.value = value, child: Container( height: _kTabBarHeight - 1, diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index da6a3cd3e..06231f8bf 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -107,9 +107,14 @@ flutter: - family: GestureIcons fonts: - asset: assets/gestures.ttf - - family: IconFont + - family: Tabbar fonts: - asset: assets/tabbar.ttf + - family: PeerSearchbar + fonts: + - asset: assets/peer_searchbar.ttf + + # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware. From b2b7ca30fde888212af527a2b7fc6eded2dae008 Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 25 Aug 2022 14:35:08 +0800 Subject: [PATCH 0285/2015] add force-always-relay menu Signed-off-by: 21pages --- .../lib/desktop/widgets/peercard_widget.dart | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/flutter/lib/desktop/widgets/peercard_widget.dart b/flutter/lib/desktop/widgets/peercard_widget.dart index f76336cda..d39f3d359 100644 --- a/flutter/lib/desktop/widgets/peercard_widget.dart +++ b/flutter/lib/desktop/widgets/peercard_widget.dart @@ -359,6 +359,17 @@ class _PeerCardState extends State<_PeerCard> _rename(id); } else if (value == 'unremember-password') { await bind.mainForgetPassword(id: id); + } else if (value == 'force-always-relay') { + String value; + String oldValue = + await bind.mainGetPeerOption(id: id, key: 'force-always-relay'); + if (oldValue.isEmpty) { + value = 'Y'; + } else { + value = ''; + } + await bind.mainSetPeerOption( + id: id, key: 'force-always-relay', value: value); } } @@ -572,6 +583,7 @@ class RecentPeerCard extends BasePeerCard { child: Text(translate('Transfer File')), value: 'file'), PopupMenuItem( child: Text(translate('TCP Tunneling')), value: 'tcp-tunnel'), + await _forceAlwaysRelayMenuItem(peer.id), PopupMenuItem(child: Text(translate('Rename')), value: 'rename'), PopupMenuItem(child: Text(translate('Remove')), value: 'remove'), PopupMenuItem( @@ -595,6 +607,7 @@ class FavoritePeerCard extends BasePeerCard { child: Text(translate('Transfer File')), value: 'file'), PopupMenuItem( child: Text(translate('TCP Tunneling')), value: 'tcp-tunnel'), + await _forceAlwaysRelayMenuItem(peer.id), PopupMenuItem(child: Text(translate('Rename')), value: 'rename'), PopupMenuItem(child: Text(translate('Remove')), value: 'remove'), PopupMenuItem( @@ -618,6 +631,7 @@ class DiscoveredPeerCard extends BasePeerCard { child: Text(translate('Transfer File')), value: 'file'), PopupMenuItem( child: Text(translate('TCP Tunneling')), value: 'tcp-tunnel'), + await _forceAlwaysRelayMenuItem(peer.id), PopupMenuItem(child: Text(translate('Rename')), value: 'rename'), PopupMenuItem(child: Text(translate('Remove')), value: 'remove'), PopupMenuItem( @@ -641,6 +655,7 @@ class AddressBookPeerCard extends BasePeerCard { child: Text(translate('Transfer File')), value: 'file'), PopupMenuItem( child: Text(translate('TCP Tunneling')), value: 'tcp-tunnel'), + await _forceAlwaysRelayMenuItem(peer.id), PopupMenuItem(child: Text(translate('Rename')), value: 'rename'), PopupMenuItem( child: Text(translate('Remove')), value: 'ab-delete'), @@ -654,3 +669,20 @@ class AddressBookPeerCard extends BasePeerCard { ]; } } + +Future> _forceAlwaysRelayMenuItem(String id) async { + bool force_always_relay = + (await bind.mainGetPeerOption(id: id, key: 'force-always-relay')) + .isNotEmpty; + return PopupMenuItem( + child: Row( + children: [ + Offstage( + offstage: !force_always_relay, + child: Icon(Icons.check), + ), + Text(translate('Always connect via relay')), + ], + ), + value: 'force-always-relay'); +} From 1fb186fd2a5711c0f38e4fa8e3003c5c19b563c4 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Thu, 25 Aug 2022 17:35:45 +0800 Subject: [PATCH 0286/2015] feat: manjaro/arch build.py --- build.py | 34 ++++++++++++++++++++++++++-------- flutter/rustdesk.desktop | 19 +++++++++++++++++++ flutter/rustdesk.service | 16 ++++++++++++++++ src/core_main.rs | 4 ++++ 4 files changed, 65 insertions(+), 8 deletions(-) create mode 100644 flutter/rustdesk.desktop create mode 100644 flutter/rustdesk.service diff --git a/build.py b/build.py index 341f4f4e6..3b5555b42 100755 --- a/build.py +++ b/build.py @@ -66,6 +66,8 @@ def make_parser(): default='', help='Integrate features, windows only.' 'Available: IddDriver, PrivacyMode. Special value is "ALL" and empty "". Default is empty.') + parser.add_argument('--flutter', action='store_true', + help='Build flutter package', default=False) parser.add_argument( '--hwcodec', action='store_true', @@ -114,6 +116,8 @@ def get_features(args): features.extend(get_rc_features(args)) if args.hwcodec: features.append('hwcodec') + if args.flutter: + features.append('flutter') print("features:", features) return features @@ -135,6 +139,7 @@ def main(): os.system('git checkout src/ui/common.tis') version = get_version() features = ",".join(get_features(args)) + flutter = args.flutter if windows: os.system('cargo build --release --features ' + features) # os.system('upx.exe target/release/rustdesk.exe') @@ -147,14 +152,26 @@ def main(): print('Not signed') os.system(f'cp -rf target/release/RustDesk.exe rustdesk-{version}-setdown.exe') elif os.path.isfile('/usr/bin/pacman'): - os.system('cargo build --release --features ' + features) - os.system('git checkout src/ui/common.tis') - os.system('strip target/release/rustdesk') - os.system("sed -i 's/pkgver=.*/pkgver=%s/g' PKGBUILD" % version) - # pacman -S -needed base-devel - os.system('HBB=`pwd` makepkg -f') - os.system('mv rustdesk-%s-0-x86_64.pkg.tar.zst rustdesk-%s-manjaro-arch.pkg.tar.zst' % (version, version)) - # pacman -U ./rustdesk.pkg.tar.zst + if flutter: + os.chdir('flutter') + os.system('flutter build linux --release') + os.system('strip build/linux/x64/release/liblibrustdesk.so') + os.system("sed -i 's/pkgver=.*/pkgver=%s/g' PKGBUILD" % version) + # pacman -S -needed base-devel + os.system('HBB=`pwd` makepkg -f') + os.system( + 'mv rustdesk-%s-0-x86_64.pkg.tar.zst ../rustdesk-%s-manjaro-arch.pkg.tar.zst' % (version, version)) + os.chdir('..') + else: + os.system('cargo build --release --features ' + features) + os.system('git checkout src/ui/common.tis') + os.system('strip target/release/rustdesk') + os.system("sed -i 's/pkgver=.*/pkgver=%s/g' PKGBUILD" % version) + # pacman -S -needed base-devel + os.system('HBB=`pwd` makepkg -f') + os.system( + 'mv rustdesk-%s-0-x86_64.pkg.tar.zst rustdesk-%s-manjaro-arch.pkg.tar.zst' % (version, version)) + # pacman -U ./rustdesk.pkg.tar.zst elif os.path.isfile('/usr/bin/yum'): os.system('cargo build --release --features ' + features) os.system('strip target/release/rustdesk') @@ -210,6 +227,7 @@ rcodesign notarize --api-issuer 69a6de7d-2907-47e3-e053-5b8c7c11a4d1 --api-key 9 else: print('Not signed') else: + # buid deb package os.system('mv target/release/bundle/deb/rustdesk*.deb ./rustdesk.deb') os.system('dpkg-deb -R rustdesk.deb tmpdeb') os.system('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/') diff --git a/flutter/rustdesk.desktop b/flutter/rustdesk.desktop new file mode 100644 index 000000000..aca57eeff --- /dev/null +++ b/flutter/rustdesk.desktop @@ -0,0 +1,19 @@ +[Desktop Entry] +Version=1.2.0 +Name=RustDesk +GenericName=Remote Desktop +Comment=Remote Desktop +Exec=/usr/lib/rustdesk/flutter_hbb %u +Icon=/usr/share/rustdesk/files/rustdesk.png +Terminal=false +Type=Application +StartupNotify=true +Categories=Network;RemoteAccess;GTK; +Keywords=internet; +Actions=new-window; + +X-Desktop-File-Install-Version=0.23 + +[Desktop Action new-window] +Name=Open a New Window + diff --git a/flutter/rustdesk.service b/flutter/rustdesk.service new file mode 100644 index 000000000..422d9e387 --- /dev/null +++ b/flutter/rustdesk.service @@ -0,0 +1,16 @@ +[Unit] +Description=RustDesk +Requires=network.target +After=systemd-user-sessions.service + +[Service] +Type=simple +ExecStart=/usr/lib/rustdesk/flutter_hbb --service +PIDFile=/run/rustdesk.pid +KillMode=mixed +TimeoutStopSec=30 +User=root +LimitNOFILE=100000 + +[Install] +WantedBy=multi-user.target diff --git a/src/core_main.rs b/src/core_main.rs index 2603e000e..c780a1cb0 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -19,6 +19,10 @@ pub fn core_main() -> bool { start_os_service(); return false; } + if args[1] == "--server" { + // TODO: server + return false; + } } true } From 5e9a31340b899822090a3731769ae79c6bf5f3e5 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 25 Aug 2022 17:39:03 +0800 Subject: [PATCH 0287/2015] minifize png --- 128x128.png | Bin 10123 -> 1629 bytes 128x128@2x.png | Bin 25356 -> 3042 bytes 32x32.png | Bin 4193 -> 504 bytes 3 files changed, 0 insertions(+), 0 deletions(-) diff --git a/128x128.png b/128x128.png index 045d8f89476309b36680e0c373000be5f89ab981..cd35a0bc80ecea70e8ecc5cf41089cb5afdd6111 100644 GIT binary patch literal 1629 zcmbW2`#;o)9>%|OF+_IcHtsV*#*vyhhfvcHixi>}xvUOy*^t4k(c+7ZhQbtsO(?mi zRgzLdVaBn_Em4%)iW4y|xrEPY|Aq5TFPC1C|Z4k$?okeF2oUfO|F=#lodK zaP%sea)FuvhvR`&08uxft)nF9`(kpD&JHe!!I$-!cUi8aR*_6qWqO(ku`QR(q96$V zKXz6Yr=s7@y?@Jk$B;+I4u*y|M7%n7dv(fEoYefUFOBVb4)G8m#2;!2sJndiAy&6| z{*@6St}Z*1Z=;bRm=GO0<+w+;wEag5ZqNM+ZijCCib(4C#s0!jCtVfs3~1a{ko4KxEj`|` zCCah7z12<$wM#>HD4{#jP!k&BqKNUOA-;;3Vj5Dch@sduQQOY!G#liwmzD}%G5CGx zLa1giB{;$&&_kqkdzap9viPoMR+qoDdGCS;m9<*-sPLXqJ?>s=+~!32R``64zDCtk zm1bjH^lJm05b<9vgYS(Ud>HH6v-f$Mymq$E4<0a}Yl z)|q{}q$9t`WQuuqrIHSasp}h2cbTIy1&lY;BRn2U@ zTCJ)yOLrG((8zHLUKjX({ti6{!!=|KLy(`0B`jC8`WjLV1(+(8CehD3V2NzdJ~RHgFqzsW_43sHP%b=NsHeEy;!OFne+@k4Smy<5tF;2az|J@}AZ)Mt=M zFx@cY44oCSn>C*7O#j4i<`rZ|y~h-GsD_|-y<&dcIA@OTvs3gN*~{aNmd&ABi*#lt zU;8her9&BB%R=EW1)kP@SOM?Zj!c1=%rsniELcr&r?;J=DL%2M-Vsu5#`>+SZl2@wR$~&ul0_~ zs=*(MZmBk&jvM;ey;0!NkA@`@ERJ3BmQz&s z%qq0`KWXa0QOh@YGIXp-EipugE#*1IZsYRvXs+!^h zmM8A;qiirSvK7yZ=-1%0- z#ubJuLX@b6f-HQE0a>#8xOh{yY=rvJ zQwP6gdZRsJWY|QQLmznjZMkGnN2nBK#x} zx6mRlJ&m``jS*fSN~Yfoo0d~ap_fFgT|Yx^#O5@{_=UWi<978CV}n;0bnA)nF->Vl wF68ooE09__B8on+xPMF7HX({WRDjtKbFV#ktn+}d;_siZvv#n$XX%spKe5{;5dZ)H literal 10123 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 XFgC7uZ$0_<<^U+ls>;+!nTP)`s}Kb4 diff --git a/128x128@2x.png b/128x128@2x.png index 39e2b23cf62d86a3ec4a4cd29a632dc1d1fd1a18..3da699f1d711a027e315d45dc5ada04bdfa2e114 100644 GIT binary patch literal 3042 zcmb7`_dnH-ATYsJmXs0dftTV$m2vLltusLV@J!Yv~s*A?DY zT{GKF;ws5XlDs~B|Ag=3aUPH7`SJO~`QfD5TA$(K5aj>>fXl+%LtXQbS2dK{ z1|3U;#+Xp>OX%|kH1iF5*b7C!hLkfPgKX%0DP)ufwN688KOyEe)c6syD~7Z)A<7t( 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 Ef?R_wi2wiq diff --git a/32x32.png b/32x32.png index bba85feb6bc83e16410e2bfaa94551c224d27aa8..21440d4223ac1cbc5a2934d45f4fb505e90c8e41 100644 GIT binary patch delta 489 zcmVHpK* z|Gmury2<~iy#JT5|BtHwh^7C6qW^N3|15a_;O76w)Bmo&|9_aU|C6o%Sc(5lg#R9O z|FgvZoU{LYp8rLG{~vb$9CiOS1|hWo000JbQchCn7tET4 z5)ONidsO9+i*s(P3xzTdZoA!Cz fLo6TsIIqbcoi-P{l5PL@00000NkvXXu0mjf`|ttP literal 4193 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 Date: Thu, 25 Aug 2022 18:36:44 +0800 Subject: [PATCH 0288/2015] feat: deb package --- PKGBUILD | 4 +- build.py | 183 +++++++++++++++++++++++++-------------- flutter/rustdesk.desktop | 2 +- rpm-suse.spec | 2 +- rpm.spec | 2 +- 5 files changed, 122 insertions(+), 71 deletions(-) diff --git a/PKGBUILD b/PKGBUILD index 0d67a28b6..6fb65d48b 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,5 +1,5 @@ pkgname=rustdesk -pkgver=1.1.9 +pkgver=1.1.10 pkgrel=0 epoch= pkgdesc="" @@ -27,5 +27,5 @@ package() { install -Dm 644 $HBB/rustdesk.service -t "${pkgdir}/usr/share/rustdesk/files" install -Dm 644 $HBB/rustdesk.desktop -t "${pkgdir}/usr/share/rustdesk/files" install -Dm 644 $HBB/pynput_service.py -t "${pkgdir}/usr/share/rustdesk/files" - install -Dm 644 $HBB/256-no-margin.png "${pkgdir}/usr/share/rustdesk/files/rustdesk.png" + install -Dm 644 $HBB/128x128@2x.png "${pkgdir}/usr/share/rustdesk/files/rustdesk.png" } diff --git a/build.py b/build.py index 3b5555b42..6b03cb57b 100755 --- a/build.py +++ b/build.py @@ -121,6 +121,53 @@ def get_features(args): print("features:", features) return features + +def build_flutter_deb(version): + os.chdir('flutter') + os.system('dpkg-deb -R rustdesk.deb tmpdeb') + # os.system('flutter build linux --release') + os.system('rm tmpdeb/usr/bin/rustdesk') + os.system('strip build/linux/x64/release/liblibrustdesk.so') + os.system('mkdir -p tmpdeb/usr/lib/rustdesk') + os.system('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/') + os.system( + 'cp -r build/linux/x64/release/bundle/* tmpdeb/usr/lib/rustdesk/') + os.system( + 'pushd tmpdeb && ln -s /usr/lib/rustdesk/flutter_hbb usr/bin/rustdesk && popd') + os.system( + 'cp build/linux/x64/release/liblibrustdesk.so tmpdeb/usr/lib/rustdesk/librustdesk.so') + os.system( + 'cp rustdesk.service tmpdeb/usr/share/rustdesk/files/systemd/') + os.system( + 'cp rustdesk.service.user tmpdeb/usr/share/rustdesk/files/systemd/') + os.system( + 'cp ../pynput_service.py tmpdeb/usr/share/rustdesk/files/') + os.system( + 'cp ../128x128@2x.png tmpdeb/usr/share/rustdesk/files/rustdesk.png') + os.system( + 'cp rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop') + os.system('mkdir -p tmpdeb/DEBIAN') + os.system('cp -a ../DEBIAN/* tmpdeb/DEBIAN/') + md5_file('usr/share/rustdesk/files/systemd/rustdesk.service') + md5_file('usr/share/rustdesk/files/systemd/rustdesk.service.user') + md5_file('usr/share/rustdesk/files/pynput_service.py') + os.system('dpkg-deb -b tmpdeb rustdesk.deb; /bin/rm -rf tmpdeb/') + os.rename('rustdesk.deb', '../rustdesk-%s.deb' % version) + os.chdir("..") + + +def build_flutter_arch_manjaro(version): + os.chdir('flutter') + os.system('flutter build linux --release') + os.system('strip build/linux/x64/release/liblibrustdesk.so') + os.system("sed -i 's/pkgver=.*/pkgver=%s/g' PKGBUILD" % version) + # pacman -S -needed base-devel + os.system('HBB=`pwd` makepkg -f') + os.system( + 'mv rustdesk-%s-0-x86_64.pkg.tar.zst ../rustdesk-%s-manjaro-arch.pkg.tar.zst' % (version, version)) + os.chdir('..') + + def main(): parser = make_parser() args = parser.parse_args() @@ -151,19 +198,11 @@ def main(): else: print('Not signed') os.system(f'cp -rf target/release/RustDesk.exe rustdesk-{version}-setdown.exe') - elif os.path.isfile('/usr/bin/pacman'): + elif os.path.isfile('/usr/bin/pacman1'): if flutter: - os.chdir('flutter') - os.system('flutter build linux --release') - os.system('strip build/linux/x64/release/liblibrustdesk.so') - os.system("sed -i 's/pkgver=.*/pkgver=%s/g' PKGBUILD" % version) - # pacman -S -needed base-devel - os.system('HBB=`pwd` makepkg -f') - os.system( - 'mv rustdesk-%s-0-x86_64.pkg.tar.zst ../rustdesk-%s-manjaro-arch.pkg.tar.zst' % (version, version)) - os.chdir('..') + build_flutter_arch_manjaro(version) else: - os.system('cargo build --release --features ' + features) + # os.system('cargo build --release --features ' + features) os.system('git checkout src/ui/common.tis') os.system('strip target/release/rustdesk') os.system("sed -i 's/pkgver=.*/pkgver=%s/g' PKGBUILD" % version) @@ -189,63 +228,75 @@ def main(): # yum localinstall rustdesk.rpm else: os.system('cargo bundle --release --features ' + features) - if osx: - os.system( - 'strip target/release/bundle/osx/RustDesk.app/Contents/MacOS/rustdesk') - os.system( - 'cp libsciter.dylib target/release/bundle/osx/RustDesk.app/Contents/MacOS/') - # https://github.com/sindresorhus/create-dmg - os.system('/bin/rm -rf *.dmg') - plist = "target/release/bundle/osx/RustDesk.app/Contents/Info.plist" - txt = open(plist).read() - with open(plist, "wt") as fh: - fh.write(txt.replace("", """ - LSUIElement - 1 -""")) - pa = os.environ.get('P') - if pa: - os.system(''' -# buggy: rcodesign sign ... path/*, have to sign one by one -#rcodesign sign --p12-file ~/.p12/rustdesk-developer-id.p12 --p12-password-file ~/.p12/.cert-pass --code-signature-flags runtime ./target/release/bundle/osx/RustDesk.app/Contents/MacOS/rustdesk -#rcodesign sign --p12-file ~/.p12/rustdesk-developer-id.p12 --p12-password-file ~/.p12/.cert-pass --code-signature-flags runtime ./target/release/bundle/osx/RustDesk.app/Contents/MacOS/libsciter.dylib -#rcodesign sign --p12-file ~/.p12/rustdesk-developer-id.p12 --p12-password-file ~/.p12/.cert-pass --code-signature-flags runtime ./target/release/bundle/osx/RustDesk.app -# goto "Keychain Access" -> "My Certificates" for below id which starts with "Developer ID Application:" -codesign -s "Developer ID Application: {0}" --force --options runtime ./target/release/bundle/osx/RustDesk.app/Contents/MacOS/* -codesign -s "Developer ID Application: {0}" --force --options runtime ./target/release/bundle/osx/RustDesk.app -'''.format(pa)) - os.system('create-dmg target/release/bundle/osx/RustDesk.app') - os.rename('RustDesk %s.dmg' % version, 'rustdesk-%s.dmg' % version) - if pa: - os.system(''' -#rcodesign sign --p12-file ~/.p12/rustdesk-developer-id.p12 --p12-password-file ~/.p12/.cert-pass --code-signature-flags runtime ./rustdesk-{1}.dmg -codesign -s "Developer ID Application: {0}" --force --options runtime ./rustdesk-{1}.dmg -# https://pyoxidizer.readthedocs.io/en/latest/apple_codesign_rcodesign.html -rcodesign notarize --api-issuer 69a6de7d-2907-47e3-e053-5b8c7c11a4d1 --api-key 9JBRHG3JHT --staple ./rustdesk-{1}.dmg -# verify: spctl -a -t exec -v /Applications/RustDesk.app -'''.format(pa, version)) + if flutter: + if osx: + # todo: OSX build + pass else: - print('Not signed') + os.system( + 'mv target/release/bundle/deb/rustdesk*.deb ./flutter/rustdesk.deb') + build_flutter_deb(version) else: - # buid deb package - os.system('mv target/release/bundle/deb/rustdesk*.deb ./rustdesk.deb') - os.system('dpkg-deb -R rustdesk.deb tmpdeb') - os.system('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/') - os.system( - 'cp rustdesk.service tmpdeb/usr/share/rustdesk/files/systemd/') - os.system( - 'cp rustdesk.service.user tmpdeb/usr/share/rustdesk/files/systemd/') - os.system('cp pynput_service.py tmpdeb/usr/share/rustdesk/files/') - os.system('cp -a DEBIAN/* tmpdeb/DEBIAN/') - os.system('strip tmpdeb/usr/bin/rustdesk') - os.system('mkdir -p tmpdeb/usr/lib/rustdesk') - os.system('cp libsciter-gtk.so tmpdeb/usr/lib/rustdesk/') - md5_file('usr/share/rustdesk/files/systemd/rustdesk.service') - md5_file('usr/share/rustdesk/files/systemd/rustdesk.service.user') - md5_file('usr/share/rustdesk/files/pynput_service.py') - md5_file('usr/lib/rustdesk/libsciter-gtk.so') - os.system('dpkg-deb -b tmpdeb rustdesk.deb; /bin/rm -rf tmpdeb/') - os.rename('rustdesk.deb', 'rustdesk-%s.deb' % version) + if osx: + os.system( + 'strip target/release/bundle/osx/RustDesk.app/Contents/MacOS/rustdesk') + os.system( + 'cp libsciter.dylib target/release/bundle/osx/RustDesk.app/Contents/MacOS/') + # https://github.com/sindresorhus/create-dmg + os.system('/bin/rm -rf *.dmg') + plist = "target/release/bundle/osx/RustDesk.app/Contents/Info.plist" + txt = open(plist).read() + with open(plist, "wt") as fh: + fh.write(txt.replace("", """ + LSUIElement + 1 + """)) + pa = os.environ.get('P') + if pa: + os.system(''' + # buggy: rcodesign sign ... path/*, have to sign one by one + #rcodesign sign --p12-file ~/.p12/rustdesk-developer-id.p12 --p12-password-file ~/.p12/.cert-pass --code-signature-flags runtime ./target/release/bundle/osx/RustDesk.app/Contents/MacOS/rustdesk + #rcodesign sign --p12-file ~/.p12/rustdesk-developer-id.p12 --p12-password-file ~/.p12/.cert-pass --code-signature-flags runtime ./target/release/bundle/osx/RustDesk.app/Contents/MacOS/libsciter.dylib + #rcodesign sign --p12-file ~/.p12/rustdesk-developer-id.p12 --p12-password-file ~/.p12/.cert-pass --code-signature-flags runtime ./target/release/bundle/osx/RustDesk.app + # goto "Keychain Access" -> "My Certificates" for below id which starts with "Developer ID Application:" + codesign -s "Developer ID Application: {0}" --force --options runtime ./target/release/bundle/osx/RustDesk.app/Contents/MacOS/* + codesign -s "Developer ID Application: {0}" --force --options runtime ./target/release/bundle/osx/RustDesk.app + '''.format(pa)) + os.system('create-dmg target/release/bundle/osx/RustDesk.app') + os.rename('RustDesk %s.dmg' % + version, 'rustdesk-%s.dmg' % version) + if pa: + os.system(''' + #rcodesign sign --p12-file ~/.p12/rustdesk-developer-id.p12 --p12-password-file ~/.p12/.cert-pass --code-signature-flags runtime ./rustdesk-{1}.dmg + codesign -s "Developer ID Application: {0}" --force --options runtime ./rustdesk-{1}.dmg + # https://pyoxidizer.readthedocs.io/en/latest/apple_codesign_rcodesign.html + rcodesign notarize --api-issuer 69a6de7d-2907-47e3-e053-5b8c7c11a4d1 --api-key 9JBRHG3JHT --staple ./rustdesk-{1}.dmg + # verify: spctl -a -t exec -v /Applications/RustDesk.app + '''.format(pa, version)) + else: + print('Not signed') + else: + # buid deb package + os.system( + 'mv target/release/bundle/deb/rustdesk*.deb ./rustdesk.deb') + os.system('dpkg-deb -R rustdesk.deb tmpdeb') + os.system('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/') + os.system( + 'cp rustdesk.service tmpdeb/usr/share/rustdesk/files/systemd/') + os.system( + 'cp rustdesk.service.user tmpdeb/usr/share/rustdesk/files/systemd/') + os.system( + 'cp pynput_service.py tmpdeb/usr/share/rustdesk/files/') + os.system('cp -a DEBIAN/* tmpdeb/DEBIAN/') + os.system('strip tmpdeb/usr/bin/rustdesk') + os.system('mkdir -p tmpdeb/usr/lib/rustdesk') + os.system('cp libsciter-gtk.so tmpdeb/usr/lib/rustdesk/') + md5_file('usr/share/rustdesk/files/systemd/rustdesk.service') + md5_file('usr/share/rustdesk/files/systemd/rustdesk.service.user') + md5_file('usr/share/rustdesk/files/pynput_service.py') + md5_file('usr/lib/rustdesk/libsciter-gtk.so') + os.system('dpkg-deb -b tmpdeb rustdesk.deb; /bin/rm -rf tmpdeb/') + os.rename('rustdesk.deb', 'rustdesk-%s.deb' % version) os.system("mv Cargo.toml.bk Cargo.toml") os.system("mv src/main.rs.bk src/main.rs") diff --git a/flutter/rustdesk.desktop b/flutter/rustdesk.desktop index aca57eeff..c94285bbd 100644 --- a/flutter/rustdesk.desktop +++ b/flutter/rustdesk.desktop @@ -1,5 +1,5 @@ [Desktop Entry] -Version=1.2.0 +Version=1.1.10 Name=RustDesk GenericName=Remote Desktop Comment=Remote Desktop diff --git a/rpm-suse.spec b/rpm-suse.spec index 16c81ae90..73a610c11 100644 --- a/rpm-suse.spec +++ b/rpm-suse.spec @@ -23,7 +23,7 @@ mkdir -p %{buildroot}/usr/share/rustdesk/files/ install -m 755 $HBB/target/release/rustdesk %{buildroot}/usr/bin/rustdesk install $HBB/libsciter-gtk.so %{buildroot}/usr/lib/rustdesk/libsciter-gtk.so install $HBB/rustdesk.service %{buildroot}/usr/share/rustdesk/files/ -install $HBB/256-no-margin.png %{buildroot}/usr/share/rustdesk/files/rustdesk.png +install $HBB/128x128@2x.png %{buildroot}/usr/share/rustdesk/files/rustdesk.png install $HBB/rustdesk.desktop %{buildroot}/usr/share/rustdesk/files/ install $HBB/pynput_service.py %{buildroot}/usr/share/rustdesk/files/ diff --git a/rpm.spec b/rpm.spec index 707f0381a..c61db5d0b 100644 --- a/rpm.spec +++ b/rpm.spec @@ -23,7 +23,7 @@ mkdir -p %{buildroot}/usr/share/rustdesk/files/ install -m 755 $HBB/target/release/rustdesk %{buildroot}/usr/bin/rustdesk install $HBB/libsciter-gtk.so %{buildroot}/usr/lib/rustdesk/libsciter-gtk.so install $HBB/rustdesk.service %{buildroot}/usr/share/rustdesk/files/ -install $HBB/256-no-margin.png %{buildroot}/usr/share/rustdesk/files/rustdesk.png +install $HBB/128x128@2x.png %{buildroot}/usr/share/rustdesk/files/rustdesk.png install $HBB/rustdesk.desktop %{buildroot}/usr/share/rustdesk/files/ install $HBB/pynput_service.py %{buildroot}/usr/share/rustdesk/files/ From 25b052ef17e7d862c265389dc4202b9e4fc74a30 Mon Sep 17 00:00:00 2001 From: Zachary Locklear Date: Thu, 25 Aug 2022 09:57:12 -0600 Subject: [PATCH 0289/2015] Grammatical correction for closing dialog. --- flutter/lib/mobile/widgets/dialog.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/flutter/lib/mobile/widgets/dialog.dart b/flutter/lib/mobile/widgets/dialog.dart index d648cd497..4169eecdf 100644 --- a/flutter/lib/mobile/widgets/dialog.dart +++ b/flutter/lib/mobile/widgets/dialog.dart @@ -6,7 +6,8 @@ import '../../models/model.dart'; import '../../models/platform_model.dart'; void clientClose(OverlayDialogManager dialogManager) { - msgBox('', 'Close', 'Are you sure to close the connection?', dialogManager); + msgBox('', 'Close', 'Are you sure you want to close the connection?', + dialogManager); } void showSuccess() { From c04168eb738620f6002121e164b4c5eb998a73bd Mon Sep 17 00:00:00 2001 From: csf Date: Fri, 26 Aug 2022 12:00:53 +0800 Subject: [PATCH 0290/2015] add flutter_lints --- flutter/pubspec.lock | 38 ++++++++++++++++++++++++++------------ flutter/pubspec.yaml | 2 ++ 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 0aca2aab1..07862bf38 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -49,7 +49,7 @@ packages: name: async url: "https://pub.flutter-io.cn" source: hosted - version: "2.9.0" + version: "2.8.2" back_button_interceptor: dependency: "direct main" description: @@ -147,7 +147,7 @@ packages: name: characters url: "https://pub.flutter-io.cn" source: hosted - version: "1.2.1" + version: "1.2.0" charcode: dependency: transitive description: @@ -168,7 +168,7 @@ packages: name: clock url: "https://pub.flutter-io.cn" source: hosted - version: "1.1.1" + version: "1.1.0" code_builder: dependency: transitive description: @@ -326,7 +326,7 @@ packages: name: fake_async url: "https://pub.flutter-io.cn" source: hosted - version: "1.3.1" + version: "1.3.0" ffi: dependency: "direct main" description: @@ -423,6 +423,13 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "0.9.3" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.1" flutter_parsed_text: dependency: transitive description: @@ -596,6 +603,13 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "4.6.0" + lints: + dependency: transitive + description: + name: lints + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.0" logging: dependency: transitive description: @@ -609,14 +623,14 @@ packages: name: matcher url: "https://pub.flutter-io.cn" source: hosted - version: "0.12.12" + version: "0.12.11" material_color_utilities: dependency: transitive description: name: material_color_utilities url: "https://pub.flutter-io.cn" source: hosted - version: "0.1.5" + version: "0.1.4" menu_base: dependency: transitive description: @@ -630,7 +644,7 @@ packages: name: meta url: "https://pub.flutter-io.cn" source: hosted - version: "1.8.0" + version: "1.7.0" mime: dependency: transitive description: @@ -707,7 +721,7 @@ packages: name: path url: "https://pub.flutter-io.cn" source: hosted - version: "1.8.2" + version: "1.8.1" path_provider: dependency: "direct main" description: @@ -957,7 +971,7 @@ packages: name: source_span url: "https://pub.flutter-io.cn" source: hosted - version: "1.9.0" + version: "1.8.2" sqflite: dependency: transitive description: @@ -999,7 +1013,7 @@ packages: name: string_scanner url: "https://pub.flutter-io.cn" source: hosted - version: "1.1.1" + version: "1.1.0" synchronized: dependency: transitive description: @@ -1013,14 +1027,14 @@ packages: name: term_glyph url: "https://pub.flutter-io.cn" source: hosted - version: "1.2.1" + version: "1.2.0" test_api: dependency: transitive description: name: test_api url: "https://pub.flutter-io.cn" source: hosted - version: "0.4.12" + version: "0.4.9" timing: dependency: transitive description: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 06231f8bf..93c2f64b2 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -83,6 +83,8 @@ dev_dependencies: sdk: flutter build_runner: ^2.1.11 freezed: ^2.0.3 + flutter_lints: ^2.0.0 + # rerun: flutter pub run flutter_launcher_icons:main flutter_icons: android: "ic_launcher" From 14f34f589ca54ff7843940e0828c601e5ce11ccb Mon Sep 17 00:00:00 2001 From: csf Date: Fri, 26 Aug 2022 12:14:14 +0800 Subject: [PATCH 0291/2015] fix tab dispose bug, add Key for PageView children --- flutter/lib/desktop/pages/connection_tab_page.dart | 5 +++-- flutter/lib/desktop/pages/desktop_tab_page.dart | 8 +++++--- flutter/lib/desktop/pages/file_manager_tab_page.dart | 6 +++--- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/flutter/lib/desktop/pages/connection_tab_page.dart b/flutter/lib/desktop/pages/connection_tab_page.dart index 66f342919..be7c76f2a 100644 --- a/flutter/lib/desktop/pages/connection_tab_page.dart +++ b/flutter/lib/desktop/pages/connection_tab_page.dart @@ -30,12 +30,13 @@ class _ConnectionTabPageState extends State { _ConnectionTabPageState(Map params) { if (params['id'] != null) { - tabController.state.value.tabs.add(TabInfo( + tabController.add(TabInfo( key: params['id'], label: params['id'], selectedIcon: selectedIcon, unselectedIcon: unselectedIcon, page: RemotePage( + key: ValueKey(params['id']), id: params['id'], tabBarHeight: _fullscreenID.value.isNotEmpty ? 0 : kDesktopRemoteTabBarHeight, @@ -63,8 +64,8 @@ class _ConnectionTabPageState extends State { label: id, selectedIcon: selectedIcon, unselectedIcon: unselectedIcon, - closable: false, page: RemotePage( + key: ValueKey(id), id: id, tabBarHeight: _fullscreenID.value.isNotEmpty ? 0 diff --git a/flutter/lib/desktop/pages/desktop_tab_page.dart b/flutter/lib/desktop/pages/desktop_tab_page.dart index 2504c699f..4a2fdb7d2 100644 --- a/flutter/lib/desktop/pages/desktop_tab_page.dart +++ b/flutter/lib/desktop/pages/desktop_tab_page.dart @@ -19,13 +19,15 @@ class _DesktopTabPageState extends State { @override void initState() { super.initState(); - tabController.state.value.tabs.add(TabInfo( + tabController.add(TabInfo( key: kTabLabelHomePage, label: kTabLabelHomePage, selectedIcon: Icons.home_sharp, unselectedIcon: Icons.home_outlined, closable: false, - page: DesktopHomePage())); + page: DesktopHomePage( + key: const ValueKey(kTabLabelHomePage), + ))); } @override @@ -59,6 +61,6 @@ class _DesktopTabPageState extends State { label: kTabLabelSettingPage, selectedIcon: Icons.build_sharp, unselectedIcon: Icons.build_outlined, - page: DesktopSettingPage())); + page: DesktopSettingPage(key: const ValueKey(kTabLabelSettingPage)))); } } diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index 7ae8e36b3..09577128f 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -26,12 +26,12 @@ class _FileManagerTabPageState extends State { static final IconData unselectedIcon = Icons.file_copy_outlined; _FileManagerTabPageState(Map params) { - tabController.state.value.tabs.add(TabInfo( + tabController.add(TabInfo( key: params['id'], label: params['id'], selectedIcon: selectedIcon, unselectedIcon: unselectedIcon, - page: FileManagerPage(id: params['id']))); + page: FileManagerPage(key: ValueKey(params['id']), id: params['id']))); } @override @@ -53,7 +53,7 @@ class _FileManagerTabPageState extends State { label: id, selectedIcon: selectedIcon, unselectedIcon: unselectedIcon, - page: FileManagerPage(id: id))); + page: FileManagerPage(key: ValueKey(id), id: id))); } else if (call.method == "onDestroy") { tabController.state.value.tabs.forEach((tab) { print("executing onDestroy hook, closing ${tab.label}}"); From 343be3ddf2044b03684f044b08dee85b8e1f603a Mon Sep 17 00:00:00 2001 From: csf Date: Fri, 26 Aug 2022 13:02:15 +0800 Subject: [PATCH 0292/2015] fix peer card double click --- .../lib/desktop/widgets/peercard_widget.dart | 80 +++++++------------ 1 file changed, 29 insertions(+), 51 deletions(-) diff --git a/flutter/lib/desktop/widgets/peercard_widget.dart b/flutter/lib/desktop/widgets/peercard_widget.dart index d39f3d359..433ca9284 100644 --- a/flutter/lib/desktop/widgets/peercard_widget.dart +++ b/flutter/lib/desktop/widgets/peercard_widget.dart @@ -35,9 +35,10 @@ class _PeerCard extends StatefulWidget { /// State for the connection page. class _PeerCardState extends State<_PeerCard> with AutomaticKeepAliveClientMixin { - var _menuPos; + var _menuPos = RelativeRect.fill; final double _cardRadis = 20; final double _borderWidth = 2; + final RxBool _iconMoreHover = false.obs; @override Widget build(BuildContext context) { @@ -64,7 +65,7 @@ class _PeerCardState extends State<_PeerCard> : null); }, child: GestureDetector( - onDoubleTapDown: (_) => _connect(peer.id), + onDoubleTap: () => _connect(peer.id), child: Obx(() => peerCardUiType.value == PeerUiType.grid ? _buildPeerCard(context, peer, deco) : _buildPeerTile(context, peer, deco))), @@ -75,7 +76,6 @@ class _PeerCardState extends State<_PeerCard> BuildContext context, Peer peer, Rx deco) { final greyStyle = TextStyle(fontSize: 12, color: MyTheme.color(context).lighterText); - RxBool iconHover = false.obs; return Obx( () => Container( foregroundDecoration: deco.value, @@ -147,30 +147,7 @@ class _PeerCardState extends State<_PeerCard> ], ), ), - InkWell( - child: CircleAvatar( - radius: 12, - backgroundColor: iconHover.value - ? MyTheme.color(context).grayBg! - : MyTheme.color(context).bg!, - child: Icon( - Icons.more_vert, - size: 18, - color: iconHover.value - ? MyTheme.color(context).text - : MyTheme.color(context).lightText, - ), - ), - onTapDown: (e) { - final x = e.globalPosition.dx; - final y = e.globalPosition.dy; - _menuPos = RelativeRect.fromLTRB(x, y, x, y); - }, - onTap: () { - _showPeerMenu(context, peer.id); - }, - onHover: (value) => iconHover.value = value, - ), + _actionMore(peer), ], ).paddingSymmetric(horizontal: 4.0), ), @@ -183,7 +160,6 @@ class _PeerCardState extends State<_PeerCard> Widget _buildPeerCard( BuildContext context, Peer peer, Rx deco) { - RxBool iconHover = false.obs; return Card( color: Colors.transparent, elevation: 0, @@ -272,29 +248,10 @@ class _PeerCardState extends State<_PeerCard> ? Colors.green : Colors.yellow)), Text('${peer.id}') - ]), - InkWell( - child: CircleAvatar( - radius: 12, - backgroundColor: iconHover.value - ? MyTheme.color(context).grayBg! - : MyTheme.color(context).bg!, - child: Icon(Icons.more_vert, - size: 18, - color: iconHover.value - ? MyTheme.color(context).text - : MyTheme.color(context).lightText)), - onTapDown: (e) { - final x = e.globalPosition.dx; - final y = e.globalPosition.dy; - _menuPos = RelativeRect.fromLTRB(x, y, x, y); - }, - onTap: () { - _showPeerMenu(context, peer.id); - }, - onHover: (value) => iconHover.value = value), + ]).paddingSymmetric(vertical: 8), + _actionMore(peer), ], - ).paddingSymmetric(vertical: 8.0, horizontal: 12.0), + ).paddingSymmetric(horizontal: 12.0), ) ], ), @@ -304,6 +261,27 @@ class _PeerCardState extends State<_PeerCard> ); } + Widget _actionMore(Peer peer) => Listener( + onPointerDown: (e) { + final x = e.position.dx; + final y = e.position.dy; + _menuPos = RelativeRect.fromLTRB(x, y, x, y); + }, + onPointerUp: (_) => _showPeerMenu(context, peer.id), + child: MouseRegion( + onEnter: (_) => _iconMoreHover.value = true, + onExit: (_) => _iconMoreHover.value = false, + child: CircleAvatar( + radius: 14, + backgroundColor: _iconMoreHover.value + ? MyTheme.color(context).grayBg! + : MyTheme.color(context).bg!, + child: Icon(Icons.more_vert, + size: 18, + color: _iconMoreHover.value + ? MyTheme.color(context).text + : MyTheme.color(context).lightText)))); + /// Connect to a peer with [id]. /// If [isFileTransfer], starts a session only for file transfer. void _connect(String id, {bool isFileTransfer = false}) async { @@ -325,7 +303,7 @@ class _PeerCardState extends State<_PeerCard> void _showPeerMenu(BuildContext context, String id) async { var value = await showMenu( context: context, - position: this._menuPos, + position: _menuPos, items: await super.widget.popupMenuItemsFunc(), elevation: 8, ); From f830b395b97358fcccc7cd96ce94fe31fcd42c26 Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Fri, 26 Aug 2022 17:07:11 +0800 Subject: [PATCH 0293/2015] Update README.md --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 79a4b18d3..b189167dc 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,9 @@ Below are the servers you are using for free, it may change along the time. If y ## Dependencies -Desktop versions use [sciter](https://sciter.com/) for GUI, please download sciter dynamic library yourself. +Desktop versions use [sciter](https://sciter.com/) or Flutter for GUI, this tutorial is for Sciter only. + +Please download sciter dynamic library yourself. [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) | From c6bcc9a0995f941a17c835d8b9717114c4b4beec Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Fri, 26 Aug 2022 17:09:04 +0800 Subject: [PATCH 0294/2015] Update README.md --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index b189167dc..456862af5 100644 --- a/README.md +++ b/README.md @@ -47,8 +47,6 @@ Please download sciter dynamic library yourself. [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) -Mobile versions use Flutter. We will migrate desktop version from Sciter to Flutter. - ## Raw steps to build - Prepare your Rust development env and C++ build env From ff5e9a8ea5672cd07f1a3c83b00a66227eb6ee67 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sat, 27 Aug 2022 00:45:09 +0800 Subject: [PATCH 0295/2015] opt: support match user/hostname/id(flutter), case insensitive Signed-off-by: Kingtous --- flutter/lib/common.dart | 35 +++++++++ flutter/lib/desktop/widgets/peer_widget.dart | 81 ++++++++++++-------- 2 files changed, 82 insertions(+), 34 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 643705d69..349b5abcc 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -8,6 +8,7 @@ import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; +import 'package:flutter_hbb/models/peer_model.dart'; import 'package:get/get.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:window_manager/window_manager.dart'; @@ -704,3 +705,37 @@ String bool2option(String option, bool b) { } return res; } + +Future matchPeer(String searchText, Peer peer) async { + if (searchText.isEmpty) { + return true; + } + if (peer.id.toLowerCase().contains(searchText)) { + return true; + } + if (peer.hostname.toLowerCase().contains(searchText) || + peer.username.toLowerCase().contains(searchText)) { + return true; + } + final alias = await bind.mainGetPeerOption(id: peer.id, key: 'alias'); + if (alias.isEmpty) { + return false; + } + return alias.toLowerCase().contains(searchText); +} + +Future>? matchPeers(String searchText, List peers) async { + if (searchText.isEmpty) { + return peers; + } + searchText = searchText.toLowerCase(); + final matches = + await Future.wait(peers.map((peer) => matchPeer(searchText, peer))); + final filteredList = List.empty(growable: true); + for (var i = 0; i < peers.length; i++) { + if (matches[i]) { + filteredList.add(peers[i]); + } + } + return filteredList; +} \ No newline at end of file diff --git a/flutter/lib/desktop/widgets/peer_widget.dart b/flutter/lib/desktop/widgets/peer_widget.dart index fa79db624..3bfff60bf 100644 --- a/flutter/lib/desktop/widgets/peer_widget.dart +++ b/flutter/lib/desktop/widgets/peer_widget.dart @@ -88,40 +88,53 @@ class _PeerWidgetState extends State<_PeerWidget> with WindowListener { ) : SingleChildScrollView( child: ObxValue((searchText) { - final cards = []; - peers.peers.where((peer) { - if (searchText.isEmpty) { - return true; - } else { - return peer.id.contains(peerSearchText.value); - } - }).forEach((peer) { - cards.add(Offstage( - offstage: super.widget._offstageFunc(peer), - child: Obx( - () => Container( - width: 220, - height: peerCardUiType.value == PeerUiType.grid - ? 140 - : 42, - child: VisibilityDetector( - key: Key('${peer.id}'), - onVisibilityChanged: (info) { - final peerId = (info.key as ValueKey).value; - if (info.visibleFraction > 0.00001) { - _curPeers.add(peerId); - } else { - _curPeers.remove(peerId); - } - _lastChangeTime = DateTime.now(); - }, - child: super.widget._peerCardWidgetFunc(peer), - ), - ), - ))); - }); - return Wrap( - children: cards, spacing: space, runSpacing: space); + return FutureBuilder>( + builder: (context, snapshot) { + if (snapshot.hasData) { + final peers = snapshot.data!; + final cards = []; + for (final peer in peers) { + cards.add(Offstage( + key: ValueKey("off${peer.id}"), + offstage: super.widget._offstageFunc(peer), + child: Obx( + () => SizedBox( + width: 220, + height: + peerCardUiType.value == PeerUiType.grid + ? 140 + : 42, + child: VisibilityDetector( + key: ValueKey(peer.id), + onVisibilityChanged: (info) { + final peerId = + (info.key as ValueKey).value; + if (info.visibleFraction > 0.00001) { + _curPeers.add(peerId); + } else { + _curPeers.remove(peerId); + } + _lastChangeTime = DateTime.now(); + }, + child: super + .widget + ._peerCardWidgetFunc(peer), + ), + ), + ))); + } + return Wrap( + spacing: space, + runSpacing: space, + children: cards); + } else { + return const Center( + child: CircularProgressIndicator(), + ); + } + }, + future: matchPeers(searchText.value, peers.peers), + ); }, peerSearchText), )), ); From 4e047f1bb27c16d1bc459198cb9ccfbebacd9f40 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sat, 27 Aug 2022 01:03:20 +0800 Subject: [PATCH 0296/2015] opt: support match user/hostname/id(sciter), case insensitive Signed-off-by: Kingtous --- flutter/lib/common.dart | 3 ++- src/ui/ab.tis | 11 ++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 349b5abcc..17e45ba95 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -725,6 +725,7 @@ Future matchPeer(String searchText, Peer peer) async { } Future>? matchPeers(String searchText, List peers) async { + searchText = searchText.trim(); if (searchText.isEmpty) { return peers; } @@ -738,4 +739,4 @@ Future>? matchPeers(String searchText, List peers) async { } } return filteredList; -} \ No newline at end of file +} diff --git a/src/ui/ab.tis b/src/ui/ab.tis index 658783623..ac2efb7dd 100644 --- a/src/ui/ab.tis +++ b/src/ui/ab.tis @@ -245,7 +245,7 @@ class SearchBar: Reactor.Component { } event change $(input) (_, el) { - this.onChange(el.value.trim()); + this.onChange(el.value.trim().toLowerCase()); } function onChange(v) { @@ -297,8 +297,13 @@ class SessionList: Reactor.Component { if (!p) return this.sessions; var tmp = []; this.sessions.map(function(s) { - var name = s[4] || s.alias || s[0] || s.id || ""; - if (name.indexOf(p) >= 0) tmp.push(s); + var name = (s[4] || s.alias || "").toLowerCase(); + var id = (s[0] || s.id || "").toLowerCase(); + var user = (s[1] || "").toLowerCase(); + var hostname = (s[2] || "").toLowerCase(); + if (name.indexOf(p) >= 0 || id.indexOf(p) >= 0 || user.indexOf(p) >= 0 || hostname.indexOf(p) >= 0) { + tmp.push(s); + } }); return tmp; } From ee19a03ecc8172754d6605e3f5631d9cb949bc8e Mon Sep 17 00:00:00 2001 From: Asura Date: Fri, 26 Aug 2022 18:29:46 -0700 Subject: [PATCH 0297/2015] Convert keycode to RdevKey --- Cargo.lock | 75 ++++++------- Cargo.toml | 2 +- flutter/lib/desktop/pages/remote_page.dart | 120 ++++++++++++++------- flutter/lib/models/model.dart | 6 ++ src/client.rs | 2 +- src/flutter.rs | 10 ++ src/flutter_ffi.rs | 6 ++ src/ui/cm.rs | 2 +- 8 files changed, 147 insertions(+), 76 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a037b4e33..3029e51ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -627,21 +627,6 @@ dependencies = [ "cc", ] -[[package]] -name = "cocoa" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "667fdc068627a2816b9ff831201dd9864249d6ee8d190b9532357f1fc0f61ea7" -dependencies = [ - "bitflags", - "block", - "core-foundation 0.9.3", - "core-graphics 0.21.0", - "foreign-types", - "libc", - "objc", -] - [[package]] name = "cocoa" version = "0.24.0" @@ -759,18 +744,6 @@ dependencies = [ "libc", ] -[[package]] -name = "core-graphics" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52a67c4378cf203eace8fb6567847eb641fd6ff933c1145a115c6ee820ebb978" -dependencies = [ - "bitflags", - "core-foundation 0.9.3", - "foreign-types", - "libc", -] - [[package]] name = "core-graphics" version = "0.22.3" @@ -1283,6 +1256,26 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "enum-map" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5a56d54c8dd9b3ad34752ed197a4eb2a6601bc010808eb097a04a58ae4c43e1" +dependencies = [ + "enum-map-derive", +] + +[[package]] +name = "enum-map-derive" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9045e2676cd5af83c3b167d917b0a5c90a4d8e266e2683d6631b235c457fc27" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "enum_dispatch" version = "0.3.8" @@ -3741,15 +3734,17 @@ dependencies = [ [[package]] name = "rdev" -version = "0.5.0" -source = "git+https://github.com/open-trade/rdev#fbbefd0b5d87095a7349965aec9ecd33de7035ac" +version = "0.5.0-2" +source = "git+https://github.com/asur4s/rdev#895c8fb1a6106714793e8877d35d2b7a1c57ce9c" dependencies = [ - "cocoa 0.22.0", - "core-foundation 0.7.0", - "core-foundation-sys 0.7.0", - "core-graphics 0.19.2", + "cocoa", + "core-foundation 0.9.3", + "core-foundation-sys 0.8.3", + "core-graphics 0.22.3", + "enum-map", "lazy_static", "libc", + "widestring 1.0.2", "winapi 0.3.9", "x11", ] @@ -3967,7 +3962,7 @@ dependencies = [ "cfg-if 1.0.0", "clap 3.1.18", "clipboard", - "cocoa 0.24.0", + "cocoa", "core-foundation 0.9.3", "core-graphics 0.22.3", "cpal", @@ -4939,7 +4934,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76863575f7842ed64fda361f417a787efa82811b4617267709066969cd4ccf3b" dependencies = [ - "cocoa 0.24.0", + "cocoa", "core-graphics 0.22.3", "gtk", "libappindicator", @@ -5357,6 +5352,12 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c168940144dd21fd8046987c16a46a33d5fc84eec29ef9dcddc2ac9e31526b7c" +[[package]] +name = "widestring" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "653f141f39ec16bba3c5abe400a0c60da7468261cc2cbf36805022876bc721a8" + [[package]] name = "winapi" version = "0.2.8" @@ -5430,7 +5431,7 @@ checksum = "0c643e10139d127d30d6d753398c8a6f0a43532e8370f6c9d29ebbff29b984ab" dependencies = [ "bitflags", "err-derive", - "widestring", + "widestring 0.4.3", "winapi 0.3.9", ] @@ -5557,7 +5558,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b43cc931d58b99461188607efd7acb2a093e65fc621f54cad78517a6063e73a" dependencies = [ "bitflags", - "cocoa 0.24.0", + "cocoa", "core-foundation 0.9.3", "core-graphics 0.22.3", "core-video-sys", diff --git a/Cargo.toml b/Cargo.toml index d65c9dde3..34ba9d2c8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -74,7 +74,7 @@ sciter-rs = { git = "https://github.com/open-trade/rust-sciter", branch = "dyn" sys-locale = "0.2" enigo = { path = "libs/enigo", features = [ "with_serde" ] } clipboard = { path = "libs/clipboard" } -rdev = { git = "https://github.com/open-trade/rdev" } +rdev = { git = "https://github.com/asur4s/rdev" } ctrlc = "3.2" arboard = "2.0" #minreq = { version = "2.4", features = ["punycode", "https-native"] } diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 025db279f..c121fe1ab 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -234,6 +234,89 @@ class _RemotePageState extends State buildBody(context, ffiModel)))); } + KeyEventResult handleRawKeyEvent(FocusNode data, RawKeyEvent e) { + String? keyboardMode = Platform.environment['KEYBOARD_MODE']; + keyboardMode ??= 'legacy'; + + if (keyboardMode == 'map') { + mapKeyboardMode(e); + } else if (keyboardMode == 'translate') { + legacyKeyboardMode(e); + } else { + legacyKeyboardMode(e); + } + + return KeyEventResult.handled; + } + + void mapKeyboardMode(RawKeyEvent e) { + int scanCode; + int keyCode; + bool down; + + if (e.data is RawKeyEventDataMacOs) { + RawKeyEventDataMacOs newData = e.data as RawKeyEventDataMacOs; + scanCode = newData.keyCode; + keyCode = newData.keyCode; + } else if (e.data is RawKeyEventDataWindows) { + RawKeyEventDataWindows newData = e.data as RawKeyEventDataWindows; + scanCode = newData.scanCode; + keyCode = newData.keyCode; + } else if (e.data is RawKeyEventDataLinux) { + RawKeyEventDataLinux newData = e.data as RawKeyEventDataLinux; + scanCode = newData.scanCode; + keyCode = newData.keyCode; + debugPrint(newData.unicodeScalarValues.toString()); + } else { + scanCode = -1; + keyCode = -1; + } + + if (e is RawKeyDownEvent){ + down = true; + }else{ + down = false; + } + + _ffi.inputRawKey(keyCode, scanCode, down); + } + + void legacyKeyboardMode(RawKeyEvent e) { + final key = e.logicalKey; + if (e is RawKeyDownEvent) { + if (e.repeat) { + sendRawKey(e, press: true); + } else { + if (e.isAltPressed && !_ffi.alt) { + _ffi.alt = true; + } else if (e.isControlPressed && !_ffi.ctrl) { + _ffi.ctrl = true; + } else if (e.isShiftPressed && !_ffi.shift) { + _ffi.shift = true; + } else if (e.isMetaPressed && !_ffi.command) { + _ffi.command = true; + } + sendRawKey(e, down: true); + } + } + if (e is RawKeyUpEvent) { + if (key == LogicalKeyboardKey.altLeft || + key == LogicalKeyboardKey.altRight) { + _ffi.alt = false; + } else if (key == LogicalKeyboardKey.controlLeft || + key == LogicalKeyboardKey.controlRight) { + _ffi.ctrl = false; + } else if (key == LogicalKeyboardKey.shiftRight || + key == LogicalKeyboardKey.shiftLeft) { + _ffi.shift = false; + } else if (key == LogicalKeyboardKey.metaLeft || + key == LogicalKeyboardKey.metaRight) { + _ffi.command = false; + } + sendRawKey(e); + } + } + Widget getRawPointerAndKeyBody(Widget child) { return Consumer( builder: (context, FfiModel, _child) => MouseRegion( @@ -249,42 +332,7 @@ class _RemotePageState extends State onFocusChange: (bool v) { _imageFocused = v; }, - onKey: (data, e) { - final key = e.logicalKey; - if (e is RawKeyDownEvent) { - if (e.repeat) { - sendRawKey(e, press: true); - } else { - if (e.isAltPressed && !_ffi.alt) { - _ffi.alt = true; - } else if (e.isControlPressed && !_ffi.ctrl) { - _ffi.ctrl = true; - } else if (e.isShiftPressed && !_ffi.shift) { - _ffi.shift = true; - } else if (e.isMetaPressed && !_ffi.command) { - _ffi.command = true; - } - sendRawKey(e, down: true); - } - } - if (e is RawKeyUpEvent) { - if (key == LogicalKeyboardKey.altLeft || - key == LogicalKeyboardKey.altRight) { - _ffi.alt = false; - } else if (key == LogicalKeyboardKey.controlLeft || - key == LogicalKeyboardKey.controlRight) { - _ffi.ctrl = false; - } else if (key == LogicalKeyboardKey.shiftRight || - key == LogicalKeyboardKey.shiftLeft) { - _ffi.shift = false; - } else if (key == LogicalKeyboardKey.metaLeft || - key == LogicalKeyboardKey.metaRight) { - _ffi.command = false; - } - sendRawKey(e); - } - return KeyEventResult.handled; - }, + onKey: handleRawKeyEvent, child: child)))); } diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index dda22a779..889e13d21 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -980,6 +980,12 @@ class FFI { msg: json.encode(modify({'type': type, 'buttons': button.value}))); } + // Raw Key + void inputRawKey(int keyCode, int scanCode, bool down){ + debugPrint(scanCode.toString()); + bind.sessionInputRawKey(id: id, keycode: keyCode, scancode: scanCode, down: down); + } + /// Send key stroke event. /// [down] indicates the key's state(down or up). /// [press] indicates a click event(down and up). diff --git a/src/client.rs b/src/client.rs index 3c1e5c3c3..f3877e530 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1649,7 +1649,7 @@ pub enum Data { } /// Keycode for key events. -#[derive(Clone)] +#[derive(Clone, Debug)] pub enum Key { ControlKey(ControlKey), Chr(u32), diff --git a/src/flutter.rs b/src/flutter.rs index d64540cda..559e7e0ad 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -373,6 +373,16 @@ impl Session { } } + pub fn input_raw_key(&self, keycode: i32, scancode: i32, down: bool){ + use rdev::{EventType::*, Key as RdevKey, *}; + if scancode < 0 || keycode < 0{ + return; + } + let key = rdev::key_from_scancode(scancode.try_into().unwrap()) as RdevKey; + + log::info!("{:?}", key); + } + /// Input a string of text. /// String is parsed into individual key presses. /// diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 53e3f1ff8..dee3f9dae 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -208,6 +208,12 @@ pub fn session_switch_display(id: String, value: i32) { } } +pub fn session_input_raw_key(id: String, keycode: i32, scancode:i32, down: bool){ + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.input_raw_key(keycode, scancode, down); + } +} + pub fn session_input_key( id: String, name: String, diff --git a/src/ui/cm.rs b/src/ui/cm.rs index f722f8372..d9a1efc1f 100644 --- a/src/ui/cm.rs +++ b/src/ui/cm.rs @@ -1,5 +1,5 @@ use crate::ipc::{self, new_listener, Connection, Data}; -#[cfg(windows)] +#[cfg(target_os = "linux")] use crate::ipc::{start_pa}; #[cfg(windows)] use crate::ipc::start_clipboard_file; From 67a95cf864768996a8f3141f56c0110772fa14d1 Mon Sep 17 00:00:00 2001 From: Asura Date: Sat, 27 Aug 2022 10:01:04 +0800 Subject: [PATCH 0298/2015] Fix compile error on MacOS --- src/ui/remote.rs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 9294c6e2b..659f3f05a 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -42,14 +42,6 @@ use hbb_common::{ }, Stream, }; -use hbb_common::{config::TransferSerde, fs::TransferJobMeta}; -use hbb_common::{ - fs::{ - can_enable_overwrite_detection, get_job, get_string, new_send_confirm, DigestCheckResult, - RemoveJobMeta, - }, - get_version_number, -}; use rdev::{Event, EventType::*, Key as RdevKey, Keyboard as RdevKeyboard, KeyboardState}; #[cfg(windows)] From b3f83b98c7f2c23fd2889d1901f6f9697652eeea Mon Sep 17 00:00:00 2001 From: Asura Date: Fri, 26 Aug 2022 21:50:21 -0700 Subject: [PATCH 0299/2015] Fix flutter pub hostname --- flutter/pubspec.lock | 346 +++++++++++++++++++++---------------------- 1 file changed, 173 insertions(+), 173 deletions(-) diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index c4492cace..4d0a07287 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -5,231 +5,231 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "46.0.0" after_layout: dependency: transitive description: name: after_layout - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.0" analyzer: dependency: transitive description: name: analyzer - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "4.6.0" animations: dependency: transitive description: name: animations - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.4" archive: dependency: transitive description: name: archive - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.3.1" args: dependency: transitive description: name: args - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.3.1" async: dependency: transitive description: name: async - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.8.2" back_button_interceptor: dependency: "direct main" description: name: back_button_interceptor - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "6.0.1" boolean_selector: dependency: transitive description: name: boolean_selector - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0" build: dependency: transitive description: name: build - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.3.0" build_config: dependency: transitive description: name: build_config - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.0" build_daemon: dependency: transitive description: name: build_daemon - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.0" build_resolvers: dependency: transitive description: name: build_resolvers - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.9" build_runner: dependency: "direct dev" description: name: build_runner - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.0" build_runner_core: dependency: transitive description: name: build_runner_core - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "7.2.3" built_collection: dependency: transitive description: name: built_collection - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "5.1.1" built_value: dependency: transitive description: name: built_value - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "8.4.1" cached_network_image: dependency: transitive description: name: cached_network_image - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.2.1" cached_network_image_platform_interface: dependency: transitive description: name: cached_network_image_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.0" cached_network_image_web: dependency: transitive description: name: cached_network_image_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.1" characters: dependency: transitive description: name: characters - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.0" charcode: dependency: transitive description: name: charcode - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.1" checked_yaml: dependency: transitive description: name: checked_yaml - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.1" clock: dependency: transitive description: name: clock - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.0" code_builder: dependency: transitive description: name: code_builder - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "4.2.0" collection: dependency: transitive description: name: collection - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.16.0" contextmenu: dependency: "direct main" description: name: contextmenu - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.0" convert: dependency: transitive description: name: convert - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.2" cross_file: dependency: transitive description: name: cross_file - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.3.3+1" crypto: dependency: transitive description: name: crypto - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.2" csslib: dependency: transitive description: name: csslib - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.17.2" cupertino_icons: dependency: "direct main" description: name: cupertino_icons - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.5" dart_style: dependency: transitive description: name: dart_style - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.3" dash_chat_2: @@ -245,7 +245,7 @@ packages: dependency: "direct main" description: name: desktop_drop - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.3.3" desktop_multi_window: @@ -261,133 +261,133 @@ packages: dependency: "direct main" description: name: device_info_plus - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "4.1.2" device_info_plus_linux: dependency: transitive description: name: device_info_plus_linux - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.0" device_info_plus_macos: dependency: transitive description: name: device_info_plus_macos - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.0" device_info_plus_platform_interface: dependency: transitive description: name: device_info_plus_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.0" device_info_plus_web: dependency: transitive description: name: device_info_plus_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.0" device_info_plus_windows: dependency: transitive description: name: device_info_plus_windows - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "4.0.0" draggable_float_widget: dependency: "direct main" description: name: draggable_float_widget - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.0.2" event_bus: dependency: transitive description: name: event_bus - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.0" external_path: dependency: "direct main" description: name: external_path - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.1" fake_async: dependency: transitive description: name: fake_async - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.0" ffi: dependency: "direct main" description: name: ffi - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.1" file: dependency: transitive description: name: file - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "6.1.4" firebase_analytics: dependency: "direct main" description: name: firebase_analytics - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "9.3.3" firebase_analytics_platform_interface: dependency: transitive description: name: firebase_analytics_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.3.3" firebase_analytics_web: dependency: transitive description: name: firebase_analytics_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.4.2+3" firebase_core: dependency: transitive description: name: firebase_core - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.21.1" firebase_core_platform_interface: dependency: transitive description: name: firebase_core_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "4.5.1" firebase_core_web: dependency: transitive description: name: firebase_core_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.7.2" fixnum: dependency: transitive description: name: fixnum - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.1" flutter: @@ -399,49 +399,49 @@ packages: dependency: transitive description: name: flutter_blurhash - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.7.0" flutter_breadcrumb: dependency: "direct main" description: name: flutter_breadcrumb - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.1" flutter_cache_manager: dependency: transitive description: name: flutter_cache_manager - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.3.0" flutter_launcher_icons: dependency: "direct dev" description: name: flutter_launcher_icons - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.9.3" flutter_lints: dependency: "direct dev" description: name: flutter_lints - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.1" flutter_parsed_text: dependency: transitive description: name: flutter_parsed_text - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.1" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.7" flutter_rust_bridge: @@ -467,490 +467,490 @@ packages: dependency: "direct dev" description: name: freezed - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0+1" freezed_annotation: dependency: "direct main" description: name: freezed_annotation - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0" frontend_server_client: dependency: transitive description: name: frontend_server_client - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.3" get: dependency: "direct main" description: name: get - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "4.6.5" glob: dependency: transitive description: name: glob - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0" graphs: dependency: transitive description: name: graphs - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0" html: dependency: transitive description: name: html - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.15.0" http: dependency: "direct main" description: name: http - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.13.5" http_multi_server: dependency: transitive description: name: http_multi_server - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.2.1" http_parser: dependency: transitive description: name: http_parser - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "4.0.1" image: dependency: "direct main" description: name: image - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.2.0" image_picker: dependency: "direct main" description: name: image_picker - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.8.5+3" image_picker_android: dependency: transitive description: name: image_picker_android - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.8.5+2" image_picker_for_web: dependency: transitive description: name: image_picker_for_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.8" image_picker_ios: dependency: transitive description: name: image_picker_ios - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.8.5+6" image_picker_platform_interface: dependency: transitive description: name: image_picker_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.6.1" intl: dependency: transitive description: name: intl - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.17.0" io: dependency: transitive description: name: io - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.3" js: dependency: transitive description: name: js - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.6.4" json_annotation: dependency: transitive description: name: json_annotation - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "4.6.0" lints: dependency: transitive description: name: lints - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.0" logging: dependency: transitive description: name: logging - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.2" matcher: dependency: transitive description: name: matcher - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.12.11" material_color_utilities: dependency: transitive description: name: material_color_utilities - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.1.4" menu_base: dependency: transitive description: name: menu_base - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.1.1" meta: dependency: transitive description: name: meta - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.7.0" mime: dependency: transitive description: name: mime - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.2" nested: dependency: transitive description: name: nested - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.0" octo_image: dependency: transitive description: name: octo_image - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.2" package_config: dependency: transitive description: name: package_config - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0" package_info_plus: dependency: "direct main" description: name: package_info_plus - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.4.3+1" package_info_plus_linux: dependency: transitive description: name: package_info_plus_linux - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.5" package_info_plus_macos: dependency: transitive description: name: package_info_plus_macos - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.0" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.2" package_info_plus_web: dependency: transitive description: name: package_info_plus_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.5" package_info_plus_windows: dependency: transitive description: name: package_info_plus_windows - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.0" path: dependency: transitive description: name: path - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.8.1" path_provider: dependency: "direct main" description: name: path_provider - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.11" path_provider_android: dependency: transitive description: name: path_provider_android - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.20" path_provider_ios: dependency: transitive description: name: path_provider_ios - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.11" path_provider_linux: dependency: transitive description: name: path_provider_linux - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.7" path_provider_macos: dependency: transitive description: name: path_provider_macos - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.6" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.4" path_provider_windows: dependency: transitive description: name: path_provider_windows - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.2" pedantic: dependency: transitive description: name: pedantic - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.11.1" petitparser: dependency: transitive description: name: petitparser - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "5.0.0" platform: dependency: transitive description: name: platform - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.0" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.2" pool: dependency: transitive description: name: pool - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.5.1" process: dependency: transitive description: name: process - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "4.2.4" provider: dependency: "direct main" description: name: provider - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "6.0.3" pub_semver: dependency: transitive description: name: pub_semver - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.1" pubspec_parse: dependency: transitive description: name: pubspec_parse - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.1" qr_code_scanner: dependency: "direct main" description: name: qr_code_scanner - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.1" quiver: dependency: transitive description: name: quiver - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.0" rxdart: dependency: transitive description: name: rxdart - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.27.5" screen_retriever: dependency: transitive description: name: screen_retriever - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.1.2" scroll_pos: dependency: "direct main" description: name: scroll_pos - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.3.0" settings_ui: dependency: "direct main" description: name: settings_ui - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.2" shared_preferences: dependency: "direct main" description: name: shared_preferences - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.15" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.12" shared_preferences_ios: dependency: transitive description: name: shared_preferences_ios - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.1" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.1" shared_preferences_macos: dependency: transitive description: name: shared_preferences_macos - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.4" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.0" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.4" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.1" shelf: dependency: transitive description: name: shelf - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.2" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.2" shortid: dependency: transitive description: name: shortid - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.1.2" sky_engine: @@ -962,91 +962,91 @@ packages: dependency: transitive description: name: source_gen - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.2" source_span: dependency: transitive description: name: source_span - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.8.2" sqflite: dependency: transitive description: name: sqflite - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.3+1" sqflite_common: dependency: transitive description: name: sqflite_common - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.1+1" stack_trace: dependency: transitive description: name: stack_trace - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.10.0" stream_channel: dependency: transitive description: name: stream_channel - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0" stream_transform: dependency: transitive description: name: stream_transform - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.0" string_scanner: dependency: transitive description: name: string_scanner - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.0" synchronized: dependency: transitive description: name: synchronized - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.0+2" term_glyph: dependency: transitive description: name: term_glyph - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.0" test_api: dependency: transitive description: name: test_api - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.4.9" timing: dependency: transitive description: name: timing - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.0" toggle_switch: dependency: "direct main" description: name: toggle_switch - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.4.0" tray_manager: @@ -1062,182 +1062,182 @@ packages: dependency: "direct main" description: name: tuple - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.0" typed_data: dependency: transitive description: name: typed_data - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.1" url_launcher: dependency: "direct main" description: name: url_launcher - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "6.1.5" url_launcher_android: dependency: transitive description: name: url_launcher_android - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "6.0.17" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "6.0.17" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.1" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0" url_launcher_web: dependency: transitive description: name: url_launcher_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.13" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.1" uuid: dependency: transitive description: name: uuid - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.6" vector_math: dependency: transitive description: name: vector_math - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.2" video_player: dependency: transitive description: name: video_player - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.4.7" video_player_android: dependency: transitive description: name: video_player_android - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.3.9" video_player_avfoundation: dependency: transitive description: name: video_player_avfoundation - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.3.5" video_player_platform_interface: dependency: transitive description: name: video_player_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "5.1.4" video_player_web: dependency: transitive description: name: video_player_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.12" visibility_detector: dependency: "direct main" description: name: visibility_detector - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.3.3" wakelock: dependency: "direct main" description: name: wakelock - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.5.6" wakelock_macos: dependency: transitive description: name: wakelock_macos - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.4.0" wakelock_platform_interface: dependency: transitive description: name: wakelock_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.3.0" wakelock_web: dependency: transitive description: name: wakelock_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.4.0" wakelock_windows: dependency: transitive description: name: wakelock_windows - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.2.0" watcher: dependency: transitive description: name: watcher - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.1" web_socket_channel: dependency: transitive description: name: web_socket_channel - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.0" win32: dependency: transitive description: name: win32 - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.7.0" window_manager: @@ -1253,28 +1253,28 @@ packages: dependency: transitive description: name: xdg_directories - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.2.0+2" xml: dependency: transitive description: name: xml - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "6.1.0" yaml: dependency: transitive description: name: yaml - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.1" zxing2: dependency: "direct main" description: name: zxing2 - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.1.0" sdks: From 52a0621d19fdeff24bf770912d972e5359b41a16 Mon Sep 17 00:00:00 2001 From: Asura Date: Fri, 26 Aug 2022 22:13:19 -0700 Subject: [PATCH 0300/2015] Fix CI --- Cargo.lock | 801 ++++++++++++++++++++++--------------------- flutter/pubspec.lock | 338 +++++++++--------- 2 files changed, 573 insertions(+), 566 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8f90fd2eb..35d87cbd7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,6 +29,17 @@ version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "739f4a8db6605981345c5654f3a85b056ce52f37a39d34da03f25bf2151ea16e" +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + [[package]] name = "aho-corasick" version = "0.7.18" @@ -40,9 +51,9 @@ dependencies = [ [[package]] name = "allo-isolate" -version = "0.1.13-beta.5" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c1a52c9b965fdaf940102bcb1a0aef4bc2f56489056f5872cef705651c7972e" +checksum = "ccb993621e6bf1b67591005b0adad126159a0ab31af379743906158aed5330d0" dependencies = [ "atomic", ] @@ -89,9 +100,9 @@ dependencies = [ [[package]] name = "android_logger" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b74b7ddf197de32e415d197aa21c1c0cb36e01e4794fd801302280ac7847ee02" +checksum = "b5e9dd62f37dea550caf48c77591dc50bd1a378ce08855be1a0c42a97b7550fb" dependencies = [ "android_log-sys", "env_logger 0.9.0", @@ -99,6 +110,15 @@ dependencies = [ "once_cell", ] +[[package]] +name = "android_system_properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7ed72e1635e121ca3e79420540282af22da58be50de153d36f81ddc6b83aa9e" +dependencies = [ + "libc", +] + [[package]] name = "ansi_term" version = "0.12.1" @@ -110,9 +130,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.57" +version = "1.0.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc" +checksum = "1485d4d2cc45e7b201ee3767015c96faa5904387c9d87c6efdd0fb511f12d305" [[package]] name = "arboard" @@ -146,9 +166,9 @@ dependencies = [ [[package]] name = "async-channel" -version = "1.6.1" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2114d64672151c0c5eaa5e131ec84a74f06e1e559830dabba01ca30605d66319" +checksum = "e14485364214912d3b19cc3435dde4df66065127f05fa0d75c712f36f12c2f28" dependencies = [ "concurrent-queue", "event-listener", @@ -171,10 +191,11 @@ dependencies = [ [[package]] name = "async-io" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5e18f61464ae81cde0a23e713ae8fd299580c54d697a35820cfd0625b8b0e07" +checksum = "0ab006897723d9352f63e2b13047177c3982d8d79709d713ce7747a8f19fd1b0" dependencies = [ + "autocfg 1.1.0", "concurrent-queue", "futures-lite", "libc", @@ -183,7 +204,7 @@ dependencies = [ "parking", "polling", "slab", - "socket2 0.4.4", + "socket2 0.4.6", "waker-fn", "winapi 0.3.9", ] @@ -199,11 +220,12 @@ dependencies = [ [[package]] name = "async-process" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf2c06e30a24e8c78a3987d07f0930edf76ef35e027e7bdb063fccafdad1f60c" +checksum = "02111fd8655a613c25069ea89fc8d9bb89331fa77486eb3bc059ee757cfa481c" dependencies = [ "async-io", + "autocfg 1.1.0", "blocking", "cfg-if 1.0.0", "event-listener", @@ -227,15 +249,15 @@ dependencies = [ [[package]] name = "async-task" -version = "4.2.0" +version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30696a84d817107fc028e049980e09d5e140e8da8f1caeb17e8e950658a3cea9" +checksum = "7a40729d2133846d9ed0ea60a8b9541bccddab49cd30f0715a1da672fe9a2524" [[package]] name = "async-trait" -version = "0.1.53" +version = "0.1.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed6aa3524a2dfcf9fe180c51eae2b58738348d819517ceadf95789c51fff7600" +checksum = "76464446b8bc32758d7e88ee1a804d9914cd9b1cb264c029899680b0be29826f" dependencies = [ "proc-macro2", "quote", @@ -250,7 +272,7 @@ checksum = "2c3d816ce6f0e2909a96830d6911c2aff044370b1ef92d7f267b43bae5addedd" dependencies = [ "atk-sys", "bitflags", - "glib 0.15.11", + "glib 0.15.12", "libc", ] @@ -309,9 +331,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "backtrace" -version = "0.3.65" +version = "0.3.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11a17d453482a265fd5f8479f2a3f405566e6ca627837aaddb85af8b1ab8ef61" +checksum = "cab84319d616cfb654d03394f38ab7e6f0919e181b1b57e1fd15e7fb4077d9a7" dependencies = [ "addr2line", "cc", @@ -400,15 +422,15 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.9.1" +version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" +checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d" [[package]] name = "bytemuck" -version = "1.9.1" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdead85bdec19c194affaeeb670c0e41fe23de31459efd1c174d049269cf02cc" +checksum = "2f5715e491b5a1598fc2bef5a606847b5dc1d48ea625bd3c02c00de8285591da" [[package]] name = "byteorder" @@ -422,7 +444,7 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db" dependencies = [ - "serde 1.0.137", + "serde 1.0.144", ] [[package]] @@ -433,13 +455,13 @@ checksum = "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c" [[package]] name = "cairo-rs" -version = "0.15.11" +version = "0.15.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62be3562254e90c1c6050a72aa638f6315593e98c5cdaba9017cedbabf0a5dee" +checksum = "c76ee391b03d35510d9fa917357c7f1855bd9a6659c95a1b392e33f49b3369bc" dependencies = [ "bitflags", "cairo-sys-rs", - "glib 0.15.11", + "glib 0.15.12", "libc", "thiserror", ] @@ -467,11 +489,11 @@ dependencies = [ [[package]] name = "camino" -version = "1.0.9" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "869119e97797867fd90f5e22af7d0bd274bd4635ebb9eb68c04f3f513ae6c412" +checksum = "88ad0e1e3e88dd237a156ab9f571021b8a158caa0ae44b1968a241efb5144c1e" dependencies = [ - "serde 1.0.137", + "serde 1.0.144", ] [[package]] @@ -480,7 +502,7 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbdb825da8a5df079a43676dbe042702f1707b1109f713a01420fbb4cc71fa27" dependencies = [ - "serde 1.0.137", + "serde 1.0.144", ] [[package]] @@ -491,9 +513,9 @@ checksum = "4acbb09d9ee8e23699b9634375c72795d095bf268439da88562cf9b501f181fa" dependencies = [ "camino", "cargo-platform", - "semver 1.0.9", - "serde 1.0.137", - "serde_json 1.0.81", + "semver 1.0.13", + "serde 1.0.144", + "serde_json 1.0.85", ] [[package]] @@ -502,14 +524,14 @@ version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b6d248e3ca02f3fbfabcb9284464c596baec223a26d91bbf44a5a62ddb0d900" dependencies = [ - "clap 3.1.18", + "clap 3.2.17", "heck 0.4.0", "indexmap", "log", "proc-macro2", "quote", - "serde 1.0.137", - "serde_json 1.0.81", + "serde 1.0.144", + "serde_json 1.0.85", "syn", "tempfile", "toml", @@ -562,11 +584,11 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.19" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1" dependencies = [ - "libc", + "iana-time-zone", "num-integer", "num-traits 0.2.15", "winapi 0.3.9", @@ -600,16 +622,16 @@ dependencies = [ [[package]] name = "clap" -version = "3.1.18" +version = "3.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2dbdf4bdacb33466e854ce889eee8dfd5729abf7ccd7664d0a2d60cd384440b" +checksum = "29e724a68d9319343bb3328c9cc2dfde263f4b3142ee1059a9980580171c954b" dependencies = [ "atty", "bitflags", "clap_derive", "clap_lex", "indexmap", - "lazy_static", + "once_cell", "strsim 0.10.0", "termcolor", "textwrap 0.15.0", @@ -617,9 +639,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "3.1.18" +version = "3.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25320346e922cffe59c0bbc5410c8d8784509efb321488971081313cb1e1a33c" +checksum = "13547f7012c01ab4a0e8f8967730ada8f9fdf419e8b6c792788f39cf4e46eefa" dependencies = [ "heck 0.4.0", "proc-macro-error", @@ -630,9 +652,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.2.0" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a37c35f1112dad5e6e0b1adaff798507497a18fceeb30cceb3bae7d1427b9213" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" dependencies = [ "os_str_bytes", ] @@ -644,16 +666,16 @@ dependencies = [ "cc", "hbb_common", "lazy_static", - "serde 1.0.137", + "serde 1.0.144", "serde_derive", "thiserror", ] [[package]] name = "clipboard-win" -version = "4.4.1" +version = "4.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f3e1238132dc01f081e1cbb9dace14e5ef4c3a51ee244bd982275fb514605db" +checksum = "c4ab1b92798304eedc095b53942963240037c0516452cb11aeba709d420b2219" dependencies = [ "error-code", "str-buf", @@ -717,9 +739,9 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" [[package]] name = "combine" -version = "4.6.4" +version = "4.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a604e93b79d1808327a6fca85a6f2d69de66461e7620f5a4cbf5fb4d1d7c948" +checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" dependencies = [ "bytes", "memchr", @@ -727,9 +749,9 @@ dependencies = [ [[package]] name = "concurrent-queue" -version = "1.2.2" +version = "1.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30ed07550be01594c6026cff2a1d7fe9c8f683caa798e12b68694ac9e88286a3" +checksum = "af4780a44ab5696ea9e28294517f1fffb421a83a25af521333c838635509db9c" dependencies = [ "cache-padded", ] @@ -740,7 +762,7 @@ version = "0.4.0" source = "git+https://github.com/open-trade/confy#630cc28a396cb7d01eefdd9f3824486fe4d8554b" dependencies = [ "directories-next", - "serde 1.0.137", + "serde 1.0.144", "thiserror", "toml", ] @@ -879,9 +901,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.2" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b" +checksum = "dc948ebb96241bb40ab73effeb80d9f93afaad49359d159a5e61be51619fe813" dependencies = [ "libc", ] @@ -895,25 +917,11 @@ dependencies = [ "cfg-if 1.0.0", ] -[[package]] -name = "crossbeam" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae5588f6b3c3cb05239e90bd110f257254aecd01e4635400391aeae07497845" -dependencies = [ - "cfg-if 1.0.0", - "crossbeam-channel", - "crossbeam-deque", - "crossbeam-epoch", - "crossbeam-queue", - "crossbeam-utils", -] - [[package]] name = "crossbeam-channel" -version = "0.5.4" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aaa7bd5fb665c6864b5f963dd9097905c54125909c7aa94c9e18507cdbe6c53" +checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521" dependencies = [ "cfg-if 1.0.0", "crossbeam-utils", @@ -921,9 +929,9 @@ dependencies = [ [[package]] name = "crossbeam-deque" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e" +checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc" dependencies = [ "cfg-if 1.0.0", "crossbeam-epoch", @@ -932,23 +940,23 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.8" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1145cf131a2c6ba0615079ab6a638f7e1973ac9c2634fcbeaaad6114246efe8c" +checksum = "045ebe27666471bb549370b4b0b3e51b07f56325befa4284db65fc89c02511b1" dependencies = [ "autocfg 1.1.0", "cfg-if 1.0.0", "crossbeam-utils", - "lazy_static", "memoffset", + "once_cell", "scopeguard", ] [[package]] name = "crossbeam-queue" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f25d8400f4a7a5778f0e4e52384a48cbd9b5c495d110786187fc750075277a2" +checksum = "1cd42583b04998a5363558e5f9291ee5a5ff6b49944332103f251e7479a82aa7" dependencies = [ "cfg-if 1.0.0", "crossbeam-utils", @@ -956,19 +964,19 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.8" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf124c720b7686e3c2663cf54062ab0f68a88af2fb6a030e87e30bf721fcb38" +checksum = "51887d4adc7b564537b15adcfb307936f8075dfcd5f00dde9a9f1d29383682bc" dependencies = [ "cfg-if 1.0.0", - "lazy_static", + "once_cell", ] [[package]] name = "crypto-common" -version = "0.1.3" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", "typenum", @@ -976,9 +984,9 @@ dependencies = [ [[package]] name = "cstr_core" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "644828c273c063ab0d39486ba42a5d1f3a499d35529c759e763a9c6cb8a0fb08" +checksum = "dd98742e4fdca832d40cab219dc2e3048de17d873248f83f17df47c1bea70956" dependencies = [ "cty", "memchr", @@ -986,11 +994,11 @@ dependencies = [ [[package]] name = "ctrlc" -version = "3.2.2" +version = "3.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b37feaa84e6861e00a1f5e5aa8da3ee56d605c9992d33e082786754828e20865" +checksum = "1d91974fbbe88ec1df0c24a4f00f99583667a7e2e6272b2b92d294d81e462173" dependencies = [ - "nix 0.24.1", + "nix 0.25.0", "winapi 0.3.9", ] @@ -1173,9 +1181,9 @@ dependencies = [ [[package]] name = "dbus" -version = "0.9.5" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de0a745c25b32caa56b82a3950f5fec7893a960f4c10ca3b02060b0c38d8c2ce" +checksum = "6f8bcdd56d2e5c4ed26a529c5a9029f5db8290d433497506f958eae3be148eb6" dependencies = [ "libc", "libdbus-sys", @@ -1320,7 +1328,7 @@ checksum = "7f3f119846c823f9eafcf953a8f6ffb6ed69bf6240883261a7f13b634579a51f" dependencies = [ "lazy_static", "regex", - "serde 1.0.137", + "serde 1.0.144", "strsim 0.10.0", ] @@ -1347,9 +1355,9 @@ dependencies = [ [[package]] name = "either" -version = "1.6.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" +checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" [[package]] name = "encoding_rs" @@ -1371,7 +1379,7 @@ dependencies = [ "objc", "pkg-config", "rdev", - "serde 1.0.137", + "serde 1.0.144", "serde_derive", "unicode-segmentation", "winapi 0.3.9", @@ -1379,30 +1387,18 @@ dependencies = [ [[package]] name = "enum-map" -<<<<<<< HEAD -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ddfe61e8040145222887d0d32a939c70c8cae681490d72fb868305e9b40ced8" -======= version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f5a56d54c8dd9b3ad34752ed197a4eb2a6601bc010808eb097a04a58ae4c43e1" ->>>>>>> flutter_desktop dependencies = [ "enum-map-derive", ] [[package]] name = "enum-map-derive" -<<<<<<< HEAD -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00d1c54e25a57236a790ecf051c2befbb57740c9b86c4273eac378ba84d620d6" -======= version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9045e2676cd5af83c3b167d917b0a5c90a4d8e266e2683d6631b235c457fc27" ->>>>>>> flutter_desktop dependencies = [ "proc-macro2", "quote", @@ -1428,7 +1424,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e75d4cd21b95383444831539909fbb14b9dc3fdceb2a6f5d36577329a1f55ccb" dependencies = [ "enumflags2_derive", - "serde 1.0.137", + "serde 1.0.144", ] [[package]] @@ -1522,9 +1518,9 @@ dependencies = [ [[package]] name = "event-listener" -version = "2.5.2" +version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77f3309417938f28bf8228fcff79a4a37103981e3e186d2ccd19c74b38f4eb71" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "failure" @@ -1537,9 +1533,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" +checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" dependencies = [ "instant", ] @@ -1556,14 +1552,14 @@ dependencies = [ [[package]] name = "filetime" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0408e2626025178a6a7f7ffc05a25bc47103229f19c113755de7bf63816290c" +checksum = "e94a7bbaa59354bc20dd75b67f23e2797b4490e9d6928203fb105c79e448c86c" dependencies = [ "cfg-if 1.0.0", "libc", "redox_syscall", - "winapi 0.3.9", + "windows-sys 0.36.1", ] [[package]] @@ -1578,14 +1574,15 @@ dependencies = [ [[package]] name = "flexi_logger" -version = "0.22.3" +version = "0.22.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "969940c39bc718475391e53a3a59b0157e64929c80cf83ad5dde5f770ecdc423" +checksum = "0c76a80dd14a27fc3d8bc696502132cb52b3f227256fd8601166c3a35e45f409" dependencies = [ "ansi_term", "atty", "chrono", - "crossbeam", + "crossbeam-channel", + "crossbeam-queue", "glob", "lazy_static", "log", @@ -1624,7 +1621,7 @@ dependencies = [ "pathdiff", "quote", "regex", - "serde 1.0.137", + "serde 1.0.144", "serde_yaml", "structopt", "syn", @@ -1699,9 +1696,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "futures" -version = "0.3.21" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f73fe65f54d1e12b726f517d3e2135ca3125a437b6d998caf1962961f7172d9e" +checksum = "ab30e97ab6aacfe635fad58f22c2bb06c8b685f7421eb1e064a729e2a5f481fa" dependencies = [ "futures-channel", "futures-core", @@ -1714,9 +1711,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.21" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010" +checksum = "2bfc52cbddcfd745bf1740338492bb0bd83d76c67b445f91c5fb29fae29ecaa1" dependencies = [ "futures-core", "futures-sink", @@ -1724,15 +1721,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.21" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" +checksum = "d2acedae88d38235936c3922476b10fced7b2b68136f5e3c03c2d5be348a1115" [[package]] name = "futures-executor" -version = "0.3.21" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6" +checksum = "1d11aa21b5b587a64682c0094c2bdd4df0076c5324961a40cc3abd7f37930528" dependencies = [ "futures-core", "futures-task", @@ -1741,9 +1738,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.21" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" +checksum = "93a66fc6d035a26a3ae255a6d2bca35eda63ae4c5512bef54449113f7a1228e5" [[package]] name = "futures-lite" @@ -1762,9 +1759,9 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.21" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512" +checksum = "0db9cce532b0eae2ccf2766ab246f114b56b9cf6d445e00c2549fbc100ca045d" dependencies = [ "proc-macro2", "quote", @@ -1773,21 +1770,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.21" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868" +checksum = "ca0bae1fe9752cf7fd9b0064c674ae63f97b37bc714d745cbde0afb7ec4e6765" [[package]] name = "futures-task" -version = "0.3.21" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a" +checksum = "842fc63b931f4056a24d59de13fb1272134ce261816e063e634ad0c15cdc5306" [[package]] name = "futures-util" -version = "0.3.21" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" +checksum = "f0828a5471e340229c11c77ca80017937ce3c58cb788a17e5f1c2d5c485a9577" dependencies = [ "futures-channel", "futures-core", @@ -1821,7 +1818,7 @@ dependencies = [ "gdk-pixbuf", "gdk-sys", "gio", - "glib 0.15.11", + "glib 0.15.12", "libc", "pango", ] @@ -1835,7 +1832,7 @@ dependencies = [ "bitflags", "gdk-pixbuf-sys", "gio", - "glib 0.15.11", + "glib 0.15.12", "libc", ] @@ -1871,9 +1868,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.5" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" +checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" dependencies = [ "typenum", "version_check", @@ -1891,33 +1888,33 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad" +checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" dependencies = [ "cfg-if 1.0.0", "libc", - "wasi 0.10.2+wasi-snapshot-preview1", + "wasi", ] [[package]] name = "gimli" -version = "0.26.1" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78cc372d058dcf6d5ecd98510e7fbc9e5aec4d21de70f65fea8fecebcd881bd4" +checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d" [[package]] name = "gio" -version = "0.15.11" +version = "0.15.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f132be35e05d9662b9fa0fee3f349c6621f7782e0105917f4cc73c1bf47eceb" +checksum = "68fdbc90312d462781a395f7a16d96a2b379bb6ef8cd6310a2df272771c4283b" dependencies = [ "bitflags", "futures-channel", "futures-core", "futures-io", "gio-sys", - "glib 0.15.11", + "glib 0.15.12", "libc", "once_cell", "thiserror", @@ -1957,9 +1954,9 @@ dependencies = [ [[package]] name = "glib" -version = "0.15.11" +version = "0.15.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd124026a2fa8c33a3d17a3fe59c103f2d9fa5bd92c19e029e037736729abeab" +checksum = "edb0306fbad0ab5428b0ca674a23893db909a98582969c9b537be4ced78c505d" dependencies = [ "bitflags", "futures-channel", @@ -1999,7 +1996,7 @@ checksum = "25a68131a662b04931e71891fb14aaf65ee4b44d08e8abc10f49e77418c86c64" dependencies = [ "anyhow", "heck 0.4.0", - "proc-macro-crate 1.1.3", + "proc-macro-crate 1.2.1", "proc-macro-error", "proc-macro2", "quote", @@ -2201,7 +2198,7 @@ dependencies = [ "gdk", "gdk-pixbuf", "gio", - "glib 0.15.11", + "glib 0.15.12", "gtk-sys", "gtk3-macros", "libc", @@ -2235,7 +2232,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24f518afe90c23fba585b2d7697856f9e6a7bbc62f65588035e66f6afb01a2e9" dependencies = [ "anyhow", - "proc-macro-crate 1.1.3", + "proc-macro-crate 1.2.1", "proc-macro-error", "proc-macro2", "quote", @@ -2244,9 +2241,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37a82c6d637fc9515a4694bbf1cb2457b79d81ce52b3108bdeea58b07dd34a57" +checksum = "5ca32592cf21ac7ccab1825cd87f6c9b3d9022c44d086172ed0966bec8af30be" dependencies = [ "bytes", "fnv", @@ -2257,7 +2254,7 @@ dependencies = [ "indexmap", "slab", "tokio", - "tokio-util 0.7.2", + "tokio-util", "tracing", ] @@ -2267,14 +2264,17 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" dependencies = [ - "ahash", + "ahash 0.4.7", ] [[package]] name = "hashbrown" -version = "0.11.2" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.6", +] [[package]] name = "hbb_common" @@ -2298,15 +2298,15 @@ dependencies = [ "quinn", "rand 0.8.5", "regex", - "serde 1.0.137", + "serde 1.0.144", "serde_derive", - "serde_json 1.0.81", + "serde_json 1.0.85", "serde_with", "socket2 0.3.19", "sodiumoxide", "tokio", "tokio-socks", - "tokio-util 0.7.2", + "tokio-util", "toml", "winapi 0.3.9", "zstd", @@ -2350,13 +2350,13 @@ checksum = "8a164bb2ceaeff4f42542bdb847c41517c78a60f5649671b2a07312b6e117549" [[package]] name = "http" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff8670570af52249509a86f5e3e18a08c60b177071826898fde8997cf5f6bfbb" +checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" dependencies = [ "bytes", "fnv", - "itoa 1.0.2", + "itoa 1.0.3", ] [[package]] @@ -2396,16 +2396,16 @@ dependencies = [ "bindgen", "cc", "log", - "serde 1.0.137", + "serde 1.0.144", "serde_derive", - "serde_json 1.0.81", + "serde_json 1.0.85", ] [[package]] name = "hyper" -version = "0.14.19" +version = "0.14.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42dc3c131584288d375f2d07f822b0cb012d8c6fb899a5b9fdb3cb7eb9b6004f" +checksum = "02c929dc5c39e335a03c405292728118860721b10190d98c2a0f0efd5baafbac" dependencies = [ "bytes", "futures-channel", @@ -2416,9 +2416,9 @@ dependencies = [ "http-body", "httparse", "httpdate", - "itoa 1.0.2", + "itoa 1.0.3", "pin-project-lite", - "socket2 0.4.4", + "socket2 0.4.6", "tokio", "tower-service", "tracing", @@ -2438,6 +2438,19 @@ dependencies = [ "tokio-rustls", ] +[[package]] +name = "iana-time-zone" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad2bfd338099682614d3ee3fe0cd72e0b6a41ca6a87f6a74a3bd593c91650501" +dependencies = [ + "android_system_properties", + "core-foundation-sys 0.8.3", + "js-sys", + "wasm-bindgen", + "winapi 0.3.9", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -2492,12 +2505,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.8.2" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6012d540c5baa3589337a98ce73408de9b5a25ec9fc2c6fd6be8f0d39e0ca5a" +checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" dependencies = [ "autocfg 1.1.0", - "hashbrown 0.11.2", + "hashbrown 0.12.3", ] [[package]] @@ -2544,9 +2557,9 @@ checksum = "8324a32baf01e2ae060e9de58ed0bc2320c9a2833491ee36cd3b4c414de4db8c" [[package]] name = "itoa" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" +checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754" [[package]] name = "jni" @@ -2585,9 +2598,9 @@ checksum = "229d53d58899083193af11e15917b5640cd40b29ff475a1fe4ef725deb02d0f2" [[package]] name = "js-sys" -version = "0.3.57" +version = "0.3.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "671a26f820db17c2a2750743f1dd03bafd15b98c9f30c7c2628c024c05d73397" +checksum = "258451ab10b34f8af53416d1fdab72c22e805f0c92a1136d59470ec0b11138b2" dependencies = [ "wasm-bindgen", ] @@ -2616,11 +2629,11 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libappindicator" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b29fab3280d59f3d06725f75da9ef9a1b001b2c748b1abfebd1c966c61d7de" +checksum = "db2d3cb96d092b4824cb306c9e544c856a4cb6210c1081945187f7f1924b47e8" dependencies = [ - "glib 0.15.11", + "glib 0.15.12", "gtk", "gtk-sys", "libappindicator-sys", @@ -2629,9 +2642,9 @@ dependencies = [ [[package]] name = "libappindicator-sys" -version = "0.7.1" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d83c2227727d7950ada2ae554613d35fd4e55b87f0a29b86d2368267d19b1d99" +checksum = "f1b3b6681973cea8cc3bce7391e6d7d5502720b80a581c9a95c9cbaf592826aa" dependencies = [ "gtk-sys", "libloading", @@ -2640,9 +2653,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.126" +version = "0.2.132" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" +checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" [[package]] name = "libdbus-sys" @@ -2734,9 +2747,9 @@ dependencies = [ [[package]] name = "linked-hash-map" -version = "0.5.4" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "lock_api" @@ -2900,13 +2913,13 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "713d550d9b44d89174e066b7a6217ae06234c10cb47819a88290d2b353c31799" +checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf" dependencies = [ "libc", "log", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", "windows-sys 0.36.1", ] @@ -3036,7 +3049,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0df7ac00c4672f9d5aece54ee3347520b7e20f158656c7db2e6de01902eb7a6c" dependencies = [ "darling", - "proc-macro-crate 1.1.3", + "proc-macro-crate 1.2.1", "proc-macro2", "quote", "syn", @@ -3096,13 +3109,26 @@ dependencies = [ [[package]] name = "nix" -version = "0.24.1" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f17df307904acd05aa8e32e97bb20f2a0df1728bbc2d771ae8f9a90463441e9" +checksum = "195cdbc1741b8134346d515b3a56a1c94b0912758009cfd53f99ea0f57b065fc" dependencies = [ "bitflags", "cfg-if 1.0.0", "libc", + "memoffset", +] + +[[package]] +name = "nix" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e322c04a9e3440c327fca7b6c8a63e6890a32fa2ad689db972425f07e0d22abb" +dependencies = [ + "autocfg 1.1.0", + "bitflags", + "cfg-if 1.0.0", + "libc", ] [[package]] @@ -3126,9 +3152,9 @@ dependencies = [ [[package]] name = "num-complex" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97fbc387afefefd5e9e39493299f3069e14a140dd34dc19b4c1c1a8fddb6a790" +checksum = "7ae39348c8bc5fbd7f40c727a9925f03517afd2ab27d46702108b6a7e5414c19" dependencies = [ "num-traits 0.2.15", ] @@ -3219,7 +3245,7 @@ version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b0498641e53dd6ac1a4f22547548caa6864cc4933784319cd1775271c5a46ce" dependencies = [ - "proc-macro-crate 1.1.3", + "proc-macro-crate 1.2.1", "proc-macro2", "quote", "syn", @@ -3265,9 +3291,9 @@ dependencies = [ [[package]] name = "object" -version = "0.28.4" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e42c982f2d955fac81dd7e1d0e1426a7d702acd9c98d19ab01083a6a0328c424" +checksum = "21158b2c33aa6d4561f1c0a6ea283ca92bc54802a93b263e910746d679a7eb53" dependencies = [ "memchr", ] @@ -3329,9 +3355,9 @@ dependencies = [ [[package]] name = "os_str_bytes" -version = "6.1.0" +version = "6.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21326818e99cfe6ce1e524c2a805c189a99b5ae555a35d19f9a284b427d86afa" +checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff" [[package]] name = "padlock" @@ -3346,7 +3372,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22e4045548659aee5313bde6c582b0d83a627b7904dd20dc2d9ef0895d414e4f" dependencies = [ "bitflags", - "glib 0.15.11", + "glib 0.15.12", "libc", "once_cell", "pango-sys", @@ -3366,8 +3392,8 @@ dependencies = [ [[package]] name = "parity-tokio-ipc" -version = "0.7.3" -source = "git+https://github.com/open-trade/parity-tokio-ipc#52515618bd30ea8101bf46f6c7835e88cec9187f" +version = "0.7.3-1" +source = "git+https://github.com/open-trade/parity-tokio-ipc#20b2895910161605210657f3e751edd55321f698" dependencies = [ "futures", "libc", @@ -3435,9 +3461,9 @@ dependencies = [ [[package]] name = "paste" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c520e05135d6e763148b6426a837e239041653ba7becd2e538c076c738025fc" +checksum = "9423e2b32f7a043629287a536f21951e8c6a82482d0acb1eeebfc90bc2225b22" [[package]] name = "pathdiff" @@ -3459,10 +3485,11 @@ checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" [[package]] name = "pest" -version = "2.1.3" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53" +checksum = "4b0560d531d1febc25a3c9398a62a71256c0178f2e3443baedd9ad4bb8c9deb4" dependencies = [ + "thiserror", "ucd-trie", ] @@ -3506,18 +3533,18 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.0.10" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58ad3879ad3baf4e44784bc6a718a8698867bb991f8ce24d1bcbe2cfb4c3a75e" +checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.0.10" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "744b6f092ba29c3650faf274db506afd39944f48420f6c86b17cfe0ee1cb36bb" +checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" dependencies = [ "proc-macro2", "quote", @@ -3556,10 +3583,11 @@ dependencies = [ [[package]] name = "polling" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "685404d509889fade3e86fe3a5803bca2ec09b0c0778d5ada6ec8bf7a8de5259" +checksum = "899b00b9c8ab553c743b3e11e87c5c7d423b2a2de229ba95b24a756344748011" dependencies = [ + "autocfg 1.1.0", "cfg-if 1.0.0", "libc", "log", @@ -3581,9 +3609,9 @@ checksum = "bc5c99d529f0d30937f6f4b8a86d988047327bb88d04d2c4afc356de74722131" [[package]] name = "primal-check" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01419cee72c1a1ca944554e23d83e483e1bccf378753344e881de28b5487511d" +checksum = "8b264861209b0641a9b7571695029f516698bd3f2bf46eb61fca408675630b8c" dependencies = [ "num-integer", ] @@ -3599,10 +3627,11 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "1.1.3" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e17d47ce914bf4de440332250b0edd23ce48c005f59fab39d3335866b114f11a" +checksum = "eda0fc3b0fb7c975631757e14d9049da17374063edb6ebbcbc54d880d4fe94e9" dependencies = [ + "once_cell", "thiserror", "toml", ] @@ -3633,9 +3662,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.39" +version = "1.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f" +checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" dependencies = [ "unicode-ident", ] @@ -3707,9 +3736,9 @@ dependencies = [ [[package]] name = "quinn" -version = "0.8.3" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7542006acd6e057ff632307d219954c44048f818898da03113d6c0086bfddd9" +checksum = "5b435e71d9bfa0d8889927231970c51fb89c58fa63bffcab117c9c7a41e5ef8f" dependencies = [ "bytes", "futures-channel", @@ -3726,9 +3755,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a13a5c0a674c1ce7150c9df7bc4a1e46c2fbbe7c710f56c0dc78b1a810e779e" +checksum = "3fce546b9688f767a57530652488420d419a8b1f44a478b451c3d1ab6d992a55" dependencies = [ "bytes", "fxhash", @@ -3746,23 +3775,23 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3149f7237331015f1a6adf065c397d1be71e032fcf110ba41da52e7926b882f" +checksum = "9f832d8958db3e84d2ec93b5eb2272b45aa23cf7f8fe6e79f578896f4e6c231b" dependencies = [ "futures-util", "libc", "quinn-proto", - "socket2 0.4.4", + "socket2 0.4.6", "tokio", "tracing", ] [[package]] name = "quote" -version = "1.0.18" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" +checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" dependencies = [ "proc-macro2", ] @@ -3970,18 +3999,18 @@ dependencies = [ [[package]] name = "realfft" -version = "3.0.0" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a83b876fe55da7e1bf5deeacb93d6411edf81eba0e1a497e79c067734729053a" +checksum = "8028eb3fabd68ddf331f744ba9c25a939804e276d820f9b218ab25a4bd7b91b8" dependencies = [ "rustfft", ] [[package]] name = "redox_syscall" -version = "0.2.13" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ "bitflags", ] @@ -3999,9 +4028,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.5.6" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1" +checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" dependencies = [ "aho-corasick", "memchr", @@ -4010,9 +4039,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.26" +version = "0.6.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64" +checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" [[package]] name = "remove_dir_all" @@ -4035,9 +4064,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.10" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46a1f7aa4f35e5e8b4160449f51afc758f0ce6454315a9fa7d0d113e958c41eb" +checksum = "b75aa69a3f06bbcc66ede33af2af253c6f7a86b1ca0033f60c580a27074fbf92" dependencies = [ "base64", "bytes", @@ -4057,12 +4086,13 @@ dependencies = [ "percent-encoding", "pin-project-lite", "rustls", - "rustls-pemfile 0.3.0", - "serde 1.0.137", - "serde_json 1.0.81", + "rustls-pemfile 1.0.1", + "serde 1.0.144", + "serde_json 1.0.85", "serde_urlencoded", "tokio", "tokio-rustls", + "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", @@ -4172,7 +4202,7 @@ dependencies = [ name = "rustdesk" version = "1.1.10" dependencies = [ - "android_logger 0.11.0", + "android_logger 0.11.1", "arboard", "async-process", "async-trait", @@ -4180,7 +4210,7 @@ dependencies = [ "bytes", "cc", "cfg-if 1.0.0", - "clap 3.1.18", + "clap 3.2.17", "clipboard", "cocoa", "core-foundation 0.9.3", @@ -4223,9 +4253,9 @@ dependencies = [ "samplerate", "sciter-rs", "scrap", - "serde 1.0.137", + "serde 1.0.144", "serde_derive", - "serde_json 1.0.81", + "serde_json 1.0.85", "sha2", "simple_rc", "sys-locale", @@ -4278,7 +4308,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0167bac7a9f490495f3c33013e7722b53cb087ecbe082fb0c6387c96f634ea50" dependencies = [ "openssl-probe", - "rustls-pemfile 1.0.0", + "rustls-pemfile 1.0.1", "schannel", "security-framework", ] @@ -4294,33 +4324,24 @@ dependencies = [ [[package]] name = "rustls-pemfile" -version = "0.3.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ee86d63972a7c661d1536fefe8c3c8407321c3df668891286de28abcd087360" -dependencies = [ - "base64", -] - -[[package]] -name = "rustls-pemfile" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7522c9de787ff061458fe9a829dc790a3f5b22dc571694fc5883f448b94d9a9" +checksum = "0864aeff53f8c05aa08d86e5ef839d3dfcf07aeba2db32f12db0ef716e87bd55" dependencies = [ "base64", ] [[package]] name = "rustversion" -version = "1.0.6" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2cc38e8fa666e2de3c4aba7edeb5ffc5246c1c2ed0e3d17e560aeeba736b23f" +checksum = "97477e48b4cf8603ad5f7aaf897467cf42ab4218a38ef76fb14c2d6773a6d6a8" [[package]] name = "ryu" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695" +checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" [[package]] name = "same-file" @@ -4395,8 +4416,8 @@ dependencies = [ "num_cpus", "quest", "repng", - "serde 1.0.137", - "serde_json 1.0.81", + "serde 1.0.144", + "serde_json 1.0.85", "target_build_utils", "tracing", "webm", @@ -4415,9 +4436,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.6.1" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dc14f172faf8a0194a3aded622712b0de276821addc574fa54fc0a1167e10dc" +checksum = "2bc1bb97804af6631813c55739f771071e0f2ed33ee20b68c86ec505d906356c" dependencies = [ "bitflags", "core-foundation 0.9.3", @@ -4447,11 +4468,11 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.9" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cb243bdfdb5936c8dc3c45762a19d12ab4550cdc753bc247637d4ec35a040fd" +checksum = "93f6841e709003d68bb2deee8c343572bf446003ec20a583e76f7b15cebf3711" dependencies = [ - "serde 1.0.137", + "serde 1.0.144", ] [[package]] @@ -4471,18 +4492,18 @@ checksum = "34b623917345a631dc9608d5194cc206b3fe6c3554cd1c75b937e55e285254af" [[package]] name = "serde" -version = "1.0.137" +version = "1.0.144" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1" +checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.137" +version = "1.0.144" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be" +checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00" dependencies = [ "proc-macro2", "quote", @@ -4503,13 +4524,13 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.81" +version = "1.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c" +checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44" dependencies = [ - "itoa 1.0.2", + "itoa 1.0.3", "ryu", - "serde 1.0.137", + "serde 1.0.144", ] [[package]] @@ -4530,9 +4551,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", - "itoa 1.0.2", + "itoa 1.0.3", "ryu", - "serde 1.0.137", + "serde 1.0.144", ] [[package]] @@ -4541,7 +4562,7 @@ version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "678b5a069e50bf00ecd22d0cd8ddf7c236f68581b03db652061ed5eb13a312ff" dependencies = [ - "serde 1.0.137", + "serde 1.0.144", "serde_with_macros", ] @@ -4559,13 +4580,13 @@ dependencies = [ [[package]] name = "serde_yaml" -version = "0.8.24" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707d15895415db6628332b737c838b88c598522e4dc70647e59b72312924aebc" +checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b" dependencies = [ "indexmap", "ryu", - "serde 1.0.137", + "serde 1.0.144", "yaml-rust", ] @@ -4622,9 +4643,9 @@ dependencies = [ [[package]] name = "signature" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f054c6c1a6e95179d6f23ed974060dcefb2d9388bb7256900badad682c499de4" +checksum = "f0ea32af43239f0d353a7dd75a22d94c329c8cdaafdcb4c1c1335aa10c298a4a" [[package]] name = "simple_rc" @@ -4632,7 +4653,7 @@ version = "0.1.0" dependencies = [ "confy", "hbb_common", - "serde 1.0.137", + "serde 1.0.144", "serde_derive", "walkdir", ] @@ -4645,15 +4666,18 @@ checksum = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac" [[package]] name = "slab" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32" +checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" +dependencies = [ + "autocfg 1.1.0", +] [[package]] name = "smallvec" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" +checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" [[package]] name = "smithay-client-toolkit" @@ -4687,9 +4711,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.4.4" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" +checksum = "10c98bba371b9b22a71a9414e420f92ddeb2369239af08200816169d5e2dd7aa" dependencies = [ "libc", "winapi 0.3.9", @@ -4704,7 +4728,7 @@ dependencies = [ "ed25519", "libc", "libsodium-sys", - "serde 1.0.137", + "serde 1.0.144", ] [[package]] @@ -4793,9 +4817,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.95" +version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbaf6116ab8924f39d52792136fb74fd60a80194cf1b1c6ffa6453eef1c3f942" +checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13" dependencies = [ "proc-macro2", "quote", @@ -4816,13 +4840,15 @@ dependencies = [ [[package]] name = "sys-locale" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3913c5a3d30054d7f77cf07cdd800c8103ace15c6e44437c5db66a43dd3a92cf" +checksum = "658ee915b6c7b73ec4c1ffcd838506b5c5a4087eadc1ec8f862f1066cf2c8132" dependencies = [ "cc", "cstr_core", + "js-sys", "libc", + "wasm-bindgen", "web-sys", "winapi 0.3.9", ] @@ -4976,18 +5002,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.31" +version = "1.0.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" +checksum = "f5f6586b7f764adc0231f4c79be7b920e766bb2f3e51b3661cdb263828f19994" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.31" +version = "1.0.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" +checksum = "12bafc5b54507e0149cdf1b145a5d80ab80a90bcd9275df43d4fff68460f6c21" dependencies = [ "proc-macro2", "quote", @@ -5020,7 +5046,7 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2702e08a7a860f005826c6815dcac101b19b5eb330c27fe4a5928fec1d20ddd" dependencies = [ - "itoa 1.0.2", + "itoa 1.0.3", "libc", "num_threads", "time-macros", @@ -5057,22 +5083,22 @@ dependencies = [ "bytes", "libc", "memchr", - "mio 0.8.3", + "mio 0.8.4", "num_cpus", "once_cell", "parking_lot 0.12.1", "pin-project-lite", "signal-hook-registry", - "socket2 0.4.4", + "socket2 0.4.6", "tokio-macros", "winapi 0.3.9", ] [[package]] name = "tokio-macros" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7" +checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484" dependencies = [ "proc-macro2", "quote", @@ -5092,8 +5118,8 @@ dependencies = [ [[package]] name = "tokio-socks" -version = "0.5.1" -source = "git+https://github.com/open-trade/tokio-socks#3de8300fbce37e2cdaef042e016aa95058d007cf" +version = "0.5.1-1" +source = "git+https://github.com/open-trade/tokio-socks#7034e79263ce25c348be072808d7601d82cd892d" dependencies = [ "bytes", "either", @@ -5103,34 +5129,21 @@ dependencies = [ "pin-project", "thiserror", "tokio", - "tokio-util 0.6.10", + "tokio-util", ] [[package]] name = "tokio-util" -version = "0.6.10" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36943ee01a6d67977dd3f84a5a1d2efeb4ada3a1ae771cadfaa535d9d9fc6507" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "log", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "tokio-util" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f988a1a1adc2fb21f9c12aa96441da33a1728193ae0b95d2be22dbd17fcb4e5c" +checksum = "cc463cd8deddc3770d20f9852143d50bf6094e640b485cb2e189a2099085ff45" dependencies = [ "bytes", "futures-core", "futures-io", "futures-sink", "futures-util", + "hashbrown 0.12.3", "pin-project-lite", "slab", "tokio", @@ -5143,20 +5156,20 @@ version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" dependencies = [ - "serde 1.0.137", + "serde 1.0.144", ] [[package]] name = "tower-service" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" -version = "0.1.34" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0ecdcb44a79f0fe9844f0c4f33a342cbcbb5117de8001e6ba0dc2351327d09" +checksum = "2fce9567bd60a67d08a16488756721ba392f24f29006402881e43b19aac64307" dependencies = [ "cfg-if 1.0.0", "pin-project-lite", @@ -5166,9 +5179,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.21" +version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc6b8ad3567499f98a1db7a752b07a7c8c7c7c34c332ec00effb2b0027974b7c" +checksum = "11c75893af559bc8e10716548bdef5cb2b983f8e637db9d0e15126b61b484ee2" dependencies = [ "proc-macro2", "quote", @@ -5177,11 +5190,11 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.26" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f54c8ca710e81886d498c2fd3331b56c93aa248d49de2222ad2742247c60072f" +checksum = "5aeea4303076558a00714b823f9ad67d58a3bbda1df83d8827d21193156e22f7" dependencies = [ - "lazy_static", + "once_cell", ] [[package]] @@ -5235,9 +5248,9 @@ checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" [[package]] name = "ucd-trie" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" +checksum = "89570599c4fe5585de2b388aab47e99f7fa4e9238a1399f707a02e356058141c" [[package]] name = "uds_windows" @@ -5257,15 +5270,15 @@ checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" [[package]] name = "unicode-ident" -version = "1.0.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee" +checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf" [[package]] name = "unicode-normalization" -version = "0.1.19" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" +checksum = "854cbdc4f7bc6ae19c820d44abdc3277ac3e1b2b93db20a636825d9322fb60e6" dependencies = [ "tinyvec", ] @@ -5308,9 +5321,9 @@ dependencies = [ [[package]] name = "uuid" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6d5d669b51467dcf7b2f1a796ce0f955f05f01cafda6c19d6e95f730df29238" +checksum = "dd6469f4314d5f1ffec476e05f17cc9a78bc7a27a6a857842170bdf8d6f98d2f" dependencies = [ "getrandom", ] @@ -5346,7 +5359,7 @@ dependencies = [ "cc", "hbb_common", "lazy_static", - "serde 1.0.137", + "serde 1.0.144", "serde_derive", "thiserror", ] @@ -5378,12 +5391,6 @@ dependencies = [ "try-lock", ] -[[package]] -name = "wasi" -version = "0.10.2+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" - [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -5392,9 +5399,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.80" +version = "0.2.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27370197c907c55e3f1a9fbe26f44e937fe6451368324e009cba39e139dc08ad" +checksum = "fc7652e3f6c4706c8d9cd54832c4a4ccb9b5336e2c3bd154d5cccfbf1c1f5f7d" dependencies = [ "cfg-if 1.0.0", "wasm-bindgen-macro", @@ -5402,13 +5409,13 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.80" +version = "0.2.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e04185bfa3a779273da532f5025e33398409573f348985af9a1cbf3774d3f4" +checksum = "662cd44805586bd52971b9586b1df85cdbbd9112e4ef4d8f41559c334dc6ac3f" dependencies = [ "bumpalo", - "lazy_static", "log", + "once_cell", "proc-macro2", "quote", "syn", @@ -5417,9 +5424,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.30" +version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f741de44b75e14c35df886aff5f1eb73aa114fa5d4d00dcd37b5e01259bf3b2" +checksum = "fa76fb221a1f8acddf5b54ace85912606980ad661ac7a503b4570ffd3a624dad" dependencies = [ "cfg-if 1.0.0", "js-sys", @@ -5429,9 +5436,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.80" +version = "0.2.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17cae7ff784d7e83a2fe7611cfe766ecf034111b49deb850a3dc7699c08251f5" +checksum = "b260f13d3012071dfb1512849c033b1925038373aea48ced3012c09df952c602" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5439,9 +5446,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.80" +version = "0.2.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99ec0dc7a4756fffc231aab1b9f2f578d23cd391390ab27f952ae0c9b3ece20b" +checksum = "5be8e654bdd9b79216c2929ab90721aa82faf65c48cdf08bdc4e7f51357b80da" dependencies = [ "proc-macro2", "quote", @@ -5452,20 +5459,20 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.80" +version = "0.2.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d554b7f530dee5964d9a9468d95c1f8b8acae4f282807e7d27d4b03099a46744" +checksum = "6598dd0bd3c7d51095ff6531a5b23e02acdc81804e30d8f07afb77b7215a140a" [[package]] name = "wayland-client" -version = "0.29.4" +version = "0.29.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91223460e73257f697d9e23d401279123d36039a3f7a449e983f123292d4458f" +checksum = "3f3b068c05a039c9f755f881dc50f01732214f5685e379829759088967c46715" dependencies = [ "bitflags", "downcast-rs", "libc", - "nix 0.22.3", + "nix 0.24.2", "scoped-tls", "wayland-commons", "wayland-scanner", @@ -5474,11 +5481,11 @@ dependencies = [ [[package]] name = "wayland-commons" -version = "0.29.4" +version = "0.29.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94f6e5e340d7c13490eca867898c4cec5af56c27a5ffe5c80c6fc4708e22d33e" +checksum = "8691f134d584a33a6606d9d717b95c4fa20065605f798a3f350d78dced02a902" dependencies = [ - "nix 0.22.3", + "nix 0.24.2", "once_cell", "smallvec", "wayland-sys", @@ -5486,20 +5493,20 @@ dependencies = [ [[package]] name = "wayland-cursor" -version = "0.29.4" +version = "0.29.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c52758f13d5e7861fc83d942d3d99bf270c83269575e52ac29e5b73cb956a6bd" +checksum = "6865c6b66f13d6257bef1cd40cbfe8ef2f150fb8ebbdb1e8e873455931377661" dependencies = [ - "nix 0.22.3", + "nix 0.24.2", "wayland-client", "xcursor", ] [[package]] name = "wayland-protocols" -version = "0.29.4" +version = "0.29.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60147ae23303402e41fe034f74fb2c35ad0780ee88a1c40ac09a3be1e7465741" +checksum = "b950621f9354b322ee817a23474e479b34be96c2e909c14f7bc0100e9a970bc6" dependencies = [ "bitflags", "wayland-client", @@ -5509,9 +5516,9 @@ dependencies = [ [[package]] name = "wayland-scanner" -version = "0.29.4" +version = "0.29.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39a1ed3143f7a143187156a2ab52742e89dac33245ba505c17224df48939f9e0" +checksum = "8f4303d8fa22ab852f789e75a967f0a2cdc430a607751c0499bada3e451cbd53" dependencies = [ "proc-macro2", "quote", @@ -5520,9 +5527,9 @@ dependencies = [ [[package]] name = "wayland-sys" -version = "0.29.4" +version = "0.29.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9341df79a8975679188e37dab3889bfa57c44ac2cb6da166f519a81cbe452d4" +checksum = "be12ce1a3c39ec7dba25594b97b42cb3195d54953ddb9d3d95a7c3902bc6e9d4" dependencies = [ "dlib", "lazy_static", @@ -5531,9 +5538,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.57" +version = "0.3.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b17e741662c70c8bd24ac5c5b18de314a2c26c32bf8346ee1e6f53de919c283" +checksum = "ed055ab27f941423197eb86b2035720b1a3ce40504df082cac2ecc6ed73335a1" dependencies = [ "js-sys", "wasm-bindgen", @@ -5569,18 +5576,18 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.22.3" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d8de8415c823c8abd270ad483c6feeac771fad964890779f9a8cb24fbbc1bf" +checksum = "f1c760f0d366a6c24a02ed7816e23e691f5d92291f94d15e836006fd11b04daf" dependencies = [ "webpki", ] [[package]] name = "weezl" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c97e489d8f836838d497091de568cf16b117486d529ec5579233521065bd5e4" +checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb" [[package]] name = "wepoll-ffi" @@ -5843,7 +5850,7 @@ dependencies = [ "lazy_static", "libc", "log", - "mio 0.8.3", + "mio 0.8.4", "ndk 0.5.0", "ndk-glue 0.5.2", "ndk-sys 0.2.2", @@ -5902,7 +5909,7 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7f97e69b28b256ccfb02472c25057132e234aa8368fea3bb0268def564ce1f2" dependencies = [ - "clap 3.1.18", + "clap 3.2.17", ] [[package]] @@ -5926,9 +5933,9 @@ dependencies = [ [[package]] name = "x11" -version = "2.19.1" +version = "2.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dd0565fa8bfba8c5efe02725b14dff114c866724eff2cfd44d76cea74bcd87a" +checksum = "f7ae97874a928d821b061fce3d1fc52f08071dd53c89a6102bc06efcac3b2908" dependencies = [ "libc", "pkg-config", @@ -5936,9 +5943,9 @@ dependencies = [ [[package]] name = "x11-dl" -version = "2.19.1" +version = "2.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea26926b4ce81a6f5d9d0f3a0bc401e5a37c6ae14a1bfaa8ff6099ca80038c59" +checksum = "0c83627bc137605acc00bb399c7b908ef460b621fc37c953db2b09f88c449ea6" dependencies = [ "lazy_static", "libc", @@ -6009,7 +6016,7 @@ dependencies = [ "once_cell", "ordered-stream", "rand 0.8.5", - "serde 1.0.137", + "serde 1.0.144", "serde_repr", "sha1", "static_assertions", @@ -6027,7 +6034,7 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f8fb5186d1c87ae88cf234974c240671238b4a679158ad3b94ec465237349a6" dependencies = [ - "proc-macro-crate 1.1.3", + "proc-macro-crate 1.2.1", "proc-macro2", "quote", "regex", @@ -6040,7 +6047,7 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41a408fd8a352695690f53906dc7fd036be924ec51ea5e05666ff42685ed0af5" dependencies = [ - "serde 1.0.137", + "serde 1.0.144", "static_assertions", "zvariant", ] @@ -6083,7 +6090,7 @@ dependencies = [ "byteorder", "enumflags2", "libc", - "serde 1.0.137", + "serde 1.0.144", "static_assertions", "zvariant_derive", ] @@ -6094,7 +6101,7 @@ version = "3.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08e977eaa3af652f63d479ce50d924254ad76722a6289ec1a1eac3231ca30430" dependencies = [ - "proc-macro-crate 1.1.3", + "proc-macro-crate 1.2.1", "proc-macro2", "quote", "syn", diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 6000671af..4d0a07287 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -5,231 +5,231 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "46.0.0" after_layout: dependency: transitive description: name: after_layout - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.0" analyzer: dependency: transitive description: name: analyzer - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "4.6.0" animations: dependency: transitive description: name: animations - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.4" archive: dependency: transitive description: name: archive - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.3.1" args: dependency: transitive description: name: args - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.3.1" async: dependency: transitive description: name: async - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.8.2" back_button_interceptor: dependency: "direct main" description: name: back_button_interceptor - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "6.0.1" boolean_selector: dependency: transitive description: name: boolean_selector - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0" build: dependency: transitive description: name: build - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.3.0" build_config: dependency: transitive description: name: build_config - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.0" build_daemon: dependency: transitive description: name: build_daemon - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.0" build_resolvers: dependency: transitive description: name: build_resolvers - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.9" build_runner: dependency: "direct dev" description: name: build_runner - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.0" build_runner_core: dependency: transitive description: name: build_runner_core - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "7.2.3" built_collection: dependency: transitive description: name: built_collection - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "5.1.1" built_value: dependency: transitive description: name: built_value - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "8.4.1" cached_network_image: dependency: transitive description: name: cached_network_image - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.2.1" cached_network_image_platform_interface: dependency: transitive description: name: cached_network_image_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.0" cached_network_image_web: dependency: transitive description: name: cached_network_image_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.1" characters: dependency: transitive description: name: characters - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.0" charcode: dependency: transitive description: name: charcode - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.1" checked_yaml: dependency: transitive description: name: checked_yaml - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.1" clock: dependency: transitive description: name: clock - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.0" code_builder: dependency: transitive description: name: code_builder - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "4.2.0" collection: dependency: transitive description: name: collection - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.16.0" contextmenu: dependency: "direct main" description: name: contextmenu - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.0" convert: dependency: transitive description: name: convert - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.2" cross_file: dependency: transitive description: name: cross_file - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.3.3+1" crypto: dependency: transitive description: name: crypto - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.2" csslib: dependency: transitive description: name: csslib - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.17.2" cupertino_icons: dependency: "direct main" description: name: cupertino_icons - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.5" dart_style: dependency: transitive description: name: dart_style - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.3" dash_chat_2: @@ -245,7 +245,7 @@ packages: dependency: "direct main" description: name: desktop_drop - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.3.3" desktop_multi_window: @@ -261,133 +261,133 @@ packages: dependency: "direct main" description: name: device_info_plus - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "4.1.2" device_info_plus_linux: dependency: transitive description: name: device_info_plus_linux - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.0" device_info_plus_macos: dependency: transitive description: name: device_info_plus_macos - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.0" device_info_plus_platform_interface: dependency: transitive description: name: device_info_plus_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.0" device_info_plus_web: dependency: transitive description: name: device_info_plus_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.0" device_info_plus_windows: dependency: transitive description: name: device_info_plus_windows - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "4.0.0" draggable_float_widget: dependency: "direct main" description: name: draggable_float_widget - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.0.2" event_bus: dependency: transitive description: name: event_bus - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.0" external_path: dependency: "direct main" description: name: external_path - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.1" fake_async: dependency: transitive description: name: fake_async - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.0" ffi: dependency: "direct main" description: name: ffi - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.1" file: dependency: transitive description: name: file - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "6.1.4" firebase_analytics: dependency: "direct main" description: name: firebase_analytics - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "9.3.3" firebase_analytics_platform_interface: dependency: transitive description: name: firebase_analytics_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.3.3" firebase_analytics_web: dependency: transitive description: name: firebase_analytics_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.4.2+3" firebase_core: dependency: transitive description: name: firebase_core - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.21.1" firebase_core_platform_interface: dependency: transitive description: name: firebase_core_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "4.5.1" firebase_core_web: dependency: transitive description: name: firebase_core_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.7.2" fixnum: dependency: transitive description: name: fixnum - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.1" flutter: @@ -399,21 +399,21 @@ packages: dependency: transitive description: name: flutter_blurhash - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.7.0" flutter_breadcrumb: dependency: "direct main" description: name: flutter_breadcrumb - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.1" flutter_cache_manager: dependency: transitive description: name: flutter_cache_manager - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.3.0" flutter_launcher_icons: @@ -434,14 +434,14 @@ packages: dependency: transitive description: name: flutter_parsed_text - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.1" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.7" flutter_rust_bridge: @@ -467,133 +467,133 @@ packages: dependency: "direct dev" description: name: freezed - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0+1" freezed_annotation: dependency: "direct main" description: name: freezed_annotation - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0" frontend_server_client: dependency: transitive description: name: frontend_server_client - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.3" get: dependency: "direct main" description: name: get - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "4.6.5" glob: dependency: transitive description: name: glob - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0" graphs: dependency: transitive description: name: graphs - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0" html: dependency: transitive description: name: html - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.15.0" http: dependency: "direct main" description: name: http - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.13.5" http_multi_server: dependency: transitive description: name: http_multi_server - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.2.1" http_parser: dependency: transitive description: name: http_parser - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "4.0.1" image: dependency: "direct main" description: name: image - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.2.0" image_picker: dependency: "direct main" description: name: image_picker - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.8.5+3" image_picker_android: dependency: transitive description: name: image_picker_android - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.8.5+2" image_picker_for_web: dependency: transitive description: name: image_picker_for_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.8" image_picker_ios: dependency: transitive description: name: image_picker_ios - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.8.5+6" image_picker_platform_interface: dependency: transitive description: name: image_picker_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.6.1" intl: dependency: transitive description: name: intl - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.17.0" io: dependency: transitive description: name: io - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.3" js: dependency: transitive description: name: js - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.6.4" json_annotation: @@ -614,343 +614,343 @@ packages: dependency: transitive description: name: logging - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.2" matcher: dependency: transitive description: name: matcher - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.12.11" material_color_utilities: dependency: transitive description: name: material_color_utilities - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.1.4" menu_base: dependency: transitive description: name: menu_base - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.1.1" meta: dependency: transitive description: name: meta - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.7.0" mime: dependency: transitive description: name: mime - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.2" nested: dependency: transitive description: name: nested - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.0" octo_image: dependency: transitive description: name: octo_image - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.2" package_config: dependency: transitive description: name: package_config - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0" package_info_plus: dependency: "direct main" description: name: package_info_plus - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.4.3+1" package_info_plus_linux: dependency: transitive description: name: package_info_plus_linux - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.5" package_info_plus_macos: dependency: transitive description: name: package_info_plus_macos - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.0" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.2" package_info_plus_web: dependency: transitive description: name: package_info_plus_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.5" package_info_plus_windows: dependency: transitive description: name: package_info_plus_windows - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.0" path: dependency: transitive description: name: path - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.8.1" path_provider: dependency: "direct main" description: name: path_provider - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.11" path_provider_android: dependency: transitive description: name: path_provider_android - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.20" path_provider_ios: dependency: transitive description: name: path_provider_ios - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.11" path_provider_linux: dependency: transitive description: name: path_provider_linux - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.7" path_provider_macos: dependency: transitive description: name: path_provider_macos - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.6" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.4" path_provider_windows: dependency: transitive description: name: path_provider_windows - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.2" pedantic: dependency: transitive description: name: pedantic - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.11.1" petitparser: dependency: transitive description: name: petitparser - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "5.0.0" platform: dependency: transitive description: name: platform - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.0" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.2" pool: dependency: transitive description: name: pool - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.5.1" process: dependency: transitive description: name: process - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "4.2.4" provider: dependency: "direct main" description: name: provider - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "6.0.3" pub_semver: dependency: transitive description: name: pub_semver - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.1" pubspec_parse: dependency: transitive description: name: pubspec_parse - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.1" qr_code_scanner: dependency: "direct main" description: name: qr_code_scanner - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.1" quiver: dependency: transitive description: name: quiver - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.0" rxdart: dependency: transitive description: name: rxdart - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.27.5" screen_retriever: dependency: transitive description: name: screen_retriever - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.1.2" scroll_pos: dependency: "direct main" description: name: scroll_pos - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.3.0" settings_ui: dependency: "direct main" description: name: settings_ui - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.2" shared_preferences: dependency: "direct main" description: name: shared_preferences - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.15" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.12" shared_preferences_ios: dependency: transitive description: name: shared_preferences_ios - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.1" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.1" shared_preferences_macos: dependency: transitive description: name: shared_preferences_macos - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.4" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.0" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.4" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.1" shelf: dependency: transitive description: name: shelf - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.2" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.2" shortid: dependency: transitive description: name: shortid - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.1.2" sky_engine: @@ -962,91 +962,91 @@ packages: dependency: transitive description: name: source_gen - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.2" source_span: dependency: transitive description: name: source_span - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.8.2" sqflite: dependency: transitive description: name: sqflite - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.3+1" sqflite_common: dependency: transitive description: name: sqflite_common - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.1+1" stack_trace: dependency: transitive description: name: stack_trace - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.10.0" stream_channel: dependency: transitive description: name: stream_channel - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0" stream_transform: dependency: transitive description: name: stream_transform - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.0" string_scanner: dependency: transitive description: name: string_scanner - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.0" synchronized: dependency: transitive description: name: synchronized - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.0+2" term_glyph: dependency: transitive description: name: term_glyph - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.0" test_api: dependency: transitive description: name: test_api - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.4.9" timing: dependency: transitive description: name: timing - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.0" toggle_switch: dependency: "direct main" description: name: toggle_switch - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.4.0" tray_manager: @@ -1062,182 +1062,182 @@ packages: dependency: "direct main" description: name: tuple - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.0" typed_data: dependency: transitive description: name: typed_data - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.1" url_launcher: dependency: "direct main" description: name: url_launcher - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "6.1.5" url_launcher_android: dependency: transitive description: name: url_launcher_android - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "6.0.17" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "6.0.17" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.1" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0" url_launcher_web: dependency: transitive description: name: url_launcher_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.13" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.1" uuid: dependency: transitive description: name: uuid - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.6" vector_math: dependency: transitive description: name: vector_math - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.2" video_player: dependency: transitive description: name: video_player - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.4.7" video_player_android: dependency: transitive description: name: video_player_android - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.3.9" video_player_avfoundation: dependency: transitive description: name: video_player_avfoundation - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.3.5" video_player_platform_interface: dependency: transitive description: name: video_player_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "5.1.4" video_player_web: dependency: transitive description: name: video_player_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.12" visibility_detector: dependency: "direct main" description: name: visibility_detector - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.3.3" wakelock: dependency: "direct main" description: name: wakelock - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.5.6" wakelock_macos: dependency: transitive description: name: wakelock_macos - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.4.0" wakelock_platform_interface: dependency: transitive description: name: wakelock_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.3.0" wakelock_web: dependency: transitive description: name: wakelock_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.4.0" wakelock_windows: dependency: transitive description: name: wakelock_windows - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.2.0" watcher: dependency: transitive description: name: watcher - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.1" web_socket_channel: dependency: transitive description: name: web_socket_channel - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.0" win32: dependency: transitive description: name: win32 - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.7.0" window_manager: @@ -1253,28 +1253,28 @@ packages: dependency: transitive description: name: xdg_directories - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.2.0+2" xml: dependency: transitive description: name: xml - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "6.1.0" yaml: dependency: transitive description: name: yaml - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.1" zxing2: dependency: "direct main" description: name: zxing2 - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.1.0" sdks: From 32f9b4c7875a75e40b805a2b13e56a72e962c53e Mon Sep 17 00:00:00 2001 From: Asura Date: Sat, 27 Aug 2022 01:03:44 -0700 Subject: [PATCH 0301/2015] Support map keyboard mode on flutter --- src/flutter.rs | 82 +++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 78 insertions(+), 4 deletions(-) diff --git a/src/flutter.rs b/src/flutter.rs index 9586f845a..09ed01aeb 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -37,6 +37,8 @@ use crate::common::{self, make_fd_to_json, CLIPBOARD_INTERVAL}; use crate::common::{check_clipboard, update_clipboard, ClipboardContext}; use crate::{client::*, flutter_ffi::EventToUI, make_fd_flutter}; +use enigo::{self, Enigo, KeyboardControllable}; +use rdev::{EventType::*, Key as RdevKey}; pub(super) const APP_TYPE_MAIN: &str = "main"; pub(super) const APP_TYPE_DESKTOP_REMOTE: &str = "remote"; @@ -46,6 +48,7 @@ lazy_static::lazy_static! { // static ref SESSION: Arc>> = Default::default(); pub static ref SESSIONS: RwLock> = Default::default(); pub static ref GLOBAL_EVENT_STREAM: RwLock>> = Default::default(); // rust to dart event channel + pub static ref ENIGO: Arc> = Arc::new(Mutex::new(Enigo::new())); } static SERVER_CLIPBOARD_ENABLED: AtomicBool = AtomicBool::new(true); @@ -220,6 +223,15 @@ impl Session { self.send(Data::Message(msg)); } + pub fn send_key_event(&self, mut evt: KeyEvent, keyboard_mode: KeyboardMode) { + // mode: legacy(0), map(1), translate(2), auto(3) + evt.mode = keyboard_mode.into(); + dbg!(&evt); + let mut msg_out = Message::new(); + msg_out.set_key_event(evt); + self.send(Data::Message(msg_out)); + } + /// Send chat message over the current session. /// /// # Arguments @@ -373,14 +385,76 @@ impl Session { } } + #[allow(dead_code)] + pub fn convert_numpad_keys(&self, key: RdevKey) -> RdevKey { + if self.get_key_state(enigo::Key::NumLock) { + return key; + } + match key { + RdevKey::Kp0 => RdevKey::Insert, + RdevKey::KpDecimal => RdevKey::Delete, + RdevKey::Kp1 => RdevKey::End, + RdevKey::Kp2 => RdevKey::DownArrow, + RdevKey::Kp3 => RdevKey::PageDown, + RdevKey::Kp4 => RdevKey::LeftArrow, + RdevKey::Kp5 => RdevKey::Clear, + RdevKey::Kp6 => RdevKey::RightArrow, + RdevKey::Kp7 => RdevKey::Home, + RdevKey::Kp8 => RdevKey::UpArrow, + RdevKey::Kp9 => RdevKey::PageUp, + _ => key, + } + } + + pub fn get_key_state(&self, key: enigo::Key) -> bool { + #[cfg(target_os = "macos")] + if key == enigo::Key::NumLock { + return true; + } + ENIGO.lock().unwrap().get_key_state(key) + } + + /// Map keyboard mode pub fn input_raw_key(&self, keycode: i32, scancode: i32, down: bool){ - use rdev::{EventType::*, Key as RdevKey, *}; if scancode < 0 || keycode < 0{ return; } - let key = rdev::key_from_scancode(scancode.try_into().unwrap()) as RdevKey; - - log::info!("{:?}", key); + let keycode: u32 = keycode as u32; + let scancode: u32 = scancode as u32; + let key = rdev::key_from_scancode(scancode) as RdevKey; + // Windows requires special handling + #[cfg(target_os = "windows")] + let key = if let Some(e) = _evt { + rdev::get_win_key(e.code.into(), e.scan_code) + } else { + key + }; + + let peer = self.peer_platform(); + + let mut key_event = KeyEvent::new(); + // According to peer platform. + let keycode: u32 = if peer == "Linux" { + rdev::linux_keycode_from_key(key).unwrap_or_default().into() + } else if peer == "Windows" { + #[cfg(not(windows))] + let key = self.convert_numpad_keys(key); + rdev::win_keycode_from_key(key).unwrap_or_default().into() + } else { + rdev::macos_keycode_from_key(key).unwrap_or_default().into() + }; + + key_event.set_chr(keycode); + key_event.down = down; + + if self.get_key_state(enigo::Key::CapsLock) { + key_event.modifiers.push(ControlKey::CapsLock.into()); + } + if self.get_key_state(enigo::Key::NumLock) { + key_event.modifiers.push(ControlKey::NumLock.into()); + } + + self.send_key_event(key_event, KeyboardMode::Map); } /// Input a string of text. From e9085ecc440240a7884e6404526dd3078e8f69f0 Mon Sep 17 00:00:00 2001 From: maninhill <41712985+maninhill@users.noreply.github.com> Date: Sat, 27 Aug 2022 19:31:07 +0800 Subject: [PATCH 0302/2015] chore: spelling correction --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 456862af5..792357225 100644 --- a/README.md +++ b/README.md @@ -32,10 +32,10 @@ RustDesk welcomes contribution from everyone. See [`CONTRIBUTING.md`](CONTRIBUTI Below are the servers you are using for free, it may change along the time. If you are not close to one of these, your network may be slow. | Location | Vendor | Specification | | --------- | ------------- | ------------------ | -| Seoul | AWS lightsail | 1 VCPU / 0.5GB RAM | -| Singapore | Vultr | 1 VCPU / 1GB RAM | -| Germany | Hetzner | 2 VCPU / 4GB RAM | -| Germany | Codext | 4 VCPU / 8GB RAM | +| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM | +| Singapore | Vultr | 1 vCPU / 1GB RAM | +| Germany | Hetzner | 2 vCPU / 4GB RAM | +| Germany | Codext | 4 vCPU / 8GB RAM | ## Dependencies From 3b5b79712b241f997892a22e03a35ba2c7f29e98 Mon Sep 17 00:00:00 2001 From: Asura Date: Sat, 27 Aug 2022 22:17:02 +0800 Subject: [PATCH 0303/2015] Fix compile error on macos --- flutter/.gitignore | 3 - flutter/lib/generated_bridge.dart | 4047 +++++++++++++++++ flutter/lib/generated_bridge.freezed.dart | 332 ++ flutter/lib/generated_plugin_registrant.dart | 35 + flutter/macos/Podfile.lock | 145 +- flutter/macos/Runner/Release.entitlements | 2 + .../macos/rustdesk.xcodeproj/project.pbxproj | 2 +- 7 files changed, 4504 insertions(+), 62 deletions(-) create mode 100644 flutter/lib/generated_bridge.dart create mode 100644 flutter/lib/generated_bridge.freezed.dart create mode 100644 flutter/lib/generated_plugin_registrant.dart diff --git a/flutter/.gitignore b/flutter/.gitignore index e5db34d22..fdd17a5ed 100644 --- a/flutter/.gitignore +++ b/flutter/.gitignore @@ -32,7 +32,6 @@ /build/ # Web related -lib/generated_plugin_registrant.dart # Symbolication related app.*.symbols @@ -44,8 +43,6 @@ jniLibs .vscode # flutter rust bridge -lib/generated_bridge.dart -lib/generated_bridge.freezed.dart # Flutter Generated Files **/flutter/GeneratedPluginRegistrant.swift diff --git a/flutter/lib/generated_bridge.dart b/flutter/lib/generated_bridge.dart new file mode 100644 index 000000000..8fa7706aa --- /dev/null +++ b/flutter/lib/generated_bridge.dart @@ -0,0 +1,4047 @@ +// AUTO GENERATED FILE, DO NOT EDIT. +// Generated by `flutter_rust_bridge`. + +// ignore_for_file: non_constant_identifier_names, unused_element, duplicate_ignore, directives_ordering, curly_braces_in_flow_control_structures, unnecessary_lambdas, slash_for_doc_comments, prefer_const_literals_to_create_immutables, implicit_dynamic_list_literal, duplicate_import, unused_import, prefer_single_quotes, prefer_const_constructors + +import 'dart:convert'; +import 'dart:typed_data'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +import 'dart:convert'; +import 'dart:typed_data'; +import 'package:flutter_rust_bridge/flutter_rust_bridge.dart'; +import 'dart:ffi' as ffi; + +part 'generated_bridge.freezed.dart'; + +abstract class Rustdesk { + /// FFI for rustdesk core's main entry. + /// Return true if the app should continue running with UI(possibly Flutter), false if the app should exit. + Future rustdeskCoreMain({dynamic hint}); + + Stream startGlobalEventStream( + {required String appType, dynamic hint}); + + Future stopGlobalEventStream({required String appType, dynamic hint}); + + Future hostStopSystemKeyPropagate( + {required bool stopped, dynamic hint}); + + Stream sessionConnect( + {required String id, required bool isFileTransfer, dynamic hint}); + + Future sessionGetRemember({required String id, dynamic hint}); + + Future sessionGetToggleOption( + {required String id, required String arg, dynamic hint}); + + bool sessionGetToggleOptionSync( + {required String id, required String arg, dynamic hint}); + + Future sessionGetImageQuality({required String id, dynamic hint}); + + Future sessionGetOption( + {required String id, required String arg, dynamic hint}); + + Future sessionLogin( + {required String id, + required String password, + required bool remember, + dynamic hint}); + + Future sessionClose({required String id, dynamic hint}); + + Future sessionRefresh({required String id, dynamic hint}); + + Future sessionReconnect({required String id, dynamic hint}); + + Future sessionToggleOption( + {required String id, required String value, dynamic hint}); + + Future sessionSetImageQuality( + {required String id, required String value, dynamic hint}); + + Future sessionLockScreen({required String id, dynamic hint}); + + Future sessionCtrlAltDel({required String id, dynamic hint}); + + Future sessionSwitchDisplay( + {required String id, required int value, dynamic hint}); + + Future sessionInputRawKey( + {required String id, + required int keycode, + required int scancode, + required bool down, + dynamic hint}); + + Future sessionInputKey( + {required String id, + required String name, + required bool down, + required bool press, + required bool alt, + required bool ctrl, + required bool shift, + required bool command, + dynamic hint}); + + Future sessionInputString( + {required String id, required String value, dynamic hint}); + + Future sessionSendChat( + {required String id, required String text, dynamic hint}); + + Future sessionPeerOption( + {required String id, + required String name, + required String value, + dynamic hint}); + + Future sessionGetPeerOption( + {required String id, required String name, dynamic hint}); + + Future sessionInputOsPassword( + {required String id, required String value, dynamic hint}); + + Future sessionReadRemoteDir( + {required String id, + required String path, + required bool includeHidden, + dynamic hint}); + + Future sessionSendFiles( + {required String id, + required int actId, + required String path, + required String to, + required int fileNum, + required bool includeHidden, + required bool isRemote, + dynamic hint}); + + Future sessionSetConfirmOverrideFile( + {required String id, + required int actId, + required int fileNum, + required bool needOverride, + required bool remember, + required bool isUpload, + dynamic hint}); + + Future sessionRemoveFile( + {required String id, + required int actId, + required String path, + required int fileNum, + required bool isRemote, + dynamic hint}); + + Future sessionReadDirRecursive( + {required String id, + required int actId, + required String path, + required bool isRemote, + required bool showHidden, + dynamic hint}); + + Future sessionRemoveAllEmptyDirs( + {required String id, + required int actId, + required String path, + required bool isRemote, + dynamic hint}); + + Future sessionCancelJob( + {required String id, required int actId, dynamic hint}); + + Future sessionCreateDir( + {required String id, + required int actId, + required String path, + required bool isRemote, + dynamic hint}); + + Future sessionReadLocalDirSync( + {required String id, + required String path, + required bool showHidden, + dynamic hint}); + + Future sessionGetPlatform( + {required String id, required bool isRemote, dynamic hint}); + + Future sessionLoadLastTransferJobs({required String id, dynamic hint}); + + Future sessionAddJob( + {required String id, + required int actId, + required String path, + required String to, + required int fileNum, + required bool includeHidden, + required bool isRemote, + dynamic hint}); + + Future sessionResumeJob( + {required String id, + required int actId, + required bool isRemote, + dynamic hint}); + + Future> mainGetSoundInputs({dynamic hint}); + + Future mainChangeId({required String newId, dynamic hint}); + + Future mainGetAsyncStatus({dynamic hint}); + + Future mainGetOption({required String key, dynamic hint}); + + Future mainSetOption( + {required String key, required String value, dynamic hint}); + + Future mainGetOptions({dynamic hint}); + + Future mainSetOptions({required String json, dynamic hint}); + + Future mainTestIfValidServer({required String server, dynamic hint}); + + Future mainSetSocks( + {required String proxy, + required String username, + required String password, + dynamic hint}); + + Future> mainGetSocks({dynamic hint}); + + Future mainGetAppName({dynamic hint}); + + Future mainGetLicense({dynamic hint}); + + Future mainGetVersion({dynamic hint}); + + Future> mainGetFav({dynamic hint}); + + Future mainStoreFav({required List favs, dynamic hint}); + + Future mainGetPeer({required String id, dynamic hint}); + + Future mainGetLanPeers({dynamic hint}); + + Future mainGetConnectStatus({dynamic hint}); + + Future mainCheckConnectStatus({dynamic hint}); + + Future mainIsUsingPublicServer({dynamic hint}); + + Future mainDiscover({dynamic hint}); + + Future mainHasRendezvousService({dynamic hint}); + + Future mainGetApiServer({dynamic hint}); + + Future mainPostRequest( + {required String url, + required String body, + required String header, + dynamic hint}); + + Future mainGetLocalOption({required String key, dynamic hint}); + + Future mainSetLocalOption( + {required String key, required String value, dynamic hint}); + + Future mainGetMyId({dynamic hint}); + + Future mainGetUuid({dynamic hint}); + + Future mainGetPeerOption( + {required String id, required String key, dynamic hint}); + + Future mainSetPeerOption( + {required String id, + required String key, + required String value, + dynamic hint}); + + Future mainForgetPassword({required String id, dynamic hint}); + + Future mainGetRecentPeers({dynamic hint}); + + Future mainLoadRecentPeers({dynamic hint}); + + Future mainLoadFavPeers({dynamic hint}); + + Future mainLoadLanPeers({dynamic hint}); + + Future mainGetLastRemoteId({dynamic hint}); + + Future mainGetSoftwareUpdateUrl({dynamic hint}); + + Future mainGetHomeDir({dynamic hint}); + + Future mainGetLangs({dynamic hint}); + + Future mainGetTemporaryPassword({dynamic hint}); + + Future mainGetPermanentPassword({dynamic hint}); + + Future mainGetOnlineStatue({dynamic hint}); + + Future mainGetClientsState({dynamic hint}); + + Future mainCheckClientsLength({required int length, dynamic hint}); + + Future mainInit({required String appDir, dynamic hint}); + + Future mainDeviceId({required String id, dynamic hint}); + + Future mainDeviceName({required String name, dynamic hint}); + + Future mainRemovePeer({required String id, dynamic hint}); + + Future mainHasHwcodec({dynamic hint}); + + Future sessionSendMouse( + {required String id, required String msg, dynamic hint}); + + Future sessionRestartRemoteDevice({required String id, dynamic hint}); + + Future mainSetHomeDir({required String home, dynamic hint}); + + Future mainStopService({dynamic hint}); + + Future mainStartService({dynamic hint}); + + Future mainUpdateTemporaryPassword({dynamic hint}); + + Future mainSetPermanentPassword( + {required String password, dynamic hint}); + + Future mainCheckSuperUserPermission({dynamic hint}); + + Future cmSendChat( + {required int connId, required String msg, dynamic hint}); + + Future cmLoginRes( + {required int connId, required bool res, dynamic hint}); + + Future cmCloseConnection({required int connId, dynamic hint}); + + Future cmCheckClickTime({required int connId, dynamic hint}); + + Future cmGetClickTime({dynamic hint}); + + Future cmSwitchPermission( + {required int connId, + required String name, + required bool enabled, + dynamic hint}); + + Future mainGetIcon({dynamic hint}); + + Future queryOnlines({required List ids, dynamic hint}); +} + +@freezed +class EventToUI with _$EventToUI { + const factory EventToUI.event( + String field0, + ) = Event; + const factory EventToUI.rgba( + Uint8List field0, + ) = Rgba; +} + +class RustdeskImpl extends FlutterRustBridgeBase + implements Rustdesk { + factory RustdeskImpl(ffi.DynamicLibrary dylib) => + RustdeskImpl.raw(RustdeskWire(dylib)); + + RustdeskImpl.raw(RustdeskWire inner) : super(inner); + + Future rustdeskCoreMain({dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_rustdesk_core_main(port_), + parseSuccessData: _wire2api_bool, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "rustdesk_core_main", + argNames: [], + ), + argValues: [], + hint: hint, + )); + + Stream startGlobalEventStream( + {required String appType, dynamic hint}) => + executeStream(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_start_global_event_stream( + port_, _api2wire_String(appType)), + parseSuccessData: _wire2api_String, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "start_global_event_stream", + argNames: ["appType"], + ), + argValues: [appType], + hint: hint, + )); + + Future stopGlobalEventStream({required String appType, dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_stop_global_event_stream( + port_, _api2wire_String(appType)), + parseSuccessData: _wire2api_unit, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "stop_global_event_stream", + argNames: ["appType"], + ), + argValues: [appType], + hint: hint, + )); + + Future hostStopSystemKeyPropagate( + {required bool stopped, dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => + inner.wire_host_stop_system_key_propagate(port_, stopped), + parseSuccessData: _wire2api_unit, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "host_stop_system_key_propagate", + argNames: ["stopped"], + ), + argValues: [stopped], + hint: hint, + )); + + Stream sessionConnect( + {required String id, required bool isFileTransfer, dynamic hint}) => + executeStream(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_session_connect( + port_, _api2wire_String(id), isFileTransfer), + parseSuccessData: _wire2api_event_to_ui, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "session_connect", + argNames: ["id", "isFileTransfer"], + ), + argValues: [id, isFileTransfer], + hint: hint, + )); + + Future sessionGetRemember({required String id, dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => + inner.wire_session_get_remember(port_, _api2wire_String(id)), + parseSuccessData: _wire2api_opt_box_autoadd_bool, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "session_get_remember", + argNames: ["id"], + ), + argValues: [id], + hint: hint, + )); + + Future sessionGetToggleOption( + {required String id, required String arg, dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_session_get_toggle_option( + port_, _api2wire_String(id), _api2wire_String(arg)), + parseSuccessData: _wire2api_opt_box_autoadd_bool, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "session_get_toggle_option", + argNames: ["id", "arg"], + ), + argValues: [id, arg], + hint: hint, + )); + + bool sessionGetToggleOptionSync( + {required String id, required String arg, dynamic hint}) => + executeSync(FlutterRustBridgeSyncTask( + callFfi: () => inner.wire_session_get_toggle_option_sync( + _api2wire_String(id), _api2wire_String(arg)), + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "session_get_toggle_option_sync", + argNames: ["id", "arg"], + ), + argValues: [id, arg], + hint: hint, + )); + + Future sessionGetImageQuality({required String id, dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => + inner.wire_session_get_image_quality(port_, _api2wire_String(id)), + parseSuccessData: _wire2api_opt_String, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "session_get_image_quality", + argNames: ["id"], + ), + argValues: [id], + hint: hint, + )); + + Future sessionGetOption( + {required String id, required String arg, dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_session_get_option( + port_, _api2wire_String(id), _api2wire_String(arg)), + parseSuccessData: _wire2api_opt_String, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "session_get_option", + argNames: ["id", "arg"], + ), + argValues: [id, arg], + hint: hint, + )); + + Future sessionLogin( + {required String id, + required String password, + required bool remember, + dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_session_login( + port_, _api2wire_String(id), _api2wire_String(password), remember), + parseSuccessData: _wire2api_unit, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "session_login", + argNames: ["id", "password", "remember"], + ), + argValues: [id, password, remember], + hint: hint, + )); + + Future sessionClose({required String id, dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => + inner.wire_session_close(port_, _api2wire_String(id)), + parseSuccessData: _wire2api_unit, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "session_close", + argNames: ["id"], + ), + argValues: [id], + hint: hint, + )); + + Future sessionRefresh({required String id, dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => + inner.wire_session_refresh(port_, _api2wire_String(id)), + parseSuccessData: _wire2api_unit, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "session_refresh", + argNames: ["id"], + ), + argValues: [id], + hint: hint, + )); + + Future sessionReconnect({required String id, dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => + inner.wire_session_reconnect(port_, _api2wire_String(id)), + parseSuccessData: _wire2api_unit, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "session_reconnect", + argNames: ["id"], + ), + argValues: [id], + hint: hint, + )); + + Future sessionToggleOption( + {required String id, required String value, dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_session_toggle_option( + port_, _api2wire_String(id), _api2wire_String(value)), + parseSuccessData: _wire2api_unit, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "session_toggle_option", + argNames: ["id", "value"], + ), + argValues: [id, value], + hint: hint, + )); + + Future sessionSetImageQuality( + {required String id, required String value, dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_session_set_image_quality( + port_, _api2wire_String(id), _api2wire_String(value)), + parseSuccessData: _wire2api_unit, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "session_set_image_quality", + argNames: ["id", "value"], + ), + argValues: [id, value], + hint: hint, + )); + + Future sessionLockScreen({required String id, dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => + inner.wire_session_lock_screen(port_, _api2wire_String(id)), + parseSuccessData: _wire2api_unit, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "session_lock_screen", + argNames: ["id"], + ), + argValues: [id], + hint: hint, + )); + + Future sessionCtrlAltDel({required String id, dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => + inner.wire_session_ctrl_alt_del(port_, _api2wire_String(id)), + parseSuccessData: _wire2api_unit, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "session_ctrl_alt_del", + argNames: ["id"], + ), + argValues: [id], + hint: hint, + )); + + Future sessionSwitchDisplay( + {required String id, required int value, dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_session_switch_display( + port_, _api2wire_String(id), _api2wire_i32(value)), + parseSuccessData: _wire2api_unit, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "session_switch_display", + argNames: ["id", "value"], + ), + argValues: [id, value], + hint: hint, + )); + + Future sessionInputRawKey( + {required String id, + required int keycode, + required int scancode, + required bool down, + dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_session_input_raw_key( + port_, + _api2wire_String(id), + _api2wire_i32(keycode), + _api2wire_i32(scancode), + down), + parseSuccessData: _wire2api_unit, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "session_input_raw_key", + argNames: ["id", "keycode", "scancode", "down"], + ), + argValues: [id, keycode, scancode, down], + hint: hint, + )); + + Future sessionInputKey( + {required String id, + required String name, + required bool down, + required bool press, + required bool alt, + required bool ctrl, + required bool shift, + required bool command, + dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_session_input_key( + port_, + _api2wire_String(id), + _api2wire_String(name), + down, + press, + alt, + ctrl, + shift, + command), + parseSuccessData: _wire2api_unit, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "session_input_key", + argNames: [ + "id", + "name", + "down", + "press", + "alt", + "ctrl", + "shift", + "command" + ], + ), + argValues: [id, name, down, press, alt, ctrl, shift, command], + hint: hint, + )); + + Future sessionInputString( + {required String id, required String value, dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_session_input_string( + port_, _api2wire_String(id), _api2wire_String(value)), + parseSuccessData: _wire2api_unit, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "session_input_string", + argNames: ["id", "value"], + ), + argValues: [id, value], + hint: hint, + )); + + Future sessionSendChat( + {required String id, required String text, dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_session_send_chat( + port_, _api2wire_String(id), _api2wire_String(text)), + parseSuccessData: _wire2api_unit, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "session_send_chat", + argNames: ["id", "text"], + ), + argValues: [id, text], + hint: hint, + )); + + Future sessionPeerOption( + {required String id, + required String name, + required String value, + dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_session_peer_option( + port_, + _api2wire_String(id), + _api2wire_String(name), + _api2wire_String(value)), + parseSuccessData: _wire2api_unit, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "session_peer_option", + argNames: ["id", "name", "value"], + ), + argValues: [id, name, value], + hint: hint, + )); + + Future sessionGetPeerOption( + {required String id, required String name, dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_session_get_peer_option( + port_, _api2wire_String(id), _api2wire_String(name)), + parseSuccessData: _wire2api_String, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "session_get_peer_option", + argNames: ["id", "name"], + ), + argValues: [id, name], + hint: hint, + )); + + Future sessionInputOsPassword( + {required String id, required String value, dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_session_input_os_password( + port_, _api2wire_String(id), _api2wire_String(value)), + parseSuccessData: _wire2api_unit, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "session_input_os_password", + argNames: ["id", "value"], + ), + argValues: [id, value], + hint: hint, + )); + + Future sessionReadRemoteDir( + {required String id, + required String path, + required bool includeHidden, + dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_session_read_remote_dir( + port_, _api2wire_String(id), _api2wire_String(path), includeHidden), + parseSuccessData: _wire2api_unit, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "session_read_remote_dir", + argNames: ["id", "path", "includeHidden"], + ), + argValues: [id, path, includeHidden], + hint: hint, + )); + + Future sessionSendFiles( + {required String id, + required int actId, + required String path, + required String to, + required int fileNum, + required bool includeHidden, + required bool isRemote, + dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_session_send_files( + port_, + _api2wire_String(id), + _api2wire_i32(actId), + _api2wire_String(path), + _api2wire_String(to), + _api2wire_i32(fileNum), + includeHidden, + isRemote), + parseSuccessData: _wire2api_unit, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "session_send_files", + argNames: [ + "id", + "actId", + "path", + "to", + "fileNum", + "includeHidden", + "isRemote" + ], + ), + argValues: [id, actId, path, to, fileNum, includeHidden, isRemote], + hint: hint, + )); + + Future sessionSetConfirmOverrideFile( + {required String id, + required int actId, + required int fileNum, + required bool needOverride, + required bool remember, + required bool isUpload, + dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_session_set_confirm_override_file( + port_, + _api2wire_String(id), + _api2wire_i32(actId), + _api2wire_i32(fileNum), + needOverride, + remember, + isUpload), + parseSuccessData: _wire2api_unit, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "session_set_confirm_override_file", + argNames: [ + "id", + "actId", + "fileNum", + "needOverride", + "remember", + "isUpload" + ], + ), + argValues: [id, actId, fileNum, needOverride, remember, isUpload], + hint: hint, + )); + + Future sessionRemoveFile( + {required String id, + required int actId, + required String path, + required int fileNum, + required bool isRemote, + dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_session_remove_file( + port_, + _api2wire_String(id), + _api2wire_i32(actId), + _api2wire_String(path), + _api2wire_i32(fileNum), + isRemote), + parseSuccessData: _wire2api_unit, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "session_remove_file", + argNames: ["id", "actId", "path", "fileNum", "isRemote"], + ), + argValues: [id, actId, path, fileNum, isRemote], + hint: hint, + )); + + Future sessionReadDirRecursive( + {required String id, + required int actId, + required String path, + required bool isRemote, + required bool showHidden, + dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_session_read_dir_recursive( + port_, + _api2wire_String(id), + _api2wire_i32(actId), + _api2wire_String(path), + isRemote, + showHidden), + parseSuccessData: _wire2api_unit, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "session_read_dir_recursive", + argNames: ["id", "actId", "path", "isRemote", "showHidden"], + ), + argValues: [id, actId, path, isRemote, showHidden], + hint: hint, + )); + + Future sessionRemoveAllEmptyDirs( + {required String id, + required int actId, + required String path, + required bool isRemote, + dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_session_remove_all_empty_dirs( + port_, + _api2wire_String(id), + _api2wire_i32(actId), + _api2wire_String(path), + isRemote), + parseSuccessData: _wire2api_unit, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "session_remove_all_empty_dirs", + argNames: ["id", "actId", "path", "isRemote"], + ), + argValues: [id, actId, path, isRemote], + hint: hint, + )); + + Future sessionCancelJob( + {required String id, required int actId, dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_session_cancel_job( + port_, _api2wire_String(id), _api2wire_i32(actId)), + parseSuccessData: _wire2api_unit, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "session_cancel_job", + argNames: ["id", "actId"], + ), + argValues: [id, actId], + hint: hint, + )); + + Future sessionCreateDir( + {required String id, + required int actId, + required String path, + required bool isRemote, + dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_session_create_dir( + port_, + _api2wire_String(id), + _api2wire_i32(actId), + _api2wire_String(path), + isRemote), + parseSuccessData: _wire2api_unit, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "session_create_dir", + argNames: ["id", "actId", "path", "isRemote"], + ), + argValues: [id, actId, path, isRemote], + hint: hint, + )); + + Future sessionReadLocalDirSync( + {required String id, + required String path, + required bool showHidden, + dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_session_read_local_dir_sync( + port_, _api2wire_String(id), _api2wire_String(path), showHidden), + parseSuccessData: _wire2api_String, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "session_read_local_dir_sync", + argNames: ["id", "path", "showHidden"], + ), + argValues: [id, path, showHidden], + hint: hint, + )); + + Future sessionGetPlatform( + {required String id, required bool isRemote, dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_session_get_platform( + port_, _api2wire_String(id), isRemote), + parseSuccessData: _wire2api_String, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "session_get_platform", + argNames: ["id", "isRemote"], + ), + argValues: [id, isRemote], + hint: hint, + )); + + Future sessionLoadLastTransferJobs( + {required String id, dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_session_load_last_transfer_jobs( + port_, _api2wire_String(id)), + parseSuccessData: _wire2api_unit, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "session_load_last_transfer_jobs", + argNames: ["id"], + ), + argValues: [id], + hint: hint, + )); + + Future sessionAddJob( + {required String id, + required int actId, + required String path, + required String to, + required int fileNum, + required bool includeHidden, + required bool isRemote, + dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_session_add_job( + port_, + _api2wire_String(id), + _api2wire_i32(actId), + _api2wire_String(path), + _api2wire_String(to), + _api2wire_i32(fileNum), + includeHidden, + isRemote), + parseSuccessData: _wire2api_unit, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "session_add_job", + argNames: [ + "id", + "actId", + "path", + "to", + "fileNum", + "includeHidden", + "isRemote" + ], + ), + argValues: [id, actId, path, to, fileNum, includeHidden, isRemote], + hint: hint, + )); + + Future sessionResumeJob( + {required String id, + required int actId, + required bool isRemote, + dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_session_resume_job( + port_, _api2wire_String(id), _api2wire_i32(actId), isRemote), + parseSuccessData: _wire2api_unit, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "session_resume_job", + argNames: ["id", "actId", "isRemote"], + ), + argValues: [id, actId, isRemote], + hint: hint, + )); + + Future> mainGetSoundInputs({dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_main_get_sound_inputs(port_), + parseSuccessData: _wire2api_StringList, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "main_get_sound_inputs", + argNames: [], + ), + argValues: [], + hint: hint, + )); + + Future mainChangeId({required String newId, dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => + inner.wire_main_change_id(port_, _api2wire_String(newId)), + parseSuccessData: _wire2api_unit, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "main_change_id", + argNames: ["newId"], + ), + argValues: [newId], + hint: hint, + )); + + Future mainGetAsyncStatus({dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_main_get_async_status(port_), + parseSuccessData: _wire2api_String, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "main_get_async_status", + argNames: [], + ), + argValues: [], + hint: hint, + )); + + Future mainGetOption({required String key, dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => + inner.wire_main_get_option(port_, _api2wire_String(key)), + parseSuccessData: _wire2api_String, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "main_get_option", + argNames: ["key"], + ), + argValues: [key], + hint: hint, + )); + + Future mainSetOption( + {required String key, required String value, dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_main_set_option( + port_, _api2wire_String(key), _api2wire_String(value)), + parseSuccessData: _wire2api_unit, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "main_set_option", + argNames: ["key", "value"], + ), + argValues: [key, value], + hint: hint, + )); + + Future mainGetOptions({dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_main_get_options(port_), + parseSuccessData: _wire2api_String, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "main_get_options", + argNames: [], + ), + argValues: [], + hint: hint, + )); + + Future mainSetOptions({required String json, dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => + inner.wire_main_set_options(port_, _api2wire_String(json)), + parseSuccessData: _wire2api_unit, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "main_set_options", + argNames: ["json"], + ), + argValues: [json], + hint: hint, + )); + + Future mainTestIfValidServer( + {required String server, dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_main_test_if_valid_server( + port_, _api2wire_String(server)), + parseSuccessData: _wire2api_String, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "main_test_if_valid_server", + argNames: ["server"], + ), + argValues: [server], + hint: hint, + )); + + Future mainSetSocks( + {required String proxy, + required String username, + required String password, + dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_main_set_socks( + port_, + _api2wire_String(proxy), + _api2wire_String(username), + _api2wire_String(password)), + parseSuccessData: _wire2api_unit, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "main_set_socks", + argNames: ["proxy", "username", "password"], + ), + argValues: [proxy, username, password], + hint: hint, + )); + + Future> mainGetSocks({dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_main_get_socks(port_), + parseSuccessData: _wire2api_StringList, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "main_get_socks", + argNames: [], + ), + argValues: [], + hint: hint, + )); + + Future mainGetAppName({dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_main_get_app_name(port_), + parseSuccessData: _wire2api_String, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "main_get_app_name", + argNames: [], + ), + argValues: [], + hint: hint, + )); + + Future mainGetLicense({dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_main_get_license(port_), + parseSuccessData: _wire2api_String, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "main_get_license", + argNames: [], + ), + argValues: [], + hint: hint, + )); + + Future mainGetVersion({dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_main_get_version(port_), + parseSuccessData: _wire2api_String, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "main_get_version", + argNames: [], + ), + argValues: [], + hint: hint, + )); + + Future> mainGetFav({dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_main_get_fav(port_), + parseSuccessData: _wire2api_StringList, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "main_get_fav", + argNames: [], + ), + argValues: [], + hint: hint, + )); + + Future mainStoreFav({required List favs, dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => + inner.wire_main_store_fav(port_, _api2wire_StringList(favs)), + parseSuccessData: _wire2api_unit, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "main_store_fav", + argNames: ["favs"], + ), + argValues: [favs], + hint: hint, + )); + + Future mainGetPeer({required String id, dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => + inner.wire_main_get_peer(port_, _api2wire_String(id)), + parseSuccessData: _wire2api_String, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "main_get_peer", + argNames: ["id"], + ), + argValues: [id], + hint: hint, + )); + + Future mainGetLanPeers({dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_main_get_lan_peers(port_), + parseSuccessData: _wire2api_String, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "main_get_lan_peers", + argNames: [], + ), + argValues: [], + hint: hint, + )); + + Future mainGetConnectStatus({dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_main_get_connect_status(port_), + parseSuccessData: _wire2api_String, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "main_get_connect_status", + argNames: [], + ), + argValues: [], + hint: hint, + )); + + Future mainCheckConnectStatus({dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_main_check_connect_status(port_), + parseSuccessData: _wire2api_unit, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "main_check_connect_status", + argNames: [], + ), + argValues: [], + hint: hint, + )); + + Future mainIsUsingPublicServer({dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_main_is_using_public_server(port_), + parseSuccessData: _wire2api_bool, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "main_is_using_public_server", + argNames: [], + ), + argValues: [], + hint: hint, + )); + + Future mainDiscover({dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_main_discover(port_), + parseSuccessData: _wire2api_unit, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "main_discover", + argNames: [], + ), + argValues: [], + hint: hint, + )); + + Future mainHasRendezvousService({dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_main_has_rendezvous_service(port_), + parseSuccessData: _wire2api_bool, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "main_has_rendezvous_service", + argNames: [], + ), + argValues: [], + hint: hint, + )); + + Future mainGetApiServer({dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_main_get_api_server(port_), + parseSuccessData: _wire2api_String, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "main_get_api_server", + argNames: [], + ), + argValues: [], + hint: hint, + )); + + Future mainPostRequest( + {required String url, + required String body, + required String header, + dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_main_post_request( + port_, + _api2wire_String(url), + _api2wire_String(body), + _api2wire_String(header)), + parseSuccessData: _wire2api_unit, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "main_post_request", + argNames: ["url", "body", "header"], + ), + argValues: [url, body, header], + hint: hint, + )); + + Future mainGetLocalOption({required String key, dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => + inner.wire_main_get_local_option(port_, _api2wire_String(key)), + parseSuccessData: _wire2api_String, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "main_get_local_option", + argNames: ["key"], + ), + argValues: [key], + hint: hint, + )); + + Future mainSetLocalOption( + {required String key, required String value, dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_main_set_local_option( + port_, _api2wire_String(key), _api2wire_String(value)), + parseSuccessData: _wire2api_unit, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "main_set_local_option", + argNames: ["key", "value"], + ), + argValues: [key, value], + hint: hint, + )); + + Future mainGetMyId({dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_main_get_my_id(port_), + parseSuccessData: _wire2api_String, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "main_get_my_id", + argNames: [], + ), + argValues: [], + hint: hint, + )); + + Future mainGetUuid({dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_main_get_uuid(port_), + parseSuccessData: _wire2api_String, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "main_get_uuid", + argNames: [], + ), + argValues: [], + hint: hint, + )); + + Future mainGetPeerOption( + {required String id, required String key, dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_main_get_peer_option( + port_, _api2wire_String(id), _api2wire_String(key)), + parseSuccessData: _wire2api_String, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "main_get_peer_option", + argNames: ["id", "key"], + ), + argValues: [id, key], + hint: hint, + )); + + Future mainSetPeerOption( + {required String id, + required String key, + required String value, + dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_main_set_peer_option( + port_, + _api2wire_String(id), + _api2wire_String(key), + _api2wire_String(value)), + parseSuccessData: _wire2api_unit, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "main_set_peer_option", + argNames: ["id", "key", "value"], + ), + argValues: [id, key, value], + hint: hint, + )); + + Future mainForgetPassword({required String id, dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => + inner.wire_main_forget_password(port_, _api2wire_String(id)), + parseSuccessData: _wire2api_unit, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "main_forget_password", + argNames: ["id"], + ), + argValues: [id], + hint: hint, + )); + + Future mainGetRecentPeers({dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_main_get_recent_peers(port_), + parseSuccessData: _wire2api_String, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "main_get_recent_peers", + argNames: [], + ), + argValues: [], + hint: hint, + )); + + Future mainLoadRecentPeers({dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_main_load_recent_peers(port_), + parseSuccessData: _wire2api_unit, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "main_load_recent_peers", + argNames: [], + ), + argValues: [], + hint: hint, + )); + + Future mainLoadFavPeers({dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_main_load_fav_peers(port_), + parseSuccessData: _wire2api_unit, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "main_load_fav_peers", + argNames: [], + ), + argValues: [], + hint: hint, + )); + + Future mainLoadLanPeers({dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_main_load_lan_peers(port_), + parseSuccessData: _wire2api_unit, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "main_load_lan_peers", + argNames: [], + ), + argValues: [], + hint: hint, + )); + + Future mainGetLastRemoteId({dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_main_get_last_remote_id(port_), + parseSuccessData: _wire2api_String, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "main_get_last_remote_id", + argNames: [], + ), + argValues: [], + hint: hint, + )); + + Future mainGetSoftwareUpdateUrl({dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_main_get_software_update_url(port_), + parseSuccessData: _wire2api_String, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "main_get_software_update_url", + argNames: [], + ), + argValues: [], + hint: hint, + )); + + Future mainGetHomeDir({dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_main_get_home_dir(port_), + parseSuccessData: _wire2api_String, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "main_get_home_dir", + argNames: [], + ), + argValues: [], + hint: hint, + )); + + Future mainGetLangs({dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_main_get_langs(port_), + parseSuccessData: _wire2api_String, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "main_get_langs", + argNames: [], + ), + argValues: [], + hint: hint, + )); + + Future mainGetTemporaryPassword({dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_main_get_temporary_password(port_), + parseSuccessData: _wire2api_String, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "main_get_temporary_password", + argNames: [], + ), + argValues: [], + hint: hint, + )); + + Future mainGetPermanentPassword({dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_main_get_permanent_password(port_), + parseSuccessData: _wire2api_String, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "main_get_permanent_password", + argNames: [], + ), + argValues: [], + hint: hint, + )); + + Future mainGetOnlineStatue({dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_main_get_online_statue(port_), + parseSuccessData: _wire2api_i64, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "main_get_online_statue", + argNames: [], + ), + argValues: [], + hint: hint, + )); + + Future mainGetClientsState({dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_main_get_clients_state(port_), + parseSuccessData: _wire2api_String, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "main_get_clients_state", + argNames: [], + ), + argValues: [], + hint: hint, + )); + + Future mainCheckClientsLength({required int length, dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_main_check_clients_length( + port_, _api2wire_usize(length)), + parseSuccessData: _wire2api_opt_String, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "main_check_clients_length", + argNames: ["length"], + ), + argValues: [length], + hint: hint, + )); + + Future mainInit({required String appDir, dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => + inner.wire_main_init(port_, _api2wire_String(appDir)), + parseSuccessData: _wire2api_unit, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "main_init", + argNames: ["appDir"], + ), + argValues: [appDir], + hint: hint, + )); + + Future mainDeviceId({required String id, dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => + inner.wire_main_device_id(port_, _api2wire_String(id)), + parseSuccessData: _wire2api_unit, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "main_device_id", + argNames: ["id"], + ), + argValues: [id], + hint: hint, + )); + + Future mainDeviceName({required String name, dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => + inner.wire_main_device_name(port_, _api2wire_String(name)), + parseSuccessData: _wire2api_unit, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "main_device_name", + argNames: ["name"], + ), + argValues: [name], + hint: hint, + )); + + Future mainRemovePeer({required String id, dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => + inner.wire_main_remove_peer(port_, _api2wire_String(id)), + parseSuccessData: _wire2api_unit, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "main_remove_peer", + argNames: ["id"], + ), + argValues: [id], + hint: hint, + )); + + Future mainHasHwcodec({dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_main_has_hwcodec(port_), + parseSuccessData: _wire2api_bool, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "main_has_hwcodec", + argNames: [], + ), + argValues: [], + hint: hint, + )); + + Future sessionSendMouse( + {required String id, required String msg, dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_session_send_mouse( + port_, _api2wire_String(id), _api2wire_String(msg)), + parseSuccessData: _wire2api_unit, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "session_send_mouse", + argNames: ["id", "msg"], + ), + argValues: [id, msg], + hint: hint, + )); + + Future sessionRestartRemoteDevice({required String id, dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_session_restart_remote_device( + port_, _api2wire_String(id)), + parseSuccessData: _wire2api_unit, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "session_restart_remote_device", + argNames: ["id"], + ), + argValues: [id], + hint: hint, + )); + + Future mainSetHomeDir({required String home, dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => + inner.wire_main_set_home_dir(port_, _api2wire_String(home)), + parseSuccessData: _wire2api_unit, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "main_set_home_dir", + argNames: ["home"], + ), + argValues: [home], + hint: hint, + )); + + Future mainStopService({dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_main_stop_service(port_), + parseSuccessData: _wire2api_unit, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "main_stop_service", + argNames: [], + ), + argValues: [], + hint: hint, + )); + + Future mainStartService({dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_main_start_service(port_), + parseSuccessData: _wire2api_unit, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "main_start_service", + argNames: [], + ), + argValues: [], + hint: hint, + )); + + Future mainUpdateTemporaryPassword({dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_main_update_temporary_password(port_), + parseSuccessData: _wire2api_unit, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "main_update_temporary_password", + argNames: [], + ), + argValues: [], + hint: hint, + )); + + Future mainSetPermanentPassword( + {required String password, dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_main_set_permanent_password( + port_, _api2wire_String(password)), + parseSuccessData: _wire2api_unit, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "main_set_permanent_password", + argNames: ["password"], + ), + argValues: [password], + hint: hint, + )); + + Future mainCheckSuperUserPermission({dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_main_check_super_user_permission(port_), + parseSuccessData: _wire2api_bool, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "main_check_super_user_permission", + argNames: [], + ), + argValues: [], + hint: hint, + )); + + Future cmSendChat( + {required int connId, required String msg, dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_cm_send_chat( + port_, _api2wire_i32(connId), _api2wire_String(msg)), + parseSuccessData: _wire2api_unit, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "cm_send_chat", + argNames: ["connId", "msg"], + ), + argValues: [connId, msg], + hint: hint, + )); + + Future cmLoginRes( + {required int connId, required bool res, dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => + inner.wire_cm_login_res(port_, _api2wire_i32(connId), res), + parseSuccessData: _wire2api_unit, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "cm_login_res", + argNames: ["connId", "res"], + ), + argValues: [connId, res], + hint: hint, + )); + + Future cmCloseConnection({required int connId, dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => + inner.wire_cm_close_connection(port_, _api2wire_i32(connId)), + parseSuccessData: _wire2api_unit, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "cm_close_connection", + argNames: ["connId"], + ), + argValues: [connId], + hint: hint, + )); + + Future cmCheckClickTime({required int connId, dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => + inner.wire_cm_check_click_time(port_, _api2wire_i32(connId)), + parseSuccessData: _wire2api_unit, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "cm_check_click_time", + argNames: ["connId"], + ), + argValues: [connId], + hint: hint, + )); + + Future cmGetClickTime({dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_cm_get_click_time(port_), + parseSuccessData: _wire2api_f64, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "cm_get_click_time", + argNames: [], + ), + argValues: [], + hint: hint, + )); + + Future cmSwitchPermission( + {required int connId, + required String name, + required bool enabled, + dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_cm_switch_permission( + port_, _api2wire_i32(connId), _api2wire_String(name), enabled), + parseSuccessData: _wire2api_unit, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "cm_switch_permission", + argNames: ["connId", "name", "enabled"], + ), + argValues: [connId, name, enabled], + hint: hint, + )); + + Future mainGetIcon({dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => inner.wire_main_get_icon(port_), + parseSuccessData: _wire2api_String, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "main_get_icon", + argNames: [], + ), + argValues: [], + hint: hint, + )); + + Future queryOnlines({required List ids, dynamic hint}) => + executeNormal(FlutterRustBridgeTask( + callFfi: (port_) => + inner.wire_query_onlines(port_, _api2wire_StringList(ids)), + parseSuccessData: _wire2api_unit, + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: "query_onlines", + argNames: ["ids"], + ), + argValues: [ids], + hint: hint, + )); + + // Section: api2wire + ffi.Pointer _api2wire_String(String raw) { + return _api2wire_uint_8_list(utf8.encoder.convert(raw)); + } + + ffi.Pointer _api2wire_StringList(List raw) { + final ans = inner.new_StringList(raw.length); + for (var i = 0; i < raw.length; i++) { + ans.ref.ptr[i] = _api2wire_String(raw[i]); + } + return ans; + } + + int _api2wire_bool(bool raw) { + return raw ? 1 : 0; + } + + int _api2wire_i32(int raw) { + return raw; + } + + int _api2wire_u8(int raw) { + return raw; + } + + ffi.Pointer _api2wire_uint_8_list(Uint8List raw) { + final ans = inner.new_uint_8_list(raw.length); + ans.ref.ptr.asTypedList(raw.length).setAll(0, raw); + return ans; + } + + int _api2wire_usize(int raw) { + return raw; + } + + // Section: api_fill_to_wire + +} + +// Section: wire2api +String _wire2api_String(dynamic raw) { + return raw as String; +} + +List _wire2api_StringList(dynamic raw) { + return (raw as List).cast(); +} + +Uint8List _wire2api_ZeroCopyBuffer_Uint8List(dynamic raw) { + return raw as Uint8List; +} + +bool _wire2api_bool(dynamic raw) { + return raw as bool; +} + +bool _wire2api_box_autoadd_bool(dynamic raw) { + return raw as bool; +} + +EventToUI _wire2api_event_to_ui(dynamic raw) { + switch (raw[0]) { + case 0: + return Event( + _wire2api_String(raw[1]), + ); + case 1: + return Rgba( + _wire2api_ZeroCopyBuffer_Uint8List(raw[1]), + ); + default: + throw Exception("unreachable"); + } +} + +double _wire2api_f64(dynamic raw) { + return raw as double; +} + +int _wire2api_i64(dynamic raw) { + return raw as int; +} + +String? _wire2api_opt_String(dynamic raw) { + return raw == null ? null : _wire2api_String(raw); +} + +bool? _wire2api_opt_box_autoadd_bool(dynamic raw) { + return raw == null ? null : _wire2api_box_autoadd_bool(raw); +} + +int _wire2api_u8(dynamic raw) { + return raw as int; +} + +Uint8List _wire2api_uint_8_list(dynamic raw) { + return raw as Uint8List; +} + +void _wire2api_unit(dynamic raw) { + return; +} + +// ignore_for_file: camel_case_types, non_constant_identifier_names, avoid_positional_boolean_parameters, annotate_overrides, constant_identifier_names + +// AUTO GENERATED FILE, DO NOT EDIT. +// +// Generated by `package:ffigen`. + +/// generated by flutter_rust_bridge +class RustdeskWire implements FlutterRustBridgeWireBase { + /// Holds the symbol lookup function. + final ffi.Pointer Function(String symbolName) + _lookup; + + /// The symbols are looked up in [dynamicLibrary]. + RustdeskWire(ffi.DynamicLibrary dynamicLibrary) + : _lookup = dynamicLibrary.lookup; + + /// The symbols are looked up with [lookup]. + RustdeskWire.fromLookup( + ffi.Pointer Function(String symbolName) + lookup) + : _lookup = lookup; + + void wire_rustdesk_core_main( + int port_, + ) { + return _wire_rustdesk_core_main( + port_, + ); + } + + late final _wire_rustdesk_core_mainPtr = + _lookup>( + 'wire_rustdesk_core_main'); + late final _wire_rustdesk_core_main = + _wire_rustdesk_core_mainPtr.asFunction(); + + void wire_start_global_event_stream( + int port_, + ffi.Pointer app_type, + ) { + return _wire_start_global_event_stream( + port_, + app_type, + ); + } + + late final _wire_start_global_event_streamPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Int64, ffi.Pointer)>>( + 'wire_start_global_event_stream'); + late final _wire_start_global_event_stream = + _wire_start_global_event_streamPtr + .asFunction)>(); + + void wire_stop_global_event_stream( + int port_, + ffi.Pointer app_type, + ) { + return _wire_stop_global_event_stream( + port_, + app_type, + ); + } + + late final _wire_stop_global_event_streamPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Int64, + ffi.Pointer)>>('wire_stop_global_event_stream'); + late final _wire_stop_global_event_stream = _wire_stop_global_event_streamPtr + .asFunction)>(); + + void wire_host_stop_system_key_propagate( + int port_, + bool stopped, + ) { + return _wire_host_stop_system_key_propagate( + port_, + stopped, + ); + } + + late final _wire_host_stop_system_key_propagatePtr = + _lookup>( + 'wire_host_stop_system_key_propagate'); + late final _wire_host_stop_system_key_propagate = + _wire_host_stop_system_key_propagatePtr + .asFunction(); + + void wire_session_connect( + int port_, + ffi.Pointer id, + bool is_file_transfer, + ) { + return _wire_session_connect( + port_, + id, + is_file_transfer, + ); + } + + late final _wire_session_connectPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Int64, ffi.Pointer, + ffi.Bool)>>('wire_session_connect'); + late final _wire_session_connect = _wire_session_connectPtr + .asFunction, bool)>(); + + void wire_session_get_remember( + int port_, + ffi.Pointer id, + ) { + return _wire_session_get_remember( + port_, + id, + ); + } + + late final _wire_session_get_rememberPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Int64, + ffi.Pointer)>>('wire_session_get_remember'); + late final _wire_session_get_remember = _wire_session_get_rememberPtr + .asFunction)>(); + + void wire_session_get_toggle_option( + int port_, + ffi.Pointer id, + ffi.Pointer arg, + ) { + return _wire_session_get_toggle_option( + port_, + id, + arg, + ); + } + + late final _wire_session_get_toggle_optionPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Int64, ffi.Pointer, + ffi.Pointer)>>( + 'wire_session_get_toggle_option'); + late final _wire_session_get_toggle_option = + _wire_session_get_toggle_optionPtr.asFunction< + void Function(int, ffi.Pointer, + ffi.Pointer)>(); + + WireSyncReturnStruct wire_session_get_toggle_option_sync( + ffi.Pointer id, + ffi.Pointer arg, + ) { + return _wire_session_get_toggle_option_sync( + id, + arg, + ); + } + + late final _wire_session_get_toggle_option_syncPtr = _lookup< + ffi.NativeFunction< + WireSyncReturnStruct Function(ffi.Pointer, + ffi.Pointer)>>( + 'wire_session_get_toggle_option_sync'); + late final _wire_session_get_toggle_option_sync = + _wire_session_get_toggle_option_syncPtr.asFunction< + WireSyncReturnStruct Function( + ffi.Pointer, ffi.Pointer)>(); + + void wire_session_get_image_quality( + int port_, + ffi.Pointer id, + ) { + return _wire_session_get_image_quality( + port_, + id, + ); + } + + late final _wire_session_get_image_qualityPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Int64, ffi.Pointer)>>( + 'wire_session_get_image_quality'); + late final _wire_session_get_image_quality = + _wire_session_get_image_qualityPtr + .asFunction)>(); + + void wire_session_get_option( + int port_, + ffi.Pointer id, + ffi.Pointer arg, + ) { + return _wire_session_get_option( + port_, + id, + arg, + ); + } + + late final _wire_session_get_optionPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Int64, ffi.Pointer, + ffi.Pointer)>>('wire_session_get_option'); + late final _wire_session_get_option = _wire_session_get_optionPtr.asFunction< + void Function( + int, ffi.Pointer, ffi.Pointer)>(); + + void wire_session_login( + int port_, + ffi.Pointer id, + ffi.Pointer password, + bool remember, + ) { + return _wire_session_login( + port_, + id, + password, + remember, + ); + } + + late final _wire_session_loginPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Int64, ffi.Pointer, + ffi.Pointer, ffi.Bool)>>('wire_session_login'); + late final _wire_session_login = _wire_session_loginPtr.asFunction< + void Function(int, ffi.Pointer, + ffi.Pointer, bool)>(); + + void wire_session_close( + int port_, + ffi.Pointer id, + ) { + return _wire_session_close( + port_, + id, + ); + } + + late final _wire_session_closePtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Int64, ffi.Pointer)>>('wire_session_close'); + late final _wire_session_close = _wire_session_closePtr + .asFunction)>(); + + void wire_session_refresh( + int port_, + ffi.Pointer id, + ) { + return _wire_session_refresh( + port_, + id, + ); + } + + late final _wire_session_refreshPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Int64, + ffi.Pointer)>>('wire_session_refresh'); + late final _wire_session_refresh = _wire_session_refreshPtr + .asFunction)>(); + + void wire_session_reconnect( + int port_, + ffi.Pointer id, + ) { + return _wire_session_reconnect( + port_, + id, + ); + } + + late final _wire_session_reconnectPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Int64, + ffi.Pointer)>>('wire_session_reconnect'); + late final _wire_session_reconnect = _wire_session_reconnectPtr + .asFunction)>(); + + void wire_session_toggle_option( + int port_, + ffi.Pointer id, + ffi.Pointer value, + ) { + return _wire_session_toggle_option( + port_, + id, + value, + ); + } + + late final _wire_session_toggle_optionPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Int64, ffi.Pointer, + ffi.Pointer)>>('wire_session_toggle_option'); + late final _wire_session_toggle_option = + _wire_session_toggle_optionPtr.asFunction< + void Function(int, ffi.Pointer, + ffi.Pointer)>(); + + void wire_session_set_image_quality( + int port_, + ffi.Pointer id, + ffi.Pointer value, + ) { + return _wire_session_set_image_quality( + port_, + id, + value, + ); + } + + late final _wire_session_set_image_qualityPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Int64, ffi.Pointer, + ffi.Pointer)>>( + 'wire_session_set_image_quality'); + late final _wire_session_set_image_quality = + _wire_session_set_image_qualityPtr.asFunction< + void Function(int, ffi.Pointer, + ffi.Pointer)>(); + + void wire_session_lock_screen( + int port_, + ffi.Pointer id, + ) { + return _wire_session_lock_screen( + port_, + id, + ); + } + + late final _wire_session_lock_screenPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Int64, + ffi.Pointer)>>('wire_session_lock_screen'); + late final _wire_session_lock_screen = _wire_session_lock_screenPtr + .asFunction)>(); + + void wire_session_ctrl_alt_del( + int port_, + ffi.Pointer id, + ) { + return _wire_session_ctrl_alt_del( + port_, + id, + ); + } + + late final _wire_session_ctrl_alt_delPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Int64, + ffi.Pointer)>>('wire_session_ctrl_alt_del'); + late final _wire_session_ctrl_alt_del = _wire_session_ctrl_alt_delPtr + .asFunction)>(); + + void wire_session_switch_display( + int port_, + ffi.Pointer id, + int value, + ) { + return _wire_session_switch_display( + port_, + id, + value, + ); + } + + late final _wire_session_switch_displayPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Int64, ffi.Pointer, + ffi.Int32)>>('wire_session_switch_display'); + late final _wire_session_switch_display = _wire_session_switch_displayPtr + .asFunction, int)>(); + + void wire_session_input_raw_key( + int port_, + ffi.Pointer id, + int keycode, + int scancode, + bool down, + ) { + return _wire_session_input_raw_key( + port_, + id, + keycode, + scancode, + down, + ); + } + + late final _wire_session_input_raw_keyPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Int64, ffi.Pointer, ffi.Int32, + ffi.Int32, ffi.Bool)>>('wire_session_input_raw_key'); + late final _wire_session_input_raw_key = + _wire_session_input_raw_keyPtr.asFunction< + void Function(int, ffi.Pointer, int, int, bool)>(); + + void wire_session_input_key( + int port_, + ffi.Pointer id, + ffi.Pointer name, + bool down, + bool press, + bool alt, + bool ctrl, + bool shift, + bool command, + ) { + return _wire_session_input_key( + port_, + id, + name, + down, + press, + alt, + ctrl, + shift, + command, + ); + } + + late final _wire_session_input_keyPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Int64, + ffi.Pointer, + ffi.Pointer, + ffi.Bool, + ffi.Bool, + ffi.Bool, + ffi.Bool, + ffi.Bool, + ffi.Bool)>>('wire_session_input_key'); + late final _wire_session_input_key = _wire_session_input_keyPtr.asFunction< + void Function(int, ffi.Pointer, + ffi.Pointer, bool, bool, bool, bool, bool, bool)>(); + + void wire_session_input_string( + int port_, + ffi.Pointer id, + ffi.Pointer value, + ) { + return _wire_session_input_string( + port_, + id, + value, + ); + } + + late final _wire_session_input_stringPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Int64, ffi.Pointer, + ffi.Pointer)>>('wire_session_input_string'); + late final _wire_session_input_string = + _wire_session_input_stringPtr.asFunction< + void Function(int, ffi.Pointer, + ffi.Pointer)>(); + + void wire_session_send_chat( + int port_, + ffi.Pointer id, + ffi.Pointer text, + ) { + return _wire_session_send_chat( + port_, + id, + text, + ); + } + + late final _wire_session_send_chatPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Int64, ffi.Pointer, + ffi.Pointer)>>('wire_session_send_chat'); + late final _wire_session_send_chat = _wire_session_send_chatPtr.asFunction< + void Function( + int, ffi.Pointer, ffi.Pointer)>(); + + void wire_session_peer_option( + int port_, + ffi.Pointer id, + ffi.Pointer name, + ffi.Pointer value, + ) { + return _wire_session_peer_option( + port_, + id, + name, + value, + ); + } + + late final _wire_session_peer_optionPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Int64, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer)>>('wire_session_peer_option'); + late final _wire_session_peer_option = + _wire_session_peer_optionPtr.asFunction< + void Function(int, ffi.Pointer, + ffi.Pointer, ffi.Pointer)>(); + + void wire_session_get_peer_option( + int port_, + ffi.Pointer id, + ffi.Pointer name, + ) { + return _wire_session_get_peer_option( + port_, + id, + name, + ); + } + + late final _wire_session_get_peer_optionPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Int64, ffi.Pointer, + ffi.Pointer)>>('wire_session_get_peer_option'); + late final _wire_session_get_peer_option = + _wire_session_get_peer_optionPtr.asFunction< + void Function(int, ffi.Pointer, + ffi.Pointer)>(); + + void wire_session_input_os_password( + int port_, + ffi.Pointer id, + ffi.Pointer value, + ) { + return _wire_session_input_os_password( + port_, + id, + value, + ); + } + + late final _wire_session_input_os_passwordPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Int64, ffi.Pointer, + ffi.Pointer)>>( + 'wire_session_input_os_password'); + late final _wire_session_input_os_password = + _wire_session_input_os_passwordPtr.asFunction< + void Function(int, ffi.Pointer, + ffi.Pointer)>(); + + void wire_session_read_remote_dir( + int port_, + ffi.Pointer id, + ffi.Pointer path, + bool include_hidden, + ) { + return _wire_session_read_remote_dir( + port_, + id, + path, + include_hidden, + ); + } + + late final _wire_session_read_remote_dirPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Int64, + ffi.Pointer, + ffi.Pointer, + ffi.Bool)>>('wire_session_read_remote_dir'); + late final _wire_session_read_remote_dir = + _wire_session_read_remote_dirPtr.asFunction< + void Function(int, ffi.Pointer, + ffi.Pointer, bool)>(); + + void wire_session_send_files( + int port_, + ffi.Pointer id, + int act_id, + ffi.Pointer path, + ffi.Pointer to, + int file_num, + bool include_hidden, + bool is_remote, + ) { + return _wire_session_send_files( + port_, + id, + act_id, + path, + to, + file_num, + include_hidden, + is_remote, + ); + } + + late final _wire_session_send_filesPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Int64, + ffi.Pointer, + ffi.Int32, + ffi.Pointer, + ffi.Pointer, + ffi.Int32, + ffi.Bool, + ffi.Bool)>>('wire_session_send_files'); + late final _wire_session_send_files = _wire_session_send_filesPtr.asFunction< + void Function( + int, + ffi.Pointer, + int, + ffi.Pointer, + ffi.Pointer, + int, + bool, + bool)>(); + + void wire_session_set_confirm_override_file( + int port_, + ffi.Pointer id, + int act_id, + int file_num, + bool need_override, + bool remember, + bool is_upload, + ) { + return _wire_session_set_confirm_override_file( + port_, + id, + act_id, + file_num, + need_override, + remember, + is_upload, + ); + } + + late final _wire_session_set_confirm_override_filePtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Int64, + ffi.Pointer, + ffi.Int32, + ffi.Int32, + ffi.Bool, + ffi.Bool, + ffi.Bool)>>('wire_session_set_confirm_override_file'); + late final _wire_session_set_confirm_override_file = + _wire_session_set_confirm_override_filePtr.asFunction< + void Function(int, ffi.Pointer, int, int, bool, + bool, bool)>(); + + void wire_session_remove_file( + int port_, + ffi.Pointer id, + int act_id, + ffi.Pointer path, + int file_num, + bool is_remote, + ) { + return _wire_session_remove_file( + port_, + id, + act_id, + path, + file_num, + is_remote, + ); + } + + late final _wire_session_remove_filePtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Int64, + ffi.Pointer, + ffi.Int32, + ffi.Pointer, + ffi.Int32, + ffi.Bool)>>('wire_session_remove_file'); + late final _wire_session_remove_file = + _wire_session_remove_filePtr.asFunction< + void Function(int, ffi.Pointer, int, + ffi.Pointer, int, bool)>(); + + void wire_session_read_dir_recursive( + int port_, + ffi.Pointer id, + int act_id, + ffi.Pointer path, + bool is_remote, + bool show_hidden, + ) { + return _wire_session_read_dir_recursive( + port_, + id, + act_id, + path, + is_remote, + show_hidden, + ); + } + + late final _wire_session_read_dir_recursivePtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Int64, + ffi.Pointer, + ffi.Int32, + ffi.Pointer, + ffi.Bool, + ffi.Bool)>>('wire_session_read_dir_recursive'); + late final _wire_session_read_dir_recursive = + _wire_session_read_dir_recursivePtr.asFunction< + void Function(int, ffi.Pointer, int, + ffi.Pointer, bool, bool)>(); + + void wire_session_remove_all_empty_dirs( + int port_, + ffi.Pointer id, + int act_id, + ffi.Pointer path, + bool is_remote, + ) { + return _wire_session_remove_all_empty_dirs( + port_, + id, + act_id, + path, + is_remote, + ); + } + + late final _wire_session_remove_all_empty_dirsPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Int64, + ffi.Pointer, + ffi.Int32, + ffi.Pointer, + ffi.Bool)>>('wire_session_remove_all_empty_dirs'); + late final _wire_session_remove_all_empty_dirs = + _wire_session_remove_all_empty_dirsPtr.asFunction< + void Function(int, ffi.Pointer, int, + ffi.Pointer, bool)>(); + + void wire_session_cancel_job( + int port_, + ffi.Pointer id, + int act_id, + ) { + return _wire_session_cancel_job( + port_, + id, + act_id, + ); + } + + late final _wire_session_cancel_jobPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Int64, ffi.Pointer, + ffi.Int32)>>('wire_session_cancel_job'); + late final _wire_session_cancel_job = _wire_session_cancel_jobPtr + .asFunction, int)>(); + + void wire_session_create_dir( + int port_, + ffi.Pointer id, + int act_id, + ffi.Pointer path, + bool is_remote, + ) { + return _wire_session_create_dir( + port_, + id, + act_id, + path, + is_remote, + ); + } + + late final _wire_session_create_dirPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Int64, + ffi.Pointer, + ffi.Int32, + ffi.Pointer, + ffi.Bool)>>('wire_session_create_dir'); + late final _wire_session_create_dir = _wire_session_create_dirPtr.asFunction< + void Function(int, ffi.Pointer, int, + ffi.Pointer, bool)>(); + + void wire_session_read_local_dir_sync( + int port_, + ffi.Pointer id, + ffi.Pointer path, + bool show_hidden, + ) { + return _wire_session_read_local_dir_sync( + port_, + id, + path, + show_hidden, + ); + } + + late final _wire_session_read_local_dir_syncPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Int64, + ffi.Pointer, + ffi.Pointer, + ffi.Bool)>>('wire_session_read_local_dir_sync'); + late final _wire_session_read_local_dir_sync = + _wire_session_read_local_dir_syncPtr.asFunction< + void Function(int, ffi.Pointer, + ffi.Pointer, bool)>(); + + void wire_session_get_platform( + int port_, + ffi.Pointer id, + bool is_remote, + ) { + return _wire_session_get_platform( + port_, + id, + is_remote, + ); + } + + late final _wire_session_get_platformPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Int64, ffi.Pointer, + ffi.Bool)>>('wire_session_get_platform'); + late final _wire_session_get_platform = _wire_session_get_platformPtr + .asFunction, bool)>(); + + void wire_session_load_last_transfer_jobs( + int port_, + ffi.Pointer id, + ) { + return _wire_session_load_last_transfer_jobs( + port_, + id, + ); + } + + late final _wire_session_load_last_transfer_jobsPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Int64, ffi.Pointer)>>( + 'wire_session_load_last_transfer_jobs'); + late final _wire_session_load_last_transfer_jobs = + _wire_session_load_last_transfer_jobsPtr + .asFunction)>(); + + void wire_session_add_job( + int port_, + ffi.Pointer id, + int act_id, + ffi.Pointer path, + ffi.Pointer to, + int file_num, + bool include_hidden, + bool is_remote, + ) { + return _wire_session_add_job( + port_, + id, + act_id, + path, + to, + file_num, + include_hidden, + is_remote, + ); + } + + late final _wire_session_add_jobPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Int64, + ffi.Pointer, + ffi.Int32, + ffi.Pointer, + ffi.Pointer, + ffi.Int32, + ffi.Bool, + ffi.Bool)>>('wire_session_add_job'); + late final _wire_session_add_job = _wire_session_add_jobPtr.asFunction< + void Function( + int, + ffi.Pointer, + int, + ffi.Pointer, + ffi.Pointer, + int, + bool, + bool)>(); + + void wire_session_resume_job( + int port_, + ffi.Pointer id, + int act_id, + bool is_remote, + ) { + return _wire_session_resume_job( + port_, + id, + act_id, + is_remote, + ); + } + + late final _wire_session_resume_jobPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Int64, ffi.Pointer, ffi.Int32, + ffi.Bool)>>('wire_session_resume_job'); + late final _wire_session_resume_job = _wire_session_resume_jobPtr.asFunction< + void Function(int, ffi.Pointer, int, bool)>(); + + void wire_main_get_sound_inputs( + int port_, + ) { + return _wire_main_get_sound_inputs( + port_, + ); + } + + late final _wire_main_get_sound_inputsPtr = + _lookup>( + 'wire_main_get_sound_inputs'); + late final _wire_main_get_sound_inputs = + _wire_main_get_sound_inputsPtr.asFunction(); + + void wire_main_change_id( + int port_, + ffi.Pointer new_id, + ) { + return _wire_main_change_id( + port_, + new_id, + ); + } + + late final _wire_main_change_idPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Int64, + ffi.Pointer)>>('wire_main_change_id'); + late final _wire_main_change_id = _wire_main_change_idPtr + .asFunction)>(); + + void wire_main_get_async_status( + int port_, + ) { + return _wire_main_get_async_status( + port_, + ); + } + + late final _wire_main_get_async_statusPtr = + _lookup>( + 'wire_main_get_async_status'); + late final _wire_main_get_async_status = + _wire_main_get_async_statusPtr.asFunction(); + + void wire_main_get_option( + int port_, + ffi.Pointer key, + ) { + return _wire_main_get_option( + port_, + key, + ); + } + + late final _wire_main_get_optionPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Int64, + ffi.Pointer)>>('wire_main_get_option'); + late final _wire_main_get_option = _wire_main_get_optionPtr + .asFunction)>(); + + void wire_main_set_option( + int port_, + ffi.Pointer key, + ffi.Pointer value, + ) { + return _wire_main_set_option( + port_, + key, + value, + ); + } + + late final _wire_main_set_optionPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Int64, ffi.Pointer, + ffi.Pointer)>>('wire_main_set_option'); + late final _wire_main_set_option = _wire_main_set_optionPtr.asFunction< + void Function( + int, ffi.Pointer, ffi.Pointer)>(); + + void wire_main_get_options( + int port_, + ) { + return _wire_main_get_options( + port_, + ); + } + + late final _wire_main_get_optionsPtr = + _lookup>( + 'wire_main_get_options'); + late final _wire_main_get_options = + _wire_main_get_optionsPtr.asFunction(); + + void wire_main_set_options( + int port_, + ffi.Pointer json, + ) { + return _wire_main_set_options( + port_, + json, + ); + } + + late final _wire_main_set_optionsPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Int64, + ffi.Pointer)>>('wire_main_set_options'); + late final _wire_main_set_options = _wire_main_set_optionsPtr + .asFunction)>(); + + void wire_main_test_if_valid_server( + int port_, + ffi.Pointer server, + ) { + return _wire_main_test_if_valid_server( + port_, + server, + ); + } + + late final _wire_main_test_if_valid_serverPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Int64, ffi.Pointer)>>( + 'wire_main_test_if_valid_server'); + late final _wire_main_test_if_valid_server = + _wire_main_test_if_valid_serverPtr + .asFunction)>(); + + void wire_main_set_socks( + int port_, + ffi.Pointer proxy, + ffi.Pointer username, + ffi.Pointer password, + ) { + return _wire_main_set_socks( + port_, + proxy, + username, + password, + ); + } + + late final _wire_main_set_socksPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Int64, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer)>>('wire_main_set_socks'); + late final _wire_main_set_socks = _wire_main_set_socksPtr.asFunction< + void Function(int, ffi.Pointer, + ffi.Pointer, ffi.Pointer)>(); + + void wire_main_get_socks( + int port_, + ) { + return _wire_main_get_socks( + port_, + ); + } + + late final _wire_main_get_socksPtr = + _lookup>( + 'wire_main_get_socks'); + late final _wire_main_get_socks = + _wire_main_get_socksPtr.asFunction(); + + void wire_main_get_app_name( + int port_, + ) { + return _wire_main_get_app_name( + port_, + ); + } + + late final _wire_main_get_app_namePtr = + _lookup>( + 'wire_main_get_app_name'); + late final _wire_main_get_app_name = + _wire_main_get_app_namePtr.asFunction(); + + void wire_main_get_license( + int port_, + ) { + return _wire_main_get_license( + port_, + ); + } + + late final _wire_main_get_licensePtr = + _lookup>( + 'wire_main_get_license'); + late final _wire_main_get_license = + _wire_main_get_licensePtr.asFunction(); + + void wire_main_get_version( + int port_, + ) { + return _wire_main_get_version( + port_, + ); + } + + late final _wire_main_get_versionPtr = + _lookup>( + 'wire_main_get_version'); + late final _wire_main_get_version = + _wire_main_get_versionPtr.asFunction(); + + void wire_main_get_fav( + int port_, + ) { + return _wire_main_get_fav( + port_, + ); + } + + late final _wire_main_get_favPtr = + _lookup>( + 'wire_main_get_fav'); + late final _wire_main_get_fav = + _wire_main_get_favPtr.asFunction(); + + void wire_main_store_fav( + int port_, + ffi.Pointer favs, + ) { + return _wire_main_store_fav( + port_, + favs, + ); + } + + late final _wire_main_store_favPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Int64, ffi.Pointer)>>('wire_main_store_fav'); + late final _wire_main_store_fav = _wire_main_store_favPtr + .asFunction)>(); + + void wire_main_get_peer( + int port_, + ffi.Pointer id, + ) { + return _wire_main_get_peer( + port_, + id, + ); + } + + late final _wire_main_get_peerPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Int64, ffi.Pointer)>>('wire_main_get_peer'); + late final _wire_main_get_peer = _wire_main_get_peerPtr + .asFunction)>(); + + void wire_main_get_lan_peers( + int port_, + ) { + return _wire_main_get_lan_peers( + port_, + ); + } + + late final _wire_main_get_lan_peersPtr = + _lookup>( + 'wire_main_get_lan_peers'); + late final _wire_main_get_lan_peers = + _wire_main_get_lan_peersPtr.asFunction(); + + void wire_main_get_connect_status( + int port_, + ) { + return _wire_main_get_connect_status( + port_, + ); + } + + late final _wire_main_get_connect_statusPtr = + _lookup>( + 'wire_main_get_connect_status'); + late final _wire_main_get_connect_status = + _wire_main_get_connect_statusPtr.asFunction(); + + void wire_main_check_connect_status( + int port_, + ) { + return _wire_main_check_connect_status( + port_, + ); + } + + late final _wire_main_check_connect_statusPtr = + _lookup>( + 'wire_main_check_connect_status'); + late final _wire_main_check_connect_status = + _wire_main_check_connect_statusPtr.asFunction(); + + void wire_main_is_using_public_server( + int port_, + ) { + return _wire_main_is_using_public_server( + port_, + ); + } + + late final _wire_main_is_using_public_serverPtr = + _lookup>( + 'wire_main_is_using_public_server'); + late final _wire_main_is_using_public_server = + _wire_main_is_using_public_serverPtr.asFunction(); + + void wire_main_discover( + int port_, + ) { + return _wire_main_discover( + port_, + ); + } + + late final _wire_main_discoverPtr = + _lookup>( + 'wire_main_discover'); + late final _wire_main_discover = + _wire_main_discoverPtr.asFunction(); + + void wire_main_has_rendezvous_service( + int port_, + ) { + return _wire_main_has_rendezvous_service( + port_, + ); + } + + late final _wire_main_has_rendezvous_servicePtr = + _lookup>( + 'wire_main_has_rendezvous_service'); + late final _wire_main_has_rendezvous_service = + _wire_main_has_rendezvous_servicePtr.asFunction(); + + void wire_main_get_api_server( + int port_, + ) { + return _wire_main_get_api_server( + port_, + ); + } + + late final _wire_main_get_api_serverPtr = + _lookup>( + 'wire_main_get_api_server'); + late final _wire_main_get_api_server = + _wire_main_get_api_serverPtr.asFunction(); + + void wire_main_post_request( + int port_, + ffi.Pointer url, + ffi.Pointer body, + ffi.Pointer header, + ) { + return _wire_main_post_request( + port_, + url, + body, + header, + ); + } + + late final _wire_main_post_requestPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Int64, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer)>>('wire_main_post_request'); + late final _wire_main_post_request = _wire_main_post_requestPtr.asFunction< + void Function(int, ffi.Pointer, + ffi.Pointer, ffi.Pointer)>(); + + void wire_main_get_local_option( + int port_, + ffi.Pointer key, + ) { + return _wire_main_get_local_option( + port_, + key, + ); + } + + late final _wire_main_get_local_optionPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Int64, + ffi.Pointer)>>('wire_main_get_local_option'); + late final _wire_main_get_local_option = _wire_main_get_local_optionPtr + .asFunction)>(); + + void wire_main_set_local_option( + int port_, + ffi.Pointer key, + ffi.Pointer value, + ) { + return _wire_main_set_local_option( + port_, + key, + value, + ); + } + + late final _wire_main_set_local_optionPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Int64, ffi.Pointer, + ffi.Pointer)>>('wire_main_set_local_option'); + late final _wire_main_set_local_option = + _wire_main_set_local_optionPtr.asFunction< + void Function(int, ffi.Pointer, + ffi.Pointer)>(); + + void wire_main_get_my_id( + int port_, + ) { + return _wire_main_get_my_id( + port_, + ); + } + + late final _wire_main_get_my_idPtr = + _lookup>( + 'wire_main_get_my_id'); + late final _wire_main_get_my_id = + _wire_main_get_my_idPtr.asFunction(); + + void wire_main_get_uuid( + int port_, + ) { + return _wire_main_get_uuid( + port_, + ); + } + + late final _wire_main_get_uuidPtr = + _lookup>( + 'wire_main_get_uuid'); + late final _wire_main_get_uuid = + _wire_main_get_uuidPtr.asFunction(); + + void wire_main_get_peer_option( + int port_, + ffi.Pointer id, + ffi.Pointer key, + ) { + return _wire_main_get_peer_option( + port_, + id, + key, + ); + } + + late final _wire_main_get_peer_optionPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Int64, ffi.Pointer, + ffi.Pointer)>>('wire_main_get_peer_option'); + late final _wire_main_get_peer_option = + _wire_main_get_peer_optionPtr.asFunction< + void Function(int, ffi.Pointer, + ffi.Pointer)>(); + + void wire_main_set_peer_option( + int port_, + ffi.Pointer id, + ffi.Pointer key, + ffi.Pointer value, + ) { + return _wire_main_set_peer_option( + port_, + id, + key, + value, + ); + } + + late final _wire_main_set_peer_optionPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Int64, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer)>>('wire_main_set_peer_option'); + late final _wire_main_set_peer_option = + _wire_main_set_peer_optionPtr.asFunction< + void Function(int, ffi.Pointer, + ffi.Pointer, ffi.Pointer)>(); + + void wire_main_forget_password( + int port_, + ffi.Pointer id, + ) { + return _wire_main_forget_password( + port_, + id, + ); + } + + late final _wire_main_forget_passwordPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Int64, + ffi.Pointer)>>('wire_main_forget_password'); + late final _wire_main_forget_password = _wire_main_forget_passwordPtr + .asFunction)>(); + + void wire_main_get_recent_peers( + int port_, + ) { + return _wire_main_get_recent_peers( + port_, + ); + } + + late final _wire_main_get_recent_peersPtr = + _lookup>( + 'wire_main_get_recent_peers'); + late final _wire_main_get_recent_peers = + _wire_main_get_recent_peersPtr.asFunction(); + + void wire_main_load_recent_peers( + int port_, + ) { + return _wire_main_load_recent_peers( + port_, + ); + } + + late final _wire_main_load_recent_peersPtr = + _lookup>( + 'wire_main_load_recent_peers'); + late final _wire_main_load_recent_peers = + _wire_main_load_recent_peersPtr.asFunction(); + + void wire_main_load_fav_peers( + int port_, + ) { + return _wire_main_load_fav_peers( + port_, + ); + } + + late final _wire_main_load_fav_peersPtr = + _lookup>( + 'wire_main_load_fav_peers'); + late final _wire_main_load_fav_peers = + _wire_main_load_fav_peersPtr.asFunction(); + + void wire_main_load_lan_peers( + int port_, + ) { + return _wire_main_load_lan_peers( + port_, + ); + } + + late final _wire_main_load_lan_peersPtr = + _lookup>( + 'wire_main_load_lan_peers'); + late final _wire_main_load_lan_peers = + _wire_main_load_lan_peersPtr.asFunction(); + + void wire_main_get_last_remote_id( + int port_, + ) { + return _wire_main_get_last_remote_id( + port_, + ); + } + + late final _wire_main_get_last_remote_idPtr = + _lookup>( + 'wire_main_get_last_remote_id'); + late final _wire_main_get_last_remote_id = + _wire_main_get_last_remote_idPtr.asFunction(); + + void wire_main_get_software_update_url( + int port_, + ) { + return _wire_main_get_software_update_url( + port_, + ); + } + + late final _wire_main_get_software_update_urlPtr = + _lookup>( + 'wire_main_get_software_update_url'); + late final _wire_main_get_software_update_url = + _wire_main_get_software_update_urlPtr.asFunction(); + + void wire_main_get_home_dir( + int port_, + ) { + return _wire_main_get_home_dir( + port_, + ); + } + + late final _wire_main_get_home_dirPtr = + _lookup>( + 'wire_main_get_home_dir'); + late final _wire_main_get_home_dir = + _wire_main_get_home_dirPtr.asFunction(); + + void wire_main_get_langs( + int port_, + ) { + return _wire_main_get_langs( + port_, + ); + } + + late final _wire_main_get_langsPtr = + _lookup>( + 'wire_main_get_langs'); + late final _wire_main_get_langs = + _wire_main_get_langsPtr.asFunction(); + + void wire_main_get_temporary_password( + int port_, + ) { + return _wire_main_get_temporary_password( + port_, + ); + } + + late final _wire_main_get_temporary_passwordPtr = + _lookup>( + 'wire_main_get_temporary_password'); + late final _wire_main_get_temporary_password = + _wire_main_get_temporary_passwordPtr.asFunction(); + + void wire_main_get_permanent_password( + int port_, + ) { + return _wire_main_get_permanent_password( + port_, + ); + } + + late final _wire_main_get_permanent_passwordPtr = + _lookup>( + 'wire_main_get_permanent_password'); + late final _wire_main_get_permanent_password = + _wire_main_get_permanent_passwordPtr.asFunction(); + + void wire_main_get_online_statue( + int port_, + ) { + return _wire_main_get_online_statue( + port_, + ); + } + + late final _wire_main_get_online_statuePtr = + _lookup>( + 'wire_main_get_online_statue'); + late final _wire_main_get_online_statue = + _wire_main_get_online_statuePtr.asFunction(); + + void wire_main_get_clients_state( + int port_, + ) { + return _wire_main_get_clients_state( + port_, + ); + } + + late final _wire_main_get_clients_statePtr = + _lookup>( + 'wire_main_get_clients_state'); + late final _wire_main_get_clients_state = + _wire_main_get_clients_statePtr.asFunction(); + + void wire_main_check_clients_length( + int port_, + int length, + ) { + return _wire_main_check_clients_length( + port_, + length, + ); + } + + late final _wire_main_check_clients_lengthPtr = + _lookup>( + 'wire_main_check_clients_length'); + late final _wire_main_check_clients_length = + _wire_main_check_clients_lengthPtr.asFunction(); + + void wire_main_init( + int port_, + ffi.Pointer app_dir, + ) { + return _wire_main_init( + port_, + app_dir, + ); + } + + late final _wire_main_initPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Int64, ffi.Pointer)>>('wire_main_init'); + late final _wire_main_init = _wire_main_initPtr + .asFunction)>(); + + void wire_main_device_id( + int port_, + ffi.Pointer id, + ) { + return _wire_main_device_id( + port_, + id, + ); + } + + late final _wire_main_device_idPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Int64, + ffi.Pointer)>>('wire_main_device_id'); + late final _wire_main_device_id = _wire_main_device_idPtr + .asFunction)>(); + + void wire_main_device_name( + int port_, + ffi.Pointer name, + ) { + return _wire_main_device_name( + port_, + name, + ); + } + + late final _wire_main_device_namePtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Int64, + ffi.Pointer)>>('wire_main_device_name'); + late final _wire_main_device_name = _wire_main_device_namePtr + .asFunction)>(); + + void wire_main_remove_peer( + int port_, + ffi.Pointer id, + ) { + return _wire_main_remove_peer( + port_, + id, + ); + } + + late final _wire_main_remove_peerPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Int64, + ffi.Pointer)>>('wire_main_remove_peer'); + late final _wire_main_remove_peer = _wire_main_remove_peerPtr + .asFunction)>(); + + void wire_main_has_hwcodec( + int port_, + ) { + return _wire_main_has_hwcodec( + port_, + ); + } + + late final _wire_main_has_hwcodecPtr = + _lookup>( + 'wire_main_has_hwcodec'); + late final _wire_main_has_hwcodec = + _wire_main_has_hwcodecPtr.asFunction(); + + void wire_session_send_mouse( + int port_, + ffi.Pointer id, + ffi.Pointer msg, + ) { + return _wire_session_send_mouse( + port_, + id, + msg, + ); + } + + late final _wire_session_send_mousePtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Int64, ffi.Pointer, + ffi.Pointer)>>('wire_session_send_mouse'); + late final _wire_session_send_mouse = _wire_session_send_mousePtr.asFunction< + void Function( + int, ffi.Pointer, ffi.Pointer)>(); + + void wire_session_restart_remote_device( + int port_, + ffi.Pointer id, + ) { + return _wire_session_restart_remote_device( + port_, + id, + ); + } + + late final _wire_session_restart_remote_devicePtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Int64, ffi.Pointer)>>( + 'wire_session_restart_remote_device'); + late final _wire_session_restart_remote_device = + _wire_session_restart_remote_devicePtr + .asFunction)>(); + + void wire_main_set_home_dir( + int port_, + ffi.Pointer home, + ) { + return _wire_main_set_home_dir( + port_, + home, + ); + } + + late final _wire_main_set_home_dirPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Int64, + ffi.Pointer)>>('wire_main_set_home_dir'); + late final _wire_main_set_home_dir = _wire_main_set_home_dirPtr + .asFunction)>(); + + void wire_main_stop_service( + int port_, + ) { + return _wire_main_stop_service( + port_, + ); + } + + late final _wire_main_stop_servicePtr = + _lookup>( + 'wire_main_stop_service'); + late final _wire_main_stop_service = + _wire_main_stop_servicePtr.asFunction(); + + void wire_main_start_service( + int port_, + ) { + return _wire_main_start_service( + port_, + ); + } + + late final _wire_main_start_servicePtr = + _lookup>( + 'wire_main_start_service'); + late final _wire_main_start_service = + _wire_main_start_servicePtr.asFunction(); + + void wire_main_update_temporary_password( + int port_, + ) { + return _wire_main_update_temporary_password( + port_, + ); + } + + late final _wire_main_update_temporary_passwordPtr = + _lookup>( + 'wire_main_update_temporary_password'); + late final _wire_main_update_temporary_password = + _wire_main_update_temporary_passwordPtr.asFunction(); + + void wire_main_set_permanent_password( + int port_, + ffi.Pointer password, + ) { + return _wire_main_set_permanent_password( + port_, + password, + ); + } + + late final _wire_main_set_permanent_passwordPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Int64, ffi.Pointer)>>( + 'wire_main_set_permanent_password'); + late final _wire_main_set_permanent_password = + _wire_main_set_permanent_passwordPtr + .asFunction)>(); + + void wire_main_check_super_user_permission( + int port_, + ) { + return _wire_main_check_super_user_permission( + port_, + ); + } + + late final _wire_main_check_super_user_permissionPtr = + _lookup>( + 'wire_main_check_super_user_permission'); + late final _wire_main_check_super_user_permission = + _wire_main_check_super_user_permissionPtr + .asFunction(); + + void wire_cm_send_chat( + int port_, + int conn_id, + ffi.Pointer msg, + ) { + return _wire_cm_send_chat( + port_, + conn_id, + msg, + ); + } + + late final _wire_cm_send_chatPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Int64, ffi.Int32, + ffi.Pointer)>>('wire_cm_send_chat'); + late final _wire_cm_send_chat = _wire_cm_send_chatPtr + .asFunction)>(); + + void wire_cm_login_res( + int port_, + int conn_id, + bool res, + ) { + return _wire_cm_login_res( + port_, + conn_id, + res, + ); + } + + late final _wire_cm_login_resPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Int64, ffi.Int32, ffi.Bool)>>('wire_cm_login_res'); + late final _wire_cm_login_res = + _wire_cm_login_resPtr.asFunction(); + + void wire_cm_close_connection( + int port_, + int conn_id, + ) { + return _wire_cm_close_connection( + port_, + conn_id, + ); + } + + late final _wire_cm_close_connectionPtr = + _lookup>( + 'wire_cm_close_connection'); + late final _wire_cm_close_connection = + _wire_cm_close_connectionPtr.asFunction(); + + void wire_cm_check_click_time( + int port_, + int conn_id, + ) { + return _wire_cm_check_click_time( + port_, + conn_id, + ); + } + + late final _wire_cm_check_click_timePtr = + _lookup>( + 'wire_cm_check_click_time'); + late final _wire_cm_check_click_time = + _wire_cm_check_click_timePtr.asFunction(); + + void wire_cm_get_click_time( + int port_, + ) { + return _wire_cm_get_click_time( + port_, + ); + } + + late final _wire_cm_get_click_timePtr = + _lookup>( + 'wire_cm_get_click_time'); + late final _wire_cm_get_click_time = + _wire_cm_get_click_timePtr.asFunction(); + + void wire_cm_switch_permission( + int port_, + int conn_id, + ffi.Pointer name, + bool enabled, + ) { + return _wire_cm_switch_permission( + port_, + conn_id, + name, + enabled, + ); + } + + late final _wire_cm_switch_permissionPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Int64, ffi.Int32, ffi.Pointer, + ffi.Bool)>>('wire_cm_switch_permission'); + late final _wire_cm_switch_permission = + _wire_cm_switch_permissionPtr.asFunction< + void Function(int, int, ffi.Pointer, bool)>(); + + void wire_main_get_icon( + int port_, + ) { + return _wire_main_get_icon( + port_, + ); + } + + late final _wire_main_get_iconPtr = + _lookup>( + 'wire_main_get_icon'); + late final _wire_main_get_icon = + _wire_main_get_iconPtr.asFunction(); + + void wire_query_onlines( + int port_, + ffi.Pointer ids, + ) { + return _wire_query_onlines( + port_, + ids, + ); + } + + late final _wire_query_onlinesPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Int64, ffi.Pointer)>>('wire_query_onlines'); + late final _wire_query_onlines = _wire_query_onlinesPtr + .asFunction)>(); + + ffi.Pointer new_StringList( + int len, + ) { + return _new_StringList( + len, + ); + } + + late final _new_StringListPtr = _lookup< + ffi.NativeFunction Function(ffi.Int32)>>( + 'new_StringList'); + late final _new_StringList = _new_StringListPtr + .asFunction Function(int)>(); + + ffi.Pointer new_uint_8_list( + int len, + ) { + return _new_uint_8_list( + len, + ); + } + + late final _new_uint_8_listPtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function( + ffi.Int32)>>('new_uint_8_list'); + late final _new_uint_8_list = _new_uint_8_listPtr + .asFunction Function(int)>(); + + void free_WireSyncReturnStruct( + WireSyncReturnStruct val, + ) { + return _free_WireSyncReturnStruct( + val, + ); + } + + late final _free_WireSyncReturnStructPtr = + _lookup>( + 'free_WireSyncReturnStruct'); + late final _free_WireSyncReturnStruct = _free_WireSyncReturnStructPtr + .asFunction(); + + void store_dart_post_cobject( + DartPostCObjectFnType ptr, + ) { + return _store_dart_post_cobject( + ptr, + ); + } + + late final _store_dart_post_cobjectPtr = + _lookup>( + 'store_dart_post_cobject'); + late final _store_dart_post_cobject = _store_dart_post_cobjectPtr + .asFunction(); + + bool rustdesk_core_main() { + return _rustdesk_core_main(); + } + + late final _rustdesk_core_mainPtr = + _lookup>('rustdesk_core_main'); + late final _rustdesk_core_main = + _rustdesk_core_mainPtr.asFunction(); +} + +class wire_uint_8_list extends ffi.Struct { + external ffi.Pointer ptr; + + @ffi.Int32() + external int len; +} + +class wire_StringList extends ffi.Struct { + external ffi.Pointer> ptr; + + @ffi.Int32() + external int len; +} + +typedef uintptr_t = ffi.UnsignedLong; +typedef DartPostCObjectFnType = ffi.Pointer< + ffi.NativeFunction)>>; +typedef DartPort = ffi.Int64; + +const int GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT = 2; + +const int GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS = 4; diff --git a/flutter/lib/generated_bridge.freezed.dart b/flutter/lib/generated_bridge.freezed.dart new file mode 100644 index 000000000..fbaa6105f --- /dev/null +++ b/flutter/lib/generated_bridge.freezed.dart @@ -0,0 +1,332 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target + +part of 'generated_bridge.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + +/// @nodoc +mixin _$EventToUI { + @optionalTypeArgs + TResult when({ + required TResult Function(String field0) event, + required TResult Function(Uint8List field0) rgba, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult Function(String field0)? event, + TResult Function(Uint8List field0)? rgba, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(String field0)? event, + TResult Function(Uint8List field0)? rgba, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(Event value) event, + required TResult Function(Rgba value) rgba, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult Function(Event value)? event, + TResult Function(Rgba value)? rgba, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(Event value)? event, + TResult Function(Rgba value)? rgba, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $EventToUICopyWith<$Res> { + factory $EventToUICopyWith(EventToUI value, $Res Function(EventToUI) then) = + _$EventToUICopyWithImpl<$Res>; +} + +/// @nodoc +class _$EventToUICopyWithImpl<$Res> implements $EventToUICopyWith<$Res> { + _$EventToUICopyWithImpl(this._value, this._then); + + final EventToUI _value; + // ignore: unused_field + final $Res Function(EventToUI) _then; +} + +/// @nodoc +abstract class _$$EventCopyWith<$Res> { + factory _$$EventCopyWith(_$Event value, $Res Function(_$Event) then) = + __$$EventCopyWithImpl<$Res>; + $Res call({String field0}); +} + +/// @nodoc +class __$$EventCopyWithImpl<$Res> extends _$EventToUICopyWithImpl<$Res> + implements _$$EventCopyWith<$Res> { + __$$EventCopyWithImpl(_$Event _value, $Res Function(_$Event) _then) + : super(_value, (v) => _then(v as _$Event)); + + @override + _$Event get _value => super._value as _$Event; + + @override + $Res call({ + Object? field0 = freezed, + }) { + return _then(_$Event( + field0 == freezed + ? _value.field0 + : field0 // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc + +class _$Event implements Event { + const _$Event(this.field0); + + @override + final String field0; + + @override + String toString() { + return 'EventToUI.event(field0: $field0)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$Event && + const DeepCollectionEquality().equals(other.field0, field0)); + } + + @override + int get hashCode => + Object.hash(runtimeType, const DeepCollectionEquality().hash(field0)); + + @JsonKey(ignore: true) + @override + _$$EventCopyWith<_$Event> get copyWith => + __$$EventCopyWithImpl<_$Event>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(String field0) event, + required TResult Function(Uint8List field0) rgba, + }) { + return event(field0); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult Function(String field0)? event, + TResult Function(Uint8List field0)? rgba, + }) { + return event?.call(field0); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(String field0)? event, + TResult Function(Uint8List field0)? rgba, + required TResult orElse(), + }) { + if (event != null) { + return event(field0); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(Event value) event, + required TResult Function(Rgba value) rgba, + }) { + return event(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult Function(Event value)? event, + TResult Function(Rgba value)? rgba, + }) { + return event?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(Event value)? event, + TResult Function(Rgba value)? rgba, + required TResult orElse(), + }) { + if (event != null) { + return event(this); + } + return orElse(); + } +} + +abstract class Event implements EventToUI { + const factory Event(final String field0) = _$Event; + + String get field0; + @JsonKey(ignore: true) + _$$EventCopyWith<_$Event> get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$RgbaCopyWith<$Res> { + factory _$$RgbaCopyWith(_$Rgba value, $Res Function(_$Rgba) then) = + __$$RgbaCopyWithImpl<$Res>; + $Res call({Uint8List field0}); +} + +/// @nodoc +class __$$RgbaCopyWithImpl<$Res> extends _$EventToUICopyWithImpl<$Res> + implements _$$RgbaCopyWith<$Res> { + __$$RgbaCopyWithImpl(_$Rgba _value, $Res Function(_$Rgba) _then) + : super(_value, (v) => _then(v as _$Rgba)); + + @override + _$Rgba get _value => super._value as _$Rgba; + + @override + $Res call({ + Object? field0 = freezed, + }) { + return _then(_$Rgba( + field0 == freezed + ? _value.field0 + : field0 // ignore: cast_nullable_to_non_nullable + as Uint8List, + )); + } +} + +/// @nodoc + +class _$Rgba implements Rgba { + const _$Rgba(this.field0); + + @override + final Uint8List field0; + + @override + String toString() { + return 'EventToUI.rgba(field0: $field0)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$Rgba && + const DeepCollectionEquality().equals(other.field0, field0)); + } + + @override + int get hashCode => + Object.hash(runtimeType, const DeepCollectionEquality().hash(field0)); + + @JsonKey(ignore: true) + @override + _$$RgbaCopyWith<_$Rgba> get copyWith => + __$$RgbaCopyWithImpl<_$Rgba>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(String field0) event, + required TResult Function(Uint8List field0) rgba, + }) { + return rgba(field0); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult Function(String field0)? event, + TResult Function(Uint8List field0)? rgba, + }) { + return rgba?.call(field0); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(String field0)? event, + TResult Function(Uint8List field0)? rgba, + required TResult orElse(), + }) { + if (rgba != null) { + return rgba(field0); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(Event value) event, + required TResult Function(Rgba value) rgba, + }) { + return rgba(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult Function(Event value)? event, + TResult Function(Rgba value)? rgba, + }) { + return rgba?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(Event value)? event, + TResult Function(Rgba value)? rgba, + required TResult orElse(), + }) { + if (rgba != null) { + return rgba(this); + } + return orElse(); + } +} + +abstract class Rgba implements EventToUI { + const factory Rgba(final Uint8List field0) = _$Rgba; + + Uint8List get field0; + @JsonKey(ignore: true) + _$$RgbaCopyWith<_$Rgba> get copyWith => throw _privateConstructorUsedError; +} diff --git a/flutter/lib/generated_plugin_registrant.dart b/flutter/lib/generated_plugin_registrant.dart new file mode 100644 index 000000000..eba9fb8cc --- /dev/null +++ b/flutter/lib/generated_plugin_registrant.dart @@ -0,0 +1,35 @@ +// +// Generated file. Do not edit. +// + +// ignore_for_file: directives_ordering +// ignore_for_file: lines_longer_than_80_chars +// ignore_for_file: depend_on_referenced_packages + +import 'package:desktop_drop/desktop_drop_web.dart'; +import 'package:device_info_plus_web/device_info_plus_web.dart'; +import 'package:firebase_analytics_web/firebase_analytics_web.dart'; +import 'package:firebase_core_web/firebase_core_web.dart'; +import 'package:image_picker_for_web/image_picker_for_web.dart'; +import 'package:package_info_plus_web/package_info_plus_web.dart'; +import 'package:shared_preferences_web/shared_preferences_web.dart'; +import 'package:url_launcher_web/url_launcher_web.dart'; +import 'package:video_player_web/video_player_web.dart'; +import 'package:wakelock_web/wakelock_web.dart'; + +import 'package:flutter_web_plugins/flutter_web_plugins.dart'; + +// ignore: public_member_api_docs +void registerPlugins(Registrar registrar) { + DesktopDropWeb.registerWith(registrar); + DeviceInfoPlusPlugin.registerWith(registrar); + FirebaseAnalyticsWeb.registerWith(registrar); + FirebaseCoreWeb.registerWith(registrar); + ImagePickerPlugin.registerWith(registrar); + PackageInfoPlugin.registerWith(registrar); + SharedPreferencesPlugin.registerWith(registrar); + UrlLauncherPlugin.registerWith(registrar); + VideoPlayerPlugin.registerWith(registrar); + WakelockWeb.registerWith(registrar); + registrar.registerMessageHandler(); +} diff --git a/flutter/macos/Podfile.lock b/flutter/macos/Podfile.lock index a83616180..e417eb188 100644 --- a/flutter/macos/Podfile.lock +++ b/flutter/macos/Podfile.lock @@ -1,78 +1,84 @@ PODS: - - bitsdojo_window_macos (0.0.1): + - desktop_drop (0.0.1): - FlutterMacOS - desktop_multi_window (0.0.1): - FlutterMacOS - device_info_plus_macos (0.0.1): - FlutterMacOS - - Firebase/Analytics (8.15.0): + - Firebase/Analytics (9.4.0): - Firebase/Core - - Firebase/Core (8.15.0): + - Firebase/Core (9.4.0): - Firebase/CoreOnly - - FirebaseAnalytics (~> 8.15.0) - - Firebase/CoreOnly (8.15.0): - - FirebaseCore (= 8.15.0) - - firebase_analytics (9.1.9): - - Firebase/Analytics (= 8.15.0) + - FirebaseAnalytics (~> 9.4.0) + - Firebase/CoreOnly (9.4.0): + - FirebaseCore (= 9.4.0) + - firebase_analytics (9.3.3): + - Firebase/Analytics (= 9.4.0) - firebase_core - FlutterMacOS - - firebase_core (1.17.1): - - Firebase/CoreOnly (~> 8.15.0) + - firebase_core (1.21.1): + - Firebase/CoreOnly (~> 9.4.0) - FlutterMacOS - - FirebaseAnalytics (8.15.0): - - FirebaseAnalytics/AdIdSupport (= 8.15.0) - - FirebaseCore (~> 8.0) - - FirebaseInstallations (~> 8.0) + - FirebaseAnalytics (9.4.0): + - FirebaseAnalytics/AdIdSupport (= 9.4.0) + - FirebaseCore (~> 9.0) + - FirebaseInstallations (~> 9.0) - GoogleUtilities/AppDelegateSwizzler (~> 7.7) - GoogleUtilities/MethodSwizzler (~> 7.7) - GoogleUtilities/Network (~> 7.7) - "GoogleUtilities/NSData+zlib (~> 7.7)" - - nanopb (~> 2.30908.0) - - FirebaseAnalytics/AdIdSupport (8.15.0): - - FirebaseCore (~> 8.0) - - FirebaseInstallations (~> 8.0) - - GoogleAppMeasurement (= 8.15.0) + - nanopb (< 2.30910.0, >= 2.30908.0) + - FirebaseAnalytics/AdIdSupport (9.4.0): + - FirebaseCore (~> 9.0) + - FirebaseInstallations (~> 9.0) + - GoogleAppMeasurement (= 9.4.0) - GoogleUtilities/AppDelegateSwizzler (~> 7.7) - GoogleUtilities/MethodSwizzler (~> 7.7) - GoogleUtilities/Network (~> 7.7) - "GoogleUtilities/NSData+zlib (~> 7.7)" - - nanopb (~> 2.30908.0) - - FirebaseCore (8.15.0): - - FirebaseCoreDiagnostics (~> 8.0) + - nanopb (< 2.30910.0, >= 2.30908.0) + - FirebaseCore (9.4.0): + - FirebaseCoreDiagnostics (~> 9.0) + - FirebaseCoreInternal (~> 9.0) - GoogleUtilities/Environment (~> 7.7) - GoogleUtilities/Logger (~> 7.7) - - FirebaseCoreDiagnostics (8.15.0): - - GoogleDataTransport (~> 9.1) + - FirebaseCoreDiagnostics (9.5.0): + - GoogleDataTransport (< 10.0.0, >= 9.1.4) - GoogleUtilities/Environment (~> 7.7) - GoogleUtilities/Logger (~> 7.7) - - nanopb (~> 2.30908.0) - - FirebaseInstallations (8.15.0): - - FirebaseCore (~> 8.0) + - nanopb (< 2.30910.0, >= 2.30908.0) + - FirebaseCoreInternal (9.5.0): + - "GoogleUtilities/NSData+zlib (~> 7.7)" + - FirebaseInstallations (9.5.0): + - FirebaseCore (~> 9.0) - GoogleUtilities/Environment (~> 7.7) - GoogleUtilities/UserDefaults (~> 7.7) - - PromisesObjC (< 3.0, >= 1.2) + - PromisesObjC (~> 2.1) - FlutterMacOS (1.0.0) - - GoogleAppMeasurement (8.15.0): - - GoogleAppMeasurement/AdIdSupport (= 8.15.0) + - FMDB (2.7.5): + - FMDB/standard (= 2.7.5) + - FMDB/standard (2.7.5) + - GoogleAppMeasurement (9.4.0): + - GoogleAppMeasurement/AdIdSupport (= 9.4.0) - GoogleUtilities/AppDelegateSwizzler (~> 7.7) - GoogleUtilities/MethodSwizzler (~> 7.7) - GoogleUtilities/Network (~> 7.7) - "GoogleUtilities/NSData+zlib (~> 7.7)" - - nanopb (~> 2.30908.0) - - GoogleAppMeasurement/AdIdSupport (8.15.0): - - GoogleAppMeasurement/WithoutAdIdSupport (= 8.15.0) + - nanopb (< 2.30910.0, >= 2.30908.0) + - GoogleAppMeasurement/AdIdSupport (9.4.0): + - GoogleAppMeasurement/WithoutAdIdSupport (= 9.4.0) - GoogleUtilities/AppDelegateSwizzler (~> 7.7) - GoogleUtilities/MethodSwizzler (~> 7.7) - GoogleUtilities/Network (~> 7.7) - "GoogleUtilities/NSData+zlib (~> 7.7)" - - nanopb (~> 2.30908.0) - - GoogleAppMeasurement/WithoutAdIdSupport (8.15.0): + - nanopb (< 2.30910.0, >= 2.30908.0) + - GoogleAppMeasurement/WithoutAdIdSupport (9.4.0): - GoogleUtilities/AppDelegateSwizzler (~> 7.7) - GoogleUtilities/MethodSwizzler (~> 7.7) - GoogleUtilities/Network (~> 7.7) - "GoogleUtilities/NSData+zlib (~> 7.7)" - - nanopb (~> 2.30908.0) - - GoogleDataTransport (9.1.4): + - nanopb (< 2.30910.0, >= 2.30908.0) + - GoogleDataTransport (9.2.0): - GoogleUtilities/Environment (~> 7.7) - nanopb (< 2.30910.0, >= 2.30908.0) - PromisesObjC (< 3.0, >= 1.2) @@ -95,18 +101,25 @@ PODS: - GoogleUtilities/Logger - GoogleUtilities/UserDefaults (7.7.0): - GoogleUtilities/Logger - - nanopb (2.30908.0): - - nanopb/decode (= 2.30908.0) - - nanopb/encode (= 2.30908.0) - - nanopb/decode (2.30908.0) - - nanopb/encode (2.30908.0) + - nanopb (2.30909.0): + - nanopb/decode (= 2.30909.0) + - nanopb/encode (= 2.30909.0) + - nanopb/decode (2.30909.0) + - nanopb/encode (2.30909.0) - package_info_plus_macos (0.0.1): - FlutterMacOS - path_provider_macos (0.0.1): - FlutterMacOS - - PromisesObjC (2.1.0) + - PromisesObjC (2.1.1) + - screen_retriever (0.0.1): + - FlutterMacOS - shared_preferences_macos (0.0.1): - FlutterMacOS + - sqflite (0.0.2): + - FlutterMacOS + - FMDB (>= 2.7.5) + - tray_manager (0.0.1): + - FlutterMacOS - url_launcher_macos (0.0.1): - FlutterMacOS - wakelock_macos (0.0.1): @@ -115,7 +128,7 @@ PODS: - FlutterMacOS DEPENDENCIES: - - bitsdojo_window_macos (from `Flutter/ephemeral/.symlinks/plugins/bitsdojo_window_macos/macos`) + - desktop_drop (from `Flutter/ephemeral/.symlinks/plugins/desktop_drop/macos`) - desktop_multi_window (from `Flutter/ephemeral/.symlinks/plugins/desktop_multi_window/macos`) - device_info_plus_macos (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus_macos/macos`) - firebase_analytics (from `Flutter/ephemeral/.symlinks/plugins/firebase_analytics/macos`) @@ -123,7 +136,10 @@ DEPENDENCIES: - FlutterMacOS (from `Flutter/ephemeral`) - package_info_plus_macos (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus_macos/macos`) - path_provider_macos (from `Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos`) + - screen_retriever (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos`) - shared_preferences_macos (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_macos/macos`) + - sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/macos`) + - tray_manager (from `Flutter/ephemeral/.symlinks/plugins/tray_manager/macos`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) - wakelock_macos (from `Flutter/ephemeral/.symlinks/plugins/wakelock_macos/macos`) - window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`) @@ -134,7 +150,9 @@ SPEC REPOS: - FirebaseAnalytics - FirebaseCore - FirebaseCoreDiagnostics + - FirebaseCoreInternal - FirebaseInstallations + - FMDB - GoogleAppMeasurement - GoogleDataTransport - GoogleUtilities @@ -142,8 +160,8 @@ SPEC REPOS: - PromisesObjC EXTERNAL SOURCES: - bitsdojo_window_macos: - :path: Flutter/ephemeral/.symlinks/plugins/bitsdojo_window_macos/macos + desktop_drop: + :path: Flutter/ephemeral/.symlinks/plugins/desktop_drop/macos desktop_multi_window: :path: Flutter/ephemeral/.symlinks/plugins/desktop_multi_window/macos device_info_plus_macos: @@ -158,8 +176,14 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus_macos/macos path_provider_macos: :path: Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos + screen_retriever: + :path: Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos shared_preferences_macos: :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_macos/macos + sqflite: + :path: Flutter/ephemeral/.symlinks/plugins/sqflite/macos + tray_manager: + :path: Flutter/ephemeral/.symlinks/plugins/tray_manager/macos url_launcher_macos: :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos wakelock_macos: @@ -168,25 +192,30 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos SPEC CHECKSUMS: - bitsdojo_window_macos: 44e3b8fe3dd463820e0321f6256c5b1c16bb6a00 + desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898 desktop_multi_window: 566489c048b501134f9d7fb6a2354c60a9126486 device_info_plus_macos: 1ad388a1ef433505c4038e7dd9605aadd1e2e9c7 - Firebase: 5f8193dff4b5b7c5d5ef72ae54bb76c08e2b841d - firebase_analytics: d448483150504ed84f25c5437a34af2591a7929e - firebase_core: 7b87364e2d1eae70018a60698e89e7d6f5320bad - FirebaseAnalytics: 7761cbadb00a717d8d0939363eb46041526474fa - FirebaseCore: 5743c5785c074a794d35f2fff7ecc254a91e08b1 - FirebaseCoreDiagnostics: 92e07a649aeb66352b319d43bdd2ee3942af84cb - FirebaseInstallations: 40bd9054049b2eae9a2c38ef1c3dd213df3605cd + Firebase: 7703fc4022824b6d6db1bf7bea58d13b8e17ec46 + firebase_analytics: 57144bae6cd39d3be367a8767a1b8857a037cee5 + firebase_core: 822a1076483bf9764284322c9310daa98e1e6817 + FirebaseAnalytics: a1a24e72b7ba7f47045a4633f1abb545c07bd29c + FirebaseCore: 9a2b10270a854731c4d4d8a97d0aa8380ec3458d + FirebaseCoreDiagnostics: 17cbf4e72b1dbd64bfdc33d4b1f07bce4f16f1d8 + FirebaseCoreInternal: 50a8e39cae8abf72d5145d07ea34c3244f70862b + FirebaseInstallations: 41f811b530c41dd90973d0174381cdb3fcb5e839 FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424 - GoogleAppMeasurement: 4c19f031220c72464d460c9daa1fb5d1acce958e - GoogleDataTransport: 5fffe35792f8b96ec8d6775f5eccd83c998d5a3b + FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a + GoogleAppMeasurement: 5d69e04287fc2c10cc43724bfa4bf31fc12c3dff + GoogleDataTransport: 1c8145da7117bd68bbbed00cf304edb6a24de00f GoogleUtilities: e0913149f6b0625b553d70dae12b49fc62914fd1 - nanopb: a0ba3315591a9ae0a16a309ee504766e90db0c96 + nanopb: b552cce312b6c8484180ef47159bc0f65a1f0431 package_info_plus_macos: f010621b07802a241d96d01876d6705f15e77c1c path_provider_macos: 3c0c3b4b0d4a76d2bf989a913c2de869c5641a19 - PromisesObjC: 99b6f43f9e1044bd87a95a60beff28c2c44ddb72 + PromisesObjC: ab77feca74fa2823e7af4249b8326368e61014cb + screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38 shared_preferences_macos: a64dc611287ed6cbe28fd1297898db1336975727 + sqflite: a5789cceda41d54d23f31d6de539d65bb14100ea + tray_manager: 9064e219c56d75c476e46b9a21182087930baf90 url_launcher_macos: 597e05b8e514239626bcf4a850fcf9ef5c856ec3 wakelock_macos: bc3f2a9bd8d2e6c89fee1e1822e7ddac3bd004a9 window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 diff --git a/flutter/macos/Runner/Release.entitlements b/flutter/macos/Runner/Release.entitlements index 852fa1a47..ee95ab7e5 100644 --- a/flutter/macos/Runner/Release.entitlements +++ b/flutter/macos/Runner/Release.entitlements @@ -4,5 +4,7 @@ com.apple.security.app-sandbox + com.apple.security.network.client + diff --git a/flutter/macos/rustdesk.xcodeproj/project.pbxproj b/flutter/macos/rustdesk.xcodeproj/project.pbxproj index bed41ae67..5e9d16659 100644 --- a/flutter/macos/rustdesk.xcodeproj/project.pbxproj +++ b/flutter/macos/rustdesk.xcodeproj/project.pbxproj @@ -33,7 +33,7 @@ /* Begin PBXFileReference section */ ADDEDBA66A6E1 /* libresolv.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libresolv.tbd; path = usr/lib/libresolv.tbd; sourceTree = SDKROOT; }; - CA603C4309E13EF4668187A5 /* Cargo.toml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = Cargo.toml; path = /Users/ruizruiz/Work/Code/Projects/RustDesk/rustdesk/Cargo.toml; sourceTree = ""; }; + CA603C4309E13EF4668187A5 /* Cargo.toml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = Cargo.toml; path = /Users/mac/Documents/project/rustdesk/Cargo.toml; sourceTree = ""; }; CA604C7415FB2A3731F5016A /* liblibrustdesk_static.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = liblibrustdesk_static.a; sourceTree = BUILT_PRODUCTS_DIR; }; CA6071B5A0F5A7A3EF2297AA /* librustdesk.dylib */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.dylib"; includeInIndex = 0; path = librustdesk.dylib; sourceTree = BUILT_PRODUCTS_DIR; }; CA60D3BC5386B357B2AB834F /* rustdesk */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = rustdesk; sourceTree = BUILT_PRODUCTS_DIR; }; From 914847bb6346d9962b5482165247a5d2ae2b28cd Mon Sep 17 00:00:00 2001 From: Asura Date: Sat, 27 Aug 2022 23:00:43 +0800 Subject: [PATCH 0304/2015] Fix generated_bridge --- flutter/lib/generated_bridge.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/lib/generated_bridge.dart b/flutter/lib/generated_bridge.dart index 8fa7706aa..b4df7c605 100644 --- a/flutter/lib/generated_bridge.dart +++ b/flutter/lib/generated_bridge.dart @@ -4039,7 +4039,7 @@ class wire_StringList extends ffi.Struct { typedef uintptr_t = ffi.UnsignedLong; typedef DartPostCObjectFnType = ffi.Pointer< - ffi.NativeFunction)>>; + ffi.NativeFunction)>>; typedef DartPort = ffi.Int64; const int GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT = 2; From 98387e06e1243b8104230fd52e3d0784dee0bc37 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sun, 28 Aug 2022 14:58:46 +0800 Subject: [PATCH 0305/2015] fix: linux main/sub window resize issue Signed-off-by: Kingtous --- flutter/pubspec.lock | 10 +++++----- flutter/pubspec.yaml | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 07862bf38..ea5ff449c 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -252,8 +252,8 @@ packages: dependency: "direct main" description: path: "." - ref: e013c81d75320bbf28adddeaadf462264ee6039d - resolved-ref: e013c81d75320bbf28adddeaadf462264ee6039d + ref: "14a001e83ab0e7c8cb119f7f65be4e3056a954fb" + resolved-ref: "14a001e83ab0e7c8cb119f7f65be4e3056a954fb" url: "https://github.com/Kingtous/rustdesk_desktop_multi_window" source: git version: "0.1.0" @@ -1244,11 +1244,11 @@ packages: dependency: "direct main" description: path: "." - ref: "799ef079e87938c3f4340591b4330c2598f38bb9" - resolved-ref: "799ef079e87938c3f4340591b4330c2598f38bb9" + ref: "247818257b4b37f78bebea1719cee765282b3079" + resolved-ref: "247818257b4b37f78bebea1719cee765282b3079" url: "https://github.com/Kingtous/rustdesk_window_manager" source: git - version: "0.2.6" + version: "0.2.7" xdg_directories: dependency: transitive description: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 93c2f64b2..b765a5b17 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -61,11 +61,11 @@ dependencies: window_manager: git: url: https://github.com/Kingtous/rustdesk_window_manager - ref: 799ef079e87938c3f4340591b4330c2598f38bb9 + ref: 247818257b4b37f78bebea1719cee765282b3079 desktop_multi_window: git: url: https://github.com/Kingtous/rustdesk_desktop_multi_window - ref: e013c81d75320bbf28adddeaadf462264ee6039d + ref: 14a001e83ab0e7c8cb119f7f65be4e3056a954fb freezed_annotation: ^2.0.3 tray_manager: git: From 6ea16e4cdb85b958dd0acad158f6f7bebb721fc9 Mon Sep 17 00:00:00 2001 From: 21pages Date: Fri, 26 Aug 2022 11:35:28 +0800 Subject: [PATCH 0306/2015] port forward ui Signed-off-by: 21pages --- flutter/lib/consts.dart | 1 + .../lib/desktop/pages/port_forward_page.dart | 348 ++++++++++++++++++ .../desktop/pages/port_forward_tab_page.dart | 106 ++++++ .../screen/desktop_port_forward_screen.dart | 26 ++ .../lib/desktop/widgets/peercard_widget.dart | 183 ++++++++- flutter/lib/main.dart | 21 ++ flutter/lib/models/model.dart | 17 +- flutter/lib/utils/multi_window_manager.dart | 33 +- src/flutter.rs | 29 +- src/flutter_ffi.rs | 34 +- 10 files changed, 769 insertions(+), 29 deletions(-) create mode 100644 flutter/lib/desktop/pages/port_forward_page.dart create mode 100644 flutter/lib/desktop/pages/port_forward_tab_page.dart create mode 100644 flutter/lib/desktop/screen/desktop_port_forward_screen.dart diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 000a1cb54..3f0abd43f 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -4,6 +4,7 @@ const double kDesktopRemoteTabBarHeight = 28.0; const String kAppTypeMain = "main"; const String kAppTypeDesktopRemote = "remote"; const String kAppTypeDesktopFileTransfer = "file transfer"; +const String kAppTypeDesktopPortForward = "port forward"; const String kTabLabelHomePage = "Home"; const String kTabLabelSettingPage = "Settings"; diff --git a/flutter/lib/desktop/pages/port_forward_page.dart b/flutter/lib/desktop/pages/port_forward_page.dart new file mode 100644 index 000000000..b83761181 --- /dev/null +++ b/flutter/lib/desktop/pages/port_forward_page.dart @@ -0,0 +1,348 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/models/model.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; +import 'package:get/get.dart'; +import 'package:wakelock/wakelock.dart'; + +const double _kColumn1Width = 30; +const double _kColumn4Width = 100; +const double _kRowHeight = 50; +const double _kTextLeftMargin = 20; + +class _PortForward { + int localPort; + String remoteHost; + int remotePort; + + _PortForward.fromJson(List json) + : localPort = json[0] as int, + remoteHost = json[1] as String, + remotePort = json[2] as int; +} + +class PortForwardPage extends StatefulWidget { + const PortForwardPage({Key? key, required this.id, required this.isRDP}) + : super(key: key); + final String id; + final bool isRDP; + + @override + State createState() => _PortForwardPageState(); +} + +class _PortForwardPageState extends State + with AutomaticKeepAliveClientMixin { + final bool isRdp = false; + final TextEditingController localPortController = TextEditingController(); + final TextEditingController remoteHostController = TextEditingController(); + final TextEditingController remotePortController = TextEditingController(); + RxList<_PortForward> pfs = RxList.empty(growable: true); + late FFI _ffi; + + @override + void initState() { + super.initState(); + _ffi = FFI(); + // _ffi.connect(widget.id, isPortForward: true); + Get.put(_ffi, tag: 'pf_${widget.id}'); + if (!Platform.isLinux) { + Wakelock.enable(); + } + print("init success with id ${widget.id}"); + } + + @override + void dispose() { + _ffi.close(); + _ffi.dialogManager.dismissAll(); + if (!Platform.isLinux) { + Wakelock.disable(); + } + Get.delete(tag: 'pf_${widget.id}'); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + super.build(context); + return Scaffold( + backgroundColor: MyTheme.color(context).grayBg, + body: FutureBuilder(future: () async { + if (!isRdp) { + refreshTunnelConfig(); + } + }(), builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + return Container( + decoration: BoxDecoration( + border: Border.all( + width: 20, color: MyTheme.color(context).grayBg!)), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + buildPrompt(context), + Flexible( + child: Container( + decoration: BoxDecoration( + color: MyTheme.color(context).bg, + border: Border.all(width: 1, color: MyTheme.border)), + child: + widget.isRDP ? buildRdp(context) : buildTunnel(context), + ), + ), + ], + ), + ); + } + return const Offstage(); + }), + ); + } + + buildPrompt(BuildContext context) { + return Obx(() => Offstage( + offstage: pfs.isEmpty && !widget.isRDP, + child: Container( + height: 45, + color: const Color(0xFF007F00), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + translate('Listening ...'), + style: const TextStyle(fontSize: 16, color: Colors.white), + ), + Text( + translate('not_close_tcp_tip'), + style: const TextStyle( + fontSize: 10, color: Color(0xFFDDDDDD), height: 1.2), + ) + ])).marginOnly(bottom: 8), + )); + } + + buildTunnel(BuildContext context) { + text(String lable) => Expanded( + child: Text(translate(lable)).marginOnly(left: _kTextLeftMargin)); + + return Theme( + data: Theme.of(context) + .copyWith(backgroundColor: MyTheme.color(context).bg), + child: Obx(() => ListView.builder( + itemCount: pfs.length + 2, + itemBuilder: ((context, index) { + if (index == 0) { + return Container( + height: 25, + color: MyTheme.color(context).grayBg, + child: Row(children: [ + text('Local Port'), + const SizedBox(width: _kColumn1Width), + text('Remote Host'), + text('Remote Port'), + SizedBox( + width: _kColumn4Width, child: Text(translate('Action'))) + ]), + ); + } else if (index == 1) { + return buildTunnelAddRow(context); + } else { + return buildTunnelDataRow(context, pfs[index - 2], index - 2); + } + }))), + ); + } + + buildTunnelAddRow(BuildContext context) { + var portInputFormatter = [ + FilteringTextInputFormatter.allow(RegExp( + 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])$')) + ]; + + return Container( + height: _kRowHeight, + decoration: BoxDecoration(color: MyTheme.color(context).bg), + child: Row(children: [ + buildTunnelInputCell(context, + controller: localPortController, + inputFormatters: portInputFormatter), + const SizedBox( + width: _kColumn1Width, child: Icon(Icons.arrow_forward_sharp)), + buildTunnelInputCell(context, + controller: remoteHostController, hint: 'localhost'), + 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.mainAddPortForward( + id: 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), + ), + ]), + ); + } + + buildTunnelInputCell(BuildContext context, + {required TextEditingController controller, + List? inputFormatters, + String? hint}) { + return Expanded( + child: TextField( + controller: controller, + inputFormatters: inputFormatters, + cursorColor: MyTheme.color(context).text, + cursorHeight: 20, + cursorWidth: 1, + decoration: InputDecoration( + border: OutlineInputBorder( + borderSide: BorderSide(color: MyTheme.color(context).border!)), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide(color: MyTheme.color(context).border!)), + fillColor: MyTheme.color(context).bg, + contentPadding: const EdgeInsets.all(10), + hintText: hint, + hintStyle: TextStyle( + color: MyTheme.color(context).placeholder, fontSize: 16)), + style: TextStyle(color: MyTheme.color(context).text, fontSize: 16), + ).marginAll(10), + ); + } + + Widget buildTunnelDataRow(BuildContext context, _PortForward pf, int index) { + text(String lable) => Expanded( + child: Text(lable, style: const TextStyle(fontSize: 20)) + .marginOnly(left: _kTextLeftMargin)); + + return Container( + height: _kRowHeight, + decoration: BoxDecoration( + color: index % 2 == 0 + ? isDarkTheme() + ? const Color(0xFF202020) + : const Color(0xFFF4F5F6) + : MyTheme.color(context).bg), + child: Row(children: [ + text(pf.localPort.toString()), + const SizedBox(width: _kColumn1Width), + text(pf.remoteHost), + text(pf.remotePort.toString()), + SizedBox( + width: _kColumn4Width, + child: IconButton( + icon: const Icon(Icons.close), + onPressed: () async { + await bind.mainRemovePortForward( + id: widget.id, localPort: pf.localPort); + refreshTunnelConfig(); + }, + ), + ), + ]), + ); + } + + void refreshTunnelConfig() async { + String peer = await bind.mainGetPeer(id: widget.id); + Map config = jsonDecode(peer); + List infos = config['port_forwards'] as List; + List<_PortForward> result = List.empty(growable: true); + for (var e in infos) { + result.add(_PortForward.fromJson(e)); + } + pfs.value = result; + } + + buildRdp(BuildContext context) { + text1(String lable) => + Expanded(child: Text(lable).marginOnly(left: _kTextLeftMargin)); + text2(String lable) => Expanded( + child: Text( + lable, + style: TextStyle(fontSize: 20), + ).marginOnly(left: _kTextLeftMargin)); + return Theme( + data: Theme.of(context) + .copyWith(backgroundColor: MyTheme.color(context).bg), + child: ListView.builder( + itemCount: 2, + itemBuilder: ((context, index) { + if (index == 0) { + return Container( + height: 25, + color: MyTheme.color(context).grayBg, + child: Row(children: [ + text1('Local Port'), + const SizedBox(width: _kColumn1Width), + text1('Remote Host'), + text1('Remote Port'), + ]), + ); + } else { + return Container( + height: _kRowHeight, + decoration: BoxDecoration(color: MyTheme.color(context).bg), + child: Row(children: [ + Expanded( + child: Align( + alignment: Alignment.centerLeft, + child: SizedBox( + width: 120, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + elevation: 0, + side: const BorderSide(color: MyTheme.border)), + onPressed: () {}, + child: Text( + translate('New RDP'), + style: TextStyle( + fontWeight: FontWeight.w300, fontSize: 14), + ), + ).marginSymmetric(vertical: 10), + ).marginOnly(left: 20), + ), + ), + const SizedBox( + width: _kColumn1Width, + child: Icon(Icons.arrow_forward_sharp)), + text2('localhost'), + text2('RDP'), + ]), + ); + } + })), + ); + } + + @override + bool get wantKeepAlive => true; +} diff --git a/flutter/lib/desktop/pages/port_forward_tab_page.dart b/flutter/lib/desktop/pages/port_forward_tab_page.dart new file mode 100644 index 000000000..28825b75a --- /dev/null +++ b/flutter/lib/desktop/pages/port_forward_tab_page.dart @@ -0,0 +1,106 @@ +import 'dart:convert'; + +import 'package:desktop_multi_window/desktop_multi_window.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/desktop/pages/port_forward_page.dart'; +import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; +import 'package:flutter_hbb/models/model.dart'; +import 'package:flutter_hbb/utils/multi_window_manager.dart'; +import 'package:get/get.dart'; + +class PortForwardTabPage extends StatefulWidget { + final Map params; + + const PortForwardTabPage({Key? key, required this.params}) : super(key: key); + + @override + State createState() => _PortForwardTabPageState(params); +} + +class _PortForwardTabPageState extends State { + final tabController = Get.put(DesktopTabController()); + + static final IconData selectedIcon = Icons.forward_sharp; + static final IconData unselectedIcon = Icons.forward_outlined; + + _PortForwardTabPageState(Map params) { + tabController.add(TabInfo( + key: params['id'] + params['isRDP'].toString(), + label: params['id'], + selectedIcon: selectedIcon, + unselectedIcon: unselectedIcon, + page: PortForwardPage( + key: ValueKey(params['id']), + id: params['id'], + isRDP: params['isRDP'], + ))); + } + + @override + void initState() { + super.initState(); + + tabController.onRemove = (_, id) => onRemoveId(id); + + rustDeskWinManager.setMethodHandler((call, fromWindowId) async { + print( + "call ${call.method} with args ${call.arguments} from window ${fromWindowId}"); + // for simplify, just replace connectionId + if (call.method == "new_port_forward") { + final args = jsonDecode(call.arguments); + final id = args['id']; + final isRDP = args['isRDP']; + window_on_top(windowId()); + tabController.add(TabInfo( + key: id, + label: id, + selectedIcon: selectedIcon, + unselectedIcon: unselectedIcon, + page: PortForwardPage(id: id, isRDP: isRDP))); + } else if (call.method == "onDestroy") { + tabController.state.value.tabs.forEach((tab) { + print("executing onDestroy hook, closing ${tab.label}}"); + final tag = tab.label; + ffi(tag).close().then((_) { + Get.delete(tag: tag); + }); + }); + Get.back(); + } + }); + } + + @override + Widget build(BuildContext context) { + final theme = isDarkTheme() ? TarBarTheme.dark() : TarBarTheme.light(); + return SubWindowDragToResizeArea( + windowId: windowId(), + child: Container( + decoration: BoxDecoration( + border: Border.all(color: MyTheme.color(context).border!)), + child: Scaffold( + backgroundColor: MyTheme.color(context).bg, + body: DesktopTab( + controller: tabController, + theme: theme, + isMainWindow: false, + tail: AddButton( + theme: theme, + ).paddingOnly(left: 10), + )), + ), + ); + } + + void onRemoveId(String id) { + ffi("pf_$id").close(); + if (tabController.state.value.tabs.length == 0) { + WindowController.fromWindowId(windowId()).close(); + } + } + + int windowId() { + return widget.params["windowId"]; + } +} diff --git a/flutter/lib/desktop/screen/desktop_port_forward_screen.dart b/flutter/lib/desktop/screen/desktop_port_forward_screen.dart new file mode 100644 index 000000000..c7c163a57 --- /dev/null +++ b/flutter/lib/desktop/screen/desktop_port_forward_screen.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/desktop/pages/port_forward_tab_page.dart'; +import 'package:provider/provider.dart'; + +/// multi-tab file port forward screen +class DesktopPortForwardScreen extends StatelessWidget { + final Map params; + + const DesktopPortForwardScreen({Key? key, required this.params}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return MultiProvider( + providers: [ + ChangeNotifierProvider.value(value: gFFI.ffiModel), + ], + child: Scaffold( + body: PortForwardTabPage( + params: params, + ), + ), + ); + } +} diff --git a/flutter/lib/desktop/widgets/peercard_widget.dart b/flutter/lib/desktop/widgets/peercard_widget.dart index 433ca9284..810b84a63 100644 --- a/flutter/lib/desktop/widgets/peercard_widget.dart +++ b/flutter/lib/desktop/widgets/peercard_widget.dart @@ -1,5 +1,6 @@ import 'package:contextmenu/contextmenu.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:get/get.dart'; @@ -284,11 +285,20 @@ class _PeerCardState extends State<_PeerCard> /// Connect to a peer with [id]. /// If [isFileTransfer], starts a session only for file transfer. - void _connect(String id, {bool isFileTransfer = false}) async { + /// If [isTcpTunneling], starts a session only for tcp tunneling. + /// If [isRDP], starts a session only for rdp. + void _connect(String id, + {bool isFileTransfer = false, + bool isTcpTunneling = false, + bool isRDP = false}) async { if (id == '') return; id = id.replaceAll(' ', ''); + assert(!(isFileTransfer && isTcpTunneling && isRDP), + "more than one connect type"); if (isFileTransfer) { await rustDeskWinManager.new_file_transfer(id); + } else if (isTcpTunneling || isRDP) { + await rustDeskWinManager.new_port_forward(id, isRDP); } else { await rustDeskWinManager.new_remote_desktop(id); } @@ -307,12 +317,18 @@ class _PeerCardState extends State<_PeerCard> items: await super.widget.popupMenuItemsFunc(), elevation: 8, ); - if (value == 'remove') { + if (value == 'connect') { + _connect(id); + } else if (value == 'file') { + _connect(id, isFileTransfer: true); + } else if (value == 'tcp-tunnel') { + _connect(id, isTcpTunneling: true); + } else if (value == 'RDP') { + _connect(id, isRDP: true); + } else if (value == 'remove') { await bind.mainRemovePeer(id: id); removePreference(id); Get.forceAppUpdate(); // TODO use inner model / state - } else if (value == 'file') { - _connect(id, isFileTransfer: true); } else if (value == 'add-fav') { final favs = (await bind.mainGetFav()).toList(); if (favs.indexOf(id) < 0) { @@ -325,8 +341,6 @@ class _PeerCardState extends State<_PeerCard> bind.mainStoreFav(favs: favs); Get.forceAppUpdate(); // TODO use inner model / state } - } else if (value == 'connect') { - _connect(id, isFileTransfer: false); } else if (value == 'ab-delete') { gFFI.abModel.deletePeer(id); await gFFI.abModel.updateAb(); @@ -554,7 +568,7 @@ class RecentPeerCard extends BasePeerCard { : super(peer: peer, key: key, type: PeerType.recent); Future>> _getPopupMenuItems() async { - return [ + var items = [ PopupMenuItem( child: Text(translate('Connect')), value: 'connect'), PopupMenuItem( @@ -570,6 +584,10 @@ class RecentPeerCard extends BasePeerCard { PopupMenuItem( child: Text(translate('Add to Favorites')), value: 'add-fav'), ]; + if (peer.platform == 'Windows') { + items.insert(3, _rdpMenuItem(peer.id)); + } + return items; } } @@ -578,7 +596,7 @@ class FavoritePeerCard extends BasePeerCard { : super(peer: peer, key: key, type: PeerType.fav); Future>> _getPopupMenuItems() async { - return [ + var items = [ PopupMenuItem( child: Text(translate('Connect')), value: 'connect'), PopupMenuItem( @@ -594,6 +612,10 @@ class FavoritePeerCard extends BasePeerCard { PopupMenuItem( child: Text(translate('Remove from Favorites')), value: 'remove-fav'), ]; + if (peer.platform == 'Windows') { + items.insert(3, _rdpMenuItem(peer.id)); + } + return items; } } @@ -602,7 +624,7 @@ class DiscoveredPeerCard extends BasePeerCard { : super(peer: peer, key: key, type: PeerType.discovered); Future>> _getPopupMenuItems() async { - return [ + var items = [ PopupMenuItem( child: Text(translate('Connect')), value: 'connect'), PopupMenuItem( @@ -618,6 +640,10 @@ class DiscoveredPeerCard extends BasePeerCard { PopupMenuItem( child: Text(translate('Add to Favorites')), value: 'add-fav'), ]; + if (peer.platform == 'Windows') { + items.insert(3, _rdpMenuItem(peer.id)); + } + return items; } } @@ -626,7 +652,7 @@ class AddressBookPeerCard extends BasePeerCard { : super(peer: peer, key: key, type: PeerType.ab); Future>> _getPopupMenuItems() async { - return [ + var items = [ PopupMenuItem( child: Text(translate('Connect')), value: 'connect'), PopupMenuItem( @@ -645,6 +671,10 @@ class AddressBookPeerCard extends BasePeerCard { PopupMenuItem( child: Text(translate('Edit Tag')), value: 'ab-edit-tag'), ]; + if (peer.platform == 'Windows') { + items.insert(3, _rdpMenuItem(peer.id)); + } + return items; } } @@ -664,3 +694,136 @@ Future> _forceAlwaysRelayMenuItem(String id) async { ), value: 'force-always-relay'); } + +PopupMenuItem _rdpMenuItem(String id) { + return PopupMenuItem( + child: Row( + children: [ + Text('RDP'), + SizedBox(width: 20), + IconButton( + icon: Icon(Icons.edit), + onPressed: () => _rdpDialog(id), + ) + ], + ), + value: 'RDP'); +} + +void _rdpDialog(String id) async { + final portController = TextEditingController( + text: await bind.mainGetPeerOption(id: id, key: 'rdp_port')); + final userController = TextEditingController( + text: await bind.mainGetPeerOption(id: id, key: 'rdp_username')); + final passwordContorller = TextEditingController( + text: await bind.mainGetPeerOption(id: id, key: 'rdp_password')); + RxBool secure = true.obs; + + gFFI.dialogManager.show((setState, close) { + return CustomAlertDialog( + title: Text('RDP ' + translate('Settings')), + content: ConstrainedBox( + constraints: BoxConstraints(minWidth: 500), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: 8.0, + ), + Row( + children: [ + ConstrainedBox( + constraints: BoxConstraints(minWidth: 100), + child: Text( + "${translate('Port')}:", + textAlign: TextAlign.start, + ).marginOnly(bottom: 16.0)), + SizedBox( + width: 24.0, + ), + Expanded( + child: TextField( + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp( + 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])$')) + ], + decoration: InputDecoration( + border: OutlineInputBorder(), hintText: '3389'), + controller: portController, + ), + ), + ], + ), + SizedBox( + height: 8.0, + ), + Row( + children: [ + ConstrainedBox( + constraints: BoxConstraints(minWidth: 100), + child: Text( + "${translate('Username')}:", + textAlign: TextAlign.start, + ).marginOnly(bottom: 16.0)), + SizedBox( + width: 24.0, + ), + Expanded( + child: TextField( + decoration: InputDecoration(border: OutlineInputBorder()), + controller: userController, + ), + ), + ], + ), + SizedBox( + height: 8.0, + ), + Row( + children: [ + ConstrainedBox( + constraints: BoxConstraints(minWidth: 100), + child: Text("${translate('Password')}:") + .marginOnly(bottom: 16.0)), + SizedBox( + width: 24.0, + ), + Expanded( + child: Obx(() => TextField( + obscureText: secure.value, + decoration: InputDecoration( + border: OutlineInputBorder(), + suffixIcon: IconButton( + onPressed: () => secure.value = !secure.value, + icon: Icon(secure.value + ? Icons.visibility_off + : Icons.visibility))), + controller: passwordContorller, + )), + ), + ], + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () { + close(); + }, + child: Text(translate("Cancel"))), + TextButton( + onPressed: () async { + await bind.mainSetPeerOption( + id: id, key: 'rdp_port', value: portController.text.trim()); + await bind.mainSetPeerOption( + id: id, key: 'rdp_username', value: userController.text); + await bind.mainSetPeerOption( + id: id, key: 'rdp_password', value: passwordContorller.text); + close(); + }, + child: Text(translate("OK"))), + ], + ); + }); +} diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 9682f19d1..6e30a15d2 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart'; import 'package:flutter_hbb/desktop/pages/server_page.dart'; import 'package:flutter_hbb/desktop/screen/desktop_file_transfer_screen.dart'; +import 'package:flutter_hbb/desktop/screen/desktop_port_forward_screen.dart'; import 'package:flutter_hbb/desktop/screen/desktop_remote_screen.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:get/get.dart'; @@ -47,6 +48,9 @@ Future main(List args) async { case WindowType.FileTransfer: runFileTransferScreen(argument); break; + case WindowType.PortForward: + runPortForwardScreen(argument); + break; default: break; } @@ -133,6 +137,23 @@ void runFileTransferScreen(Map argument) async { ); } +void runPortForwardScreen(Map argument) async { + await initEnv(kAppTypeDesktopPortForward); + runApp( + GetMaterialApp( + navigatorKey: globalKey, + debugShowCheckedModeBanner: false, + title: 'RustDesk - Port Forward', + theme: getCurrentTheme(), + home: DesktopPortForwardScreen(params: argument), + navigatorObservers: [ + // FirebaseAnalyticsObserver(analytics: analytics), + ], + builder: _keepScaleBuilder(), + ), + ); +} + void runConnectionManagerScreen() async { // initialize window WindowOptions windowOptions = getHiddenTitleBarWindowOptions(Size(300, 400)); diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index dda22a779..f9da557f2 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -1034,17 +1034,24 @@ class FFI { return []; } - /// Connect with the given [id]. Only transfer file if [isFileTransfer]. + /// Connect with the given [id]. Only transfer file if [isFileTransfer], only port forward if [isPortForward]. void connect(String id, - {bool isFileTransfer = false, double tabBarHeight = 0.0}) { - if (!isFileTransfer) { + {bool isFileTransfer = false, + bool isPortForward = false, + double tabBarHeight = 0.0}) { + assert(!(isFileTransfer && isPortForward), "more than one connect type"); + if (isFileTransfer) { + id = 'ft_${id}'; + } else if (isPortForward) { + id = 'pf_${id}'; + } else { chatModel.resetClientMode(); canvasModel.id = id; imageModel._id = id; cursorModel.id = id; } - id = isFileTransfer ? 'ft_${id}' : id; - final stream = bind.sessionConnect(id: id, isFileTransfer: isFileTransfer); + final stream = bind.sessionConnect( + id: id, isFileTransfer: isFileTransfer, isPortForward: isPortForward); final cb = ffiModel.startEventListener(id); () async { await for (final message in stream) { diff --git a/flutter/lib/utils/multi_window_manager.dart b/flutter/lib/utils/multi_window_manager.dart index 9b26870c0..b01b84a9d 100644 --- a/flutter/lib/utils/multi_window_manager.dart +++ b/flutter/lib/utils/multi_window_manager.dart @@ -35,6 +35,7 @@ class RustDeskMultiWindowManager { int? _remoteDesktopWindowId; int? _fileTransferWindowId; + int? _portForwardWindowId; Future new_remote_desktop(String remote_id) async { final msg = @@ -87,6 +88,34 @@ class RustDeskMultiWindowManager { } } + Future new_port_forward(String remote_id, bool isRDP) async { + final msg = jsonEncode({ + "type": WindowType.PortForward.index, + "id": remote_id, + "isRDP": isRDP + }); + + try { + final ids = await DesktopMultiWindow.getAllSubWindowIds(); + if (!ids.contains(_portForwardWindowId)) { + _portForwardWindowId = null; + } + } on Error { + _portForwardWindowId = null; + } + if (_portForwardWindowId == null) { + final portForwardController = await DesktopMultiWindow.createWindow(msg); + portForwardController + ..setFrame(const Offset(0, 0) & const Size(1280, 720)) + ..center() + ..setTitle("rustdesk - port forward") + ..show(); + _portForwardWindowId = portForwardController.windowId; + } else { + return call(WindowType.PortForward, "new_port_forward", msg); + } + } + Future call(WindowType type, String methodName, dynamic args) async { int? windowId = findWindowByType(type); if (windowId == null) { @@ -104,7 +133,7 @@ class RustDeskMultiWindowManager { case WindowType.FileTransfer: return _fileTransferWindowId; case WindowType.PortForward: - break; + return _portForwardWindowId; case WindowType.Unknown: break; } @@ -120,7 +149,7 @@ class RustDeskMultiWindowManager { await Future.wait(WindowType.values.map((e) => closeWindows(e))); } - Future closeWindows(WindowType type) async { + Future closeWindows(WindowType type) async { if (type == WindowType.Main) { // skip main window, use window manager instead return; diff --git a/src/flutter.rs b/src/flutter.rs index ca00807f1..1a0499565 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -70,7 +70,13 @@ impl Session { /// /// * `id` - The identifier of the remote session with prefix. Regex: [\w]*[\_]*[\d]+ /// * `is_file_transfer` - If the session is used for file transfer. - pub fn start(identifier: &str, is_file_transfer: bool, events2ui: StreamSink) { + /// * `is_port_forward` - If the session is used for port forward. + pub fn start( + identifier: &str, + is_file_transfer: bool, + is_port_forward: bool, + events2ui: StreamSink, + ) { // TODO check same id let session_id = get_session_id(identifier.to_owned()); LocalConfig::set_remote_id(&session_id); @@ -83,17 +89,17 @@ impl Session { lc: Default::default(), events2ui, }; - session - .lc - .write() - .unwrap() - .initialize(session_id.clone(), is_file_transfer, false); + session.lc.write().unwrap().initialize( + session_id.clone(), + is_file_transfer, + is_port_forward, + ); SESSIONS .write() .unwrap() .insert(identifier.to_owned(), session.clone()); std::thread::spawn(move || { - Connection::start(session, is_file_transfer); + Connection::start(session, is_file_transfer, is_port_forward); }); } @@ -201,7 +207,7 @@ impl Session { self.send(Data::Close); let session = self.clone(); std::thread::spawn(move || { - Connection::start(session, false); + Connection::start(session, false, false); }); } @@ -719,18 +725,21 @@ impl Connection { /// /// * `session` - The session to create a new connection for. /// * `is_file_transfer` - Whether the connection is for file transfer. + /// * `is_port_forward` - Whether the connection is for port forward. #[tokio::main(flavor = "current_thread")] - async fn start(session: Session, is_file_transfer: bool) { + async fn start(session: Session, is_file_transfer: bool, is_port_forward: bool) { let mut last_recv_time = Instant::now(); let (sender, mut receiver) = mpsc::unbounded_channel::(); let mut stop_clipboard = None; - if !is_file_transfer { + if !is_file_transfer && !is_port_forward { stop_clipboard = Self::start_clipboard(sender.clone(), session.lc.clone()); } *session.sender.write().unwrap() = Some(sender); let conn_type = if is_file_transfer { session.lc.write().unwrap().is_file_transfer = true; ConnType::FILE_TRANSFER + } else if is_port_forward { + ConnType::PORT_FORWARD // TODO: RDP } else { ConnType::DEFAULT_CONN }; diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index aa46e4faf..d9bc31d96 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -111,8 +111,9 @@ pub fn session_connect( events2ui: StreamSink, id: String, is_file_transfer: bool, + is_port_forward: bool, ) -> ResultType<()> { - Session::start(&id, is_file_transfer, events2ui); + Session::start(&id, is_file_transfer, is_port_forward, events2ui); Ok(()) } @@ -592,12 +593,41 @@ pub fn main_load_lan_peers() { { let data = HashMap::from([ ("name", "load_lan_peers".to_owned()), - ("peers", serde_json::to_string(&get_lan_peers()).unwrap_or_default()), + ( + "peers", + serde_json::to_string(&get_lan_peers()).unwrap_or_default(), + ), ]); s.add(serde_json::ser::to_string(&data).unwrap_or("".to_owned())); }; } +pub fn main_add_port_forward(id: String, local_port: i32, remote_host: String, remote_port: i32) { + let mut config = get_peer(id.clone()); + if config + .port_forwards + .iter() + .filter(|x| x.0 == local_port) + .next() + .is_some() + { + return; + } + let pf = (local_port, remote_host, remote_port); + config.port_forwards.push(pf); + config.store(&id); +} + +pub fn main_remove_port_forward(id: String, local_port: i32) { + let mut config = get_peer(id.clone()); + config.port_forwards = config + .port_forwards + .drain(..) + .filter(|x| x.0 != local_port) + .collect(); + config.store(&id); +} + pub fn main_get_last_remote_id() -> String { // if !config::APP_DIR.read().unwrap().is_empty() { // res = LocalConfig::get_remote_id(); From ea77d9284b96db8d994863e46f612c7f469e45eb Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 26 Aug 2022 23:28:08 +0800 Subject: [PATCH 0307/2015] flutter_desktop: new remote menu, mid commit Signed-off-by: fufesou --- flutter/lib/common.dart | 41 +- .../desktop/pages/connection_tab_page.dart | 80 +- .../lib/desktop/pages/desktop_tab_page.dart | 44 +- flutter/lib/desktop/pages/remote_page.dart | 48 +- .../desktop/screen/desktop_remote_screen.dart | 3 + .../widgets/material_mod_popup_menu.dart | 1321 +++++++++++++++++ flutter/lib/desktop/widgets/popup_menu.dart | 375 +++++ .../lib/desktop/widgets/remote_menubar.dart | 560 +++++++ flutter/lib/models/model.dart | 29 +- src/lang/cn.rs | 9 + src/lang/cs.rs | 9 + src/lang/da.rs | 9 + src/lang/de.rs | 9 + src/lang/eo.rs | 9 + src/lang/es.rs | 9 + src/lang/fr.rs | 9 + src/lang/hu.rs | 9 + src/lang/id.rs | 9 + src/lang/it.rs | 9 + src/lang/ja.rs | 9 + src/lang/ko.rs | 9 + src/lang/pl.rs | 9 + src/lang/pt_PT | 9 + src/lang/ptbr.rs | 9 + src/lang/ru.rs | 9 + src/lang/sk.rs | 9 + src/lang/template.rs | 9 + src/lang/tr.rs | 9 + src/lang/tw.rs | 9 + src/lang/vn.rs | 9 + 30 files changed, 2606 insertions(+), 84 deletions(-) create mode 100644 flutter/lib/desktop/widgets/material_mod_popup_menu.dart create mode 100644 flutter/lib/desktop/widgets/popup_menu.dart create mode 100644 flutter/lib/desktop/widgets/remote_menubar.dart diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 17e45ba95..6027fb8de 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -447,7 +447,10 @@ void msgBox( 0, wrap(translate('OK'), () { dialogManager.dismissAll(); - closeConnection(); + // https://github.com/fufesou/rustdesk/blob/5e9a31340b899822090a3731769ae79c6bf5f3e5/src/ui/common.tis#L263 + if (type.indexOf("custom") < 0) { + closeConnection(); + } })); } if (hasCancel == null) { @@ -740,3 +743,39 @@ Future>? matchPeers(String searchText, List peers) async { } return filteredList; } + +class PrivacyModeState { + static String tag(String id) => 'privacy_mode_' + id; + + static void init(String id) { + final RxBool state = false.obs; + Get.put(state, tag: tag(id)); + } + + static void delete(String id) => Get.delete(tag: tag(id)); + static RxBool find(String id) => Get.find(tag: tag(id)); +} + +class BlockInputState { + static String tag(String id) => 'block_input_' + id; + + static void init(String id) { + final RxBool state = false.obs; + Get.put(state, tag: tag(id)); + } + + static void delete(String id) => Get.delete(tag: tag(id)); + static RxBool find(String id) => Get.find(tag: tag(id)); +} + +class CurrentDisplayState { + static String tag(String id) => 'current_display_' + id; + + static void init(String id) { + final RxInt state = RxInt(0); + Get.put(state, tag: tag(id)); + } + + static void delete(String id) => Get.delete(tag: tag(id)); + static RxInt find(String id) => Get.find(tag: tag(id)); +} diff --git a/flutter/lib/desktop/pages/connection_tab_page.dart b/flutter/lib/desktop/pages/connection_tab_page.dart index be7c76f2a..c8cde79ad 100644 --- a/flutter/lib/desktop/pages/connection_tab_page.dart +++ b/flutter/lib/desktop/pages/connection_tab_page.dart @@ -22,26 +22,25 @@ class ConnectionTabPage extends StatefulWidget { class _ConnectionTabPageState extends State { final tabController = Get.put(DesktopTabController()); - static final Rx _fullscreenID = "".obs; static final IconData selectedIcon = Icons.desktop_windows_sharp; static final IconData unselectedIcon = Icons.desktop_windows_outlined; var connectionMap = RxList.empty(growable: true); _ConnectionTabPageState(Map params) { + final RxBool fullscreen = Get.find(tag: 'fullscreen'); if (params['id'] != null) { tabController.add(TabInfo( key: params['id'], label: params['id'], selectedIcon: selectedIcon, unselectedIcon: unselectedIcon, - page: RemotePage( - key: ValueKey(params['id']), - id: params['id'], - tabBarHeight: - _fullscreenID.value.isNotEmpty ? 0 : kDesktopRemoteTabBarHeight, - fullscreenID: _fullscreenID, - ))); + page: Obx(() => RemotePage( + key: ValueKey(params['id']), + id: params['id'], + tabBarHeight: + fullscreen.isTrue ? 0 : kDesktopRemoteTabBarHeight, + )))); } } @@ -54,6 +53,8 @@ class _ConnectionTabPageState extends State { rustDeskWinManager.setMethodHandler((call, fromWindowId) async { print( "call ${call.method} with args ${call.arguments} from window ${fromWindowId}"); + + final RxBool fullscreen = Get.find(tag: 'fullscreen'); // for simplify, just replace connectionId if (call.method == "new_remote_desktop") { final args = jsonDecode(call.arguments); @@ -64,14 +65,13 @@ class _ConnectionTabPageState extends State { label: id, selectedIcon: selectedIcon, unselectedIcon: unselectedIcon, - page: RemotePage( - key: ValueKey(id), - id: id, - tabBarHeight: _fullscreenID.value.isNotEmpty - ? 0 - : kDesktopRemoteTabBarHeight, - fullscreenID: _fullscreenID, - ))); + closable: false, + page: Obx(() => RemotePage( + key: ValueKey(id), + id: id, + tabBarHeight: + fullscreen.isTrue ? 0 : kDesktopRemoteTabBarHeight, + )))); } else if (call.method == "onDestroy") { tabController.state.value.tabs.forEach((tab) { print("executing onDestroy hook, closing ${tab.label}}"); @@ -88,29 +88,31 @@ class _ConnectionTabPageState extends State { @override Widget build(BuildContext context) { final theme = isDarkTheme() ? TarBarTheme.dark() : TarBarTheme.light(); - return SubWindowDragToResizeArea( - windowId: windowId(), - child: Container( - decoration: BoxDecoration( - border: Border.all(color: MyTheme.color(context).border!)), - child: Scaffold( - backgroundColor: MyTheme.color(context).bg, - body: Obx(() => DesktopTab( - controller: tabController, - theme: theme, - isMainWindow: false, - showTabBar: _fullscreenID.value.isEmpty, - tail: AddButton( - theme: theme, - ).paddingOnly(left: 10), - pageViewBuilder: (pageView) { - WindowController.fromWindowId(windowId()) - .setFullscreen(_fullscreenID.value.isNotEmpty); - return pageView; - }, - ))), - ), - ); + final RxBool fullscreen = Get.find(tag: 'fullscreen'); + return Obx(() => SubWindowDragToResizeArea( + resizeEdgeSize: fullscreen.value ? 1.0 : 8.0, + windowId: windowId(), + child: Container( + decoration: BoxDecoration( + border: Border.all(color: MyTheme.color(context).border!)), + child: Scaffold( + backgroundColor: MyTheme.color(context).bg, + body: Obx(() => DesktopTab( + controller: tabController, + theme: theme, + isMainWindow: false, + showTabBar: fullscreen.isFalse, + tail: AddButton( + theme: theme, + ).paddingOnly(left: 10), + pageViewBuilder: (pageView) { + WindowController.fromWindowId(windowId()) + .setFullscreen(fullscreen.isTrue); + return pageView; + }, + ))), + ), + )); } void onRemoveId(String id) { diff --git a/flutter/lib/desktop/pages/desktop_tab_page.dart b/flutter/lib/desktop/pages/desktop_tab_page.dart index 4a2fdb7d2..a7a93d7ad 100644 --- a/flutter/lib/desktop/pages/desktop_tab_page.dart +++ b/flutter/lib/desktop/pages/desktop_tab_page.dart @@ -4,6 +4,7 @@ import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart'; import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart'; import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; +import 'package:get/get.dart'; import 'package:window_manager/window_manager.dart'; class DesktopTabPage extends StatefulWidget { @@ -33,26 +34,29 @@ class _DesktopTabPageState extends State { @override Widget build(BuildContext context) { final dark = isDarkTheme(); - return DragToResizeArea( - child: Container( - decoration: BoxDecoration( - border: Border.all(color: MyTheme.color(context).border!)), - child: Scaffold( - backgroundColor: MyTheme.color(context).bg, - body: DesktopTab( - controller: tabController, - theme: dark ? TarBarTheme.dark() : TarBarTheme.light(), - isMainWindow: true, - tail: ActionIcon( - message: 'Settings', - icon: IconFont.menu, - theme: dark ? TarBarTheme.dark() : TarBarTheme.light(), - onTap: onAddSetting, - is_close: false, - ), - )), - ), - ); + RxBool fullscreen = false.obs; + Get.put(fullscreen, tag: 'fullscreen'); + return Obx(() => DragToResizeArea( + resizeEdgeSize: fullscreen.value ? 1.0 : 8.0, + child: Container( + decoration: BoxDecoration( + border: Border.all(color: MyTheme.color(context).border!)), + child: Scaffold( + backgroundColor: MyTheme.color(context).bg, + body: DesktopTab( + controller: tabController, + theme: dark ? TarBarTheme.dark() : TarBarTheme.light(), + isMainWindow: true, + tail: ActionIcon( + message: 'Settings', + icon: IconFont.menu, + theme: dark ? TarBarTheme.dark() : TarBarTheme.light(), + onTap: onAddSetting, + is_close: false, + ), + )), + ), + )); } void onAddSetting() { diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 025db279f..8ca9c0cfb 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -9,9 +9,11 @@ import 'package:flutter_hbb/models/chat_model.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:wakelock/wakelock.dart'; +import 'package:tuple/tuple.dart'; // import 'package:window_manager/window_manager.dart'; +import '../widgets/remote_menubar.dart'; import '../../common.dart'; import '../../mobile/widgets/dialog.dart'; import '../../mobile/widgets/overlay.dart'; @@ -21,16 +23,14 @@ import '../../models/platform_model.dart'; final initText = '\1' * 1024; class RemotePage extends StatefulWidget { - RemotePage( - {Key? key, - required this.id, - required this.tabBarHeight, - required this.fullscreenID}) - : super(key: key); + RemotePage({ + Key? key, + required this.id, + required this.tabBarHeight, + }) : super(key: key); final String id; final double tabBarHeight; - final Rx fullscreenID; @override _RemotePageState createState() => _RemotePageState(); @@ -50,11 +50,15 @@ class _RemotePageState extends State late FFI _ffi; + void _updateTabBarHeight() { + _ffi.canvasModel.tabBarHeight = widget.tabBarHeight; + } + @override void initState() { super.initState(); _ffi = FFI(); - _ffi.canvasModel.tabBarHeight = super.widget.tabBarHeight; + _updateTabBarHeight(); Get.put(_ffi, tag: widget.id); _ffi.connect(widget.id, tabBarHeight: super.widget.tabBarHeight); WidgetsBinding.instance.addPostFrameCallback((_) { @@ -70,6 +74,9 @@ class _RemotePageState extends State _ffi.listenToMouse(true); _ffi.qualityMonitorModel.checkShowQualityMonitor(widget.id); // WindowManager.instance.addListener(this); + PrivacyModeState.init(widget.id); + BlockInputState.init(widget.id); + CurrentDisplayState.init(widget.id); } @override @@ -90,6 +97,9 @@ class _RemotePageState extends State // WindowManager.instance.removeListener(this); Get.delete(tag: widget.id); super.dispose(); + PrivacyModeState.delete(widget.id); + BlockInputState.delete(widget.id); + CurrentDisplayState.delete(widget.id); } void resetTool() { @@ -217,6 +227,7 @@ class _RemotePageState extends State @override Widget build(BuildContext context) { super.build(context); + _updateTabBarHeight(); return WillPopScope( onWillPop: () async { clientClose(_ffi.dialogManager); @@ -289,6 +300,7 @@ class _RemotePageState extends State } Widget? getBottomAppBar(FfiModel ffiModel) { + final RxBool fullscreen = Get.find(tag: 'fullscreen'); return MouseRegion( cursor: SystemMouseCursors.basic, child: BottomAppBar( @@ -323,15 +335,11 @@ class _RemotePageState extends State : [ IconButton( color: Colors.white, - icon: Icon(widget.fullscreenID.value.isEmpty + icon: Icon(fullscreen.isTrue ? Icons.fullscreen : Icons.close_fullscreen), onPressed: () { - if (widget.fullscreenID.value.isEmpty) { - widget.fullscreenID.value = widget.id; - } else { - widget.fullscreenID.value = ""; - } + fullscreen.value = !fullscreen.value; }, ) ]) + @@ -404,7 +412,7 @@ class _RemotePageState extends State } if (_isPhysicalMouse) { _ffi.handleMouse(getEvent(e, 'mousemove'), - tabBarHeight: super.widget.tabBarHeight); + tabBarHeight: widget.tabBarHeight); } } @@ -418,7 +426,7 @@ class _RemotePageState extends State } if (_isPhysicalMouse) { _ffi.handleMouse(getEvent(e, 'mousedown'), - tabBarHeight: super.widget.tabBarHeight); + tabBarHeight: widget.tabBarHeight); } } @@ -426,7 +434,7 @@ class _RemotePageState extends State if (e.kind != ui.PointerDeviceKind.mouse) return; if (_isPhysicalMouse) { _ffi.handleMouse(getEvent(e, 'mouseup'), - tabBarHeight: super.widget.tabBarHeight); + tabBarHeight: widget.tabBarHeight); } } @@ -434,7 +442,7 @@ class _RemotePageState extends State if (e.kind != ui.PointerDeviceKind.mouse) return; if (_isPhysicalMouse) { _ffi.handleMouse(getEvent(e, 'mousemove'), - tabBarHeight: super.widget.tabBarHeight); + tabBarHeight: widget.tabBarHeight); } } @@ -500,6 +508,10 @@ class _RemotePageState extends State )); } paints.add(QualityMonitor(_ffi.qualityMonitorModel)); + paints.add(RemoteMenubar( + id: widget.id, + ffi: _ffi, + )); return Stack( children: paints, ); diff --git a/flutter/lib/desktop/screen/desktop_remote_screen.dart b/flutter/lib/desktop/screen/desktop_remote_screen.dart index 4e941ed7c..5b5dd07c2 100644 --- a/flutter/lib/desktop/screen/desktop_remote_screen.dart +++ b/flutter/lib/desktop/screen/desktop_remote_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/desktop/pages/connection_tab_page.dart'; +import 'package:get/get.dart'; import 'package:provider/provider.dart'; /// multi-tab desktop remote screen @@ -11,6 +12,8 @@ class DesktopRemoteScreen extends StatelessWidget { @override Widget build(BuildContext context) { + RxBool fullscreen = false.obs; + Get.put(fullscreen, tag: 'fullscreen'); return MultiProvider( providers: [ ChangeNotifierProvider.value(value: gFFI.ffiModel), diff --git a/flutter/lib/desktop/widgets/material_mod_popup_menu.dart b/flutter/lib/desktop/widgets/material_mod_popup_menu.dart new file mode 100644 index 000000000..a9aec932b --- /dev/null +++ b/flutter/lib/desktop/widgets/material_mod_popup_menu.dart @@ -0,0 +1,1321 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/foundation.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/material.dart'; + +// Examples can assume: +// enum Commands { heroAndScholar, hurricaneCame } +// late bool _heroAndScholar; +// late dynamic _selection; +// late BuildContext context; +// void setState(VoidCallback fn) { } +// enum Menu { itemOne, itemTwo, itemThree, itemFour } + +const Duration _kMenuDuration = Duration(milliseconds: 300); +const double _kMenuCloseIntervalEnd = 2.0 / 3.0; +const double _kMenuHorizontalPadding = 16.0; +const double _kMenuDividerHeight = 16.0; +//const double _kMenuMaxWidth = 5.0 * _kMenuWidthStep; +const double _kMenuMinWidth = 2.0 * _kMenuWidthStep; +const double _kMenuMaxWidth = double.infinity; +// const double _kMenuVerticalPadding = 8.0; +const double _kMenuVerticalPadding = 0.0; +const double _kMenuWidthStep = 0.0; +//const double _kMenuScreenPadding = 8.0; +const double _kMenuScreenPadding = 0.0; +const double _kDefaultIconSize = 24.0; + +/// Used to configure how the [PopupMenuButton] positions its popup menu. +enum PopupMenuPosition { + /// Menu is positioned over the anchor. + over, + + /// Menu is positioned under the anchor. + under, + + // Only support right side (TextDirection.ltr) for now + /// Menu is positioned over side the anchor + overSide, + + // Only support right side (TextDirection.ltr) for now + /// Menu is positioned under side the anchor + underSide, +} + +/// A base class for entries in a material design popup menu. +/// +/// The popup menu widget uses this interface to interact with the menu items. +/// To show a popup menu, use the [showMenu] function. To create a button that +/// shows a popup menu, consider using [PopupMenuButton]. +/// +/// The type `T` is the type of the value(s) the entry represents. All the +/// entries in a given menu must represent values with consistent types. +/// +/// A [PopupMenuEntry] may represent multiple values, for example a row with +/// several icons, or a single entry, for example a menu item with an icon (see +/// [PopupMenuItem]), or no value at all (for example, [PopupMenuDivider]). +/// +/// See also: +/// +/// * [PopupMenuItem], a popup menu entry for a single value. +/// * [PopupMenuDivider], a popup menu entry that is just a horizontal line. +/// * [CheckedPopupMenuItem], a popup menu item with a checkmark. +/// * [showMenu], a method to dynamically show a popup menu at a given location. +/// * [PopupMenuButton], an [IconButton] that automatically shows a menu when +/// it is tapped. +abstract class PopupMenuEntry extends StatefulWidget { + /// Abstract const constructor. This constructor enables subclasses to provide + /// const constructors so that they can be used in const expressions. + const PopupMenuEntry({Key? key}) : super(key: key); + + /// The amount of vertical space occupied by this entry. + /// + /// This value is used at the time the [showMenu] method is called, if the + /// `initialValue` argument is provided, to determine the position of this + /// entry when aligning the selected entry over the given `position`. It is + /// otherwise ignored. + double get height; + + /// Whether this entry represents a particular value. + /// + /// This method is used by [showMenu], when it is called, to align the entry + /// representing the `initialValue`, if any, to the given `position`, and then + /// later is called on each entry to determine if it should be highlighted (if + /// the method returns true, the entry will have its background color set to + /// the ambient [ThemeData.highlightColor]). If `initialValue` is null, then + /// this method is not called. + /// + /// If the [PopupMenuEntry] represents a single value, this should return true + /// if the argument matches that value. If it represents multiple values, it + /// should return true if the argument matches any of them. + bool represents(T? value); +} + +/// A horizontal divider in a material design popup menu. +/// +/// This widget adapts the [Divider] for use in popup menus. +/// +/// See also: +/// +/// * [PopupMenuItem], for the kinds of items that this widget divides. +/// * [showMenu], a method to dynamically show a popup menu at a given location. +/// * [PopupMenuButton], an [IconButton] that automatically shows a menu when +/// it is tapped. +class PopupMenuDivider extends PopupMenuEntry { + /// Creates a horizontal divider for a popup menu. + /// + /// By default, the divider has a height of 16 logical pixels. + const PopupMenuDivider({Key? key, this.height = _kMenuDividerHeight}) + : super(key: key); + + /// The height of the divider entry. + /// + /// Defaults to 16 pixels. + @override + final double height; + + @override + bool represents(void value) => false; + + @override + State createState() => _PopupMenuDividerState(); +} + +class _PopupMenuDividerState extends State { + @override + Widget build(BuildContext context) => Divider(height: widget.height); +} + +// This widget only exists to enable _PopupMenuRoute to save the sizes of +// each menu item. The sizes are used by _PopupMenuRouteLayout to compute the +// y coordinate of the menu's origin so that the center of selected menu +// item lines up with the center of its PopupMenuButton. +class _MenuItem extends SingleChildRenderObjectWidget { + const _MenuItem({ + Key? key, + required this.onLayout, + required Widget? child, + }) : assert(onLayout != null), + super(key: key, child: child); + + final ValueChanged onLayout; + + @override + RenderObject createRenderObject(BuildContext context) { + return _RenderMenuItem(onLayout); + } + + @override + void updateRenderObject( + BuildContext context, covariant _RenderMenuItem renderObject) { + renderObject.onLayout = onLayout; + } +} + +class _RenderMenuItem extends RenderShiftedBox { + _RenderMenuItem(this.onLayout, [RenderBox? child]) + : assert(onLayout != null), + super(child); + + ValueChanged onLayout; + + @override + Size computeDryLayout(BoxConstraints constraints) { + if (child == null) { + return Size.zero; + } + return child!.getDryLayout(constraints); + } + + @override + void performLayout() { + if (child == null) { + size = Size.zero; + } else { + child!.layout(constraints, parentUsesSize: true); + size = constraints.constrain(child!.size); + final BoxParentData childParentData = child!.parentData! as BoxParentData; + childParentData.offset = Offset.zero; + } + onLayout(size); + } +} + +/// An item in a material design popup menu. +/// +/// To show a popup menu, use the [showMenu] function. To create a button that +/// shows a popup menu, consider using [PopupMenuButton]. +/// +/// To show a checkmark next to a popup menu item, consider using +/// [CheckedPopupMenuItem]. +/// +/// Typically the [child] of a [PopupMenuItem] is a [Text] widget. More +/// elaborate menus with icons can use a [ListTile]. By default, a +/// [PopupMenuItem] is [kMinInteractiveDimension] pixels high. If you use a widget +/// with a different height, it must be specified in the [height] property. +/// +/// {@tool snippet} +/// +/// Here, a [Text] widget is used with a popup menu item. The `Menu` type +/// is an enum, not shown here. +/// +/// ```dart +/// const PopupMenuItem( +/// value: Menu.itemOne, +/// child: Text('Item 1'), +/// ) +/// ``` +/// {@end-tool} +/// +/// See the example at [PopupMenuButton] for how this example could be used in a +/// complete menu, and see the example at [CheckedPopupMenuItem] for one way to +/// keep the text of [PopupMenuItem]s that use [Text] widgets in their [child] +/// slot aligned with the text of [CheckedPopupMenuItem]s or of [PopupMenuItem] +/// that use a [ListTile] in their [child] slot. +/// +/// See also: +/// +/// * [PopupMenuDivider], which can be used to divide items from each other. +/// * [CheckedPopupMenuItem], a variant of [PopupMenuItem] with a checkmark. +/// * [showMenu], a method to dynamically show a popup menu at a given location. +/// * [PopupMenuButton], an [IconButton] that automatically shows a menu when +/// it is tapped. +class PopupMenuItem extends PopupMenuEntry { + /// Creates an item for a popup menu. + /// + /// By default, the item is [enabled]. + /// + /// The `enabled` and `height` arguments must not be null. + const PopupMenuItem({ + Key? key, + this.value, + this.onTap, + this.enabled = true, + this.height = kMinInteractiveDimension, + this.padding, + this.textStyle, + this.mouseCursor, + required this.child, + }) : assert(enabled != null), + assert(height != null), + super(key: key); + + /// The value that will be returned by [showMenu] if this entry is selected. + final T? value; + + /// Called when the menu item is tapped. + final VoidCallback? onTap; + + /// Whether the user is permitted to select this item. + /// + /// Defaults to true. If this is false, then the item will not react to + /// touches. + final bool enabled; + + /// The minimum height of the menu item. + /// + /// Defaults to [kMinInteractiveDimension] pixels. + @override + final double height; + + /// The padding of the menu item. + /// + /// Note that [height] may interact with the applied padding. For example, + /// If a [height] greater than the height of the sum of the padding and [child] + /// is provided, then the padding's effect will not be visible. + /// + /// When null, the horizontal padding defaults to 16.0 on both sides. + final EdgeInsets? padding; + + /// The text style of the popup menu item. + /// + /// If this property is null, then [PopupMenuThemeData.textStyle] is used. + /// If [PopupMenuThemeData.textStyle] is also null, then [TextTheme.subtitle1] + /// of [ThemeData.textTheme] is used. + final TextStyle? textStyle; + + /// {@template flutter.material.popupmenu.mouseCursor} + /// The cursor for a mouse pointer when it enters or is hovering over the + /// widget. + /// + /// If [mouseCursor] is a [MaterialStateProperty], + /// [MaterialStateProperty.resolve] is used for the following [MaterialState]s: + /// + /// * [MaterialState.hovered]. + /// * [MaterialState.focused]. + /// * [MaterialState.disabled]. + /// {@endtemplate} + /// + /// If null, then the value of [PopupMenuThemeData.mouseCursor] is used. If + /// that is also null, then [MaterialStateMouseCursor.clickable] is used. + final MouseCursor? mouseCursor; + + /// The widget below this widget in the tree. + /// + /// Typically a single-line [ListTile] (for menus with icons) or a [Text]. An + /// appropriate [DefaultTextStyle] is put in scope for the child. In either + /// case, the text should be short enough that it won't wrap. + final Widget? child; + + @override + bool represents(T? value) => value == this.value; + + @override + PopupMenuItemState> createState() => + PopupMenuItemState>(); +} + +/// The [State] for [PopupMenuItem] subclasses. +/// +/// By default this implements the basic styling and layout of Material Design +/// popup menu items. +/// +/// The [buildChild] method can be overridden to adjust exactly what gets placed +/// in the menu. By default it returns [PopupMenuItem.child]. +/// +/// The [handleTap] method can be overridden to adjust exactly what happens when +/// the item is tapped. By default, it uses [Navigator.pop] to return the +/// [PopupMenuItem.value] from the menu route. +/// +/// This class takes two type arguments. The second, `W`, is the exact type of +/// the [Widget] that is using this [State]. It must be a subclass of +/// [PopupMenuItem]. The first, `T`, must match the type argument of that widget +/// class, and is the type of values returned from this menu. +class PopupMenuItemState> extends State { + /// The menu item contents. + /// + /// Used by the [build] method. + /// + /// By default, this returns [PopupMenuItem.child]. Override this to put + /// something else in the menu entry. + @protected + Widget? buildChild() => widget.child; + + /// The handler for when the user selects the menu item. + /// + /// Used by the [InkWell] inserted by the [build] method. + /// + /// By default, uses [Navigator.pop] to return the [PopupMenuItem.value] from + /// the menu route. + @protected + void handleTap() { + widget.onTap?.call(); + + Navigator.pop(context, widget.value); + } + + @override + Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context); + TextStyle style = widget.textStyle ?? + popupMenuTheme.textStyle ?? + theme.textTheme.subtitle1!; + + if (!widget.enabled) style = style.copyWith(color: theme.disabledColor); + + Widget item = AnimatedDefaultTextStyle( + style: style, + duration: kThemeChangeDuration, + child: Container( + alignment: AlignmentDirectional.centerStart, + constraints: BoxConstraints(minHeight: widget.height), + padding: widget.padding ?? + const EdgeInsets.symmetric(horizontal: _kMenuHorizontalPadding), + child: buildChild(), + ), + ); + + if (!widget.enabled) { + final bool isDark = theme.brightness == Brightness.dark; + item = IconTheme.merge( + data: IconThemeData(opacity: isDark ? 0.5 : 0.38), + child: item, + ); + } + + return MergeSemantics( + child: Semantics( + enabled: widget.enabled, + button: true, + child: InkWell( + onTap: widget.enabled ? handleTap : null, + canRequestFocus: widget.enabled, + mouseCursor: _EffectiveMouseCursor( + widget.mouseCursor, popupMenuTheme.mouseCursor), + child: item, + ), + ), + ); + } +} + +/// An item with a checkmark in a material design popup menu. +/// +/// To show a popup menu, use the [showMenu] function. To create a button that +/// shows a popup menu, consider using [PopupMenuButton]. +/// +/// A [CheckedPopupMenuItem] is kMinInteractiveDimension pixels high, which +/// matches the default minimum height of a [PopupMenuItem]. The horizontal +/// layout uses [ListTile]; the checkmark is an [Icons.done] icon, shown in the +/// [ListTile.leading] position. +/// +/// {@tool snippet} +/// +/// Suppose a `Commands` enum exists that lists the possible commands from a +/// particular popup menu, including `Commands.heroAndScholar` and +/// `Commands.hurricaneCame`, and further suppose that there is a +/// `_heroAndScholar` member field which is a boolean. The example below shows a +/// menu with one menu item with a checkmark that can toggle the boolean, and +/// one menu item without a checkmark for selecting the second option. (It also +/// shows a divider placed between the two menu items.) +/// +/// ```dart +/// PopupMenuButton( +/// onSelected: (Commands result) { +/// switch (result) { +/// case Commands.heroAndScholar: +/// setState(() { _heroAndScholar = !_heroAndScholar; }); +/// break; +/// case Commands.hurricaneCame: +/// // ...handle hurricane option +/// break; +/// // ...other items handled here +/// } +/// }, +/// itemBuilder: (BuildContext context) => >[ +/// CheckedPopupMenuItem( +/// checked: _heroAndScholar, +/// value: Commands.heroAndScholar, +/// child: const Text('Hero and scholar'), +/// ), +/// const PopupMenuDivider(), +/// const PopupMenuItem( +/// value: Commands.hurricaneCame, +/// child: ListTile(leading: Icon(null), title: Text('Bring hurricane')), +/// ), +/// // ...other items listed here +/// ], +/// ) +/// ``` +/// {@end-tool} +/// +/// In particular, observe how the second menu item uses a [ListTile] with a +/// blank [Icon] in the [ListTile.leading] position to get the same alignment as +/// the item with the checkmark. +/// +/// See also: +/// +/// * [PopupMenuItem], a popup menu entry for picking a command (as opposed to +/// toggling a value). +/// * [PopupMenuDivider], a popup menu entry that is just a horizontal line. +/// * [showMenu], a method to dynamically show a popup menu at a given location. +/// * [PopupMenuButton], an [IconButton] that automatically shows a menu when +/// it is tapped. +class CheckedPopupMenuItem extends PopupMenuItem { + /// Creates a popup menu item with a checkmark. + /// + /// By default, the menu item is [enabled] but unchecked. To mark the item as + /// checked, set [checked] to true. + /// + /// The `checked` and `enabled` arguments must not be null. + const CheckedPopupMenuItem({ + Key? key, + T? value, + this.checked = false, + bool enabled = true, + EdgeInsets? padding, + double height = kMinInteractiveDimension, + Widget? child, + }) : assert(checked != null), + super( + key: key, + value: value, + enabled: enabled, + padding: padding, + height: height, + child: child, + ); + + /// Whether to display a checkmark next to the menu item. + /// + /// Defaults to false. + /// + /// When true, an [Icons.done] checkmark is displayed. + /// + /// When this popup menu item is selected, the checkmark will fade in or out + /// as appropriate to represent the implied new state. + final bool checked; + + /// The widget below this widget in the tree. + /// + /// Typically a [Text]. An appropriate [DefaultTextStyle] is put in scope for + /// the child. The text should be short enough that it won't wrap. + /// + /// This widget is placed in the [ListTile.title] slot of a [ListTile] whose + /// [ListTile.leading] slot is an [Icons.done] icon. + @override + Widget? get child => super.child; + + @override + PopupMenuItemState> createState() => + _CheckedPopupMenuItemState(); +} + +class _CheckedPopupMenuItemState + extends PopupMenuItemState> + with SingleTickerProviderStateMixin { + static const Duration _fadeDuration = Duration(milliseconds: 150); + late AnimationController _controller; + Animation get _opacity => _controller.view; + + @override + void initState() { + super.initState(); + _controller = AnimationController(duration: _fadeDuration, vsync: this) + ..value = widget.checked ? 1.0 : 0.0 + ..addListener(() => setState(() {/* animation changed */})); + } + + @override + void handleTap() { + // This fades the checkmark in or out when tapped. + if (widget.checked) + _controller.reverse(); + else + _controller.forward(); + super.handleTap(); + } + + @override + Widget buildChild() { + return ListTile( + enabled: widget.enabled, + leading: FadeTransition( + opacity: _opacity, + child: Icon(_controller.isDismissed ? null : Icons.done), + ), + title: widget.child, + ); + } +} + +class _PopupMenu extends StatelessWidget { + const _PopupMenu({ + Key? key, + required this.route, + required this.semanticLabel, + this.constraints, + }) : super(key: key); + + final _PopupMenuRoute route; + final String? semanticLabel; + final BoxConstraints? constraints; + + @override + Widget build(BuildContext context) { + final double unit = 1.0 / + (route.items.length + + 1.5); // 1.0 for the width and 0.5 for the last item's fade. + final List children = []; + final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context); + + for (int i = 0; i < route.items.length; i += 1) { + final double start = (i + 1) * unit; + final double end = (start + 1.5 * unit).clamp(0.0, 1.0); + final CurvedAnimation opacity = CurvedAnimation( + parent: route.animation!, + curve: Interval(start, end), + ); + Widget item = route.items[i]; + if (route.initialValue != null && + route.items[i].represents(route.initialValue)) { + item = Container( + color: Theme.of(context).highlightColor, + child: item, + ); + } + children.add( + _MenuItem( + onLayout: (Size size) { + route.itemSizes[i] = size; + }, + child: FadeTransition( + opacity: opacity, + child: item, + ), + ), + ); + } + + final CurveTween opacity = + CurveTween(curve: const Interval(0.0, 1.0 / 3.0)); + final CurveTween width = CurveTween(curve: Interval(0.0, unit)); + final CurveTween height = + CurveTween(curve: Interval(0.0, unit * route.items.length)); + + final Widget child = ConstrainedBox( + constraints: constraints ?? + const BoxConstraints( + minWidth: _kMenuMinWidth, + maxWidth: _kMenuMaxWidth, + ), + child: IntrinsicWidth( + stepWidth: _kMenuWidthStep, + child: Semantics( + scopesRoute: true, + namesRoute: true, + explicitChildNodes: true, + label: semanticLabel, + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric( + vertical: _kMenuVerticalPadding, + ), + child: ListBody(children: children), + ), + ), + ), + ); + + return AnimatedBuilder( + animation: route.animation!, + builder: (BuildContext context, Widget? child) { + return FadeTransition( + opacity: opacity.animate(route.animation!), + child: Material( + shape: route.shape ?? popupMenuTheme.shape, + color: route.color ?? popupMenuTheme.color, + type: MaterialType.card, + elevation: route.elevation ?? popupMenuTheme.elevation ?? 8.0, + child: Align( + alignment: AlignmentDirectional.topEnd, + widthFactor: width.evaluate(route.animation!), + heightFactor: height.evaluate(route.animation!), + child: child, + ), + ), + ); + }, + child: child, + ); + } +} + +// Positioning of the menu on the screen. +class _PopupMenuRouteLayout extends SingleChildLayoutDelegate { + _PopupMenuRouteLayout( + this.position, + this.itemSizes, + this.selectedItemIndex, + this.textDirection, + this.padding, + this.avoidBounds, + ); + + // Rectangle of underlying button, relative to the overlay's dimensions. + final RelativeRect position; + + // The sizes of each item are computed when the menu is laid out, and before + // the route is laid out. + List itemSizes; + + // The index of the selected item, or null if PopupMenuButton.initialValue + // was not specified. + final int? selectedItemIndex; + + // Whether to prefer going to the left or to the right. + final TextDirection textDirection; + + // The padding of unsafe area. + EdgeInsets padding; + + // List of rectangles that we should avoid overlapping. Unusable screen area. + final Set avoidBounds; + + // We put the child wherever position specifies, so long as it will fit within + // the specified parent size padded (inset) by 8. If necessary, we adjust the + // child's position so that it fits. + + @override + BoxConstraints getConstraintsForChild(BoxConstraints constraints) { + // The menu can be at most the size of the overlay minus 8.0 pixels in each + // direction. + return BoxConstraints.loose(constraints.biggest).deflate( + const EdgeInsets.all(_kMenuScreenPadding) + padding, + ); + } + + @override + Offset getPositionForChild(Size size, Size childSize) { + // size: The size of the overlay. + // childSize: The size of the menu, when fully open, as determined by + // getConstraintsForChild. + + final double buttonHeight = size.height - position.top - position.bottom; + // Find the ideal vertical position. + double y = position.top; + if (selectedItemIndex != null && itemSizes != null) { + double selectedItemOffset = _kMenuVerticalPadding; + for (int index = 0; index < selectedItemIndex!; index += 1) { + selectedItemOffset += itemSizes[index]!.height; + } + selectedItemOffset += itemSizes[selectedItemIndex!]!.height / 2; + y = y + buttonHeight / 2.0 - selectedItemOffset; + } + + // Find the ideal horizontal position. + double x; + // if (position.left > position.right) { + // // Menu button is closer to the right edge, so grow to the left, aligned to the right edge. + // x = size.width - position.right - childSize.width; + // } else if (position.left < position.right) { + // // Menu button is closer to the left edge, so grow to the right, aligned to the left edge. + // x = position.left; + // } else { + // Menu button is equidistant from both edges, so grow in reading direction. + assert(textDirection != null); + switch (textDirection) { + case TextDirection.rtl: + x = size.width - position.right - childSize.width; + break; + case TextDirection.ltr: + x = position.left; + break; + } + //} + final Offset wantedPosition = Offset(x, y); + final Offset originCenter = position.toRect(Offset.zero & size).center; + final Iterable subScreens = + DisplayFeatureSubScreen.subScreensInBounds( + Offset.zero & size, avoidBounds); + final Rect subScreen = _closestScreen(subScreens, originCenter); + return _fitInsideScreen(subScreen, childSize, wantedPosition); + } + + Rect _closestScreen(Iterable screens, Offset point) { + Rect closest = screens.first; + for (final Rect screen in screens) { + if ((screen.center - point).distance < + (closest.center - point).distance) { + closest = screen; + } + } + return closest; + } + + Offset _fitInsideScreen(Rect screen, Size childSize, Offset wantedPosition) { + double x = wantedPosition.dx; + double y = wantedPosition.dy; + // Avoid going outside an area defined as the rectangle 8.0 pixels from the + // edge of the screen in every direction. + if (x < screen.left + _kMenuScreenPadding + padding.left) { + x = screen.left + _kMenuScreenPadding + padding.left; + } else if (x + childSize.width > + screen.right - _kMenuScreenPadding - padding.right) { + x = screen.right - childSize.width - _kMenuScreenPadding - padding.right; + } + if (y < screen.top + _kMenuScreenPadding + padding.top) { + y = _kMenuScreenPadding + padding.top; + } else if (y + childSize.height > + screen.bottom - _kMenuScreenPadding - padding.bottom) { + y = screen.bottom - + childSize.height - + _kMenuScreenPadding - + padding.bottom; + } + + return Offset(x, y); + } + + @override + bool shouldRelayout(_PopupMenuRouteLayout oldDelegate) { + // If called when the old and new itemSizes have been initialized then + // we expect them to have the same length because there's no practical + // way to change length of the items list once the menu has been shown. + assert(itemSizes.length == oldDelegate.itemSizes.length); + + return position != oldDelegate.position || + selectedItemIndex != oldDelegate.selectedItemIndex || + textDirection != oldDelegate.textDirection || + !listEquals(itemSizes, oldDelegate.itemSizes) || + padding != oldDelegate.padding || + !setEquals(avoidBounds, oldDelegate.avoidBounds); + } +} + +class _PopupMenuRoute extends PopupRoute { + _PopupMenuRoute({ + required this.position, + required this.items, + this.initialValue, + this.elevation, + required this.barrierLabel, + this.semanticLabel, + this.shape, + this.color, + required this.capturedThemes, + this.constraints, + }) : itemSizes = List.filled(items.length, null); + + final RelativeRect position; + final List> items; + final List itemSizes; + final T? initialValue; + final double? elevation; + final String? semanticLabel; + final ShapeBorder? shape; + final Color? color; + final CapturedThemes capturedThemes; + final BoxConstraints? constraints; + + @override + Animation createAnimation() { + return CurvedAnimation( + parent: super.createAnimation(), + curve: Curves.linear, + reverseCurve: const Interval(0.0, _kMenuCloseIntervalEnd), + ); + } + + @override + Duration get transitionDuration => _kMenuDuration; + + @override + bool get barrierDismissible => true; + + @override + Color? get barrierColor => null; + + @override + final String barrierLabel; + + @override + Widget buildPage(BuildContext context, Animation animation, + Animation secondaryAnimation) { + int? selectedItemIndex; + if (initialValue != null) { + for (int index = 0; + selectedItemIndex == null && index < items.length; + index += 1) { + if (items[index].represents(initialValue)) selectedItemIndex = index; + } + } + + final Widget menu = _PopupMenu( + route: this, + semanticLabel: semanticLabel, + constraints: constraints, + ); + final MediaQueryData mediaQuery = MediaQuery.of(context); + return MediaQuery.removePadding( + context: context, + removeTop: true, + removeBottom: true, + removeLeft: true, + removeRight: true, + child: Builder( + builder: (BuildContext context) { + return CustomSingleChildLayout( + delegate: _PopupMenuRouteLayout( + position, + itemSizes, + selectedItemIndex, + Directionality.of(context), + mediaQuery.padding, + _avoidBounds(mediaQuery), + ), + child: capturedThemes.wrap(menu), + ); + }, + ), + ); + } + + Set _avoidBounds(MediaQueryData mediaQuery) { + return DisplayFeatureSubScreen.avoidBounds(mediaQuery).toSet(); + } +} + +/// Show a popup menu that contains the `items` at `position`. +/// +/// `items` should be non-null and not empty. +/// +/// If `initialValue` is specified then the first item with a matching value +/// will be highlighted and the value of `position` gives the rectangle whose +/// vertical center will be aligned with the vertical center of the highlighted +/// item (when possible). +/// +/// If `initialValue` is not specified then the top of the menu will be aligned +/// with the top of the `position` rectangle. +/// +/// In both cases, the menu position will be adjusted if necessary to fit on the +/// screen. +/// +/// Horizontally, the menu is positioned so that it grows in the direction that +/// has the most room. For example, if the `position` describes a rectangle on +/// the left edge of the screen, then the left edge of the menu is aligned with +/// the left edge of the `position`, and the menu grows to the right. If both +/// edges of the `position` are equidistant from the opposite edge of the +/// screen, then the ambient [Directionality] is used as a tie-breaker, +/// preferring to grow in the reading direction. +/// +/// The positioning of the `initialValue` at the `position` is implemented by +/// iterating over the `items` to find the first whose +/// [PopupMenuEntry.represents] method returns true for `initialValue`, and then +/// summing the values of [PopupMenuEntry.height] for all the preceding widgets +/// in the list. +/// +/// The `elevation` argument specifies the z-coordinate at which to place the +/// menu. The elevation defaults to 8, the appropriate elevation for popup +/// menus. +/// +/// The `context` argument is used to look up the [Navigator] and [Theme] for +/// the menu. It is only used when the method is called. Its corresponding +/// widget can be safely removed from the tree before the popup menu is closed. +/// +/// The `useRootNavigator` argument is used to determine whether to push the +/// menu to the [Navigator] furthest from or nearest to the given `context`. It +/// is `false` by default. +/// +/// The `semanticLabel` argument is used by accessibility frameworks to +/// announce screen transitions when the menu is opened and closed. If this +/// label is not provided, it will default to +/// [MaterialLocalizations.popupMenuLabel]. +/// +/// See also: +/// +/// * [PopupMenuItem], a popup menu entry for a single value. +/// * [PopupMenuDivider], a popup menu entry that is just a horizontal line. +/// * [CheckedPopupMenuItem], a popup menu item with a checkmark. +/// * [PopupMenuButton], which provides an [IconButton] that shows a menu by +/// calling this method automatically. +/// * [SemanticsConfiguration.namesRoute], for a description of edge triggered +/// semantics. +Future showMenu({ + required BuildContext context, + required RelativeRect position, + required List> items, + T? initialValue, + double? elevation, + String? semanticLabel, + ShapeBorder? shape, + Color? color, + bool useRootNavigator = false, + BoxConstraints? constraints, +}) { + assert(context != null); + assert(position != null); + assert(useRootNavigator != null); + assert(items != null && items.isNotEmpty); + assert(debugCheckHasMaterialLocalizations(context)); + + switch (Theme.of(context).platform) { + case TargetPlatform.iOS: + case TargetPlatform.macOS: + break; + case TargetPlatform.android: + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + case TargetPlatform.windows: + semanticLabel ??= MaterialLocalizations.of(context).popupMenuLabel; + } + + final NavigatorState navigator = + Navigator.of(context, rootNavigator: useRootNavigator); + return navigator.push(_PopupMenuRoute( + position: position, + items: items, + initialValue: initialValue, + elevation: elevation, + semanticLabel: semanticLabel, + barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel, + shape: shape, + color: color, + capturedThemes: + InheritedTheme.capture(from: context, to: navigator.context), + constraints: constraints, + )); +} + +/// Signature for the callback invoked when a menu item is selected. The +/// argument is the value of the [PopupMenuItem] that caused its menu to be +/// dismissed. +/// +/// Used by [PopupMenuButton.onSelected]. +typedef PopupMenuItemSelected = void Function(T value); + +/// Signature for the callback invoked when a [PopupMenuButton] is dismissed +/// without selecting an item. +/// +/// Used by [PopupMenuButton.onCanceled]. +typedef PopupMenuCanceled = void Function(); + +/// Signature used by [PopupMenuButton] to lazily construct the items shown when +/// the button is pressed. +/// +/// Used by [PopupMenuButton.itemBuilder]. +typedef PopupMenuItemBuilder = List> Function( + BuildContext context); + +/// Displays a menu when pressed and calls [onSelected] when the menu is dismissed +/// because an item was selected. The value passed to [onSelected] is the value of +/// the selected menu item. +/// +/// One of [child] or [icon] may be provided, but not both. If [icon] is provided, +/// then [PopupMenuButton] behaves like an [IconButton]. +/// +/// If both are null, then a standard overflow icon is created (depending on the +/// platform). +/// +/// {@tool dartpad} +/// This example shows a menu with four items, selecting between an enum's +/// values and setting a `_selectedMenu` field based on the selection +/// +/// ** See code in examples/api/lib/material/popupmenu/popupmenu.0.dart ** +/// {@end-tool} +/// +/// See also: +/// +/// * [PopupMenuItem], a popup menu entry for a single value. +/// * [PopupMenuDivider], a popup menu entry that is just a horizontal line. +/// * [CheckedPopupMenuItem], a popup menu item with a checkmark. +/// * [showMenu], a method to dynamically show a popup menu at a given location. +class PopupMenuButton extends StatefulWidget { + /// Creates a button that shows a popup menu. + /// + /// The [itemBuilder] argument must not be null. + const PopupMenuButton({ + Key? key, + required this.itemBuilder, + this.initialValue, + this.onSelected, + this.onCanceled, + this.tooltip, + this.elevation, + this.padding = const EdgeInsets.all(8.0), + this.child, + this.splashRadius, + this.icon, + this.iconSize, + this.offset = Offset.zero, + this.enabled = true, + this.shape, + this.color, + this.enableFeedback, + this.constraints, + this.position = PopupMenuPosition.over, + }) : assert(itemBuilder != null), + assert(enabled != null), + assert( + !(child != null && icon != null), + 'You can only pass [child] or [icon], not both.', + ), + super(key: key); + + /// Called when the button is pressed to create the items to show in the menu. + final PopupMenuItemBuilder itemBuilder; + + /// The value of the menu item, if any, that should be highlighted when the menu opens. + final T? initialValue; + + /// Called when the user selects a value from the popup menu created by this button. + /// + /// If the popup menu is dismissed without selecting a value, [onCanceled] is + /// called instead. + final PopupMenuItemSelected? onSelected; + + /// Called when the user dismisses the popup menu without selecting an item. + /// + /// If the user selects a value, [onSelected] is called instead. + final PopupMenuCanceled? onCanceled; + + /// Text that describes the action that will occur when the button is pressed. + /// + /// This text is displayed when the user long-presses on the button and is + /// used for accessibility. + final String? tooltip; + + /// The z-coordinate at which to place the menu when open. This controls the + /// size of the shadow below the menu. + /// + /// Defaults to 8, the appropriate elevation for popup menus. + final double? elevation; + + /// Matches IconButton's 8 dps padding by default. In some cases, notably where + /// this button appears as the trailing element of a list item, it's useful to be able + /// to set the padding to zero. + final EdgeInsetsGeometry padding; + + /// The splash radius. + /// + /// If null, default splash radius of [InkWell] or [IconButton] is used. + final double? splashRadius; + + /// If provided, [child] is the widget used for this button + /// and the button will utilize an [InkWell] for taps. + final Widget? child; + + /// If provided, the [icon] is used for this button + /// and the button will behave like an [IconButton]. + final Widget? icon; + + /// The offset is applied relative to the initial position + /// set by the [position]. + /// + /// When not set, the offset defaults to [Offset.zero]. + final Offset offset; + + /// Whether this popup menu button is interactive. + /// + /// Must be non-null, defaults to `true` + /// + /// If `true` the button will respond to presses by displaying the menu. + /// + /// If `false`, the button is styled with the disabled color from the + /// current [Theme] and will not respond to presses or show the popup + /// menu and [onSelected], [onCanceled] and [itemBuilder] will not be called. + /// + /// This can be useful in situations where the app needs to show the button, + /// but doesn't currently have anything to show in the menu. + final bool enabled; + + /// If provided, the shape used for the menu. + /// + /// If this property is null, then [PopupMenuThemeData.shape] is used. + /// If [PopupMenuThemeData.shape] is also null, then the default shape for + /// [MaterialType.card] is used. This default shape is a rectangle with + /// rounded edges of BorderRadius.circular(2.0). + final ShapeBorder? shape; + + /// If provided, the background color used for the menu. + /// + /// If this property is null, then [PopupMenuThemeData.color] is used. + /// If [PopupMenuThemeData.color] is also null, then + /// Theme.of(context).cardColor is used. + final Color? color; + + /// Whether detected gestures should provide acoustic and/or haptic feedback. + /// + /// For example, on Android a tap will produce a clicking sound and a + /// long-press will produce a short vibration, when feedback is enabled. + /// + /// See also: + /// + /// * [Feedback] for providing platform-specific feedback to certain actions. + final bool? enableFeedback; + + /// If provided, the size of the [Icon]. + /// + /// If this property is null, then [IconThemeData.size] is used. + /// If [IconThemeData.size] is also null, then + /// default size is 24.0 pixels. + final double? iconSize; + + /// Optional size constraints for the menu. + /// + /// When unspecified, defaults to: + /// ```dart + /// const BoxConstraints( + /// minWidth: 2.0 * 56.0, + /// maxWidth: 5.0 * 56.0, + /// ) + /// ``` + /// + /// The default constraints ensure that the menu width matches maximum width + /// recommended by the material design guidelines. + /// Specifying this parameter enables creation of menu wider than + /// the default maximum width. + final BoxConstraints? constraints; + + /// Whether the popup menu is positioned over or under the popup menu button. + /// + /// [offset] is used to change the position of the popup menu relative to the + /// position set by this parameter. + /// + /// When not set, the position defaults to [PopupMenuPosition.over] which makes the + /// popup menu appear directly over the button that was used to create it. + final PopupMenuPosition position; + + @override + PopupMenuButtonState createState() => PopupMenuButtonState(); +} + +/// The [State] for a [PopupMenuButton]. +/// +/// See [showButtonMenu] for a way to programmatically open the popup menu +/// of your button state. +class PopupMenuButtonState extends State> { + /// A method to show a popup menu with the items supplied to + /// [PopupMenuButton.itemBuilder] at the position of your [PopupMenuButton]. + /// + /// By default, it is called when the user taps the button and [PopupMenuButton.enabled] + /// is set to `true`. Moreover, you can open the button by calling the method manually. + /// + /// You would access your [PopupMenuButtonState] using a [GlobalKey] and + /// show the menu of the button with `globalKey.currentState.showButtonMenu`. + void showButtonMenu() { + final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context); + final RenderBox button = context.findRenderObject()! as RenderBox; + final RenderBox overlay = + Navigator.of(context).overlay!.context.findRenderObject()! as RenderBox; + final Offset offset; + switch (widget.position) { + case PopupMenuPosition.over: + offset = widget.offset; + break; + case PopupMenuPosition.under: + offset = + Offset(0.0, button.size.height - (widget.padding.vertical / 2)) + + widget.offset; + break; + case PopupMenuPosition.overSide: + offset = + Offset(button.size.width - (widget.padding.horizontal / 2), 0.0) + + widget.offset; + break; + case PopupMenuPosition.underSide: + offset = Offset(button.size.width - (widget.padding.horizontal / 2), + button.size.height - (widget.padding.vertical / 2)) + + widget.offset; + break; + } + final RelativeRect position = RelativeRect.fromRect( + Rect.fromPoints( + button.localToGlobal(offset, ancestor: overlay), + button.localToGlobal(button.size.bottomRight(Offset.zero) + offset, + ancestor: overlay), + ), + Offset.zero & overlay.size, + ); + final List> items = widget.itemBuilder(context); + // Only show the menu if there is something to show + if (items.isNotEmpty) { + showMenu( + context: context, + elevation: widget.elevation ?? popupMenuTheme.elevation, + items: items, + initialValue: widget.initialValue, + position: position, + shape: widget.shape ?? popupMenuTheme.shape, + color: widget.color ?? popupMenuTheme.color, + constraints: widget.constraints, + ).then((T? newValue) { + if (!mounted) return null; + if (newValue == null) { + widget.onCanceled?.call(); + return null; + } + widget.onSelected?.call(newValue); + }); + } + } + + bool get _canRequestFocus { + final NavigationMode mode = MediaQuery.maybeOf(context)?.navigationMode ?? + NavigationMode.traditional; + switch (mode) { + case NavigationMode.traditional: + return widget.enabled; + case NavigationMode.directional: + return true; + } + } + + @override + Widget build(BuildContext context) { + final IconThemeData iconTheme = IconTheme.of(context); + final bool enableFeedback = widget.enableFeedback ?? + PopupMenuTheme.of(context).enableFeedback ?? + true; + + assert(debugCheckHasMaterialLocalizations(context)); + + if (widget.child != null) + return Tooltip( + message: + widget.tooltip ?? MaterialLocalizations.of(context).showMenuTooltip, + child: InkWell( + onTap: widget.enabled ? showButtonMenu : null, + canRequestFocus: _canRequestFocus, + radius: widget.splashRadius, + enableFeedback: enableFeedback, + child: widget.child, + ), + ); + + return IconButton( + 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, + ); + } +} + +// This MaterialStateProperty is passed along to the menu item's InkWell which +// resolves the property against MaterialState.disabled, MaterialState.hovered, +// MaterialState.focused. +class _EffectiveMouseCursor extends MaterialStateMouseCursor { + const _EffectiveMouseCursor(this.widgetCursor, this.themeCursor); + + final MouseCursor? widgetCursor; + final MaterialStateProperty? themeCursor; + + @override + MouseCursor resolve(Set states) { + return MaterialStateProperty.resolveAs( + widgetCursor, states) ?? + themeCursor?.resolve(states) ?? + MaterialStateMouseCursor.clickable.resolve(states); + } + + @override + String get debugDescription => 'MaterialStateMouseCursor(PopupMenuItemState)'; +} diff --git a/flutter/lib/desktop/widgets/popup_menu.dart b/flutter/lib/desktop/widgets/popup_menu.dart new file mode 100644 index 000000000..acb8f184c --- /dev/null +++ b/flutter/lib/desktop/widgets/popup_menu.dart @@ -0,0 +1,375 @@ +import 'dart:core'; + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:tuple/tuple.dart'; + +import './material_mod_popup_menu.dart' as modMenu; + +const kInvalidValueStr = "InvalidValueStr"; + +// https://stackoverflow.com/questions/68318314/flutter-popup-menu-inside-popup-menu +class PopupMenuChildrenItem extends modMenu.PopupMenuEntry { + const PopupMenuChildrenItem({ + key, + this.height = kMinInteractiveDimension, + this.padding, + this.enable = true, + this.textStyle, + this.onTap, + this.position = modMenu.PopupMenuPosition.overSide, + this.offset = Offset.zero, + required this.itemBuilder, + required this.child, + }) : super(key: key); + + final modMenu.PopupMenuPosition position; + final Offset offset; + final TextStyle? textStyle; + final EdgeInsets? padding; + final bool enable; + final void Function()? onTap; + final List> Function(BuildContext) itemBuilder; + final Widget child; + + @override + final double height; + + @override + bool represents(T? value) => false; + + @override + MyPopupMenuItemState> createState() => + MyPopupMenuItemState>(); +} + +class MyPopupMenuItemState> + extends State { + @protected + void handleTap(T value) { + widget.onTap?.call(); + Navigator.pop(context, value); + } + + @override + Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context); + TextStyle style = widget.textStyle ?? + popupMenuTheme.textStyle ?? + theme.textTheme.subtitle1!; + + return modMenu.PopupMenuButton( + enabled: widget.enable, + position: widget.position, + offset: widget.offset, + onSelected: handleTap, + itemBuilder: widget.itemBuilder, + padding: EdgeInsets.zero, + child: AnimatedDefaultTextStyle( + style: style, + duration: kThemeChangeDuration, + child: Container( + alignment: AlignmentDirectional.centerStart, + constraints: BoxConstraints(minHeight: widget.height), + padding: widget.padding ?? const EdgeInsets.symmetric(horizontal: 16), + child: widget.child, + ), + ), + ); + } +} + +class MenuConfig { + // adapt to the screen height + static const fontSize = 14.0; + static const midPadding = 10.0; + static const iconScale = 0.8; + static const iconWidth = 12.0; + static const iconHeight = 12.0; + + final double secondMenuHeight; + final Color commonColor; + + const MenuConfig( + {required this.commonColor, + this.secondMenuHeight = kMinInteractiveDimension}); +} + +abstract class MenuEntryBase { + modMenu.PopupMenuEntry build(BuildContext context, MenuConfig conf); +} + +class MenuEntryDivider extends MenuEntryBase { + @override + modMenu.PopupMenuEntry build(BuildContext context, MenuConfig conf) { + return const modMenu.PopupMenuDivider(); + } +} + +typedef RadioOptionsGetter = List> Function(); +typedef RadioCurOptionGetter = Future Function(); +typedef RadioOptionSetter = Future Function(String); + +class MenuEntrySubRadios extends MenuEntryBase { + final String text; + final RadioOptionsGetter optionsGetter; + final RadioCurOptionGetter curOptionGetter; + final RadioOptionSetter optionSetter; + final RxString _curOption = "".obs; + + MenuEntrySubRadios( + {required this.text, + required this.optionsGetter, + required this.curOptionGetter, + required this.optionSetter}) { + () async { + _curOption.value = await curOptionGetter(); + }(); + } + + List> get options => optionsGetter(); + RxString get curOption => _curOption; + setOption(String option) async { + await optionSetter(option); + final opt = await curOptionGetter(); + if (_curOption.value != opt) { + _curOption.value = opt; + } + } + + modMenu.PopupMenuEntry _buildSecondMenu( + BuildContext context, MenuConfig conf, Tuple2 opt) { + return modMenu.PopupMenuItem( + padding: EdgeInsets.zero, + child: TextButton( + child: Container( + alignment: AlignmentDirectional.centerStart, + constraints: BoxConstraints(minHeight: conf.secondMenuHeight), + child: Row( + children: [ + SizedBox( + width: 20.0, + height: 20.0, + child: Obx(() => opt.item2 == curOption.value + ? Icon( + Icons.check, + color: conf.commonColor, + ) + : SizedBox.shrink())), + const SizedBox(width: MenuConfig.midPadding), + Text( + opt.item1, + style: const TextStyle( + color: Colors.black, + fontSize: MenuConfig.fontSize, + fontWeight: FontWeight.normal), + ) + ], + ), + ), + onPressed: () { + if (opt.item2 != curOption.value) { + setOption(opt.item2); + } + }, + ), + ); + } + + @override + modMenu.PopupMenuEntry build(BuildContext context, MenuConfig conf) { + return PopupMenuChildrenItem( + height: conf.secondMenuHeight, + padding: EdgeInsets.zero, + itemBuilder: (BuildContext context) => + options.map((opt) => _buildSecondMenu(context, conf, opt)).toList(), + child: Row(children: [ + const SizedBox(width: MenuConfig.midPadding), + Text( + text, + style: const TextStyle( + color: Colors.black, + fontSize: MenuConfig.fontSize, + fontWeight: FontWeight.normal), + ), + Expanded( + child: Align( + alignment: Alignment.centerRight, + child: Icon( + Icons.keyboard_arrow_right, + color: conf.commonColor, + ), + )) + ]), + ); + } +} + +typedef SwitchGetter = Future Function(); +typedef SwitchSetter = Future Function(bool); + +abstract class MenuEntrySwitchBase extends MenuEntryBase { + final String text; + + MenuEntrySwitchBase({required this.text}); + + RxBool get curOption; + Future setOption(bool option); + + @override + modMenu.PopupMenuEntry build(BuildContext context, MenuConfig conf) { + return modMenu.PopupMenuItem( + padding: EdgeInsets.zero, + child: Obx( + () => SwitchListTile( + value: curOption.value, + onChanged: (v) { + setOption(v); + }, + title: Container( + alignment: AlignmentDirectional.centerStart, + constraints: BoxConstraints(minHeight: conf.secondMenuHeight), + child: Text( + text, + style: const TextStyle( + color: Colors.black, + fontSize: MenuConfig.fontSize, + fontWeight: FontWeight.normal), + )), + dense: true, + visualDensity: const VisualDensity( + horizontal: VisualDensity.minimumDensity, + vertical: VisualDensity.minimumDensity, + ), + contentPadding: EdgeInsets.only(left: 8.0), + ), + ), + ); + } +} + +class MenuEntrySwitch extends MenuEntrySwitchBase { + final SwitchGetter getter; + final SwitchSetter setter; + final RxBool _curOption = false.obs; + + MenuEntrySwitch( + {required String text, required this.getter, required this.setter}) + : super(text: text) { + () async { + _curOption.value = await getter(); + }(); + } + + @override + RxBool get curOption => _curOption; + @override + setOption(bool option) async { + await setter(option); + final opt = await getter(); + if (_curOption.value != opt) { + _curOption.value = opt; + } + } +} + +typedef Switch2Getter = RxBool Function(); +typedef Switch2Setter = Future Function(bool); + +class MenuEntrySwitch2 extends MenuEntrySwitchBase { + final Switch2Getter getter; + final SwitchSetter setter; + + MenuEntrySwitch2( + {required String text, required this.getter, required this.setter}) + : super(text: text); + + @override + RxBool get curOption => getter(); + @override + setOption(bool option) async { + await setter(option); + } +} + +class MenuEntrySubMenu extends MenuEntryBase { + final String text; + final List> entries; + + MenuEntrySubMenu({ + required this.text, + required this.entries, + }); + + @override + modMenu.PopupMenuEntry build(BuildContext context, MenuConfig conf) { + return PopupMenuChildrenItem( + height: conf.secondMenuHeight, + padding: EdgeInsets.zero, + position: modMenu.PopupMenuPosition.overSide, + itemBuilder: (BuildContext context) => + entries.map((entry) => entry.build(context, conf)).toList(), + child: Row(children: [ + const SizedBox(width: MenuConfig.midPadding), + Text( + text, + style: const TextStyle( + color: Colors.black, + fontSize: MenuConfig.fontSize, + fontWeight: FontWeight.normal), + ), + Expanded( + child: Align( + alignment: Alignment.centerRight, + child: Icon( + Icons.keyboard_arrow_right, + color: conf.commonColor, + ), + )) + ]), + ); + } +} + +class MenuEntryButton extends MenuEntryBase { + final Widget Function(TextStyle? style) childBuilder; + Function() proc; + + MenuEntryButton({ + required this.childBuilder, + required this.proc, + }); + + @override + modMenu.PopupMenuEntry build(BuildContext context, MenuConfig conf) { + return modMenu.PopupMenuItem( + padding: EdgeInsets.zero, + child: TextButton( + child: Container( + alignment: AlignmentDirectional.centerStart, + constraints: BoxConstraints(minHeight: conf.secondMenuHeight), + child: childBuilder( + const TextStyle( + color: Colors.black, + fontSize: MenuConfig.fontSize, + fontWeight: FontWeight.normal), + )), + onPressed: () { + proc(); + }, + ), + ); + } +} + +class CustomMenu { + final List> entries; + final MenuConfig conf; + + const CustomMenu({required this.entries, required this.conf}); + + List> build(BuildContext context) { + return entries.map((entry) => entry.build(context, conf)).toList(); + } +} diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart new file mode 100644 index 000000000..9568d0404 --- /dev/null +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -0,0 +1,560 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hbb/models/chat_model.dart'; +import 'package:get/get.dart'; +import 'package:tuple/tuple.dart'; + +import '../../common.dart'; +import '../../mobile/widgets/dialog.dart'; +import '../../mobile/widgets/overlay.dart'; +import '../../models/model.dart'; +import '../../models/platform_model.dart'; +import './popup_menu.dart'; +import './material_mod_popup_menu.dart' as modMenu; + +class _MenubarTheme { + static const Color commonColor = MyTheme.accent; + static const double height = kMinInteractiveDimension; +} + +class RemoteMenubar extends StatefulWidget { + final String id; + final FFI ffi; + + const RemoteMenubar({ + Key? key, + required this.id, + required this.ffi, + }) : super(key: key); + + @override + State createState() => _RemoteMenubarState(); +} + +class _RemoteMenubarState extends State { + final RxBool _show = false.obs; + final Rx _hideColor = Colors.white12.obs; + + bool get isFullscreen => Get.find(tag: 'fullscreen').isTrue; + void setFullscreen(bool v) { + Get.find(tag: 'fullscreen').value = v; + } + + @override + Widget build(BuildContext context) { + return Align( + alignment: Alignment.topCenter, + child: Obx( + () => _show.value ? _buildMenubar(context) : _buildShowHide(context)), + ); + } + + Widget _buildShowHide(BuildContext context) { + return SizedBox( + width: 100, + height: 5, + child: TextButton( + onHover: (bool v) { + _hideColor.value = v ? Colors.white60 : Colors.white24; + }, + onPressed: () { + _show.value = !_show.value; + }, + child: Obx(() => Container( + color: _hideColor.value, + )))); + } + + Widget _buildMenubar(BuildContext context) { + final List menubarItems = []; + if (!isWebDesktop) { + menubarItems.add(_buildFullscreen(context)); + if (widget.ffi.ffiModel.isPeerAndroid) { + menubarItems.add(IconButton( + tooltip: translate('Mobile Actions'), + color: _MenubarTheme.commonColor, + icon: Icon(Icons.build), + onPressed: () { + if (mobileActionsOverlayEntry == null) { + showMobileActionsOverlay(); + } else { + hideMobileActionsOverlay(); + } + }, + )); + } + } + menubarItems.add(_buildMonitor(context)); + menubarItems.add(_buildControl(context)); + menubarItems.add(_buildDisplay(context)); + if (!isWeb) { + menubarItems.add(_buildChat(context)); + } + menubarItems.add(_buildClose(context)); + return PopupMenuTheme( + data: PopupMenuThemeData( + textStyle: TextStyle(color: _MenubarTheme.commonColor)), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + Container( + color: Colors.white, + child: Row( + mainAxisSize: MainAxisSize.min, + children: menubarItems, + )), + _buildShowHide(context), + ])); + } + + Widget _buildFullscreen(BuildContext context) { + return IconButton( + tooltip: translate(isFullscreen ? 'Exit Fullscreen' : 'Fullscreen'), + onPressed: () { + setFullscreen(!isFullscreen); + }, + icon: Obx(() => isFullscreen + ? Icon( + Icons.fullscreen_exit, + color: _MenubarTheme.commonColor, + ) + : Icon( + Icons.fullscreen, + color: _MenubarTheme.commonColor, + )), + ); + } + + Widget _buildChat(BuildContext context) { + return IconButton( + tooltip: translate('Chat'), + onPressed: () { + widget.ffi.chatModel.changeCurrentID(ChatModel.clientModeID); + widget.ffi.chatModel.toggleChatOverlay(); + }, + icon: Icon( + Icons.message, + color: _MenubarTheme.commonColor, + ), + ); + } + + Widget _buildMonitor(BuildContext context) { + final pi = widget.ffi.ffiModel.pi; + return modMenu.PopupMenuButton( + tooltip: translate('Select Monitor'), + padding: EdgeInsets.zero, + position: modMenu.PopupMenuPosition.under, + icon: Stack( + alignment: Alignment.center, + children: [ + Icon( + Icons.personal_video, + color: _MenubarTheme.commonColor, + ), + Padding( + padding: EdgeInsets.only(bottom: 3.9), + child: Obx(() { + RxInt display = CurrentDisplayState.find(widget.id); + return Text( + "${display.value + 1}/${pi.displays.length}", + style: TextStyle(color: _MenubarTheme.commonColor, fontSize: 8), + ); + }), + ) + ], + ), + itemBuilder: (BuildContext context) { + final List rowChildren = []; + final double selectorScale = 1.3; + for (int i = 0; i < pi.displays.length; i++) { + rowChildren.add(Transform.scale( + scale: selectorScale, + child: Stack( + alignment: Alignment.center, + children: [ + Icon( + Icons.personal_video, + color: _MenubarTheme.commonColor, + ), + TextButton( + child: Container( + alignment: AlignmentDirectional.center, + constraints: + BoxConstraints(minHeight: _MenubarTheme.height), + child: Padding( + padding: EdgeInsets.only(bottom: 2.5), + child: Text( + (i + 1).toString(), + style: TextStyle(color: _MenubarTheme.commonColor), + ), + )), + onPressed: () { + RxInt display = CurrentDisplayState.find(widget.id); + if (display.value != i) { + bind.sessionSwitchDisplay(id: widget.id, value: i); + pi.currentDisplay = i; + display.value = i; + } + }, + ) + ], + ), + )); + } + return >[ + modMenu.PopupMenuItem( + height: _MenubarTheme.height, + padding: EdgeInsets.zero, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: rowChildren), + ) + ]; + }, + ); + } + + Widget _buildControl(BuildContext context) { + return modMenu.PopupMenuButton( + padding: EdgeInsets.zero, + icon: Icon( + Icons.bolt, + color: _MenubarTheme.commonColor, + ), + tooltip: translate('Control Actions'), + position: modMenu.PopupMenuPosition.under, + itemBuilder: (BuildContext context) => _getControlMenu() + .map((entry) => entry.build( + context, + MenuConfig( + commonColor: _MenubarTheme.commonColor, + secondMenuHeight: _MenubarTheme.height, + ))) + .toList(), + ); + } + + Widget _buildDisplay(BuildContext context) { + return modMenu.PopupMenuButton( + padding: EdgeInsets.zero, + icon: Icon( + Icons.tv, + color: _MenubarTheme.commonColor, + ), + tooltip: translate('Display Settings'), + position: modMenu.PopupMenuPosition.under, + onSelected: (String item) {}, + itemBuilder: (BuildContext context) => _getDisplayMenu() + .map((entry) => entry.build( + context, + MenuConfig( + commonColor: _MenubarTheme.commonColor, + secondMenuHeight: _MenubarTheme.height, + ))) + .toList(), + ); + } + + Widget _buildClose(BuildContext context) { + return IconButton( + tooltip: translate('Close'), + onPressed: () { + clientClose(widget.ffi.dialogManager); + }, + icon: Icon( + Icons.close, + color: _MenubarTheme.commonColor, + ), + ); + } + + List> _getControlMenu() { + final pi = widget.ffi.ffiModel.pi; + final perms = widget.ffi.ffiModel.permissions; + + final List> displayMenu = []; + + if (pi.version.isNotEmpty) { + displayMenu.add(MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Refresh'), + style: style, + ), + proc: () { + Navigator.pop(context); + bind.sessionRefresh(id: widget.id); + }, + )); + } + displayMenu.add(MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('OS Password'), + style: style, + ), + proc: () { + Navigator.pop(context); + showSetOSPassword(widget.id, false, widget.ffi.dialogManager); + }, + )); + + if (!isWebDesktop) { + if (perms['keyboard'] != false && perms['clipboard'] != false) { + displayMenu.add(MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Paste'), + style: style, + ), + proc: () { + Navigator.pop(context); + () async { + ClipboardData? data = + await Clipboard.getData(Clipboard.kTextPlain); + if (data != null && data.text != null) { + bind.sessionInputString(id: widget.id, value: data.text ?? ""); + } + }(); + }, + )); + } + + displayMenu.add(MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Reset canvas'), + style: style, + ), + proc: () { + Navigator.pop(context); + widget.ffi.cursorModel.reset(); + }, + )); + } + + if (perms['keyboard'] != false) { + if (pi.platform == 'Linux' || pi.sasEnabled) { + displayMenu.add(MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Insert') + ' Ctrl + Alt + Del', + style: style, + ), + proc: () { + Navigator.pop(context); + bind.sessionCtrlAltDel(id: widget.id); + }, + )); + } + + displayMenu.add(MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Insert Lock'), + style: style, + ), + proc: () { + Navigator.pop(context); + bind.sessionLockScreen(id: widget.id); + }, + )); + + if (pi.platform == 'Windows') { + displayMenu.add(MenuEntryButton( + childBuilder: (TextStyle? style) => Obx(() => Text( + translate( + (BlockInputState.find(widget.id).value ? 'Unb' : 'B') + + 'lock user input'), + style: style, + )), + proc: () { + Navigator.pop(context); + RxBool blockInput = BlockInputState.find(widget.id); + bind.sessionToggleOption( + id: widget.id, + value: (blockInput.value ? 'un' : '') + 'block-input'); + blockInput.value = !blockInput.value; + }, + )); + } + } + + if (gFFI.ffiModel.permissions["restart"] != false && + (pi.platform == "Linux" || + pi.platform == "Windows" || + pi.platform == "Mac OS")) { + displayMenu.add(MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Restart Remote Device'), + style: style, + ), + proc: () { + Navigator.pop(context); + showRestartRemoteDevice(pi, widget.id, gFFI.dialogManager); + }, + )); + } + + return displayMenu; + } + + List> _getDisplayMenu() { + final displayMenu = [ + MenuEntrySubRadios( + text: translate('Ratio'), + optionsGetter: () => [ + Tuple2(translate('Original'), 'original'), + Tuple2(translate('Shrink'), 'shrink'), + Tuple2(translate('Stretch'), 'stretch'), + ], + curOptionGetter: () async { + return await bind.sessionGetOption( + id: widget.id, arg: 'view-style') ?? + ''; + }, + optionSetter: (String v) async { + await bind.sessionPeerOption( + id: widget.id, name: "view-style", value: v); + widget.ffi.canvasModel.updateViewStyle(); + }), + MenuEntrySubRadios( + text: translate('Scroll Style'), + optionsGetter: () => [ + Tuple2(translate('ScrollAuto'), 'scrollauto'), + Tuple2(translate('Scrollbar'), 'scrollbar'), + ], + curOptionGetter: () async { + return await bind.sessionGetOption( + id: widget.id, arg: 'scroll-style') ?? + ''; + }, + optionSetter: (String v) async { + await bind.sessionPeerOption( + id: widget.id, name: "scroll-style", value: v); + widget.ffi.canvasModel.updateScrollStyle(); + }), + MenuEntrySubRadios( + text: translate('Image Quality'), + optionsGetter: () => [ + Tuple2(translate('Good image quality'), 'best'), + Tuple2(translate('Balanced'), 'balanced'), + Tuple2( + translate('Optimize reaction time'), 'low'), + ], + curOptionGetter: () async { + String quality = + await bind.sessionGetImageQuality(id: widget.id) ?? 'balanced'; + if (quality == '') quality = 'balanced'; + return quality; + }, + optionSetter: (String v) async { + await bind.sessionSetImageQuality(id: widget.id, value: v); + }), + MenuEntrySwitch( + text: translate('Show remote cursor'), + getter: () async { + return await bind.sessionGetToggleOptionSync( + id: widget.id, arg: 'show-remote-cursor'); + }, + setter: (bool v) async { + await bind.sessionToggleOption( + id: widget.id, value: 'show-remote-cursor'); + }), + MenuEntrySwitch( + text: translate('Show quality monitor'), + getter: () async { + return await bind.sessionGetToggleOptionSync( + id: widget.id, arg: 'show-quality-monitor'); + }, + setter: (bool v) async { + await bind.sessionToggleOption( + id: widget.id, value: 'show-quality-monitor'); + widget.ffi.qualityMonitorModel.checkShowQualityMonitor(widget.id); + }), + ]; + + final perms = widget.ffi.ffiModel.permissions; + final pi = widget.ffi.ffiModel.pi; + + if (perms['audio'] != false) { + displayMenu.add(_createSwitchMenuEntry('Mute', 'disable-audio')); + } + if (perms['keyboard'] != false) { + if (perms['clipboard'] != false) { + displayMenu.add( + _createSwitchMenuEntry('Disable clipboard', 'disable-clipboard')); + } + displayMenu.add(_createSwitchMenuEntry( + 'Lock after session end', 'lock-after-session-end')); + if (pi.platform == 'Windows') { + displayMenu.add(MenuEntrySwitch2( + text: translate('Privacy mode'), + getter: () { + return PrivacyModeState.find(widget.id); + }, + setter: (bool v) async { + Navigator.pop(context); + await bind.sessionToggleOption( + id: widget.id, value: 'privacy-mode'); + })); + } + } + return displayMenu; + } + + MenuEntrySwitch _createSwitchMenuEntry(String text, String option) { + return MenuEntrySwitch( + text: translate(text), + getter: () async { + return bind.sessionGetToggleOptionSync(id: widget.id, arg: option); + }, + setter: (bool v) async { + await bind.sessionToggleOption(id: widget.id, value: option); + }); + } +} + +void showSetOSPassword( + String id, bool login, OverlayDialogManager dialogManager) async { + final controller = TextEditingController(); + var password = await bind.sessionGetOption(id: id, arg: "os-password") ?? ""; + var autoLogin = await bind.sessionGetOption(id: id, arg: "auto-login") != ""; + controller.text = password; + dialogManager.show((setState, close) { + return CustomAlertDialog( + title: Text(translate('OS Password')), + content: Column(mainAxisSize: MainAxisSize.min, children: [ + PasswordWidget(controller: controller), + CheckboxListTile( + contentPadding: const EdgeInsets.all(0), + dense: true, + controlAffinity: ListTileControlAffinity.leading, + title: Text( + translate('Auto Login'), + ), + value: autoLogin, + onChanged: (v) { + if (v == null) return; + setState(() => autoLogin = v); + }, + ), + ]), + actions: [ + TextButton( + style: flatButtonStyle, + onPressed: () { + close(); + }, + child: Text(translate('Cancel')), + ), + TextButton( + style: flatButtonStyle, + onPressed: () { + var text = controller.text.trim(); + bind.sessionPeerOption(id: id, name: "os-password", value: text); + bind.sessionPeerOption( + id: id, name: "auto-login", value: autoLogin ? 'Y' : ''); + if (text != "" && login) { + bind.sessionInputOsPassword(id: id, value: text); + } + close(); + }, + child: Text(translate('OK')), + ), + ]); + }); +} diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index dda22a779..b0ac2dc7e 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -172,9 +172,9 @@ class FfiModel with ChangeNotifier { } else if (name == 'update_quality_status') { parent.target?.qualityMonitorModel.updateQualityStatus(evt); } else if (name == 'update_block_input_state') { - updateBlockInputState(evt); + updateBlockInputState(evt, peerId); } else if (name == 'update_privacy_mode') { - updatePrivacyMode(evt); + updatePrivacyMode(evt, peerId); } }; } @@ -231,9 +231,9 @@ class FfiModel with ChangeNotifier { } else if (name == 'update_quality_status') { parent.target?.qualityMonitorModel.updateQualityStatus(evt); } else if (name == 'update_block_input_state') { - updateBlockInputState(evt); + updateBlockInputState(evt, peerId); } else if (name == 'update_privacy_mode') { - updatePrivacyMode(evt); + updatePrivacyMode(evt, peerId); } }; platformFFI.setEventCallback(cb); @@ -305,6 +305,12 @@ class FfiModel with ChangeNotifier { _pi.sasEnabled = evt['sas_enabled'] == "true"; _pi.currentDisplay = int.parse(evt['current_display']); + try { + CurrentDisplayState.find(peerId).value = _pi.currentDisplay; + } catch (e) { + // + } + if (isPeerAndroid) { _touchMode = true; if (parent.target?.ffiModel.permissions['keyboard'] != false) { @@ -343,13 +349,24 @@ class FfiModel with ChangeNotifier { notifyListeners(); } - updateBlockInputState(Map evt) { + updateBlockInputState(Map evt, String peerId) { _inputBlocked = evt['input_state'] == 'on'; notifyListeners(); + try { + BlockInputState.find(peerId).value = evt['input_state'] == 'on'; + } catch (e) { + // + } } - updatePrivacyMode(Map evt) { + updatePrivacyMode(Map evt, String peerId) { notifyListeners(); + try { + PrivacyModeState.find(peerId).value = + bind.sessionGetToggleOptionSync(id: peerId, arg: 'privacy-mode'); + } catch (e) { + // + } } } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 730c8be94..4c66acff4 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -302,5 +302,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Restarting Remote Device", "正在重启远程设备"), ("remote_restarting_tip", "远程设备正在重启, 请关闭当前提示框, 并在一段时间后使用永久密码重新连接"), ("Copied", "已复制"), + ("Exit Fullscreen", "退出全屏"), + ("Fullscreen", "全屏"), + ("Mobile Actions", "移动端操作"), + ("Select Monitor", "选择监视器"), + ("Control Actions", "控制操作"), + ("Display Settings", "显示设置"), + ("Ratio", "比例"), + ("Image Quality", "画质"), + ("Scroll Style", "滚屏方式"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index f174850fc..fbe0287aa 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -302,5 +302,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Restarting Remote Device", ""), ("remote_restarting_tip", ""), ("Copied", ""), + ("Exit Fullscreen", "Ukončete celou obrazovku"), + ("Fullscreen", "Celá obrazovka"), + ("Mobile Actions", "Mobilní akce"), + ("Select Monitor", "Vyberte možnost Monitor"), + ("Control Actions", "Ovládací akce"), + ("Display Settings", "Nastavení obrazovky"), + ("Ratio", "Poměr"), + ("Image Quality", "Kvalita obrazu"), + ("Scroll Style", "Štýl posúvania"), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 60ebeafbb..5b88db08d 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -302,5 +302,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Restarting Remote Device", ""), ("remote_restarting_tip", ""), ("Copied", ""), + ("Exit Fullscreen", "Afslut fuldskærm"), + ("Fullscreen", "Fuld skærm"), + ("Mobile Actions", "Mobile handlinger"), + ("Select Monitor", "Vælg Monitor"), + ("Control Actions", "Kontrolhandlinger"), + ("Display Settings", "Skærmindstillinger"), + ("Ratio", "Forhold"), + ("Image Quality", "Billede kvalitet"), + ("Scroll Style", "Rulstil"), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 02c73d095..f41d8c313 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -302,5 +302,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Restarting Remote Device", "Entferntes Gerät wird neu gestartet"), ("remote_restarting_tip", "Entferntes Gerät startet neu, bitte schließen Sie diese Meldung und verbinden Sie sich mit dem dauerhaften Passwort erneut."), ("Copied", ""), + ("Exit Fullscreen", "Vollbild beenden"), + ("Fullscreen", "Ganzer Bildschirm"), + ("Mobile Actions", "Mobile Aktionen"), + ("Select Monitor", "Wählen Sie Überwachen aus"), + ("Control Actions", "Kontrollaktionen"), + ("Display Settings", "Bildschirmeinstellungen"), + ("Ratio", "Verhältnis"), + ("Image Quality", "Bildqualität"), + ("Scroll Style", "Scroll-Stil"), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 3ce5c24f9..b1207ff47 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -302,5 +302,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Restarting Remote Device", ""), ("remote_restarting_tip", ""), ("Copied", ""), + ("Exit Fullscreen", "Eliru Plenekranon"), + ("Fullscreen", "Plenekrane"), + ("Mobile Actions", "Poŝtelefonaj Agoj"), + ("Select Monitor", "Elektu Monitoron"), + ("Control Actions", "Kontrolaj Agoj"), + ("Display Settings", "Montraj Agordoj"), + ("Ratio", "Proporcio"), + ("Image Quality", "Bilda Kvalito"), + ("Scroll Style", "Ruluma Stilo"), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 2fa92bac8..a018472f7 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -302,5 +302,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Restarting Remote Device", "Reiniciando dispositivo remoto"), ("remote_restarting_tip", "Dispositivo remoto reiniciando, favor de cerrar este mensaje y reconectarse con la contraseña permamente despues de un momento."), ("Copied", ""), + ("Exit Fullscreen", "Salir de pantalla completa"), + ("Fullscreen", "Pantalla completa"), + ("Mobile Actions", "Acciones móviles"), + ("Select Monitor", "Seleccionar monitor"), + ("Control Actions", "Acciones de control"), + ("Display Settings", "Configuración de pantalla"), + ("Ratio", "Relación"), + ("Image Quality", "La calidad de imagen"), + ("Scroll Style", "Estilo de desplazamiento"), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 4efc804e1..73624290b 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -302,5 +302,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Restarting Remote Device", ""), ("remote_restarting_tip", ""), ("Copied", ""), + ("Exit Fullscreen", "Quitter le mode plein écran"), + ("Fullscreen", "Plein écran"), + ("Mobile Actions", "Actions mobiles"), + ("Select Monitor", "Sélectionnez Moniteur"), + ("Control Actions", "Actions de contrôle"), + ("Display Settings", "Paramètres d'affichage"), + ("Ratio", "Rapport"), + ("Image Quality", "Qualité d'image"), + ("Scroll Style", "Style de défilement"), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index fee1fc450..15175175f 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -302,5 +302,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Restarting Remote Device", ""), ("remote_restarting_tip", ""), ("Copied", ""), + ("Exit Fullscreen", "Lépjen ki a teljes képernyőről"), + ("Fullscreen", "Teljes képernyő"), + ("Mobile Actions", "mobil műveletek"), + ("Select Monitor", "Válassza a Monitor lehetőséget"), + ("Control Actions", "Irányítási műveletek"), + ("Display Settings", "Megjelenítési beállítások"), + ("Ratio", "Hányados"), + ("Image Quality", "Képminőség"), + ("Scroll Style", "Görgetési stílus"), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index b6d9dbd0d..4415488f4 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -302,5 +302,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Restarting Remote Device", "Memulai Ulang Perangkat Jarak Jauh"), ("remote_restarting_tip", ""), ("Copied", ""), + ("Exit Fullscreen", "Keluar dari Layar Penuh"), + ("Fullscreen", "Layar penuh"), + ("Mobile Actions", "Tindakan Seluler"), + ("Select Monitor", "Pilih Monitor"), + ("Control Actions", "Tindakan Kontrol"), + ("Display Settings", "Pengaturan tampilan"), + ("Ratio", "Perbandingan"), + ("Image Quality", "Kualitas gambar"), + ("Scroll Style", "Gaya Gulir"), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index c8ad93476..c8cad290f 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -301,5 +301,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Are you sure you want to restart", "Sei sicuro di voler riavviare?"), ("Restarting Remote Device", "Il dispositivo remoto si sta riavviando"), ("remote_restarting_tip", "Riavviare il dispositivo remoto"), + ("Exit Fullscreen", "Esci dalla modalità schermo intero"), + ("Fullscreen", "A schermo intero"), + ("Mobile Actions", "Azioni mobili"), + ("Select Monitor", "Seleziona Monitora"), + ("Control Actions", "Azioni di controllo"), + ("Display Settings", "Impostazioni di visualizzazione"), + ("Ratio", "Rapporto"), + ("Image Quality", "Qualità dell'immagine"), + ("Scroll Style", "Stile di scorrimento"), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 5c6ba1da7..2cb0ce6c5 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -299,5 +299,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Are you sure you want to restart", "本当に再起動しますか"), ("Restarting Remote Device", "リモート端末を再起動中"), ("remote_restarting_tip", "リモート端末は再起動中です。このメッセージボックスを閉じて、しばらくした後に固定のパスワードを使用して再接続してください。"), + ("Exit Fullscreen", "全画面表示を終了"), + ("Fullscreen", "全画面表示"), + ("Mobile Actions", "モバイル アクション"), + ("Select Monitor", "モニターを選択"), + ("Control Actions", "コントロール アクション"), + ("Display Settings", "ディスプレイの設定"), + ("Ratio", "比率"), + ("Image Quality", "画質"), + ("Scroll Style", "スクロール スタイル"), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index a0292adcb..24e1db7bf 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -299,5 +299,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Are you sure you want to restart", "정말로 재시작 하시겠습니까"), ("Restarting Remote Device", "원격 기기를 다시 시작하는중"), ("remote_restarting_tip", "원격 장치를 다시 시작하는 중입니다. 이 메시지 상자를 닫고 잠시 후 영구 비밀번호로 다시 연결하십시오."), + ("Exit Fullscreen", "전체 화면 종료"), + ("Fullscreen", "전체화면"), + ("Mobile Actions", "모바일 액션"), + ("Select Monitor", "모니터 선택"), + ("Control Actions", "제어 작업"), + ("Display Settings", "화면 설정"), + ("Ratio", "비율"), + ("Image Quality", "이미지 품질"), + ("Scroll Style", "스크롤 스타일"), ].iter().cloned().collect(); } \ No newline at end of file diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 81eaddfaf..16ff7c4e7 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -303,5 +303,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Set security password", "Ustaw hasło zabezpieczające"), ("Connection not allowed", "Połączenie niedozwolone"), ("Copied", ""), + ("Exit Fullscreen", "Wyłączyć tryb pełnoekranowy"), + ("Fullscreen", "Pełny ekran"), + ("Mobile Actions", "Działania mobilne"), + ("Select Monitor", "Wybierz Monitor"), + ("Control Actions", "Działania kontrolne"), + ("Display Settings", "Ustawienia wyświetlania"), + ("Ratio", "Stosunek"), + ("Image Quality", "Jakość obrazu"), + ("Scroll Style", "Styl przewijania"), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT b/src/lang/pt_PT index e6e282575..adac5af15 100644 --- a/src/lang/pt_PT +++ b/src/lang/pt_PT @@ -299,5 +299,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Are you sure you want to restart", "Tem a certeza que pretende reiniciar"), ("Restarting Remote Device", "A reiniciar sistema remoto"), ("remote_restarting_tip", ""), + ("Exit Fullscreen", "Sair da tela cheia"), + ("Fullscreen", "Tela cheia"), + ("Mobile Actions", "Ações para celular"), + ("Select Monitor", "Selecionar monitor"), + ("Control Actions", "Ações de controle"), + ("Display Settings", "Configurações do visor"), + ("Ratio", "Razão"), + ("Image Quality", "Qualidade da imagem"), + ("Scroll Style", "Estilo de rolagem"), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 8d1ffbbcf..cd40fb2a5 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -302,5 +302,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Restarting Remote Device", ""), ("remote_restarting_tip", ""), ("Copied", ""), + ("Exit Fullscreen", ""), + ("Fullscreen", ""), + ("Mobile Actions", ""), + ("Select Monitor", ""), + ("Control Actions", ""), + ("Display Settings", ""), + ("Ratio", ""), + ("Image Quality", ""), + ("Scroll Style", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index ade2c7806..df07e5e8e 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -302,5 +302,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Restarting Remote Device", "Перезагрузка удаленного устройства"), ("remote_restarting_tip", "Удаленное устройство перезапускается. Пожалуйста, закройте это сообщение и через некоторое время переподключитесь, используя постоянный пароль."), ("Copied", ""), + ("Exit Fullscreen", "Выйти из полноэкранного режима"), + ("Fullscreen", "Полноэкранный"), + ("Mobile Actions", "Мобильные действия"), + ("Select Monitor", "Выберите монитор"), + ("Control Actions", "Действия по управлению"), + ("Display Settings", "Настройки отображения"), + ("Ratio", "Соотношение"), + ("Image Quality", "Качество изображения"), + ("Scroll Style", "Стиль прокрутки"), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index a887cc34a..935544eb2 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -302,5 +302,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Restarting Remote Device", ""), ("remote_restarting_tip", ""), ("Copied", ""), + ("Exit Fullscreen", "Ukončiť celú obrazovku"), + ("Fullscreen", "Celá obrazovka"), + ("Mobile Actions", "Mobilné akcie"), + ("Select Monitor", "Vyberte možnosť Monitor"), + ("Control Actions", "Kontrolné akcie"), + ("Display Settings", "Nastavenia displeja"), + ("Ratio", "Pomer"), + ("Image Quality", "Kvalita obrazu"), + ("Scroll Style", "Štýl posúvania"), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index e0b64cdfa..e92a032fb 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -302,5 +302,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Restarting Remote Device", ""), ("remote_restarting_tip", ""), ("Copied", ""), + ("Exit Fullscreen", ""), + ("Fullscreen", ""), + ("Mobile Actions", ""), + ("Select Monitor", ""), + ("Control Actions", ""), + ("Display Settings", ""), + ("Ratio", ""), + ("Image Quality", ""), + ("Scroll Style", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 410d918eb..215c14058 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -302,5 +302,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Restarting Remote Device", "Uzaktan yeniden başlatılıyor"), ("remote_restarting_tip", ""), ("Copied", ""), + ("Exit Fullscreen", "Tam ekrandan çık"), + ("Fullscreen", "Tam ekran"), + ("Mobile Actions", "Mobil İşlemler"), + ("Select Monitor", "Monitörü Seç"), + ("Control Actions", "Kontrol Eylemleri"), + ("Display Settings", "Görüntü ayarları"), + ("Ratio", "Oran"), + ("Image Quality", "Görüntü kalitesi"), + ("Scroll Style", "Kaydırma Stili"), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 5f0acdd06..39d636592 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -302,5 +302,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Restarting Remote Device", "正在重啓遠程設備"), ("remote_restarting_tip", "遠程設備正在重啓,請關閉當前提示框,並在一段時間後使用永久密碼重新連接"), ("Copied", "已複製"), + ("Exit Fullscreen", "退出全屏"), + ("Fullscreen", "全屏"), + ("Mobile Actions", "移動端操作"), + ("Select Monitor", "選擇監視器"), + ("Control Actions", "控制操作"), + ("Display Settings", "顯示設置"), + ("Ratio", "比例"), + ("Image Quality", "畫質"), + ("Scroll Style", "滾動樣式"), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 5704bf9ee..2f9dfcafd 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -302,5 +302,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Restarting Remote Device", "Đang khởi động lại thiết bị từ xa"), ("remote_restarting_tip", "Thiết bị từ xa đang khởi động lại, hãy đóng cửa sổ tin nhắn này và kết nối lại với mật khẩu vĩnh viễn sau một khoảng thời gian"), ("Copied", ""), + ("Exit Fullscreen", "Thoát toàn màn hình"), + ("Fullscreen", "Toàn màn hình"), + ("Mobile Actions", "Hành động trên thiết bị di động"), + ("Select Monitor", "Chọn màn hình"), + ("Control Actions", "Kiểm soát hành động"), + ("Display Settings", "Thiết lập hiển thị"), + ("Ratio", "Tỉ lệ"), + ("Image Quality", "Chất lượng hình ảnh"), + ("Scroll Style", "Kiểu cuộn"), ].iter().cloned().collect(); } From 55ba191ad9b003d8aca50b2195fbf222c1515322 Mon Sep 17 00:00:00 2001 From: fufesou Date: Sun, 28 Aug 2022 21:55:16 +0800 Subject: [PATCH 0308/2015] flutter_desktop: show/hide menubar tooltip Signed-off-by: fufesou --- flutter/lib/desktop/pages/remote_page.dart | 26 ++--- .../lib/desktop/widgets/remote_menubar.dart | 96 ++++++++++--------- 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/fr.rs | 2 + src/lang/hu.rs | 2 + src/lang/id.rs | 2 + src/lang/it.rs | 2 + src/lang/ja.rs | 2 + src/lang/ko.rs | 2 + src/lang/pl.rs | 2 + src/lang/{pt_PT => pt_PT.rs} | 2 + src/lang/ptbr.rs | 2 + src/lang/ru.rs | 2 + src/lang/sk.rs | 2 + src/lang/template.rs | 2 + src/lang/tr.rs | 2 + src/lang/tw.rs | 2 + src/lang/vn.rs | 2 + 23 files changed, 105 insertions(+), 59 deletions(-) rename src/lang/{pt_PT => pt_PT.rs} (99%) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 8ca9c0cfb..fbba5bafe 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -197,19 +197,19 @@ class _RemotePageState extends State return Scaffold( backgroundColor: MyTheme.color(context).bg, // resizeToAvoidBottomInset: true, - floatingActionButton: _showBar - ? null - : FloatingActionButton( - mini: true, - child: Icon(Icons.expand_less), - backgroundColor: MyTheme.accent, - onPressed: () { - setState(() { - _showBar = !_showBar; - }); - }), - bottomNavigationBar: - _showBar && hasDisplays ? getBottomAppBar(ffiModel) : null, + // floatingActionButton: _showBar + // ? null + // : FloatingActionButton( + // mini: true, + // child: Icon(Icons.expand_less), + // backgroundColor: MyTheme.accent, + // onPressed: () { + // setState(() { + // _showBar = !_showBar; + // }); + // }), + // bottomNavigationBar: + // _showBar && hasDisplays ? getBottomAppBar(ffiModel) : null, body: Overlay( initialEntries: [ OverlayEntry(builder: (context) { diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 9568d0404..011525ba9 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -10,7 +10,7 @@ import '../../mobile/widgets/overlay.dart'; import '../../models/model.dart'; import '../../models/platform_model.dart'; import './popup_menu.dart'; -import './material_mod_popup_menu.dart' as modMenu; +import './material_mod_popup_menu.dart' as mod_menu; class _MenubarTheme { static const Color commonColor = MyTheme.accent; @@ -50,19 +50,22 @@ class _RemoteMenubarState extends State { } Widget _buildShowHide(BuildContext context) { - return SizedBox( - width: 100, - height: 5, - child: TextButton( - onHover: (bool v) { - _hideColor.value = v ? Colors.white60 : Colors.white24; - }, - onPressed: () { - _show.value = !_show.value; - }, - child: Obx(() => Container( - color: _hideColor.value, - )))); + return Obx(() => Tooltip( + message: translate(_show.value ? "Hide Menubar" : "Show Menubar"), + child: SizedBox( + width: 100, + height: 5, + child: TextButton( + onHover: (bool v) { + _hideColor.value = v ? Colors.white60 : Colors.white24; + }, + onPressed: () { + _show.value = !_show.value; + }, + child: Obx(() => Container( + color: _hideColor.value, + )))), + )); } Widget _buildMenubar(BuildContext context) { @@ -73,7 +76,7 @@ class _RemoteMenubarState extends State { menubarItems.add(IconButton( tooltip: translate('Mobile Actions'), color: _MenubarTheme.commonColor, - icon: Icon(Icons.build), + icon: const Icon(Icons.build), onPressed: () { if (mobileActionsOverlayEntry == null) { showMobileActionsOverlay(); @@ -92,7 +95,7 @@ class _RemoteMenubarState extends State { } menubarItems.add(_buildClose(context)); return PopupMenuTheme( - data: PopupMenuThemeData( + data: const PopupMenuThemeData( textStyle: TextStyle(color: _MenubarTheme.commonColor)), child: Column(mainAxisSize: MainAxisSize.min, children: [ Container( @@ -112,11 +115,11 @@ class _RemoteMenubarState extends State { setFullscreen(!isFullscreen); }, icon: Obx(() => isFullscreen - ? Icon( + ? const Icon( Icons.fullscreen_exit, color: _MenubarTheme.commonColor, ) - : Icon( + : const Icon( Icons.fullscreen, color: _MenubarTheme.commonColor, )), @@ -130,7 +133,7 @@ class _RemoteMenubarState extends State { widget.ffi.chatModel.changeCurrentID(ChatModel.clientModeID); widget.ffi.chatModel.toggleChatOverlay(); }, - icon: Icon( + icon: const Icon( Icons.message, color: _MenubarTheme.commonColor, ), @@ -139,24 +142,25 @@ class _RemoteMenubarState extends State { Widget _buildMonitor(BuildContext context) { final pi = widget.ffi.ffiModel.pi; - return modMenu.PopupMenuButton( + return mod_menu.PopupMenuButton( tooltip: translate('Select Monitor'), padding: EdgeInsets.zero, - position: modMenu.PopupMenuPosition.under, + position: mod_menu.PopupMenuPosition.under, icon: Stack( alignment: Alignment.center, children: [ - Icon( + const Icon( Icons.personal_video, color: _MenubarTheme.commonColor, ), Padding( - padding: EdgeInsets.only(bottom: 3.9), + padding: const EdgeInsets.only(bottom: 3.9), child: Obx(() { RxInt display = CurrentDisplayState.find(widget.id); return Text( "${display.value + 1}/${pi.displays.length}", - style: TextStyle(color: _MenubarTheme.commonColor, fontSize: 8), + style: const TextStyle( + color: _MenubarTheme.commonColor, fontSize: 8), ); }), ) @@ -164,14 +168,14 @@ class _RemoteMenubarState extends State { ), itemBuilder: (BuildContext context) { final List rowChildren = []; - final double selectorScale = 1.3; + const double selectorScale = 1.3; for (int i = 0; i < pi.displays.length; i++) { rowChildren.add(Transform.scale( scale: selectorScale, child: Stack( alignment: Alignment.center, children: [ - Icon( + const Icon( Icons.personal_video, color: _MenubarTheme.commonColor, ), @@ -179,12 +183,13 @@ class _RemoteMenubarState extends State { child: Container( alignment: AlignmentDirectional.center, constraints: - BoxConstraints(minHeight: _MenubarTheme.height), + const BoxConstraints(minHeight: _MenubarTheme.height), child: Padding( - padding: EdgeInsets.only(bottom: 2.5), + padding: const EdgeInsets.only(bottom: 2.5), child: Text( (i + 1).toString(), - style: TextStyle(color: _MenubarTheme.commonColor), + style: + const TextStyle(color: _MenubarTheme.commonColor), ), )), onPressed: () { @@ -200,8 +205,8 @@ class _RemoteMenubarState extends State { ), )); } - return >[ - modMenu.PopupMenuItem( + return >[ + mod_menu.PopupMenuItem( height: _MenubarTheme.height, padding: EdgeInsets.zero, child: Row( @@ -214,18 +219,18 @@ class _RemoteMenubarState extends State { } Widget _buildControl(BuildContext context) { - return modMenu.PopupMenuButton( + return mod_menu.PopupMenuButton( padding: EdgeInsets.zero, - icon: Icon( + icon: const Icon( Icons.bolt, color: _MenubarTheme.commonColor, ), tooltip: translate('Control Actions'), - position: modMenu.PopupMenuPosition.under, + position: mod_menu.PopupMenuPosition.under, itemBuilder: (BuildContext context) => _getControlMenu() .map((entry) => entry.build( context, - MenuConfig( + const MenuConfig( commonColor: _MenubarTheme.commonColor, secondMenuHeight: _MenubarTheme.height, ))) @@ -234,19 +239,19 @@ class _RemoteMenubarState extends State { } Widget _buildDisplay(BuildContext context) { - return modMenu.PopupMenuButton( + return mod_menu.PopupMenuButton( padding: EdgeInsets.zero, - icon: Icon( + icon: const Icon( Icons.tv, color: _MenubarTheme.commonColor, ), tooltip: translate('Display Settings'), - position: modMenu.PopupMenuPosition.under, + position: mod_menu.PopupMenuPosition.under, onSelected: (String item) {}, itemBuilder: (BuildContext context) => _getDisplayMenu() .map((entry) => entry.build( context, - MenuConfig( + const MenuConfig( commonColor: _MenubarTheme.commonColor, secondMenuHeight: _MenubarTheme.height, ))) @@ -260,7 +265,7 @@ class _RemoteMenubarState extends State { onPressed: () { clientClose(widget.ffi.dialogManager); }, - icon: Icon( + icon: const Icon( Icons.close, color: _MenubarTheme.commonColor, ), @@ -332,7 +337,7 @@ class _RemoteMenubarState extends State { if (pi.platform == 'Linux' || pi.sasEnabled) { displayMenu.add(MenuEntryButton( childBuilder: (TextStyle? style) => Text( - translate('Insert') + ' Ctrl + Alt + Del', + '${translate("Insert")} Ctrl + Alt + Del', style: style, ), proc: () { @@ -357,8 +362,7 @@ class _RemoteMenubarState extends State { displayMenu.add(MenuEntryButton( childBuilder: (TextStyle? style) => Obx(() => Text( translate( - (BlockInputState.find(widget.id).value ? 'Unb' : 'B') + - 'lock user input'), + '${BlockInputState.find(widget.id).value ? "Unb" : "B"}lock user input'), style: style, )), proc: () { @@ -366,7 +370,7 @@ class _RemoteMenubarState extends State { RxBool blockInput = BlockInputState.find(widget.id); bind.sessionToggleOption( id: widget.id, - value: (blockInput.value ? 'un' : '') + 'block-input'); + value: '${blockInput.value ? "un" : ""}block-input'); blockInput.value = !blockInput.value; }, )); @@ -447,7 +451,7 @@ class _RemoteMenubarState extends State { MenuEntrySwitch( text: translate('Show remote cursor'), getter: () async { - return await bind.sessionGetToggleOptionSync( + return bind.sessionGetToggleOptionSync( id: widget.id, arg: 'show-remote-cursor'); }, setter: (bool v) async { @@ -457,7 +461,7 @@ class _RemoteMenubarState extends State { MenuEntrySwitch( text: translate('Show quality monitor'), getter: () async { - return await bind.sessionGetToggleOptionSync( + return bind.sessionGetToggleOptionSync( id: widget.id, arg: 'show-quality-monitor'); }, setter: (bool v) async { diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 4c66acff4..3e50396e6 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -311,5 +311,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Ratio", "比例"), ("Image Quality", "画质"), ("Scroll Style", "滚屏方式"), + ("Show Menubar", "显示菜单栏"), + ("Hide Menubar", "隐藏菜单栏"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index fbe0287aa..f94df1ceb 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -311,5 +311,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Ratio", "Poměr"), ("Image Quality", "Kvalita obrazu"), ("Scroll Style", "Štýl posúvania"), + ("Show Menubar", "Zobrazit panel nabídek"), + ("Hide Menubar", "skrýt panel nabídek"), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 5b88db08d..c0f7abf91 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -311,5 +311,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Ratio", "Forhold"), ("Image Quality", "Billede kvalitet"), ("Scroll Style", "Rulstil"), + ("Show Menubar", "Vis menulinje"), + ("Hide Menubar", "skjul menulinjen"), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index f41d8c313..e411a751d 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -311,5 +311,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Ratio", "Verhältnis"), ("Image Quality", "Bildqualität"), ("Scroll Style", "Scroll-Stil"), + ("Show Menubar", "Menüleiste anzeigen"), + ("Hide Menubar", "Menüleiste ausblenden"), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index b1207ff47..211e6728d 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -311,5 +311,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Ratio", "Proporcio"), ("Image Quality", "Bilda Kvalito"), ("Scroll Style", "Ruluma Stilo"), + ("Show Menubar", "Montru menubreton"), + ("Hide Menubar", "kaŝi menubreton"), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index a018472f7..068442bf4 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -311,5 +311,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Ratio", "Relación"), ("Image Quality", "La calidad de imagen"), ("Scroll Style", "Estilo de desplazamiento"), + ("Show Menubar", "ajustes de pantalla"), + ("Hide Menubar", "ocultar barra de menú"), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 73624290b..d568f050b 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -311,5 +311,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Ratio", "Rapport"), ("Image Quality", "Qualité d'image"), ("Scroll Style", "Style de défilement"), + ("Show Menubar", "Afficher la barre de menus"), + ("Hide Menubar", "masquer la barre de menus"), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 15175175f..1fe693248 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -311,5 +311,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Ratio", "Hányados"), ("Image Quality", "Képminőség"), ("Scroll Style", "Görgetési stílus"), + ("Show Menubar", "Menüsor megjelenítése"), + ("Hide Menubar", "menüsor elrejtése"), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 4415488f4..d5d6ed920 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -311,5 +311,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Ratio", "Perbandingan"), ("Image Quality", "Kualitas gambar"), ("Scroll Style", "Gaya Gulir"), + ("Show Menubar", "Tampilkan bilah menu"), + ("Hide Menubar", "sembunyikan bilah menu"), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index c8cad290f..26e7d4073 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -310,5 +310,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Ratio", "Rapporto"), ("Image Quality", "Qualità dell'immagine"), ("Scroll Style", "Stile di scorrimento"), + ("Show Menubar", "Mostra la barra dei menu"), + ("Hide Menubar", "nascondi la barra dei menu"), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 2cb0ce6c5..f1331b01d 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -308,5 +308,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Ratio", "比率"), ("Image Quality", "画質"), ("Scroll Style", "スクロール スタイル"), + ("Show Menubar", "メニューバーを表示"), + ("Hide Menubar", "メニューバーを隠す"), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 24e1db7bf..7a0d8dbdf 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -308,5 +308,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Ratio", "비율"), ("Image Quality", "이미지 품질"), ("Scroll Style", "스크롤 스타일"), + ("Show Menubar", "메뉴 표시줄 표시"), + ("Hide Menubar", "메뉴 표시줄 숨기기"), ].iter().cloned().collect(); } \ No newline at end of file diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 16ff7c4e7..6f6326121 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -312,5 +312,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Ratio", "Stosunek"), ("Image Quality", "Jakość obrazu"), ("Scroll Style", "Styl przewijania"), + ("Show Menubar", "Pokaż pasek menu"), + ("Hide Menubar", "ukryj pasek menu"), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT b/src/lang/pt_PT.rs similarity index 99% rename from src/lang/pt_PT rename to src/lang/pt_PT.rs index adac5af15..2df6c63dc 100644 --- a/src/lang/pt_PT +++ b/src/lang/pt_PT.rs @@ -308,5 +308,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Ratio", "Razão"), ("Image Quality", "Qualidade da imagem"), ("Scroll Style", "Estilo de rolagem"), + ("Show Menubar", "Mostrar barra de menus"), + ("Hide Menubar", "ocultar barra de menu"), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index cd40fb2a5..a0981f867 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -311,5 +311,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Ratio", ""), ("Image Quality", ""), ("Scroll Style", ""), + ("Show Menubar", ""), + ("Hide Menubar", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index df07e5e8e..eed658dfd 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -311,5 +311,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Ratio", "Соотношение"), ("Image Quality", "Качество изображения"), ("Scroll Style", "Стиль прокрутки"), + ("Show Menubar", "Показать строку меню"), + ("Hide Menubar", "скрыть строку меню"), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 935544eb2..b4e61e83f 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -311,5 +311,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Ratio", "Pomer"), ("Image Quality", "Kvalita obrazu"), ("Scroll Style", "Štýl posúvania"), + ("Show Menubar", "Zobraziť panel s ponukami"), + ("Hide Menubar", "skryť panel s ponukami"), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index e92a032fb..2e5c67cd8 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -311,5 +311,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Ratio", ""), ("Image Quality", ""), ("Scroll Style", ""), + ("Show Menubar", ""), + ("Hide Menubar", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 215c14058..829659954 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -311,5 +311,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Ratio", "Oran"), ("Image Quality", "Görüntü kalitesi"), ("Scroll Style", "Kaydırma Stili"), + ("Show Menubar", "Menü çubuğunu göster"), + ("Hide Menubar", "menü çubuğunu gizle"), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 39d636592..f7d7cbe1d 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -311,5 +311,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Ratio", "比例"), ("Image Quality", "畫質"), ("Scroll Style", "滾動樣式"), + ("Show Menubar", "顯示菜單欄"), + ("Hide Menubar", "隱藏菜單欄"), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 2f9dfcafd..1c77139b3 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -311,5 +311,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Ratio", "Tỉ lệ"), ("Image Quality", "Chất lượng hình ảnh"), ("Scroll Style", "Kiểu cuộn"), + ("Show Menubar", "Hiển thị thanh menu"), + ("Hide Menubar", "ẩn thanh menu"), ].iter().cloned().collect(); } From b004f4b9eed8539d6db3aa2c759f527a59c83e2a Mon Sep 17 00:00:00 2001 From: 21pages Date: Sun, 28 Aug 2022 21:43:18 +0800 Subject: [PATCH 0309/2015] fix TextField cursor problem Signed-off-by: 21pages --- .../lib/desktop/pages/desktop_home_page.dart | 16 ++--- .../desktop/pages/desktop_setting_page.dart | 65 +++++++------------ .../lib/desktop/widgets/peercard_widget.dart | 42 ++++-------- 3 files changed, 42 insertions(+), 81 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 12f17c95e..632177e29 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -806,6 +806,8 @@ Future loginDialog() async { var userNameMsg = ""; String pass = ""; var passMsg = ""; + var userContontroller = TextEditingController(text: userName); + var pwdController = TextEditingController(text: pass); var isInProgress = false; var completer = Completer(); @@ -833,13 +835,10 @@ Future loginDialog() async { ), Expanded( child: TextField( - onChanged: (s) { - userName = s; - }, decoration: InputDecoration( border: OutlineInputBorder(), errorText: userNameMsg.isNotEmpty ? userNameMsg : null), - controller: TextEditingController(text: userName), + controller: userContontroller, ), ), ], @@ -859,13 +858,10 @@ Future loginDialog() async { Expanded( child: TextField( obscureText: true, - onChanged: (s) { - pass = s; - }, decoration: InputDecoration( border: OutlineInputBorder(), errorText: passMsg.isNotEmpty ? passMsg : null), - controller: TextEditingController(text: pass), + controller: pwdController, ), ), ], @@ -896,8 +892,8 @@ Future loginDialog() async { isInProgress = false; }); }; - userName = userName; - pass = pass; + userName = userContontroller.text; + pass = pwdController.text; if (userName.isEmpty) { userNameMsg = translate("Username missed"); cancel(); diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 4f86974f1..120f8bc7a 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -1025,7 +1025,6 @@ class _ComboBox extends StatelessWidget { void changeServer() async { Map oldOptions = jsonDecode(await bind.mainGetOptions()); - print("${oldOptions}"); String idServer = oldOptions['custom-rendezvous-server'] ?? ""; var idServerMsg = ""; String relayServer = oldOptions['relay-server'] ?? ""; @@ -1033,6 +1032,10 @@ void changeServer() async { String apiServer = oldOptions['api-server'] ?? ""; var apiServerMsg = ""; var key = oldOptions['key'] ?? ""; + var idController = TextEditingController(text: idServer); + var relayController = TextEditingController(text: relayServer); + var apiController = TextEditingController(text: apiServer); + var keyController = TextEditingController(text: key); var isInProgress = false; gFFI.dialogManager.show((setState, close) { @@ -1057,13 +1060,10 @@ void changeServer() async { ), Expanded( child: TextField( - onChanged: (s) { - idServer = s; - }, decoration: InputDecoration( border: OutlineInputBorder(), errorText: idServerMsg.isNotEmpty ? idServerMsg : null), - controller: TextEditingController(text: idServer), + controller: idController, ), ), ], @@ -1082,14 +1082,11 @@ void changeServer() async { ), Expanded( child: TextField( - onChanged: (s) { - relayServer = s; - }, decoration: InputDecoration( border: OutlineInputBorder(), errorText: relayServerMsg.isNotEmpty ? relayServerMsg : null), - controller: TextEditingController(text: relayServer), + controller: relayController, ), ), ], @@ -1108,14 +1105,11 @@ void changeServer() async { ), Expanded( child: TextField( - onChanged: (s) { - apiServer = s; - }, decoration: InputDecoration( border: OutlineInputBorder(), errorText: apiServerMsg.isNotEmpty ? apiServerMsg : null), - controller: TextEditingController(text: apiServer), + controller: apiController, ), ), ], @@ -1134,13 +1128,10 @@ void changeServer() async { ), Expanded( child: TextField( - onChanged: (s) { - key = s; - }, decoration: InputDecoration( border: OutlineInputBorder(), ), - controller: TextEditingController(text: key), + controller: keyController, ), ), ], @@ -1171,10 +1162,10 @@ void changeServer() async { isInProgress = false; }); }; - idServer = idServer.trim(); - relayServer = relayServer.trim(); - apiServer = apiServer.trim(); - key = key.trim(); + idServer = idController.text.trim(); + relayServer = relayController.text.trim(); + apiServer = apiController.text.trim().toLowerCase(); + key = keyController.text.trim(); if (idServer.isNotEmpty) { idServerMsg = translate( @@ -1230,6 +1221,7 @@ void changeWhiteList() async { Map oldOptions = jsonDecode(await bind.mainGetOptions()); var newWhiteList = ((oldOptions['whitelist'] ?? "") as String).split(','); var newWhiteListField = newWhiteList.join('\n'); + var controller = TextEditingController(text: newWhiteListField); var msg = ""; var isInProgress = false; gFFI.dialogManager.show((setState, close) { @@ -1246,15 +1238,12 @@ void changeWhiteList() async { children: [ Expanded( child: TextField( - onChanged: (s) { - newWhiteListField = s; - }, maxLines: null, decoration: InputDecoration( border: OutlineInputBorder(), errorText: msg.isEmpty ? null : translate(msg), ), - controller: TextEditingController(text: newWhiteListField), + controller: controller, ), ), ], @@ -1277,7 +1266,7 @@ void changeWhiteList() async { msg = ""; isInProgress = true; }); - newWhiteListField = newWhiteListField.trim(); + newWhiteListField = controller.text.trim(); var newWhiteList = ""; if (newWhiteListField.isEmpty) { // pass @@ -1319,6 +1308,9 @@ void changeSocks5Proxy() async { username = socks[1]; password = socks[2]; } + var proxyController = TextEditingController(text: proxy); + var userController = TextEditingController(text: username); + var pwdController = TextEditingController(text: password); var isInProgress = false; gFFI.dialogManager.show((setState, close) { @@ -1343,13 +1335,10 @@ void changeSocks5Proxy() async { ), Expanded( child: TextField( - onChanged: (s) { - proxy = s; - }, decoration: InputDecoration( border: OutlineInputBorder(), errorText: proxyMsg.isNotEmpty ? proxyMsg : null), - controller: TextEditingController(text: proxy), + controller: proxyController, ), ), ], @@ -1368,13 +1357,10 @@ void changeSocks5Proxy() async { ), Expanded( child: TextField( - onChanged: (s) { - username = s; - }, decoration: InputDecoration( border: OutlineInputBorder(), ), - controller: TextEditingController(text: username), + controller: userController, ), ), ], @@ -1393,13 +1379,10 @@ void changeSocks5Proxy() async { ), Expanded( child: TextField( - onChanged: (s) { - password = s; - }, decoration: InputDecoration( border: OutlineInputBorder(), ), - controller: TextEditingController(text: password), + controller: pwdController, ), ), ], @@ -1428,9 +1411,9 @@ void changeSocks5Proxy() async { isInProgress = false; }); }; - proxy = proxy.trim(); - username = username.trim(); - password = password.trim(); + proxy = proxyController.text.trim(); + username = userController.text.trim(); + password = pwdController.text.trim(); if (proxy.isNotEmpty) { proxyMsg = diff --git a/flutter/lib/desktop/widgets/peercard_widget.dart b/flutter/lib/desktop/widgets/peercard_widget.dart index 810b84a63..4db43398a 100644 --- a/flutter/lib/desktop/widgets/peercard_widget.dart +++ b/flutter/lib/desktop/widgets/peercard_widget.dart @@ -462,6 +462,7 @@ class _PeerCardState extends State<_PeerCard> void _rename(String id) async { var isInProgress = false; var name = await bind.mainGetPeerOption(id: id, key: 'alias'); + var controller = TextEditingController(text: name); if (widget.type == PeerType.ab) { final peer = gFFI.abModel.peers.firstWhere((p) => id == p['id']); if (peer == null) { @@ -470,7 +471,6 @@ class _PeerCardState extends State<_PeerCard> name = peer['alias'] ?? ""; } } - final k = GlobalKey(); gFFI.dialogManager.show((setState, close) { return CustomAlertDialog( title: Text(translate("Rename")), @@ -480,22 +480,9 @@ class _PeerCardState extends State<_PeerCard> Container( padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), child: Form( - key: k, child: TextFormField( - controller: TextEditingController(text: name), + controller: controller, decoration: InputDecoration(border: OutlineInputBorder()), - onChanged: (newStr) { - name = newStr; - }, - validator: (s) { - if (s == null || s.isEmpty) { - return translate("Empty"); - } - return null; - }, - onSaved: (s) { - name = s ?? "unnamed"; - }, ), ), ), @@ -513,22 +500,17 @@ class _PeerCardState extends State<_PeerCard> setState(() { isInProgress = true; }); - if (k.currentState != null) { - if (k.currentState!.validate()) { - k.currentState!.save(); - await bind.mainSetPeerOption( - id: id, key: 'alias', value: name); - if (widget.type == PeerType.ab) { - gFFI.abModel.setPeerOption(id, 'alias', name); - await gFFI.abModel.updateAb(); - } else { - Future.delayed(Duration.zero, () { - this.setState(() {}); - }); - } - close(); - } + name = controller.text; + await bind.mainSetPeerOption(id: id, key: 'alias', value: name); + if (widget.type == PeerType.ab) { + gFFI.abModel.setPeerOption(id, 'alias', name); + await gFFI.abModel.updateAb(); + } else { + Future.delayed(Duration.zero, () { + this.setState(() {}); + }); } + close(); setState(() { isInProgress = false; }); From e0579a9b57b3235874bf15f099c04144c40c6fa8 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sun, 28 Aug 2022 22:28:19 +0800 Subject: [PATCH 0310/2015] add keeping android font scale factor Signed-off-by: 21pages --- flutter/lib/main.dart | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 6e30a15d2..401b7febc 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -211,8 +211,13 @@ class App extends StatelessWidget { // FirebaseAnalyticsObserver(analytics: analytics), ], builder: isAndroid - ? (_, child) => AccessibilityListener( - child: child, + ? (context, child) => AccessibilityListener( + child: MediaQuery( + data: MediaQuery.of(context).copyWith( + textScaleFactor: 1.0, + ), + child: child ?? Container(), + ), ) : _keepScaleBuilder(), ), From a90973621ad9170b64e41cdadbd5b66340920c58 Mon Sep 17 00:00:00 2001 From: csf Date: Mon, 29 Aug 2022 13:08:42 +0800 Subject: [PATCH 0311/2015] rust port-forward --- .../lib/desktop/pages/port_forward_page.dart | 10 +- src/flutter.rs | 158 +++++++++++++++++- src/flutter_ffi.rs | 28 +--- 3 files changed, 167 insertions(+), 29 deletions(-) diff --git a/flutter/lib/desktop/pages/port_forward_page.dart b/flutter/lib/desktop/pages/port_forward_page.dart index b83761181..6cfd0cdb2 100644 --- a/flutter/lib/desktop/pages/port_forward_page.dart +++ b/flutter/lib/desktop/pages/port_forward_page.dart @@ -48,7 +48,7 @@ class _PortForwardPageState extends State void initState() { super.initState(); _ffi = FFI(); - // _ffi.connect(widget.id, isPortForward: true); + _ffi.connect(widget.id, isPortForward: true); Get.put(_ffi, tag: 'pf_${widget.id}'); if (!Platform.isLinux) { Wakelock.enable(); @@ -190,8 +190,8 @@ class _PortForwardPageState extends State remotePort != null && (remoteHostController.text.isEmpty || remoteHostController.text.trim().isNotEmpty)) { - await bind.mainAddPortForward( - id: widget.id, + await bind.sessionAddPortForward( + id: 'pf_${widget.id}', localPort: localPort, remoteHost: remoteHostController.text.trim().isEmpty ? 'localhost' @@ -261,8 +261,8 @@ class _PortForwardPageState extends State child: IconButton( icon: const Icon(Icons.close), onPressed: () async { - await bind.mainRemovePortForward( - id: widget.id, localPort: pf.localPort); + await bind.sessionRemovePortForward( + id: 'pf_${widget.id}', localPort: pf.localPort); refreshTunnelConfig(); }, ), diff --git a/src/flutter.rs b/src/flutter.rs index 1a0499565..880177c78 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -539,6 +539,39 @@ impl Session { ], ); } + + pub fn remove_port_forward(&mut self, port: i32) { + let mut config = self.load_config(); + config.port_forwards = config + .port_forwards + .drain(..) + .filter(|x| x.0 != port) + .collect(); + self.save_config(&config); + self.send(Data::RemovePortForward(port)); + } + + pub fn add_port_forward(&mut self, port: i32, remote_host: String, remote_port: i32) { + let mut config = self.load_config(); + if config + .port_forwards + .iter() + .filter(|x| x.0 == port) + .next() + .is_some() + { + return; + } + let pf = (port, remote_host, remote_port); + config.port_forwards.push(pf.clone()); + self.save_config(&config); + self.send(Data::AddPortForward(pf)); + } + + + fn on_error(&self, err: &str) { + self.msgbox("error", "Error", err); + } } impl FileManager for Session {} @@ -734,7 +767,7 @@ impl Connection { if !is_file_transfer && !is_port_forward { stop_clipboard = Self::start_clipboard(sender.clone(), session.lc.clone()); } - *session.sender.write().unwrap() = Some(sender); + *session.sender.write().unwrap() = Some(sender.clone()); let conn_type = if is_file_transfer { session.lc.write().unwrap().is_file_transfer = true; ConnType::FILE_TRANSFER @@ -743,6 +776,99 @@ impl Connection { } else { ConnType::DEFAULT_CONN }; + let key = Config::get_option("key"); + let token = Config::get_option("access_token"); + + // TODO rdp & cli args + let is_rdp = false; + let args: Vec = Vec::new(); + + if is_port_forward { + if is_rdp { + // let port = handler + // .get_option("rdp_port".to_owned()) + // .parse::() + // .unwrap_or(3389); + // std::env::set_var( + // "rdp_username", + // handler.get_option("rdp_username".to_owned()), + // ); + // std::env::set_var( + // "rdp_password", + // handler.get_option("rdp_password".to_owned()), + // ); + // log::info!("Remote rdp port: {}", port); + // start_one_port_forward(handler, 0, "".to_owned(), port, receiver, &key, &token).await; + } else if args.len() == 0 { + let pfs = session.lc.read().unwrap().port_forwards.clone(); + let mut queues = HashMap::>::new(); + for d in pfs { + sender.send(Data::AddPortForward(d)).ok(); + } + loop { + match receiver.recv().await { + Some(Data::AddPortForward((port, remote_host, remote_port))) => { + if port <= 0 || remote_port <= 0 { + continue; + } + let (sender, receiver) = mpsc::unbounded_channel::(); + queues.insert(port, sender); + let handler = session.clone(); + let key = key.clone(); + let token = token.clone(); + tokio::spawn(async move { + start_one_port_forward( + handler, + port, + remote_host, + remote_port, + receiver, + &key, + &token, + ) + .await; + }); + } + Some(Data::RemovePortForward(port)) => { + if let Some(s) = queues.remove(&port) { + s.send(Data::Close).ok(); + } + } + Some(Data::Close) => { + break; + } + Some(d) => { + for (_, s) in queues.iter() { + s.send(d.clone()).ok(); + } + } + _ => {} + } + } + } else { + // let port = handler.args[0].parse::().unwrap_or(0); + // if handler.args.len() != 3 + // || handler.args[2].parse::().unwrap_or(0) <= 0 + // || port <= 0 + // { + // handler.on_error("Invalid arguments, usage:

    rustdesk --port-forward remote-id listen-port remote-host remote-port"); + // } + // let remote_host = handler.args[1].clone(); + // let remote_port = handler.args[2].parse::().unwrap_or(0); + // start_one_port_forward( + // handler, + // port, + // remote_host, + // remote_port, + // receiver, + // &key, + // &token, + // ) + // .await; + } + return; + } + let latency_controller = LatencyController::new(); let latency_controller_cl = latency_controller.clone(); @@ -759,8 +885,7 @@ impl Connection { frame_count: Arc::new(AtomicUsize::new(0)), video_format: CodecFormat::Unknown, }; - let key = Config::get_option("key"); - let token = Config::get_option("access_token"); + match Client::start(&session.id, &key, &token, conn_type, session.clone()).await { Ok((mut peer, direct)) => { @@ -2288,3 +2413,30 @@ pub fn get_session_id(id: String) -> String { id }; } + + +async fn start_one_port_forward( + handler: Session, + port: i32, + remote_host: String, + remote_port: i32, + receiver: mpsc::UnboundedReceiver, + key: &str, + token: &str, +) { + handler.lc.write().unwrap().port_forward = (remote_host, remote_port); + if let Err(err) = crate::port_forward::listen( + handler.id.clone(), + String::new(), // TODO + port, + handler.clone(), + receiver, + key, + token, + ) + .await + { + handler.on_error(&format!("Failed to listen on {}: {}", port, err)); + } + log::info!("port forward (:{}) exit", port); +} \ No newline at end of file diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index d9bc31d96..dd147bb77 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -602,30 +602,16 @@ pub fn main_load_lan_peers() { }; } -pub fn main_add_port_forward(id: String, local_port: i32, remote_host: String, remote_port: i32) { - let mut config = get_peer(id.clone()); - if config - .port_forwards - .iter() - .filter(|x| x.0 == local_port) - .next() - .is_some() - { - return; +pub fn session_add_port_forward(id: String, local_port: i32, remote_host: String, remote_port: i32) { + if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) { + session.add_port_forward(local_port, remote_host, remote_port); } - let pf = (local_port, remote_host, remote_port); - config.port_forwards.push(pf); - config.store(&id); } -pub fn main_remove_port_forward(id: String, local_port: i32) { - let mut config = get_peer(id.clone()); - config.port_forwards = config - .port_forwards - .drain(..) - .filter(|x| x.0 != local_port) - .collect(); - config.store(&id); +pub fn session_remove_port_forward(id: String, local_port: i32) { + if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) { + session.remove_port_forward(local_port); + } } pub fn main_get_last_remote_id() -> String { From 5d69a99427ac8366a0b28fe8eb339f3f57b9eba2 Mon Sep 17 00:00:00 2001 From: Asura Date: Mon, 29 Aug 2022 15:25:53 +0800 Subject: [PATCH 0312/2015] Fix compile error on windows --- src/flutter.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/flutter.rs b/src/flutter.rs index 09ed01aeb..9c5dd319d 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -421,14 +421,12 @@ impl Session { } let keycode: u32 = keycode as u32; let scancode: u32 = scancode as u32; + + #[cfg(not(target_os = "windows"))] let key = rdev::key_from_scancode(scancode) as RdevKey; // Windows requires special handling #[cfg(target_os = "windows")] - let key = if let Some(e) = _evt { - rdev::get_win_key(e.code.into(), e.scan_code) - } else { - key - }; + let key = rdev::get_win_key(keycode, scancode); let peer = self.peer_platform(); From 4423a18e797c902022879cfb367dabab0ae3142f Mon Sep 17 00:00:00 2001 From: Asura Date: Mon, 29 Aug 2022 17:14:05 +0800 Subject: [PATCH 0313/2015] Opt svg of keyboard --- src/ui/common.tis | 91 +---------------------------------------------- 1 file changed, 1 insertion(+), 90 deletions(-) diff --git a/src/ui/common.tis b/src/ui/common.tis index 8a0beef98..69e5565f0 100644 --- a/src/ui/common.tis +++ b/src/ui/common.tis @@ -155,96 +155,7 @@ var svg_send = var svg_chat = ; -var svg_keyboard = - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -; +var svg_keyboard = ; function scrollToBottom(el) { var y = el.box(#height, #content) - el.box(#height, #client); From 37617fa88893abd9e9b7c25613746af8b2171586 Mon Sep 17 00:00:00 2001 From: csf Date: Mon, 29 Aug 2022 18:37:03 +0800 Subject: [PATCH 0314/2015] fix port forward session id & file session dispose --- flutter/lib/desktop/pages/file_manager_tab_page.dart | 2 +- flutter/lib/desktop/pages/port_forward_tab_page.dart | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index 09577128f..da76890d4 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -57,7 +57,7 @@ class _FileManagerTabPageState extends State { } else if (call.method == "onDestroy") { tabController.state.value.tabs.forEach((tab) { print("executing onDestroy hook, closing ${tab.label}}"); - final tag = tab.label; + final tag = 'ft_${tab.label}'; ffi(tag).close().then((_) { Get.delete(tag: tag); }); diff --git a/flutter/lib/desktop/pages/port_forward_tab_page.dart b/flutter/lib/desktop/pages/port_forward_tab_page.dart index 28825b75a..6323a0af9 100644 --- a/flutter/lib/desktop/pages/port_forward_tab_page.dart +++ b/flutter/lib/desktop/pages/port_forward_tab_page.dart @@ -26,7 +26,7 @@ class _PortForwardTabPageState extends State { _PortForwardTabPageState(Map params) { tabController.add(TabInfo( - key: params['id'] + params['isRDP'].toString(), + key: params['id'], label: params['id'], selectedIcon: selectedIcon, unselectedIcon: unselectedIcon, @@ -61,7 +61,7 @@ class _PortForwardTabPageState extends State { } else if (call.method == "onDestroy") { tabController.state.value.tabs.forEach((tab) { print("executing onDestroy hook, closing ${tab.label}}"); - final tag = tab.label; + final tag = 'pf_${tab.label}'; ffi(tag).close().then((_) { Get.delete(tag: tag); }); From fcc62febb1dfc6da4081571a4c89ff86c0c893f0 Mon Sep 17 00:00:00 2001 From: csf Date: Mon, 29 Aug 2022 19:45:06 +0800 Subject: [PATCH 0315/2015] update port-forward 1. fix multi remote port override. 2. add connection.rs port-forward failed to close --- src/flutter.rs | 9 ++++----- src/port_forward.rs | 6 ++++++ src/server/connection.rs | 1 + src/ui/remote.rs | 4 +++- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/flutter.rs b/src/flutter.rs index 880177c78..7192c0fdf 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -568,7 +568,6 @@ impl Session { self.send(Data::AddPortForward(pf)); } - fn on_error(&self, err: &str) { self.msgbox("error", "Error", err); } @@ -886,7 +885,6 @@ impl Connection { video_format: CodecFormat::Unknown, }; - match Client::start(&session.id, &key, &token, conn_type, session.clone()).await { Ok((mut peer, direct)) => { SERVER_KEYBOARD_ENABLED.store(true, Ordering::SeqCst); @@ -2414,7 +2412,6 @@ pub fn get_session_id(id: String) -> String { }; } - async fn start_one_port_forward( handler: Session, port: i32, @@ -2424,7 +2421,6 @@ async fn start_one_port_forward( key: &str, token: &str, ) { - handler.lc.write().unwrap().port_forward = (remote_host, remote_port); if let Err(err) = crate::port_forward::listen( handler.id.clone(), String::new(), // TODO @@ -2433,10 +2429,13 @@ async fn start_one_port_forward( receiver, key, token, + handler.lc.clone(), + remote_host, + remote_port, ) .await { handler.on_error(&format!("Failed to listen on {}: {}", port, err)); } log::info!("port forward (:{}) exit", port); -} \ No newline at end of file +} diff --git a/src/port_forward.rs b/src/port_forward.rs index 9a697da42..934743edc 100644 --- a/src/port_forward.rs +++ b/src/port_forward.rs @@ -1,3 +1,5 @@ +use std::sync::{Arc, RwLock}; + use crate::client::*; use hbb_common::{ allow_err, bail, @@ -48,6 +50,9 @@ pub async fn listen( ui_receiver: mpsc::UnboundedReceiver, key: &str, token: &str, + lc: Arc>, + remote_host: String, + remote_port: i32, ) -> ResultType<()> { let listener = tcp::new_listener(format!("0.0.0.0:{}", port), true).await?; let addr = listener.local_addr()?; @@ -61,6 +66,7 @@ pub async fn listen( tokio::select! { Ok((forward, addr)) = listener.accept() => { log::info!("new connection from {:?}", addr); + lc.write().unwrap().port_forward = (remote_host.clone(), remote_port); let id = id.clone(); let password = password.clone(); let mut forward = Framed::new(forward, BytesCodec::new()); diff --git a/src/server/connection.rs b/src/server/connection.rs index 346477851..d4e353a16 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -949,6 +949,7 @@ impl Connection { addr )) .await; + return false; } } } diff --git a/src/ui/remote.rs b/src/ui/remote.rs index ee0bc3f1d..a0245c28a 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -1253,7 +1253,6 @@ async fn start_one_port_forward( key: &str, token: &str, ) { - handler.lc.write().unwrap().port_forward = (remote_host, remote_port); if let Err(err) = crate::port_forward::listen( handler.id.clone(), handler.password.clone(), @@ -1262,6 +1261,9 @@ async fn start_one_port_forward( receiver, key, token, + handler.lc.clone(), + remote_host, + remote_port, ) .await { From a0cb39af9c0c3b7718113173ac98ca2acccd7353 Mon Sep 17 00:00:00 2001 From: Asura Date: Tue, 30 Aug 2022 15:35:39 +0800 Subject: [PATCH 0316/2015] Fix numlock and capslock on Mac --- src/server/input_service.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/server/input_service.rs b/src/server/input_service.rs index c01212184..684ada483 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -655,12 +655,21 @@ fn sync_status(evt: &KeyEvent) -> (bool, bool) { fn map_keyboard_mode(evt: &KeyEvent) { // map mode(1): Send keycode according to the peer platform. let (click_capslock, click_numlock) = sync_status(evt); + + #[cfg(not(target_os = "macos"))] if click_capslock { rdev_key_click(RdevKey::CapsLock); } + #[cfg(not(target_os = "macos"))] if click_numlock { rdev_key_click(RdevKey::NumLock); } + #[cfg(target_os = "macos")] + if evt.down && click_capslock { + rdev_key_down_or_up(RdevKey::CapsLock, evt.down); + } + log::info!("click capslog {:?} click_numlock {:?}", click_capslock, click_numlock); + rdev_key_down_or_up(RdevKey::Unknown(evt.chr()), evt.down); return; } From 30bfa59e7d861fb3a361b562d4f036bda7f7536d Mon Sep 17 00:00:00 2001 From: Asura Date: Tue, 30 Aug 2022 15:53:44 +0800 Subject: [PATCH 0317/2015] Without Clear Key on Mac OS --- src/ui/remote.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 659f3f05a..34d4251fa 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -1061,6 +1061,10 @@ impl Handler { let key = self.convert_numpad_keys(key); rdev::win_keycode_from_key(key).unwrap_or_default().into() } else { + // Without Clear Key on Mac OS + if key == rdev::Key::Clear{ + return; + } rdev::macos_keycode_from_key(key).unwrap_or_default().into() }; From 66a2c51ca509dfb81d6ad07d389d08b1a278bedf Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 30 Aug 2022 16:45:47 +0800 Subject: [PATCH 0318/2015] fix: linux memory-safe workaround Signed-off-by: Kingtous --- .../desktop/pages/connection_tab_page.dart | 16 +++------------ .../desktop/pages/file_manager_tab_page.dart | 20 ++++++------------- .../desktop/pages/port_forward_tab_page.dart | 16 ++++----------- .../lib/desktop/widgets/tabbar_widget.dart | 5 +++-- flutter/lib/models/model.dart | 2 +- flutter/lib/utils/multi_window_manager.dart | 2 +- flutter/pubspec.lock | 4 ++-- flutter/pubspec.yaml | 2 +- 8 files changed, 21 insertions(+), 46 deletions(-) diff --git a/flutter/lib/desktop/pages/connection_tab_page.dart b/flutter/lib/desktop/pages/connection_tab_page.dart index c8cde79ad..445590037 100644 --- a/flutter/lib/desktop/pages/connection_tab_page.dart +++ b/flutter/lib/desktop/pages/connection_tab_page.dart @@ -9,8 +9,6 @@ import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:get/get.dart'; -import '../../models/model.dart'; - class ConnectionTabPage extends StatefulWidget { final Map params; @@ -73,14 +71,7 @@ class _ConnectionTabPageState extends State { fullscreen.isTrue ? 0 : kDesktopRemoteTabBarHeight, )))); } else if (call.method == "onDestroy") { - tabController.state.value.tabs.forEach((tab) { - print("executing onDestroy hook, closing ${tab.label}}"); - final tag = tab.label; - ffi(tag).close().then((_) { - Get.delete(tag: tag); - }); - }); - Get.back(); + tabController.state.value.tabs.clear(); } }); } @@ -116,9 +107,8 @@ class _ConnectionTabPageState extends State { } void onRemoveId(String id) { - ffi(id).close(); - if (tabController.state.value.tabs.length == 0) { - WindowController.fromWindowId(windowId()).close(); + if (tabController.state.value.tabs.isEmpty) { + WindowController.fromWindowId(windowId()).hide(); } } diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index da76890d4..e391afd71 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -5,7 +5,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/desktop/pages/file_manager_page.dart'; import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; -import 'package:flutter_hbb/models/model.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:get/get.dart'; @@ -42,7 +41,7 @@ class _FileManagerTabPageState extends State { rustDeskWinManager.setMethodHandler((call, fromWindowId) async { print( - "call ${call.method} with args ${call.arguments} from window ${fromWindowId}"); + "call ${call.method} with args ${call.arguments} from window ${fromWindowId} to ${windowId()}"); // for simplify, just replace connectionId if (call.method == "new_file_transfer") { final args = jsonDecode(call.arguments); @@ -55,21 +54,15 @@ class _FileManagerTabPageState extends State { unselectedIcon: unselectedIcon, page: FileManagerPage(key: ValueKey(id), id: id))); } else if (call.method == "onDestroy") { - tabController.state.value.tabs.forEach((tab) { - print("executing onDestroy hook, closing ${tab.label}}"); - final tag = 'ft_${tab.label}'; - ffi(tag).close().then((_) { - Get.delete(tag: tag); - }); - }); - Get.back(); + tabController.state.value.tabs.clear(); } }); } @override Widget build(BuildContext context) { - final theme = isDarkTheme() ? TarBarTheme.dark() : TarBarTheme.light(); + final theme = + isDarkTheme() ? const TarBarTheme.dark() : const TarBarTheme.light(); return SubWindowDragToResizeArea( windowId: windowId(), child: Container( @@ -90,9 +83,8 @@ class _FileManagerTabPageState extends State { } void onRemoveId(String id) { - ffi("ft_$id").close(); - if (tabController.state.value.tabs.length == 0) { - WindowController.fromWindowId(windowId()).close(); + if (tabController.state.value.tabs.isEmpty) { + WindowController.fromWindowId(windowId()).hide(); } } diff --git a/flutter/lib/desktop/pages/port_forward_tab_page.dart b/flutter/lib/desktop/pages/port_forward_tab_page.dart index 6323a0af9..7555d9745 100644 --- a/flutter/lib/desktop/pages/port_forward_tab_page.dart +++ b/flutter/lib/desktop/pages/port_forward_tab_page.dart @@ -5,7 +5,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/desktop/pages/port_forward_page.dart'; import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; -import 'package:flutter_hbb/models/model.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:get/get.dart'; @@ -44,7 +43,7 @@ class _PortForwardTabPageState extends State { tabController.onRemove = (_, id) => onRemoveId(id); rustDeskWinManager.setMethodHandler((call, fromWindowId) async { - print( + debugPrint( "call ${call.method} with args ${call.arguments} from window ${fromWindowId}"); // for simplify, just replace connectionId if (call.method == "new_port_forward") { @@ -59,14 +58,7 @@ class _PortForwardTabPageState extends State { unselectedIcon: unselectedIcon, page: PortForwardPage(id: id, isRDP: isRDP))); } else if (call.method == "onDestroy") { - tabController.state.value.tabs.forEach((tab) { - print("executing onDestroy hook, closing ${tab.label}}"); - final tag = 'pf_${tab.label}'; - ffi(tag).close().then((_) { - Get.delete(tag: tag); - }); - }); - Get.back(); + tabController.state.value.tabs.clear(); } }); } @@ -95,8 +87,8 @@ class _PortForwardTabPageState extends State { void onRemoveId(String id) { ffi("pf_$id").close(); - if (tabController.state.value.tabs.length == 0) { - WindowController.fromWindowId(windowId()).close(); + if (tabController.state.value.tabs.isEmpty) { + WindowController.fromWindowId(windowId()).hide(); } } diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 09f1ee4b5..b126ca7e3 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -6,8 +6,8 @@ import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/main.dart'; import 'package:get/get.dart'; -import 'package:window_manager/window_manager.dart'; import 'package:scroll_pos/scroll_pos.dart'; +import 'package:window_manager/window_manager.dart'; import '../../utils/multi_window_manager.dart'; @@ -323,7 +323,8 @@ class WindowActionPanel extends StatelessWidget { if (mainTab) { windowManager.close(); } else { - WindowController.fromWindowId(windowId!).close(); + // only hide for multi window, not close + WindowController.fromWindowId(windowId!).hide(); } }, is_close: true, diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 58ea849ce..70e922bce 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -1110,7 +1110,7 @@ class FFI { ffiModel.clear(); canvasModel.clear(); resetModifiers(); - print("model closed"); + debugPrint("model $id closed"); } /// Send **get** command to the Rust core based on [name] and [arg]. diff --git a/flutter/lib/utils/multi_window_manager.dart b/flutter/lib/utils/multi_window_manager.dart index b01b84a9d..fb6ce11ed 100644 --- a/flutter/lib/utils/multi_window_manager.dart +++ b/flutter/lib/utils/multi_window_manager.dart @@ -163,7 +163,7 @@ class RustDeskMultiWindowManager { // no such window already return; } - await WindowController.fromWindowId(wId).close(); + await WindowController.fromWindowId(wId).hide(); } on Error { return; } diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index ea5ff449c..d2bc7b1a8 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -252,8 +252,8 @@ packages: dependency: "direct main" description: path: "." - ref: "14a001e83ab0e7c8cb119f7f65be4e3056a954fb" - resolved-ref: "14a001e83ab0e7c8cb119f7f65be4e3056a954fb" + ref: e0368a023ba195462acc00d33ab361b499f0e413 + resolved-ref: e0368a023ba195462acc00d33ab361b499f0e413 url: "https://github.com/Kingtous/rustdesk_desktop_multi_window" source: git version: "0.1.0" diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index b765a5b17..799a2797a 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -65,7 +65,7 @@ dependencies: desktop_multi_window: git: url: https://github.com/Kingtous/rustdesk_desktop_multi_window - ref: 14a001e83ab0e7c8cb119f7f65be4e3056a954fb + ref: e0368a023ba195462acc00d33ab361b499f0e413 freezed_annotation: ^2.0.3 tray_manager: git: From c72e48bef1316affc61d2ee09d60c209e137cffa Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 30 Aug 2022 20:48:03 +0800 Subject: [PATCH 0319/2015] fix: close all typed sessions when hide subwindow --- .../desktop/pages/connection_tab_page.dart | 6 ++-- .../desktop/pages/file_manager_tab_page.dart | 10 ++++-- .../desktop/pages/port_forward_tab_page.dart | 5 ++- .../lib/desktop/widgets/tabbar_widget.dart | 34 ++++++++++++++----- flutter/lib/main.dart | 15 ++++---- flutter/lib/models/native_model.dart | 4 +-- flutter/lib/utils/multi_window_manager.dart | 2 +- flutter/pubspec.yaml | 2 +- 8 files changed, 51 insertions(+), 27 deletions(-) diff --git a/flutter/lib/desktop/pages/connection_tab_page.dart b/flutter/lib/desktop/pages/connection_tab_page.dart index 445590037..1d00cdc8a 100644 --- a/flutter/lib/desktop/pages/connection_tab_page.dart +++ b/flutter/lib/desktop/pages/connection_tab_page.dart @@ -63,7 +63,6 @@ class _ConnectionTabPageState extends State { label: id, selectedIcon: selectedIcon, unselectedIcon: unselectedIcon, - closable: false, page: Obx(() => RemotePage( key: ValueKey(id), id: id, @@ -71,7 +70,7 @@ class _ConnectionTabPageState extends State { fullscreen.isTrue ? 0 : kDesktopRemoteTabBarHeight, )))); } else if (call.method == "onDestroy") { - tabController.state.value.tabs.clear(); + tabController.clear(); } }); } @@ -93,6 +92,9 @@ class _ConnectionTabPageState extends State { theme: theme, isMainWindow: false, showTabBar: fullscreen.isFalse, + onClose: () { + tabController.clear(); + }, tail: AddButton( theme: theme, ).paddingOnly(left: 10), diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index e391afd71..e7f08a516 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -19,12 +19,13 @@ class FileManagerTabPage extends StatefulWidget { } class _FileManagerTabPageState extends State { - final tabController = Get.put(DesktopTabController()); + DesktopTabController get tabController => Get.find(); static final IconData selectedIcon = Icons.file_copy_sharp; static final IconData unselectedIcon = Icons.file_copy_outlined; _FileManagerTabPageState(Map params) { + Get.put(DesktopTabController()); tabController.add(TabInfo( key: params['id'], label: params['id'], @@ -36,7 +37,7 @@ class _FileManagerTabPageState extends State { @override void initState() { super.initState(); - + tabController.onRemove = (_, id) => onRemoveId(id); rustDeskWinManager.setMethodHandler((call, fromWindowId) async { @@ -54,7 +55,7 @@ class _FileManagerTabPageState extends State { unselectedIcon: unselectedIcon, page: FileManagerPage(key: ValueKey(id), id: id))); } else if (call.method == "onDestroy") { - tabController.state.value.tabs.clear(); + tabController.clear(); } }); } @@ -74,6 +75,9 @@ class _FileManagerTabPageState extends State { controller: tabController, theme: theme, isMainWindow: false, + onClose: () { + tabController.clear(); + }, tail: AddButton( theme: theme, ).paddingOnly(left: 10), diff --git a/flutter/lib/desktop/pages/port_forward_tab_page.dart b/flutter/lib/desktop/pages/port_forward_tab_page.dart index 7555d9745..8db4c7f98 100644 --- a/flutter/lib/desktop/pages/port_forward_tab_page.dart +++ b/flutter/lib/desktop/pages/port_forward_tab_page.dart @@ -58,7 +58,7 @@ class _PortForwardTabPageState extends State { unselectedIcon: unselectedIcon, page: PortForwardPage(id: id, isRDP: isRDP))); } else if (call.method == "onDestroy") { - tabController.state.value.tabs.clear(); + tabController.clear(); } }); } @@ -77,6 +77,9 @@ class _PortForwardTabPageState extends State { controller: tabController, theme: theme, isMainWindow: false, + onClose: () { + tabController.clear(); + }, tail: AddButton( theme: theme, ).paddingOnly(left: 10), diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index b126ca7e3..8ef082b49 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -1,3 +1,4 @@ +import 'dart:io'; import 'dart:math'; import 'package:desktop_multi_window/desktop_multi_window.dart'; @@ -113,6 +114,11 @@ class DesktopTabController { remove(state.value.selected); } } + + void clear() { + state.value.tabs.clear(); + state.refresh(); + } } class DesktopTab extends StatelessWidget { @@ -127,11 +133,12 @@ class DesktopTab extends StatelessWidget { final bool showClose; final Widget Function(Widget pageView)? pageViewBuilder; final Widget? tail; + final VoidCallback? onClose; final DesktopTabController controller; - late final state = controller.state; + Rx get state => controller.state; - DesktopTab( + const DesktopTab( {required this.controller, required this.isMainWindow, this.theme = const TarBarTheme.light(), @@ -143,7 +150,8 @@ class DesktopTab extends StatelessWidget { this.showMaximize = true, this.showClose = true, this.pageViewBuilder, - this.tail}); + this.tail, + this.onClose}); @override Widget build(BuildContext context) { @@ -185,6 +193,9 @@ class DesktopTab extends StatelessWidget { Expanded( child: Row( children: [ + Offstage( + offstage: !Platform.isMacOS, + child: const SizedBox(width: 78,)), Row(children: [ Offstage( offstage: !showLogo, @@ -229,6 +240,7 @@ class DesktopTab extends StatelessWidget { showMinimize: showMinimize, showMaximize: showMaximize, showClose: showClose, + onClose: onClose, ) ], ); @@ -242,6 +254,7 @@ class WindowActionPanel extends StatelessWidget { final bool showMinimize; final bool showMaximize; final bool showClose; + final VoidCallback? onClose; const WindowActionPanel( {Key? key, @@ -249,7 +262,8 @@ class WindowActionPanel extends StatelessWidget { required this.theme, this.showMinimize = true, this.showMaximize = true, - this.showClose = true}) + this.showClose = true, + this.onClose}) : super(key: key); @override @@ -324,8 +338,11 @@ class WindowActionPanel extends StatelessWidget { windowManager.close(); } else { // only hide for multi window, not close - WindowController.fromWindowId(windowId!).hide(); + Future.delayed(Duration.zero, () { + WindowController.fromWindowId(windowId!).hide(); + }); } + onClose?.call(); }, is_close: true, )), @@ -337,13 +354,12 @@ class WindowActionPanel extends StatelessWidget { // ignore: must_be_immutable class _ListView extends StatelessWidget { final DesktopTabController controller; - late final Rx state; final Function(String key)? onTabClose; final TarBarTheme theme; + Rx get state => controller.state; - _ListView( - {required this.controller, required this.onTabClose, required this.theme}) - : this.state = controller.state; + const _ListView( + {required this.controller, required this.onTabClose, required this.theme}); @override Widget build(BuildContext context) { diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 401b7febc..da3b07567 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -80,14 +80,7 @@ Future initEnv(String appType) async { } void runMainApp(bool startService) async { - WindowOptions windowOptions = getHiddenTitleBarWindowOptions(Size(1280, 720)); - await Future.wait([ - initEnv(kAppTypeMain), - windowManager.waitUntilReadyToShow(windowOptions, () async { - await windowManager.show(); - await windowManager.focus(); - }) - ]); + await initEnv(kAppTypeMain); if (startService) { // await windowManager.ensureInitialized(); // disable tray @@ -95,6 +88,12 @@ void runMainApp(bool startService) async { gFFI.serverModel.startService(); } runApp(App()); + // set window option + WindowOptions windowOptions = getHiddenTitleBarWindowOptions(const Size(1280, 720)); + windowManager.waitUntilReadyToShow(windowOptions, () async { + await windowManager.show(); + await windowManager.focus(); + }); } void runMobileApp() async { diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index 55f2d0e79..57372cdb9 100644 --- a/flutter/lib/models/native_model.dart +++ b/flutter/lib/models/native_model.dart @@ -117,7 +117,7 @@ class PlatformFFI { _homeDir = (await getDownloadsDirectory())?.path ?? ""; } } catch (e) { - print(e); + print("initialize failed: $e"); } String id = 'NA'; String name = 'Flutter'; @@ -151,7 +151,7 @@ class PlatformFFI { await _ffiBind.mainSetHomeDir(home: _homeDir); await _ffiBind.mainInit(appDir: _dir); } catch (e) { - print(e); + print("initialize failed: $e"); } version = await getVersion(); } diff --git a/flutter/lib/utils/multi_window_manager.dart b/flutter/lib/utils/multi_window_manager.dart index fb6ce11ed..b01b84a9d 100644 --- a/flutter/lib/utils/multi_window_manager.dart +++ b/flutter/lib/utils/multi_window_manager.dart @@ -163,7 +163,7 @@ class RustDeskMultiWindowManager { // no such window already return; } - await WindowController.fromWindowId(wId).hide(); + await WindowController.fromWindowId(wId).close(); } on Error { return; } diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 799a2797a..fc59b8bd3 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -34,7 +34,7 @@ dependencies: provider: ^6.0.3 tuple: ^2.0.0 wakelock: ^0.5.2 - device_info_plus: ^4.0.2 + device_info_plus: ^4.1.2 firebase_analytics: ^9.1.5 package_info_plus: ^1.4.2 url_launcher: ^6.0.9 From 01e96a1134a81069528cb58cb9e5a2ca3b02aba8 Mon Sep 17 00:00:00 2001 From: fufesou Date: Mon, 29 Aug 2022 18:48:12 +0800 Subject: [PATCH 0320/2015] flutter_desktop: connection type, mid commit Signed-off-by: fufesou --- flutter/lib/common.dart | 36 ----- flutter/lib/common/shared_state.dart | 73 +++++++++ flutter/lib/consts.dart | 2 + .../desktop/pages/connection_tab_page.dart | 46 +++++- flutter/lib/desktop/pages/remote_page.dart | 30 ++-- flutter/lib/desktop/widgets/popup_menu.dart | 74 ++++----- .../lib/desktop/widgets/remote_menubar.dart | 19 ++- .../lib/desktop/widgets/tabbar_widget.dart | 153 ++++++++++++------ flutter/lib/main.dart | 2 +- flutter/lib/models/model.dart | 33 ++-- 10 files changed, 303 insertions(+), 165 deletions(-) create mode 100644 flutter/lib/common/shared_state.dart diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 6027fb8de..b991c7a96 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -743,39 +743,3 @@ Future>? matchPeers(String searchText, List peers) async { } return filteredList; } - -class PrivacyModeState { - static String tag(String id) => 'privacy_mode_' + id; - - static void init(String id) { - final RxBool state = false.obs; - Get.put(state, tag: tag(id)); - } - - static void delete(String id) => Get.delete(tag: tag(id)); - static RxBool find(String id) => Get.find(tag: tag(id)); -} - -class BlockInputState { - static String tag(String id) => 'block_input_' + id; - - static void init(String id) { - final RxBool state = false.obs; - Get.put(state, tag: tag(id)); - } - - static void delete(String id) => Get.delete(tag: tag(id)); - static RxBool find(String id) => Get.find(tag: tag(id)); -} - -class CurrentDisplayState { - static String tag(String id) => 'current_display_' + id; - - static void init(String id) { - final RxInt state = RxInt(0); - Get.put(state, tag: tag(id)); - } - - static void delete(String id) => Get.delete(tag: tag(id)); - static RxInt find(String id) => Get.find(tag: tag(id)); -} diff --git a/flutter/lib/common/shared_state.dart b/flutter/lib/common/shared_state.dart new file mode 100644 index 000000000..8ff4e667e --- /dev/null +++ b/flutter/lib/common/shared_state.dart @@ -0,0 +1,73 @@ +import 'package:get/get.dart'; + +import '../consts.dart'; + +class PrivacyModeState { + static String tag(String id) => 'privacy_mode_$id'; + + static void init(String id) { + final RxBool state = false.obs; + Get.put(state, tag: tag(id)); + } + + static void delete(String id) => Get.delete(tag: tag(id)); + static RxBool find(String id) => Get.find(tag: tag(id)); +} + +class BlockInputState { + static String tag(String id) => 'block_input_$id'; + + static void init(String id) { + final RxBool state = false.obs; + Get.put(state, tag: tag(id)); + } + + static void delete(String id) => Get.delete(tag: tag(id)); + static RxBool find(String id) => Get.find(tag: tag(id)); +} + +class CurrentDisplayState { + static String tag(String id) => 'current_display_$id'; + + static void init(String id) { + final RxInt state = RxInt(0); + Get.put(state, tag: tag(id)); + } + + static void delete(String id) => Get.delete(tag: tag(id)); + static RxInt find(String id) => Get.find(tag: tag(id)); +} + +class ConnectionType { + final Rx _secure = kInvalidValueStr.obs; + final Rx _direct = kInvalidValueStr.obs; + + Rx get secure => _secure; + Rx get direct => _direct; + + void setSecure(bool v) { + _secure.value = v ? 'secure' : 'insecure'; + } + + void setDirect(bool v) { + _direct.value = v ? '' : '_relay'; + } + + bool isValid() { + return _secure.value != kInvalidValueStr && + _direct.value != kInvalidValueStr; + } +} + +class ConnectionTypeState { + static String tag(String id) => 'connection_type_$id'; + + static void init(String id) { + final ConnectionType collectionType = ConnectionType(); + Get.put(collectionType, tag: tag(id)); + } + + static void delete(String id) => Get.delete(tag: tag(id)); + static ConnectionType find(String id) => + Get.find(tag: tag(id)); +} diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 3f0abd43f..6c67e2ab9 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -10,3 +10,5 @@ const String kTabLabelSettingPage = "Settings"; const int kDefaultDisplayWidth = 1280; const int kDefaultDisplayHeight = 720; + +const kInvalidValueStr = "InvalidValueStr"; diff --git a/flutter/lib/desktop/pages/connection_tab_page.dart b/flutter/lib/desktop/pages/connection_tab_page.dart index 1d00cdc8a..75471af0e 100644 --- a/flutter/lib/desktop/pages/connection_tab_page.dart +++ b/flutter/lib/desktop/pages/connection_tab_page.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/common/shared_state.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/desktop/pages/remote_page.dart'; import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; @@ -20,8 +21,8 @@ class ConnectionTabPage extends StatefulWidget { class _ConnectionTabPageState extends State { final tabController = Get.put(DesktopTabController()); - static final IconData selectedIcon = Icons.desktop_windows_sharp; - static final IconData unselectedIcon = Icons.desktop_windows_outlined; + static const IconData selectedIcon = Icons.desktop_windows_sharp; + static const IconData unselectedIcon = Icons.desktop_windows_outlined; var connectionMap = RxList.empty(growable: true); @@ -34,7 +35,7 @@ class _ConnectionTabPageState extends State { selectedIcon: selectedIcon, unselectedIcon: unselectedIcon, page: Obx(() => RemotePage( - key: ValueKey(params['id']), + key: ValueKey(params['id']), id: params['id'], tabBarHeight: fullscreen.isTrue ? 0 : kDesktopRemoteTabBarHeight, @@ -88,10 +89,10 @@ class _ConnectionTabPageState extends State { child: Scaffold( backgroundColor: MyTheme.color(context).bg, body: Obx(() => DesktopTab( - controller: tabController, - theme: theme, - isMainWindow: false, - showTabBar: fullscreen.isFalse, + controller: tabController, + theme: theme, + isMainWindow: false, + showTabBar: fullscreen.isFalse, onClose: () { tabController.clear(); }, @@ -103,7 +104,36 @@ class _ConnectionTabPageState extends State { .setFullscreen(fullscreen.isTrue); return pageView; }, - ))), + tabBuilder: (key, icon, label, themeConf) { + final connectionType = ConnectionTypeState.find(key); + if (!connectionType.isValid()) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + icon, + label, + ], + ); + } else { + final iconName = + '${connectionType.secure.value}${connectionType.direct.value}'; + final connectionIcon = Image.asset( + 'assets/$iconName.png', + width: themeConf.iconSize, + height: themeConf.iconSize, + color: theme.selectedtabIconColor, + ); + //.paddingOnly(right: 5); + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + icon, + connectionIcon, + label, + ], + ); + } + }))), ), )); } diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index fbba5bafe..14635e5a1 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -5,11 +5,9 @@ import 'dart:ui' as ui; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_hbb/models/chat_model.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:wakelock/wakelock.dart'; -import 'package:tuple/tuple.dart'; // import 'package:window_manager/window_manager.dart'; @@ -19,6 +17,8 @@ import '../../mobile/widgets/dialog.dart'; import '../../mobile/widgets/overlay.dart'; import '../../models/model.dart'; import '../../models/platform_model.dart'; +import '../../models/chat_model.dart'; +import '../../common/shared_state.dart'; final initText = '\1' * 1024; @@ -41,7 +41,7 @@ class _RemotePageState extends State Timer? _timer; bool _showBar = !isWebDesktop; String _value = ''; - var _cursorOverImage = false.obs; + final _cursorOverImage = false.obs; final FocusNode _mobileFocusNode = FocusNode(); final FocusNode _physicalFocusNode = FocusNode(); @@ -54,6 +54,20 @@ class _RemotePageState extends State _ffi.canvasModel.tabBarHeight = widget.tabBarHeight; } + void _initStates(String id) { + PrivacyModeState.init(id); + BlockInputState.init(id); + CurrentDisplayState.init(id); + ConnectionTypeState.init(id); + } + + void _removeStates(String id) { + PrivacyModeState.delete(id); + BlockInputState.delete(id); + CurrentDisplayState.delete(id); + ConnectionTypeState.delete(id); + } + @override void initState() { super.initState(); @@ -74,14 +88,12 @@ class _RemotePageState extends State _ffi.listenToMouse(true); _ffi.qualityMonitorModel.checkShowQualityMonitor(widget.id); // WindowManager.instance.addListener(this); - PrivacyModeState.init(widget.id); - BlockInputState.init(widget.id); - CurrentDisplayState.init(widget.id); + _initStates(widget.id); } @override void dispose() { - print("REMOTE PAGE dispose ${widget.id}"); + debugPrint("REMOTE PAGE dispose ${widget.id}"); hideMobileActionsOverlay(); _ffi.listenToMouse(false); _mobileFocusNode.dispose(); @@ -97,9 +109,7 @@ class _RemotePageState extends State // WindowManager.instance.removeListener(this); Get.delete(tag: widget.id); super.dispose(); - PrivacyModeState.delete(widget.id); - BlockInputState.delete(widget.id); - CurrentDisplayState.delete(widget.id); + _removeStates(widget.id); } void resetTool() { diff --git a/flutter/lib/desktop/widgets/popup_menu.dart b/flutter/lib/desktop/widgets/popup_menu.dart index acb8f184c..00f940fdb 100644 --- a/flutter/lib/desktop/widgets/popup_menu.dart +++ b/flutter/lib/desktop/widgets/popup_menu.dart @@ -4,12 +4,10 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:tuple/tuple.dart'; -import './material_mod_popup_menu.dart' as modMenu; - -const kInvalidValueStr = "InvalidValueStr"; +import './material_mod_popup_menu.dart' as mod_menu; // https://stackoverflow.com/questions/68318314/flutter-popup-menu-inside-popup-menu -class PopupMenuChildrenItem extends modMenu.PopupMenuEntry { +class PopupMenuChildrenItem extends mod_menu.PopupMenuEntry { const PopupMenuChildrenItem({ key, this.height = kMinInteractiveDimension, @@ -17,19 +15,19 @@ class PopupMenuChildrenItem extends modMenu.PopupMenuEntry { this.enable = true, this.textStyle, this.onTap, - this.position = modMenu.PopupMenuPosition.overSide, + this.position = mod_menu.PopupMenuPosition.overSide, this.offset = Offset.zero, required this.itemBuilder, required this.child, }) : super(key: key); - final modMenu.PopupMenuPosition position; + final mod_menu.PopupMenuPosition position; final Offset offset; final TextStyle? textStyle; final EdgeInsets? padding; final bool enable; final void Function()? onTap; - final List> Function(BuildContext) itemBuilder; + final List> Function(BuildContext) itemBuilder; final Widget child; @override @@ -59,7 +57,7 @@ class MyPopupMenuItemState> popupMenuTheme.textStyle ?? theme.textTheme.subtitle1!; - return modMenu.PopupMenuButton( + return mod_menu.PopupMenuButton( enabled: widget.enable, position: widget.position, offset: widget.offset, @@ -88,22 +86,26 @@ class MenuConfig { static const iconWidth = 12.0; static const iconHeight = 12.0; - final double secondMenuHeight; + final double height; + final double dividerHeight; final Color commonColor; const MenuConfig( {required this.commonColor, - this.secondMenuHeight = kMinInteractiveDimension}); + this.height = kMinInteractiveDimension, + this.dividerHeight = 16.0}); } abstract class MenuEntryBase { - modMenu.PopupMenuEntry build(BuildContext context, MenuConfig conf); + mod_menu.PopupMenuEntry build(BuildContext context, MenuConfig conf); } class MenuEntryDivider extends MenuEntryBase { @override - modMenu.PopupMenuEntry build(BuildContext context, MenuConfig conf) { - return const modMenu.PopupMenuDivider(); + mod_menu.PopupMenuEntry build(BuildContext context, MenuConfig conf) { + return mod_menu.PopupMenuDivider( + height: conf.dividerHeight, + ); } } @@ -138,14 +140,15 @@ class MenuEntrySubRadios extends MenuEntryBase { } } - modMenu.PopupMenuEntry _buildSecondMenu( + mod_menu.PopupMenuEntry _buildSecondMenu( BuildContext context, MenuConfig conf, Tuple2 opt) { - return modMenu.PopupMenuItem( + return mod_menu.PopupMenuItem( padding: EdgeInsets.zero, + height: conf.height, child: TextButton( child: Container( alignment: AlignmentDirectional.centerStart, - constraints: BoxConstraints(minHeight: conf.secondMenuHeight), + constraints: BoxConstraints(minHeight: conf.height), child: Row( children: [ SizedBox( @@ -156,7 +159,7 @@ class MenuEntrySubRadios extends MenuEntryBase { Icons.check, color: conf.commonColor, ) - : SizedBox.shrink())), + : const SizedBox.shrink())), const SizedBox(width: MenuConfig.midPadding), Text( opt.item1, @@ -178,10 +181,10 @@ class MenuEntrySubRadios extends MenuEntryBase { } @override - modMenu.PopupMenuEntry build(BuildContext context, MenuConfig conf) { + mod_menu.PopupMenuEntry build(BuildContext context, MenuConfig conf) { return PopupMenuChildrenItem( - height: conf.secondMenuHeight, padding: EdgeInsets.zero, + height: conf.height, itemBuilder: (BuildContext context) => options.map((opt) => _buildSecondMenu(context, conf, opt)).toList(), child: Row(children: [ @@ -218,9 +221,10 @@ abstract class MenuEntrySwitchBase extends MenuEntryBase { Future setOption(bool option); @override - modMenu.PopupMenuEntry build(BuildContext context, MenuConfig conf) { - return modMenu.PopupMenuItem( + mod_menu.PopupMenuEntry build(BuildContext context, MenuConfig conf) { + return mod_menu.PopupMenuItem( padding: EdgeInsets.zero, + height: conf.height, child: Obx( () => SwitchListTile( value: curOption.value, @@ -229,7 +233,7 @@ abstract class MenuEntrySwitchBase extends MenuEntryBase { }, title: Container( alignment: AlignmentDirectional.centerStart, - constraints: BoxConstraints(minHeight: conf.secondMenuHeight), + constraints: BoxConstraints(minHeight: conf.height), child: Text( text, style: const TextStyle( @@ -242,7 +246,7 @@ abstract class MenuEntrySwitchBase extends MenuEntryBase { horizontal: VisualDensity.minimumDensity, vertical: VisualDensity.minimumDensity, ), - contentPadding: EdgeInsets.only(left: 8.0), + contentPadding: const EdgeInsets.only(left: 8.0), ), ), ); @@ -303,11 +307,11 @@ class MenuEntrySubMenu extends MenuEntryBase { }); @override - modMenu.PopupMenuEntry build(BuildContext context, MenuConfig conf) { + mod_menu.PopupMenuEntry build(BuildContext context, MenuConfig conf) { return PopupMenuChildrenItem( - height: conf.secondMenuHeight, + height: conf.height, padding: EdgeInsets.zero, - position: modMenu.PopupMenuPosition.overSide, + position: mod_menu.PopupMenuPosition.overSide, itemBuilder: (BuildContext context) => entries.map((entry) => entry.build(context, conf)).toList(), child: Row(children: [ @@ -342,13 +346,14 @@ class MenuEntryButton extends MenuEntryBase { }); @override - modMenu.PopupMenuEntry build(BuildContext context, MenuConfig conf) { - return modMenu.PopupMenuItem( + mod_menu.PopupMenuEntry build(BuildContext context, MenuConfig conf) { + return mod_menu.PopupMenuItem( padding: EdgeInsets.zero, + height: conf.height, child: TextButton( child: Container( alignment: AlignmentDirectional.centerStart, - constraints: BoxConstraints(minHeight: conf.secondMenuHeight), + constraints: BoxConstraints(minHeight: conf.height), child: childBuilder( const TextStyle( color: Colors.black, @@ -362,14 +367,3 @@ class MenuEntryButton extends MenuEntryBase { ); } } - -class CustomMenu { - final List> entries; - final MenuConfig conf; - - const CustomMenu({required this.entries, required this.conf}); - - List> build(BuildContext context) { - return entries.map((entry) => entry.build(context, conf)).toList(); - } -} diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 011525ba9..620f5f226 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -9,12 +9,15 @@ import '../../mobile/widgets/dialog.dart'; import '../../mobile/widgets/overlay.dart'; import '../../models/model.dart'; import '../../models/platform_model.dart'; +import '../../common/shared_state.dart'; import './popup_menu.dart'; import './material_mod_popup_menu.dart' as mod_menu; class _MenubarTheme { static const Color commonColor = MyTheme.accent; - static const double height = kMinInteractiveDimension; + // kMinInteractiveDimension + static const double height = 24.0; + static const double dividerHeight = 12.0; } class RemoteMenubar extends StatefulWidget { @@ -168,11 +171,9 @@ class _RemoteMenubarState extends State { ), itemBuilder: (BuildContext context) { final List rowChildren = []; - const double selectorScale = 1.3; for (int i = 0; i < pi.displays.length; i++) { - rowChildren.add(Transform.scale( - scale: selectorScale, - child: Stack( + rowChildren.add( + Stack( alignment: Alignment.center, children: [ const Icon( @@ -203,7 +204,7 @@ class _RemoteMenubarState extends State { ) ], ), - )); + ); } return >[ mod_menu.PopupMenuItem( @@ -232,7 +233,8 @@ class _RemoteMenubarState extends State { context, const MenuConfig( commonColor: _MenubarTheme.commonColor, - secondMenuHeight: _MenubarTheme.height, + height: _MenubarTheme.height, + dividerHeight: _MenubarTheme.dividerHeight, ))) .toList(), ); @@ -253,7 +255,8 @@ class _RemoteMenubarState extends State { context, const MenuConfig( commonColor: _MenubarTheme.commonColor, - secondMenuHeight: _MenubarTheme.height, + height: _MenubarTheme.height, + dividerHeight: _MenubarTheme.dividerHeight, ))) .toList(), ); diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 8ef082b49..6e0ce747d 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -121,6 +121,16 @@ class DesktopTabController { } } +class TabThemeConf { + double iconSize; + TarBarTheme theme; + TabThemeConf({required this.iconSize, required this.theme}); +} + +typedef TabBuilder = Widget Function( + String key, Widget icon, Widget label, TabThemeConf themeConf); +typedef LabelGetter = Rx Function(String key); + class DesktopTab extends StatelessWidget { final Function(String)? onTabClose; final TarBarTheme theme; @@ -134,24 +144,29 @@ class DesktopTab extends StatelessWidget { final Widget Function(Widget pageView)? pageViewBuilder; final Widget? tail; final VoidCallback? onClose; + final TabBuilder? tabBuilder; + final LabelGetter? labelGetter; final DesktopTabController controller; Rx get state => controller.state; - const DesktopTab( - {required this.controller, - required this.isMainWindow, - this.theme = const TarBarTheme.light(), - this.onTabClose, - this.showTabBar = true, - this.showLogo = true, - this.showTitle = true, - this.showMinimize = true, - this.showMaximize = true, - this.showClose = true, - this.pageViewBuilder, - this.tail, - this.onClose}); + const DesktopTab({ + required this.controller, + required this.isMainWindow, + this.theme = const TarBarTheme.light(), + this.onTabClose, + this.showTabBar = true, + this.showLogo = true, + this.showTitle = true, + this.showMinimize = true, + this.showMaximize = true, + this.showClose = true, + this.pageViewBuilder, + this.tail, + this.onClose, + this.tabBuilder, + this.labelGetter, + }); @override Widget build(BuildContext context) { @@ -194,8 +209,10 @@ class DesktopTab extends StatelessWidget { child: Row( children: [ Offstage( - offstage: !Platform.isMacOS, - child: const SizedBox(width: 78,)), + offstage: !Platform.isMacOS, + child: const SizedBox( + width: 78, + )), Row(children: [ Offstage( offstage: !showLogo, @@ -228,6 +245,8 @@ class DesktopTab extends StatelessWidget { controller: controller, onTabClose: onTabClose, theme: theme, + tabBuilder: tabBuilder, + labelGetter: labelGetter, )), ), ], @@ -356,10 +375,18 @@ class _ListView extends StatelessWidget { final DesktopTabController controller; final Function(String key)? onTabClose; final TarBarTheme theme; + + final TabBuilder? tabBuilder; + final LabelGetter? labelGetter; + Rx get state => controller.state; - const _ListView( - {required this.controller, required this.onTabClose, required this.theme}); + _ListView( + {required this.controller, + required this.onTabClose, + required this.theme, + this.tabBuilder, + this.labelGetter}); @override Widget build(BuildContext context) { @@ -373,7 +400,9 @@ class _ListView extends StatelessWidget { final tab = e.value; return _Tab( index: index, - label: tab.label, + label: labelGetter == null + ? Rx(tab.label) + : labelGetter!(tab.label), selectedIcon: tab.selectedIcon, unselectedIcon: tab.unselectedIcon, closable: tab.closable, @@ -381,6 +410,16 @@ class _ListView extends StatelessWidget { onClose: () => controller.remove(index), onSelected: () => controller.jumpTo(index), theme: theme, + tabBuilder: tabBuilder == null + ? null + : (Widget icon, Widget labelWidget, TabThemeConf themeConf) { + return tabBuilder!( + tab.label, + icon, + labelWidget, + themeConf, + ); + }, ); }).toList())); } @@ -388,7 +427,7 @@ class _ListView extends StatelessWidget { class _Tab extends StatelessWidget { late final int index; - late final String label; + late final Rx label; late final IconData? selectedIcon; late final IconData? unselectedIcon; late final bool closable; @@ -397,6 +436,8 @@ class _Tab extends StatelessWidget { late final Function() onSelected; final RxBool _hover = false.obs; late final TarBarTheme theme; + final Widget Function(Widget icon, Widget label, TabThemeConf themeConf)? + tabBuilder; _Tab( {Key? key, @@ -404,6 +445,7 @@ class _Tab extends StatelessWidget { required this.label, this.selectedIcon, this.unselectedIcon, + this.tabBuilder, required this.closable, required this.selected, required this.onClose, @@ -411,11 +453,49 @@ class _Tab extends StatelessWidget { required this.theme}) : super(key: key); + Widget _buildTabContent() { + bool showIcon = selectedIcon != null && unselectedIcon != null; + bool isSelected = index == selected; + + final icon = Offstage( + offstage: !showIcon, + child: Icon( + isSelected ? selectedIcon : unselectedIcon, + size: _kIconSize, + color: isSelected + ? theme.selectedtabIconColor + : theme.unSelectedtabIconColor, + ).paddingOnly(right: 5)); + final labelWidget = Obx(() { + return Text( + translate(label.value), + textAlign: TextAlign.center, + style: TextStyle( + color: isSelected + ? theme.selectedTextColor + : theme.unSelectedTextColor), + ); + }); + + if (tabBuilder == null) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + icon, + labelWidget, + ], + ); + } else { + return tabBuilder!( + icon, labelWidget, TabThemeConf(iconSize: _kIconSize, theme: theme)); + } + } + @override Widget build(BuildContext context) { - bool show_icon = selectedIcon != null && unselectedIcon != null; - bool is_selected = index == selected; - bool show_divider = index != selected - 1 && index != selected; + bool showIcon = selectedIcon != null && unselectedIcon != null; + bool isSelected = index == selected; + bool showDivider = index != selected - 1 && index != selected; return Ink( child: InkWell( onHover: (hover) => _hover.value = hover, @@ -427,40 +507,19 @@ class _Tab extends StatelessWidget { child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Offstage( - offstage: !show_icon, - child: Icon( - is_selected ? selectedIcon : unselectedIcon, - size: _kIconSize, - color: is_selected - ? theme.selectedtabIconColor - : theme.unSelectedtabIconColor, - ).paddingOnly(right: 5)), - Text( - translate(label), - textAlign: TextAlign.center, - style: TextStyle( - color: is_selected - ? theme.selectedTextColor - : theme.unSelectedTextColor), - ), - ], - ), + _buildTabContent(), Offstage( offstage: !closable, child: Obx((() => _CloseButton( visiable: _hover.value, - tabSelected: is_selected, + tabSelected: isSelected, onClose: () => onClose(), theme: theme, ))), ) ])).paddingSymmetric(horizontal: 10), Offstage( - offstage: !show_divider, + offstage: !showDivider, child: VerticalDivider( width: 1, indent: _kDividerIndent, diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index da3b07567..efca88180 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -202,7 +202,7 @@ class App extends StatelessWidget { title: 'RustDesk', theme: getCurrentTheme(), home: isDesktop - ? DesktopTabPage() + ? const DesktopTabPage() : !isAndroid ? WebHomePage() : HomePage(), diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 70e922bce..4a54ba4e8 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -17,6 +17,7 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'package:tuple/tuple.dart'; import '../common.dart'; +import '../common/shared_state.dart'; import '../mobile/widgets/dialog.dart'; import '../mobile/widgets/overlay.dart'; import 'peer_model.dart'; @@ -96,25 +97,26 @@ class FfiModel with ChangeNotifier { clearPermissions(); } - void setConnectionType(bool secure, bool direct) { + void setConnectionType(String peerId, bool secure, bool direct) { _secure = secure; _direct = direct; + try { + var connectionType = ConnectionTypeState.find(peerId); + connectionType.setSecure(secure); + connectionType.setDirect(direct); + } catch (e) { + // + } } Image? getConnectionImage() { - String? icon; - if (secure == true && direct == true) { - icon = 'secure'; - } else if (secure == false && direct == true) { - icon = 'insecure'; - } else if (secure == false && direct == false) { - icon = 'insecure_relay'; - } else if (secure == true && direct == false) { - icon = 'secure_relay'; + if (secure == null || direct == null) { + return null; + } else { + final icon = + '${secure == true ? "secure" : "insecure"}${direct == true ? "" : "_relay"}'; + return Image.asset('assets/$icon.png', width: 48, height: 48); } - return icon == null - ? null - : Image.asset('assets/$icon.png', width: 48, height: 48); } void clearPermissions() { @@ -130,7 +132,8 @@ class FfiModel with ChangeNotifier { } else if (name == 'peer_info') { handlePeerInfo(evt, peerId); } else if (name == 'connection_ready') { - setConnectionType(evt['secure'] == 'true', evt['direct'] == 'true'); + setConnectionType( + peerId, evt['secure'] == 'true', evt['direct'] == 'true'); } else if (name == 'switch_display') { handleSwitchDisplay(evt); } else if (name == 'cursor_data') { @@ -189,7 +192,7 @@ class FfiModel with ChangeNotifier { handlePeerInfo(evt, peerId); } else if (name == 'connection_ready') { parent.target?.ffiModel.setConnectionType( - evt['secure'] == 'true', evt['direct'] == 'true'); + peerId, evt['secure'] == 'true', evt['direct'] == 'true'); } else if (name == 'switch_display') { handleSwitchDisplay(evt); } else if (name == 'cursor_data') { From f42c6ffeaf6ec66101d7389586bab7fe906453aa Mon Sep 17 00:00:00 2001 From: fufesou Date: Mon, 29 Aug 2022 22:46:19 +0800 Subject: [PATCH 0321/2015] flutter_desktop: connection type icon, tested windows Signed-off-by: fufesou --- flutter/lib/common/shared_state.dart | 24 +++-- .../desktop/pages/connection_tab_page.dart | 90 +++++++++++-------- flutter/lib/desktop/pages/remote_page.dart | 2 - src/lang/cn.rs | 4 + src/lang/cs.rs | 4 + src/lang/da.rs | 4 + src/lang/de.rs | 4 + src/lang/eo.rs | 4 + src/lang/es.rs | 4 + src/lang/fr.rs | 4 + src/lang/hu.rs | 4 + src/lang/id.rs | 4 + src/lang/it.rs | 4 + src/lang/ja.rs | 4 + src/lang/ko.rs | 4 + src/lang/pl.rs | 4 + src/lang/pt_PT.rs | 4 + src/lang/ptbr.rs | 4 + src/lang/ru.rs | 4 + src/lang/sk.rs | 4 + src/lang/template.rs | 4 + src/lang/tr.rs | 4 + src/lang/tw.rs | 4 + src/lang/vn.rs | 4 + 24 files changed, 154 insertions(+), 46 deletions(-) diff --git a/flutter/lib/common/shared_state.dart b/flutter/lib/common/shared_state.dart index 8ff4e667e..7232cb6ad 100644 --- a/flutter/lib/common/shared_state.dart +++ b/flutter/lib/common/shared_state.dart @@ -45,12 +45,17 @@ class ConnectionType { Rx get secure => _secure; Rx get direct => _direct; + static String get strSecure => 'secure'; + static String get strInsecure => 'insecure'; + static String get strDirect => ''; + static String get strIndirect => '_relay'; + void setSecure(bool v) { - _secure.value = v ? 'secure' : 'insecure'; + _secure.value = v ? strSecure : strInsecure; } void setDirect(bool v) { - _direct.value = v ? '' : '_relay'; + _direct.value = v ? strDirect : strIndirect; } bool isValid() { @@ -63,11 +68,20 @@ class ConnectionTypeState { static String tag(String id) => 'connection_type_$id'; static void init(String id) { - final ConnectionType collectionType = ConnectionType(); - Get.put(collectionType, tag: tag(id)); + final key = tag(id); + if (!Get.isRegistered(tag: key)) { + final ConnectionType collectionType = ConnectionType(); + Get.put(collectionType, tag: key); + } + } + + static void delete(String id) { + final key = tag(id); + if (Get.isRegistered(tag: key)) { + Get.delete(tag: key); + } } - static void delete(String id) => Get.delete(tag: tag(id)); static ConnectionType find(String id) => Get.find(tag: tag(id)); } diff --git a/flutter/lib/desktop/pages/connection_tab_page.dart b/flutter/lib/desktop/pages/connection_tab_page.dart index 75471af0e..1b9e04ebf 100644 --- a/flutter/lib/desktop/pages/connection_tab_page.dart +++ b/flutter/lib/desktop/pages/connection_tab_page.dart @@ -28,15 +28,17 @@ class _ConnectionTabPageState extends State { _ConnectionTabPageState(Map params) { final RxBool fullscreen = Get.find(tag: 'fullscreen'); - if (params['id'] != null) { + final peerId = params['id']; + if (peerId != null) { + ConnectionTypeState.init(peerId); tabController.add(TabInfo( - key: params['id'], - label: params['id'], + key: peerId, + label: peerId, selectedIcon: selectedIcon, unselectedIcon: unselectedIcon, page: Obx(() => RemotePage( - key: ValueKey(params['id']), - id: params['id'], + key: ValueKey(peerId), + id: peerId, tabBarHeight: fullscreen.isTrue ? 0 : kDesktopRemoteTabBarHeight, )))); @@ -89,10 +91,10 @@ class _ConnectionTabPageState extends State { child: Scaffold( backgroundColor: MyTheme.color(context).bg, body: Obx(() => DesktopTab( - controller: tabController, - theme: theme, - isMainWindow: false, - showTabBar: fullscreen.isFalse, + controller: tabController, + theme: theme, + isMainWindow: false, + showTabBar: fullscreen.isFalse, onClose: () { tabController.clear(); }, @@ -104,36 +106,45 @@ class _ConnectionTabPageState extends State { .setFullscreen(fullscreen.isTrue); return pageView; }, - tabBuilder: (key, icon, label, themeConf) { - final connectionType = ConnectionTypeState.find(key); - if (!connectionType.isValid()) { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - icon, - label, - ], - ); - } else { - final iconName = - '${connectionType.secure.value}${connectionType.direct.value}'; - final connectionIcon = Image.asset( - 'assets/$iconName.png', - width: themeConf.iconSize, - height: themeConf.iconSize, - color: theme.selectedtabIconColor, - ); - //.paddingOnly(right: 5); - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - icon, - connectionIcon, - label, - ], - ); - } - }))), + tabBuilder: (key, icon, label, themeConf) => Obx(() { + final connectionType = ConnectionTypeState.find(key); + if (!ConnectionTypeState.find(key).isValid()) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + icon, + label, + ], + ); + } else { + final msgDirect = translate( + connectionType.direct.value == + ConnectionType.strDirect + ? 'Direct Connection' + : 'Relay Connection'); + final msgSecure = translate( + connectionType.secure.value == + ConnectionType.strSecure + ? 'Secure Connection' + : 'Insecure Connection'); + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + icon, + Tooltip( + message: '$msgDirect\n$msgSecure', + child: Image.asset( + 'assets/${connectionType.secure.value}${connectionType.direct.value}.png', + width: themeConf.iconSize, + height: themeConf.iconSize, + ).paddingOnly(right: 5), + ), + label, + ], + ); + } + }), + ))), ), )); } @@ -142,6 +153,7 @@ class _ConnectionTabPageState extends State { if (tabController.state.value.tabs.isEmpty) { WindowController.fromWindowId(windowId()).hide(); } + ConnectionTypeState.delete(id); } int windowId() { diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 14635e5a1..f723b17d0 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -58,14 +58,12 @@ class _RemotePageState extends State PrivacyModeState.init(id); BlockInputState.init(id); CurrentDisplayState.init(id); - ConnectionTypeState.init(id); } void _removeStates(String id) { PrivacyModeState.delete(id); BlockInputState.delete(id); CurrentDisplayState.delete(id); - ConnectionTypeState.delete(id); } @override diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 3e50396e6..fdb23f88e 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -313,5 +313,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scroll Style", "滚屏方式"), ("Show Menubar", "显示菜单栏"), ("Hide Menubar", "隐藏菜单栏"), + ("Direct Connection", "直接连接"), + ("Relay Connection", "中继连接"), + ("Secure Connection", "安全连接"), + ("Insecure Connection", "非安全连接"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index f94df1ceb..d9ddf78cc 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -313,5 +313,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scroll Style", "Štýl posúvania"), ("Show Menubar", "Zobrazit panel nabídek"), ("Hide Menubar", "skrýt panel nabídek"), + ("Direct Connection", "Přímé spojení"), + ("Relay Connection", "Připojení relé"), + ("Secure Connection", "Zabezpečené připojení"), + ("Insecure Connection", "Nezabezpečené připojení"), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index c0f7abf91..0e2d99425 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -313,5 +313,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scroll Style", "Rulstil"), ("Show Menubar", "Vis menulinje"), ("Hide Menubar", "skjul menulinjen"), + ("Direct Connection", "Direkte forbindelse"), + ("Relay Connection", "Relæforbindelse"), + ("Secure Connection", "Sikker forbindelse"), + ("Insecure Connection", "Usikker forbindelse"), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index e411a751d..20cd9330e 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -313,5 +313,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scroll Style", "Scroll-Stil"), ("Show Menubar", "Menüleiste anzeigen"), ("Hide Menubar", "Menüleiste ausblenden"), + ("Direct Connection", "Direkte Verbindung"), + ("Relay Connection", "Relaisverbindung"), + ("Secure Connection", "Sichere Verbindung"), + ("Insecure Connection", "Unsichere Verbindung"), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 211e6728d..fe12e2d24 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -313,5 +313,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scroll Style", "Ruluma Stilo"), ("Show Menubar", "Montru menubreton"), ("Hide Menubar", "kaŝi menubreton"), + ("Direct Connection", "Rekta Konekto"), + ("Relay Connection", "Relajsa Konekto"), + ("Secure Connection", "Sekura Konekto"), + ("Insecure Connection", "Nesekura Konekto"), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 068442bf4..313ea8cac 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -313,5 +313,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scroll Style", "Estilo de desplazamiento"), ("Show Menubar", "ajustes de pantalla"), ("Hide Menubar", "ocultar barra de menú"), + ("Direct Connection", "Conexión directa"), + ("Relay Connection", "Conexión de relé"), + ("Secure Connection", "Conexión segura"), + ("Insecure Connection", "Conexión insegura"), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index d568f050b..c8b12243f 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -313,5 +313,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scroll Style", "Style de défilement"), ("Show Menubar", "Afficher la barre de menus"), ("Hide Menubar", "masquer la barre de menus"), + ("Direct Connection", "Connexion directe"), + ("Relay Connection", "Connexion relais"), + ("Secure Connection", "Connexion sécurisée"), + ("Insecure Connection", "Connexion non sécurisée"), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 1fe693248..a6356b000 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -313,5 +313,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scroll Style", "Görgetési stílus"), ("Show Menubar", "Menüsor megjelenítése"), ("Hide Menubar", "menüsor elrejtése"), + ("Direct Connection", "Közvetlen kapcsolat"), + ("Relay Connection", "Relé csatlakozás"), + ("Secure Connection", "Biztonságos kapcsolat"), + ("Insecure Connection", "Nem biztonságos kapcsolat"), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index d5d6ed920..8548eb6bc 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -313,5 +313,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scroll Style", "Gaya Gulir"), ("Show Menubar", "Tampilkan bilah menu"), ("Hide Menubar", "sembunyikan bilah menu"), + ("Direct Connection", "Koneksi langsung"), + ("Relay Connection", "Koneksi Relay"), + ("Secure Connection", "Koneksi aman"), + ("Insecure Connection", "Koneksi Tidak Aman"), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 26e7d4073..fdf8d27d9 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -312,5 +312,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scroll Style", "Stile di scorrimento"), ("Show Menubar", "Mostra la barra dei menu"), ("Hide Menubar", "nascondi la barra dei menu"), + ("Direct Connection", "Connessione diretta"), + ("Relay Connection", "Collegamento a relè"), + ("Secure Connection", "Connessione sicura"), + ("Insecure Connection", "Connessione insicura"), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index f1331b01d..1d031f2f2 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -310,5 +310,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scroll Style", "スクロール スタイル"), ("Show Menubar", "メニューバーを表示"), ("Hide Menubar", "メニューバーを隠す"), + ("Direct Connection", "直接接続"), + ("Relay Connection", "リレー接続"), + ("Secure Connection", "安全な接続"), + ("Insecure Connection", "安全でない接続"), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 7a0d8dbdf..19d4c7ddf 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -310,5 +310,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scroll Style", "스크롤 스타일"), ("Show Menubar", "메뉴 표시줄 표시"), ("Hide Menubar", "메뉴 표시줄 숨기기"), + ("Direct Connection", "직접 연결"), + ("Relay Connection", "릴레이 연결"), + ("Secure Connection", "보안 연결"), + ("Insecure Connection", "안전하지 않은 연결"), ].iter().cloned().collect(); } \ No newline at end of file diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 6f6326121..251c349a2 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -314,5 +314,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scroll Style", "Styl przewijania"), ("Show Menubar", "Pokaż pasek menu"), ("Hide Menubar", "ukryj pasek menu"), + ("Direct Connection", "Bezpośrednie połączenie"), + ("Relay Connection", "Połączenie przekaźnika"), + ("Secure Connection", "Bezpieczne połączenie"), + ("Insecure Connection", "Niepewne połączenie"), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 2df6c63dc..fd4384767 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -310,5 +310,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scroll Style", "Estilo de rolagem"), ("Show Menubar", "Mostrar barra de menus"), ("Hide Menubar", "ocultar barra de menu"), + ("Direct Connection", "Conexão direta"), + ("Relay Connection", "Conexão de relé"), + ("Secure Connection", "Conexão segura"), + ("Insecure Connection", "Conexão insegura"), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index a0981f867..85eda60e6 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -313,5 +313,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scroll Style", ""), ("Show Menubar", ""), ("Hide Menubar", ""), + ("Direct Connection", ""), + ("Relay Connection", ""), + ("Secure Connection", ""), + ("Insecure Connection", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index eed658dfd..def560217 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -313,5 +313,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scroll Style", "Стиль прокрутки"), ("Show Menubar", "Показать строку меню"), ("Hide Menubar", "скрыть строку меню"), + ("Direct Connection", "Прямая связь"), + ("Relay Connection", "Релейное соединение"), + ("Secure Connection", "Безопасное соединение"), + ("Insecure Connection", "Небезопасное соединение"), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index b4e61e83f..4c04618aa 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -313,5 +313,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scroll Style", "Štýl posúvania"), ("Show Menubar", "Zobraziť panel s ponukami"), ("Hide Menubar", "skryť panel s ponukami"), + ("Direct Connection", "Priame pripojenie"), + ("Relay Connection", "Reléové pripojenie"), + ("Secure Connection", "Zabezpečené pripojenie"), + ("Insecure Connection", "Nezabezpečené pripojenie"), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 2e5c67cd8..081b7bf55 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -313,5 +313,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scroll Style", ""), ("Show Menubar", ""), ("Hide Menubar", ""), + ("Direct Connection", ""), + ("Relay Connection", ""), + ("Secure Connection", ""), + ("Insecure Connection", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 829659954..9738ed469 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -313,5 +313,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scroll Style", "Kaydırma Stili"), ("Show Menubar", "Menü çubuğunu göster"), ("Hide Menubar", "menü çubuğunu gizle"), + ("Direct Connection", "Doğrudan Bağlantı"), + ("Relay Connection", "Röle Bağlantısı"), + ("Secure Connection", "Güvenli bağlantı"), + ("Insecure Connection", "Güvenli Bağlantı"), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index f7d7cbe1d..46276dd2a 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -313,5 +313,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scroll Style", "滾動樣式"), ("Show Menubar", "顯示菜單欄"), ("Hide Menubar", "隱藏菜單欄"), + ("Direct Connection", "直接連接"), + ("Relay Connection", "中繼連接"), + ("Secure Connection", "安全連接"), + ("Insecure Connection", "非安全連接"), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 1c77139b3..474e57337 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -313,5 +313,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scroll Style", "Kiểu cuộn"), ("Show Menubar", "Hiển thị thanh menu"), ("Hide Menubar", "ẩn thanh menu"), + ("Direct Connection", "Kết nối trực tiếp"), + ("Relay Connection", "Kết nối chuyển tiếp"), + ("Secure Connection", "Kết nối an toàn"), + ("Insecure Connection", "Kết nối không an toàn"), ].iter().cloned().collect(); } From 4d914e9a01801d5c1909ffab942197b0a7e68fe8 Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 30 Aug 2022 17:20:25 +0800 Subject: [PATCH 0322/2015] flutter_desktop: remote menubar remove submenu Signed-off-by: fufesou --- flutter/lib/desktop/widgets/popup_menu.dart | 321 ++++++++++++------ .../lib/desktop/widgets/remote_menubar.dart | 11 +- flutter/pubspec.yaml | 5 +- 3 files changed, 219 insertions(+), 118 deletions(-) diff --git a/flutter/lib/desktop/widgets/popup_menu.dart b/flutter/lib/desktop/widgets/popup_menu.dart index 00f940fdb..3d5fdf7f6 100644 --- a/flutter/lib/desktop/widgets/popup_menu.dart +++ b/flutter/lib/desktop/widgets/popup_menu.dart @@ -97,15 +97,18 @@ class MenuConfig { } abstract class MenuEntryBase { - mod_menu.PopupMenuEntry build(BuildContext context, MenuConfig conf); + List> build(BuildContext context, MenuConfig conf); } class MenuEntryDivider extends MenuEntryBase { @override - mod_menu.PopupMenuEntry build(BuildContext context, MenuConfig conf) { - return mod_menu.PopupMenuDivider( - height: conf.dividerHeight, - ); + List> build( + BuildContext context, MenuConfig conf) { + return [ + mod_menu.PopupMenuDivider( + height: conf.dividerHeight, + ) + ]; } } @@ -113,6 +116,85 @@ typedef RadioOptionsGetter = List> Function(); typedef RadioCurOptionGetter = Future Function(); typedef RadioOptionSetter = Future Function(String); +class MenuEntryRadioUtils {} + +class MenuEntryRadios extends MenuEntryBase { + final String text; + final RadioOptionsGetter optionsGetter; + final RadioCurOptionGetter curOptionGetter; + final RadioOptionSetter optionSetter; + final RxString _curOption = "".obs; + + MenuEntryRadios( + {required this.text, + required this.optionsGetter, + required this.curOptionGetter, + required this.optionSetter}) { + () async { + _curOption.value = await curOptionGetter(); + }(); + } + + List> get options => optionsGetter(); + RxString get curOption => _curOption; + setOption(String option) async { + await optionSetter(option); + final opt = await curOptionGetter(); + if (_curOption.value != opt) { + _curOption.value = opt; + } + } + + mod_menu.PopupMenuEntry _buildMenuItem( + BuildContext context, MenuConfig conf, Tuple2 opt) { + return mod_menu.PopupMenuItem( + padding: EdgeInsets.zero, + height: conf.height, + child: TextButton( + child: Container( + alignment: AlignmentDirectional.centerStart, + constraints: BoxConstraints(minHeight: conf.height), + child: Row( + children: [ + Text( + opt.item1, + style: const TextStyle( + color: Colors.black, + fontSize: MenuConfig.fontSize, + fontWeight: FontWeight.normal), + ), + Expanded( + child: Align( + alignment: Alignment.centerRight, + child: SizedBox( + width: 20.0, + height: 20.0, + child: Obx(() => opt.item2 == curOption.value + ? Icon( + Icons.check, + color: conf.commonColor, + ) + : const SizedBox.shrink())), + )), + ], + ), + ), + onPressed: () { + if (opt.item2 != curOption.value) { + setOption(opt.item2); + } + }, + ), + ); + } + + @override + List> build( + BuildContext context, MenuConfig conf) { + return options.map((opt) => _buildMenuItem(context, conf, opt)).toList(); + } +} + class MenuEntrySubRadios extends MenuEntryBase { final String text; final RadioOptionsGetter optionsGetter; @@ -151,23 +233,26 @@ class MenuEntrySubRadios extends MenuEntryBase { constraints: BoxConstraints(minHeight: conf.height), child: Row( children: [ - SizedBox( - width: 20.0, - height: 20.0, - child: Obx(() => opt.item2 == curOption.value - ? Icon( - Icons.check, - color: conf.commonColor, - ) - : const SizedBox.shrink())), - const SizedBox(width: MenuConfig.midPadding), Text( opt.item1, style: const TextStyle( color: Colors.black, fontSize: MenuConfig.fontSize, fontWeight: FontWeight.normal), - ) + ), + Expanded( + child: Align( + alignment: Alignment.centerRight, + child: SizedBox( + width: 20.0, + height: 20.0, + child: Obx(() => opt.item2 == curOption.value + ? Icon( + Icons.check, + color: conf.commonColor, + ) + : const SizedBox.shrink())), + )), ], ), ), @@ -181,31 +266,34 @@ class MenuEntrySubRadios extends MenuEntryBase { } @override - mod_menu.PopupMenuEntry build(BuildContext context, MenuConfig conf) { - return PopupMenuChildrenItem( - padding: EdgeInsets.zero, - height: conf.height, - itemBuilder: (BuildContext context) => - options.map((opt) => _buildSecondMenu(context, conf, opt)).toList(), - child: Row(children: [ - const SizedBox(width: MenuConfig.midPadding), - Text( - text, - style: const TextStyle( - color: Colors.black, - fontSize: MenuConfig.fontSize, - fontWeight: FontWeight.normal), - ), - Expanded( - child: Align( - alignment: Alignment.centerRight, - child: Icon( - Icons.keyboard_arrow_right, - color: conf.commonColor, + List> build( + BuildContext context, MenuConfig conf) { + return [ + PopupMenuChildrenItem( + padding: EdgeInsets.zero, + height: conf.height, + itemBuilder: (BuildContext context) => + options.map((opt) => _buildSecondMenu(context, conf, opt)).toList(), + child: Row(children: [ + const SizedBox(width: MenuConfig.midPadding), + Text( + text, + style: const TextStyle( + color: Colors.black, + fontSize: MenuConfig.fontSize, + fontWeight: FontWeight.normal), ), - )) - ]), - ); + Expanded( + child: Align( + alignment: Alignment.centerRight, + child: Icon( + Icons.keyboard_arrow_right, + color: conf.commonColor, + ), + )) + ]), + ) + ]; } } @@ -221,35 +309,38 @@ abstract class MenuEntrySwitchBase extends MenuEntryBase { Future setOption(bool option); @override - mod_menu.PopupMenuEntry build(BuildContext context, MenuConfig conf) { - return mod_menu.PopupMenuItem( - padding: EdgeInsets.zero, - height: conf.height, - child: Obx( - () => SwitchListTile( - value: curOption.value, - onChanged: (v) { - setOption(v); - }, - title: Container( - alignment: AlignmentDirectional.centerStart, - constraints: BoxConstraints(minHeight: conf.height), - child: Text( - text, - style: const TextStyle( - color: Colors.black, - fontSize: MenuConfig.fontSize, - fontWeight: FontWeight.normal), - )), - dense: true, - visualDensity: const VisualDensity( - horizontal: VisualDensity.minimumDensity, - vertical: VisualDensity.minimumDensity, + List> build( + BuildContext context, MenuConfig conf) { + return [ + mod_menu.PopupMenuItem( + padding: EdgeInsets.zero, + height: conf.height, + child: Obx( + () => SwitchListTile( + value: curOption.value, + onChanged: (v) { + setOption(v); + }, + title: Container( + alignment: AlignmentDirectional.centerStart, + constraints: BoxConstraints(minHeight: conf.height), + child: Text( + text, + style: const TextStyle( + color: Colors.black, + fontSize: MenuConfig.fontSize, + fontWeight: FontWeight.normal), + )), + dense: true, + visualDensity: const VisualDensity( + horizontal: VisualDensity.minimumDensity, + vertical: VisualDensity.minimumDensity, + ), + contentPadding: const EdgeInsets.only(left: 8.0), ), - contentPadding: const EdgeInsets.only(left: 8.0), ), - ), - ); + ) + ]; } } @@ -307,32 +398,37 @@ class MenuEntrySubMenu extends MenuEntryBase { }); @override - mod_menu.PopupMenuEntry build(BuildContext context, MenuConfig conf) { - return PopupMenuChildrenItem( - height: conf.height, - padding: EdgeInsets.zero, - position: mod_menu.PopupMenuPosition.overSide, - itemBuilder: (BuildContext context) => - entries.map((entry) => entry.build(context, conf)).toList(), - child: Row(children: [ - const SizedBox(width: MenuConfig.midPadding), - Text( - text, - style: const TextStyle( - color: Colors.black, - fontSize: MenuConfig.fontSize, - fontWeight: FontWeight.normal), - ), - Expanded( - child: Align( - alignment: Alignment.centerRight, - child: Icon( - Icons.keyboard_arrow_right, - color: conf.commonColor, + List> build( + BuildContext context, MenuConfig conf) { + return [ + PopupMenuChildrenItem( + height: conf.height, + padding: EdgeInsets.zero, + position: mod_menu.PopupMenuPosition.overSide, + itemBuilder: (BuildContext context) => entries + .map((entry) => entry.build(context, conf)) + .expand((i) => i) + .toList(), + child: Row(children: [ + const SizedBox(width: MenuConfig.midPadding), + Text( + text, + style: const TextStyle( + color: Colors.black, + fontSize: MenuConfig.fontSize, + fontWeight: FontWeight.normal), ), - )) - ]), - ); + Expanded( + child: Align( + alignment: Alignment.centerRight, + child: Icon( + Icons.keyboard_arrow_right, + color: conf.commonColor, + ), + )) + ]), + ) + ]; } } @@ -346,24 +442,27 @@ class MenuEntryButton extends MenuEntryBase { }); @override - mod_menu.PopupMenuEntry build(BuildContext context, MenuConfig conf) { - return mod_menu.PopupMenuItem( - padding: EdgeInsets.zero, - height: conf.height, - child: TextButton( - child: Container( - alignment: AlignmentDirectional.centerStart, - constraints: BoxConstraints(minHeight: conf.height), - child: childBuilder( - const TextStyle( - color: Colors.black, - fontSize: MenuConfig.fontSize, - fontWeight: FontWeight.normal), - )), - onPressed: () { - proc(); - }, - ), - ); + List> build( + BuildContext context, MenuConfig conf) { + return [ + mod_menu.PopupMenuItem( + padding: EdgeInsets.zero, + height: conf.height, + child: TextButton( + child: Container( + alignment: AlignmentDirectional.centerStart, + constraints: BoxConstraints(minHeight: conf.height), + child: childBuilder( + const TextStyle( + color: Colors.black, + fontSize: MenuConfig.fontSize, + fontWeight: FontWeight.normal), + )), + onPressed: () { + proc(); + }, + ), + ) + ]; } } diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 620f5f226..0e931dd71 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -236,6 +236,7 @@ class _RemoteMenubarState extends State { height: _MenubarTheme.height, dividerHeight: _MenubarTheme.dividerHeight, ))) + .expand((i) => i) .toList(), ); } @@ -258,6 +259,7 @@ class _RemoteMenubarState extends State { height: _MenubarTheme.height, dividerHeight: _MenubarTheme.dividerHeight, ))) + .expand((i) => i) .toList(), ); } @@ -401,7 +403,7 @@ class _RemoteMenubarState extends State { List> _getDisplayMenu() { final displayMenu = [ - MenuEntrySubRadios( + MenuEntryRadios( text: translate('Ratio'), optionsGetter: () => [ Tuple2(translate('Original'), 'original'), @@ -418,7 +420,8 @@ class _RemoteMenubarState extends State { id: widget.id, name: "view-style", value: v); widget.ffi.canvasModel.updateViewStyle(); }), - MenuEntrySubRadios( + MenuEntryDivider(), + MenuEntryRadios( text: translate('Scroll Style'), optionsGetter: () => [ Tuple2(translate('ScrollAuto'), 'scrollauto'), @@ -434,7 +437,8 @@ class _RemoteMenubarState extends State { id: widget.id, name: "scroll-style", value: v); widget.ffi.canvasModel.updateScrollStyle(); }), - MenuEntrySubRadios( + MenuEntryDivider(), + MenuEntryRadios( text: translate('Image Quality'), optionsGetter: () => [ Tuple2(translate('Good image quality'), 'best'), @@ -451,6 +455,7 @@ class _RemoteMenubarState extends State { optionSetter: (String v) async { await bind.sessionSetImageQuality(id: widget.id, value: v); }), + MenuEntryDivider(), MenuEntrySwitch( text: translate('Show remote cursor'), getter: () async { diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index fc59b8bd3..a35f1c872 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -40,10 +40,7 @@ dependencies: url_launcher: ^6.0.9 shared_preferences: ^2.0.6 toggle_switch: ^1.4.0 - dash_chat_2: - git: - url: https://github.com/fufesou/Dash-Chat-2 - ref: feat_maxWidth + dash_chat_2: ^0.0.14 draggable_float_widget: ^0.0.2 settings_ui: ^2.0.2 flutter_breadcrumb: ^1.0.1 From b7ce85e0626e40e8b1c29cfa0e415ca2a6435ce9 Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 30 Aug 2022 21:15:35 +0800 Subject: [PATCH 0323/2015] flutter_deskop: sync session add, mid commit Signed-off-by: fufesou --- flutter/lib/models/model.dart | 4 +- src/client.rs | 2 +- src/flutter.rs | 69 +++++++++++++++++++++-------------- src/flutter_ffi.rs | 27 +++++++++----- 4 files changed, 63 insertions(+), 39 deletions(-) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 4a54ba4e8..171a41dfa 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -1070,8 +1070,10 @@ class FFI { imageModel._id = id; cursorModel.id = id; } - final stream = bind.sessionConnect( + // ignore: unused_local_variable + final addRes = bind.sessionAddSync( id: id, isFileTransfer: isFileTransfer, isPortForward: isPortForward); + final stream = bind.sessionStart(id: id); final cb = ffiModel.startEventListener(id); () async { await for (final message in stream) { diff --git a/src/client.rs b/src/client.rs index d7f0bf4fa..0bc69a7c1 100644 --- a/src/client.rs +++ b/src/client.rs @@ -847,7 +847,7 @@ impl VideoHandler { pub struct LoginConfigHandler { id: String, pub is_file_transfer: bool, - is_port_forward: bool, + pub is_port_forward: bool, hash: Hash, password: Vec, // remember password for reconnect pub remember: bool, diff --git a/src/flutter.rs b/src/flutter.rs index 7192c0fdf..60650aa9b 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -8,16 +8,13 @@ use std::{ use flutter_rust_bridge::{StreamSink, ZeroCopyBuffer}; -use hbb_common::config::{PeerConfig, TransferSerde}; -use hbb_common::fs::get_job; use hbb_common::{ - allow_err, + allow_err, bail, compress::decompress, - config::{Config, LocalConfig}, - fs, + config::{Config, LocalConfig, PeerConfig, TransferSerde}, fs::{ - can_enable_overwrite_detection, get_string, new_send_confirm, transform_windows_path, - DigestCheckResult, + self, can_enable_overwrite_detection, get_job, get_string, new_send_confirm, + transform_windows_path, DigestCheckResult, }, log, message_proto::*, @@ -28,7 +25,7 @@ use hbb_common::{ sync::mpsc, time::{self, Duration, Instant, Interval}, }, - Stream, + ResultType, Stream, }; use crate::common::{self, make_fd_to_json, CLIPBOARD_INTERVAL}; @@ -60,7 +57,7 @@ pub struct Session { id: String, sender: Arc>>>, // UI to rust lc: Arc>, - events2ui: Arc>>, + events2ui: Arc>>>, } impl Session { @@ -71,23 +68,17 @@ impl Session { /// * `id` - The identifier of the remote session with prefix. Regex: [\w]*[\_]*[\d]+ /// * `is_file_transfer` - If the session is used for file transfer. /// * `is_port_forward` - If the session is used for port forward. - pub fn start( - identifier: &str, - is_file_transfer: bool, - is_port_forward: bool, - events2ui: StreamSink, - ) { + pub fn add(id: &str, is_file_transfer: bool, is_port_forward: bool) -> ResultType<()> { // TODO check same id - let session_id = get_session_id(identifier.to_owned()); + let session_id = get_session_id(id.to_owned()); LocalConfig::set_remote_id(&session_id); // TODO close // Self::close(); - let events2ui = Arc::new(RwLock::new(events2ui)); let session = Session { id: session_id.clone(), sender: Default::default(), lc: Default::default(), - events2ui, + events2ui: Arc::new(RwLock::new(None)), }; session.lc.write().unwrap().initialize( session_id.clone(), @@ -97,10 +88,29 @@ impl Session { SESSIONS .write() .unwrap() - .insert(identifier.to_owned(), session.clone()); - std::thread::spawn(move || { - Connection::start(session, is_file_transfer, is_port_forward); - }); + .insert(id.to_owned(), session.clone()); + Ok(()) + } + + /// Create a new remote session with the given id. + /// + /// # Arguments + /// + /// * `id` - The identifier of the remote session with prefix. Regex: [\w]*[\_]*[\d]+ + /// * `events2ui` - The events channel to ui. + pub fn start(id: &str, events2ui: StreamSink) -> ResultType<()> { + if let Some(session) = SESSIONS.write().unwrap().get_mut(id) { + *session.events2ui.write().unwrap() = Some(events2ui); + let session = session.clone(); + std::thread::spawn(move || { + let is_file_transfer = session.lc.read().unwrap().is_file_transfer; + let is_port_forward = session.lc.read().unwrap().is_port_forward; + Connection::start(session, is_file_transfer, is_port_forward); + }); + Ok(()) + } else { + bail!("No session with peer id {}", id) + } } /// Get the current session instance. @@ -305,7 +315,9 @@ impl Session { assert!(h.get("name").is_none()); h.insert("name", name); let out = serde_json::ser::to_string(&h).unwrap_or("".to_owned()); - self.events2ui.read().unwrap().add(EventToUI::Event(out)); + if let Some(stream) = &*self.events2ui.read().unwrap() { + stream.add(EventToUI::Event(out)); + } } /// Get platform of peer. @@ -998,11 +1010,12 @@ impl Connection { }) }; if let Ok(true) = self.video_handler.handle_frame(vf) { - let stream = self.session.events2ui.read().unwrap(); - self.frame_count.fetch_add(1, Ordering::Relaxed); - stream.add(EventToUI::Rgba(ZeroCopyBuffer( - self.video_handler.rgb.clone(), - ))); + if let Some(stream) = &*self.session.events2ui.read().unwrap() { + self.frame_count.fetch_add(1, Ordering::Relaxed); + stream.add(EventToUI::Rgba(ZeroCopyBuffer( + self.video_handler.rgb.clone(), + ))); + } } } Some(message::Union::Hash(hash)) => { diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index dd147bb77..db8030782 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -107,14 +107,18 @@ pub fn host_stop_system_key_propagate(stopped: bool) { crate::platform::windows::stop_system_key_propagate(stopped); } -pub fn session_connect( - events2ui: StreamSink, - id: String, - is_file_transfer: bool, - is_port_forward: bool, -) -> ResultType<()> { - Session::start(&id, is_file_transfer, is_port_forward, events2ui); - Ok(()) +// FIXME: -> ResultType<()> cannot be parsed by frb_codegen +// thread 'main' panicked at 'Failed to parse function output type `ResultType<()>`', $HOME\.cargo\git\checkouts\flutter_rust_bridge-ddba876d3ebb2a1e\e5adce5\frb_codegen\src\parser\mod.rs:151:25 +pub fn session_add_sync(id: String, is_file_transfer: bool, is_port_forward: bool) -> SyncReturn { + if let Err(e) = Session::add(&id, is_file_transfer, is_port_forward) { + SyncReturn(format!("Failed to add session with id {}, {}", &id, e)) + } else { + SyncReturn("".to_owned()) + } +} + +pub fn session_start(events2ui: StreamSink, id: String) -> ResultType<()> { + Session::start(&id, events2ui) } pub fn session_get_remember(id: String) -> Option { @@ -602,7 +606,12 @@ pub fn main_load_lan_peers() { }; } -pub fn session_add_port_forward(id: String, local_port: i32, remote_host: String, remote_port: i32) { +pub fn session_add_port_forward( + id: String, + local_port: i32, + remote_host: String, + remote_port: i32, +) { if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) { session.add_port_forward(local_port, remote_host, remote_port); } From a621ac0884fb843ab3052d5394ccdbfbf00b8a0d Mon Sep 17 00:00:00 2001 From: Heizi <35397814+ElisaMin@users.noreply.github.com> Date: Wed, 31 Aug 2022 02:49:19 +0800 Subject: [PATCH 0324/2015] =?UTF-8?q?Translate=20Germany=20to=20"=E5=BE=B7?= =?UTF-8?q?=E5=9B=BD"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README-ZH.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README-ZH.md b/README-ZH.md index e17254670..eaf990674 100644 --- a/README-ZH.md +++ b/README-ZH.md @@ -27,8 +27,8 @@ Chat with us: [知乎](https://www.zhihu.com/people/rustdesk) | [Discord](https: - 首尔, AWS lightsail, 1 VCPU/0.5G RAM - 新加坡, Vultr, 1 VCPU/1G RAM - 达拉斯, Vultr, 1 VCPU/1G RAM -- Germany, Codext, 2 VCPU / 4GB RAM -- Germany, Hetzner, 4 VCPU / 8GB RAM +- 德国, Codext, 2 VCPU / 4GB RAM +- 德国, Hetzner, 4 VCPU / 8GB RAM ## 依赖 From d8497e43d2e05b4cf9bc971619ea99e2bb176c42 Mon Sep 17 00:00:00 2001 From: Heizi <35397814+ElisaMin@users.noreply.github.com> Date: Wed, 31 Aug 2022 05:11:14 +0800 Subject: [PATCH 0325/2015] Update README-ZH.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 重新排版 + 地道国语化 (Retype & Authentic Chinese) --- README-ZH.md | 118 ++++++++++++++++++++++++++------------------------- 1 file changed, 60 insertions(+), 58 deletions(-) diff --git a/README-ZH.md b/README-ZH.md index eaf990674..dffb9d82e 100644 --- a/README-ZH.md +++ b/README-ZH.md @@ -20,9 +20,9 @@ Chat with us: [知乎](https://www.zhihu.com/people/rustdesk) | [Discord](https: [**可执行程序下载**](https://github.com/rustdesk/rustdesk/releases) -## 免费公共服务器 +## 免费的公共服务器 -以下是您免费使用的服务器,它可能会随着时间的推移而变化。如果您不靠近其中之一,您的网络可能会很慢。 +以下是您可以使用的、免费的、会随时更新的公共服务器列表,在国内也许网速会很慢或者无法访问。 - 首尔, AWS lightsail, 1 VCPU/0.5G RAM - 新加坡, Vultr, 1 VCPU/1G RAM @@ -113,100 +113,102 @@ cargo run ### 把 Wayland 修改成 X11 (Xorg) -RustDesk 暂时不支持 Wayland,不过正在积极开发中. -请查看[this](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/)配置 X11. +RustDesk 暂时不支持 Wayland,不过正在积极开发中。 +> [点我](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) +查看 如何将Xorg设置成默认的GNOME session ## 使用 Docker 编译 -首先克隆存储库并构建 docker 容器: +### 构建Docker容器 ```sh -git clone https://github.com/rustdesk/rustdesk -cd rustdesk -docker build -t "rustdesk-builder" . +git clone https://github.com/rustdesk/rustdesk # 克隆Github存储库 +cd rustdesk # 进入文件夹 +docker build -t "rustdesk-builder" . # 构建容器 ``` +请注意: +* 针对国内网络访问问题,可以做以下几点优化: + 1. Dockerfile 中修改系统的源到国内镜像 + ``` + 在Dockerfile的RUN apt update之前插入两行: + + RUN sed -i "s/deb.debian.org/mirrors.163.com/g" /etc/apt/sources.list + RUN sed -i "s/security.debian.org/mirrors.163.com/g" /etc/apt/sources.list + ``` -针对国内网络访问问题,可以做以下几点优化: + 2. 修改容器系统中的 cargo 源,在`RUN ./rustup.sh -y`后插入下面代码: -1. Dockerfile 中修改系统的源到国内镜像 + ``` + RUN echo '[source.crates-io]' > ~/.cargo/config \ + && echo 'registry = "https://github.com/rust-lang/crates.io-index"' >> ~/.cargo/config \ + && echo '# 替换成你偏好的镜像源' >> ~/.cargo/config \ + && echo "replace-with = 'sjtu'" >> ~/.cargo/config \ + && echo '# 上海交通大学' >> ~/.cargo/config \ + && echo '[source.sjtu]' >> ~/.cargo/config \ + && echo 'registry = "https://mirrors.sjtug.sjtu.edu.cn/git/crates.io-index"' >> ~/.cargo/config \ + && echo '' >> ~/.cargo/config + ``` - ``` - 在Dockerfile的RUN apt update之前插入两行: + 3. Dockerfile 中加入代理的 env - RUN sed -i "s/deb.debian.org/mirrors.163.com/g" /etc/apt/sources.list - RUN sed -i "s/security.debian.org/mirrors.163.com/g" /etc/apt/sources.list - ``` + ``` + 在User root后插入两行 -2. 修改容器系统中的 cargo 源,在`RUN ./rustup.sh -y`后插入下面代码: + ENV http_proxy=http://host:port + ENV https_proxy=http://host:port + ``` - ``` - RUN echo '[source.crates-io]' > ~/.cargo/config \ - && echo 'registry = "https://github.com/rust-lang/crates.io-index"' >> ~/.cargo/config \ - && echo '# 替换成你偏好的镜像源' >> ~/.cargo/config \ - && echo "replace-with = 'sjtu'" >> ~/.cargo/config \ - && echo '# 上海交通大学' >> ~/.cargo/config \ - && echo '[source.sjtu]' >> ~/.cargo/config \ - && echo 'registry = "https://mirrors.sjtug.sjtu.edu.cn/git/crates.io-index"' >> ~/.cargo/config \ - && echo '' >> ~/.cargo/config - ``` + 4. docker build 命令后面加上 proxy 参数 -3. Dockerfile 中加入代理的 env + ``` + docker build -t "rustdesk-builder" . --build-arg http_proxy=http://host:port --build-arg https_proxy=http://host:port + ``` - ``` - 在User root后插入两行 - - ENV http_proxy=http://host:port - ENV https_proxy=http://host:port - ``` - -4. docker build 命令后面加上 proxy 参数 - - ``` - docker build -t "rustdesk-builder" . --build-arg http_proxy=http://host:port --build-arg https_proxy=http://host:port - ``` - -然后,每次需要构建应用程序时,运行以下命令: +### 构建RustDesk程序 +容器构建完成后,运行下列指令以完成对RustDesk应用程序的构建: ```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 ``` -运行若遇到无权限问题,出现以下提示: +请注意: +* 因为需要缓存依赖项,首次构建一般很慢(国内网络会经常出现拉取失败,可以多试几次)。 +* 如果您需要添加不同的构建参数,可以在指令末尾的`` 位置进行修改。例如构建一个"Release"版本,在指令后面加上` --release`即可。 +* 如果出现以下的提示,则是无权限问题,可以尝试把`-e PUID="$(id -u)" -e PGID="$(id -g)"`参数去掉。 + ``` + usermod: user user is currently used by process 1 + groupmod: Permission denied. + groupmod: cannot lock /etc/group; try again later. + ``` + > **原因:** 容器的entrypoint脚本会检测UID和GID,在度判和给定的环境变量的不一致时,会强行修改user的UID和GID并重新运行。但在重启后读不到环境中的UID和GID,然后再次进入判错重启环节 -``` -usermod: user user is currently used by process 1 -groupmod: Permission denied. -groupmod: cannot lock /etc/group; try again later. -``` -可以尝试把`-e PUID="$(id -u)" -e PGID="$(id -g)"`参数去掉。(出现这一问题的原因是容器中的 entrypoint 脚本中判定 uid 和 gid 与给定的环境变量不一致时会修改 user 的 uid 和 gid 重新运行,但是重新运行时取不到环境变量中的 uid 和 gid 了,会再次进入 uid 与 gid 与给定值不一致的逻辑分支) - -请注意,第一次构建可能需要比较长的时间,因为需要缓存依赖项(国内网络经常出现拉取失败,可多尝试几次),后续构建会更快。此外,如果您需要为构建命令指定不同的参数, -您可以在命令末尾的 `` 位置执行此操作。例如,如果你想构建一个优化的发布版本,你可以在命令后跟 `--release`。 -将在 target 下产生可执行程序,请通过以下方式运行调试版本: +### 运行RustDesk程序 +生成的可执行程序在target目录下,可直接通过指令运行调试(Debug)版本的RustDesk: ```sh target/debug/rustdesk ``` -或者运行发布版本: +或者您想运行发行(Release)版本: ```sh target/release/rustdesk ``` -请确保您从 RustDesk 存储库的根目录运行这些命令,否则应用程序可能无法找到所需的资源。另请注意,此方法当前不支持其他`Cargo`子命令, -例如 `install` 或 `run`,因为运行在容器里,而不是宿主机上。 +请注意: +* 请保证您运行的目录是在RustDesk库的根目录内,否则软件会读不到文件。 +* `install`、`run`等Cargo的子指令在容器内不可用,宿主机才行。 ## 文件结构 - **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: 视频编解码, 配置, tcp/udp 封装, protobuf, 文件传输相关文件系统操作函数, 以及一些其他实用函数 -- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: 截屏 +- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: 屏幕截取 - **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: 平台相关的鼠标键盘输入 - **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI -- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: 被控端服务,audio/clipboard/input/video 服务, 以及连接的实现 +- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: 被控端服务音频、剪切板、输入、视频服务、网络连接的实现 - **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: 控制端 -- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: 与[rustdesk-server](https://github.com/rustdesk/rustdesk-server)保持 UDP 通讯, 等待远程连接(通过打洞直连或者中继) +- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: 与[rustdesk-server](https://github.com/rustdesk/rustdesk-server)保持UDP通讯, 等待远程连接(通过打洞直连或者中继) - **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: 平台服务相关代码 - **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: 移动版本的Flutter代码 - **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Flutter Web版本中的Javascript代码 From 7fce02e68822ed1293c70591d4039bf2b08ac1fa Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 29 Aug 2022 19:28:00 +0800 Subject: [PATCH 0326/2015] fix: not use fixed button width Signed-off-by: 21pages --- flutter/lib/desktop/pages/connection_page.dart | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 29219df2a..d366d06ec 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -233,7 +233,6 @@ class _ConnectionPageState extends State { }, child: Container( height: 24, - width: 72, alignment: Alignment.center, decoration: BoxDecoration( color: ftPressed.value @@ -257,7 +256,7 @@ class _ConnectionPageState extends State { color: ftPressed.value ? MyTheme.color(context).bg : MyTheme.color(context).text), - ), + ).marginSymmetric(horizontal: 12), ), )), SizedBox( @@ -272,7 +271,6 @@ class _ConnectionPageState extends State { onTap: onConnect, child: Container( height: 24, - width: 65, decoration: BoxDecoration( color: connPressed.value ? MyTheme.accent @@ -289,12 +287,12 @@ class _ConnectionPageState extends State { child: Center( child: Text( translate( - "Connection", + "Connect", ), style: TextStyle( fontSize: 12, color: MyTheme.color(context).bg), ), - ), + ).marginSymmetric(horizontal: 12), ), ), ), From 839be76b8f890919be69c36a8ae56f5672508df8 Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 30 Aug 2022 14:43:57 +0800 Subject: [PATCH 0327/2015] tabbar: check before scroll Signed-off-by: 21pages --- flutter/lib/desktop/widgets/tabbar_widget.dart | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 6e0ce747d..aea90c868 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -64,6 +64,7 @@ class DesktopTabController { state.update((val) { val!.tabs.add(tab); }); + state.value.scrollController.itemCount = state.value.tabs.length; toIndex = state.value.tabs.length - 1; assert(toIndex >= 0); } @@ -96,8 +97,16 @@ class DesktopTabController { void jumpTo(int index) { state.update((val) { val!.selected = index; - val.pageController.jumpToPage(index); - val.scrollController.scrollToItem(index, center: true, animate: true); + Future.delayed(Duration.zero, (() { + if (val.pageController.hasClients) { + val.pageController.jumpToPage(index); + } + if (val.scrollController.hasClients && + val.scrollController.canScroll && + val.scrollController.itemCount >= index) { + val.scrollController.scrollToItem(index, center: true, animate: true); + } + })); }); onSelected?.call(index); } From 38abd273840723bd7caee72b201c8444283f9a18 Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 30 Aug 2022 16:50:25 +0800 Subject: [PATCH 0328/2015] impl option remote modification Signed-off-by: 21pages --- flutter/lib/common.dart | 2 - .../desktop/pages/connection_tab_page.dart | 2 +- .../lib/desktop/pages/desktop_tab_page.dart | 2 +- .../desktop/pages/file_manager_tab_page.dart | 6 +- .../desktop/pages/port_forward_tab_page.dart | 10 +-- flutter/lib/desktop/pages/server_page.dart | 26 ++++++-- .../lib/desktop/widgets/tabbar_widget.dart | 62 +++++++++++++++++-- flutter/lib/main.dart | 9 ++- src/flutter_ffi.rs | 23 ++++--- 9 files changed, 109 insertions(+), 33 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index b991c7a96..6ba917bf3 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -664,8 +664,6 @@ Future initGlobalFFI() async { debugPrint("_globalFFI init end"); // after `put`, can also be globally found by Get.find(); Get.put(_globalFFI, permanent: true); - // trigger connection status updater - await bind.mainCheckConnectStatus(); // global shared preference await Get.putAsync(() => SharedPreferences.getInstance()); } diff --git a/flutter/lib/desktop/pages/connection_tab_page.dart b/flutter/lib/desktop/pages/connection_tab_page.dart index 1b9e04ebf..4175bd11b 100644 --- a/flutter/lib/desktop/pages/connection_tab_page.dart +++ b/flutter/lib/desktop/pages/connection_tab_page.dart @@ -93,7 +93,7 @@ class _ConnectionTabPageState extends State { body: Obx(() => DesktopTab( controller: tabController, theme: theme, - isMainWindow: false, + tabType: DesktopTabType.remoteScreen, showTabBar: fullscreen.isFalse, onClose: () { tabController.clear(); diff --git a/flutter/lib/desktop/pages/desktop_tab_page.dart b/flutter/lib/desktop/pages/desktop_tab_page.dart index a7a93d7ad..57ee43e14 100644 --- a/flutter/lib/desktop/pages/desktop_tab_page.dart +++ b/flutter/lib/desktop/pages/desktop_tab_page.dart @@ -46,7 +46,7 @@ class _DesktopTabPageState extends State { body: DesktopTab( controller: tabController, theme: dark ? TarBarTheme.dark() : TarBarTheme.light(), - isMainWindow: true, + tabType: DesktopTabType.main, tail: ActionIcon( message: 'Settings', icon: IconFont.menu, diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index e7f08a516..6c8b58a30 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -37,7 +37,7 @@ class _FileManagerTabPageState extends State { @override void initState() { super.initState(); - + tabController.onRemove = (_, id) => onRemoveId(id); rustDeskWinManager.setMethodHandler((call, fromWindowId) async { @@ -74,9 +74,9 @@ class _FileManagerTabPageState extends State { body: DesktopTab( controller: tabController, theme: theme, - isMainWindow: false, + tabType: DesktopTabType.fileTransfer, onClose: () { - tabController.clear(); + tabController.clear(); }, tail: AddButton( theme: theme, diff --git a/flutter/lib/desktop/pages/port_forward_tab_page.dart b/flutter/lib/desktop/pages/port_forward_tab_page.dart index 8db4c7f98..1e2c8e2bc 100644 --- a/flutter/lib/desktop/pages/port_forward_tab_page.dart +++ b/flutter/lib/desktop/pages/port_forward_tab_page.dart @@ -19,11 +19,13 @@ class PortForwardTabPage extends StatefulWidget { class _PortForwardTabPageState extends State { final tabController = Get.put(DesktopTabController()); + late final bool isRDP; - static final IconData selectedIcon = Icons.forward_sharp; - static final IconData unselectedIcon = Icons.forward_outlined; + static const IconData selectedIcon = Icons.forward_sharp; + static const IconData unselectedIcon = Icons.forward_outlined; _PortForwardTabPageState(Map params) { + isRDP = params['isRDP']; tabController.add(TabInfo( key: params['id'], label: params['id'], @@ -32,7 +34,7 @@ class _PortForwardTabPageState extends State { page: PortForwardPage( key: ValueKey(params['id']), id: params['id'], - isRDP: params['isRDP'], + isRDP: isRDP, ))); } @@ -76,7 +78,7 @@ class _PortForwardTabPageState extends State { body: DesktopTab( controller: tabController, theme: theme, - isMainWindow: false, + tabType: isRDP ? DesktopTabType.rdp : DesktopTabType.portForward, onClose: () { tabController.clear(); }, diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index d96efc710..e7922403b 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -111,7 +111,7 @@ class ConnectionManagerState extends State { showMaximize: false, showMinimize: false, controller: serverModel.tabController, - isMainWindow: true, + tabType: DesktopTabType.cm, pageViewBuilder: (pageView) => Row(children: [ Expanded(child: pageView), Consumer( @@ -294,7 +294,8 @@ class _CmHeaderState extends State<_CmHeader> Offstage( offstage: client.isFileTransfer, child: IconButton( - onPressed: () => gFFI.chatModel.toggleCMChatPage(client.id), + onPressed: () => checkClickTime( + client.id, () => gFFI.chatModel.toggleCMChatPage(client.id)), icon: Icon(Icons.message_outlined), ), ) @@ -326,7 +327,8 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> { BoxDecoration(color: enabled ? MyTheme.accent80 : Colors.grey), padding: EdgeInsets.all(4.0), child: InkWell( - onTap: () => onTap?.call(!enabled), + onTap: () => + checkClickTime(widget.client.id, () => onTap?.call(!enabled)), child: Image( image: icon, width: 50, @@ -422,7 +424,8 @@ class _CmControlPanel extends StatelessWidget { decoration: BoxDecoration( color: Colors.redAccent, borderRadius: BorderRadius.circular(10)), child: InkWell( - onTap: () => handleDisconnect(context), + onTap: () => + checkClickTime(client.id, () => handleDisconnect(context)), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -447,7 +450,8 @@ class _CmControlPanel extends StatelessWidget { decoration: BoxDecoration( color: MyTheme.accent, borderRadius: BorderRadius.circular(10)), child: InkWell( - onTap: () => handleAccept(context), + onTap: () => + checkClickTime(client.id, () => handleAccept(context)), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -469,7 +473,8 @@ class _CmControlPanel extends StatelessWidget { borderRadius: BorderRadius.circular(10), border: Border.all(color: Colors.grey)), child: InkWell( - onTap: () => handleDisconnect(context), + onTap: () => + checkClickTime(client.id, () => handleDisconnect(context)), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -572,3 +577,12 @@ Widget clientInfo(Client client) { ), ])); } + +void checkClickTime(int id, Function() callback) async { + var clickCallbackTime = DateTime.now().millisecondsSinceEpoch; + await bind.cmCheckClickTime(connId: id); + Timer(const Duration(milliseconds: 120), () async { + var d = clickCallbackTime - await bind.cmGetClickTime(); + if (d > 120) callback(); + }); +} diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index aea90c868..a89d0da38 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'dart:async'; import 'dart:math'; import 'package:desktop_multi_window/desktop_multi_window.dart'; @@ -6,6 +7,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/main.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; import 'package:get/get.dart'; import 'package:scroll_pos/scroll_pos.dart'; import 'package:window_manager/window_manager.dart'; @@ -34,6 +36,15 @@ class TabInfo { required this.page}); } +enum DesktopTabType { + main, + cm, + remoteScreen, + fileTransfer, + portForward, + rdp, +} + class DesktopTabState { final List tabs = []; final ScrollPosController scrollController = @@ -143,6 +154,7 @@ typedef LabelGetter = Rx Function(String key); class DesktopTab extends StatelessWidget { final Function(String)? onTabClose; final TarBarTheme theme; + final DesktopTabType tabType; final bool isMainWindow; final bool showTabBar; final bool showLogo; @@ -161,7 +173,7 @@ class DesktopTab extends StatelessWidget { const DesktopTab({ required this.controller, - required this.isMainWindow, + required this.tabType, this.theme = const TarBarTheme.light(), this.onTabClose, this.showTabBar = true, @@ -175,7 +187,8 @@ class DesktopTab extends StatelessWidget { this.onClose, this.tabBuilder, this.labelGetter, - }); + }) : isMainWindow = + tabType == DesktopTabType.main || tabType == DesktopTabType.cm; @override Widget build(BuildContext context) { @@ -204,11 +217,48 @@ class DesktopTab extends StatelessWidget { ]); } + Widget _buildBlock({required Widget child}) { + if (tabType != DesktopTabType.main) { + return child; + } + var block = false.obs; + return Obx(() => MouseRegion( + onEnter: (_) async { + if (!option2bool( + 'allow-remote-config-modification', + await bind.mainGetOption( + key: 'allow-remote-config-modification'))) { + var time0 = DateTime.now().millisecondsSinceEpoch; + await bind.mainCheckMouseTime(); + Timer(const Duration(milliseconds: 120), () async { + var d = time0 - await bind.mainGetMouseTime(); + if (d < 120) { + block.value = true; + } + }); + } + }, + onExit: (_) => block.value = false, + child: Stack( + children: [ + child, + Offstage( + offstage: !block.value, + child: Container( + color: Colors.black.withOpacity(0.5), + )), + ], + ), + )); + } + Widget _buildPageView() { - return Obx(() => PageView( - controller: state.value.pageController, - children: - state.value.tabs.map((tab) => tab.page).toList(growable: false))); + return _buildBlock( + child: Obx(() => PageView( + controller: state.value.pageController, + children: state.value.tabs + .map((tab) => tab.page) + .toList(growable: false)))); } Widget _buildBar() { diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index efca88180..e1c254942 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -81,6 +81,8 @@ Future initEnv(String appType) async { void runMainApp(bool startService) async { await initEnv(kAppTypeMain); + // trigger connection status updater + await bind.mainCheckConnectStatus(); if (startService) { // await windowManager.ensureInitialized(); // disable tray @@ -89,10 +91,11 @@ void runMainApp(bool startService) async { } runApp(App()); // set window option - WindowOptions windowOptions = getHiddenTitleBarWindowOptions(const Size(1280, 720)); + WindowOptions windowOptions = + getHiddenTitleBarWindowOptions(const Size(1280, 720)); windowManager.waitUntilReadyToShow(windowOptions, () async { - await windowManager.show(); - await windowManager.focus(); + await windowManager.show(); + await windowManager.focus(); }); } diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index db8030782..a17b3d695 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -22,12 +22,13 @@ use crate::ui_interface; #[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::ui_interface::{change_id, check_connect_status, is_ok_change_id}; use crate::ui_interface::{ - check_super_user_permission, discover, forget_password, get_api_server, get_app_name, - get_async_job_status, get_connect_status, get_fav, get_id, get_lan_peers, get_langs, - get_license, get_local_option, get_option, get_options, get_peer, get_peer_option, get_socks, - get_sound_inputs, get_uuid, get_version, has_hwcodec, has_rendezvous_service, post_request, - set_local_option, set_option, set_options, set_peer_option, set_permanent_password, set_socks, - store_fav, test_if_valid_server, update_temporary_password, using_public_server, + check_mouse_time, check_super_user_permission, discover, forget_password, get_api_server, + get_app_name, get_async_job_status, get_connect_status, get_fav, get_id, get_lan_peers, + get_langs, get_license, get_local_option, get_mouse_time, get_option, get_options, get_peer, + get_peer_option, get_socks, get_sound_inputs, get_uuid, get_version, has_hwcodec, + has_rendezvous_service, post_request, set_local_option, set_option, set_options, + set_peer_option, set_permanent_password, set_socks, store_fav, test_if_valid_server, + update_temporary_password, using_public_server, }; fn initialize(app_dir: &str) { @@ -472,7 +473,7 @@ pub fn main_get_connect_status() -> String { pub fn main_check_connect_status() { #[cfg(not(any(target_os = "android", target_os = "ios")))] - check_connect_status(true); + check_mouse_time(); // avoid multi calls } pub fn main_is_using_public_server() -> bool { @@ -764,6 +765,14 @@ pub fn main_check_super_user_permission() -> bool { check_super_user_permission() } +pub fn main_check_mouse_time() { + check_mouse_time(); +} + +pub fn main_get_mouse_time() -> f64 { + get_mouse_time() +} + pub fn cm_send_chat(conn_id: i32, msg: String) { connection_manager::send_chat(conn_id, msg); } From bdcb848a7562ec899b7ea1c0656056ef5f68ce06 Mon Sep 17 00:00:00 2001 From: csf Date: Wed, 31 Aug 2022 16:31:31 +0800 Subject: [PATCH 0329/2015] refactor remote interface --- src/client.rs | 6 + src/client/file_trait.rs | 24 +- src/flutter.rs | 74 +-- src/lib.rs | 1 + src/ui.rs | 2 +- src/ui/remote.rs | 870 ++++++++++++++++++++++-------------- src/ui_session_interface.rs | 257 +++++++++++ 7 files changed, 826 insertions(+), 408 deletions(-) create mode 100644 src/ui_session_interface.rs diff --git a/src/client.rs b/src/client.rs index 0bc69a7c1..64c7daf4d 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1651,6 +1651,12 @@ pub trait Interface: Send + Clone + 'static + Sized { fn handle_login_error(&mut self, err: &str) -> bool; fn handle_peer_info(&mut self, pi: PeerInfo); fn set_force_relay(&mut self, direct: bool, received: bool); + fn is_file_transfer(&self) -> bool; + fn is_port_forward(&self) -> bool; + fn is_rdp(&self) -> bool; + fn on_error(&self, err: &str) { + self.msgbox("error", "Error", err); + } fn is_force_relay(&self) -> bool; async fn handle_hash(&mut self, pass: &str, hash: Hash, peer: &mut Stream); async fn handle_login_from_ui(&mut self, password: String, remember: bool, peer: &mut Stream); diff --git a/src/client/file_trait.rs b/src/client/file_trait.rs index cc149c53f..d2f7b1648 100644 --- a/src/client/file_trait.rs +++ b/src/client/file_trait.rs @@ -1,4 +1,4 @@ -use hbb_common::{fs, message_proto::*}; +use hbb_common::{fs, message_proto::*, log}; use super::{Data, Interface}; @@ -114,4 +114,26 @@ pub trait FileManager: Interface { fn resume_job(&self, id: i32, is_remote: bool) { self.send(Data::ResumeJob((id, is_remote))); } + + fn set_confirm_override_file( + &self, + id: i32, + file_num: i32, + need_override: bool, + remember: bool, + is_upload: bool, + ) { + log::info!( + "confirm file transfer, job: {}, need_override: {}", + id, + need_override + ); + self.send(Data::SetConfirmOverrideFile(( + id, + file_num, + need_override, + remember, + is_upload, + ))); + } } diff --git a/src/flutter.rs b/src/flutter.rs index 60650aa9b..392f0f733 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -39,8 +39,9 @@ pub(super) const APP_TYPE_MAIN: &str = "main"; pub(super) const APP_TYPE_DESKTOP_REMOTE: &str = "remote"; pub(super) const APP_TYPE_DESKTOP_FILE_TRANSFER: &str = "file transfer"; +const MILLI1: Duration = Duration::from_millis(1); + lazy_static::lazy_static! { - // static ref SESSION: Arc>> = Default::default(); pub static ref SESSIONS: RwLock> = Default::default(); pub static ref GLOBAL_EVENT_STREAM: RwLock>> = Default::default(); // rust to dart event channel } @@ -48,9 +49,6 @@ lazy_static::lazy_static! { static SERVER_CLIPBOARD_ENABLED: AtomicBool = AtomicBool::new(true); static SERVER_KEYBOARD_ENABLED: AtomicBool = AtomicBool::new(true); -// pub fn get_session<'a>(id: &str) -> Option<&'a Session> { -// SESSIONS.read().unwrap().get(id) -// } #[derive(Clone)] pub struct Session { @@ -113,10 +111,6 @@ impl Session { } } - /// Get the current session instance. - // pub fn get() -> Arc>> { - // SESSION.clone() - // } /// Get the option of the current session. /// @@ -252,57 +246,6 @@ impl Session { self.send_msg(msg_out); } - // file trait - /// Send file over the current session. - // pub fn send_files( - // id: i32, - // path: String, - // to: String, - // file_num: i32, - // include_hidden: bool, - // is_remote: bool, - // ) { - // if let Some(session) = SESSION.write().unwrap().as_mut() { - // session.send_files(id, path, to, file_num, include_hidden, is_remote); - // } - // } - - // TODO into file trait - /// Confirm file override. - pub fn set_confirm_override_file( - &self, - id: i32, - file_num: i32, - need_override: bool, - remember: bool, - is_upload: bool, - ) { - log::info!( - "confirm file transfer, job: {}, need_override: {}", - id, - need_override - ); - self.send(Data::SetConfirmOverrideFile(( - id, - file_num, - need_override, - remember, - is_upload, - ))); - } - - /// Static method to send message over the current session. - /// - /// # Arguments - /// - /// * `msg` - The message to send. - // #[inline] - // pub fn send_msg_static(msg: Message) { - // if let Some(session) = SESSION.read().unwrap().as_ref() { - // session.send_msg(msg); - // } - // } - /// Push an event to the event queue. /// An event is stored as json in the event queue. /// @@ -595,6 +538,18 @@ impl Interface for Session { } } + fn is_file_transfer(&self) -> bool { + todo!() + } + + fn is_port_forward(&self) -> bool { + todo!() + } + + fn is_rdp(&self) -> bool { + todo!() + } + fn msgbox(&self, msgtype: &str, title: &str, text: &str) { let has_retry = if check_if_retry(msgtype, title, text) { "true" @@ -706,7 +661,6 @@ impl Interface for Session { } } -const MILLI1: Duration = Duration::from_millis(1); struct Connection { video_handler: VideoHandler, diff --git a/src/lib.rs b/src/lib.rs index b7d1883c8..f554d447e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -48,6 +48,7 @@ mod port_forward; mod tray; mod ui_interface; +mod ui_session_interface; #[cfg(windows)] pub mod clipboard_file; diff --git a/src/ui.rs b/src/ui.rs index 78654e9ec..b66d1453b 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -146,7 +146,7 @@ pub fn start(args: &mut [String]) { let args: Vec = iter.map(|x| x.clone()).collect(); frame.set_title(&id); frame.register_behavior("native-remote", move || { - Box::new(remote::Handler::new( + Box::new(remote::SciterSession::new( cmd.clone(), id.clone(), pass.clone(), diff --git a/src/ui/remote.rs b/src/ui/remote.rs index a0245c28a..f5abb3d73 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -1,6 +1,6 @@ use std::{ collections::HashMap, - ops::Deref, + ops::{Deref, DerefMut}, sync::{ atomic::{AtomicBool, AtomicUsize, Ordering}, Arc, Mutex, RwLock, @@ -48,6 +48,7 @@ use crate::clipboard_file::*; use crate::{ client::*, common::{self, check_clipboard, update_clipboard, ClipboardContext, CLIPBOARD_INTERVAL}, + ui_session_interface::{InvokeUi, Session}, }; use errno; @@ -74,46 +75,200 @@ static SERVER_CLIPBOARD_ENABLED: AtomicBool = AtomicBool::new(true); #[cfg(windows)] static mut IS_ALT_GR: bool = false; -#[derive(Default)] -pub struct HandlerInner { - element: Option, - sender: Option>, - thread: Option>, +/// SciterHandler +/// * element +/// * thread TODO check if flutter need +/// * close_state for file path when close +#[derive(Clone, Default)] +pub struct SciterHandler { + element: Arc>>, + thread: Arc>>>, close_state: HashMap, } -#[derive(Clone, Default)] -pub struct Handler { - inner: Arc>, - cmd: String, - id: String, - password: String, - args: Vec, - lc: Arc>, -} +impl SciterHandler { + #[inline] + fn call(&self, func: &str, args: &[Value]) { + if let Some(ref e) = self.element.lock().unwrap().as_ref() { + allow_err!(e.call_method(func, args)); + } + } -impl Deref for Handler { - type Target = Arc>; - - fn deref(&self) -> &Self::Target { - &self.inner + #[inline] + fn call2(&self, func: &str, args: &[Value]) { + if let Some(ref e) = self.element.lock().unwrap().as_ref() { + allow_err!(e.call_method(func, &super::value_crash_workaround(args)[..])); + } } } -impl FileManager for Handler {} +impl InvokeUi for SciterHandler { + fn set_cursor_data(&self, cd: CursorData) { + let mut colors = hbb_common::compress::decompress(&cd.colors); + if colors.iter().filter(|x| **x != 0).next().is_none() { + log::info!("Fix transparent"); + // somehow all 0 images shows black rect, here is a workaround + colors[3] = 1; + } + let mut png = Vec::new(); + if let Ok(()) = repng::encode(&mut png, cd.width as _, cd.height as _, &colors) { + self.call( + "setCursorData", + &make_args!( + cd.id.to_string(), + cd.hotx, + cd.hoty, + cd.width, + cd.height, + &png[..] + ), + ); + } + } -impl sciter::EventHandler for Handler { + fn set_display(&self, x: i32, y: i32, w: i32, h: i32) { + self.call("setDisplay", &make_args!(x, y, w, h)); + } + + fn update_privacy_mode(&self) { + self.call("updatePrivacyMode", &[]); + } + + fn set_permission(&self, name: &str, value: bool) { + self.call2("setPermission", &make_args!(name, value)); + } + + fn update_pi(&self, pi: PeerInfo) {} + + fn close_success(&self) { + self.call2("closeSuccess", &make_args!()); + } + + fn update_quality_status(&self, status: QualityStatus) { + self.call2( + "updateQualityStatus", + &make_args!( + status.speed.map_or(Value::null(), |it| it.into()), + status.fps.map_or(Value::null(), |it| it.into()), + status.delay.map_or(Value::null(), |it| it.into()), + status.target_bitrate.map_or(Value::null(), |it| it.into()), + status + .codec_format + .map_or(Value::null(), |it| it.to_string().into()) + ), + ); + } + + fn set_cursor_id(&self, id: String) { + self.call("setCursorId", &make_args!(id)); + } + + fn set_cursor_position(&self, cp: CursorPosition) { + self.call("setCursorPosition", &make_args!(cp.x, cp.y)); + } + + fn set_connection_type(&self, is_secured: bool, direct: bool) { + self.call("setConnectionType", &make_args!(is_secured, direct)); + } + + fn job_error(&self, id: i32, err: String, file_num: i32) { + todo!() + } + + fn job_done(&self, id: i32, file_num: i32) { + todo!() + } + + fn clear_all_jobs(&self) { + todo!() + } + + fn add_job( + &self, + id: i32, + path: String, + to: String, + file_num: i32, + show_hidden: bool, + is_remote: bool, + ) { + todo!() + } + + fn update_transfer_list(&self) { + todo!() + } + + fn confirm_delete_files(&self, id: i32, i: i32, name: String) { + todo!() + } + + fn override_file_confirm(&self, id: i32, file_num: i32, to: String, is_upload: bool) { + todo!() + } + + fn job_progress(&self, id: i32, file_num: i32, speed: f64, finished_size: f64) { + todo!() + } + + fn adapt_size(&self) { + self.call("adaptSize", &make_args!()); + } +} + +pub struct SciterSession(Session); + +impl Deref for SciterSession { + type Target = Session; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for SciterSession { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +// #[derive(Default)] +// pub struct HandlerInner { +// element: Option, +// sender: Option>, +// thread: Option>, +// close_state: HashMap, +// } + +// #[derive(Clone, Default)] +// pub struct Handler { +// inner: Arc>, +// cmd: String, +// id: String, +// password: String, +// args: Vec, +// lc: Arc>, +// } + +// impl Deref for Handler { +// type Target = Arc>; + +// fn deref(&self) -> &Self::Target { +// &self.inner +// } +// } + +impl sciter::EventHandler for SciterSession { fn get_subscription(&mut self) -> Option { Some(EVENT_GROUPS::HANDLE_BEHAVIOR_EVENT) } fn attached(&mut self, root: HELEMENT) { - self.write().unwrap().element = Some(Element::from(root)); + *self.element.lock().unwrap() = Some(Element::from(root)); } fn detached(&mut self, _root: HELEMENT) { - self.write().unwrap().element = None; - self.write().unwrap().sender.take().map(|sender| { + *self.element.lock().unwrap() = None; + self.sender.write().unwrap().take().map(|sender| { sender.send(Data::Close).ok(); }); } @@ -239,38 +394,40 @@ impl sciter::EventHandler for Handler { } } -impl Handler { +impl SciterSession { pub fn new(cmd: String, id: String, password: String, args: Vec) -> Self { - let me = Self { + let session: Session = Session { cmd, id: id.clone(), password: password.clone(), args, ..Default::default() }; - me.lc - .write() - .unwrap() - .initialize(id, me.is_file_transfer(), me.is_port_forward()); - me - } - - fn update_quality_status(&self, status: QualityStatus) { - self.call2( - "updateQualityStatus", - &make_args!( - status.speed.map_or(Value::null(), |it| it.into()), - status.fps.map_or(Value::null(), |it| it.into()), - status.delay.map_or(Value::null(), |it| it.into()), - status.target_bitrate.map_or(Value::null(), |it| it.into()), - status - .codec_format - .map_or(Value::null(), |it| it.to_string().into()) - ), + session.lc.write().unwrap().initialize( + id, + session.is_file_transfer(), + session.is_port_forward(), ); + + Self(session) } - fn start_keyboard_hook(&self) { + // fn update_quality_status(&self, status: QualityStatus) { + // self.call2( + // "updateQualityStatus", + // &make_args!( + // status.speed.map_or(Value::null(), |it| it.into()), + // status.fps.map_or(Value::null(), |it| it.into()), + // status.delay.map_or(Value::null(), |it| it.into()), + // status.target_bitrate.map_or(Value::null(), |it| it.into()), + // status + // .codec_format + // .map_or(Value::null(), |it| it.to_string().into()) + // ), + // ); + // } + + fn start_keyboard_hook(&'static self) { if self.is_port_forward() || self.is_file_transfer() { return; } @@ -278,7 +435,7 @@ impl Handler { return; } log::info!("keyboard hooked"); - let mut me = self.clone(); + let me = self.clone(); let peer = self.peer_platform(); let is_win = peer == "Windows"; #[cfg(windows)] @@ -364,7 +521,7 @@ impl Handler { Key::UpArrow => Some(ControlKey::UpArrow), Key::Delete => { if is_win && ctrl && alt { - me.ctrl_alt_del(); + // me.ctrl_alt_del(); // TODO return; } Some(ControlKey::Delete) @@ -485,7 +642,7 @@ impl Handler { } if chr != '\0' { if chr == 'l' && is_win && command { - me.lock_screen(); + // me.lock_screen(); // TODO return; } key_event.set_chr(chr as _); @@ -494,7 +651,7 @@ impl Handler { return; } } - me.key_down_or_up(down, key_event, alt, ctrl, shift, command); + // me.key_down_or_up(down, key_event, alt, ctrl, shift, command); // TODO }; if let Err(error) = rdev::listen(func) { log::error!("rdev: {:?}", error); @@ -518,19 +675,19 @@ impl Handler { v } - #[inline] - pub(super) fn save_config(&self, config: PeerConfig) { - self.lc.write().unwrap().save_config(config); - } + // #[inline] + // pub(super) fn save_config(&self, config: PeerConfig) { + // self.lc.write().unwrap().save_config(config); + // } fn save_view_style(&mut self, value: String) { self.lc.write().unwrap().save_view_style(value); } - #[inline] - pub(super) fn load_config(&self) -> PeerConfig { - load_config(&self.id) - } + // #[inline] + // pub(super) fn load_config(&self) -> PeerConfig { + // load_config(&self.id) + // } fn toggle_option(&mut self, name: String) { let msg = self.lc.write().unwrap().toggle_option(name.clone()); @@ -635,9 +792,9 @@ impl Handler { self.send(Data::Message(msg)); } - pub fn is_restarting_remote_device(&self) -> bool { - self.lc.read().unwrap().restarting_remote_device - } + // pub fn is_restarting_remote_device(&self) -> bool { + // self.lc.read().unwrap().restarting_remote_device + // } fn t(&self, name: String) -> String { crate::client::translate(name) @@ -672,7 +829,7 @@ impl Handler { let size = (x, y, w, h); let mut config = self.load_config(); if self.is_file_transfer() { - let close_state = self.read().unwrap().close_state.clone(); + let close_state = self.close_state.clone(); let mut has_change = false; for (k, mut v) in close_state { if k == "remote_dir" { @@ -785,20 +942,20 @@ impl Handler { pi } - fn get_option(&self, k: String) -> String { - self.lc.read().unwrap().get_option(&k) - } + // fn get_option(&self, k: String) -> String { + // self.lc.read().unwrap().get_option(&k) + // } - fn set_option(&self, k: String, v: String) { - self.lc.write().unwrap().set_option(k, v); - } + // fn set_option(&self, k: String, v: String) { + // self.lc.write().unwrap().set_option(k, v); + // } fn input_os_password(&mut self, pass: String, activate: bool) { input_os_password(pass, activate, self.clone()); } - fn save_close_state(&self, k: String, v: String) { - self.write().unwrap().close_state.insert(k, v); + fn save_close_state(&mut self, k: String, v: String) { + self.close_state.insert(k, v); } fn get_chatbox(&mut self) -> String { @@ -834,49 +991,49 @@ impl Handler { self.send(Data::Message(msg_out)); } - fn is_file_transfer(&self) -> bool { - self.cmd == "--file-transfer" - } + // fn is_file_transfer(&self) -> bool { + // self.cmd == "--file-transfer" + // } - fn is_port_forward(&self) -> bool { - self.cmd == "--port-forward" || self.is_rdp() - } + // fn is_port_forward(&self) -> bool { + // self.cmd == "--port-forward" || self.is_rdp() + // } - fn is_rdp(&self) -> bool { - self.cmd == "--rdp" - } + // fn is_rdp(&self) -> bool { + // self.cmd == "--rdp" + // } fn reconnect(&mut self) { println!("reconnecting"); let cloned = self.clone(); - let mut lock = self.write().unwrap(); - lock.thread.take().map(|t| t.join()); - lock.thread = Some(std::thread::spawn(move || { + let mut lock = self.thread.lock().unwrap(); + lock.take().map(|t| t.join()); + *lock = Some(std::thread::spawn(move || { io_loop(cloned); })); } - #[inline] - fn peer_platform(&self) -> String { - self.lc.read().unwrap().info.platform.clone() - } + // #[inline] + // fn peer_platform(&self) -> String { + // self.lc.read().unwrap().info.platform.clone() + // } - fn get_platform(&mut self, is_remote: bool) -> String { - if is_remote { - self.peer_platform() - } else { - whoami::platform().to_string() - } - } + // fn get_platform(&mut self, is_remote: bool) -> String { + // if is_remote { + // self.peer_platform() + // } else { + // whoami::platform().to_string() + // } + // } - fn get_path_sep(&mut self, is_remote: bool) -> &'static str { - let p = self.get_platform(is_remote); - if &p == "Windows" { - return "\\"; - } else { - return "/"; - } - } + // fn get_path_sep(&mut self, is_remote: bool) -> &'static str { + // let p = self.get_platform(is_remote); + // if &p == "Windows" { + // return "\\"; + // } else { + // return "/"; + // } + // } fn get_icon_path(&mut self, file_type: i32, ext: String) -> String { let mut path = Config::icon_path(); @@ -967,7 +1124,7 @@ impl Handler { } } - send_mouse(mask, x, y, alt, ctrl, shift, command, self); + send_mouse(mask, x, y, alt, ctrl, shift, command, &self.0); // on macos, ctrl + left button down = right button down, up won't emit, so we need to // emit up myself if peer is not macos // to-do: how about ctrl + left from win to macos @@ -1210,42 +1367,26 @@ impl Handler { self.send(Data::Message(msg_out)); } - #[inline] - fn set_cursor_id(&mut self, id: String) { - self.call("setCursorId", &make_args!(id)); - } + // #[inline] + // fn set_cursor_id(&mut self, id: String) { + // self.call("setCursorId", &make_args!(id)); + // } - #[inline] - fn set_cursor_position(&mut self, cd: CursorPosition) { - self.call("setCursorPosition", &make_args!(cd.x, cd.y)); - } + // #[inline] + // fn set_cursor_position(&mut self, cd: CursorPosition) { + // self.call("setCursorPosition", &make_args!(cd.x, cd.y)); + // } - #[inline] - fn call(&self, func: &str, args: &[Value]) { - let r = self.read().unwrap(); - if let Some(ref e) = r.element { - allow_err!(e.call_method(func, args)); - } - } - - #[inline] - fn call2(&self, func: &str, args: &[Value]) { - let r = self.read().unwrap(); - if let Some(ref e) = r.element { - allow_err!(e.call_method(func, &super::value_crash_workaround(args)[..])); - } - } - - #[inline] - fn set_display(&self, x: i32, y: i32, w: i32, h: i32) { - self.call("setDisplay", &make_args!(x, y, w, h)); - } + // #[inline] + // fn set_display(&self, x: i32, y: i32, w: i32, h: i32) { + // self.call("setDisplay", &make_args!(x, y, w, h)); + // } } const MILLI1: Duration = Duration::from_millis(1); -async fn start_one_port_forward( - handler: Handler, +async fn start_one_port_forward( + handler: Session, port: i32, remote_host: String, remote_port: i32, @@ -1273,9 +1414,9 @@ async fn start_one_port_forward( } #[tokio::main(flavor = "current_thread")] -async fn io_loop(handler: Handler) { +async fn io_loop(handler: Session) { let (sender, mut receiver) = mpsc::unbounded_channel::(); - handler.write().unwrap().sender = Some(sender.clone()); + *handler.sender.write().unwrap() = Some(sender.clone()); let mut options = crate::ipc::get_options_async().await; let mut key = options.remove("key").unwrap_or("".to_owned()); let token = LocalConfig::get_option("access_token"); @@ -1431,8 +1572,8 @@ impl RemoveJob { } } -struct Remote { - handler: Handler, +struct Remote { + handler: Session, video_sender: MediaSender, audio_sender: MediaSender, receiver: mpsc::UnboundedReceiver, @@ -1451,7 +1592,7 @@ struct Remote { video_format: CodecFormat, } -impl Remote { +impl Remote { async fn io_loop(&mut self, key: &str, token: &str) { let stop_clipboard = self.start_clipboard(); let mut last_recv_time = Instant::now(); @@ -1474,8 +1615,9 @@ impl Remote { SERVER_KEYBOARD_ENABLED.store(true, Ordering::SeqCst); SERVER_CLIPBOARD_ENABLED.store(true, Ordering::SeqCst); SERVER_FILE_TRANSFER_ENABLED.store(true, Ordering::SeqCst); - self.handler - .call("setConnectionType", &make_args!(peer.is_secured(), direct)); + // self.handler + // .call("setConnectionType", &make_args!(peer.is_secured(), direct)); + self.handler.set_connection_type(peer.is_secured(), direct); // just build for now #[cfg(not(windows))] @@ -1597,10 +1739,12 @@ impl Remote { } } if let Some(err) = err { - self.handler - .call("jobError", &make_args!(id, err, file_num)); + // self.handler + // .call("jobError", &make_args!(id, err, file_num)); + self.handler.job_error(id, err, file_num); } else { - self.handler.call("jobDone", &make_args!(id, file_num)); + // self.handler.call("jobDone", &make_args!(id, file_num)); + self.handler.job_done(id, file_num); } } @@ -1645,7 +1789,8 @@ impl Remote { async fn load_last_jobs(&mut self) { log::info!("start load last jobs"); - self.handler.call("clearAllJobs", &make_args!()); + // self.handler.call("clearAllJobs", &make_args!()); + self.handler.clear_all_jobs(); let pc = self.handler.load_config(); if pc.transfer.write_jobs.is_empty() && pc.transfer.read_jobs.is_empty() { // no last jobs @@ -1656,16 +1801,24 @@ impl Remote { for job_str in pc.transfer.read_jobs.iter() { let job: Result = serde_json::from_str(&job_str); if let Ok(job) = job { - self.handler.call( - "addJob", - &make_args!( - cnt, - job.to.clone(), - job.remote.clone(), - job.file_num, - job.show_hidden, - false - ), + // self.handler.call( + // "addJob", + // &make_args!( + // cnt, + // job.to.clone(), + // job.remote.clone(), + // job.file_num, + // job.show_hidden, + // false + // ), + // ); + self.handler.add_job( + cnt, + job.to.clone(), + job.remote.clone(), + job.file_num, + job.show_hidden, + false, ); cnt += 1; println!("restore read_job: {:?}", job); @@ -1674,22 +1827,31 @@ impl Remote { for job_str in pc.transfer.write_jobs.iter() { let job: Result = serde_json::from_str(&job_str); if let Ok(job) = job { - self.handler.call( - "addJob", - &make_args!( - cnt, - job.remote.clone(), - job.to.clone(), - job.file_num, - job.show_hidden, - true - ), + // self.handler.call( + // "addJob", + // &make_args!( + // cnt, + // job.remote.clone(), + // job.to.clone(), + // job.file_num, + // job.show_hidden, + // true + // ), + // ); + self.handler.add_job( + cnt, + job.remote.clone(), + job.to.clone(), + job.file_num, + job.show_hidden, + true, ); cnt += 1; println!("restore write_job: {:?}", job); } } - self.handler.call("updateTransferList", &make_args!()); + // self.handler.call("updateTransferList", &make_args!()); + self.handler.update_transfer_list(); } async fn handle_msg_from_ui(&mut self, data: Data, peer: &mut Stream) -> bool { @@ -1753,8 +1915,8 @@ impl Remote { to, job.files().len() ); - let m = make_fd(job.id(), job.files(), true); - self.handler.call("updateFolderFiles", &make_args!(m)); + // let m = make_fd(job.id(), job.files(), true); + // self.handler.call("updateFolderFiles", &make_args!(m)); // TODO #[cfg(not(windows))] let files = job.files().clone(); #[cfg(windows)] @@ -1813,8 +1975,8 @@ impl Remote { to, job.files().len() ); - let m = make_fd(job.id(), job.files(), true); - self.handler.call("updateFolderFiles", &make_args!(m)); + // let m = make_fd(job.id(), job.files(), true); + // self.handler.call("updateFolderFiles", &make_args!(m)); job.is_last_job = true; self.read_jobs.push(job); self.timer = time::interval(MILLI1); @@ -1860,10 +2022,11 @@ impl Remote { if let Some(job) = self.remove_jobs.get_mut(&id) { let i = file_num as usize; if i < job.files.len() { - self.handler.call( - "confirmDeleteFiles", - &make_args!(id, file_num, job.files[i].name.clone()), - ); + // self.handler.call( + // "confirmDeleteFiles", + // &make_args!(id, file_num, job.files[i].name.clone()), + // ); + self.handler.confirm_delete_files(id, file_num); } } } @@ -1924,8 +2087,8 @@ impl Remote { } else { match fs::get_recursive_files(&path, include_hidden) { Ok(entries) => { - let m = make_fd(id, &entries, true); - self.handler.call("updateFolderFiles", &make_args!(m)); + // let m = make_fd(id, &entries, true); + // self.handler.call("updateFolderFiles", &make_args!(m)); self.remove_jobs .insert(id, RemoveJob::new(entries, path, sep, is_remote)); } @@ -2018,7 +2181,7 @@ impl Remote { job: &fs::TransferJob, elapsed: i32, last_update_jobs_status: &mut (Instant, HashMap), - handler: &mut Handler, + handler: &mut Session, ) { if elapsed <= 0 { return; @@ -2034,10 +2197,11 @@ impl Remote { last_update_jobs_status.1.insert(job.id(), transferred); let speed = (transferred - last_transferred) as f64 / (elapsed as f64 / 1000.); let file_num = job.file_num() - 1; - handler.call( - "jobProgress", - &make_args!(job.id(), file_num, speed, job.finished_size() as f64), - ); + // handler.call( + // "jobProgress", + // &make_args!(job.id(), file_num, speed, job.finished_size() as f64), + // ); + handler.job_progress(job.id(), file_num, speed, job.finished_size() as f64); } fn update_jobs_status(&mut self) { @@ -2103,8 +2267,10 @@ impl Remote { Some(message::Union::VideoFrame(vf)) => { if !self.first_frame { self.first_frame = true; - self.handler.call2("closeSuccess", &make_args!()); - self.handler.call("adaptSize", &make_args!()); + // self.handler.call2("closeSuccess", &make_args!()); + self.handler.close_success(); + // self.handler.call("adaptSize", &make_args!()); + self.handler.adapt_size(); self.send_opts_after_login(peer).await; } let incomming_format = CodecFormat::from(&vf); @@ -2192,11 +2358,11 @@ impl Remote { fs::transform_windows_path(&mut entries); } } - let mut m = make_fd(fd.id, &entries, fd.id > 0); - if fd.id <= 0 { - m.set_item("path", fd.path); - } - self.handler.call("updateFolderFiles", &make_args!(m)); + // let mut m = make_fd(fd.id, &entries, fd.id > 0); + // if fd.id <= 0 { + // m.set_item("path", fd.path); + // } + // self.handler.call("updateFolderFiles", &make_args!(m)); if let Some(job) = fs::get_job(fd.id, &mut self.write_jobs) { log::info!("job set_files: {:?}", entries); job.set_files(entries); @@ -2227,14 +2393,20 @@ impl Remote { let msg = new_send_confirm(req); allow_err!(peer.send(&msg).await); } else { - self.handler.call( - "overrideFileConfirm", - &make_args!( - digest.id, - digest.file_num, - read_path, - true - ), + // self.handler.call( + // "overrideFileConfirm", + // &make_args!( + // digest.id, + // digest.file_num, + // read_path, + // true + // ), + // ); + self.handler.override_file_confirm( + digest.id, + digest.file_num, + read_path, + true, ); } } @@ -2271,14 +2443,20 @@ impl Remote { ); allow_err!(peer.send(&msg).await); } else { - self.handler.call( - "overrideFileConfirm", - &make_args!( - digest.id, - digest.file_num, - write_path, - false - ), + // self.handler.call( + // "overrideFileConfirm", + // &make_args!( + // digest.id, + // digest.file_num, + // write_path, + // false + // ), + // ); + self.handler.override_file_confirm( + digest.id, + digest.file_num, + write_path, + false, ); } } @@ -2333,24 +2511,27 @@ impl Remote { self.audio_sender.send(MediaData::AudioFormat(f)).ok(); } Some(misc::Union::ChatMessage(c)) => { - self.handler.call("newMessage", &make_args!(c.text)); + // self.handler.call("newMessage", &make_args!(c.text)); // TODO } Some(misc::Union::PermissionInfo(p)) => { 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); - self.handler - .call2("setPermission", &make_args!("keyboard", p.enabled)); + // self.handler + // .call2("setPermission", &make_args!("keyboard", p.enabled)); + self.handler.set_permission("keyboard", p.enabled); } Permission::Clipboard => { SERVER_CLIPBOARD_ENABLED.store(p.enabled, Ordering::SeqCst); - self.handler - .call2("setPermission", &make_args!("clipboard", p.enabled)); + // self.handler + // .call2("setPermission", &make_args!("clipboard", p.enabled)); + self.handler.set_permission("clipboard", p.enabled); } Permission::Audio => { - self.handler - .call2("setPermission", &make_args!("audio", p.enabled)); + // self.handler + // .call2("setPermission", &make_args!("audio", p.enabled)); + self.handler.set_permission("audio", p.enabled); } Permission::File => { SERVER_FILE_TRANSFER_ENABLED.store(p.enabled, Ordering::SeqCst); @@ -2358,17 +2539,19 @@ impl Remote { return true; } self.check_clipboard_file_context(); - self.handler - .call2("setPermission", &make_args!("file", p.enabled)); + // self.handler + // .call2("setPermission", &make_args!("file", p.enabled)); + self.handler.set_permission("file", p.enabled); } Permission::Restart => { - self.handler - .call2("setPermission", &make_args!("restart", p.enabled)); + // self.handler + // .call2("setPermission", &make_args!("restart", p.enabled)); + self.handler.set_permission("restart", p.enabled); } } } Some(misc::Union::SwitchDisplay(s)) => { - self.handler.call("switchDisplay", &make_args!(s.display)); + // self.handler.call("switchDisplay", &make_args!(s.display)); // TODO self.video_sender.send(MediaData::Reset).ok(); if s.width > 0 && s.height > 0 { VIDEO.lock().unwrap().as_mut().map(|v| { @@ -2441,7 +2624,7 @@ impl Remote { #[inline(always)] fn update_block_input_state(&mut self, on: bool) { - self.handler.call("updateBlockInputState", &make_args!(on)); + // self.handler.call("updateBlockInputState", &make_args!(on)); // TODO } async fn handle_back_msg_block_input(&mut self, state: back_notification::BlockInputState) { @@ -2471,7 +2654,8 @@ impl Remote { config.privacy_mode = on; self.handler.save_config(config); - self.handler.call("updatePrivacyMode", &[]); + // self.handler.call("updatePrivacyMode", &[]); + self.handler.update_privacy_mode(); } async fn handle_back_msg_privacy_mode( @@ -2593,143 +2777,137 @@ pub fn make_fd(id: i32, entries: &Vec, only_count: bool) -> Value { m } -#[async_trait] -impl Interface for Handler { - fn send(&self, data: Data) { - if let Some(ref sender) = self.read().unwrap().sender { - sender.send(data).ok(); - } - } +// #[async_trait] +// impl Interface for Handler { +// fn send(&self, data: Data) { +// if let Some(ref sender) = self.read().unwrap().sender { +// sender.send(data).ok(); +// } +// } - fn msgbox(&self, msgtype: &str, title: &str, text: &str) { - let retry = check_if_retry(msgtype, title, text); - self.call2("msgbox_retry", &make_args!(msgtype, title, text, retry)); - } +// fn msgbox(&self, msgtype: &str, title: &str, text: &str) { +// let retry = check_if_retry(msgtype, title, text); +// self.call2("msgbox_retry", &make_args!(msgtype, title, text, retry)); +// } - fn handle_login_error(&mut self, err: &str) -> bool { - self.lc.write().unwrap().handle_login_error(err, self) - } +// fn handle_login_error(&mut self, err: &str) -> bool { +// self.lc.write().unwrap().handle_login_error(err, self) +// } - fn handle_peer_info(&mut self, pi: PeerInfo) { - let mut pi_sciter = Value::map(); - let username = self.lc.read().unwrap().get_username(&pi); - pi_sciter.set_item("username", username.clone()); - 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); - if get_version_number(&pi.version) < get_version_number("1.1.10") { - self.call2("setPermission", &make_args!("restart", false)); - } - if self.is_file_transfer() { - if pi.username.is_empty() { - self.on_error("No active console user logged on, please connect and logon first."); - return; - } - } else if !self.is_port_forward() { - if pi.displays.is_empty() { - self.lc.write().unwrap().handle_peer_info(username, pi); - self.call("updatePrivacyMode", &[]); - self.msgbox("error", "Remote Error", "No Display"); - return; - } - 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); - displays.push(display); - } - pi_sciter.set_item("displays", displays); - let mut current = pi.current_display as usize; - if current >= pi.displays.len() { - current = 0; - } - pi_sciter.set_item("current_display", current as i32); - let current = &pi.displays[current]; - self.set_display(current.x, current.y, current.width, current.height); - // https://sciter.com/forums/topic/color_spaceiyuv-crash - // Nothing spectacular in decoder – done on CPU side. - // So if you can do BGRA translation on your side – the better. - // BGRA is used as internal image format so it will not require additional transformations. - VIDEO.lock().unwrap().as_mut().map(|v| { - let ok = v.start_streaming( - (current.width as _, current.height as _), - COLOR_SPACE::Rgb32, - None, - ); - log::info!("[video] initialized: {:?}", ok); - }); - let p = self.lc.read().unwrap().should_auto_login(); - if !p.is_empty() { - input_os_password(p, true, self.clone()); - } - } - self.lc.write().unwrap().handle_peer_info(username, pi); - self.call("updatePrivacyMode", &[]); - self.call("updatePi", &make_args!(pi_sciter)); - if self.is_file_transfer() { - self.call2("closeSuccess", &make_args!()); - } else if !self.is_port_forward() { - self.msgbox("success", "Successful", "Connected, waiting for image..."); - } - #[cfg(windows)] - { - let mut path = std::env::temp_dir(); - path.push(&self.id); - let path = path.with_extension(crate::get_app_name().to_lowercase()); - std::fs::File::create(&path).ok(); - if let Some(path) = path.to_str() { - crate::platform::windows::add_recent_document(&path); - } - } - self.start_keyboard_hook(); - } +// fn handle_peer_info(&mut self, pi: PeerInfo) { +// let mut pi_sciter = Value::map(); +// let username = self.lc.read().unwrap().get_username(&pi); +// pi_sciter.set_item("username", username.clone()); +// 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); +// if get_version_number(&pi.version) < get_version_number("1.1.10") { +// self.call2("setPermission", &make_args!("restart", false)); +// } +// if self.is_file_transfer() { +// if pi.username.is_empty() { +// self.on_error("No active console user logged on, please connect and logon first."); +// return; +// } +// } else if !self.is_port_forward() { +// if pi.displays.is_empty() { +// self.lc.write().unwrap().handle_peer_info(username, pi); +// self.call("updatePrivacyMode", &[]); +// self.msgbox("error", "Remote Error", "No Display"); +// return; +// } +// 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); +// displays.push(display); +// } +// pi_sciter.set_item("displays", displays); +// let mut current = pi.current_display as usize; +// if current >= pi.displays.len() { +// current = 0; +// } +// pi_sciter.set_item("current_display", current as i32); +// let current = &pi.displays[current]; +// self.set_display(current.x, current.y, current.width, current.height); +// // https://sciter.com/forums/topic/color_spaceiyuv-crash +// // Nothing spectacular in decoder – done on CPU side. +// // So if you can do BGRA translation on your side – the better. +// // BGRA is used as internal image format so it will not require additional transformations. +// VIDEO.lock().unwrap().as_mut().map(|v| { +// let ok = v.start_streaming( +// (current.width as _, current.height as _), +// COLOR_SPACE::Rgb32, +// None, +// ); +// log::info!("[video] initialized: {:?}", ok); +// }); +// let p = self.lc.read().unwrap().should_auto_login(); +// if !p.is_empty() { +// input_os_password(p, true, self.clone()); +// } +// } +// self.lc.write().unwrap().handle_peer_info(username, pi); +// self.call("updatePrivacyMode", &[]); +// self.call("updatePi", &make_args!(pi_sciter)); +// if self.is_file_transfer() { +// self.call2("closeSuccess", &make_args!()); +// } else if !self.is_port_forward() { +// self.msgbox("success", "Successful", "Connected, waiting for image..."); +// } +// #[cfg(windows)] +// { +// let mut path = std::env::temp_dir(); +// path.push(&self.id); +// let path = path.with_extension(crate::get_app_name().to_lowercase()); +// std::fs::File::create(&path).ok(); +// if let Some(path) = path.to_str() { +// crate::platform::windows::add_recent_document(&path); +// } +// } +// self.start_keyboard_hook(); +// } - async fn handle_hash(&mut self, pass: &str, hash: Hash, peer: &mut Stream) { - handle_hash(self.lc.clone(), pass, hash, self, peer).await; - } +// async fn handle_hash(&mut self, pass: &str, hash: Hash, peer: &mut Stream) { +// handle_hash(self.lc.clone(), pass, hash, self, peer).await; +// } - async fn handle_login_from_ui(&mut self, password: String, remember: bool, peer: &mut Stream) { - handle_login_from_ui(self.lc.clone(), password, remember, peer).await; - } +// async fn handle_login_from_ui(&mut self, password: String, remember: bool, peer: &mut Stream) { +// handle_login_from_ui(self.lc.clone(), password, remember, peer).await; +// } - async fn handle_test_delay(&mut self, t: TestDelay, peer: &mut Stream) { - if !t.from_client { - self.update_quality_status(QualityStatus { - delay: Some(t.last_delay as _), - target_bitrate: Some(t.target_bitrate as _), - ..Default::default() - }); - handle_test_delay(t, peer).await; - } - } +// async fn handle_test_delay(&mut self, t: TestDelay, peer: &mut Stream) { +// if !t.from_client { +// self.update_quality_status(QualityStatus { +// delay: Some(t.last_delay as _), +// target_bitrate: Some(t.target_bitrate as _), +// ..Default::default() +// }); +// handle_test_delay(t, peer).await; +// } +// } - fn set_force_relay(&mut self, direct: bool, received: bool) { - let mut lc = self.lc.write().unwrap(); - lc.force_relay = false; - if direct && !received { - let errno = errno::errno().0; - log::info!("errno is {}", errno); - // TODO: check mac and ios - if cfg!(windows) && errno == 10054 || !cfg!(windows) && errno == 104 { - lc.force_relay = true; - lc.set_option("force-always-relay".to_owned(), "Y".to_owned()); - } - } - } +// fn set_force_relay(&mut self, direct: bool, received: bool) { +// let mut lc = self.lc.write().unwrap(); +// lc.force_relay = false; +// if direct && !received { +// let errno = errno::errno().0; +// log::info!("errno is {}", errno); +// // TODO: check mac and ios +// if cfg!(windows) && errno == 10054 || !cfg!(windows) && errno == 104 { +// lc.force_relay = true; +// lc.set_option("force-always-relay".to_owned(), "Y".to_owned()); +// } +// } +// } - fn is_force_relay(&self) -> bool { - self.lc.read().unwrap().force_relay - } -} - -impl Handler { - fn on_error(&self, err: &str) { - self.msgbox("error", "Error", err); - } -} +// fn is_force_relay(&self) -> bool { +// self.lc.read().unwrap().force_relay +// } +// } #[tokio::main(flavor = "current_thread")] async fn send_note(url: String, id: String, conn_id: i32, note: String) { diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs new file mode 100644 index 000000000..8f7a0b904 --- /dev/null +++ b/src/ui_session_interface.rs @@ -0,0 +1,257 @@ +use crate::client::{ + self, check_if_retry, handle_hash, handle_login_from_ui, handle_test_delay, input_os_password, + FileManager, LoginConfigHandler, QualityStatus, load_config, +}; +use crate::{client::Data, client::Interface}; +use async_trait::async_trait; +use hbb_common::config::PeerConfig; +use hbb_common::message_proto::{CursorData, Hash, PeerInfo, TestDelay, CursorPosition}; +use hbb_common::tokio::{ + self, + sync::mpsc, + time::{self, Duration, Instant, Interval}, +}; +use hbb_common::{get_version_number, log, Stream}; +use std::ops::{Deref, DerefMut}; +use std::sync::{Arc, RwLock}; + +#[derive(Clone, Default)] +pub struct Session { + pub cmd: String, + pub id: String, + pub password: String, + pub args: Vec, + pub lc: Arc>, + pub sender: Arc>>>, + pub ui_handler: T, +} + +impl Session { + pub fn get_option(&self, k: String) -> String { + self.lc.read().unwrap().get_option(&k) + } + + pub fn set_option(&self, k: String, v: String) { + self.lc.write().unwrap().set_option(k, v); + } + + #[inline] + pub fn load_config(&self) -> PeerConfig { + load_config(&self.id) + } + + #[inline] + pub(super) fn save_config(&self, config: PeerConfig) { + self.lc.write().unwrap().save_config(config); + } + + pub fn is_restarting_remote_device(&self) -> bool { + self.lc.read().unwrap().restarting_remote_device + } + + #[inline] + pub fn peer_platform(&self) -> String { + self.lc.read().unwrap().info.platform.clone() + } + + pub fn get_platform(&mut self, is_remote: bool) -> String { + if is_remote { + self.peer_platform() + } else { + whoami::platform().to_string() + } + } + + pub fn get_path_sep(&mut self, is_remote: bool) -> &'static str { + let p = self.get_platform(is_remote); + if &p == "Windows" { + return "\\"; + } else { + return "/"; + } + } +} + +pub trait InvokeUi: Send + Sync + Clone + 'static + Sized + Default { + fn set_cursor_data(&self, cd: CursorData); + fn set_cursor_id(&self, id: String); + fn set_cursor_position(&self, cp:CursorPosition); + fn set_display(&self, x: i32, y: i32, w: i32, h: i32); + fn update_privacy_mode(&self); + fn set_permission(&self, name: &str, value: bool); + fn update_pi(&self, pi: PeerInfo); + fn close_success(&self); + fn update_quality_status(&self, qs: QualityStatus); + fn set_connection_type(&self,is_secured: bool, direct: bool); + fn job_error(&self,id:i32, err:String, file_num:i32); + fn job_done(&self,id:i32, file_num:i32); + fn clear_all_jobs(&self); + fn add_job(&self, id:i32, path:String, to:String, file_num:i32, show_hidden:bool, is_remote:bool); + fn update_transfer_list(&self); + // fn update_folder_files(&self); // TODO + fn confirm_delete_files(&self,id:i32, i:i32, name:String); + fn override_file_confirm(&self, id:i32, file_num:i32, to:String, is_upload:bool); + fn job_progress(&self, id:i32, file_num:i32, speed:f64, finished_size:f64); + fn adapt_size(&self); +} + + +impl Deref for Session { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.ui_handler + } +} + +impl DerefMut for Session { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.ui_handler + } +} + +impl FileManager for Session {} + +#[async_trait] +impl Interface for Session { + fn send(&self, data: Data) { + if let Some(sender) = self.sender.read().unwrap().as_ref() { + sender.send(data).ok(); + } + } + + fn is_file_transfer(&self) -> bool { + self.cmd == "--file-transfer" + } + + fn is_port_forward(&self) -> bool { + self.cmd == "--port-forward" || self.is_rdp() + } + + fn is_rdp(&self) -> bool { + self.cmd == "--rdp" + } + + fn msgbox(&self, msgtype: &str, title: &str, text: &str) { + let retry = check_if_retry(msgtype, title, text); + // self.call2("msgbox_retry", &make_args!(msgtype, title, text, retry)); + } + + fn handle_login_error(&mut self, err: &str) -> bool { + self.lc.write().unwrap().handle_login_error(err, self) + } + + fn handle_peer_info(&mut self, pi: PeerInfo) { + // let mut pi_sciter = Value::map(); + let username = self.lc.read().unwrap().get_username(&pi); + // pi_sciter.set_item("username", username.clone()); + // 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); + if get_version_number(&pi.version) < get_version_number("1.1.10") { + self.set_permission("restart", false); + } + if self.is_file_transfer() { + if pi.username.is_empty() { + self.on_error("No active console user logged on, please connect and logon first."); + return; + } + } else if !self.is_port_forward() { + if pi.displays.is_empty() { + self.lc.write().unwrap().handle_peer_info(username, pi); + self.update_privacy_mode(); + self.msgbox("error", "Remote Error", "No Display"); + return; + } + // 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); + // displays.push(display); + // } + // pi_sciter.set_item("displays", displays); + let mut current = pi.current_display as usize; + if current >= pi.displays.len() { + current = 0; + } + // pi_sciter.set_item("current_display", current as i32); + let current = &pi.displays[current]; + self.set_display(current.x, current.y, current.width, current.height); + // https://sciter.com/forums/topic/color_spaceiyuv-crash + // Nothing spectacular in decoder – done on CPU side. + // So if you can do BGRA translation on your side – the better. + // BGRA is used as internal image format so it will not require additional transformations. + // VIDEO.lock().unwrap().as_mut().map(|v| { + // let ok = v.start_streaming( + // (current.width as _, current.height as _), + // COLOR_SPACE::Rgb32, + // None, + // ); + // log::info!("[video] initialized: {:?}", ok); + // }); + let p = self.lc.read().unwrap().should_auto_login(); + if !p.is_empty() { + input_os_password(p, true, self.clone()); + } + } + self.lc.write().unwrap().handle_peer_info(username, pi); + self.update_privacy_mode(); + // self.update_pi(pi); + if self.is_file_transfer() { + self.close_success(); + } else if !self.is_port_forward() { + self.msgbox("success", "Successful", "Connected, waiting for image..."); + } + #[cfg(windows)] + { + let mut path = std::env::temp_dir(); + path.push(&self.id); + let path = path.with_extension(crate::get_app_name().to_lowercase()); + std::fs::File::create(&path).ok(); + if let Some(path) = path.to_str() { + crate::platform::windows::add_recent_document(&path); + } + } + // self.start_keyboard_hook(); // TODO + } + + async fn handle_hash(&mut self, pass: &str, hash: Hash, peer: &mut Stream) { + handle_hash(self.lc.clone(), pass, hash, self, peer).await; + } + + async fn handle_login_from_ui(&mut self, password: String, remember: bool, peer: &mut Stream) { + handle_login_from_ui(self.lc.clone(), password, remember, peer).await; + } + + async fn handle_test_delay(&mut self, t: TestDelay, peer: &mut Stream) { + if !t.from_client { + self.update_quality_status(QualityStatus { + delay: Some(t.last_delay as _), + target_bitrate: Some(t.target_bitrate as _), + ..Default::default() + }); + handle_test_delay(t, peer).await; + } + } + + fn set_force_relay(&mut self, direct: bool, received: bool) { + let mut lc = self.lc.write().unwrap(); + lc.force_relay = false; + if direct && !received { + let errno = errno::errno().0; + log::info!("errno is {}", errno); + // TODO: check mac and ios + if cfg!(windows) && errno == 10054 || !cfg!(windows) && errno == 104 { + lc.force_relay = true; + lc.set_option("force-always-relay".to_owned(), "Y".to_owned()); + } + } + } + + fn is_force_relay(&self) -> bool { + self.lc.read().unwrap().force_relay + } +} From 78112e97541b0264fac747f18e6bd9719ab8276b Mon Sep 17 00:00:00 2001 From: Asura Date: Wed, 31 Aug 2022 03:54:31 -0700 Subject: [PATCH 0330/2015] Replace pynput with tfc --- src/server/input_service.rs | 152 ++++++++++++++++++++++++++++++++++-- 1 file changed, 145 insertions(+), 7 deletions(-) diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 684ada483..eddfa3c73 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -9,7 +9,7 @@ use std::{ sync::atomic::{AtomicBool, Ordering}, time::Instant, }; -use tfc::{traits::*, Context}; +use tfc::{traits::*, Context as TFC_Context, Key as TFC_Key}; #[derive(Default)] struct StateCursor { @@ -180,7 +180,7 @@ lazy_static::lazy_static! { }; static ref KEYS_DOWN: Arc>> = Default::default(); static ref LATEST_INPUT: Arc> = Default::default(); - static ref KBD_CONTEXT: Mutex = Mutex::new(Context::new().expect("kbd context error")); + static ref TFC_CONTEXT: Mutex = Mutex::new(TFC_Context::new().expect("kbd context error")); } static EXITING: AtomicBool = AtomicBool::new(false); @@ -668,12 +668,113 @@ fn map_keyboard_mode(evt: &KeyEvent) { if evt.down && click_capslock { rdev_key_down_or_up(RdevKey::CapsLock, evt.down); } - log::info!("click capslog {:?} click_numlock {:?}", click_capslock, click_numlock); + log::info!( + "click capslog {:?} click_numlock {:?}", + click_capslock, + click_numlock + ); rdev_key_down_or_up(RdevKey::Unknown(evt.chr()), evt.down); return; } +fn tfc_key_down_or_up(key: Key, down: bool, up: bool) { + if let Key::Layout(chr) = key { + log::info!("tfc_key_down_or_up: {:?}", chr); + if down { + TFC_CONTEXT.lock().unwrap().unicode_char_down(chr); + } + if up { + TFC_CONTEXT.lock().unwrap().unicode_char_up(chr); + } + return; + } + + let key = match key { + Key::Alt => TFC_Key::Alt, + Key::Backspace => TFC_Key::DeleteOrBackspace, + Key::CapsLock => TFC_Key::CapsLock, + Key::Control => TFC_Key::Control, + Key::Delete => TFC_Key::ForwardDelete, + Key::DownArrow => TFC_Key::DownArrow, + Key::End => TFC_Key::End, + Key::Escape => TFC_Key::Escape, + Key::F1 => TFC_Key::F1, + Key::F10 => TFC_Key::F10, + Key::F11 => TFC_Key::F11, + Key::F12 => TFC_Key::F12, + Key::F2 => TFC_Key::F2, + Key::F3 => TFC_Key::F3, + Key::F4 => TFC_Key::F4, + Key::F5 => TFC_Key::F5, + Key::F6 => TFC_Key::F6, + Key::F7 => TFC_Key::F7, + Key::F8 => TFC_Key::F8, + Key::F9 => TFC_Key::F9, + Key::Home => TFC_Key::Home, + Key::LeftArrow => TFC_Key::LeftArrow, + Key::Option => TFC_Key::Alt, + Key::PageDown => TFC_Key::PageDown, + Key::PageUp => TFC_Key::PageUp, + Key::Return => TFC_Key::ReturnOrEnter, + Key::RightArrow => TFC_Key::RightArrow, + Key::Shift => TFC_Key::Shift, + Key::Space => TFC_Key::Space, + Key::Tab => TFC_Key::Tab, + Key::UpArrow => TFC_Key::UpArrow, + Key::Numpad0 => TFC_Key::N0, + Key::Numpad1 => TFC_Key::N1, + Key::Numpad2 => TFC_Key::N2, + Key::Numpad3 => TFC_Key::N3, + Key::Numpad4 => TFC_Key::N4, + Key::Numpad5 => TFC_Key::N5, + Key::Numpad6 => TFC_Key::N6, + Key::Numpad7 => TFC_Key::N7, + Key::Numpad8 => TFC_Key::N8, + Key::Numpad9 => TFC_Key::N9, + Key::Decimal => TFC_Key::NumpadDecimal, + // Key::Cancel => TFC_Key::Cancel, + Key::Clear => TFC_Key::NumpadClear, + Key::Pause => TFC_Key::PlayPause, + // Key::Kana => TFC_Key::, + // Key::Hangul => "Hangul", + // Key::Hanja => "Hanja", + // Key::Kanji => "Kanji", + // Key::Select => TFC_Key::Sel, + // Key::Print => TFC_Key::P, + // Key::Execute => "Execute", + // Key::Snapshot => "3270_PrintScreen", + // Key::Insert => TFC_Key:, + // Key::Help => "Help", + // Key::Separator => "KP_Separator", + // Key::Scroll => "Scroll_Lock", + // Key::NumLock => "Num_Lock", + Key::RWin => TFC_Key::Meta, + // Key::Apps => "Menu", + Key::Multiply => TFC_Key::NumpadMultiply, + Key::Add => TFC_Key::NumpadPlus, + Key::Subtract => TFC_Key::NumpadMinus, + Key::Divide => TFC_Key::NumpadDivide, + Key::Equals => TFC_Key::NumpadEquals, + Key::NumpadEnter => TFC_Key::NumpadEnter, + Key::RightShift => TFC_Key::RightShift, + Key::RightControl => TFC_Key::RightControl, + Key::RightAlt => TFC_Key::RightAlt, + Key::Command | Key::Super | Key::Windows | Key::Meta => TFC_Key::Meta, + _ => { + return; + } + }; + + log::info!("tfc_key_down_or_up: {:?}", key); + if down { + TFC_CONTEXT.lock().unwrap().key_down(key); + } + if up { + TFC_CONTEXT.lock().unwrap().key_up(key); + } +} + fn legacy_keyboard_mode(evt: &KeyEvent) { let (click_capslock, click_numlock) = sync_status(evt); @@ -681,9 +782,15 @@ fn legacy_keyboard_mode(evt: &KeyEvent) { crate::platform::windows::try_change_desktop(); let mut en = ENIGO.lock().unwrap(); if click_capslock { + #[cfg(target_os = "linux")] + tfc_key_down_or_up(Key::CapsLock, true, true); + #[cfg(not(target_os = "linux"))] en.key_click(Key::CapsLock); } if click_numlock { + #[cfg(target_os = "linux")] + tfc_key_down_or_up(Key::NumLock, true, true); + #[cfg(not(target_os = "linux"))] en.key_click(Key::NumLock); } // disable numlock if press home etc when numlock is on, @@ -692,11 +799,11 @@ fn legacy_keyboard_mode(evt: &KeyEvent) { let mut disable_numlock = false; #[cfg(target_os = "macos")] en.reset_flag(); - // When long-pressed the command key, then press and release + // When long-pressed the command key, then press and release // the Tab key, there should be CGEventFlagCommand in the flag. #[cfg(target_os = "macos")] - for ck in evt.modifiers.iter(){ - if let Some(key) = KEY_MAP.get(&ck.value()){ + for ck in evt.modifiers.iter() { + if let Some(key) = KEY_MAP.get(&ck.value()) { en.add_flag(key); } } @@ -738,6 +845,9 @@ fn legacy_keyboard_mode(evt: &KeyEvent) { } } else { if !get_modifier_state(key.clone(), &mut en) { + #[cfg(target_os = "linux")] + tfc_key_down_or_up(key.clone(), true, false); + #[cfg(not(target_os = "linux"))] en.key_down(key.clone()).ok(); modifier_sleep(); to_release.push(key); @@ -749,7 +859,11 @@ fn legacy_keyboard_mode(evt: &KeyEvent) { } #[cfg(not(target_os = "macos"))] if has_cap != en.get_key_state(Key::CapsLock) { + #[cfg(target_os = "linux")] + tfc_key_down_or_up(Key::CapsLock, true, true); + #[cfg(not(target_os = "linux"))] en.key_down(Key::CapsLock).ok(); + #[cfg(not(target_os = "linux"))] en.key_up(Key::CapsLock); } #[cfg(windows)] @@ -771,12 +885,18 @@ fn legacy_keyboard_mode(evt: &KeyEvent) { } } if evt.down { + #[cfg(target_os = "linux")] + tfc_key_down_or_up(key.clone(), true, false); + #[cfg(not(target_os = "linux"))] allow_err!(en.key_down(key.clone())); KEYS_DOWN .lock() .unwrap() .insert(ck.value() as _, Instant::now()); } else { + #[cfg(target_os = "linux")] + tfc_key_down_or_up(key.clone(), false, true); + #[cfg(not(target_os = "linux"))] en.key_up(key.clone()); KEYS_DOWN.lock().unwrap().remove(&(ck.value() as _)); } @@ -791,6 +911,14 @@ fn legacy_keyboard_mode(evt: &KeyEvent) { } Some(key_event::Union::Chr(chr)) => { if evt.down { + #[cfg(target_os = "linux")] + tfc_key_down_or_up(get_layout(chr), true, false); + #[cfg(target_os = "linux")] + KEYS_DOWN + .lock() + .unwrap() + .insert(chr as u64 + KEY_CHAR_START, Instant::now()); + #[cfg(not(target_os = "linux"))] if en.key_down(get_layout(chr)).is_ok() { KEYS_DOWN .lock() @@ -808,6 +936,9 @@ fn legacy_keyboard_mode(evt: &KeyEvent) { } } } else { + #[cfg(target_os = "linux")] + tfc_key_down_or_up(get_layout(chr), false, true); + #[cfg(not(target_os = "linux"))] en.key_up(get_layout(chr)); KEYS_DOWN .lock() @@ -827,6 +958,9 @@ fn legacy_keyboard_mode(evt: &KeyEvent) { } #[cfg(not(target_os = "macos"))] for key in to_release { + #[cfg(target_os = "linux")] + tfc_key_down_or_up(key.clone(), false, true); + #[cfg(not(target_os = "linux"))] en.key_up(key.clone()); } #[cfg(windows)] @@ -840,7 +974,11 @@ fn translate_keyboard_mode(evt: &KeyEvent) { let chr = char::from_u32(evt.chr()).unwrap_or_default(); // down(true)->press && press(false)-> release if evt.down && !evt.press { - KBD_CONTEXT.lock().unwrap().unicode_char(chr).expect("unicode_char_down error"); + TFC_CONTEXT + .lock() + .unwrap() + .unicode_char(chr) + .expect("unicode_char_down error"); } } From 9999e8864e0ac23b314bb5b87fda9f2263fafbc8 Mon Sep 17 00:00:00 2001 From: Asura Date: Wed, 31 Aug 2022 04:16:49 -0700 Subject: [PATCH 0331/2015] Update pubspec.lock --- flutter/pubspec.lock | 4 ++-- flutter/pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 4d0a07287..2a4558945 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -1244,8 +1244,8 @@ packages: dependency: "direct main" description: path: "." - ref: "799ef079e87938c3f4340591b4330c2598f38bb9" - resolved-ref: "799ef079e87938c3f4340591b4330c2598f38bb9" + ref: a25f1776ccc1119cbb2a8541174293aa36d532ed + resolved-ref: a25f1776ccc1119cbb2a8541174293aa36d532ed url: "https://github.com/Kingtous/rustdesk_window_manager" source: git version: "0.2.6" diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 93c2f64b2..e109be507 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -61,7 +61,7 @@ dependencies: window_manager: git: url: https://github.com/Kingtous/rustdesk_window_manager - ref: 799ef079e87938c3f4340591b4330c2598f38bb9 + ref: a25f1776ccc1119cbb2a8541174293aa36d532ed desktop_multi_window: git: url: https://github.com/Kingtous/rustdesk_desktop_multi_window From 8f6fed5416eae311413b4a17fa1504d9897ed931 Mon Sep 17 00:00:00 2001 From: 21pages Date: Wed, 31 Aug 2022 19:23:32 +0800 Subject: [PATCH 0332/2015] fix tabbar close button can't show when selected && hovered Signed-off-by: 21pages --- .../lib/desktop/widgets/tabbar_widget.dart | 76 +++++++++++-------- 1 file changed, 46 insertions(+), 30 deletions(-) diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index a89d0da38..38e724bad 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -484,7 +484,7 @@ class _ListView extends StatelessWidget { } } -class _Tab extends StatelessWidget { +class _Tab extends StatefulWidget { late final int index; late final Rx label; late final IconData? selectedIcon; @@ -493,7 +493,6 @@ class _Tab extends StatelessWidget { late final int selected; late final Function() onClose; late final Function() onSelected; - final RxBool _hover = false.obs; late final TarBarTheme theme; final Widget Function(Widget icon, Widget label, TabThemeConf themeConf)? tabBuilder; @@ -512,31 +511,39 @@ class _Tab extends StatelessWidget { required this.theme}) : super(key: key); + @override + State<_Tab> createState() => _TabState(); +} + +class _TabState extends State<_Tab> with RestorationMixin { + final RestorableBool restoreHover = RestorableBool(false); + Widget _buildTabContent() { - bool showIcon = selectedIcon != null && unselectedIcon != null; - bool isSelected = index == selected; + bool showIcon = + widget.selectedIcon != null && widget.unselectedIcon != null; + bool isSelected = widget.index == widget.selected; final icon = Offstage( offstage: !showIcon, child: Icon( - isSelected ? selectedIcon : unselectedIcon, + isSelected ? widget.selectedIcon : widget.unselectedIcon, size: _kIconSize, color: isSelected - ? theme.selectedtabIconColor - : theme.unSelectedtabIconColor, + ? widget.theme.selectedtabIconColor + : widget.theme.unSelectedtabIconColor, ).paddingOnly(right: 5)); final labelWidget = Obx(() { return Text( - translate(label.value), + translate(widget.label.value), textAlign: TextAlign.center, style: TextStyle( color: isSelected - ? theme.selectedTextColor - : theme.unSelectedTextColor), + ? widget.theme.selectedTextColor + : widget.theme.unSelectedTextColor), ); }); - if (tabBuilder == null) { + if (widget.tabBuilder == null) { return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -545,37 +552,38 @@ class _Tab extends StatelessWidget { ], ); } else { - return tabBuilder!( - icon, labelWidget, TabThemeConf(iconSize: _kIconSize, theme: theme)); + return widget.tabBuilder!(icon, labelWidget, + TabThemeConf(iconSize: _kIconSize, theme: widget.theme)); } } @override Widget build(BuildContext context) { - bool showIcon = selectedIcon != null && unselectedIcon != null; - bool isSelected = index == selected; - bool showDivider = index != selected - 1 && index != selected; + bool isSelected = widget.index == widget.selected; + bool showDivider = + widget.index != widget.selected - 1 && widget.index != widget.selected; + RxBool hover = restoreHover.value.obs; return Ink( child: InkWell( - onHover: (hover) => _hover.value = hover, - onTap: () => onSelected(), + onHover: (value) { + hover.value = value; + restoreHover.value = value; + }, + onTap: () => widget.onSelected(), child: Row( children: [ - Container( + SizedBox( height: _kTabBarHeight, child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ _buildTabContent(), - Offstage( - offstage: !closable, - child: Obx((() => _CloseButton( - visiable: _hover.value, - tabSelected: isSelected, - onClose: () => onClose(), - theme: theme, - ))), - ) + Obx((() => _CloseButton( + visiable: hover.value && widget.closable, + tabSelected: isSelected, + onClose: () => widget.onClose(), + theme: widget.theme, + ))) ])).paddingSymmetric(horizontal: 10), Offstage( offstage: !showDivider, @@ -583,7 +591,7 @@ class _Tab extends StatelessWidget { width: 1, indent: _kDividerIndent, endIndent: _kDividerIndent, - color: theme.dividerColor, + color: widget.theme.dividerColor, thickness: 1, ), ) @@ -592,6 +600,14 @@ class _Tab extends StatelessWidget { ), ); } + + @override + String? get restorationId => "_Tab${widget.label.value}"; + + @override + void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + registerForRestoration(restoreHover, 'restoreHover'); + } } class _CloseButton extends StatelessWidget { @@ -615,7 +631,7 @@ class _CloseButton extends StatelessWidget { child: Offstage( offstage: !visiable, child: InkWell( - customBorder: RoundedRectangleBorder(), + customBorder: const RoundedRectangleBorder(), onTap: () => onClose(), child: Icon( Icons.close, From 3e8f7ed36df60045fc98f1cc989151548442a141 Mon Sep 17 00:00:00 2001 From: XieJiSS Date: Wed, 31 Aug 2022 20:24:48 +0800 Subject: [PATCH 0333/2015] fix: unicode-related error during .ts generation The user may uses a different codepage/encoding which is not unicode, so we'd like to get rid of that --- flutter/web/js/gen_js_from_hbb.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/flutter/web/js/gen_js_from_hbb.py b/flutter/web/js/gen_js_from_hbb.py index 13dbc96fc..0bdde54e4 100755 --- a/flutter/web/js/gen_js_from_hbb.py +++ b/flutter/web/js/gen_js_from_hbb.py @@ -5,25 +5,36 @@ import os import glob from tabnanny import check +def pad_start(s, n, c = ' '): + if len(s) >= n: + return s + return c * (n - len(s)) + s + +def safe_unicode(s): + res = "" + for c in s: + res += r"\u{}".format(pad_start(hex(ord(c))[2:], 4, '0')) + return res + def main(): print('export const LANGS = {') for fn in glob.glob('../../../src/lang/*'): lang = os.path.basename(fn)[:-3] if lang == 'template': continue print(' %s: {'%lang) - for ln in open(fn): + for ln in open(fn, encoding='utf-8'): ln = ln.strip() if ln.startswith('("'): toks = ln.split('", "') assert(len(toks) == 2) a = toks[0][2:] b = toks[1][:-3] - print(' "%s": "%s",'%(a, b)) + print(' "%s": "%s",'%(safe_unicode(a), safe_unicode(b))) print(' },') print('}') check_if_retry = ['', False] KEY_MAP = ['', False] - for ln in open('../../../src/client.rs'): + for ln in open('../../../src/client.rs', encoding='utf-8'): ln = ln.strip() if 'check_if_retry' in ln: check_if_retry[1] = True @@ -55,7 +66,7 @@ def main(): print('export const KEY_MAP: any = {') print(KEY_MAP[0]) print('}') - for ln in open('../../../Cargo.toml'): + for ln in open('../../../Cargo.toml', encoding='utf-8'): if ln.startswith('version ='): print('export const ' + ln) From e5c45542218daa8280e5054f317b3ab18093a7d1 Mon Sep 17 00:00:00 2001 From: csf Date: Wed, 31 Aug 2022 20:46:30 +0800 Subject: [PATCH 0334/2015] refactor remote, sciter / flutter run success --- src/flutter.rs | 3483 +++++++++++++++++++---------------- src/flutter_ffi.rs | 36 +- src/ui/remote.rs | 2027 +++----------------- src/ui_session_interface.rs | 1767 +++++++++++++++++- 4 files changed, 3888 insertions(+), 3425 deletions(-) diff --git a/src/flutter.rs b/src/flutter.rs index 392f0f733..88c6f1961 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -28,7 +28,10 @@ use hbb_common::{ ResultType, Stream, }; -use crate::common::{self, make_fd_to_json, CLIPBOARD_INTERVAL}; +use crate::{ + common::{self, make_fd_to_json, CLIPBOARD_INTERVAL}, + ui_session_interface::{io_loop, InvokeUi, Session}, +}; #[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::common::{check_clipboard, update_clipboard, ClipboardContext}; @@ -42,210 +45,19 @@ pub(super) const APP_TYPE_DESKTOP_FILE_TRANSFER: &str = "file transfer"; const MILLI1: Duration = Duration::from_millis(1); lazy_static::lazy_static! { - pub static ref SESSIONS: RwLock> = Default::default(); + pub static ref SESSIONS: RwLock>> = Default::default(); pub static ref GLOBAL_EVENT_STREAM: RwLock>> = Default::default(); // rust to dart event channel } static SERVER_CLIPBOARD_ENABLED: AtomicBool = AtomicBool::new(true); static SERVER_KEYBOARD_ENABLED: AtomicBool = AtomicBool::new(true); - -#[derive(Clone)] -pub struct Session { - id: String, - sender: Arc>>>, // UI to rust - lc: Arc>, - events2ui: Arc>>>, +#[derive(Default, Clone)] +pub struct FlutterHandler { + pub event_stream: Arc>>>, } -impl Session { - /// Create a new remote session with the given id. - /// - /// # Arguments - /// - /// * `id` - The identifier of the remote session with prefix. Regex: [\w]*[\_]*[\d]+ - /// * `is_file_transfer` - If the session is used for file transfer. - /// * `is_port_forward` - If the session is used for port forward. - pub fn add(id: &str, is_file_transfer: bool, is_port_forward: bool) -> ResultType<()> { - // TODO check same id - let session_id = get_session_id(id.to_owned()); - LocalConfig::set_remote_id(&session_id); - // TODO close - // Self::close(); - let session = Session { - id: session_id.clone(), - sender: Default::default(), - lc: Default::default(), - events2ui: Arc::new(RwLock::new(None)), - }; - session.lc.write().unwrap().initialize( - session_id.clone(), - is_file_transfer, - is_port_forward, - ); - SESSIONS - .write() - .unwrap() - .insert(id.to_owned(), session.clone()); - Ok(()) - } - - /// Create a new remote session with the given id. - /// - /// # Arguments - /// - /// * `id` - The identifier of the remote session with prefix. Regex: [\w]*[\_]*[\d]+ - /// * `events2ui` - The events channel to ui. - pub fn start(id: &str, events2ui: StreamSink) -> ResultType<()> { - if let Some(session) = SESSIONS.write().unwrap().get_mut(id) { - *session.events2ui.write().unwrap() = Some(events2ui); - let session = session.clone(); - std::thread::spawn(move || { - let is_file_transfer = session.lc.read().unwrap().is_file_transfer; - let is_port_forward = session.lc.read().unwrap().is_port_forward; - Connection::start(session, is_file_transfer, is_port_forward); - }); - Ok(()) - } else { - bail!("No session with peer id {}", id) - } - } - - - /// Get the option of the current session. - /// - /// # Arguments - /// - /// * `name` - The name of the option to get. Currently only `remote_dir` is supported. - pub fn get_option(&self, name: &str) -> String { - if name == "remote_dir" { - return self.lc.read().unwrap().get_remote_dir(); - } - self.lc.read().unwrap().get_option(name) - } - - /// Set the option of the current session. - /// - /// # Arguments - /// - /// * `name` - The name of the option to set. Currently only `remote_dir` is supported. - /// * `value` - The value of the option to set. - pub fn set_option(&self, name: String, value: String) { - let mut value = value; - let mut lc = self.lc.write().unwrap(); - if name == "remote_dir" { - value = lc.get_all_remote_dir(value); - } - lc.set_option(name, value); - } - - /// Input the OS password. - pub fn input_os_password(&self, pass: String, activate: bool) { - input_os_password(pass, activate, self.clone()); - } - - pub fn restart_remote_device(&self) { - let mut lc = self.lc.write().unwrap(); - lc.restarting_remote_device = true; - let msg = lc.restart_remote_device(); - self.send_msg(msg); - } - - /// Toggle an option. - pub fn toggle_option(&self, name: &str) { - let msg = self.lc.write().unwrap().toggle_option(name.to_owned()); - if let Some(msg) = msg { - self.send_msg(msg); - } - } - - /// Send a refresh command. - pub fn refresh(&self) { - self.send(Data::Message(LoginConfigHandler::refresh())); - } - - /// Get image quality. - pub fn get_image_quality(&self) -> String { - self.lc.read().unwrap().image_quality.clone() - } - - /// Set image quality. - pub fn set_image_quality(&self, value: &str) { - let msg = self - .lc - .write() - .unwrap() - .save_image_quality(value.to_owned()); - if let Some(msg) = msg { - self.send_msg(msg); - } - } - - /// Get the status of a toggle option. - /// Return `None` if the option is not found. - /// - /// # Arguments - /// - /// * `name` - The name of the option to get. - pub fn get_toggle_option(&self, name: &str) -> bool { - self.lc.write().unwrap().get_toggle_option(name) - } - - /// Login. - /// - /// # Arguments - /// - /// * `password` - The password to login. - /// * `remember` - If the password should be remembered. - pub fn login(&self, password: &str, remember: bool) { - self.send(Data::Login((password.to_owned(), remember))); - } - - /// Close the session. - pub fn close(&self) { - self.send(Data::Close); - } - - /// Reconnect to the current session. - pub fn reconnect(&self) { - self.send(Data::Close); - let session = self.clone(); - std::thread::spawn(move || { - Connection::start(session, false, false); - }); - } - - /// Get `remember` flag in [`LoginConfigHandler`]. - pub fn get_remember(&self) -> bool { - self.lc.read().unwrap().remember - } - - /// Send message over the current session. - /// - /// # Arguments - /// - /// * `msg` - The message to send. - #[inline] - pub fn send_msg(&self, msg: Message) { - self.send(Data::Message(msg)); - } - - /// Send chat message over the current session. - /// - /// # Arguments - /// - /// * `text` - The message to send. - pub fn send_chat(&self, text: String) { - let mut misc = Misc::new(); - misc.set_chat_message(ChatMessage { - text, - ..Default::default() - }); - let mut msg_out = Message::new(); - msg_out.set_misc(misc); - self.send_msg(msg_out); - } - +impl FlutterHandler { /// Push an event to the event queue. /// An event is stored as json in the event queue. /// @@ -258,221 +70,60 @@ impl Session { assert!(h.get("name").is_none()); h.insert("name", name); let out = serde_json::ser::to_string(&h).unwrap_or("".to_owned()); - if let Some(stream) = &*self.events2ui.read().unwrap() { + if let Some(stream) = &*self.event_stream.read().unwrap() { stream.add(EventToUI::Event(out)); } } +} - /// Get platform of peer. - #[inline] - fn peer_platform(&self) -> String { - self.lc.read().unwrap().info.platform.clone() +impl InvokeUi for FlutterHandler { + fn set_cursor_data(&self, cd: CursorData) { + let colors = hbb_common::compress::decompress(&cd.colors); + self.push_event( + "cursor_data", + vec![ + ("id", &cd.id.to_string()), + ("hotx", &cd.hotx.to_string()), + ("hoty", &cd.hoty.to_string()), + ("width", &cd.width.to_string()), + ("height", &cd.height.to_string()), + ( + "colors", + &serde_json::ser::to_string(&colors).unwrap_or("".to_owned()), + ), + ], + ); } - /// Quick method for sending a ctrl_alt_del command. - pub fn ctrl_alt_del(&self) { - if self.peer_platform() == "Windows" { - let k = Key::ControlKey(ControlKey::CtrlAltDel); - self.key_down_or_up(1, k, false, false, false, false); - } else { - let k = Key::ControlKey(ControlKey::Delete); - self.key_down_or_up(3, k, true, true, false, false); - } + fn set_cursor_id(&self, id: String) { + self.push_event("cursor_id", vec![("id", &id.to_string())]); } - /// Switch the display. - /// - /// # Arguments - /// - /// * `display` - The display to switch to. - pub fn switch_display(&self, display: i32) { - let mut misc = Misc::new(); - misc.set_switch_display(SwitchDisplay { - display, - ..Default::default() - }); - let mut msg_out = Message::new(); - msg_out.set_misc(misc); - self.send_msg(msg_out); + fn set_cursor_position(&self, cp: CursorPosition) { + self.push_event( + "cursor_position", + vec![("x", &cp.x.to_string()), ("y", &cp.y.to_string())], + ); } - /// Send lock screen command. - pub fn lock_screen(&self) { - let k = Key::ControlKey(ControlKey::LockScreen); - self.key_down_or_up(1, k, false, false, false, false); + fn set_display(&self, x: i32, y: i32, w: i32, h: i32) { + // todo!() } - /// Send key input command. - /// - /// # Arguments - /// - /// * `name` - The name of the key. - /// * `down` - Whether the key is down or up. - /// * `press` - If the key is simply being pressed(Down+Up). - /// * `alt` - If the alt key is also pressed. - /// * `ctrl` - If the ctrl key is also pressed. - /// * `shift` - If the shift key is also pressed. - /// * `command` - If the command key is also pressed. - pub fn input_key( - &self, - name: &str, - down: bool, - press: bool, - alt: bool, - ctrl: bool, - shift: bool, - command: bool, - ) { - let chars: Vec = name.chars().collect(); - if chars.len() == 1 { - let key = Key::_Raw(chars[0] as _); - self._input_key(key, down, press, alt, ctrl, shift, command); - } else { - if let Some(key) = KEY_MAP.get(name) { - self._input_key(key.clone(), down, press, alt, ctrl, shift, command); - } - } + fn update_privacy_mode(&self) { + self.push_event("update_privacy_mode", [].into()); } - /// Input a string of text. - /// String is parsed into individual key presses. - /// - /// # Arguments - /// - /// * `value` - The text to input. TODO &str -> String - pub fn input_string(&self, value: &str) { - let mut key_event = KeyEvent::new(); - key_event.set_seq(value.to_owned()); - let mut msg_out = Message::new(); - msg_out.set_key_event(key_event); - self.send_msg(msg_out); + fn set_permission(&self, name: &str, value: bool) { + // todo!() } - fn _input_key( - &self, - key: Key, - down: bool, - press: bool, - alt: bool, - ctrl: bool, - shift: bool, - command: bool, - ) { - let v = if press { - 3 - } else if down { - 1 - } else { - 0 - }; - self.key_down_or_up(v, key, alt, ctrl, shift, command); + fn update_pi(&self, pi: PeerInfo) { + // todo!() } - pub fn send_mouse( - &self, - mask: i32, - x: i32, - y: i32, - alt: bool, - ctrl: bool, - shift: bool, - command: bool, - ) { - send_mouse(mask, x, y, alt, ctrl, shift, command, self); - } - - fn key_down_or_up( - &self, - down_or_up: i32, - key: Key, - alt: bool, - ctrl: bool, - shift: bool, - command: bool, - ) { - let mut down_or_up = down_or_up; - let mut key_event = KeyEvent::new(); - match key { - Key::Chr(chr) => { - key_event.set_chr(chr); - } - Key::ControlKey(key) => { - key_event.set_control_key(key.clone()); - } - Key::_Raw(raw) => { - if raw > 'z' as u32 || raw < 'a' as u32 { - key_event.set_unicode(raw); - if down_or_up == 0 { - // ignore up, avoiding trigger twice - return; - } - down_or_up = 1; // if press, turn into down for avoiding trigger twice on server side - } else { - // to make ctrl+c works on windows - key_event.set_chr(raw); - } - } - } - if alt { - key_event.modifiers.push(ControlKey::Alt.into()); - } - if shift { - key_event.modifiers.push(ControlKey::Shift.into()); - } - if ctrl { - key_event.modifiers.push(ControlKey::Control.into()); - } - if command { - key_event.modifiers.push(ControlKey::Meta.into()); - } - if down_or_up == 1 { - key_event.down = true; - } else if down_or_up == 3 { - key_event.press = true; - } - let mut msg_out = Message::new(); - msg_out.set_key_event(key_event); - log::debug!("{:?}", msg_out); - self.send_msg(msg_out); - } - - pub fn load_config(&self) -> PeerConfig { - load_config(&self.id) - } - - pub fn save_config(&self, config: &PeerConfig) { - config.store(&self.id); - } - - pub fn get_platform(&self, is_remote: bool) -> String { - if is_remote { - self.lc.read().unwrap().info.platform.clone() - } else { - whoami::platform().to_string() - } - } - - pub fn load_last_jobs(&self) { - let pc = self.load_config(); - if pc.transfer.write_jobs.is_empty() && pc.transfer.read_jobs.is_empty() { - // no last jobs - return; - } - let mut cnt = 1; - for job_str in pc.transfer.read_jobs.iter() { - if !job_str.is_empty() { - self.push_event("load_last_job", vec![("value", job_str)]); - cnt += 1; - println!("restore read_job: {:?}", job_str); - } - } - for job_str in pc.transfer.write_jobs.iter() { - if !job_str.is_empty() { - self.push_event("load_last_job", vec![("value", job_str)]); - cnt += 1; - println!("restore write_job: {:?}", job_str); - } - } + fn close_success(&self) { + // todo!() } fn update_quality_status(&self, status: QualityStatus) { @@ -495,67 +146,95 @@ impl Session { ); } - pub fn remove_port_forward(&mut self, port: i32) { - let mut config = self.load_config(); - config.port_forwards = config - .port_forwards - .drain(..) - .filter(|x| x.0 != port) - .collect(); - self.save_config(&config); - self.send(Data::RemovePortForward(port)); + fn set_connection_type(&self, is_secured: bool, direct: bool) { + self.push_event( + "connection_ready", + vec![ + ("secure", &is_secured.to_string()), + ("direct", &direct.to_string()), + ], + ); } - pub fn add_port_forward(&mut self, port: i32, remote_host: String, remote_port: i32) { - let mut config = self.load_config(); - if config - .port_forwards - .iter() - .filter(|x| x.0 == port) - .next() - .is_some() - { - return; - } - let pf = (port, remote_host, remote_port); - config.port_forwards.push(pf.clone()); - self.save_config(&config); - self.send(Data::AddPortForward(pf)); + fn job_error(&self, id: i32, err: String, file_num: i32) { + // todo!() } - fn on_error(&self, err: &str) { - self.msgbox("error", "Error", err); + fn job_done(&self, id: i32, file_num: i32) { + // todo!() } -} -impl FileManager for Session {} + fn clear_all_jobs(&self) { + // todo!() + } -#[async_trait] -impl Interface for Session { - fn send(&self, data: Data) { - if let Some(sender) = self.sender.read().unwrap().as_ref() { - sender.send(data).ok(); + fn add_job( + &self, + id: i32, + path: String, + to: String, + file_num: i32, + show_hidden: bool, + is_remote: bool, + ) { + // todo!() + } + + fn update_transfer_list(&self) { + // todo!() + } + + fn confirm_delete_files(&self, id: i32, i: i32, name: String) { + // todo!() + } + + fn override_file_confirm(&self, id: i32, file_num: i32, to: String, is_upload: bool) { + // todo!() + } + + fn job_progress(&self, id: i32, file_num: i32, speed: f64, finished_size: f64) { + // todo!() + } + + fn adapt_size(&self) { + // todo!() + } + + fn on_rgba(&self, data: &[u8]) { + if let Some(stream) = &*self.event_stream.read().unwrap() { + stream.add(EventToUI::Rgba(ZeroCopyBuffer(data.to_owned()))); } } - fn is_file_transfer(&self) -> bool { - todo!() + fn set_peer_info( + &self, + username: &str, + hostname: &str, + platform: &str, + sas_enabled: bool, + displays: &Vec>, + version: &str, + current_display: usize, + is_file_transfer: bool, + ) { + let displays = serde_json::ser::to_string(displays).unwrap_or("".to_owned()); + self.push_event( + "peer_info", + vec![ + ("username", username), + ("hostname", hostname), + ("platform", platform), + ("sas_enabled", &sas_enabled.to_string()), + ("displays", &displays), + ("version", &version), + ("current_display", ¤t_display.to_string()), + ("is_file_transfer", &is_file_transfer.to_string()), + ], + ); } - fn is_port_forward(&self) -> bool { - todo!() - } - - fn is_rdp(&self) -> bool { - todo!() - } - - fn msgbox(&self, msgtype: &str, title: &str, text: &str) { - let has_retry = if check_if_retry(msgtype, title, text) { - "true" - } else { - "" - }; + fn msgbox(&self, msgtype: &str, title: &str, text: &str, retry: bool) { + let has_retry = if retry { "true" } else { "" }; self.push_event( "msgbox", vec![ @@ -566,1184 +245,1754 @@ impl Interface for Session { ], ); } +} - fn handle_login_error(&mut self, err: &str) -> bool { - self.lc.write().unwrap().handle_login_error(err, self) - } +/// Create a new remote session with the given id. +/// +/// # Arguments +/// +/// * `id` - The identifier of the remote session with prefix. Regex: [\w]*[\_]*[\d]+ +/// * `is_file_transfer` - If the session is used for file transfer. +/// * `is_port_forward` - If the session is used for port forward. +pub fn session_add(id: &str, is_file_transfer: bool, is_port_forward: bool) -> ResultType<()> { + // TODO check same id + let session_id = get_session_id(id.to_owned()); + LocalConfig::set_remote_id(&session_id); + // TODO close + // Self::close(); - fn handle_peer_info(&mut self, pi: PeerInfo) { - let mut lc = self.lc.write().unwrap(); - let username = lc.get_username(&pi); - let mut displays = Vec::new(); - let mut current = pi.current_display as usize; + // TODO cmd passwd args + let session: Session = Session { + id: session_id.clone(), + ..Default::default() + }; - if lc.is_file_transfer { - if pi.username.is_empty() { - self.msgbox( - "error", - "Error", - "No active console user logged on, please connect and logon first.", - ); - return; - } - } else { - if pi.displays.is_empty() { - self.msgbox("error", "Remote Error", "No Display"); - } - 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); - displays.push(h); - } - if current >= pi.displays.len() { - current = 0; - } - } - let displays = serde_json::ser::to_string(&displays).unwrap_or("".to_owned()); - self.push_event( - "peer_info", - vec![ - ("username", &username), - ("hostname", &pi.hostname), - ("platform", &pi.platform), - ("sas_enabled", &pi.sas_enabled.to_string()), - ("displays", &displays), - ("version", &pi.version), - ("current_display", ¤t.to_string()), - ("is_file_transfer", &lc.is_file_transfer.to_string()), - ], - ); - lc.handle_peer_info(username, pi); - let p = lc.should_auto_login(); - if !p.is_empty() { - input_os_password(p, true, self.clone()); - } - } + session + .lc + .write() + .unwrap() + .initialize(session_id.clone(), is_file_transfer, is_port_forward); + SESSIONS + .write() + .unwrap() + .insert(id.to_owned(), session.clone()); + Ok(()) +} - fn set_force_relay(&mut self, direct: bool, received: bool) { - let mut lc = self.lc.write().unwrap(); - lc.force_relay = false; - if direct && !received { - let errno = errno::errno().0; - log::info!("errno is {}", errno); - // TODO: check mac and ios - if cfg!(windows) && errno == 10054 || !cfg!(windows) && errno == 104 { - lc.force_relay = true; - lc.set_option("force-always-relay".to_owned(), "Y".to_owned()); - } - } - } - - fn is_force_relay(&self) -> bool { - self.lc.read().unwrap().force_relay - } - - async fn handle_hash(&mut self, pass: &str, hash: Hash, peer: &mut Stream) { - handle_hash(self.lc.clone(), pass, hash, self, peer).await; - } - - async fn handle_login_from_ui(&mut self, password: String, remember: bool, peer: &mut Stream) { - handle_login_from_ui(self.lc.clone(), password, remember, peer).await; - } - - async fn handle_test_delay(&mut self, t: TestDelay, peer: &mut Stream) { - if !t.from_client { - self.update_quality_status(QualityStatus { - delay: Some(t.last_delay as _), - target_bitrate: Some(t.target_bitrate as _), - ..Default::default() - }); - handle_test_delay(t, peer).await; - } +/// start a session with the given id. +/// +/// # Arguments +/// +/// * `id` - The identifier of the remote session with prefix. Regex: [\w]*[\_]*[\d]+ +/// * `events2ui` - The events channel to ui. +pub fn session_start_(id: &str, event_stream: StreamSink) -> ResultType<()> { + if let Some(session) = SESSIONS.write().unwrap().get_mut(id) { + *session.event_stream.write().unwrap() = Some(event_stream); + let session = session.clone(); + std::thread::spawn(move || { + // let is_file_transfer = session.lc.read().unwrap().is_file_transfer; + // let is_port_forward = session.lc.read().unwrap().is_port_forward; + // Connection::start(session, is_file_transfer, is_port_forward); + io_loop(session); + }); + Ok(()) + } else { + bail!("No session with peer id {}", id) } } +// #[derive(Clone)] +// pub struct Session { +// id: String, +// sender: Arc>>>, // UI to rust +// lc: Arc>, +// events2ui: Arc>>>, +// } -struct Connection { - video_handler: VideoHandler, - audio_handler: AudioHandler, - session: Session, - first_frame: bool, - read_jobs: Vec, - write_jobs: Vec, - timer: Interval, - last_update_jobs_status: (Instant, HashMap), - data_count: Arc, - frame_count: Arc, - video_format: CodecFormat, -} +// impl Session1 { +// /// Create a new remote session with the given id. +// /// +// /// # Arguments +// /// +// /// * `id` - The identifier of the remote session with prefix. Regex: [\w]*[\_]*[\d]+ +// /// * `is_file_transfer` - If the session is used for file transfer. +// /// * `is_port_forward` - If the session is used for port forward. +// pub fn add(id: &str, is_file_transfer: bool, is_port_forward: bool) -> ResultType<()> { +// // TODO check same id +// let session_id = get_session_id(id.to_owned()); +// LocalConfig::set_remote_id(&session_id); +// // TODO close +// // Self::close(); +// let session = Session { +// id: session_id.clone(), +// sender: Default::default(), +// lc: Default::default(), +// events2ui: Arc::new(RwLock::new(None)), +// }; +// session.lc.write().unwrap().initialize( +// session_id.clone(), +// is_file_transfer, +// is_port_forward, +// ); +// SESSIONS +// .write() +// .unwrap() +// .insert(id.to_owned(), session.clone()); +// Ok(()) +// } -impl Connection { - // TODO: Similar to remote::start_clipboard - // merge the code - fn start_clipboard( - tx_protobuf: mpsc::UnboundedSender, - lc: Arc>, - ) -> Option> { - let (tx, rx) = std::sync::mpsc::channel(); - #[cfg(not(any(target_os = "android", target_os = "ios")))] - match ClipboardContext::new() { - Ok(mut ctx) => { - let old_clipboard: Arc> = Default::default(); - // 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 - { - 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) - } +// /// Create a new remote session with the given id. +// /// +// /// # Arguments +// /// +// /// * `id` - The identifier of the remote session with prefix. Regex: [\w]*[\_]*[\d]+ +// /// * `events2ui` - The events channel to ui. +// pub fn start(id: &str, events2ui: StreamSink) -> ResultType<()> { +// if let Some(session) = SESSIONS.write().unwrap().get_mut(id) { +// *session.events2ui.write().unwrap() = Some(events2ui); +// let session = session.clone(); +// std::thread::spawn(move || { +// let is_file_transfer = session.lc.read().unwrap().is_file_transfer; +// let is_port_forward = session.lc.read().unwrap().is_port_forward; +// Connection::start(session, is_file_transfer, is_port_forward); +// }); +// Ok(()) +// } else { +// bail!("No session with peer id {}", id) +// } +// } - /// Create a new connection. - /// - /// # Arguments - /// - /// * `session` - The session to create a new connection for. - /// * `is_file_transfer` - Whether the connection is for file transfer. - /// * `is_port_forward` - Whether the connection is for port forward. - #[tokio::main(flavor = "current_thread")] - async fn start(session: Session, is_file_transfer: bool, is_port_forward: bool) { - let mut last_recv_time = Instant::now(); - let (sender, mut receiver) = mpsc::unbounded_channel::(); - let mut stop_clipboard = None; - if !is_file_transfer && !is_port_forward { - stop_clipboard = Self::start_clipboard(sender.clone(), session.lc.clone()); - } - *session.sender.write().unwrap() = Some(sender.clone()); - let conn_type = if is_file_transfer { - session.lc.write().unwrap().is_file_transfer = true; - ConnType::FILE_TRANSFER - } else if is_port_forward { - ConnType::PORT_FORWARD // TODO: RDP - } else { - ConnType::DEFAULT_CONN - }; - let key = Config::get_option("key"); - let token = Config::get_option("access_token"); +// /// Get the option of the current session. +// /// +// /// # Arguments +// /// +// /// * `name` - The name of the option to get. Currently only `remote_dir` is supported. +// pub fn get_option(&self, name: &str) -> String { +// if name == "remote_dir" { +// return self.lc.read().unwrap().get_remote_dir(); +// } +// self.lc.read().unwrap().get_option(name) +// } - // TODO rdp & cli args - let is_rdp = false; - let args: Vec = Vec::new(); +// /// Set the option of the current session. +// /// +// /// # Arguments +// /// +// /// * `name` - The name of the option to set. Currently only `remote_dir` is supported. +// /// * `value` - The value of the option to set. +// pub fn set_option(&self, name: String, value: String) { +// let mut value = value; +// let mut lc = self.lc.write().unwrap(); +// if name == "remote_dir" { +// value = lc.get_all_remote_dir(value); +// } +// lc.set_option(name, value); +// } - if is_port_forward { - if is_rdp { - // let port = handler - // .get_option("rdp_port".to_owned()) - // .parse::() - // .unwrap_or(3389); - // std::env::set_var( - // "rdp_username", - // handler.get_option("rdp_username".to_owned()), - // ); - // std::env::set_var( - // "rdp_password", - // handler.get_option("rdp_password".to_owned()), - // ); - // log::info!("Remote rdp port: {}", port); - // start_one_port_forward(handler, 0, "".to_owned(), port, receiver, &key, &token).await; - } else if args.len() == 0 { - let pfs = session.lc.read().unwrap().port_forwards.clone(); - let mut queues = HashMap::>::new(); - for d in pfs { - sender.send(Data::AddPortForward(d)).ok(); - } - loop { - match receiver.recv().await { - Some(Data::AddPortForward((port, remote_host, remote_port))) => { - if port <= 0 || remote_port <= 0 { - continue; - } - let (sender, receiver) = mpsc::unbounded_channel::(); - queues.insert(port, sender); - let handler = session.clone(); - let key = key.clone(); - let token = token.clone(); - tokio::spawn(async move { - start_one_port_forward( - handler, - port, - remote_host, - remote_port, - receiver, - &key, - &token, - ) - .await; - }); - } - Some(Data::RemovePortForward(port)) => { - if let Some(s) = queues.remove(&port) { - s.send(Data::Close).ok(); - } - } - Some(Data::Close) => { - break; - } - Some(d) => { - for (_, s) in queues.iter() { - s.send(d.clone()).ok(); - } - } - _ => {} - } - } - } else { - // let port = handler.args[0].parse::().unwrap_or(0); - // if handler.args.len() != 3 - // || handler.args[2].parse::().unwrap_or(0) <= 0 - // || port <= 0 - // { - // handler.on_error("Invalid arguments, usage:

    rustdesk --port-forward remote-id listen-port remote-host remote-port"); - // } - // let remote_host = handler.args[1].clone(); - // let remote_port = handler.args[2].parse::().unwrap_or(0); - // start_one_port_forward( - // handler, - // port, - // remote_host, - // remote_port, - // receiver, - // &key, - // &token, - // ) - // .await; - } - return; - } +// /// Input the OS password. +// pub fn input_os_password(&self, pass: String, activate: bool) { +// input_os_password(pass, activate, self.clone()); +// } - let latency_controller = LatencyController::new(); - let latency_controller_cl = latency_controller.clone(); +// pub fn restart_remote_device(&self) { +// let mut lc = self.lc.write().unwrap(); +// lc.restarting_remote_device = true; +// let msg = lc.restart_remote_device(); +// self.send_msg(msg); +// } - let mut conn = Connection { - video_handler: VideoHandler::new(latency_controller), - audio_handler: AudioHandler::new(latency_controller_cl), - session: session.clone(), - first_frame: false, - read_jobs: Vec::new(), - write_jobs: Vec::new(), - timer: time::interval(SEC30), - last_update_jobs_status: (Instant::now(), Default::default()), - data_count: Arc::new(AtomicUsize::new(0)), - frame_count: Arc::new(AtomicUsize::new(0)), - video_format: CodecFormat::Unknown, - }; +// /// Toggle an option. +// pub fn toggle_option(&self, name: &str) { +// let msg = self.lc.write().unwrap().toggle_option(name.to_owned()); +// if let Some(msg) = msg { +// self.send_msg(msg); +// } +// } - match Client::start(&session.id, &key, &token, conn_type, session.clone()).await { - Ok((mut peer, direct)) => { - SERVER_KEYBOARD_ENABLED.store(true, Ordering::SeqCst); - SERVER_CLIPBOARD_ENABLED.store(true, Ordering::SeqCst); +// /// Send a refresh command. +// pub fn refresh(&self) { +// self.send(Data::Message(LoginConfigHandler::refresh())); +// } - session.push_event( - "connection_ready", - vec![ - ("secure", &peer.is_secured().to_string()), - ("direct", &direct.to_string()), - ], - ); +// /// Get image quality. +// pub fn get_image_quality(&self) -> String { +// self.lc.read().unwrap().image_quality.clone() +// } - let mut status_timer = time::interval(Duration::new(1, 0)); +// /// Set image quality. +// pub fn set_image_quality(&self, value: &str) { +// let msg = self +// .lc +// .write() +// .unwrap() +// .save_image_quality(value.to_owned()); +// if let Some(msg) = msg { +// self.send_msg(msg); +// } +// } - loop { - tokio::select! { - res = peer.next() => { - if let Some(res) = res { - match res { - Err(err) => { - log::error!("Connection closed: {}", err); - session.msgbox("error", "Connection Error", &err.to_string()); - break; - } - Ok(ref bytes) => { - last_recv_time = Instant::now(); - conn.data_count.fetch_add(bytes.len(), Ordering::Relaxed); - if !conn.handle_msg_from_peer(bytes, &mut peer).await { - break - } - } - } - } else { - if session.lc.read().unwrap().restarting_remote_device { - log::info!("Restart remote device"); - session.msgbox("restarting", "Restarting Remote Device", "remote_restarting_tip"); - } else { - log::info!("Reset by the peer"); - session.msgbox("error", "Connection Error", "Reset by the peer"); - } - break; - } - } - d = receiver.recv() => { - if let Some(d) = d { - if !conn.handle_msg_from_ui(d, &mut peer).await { - break; - } - } - } - _ = conn.timer.tick() => { - if last_recv_time.elapsed() >= SEC30 { - session.msgbox("error", "Connection Error", "Timeout"); - break; - } - if !conn.read_jobs.is_empty() { - if let Err(err) = fs::handle_read_jobs(&mut conn.read_jobs, &mut peer).await { - log::debug!("Connection Error: {}", err); - break; - } - conn.update_jobs_status(); - } else { - conn.timer = time::interval_at(Instant::now() + SEC30, SEC30); - } - } - _ = status_timer.tick() => { - let speed = conn.data_count.swap(0, Ordering::Relaxed); - let speed = format!("{:.2}kB/s", speed as f32 / 1024 as f32); - let fps = conn.frame_count.swap(0, Ordering::Relaxed) as _; - conn.session.update_quality_status(QualityStatus { - speed:Some(speed), - fps:Some(fps), - ..Default::default() - }); - } - } - } - log::debug!("Exit io_loop of id={}", session.id); - } - Err(err) => { - session.msgbox("error", "Connection Error", &err.to_string()); - } - } +// /// Get the status of a toggle option. +// /// Return `None` if the option is not found. +// /// +// /// # Arguments +// /// +// /// * `name` - The name of the option to get. +// pub fn get_toggle_option(&self, name: &str) -> bool { +// self.lc.write().unwrap().get_toggle_option(name) +// } - if let Some(stop) = stop_clipboard { - stop.send(()).ok(); - } - SERVER_KEYBOARD_ENABLED.store(false, Ordering::SeqCst); - SERVER_CLIPBOARD_ENABLED.store(false, Ordering::SeqCst); - } +// /// Login. +// /// +// /// # Arguments +// /// +// /// * `password` - The password to login. +// /// * `remember` - If the password should be remembered. +// pub fn login(&self, password: &str, remember: bool) { +// self.send(Data::Login((password.to_owned(), remember))); +// } - /// Handle message from peer. - /// Return false if the connection should be closed. - /// - /// The message is handled by [`Message`], see [`message::Union`] for possible types. - async fn handle_msg_from_peer(&mut self, data: &[u8], peer: &mut Stream) -> bool { - if let Ok(msg_in) = Message::parse_from_bytes(&data) { - match msg_in.union { - Some(message::Union::VideoFrame(vf)) => { - if !self.first_frame { - self.first_frame = true; - common::send_opts_after_login(&self.session.lc.read().unwrap(), peer).await; - } - let incomming_format = CodecFormat::from(&vf); - if self.video_format != incomming_format { - self.video_format = incomming_format.clone(); - self.session.update_quality_status(QualityStatus { - codec_format: Some(incomming_format), - ..Default::default() - }) - }; - if let Ok(true) = self.video_handler.handle_frame(vf) { - if let Some(stream) = &*self.session.events2ui.read().unwrap() { - self.frame_count.fetch_add(1, Ordering::Relaxed); - stream.add(EventToUI::Rgba(ZeroCopyBuffer( - self.video_handler.rgb.clone(), - ))); - } - } - } - Some(message::Union::Hash(hash)) => { - self.session.handle_hash("", hash, peer).await; - } - Some(message::Union::LoginResponse(lr)) => match lr.union { - Some(login_response::Union::Error(err)) => { - if !self.session.handle_login_error(&err) { - return false; - } - } - Some(login_response::Union::PeerInfo(pi)) => { - self.session.handle_peer_info(pi); - } - _ => {} - }, - Some(message::Union::Clipboard(cb)) => { - if !self.session.lc.read().unwrap().disable_clipboard { - let content = if cb.compress { - decompress(&cb.content) - } else { - cb.content.into() - }; - if let Ok(content) = String::from_utf8(content) { - self.session - .push_event("clipboard", vec![("content", &content)]); - } - } - } - Some(message::Union::CursorData(cd)) => { - let colors = hbb_common::compress::decompress(&cd.colors); - self.session.push_event( - "cursor_data", - vec![ - ("id", &cd.id.to_string()), - ("hotx", &cd.hotx.to_string()), - ("hoty", &cd.hoty.to_string()), - ("width", &cd.width.to_string()), - ("height", &cd.height.to_string()), - ( - "colors", - &serde_json::ser::to_string(&colors).unwrap_or("".to_owned()), - ), - ], - ); - } - Some(message::Union::CursorId(id)) => { - self.session - .push_event("cursor_id", vec![("id", &id.to_string())]); - } - Some(message::Union::CursorPosition(cp)) => { - self.session.push_event( - "cursor_position", - vec![("x", &cp.x.to_string()), ("y", &cp.y.to_string())], - ); - } - Some(message::Union::FileResponse(fr)) => { - match fr.union { - Some(file_response::Union::Dir(fd)) => { - let mut entries = fd.entries.to_vec(); - if self.session.peer_platform() == "Windows" { - transform_windows_path(&mut entries); - } - let id = fd.id; - self.session.push_event( - "file_dir", - vec![("value", &make_fd_to_json(fd)), ("is_local", "false")], - ); - if let Some(job) = fs::get_job(id, &mut self.write_jobs) { - job.set_files(entries); - } - } - Some(file_response::Union::Block(block)) => { - if let Some(job) = fs::get_job(block.id, &mut self.write_jobs) { - if let Err(_err) = job.write(block, None).await { - // to-do: add "skip" for writing job - } - self.update_jobs_status(); - } - } - Some(file_response::Union::Done(d)) => { - if let Some(job) = fs::get_job(d.id, &mut self.write_jobs) { - job.modify_time(); - fs::remove_job(d.id, &mut self.write_jobs); - } - self.handle_job_status(d.id, d.file_num, None); - } - Some(file_response::Union::Error(e)) => { - self.handle_job_status(e.id, e.file_num, Some(e.error)); - } - Some(file_response::Union::Digest(digest)) => { - if digest.is_upload { - if let Some(job) = fs::get_job(digest.id, &mut self.read_jobs) { - if let Some(file) = job.files().get(digest.file_num as usize) { - let read_path = get_string(&job.join(&file.name)); - let overwrite_strategy = job.default_overwrite_strategy(); - if let Some(overwrite) = overwrite_strategy { - let req = FileTransferSendConfirmRequest { - id: digest.id, - file_num: digest.file_num, - union: Some(if overwrite { - file_transfer_send_confirm_request::Union::OffsetBlk(0) - } else { - file_transfer_send_confirm_request::Union::Skip( - true, - ) - }), - ..Default::default() - }; - job.confirm(&req); - let msg = new_send_confirm(req); - allow_err!(peer.send(&msg).await); - } else { - self.handle_override_file_confirm( - digest.id, - digest.file_num, - read_path, - true, - ); - } - } - } - } else { - if let Some(job) = fs::get_job(digest.id, &mut self.write_jobs) { - if let Some(file) = job.files().get(digest.file_num as usize) { - let write_path = get_string(&job.join(&file.name)); - let overwrite_strategy = job.default_overwrite_strategy(); - match fs::is_write_need_confirmation(&write_path, &digest) { - Ok(res) => match res { - DigestCheckResult::IsSame => { - let msg= new_send_confirm(FileTransferSendConfirmRequest { - id: digest.id, - file_num: digest.file_num, - union: Some(file_transfer_send_confirm_request::Union::Skip(true)), - ..Default::default() - }); - self.session.send_msg(msg); - } - DigestCheckResult::NeedConfirm(digest) => { - if let Some(overwrite) = overwrite_strategy { - let msg = new_send_confirm( - FileTransferSendConfirmRequest { - id: digest.id, - file_num: digest.file_num, - union: Some(if overwrite { - file_transfer_send_confirm_request::Union::OffsetBlk(0) - } else { - file_transfer_send_confirm_request::Union::Skip(true) - }), - ..Default::default() - }, - ); - self.session.send_msg(msg); - } else { - self.handle_override_file_confirm( - digest.id, - digest.file_num, - write_path.to_string(), - false, - ); - } - } - DigestCheckResult::NoSuchFile => { - let msg = new_send_confirm( - FileTransferSendConfirmRequest { - id: digest.id, - file_num: digest.file_num, - union: Some(file_transfer_send_confirm_request::Union::OffsetBlk(0)), - ..Default::default() - }, - ); - self.session.send_msg(msg); - } - }, - Err(err) => { - println!("error recving digest: {}", err); - } - } - } - } - } - } - _ => {} - } - } - Some(message::Union::Misc(misc)) => match misc.union { - Some(misc::Union::AudioFormat(f)) => { - self.audio_handler.handle_format(f); // - } - Some(misc::Union::ChatMessage(c)) => { - self.session - .push_event("chat_client_mode", vec![("text", &c.text)]); - } - Some(misc::Union::PermissionInfo(p)) => { - log::info!("Change permission {:?} -> {}", p.permission, p.enabled); - use permission_info::Permission; - self.session.push_event( - "permission", - vec![( - match p.permission.enum_value_or_default() { - Permission::Keyboard => "keyboard", - Permission::Clipboard => "clipboard", - Permission::Audio => "audio", - Permission::Restart => "restart", - _ => "", - }, - &p.enabled.to_string(), - )], - ); - } - Some(misc::Union::SwitchDisplay(s)) => { - self.video_handler.reset(); - self.session.push_event( - "switch_display", - vec![ - ("display", &s.display.to_string()), - ("x", &s.x.to_string()), - ("y", &s.y.to_string()), - ("width", &s.width.to_string()), - ("height", &s.height.to_string()), - ], - ); - } - Some(misc::Union::CloseReason(c)) => { - self.session.msgbox("error", "Connection Error", &c); - return false; - } - Some(misc::Union::BackNotification(notification)) => { - if !self.handle_back_notification(notification).await { - return false; - } - } - _ => {} - }, - Some(message::Union::TestDelay(t)) => { - self.session.handle_test_delay(t, peer).await; - } - Some(message::Union::AudioFrame(frame)) => { - if !self.session.lc.read().unwrap().disable_audio { - self.audio_handler.handle_frame(frame); - } - } - Some(message::Union::FileAction(action)) => match action.union { - Some(file_action::Union::SendConfirm(c)) => { - if let Some(job) = fs::get_job(c.id, &mut self.read_jobs) { - job.confirm(&c); - } - } - _ => {} - }, - _ => {} - } - } - true - } +// /// Close the session. +// pub fn close(&self) { +// self.send(Data::Close); +// } - async fn handle_back_notification(&mut self, notification: BackNotification) -> bool { - match notification.union { - Some(back_notification::Union::BlockInputState(state)) => { - self.handle_back_msg_block_input( - state.enum_value_or(back_notification::BlockInputState::BlkStateUnknown), - ) - .await; - } - Some(back_notification::Union::PrivacyModeState(state)) => { - if !self - .handle_back_msg_privacy_mode( - state.enum_value_or(back_notification::PrivacyModeState::PrvStateUnknown), - ) - .await - { - return false; - } - } - _ => {} - } - true - } +// /// Reconnect to the current session. +// pub fn reconnect(&self) { +// self.send(Data::Close); +// let session = self.clone(); +// std::thread::spawn(move || { +// Connection::start(session, false, false); +// }); +// } - #[inline(always)] - fn update_block_input_state(&mut self, on: bool) { - self.session.push_event( - "update_block_input_state", - [("input_state", if on { "on" } else { "off" })].into(), - ); - } +// /// Get `remember` flag in [`LoginConfigHandler`]. +// pub fn get_remember(&self) -> bool { +// self.lc.read().unwrap().remember +// } - async fn handle_back_msg_block_input(&mut self, state: back_notification::BlockInputState) { - match state { - back_notification::BlockInputState::BlkOnSucceeded => { - self.update_block_input_state(true); - } - back_notification::BlockInputState::BlkOnFailed => { - self.session - .msgbox("custom-error", "Block user input", "Failed"); - self.update_block_input_state(false); - } - back_notification::BlockInputState::BlkOffSucceeded => { - self.update_block_input_state(false); - } - back_notification::BlockInputState::BlkOffFailed => { - self.session - .msgbox("custom-error", "Unblock user input", "Failed"); - } - _ => {} - } - } +// /// Send message over the current session. +// /// +// /// # Arguments +// /// +// /// * `msg` - The message to send. +// #[inline] +// pub fn send_msg(&self, msg: Message) { +// self.send(Data::Message(msg)); +// } - #[inline(always)] - fn update_privacy_mode(&mut self, on: bool) { - let mut config = self.session.load_config(); - config.privacy_mode = on; - self.session.save_config(&config); - self.session.lc.write().unwrap().get_config().privacy_mode = on; - self.session.push_event("update_privacy_mode", [].into()); - } +// /// Send chat message over the current session. +// /// +// /// # Arguments +// /// +// /// * `text` - The message to send. +// pub fn send_chat(&self, text: String) { +// let mut misc = Misc::new(); +// misc.set_chat_message(ChatMessage { +// text, +// ..Default::default() +// }); +// let mut msg_out = Message::new(); +// msg_out.set_misc(misc); +// self.send_msg(msg_out); +// } - async fn handle_back_msg_privacy_mode( - &mut self, - state: back_notification::PrivacyModeState, - ) -> bool { - match state { - back_notification::PrivacyModeState::PrvOnByOther => { - self.session.msgbox( - "error", - "Connecting...", - "Someone turns on privacy mode, exit", - ); - return false; - } - back_notification::PrivacyModeState::PrvNotSupported => { - self.session - .msgbox("custom-error", "Privacy mode", "Unsupported"); - self.update_privacy_mode(false); - } - back_notification::PrivacyModeState::PrvOnSucceeded => { - self.session - .msgbox("custom-nocancel", "Privacy mode", "In privacy mode"); - self.update_privacy_mode(true); - } - back_notification::PrivacyModeState::PrvOnFailedDenied => { - self.session - .msgbox("custom-error", "Privacy mode", "Peer denied"); - self.update_privacy_mode(false); - } - back_notification::PrivacyModeState::PrvOnFailedPlugin => { - self.session - .msgbox("custom-error", "Privacy mode", "Please install plugins"); - self.update_privacy_mode(false); - } - back_notification::PrivacyModeState::PrvOnFailed => { - self.session - .msgbox("custom-error", "Privacy mode", "Failed"); - self.update_privacy_mode(false); - } - back_notification::PrivacyModeState::PrvOffSucceeded => { - self.session - .msgbox("custom-nocancel", "Privacy mode", "Out privacy mode"); - self.update_privacy_mode(false); - } - back_notification::PrivacyModeState::PrvOffByPeer => { - self.session - .msgbox("custom-error", "Privacy mode", "Peer exit"); - self.update_privacy_mode(false); - } - back_notification::PrivacyModeState::PrvOffFailed => { - self.session - .msgbox("custom-error", "Privacy mode", "Failed to turn off"); - } - back_notification::PrivacyModeState::PrvOffUnknown => { - self.session - .msgbox("custom-error", "Privacy mode", "Turned off"); - // log::error!("Privacy mode is turned off with unknown reason"); - self.update_privacy_mode(false); - } - _ => {} - } - true - } +// /// Push an event to the event queue. +// /// An event is stored as json in the event queue. +// /// +// /// # Arguments +// /// +// /// * `name` - The name of the event. +// /// * `event` - Fields of the event content. +// fn push_event(&self, name: &str, event: Vec<(&str, &str)>) { +// let mut h: HashMap<&str, &str> = event.iter().cloned().collect(); +// assert!(h.get("name").is_none()); +// h.insert("name", name); +// let out = serde_json::ser::to_string(&h).unwrap_or("".to_owned()); +// if let Some(stream) = &*self.events2ui.read().unwrap() { +// stream.add(EventToUI::Event(out)); +// } +// } - async fn handle_msg_from_ui(&mut self, data: Data, peer: &mut Stream) -> bool { - match data { - Data::Close => { - self.sync_jobs_status_to_local().await; - return false; - } - Data::Login((password, remember)) => { - self.session - .handle_login_from_ui(password, remember, peer) - .await; - } - Data::Message(msg) => { - allow_err!(peer.send(&msg).await); - } - Data::SendFiles((id, path, to, file_num, include_hidden, is_remote)) => { - let od = can_enable_overwrite_detection(self.session.lc.read().unwrap().version); - if is_remote { - log::debug!("New job {}, write to {} from remote {}", id, to, path); - self.write_jobs.push(fs::TransferJob::new_write( - id, - path.clone(), - to, - file_num, - include_hidden, - is_remote, - Vec::new(), - od, - )); - allow_err!( - peer.send(&fs::new_send(id, path, file_num, include_hidden)) - .await - ); - } else { - match fs::TransferJob::new_read( - id, - to.clone(), - path.clone(), - file_num, - include_hidden, - is_remote, - od, - ) { - Err(err) => { - self.handle_job_status(id, -1, Some(err.to_string())); - } - Ok(job) => { - log::debug!( - "New job {}, read {} to remote {}, {} files", - id, - path, - to, - job.files().len() - ); - let m = make_fd_flutter(id, job.files(), true); - self.session - .push_event("update_folder_files", vec![("info", &m)]); - let files = job.files().clone(); - self.read_jobs.push(job); - self.timer = time::interval(MILLI1); - allow_err!(peer.send(&fs::new_receive(id, to, file_num, files)).await); - } - } - } - } - Data::RemoveDirAll((id, path, is_remote, include_hidden)) => { - if is_remote { - let mut msg_out = Message::new(); - let mut file_action = FileAction::new(); - file_action.set_all_files(ReadAllFiles { - id, - path: path.clone(), - include_hidden, - ..Default::default() - }); - msg_out.set_file_action(file_action); - allow_err!(peer.send(&msg_out).await); - } else { - match fs::get_recursive_files(&path, include_hidden) { - Ok(entries) => { - let mut fd = FileDirectory::new(); - fd.id = id; - fd.path = path; - fd.entries = entries; - self.session.push_event( - "file_dir", - vec![("value", &make_fd_to_json(fd)), ("is_local", "true")], - ); - } - Err(err) => { - self.handle_job_status(id, -1, Some(err.to_string())); - } - } - } - } - Data::CancelJob(id) => { - let mut msg_out = Message::new(); - let mut file_action = FileAction::new(); - file_action.set_cancel(FileTransferCancel { - id: id, - ..Default::default() - }); - msg_out.set_file_action(file_action); - allow_err!(peer.send(&msg_out).await); - if let Some(job) = fs::get_job(id, &mut self.write_jobs) { - job.remove_download_file(); - fs::remove_job(id, &mut self.write_jobs); - } - fs::remove_job(id, &mut self.read_jobs); - } - Data::RemoveDir((id, path)) => { - let mut msg_out = Message::new(); - let mut file_action = FileAction::new(); - file_action.set_remove_dir(FileRemoveDir { - id, - path, - recursive: true, - ..Default::default() - }); - msg_out.set_file_action(file_action); - allow_err!(peer.send(&msg_out).await); - } - Data::RemoveFile((id, path, file_num, is_remote)) => { - if is_remote { - let mut msg_out = Message::new(); - let mut file_action = FileAction::new(); - file_action.set_remove_file(FileRemoveFile { - id, - path, - file_num, - ..Default::default() - }); - msg_out.set_file_action(file_action); - allow_err!(peer.send(&msg_out).await); - } else { - match fs::remove_file(&path) { - Err(err) => { - self.handle_job_status(id, file_num, Some(err.to_string())); - } - Ok(()) => { - self.handle_job_status(id, file_num, None); - } - } - } - } - Data::CreateDir((id, path, is_remote)) => { - if is_remote { - let mut msg_out = Message::new(); - let mut file_action = FileAction::new(); - file_action.set_create(FileDirCreate { - id, - path, - ..Default::default() - }); - msg_out.set_file_action(file_action); - allow_err!(peer.send(&msg_out).await); - } else { - match fs::create_dir(&path) { - Err(err) => { - self.handle_job_status(id, -1, Some(err.to_string())); - } - Ok(()) => { - self.handle_job_status(id, -1, None); - } - } - } - } - Data::SetConfirmOverrideFile((id, file_num, need_override, remember, is_upload)) => { - if is_upload { - if let Some(job) = fs::get_job(id, &mut self.read_jobs) { - if remember { - job.set_overwrite_strategy(Some(need_override)); - } - job.confirm(&FileTransferSendConfirmRequest { - id, - file_num, - union: if need_override { - Some(file_transfer_send_confirm_request::Union::OffsetBlk(0)) - } else { - Some(file_transfer_send_confirm_request::Union::Skip(true)) - }, - ..Default::default() - }); - } - } else { - if let Some(job) = fs::get_job(id, &mut self.write_jobs) { - if remember { - job.set_overwrite_strategy(Some(need_override)); - } - let mut msg = Message::new(); - let mut file_action = FileAction::new(); - file_action.set_send_confirm(FileTransferSendConfirmRequest { - id, - file_num, - union: if need_override { - Some(file_transfer_send_confirm_request::Union::OffsetBlk(0)) - } else { - Some(file_transfer_send_confirm_request::Union::Skip(true)) - }, - ..Default::default() - }); - msg.set_file_action(file_action); - self.session.send_msg(msg); - } - } - } - Data::AddJob((id, path, to, file_num, include_hidden, is_remote)) => { - let od = can_enable_overwrite_detection(self.session.lc.read().unwrap().version); - if is_remote { - log::debug!( - "new write waiting job {}, write to {} from remote {}", - id, - to, - path - ); - let mut job = fs::TransferJob::new_write( - id, - path.clone(), - to, - file_num, - include_hidden, - is_remote, - Vec::new(), - od, - ); - job.is_last_job = true; - self.write_jobs.push(job); - } else { - match fs::TransferJob::new_read( - id, - to.clone(), - path.clone(), - file_num, - include_hidden, - is_remote, - od, - ) { - Err(err) => { - self.handle_job_status(id, -1, Some(err.to_string())); - } - Ok(mut job) => { - log::debug!( - "new read waiting job {}, read {} to remote {}, {} files", - id, - path, - to, - job.files().len() - ); - let m = make_fd_flutter(job.id(), job.files(), true); - self.session - .push_event("update_folder_files", vec![("info", &m)]); - job.is_last_job = true; - self.read_jobs.push(job); - self.timer = time::interval(MILLI1); - } - } - } - } - Data::ResumeJob((id, is_remote)) => { - if is_remote { - if let Some(job) = get_job(id, &mut self.write_jobs) { - job.is_last_job = false; - allow_err!( - peer.send(&fs::new_send( - id, - job.remote.clone(), - job.file_num, - job.show_hidden - )) - .await - ); - } - } else { - if let Some(job) = get_job(id, &mut self.read_jobs) { - job.is_last_job = false; - allow_err!( - peer.send(&fs::new_receive( - id, - job.path.to_string_lossy().to_string(), - job.file_num, - job.files.clone() - )) - .await - ); - } - } - } - _ => {} - } - true - } +// /// Get platform of peer. +// #[inline] +// fn peer_platform(&self) -> String { +// self.lc.read().unwrap().info.platform.clone() +// } - #[inline] - fn update_job_status( - job: &fs::TransferJob, - elapsed: i32, - last_update_jobs_status: &mut (Instant, HashMap), - session: &Session, - ) { - if elapsed <= 0 { - return; - } - let transferred = job.transferred(); - let last_transferred = { - if let Some(v) = last_update_jobs_status.1.get(&job.id()) { - v.to_owned() - } else { - 0 - } - }; - last_update_jobs_status.1.insert(job.id(), transferred); - let speed = (transferred - last_transferred) as f64 / (elapsed as f64 / 1000.); - let file_num = job.file_num() - 1; - session.push_event( - "job_progress", - vec![ - ("id", &job.id().to_string()), - ("file_num", &file_num.to_string()), - ("speed", &speed.to_string()), - ("finished_size", &job.finished_size().to_string()), - ], - ); - } +// /// Quick method for sending a ctrl_alt_del command. +// pub fn ctrl_alt_del(&self) { +// if self.peer_platform() == "Windows" { +// let k = Key::ControlKey(ControlKey::CtrlAltDel); +// self.key_down_or_up(1, k, false, false, false, false); +// } else { +// let k = Key::ControlKey(ControlKey::Delete); +// self.key_down_or_up(3, k, true, true, false, false); +// } +// } - fn update_jobs_status(&mut self) { - let elapsed = self.last_update_jobs_status.0.elapsed().as_millis() as i32; - if elapsed >= 1000 { - for job in self.read_jobs.iter() { - Self::update_job_status( - job, - elapsed, - &mut self.last_update_jobs_status, - &self.session, - ); - } - for job in self.write_jobs.iter() { - Self::update_job_status( - job, - elapsed, - &mut self.last_update_jobs_status, - &self.session, - ); - } - self.last_update_jobs_status.0 = Instant::now(); - } - } +// /// Switch the display. +// /// +// /// # Arguments +// /// +// /// * `display` - The display to switch to. +// pub fn switch_display(&self, display: i32) { +// let mut misc = Misc::new(); +// misc.set_switch_display(SwitchDisplay { +// display, +// ..Default::default() +// }); +// let mut msg_out = Message::new(); +// msg_out.set_misc(misc); +// self.send_msg(msg_out); +// } - fn handle_job_status(&mut self, id: i32, file_num: i32, err: Option) { - if let Some(err) = err { - self.session - .push_event("job_error", vec![("id", &id.to_string()), ("err", &err)]); - } else { - self.session.push_event( - "job_done", - vec![("id", &id.to_string()), ("file_num", &file_num.to_string())], - ); - } - } +// /// Send lock screen command. +// pub fn lock_screen(&self) { +// let k = Key::ControlKey(ControlKey::LockScreen); +// self.key_down_or_up(1, k, false, false, false, false); +// } - fn handle_override_file_confirm( - &mut self, - id: i32, - file_num: i32, - read_path: String, - is_upload: bool, - ) { - self.session.push_event( - "override_file_confirm", - vec![ - ("id", &id.to_string()), - ("file_num", &file_num.to_string()), - ("read_path", &read_path), - ("is_upload", &is_upload.to_string()), - ], - ); - } +// /// Send key input command. +// /// +// /// # Arguments +// /// +// /// * `name` - The name of the key. +// /// * `down` - Whether the key is down or up. +// /// * `press` - If the key is simply being pressed(Down+Up). +// /// * `alt` - If the alt key is also pressed. +// /// * `ctrl` - If the ctrl key is also pressed. +// /// * `shift` - If the shift key is also pressed. +// /// * `command` - If the command key is also pressed. +// pub fn input_key( +// &self, +// name: &str, +// down: bool, +// press: bool, +// alt: bool, +// ctrl: bool, +// shift: bool, +// command: bool, +// ) { +// let chars: Vec = name.chars().collect(); +// if chars.len() == 1 { +// let key = Key::_Raw(chars[0] as _); +// self._input_key(key, down, press, alt, ctrl, shift, command); +// } else { +// if let Some(key) = KEY_MAP.get(name) { +// self._input_key(key.clone(), down, press, alt, ctrl, shift, command); +// } +// } +// } - async fn sync_jobs_status_to_local(&mut self) -> bool { - log::info!("sync transfer job status"); - let mut config: PeerConfig = self.session.load_config(); - let mut transfer_metas = TransferSerde::default(); - for job in self.read_jobs.iter() { - let json_str = serde_json::to_string(&job.gen_meta()).unwrap(); - transfer_metas.read_jobs.push(json_str); - } - for job in self.write_jobs.iter() { - let json_str = serde_json::to_string(&job.gen_meta()).unwrap(); - transfer_metas.write_jobs.push(json_str); - } - log::info!("meta: {:?}", transfer_metas); - config.transfer = transfer_metas; - self.session.save_config(&config); - true - } -} +// /// Input a string of text. +// /// String is parsed into individual key presses. +// /// +// /// # Arguments +// /// +// /// * `value` - The text to input. TODO &str -> String +// pub fn input_string(&self, value: &str) { +// let mut key_event = KeyEvent::new(); +// key_event.set_seq(value.to_owned()); +// let mut msg_out = Message::new(); +// msg_out.set_key_event(key_event); +// self.send_msg(msg_out); +// } + +// fn _input_key( +// &self, +// key: Key, +// down: bool, +// press: bool, +// alt: bool, +// ctrl: bool, +// shift: bool, +// command: bool, +// ) { +// let v = if press { +// 3 +// } else if down { +// 1 +// } else { +// 0 +// }; +// self.key_down_or_up(v, key, alt, ctrl, shift, command); +// } + +// pub fn send_mouse( +// &self, +// mask: i32, +// x: i32, +// y: i32, +// alt: bool, +// ctrl: bool, +// shift: bool, +// command: bool, +// ) { +// send_mouse(mask, x, y, alt, ctrl, shift, command, self); +// } + +// fn key_down_or_up( +// &self, +// down_or_up: i32, +// key: Key, +// alt: bool, +// ctrl: bool, +// shift: bool, +// command: bool, +// ) { +// let mut down_or_up = down_or_up; +// let mut key_event = KeyEvent::new(); +// match key { +// Key::Chr(chr) => { +// key_event.set_chr(chr); +// } +// Key::ControlKey(key) => { +// key_event.set_control_key(key.clone()); +// } +// Key::_Raw(raw) => { +// if raw > 'z' as u32 || raw < 'a' as u32 { +// key_event.set_unicode(raw); +// if down_or_up == 0 { +// // ignore up, avoiding trigger twice +// return; +// } +// down_or_up = 1; // if press, turn into down for avoiding trigger twice on server side +// } else { +// // to make ctrl+c works on windows +// key_event.set_chr(raw); +// } +// } +// } +// if alt { +// key_event.modifiers.push(ControlKey::Alt.into()); +// } +// if shift { +// key_event.modifiers.push(ControlKey::Shift.into()); +// } +// if ctrl { +// key_event.modifiers.push(ControlKey::Control.into()); +// } +// if command { +// key_event.modifiers.push(ControlKey::Meta.into()); +// } +// if down_or_up == 1 { +// key_event.down = true; +// } else if down_or_up == 3 { +// key_event.press = true; +// } +// let mut msg_out = Message::new(); +// msg_out.set_key_event(key_event); +// log::debug!("{:?}", msg_out); +// self.send_msg(msg_out); +// } + +// pub fn load_config(&self) -> PeerConfig { +// load_config(&self.id) +// } + +// pub fn save_config(&self, config: &PeerConfig) { +// config.store(&self.id); +// } + +// pub fn get_platform(&self, is_remote: bool) -> String { +// if is_remote { +// self.lc.read().unwrap().info.platform.clone() +// } else { +// whoami::platform().to_string() +// } +// } + +// pub fn load_last_jobs(&self) { +// let pc = self.load_config(); +// if pc.transfer.write_jobs.is_empty() && pc.transfer.read_jobs.is_empty() { +// // no last jobs +// return; +// } +// let mut cnt = 1; +// for job_str in pc.transfer.read_jobs.iter() { +// if !job_str.is_empty() { +// self.push_event("load_last_job", vec![("value", job_str)]); +// cnt += 1; +// println!("restore read_job: {:?}", job_str); +// } +// } +// for job_str in pc.transfer.write_jobs.iter() { +// if !job_str.is_empty() { +// self.push_event("load_last_job", vec![("value", job_str)]); +// cnt += 1; +// println!("restore write_job: {:?}", job_str); +// } +// } +// } + +// fn update_quality_status(&self, status: QualityStatus) { +// const NULL: String = String::new(); +// self.push_event( +// "update_quality_status", +// vec![ +// ("speed", &status.speed.map_or(NULL, |it| it)), +// ("fps", &status.fps.map_or(NULL, |it| it.to_string())), +// ("delay", &status.delay.map_or(NULL, |it| it.to_string())), +// ( +// "target_bitrate", +// &status.target_bitrate.map_or(NULL, |it| it.to_string()), +// ), +// ( +// "codec_format", +// &status.codec_format.map_or(NULL, |it| it.to_string()), +// ), +// ], +// ); +// } + +// pub fn remove_port_forward(&mut self, port: i32) { +// let mut config = self.load_config(); +// config.port_forwards = config +// .port_forwards +// .drain(..) +// .filter(|x| x.0 != port) +// .collect(); +// self.save_config(&config); +// self.send(Data::RemovePortForward(port)); +// } + +// pub fn add_port_forward(&mut self, port: i32, remote_host: String, remote_port: i32) { +// let mut config = self.load_config(); +// if config +// .port_forwards +// .iter() +// .filter(|x| x.0 == port) +// .next() +// .is_some() +// { +// return; +// } +// let pf = (port, remote_host, remote_port); +// config.port_forwards.push(pf.clone()); +// self.save_config(&config); +// self.send(Data::AddPortForward(pf)); +// } + +// fn on_error(&self, err: &str) { +// self.msgbox("error", "Error", err); +// } +// } + +// impl FileManager for Session {} + +// #[async_trait] +// impl Interface for Session { +// fn send(&self, data: Data) { +// if let Some(sender) = self.sender.read().unwrap().as_ref() { +// sender.send(data).ok(); +// } +// } + +// fn is_file_transfer(&self) -> bool { +// todo!() +// } + +// fn is_port_forward(&self) -> bool { +// todo!() +// } + +// fn is_rdp(&self) -> bool { +// todo!() +// } + +// fn msgbox(&self, msgtype: &str, title: &str, text: &str) { +// let has_retry = if check_if_retry(msgtype, title, text) { +// "true" +// } else { +// "" +// }; +// self.push_event( +// "msgbox", +// vec![ +// ("type", msgtype), +// ("title", title), +// ("text", text), +// ("hasRetry", has_retry), +// ], +// ); +// } + +// fn handle_login_error(&mut self, err: &str) -> bool { +// self.lc.write().unwrap().handle_login_error(err, self) +// } + +// fn handle_peer_info(&mut self, pi: PeerInfo) { +// let mut lc = self.lc.write().unwrap(); +// let username = lc.get_username(&pi); +// let mut displays = Vec::new(); +// let mut current = pi.current_display as usize; + +// if lc.is_file_transfer { +// if pi.username.is_empty() { +// self.msgbox( +// "error", +// "Error", +// "No active console user logged on, please connect and logon first.", +// ); +// return; +// } +// } else { +// if pi.displays.is_empty() { +// self.msgbox("error", "Remote Error", "No Display"); +// } +// 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); +// displays.push(h); +// } +// if current >= pi.displays.len() { +// current = 0; +// } +// } +// let displays = serde_json::ser::to_string(&displays).unwrap_or("".to_owned()); +// self.push_event( +// "peer_info", +// vec![ +// ("username", &username), +// ("hostname", &pi.hostname), +// ("platform", &pi.platform), +// ("sas_enabled", &pi.sas_enabled.to_string()), +// ("displays", &displays), +// ("version", &pi.version), +// ("current_display", ¤t.to_string()), +// ("is_file_transfer", &lc.is_file_transfer.to_string()), +// ], +// ); +// lc.handle_peer_info(username, pi); +// let p = lc.should_auto_login(); +// if !p.is_empty() { +// input_os_password(p, true, self.clone()); +// } +// } + +// fn set_force_relay(&mut self, direct: bool, received: bool) { +// let mut lc = self.lc.write().unwrap(); +// lc.force_relay = false; +// if direct && !received { +// let errno = errno::errno().0; +// log::info!("errno is {}", errno); +// // TODO: check mac and ios +// if cfg!(windows) && errno == 10054 || !cfg!(windows) && errno == 104 { +// lc.force_relay = true; +// lc.set_option("force-always-relay".to_owned(), "Y".to_owned()); +// } +// } +// } + +// fn is_force_relay(&self) -> bool { +// self.lc.read().unwrap().force_relay +// } + +// async fn handle_hash(&mut self, pass: &str, hash: Hash, peer: &mut Stream) { +// handle_hash(self.lc.clone(), pass, hash, self, peer).await; +// } + +// async fn handle_login_from_ui(&mut self, password: String, remember: bool, peer: &mut Stream) { +// handle_login_from_ui(self.lc.clone(), password, remember, peer).await; +// } + +// async fn handle_test_delay(&mut self, t: TestDelay, peer: &mut Stream) { +// if !t.from_client { +// self.update_quality_status(QualityStatus { +// delay: Some(t.last_delay as _), +// target_bitrate: Some(t.target_bitrate as _), +// ..Default::default() +// }); +// handle_test_delay(t, peer).await; +// } +// } +// } + +// struct Connection { +// video_handler: VideoHandler, +// audio_handler: AudioHandler, +// session: Session, +// first_frame: bool, +// read_jobs: Vec, +// write_jobs: Vec, +// timer: Interval, +// last_update_jobs_status: (Instant, HashMap), +// data_count: Arc, +// frame_count: Arc, +// video_format: CodecFormat, +// } + +// impl Connection { +// // TODO: Similar to remote::start_clipboard +// // merge the code +// fn start_clipboard( +// tx_protobuf: mpsc::UnboundedSender, +// lc: Arc>, +// ) -> Option> { +// let (tx, rx) = std::sync::mpsc::channel(); +// #[cfg(not(any(target_os = "android", target_os = "ios")))] +// match ClipboardContext::new() { +// Ok(mut ctx) => { +// let old_clipboard: Arc> = Default::default(); +// // 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 +// { +// 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) +// } + +// /// Create a new connection. +// /// +// /// # Arguments +// /// +// /// * `session` - The session to create a new connection for. +// /// * `is_file_transfer` - Whether the connection is for file transfer. +// /// * `is_port_forward` - Whether the connection is for port forward. +// #[tokio::main(flavor = "current_thread")] +// async fn start(session: Session, is_file_transfer: bool, is_port_forward: bool) { +// let mut last_recv_time = Instant::now(); +// let (sender, mut receiver) = mpsc::unbounded_channel::(); +// let mut stop_clipboard = None; +// if !is_file_transfer && !is_port_forward { +// stop_clipboard = Self::start_clipboard(sender.clone(), session.lc.clone()); +// } +// *session.sender.write().unwrap() = Some(sender.clone()); +// let conn_type = if is_file_transfer { +// session.lc.write().unwrap().is_file_transfer = true; +// ConnType::FILE_TRANSFER +// } else if is_port_forward { +// ConnType::PORT_FORWARD // TODO: RDP +// } else { +// ConnType::DEFAULT_CONN +// }; +// let key = Config::get_option("key"); +// let token = Config::get_option("access_token"); + +// // TODO rdp & cli args +// let is_rdp = false; +// let args: Vec = Vec::new(); + +// if is_port_forward { +// if is_rdp { +// // let port = handler +// // .get_option("rdp_port".to_owned()) +// // .parse::() +// // .unwrap_or(3389); +// // std::env::set_var( +// // "rdp_username", +// // handler.get_option("rdp_username".to_owned()), +// // ); +// // std::env::set_var( +// // "rdp_password", +// // handler.get_option("rdp_password".to_owned()), +// // ); +// // log::info!("Remote rdp port: {}", port); +// // start_one_port_forward(handler, 0, "".to_owned(), port, receiver, &key, &token).await; +// } else if args.len() == 0 { +// let pfs = session.lc.read().unwrap().port_forwards.clone(); +// let mut queues = HashMap::>::new(); +// for d in pfs { +// sender.send(Data::AddPortForward(d)).ok(); +// } +// loop { +// match receiver.recv().await { +// Some(Data::AddPortForward((port, remote_host, remote_port))) => { +// if port <= 0 || remote_port <= 0 { +// continue; +// } +// let (sender, receiver) = mpsc::unbounded_channel::(); +// queues.insert(port, sender); +// let handler = session.clone(); +// let key = key.clone(); +// let token = token.clone(); +// tokio::spawn(async move { +// start_one_port_forward( +// handler, +// port, +// remote_host, +// remote_port, +// receiver, +// &key, +// &token, +// ) +// .await; +// }); +// } +// Some(Data::RemovePortForward(port)) => { +// if let Some(s) = queues.remove(&port) { +// s.send(Data::Close).ok(); +// } +// } +// Some(Data::Close) => { +// break; +// } +// Some(d) => { +// for (_, s) in queues.iter() { +// s.send(d.clone()).ok(); +// } +// } +// _ => {} +// } +// } +// } else { +// // let port = handler.args[0].parse::().unwrap_or(0); +// // if handler.args.len() != 3 +// // || handler.args[2].parse::().unwrap_or(0) <= 0 +// // || port <= 0 +// // { +// // handler.on_error("Invalid arguments, usage:

    rustdesk --port-forward remote-id listen-port remote-host remote-port"); +// // } +// // let remote_host = handler.args[1].clone(); +// // let remote_port = handler.args[2].parse::().unwrap_or(0); +// // start_one_port_forward( +// // handler, +// // port, +// // remote_host, +// // remote_port, +// // receiver, +// // &key, +// // &token, +// // ) +// // .await; +// } +// return; +// } + +// let latency_controller = LatencyController::new(); +// let latency_controller_cl = latency_controller.clone(); + +// let mut conn = Connection { +// video_handler: VideoHandler::new(latency_controller), +// audio_handler: AudioHandler::new(latency_controller_cl), +// session: session.clone(), +// first_frame: false, +// read_jobs: Vec::new(), +// write_jobs: Vec::new(), +// timer: time::interval(SEC30), +// last_update_jobs_status: (Instant::now(), Default::default()), +// data_count: Arc::new(AtomicUsize::new(0)), +// frame_count: Arc::new(AtomicUsize::new(0)), +// video_format: CodecFormat::Unknown, +// }; + +// match Client::start(&session.id, &key, &token, conn_type, session.clone()).await { +// Ok((mut peer, direct)) => { +// SERVER_KEYBOARD_ENABLED.store(true, Ordering::SeqCst); +// SERVER_CLIPBOARD_ENABLED.store(true, Ordering::SeqCst); + +// session.push_event( +// "connection_ready", +// vec![ +// ("secure", &peer.is_secured().to_string()), +// ("direct", &direct.to_string()), +// ], +// ); + +// let mut status_timer = time::interval(Duration::new(1, 0)); + +// loop { +// tokio::select! { +// res = peer.next() => { +// if let Some(res) = res { +// match res { +// Err(err) => { +// log::error!("Connection closed: {}", err); +// session.msgbox("error", "Connection Error", &err.to_string()); +// break; +// } +// Ok(ref bytes) => { +// last_recv_time = Instant::now(); +// conn.data_count.fetch_add(bytes.len(), Ordering::Relaxed); +// if !conn.handle_msg_from_peer(bytes, &mut peer).await { +// break +// } +// } +// } +// } else { +// if session.lc.read().unwrap().restarting_remote_device { +// log::info!("Restart remote device"); +// session.msgbox("restarting", "Restarting Remote Device", "remote_restarting_tip"); +// } else { +// log::info!("Reset by the peer"); +// session.msgbox("error", "Connection Error", "Reset by the peer"); +// } +// break; +// } +// } +// d = receiver.recv() => { +// if let Some(d) = d { +// if !conn.handle_msg_from_ui(d, &mut peer).await { +// break; +// } +// } +// } +// _ = conn.timer.tick() => { +// if last_recv_time.elapsed() >= SEC30 { +// session.msgbox("error", "Connection Error", "Timeout"); +// break; +// } +// if !conn.read_jobs.is_empty() { +// if let Err(err) = fs::handle_read_jobs(&mut conn.read_jobs, &mut peer).await { +// log::debug!("Connection Error: {}", err); +// break; +// } +// conn.update_jobs_status(); +// } else { +// conn.timer = time::interval_at(Instant::now() + SEC30, SEC30); +// } +// } +// _ = status_timer.tick() => { +// let speed = conn.data_count.swap(0, Ordering::Relaxed); +// let speed = format!("{:.2}kB/s", speed as f32 / 1024 as f32); +// let fps = conn.frame_count.swap(0, Ordering::Relaxed) as _; +// conn.session.update_quality_status(QualityStatus { +// speed:Some(speed), +// fps:Some(fps), +// ..Default::default() +// }); +// } +// } +// } +// log::debug!("Exit io_loop of id={}", session.id); +// } +// Err(err) => { +// session.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); +// } + +// /// Handle message from peer. +// /// Return false if the connection should be closed. +// /// +// /// The message is handled by [`Message`], see [`message::Union`] for possible types. +// async fn handle_msg_from_peer(&mut self, data: &[u8], peer: &mut Stream) -> bool { +// if let Ok(msg_in) = Message::parse_from_bytes(&data) { +// match msg_in.union { +// Some(message::Union::VideoFrame(vf)) => { +// if !self.first_frame { +// self.first_frame = true; +// common::send_opts_after_login(&self.session.lc.read().unwrap(), peer).await; +// } +// let incomming_format = CodecFormat::from(&vf); +// if self.video_format != incomming_format { +// self.video_format = incomming_format.clone(); +// self.session.update_quality_status(QualityStatus { +// codec_format: Some(incomming_format), +// ..Default::default() +// }) +// }; +// if let Ok(true) = self.video_handler.handle_frame(vf) { +// if let Some(stream) = &*self.session.events2ui.read().unwrap() { +// self.frame_count.fetch_add(1, Ordering::Relaxed); +// stream.add(EventToUI::Rgba(ZeroCopyBuffer( +// self.video_handler.rgb.clone(), +// ))); +// } +// } +// } +// Some(message::Union::Hash(hash)) => { +// self.session.handle_hash("", hash, peer).await; +// } +// Some(message::Union::LoginResponse(lr)) => match lr.union { +// Some(login_response::Union::Error(err)) => { +// if !self.session.handle_login_error(&err) { +// return false; +// } +// } +// Some(login_response::Union::PeerInfo(pi)) => { +// self.session.handle_peer_info(pi); +// } +// _ => {} +// }, +// Some(message::Union::Clipboard(cb)) => { +// if !self.session.lc.read().unwrap().disable_clipboard { +// let content = if cb.compress { +// decompress(&cb.content) +// } else { +// cb.content.into() +// }; +// if let Ok(content) = String::from_utf8(content) { +// self.session +// .push_event("clipboard", vec![("content", &content)]); +// } +// } +// } +// Some(message::Union::CursorData(cd)) => { +// let colors = hbb_common::compress::decompress(&cd.colors); +// self.session.push_event( +// "cursor_data", +// vec![ +// ("id", &cd.id.to_string()), +// ("hotx", &cd.hotx.to_string()), +// ("hoty", &cd.hoty.to_string()), +// ("width", &cd.width.to_string()), +// ("height", &cd.height.to_string()), +// ( +// "colors", +// &serde_json::ser::to_string(&colors).unwrap_or("".to_owned()), +// ), +// ], +// ); +// } +// Some(message::Union::CursorId(id)) => { +// self.session +// .push_event("cursor_id", vec![("id", &id.to_string())]); +// } +// Some(message::Union::CursorPosition(cp)) => { +// self.session.push_event( +// "cursor_position", +// vec![("x", &cp.x.to_string()), ("y", &cp.y.to_string())], +// ); +// } +// Some(message::Union::FileResponse(fr)) => { +// match fr.union { +// Some(file_response::Union::Dir(fd)) => { +// let mut entries = fd.entries.to_vec(); +// if self.session.peer_platform() == "Windows" { +// transform_windows_path(&mut entries); +// } +// let id = fd.id; +// self.session.push_event( +// "file_dir", +// vec![("value", &make_fd_to_json(fd)), ("is_local", "false")], +// ); +// if let Some(job) = fs::get_job(id, &mut self.write_jobs) { +// job.set_files(entries); +// } +// } +// Some(file_response::Union::Block(block)) => { +// if let Some(job) = fs::get_job(block.id, &mut self.write_jobs) { +// if let Err(_err) = job.write(block, None).await { +// // to-do: add "skip" for writing job +// } +// self.update_jobs_status(); +// } +// } +// Some(file_response::Union::Done(d)) => { +// if let Some(job) = fs::get_job(d.id, &mut self.write_jobs) { +// job.modify_time(); +// fs::remove_job(d.id, &mut self.write_jobs); +// } +// self.handle_job_status(d.id, d.file_num, None); +// } +// Some(file_response::Union::Error(e)) => { +// self.handle_job_status(e.id, e.file_num, Some(e.error)); +// } +// Some(file_response::Union::Digest(digest)) => { +// if digest.is_upload { +// if let Some(job) = fs::get_job(digest.id, &mut self.read_jobs) { +// if let Some(file) = job.files().get(digest.file_num as usize) { +// let read_path = get_string(&job.join(&file.name)); +// let overwrite_strategy = job.default_overwrite_strategy(); +// if let Some(overwrite) = overwrite_strategy { +// let req = FileTransferSendConfirmRequest { +// id: digest.id, +// file_num: digest.file_num, +// union: Some(if overwrite { +// file_transfer_send_confirm_request::Union::OffsetBlk(0) +// } else { +// file_transfer_send_confirm_request::Union::Skip( +// true, +// ) +// }), +// ..Default::default() +// }; +// job.confirm(&req); +// let msg = new_send_confirm(req); +// allow_err!(peer.send(&msg).await); +// } else { +// self.handle_override_file_confirm( +// digest.id, +// digest.file_num, +// read_path, +// true, +// ); +// } +// } +// } +// } else { +// if let Some(job) = fs::get_job(digest.id, &mut self.write_jobs) { +// if let Some(file) = job.files().get(digest.file_num as usize) { +// let write_path = get_string(&job.join(&file.name)); +// let overwrite_strategy = job.default_overwrite_strategy(); +// match fs::is_write_need_confirmation(&write_path, &digest) { +// Ok(res) => match res { +// DigestCheckResult::IsSame => { +// let msg= new_send_confirm(FileTransferSendConfirmRequest { +// id: digest.id, +// file_num: digest.file_num, +// union: Some(file_transfer_send_confirm_request::Union::Skip(true)), +// ..Default::default() +// }); +// self.session.send_msg(msg); +// } +// DigestCheckResult::NeedConfirm(digest) => { +// if let Some(overwrite) = overwrite_strategy { +// let msg = new_send_confirm( +// FileTransferSendConfirmRequest { +// id: digest.id, +// file_num: digest.file_num, +// union: Some(if overwrite { +// file_transfer_send_confirm_request::Union::OffsetBlk(0) +// } else { +// file_transfer_send_confirm_request::Union::Skip(true) +// }), +// ..Default::default() +// }, +// ); +// self.session.send_msg(msg); +// } else { +// self.handle_override_file_confirm( +// digest.id, +// digest.file_num, +// write_path.to_string(), +// false, +// ); +// } +// } +// DigestCheckResult::NoSuchFile => { +// let msg = new_send_confirm( +// FileTransferSendConfirmRequest { +// id: digest.id, +// file_num: digest.file_num, +// union: Some(file_transfer_send_confirm_request::Union::OffsetBlk(0)), +// ..Default::default() +// }, +// ); +// self.session.send_msg(msg); +// } +// }, +// Err(err) => { +// println!("error recving digest: {}", err); +// } +// } +// } +// } +// } +// } +// _ => {} +// } +// } +// Some(message::Union::Misc(misc)) => match misc.union { +// Some(misc::Union::AudioFormat(f)) => { +// self.audio_handler.handle_format(f); // +// } +// Some(misc::Union::ChatMessage(c)) => { +// self.session +// .push_event("chat_client_mode", vec![("text", &c.text)]); +// } +// Some(misc::Union::PermissionInfo(p)) => { +// log::info!("Change permission {:?} -> {}", p.permission, p.enabled); +// use permission_info::Permission; +// self.session.push_event( +// "permission", +// vec![( +// match p.permission.enum_value_or_default() { +// Permission::Keyboard => "keyboard", +// Permission::Clipboard => "clipboard", +// Permission::Audio => "audio", +// Permission::Restart => "restart", +// _ => "", +// }, +// &p.enabled.to_string(), +// )], +// ); +// } +// Some(misc::Union::SwitchDisplay(s)) => { +// self.video_handler.reset(); +// self.session.push_event( +// "switch_display", +// vec![ +// ("display", &s.display.to_string()), +// ("x", &s.x.to_string()), +// ("y", &s.y.to_string()), +// ("width", &s.width.to_string()), +// ("height", &s.height.to_string()), +// ], +// ); +// } +// Some(misc::Union::CloseReason(c)) => { +// self.session.msgbox("error", "Connection Error", &c); +// return false; +// } +// Some(misc::Union::BackNotification(notification)) => { +// if !self.handle_back_notification(notification).await { +// return false; +// } +// } +// _ => {} +// }, +// Some(message::Union::TestDelay(t)) => { +// self.session.handle_test_delay(t, peer).await; +// } +// Some(message::Union::AudioFrame(frame)) => { +// if !self.session.lc.read().unwrap().disable_audio { +// self.audio_handler.handle_frame(frame); +// } +// } +// Some(message::Union::FileAction(action)) => match action.union { +// Some(file_action::Union::SendConfirm(c)) => { +// if let Some(job) = fs::get_job(c.id, &mut self.read_jobs) { +// job.confirm(&c); +// } +// } +// _ => {} +// }, +// _ => {} +// } +// } +// true +// } + +// async fn handle_back_notification(&mut self, notification: BackNotification) -> bool { +// match notification.union { +// Some(back_notification::Union::BlockInputState(state)) => { +// self.handle_back_msg_block_input( +// state.enum_value_or(back_notification::BlockInputState::BlkStateUnknown), +// ) +// .await; +// } +// Some(back_notification::Union::PrivacyModeState(state)) => { +// if !self +// .handle_back_msg_privacy_mode( +// state.enum_value_or(back_notification::PrivacyModeState::PrvStateUnknown), +// ) +// .await +// { +// return false; +// } +// } +// _ => {} +// } +// true +// } + +// #[inline(always)] +// fn update_block_input_state(&mut self, on: bool) { +// self.session.push_event( +// "update_block_input_state", +// [("input_state", if on { "on" } else { "off" })].into(), +// ); +// } + +// async fn handle_back_msg_block_input(&mut self, state: back_notification::BlockInputState) { +// match state { +// back_notification::BlockInputState::BlkOnSucceeded => { +// self.update_block_input_state(true); +// } +// back_notification::BlockInputState::BlkOnFailed => { +// self.session +// .msgbox("custom-error", "Block user input", "Failed"); +// self.update_block_input_state(false); +// } +// back_notification::BlockInputState::BlkOffSucceeded => { +// self.update_block_input_state(false); +// } +// back_notification::BlockInputState::BlkOffFailed => { +// self.session +// .msgbox("custom-error", "Unblock user input", "Failed"); +// } +// _ => {} +// } +// } + +// #[inline(always)] +// fn update_privacy_mode(&mut self, on: bool) { +// let mut config = self.session.load_config(); +// config.privacy_mode = on; +// self.session.save_config(&config); +// self.session.lc.write().unwrap().get_config().privacy_mode = on; +// self.session.push_event("update_privacy_mode", [].into()); +// } + +// async fn handle_back_msg_privacy_mode( +// &mut self, +// state: back_notification::PrivacyModeState, +// ) -> bool { +// match state { +// back_notification::PrivacyModeState::PrvOnByOther => { +// self.session.msgbox( +// "error", +// "Connecting...", +// "Someone turns on privacy mode, exit", +// ); +// return false; +// } +// back_notification::PrivacyModeState::PrvNotSupported => { +// self.session +// .msgbox("custom-error", "Privacy mode", "Unsupported"); +// self.update_privacy_mode(false); +// } +// back_notification::PrivacyModeState::PrvOnSucceeded => { +// self.session +// .msgbox("custom-nocancel", "Privacy mode", "In privacy mode"); +// self.update_privacy_mode(true); +// } +// back_notification::PrivacyModeState::PrvOnFailedDenied => { +// self.session +// .msgbox("custom-error", "Privacy mode", "Peer denied"); +// self.update_privacy_mode(false); +// } +// back_notification::PrivacyModeState::PrvOnFailedPlugin => { +// self.session +// .msgbox("custom-error", "Privacy mode", "Please install plugins"); +// self.update_privacy_mode(false); +// } +// back_notification::PrivacyModeState::PrvOnFailed => { +// self.session +// .msgbox("custom-error", "Privacy mode", "Failed"); +// self.update_privacy_mode(false); +// } +// back_notification::PrivacyModeState::PrvOffSucceeded => { +// self.session +// .msgbox("custom-nocancel", "Privacy mode", "Out privacy mode"); +// self.update_privacy_mode(false); +// } +// back_notification::PrivacyModeState::PrvOffByPeer => { +// self.session +// .msgbox("custom-error", "Privacy mode", "Peer exit"); +// self.update_privacy_mode(false); +// } +// back_notification::PrivacyModeState::PrvOffFailed => { +// self.session +// .msgbox("custom-error", "Privacy mode", "Failed to turn off"); +// } +// back_notification::PrivacyModeState::PrvOffUnknown => { +// self.session +// .msgbox("custom-error", "Privacy mode", "Turned off"); +// // log::error!("Privacy mode is turned off with unknown reason"); +// self.update_privacy_mode(false); +// } +// _ => {} +// } +// true +// } + +// async fn handle_msg_from_ui(&mut self, data: Data, peer: &mut Stream) -> bool { +// match data { +// Data::Close => { +// self.sync_jobs_status_to_local().await; +// return false; +// } +// Data::Login((password, remember)) => { +// self.session +// .handle_login_from_ui(password, remember, peer) +// .await; +// } +// Data::Message(msg) => { +// allow_err!(peer.send(&msg).await); +// } +// Data::SendFiles((id, path, to, file_num, include_hidden, is_remote)) => { +// let od = can_enable_overwrite_detection(self.session.lc.read().unwrap().version); +// if is_remote { +// log::debug!("New job {}, write to {} from remote {}", id, to, path); +// self.write_jobs.push(fs::TransferJob::new_write( +// id, +// path.clone(), +// to, +// file_num, +// include_hidden, +// is_remote, +// Vec::new(), +// od, +// )); +// allow_err!( +// peer.send(&fs::new_send(id, path, file_num, include_hidden)) +// .await +// ); +// } else { +// match fs::TransferJob::new_read( +// id, +// to.clone(), +// path.clone(), +// file_num, +// include_hidden, +// is_remote, +// od, +// ) { +// Err(err) => { +// self.handle_job_status(id, -1, Some(err.to_string())); +// } +// Ok(job) => { +// log::debug!( +// "New job {}, read {} to remote {}, {} files", +// id, +// path, +// to, +// job.files().len() +// ); +// let m = make_fd_flutter(id, job.files(), true); +// self.session +// .push_event("update_folder_files", vec![("info", &m)]); +// let files = job.files().clone(); +// self.read_jobs.push(job); +// self.timer = time::interval(MILLI1); +// allow_err!(peer.send(&fs::new_receive(id, to, file_num, files)).await); +// } +// } +// } +// } +// Data::RemoveDirAll((id, path, is_remote, include_hidden)) => { +// if is_remote { +// let mut msg_out = Message::new(); +// let mut file_action = FileAction::new(); +// file_action.set_all_files(ReadAllFiles { +// id, +// path: path.clone(), +// include_hidden, +// ..Default::default() +// }); +// msg_out.set_file_action(file_action); +// allow_err!(peer.send(&msg_out).await); +// } else { +// match fs::get_recursive_files(&path, include_hidden) { +// Ok(entries) => { +// let mut fd = FileDirectory::new(); +// fd.id = id; +// fd.path = path; +// fd.entries = entries; +// self.session.push_event( +// "file_dir", +// vec![("value", &make_fd_to_json(fd)), ("is_local", "true")], +// ); +// } +// Err(err) => { +// self.handle_job_status(id, -1, Some(err.to_string())); +// } +// } +// } +// } +// Data::CancelJob(id) => { +// let mut msg_out = Message::new(); +// let mut file_action = FileAction::new(); +// file_action.set_cancel(FileTransferCancel { +// id: id, +// ..Default::default() +// }); +// msg_out.set_file_action(file_action); +// allow_err!(peer.send(&msg_out).await); +// if let Some(job) = fs::get_job(id, &mut self.write_jobs) { +// job.remove_download_file(); +// fs::remove_job(id, &mut self.write_jobs); +// } +// fs::remove_job(id, &mut self.read_jobs); +// } +// Data::RemoveDir((id, path)) => { +// let mut msg_out = Message::new(); +// let mut file_action = FileAction::new(); +// file_action.set_remove_dir(FileRemoveDir { +// id, +// path, +// recursive: true, +// ..Default::default() +// }); +// msg_out.set_file_action(file_action); +// allow_err!(peer.send(&msg_out).await); +// } +// Data::RemoveFile((id, path, file_num, is_remote)) => { +// if is_remote { +// let mut msg_out = Message::new(); +// let mut file_action = FileAction::new(); +// file_action.set_remove_file(FileRemoveFile { +// id, +// path, +// file_num, +// ..Default::default() +// }); +// msg_out.set_file_action(file_action); +// allow_err!(peer.send(&msg_out).await); +// } else { +// match fs::remove_file(&path) { +// Err(err) => { +// self.handle_job_status(id, file_num, Some(err.to_string())); +// } +// Ok(()) => { +// self.handle_job_status(id, file_num, None); +// } +// } +// } +// } +// Data::CreateDir((id, path, is_remote)) => { +// if is_remote { +// let mut msg_out = Message::new(); +// let mut file_action = FileAction::new(); +// file_action.set_create(FileDirCreate { +// id, +// path, +// ..Default::default() +// }); +// msg_out.set_file_action(file_action); +// allow_err!(peer.send(&msg_out).await); +// } else { +// match fs::create_dir(&path) { +// Err(err) => { +// self.handle_job_status(id, -1, Some(err.to_string())); +// } +// Ok(()) => { +// self.handle_job_status(id, -1, None); +// } +// } +// } +// } +// Data::SetConfirmOverrideFile((id, file_num, need_override, remember, is_upload)) => { +// if is_upload { +// if let Some(job) = fs::get_job(id, &mut self.read_jobs) { +// if remember { +// job.set_overwrite_strategy(Some(need_override)); +// } +// job.confirm(&FileTransferSendConfirmRequest { +// id, +// file_num, +// union: if need_override { +// Some(file_transfer_send_confirm_request::Union::OffsetBlk(0)) +// } else { +// Some(file_transfer_send_confirm_request::Union::Skip(true)) +// }, +// ..Default::default() +// }); +// } +// } else { +// if let Some(job) = fs::get_job(id, &mut self.write_jobs) { +// if remember { +// job.set_overwrite_strategy(Some(need_override)); +// } +// let mut msg = Message::new(); +// let mut file_action = FileAction::new(); +// file_action.set_send_confirm(FileTransferSendConfirmRequest { +// id, +// file_num, +// union: if need_override { +// Some(file_transfer_send_confirm_request::Union::OffsetBlk(0)) +// } else { +// Some(file_transfer_send_confirm_request::Union::Skip(true)) +// }, +// ..Default::default() +// }); +// msg.set_file_action(file_action); +// self.session.send_msg(msg); +// } +// } +// } +// Data::AddJob((id, path, to, file_num, include_hidden, is_remote)) => { +// let od = can_enable_overwrite_detection(self.session.lc.read().unwrap().version); +// if is_remote { +// log::debug!( +// "new write waiting job {}, write to {} from remote {}", +// id, +// to, +// path +// ); +// let mut job = fs::TransferJob::new_write( +// id, +// path.clone(), +// to, +// file_num, +// include_hidden, +// is_remote, +// Vec::new(), +// od, +// ); +// job.is_last_job = true; +// self.write_jobs.push(job); +// } else { +// match fs::TransferJob::new_read( +// id, +// to.clone(), +// path.clone(), +// file_num, +// include_hidden, +// is_remote, +// od, +// ) { +// Err(err) => { +// self.handle_job_status(id, -1, Some(err.to_string())); +// } +// Ok(mut job) => { +// log::debug!( +// "new read waiting job {}, read {} to remote {}, {} files", +// id, +// path, +// to, +// job.files().len() +// ); +// let m = make_fd_flutter(job.id(), job.files(), true); +// self.session +// .push_event("update_folder_files", vec![("info", &m)]); +// job.is_last_job = true; +// self.read_jobs.push(job); +// self.timer = time::interval(MILLI1); +// } +// } +// } +// } +// Data::ResumeJob((id, is_remote)) => { +// if is_remote { +// if let Some(job) = get_job(id, &mut self.write_jobs) { +// job.is_last_job = false; +// allow_err!( +// peer.send(&fs::new_send( +// id, +// job.remote.clone(), +// job.file_num, +// job.show_hidden +// )) +// .await +// ); +// } +// } else { +// if let Some(job) = get_job(id, &mut self.read_jobs) { +// job.is_last_job = false; +// allow_err!( +// peer.send(&fs::new_receive( +// id, +// job.path.to_string_lossy().to_string(), +// job.file_num, +// job.files.clone() +// )) +// .await +// ); +// } +// } +// } +// _ => {} +// } +// true +// } + +// #[inline] +// fn update_job_status( +// job: &fs::TransferJob, +// elapsed: i32, +// last_update_jobs_status: &mut (Instant, HashMap), +// session: &Session, +// ) { +// if elapsed <= 0 { +// return; +// } +// let transferred = job.transferred(); +// let last_transferred = { +// if let Some(v) = last_update_jobs_status.1.get(&job.id()) { +// v.to_owned() +// } else { +// 0 +// } +// }; +// last_update_jobs_status.1.insert(job.id(), transferred); +// let speed = (transferred - last_transferred) as f64 / (elapsed as f64 / 1000.); +// let file_num = job.file_num() - 1; +// session.push_event( +// "job_progress", +// vec![ +// ("id", &job.id().to_string()), +// ("file_num", &file_num.to_string()), +// ("speed", &speed.to_string()), +// ("finished_size", &job.finished_size().to_string()), +// ], +// ); +// } + +// fn update_jobs_status(&mut self) { +// let elapsed = self.last_update_jobs_status.0.elapsed().as_millis() as i32; +// if elapsed >= 1000 { +// for job in self.read_jobs.iter() { +// Self::update_job_status( +// job, +// elapsed, +// &mut self.last_update_jobs_status, +// &self.session, +// ); +// } +// for job in self.write_jobs.iter() { +// Self::update_job_status( +// job, +// elapsed, +// &mut self.last_update_jobs_status, +// &self.session, +// ); +// } +// self.last_update_jobs_status.0 = Instant::now(); +// } +// } + +// fn handle_job_status(&mut self, id: i32, file_num: i32, err: Option) { +// if let Some(err) = err { +// self.session +// .push_event("job_error", vec![("id", &id.to_string()), ("err", &err)]); +// } else { +// self.session.push_event( +// "job_done", +// vec![("id", &id.to_string()), ("file_num", &file_num.to_string())], +// ); +// } +// } + +// fn handle_override_file_confirm( +// &mut self, +// id: i32, +// file_num: i32, +// read_path: String, +// is_upload: bool, +// ) { +// self.session.push_event( +// "override_file_confirm", +// vec![ +// ("id", &id.to_string()), +// ("file_num", &file_num.to_string()), +// ("read_path", &read_path), +// ("is_upload", &is_upload.to_string()), +// ], +// ); +// } + +// async fn sync_jobs_status_to_local(&mut self) -> bool { +// log::info!("sync transfer job status"); +// let mut config: PeerConfig = self.session.load_config(); +// let mut transfer_metas = TransferSerde::default(); +// for job in self.read_jobs.iter() { +// let json_str = serde_json::to_string(&job.gen_meta()).unwrap(); +// transfer_metas.read_jobs.push(json_str); +// } +// for job in self.write_jobs.iter() { +// let json_str = serde_json::to_string(&job.gen_meta()).unwrap(); +// transfer_metas.write_jobs.push(json_str); +// } +// log::info!("meta: {:?}", transfer_metas); +// config.transfer = transfer_metas; +// self.session.save_config(&config); +// true +// } +// } // Server Side // TODO connection_manager need use struct and trait,impl default method @@ -2379,30 +2628,30 @@ pub fn get_session_id(id: String) -> String { }; } -async fn start_one_port_forward( - handler: Session, - port: i32, - remote_host: String, - remote_port: i32, - receiver: mpsc::UnboundedReceiver, - key: &str, - token: &str, -) { - if let Err(err) = crate::port_forward::listen( - handler.id.clone(), - String::new(), // TODO - port, - handler.clone(), - receiver, - key, - token, - handler.lc.clone(), - remote_host, - remote_port, - ) - .await - { - handler.on_error(&format!("Failed to listen on {}: {}", port, err)); - } - log::info!("port forward (:{}) exit", port); -} +// async fn start_one_port_forward( +// handler: Session, +// port: i32, +// remote_host: String, +// remote_port: i32, +// receiver: mpsc::UnboundedReceiver, +// key: &str, +// token: &str, +// ) { +// if let Err(err) = crate::port_forward::listen( +// handler.id.clone(), +// String::new(), // TODO +// port, +// handler.clone(), +// receiver, +// key, +// token, +// handler.lc.clone(), +// remote_host, +// remote_port, +// ) +// .await +// { +// handler.on_error(&format!("Failed to listen on {}: {}", port, err)); +// } +// log::info!("port forward (:{}) exit", port); +// } diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index db8030782..5226416b2 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -13,10 +13,10 @@ use hbb_common::{ }; use hbb_common::{password_security, ResultType}; -use crate::client::file_trait::FileManager; +use crate::{client::file_trait::FileManager, flutter::{session_add, session_start_}}; use crate::common::make_fd_to_json; use crate::flutter::connection_manager::{self, get_clients_length, get_clients_state}; -use crate::flutter::{self, Session, SESSIONS}; +use crate::flutter::{self, SESSIONS}; use crate::start_server; use crate::ui_interface; #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -110,7 +110,7 @@ pub fn host_stop_system_key_propagate(stopped: bool) { // FIXME: -> ResultType<()> cannot be parsed by frb_codegen // thread 'main' panicked at 'Failed to parse function output type `ResultType<()>`', $HOME\.cargo\git\checkouts\flutter_rust_bridge-ddba876d3ebb2a1e\e5adce5\frb_codegen\src\parser\mod.rs:151:25 pub fn session_add_sync(id: String, is_file_transfer: bool, is_port_forward: bool) -> SyncReturn { - if let Err(e) = Session::add(&id, is_file_transfer, is_port_forward) { + if let Err(e) = session_add(&id, is_file_transfer, is_port_forward) { SyncReturn(format!("Failed to add session with id {}, {}", &id, e)) } else { SyncReturn("".to_owned()) @@ -118,7 +118,7 @@ pub fn session_add_sync(id: String, is_file_transfer: bool, is_port_forward: boo } pub fn session_start(events2ui: StreamSink, id: String) -> ResultType<()> { - Session::start(&id, events2ui) + session_start_(&id, events2ui) } pub fn session_get_remember(id: String) -> Option { @@ -131,7 +131,7 @@ pub fn session_get_remember(id: String) -> Option { 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)) + Some(session.get_toggle_option(arg)) } else { None } @@ -152,7 +152,7 @@ pub fn session_get_image_quality(id: String) -> Option { pub fn session_get_option(id: String, arg: String) -> Option { if let Some(session) = SESSIONS.read().unwrap().get(&id) { - Some(session.get_option(&arg)) + Some(session.get_option(arg)) } else { None } @@ -160,7 +160,7 @@ pub fn session_get_option(id: String, arg: String) -> Option { pub fn session_login(id: String, password: String, remember: bool) { if let Some(session) = SESSIONS.read().unwrap().get(&id) { - session.login(&password, remember); + session.login(password, remember); } } @@ -173,7 +173,7 @@ pub fn session_close(id: String) { pub fn session_refresh(id: String) { if let Some(session) = SESSIONS.read().unwrap().get(&id) { - session.refresh(); + session.refresh_video(); } } @@ -184,26 +184,26 @@ pub fn session_reconnect(id: String) { } pub fn session_toggle_option(id: String, value: String) { - if let Some(session) = SESSIONS.read().unwrap().get(&id) { - session.toggle_option(&value); + if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) { + session.toggle_option(value); } } pub fn session_set_image_quality(id: String, value: String) { if let Some(session) = SESSIONS.read().unwrap().get(&id) { - session.set_image_quality(&value); + // session.set_image_quality(value); } } pub fn session_lock_screen(id: String) { if let Some(session) = SESSIONS.read().unwrap().get(&id) { - session.lock_screen(); + // session.lock_screen(); } } pub fn session_ctrl_alt_del(id: String) { if let Some(session) = SESSIONS.read().unwrap().get(&id) { - session.ctrl_alt_del(); + // session.ctrl_alt_del(); } } @@ -224,13 +224,13 @@ pub fn session_input_key( command: bool, ) { if let Some(session) = SESSIONS.read().unwrap().get(&id) { - session.input_key(&name, down, press, alt, ctrl, shift, command); + // session.input_key(&name, down, press, alt, ctrl, shift, command); } } pub fn session_input_string(id: String, value: String) { if let Some(session) = SESSIONS.read().unwrap().get(&id) { - session.input_string(&value); + // session.input_string(&value); } } @@ -249,7 +249,7 @@ pub fn session_peer_option(id: String, name: String, value: String) { pub fn session_get_peer_option(id: String, name: String) -> String { if let Some(session) = SESSIONS.read().unwrap().get(&id) { - return session.get_option(&name); + return session.get_option(name); } "".to_string() } @@ -348,7 +348,7 @@ pub fn session_get_platform(id: String, is_remote: bool) -> String { pub fn session_load_last_transfer_jobs(id: String) { if let Some(session) = SESSIONS.read().unwrap().get(&id) { - return session.load_last_jobs(); + // return session.load_last_jobs(); } else { // a tip for flutter dev eprintln!( @@ -719,7 +719,7 @@ pub fn session_send_mouse(id: String, msg: String) { } << 3; } if let Some(session) = SESSIONS.read().unwrap().get(&id) { - session.send_mouse(mask, x, y, alt, ctrl, shift, command); + // session.send_mouse(mask, x, y, alt, ctrl, shift, command); } } } diff --git a/src/ui/remote.rs b/src/ui/remote.rs index f5abb3d73..49812e09a 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -48,7 +48,7 @@ use crate::clipboard_file::*; use crate::{ client::*, common::{self, check_clipboard, update_clipboard, ClipboardContext, CLIPBOARD_INTERVAL}, - ui_session_interface::{InvokeUi, Session}, + ui_session_interface::{io_loop, InvokeUi, Remote, Session, SERVER_KEYBOARD_ENABLED}, }; use errno; @@ -69,9 +69,7 @@ fn get_key_state(key: enigo::Key) -> bool { static IS_IN: AtomicBool = AtomicBool::new(false); static KEYBOARD_HOOKED: AtomicBool = AtomicBool::new(false); -static SERVER_KEYBOARD_ENABLED: AtomicBool = AtomicBool::new(true); -static SERVER_FILE_TRANSFER_ENABLED: AtomicBool = AtomicBool::new(true); -static SERVER_CLIPBOARD_ENABLED: AtomicBool = AtomicBool::new(true); + #[cfg(windows)] static mut IS_ALT_GR: bool = false; @@ -82,7 +80,6 @@ static mut IS_ALT_GR: bool = false; #[derive(Clone, Default)] pub struct SciterHandler { element: Arc>>, - thread: Arc>>>, close_state: HashMap, } @@ -128,6 +125,11 @@ impl InvokeUi for SciterHandler { fn set_display(&self, x: i32, y: i32, w: i32, h: i32) { self.call("setDisplay", &make_args!(x, y, w, h)); + VIDEO.lock().unwrap().as_mut().map(|v| { + v.stop_streaming().ok(); + let ok = v.start_streaming((w, h), COLOR_SPACE::Rgb32, None); + log::info!("[video] reinitialized: {:?}", ok); + }); } fn update_privacy_mode(&self) { @@ -214,6 +216,32 @@ impl InvokeUi for SciterHandler { fn adapt_size(&self) { self.call("adaptSize", &make_args!()); } + + fn on_rgba(&self, data: &[u8]) { + VIDEO + .lock() + .unwrap() + .as_mut() + .map(|v| v.render_frame(data).ok()); + } + + fn set_peer_info( + &self, + username: &str, + hostname: &str, + platform: &str, + sas_enabled: bool, + displays: &Vec>, + version: &str, + current_display: usize, + is_file_transfer: bool, + ) { + todo!() + } + + fn msgbox(&self, msgtype: &str, title: &str, text: &str, retry: bool) { + todo!() + } } pub struct SciterSession(Session); @@ -231,32 +259,6 @@ impl DerefMut for SciterSession { } } -// #[derive(Default)] -// pub struct HandlerInner { -// element: Option, -// sender: Option>, -// thread: Option>, -// close_state: HashMap, -// } - -// #[derive(Clone, Default)] -// pub struct Handler { -// inner: Arc>, -// cmd: String, -// id: String, -// password: String, -// args: Vec, -// lc: Arc>, -// } - -// impl Deref for Handler { -// type Target = Arc>; - -// fn deref(&self) -> &Self::Target { -// &self.inner -// } -// } - impl sciter::EventHandler for SciterSession { fn get_subscription(&mut self) -> Option { Some(EVENT_GROUPS::HANDLE_BEHAVIOR_EVENT) @@ -659,14 +661,15 @@ impl SciterSession { }); } - fn get_view_style(&mut self) -> String { - return self.lc.read().unwrap().view_style.clone(); - } + // fn get_view_style(&mut self) -> String { + // return self.lc.read().unwrap().view_style.clone(); + // } - fn get_image_quality(&mut self) -> String { - return self.lc.read().unwrap().image_quality.clone(); - } + // fn get_image_quality(&mut self) -> String { + // return self.lc.read().unwrap().image_quality.clone(); + // } + // TODO fn get_custom_image_quality(&mut self) -> Value { let mut v = Value::array(0); for x in self.lc.read().unwrap().custom_image_quality.iter() { @@ -680,82 +683,83 @@ impl SciterSession { // self.lc.write().unwrap().save_config(config); // } - fn save_view_style(&mut self, value: String) { - self.lc.write().unwrap().save_view_style(value); - } + // fn save_view_style(&mut self, value: String) { + // self.lc.write().unwrap().save_view_style(value); + // } // #[inline] // pub(super) fn load_config(&self) -> PeerConfig { // load_config(&self.id) // } - fn toggle_option(&mut self, name: String) { - let msg = self.lc.write().unwrap().toggle_option(name.clone()); - if name == "enable-file-transfer" { - self.send(Data::ToggleClipboardFile); - } - if let Some(msg) = msg { - self.send(Data::Message(msg)); - } - } + // fn toggle_option(&mut self, name: String) { + // let msg = self.lc.write().unwrap().toggle_option(name.clone()); + // if name == "enable-file-transfer" { + // self.send(Data::ToggleClipboardFile); + // } + // if let Some(msg) = msg { + // self.send(Data::Message(msg)); + // } + // } - fn get_toggle_option(&mut self, name: String) -> bool { - self.lc.read().unwrap().get_toggle_option(&name) - } + // fn get_toggle_option(&mut self, name: String) -> bool { + // self.lc.read().unwrap().get_toggle_option(&name) + // } - fn is_privacy_mode_supported(&self) -> bool { - self.lc.read().unwrap().is_privacy_mode_supported() - } + // fn is_privacy_mode_supported(&self) -> bool { + // self.lc.read().unwrap().is_privacy_mode_supported() + // } - fn refresh_video(&mut self) { - self.send(Data::Message(LoginConfigHandler::refresh())); - } + // fn refresh_video(&mut self) { + // self.send(Data::Message(LoginConfigHandler::refresh())); + // } - fn save_custom_image_quality(&mut self, custom_image_quality: i32) { - let msg = self - .lc - .write() - .unwrap() - .save_custom_image_quality(custom_image_quality); - self.send(Data::Message(msg)); - } + // fn save_custom_image_quality(&mut self, custom_image_quality: i32) { + // let msg = self + // .lc + // .write() + // .unwrap() + // .save_custom_image_quality(custom_image_quality); + // self.send(Data::Message(msg)); + // } - fn save_image_quality(&mut self, value: String) { - let msg = self.lc.write().unwrap().save_image_quality(value); - if let Some(msg) = msg { - self.send(Data::Message(msg)); - } - } + // fn save_image_quality(&mut self, value: String) { + // let msg = self.lc.write().unwrap().save_image_quality(value); + // if let Some(msg) = msg { + // self.send(Data::Message(msg)); + // } + // } - fn get_remember(&mut self) -> bool { - self.lc.read().unwrap().remember - } + // fn get_remember(&mut self) -> bool { + // self.lc.read().unwrap().remember + // } - fn set_write_override( - &mut self, - job_id: i32, - file_num: i32, - is_override: bool, - remember: bool, - is_upload: bool, - ) -> bool { - self.send(Data::SetConfirmOverrideFile(( - job_id, - file_num, - is_override, - remember, - is_upload, - ))); - true - } + // fn set_write_override( + // &mut self, + // job_id: i32, + // file_num: i32, + // is_override: bool, + // remember: bool, + // is_upload: bool, + // ) -> bool { + // self.send(Data::SetConfirmOverrideFile(( + // job_id, + // file_num, + // is_override, + // remember, + // is_upload, + // ))); + // true + // } - fn has_hwcodec(&self) -> bool { - #[cfg(not(feature = "hwcodec"))] - return false; - #[cfg(feature = "hwcodec")] - return true; - } + // fn has_hwcodec(&self) -> bool { + // #[cfg(not(feature = "hwcodec"))] + // return false; + // #[cfg(feature = "hwcodec")] + // return true; + // } + // TODO fn supported_hwcodec(&self) -> Value { #[cfg(feature = "hwcodec")] { @@ -780,51 +784,52 @@ impl SciterSession { } } - fn change_prefer_codec(&self) { - let msg = self.lc.write().unwrap().change_prefer_codec(); - self.send(Data::Message(msg)); - } + // fn change_prefer_codec(&self) { + // let msg = self.lc.write().unwrap().change_prefer_codec(); + // self.send(Data::Message(msg)); + // } - fn restart_remote_device(&mut self) { - let mut lc = self.lc.write().unwrap(); - lc.restarting_remote_device = true; - let msg = lc.restart_remote_device(); - self.send(Data::Message(msg)); - } + // fn restart_remote_device(&mut self) { + // let mut lc = self.lc.write().unwrap(); + // lc.restarting_remote_device = true; + // let msg = lc.restart_remote_device(); + // self.send(Data::Message(msg)); + // } // pub fn is_restarting_remote_device(&self) -> bool { // self.lc.read().unwrap().restarting_remote_device // } - fn t(&self, name: String) -> String { - crate::client::translate(name) - } + // fn t(&self, name: String) -> String { + // crate::client::translate(name) + // } - fn get_audit_server(&self) -> String { - if self.lc.read().unwrap().conn_id <= 0 - || LocalConfig::get_option("access_token").is_empty() - { - return "".to_owned(); - } - crate::get_audit_server( - Config::get_option("api-server"), - Config::get_option("custom-rendezvous-server"), - ) - } + // fn get_audit_server(&self) -> String { + // if self.lc.read().unwrap().conn_id <= 0 + // || LocalConfig::get_option("access_token").is_empty() + // { + // return "".to_owned(); + // } + // crate::get_audit_server( + // Config::get_option("api-server"), + // Config::get_option("custom-rendezvous-server"), + // ) + // } - fn send_note(&self, note: String) { - let url = self.get_audit_server(); - let id = self.id.clone(); - let conn_id = self.lc.read().unwrap().conn_id; - std::thread::spawn(move || { - send_note(url, id, conn_id, note); - }); - } + // fn send_note(&self, note: String) { + // let url = self.get_audit_server(); + // let id = self.id.clone(); + // let conn_id = self.lc.read().unwrap().conn_id; + // std::thread::spawn(move || { + // send_note(url, id, conn_id, note); + // }); + // } - fn is_xfce(&self) -> bool { - crate::platform::is_xfce() - } + // fn is_xfce(&self) -> bool { + // crate::platform::is_xfce() + // } + // TODO fn save_size(&mut self, x: i32, y: i32, w: i32, h: i32) { let size = (x, y, w, h); let mut config = self.load_config(); @@ -885,33 +890,33 @@ impl SciterSession { v } - fn remove_port_forward(&mut self, port: i32) { - let mut config = self.load_config(); - config.port_forwards = config - .port_forwards - .drain(..) - .filter(|x| x.0 != port) - .collect(); - self.save_config(config); - self.send(Data::RemovePortForward(port)); - } + // fn remove_port_forward(&mut self, port: i32) { + // let mut config = self.load_config(); + // config.port_forwards = config + // .port_forwards + // .drain(..) + // .filter(|x| x.0 != port) + // .collect(); + // self.save_config(config); + // self.send(Data::RemovePortForward(port)); + // } - fn add_port_forward(&mut self, port: i32, remote_host: String, remote_port: i32) { - let mut config = self.load_config(); - if config - .port_forwards - .iter() - .filter(|x| x.0 == port) - .next() - .is_some() - { - return; - } - let pf = (port, remote_host, remote_port); - config.port_forwards.push(pf.clone()); - self.save_config(config); - self.send(Data::AddPortForward(pf)); - } + // fn add_port_forward(&mut self, port: i32, remote_host: String, remote_port: i32) { + // let mut config = self.load_config(); + // if config + // .port_forwards + // .iter() + // .filter(|x| x.0 == port) + // .next() + // .is_some() + // { + // return; + // } + // let pf = (port, remote_host, remote_port); + // config.port_forwards.push(pf.clone()); + // self.save_config(config); + // self.send(Data::AddPortForward(pf)); + // } fn get_size(&mut self) -> Value { let s = if self.is_file_transfer() { @@ -929,9 +934,9 @@ impl SciterSession { v } - fn get_id(&mut self) -> String { - self.id.clone() - } + // fn get_id(&mut self) -> String { + // self.id.clone() + // } fn get_default_pi(&mut self) -> Value { let mut pi = Value::map(); @@ -950,46 +955,47 @@ impl SciterSession { // self.lc.write().unwrap().set_option(k, v); // } - fn input_os_password(&mut self, pass: String, activate: bool) { - input_os_password(pass, activate, self.clone()); - } + // fn input_os_password(&mut self, pass: String, activate: bool) { + // input_os_password(pass, activate, self.clone()); + // } + // close_state sciter only fn save_close_state(&mut self, k: String, v: String) { self.close_state.insert(k, v); } - fn get_chatbox(&mut self) -> String { - #[cfg(feature = "inline")] - return super::inline::get_chatbox(); - #[cfg(not(feature = "inline"))] - return "".to_owned(); - } + // fn get_chatbox(&mut self) -> String { + // #[cfg(feature = "inline")] + // return super::inline::get_chatbox(); + // #[cfg(not(feature = "inline"))] + // return "".to_owned(); + // } - fn get_icon(&mut self) -> String { - crate::get_icon() - } + // fn get_icon(&mut self) -> String { + // crate::get_icon() + // } - fn send_chat(&mut self, text: String) { - let mut misc = Misc::new(); - misc.set_chat_message(ChatMessage { - text, - ..Default::default() - }); - let mut msg_out = Message::new(); - msg_out.set_misc(misc); - self.send(Data::Message(msg_out)); - } + // fn send_chat(&mut self, text: String) { + // let mut misc = Misc::new(); + // misc.set_chat_message(ChatMessage { + // text, + // ..Default::default() + // }); + // let mut msg_out = Message::new(); + // msg_out.set_misc(misc); + // self.send(Data::Message(msg_out)); + // } - fn switch_display(&mut self, display: i32) { - let mut misc = Misc::new(); - misc.set_switch_display(SwitchDisplay { - display, - ..Default::default() - }); - let mut msg_out = Message::new(); - msg_out.set_misc(misc); - self.send(Data::Message(msg_out)); - } + // fn switch_display(&mut self, display: i32) { + // let mut misc = Misc::new(); + // misc.set_switch_display(SwitchDisplay { + // display, + // ..Default::default() + // }); + // let mut msg_out = Message::new(); + // msg_out.set_misc(misc); + // self.send(Data::Message(msg_out)); + // } // fn is_file_transfer(&self) -> bool { // self.cmd == "--file-transfer" @@ -1003,15 +1009,15 @@ impl SciterSession { // self.cmd == "--rdp" // } - fn reconnect(&mut self) { - println!("reconnecting"); - let cloned = self.clone(); - let mut lock = self.thread.lock().unwrap(); - lock.take().map(|t| t.join()); - *lock = Some(std::thread::spawn(move || { - io_loop(cloned); - })); - } + // fn reconnect(&mut self) { + // println!("reconnecting"); + // let cloned = self.clone(); + // let mut lock = self.thread.lock().unwrap(); + // lock.take().map(|t| t.join()); + // *lock = Some(std::thread::spawn(move || { + // io_loop(cloned); + // })); + // } // #[inline] // fn peer_platform(&self) -> String { @@ -1035,63 +1041,63 @@ impl SciterSession { // } // } - fn get_icon_path(&mut self, file_type: i32, ext: String) -> String { - let mut path = Config::icon_path(); - if file_type == FileType::DirLink as i32 { - let new_path = path.join("dir_link"); - if !std::fs::metadata(&new_path).is_ok() { - #[cfg(windows)] - allow_err!(std::os::windows::fs::symlink_file(&path, &new_path)); - #[cfg(not(windows))] - allow_err!(std::os::unix::fs::symlink(&path, &new_path)); - } - path = new_path; - } else if file_type == FileType::File as i32 { - if !ext.is_empty() { - path = path.join(format!("file.{}", ext)); - } else { - path = path.join("file"); - } - if !std::fs::metadata(&path).is_ok() { - allow_err!(std::fs::File::create(&path)); - } - } else if file_type == FileType::FileLink as i32 { - let new_path = path.join("file_link"); - if !std::fs::metadata(&new_path).is_ok() { - path = path.join("file"); - if !std::fs::metadata(&path).is_ok() { - allow_err!(std::fs::File::create(&path)); - } - #[cfg(windows)] - allow_err!(std::os::windows::fs::symlink_file(&path, &new_path)); - #[cfg(not(windows))] - allow_err!(std::os::unix::fs::symlink(&path, &new_path)); - } - path = new_path; - } else if file_type == FileType::DirDrive as i32 { - if cfg!(windows) { - path = fs::get_path("C:"); - } else if cfg!(target_os = "macos") { - if let Ok(entries) = fs::get_path("/Volumes/").read_dir() { - for entry in entries { - if let Ok(entry) = entry { - path = entry.path(); - break; - } - } - } - } - } - fs::get_string(&path) - } + // fn get_icon_path(&mut self, file_type: i32, ext: String) -> String { + // let mut path = Config::icon_path(); + // if file_type == FileType::DirLink as i32 { + // let new_path = path.join("dir_link"); + // if !std::fs::metadata(&new_path).is_ok() { + // #[cfg(windows)] + // allow_err!(std::os::windows::fs::symlink_file(&path, &new_path)); + // #[cfg(not(windows))] + // allow_err!(std::os::unix::fs::symlink(&path, &new_path)); + // } + // path = new_path; + // } else if file_type == FileType::File as i32 { + // if !ext.is_empty() { + // path = path.join(format!("file.{}", ext)); + // } else { + // path = path.join("file"); + // } + // if !std::fs::metadata(&path).is_ok() { + // allow_err!(std::fs::File::create(&path)); + // } + // } else if file_type == FileType::FileLink as i32 { + // let new_path = path.join("file_link"); + // if !std::fs::metadata(&new_path).is_ok() { + // path = path.join("file"); + // if !std::fs::metadata(&path).is_ok() { + // allow_err!(std::fs::File::create(&path)); + // } + // #[cfg(windows)] + // allow_err!(std::os::windows::fs::symlink_file(&path, &new_path)); + // #[cfg(not(windows))] + // allow_err!(std::os::unix::fs::symlink(&path, &new_path)); + // } + // path = new_path; + // } else if file_type == FileType::DirDrive as i32 { + // if cfg!(windows) { + // path = fs::get_path("C:"); + // } else if cfg!(target_os = "macos") { + // if let Ok(entries) = fs::get_path("/Volumes/").read_dir() { + // for entry in entries { + // if let Ok(entry) = entry { + // path = entry.path(); + // break; + // } + // } + // } + // } + // } + // fs::get_string(&path) + // } - fn login(&mut self, password: String, remember: bool) { - self.send(Data::Login((password, remember))); - } + // fn login(&mut self, password: String, remember: bool) { + // self.send(Data::Login((password, remember))); + // } - fn new_rdp(&mut self) { - self.send(Data::NewRDP); - } + // fn new_rdp(&mut self) { + // self.send(Data::NewRDP); + // } fn enter(&mut self) { #[cfg(windows)] @@ -1383,1373 +1389,6 @@ impl SciterSession { // } } -const MILLI1: Duration = Duration::from_millis(1); - -async fn start_one_port_forward( - handler: Session, - port: i32, - remote_host: String, - remote_port: i32, - receiver: mpsc::UnboundedReceiver, - key: &str, - token: &str, -) { - if let Err(err) = crate::port_forward::listen( - handler.id.clone(), - handler.password.clone(), - port, - handler.clone(), - receiver, - key, - token, - handler.lc.clone(), - remote_host, - remote_port, - ) - .await - { - handler.on_error(&format!("Failed to listen on {}: {}", port, err)); - } - log::info!("port forward (:{}) exit", port); -} - -#[tokio::main(flavor = "current_thread")] -async fn io_loop(handler: Session) { - let (sender, mut receiver) = mpsc::unbounded_channel::(); - *handler.sender.write().unwrap() = Some(sender.clone()); - let mut options = crate::ipc::get_options_async().await; - let mut key = options.remove("key").unwrap_or("".to_owned()); - let token = LocalConfig::get_option("access_token"); - if key.is_empty() { - key = crate::platform::get_license_key(); - } - if handler.is_port_forward() { - if handler.is_rdp() { - let port = handler - .get_option("rdp_port".to_owned()) - .parse::() - .unwrap_or(3389); - std::env::set_var( - "rdp_username", - handler.get_option("rdp_username".to_owned()), - ); - std::env::set_var( - "rdp_password", - handler.get_option("rdp_password".to_owned()), - ); - log::info!("Remote rdp port: {}", port); - start_one_port_forward(handler, 0, "".to_owned(), port, receiver, &key, &token).await; - } else if handler.args.len() == 0 { - let pfs = handler.lc.read().unwrap().port_forwards.clone(); - let mut queues = HashMap::>::new(); - for d in pfs { - sender.send(Data::AddPortForward(d)).ok(); - } - loop { - match receiver.recv().await { - Some(Data::AddPortForward((port, remote_host, remote_port))) => { - if port <= 0 || remote_port <= 0 { - continue; - } - let (sender, receiver) = mpsc::unbounded_channel::(); - queues.insert(port, sender); - let handler = handler.clone(); - let key = key.clone(); - let token = token.clone(); - tokio::spawn(async move { - start_one_port_forward( - handler, - port, - remote_host, - remote_port, - receiver, - &key, - &token, - ) - .await; - }); - } - Some(Data::RemovePortForward(port)) => { - if let Some(s) = queues.remove(&port) { - s.send(Data::Close).ok(); - } - } - Some(Data::Close) => { - break; - } - Some(d) => { - for (_, s) in queues.iter() { - s.send(d.clone()).ok(); - } - } - _ => {} - } - } - } else { - let port = handler.args[0].parse::().unwrap_or(0); - if handler.args.len() != 3 - || handler.args[2].parse::().unwrap_or(0) <= 0 - || port <= 0 - { - handler.on_error("Invalid arguments, usage:

    rustdesk --port-forward remote-id listen-port remote-host remote-port"); - } - let remote_host = handler.args[1].clone(); - let remote_port = handler.args[2].parse::().unwrap_or(0); - start_one_port_forward( - handler, - port, - remote_host, - remote_port, - receiver, - &key, - &token, - ) - .await; - } - return; - } - let frame_count = Arc::new(AtomicUsize::new(0)); - let frame_count_cl = frame_count.clone(); - let (video_sender, audio_sender) = start_video_audio_threads(move |data: &[u8]| { - frame_count_cl.fetch_add(1, Ordering::Relaxed); - VIDEO - .lock() - .unwrap() - .as_mut() - .map(|v| v.render_frame(data).ok()); - }); - - let mut remote = Remote { - handler, - video_sender, - audio_sender, - receiver, - sender, - old_clipboard: Default::default(), - read_jobs: Vec::new(), - write_jobs: Vec::new(), - remove_jobs: Default::default(), - timer: time::interval(SEC30), - last_update_jobs_status: (Instant::now(), Default::default()), - first_frame: false, - #[cfg(windows)] - clipboard_file_context: None, - data_count: Arc::new(AtomicUsize::new(0)), - frame_count, - video_format: CodecFormat::Unknown, - }; - remote.io_loop(&key, &token).await; - remote.sync_jobs_status_to_local().await; -} - -struct RemoveJob { - files: Vec, - path: String, - sep: &'static str, - is_remote: bool, - no_confirm: bool, - last_update_job_status: Instant, -} - -impl RemoveJob { - fn new(files: Vec, path: String, sep: &'static str, is_remote: bool) -> Self { - Self { - files, - path, - sep, - is_remote, - no_confirm: false, - last_update_job_status: Instant::now(), - } - } - - pub fn _gen_meta(&self) -> RemoveJobMeta { - RemoveJobMeta { - path: self.path.clone(), - is_remote: self.is_remote, - no_confirm: self.no_confirm, - } - } -} - -struct Remote { - handler: Session, - video_sender: MediaSender, - audio_sender: MediaSender, - receiver: mpsc::UnboundedReceiver, - sender: mpsc::UnboundedSender, - old_clipboard: Arc>, - read_jobs: Vec, - write_jobs: Vec, - remove_jobs: HashMap, - timer: Interval, - last_update_jobs_status: (Instant, HashMap), - first_frame: bool, - #[cfg(windows)] - clipboard_file_context: Option>, - data_count: Arc, - frame_count: Arc, - video_format: CodecFormat, -} - -impl Remote { - 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() { - ConnType::FILE_TRANSFER - } else { - ConnType::default() - }; - match Client::start( - &self.handler.id, - key, - token, - conn_type, - self.handler.clone(), - ) - .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 - // .call("setConnectionType", &make_args!(peer.is_secured(), direct)); - self.handler.set_connection_type(peer.is_secured(), direct); - - // just build for now - #[cfg(not(windows))] - let (_tx_holder, mut rx_clip_client) = mpsc::unbounded_channel::(); - #[cfg(windows)] - let mut rx_clip_client = get_rx_clip_client().lock().await; - - let mut status_timer = time::interval(Duration::new(1, 0)); - - loop { - tokio::select! { - res = peer.next() => { - if let Some(res) = res { - match res { - Err(err) => { - log::error!("Connection closed: {}", err); - self.handler.set_force_relay(direct, received); - self.handler.msgbox("error", "Connection Error", &err.to_string()); - break; - } - Ok(ref bytes) => { - last_recv_time = Instant::now(); - received = true; - self.data_count.fetch_add(bytes.len(), Ordering::Relaxed); - if !self.handle_msg_from_peer(bytes, &mut peer).await { - break - } - } - } - } else { - if self.handler.is_restarting_remote_device() { - log::info!("Restart remote device"); - self.handler.msgbox("restarting", "Restarting Remote Device", "remote_restarting_tip"); - } else { - log::info!("Reset by the peer"); - self.handler.msgbox("error", "Connection Error", "Reset by the peer"); - } - break; - } - } - d = self.receiver.recv() => { - if let Some(d) = d { - if !self.handle_msg_from_ui(d, &mut peer).await { - break; - } - } - } - _msg = rx_clip_client.recv() => { - #[cfg(windows)] - match _msg { - Some((_, clip)) => { - allow_err!(peer.send(&clip_2_msg(clip)).await); - } - None => { - // unreachable!() - } - } - } - _ = self.timer.tick() => { - if last_recv_time.elapsed() >= SEC30 { - self.handler.msgbox("error", "Connection Error", "Timeout"); - break; - } - if !self.read_jobs.is_empty() { - if let Err(err) = fs::handle_read_jobs(&mut self.read_jobs, &mut peer).await { - self.handler.msgbox("error", "Connection Error", &err.to_string()); - break; - } - self.update_jobs_status(); - } else { - self.timer = time::interval_at(Instant::now() + SEC30, SEC30); - } - } - _ = status_timer.tick() => { - let speed = self.data_count.swap(0, Ordering::Relaxed); - let speed = format!("{:.2}kB/s", speed as f32 / 1024 as f32); - let fps = self.frame_count.swap(0, Ordering::Relaxed) as _; - self.handler.update_quality_status(QualityStatus { - speed:Some(speed), - fps:Some(fps), - ..Default::default() - }); - } - } - } - log::debug!("Exit io_loop of id={}", self.handler.id); - } - Err(err) => { - self.handler - .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); - } - - fn handle_job_status(&mut self, id: i32, file_num: i32, err: Option) { - if let Some(job) = self.remove_jobs.get_mut(&id) { - if job.no_confirm { - let file_num = (file_num + 1) as usize; - if file_num < job.files.len() { - let path = format!("{}{}{}", job.path, job.sep, job.files[file_num].name); - self.sender - .send(Data::RemoveFile((id, path, file_num as i32, job.is_remote))) - .ok(); - let elapsed = job.last_update_job_status.elapsed().as_millis() as i32; - if elapsed >= 1000 { - job.last_update_job_status = Instant::now(); - } else { - return; - } - } else { - self.remove_jobs.remove(&id); - } - } - } - if let Some(err) = err { - // self.handler - // .call("jobError", &make_args!(id, err, file_num)); - self.handler.job_error(id, err, file_num); - } else { - // self.handler.call("jobDone", &make_args!(id, file_num)); - self.handler.job_done(id, file_num); - } - } - - 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(); - 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 - { - 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 load_last_jobs(&mut self) { - log::info!("start load last jobs"); - // self.handler.call("clearAllJobs", &make_args!()); - self.handler.clear_all_jobs(); - let pc = self.handler.load_config(); - if pc.transfer.write_jobs.is_empty() && pc.transfer.read_jobs.is_empty() { - // no last jobs - return; - } - // TODO: can add a confirm dialog - let mut cnt = 1; - for job_str in pc.transfer.read_jobs.iter() { - let job: Result = serde_json::from_str(&job_str); - if let Ok(job) = job { - // self.handler.call( - // "addJob", - // &make_args!( - // cnt, - // job.to.clone(), - // job.remote.clone(), - // job.file_num, - // job.show_hidden, - // false - // ), - // ); - self.handler.add_job( - cnt, - job.to.clone(), - job.remote.clone(), - job.file_num, - job.show_hidden, - false, - ); - cnt += 1; - println!("restore read_job: {:?}", job); - } - } - for job_str in pc.transfer.write_jobs.iter() { - let job: Result = serde_json::from_str(&job_str); - if let Ok(job) = job { - // self.handler.call( - // "addJob", - // &make_args!( - // cnt, - // job.remote.clone(), - // job.to.clone(), - // job.file_num, - // job.show_hidden, - // true - // ), - // ); - self.handler.add_job( - cnt, - job.remote.clone(), - job.to.clone(), - job.file_num, - job.show_hidden, - true, - ); - cnt += 1; - println!("restore write_job: {:?}", job); - } - } - // self.handler.call("updateTransferList", &make_args!()); - self.handler.update_transfer_list(); - } - - async fn handle_msg_from_ui(&mut self, data: Data, peer: &mut Stream) -> bool { - match data { - Data::Close => { - let mut misc = Misc::new(); - misc.set_close_reason("".to_owned()); - let mut msg = Message::new(); - msg.set_misc(misc); - allow_err!(peer.send(&msg).await); - return false; - } - Data::Login((password, remember)) => { - self.handler - .handle_login_from_ui(password, remember, peer) - .await; - } - Data::ToggleClipboardFile => { - self.check_clipboard_file_context(); - } - Data::Message(msg) => { - allow_err!(peer.send(&msg).await); - } - Data::SendFiles((id, path, to, file_num, include_hidden, is_remote)) => { - log::info!("send files, is remote {}", is_remote); - let od = can_enable_overwrite_detection(self.handler.lc.read().unwrap().version); - if is_remote { - log::debug!("New job {}, write to {} from remote {}", id, to, path); - self.write_jobs.push(fs::TransferJob::new_write( - id, - path.clone(), - to, - file_num, - include_hidden, - is_remote, - Vec::new(), - od, - )); - allow_err!( - peer.send(&fs::new_send(id, path, file_num, include_hidden)) - .await - ); - } else { - match fs::TransferJob::new_read( - id, - to.clone(), - path.clone(), - file_num, - include_hidden, - is_remote, - od, - ) { - Err(err) => { - self.handle_job_status(id, -1, Some(err.to_string())); - } - Ok(job) => { - log::debug!( - "New job {}, read {} to remote {}, {} files", - id, - path, - to, - job.files().len() - ); - // let m = make_fd(job.id(), job.files(), true); - // self.handler.call("updateFolderFiles", &make_args!(m)); // TODO - #[cfg(not(windows))] - let files = job.files().clone(); - #[cfg(windows)] - let mut files = job.files().clone(); - #[cfg(windows)] - if self.handler.peer_platform() != "Windows" { - // peer is not windows, need transform \ to / - fs::transform_windows_path(&mut files); - } - self.read_jobs.push(job); - self.timer = time::interval(MILLI1); - allow_err!(peer.send(&fs::new_receive(id, to, file_num, files)).await); - } - } - } - } - Data::AddJob((id, path, to, file_num, include_hidden, is_remote)) => { - let od = can_enable_overwrite_detection(self.handler.lc.read().unwrap().version); - if is_remote { - log::debug!( - "new write waiting job {}, write to {} from remote {}", - id, - to, - path - ); - let mut job = fs::TransferJob::new_write( - id, - path.clone(), - to, - file_num, - include_hidden, - is_remote, - Vec::new(), - od, - ); - job.is_last_job = true; - self.write_jobs.push(job); - } else { - match fs::TransferJob::new_read( - id, - to.clone(), - path.clone(), - file_num, - include_hidden, - is_remote, - od, - ) { - Err(err) => { - self.handle_job_status(id, -1, Some(err.to_string())); - } - Ok(mut job) => { - log::debug!( - "new read waiting job {}, read {} to remote {}, {} files", - id, - path, - to, - job.files().len() - ); - // let m = make_fd(job.id(), job.files(), true); - // self.handler.call("updateFolderFiles", &make_args!(m)); - job.is_last_job = true; - self.read_jobs.push(job); - self.timer = time::interval(MILLI1); - } - } - } - } - Data::ResumeJob((id, is_remote)) => { - if is_remote { - if let Some(job) = get_job(id, &mut self.write_jobs) { - job.is_last_job = false; - allow_err!( - peer.send(&fs::new_send( - id, - job.remote.clone(), - job.file_num, - job.show_hidden - )) - .await - ); - } - } else { - if let Some(job) = get_job(id, &mut self.read_jobs) { - job.is_last_job = false; - allow_err!( - peer.send(&fs::new_receive( - id, - job.path.to_string_lossy().to_string(), - job.file_num, - job.files.clone() - )) - .await - ); - } - } - } - Data::SetNoConfirm(id) => { - if let Some(job) = self.remove_jobs.get_mut(&id) { - job.no_confirm = true; - } - } - Data::ConfirmDeleteFiles((id, file_num)) => { - if let Some(job) = self.remove_jobs.get_mut(&id) { - let i = file_num as usize; - if i < job.files.len() { - // self.handler.call( - // "confirmDeleteFiles", - // &make_args!(id, file_num, job.files[i].name.clone()), - // ); - self.handler.confirm_delete_files(id, file_num); - } - } - } - Data::SetConfirmOverrideFile((id, file_num, need_override, remember, is_upload)) => { - if is_upload { - if let Some(job) = fs::get_job(id, &mut self.read_jobs) { - if remember { - job.set_overwrite_strategy(Some(need_override)); - } - job.confirm(&FileTransferSendConfirmRequest { - id, - file_num, - union: if need_override { - Some(file_transfer_send_confirm_request::Union::OffsetBlk(0)) - } else { - Some(file_transfer_send_confirm_request::Union::Skip(true)) - }, - ..Default::default() - }); - } - } else { - if let Some(job) = fs::get_job(id, &mut self.write_jobs) { - if remember { - job.set_overwrite_strategy(Some(need_override)); - } - let mut msg = Message::new(); - let mut file_action = FileAction::new(); - file_action.set_send_confirm(FileTransferSendConfirmRequest { - id, - file_num, - union: if need_override { - Some(file_transfer_send_confirm_request::Union::OffsetBlk(0)) - } else { - Some(file_transfer_send_confirm_request::Union::Skip(true)) - }, - ..Default::default() - }); - msg.set_file_action(file_action); - allow_err!(peer.send(&msg).await); - } - } - } - Data::RemoveDirAll((id, path, is_remote, include_hidden)) => { - let sep = self.handler.get_path_sep(is_remote); - if is_remote { - let mut msg_out = Message::new(); - let mut file_action = FileAction::new(); - file_action.set_all_files(ReadAllFiles { - id, - path: path.clone(), - include_hidden, - ..Default::default() - }); - msg_out.set_file_action(file_action); - allow_err!(peer.send(&msg_out).await); - self.remove_jobs - .insert(id, RemoveJob::new(Vec::new(), path, sep, is_remote)); - } else { - match fs::get_recursive_files(&path, include_hidden) { - Ok(entries) => { - // let m = make_fd(id, &entries, true); - // self.handler.call("updateFolderFiles", &make_args!(m)); - self.remove_jobs - .insert(id, RemoveJob::new(entries, path, sep, is_remote)); - } - Err(err) => { - self.handle_job_status(id, -1, Some(err.to_string())); - } - } - } - } - Data::CancelJob(id) => { - let mut msg_out = Message::new(); - let mut file_action = FileAction::new(); - file_action.set_cancel(FileTransferCancel { - id: id, - ..Default::default() - }); - msg_out.set_file_action(file_action); - allow_err!(peer.send(&msg_out).await); - if let Some(job) = fs::get_job(id, &mut self.write_jobs) { - job.remove_download_file(); - fs::remove_job(id, &mut self.write_jobs); - } - fs::remove_job(id, &mut self.read_jobs); - self.remove_jobs.remove(&id); - } - Data::RemoveDir((id, path)) => { - let mut msg_out = Message::new(); - let mut file_action = FileAction::new(); - file_action.set_remove_dir(FileRemoveDir { - id, - path, - recursive: true, - ..Default::default() - }); - msg_out.set_file_action(file_action); - allow_err!(peer.send(&msg_out).await); - } - Data::RemoveFile((id, path, file_num, is_remote)) => { - if is_remote { - let mut msg_out = Message::new(); - let mut file_action = FileAction::new(); - file_action.set_remove_file(FileRemoveFile { - id, - path, - file_num, - ..Default::default() - }); - msg_out.set_file_action(file_action); - allow_err!(peer.send(&msg_out).await); - } else { - match fs::remove_file(&path) { - Err(err) => { - self.handle_job_status(id, file_num, Some(err.to_string())); - } - Ok(()) => { - self.handle_job_status(id, file_num, None); - } - } - } - } - Data::CreateDir((id, path, is_remote)) => { - if is_remote { - let mut msg_out = Message::new(); - let mut file_action = FileAction::new(); - file_action.set_create(FileDirCreate { - id, - path, - ..Default::default() - }); - msg_out.set_file_action(file_action); - allow_err!(peer.send(&msg_out).await); - } else { - match fs::create_dir(&path) { - Err(err) => { - self.handle_job_status(id, -1, Some(err.to_string())); - } - Ok(()) => { - self.handle_job_status(id, -1, None); - } - } - } - } - _ => {} - } - true - } - - #[inline] - fn update_job_status( - job: &fs::TransferJob, - elapsed: i32, - last_update_jobs_status: &mut (Instant, HashMap), - handler: &mut Session, - ) { - if elapsed <= 0 { - return; - } - let transferred = job.transferred(); - let last_transferred = { - if let Some(v) = last_update_jobs_status.1.get(&job.id()) { - v.to_owned() - } else { - 0 - } - }; - last_update_jobs_status.1.insert(job.id(), transferred); - let speed = (transferred - last_transferred) as f64 / (elapsed as f64 / 1000.); - let file_num = job.file_num() - 1; - // handler.call( - // "jobProgress", - // &make_args!(job.id(), file_num, speed, job.finished_size() as f64), - // ); - handler.job_progress(job.id(), file_num, speed, job.finished_size() as f64); - } - - fn update_jobs_status(&mut self) { - let elapsed = self.last_update_jobs_status.0.elapsed().as_millis() as i32; - if elapsed >= 1000 { - for job in self.read_jobs.iter() { - Self::update_job_status( - job, - elapsed, - &mut self.last_update_jobs_status, - &mut self.handler, - ); - } - for job in self.write_jobs.iter() { - Self::update_job_status( - job, - elapsed, - &mut self.last_update_jobs_status, - &mut self.handler, - ); - } - self.last_update_jobs_status.0 = Instant::now(); - } - } - - async fn sync_jobs_status_to_local(&mut self) -> bool { - log::info!("sync transfer job status"); - let mut config: PeerConfig = self.handler.load_config(); - let mut transfer_metas = TransferSerde::default(); - for job in self.read_jobs.iter() { - let json_str = serde_json::to_string(&job.gen_meta()).unwrap_or_default(); - transfer_metas.read_jobs.push(json_str); - } - for job in self.write_jobs.iter() { - let json_str = serde_json::to_string(&job.gen_meta()).unwrap_or_default(); - transfer_metas.write_jobs.push(json_str); - } - log::info!("meta: {:?}", transfer_metas); - config.transfer = transfer_metas; - self.handler.save_config(config); - true - } - - async fn send_opts_after_login(&self, peer: &mut Stream) { - if let Some(opts) = self - .handler - .lc - .read() - .unwrap() - .get_option_message_after_login() - { - let mut misc = Misc::new(); - misc.set_option(opts); - let mut msg_out = Message::new(); - msg_out.set_misc(misc); - allow_err!(peer.send(&msg_out).await); - } - } - - async fn handle_msg_from_peer(&mut self, data: &[u8], peer: &mut Stream) -> bool { - if let Ok(msg_in) = Message::parse_from_bytes(&data) { - match msg_in.union { - Some(message::Union::VideoFrame(vf)) => { - if !self.first_frame { - self.first_frame = true; - // self.handler.call2("closeSuccess", &make_args!()); - self.handler.close_success(); - // self.handler.call("adaptSize", &make_args!()); - self.handler.adapt_size(); - self.send_opts_after_login(peer).await; - } - let incomming_format = CodecFormat::from(&vf); - if self.video_format != incomming_format { - self.video_format = incomming_format.clone(); - self.handler.update_quality_status(QualityStatus { - codec_format: Some(incomming_format), - ..Default::default() - }) - }; - self.video_sender.send(MediaData::VideoFrame(vf)).ok(); - } - Some(message::Union::Hash(hash)) => { - self.handler - .handle_hash(&self.handler.password.clone(), hash, peer) - .await; - } - Some(message::Union::LoginResponse(lr)) => match lr.union { - Some(login_response::Union::Error(err)) => { - if !self.handler.handle_login_error(&err) { - return false; - } - } - 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) - { - 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.load_last_jobs().await; - } - } - _ => {} - }, - Some(message::Union::CursorData(cd)) => { - self.handler.set_cursor_data(cd); - } - Some(message::Union::CursorId(id)) => { - self.handler.set_cursor_id(id.to_string()); - } - Some(message::Union::CursorPosition(cp)) => { - self.handler.set_cursor_position(cp); - } - Some(message::Union::Clipboard(cb)) => { - if !self.handler.lc.read().unwrap().disable_clipboard { - update_clipboard(cb, Some(&self.old_clipboard)); - } - } - #[cfg(windows)] - Some(message::Union::Cliprdr(clip)) => { - if !self.handler.lc.read().unwrap().disable_clipboard { - if let Some(context) = &mut self.clipboard_file_context { - if let Some(clip) = msg_2_clip(clip) { - server_clip_file(context, 0, clip); - } - } - } - } - Some(message::Union::FileResponse(fr)) => { - match fr.union { - Some(file_response::Union::Dir(fd)) => { - #[cfg(windows)] - let entries = fd.entries.to_vec(); - #[cfg(not(windows))] - let mut entries = fd.entries.to_vec(); - #[cfg(not(windows))] - { - if self.handler.peer_platform() == "Windows" { - fs::transform_windows_path(&mut entries); - } - } - // let mut m = make_fd(fd.id, &entries, fd.id > 0); - // if fd.id <= 0 { - // m.set_item("path", fd.path); - // } - // self.handler.call("updateFolderFiles", &make_args!(m)); - if let Some(job) = fs::get_job(fd.id, &mut self.write_jobs) { - log::info!("job set_files: {:?}", entries); - job.set_files(entries); - } else if let Some(job) = self.remove_jobs.get_mut(&fd.id) { - job.files = entries; - } - } - Some(file_response::Union::Digest(digest)) => { - if digest.is_upload { - if let Some(job) = fs::get_job(digest.id, &mut self.read_jobs) { - if let Some(file) = job.files().get(digest.file_num as usize) { - let read_path = get_string(&job.join(&file.name)); - let overwrite_strategy = job.default_overwrite_strategy(); - if let Some(overwrite) = overwrite_strategy { - let req = FileTransferSendConfirmRequest { - id: digest.id, - file_num: digest.file_num, - union: Some(if overwrite { - file_transfer_send_confirm_request::Union::OffsetBlk(0) - } else { - file_transfer_send_confirm_request::Union::Skip( - true, - ) - }), - ..Default::default() - }; - job.confirm(&req); - let msg = new_send_confirm(req); - allow_err!(peer.send(&msg).await); - } else { - // self.handler.call( - // "overrideFileConfirm", - // &make_args!( - // digest.id, - // digest.file_num, - // read_path, - // true - // ), - // ); - self.handler.override_file_confirm( - digest.id, - digest.file_num, - read_path, - true, - ); - } - } - } - } else { - if let Some(job) = fs::get_job(digest.id, &mut self.write_jobs) { - if let Some(file) = job.files().get(digest.file_num as usize) { - let write_path = get_string(&job.join(&file.name)); - let overwrite_strategy = job.default_overwrite_strategy(); - match fs::is_write_need_confirmation(&write_path, &digest) { - Ok(res) => match res { - DigestCheckResult::IsSame => { - let msg= new_send_confirm(FileTransferSendConfirmRequest { - id: digest.id, - file_num: digest.file_num, - union: Some(file_transfer_send_confirm_request::Union::Skip(true)), - ..Default::default() - }); - allow_err!(peer.send(&msg).await); - } - DigestCheckResult::NeedConfirm(digest) => { - if let Some(overwrite) = overwrite_strategy { - let msg = new_send_confirm( - FileTransferSendConfirmRequest { - id: digest.id, - file_num: digest.file_num, - union: Some(if overwrite { - file_transfer_send_confirm_request::Union::OffsetBlk(0) - } else { - file_transfer_send_confirm_request::Union::Skip(true) - }), - ..Default::default() - }, - ); - allow_err!(peer.send(&msg).await); - } else { - // self.handler.call( - // "overrideFileConfirm", - // &make_args!( - // digest.id, - // digest.file_num, - // write_path, - // false - // ), - // ); - self.handler.override_file_confirm( - digest.id, - digest.file_num, - write_path, - false, - ); - } - } - DigestCheckResult::NoSuchFile => { - let msg = new_send_confirm( - FileTransferSendConfirmRequest { - id: digest.id, - file_num: digest.file_num, - union: Some(file_transfer_send_confirm_request::Union::OffsetBlk(0)), - ..Default::default() - }, - ); - allow_err!(peer.send(&msg).await); - } - }, - Err(err) => { - println!("error recving digest: {}", err); - } - } - } - } - } - } - Some(file_response::Union::Block(block)) => { - log::info!( - "file response block, file id:{}, file num: {}", - block.id, - block.file_num - ); - if let Some(job) = fs::get_job(block.id, &mut self.write_jobs) { - if let Err(_err) = job.write(block, None).await { - // to-do: add "skip" for writing job - } - self.update_jobs_status(); - } - } - Some(file_response::Union::Done(d)) => { - if let Some(job) = fs::get_job(d.id, &mut self.write_jobs) { - job.modify_time(); - fs::remove_job(d.id, &mut self.write_jobs); - } - self.handle_job_status(d.id, d.file_num, None); - } - Some(file_response::Union::Error(e)) => { - self.handle_job_status(e.id, e.file_num, Some(e.error)); - } - _ => {} - } - } - Some(message::Union::Misc(misc)) => match misc.union { - Some(misc::Union::AudioFormat(f)) => { - self.audio_sender.send(MediaData::AudioFormat(f)).ok(); - } - Some(misc::Union::ChatMessage(c)) => { - // self.handler.call("newMessage", &make_args!(c.text)); // TODO - } - Some(misc::Union::PermissionInfo(p)) => { - 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); - // self.handler - // .call2("setPermission", &make_args!("keyboard", p.enabled)); - self.handler.set_permission("keyboard", p.enabled); - } - Permission::Clipboard => { - SERVER_CLIPBOARD_ENABLED.store(p.enabled, Ordering::SeqCst); - // self.handler - // .call2("setPermission", &make_args!("clipboard", p.enabled)); - self.handler.set_permission("clipboard", p.enabled); - } - Permission::Audio => { - // self.handler - // .call2("setPermission", &make_args!("audio", p.enabled)); - self.handler.set_permission("audio", p.enabled); - } - Permission::File => { - SERVER_FILE_TRANSFER_ENABLED.store(p.enabled, Ordering::SeqCst); - if !p.enabled && self.handler.is_file_transfer() { - return true; - } - self.check_clipboard_file_context(); - // self.handler - // .call2("setPermission", &make_args!("file", p.enabled)); - self.handler.set_permission("file", p.enabled); - } - Permission::Restart => { - // self.handler - // .call2("setPermission", &make_args!("restart", p.enabled)); - self.handler.set_permission("restart", p.enabled); - } - } - } - Some(misc::Union::SwitchDisplay(s)) => { - // self.handler.call("switchDisplay", &make_args!(s.display)); // TODO - self.video_sender.send(MediaData::Reset).ok(); - if s.width > 0 && s.height > 0 { - VIDEO.lock().unwrap().as_mut().map(|v| { - v.stop_streaming().ok(); - let ok = v.start_streaming( - (s.width, s.height), - COLOR_SPACE::Rgb32, - None, - ); - log::info!("[video] reinitialized: {:?}", ok); - }); - self.handler.set_display(s.x, s.y, s.width, s.height); - } - } - Some(misc::Union::CloseReason(c)) => { - self.handler.msgbox("error", "Connection Error", &c); - return false; - } - Some(misc::Union::BackNotification(notification)) => { - if !self.handle_back_notification(notification).await { - return false; - } - } - _ => {} - }, - Some(message::Union::TestDelay(t)) => { - self.handler.handle_test_delay(t, peer).await; - } - Some(message::Union::AudioFrame(frame)) => { - if !self.handler.lc.read().unwrap().disable_audio { - self.audio_sender.send(MediaData::AudioFrame(frame)).ok(); - } - } - Some(message::Union::FileAction(action)) => match action.union { - Some(file_action::Union::SendConfirm(c)) => { - if let Some(job) = fs::get_job(c.id, &mut self.read_jobs) { - job.confirm(&c); - } - } - _ => {} - }, - _ => {} - } - } - true - } - - async fn handle_back_notification(&mut self, notification: BackNotification) -> bool { - match notification.union { - Some(back_notification::Union::BlockInputState(state)) => { - self.handle_back_msg_block_input( - state.enum_value_or(back_notification::BlockInputState::BlkStateUnknown), - ) - .await; - } - Some(back_notification::Union::PrivacyModeState(state)) => { - if !self - .handle_back_msg_privacy_mode( - state.enum_value_or(back_notification::PrivacyModeState::PrvStateUnknown), - ) - .await - { - return false; - } - } - _ => {} - } - true - } - - #[inline(always)] - fn update_block_input_state(&mut self, on: bool) { - // self.handler.call("updateBlockInputState", &make_args!(on)); // TODO - } - - async fn handle_back_msg_block_input(&mut self, state: back_notification::BlockInputState) { - match state { - back_notification::BlockInputState::BlkOnSucceeded => { - self.update_block_input_state(true); - } - back_notification::BlockInputState::BlkOnFailed => { - self.handler - .msgbox("custom-error", "Block user input", "Failed"); - self.update_block_input_state(false); - } - back_notification::BlockInputState::BlkOffSucceeded => { - self.update_block_input_state(false); - } - back_notification::BlockInputState::BlkOffFailed => { - self.handler - .msgbox("custom-error", "Unblock user input", "Failed"); - } - _ => {} - } - } - - #[inline(always)] - fn update_privacy_mode(&mut self, on: bool) { - let mut config = self.handler.load_config(); - config.privacy_mode = on; - self.handler.save_config(config); - - // self.handler.call("updatePrivacyMode", &[]); - self.handler.update_privacy_mode(); - } - - async fn handle_back_msg_privacy_mode( - &mut self, - state: back_notification::PrivacyModeState, - ) -> bool { - match state { - back_notification::PrivacyModeState::PrvOnByOther => { - self.handler.msgbox( - "error", - "Connecting...", - "Someone turns on privacy mode, exit", - ); - return false; - } - back_notification::PrivacyModeState::PrvNotSupported => { - self.handler - .msgbox("custom-error", "Privacy mode", "Unsupported"); - self.update_privacy_mode(false); - } - back_notification::PrivacyModeState::PrvOnSucceeded => { - self.handler - .msgbox("custom-nocancel", "Privacy mode", "In privacy mode"); - self.update_privacy_mode(true); - } - back_notification::PrivacyModeState::PrvOnFailedDenied => { - self.handler - .msgbox("custom-error", "Privacy mode", "Peer denied"); - self.update_privacy_mode(false); - } - back_notification::PrivacyModeState::PrvOnFailedPlugin => { - self.handler - .msgbox("custom-error", "Privacy mode", "Please install plugins"); - self.update_privacy_mode(false); - } - back_notification::PrivacyModeState::PrvOnFailed => { - self.handler - .msgbox("custom-error", "Privacy mode", "Failed"); - self.update_privacy_mode(false); - } - back_notification::PrivacyModeState::PrvOffSucceeded => { - self.handler - .msgbox("custom-nocancel", "Privacy mode", "Out privacy mode"); - self.update_privacy_mode(false); - } - back_notification::PrivacyModeState::PrvOffByPeer => { - self.handler - .msgbox("custom-error", "Privacy mode", "Peer exit"); - self.update_privacy_mode(false); - } - back_notification::PrivacyModeState::PrvOffFailed => { - self.handler - .msgbox("custom-error", "Privacy mode", "Failed to turn off"); - } - back_notification::PrivacyModeState::PrvOffUnknown => { - self.handler - .msgbox("custom-error", "Privacy mode", "Turned off"); - // log::error!("Privacy mode is turned off with unknown reason"); - self.update_privacy_mode(false); - } - _ => {} - } - true - } - - fn check_clipboard_file_context(&mut self) { - #[cfg(windows)] - { - let enabled = SERVER_FILE_TRANSFER_ENABLED.load(Ordering::SeqCst) - && self.handler.lc.read().unwrap().enable_file_transfer; - if enabled == self.clipboard_file_context.is_none() { - self.clipboard_file_context = if enabled { - match create_clipboard_file_context(true, false) { - Ok(context) => { - log::info!("clipboard context for file transfer created."); - Some(context) - } - Err(err) => { - log::error!( - "Create clipboard context for file transfer: {}", - err.to_string() - ); - None - } - } - } else { - log::info!("clipboard context for file transfer destroyed."); - None - }; - } - } - } -} - pub fn make_fd(id: i32, entries: &Vec, only_count: bool) -> Value { let mut m = Value::map(); m.set_item("id", id); @@ -2776,141 +1415,3 @@ pub fn make_fd(id: i32, entries: &Vec, only_count: bool) -> Value { m.set_item("total_size", n as f64); m } - -// #[async_trait] -// impl Interface for Handler { -// fn send(&self, data: Data) { -// if let Some(ref sender) = self.read().unwrap().sender { -// sender.send(data).ok(); -// } -// } - -// fn msgbox(&self, msgtype: &str, title: &str, text: &str) { -// let retry = check_if_retry(msgtype, title, text); -// self.call2("msgbox_retry", &make_args!(msgtype, title, text, retry)); -// } - -// fn handle_login_error(&mut self, err: &str) -> bool { -// self.lc.write().unwrap().handle_login_error(err, self) -// } - -// fn handle_peer_info(&mut self, pi: PeerInfo) { -// let mut pi_sciter = Value::map(); -// let username = self.lc.read().unwrap().get_username(&pi); -// pi_sciter.set_item("username", username.clone()); -// 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); -// if get_version_number(&pi.version) < get_version_number("1.1.10") { -// self.call2("setPermission", &make_args!("restart", false)); -// } -// if self.is_file_transfer() { -// if pi.username.is_empty() { -// self.on_error("No active console user logged on, please connect and logon first."); -// return; -// } -// } else if !self.is_port_forward() { -// if pi.displays.is_empty() { -// self.lc.write().unwrap().handle_peer_info(username, pi); -// self.call("updatePrivacyMode", &[]); -// self.msgbox("error", "Remote Error", "No Display"); -// return; -// } -// 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); -// displays.push(display); -// } -// pi_sciter.set_item("displays", displays); -// let mut current = pi.current_display as usize; -// if current >= pi.displays.len() { -// current = 0; -// } -// pi_sciter.set_item("current_display", current as i32); -// let current = &pi.displays[current]; -// self.set_display(current.x, current.y, current.width, current.height); -// // https://sciter.com/forums/topic/color_spaceiyuv-crash -// // Nothing spectacular in decoder – done on CPU side. -// // So if you can do BGRA translation on your side – the better. -// // BGRA is used as internal image format so it will not require additional transformations. -// VIDEO.lock().unwrap().as_mut().map(|v| { -// let ok = v.start_streaming( -// (current.width as _, current.height as _), -// COLOR_SPACE::Rgb32, -// None, -// ); -// log::info!("[video] initialized: {:?}", ok); -// }); -// let p = self.lc.read().unwrap().should_auto_login(); -// if !p.is_empty() { -// input_os_password(p, true, self.clone()); -// } -// } -// self.lc.write().unwrap().handle_peer_info(username, pi); -// self.call("updatePrivacyMode", &[]); -// self.call("updatePi", &make_args!(pi_sciter)); -// if self.is_file_transfer() { -// self.call2("closeSuccess", &make_args!()); -// } else if !self.is_port_forward() { -// self.msgbox("success", "Successful", "Connected, waiting for image..."); -// } -// #[cfg(windows)] -// { -// let mut path = std::env::temp_dir(); -// path.push(&self.id); -// let path = path.with_extension(crate::get_app_name().to_lowercase()); -// std::fs::File::create(&path).ok(); -// if let Some(path) = path.to_str() { -// crate::platform::windows::add_recent_document(&path); -// } -// } -// self.start_keyboard_hook(); -// } - -// async fn handle_hash(&mut self, pass: &str, hash: Hash, peer: &mut Stream) { -// handle_hash(self.lc.clone(), pass, hash, self, peer).await; -// } - -// async fn handle_login_from_ui(&mut self, password: String, remember: bool, peer: &mut Stream) { -// handle_login_from_ui(self.lc.clone(), password, remember, peer).await; -// } - -// async fn handle_test_delay(&mut self, t: TestDelay, peer: &mut Stream) { -// if !t.from_client { -// self.update_quality_status(QualityStatus { -// delay: Some(t.last_delay as _), -// target_bitrate: Some(t.target_bitrate as _), -// ..Default::default() -// }); -// handle_test_delay(t, peer).await; -// } -// } - -// fn set_force_relay(&mut self, direct: bool, received: bool) { -// let mut lc = self.lc.write().unwrap(); -// lc.force_relay = false; -// if direct && !received { -// let errno = errno::errno().0; -// log::info!("errno is {}", errno); -// // TODO: check mac and ios -// if cfg!(windows) && errno == 10054 || !cfg!(windows) && errno == 104 { -// lc.force_relay = true; -// lc.set_option("force-always-relay".to_owned(), "Y".to_owned()); -// } -// } -// } - -// fn is_force_relay(&self) -> bool { -// self.lc.read().unwrap().force_relay -// } -// } - -#[tokio::main(flavor = "current_thread")] -async fn send_note(url: String, id: String, conn_id: i32, note: String) { - let body = serde_json::json!({ "id": id, "Id": conn_id, "note": note }); - allow_err!(crate::post_request(url, body.to_string(), "").await); -} diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 8f7a0b904..a8871fac1 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -1,19 +1,33 @@ use crate::client::{ self, check_if_retry, handle_hash, handle_login_from_ui, handle_test_delay, input_os_password, - FileManager, LoginConfigHandler, QualityStatus, load_config, + load_config, start_video_audio_threads, Client, CodecFormat, FileManager, LoginConfigHandler, + MediaData, MediaSender, QualityStatus, SEC30, }; +use crate::common::{ + self, check_clipboard, update_clipboard, ClipboardContext, CLIPBOARD_INTERVAL, +}; +use crate::platform; use crate::{client::Data, client::Interface}; use async_trait::async_trait; -use hbb_common::config::PeerConfig; -use hbb_common::message_proto::{CursorData, Hash, PeerInfo, TestDelay, CursorPosition}; +use hbb_common::config::{Config, LocalConfig, PeerConfig, TransferSerde}; +use hbb_common::fs::{ + can_enable_overwrite_detection, get_job, get_string, new_send_confirm, DigestCheckResult, + RemoveJobMeta, TransferJobMeta, +}; +use hbb_common::message_proto::permission_info::Permission; +use hbb_common::protobuf::Message as _; +use hbb_common::rendezvous_proto::ConnType; use hbb_common::tokio::{ self, sync::mpsc, time::{self, Duration, Instant, Interval}, }; -use hbb_common::{get_version_number, log, Stream}; +use hbb_common::{allow_err, message_proto::*, sleep}; +use hbb_common::{fs, get_version_number, log, Stream}; +use std::collections::HashMap; use std::ops::{Deref, DerefMut}; -use std::sync::{Arc, RwLock}; +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; +use std::sync::{Arc, Mutex, RwLock}; #[derive(Clone, Default)] pub struct Session { @@ -23,18 +37,173 @@ pub struct Session { pub args: Vec, pub lc: Arc>, pub sender: Arc>>>, + pub thread: Arc>>>, pub ui_handler: T, } impl Session { + pub fn get_view_style(&self) -> String { + return self.lc.read().unwrap().view_style.clone(); + } + + pub fn get_image_quality(&self) -> String { + return self.lc.read().unwrap().image_quality.clone(); + } + + pub fn save_view_style(&mut self, value: String) { + self.lc.write().unwrap().save_view_style(value); + } + + pub fn toggle_option(&mut self, name: String) { + let msg = self.lc.write().unwrap().toggle_option(name.clone()); + if name == "enable-file-transfer" { + self.send(Data::ToggleClipboardFile); + } + if let Some(msg) = msg { + self.send(Data::Message(msg)); + } + } + + pub fn get_toggle_option(&self, name: String) -> bool { + let res = self.lc.read().unwrap().get_toggle_option(&name); + return res; + } + + pub fn is_privacy_mode_supported(&self) -> bool { + self.lc.read().unwrap().is_privacy_mode_supported() + } + + pub fn refresh_video(&self) { + self.send(Data::Message(LoginConfigHandler::refresh())); + } + + pub fn save_custom_image_quality(&mut self, custom_image_quality: i32) { + let msg = self + .lc + .write() + .unwrap() + .save_custom_image_quality(custom_image_quality); + self.send(Data::Message(msg)); + } + + pub fn save_image_quality(&mut self, value: String) { + let msg = self.lc.write().unwrap().save_image_quality(value); + if let Some(msg) = msg { + self.send(Data::Message(msg)); + } + } + + pub fn get_remember(&self) -> bool { + self.lc.read().unwrap().remember + } + + pub fn set_write_override( + &mut self, + job_id: i32, + file_num: i32, + is_override: bool, + remember: bool, + is_upload: bool, + ) -> bool { + self.send(Data::SetConfirmOverrideFile(( + job_id, + file_num, + is_override, + remember, + is_upload, + ))); + true + } + + pub fn has_hwcodec(&self) -> bool { + #[cfg(not(feature = "hwcodec"))] + return false; + #[cfg(feature = "hwcodec")] + return true; + } + + pub fn change_prefer_codec(&self) { + let msg = self.lc.write().unwrap().change_prefer_codec(); + self.send(Data::Message(msg)); + } + + pub fn restart_remote_device(&self) { + let mut lc = self.lc.write().unwrap(); + lc.restarting_remote_device = true; + let msg = lc.restart_remote_device(); + self.send(Data::Message(msg)); + } + + pub fn t(&self, name: String) -> String { + crate::client::translate(name) + } + + pub fn get_audit_server(&self) -> String { + if self.lc.read().unwrap().conn_id <= 0 + || LocalConfig::get_option("access_token").is_empty() + { + return "".to_owned(); + } + crate::get_audit_server( + Config::get_option("api-server"), + Config::get_option("custom-rendezvous-server"), + ) + } + + pub fn send_note(&self, note: String) { + let url = self.get_audit_server(); + let id = self.id.clone(); + let conn_id = self.lc.read().unwrap().conn_id; + std::thread::spawn(move || { + send_note(url, id, conn_id, note); + }); + } + + pub fn is_xfce(&self) -> bool { + crate::platform::is_xfce() + } + + pub fn remove_port_forward(&self, port: i32) { + let mut config = self.load_config(); + config.port_forwards = config + .port_forwards + .drain(..) + .filter(|x| x.0 != port) + .collect(); + self.save_config(config); + self.send(Data::RemovePortForward(port)); + } + + pub fn add_port_forward(&mut self, port: i32, remote_host: String, remote_port: i32) { + let mut config = self.load_config(); + if config + .port_forwards + .iter() + .filter(|x| x.0 == port) + .next() + .is_some() + { + return; + } + let pf = (port, remote_host, remote_port); + config.port_forwards.push(pf.clone()); + self.save_config(config); + self.send(Data::AddPortForward(pf)); + } + + pub fn get_id(&self) -> String { + self.id.clone() + } + pub fn get_option(&self, k: String) -> String { - self.lc.read().unwrap().get_option(&k) + let res = self.lc.read().unwrap().get_option(&k); + return res; } pub fn set_option(&self, k: String, v: String) { - self.lc.write().unwrap().set_option(k, v); + self.lc.write().unwrap().set_option(k.clone(), v); } - + #[inline] pub fn load_config(&self) -> PeerConfig { load_config(&self.id) @@ -54,7 +223,7 @@ impl Session { self.lc.read().unwrap().info.platform.clone() } - pub fn get_platform(&mut self, is_remote: bool) -> String { + pub fn get_platform(&self, is_remote: bool) -> String { if is_remote { self.peer_platform() } else { @@ -62,7 +231,7 @@ impl Session { } } - pub fn get_path_sep(&mut self, is_remote: bool) -> &'static str { + pub fn get_path_sep(&self, is_remote: bool) -> &'static str { let p = self.get_platform(is_remote); if &p == "Windows" { return "\\"; @@ -70,32 +239,161 @@ impl Session { return "/"; } } + + pub fn input_os_password(&self, pass: String, activate: bool) { + input_os_password(pass, activate, self.clone()); + } + + pub fn get_chatbox(&self) -> String { + #[cfg(feature = "inline")] + return super::inline::get_chatbox(); + #[cfg(not(feature = "inline"))] + return "".to_owned(); + } + + pub fn get_icon(&self) -> String { + crate::get_icon() + } + + pub fn send_chat(&self, text: String) { + let mut misc = Misc::new(); + misc.set_chat_message(ChatMessage { + text, + ..Default::default() + }); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + self.send(Data::Message(msg_out)); + } + + pub fn switch_display(&self, display: i32) { + let mut misc = Misc::new(); + misc.set_switch_display(SwitchDisplay { + display, + ..Default::default() + }); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + self.send(Data::Message(msg_out)); + } + + pub fn reconnect(&self) { + println!("reconnecting"); + let cloned = self.clone(); + let mut lock = self.thread.lock().unwrap(); + lock.take().map(|t| t.join()); + *lock = Some(std::thread::spawn(move || { + io_loop(cloned); + })); + } + + pub fn get_icon_path(&self, file_type: i32, ext: String) -> String { + let mut path = Config::icon_path(); + if file_type == FileType::DirLink as i32 { + let new_path = path.join("dir_link"); + if !std::fs::metadata(&new_path).is_ok() { + #[cfg(windows)] + allow_err!(std::os::windows::fs::symlink_file(&path, &new_path)); + #[cfg(not(windows))] + allow_err!(std::os::unix::fs::symlink(&path, &new_path)); + } + path = new_path; + } else if file_type == FileType::File as i32 { + if !ext.is_empty() { + path = path.join(format!("file.{}", ext)); + } else { + path = path.join("file"); + } + if !std::fs::metadata(&path).is_ok() { + allow_err!(std::fs::File::create(&path)); + } + } else if file_type == FileType::FileLink as i32 { + let new_path = path.join("file_link"); + if !std::fs::metadata(&new_path).is_ok() { + path = path.join("file"); + if !std::fs::metadata(&path).is_ok() { + allow_err!(std::fs::File::create(&path)); + } + #[cfg(windows)] + allow_err!(std::os::windows::fs::symlink_file(&path, &new_path)); + #[cfg(not(windows))] + allow_err!(std::os::unix::fs::symlink(&path, &new_path)); + } + path = new_path; + } else if file_type == FileType::DirDrive as i32 { + if cfg!(windows) { + path = fs::get_path("C:"); + } else if cfg!(target_os = "macos") { + if let Ok(entries) = fs::get_path("/Volumes/").read_dir() { + for entry in entries { + if let Ok(entry) = entry { + path = entry.path(); + break; + } + } + } + } + } + fs::get_string(&path) + } + + pub fn login(&self, password: String, remember: bool) { + self.send(Data::Login((password, remember))); + } + + pub fn new_rdp(&self) { + self.send(Data::NewRDP); + } + + pub fn close(&self) { + self.send(Data::Close); + } } pub trait InvokeUi: Send + Sync + Clone + 'static + Sized + Default { fn set_cursor_data(&self, cd: CursorData); fn set_cursor_id(&self, id: String); - fn set_cursor_position(&self, cp:CursorPosition); + fn set_cursor_position(&self, cp: CursorPosition); fn set_display(&self, x: i32, y: i32, w: i32, h: i32); + fn set_peer_info( + &self, + username: &str, + hostname: &str, + platform: &str, + sas_enabled: bool, + displays: &Vec>, + version: &str, + current_display: usize, + is_file_transfer: bool, + ); // flutter fn update_privacy_mode(&self); fn set_permission(&self, name: &str, value: bool); fn update_pi(&self, pi: PeerInfo); fn close_success(&self); fn update_quality_status(&self, qs: QualityStatus); - fn set_connection_type(&self,is_secured: bool, direct: bool); - fn job_error(&self,id:i32, err:String, file_num:i32); - fn job_done(&self,id:i32, file_num:i32); + fn set_connection_type(&self, is_secured: bool, direct: bool); + fn job_error(&self, id: i32, err: String, file_num: i32); + fn job_done(&self, id: i32, file_num: i32); fn clear_all_jobs(&self); - fn add_job(&self, id:i32, path:String, to:String, file_num:i32, show_hidden:bool, is_remote:bool); + fn add_job( + &self, + id: i32, + path: String, + to: String, + file_num: i32, + show_hidden: bool, + is_remote: bool, + ); fn update_transfer_list(&self); // fn update_folder_files(&self); // TODO - fn confirm_delete_files(&self,id:i32, i:i32, name:String); - fn override_file_confirm(&self, id:i32, file_num:i32, to:String, is_upload:bool); - fn job_progress(&self, id:i32, file_num:i32, speed:f64, finished_size:f64); + fn confirm_delete_files(&self, id: i32, i: i32, name: String); + fn override_file_confirm(&self, id: i32, file_num: i32, to: String, is_upload: 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 msgbox(&self, msgtype: &str, title: &str, text: &str, retry: bool); } - impl Deref for Session { type Target = T; @@ -135,6 +433,7 @@ impl Interface for Session { fn msgbox(&self, msgtype: &str, title: &str, text: &str) { let retry = check_if_retry(msgtype, title, text); // self.call2("msgbox_retry", &make_args!(msgtype, title, text, retry)); + self.ui_handler.msgbox(msgtype, title, text, retry); } fn handle_login_error(&mut self, err: &str) -> bool { @@ -142,8 +441,15 @@ impl Interface for Session { } fn handle_peer_info(&mut self, pi: PeerInfo) { + let mut lc = self.lc.write().unwrap(); + // let mut pi_sciter = Value::map(); - let username = self.lc.read().unwrap().get_username(&pi); + let username = lc.get_username(&pi); + + // flutter + let mut displays = Vec::new(); + let mut current_index = pi.current_display as usize; + // pi_sciter.set_item("username", username.clone()); // pi_sciter.set_item("hostname", pi.hostname.clone()); // pi_sciter.set_item("platform", pi.platform.clone()); @@ -158,7 +464,7 @@ impl Interface for Session { } } else if !self.is_port_forward() { if pi.displays.is_empty() { - self.lc.write().unwrap().handle_peer_info(username, pi); + lc.handle_peer_info(username, pi); self.update_privacy_mode(); self.msgbox("error", "Remote Error", "No Display"); return; @@ -173,13 +479,38 @@ impl Interface for Session { // displays.push(display); // } // pi_sciter.set_item("displays", displays); - let mut current = pi.current_display as usize; - if current >= pi.displays.len() { - current = 0; + + // flutter + 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); + displays.push(h); + } + if current_index >= pi.displays.len() { + current_index = 0; + } + + if current_index >= pi.displays.len() { + current_index = 0; } // pi_sciter.set_item("current_display", current as i32); - let current = &pi.displays[current]; + let current = &pi.displays[current_index]; self.set_display(current.x, current.y, current.width, current.height); + + self.set_peer_info( + &username, + &pi.hostname, + &pi.platform, + pi.sas_enabled, + &displays, + &pi.version, + current_index, + lc.is_file_transfer, + ); + // https://sciter.com/forums/topic/color_spaceiyuv-crash // Nothing spectacular in decoder – done on CPU side. // So if you can do BGRA translation on your side – the better. @@ -192,12 +523,12 @@ impl Interface for Session { // ); // log::info!("[video] initialized: {:?}", ok); // }); - let p = self.lc.read().unwrap().should_auto_login(); + let p = lc.should_auto_login(); if !p.is_empty() { input_os_password(p, true, self.clone()); } } - self.lc.write().unwrap().handle_peer_info(username, pi); + lc.handle_peer_info(username, pi); self.update_privacy_mode(); // self.update_pi(pi); if self.is_file_transfer() { @@ -255,3 +586,1385 @@ impl Interface for Session { self.lc.read().unwrap().force_relay } } + +#[tokio::main(flavor = "current_thread")] +pub async fn io_loop(handler: Session) { + let (sender, mut receiver) = mpsc::unbounded_channel::(); + *handler.sender.write().unwrap() = Some(sender.clone()); + let mut options = crate::ipc::get_options_async().await; + let mut key = options.remove("key").unwrap_or("".to_owned()); + let token = LocalConfig::get_option("access_token"); + if key.is_empty() { + key = crate::platform::get_license_key(); + } + if handler.is_port_forward() { + if handler.is_rdp() { + let port = handler + .get_option("rdp_port".to_owned()) + .parse::() + .unwrap_or(3389); + std::env::set_var( + "rdp_username", + handler.get_option("rdp_username".to_owned()), + ); + std::env::set_var( + "rdp_password", + handler.get_option("rdp_password".to_owned()), + ); + log::info!("Remote rdp port: {}", port); + start_one_port_forward(handler, 0, "".to_owned(), port, receiver, &key, &token).await; + } else if handler.args.len() == 0 { + let pfs = handler.lc.read().unwrap().port_forwards.clone(); + let mut queues = HashMap::>::new(); + for d in pfs { + sender.send(Data::AddPortForward(d)).ok(); + } + loop { + match receiver.recv().await { + Some(Data::AddPortForward((port, remote_host, remote_port))) => { + if port <= 0 || remote_port <= 0 { + continue; + } + let (sender, receiver) = mpsc::unbounded_channel::(); + queues.insert(port, sender); + let handler = handler.clone(); + let key = key.clone(); + let token = token.clone(); + tokio::spawn(async move { + start_one_port_forward( + handler, + port, + remote_host, + remote_port, + receiver, + &key, + &token, + ) + .await; + }); + } + Some(Data::RemovePortForward(port)) => { + if let Some(s) = queues.remove(&port) { + s.send(Data::Close).ok(); + } + } + Some(Data::Close) => { + break; + } + Some(d) => { + for (_, s) in queues.iter() { + s.send(d.clone()).ok(); + } + } + _ => {} + } + } + } else { + let port = handler.args[0].parse::().unwrap_or(0); + if handler.args.len() != 3 + || handler.args[2].parse::().unwrap_or(0) <= 0 + || port <= 0 + { + handler.on_error("Invalid arguments, usage:

    rustdesk --port-forward remote-id listen-port remote-host remote-port"); + } + let remote_host = handler.args[1].clone(); + let remote_port = handler.args[2].parse::().unwrap_or(0); + start_one_port_forward( + handler, + port, + remote_host, + remote_port, + receiver, + &key, + &token, + ) + .await; + } + return; + } + 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]| { + frame_count_cl.fetch_add(1, Ordering::Relaxed); + ui_handler.on_rgba(data); + }); + + let mut remote = Remote::new( + handler, + video_sender, + audio_sender, + receiver, + sender, + frame_count, + ); + remote.io_loop(&key, &token).await; + remote.sync_jobs_status_to_local().await; +} + +async fn start_one_port_forward( + handler: Session, + port: i32, + remote_host: String, + remote_port: i32, + receiver: mpsc::UnboundedReceiver, + key: &str, + token: &str, +) { + if let Err(err) = crate::port_forward::listen( + handler.id.clone(), + handler.password.clone(), + port, + handler.clone(), + receiver, + key, + token, + handler.lc.clone(), + remote_host, + remote_port, + ) + .await + { + handler.on_error(&format!("Failed to listen on {}: {}", port, err)); + } + log::info!("port forward (:{}) exit", port); +} + +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); +const MILLI1: Duration = Duration::from_millis(1); + +pub struct Remote { + handler: Session, + video_sender: MediaSender, + audio_sender: MediaSender, + receiver: mpsc::UnboundedReceiver, + sender: mpsc::UnboundedSender, + old_clipboard: Arc>, + read_jobs: Vec, + write_jobs: Vec, + remove_jobs: HashMap, + timer: Interval, + last_update_jobs_status: (Instant, HashMap), + first_frame: bool, + #[cfg(windows)] + clipboard_file_context: Option>, + data_count: Arc, + frame_count: Arc, + video_format: CodecFormat, +} + +impl Remote { + pub fn new( + handler: Session, + video_sender: MediaSender, + audio_sender: MediaSender, + receiver: mpsc::UnboundedReceiver, + sender: mpsc::UnboundedSender, + frame_count: Arc, + ) -> Self { + Self { + handler, + video_sender, + audio_sender, + receiver, + sender, + old_clipboard: Default::default(), + read_jobs: Vec::new(), + write_jobs: Vec::new(), + remove_jobs: Default::default(), + timer: time::interval(SEC30), + last_update_jobs_status: (Instant::now(), Default::default()), + first_frame: false, + #[cfg(windows)] + clipboard_file_context: None, + data_count: Arc::new(AtomicUsize::new(0)), + frame_count, + video_format: CodecFormat::Unknown, + } + } + + 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() { + ConnType::FILE_TRANSFER + } else { + ConnType::default() + }; + match Client::start( + &self.handler.id, + key, + token, + conn_type, + self.handler.clone(), + ) + .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 + // .call("setConnectionType", &make_args!(peer.is_secured(), direct)); + self.handler.set_connection_type(peer.is_secured(), direct); // flutter -> connection_ready + + // just build for now + #[cfg(not(windows))] + let (_tx_holder, mut rx_clip_client) = mpsc::unbounded_channel::(); + #[cfg(windows)] + let mut rx_clip_client = get_rx_clip_client().lock().await; + + let mut status_timer = time::interval(Duration::new(1, 0)); + + loop { + tokio::select! { + res = peer.next() => { + if let Some(res) = res { + match res { + Err(err) => { + log::error!("Connection closed: {}", err); + self.handler.set_force_relay(direct, received); + self.handler.msgbox("error", "Connection Error", &err.to_string()); + break; + } + Ok(ref bytes) => { + last_recv_time = Instant::now(); + received = true; + self.data_count.fetch_add(bytes.len(), Ordering::Relaxed); + if !self.handle_msg_from_peer(bytes, &mut peer).await { + break + } + } + } + } else { + if self.handler.is_restarting_remote_device() { + log::info!("Restart remote device"); + self.handler.msgbox("restarting", "Restarting Remote Device", "remote_restarting_tip"); + } else { + log::info!("Reset by the peer"); + self.handler.msgbox("error", "Connection Error", "Reset by the peer"); + } + break; + } + } + d = self.receiver.recv() => { + if let Some(d) = d { + if !self.handle_msg_from_ui(d, &mut peer).await { + break; + } + } + } + _msg = rx_clip_client.recv() => { + #[cfg(windows)] + match _msg { + Some((_, clip)) => { + allow_err!(peer.send(&clip_2_msg(clip)).await); + } + None => { + // unreachable!() + } + } + } + _ = self.timer.tick() => { + if last_recv_time.elapsed() >= SEC30 { + self.handler.msgbox("error", "Connection Error", "Timeout"); + break; + } + if !self.read_jobs.is_empty() { + if let Err(err) = fs::handle_read_jobs(&mut self.read_jobs, &mut peer).await { + self.handler.msgbox("error", "Connection Error", &err.to_string()); + break; + } + self.update_jobs_status(); + } else { + self.timer = time::interval_at(Instant::now() + SEC30, SEC30); + } + } + _ = status_timer.tick() => { + let speed = self.data_count.swap(0, Ordering::Relaxed); + let speed = format!("{:.2}kB/s", speed as f32 / 1024 as f32); + let fps = self.frame_count.swap(0, Ordering::Relaxed) as _; + self.handler.update_quality_status(QualityStatus { + speed:Some(speed), + fps:Some(fps), + ..Default::default() + }); + } + } + } + log::debug!("Exit io_loop of id={}", self.handler.id); + } + Err(err) => { + self.handler + .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); + } + + fn handle_job_status(&mut self, id: i32, file_num: i32, err: Option) { + if let Some(job) = self.remove_jobs.get_mut(&id) { + if job.no_confirm { + let file_num = (file_num + 1) as usize; + if file_num < job.files.len() { + let path = format!("{}{}{}", job.path, job.sep, job.files[file_num].name); + self.sender + .send(Data::RemoveFile((id, path, file_num as i32, job.is_remote))) + .ok(); + let elapsed = job.last_update_job_status.elapsed().as_millis() as i32; + if elapsed >= 1000 { + job.last_update_job_status = Instant::now(); + } else { + return; + } + } else { + self.remove_jobs.remove(&id); + } + } + } + if let Some(err) = err { + // self.handler + // .call("jobError", &make_args!(id, err, file_num)); + self.handler.job_error(id, err, file_num); + } else { + // self.handler.call("jobDone", &make_args!(id, file_num)); + self.handler.job_done(id, file_num); + } + } + + 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(); + 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 + { + 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 load_last_jobs(&mut self) { + log::info!("start load last jobs"); + // self.handler.call("clearAllJobs", &make_args!()); + self.handler.clear_all_jobs(); + let pc = self.handler.load_config(); + if pc.transfer.write_jobs.is_empty() && pc.transfer.read_jobs.is_empty() { + // no last jobs + return; + } + // TODO: can add a confirm dialog + let mut cnt = 1; + for job_str in pc.transfer.read_jobs.iter() { + let job: Result = serde_json::from_str(&job_str); + if let Ok(job) = job { + // self.handler.call( + // "addJob", + // &make_args!( + // cnt, + // job.to.clone(), + // job.remote.clone(), + // job.file_num, + // job.show_hidden, + // false + // ), + // ); + self.handler.add_job( + cnt, + job.to.clone(), + job.remote.clone(), + job.file_num, + job.show_hidden, + false, + ); + cnt += 1; + println!("restore read_job: {:?}", job); + } + } + for job_str in pc.transfer.write_jobs.iter() { + let job: Result = serde_json::from_str(&job_str); + if let Ok(job) = job { + // self.handler.call( + // "addJob", + // &make_args!( + // cnt, + // job.remote.clone(), + // job.to.clone(), + // job.file_num, + // job.show_hidden, + // true + // ), + // ); + self.handler.add_job( + cnt, + job.remote.clone(), + job.to.clone(), + job.file_num, + job.show_hidden, + true, + ); + cnt += 1; + println!("restore write_job: {:?}", job); + } + } + // self.handler.call("updateTransferList", &make_args!()); + self.handler.update_transfer_list(); + } + + async fn handle_msg_from_ui(&mut self, data: Data, peer: &mut Stream) -> bool { + match data { + Data::Close => { + let mut misc = Misc::new(); + misc.set_close_reason("".to_owned()); + let mut msg = Message::new(); + msg.set_misc(misc); + allow_err!(peer.send(&msg).await); + return false; + } + Data::Login((password, remember)) => { + self.handler + .handle_login_from_ui(password, remember, peer) + .await; + } + Data::ToggleClipboardFile => { + self.check_clipboard_file_context(); + } + Data::Message(msg) => { + allow_err!(peer.send(&msg).await); + } + Data::SendFiles((id, path, to, file_num, include_hidden, is_remote)) => { + log::info!("send files, is remote {}", is_remote); + let od = can_enable_overwrite_detection(self.handler.lc.read().unwrap().version); + if is_remote { + log::debug!("New job {}, write to {} from remote {}", id, to, path); + self.write_jobs.push(fs::TransferJob::new_write( + id, + path.clone(), + to, + file_num, + include_hidden, + is_remote, + Vec::new(), + od, + )); + allow_err!( + peer.send(&fs::new_send(id, path, file_num, include_hidden)) + .await + ); + } else { + match fs::TransferJob::new_read( + id, + to.clone(), + path.clone(), + file_num, + include_hidden, + is_remote, + od, + ) { + Err(err) => { + self.handle_job_status(id, -1, Some(err.to_string())); + } + Ok(job) => { + log::debug!( + "New job {}, read {} to remote {}, {} files", + id, + path, + to, + job.files().len() + ); + // let m = make_fd(job.id(), job.files(), true); + // self.handler.call("updateFolderFiles", &make_args!(m)); // TODO + #[cfg(not(windows))] + let files = job.files().clone(); + #[cfg(windows)] + let mut files = job.files().clone(); + #[cfg(windows)] + if self.handler.peer_platform() != "Windows" { + // peer is not windows, need transform \ to / + fs::transform_windows_path(&mut files); + } + self.read_jobs.push(job); + self.timer = time::interval(MILLI1); + allow_err!(peer.send(&fs::new_receive(id, to, file_num, files)).await); + } + } + } + } + Data::AddJob((id, path, to, file_num, include_hidden, is_remote)) => { + let od = can_enable_overwrite_detection(self.handler.lc.read().unwrap().version); + if is_remote { + log::debug!( + "new write waiting job {}, write to {} from remote {}", + id, + to, + path + ); + let mut job = fs::TransferJob::new_write( + id, + path.clone(), + to, + file_num, + include_hidden, + is_remote, + Vec::new(), + od, + ); + job.is_last_job = true; + self.write_jobs.push(job); + } else { + match fs::TransferJob::new_read( + id, + to.clone(), + path.clone(), + file_num, + include_hidden, + is_remote, + od, + ) { + Err(err) => { + self.handle_job_status(id, -1, Some(err.to_string())); + } + Ok(mut job) => { + log::debug!( + "new read waiting job {}, read {} to remote {}, {} files", + id, + path, + to, + job.files().len() + ); + // let m = make_fd(job.id(), job.files(), true); + // self.handler.call("updateFolderFiles", &make_args!(m)); + job.is_last_job = true; + self.read_jobs.push(job); + self.timer = time::interval(MILLI1); + } + } + } + } + Data::ResumeJob((id, is_remote)) => { + if is_remote { + if let Some(job) = get_job(id, &mut self.write_jobs) { + job.is_last_job = false; + allow_err!( + peer.send(&fs::new_send( + id, + job.remote.clone(), + job.file_num, + job.show_hidden + )) + .await + ); + } + } else { + if let Some(job) = get_job(id, &mut self.read_jobs) { + job.is_last_job = false; + allow_err!( + peer.send(&fs::new_receive( + id, + job.path.to_string_lossy().to_string(), + job.file_num, + job.files.clone() + )) + .await + ); + } + } + } + Data::SetNoConfirm(id) => { + if let Some(job) = self.remove_jobs.get_mut(&id) { + job.no_confirm = true; + } + } + Data::ConfirmDeleteFiles((id, file_num)) => { + if let Some(job) = self.remove_jobs.get_mut(&id) { + let i = file_num as usize; + if i < job.files.len() { + // self.handler.call( + // "confirmDeleteFiles", + // &make_args!(id, file_num, job.files[i].name.clone()), + // ); + self.handler.confirm_delete_files(id, file_num); + } + } + } + Data::SetConfirmOverrideFile((id, file_num, need_override, remember, is_upload)) => { + if is_upload { + if let Some(job) = fs::get_job(id, &mut self.read_jobs) { + if remember { + job.set_overwrite_strategy(Some(need_override)); + } + job.confirm(&FileTransferSendConfirmRequest { + id, + file_num, + union: if need_override { + Some(file_transfer_send_confirm_request::Union::OffsetBlk(0)) + } else { + Some(file_transfer_send_confirm_request::Union::Skip(true)) + }, + ..Default::default() + }); + } + } else { + if let Some(job) = fs::get_job(id, &mut self.write_jobs) { + if remember { + job.set_overwrite_strategy(Some(need_override)); + } + let mut msg = Message::new(); + let mut file_action = FileAction::new(); + file_action.set_send_confirm(FileTransferSendConfirmRequest { + id, + file_num, + union: if need_override { + Some(file_transfer_send_confirm_request::Union::OffsetBlk(0)) + } else { + Some(file_transfer_send_confirm_request::Union::Skip(true)) + }, + ..Default::default() + }); + msg.set_file_action(file_action); + allow_err!(peer.send(&msg).await); + } + } + } + Data::RemoveDirAll((id, path, is_remote, include_hidden)) => { + let sep = self.handler.get_path_sep(is_remote); + if is_remote { + let mut msg_out = Message::new(); + let mut file_action = FileAction::new(); + file_action.set_all_files(ReadAllFiles { + id, + path: path.clone(), + include_hidden, + ..Default::default() + }); + msg_out.set_file_action(file_action); + allow_err!(peer.send(&msg_out).await); + self.remove_jobs + .insert(id, RemoveJob::new(Vec::new(), path, sep, is_remote)); + } else { + match fs::get_recursive_files(&path, include_hidden) { + Ok(entries) => { + // let m = make_fd(id, &entries, true); + // self.handler.call("updateFolderFiles", &make_args!(m)); + self.remove_jobs + .insert(id, RemoveJob::new(entries, path, sep, is_remote)); + } + Err(err) => { + self.handle_job_status(id, -1, Some(err.to_string())); + } + } + } + } + Data::CancelJob(id) => { + let mut msg_out = Message::new(); + let mut file_action = FileAction::new(); + file_action.set_cancel(FileTransferCancel { + id: id, + ..Default::default() + }); + msg_out.set_file_action(file_action); + allow_err!(peer.send(&msg_out).await); + if let Some(job) = fs::get_job(id, &mut self.write_jobs) { + job.remove_download_file(); + fs::remove_job(id, &mut self.write_jobs); + } + fs::remove_job(id, &mut self.read_jobs); + self.remove_jobs.remove(&id); + } + Data::RemoveDir((id, path)) => { + let mut msg_out = Message::new(); + let mut file_action = FileAction::new(); + file_action.set_remove_dir(FileRemoveDir { + id, + path, + recursive: true, + ..Default::default() + }); + msg_out.set_file_action(file_action); + allow_err!(peer.send(&msg_out).await); + } + Data::RemoveFile((id, path, file_num, is_remote)) => { + if is_remote { + let mut msg_out = Message::new(); + let mut file_action = FileAction::new(); + file_action.set_remove_file(FileRemoveFile { + id, + path, + file_num, + ..Default::default() + }); + msg_out.set_file_action(file_action); + allow_err!(peer.send(&msg_out).await); + } else { + match fs::remove_file(&path) { + Err(err) => { + self.handle_job_status(id, file_num, Some(err.to_string())); + } + Ok(()) => { + self.handle_job_status(id, file_num, None); + } + } + } + } + Data::CreateDir((id, path, is_remote)) => { + if is_remote { + let mut msg_out = Message::new(); + let mut file_action = FileAction::new(); + file_action.set_create(FileDirCreate { + id, + path, + ..Default::default() + }); + msg_out.set_file_action(file_action); + allow_err!(peer.send(&msg_out).await); + } else { + match fs::create_dir(&path) { + Err(err) => { + self.handle_job_status(id, -1, Some(err.to_string())); + } + Ok(()) => { + self.handle_job_status(id, -1, None); + } + } + } + } + _ => {} + } + true + } + + #[inline] + fn update_job_status( + job: &fs::TransferJob, + elapsed: i32, + last_update_jobs_status: &mut (Instant, HashMap), + handler: &mut Session, + ) { + if elapsed <= 0 { + return; + } + let transferred = job.transferred(); + let last_transferred = { + if let Some(v) = last_update_jobs_status.1.get(&job.id()) { + v.to_owned() + } else { + 0 + } + }; + last_update_jobs_status.1.insert(job.id(), transferred); + let speed = (transferred - last_transferred) as f64 / (elapsed as f64 / 1000.); + let file_num = job.file_num() - 1; + // handler.call( + // "jobProgress", + // &make_args!(job.id(), file_num, speed, job.finished_size() as f64), + // ); + handler.job_progress(job.id(), file_num, speed, job.finished_size() as f64); + } + + fn update_jobs_status(&mut self) { + let elapsed = self.last_update_jobs_status.0.elapsed().as_millis() as i32; + if elapsed >= 1000 { + for job in self.read_jobs.iter() { + Self::update_job_status( + job, + elapsed, + &mut self.last_update_jobs_status, + &mut self.handler, + ); + } + for job in self.write_jobs.iter() { + Self::update_job_status( + job, + elapsed, + &mut self.last_update_jobs_status, + &mut self.handler, + ); + } + self.last_update_jobs_status.0 = Instant::now(); + } + } + + pub async fn sync_jobs_status_to_local(&mut self) -> bool { + log::info!("sync transfer job status"); + let mut config: PeerConfig = self.handler.load_config(); + let mut transfer_metas = TransferSerde::default(); + for job in self.read_jobs.iter() { + let json_str = serde_json::to_string(&job.gen_meta()).unwrap_or_default(); + transfer_metas.read_jobs.push(json_str); + } + for job in self.write_jobs.iter() { + let json_str = serde_json::to_string(&job.gen_meta()).unwrap_or_default(); + transfer_metas.write_jobs.push(json_str); + } + log::info!("meta: {:?}", transfer_metas); + config.transfer = transfer_metas; + self.handler.save_config(config); + true + } + + async fn send_opts_after_login(&self, peer: &mut Stream) { + if let Some(opts) = self + .handler + .lc + .read() + .unwrap() + .get_option_message_after_login() + { + let mut misc = Misc::new(); + misc.set_option(opts); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + allow_err!(peer.send(&msg_out).await); + } + } + + async fn handle_msg_from_peer(&mut self, data: &[u8], peer: &mut Stream) -> bool { + if let Ok(msg_in) = Message::parse_from_bytes(&data) { + match msg_in.union { + Some(message::Union::VideoFrame(vf)) => { + if !self.first_frame { + self.first_frame = true; + // self.handler.call2("closeSuccess", &make_args!()); + self.handler.close_success(); + // self.handler.call("adaptSize", &make_args!()); + self.handler.adapt_size(); + self.send_opts_after_login(peer).await; + } + let incomming_format = CodecFormat::from(&vf); + if self.video_format != incomming_format { + self.video_format = incomming_format.clone(); + self.handler.update_quality_status(QualityStatus { + codec_format: Some(incomming_format), + ..Default::default() + }) + }; + self.video_sender.send(MediaData::VideoFrame(vf)).ok(); + } + Some(message::Union::Hash(hash)) => { + self.handler + .handle_hash(&self.handler.password.clone(), hash, peer) + .await; + } + Some(message::Union::LoginResponse(lr)) => match lr.union { + Some(login_response::Union::Error(err)) => { + if !self.handler.handle_login_error(&err) { + return false; + } + } + 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) + // { + // 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.load_last_jobs().await; + // } + } + _ => {} + }, + Some(message::Union::CursorData(cd)) => { + self.handler.set_cursor_data(cd); + } + Some(message::Union::CursorId(id)) => { + self.handler.set_cursor_id(id.to_string()); + } + Some(message::Union::CursorPosition(cp)) => { + self.handler.set_cursor_position(cp); + } + Some(message::Union::Clipboard(cb)) => { + if !self.handler.lc.read().unwrap().disable_clipboard { + update_clipboard(cb, Some(&self.old_clipboard)); + } + } + #[cfg(windows)] + Some(message::Union::Cliprdr(clip)) => { + if !self.handler.lc.read().unwrap().disable_clipboard { + if let Some(context) = &mut self.clipboard_file_context { + if let Some(clip) = msg_2_clip(clip) { + server_clip_file(context, 0, clip); + } + } + } + } + Some(message::Union::FileResponse(fr)) => { + match fr.union { + Some(file_response::Union::Dir(fd)) => { + #[cfg(windows)] + let entries = fd.entries.to_vec(); + #[cfg(not(windows))] + let mut entries = fd.entries.to_vec(); + #[cfg(not(windows))] + { + if self.handler.peer_platform() == "Windows" { + fs::transform_windows_path(&mut entries); + } + } + // let mut m = make_fd(fd.id, &entries, fd.id > 0); + // if fd.id <= 0 { + // m.set_item("path", fd.path); + // } + // self.handler.call("updateFolderFiles", &make_args!(m)); + if let Some(job) = fs::get_job(fd.id, &mut self.write_jobs) { + log::info!("job set_files: {:?}", entries); + job.set_files(entries); + } else if let Some(job) = self.remove_jobs.get_mut(&fd.id) { + job.files = entries; + } + } + Some(file_response::Union::Digest(digest)) => { + if digest.is_upload { + if let Some(job) = fs::get_job(digest.id, &mut self.read_jobs) { + if let Some(file) = job.files().get(digest.file_num as usize) { + let read_path = get_string(&job.join(&file.name)); + let overwrite_strategy = job.default_overwrite_strategy(); + if let Some(overwrite) = overwrite_strategy { + let req = FileTransferSendConfirmRequest { + id: digest.id, + file_num: digest.file_num, + union: Some(if overwrite { + file_transfer_send_confirm_request::Union::OffsetBlk(0) + } else { + file_transfer_send_confirm_request::Union::Skip( + true, + ) + }), + ..Default::default() + }; + job.confirm(&req); + let msg = new_send_confirm(req); + allow_err!(peer.send(&msg).await); + } else { + // self.handler.call( + // "overrideFileConfirm", + // &make_args!( + // digest.id, + // digest.file_num, + // read_path, + // true + // ), + // ); + self.handler.override_file_confirm( + digest.id, + digest.file_num, + read_path, + true, + ); + } + } + } + } else { + if let Some(job) = fs::get_job(digest.id, &mut self.write_jobs) { + if let Some(file) = job.files().get(digest.file_num as usize) { + let write_path = get_string(&job.join(&file.name)); + let overwrite_strategy = job.default_overwrite_strategy(); + match fs::is_write_need_confirmation(&write_path, &digest) { + Ok(res) => match res { + DigestCheckResult::IsSame => { + let msg= new_send_confirm(FileTransferSendConfirmRequest { + id: digest.id, + file_num: digest.file_num, + union: Some(file_transfer_send_confirm_request::Union::Skip(true)), + ..Default::default() + }); + allow_err!(peer.send(&msg).await); + } + DigestCheckResult::NeedConfirm(digest) => { + if let Some(overwrite) = overwrite_strategy { + let msg = new_send_confirm( + FileTransferSendConfirmRequest { + id: digest.id, + file_num: digest.file_num, + union: Some(if overwrite { + file_transfer_send_confirm_request::Union::OffsetBlk(0) + } else { + file_transfer_send_confirm_request::Union::Skip(true) + }), + ..Default::default() + }, + ); + allow_err!(peer.send(&msg).await); + } else { + // self.handler.call( + // "overrideFileConfirm", + // &make_args!( + // digest.id, + // digest.file_num, + // write_path, + // false + // ), + // ); + self.handler.override_file_confirm( + digest.id, + digest.file_num, + write_path, + false, + ); + } + } + DigestCheckResult::NoSuchFile => { + let msg = new_send_confirm( + FileTransferSendConfirmRequest { + id: digest.id, + file_num: digest.file_num, + union: Some(file_transfer_send_confirm_request::Union::OffsetBlk(0)), + ..Default::default() + }, + ); + allow_err!(peer.send(&msg).await); + } + }, + Err(err) => { + println!("error recving digest: {}", err); + } + } + } + } + } + } + Some(file_response::Union::Block(block)) => { + log::info!( + "file response block, file id:{}, file num: {}", + block.id, + block.file_num + ); + if let Some(job) = fs::get_job(block.id, &mut self.write_jobs) { + if let Err(_err) = job.write(block, None).await { + // to-do: add "skip" for writing job + } + self.update_jobs_status(); + } + } + Some(file_response::Union::Done(d)) => { + if let Some(job) = fs::get_job(d.id, &mut self.write_jobs) { + job.modify_time(); + fs::remove_job(d.id, &mut self.write_jobs); + } + self.handle_job_status(d.id, d.file_num, None); + } + Some(file_response::Union::Error(e)) => { + self.handle_job_status(e.id, e.file_num, Some(e.error)); + } + _ => {} + } + } + Some(message::Union::Misc(misc)) => match misc.union { + Some(misc::Union::AudioFormat(f)) => { + self.audio_sender.send(MediaData::AudioFormat(f)).ok(); + } + Some(misc::Union::ChatMessage(c)) => { + // self.handler.call("newMessage", &make_args!(c.text)); // TODO + } + Some(misc::Union::PermissionInfo(p)) => { + 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); + // self.handler + // .call2("setPermission", &make_args!("keyboard", p.enabled)); + self.handler.set_permission("keyboard", p.enabled); + } + Permission::Clipboard => { + SERVER_CLIPBOARD_ENABLED.store(p.enabled, Ordering::SeqCst); + // self.handler + // .call2("setPermission", &make_args!("clipboard", p.enabled)); + self.handler.set_permission("clipboard", p.enabled); + } + Permission::Audio => { + // self.handler + // .call2("setPermission", &make_args!("audio", p.enabled)); + self.handler.set_permission("audio", p.enabled); + } + Permission::File => { + SERVER_FILE_TRANSFER_ENABLED.store(p.enabled, Ordering::SeqCst); + if !p.enabled && self.handler.is_file_transfer() { + return true; + } + self.check_clipboard_file_context(); + // self.handler + // .call2("setPermission", &make_args!("file", p.enabled)); + self.handler.set_permission("file", p.enabled); + } + Permission::Restart => { + // self.handler + // .call2("setPermission", &make_args!("restart", p.enabled)); + self.handler.set_permission("restart", p.enabled); + } + } + } + Some(misc::Union::SwitchDisplay(s)) => { + // self.handler.call("switchDisplay", &make_args!(s.display)); // TODO + self.video_sender.send(MediaData::Reset).ok(); + if s.width > 0 && s.height > 0 { + self.handler.set_display(s.x, s.y, s.width, s.height); + } + } + Some(misc::Union::CloseReason(c)) => { + self.handler.msgbox("error", "Connection Error", &c); + return false; + } + Some(misc::Union::BackNotification(notification)) => { + if !self.handle_back_notification(notification).await { + return false; + } + } + _ => {} + }, + Some(message::Union::TestDelay(t)) => { + self.handler.handle_test_delay(t, peer).await; + } + Some(message::Union::AudioFrame(frame)) => { + if !self.handler.lc.read().unwrap().disable_audio { + self.audio_sender.send(MediaData::AudioFrame(frame)).ok(); + } + } + Some(message::Union::FileAction(action)) => match action.union { + Some(file_action::Union::SendConfirm(c)) => { + if let Some(job) = fs::get_job(c.id, &mut self.read_jobs) { + job.confirm(&c); + } + } + _ => {} + }, + _ => {} + } + } + true + } + + async fn handle_back_notification(&mut self, notification: BackNotification) -> bool { + match notification.union { + Some(back_notification::Union::BlockInputState(state)) => { + self.handle_back_msg_block_input( + state.enum_value_or(back_notification::BlockInputState::BlkStateUnknown), + ) + .await; + } + Some(back_notification::Union::PrivacyModeState(state)) => { + if !self + .handle_back_msg_privacy_mode( + state.enum_value_or(back_notification::PrivacyModeState::PrvStateUnknown), + ) + .await + { + return false; + } + } + _ => {} + } + true + } + + #[inline(always)] + fn update_block_input_state(&mut self, on: bool) { + // self.handler.call("updateBlockInputState", &make_args!(on)); // TODO + } + + async fn handle_back_msg_block_input(&mut self, state: back_notification::BlockInputState) { + match state { + back_notification::BlockInputState::BlkOnSucceeded => { + self.update_block_input_state(true); + } + back_notification::BlockInputState::BlkOnFailed => { + self.handler + .msgbox("custom-error", "Block user input", "Failed"); + self.update_block_input_state(false); + } + back_notification::BlockInputState::BlkOffSucceeded => { + self.update_block_input_state(false); + } + back_notification::BlockInputState::BlkOffFailed => { + self.handler + .msgbox("custom-error", "Unblock user input", "Failed"); + } + _ => {} + } + } + + #[inline(always)] + fn update_privacy_mode(&mut self, on: bool) { + let mut config = self.handler.load_config(); + config.privacy_mode = on; + self.handler.save_config(config); + + // self.handler.call("updatePrivacyMode", &[]); + self.handler.update_privacy_mode(); + } + + async fn handle_back_msg_privacy_mode( + &mut self, + state: back_notification::PrivacyModeState, + ) -> bool { + match state { + back_notification::PrivacyModeState::PrvOnByOther => { + self.handler.msgbox( + "error", + "Connecting...", + "Someone turns on privacy mode, exit", + ); + return false; + } + back_notification::PrivacyModeState::PrvNotSupported => { + self.handler + .msgbox("custom-error", "Privacy mode", "Unsupported"); + self.update_privacy_mode(false); + } + back_notification::PrivacyModeState::PrvOnSucceeded => { + self.handler + .msgbox("custom-nocancel", "Privacy mode", "In privacy mode"); + self.update_privacy_mode(true); + } + back_notification::PrivacyModeState::PrvOnFailedDenied => { + self.handler + .msgbox("custom-error", "Privacy mode", "Peer denied"); + self.update_privacy_mode(false); + } + back_notification::PrivacyModeState::PrvOnFailedPlugin => { + self.handler + .msgbox("custom-error", "Privacy mode", "Please install plugins"); + self.update_privacy_mode(false); + } + back_notification::PrivacyModeState::PrvOnFailed => { + self.handler + .msgbox("custom-error", "Privacy mode", "Failed"); + self.update_privacy_mode(false); + } + back_notification::PrivacyModeState::PrvOffSucceeded => { + self.handler + .msgbox("custom-nocancel", "Privacy mode", "Out privacy mode"); + self.update_privacy_mode(false); + } + back_notification::PrivacyModeState::PrvOffByPeer => { + self.handler + .msgbox("custom-error", "Privacy mode", "Peer exit"); + self.update_privacy_mode(false); + } + back_notification::PrivacyModeState::PrvOffFailed => { + self.handler + .msgbox("custom-error", "Privacy mode", "Failed to turn off"); + } + back_notification::PrivacyModeState::PrvOffUnknown => { + self.handler + .msgbox("custom-error", "Privacy mode", "Turned off"); + // log::error!("Privacy mode is turned off with unknown reason"); + self.update_privacy_mode(false); + } + _ => {} + } + true + } + + fn check_clipboard_file_context(&mut self) { + #[cfg(windows)] + { + let enabled = SERVER_FILE_TRANSFER_ENABLED.load(Ordering::SeqCst) + && self.handler.lc.read().unwrap().enable_file_transfer; + if enabled == self.clipboard_file_context.is_none() { + self.clipboard_file_context = if enabled { + match create_clipboard_file_context(true, false) { + Ok(context) => { + log::info!("clipboard context for file transfer created."); + Some(context) + } + Err(err) => { + log::error!( + "Create clipboard context for file transfer: {}", + err.to_string() + ); + None + } + } + } else { + log::info!("clipboard context for file transfer destroyed."); + None + }; + } + } + } +} + +struct RemoveJob { + files: Vec, + path: String, + sep: &'static str, + is_remote: bool, + no_confirm: bool, + last_update_job_status: Instant, +} + +impl RemoveJob { + fn new(files: Vec, path: String, sep: &'static str, is_remote: bool) -> Self { + Self { + files, + path, + sep, + is_remote, + no_confirm: false, + last_update_job_status: Instant::now(), + } + } + + pub fn _gen_meta(&self) -> RemoveJobMeta { + RemoveJobMeta { + path: self.path.clone(), + is_remote: self.is_remote, + no_confirm: self.no_confirm, + } + } +} + +#[tokio::main(flavor = "current_thread")] +async fn send_note(url: String, id: String, conn_id: i32, note: String) { + let body = serde_json::json!({ "id": id, "Id": conn_id, "note": note }); + allow_err!(crate::post_request(url, body.to_string(), "").await); +} From ae265ca83619b15261690eeb5aa60bb3b519cd4f Mon Sep 17 00:00:00 2001 From: csf Date: Wed, 31 Aug 2022 22:24:57 +0800 Subject: [PATCH 0335/2015] flutter.rs Session -> ui_session_interface.rs --- src/client.rs | 13 + src/flutter.rs | 610 ------------------------------------ src/ui/remote.rs | 114 +------ src/ui_session_interface.rs | 234 +++++++++++--- 4 files changed, 213 insertions(+), 758 deletions(-) diff --git a/src/client.rs b/src/client.rs index 64c7daf4d..ddb093b08 100644 --- a/src/client.rs +++ b/src/client.rs @@ -11,6 +11,7 @@ use cpal::{ traits::{DeviceTrait, HostTrait, StreamTrait}, Device, Host, StreamConfig, }; +use enigo::{Enigo, KeyboardControllable}; use magnum_opus::{Channels::*, Decoder as AudioDecoder}; use sha2::{Digest, Sha256}; use uuid::Uuid; @@ -58,6 +59,18 @@ lazy_static::lazy_static! { static ref AUDIO_HOST: Host = cpal::default_host(); } +lazy_static::lazy_static! { + static ref ENIGO: Arc> = Arc::new(Mutex::new(Enigo::new())); +} + +pub fn get_key_state(key: enigo::Key) -> bool { + #[cfg(target_os = "macos")] + if key == enigo::Key::NumLock { + return true; + } + ENIGO.lock().unwrap().get_key_state(key) +} + cfg_if::cfg_if! { if #[cfg(target_os = "android")] { diff --git a/src/flutter.rs b/src/flutter.rs index 88c6f1961..514048e31 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -301,616 +301,6 @@ pub fn session_start_(id: &str, event_stream: StreamSink) -> ResultTy } } -// #[derive(Clone)] -// pub struct Session { -// id: String, -// sender: Arc>>>, // UI to rust -// lc: Arc>, -// events2ui: Arc>>>, -// } - -// impl Session1 { -// /// Create a new remote session with the given id. -// /// -// /// # Arguments -// /// -// /// * `id` - The identifier of the remote session with prefix. Regex: [\w]*[\_]*[\d]+ -// /// * `is_file_transfer` - If the session is used for file transfer. -// /// * `is_port_forward` - If the session is used for port forward. -// pub fn add(id: &str, is_file_transfer: bool, is_port_forward: bool) -> ResultType<()> { -// // TODO check same id -// let session_id = get_session_id(id.to_owned()); -// LocalConfig::set_remote_id(&session_id); -// // TODO close -// // Self::close(); -// let session = Session { -// id: session_id.clone(), -// sender: Default::default(), -// lc: Default::default(), -// events2ui: Arc::new(RwLock::new(None)), -// }; -// session.lc.write().unwrap().initialize( -// session_id.clone(), -// is_file_transfer, -// is_port_forward, -// ); -// SESSIONS -// .write() -// .unwrap() -// .insert(id.to_owned(), session.clone()); -// Ok(()) -// } - -// /// Create a new remote session with the given id. -// /// -// /// # Arguments -// /// -// /// * `id` - The identifier of the remote session with prefix. Regex: [\w]*[\_]*[\d]+ -// /// * `events2ui` - The events channel to ui. -// pub fn start(id: &str, events2ui: StreamSink) -> ResultType<()> { -// if let Some(session) = SESSIONS.write().unwrap().get_mut(id) { -// *session.events2ui.write().unwrap() = Some(events2ui); -// let session = session.clone(); -// std::thread::spawn(move || { -// let is_file_transfer = session.lc.read().unwrap().is_file_transfer; -// let is_port_forward = session.lc.read().unwrap().is_port_forward; -// Connection::start(session, is_file_transfer, is_port_forward); -// }); -// Ok(()) -// } else { -// bail!("No session with peer id {}", id) -// } -// } - -// /// Get the option of the current session. -// /// -// /// # Arguments -// /// -// /// * `name` - The name of the option to get. Currently only `remote_dir` is supported. -// pub fn get_option(&self, name: &str) -> String { -// if name == "remote_dir" { -// return self.lc.read().unwrap().get_remote_dir(); -// } -// self.lc.read().unwrap().get_option(name) -// } - -// /// Set the option of the current session. -// /// -// /// # Arguments -// /// -// /// * `name` - The name of the option to set. Currently only `remote_dir` is supported. -// /// * `value` - The value of the option to set. -// pub fn set_option(&self, name: String, value: String) { -// let mut value = value; -// let mut lc = self.lc.write().unwrap(); -// if name == "remote_dir" { -// value = lc.get_all_remote_dir(value); -// } -// lc.set_option(name, value); -// } - -// /// Input the OS password. -// pub fn input_os_password(&self, pass: String, activate: bool) { -// input_os_password(pass, activate, self.clone()); -// } - -// pub fn restart_remote_device(&self) { -// let mut lc = self.lc.write().unwrap(); -// lc.restarting_remote_device = true; -// let msg = lc.restart_remote_device(); -// self.send_msg(msg); -// } - -// /// Toggle an option. -// pub fn toggle_option(&self, name: &str) { -// let msg = self.lc.write().unwrap().toggle_option(name.to_owned()); -// if let Some(msg) = msg { -// self.send_msg(msg); -// } -// } - -// /// Send a refresh command. -// pub fn refresh(&self) { -// self.send(Data::Message(LoginConfigHandler::refresh())); -// } - -// /// Get image quality. -// pub fn get_image_quality(&self) -> String { -// self.lc.read().unwrap().image_quality.clone() -// } - -// /// Set image quality. -// pub fn set_image_quality(&self, value: &str) { -// let msg = self -// .lc -// .write() -// .unwrap() -// .save_image_quality(value.to_owned()); -// if let Some(msg) = msg { -// self.send_msg(msg); -// } -// } - -// /// Get the status of a toggle option. -// /// Return `None` if the option is not found. -// /// -// /// # Arguments -// /// -// /// * `name` - The name of the option to get. -// pub fn get_toggle_option(&self, name: &str) -> bool { -// self.lc.write().unwrap().get_toggle_option(name) -// } - -// /// Login. -// /// -// /// # Arguments -// /// -// /// * `password` - The password to login. -// /// * `remember` - If the password should be remembered. -// pub fn login(&self, password: &str, remember: bool) { -// self.send(Data::Login((password.to_owned(), remember))); -// } - -// /// Close the session. -// pub fn close(&self) { -// self.send(Data::Close); -// } - -// /// Reconnect to the current session. -// pub fn reconnect(&self) { -// self.send(Data::Close); -// let session = self.clone(); -// std::thread::spawn(move || { -// Connection::start(session, false, false); -// }); -// } - -// /// Get `remember` flag in [`LoginConfigHandler`]. -// pub fn get_remember(&self) -> bool { -// self.lc.read().unwrap().remember -// } - -// /// Send message over the current session. -// /// -// /// # Arguments -// /// -// /// * `msg` - The message to send. -// #[inline] -// pub fn send_msg(&self, msg: Message) { -// self.send(Data::Message(msg)); -// } - -// /// Send chat message over the current session. -// /// -// /// # Arguments -// /// -// /// * `text` - The message to send. -// pub fn send_chat(&self, text: String) { -// let mut misc = Misc::new(); -// misc.set_chat_message(ChatMessage { -// text, -// ..Default::default() -// }); -// let mut msg_out = Message::new(); -// msg_out.set_misc(misc); -// self.send_msg(msg_out); -// } - -// /// Push an event to the event queue. -// /// An event is stored as json in the event queue. -// /// -// /// # Arguments -// /// -// /// * `name` - The name of the event. -// /// * `event` - Fields of the event content. -// fn push_event(&self, name: &str, event: Vec<(&str, &str)>) { -// let mut h: HashMap<&str, &str> = event.iter().cloned().collect(); -// assert!(h.get("name").is_none()); -// h.insert("name", name); -// let out = serde_json::ser::to_string(&h).unwrap_or("".to_owned()); -// if let Some(stream) = &*self.events2ui.read().unwrap() { -// stream.add(EventToUI::Event(out)); -// } -// } - -// /// Get platform of peer. -// #[inline] -// fn peer_platform(&self) -> String { -// self.lc.read().unwrap().info.platform.clone() -// } - -// /// Quick method for sending a ctrl_alt_del command. -// pub fn ctrl_alt_del(&self) { -// if self.peer_platform() == "Windows" { -// let k = Key::ControlKey(ControlKey::CtrlAltDel); -// self.key_down_or_up(1, k, false, false, false, false); -// } else { -// let k = Key::ControlKey(ControlKey::Delete); -// self.key_down_or_up(3, k, true, true, false, false); -// } -// } - -// /// Switch the display. -// /// -// /// # Arguments -// /// -// /// * `display` - The display to switch to. -// pub fn switch_display(&self, display: i32) { -// let mut misc = Misc::new(); -// misc.set_switch_display(SwitchDisplay { -// display, -// ..Default::default() -// }); -// let mut msg_out = Message::new(); -// msg_out.set_misc(misc); -// self.send_msg(msg_out); -// } - -// /// Send lock screen command. -// pub fn lock_screen(&self) { -// let k = Key::ControlKey(ControlKey::LockScreen); -// self.key_down_or_up(1, k, false, false, false, false); -// } - -// /// Send key input command. -// /// -// /// # Arguments -// /// -// /// * `name` - The name of the key. -// /// * `down` - Whether the key is down or up. -// /// * `press` - If the key is simply being pressed(Down+Up). -// /// * `alt` - If the alt key is also pressed. -// /// * `ctrl` - If the ctrl key is also pressed. -// /// * `shift` - If the shift key is also pressed. -// /// * `command` - If the command key is also pressed. -// pub fn input_key( -// &self, -// name: &str, -// down: bool, -// press: bool, -// alt: bool, -// ctrl: bool, -// shift: bool, -// command: bool, -// ) { -// let chars: Vec = name.chars().collect(); -// if chars.len() == 1 { -// let key = Key::_Raw(chars[0] as _); -// self._input_key(key, down, press, alt, ctrl, shift, command); -// } else { -// if let Some(key) = KEY_MAP.get(name) { -// self._input_key(key.clone(), down, press, alt, ctrl, shift, command); -// } -// } -// } - -// /// Input a string of text. -// /// String is parsed into individual key presses. -// /// -// /// # Arguments -// /// -// /// * `value` - The text to input. TODO &str -> String -// pub fn input_string(&self, value: &str) { -// let mut key_event = KeyEvent::new(); -// key_event.set_seq(value.to_owned()); -// let mut msg_out = Message::new(); -// msg_out.set_key_event(key_event); -// self.send_msg(msg_out); -// } - -// fn _input_key( -// &self, -// key: Key, -// down: bool, -// press: bool, -// alt: bool, -// ctrl: bool, -// shift: bool, -// command: bool, -// ) { -// let v = if press { -// 3 -// } else if down { -// 1 -// } else { -// 0 -// }; -// self.key_down_or_up(v, key, alt, ctrl, shift, command); -// } - -// pub fn send_mouse( -// &self, -// mask: i32, -// x: i32, -// y: i32, -// alt: bool, -// ctrl: bool, -// shift: bool, -// command: bool, -// ) { -// send_mouse(mask, x, y, alt, ctrl, shift, command, self); -// } - -// fn key_down_or_up( -// &self, -// down_or_up: i32, -// key: Key, -// alt: bool, -// ctrl: bool, -// shift: bool, -// command: bool, -// ) { -// let mut down_or_up = down_or_up; -// let mut key_event = KeyEvent::new(); -// match key { -// Key::Chr(chr) => { -// key_event.set_chr(chr); -// } -// Key::ControlKey(key) => { -// key_event.set_control_key(key.clone()); -// } -// Key::_Raw(raw) => { -// if raw > 'z' as u32 || raw < 'a' as u32 { -// key_event.set_unicode(raw); -// if down_or_up == 0 { -// // ignore up, avoiding trigger twice -// return; -// } -// down_or_up = 1; // if press, turn into down for avoiding trigger twice on server side -// } else { -// // to make ctrl+c works on windows -// key_event.set_chr(raw); -// } -// } -// } -// if alt { -// key_event.modifiers.push(ControlKey::Alt.into()); -// } -// if shift { -// key_event.modifiers.push(ControlKey::Shift.into()); -// } -// if ctrl { -// key_event.modifiers.push(ControlKey::Control.into()); -// } -// if command { -// key_event.modifiers.push(ControlKey::Meta.into()); -// } -// if down_or_up == 1 { -// key_event.down = true; -// } else if down_or_up == 3 { -// key_event.press = true; -// } -// let mut msg_out = Message::new(); -// msg_out.set_key_event(key_event); -// log::debug!("{:?}", msg_out); -// self.send_msg(msg_out); -// } - -// pub fn load_config(&self) -> PeerConfig { -// load_config(&self.id) -// } - -// pub fn save_config(&self, config: &PeerConfig) { -// config.store(&self.id); -// } - -// pub fn get_platform(&self, is_remote: bool) -> String { -// if is_remote { -// self.lc.read().unwrap().info.platform.clone() -// } else { -// whoami::platform().to_string() -// } -// } - -// pub fn load_last_jobs(&self) { -// let pc = self.load_config(); -// if pc.transfer.write_jobs.is_empty() && pc.transfer.read_jobs.is_empty() { -// // no last jobs -// return; -// } -// let mut cnt = 1; -// for job_str in pc.transfer.read_jobs.iter() { -// if !job_str.is_empty() { -// self.push_event("load_last_job", vec![("value", job_str)]); -// cnt += 1; -// println!("restore read_job: {:?}", job_str); -// } -// } -// for job_str in pc.transfer.write_jobs.iter() { -// if !job_str.is_empty() { -// self.push_event("load_last_job", vec![("value", job_str)]); -// cnt += 1; -// println!("restore write_job: {:?}", job_str); -// } -// } -// } - -// fn update_quality_status(&self, status: QualityStatus) { -// const NULL: String = String::new(); -// self.push_event( -// "update_quality_status", -// vec![ -// ("speed", &status.speed.map_or(NULL, |it| it)), -// ("fps", &status.fps.map_or(NULL, |it| it.to_string())), -// ("delay", &status.delay.map_or(NULL, |it| it.to_string())), -// ( -// "target_bitrate", -// &status.target_bitrate.map_or(NULL, |it| it.to_string()), -// ), -// ( -// "codec_format", -// &status.codec_format.map_or(NULL, |it| it.to_string()), -// ), -// ], -// ); -// } - -// pub fn remove_port_forward(&mut self, port: i32) { -// let mut config = self.load_config(); -// config.port_forwards = config -// .port_forwards -// .drain(..) -// .filter(|x| x.0 != port) -// .collect(); -// self.save_config(&config); -// self.send(Data::RemovePortForward(port)); -// } - -// pub fn add_port_forward(&mut self, port: i32, remote_host: String, remote_port: i32) { -// let mut config = self.load_config(); -// if config -// .port_forwards -// .iter() -// .filter(|x| x.0 == port) -// .next() -// .is_some() -// { -// return; -// } -// let pf = (port, remote_host, remote_port); -// config.port_forwards.push(pf.clone()); -// self.save_config(&config); -// self.send(Data::AddPortForward(pf)); -// } - -// fn on_error(&self, err: &str) { -// self.msgbox("error", "Error", err); -// } -// } - -// impl FileManager for Session {} - -// #[async_trait] -// impl Interface for Session { -// fn send(&self, data: Data) { -// if let Some(sender) = self.sender.read().unwrap().as_ref() { -// sender.send(data).ok(); -// } -// } - -// fn is_file_transfer(&self) -> bool { -// todo!() -// } - -// fn is_port_forward(&self) -> bool { -// todo!() -// } - -// fn is_rdp(&self) -> bool { -// todo!() -// } - -// fn msgbox(&self, msgtype: &str, title: &str, text: &str) { -// let has_retry = if check_if_retry(msgtype, title, text) { -// "true" -// } else { -// "" -// }; -// self.push_event( -// "msgbox", -// vec![ -// ("type", msgtype), -// ("title", title), -// ("text", text), -// ("hasRetry", has_retry), -// ], -// ); -// } - -// fn handle_login_error(&mut self, err: &str) -> bool { -// self.lc.write().unwrap().handle_login_error(err, self) -// } - -// fn handle_peer_info(&mut self, pi: PeerInfo) { -// let mut lc = self.lc.write().unwrap(); -// let username = lc.get_username(&pi); -// let mut displays = Vec::new(); -// let mut current = pi.current_display as usize; - -// if lc.is_file_transfer { -// if pi.username.is_empty() { -// self.msgbox( -// "error", -// "Error", -// "No active console user logged on, please connect and logon first.", -// ); -// return; -// } -// } else { -// if pi.displays.is_empty() { -// self.msgbox("error", "Remote Error", "No Display"); -// } -// 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); -// displays.push(h); -// } -// if current >= pi.displays.len() { -// current = 0; -// } -// } -// let displays = serde_json::ser::to_string(&displays).unwrap_or("".to_owned()); -// self.push_event( -// "peer_info", -// vec![ -// ("username", &username), -// ("hostname", &pi.hostname), -// ("platform", &pi.platform), -// ("sas_enabled", &pi.sas_enabled.to_string()), -// ("displays", &displays), -// ("version", &pi.version), -// ("current_display", ¤t.to_string()), -// ("is_file_transfer", &lc.is_file_transfer.to_string()), -// ], -// ); -// lc.handle_peer_info(username, pi); -// let p = lc.should_auto_login(); -// if !p.is_empty() { -// input_os_password(p, true, self.clone()); -// } -// } - -// fn set_force_relay(&mut self, direct: bool, received: bool) { -// let mut lc = self.lc.write().unwrap(); -// lc.force_relay = false; -// if direct && !received { -// let errno = errno::errno().0; -// log::info!("errno is {}", errno); -// // TODO: check mac and ios -// if cfg!(windows) && errno == 10054 || !cfg!(windows) && errno == 104 { -// lc.force_relay = true; -// lc.set_option("force-always-relay".to_owned(), "Y".to_owned()); -// } -// } -// } - -// fn is_force_relay(&self) -> bool { -// self.lc.read().unwrap().force_relay -// } - -// async fn handle_hash(&mut self, pass: &str, hash: Hash, peer: &mut Stream) { -// handle_hash(self.lc.clone(), pass, hash, self, peer).await; -// } - -// async fn handle_login_from_ui(&mut self, password: String, remember: bool, peer: &mut Stream) { -// handle_login_from_ui(self.lc.clone(), password, remember, peer).await; -// } - -// async fn handle_test_delay(&mut self, t: TestDelay, peer: &mut Stream) { -// if !t.from_client { -// self.update_quality_status(QualityStatus { -// delay: Some(t.last_delay as _), -// target_bitrate: Some(t.target_bitrate as _), -// ..Default::default() -// }); -// handle_test_delay(t, peer).await; -// } -// } -// } - // struct Connection { // video_handler: VideoHandler, // audio_handler: AudioHandler, diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 49812e09a..9e8c8fc51 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -55,18 +55,9 @@ use errno; type Video = AssetPtr; lazy_static::lazy_static! { - static ref ENIGO: Arc> = Arc::new(Mutex::new(Enigo::new())); static ref VIDEO: Arc>> = Default::default(); } -fn get_key_state(key: enigo::Key) -> bool { - #[cfg(target_os = "macos")] - if key == enigo::Key::NumLock { - return true; - } - ENIGO.lock().unwrap().get_key_state(key) -} - static IS_IN: AtomicBool = AtomicBool::new(false); static KEYBOARD_HOOKED: AtomicBool = AtomicBool::new(false); @@ -1111,38 +1102,7 @@ impl SciterSession { IS_IN.store(false, Ordering::SeqCst); } - fn send_mouse( - &mut self, - mask: i32, - x: i32, - y: i32, - alt: bool, - ctrl: bool, - shift: bool, - command: bool, - ) { - #[allow(unused_mut)] - let mut command = command; - #[cfg(windows)] - { - if !command && crate::platform::windows::get_win_key_state() { - command = true; - } - } - - send_mouse(mask, x, y, alt, ctrl, shift, command, &self.0); - // on macos, ctrl + left button down = right button down, up won't emit, so we need to - // emit up myself if peer is not macos - // to-do: how about ctrl + left from win to macos - if cfg!(target_os = "macos") { - let buttons = mask >> 3; - let evt_type = mask & 0x7; - if buttons == 1 && evt_type == 1 && ctrl && self.peer_platform() != "Mac OS" { - self.send_mouse((1 << 3 | 2) as _, x, y, alt, ctrl, shift, command); - } - } - } - + // TODO fn set_cursor_data(&mut self, cd: CursorData) { let mut colors = hbb_common::compress::decompress(&cd.colors); if colors.iter().filter(|x| **x != 0).next().is_none() { @@ -1285,24 +1245,6 @@ impl SciterSession { "".to_owned() } - fn ctrl_alt_del(&mut self) { - if self.peer_platform() == "Windows" { - let mut key_event = KeyEvent::new(); - key_event.set_control_key(ControlKey::CtrlAltDel); - self.key_down_or_up(1, key_event, false, false, false, false); - } else { - let mut key_event = KeyEvent::new(); - key_event.set_control_key(ControlKey::Delete); - self.key_down_or_up(3, key_event, true, true, false, false); - } - } - - fn lock_screen(&mut self) { - let mut key_event = KeyEvent::new(); - key_event.set_control_key(ControlKey::LockScreen); - self.key_down_or_up(1, key_event, false, false, false, false); - } - fn transfer_file(&mut self) { let id = self.get_id(); let args = vec!["--file-transfer", &id, &self.password]; @@ -1319,60 +1261,6 @@ impl SciterSession { } } - fn key_down_or_up( - &mut self, - down_or_up: i32, - evt: KeyEvent, - alt: bool, - ctrl: bool, - shift: bool, - command: bool, - ) { - let mut key_event = evt; - - if alt - && !crate::is_control_key(&key_event, &ControlKey::Alt) - && !crate::is_control_key(&key_event, &ControlKey::RAlt) - { - key_event.modifiers.push(ControlKey::Alt.into()); - } - if shift - && !crate::is_control_key(&key_event, &ControlKey::Shift) - && !crate::is_control_key(&key_event, &ControlKey::RShift) - { - key_event.modifiers.push(ControlKey::Shift.into()); - } - if ctrl - && !crate::is_control_key(&key_event, &ControlKey::Control) - && !crate::is_control_key(&key_event, &ControlKey::RControl) - { - key_event.modifiers.push(ControlKey::Control.into()); - } - if command - && !crate::is_control_key(&key_event, &ControlKey::Meta) - && !crate::is_control_key(&key_event, &ControlKey::RWin) - { - key_event.modifiers.push(ControlKey::Meta.into()); - } - if get_key_state(enigo::Key::CapsLock) { - key_event.modifiers.push(ControlKey::CapsLock.into()); - } - if self.peer_platform() != "Mac OS" { - if get_key_state(enigo::Key::NumLock) && common::valid_for_numlock(&key_event) { - key_event.modifiers.push(ControlKey::NumLock.into()); - } - } - if down_or_up == 1 { - key_event.down = true; - } else if down_or_up == 3 { - key_event.press = true; - } - let mut msg_out = Message::new(); - msg_out.set_key_event(key_event); - log::debug!("{:?}", msg_out); - self.send(Data::Message(msg_out)); - } - // #[inline] // fn set_cursor_id(&mut self, id: String) { // self.call("setCursorId", &make_args!(id)); diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index a8871fac1..03666ed92 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -1,7 +1,7 @@ use crate::client::{ - self, check_if_retry, handle_hash, handle_login_from_ui, handle_test_delay, input_os_password, - load_config, start_video_audio_threads, Client, CodecFormat, FileManager, LoginConfigHandler, - MediaData, MediaSender, QualityStatus, SEC30, + self, check_if_retry, get_key_state, handle_hash, handle_login_from_ui, handle_test_delay, + input_os_password, load_config, send_mouse, start_video_audio_threads, Client, CodecFormat, + FileManager, Key, LoginConfigHandler, MediaData, MediaSender, QualityStatus, KEY_MAP, SEC30, }; use crate::common::{ self, check_clipboard, update_clipboard, ClipboardContext, CLIPBOARD_INTERVAL, @@ -9,6 +9,7 @@ use crate::common::{ use crate::platform; use crate::{client::Data, client::Interface}; use async_trait::async_trait; +use enigo::{Enigo, KeyboardControllable}; use hbb_common::config::{Config, LocalConfig, PeerConfig, TransferSerde}; use hbb_common::fs::{ can_enable_overwrite_detection, get_job, get_string, new_send_confirm, DigestCheckResult, @@ -43,11 +44,11 @@ pub struct Session { impl Session { pub fn get_view_style(&self) -> String { - return self.lc.read().unwrap().view_style.clone(); + self.lc.read().unwrap().view_style.clone() } pub fn get_image_quality(&self) -> String { - return self.lc.read().unwrap().image_quality.clone(); + self.lc.read().unwrap().image_quality.clone() } pub fn save_view_style(&mut self, value: String) { @@ -65,8 +66,7 @@ impl Session { } pub fn get_toggle_option(&self, name: String) -> bool { - let res = self.lc.read().unwrap().get_toggle_option(&name); - return res; + self.lc.read().unwrap().get_toggle_option(&name) } pub fn is_privacy_mode_supported(&self) -> bool { @@ -196,12 +196,18 @@ impl Session { } pub fn get_option(&self, k: String) -> String { - let res = self.lc.read().unwrap().get_option(&k); - return res; + if k.eq("remote_dir") { + return self.lc.read().unwrap().get_remote_dir(); + } + self.lc.read().unwrap().get_option(&k) } - pub fn set_option(&self, k: String, v: String) { - self.lc.write().unwrap().set_option(k.clone(), v); + pub fn set_option(&self, k: String, mut v: String) { + let mut lc = self.lc.write().unwrap(); + if k.eq("remote_dir") { + v = lc.get_all_remote_dir(v); + } + lc.set_option(k, v); } #[inline] @@ -223,6 +229,72 @@ impl Session { self.lc.read().unwrap().info.platform.clone() } + pub fn ctrl_alt_del(&mut self) { + if self.peer_platform() == "Windows" { + let mut key_event = KeyEvent::new(); + key_event.set_control_key(ControlKey::CtrlAltDel); + self.key_down_or_up(1, key_event, false, false, false, false); + } else { + let mut key_event = KeyEvent::new(); + key_event.set_control_key(ControlKey::Delete); + self.key_down_or_up(3, key_event, true, true, false, false); + } + } + + pub fn key_down_or_up( + &self, + down_or_up: i32, + evt: KeyEvent, + alt: bool, + ctrl: bool, + shift: bool, + command: bool, + ) { + let mut key_event = evt; + + if alt + && !crate::is_control_key(&key_event, &ControlKey::Alt) + && !crate::is_control_key(&key_event, &ControlKey::RAlt) + { + key_event.modifiers.push(ControlKey::Alt.into()); + } + if shift + && !crate::is_control_key(&key_event, &ControlKey::Shift) + && !crate::is_control_key(&key_event, &ControlKey::RShift) + { + key_event.modifiers.push(ControlKey::Shift.into()); + } + if ctrl + && !crate::is_control_key(&key_event, &ControlKey::Control) + && !crate::is_control_key(&key_event, &ControlKey::RControl) + { + key_event.modifiers.push(ControlKey::Control.into()); + } + if command + && !crate::is_control_key(&key_event, &ControlKey::Meta) + && !crate::is_control_key(&key_event, &ControlKey::RWin) + { + key_event.modifiers.push(ControlKey::Meta.into()); + } + if get_key_state(enigo::Key::CapsLock) { + key_event.modifiers.push(ControlKey::CapsLock.into()); + } + if self.peer_platform() != "Mac OS" { + if get_key_state(enigo::Key::NumLock) && common::valid_for_numlock(&key_event) { + key_event.modifiers.push(ControlKey::NumLock.into()); + } + } + if down_or_up == 1 { + key_event.down = true; + } else if down_or_up == 3 { + key_event.press = true; + } + let mut msg_out = Message::new(); + msg_out.set_key_event(key_event); + log::debug!("{:?}", msg_out); + self.send(Data::Message(msg_out)); + } + pub fn get_platform(&self, is_remote: bool) -> String { if is_remote { self.peer_platform() @@ -277,8 +349,122 @@ impl Session { self.send(Data::Message(msg_out)); } + pub fn lock_screen(&mut self) { + let mut key_event = KeyEvent::new(); + key_event.set_control_key(ControlKey::LockScreen); + self.key_down_or_up(1, key_event, false, false, false, false); + } + + // flutter only TODO new input + pub fn input_key( + &self, + name: &str, + down: bool, + press: bool, + alt: bool, + ctrl: bool, + shift: bool, + command: bool, + ) { + let chars: Vec = name.chars().collect(); + if chars.len() == 1 { + let key = Key::_Raw(chars[0] as _); + self._input_key(key, down, press, alt, ctrl, shift, command); + } else { + if let Some(key) = KEY_MAP.get(name) { + self._input_key(key.clone(), down, press, alt, ctrl, shift, command); + } + } + } + + // flutter only TODO new input + pub fn input_string(&self, value: &str) { + let mut key_event = KeyEvent::new(); + key_event.set_seq(value.to_owned()); + let mut msg_out = Message::new(); + msg_out.set_key_event(key_event); + self.send(Data::Message(msg_out)); + } + + // flutter only TODO new input + fn _input_key( + &self, + key: Key, + down: bool, + press: bool, + alt: bool, + ctrl: bool, + shift: bool, + command: bool, + ) { + let v = if press { + 3 + } else if down { + 1 + } else { + 0 + }; + let mut key_event = KeyEvent::new(); + match key { + Key::Chr(chr) => { + key_event.set_chr(chr); + } + Key::ControlKey(key) => { + key_event.set_control_key(key.clone()); + } + Key::_Raw(raw) => { + if raw > 'z' as u32 || raw < 'a' as u32 { + key_event.set_unicode(raw); + // TODO + // if down_or_up == 0 { + // // ignore up, avoiding trigger twice + // return; + // } + // down_or_up = 1; // if press, turn into down for avoiding trigger twice on server side + } else { + // to make ctrl+c works on windows + key_event.set_chr(raw); + } + } + } + + self.key_down_or_up(v, key_event, alt, ctrl, shift, command); + } + + pub fn send_mouse( + &mut self, + mask: i32, + x: i32, + y: i32, + alt: bool, + ctrl: bool, + shift: bool, + command: bool, + ) { + #[allow(unused_mut)] + let mut command = command; + #[cfg(windows)] + { + if !command && crate::platform::windows::get_win_key_state() { + command = true; + } + } + + send_mouse(mask, x, y, alt, ctrl, shift, command, self); + // on macos, ctrl + left button down = right button down, up won't emit, so we need to + // emit up myself if peer is not macos + // to-do: how about ctrl + left from win to macos + if cfg!(target_os = "macos") { + let buttons = mask >> 3; + let evt_type = mask & 0x7; + if buttons == 1 && evt_type == 1 && ctrl && self.peer_platform() != "Mac OS" { + self.send_mouse((1 << 3 | 2) as _, x, y, alt, ctrl, shift, command); + } + } + } + pub fn reconnect(&self) { - println!("reconnecting"); + self.send(Data::Close); let cloned = self.clone(); let mut lock = self.thread.lock().unwrap(); lock.take().map(|t| t.join()); @@ -979,7 +1165,7 @@ impl Remote { Some(tx) } - async fn load_last_jobs(&mut self) { + fn load_last_jobs(&mut self) { log::info!("start load last jobs"); // self.handler.call("clearAllJobs", &make_args!()); self.handler.clear_all_jobs(); @@ -993,17 +1179,6 @@ impl Remote { for job_str in pc.transfer.read_jobs.iter() { let job: Result = serde_json::from_str(&job_str); if let Ok(job) = job { - // self.handler.call( - // "addJob", - // &make_args!( - // cnt, - // job.to.clone(), - // job.remote.clone(), - // job.file_num, - // job.show_hidden, - // false - // ), - // ); self.handler.add_job( cnt, job.to.clone(), @@ -1019,17 +1194,6 @@ impl Remote { for job_str in pc.transfer.write_jobs.iter() { let job: Result = serde_json::from_str(&job_str); if let Ok(job) = job { - // self.handler.call( - // "addJob", - // &make_args!( - // cnt, - // job.remote.clone(), - // job.to.clone(), - // job.file_num, - // job.show_hidden, - // true - // ), - // ); self.handler.add_job( cnt, job.remote.clone(), From 41a53e4983fe681442e3d8aee2cdfec91c91f3ec Mon Sep 17 00:00:00 2001 From: csf Date: Thu, 1 Sep 2022 09:48:53 +0800 Subject: [PATCH 0336/2015] refactor io_loop --- src/client.rs | 7 +- src/client/io_loop.rs | 1205 +++++++++++++++++++++++++++++++++ src/flutter.rs | 1176 ++------------------------------ src/ui/remote.rs | 435 +----------- src/ui_session_interface.rs | 1258 +---------------------------------- 5 files changed, 1326 insertions(+), 2755 deletions(-) create mode 100644 src/client/io_loop.rs diff --git a/src/client.rs b/src/client.rs index ddb093b08..6346af6d0 100644 --- a/src/client.rs +++ b/src/client.rs @@ -2,7 +2,7 @@ use std::{ collections::HashMap, net::SocketAddr, ops::{Deref, Not}, - sync::{mpsc, Arc, Mutex, RwLock}, + sync::{mpsc, Arc, Mutex, RwLock, atomic::AtomicBool}, }; pub use async_trait::async_trait; @@ -48,7 +48,12 @@ 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. diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs new file mode 100644 index 000000000..f7f8f4f18 --- /dev/null +++ b/src/client/io_loop.rs @@ -0,0 +1,1205 @@ +use crate::client::{ + Client, CodecFormat, FileManager, MediaData, MediaSender, QualityStatus, MILLI1, SEC30, + SERVER_CLIPBOARD_ENABLED, SERVER_FILE_TRANSFER_ENABLED, SERVER_KEYBOARD_ENABLED, +}; +use crate::common::{check_clipboard, update_clipboard, ClipboardContext, CLIPBOARD_INTERVAL}; + +use crate::ui_session_interface::{InvokeUi, Session}; +use crate::{client::Data, client::Interface}; + +use hbb_common::config::{PeerConfig, TransferSerde}; +use hbb_common::fs::{ + can_enable_overwrite_detection, get_job, get_string, new_send_confirm, DigestCheckResult, + RemoveJobMeta, TransferJobMeta, +}; +use hbb_common::message_proto::permission_info::Permission; +use hbb_common::protobuf::Message as _; +use hbb_common::rendezvous_proto::ConnType; +use hbb_common::tokio::{ + self, + sync::mpsc, + time::{self, Duration, Instant, Interval}, +}; +use hbb_common::{allow_err, message_proto::*}; +use hbb_common::{fs, log, Stream}; +use std::collections::HashMap; + +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::{Arc, Mutex}; + +pub struct Remote { + handler: Session, + video_sender: MediaSender, + audio_sender: MediaSender, + receiver: mpsc::UnboundedReceiver, + sender: mpsc::UnboundedSender, + old_clipboard: Arc>, + read_jobs: Vec, + write_jobs: Vec, + remove_jobs: HashMap, + timer: Interval, + last_update_jobs_status: (Instant, HashMap), + first_frame: bool, + #[cfg(windows)] + clipboard_file_context: Option>, + data_count: Arc, + frame_count: Arc, + video_format: CodecFormat, +} + +impl Remote { + pub fn new( + handler: Session, + video_sender: MediaSender, + audio_sender: MediaSender, + receiver: mpsc::UnboundedReceiver, + sender: mpsc::UnboundedSender, + frame_count: Arc, + ) -> Self { + Self { + handler, + video_sender, + audio_sender, + receiver, + sender, + old_clipboard: Default::default(), + read_jobs: Vec::new(), + write_jobs: Vec::new(), + remove_jobs: Default::default(), + timer: time::interval(SEC30), + last_update_jobs_status: (Instant::now(), Default::default()), + first_frame: false, + #[cfg(windows)] + clipboard_file_context: None, + data_count: Arc::new(AtomicUsize::new(0)), + frame_count, + video_format: CodecFormat::Unknown, + } + } + + 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() { + ConnType::FILE_TRANSFER + } else { + ConnType::default() + }; + match Client::start( + &self.handler.id, + key, + token, + conn_type, + self.handler.clone(), + ) + .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 + + // just build for now + #[cfg(not(windows))] + let (_tx_holder, mut rx_clip_client) = mpsc::unbounded_channel::(); + #[cfg(windows)] + let mut rx_clip_client = get_rx_clip_client().lock().await; + + let mut status_timer = time::interval(Duration::new(1, 0)); + + loop { + tokio::select! { + res = peer.next() => { + if let Some(res) = res { + match res { + Err(err) => { + log::error!("Connection closed: {}", err); + self.handler.set_force_relay(direct, received); + self.handler.msgbox("error", "Connection Error", &err.to_string()); + break; + } + Ok(ref bytes) => { + last_recv_time = Instant::now(); + received = true; + self.data_count.fetch_add(bytes.len(), Ordering::Relaxed); + if !self.handle_msg_from_peer(bytes, &mut peer).await { + break + } + } + } + } else { + if self.handler.is_restarting_remote_device() { + log::info!("Restart remote device"); + self.handler.msgbox("restarting", "Restarting Remote Device", "remote_restarting_tip"); + } else { + log::info!("Reset by the peer"); + self.handler.msgbox("error", "Connection Error", "Reset by the peer"); + } + break; + } + } + d = self.receiver.recv() => { + if let Some(d) = d { + if !self.handle_msg_from_ui(d, &mut peer).await { + break; + } + } + } + _msg = rx_clip_client.recv() => { + #[cfg(windows)] + match _msg { + Some((_, clip)) => { + allow_err!(peer.send(&clip_2_msg(clip)).await); + } + None => { + // unreachable!() + } + } + } + _ = self.timer.tick() => { + if last_recv_time.elapsed() >= SEC30 { + self.handler.msgbox("error", "Connection Error", "Timeout"); + break; + } + if !self.read_jobs.is_empty() { + if let Err(err) = fs::handle_read_jobs(&mut self.read_jobs, &mut peer).await { + self.handler.msgbox("error", "Connection Error", &err.to_string()); + break; + } + self.update_jobs_status(); + } else { + self.timer = time::interval_at(Instant::now() + SEC30, SEC30); + } + } + _ = status_timer.tick() => { + let speed = self.data_count.swap(0, Ordering::Relaxed); + let speed = format!("{:.2}kB/s", speed as f32 / 1024 as f32); + let fps = self.frame_count.swap(0, Ordering::Relaxed) as _; + self.handler.update_quality_status(QualityStatus { + speed:Some(speed), + fps:Some(fps), + ..Default::default() + }); + } + } + } + log::debug!("Exit io_loop of id={}", self.handler.id); + } + Err(err) => { + self.handler + .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); + } + + fn handle_job_status(&mut self, id: i32, file_num: i32, err: Option) { + if let Some(job) = self.remove_jobs.get_mut(&id) { + if job.no_confirm { + let file_num = (file_num + 1) as usize; + if file_num < job.files.len() { + let path = format!("{}{}{}", job.path, job.sep, job.files[file_num].name); + self.sender + .send(Data::RemoveFile((id, path, file_num as i32, job.is_remote))) + .ok(); + let elapsed = job.last_update_job_status.elapsed().as_millis() as i32; + if elapsed >= 1000 { + job.last_update_job_status = Instant::now(); + } else { + return; + } + } else { + self.remove_jobs.remove(&id); + } + } + } + if let Some(err) = err { + self.handler.job_error(id, err, file_num); + } else { + self.handler.job_done(id, file_num); + } + } + + 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(); + 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 + { + 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) + } + + fn load_last_jobs(&mut self) { + log::info!("start load last jobs"); + self.handler.clear_all_jobs(); + let pc = self.handler.load_config(); + if pc.transfer.write_jobs.is_empty() && pc.transfer.read_jobs.is_empty() { + // no last jobs + return; + } + // TODO: can add a confirm dialog + let mut cnt = 1; + for job_str in pc.transfer.read_jobs.iter() { + let job: Result = serde_json::from_str(&job_str); + if let Ok(job) = job { + self.handler.add_job( + cnt, + job.to.clone(), + job.remote.clone(), + job.file_num, + job.show_hidden, + false, + ); + cnt += 1; + println!("restore read_job: {:?}", job); + } + } + for job_str in pc.transfer.write_jobs.iter() { + let job: Result = serde_json::from_str(&job_str); + if let Ok(job) = job { + self.handler.add_job( + cnt, + job.remote.clone(), + job.to.clone(), + job.file_num, + job.show_hidden, + true, + ); + cnt += 1; + println!("restore write_job: {:?}", job); + } + } + self.handler.update_transfer_list(); + } + + async fn handle_msg_from_ui(&mut self, data: Data, peer: &mut Stream) -> bool { + match data { + Data::Close => { + let mut misc = Misc::new(); + misc.set_close_reason("".to_owned()); + let mut msg = Message::new(); + msg.set_misc(misc); + allow_err!(peer.send(&msg).await); + return false; + } + Data::Login((password, remember)) => { + self.handler + .handle_login_from_ui(password, remember, peer) + .await; + } + Data::ToggleClipboardFile => { + self.check_clipboard_file_context(); + } + Data::Message(msg) => { + allow_err!(peer.send(&msg).await); + } + Data::SendFiles((id, path, to, file_num, include_hidden, is_remote)) => { + log::info!("send files, is remote {}", is_remote); + let od = can_enable_overwrite_detection(self.handler.lc.read().unwrap().version); + if is_remote { + log::debug!("New job {}, write to {} from remote {}", id, to, path); + self.write_jobs.push(fs::TransferJob::new_write( + id, + path.clone(), + to, + file_num, + include_hidden, + is_remote, + Vec::new(), + od, + )); + allow_err!( + peer.send(&fs::new_send(id, path, file_num, include_hidden)) + .await + ); + } else { + match fs::TransferJob::new_read( + id, + to.clone(), + path.clone(), + file_num, + include_hidden, + is_remote, + od, + ) { + Err(err) => { + self.handle_job_status(id, -1, Some(err.to_string())); + } + Ok(job) => { + log::debug!( + "New job {}, read {} to remote {}, {} files", + id, + path, + to, + job.files().len() + ); + // let m = make_fd(job.id(), job.files(), true); + // self.handler.call("updateFolderFiles", &make_args!(m)); // TODO + #[cfg(not(windows))] + let files = job.files().clone(); + #[cfg(windows)] + let mut files = job.files().clone(); + #[cfg(windows)] + if self.handler.peer_platform() != "Windows" { + // peer is not windows, need transform \ to / + fs::transform_windows_path(&mut files); + } + self.read_jobs.push(job); + self.timer = time::interval(MILLI1); + allow_err!(peer.send(&fs::new_receive(id, to, file_num, files)).await); + } + } + } + } + Data::AddJob((id, path, to, file_num, include_hidden, is_remote)) => { + let od = can_enable_overwrite_detection(self.handler.lc.read().unwrap().version); + if is_remote { + log::debug!( + "new write waiting job {}, write to {} from remote {}", + id, + to, + path + ); + let mut job = fs::TransferJob::new_write( + id, + path.clone(), + to, + file_num, + include_hidden, + is_remote, + Vec::new(), + od, + ); + job.is_last_job = true; + self.write_jobs.push(job); + } else { + match fs::TransferJob::new_read( + id, + to.clone(), + path.clone(), + file_num, + include_hidden, + is_remote, + od, + ) { + Err(err) => { + self.handle_job_status(id, -1, Some(err.to_string())); + } + Ok(mut job) => { + log::debug!( + "new read waiting job {}, read {} to remote {}, {} files", + id, + path, + to, + job.files().len() + ); + // let m = make_fd(job.id(), job.files(), true); + // self.handler.call("updateFolderFiles", &make_args!(m)); + job.is_last_job = true; + self.read_jobs.push(job); + self.timer = time::interval(MILLI1); + } + } + } + } + Data::ResumeJob((id, is_remote)) => { + if is_remote { + if let Some(job) = get_job(id, &mut self.write_jobs) { + job.is_last_job = false; + allow_err!( + peer.send(&fs::new_send( + id, + job.remote.clone(), + job.file_num, + job.show_hidden + )) + .await + ); + } + } else { + if let Some(job) = get_job(id, &mut self.read_jobs) { + job.is_last_job = false; + allow_err!( + peer.send(&fs::new_receive( + id, + job.path.to_string_lossy().to_string(), + job.file_num, + job.files.clone() + )) + .await + ); + } + } + } + Data::SetNoConfirm(id) => { + if let Some(job) = self.remove_jobs.get_mut(&id) { + job.no_confirm = true; + } + } + Data::ConfirmDeleteFiles((id, file_num)) => { + if let Some(job) = self.remove_jobs.get_mut(&id) { + let i = file_num as usize; + if i < job.files.len() { + self.handler.ui_handler.confirm_delete_files( + id, + file_num, + job.files[i].name.clone(), + ); + self.handler.confirm_delete_files(id, file_num); + } + } + } + Data::SetConfirmOverrideFile((id, file_num, need_override, remember, is_upload)) => { + if is_upload { + if let Some(job) = fs::get_job(id, &mut self.read_jobs) { + if remember { + job.set_overwrite_strategy(Some(need_override)); + } + job.confirm(&FileTransferSendConfirmRequest { + id, + file_num, + union: if need_override { + Some(file_transfer_send_confirm_request::Union::OffsetBlk(0)) + } else { + Some(file_transfer_send_confirm_request::Union::Skip(true)) + }, + ..Default::default() + }); + } + } else { + if let Some(job) = fs::get_job(id, &mut self.write_jobs) { + if remember { + job.set_overwrite_strategy(Some(need_override)); + } + let mut msg = Message::new(); + let mut file_action = FileAction::new(); + file_action.set_send_confirm(FileTransferSendConfirmRequest { + id, + file_num, + union: if need_override { + Some(file_transfer_send_confirm_request::Union::OffsetBlk(0)) + } else { + Some(file_transfer_send_confirm_request::Union::Skip(true)) + }, + ..Default::default() + }); + msg.set_file_action(file_action); + allow_err!(peer.send(&msg).await); + } + } + } + Data::RemoveDirAll((id, path, is_remote, include_hidden)) => { + let sep = self.handler.get_path_sep(is_remote); + if is_remote { + let mut msg_out = Message::new(); + let mut file_action = FileAction::new(); + file_action.set_all_files(ReadAllFiles { + id, + path: path.clone(), + include_hidden, + ..Default::default() + }); + msg_out.set_file_action(file_action); + allow_err!(peer.send(&msg_out).await); + self.remove_jobs + .insert(id, RemoveJob::new(Vec::new(), path, sep, is_remote)); + } else { + match fs::get_recursive_files(&path, include_hidden) { + Ok(entries) => { + // let m = make_fd(id, &entries, true); + // self.handler.call("updateFolderFiles", &make_args!(m)); + self.remove_jobs + .insert(id, RemoveJob::new(entries, path, sep, is_remote)); + } + Err(err) => { + self.handle_job_status(id, -1, Some(err.to_string())); + } + } + } + } + Data::CancelJob(id) => { + let mut msg_out = Message::new(); + let mut file_action = FileAction::new(); + file_action.set_cancel(FileTransferCancel { + id: id, + ..Default::default() + }); + msg_out.set_file_action(file_action); + allow_err!(peer.send(&msg_out).await); + if let Some(job) = fs::get_job(id, &mut self.write_jobs) { + job.remove_download_file(); + fs::remove_job(id, &mut self.write_jobs); + } + fs::remove_job(id, &mut self.read_jobs); + self.remove_jobs.remove(&id); + } + Data::RemoveDir((id, path)) => { + let mut msg_out = Message::new(); + let mut file_action = FileAction::new(); + file_action.set_remove_dir(FileRemoveDir { + id, + path, + recursive: true, + ..Default::default() + }); + msg_out.set_file_action(file_action); + allow_err!(peer.send(&msg_out).await); + } + Data::RemoveFile((id, path, file_num, is_remote)) => { + if is_remote { + let mut msg_out = Message::new(); + let mut file_action = FileAction::new(); + file_action.set_remove_file(FileRemoveFile { + id, + path, + file_num, + ..Default::default() + }); + msg_out.set_file_action(file_action); + allow_err!(peer.send(&msg_out).await); + } else { + match fs::remove_file(&path) { + Err(err) => { + self.handle_job_status(id, file_num, Some(err.to_string())); + } + Ok(()) => { + self.handle_job_status(id, file_num, None); + } + } + } + } + Data::CreateDir((id, path, is_remote)) => { + if is_remote { + let mut msg_out = Message::new(); + let mut file_action = FileAction::new(); + file_action.set_create(FileDirCreate { + id, + path, + ..Default::default() + }); + msg_out.set_file_action(file_action); + allow_err!(peer.send(&msg_out).await); + } else { + match fs::create_dir(&path) { + Err(err) => { + self.handle_job_status(id, -1, Some(err.to_string())); + } + Ok(()) => { + self.handle_job_status(id, -1, None); + } + } + } + } + _ => {} + } + true + } + + #[inline] + fn update_job_status( + job: &fs::TransferJob, + elapsed: i32, + last_update_jobs_status: &mut (Instant, HashMap), + handler: &mut Session, + ) { + if elapsed <= 0 { + return; + } + let transferred = job.transferred(); + let last_transferred = { + if let Some(v) = last_update_jobs_status.1.get(&job.id()) { + v.to_owned() + } else { + 0 + } + }; + last_update_jobs_status.1.insert(job.id(), transferred); + let speed = (transferred - last_transferred) as f64 / (elapsed as f64 / 1000.); + let file_num = job.file_num() - 1; + handler.job_progress(job.id(), file_num, speed, job.finished_size() as f64); + } + + fn update_jobs_status(&mut self) { + let elapsed = self.last_update_jobs_status.0.elapsed().as_millis() as i32; + if elapsed >= 1000 { + for job in self.read_jobs.iter() { + Self::update_job_status( + job, + elapsed, + &mut self.last_update_jobs_status, + &mut self.handler, + ); + } + for job in self.write_jobs.iter() { + Self::update_job_status( + job, + elapsed, + &mut self.last_update_jobs_status, + &mut self.handler, + ); + } + self.last_update_jobs_status.0 = Instant::now(); + } + } + + pub async fn sync_jobs_status_to_local(&mut self) -> bool { + log::info!("sync transfer job status"); + let mut config: PeerConfig = self.handler.load_config(); + let mut transfer_metas = TransferSerde::default(); + for job in self.read_jobs.iter() { + let json_str = serde_json::to_string(&job.gen_meta()).unwrap_or_default(); + transfer_metas.read_jobs.push(json_str); + } + for job in self.write_jobs.iter() { + let json_str = serde_json::to_string(&job.gen_meta()).unwrap_or_default(); + transfer_metas.write_jobs.push(json_str); + } + log::info!("meta: {:?}", transfer_metas); + config.transfer = transfer_metas; + self.handler.save_config(config); + true + } + + async fn send_opts_after_login(&self, peer: &mut Stream) { + if let Some(opts) = self + .handler + .lc + .read() + .unwrap() + .get_option_message_after_login() + { + let mut misc = Misc::new(); + misc.set_option(opts); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + allow_err!(peer.send(&msg_out).await); + } + } + + async fn handle_msg_from_peer(&mut self, data: &[u8], peer: &mut Stream) -> bool { + if let Ok(msg_in) = Message::parse_from_bytes(&data) { + match msg_in.union { + Some(message::Union::VideoFrame(vf)) => { + if !self.first_frame { + self.first_frame = true; + self.handler.close_success(); + self.handler.adapt_size(); + self.send_opts_after_login(peer).await; + } + let incomming_format = CodecFormat::from(&vf); + if self.video_format != incomming_format { + self.video_format = incomming_format.clone(); + self.handler.update_quality_status(QualityStatus { + codec_format: Some(incomming_format), + ..Default::default() + }) + }; + self.video_sender.send(MediaData::VideoFrame(vf)).ok(); + } + Some(message::Union::Hash(hash)) => { + self.handler + .handle_hash(&self.handler.password.clone(), hash, peer) + .await; + } + Some(message::Union::LoginResponse(lr)) => match lr.union { + Some(login_response::Union::Error(err)) => { + if !self.handler.handle_login_error(&err) { + return false; + } + } + 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) + // { + // 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.load_last_jobs().await; + // } + } + _ => {} + }, + Some(message::Union::CursorData(cd)) => { + self.handler.set_cursor_data(cd); + } + Some(message::Union::CursorId(id)) => { + self.handler.set_cursor_id(id.to_string()); + } + Some(message::Union::CursorPosition(cp)) => { + self.handler.set_cursor_position(cp); + } + Some(message::Union::Clipboard(cb)) => { + if !self.handler.lc.read().unwrap().disable_clipboard { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + update_clipboard(cb, Some(&self.old_clipboard)); + #[cfg(any(target_os = "android", target_os = "ios"))] + { + let content = if cb.compress { + hbb_common::compress::decompress(&cb.content) + } else { + cb.content.into() + }; + if let Ok(content) = String::from_utf8(content) { + self.handler.clipboard(content); + } + } + } + } + #[cfg(windows)] + Some(message::Union::Cliprdr(clip)) => { + if !self.handler.lc.read().unwrap().disable_clipboard { + if let Some(context) = &mut self.clipboard_file_context { + if let Some(clip) = msg_2_clip(clip) { + server_clip_file(context, 0, clip); + } + } + } + } + Some(message::Union::FileResponse(fr)) => { + match fr.union { + Some(file_response::Union::Dir(fd)) => { + #[cfg(windows)] + let entries = fd.entries.to_vec(); + #[cfg(not(windows))] + let mut entries = fd.entries.to_vec(); + #[cfg(not(windows))] + { + if self.handler.peer_platform() == "Windows" { + fs::transform_windows_path(&mut entries); + } + } + // let mut m = make_fd(fd.id, &entries, fd.id > 0); + // if fd.id <= 0 { + // m.set_item("path", fd.path); + // } + // self.handler.call("updateFolderFiles", &make_args!(m)); + if let Some(job) = fs::get_job(fd.id, &mut self.write_jobs) { + log::info!("job set_files: {:?}", entries); + job.set_files(entries); + } else if let Some(job) = self.remove_jobs.get_mut(&fd.id) { + job.files = entries; + } + } + Some(file_response::Union::Digest(digest)) => { + if digest.is_upload { + if let Some(job) = fs::get_job(digest.id, &mut self.read_jobs) { + if let Some(file) = job.files().get(digest.file_num as usize) { + let read_path = get_string(&job.join(&file.name)); + let overwrite_strategy = job.default_overwrite_strategy(); + if let Some(overwrite) = overwrite_strategy { + let req = FileTransferSendConfirmRequest { + id: digest.id, + file_num: digest.file_num, + union: Some(if overwrite { + file_transfer_send_confirm_request::Union::OffsetBlk(0) + } else { + file_transfer_send_confirm_request::Union::Skip( + true, + ) + }), + ..Default::default() + }; + job.confirm(&req); + let msg = new_send_confirm(req); + allow_err!(peer.send(&msg).await); + } else { + self.handler.override_file_confirm( + digest.id, + digest.file_num, + read_path, + true, + ); + } + } + } + } else { + if let Some(job) = fs::get_job(digest.id, &mut self.write_jobs) { + if let Some(file) = job.files().get(digest.file_num as usize) { + let write_path = get_string(&job.join(&file.name)); + let overwrite_strategy = job.default_overwrite_strategy(); + match fs::is_write_need_confirmation(&write_path, &digest) { + Ok(res) => match res { + DigestCheckResult::IsSame => { + let msg= new_send_confirm(FileTransferSendConfirmRequest { + id: digest.id, + file_num: digest.file_num, + union: Some(file_transfer_send_confirm_request::Union::Skip(true)), + ..Default::default() + }); + allow_err!(peer.send(&msg).await); + } + DigestCheckResult::NeedConfirm(digest) => { + if let Some(overwrite) = overwrite_strategy { + let msg = new_send_confirm( + FileTransferSendConfirmRequest { + id: digest.id, + file_num: digest.file_num, + union: Some(if overwrite { + file_transfer_send_confirm_request::Union::OffsetBlk(0) + } else { + file_transfer_send_confirm_request::Union::Skip(true) + }), + ..Default::default() + }, + ); + allow_err!(peer.send(&msg).await); + } else { + self.handler.override_file_confirm( + digest.id, + digest.file_num, + write_path, + false, + ); + } + } + DigestCheckResult::NoSuchFile => { + let msg = new_send_confirm( + FileTransferSendConfirmRequest { + id: digest.id, + file_num: digest.file_num, + union: Some(file_transfer_send_confirm_request::Union::OffsetBlk(0)), + ..Default::default() + }, + ); + allow_err!(peer.send(&msg).await); + } + }, + Err(err) => { + println!("error recving digest: {}", err); + } + } + } + } + } + } + Some(file_response::Union::Block(block)) => { + log::info!( + "file response block, file id:{}, file num: {}", + block.id, + block.file_num + ); + if let Some(job) = fs::get_job(block.id, &mut self.write_jobs) { + if let Err(_err) = job.write(block, None).await { + // to-do: add "skip" for writing job + } + self.update_jobs_status(); + } + } + Some(file_response::Union::Done(d)) => { + if let Some(job) = fs::get_job(d.id, &mut self.write_jobs) { + job.modify_time(); + fs::remove_job(d.id, &mut self.write_jobs); + } + self.handle_job_status(d.id, d.file_num, None); + } + Some(file_response::Union::Error(e)) => { + self.handle_job_status(e.id, e.file_num, Some(e.error)); + } + _ => {} + } + } + Some(message::Union::Misc(misc)) => match misc.union { + Some(misc::Union::AudioFormat(f)) => { + self.audio_sender.send(MediaData::AudioFormat(f)).ok(); + } + Some(misc::Union::ChatMessage(c)) => { + self.handler.new_message(c.text); + } + Some(misc::Union::PermissionInfo(p)) => { + 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); + self.handler.set_permission("keyboard", p.enabled); + } + Permission::Clipboard => { + SERVER_CLIPBOARD_ENABLED.store(p.enabled, Ordering::SeqCst); + 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); + if !p.enabled && self.handler.is_file_transfer() { + return true; + } + self.check_clipboard_file_context(); + self.handler.set_permission("file", p.enabled); + } + Permission::Restart => { + self.handler.set_permission("restart", p.enabled); + } + } + } + Some(misc::Union::SwitchDisplay(s)) => { + self.handler.ui_handler.switch_display(&s); + self.video_sender.send(MediaData::Reset).ok(); + if s.width > 0 && s.height > 0 { + self.handler.set_display(s.x, s.y, s.width, s.height); + } + } + Some(misc::Union::CloseReason(c)) => { + self.handler.msgbox("error", "Connection Error", &c); + return false; + } + Some(misc::Union::BackNotification(notification)) => { + if !self.handle_back_notification(notification).await { + return false; + } + } + _ => {} + }, + Some(message::Union::TestDelay(t)) => { + self.handler.handle_test_delay(t, peer).await; + } + Some(message::Union::AudioFrame(frame)) => { + if !self.handler.lc.read().unwrap().disable_audio { + self.audio_sender.send(MediaData::AudioFrame(frame)).ok(); + } + } + Some(message::Union::FileAction(action)) => match action.union { + Some(file_action::Union::SendConfirm(c)) => { + if let Some(job) = fs::get_job(c.id, &mut self.read_jobs) { + job.confirm(&c); + } + } + _ => {} + }, + _ => {} + } + } + true + } + + async fn handle_back_notification(&mut self, notification: BackNotification) -> bool { + match notification.union { + Some(back_notification::Union::BlockInputState(state)) => { + self.handle_back_msg_block_input( + state.enum_value_or(back_notification::BlockInputState::BlkStateUnknown), + ) + .await; + } + Some(back_notification::Union::PrivacyModeState(state)) => { + if !self + .handle_back_msg_privacy_mode( + state.enum_value_or(back_notification::PrivacyModeState::PrvStateUnknown), + ) + .await + { + return false; + } + } + _ => {} + } + true + } + + #[inline(always)] + fn update_block_input_state(&mut self, on: bool) { + self.handler.update_block_input_state(on); + } + + async fn handle_back_msg_block_input(&mut self, state: back_notification::BlockInputState) { + match state { + back_notification::BlockInputState::BlkOnSucceeded => { + self.update_block_input_state(true); + } + back_notification::BlockInputState::BlkOnFailed => { + self.handler + .msgbox("custom-error", "Block user input", "Failed"); + self.update_block_input_state(false); + } + back_notification::BlockInputState::BlkOffSucceeded => { + self.update_block_input_state(false); + } + back_notification::BlockInputState::BlkOffFailed => { + self.handler + .msgbox("custom-error", "Unblock user input", "Failed"); + } + _ => {} + } + } + + #[inline(always)] + fn update_privacy_mode(&mut self, on: bool) { + let mut config = self.handler.load_config(); + config.privacy_mode = on; + self.handler.save_config(config); + + self.handler.update_privacy_mode(); + } + + async fn handle_back_msg_privacy_mode( + &mut self, + state: back_notification::PrivacyModeState, + ) -> bool { + match state { + back_notification::PrivacyModeState::PrvOnByOther => { + self.handler.msgbox( + "error", + "Connecting...", + "Someone turns on privacy mode, exit", + ); + return false; + } + back_notification::PrivacyModeState::PrvNotSupported => { + self.handler + .msgbox("custom-error", "Privacy mode", "Unsupported"); + self.update_privacy_mode(false); + } + back_notification::PrivacyModeState::PrvOnSucceeded => { + self.handler + .msgbox("custom-nocancel", "Privacy mode", "In privacy mode"); + self.update_privacy_mode(true); + } + back_notification::PrivacyModeState::PrvOnFailedDenied => { + self.handler + .msgbox("custom-error", "Privacy mode", "Peer denied"); + self.update_privacy_mode(false); + } + back_notification::PrivacyModeState::PrvOnFailedPlugin => { + self.handler + .msgbox("custom-error", "Privacy mode", "Please install plugins"); + self.update_privacy_mode(false); + } + back_notification::PrivacyModeState::PrvOnFailed => { + self.handler + .msgbox("custom-error", "Privacy mode", "Failed"); + self.update_privacy_mode(false); + } + back_notification::PrivacyModeState::PrvOffSucceeded => { + self.handler + .msgbox("custom-nocancel", "Privacy mode", "Out privacy mode"); + self.update_privacy_mode(false); + } + back_notification::PrivacyModeState::PrvOffByPeer => { + self.handler + .msgbox("custom-error", "Privacy mode", "Peer exit"); + self.update_privacy_mode(false); + } + back_notification::PrivacyModeState::PrvOffFailed => { + self.handler + .msgbox("custom-error", "Privacy mode", "Failed to turn off"); + } + back_notification::PrivacyModeState::PrvOffUnknown => { + self.handler + .msgbox("custom-error", "Privacy mode", "Turned off"); + // log::error!("Privacy mode is turned off with unknown reason"); + self.update_privacy_mode(false); + } + _ => {} + } + true + } + + fn check_clipboard_file_context(&mut self) { + #[cfg(windows)] + { + let enabled = SERVER_FILE_TRANSFER_ENABLED.load(Ordering::SeqCst) + && self.handler.lc.read().unwrap().enable_file_transfer; + if enabled == self.clipboard_file_context.is_none() { + self.clipboard_file_context = if enabled { + match create_clipboard_file_context(true, false) { + Ok(context) => { + log::info!("clipboard context for file transfer created."); + Some(context) + } + Err(err) => { + log::error!( + "Create clipboard context for file transfer: {}", + err.to_string() + ); + None + } + } + } else { + log::info!("clipboard context for file transfer destroyed."); + None + }; + } + } + } +} + +struct RemoveJob { + files: Vec, + path: String, + sep: &'static str, + is_remote: bool, + no_confirm: bool, + last_update_job_status: Instant, +} + +impl RemoveJob { + fn new(files: Vec, path: String, sep: &'static str, is_remote: bool) -> Self { + Self { + files, + path, + sep, + is_remote, + no_confirm: false, + last_update_job_status: Instant::now(), + } + } + + pub fn _gen_meta(&self) -> RemoveJobMeta { + RemoveJobMeta { + path: self.path.clone(), + is_remote: self.is_remote, + no_confirm: self.no_confirm, + } + } +} diff --git a/src/flutter.rs b/src/flutter.rs index 514048e31..0b8c3626f 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -1,57 +1,36 @@ use std::{ collections::HashMap, sync::{ - atomic::{AtomicBool, AtomicUsize, Ordering}, - Arc, Mutex, RwLock, + Arc, RwLock, }, }; use flutter_rust_bridge::{StreamSink, ZeroCopyBuffer}; use hbb_common::{ - allow_err, bail, - compress::decompress, - config::{Config, LocalConfig, PeerConfig, TransferSerde}, - fs::{ - self, can_enable_overwrite_detection, get_job, get_string, new_send_confirm, - transform_windows_path, DigestCheckResult, - }, - log, + bail, + config::{LocalConfig}, message_proto::*, - protobuf::Message as _, - rendezvous_proto::ConnType, - tokio::{ - self, - sync::mpsc, - time::{self, Duration, Instant, Interval}, - }, - ResultType, Stream, + ResultType, }; use crate::{ - common::{self, make_fd_to_json, CLIPBOARD_INTERVAL}, ui_session_interface::{io_loop, InvokeUi, Session}, }; -#[cfg(not(any(target_os = "android", target_os = "ios")))] -use crate::common::{check_clipboard, update_clipboard, ClipboardContext}; -use crate::{client::*, flutter_ffi::EventToUI, make_fd_flutter}; + +use crate::{client::*, flutter_ffi::EventToUI}; pub(super) const APP_TYPE_MAIN: &str = "main"; pub(super) const APP_TYPE_DESKTOP_REMOTE: &str = "remote"; pub(super) const APP_TYPE_DESKTOP_FILE_TRANSFER: &str = "file transfer"; -const MILLI1: Duration = Duration::from_millis(1); - lazy_static::lazy_static! { pub static ref SESSIONS: RwLock>> = Default::default(); pub static ref GLOBAL_EVENT_STREAM: RwLock>> = Default::default(); // rust to dart event channel } -static SERVER_CLIPBOARD_ENABLED: AtomicBool = AtomicBool::new(true); -static SERVER_KEYBOARD_ENABLED: AtomicBool = AtomicBool::new(true); - #[derive(Default, Clone)] pub struct FlutterHandler { pub event_stream: Arc>>>, @@ -115,7 +94,7 @@ impl InvokeUi for FlutterHandler { } fn set_permission(&self, name: &str, value: bool) { - // todo!() + self.push_event("permission", vec![(name, &value.to_string())]); } fn update_pi(&self, pi: PeerInfo) { @@ -157,11 +136,14 @@ impl InvokeUi for FlutterHandler { } fn job_error(&self, id: i32, err: String, file_num: i32) { - // todo!() + self.push_event("job_error", vec![("id", &id.to_string()), ("err", &err)]); } fn job_done(&self, id: i32, file_num: i32) { - // todo!() + self.push_event( + "job_done", + vec![("id", &id.to_string()), ("file_num", &file_num.to_string())], + ); } fn clear_all_jobs(&self) { @@ -189,11 +171,27 @@ impl InvokeUi for FlutterHandler { } fn override_file_confirm(&self, id: i32, file_num: i32, to: String, is_upload: bool) { - // todo!() + self.push_event( + "override_file_confirm", + vec![ + ("id", &id.to_string()), + ("file_num", &file_num.to_string()), + ("read_path", &to), + ("is_upload", &is_upload.to_string()), + ], + ); } fn job_progress(&self, id: i32, file_num: i32, speed: f64, finished_size: f64) { - // todo!() + self.push_event( + "job_progress", + vec![ + ("id", &id.to_string()), + ("file_num", &file_num.to_string()), + ("speed", &speed.to_string()), + ("finished_size", &finished_size.to_string()), + ], + ); } fn adapt_size(&self) { @@ -245,6 +243,35 @@ impl InvokeUi for FlutterHandler { ], ); } + + fn new_message(&self, msg: String) { + self.push_event("chat_client_mode", vec![("text", &msg)]); + } + + fn switch_display(&self, display: &SwitchDisplay) { + self.push_event( + "switch_display", + vec![ + ("display", &display.to_string()), + ("x", &display.x.to_string()), + ("y", &display.y.to_string()), + ("width", &display.width.to_string()), + ("height", &display.height.to_string()), + ], + ); + } + + fn update_block_input_state(&self, on: bool) { + self.push_event( + "update_block_input_state", + [("input_state", if on { "on" } else { "off" })].into(), + ); + } + + #[cfg(any(target_os = "android", target_os = "ios"))] + fn clipboard(&self, content: String) { + self.push_event("clipboard", vec![("content", &content)]); + } } /// Create a new remote session with the given id. @@ -290,6 +317,7 @@ pub fn session_start_(id: &str, event_stream: StreamSink) -> ResultTy *session.event_stream.write().unwrap() = Some(event_stream); let session = session.clone(); std::thread::spawn(move || { + // TODO is_file_transfer is_port_forward // let is_file_transfer = session.lc.read().unwrap().is_file_transfer; // let is_port_forward = session.lc.read().unwrap().is_port_forward; // Connection::start(session, is_file_transfer, is_port_forward); @@ -301,1091 +329,7 @@ pub fn session_start_(id: &str, event_stream: StreamSink) -> ResultTy } } -// struct Connection { -// video_handler: VideoHandler, -// audio_handler: AudioHandler, -// session: Session, -// first_frame: bool, -// read_jobs: Vec, -// write_jobs: Vec, -// timer: Interval, -// last_update_jobs_status: (Instant, HashMap), -// data_count: Arc, -// frame_count: Arc, -// video_format: CodecFormat, -// } - -// impl Connection { -// // TODO: Similar to remote::start_clipboard -// // merge the code -// fn start_clipboard( -// tx_protobuf: mpsc::UnboundedSender, -// lc: Arc>, -// ) -> Option> { -// let (tx, rx) = std::sync::mpsc::channel(); -// #[cfg(not(any(target_os = "android", target_os = "ios")))] -// match ClipboardContext::new() { -// Ok(mut ctx) => { -// let old_clipboard: Arc> = Default::default(); -// // 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 -// { -// 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) -// } - -// /// Create a new connection. -// /// -// /// # Arguments -// /// -// /// * `session` - The session to create a new connection for. -// /// * `is_file_transfer` - Whether the connection is for file transfer. -// /// * `is_port_forward` - Whether the connection is for port forward. -// #[tokio::main(flavor = "current_thread")] -// async fn start(session: Session, is_file_transfer: bool, is_port_forward: bool) { -// let mut last_recv_time = Instant::now(); -// let (sender, mut receiver) = mpsc::unbounded_channel::(); -// let mut stop_clipboard = None; -// if !is_file_transfer && !is_port_forward { -// stop_clipboard = Self::start_clipboard(sender.clone(), session.lc.clone()); -// } -// *session.sender.write().unwrap() = Some(sender.clone()); -// let conn_type = if is_file_transfer { -// session.lc.write().unwrap().is_file_transfer = true; -// ConnType::FILE_TRANSFER -// } else if is_port_forward { -// ConnType::PORT_FORWARD // TODO: RDP -// } else { -// ConnType::DEFAULT_CONN -// }; -// let key = Config::get_option("key"); -// let token = Config::get_option("access_token"); - -// // TODO rdp & cli args -// let is_rdp = false; -// let args: Vec = Vec::new(); - -// if is_port_forward { -// if is_rdp { -// // let port = handler -// // .get_option("rdp_port".to_owned()) -// // .parse::() -// // .unwrap_or(3389); -// // std::env::set_var( -// // "rdp_username", -// // handler.get_option("rdp_username".to_owned()), -// // ); -// // std::env::set_var( -// // "rdp_password", -// // handler.get_option("rdp_password".to_owned()), -// // ); -// // log::info!("Remote rdp port: {}", port); -// // start_one_port_forward(handler, 0, "".to_owned(), port, receiver, &key, &token).await; -// } else if args.len() == 0 { -// let pfs = session.lc.read().unwrap().port_forwards.clone(); -// let mut queues = HashMap::>::new(); -// for d in pfs { -// sender.send(Data::AddPortForward(d)).ok(); -// } -// loop { -// match receiver.recv().await { -// Some(Data::AddPortForward((port, remote_host, remote_port))) => { -// if port <= 0 || remote_port <= 0 { -// continue; -// } -// let (sender, receiver) = mpsc::unbounded_channel::(); -// queues.insert(port, sender); -// let handler = session.clone(); -// let key = key.clone(); -// let token = token.clone(); -// tokio::spawn(async move { -// start_one_port_forward( -// handler, -// port, -// remote_host, -// remote_port, -// receiver, -// &key, -// &token, -// ) -// .await; -// }); -// } -// Some(Data::RemovePortForward(port)) => { -// if let Some(s) = queues.remove(&port) { -// s.send(Data::Close).ok(); -// } -// } -// Some(Data::Close) => { -// break; -// } -// Some(d) => { -// for (_, s) in queues.iter() { -// s.send(d.clone()).ok(); -// } -// } -// _ => {} -// } -// } -// } else { -// // let port = handler.args[0].parse::().unwrap_or(0); -// // if handler.args.len() != 3 -// // || handler.args[2].parse::().unwrap_or(0) <= 0 -// // || port <= 0 -// // { -// // handler.on_error("Invalid arguments, usage:

    rustdesk --port-forward remote-id listen-port remote-host remote-port"); -// // } -// // let remote_host = handler.args[1].clone(); -// // let remote_port = handler.args[2].parse::().unwrap_or(0); -// // start_one_port_forward( -// // handler, -// // port, -// // remote_host, -// // remote_port, -// // receiver, -// // &key, -// // &token, -// // ) -// // .await; -// } -// return; -// } - -// let latency_controller = LatencyController::new(); -// let latency_controller_cl = latency_controller.clone(); - -// let mut conn = Connection { -// video_handler: VideoHandler::new(latency_controller), -// audio_handler: AudioHandler::new(latency_controller_cl), -// session: session.clone(), -// first_frame: false, -// read_jobs: Vec::new(), -// write_jobs: Vec::new(), -// timer: time::interval(SEC30), -// last_update_jobs_status: (Instant::now(), Default::default()), -// data_count: Arc::new(AtomicUsize::new(0)), -// frame_count: Arc::new(AtomicUsize::new(0)), -// video_format: CodecFormat::Unknown, -// }; - -// match Client::start(&session.id, &key, &token, conn_type, session.clone()).await { -// Ok((mut peer, direct)) => { -// SERVER_KEYBOARD_ENABLED.store(true, Ordering::SeqCst); -// SERVER_CLIPBOARD_ENABLED.store(true, Ordering::SeqCst); - -// session.push_event( -// "connection_ready", -// vec![ -// ("secure", &peer.is_secured().to_string()), -// ("direct", &direct.to_string()), -// ], -// ); - -// let mut status_timer = time::interval(Duration::new(1, 0)); - -// loop { -// tokio::select! { -// res = peer.next() => { -// if let Some(res) = res { -// match res { -// Err(err) => { -// log::error!("Connection closed: {}", err); -// session.msgbox("error", "Connection Error", &err.to_string()); -// break; -// } -// Ok(ref bytes) => { -// last_recv_time = Instant::now(); -// conn.data_count.fetch_add(bytes.len(), Ordering::Relaxed); -// if !conn.handle_msg_from_peer(bytes, &mut peer).await { -// break -// } -// } -// } -// } else { -// if session.lc.read().unwrap().restarting_remote_device { -// log::info!("Restart remote device"); -// session.msgbox("restarting", "Restarting Remote Device", "remote_restarting_tip"); -// } else { -// log::info!("Reset by the peer"); -// session.msgbox("error", "Connection Error", "Reset by the peer"); -// } -// break; -// } -// } -// d = receiver.recv() => { -// if let Some(d) = d { -// if !conn.handle_msg_from_ui(d, &mut peer).await { -// break; -// } -// } -// } -// _ = conn.timer.tick() => { -// if last_recv_time.elapsed() >= SEC30 { -// session.msgbox("error", "Connection Error", "Timeout"); -// break; -// } -// if !conn.read_jobs.is_empty() { -// if let Err(err) = fs::handle_read_jobs(&mut conn.read_jobs, &mut peer).await { -// log::debug!("Connection Error: {}", err); -// break; -// } -// conn.update_jobs_status(); -// } else { -// conn.timer = time::interval_at(Instant::now() + SEC30, SEC30); -// } -// } -// _ = status_timer.tick() => { -// let speed = conn.data_count.swap(0, Ordering::Relaxed); -// let speed = format!("{:.2}kB/s", speed as f32 / 1024 as f32); -// let fps = conn.frame_count.swap(0, Ordering::Relaxed) as _; -// conn.session.update_quality_status(QualityStatus { -// speed:Some(speed), -// fps:Some(fps), -// ..Default::default() -// }); -// } -// } -// } -// log::debug!("Exit io_loop of id={}", session.id); -// } -// Err(err) => { -// session.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); -// } - -// /// Handle message from peer. -// /// Return false if the connection should be closed. -// /// -// /// The message is handled by [`Message`], see [`message::Union`] for possible types. -// async fn handle_msg_from_peer(&mut self, data: &[u8], peer: &mut Stream) -> bool { -// if let Ok(msg_in) = Message::parse_from_bytes(&data) { -// match msg_in.union { -// Some(message::Union::VideoFrame(vf)) => { -// if !self.first_frame { -// self.first_frame = true; -// common::send_opts_after_login(&self.session.lc.read().unwrap(), peer).await; -// } -// let incomming_format = CodecFormat::from(&vf); -// if self.video_format != incomming_format { -// self.video_format = incomming_format.clone(); -// self.session.update_quality_status(QualityStatus { -// codec_format: Some(incomming_format), -// ..Default::default() -// }) -// }; -// if let Ok(true) = self.video_handler.handle_frame(vf) { -// if let Some(stream) = &*self.session.events2ui.read().unwrap() { -// self.frame_count.fetch_add(1, Ordering::Relaxed); -// stream.add(EventToUI::Rgba(ZeroCopyBuffer( -// self.video_handler.rgb.clone(), -// ))); -// } -// } -// } -// Some(message::Union::Hash(hash)) => { -// self.session.handle_hash("", hash, peer).await; -// } -// Some(message::Union::LoginResponse(lr)) => match lr.union { -// Some(login_response::Union::Error(err)) => { -// if !self.session.handle_login_error(&err) { -// return false; -// } -// } -// Some(login_response::Union::PeerInfo(pi)) => { -// self.session.handle_peer_info(pi); -// } -// _ => {} -// }, -// Some(message::Union::Clipboard(cb)) => { -// if !self.session.lc.read().unwrap().disable_clipboard { -// let content = if cb.compress { -// decompress(&cb.content) -// } else { -// cb.content.into() -// }; -// if let Ok(content) = String::from_utf8(content) { -// self.session -// .push_event("clipboard", vec![("content", &content)]); -// } -// } -// } -// Some(message::Union::CursorData(cd)) => { -// let colors = hbb_common::compress::decompress(&cd.colors); -// self.session.push_event( -// "cursor_data", -// vec![ -// ("id", &cd.id.to_string()), -// ("hotx", &cd.hotx.to_string()), -// ("hoty", &cd.hoty.to_string()), -// ("width", &cd.width.to_string()), -// ("height", &cd.height.to_string()), -// ( -// "colors", -// &serde_json::ser::to_string(&colors).unwrap_or("".to_owned()), -// ), -// ], -// ); -// } -// Some(message::Union::CursorId(id)) => { -// self.session -// .push_event("cursor_id", vec![("id", &id.to_string())]); -// } -// Some(message::Union::CursorPosition(cp)) => { -// self.session.push_event( -// "cursor_position", -// vec![("x", &cp.x.to_string()), ("y", &cp.y.to_string())], -// ); -// } -// Some(message::Union::FileResponse(fr)) => { -// match fr.union { -// Some(file_response::Union::Dir(fd)) => { -// let mut entries = fd.entries.to_vec(); -// if self.session.peer_platform() == "Windows" { -// transform_windows_path(&mut entries); -// } -// let id = fd.id; -// self.session.push_event( -// "file_dir", -// vec![("value", &make_fd_to_json(fd)), ("is_local", "false")], -// ); -// if let Some(job) = fs::get_job(id, &mut self.write_jobs) { -// job.set_files(entries); -// } -// } -// Some(file_response::Union::Block(block)) => { -// if let Some(job) = fs::get_job(block.id, &mut self.write_jobs) { -// if let Err(_err) = job.write(block, None).await { -// // to-do: add "skip" for writing job -// } -// self.update_jobs_status(); -// } -// } -// Some(file_response::Union::Done(d)) => { -// if let Some(job) = fs::get_job(d.id, &mut self.write_jobs) { -// job.modify_time(); -// fs::remove_job(d.id, &mut self.write_jobs); -// } -// self.handle_job_status(d.id, d.file_num, None); -// } -// Some(file_response::Union::Error(e)) => { -// self.handle_job_status(e.id, e.file_num, Some(e.error)); -// } -// Some(file_response::Union::Digest(digest)) => { -// if digest.is_upload { -// if let Some(job) = fs::get_job(digest.id, &mut self.read_jobs) { -// if let Some(file) = job.files().get(digest.file_num as usize) { -// let read_path = get_string(&job.join(&file.name)); -// let overwrite_strategy = job.default_overwrite_strategy(); -// if let Some(overwrite) = overwrite_strategy { -// let req = FileTransferSendConfirmRequest { -// id: digest.id, -// file_num: digest.file_num, -// union: Some(if overwrite { -// file_transfer_send_confirm_request::Union::OffsetBlk(0) -// } else { -// file_transfer_send_confirm_request::Union::Skip( -// true, -// ) -// }), -// ..Default::default() -// }; -// job.confirm(&req); -// let msg = new_send_confirm(req); -// allow_err!(peer.send(&msg).await); -// } else { -// self.handle_override_file_confirm( -// digest.id, -// digest.file_num, -// read_path, -// true, -// ); -// } -// } -// } -// } else { -// if let Some(job) = fs::get_job(digest.id, &mut self.write_jobs) { -// if let Some(file) = job.files().get(digest.file_num as usize) { -// let write_path = get_string(&job.join(&file.name)); -// let overwrite_strategy = job.default_overwrite_strategy(); -// match fs::is_write_need_confirmation(&write_path, &digest) { -// Ok(res) => match res { -// DigestCheckResult::IsSame => { -// let msg= new_send_confirm(FileTransferSendConfirmRequest { -// id: digest.id, -// file_num: digest.file_num, -// union: Some(file_transfer_send_confirm_request::Union::Skip(true)), -// ..Default::default() -// }); -// self.session.send_msg(msg); -// } -// DigestCheckResult::NeedConfirm(digest) => { -// if let Some(overwrite) = overwrite_strategy { -// let msg = new_send_confirm( -// FileTransferSendConfirmRequest { -// id: digest.id, -// file_num: digest.file_num, -// union: Some(if overwrite { -// file_transfer_send_confirm_request::Union::OffsetBlk(0) -// } else { -// file_transfer_send_confirm_request::Union::Skip(true) -// }), -// ..Default::default() -// }, -// ); -// self.session.send_msg(msg); -// } else { -// self.handle_override_file_confirm( -// digest.id, -// digest.file_num, -// write_path.to_string(), -// false, -// ); -// } -// } -// DigestCheckResult::NoSuchFile => { -// let msg = new_send_confirm( -// FileTransferSendConfirmRequest { -// id: digest.id, -// file_num: digest.file_num, -// union: Some(file_transfer_send_confirm_request::Union::OffsetBlk(0)), -// ..Default::default() -// }, -// ); -// self.session.send_msg(msg); -// } -// }, -// Err(err) => { -// println!("error recving digest: {}", err); -// } -// } -// } -// } -// } -// } -// _ => {} -// } -// } -// Some(message::Union::Misc(misc)) => match misc.union { -// Some(misc::Union::AudioFormat(f)) => { -// self.audio_handler.handle_format(f); // -// } -// Some(misc::Union::ChatMessage(c)) => { -// self.session -// .push_event("chat_client_mode", vec![("text", &c.text)]); -// } -// Some(misc::Union::PermissionInfo(p)) => { -// log::info!("Change permission {:?} -> {}", p.permission, p.enabled); -// use permission_info::Permission; -// self.session.push_event( -// "permission", -// vec![( -// match p.permission.enum_value_or_default() { -// Permission::Keyboard => "keyboard", -// Permission::Clipboard => "clipboard", -// Permission::Audio => "audio", -// Permission::Restart => "restart", -// _ => "", -// }, -// &p.enabled.to_string(), -// )], -// ); -// } -// Some(misc::Union::SwitchDisplay(s)) => { -// self.video_handler.reset(); -// self.session.push_event( -// "switch_display", -// vec![ -// ("display", &s.display.to_string()), -// ("x", &s.x.to_string()), -// ("y", &s.y.to_string()), -// ("width", &s.width.to_string()), -// ("height", &s.height.to_string()), -// ], -// ); -// } -// Some(misc::Union::CloseReason(c)) => { -// self.session.msgbox("error", "Connection Error", &c); -// return false; -// } -// Some(misc::Union::BackNotification(notification)) => { -// if !self.handle_back_notification(notification).await { -// return false; -// } -// } -// _ => {} -// }, -// Some(message::Union::TestDelay(t)) => { -// self.session.handle_test_delay(t, peer).await; -// } -// Some(message::Union::AudioFrame(frame)) => { -// if !self.session.lc.read().unwrap().disable_audio { -// self.audio_handler.handle_frame(frame); -// } -// } -// Some(message::Union::FileAction(action)) => match action.union { -// Some(file_action::Union::SendConfirm(c)) => { -// if let Some(job) = fs::get_job(c.id, &mut self.read_jobs) { -// job.confirm(&c); -// } -// } -// _ => {} -// }, -// _ => {} -// } -// } -// true -// } - -// async fn handle_back_notification(&mut self, notification: BackNotification) -> bool { -// match notification.union { -// Some(back_notification::Union::BlockInputState(state)) => { -// self.handle_back_msg_block_input( -// state.enum_value_or(back_notification::BlockInputState::BlkStateUnknown), -// ) -// .await; -// } -// Some(back_notification::Union::PrivacyModeState(state)) => { -// if !self -// .handle_back_msg_privacy_mode( -// state.enum_value_or(back_notification::PrivacyModeState::PrvStateUnknown), -// ) -// .await -// { -// return false; -// } -// } -// _ => {} -// } -// true -// } - -// #[inline(always)] -// fn update_block_input_state(&mut self, on: bool) { -// self.session.push_event( -// "update_block_input_state", -// [("input_state", if on { "on" } else { "off" })].into(), -// ); -// } - -// async fn handle_back_msg_block_input(&mut self, state: back_notification::BlockInputState) { -// match state { -// back_notification::BlockInputState::BlkOnSucceeded => { -// self.update_block_input_state(true); -// } -// back_notification::BlockInputState::BlkOnFailed => { -// self.session -// .msgbox("custom-error", "Block user input", "Failed"); -// self.update_block_input_state(false); -// } -// back_notification::BlockInputState::BlkOffSucceeded => { -// self.update_block_input_state(false); -// } -// back_notification::BlockInputState::BlkOffFailed => { -// self.session -// .msgbox("custom-error", "Unblock user input", "Failed"); -// } -// _ => {} -// } -// } - -// #[inline(always)] -// fn update_privacy_mode(&mut self, on: bool) { -// let mut config = self.session.load_config(); -// config.privacy_mode = on; -// self.session.save_config(&config); -// self.session.lc.write().unwrap().get_config().privacy_mode = on; -// self.session.push_event("update_privacy_mode", [].into()); -// } - -// async fn handle_back_msg_privacy_mode( -// &mut self, -// state: back_notification::PrivacyModeState, -// ) -> bool { -// match state { -// back_notification::PrivacyModeState::PrvOnByOther => { -// self.session.msgbox( -// "error", -// "Connecting...", -// "Someone turns on privacy mode, exit", -// ); -// return false; -// } -// back_notification::PrivacyModeState::PrvNotSupported => { -// self.session -// .msgbox("custom-error", "Privacy mode", "Unsupported"); -// self.update_privacy_mode(false); -// } -// back_notification::PrivacyModeState::PrvOnSucceeded => { -// self.session -// .msgbox("custom-nocancel", "Privacy mode", "In privacy mode"); -// self.update_privacy_mode(true); -// } -// back_notification::PrivacyModeState::PrvOnFailedDenied => { -// self.session -// .msgbox("custom-error", "Privacy mode", "Peer denied"); -// self.update_privacy_mode(false); -// } -// back_notification::PrivacyModeState::PrvOnFailedPlugin => { -// self.session -// .msgbox("custom-error", "Privacy mode", "Please install plugins"); -// self.update_privacy_mode(false); -// } -// back_notification::PrivacyModeState::PrvOnFailed => { -// self.session -// .msgbox("custom-error", "Privacy mode", "Failed"); -// self.update_privacy_mode(false); -// } -// back_notification::PrivacyModeState::PrvOffSucceeded => { -// self.session -// .msgbox("custom-nocancel", "Privacy mode", "Out privacy mode"); -// self.update_privacy_mode(false); -// } -// back_notification::PrivacyModeState::PrvOffByPeer => { -// self.session -// .msgbox("custom-error", "Privacy mode", "Peer exit"); -// self.update_privacy_mode(false); -// } -// back_notification::PrivacyModeState::PrvOffFailed => { -// self.session -// .msgbox("custom-error", "Privacy mode", "Failed to turn off"); -// } -// back_notification::PrivacyModeState::PrvOffUnknown => { -// self.session -// .msgbox("custom-error", "Privacy mode", "Turned off"); -// // log::error!("Privacy mode is turned off with unknown reason"); -// self.update_privacy_mode(false); -// } -// _ => {} -// } -// true -// } - -// async fn handle_msg_from_ui(&mut self, data: Data, peer: &mut Stream) -> bool { -// match data { -// Data::Close => { -// self.sync_jobs_status_to_local().await; -// return false; -// } -// Data::Login((password, remember)) => { -// self.session -// .handle_login_from_ui(password, remember, peer) -// .await; -// } -// Data::Message(msg) => { -// allow_err!(peer.send(&msg).await); -// } -// Data::SendFiles((id, path, to, file_num, include_hidden, is_remote)) => { -// let od = can_enable_overwrite_detection(self.session.lc.read().unwrap().version); -// if is_remote { -// log::debug!("New job {}, write to {} from remote {}", id, to, path); -// self.write_jobs.push(fs::TransferJob::new_write( -// id, -// path.clone(), -// to, -// file_num, -// include_hidden, -// is_remote, -// Vec::new(), -// od, -// )); -// allow_err!( -// peer.send(&fs::new_send(id, path, file_num, include_hidden)) -// .await -// ); -// } else { -// match fs::TransferJob::new_read( -// id, -// to.clone(), -// path.clone(), -// file_num, -// include_hidden, -// is_remote, -// od, -// ) { -// Err(err) => { -// self.handle_job_status(id, -1, Some(err.to_string())); -// } -// Ok(job) => { -// log::debug!( -// "New job {}, read {} to remote {}, {} files", -// id, -// path, -// to, -// job.files().len() -// ); -// let m = make_fd_flutter(id, job.files(), true); -// self.session -// .push_event("update_folder_files", vec![("info", &m)]); -// let files = job.files().clone(); -// self.read_jobs.push(job); -// self.timer = time::interval(MILLI1); -// allow_err!(peer.send(&fs::new_receive(id, to, file_num, files)).await); -// } -// } -// } -// } -// Data::RemoveDirAll((id, path, is_remote, include_hidden)) => { -// if is_remote { -// let mut msg_out = Message::new(); -// let mut file_action = FileAction::new(); -// file_action.set_all_files(ReadAllFiles { -// id, -// path: path.clone(), -// include_hidden, -// ..Default::default() -// }); -// msg_out.set_file_action(file_action); -// allow_err!(peer.send(&msg_out).await); -// } else { -// match fs::get_recursive_files(&path, include_hidden) { -// Ok(entries) => { -// let mut fd = FileDirectory::new(); -// fd.id = id; -// fd.path = path; -// fd.entries = entries; -// self.session.push_event( -// "file_dir", -// vec![("value", &make_fd_to_json(fd)), ("is_local", "true")], -// ); -// } -// Err(err) => { -// self.handle_job_status(id, -1, Some(err.to_string())); -// } -// } -// } -// } -// Data::CancelJob(id) => { -// let mut msg_out = Message::new(); -// let mut file_action = FileAction::new(); -// file_action.set_cancel(FileTransferCancel { -// id: id, -// ..Default::default() -// }); -// msg_out.set_file_action(file_action); -// allow_err!(peer.send(&msg_out).await); -// if let Some(job) = fs::get_job(id, &mut self.write_jobs) { -// job.remove_download_file(); -// fs::remove_job(id, &mut self.write_jobs); -// } -// fs::remove_job(id, &mut self.read_jobs); -// } -// Data::RemoveDir((id, path)) => { -// let mut msg_out = Message::new(); -// let mut file_action = FileAction::new(); -// file_action.set_remove_dir(FileRemoveDir { -// id, -// path, -// recursive: true, -// ..Default::default() -// }); -// msg_out.set_file_action(file_action); -// allow_err!(peer.send(&msg_out).await); -// } -// Data::RemoveFile((id, path, file_num, is_remote)) => { -// if is_remote { -// let mut msg_out = Message::new(); -// let mut file_action = FileAction::new(); -// file_action.set_remove_file(FileRemoveFile { -// id, -// path, -// file_num, -// ..Default::default() -// }); -// msg_out.set_file_action(file_action); -// allow_err!(peer.send(&msg_out).await); -// } else { -// match fs::remove_file(&path) { -// Err(err) => { -// self.handle_job_status(id, file_num, Some(err.to_string())); -// } -// Ok(()) => { -// self.handle_job_status(id, file_num, None); -// } -// } -// } -// } -// Data::CreateDir((id, path, is_remote)) => { -// if is_remote { -// let mut msg_out = Message::new(); -// let mut file_action = FileAction::new(); -// file_action.set_create(FileDirCreate { -// id, -// path, -// ..Default::default() -// }); -// msg_out.set_file_action(file_action); -// allow_err!(peer.send(&msg_out).await); -// } else { -// match fs::create_dir(&path) { -// Err(err) => { -// self.handle_job_status(id, -1, Some(err.to_string())); -// } -// Ok(()) => { -// self.handle_job_status(id, -1, None); -// } -// } -// } -// } -// Data::SetConfirmOverrideFile((id, file_num, need_override, remember, is_upload)) => { -// if is_upload { -// if let Some(job) = fs::get_job(id, &mut self.read_jobs) { -// if remember { -// job.set_overwrite_strategy(Some(need_override)); -// } -// job.confirm(&FileTransferSendConfirmRequest { -// id, -// file_num, -// union: if need_override { -// Some(file_transfer_send_confirm_request::Union::OffsetBlk(0)) -// } else { -// Some(file_transfer_send_confirm_request::Union::Skip(true)) -// }, -// ..Default::default() -// }); -// } -// } else { -// if let Some(job) = fs::get_job(id, &mut self.write_jobs) { -// if remember { -// job.set_overwrite_strategy(Some(need_override)); -// } -// let mut msg = Message::new(); -// let mut file_action = FileAction::new(); -// file_action.set_send_confirm(FileTransferSendConfirmRequest { -// id, -// file_num, -// union: if need_override { -// Some(file_transfer_send_confirm_request::Union::OffsetBlk(0)) -// } else { -// Some(file_transfer_send_confirm_request::Union::Skip(true)) -// }, -// ..Default::default() -// }); -// msg.set_file_action(file_action); -// self.session.send_msg(msg); -// } -// } -// } -// Data::AddJob((id, path, to, file_num, include_hidden, is_remote)) => { -// let od = can_enable_overwrite_detection(self.session.lc.read().unwrap().version); -// if is_remote { -// log::debug!( -// "new write waiting job {}, write to {} from remote {}", -// id, -// to, -// path -// ); -// let mut job = fs::TransferJob::new_write( -// id, -// path.clone(), -// to, -// file_num, -// include_hidden, -// is_remote, -// Vec::new(), -// od, -// ); -// job.is_last_job = true; -// self.write_jobs.push(job); -// } else { -// match fs::TransferJob::new_read( -// id, -// to.clone(), -// path.clone(), -// file_num, -// include_hidden, -// is_remote, -// od, -// ) { -// Err(err) => { -// self.handle_job_status(id, -1, Some(err.to_string())); -// } -// Ok(mut job) => { -// log::debug!( -// "new read waiting job {}, read {} to remote {}, {} files", -// id, -// path, -// to, -// job.files().len() -// ); -// let m = make_fd_flutter(job.id(), job.files(), true); -// self.session -// .push_event("update_folder_files", vec![("info", &m)]); -// job.is_last_job = true; -// self.read_jobs.push(job); -// self.timer = time::interval(MILLI1); -// } -// } -// } -// } -// Data::ResumeJob((id, is_remote)) => { -// if is_remote { -// if let Some(job) = get_job(id, &mut self.write_jobs) { -// job.is_last_job = false; -// allow_err!( -// peer.send(&fs::new_send( -// id, -// job.remote.clone(), -// job.file_num, -// job.show_hidden -// )) -// .await -// ); -// } -// } else { -// if let Some(job) = get_job(id, &mut self.read_jobs) { -// job.is_last_job = false; -// allow_err!( -// peer.send(&fs::new_receive( -// id, -// job.path.to_string_lossy().to_string(), -// job.file_num, -// job.files.clone() -// )) -// .await -// ); -// } -// } -// } -// _ => {} -// } -// true -// } - -// #[inline] -// fn update_job_status( -// job: &fs::TransferJob, -// elapsed: i32, -// last_update_jobs_status: &mut (Instant, HashMap), -// session: &Session, -// ) { -// if elapsed <= 0 { -// return; -// } -// let transferred = job.transferred(); -// let last_transferred = { -// if let Some(v) = last_update_jobs_status.1.get(&job.id()) { -// v.to_owned() -// } else { -// 0 -// } -// }; -// last_update_jobs_status.1.insert(job.id(), transferred); -// let speed = (transferred - last_transferred) as f64 / (elapsed as f64 / 1000.); -// let file_num = job.file_num() - 1; -// session.push_event( -// "job_progress", -// vec![ -// ("id", &job.id().to_string()), -// ("file_num", &file_num.to_string()), -// ("speed", &speed.to_string()), -// ("finished_size", &job.finished_size().to_string()), -// ], -// ); -// } - -// fn update_jobs_status(&mut self) { -// let elapsed = self.last_update_jobs_status.0.elapsed().as_millis() as i32; -// if elapsed >= 1000 { -// for job in self.read_jobs.iter() { -// Self::update_job_status( -// job, -// elapsed, -// &mut self.last_update_jobs_status, -// &self.session, -// ); -// } -// for job in self.write_jobs.iter() { -// Self::update_job_status( -// job, -// elapsed, -// &mut self.last_update_jobs_status, -// &self.session, -// ); -// } -// self.last_update_jobs_status.0 = Instant::now(); -// } -// } - -// fn handle_job_status(&mut self, id: i32, file_num: i32, err: Option) { -// if let Some(err) = err { -// self.session -// .push_event("job_error", vec![("id", &id.to_string()), ("err", &err)]); -// } else { -// self.session.push_event( -// "job_done", -// vec![("id", &id.to_string()), ("file_num", &file_num.to_string())], -// ); -// } -// } - -// fn handle_override_file_confirm( -// &mut self, -// id: i32, -// file_num: i32, -// read_path: String, -// is_upload: bool, -// ) { -// self.session.push_event( -// "override_file_confirm", -// vec![ -// ("id", &id.to_string()), -// ("file_num", &file_num.to_string()), -// ("read_path", &read_path), -// ("is_upload", &is_upload.to_string()), -// ], -// ); -// } - -// async fn sync_jobs_status_to_local(&mut self) -> bool { -// log::info!("sync transfer job status"); -// let mut config: PeerConfig = self.session.load_config(); -// let mut transfer_metas = TransferSerde::default(); -// for job in self.read_jobs.iter() { -// let json_str = serde_json::to_string(&job.gen_meta()).unwrap(); -// transfer_metas.read_jobs.push(json_str); -// } -// for job in self.write_jobs.iter() { -// let json_str = serde_json::to_string(&job.gen_meta()).unwrap(); -// transfer_metas.write_jobs.push(json_str); -// } -// log::info!("meta: {:?}", transfer_metas); -// config.transfer = transfer_metas; -// self.session.save_config(&config); -// true -// } -// } - // Server Side -// TODO connection_manager need use struct and trait,impl default method #[cfg(not(any(target_os = "ios")))] pub mod connection_manager { use std::{ diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 9e8c8fc51..2d412ea9b 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -2,8 +2,8 @@ use std::{ collections::HashMap, ops::{Deref, DerefMut}, sync::{ - atomic::{AtomicBool, AtomicUsize, Ordering}, - Arc, Mutex, RwLock, + atomic::{AtomicBool, Ordering}, + Arc, Mutex, }, }; @@ -22,35 +22,15 @@ use clipboard::{ cliprdr::CliprdrClientContext, create_cliprdr_context as create_clipboard_file_context, get_rx_clip_client, server_clip_file, }; -use enigo::{self, Enigo, KeyboardControllable}; -use hbb_common::{ - allow_err, - config::{Config, LocalConfig, PeerConfig, TransferSerde}, - fs::{ - self, can_enable_overwrite_detection, get_job, get_string, new_send_confirm, - DigestCheckResult, RemoveJobMeta, TransferJobMeta, - }, - get_version_number, log, - message_proto::{permission_info::Permission, *}, - protobuf::Message as _, - rendezvous_proto::ConnType, - sleep, - tokio::{ - self, - sync::mpsc, - time::{self, Duration, Instant, Interval}, - }, - Stream, -}; +use enigo::{self}; +use hbb_common::{allow_err, log, message_proto::*}; #[cfg(windows)] use crate::clipboard_file::*; use crate::{ client::*, - common::{self, check_clipboard, update_clipboard, ClipboardContext, CLIPBOARD_INTERVAL}, - ui_session_interface::{io_loop, InvokeUi, Remote, Session, SERVER_KEYBOARD_ENABLED}, + ui_session_interface::{InvokeUi, Session}, }; -use errno; type Video = AssetPtr; @@ -131,7 +111,7 @@ impl InvokeUi for SciterHandler { self.call2("setPermission", &make_args!(name, value)); } - fn update_pi(&self, pi: PeerInfo) {} + fn update_pi(&self, pi: PeerInfo) {} // TODO dup flutter fn close_success(&self) { self.call2("closeSuccess", &make_args!()); @@ -165,15 +145,15 @@ impl InvokeUi for SciterHandler { } fn job_error(&self, id: i32, err: String, file_num: i32) { - todo!() + self.call("jobError", &make_args!(id, err, file_num)); } fn job_done(&self, id: i32, file_num: i32) { - todo!() + self.call("jobDone", &make_args!(id, file_num)); } fn clear_all_jobs(&self) { - todo!() + self.call("clearAllJobs", &make_args!()); } fn add_job( @@ -189,19 +169,25 @@ impl InvokeUi for SciterHandler { } fn update_transfer_list(&self) { - todo!() + self.call("updateTransferList", &make_args!()); } fn confirm_delete_files(&self, id: i32, i: i32, name: String) { - todo!() + self.call("confirmDeleteFiles", &make_args!(id, i, name)); } fn override_file_confirm(&self, id: i32, file_num: i32, to: String, is_upload: bool) { - todo!() + self.call( + "overrideFileConfirm", + &make_args!(id, file_num, to, is_upload), + ); } fn job_progress(&self, id: i32, file_num: i32, speed: f64, finished_size: f64) { - todo!() + self.call( + "jobProgress", + &make_args!(id, file_num, speed, finished_size), + ); } fn adapt_size(&self) { @@ -227,11 +213,22 @@ impl InvokeUi for SciterHandler { current_display: usize, is_file_transfer: bool, ) { - todo!() } fn msgbox(&self, msgtype: &str, title: &str, text: &str, retry: bool) { - todo!() + self.call2("msgbox_retry", &make_args!(msgtype, title, text, retry)); + } + + fn new_message(&self, msg: String) { + self.call("newMessage", &make_args!(msg)); + } + + fn switch_display(&self, display: &SwitchDisplay) { + self.call("switchDisplay", &make_args!(display.display)); + } + + fn update_block_input_state(&self, on: bool) { + self.call("updateBlockInputState", &make_args!(on)); } } @@ -405,21 +402,7 @@ impl SciterSession { Self(session) } - // fn update_quality_status(&self, status: QualityStatus) { - // self.call2( - // "updateQualityStatus", - // &make_args!( - // status.speed.map_or(Value::null(), |it| it.into()), - // status.fps.map_or(Value::null(), |it| it.into()), - // status.delay.map_or(Value::null(), |it| it.into()), - // status.target_bitrate.map_or(Value::null(), |it| it.into()), - // status - // .codec_format - // .map_or(Value::null(), |it| it.to_string().into()) - // ), - // ); - // } - + // TODO fn start_keyboard_hook(&'static self) { if self.is_port_forward() || self.is_file_transfer() { return; @@ -652,14 +635,6 @@ impl SciterSession { }); } - // fn get_view_style(&mut self) -> String { - // return self.lc.read().unwrap().view_style.clone(); - // } - - // fn get_image_quality(&mut self) -> String { - // return self.lc.read().unwrap().image_quality.clone(); - // } - // TODO fn get_custom_image_quality(&mut self) -> Value { let mut v = Value::array(0); @@ -669,87 +644,6 @@ impl SciterSession { v } - // #[inline] - // pub(super) fn save_config(&self, config: PeerConfig) { - // self.lc.write().unwrap().save_config(config); - // } - - // fn save_view_style(&mut self, value: String) { - // self.lc.write().unwrap().save_view_style(value); - // } - - // #[inline] - // pub(super) fn load_config(&self) -> PeerConfig { - // load_config(&self.id) - // } - - // fn toggle_option(&mut self, name: String) { - // let msg = self.lc.write().unwrap().toggle_option(name.clone()); - // if name == "enable-file-transfer" { - // self.send(Data::ToggleClipboardFile); - // } - // if let Some(msg) = msg { - // self.send(Data::Message(msg)); - // } - // } - - // fn get_toggle_option(&mut self, name: String) -> bool { - // self.lc.read().unwrap().get_toggle_option(&name) - // } - - // fn is_privacy_mode_supported(&self) -> bool { - // self.lc.read().unwrap().is_privacy_mode_supported() - // } - - // fn refresh_video(&mut self) { - // self.send(Data::Message(LoginConfigHandler::refresh())); - // } - - // fn save_custom_image_quality(&mut self, custom_image_quality: i32) { - // let msg = self - // .lc - // .write() - // .unwrap() - // .save_custom_image_quality(custom_image_quality); - // self.send(Data::Message(msg)); - // } - - // fn save_image_quality(&mut self, value: String) { - // let msg = self.lc.write().unwrap().save_image_quality(value); - // if let Some(msg) = msg { - // self.send(Data::Message(msg)); - // } - // } - - // fn get_remember(&mut self) -> bool { - // self.lc.read().unwrap().remember - // } - - // fn set_write_override( - // &mut self, - // job_id: i32, - // file_num: i32, - // is_override: bool, - // remember: bool, - // is_upload: bool, - // ) -> bool { - // self.send(Data::SetConfirmOverrideFile(( - // job_id, - // file_num, - // is_override, - // remember, - // is_upload, - // ))); - // true - // } - - // fn has_hwcodec(&self) -> bool { - // #[cfg(not(feature = "hwcodec"))] - // return false; - // #[cfg(feature = "hwcodec")] - // return true; - // } - // TODO fn supported_hwcodec(&self) -> Value { #[cfg(feature = "hwcodec")] @@ -775,51 +669,6 @@ impl SciterSession { } } - // fn change_prefer_codec(&self) { - // let msg = self.lc.write().unwrap().change_prefer_codec(); - // self.send(Data::Message(msg)); - // } - - // fn restart_remote_device(&mut self) { - // let mut lc = self.lc.write().unwrap(); - // lc.restarting_remote_device = true; - // let msg = lc.restart_remote_device(); - // self.send(Data::Message(msg)); - // } - - // pub fn is_restarting_remote_device(&self) -> bool { - // self.lc.read().unwrap().restarting_remote_device - // } - - // fn t(&self, name: String) -> String { - // crate::client::translate(name) - // } - - // fn get_audit_server(&self) -> String { - // if self.lc.read().unwrap().conn_id <= 0 - // || LocalConfig::get_option("access_token").is_empty() - // { - // return "".to_owned(); - // } - // crate::get_audit_server( - // Config::get_option("api-server"), - // Config::get_option("custom-rendezvous-server"), - // ) - // } - - // fn send_note(&self, note: String) { - // let url = self.get_audit_server(); - // let id = self.id.clone(); - // let conn_id = self.lc.read().unwrap().conn_id; - // std::thread::spawn(move || { - // send_note(url, id, conn_id, note); - // }); - // } - - // fn is_xfce(&self) -> bool { - // crate::platform::is_xfce() - // } - // TODO fn save_size(&mut self, x: i32, y: i32, w: i32, h: i32) { let size = (x, y, w, h); @@ -881,34 +730,6 @@ impl SciterSession { v } - // fn remove_port_forward(&mut self, port: i32) { - // let mut config = self.load_config(); - // config.port_forwards = config - // .port_forwards - // .drain(..) - // .filter(|x| x.0 != port) - // .collect(); - // self.save_config(config); - // self.send(Data::RemovePortForward(port)); - // } - - // fn add_port_forward(&mut self, port: i32, remote_host: String, remote_port: i32) { - // let mut config = self.load_config(); - // if config - // .port_forwards - // .iter() - // .filter(|x| x.0 == port) - // .next() - // .is_some() - // { - // return; - // } - // let pf = (port, remote_host, remote_port); - // config.port_forwards.push(pf.clone()); - // self.save_config(config); - // self.send(Data::AddPortForward(pf)); - // } - fn get_size(&mut self) -> Value { let s = if self.is_file_transfer() { self.lc.read().unwrap().size_ft @@ -925,10 +746,6 @@ impl SciterSession { v } - // fn get_id(&mut self) -> String { - // self.id.clone() - // } - fn get_default_pi(&mut self) -> Value { let mut pi = Value::map(); let info = self.lc.read().unwrap().info.clone(); @@ -938,158 +755,11 @@ impl SciterSession { pi } - // fn get_option(&self, k: String) -> String { - // self.lc.read().unwrap().get_option(&k) - // } - - // fn set_option(&self, k: String, v: String) { - // self.lc.write().unwrap().set_option(k, v); - // } - - // fn input_os_password(&mut self, pass: String, activate: bool) { - // input_os_password(pass, activate, self.clone()); - // } - // close_state sciter only fn save_close_state(&mut self, k: String, v: String) { self.close_state.insert(k, v); } - // fn get_chatbox(&mut self) -> String { - // #[cfg(feature = "inline")] - // return super::inline::get_chatbox(); - // #[cfg(not(feature = "inline"))] - // return "".to_owned(); - // } - - // fn get_icon(&mut self) -> String { - // crate::get_icon() - // } - - // fn send_chat(&mut self, text: String) { - // let mut misc = Misc::new(); - // misc.set_chat_message(ChatMessage { - // text, - // ..Default::default() - // }); - // let mut msg_out = Message::new(); - // msg_out.set_misc(misc); - // self.send(Data::Message(msg_out)); - // } - - // fn switch_display(&mut self, display: i32) { - // let mut misc = Misc::new(); - // misc.set_switch_display(SwitchDisplay { - // display, - // ..Default::default() - // }); - // let mut msg_out = Message::new(); - // msg_out.set_misc(misc); - // self.send(Data::Message(msg_out)); - // } - - // fn is_file_transfer(&self) -> bool { - // self.cmd == "--file-transfer" - // } - - // fn is_port_forward(&self) -> bool { - // self.cmd == "--port-forward" || self.is_rdp() - // } - - // fn is_rdp(&self) -> bool { - // self.cmd == "--rdp" - // } - - // fn reconnect(&mut self) { - // println!("reconnecting"); - // let cloned = self.clone(); - // let mut lock = self.thread.lock().unwrap(); - // lock.take().map(|t| t.join()); - // *lock = Some(std::thread::spawn(move || { - // io_loop(cloned); - // })); - // } - - // #[inline] - // fn peer_platform(&self) -> String { - // self.lc.read().unwrap().info.platform.clone() - // } - - // fn get_platform(&mut self, is_remote: bool) -> String { - // if is_remote { - // self.peer_platform() - // } else { - // whoami::platform().to_string() - // } - // } - - // fn get_path_sep(&mut self, is_remote: bool) -> &'static str { - // let p = self.get_platform(is_remote); - // if &p == "Windows" { - // return "\\"; - // } else { - // return "/"; - // } - // } - - // fn get_icon_path(&mut self, file_type: i32, ext: String) -> String { - // let mut path = Config::icon_path(); - // if file_type == FileType::DirLink as i32 { - // let new_path = path.join("dir_link"); - // if !std::fs::metadata(&new_path).is_ok() { - // #[cfg(windows)] - // allow_err!(std::os::windows::fs::symlink_file(&path, &new_path)); - // #[cfg(not(windows))] - // allow_err!(std::os::unix::fs::symlink(&path, &new_path)); - // } - // path = new_path; - // } else if file_type == FileType::File as i32 { - // if !ext.is_empty() { - // path = path.join(format!("file.{}", ext)); - // } else { - // path = path.join("file"); - // } - // if !std::fs::metadata(&path).is_ok() { - // allow_err!(std::fs::File::create(&path)); - // } - // } else if file_type == FileType::FileLink as i32 { - // let new_path = path.join("file_link"); - // if !std::fs::metadata(&new_path).is_ok() { - // path = path.join("file"); - // if !std::fs::metadata(&path).is_ok() { - // allow_err!(std::fs::File::create(&path)); - // } - // #[cfg(windows)] - // allow_err!(std::os::windows::fs::symlink_file(&path, &new_path)); - // #[cfg(not(windows))] - // allow_err!(std::os::unix::fs::symlink(&path, &new_path)); - // } - // path = new_path; - // } else if file_type == FileType::DirDrive as i32 { - // if cfg!(windows) { - // path = fs::get_path("C:"); - // } else if cfg!(target_os = "macos") { - // if let Ok(entries) = fs::get_path("/Volumes/").read_dir() { - // for entry in entries { - // if let Ok(entry) = entry { - // path = entry.path(); - // break; - // } - // } - // } - // } - // } - // fs::get_string(&path) - // } - - // fn login(&mut self, password: String, remember: bool) { - // self.send(Data::Login((password, remember))); - // } - - // fn new_rdp(&mut self) { - // self.send(Data::NewRDP); - // } - fn enter(&mut self) { #[cfg(windows)] crate::platform::windows::stop_system_key_propagate(true); @@ -1102,30 +772,6 @@ impl SciterSession { IS_IN.store(false, Ordering::SeqCst); } - // TODO - fn set_cursor_data(&mut self, cd: CursorData) { - let mut colors = hbb_common::compress::decompress(&cd.colors); - if colors.iter().filter(|x| **x != 0).next().is_none() { - log::info!("Fix transparent"); - // somehow all 0 images shows black rect, here is a workaround - colors[3] = 1; - } - let mut png = Vec::new(); - if let Ok(()) = repng::encode(&mut png, cd.width as _, cd.height as _, &colors) { - self.call( - "setCursorData", - &make_args!( - cd.id.to_string(), - cd.hotx, - cd.hoty, - cd.width, - cd.height, - &png[..] - ), - ); - } - } - fn get_key_event(&self, down_or_up: i32, name: &str, code: i32) -> Option { let mut key_event = KeyEvent::new(); if down_or_up == 2 { @@ -1260,21 +906,6 @@ impl SciterSession { log::error!("Failed to spawn IP tunneling: {}", err); } } - - // #[inline] - // fn set_cursor_id(&mut self, id: String) { - // self.call("setCursorId", &make_args!(id)); - // } - - // #[inline] - // fn set_cursor_position(&mut self, cd: CursorPosition) { - // self.call("setCursorPosition", &make_args!(cd.x, cd.y)); - // } - - // #[inline] - // fn set_display(&self, x: i32, y: i32, w: i32, h: i32) { - // self.call("setDisplay", &make_args!(x, y, w, h)); - // } } pub fn make_fd(id: i32, entries: &Vec, only_count: bool) -> Value { diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 03666ed92..a164a2d94 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -1,33 +1,23 @@ +use crate::client::io_loop::Remote; use crate::client::{ - self, check_if_retry, get_key_state, handle_hash, handle_login_from_ui, handle_test_delay, - input_os_password, load_config, send_mouse, start_video_audio_threads, Client, CodecFormat, - FileManager, Key, LoginConfigHandler, MediaData, MediaSender, QualityStatus, KEY_MAP, SEC30, + check_if_retry, get_key_state, handle_hash, 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, check_clipboard, update_clipboard, ClipboardContext, CLIPBOARD_INTERVAL, -}; -use crate::platform; +use crate::common; + use crate::{client::Data, client::Interface}; use async_trait::async_trait; -use enigo::{Enigo, KeyboardControllable}; -use hbb_common::config::{Config, LocalConfig, PeerConfig, TransferSerde}; -use hbb_common::fs::{ - can_enable_overwrite_detection, get_job, get_string, new_send_confirm, DigestCheckResult, - RemoveJobMeta, TransferJobMeta, -}; -use hbb_common::message_proto::permission_info::Permission; -use hbb_common::protobuf::Message as _; -use hbb_common::rendezvous_proto::ConnType; -use hbb_common::tokio::{ - self, - sync::mpsc, - time::{self, Duration, Instant, Interval}, -}; -use hbb_common::{allow_err, message_proto::*, sleep}; + +use hbb_common::config::{Config, LocalConfig, PeerConfig}; + +use hbb_common::tokio::{self, sync::mpsc}; + +use hbb_common::{allow_err, message_proto::*}; use hbb_common::{fs, get_version_number, log, Stream}; use std::collections::HashMap; use std::ops::{Deref, DerefMut}; -use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; +use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::{Arc, Mutex, RwLock}; #[derive(Clone, Default)] @@ -541,6 +531,7 @@ pub trait InvokeUi: Send + Sync + Clone + 'static + Sized + Default { fn set_cursor_id(&self, id: String); fn set_cursor_position(&self, cp: CursorPosition); fn set_display(&self, x: i32, y: i32, w: i32, h: i32); + fn switch_display(&self, display: &SwitchDisplay); fn set_peer_info( &self, username: &str, @@ -570,14 +561,18 @@ pub trait InvokeUi: Send + Sync + Clone + 'static + Sized + Default { show_hidden: bool, is_remote: bool, ); + fn new_message(&self, msg: String); fn update_transfer_list(&self); - // fn update_folder_files(&self); // TODO + // fn update_folder_files(&self); // TODO flutter with file_dir and update_folder_files fn confirm_delete_files(&self, id: i32, i: i32, name: String); fn override_file_confirm(&self, id: i32, file_num: i32, to: String, is_upload: bool); + 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 msgbox(&self, msgtype: &str, title: &str, text: &str, retry: bool); + #[cfg(any(target_os = "android", target_os = "ios"))] + fn clipboard(&self, content: String); } impl Deref for Session { @@ -604,21 +599,23 @@ impl Interface for Session { } } + // TODO flutter fn is_file_transfer(&self) -> bool { self.cmd == "--file-transfer" } + // TODO flutter fn is_port_forward(&self) -> bool { self.cmd == "--port-forward" || self.is_rdp() } + // TODO flutter fn is_rdp(&self) -> bool { self.cmd == "--rdp" } fn msgbox(&self, msgtype: &str, title: &str, text: &str) { let retry = check_if_retry(msgtype, title, text); - // self.call2("msgbox_retry", &make_args!(msgtype, title, text, retry)); self.ui_handler.msgbox(msgtype, title, text, retry); } @@ -916,1217 +913,6 @@ async fn start_one_port_forward( log::info!("port forward (:{}) exit", port); } -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); -const MILLI1: Duration = Duration::from_millis(1); - -pub struct Remote { - handler: Session, - video_sender: MediaSender, - audio_sender: MediaSender, - receiver: mpsc::UnboundedReceiver, - sender: mpsc::UnboundedSender, - old_clipboard: Arc>, - read_jobs: Vec, - write_jobs: Vec, - remove_jobs: HashMap, - timer: Interval, - last_update_jobs_status: (Instant, HashMap), - first_frame: bool, - #[cfg(windows)] - clipboard_file_context: Option>, - data_count: Arc, - frame_count: Arc, - video_format: CodecFormat, -} - -impl Remote { - pub fn new( - handler: Session, - video_sender: MediaSender, - audio_sender: MediaSender, - receiver: mpsc::UnboundedReceiver, - sender: mpsc::UnboundedSender, - frame_count: Arc, - ) -> Self { - Self { - handler, - video_sender, - audio_sender, - receiver, - sender, - old_clipboard: Default::default(), - read_jobs: Vec::new(), - write_jobs: Vec::new(), - remove_jobs: Default::default(), - timer: time::interval(SEC30), - last_update_jobs_status: (Instant::now(), Default::default()), - first_frame: false, - #[cfg(windows)] - clipboard_file_context: None, - data_count: Arc::new(AtomicUsize::new(0)), - frame_count, - video_format: CodecFormat::Unknown, - } - } - - 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() { - ConnType::FILE_TRANSFER - } else { - ConnType::default() - }; - match Client::start( - &self.handler.id, - key, - token, - conn_type, - self.handler.clone(), - ) - .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 - // .call("setConnectionType", &make_args!(peer.is_secured(), direct)); - self.handler.set_connection_type(peer.is_secured(), direct); // flutter -> connection_ready - - // just build for now - #[cfg(not(windows))] - let (_tx_holder, mut rx_clip_client) = mpsc::unbounded_channel::(); - #[cfg(windows)] - let mut rx_clip_client = get_rx_clip_client().lock().await; - - let mut status_timer = time::interval(Duration::new(1, 0)); - - loop { - tokio::select! { - res = peer.next() => { - if let Some(res) = res { - match res { - Err(err) => { - log::error!("Connection closed: {}", err); - self.handler.set_force_relay(direct, received); - self.handler.msgbox("error", "Connection Error", &err.to_string()); - break; - } - Ok(ref bytes) => { - last_recv_time = Instant::now(); - received = true; - self.data_count.fetch_add(bytes.len(), Ordering::Relaxed); - if !self.handle_msg_from_peer(bytes, &mut peer).await { - break - } - } - } - } else { - if self.handler.is_restarting_remote_device() { - log::info!("Restart remote device"); - self.handler.msgbox("restarting", "Restarting Remote Device", "remote_restarting_tip"); - } else { - log::info!("Reset by the peer"); - self.handler.msgbox("error", "Connection Error", "Reset by the peer"); - } - break; - } - } - d = self.receiver.recv() => { - if let Some(d) = d { - if !self.handle_msg_from_ui(d, &mut peer).await { - break; - } - } - } - _msg = rx_clip_client.recv() => { - #[cfg(windows)] - match _msg { - Some((_, clip)) => { - allow_err!(peer.send(&clip_2_msg(clip)).await); - } - None => { - // unreachable!() - } - } - } - _ = self.timer.tick() => { - if last_recv_time.elapsed() >= SEC30 { - self.handler.msgbox("error", "Connection Error", "Timeout"); - break; - } - if !self.read_jobs.is_empty() { - if let Err(err) = fs::handle_read_jobs(&mut self.read_jobs, &mut peer).await { - self.handler.msgbox("error", "Connection Error", &err.to_string()); - break; - } - self.update_jobs_status(); - } else { - self.timer = time::interval_at(Instant::now() + SEC30, SEC30); - } - } - _ = status_timer.tick() => { - let speed = self.data_count.swap(0, Ordering::Relaxed); - let speed = format!("{:.2}kB/s", speed as f32 / 1024 as f32); - let fps = self.frame_count.swap(0, Ordering::Relaxed) as _; - self.handler.update_quality_status(QualityStatus { - speed:Some(speed), - fps:Some(fps), - ..Default::default() - }); - } - } - } - log::debug!("Exit io_loop of id={}", self.handler.id); - } - Err(err) => { - self.handler - .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); - } - - fn handle_job_status(&mut self, id: i32, file_num: i32, err: Option) { - if let Some(job) = self.remove_jobs.get_mut(&id) { - if job.no_confirm { - let file_num = (file_num + 1) as usize; - if file_num < job.files.len() { - let path = format!("{}{}{}", job.path, job.sep, job.files[file_num].name); - self.sender - .send(Data::RemoveFile((id, path, file_num as i32, job.is_remote))) - .ok(); - let elapsed = job.last_update_job_status.elapsed().as_millis() as i32; - if elapsed >= 1000 { - job.last_update_job_status = Instant::now(); - } else { - return; - } - } else { - self.remove_jobs.remove(&id); - } - } - } - if let Some(err) = err { - // self.handler - // .call("jobError", &make_args!(id, err, file_num)); - self.handler.job_error(id, err, file_num); - } else { - // self.handler.call("jobDone", &make_args!(id, file_num)); - self.handler.job_done(id, file_num); - } - } - - 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(); - 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 - { - 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) - } - - fn load_last_jobs(&mut self) { - log::info!("start load last jobs"); - // self.handler.call("clearAllJobs", &make_args!()); - self.handler.clear_all_jobs(); - let pc = self.handler.load_config(); - if pc.transfer.write_jobs.is_empty() && pc.transfer.read_jobs.is_empty() { - // no last jobs - return; - } - // TODO: can add a confirm dialog - let mut cnt = 1; - for job_str in pc.transfer.read_jobs.iter() { - let job: Result = serde_json::from_str(&job_str); - if let Ok(job) = job { - self.handler.add_job( - cnt, - job.to.clone(), - job.remote.clone(), - job.file_num, - job.show_hidden, - false, - ); - cnt += 1; - println!("restore read_job: {:?}", job); - } - } - for job_str in pc.transfer.write_jobs.iter() { - let job: Result = serde_json::from_str(&job_str); - if let Ok(job) = job { - self.handler.add_job( - cnt, - job.remote.clone(), - job.to.clone(), - job.file_num, - job.show_hidden, - true, - ); - cnt += 1; - println!("restore write_job: {:?}", job); - } - } - // self.handler.call("updateTransferList", &make_args!()); - self.handler.update_transfer_list(); - } - - async fn handle_msg_from_ui(&mut self, data: Data, peer: &mut Stream) -> bool { - match data { - Data::Close => { - let mut misc = Misc::new(); - misc.set_close_reason("".to_owned()); - let mut msg = Message::new(); - msg.set_misc(misc); - allow_err!(peer.send(&msg).await); - return false; - } - Data::Login((password, remember)) => { - self.handler - .handle_login_from_ui(password, remember, peer) - .await; - } - Data::ToggleClipboardFile => { - self.check_clipboard_file_context(); - } - Data::Message(msg) => { - allow_err!(peer.send(&msg).await); - } - Data::SendFiles((id, path, to, file_num, include_hidden, is_remote)) => { - log::info!("send files, is remote {}", is_remote); - let od = can_enable_overwrite_detection(self.handler.lc.read().unwrap().version); - if is_remote { - log::debug!("New job {}, write to {} from remote {}", id, to, path); - self.write_jobs.push(fs::TransferJob::new_write( - id, - path.clone(), - to, - file_num, - include_hidden, - is_remote, - Vec::new(), - od, - )); - allow_err!( - peer.send(&fs::new_send(id, path, file_num, include_hidden)) - .await - ); - } else { - match fs::TransferJob::new_read( - id, - to.clone(), - path.clone(), - file_num, - include_hidden, - is_remote, - od, - ) { - Err(err) => { - self.handle_job_status(id, -1, Some(err.to_string())); - } - Ok(job) => { - log::debug!( - "New job {}, read {} to remote {}, {} files", - id, - path, - to, - job.files().len() - ); - // let m = make_fd(job.id(), job.files(), true); - // self.handler.call("updateFolderFiles", &make_args!(m)); // TODO - #[cfg(not(windows))] - let files = job.files().clone(); - #[cfg(windows)] - let mut files = job.files().clone(); - #[cfg(windows)] - if self.handler.peer_platform() != "Windows" { - // peer is not windows, need transform \ to / - fs::transform_windows_path(&mut files); - } - self.read_jobs.push(job); - self.timer = time::interval(MILLI1); - allow_err!(peer.send(&fs::new_receive(id, to, file_num, files)).await); - } - } - } - } - Data::AddJob((id, path, to, file_num, include_hidden, is_remote)) => { - let od = can_enable_overwrite_detection(self.handler.lc.read().unwrap().version); - if is_remote { - log::debug!( - "new write waiting job {}, write to {} from remote {}", - id, - to, - path - ); - let mut job = fs::TransferJob::new_write( - id, - path.clone(), - to, - file_num, - include_hidden, - is_remote, - Vec::new(), - od, - ); - job.is_last_job = true; - self.write_jobs.push(job); - } else { - match fs::TransferJob::new_read( - id, - to.clone(), - path.clone(), - file_num, - include_hidden, - is_remote, - od, - ) { - Err(err) => { - self.handle_job_status(id, -1, Some(err.to_string())); - } - Ok(mut job) => { - log::debug!( - "new read waiting job {}, read {} to remote {}, {} files", - id, - path, - to, - job.files().len() - ); - // let m = make_fd(job.id(), job.files(), true); - // self.handler.call("updateFolderFiles", &make_args!(m)); - job.is_last_job = true; - self.read_jobs.push(job); - self.timer = time::interval(MILLI1); - } - } - } - } - Data::ResumeJob((id, is_remote)) => { - if is_remote { - if let Some(job) = get_job(id, &mut self.write_jobs) { - job.is_last_job = false; - allow_err!( - peer.send(&fs::new_send( - id, - job.remote.clone(), - job.file_num, - job.show_hidden - )) - .await - ); - } - } else { - if let Some(job) = get_job(id, &mut self.read_jobs) { - job.is_last_job = false; - allow_err!( - peer.send(&fs::new_receive( - id, - job.path.to_string_lossy().to_string(), - job.file_num, - job.files.clone() - )) - .await - ); - } - } - } - Data::SetNoConfirm(id) => { - if let Some(job) = self.remove_jobs.get_mut(&id) { - job.no_confirm = true; - } - } - Data::ConfirmDeleteFiles((id, file_num)) => { - if let Some(job) = self.remove_jobs.get_mut(&id) { - let i = file_num as usize; - if i < job.files.len() { - // self.handler.call( - // "confirmDeleteFiles", - // &make_args!(id, file_num, job.files[i].name.clone()), - // ); - self.handler.confirm_delete_files(id, file_num); - } - } - } - Data::SetConfirmOverrideFile((id, file_num, need_override, remember, is_upload)) => { - if is_upload { - if let Some(job) = fs::get_job(id, &mut self.read_jobs) { - if remember { - job.set_overwrite_strategy(Some(need_override)); - } - job.confirm(&FileTransferSendConfirmRequest { - id, - file_num, - union: if need_override { - Some(file_transfer_send_confirm_request::Union::OffsetBlk(0)) - } else { - Some(file_transfer_send_confirm_request::Union::Skip(true)) - }, - ..Default::default() - }); - } - } else { - if let Some(job) = fs::get_job(id, &mut self.write_jobs) { - if remember { - job.set_overwrite_strategy(Some(need_override)); - } - let mut msg = Message::new(); - let mut file_action = FileAction::new(); - file_action.set_send_confirm(FileTransferSendConfirmRequest { - id, - file_num, - union: if need_override { - Some(file_transfer_send_confirm_request::Union::OffsetBlk(0)) - } else { - Some(file_transfer_send_confirm_request::Union::Skip(true)) - }, - ..Default::default() - }); - msg.set_file_action(file_action); - allow_err!(peer.send(&msg).await); - } - } - } - Data::RemoveDirAll((id, path, is_remote, include_hidden)) => { - let sep = self.handler.get_path_sep(is_remote); - if is_remote { - let mut msg_out = Message::new(); - let mut file_action = FileAction::new(); - file_action.set_all_files(ReadAllFiles { - id, - path: path.clone(), - include_hidden, - ..Default::default() - }); - msg_out.set_file_action(file_action); - allow_err!(peer.send(&msg_out).await); - self.remove_jobs - .insert(id, RemoveJob::new(Vec::new(), path, sep, is_remote)); - } else { - match fs::get_recursive_files(&path, include_hidden) { - Ok(entries) => { - // let m = make_fd(id, &entries, true); - // self.handler.call("updateFolderFiles", &make_args!(m)); - self.remove_jobs - .insert(id, RemoveJob::new(entries, path, sep, is_remote)); - } - Err(err) => { - self.handle_job_status(id, -1, Some(err.to_string())); - } - } - } - } - Data::CancelJob(id) => { - let mut msg_out = Message::new(); - let mut file_action = FileAction::new(); - file_action.set_cancel(FileTransferCancel { - id: id, - ..Default::default() - }); - msg_out.set_file_action(file_action); - allow_err!(peer.send(&msg_out).await); - if let Some(job) = fs::get_job(id, &mut self.write_jobs) { - job.remove_download_file(); - fs::remove_job(id, &mut self.write_jobs); - } - fs::remove_job(id, &mut self.read_jobs); - self.remove_jobs.remove(&id); - } - Data::RemoveDir((id, path)) => { - let mut msg_out = Message::new(); - let mut file_action = FileAction::new(); - file_action.set_remove_dir(FileRemoveDir { - id, - path, - recursive: true, - ..Default::default() - }); - msg_out.set_file_action(file_action); - allow_err!(peer.send(&msg_out).await); - } - Data::RemoveFile((id, path, file_num, is_remote)) => { - if is_remote { - let mut msg_out = Message::new(); - let mut file_action = FileAction::new(); - file_action.set_remove_file(FileRemoveFile { - id, - path, - file_num, - ..Default::default() - }); - msg_out.set_file_action(file_action); - allow_err!(peer.send(&msg_out).await); - } else { - match fs::remove_file(&path) { - Err(err) => { - self.handle_job_status(id, file_num, Some(err.to_string())); - } - Ok(()) => { - self.handle_job_status(id, file_num, None); - } - } - } - } - Data::CreateDir((id, path, is_remote)) => { - if is_remote { - let mut msg_out = Message::new(); - let mut file_action = FileAction::new(); - file_action.set_create(FileDirCreate { - id, - path, - ..Default::default() - }); - msg_out.set_file_action(file_action); - allow_err!(peer.send(&msg_out).await); - } else { - match fs::create_dir(&path) { - Err(err) => { - self.handle_job_status(id, -1, Some(err.to_string())); - } - Ok(()) => { - self.handle_job_status(id, -1, None); - } - } - } - } - _ => {} - } - true - } - - #[inline] - fn update_job_status( - job: &fs::TransferJob, - elapsed: i32, - last_update_jobs_status: &mut (Instant, HashMap), - handler: &mut Session, - ) { - if elapsed <= 0 { - return; - } - let transferred = job.transferred(); - let last_transferred = { - if let Some(v) = last_update_jobs_status.1.get(&job.id()) { - v.to_owned() - } else { - 0 - } - }; - last_update_jobs_status.1.insert(job.id(), transferred); - let speed = (transferred - last_transferred) as f64 / (elapsed as f64 / 1000.); - let file_num = job.file_num() - 1; - // handler.call( - // "jobProgress", - // &make_args!(job.id(), file_num, speed, job.finished_size() as f64), - // ); - handler.job_progress(job.id(), file_num, speed, job.finished_size() as f64); - } - - fn update_jobs_status(&mut self) { - let elapsed = self.last_update_jobs_status.0.elapsed().as_millis() as i32; - if elapsed >= 1000 { - for job in self.read_jobs.iter() { - Self::update_job_status( - job, - elapsed, - &mut self.last_update_jobs_status, - &mut self.handler, - ); - } - for job in self.write_jobs.iter() { - Self::update_job_status( - job, - elapsed, - &mut self.last_update_jobs_status, - &mut self.handler, - ); - } - self.last_update_jobs_status.0 = Instant::now(); - } - } - - pub async fn sync_jobs_status_to_local(&mut self) -> bool { - log::info!("sync transfer job status"); - let mut config: PeerConfig = self.handler.load_config(); - let mut transfer_metas = TransferSerde::default(); - for job in self.read_jobs.iter() { - let json_str = serde_json::to_string(&job.gen_meta()).unwrap_or_default(); - transfer_metas.read_jobs.push(json_str); - } - for job in self.write_jobs.iter() { - let json_str = serde_json::to_string(&job.gen_meta()).unwrap_or_default(); - transfer_metas.write_jobs.push(json_str); - } - log::info!("meta: {:?}", transfer_metas); - config.transfer = transfer_metas; - self.handler.save_config(config); - true - } - - async fn send_opts_after_login(&self, peer: &mut Stream) { - if let Some(opts) = self - .handler - .lc - .read() - .unwrap() - .get_option_message_after_login() - { - let mut misc = Misc::new(); - misc.set_option(opts); - let mut msg_out = Message::new(); - msg_out.set_misc(misc); - allow_err!(peer.send(&msg_out).await); - } - } - - async fn handle_msg_from_peer(&mut self, data: &[u8], peer: &mut Stream) -> bool { - if let Ok(msg_in) = Message::parse_from_bytes(&data) { - match msg_in.union { - Some(message::Union::VideoFrame(vf)) => { - if !self.first_frame { - self.first_frame = true; - // self.handler.call2("closeSuccess", &make_args!()); - self.handler.close_success(); - // self.handler.call("adaptSize", &make_args!()); - self.handler.adapt_size(); - self.send_opts_after_login(peer).await; - } - let incomming_format = CodecFormat::from(&vf); - if self.video_format != incomming_format { - self.video_format = incomming_format.clone(); - self.handler.update_quality_status(QualityStatus { - codec_format: Some(incomming_format), - ..Default::default() - }) - }; - self.video_sender.send(MediaData::VideoFrame(vf)).ok(); - } - Some(message::Union::Hash(hash)) => { - self.handler - .handle_hash(&self.handler.password.clone(), hash, peer) - .await; - } - Some(message::Union::LoginResponse(lr)) => match lr.union { - Some(login_response::Union::Error(err)) => { - if !self.handler.handle_login_error(&err) { - return false; - } - } - 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) - // { - // 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.load_last_jobs().await; - // } - } - _ => {} - }, - Some(message::Union::CursorData(cd)) => { - self.handler.set_cursor_data(cd); - } - Some(message::Union::CursorId(id)) => { - self.handler.set_cursor_id(id.to_string()); - } - Some(message::Union::CursorPosition(cp)) => { - self.handler.set_cursor_position(cp); - } - Some(message::Union::Clipboard(cb)) => { - if !self.handler.lc.read().unwrap().disable_clipboard { - update_clipboard(cb, Some(&self.old_clipboard)); - } - } - #[cfg(windows)] - Some(message::Union::Cliprdr(clip)) => { - if !self.handler.lc.read().unwrap().disable_clipboard { - if let Some(context) = &mut self.clipboard_file_context { - if let Some(clip) = msg_2_clip(clip) { - server_clip_file(context, 0, clip); - } - } - } - } - Some(message::Union::FileResponse(fr)) => { - match fr.union { - Some(file_response::Union::Dir(fd)) => { - #[cfg(windows)] - let entries = fd.entries.to_vec(); - #[cfg(not(windows))] - let mut entries = fd.entries.to_vec(); - #[cfg(not(windows))] - { - if self.handler.peer_platform() == "Windows" { - fs::transform_windows_path(&mut entries); - } - } - // let mut m = make_fd(fd.id, &entries, fd.id > 0); - // if fd.id <= 0 { - // m.set_item("path", fd.path); - // } - // self.handler.call("updateFolderFiles", &make_args!(m)); - if let Some(job) = fs::get_job(fd.id, &mut self.write_jobs) { - log::info!("job set_files: {:?}", entries); - job.set_files(entries); - } else if let Some(job) = self.remove_jobs.get_mut(&fd.id) { - job.files = entries; - } - } - Some(file_response::Union::Digest(digest)) => { - if digest.is_upload { - if let Some(job) = fs::get_job(digest.id, &mut self.read_jobs) { - if let Some(file) = job.files().get(digest.file_num as usize) { - let read_path = get_string(&job.join(&file.name)); - let overwrite_strategy = job.default_overwrite_strategy(); - if let Some(overwrite) = overwrite_strategy { - let req = FileTransferSendConfirmRequest { - id: digest.id, - file_num: digest.file_num, - union: Some(if overwrite { - file_transfer_send_confirm_request::Union::OffsetBlk(0) - } else { - file_transfer_send_confirm_request::Union::Skip( - true, - ) - }), - ..Default::default() - }; - job.confirm(&req); - let msg = new_send_confirm(req); - allow_err!(peer.send(&msg).await); - } else { - // self.handler.call( - // "overrideFileConfirm", - // &make_args!( - // digest.id, - // digest.file_num, - // read_path, - // true - // ), - // ); - self.handler.override_file_confirm( - digest.id, - digest.file_num, - read_path, - true, - ); - } - } - } - } else { - if let Some(job) = fs::get_job(digest.id, &mut self.write_jobs) { - if let Some(file) = job.files().get(digest.file_num as usize) { - let write_path = get_string(&job.join(&file.name)); - let overwrite_strategy = job.default_overwrite_strategy(); - match fs::is_write_need_confirmation(&write_path, &digest) { - Ok(res) => match res { - DigestCheckResult::IsSame => { - let msg= new_send_confirm(FileTransferSendConfirmRequest { - id: digest.id, - file_num: digest.file_num, - union: Some(file_transfer_send_confirm_request::Union::Skip(true)), - ..Default::default() - }); - allow_err!(peer.send(&msg).await); - } - DigestCheckResult::NeedConfirm(digest) => { - if let Some(overwrite) = overwrite_strategy { - let msg = new_send_confirm( - FileTransferSendConfirmRequest { - id: digest.id, - file_num: digest.file_num, - union: Some(if overwrite { - file_transfer_send_confirm_request::Union::OffsetBlk(0) - } else { - file_transfer_send_confirm_request::Union::Skip(true) - }), - ..Default::default() - }, - ); - allow_err!(peer.send(&msg).await); - } else { - // self.handler.call( - // "overrideFileConfirm", - // &make_args!( - // digest.id, - // digest.file_num, - // write_path, - // false - // ), - // ); - self.handler.override_file_confirm( - digest.id, - digest.file_num, - write_path, - false, - ); - } - } - DigestCheckResult::NoSuchFile => { - let msg = new_send_confirm( - FileTransferSendConfirmRequest { - id: digest.id, - file_num: digest.file_num, - union: Some(file_transfer_send_confirm_request::Union::OffsetBlk(0)), - ..Default::default() - }, - ); - allow_err!(peer.send(&msg).await); - } - }, - Err(err) => { - println!("error recving digest: {}", err); - } - } - } - } - } - } - Some(file_response::Union::Block(block)) => { - log::info!( - "file response block, file id:{}, file num: {}", - block.id, - block.file_num - ); - if let Some(job) = fs::get_job(block.id, &mut self.write_jobs) { - if let Err(_err) = job.write(block, None).await { - // to-do: add "skip" for writing job - } - self.update_jobs_status(); - } - } - Some(file_response::Union::Done(d)) => { - if let Some(job) = fs::get_job(d.id, &mut self.write_jobs) { - job.modify_time(); - fs::remove_job(d.id, &mut self.write_jobs); - } - self.handle_job_status(d.id, d.file_num, None); - } - Some(file_response::Union::Error(e)) => { - self.handle_job_status(e.id, e.file_num, Some(e.error)); - } - _ => {} - } - } - Some(message::Union::Misc(misc)) => match misc.union { - Some(misc::Union::AudioFormat(f)) => { - self.audio_sender.send(MediaData::AudioFormat(f)).ok(); - } - Some(misc::Union::ChatMessage(c)) => { - // self.handler.call("newMessage", &make_args!(c.text)); // TODO - } - Some(misc::Union::PermissionInfo(p)) => { - 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); - // self.handler - // .call2("setPermission", &make_args!("keyboard", p.enabled)); - self.handler.set_permission("keyboard", p.enabled); - } - Permission::Clipboard => { - SERVER_CLIPBOARD_ENABLED.store(p.enabled, Ordering::SeqCst); - // self.handler - // .call2("setPermission", &make_args!("clipboard", p.enabled)); - self.handler.set_permission("clipboard", p.enabled); - } - Permission::Audio => { - // self.handler - // .call2("setPermission", &make_args!("audio", p.enabled)); - self.handler.set_permission("audio", p.enabled); - } - Permission::File => { - SERVER_FILE_TRANSFER_ENABLED.store(p.enabled, Ordering::SeqCst); - if !p.enabled && self.handler.is_file_transfer() { - return true; - } - self.check_clipboard_file_context(); - // self.handler - // .call2("setPermission", &make_args!("file", p.enabled)); - self.handler.set_permission("file", p.enabled); - } - Permission::Restart => { - // self.handler - // .call2("setPermission", &make_args!("restart", p.enabled)); - self.handler.set_permission("restart", p.enabled); - } - } - } - Some(misc::Union::SwitchDisplay(s)) => { - // self.handler.call("switchDisplay", &make_args!(s.display)); // TODO - self.video_sender.send(MediaData::Reset).ok(); - if s.width > 0 && s.height > 0 { - self.handler.set_display(s.x, s.y, s.width, s.height); - } - } - Some(misc::Union::CloseReason(c)) => { - self.handler.msgbox("error", "Connection Error", &c); - return false; - } - Some(misc::Union::BackNotification(notification)) => { - if !self.handle_back_notification(notification).await { - return false; - } - } - _ => {} - }, - Some(message::Union::TestDelay(t)) => { - self.handler.handle_test_delay(t, peer).await; - } - Some(message::Union::AudioFrame(frame)) => { - if !self.handler.lc.read().unwrap().disable_audio { - self.audio_sender.send(MediaData::AudioFrame(frame)).ok(); - } - } - Some(message::Union::FileAction(action)) => match action.union { - Some(file_action::Union::SendConfirm(c)) => { - if let Some(job) = fs::get_job(c.id, &mut self.read_jobs) { - job.confirm(&c); - } - } - _ => {} - }, - _ => {} - } - } - true - } - - async fn handle_back_notification(&mut self, notification: BackNotification) -> bool { - match notification.union { - Some(back_notification::Union::BlockInputState(state)) => { - self.handle_back_msg_block_input( - state.enum_value_or(back_notification::BlockInputState::BlkStateUnknown), - ) - .await; - } - Some(back_notification::Union::PrivacyModeState(state)) => { - if !self - .handle_back_msg_privacy_mode( - state.enum_value_or(back_notification::PrivacyModeState::PrvStateUnknown), - ) - .await - { - return false; - } - } - _ => {} - } - true - } - - #[inline(always)] - fn update_block_input_state(&mut self, on: bool) { - // self.handler.call("updateBlockInputState", &make_args!(on)); // TODO - } - - async fn handle_back_msg_block_input(&mut self, state: back_notification::BlockInputState) { - match state { - back_notification::BlockInputState::BlkOnSucceeded => { - self.update_block_input_state(true); - } - back_notification::BlockInputState::BlkOnFailed => { - self.handler - .msgbox("custom-error", "Block user input", "Failed"); - self.update_block_input_state(false); - } - back_notification::BlockInputState::BlkOffSucceeded => { - self.update_block_input_state(false); - } - back_notification::BlockInputState::BlkOffFailed => { - self.handler - .msgbox("custom-error", "Unblock user input", "Failed"); - } - _ => {} - } - } - - #[inline(always)] - fn update_privacy_mode(&mut self, on: bool) { - let mut config = self.handler.load_config(); - config.privacy_mode = on; - self.handler.save_config(config); - - // self.handler.call("updatePrivacyMode", &[]); - self.handler.update_privacy_mode(); - } - - async fn handle_back_msg_privacy_mode( - &mut self, - state: back_notification::PrivacyModeState, - ) -> bool { - match state { - back_notification::PrivacyModeState::PrvOnByOther => { - self.handler.msgbox( - "error", - "Connecting...", - "Someone turns on privacy mode, exit", - ); - return false; - } - back_notification::PrivacyModeState::PrvNotSupported => { - self.handler - .msgbox("custom-error", "Privacy mode", "Unsupported"); - self.update_privacy_mode(false); - } - back_notification::PrivacyModeState::PrvOnSucceeded => { - self.handler - .msgbox("custom-nocancel", "Privacy mode", "In privacy mode"); - self.update_privacy_mode(true); - } - back_notification::PrivacyModeState::PrvOnFailedDenied => { - self.handler - .msgbox("custom-error", "Privacy mode", "Peer denied"); - self.update_privacy_mode(false); - } - back_notification::PrivacyModeState::PrvOnFailedPlugin => { - self.handler - .msgbox("custom-error", "Privacy mode", "Please install plugins"); - self.update_privacy_mode(false); - } - back_notification::PrivacyModeState::PrvOnFailed => { - self.handler - .msgbox("custom-error", "Privacy mode", "Failed"); - self.update_privacy_mode(false); - } - back_notification::PrivacyModeState::PrvOffSucceeded => { - self.handler - .msgbox("custom-nocancel", "Privacy mode", "Out privacy mode"); - self.update_privacy_mode(false); - } - back_notification::PrivacyModeState::PrvOffByPeer => { - self.handler - .msgbox("custom-error", "Privacy mode", "Peer exit"); - self.update_privacy_mode(false); - } - back_notification::PrivacyModeState::PrvOffFailed => { - self.handler - .msgbox("custom-error", "Privacy mode", "Failed to turn off"); - } - back_notification::PrivacyModeState::PrvOffUnknown => { - self.handler - .msgbox("custom-error", "Privacy mode", "Turned off"); - // log::error!("Privacy mode is turned off with unknown reason"); - self.update_privacy_mode(false); - } - _ => {} - } - true - } - - fn check_clipboard_file_context(&mut self) { - #[cfg(windows)] - { - let enabled = SERVER_FILE_TRANSFER_ENABLED.load(Ordering::SeqCst) - && self.handler.lc.read().unwrap().enable_file_transfer; - if enabled == self.clipboard_file_context.is_none() { - self.clipboard_file_context = if enabled { - match create_clipboard_file_context(true, false) { - Ok(context) => { - log::info!("clipboard context for file transfer created."); - Some(context) - } - Err(err) => { - log::error!( - "Create clipboard context for file transfer: {}", - err.to_string() - ); - None - } - } - } else { - log::info!("clipboard context for file transfer destroyed."); - None - }; - } - } - } -} - -struct RemoveJob { - files: Vec, - path: String, - sep: &'static str, - is_remote: bool, - no_confirm: bool, - last_update_job_status: Instant, -} - -impl RemoveJob { - fn new(files: Vec, path: String, sep: &'static str, is_remote: bool) -> Self { - Self { - files, - path, - sep, - is_remote, - no_confirm: false, - last_update_job_status: Instant::now(), - } - } - - pub fn _gen_meta(&self) -> RemoveJobMeta { - RemoveJobMeta { - path: self.path.clone(), - is_remote: self.is_remote, - no_confirm: self.no_confirm, - } - } -} - #[tokio::main(flavor = "current_thread")] async fn send_note(url: String, id: String, conn_id: i32, note: String) { let body = serde_json::json!({ "id": id, "Id": conn_id, "note": note }); From 37dbfcc86d77ae09a44007815ed0ab387029cb0e Mon Sep 17 00:00:00 2001 From: Asura Date: Wed, 31 Aug 2022 23:07:52 -0700 Subject: [PATCH 0337/2015] Delete pynput from repo --- Cargo.lock | 14 +- DEBIAN/postinst | 3 - PKGBUILD | 1 - README-AR.md | 5 - README-CS.md | 6 - README-FA.md | 6 - README-HU.md | 6 - README-JP.md | 6 - README-KR.md | 6 - README-VN.md | 6 - README.md | 6 - appimage/AppImageBuilder.yml | 2 - appimage/requirements.txt | 1 - build.py | 6 - libs/enigo/src/linux/mod.rs | 1 - libs/enigo/src/linux/nix_impl.rs | 14 +- libs/enigo/src/linux/pynput.rs | 279 ------------------------------- pacman_install | 2 +- pynput_service.py | 240 -------------------------- rpm-suse.spec | 3 - rpm.spec | 3 - snap/snapcraft.yaml | 6 - src/flutter.rs | 1 - src/server/input_service.rs | 9 - src/ui/remote.rs | 2 - 25 files changed, 16 insertions(+), 618 deletions(-) delete mode 100644 appimage/requirements.txt delete mode 100644 libs/enigo/src/linux/pynput.rs delete mode 100644 pynput_service.py diff --git a/Cargo.lock b/Cargo.lock index 35d87cbd7..96ff5a811 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3985,7 +3985,7 @@ dependencies = [ "libc", "widestring 1.0.2", "winapi 0.3.9", - "x11", + "x11 2.20.0", ] [[package]] @@ -4993,11 +4993,12 @@ checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" [[package]] name = "tfc" version = "0.6.1" -source = "git+https://github.com/asur4s/The-Fat-Controller#25bfa7ef1cb0bd0b522cc4155dea6b99673bcfd4" +source = "git+https://github.com/asur4s/The-Fat-Controller#8ef82be83d8d941f08bdb84e77bea52290f92050" dependencies = [ "core-graphics 0.22.3", "unicode-segmentation", "winapi 0.3.9", + "x11 2.19.0", ] [[package]] @@ -5931,6 +5932,15 @@ dependencies = [ "tap", ] +[[package]] +name = "x11" +version = "2.19.0" +source = "git+https://github.com/bjornsnoen/x11-rs#c2e9bfaa7b196938f8700245564d8ac5d447786a" +dependencies = [ + "libc", + "pkg-config", +] + [[package]] name = "x11" version = "2.20.0" diff --git a/DEBIAN/postinst b/DEBIAN/postinst index 1c7697acc..d643c5caf 100755 --- a/DEBIAN/postinst +++ b/DEBIAN/postinst @@ -12,9 +12,6 @@ if [ "$1" = configure ]; then fi version=$(python3 -V 2>&1 | grep -Po '(?<=Python )(.+)') parsedVersion=$(echo "${version//./}") - if [[ "$parsedVersion" -gt "360" ]]; then - sudo -H pip3 install pynput - fi cp /usr/share/rustdesk/files/systemd/rustdesk.service /usr/lib/systemd/system/rustdesk.service systemctl daemon-reload systemctl enable rustdesk diff --git a/PKGBUILD b/PKGBUILD index 6fb65d48b..1d1956ede 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -26,6 +26,5 @@ package() { install -Dm 644 ${HBB}/libsciter-gtk.so -t "${pkgdir}/usr/lib/rustdesk" install -Dm 644 $HBB/rustdesk.service -t "${pkgdir}/usr/share/rustdesk/files" install -Dm 644 $HBB/rustdesk.desktop -t "${pkgdir}/usr/share/rustdesk/files" - install -Dm 644 $HBB/pynput_service.py -t "${pkgdir}/usr/share/rustdesk/files" install -Dm 644 $HBB/128x128@2x.png "${pkgdir}/usr/share/rustdesk/files/rustdesk.png" } diff --git a/README-AR.md b/README-AR.md index 2deb4914b..c0186037e 100644 --- a/README-AR.md +++ b/README-AR.md @@ -80,11 +80,6 @@ sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb- sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pulseaudio ``` -### pynput package تثبيت - -```sh -pip3 install pynput -``` ### vcpkg تثبيت diff --git a/README-CS.md b/README-CS.md index 4f2c0e80f..7ad86e08b 100644 --- a/README-CS.md +++ b/README-CS.md @@ -75,12 +75,6 @@ sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb- sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pulseaudio ``` -### Instalace balíčku pynput - -```sh -pip3 install pynput -``` - ### Instalace vcpkg ```sh diff --git a/README-FA.md b/README-FA.md index 5fd3c0d03..f7de9aa87 100644 --- a/README-FA.md +++ b/README-FA.md @@ -78,12 +78,6 @@ sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb- sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pulseaudio ``` -### بسته pynput را نصب کنید - -```sh -pip3 install pynput -``` - ### نرم افزار vcpkg را نصب کنید ```sh diff --git a/README-HU.md b/README-HU.md index 3960d8b40..cfc6c793d 100644 --- a/README-HU.md +++ b/README-HU.md @@ -81,12 +81,6 @@ sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb- sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pulseaudio ``` -### Telepítsd a pynput csomagot - -```sh -pip3 install pynput -``` - ### Telepítsd a vcpkg-t ```sh diff --git a/README-JP.md b/README-JP.md index c1722a90f..394fbc52a 100644 --- a/README-JP.md +++ b/README-JP.md @@ -80,12 +80,6 @@ sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb- sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pulseaudio ``` -### Install pynput package - -```sh -pip3 install pynput -``` - ### Install vcpkg ```sh diff --git a/README-KR.md b/README-KR.md index c7cf423da..9a87d8ab1 100644 --- a/README-KR.md +++ b/README-KR.md @@ -78,12 +78,6 @@ sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb- sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pulseaudio ``` -### Install pynput package - -```sh -pip3 install pynput -``` - ### Install vcpkg ```sh diff --git a/README-VN.md b/README-VN.md index 641b80ebd..b7b683e4f 100644 --- a/README-VN.md +++ b/README-VN.md @@ -82,12 +82,6 @@ sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb- sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pulseaudio ``` -### Cách tải về gói hàng pynput - -```sh -pip3 install pynput -``` - ### Cách cài vcpkg ```sh diff --git a/README.md b/README.md index 456862af5..fbbc1b60b 100644 --- a/README.md +++ b/README.md @@ -80,12 +80,6 @@ sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb- sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pulseaudio ``` -### Install pynput package - -```sh -pip3 install pynput -``` - ### Install vcpkg ```sh diff --git a/appimage/AppImageBuilder.yml b/appimage/AppImageBuilder.yml index 08a4f0786..cdaead908 100644 --- a/appimage/AppImageBuilder.yml +++ b/appimage/AppImageBuilder.yml @@ -9,8 +9,6 @@ script: # Download sciter.so - mkdir -p AppDir/usr/lib/rustdesk/ - pushd AppDir/usr/lib/rustdesk && wget https://github.com/c-smile/sciter-sdk/raw/29a598b6d20220b93848b5e8abab704619296857/bin.lnx/x64/libsciter-gtk.so && popd - # pynput_service.py - - cp ../pynput_service.py ./AppDir/usr/lib/rustdesk # Build rustdesk - pushd .. && python3 inline-sciter.py && cargo build --features inline,appimage --release && popd - mkdir -p AppDir/usr/bin diff --git a/appimage/requirements.txt b/appimage/requirements.txt deleted file mode 100644 index d632797e5..000000000 --- a/appimage/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -pynput \ No newline at end of file diff --git a/build.py b/build.py index 6b03cb57b..095f8b7f1 100755 --- a/build.py +++ b/build.py @@ -140,8 +140,6 @@ def build_flutter_deb(version): 'cp rustdesk.service tmpdeb/usr/share/rustdesk/files/systemd/') os.system( 'cp rustdesk.service.user tmpdeb/usr/share/rustdesk/files/systemd/') - os.system( - 'cp ../pynput_service.py tmpdeb/usr/share/rustdesk/files/') os.system( 'cp ../128x128@2x.png tmpdeb/usr/share/rustdesk/files/rustdesk.png') os.system( @@ -150,7 +148,6 @@ def build_flutter_deb(version): os.system('cp -a ../DEBIAN/* tmpdeb/DEBIAN/') md5_file('usr/share/rustdesk/files/systemd/rustdesk.service') md5_file('usr/share/rustdesk/files/systemd/rustdesk.service.user') - md5_file('usr/share/rustdesk/files/pynput_service.py') os.system('dpkg-deb -b tmpdeb rustdesk.deb; /bin/rm -rf tmpdeb/') os.rename('rustdesk.deb', '../rustdesk-%s.deb' % version) os.chdir("..") @@ -285,15 +282,12 @@ def main(): 'cp rustdesk.service tmpdeb/usr/share/rustdesk/files/systemd/') os.system( 'cp rustdesk.service.user tmpdeb/usr/share/rustdesk/files/systemd/') - os.system( - 'cp pynput_service.py tmpdeb/usr/share/rustdesk/files/') os.system('cp -a DEBIAN/* tmpdeb/DEBIAN/') os.system('strip tmpdeb/usr/bin/rustdesk') os.system('mkdir -p tmpdeb/usr/lib/rustdesk') os.system('cp libsciter-gtk.so tmpdeb/usr/lib/rustdesk/') md5_file('usr/share/rustdesk/files/systemd/rustdesk.service') md5_file('usr/share/rustdesk/files/systemd/rustdesk.service.user') - md5_file('usr/share/rustdesk/files/pynput_service.py') md5_file('usr/lib/rustdesk/libsciter-gtk.so') os.system('dpkg-deb -b tmpdeb rustdesk.deb; /bin/rm -rf tmpdeb/') os.rename('rustdesk.deb', 'rustdesk-%s.deb' % version) diff --git a/libs/enigo/src/linux/mod.rs b/libs/enigo/src/linux/mod.rs index 42e1dfebf..1f73004ad 100644 --- a/libs/enigo/src/linux/mod.rs +++ b/libs/enigo/src/linux/mod.rs @@ -1,5 +1,4 @@ mod nix_impl; -mod pynput; mod xdo; pub use self::nix_impl::Enigo; diff --git a/libs/enigo/src/linux/nix_impl.rs b/libs/enigo/src/linux/nix_impl.rs index 7a8f6668e..0c9b30eff 100644 --- a/libs/enigo/src/linux/nix_impl.rs +++ b/libs/enigo/src/linux/nix_impl.rs @@ -1,11 +1,10 @@ -use super::{pynput::EnigoPynput, xdo::EnigoXdo}; +use super::{xdo::EnigoXdo}; use crate::{Key, KeyboardControllable, MouseButton, MouseControllable}; /// The main struct for handling the event emitting // #[derive(Default)] pub struct Enigo { xdo: EnigoXdo, - pynput: EnigoPynput, is_x11: bool, uinput_keyboard: Option>, uinput_mouse: Option>, @@ -20,9 +19,9 @@ impl Enigo { pub fn set_delay(&mut self, delay: u64) { self.xdo.set_delay(delay) } - /// Reset pynput. + /// Reset pynput?. pub fn reset(&mut self) { - self.pynput.reset(); + todo!() } /// Set uinput keyboard. pub fn set_uinput_keyboard( @@ -44,7 +43,6 @@ impl Default for Enigo { uinput_keyboard: None, uinput_mouse: None, xdo: EnigoXdo::default(), - pynput: EnigoPynput::default(), } } } @@ -142,9 +140,6 @@ impl KeyboardControllable for Enigo { fn key_down(&mut self, key: Key) -> crate::ResultType { if self.is_x11 { - if self.pynput.send_pynput(&key, true) { - return Ok(()); - } self.xdo.key_down(key) } else { if let Some(keyboard) = &mut self.uinput_keyboard { @@ -156,9 +151,6 @@ impl KeyboardControllable for Enigo { } fn key_up(&mut self, key: Key) { if self.is_x11 { - if self.pynput.send_pynput(&key, false) { - return; - } self.xdo.key_up(key) } else { if let Some(keyboard) = &mut self.uinput_keyboard { diff --git a/libs/enigo/src/linux/pynput.rs b/libs/enigo/src/linux/pynput.rs deleted file mode 100644 index 836c645fe..000000000 --- a/libs/enigo/src/linux/pynput.rs +++ /dev/null @@ -1,279 +0,0 @@ -use crate::Key; -use std::{io::prelude::*, sync::mpsc}; - -enum PyMsg { - Char(char), - Str(&'static str), -} - -/// The main struct for handling the event emitting -pub(super) struct EnigoPynput { - tx: mpsc::Sender<(PyMsg, bool)>, -} - -impl Default for EnigoPynput { - fn default() -> Self { - let (tx, rx) = mpsc::channel(); - start_pynput_service(rx); - Self { tx } - } -} -impl EnigoPynput { - pub(super) fn reset(&mut self) { - self.tx.send((PyMsg::Char('\0'), true)).ok(); - } - - #[inline] - pub(super) fn send_pynput(&mut self, key: &Key, is_press: bool) -> bool { - if unsafe { PYNPUT_EXIT || !PYNPUT_REDAY } { - return false; - } - if let Key::Layout(c) = key { - return self.tx.send((PyMsg::Char(*c), is_press)).is_ok(); - } - if let Key::Raw(_) = key { - return false; - } - #[allow(deprecated)] - let s = match key { - Key::Alt => "Alt_L", - Key::Backspace => "BackSpace", - Key::CapsLock => "Caps_Lock", - Key::Control => "Control_L", - Key::Delete => "Delete", - Key::DownArrow => "Down", - Key::End => "End", - Key::Escape => "Escape", - Key::F1 => "F1", - Key::F10 => "F10", - Key::F11 => "F11", - Key::F12 => "F12", - Key::F2 => "F2", - Key::F3 => "F3", - Key::F4 => "F4", - Key::F5 => "F5", - Key::F6 => "F6", - Key::F7 => "F7", - Key::F8 => "F8", - Key::F9 => "F9", - Key::Home => "Home", - Key::LeftArrow => "Left", - Key::Option => "Option", - Key::PageDown => "Page_Down", - Key::PageUp => "Page_Up", - Key::Return => "Return", - Key::RightArrow => "Right", - Key::Shift => "Shift_L", - Key::Space => "space", - Key::Tab => "Tab", - Key::UpArrow => "Up", - Key::Numpad0 => "KP_0", - Key::Numpad1 => "KP_1", - Key::Numpad2 => "KP_2", - Key::Numpad3 => "KP_3", - Key::Numpad4 => "KP_4", - Key::Numpad5 => "KP_5", - Key::Numpad6 => "KP_6", - Key::Numpad7 => "KP_7", - Key::Numpad8 => "KP_8", - Key::Numpad9 => "KP_9", - Key::Decimal => "KP_Decimal", - Key::Cancel => "Cancel", - Key::Clear => "Clear", - Key::Pause => "Pause", - Key::Kana => "Kana", - Key::Hangul => "Hangul", - Key::Hanja => "Hanja", - Key::Kanji => "Kanji", - Key::Select => "Select", - Key::Print => "Print", - Key::Execute => "Execute", - Key::Snapshot => "3270_PrintScreen", - Key::Insert => "Insert", - Key::Help => "Help", - Key::Separator => "KP_Separator", - Key::Scroll => "Scroll_Lock", - Key::NumLock => "Num_Lock", - Key::RWin => "Super_R", - Key::Apps => "Menu", - Key::Multiply => "KP_Multiply", - Key::Add => "KP_Add", - Key::Subtract => "KP_Subtract", - Key::Divide => "KP_Divide", - Key::Equals => "KP_Equal", - Key::NumpadEnter => "KP_Enter", - Key::RightShift => "Shift_R", - Key::RightControl => "Control_R", - Key::RightAlt => "Mode_switch", - Key::Command | Key::Super | Key::Windows | Key::Meta => "Super_L", - _ => { - return true; - } - }; - return self.tx.send((PyMsg::Str(s), is_press)).is_ok(); - } -} - -// impl MouseControllable for EnigoPynput { -// fn mouse_move_to(&mut self, _x: i32, _y: i32) { -// unimplemented!() -// } -// fn mouse_move_relative(&mut self, _x: i32, _y: i32) { -// unimplemented!() -// } -// fn mouse_down(&mut self, _button: MouseButton) -> crate::ResultType { -// unimplemented!() -// } -// fn mouse_up(&mut self, _button: MouseButton) { -// unimplemented!() -// } -// fn mouse_click(&mut self, _button: MouseButton) { -// unimplemented!() -// } -// fn mouse_scroll_x(&mut self, _length: i32) { -// unimplemented!() -// } -// fn mouse_scroll_y(&mut self, _length: i32) { -// unimplemented!() -// } -// } - -// impl KeyboardControllable for EnigoPynput { -// fn get_key_state(&mut self, _key: Key) -> bool { -// unimplemented!() -// } - -// fn key_sequence(&mut self, _sequence: &str) { -// unimplemented!() -// } -// fn key_down(&mut self, key: Key) -> crate::ResultType { -// let _ = self.send_pynput(&key, true); -// Ok(()) -// } -// fn key_up(&mut self, key: Key) { -// let _ = self.send_pynput(&key, false); -// } -// fn key_click(&mut self, _key: Key) { -// unimplemented!() -// } -// } - -static mut PYNPUT_EXIT: bool = false; -static mut PYNPUT_REDAY: bool = false; -static IPC_FILE: &'static str = "/tmp/RustDesk/pynput_service"; - -fn start_pynput_service(rx: mpsc::Receiver<(PyMsg, bool)>) { - let mut py = "./pynput_service.py".to_owned(); - if !std::path::Path::new(&py).exists() { - py = "/usr/share/rustdesk/files/pynput_service.py".to_owned(); - if !std::path::Path::new(&py).exists() { - py = "/usr/lib/rustdesk/pynput_service.py".to_owned(); - if !std::path::Path::new(&py).exists() { - log::error!("{} not exits", py); - } - } - } - log::info!("pynput service: {}", py); - std::thread::spawn(move || { - let username = std::env::var("PYNPUT_USERNAME").unwrap_or("".to_owned()); - let userid = std::env::var("PYNPUT_USERID").unwrap_or("".to_owned()); - let status = if username.is_empty() { - std::process::Command::new("python3") - .arg(&py) - .arg(IPC_FILE) - .status() - .map(|x| x.success()) - } else { - let mut status = Ok(true); - for i in 0..100 { - if i % 10 == 0 { - log::info!("#{} try to start pynput server", i); - } - status = std::process::Command::new("sudo") - .args(vec![ - "-E", - &format!("XDG_RUNTIME_DIR=/run/user/{}", userid) as &str, - "-u", - &username, - "python3", - &py, - IPC_FILE, - ]) - .status() - .map(|x| x.success()); - match status { - Ok(true) => break, - _ => {} - } - std::thread::sleep(std::time::Duration::from_millis(100)); - } - status - }; - log::info!( - "pynput server exit with username/id {}/{}: {:?}", - username, - userid, - status - ); - unsafe { - PYNPUT_EXIT = true; - } - }); - std::thread::spawn(move || { - for i in 0..300 { - std::thread::sleep(std::time::Duration::from_millis(100)); - let mut conn = match std::os::unix::net::UnixStream::connect(IPC_FILE) { - Ok(conn) => conn, - Err(err) => { - if i % 15 == 0 { - log::warn!("Failed to connect to {}: {}", IPC_FILE, err); - } - continue; - } - }; - if let Err(err) = conn.set_nonblocking(true) { - log::error!("Failed to set ipc nonblocking: {}", err); - return; - } - log::info!("Conntected to pynput server"); - let d = std::time::Duration::from_millis(30); - unsafe { - PYNPUT_REDAY = true; - } - let mut buf = [0u8; 1024]; - loop { - if unsafe { PYNPUT_EXIT } { - break; - } - match rx.recv_timeout(d) { - Ok((msg, is_press)) => { - let msg = match msg { - PyMsg::Char(chr) => { - format!("{}{}", if is_press { 'p' } else { 'r' }, chr) - } - PyMsg::Str(s) => format!("{}{}", if is_press { 'p' } else { 'r' }, s), - }; - let n = msg.len(); - buf[0] = n as _; - buf[1..(n + 1)].copy_from_slice(msg.as_bytes()); - if let Err(err) = conn.write_all(&buf[..n + 1]) { - log::error!("Failed to write to ipc: {}", err); - break; - } - } - Err(err) => match err { - mpsc::RecvTimeoutError::Disconnected => { - log::error!("pynput sender disconnecte"); - break; - } - _ => {} - }, - } - } - unsafe { - PYNPUT_REDAY = false; - } - break; - } - }); -} diff --git a/pacman_install b/pacman_install index d22423574..cfd3bdd60 100644 --- a/pacman_install +++ b/pacman_install @@ -7,7 +7,7 @@ post_install() { # do something here cp /usr/share/rustdesk/files/rustdesk.service /etc/systemd/system/rustdesk.service cp /usr/share/rustdesk/files/rustdesk.desktop /usr/share/applications/ - sudo -H pip3 install pynput + sudo -H pip3 install systemctl daemon-reload systemctl enable rustdesk systemctl start rustdesk diff --git a/pynput_service.py b/pynput_service.py deleted file mode 100644 index 5aca57986..000000000 --- a/pynput_service.py +++ /dev/null @@ -1,240 +0,0 @@ -from pynput.keyboard import Key, Controller -from pynput.keyboard._xorg import KeyCode -from pynput._util.xorg import display_manager -import Xlib -from pynput._util.xorg import * -import Xlib -import os -import sys -import socket - -KeyCode._from_symbol("\0") # test - -DEAD_KEYS = { - '`': 65104, - '´': 65105, - '^': 65106, - '~': 65107, - '¯': 65108, - '˘': 65109, - '˙': 65110, - '¨': 65111, - '˚': 65112, - '˝': 65113, - 'ˇ': 65114, - '¸': 65115, - '˛': 65116, - '℩': 65117, # ? - '゛': 65118, # ? - '゚ ': 65119, - 'ٜ': 65120, - '↪': 65121, - ' ̛': 65122, -} - - - -def my_keyboard_mapping(display): - """Generates a mapping from *keysyms* to *key codes* and required - modifier shift states. - - :param Xlib.display.Display display: The display for which to retrieve the - keyboard mapping. - - :return: the keyboard mapping - """ - mapping = {} - - shift_mask = 1 << 0 - group_mask = alt_gr_mask(display) - - # Iterate over all keysym lists in the keyboard mapping - min_keycode = display.display.info.min_keycode - keycode_count = display.display.info.max_keycode - min_keycode + 1 - for index, keysyms in enumerate(display.get_keyboard_mapping( - min_keycode, keycode_count)): - key_code = index + min_keycode - - # Normalise the keysym list to yield a tuple containing the two groups - normalized = keysym_normalize(keysyms) - if not normalized: - continue - - # Iterate over the groups to extract the shift and modifier state - for groups, group in zip(normalized, (False, True)): - for keysym, shift in zip(groups, (False, True)): - - if not keysym: - continue - shift_state = 0 \ - | (shift_mask if shift else 0) \ - | (group_mask if group else 0) - - # !!!: Save all keycode combinations of keysym - if keysym in mapping: - mapping[keysym].append((key_code, shift_state)) - else: - mapping[keysym] = [(key_code, shift_state)] - return mapping - - -class MyController(Controller): - def _update_keyboard_mapping(self): - """Updates the keyboard mapping. - """ - with display_manager(self._display) as dm: - self._keyboard_mapping = my_keyboard_mapping(dm) - - def send_event(self, event, keycode, shift_state): - with display_manager(self._display) as dm, self.modifiers as modifiers: - # Under certain cimcumstances, such as when running under Xephyr, - # the value returned by dm.get_input_focus is an int - window = dm.get_input_focus().focus - send_event = getattr( - window, - 'send_event', - lambda event: dm.send_event(window, event)) - send_event(event( - detail=keycode, - state=shift_state | self._shift_mask(modifiers), - time=0, - root=dm.screen().root, - window=window, - same_screen=0, - child=Xlib.X.NONE, - root_x=0, root_y=0, event_x=0, event_y=0)) - - def fake_input(self, keycode, is_press): - with display_manager(self._display) as dm: - Xlib.ext.xtest.fake_input( - dm, - Xlib.X.KeyPress if is_press else Xlib.X.KeyRelease, - keycode) - - def _handle(self, key, is_press): - """Resolves a key identifier and sends a keyboard event. - :param event: The *X* keyboard event. - :param int keysym: The keysym to handle. - """ - event = Xlib.display.event.KeyPress if is_press \ - else Xlib.display.event.KeyRelease - keysym = self._keysym(key) - - if key.vk is not None: - keycode = self._display.keysym_to_keycode(key.vk) - self.fake_input(keycode, is_press) - # Otherwise use XSendEvent; we need to use this in the general case to - # work around problems with keyboard layouts - self._emit('_on_fake_event', key, is_press) - return - - # Make sure to verify that the key was resolved - if keysym is None: - raise self.InvalidKeyException(key) - - # There may be multiple keycodes for keysym in keyboard_mapping - keycode_flag = len(self.keyboard_mapping[keysym]) == 1 - if keycode_flag: - keycode, shift_state = self.keyboard_mapping[keysym][0] - else: - keycode, shift_state = self._display.keysym_to_keycode(keysym), 0 - - keycode_set = set(map(lambda x: x[0], self.keyboard_mapping[keysym])) - # The keycode of the dead key is inconsistent, The keysym has multiple combinations of a keycode. - if keycode != self._display.keysym_to_keycode(keysym) \ - or (keycode_flag == False and keycode == list(keycode_set)[0] and len(keycode_set) == 1): - deakkey_chr = str(key).replace("'", '') - keysym = DEAD_KEYS[deakkey_chr] - # shift_state = 0 - keycode, shift_state = list( - filter(lambda x: x[1] == 0, - self.keyboard_mapping[keysym]) - )[0] - - # If the key has a virtual key code, use that immediately with - # fake_input; fake input,being an X server extension, has access to - # more internal state that we do - - try: - with self.modifiers as modifiers: - alt_gr = Key.alt_gr in modifiers - # !!!: Send_event can't support lock screen, this condition cann't be modified - if alt_gr: - self.send_event( - event, keycode, shift_state) - else: - self.fake_input(keycode, is_press) - except KeyError: - with self._borrow_lock: - keycode, index, count = self._borrows[keysym] - self._send_key( - event, - keycode, - index_to_shift(self._display, index)) - count += 1 if is_press else -1 - self._borrows[keysym] = (keycode, index, count) - - # Notify any running listeners - self._emit('_on_fake_event', key, is_press) - - -keyboard = MyController() - -server_address = sys.argv[1] -if not os.path.exists(os.path.dirname(server_address)): - os.makedirs(os.path.dirname(server_address)) - -try: - os.unlink(server_address) -except OSError: - if os.path.exists(server_address): - raise - -server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) -server.bind(server_address) -server.listen(1) -clientsocket, address = server.accept() -os.system('chmod a+rw %s' % server_address) -print("Got pynput connection") - - -def loop(): - global keyboard - buf = [] - while True: - data = clientsocket.recv(1024) - if not data: - print("Connection broken") - break - buf.extend(data) - while buf: - n = buf[0] - n = n + 1 - if len(buf) < n: - break - msg = bytearray(buf[1:n]).decode("utf-8") - buf = buf[n:] - if len(msg) < 2: - continue - if msg[1] == "\0": - keyboard = MyController() - print("Keyboard reset") - continue - if len(msg) == 2: - name = msg[1] - else: - name = KeyCode._from_symbol(msg[1:]) - if str(name) == "<0>": - continue - try: - if msg[0] == "p": - keyboard.press(name) - else: - keyboard.release(name) - except Exception as e: - print('[x] error key',e) - - -loop() -clientsocket.close() -server.close() diff --git a/rpm-suse.spec b/rpm-suse.spec index 73a610c11..db4bfe66f 100644 --- a/rpm-suse.spec +++ b/rpm-suse.spec @@ -25,7 +25,6 @@ install $HBB/libsciter-gtk.so %{buildroot}/usr/lib/rustdesk/libsciter-gtk.so install $HBB/rustdesk.service %{buildroot}/usr/share/rustdesk/files/ install $HBB/128x128@2x.png %{buildroot}/usr/share/rustdesk/files/rustdesk.png install $HBB/rustdesk.desktop %{buildroot}/usr/share/rustdesk/files/ -install $HBB/pynput_service.py %{buildroot}/usr/share/rustdesk/files/ %files /usr/bin/rustdesk @@ -33,7 +32,6 @@ install $HBB/pynput_service.py %{buildroot}/usr/share/rustdesk/files/ /usr/share/rustdesk/files/rustdesk.service /usr/share/rustdesk/files/rustdesk.png /usr/share/rustdesk/files/rustdesk.desktop -/usr/share/rustdesk/files/pynput_service.py %changelog # let's skip this for now @@ -54,7 +52,6 @@ esac %post cp /usr/share/rustdesk/files/rustdesk.service /etc/systemd/system/rustdesk.service cp /usr/share/rustdesk/files/rustdesk.desktop /usr/share/applications/ -sudo -H pip3 install pynput systemctl daemon-reload systemctl enable rustdesk systemctl start rustdesk diff --git a/rpm.spec b/rpm.spec index c61db5d0b..37e8ea4cc 100644 --- a/rpm.spec +++ b/rpm.spec @@ -25,7 +25,6 @@ install $HBB/libsciter-gtk.so %{buildroot}/usr/lib/rustdesk/libsciter-gtk.so install $HBB/rustdesk.service %{buildroot}/usr/share/rustdesk/files/ install $HBB/128x128@2x.png %{buildroot}/usr/share/rustdesk/files/rustdesk.png install $HBB/rustdesk.desktop %{buildroot}/usr/share/rustdesk/files/ -install $HBB/pynput_service.py %{buildroot}/usr/share/rustdesk/files/ %files /usr/bin/rustdesk @@ -33,7 +32,6 @@ install $HBB/pynput_service.py %{buildroot}/usr/share/rustdesk/files/ /usr/share/rustdesk/files/rustdesk.service /usr/share/rustdesk/files/rustdesk.png /usr/share/rustdesk/files/rustdesk.desktop -/usr/share/rustdesk/files/pynput_service.py /usr/share/rustdesk/files/__pycache__/* %changelog @@ -55,7 +53,6 @@ esac %post cp /usr/share/rustdesk/files/rustdesk.service /etc/systemd/system/rustdesk.service cp /usr/share/rustdesk/files/rustdesk.desktop /usr/share/applications/ -sudo -H pip3 install pynput systemctl daemon-reload systemctl enable rustdesk systemctl start rustdesk diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 310c6f3f6..24882ce4f 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -108,13 +108,7 @@ parts: plugin: nil override-pull: | mkdir -p ${SNAPCRAFT_PART_INSTALL}/usr/share/rustdesk/files/systemd/ - cp ${SNAPCRAFT_PART_SRC}/../../rustdesk/src/pynput_service.py ${SNAPCRAFT_PART_INSTALL}/usr/share/rustdesk/files/ cp ${SNAPCRAFT_PART_SRC}/../../rustdesk/src/rustdesk.service ${SNAPCRAFT_PART_INSTALL}/usr/share/rustdesk/files/systemd/ - - python3-deps: - plugin: python - python-packages: - - pynput == 1.7.6 layout: /usr/share/rustdesk: diff --git a/src/flutter.rs b/src/flutter.rs index 9c5dd319d..6653f96f3 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -226,7 +226,6 @@ impl Session { pub fn send_key_event(&self, mut evt: KeyEvent, keyboard_mode: KeyboardMode) { // mode: legacy(0), map(1), translate(2), auto(3) evt.mode = keyboard_mode.into(); - dbg!(&evt); let mut msg_out = Message::new(); msg_out.set_key_event(evt); self.send(Data::Message(msg_out)); diff --git a/src/server/input_service.rs b/src/server/input_service.rs index eddfa3c73..1e6a10613 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -169,13 +169,6 @@ fn run_cursor(sp: MouseCursorService, state: &mut StateCursor) -> ResultType<()> lazy_static::lazy_static! { static ref ENIGO: Arc> = { - #[cfg(target_os = "linux")] - { - if crate::platform::is_root() { - std::env::set_var("PYNPUT_USERNAME", crate::platform::linux::get_active_username()); - std::env::set_var("PYNPUT_USERID", crate::platform::linux::get_active_userid()); - } - } Arc::new(Mutex::new(Enigo::new())) }; static ref KEYS_DOWN: Arc>> = Default::default(); @@ -680,7 +673,6 @@ fn map_keyboard_mode(evt: &KeyEvent) { fn tfc_key_down_or_up(key: Key, down: bool, up: bool) { if let Key::Layout(chr) = key { - log::info!("tfc_key_down_or_up: {:?}", chr); if down { TFC_CONTEXT.lock().unwrap().unicode_char_down(chr); } @@ -766,7 +758,6 @@ fn tfc_key_down_or_up(key: Key, down: bool, up: bool) { } }; - log::info!("tfc_key_down_or_up: {:?}", key); if down { TFC_CONTEXT.lock().unwrap().key_down(key); } diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 34d4251fa..6100c8b87 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -1106,8 +1106,6 @@ impl Handler { if let Some(chars) = chars { for chr in chars { - dbg!(chr); - let mut key_event = KeyEvent::new(); key_event.set_chr(chr as _); key_event.down = true; From 763456519e40a61da3d1bbfc10f878f2cab9a511 Mon Sep 17 00:00:00 2001 From: Asura Date: Thu, 1 Sep 2022 00:36:24 -0700 Subject: [PATCH 0338/2015] Add Key for tfc --- Cargo.lock | 2 +- src/server/input_service.rs | 24 ++++++++---------------- 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 96ff5a811..62b412062 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4993,7 +4993,7 @@ checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" [[package]] name = "tfc" version = "0.6.1" -source = "git+https://github.com/asur4s/The-Fat-Controller#8ef82be83d8d941f08bdb84e77bea52290f92050" +source = "git+https://github.com/asur4s/The-Fat-Controller#a091f887edc2440b17d86c9ba580f2f35ce0cfcc" dependencies = [ "core-graphics 0.22.3", "unicode-segmentation", diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 1e6a10613..ffcaf32a9 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -673,6 +673,7 @@ fn map_keyboard_mode(evt: &KeyEvent) { fn tfc_key_down_or_up(key: Key, down: bool, up: bool) { if let Key::Layout(chr) = key { + log::info!("tfc_key_down_or_up :{:?}", chr); if down { TFC_CONTEXT.lock().unwrap().unicode_char_down(chr); } @@ -705,7 +706,6 @@ fn tfc_key_down_or_up(key: Key, down: bool, up: bool) { Key::F9 => TFC_Key::F9, Key::Home => TFC_Key::Home, Key::LeftArrow => TFC_Key::LeftArrow, - Key::Option => TFC_Key::Alt, Key::PageDown => TFC_Key::PageDown, Key::PageUp => TFC_Key::PageUp, Key::Return => TFC_Key::ReturnOrEnter, @@ -725,24 +725,15 @@ fn tfc_key_down_or_up(key: Key, down: bool, up: bool) { Key::Numpad8 => TFC_Key::N8, Key::Numpad9 => TFC_Key::N9, Key::Decimal => TFC_Key::NumpadDecimal, - // Key::Cancel => TFC_Key::Cancel, Key::Clear => TFC_Key::NumpadClear, Key::Pause => TFC_Key::PlayPause, - // Key::Kana => TFC_Key::, - // Key::Hangul => "Hangul", - // Key::Hanja => "Hanja", - // Key::Kanji => "Kanji", - // Key::Select => TFC_Key::Sel, - // Key::Print => TFC_Key::P, - // Key::Execute => "Execute", - // Key::Snapshot => "3270_PrintScreen", - // Key::Insert => TFC_Key:, - // Key::Help => "Help", - // Key::Separator => "KP_Separator", - // Key::Scroll => "Scroll_Lock", - // Key::NumLock => "Num_Lock", + Key::Print => TFC_Key::Print, + Key::Snapshot => TFC_Key::PrintScreen, + Key::Insert => TFC_Key::Insert, + Key::Scroll => TFC_Key::ScrollLock, + Key::NumLock => TFC_Key::NumLock, Key::RWin => TFC_Key::Meta, - // Key::Apps => "Menu", + Key::Apps => TFC_Key::Apps, Key::Multiply => TFC_Key::NumpadMultiply, Key::Add => TFC_Key::NumpadPlus, Key::Subtract => TFC_Key::NumpadMinus, @@ -758,6 +749,7 @@ fn tfc_key_down_or_up(key: Key, down: bool, up: bool) { } }; + log::info!("tfc_key_down_or_up: {:?}", key); if down { TFC_CONTEXT.lock().unwrap().key_down(key); } From 2891c1b148e38a639f97207ffdcc0526e3463a0d Mon Sep 17 00:00:00 2001 From: csf Date: Thu, 1 Sep 2022 16:21:41 +0800 Subject: [PATCH 0339/2015] refactor set_peer_info --- flutter/lib/models/model.dart | 1 + src/client.rs | 4 +- src/flutter.rs | 57 ++++++++------------ src/ui/remote.rs | 36 ++++++++----- src/ui_session_interface.rs | 97 +++++------------------------------ 5 files changed, 59 insertions(+), 136 deletions(-) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 171a41dfa..c01451280 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -325,6 +325,7 @@ class FfiModel with ChangeNotifier { } if (evt['is_file_transfer'] == "true") { + // TODO is file transfer parent.target?.fileModel.onReady(); } else { _pi.displays = []; diff --git a/src/client.rs b/src/client.rs index 6346af6d0..184170e4a 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1278,13 +1278,13 @@ impl LoginConfigHandler { /// /// * `username` - The name of the peer. /// * `pi` - The peer info. - pub fn handle_peer_info(&mut self, username: String, pi: PeerInfo) { + pub fn handle_peer_info(&mut self, pi: PeerInfo) { if !pi.version.is_empty() { self.version = hbb_common::get_version_number(&pi.version); } self.features = pi.features.into_option(); let serde = PeerInfoSerde { - username, + username: pi.username.clone(), hostname: pi.hostname.clone(), platform: pi.platform.clone(), }; diff --git a/src/flutter.rs b/src/flutter.rs index 0b8c3626f..1244e521a 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -1,24 +1,13 @@ use std::{ collections::HashMap, - sync::{ - Arc, RwLock, - }, + sync::{Arc, RwLock}, }; use flutter_rust_bridge::{StreamSink, ZeroCopyBuffer}; -use hbb_common::{ - bail, - config::{LocalConfig}, - message_proto::*, - ResultType, -}; - -use crate::{ - ui_session_interface::{io_loop, InvokeUi, Session}, -}; - +use hbb_common::{bail, config::LocalConfig, message_proto::*, ResultType}; +use crate::ui_session_interface::{io_loop, InvokeUi, Session}; use crate::{client::*, flutter_ffi::EventToUI}; @@ -97,10 +86,6 @@ impl InvokeUi for FlutterHandler { self.push_event("permission", vec![(name, &value.to_string())]); } - fn update_pi(&self, pi: PeerInfo) { - // todo!() - } - fn close_success(&self) { // todo!() } @@ -204,29 +189,27 @@ impl InvokeUi for FlutterHandler { } } - fn set_peer_info( - &self, - username: &str, - hostname: &str, - platform: &str, - sas_enabled: bool, - displays: &Vec>, - version: &str, - current_display: usize, - is_file_transfer: bool, - ) { - let displays = serde_json::ser::to_string(displays).unwrap_or("".to_owned()); + 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); + displays.push(h); + } + let displays = serde_json::ser::to_string(&displays).unwrap_or("".to_owned()); self.push_event( "peer_info", vec![ - ("username", username), - ("hostname", hostname), - ("platform", platform), - ("sas_enabled", &sas_enabled.to_string()), + ("username", &pi.username), + ("hostname", &pi.hostname), + ("platform", &pi.platform), + ("sas_enabled", &pi.sas_enabled.to_string()), ("displays", &displays), - ("version", &version), - ("current_display", ¤t_display.to_string()), - ("is_file_transfer", &is_file_transfer.to_string()), + ("version", &pi.version), + ("current_display", &pi.current_display.to_string()), ], ); } diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 2d412ea9b..4310f64bf 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -96,6 +96,10 @@ impl InvokeUi for SciterHandler { fn set_display(&self, x: i32, y: i32, w: i32, h: i32) { self.call("setDisplay", &make_args!(x, y, w, h)); + // https://sciter.com/forums/topic/color_spaceiyuv-crash + // Nothing spectacular in decoder – done on CPU side. + // So if you can do BGRA translation on your side – the better. + // BGRA is used as internal image format so it will not require additional transformations. VIDEO.lock().unwrap().as_mut().map(|v| { v.stop_streaming().ok(); let ok = v.start_streaming((w, h), COLOR_SPACE::Rgb32, None); @@ -111,8 +115,6 @@ impl InvokeUi for SciterHandler { self.call2("setPermission", &make_args!(name, value)); } - fn update_pi(&self, pi: PeerInfo) {} // TODO dup flutter - fn close_success(&self) { self.call2("closeSuccess", &make_args!()); } @@ -202,17 +204,25 @@ impl InvokeUi for SciterHandler { .map(|v| v.render_frame(data).ok()); } - fn set_peer_info( - &self, - username: &str, - hostname: &str, - platform: &str, - sas_enabled: bool, - displays: &Vec>, - version: &str, - current_display: usize, - is_file_transfer: bool, - ) { + fn set_peer_info(&self, pi: &PeerInfo) { + let mut pi_sciter = Value::map(); + pi_sciter.set_item("username", pi.username.clone()); + 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); + displays.push(display); + } + pi_sciter.set_item("displays", displays); + pi_sciter.set_item("current_display", pi.current_display); + self.call("updatePi", &make_args!(pi_sciter)); } fn msgbox(&self, msgtype: &str, title: &str, text: &str, retry: bool) { diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index a164a2d94..2f401e26f 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -532,20 +532,9 @@ pub trait InvokeUi: Send + Sync + Clone + 'static + Sized + Default { fn set_cursor_position(&self, cp: CursorPosition); fn set_display(&self, x: i32, y: i32, w: i32, h: i32); fn switch_display(&self, display: &SwitchDisplay); - fn set_peer_info( - &self, - username: &str, - hostname: &str, - platform: &str, - sas_enabled: bool, - displays: &Vec>, - version: &str, - current_display: usize, - is_file_transfer: bool, - ); // flutter + fn set_peer_info(&self, peer_info: &PeerInfo); // flutter fn update_privacy_mode(&self); fn set_permission(&self, name: &str, value: bool); - fn update_pi(&self, pi: PeerInfo); fn close_success(&self); fn update_quality_status(&self, qs: QualityStatus); fn set_connection_type(&self, is_secured: bool, direct: bool); @@ -623,20 +612,11 @@ impl Interface for Session { self.lc.write().unwrap().handle_login_error(err, self) } - fn handle_peer_info(&mut self, pi: PeerInfo) { - let mut lc = self.lc.write().unwrap(); - - // let mut pi_sciter = Value::map(); - let username = lc.get_username(&pi); - - // flutter - let mut displays = Vec::new(); - let mut current_index = pi.current_display as usize; - - // pi_sciter.set_item("username", username.clone()); - // 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); + fn handle_peer_info(&mut self, mut pi: PeerInfo) { + pi.username = self.lc.read().unwrap().get_username(&pi); + if pi.current_display as usize >= pi.displays.len() { + pi.current_display = 0; + } if get_version_number(&pi.version) < get_version_number("1.1.10") { self.set_permission("restart", false); } @@ -647,73 +627,22 @@ impl Interface for Session { } } else if !self.is_port_forward() { if pi.displays.is_empty() { - lc.handle_peer_info(username, pi); + self.lc.write().unwrap().handle_peer_info(pi); self.update_privacy_mode(); self.msgbox("error", "Remote Error", "No Display"); return; } - // 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); - // displays.push(display); - // } - // pi_sciter.set_item("displays", displays); - - // flutter - 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); - displays.push(h); - } - if current_index >= pi.displays.len() { - current_index = 0; - } - - if current_index >= pi.displays.len() { - current_index = 0; - } - // pi_sciter.set_item("current_display", current as i32); - let current = &pi.displays[current_index]; - self.set_display(current.x, current.y, current.width, current.height); - - self.set_peer_info( - &username, - &pi.hostname, - &pi.platform, - pi.sas_enabled, - &displays, - &pi.version, - current_index, - lc.is_file_transfer, - ); - - // https://sciter.com/forums/topic/color_spaceiyuv-crash - // Nothing spectacular in decoder – done on CPU side. - // So if you can do BGRA translation on your side – the better. - // BGRA is used as internal image format so it will not require additional transformations. - // VIDEO.lock().unwrap().as_mut().map(|v| { - // let ok = v.start_streaming( - // (current.width as _, current.height as _), - // COLOR_SPACE::Rgb32, - // None, - // ); - // log::info!("[video] initialized: {:?}", ok); - // }); - let p = lc.should_auto_login(); + let p = self.lc.read().unwrap().should_auto_login(); if !p.is_empty() { input_os_password(p, true, self.clone()); } + let current = &pi.displays[pi.current_display as usize]; + self.set_display(current.x, current.y, current.width, current.height); } - lc.handle_peer_info(username, pi); self.update_privacy_mode(); - // self.update_pi(pi); + self.set_peer_info(&pi); + self.lc.write().unwrap().handle_peer_info(pi); + if self.is_file_transfer() { self.close_success(); } else if !self.is_port_forward() { From ee839875234505ce29197d1eeee4825886dd4fad Mon Sep 17 00:00:00 2001 From: csf Date: Thu, 1 Sep 2022 17:36:37 +0800 Subject: [PATCH 0340/2015] sciter input & conn_type and other InvokeUi impl --- src/client.rs | 32 ++--- src/flutter.rs | 43 +++--- src/flutter_ffi.rs | 15 +- src/ui/remote.rs | 265 +++--------------------------------- src/ui_session_interface.rs | 263 +++++++++++++++++++++++++++++++++-- 5 files changed, 312 insertions(+), 306 deletions(-) diff --git a/src/client.rs b/src/client.rs index 184170e4a..8f6cb12bb 100644 --- a/src/client.rs +++ b/src/client.rs @@ -2,7 +2,7 @@ use std::{ collections::HashMap, net::SocketAddr, ops::{Deref, Not}, - sync::{mpsc, Arc, Mutex, RwLock, atomic::AtomicBool}, + sync::{atomic::AtomicBool, mpsc, Arc, Mutex, RwLock}, }; pub use async_trait::async_trait; @@ -864,8 +864,7 @@ impl VideoHandler { #[derive(Default)] pub struct LoginConfigHandler { id: String, - pub is_file_transfer: bool, - pub is_port_forward: bool, + pub conn_type: ConnType, hash: Hash, password: Vec, // remember password for reconnect pub remember: bool, @@ -904,12 +903,10 @@ impl LoginConfigHandler { /// # Arguments /// /// * `id` - id of peer - /// * `is_file_transfer` - Whether the connection is file transfer. - /// * `is_port_forward` - Whether the connection is port forward. - pub fn initialize(&mut self, id: String, is_file_transfer: bool, is_port_forward: bool) { + /// * `conn_type` - Connection type enum. + pub fn initialize(&mut self, id: String, conn_type: ConnType) { self.id = id; - self.is_file_transfer = is_file_transfer; - self.is_port_forward = is_port_forward; + self.conn_type = conn_type; let config = self.load_config(); self.remember = !config.password.is_empty(); self.config = config; @@ -1066,7 +1063,8 @@ impl LoginConfigHandler { /// /// * `ignore_default` - If `true`, ignore the default value of the option. fn get_option_message(&self, ignore_default: bool) -> Option { - if self.is_port_forward || self.is_file_transfer { + if self.conn_type.eq(&ConnType::FILE_TRANSFER) || self.conn_type.eq(&ConnType::PORT_FORWARD) + { return None; } let mut n = 0; @@ -1112,7 +1110,8 @@ impl LoginConfigHandler { } pub fn get_option_message_after_login(&self) -> Option { - if self.is_port_forward || self.is_file_transfer { + if self.conn_type.eq(&ConnType::FILE_TRANSFER) || self.conn_type.eq(&ConnType::PORT_FORWARD) + { return None; } let mut n = 0; @@ -1348,19 +1347,20 @@ impl LoginConfigHandler { version: crate::VERSION.to_string(), ..Default::default() }; - if self.is_file_transfer { - lr.set_file_transfer(FileTransfer { + match self.conn_type { + ConnType::FILE_TRANSFER => lr.set_file_transfer(FileTransfer { dir: self.get_remote_dir(), show_hidden: !self.get_option("remote_show_hidden").is_empty(), ..Default::default() - }); - } else if self.is_port_forward { - lr.set_port_forward(PortForward { + }), + ConnType::PORT_FORWARD => lr.set_port_forward(PortForward { host: self.port_forward.0.clone(), port: self.port_forward.1, ..Default::default() - }); + }), + _ => {} } + let mut msg_out = Message::new(); msg_out.set_login_request(lr); msg_out diff --git a/src/flutter.rs b/src/flutter.rs index 1244e521a..b84e91ce8 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -5,7 +5,7 @@ use std::{ use flutter_rust_bridge::{StreamSink, ZeroCopyBuffer}; -use hbb_common::{bail, config::LocalConfig, message_proto::*, ResultType}; +use hbb_common::{bail, config::LocalConfig, message_proto::*, ResultType, rendezvous_proto::ConnType}; use crate::ui_session_interface::{io_loop, InvokeUi, Session}; @@ -74,9 +74,8 @@ impl InvokeUi for FlutterHandler { ); } - fn set_display(&self, x: i32, y: i32, w: i32, h: i32) { - // todo!() - } + /// unused in flutter, use switch_display or set_peer_info + fn set_display(&self, _x: i32, _y: i32, _w: i32, _h: i32) {} fn update_privacy_mode(&self) { self.push_event("update_privacy_mode", [].into()); @@ -86,9 +85,7 @@ impl InvokeUi for FlutterHandler { self.push_event("permission", vec![(name, &value.to_string())]); } - fn close_success(&self) { - // todo!() - } + fn close_success(&self) {} fn update_quality_status(&self, status: QualityStatus) { const NULL: String = String::new(); @@ -179,9 +176,7 @@ impl InvokeUi for FlutterHandler { ); } - fn adapt_size(&self) { - // todo!() - } + fn adapt_size(&self) {} fn on_rgba(&self, data: &[u8]) { if let Some(stream) = &*self.event_stream.read().unwrap() { @@ -265,27 +260,37 @@ impl InvokeUi for FlutterHandler { /// * `is_file_transfer` - If the session is used for file transfer. /// * `is_port_forward` - If the session is used for port forward. pub fn session_add(id: &str, is_file_transfer: bool, is_port_forward: bool) -> ResultType<()> { - // TODO check same id let session_id = get_session_id(id.to_owned()); LocalConfig::set_remote_id(&session_id); - // TODO close - // Self::close(); - // TODO cmd passwd args let session: Session = Session { id: session_id.clone(), ..Default::default() }; + // TODO rdp + let conn_type = if is_file_transfer { + ConnType::FILE_TRANSFER + } else if is_port_forward { + ConnType::PORT_FORWARD + } else { + ConnType::DEFAULT_CONN + }; + session .lc .write() .unwrap() - .initialize(session_id.clone(), is_file_transfer, is_port_forward); - SESSIONS + .initialize(session_id, conn_type); + + if let Some(same_id_session) = SESSIONS .write() .unwrap() - .insert(id.to_owned(), session.clone()); + .insert(id.to_owned(), session) + { + same_id_session.close(); + } + Ok(()) } @@ -300,10 +305,6 @@ pub fn session_start_(id: &str, event_stream: StreamSink) -> ResultTy *session.event_stream.write().unwrap() = Some(event_stream); let session = session.clone(); std::thread::spawn(move || { - // TODO is_file_transfer is_port_forward - // let is_file_transfer = session.lc.read().unwrap().is_file_transfer; - // let is_port_forward = session.lc.read().unwrap().is_port_forward; - // Connection::start(session, is_file_transfer, is_port_forward); io_loop(session); }); Ok(()) diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 5226416b2..69da5f540 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -190,20 +190,20 @@ pub fn session_toggle_option(id: String, value: String) { } pub fn session_set_image_quality(id: String, value: String) { - if let Some(session) = SESSIONS.read().unwrap().get(&id) { - // session.set_image_quality(value); + if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) { + session.save_image_quality(value); } } pub fn session_lock_screen(id: String) { if let Some(session) = SESSIONS.read().unwrap().get(&id) { - // session.lock_screen(); + session.lock_screen(); } } pub fn session_ctrl_alt_del(id: String) { if let Some(session) = SESSIONS.read().unwrap().get(&id) { - // session.ctrl_alt_del(); + session.ctrl_alt_del(); } } @@ -224,13 +224,13 @@ pub fn session_input_key( command: bool, ) { if let Some(session) = SESSIONS.read().unwrap().get(&id) { - // session.input_key(&name, down, press, alt, ctrl, shift, command); + session.input_key(&name, down, press, alt, ctrl, shift, command); } } pub fn session_input_string(id: String, value: String) { if let Some(session) = SESSIONS.read().unwrap().get(&id) { - // session.input_string(&value); + session.input_string(&value); } } @@ -686,7 +686,6 @@ pub fn main_has_hwcodec() -> bool { has_hwcodec() } -// TODO pub fn session_send_mouse(id: String, msg: String) { if let Ok(m) = serde_json::from_str::>(&msg) { let alt = m.get("alt").is_some(); @@ -719,7 +718,7 @@ pub fn session_send_mouse(id: String, msg: String) { } << 3; } if let Some(session) = SESSIONS.read().unwrap().get(&id) { - // session.send_mouse(mask, x, y, alt, ctrl, shift, command); + session.send_mouse(mask, x, y, alt, ctrl, shift, command); } } } diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 4310f64bf..7e2c5cd9c 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -22,14 +22,14 @@ use clipboard::{ cliprdr::CliprdrClientContext, create_cliprdr_context as create_clipboard_file_context, get_rx_clip_client, server_clip_file, }; -use enigo::{self}; -use hbb_common::{allow_err, log, message_proto::*}; + +use hbb_common::{allow_err, log, message_proto::*, rendezvous_proto::ConnType}; #[cfg(windows)] use crate::clipboard_file::*; use crate::{ client::*, - ui_session_interface::{InvokeUi, Session}, + ui_session_interface::{InvokeUi, Session, IS_IN}, }; type Video = AssetPtr; @@ -38,9 +38,6 @@ lazy_static::lazy_static! { static ref VIDEO: Arc>> = Default::default(); } -static IS_IN: AtomicBool = AtomicBool::new(false); -static KEYBOARD_HOOKED: AtomicBool = AtomicBool::new(false); - #[cfg(windows)] static mut IS_ALT_GR: bool = false; @@ -397,255 +394,28 @@ impl sciter::EventHandler for SciterSession { impl SciterSession { pub fn new(cmd: String, id: String, password: String, args: Vec) -> Self { let session: Session = Session { - cmd, + cmd: cmd.clone(), id: id.clone(), password: password.clone(), args, ..Default::default() }; - session.lc.write().unwrap().initialize( - id, - session.is_file_transfer(), - session.is_port_forward(), - ); + + let conn_type = if cmd.eq("--file-transfer") { + ConnType::FILE_TRANSFER + } else if cmd.eq("--port-forward") { + ConnType::PORT_FORWARD + } else if cmd.eq("--rdp") { + ConnType::RDP + } else { + ConnType::DEFAULT_CONN + }; + + session.lc.write().unwrap().initialize(id, conn_type); Self(session) } - // TODO - fn start_keyboard_hook(&'static self) { - if self.is_port_forward() || self.is_file_transfer() { - return; - } - if KEYBOARD_HOOKED.swap(true, Ordering::SeqCst) { - return; - } - log::info!("keyboard hooked"); - let me = self.clone(); - let peer = self.peer_platform(); - let is_win = peer == "Windows"; - #[cfg(windows)] - crate::platform::windows::enable_lowlevel_keyboard(std::ptr::null_mut() as _); - std::thread::spawn(move || { - // This will block. - std::env::set_var("KEYBOARD_ONLY", "y"); // pass to rdev - use rdev::{EventType::*, *}; - let func = move |evt: Event| { - if !IS_IN.load(Ordering::SeqCst) || !SERVER_KEYBOARD_ENABLED.load(Ordering::SeqCst) - { - return; - } - let (key, down) = match evt.event_type { - KeyPress(k) => (k, 1), - KeyRelease(k) => (k, 0), - _ => return, - }; - let alt = get_key_state(enigo::Key::Alt); - #[cfg(windows)] - let ctrl = { - let mut tmp = get_key_state(enigo::Key::Control); - unsafe { - if IS_ALT_GR { - if alt || key == Key::AltGr { - if tmp { - tmp = false; - } - } else { - IS_ALT_GR = false; - } - } - } - tmp - }; - #[cfg(not(windows))] - let ctrl = get_key_state(enigo::Key::Control); - let shift = get_key_state(enigo::Key::Shift); - #[cfg(windows)] - let command = crate::platform::windows::get_win_key_state(); - #[cfg(not(windows))] - let command = get_key_state(enigo::Key::Meta); - let control_key = match key { - Key::Alt => Some(ControlKey::Alt), - Key::AltGr => Some(ControlKey::RAlt), - Key::Backspace => Some(ControlKey::Backspace), - Key::ControlLeft => { - // when pressing AltGr, an extra VK_LCONTROL with a special - // scancode with bit 9 set is sent, let's ignore this. - #[cfg(windows)] - if evt.scan_code & 0x200 != 0 { - unsafe { - IS_ALT_GR = true; - } - return; - } - Some(ControlKey::Control) - } - Key::ControlRight => Some(ControlKey::RControl), - Key::DownArrow => Some(ControlKey::DownArrow), - Key::Escape => Some(ControlKey::Escape), - Key::F1 => Some(ControlKey::F1), - Key::F10 => Some(ControlKey::F10), - Key::F11 => Some(ControlKey::F11), - Key::F12 => Some(ControlKey::F12), - Key::F2 => Some(ControlKey::F2), - Key::F3 => Some(ControlKey::F3), - Key::F4 => Some(ControlKey::F4), - Key::F5 => Some(ControlKey::F5), - Key::F6 => Some(ControlKey::F6), - Key::F7 => Some(ControlKey::F7), - Key::F8 => Some(ControlKey::F8), - Key::F9 => Some(ControlKey::F9), - Key::LeftArrow => Some(ControlKey::LeftArrow), - Key::MetaLeft => Some(ControlKey::Meta), - Key::MetaRight => Some(ControlKey::RWin), - Key::Return => Some(ControlKey::Return), - Key::RightArrow => Some(ControlKey::RightArrow), - Key::ShiftLeft => Some(ControlKey::Shift), - Key::ShiftRight => Some(ControlKey::RShift), - Key::Space => Some(ControlKey::Space), - Key::Tab => Some(ControlKey::Tab), - Key::UpArrow => Some(ControlKey::UpArrow), - Key::Delete => { - if is_win && ctrl && alt { - // me.ctrl_alt_del(); // TODO - return; - } - Some(ControlKey::Delete) - } - Key::Apps => Some(ControlKey::Apps), - Key::Cancel => Some(ControlKey::Cancel), - Key::Clear => Some(ControlKey::Clear), - Key::Kana => Some(ControlKey::Kana), - Key::Hangul => Some(ControlKey::Hangul), - Key::Junja => Some(ControlKey::Junja), - Key::Final => Some(ControlKey::Final), - Key::Hanja => Some(ControlKey::Hanja), - Key::Hanji => Some(ControlKey::Hanja), - Key::Convert => Some(ControlKey::Convert), - Key::Print => Some(ControlKey::Print), - Key::Select => Some(ControlKey::Select), - Key::Execute => Some(ControlKey::Execute), - Key::PrintScreen => Some(ControlKey::Snapshot), - Key::Help => Some(ControlKey::Help), - Key::Sleep => Some(ControlKey::Sleep), - Key::Separator => Some(ControlKey::Separator), - Key::KpReturn => Some(ControlKey::NumpadEnter), - Key::Kp0 => Some(ControlKey::Numpad0), - Key::Kp1 => Some(ControlKey::Numpad1), - Key::Kp2 => Some(ControlKey::Numpad2), - Key::Kp3 => Some(ControlKey::Numpad3), - Key::Kp4 => Some(ControlKey::Numpad4), - Key::Kp5 => Some(ControlKey::Numpad5), - Key::Kp6 => Some(ControlKey::Numpad6), - Key::Kp7 => Some(ControlKey::Numpad7), - Key::Kp8 => Some(ControlKey::Numpad8), - Key::Kp9 => Some(ControlKey::Numpad9), - Key::KpDivide => Some(ControlKey::Divide), - Key::KpMultiply => Some(ControlKey::Multiply), - Key::KpDecimal => Some(ControlKey::Decimal), - Key::KpMinus => Some(ControlKey::Subtract), - Key::KpPlus => Some(ControlKey::Add), - Key::CapsLock | Key::NumLock | Key::ScrollLock => { - return; - } - Key::Home => Some(ControlKey::Home), - Key::End => Some(ControlKey::End), - Key::Insert => Some(ControlKey::Insert), - Key::PageUp => Some(ControlKey::PageUp), - Key::PageDown => Some(ControlKey::PageDown), - Key::Pause => Some(ControlKey::Pause), - _ => None, - }; - let mut key_event = KeyEvent::new(); - if let Some(k) = control_key { - key_event.set_control_key(k); - } else { - let mut chr = match evt.name { - Some(ref s) => { - if s.len() <= 2 { - // exclude chinese characters - s.chars().next().unwrap_or('\0') - } else { - '\0' - } - } - _ => '\0', - }; - if chr == '·' { - // special for Chinese - chr = '`'; - } - if chr == '\0' { - chr = match key { - Key::Num1 => '1', - Key::Num2 => '2', - Key::Num3 => '3', - Key::Num4 => '4', - Key::Num5 => '5', - Key::Num6 => '6', - Key::Num7 => '7', - Key::Num8 => '8', - Key::Num9 => '9', - Key::Num0 => '0', - Key::KeyA => 'a', - Key::KeyB => 'b', - Key::KeyC => 'c', - Key::KeyD => 'd', - Key::KeyE => 'e', - Key::KeyF => 'f', - Key::KeyG => 'g', - Key::KeyH => 'h', - Key::KeyI => 'i', - Key::KeyJ => 'j', - Key::KeyK => 'k', - Key::KeyL => 'l', - Key::KeyM => 'm', - Key::KeyN => 'n', - Key::KeyO => 'o', - Key::KeyP => 'p', - Key::KeyQ => 'q', - Key::KeyR => 'r', - Key::KeyS => 's', - Key::KeyT => 't', - Key::KeyU => 'u', - Key::KeyV => 'v', - Key::KeyW => 'w', - Key::KeyX => 'x', - Key::KeyY => 'y', - Key::KeyZ => 'z', - Key::Comma => ',', - Key::Dot => '.', - Key::SemiColon => ';', - Key::Quote => '\'', - Key::LeftBracket => '[', - Key::RightBracket => ']', - Key::BackSlash => '\\', - Key::Minus => '-', - Key::Equal => '=', - Key::BackQuote => '`', - _ => '\0', - } - } - if chr != '\0' { - if chr == 'l' && is_win && command { - // me.lock_screen(); // TODO - return; - } - key_event.set_chr(chr as _); - } else { - log::error!("Unknown key {:?}", evt); - return; - } - } - // me.key_down_or_up(down, key_event, alt, ctrl, shift, command); // TODO - }; - if let Err(error) = rdev::listen(func) { - log::error!("rdev: {:?}", error); - } - }); - } - - // TODO fn get_custom_image_quality(&mut self) -> Value { let mut v = Value::array(0); for x in self.lc.read().unwrap().custom_image_quality.iter() { @@ -654,7 +424,6 @@ impl SciterSession { v } - // TODO fn supported_hwcodec(&self) -> Value { #[cfg(feature = "hwcodec")] { @@ -679,7 +448,6 @@ impl SciterSession { } } - // TODO fn save_size(&mut self, x: i32, y: i32, w: i32, h: i32) { let size = (x, y, w, h); let mut config = self.load_config(); @@ -765,7 +533,6 @@ impl SciterSession { pi } - // close_state sciter only fn save_close_state(&mut self, k: String, v: String) { self.close_state.insert(k, v); } diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 2f401e26f..c08cc09ce 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -2,7 +2,7 @@ use crate::client::io_loop::Remote; use crate::client::{ check_if_retry, get_key_state, handle_hash, handle_login_from_ui, handle_test_delay, input_os_password, load_config, send_mouse, start_video_audio_threads, FileManager, Key, - LoginConfigHandler, QualityStatus, KEY_MAP, + LoginConfigHandler, QualityStatus, KEY_MAP, SERVER_KEYBOARD_ENABLED, }; use crate::common; @@ -11,15 +11,20 @@ use async_trait::async_trait; use hbb_common::config::{Config, LocalConfig, PeerConfig}; +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 std::collections::HashMap; use std::ops::{Deref, DerefMut}; -use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::atomic::{AtomicUsize, Ordering, AtomicBool}; use std::sync::{Arc, Mutex, RwLock}; +/// IS_IN KEYBOARD_HOOKED sciter only +pub static IS_IN: AtomicBool = AtomicBool::new(false); +static KEYBOARD_HOOKED: AtomicBool = AtomicBool::new(false); + #[derive(Clone, Default)] pub struct Session { pub cmd: String, @@ -219,7 +224,7 @@ impl Session { self.lc.read().unwrap().info.platform.clone() } - pub fn ctrl_alt_del(&mut self) { + pub fn ctrl_alt_del(&self) { if self.peer_platform() == "Windows" { let mut key_event = KeyEvent::new(); key_event.set_control_key(ControlKey::CtrlAltDel); @@ -339,7 +344,7 @@ impl Session { self.send(Data::Message(msg_out)); } - pub fn lock_screen(&mut self) { + pub fn lock_screen(&self) { let mut key_event = KeyEvent::new(); key_event.set_control_key(ControlKey::LockScreen); self.key_down_or_up(1, key_event, false, false, false, false); @@ -422,7 +427,7 @@ impl Session { } pub fn send_mouse( - &mut self, + &self, mask: i32, x: i32, y: i32, @@ -588,19 +593,16 @@ impl Interface for Session { } } - // TODO flutter fn is_file_transfer(&self) -> bool { - self.cmd == "--file-transfer" + self.lc.read().unwrap().conn_type.eq(&ConnType::FILE_TRANSFER) } - // TODO flutter fn is_port_forward(&self) -> bool { - self.cmd == "--port-forward" || self.is_rdp() + self.lc.read().unwrap().conn_type.eq(&ConnType::PORT_FORWARD) } - // TODO flutter fn is_rdp(&self) -> bool { - self.cmd == "--rdp" + self.lc.read().unwrap().conn_type.eq(&ConnType::RDP) } fn msgbox(&self, msgtype: &str, title: &str, text: &str) { @@ -658,7 +660,8 @@ impl Interface for Session { crate::platform::windows::add_recent_document(&path); } } - // self.start_keyboard_hook(); // TODO + // TODO use event callbcak + self.start_keyboard_hook(); } async fn handle_hash(&mut self, pass: &str, hash: Hash, peer: &mut Stream) { @@ -699,6 +702,242 @@ impl Interface for Session { } } +// TODO use event callbcak +// sciter only +impl Session { + fn start_keyboard_hook(&self) { + if self.is_port_forward() || self.is_file_transfer() { + return; + } + if KEYBOARD_HOOKED.swap(true, Ordering::SeqCst) { + return; + } + log::info!("keyboard hooked"); + let me = self.clone(); + let peer = self.peer_platform(); + let is_win = peer == "Windows"; + #[cfg(windows)] + crate::platform::windows::enable_lowlevel_keyboard(std::ptr::null_mut() as _); + std::thread::spawn(move || { + // This will block. + std::env::set_var("KEYBOARD_ONLY", "y"); // pass to rdev + use rdev::{EventType::*, *}; + let func = move |evt: Event| { + if !IS_IN.load(Ordering::SeqCst) || !SERVER_KEYBOARD_ENABLED.load(Ordering::SeqCst) + { + return; + } + let (key, down) = match evt.event_type { + KeyPress(k) => (k, 1), + KeyRelease(k) => (k, 0), + _ => return, + }; + let alt = get_key_state(enigo::Key::Alt); + #[cfg(windows)] + let ctrl = { + let mut tmp = get_key_state(enigo::Key::Control); + unsafe { + if IS_ALT_GR { + if alt || key == Key::AltGr { + if tmp { + tmp = false; + } + } else { + IS_ALT_GR = false; + } + } + } + tmp + }; + #[cfg(not(windows))] + let ctrl = get_key_state(enigo::Key::Control); + let shift = get_key_state(enigo::Key::Shift); + #[cfg(windows)] + let command = crate::platform::windows::get_win_key_state(); + #[cfg(not(windows))] + let command = get_key_state(enigo::Key::Meta); + let control_key = match key { + Key::Alt => Some(ControlKey::Alt), + Key::AltGr => Some(ControlKey::RAlt), + Key::Backspace => Some(ControlKey::Backspace), + Key::ControlLeft => { + // when pressing AltGr, an extra VK_LCONTROL with a special + // scancode with bit 9 set is sent, let's ignore this. + #[cfg(windows)] + if evt.scan_code & 0x200 != 0 { + unsafe { + IS_ALT_GR = true; + } + return; + } + Some(ControlKey::Control) + } + Key::ControlRight => Some(ControlKey::RControl), + Key::DownArrow => Some(ControlKey::DownArrow), + Key::Escape => Some(ControlKey::Escape), + Key::F1 => Some(ControlKey::F1), + Key::F10 => Some(ControlKey::F10), + Key::F11 => Some(ControlKey::F11), + Key::F12 => Some(ControlKey::F12), + Key::F2 => Some(ControlKey::F2), + Key::F3 => Some(ControlKey::F3), + Key::F4 => Some(ControlKey::F4), + Key::F5 => Some(ControlKey::F5), + Key::F6 => Some(ControlKey::F6), + Key::F7 => Some(ControlKey::F7), + Key::F8 => Some(ControlKey::F8), + Key::F9 => Some(ControlKey::F9), + Key::LeftArrow => Some(ControlKey::LeftArrow), + Key::MetaLeft => Some(ControlKey::Meta), + Key::MetaRight => Some(ControlKey::RWin), + Key::Return => Some(ControlKey::Return), + Key::RightArrow => Some(ControlKey::RightArrow), + Key::ShiftLeft => Some(ControlKey::Shift), + Key::ShiftRight => Some(ControlKey::RShift), + Key::Space => Some(ControlKey::Space), + Key::Tab => Some(ControlKey::Tab), + Key::UpArrow => Some(ControlKey::UpArrow), + Key::Delete => { + if is_win && ctrl && alt { + me.ctrl_alt_del(); + return; + } + Some(ControlKey::Delete) + } + Key::Apps => Some(ControlKey::Apps), + Key::Cancel => Some(ControlKey::Cancel), + Key::Clear => Some(ControlKey::Clear), + Key::Kana => Some(ControlKey::Kana), + Key::Hangul => Some(ControlKey::Hangul), + Key::Junja => Some(ControlKey::Junja), + Key::Final => Some(ControlKey::Final), + Key::Hanja => Some(ControlKey::Hanja), + Key::Hanji => Some(ControlKey::Hanja), + Key::Convert => Some(ControlKey::Convert), + Key::Print => Some(ControlKey::Print), + Key::Select => Some(ControlKey::Select), + Key::Execute => Some(ControlKey::Execute), + Key::PrintScreen => Some(ControlKey::Snapshot), + Key::Help => Some(ControlKey::Help), + Key::Sleep => Some(ControlKey::Sleep), + Key::Separator => Some(ControlKey::Separator), + Key::KpReturn => Some(ControlKey::NumpadEnter), + Key::Kp0 => Some(ControlKey::Numpad0), + Key::Kp1 => Some(ControlKey::Numpad1), + Key::Kp2 => Some(ControlKey::Numpad2), + Key::Kp3 => Some(ControlKey::Numpad3), + Key::Kp4 => Some(ControlKey::Numpad4), + Key::Kp5 => Some(ControlKey::Numpad5), + Key::Kp6 => Some(ControlKey::Numpad6), + Key::Kp7 => Some(ControlKey::Numpad7), + Key::Kp8 => Some(ControlKey::Numpad8), + Key::Kp9 => Some(ControlKey::Numpad9), + Key::KpDivide => Some(ControlKey::Divide), + Key::KpMultiply => Some(ControlKey::Multiply), + Key::KpDecimal => Some(ControlKey::Decimal), + Key::KpMinus => Some(ControlKey::Subtract), + Key::KpPlus => Some(ControlKey::Add), + Key::CapsLock | Key::NumLock | Key::ScrollLock => { + return; + } + Key::Home => Some(ControlKey::Home), + Key::End => Some(ControlKey::End), + Key::Insert => Some(ControlKey::Insert), + Key::PageUp => Some(ControlKey::PageUp), + Key::PageDown => Some(ControlKey::PageDown), + Key::Pause => Some(ControlKey::Pause), + _ => None, + }; + let mut key_event = KeyEvent::new(); + if let Some(k) = control_key { + key_event.set_control_key(k); + } else { + let mut chr = match evt.name { + Some(ref s) => { + if s.len() <= 2 { + // exclude chinese characters + s.chars().next().unwrap_or('\0') + } else { + '\0' + } + } + _ => '\0', + }; + if chr == '·' { + // special for Chinese + chr = '`'; + } + if chr == '\0' { + chr = match key { + Key::Num1 => '1', + Key::Num2 => '2', + Key::Num3 => '3', + Key::Num4 => '4', + Key::Num5 => '5', + Key::Num6 => '6', + Key::Num7 => '7', + Key::Num8 => '8', + Key::Num9 => '9', + Key::Num0 => '0', + Key::KeyA => 'a', + Key::KeyB => 'b', + Key::KeyC => 'c', + Key::KeyD => 'd', + Key::KeyE => 'e', + Key::KeyF => 'f', + Key::KeyG => 'g', + Key::KeyH => 'h', + Key::KeyI => 'i', + Key::KeyJ => 'j', + Key::KeyK => 'k', + Key::KeyL => 'l', + Key::KeyM => 'm', + Key::KeyN => 'n', + Key::KeyO => 'o', + Key::KeyP => 'p', + Key::KeyQ => 'q', + Key::KeyR => 'r', + Key::KeyS => 's', + Key::KeyT => 't', + Key::KeyU => 'u', + Key::KeyV => 'v', + Key::KeyW => 'w', + Key::KeyX => 'x', + Key::KeyY => 'y', + Key::KeyZ => 'z', + Key::Comma => ',', + Key::Dot => '.', + Key::SemiColon => ';', + Key::Quote => '\'', + Key::LeftBracket => '[', + Key::RightBracket => ']', + Key::BackSlash => '\\', + Key::Minus => '-', + Key::Equal => '=', + Key::BackQuote => '`', + _ => '\0', + } + } + if chr != '\0' { + if chr == 'l' && is_win && command { + me.lock_screen(); + return; + } + key_event.set_chr(chr as _); + } else { + log::error!("Unknown key {:?}", evt); + return; + } + } + me.key_down_or_up(down, key_event, alt, ctrl, shift, command); // TODO + }; + if let Err(error) = rdev::listen(func) { + log::error!("rdev: {:?}", error); + } + }); + } +} + #[tokio::main(flavor = "current_thread")] pub async fn io_loop(handler: Session) { let (sender, mut receiver) = mpsc::unbounded_channel::(); From a435fc999a49ee99085f3607fa7c1db2b5eb4617 Mon Sep 17 00:00:00 2001 From: csf Date: Thu, 1 Sep 2022 18:23:06 +0800 Subject: [PATCH 0341/2015] mobile build --- src/client.rs | 9 +++++---- src/client/io_loop.rs | 3 +++ src/ui_session_interface.rs | 13 +++++++++++-- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/client.rs b/src/client.rs index 8f6cb12bb..25061bcfe 100644 --- a/src/client.rs +++ b/src/client.rs @@ -11,7 +11,6 @@ use cpal::{ traits::{DeviceTrait, HostTrait, StreamTrait}, Device, Host, StreamConfig, }; -use enigo::{Enigo, KeyboardControllable}; use magnum_opus::{Channels::*, Decoder as AudioDecoder}; use sha2::{Digest, Sha256}; use uuid::Uuid; @@ -38,7 +37,6 @@ use hbb_common::{ }; pub use helper::LatencyController; pub use helper::*; -use scrap::Image; use scrap::{ codec::{Decoder, DecoderCfg}, VpxDecoderConfig, VpxVideoCodecId, @@ -61,14 +59,17 @@ pub struct Client; #[cfg(not(any(target_os = "android", target_os = "linux")))] lazy_static::lazy_static! { -static ref AUDIO_HOST: Host = cpal::default_host(); + static ref AUDIO_HOST: Host = cpal::default_host(); } +#[cfg(not(any(target_os = "android", target_os = "ios")))] lazy_static::lazy_static! { - static ref ENIGO: Arc> = Arc::new(Mutex::new(Enigo::new())); + static ref ENIGO: Arc> = Arc::new(Mutex::new(enigo::Enigo::new())); } +#[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn get_key_state(key: enigo::Key) -> bool { + use enigo::KeyboardControllable; #[cfg(target_os = "macos")] if key == enigo::Key::NumLock { return true; diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index f7f8f4f18..e61690c32 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -2,6 +2,7 @@ use crate::client::{ Client, CodecFormat, FileManager, 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}; use crate::ui_session_interface::{InvokeUi, Session}; @@ -235,6 +236,7 @@ impl Remote { 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 @@ -266,6 +268,7 @@ impl Remote { Some(tx) } + // TODO fn load_last_jobs(&mut self) { log::info!("start load last jobs"); self.handler.clear_all_jobs(); diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index c08cc09ce..d89ce2d3b 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -1,11 +1,12 @@ use crate::client::io_loop::Remote; use crate::client::{ - check_if_retry, get_key_state, handle_hash, handle_login_from_ui, handle_test_delay, + check_if_retry, handle_hash, handle_login_from_ui, handle_test_delay, input_os_password, load_config, send_mouse, start_video_audio_threads, FileManager, Key, LoginConfigHandler, QualityStatus, KEY_MAP, SERVER_KEYBOARD_ENABLED, }; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use crate::client::get_key_state; use crate::common; - use crate::{client::Data, client::Interface}; use async_trait::async_trait; @@ -129,6 +130,7 @@ impl Session { self.send(Data::Message(msg)); } + #[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn t(&self, name: String) -> String { crate::client::translate(name) } @@ -271,9 +273,11 @@ impl Session { { key_event.modifiers.push(ControlKey::Meta.into()); } + #[cfg(not(any(target_os = "android", target_os = "ios")))] if get_key_state(enigo::Key::CapsLock) { key_event.modifiers.push(ControlKey::CapsLock.into()); } + #[cfg(not(any(target_os = "android", target_os = "ios")))] if self.peer_platform() != "Mac OS" { if get_key_state(enigo::Key::NumLock) && common::valid_for_numlock(&key_event) { key_event.modifiers.push(ControlKey::NumLock.into()); @@ -318,6 +322,7 @@ impl Session { return "".to_owned(); } + #[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn get_icon(&self) -> String { crate::get_icon() } @@ -661,6 +666,7 @@ impl Interface for Session { } } // TODO use event callbcak + #[cfg(not(any(target_os = "android", target_os = "ios")))] self.start_keyboard_hook(); } @@ -704,6 +710,7 @@ impl Interface for Session { // TODO use event callbcak // sciter only +#[cfg(not(any(target_os = "android", target_os = "ios")))] impl Session { fn start_keyboard_hook(&self) { if self.is_port_forward() || self.is_file_transfer() { @@ -948,6 +955,7 @@ pub async fn io_loop(handler: Session) { if key.is_empty() { key = crate::platform::get_license_key(); } + #[cfg(not(any(target_os = "android", target_os = "ios")))] if handler.is_port_forward() { if handler.is_rdp() { let port = handler @@ -1053,6 +1061,7 @@ pub async fn io_loop(handler: Session) { remote.sync_jobs_status_to_local().await; } +#[cfg(not(any(target_os = "android", target_os = "ios")))] async fn start_one_port_forward( handler: Session, port: i32, From 59f0ffa82fafb7f8ace87b1a5503f2ec2cdd418d Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 31 Aug 2022 16:41:05 +0800 Subject: [PATCH 0342/2015] flutter_desktop: menu bar, switch menu & shrink-stretch -> adaptive Signed-off-by: fufesou --- flutter/lib/desktop/widgets/popup_menu.dart | 38 ++++++++++--------- .../lib/desktop/widgets/remote_menubar.dart | 7 ++-- flutter/lib/models/model.dart | 38 +++---------------- flutter/lib/models/native_model.dart | 22 +++++------ 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/fr.rs | 2 + src/lang/hu.rs | 2 + src/lang/id.rs | 2 + src/lang/it.rs | 2 + src/lang/ja.rs | 2 + src/lang/ko.rs | 2 + src/lang/pl.rs | 2 + src/lang/pt_PT.rs | 2 + src/lang/ptbr.rs | 2 + src/lang/ru.rs | 2 + src/lang/sk.rs | 2 + src/lang/template.rs | 2 + src/lang/tr.rs | 2 + src/lang/tw.rs | 2 + src/lang/vn.rs | 2 + 25 files changed, 81 insertions(+), 66 deletions(-) diff --git a/flutter/lib/desktop/widgets/popup_menu.dart b/flutter/lib/desktop/widgets/popup_menu.dart index 3d5fdf7f6..6dbe4f8cd 100644 --- a/flutter/lib/desktop/widgets/popup_menu.dart +++ b/flutter/lib/desktop/widgets/popup_menu.dart @@ -315,29 +315,31 @@ abstract class MenuEntrySwitchBase extends MenuEntryBase { mod_menu.PopupMenuItem( padding: EdgeInsets.zero, height: conf.height, - child: Obx( - () => SwitchListTile( - value: curOption.value, - onChanged: (v) { - setOption(v); - }, - title: Container( - alignment: AlignmentDirectional.centerStart, - constraints: BoxConstraints(minHeight: conf.height), - child: Text( + child: TextButton( + child: Container( + alignment: AlignmentDirectional.centerStart, + height: conf.height, + child: Row(children: [ + // const SizedBox(width: MenuConfig.midPadding), + Text( text, style: const TextStyle( color: Colors.black, fontSize: MenuConfig.fontSize, fontWeight: FontWeight.normal), - )), - dense: true, - visualDensity: const VisualDensity( - horizontal: VisualDensity.minimumDensity, - vertical: VisualDensity.minimumDensity, - ), - contentPadding: const EdgeInsets.only(left: 8.0), - ), + ), + Expanded( + child: Align( + alignment: Alignment.centerRight, + child: Obx(() => Switch( + value: curOption.value, + onChanged: (v) => setOption(v), + )), + )) + ])), + onPressed: () { + setOption(!curOption.value); + }, ), ) ]; diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 0e931dd71..2e8c7fb63 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -406,14 +406,13 @@ class _RemoteMenubarState extends State { MenuEntryRadios( text: translate('Ratio'), optionsGetter: () => [ - Tuple2(translate('Original'), 'original'), - Tuple2(translate('Shrink'), 'shrink'), - Tuple2(translate('Stretch'), 'stretch'), + Tuple2(translate('Scale original'), 'original'), + Tuple2(translate('Scale adaptive'), 'adaptive'), ], curOptionGetter: () async { return await bind.sessionGetOption( id: widget.id, arg: 'view-style') ?? - ''; + 'adaptive'; }, optionSetter: (String v) async { await bind.sessionPeerOption( diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index c01451280..da06b7fb9 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -497,39 +497,11 @@ class CanvasModel with ChangeNotifier { return; } - final s1 = size.width / (parent.target?.ffiModel.display.width ?? 720); - final s2 = size.height / (parent.target?.ffiModel.display.height ?? 1280); - - // Closure to perform shrink operation. - final shrinkOp = () { - final s = s1 < s2 ? s1 : s2; - if (s < 1) { - _scale = s; - } - }; - // Closure to perform stretch operation. - final stretchOp = () { - final s = s1 < s2 ? s1 : s2; - if (s > 1) { - _scale = s; - } - }; - // Closure to perform default operation(set the scale to 1.0). - final defaultOp = () { - _scale = 1.0; - }; - - // // On desktop, shrink is the default behavior. - // if (isDesktop) { - // shrinkOp(); - // } else { - defaultOp(); - // } - - if (style == 'shrink') { - shrinkOp(); - } else if (style == 'stretch') { - stretchOp(); + _scale = 1.0; + if (style == 'adaptive') { + final s1 = size.width / (parent.target?.ffiModel.display.width ?? 720); + final s2 = size.height / (parent.target?.ffiModel.display.height ?? 1280); + _scale = s1 < s2 ? s1 : s2; } _x = (size.width - getDisplayWidth() * _scale) / 2; diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index 57372cdb9..2a8391723 100644 --- a/flutter/lib/models/native_model.dart +++ b/flutter/lib/models/native_model.dart @@ -30,7 +30,7 @@ class PlatformFFI { String _dir = ''; String _homeDir = ''; F2? _translate; - var _eventHandlers = Map>(); + final _eventHandlers = Map>(); late RustdeskImpl _ffiBind; late String _appType; void Function(Map)? _eventCallback; @@ -50,27 +50,27 @@ class PlatformFFI { } bool registerEventHandler( - String event_name, String handler_name, HandleEvent handler) { - debugPrint('registerEventHandler $event_name $handler_name'); - var handlers = _eventHandlers[event_name]; + String eventName, String handlerName, HandleEvent handler) { + debugPrint('registerEventHandler $eventName $handlerName'); + var handlers = _eventHandlers[eventName]; if (handlers == null) { - _eventHandlers[event_name] = {handler_name: handler}; + _eventHandlers[eventName] = {handlerName: handler}; return true; } else { - if (handlers.containsKey(handler_name)) { + if (handlers.containsKey(handlerName)) { return false; } else { - handlers[handler_name] = handler; + handlers[handlerName] = handler; return true; } } } - void unregisterEventHandler(String event_name, String handler_name) { - debugPrint('unregisterEventHandler $event_name $handler_name'); - var handlers = _eventHandlers[event_name]; + void unregisterEventHandler(String eventName, String handlerName) { + debugPrint('unregisterEventHandler $eventName $handlerName'); + var handlers = _eventHandlers[eventName]; if (handlers != null) { - handlers.remove(handler_name); + handlers.remove(handlerName); } } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index fdb23f88e..c6efaee85 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -317,5 +317,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Relay Connection", "中继连接"), ("Secure Connection", "安全连接"), ("Insecure Connection", "非安全连接"), + ("Scale original", "原始尺寸"), + ("Scale adaptive", "适应窗口"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index d9ddf78cc..99d0ae694 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -317,5 +317,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Relay Connection", "Připojení relé"), ("Secure Connection", "Zabezpečené připojení"), ("Insecure Connection", "Nezabezpečené připojení"), + ("Scale original", "Měřítko původní"), + ("Scale adaptive", "Měřítko adaptivní"), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 0e2d99425..883ef27a5 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -317,5 +317,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Relay Connection", "Relæforbindelse"), ("Secure Connection", "Sikker forbindelse"), ("Insecure Connection", "Usikker forbindelse"), + ("Scale original", "Skala original"), + ("Scale adaptive", "Skala adaptiv"), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 20cd9330e..649199f0d 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -317,5 +317,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Relay Connection", "Relaisverbindung"), ("Secure Connection", "Sichere Verbindung"), ("Insecure Connection", "Unsichere Verbindung"), + ("Scale original", "Original skalieren"), + ("Scale adaptive", "Adaptiv skalieren"), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index fe12e2d24..34b89c350 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -317,5 +317,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Relay Connection", "Relajsa Konekto"), ("Secure Connection", "Sekura Konekto"), ("Insecure Connection", "Nesekura Konekto"), + ("Scale original", "Skalo originalo"), + ("Scale adaptive", "Skalo adapta"), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 313ea8cac..82395dd7a 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -317,5 +317,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Relay Connection", "Conexión de relé"), ("Secure Connection", "Conexión segura"), ("Insecure Connection", "Conexión insegura"), + ("Scale original", "escala originales"), + ("Scale adaptive", "Adaptable a escala"), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index c8b12243f..00cfe3a3c 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -317,5 +317,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Relay Connection", "Connexion relais"), ("Secure Connection", "Connexion sécurisée"), ("Insecure Connection", "Connexion non sécurisée"), + ("Scale original", "Échelle d'origine"), + ("Scale adaptive", "Échelle adaptative"), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index a6356b000..d1a259119 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -317,5 +317,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Relay Connection", "Relé csatlakozás"), ("Secure Connection", "Biztonságos kapcsolat"), ("Insecure Connection", "Nem biztonságos kapcsolat"), + ("Scale original", "Eredeti méretarány"), + ("Scale adaptive", "Skála adaptív"), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 8548eb6bc..3447d3388 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -317,5 +317,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Relay Connection", "Koneksi Relay"), ("Secure Connection", "Koneksi aman"), ("Insecure Connection", "Koneksi Tidak Aman"), + ("Scale original", "Skala asli"), + ("Scale adaptive", "Skala adaptif"), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index fdf8d27d9..65cdd4b3d 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -316,5 +316,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Relay Connection", "Collegamento a relè"), ("Secure Connection", "Connessione sicura"), ("Insecure Connection", "Connessione insicura"), + ("Scale original", "Scala originale"), + ("Scale adaptive", "Scala adattiva"), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 1d031f2f2..cbe31bf9f 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -314,5 +314,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Relay Connection", "リレー接続"), ("Secure Connection", "安全な接続"), ("Insecure Connection", "安全でない接続"), + ("Scale original", "オリジナルサイズ"), + ("Scale adaptive", "フィットウィンドウ"), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 19d4c7ddf..44ba589f6 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -314,5 +314,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Relay Connection", "릴레이 연결"), ("Secure Connection", "보안 연결"), ("Insecure Connection", "안전하지 않은 연결"), + ("Scale original", "원래 크기"), + ("Scale adaptive", "맞는 창"), ].iter().cloned().collect(); } \ No newline at end of file diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 251c349a2..42bd49bbb 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -318,5 +318,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Relay Connection", "Połączenie przekaźnika"), ("Secure Connection", "Bezpieczne połączenie"), ("Insecure Connection", "Niepewne połączenie"), + ("Scale original", "Skala oryginalna"), + ("Scale adaptive", "Skala adaptacyjna"), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index fd4384767..e8c62d78a 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -314,5 +314,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Relay Connection", "Conexão de relé"), ("Secure Connection", "Conexão segura"), ("Insecure Connection", "Conexão insegura"), + ("Scale original", "Escala original"), + ("Scale adaptive", "Escala adaptável"), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 85eda60e6..cdd4128b5 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -317,5 +317,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Relay Connection", ""), ("Secure Connection", ""), ("Insecure Connection", ""), + ("Scale original", ""), + ("Scale adaptive", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index def560217..5bbdd846d 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -317,5 +317,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Relay Connection", "Релейное соединение"), ("Secure Connection", "Безопасное соединение"), ("Insecure Connection", "Небезопасное соединение"), + ("Scale original", "Оригинал масштаба"), + ("Scale adaptive", "Масштаб адаптивный"), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 4c04618aa..b92d0aca6 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -317,5 +317,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Relay Connection", "Reléové pripojenie"), ("Secure Connection", "Zabezpečené pripojenie"), ("Insecure Connection", "Nezabezpečené pripojenie"), + ("Scale original", "Pôvodná mierka"), + ("Scale adaptive", "Prispôsobivá mierka"), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 081b7bf55..8cf46a196 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -317,5 +317,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Relay Connection", ""), ("Secure Connection", ""), ("Insecure Connection", ""), + ("Scale original", ""), + ("Scale adaptive", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 9738ed469..c5ec537b4 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -317,5 +317,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Relay Connection", "Röle Bağlantısı"), ("Secure Connection", "Güvenli bağlantı"), ("Insecure Connection", "Güvenli Bağlantı"), + ("Scale original", "Orijinali ölçeklendir"), + ("Scale adaptive", "Ölçek uyarlanabilir"), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 46276dd2a..836b5ad12 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -317,5 +317,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Relay Connection", "中繼連接"), ("Secure Connection", "安全連接"), ("Insecure Connection", "非安全連接"), + ("Scale original", "原始尺寸"), + ("Scale adaptive", "適應窗口"), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 474e57337..ba9e4cb86 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -317,5 +317,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Relay Connection", "Kết nối chuyển tiếp"), ("Secure Connection", "Kết nối an toàn"), ("Insecure Connection", "Kết nối không an toàn"), + ("Scale original", "Quy mô gốc"), + ("Scale adaptive", "Quy mô thích ứng"), ].iter().cloned().collect(); } From 4b9805b0f3211569680cab2f9b23e1966f237c89 Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 31 Aug 2022 18:41:55 +0800 Subject: [PATCH 0343/2015] flutter_desktop: custom image quality Signed-off-by: fufesou --- flutter/lib/common.dart | 86 +++++++------ flutter/lib/consts.dart | 7 +- .../desktop/pages/connection_tab_page.dart | 3 +- flutter/lib/desktop/pages/remote_page.dart | 15 +-- flutter/lib/desktop/widgets/popup_menu.dart | 116 ++++++++++++------ .../lib/desktop/widgets/remote_menubar.dart | 89 ++++++++++---- flutter/lib/models/model.dart | 15 ++- src/flutter_ffi.rs | 30 +++-- 8 files changed, 240 insertions(+), 121 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 6ba917bf3..e8632caaa 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -427,7 +427,45 @@ class CustomAlertDialog extends StatelessWidget { void msgBox( String type, String title, String text, OverlayDialogManager dialogManager, {bool? hasCancel}) { - var wrap = (String text, void Function() onPressed) => ButtonTheme( + dialogManager.dismissAll(); + List buttons = []; + if (type != "connecting" && type != "success" && !type.contains("nook")) { + buttons.insert( + 0, + getMsgBoxButton(translate('OK'), () { + dialogManager.dismissAll(); + // https://github.com/fufesou/rustdesk/blob/5e9a31340b899822090a3731769ae79c6bf5f3e5/src/ui/common.tis#L263 + if (!type.contains("custom")) { + closeConnection(); + } + })); + } + hasCancel ??= !type.contains("error") && + !type.contains("nocancel") && + type != "restarting"; + if (hasCancel) { + buttons.insert( + 0, + getMsgBoxButton(translate('Cancel'), () { + dialogManager.dismissAll(); + })); + } + // TODO: test this button + if (type.contains("hasclose")) { + buttons.insert( + 0, + getMsgBoxButton(translate('Close'), () { + dialogManager.dismissAll(); + })); + } + dialogManager.show((setState, close) => CustomAlertDialog( + title: Text(translate(title), style: TextStyle(fontSize: 21)), + content: Text(translate(text), style: TextStyle(fontSize: 15)), + actions: buttons)); +} + +Widget getMsgBoxButton(String text, void Function() onPressed) { + return ButtonTheme( padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10), materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, //limits the touch area to the button area @@ -439,44 +477,14 @@ void msgBox( onPressed: onPressed, child: Text(translate(text), style: TextStyle(color: MyTheme.accent)))); +} +void msgBoxCommon(OverlayDialogManager dialogManager, String title, + Widget content, List buttons) { dialogManager.dismissAll(); - List buttons = []; - if (type != "connecting" && type != "success" && type.indexOf("nook") < 0) { - buttons.insert( - 0, - wrap(translate('OK'), () { - dialogManager.dismissAll(); - // https://github.com/fufesou/rustdesk/blob/5e9a31340b899822090a3731769ae79c6bf5f3e5/src/ui/common.tis#L263 - if (type.indexOf("custom") < 0) { - closeConnection(); - } - })); - } - if (hasCancel == null) { - // hasCancel = type != 'error'; - hasCancel = type.indexOf("error") < 0 && - type.indexOf("nocancel") < 0 && - type != "restarting"; - } - if (hasCancel) { - buttons.insert( - 0, - wrap(translate('Cancel'), () { - dialogManager.dismissAll(); - })); - } - // TODO: test this button - if (type.indexOf("hasclose") >= 0) { - buttons.insert( - 0, - wrap(translate('Close'), () { - dialogManager.dismissAll(); - })); - } dialogManager.show((setState, close) => CustomAlertDialog( title: Text(translate(title), style: TextStyle(fontSize: 21)), - content: Text(translate(text), style: TextStyle(fontSize: 15)), + content: content, actions: buttons)); } @@ -495,13 +503,13 @@ const G = M * K; String readableFileSize(double size) { if (size < K) { - return size.toStringAsFixed(2) + " B"; + return "${size.toStringAsFixed(2)} B"; } else if (size < M) { - return (size / K).toStringAsFixed(2) + " KB"; + return "${(size / K).toStringAsFixed(2)} KB"; } else if (size < G) { - return (size / M).toStringAsFixed(2) + " MB"; + return "${(size / M).toStringAsFixed(2)} MB"; } else { - return (size / G).toStringAsFixed(2) + " GB"; + return "${(size / G).toStringAsFixed(2)} GB"; } } diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 6c67e2ab9..95a6faaa2 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -8,7 +8,10 @@ const String kAppTypeDesktopPortForward = "port forward"; const String kTabLabelHomePage = "Home"; const String kTabLabelSettingPage = "Settings"; -const int kDefaultDisplayWidth = 1280; -const int kDefaultDisplayHeight = 720; +const int kMobileDefaultDisplayWidth = 720; +const int kMobileDefaultDisplayHeight = 1280; + +const int kDesktopDefaultDisplayWidth = 1080; +const int kDesktopDefaultDisplayHeight = 720; const kInvalidValueStr = "InvalidValueStr"; diff --git a/flutter/lib/desktop/pages/connection_tab_page.dart b/flutter/lib/desktop/pages/connection_tab_page.dart index 4175bd11b..5687c5c7e 100644 --- a/flutter/lib/desktop/pages/connection_tab_page.dart +++ b/flutter/lib/desktop/pages/connection_tab_page.dart @@ -61,6 +61,7 @@ class _ConnectionTabPageState extends State { final args = jsonDecode(call.arguments); final id = args['id']; window_on_top(windowId()); + ConnectionTypeState.init(id); tabController.add(TabInfo( key: id, label: id, @@ -108,7 +109,7 @@ class _ConnectionTabPageState extends State { }, tabBuilder: (key, icon, label, themeConf) => Obx(() { final connectionType = ConnectionTypeState.find(key); - if (!ConnectionTypeState.find(key).isValid()) { + if (!connectionType.isValid()) { return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index f723b17d0..16c04f572 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -689,11 +689,11 @@ class ImagePaint extends StatelessWidget { width: c.getDisplayWidth() * s, height: c.getDisplayHeight() * s, child: CustomPaint( - painter: new ImagePainter(image: m.image, x: 0, y: 0, scale: s), + painter: ImagePainter(image: m.image, x: 0, y: 0, scale: s), )); return Center( child: NotificationListener( - onNotification: (_notification) { + onNotification: (notification) { final percentX = _horizontal.position.extentBefore / (_horizontal.position.extentBefore + _horizontal.position.extentInside + @@ -716,8 +716,8 @@ class ImagePaint extends StatelessWidget { width: c.size.width, height: c.size.height, child: CustomPaint( - painter: new ImagePainter( - image: m.image, x: c.x / s, y: c.y / s, scale: s), + painter: + ImagePainter(image: m.image, x: c.x / s, y: c.y / s, scale: s), )); return _buildListener(imageWidget); } @@ -771,7 +771,7 @@ class CursorPaint extends StatelessWidget { // final adjust = m.adjustForKeyboard(); var s = c.scale; return CustomPaint( - painter: new ImagePainter( + painter: ImagePainter( image: m.image, x: m.x * s - m.hotx + c.x, y: m.y * s - m.hoty + c.y, @@ -796,15 +796,16 @@ class ImagePainter extends CustomPainter { @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 = new Paint(); + var paint = Paint(); paint.filterQuality = FilterQuality.medium; if (scale > 10.00000) { paint.filterQuality = FilterQuality.high; } - canvas.drawImage(image!, new Offset(x, y), paint); + canvas.drawImage(image!, Offset(x, y), paint); } @override diff --git a/flutter/lib/desktop/widgets/popup_menu.dart b/flutter/lib/desktop/widgets/popup_menu.dart index 6dbe4f8cd..3512d640f 100644 --- a/flutter/lib/desktop/widgets/popup_menu.dart +++ b/flutter/lib/desktop/widgets/popup_menu.dart @@ -97,6 +97,9 @@ class MenuConfig { } abstract class MenuEntryBase { + bool dismissOnClicked; + + MenuEntryBase({this.dismissOnClicked = false}); List> build(BuildContext context, MenuConfig conf); } @@ -112,9 +115,19 @@ class MenuEntryDivider extends MenuEntryBase { } } -typedef RadioOptionsGetter = List> Function(); +class MenuEntryRadioOption { + String text; + String value; + bool dismissOnClicked; + + MenuEntryRadioOption( + {required this.text, required this.value, this.dismissOnClicked = false}); +} + +typedef RadioOptionsGetter = List Function(); typedef RadioCurOptionGetter = Future Function(); -typedef RadioOptionSetter = Future Function(String); +typedef RadioOptionSetter = Future Function( + String oldValue, String newValue); class MenuEntryRadioUtils {} @@ -129,24 +142,28 @@ class MenuEntryRadios extends MenuEntryBase { {required this.text, required this.optionsGetter, required this.curOptionGetter, - required this.optionSetter}) { + required this.optionSetter, + dismissOnClicked = false}) + : super(dismissOnClicked: dismissOnClicked) { () async { _curOption.value = await curOptionGetter(); }(); } - List> get options => optionsGetter(); + List get options => optionsGetter(); RxString get curOption => _curOption; setOption(String option) async { - await optionSetter(option); - final opt = await curOptionGetter(); - if (_curOption.value != opt) { - _curOption.value = opt; + await optionSetter(_curOption.value, option); + if (_curOption.value != option) { + final opt = await curOptionGetter(); + if (_curOption.value != opt) { + _curOption.value = opt; + } } } mod_menu.PopupMenuEntry _buildMenuItem( - BuildContext context, MenuConfig conf, Tuple2 opt) { + BuildContext context, MenuConfig conf, MenuEntryRadioOption opt) { return mod_menu.PopupMenuItem( padding: EdgeInsets.zero, height: conf.height, @@ -157,7 +174,7 @@ class MenuEntryRadios extends MenuEntryBase { child: Row( children: [ Text( - opt.item1, + opt.text, style: const TextStyle( color: Colors.black, fontSize: MenuConfig.fontSize, @@ -169,7 +186,7 @@ class MenuEntryRadios extends MenuEntryBase { child: SizedBox( width: 20.0, height: 20.0, - child: Obx(() => opt.item2 == curOption.value + child: Obx(() => opt.value == curOption.value ? Icon( Icons.check, color: conf.commonColor, @@ -180,9 +197,10 @@ class MenuEntryRadios extends MenuEntryBase { ), ), onPressed: () { - if (opt.item2 != curOption.value) { - setOption(opt.item2); + if (opt.dismissOnClicked && Navigator.canPop(context)) { + Navigator.pop(context); } + setOption(opt.value); }, ), ); @@ -206,24 +224,28 @@ class MenuEntrySubRadios extends MenuEntryBase { {required this.text, required this.optionsGetter, required this.curOptionGetter, - required this.optionSetter}) { + required this.optionSetter, + dismissOnClicked = false}) + : super(dismissOnClicked: dismissOnClicked) { () async { _curOption.value = await curOptionGetter(); }(); } - List> get options => optionsGetter(); + List get options => optionsGetter(); RxString get curOption => _curOption; setOption(String option) async { - await optionSetter(option); - final opt = await curOptionGetter(); - if (_curOption.value != opt) { - _curOption.value = opt; + await optionSetter(_curOption.value, option); + if (_curOption.value != option) { + final opt = await curOptionGetter(); + if (_curOption.value != opt) { + _curOption.value = opt; + } } } mod_menu.PopupMenuEntry _buildSecondMenu( - BuildContext context, MenuConfig conf, Tuple2 opt) { + BuildContext context, MenuConfig conf, MenuEntryRadioOption opt) { return mod_menu.PopupMenuItem( padding: EdgeInsets.zero, height: conf.height, @@ -234,7 +256,7 @@ class MenuEntrySubRadios extends MenuEntryBase { child: Row( children: [ Text( - opt.item1, + opt.text, style: const TextStyle( color: Colors.black, fontSize: MenuConfig.fontSize, @@ -246,7 +268,7 @@ class MenuEntrySubRadios extends MenuEntryBase { child: SizedBox( width: 20.0, height: 20.0, - child: Obx(() => opt.item2 == curOption.value + child: Obx(() => opt.value == curOption.value ? Icon( Icons.check, color: conf.commonColor, @@ -257,9 +279,10 @@ class MenuEntrySubRadios extends MenuEntryBase { ), ), onPressed: () { - if (opt.item2 != curOption.value) { - setOption(opt.item2); + if (opt.dismissOnClicked && Navigator.canPop(context)) { + Navigator.pop(context); } + setOption(opt.value); }, ), ); @@ -303,7 +326,8 @@ typedef SwitchSetter = Future Function(bool); abstract class MenuEntrySwitchBase extends MenuEntryBase { final String text; - MenuEntrySwitchBase({required this.text}); + MenuEntrySwitchBase({required this.text, required dismissOnClicked}) + : super(dismissOnClicked: dismissOnClicked); RxBool get curOption; Future setOption(bool option); @@ -333,11 +357,20 @@ abstract class MenuEntrySwitchBase extends MenuEntryBase { alignment: Alignment.centerRight, child: Obx(() => Switch( value: curOption.value, - onChanged: (v) => setOption(v), + onChanged: (v) { + if (super.dismissOnClicked && + Navigator.canPop(context)) { + Navigator.pop(context); + } + setOption(v); + }, )), )) ])), onPressed: () { + if (super.dismissOnClicked && Navigator.canPop(context)) { + Navigator.pop(context); + } setOption(!curOption.value); }, ), @@ -352,8 +385,11 @@ class MenuEntrySwitch extends MenuEntrySwitchBase { final RxBool _curOption = false.obs; MenuEntrySwitch( - {required String text, required this.getter, required this.setter}) - : super(text: text) { + {required String text, + required this.getter, + required this.setter, + dismissOnClicked = false}) + : super(text: text, dismissOnClicked: dismissOnClicked) { () async { _curOption.value = await getter(); }(); @@ -379,8 +415,11 @@ class MenuEntrySwitch2 extends MenuEntrySwitchBase { final SwitchSetter setter; MenuEntrySwitch2( - {required String text, required this.getter, required this.setter}) - : super(text: text); + {required String text, + required this.getter, + required this.setter, + dismissOnClicked = false}) + : super(text: text, dismissOnClicked: dismissOnClicked); @override RxBool get curOption => getter(); @@ -394,10 +433,7 @@ class MenuEntrySubMenu extends MenuEntryBase { final String text; final List> entries; - MenuEntrySubMenu({ - required this.text, - required this.entries, - }); + MenuEntrySubMenu({required this.text, required this.entries}); @override List> build( @@ -438,10 +474,11 @@ class MenuEntryButton extends MenuEntryBase { final Widget Function(TextStyle? style) childBuilder; Function() proc; - MenuEntryButton({ - required this.childBuilder, - required this.proc, - }); + MenuEntryButton( + {required this.childBuilder, + required this.proc, + dismissOnClicked = false}) + : super(dismissOnClicked: dismissOnClicked); @override List> build( @@ -461,6 +498,9 @@ class MenuEntryButton extends MenuEntryBase { fontWeight: FontWeight.normal), )), onPressed: () { + if (super.dismissOnClicked && Navigator.canPop(context)) { + Navigator.pop(context); + } proc(); }, ), diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 2e8c7fb63..26789ac4f 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -290,9 +290,9 @@ class _RemoteMenubarState extends State { style: style, ), proc: () { - Navigator.pop(context); bind.sessionRefresh(id: widget.id); }, + dismissOnClicked: true, )); } displayMenu.add(MenuEntryButton( @@ -301,9 +301,9 @@ class _RemoteMenubarState extends State { style: style, ), proc: () { - Navigator.pop(context); showSetOSPassword(widget.id, false, widget.ffi.dialogManager); }, + dismissOnClicked: true, )); if (!isWebDesktop) { @@ -314,7 +314,6 @@ class _RemoteMenubarState extends State { style: style, ), proc: () { - Navigator.pop(context); () async { ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain); @@ -323,6 +322,7 @@ class _RemoteMenubarState extends State { } }(); }, + dismissOnClicked: true, )); } @@ -332,9 +332,9 @@ class _RemoteMenubarState extends State { style: style, ), proc: () { - Navigator.pop(context); widget.ffi.cursorModel.reset(); }, + dismissOnClicked: true, )); } @@ -346,9 +346,9 @@ class _RemoteMenubarState extends State { style: style, ), proc: () { - Navigator.pop(context); bind.sessionCtrlAltDel(id: widget.id); }, + dismissOnClicked: true, )); } @@ -358,9 +358,9 @@ class _RemoteMenubarState extends State { style: style, ), proc: () { - Navigator.pop(context); bind.sessionLockScreen(id: widget.id); }, + dismissOnClicked: true, )); if (pi.platform == 'Windows') { @@ -371,13 +371,13 @@ class _RemoteMenubarState extends State { style: style, )), proc: () { - Navigator.pop(context); RxBool blockInput = BlockInputState.find(widget.id); bind.sessionToggleOption( id: widget.id, value: '${blockInput.value ? "un" : ""}block-input'); blockInput.value = !blockInput.value; }, + dismissOnClicked: true, )); } } @@ -392,9 +392,9 @@ class _RemoteMenubarState extends State { style: style, ), proc: () { - Navigator.pop(context); showRestartRemoteDevice(pi, widget.id, gFFI.dialogManager); }, + dismissOnClicked: true, )); } @@ -406,44 +406,54 @@ class _RemoteMenubarState extends State { MenuEntryRadios( text: translate('Ratio'), optionsGetter: () => [ - Tuple2(translate('Scale original'), 'original'), - Tuple2(translate('Scale adaptive'), 'adaptive'), + MenuEntryRadioOption( + text: translate('Scale original'), value: 'original'), + MenuEntryRadioOption( + text: translate('Scale adaptive'), value: 'adaptive'), ], curOptionGetter: () async { return await bind.sessionGetOption( id: widget.id, arg: 'view-style') ?? 'adaptive'; }, - optionSetter: (String v) async { + optionSetter: (String oldValue, String newValue) async { await bind.sessionPeerOption( - id: widget.id, name: "view-style", value: v); + id: widget.id, name: "view-style", value: newValue); widget.ffi.canvasModel.updateViewStyle(); }), MenuEntryDivider(), MenuEntryRadios( text: translate('Scroll Style'), optionsGetter: () => [ - Tuple2(translate('ScrollAuto'), 'scrollauto'), - Tuple2(translate('Scrollbar'), 'scrollbar'), + MenuEntryRadioOption( + text: translate('ScrollAuto'), value: 'scrollauto'), + MenuEntryRadioOption( + text: translate('Scrollbar'), value: 'scrollbar'), ], curOptionGetter: () async { return await bind.sessionGetOption( id: widget.id, arg: 'scroll-style') ?? ''; }, - optionSetter: (String v) async { + optionSetter: (String oldValue, String newValue) async { await bind.sessionPeerOption( - id: widget.id, name: "scroll-style", value: v); + id: widget.id, name: "scroll-style", value: newValue); widget.ffi.canvasModel.updateScrollStyle(); }), MenuEntryDivider(), MenuEntryRadios( text: translate('Image Quality'), optionsGetter: () => [ - Tuple2(translate('Good image quality'), 'best'), - Tuple2(translate('Balanced'), 'balanced'), - Tuple2( - translate('Optimize reaction time'), 'low'), + MenuEntryRadioOption( + text: translate('Good image quality'), value: 'best'), + MenuEntryRadioOption( + text: translate('Balanced'), value: 'balanced'), + MenuEntryRadioOption( + text: translate('Optimize reaction time'), value: 'low'), + MenuEntryRadioOption( + text: translate('Custom'), + value: 'custom', + dismissOnClicked: true), ], curOptionGetter: () async { String quality = @@ -451,8 +461,43 @@ class _RemoteMenubarState extends State { if (quality == '') quality = 'balanced'; return quality; }, - optionSetter: (String v) async { - await bind.sessionSetImageQuality(id: widget.id, value: v); + optionSetter: (String oldValue, String newValue) async { + if (oldValue != newValue) { + await bind.sessionSetImageQuality(id: widget.id, value: newValue); + } + + if (newValue == 'custom') { + final btnCancel = getMsgBoxButton(translate('Cancel'), () { + widget.ffi.dialogManager.dismissAll(); + }); + final quality = + await bind.sessionGetCustomImageQuality(id: widget.id); + final double initValue = quality != null && quality.isNotEmpty + ? quality[0].toDouble() + : 50.0; + // final slider = _ImageCustomQualitySlider( + // id: widget.id, v: RxDouble(initValue)); + final RxDouble sliderValue = RxDouble(initValue); + final slider = Obx(() => Slider( + value: sliderValue.value, + max: 100, + label: sliderValue.value.round().toString(), + onChanged: (double value) { + () async { + await bind.sessionSetCustomImageQuality( + id: widget.id, value: value.toInt()); + final quality = await bind.sessionGetCustomImageQuality( + id: widget.id); + sliderValue.value = + quality != null && quality.isNotEmpty + ? quality[0].toDouble() + : 50.0; + }(); + }, + )); + msgBoxCommon(widget.ffi.dialogManager, 'Custom Image Quality', + slider, [btnCancel]); + } }), MenuEntryDivider(), MenuEntrySwitch( diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index da06b7fb9..ba9981db6 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -7,6 +7,7 @@ import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/generated_bridge.dart'; import 'package:flutter_hbb/models/ab_model.dart'; import 'package:flutter_hbb/models/chat_model.dart'; @@ -499,8 +500,8 @@ class CanvasModel with ChangeNotifier { _scale = 1.0; if (style == 'adaptive') { - final s1 = size.width / (parent.target?.ffiModel.display.width ?? 720); - final s2 = size.height / (parent.target?.ffiModel.display.height ?? 1280); + final s1 = size.width / getDisplayWidth(); + final s2 = size.height / getDisplayHeight(); _scale = s1 < s2 ? s1 : s2; } @@ -529,11 +530,17 @@ class CanvasModel with ChangeNotifier { } int getDisplayWidth() { - return parent.target?.ffiModel.display.width ?? 1080; + final defaultWidth = (isDesktop || isWebDesktop) + ? kDesktopDefaultDisplayWidth + : kMobileDefaultDisplayWidth; + return parent.target?.ffiModel.display.width ?? defaultWidth; } int getDisplayHeight() { - return parent.target?.ffiModel.display.height ?? 720; + final defaultHeight = (isDesktop || isWebDesktop) + ? kDesktopDefaultDisplayHeight + : kMobileDefaultDisplayHeight; + return parent.target?.ffiModel.display.height ?? defaultHeight; } Size get size { diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 167124212..716afacb9 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -143,14 +143,6 @@ pub fn session_get_toggle_option_sync(id: String, arg: String) -> SyncReturn Option { - if let Some(session) = SESSIONS.read().unwrap().get(&id) { - Some(session.get_image_quality()) - } else { - None - } -} - pub fn session_get_option(id: String, arg: String) -> Option { if let Some(session) = SESSIONS.read().unwrap().get(&id) { Some(session.get_option(arg)) @@ -190,12 +182,34 @@ pub fn session_toggle_option(id: String, value: String) { } } +pub fn session_get_image_quality(id: String) -> Option { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + Some(session.get_image_quality()) + } else { + None + } +} + pub fn session_set_image_quality(id: String, value: String) { if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) { session.save_image_quality(value); } } +pub fn session_get_custom_image_quality(id: String) -> Option> { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + Some(session.get_custom_image_quality()) + } else { + None + } +} + +pub fn session_set_custom_image_quality(id: String, value: i32) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.set_custom_image_quality(value); + } +} + pub fn session_lock_screen(id: String) { if let Some(session) = SESSIONS.read().unwrap().get(&id) { session.lock_screen(); From 7cb079afc8dd30caeb538b0334de74e5e873e920 Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 31 Aug 2022 23:02:16 +0800 Subject: [PATCH 0344/2015] flutter_desktop: add debug print Signed-off-by: fufesou --- flutter/lib/models/model.dart | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index ba9981db6..962998bbd 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -556,9 +556,19 @@ class CanvasModel with ChangeNotifier { var dxOffset = 0; var dyOffset = 0; if (dw > size.width) { + final xxxx = x - dw * (x / size.width) - _x; + if (xxxx.isInfinite || xxxx.isNaN) { + debugPrint( + 'REMOVE ME ============================ xxxx $x,$dw,$_scale,${size.width},$_x'); + } dxOffset = (x - dw * (x / size.width) - _x).toInt(); } if (dh > size.height) { + final yyyy = y - dh * (y / size.height) - _y; + if (yyyy.isInfinite || yyyy.isNaN) { + debugPrint( + 'REMOVE ME ============================ xxxx $y,$dh,$_scale,${size.height},$_y'); + } dyOffset = (y - dh * (y / size.height) - _y).toInt(); } _x += dxOffset; @@ -926,16 +936,16 @@ class FFI { late final QualityMonitorModel qualityMonitorModel; // session FFI() { - this.imageModel = ImageModel(WeakReference(this)); - this.ffiModel = FfiModel(WeakReference(this)); - this.cursorModel = CursorModel(WeakReference(this)); - this.canvasModel = CanvasModel(WeakReference(this)); - this.serverModel = ServerModel(WeakReference(this)); // use global FFI - this.chatModel = ChatModel(WeakReference(this)); - this.fileModel = FileModel(WeakReference(this)); - this.abModel = AbModel(WeakReference(this)); - this.userModel = UserModel(WeakReference(this)); - this.qualityMonitorModel = QualityMonitorModel(WeakReference(this)); + imageModel = ImageModel(WeakReference(this)); + ffiModel = FfiModel(WeakReference(this)); + cursorModel = CursorModel(WeakReference(this)); + canvasModel = CanvasModel(WeakReference(this)); + serverModel = ServerModel(WeakReference(this)); // use global FFI + chatModel = ChatModel(WeakReference(this)); + fileModel = FileModel(WeakReference(this)); + abModel = AbModel(WeakReference(this)); + userModel = UserModel(WeakReference(this)); + qualityMonitorModel = QualityMonitorModel(WeakReference(this)); } /// Send a mouse tap event(down and up). From ce1a504e9fdc564a1d42ec8034f01f9baf05f5a3 Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 31 Aug 2022 23:02:02 -0700 Subject: [PATCH 0345/2015] flutter_desktop: custom image quality Signed-off-by: fufesou --- flutter/.gitignore | 2 +- flutter/lib/common.dart | 14 +++--- .../lib/desktop/widgets/remote_menubar.dart | 45 ++++++++++--------- flutter/pubspec.yaml | 1 + 4 files changed, 34 insertions(+), 28 deletions(-) diff --git a/flutter/.gitignore b/flutter/.gitignore index e5db34d22..ec3fef74e 100644 --- a/flutter/.gitignore +++ b/flutter/.gitignore @@ -48,7 +48,7 @@ lib/generated_bridge.dart lib/generated_bridge.freezed.dart # Flutter Generated Files -**/flutter/GeneratedPluginRegistrant.swift +**/GeneratedPluginRegistrant.swift **/flutter/generated_plugin_registrant.cc **/flutter/generated_plugin_registrant.h **/flutter/generated_plugins.cmake diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index e8632caaa..75328c840 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -432,7 +432,7 @@ void msgBox( if (type != "connecting" && type != "success" && !type.contains("nook")) { buttons.insert( 0, - getMsgBoxButton(translate('OK'), () { + msgBoxButton(translate('OK'), () { dialogManager.dismissAll(); // https://github.com/fufesou/rustdesk/blob/5e9a31340b899822090a3731769ae79c6bf5f3e5/src/ui/common.tis#L263 if (!type.contains("custom")) { @@ -446,7 +446,7 @@ void msgBox( if (hasCancel) { buttons.insert( 0, - getMsgBoxButton(translate('Cancel'), () { + msgBoxButton(translate('Cancel'), () { dialogManager.dismissAll(); })); } @@ -454,17 +454,17 @@ void msgBox( if (type.contains("hasclose")) { buttons.insert( 0, - getMsgBoxButton(translate('Close'), () { + msgBoxButton(translate('Close'), () { dialogManager.dismissAll(); })); } dialogManager.show((setState, close) => CustomAlertDialog( - title: Text(translate(title), style: TextStyle(fontSize: 21)), + title: _msgBoxTitle(title), content: Text(translate(text), style: TextStyle(fontSize: 15)), actions: buttons)); } -Widget getMsgBoxButton(String text, void Function() onPressed) { +Widget msgBoxButton(String text, void Function() onPressed) { return ButtonTheme( padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10), materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, @@ -479,11 +479,13 @@ Widget getMsgBoxButton(String text, void Function() onPressed) { Text(translate(text), style: TextStyle(color: MyTheme.accent)))); } +Widget _msgBoxTitle(String title) => Text(translate(title), style: TextStyle(fontSize: 21)); + void msgBoxCommon(OverlayDialogManager dialogManager, String title, Widget content, List buttons) { dialogManager.dismissAll(); dialogManager.show((setState, close) => CustomAlertDialog( - title: Text(translate(title), style: TextStyle(fontSize: 21)), + title: _msgBoxTitle(title), content: content, actions: buttons)); } diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 26789ac4f..66edb7a96 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hbb/models/chat_model.dart'; import 'package:get/get.dart'; -import 'package:tuple/tuple.dart'; +import 'package:rxdart/rxdart.dart' as rxdart; import '../../common.dart'; import '../../mobile/widgets/dialog.dart'; @@ -467,7 +467,7 @@ class _RemoteMenubarState extends State { } if (newValue == 'custom') { - final btnCancel = getMsgBoxButton(translate('Cancel'), () { + final btnCancel = msgBoxButton(translate('Close'), () { widget.ffi.dialogManager.dismissAll(); }); final quality = @@ -475,26 +475,29 @@ class _RemoteMenubarState extends State { final double initValue = quality != null && quality.isNotEmpty ? quality[0].toDouble() : 50.0; - // final slider = _ImageCustomQualitySlider( - // id: widget.id, v: RxDouble(initValue)); final RxDouble sliderValue = RxDouble(initValue); - final slider = Obx(() => Slider( - value: sliderValue.value, - max: 100, - label: sliderValue.value.round().toString(), - onChanged: (double value) { - () async { - await bind.sessionSetCustomImageQuality( - id: widget.id, value: value.toInt()); - final quality = await bind.sessionGetCustomImageQuality( - id: widget.id); - sliderValue.value = - quality != null && quality.isNotEmpty - ? quality[0].toDouble() - : 50.0; - }(); - }, - )); + final rxReplay = rxdart.ReplaySubject(); + rxReplay + .throttleTime(const Duration(milliseconds: 1000), + trailing: true, leading: false) + .listen((double v) { + () async { + await bind.sessionSetCustomImageQuality( + id: widget.id, value: v.toInt()); + }(); + }); + final slider = Obx(() { + return Slider( + value: sliderValue.value, + max: 100, + divisions: 100, + label: sliderValue.value.round().toString(), + onChanged: (double value) { + sliderValue.value = value; + rxReplay.add(value); + }, + ); + }); msgBoxCommon(widget.ffi.dialogManager, 'Custom Image Quality', slider, [btnCancel]); } diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index a35f1c872..b6ce5d20b 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -73,6 +73,7 @@ dependencies: contextmenu: ^3.0.0 desktop_drop: ^0.3.3 scroll_pos: ^0.3.0 + rxdart: ^0.27.5 dev_dependencies: flutter_launcher_icons: ^0.9.1 From ec02f9e7213779548a24743794345bc7cb034ccd Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 1 Sep 2022 03:56:12 -0700 Subject: [PATCH 0346/2015] flutter_desktop: refactor peercard menu Signed-off-by: fufesou --- .../lib/desktop/widgets/peercard_widget.dart | 865 ++++++++++-------- flutter/lib/desktop/widgets/popup_menu.dart | 1 - .../lib/desktop/widgets/remote_menubar.dart | 2 +- 3 files changed, 509 insertions(+), 359 deletions(-) diff --git a/flutter/lib/desktop/widgets/peercard_widget.dart b/flutter/lib/desktop/widgets/peercard_widget.dart index 4db43398a..3b4a30ee0 100644 --- a/flutter/lib/desktop/widgets/peercard_widget.dart +++ b/flutter/lib/desktop/widgets/peercard_widget.dart @@ -8,10 +8,18 @@ import '../../common.dart'; import '../../models/model.dart'; import '../../models/peer_model.dart'; import '../../models/platform_model.dart'; +import './material_mod_popup_menu.dart' as mod_menu; +import './popup_menu.dart'; -typedef PopupMenuItemsFunc = Future>> Function(); +class _PopupMenuTheme { + static const Color commonColor = MyTheme.accent; + // kMinInteractiveDimension + static const double height = 25.0; + static const double dividerHeight = 12.0; +} -enum PeerType { recent, fav, discovered, ab } +typedef PopupMenuEntryBuilder = Future>> + Function(BuildContext); enum PeerUiType { grid, list } @@ -19,14 +27,16 @@ final peerCardUiType = PeerUiType.grid.obs; class _PeerCard extends StatefulWidget { final Peer peer; - final PopupMenuItemsFunc popupMenuItemsFunc; - final PeerType type; + final RxString alias; + final Function(BuildContext, String) connect; + final PopupMenuEntryBuilder popupMenuEntryBuilder; _PeerCard( {required this.peer, - required this.popupMenuItemsFunc, - Key? key, - required this.type}) + required this.alias, + required this.connect, + required this.popupMenuEntryBuilder, + Key? key}) : super(key: key); @override @@ -36,7 +46,6 @@ class _PeerCard extends StatefulWidget { /// State for the connection page. class _PeerCardState extends State<_PeerCard> with AutomaticKeepAliveClientMixin { - var _menuPos = RelativeRect.fill; final double _cardRadis = 20; final double _borderWidth = 2; final RxBool _iconMoreHover = false.obs; @@ -66,7 +75,7 @@ class _PeerCardState extends State<_PeerCard> : null); }, child: GestureDetector( - onDoubleTap: () => _connect(peer.id), + onDoubleTap: () => widget.connect(context, peer.id), child: Obx(() => peerCardUiType.value == PeerUiType.grid ? _buildPeerCard(context, peer, deco) : _buildPeerTile(context, peer, deco))), @@ -185,46 +194,28 @@ class _PeerCardState extends State<_PeerCard> children: [ Container( padding: const EdgeInsets.all(6), - child: - _getPlatformImage('${peer.platform}', 60), + child: _getPlatformImage(peer.platform, 60), ), Row( children: [ Expanded( - child: FutureBuilder( - future: bind.mainGetPeerOption( - id: peer.id, key: 'alias'), - builder: (_, snapshot) { - if (snapshot.hasData) { - final name = snapshot.data!.isEmpty - ? '${peer.username}@${peer.hostname}' - : snapshot.data!; - return Tooltip( - message: name, - waitDuration: Duration(seconds: 1), - child: Text( - name, - style: TextStyle( - color: Colors.white70, - fontSize: 12), - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - ), - ); - } else { - // alias has not arrived - return Center( - child: Text( - '${peer.username}@${peer.hostname}', - style: TextStyle( - color: Colors.white70, - fontSize: 12), - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - )); - } - }, - ), + child: Obx(() { + final name = widget.alias.value.isEmpty + ? '${peer.username}@${peer.hostname}' + : widget.alias.value; + return Tooltip( + message: name, + waitDuration: Duration(seconds: 1), + child: Text( + name, + style: TextStyle( + color: Colors.white70, + fontSize: 12), + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + ), + ); + }), ), ], ), @@ -248,7 +239,7 @@ class _PeerCardState extends State<_PeerCard> backgroundColor: peer.online ? Colors.green : Colors.yellow)), - Text('${peer.id}') + Text(peer.id) ]).paddingSymmetric(vertical: 8), _actionMore(peer), ], @@ -262,32 +253,93 @@ class _PeerCardState extends State<_PeerCard> ); } - Widget _actionMore(Peer peer) => Listener( - onPointerDown: (e) { - final x = e.position.dx; - final y = e.position.dy; - _menuPos = RelativeRect.fromLTRB(x, y, x, y); - }, - onPointerUp: (_) => _showPeerMenu(context, peer.id), - child: MouseRegion( - onEnter: (_) => _iconMoreHover.value = true, - onExit: (_) => _iconMoreHover.value = false, - child: CircleAvatar( - radius: 14, - backgroundColor: _iconMoreHover.value - ? MyTheme.color(context).grayBg! - : MyTheme.color(context).bg!, - child: Icon(Icons.more_vert, - size: 18, - color: _iconMoreHover.value - ? MyTheme.color(context).text - : MyTheme.color(context).lightText)))); + Widget _actionMore(Peer peer) { + return FutureBuilder( + future: widget.popupMenuEntryBuilder(context), + initialData: const >[], + builder: (BuildContext context, + AsyncSnapshot>> snapshot) { + if (snapshot.hasData) { + return Listener( + child: MouseRegion( + onEnter: (_) => _iconMoreHover.value = true, + onExit: (_) => _iconMoreHover.value = false, + child: CircleAvatar( + radius: 14, + backgroundColor: _iconMoreHover.value + ? MyTheme.color(context).grayBg! + : MyTheme.color(context).bg!, + child: mod_menu.PopupMenuButton( + padding: EdgeInsets.zero, + icon: Icon(Icons.more_vert, + size: 18, + color: _iconMoreHover.value + ? MyTheme.color(context).text + : MyTheme.color(context).lightText), + position: mod_menu.PopupMenuPosition.under, + itemBuilder: (BuildContext context) => snapshot.data!, + )))); + } else { + return Container(); + } + }); + } + + /// Get the image for the current [platform]. + Widget _getPlatformImage(String platform, double size) { + platform = platform.toLowerCase(); + if (platform == 'mac os') { + platform = 'mac'; + } else if (platform != 'linux' && platform != 'android') { + platform = 'win'; + } + return Image.asset('assets/$platform.png', height: size, width: size); + } + + @override + bool get wantKeepAlive => true; +} + +abstract class BasePeerCard extends StatelessWidget { + final RxString alias = ''.obs; + final Peer peer; + + BasePeerCard({required this.peer, Key? key}) : super(key: key) { + bind + .mainGetPeerOption(id: peer.id, key: 'alias') + .then((value) => alias.value = value); + } + + @override + Widget build(BuildContext context) { + return _PeerCard( + peer: peer, + alias: alias, + connect: (BuildContext context, String id) => _connect(context, id), + popupMenuEntryBuilder: _buildPopupMenuEntry, + ); + } + + Future>> _buildPopupMenuEntry( + BuildContext context) async => + (await _buildMenuItems(context)) + .map((e) => e.build( + context, + const MenuConfig( + commonColor: _PopupMenuTheme.commonColor, + height: _PopupMenuTheme.height, + dividerHeight: _PopupMenuTheme.dividerHeight))) + .expand((i) => i) + .toList(); + + @protected + Future>> _buildMenuItems(BuildContext context); /// Connect to a peer with [id]. /// If [isFileTransfer], starts a session only for file transfer. /// If [isTcpTunneling], starts a session only for tcp tunneling. /// If [isRDP], starts a session only for rdp. - void _connect(String id, + void _connect(BuildContext context, String id, {bool isFileTransfer = false, bool isTcpTunneling = false, bool isRDP = false}) async { @@ -308,105 +360,369 @@ class _PeerCardState extends State<_PeerCard> } } - /// Show the peer menu and handle user's choice. - /// User might remove the peer or send a file to the peer. - void _showPeerMenu(BuildContext context, String id) async { - var value = await showMenu( - context: context, - position: _menuPos, - items: await super.widget.popupMenuItemsFunc(), - elevation: 8, - ); - if (value == 'connect') { - _connect(id); - } else if (value == 'file') { - _connect(id, isFileTransfer: true); - } else if (value == 'tcp-tunnel') { - _connect(id, isTcpTunneling: true); - } else if (value == 'RDP') { - _connect(id, isRDP: true); - } else if (value == 'remove') { - await bind.mainRemovePeer(id: id); - removePreference(id); - Get.forceAppUpdate(); // TODO use inner model / state - } else if (value == 'add-fav') { - final favs = (await bind.mainGetFav()).toList(); - if (favs.indexOf(id) < 0) { - favs.add(id); - bind.mainStoreFav(favs: favs); - } - } else if (value == 'remove-fav') { - final favs = (await bind.mainGetFav()).toList(); - if (favs.remove(id)) { - bind.mainStoreFav(favs: favs); - Get.forceAppUpdate(); // TODO use inner model / state - } - } else if (value == 'ab-delete') { - gFFI.abModel.deletePeer(id); - await gFFI.abModel.updateAb(); - setState(() {}); - } else if (value == 'ab-edit-tag') { - _abEditTag(id); - } else if (value == 'rename') { - _rename(id); - } else if (value == 'unremember-password') { - await bind.mainForgetPassword(id: id); - } else if (value == 'force-always-relay') { - String value; - String oldValue = - await bind.mainGetPeerOption(id: id, key: 'force-always-relay'); - if (oldValue.isEmpty) { - value = 'Y'; - } else { - value = ''; - } - await bind.mainSetPeerOption( - id: id, key: 'force-always-relay', value: value); - } - } - - Widget _buildTag(String tagName, RxList rxTags, - {Function()? onTap}) { - return ContextMenuArea( - width: 100, - builder: (context) => [ - ListTile( - title: Text(translate("Delete")), - onTap: () { - gFFI.abModel.deleteTag(tagName); - gFFI.abModel.updateAb(); - Future.delayed(Duration.zero, () => Get.back()); - }, - ) - ], - child: GestureDetector( - onTap: onTap, - child: Obx( - () => Container( - decoration: BoxDecoration( - color: rxTags.contains(tagName) ? Colors.blue : null, - border: Border.all(color: MyTheme.darkGray), - borderRadius: BorderRadius.circular(10)), - margin: EdgeInsets.symmetric(horizontal: 4.0, vertical: 8.0), - padding: EdgeInsets.symmetric(vertical: 2.0, horizontal: 8.0), - child: Text( - tagName, - style: TextStyle( - color: rxTags.contains(tagName) ? MyTheme.white : null), - ), - ), - ), + MenuEntryBase _connectCommonAction( + BuildContext context, String id, String title, + {bool isFileTransfer = false, + bool isTcpTunneling = false, + bool isRDP = false}) { + return MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate(title), + style: style, ), + proc: () { + _connect( + context, + peer.id, + isFileTransfer: isFileTransfer, + isTcpTunneling: isTcpTunneling, + isRDP: isRDP, + ); + }, + dismissOnClicked: true, ); } - /// Get the image for the current [platform]. - Widget _getPlatformImage(String platform, double size) { - platform = platform.toLowerCase(); - if (platform == 'mac os') - platform = 'mac'; - else if (platform != 'linux' && platform != 'android') platform = 'win'; - return Image.asset('assets/$platform.png', height: size, width: size); + @protected + MenuEntryBase _connectAction(BuildContext context, String id) { + return _connectCommonAction(context, id, 'Connect'); + } + + @protected + MenuEntryBase _transferFileAction(BuildContext context, String id) { + return _connectCommonAction( + context, + id, + 'Transfer File', + isFileTransfer: true, + ); + } + + @protected + MenuEntryBase _tcpTunnelingAction(BuildContext context, String id) { + return _connectCommonAction( + context, + id, + 'TCP Tunneling', + isTcpTunneling: true, + ); + } + + @protected + MenuEntryBase _rdpAction(BuildContext context, String id) { + return MenuEntryButton( + childBuilder: (TextStyle? style) => Row( + children: [ + Text( + translate('RDP'), + style: style, + ), + SizedBox(width: 20), + IconButton( + icon: Icon(Icons.edit), + onPressed: () => _rdpDialog(id), + ) + ], + ), + proc: () { + _connect(context, id, isRDP: true); + }, + dismissOnClicked: true, + ); + } + + @protected + Future> _forceAlwaysRelayAction(String id) async { + const option = 'force-always-relay'; + return MenuEntrySwitch( + text: translate('Always connect via relay'), + getter: () async { + return (await bind.mainGetPeerOption(id: id, key: option)).isNotEmpty; + }, + setter: (bool v) async { + String value; + String oldValue = await bind.mainGetPeerOption(id: id, key: option); + if (oldValue.isEmpty) { + value = 'Y'; + } else { + value = ''; + } + await bind.mainSetPeerOption(id: id, key: option, value: value); + }, + dismissOnClicked: true, + ); + } + + @protected + MenuEntryBase _renameAction(String id, bool isAddressBook) { + return MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Rename'), + style: style, + ), + proc: () { + _rename(id, isAddressBook); + }, + dismissOnClicked: true, + ); + } + + @protected + MenuEntryBase _removeAction(String id) { + return MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Remove'), + style: style, + ), + proc: () { + () async { + await bind.mainRemovePeer(id: id); + removePreference(id); + Get.forceAppUpdate(); // TODO use inner model / state + }(); + }, + dismissOnClicked: true, + ); + } + + @protected + MenuEntryBase _unrememberPasswordAction(String id) { + return MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Unremember Password'), + style: style, + ), + proc: () { + bind.mainForgetPassword(id: id); + }, + dismissOnClicked: true, + ); + } + + @protected + MenuEntryBase _addFavAction(String id) { + return MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Add to Favorites'), + style: style, + ), + proc: () { + () async { + final favs = (await bind.mainGetFav()).toList(); + if (!favs.contains(id)) { + favs.add(id); + bind.mainStoreFav(favs: favs); + } + }(); + }, + dismissOnClicked: true, + ); + } + + @protected + MenuEntryBase _rmFavAction(String id) { + return MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Remove from Favorites'), + style: style, + ), + proc: () { + () async { + final favs = (await bind.mainGetFav()).toList(); + if (favs.remove(id)) { + bind.mainStoreFav(favs: favs); + Get.forceAppUpdate(); // TODO use inner model / state + } + }(); + }, + dismissOnClicked: true, + ); + } + + void _rename(String id, bool isAddressBook) async { + RxBool isInProgress = false.obs; + var name = await bind.mainGetPeerOption(id: id, key: 'alias'); + var controller = TextEditingController(text: name); + if (isAddressBook) { + final peer = gFFI.abModel.peers.firstWhere((p) => id == p['id']); + if (peer == null) { + // this should not happen + } else { + name = peer['alias'] ?? ''; + } + } + gFFI.dialogManager.show((setState, close) { + return CustomAlertDialog( + title: Text(translate('Rename')), + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + child: Form( + child: TextFormField( + controller: controller, + decoration: InputDecoration(border: OutlineInputBorder()), + ), + ), + ), + Obx(() => Offstage( + offstage: isInProgress.isFalse, + child: LinearProgressIndicator())), + ], + ), + actions: [ + TextButton( + onPressed: () { + close(); + }, + child: Text(translate("Cancel"))), + TextButton( + onPressed: () async { + isInProgress.value = true; + name = controller.text; + await bind.mainSetPeerOption(id: id, key: 'alias', value: name); + if (isAddressBook) { + gFFI.abModel.setPeerOption(id, 'alias', name); + await gFFI.abModel.updateAb(); + } + alias.value = + await bind.mainGetPeerOption(id: peer.id, key: 'alias'); + close(); + isInProgress.value = false; + }, + child: Text(translate("OK"))), + ], + ); + }); + } +} + +class RecentPeerCard extends BasePeerCard { + RecentPeerCard({required Peer peer, Key? key}) : super(peer: peer, key: key); + + @override + Future>> _buildMenuItems( + BuildContext context) async { + final List> menuItems = [ + _connectAction(context, peer.id), + _transferFileAction(context, peer.id), + _tcpTunnelingAction(context, peer.id), + ]; + if (peer.platform == 'Windows') { + menuItems.add(_rdpAction(context, peer.id)); + } + menuItems.add(await _forceAlwaysRelayAction(peer.id)); + menuItems.add(_renameAction(peer.id, false)); + menuItems.add(_removeAction(peer.id)); + menuItems.add(_unrememberPasswordAction(peer.id)); + menuItems.add(_addFavAction(peer.id)); + return menuItems; + } +} + +class FavoritePeerCard extends BasePeerCard { + FavoritePeerCard({required Peer peer, Key? key}) + : super(peer: peer, key: key); + + @override + Future>> _buildMenuItems( + BuildContext context) async { + final List> menuItems = [ + _connectAction(context, peer.id), + _transferFileAction(context, peer.id), + _tcpTunnelingAction(context, peer.id), + ]; + if (peer.platform == 'Windows') { + menuItems.add(_rdpAction(context, peer.id)); + } + menuItems.add(await _forceAlwaysRelayAction(peer.id)); + menuItems.add(_renameAction(peer.id, false)); + menuItems.add(_removeAction(peer.id)); + menuItems.add(_unrememberPasswordAction(peer.id)); + menuItems.add(_rmFavAction(peer.id)); + return menuItems; + } +} + +class DiscoveredPeerCard extends BasePeerCard { + DiscoveredPeerCard({required Peer peer, Key? key}) + : super(peer: peer, key: key); + + @override + Future>> _buildMenuItems( + BuildContext context) async { + final List> menuItems = [ + _connectAction(context, peer.id), + _transferFileAction(context, peer.id), + _tcpTunnelingAction(context, peer.id), + ]; + if (peer.platform == 'Windows') { + menuItems.add(_rdpAction(context, peer.id)); + } + menuItems.add(await _forceAlwaysRelayAction(peer.id)); + menuItems.add(_renameAction(peer.id, false)); + menuItems.add(_removeAction(peer.id)); + menuItems.add(_unrememberPasswordAction(peer.id)); + menuItems.add(_addFavAction(peer.id)); + return menuItems; + } +} + +class AddressBookPeerCard extends BasePeerCard { + AddressBookPeerCard({required Peer peer, Key? key}) + : super(peer: peer, key: key); + + @override + Future>> _buildMenuItems( + BuildContext context) async { + final List> menuItems = [ + _connectAction(context, peer.id), + _transferFileAction(context, peer.id), + _tcpTunnelingAction(context, peer.id), + ]; + if (peer.platform == 'Windows') { + menuItems.add(_rdpAction(context, peer.id)); + } + menuItems.add(await _forceAlwaysRelayAction(peer.id)); + menuItems.add(_renameAction(peer.id, false)); + menuItems.add(_removeAction(peer.id)); + menuItems.add(_unrememberPasswordAction(peer.id)); + menuItems.add(_addFavAction(peer.id)); + menuItems.add(_editTagAction(peer.id)); + return menuItems; + } + + @protected + @override + MenuEntryBase _removeAction(String id) { + return MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Remove'), + style: style, + ), + proc: () { + () async { + gFFI.abModel.deletePeer(id); + await gFFI.abModel.updateAb(); + }(); + }, + dismissOnClicked: true, + ); + } + + @protected + MenuEntryBase _editTagAction(String id) { + return MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Edit Tag'), + style: style, + ), + proc: () { + _abEditTag(id); + }, + dismissOnClicked: true, + ); } void _abEditTag(String id) { @@ -459,205 +775,40 @@ class _PeerCardState extends State<_PeerCard> }); } - void _rename(String id) async { - var isInProgress = false; - var name = await bind.mainGetPeerOption(id: id, key: 'alias'); - var controller = TextEditingController(text: name); - if (widget.type == PeerType.ab) { - final peer = gFFI.abModel.peers.firstWhere((p) => id == p['id']); - if (peer == null) { - // this should not happen - } else { - name = peer['alias'] ?? ""; - } - } - gFFI.dialogManager.show((setState, close) { - return CustomAlertDialog( - title: Text(translate("Rename")), - content: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), - child: Form( - child: TextFormField( - controller: controller, - decoration: InputDecoration(border: OutlineInputBorder()), - ), - ), + Widget _buildTag(String tagName, RxList rxTags, + {Function()? onTap}) { + return ContextMenuArea( + width: 100, + builder: (context) => [ + ListTile( + title: Text(translate("Delete")), + onTap: () { + gFFI.abModel.deleteTag(tagName); + gFFI.abModel.updateAb(); + Future.delayed(Duration.zero, () => Get.back()); + }, + ) + ], + child: GestureDetector( + onTap: onTap, + child: Obx( + () => Container( + decoration: BoxDecoration( + color: rxTags.contains(tagName) ? Colors.blue : null, + border: Border.all(color: MyTheme.darkGray), + borderRadius: BorderRadius.circular(10)), + margin: EdgeInsets.symmetric(horizontal: 4.0, vertical: 8.0), + padding: EdgeInsets.symmetric(vertical: 2.0, horizontal: 8.0), + child: Text( + tagName, + style: TextStyle( + color: rxTags.contains(tagName) ? MyTheme.white : null), ), - Offstage(offstage: !isInProgress, child: LinearProgressIndicator()) - ], + ), ), - actions: [ - TextButton( - onPressed: () { - close(); - }, - child: Text(translate("Cancel"))), - TextButton( - onPressed: () async { - setState(() { - isInProgress = true; - }); - name = controller.text; - await bind.mainSetPeerOption(id: id, key: 'alias', value: name); - if (widget.type == PeerType.ab) { - gFFI.abModel.setPeerOption(id, 'alias', name); - await gFFI.abModel.updateAb(); - } else { - Future.delayed(Duration.zero, () { - this.setState(() {}); - }); - } - close(); - setState(() { - isInProgress = false; - }); - }, - child: Text(translate("OK"))), - ], - ); - }); - } - - @override - bool get wantKeepAlive => true; -} - -abstract class BasePeerCard extends StatelessWidget { - final Peer peer; - final PeerType type; - - BasePeerCard({required this.peer, required this.type, Key? key}) - : super(key: key); - - @override - Widget build(BuildContext context) { - return _PeerCard( - peer: peer, - popupMenuItemsFunc: _getPopupMenuItems, - type: type, + ), ); } - - @protected - Future>> _getPopupMenuItems(); -} - -class RecentPeerCard extends BasePeerCard { - RecentPeerCard({required Peer peer, Key? key}) - : super(peer: peer, key: key, type: PeerType.recent); - - Future>> _getPopupMenuItems() async { - var items = [ - PopupMenuItem( - child: Text(translate('Connect')), value: 'connect'), - PopupMenuItem( - child: Text(translate('Transfer File')), value: 'file'), - PopupMenuItem( - child: Text(translate('TCP Tunneling')), value: 'tcp-tunnel'), - await _forceAlwaysRelayMenuItem(peer.id), - PopupMenuItem(child: Text(translate('Rename')), value: 'rename'), - PopupMenuItem(child: Text(translate('Remove')), value: 'remove'), - PopupMenuItem( - child: Text(translate('Unremember Password')), - value: 'unremember-password'), - PopupMenuItem( - child: Text(translate('Add to Favorites')), value: 'add-fav'), - ]; - if (peer.platform == 'Windows') { - items.insert(3, _rdpMenuItem(peer.id)); - } - return items; - } -} - -class FavoritePeerCard extends BasePeerCard { - FavoritePeerCard({required Peer peer, Key? key}) - : super(peer: peer, key: key, type: PeerType.fav); - - Future>> _getPopupMenuItems() async { - var items = [ - PopupMenuItem( - child: Text(translate('Connect')), value: 'connect'), - PopupMenuItem( - child: Text(translate('Transfer File')), value: 'file'), - PopupMenuItem( - child: Text(translate('TCP Tunneling')), value: 'tcp-tunnel'), - await _forceAlwaysRelayMenuItem(peer.id), - PopupMenuItem(child: Text(translate('Rename')), value: 'rename'), - PopupMenuItem(child: Text(translate('Remove')), value: 'remove'), - PopupMenuItem( - child: Text(translate('Unremember Password')), - value: 'unremember-password'), - PopupMenuItem( - child: Text(translate('Remove from Favorites')), value: 'remove-fav'), - ]; - if (peer.platform == 'Windows') { - items.insert(3, _rdpMenuItem(peer.id)); - } - return items; - } -} - -class DiscoveredPeerCard extends BasePeerCard { - DiscoveredPeerCard({required Peer peer, Key? key}) - : super(peer: peer, key: key, type: PeerType.discovered); - - Future>> _getPopupMenuItems() async { - var items = [ - PopupMenuItem( - child: Text(translate('Connect')), value: 'connect'), - PopupMenuItem( - child: Text(translate('Transfer File')), value: 'file'), - PopupMenuItem( - child: Text(translate('TCP Tunneling')), value: 'tcp-tunnel'), - await _forceAlwaysRelayMenuItem(peer.id), - PopupMenuItem(child: Text(translate('Rename')), value: 'rename'), - PopupMenuItem(child: Text(translate('Remove')), value: 'remove'), - PopupMenuItem( - child: Text(translate('Unremember Password')), - value: 'unremember-password'), - PopupMenuItem( - child: Text(translate('Add to Favorites')), value: 'add-fav'), - ]; - if (peer.platform == 'Windows') { - items.insert(3, _rdpMenuItem(peer.id)); - } - return items; - } -} - -class AddressBookPeerCard extends BasePeerCard { - AddressBookPeerCard({required Peer peer, Key? key}) - : super(peer: peer, key: key, type: PeerType.ab); - - Future>> _getPopupMenuItems() async { - var items = [ - PopupMenuItem( - child: Text(translate('Connect')), value: 'connect'), - PopupMenuItem( - child: Text(translate('Transfer File')), value: 'file'), - PopupMenuItem( - child: Text(translate('TCP Tunneling')), value: 'tcp-tunnel'), - await _forceAlwaysRelayMenuItem(peer.id), - PopupMenuItem(child: Text(translate('Rename')), value: 'rename'), - PopupMenuItem( - child: Text(translate('Remove')), value: 'ab-delete'), - PopupMenuItem( - child: Text(translate('Unremember Password')), - value: 'unremember-password'), - PopupMenuItem( - child: Text(translate('Add to Favorites')), value: 'add-fav'), - PopupMenuItem( - child: Text(translate('Edit Tag')), value: 'ab-edit-tag'), - ]; - if (peer.platform == 'Windows') { - items.insert(3, _rdpMenuItem(peer.id)); - } - return items; - } } Future> _forceAlwaysRelayMenuItem(String id) async { diff --git a/flutter/lib/desktop/widgets/popup_menu.dart b/flutter/lib/desktop/widgets/popup_menu.dart index 3512d640f..45e52cf81 100644 --- a/flutter/lib/desktop/widgets/popup_menu.dart +++ b/flutter/lib/desktop/widgets/popup_menu.dart @@ -2,7 +2,6 @@ import 'dart:core'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:tuple/tuple.dart'; import './material_mod_popup_menu.dart' as mod_menu; diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 66edb7a96..47536011d 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -16,7 +16,7 @@ import './material_mod_popup_menu.dart' as mod_menu; class _MenubarTheme { static const Color commonColor = MyTheme.accent; // kMinInteractiveDimension - static const double height = 24.0; + static const double height = 25.0; static const double dividerHeight = 12.0; } From 9085a938884fa8e1497fa557a3665bf36a8b28a0 Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 1 Sep 2022 06:18:29 -0700 Subject: [PATCH 0347/2015] flutter_desktop: fix peer page bugs Signed-off-by: fufesou --- .../lib/desktop/pages/connection_page.dart | 10 +- flutter/lib/desktop/widgets/peer_widget.dart | 233 ++++++++++-------- .../lib/desktop/widgets/peercard_widget.dart | 30 ++- flutter/lib/models/model.dart | 23 +- flutter/lib/models/peer_model.dart | 58 ++--- flutter/lib/utils/multi_window_manager.dart | 18 +- src/client.rs | 4 +- src/flutter_ffi.rs | 4 +- src/ui_session_interface.rs | 11 +- 9 files changed, 208 insertions(+), 183 deletions(-) diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index d366d06ec..5fd6b4a28 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -33,7 +33,7 @@ class _ConnectionPageState extends State { final _idController = TextEditingController(); /// Update url. If it's not null, means an update is available. - var _updateUrl = ''; + final _updateUrl = ''; Timer? _updateTimer; @@ -92,7 +92,7 @@ class _ConnectionPageState extends State { if (snapshot.hasData) { return snapshot.data!; } else { - return Offstage(); + return const Offstage(); } }), ], @@ -110,7 +110,7 @@ class _ConnectionPageState extends State { /// Callback for the connect button. /// Connects to the selected peer. void onConnect({bool isFileTransfer = false}) { - var id = _idController.text.trim(); + final id = _idController.text.trim(); connect(id, isFileTransfer: isFileTransfer); } @@ -120,9 +120,9 @@ class _ConnectionPageState extends State { if (id == '') return; id = id.replaceAll(' ', ''); if (isFileTransfer) { - await rustDeskWinManager.new_file_transfer(id); + await rustDeskWinManager.newFileTransfer(id); } else { - await rustDeskWinManager.new_remote_desktop(id); + await rustDeskWinManager.newRemoteDesktop(id); } FocusScopeNode currentFocus = FocusScope.of(context); if (!currentFocus.hasPrimaryFocus) { diff --git a/flutter/lib/desktop/widgets/peer_widget.dart b/flutter/lib/desktop/widgets/peer_widget.dart index 3bfff60bf..02b5b9f00 100644 --- a/flutter/lib/desktop/widgets/peer_widget.dart +++ b/flutter/lib/desktop/widgets/peer_widget.dart @@ -21,18 +21,16 @@ final peerSearchTextController = TextEditingController(text: peerSearchText.value); class _PeerWidget extends StatefulWidget { - late final _peers; - late final OffstageFunc _offstageFunc; - late final PeerCardWidgetFunc _peerCardWidgetFunc; + final Peers peers; + final OffstageFunc offstageFunc; + final PeerCardWidgetFunc peerCardWidgetFunc; - _PeerWidget(Peers peers, OffstageFunc offstageFunc, - PeerCardWidgetFunc peerCardWidgetFunc, - {Key? key}) - : super(key: key) { - _peers = peers; - _offstageFunc = offstageFunc; - _peerCardWidgetFunc = peerCardWidgetFunc; - } + const _PeerWidget( + {required this.peers, + required this.offstageFunc, + required this.peerCardWidgetFunc, + Key? key}) + : super(key: key); @override _PeerWidgetState createState() => _PeerWidgetState(); @@ -42,9 +40,9 @@ class _PeerWidget extends StatefulWidget { class _PeerWidgetState extends State<_PeerWidget> with WindowListener { static const int _maxQueryCount = 3; - var _curPeers = Set(); + final _curPeers = {}; var _lastChangeTime = DateTime.now(); - var _lastQueryPeers = Set(); + var _lastQueryPeers = {}; var _lastQueryTime = DateTime.now().subtract(Duration(hours: 1)); var _queryCoun = 0; var _exit = false; @@ -78,65 +76,62 @@ class _PeerWidgetState extends State<_PeerWidget> with WindowListener { @override Widget build(BuildContext context) { - final space = 12.0; + const space = 12.0; return ChangeNotifierProvider( - create: (context) => super.widget._peers, + create: (context) => widget.peers, child: Consumer( - builder: (context, peers, child) => peers.peers.isEmpty - ? Center( - child: Text(translate("Empty")), - ) - : SingleChildScrollView( - child: ObxValue((searchText) { - return FutureBuilder>( - builder: (context, snapshot) { - if (snapshot.hasData) { - final peers = snapshot.data!; - final cards = []; - for (final peer in peers) { - cards.add(Offstage( - key: ValueKey("off${peer.id}"), - offstage: super.widget._offstageFunc(peer), - child: Obx( - () => SizedBox( - width: 220, - height: - peerCardUiType.value == PeerUiType.grid - ? 140 - : 42, - child: VisibilityDetector( - key: ValueKey(peer.id), - onVisibilityChanged: (info) { - final peerId = - (info.key as ValueKey).value; - if (info.visibleFraction > 0.00001) { - _curPeers.add(peerId); - } else { - _curPeers.remove(peerId); - } - _lastChangeTime = DateTime.now(); - }, - child: super - .widget - ._peerCardWidgetFunc(peer), - ), + builder: (context, peers, child) => peers.peers.isEmpty + ? Center( + child: Text(translate("Empty")), + ) + : SingleChildScrollView( + child: ObxValue((searchText) { + return FutureBuilder>( + builder: (context, snapshot) { + if (snapshot.hasData) { + final peers = snapshot.data!; + final cards = []; + for (final peer in peers) { + cards.add(Offstage( + key: ValueKey("off${peer.id}"), + offstage: widget.offstageFunc(peer), + child: Obx( + () => SizedBox( + width: 220, + height: + peerCardUiType.value == PeerUiType.grid + ? 140 + : 42, + child: VisibilityDetector( + key: ValueKey(peer.id), + onVisibilityChanged: (info) { + final peerId = + (info.key as ValueKey).value; + if (info.visibleFraction > 0.00001) { + _curPeers.add(peerId); + } else { + _curPeers.remove(peerId); + } + _lastChangeTime = DateTime.now(); + }, + child: widget.peerCardWidgetFunc(peer), ), - ))); - } - return Wrap( - spacing: space, - runSpacing: space, - children: cards); - } else { - return const Center( - child: CircularProgressIndicator(), - ); + ), + ))); } - }, - future: matchPeers(searchText.value, peers.peers), - ); - }, peerSearchText), - )), + return Wrap( + spacing: space, runSpacing: space, children: cards); + } else { + return const Center( + child: CircularProgressIndicator(), + ); + } + }, + future: matchPeers(searchText.value, peers.peers), + ); + }, peerSearchText), + ), + ), ); } @@ -175,31 +170,42 @@ class _PeerWidgetState extends State<_PeerWidget> with WindowListener { } abstract class BasePeerWidget extends StatelessWidget { - late final _name; - late final _loadEvent; - late final OffstageFunc _offstageFunc; - late final PeerCardWidgetFunc _peerCardWidgetFunc; - late final List _initPeers; + final String name; + final String loadEvent; + final OffstageFunc offstageFunc; + final PeerCardWidgetFunc peerCardWidgetFunc; + final List initPeers; - BasePeerWidget({Key? key}) : super(key: key) {} + const BasePeerWidget({ + Key? key, + required this.name, + required this.loadEvent, + required this.offstageFunc, + required this.peerCardWidgetFunc, + required this.initPeers, + }) : super(key: key); @override Widget build(BuildContext context) { - return _PeerWidget(Peers(_name, _loadEvent, _initPeers), _offstageFunc, - _peerCardWidgetFunc); + return _PeerWidget( + peers: Peers(name: name, loadEvent: loadEvent, peers: initPeers), + offstageFunc: offstageFunc, + peerCardWidgetFunc: peerCardWidgetFunc); } } class RecentPeerWidget extends BasePeerWidget { - RecentPeerWidget({Key? key}) : super(key: key) { - super._name = "recent peer"; - super._loadEvent = "load_recent_peers"; - super._offstageFunc = (Peer _peer) => false; - super._peerCardWidgetFunc = (Peer peer) => RecentPeerCard( - peer: peer, + RecentPeerWidget({Key? key}) + : super( + key: key, + name: 'recent peer', + loadEvent: 'load_recent_peers', + offstageFunc: (Peer peer) => false, + peerCardWidgetFunc: (Peer peer) => RecentPeerCard( + peer: peer, + ), + initPeers: [], ); - super._initPeers = []; - } @override Widget build(BuildContext context) { @@ -210,13 +216,17 @@ class RecentPeerWidget extends BasePeerWidget { } class FavoritePeerWidget extends BasePeerWidget { - FavoritePeerWidget({Key? key}) : super(key: key) { - super._name = "favorite peer"; - super._loadEvent = "load_fav_peers"; - super._offstageFunc = (Peer _peer) => false; - super._peerCardWidgetFunc = (Peer peer) => FavoritePeerCard(peer: peer); - super._initPeers = []; - } + FavoritePeerWidget({Key? key}) + : super( + key: key, + name: 'favorite peer', + loadEvent: 'load_fav_peers', + offstageFunc: (Peer peer) => false, + peerCardWidgetFunc: (Peer peer) => FavoritePeerCard( + peer: peer, + ), + initPeers: [], + ); @override Widget build(BuildContext context) { @@ -227,13 +237,17 @@ class FavoritePeerWidget extends BasePeerWidget { } class DiscoveredPeerWidget extends BasePeerWidget { - DiscoveredPeerWidget({Key? key}) : super(key: key) { - super._name = "discovered peer"; - super._loadEvent = "load_lan_peers"; - super._offstageFunc = (Peer _peer) => false; - super._peerCardWidgetFunc = (Peer peer) => DiscoveredPeerCard(peer: peer); - super._initPeers = []; - } + DiscoveredPeerWidget({Key? key}) + : super( + key: key, + name: 'discovered peer', + loadEvent: 'load_lan_peers', + offstageFunc: (Peer peer) => false, + peerCardWidgetFunc: (Peer peer) => DiscoveredPeerCard( + peer: peer, + ), + initPeers: [], + ); @override Widget build(BuildContext context) { @@ -244,21 +258,26 @@ class DiscoveredPeerWidget extends BasePeerWidget { } class AddressBookPeerWidget extends BasePeerWidget { - AddressBookPeerWidget({Key? key}) : super(key: key) { - super._name = "address book peer"; - super._offstageFunc = - (Peer peer) => !_hitTag(gFFI.abModel.selectedTags, peer.tags); - super._peerCardWidgetFunc = (Peer peer) => AddressBookPeerCard(peer: peer); - super._initPeers = _loadPeers(); - } + AddressBookPeerWidget({Key? key}) + : super( + key: key, + name: 'address book peer', + loadEvent: 'load_address_book_peers', + offstageFunc: (Peer peer) => + !_hitTag(gFFI.abModel.selectedTags, peer.tags), + peerCardWidgetFunc: (Peer peer) => DiscoveredPeerCard( + peer: peer, + ), + initPeers: _loadPeers(), + ); - List _loadPeers() { + static List _loadPeers() { return gFFI.abModel.peers.map((e) { return Peer.fromJson(e['id'], e); }).toList(); } - bool _hitTag(List selectedTags, List idents) { + static bool _hitTag(List selectedTags, List idents) { if (selectedTags.isEmpty) { return true; } diff --git a/flutter/lib/desktop/widgets/peercard_widget.dart b/flutter/lib/desktop/widgets/peercard_widget.dart index 3b4a30ee0..114f4146e 100644 --- a/flutter/lib/desktop/widgets/peercard_widget.dart +++ b/flutter/lib/desktop/widgets/peercard_widget.dart @@ -348,11 +348,11 @@ abstract class BasePeerCard extends StatelessWidget { assert(!(isFileTransfer && isTcpTunneling && isRDP), "more than one connect type"); if (isFileTransfer) { - await rustDeskWinManager.new_file_transfer(id); + await rustDeskWinManager.newFileTransfer(id); } else if (isTcpTunneling || isRDP) { - await rustDeskWinManager.new_port_forward(id, isRDP); + await rustDeskWinManager.newPortForward(id, isRDP); } else { - await rustDeskWinManager.new_remote_desktop(id); + await rustDeskWinManager.newRemoteDesktop(id); } FocusScopeNode currentFocus = FocusScope.of(context); if (!currentFocus.hasPrimaryFocus) { @@ -468,7 +468,8 @@ abstract class BasePeerCard extends StatelessWidget { } @protected - MenuEntryBase _removeAction(String id) { + MenuEntryBase _removeAction( + String id, Future Function() reloadFunc) { return MenuEntryButton( childBuilder: (TextStyle? style) => Text( translate('Remove'), @@ -478,7 +479,8 @@ abstract class BasePeerCard extends StatelessWidget { () async { await bind.mainRemovePeer(id: id); removePreference(id); - Get.forceAppUpdate(); // TODO use inner model / state + await reloadFunc(); + // Get.forceAppUpdate(); // TODO use inner model / state }(); }, dismissOnClicked: true, @@ -614,7 +616,9 @@ class RecentPeerCard extends BasePeerCard { } menuItems.add(await _forceAlwaysRelayAction(peer.id)); menuItems.add(_renameAction(peer.id, false)); - menuItems.add(_removeAction(peer.id)); + menuItems.add(_removeAction(peer.id, () async { + await bind.mainLoadRecentPeers(); + })); menuItems.add(_unrememberPasswordAction(peer.id)); menuItems.add(_addFavAction(peer.id)); return menuItems; @@ -638,7 +642,9 @@ class FavoritePeerCard extends BasePeerCard { } menuItems.add(await _forceAlwaysRelayAction(peer.id)); menuItems.add(_renameAction(peer.id, false)); - menuItems.add(_removeAction(peer.id)); + menuItems.add(_removeAction(peer.id, () async { + await bind.mainLoadFavPeers(); + })); menuItems.add(_unrememberPasswordAction(peer.id)); menuItems.add(_rmFavAction(peer.id)); return menuItems; @@ -662,9 +668,10 @@ class DiscoveredPeerCard extends BasePeerCard { } menuItems.add(await _forceAlwaysRelayAction(peer.id)); menuItems.add(_renameAction(peer.id, false)); - menuItems.add(_removeAction(peer.id)); + menuItems.add(_removeAction(peer.id, () async { + await bind.mainLoadLanPeers(); + })); menuItems.add(_unrememberPasswordAction(peer.id)); - menuItems.add(_addFavAction(peer.id)); return menuItems; } } @@ -686,7 +693,7 @@ class AddressBookPeerCard extends BasePeerCard { } menuItems.add(await _forceAlwaysRelayAction(peer.id)); menuItems.add(_renameAction(peer.id, false)); - menuItems.add(_removeAction(peer.id)); + menuItems.add(_removeAction(peer.id, () async {})); menuItems.add(_unrememberPasswordAction(peer.id)); menuItems.add(_addFavAction(peer.id)); menuItems.add(_editTagAction(peer.id)); @@ -695,7 +702,8 @@ class AddressBookPeerCard extends BasePeerCard { @protected @override - MenuEntryBase _removeAction(String id) { + MenuEntryBase _removeAction( + String id, Future Function() reloadFunc) { return MenuEntryButton( childBuilder: (TextStyle? style) => Text( translate('Remove'), diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 962998bbd..887bf7d35 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -301,6 +301,9 @@ class FfiModel with ChangeNotifier { /// Handle the peer info event based on [evt]. void 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) + bind.mainLoadRecentPeers(); + parent.target?.dialogManager.dismissAll(); _pi.version = evt['version']; _pi.username = evt['username']; @@ -556,18 +559,18 @@ class CanvasModel with ChangeNotifier { var dxOffset = 0; var dyOffset = 0; if (dw > size.width) { - final xxxx = x - dw * (x / size.width) - _x; - if (xxxx.isInfinite || xxxx.isNaN) { + final X_debugNanOrInfinite = x - dw * (x / size.width) - _x; + if (X_debugNanOrInfinite.isInfinite || X_debugNanOrInfinite.isNaN) { debugPrint( - 'REMOVE ME ============================ xxxx $x,$dw,$_scale,${size.width},$_x'); + 'REMOVE ME ============================ X_debugNanOrInfinite $x,$dw,$_scale,${size.width},$_x'); } dxOffset = (x - dw * (x / size.width) - _x).toInt(); } if (dh > size.height) { - final yyyy = y - dh * (y / size.height) - _y; - if (yyyy.isInfinite || yyyy.isNaN) { + final Y_debugNanOrInfinite = y - dh * (y / size.height) - _y; + if (Y_debugNanOrInfinite.isInfinite || Y_debugNanOrInfinite.isNaN) { debugPrint( - 'REMOVE ME ============================ xxxx $y,$dh,$_scale,${size.height},$_y'); + 'REMOVE ME ============================ Y_debugNanOrInfinite $y,$dh,$_scale,${size.height},$_y'); } dyOffset = (y - dh * (y / size.height) - _y).toInt(); } @@ -1249,20 +1252,20 @@ class PeerInfo { Future savePreference(String id, double xCursor, double yCursor, double xCanvas, double yCanvas, double scale, int currentDisplay) async { SharedPreferences prefs = await SharedPreferences.getInstance(); - final p = Map(); + final p = {}; p['xCursor'] = xCursor; p['yCursor'] = yCursor; p['xCanvas'] = xCanvas; p['yCanvas'] = yCanvas; p['scale'] = scale; p['currentDisplay'] = currentDisplay; - prefs.setString('peer' + id, json.encode(p)); + prefs.setString('peer$id', json.encode(p)); } Future?> getPreference(String id) async { if (!isWebDesktop) return null; SharedPreferences prefs = await SharedPreferences.getInstance(); - var p = prefs.getString('peer' + id); + var p = prefs.getString('peer$id'); if (p == null) return null; Map m = json.decode(p); return m; @@ -1270,7 +1273,7 @@ Future?> getPreference(String id) async { void removePreference(String id) async { SharedPreferences prefs = await SharedPreferences.getInstance(); - prefs.remove('peer' + id); + prefs.remove('peer$id'); } void initializeCursorAndCanvas(FFI ffi) async { diff --git a/flutter/lib/models/peer_model.dart b/flutter/lib/models/peer_model.dart index 5c889e60f..79b71e6db 100644 --- a/flutter/lib/models/peer_model.dart +++ b/flutter/lib/models/peer_model.dart @@ -10,9 +10,8 @@ class Peer { final List tags; bool online = false; - Peer.fromJson(String id, Map json) - : id = id, - username = json['username'] ?? '', + Peer.fromJson(this.id, Map json) + : username = json['username'] ?? '', hostname = json['hostname'] ?? '', platform = json['platform'] ?? '', tags = json['tags'] ?? []; @@ -35,57 +34,52 @@ class Peer { } class Peers extends ChangeNotifier { - late String _name; - late List _peers; - late final _loadEvent; + final String name; + final String loadEvent; + List peers; static const _cbQueryOnlines = 'callback_query_onlines'; - Peers(String name, String loadEvent, List _initPeers) { - _name = name; - _loadEvent = loadEvent; - _peers = _initPeers; - platformFFI.registerEventHandler(_cbQueryOnlines, _name, (evt) { + Peers({required this.name, required this.peers, required this.loadEvent}) { + platformFFI.registerEventHandler(_cbQueryOnlines, name, (evt) { _updateOnlineState(evt); }); - platformFFI.registerEventHandler(_loadEvent, _name, (evt) { + platformFFI.registerEventHandler(loadEvent, name, (evt) { _updatePeers(evt); }); } - List get peers => _peers; - @override void dispose() { - platformFFI.unregisterEventHandler(_cbQueryOnlines, _name); - platformFFI.unregisterEventHandler(_loadEvent, _name); + platformFFI.unregisterEventHandler(_cbQueryOnlines, name); + platformFFI.unregisterEventHandler(loadEvent, name); super.dispose(); } Peer getByIndex(int index) { - if (index < _peers.length) { - return _peers[index]; + if (index < peers.length) { + return peers[index]; } else { return Peer.loading(); } } int getPeersCount() { - return _peers.length; + return peers.length; } void _updateOnlineState(Map evt) { evt['onlines'].split(',').forEach((online) { - for (var i = 0; i < _peers.length; i++) { - if (_peers[i].id == online) { - _peers[i].online = true; + for (var i = 0; i < peers.length; i++) { + if (peers[i].id == online) { + peers[i].online = true; } } }); evt['offlines'].split(',').forEach((offline) { - for (var i = 0; i < _peers.length; i++) { - if (_peers[i].id == offline) { - _peers[i].online = false; + for (var i = 0; i < peers.length; i++) { + if (peers[i].id == offline) { + peers[i].online = false; } } }); @@ -95,19 +89,19 @@ class Peers extends ChangeNotifier { void _updatePeers(Map evt) { final onlineStates = _getOnlineStates(); - _peers = _decodePeers(evt['peers']); - _peers.forEach((peer) { + peers = _decodePeers(evt['peers']); + for (var peer in peers) { final state = onlineStates[peer.id]; peer.online = state != null && state != false; - }); + } notifyListeners(); } Map _getOnlineStates() { - var onlineStates = new Map(); - _peers.forEach((peer) { + var onlineStates = {}; + for (var peer in peers) { onlineStates[peer.id] = peer.online; - }); + } return onlineStates; } @@ -121,7 +115,7 @@ class Peers extends ChangeNotifier { Peer.fromJson(s[0] as String, s[1] as Map)) .toList(); } catch (e) { - print('peers(): $e'); + debugPrint('peers(): $e'); } return []; } diff --git a/flutter/lib/utils/multi_window_manager.dart b/flutter/lib/utils/multi_window_manager.dart index b01b84a9d..97d5a5e23 100644 --- a/flutter/lib/utils/multi_window_manager.dart +++ b/flutter/lib/utils/multi_window_manager.dart @@ -1,5 +1,4 @@ import 'dart:convert'; -import 'dart:ui'; import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/material.dart'; @@ -37,9 +36,9 @@ class RustDeskMultiWindowManager { int? _fileTransferWindowId; int? _portForwardWindowId; - Future new_remote_desktop(String remote_id) async { + Future newRemoteDesktop(String remoteId) async { final msg = - jsonEncode({"type": WindowType.RemoteDesktop.index, "id": remote_id}); + jsonEncode({"type": WindowType.RemoteDesktop.index, "id": remoteId}); try { final ids = await DesktopMultiWindow.getAllSubWindowIds(); @@ -63,9 +62,9 @@ class RustDeskMultiWindowManager { } } - Future new_file_transfer(String remote_id) async { + Future newFileTransfer(String remoteId) async { final msg = - jsonEncode({"type": WindowType.FileTransfer.index, "id": remote_id}); + jsonEncode({"type": WindowType.FileTransfer.index, "id": remoteId}); try { final ids = await DesktopMultiWindow.getAllSubWindowIds(); @@ -88,12 +87,9 @@ class RustDeskMultiWindowManager { } } - Future new_port_forward(String remote_id, bool isRDP) async { - final msg = jsonEncode({ - "type": WindowType.PortForward.index, - "id": remote_id, - "isRDP": isRDP - }); + Future newPortForward(String remoteId, bool isRDP) async { + final msg = jsonEncode( + {"type": WindowType.PortForward.index, "id": remoteId, "isRDP": isRDP}); try { final ids = await DesktopMultiWindow.getAllSubWindowIds(); diff --git a/src/client.rs b/src/client.rs index 25061bcfe..32c0003fd 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1278,11 +1278,11 @@ impl LoginConfigHandler { /// /// * `username` - The name of the peer. /// * `pi` - The peer info. - pub fn handle_peer_info(&mut self, pi: PeerInfo) { + pub fn handle_peer_info(&mut self, pi: &PeerInfo) { if !pi.version.is_empty() { self.version = hbb_common::get_version_number(&pi.version); } - self.features = pi.features.into_option(); + self.features = pi.features.clone().into_option(); let serde = PeerInfoSerde { username: pi.username.clone(), hostname: pi.hostname.clone(), diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 716afacb9..6a3d19880 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -205,8 +205,8 @@ pub fn session_get_custom_image_quality(id: String) -> Option> { } pub fn session_set_custom_image_quality(id: String, value: i32) { - if let Some(session) = SESSIONS.read().unwrap().get(&id) { - session.set_custom_image_quality(value); + if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) { + session.save_custom_image_quality(value); } } diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index d89ce2d3b..5ab6089a0 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -47,6 +47,11 @@ impl Session { self.lc.read().unwrap().image_quality.clone() } + /// Get custom image quality. + pub fn get_custom_image_quality(&self) -> Vec { + self.lc.read().unwrap().custom_image_quality.clone() + } + pub fn save_view_style(&mut self, value: String) { self.lc.write().unwrap().save_view_style(value); } @@ -634,7 +639,7 @@ impl Interface for Session { } } else if !self.is_port_forward() { if pi.displays.is_empty() { - self.lc.write().unwrap().handle_peer_info(pi); + self.lc.write().unwrap().handle_peer_info(&pi); self.update_privacy_mode(); self.msgbox("error", "Remote Error", "No Display"); return; @@ -647,9 +652,9 @@ impl Interface for Session { self.set_display(current.x, current.y, current.width, current.height); } self.update_privacy_mode(); + // Save recent peers, then push event to flutter. So flutter can refresh peer page. + self.lc.write().unwrap().handle_peer_info(&pi); self.set_peer_info(&pi); - self.lc.write().unwrap().handle_peer_info(pi); - if self.is_file_transfer() { self.close_success(); } else if !self.is_port_forward() { From 39a1545e94af91c39915e73cd33fc2d85af8122b Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 1 Sep 2022 12:07:05 +0800 Subject: [PATCH 0348/2015] add close confirmation dialog Signed-off-by: 21pages --- flutter/lib/desktop/pages/server_page.dart | 38 +++++++------ .../lib/desktop/widgets/tabbar_widget.dart | 55 ++++++++++++++++--- 2 files changed, 67 insertions(+), 26 deletions(-) diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index e7922403b..b4573297a 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -48,22 +48,25 @@ class _DesktopServerPageState extends State ], child: Consumer( builder: (context, serverModel, child) => Container( - decoration: BoxDecoration( - border: - Border.all(color: MyTheme.color(context).border!)), - child: Scaffold( - backgroundColor: MyTheme.color(context).bg, - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Expanded(child: ConnectionManager()), - SizedBox.fromSize(size: Size(0, 15.0)), - ], - ), - ), - ), - ))); + decoration: BoxDecoration( + border: Border.all(color: MyTheme.color(context).border!)), + child: Scaffold( + backgroundColor: MyTheme.color(context).bg, + body: Overlay(initialEntries: [ + OverlayEntry(builder: (context) { + gFFI.dialogManager.setOverlayState(Overlay.of(context)); + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Expanded(child: ConnectionManager()), + SizedBox.fromSize(size: Size(0, 15.0)), + ], + ), + ); + }) + ]), + )))); } @override @@ -109,7 +112,8 @@ class ConnectionManagerState extends State { theme: isDarkTheme() ? TarBarTheme.dark() : TarBarTheme.light(), showTitle: false, showMaximize: false, - showMinimize: false, + showMinimize: true, + showClose: true, controller: serverModel.tabController, tabType: DesktopTabType.cm, pageViewBuilder: (pageView) => Row(children: [ diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 38e724bad..755d6946c 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -314,6 +314,8 @@ class DesktopTab extends StatelessWidget { Offstage(offstage: tail == null, child: tail), WindowActionPanel( mainTab: isMainWindow, + tabType: tabType, + state: state, theme: theme, showMinimize: showMinimize, showMaximize: showMaximize, @@ -327,6 +329,8 @@ class DesktopTab extends StatelessWidget { class WindowActionPanel extends StatelessWidget { final bool mainTab; + final DesktopTabType tabType; + final Rx state; final TarBarTheme theme; final bool showMinimize; @@ -337,6 +341,8 @@ class WindowActionPanel extends StatelessWidget { const WindowActionPanel( {Key? key, required this.mainTab, + required this.tabType, + required this.state, required this.theme, this.showMinimize = true, this.showMaximize = true, @@ -411,22 +417,53 @@ class WindowActionPanel extends StatelessWidget { message: 'Close', icon: IconFont.close, theme: theme, - onTap: () { - if (mainTab) { - windowManager.close(); - } else { - // only hide for multi window, not close - Future.delayed(Duration.zero, () { - WindowController.fromWindowId(windowId!).hide(); - }); + onTap: () async { + action() { + if (mainTab) { + windowManager.close(); + } else { + // only hide for multi window, not close + Future.delayed(Duration.zero, () { + WindowController.fromWindowId(windowId!).hide(); + }); + } + onClose?.call(); + } + + if (tabType != DesktopTabType.main && + state.value.tabs.length > 1) { + closeConfirmDialog(action); + } else { + action(); } - onClose?.call(); }, is_close: true, )), ], ); } + + closeConfirmDialog(Function() callback) async { + final res = await gFFI.dialogManager + .show((setState, close) => CustomAlertDialog( + title: Row(children: [ + Icon(Icons.warning_amber_sharp, + color: Colors.redAccent, size: 28), + SizedBox(width: 10), + Text(translate("Warning")), + ]), + content: Text(translate("Disconnect all devices?")), + actions: [ + TextButton( + onPressed: () => close(), child: Text(translate("Cancel"))), + ElevatedButton( + onPressed: () => close(true), child: Text(translate("OK"))), + ], + )); + if (res == true) { + callback(); + } + } } // ignore: must_be_immutable From 155fa51ff4f933876ede66e6a9e9d819aa96505b Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 2 Sep 2022 10:49:20 +0800 Subject: [PATCH 0349/2015] fix: linux wayland setAlignment crash workaround Signed-off-by: Kingtous --- flutter/pubspec.lock | 12 +++++++----- flutter/pubspec.yaml | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index d2bc7b1a8..61fbfc293 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -858,9 +858,11 @@ packages: screen_retriever: dependency: transitive description: - name: screen_retriever - url: "https://pub.flutter-io.cn" - source: hosted + path: "." + ref: "406b9b0" + resolved-ref: "406b9b038b2c1d779f1e7bf609c8c248be247372" + url: "https://github.com/Kingtous/rustdesk_screen_retriever.git" + source: git version: "0.1.2" scroll_pos: dependency: "direct main" @@ -1244,8 +1246,8 @@ packages: dependency: "direct main" description: path: "." - ref: "247818257b4b37f78bebea1719cee765282b3079" - resolved-ref: "247818257b4b37f78bebea1719cee765282b3079" + ref: "4627ba808ed08ff0c08706b01a7f9cc8b747accd" + resolved-ref: "4627ba808ed08ff0c08706b01a7f9cc8b747accd" url: "https://github.com/Kingtous/rustdesk_window_manager" source: git version: "0.2.7" diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index b6ce5d20b..f2d038af3 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -58,7 +58,7 @@ dependencies: window_manager: git: url: https://github.com/Kingtous/rustdesk_window_manager - ref: 247818257b4b37f78bebea1719cee765282b3079 + ref: 4627ba808ed08ff0c08706b01a7f9cc8b747accd desktop_multi_window: git: url: https://github.com/Kingtous/rustdesk_desktop_multi_window From f6bc448cec584021a686cf16afca97a2db29df00 Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 1 Sep 2022 21:18:53 +0800 Subject: [PATCH 0350/2015] adjust cm display behavior Signed-off-by: 21pages --- .../desktop/pages/connection_tab_page.dart | 5 ++- .../lib/desktop/pages/desktop_tab_page.dart | 3 +- .../desktop/pages/file_manager_tab_page.dart | 3 +- .../desktop/pages/port_forward_tab_page.dart | 5 ++- flutter/lib/desktop/pages/server_page.dart | 16 ++++++-- .../lib/desktop/widgets/tabbar_widget.dart | 38 +++++++++++++----- flutter/lib/main.dart | 1 + flutter/lib/mobile/pages/chat_page.dart | 1 + flutter/lib/models/chat_model.dart | 11 ++++- flutter/lib/models/server_model.dart | 40 ++++++++++--------- 10 files changed, 83 insertions(+), 40 deletions(-) diff --git a/flutter/lib/desktop/pages/connection_tab_page.dart b/flutter/lib/desktop/pages/connection_tab_page.dart index 5687c5c7e..d9bc86fe2 100644 --- a/flutter/lib/desktop/pages/connection_tab_page.dart +++ b/flutter/lib/desktop/pages/connection_tab_page.dart @@ -20,7 +20,8 @@ class ConnectionTabPage extends StatefulWidget { } class _ConnectionTabPageState extends State { - final tabController = Get.put(DesktopTabController()); + final tabController = + Get.put(DesktopTabController(tabType: DesktopTabType.remoteScreen)); static const IconData selectedIcon = Icons.desktop_windows_sharp; static const IconData unselectedIcon = Icons.desktop_windows_outlined; @@ -60,6 +61,7 @@ class _ConnectionTabPageState extends State { if (call.method == "new_remote_desktop") { final args = jsonDecode(call.arguments); final id = args['id']; + ConnectionTypeState.init(id); window_on_top(windowId()); ConnectionTypeState.init(id); tabController.add(TabInfo( @@ -94,7 +96,6 @@ class _ConnectionTabPageState extends State { body: Obx(() => DesktopTab( controller: tabController, theme: theme, - tabType: DesktopTabType.remoteScreen, showTabBar: fullscreen.isFalse, onClose: () { tabController.clear(); diff --git a/flutter/lib/desktop/pages/desktop_tab_page.dart b/flutter/lib/desktop/pages/desktop_tab_page.dart index 57ee43e14..874a71dcf 100644 --- a/flutter/lib/desktop/pages/desktop_tab_page.dart +++ b/flutter/lib/desktop/pages/desktop_tab_page.dart @@ -15,7 +15,7 @@ class DesktopTabPage extends StatefulWidget { } class _DesktopTabPageState extends State { - final tabController = DesktopTabController(); + final tabController = DesktopTabController(tabType: DesktopTabType.main); @override void initState() { @@ -46,7 +46,6 @@ class _DesktopTabPageState extends State { body: DesktopTab( controller: tabController, theme: dark ? TarBarTheme.dark() : TarBarTheme.light(), - tabType: DesktopTabType.main, tail: ActionIcon( message: 'Settings', icon: IconFont.menu, diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index 6c8b58a30..add5eed9f 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -25,7 +25,7 @@ class _FileManagerTabPageState extends State { static final IconData unselectedIcon = Icons.file_copy_outlined; _FileManagerTabPageState(Map params) { - Get.put(DesktopTabController()); + Get.put(DesktopTabController(tabType: DesktopTabType.fileTransfer)); tabController.add(TabInfo( key: params['id'], label: params['id'], @@ -74,7 +74,6 @@ class _FileManagerTabPageState extends State { body: DesktopTab( controller: tabController, theme: theme, - tabType: DesktopTabType.fileTransfer, onClose: () { tabController.clear(); }, diff --git a/flutter/lib/desktop/pages/port_forward_tab_page.dart b/flutter/lib/desktop/pages/port_forward_tab_page.dart index 1e2c8e2bc..2340a4ca1 100644 --- a/flutter/lib/desktop/pages/port_forward_tab_page.dart +++ b/flutter/lib/desktop/pages/port_forward_tab_page.dart @@ -18,7 +18,7 @@ class PortForwardTabPage extends StatefulWidget { } class _PortForwardTabPageState extends State { - final tabController = Get.put(DesktopTabController()); + late final DesktopTabController tabController; late final bool isRDP; static const IconData selectedIcon = Icons.forward_sharp; @@ -26,6 +26,8 @@ class _PortForwardTabPageState extends State { _PortForwardTabPageState(Map params) { isRDP = params['isRDP']; + tabController = Get.put(DesktopTabController( + tabType: isRDP ? DesktopTabType.rdp : DesktopTabType.portForward)); tabController.add(TabInfo( key: params['id'], label: params['id'], @@ -78,7 +80,6 @@ class _PortForwardTabPageState extends State { body: DesktopTab( controller: tabController, theme: theme, - tabType: isRDP ? DesktopTabType.rdp : DesktopTabType.portForward, onClose: () { tabController.clear(); }, diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index b4573297a..f64adfca2 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -19,10 +19,12 @@ class DesktopServerPage extends StatefulWidget { class _DesktopServerPageState extends State with WindowListener, AutomaticKeepAliveClientMixin { + final tabController = gFFI.serverModel.tabController; @override void initState() { gFFI.ffiModel.updateEventListener(""); windowManager.addListener(this); + tabController.onRemove = (_, id) => onRemoveId(id); super.initState(); } @@ -39,6 +41,13 @@ class _DesktopServerPageState extends State super.onWindowClose(); } + void onRemoveId(String id) { + if (tabController.state.value.tabs.isEmpty) { + windowManager.close(); + } + } + + @override Widget build(BuildContext context) { super.build(context); return MultiProvider( @@ -115,7 +124,6 @@ class ConnectionManagerState extends State { showMinimize: true, showClose: true, controller: serverModel.tabController, - tabType: DesktopTabType.cm, pageViewBuilder: (pageView) => Row(children: [ Expanded(child: pageView), Consumer( @@ -454,8 +462,10 @@ class _CmControlPanel extends StatelessWidget { decoration: BoxDecoration( color: MyTheme.accent, borderRadius: BorderRadius.circular(10)), child: InkWell( - onTap: () => - checkClickTime(client.id, () => handleAccept(context)), + onTap: () => checkClickTime(client.id, () { + handleAccept(context); + windowManager.minimize(); + }), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 755d6946c..1a19dd833 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -59,13 +59,15 @@ class DesktopTabState { class DesktopTabController { final state = DesktopTabState().obs; + final DesktopTabType tabType; /// index, key Function(int, String)? onRemove; - Function(int)? onSelected; - void add(TabInfo tab) { + DesktopTabController({required this.tabType}); + + void add(TabInfo tab, {bool authorized = false}) { if (!isDesktop) return; final index = state.value.tabs.indexWhere((e) => e.key == tab.key); int toIndex; @@ -79,6 +81,16 @@ class DesktopTabController { toIndex = state.value.tabs.length - 1; assert(toIndex >= 0); } + if (tabType == DesktopTabType.cm) { + Future.delayed(Duration.zero, () async { + window_on_top(null); + }); + if (authorized) { + Future.delayed(const Duration(seconds: 3), () { + windowManager.minimize(); + }); + } + } try { jumpTo(toIndex); } catch (e) { @@ -106,6 +118,7 @@ class DesktopTabController { } void jumpTo(int index) { + if (!isDesktop || index < 0) return; state.update((val) { val!.selected = index; Future.delayed(Duration.zero, (() { @@ -114,12 +127,14 @@ class DesktopTabController { } if (val.scrollController.hasClients && val.scrollController.canScroll && - val.scrollController.itemCount >= index) { + val.scrollController.itemCount > index) { val.scrollController.scrollToItem(index, center: true, animate: true); } })); }); - onSelected?.call(index); + if (state.value.tabs.length > index) { + onSelected?.call(index); + } } void closeBy(String? key) { @@ -154,8 +169,6 @@ typedef LabelGetter = Rx Function(String key); class DesktopTab extends StatelessWidget { final Function(String)? onTabClose; final TarBarTheme theme; - final DesktopTabType tabType; - final bool isMainWindow; final bool showTabBar; final bool showLogo; final bool showTitle; @@ -170,10 +183,12 @@ class DesktopTab extends StatelessWidget { final DesktopTabController controller; Rx get state => controller.state; + late final DesktopTabType tabType; + late final bool isMainWindow; - const DesktopTab({ + DesktopTab({ + Key? key, required this.controller, - required this.tabType, this.theme = const TarBarTheme.light(), this.onTabClose, this.showTabBar = true, @@ -187,8 +202,11 @@ class DesktopTab extends StatelessWidget { this.onClose, this.tabBuilder, this.labelGetter, - }) : isMainWindow = - tabType == DesktopTabType.main || tabType == DesktopTabType.cm; + }) : super(key: key) { + tabType = controller.tabType; + isMainWindow = + tabType == DesktopTabType.main || tabType == DesktopTabType.cm; + } @override Widget build(BuildContext context) { diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index e1c254942..2f1d0680f 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -165,6 +165,7 @@ void runConnectionManagerScreen() async { await windowManager.setAlignment(Alignment.topRight); await windowManager.show(); await windowManager.focus(); + await windowManager.setAlignment(Alignment.topRight); // ensure }) ]); runApp(GetMaterialApp( diff --git a/flutter/lib/mobile/pages/chat_page.dart b/flutter/lib/mobile/pages/chat_page.dart index 738f34e89..b265f6995 100644 --- a/flutter/lib/mobile/pages/chat_page.dart +++ b/flutter/lib/mobile/pages/chat_page.dart @@ -59,6 +59,7 @@ class ChatPage extends StatelessWidget implements PageShape { messages: chatModel .messages[chatModel.currentID]?.chatMessages ?? [], + inputOptions: const InputOptions(sendOnEnter: true), messageOptions: MessageOptions( showOtherUsersAvatar: false, showTime: true, diff --git a/flutter/lib/models/chat_model.dart b/flutter/lib/models/chat_model.dart index de949c782..a9c791ef7 100644 --- a/flutter/lib/models/chat_model.dart +++ b/flutter/lib/models/chat_model.dart @@ -209,10 +209,19 @@ class ChatModel with ChangeNotifier { id: await bind.mainGetLastRemoteId(), ); } else { - final client = _ffi.target?.serverModel.clients[id]; + final client = _ffi.target?.serverModel.clients + .firstWhere((client) => client.id == id); if (client == null) { return debugPrint("Failed to receive msg,user doesn't exist"); } + if (isDesktop) { + window_on_top(null); + var index = _ffi.target?.serverModel.clients + .indexWhere((client) => client.id == id); + if (index != null && index >= 0) { + gFFI.serverModel.tabController.jumpTo(index); + } + } chatUser = ChatUser(id: client.peerId, firstName: client.name); } diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index fa7f15e54..31c579f83 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -32,7 +32,7 @@ class ServerModel with ChangeNotifier { late final TextEditingController _serverId; final _serverPasswd = TextEditingController(text: ""); - final tabController = DesktopTabController(); + final tabController = DesktopTabController(tabType: DesktopTabType.cm); List _clients = []; @@ -347,20 +347,18 @@ class ServerModel with ChangeNotifier { var res = await bind.mainGetClientsState(); try { final List clientsJson = jsonDecode(res); - if (isDesktop && clientsJson.isEmpty && _clients.isNotEmpty) { - // exit cm when >1 peers to no peers - exit(0); - } _clients.clear(); tabController.state.value.tabs.clear(); for (var clientJson in clientsJson) { final client = Client.fromJson(clientJson); _clients.add(client); - tabController.add(TabInfo( - key: client.id.toString(), - label: client.name, - closable: false, - page: Desktop.buildConnectionCard(client))); + tabController.add( + TabInfo( + key: client.id.toString(), + label: client.name, + closable: false, + page: Desktop.buildConnectionCard(client)), + authorized: client.authorized); } notifyListeners(); } catch (e) { @@ -471,14 +469,18 @@ class ServerModel with ChangeNotifier { } else { _clients[index].authorized = true; } - tabController.add(TabInfo( - key: client.id.toString(), - label: client.name, - closable: false, - page: Desktop.buildConnectionCard(client))); + tabController.add( + TabInfo( + key: client.id.toString(), + label: client.name, + closable: false, + page: Desktop.buildConnectionCard(client)), + authorized: true); scrollToBottom(); notifyListeners(); - } catch (e) {} + } catch (e) { + debugPrint("onClientAuthorized:$e"); + } } void onClientRemove(Map evt) { @@ -486,8 +488,10 @@ class ServerModel with ChangeNotifier { final id = int.parse(evt['id'] as String); if (_clients.any((c) => c.id == id)) { final index = _clients.indexWhere((client) => client.id == id); - _clients.removeAt(index); - tabController.remove(index); + if (index >= 0) { + _clients.removeAt(index); + tabController.remove(index); + } parent.target?.dialogManager.dismissByTag(getLoginDialogTag(id)); parent.target?.invokeMethod("cancel_notification", id); } From 4421d08384a17d3ee2b251d53aaa8bf319b4a7f4 Mon Sep 17 00:00:00 2001 From: Asura Date: Thu, 1 Sep 2022 20:24:50 -0700 Subject: [PATCH 0351/2015] Delete reset function about enigo --- libs/enigo/src/linux/nix_impl.rs | 4 ---- src/server/input_service.rs | 2 -- 2 files changed, 6 deletions(-) diff --git a/libs/enigo/src/linux/nix_impl.rs b/libs/enigo/src/linux/nix_impl.rs index 0c9b30eff..2e3fb8a24 100644 --- a/libs/enigo/src/linux/nix_impl.rs +++ b/libs/enigo/src/linux/nix_impl.rs @@ -19,10 +19,6 @@ impl Enigo { pub fn set_delay(&mut self, delay: u64) { self.xdo.set_delay(delay) } - /// Reset pynput?. - pub fn reset(&mut self) { - todo!() - } /// Set uinput keyboard. pub fn set_uinput_keyboard( &mut self, diff --git a/src/server/input_service.rs b/src/server/input_service.rs index ffcaf32a9..f8e338a93 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -23,8 +23,6 @@ impl super::service::Reset for StateCursor { *self = Default::default(); crate::platform::reset_input_cache(); fix_key_down_timeout(true); - #[cfg(target_os = "linux")] - ENIGO.lock().unwrap().reset(); } } From 2dc8c02d15daf2d2b53ba07b56afac4804e21180 Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 1 Sep 2022 19:39:51 -0700 Subject: [PATCH 0352/2015] flutter_desktop: custom image quality ui Signed-off-by: fufesou --- flutter/lib/desktop/widgets/peercard_widget.dart | 2 +- flutter/lib/desktop/widgets/remote_menubar.dart | 15 +++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/flutter/lib/desktop/widgets/peercard_widget.dart b/flutter/lib/desktop/widgets/peercard_widget.dart index 114f4146e..a91f300fd 100644 --- a/flutter/lib/desktop/widgets/peercard_widget.dart +++ b/flutter/lib/desktop/widgets/peercard_widget.dart @@ -276,7 +276,7 @@ class _PeerCardState extends State<_PeerCard> color: _iconMoreHover.value ? MyTheme.color(context).text : MyTheme.color(context).lightText), - position: mod_menu.PopupMenuPosition.under, + position: mod_menu.PopupMenuPosition.over, itemBuilder: (BuildContext context) => snapshot.data!, )))); } else { diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 47536011d..a6399b77b 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -489,17 +489,24 @@ class _RemoteMenubarState extends State { final slider = Obx(() { return Slider( value: sliderValue.value, - max: 100, - divisions: 100, - label: sliderValue.value.round().toString(), + min: 10.0, + max: 100.0, + divisions: 90, + // label: sliderValue.value.round().toString(), onChanged: (double value) { sliderValue.value = value; rxReplay.add(value); }, ); }); + final content = Row( + children: [ + slider, + Obx(() => Text('${sliderValue.value.round()}% Bitrate')) + ], + ); msgBoxCommon(widget.ffi.dialogManager, 'Custom Image Quality', - slider, [btnCancel]); + content, [btnCancel]); } }), MenuEntryDivider(), From 722a4d3de7683de72e854a6d2bbc60269c494ab1 Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 1 Sep 2022 22:36:40 -0700 Subject: [PATCH 0353/2015] flutter desktop: ui changes Signed-off-by: fufesou --- .../lib/desktop/pages/desktop_home_page.dart | 3 + flutter/lib/desktop/pages/remote_page.dart | 407 +----------------- .../lib/desktop/widgets/peercard_widget.dart | 97 +++-- .../lib/desktop/widgets/remote_menubar.dart | 22 +- 4 files changed, 88 insertions(+), 441 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 632177e29..5a082a8fd 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -6,6 +6,9 @@ import 'package:flutter/services.dart'; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/desktop/pages/connection_page.dart'; import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart'; +import 'package:flutter_hbb/desktop/widgets/popup_menu.dart'; +import 'package:flutter_hbb/desktop/widgets/material_mod_popup_menu.dart' + as mod_menu; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/server_model.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 16c04f572..a245f1f12 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -17,7 +17,6 @@ import '../../mobile/widgets/dialog.dart'; import '../../mobile/widgets/overlay.dart'; import '../../models/model.dart'; import '../../models/platform_model.dart'; -import '../../models/chat_model.dart'; import '../../common/shared_state.dart'; final initText = '\1' * 1024; @@ -39,7 +38,6 @@ class RemotePage extends StatefulWidget { class _RemotePageState extends State with AutomaticKeepAliveClientMixin { Timer? _timer; - bool _showBar = !isWebDesktop; String _value = ''; final _cursorOverImage = false.obs; @@ -131,7 +129,7 @@ class _RemotePageState extends State common < oldValue.length && common < newValue.length && newValue[common] == oldValue[common]; - ++common); + ++common) {} for (i = 0; i < oldValue.length - common; ++i) { _ffi.inputKey('VK_BACK'); } @@ -145,8 +143,8 @@ class _RemotePageState extends State } return; } - if (oldValue.length > 0 && - newValue.length > 0 && + if (oldValue.isNotEmpty && + newValue.isNotEmpty && oldValue[0] == '\1' && newValue[0] != '\1') { // clipboard @@ -155,7 +153,7 @@ class _RemotePageState extends State if (newValue.length == oldValue.length) { // ? } else if (newValue.length < oldValue.length) { - final char = 'VK_BACK'; + const char = 'VK_BACK'; _ffi.inputKey(char); } else { final content = newValue.substring(oldValue.length); @@ -200,24 +198,9 @@ class _RemotePageState extends State } Widget buildBody(BuildContext context, FfiModel ffiModel) { - final hasDisplays = ffiModel.pi.displays.length > 0; final keyboard = ffiModel.permissions['keyboard'] != false; return Scaffold( backgroundColor: MyTheme.color(context).bg, - // resizeToAvoidBottomInset: true, - // floatingActionButton: _showBar - // ? null - // : FloatingActionButton( - // mini: true, - // child: Icon(Icons.expand_less), - // backgroundColor: MyTheme.accent, - // onPressed: () { - // setState(() { - // _showBar = !_showBar; - // }); - // }), - // bottomNavigationBar: - // _showBar && hasDisplays ? getBottomAppBar(ffiModel) : null, body: Overlay( initialEntries: [ OverlayEntry(builder: (context) { @@ -249,7 +232,7 @@ class _RemotePageState extends State ChangeNotifierProvider.value(value: _ffi.canvasModel), ], child: Consumer( - builder: (context, ffiModel, _child) => + builder: (context, ffiModel, child) => buildBody(context, ffiModel)))); } @@ -307,100 +290,6 @@ class _RemotePageState extends State child: child)))); } - Widget? getBottomAppBar(FfiModel ffiModel) { - final RxBool fullscreen = Get.find(tag: 'fullscreen'); - return MouseRegion( - cursor: SystemMouseCursors.basic, - child: BottomAppBar( - elevation: 10, - color: MyTheme.accent, - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - IconButton( - color: Colors.white, - icon: Icon(Icons.clear), - onPressed: () { - clientClose(_ffi.dialogManager); - }, - ) - ] + - [ - IconButton( - color: Colors.white, - icon: Icon(Icons.tv), - onPressed: () { - _ffi.dialogManager.dismissAll(); - showOptions(widget.id); - }, - ) - ] + - (isWebDesktop - ? [] - : [ - IconButton( - color: Colors.white, - icon: Icon(fullscreen.isTrue - ? Icons.fullscreen - : Icons.close_fullscreen), - onPressed: () { - fullscreen.value = !fullscreen.value; - }, - ) - ]) + - (isWebDesktop - ? [] - : _ffi.ffiModel.isPeerAndroid - ? [ - IconButton( - color: Colors.white, - icon: Icon(Icons.build), - onPressed: () { - if (mobileActionsOverlayEntry == null) { - showMobileActionsOverlay(); - } else { - hideMobileActionsOverlay(); - } - }, - ) - ] - : []) + - (isWeb - ? [] - : [ - IconButton( - color: Colors.white, - icon: Icon(Icons.message), - onPressed: () { - _ffi.chatModel - .changeCurrentID(ChatModel.clientModeID); - _ffi.chatModel.toggleChatOverlay(); - }, - ) - ]) + - [ - IconButton( - color: Colors.white, - icon: Icon(Icons.more_vert), - onPressed: () { - showActions(widget.id, ffiModel); - }, - ), - ]), - IconButton( - color: Colors.white, - icon: Icon(Icons.expand_more), - onPressed: () { - setState(() => _showBar = !_showBar); - }), - ], - ), - )); - } - /// touchMode only: /// LongPress -> right click /// OneFingerPan -> start/end -> left down start/end @@ -458,12 +347,16 @@ class _RemotePageState extends State if (e is PointerScrollEvent) { var dx = e.scrollDelta.dx.toInt(); var dy = e.scrollDelta.dy.toInt(); - if (dx > 0) + if (dx > 0) { dx = -1; - else if (dx < 0) dx = 1; - if (dy > 0) + } else if (dx < 0) { + dx = 1; + } + if (dy > 0) { dy = -1; - else if (dy < 0) dy = 1; + } else if (dy < 0) { + dy = 1; + } bind.sessionSendMouse( id: widget.id, msg: '{"type": "wheel", "x": "$dx", "y": "$dy"}'); } @@ -546,106 +439,6 @@ class _RemotePageState extends State return out; } - void showActions(String id, FfiModel ffiModel) async { - final size = MediaQuery.of(context).size; - final x = 120.0; - final y = size.height - super.widget.tabBarHeight; - final more = >[]; - final pi = _ffi.ffiModel.pi; - final perms = _ffi.ffiModel.permissions; - if (pi.version.isNotEmpty) { - more.add(PopupMenuItem( - child: Text(translate('Refresh')), value: 'refresh')); - } - more.add(PopupMenuItem( - child: Row( - children: ([ - Text(translate('OS Password')), - TextButton( - style: flatButtonStyle, - onPressed: () { - showSetOSPassword(widget.id, false, _ffi.dialogManager); - }, - child: Icon(Icons.edit, color: MyTheme.accent), - ) - ])), - value: 'enter_os_password')); - if (!isWebDesktop) { - if (perms['keyboard'] != false && perms['clipboard'] != false) { - more.add(PopupMenuItem( - child: Text(translate('Paste')), value: 'paste')); - } - more.add(PopupMenuItem( - child: Text(translate('Reset canvas')), value: 'reset_canvas')); - } - if (perms['keyboard'] != false) { - if (pi.platform == 'Linux' || pi.sasEnabled) { - more.add(PopupMenuItem( - child: Text(translate('Insert') + ' Ctrl + Alt + Del'), - value: 'cad')); - } - more.add(PopupMenuItem( - child: Text(translate('Insert Lock')), value: 'lock')); - if (pi.platform == 'Windows' && - await bind.sessionGetToggleOption(id: id, arg: 'privacy-mode') != - true) { - more.add(PopupMenuItem( - child: Text(translate( - (ffiModel.inputBlocked ? 'Unb' : 'B') + 'lock user input')), - value: 'block-input')); - } - } - if (gFFI.ffiModel.permissions["restart"] != false && - (pi.platform == "Linux" || - pi.platform == "Windows" || - pi.platform == "Mac OS")) { - more.add(PopupMenuItem( - child: Text(translate('Restart Remote Device')), value: 'restart')); - } - () async { - var value = await showMenu( - context: context, - position: RelativeRect.fromLTRB(x, y, x, y), - items: more, - elevation: 8, - ); - if (value == 'cad') { - bind.sessionCtrlAltDel(id: widget.id); - } else if (value == 'lock') { - bind.sessionLockScreen(id: widget.id); - } else if (value == 'block-input') { - bind.sessionToggleOption( - id: widget.id, - value: (_ffi.ffiModel.inputBlocked ? 'un' : '') + 'block-input'); - _ffi.ffiModel.inputBlocked = !_ffi.ffiModel.inputBlocked; - } else if (value == 'refresh') { - bind.sessionRefresh(id: widget.id); - } else if (value == 'paste') { - () async { - ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain); - if (data != null && data.text != null) { - bind.sessionInputString(id: widget.id, value: data.text ?? ""); - } - }(); - } else if (value == 'enter_os_password') { - // FIXME: - // TODO icon diff - // null means no session of id - // empty string means no password - var password = await bind.sessionGetOption(id: id, arg: "os-password"); - if (password != null) { - bind.sessionInputOsPassword(id: widget.id, value: password); - } else { - showSetOSPassword(widget.id, true, _ffi.dialogManager); - } - } else if (value == 'reset_canvas') { - _ffi.cursorModel.reset(); - } else if (value == 'restart') { - showRestartRemoteDevice(pi, widget.id, gFFI.dialogManager); - } - }(); - } - @override void onWindowEvent(String eventName) { print("window event: $eventName"); @@ -676,7 +469,7 @@ class ImagePaint extends StatelessWidget { {Key? key, required this.id, required this.cursorOverImage, - this.listenerBuilder = null}) + this.listenerBuilder}) : super(key: key); @override @@ -855,177 +648,7 @@ class QualityMonitor extends StatelessWidget { ], ), ) - : SizedBox.shrink()))); -} - -void showOptions(String id) async { - final _ffi = ffi(id); - String quality = await bind.sessionGetImageQuality(id: id) ?? 'balanced'; - if (quality == '') quality = 'balanced'; - String viewStyle = - await bind.sessionGetOption(id: id, arg: 'view-style') ?? ''; - String scrollStyle = - await bind.sessionGetOption(id: id, arg: 'scroll-style') ?? ''; - var displays = []; - final pi = _ffi.ffiModel.pi; - final image = _ffi.ffiModel.getConnectionImage(); - if (image != null) - displays.add(Padding(padding: const EdgeInsets.only(top: 8), child: image)); - if (pi.displays.length > 1) { - final cur = pi.currentDisplay; - final children = []; - for (var i = 0; i < pi.displays.length; ++i) - children.add(InkWell( - onTap: () { - if (i == cur) return; - bind.sessionSwitchDisplay(id: id, value: i); - _ffi.dialogManager.dismissAll(); - }, - child: Ink( - width: 40, - height: 40, - decoration: BoxDecoration( - border: Border.all(color: Colors.black87), - color: i == cur ? Colors.black87 : Colors.white), - child: Center( - child: Text((i + 1).toString(), - style: TextStyle( - color: i == cur ? Colors.white : Colors.black87)))))); - displays.add(Padding( - padding: const EdgeInsets.only(top: 8), - child: Wrap( - alignment: WrapAlignment.center, - spacing: 8, - children: children, - ))); - } - if (displays.isNotEmpty) { - displays.add(Divider(color: MyTheme.border)); - } - final perms = _ffi.ffiModel.permissions; - - _ffi.dialogManager.show((setState, close) { - final more = []; - if (perms['audio'] != false) { - more.add(getToggle(id, setState, 'disable-audio', 'Mute')); - } - if (perms['keyboard'] != false) { - if (perms['clipboard'] != false) - more.add( - getToggle(id, setState, 'disable-clipboard', 'Disable clipboard')); - more.add(getToggle( - id, setState, 'lock-after-session-end', 'Lock after session end')); - if (pi.platform == 'Windows') { - more.add(Consumer( - builder: (_context, _ffiModel, _child) => () { - return getToggle( - id, setState, 'privacy-mode', 'Privacy mode'); - }())); - } - } - var setQuality = (String? value) { - if (value == null) return; - setState(() { - quality = value; - bind.sessionSetImageQuality(id: id, value: value); - }); - }; - var setViewStyle = (String? value) { - if (value == null) return; - setState(() { - viewStyle = value; - bind.sessionPeerOption(id: id, name: "view-style", value: value); - _ffi.canvasModel.updateViewStyle(); - }); - }; - var setScrollStyle = (String? value) { - if (value == null) return; - setState(() { - scrollStyle = value; - bind.sessionPeerOption(id: id, name: "scroll-style", value: value); - _ffi.canvasModel.updateScrollStyle(); - }); - }; - return CustomAlertDialog( - title: SizedBox.shrink(), - content: Column( - mainAxisSize: MainAxisSize.min, - children: displays + - [ - getRadio('Original', 'original', viewStyle, setViewStyle), - getRadio('Shrink', 'shrink', viewStyle, setViewStyle), - getRadio('Stretch', 'stretch', viewStyle, setViewStyle), - Divider(color: MyTheme.border), - getRadio( - 'ScrollAuto', 'scrollauto', scrollStyle, setScrollStyle), - getRadio('Scrollbar', 'scrollbar', scrollStyle, setScrollStyle), - Divider(color: MyTheme.border), - getRadio('Good image quality', 'best', quality, setQuality), - getRadio('Balanced', 'balanced', quality, setQuality), - getRadio('Optimize reaction time', 'low', quality, setQuality), - Divider(color: MyTheme.border), - getToggle( - id, setState, 'show-remote-cursor', 'Show remote cursor'), - getToggle(id, setState, 'show-quality-monitor', - 'Show quality monitor', - ffi: _ffi), - ] + - more), - actions: [], - contentPadding: 0, - ); - }, clickMaskDismiss: true, backDismiss: true); -} - -void showSetOSPassword( - String id, bool login, OverlayDialogManager dialogManager) async { - final controller = TextEditingController(); - var password = await bind.sessionGetOption(id: id, arg: "os-password") ?? ""; - var autoLogin = await bind.sessionGetOption(id: id, arg: "auto-login") != ""; - controller.text = password; - dialogManager.show((setState, close) { - return CustomAlertDialog( - title: Text(translate('OS Password')), - content: Column(mainAxisSize: MainAxisSize.min, children: [ - PasswordWidget(controller: controller), - CheckboxListTile( - contentPadding: const EdgeInsets.all(0), - dense: true, - controlAffinity: ListTileControlAffinity.leading, - title: Text( - translate('Auto Login'), - ), - value: autoLogin, - onChanged: (v) { - if (v == null) return; - setState(() => autoLogin = v); - }, - ), - ]), - actions: [ - TextButton( - style: flatButtonStyle, - onPressed: () { - close(); - }, - child: Text(translate('Cancel')), - ), - TextButton( - style: flatButtonStyle, - onPressed: () { - var text = controller.text.trim(); - bind.sessionPeerOption(id: id, name: "os-password", value: text); - bind.sessionPeerOption( - id: id, name: "auto-login", value: autoLogin ? 'Y' : ''); - if (text != "" && login) { - bind.sessionInputOsPassword(id: id, value: text); - } - close(); - }, - child: Text(translate('OK')), - ), - ]); - }); + : const SizedBox.shrink()))); } void sendPrompt(String id, bool isMac, String key) { diff --git a/flutter/lib/desktop/widgets/peercard_widget.dart b/flutter/lib/desktop/widgets/peercard_widget.dart index a91f300fd..d9c0015f8 100644 --- a/flutter/lib/desktop/widgets/peercard_widget.dart +++ b/flutter/lib/desktop/widgets/peercard_widget.dart @@ -15,7 +15,7 @@ class _PopupMenuTheme { static const Color commonColor = MyTheme.accent; // kMinInteractiveDimension static const double height = 25.0; - static const double dividerHeight = 12.0; + static const double dividerHeight = 3.0; } typedef PopupMenuEntryBuilder = Future>> @@ -46,6 +46,7 @@ class _PeerCard extends StatefulWidget { /// State for the connection page. class _PeerCardState extends State<_PeerCard> with AutomaticKeepAliveClientMixin { + var _menuPos = RelativeRect.fill; final double _cardRadis = 20; final double _borderWidth = 2; final RxBool _iconMoreHover = false.obs; @@ -253,36 +254,36 @@ class _PeerCardState extends State<_PeerCard> ); } - Widget _actionMore(Peer peer) { - return FutureBuilder( - future: widget.popupMenuEntryBuilder(context), - initialData: const >[], - builder: (BuildContext context, - AsyncSnapshot>> snapshot) { - if (snapshot.hasData) { - return Listener( - child: MouseRegion( - onEnter: (_) => _iconMoreHover.value = true, - onExit: (_) => _iconMoreHover.value = false, - child: CircleAvatar( - radius: 14, - backgroundColor: _iconMoreHover.value - ? MyTheme.color(context).grayBg! - : MyTheme.color(context).bg!, - child: mod_menu.PopupMenuButton( - padding: EdgeInsets.zero, - icon: Icon(Icons.more_vert, - size: 18, - color: _iconMoreHover.value - ? MyTheme.color(context).text - : MyTheme.color(context).lightText), - position: mod_menu.PopupMenuPosition.over, - itemBuilder: (BuildContext context) => snapshot.data!, - )))); - } else { - return Container(); - } - }); + Widget _actionMore(Peer peer) => Listener( + onPointerDown: (e) { + final x = e.position.dx; + final y = e.position.dy; + _menuPos = RelativeRect.fromLTRB(x, y, x, y); + }, + onPointerUp: (_) => _showPeerMenu(context, peer.id), + child: MouseRegion( + onEnter: (_) => _iconMoreHover.value = true, + onExit: (_) => _iconMoreHover.value = false, + child: CircleAvatar( + radius: 14, + backgroundColor: _iconMoreHover.value + ? MyTheme.color(context).grayBg! + : MyTheme.color(context).bg!, + child: Icon(Icons.more_vert, + size: 18, + color: _iconMoreHover.value + ? MyTheme.color(context).text + : MyTheme.color(context).lightText)))); + + /// Show the peer menu and handle user's choice. + /// User might remove the peer or send a file to the peer. + void _showPeerMenu(BuildContext context, String id) async { + await mod_menu.showMenu( + context: context, + position: _menuPos, + items: await super.widget.popupMenuEntryBuilder(context), + elevation: 8, + ); } /// Get the image for the current [platform]. @@ -411,19 +412,26 @@ abstract class BasePeerCard extends StatelessWidget { @protected MenuEntryBase _rdpAction(BuildContext context, String id) { return MenuEntryButton( - childBuilder: (TextStyle? style) => Row( - children: [ - Text( - translate('RDP'), - style: style, - ), - SizedBox(width: 20), - IconButton( - icon: Icon(Icons.edit), - onPressed: () => _rdpDialog(id), - ) - ], - ), + childBuilder: (TextStyle? style) => Container( + alignment: AlignmentDirectional.center, + height: _PopupMenuTheme.height, + child: Row( + children: [ + Text( + translate('RDP'), + style: style, + ), + Expanded( + child: Align( + alignment: Alignment.centerRight, + child: IconButton( + padding: EdgeInsets.zero, + icon: Icon(Icons.edit), + onPressed: () => _rdpDialog(id), + ), + )) + ], + )), proc: () { _connect(context, id, isRDP: true); }, @@ -614,6 +622,7 @@ class RecentPeerCard extends BasePeerCard { if (peer.platform == 'Windows') { menuItems.add(_rdpAction(context, peer.id)); } + menuItems.add(MenuEntryDivider()); menuItems.add(await _forceAlwaysRelayAction(peer.id)); menuItems.add(_renameAction(peer.id, false)); menuItems.add(_removeAction(peer.id, () async { diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index a6399b77b..dbe7592e6 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -472,9 +472,17 @@ class _RemoteMenubarState extends State { }); final quality = await bind.sessionGetCustomImageQuality(id: widget.id); - final double initValue = quality != null && quality.isNotEmpty + double initValue = quality != null && quality.isNotEmpty ? quality[0].toDouble() : 50.0; + const minValue = 10.0; + const maxValue = 100.0; + if (initValue < minValue) { + initValue = minValue; + } + if (initValue > maxValue) { + initValue = maxValue; + } final RxDouble sliderValue = RxDouble(initValue); final rxReplay = rxdart.ReplaySubject(); rxReplay @@ -489,10 +497,9 @@ class _RemoteMenubarState extends State { final slider = Obx(() { return Slider( value: sliderValue.value, - min: 10.0, - max: 100.0, + min: minValue, + max: maxValue, divisions: 90, - // label: sliderValue.value.round().toString(), onChanged: (double value) { sliderValue.value = value; rxReplay.add(value); @@ -502,7 +509,12 @@ class _RemoteMenubarState extends State { final content = Row( children: [ slider, - Obx(() => Text('${sliderValue.value.round()}% Bitrate')) + SizedBox( + width: 90, + child: Obx(() => Text( + '${sliderValue.value.round()}% Bitrate', + style: const TextStyle(fontSize: 15), + ))) ], ); msgBoxCommon(widget.ffi.dialogManager, 'Custom Image Quality', From a4ee1bcc38c1d874bb8b735d23a883c156df0efe Mon Sep 17 00:00:00 2001 From: Asura Date: Thu, 1 Sep 2022 23:58:14 -0700 Subject: [PATCH 0354/2015] Get flutter deps when build --- build.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/build.rs b/build.rs index 860ebae77..5ed40b001 100644 --- a/build.rs +++ b/build.rs @@ -77,6 +77,17 @@ fn install_oboe() { } fn gen_flutter_rust_bridge() { + // Get dependent of flutter + println!("cargo:rerun-if-changed=flutter/pubspec.lock"); + println!("cargo:rerun-if-changed=flutter/pubspec.yaml"); + if !std::path::Path::new("./flutter/.packages").exists(){ + std::process::Command::new("flutter") + .args(["pub", "get"]) + .current_dir("./flutter") + .output() + .expect("failed to execute flutter pub get"); + }; + let llvm_path = match std::env::var("LLVM_HOME") { Ok(path) => Some(vec![path]), Err(_) => None, From 2b0778987cd510cada2b49b36baa0bd560a33abc Mon Sep 17 00:00:00 2001 From: Asura Date: Fri, 2 Sep 2022 00:24:09 -0700 Subject: [PATCH 0355/2015] Update pubspec.lock to fix CI --- flutter/pubspec.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index a6dade189..69e7f3c4b 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -1242,8 +1242,8 @@ packages: dependency: "direct main" description: path: "." - ref: "799ef079e87938c3f4340591b4330c2598f38bb9" - resolved-ref: "799ef079e87938c3f4340591b4330c2598f38bb9" + ref: 4627ba808ed08ff0c08706b01a7f9cc8b747accd + resolved-ref: 4627ba808ed08ff0c08706b01a7f9cc8b747accd url: "https://github.com/Kingtous/rustdesk_window_manager" source: git version: "0.2.6" From be14a102b9971fe611949ce44fa7f074b6798f3e Mon Sep 17 00:00:00 2001 From: Asura Date: Fri, 2 Sep 2022 01:11:35 -0700 Subject: [PATCH 0356/2015] Opt: handle error in tfc --- src/server/input_service.rs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/server/input_service.rs b/src/server/input_service.rs index f8e338a93..b72370d12 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -7,7 +7,7 @@ use rdev::{simulate, EventType, Key as RdevKey}; use std::{ convert::TryFrom, sync::atomic::{AtomicBool, Ordering}, - time::Instant, + time::Instant }; use tfc::{traits::*, Context as TFC_Context, Key as TFC_Key}; @@ -673,10 +673,14 @@ fn tfc_key_down_or_up(key: Key, down: bool, up: bool) { if let Key::Layout(chr) = key { log::info!("tfc_key_down_or_up :{:?}", chr); if down { - TFC_CONTEXT.lock().unwrap().unicode_char_down(chr); + if let Err(e) = TFC_CONTEXT.lock().unwrap().unicode_char_down(chr){ + log::error!("Failed to press char {:?}", chr); + }; } if up { - TFC_CONTEXT.lock().unwrap().unicode_char_up(chr); + if let Err(e) = TFC_CONTEXT.lock().unwrap().unicode_char_down(chr){ + log::error!("Failed to press char {:?}",chr); + }; } return; } @@ -749,10 +753,14 @@ fn tfc_key_down_or_up(key: Key, down: bool, up: bool) { log::info!("tfc_key_down_or_up: {:?}", key); if down { - TFC_CONTEXT.lock().unwrap().key_down(key); + if let Err(e) = TFC_CONTEXT.lock().unwrap().key_down(key){ + log::error!("Failed to press char {:?}", key); + }; } if up { - TFC_CONTEXT.lock().unwrap().key_up(key); + if let Err(e) = TFC_CONTEXT.lock().unwrap().key_up(key){ + log::error!("Failed to press char {:?}", key); + }; } } From 03315a3bc4b429a3be0c7bde63abefbd4c345ecb Mon Sep 17 00:00:00 2001 From: Asura Date: Fri, 2 Sep 2022 01:11:59 -0700 Subject: [PATCH 0357/2015] Update pubspec.lock --- flutter/pubspec.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 69e7f3c4b..a6dade189 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -1242,8 +1242,8 @@ packages: dependency: "direct main" description: path: "." - ref: 4627ba808ed08ff0c08706b01a7f9cc8b747accd - resolved-ref: 4627ba808ed08ff0c08706b01a7f9cc8b747accd + ref: "799ef079e87938c3f4340591b4330c2598f38bb9" + resolved-ref: "799ef079e87938c3f4340591b4330c2598f38bb9" url: "https://github.com/Kingtous/rustdesk_window_manager" source: git version: "0.2.6" From 41241867b729ebb19b3339122b620e207cad330e Mon Sep 17 00:00:00 2001 From: Asura Date: Fri, 2 Sep 2022 01:20:48 -0700 Subject: [PATCH 0358/2015] Fix compile warning --- build.rs | 2 -- src/client.rs | 1 - src/common.rs | 1 - src/flutter_ffi.rs | 8 ++++---- src/rendezvous_mediator.rs | 5 +---- src/server/connection.rs | 2 +- src/server/input_service.rs | 8 ++++---- src/ui.rs | 4 ++-- src/ui/cm.rs | 4 ++-- src/ui/remote.rs | 2 +- src/ui_session_interface.rs | 1 - 11 files changed, 15 insertions(+), 23 deletions(-) diff --git a/build.rs b/build.rs index 5ed40b001..d9b7a5516 100644 --- a/build.rs +++ b/build.rs @@ -78,8 +78,6 @@ fn install_oboe() { fn gen_flutter_rust_bridge() { // Get dependent of flutter - println!("cargo:rerun-if-changed=flutter/pubspec.lock"); - println!("cargo:rerun-if-changed=flutter/pubspec.yaml"); if !std::path::Path::new("./flutter/.packages").exists(){ std::process::Command::new("flutter") .args(["pub", "get"]) diff --git a/src/client.rs b/src/client.rs index e47667f72..2bc60765a 100644 --- a/src/client.rs +++ b/src/client.rs @@ -61,7 +61,6 @@ pub struct Client; lazy_static::lazy_static! { static ref AUDIO_HOST: Host = cpal::default_host(); } -use rdev::{Event, EventType::*, Key as RdevKey, Keyboard as RdevKeyboard, KeyboardState}; #[cfg(not(any(target_os = "android", target_os = "ios")))] lazy_static::lazy_static! { diff --git a/src/common.rs b/src/common.rs index 5c387c07e..81adebb0a 100644 --- a/src/common.rs +++ b/src/common.rs @@ -668,7 +668,6 @@ pub fn make_privacy_mode_msg(state: back_notification::PrivacyModeState) -> Mess } pub fn make_fd_to_json(fd: FileDirectory) -> String { - use serde_json::json; let mut fd_json = serde_json::Map::new(); fd_json.insert("id".into(), json!(fd.id)); fd_json.insert("path".into(), json!(fd.path)); diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index bc1335970..78f53bf89 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -5,13 +5,13 @@ use std::{ }; use flutter_rust_bridge::{StreamSink, SyncReturn, ZeroCopyBuffer}; -use serde_json::{json, Number, Value}; +use serde_json::json; use hbb_common::{ - config::{self, Config, LocalConfig, PeerConfig, ONLINE}, + config::{self, LocalConfig, PeerConfig, ONLINE}, fs, log, }; -use hbb_common::{password_security, ResultType}; +use hbb_common::{ResultType}; use crate::{client::file_trait::FileManager, flutter::{session_add, session_start_}}; use crate::common::make_fd_to_json; @@ -20,7 +20,7 @@ use crate::flutter::{self, SESSIONS}; use crate::start_server; use crate::ui_interface; #[cfg(not(any(target_os = "android", target_os = "ios")))] -use crate::ui_interface::{change_id, check_connect_status, is_ok_change_id}; +use crate::ui_interface::{change_id}; use crate::ui_interface::{ check_mouse_time, check_super_user_permission, discover, forget_password, get_api_server, get_app_name, get_async_job_status, get_connect_status, get_fav, get_id, get_lan_peers, diff --git a/src/rendezvous_mediator.rs b/src/rendezvous_mediator.rs index 08a1316f0..802fed1de 100644 --- a/src/rendezvous_mediator.rs +++ b/src/rendezvous_mediator.rs @@ -1,4 +1,3 @@ -use std::collections::HashMap; use std::{ net::SocketAddr, sync::{ @@ -10,12 +9,10 @@ use std::{ use uuid::Uuid; -use hbb_common::config::DiscoveryPeer; use hbb_common::tcp::FramedStream; use hbb_common::{ allow_err, anyhow::bail, - config, config::{Config, REG_INTERVAL, RENDEZVOUS_PORT, RENDEZVOUS_TIMEOUT}, futures::future::join_all, log, @@ -640,7 +637,7 @@ pub async fn query_online_states, Vec)>(ids: Vec ResultType { - let (mut rendezvous_server, servers, contained) = crate::get_rendezvous_server(1_000).await; + let (mut rendezvous_server, _servers, _contained) = crate::get_rendezvous_server(1_000).await; let tmp: Vec<&str> = rendezvous_server.split(":").collect(); if tmp.len() != 2 { bail!("Invalid server address: {}", rendezvous_server); diff --git a/src/server/connection.rs b/src/server/connection.rs index e71b32e35..d93d6d775 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -633,7 +633,7 @@ impl Connection { let mut pi = PeerInfo { username: username.clone(), conn_id: self.inner.id, - version: crate::VERSION.to_owned(), + version: VERSION.to_owned(), ..Default::default() }; diff --git a/src/server/input_service.rs b/src/server/input_service.rs index b72370d12..e0707bded 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -673,12 +673,12 @@ fn tfc_key_down_or_up(key: Key, down: bool, up: bool) { if let Key::Layout(chr) = key { log::info!("tfc_key_down_or_up :{:?}", chr); if down { - if let Err(e) = TFC_CONTEXT.lock().unwrap().unicode_char_down(chr){ + if let Err(_) = TFC_CONTEXT.lock().unwrap().unicode_char_down(chr){ log::error!("Failed to press char {:?}", chr); }; } if up { - if let Err(e) = TFC_CONTEXT.lock().unwrap().unicode_char_down(chr){ + if let Err(_) = TFC_CONTEXT.lock().unwrap().unicode_char_down(chr){ log::error!("Failed to press char {:?}",chr); }; } @@ -753,12 +753,12 @@ fn tfc_key_down_or_up(key: Key, down: bool, up: bool) { log::info!("tfc_key_down_or_up: {:?}", key); if down { - if let Err(e) = TFC_CONTEXT.lock().unwrap().key_down(key){ + if let Err(_) = TFC_CONTEXT.lock().unwrap().key_down(key){ log::error!("Failed to press char {:?}", key); }; } if up { - if let Err(e) = TFC_CONTEXT.lock().unwrap().key_up(key){ + if let Err(_) = TFC_CONTEXT.lock().unwrap().key_up(key){ log::error!("Failed to press char {:?}", key); }; } diff --git a/src/ui.rs b/src/ui.rs index b66d1453b..0e754e622 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -9,7 +9,7 @@ use sciter::Value; use hbb_common::{ allow_err, - config::{self, Config, LocalConfig, PeerConfig, RENDEZVOUS_PORT, RENDEZVOUS_TIMEOUT}, + config::{self, Config, PeerConfig, RENDEZVOUS_PORT, RENDEZVOUS_TIMEOUT}, futures::future::join_all, log, protobuf::Message as _, @@ -19,7 +19,7 @@ use hbb_common::{ tokio::{self, sync::mpsc, time}, }; -use crate::common::{get_app_name, SOFTWARE_UPDATE_URL}; +use crate::common::{get_app_name}; use crate::ipc; use crate::ui_interface::{ check_mouse_time, closing, create_shortcut, current_is_wayland, fix_login_wayland, diff --git a/src/ui/cm.rs b/src/ui/cm.rs index 3200d51b4..bef0ee3bd 100644 --- a/src/ui/cm.rs +++ b/src/ui/cm.rs @@ -6,13 +6,13 @@ use clipboard::{ create_cliprdr_context, empty_clipboard, get_rx_clip_client, server_clip_file, set_conn_enabled, }; use hbb_common::fs::{ - can_enable_overwrite_detection, get_string, is_write_need_confirmation, new_send_confirm, + get_string, is_write_need_confirmation, new_send_confirm, DigestCheckResult, }; use hbb_common::{ allow_err, config::Config, - fs, get_version_number, log, + fs, log, message_proto::*, protobuf::Message as _, tokio::{self, sync::mpsc, task::spawn_blocking}, diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 91dd08fdc..093ad901e 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -2,7 +2,7 @@ use std::{ collections::HashMap, ops::{Deref, DerefMut}, sync::{ - atomic::{AtomicBool, Ordering}, + atomic::{Ordering}, Arc, Mutex, }, }; diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 33949a116..4f5ec55b8 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -6,7 +6,6 @@ use crate::client::{ load_config, send_mouse, start_video_audio_threads, FileManager, Key, LoginConfigHandler, QualityStatus, KEY_MAP, SERVER_KEYBOARD_ENABLED, }; -use crate::common; use crate::{client::Data, client::Interface}; use async_trait::async_trait; From a2763c2d6f194e573e3e58f41109f1e11a00dca1 Mon Sep 17 00:00:00 2001 From: Asura Date: Fri, 2 Sep 2022 02:05:29 -0700 Subject: [PATCH 0359/2015] Remove generate file --- flutter/.gitignore | 5 +- flutter/lib/generated_bridge.dart | 4289 ------------------ flutter/lib/generated_bridge.freezed.dart | 332 -- flutter/lib/generated_plugin_registrant.dart | 35 - 4 files changed, 3 insertions(+), 4658 deletions(-) delete mode 100644 flutter/lib/generated_bridge.dart delete mode 100644 flutter/lib/generated_bridge.freezed.dart delete mode 100644 flutter/lib/generated_plugin_registrant.dart diff --git a/flutter/.gitignore b/flutter/.gitignore index af69247b0..3cbfc0f54 100644 --- a/flutter/.gitignore +++ b/flutter/.gitignore @@ -32,17 +32,18 @@ /build/ # Web related +lib/generated_plugin_registrant.dart # Symbolication related app.*.symbols - # Obfuscation related app.*.map.json jniLibs - .vscode # flutter rust bridge +lib/generated_bridge.dart +lib/generated_bridge.freezed.dart # Flutter Generated Files **/GeneratedPluginRegistrant.swift diff --git a/flutter/lib/generated_bridge.dart b/flutter/lib/generated_bridge.dart deleted file mode 100644 index 2cbeb430e..000000000 --- a/flutter/lib/generated_bridge.dart +++ /dev/null @@ -1,4289 +0,0 @@ -// AUTO GENERATED FILE, DO NOT EDIT. -// Generated by `flutter_rust_bridge`. - -// ignore_for_file: non_constant_identifier_names, unused_element, duplicate_ignore, directives_ordering, curly_braces_in_flow_control_structures, unnecessary_lambdas, slash_for_doc_comments, prefer_const_literals_to_create_immutables, implicit_dynamic_list_literal, duplicate_import, unused_import, prefer_single_quotes, prefer_const_constructors - -import 'dart:convert'; -import 'dart:typed_data'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -import 'dart:convert'; -import 'dart:typed_data'; -import 'package:flutter_rust_bridge/flutter_rust_bridge.dart'; -import 'dart:ffi' as ffi; - -part 'generated_bridge.freezed.dart'; - -abstract class Rustdesk { - /// FFI for rustdesk core's main entry. - /// Return true if the app should continue running with UI(possibly Flutter), false if the app should exit. - Future rustdeskCoreMain({dynamic hint}); - - Stream startGlobalEventStream( - {required String appType, dynamic hint}); - - Future stopGlobalEventStream({required String appType, dynamic hint}); - - Future hostStopSystemKeyPropagate( - {required bool stopped, dynamic hint}); - - String sessionAddSync( - {required String id, - required bool isFileTransfer, - required bool isPortForward, - dynamic hint}); - - Stream sessionStart({required String id, dynamic hint}); - - Future sessionGetRemember({required String id, dynamic hint}); - - Future sessionGetToggleOption( - {required String id, required String arg, dynamic hint}); - - bool sessionGetToggleOptionSync( - {required String id, required String arg, dynamic hint}); - - Future sessionGetOption( - {required String id, required String arg, dynamic hint}); - - Future sessionLogin( - {required String id, - required String password, - required bool remember, - dynamic hint}); - - Future sessionClose({required String id, dynamic hint}); - - Future sessionRefresh({required String id, dynamic hint}); - - Future sessionReconnect({required String id, dynamic hint}); - - Future sessionToggleOption( - {required String id, required String value, dynamic hint}); - - Future sessionGetImageQuality({required String id, dynamic hint}); - - Future sessionSetImageQuality( - {required String id, required String value, dynamic hint}); - - Future sessionGetCustomImageQuality( - {required String id, dynamic hint}); - - Future sessionSetCustomImageQuality( - {required String id, required int value, dynamic hint}); - - Future sessionLockScreen({required String id, dynamic hint}); - - Future sessionCtrlAltDel({required String id, dynamic hint}); - - Future sessionSwitchDisplay( - {required String id, required int value, dynamic hint}); - - Future sessionInputKey( - {required String id, - required String name, - required bool down, - required bool press, - required bool alt, - required bool ctrl, - required bool shift, - required bool command, - dynamic hint}); - - Future sessionInputString( - {required String id, required String value, dynamic hint}); - - Future sessionSendChat( - {required String id, required String text, dynamic hint}); - - Future sessionPeerOption( - {required String id, - required String name, - required String value, - dynamic hint}); - - Future sessionGetPeerOption( - {required String id, required String name, dynamic hint}); - - Future sessionInputOsPassword( - {required String id, required String value, dynamic hint}); - - Future sessionReadRemoteDir( - {required String id, - required String path, - required bool includeHidden, - dynamic hint}); - - Future sessionSendFiles( - {required String id, - required int actId, - required String path, - required String to, - required int fileNum, - required bool includeHidden, - required bool isRemote, - dynamic hint}); - - Future sessionSetConfirmOverrideFile( - {required String id, - required int actId, - required int fileNum, - required bool needOverride, - required bool remember, - required bool isUpload, - dynamic hint}); - - Future sessionRemoveFile( - {required String id, - required int actId, - required String path, - required int fileNum, - required bool isRemote, - dynamic hint}); - - Future sessionReadDirRecursive( - {required String id, - required int actId, - required String path, - required bool isRemote, - required bool showHidden, - dynamic hint}); - - Future sessionRemoveAllEmptyDirs( - {required String id, - required int actId, - required String path, - required bool isRemote, - dynamic hint}); - - Future sessionCancelJob( - {required String id, required int actId, dynamic hint}); - - Future sessionCreateDir( - {required String id, - required int actId, - required String path, - required bool isRemote, - dynamic hint}); - - Future sessionReadLocalDirSync( - {required String id, - required String path, - required bool showHidden, - dynamic hint}); - - Future sessionGetPlatform( - {required String id, required bool isRemote, dynamic hint}); - - Future sessionLoadLastTransferJobs({required String id, dynamic hint}); - - Future sessionAddJob( - {required String id, - required int actId, - required String path, - required String to, - required int fileNum, - required bool includeHidden, - required bool isRemote, - dynamic hint}); - - Future sessionResumeJob( - {required String id, - required int actId, - required bool isRemote, - dynamic hint}); - - Future> mainGetSoundInputs({dynamic hint}); - - Future mainChangeId({required String newId, dynamic hint}); - - Future mainGetAsyncStatus({dynamic hint}); - - Future mainGetOption({required String key, dynamic hint}); - - Future mainSetOption( - {required String key, required String value, dynamic hint}); - - Future mainGetOptions({dynamic hint}); - - Future mainSetOptions({required String json, dynamic hint}); - - Future mainTestIfValidServer({required String server, dynamic hint}); - - Future mainSetSocks( - {required String proxy, - required String username, - required String password, - dynamic hint}); - - Future> mainGetSocks({dynamic hint}); - - Future mainGetAppName({dynamic hint}); - - Future mainGetLicense({dynamic hint}); - - Future mainGetVersion({dynamic hint}); - - Future> mainGetFav({dynamic hint}); - - Future mainStoreFav({required List favs, dynamic hint}); - - Future mainGetPeer({required String id, dynamic hint}); - - Future mainGetLanPeers({dynamic hint}); - - Future mainGetConnectStatus({dynamic hint}); - - Future mainCheckConnectStatus({dynamic hint}); - - Future mainIsUsingPublicServer({dynamic hint}); - - Future mainDiscover({dynamic hint}); - - Future mainHasRendezvousService({dynamic hint}); - - Future mainGetApiServer({dynamic hint}); - - Future mainPostRequest( - {required String url, - required String body, - required String header, - dynamic hint}); - - Future mainGetLocalOption({required String key, dynamic hint}); - - Future mainSetLocalOption( - {required String key, required String value, dynamic hint}); - - Future mainGetMyId({dynamic hint}); - - Future mainGetUuid({dynamic hint}); - - Future mainGetPeerOption( - {required String id, required String key, dynamic hint}); - - Future mainSetPeerOption( - {required String id, - required String key, - required String value, - dynamic hint}); - - Future mainForgetPassword({required String id, dynamic hint}); - - Future mainGetRecentPeers({dynamic hint}); - - Future mainLoadRecentPeers({dynamic hint}); - - Future mainLoadFavPeers({dynamic hint}); - - Future mainLoadLanPeers({dynamic hint}); - - Future sessionAddPortForward( - {required String id, - required int localPort, - required String remoteHost, - required int remotePort, - dynamic hint}); - - Future sessionRemovePortForward( - {required String id, required int localPort, dynamic hint}); - - Future mainGetLastRemoteId({dynamic hint}); - - Future mainGetSoftwareUpdateUrl({dynamic hint}); - - Future mainGetHomeDir({dynamic hint}); - - Future mainGetLangs({dynamic hint}); - - Future mainGetTemporaryPassword({dynamic hint}); - - Future mainGetPermanentPassword({dynamic hint}); - - Future mainGetOnlineStatue({dynamic hint}); - - Future mainGetClientsState({dynamic hint}); - - Future mainCheckClientsLength({required int length, dynamic hint}); - - Future mainInit({required String appDir, dynamic hint}); - - Future mainDeviceId({required String id, dynamic hint}); - - Future mainDeviceName({required String name, dynamic hint}); - - Future mainRemovePeer({required String id, dynamic hint}); - - Future mainHasHwcodec({dynamic hint}); - - Future sessionSendMouse( - {required String id, required String msg, dynamic hint}); - - Future sessionRestartRemoteDevice({required String id, dynamic hint}); - - Future mainSetHomeDir({required String home, dynamic hint}); - - Future mainStopService({dynamic hint}); - - Future mainStartService({dynamic hint}); - - Future mainUpdateTemporaryPassword({dynamic hint}); - - Future mainSetPermanentPassword( - {required String password, dynamic hint}); - - Future mainCheckSuperUserPermission({dynamic hint}); - - Future mainCheckMouseTime({dynamic hint}); - - Future mainGetMouseTime({dynamic hint}); - - Future cmSendChat( - {required int connId, required String msg, dynamic hint}); - - Future cmLoginRes( - {required int connId, required bool res, dynamic hint}); - - Future cmCloseConnection({required int connId, dynamic hint}); - - Future cmCheckClickTime({required int connId, dynamic hint}); - - Future cmGetClickTime({dynamic hint}); - - Future cmSwitchPermission( - {required int connId, - required String name, - required bool enabled, - dynamic hint}); - - Future mainGetIcon({dynamic hint}); - - Future queryOnlines({required List ids, dynamic hint}); -} - -@freezed -class EventToUI with _$EventToUI { - const factory EventToUI.event( - String field0, - ) = Event; - const factory EventToUI.rgba( - Uint8List field0, - ) = Rgba; -} - -class RustdeskImpl extends FlutterRustBridgeBase - implements Rustdesk { - factory RustdeskImpl(ffi.DynamicLibrary dylib) => - RustdeskImpl.raw(RustdeskWire(dylib)); - - RustdeskImpl.raw(RustdeskWire inner) : super(inner); - - Future rustdeskCoreMain({dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_rustdesk_core_main(port_), - parseSuccessData: _wire2api_bool, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "rustdesk_core_main", - argNames: [], - ), - argValues: [], - hint: hint, - )); - - Stream startGlobalEventStream( - {required String appType, dynamic hint}) => - executeStream(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_start_global_event_stream( - port_, _api2wire_String(appType)), - parseSuccessData: _wire2api_String, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "start_global_event_stream", - argNames: ["appType"], - ), - argValues: [appType], - hint: hint, - )); - - Future stopGlobalEventStream({required String appType, dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_stop_global_event_stream( - port_, _api2wire_String(appType)), - parseSuccessData: _wire2api_unit, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "stop_global_event_stream", - argNames: ["appType"], - ), - argValues: [appType], - hint: hint, - )); - - Future hostStopSystemKeyPropagate( - {required bool stopped, dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => - inner.wire_host_stop_system_key_propagate(port_, stopped), - parseSuccessData: _wire2api_unit, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "host_stop_system_key_propagate", - argNames: ["stopped"], - ), - argValues: [stopped], - hint: hint, - )); - - String sessionAddSync( - {required String id, - required bool isFileTransfer, - required bool isPortForward, - dynamic hint}) => - executeSync(FlutterRustBridgeSyncTask( - callFfi: () => inner.wire_session_add_sync( - _api2wire_String(id), isFileTransfer, isPortForward), - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "session_add_sync", - argNames: ["id", "isFileTransfer", "isPortForward"], - ), - argValues: [id, isFileTransfer, isPortForward], - hint: hint, - )); - - Stream sessionStart({required String id, dynamic hint}) => - executeStream(FlutterRustBridgeTask( - callFfi: (port_) => - inner.wire_session_start(port_, _api2wire_String(id)), - parseSuccessData: _wire2api_event_to_ui, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "session_start", - argNames: ["id"], - ), - argValues: [id], - hint: hint, - )); - - Future sessionGetRemember({required String id, dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => - inner.wire_session_get_remember(port_, _api2wire_String(id)), - parseSuccessData: _wire2api_opt_box_autoadd_bool, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "session_get_remember", - argNames: ["id"], - ), - argValues: [id], - hint: hint, - )); - - Future sessionGetToggleOption( - {required String id, required String arg, dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_session_get_toggle_option( - port_, _api2wire_String(id), _api2wire_String(arg)), - parseSuccessData: _wire2api_opt_box_autoadd_bool, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "session_get_toggle_option", - argNames: ["id", "arg"], - ), - argValues: [id, arg], - hint: hint, - )); - - bool sessionGetToggleOptionSync( - {required String id, required String arg, dynamic hint}) => - executeSync(FlutterRustBridgeSyncTask( - callFfi: () => inner.wire_session_get_toggle_option_sync( - _api2wire_String(id), _api2wire_String(arg)), - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "session_get_toggle_option_sync", - argNames: ["id", "arg"], - ), - argValues: [id, arg], - hint: hint, - )); - - Future sessionGetOption( - {required String id, required String arg, dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_session_get_option( - port_, _api2wire_String(id), _api2wire_String(arg)), - parseSuccessData: _wire2api_opt_String, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "session_get_option", - argNames: ["id", "arg"], - ), - argValues: [id, arg], - hint: hint, - )); - - Future sessionLogin( - {required String id, - required String password, - required bool remember, - dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_session_login( - port_, _api2wire_String(id), _api2wire_String(password), remember), - parseSuccessData: _wire2api_unit, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "session_login", - argNames: ["id", "password", "remember"], - ), - argValues: [id, password, remember], - hint: hint, - )); - - Future sessionClose({required String id, dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => - inner.wire_session_close(port_, _api2wire_String(id)), - parseSuccessData: _wire2api_unit, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "session_close", - argNames: ["id"], - ), - argValues: [id], - hint: hint, - )); - - Future sessionRefresh({required String id, dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => - inner.wire_session_refresh(port_, _api2wire_String(id)), - parseSuccessData: _wire2api_unit, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "session_refresh", - argNames: ["id"], - ), - argValues: [id], - hint: hint, - )); - - Future sessionReconnect({required String id, dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => - inner.wire_session_reconnect(port_, _api2wire_String(id)), - parseSuccessData: _wire2api_unit, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "session_reconnect", - argNames: ["id"], - ), - argValues: [id], - hint: hint, - )); - - Future sessionToggleOption( - {required String id, required String value, dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_session_toggle_option( - port_, _api2wire_String(id), _api2wire_String(value)), - parseSuccessData: _wire2api_unit, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "session_toggle_option", - argNames: ["id", "value"], - ), - argValues: [id, value], - hint: hint, - )); - - Future sessionGetImageQuality({required String id, dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => - inner.wire_session_get_image_quality(port_, _api2wire_String(id)), - parseSuccessData: _wire2api_opt_String, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "session_get_image_quality", - argNames: ["id"], - ), - argValues: [id], - hint: hint, - )); - - Future sessionSetImageQuality( - {required String id, required String value, dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_session_set_image_quality( - port_, _api2wire_String(id), _api2wire_String(value)), - parseSuccessData: _wire2api_unit, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "session_set_image_quality", - argNames: ["id", "value"], - ), - argValues: [id, value], - hint: hint, - )); - - Future sessionGetCustomImageQuality( - {required String id, dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_session_get_custom_image_quality( - port_, _api2wire_String(id)), - parseSuccessData: _wire2api_opt_int_32_list, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "session_get_custom_image_quality", - argNames: ["id"], - ), - argValues: [id], - hint: hint, - )); - - Future sessionSetCustomImageQuality( - {required String id, required int value, dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_session_set_custom_image_quality( - port_, _api2wire_String(id), _api2wire_i32(value)), - parseSuccessData: _wire2api_unit, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "session_set_custom_image_quality", - argNames: ["id", "value"], - ), - argValues: [id, value], - hint: hint, - )); - - Future sessionLockScreen({required String id, dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => - inner.wire_session_lock_screen(port_, _api2wire_String(id)), - parseSuccessData: _wire2api_unit, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "session_lock_screen", - argNames: ["id"], - ), - argValues: [id], - hint: hint, - )); - - Future sessionCtrlAltDel({required String id, dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => - inner.wire_session_ctrl_alt_del(port_, _api2wire_String(id)), - parseSuccessData: _wire2api_unit, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "session_ctrl_alt_del", - argNames: ["id"], - ), - argValues: [id], - hint: hint, - )); - - Future sessionSwitchDisplay( - {required String id, required int value, dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_session_switch_display( - port_, _api2wire_String(id), _api2wire_i32(value)), - parseSuccessData: _wire2api_unit, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "session_switch_display", - argNames: ["id", "value"], - ), - argValues: [id, value], - hint: hint, - )); - - Future sessionInputKey( - {required String id, - required String name, - required bool down, - required bool press, - required bool alt, - required bool ctrl, - required bool shift, - required bool command, - dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_session_input_key( - port_, - _api2wire_String(id), - _api2wire_String(name), - down, - press, - alt, - ctrl, - shift, - command), - parseSuccessData: _wire2api_unit, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "session_input_key", - argNames: [ - "id", - "name", - "down", - "press", - "alt", - "ctrl", - "shift", - "command" - ], - ), - argValues: [id, name, down, press, alt, ctrl, shift, command], - hint: hint, - )); - - Future sessionInputString( - {required String id, required String value, dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_session_input_string( - port_, _api2wire_String(id), _api2wire_String(value)), - parseSuccessData: _wire2api_unit, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "session_input_string", - argNames: ["id", "value"], - ), - argValues: [id, value], - hint: hint, - )); - - Future sessionSendChat( - {required String id, required String text, dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_session_send_chat( - port_, _api2wire_String(id), _api2wire_String(text)), - parseSuccessData: _wire2api_unit, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "session_send_chat", - argNames: ["id", "text"], - ), - argValues: [id, text], - hint: hint, - )); - - Future sessionPeerOption( - {required String id, - required String name, - required String value, - dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_session_peer_option( - port_, - _api2wire_String(id), - _api2wire_String(name), - _api2wire_String(value)), - parseSuccessData: _wire2api_unit, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "session_peer_option", - argNames: ["id", "name", "value"], - ), - argValues: [id, name, value], - hint: hint, - )); - - Future sessionGetPeerOption( - {required String id, required String name, dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_session_get_peer_option( - port_, _api2wire_String(id), _api2wire_String(name)), - parseSuccessData: _wire2api_String, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "session_get_peer_option", - argNames: ["id", "name"], - ), - argValues: [id, name], - hint: hint, - )); - - Future sessionInputOsPassword( - {required String id, required String value, dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_session_input_os_password( - port_, _api2wire_String(id), _api2wire_String(value)), - parseSuccessData: _wire2api_unit, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "session_input_os_password", - argNames: ["id", "value"], - ), - argValues: [id, value], - hint: hint, - )); - - Future sessionReadRemoteDir( - {required String id, - required String path, - required bool includeHidden, - dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_session_read_remote_dir( - port_, _api2wire_String(id), _api2wire_String(path), includeHidden), - parseSuccessData: _wire2api_unit, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "session_read_remote_dir", - argNames: ["id", "path", "includeHidden"], - ), - argValues: [id, path, includeHidden], - hint: hint, - )); - - Future sessionSendFiles( - {required String id, - required int actId, - required String path, - required String to, - required int fileNum, - required bool includeHidden, - required bool isRemote, - dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_session_send_files( - port_, - _api2wire_String(id), - _api2wire_i32(actId), - _api2wire_String(path), - _api2wire_String(to), - _api2wire_i32(fileNum), - includeHidden, - isRemote), - parseSuccessData: _wire2api_unit, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "session_send_files", - argNames: [ - "id", - "actId", - "path", - "to", - "fileNum", - "includeHidden", - "isRemote" - ], - ), - argValues: [id, actId, path, to, fileNum, includeHidden, isRemote], - hint: hint, - )); - - Future sessionSetConfirmOverrideFile( - {required String id, - required int actId, - required int fileNum, - required bool needOverride, - required bool remember, - required bool isUpload, - dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_session_set_confirm_override_file( - port_, - _api2wire_String(id), - _api2wire_i32(actId), - _api2wire_i32(fileNum), - needOverride, - remember, - isUpload), - parseSuccessData: _wire2api_unit, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "session_set_confirm_override_file", - argNames: [ - "id", - "actId", - "fileNum", - "needOverride", - "remember", - "isUpload" - ], - ), - argValues: [id, actId, fileNum, needOverride, remember, isUpload], - hint: hint, - )); - - Future sessionRemoveFile( - {required String id, - required int actId, - required String path, - required int fileNum, - required bool isRemote, - dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_session_remove_file( - port_, - _api2wire_String(id), - _api2wire_i32(actId), - _api2wire_String(path), - _api2wire_i32(fileNum), - isRemote), - parseSuccessData: _wire2api_unit, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "session_remove_file", - argNames: ["id", "actId", "path", "fileNum", "isRemote"], - ), - argValues: [id, actId, path, fileNum, isRemote], - hint: hint, - )); - - Future sessionReadDirRecursive( - {required String id, - required int actId, - required String path, - required bool isRemote, - required bool showHidden, - dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_session_read_dir_recursive( - port_, - _api2wire_String(id), - _api2wire_i32(actId), - _api2wire_String(path), - isRemote, - showHidden), - parseSuccessData: _wire2api_unit, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "session_read_dir_recursive", - argNames: ["id", "actId", "path", "isRemote", "showHidden"], - ), - argValues: [id, actId, path, isRemote, showHidden], - hint: hint, - )); - - Future sessionRemoveAllEmptyDirs( - {required String id, - required int actId, - required String path, - required bool isRemote, - dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_session_remove_all_empty_dirs( - port_, - _api2wire_String(id), - _api2wire_i32(actId), - _api2wire_String(path), - isRemote), - parseSuccessData: _wire2api_unit, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "session_remove_all_empty_dirs", - argNames: ["id", "actId", "path", "isRemote"], - ), - argValues: [id, actId, path, isRemote], - hint: hint, - )); - - Future sessionCancelJob( - {required String id, required int actId, dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_session_cancel_job( - port_, _api2wire_String(id), _api2wire_i32(actId)), - parseSuccessData: _wire2api_unit, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "session_cancel_job", - argNames: ["id", "actId"], - ), - argValues: [id, actId], - hint: hint, - )); - - Future sessionCreateDir( - {required String id, - required int actId, - required String path, - required bool isRemote, - dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_session_create_dir( - port_, - _api2wire_String(id), - _api2wire_i32(actId), - _api2wire_String(path), - isRemote), - parseSuccessData: _wire2api_unit, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "session_create_dir", - argNames: ["id", "actId", "path", "isRemote"], - ), - argValues: [id, actId, path, isRemote], - hint: hint, - )); - - Future sessionReadLocalDirSync( - {required String id, - required String path, - required bool showHidden, - dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_session_read_local_dir_sync( - port_, _api2wire_String(id), _api2wire_String(path), showHidden), - parseSuccessData: _wire2api_String, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "session_read_local_dir_sync", - argNames: ["id", "path", "showHidden"], - ), - argValues: [id, path, showHidden], - hint: hint, - )); - - Future sessionGetPlatform( - {required String id, required bool isRemote, dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_session_get_platform( - port_, _api2wire_String(id), isRemote), - parseSuccessData: _wire2api_String, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "session_get_platform", - argNames: ["id", "isRemote"], - ), - argValues: [id, isRemote], - hint: hint, - )); - - Future sessionLoadLastTransferJobs( - {required String id, dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_session_load_last_transfer_jobs( - port_, _api2wire_String(id)), - parseSuccessData: _wire2api_unit, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "session_load_last_transfer_jobs", - argNames: ["id"], - ), - argValues: [id], - hint: hint, - )); - - Future sessionAddJob( - {required String id, - required int actId, - required String path, - required String to, - required int fileNum, - required bool includeHidden, - required bool isRemote, - dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_session_add_job( - port_, - _api2wire_String(id), - _api2wire_i32(actId), - _api2wire_String(path), - _api2wire_String(to), - _api2wire_i32(fileNum), - includeHidden, - isRemote), - parseSuccessData: _wire2api_unit, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "session_add_job", - argNames: [ - "id", - "actId", - "path", - "to", - "fileNum", - "includeHidden", - "isRemote" - ], - ), - argValues: [id, actId, path, to, fileNum, includeHidden, isRemote], - hint: hint, - )); - - Future sessionResumeJob( - {required String id, - required int actId, - required bool isRemote, - dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_session_resume_job( - port_, _api2wire_String(id), _api2wire_i32(actId), isRemote), - parseSuccessData: _wire2api_unit, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "session_resume_job", - argNames: ["id", "actId", "isRemote"], - ), - argValues: [id, actId, isRemote], - hint: hint, - )); - - Future> mainGetSoundInputs({dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_main_get_sound_inputs(port_), - parseSuccessData: _wire2api_StringList, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "main_get_sound_inputs", - argNames: [], - ), - argValues: [], - hint: hint, - )); - - Future mainChangeId({required String newId, dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => - inner.wire_main_change_id(port_, _api2wire_String(newId)), - parseSuccessData: _wire2api_unit, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "main_change_id", - argNames: ["newId"], - ), - argValues: [newId], - hint: hint, - )); - - Future mainGetAsyncStatus({dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_main_get_async_status(port_), - parseSuccessData: _wire2api_String, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "main_get_async_status", - argNames: [], - ), - argValues: [], - hint: hint, - )); - - Future mainGetOption({required String key, dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => - inner.wire_main_get_option(port_, _api2wire_String(key)), - parseSuccessData: _wire2api_String, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "main_get_option", - argNames: ["key"], - ), - argValues: [key], - hint: hint, - )); - - Future mainSetOption( - {required String key, required String value, dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_main_set_option( - port_, _api2wire_String(key), _api2wire_String(value)), - parseSuccessData: _wire2api_unit, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "main_set_option", - argNames: ["key", "value"], - ), - argValues: [key, value], - hint: hint, - )); - - Future mainGetOptions({dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_main_get_options(port_), - parseSuccessData: _wire2api_String, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "main_get_options", - argNames: [], - ), - argValues: [], - hint: hint, - )); - - Future mainSetOptions({required String json, dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => - inner.wire_main_set_options(port_, _api2wire_String(json)), - parseSuccessData: _wire2api_unit, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "main_set_options", - argNames: ["json"], - ), - argValues: [json], - hint: hint, - )); - - Future mainTestIfValidServer( - {required String server, dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_main_test_if_valid_server( - port_, _api2wire_String(server)), - parseSuccessData: _wire2api_String, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "main_test_if_valid_server", - argNames: ["server"], - ), - argValues: [server], - hint: hint, - )); - - Future mainSetSocks( - {required String proxy, - required String username, - required String password, - dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_main_set_socks( - port_, - _api2wire_String(proxy), - _api2wire_String(username), - _api2wire_String(password)), - parseSuccessData: _wire2api_unit, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "main_set_socks", - argNames: ["proxy", "username", "password"], - ), - argValues: [proxy, username, password], - hint: hint, - )); - - Future> mainGetSocks({dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_main_get_socks(port_), - parseSuccessData: _wire2api_StringList, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "main_get_socks", - argNames: [], - ), - argValues: [], - hint: hint, - )); - - Future mainGetAppName({dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_main_get_app_name(port_), - parseSuccessData: _wire2api_String, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "main_get_app_name", - argNames: [], - ), - argValues: [], - hint: hint, - )); - - Future mainGetLicense({dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_main_get_license(port_), - parseSuccessData: _wire2api_String, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "main_get_license", - argNames: [], - ), - argValues: [], - hint: hint, - )); - - Future mainGetVersion({dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_main_get_version(port_), - parseSuccessData: _wire2api_String, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "main_get_version", - argNames: [], - ), - argValues: [], - hint: hint, - )); - - Future> mainGetFav({dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_main_get_fav(port_), - parseSuccessData: _wire2api_StringList, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "main_get_fav", - argNames: [], - ), - argValues: [], - hint: hint, - )); - - Future mainStoreFav({required List favs, dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => - inner.wire_main_store_fav(port_, _api2wire_StringList(favs)), - parseSuccessData: _wire2api_unit, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "main_store_fav", - argNames: ["favs"], - ), - argValues: [favs], - hint: hint, - )); - - Future mainGetPeer({required String id, dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => - inner.wire_main_get_peer(port_, _api2wire_String(id)), - parseSuccessData: _wire2api_String, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "main_get_peer", - argNames: ["id"], - ), - argValues: [id], - hint: hint, - )); - - Future mainGetLanPeers({dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_main_get_lan_peers(port_), - parseSuccessData: _wire2api_String, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "main_get_lan_peers", - argNames: [], - ), - argValues: [], - hint: hint, - )); - - Future mainGetConnectStatus({dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_main_get_connect_status(port_), - parseSuccessData: _wire2api_String, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "main_get_connect_status", - argNames: [], - ), - argValues: [], - hint: hint, - )); - - Future mainCheckConnectStatus({dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_main_check_connect_status(port_), - parseSuccessData: _wire2api_unit, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "main_check_connect_status", - argNames: [], - ), - argValues: [], - hint: hint, - )); - - Future mainIsUsingPublicServer({dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_main_is_using_public_server(port_), - parseSuccessData: _wire2api_bool, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "main_is_using_public_server", - argNames: [], - ), - argValues: [], - hint: hint, - )); - - Future mainDiscover({dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_main_discover(port_), - parseSuccessData: _wire2api_unit, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "main_discover", - argNames: [], - ), - argValues: [], - hint: hint, - )); - - Future mainHasRendezvousService({dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_main_has_rendezvous_service(port_), - parseSuccessData: _wire2api_bool, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "main_has_rendezvous_service", - argNames: [], - ), - argValues: [], - hint: hint, - )); - - Future mainGetApiServer({dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_main_get_api_server(port_), - parseSuccessData: _wire2api_String, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "main_get_api_server", - argNames: [], - ), - argValues: [], - hint: hint, - )); - - Future mainPostRequest( - {required String url, - required String body, - required String header, - dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_main_post_request( - port_, - _api2wire_String(url), - _api2wire_String(body), - _api2wire_String(header)), - parseSuccessData: _wire2api_unit, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "main_post_request", - argNames: ["url", "body", "header"], - ), - argValues: [url, body, header], - hint: hint, - )); - - Future mainGetLocalOption({required String key, dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => - inner.wire_main_get_local_option(port_, _api2wire_String(key)), - parseSuccessData: _wire2api_String, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "main_get_local_option", - argNames: ["key"], - ), - argValues: [key], - hint: hint, - )); - - Future mainSetLocalOption( - {required String key, required String value, dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_main_set_local_option( - port_, _api2wire_String(key), _api2wire_String(value)), - parseSuccessData: _wire2api_unit, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "main_set_local_option", - argNames: ["key", "value"], - ), - argValues: [key, value], - hint: hint, - )); - - Future mainGetMyId({dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_main_get_my_id(port_), - parseSuccessData: _wire2api_String, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "main_get_my_id", - argNames: [], - ), - argValues: [], - hint: hint, - )); - - Future mainGetUuid({dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_main_get_uuid(port_), - parseSuccessData: _wire2api_String, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "main_get_uuid", - argNames: [], - ), - argValues: [], - hint: hint, - )); - - Future mainGetPeerOption( - {required String id, required String key, dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_main_get_peer_option( - port_, _api2wire_String(id), _api2wire_String(key)), - parseSuccessData: _wire2api_String, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "main_get_peer_option", - argNames: ["id", "key"], - ), - argValues: [id, key], - hint: hint, - )); - - Future mainSetPeerOption( - {required String id, - required String key, - required String value, - dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_main_set_peer_option( - port_, - _api2wire_String(id), - _api2wire_String(key), - _api2wire_String(value)), - parseSuccessData: _wire2api_unit, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "main_set_peer_option", - argNames: ["id", "key", "value"], - ), - argValues: [id, key, value], - hint: hint, - )); - - Future mainForgetPassword({required String id, dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => - inner.wire_main_forget_password(port_, _api2wire_String(id)), - parseSuccessData: _wire2api_unit, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "main_forget_password", - argNames: ["id"], - ), - argValues: [id], - hint: hint, - )); - - Future mainGetRecentPeers({dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_main_get_recent_peers(port_), - parseSuccessData: _wire2api_String, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "main_get_recent_peers", - argNames: [], - ), - argValues: [], - hint: hint, - )); - - Future mainLoadRecentPeers({dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_main_load_recent_peers(port_), - parseSuccessData: _wire2api_unit, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "main_load_recent_peers", - argNames: [], - ), - argValues: [], - hint: hint, - )); - - Future mainLoadFavPeers({dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_main_load_fav_peers(port_), - parseSuccessData: _wire2api_unit, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "main_load_fav_peers", - argNames: [], - ), - argValues: [], - hint: hint, - )); - - Future mainLoadLanPeers({dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_main_load_lan_peers(port_), - parseSuccessData: _wire2api_unit, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "main_load_lan_peers", - argNames: [], - ), - argValues: [], - hint: hint, - )); - - Future sessionAddPortForward( - {required String id, - required int localPort, - required String remoteHost, - required int remotePort, - dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_session_add_port_forward( - port_, - _api2wire_String(id), - _api2wire_i32(localPort), - _api2wire_String(remoteHost), - _api2wire_i32(remotePort)), - parseSuccessData: _wire2api_unit, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "session_add_port_forward", - argNames: ["id", "localPort", "remoteHost", "remotePort"], - ), - argValues: [id, localPort, remoteHost, remotePort], - hint: hint, - )); - - Future sessionRemovePortForward( - {required String id, required int localPort, dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_session_remove_port_forward( - port_, _api2wire_String(id), _api2wire_i32(localPort)), - parseSuccessData: _wire2api_unit, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "session_remove_port_forward", - argNames: ["id", "localPort"], - ), - argValues: [id, localPort], - hint: hint, - )); - - Future mainGetLastRemoteId({dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_main_get_last_remote_id(port_), - parseSuccessData: _wire2api_String, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "main_get_last_remote_id", - argNames: [], - ), - argValues: [], - hint: hint, - )); - - Future mainGetSoftwareUpdateUrl({dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_main_get_software_update_url(port_), - parseSuccessData: _wire2api_String, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "main_get_software_update_url", - argNames: [], - ), - argValues: [], - hint: hint, - )); - - Future mainGetHomeDir({dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_main_get_home_dir(port_), - parseSuccessData: _wire2api_String, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "main_get_home_dir", - argNames: [], - ), - argValues: [], - hint: hint, - )); - - Future mainGetLangs({dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_main_get_langs(port_), - parseSuccessData: _wire2api_String, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "main_get_langs", - argNames: [], - ), - argValues: [], - hint: hint, - )); - - Future mainGetTemporaryPassword({dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_main_get_temporary_password(port_), - parseSuccessData: _wire2api_String, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "main_get_temporary_password", - argNames: [], - ), - argValues: [], - hint: hint, - )); - - Future mainGetPermanentPassword({dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_main_get_permanent_password(port_), - parseSuccessData: _wire2api_String, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "main_get_permanent_password", - argNames: [], - ), - argValues: [], - hint: hint, - )); - - Future mainGetOnlineStatue({dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_main_get_online_statue(port_), - parseSuccessData: _wire2api_i64, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "main_get_online_statue", - argNames: [], - ), - argValues: [], - hint: hint, - )); - - Future mainGetClientsState({dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_main_get_clients_state(port_), - parseSuccessData: _wire2api_String, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "main_get_clients_state", - argNames: [], - ), - argValues: [], - hint: hint, - )); - - Future mainCheckClientsLength({required int length, dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_main_check_clients_length( - port_, _api2wire_usize(length)), - parseSuccessData: _wire2api_opt_String, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "main_check_clients_length", - argNames: ["length"], - ), - argValues: [length], - hint: hint, - )); - - Future mainInit({required String appDir, dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => - inner.wire_main_init(port_, _api2wire_String(appDir)), - parseSuccessData: _wire2api_unit, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "main_init", - argNames: ["appDir"], - ), - argValues: [appDir], - hint: hint, - )); - - Future mainDeviceId({required String id, dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => - inner.wire_main_device_id(port_, _api2wire_String(id)), - parseSuccessData: _wire2api_unit, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "main_device_id", - argNames: ["id"], - ), - argValues: [id], - hint: hint, - )); - - Future mainDeviceName({required String name, dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => - inner.wire_main_device_name(port_, _api2wire_String(name)), - parseSuccessData: _wire2api_unit, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "main_device_name", - argNames: ["name"], - ), - argValues: [name], - hint: hint, - )); - - Future mainRemovePeer({required String id, dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => - inner.wire_main_remove_peer(port_, _api2wire_String(id)), - parseSuccessData: _wire2api_unit, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "main_remove_peer", - argNames: ["id"], - ), - argValues: [id], - hint: hint, - )); - - Future mainHasHwcodec({dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_main_has_hwcodec(port_), - parseSuccessData: _wire2api_bool, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "main_has_hwcodec", - argNames: [], - ), - argValues: [], - hint: hint, - )); - - Future sessionSendMouse( - {required String id, required String msg, dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_session_send_mouse( - port_, _api2wire_String(id), _api2wire_String(msg)), - parseSuccessData: _wire2api_unit, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "session_send_mouse", - argNames: ["id", "msg"], - ), - argValues: [id, msg], - hint: hint, - )); - - Future sessionRestartRemoteDevice({required String id, dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_session_restart_remote_device( - port_, _api2wire_String(id)), - parseSuccessData: _wire2api_unit, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "session_restart_remote_device", - argNames: ["id"], - ), - argValues: [id], - hint: hint, - )); - - Future mainSetHomeDir({required String home, dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => - inner.wire_main_set_home_dir(port_, _api2wire_String(home)), - parseSuccessData: _wire2api_unit, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "main_set_home_dir", - argNames: ["home"], - ), - argValues: [home], - hint: hint, - )); - - Future mainStopService({dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_main_stop_service(port_), - parseSuccessData: _wire2api_unit, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "main_stop_service", - argNames: [], - ), - argValues: [], - hint: hint, - )); - - Future mainStartService({dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_main_start_service(port_), - parseSuccessData: _wire2api_unit, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "main_start_service", - argNames: [], - ), - argValues: [], - hint: hint, - )); - - Future mainUpdateTemporaryPassword({dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_main_update_temporary_password(port_), - parseSuccessData: _wire2api_unit, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "main_update_temporary_password", - argNames: [], - ), - argValues: [], - hint: hint, - )); - - Future mainSetPermanentPassword( - {required String password, dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_main_set_permanent_password( - port_, _api2wire_String(password)), - parseSuccessData: _wire2api_unit, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "main_set_permanent_password", - argNames: ["password"], - ), - argValues: [password], - hint: hint, - )); - - Future mainCheckSuperUserPermission({dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_main_check_super_user_permission(port_), - parseSuccessData: _wire2api_bool, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "main_check_super_user_permission", - argNames: [], - ), - argValues: [], - hint: hint, - )); - - Future mainCheckMouseTime({dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_main_check_mouse_time(port_), - parseSuccessData: _wire2api_unit, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "main_check_mouse_time", - argNames: [], - ), - argValues: [], - hint: hint, - )); - - Future mainGetMouseTime({dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_main_get_mouse_time(port_), - parseSuccessData: _wire2api_f64, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "main_get_mouse_time", - argNames: [], - ), - argValues: [], - hint: hint, - )); - - Future cmSendChat( - {required int connId, required String msg, dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_cm_send_chat( - port_, _api2wire_i32(connId), _api2wire_String(msg)), - parseSuccessData: _wire2api_unit, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "cm_send_chat", - argNames: ["connId", "msg"], - ), - argValues: [connId, msg], - hint: hint, - )); - - Future cmLoginRes( - {required int connId, required bool res, dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => - inner.wire_cm_login_res(port_, _api2wire_i32(connId), res), - parseSuccessData: _wire2api_unit, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "cm_login_res", - argNames: ["connId", "res"], - ), - argValues: [connId, res], - hint: hint, - )); - - Future cmCloseConnection({required int connId, dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => - inner.wire_cm_close_connection(port_, _api2wire_i32(connId)), - parseSuccessData: _wire2api_unit, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "cm_close_connection", - argNames: ["connId"], - ), - argValues: [connId], - hint: hint, - )); - - Future cmCheckClickTime({required int connId, dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => - inner.wire_cm_check_click_time(port_, _api2wire_i32(connId)), - parseSuccessData: _wire2api_unit, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "cm_check_click_time", - argNames: ["connId"], - ), - argValues: [connId], - hint: hint, - )); - - Future cmGetClickTime({dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_cm_get_click_time(port_), - parseSuccessData: _wire2api_f64, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "cm_get_click_time", - argNames: [], - ), - argValues: [], - hint: hint, - )); - - Future cmSwitchPermission( - {required int connId, - required String name, - required bool enabled, - dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_cm_switch_permission( - port_, _api2wire_i32(connId), _api2wire_String(name), enabled), - parseSuccessData: _wire2api_unit, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "cm_switch_permission", - argNames: ["connId", "name", "enabled"], - ), - argValues: [connId, name, enabled], - hint: hint, - )); - - Future mainGetIcon({dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => inner.wire_main_get_icon(port_), - parseSuccessData: _wire2api_String, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "main_get_icon", - argNames: [], - ), - argValues: [], - hint: hint, - )); - - Future queryOnlines({required List ids, dynamic hint}) => - executeNormal(FlutterRustBridgeTask( - callFfi: (port_) => - inner.wire_query_onlines(port_, _api2wire_StringList(ids)), - parseSuccessData: _wire2api_unit, - constMeta: const FlutterRustBridgeTaskConstMeta( - debugName: "query_onlines", - argNames: ["ids"], - ), - argValues: [ids], - hint: hint, - )); - - // Section: api2wire - ffi.Pointer _api2wire_String(String raw) { - return _api2wire_uint_8_list(utf8.encoder.convert(raw)); - } - - ffi.Pointer _api2wire_StringList(List raw) { - final ans = inner.new_StringList(raw.length); - for (var i = 0; i < raw.length; i++) { - ans.ref.ptr[i] = _api2wire_String(raw[i]); - } - return ans; - } - - int _api2wire_bool(bool raw) { - return raw ? 1 : 0; - } - - int _api2wire_i32(int raw) { - return raw; - } - - int _api2wire_u8(int raw) { - return raw; - } - - ffi.Pointer _api2wire_uint_8_list(Uint8List raw) { - final ans = inner.new_uint_8_list(raw.length); - ans.ref.ptr.asTypedList(raw.length).setAll(0, raw); - return ans; - } - - int _api2wire_usize(int raw) { - return raw; - } - - // Section: api_fill_to_wire - -} - -// Section: wire2api -String _wire2api_String(dynamic raw) { - return raw as String; -} - -List _wire2api_StringList(dynamic raw) { - return (raw as List).cast(); -} - -String _wire2api_SyncReturnString(dynamic raw) { - return raw as String; -} - -Uint8List _wire2api_ZeroCopyBuffer_Uint8List(dynamic raw) { - return raw as Uint8List; -} - -bool _wire2api_bool(dynamic raw) { - return raw as bool; -} - -bool _wire2api_box_autoadd_bool(dynamic raw) { - return raw as bool; -} - -EventToUI _wire2api_event_to_ui(dynamic raw) { - switch (raw[0]) { - case 0: - return Event( - _wire2api_String(raw[1]), - ); - case 1: - return Rgba( - _wire2api_ZeroCopyBuffer_Uint8List(raw[1]), - ); - default: - throw Exception("unreachable"); - } -} - -double _wire2api_f64(dynamic raw) { - return raw as double; -} - -int _wire2api_i32(dynamic raw) { - return raw as int; -} - -int _wire2api_i64(dynamic raw) { - return raw as int; -} - -Int32List _wire2api_int_32_list(dynamic raw) { - return raw as Int32List; -} - -String? _wire2api_opt_String(dynamic raw) { - return raw == null ? null : _wire2api_String(raw); -} - -bool? _wire2api_opt_box_autoadd_bool(dynamic raw) { - return raw == null ? null : _wire2api_box_autoadd_bool(raw); -} - -Int32List? _wire2api_opt_int_32_list(dynamic raw) { - return raw == null ? null : _wire2api_int_32_list(raw); -} - -int _wire2api_u8(dynamic raw) { - return raw as int; -} - -Uint8List _wire2api_uint_8_list(dynamic raw) { - return raw as Uint8List; -} - -void _wire2api_unit(dynamic raw) { - return; -} - -// ignore_for_file: camel_case_types, non_constant_identifier_names, avoid_positional_boolean_parameters, annotate_overrides, constant_identifier_names - -// AUTO GENERATED FILE, DO NOT EDIT. -// -// Generated by `package:ffigen`. - -/// generated by flutter_rust_bridge -class RustdeskWire implements FlutterRustBridgeWireBase { - /// Holds the symbol lookup function. - final ffi.Pointer Function(String symbolName) - _lookup; - - /// The symbols are looked up in [dynamicLibrary]. - RustdeskWire(ffi.DynamicLibrary dynamicLibrary) - : _lookup = dynamicLibrary.lookup; - - /// The symbols are looked up with [lookup]. - RustdeskWire.fromLookup( - ffi.Pointer Function(String symbolName) - lookup) - : _lookup = lookup; - - void wire_rustdesk_core_main( - int port_, - ) { - return _wire_rustdesk_core_main( - port_, - ); - } - - late final _wire_rustdesk_core_mainPtr = - _lookup>( - 'wire_rustdesk_core_main'); - late final _wire_rustdesk_core_main = - _wire_rustdesk_core_mainPtr.asFunction(); - - void wire_start_global_event_stream( - int port_, - ffi.Pointer app_type, - ) { - return _wire_start_global_event_stream( - port_, - app_type, - ); - } - - late final _wire_start_global_event_streamPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Int64, ffi.Pointer)>>( - 'wire_start_global_event_stream'); - late final _wire_start_global_event_stream = - _wire_start_global_event_streamPtr - .asFunction)>(); - - void wire_stop_global_event_stream( - int port_, - ffi.Pointer app_type, - ) { - return _wire_stop_global_event_stream( - port_, - app_type, - ); - } - - late final _wire_stop_global_event_streamPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Int64, - ffi.Pointer)>>('wire_stop_global_event_stream'); - late final _wire_stop_global_event_stream = _wire_stop_global_event_streamPtr - .asFunction)>(); - - void wire_host_stop_system_key_propagate( - int port_, - ffi.Pointer stopped, - ) { - return _wire_host_stop_system_key_propagate( - port_, - stopped, - ); - } - - late final _wire_host_stop_system_key_propagatePtr = _lookup< - ffi.NativeFunction)>>( - 'wire_host_stop_system_key_propagate'); - late final _wire_host_stop_system_key_propagate = - _wire_host_stop_system_key_propagatePtr - .asFunction)>(); - - WireSyncReturnStruct wire_session_add_sync( - ffi.Pointer id, - ffi.Pointer is_file_transfer, - ffi.Pointer is_port_forward, - ) { - return _wire_session_add_sync( - id, - is_file_transfer, - is_port_forward, - ); - } - - late final _wire_session_add_syncPtr = _lookup< - ffi.NativeFunction< - WireSyncReturnStruct Function(ffi.Pointer, - ffi.Pointer, ffi.Pointer)>>('wire_session_add_sync'); - late final _wire_session_add_sync = _wire_session_add_syncPtr.asFunction< - WireSyncReturnStruct Function(ffi.Pointer, - ffi.Pointer, ffi.Pointer)>(); - - void wire_session_start( - int port_, - ffi.Pointer id, - ) { - return _wire_session_start( - port_, - id, - ); - } - - late final _wire_session_startPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function( - ffi.Int64, ffi.Pointer)>>('wire_session_start'); - late final _wire_session_start = _wire_session_startPtr - .asFunction)>(); - - void wire_session_get_remember( - int port_, - ffi.Pointer id, - ) { - return _wire_session_get_remember( - port_, - id, - ); - } - - late final _wire_session_get_rememberPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Int64, - ffi.Pointer)>>('wire_session_get_remember'); - late final _wire_session_get_remember = _wire_session_get_rememberPtr - .asFunction)>(); - - void wire_session_get_toggle_option( - int port_, - ffi.Pointer id, - ffi.Pointer arg, - ) { - return _wire_session_get_toggle_option( - port_, - id, - arg, - ); - } - - late final _wire_session_get_toggle_optionPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Int64, ffi.Pointer, - ffi.Pointer)>>( - 'wire_session_get_toggle_option'); - late final _wire_session_get_toggle_option = - _wire_session_get_toggle_optionPtr.asFunction< - void Function(int, ffi.Pointer, - ffi.Pointer)>(); - - WireSyncReturnStruct wire_session_get_toggle_option_sync( - ffi.Pointer id, - ffi.Pointer arg, - ) { - return _wire_session_get_toggle_option_sync( - id, - arg, - ); - } - - late final _wire_session_get_toggle_option_syncPtr = _lookup< - ffi.NativeFunction< - WireSyncReturnStruct Function(ffi.Pointer, - ffi.Pointer)>>( - 'wire_session_get_toggle_option_sync'); - late final _wire_session_get_toggle_option_sync = - _wire_session_get_toggle_option_syncPtr.asFunction< - WireSyncReturnStruct Function( - ffi.Pointer, ffi.Pointer)>(); - - void wire_session_get_option( - int port_, - ffi.Pointer id, - ffi.Pointer arg, - ) { - return _wire_session_get_option( - port_, - id, - arg, - ); - } - - late final _wire_session_get_optionPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Int64, ffi.Pointer, - ffi.Pointer)>>('wire_session_get_option'); - late final _wire_session_get_option = _wire_session_get_optionPtr.asFunction< - void Function( - int, ffi.Pointer, ffi.Pointer)>(); - - void wire_session_login( - int port_, - ffi.Pointer id, - ffi.Pointer password, - ffi.Pointer remember, - ) { - return _wire_session_login( - port_, - id, - password, - remember, - ); - } - - late final _wire_session_loginPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function( - ffi.Int64, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer)>>('wire_session_login'); - late final _wire_session_login = _wire_session_loginPtr.asFunction< - void Function(int, ffi.Pointer, - ffi.Pointer, ffi.Pointer)>(); - - void wire_session_close( - int port_, - ffi.Pointer id, - ) { - return _wire_session_close( - port_, - id, - ); - } - - late final _wire_session_closePtr = _lookup< - ffi.NativeFunction< - ffi.Void Function( - ffi.Int64, ffi.Pointer)>>('wire_session_close'); - late final _wire_session_close = _wire_session_closePtr - .asFunction)>(); - - void wire_session_refresh( - int port_, - ffi.Pointer id, - ) { - return _wire_session_refresh( - port_, - id, - ); - } - - late final _wire_session_refreshPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Int64, - ffi.Pointer)>>('wire_session_refresh'); - late final _wire_session_refresh = _wire_session_refreshPtr - .asFunction)>(); - - void wire_session_reconnect( - int port_, - ffi.Pointer id, - ) { - return _wire_session_reconnect( - port_, - id, - ); - } - - late final _wire_session_reconnectPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Int64, - ffi.Pointer)>>('wire_session_reconnect'); - late final _wire_session_reconnect = _wire_session_reconnectPtr - .asFunction)>(); - - void wire_session_toggle_option( - int port_, - ffi.Pointer id, - ffi.Pointer value, - ) { - return _wire_session_toggle_option( - port_, - id, - value, - ); - } - - late final _wire_session_toggle_optionPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Int64, ffi.Pointer, - ffi.Pointer)>>('wire_session_toggle_option'); - late final _wire_session_toggle_option = - _wire_session_toggle_optionPtr.asFunction< - void Function(int, ffi.Pointer, - ffi.Pointer)>(); - - void wire_session_get_image_quality( - int port_, - ffi.Pointer id, - ) { - return _wire_session_get_image_quality( - port_, - id, - ); - } - - late final _wire_session_get_image_qualityPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Int64, ffi.Pointer)>>( - 'wire_session_get_image_quality'); - late final _wire_session_get_image_quality = - _wire_session_get_image_qualityPtr - .asFunction)>(); - - void wire_session_set_image_quality( - int port_, - ffi.Pointer id, - ffi.Pointer value, - ) { - return _wire_session_set_image_quality( - port_, - id, - value, - ); - } - - late final _wire_session_set_image_qualityPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Int64, ffi.Pointer, - ffi.Pointer)>>( - 'wire_session_set_image_quality'); - late final _wire_session_set_image_quality = - _wire_session_set_image_qualityPtr.asFunction< - void Function(int, ffi.Pointer, - ffi.Pointer)>(); - - void wire_session_get_custom_image_quality( - int port_, - ffi.Pointer id, - ) { - return _wire_session_get_custom_image_quality( - port_, - id, - ); - } - - late final _wire_session_get_custom_image_qualityPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Int64, ffi.Pointer)>>( - 'wire_session_get_custom_image_quality'); - late final _wire_session_get_custom_image_quality = - _wire_session_get_custom_image_qualityPtr - .asFunction)>(); - - void wire_session_set_custom_image_quality( - int port_, - ffi.Pointer id, - int value, - ) { - return _wire_session_set_custom_image_quality( - port_, - id, - value, - ); - } - - late final _wire_session_set_custom_image_qualityPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Int64, ffi.Pointer, - ffi.Int32)>>('wire_session_set_custom_image_quality'); - late final _wire_session_set_custom_image_quality = - _wire_session_set_custom_image_qualityPtr - .asFunction, int)>(); - - void wire_session_lock_screen( - int port_, - ffi.Pointer id, - ) { - return _wire_session_lock_screen( - port_, - id, - ); - } - - late final _wire_session_lock_screenPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Int64, - ffi.Pointer)>>('wire_session_lock_screen'); - late final _wire_session_lock_screen = _wire_session_lock_screenPtr - .asFunction)>(); - - void wire_session_ctrl_alt_del( - int port_, - ffi.Pointer id, - ) { - return _wire_session_ctrl_alt_del( - port_, - id, - ); - } - - late final _wire_session_ctrl_alt_delPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Int64, - ffi.Pointer)>>('wire_session_ctrl_alt_del'); - late final _wire_session_ctrl_alt_del = _wire_session_ctrl_alt_delPtr - .asFunction)>(); - - void wire_session_switch_display( - int port_, - ffi.Pointer id, - int value, - ) { - return _wire_session_switch_display( - port_, - id, - value, - ); - } - - late final _wire_session_switch_displayPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Int64, ffi.Pointer, - ffi.Int32)>>('wire_session_switch_display'); - late final _wire_session_switch_display = _wire_session_switch_displayPtr - .asFunction, int)>(); - - void wire_session_input_key( - int port_, - ffi.Pointer id, - ffi.Pointer name, - ffi.Pointer down, - ffi.Pointer press, - ffi.Pointer alt, - ffi.Pointer ctrl, - ffi.Pointer shift, - ffi.Pointer command, - ) { - return _wire_session_input_key( - port_, - id, - name, - down, - press, - alt, - ctrl, - shift, - command, - ); - } - - late final _wire_session_input_keyPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function( - ffi.Int64, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer)>>('wire_session_input_key'); - late final _wire_session_input_key = _wire_session_input_keyPtr.asFunction< - void Function( - int, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer)>(); - - void wire_session_input_string( - int port_, - ffi.Pointer id, - ffi.Pointer value, - ) { - return _wire_session_input_string( - port_, - id, - value, - ); - } - - late final _wire_session_input_stringPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Int64, ffi.Pointer, - ffi.Pointer)>>('wire_session_input_string'); - late final _wire_session_input_string = - _wire_session_input_stringPtr.asFunction< - void Function(int, ffi.Pointer, - ffi.Pointer)>(); - - void wire_session_send_chat( - int port_, - ffi.Pointer id, - ffi.Pointer text, - ) { - return _wire_session_send_chat( - port_, - id, - text, - ); - } - - late final _wire_session_send_chatPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Int64, ffi.Pointer, - ffi.Pointer)>>('wire_session_send_chat'); - late final _wire_session_send_chat = _wire_session_send_chatPtr.asFunction< - void Function( - int, ffi.Pointer, ffi.Pointer)>(); - - void wire_session_peer_option( - int port_, - ffi.Pointer id, - ffi.Pointer name, - ffi.Pointer value, - ) { - return _wire_session_peer_option( - port_, - id, - name, - value, - ); - } - - late final _wire_session_peer_optionPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function( - ffi.Int64, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer)>>('wire_session_peer_option'); - late final _wire_session_peer_option = - _wire_session_peer_optionPtr.asFunction< - void Function(int, ffi.Pointer, - ffi.Pointer, ffi.Pointer)>(); - - void wire_session_get_peer_option( - int port_, - ffi.Pointer id, - ffi.Pointer name, - ) { - return _wire_session_get_peer_option( - port_, - id, - name, - ); - } - - late final _wire_session_get_peer_optionPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Int64, ffi.Pointer, - ffi.Pointer)>>('wire_session_get_peer_option'); - late final _wire_session_get_peer_option = - _wire_session_get_peer_optionPtr.asFunction< - void Function(int, ffi.Pointer, - ffi.Pointer)>(); - - void wire_session_input_os_password( - int port_, - ffi.Pointer id, - ffi.Pointer value, - ) { - return _wire_session_input_os_password( - port_, - id, - value, - ); - } - - late final _wire_session_input_os_passwordPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Int64, ffi.Pointer, - ffi.Pointer)>>( - 'wire_session_input_os_password'); - late final _wire_session_input_os_password = - _wire_session_input_os_passwordPtr.asFunction< - void Function(int, ffi.Pointer, - ffi.Pointer)>(); - - void wire_session_read_remote_dir( - int port_, - ffi.Pointer id, - ffi.Pointer path, - ffi.Pointer include_hidden, - ) { - return _wire_session_read_remote_dir( - port_, - id, - path, - include_hidden, - ); - } - - late final _wire_session_read_remote_dirPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function( - ffi.Int64, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer)>>('wire_session_read_remote_dir'); - late final _wire_session_read_remote_dir = - _wire_session_read_remote_dirPtr.asFunction< - void Function(int, ffi.Pointer, - ffi.Pointer, ffi.Pointer)>(); - - void wire_session_send_files( - int port_, - ffi.Pointer id, - int act_id, - ffi.Pointer path, - ffi.Pointer to, - int file_num, - ffi.Pointer include_hidden, - ffi.Pointer is_remote, - ) { - return _wire_session_send_files( - port_, - id, - act_id, - path, - to, - file_num, - include_hidden, - is_remote, - ); - } - - late final _wire_session_send_filesPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function( - ffi.Int64, - ffi.Pointer, - ffi.Int32, - ffi.Pointer, - ffi.Pointer, - ffi.Int32, - ffi.Pointer, - ffi.Pointer)>>('wire_session_send_files'); - late final _wire_session_send_files = _wire_session_send_filesPtr.asFunction< - void Function( - int, - ffi.Pointer, - int, - ffi.Pointer, - ffi.Pointer, - int, - ffi.Pointer, - ffi.Pointer)>(); - - void wire_session_set_confirm_override_file( - int port_, - ffi.Pointer id, - int act_id, - int file_num, - ffi.Pointer need_override, - ffi.Pointer remember, - ffi.Pointer is_upload, - ) { - return _wire_session_set_confirm_override_file( - port_, - id, - act_id, - file_num, - need_override, - remember, - is_upload, - ); - } - - late final _wire_session_set_confirm_override_filePtr = _lookup< - ffi.NativeFunction< - ffi.Void Function( - ffi.Int64, - ffi.Pointer, - ffi.Int32, - ffi.Int32, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer)>>('wire_session_set_confirm_override_file'); - late final _wire_session_set_confirm_override_file = - _wire_session_set_confirm_override_filePtr.asFunction< - void Function(int, ffi.Pointer, int, int, - ffi.Pointer, ffi.Pointer, ffi.Pointer)>(); - - void wire_session_remove_file( - int port_, - ffi.Pointer id, - int act_id, - ffi.Pointer path, - int file_num, - ffi.Pointer is_remote, - ) { - return _wire_session_remove_file( - port_, - id, - act_id, - path, - file_num, - is_remote, - ); - } - - late final _wire_session_remove_filePtr = _lookup< - ffi.NativeFunction< - ffi.Void Function( - ffi.Int64, - ffi.Pointer, - ffi.Int32, - ffi.Pointer, - ffi.Int32, - ffi.Pointer)>>('wire_session_remove_file'); - late final _wire_session_remove_file = - _wire_session_remove_filePtr.asFunction< - void Function(int, ffi.Pointer, int, - ffi.Pointer, int, ffi.Pointer)>(); - - void wire_session_read_dir_recursive( - int port_, - ffi.Pointer id, - int act_id, - ffi.Pointer path, - ffi.Pointer is_remote, - ffi.Pointer show_hidden, - ) { - return _wire_session_read_dir_recursive( - port_, - id, - act_id, - path, - is_remote, - show_hidden, - ); - } - - late final _wire_session_read_dir_recursivePtr = _lookup< - ffi.NativeFunction< - ffi.Void Function( - ffi.Int64, - ffi.Pointer, - ffi.Int32, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer)>>('wire_session_read_dir_recursive'); - late final _wire_session_read_dir_recursive = - _wire_session_read_dir_recursivePtr.asFunction< - void Function( - int, - ffi.Pointer, - int, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer)>(); - - void wire_session_remove_all_empty_dirs( - int port_, - ffi.Pointer id, - int act_id, - ffi.Pointer path, - ffi.Pointer is_remote, - ) { - return _wire_session_remove_all_empty_dirs( - port_, - id, - act_id, - path, - is_remote, - ); - } - - late final _wire_session_remove_all_empty_dirsPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function( - ffi.Int64, - ffi.Pointer, - ffi.Int32, - ffi.Pointer, - ffi.Pointer)>>('wire_session_remove_all_empty_dirs'); - late final _wire_session_remove_all_empty_dirs = - _wire_session_remove_all_empty_dirsPtr.asFunction< - void Function(int, ffi.Pointer, int, - ffi.Pointer, ffi.Pointer)>(); - - void wire_session_cancel_job( - int port_, - ffi.Pointer id, - int act_id, - ) { - return _wire_session_cancel_job( - port_, - id, - act_id, - ); - } - - late final _wire_session_cancel_jobPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Int64, ffi.Pointer, - ffi.Int32)>>('wire_session_cancel_job'); - late final _wire_session_cancel_job = _wire_session_cancel_jobPtr - .asFunction, int)>(); - - void wire_session_create_dir( - int port_, - ffi.Pointer id, - int act_id, - ffi.Pointer path, - ffi.Pointer is_remote, - ) { - return _wire_session_create_dir( - port_, - id, - act_id, - path, - is_remote, - ); - } - - late final _wire_session_create_dirPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function( - ffi.Int64, - ffi.Pointer, - ffi.Int32, - ffi.Pointer, - ffi.Pointer)>>('wire_session_create_dir'); - late final _wire_session_create_dir = _wire_session_create_dirPtr.asFunction< - void Function(int, ffi.Pointer, int, - ffi.Pointer, ffi.Pointer)>(); - - void wire_session_read_local_dir_sync( - int port_, - ffi.Pointer id, - ffi.Pointer path, - ffi.Pointer show_hidden, - ) { - return _wire_session_read_local_dir_sync( - port_, - id, - path, - show_hidden, - ); - } - - late final _wire_session_read_local_dir_syncPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function( - ffi.Int64, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer)>>('wire_session_read_local_dir_sync'); - late final _wire_session_read_local_dir_sync = - _wire_session_read_local_dir_syncPtr.asFunction< - void Function(int, ffi.Pointer, - ffi.Pointer, ffi.Pointer)>(); - - void wire_session_get_platform( - int port_, - ffi.Pointer id, - ffi.Pointer is_remote, - ) { - return _wire_session_get_platform( - port_, - id, - is_remote, - ); - } - - late final _wire_session_get_platformPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Int64, ffi.Pointer, - ffi.Pointer)>>('wire_session_get_platform'); - late final _wire_session_get_platform = - _wire_session_get_platformPtr.asFunction< - void Function( - int, ffi.Pointer, ffi.Pointer)>(); - - void wire_session_load_last_transfer_jobs( - int port_, - ffi.Pointer id, - ) { - return _wire_session_load_last_transfer_jobs( - port_, - id, - ); - } - - late final _wire_session_load_last_transfer_jobsPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Int64, ffi.Pointer)>>( - 'wire_session_load_last_transfer_jobs'); - late final _wire_session_load_last_transfer_jobs = - _wire_session_load_last_transfer_jobsPtr - .asFunction)>(); - - void wire_session_add_job( - int port_, - ffi.Pointer id, - int act_id, - ffi.Pointer path, - ffi.Pointer to, - int file_num, - ffi.Pointer include_hidden, - ffi.Pointer is_remote, - ) { - return _wire_session_add_job( - port_, - id, - act_id, - path, - to, - file_num, - include_hidden, - is_remote, - ); - } - - late final _wire_session_add_jobPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function( - ffi.Int64, - ffi.Pointer, - ffi.Int32, - ffi.Pointer, - ffi.Pointer, - ffi.Int32, - ffi.Pointer, - ffi.Pointer)>>('wire_session_add_job'); - late final _wire_session_add_job = _wire_session_add_jobPtr.asFunction< - void Function( - int, - ffi.Pointer, - int, - ffi.Pointer, - ffi.Pointer, - int, - ffi.Pointer, - ffi.Pointer)>(); - - void wire_session_resume_job( - int port_, - ffi.Pointer id, - int act_id, - ffi.Pointer is_remote, - ) { - return _wire_session_resume_job( - port_, - id, - act_id, - is_remote, - ); - } - - late final _wire_session_resume_jobPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Int64, ffi.Pointer, ffi.Int32, - ffi.Pointer)>>('wire_session_resume_job'); - late final _wire_session_resume_job = _wire_session_resume_jobPtr.asFunction< - void Function( - int, ffi.Pointer, int, ffi.Pointer)>(); - - void wire_main_get_sound_inputs( - int port_, - ) { - return _wire_main_get_sound_inputs( - port_, - ); - } - - late final _wire_main_get_sound_inputsPtr = - _lookup>( - 'wire_main_get_sound_inputs'); - late final _wire_main_get_sound_inputs = - _wire_main_get_sound_inputsPtr.asFunction(); - - void wire_main_change_id( - int port_, - ffi.Pointer new_id, - ) { - return _wire_main_change_id( - port_, - new_id, - ); - } - - late final _wire_main_change_idPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Int64, - ffi.Pointer)>>('wire_main_change_id'); - late final _wire_main_change_id = _wire_main_change_idPtr - .asFunction)>(); - - void wire_main_get_async_status( - int port_, - ) { - return _wire_main_get_async_status( - port_, - ); - } - - late final _wire_main_get_async_statusPtr = - _lookup>( - 'wire_main_get_async_status'); - late final _wire_main_get_async_status = - _wire_main_get_async_statusPtr.asFunction(); - - void wire_main_get_option( - int port_, - ffi.Pointer key, - ) { - return _wire_main_get_option( - port_, - key, - ); - } - - late final _wire_main_get_optionPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Int64, - ffi.Pointer)>>('wire_main_get_option'); - late final _wire_main_get_option = _wire_main_get_optionPtr - .asFunction)>(); - - void wire_main_set_option( - int port_, - ffi.Pointer key, - ffi.Pointer value, - ) { - return _wire_main_set_option( - port_, - key, - value, - ); - } - - late final _wire_main_set_optionPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Int64, ffi.Pointer, - ffi.Pointer)>>('wire_main_set_option'); - late final _wire_main_set_option = _wire_main_set_optionPtr.asFunction< - void Function( - int, ffi.Pointer, ffi.Pointer)>(); - - void wire_main_get_options( - int port_, - ) { - return _wire_main_get_options( - port_, - ); - } - - late final _wire_main_get_optionsPtr = - _lookup>( - 'wire_main_get_options'); - late final _wire_main_get_options = - _wire_main_get_optionsPtr.asFunction(); - - void wire_main_set_options( - int port_, - ffi.Pointer json, - ) { - return _wire_main_set_options( - port_, - json, - ); - } - - late final _wire_main_set_optionsPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Int64, - ffi.Pointer)>>('wire_main_set_options'); - late final _wire_main_set_options = _wire_main_set_optionsPtr - .asFunction)>(); - - void wire_main_test_if_valid_server( - int port_, - ffi.Pointer server, - ) { - return _wire_main_test_if_valid_server( - port_, - server, - ); - } - - late final _wire_main_test_if_valid_serverPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Int64, ffi.Pointer)>>( - 'wire_main_test_if_valid_server'); - late final _wire_main_test_if_valid_server = - _wire_main_test_if_valid_serverPtr - .asFunction)>(); - - void wire_main_set_socks( - int port_, - ffi.Pointer proxy, - ffi.Pointer username, - ffi.Pointer password, - ) { - return _wire_main_set_socks( - port_, - proxy, - username, - password, - ); - } - - late final _wire_main_set_socksPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function( - ffi.Int64, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer)>>('wire_main_set_socks'); - late final _wire_main_set_socks = _wire_main_set_socksPtr.asFunction< - void Function(int, ffi.Pointer, - ffi.Pointer, ffi.Pointer)>(); - - void wire_main_get_socks( - int port_, - ) { - return _wire_main_get_socks( - port_, - ); - } - - late final _wire_main_get_socksPtr = - _lookup>( - 'wire_main_get_socks'); - late final _wire_main_get_socks = - _wire_main_get_socksPtr.asFunction(); - - void wire_main_get_app_name( - int port_, - ) { - return _wire_main_get_app_name( - port_, - ); - } - - late final _wire_main_get_app_namePtr = - _lookup>( - 'wire_main_get_app_name'); - late final _wire_main_get_app_name = - _wire_main_get_app_namePtr.asFunction(); - - void wire_main_get_license( - int port_, - ) { - return _wire_main_get_license( - port_, - ); - } - - late final _wire_main_get_licensePtr = - _lookup>( - 'wire_main_get_license'); - late final _wire_main_get_license = - _wire_main_get_licensePtr.asFunction(); - - void wire_main_get_version( - int port_, - ) { - return _wire_main_get_version( - port_, - ); - } - - late final _wire_main_get_versionPtr = - _lookup>( - 'wire_main_get_version'); - late final _wire_main_get_version = - _wire_main_get_versionPtr.asFunction(); - - void wire_main_get_fav( - int port_, - ) { - return _wire_main_get_fav( - port_, - ); - } - - late final _wire_main_get_favPtr = - _lookup>( - 'wire_main_get_fav'); - late final _wire_main_get_fav = - _wire_main_get_favPtr.asFunction(); - - void wire_main_store_fav( - int port_, - ffi.Pointer favs, - ) { - return _wire_main_store_fav( - port_, - favs, - ); - } - - late final _wire_main_store_favPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function( - ffi.Int64, ffi.Pointer)>>('wire_main_store_fav'); - late final _wire_main_store_fav = _wire_main_store_favPtr - .asFunction)>(); - - void wire_main_get_peer( - int port_, - ffi.Pointer id, - ) { - return _wire_main_get_peer( - port_, - id, - ); - } - - late final _wire_main_get_peerPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function( - ffi.Int64, ffi.Pointer)>>('wire_main_get_peer'); - late final _wire_main_get_peer = _wire_main_get_peerPtr - .asFunction)>(); - - void wire_main_get_lan_peers( - int port_, - ) { - return _wire_main_get_lan_peers( - port_, - ); - } - - late final _wire_main_get_lan_peersPtr = - _lookup>( - 'wire_main_get_lan_peers'); - late final _wire_main_get_lan_peers = - _wire_main_get_lan_peersPtr.asFunction(); - - void wire_main_get_connect_status( - int port_, - ) { - return _wire_main_get_connect_status( - port_, - ); - } - - late final _wire_main_get_connect_statusPtr = - _lookup>( - 'wire_main_get_connect_status'); - late final _wire_main_get_connect_status = - _wire_main_get_connect_statusPtr.asFunction(); - - void wire_main_check_connect_status( - int port_, - ) { - return _wire_main_check_connect_status( - port_, - ); - } - - late final _wire_main_check_connect_statusPtr = - _lookup>( - 'wire_main_check_connect_status'); - late final _wire_main_check_connect_status = - _wire_main_check_connect_statusPtr.asFunction(); - - void wire_main_is_using_public_server( - int port_, - ) { - return _wire_main_is_using_public_server( - port_, - ); - } - - late final _wire_main_is_using_public_serverPtr = - _lookup>( - 'wire_main_is_using_public_server'); - late final _wire_main_is_using_public_server = - _wire_main_is_using_public_serverPtr.asFunction(); - - void wire_main_discover( - int port_, - ) { - return _wire_main_discover( - port_, - ); - } - - late final _wire_main_discoverPtr = - _lookup>( - 'wire_main_discover'); - late final _wire_main_discover = - _wire_main_discoverPtr.asFunction(); - - void wire_main_has_rendezvous_service( - int port_, - ) { - return _wire_main_has_rendezvous_service( - port_, - ); - } - - late final _wire_main_has_rendezvous_servicePtr = - _lookup>( - 'wire_main_has_rendezvous_service'); - late final _wire_main_has_rendezvous_service = - _wire_main_has_rendezvous_servicePtr.asFunction(); - - void wire_main_get_api_server( - int port_, - ) { - return _wire_main_get_api_server( - port_, - ); - } - - late final _wire_main_get_api_serverPtr = - _lookup>( - 'wire_main_get_api_server'); - late final _wire_main_get_api_server = - _wire_main_get_api_serverPtr.asFunction(); - - void wire_main_post_request( - int port_, - ffi.Pointer url, - ffi.Pointer body, - ffi.Pointer header, - ) { - return _wire_main_post_request( - port_, - url, - body, - header, - ); - } - - late final _wire_main_post_requestPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function( - ffi.Int64, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer)>>('wire_main_post_request'); - late final _wire_main_post_request = _wire_main_post_requestPtr.asFunction< - void Function(int, ffi.Pointer, - ffi.Pointer, ffi.Pointer)>(); - - void wire_main_get_local_option( - int port_, - ffi.Pointer key, - ) { - return _wire_main_get_local_option( - port_, - key, - ); - } - - late final _wire_main_get_local_optionPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Int64, - ffi.Pointer)>>('wire_main_get_local_option'); - late final _wire_main_get_local_option = _wire_main_get_local_optionPtr - .asFunction)>(); - - void wire_main_set_local_option( - int port_, - ffi.Pointer key, - ffi.Pointer value, - ) { - return _wire_main_set_local_option( - port_, - key, - value, - ); - } - - late final _wire_main_set_local_optionPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Int64, ffi.Pointer, - ffi.Pointer)>>('wire_main_set_local_option'); - late final _wire_main_set_local_option = - _wire_main_set_local_optionPtr.asFunction< - void Function(int, ffi.Pointer, - ffi.Pointer)>(); - - void wire_main_get_my_id( - int port_, - ) { - return _wire_main_get_my_id( - port_, - ); - } - - late final _wire_main_get_my_idPtr = - _lookup>( - 'wire_main_get_my_id'); - late final _wire_main_get_my_id = - _wire_main_get_my_idPtr.asFunction(); - - void wire_main_get_uuid( - int port_, - ) { - return _wire_main_get_uuid( - port_, - ); - } - - late final _wire_main_get_uuidPtr = - _lookup>( - 'wire_main_get_uuid'); - late final _wire_main_get_uuid = - _wire_main_get_uuidPtr.asFunction(); - - void wire_main_get_peer_option( - int port_, - ffi.Pointer id, - ffi.Pointer key, - ) { - return _wire_main_get_peer_option( - port_, - id, - key, - ); - } - - late final _wire_main_get_peer_optionPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Int64, ffi.Pointer, - ffi.Pointer)>>('wire_main_get_peer_option'); - late final _wire_main_get_peer_option = - _wire_main_get_peer_optionPtr.asFunction< - void Function(int, ffi.Pointer, - ffi.Pointer)>(); - - void wire_main_set_peer_option( - int port_, - ffi.Pointer id, - ffi.Pointer key, - ffi.Pointer value, - ) { - return _wire_main_set_peer_option( - port_, - id, - key, - value, - ); - } - - late final _wire_main_set_peer_optionPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function( - ffi.Int64, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer)>>('wire_main_set_peer_option'); - late final _wire_main_set_peer_option = - _wire_main_set_peer_optionPtr.asFunction< - void Function(int, ffi.Pointer, - ffi.Pointer, ffi.Pointer)>(); - - void wire_main_forget_password( - int port_, - ffi.Pointer id, - ) { - return _wire_main_forget_password( - port_, - id, - ); - } - - late final _wire_main_forget_passwordPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Int64, - ffi.Pointer)>>('wire_main_forget_password'); - late final _wire_main_forget_password = _wire_main_forget_passwordPtr - .asFunction)>(); - - void wire_main_get_recent_peers( - int port_, - ) { - return _wire_main_get_recent_peers( - port_, - ); - } - - late final _wire_main_get_recent_peersPtr = - _lookup>( - 'wire_main_get_recent_peers'); - late final _wire_main_get_recent_peers = - _wire_main_get_recent_peersPtr.asFunction(); - - void wire_main_load_recent_peers( - int port_, - ) { - return _wire_main_load_recent_peers( - port_, - ); - } - - late final _wire_main_load_recent_peersPtr = - _lookup>( - 'wire_main_load_recent_peers'); - late final _wire_main_load_recent_peers = - _wire_main_load_recent_peersPtr.asFunction(); - - void wire_main_load_fav_peers( - int port_, - ) { - return _wire_main_load_fav_peers( - port_, - ); - } - - late final _wire_main_load_fav_peersPtr = - _lookup>( - 'wire_main_load_fav_peers'); - late final _wire_main_load_fav_peers = - _wire_main_load_fav_peersPtr.asFunction(); - - void wire_main_load_lan_peers( - int port_, - ) { - return _wire_main_load_lan_peers( - port_, - ); - } - - late final _wire_main_load_lan_peersPtr = - _lookup>( - 'wire_main_load_lan_peers'); - late final _wire_main_load_lan_peers = - _wire_main_load_lan_peersPtr.asFunction(); - - void wire_session_add_port_forward( - int port_, - ffi.Pointer id, - int local_port, - ffi.Pointer remote_host, - int remote_port, - ) { - return _wire_session_add_port_forward( - port_, - id, - local_port, - remote_host, - remote_port, - ); - } - - late final _wire_session_add_port_forwardPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function( - ffi.Int64, - ffi.Pointer, - ffi.Int32, - ffi.Pointer, - ffi.Int32)>>('wire_session_add_port_forward'); - late final _wire_session_add_port_forward = - _wire_session_add_port_forwardPtr.asFunction< - void Function(int, ffi.Pointer, int, - ffi.Pointer, int)>(); - - void wire_session_remove_port_forward( - int port_, - ffi.Pointer id, - int local_port, - ) { - return _wire_session_remove_port_forward( - port_, - id, - local_port, - ); - } - - late final _wire_session_remove_port_forwardPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Int64, ffi.Pointer, - ffi.Int32)>>('wire_session_remove_port_forward'); - late final _wire_session_remove_port_forward = - _wire_session_remove_port_forwardPtr - .asFunction, int)>(); - - void wire_main_get_last_remote_id( - int port_, - ) { - return _wire_main_get_last_remote_id( - port_, - ); - } - - late final _wire_main_get_last_remote_idPtr = - _lookup>( - 'wire_main_get_last_remote_id'); - late final _wire_main_get_last_remote_id = - _wire_main_get_last_remote_idPtr.asFunction(); - - void wire_main_get_software_update_url( - int port_, - ) { - return _wire_main_get_software_update_url( - port_, - ); - } - - late final _wire_main_get_software_update_urlPtr = - _lookup>( - 'wire_main_get_software_update_url'); - late final _wire_main_get_software_update_url = - _wire_main_get_software_update_urlPtr.asFunction(); - - void wire_main_get_home_dir( - int port_, - ) { - return _wire_main_get_home_dir( - port_, - ); - } - - late final _wire_main_get_home_dirPtr = - _lookup>( - 'wire_main_get_home_dir'); - late final _wire_main_get_home_dir = - _wire_main_get_home_dirPtr.asFunction(); - - void wire_main_get_langs( - int port_, - ) { - return _wire_main_get_langs( - port_, - ); - } - - late final _wire_main_get_langsPtr = - _lookup>( - 'wire_main_get_langs'); - late final _wire_main_get_langs = - _wire_main_get_langsPtr.asFunction(); - - void wire_main_get_temporary_password( - int port_, - ) { - return _wire_main_get_temporary_password( - port_, - ); - } - - late final _wire_main_get_temporary_passwordPtr = - _lookup>( - 'wire_main_get_temporary_password'); - late final _wire_main_get_temporary_password = - _wire_main_get_temporary_passwordPtr.asFunction(); - - void wire_main_get_permanent_password( - int port_, - ) { - return _wire_main_get_permanent_password( - port_, - ); - } - - late final _wire_main_get_permanent_passwordPtr = - _lookup>( - 'wire_main_get_permanent_password'); - late final _wire_main_get_permanent_password = - _wire_main_get_permanent_passwordPtr.asFunction(); - - void wire_main_get_online_statue( - int port_, - ) { - return _wire_main_get_online_statue( - port_, - ); - } - - late final _wire_main_get_online_statuePtr = - _lookup>( - 'wire_main_get_online_statue'); - late final _wire_main_get_online_statue = - _wire_main_get_online_statuePtr.asFunction(); - - void wire_main_get_clients_state( - int port_, - ) { - return _wire_main_get_clients_state( - port_, - ); - } - - late final _wire_main_get_clients_statePtr = - _lookup>( - 'wire_main_get_clients_state'); - late final _wire_main_get_clients_state = - _wire_main_get_clients_statePtr.asFunction(); - - void wire_main_check_clients_length( - int port_, - int length, - ) { - return _wire_main_check_clients_length( - port_, - length, - ); - } - - late final _wire_main_check_clients_lengthPtr = - _lookup>( - 'wire_main_check_clients_length'); - late final _wire_main_check_clients_length = - _wire_main_check_clients_lengthPtr.asFunction(); - - void wire_main_init( - int port_, - ffi.Pointer app_dir, - ) { - return _wire_main_init( - port_, - app_dir, - ); - } - - late final _wire_main_initPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function( - ffi.Int64, ffi.Pointer)>>('wire_main_init'); - late final _wire_main_init = _wire_main_initPtr - .asFunction)>(); - - void wire_main_device_id( - int port_, - ffi.Pointer id, - ) { - return _wire_main_device_id( - port_, - id, - ); - } - - late final _wire_main_device_idPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Int64, - ffi.Pointer)>>('wire_main_device_id'); - late final _wire_main_device_id = _wire_main_device_idPtr - .asFunction)>(); - - void wire_main_device_name( - int port_, - ffi.Pointer name, - ) { - return _wire_main_device_name( - port_, - name, - ); - } - - late final _wire_main_device_namePtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Int64, - ffi.Pointer)>>('wire_main_device_name'); - late final _wire_main_device_name = _wire_main_device_namePtr - .asFunction)>(); - - void wire_main_remove_peer( - int port_, - ffi.Pointer id, - ) { - return _wire_main_remove_peer( - port_, - id, - ); - } - - late final _wire_main_remove_peerPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Int64, - ffi.Pointer)>>('wire_main_remove_peer'); - late final _wire_main_remove_peer = _wire_main_remove_peerPtr - .asFunction)>(); - - void wire_main_has_hwcodec( - int port_, - ) { - return _wire_main_has_hwcodec( - port_, - ); - } - - late final _wire_main_has_hwcodecPtr = - _lookup>( - 'wire_main_has_hwcodec'); - late final _wire_main_has_hwcodec = - _wire_main_has_hwcodecPtr.asFunction(); - - void wire_session_send_mouse( - int port_, - ffi.Pointer id, - ffi.Pointer msg, - ) { - return _wire_session_send_mouse( - port_, - id, - msg, - ); - } - - late final _wire_session_send_mousePtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Int64, ffi.Pointer, - ffi.Pointer)>>('wire_session_send_mouse'); - late final _wire_session_send_mouse = _wire_session_send_mousePtr.asFunction< - void Function( - int, ffi.Pointer, ffi.Pointer)>(); - - void wire_session_restart_remote_device( - int port_, - ffi.Pointer id, - ) { - return _wire_session_restart_remote_device( - port_, - id, - ); - } - - late final _wire_session_restart_remote_devicePtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Int64, ffi.Pointer)>>( - 'wire_session_restart_remote_device'); - late final _wire_session_restart_remote_device = - _wire_session_restart_remote_devicePtr - .asFunction)>(); - - void wire_main_set_home_dir( - int port_, - ffi.Pointer home, - ) { - return _wire_main_set_home_dir( - port_, - home, - ); - } - - late final _wire_main_set_home_dirPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Int64, - ffi.Pointer)>>('wire_main_set_home_dir'); - late final _wire_main_set_home_dir = _wire_main_set_home_dirPtr - .asFunction)>(); - - void wire_main_stop_service( - int port_, - ) { - return _wire_main_stop_service( - port_, - ); - } - - late final _wire_main_stop_servicePtr = - _lookup>( - 'wire_main_stop_service'); - late final _wire_main_stop_service = - _wire_main_stop_servicePtr.asFunction(); - - void wire_main_start_service( - int port_, - ) { - return _wire_main_start_service( - port_, - ); - } - - late final _wire_main_start_servicePtr = - _lookup>( - 'wire_main_start_service'); - late final _wire_main_start_service = - _wire_main_start_servicePtr.asFunction(); - - void wire_main_update_temporary_password( - int port_, - ) { - return _wire_main_update_temporary_password( - port_, - ); - } - - late final _wire_main_update_temporary_passwordPtr = - _lookup>( - 'wire_main_update_temporary_password'); - late final _wire_main_update_temporary_password = - _wire_main_update_temporary_passwordPtr.asFunction(); - - void wire_main_set_permanent_password( - int port_, - ffi.Pointer password, - ) { - return _wire_main_set_permanent_password( - port_, - password, - ); - } - - late final _wire_main_set_permanent_passwordPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Int64, ffi.Pointer)>>( - 'wire_main_set_permanent_password'); - late final _wire_main_set_permanent_password = - _wire_main_set_permanent_passwordPtr - .asFunction)>(); - - void wire_main_check_super_user_permission( - int port_, - ) { - return _wire_main_check_super_user_permission( - port_, - ); - } - - late final _wire_main_check_super_user_permissionPtr = - _lookup>( - 'wire_main_check_super_user_permission'); - late final _wire_main_check_super_user_permission = - _wire_main_check_super_user_permissionPtr - .asFunction(); - - void wire_main_check_mouse_time( - int port_, - ) { - return _wire_main_check_mouse_time( - port_, - ); - } - - late final _wire_main_check_mouse_timePtr = - _lookup>( - 'wire_main_check_mouse_time'); - late final _wire_main_check_mouse_time = - _wire_main_check_mouse_timePtr.asFunction(); - - void wire_main_get_mouse_time( - int port_, - ) { - return _wire_main_get_mouse_time( - port_, - ); - } - - late final _wire_main_get_mouse_timePtr = - _lookup>( - 'wire_main_get_mouse_time'); - late final _wire_main_get_mouse_time = - _wire_main_get_mouse_timePtr.asFunction(); - - void wire_cm_send_chat( - int port_, - int conn_id, - ffi.Pointer msg, - ) { - return _wire_cm_send_chat( - port_, - conn_id, - msg, - ); - } - - late final _wire_cm_send_chatPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Int64, ffi.Int32, - ffi.Pointer)>>('wire_cm_send_chat'); - late final _wire_cm_send_chat = _wire_cm_send_chatPtr - .asFunction)>(); - - void wire_cm_login_res( - int port_, - int conn_id, - ffi.Pointer res, - ) { - return _wire_cm_login_res( - port_, - conn_id, - res, - ); - } - - late final _wire_cm_login_resPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function( - ffi.Int64, ffi.Int32, ffi.Pointer)>>('wire_cm_login_res'); - late final _wire_cm_login_res = _wire_cm_login_resPtr - .asFunction)>(); - - void wire_cm_close_connection( - int port_, - int conn_id, - ) { - return _wire_cm_close_connection( - port_, - conn_id, - ); - } - - late final _wire_cm_close_connectionPtr = - _lookup>( - 'wire_cm_close_connection'); - late final _wire_cm_close_connection = - _wire_cm_close_connectionPtr.asFunction(); - - void wire_cm_check_click_time( - int port_, - int conn_id, - ) { - return _wire_cm_check_click_time( - port_, - conn_id, - ); - } - - late final _wire_cm_check_click_timePtr = - _lookup>( - 'wire_cm_check_click_time'); - late final _wire_cm_check_click_time = - _wire_cm_check_click_timePtr.asFunction(); - - void wire_cm_get_click_time( - int port_, - ) { - return _wire_cm_get_click_time( - port_, - ); - } - - late final _wire_cm_get_click_timePtr = - _lookup>( - 'wire_cm_get_click_time'); - late final _wire_cm_get_click_time = - _wire_cm_get_click_timePtr.asFunction(); - - void wire_cm_switch_permission( - int port_, - int conn_id, - ffi.Pointer name, - ffi.Pointer enabled, - ) { - return _wire_cm_switch_permission( - port_, - conn_id, - name, - enabled, - ); - } - - late final _wire_cm_switch_permissionPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Int64, ffi.Int32, ffi.Pointer, - ffi.Pointer)>>('wire_cm_switch_permission'); - late final _wire_cm_switch_permission = - _wire_cm_switch_permissionPtr.asFunction< - void Function( - int, int, ffi.Pointer, ffi.Pointer)>(); - - void wire_main_get_icon( - int port_, - ) { - return _wire_main_get_icon( - port_, - ); - } - - late final _wire_main_get_iconPtr = - _lookup>( - 'wire_main_get_icon'); - late final _wire_main_get_icon = - _wire_main_get_iconPtr.asFunction(); - - void wire_query_onlines( - int port_, - ffi.Pointer ids, - ) { - return _wire_query_onlines( - port_, - ids, - ); - } - - late final _wire_query_onlinesPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function( - ffi.Int64, ffi.Pointer)>>('wire_query_onlines'); - late final _wire_query_onlines = _wire_query_onlinesPtr - .asFunction)>(); - - ffi.Pointer new_StringList( - int len, - ) { - return _new_StringList( - len, - ); - } - - late final _new_StringListPtr = _lookup< - ffi.NativeFunction Function(ffi.Int32)>>( - 'new_StringList'); - late final _new_StringList = _new_StringListPtr - .asFunction Function(int)>(); - - ffi.Pointer new_uint_8_list( - int len, - ) { - return _new_uint_8_list( - len, - ); - } - - late final _new_uint_8_listPtr = _lookup< - ffi.NativeFunction< - ffi.Pointer Function( - ffi.Int32)>>('new_uint_8_list'); - late final _new_uint_8_list = _new_uint_8_listPtr - .asFunction Function(int)>(); - - void free_WireSyncReturnStruct( - WireSyncReturnStruct val, - ) { - return _free_WireSyncReturnStruct( - val, - ); - } - - late final _free_WireSyncReturnStructPtr = - _lookup>( - 'free_WireSyncReturnStruct'); - late final _free_WireSyncReturnStruct = _free_WireSyncReturnStructPtr - .asFunction(); - - void store_dart_post_cobject( - int ptr, - ) { - return _store_dart_post_cobject( - ptr, - ); - } - - late final _store_dart_post_cobjectPtr = - _lookup>( - 'store_dart_post_cobject'); - late final _store_dart_post_cobject = - _store_dart_post_cobjectPtr.asFunction(); - - int rustdesk_core_main() { - return _rustdesk_core_main(); - } - - late final _rustdesk_core_mainPtr = - _lookup>('rustdesk_core_main'); - late final _rustdesk_core_main = - _rustdesk_core_mainPtr.asFunction(); -} - -class wire_uint_8_list extends ffi.Struct { - external ffi.Pointer ptr; - - @ffi.Int32() - external int len; -} - -class wire_StringList extends ffi.Struct { - external ffi.Pointer> ptr; - - @ffi.Int32() - external int len; -} - -typedef bool = ffi.NativeFunction)>; -typedef uintptr_t = ffi.UnsignedLong; - -const int GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT = 2; - -const int GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS = 4; diff --git a/flutter/lib/generated_bridge.freezed.dart b/flutter/lib/generated_bridge.freezed.dart deleted file mode 100644 index fbaa6105f..000000000 --- a/flutter/lib/generated_bridge.freezed.dart +++ /dev/null @@ -1,332 +0,0 @@ -// coverage:ignore-file -// GENERATED CODE - DO NOT MODIFY BY HAND -// ignore_for_file: type=lint -// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target - -part of 'generated_bridge.dart'; - -// ************************************************************************** -// FreezedGenerator -// ************************************************************************** - -T _$identity(T value) => value; - -final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); - -/// @nodoc -mixin _$EventToUI { - @optionalTypeArgs - TResult when({ - required TResult Function(String field0) event, - required TResult Function(Uint8List field0) rgba, - }) => - throw _privateConstructorUsedError; - @optionalTypeArgs - TResult? whenOrNull({ - TResult Function(String field0)? event, - TResult Function(Uint8List field0)? rgba, - }) => - throw _privateConstructorUsedError; - @optionalTypeArgs - TResult maybeWhen({ - TResult Function(String field0)? event, - TResult Function(Uint8List field0)? rgba, - required TResult orElse(), - }) => - throw _privateConstructorUsedError; - @optionalTypeArgs - TResult map({ - required TResult Function(Event value) event, - required TResult Function(Rgba value) rgba, - }) => - throw _privateConstructorUsedError; - @optionalTypeArgs - TResult? mapOrNull({ - TResult Function(Event value)? event, - TResult Function(Rgba value)? rgba, - }) => - throw _privateConstructorUsedError; - @optionalTypeArgs - TResult maybeMap({ - TResult Function(Event value)? event, - TResult Function(Rgba value)? rgba, - required TResult orElse(), - }) => - throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class $EventToUICopyWith<$Res> { - factory $EventToUICopyWith(EventToUI value, $Res Function(EventToUI) then) = - _$EventToUICopyWithImpl<$Res>; -} - -/// @nodoc -class _$EventToUICopyWithImpl<$Res> implements $EventToUICopyWith<$Res> { - _$EventToUICopyWithImpl(this._value, this._then); - - final EventToUI _value; - // ignore: unused_field - final $Res Function(EventToUI) _then; -} - -/// @nodoc -abstract class _$$EventCopyWith<$Res> { - factory _$$EventCopyWith(_$Event value, $Res Function(_$Event) then) = - __$$EventCopyWithImpl<$Res>; - $Res call({String field0}); -} - -/// @nodoc -class __$$EventCopyWithImpl<$Res> extends _$EventToUICopyWithImpl<$Res> - implements _$$EventCopyWith<$Res> { - __$$EventCopyWithImpl(_$Event _value, $Res Function(_$Event) _then) - : super(_value, (v) => _then(v as _$Event)); - - @override - _$Event get _value => super._value as _$Event; - - @override - $Res call({ - Object? field0 = freezed, - }) { - return _then(_$Event( - field0 == freezed - ? _value.field0 - : field0 // ignore: cast_nullable_to_non_nullable - as String, - )); - } -} - -/// @nodoc - -class _$Event implements Event { - const _$Event(this.field0); - - @override - final String field0; - - @override - String toString() { - return 'EventToUI.event(field0: $field0)'; - } - - @override - bool operator ==(dynamic other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$Event && - const DeepCollectionEquality().equals(other.field0, field0)); - } - - @override - int get hashCode => - Object.hash(runtimeType, const DeepCollectionEquality().hash(field0)); - - @JsonKey(ignore: true) - @override - _$$EventCopyWith<_$Event> get copyWith => - __$$EventCopyWithImpl<_$Event>(this, _$identity); - - @override - @optionalTypeArgs - TResult when({ - required TResult Function(String field0) event, - required TResult Function(Uint8List field0) rgba, - }) { - return event(field0); - } - - @override - @optionalTypeArgs - TResult? whenOrNull({ - TResult Function(String field0)? event, - TResult Function(Uint8List field0)? rgba, - }) { - return event?.call(field0); - } - - @override - @optionalTypeArgs - TResult maybeWhen({ - TResult Function(String field0)? event, - TResult Function(Uint8List field0)? rgba, - required TResult orElse(), - }) { - if (event != null) { - return event(field0); - } - return orElse(); - } - - @override - @optionalTypeArgs - TResult map({ - required TResult Function(Event value) event, - required TResult Function(Rgba value) rgba, - }) { - return event(this); - } - - @override - @optionalTypeArgs - TResult? mapOrNull({ - TResult Function(Event value)? event, - TResult Function(Rgba value)? rgba, - }) { - return event?.call(this); - } - - @override - @optionalTypeArgs - TResult maybeMap({ - TResult Function(Event value)? event, - TResult Function(Rgba value)? rgba, - required TResult orElse(), - }) { - if (event != null) { - return event(this); - } - return orElse(); - } -} - -abstract class Event implements EventToUI { - const factory Event(final String field0) = _$Event; - - String get field0; - @JsonKey(ignore: true) - _$$EventCopyWith<_$Event> get copyWith => throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class _$$RgbaCopyWith<$Res> { - factory _$$RgbaCopyWith(_$Rgba value, $Res Function(_$Rgba) then) = - __$$RgbaCopyWithImpl<$Res>; - $Res call({Uint8List field0}); -} - -/// @nodoc -class __$$RgbaCopyWithImpl<$Res> extends _$EventToUICopyWithImpl<$Res> - implements _$$RgbaCopyWith<$Res> { - __$$RgbaCopyWithImpl(_$Rgba _value, $Res Function(_$Rgba) _then) - : super(_value, (v) => _then(v as _$Rgba)); - - @override - _$Rgba get _value => super._value as _$Rgba; - - @override - $Res call({ - Object? field0 = freezed, - }) { - return _then(_$Rgba( - field0 == freezed - ? _value.field0 - : field0 // ignore: cast_nullable_to_non_nullable - as Uint8List, - )); - } -} - -/// @nodoc - -class _$Rgba implements Rgba { - const _$Rgba(this.field0); - - @override - final Uint8List field0; - - @override - String toString() { - return 'EventToUI.rgba(field0: $field0)'; - } - - @override - bool operator ==(dynamic other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$Rgba && - const DeepCollectionEquality().equals(other.field0, field0)); - } - - @override - int get hashCode => - Object.hash(runtimeType, const DeepCollectionEquality().hash(field0)); - - @JsonKey(ignore: true) - @override - _$$RgbaCopyWith<_$Rgba> get copyWith => - __$$RgbaCopyWithImpl<_$Rgba>(this, _$identity); - - @override - @optionalTypeArgs - TResult when({ - required TResult Function(String field0) event, - required TResult Function(Uint8List field0) rgba, - }) { - return rgba(field0); - } - - @override - @optionalTypeArgs - TResult? whenOrNull({ - TResult Function(String field0)? event, - TResult Function(Uint8List field0)? rgba, - }) { - return rgba?.call(field0); - } - - @override - @optionalTypeArgs - TResult maybeWhen({ - TResult Function(String field0)? event, - TResult Function(Uint8List field0)? rgba, - required TResult orElse(), - }) { - if (rgba != null) { - return rgba(field0); - } - return orElse(); - } - - @override - @optionalTypeArgs - TResult map({ - required TResult Function(Event value) event, - required TResult Function(Rgba value) rgba, - }) { - return rgba(this); - } - - @override - @optionalTypeArgs - TResult? mapOrNull({ - TResult Function(Event value)? event, - TResult Function(Rgba value)? rgba, - }) { - return rgba?.call(this); - } - - @override - @optionalTypeArgs - TResult maybeMap({ - TResult Function(Event value)? event, - TResult Function(Rgba value)? rgba, - required TResult orElse(), - }) { - if (rgba != null) { - return rgba(this); - } - return orElse(); - } -} - -abstract class Rgba implements EventToUI { - const factory Rgba(final Uint8List field0) = _$Rgba; - - Uint8List get field0; - @JsonKey(ignore: true) - _$$RgbaCopyWith<_$Rgba> get copyWith => throw _privateConstructorUsedError; -} diff --git a/flutter/lib/generated_plugin_registrant.dart b/flutter/lib/generated_plugin_registrant.dart deleted file mode 100644 index eba9fb8cc..000000000 --- a/flutter/lib/generated_plugin_registrant.dart +++ /dev/null @@ -1,35 +0,0 @@ -// -// Generated file. Do not edit. -// - -// ignore_for_file: directives_ordering -// ignore_for_file: lines_longer_than_80_chars -// ignore_for_file: depend_on_referenced_packages - -import 'package:desktop_drop/desktop_drop_web.dart'; -import 'package:device_info_plus_web/device_info_plus_web.dart'; -import 'package:firebase_analytics_web/firebase_analytics_web.dart'; -import 'package:firebase_core_web/firebase_core_web.dart'; -import 'package:image_picker_for_web/image_picker_for_web.dart'; -import 'package:package_info_plus_web/package_info_plus_web.dart'; -import 'package:shared_preferences_web/shared_preferences_web.dart'; -import 'package:url_launcher_web/url_launcher_web.dart'; -import 'package:video_player_web/video_player_web.dart'; -import 'package:wakelock_web/wakelock_web.dart'; - -import 'package:flutter_web_plugins/flutter_web_plugins.dart'; - -// ignore: public_member_api_docs -void registerPlugins(Registrar registrar) { - DesktopDropWeb.registerWith(registrar); - DeviceInfoPlusPlugin.registerWith(registrar); - FirebaseAnalyticsWeb.registerWith(registrar); - FirebaseCoreWeb.registerWith(registrar); - ImagePickerPlugin.registerWith(registrar); - PackageInfoPlugin.registerWith(registrar); - SharedPreferencesPlugin.registerWith(registrar); - UrlLauncherPlugin.registerWith(registrar); - VideoPlayerPlugin.registerWith(registrar); - WakelockWeb.registerWith(registrar); - registrar.registerMessageHandler(); -} From c3fe407d5cfc0975ffaf6ba88b90ea0d779c2512 Mon Sep 17 00:00:00 2001 From: Asura Date: Fri, 2 Sep 2022 02:06:40 -0700 Subject: [PATCH 0360/2015] Check LLVM_HOME when build --- build.rs | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/build.rs b/build.rs index d9b7a5516..73f48ded2 100644 --- a/build.rs +++ b/build.rs @@ -78,18 +78,24 @@ fn install_oboe() { fn gen_flutter_rust_bridge() { // Get dependent of flutter - if !std::path::Path::new("./flutter/.packages").exists(){ + if !std::path::Path::new("./flutter/.packages").exists() { std::process::Command::new("flutter") - .args(["pub", "get"]) - .current_dir("./flutter") - .output() - .expect("failed to execute flutter pub get"); + .args(["pub", "get"]) + .current_dir("./flutter") + .output() + .expect("failed to execute flutter pub get"); }; - let llvm_path = match std::env::var("LLVM_HOME") { - Ok(path) => Some(vec![path]), - Err(_) => None, + Ok(path) => { + if !path.is_empty() { + Some(vec![path]) + } else { + panic!("Missing LVM_HOME environment variable"); + } + } + Err(_) => panic!("Failure to get environments"), }; + // Tell Cargo that if the given file changes, to rerun this build script. println!("cargo:rerun-if-changed=src/flutter_ffi.rs"); // settings for fbr_codegen From f69bedeac5052be74687d123c1dde3c6e45dfba5 Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 2 Sep 2022 18:56:03 +0800 Subject: [PATCH 0361/2015] sciter_desktop: fix cursor size(resize window) and id(after connection) Signed-off-by: fufesou --- src/ui/remote.tis | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/ui/remote.tis b/src/ui/remote.tis index 65e7e5030..835136442 100644 --- a/src/ui/remote.tis +++ b/src/ui/remote.tis @@ -67,6 +67,7 @@ function adaptDisplay() { } } } + refreshCursor(); handler.style.set { width: w / scaleFactor + "px", height: h / scaleFactor + "px", @@ -98,6 +99,7 @@ var acc_wheel_delta_y0 = 0; var total_wheel_time = 0; var wheeling = false; var dragging = false; +var is_mouse_event_triggered = false; // https://stackoverflow.com/questions/5833399/calculating-scroll-inertia-momentum function resetWheel() { @@ -139,6 +141,7 @@ function accWheel(v, is_x) { function handler.onMouse(evt) { + is_mouse_event_triggered = true; if (is_file_transfer || is_port_forward) return false; if (view.windowState == View.WINDOW_FULL_SCREEN && !dragging) { var dy = evt.y - scroll_body.scroll(#top); @@ -317,6 +320,7 @@ function handler.onMouse(evt) return true; }; +var cur_id = -1; var cur_hotx = 0; var cur_hoty = 0; var cur_img = null; @@ -345,7 +349,7 @@ function scaleCursorImage(img) { var useSystemCursor = true; function updateCursor(system=false) { stdout.println("Update cursor, system: " + system); - useSystemCursor= system; + useSystemCursor = system; if (system) { handler.style#cursor = undefined; } else if (cur_img) { @@ -353,6 +357,12 @@ function updateCursor(system=false) { } } +function refreshCursor() { + if (cur_id != -1) { + handler.setCursorId(cur_id); + } +} + handler.setCursorData = function(id, hotx, hoty, width, height, colors) { cur_hotx = hotx; cur_hoty = hoty; @@ -360,8 +370,9 @@ handler.setCursorData = function(id, hotx, hoty, width, height, colors) { if (img) { image_binded = true; cursors[id] = [img, hotx, hoty, width, height]; + cur_id = id; img = scaleCursorImage(img); - if (cursor_img.style#display == 'none') { + if (!first_mouse_event_triggered || cursor_img.style#display == 'none') { self.timer(1ms, updateCursor); } cur_img = img; @@ -371,11 +382,12 @@ handler.setCursorData = function(id, hotx, hoty, width, height, colors) { handler.setCursorId = function(id) { var img = cursors[id]; if (img) { + cur_id = id; image_binded = true; cur_hotx = img[1]; cur_hoty = img[2]; img = scaleCursorImage(img[0]); - if (cursor_img.style#display == 'none') { + if (!first_mouse_event_triggered || cursor_img.style#display == 'none') { self.timer(1ms, updateCursor); } cur_img = img; From 34d7089a8e2c8c0260932b9992924467e1daf577 Mon Sep 17 00:00:00 2001 From: Asura Date: Fri, 2 Sep 2022 04:49:36 -0700 Subject: [PATCH 0362/2015] Refactor: map keyboard --- flutter/lib/desktop/pages/remote_page.dart | 8 ++-- flutter/lib/models/model.dart | 12 ++++-- src/flutter_ffi.rs | 31 ++++++++++---- src/ui_session_interface.rs | 50 ++++++++++++++++++---- 4 files changed, 75 insertions(+), 26 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index c3f4b3773..51225672b 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -291,13 +291,13 @@ class _RemotePageState extends State keyCode = -1; } - if (e is RawKeyDownEvent){ + if (e is RawKeyDownEvent) { down = true; - }else{ + } else { down = false; } - - _ffi.inputRawKey(keyCode, scanCode, down); + + _ffi.inputRawKey(e.character ?? "", keyCode, scanCode, down); } void legacyKeyboardMode(RawKeyEvent e) { diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 967a334e8..dcf9a7b22 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -993,10 +993,14 @@ class FFI { msg: json.encode(modify({'type': type, 'buttons': button.value}))); } - // Raw Key - void inputRawKey(int keyCode, int scanCode, bool down){ - debugPrint(scanCode.toString()); - // bind.sessionInputRawKey(id: id, keycode: keyCode, scancode: scanCode, down: down); + // Raw Key + void inputRawKey(String name, int keyCode, int scanCode, bool down) { + bind.sessionHandleFlutterKeyEvent( + id: id, + name: name, + keycode: keyCode, + scancode: scanCode, + downOrUp: down); } /// Send key stroke event. diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 78f53bf89..72ccbe603 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -7,20 +7,19 @@ use std::{ use flutter_rust_bridge::{StreamSink, SyncReturn, ZeroCopyBuffer}; use serde_json::json; +use hbb_common::ResultType; use hbb_common::{ config::{self, LocalConfig, PeerConfig, ONLINE}, fs, log, }; -use hbb_common::{ResultType}; -use crate::{client::file_trait::FileManager, flutter::{session_add, session_start_}}; use crate::common::make_fd_to_json; use crate::flutter::connection_manager::{self, get_clients_length, get_clients_state}; use crate::flutter::{self, SESSIONS}; use crate::start_server; use crate::ui_interface; #[cfg(not(any(target_os = "android", target_os = "ios")))] -use crate::ui_interface::{change_id}; +use crate::ui_interface::change_id; use crate::ui_interface::{ check_mouse_time, check_super_user_permission, discover, forget_password, get_api_server, get_app_name, get_async_job_status, get_connect_status, get_fav, get_id, get_lan_peers, @@ -30,6 +29,10 @@ use crate::ui_interface::{ set_peer_option, set_permanent_password, set_socks, store_fav, test_if_valid_server, update_temporary_password, using_public_server, }; +use crate::{ + client::file_trait::FileManager, + flutter::{session_add, session_start_}, +}; fn initialize(app_dir: &str) { *config::APP_DIR.write().unwrap() = app_dir.to_owned(); @@ -110,7 +113,11 @@ pub fn host_stop_system_key_propagate(stopped: bool) { // FIXME: -> ResultType<()> cannot be parsed by frb_codegen // thread 'main' panicked at 'Failed to parse function output type `ResultType<()>`', $HOME\.cargo\git\checkouts\flutter_rust_bridge-ddba876d3ebb2a1e\e5adce5\frb_codegen\src\parser\mod.rs:151:25 -pub fn session_add_sync(id: String, is_file_transfer: bool, is_port_forward: bool) -> SyncReturn { +pub fn session_add_sync( + id: String, + is_file_transfer: bool, + is_port_forward: bool, +) -> SyncReturn { if let Err(e) = session_add(&id, is_file_transfer, is_port_forward) { SyncReturn(format!("Failed to add session with id {}, {}", &id, e)) } else { @@ -228,11 +235,17 @@ pub fn session_switch_display(id: String, value: i32) { } } -// pub fn session_input_raw_key(id: String, keycode: i32, scancode:i32, down: bool){ -// if let Some(session) = SESSIONS.read().unwrap().get(&id) { -// session.input_raw_key(keycode, scancode, down); -// } -// } +pub fn session_handle_flutter_key_event( + id: String, + name: String, + keycode: i32, + scancode: i32, + down_or_up: bool, +) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.handle_flutter_key_event(&name, keycode, scancode, down_or_up); + } +} pub fn session_input_key( id: String, diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 4f5ec55b8..7143ed97a 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -57,13 +57,13 @@ impl Session { self.lc.read().unwrap().custom_image_quality.clone() } - pub fn get_keyboard_mode(&mut self) -> String { + pub fn get_keyboard_mode(&self) -> String { return std::env::var("KEYBOARD_MODE") .unwrap_or(String::from("legacy")) .to_lowercase(); } - pub fn save_keyboard_mode(&mut self, value: String) { + pub fn save_keyboard_mode(&self, value: String) { std::env::set_var("KEYBOARD_MODE", value); } @@ -272,7 +272,7 @@ impl Session { } #[allow(dead_code)] - fn convert_numpad_keys(&mut self, key: RdevKey) -> RdevKey { + fn convert_numpad_keys(&self, key: RdevKey) -> RdevKey { if get_key_state(enigo::Key::NumLock) { return key; } @@ -292,7 +292,7 @@ impl Session { } } - fn map_keyboard_mode(&mut self, down_or_up: bool, key: RdevKey, _evt: Option) { + fn map_keyboard_mode(&self, down_or_up: bool, key: RdevKey, _evt: Option) { // map mode(1): Send keycode according to the peer platform. #[cfg(target_os = "windows")] let key = if let Some(e) = _evt { @@ -328,11 +328,12 @@ impl Session { if get_key_state(enigo::Key::NumLock) { key_event.modifiers.push(ControlKey::NumLock.into()); } + log::info!("{:?}", get_key_state(enigo::Key::NumLock)); self.send_key_event(key_event, KeyboardMode::Map); } - fn translate_keyboard_mode(&mut self, down_or_up: bool, key: RdevKey, evt: Event) { + fn translate_keyboard_mode(&self, down_or_up: bool, key: RdevKey, evt: Event) { // translate mode(2): locally generated characters are send to the peer. // get char @@ -423,7 +424,7 @@ impl Session { } } - fn legacy_keyboard_mode(&mut self, down_or_up: bool, key: RdevKey, evt: Event) { + fn legacy_keyboard_mode(&self, down_or_up: bool, key: RdevKey, evt: Event) { // legacy mode(0): Generate characters locally, look for keycode on other side. let peer = self.peer_platform(); let is_win = peer == "Windows"; @@ -635,7 +636,7 @@ impl Session { self.send_key_event(key_event, KeyboardMode::Legacy) } - fn key_down_or_up(&mut self, down_or_up: bool, key: RdevKey, evt: Event) { + fn key_down_or_up(&self, down_or_up: bool, key: RdevKey, evt: Event) { // Call different functions according to keyboard mode. let mode = match self.get_keyboard_mode().as_str() { "map" => KeyboardMode::Map, @@ -724,6 +725,35 @@ impl Session { self.send_key_event(key_event, KeyboardMode::Legacy); } + pub fn handle_flutter_key_event(&self, name: &str, keycode: i32, scancode:i32, down_or_up: bool){ + if scancode < 0 || keycode < 0{ + return; + } + let keycode: u32 = keycode as u32; + let scancode: u32 = scancode as u32; + + #[cfg(not(target_os = "windows"))] + let key = rdev::key_from_scancode(scancode) as RdevKey; + // Windows requires special handling + #[cfg(target_os = "windows")] + let key = rdev::get_win_key(keycode, scancode); + + let event_type = if down_or_up{ + KeyPress(key) + }else{ + KeyRelease(key) + }; + let evt = Event{ + time: std::time::SystemTime::now(), + name: Option::Some(name.to_owned()), + code: keycode as _, + scan_code: scancode as _, + event_type: event_type, + }; + + self.key_down_or_up(down_or_up, key, evt) + } + // flutter only TODO new input pub fn input_key( &self, @@ -796,8 +826,10 @@ impl Session { } } } - // asur4s-todo - // self.key_down_or_up(v, key_event, alt, ctrl, shift, command); + + self.legacy_modifiers(&mut key_event, true, true, false, false); + key_event.press = down; + self.send_key_event(key_event, KeyboardMode::Legacy); } pub fn send_mouse( From 815d02b72875ffef3b6ec89b6d3a36476f5aaf06 Mon Sep 17 00:00:00 2001 From: Asura Date: Fri, 2 Sep 2022 04:54:04 -0700 Subject: [PATCH 0363/2015] Fix misspell --- build.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.rs b/build.rs index 73f48ded2..14917ffd4 100644 --- a/build.rs +++ b/build.rs @@ -90,7 +90,7 @@ fn gen_flutter_rust_bridge() { if !path.is_empty() { Some(vec![path]) } else { - panic!("Missing LVM_HOME environment variable"); + panic!("Missing LLVM_HOME environment variable"); } } Err(_) => panic!("Failure to get environments"), From 15b8a5592d6f44408da4c2319ecb5d2c8cbee73b Mon Sep 17 00:00:00 2001 From: Asura Date: Fri, 2 Sep 2022 05:41:50 -0700 Subject: [PATCH 0364/2015] Refactor: check env variable --- build.rs | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/build.rs b/build.rs index 14917ffd4..9b353b42e 100644 --- a/build.rs +++ b/build.rs @@ -41,6 +41,16 @@ fn build_rc_source() { .unwrap(); } +fn check_environment() { + // Check env variable + let env_list = vec!["LLVM_HOME", "VCPKG_ROOT"]; + for env in env_list.iter() { + if std::env::var(env).is_err() { + panic!("Missing environment variable: {:?}", env); + }; + } +} + fn install_oboe() { let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); if target_os != "android" { @@ -83,16 +93,10 @@ fn gen_flutter_rust_bridge() { .args(["pub", "get"]) .current_dir("./flutter") .output() - .expect("failed to execute flutter pub get"); + .expect("Failed to execute flutter pub get"); }; let llvm_path = match std::env::var("LLVM_HOME") { - Ok(path) => { - if !path.is_empty() { - Some(vec![path]) - } else { - panic!("Missing LLVM_HOME environment variable"); - } - } + Ok(path) => Some(vec![path]), Err(_) => panic!("Failure to get environments"), }; @@ -115,6 +119,8 @@ fn gen_flutter_rust_bridge() { } fn main() { + check_environment(); + hbb_common::gen_version(); install_oboe(); // there is problem with cfg(target_os) in build.rs, so use our workaround From fe4790c426e06b904e46fccec688518188437161 Mon Sep 17 00:00:00 2001 From: darmenerk <51264035+darmenerk@users.noreply.github.com> Date: Fri, 2 Sep 2022 22:15:32 +0500 Subject: [PATCH 0365/2015] Create kz.rs --- src/lang/kz.rs | 323 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 323 insertions(+) create mode 100644 src/lang/kz.rs diff --git a/src/lang/kz.rs b/src/lang/kz.rs new file mode 100644 index 000000000..8b7b3402d --- /dev/null +++ b/src/lang/kz.rs @@ -0,0 +1,323 @@ +lazy_static::lazy_static! { + pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "Күй"), + ("Your Desktop", "Сіздің Жұмыс үстеліңіз"), + ("desk_tip", "Сіздің Жұмыс үстеліңіз осы ID мен құпия сөз арқылы қолжетімді"), + ("Password", "Құпия сөз"), + ("Ready", "Дайын"), + ("Established", "Қосылды"), + ("connecting_status", "RustDesk желісіне қосылуда..."), + ("Enable Service", "Сербесті қосу"), + ("Start Service", "Сербесті іске қосу"), + ("Service is running", "Сербес істеуде"), + ("Service is not running", "Сербес істемеуде"), + ("not_ready_status", "Дайын емес. Қосылымды тексеруді өтінеміз"), + ("Control Remote Desktop", "Қашықтағы Жұмыс үстелін Басқару"), + ("Transfer File", "Файыл Тасымалдау"), + ("Connect", "Қосылу"), + ("Recent Sessions", "Соңғы Сештер"), + ("Address Book", "Мекенжай Кітабы"), + ("Confirmation", "Мақұлдау"), + ("TCP Tunneling", "TCP тунелдеу"), + ("Remove", "Жою"), + ("Refresh random password", "Кездейсоқ құпия сөзді жаңарту"), + ("Set your own password", "Өз құпия сөзіңізді орнатыңыз"), + ("Enable Keyboard/Mouse", "Пернетақта/Тінтуірді қосу"), + ("Enable Clipboard", "Көшіру-тақтасын қосу"), + ("Enable File Transfer", "Файыл Тасымалдауды қосу"), + ("Enable TCP Tunneling", "TCP тунелдеуді қосу"), + ("IP Whitelisting", "IP Ақ-тізімі"), + ("ID/Relay Server", "ID/Relay сербері"), + ("Stop service", "Сербесті тоқтату"), + ("Change ID", "ID ауыстыру"), + ("Website", "Web-сайт"), + ("About", "Туралы"), + ("Mute", "Дыбыссыздандыру"), + ("Audio Input", "Аудио Еңгізу"), + ("Enhancements", "Жақсартулар"), + ("Hardware Codec", "Hardware Codec"), + ("Adaptive Bitrate", "Adaptive Bitrate"), + ("ID Server", "ID Сербері"), + ("Relay Server", "Relay Сербері"), + ("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", "Қолжетімсіз"), + ("Too frequent", "Тым жиі"), + ("Cancel", "Болдырмау"), + ("Skip", "Өткізіп жіберу"), + ("Close", "Жабу"), + ("Retry", "Қайтадан көру"), + ("OK", "OK"), + ("Password Required", "Құпия сөз Қажет"), + ("Please enter your password", "Құпия сөзіңізді еңгізуді өтінеміз"), + ("Remember password", "Құпия сөзді есте сақтау"), + ("Wrong Password", "Бұрыс Құпия сөз"), + ("Do you want to enter again?", "Қайтадан кіргіңіз келеді ме?"), + ("Connection Error", "Қосылым Қатесі"), + ("Error", "Қате"), + ("Reset by the peer", "Пир қалпына келтірді"), + ("Connecting...", "Қосылуда..."), + ("Connection in progress. Please wait.", "Қосылым барысында. Күтуді өтінеміз"), + ("Please try 1 minute later", "1 минуттан соң қайта көріңіз"), + ("Login Error", "Кіру Қатесі"), + ("Successful", "Сәтті"), + ("Connected, waiting for image...", "Қосылды, сурет күтілуде..."), + ("Name", "Ат"), + ("Type", "Түр"), + ("Modified", "Өзгертілді"), + ("Size", "Өлшем"), + ("Show Hidden Files", "Жасырын Файылдарды Көрсету"), + ("Receive", "Қабылдау"), + ("Send", "Жіберу"), + ("Refresh File", "Файылды жаңарту"), + ("Local", "Лақал"), + ("Remote", "Қашықтағы"), + ("Remote Computer", "Қашықтағы Қампұтыр"), + ("Local Computer", "Лақал Қампұтыр"), + ("Confirm Delete", "Жоюды Растау"), + ("Delete", "Жою"), + ("Properties", "Қасиеттер"), + ("Multi Select", "Көптік таңдау"), + ("Empty Directory", "Бос Бума"), + ("Not an empty directory", "Бос бума емес"), + ("Are you sure you want to delete this file?", "Бұл файылды жоюға сенімдісіз бе?"), + ("Are you sure you want to delete this empty directory?", "Бұл бос буманы жоюға сенімдісіз бе?"), + ("Are you sure you want to delete the file of this directory?", "Бұл буманың файылын жоюға сенімдісіз бе?"), + ("Do this for all conflicts", "Мұны барлық қанпілектер үшін жасау"), + ("This is irreversible!", "Бұл қайтымсыз!"), + ("Deleting", "Жойылу"), + ("files", "файылдар"), + ("Waiting", "Күту"), + ("Finished", "Аяқталды"), + ("Speed", "Жылдамдық"), + ("Custom Image Quality", "Теңшеулі Сурет Сапасы"), + ("Privacy mode", "Құпиялылық Модасы"), + ("Block user input", "Қолданушы еңгізуін бұғаттау"), + ("Unblock user input", "Қолданушы еңгізуін бұғаттан шығару"), + ("Adjust Window", "Терезені Реттеу"), + ("Original", "Түпнұсқа"), + ("Shrink", "Қысу"), + ("Stretch", "Созу"), + ("Scrollbar", "Scrollbar"), + ("ScrollAuto", "ScrollAuto"), + ("Good image quality", "Жақсы сурет сапасы"), + ("Balanced", "Теңдестірілген"), + ("Optimize reaction time", "Реакция уақытын оңтайландыру"), + ("Custom", "Теңшеулі"), + ("Show remote cursor", "Қашықтағы курсорды көрсету"), + ("Show quality monitor", "Сапа мониторын көрсету"), + ("Disable clipboard", "Көшіру-тақтасын өшіру"), + ("Lock after session end", "Сеш аяқталған соң құлыптау"), + ("Insert", "Кірістіру"), + ("Insert Lock", "Кірістіруді Құлыптау"), + ("Refresh", "Жаңарту"), + ("ID does not exist", "ID табылмады"), + ("Failed to connect to rendezvous server", "Rendezvous серберіне қосылу сәтсіз"), + ("Please try later", "Кейінірек қайта көруді өтінеміз"), + ("Remote desktop is offline", "Қашықтағы жұмыс үстелі офлайн күйінде"), + ("Key mismatch", "Кілт сәйкессіздігі"), + ("Timeout", "Үзіліс"), + ("Failed to connect to relay server", "Relay серберіне қосылу сәтсіз"), + ("Failed to connect via rendezvous server", "Rendezvous сербері арқылы қосылу сәтсіз"), + ("Failed to connect via relay server", "Relay сербері арқылы қосылу сәтсіз"), + ("Failed to make direct connection to remote desktop", "Қашықтағы жұмыс үстеліне тікелей қосылым жасау сәтсіз"), + ("Set Password", "Құпия сөзді Орнату"), + ("OS Password", "OS Құпия сөзі"), + ("install_tip", "UAC кесірінен, RustDesk кейбірде қашықтағы жақ ретінде дұрыс жұмыс істей алмайды. UAC'пен қиындықты болдырмау үшін, төмендегі батырманы басып RustDesk'ті жүйеге орнатыңыз."), + ("Click to upgrade", "Жаңғырту үшін басыңыз"), + ("Click to download", "Жүктеу үшін басыңыз"), + ("Click to update", "Жаңарту үшін басыңыз"), + ("Configure", "Қалыптау"), + ("config_acc", "Сіздің Жұмыс үстеліңізді қашықтан басқару үшін, RustDesk'ке \"Қолжетімділік\" рұқсаттарын беруіңіз керек."), + ("config_screen", "Сіздің Жұмыс үстеліңізге қашықтан қол жеткізу үшін, RustDesk'ке \"Екіренді Жазу\" рұқсаттарын беруіңіз керек."), + ("Installing ...", "Орнатылу..."), + ("Install", "Орнату"), + ("Installation", "Орнатылу"), + ("Installation Path", "Орнатылу Жолы"), + ("Create start menu shortcuts", "Бастау мәзірі белгішесің жасау"), + ("Create desktop icon", "Жұмыс үстелі белгішесің жасау"), + ("agreement_tip", "Орнатуды бастасаңыз, сіз лисензе келісімін қабылдайсыз."), + ("Accept and Install", "Қабылдау және Орнату"), + ("End-user license agreement", "Түпкі қолданушының лисензе келісімі"), + ("Generating ...", "Генератталуда..."), + ("Your installation is lower version.", "Сіздің орнатуыныз төменгі нұсқа."), + ("not_close_tcp_tip", "Тунел қолдану кезінде бұл терезені жаппаңыз"), + ("Listening ...", "Тыңдау ..."), + ("Remote Host", "Қашықтағы Хост"), + ("Remote Port", "Қашықтағы Порт"), + ("Action", "Әрекет"), + ("Add", "Қосу"), + ("Local Port", "Лақал Порт"), + ("setup_server_tip", "Тез қосылым үшін өз серберіңізді орнатуды өтінеміз"), + ("Too short, at least 6 characters.", "Тым қысқа, кемінде 6 таңба."), + ("The confirmation is not identical.", "Растау сәйкес келмейді."), + ("Permissions", "Рұқсаттар"), + ("Accept", "Қабылдау"), + ("Dismiss", "Босату"), + ("Disconnect", "Ажырату"), + ("Allow using keyboard and mouse", "Пернетақта мен тінтуірді қолдануды рұқсат ету"), + ("Allow using clipboard", "Көшіру-тақтасын рұқсат ету"), + ("Allow hearing sound", "Дыбыс естуді рұқсат ету"), + ("Allow file copy and paste", "Файылды көшіру мен қоюды рұқсат ету"), + ("Connected", "Қосылды"), + ("Direct and encrypted connection", "Тікелей және кіриптелген қосылым"), + ("Relayed and encrypted connection", "Релайданған және кіриптелген қосылым"), + ("Direct and unencrypted connection", "Тікелей және кіриптелмеген қосылым"), + ("Relayed and unencrypted connection", "Релайданған және кіриптелмеген қосылым"), + ("Enter Remote ID", "Қашықтағы ID еңгізіңіз"), + ("Enter your password", "Құпия сөзіңізді енгізіңіз"), + ("Logging in...", "Кіруде..."), + ("Enable RDP session sharing", "RDP сешті бөлісуді іске қосу"), + ("Auto Login", "Ауты Кіру (\"Сеш аяқталған соң құлыптау\"'ды орнатқанда ғана жарамды)"), + ("Enable Direct IP Access", "Тікелей IP Қолжетімді іске қосу"), + ("Rename", "Атын өзгерту"), + ("Space", "Орын"), + ("Create Desktop Shortcut", "Жұмыс үстелі Таңбашасын Жасау"), + ("Change Path", "Жолды өзгерту"), + ("Create Folder", "Бума жасау"), + ("Please enter the folder name", "Буманың атауын еңгізуді өтінеміз"), + ("Fix it", "Түзету"), + ("Warning", "Ескерту"), + ("Login screen using Wayland is not supported", "Wayland қолданған Кіру екіреніне қолдау көрсетілмейді"), + ("Reboot required", "Қайта-қосу қажет"), + ("Unsupported display server ", "Қолдаусыз дисплей сербері"), + ("x11 expected", "x11 күтілген"), + ("Port", "Порт"), + ("Settings", "Орнатпалар"), + ("Username", "Қолданушы аты"), + ("Invalid port", "Бұрыс порт"), + ("Closed manually by the peer", "Пир қолымен жабылған"), + ("Enable remote configuration modification", "Қашықтан қалыптарды өзгертуді іске қосу"), + ("Run without install", "Орнатпай-ақ Іске қосу"), + ("Always connected via relay", "Әрқашан да релай сербері арқылы қосулы"), + ("Always connect via relay", "Әрқашан да релай сербері арқылы қосылу"), + ("whitelist_tip", "Маған тек ақ-тізімделген IP қол жеткізе алады"), + ("Login", "Кіру"), + ("Logout", "Шығу"), + ("Tags", "Тақтар"), + ("Search ID", "ID Іздеу"), + ("Current Wayland display server is not supported", "Ағымдағы Wayland дисплей серберіне қолдау көрсетілмейді"), + ("whitelist_sep", "Үтір, нүктелі үтір, бос орын және жаңа жолал арқылы бөлінеді"), + ("Add ID", "ID Қосу"), + ("Add Tag", "Тақ Қосу"), + ("Unselect all tags", "Барлық тақтардың таңдауын алып тастау"), + ("Network error", "Желі қатесі"), + ("Username missed", "Қолданушы аты бос"), + ("Password missed", "Құпия сөз бос"), + ("Wrong credentials", "Бұрыс тіркелгі деректер"), + ("Edit Tag", "Тақты Өндеу"), + ("Unremember Password", "Құпия сөзді Ұмыту"), + ("Favorites", "Таңдаулылар"), + ("Add to Favorites", "Таңдаулыларға Қосу"), + ("Remove from Favorites", "Таңдаулылардан алып тастау"), + ("Empty", "Бос"), + ("Invalid folder name", "Бұрыс бума атауы"), + ("Socks5 Proxy", "Socks5 Proxy"), + ("Hostname", "Хост атауы"), + ("Discovered", "Табылды"), + ("install_daemon_tip", "Бут кезінде қосылу үшін жүйелік сербесті орнатуыныз керек."), + ("Remote ID", "Қашықтағы ID"), + ("Paste", "Қою"), + ("Paste here?", "Осында қою керек пе?"), + ("Are you sure to close the connection?", "Қосылымды жабуға сенімдісіз бе?"), + ("Download new version", "Жаңа нұсқаны жүктеу"), + ("Touch mode", "Жанасатын мода"), + ("Mouse mode", "Тінтуірлі мода"), + ("One-Finger Tap", "Бір-Саусақпен Түрту"), + ("Left Mouse", "Солақ Тінтуір"), + ("One-Long Tap", "Бір-Ұзақ Түрту"), + ("Two-Finger Tap", "Екі-Саусақпен Түрту"), + ("Right Mouse", "Оңақ Тінтуір"), + ("One-Finger Move", "Бір-Саусақпен Жылжыту"), + ("Double Tap & Move", "Екі-рет Түртіп Жылжыту"), + ("Mouse Drag", "Тінтуір Тартуы"), + ("Three-Finger vertically", "Үш-Саусақпен тік-бағытты"), + ("Mouse Wheel", "Тінтуір Дөңгелегі"), + ("Two-Finger Move", "Екі-Саусақпен Жылжыту"), + ("Canvas Move", "Кенеп Жылжуы"), + ("Pinch to Zoom", "Зумдау үшін Шымшыңыз"), + ("Canvas Zoom", "Кенеп Зумы"), + ("Reset canvas", "Кенепті қалпына келтіру"), + ("No permission of file transfer", "Файыл алмасуға рұқсат берілмеген"), + ("Note", "Нота"), + ("Connection", "Қосылым"), + ("Share Screen", "Екіренді Бөлісу"), + ("CLOSE", "ЖАБУ"), + ("OPEN", "АШУ"), + ("Chat", "Чат"), + ("Total", "Барлығы"), + ("items", "зат"), + ("Selected", "Таңдалған"), + ("Screen Capture", "Екіренді Түсіру"), + ("Input Control", "Еңгізуді Басқару/Қадағалау"), + ("Audio Capture", "Аудио Түсіру"), + ("File Connection", "Файыл Қосылымы"), + ("Screen Connection", "Екірен Қосылымы"), + ("Do you accept?", "Қабылдайсыз ба?"), + ("Open System Setting", "Жүйе Орнатпаларын Ашу"), + ("How to get Android input permission?", "Android еңгізу рұқсатын қалай алуға болады?"), + ("android_input_permission_tip1", "Қашықтағы құрылғы сіздің Android құрылғыңызды тінтуір немесе түрту арқылы басқару үшін, RustDesk'ке \"Қолжетімділік\" сербесін қолдануға рұқсат беруініз керек."), + ("android_input_permission_tip2", "Келесі Жүйе Орнатпалары бетіне барып, [Орнатылған Сербестер]'ді тауып кіріңіз, сосын [RustDesk Еңгізу] сербесін іске қосыңыз."), + ("android_new_connection_tip", "Сіздің ағымдағы құрылғыңызды басқаруды қалайтын жаңа басқару сұранысы түсті."), + ("android_service_will_start_tip", "\"Екіренді Тұсіру\" қосылған кезде сербес аутыматты іске қосылып, басқа құрылғыларға сіздің құрылғыға қосылым сұраныстауға мүмкіндің береді."), + ("android_stop_service_tip", "Сербесті жабу аутыматты түрде барлық орнатылған қосылымдарды жабады."), + ("android_version_audio_tip", "Ағымдағы Android нұсқасы аудионы түсіруді қолдамайды, Android 10 не жоғарғысына жаңғыртуды өтінеміз."), + ("android_start_service_tip", "[Сербесті Іске қосу]'ды түртіңіз не [Екіренді Түсіру] рұқсатын АШУ арқылы екіренді бөлісу сербесін іске қосыңыз."), + ("Account", "Есепкі"), + ("Overwrite", "Үстінен қайта жазу"), + ("This file exists, skip or overwrite this file?", "Бұл файыл бар, өткізіп жіберу әлде үстінен қайта жазу керек пе?"), + ("Quit", "Шығу"), + ("doc_mac_permission", ""), + ("Help", "Көмек"), + ("Failed", "Сәтсіз"), + ("Succeeded", "Сәтті"), + ("Someone turns on privacy mode, exit", "Біреу құпиялылық модасын қосты, шығу"), + ("Unsupported", "Қолдаусыз"), + ("Peer denied", "Пир қабылдамады"), + ("Please install plugins", "Плагиндерді орнатуды өтінеміз"), + ("Peer exit", "Пирдің шығуы"), + ("Failed to turn off", "Сөндіру сәтсіз болды"), + ("Turned off", "Өшірілген"), + ("In privacy mode", "Құпиялылық модасында"), + ("Out privacy mode", "Құпиялылық модасынан Шығу"), + ("Language", "Тіл"), + ("Keep RustDesk background service", "Артжақтағы RustDesk сербесін сақтап тұру"), + ("Ignore Battery Optimizations", "Бәтері Оңтайландыруларын Елемеу"), + ("android_open_battery_optimizations_tip", "Егер де бұл ерекшелікті өшіруді қаласаңыз, келесі RustDesk апылқат орнатпалары бетіне барып, [Бәтері]'ні тауып кіріңіз де [Шектеусіз]'ден құсбелгіні алып тастауды өтінеміз"), + ("Connection not allowed", "Қосылу рұқсат етілмеген"), + ("Use temporary password", "Уақытша құпия сөзді қолдану"), + ("Use permanent password", "Тұрақты құпия сөзді қолдану"), + ("Use both passwords", "Қос құпия сөзді қолдану"), + ("Set permanent password", "Тұрақты құпия сөзді орнату"), + ("Set temporary password length", "Уақытша құпия сөздің ұзындығын орнату"), + ("Enable Remote Restart", "Қашықтан қайта-қосуды іске қосу"), + ("Allow remote restart", "Қашықтан қайта-қосуды рұқсат ету"), + ("Restart Remote Device", "Қашықтағы құрылғыны қайта-қосу"), + ("Are you sure you want to restart", "Қайта-қосуға сенімдісіз бе?"), + ("Restarting Remote Device", "Қашықтағы Құрылғыны қайта-қосуда"), + ("remote_restarting_tip", "Қашықтағы құрылғы қайта-қосылуда, бұл хабар терезесін жабып, біраздан соң тұрақты құпия сөзбен қайта қосылуды өтінеміз"), + ("Copied", "Көшірілді"), + ("Exit Fullscreen", "Толық екіреннен Шығу"), + ("Fullscreen", "Толық екірен"), + ("Mobile Actions", "Мабыл Әрекеттері"), + ("Select Monitor", "Мониторды Таңдау"), + ("Control Actions", "Басқару Әрекеттері"), + ("Display Settings", "Дисплей Орнатпалары"), + ("Ratio", "Арақатынас"), + ("Image Quality", "Сурет Сапасы"), + ("Scroll Style", "Scroll Теңшетұрі"), + ("Show Menubar", "Мәзір жолағын көрсету"), + ("Hide Menubar", "Мәзір жолағын жасыру"), + ("Direct Connection", "Тікелей Қосылым"), + ("Relay Connection", "Релай Қосылым"), + ("Secure Connection", "Қауіпсіз Қосылым"), + ("Insecure Connection", "Қатерлі Қосылым"), + ("Scale original", "Scale original"), + ("Scale adaptive", "Scale adaptive"), + ].iter().cloned().collect(); + } From bec8daafb95823ce17a704a8d2788cf5340127e9 Mon Sep 17 00:00:00 2001 From: asur4s Date: Fri, 2 Sep 2022 15:29:25 -0400 Subject: [PATCH 0366/2015] Fix simulate in wayland --- flutter/pubspec.lock | 38 ++++---- flutter/pubspec.yaml | 2 +- libs/hbb_common/src/platform/linux.rs | 5 + src/server/input_service.rs | 126 ++++++++++++++------------ 4 files changed, 93 insertions(+), 78 deletions(-) diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index a6dade189..d9205b76c 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -49,7 +49,7 @@ packages: name: async url: "https://pub.flutter-io.cn" source: hosted - version: "2.8.2" + version: "2.9.0" back_button_interceptor: dependency: "direct main" description: @@ -147,7 +147,7 @@ packages: name: characters url: "https://pub.flutter-io.cn" source: hosted - version: "1.2.0" + version: "1.2.1" charcode: dependency: transitive description: @@ -168,7 +168,7 @@ packages: name: clock url: "https://pub.flutter-io.cn" source: hosted - version: "1.1.0" + version: "1.1.1" code_builder: dependency: transitive description: @@ -324,7 +324,7 @@ packages: name: fake_async url: "https://pub.flutter-io.cn" source: hosted - version: "1.3.0" + version: "1.3.1" ffi: dependency: "direct main" description: @@ -621,14 +621,14 @@ packages: name: matcher url: "https://pub.flutter-io.cn" source: hosted - version: "0.12.11" + version: "0.12.12" material_color_utilities: dependency: transitive description: name: material_color_utilities url: "https://pub.flutter-io.cn" source: hosted - version: "0.1.4" + version: "0.1.5" menu_base: dependency: transitive description: @@ -642,7 +642,7 @@ packages: name: meta url: "https://pub.flutter-io.cn" source: hosted - version: "1.7.0" + version: "1.8.0" mime: dependency: transitive description: @@ -719,7 +719,7 @@ packages: name: path url: "https://pub.flutter-io.cn" source: hosted - version: "1.8.1" + version: "1.8.2" path_provider: dependency: "direct main" description: @@ -856,9 +856,11 @@ packages: screen_retriever: dependency: transitive description: - name: screen_retriever - url: "https://pub.flutter-io.cn" - source: hosted + path: "." + ref: "406b9b0" + resolved-ref: "406b9b038b2c1d779f1e7bf609c8c248be247372" + url: "https://github.com/Kingtous/rustdesk_screen_retriever.git" + source: git version: "0.1.2" scroll_pos: dependency: "direct main" @@ -969,7 +971,7 @@ packages: name: source_span url: "https://pub.flutter-io.cn" source: hosted - version: "1.8.2" + version: "1.9.0" sqflite: dependency: transitive description: @@ -1011,7 +1013,7 @@ packages: name: string_scanner url: "https://pub.flutter-io.cn" source: hosted - version: "1.1.0" + version: "1.1.1" synchronized: dependency: transitive description: @@ -1025,14 +1027,14 @@ packages: name: term_glyph url: "https://pub.flutter-io.cn" source: hosted - version: "1.2.0" + version: "1.2.1" test_api: dependency: transitive description: name: test_api url: "https://pub.flutter-io.cn" source: hosted - version: "0.4.9" + version: "0.4.12" timing: dependency: transitive description: @@ -1242,11 +1244,11 @@ packages: dependency: "direct main" description: path: "." - ref: "799ef079e87938c3f4340591b4330c2598f38bb9" - resolved-ref: "799ef079e87938c3f4340591b4330c2598f38bb9" + ref: "4627ba808ed08ff0c08706b01a7f9cc8b747accd" + resolved-ref: "4627ba808ed08ff0c08706b01a7f9cc8b747accd" url: "https://github.com/Kingtous/rustdesk_window_manager" source: git - version: "0.2.6" + version: "0.2.7" xdg_directories: dependency: transitive description: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index fecef77e6..f2d038af3 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -58,7 +58,7 @@ dependencies: window_manager: git: url: https://github.com/Kingtous/rustdesk_window_manager - ref: 799ef079e87938c3f4340591b4330c2598f38bb9 + ref: 4627ba808ed08ff0c08706b01a7f9cc8b747accd desktop_multi_window: git: url: https://github.com/Kingtous/rustdesk_desktop_multi_window diff --git a/libs/hbb_common/src/platform/linux.rs b/libs/hbb_common/src/platform/linux.rs index 865033204..1a41ebad6 100644 --- a/libs/hbb_common/src/platform/linux.rs +++ b/libs/hbb_common/src/platform/linux.rs @@ -1,4 +1,9 @@ use crate::ResultType; +use std::sync::Mutex; + +lazy_static::lazy_static! { + pub static ref IS_X11: Mutex = Mutex::new("x11" == get_display_server()); +} pub fn get_display_server() -> String { let session = get_value_of_seat0(0); diff --git a/src/server/input_service.rs b/src/server/input_service.rs index e0707bded..f8edef399 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -2,6 +2,7 @@ use super::*; #[cfg(target_os = "macos")] use dispatch::Queue; use enigo::{Enigo, Key, KeyboardControllable, MouseButton, MouseControllable}; +use hbb_common::platform::linux::IS_X11; use hbb_common::{config::COMPRESS_LEVEL, protobuf::EnumOrUnknown}; use rdev::{simulate, EventType, Key as RdevKey}; use std::{ @@ -673,13 +674,13 @@ fn tfc_key_down_or_up(key: Key, down: bool, up: bool) { if let Key::Layout(chr) = key { log::info!("tfc_key_down_or_up :{:?}", chr); if down { - if let Err(_) = TFC_CONTEXT.lock().unwrap().unicode_char_down(chr){ + if let Err(_) = TFC_CONTEXT.lock().unwrap().unicode_char_down(chr) { log::error!("Failed to press char {:?}", chr); }; } if up { - if let Err(_) = TFC_CONTEXT.lock().unwrap().unicode_char_down(chr){ - log::error!("Failed to press char {:?}",chr); + if let Err(_) = TFC_CONTEXT.lock().unwrap().unicode_char_down(chr) { + log::error!("Failed to press char {:?}", chr); }; } return; @@ -753,12 +754,12 @@ fn tfc_key_down_or_up(key: Key, down: bool, up: bool) { log::info!("tfc_key_down_or_up: {:?}", key); if down { - if let Err(_) = TFC_CONTEXT.lock().unwrap().key_down(key){ + if let Err(_) = TFC_CONTEXT.lock().unwrap().key_down(key) { log::error!("Failed to press char {:?}", key); }; } if up { - if let Err(_) = TFC_CONTEXT.lock().unwrap().key_up(key){ + if let Err(_) = TFC_CONTEXT.lock().unwrap().key_up(key) { log::error!("Failed to press char {:?}", key); }; } @@ -771,16 +772,18 @@ fn legacy_keyboard_mode(evt: &KeyEvent) { crate::platform::windows::try_change_desktop(); let mut en = ENIGO.lock().unwrap(); if click_capslock { - #[cfg(target_os = "linux")] - tfc_key_down_or_up(Key::CapsLock, true, true); - #[cfg(not(target_os = "linux"))] - en.key_click(Key::CapsLock); + if *IS_X11.lock().unwrap() { + tfc_key_down_or_up(Key::CapsLock, true, true); + } else { + en.key_click(Key::CapsLock); + } } if click_numlock { - #[cfg(target_os = "linux")] - tfc_key_down_or_up(Key::NumLock, true, true); - #[cfg(not(target_os = "linux"))] - en.key_click(Key::NumLock); + if *IS_X11.lock().unwrap() { + tfc_key_down_or_up(Key::NumLock, true, true); + } else { + en.key_click(Key::NumLock); + } } // disable numlock if press home etc when numlock is on, // because we will get numpad value (7,8,9 etc) if not @@ -834,10 +837,11 @@ fn legacy_keyboard_mode(evt: &KeyEvent) { } } else { if !get_modifier_state(key.clone(), &mut en) { - #[cfg(target_os = "linux")] - tfc_key_down_or_up(key.clone(), true, false); - #[cfg(not(target_os = "linux"))] - en.key_down(key.clone()).ok(); + if *IS_X11.lock().unwrap() { + tfc_key_down_or_up(key.clone(), true, false); + } else { + en.key_down(key.clone()).ok(); + } modifier_sleep(); to_release.push(key); } @@ -848,12 +852,12 @@ fn legacy_keyboard_mode(evt: &KeyEvent) { } #[cfg(not(target_os = "macos"))] if has_cap != en.get_key_state(Key::CapsLock) { - #[cfg(target_os = "linux")] - tfc_key_down_or_up(Key::CapsLock, true, true); - #[cfg(not(target_os = "linux"))] - en.key_down(Key::CapsLock).ok(); - #[cfg(not(target_os = "linux"))] - en.key_up(Key::CapsLock); + if *IS_X11.lock().unwrap() { + tfc_key_down_or_up(Key::CapsLock, true, true); + } else { + en.key_down(Key::CapsLock).ok(); + en.key_up(Key::CapsLock); + } } #[cfg(windows)] if crate::common::valid_for_numlock(evt) { @@ -874,19 +878,21 @@ fn legacy_keyboard_mode(evt: &KeyEvent) { } } if evt.down { - #[cfg(target_os = "linux")] - tfc_key_down_or_up(key.clone(), true, false); - #[cfg(not(target_os = "linux"))] - allow_err!(en.key_down(key.clone())); + if *IS_X11.lock().unwrap() { + tfc_key_down_or_up(key.clone(), true, false); + } else { + allow_err!(en.key_down(key.clone())); + } KEYS_DOWN .lock() .unwrap() .insert(ck.value() as _, Instant::now()); } else { - #[cfg(target_os = "linux")] - tfc_key_down_or_up(key.clone(), false, true); - #[cfg(not(target_os = "linux"))] - en.key_up(key.clone()); + if *IS_X11.lock().unwrap() { + tfc_key_down_or_up(key.clone(), false, true); + } else { + en.key_up(key.clone()); + } KEYS_DOWN.lock().unwrap().remove(&(ck.value() as _)); } } else if ck.value() == ControlKey::CtrlAltDel.value() { @@ -900,35 +906,36 @@ fn legacy_keyboard_mode(evt: &KeyEvent) { } Some(key_event::Union::Chr(chr)) => { if evt.down { - #[cfg(target_os = "linux")] - tfc_key_down_or_up(get_layout(chr), true, false); - #[cfg(target_os = "linux")] + if *IS_X11.lock().unwrap() { + tfc_key_down_or_up(get_layout(chr), true, false); + } else { + if en.key_down(get_layout(chr)).is_ok() { + KEYS_DOWN + .lock() + .unwrap() + .insert(chr as u64 + KEY_CHAR_START, Instant::now()); + } else { + if let Ok(chr) = char::try_from(chr) { + let mut x = chr.to_string(); + if get_modifier_state(Key::Shift, &mut en) + || get_modifier_state(Key::CapsLock, &mut en) + { + x = x.to_uppercase(); + } + en.key_sequence(&x); + } + } + } KEYS_DOWN .lock() .unwrap() .insert(chr as u64 + KEY_CHAR_START, Instant::now()); - #[cfg(not(target_os = "linux"))] - if en.key_down(get_layout(chr)).is_ok() { - KEYS_DOWN - .lock() - .unwrap() - .insert(chr as u64 + KEY_CHAR_START, Instant::now()); - } else { - if let Ok(chr) = char::try_from(chr) { - let mut x = chr.to_string(); - if get_modifier_state(Key::Shift, &mut en) - || get_modifier_state(Key::CapsLock, &mut en) - { - x = x.to_uppercase(); - } - en.key_sequence(&x); - } - } } else { - #[cfg(target_os = "linux")] - tfc_key_down_or_up(get_layout(chr), false, true); - #[cfg(not(target_os = "linux"))] - en.key_up(get_layout(chr)); + if *IS_X11.lock().unwrap() { + tfc_key_down_or_up(get_layout(chr), false, true); + } else { + en.key_up(get_layout(chr)); + } KEYS_DOWN .lock() .unwrap() @@ -947,10 +954,11 @@ fn legacy_keyboard_mode(evt: &KeyEvent) { } #[cfg(not(target_os = "macos"))] for key in to_release { - #[cfg(target_os = "linux")] - tfc_key_down_or_up(key.clone(), false, true); - #[cfg(not(target_os = "linux"))] - en.key_up(key.clone()); + if *IS_X11.lock().unwrap() { + tfc_key_down_or_up(key.clone(), false, true); + } else { + en.key_up(key.clone()); + } } #[cfg(windows)] if disable_numlock { From 40534fd79f23f3b5879d15af1d28d420d70b5f4a Mon Sep 17 00:00:00 2001 From: asur4s Date: Fri, 2 Sep 2022 19:54:53 -0400 Subject: [PATCH 0367/2015] Fix ci --- build.rs | 25 ++----------------------- 1 file changed, 2 insertions(+), 23 deletions(-) diff --git a/build.rs b/build.rs index 9b353b42e..01fe8ada1 100644 --- a/build.rs +++ b/build.rs @@ -41,16 +41,6 @@ fn build_rc_source() { .unwrap(); } -fn check_environment() { - // Check env variable - let env_list = vec!["LLVM_HOME", "VCPKG_ROOT"]; - for env in env_list.iter() { - if std::env::var(env).is_err() { - panic!("Missing environment variable: {:?}", env); - }; - } -} - fn install_oboe() { let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); if target_os != "android" { @@ -87,19 +77,10 @@ fn install_oboe() { } fn gen_flutter_rust_bridge() { - // Get dependent of flutter - if !std::path::Path::new("./flutter/.packages").exists() { - std::process::Command::new("flutter") - .args(["pub", "get"]) - .current_dir("./flutter") - .output() - .expect("Failed to execute flutter pub get"); - }; let llvm_path = match std::env::var("LLVM_HOME") { Ok(path) => Some(vec![path]), - Err(_) => panic!("Failure to get environments"), + Err(_) => None, }; - // Tell Cargo that if the given file changes, to rerun this build script. println!("cargo:rerun-if-changed=src/flutter_ffi.rs"); // settings for fbr_codegen @@ -119,8 +100,6 @@ fn gen_flutter_rust_bridge() { } fn main() { - check_environment(); - hbb_common::gen_version(); install_oboe(); // there is problem with cfg(target_os) in build.rs, so use our workaround @@ -137,4 +116,4 @@ fn main() { build_windows(); #[cfg(target_os = "macos")] println!("cargo:rustc-link-lib=framework=ApplicationServices"); -} +} \ No newline at end of file From 11c5364e71179c59e8aefaa53a26fac0f5c6a364 Mon Sep 17 00:00:00 2001 From: fufesou Date: Sat, 3 Sep 2022 10:39:33 +0800 Subject: [PATCH 0368/2015] flutter_desktop: fix cursor, mid commit Signed-off-by: fufesou --- flutter/lib/common/shared_state.dart | 89 ++++++- flutter/lib/desktop/pages/remote_page.dart | 231 +++++++++--------- .../lib/desktop/widgets/remote_menubar.dart | 25 +- flutter/lib/models/model.dart | 61 ++--- 4 files changed, 242 insertions(+), 164 deletions(-) diff --git a/flutter/lib/common/shared_state.dart b/flutter/lib/common/shared_state.dart index 7232cb6ad..67752d888 100644 --- a/flutter/lib/common/shared_state.dart +++ b/flutter/lib/common/shared_state.dart @@ -1,16 +1,26 @@ import 'package:get/get.dart'; import '../consts.dart'; +import '../models/platform_model.dart'; class PrivacyModeState { static String tag(String id) => 'privacy_mode_$id'; static void init(String id) { - final RxBool state = false.obs; - Get.put(state, tag: tag(id)); + final key = tag(id); + if (!Get.isRegistered(tag: key)) { + final RxBool state = false.obs; + Get.put(state, tag: key); + } + } + + static void delete(String id) { + final key = tag(id); + if (Get.isRegistered(tag: key)) { + Get.delete(tag: key); + } } - static void delete(String id) => Get.delete(tag: tag(id)); static RxBool find(String id) => Get.find(tag: tag(id)); } @@ -18,11 +28,20 @@ class BlockInputState { static String tag(String id) => 'block_input_$id'; static void init(String id) { - final RxBool state = false.obs; - Get.put(state, tag: tag(id)); + final key = tag(id); + if (!Get.isRegistered(tag: key)) { + final RxBool state = false.obs; + Get.put(state, tag: key); + } + } + + static void delete(String id) { + final key = tag(id); + if (Get.isRegistered(tag: key)) { + Get.delete(tag: key); + } } - static void delete(String id) => Get.delete(tag: tag(id)); static RxBool find(String id) => Get.find(tag: tag(id)); } @@ -30,11 +49,20 @@ class CurrentDisplayState { static String tag(String id) => 'current_display_$id'; static void init(String id) { - final RxInt state = RxInt(0); - Get.put(state, tag: tag(id)); + final key = tag(id); + if (!Get.isRegistered(tag: key)) { + final RxInt state = RxInt(0); + Get.put(state, tag: key); + } + } + + static void delete(String id) { + final key = tag(id); + if (Get.isRegistered(tag: key)) { + Get.delete(tag: key); + } } - static void delete(String id) => Get.delete(tag: tag(id)); static RxInt find(String id) => Get.find(tag: tag(id)); } @@ -85,3 +113,46 @@ class ConnectionTypeState { static ConnectionType find(String id) => Get.find(tag: tag(id)); } + +class ShowRemoteCursorState { + static String tag(String id) => 'show_remote_cursor_$id'; + + static void init(String id) { + final key = tag(id); + if (!Get.isRegistered(tag: key)) { + final RxBool state = false.obs; + Get.put(state, tag: key); + } + } + + static void delete(String id) { + final key = tag(id); + if (Get.isRegistered(tag: key)) { + Get.delete(tag: key); + } + } + + static RxBool find(String id) => Get.find(tag: tag(id)); +} + +class KeyboardEnabledState { + static String tag(String id) => 'keyboard_enabled_$id'; + + static void init(String id) { + final key = tag(id); + if (!Get.isRegistered(tag: key)) { + // Server side, default true + final RxBool state = true.obs; + Get.put(state, tag: key); + } + } + + static void delete(String id) { + final key = tag(id); + if (Get.isRegistered(tag: key)) { + Get.delete(tag: key); + } + } + + static RxBool find(String id) => Get.find(tag: tag(id)); +} diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index a245f1f12..23e4e4900 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -40,6 +40,8 @@ class _RemotePageState extends State Timer? _timer; String _value = ''; final _cursorOverImage = false.obs; + late RxBool _showRemoteCursor; + late RxBool _keyboardEnabled; final FocusNode _mobileFocusNode = FocusNode(); final FocusNode _physicalFocusNode = FocusNode(); @@ -56,17 +58,24 @@ class _RemotePageState extends State PrivacyModeState.init(id); BlockInputState.init(id); CurrentDisplayState.init(id); + KeyboardEnabledState.init(id); + ShowRemoteCursorState.init(id); + _showRemoteCursor = ShowRemoteCursorState.find(id); + _keyboardEnabled = KeyboardEnabledState.find(id); } void _removeStates(String id) { PrivacyModeState.delete(id); BlockInputState.delete(id); CurrentDisplayState.delete(id); + ShowRemoteCursorState.delete(id); + KeyboardEnabledState.delete(id); } @override void initState() { super.initState(); + _initStates(widget.id); _ffi = FFI(); _updateTabBarHeight(); Get.put(_ffi, tag: widget.id); @@ -84,7 +93,8 @@ class _RemotePageState extends State _ffi.listenToMouse(true); _ffi.qualityMonitorModel.checkShowQualityMonitor(widget.id); // WindowManager.instance.addListener(this); - _initStates(widget.id); + _showRemoteCursor.value = bind.sessionGetToggleOptionSync( + id: widget.id, arg: 'show-remote-cursor'); } @override @@ -197,8 +207,7 @@ class _RemotePageState extends State _ffi.inputKey(label, down: down, press: press ?? false); } - Widget buildBody(BuildContext context, FfiModel ffiModel) { - final keyboard = ffiModel.permissions['keyboard'] != false; + Widget buildBody(BuildContext context) { return Scaffold( backgroundColor: MyTheme.color(context).bg, body: Overlay( @@ -208,8 +217,7 @@ class _RemotePageState extends State _ffi.dialogManager.setOverlayState(Overlay.of(context)); return Container( color: Colors.black, - child: getRawPointerAndKeyBody( - getBodyForDesktop(context, keyboard))); + child: getRawPointerAndKeyBody(getBodyForDesktop(context))); }) ], )); @@ -224,70 +232,61 @@ class _RemotePageState extends State clientClose(_ffi.dialogManager); return false; }, - child: MultiProvider( - providers: [ - ChangeNotifierProvider.value(value: _ffi.ffiModel), - ChangeNotifierProvider.value(value: _ffi.imageModel), - ChangeNotifierProvider.value(value: _ffi.cursorModel), - ChangeNotifierProvider.value(value: _ffi.canvasModel), - ], - child: Consumer( - builder: (context, ffiModel, child) => - buildBody(context, ffiModel)))); + child: MultiProvider(providers: [ + ChangeNotifierProvider.value(value: _ffi.ffiModel), + ChangeNotifierProvider.value(value: _ffi.imageModel), + ChangeNotifierProvider.value(value: _ffi.cursorModel), + ChangeNotifierProvider.value(value: _ffi.canvasModel), + ], child: buildBody(context))); } Widget getRawPointerAndKeyBody(Widget child) { - return Consumer( - builder: (context, FfiModel, _child) => MouseRegion( - cursor: FfiModel.permissions['keyboard'] != false - ? SystemMouseCursors.none - : MouseCursor.defer, - child: FocusScope( - autofocus: true, - child: Focus( - autofocus: true, - canRequestFocus: true, - focusNode: _physicalFocusNode, - onFocusChange: (bool v) { - _imageFocused = v; - }, - onKey: (data, e) { - final key = e.logicalKey; - if (e is RawKeyDownEvent) { - if (e.repeat) { - sendRawKey(e, press: true); - } else { - if (e.isAltPressed && !_ffi.alt) { - _ffi.alt = true; - } else if (e.isControlPressed && !_ffi.ctrl) { - _ffi.ctrl = true; - } else if (e.isShiftPressed && !_ffi.shift) { - _ffi.shift = true; - } else if (e.isMetaPressed && !_ffi.command) { - _ffi.command = true; - } - sendRawKey(e, down: true); - } - } - if (e is RawKeyUpEvent) { - if (key == LogicalKeyboardKey.altLeft || - key == LogicalKeyboardKey.altRight) { - _ffi.alt = false; - } else if (key == LogicalKeyboardKey.controlLeft || - key == LogicalKeyboardKey.controlRight) { - _ffi.ctrl = false; - } else if (key == LogicalKeyboardKey.shiftRight || - key == LogicalKeyboardKey.shiftLeft) { - _ffi.shift = false; - } else if (key == LogicalKeyboardKey.metaLeft || - key == LogicalKeyboardKey.metaRight) { - _ffi.command = false; - } - sendRawKey(e); - } - return KeyEventResult.handled; - }, - child: child)))); + return FocusScope( + autofocus: true, + child: Focus( + autofocus: true, + canRequestFocus: true, + focusNode: _physicalFocusNode, + onFocusChange: (bool v) { + _imageFocused = v; + }, + onKey: (data, e) { + final key = e.logicalKey; + if (e is RawKeyDownEvent) { + if (e.repeat) { + sendRawKey(e, press: true); + } else { + if (e.isAltPressed && !_ffi.alt) { + _ffi.alt = true; + } else if (e.isControlPressed && !_ffi.ctrl) { + _ffi.ctrl = true; + } else if (e.isShiftPressed && !_ffi.shift) { + _ffi.shift = true; + } else if (e.isMetaPressed && !_ffi.command) { + _ffi.command = true; + } + sendRawKey(e, down: true); + } + } + if (e is RawKeyUpEvent) { + if (key == LogicalKeyboardKey.altLeft || + key == LogicalKeyboardKey.altRight) { + _ffi.alt = false; + } else if (key == LogicalKeyboardKey.controlLeft || + key == LogicalKeyboardKey.controlRight) { + _ffi.ctrl = false; + } else if (key == LogicalKeyboardKey.shiftRight || + key == LogicalKeyboardKey.shiftLeft) { + _ffi.shift = false; + } else if (key == LogicalKeyboardKey.metaLeft || + key == LogicalKeyboardKey.metaRight) { + _ffi.command = false; + } + sendRawKey(e); + } + return KeyEventResult.handled; + }, + child: child)); } /// touchMode only: @@ -382,32 +381,30 @@ class _RemotePageState extends State child: child)); } - Widget getBodyForDesktop(BuildContext context, bool keyboard) { + Widget getBodyForDesktop(BuildContext context) { var paints = [ MouseRegion(onEnter: (evt) { bind.hostStopSystemKeyPropagate(stopped: false); }, onExit: (evt) { bind.hostStopSystemKeyPropagate(stopped: true); - }, child: Container( - child: LayoutBuilder(builder: (context, constraints) { - Future.delayed(Duration.zero, () { - Provider.of(context, listen: false).updateViewStyle(); - }); - return ImagePaint( - id: widget.id, - cursorOverImage: _cursorOverImage, - listenerBuilder: _buildImageListener, - ); - }), - )) + }, child: LayoutBuilder(builder: (context, constraints) { + Future.delayed(Duration.zero, () { + Provider.of(context, listen: false).updateViewStyle(); + }); + return ImagePaint( + id: widget.id, + cursorOverImage: _cursorOverImage, + keyboardEnabled: _keyboardEnabled, + listenerBuilder: _buildImageListener, + ); + })) ]; - final cursor = bind.sessionGetToggleOptionSync( - id: widget.id, arg: 'show-remote-cursor'); - if (keyboard || cursor) { - paints.add(CursorPaint( - id: widget.id, - )); - } + + paints.add(Obx(() => Visibility( + visible: _keyboardEnabled.isTrue || _showRemoteCursor.isTrue, + child: CursorPaint( + id: widget.id, + )))); paints.add(QualityMonitor(_ffi.qualityMonitorModel)); paints.add(RemoteMenubar( id: widget.id, @@ -447,7 +444,7 @@ class _RemotePageState extends State _ffi.canvasModel.updateViewStyle(); break; case 'maximize': - Future.delayed(Duration(milliseconds: 100), () { + Future.delayed(const Duration(milliseconds: 100), () { _ffi.canvasModel.updateViewStyle(); }); break; @@ -461,6 +458,7 @@ class _RemotePageState extends State class ImagePaint extends StatelessWidget { final String id; final Rx cursorOverImage; + final Rx keyboardEnabled; final Widget Function(Widget)? listenerBuilder; final ScrollController _horizontal = ScrollController(); final ScrollController _vertical = ScrollController(); @@ -469,6 +467,7 @@ class ImagePaint extends StatelessWidget { {Key? key, required this.id, required this.cursorOverImage, + required this.keyboardEnabled, this.listenerBuilder}) : super(key: key); @@ -485,25 +484,26 @@ class ImagePaint extends StatelessWidget { painter: ImagePainter(image: m.image, x: 0, y: 0, scale: s), )); return Center( - child: NotificationListener( - onNotification: (notification) { - final percentX = _horizontal.position.extentBefore / - (_horizontal.position.extentBefore + - _horizontal.position.extentInside + - _horizontal.position.extentAfter); - final percentY = _vertical.position.extentBefore / - (_vertical.position.extentBefore + - _vertical.position.extentInside + - _vertical.position.extentAfter); - c.setScrollPercent(percentX, percentY); - return false; - }, - child: Obx(() => MouseRegion( - cursor: cursorOverImage.value - ? SystemMouseCursors.none - : SystemMouseCursors.basic, - child: _buildCrossScrollbar(_buildListener(imageWidget)))), - )); + child: NotificationListener( + onNotification: (notification) { + final percentX = _horizontal.position.extentBefore / + (_horizontal.position.extentBefore + + _horizontal.position.extentInside + + _horizontal.position.extentAfter); + final percentY = _vertical.position.extentBefore / + (_vertical.position.extentBefore + + _vertical.position.extentInside + + _vertical.position.extentAfter); + c.setScrollPercent(percentX, percentY); + return false; + }, + child: Obx(() => MouseRegion( + cursor: (keyboardEnabled.isTrue && cursorOverImage.isTrue) + ? SystemMouseCursors.none + : MouseCursor.defer, + child: _buildCrossScrollbar(_buildListener(imageWidget)))), + ), + ); } else { final imageWidget = SizedBox( width: c.size.width, @@ -562,13 +562,12 @@ class CursorPaint extends StatelessWidget { final m = Provider.of(context); final c = Provider.of(context); // final adjust = m.adjustForKeyboard(); - var s = c.scale; return CustomPaint( painter: ImagePainter( image: m.image, - x: m.x * s - m.hotx + c.x, - y: m.y * s - m.hoty + c.y, - scale: 1), + x: m.x - m.hotx + c.x / c.scale, + y: m.y - m.hoty + c.y / c.scale, + scale: c.scale), ); } } @@ -620,30 +619,30 @@ class QualityMonitor extends StatelessWidget { right: 10, child: qualityMonitorModel.show ? Container( - padding: EdgeInsets.all(8), + padding: const EdgeInsets.all(8), color: MyTheme.canvasColor.withAlpha(120), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "Speed: ${qualityMonitorModel.data.speed ?? ''}", - style: TextStyle(color: MyTheme.grayBg), + style: const TextStyle(color: MyTheme.grayBg), ), Text( "FPS: ${qualityMonitorModel.data.fps ?? ''}", - style: TextStyle(color: MyTheme.grayBg), + style: const TextStyle(color: MyTheme.grayBg), ), Text( "Delay: ${qualityMonitorModel.data.delay ?? ''} ms", - style: TextStyle(color: MyTheme.grayBg), + style: const TextStyle(color: MyTheme.grayBg), ), Text( "Target Bitrate: ${qualityMonitorModel.data.targetBitrate ?? ''}kb", - style: TextStyle(color: MyTheme.grayBg), + style: const TextStyle(color: MyTheme.grayBg), ), Text( "Codec: ${qualityMonitorModel.data.codecFormat ?? ''}", - style: TextStyle(color: MyTheme.grayBg), + style: const TextStyle(color: MyTheme.grayBg), ), ], ), diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index dbe7592e6..dc3c249f0 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -522,16 +522,19 @@ class _RemoteMenubarState extends State { } }), MenuEntryDivider(), - MenuEntrySwitch( - text: translate('Show remote cursor'), - getter: () async { - return bind.sessionGetToggleOptionSync( - id: widget.id, arg: 'show-remote-cursor'); - }, - setter: (bool v) async { - await bind.sessionToggleOption( - id: widget.id, value: 'show-remote-cursor'); - }), + () { + final state = ShowRemoteCursorState.find(widget.id); + return MenuEntrySwitch2( + text: translate('Show remote cursor'), + getter: () { + return state; + }, + setter: (bool v) async { + state.value = v; + await bind.sessionToggleOption( + id: widget.id, value: 'show-remote-cursor'); + }); + }(), MenuEntrySwitch( text: translate('Show quality monitor'), getter: () async { @@ -560,12 +563,12 @@ class _RemoteMenubarState extends State { 'Lock after session end', 'lock-after-session-end')); if (pi.platform == 'Windows') { displayMenu.add(MenuEntrySwitch2( + dismissOnClicked: true, text: translate('Privacy mode'), getter: () { return PrivacyModeState.find(widget.id); }, setter: (bool v) async { - Navigator.pop(context); await bind.sessionToggleOption( id: widget.id, value: 'privacy-mode'); })); diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 887bf7d35..384d7692a 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -32,7 +32,7 @@ class FfiModel with ChangeNotifier { Display _display = Display(); var _inputBlocked = false; - final _permissions = Map(); + final _permissions = {}; bool? _secure; bool? _direct; bool _touchMode = false; @@ -71,12 +71,13 @@ class FfiModel with ChangeNotifier { } } - void updatePermission(Map evt) { + void updatePermission(Map evt, String id) { evt.forEach((k, v) { if (k == 'name' || k.isEmpty) return; _permissions[k] = v == 'true'; }); - print('$_permissions'); + KeyboardEnabledState.find(id).value = _permissions['keyboard'] != false; + debugPrint('$_permissions'); notifyListeners(); } @@ -146,7 +147,7 @@ class FfiModel with ChangeNotifier { } else if (name == 'clipboard') { Clipboard.setData(ClipboardData(text: evt['content'])); } else if (name == 'permission') { - parent.target?.ffiModel.updatePermission(evt); + parent.target?.ffiModel.updatePermission(evt, peerId); } else if (name == 'chat_client_mode') { parent.target?.chatModel .receive(ChatModel.clientModeID, evt['text'] ?? ""); @@ -185,7 +186,7 @@ class FfiModel with ChangeNotifier { /// Bind the event listener to receive events from the Rust core. void updateEventListener(String peerId) { - final void Function(Map) cb = (evt) { + cb(evt) { var name = evt['name']; if (name == 'msgbox') { handleMsgBox(evt, peerId); @@ -205,7 +206,7 @@ class FfiModel with ChangeNotifier { } else if (name == 'clipboard') { Clipboard.setData(ClipboardData(text: evt['content'])); } else if (name == 'permission') { - parent.target?.ffiModel.updatePermission(evt); + parent.target?.ffiModel.updatePermission(evt, peerId); } else if (name == 'chat_client_mode') { parent.target?.chatModel .receive(ChatModel.clientModeID, evt['text'] ?? ""); @@ -239,7 +240,8 @@ class FfiModel with ChangeNotifier { } else if (name == 'update_privacy_mode') { updatePrivacyMode(evt, peerId); } - }; + } + platformFFI.setEventCallback(cb); } @@ -321,7 +323,7 @@ class FfiModel with ChangeNotifier { if (isPeerAndroid) { _touchMode = true; if (parent.target?.ffiModel.permissions['keyboard'] != false) { - Timer(Duration(milliseconds: 100), showMobileActionsOverlay); + Timer(const Duration(milliseconds: 100), showMobileActionsOverlay); } } else { _touchMode = @@ -464,15 +466,20 @@ enum ScrollStyle { } class CanvasModel with ChangeNotifier { + // image offset of canvas + double _x = 0; + // image offset of canvas + double _y = 0; + // image scale + double _scale = 1.0; + // the tabbar over the image + double tabBarHeight = 0.0; + // TODO multi canvas model + String id = ""; // scroll offset x percent double _scrollX = 0.0; // scroll offset y percent double _scrollY = 0.0; - double _x = 0; - double _y = 0; - double _scale = 1.0; - double _tabBarHeight = 0.0; - String id = ""; // TODO multi canvas model ScrollStyle _scrollStyle = ScrollStyle.scrollauto; WeakReference parent; @@ -492,9 +499,6 @@ class CanvasModel with ChangeNotifier { double get scrollX => _scrollX; double get scrollY => _scrollY; - set tabBarHeight(double h) => _tabBarHeight = h; - double get tabBarHeight => _tabBarHeight; - void updateViewStyle() async { final style = await bind.sessionGetOption(id: id, arg: 'view-style'); if (style == null) { @@ -548,12 +552,11 @@ class CanvasModel with ChangeNotifier { Size get size { final size = MediaQueryData.fromWindow(ui.window).size; - return Size(size.width, size.height - _tabBarHeight); + return Size(size.width, size.height - tabBarHeight); } void moveDesktopMouse(double x, double y) { // On mobile platforms, move the canvas with the cursor. - //if (!isDesktop) { final dw = getDisplayWidth() * _scale; final dh = getDisplayHeight() * _scale; var dxOffset = 0; @@ -579,8 +582,13 @@ class CanvasModel with ChangeNotifier { if (dxOffset != 0 || dyOffset != 0) { notifyListeners(); } - //} - parent.target?.cursorModel.moveLocal(x, y); + + // If keyboard is not permitted, do not move cursor when mouse is moving. + if (parent.target != null) { + if (parent.target!.ffiModel.keyboard()) { + parent.target!.cursorModel.moveLocal(x, y); + } + } } set scale(v) { @@ -597,11 +605,8 @@ class CanvasModel with ChangeNotifier { if (isWebDesktop) { updateViewStyle(); } else { - final size = MediaQueryData.fromWindow(ui.window).size; - final canvasWidth = size.width; - final canvasHeight = size.height - _tabBarHeight; - _x = (canvasWidth - getDisplayWidth() * _scale) / 2; - _y = (canvasHeight - getDisplayHeight() * _scale) / 2; + _x = (size.width - getDisplayWidth() * _scale) / 2; + _y = (size.height - getDisplayHeight() * _scale) / 2; } notifyListeners(); } @@ -613,7 +618,7 @@ class CanvasModel with ChangeNotifier { void updateScale(double v) { if (parent.target?.imageModel.image == null) return; - final offset = parent.target?.cursorModel.offset ?? Offset(0, 0); + final offset = parent.target?.cursorModel.offset ?? const Offset(0, 0); var r = parent.target?.cursorModel.getVisibleRect() ?? Rect.zero; final px0 = (offset.dx - r.left) * _scale; final py0 = (offset.dy - r.top) * _scale; @@ -640,7 +645,7 @@ class CanvasModel with ChangeNotifier { class CursorModel with ChangeNotifier { ui.Image? _image; - final _images = Map>(); + final _images = >{}; double _x = -10000; double _y = -10000; double _hotx = 0; @@ -807,7 +812,7 @@ class CursorModel with ChangeNotifier { // my throw exception, because the listener maybe already dispose notifyListeners(); } catch (e) { - print('notify cursor: $e'); + debugPrint('notify cursor: $e'); } }); } From 1b56304d9ad461c18b4ee4f9a08faf0036c4e011 Mon Sep 17 00:00:00 2001 From: 21pages Date: Fri, 2 Sep 2022 17:19:44 +0800 Subject: [PATCH 0369/2015] format id Signed-off-by: 21pages --- .../lib/common/formatter/id_formatter.dart | 52 +++++++++++++- .../lib/desktop/pages/connection_page.dart | 71 ++++++++++--------- .../lib/desktop/widgets/peercard_widget.dart | 5 +- flutter/lib/models/server_model.dart | 13 ++-- 4 files changed, 99 insertions(+), 42 deletions(-) diff --git a/flutter/lib/common/formatter/id_formatter.dart b/flutter/lib/common/formatter/id_formatter.dart index 29aea84ff..c7ce14da4 100644 --- a/flutter/lib/common/formatter/id_formatter.dart +++ b/flutter/lib/common/formatter/id_formatter.dart @@ -1,4 +1,52 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; -/// TODO: Divide every 3 number to display ID -class IdFormController extends TextEditingController {} +class IDTextEditingController extends TextEditingController { + IDTextEditingController({String? text}) : super(text: text); + + String get id => trimID(value.text); + + set id(String newID) => text = formatID(newID); +} + +class IDTextInputFormatter extends TextInputFormatter { + @override + TextEditingValue formatEditUpdate( + TextEditingValue oldValue, TextEditingValue newValue) { + if (newValue.text.isEmpty) { + return newValue.copyWith(text: ''); + } else if (newValue.text.compareTo(oldValue.text) == 0) { + return newValue; + } else { + int selectionIndexFromTheRight = + newValue.text.length - newValue.selection.extentOffset; + String newID = formatID(newValue.text); + return TextEditingValue( + text: newID, + selection: TextSelection.collapsed( + offset: newID.length - selectionIndexFromTheRight, + ), + ); + } + } +} + +String formatID(String id) { + String id2 = id.replaceAll(' ', ''); + String newID = ''; + if (id2.length <= 3) { + newID = id2; + } else { + var n = id2.length; + var a = n % 3 != 0 ? n % 3 : 3; + newID = id2.substring(0, a); + for (var i = a; i < n; i += 3) { + newID += " ${id2.substring(i, i + 3)}"; + } + } + return newID; +} + +String trimID(String id) { + return id.replaceAll(' ', ''); +} diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 5fd6b4a28..fe363c4c9 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -12,6 +12,7 @@ import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher_string.dart'; import '../../common.dart'; +import '../../common/formatter/id_formatter.dart'; import '../../mobile/pages/scan_page.dart'; import '../../mobile/pages/settings_page.dart'; import '../../models/model.dart'; @@ -30,7 +31,7 @@ class ConnectionPage extends StatefulWidget { /// State for the connection page. class _ConnectionPageState extends State { /// Controller for the id input bar. - final _idController = TextEditingController(); + final _idController = IDTextEditingController(); /// Update url. If it's not null, means an update is available. final _updateUrl = ''; @@ -43,9 +44,9 @@ class _ConnectionPageState extends State { if (_idController.text.isEmpty) { () async { final lastRemoteId = await bind.mainGetLastRemoteId(); - if (lastRemoteId != _idController.text) { + if (lastRemoteId != _idController.id) { setState(() { - _idController.text = lastRemoteId; + _idController.id = lastRemoteId; }); } }(); @@ -110,7 +111,7 @@ class _ConnectionPageState extends State { /// Callback for the connect button. /// Connects to the selected peer. void onConnect({bool isFileTransfer = false}) { - final id = _idController.text.trim(); + final id = _idController.id; connect(id, isFileTransfer: isFileTransfer); } @@ -185,35 +186,41 @@ class _ConnectionPageState extends State { Row( children: [ Expanded( - child: TextField( - autocorrect: false, - enableSuggestions: false, - keyboardType: TextInputType.visiblePassword, - style: TextStyle( - fontFamily: 'WorkSans', - fontSize: 22, - height: 1, - ), - decoration: InputDecoration( - hintText: translate('Enter Remote ID'), - hintStyle: TextStyle( - color: MyTheme.color(context).placeholder), - border: OutlineInputBorder( + child: Obx( + () => TextField( + autocorrect: false, + enableSuggestions: false, + keyboardType: TextInputType.visiblePassword, + focusNode: focusNode, + style: TextStyle( + fontFamily: 'WorkSans', + fontSize: 22, + height: 1, + ), + decoration: InputDecoration( + hintText: inputFocused.value + ? null + : translate('Enter Remote ID'), + hintStyle: TextStyle( + color: MyTheme.color(context).placeholder), + border: OutlineInputBorder( + borderRadius: BorderRadius.zero, + borderSide: BorderSide( + color: MyTheme.color(context).placeholder!)), + focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.zero, - borderSide: BorderSide( - color: MyTheme.color(context).placeholder!)), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.zero, - borderSide: - BorderSide(color: MyTheme.button, width: 3), - ), - isDense: true, - contentPadding: - EdgeInsets.symmetric(horizontal: 10, vertical: 12)), - controller: _idController, - onSubmitted: (s) { - onConnect(); - }, + borderSide: + BorderSide(color: MyTheme.button, width: 3), + ), + isDense: true, + contentPadding: EdgeInsets.symmetric( + horizontal: 10, vertical: 12)), + controller: _idController, + inputFormatters: [IDTextInputFormatter()], + onSubmitted: (s) { + onConnect(); + }, + ), ), ), ], diff --git a/flutter/lib/desktop/widgets/peercard_widget.dart b/flutter/lib/desktop/widgets/peercard_widget.dart index d9c0015f8..13cf02699 100644 --- a/flutter/lib/desktop/widgets/peercard_widget.dart +++ b/flutter/lib/desktop/widgets/peercard_widget.dart @@ -5,6 +5,7 @@ import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:get/get.dart'; import '../../common.dart'; +import '../../common/formatter/id_formatter.dart'; import '../../models/model.dart'; import '../../models/peer_model.dart'; import '../../models/platform_model.dart'; @@ -119,7 +120,7 @@ class _PeerCardState extends State<_PeerCard> ? Colors.green : Colors.yellow)), Text( - '${peer.id}', + formatID('${peer.id}'), style: TextStyle(fontWeight: FontWeight.w400), ), ]), @@ -240,7 +241,7 @@ class _PeerCardState extends State<_PeerCard> backgroundColor: peer.online ? Colors.green : Colors.yellow)), - Text(peer.id) + Text(formatID(peer.id)) ]).paddingSymmetric(vertical: 8), _actionMore(peer), ], diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index 31c579f83..5d23dc949 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -7,6 +7,7 @@ import 'package:flutter_hbb/models/platform_model.dart'; import 'package:wakelock/wakelock.dart'; import '../common.dart'; +import '../common/formatter/id_formatter.dart'; import '../desktop/pages/server_page.dart' as Desktop; import '../desktop/widgets/tabbar_widget.dart'; import '../mobile/pages/server_page.dart'; @@ -29,7 +30,7 @@ class ServerModel with ChangeNotifier { String _temporaryPasswordLength = ""; late String _emptyIdShow; - late final TextEditingController _serverId; + late final IDTextEditingController _serverId; final _serverPasswd = TextEditingController(text: ""); final tabController = DesktopTabController(tabType: DesktopTabType.cm); @@ -88,7 +89,7 @@ class ServerModel with ChangeNotifier { ServerModel(this.parent) { _emptyIdShow = translate("Generating ..."); - _serverId = TextEditingController(text: this._emptyIdShow); + _serverId = IDTextEditingController(text: _emptyIdShow); Timer.periodic(Duration(seconds: 1), (timer) async { var status = await bind.mainGetOnlineStatue(); @@ -300,7 +301,7 @@ class ServerModel with ChangeNotifier { } _fetchID() async { - final old = _serverId.text; + final old = _serverId.id; var count = 0; const maxCount = 10; while (count < maxCount) { @@ -309,12 +310,12 @@ class ServerModel with ChangeNotifier { if (id.isEmpty) { continue; } else { - _serverId.text = id; + _serverId.id = id; } - debugPrint("fetch id again at $count:id:${_serverId.text}"); + debugPrint("fetch id again at $count:id:${_serverId.id}"); count++; - if (_serverId.text != old) { + if (_serverId.id != old) { break; } } From abf79ba61dcea5b3b45cad788949a3e2b75cc994 Mon Sep 17 00:00:00 2001 From: asur4s Date: Sat, 3 Sep 2022 11:07:55 -0400 Subject: [PATCH 0370/2015] Fix down and press of key_event in legacy --- src/ui_session_interface.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 7143ed97a..92a08ef3d 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -827,8 +827,13 @@ impl Session { } } - self.legacy_modifiers(&mut key_event, true, true, false, false); - key_event.press = down; + self.legacy_modifiers(&mut key_event, alt, ctrl, shift, command); + if v == 1 { + key_event.down = true; + } else if v == 3 { + key_event.press = true; + } + self.send_key_event(key_event, KeyboardMode::Legacy); } From a553334157c6af108f1fd672aa2d102aa84bf87c Mon Sep 17 00:00:00 2001 From: 21pages Date: Sat, 3 Sep 2022 18:19:50 +0800 Subject: [PATCH 0371/2015] dialog focus && deal with Enter/Esc key Signed-off-by: 21pages --- flutter/lib/common.dart | 148 ++++++--- .../lib/desktop/pages/connection_page.dart | 207 ++++++------ .../lib/desktop/pages/desktop_home_page.dart | 314 +++++++++--------- .../desktop/pages/desktop_setting_page.dart | 300 ++++++++--------- .../lib/desktop/pages/desktop_tab_page.dart | 46 +-- .../lib/desktop/pages/file_manager_page.dart | 84 ++--- .../lib/desktop/pages/port_forward_page.dart | 69 ++-- .../lib/desktop/widgets/peercard_widget.dart | 140 ++++---- .../lib/desktop/widgets/remote_menubar.dart | 81 ++--- .../lib/desktop/widgets/tabbar_widget.dart | 34 +- flutter/lib/models/file_model.dart | 194 ++++++----- flutter/lib/models/server_model.dart | 201 +++++------ 12 files changed, 959 insertions(+), 859 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 75328c840..4a0a0dc82 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -7,6 +7,7 @@ import 'package:back_button_interceptor/back_button_interceptor.dart'; import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; import 'package:flutter_hbb/models/peer_model.dart'; import 'package:get/get.dart'; @@ -340,34 +341,41 @@ class OverlayDialogManager { {bool clickMaskDismiss = false, bool showCancel = true, VoidCallback? onCancel}) { - show((setState, close) => CustomAlertDialog( + show((setState, close) { + cancel() { + dismissAll(); + if (onCancel != null) { + onCancel(); + } + } + + return CustomAlertDialog( content: Container( - constraints: BoxConstraints(maxWidth: 240), + constraints: const BoxConstraints(maxWidth: 240), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox(height: 30), - Center(child: CircularProgressIndicator()), - SizedBox(height: 20), + const SizedBox(height: 30), + const Center(child: CircularProgressIndicator()), + const SizedBox(height: 20), Center( child: Text(translate(text), - style: TextStyle(fontSize: 15))), - SizedBox(height: 20), + style: const TextStyle(fontSize: 15))), + const SizedBox(height: 20), Offstage( offstage: !showCancel, child: Center( child: TextButton( style: flatButtonStyle, - onPressed: () { - dismissAll(); - if (onCancel != null) { - onCancel(); - } - }, + onPressed: cancel, child: Text(translate('Cancel'), - style: TextStyle(color: MyTheme.accent))))) - ])))); + style: + const TextStyle(color: MyTheme.accent))))) + ])), + onCancel: showCancel ? cancel : null, + ); + }); } } @@ -377,18 +385,18 @@ void showToast(String text, {Duration timeout = const Duration(seconds: 2)}) { final entry = OverlayEntry(builder: (_) { return IgnorePointer( child: Align( - alignment: Alignment(0.0, 0.8), + alignment: const Alignment(0.0, 0.8), child: Container( decoration: BoxDecoration( color: Colors.black.withOpacity(0.6), - borderRadius: BorderRadius.all( + borderRadius: const BorderRadius.all( Radius.circular(20), ), ), - padding: EdgeInsets.symmetric(horizontal: 20, vertical: 5), + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 5), child: Text( text, - style: TextStyle( + style: const TextStyle( decoration: TextDecoration.none, fontWeight: FontWeight.w300, fontSize: 18, @@ -403,23 +411,54 @@ void showToast(String text, {Duration timeout = const Duration(seconds: 2)}) { } class CustomAlertDialog extends StatelessWidget { - CustomAlertDialog( - {this.title, required this.content, this.actions, this.contentPadding}); + const CustomAlertDialog( + {Key? key, + this.title, + required this.content, + this.actions, + this.contentPadding, + this.onSubmit, + this.onCancel}) + : super(key: key); final Widget? title; final Widget content; final List? actions; final double? contentPadding; + final Function()? onSubmit; + final Function()? onCancel; @override Widget build(BuildContext context) { - return AlertDialog( - scrollable: true, - title: title, - contentPadding: - EdgeInsets.symmetric(horizontal: contentPadding ?? 25, vertical: 10), - content: content, - actions: actions, + FocusNode focusNode = FocusNode(); + // request focus if there is no focused FocusNode in the dialog + Future.delayed(Duration.zero, () { + if (!focusNode.hasFocus) focusNode.requestFocus(); + }); + return Focus( + focusNode: focusNode, + autofocus: true, + onKey: (node, key) { + if (key.logicalKey == LogicalKeyboardKey.escape) { + if (key is RawKeyDownEvent) { + onCancel?.call(); + } + return KeyEventResult.handled; // avoid TextField exception on escape + } else if (onSubmit != null && + key.logicalKey == LogicalKeyboardKey.enter) { + if (key is RawKeyDownEvent) onSubmit?.call(); + return KeyEventResult.handled; + } + return KeyEventResult.ignored; + }, + child: AlertDialog( + scrollable: true, + title: title, + contentPadding: EdgeInsets.symmetric( + horizontal: contentPadding ?? 25, vertical: 10), + content: content, + actions: actions, + ), ); } } @@ -429,26 +468,28 @@ void msgBox( {bool? hasCancel}) { dialogManager.dismissAll(); List buttons = []; + bool hasOk = false; + submit() { + dialogManager.dismissAll(); + // https://github.com/fufesou/rustdesk/blob/5e9a31340b899822090a3731769ae79c6bf5f3e5/src/ui/common.tis#L263 + if (!type.contains("custom")) { + closeConnection(); + } + } + + cancel() { + dialogManager.dismissAll(); + } + if (type != "connecting" && type != "success" && !type.contains("nook")) { - buttons.insert( - 0, - msgBoxButton(translate('OK'), () { - dialogManager.dismissAll(); - // https://github.com/fufesou/rustdesk/blob/5e9a31340b899822090a3731769ae79c6bf5f3e5/src/ui/common.tis#L263 - if (!type.contains("custom")) { - closeConnection(); - } - })); + hasOk = true; + buttons.insert(0, msgBoxButton(translate('OK'), submit)); } hasCancel ??= !type.contains("error") && !type.contains("nocancel") && type != "restarting"; if (hasCancel) { - buttons.insert( - 0, - msgBoxButton(translate('Cancel'), () { - dialogManager.dismissAll(); - })); + buttons.insert(0, msgBoxButton(translate('Cancel'), cancel)); } // TODO: test this button if (type.contains("hasclose")) { @@ -459,9 +500,12 @@ void msgBox( })); } dialogManager.show((setState, close) => CustomAlertDialog( - title: _msgBoxTitle(title), - content: Text(translate(text), style: TextStyle(fontSize: 15)), - actions: buttons)); + title: _msgBoxTitle(title), + content: Text(translate(text), style: const TextStyle(fontSize: 15)), + actions: buttons, + onSubmit: hasOk ? submit : null, + onCancel: hasCancel == true ? cancel : null, + )); } Widget msgBoxButton(String text, void Function() onPressed) { @@ -479,15 +523,19 @@ Widget msgBoxButton(String text, void Function() onPressed) { Text(translate(text), style: TextStyle(color: MyTheme.accent)))); } -Widget _msgBoxTitle(String title) => Text(translate(title), style: TextStyle(fontSize: 21)); +Widget _msgBoxTitle(String title) => + Text(translate(title), style: TextStyle(fontSize: 21)); void msgBoxCommon(OverlayDialogManager dialogManager, String title, - Widget content, List buttons) { + Widget content, List buttons, + {bool hasCancel = true}) { dialogManager.dismissAll(); dialogManager.show((setState, close) => CustomAlertDialog( - title: _msgBoxTitle(title), - content: content, - actions: buttons)); + title: _msgBoxTitle(title), + content: content, + actions: buttons, + onCancel: hasCancel ? close : null, + )); } Color str2color(String str, [alpha = 0xFF]) { diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index fe363c4c9..e4f6527ca 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -167,7 +167,7 @@ class _ConnectionPageState extends State { }); var w = Container( width: 320 + 20 * 2, - padding: EdgeInsets.fromLTRB(20, 24, 20, 22), + padding: const EdgeInsets.fromLTRB(20, 24, 20, 22), decoration: BoxDecoration( color: MyTheme.color(context).bg, borderRadius: const BorderRadius.all(Radius.circular(13)), @@ -179,7 +179,7 @@ class _ConnectionPageState extends State { children: [ Text( translate('Control Remote Desktop'), - style: TextStyle(fontSize: 19, height: 1), + style: const TextStyle(fontSize: 19, height: 1), ), ], ).marginOnly(bottom: 15), @@ -192,11 +192,13 @@ class _ConnectionPageState extends State { enableSuggestions: false, keyboardType: TextInputType.visiblePassword, focusNode: focusNode, - style: TextStyle( + style: const TextStyle( fontFamily: 'WorkSans', fontSize: 22, height: 1, ), + maxLines: 1, + cursorColor: MyTheme.color(context).text!, decoration: InputDecoration( hintText: inputFocused.value ? null @@ -206,14 +208,18 @@ class _ConnectionPageState extends State { border: OutlineInputBorder( borderRadius: BorderRadius.zero, borderSide: BorderSide( - color: MyTheme.color(context).placeholder!)), - focusedBorder: OutlineInputBorder( + color: MyTheme.color(context).border!)), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.zero, + borderSide: BorderSide( + color: MyTheme.color(context).border!)), + focusedBorder: const OutlineInputBorder( borderRadius: BorderRadius.zero, borderSide: BorderSide(color: MyTheme.button, width: 3), ), isDense: true, - contentPadding: EdgeInsets.symmetric( + contentPadding: const EdgeInsets.symmetric( horizontal: 10, vertical: 12)), controller: _idController, inputFormatters: [IDTextInputFormatter()], @@ -266,7 +272,7 @@ class _ConnectionPageState extends State { ).marginSymmetric(horizontal: 12), ), )), - SizedBox( + const SizedBox( width: 17, ), Obx( @@ -311,7 +317,8 @@ class _ConnectionPageState extends State { ), ); return Center( - child: Container(constraints: BoxConstraints(maxWidth: 600), child: w)); + child: Container( + constraints: const BoxConstraints(maxWidth: 600), child: w)); } @override @@ -661,71 +668,69 @@ class _ConnectionPageState extends State { var field = ""; var msg = ""; var isInProgress = false; + TextEditingController controller = TextEditingController(text: field); + gFFI.dialogManager.show((setState, close) { + submit() async { + setState(() { + msg = ""; + isInProgress = true; + }); + field = controller.text.trim(); + if (field.isEmpty) { + // pass + } else { + final ids = field.trim().split(RegExp(r"[\s,;\n]+")); + field = ids.join(','); + for (final newId in ids) { + if (gFFI.abModel.idContainBy(newId)) { + continue; + } + gFFI.abModel.addId(newId); + } + await gFFI.abModel.updateAb(); + this.setState(() {}); + // final currentPeers + } + close(); + } + return CustomAlertDialog( title: Text(translate("Add ID")), content: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(translate("whitelist_sep")), - SizedBox( + const SizedBox( height: 8.0, ), Row( children: [ Expanded( child: TextField( - onChanged: (s) { - field = s; - }, - maxLines: null, - decoration: InputDecoration( - border: OutlineInputBorder(), - errorText: msg.isEmpty ? null : translate(msg), - ), - controller: TextEditingController(text: field), - ), + maxLines: null, + decoration: InputDecoration( + border: const OutlineInputBorder(), + errorText: msg.isEmpty ? null : translate(msg), + ), + controller: controller, + focusNode: FocusNode()..requestFocus()), ), ], ), - SizedBox( + const SizedBox( height: 4.0, ), - Offstage(offstage: !isInProgress, child: LinearProgressIndicator()) + Offstage( + offstage: !isInProgress, child: const LinearProgressIndicator()) ], ), actions: [ - TextButton( - onPressed: () { - close(); - }, - child: Text(translate("Cancel"))), - TextButton( - onPressed: () async { - setState(() { - msg = ""; - isInProgress = true; - }); - field = field.trim(); - if (field.isEmpty) { - // pass - } else { - final ids = field.trim().split(RegExp(r"[\s,;\n]+")); - field = ids.join(','); - for (final newId in ids) { - if (gFFI.abModel.idContainBy(newId)) { - continue; - } - gFFI.abModel.addId(newId); - } - await gFFI.abModel.updateAb(); - this.setState(() {}); - // final currentPeers - } - close(); - }, - child: Text(translate("OK"))), + TextButton(onPressed: close, child: Text(translate("Cancel"))), + TextButton(onPressed: submit, child: Text(translate("OK"))), ], + onSubmit: submit, + onCancel: close, ); }); } @@ -734,67 +739,65 @@ class _ConnectionPageState extends State { var field = ""; var msg = ""; var isInProgress = false; + TextEditingController controller = TextEditingController(text: field); gFFI.dialogManager.show((setState, close) { + submit() async { + setState(() { + msg = ""; + isInProgress = true; + }); + field = controller.text.trim(); + if (field.isEmpty) { + // pass + } else { + final tags = field.trim().split(RegExp(r"[\s,;\n]+")); + field = tags.join(','); + for (final tag in tags) { + gFFI.abModel.addTag(tag); + } + await gFFI.abModel.updateAb(); + // final currentPeers + } + close(); + } + return CustomAlertDialog( title: Text(translate("Add Tag")), content: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(translate("whitelist_sep")), - SizedBox( + const SizedBox( height: 8.0, ), Row( children: [ Expanded( child: TextField( - onChanged: (s) { - field = s; - }, maxLines: null, decoration: InputDecoration( - border: OutlineInputBorder(), + border: const OutlineInputBorder(), errorText: msg.isEmpty ? null : translate(msg), ), - controller: TextEditingController(text: field), + controller: controller, + focusNode: FocusNode()..requestFocus(), ), ), ], ), - SizedBox( + const SizedBox( height: 4.0, ), - Offstage(offstage: !isInProgress, child: LinearProgressIndicator()) + Offstage( + offstage: !isInProgress, child: const LinearProgressIndicator()) ], ), actions: [ - TextButton( - onPressed: () { - close(); - }, - child: Text(translate("Cancel"))), - TextButton( - onPressed: () async { - setState(() { - msg = ""; - isInProgress = true; - }); - field = field.trim(); - if (field.isEmpty) { - // pass - } else { - final tags = field.trim().split(RegExp(r"[\s,;\n]+")); - field = tags.join(','); - for (final tag in tags) { - gFFI.abModel.addTag(tag); - } - await gFFI.abModel.updateAb(); - // final currentPeers - } - close(); - }, - child: Text(translate("OK"))), + TextButton(onPressed: close, child: Text(translate("Cancel"))), + TextButton(onPressed: submit, child: Text(translate("OK"))), ], + onSubmit: submit, + onCancel: close, ); }); } @@ -806,13 +809,23 @@ class _ConnectionPageState extends State { var selectedTag = gFFI.abModel.getPeerTags(id).obs; gFFI.dialogManager.show((setState, close) { + submit() async { + setState(() { + isInProgress = true; + }); + gFFI.abModel.changeTagForPeer(id, selectedTag); + await gFFI.abModel.updateAb(); + close(); + } + return CustomAlertDialog( title: Text(translate("Edit Tag")), content: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( - padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + padding: + const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), child: Wrap( children: tags .map((e) => buildTag(e, selectedTag, onTap: () { @@ -825,26 +838,16 @@ class _ConnectionPageState extends State { .toList(growable: false), ), ), - Offstage(offstage: !isInProgress, child: LinearProgressIndicator()) + Offstage( + offstage: !isInProgress, child: const LinearProgressIndicator()) ], ), actions: [ - TextButton( - onPressed: () { - close(); - }, - child: Text(translate("Cancel"))), - TextButton( - onPressed: () async { - setState(() { - isInProgress = true; - }); - gFFI.abModel.changeTagForPeer(id, selectedTag); - await gFFI.abModel.updateAb(); - close(); - }, - child: Text(translate("OK"))), + TextButton(onPressed: close, child: Text(translate("Cancel"))), + TextButton(onPressed: submit, child: Text(translate("OK"))), ], + onSubmit: submit, + onCancel: close, ); }); } diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 5a082a8fd..0ccb86d1f 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -55,7 +55,7 @@ class _DesktopHomePageState extends State return Row( children: [ buildServerInfo(context), - VerticalDivider( + const VerticalDivider( width: 1, thickness: 1, ), @@ -93,7 +93,7 @@ class _DesktopHomePageState extends State buildIDBoard(BuildContext context) { final model = gFFI.serverModel; return Container( - margin: EdgeInsets.only(left: 20, right: 16), + margin: const EdgeInsets.only(left: 20, right: 16), height: 52, child: Row( crossAxisAlignment: CrossAxisAlignment.baseline, @@ -101,7 +101,7 @@ class _DesktopHomePageState extends State children: [ Container( width: 2, - decoration: BoxDecoration(color: MyTheme.accent), + decoration: const BoxDecoration(color: MyTheme.accent), ), Expanded( child: Padding( @@ -109,7 +109,7 @@ class _DesktopHomePageState extends State child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Container( + SizedBox( height: 25, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -135,11 +135,11 @@ class _DesktopHomePageState extends State child: TextFormField( controller: model.serverId, readOnly: true, - decoration: InputDecoration( + decoration: const InputDecoration( border: InputBorder.none, contentPadding: EdgeInsets.only(bottom: 18), ), - style: TextStyle( + style: const TextStyle( fontSize: 22, ), ), @@ -642,76 +642,76 @@ class _DesktopHomePageState extends State var newId = ""; var msg = ""; var isInProgress = false; + TextEditingController controller = TextEditingController(); gFFI.dialogManager.show((setState, close) { + submit() async { + newId = controller.text.trim(); + setState(() { + msg = ""; + isInProgress = true; + bind.mainChangeId(newId: newId); + }); + + var status = await bind.mainGetAsyncStatus(); + while (status == " ") { + await Future.delayed(const Duration(milliseconds: 100)); + status = await bind.mainGetAsyncStatus(); + } + if (status.isEmpty) { + // ok + close(); + return; + } + setState(() { + isInProgress = false; + msg = translate(status); + }); + } + return CustomAlertDialog( title: Text(translate("Change ID")), content: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(translate("id_change_tip")), - SizedBox( + const SizedBox( height: 8.0, ), Row( children: [ - Text("ID:").marginOnly(bottom: 16.0), - SizedBox( + const Text("ID:").marginOnly(bottom: 16.0), + const SizedBox( width: 24.0, ), Expanded( child: TextField( - onChanged: (s) { - newId = s; - }, decoration: InputDecoration( - border: OutlineInputBorder(), + border: const OutlineInputBorder(), errorText: msg.isEmpty ? null : translate(msg)), inputFormatters: [ LengthLimitingTextInputFormatter(16), // FilteringTextInputFormatter(RegExp(r"[a-zA-z][a-zA-z0-9\_]*"), allow: true) ], maxLength: 16, + controller: controller, + focusNode: FocusNode()..requestFocus(), ), ), ], ), - SizedBox( + const SizedBox( height: 4.0, ), - Offstage(offstage: !isInProgress, child: LinearProgressIndicator()) + Offstage( + offstage: !isInProgress, child: const LinearProgressIndicator()) ], ), actions: [ - TextButton( - onPressed: () { - close(); - }, - child: Text(translate("Cancel"))), - TextButton( - onPressed: () async { - setState(() { - msg = ""; - isInProgress = true; - bind.mainChangeId(newId: newId); - }); - - var status = await bind.mainGetAsyncStatus(); - while (status == " ") { - await Future.delayed(Duration(milliseconds: 100)); - status = await bind.mainGetAsyncStatus(); - } - if (status.isEmpty) { - // ok - close(); - return; - } - setState(() { - isInProgress = false; - msg = translate(status); - }); - }, - child: Text(translate("OK"))), + TextButton(onPressed: close, child: Text(translate("Cancel"))), + TextButton(onPressed: submit, child: Text(translate("OK"))), ], + onSubmit: submit, + onCancel: close, ); }); } @@ -720,16 +720,16 @@ class _DesktopHomePageState extends State final appName = await bind.mainGetAppName(); final license = await bind.mainGetLicense(); final version = await bind.mainGetVersion(); - final linkStyle = TextStyle(decoration: TextDecoration.underline); + const linkStyle = TextStyle(decoration: TextDecoration.underline); gFFI.dialogManager.show((setState, close) { return CustomAlertDialog( title: Text("About $appName"), content: ConstrainedBox( - constraints: BoxConstraints(minWidth: 500), + constraints: const BoxConstraints(minWidth: 500), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox( + const SizedBox( height: 8.0, ), Text("Version: $version").marginSymmetric(vertical: 4.0), @@ -737,7 +737,7 @@ class _DesktopHomePageState extends State onTap: () { launchUrlString("https://rustdesk.com/privacy"); }, - child: Text( + child: const Text( "Privacy Statement", style: linkStyle, ).marginSymmetric(vertical: 4.0)), @@ -745,13 +745,14 @@ class _DesktopHomePageState extends State onTap: () { launchUrlString("https://rustdesk.com"); }, - child: Text( + child: const Text( "Website", style: linkStyle, ).marginSymmetric(vertical: 4.0)), Container( - decoration: BoxDecoration(color: Color(0xFF2c8cff)), - padding: EdgeInsets.symmetric(vertical: 24, horizontal: 8), + decoration: const BoxDecoration(color: Color(0xFF2c8cff)), + padding: + const EdgeInsets.symmetric(vertical: 24, horizontal: 8), child: Row( children: [ Expanded( @@ -760,9 +761,9 @@ class _DesktopHomePageState extends State children: [ Text( "Copyright © 2022 Purslane Ltd.\n$license", - style: TextStyle(color: Colors.white), + style: const TextStyle(color: Colors.white), ), - Text( + const Text( "Made with heart in this chaotic world!", style: TextStyle( fontWeight: FontWeight.w800, @@ -778,12 +779,10 @@ class _DesktopHomePageState extends State ), ), actions: [ - TextButton( - onPressed: () async { - close(); - }, - child: Text(translate("OK"))), + TextButton(onPressed: close, child: Text(translate("OK"))), ], + onSubmit: close, + onCancel: close, ); }); } @@ -815,118 +814,124 @@ Future loginDialog() async { var isInProgress = false; var completer = Completer(); gFFI.dialogManager.show((setState, close) { + submit() async { + setState(() { + userNameMsg = ""; + passMsg = ""; + isInProgress = true; + }); + cancel() { + setState(() { + isInProgress = false; + }); + } + + userName = userContontroller.text; + pass = pwdController.text; + if (userName.isEmpty) { + userNameMsg = translate("Username missed"); + cancel(); + return; + } + if (pass.isEmpty) { + passMsg = translate("Password missed"); + cancel(); + return; + } + try { + final resp = await gFFI.userModel.login(userName, pass); + if (resp.containsKey('error')) { + passMsg = resp['error']; + cancel(); + return; + } + // {access_token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJndWlkIjoiMDFkZjQ2ZjgtZjg3OS00MDE0LTk5Y2QtMGMwYzM2MmViZGJlIiwiZXhwIjoxNjYxNDg2NzYwfQ.GZpe1oI8TfM5yTYNrpcwbI599P4Z_-b2GmnwNl2Lr-w, + // token_type: Bearer, user: {id: , name: admin, email: null, note: null, status: null, grp: null, is_admin: true}} + debugPrint("$resp"); + completer.complete(true); + } catch (err) { + // ignore: avoid_print + print(err.toString()); + cancel(); + return; + } + close(); + } + + cancel() { + completer.complete(false); + close(); + } + return CustomAlertDialog( title: Text(translate("Login")), content: ConstrainedBox( - constraints: BoxConstraints(minWidth: 500), + constraints: const BoxConstraints(minWidth: 500), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox( + const SizedBox( height: 8.0, ), Row( children: [ ConstrainedBox( - constraints: BoxConstraints(minWidth: 100), + constraints: const BoxConstraints(minWidth: 100), child: Text( "${translate('Username')}:", textAlign: TextAlign.start, ).marginOnly(bottom: 16.0)), - SizedBox( + const SizedBox( width: 24.0, ), Expanded( child: TextField( decoration: InputDecoration( - border: OutlineInputBorder(), + border: const OutlineInputBorder(), errorText: userNameMsg.isNotEmpty ? userNameMsg : null), controller: userContontroller, + focusNode: FocusNode()..requestFocus(), ), ), ], ), - SizedBox( + const SizedBox( height: 8.0, ), Row( children: [ ConstrainedBox( - constraints: BoxConstraints(minWidth: 100), + constraints: const BoxConstraints(minWidth: 100), child: Text("${translate('Password')}:") .marginOnly(bottom: 16.0)), - SizedBox( + const SizedBox( width: 24.0, ), Expanded( child: TextField( obscureText: true, decoration: InputDecoration( - border: OutlineInputBorder(), + border: const OutlineInputBorder(), errorText: passMsg.isNotEmpty ? passMsg : null), controller: pwdController, ), ), ], ), - SizedBox( + const SizedBox( height: 4.0, ), - Offstage(offstage: !isInProgress, child: LinearProgressIndicator()) + Offstage( + offstage: !isInProgress, child: const LinearProgressIndicator()) ], ), ), actions: [ - TextButton( - onPressed: () { - completer.complete(false); - close(); - }, - child: Text(translate("Cancel"))), - TextButton( - onPressed: () async { - setState(() { - userNameMsg = ""; - passMsg = ""; - isInProgress = true; - }); - final cancel = () { - setState(() { - isInProgress = false; - }); - }; - userName = userContontroller.text; - pass = pwdController.text; - if (userName.isEmpty) { - userNameMsg = translate("Username missed"); - cancel(); - return; - } - if (pass.isEmpty) { - passMsg = translate("Password missed"); - cancel(); - return; - } - try { - final resp = await gFFI.userModel.login(userName, pass); - if (resp.containsKey('error')) { - passMsg = resp['error']; - cancel(); - return; - } - // {access_token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJndWlkIjoiMDFkZjQ2ZjgtZjg3OS00MDE0LTk5Y2QtMGMwYzM2MmViZGJlIiwiZXhwIjoxNjYxNDg2NzYwfQ.GZpe1oI8TfM5yTYNrpcwbI599P4Z_-b2GmnwNl2Lr-w, - // token_type: Bearer, user: {id: , name: admin, email: null, note: null, status: null, grp: null, is_admin: true}} - debugPrint("$resp"); - completer.complete(true); - } catch (err) { - print(err.toString()); - cancel(); - return; - } - close(); - }, - child: Text(translate("OK"))), + TextButton(onPressed: cancel, child: Text(translate("Cancel"))), + TextButton(onPressed: submit, child: Text(translate("OK"))), ], + onSubmit: submit, + onCancel: cancel, ); }); return completer.future; @@ -940,55 +945,78 @@ void setPasswordDialog() async { var errMsg1 = ""; gFFI.dialogManager.show((setState, close) { + submit() { + setState(() { + errMsg0 = ""; + errMsg1 = ""; + }); + final pass = p0.text.trim(); + if (pass.length < 6) { + setState(() { + errMsg0 = translate("Too short, at least 6 characters."); + }); + return; + } + if (p1.text.trim() != pass) { + setState(() { + errMsg1 = translate("The confirmation is not identical."); + }); + return; + } + bind.mainSetPermanentPassword(password: pass); + close(); + } + return CustomAlertDialog( title: Text(translate("Set Password")), content: ConstrainedBox( - constraints: BoxConstraints(minWidth: 500), + constraints: const BoxConstraints(minWidth: 500), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox( + const SizedBox( height: 8.0, ), Row( children: [ ConstrainedBox( - constraints: BoxConstraints(minWidth: 100), + constraints: const BoxConstraints(minWidth: 100), child: Text( "${translate('Password')}:", textAlign: TextAlign.start, ).marginOnly(bottom: 16.0)), - SizedBox( + const SizedBox( width: 24.0, ), Expanded( child: TextField( obscureText: true, decoration: InputDecoration( - border: OutlineInputBorder(), + border: const OutlineInputBorder(), errorText: errMsg0.isNotEmpty ? errMsg0 : null), controller: p0, + focusNode: FocusNode()..requestFocus(), ), ), ], ), - SizedBox( + const SizedBox( height: 8.0, ), Row( children: [ ConstrainedBox( - constraints: BoxConstraints(minWidth: 100), + constraints: const BoxConstraints(minWidth: 100), child: Text("${translate('Confirmation')}:") .marginOnly(bottom: 16.0)), - SizedBox( + const SizedBox( width: 24.0, ), Expanded( child: TextField( obscureText: true, decoration: InputDecoration( - border: OutlineInputBorder(), + border: const OutlineInputBorder(), errorText: errMsg1.isNotEmpty ? errMsg1 : null), controller: p1, ), @@ -999,35 +1027,11 @@ void setPasswordDialog() async { ), ), actions: [ - TextButton( - onPressed: () { - close(); - }, - child: Text(translate("Cancel"))), - TextButton( - onPressed: () { - setState(() { - errMsg0 = ""; - errMsg1 = ""; - }); - final pass = p0.text.trim(); - if (pass.length < 6) { - setState(() { - errMsg0 = translate("Too short, at least 6 characters."); - }); - return; - } - if (p1.text.trim() != pass) { - setState(() { - errMsg1 = translate("The confirmation is not identical."); - }); - return; - } - bind.mainSetPermanentPassword(password: pass); - close(); - }, - child: Text(translate("OK"))), + TextButton(onPressed: close, child: Text(translate("Cancel"))), + TextButton(onPressed: submit, child: Text(translate("OK"))), ], + onSubmit: submit, + onCancel: close, ); }); } diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 120f8bc7a..867c8a54c 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -1038,52 +1038,117 @@ void changeServer() async { var keyController = TextEditingController(text: key); var isInProgress = false; + gFFI.dialogManager.show((setState, close) { + submit() async { + setState(() { + [idServerMsg, relayServerMsg, apiServerMsg].forEach((element) { + element = ""; + }); + isInProgress = true; + }); + cancel() { + setState(() { + isInProgress = false; + }); + } + + idServer = idController.text.trim(); + relayServer = relayController.text.trim(); + apiServer = apiController.text.trim().toLowerCase(); + key = keyController.text.trim(); + + if (idServer.isNotEmpty) { + idServerMsg = + translate(await bind.mainTestIfValidServer(server: idServer)); + if (idServerMsg.isEmpty) { + oldOptions['custom-rendezvous-server'] = idServer; + } else { + cancel(); + return; + } + } else { + oldOptions['custom-rendezvous-server'] = ""; + } + + if (relayServer.isNotEmpty) { + relayServerMsg = + translate(await bind.mainTestIfValidServer(server: relayServer)); + if (relayServerMsg.isEmpty) { + oldOptions['relay-server'] = relayServer; + } else { + cancel(); + return; + } + } else { + oldOptions['relay-server'] = ""; + } + + if (apiServer.isNotEmpty) { + if (apiServer.startsWith('http://') || + apiServer.startsWith("https://")) { + oldOptions['api-server'] = apiServer; + return; + } else { + apiServerMsg = translate("invalid_http"); + cancel(); + return; + } + } else { + oldOptions['api-server'] = ""; + } + // ok + oldOptions['key'] = key; + await bind.mainSetOptions(json: jsonEncode(oldOptions)); + close(); + } + return CustomAlertDialog( title: Text(translate("ID/Relay Server")), content: ConstrainedBox( - constraints: BoxConstraints(minWidth: 500), + constraints: const BoxConstraints(minWidth: 500), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox( + const SizedBox( height: 8.0, ), Row( children: [ ConstrainedBox( - constraints: BoxConstraints(minWidth: 100), + constraints: const BoxConstraints(minWidth: 100), child: Text("${translate('ID Server')}:") .marginOnly(bottom: 16.0)), - SizedBox( + const SizedBox( width: 24.0, ), Expanded( child: TextField( decoration: InputDecoration( - border: OutlineInputBorder(), + border: const OutlineInputBorder(), errorText: idServerMsg.isNotEmpty ? idServerMsg : null), controller: idController, + focusNode: FocusNode()..requestFocus(), ), ), ], ), - SizedBox( + const SizedBox( height: 8.0, ), Row( children: [ ConstrainedBox( - constraints: BoxConstraints(minWidth: 100), + constraints: const BoxConstraints(minWidth: 100), child: Text("${translate('Relay Server')}:") .marginOnly(bottom: 16.0)), - SizedBox( + const SizedBox( width: 24.0, ), Expanded( child: TextField( decoration: InputDecoration( - border: OutlineInputBorder(), + border: const OutlineInputBorder(), errorText: relayServerMsg.isNotEmpty ? relayServerMsg : null), controller: relayController, @@ -1091,22 +1156,22 @@ void changeServer() async { ), ], ), - SizedBox( + const SizedBox( height: 8.0, ), Row( children: [ ConstrainedBox( - constraints: BoxConstraints(minWidth: 100), + constraints: const BoxConstraints(minWidth: 100), child: Text("${translate('API Server')}:") .marginOnly(bottom: 16.0)), - SizedBox( + const SizedBox( width: 24.0, ), Expanded( child: TextField( decoration: InputDecoration( - border: OutlineInputBorder(), + border: const OutlineInputBorder(), errorText: apiServerMsg.isNotEmpty ? apiServerMsg : null), controller: apiController, @@ -1114,21 +1179,21 @@ void changeServer() async { ), ], ), - SizedBox( + const SizedBox( height: 8.0, ), Row( children: [ ConstrainedBox( - constraints: BoxConstraints(minWidth: 100), + constraints: const BoxConstraints(minWidth: 100), child: Text("${translate('Key')}:").marginOnly(bottom: 16.0)), - SizedBox( + const SizedBox( width: 24.0, ), Expanded( child: TextField( - decoration: InputDecoration( + decoration: const InputDecoration( border: OutlineInputBorder(), ), controller: keyController, @@ -1136,83 +1201,20 @@ void changeServer() async { ), ], ), - SizedBox( + const SizedBox( height: 4.0, ), - Offstage(offstage: !isInProgress, child: LinearProgressIndicator()) + Offstage( + offstage: !isInProgress, child: const LinearProgressIndicator()) ], ), ), actions: [ - TextButton( - onPressed: () { - close(); - }, - child: Text(translate("Cancel"))), - TextButton( - onPressed: () async { - setState(() { - [idServerMsg, relayServerMsg, apiServerMsg].forEach((element) { - element = ""; - }); - isInProgress = true; - }); - final cancel = () { - setState(() { - isInProgress = false; - }); - }; - idServer = idController.text.trim(); - relayServer = relayController.text.trim(); - apiServer = apiController.text.trim().toLowerCase(); - key = keyController.text.trim(); - - if (idServer.isNotEmpty) { - idServerMsg = translate( - await bind.mainTestIfValidServer(server: idServer)); - if (idServerMsg.isEmpty) { - oldOptions['custom-rendezvous-server'] = idServer; - } else { - cancel(); - return; - } - } else { - oldOptions['custom-rendezvous-server'] = ""; - } - - if (relayServer.isNotEmpty) { - relayServerMsg = translate( - await bind.mainTestIfValidServer(server: relayServer)); - if (relayServerMsg.isEmpty) { - oldOptions['relay-server'] = relayServer; - } else { - cancel(); - return; - } - } else { - oldOptions['relay-server'] = ""; - } - - if (apiServer.isNotEmpty) { - if (apiServer.startsWith('http://') || - apiServer.startsWith("https://")) { - oldOptions['api-server'] = apiServer; - return; - } else { - apiServerMsg = translate("invalid_http"); - cancel(); - return; - } - } else { - oldOptions['api-server'] = ""; - } - // ok - oldOptions['key'] = key; - await bind.mainSetOptions(json: jsonEncode(oldOptions)); - close(); - }, - child: Text(translate("OK"))), + TextButton(onPressed: close, child: Text(translate("Cancel"))), + TextButton(onPressed: submit, child: Text(translate("OK"))), ], + onSubmit: submit, + onCancel: close, ); }); } @@ -1231,27 +1233,28 @@ void changeWhiteList() async { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(translate("whitelist_sep")), - SizedBox( + const SizedBox( height: 8.0, ), Row( children: [ Expanded( child: TextField( - maxLines: null, - decoration: InputDecoration( - border: OutlineInputBorder(), - errorText: msg.isEmpty ? null : translate(msg), - ), - controller: controller, - ), + maxLines: null, + decoration: InputDecoration( + border: const OutlineInputBorder(), + errorText: msg.isEmpty ? null : translate(msg), + ), + controller: controller, + focusNode: FocusNode()..requestFocus()), ), ], ), - SizedBox( + const SizedBox( height: 4.0, ), - Offstage(offstage: !isInProgress, child: LinearProgressIndicator()) + Offstage( + offstage: !isInProgress, child: const LinearProgressIndicator()) ], ), actions: [ @@ -1277,7 +1280,7 @@ void changeWhiteList() async { final ipMatch = RegExp(r"^\d+\.\d+\.\d+\.\d+$"); for (final ip in ips) { if (!ipMatch.hasMatch(ip)) { - msg = translate("Invalid IP") + " $ip"; + msg = "${translate("Invalid IP")} $ip"; setState(() { isInProgress = false; }); @@ -1292,6 +1295,7 @@ void changeWhiteList() async { }, child: Text(translate("OK"))), ], + onCancel: close, ); }); } @@ -1314,50 +1318,80 @@ void changeSocks5Proxy() async { var isInProgress = false; gFFI.dialogManager.show((setState, close) { + submit() async { + setState(() { + proxyMsg = ""; + isInProgress = true; + }); + cancel() { + setState(() { + isInProgress = false; + }); + } + + proxy = proxyController.text.trim(); + username = userController.text.trim(); + password = pwdController.text.trim(); + + if (proxy.isNotEmpty) { + proxyMsg = translate(await bind.mainTestIfValidServer(server: proxy)); + if (proxyMsg.isEmpty) { + // ignore + } else { + cancel(); + return; + } + } + await bind.mainSetSocks( + proxy: proxy, username: username, password: password); + close(); + } + return CustomAlertDialog( title: Text(translate("Socks5 Proxy")), content: ConstrainedBox( - constraints: BoxConstraints(minWidth: 500), + constraints: const BoxConstraints(minWidth: 500), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox( + const SizedBox( height: 8.0, ), Row( children: [ ConstrainedBox( - constraints: BoxConstraints(minWidth: 100), + constraints: const BoxConstraints(minWidth: 100), child: Text("${translate('Hostname')}:") .marginOnly(bottom: 16.0)), - SizedBox( + const SizedBox( width: 24.0, ), Expanded( child: TextField( decoration: InputDecoration( - border: OutlineInputBorder(), + border: const OutlineInputBorder(), errorText: proxyMsg.isNotEmpty ? proxyMsg : null), controller: proxyController, + focusNode: FocusNode()..requestFocus(), ), ), ], ), - SizedBox( + const SizedBox( height: 8.0, ), Row( children: [ ConstrainedBox( - constraints: BoxConstraints(minWidth: 100), + constraints: const BoxConstraints(minWidth: 100), child: Text("${translate('Username')}:") .marginOnly(bottom: 16.0)), - SizedBox( + const SizedBox( width: 24.0, ), Expanded( child: TextField( - decoration: InputDecoration( + decoration: const InputDecoration( border: OutlineInputBorder(), ), controller: userController, @@ -1365,21 +1399,21 @@ void changeSocks5Proxy() async { ), ], ), - SizedBox( + const SizedBox( height: 8.0, ), Row( children: [ ConstrainedBox( - constraints: BoxConstraints(minWidth: 100), + constraints: const BoxConstraints(minWidth: 100), child: Text("${translate('Password')}:") .marginOnly(bottom: 16.0)), - SizedBox( + const SizedBox( width: 24.0, ), Expanded( child: TextField( - decoration: InputDecoration( + decoration: const InputDecoration( border: OutlineInputBorder(), ), controller: pwdController, @@ -1387,50 +1421,20 @@ void changeSocks5Proxy() async { ), ], ), - SizedBox( + const SizedBox( height: 8.0, ), - Offstage(offstage: !isInProgress, child: LinearProgressIndicator()) + Offstage( + offstage: !isInProgress, child: const LinearProgressIndicator()) ], ), ), actions: [ - TextButton( - onPressed: () { - close(); - }, - child: Text(translate("Cancel"))), - TextButton( - onPressed: () async { - setState(() { - proxyMsg = ""; - isInProgress = true; - }); - final cancel = () { - setState(() { - isInProgress = false; - }); - }; - proxy = proxyController.text.trim(); - username = userController.text.trim(); - password = pwdController.text.trim(); - - if (proxy.isNotEmpty) { - proxyMsg = - translate(await bind.mainTestIfValidServer(server: proxy)); - if (proxyMsg.isEmpty) { - // ignore - } else { - cancel(); - return; - } - } - await bind.mainSetSocks( - proxy: proxy, username: username, password: password); - close(); - }, - child: Text(translate("OK"))), + TextButton(onPressed: close, child: Text(translate("Cancel"))), + TextButton(onPressed: submit, child: Text(translate("OK"))), ], + onSubmit: submit, + onCancel: close, ); }); } diff --git a/flutter/lib/desktop/pages/desktop_tab_page.dart b/flutter/lib/desktop/pages/desktop_tab_page.dart index 874a71dcf..0546f0503 100644 --- a/flutter/lib/desktop/pages/desktop_tab_page.dart +++ b/flutter/lib/desktop/pages/desktop_tab_page.dart @@ -37,25 +37,33 @@ class _DesktopTabPageState extends State { RxBool fullscreen = false.obs; Get.put(fullscreen, tag: 'fullscreen'); return Obx(() => DragToResizeArea( - resizeEdgeSize: fullscreen.value ? 1.0 : 8.0, - child: Container( - decoration: BoxDecoration( - border: Border.all(color: MyTheme.color(context).border!)), - child: Scaffold( - backgroundColor: MyTheme.color(context).bg, - body: DesktopTab( - controller: tabController, - theme: dark ? TarBarTheme.dark() : TarBarTheme.light(), - tail: ActionIcon( - message: 'Settings', - icon: IconFont.menu, - theme: dark ? TarBarTheme.dark() : TarBarTheme.light(), - onTap: onAddSetting, - is_close: false, - ), - )), - ), - )); + resizeEdgeSize: fullscreen.value ? 1.0 : 8.0, + 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: MyTheme.color(context).bg, + body: DesktopTab( + controller: tabController, + theme: dark + ? const TarBarTheme.dark() + : const TarBarTheme.light(), + tail: ActionIcon( + message: 'Settings', + icon: IconFont.menu, + theme: dark + ? const TarBarTheme.dark() + : const TarBarTheme.light(), + onTap: onAddSetting, + is_close: false, + ), + )); + }) + ]), + ))); } void onAddSetting() { diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 4a2f11553..b13f40a5f 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -642,47 +642,51 @@ class _FileManagerPageState extends State IconButton( onPressed: () { final name = TextEditingController(); - _ffi.dialogManager - .show((setState, close) => CustomAlertDialog( - title: Text(translate("Create Folder")), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TextFormField( - decoration: InputDecoration( - labelText: translate( - "Please enter the folder name"), - ), - controller: name, - ), - ], - ), - actions: [ - TextButton( - style: flatButtonStyle, - onPressed: () => close(false), - child: Text(translate("Cancel"))), - ElevatedButton( - style: flatButtonStyle, - onPressed: () { - if (name.value.text.isNotEmpty) { - model.createDir( - PathUtil.join( - model - .getCurrentDir( - isLocal) - .path, - name.value.text, - model.getCurrentIsWindows( - isLocal)), - isLocal: isLocal); - close(); - } - }, - child: Text(translate("OK"))) - ])); + _ffi.dialogManager.show((setState, close) { + submit() { + if (name.value.text.isNotEmpty) { + model.createDir( + PathUtil.join( + model.getCurrentDir(isLocal).path, + name.value.text, + model.getCurrentIsWindows(isLocal)), + isLocal: isLocal); + close(); + } + } + + cancel() => close(false); + return CustomAlertDialog( + title: Text(translate("Create Folder")), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextFormField( + decoration: InputDecoration( + labelText: translate( + "Please enter the folder name"), + ), + controller: name, + focusNode: FocusNode()..requestFocus(), + ), + ], + ), + actions: [ + TextButton( + style: flatButtonStyle, + onPressed: cancel, + child: Text(translate("Cancel"))), + ElevatedButton( + style: flatButtonStyle, + onPressed: submit, + child: Text(translate("OK"))) + ], + onSubmit: submit, + onCancel: cancel, + ); + }); }, - icon: Icon(Icons.create_new_folder_outlined)), + icon: const Icon(Icons.create_new_folder_outlined)), IconButton( onPressed: () async { final items = isLocal diff --git a/flutter/lib/desktop/pages/port_forward_page.dart b/flutter/lib/desktop/pages/port_forward_page.dart index 6cfd0cdb2..28ee0d70e 100644 --- a/flutter/lib/desktop/pages/port_forward_page.dart +++ b/flutter/lib/desktop/pages/port_forward_page.dart @@ -70,38 +70,45 @@ class _PortForwardPageState extends State @override Widget build(BuildContext context) { super.build(context); - return Scaffold( - backgroundColor: MyTheme.color(context).grayBg, - body: FutureBuilder(future: () async { - if (!isRdp) { - refreshTunnelConfig(); - } - }(), builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - return Container( - decoration: BoxDecoration( - border: Border.all( - width: 20, color: MyTheme.color(context).grayBg!)), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - buildPrompt(context), - Flexible( - child: Container( - decoration: BoxDecoration( - color: MyTheme.color(context).bg, - border: Border.all(width: 1, color: MyTheme.border)), - child: - widget.isRDP ? buildRdp(context) : buildTunnel(context), - ), + return Overlay(initialEntries: [ + OverlayEntry(builder: (context) { + _ffi.dialogManager.setOverlayState(Overlay.of(context)); + return Scaffold( + backgroundColor: MyTheme.color(context).grayBg, + body: FutureBuilder(future: () async { + if (!isRdp) { + refreshTunnelConfig(); + } + }(), builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + return Container( + decoration: BoxDecoration( + border: Border.all( + width: 20, color: MyTheme.color(context).grayBg!)), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + buildPrompt(context), + Flexible( + child: Container( + decoration: BoxDecoration( + color: MyTheme.color(context).bg, + border: + Border.all(width: 1, color: MyTheme.border)), + child: widget.isRDP + ? buildRdp(context) + : buildTunnel(context), + ), + ), + ], ), - ], - ), - ); - } - return const Offstage(); - }), - ); + ); + } + return const Offstage(); + }), + ); + }) + ]); } buildPrompt(BuildContext context) { diff --git a/flutter/lib/desktop/widgets/peercard_widget.dart b/flutter/lib/desktop/widgets/peercard_widget.dart index 13cf02699..1bff02508 100644 --- a/flutter/lib/desktop/widgets/peercard_widget.dart +++ b/flutter/lib/desktop/widgets/peercard_widget.dart @@ -563,47 +563,47 @@ abstract class BasePeerCard extends StatelessWidget { } } gFFI.dialogManager.show((setState, close) { + submit() async { + isInProgress.value = true; + name = controller.text; + await bind.mainSetPeerOption(id: id, key: 'alias', value: name); + if (isAddressBook) { + gFFI.abModel.setPeerOption(id, 'alias', name); + await gFFI.abModel.updateAb(); + } + alias.value = await bind.mainGetPeerOption(id: peer.id, key: 'alias'); + close(); + isInProgress.value = false; + } + return CustomAlertDialog( title: Text(translate('Rename')), content: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( - padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + padding: + const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), child: Form( child: TextFormField( controller: controller, - decoration: InputDecoration(border: OutlineInputBorder()), + focusNode: FocusNode()..requestFocus(), + decoration: + const InputDecoration(border: OutlineInputBorder()), ), ), ), Obx(() => Offstage( offstage: isInProgress.isFalse, - child: LinearProgressIndicator())), + child: const LinearProgressIndicator())), ], ), actions: [ - TextButton( - onPressed: () { - close(); - }, - child: Text(translate("Cancel"))), - TextButton( - onPressed: () async { - isInProgress.value = true; - name = controller.text; - await bind.mainSetPeerOption(id: id, key: 'alias', value: name); - if (isAddressBook) { - gFFI.abModel.setPeerOption(id, 'alias', name); - await gFFI.abModel.updateAb(); - } - alias.value = - await bind.mainGetPeerOption(id: peer.id, key: 'alias'); - close(); - isInProgress.value = false; - }, - child: Text(translate("OK"))), + TextButton(onPressed: close, child: Text(translate("Cancel"))), + TextButton(onPressed: submit, child: Text(translate("OK"))), ], + onSubmit: submit, + onCancel: close, ); }); } @@ -750,13 +750,23 @@ class AddressBookPeerCard extends BasePeerCard { var selectedTag = gFFI.abModel.getPeerTags(id).obs; gFFI.dialogManager.show((setState, close) { + submit() async { + setState(() { + isInProgress = true; + }); + gFFI.abModel.changeTagForPeer(id, selectedTag); + await gFFI.abModel.updateAb(); + close(); + } + return CustomAlertDialog( title: Text(translate("Edit Tag")), content: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( - padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + padding: + const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), child: Wrap( children: tags .map((e) => _buildTag(e, selectedTag, onTap: () { @@ -769,26 +779,16 @@ class AddressBookPeerCard extends BasePeerCard { .toList(growable: false), ), ), - Offstage(offstage: !isInProgress, child: LinearProgressIndicator()) + Offstage( + offstage: !isInProgress, child: const LinearProgressIndicator()) ], ), actions: [ - TextButton( - onPressed: () { - close(); - }, - child: Text(translate("Cancel"))), - TextButton( - onPressed: () async { - setState(() { - isInProgress = true; - }); - gFFI.abModel.changeTagForPeer(id, selectedTag); - await gFFI.abModel.updateAb(); - close(); - }, - child: Text(translate("OK"))), + TextButton(onPressed: close, child: Text(translate("Cancel"))), + TextButton(onPressed: submit, child: Text(translate("OK"))), ], + onSubmit: submit, + onCancel: close, ); }); } @@ -871,25 +871,35 @@ void _rdpDialog(String id) async { RxBool secure = true.obs; gFFI.dialogManager.show((setState, close) { + submit() async { + await bind.mainSetPeerOption( + id: id, key: 'rdp_port', value: portController.text.trim()); + await bind.mainSetPeerOption( + id: id, key: 'rdp_username', value: userController.text); + await bind.mainSetPeerOption( + id: id, key: 'rdp_password', value: passwordContorller.text); + close(); + } + return CustomAlertDialog( - title: Text('RDP ' + translate('Settings')), + title: Text('RDP ${translate('Settings')}'), content: ConstrainedBox( - constraints: BoxConstraints(minWidth: 500), + constraints: const BoxConstraints(minWidth: 500), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox( + const SizedBox( height: 8.0, ), Row( children: [ ConstrainedBox( - constraints: BoxConstraints(minWidth: 100), + constraints: const BoxConstraints(minWidth: 100), child: Text( "${translate('Port')}:", textAlign: TextAlign.start, ).marginOnly(bottom: 16.0)), - SizedBox( + const SizedBox( width: 24.0, ), Expanded( @@ -898,52 +908,54 @@ void _rdpDialog(String id) async { FilteringTextInputFormatter.allow(RegExp( 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])$')) ], - decoration: InputDecoration( + decoration: const InputDecoration( border: OutlineInputBorder(), hintText: '3389'), controller: portController, + focusNode: FocusNode()..requestFocus(), ), ), ], ), - SizedBox( + const SizedBox( height: 8.0, ), Row( children: [ ConstrainedBox( - constraints: BoxConstraints(minWidth: 100), + constraints: const BoxConstraints(minWidth: 100), child: Text( "${translate('Username')}:", textAlign: TextAlign.start, ).marginOnly(bottom: 16.0)), - SizedBox( + const SizedBox( width: 24.0, ), Expanded( child: TextField( - decoration: InputDecoration(border: OutlineInputBorder()), + decoration: + const InputDecoration(border: OutlineInputBorder()), controller: userController, ), ), ], ), - SizedBox( + const SizedBox( height: 8.0, ), Row( children: [ ConstrainedBox( - constraints: BoxConstraints(minWidth: 100), + constraints: const BoxConstraints(minWidth: 100), child: Text("${translate('Password')}:") .marginOnly(bottom: 16.0)), - SizedBox( + const SizedBox( width: 24.0, ), Expanded( child: Obx(() => TextField( obscureText: secure.value, decoration: InputDecoration( - border: OutlineInputBorder(), + border: const OutlineInputBorder(), suffixIcon: IconButton( onPressed: () => secure.value = !secure.value, icon: Icon(secure.value @@ -958,23 +970,11 @@ void _rdpDialog(String id) async { ), ), actions: [ - TextButton( - onPressed: () { - close(); - }, - child: Text(translate("Cancel"))), - TextButton( - onPressed: () async { - await bind.mainSetPeerOption( - id: id, key: 'rdp_port', value: portController.text.trim()); - await bind.mainSetPeerOption( - id: id, key: 'rdp_username', value: userController.text); - await bind.mainSetPeerOption( - id: id, key: 'rdp_password', value: passwordContorller.text); - close(); - }, - child: Text(translate("OK"))), + TextButton(onPressed: close, child: Text(translate("Cancel"))), + TextButton(onPressed: submit, child: Text(translate("OK"))), ], + onSubmit: submit, + onCancel: close, ); }); } diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index dc3c249f0..c83f61a17 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -596,46 +596,49 @@ void showSetOSPassword( var autoLogin = await bind.sessionGetOption(id: id, arg: "auto-login") != ""; controller.text = password; dialogManager.show((setState, close) { + submit() { + var text = controller.text.trim(); + bind.sessionPeerOption(id: id, name: "os-password", value: text); + bind.sessionPeerOption( + id: id, name: "auto-login", value: autoLogin ? 'Y' : ''); + if (text != "" && login) { + bind.sessionInputOsPassword(id: id, value: text); + } + close(); + } + return CustomAlertDialog( - title: Text(translate('OS Password')), - content: Column(mainAxisSize: MainAxisSize.min, children: [ - PasswordWidget(controller: controller), - CheckboxListTile( - contentPadding: const EdgeInsets.all(0), - dense: true, - controlAffinity: ListTileControlAffinity.leading, - title: Text( - translate('Auto Login'), - ), - value: autoLogin, - onChanged: (v) { - if (v == null) return; - setState(() => autoLogin = v); - }, + title: Text(translate('OS Password')), + content: Column(mainAxisSize: MainAxisSize.min, children: [ + PasswordWidget(controller: controller), + CheckboxListTile( + contentPadding: const EdgeInsets.all(0), + dense: true, + controlAffinity: ListTileControlAffinity.leading, + title: Text( + translate('Auto Login'), ), - ]), - actions: [ - TextButton( - style: flatButtonStyle, - onPressed: () { - close(); - }, - child: Text(translate('Cancel')), - ), - TextButton( - style: flatButtonStyle, - onPressed: () { - var text = controller.text.trim(); - bind.sessionPeerOption(id: id, name: "os-password", value: text); - bind.sessionPeerOption( - id: id, name: "auto-login", value: autoLogin ? 'Y' : ''); - if (text != "" && login) { - bind.sessionInputOsPassword(id: id, value: text); - } - close(); - }, - child: Text(translate('OK')), - ), - ]); + value: autoLogin, + onChanged: (v) { + if (v == null) return; + setState(() => autoLogin = v); + }, + ), + ]), + actions: [ + TextButton( + style: flatButtonStyle, + onPressed: close, + child: Text(translate('Cancel')), + ), + TextButton( + style: flatButtonStyle, + onPressed: submit, + child: Text(translate('OK')), + ), + ], + onSubmit: submit, + onCancel: close, + ); }); } diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 1a19dd833..3c2b28ab0 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -462,22 +462,24 @@ class WindowActionPanel extends StatelessWidget { } closeConfirmDialog(Function() callback) async { - final res = await gFFI.dialogManager - .show((setState, close) => CustomAlertDialog( - title: Row(children: [ - Icon(Icons.warning_amber_sharp, - color: Colors.redAccent, size: 28), - SizedBox(width: 10), - Text(translate("Warning")), - ]), - content: Text(translate("Disconnect all devices?")), - actions: [ - TextButton( - onPressed: () => close(), child: Text(translate("Cancel"))), - ElevatedButton( - onPressed: () => close(true), child: Text(translate("OK"))), - ], - )); + final res = await gFFI.dialogManager.show((setState, close) { + submit() => close(true); + return CustomAlertDialog( + title: Row(children: [ + const Icon(Icons.warning_amber_sharp, + color: Colors.redAccent, size: 28), + const SizedBox(width: 10), + Text(translate("Warning")), + ]), + content: Text(translate("Disconnect all devices?")), + actions: [ + TextButton(onPressed: close, child: Text(translate("Cancel"))), + ElevatedButton(onPressed: submit, child: Text(translate("OK"))), + ], + onSubmit: submit, + onCancel: close, + ); + }); if (res == true) { callback(); } diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index 74c2cd515..dedca5efa 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -559,49 +559,55 @@ class FileModel extends ChangeNotifier { Future showRemoveDialog( String title, String content, bool showCheckbox) async { return await parent.target?.dialogManager.show( - (setState, Function(bool v) close) => CustomAlertDialog( - title: Row( - children: [ - Icon(Icons.warning, color: Colors.red), - SizedBox(width: 20), - Text(title) - ], - ), - content: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text(content), - SizedBox(height: 5), - Text(translate("This is irreversible!"), - style: TextStyle(fontWeight: FontWeight.bold)), - showCheckbox - ? CheckboxListTile( - contentPadding: const EdgeInsets.all(0), - dense: true, - controlAffinity: ListTileControlAffinity.leading, - title: Text( - translate("Do this for all conflicts"), - ), - value: removeCheckboxRemember, - onChanged: (v) { - if (v == null) return; - setState(() => removeCheckboxRemember = v); - }, - ) - : SizedBox.shrink() - ]), - actions: [ - TextButton( - style: flatButtonStyle, - onPressed: () => close(false), - child: Text(translate("Cancel"))), - TextButton( - style: flatButtonStyle, - onPressed: () => close(true), - child: Text(translate("OK"))), - ]), - useAnimation: false); + (setState, Function(bool v) close) { + cancel() => close(false); + submit() => close(true); + return CustomAlertDialog( + title: Row( + children: [ + const Icon(Icons.warning, color: Colors.red), + const SizedBox(width: 20), + Text(title) + ], + ), + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text(content), + const SizedBox(height: 5), + Text(translate("This is irreversible!"), + style: const TextStyle(fontWeight: FontWeight.bold)), + showCheckbox + ? CheckboxListTile( + contentPadding: const EdgeInsets.all(0), + dense: true, + controlAffinity: ListTileControlAffinity.leading, + title: Text( + translate("Do this for all conflicts"), + ), + value: removeCheckboxRemember, + onChanged: (v) { + if (v == null) return; + setState(() => removeCheckboxRemember = v); + }, + ) + : const SizedBox.shrink() + ]), + actions: [ + TextButton( + style: flatButtonStyle, + onPressed: cancel, + child: Text(translate("Cancel"))), + TextButton( + style: flatButtonStyle, + onPressed: submit, + child: Text(translate("OK"))), + ], + onSubmit: submit, + onCancel: cancel, + ); + }, useAnimation: false); } bool fileConfirmCheckboxRemember = false; @@ -610,55 +616,59 @@ class FileModel extends ChangeNotifier { String title, String content, bool showCheckbox) async { fileConfirmCheckboxRemember = false; return await parent.target?.dialogManager.show( - (setState, Function(bool? v) close) => CustomAlertDialog( - title: Row( - children: [ - Icon(Icons.warning, color: Colors.red), - SizedBox(width: 20), - Text(title) - ], - ), - content: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - translate( - "This file exists, skip or overwrite this file?"), - style: TextStyle(fontWeight: FontWeight.bold)), - SizedBox(height: 5), - Text(content), - showCheckbox - ? CheckboxListTile( - contentPadding: const EdgeInsets.all(0), - dense: true, - controlAffinity: ListTileControlAffinity.leading, - title: Text( - translate("Do this for all conflicts"), - ), - value: fileConfirmCheckboxRemember, - onChanged: (v) { - if (v == null) return; - setState(() => fileConfirmCheckboxRemember = v); - }, - ) - : SizedBox.shrink() - ]), - actions: [ - TextButton( - style: flatButtonStyle, - onPressed: () => close(false), - child: Text(translate("Cancel"))), - TextButton( - style: flatButtonStyle, - onPressed: () => close(null), - child: Text(translate("Skip"))), - TextButton( - style: flatButtonStyle, - onPressed: () => close(true), - child: Text(translate("OK"))), - ]), - useAnimation: false); + (setState, Function(bool? v) close) { + cancel() => close(false); + submit() => close(true); + return CustomAlertDialog( + title: Row( + children: [ + const Icon(Icons.warning, color: Colors.red), + const SizedBox(width: 20), + Text(title) + ], + ), + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text(translate("This file exists, skip or overwrite this file?"), + style: const TextStyle(fontWeight: FontWeight.bold)), + const SizedBox(height: 5), + Text(content), + showCheckbox + ? CheckboxListTile( + contentPadding: const EdgeInsets.all(0), + dense: true, + controlAffinity: ListTileControlAffinity.leading, + title: Text( + translate("Do this for all conflicts"), + ), + value: fileConfirmCheckboxRemember, + onChanged: (v) { + if (v == null) return; + setState(() => fileConfirmCheckboxRemember = v); + }, + ) + : const SizedBox.shrink() + ]), + actions: [ + TextButton( + style: flatButtonStyle, + onPressed: cancel, + child: Text(translate("Cancel"))), + TextButton( + style: flatButtonStyle, + onPressed: () => close(null), + child: Text(translate("Skip"))), + TextButton( + style: flatButtonStyle, + onPressed: submit, + child: Text(translate("OK"))), + ], + onSubmit: submit, + onCancel: cancel, + ); + }, useAnimation: false); } sendRemoveFile(String path, int fileNum, bool isLocal) { diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index 5d23dc949..f78f8cf70 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -209,46 +209,48 @@ class ServerModel with ChangeNotifier { /// Toggle the screen sharing service. toggleService() async { if (_isStart) { - final res = await parent.target?.dialogManager - .show((setState, close) => CustomAlertDialog( - title: Row(children: [ - Icon(Icons.warning_amber_sharp, - color: Colors.redAccent, size: 28), - SizedBox(width: 10), - Text(translate("Warning")), - ]), - content: Text(translate("android_stop_service_tip")), - actions: [ - TextButton( - onPressed: () => close(), - child: Text(translate("Cancel"))), - ElevatedButton( - onPressed: () => close(true), - child: Text(translate("OK"))), - ], - )); + final res = + await parent.target?.dialogManager.show((setState, close) { + submit() => close(true); + return CustomAlertDialog( + title: Row(children: [ + const Icon(Icons.warning_amber_sharp, + color: Colors.redAccent, size: 28), + const SizedBox(width: 10), + Text(translate("Warning")), + ]), + content: Text(translate("android_stop_service_tip")), + actions: [ + TextButton(onPressed: close, child: Text(translate("Cancel"))), + ElevatedButton(onPressed: submit, child: Text(translate("OK"))), + ], + onSubmit: submit, + onCancel: close, + ); + }); if (res == true) { stopService(); } } else { - final res = await parent.target?.dialogManager - .show((setState, close) => CustomAlertDialog( - title: Row(children: [ - Icon(Icons.warning_amber_sharp, - color: Colors.redAccent, size: 28), - SizedBox(width: 10), - Text(translate("Warning")), - ]), - content: Text(translate("android_service_will_start_tip")), - actions: [ - TextButton( - onPressed: () => close(), - child: Text(translate("Cancel"))), - ElevatedButton( - onPressed: () => close(true), - child: Text(translate("OK"))), - ], - )); + final res = + await parent.target?.dialogManager.show((setState, close) { + submit() => close(true); + return CustomAlertDialog( + title: Row(children: [ + const Icon(Icons.warning_amber_sharp, + color: Colors.redAccent, size: 28), + const SizedBox(width: 10), + Text(translate("Warning")), + ]), + content: Text(translate("android_service_will_start_tip")), + actions: [ + TextButton(onPressed: close, child: Text(translate("Cancel"))), + ElevatedButton(onPressed: submit, child: Text(translate("OK"))), + ], + onSubmit: submit, + onCancel: close, + ); + }); if (res == true) { startService(); } @@ -388,49 +390,49 @@ class ServerModel with ChangeNotifier { } void showLoginDialog(Client client) { - parent.target?.dialogManager.show( - (setState, close) => CustomAlertDialog( - title: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(translate(client.isFileTransfer - ? "File Connection" - : "Screen Connection")), - IconButton( - onPressed: () { - close(); - }, - icon: Icon(Icons.close)) - ]), - content: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(translate("Do you accept?")), - clientInfo(client), - Text( - translate("android_new_connection_tip"), - style: TextStyle(color: Colors.black54), - ), - ], - ), - actions: [ - TextButton( - child: Text(translate("Dismiss")), - onPressed: () { - sendLoginResponse(client, false); - close(); - }), - ElevatedButton( - child: Text(translate("Accept")), - onPressed: () { - sendLoginResponse(client, true); - close(); - }), - ], + parent.target?.dialogManager.show((setState, close) { + cancel() { + sendLoginResponse(client, false); + close(); + } + + submit() { + sendLoginResponse(client, true); + close(); + } + + return CustomAlertDialog( + title: + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + Text(translate( + client.isFileTransfer ? "File Connection" : "Screen Connection")), + IconButton( + onPressed: () { + close(); + }, + icon: const Icon(Icons.close)) + ]), + content: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(translate("Do you accept?")), + clientInfo(client), + Text( + translate("android_new_connection_tip"), + style: const TextStyle(color: Colors.black54), ), - tag: getLoginDialogTag(client.id)); + ], + ), + actions: [ + TextButton(onPressed: cancel, child: Text(translate("Dismiss"))), + ElevatedButton(onPressed: submit, child: Text(translate("Accept"))), + ], + onSubmit: submit, + onCancel: cancel, + ); + }, tag: getLoginDialogTag(client.id)); } scrollToBottom() { @@ -563,24 +565,29 @@ String getLoginDialogTag(int id) { } showInputWarnAlert(FFI ffi) { - ffi.dialogManager.show((setState, close) => CustomAlertDialog( - title: Text(translate("How to get Android input permission?")), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(translate("android_input_permission_tip1")), - SizedBox(height: 10), - Text(translate("android_input_permission_tip2")), - ], - ), - actions: [ - TextButton(child: Text(translate("Cancel")), onPressed: close), - ElevatedButton( - child: Text(translate("Open System Setting")), - onPressed: () { - ffi.serverModel.initInput(); - close(); - }), + ffi.dialogManager.show((setState, close) { + submit() { + ffi.serverModel.initInput(); + close(); + } + + return CustomAlertDialog( + title: Text(translate("How to get Android input permission?")), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(translate("android_input_permission_tip1")), + const SizedBox(height: 10), + Text(translate("android_input_permission_tip2")), ], - )); + ), + actions: [ + TextButton(onPressed: close, child: Text(translate("Cancel"))), + ElevatedButton( + onPressed: submit, child: Text(translate("Open System Setting"))), + ], + onSubmit: submit, + onCancel: close, + ); + }); } From 925a9e43cb442d1c462c1aea5477f315eb9ede55 Mon Sep 17 00:00:00 2001 From: asur4s Date: Sat, 3 Sep 2022 21:49:58 -0400 Subject: [PATCH 0372/2015] Refactor: env of keyboard mode --- src/common.rs | 11 +++++++++++ src/ui/remote.rs | 1 + src/ui_session_interface.rs | 7 +++---- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/common.rs b/src/common.rs index 81adebb0a..2be4b5295 100644 --- a/src/common.rs +++ b/src/common.rs @@ -711,3 +711,14 @@ pub fn make_fd_flutter(id: i32, entries: &Vec, only_count: bool) -> S m.insert("total_size".into(), json!(n as f64)); serde_json::to_string(&m).unwrap_or("".into()) } + + +pub fn get_keyboard_mode() -> String { + return std::env::var("KEYBOARD_MODE") + .unwrap_or(String::from("legacy")) + .to_lowercase(); +} + +pub fn save_keyboard_mode(value: String) { + std::env::set_var("KEYBOARD_MODE", value); +} \ No newline at end of file diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 093ad901e..26f323831 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -1,3 +1,4 @@ +use crate::common::{get_keyboard_mode, save_keyboard_mode}; use std::{ collections::HashMap, ops::{Deref, DerefMut}, diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 92a08ef3d..05f34f37a 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -7,6 +7,7 @@ use crate::client::{ QualityStatus, KEY_MAP, SERVER_KEYBOARD_ENABLED, }; use crate::{client::Data, client::Interface}; +use crate::common::{get_keyboard_mode, save_keyboard_mode}; use async_trait::async_trait; use hbb_common::config::{Config, LocalConfig, PeerConfig}; @@ -58,13 +59,11 @@ impl Session { } pub fn get_keyboard_mode(&self) -> String { - return std::env::var("KEYBOARD_MODE") - .unwrap_or(String::from("legacy")) - .to_lowercase(); + return get_keyboard_mode(); } pub fn save_keyboard_mode(&self, value: String) { - std::env::set_var("KEYBOARD_MODE", value); + save_keyboard_mode(value); } pub fn save_view_style(&mut self, value: String) { From 62870e453ca0b876ca481dc67566d0d787d04e29 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sun, 4 Sep 2022 11:03:16 +0800 Subject: [PATCH 0373/2015] add tabbar theme extension to fix theme update failure after overlay added Signed-off-by: 21pages --- flutter/lib/common.dart | 10 +- .../desktop/pages/connection_tab_page.dart | 6 +- .../lib/desktop/pages/desktop_tab_page.dart | 8 +- .../desktop/pages/file_manager_tab_page.dart | 7 +- .../desktop/pages/port_forward_tab_page.dart | 6 +- flutter/lib/desktop/pages/server_page.dart | 1 - .../lib/desktop/widgets/tabbar_widget.dart | 262 ++++++++++-------- 7 files changed, 162 insertions(+), 138 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 4a0a0dc82..309ae9892 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -155,7 +155,7 @@ class MyTheme { brightness: Brightness.light, primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, - tabBarTheme: TabBarTheme( + tabBarTheme: const TabBarTheme( labelColor: Colors.black87, ), splashColor: Colors.transparent, @@ -163,13 +163,14 @@ class MyTheme { ).copyWith( extensions: >[ ColorThemeExtension.light, + TabbarTheme.light, ], ); static ThemeData darkTheme = ThemeData( brightness: Brightness.dark, primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, - tabBarTheme: TabBarTheme( + tabBarTheme: const TabBarTheme( labelColor: Colors.white70, ), splashColor: Colors.transparent, @@ -177,12 +178,17 @@ class MyTheme { ).copyWith( extensions: >[ ColorThemeExtension.dark, + TabbarTheme.dark, ], ); static ColorThemeExtension color(BuildContext context) { return Theme.of(context).extension()!; } + + static TabbarTheme tabbar(BuildContext context) { + return Theme.of(context).extension()!; + } } bool isDarkTheme() { diff --git a/flutter/lib/desktop/pages/connection_tab_page.dart b/flutter/lib/desktop/pages/connection_tab_page.dart index d9bc86fe2..8f5350792 100644 --- a/flutter/lib/desktop/pages/connection_tab_page.dart +++ b/flutter/lib/desktop/pages/connection_tab_page.dart @@ -83,7 +83,6 @@ class _ConnectionTabPageState extends State { @override Widget build(BuildContext context) { - final theme = isDarkTheme() ? TarBarTheme.dark() : TarBarTheme.light(); final RxBool fullscreen = Get.find(tag: 'fullscreen'); return Obx(() => SubWindowDragToResizeArea( resizeEdgeSize: fullscreen.value ? 1.0 : 8.0, @@ -95,14 +94,11 @@ class _ConnectionTabPageState extends State { backgroundColor: MyTheme.color(context).bg, body: Obx(() => DesktopTab( controller: tabController, - theme: theme, showTabBar: fullscreen.isFalse, onClose: () { tabController.clear(); }, - tail: AddButton( - theme: theme, - ).paddingOnly(left: 10), + tail: AddButton().paddingOnly(left: 10), pageViewBuilder: (pageView) { WindowController.fromWindowId(windowId()) .setFullscreen(fullscreen.isTrue); diff --git a/flutter/lib/desktop/pages/desktop_tab_page.dart b/flutter/lib/desktop/pages/desktop_tab_page.dart index 0546f0503..87082284b 100644 --- a/flutter/lib/desktop/pages/desktop_tab_page.dart +++ b/flutter/lib/desktop/pages/desktop_tab_page.dart @@ -48,17 +48,11 @@ class _DesktopTabPageState extends State { backgroundColor: MyTheme.color(context).bg, body: DesktopTab( controller: tabController, - theme: dark - ? const TarBarTheme.dark() - : const TarBarTheme.light(), tail: ActionIcon( message: 'Settings', icon: IconFont.menu, - theme: dark - ? const TarBarTheme.dark() - : const TarBarTheme.light(), onTap: onAddSetting, - is_close: false, + isClose: false, ), )); }) diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index add5eed9f..18ea039a7 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -62,8 +62,6 @@ class _FileManagerTabPageState extends State { @override Widget build(BuildContext context) { - final theme = - isDarkTheme() ? const TarBarTheme.dark() : const TarBarTheme.light(); return SubWindowDragToResizeArea( windowId: windowId(), child: Container( @@ -73,13 +71,10 @@ class _FileManagerTabPageState extends State { backgroundColor: MyTheme.color(context).bg, body: DesktopTab( controller: tabController, - theme: theme, onClose: () { tabController.clear(); }, - tail: AddButton( - theme: theme, - ).paddingOnly(left: 10), + tail: AddButton().paddingOnly(left: 10), )), ), ); diff --git a/flutter/lib/desktop/pages/port_forward_tab_page.dart b/flutter/lib/desktop/pages/port_forward_tab_page.dart index 2340a4ca1..e0384b614 100644 --- a/flutter/lib/desktop/pages/port_forward_tab_page.dart +++ b/flutter/lib/desktop/pages/port_forward_tab_page.dart @@ -69,7 +69,6 @@ class _PortForwardTabPageState extends State { @override Widget build(BuildContext context) { - final theme = isDarkTheme() ? TarBarTheme.dark() : TarBarTheme.light(); return SubWindowDragToResizeArea( windowId: windowId(), child: Container( @@ -79,13 +78,10 @@ class _PortForwardTabPageState extends State { backgroundColor: MyTheme.color(context).bg, body: DesktopTab( controller: tabController, - theme: theme, onClose: () { tabController.clear(); }, - tail: AddButton( - theme: theme, - ).paddingOnly(left: 10), + tail: AddButton().paddingOnly(left: 10), )), ), ); diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index f64adfca2..ac2fb7caa 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -118,7 +118,6 @@ class ConnectionManagerState extends State { ], ) : DesktopTab( - theme: isDarkTheme() ? TarBarTheme.dark() : TarBarTheme.light(), showTitle: false, showMaximize: false, showMinimize: true, diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 3c2b28ab0..5daa1aeb6 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'dart:math'; import 'package:desktop_multi_window/desktop_multi_window.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart' hide TabBarTheme; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/main.dart'; @@ -158,8 +158,7 @@ class DesktopTabController { class TabThemeConf { double iconSize; - TarBarTheme theme; - TabThemeConf({required this.iconSize, required this.theme}); + TabThemeConf({required this.iconSize}); } typedef TabBuilder = Widget Function( @@ -168,7 +167,6 @@ typedef LabelGetter = Rx Function(String key); class DesktopTab extends StatelessWidget { final Function(String)? onTabClose; - final TarBarTheme theme; final bool showTabBar; final bool showLogo; final bool showTitle; @@ -189,7 +187,6 @@ class DesktopTab extends StatelessWidget { DesktopTab({ Key? key, required this.controller, - this.theme = const TarBarTheme.light(), this.onTabClose, this.showTabBar = true, this.showLogo = true, @@ -213,15 +210,15 @@ class DesktopTab extends StatelessWidget { return Column(children: [ Offstage( offstage: !showTabBar, - child: Container( + child: SizedBox( height: _kTabBarHeight, child: Column( children: [ - Container( + SizedBox( height: _kTabBarHeight - 1, child: _buildBar(), ), - Divider( + const Divider( height: 1, thickness: 1, ), @@ -300,7 +297,7 @@ class DesktopTab extends StatelessWidget { )), Offstage( offstage: !showTitle, - child: Text( + child: const Text( "RustDesk", style: TextStyle(fontSize: 13), ).marginOnly(left: 2)) @@ -321,7 +318,6 @@ class DesktopTab extends StatelessWidget { child: _ListView( controller: controller, onTabClose: onTabClose, - theme: theme, tabBuilder: tabBuilder, labelGetter: labelGetter, )), @@ -334,7 +330,6 @@ class DesktopTab extends StatelessWidget { mainTab: isMainWindow, tabType: tabType, state: state, - theme: theme, showMinimize: showMinimize, showMaximize: showMaximize, showClose: showClose, @@ -349,7 +344,6 @@ class WindowActionPanel extends StatelessWidget { final bool mainTab; final DesktopTabType tabType; final Rx state; - final TarBarTheme theme; final bool showMinimize; final bool showMaximize; @@ -361,7 +355,6 @@ class WindowActionPanel extends StatelessWidget { required this.mainTab, required this.tabType, required this.state, - required this.theme, this.showMinimize = true, this.showMaximize = true, this.showClose = true, @@ -377,7 +370,6 @@ class WindowActionPanel extends StatelessWidget { child: ActionIcon( message: 'Minimize', icon: IconFont.min, - theme: theme, onTap: () { if (mainTab) { windowManager.minimize(); @@ -385,31 +377,30 @@ class WindowActionPanel extends StatelessWidget { WindowController.fromWindowId(windowId!).minimize(); } }, - is_close: false, + isClose: false, )), // TODO: drag makes window restore Offstage( offstage: !showMaximize, child: FutureBuilder(builder: (context, snapshot) { - RxBool is_maximized = false.obs; + RxBool isMaximized = false.obs; if (mainTab) { windowManager.isMaximized().then((maximized) { - is_maximized.value = maximized; + isMaximized.value = maximized; }); } else { final wc = WindowController.fromWindowId(windowId!); wc.isMaximized().then((maximized) { - is_maximized.value = maximized; + isMaximized.value = maximized; }); } return Obx( () => ActionIcon( - message: is_maximized.value ? "Restore" : "Maximize", - icon: is_maximized.value ? IconFont.restore : IconFont.max, - theme: theme, + message: isMaximized.value ? "Restore" : "Maximize", + icon: isMaximized.value ? IconFont.restore : IconFont.max, onTap: () { if (mainTab) { - if (is_maximized.value) { + if (isMaximized.value) { windowManager.unmaximize(); } else { windowManager.maximize(); @@ -417,15 +408,15 @@ class WindowActionPanel extends StatelessWidget { } else { // TODO: subwindow is maximized but first query result is not maximized. final wc = WindowController.fromWindowId(windowId!); - if (is_maximized.value) { + if (isMaximized.value) { wc.unmaximize(); } else { wc.maximize(); } } - is_maximized.value = !is_maximized.value; + isMaximized.value = !isMaximized.value; }, - is_close: false, + isClose: false, ), ); })), @@ -434,7 +425,6 @@ class WindowActionPanel extends StatelessWidget { child: ActionIcon( message: 'Close', icon: IconFont.close, - theme: theme, onTap: () async { action() { if (mainTab) { @@ -455,7 +445,7 @@ class WindowActionPanel extends StatelessWidget { action(); } }, - is_close: true, + isClose: true, )), ], ); @@ -490,17 +480,15 @@ class WindowActionPanel extends StatelessWidget { class _ListView extends StatelessWidget { final DesktopTabController controller; final Function(String key)? onTabClose; - final TarBarTheme theme; final TabBuilder? tabBuilder; final LabelGetter? labelGetter; Rx get state => controller.state; - _ListView( + const _ListView( {required this.controller, required this.onTabClose, - required this.theme, this.tabBuilder, this.labelGetter}); @@ -510,7 +498,7 @@ class _ListView extends StatelessWidget { controller: state.value.scrollController, scrollDirection: Axis.horizontal, shrinkWrap: true, - physics: BouncingScrollPhysics(), + physics: const BouncingScrollPhysics(), children: state.value.tabs.asMap().entries.map((e) { final index = e.key; final tab = e.value; @@ -525,7 +513,6 @@ class _ListView extends StatelessWidget { selected: state.value.selected, onClose: () => controller.remove(index), onSelected: () => controller.jumpTo(index), - theme: theme, tabBuilder: tabBuilder == null ? null : (Widget icon, Widget labelWidget, TabThemeConf themeConf) { @@ -542,31 +529,29 @@ class _ListView extends StatelessWidget { } class _Tab extends StatefulWidget { - late final int index; - late final Rx label; - late final IconData? selectedIcon; - late final IconData? unselectedIcon; - late final bool closable; - late final int selected; - late final Function() onClose; - late final Function() onSelected; - late final TarBarTheme theme; + final int index; + final Rx label; + final IconData? selectedIcon; + final IconData? unselectedIcon; + final bool closable; + final int selected; + final Function() onClose; + final Function() onSelected; final Widget Function(Widget icon, Widget label, TabThemeConf themeConf)? tabBuilder; - _Tab( - {Key? key, - required this.index, - required this.label, - this.selectedIcon, - this.unselectedIcon, - this.tabBuilder, - required this.closable, - required this.selected, - required this.onClose, - required this.onSelected, - required this.theme}) - : super(key: key); + const _Tab({ + Key? key, + required this.index, + required this.label, + this.selectedIcon, + this.unselectedIcon, + this.tabBuilder, + required this.closable, + required this.selected, + required this.onClose, + required this.onSelected, + }) : super(key: key); @override State<_Tab> createState() => _TabState(); @@ -586,8 +571,8 @@ class _TabState extends State<_Tab> with RestorationMixin { isSelected ? widget.selectedIcon : widget.unselectedIcon, size: _kIconSize, color: isSelected - ? widget.theme.selectedtabIconColor - : widget.theme.unSelectedtabIconColor, + ? MyTheme.tabbar(context).selectedTabIconColor + : MyTheme.tabbar(context).unSelectedTabIconColor, ).paddingOnly(right: 5)); final labelWidget = Obx(() { return Text( @@ -595,8 +580,8 @@ class _TabState extends State<_Tab> with RestorationMixin { textAlign: TextAlign.center, style: TextStyle( color: isSelected - ? widget.theme.selectedTextColor - : widget.theme.unSelectedTextColor), + ? MyTheme.tabbar(context).selectedTextColor + : MyTheme.tabbar(context).unSelectedTextColor), ); }); @@ -609,8 +594,8 @@ class _TabState extends State<_Tab> with RestorationMixin { ], ); } else { - return widget.tabBuilder!(icon, labelWidget, - TabThemeConf(iconSize: _kIconSize, theme: widget.theme)); + return widget.tabBuilder!( + icon, labelWidget, TabThemeConf(iconSize: _kIconSize)); } } @@ -639,7 +624,6 @@ class _TabState extends State<_Tab> with RestorationMixin { visiable: hover.value && widget.closable, tabSelected: isSelected, onClose: () => widget.onClose(), - theme: widget.theme, ))) ])).paddingSymmetric(horizontal: 10), Offstage( @@ -648,7 +632,7 @@ class _TabState extends State<_Tab> with RestorationMixin { width: 1, indent: _kDividerIndent, endIndent: _kDividerIndent, - color: widget.theme.dividerColor, + color: MyTheme.tabbar(context).dividerColor, thickness: 1, ), ) @@ -671,14 +655,12 @@ class _CloseButton extends StatelessWidget { final bool visiable; final bool tabSelected; final Function onClose; - late final TarBarTheme theme; - _CloseButton({ + const _CloseButton({ Key? key, required this.visiable, required this.tabSelected, required this.onClose, - required this.theme, }) : super(key: key); @override @@ -694,8 +676,8 @@ class _CloseButton extends StatelessWidget { Icons.close, size: _kIconSize, color: tabSelected - ? theme.selectedIconColor - : theme.unSelectedIconColor, + ? MyTheme.tabbar(context).selectedIconColor + : MyTheme.tabbar(context).unSelectedIconColor, ), ), )).paddingOnly(left: 5); @@ -705,16 +687,14 @@ class _CloseButton extends StatelessWidget { class ActionIcon extends StatelessWidget { final String message; final IconData icon; - final TarBarTheme theme; final Function() onTap; - final bool is_close; + final bool isClose; const ActionIcon({ Key? key, required this.message, required this.icon, - required this.theme, required this.onTap, - required this.is_close, + required this.isClose, }) : super(key: key); @override @@ -722,34 +702,32 @@ class ActionIcon extends StatelessWidget { RxBool hover = false.obs; return Obx(() => Tooltip( message: translate(message), - waitDuration: Duration(seconds: 1), + waitDuration: const Duration(seconds: 1), child: InkWell( - hoverColor: - is_close ? Color.fromARGB(255, 196, 43, 28) : theme.hoverColor, + hoverColor: isClose + ? const Color.fromARGB(255, 196, 43, 28) + : MyTheme.tabbar(context).hoverColor, onHover: (value) => hover.value = value, - child: Container( + onTap: onTap, + child: SizedBox( height: _kTabBarHeight - 1, width: _kTabBarHeight - 1, child: Icon( icon, - color: hover.value && is_close + color: hover.value && isClose ? Colors.white - : theme.unSelectedIconColor, + : MyTheme.tabbar(context).unSelectedIconColor, size: _kActionIconSize, ), ), - onTap: onTap, ), )); } } class AddButton extends StatelessWidget { - late final TarBarTheme theme; - - AddButton({ + const AddButton({ Key? key, - required this.theme, }) : super(key: key); @override @@ -757,41 +735,101 @@ class AddButton extends StatelessWidget { return ActionIcon( message: 'New Connection', icon: IconFont.add, - theme: theme, onTap: () => rustDeskWinManager.call(WindowType.Main, "main_window_on_top", ""), - is_close: false); + isClose: false); } } -class TarBarTheme { - final Color unSelectedtabIconColor; - final Color selectedtabIconColor; - final Color selectedTextColor; - final Color unSelectedTextColor; - final Color selectedIconColor; - final Color unSelectedIconColor; - final Color dividerColor; - final Color hoverColor; +class TabbarTheme extends ThemeExtension { + final Color? selectedTabIconColor; + final Color? unSelectedTabIconColor; + final Color? selectedTextColor; + final Color? unSelectedTextColor; + final Color? selectedIconColor; + final Color? unSelectedIconColor; + final Color? dividerColor; + final Color? hoverColor; - const TarBarTheme.light() - : unSelectedtabIconColor = const Color.fromARGB(255, 162, 203, 241), - selectedtabIconColor = MyTheme.accent, - selectedTextColor = const Color.fromARGB(255, 26, 26, 26), - unSelectedTextColor = const Color.fromARGB(255, 96, 96, 96), - selectedIconColor = const Color.fromARGB(255, 26, 26, 26), - unSelectedIconColor = const Color.fromARGB(255, 96, 96, 96), - dividerColor = const Color.fromARGB(255, 238, 238, 238), - hoverColor = const Color.fromARGB( - 51, 158, 158, 158); // Colors.grey; //0xFF9E9E9E + const TabbarTheme( + {required this.selectedTabIconColor, + required this.unSelectedTabIconColor, + required this.selectedTextColor, + required this.unSelectedTextColor, + required this.selectedIconColor, + required this.unSelectedIconColor, + required this.dividerColor, + required this.hoverColor}); - const TarBarTheme.dark() - : unSelectedtabIconColor = const Color.fromARGB(255, 30, 65, 98), - selectedtabIconColor = MyTheme.accent, - selectedTextColor = const Color.fromARGB(255, 255, 255, 255), - unSelectedTextColor = const Color.fromARGB(255, 207, 207, 207), - selectedIconColor = const Color.fromARGB(255, 215, 215, 215), - unSelectedIconColor = const Color.fromARGB(255, 255, 255, 255), - dividerColor = const Color.fromARGB(255, 64, 64, 64), - hoverColor = Colors.black26; + static const light = TabbarTheme( + selectedTabIconColor: MyTheme.accent, + unSelectedTabIconColor: Color.fromARGB(255, 162, 203, 241), + selectedTextColor: Color.fromARGB(255, 26, 26, 26), + unSelectedTextColor: Color.fromARGB(255, 96, 96, 96), + selectedIconColor: Color.fromARGB(255, 26, 26, 26), + unSelectedIconColor: Color.fromARGB(255, 96, 96, 96), + dividerColor: Color.fromARGB(255, 238, 238, 238), + hoverColor: Color.fromARGB(51, 158, 158, 158)); + + static const dark = TabbarTheme( + selectedTabIconColor: MyTheme.accent, + unSelectedTabIconColor: Color.fromARGB(255, 30, 65, 98), + selectedTextColor: Color.fromARGB(255, 255, 255, 255), + unSelectedTextColor: Color.fromARGB(255, 207, 207, 207), + selectedIconColor: Color.fromARGB(255, 215, 215, 215), + unSelectedIconColor: Color.fromARGB(255, 255, 255, 255), + dividerColor: Color.fromARGB(255, 64, 64, 64), + hoverColor: Colors.black26); + + @override + ThemeExtension copyWith({ + Color? selectedTabIconColor, + Color? unSelectedTabIconColor, + Color? selectedTextColor, + Color? unSelectedTextColor, + Color? selectedIconColor, + Color? unSelectedIconColor, + Color? dividerColor, + Color? hoverColor, + }) { + return TabbarTheme( + selectedTabIconColor: selectedTabIconColor ?? this.selectedTabIconColor, + unSelectedTabIconColor: + unSelectedTabIconColor ?? this.unSelectedTabIconColor, + selectedTextColor: selectedTextColor ?? this.selectedTextColor, + unSelectedTextColor: unSelectedTextColor ?? this.unSelectedTextColor, + selectedIconColor: selectedIconColor ?? this.selectedIconColor, + unSelectedIconColor: unSelectedIconColor ?? this.unSelectedIconColor, + dividerColor: dividerColor ?? this.dividerColor, + hoverColor: hoverColor ?? this.hoverColor, + ); + } + + @override + ThemeExtension lerp( + ThemeExtension? other, double t) { + if (other is! TabbarTheme) { + return this; + } + return TabbarTheme( + selectedTabIconColor: + Color.lerp(selectedTabIconColor, other.selectedTabIconColor, t), + unSelectedTabIconColor: + Color.lerp(unSelectedTabIconColor, other.unSelectedTabIconColor, t), + selectedTextColor: + Color.lerp(selectedTextColor, other.selectedTextColor, t), + unSelectedTextColor: + Color.lerp(unSelectedTextColor, other.unSelectedTextColor, t), + selectedIconColor: + Color.lerp(selectedIconColor, other.selectedIconColor, t), + unSelectedIconColor: + Color.lerp(unSelectedIconColor, other.unSelectedIconColor, t), + dividerColor: Color.lerp(dividerColor, other.dividerColor, t), + hoverColor: Color.lerp(hoverColor, other.hoverColor, t), + ); + } + + static color(BuildContext context) { + return Theme.of(context).extension()!; + } } From 071720fe8b7761684c977375e1da27b748c68d24 Mon Sep 17 00:00:00 2001 From: asur4s Date: Sat, 3 Sep 2022 23:30:41 -0400 Subject: [PATCH 0374/2015] Feat: Support map keyboard mode in wayland --- src/server/input_service.rs | 31 +++++++++++++++++++++++++------ src/server/uinput.rs | 12 +++++++++++- 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/src/server/input_service.rs b/src/server/input_service.rs index f8edef399..26806a7a5 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -8,7 +8,7 @@ use rdev::{simulate, EventType, Key as RdevKey}; use std::{ convert::TryFrom, sync::atomic::{AtomicBool, Ordering}, - time::Instant + time::Instant, }; use tfc::{traits::*, Context as TFC_Context, Key as TFC_Key}; @@ -648,6 +648,30 @@ fn map_keyboard_mode(evt: &KeyEvent) { // map mode(1): Send keycode according to the peer platform. let (click_capslock, click_numlock) = sync_status(evt); + // Wayland + if !*IS_X11.lock().unwrap() { + let mut en = ENIGO.lock().unwrap(); + let code = evt.chr() as u16; + + #[cfg(not(target_os = "macos"))] + if click_capslock { + en.key_click(enigo::Key::CapsLock); + } + #[cfg(not(target_os = "macos"))] + if click_numlock { + en.key_click(enigo::Key::NumLock); + } + #[cfg(target_os = "macos")] + en.key_down(enigo::Key::CapsLock); + + if evt.down { + en.key_down(enigo::Key::Raw(code)); + } else { + en.key_up(enigo::Key::Raw(code)); + } + return; + } + #[cfg(not(target_os = "macos"))] if click_capslock { rdev_key_click(RdevKey::CapsLock); @@ -660,11 +684,6 @@ fn map_keyboard_mode(evt: &KeyEvent) { if evt.down && click_capslock { rdev_key_down_or_up(RdevKey::CapsLock, evt.down); } - log::info!( - "click capslog {:?} click_numlock {:?}", - click_capslock, - click_numlock - ); rdev_key_down_or_up(RdevKey::Unknown(evt.chr()), evt.down); return; diff --git a/src/server/uinput.rs b/src/server/uinput.rs index 7a6d47cff..85b79ddb0 100644 --- a/src/server/uinput.rs +++ b/src/server/uinput.rs @@ -314,7 +314,7 @@ pub mod service { fn map_key(key: &enigo::Key) -> ResultType { if let Some(k) = KEY_MAP.get(&key) { - log::trace!("mapkey {:?}, get {:?}", &key, &k); + log::info!("mapkey {:?}, get {:?}", &key, &k); return Ok(k.clone()); } else { match key { @@ -350,6 +350,16 @@ pub mod service { DataKeyboard::Sequence(_seq) => { // ignore } + DataKeyboard::KeyDown(enigo::Key::Raw(code)) => { + log::info!("keycode {:?}", *code - 8); + let down_event = InputEvent::new(EventType::KEY, *code - 8, 1); + allow_err!(keyboard.emit(&[down_event])); + } + DataKeyboard::KeyUp(enigo::Key::Raw(code)) => { + log::info!("keycode {:?}", *code - 8); + let down_event = InputEvent::new(EventType::KEY, *code - 8, 0); + allow_err!(keyboard.emit(&[down_event])); + } DataKeyboard::KeyDown(key) => { if let Ok(k) = map_key(key) { let down_event = InputEvent::new(EventType::KEY, k.code(), 1); From 7c2f26eab261798445ab072cc129ce196d276cb2 Mon Sep 17 00:00:00 2001 From: asur4s Date: Sun, 4 Sep 2022 02:29:14 -0400 Subject: [PATCH 0375/2015] Fix numlock in wayland --- src/server/input_service.rs | 50 ++++++------------------------------- src/server/uinput.rs | 13 +++++++--- 2 files changed, 17 insertions(+), 46 deletions(-) diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 26806a7a5..3765eb9e0 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -820,10 +820,7 @@ fn legacy_keyboard_mode(evt: &KeyEvent) { } #[cfg(not(target_os = "macos"))] let mut to_release = Vec::new(); - #[cfg(not(target_os = "macos"))] - let mut has_cap = false; - #[cfg(windows)] - let mut has_numlock = false; + if evt.down { let ck = if let Some(key_event::Union::ControlKey(ck)) = evt.union { ck.value() @@ -846,45 +843,19 @@ fn legacy_keyboard_mode(evt: &KeyEvent) { } } #[cfg(not(target_os = "macos"))] - { - if key == &Key::CapsLock { - has_cap = true; - } else if key == &Key::NumLock { - #[cfg(windows)] - { - has_numlock = true; - } + if !get_modifier_state(key.clone(), &mut en) { + if *IS_X11.lock().unwrap() { + tfc_key_down_or_up(key.clone(), true, false); } else { - if !get_modifier_state(key.clone(), &mut en) { - if *IS_X11.lock().unwrap() { - tfc_key_down_or_up(key.clone(), true, false); - } else { - en.key_down(key.clone()).ok(); - } - modifier_sleep(); - to_release.push(key); - } + en.key_down(key.clone()).ok(); } + modifier_sleep(); + to_release.push(key); } } } } - #[cfg(not(target_os = "macos"))] - if has_cap != en.get_key_state(Key::CapsLock) { - if *IS_X11.lock().unwrap() { - tfc_key_down_or_up(Key::CapsLock, true, true); - } else { - en.key_down(Key::CapsLock).ok(); - en.key_up(Key::CapsLock); - } - } - #[cfg(windows)] - if crate::common::valid_for_numlock(evt) { - if has_numlock != en.get_key_state(Key::NumLock) { - en.key_down(Key::NumLock).ok(); - en.key_up(Key::NumLock); - } - } + match evt.union { Some(key_event::Union::ControlKey(ck)) => { if let Some(key) = KEY_MAP.get(&ck.value()) { @@ -979,11 +950,6 @@ fn legacy_keyboard_mode(evt: &KeyEvent) { en.key_up(key.clone()); } } - #[cfg(windows)] - if disable_numlock { - en.key_down(Key::NumLock).ok(); - en.key_up(Key::NumLock); - } } fn translate_keyboard_mode(evt: &KeyEvent) { diff --git a/src/server/uinput.rs b/src/server/uinput.rs index 85b79ddb0..5051d548d 100644 --- a/src/server/uinput.rs +++ b/src/server/uinput.rs @@ -314,7 +314,7 @@ pub mod service { fn map_key(key: &enigo::Key) -> ResultType { if let Some(k) = KEY_MAP.get(&key) { - log::info!("mapkey {:?}, get {:?}", &key, &k); + log::trace!("mapkey {:?}, get {:?}", &key, &k); return Ok(k.clone()); } else { match key { @@ -351,12 +351,10 @@ pub mod service { // ignore } DataKeyboard::KeyDown(enigo::Key::Raw(code)) => { - log::info!("keycode {:?}", *code - 8); let down_event = InputEvent::new(EventType::KEY, *code - 8, 1); allow_err!(keyboard.emit(&[down_event])); } DataKeyboard::KeyUp(enigo::Key::Raw(code)) => { - log::info!("keycode {:?}", *code - 8); let down_event = InputEvent::new(EventType::KEY, *code - 8, 0); allow_err!(keyboard.emit(&[down_event])); } @@ -388,6 +386,14 @@ pub mod service { false } } + } else if enigo::Key::NumLock == *key { + match keyboard.get_led_state() { + Ok(leds) => leds.contains(evdev::LedType::LED_NUML), + Err(_e) => { + // log::debug!("Failed to get led state {}", &_e); + false + } + } } else { match keyboard.get_key_state() { Ok(keys) => match key { @@ -403,7 +409,6 @@ pub mod service { keys.contains(evdev::Key::KEY_LEFTALT) || keys.contains(evdev::Key::KEY_RIGHTALT) } - enigo::Key::NumLock => keys.contains(evdev::Key::KEY_NUMLOCK), enigo::Key::Meta => { keys.contains(evdev::Key::KEY_LEFTMETA) || keys.contains(evdev::Key::KEY_RIGHTMETA) From 7a1b1d87e996bdf61b513550c3d269836d7e5dd7 Mon Sep 17 00:00:00 2001 From: asur4s Date: Sun, 4 Sep 2022 04:20:21 -0400 Subject: [PATCH 0376/2015] Fix uinput server in wayland clien --- src/client.rs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/client.rs b/src/client.rs index 2bc60765a..9f330ea1c 100644 --- a/src/client.rs +++ b/src/client.rs @@ -154,7 +154,20 @@ impl Client { return Err(err); } } - Ok(x) => Ok(x), + Ok(x) => { + #[cfg(target_os = "linux")] + if !*IS_X11.lock().unwrap() { + let keyboard = super::uinput::client::UInputKeyboard::new().await?; + log::info!("UInput keyboard created"); + let mouse = super::uinput::client::UInputMouse::new().await?; + log::info!("UInput mouse created"); + + let mut en = ENIGO.lock().unwrap(); + en.set_uinput_keyboard(Some(Box::new(keyboard))); + en.set_uinput_mouse(Some(Box::new(mouse))); + } + + Ok(x)}, } } From 9f80202c798e7dd95285b5bc54aac8f1e853ef0d Mon Sep 17 00:00:00 2001 From: asur4s Date: Sun, 4 Sep 2022 04:25:34 -0400 Subject: [PATCH 0377/2015] Refactor is_x11 --- libs/hbb_common/src/platform/linux.rs | 4 ---- src/client.rs | 2 +- src/common.rs | 9 +++++++++ src/server/input_service.rs | 2 +- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/libs/hbb_common/src/platform/linux.rs b/libs/hbb_common/src/platform/linux.rs index 1a41ebad6..d4d44270a 100644 --- a/libs/hbb_common/src/platform/linux.rs +++ b/libs/hbb_common/src/platform/linux.rs @@ -1,10 +1,6 @@ use crate::ResultType; use std::sync::Mutex; -lazy_static::lazy_static! { - pub static ref IS_X11: Mutex = Mutex::new("x11" == get_display_server()); -} - pub fn get_display_server() -> String { let session = get_value_of_seat0(0); get_display_server_of_session(&session) diff --git a/src/client.rs b/src/client.rs index 9f330ea1c..6cda7f9ad 100644 --- a/src/client.rs +++ b/src/client.rs @@ -156,7 +156,7 @@ impl Client { } Ok(x) => { #[cfg(target_os = "linux")] - if !*IS_X11.lock().unwrap() { + if !*crate::common::IS_X11.lock().unwrap() { let keyboard = super::uinput::client::UInputKeyboard::new().await?; log::info!("UInput keyboard created"); let mouse = super::uinput::client::UInputMouse::new().await?; diff --git a/src/common.rs b/src/common.rs index 2be4b5295..58b456db9 100644 --- a/src/common.rs +++ b/src/common.rs @@ -721,4 +721,13 @@ pub fn get_keyboard_mode() -> String { pub fn save_keyboard_mode(value: String) { std::env::set_var("KEYBOARD_MODE", value); +} + +lazy_static::lazy_static! { + pub static ref IS_X11: Mutex = { + #[cfg(not(target_os = "linux"))] + Mutex::new("x11" == hbb_common::platform::linux::get_display_server()) + #[cfg(not(target_os = "linux"))] + false + }; } \ No newline at end of file diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 3765eb9e0..19be48b8e 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -2,7 +2,7 @@ use super::*; #[cfg(target_os = "macos")] use dispatch::Queue; use enigo::{Enigo, Key, KeyboardControllable, MouseButton, MouseControllable}; -use hbb_common::platform::linux::IS_X11; +use crate::common::IS_X11; use hbb_common::{config::COMPRESS_LEVEL, protobuf::EnumOrUnknown}; use rdev::{simulate, EventType, Key as RdevKey}; use std::{ From 760ab519198e28cb8e50eef301f092f151bf998e Mon Sep 17 00:00:00 2001 From: 21pages Date: Sun, 4 Sep 2022 16:26:08 +0800 Subject: [PATCH 0378/2015] dark theme adjustment Signed-off-by: 21pages --- flutter/lib/desktop/widgets/popup_menu.dart | 25 +++++++++++---------- flutter/lib/mobile/pages/chat_page.dart | 11 +++++++-- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/flutter/lib/desktop/widgets/popup_menu.dart b/flutter/lib/desktop/widgets/popup_menu.dart index 45e52cf81..ea678673a 100644 --- a/flutter/lib/desktop/widgets/popup_menu.dart +++ b/flutter/lib/desktop/widgets/popup_menu.dart @@ -1,6 +1,7 @@ import 'dart:core'; import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; import 'package:get/get.dart'; import './material_mod_popup_menu.dart' as mod_menu; @@ -174,8 +175,8 @@ class MenuEntryRadios extends MenuEntryBase { children: [ Text( opt.text, - style: const TextStyle( - color: Colors.black, + style: TextStyle( + color: MyTheme.color(context).text, fontSize: MenuConfig.fontSize, fontWeight: FontWeight.normal), ), @@ -256,8 +257,8 @@ class MenuEntrySubRadios extends MenuEntryBase { children: [ Text( opt.text, - style: const TextStyle( - color: Colors.black, + style: TextStyle( + color: MyTheme.color(context).text, fontSize: MenuConfig.fontSize, fontWeight: FontWeight.normal), ), @@ -300,8 +301,8 @@ class MenuEntrySubRadios extends MenuEntryBase { const SizedBox(width: MenuConfig.midPadding), Text( text, - style: const TextStyle( - color: Colors.black, + style: TextStyle( + color: MyTheme.color(context).text, fontSize: MenuConfig.fontSize, fontWeight: FontWeight.normal), ), @@ -346,8 +347,8 @@ abstract class MenuEntrySwitchBase extends MenuEntryBase { // const SizedBox(width: MenuConfig.midPadding), Text( text, - style: const TextStyle( - color: Colors.black, + style: TextStyle( + color: MyTheme.color(context).text, fontSize: MenuConfig.fontSize, fontWeight: FontWeight.normal), ), @@ -450,8 +451,8 @@ class MenuEntrySubMenu extends MenuEntryBase { const SizedBox(width: MenuConfig.midPadding), Text( text, - style: const TextStyle( - color: Colors.black, + style: TextStyle( + color: MyTheme.color(context).text, fontSize: MenuConfig.fontSize, fontWeight: FontWeight.normal), ), @@ -491,8 +492,8 @@ class MenuEntryButton extends MenuEntryBase { alignment: AlignmentDirectional.centerStart, constraints: BoxConstraints(minHeight: conf.height), child: childBuilder( - const TextStyle( - color: Colors.black, + TextStyle( + color: MyTheme.color(context).text, fontSize: MenuConfig.fontSize, fontWeight: FontWeight.normal), )), diff --git a/flutter/lib/mobile/pages/chat_page.dart b/flutter/lib/mobile/pages/chat_page.dart index b265f6995..2151f17be 100644 --- a/flutter/lib/mobile/pages/chat_page.dart +++ b/flutter/lib/mobile/pages/chat_page.dart @@ -45,7 +45,7 @@ class ChatPage extends StatelessWidget implements PageShape { return ChangeNotifierProvider.value( value: chatModel, child: Container( - color: MyTheme.grayBg, + color: MyTheme.color(context).grayBg, child: Consumer(builder: (context, chatModel, child) { final currentUser = chatModel.currentUser; return Stack( @@ -59,7 +59,14 @@ class ChatPage extends StatelessWidget implements PageShape { messages: chatModel .messages[chatModel.currentID]?.chatMessages ?? [], - inputOptions: const InputOptions(sendOnEnter: true), + inputOptions: InputOptions( + sendOnEnter: true, + inputDecoration: defaultInputDecoration( + fillColor: MyTheme.color(context).bg), + sendButtonBuilder: defaultSendButton( + color: MyTheme.color(context).text!), + inputTextStyle: + TextStyle(color: MyTheme.color(context).text)), messageOptions: MessageOptions( showOtherUsersAvatar: false, showTime: true, From 7a35119d33e8aed30b41dd06baca0f45c3512a30 Mon Sep 17 00:00:00 2001 From: Asura Date: Sun, 4 Sep 2022 16:50:02 +0800 Subject: [PATCH 0379/2015] Fix misspell --- src/common.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/common.rs b/src/common.rs index 58b456db9..2e772118c 100644 --- a/src/common.rs +++ b/src/common.rs @@ -725,9 +725,9 @@ pub fn save_keyboard_mode(value: String) { lazy_static::lazy_static! { pub static ref IS_X11: Mutex = { + #[cfg(target_os = "linux")] + Mutex::new("x11" == hbb_common::platform::linux::get_display_server()); #[cfg(not(target_os = "linux"))] - Mutex::new("x11" == hbb_common::platform::linux::get_display_server()) - #[cfg(not(target_os = "linux"))] - false + Mutex::new(false) }; } \ No newline at end of file From 79aec0a63f3f965ee80c1c08c1cb2797d6219a3f Mon Sep 17 00:00:00 2001 From: Asura Date: Sun, 4 Sep 2022 16:50:25 +0800 Subject: [PATCH 0380/2015] Fix compile error --- Cargo.lock | 2 +- flutter/pubspec.lock | 24 ++++++++++++------------ src/client/io_loop.rs | 7 +++++++ src/ui_session_interface.rs | 2 ++ 4 files changed, 22 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 62b412062..750ad4e8e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4993,7 +4993,7 @@ checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" [[package]] name = "tfc" version = "0.6.1" -source = "git+https://github.com/asur4s/The-Fat-Controller#a091f887edc2440b17d86c9ba580f2f35ce0cfcc" +source = "git+https://github.com/asur4s/The-Fat-Controller#48303c5dacded6ea1873bc5d69bdde3175cf336a" dependencies = [ "core-graphics 0.22.3", "unicode-segmentation", diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index d9205b76c..cee8975ee 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -49,7 +49,7 @@ packages: name: async url: "https://pub.flutter-io.cn" source: hosted - version: "2.9.0" + version: "2.8.2" back_button_interceptor: dependency: "direct main" description: @@ -147,7 +147,7 @@ packages: name: characters url: "https://pub.flutter-io.cn" source: hosted - version: "1.2.1" + version: "1.2.0" charcode: dependency: transitive description: @@ -168,7 +168,7 @@ packages: name: clock url: "https://pub.flutter-io.cn" source: hosted - version: "1.1.1" + version: "1.1.0" code_builder: dependency: transitive description: @@ -324,7 +324,7 @@ packages: name: fake_async url: "https://pub.flutter-io.cn" source: hosted - version: "1.3.1" + version: "1.3.0" ffi: dependency: "direct main" description: @@ -621,14 +621,14 @@ packages: name: matcher url: "https://pub.flutter-io.cn" source: hosted - version: "0.12.12" + version: "0.12.11" material_color_utilities: dependency: transitive description: name: material_color_utilities url: "https://pub.flutter-io.cn" source: hosted - version: "0.1.5" + version: "0.1.4" menu_base: dependency: transitive description: @@ -642,7 +642,7 @@ packages: name: meta url: "https://pub.flutter-io.cn" source: hosted - version: "1.8.0" + version: "1.7.0" mime: dependency: transitive description: @@ -719,7 +719,7 @@ packages: name: path url: "https://pub.flutter-io.cn" source: hosted - version: "1.8.2" + version: "1.8.1" path_provider: dependency: "direct main" description: @@ -971,7 +971,7 @@ packages: name: source_span url: "https://pub.flutter-io.cn" source: hosted - version: "1.9.0" + version: "1.8.2" sqflite: dependency: transitive description: @@ -1013,7 +1013,7 @@ packages: name: string_scanner url: "https://pub.flutter-io.cn" source: hosted - version: "1.1.1" + version: "1.1.0" synchronized: dependency: transitive description: @@ -1027,14 +1027,14 @@ packages: name: term_glyph url: "https://pub.flutter-io.cn" source: hosted - version: "1.2.1" + version: "1.2.0" test_api: dependency: transitive description: name: test_api url: "https://pub.flutter-io.cn" source: hosted - version: "0.4.12" + version: "0.4.9" timing: dependency: transitive description: diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index e61690c32..f1a481a36 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -5,6 +5,13 @@ use crate::client::{ #[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::common::{check_clipboard, update_clipboard, ClipboardContext, CLIPBOARD_INTERVAL}; +#[cfg(windows)] +use clipboard::{ + cliprdr::CliprdrClientContext, create_cliprdr_context as create_clipboard_file_context, + get_rx_clip_client, server_clip_file, +}; +#[cfg(windows)] +use crate::clipboard_file::*; use crate::ui_session_interface::{InvokeUi, Session}; use crate::{client::Data, client::Interface}; diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 05f34f37a..312a0d21d 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -26,6 +26,8 @@ use std::sync::{Arc, Mutex, RwLock}; pub static IS_IN: AtomicBool = AtomicBool::new(false); static KEYBOARD_HOOKED: AtomicBool = AtomicBool::new(false); +#[cfg(windows)] +static mut IS_ALT_GR: bool = false; #[cfg(not(any(target_os = "android", target_os = "ios")))] lazy_static::lazy_static! { From 8df3000b6bfbeca157b814c162b2732d36d9def4 Mon Sep 17 00:00:00 2001 From: asur4s Date: Sun, 4 Sep 2022 04:58:24 -0400 Subject: [PATCH 0381/2015] Fix syntax error --- src/common.rs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/common.rs b/src/common.rs index 2e772118c..137352052 100644 --- a/src/common.rs +++ b/src/common.rs @@ -712,7 +712,6 @@ pub fn make_fd_flutter(id: i32, entries: &Vec, only_count: bool) -> S serde_json::to_string(&m).unwrap_or("".into()) } - pub fn get_keyboard_mode() -> String { return std::env::var("KEYBOARD_MODE") .unwrap_or(String::from("legacy")) @@ -723,11 +722,13 @@ pub fn save_keyboard_mode(value: String) { std::env::set_var("KEYBOARD_MODE", value); } +#[cfg(not(target_os = "linux"))] lazy_static::lazy_static! { - pub static ref IS_X11: Mutex = { - #[cfg(target_os = "linux")] - Mutex::new("x11" == hbb_common::platform::linux::get_display_server()); - #[cfg(not(target_os = "linux"))] - Mutex::new(false) - }; -} \ No newline at end of file + pub static ref IS_X11: Mutex = Mutex::new(false); + +} + +#[cfg(target_os = "linux")] +lazy_static::lazy_static! { + pub static ref IS_X11: Mutex = Mutex::new("x11" == hbb_common::platform::linux::get_display_server()); +} From f47254c5e2fb4c2244f9329355dafca61469f54a Mon Sep 17 00:00:00 2001 From: 21pages Date: Sun, 4 Sep 2022 20:57:57 +0800 Subject: [PATCH 0382/2015] adjust geometry Signed-off-by: 21pages --- .../lib/desktop/pages/desktop_home_page.dart | 24 +++++++++---------- .../desktop/pages/desktop_setting_page.dart | 4 ++-- .../lib/desktop/widgets/peercard_widget.dart | 2 +- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 0ccb86d1f..3ce956c23 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -93,8 +93,8 @@ class _DesktopHomePageState extends State buildIDBoard(BuildContext context) { final model = gFFI.serverModel; return Container( - margin: const EdgeInsets.only(left: 20, right: 16), - height: 52, + margin: const EdgeInsets.only(left: 20, right: 11), + height: 57, child: Row( crossAxisAlignment: CrossAxisAlignment.baseline, textBaseline: TextBaseline.alphabetic, @@ -102,10 +102,10 @@ class _DesktopHomePageState extends State Container( width: 2, decoration: const BoxDecoration(color: MyTheme.accent), - ), + ).marginOnly(top: 5), Expanded( child: Padding( - padding: const EdgeInsets.only(left: 8.0), + padding: const EdgeInsets.only(left: 7), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -120,7 +120,7 @@ class _DesktopHomePageState extends State style: TextStyle( fontSize: 14, color: MyTheme.color(context).lightText), - ), + ).marginOnly(top: 5), buildPopupMenu(context) ], ), @@ -137,7 +137,7 @@ class _DesktopHomePageState extends State readOnly: true, decoration: const InputDecoration( border: InputBorder.none, - contentPadding: EdgeInsets.only(bottom: 18), + contentPadding: EdgeInsets.only(bottom: 20), ), style: const TextStyle( fontSize: 22, @@ -244,7 +244,7 @@ class _DesktopHomePageState extends State }, child: Obx( () => CircleAvatar( - radius: 12, + radius: 15, backgroundColor: hover.value ? MyTheme.color(context).grayBg! : MyTheme.color(context).bg!, @@ -277,7 +277,7 @@ class _DesktopHomePageState extends State ), Expanded( child: Padding( - padding: const EdgeInsets.only(left: 8.0), + padding: const EdgeInsets.only(left: 7), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -303,7 +303,7 @@ class _DesktopHomePageState extends State readOnly: true, decoration: InputDecoration( border: InputBorder.none, - contentPadding: EdgeInsets.only(bottom: 8), + contentPadding: EdgeInsets.only(bottom: 2), ), style: TextStyle(fontSize: 15), ), @@ -317,7 +317,7 @@ class _DesktopHomePageState extends State ? MyTheme.color(context).text : Color(0xFFDDDDDD), size: 22, - ).marginOnly(right: 10, bottom: 8), + ).marginOnly(right: 8, bottom: 2), ), onTap: () => bind.mainUpdateTemporaryPassword(), onHover: (value) => refreshHover.value = value, @@ -425,13 +425,13 @@ class _DesktopHomePageState extends State color: editHover.value ? MyTheme.color(context).text : Color(0xFFDDDDDD)) - .marginOnly(bottom: 8))); + .marginOnly(bottom: 2))); } buildTip(BuildContext context) { return Padding( padding: - const EdgeInsets.only(left: 20.0, right: 16, top: 16.0, bottom: 14), + const EdgeInsets.only(left: 20.0, right: 16, top: 16.0, bottom: 5), child: Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 867c8a54c..b4bb00ab8 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -49,7 +49,7 @@ class _DesktopSettingPageState extends State 'Display', Icons.desktop_windows_outlined, Icons.desktop_windows_sharp), _TabInfo('Audio', Icons.volume_up_outlined, Icons.volume_up_sharp), _TabInfo('Connection', Icons.link_outlined, Icons.link_sharp), - _TabInfo('About RustDesk', Icons.info_outline, Icons.info_sharp) + _TabInfo('About', Icons.info_outline, Icons.info_sharp) ]; late PageController controller; @@ -714,7 +714,7 @@ class _AboutState extends State<_About> { ], ).marginOnly(left: _kContentHMargin) ]), - ]).marginOnly(left: _kCardLeftMargin); + ]); }); } } diff --git a/flutter/lib/desktop/widgets/peercard_widget.dart b/flutter/lib/desktop/widgets/peercard_widget.dart index 1bff02508..13ab92ffe 100644 --- a/flutter/lib/desktop/widgets/peercard_widget.dart +++ b/flutter/lib/desktop/widgets/peercard_widget.dart @@ -48,7 +48,7 @@ class _PeerCard extends StatefulWidget { class _PeerCardState extends State<_PeerCard> with AutomaticKeepAliveClientMixin { var _menuPos = RelativeRect.fill; - final double _cardRadis = 20; + final double _cardRadis = 16; final double _borderWidth = 2; final RxBool _iconMoreHover = false.obs; From 09b769d92f927679b119d59b3aea2d0636a1f53b Mon Sep 17 00:00:00 2001 From: csf Date: Mon, 5 Sep 2022 10:27:33 +0800 Subject: [PATCH 0383/2015] WIP file transfer --- src/client/io_loop.rs | 89 +++++++++--------------- src/common.rs | 46 ------------ src/flutter.rs | 135 +++++++++++++++++++++++------------- src/flutter_ffi.rs | 24 ++++--- src/ui/file_transfer.tis | 2 +- src/ui/remote.rs | 46 +++++++++--- src/ui_session_interface.rs | 45 +++++++----- 7 files changed, 196 insertions(+), 191 deletions(-) diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index e61690c32..0913251dd 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -2,6 +2,7 @@ use crate::client::{ Client, CodecFormat, FileManager, 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}; @@ -21,7 +22,7 @@ use hbb_common::tokio::{ sync::mpsc, time::{self, Duration, Instant, Interval}, }; -use hbb_common::{allow_err, message_proto::*}; +use hbb_common::{allow_err, message_proto::*, sleep}; use hbb_common::{fs, log, Stream}; use std::collections::HashMap; @@ -270,7 +271,6 @@ impl Remote { // TODO fn load_last_jobs(&mut self) { - log::info!("start load last jobs"); self.handler.clear_all_jobs(); let pc = self.handler.load_config(); if pc.transfer.write_jobs.is_empty() && pc.transfer.read_jobs.is_empty() { @@ -280,33 +280,17 @@ impl Remote { // TODO: can add a confirm dialog let mut cnt = 1; for job_str in pc.transfer.read_jobs.iter() { - let job: Result = serde_json::from_str(&job_str); - if let Ok(job) = job { - self.handler.add_job( - cnt, - job.to.clone(), - job.remote.clone(), - job.file_num, - job.show_hidden, - false, - ); + if !job_str.is_empty() { + self.handler.load_last_job(cnt, job_str); cnt += 1; - println!("restore read_job: {:?}", job); + log::info!("restore read_job: {:?}", job_str); } } for job_str in pc.transfer.write_jobs.iter() { - let job: Result = serde_json::from_str(&job_str); - if let Ok(job) = job { - self.handler.add_job( - cnt, - job.remote.clone(), - job.to.clone(), - job.file_num, - job.show_hidden, - true, - ); + if !job_str.is_empty() { + self.handler.load_last_job(cnt, job_str); cnt += 1; - println!("restore write_job: {:?}", job); + log::info!("restore write_job: {:?}", job_str); } } self.handler.update_transfer_list(); @@ -373,8 +357,7 @@ impl Remote { to, job.files().len() ); - // let m = make_fd(job.id(), job.files(), true); - // self.handler.call("updateFolderFiles", &make_args!(m)); // TODO + self.handler.update_folder_files(job.id(), job.files(), path,!is_remote, true); #[cfg(not(windows))] let files = job.files().clone(); #[cfg(windows)] @@ -433,8 +416,7 @@ impl Remote { to, job.files().len() ); - // let m = make_fd(job.id(), job.files(), true); - // self.handler.call("updateFolderFiles", &make_args!(m)); + self.handler.update_folder_files(job.id(), job.files(), path,!is_remote, true); job.is_last_job = true; self.read_jobs.push(job); self.timer = time::interval(MILLI1); @@ -546,8 +528,7 @@ impl Remote { } else { match fs::get_recursive_files(&path, include_hidden) { Ok(entries) => { - // let m = make_fd(id, &entries, true); - // self.handler.call("updateFolderFiles", &make_args!(m)); + self.handler.update_folder_files(id, &entries, path.clone(),!is_remote, false); self.remove_jobs .insert(id, RemoveJob::new(entries, path, sep, is_remote)); } @@ -749,28 +730,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) - // { - // 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(); - // }); - // } - // } + 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) + { + 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.load_last_jobs().await; - // } + if self.handler.is_file_transfer() { + self.load_last_jobs(); + } } _ => {} }, @@ -823,11 +804,7 @@ impl Remote { fs::transform_windows_path(&mut entries); } } - // let mut m = make_fd(fd.id, &entries, fd.id > 0); - // if fd.id <= 0 { - // m.set_item("path", fd.path); - // } - // self.handler.call("updateFolderFiles", &make_args!(m)); + self.handler.update_folder_files(fd.id, &entries, fd.path, false, fd.id > 0); if let Some(job) = fs::get_job(fd.id, &mut self.write_jobs) { log::info!("job set_files: {:?}", entries); job.set_files(entries); diff --git a/src/common.rs b/src/common.rs index 5c387c07e..471d6d4e2 100644 --- a/src/common.rs +++ b/src/common.rs @@ -666,49 +666,3 @@ pub fn make_privacy_mode_msg(state: back_notification::PrivacyModeState) -> Mess msg_out.set_misc(misc); msg_out } - -pub fn make_fd_to_json(fd: FileDirectory) -> String { - use serde_json::json; - let mut fd_json = serde_json::Map::new(); - fd_json.insert("id".into(), json!(fd.id)); - fd_json.insert("path".into(), json!(fd.path)); - - let mut entries = vec![]; - for entry in fd.entries { - let mut entry_map = serde_json::Map::new(); - entry_map.insert("entry_type".into(), json!(entry.entry_type.value())); - entry_map.insert("name".into(), json!(entry.name)); - entry_map.insert("size".into(), json!(entry.size)); - entry_map.insert("modified_time".into(), json!(entry.modified_time)); - entries.push(entry_map); - } - fd_json.insert("entries".into(), json!(entries)); - serde_json::to_string(&fd_json).unwrap_or("".into()) -} - -pub fn make_fd_flutter(id: i32, entries: &Vec, only_count: bool) -> String { - let mut m = serde_json::Map::new(); - m.insert("id".into(), json!(id)); - let mut a = vec![]; - let mut n: u64 = 0; - for entry in entries { - n += entry.size; - if only_count { - continue; - } - let mut e = serde_json::Map::new(); - e.insert("name".into(), json!(entry.name.to_owned())); - let tmp = entry.entry_type.value(); - e.insert("type".into(), json!(if tmp == 0 { 1 } else { tmp })); - e.insert("time".into(), json!(entry.modified_time as f64)); - e.insert("size".into(), json!(entry.size as f64)); - a.push(e); - } - if only_count { - m.insert("num_entries".into(), json!(entries.len() as i32)); - } else { - m.insert("entries".into(), json!(a)); - } - m.insert("total_size".into(), json!(n as f64)); - serde_json::to_string(&m).unwrap_or("".into()) -} diff --git a/src/flutter.rs b/src/flutter.rs index b84e91ce8..a2f03d2ff 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -5,7 +5,10 @@ use std::{ use flutter_rust_bridge::{StreamSink, ZeroCopyBuffer}; -use hbb_common::{bail, config::LocalConfig, message_proto::*, ResultType, rendezvous_proto::ConnType}; +use hbb_common::{ + bail, config::LocalConfig, message_proto::*, rendezvous_proto::ConnType, ResultType, +}; +use serde_json::json; use crate::ui_session_interface::{io_loop, InvokeUi, Session}; @@ -85,6 +88,7 @@ impl InvokeUi for FlutterHandler { self.push_event("permission", vec![(name, &value.to_string())]); } + // unused in flutter fn close_success(&self) {} fn update_quality_status(&self, status: QualityStatus) { @@ -118,7 +122,14 @@ impl InvokeUi for FlutterHandler { } fn job_error(&self, id: i32, err: String, file_num: i32) { - self.push_event("job_error", vec![("id", &id.to_string()), ("err", &err)]); + self.push_event( + "job_error", + vec![ + ("id", &id.to_string()), + ("err", &err), + ("file_num", &file_num.to_string()), + ], + ); } fn job_done(&self, id: i32, file_num: i32) { @@ -128,29 +139,43 @@ impl InvokeUi for FlutterHandler { ); } - fn clear_all_jobs(&self) { - // todo!() + // unused in flutter + fn clear_all_jobs(&self) {} + + fn load_last_job(&self, _cnt: i32, job_json: &str) { + self.push_event("load_last_job", vec![("value", job_json)]); } - fn add_job( + fn update_folder_files( &self, id: i32, + entries: &Vec, path: String, - to: String, - file_num: i32, - show_hidden: bool, - is_remote: bool, + is_local: bool, + only_count: bool, ) { - // todo!() + // TODO opt + if only_count { + self.push_event( + "update_folder_files", + vec![("info", &make_fd_flutter(id, entries, only_count))], + ); + } else { + self.push_event( + "file_dir", + vec![ + ("value", &make_fd_to_json(id, path, entries)), + ("is_local", "false"), + ], + ); + } } - fn update_transfer_list(&self) { - // todo!() - } + // unused in flutter + fn update_transfer_list(&self) {} - fn confirm_delete_files(&self, id: i32, i: i32, name: String) { - // todo!() - } + // unused in flutter // TEST flutter + fn confirm_delete_files(&self, _id: i32, _i: i32, _name: String) {} fn override_file_confirm(&self, id: i32, file_num: i32, to: String, is_upload: bool) { self.push_event( @@ -176,6 +201,7 @@ impl InvokeUi for FlutterHandler { ); } + // unused in flutter fn adapt_size(&self) {} fn on_rgba(&self, data: &[u8]) { @@ -283,11 +309,7 @@ pub fn session_add(id: &str, is_file_transfer: bool, is_port_forward: bool) -> R .unwrap() .initialize(session_id, conn_type); - if let Some(same_id_session) = SESSIONS - .write() - .unwrap() - .insert(id.to_owned(), session) - { + if let Some(same_id_session) = SESSIONS.write().unwrap().insert(id.to_owned(), session) { same_id_session.close(); } @@ -946,30 +968,47 @@ pub fn get_session_id(id: String) -> String { }; } -// async fn start_one_port_forward( -// handler: Session, -// port: i32, -// remote_host: String, -// remote_port: i32, -// receiver: mpsc::UnboundedReceiver, -// key: &str, -// token: &str, -// ) { -// if let Err(err) = crate::port_forward::listen( -// handler.id.clone(), -// String::new(), // TODO -// port, -// handler.clone(), -// receiver, -// key, -// token, -// handler.lc.clone(), -// remote_host, -// remote_port, -// ) -// .await -// { -// handler.on_error(&format!("Failed to listen on {}: {}", port, err)); -// } -// log::info!("port forward (:{}) exit", port); -// } +pub fn make_fd_to_json(id: i32, path: String, entries: &Vec) -> String { + let mut fd_json = serde_json::Map::new(); + fd_json.insert("id".into(), json!(id)); + fd_json.insert("path".into(), json!(path)); + + let mut entries_out = vec![]; + for entry in entries { + let mut entry_map = serde_json::Map::new(); + entry_map.insert("entry_type".into(), json!(entry.entry_type.value())); + entry_map.insert("name".into(), json!(entry.name)); + entry_map.insert("size".into(), json!(entry.size)); + entry_map.insert("modified_time".into(), json!(entry.modified_time)); + entries_out.push(entry_map); + } + fd_json.insert("entries".into(), json!(entries_out)); + serde_json::to_string(&fd_json).unwrap_or("".into()) +} + +pub fn make_fd_flutter(id: i32, entries: &Vec, only_count: bool) -> String { + let mut m = serde_json::Map::new(); + m.insert("id".into(), json!(id)); + let mut a = vec![]; + let mut n: u64 = 0; + for entry in entries { + n += entry.size; + if only_count { + continue; + } + let mut e = serde_json::Map::new(); + e.insert("name".into(), json!(entry.name.to_owned())); + let tmp = entry.entry_type.value(); + e.insert("type".into(), json!(if tmp == 0 { 1 } else { tmp })); + e.insert("time".into(), json!(entry.modified_time as f64)); + e.insert("size".into(), json!(entry.size as f64)); + a.push(e); + } + if only_count { + m.insert("num_entries".into(), json!(entries.len() as i32)); + } else { + m.insert("entries".into(), json!(a)); + } + m.insert("total_size".into(), json!(n as f64)); + serde_json::to_string(&m).unwrap_or("".into()) +} diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 6a3d19880..032be5b1f 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -5,16 +5,14 @@ use std::{ }; use flutter_rust_bridge::{StreamSink, SyncReturn, ZeroCopyBuffer}; -use serde_json::{json, Number, Value}; +use serde_json::json; +use hbb_common::ResultType; use hbb_common::{ - config::{self, Config, LocalConfig, PeerConfig, ONLINE}, + config::{self, LocalConfig, PeerConfig, ONLINE}, fs, log, }; -use hbb_common::{password_security, ResultType}; -use crate::{client::file_trait::FileManager, flutter::{session_add, session_start_}}; -use crate::common::make_fd_to_json; use crate::flutter::connection_manager::{self, get_clients_length, get_clients_state}; use crate::flutter::{self, SESSIONS}; use crate::start_server; @@ -30,6 +28,10 @@ use crate::ui_interface::{ set_peer_option, set_permanent_password, set_socks, store_fav, test_if_valid_server, update_temporary_password, using_public_server, }; +use crate::{ + client::file_trait::FileManager, + flutter::{make_fd_to_json, session_add, session_start_}, +}; fn initialize(app_dir: &str) { *config::APP_DIR.write().unwrap() = app_dir.to_owned(); @@ -110,7 +112,11 @@ pub fn host_stop_system_key_propagate(stopped: bool) { // FIXME: -> ResultType<()> cannot be parsed by frb_codegen // thread 'main' panicked at 'Failed to parse function output type `ResultType<()>`', $HOME\.cargo\git\checkouts\flutter_rust_bridge-ddba876d3ebb2a1e\e5adce5\frb_codegen\src\parser\mod.rs:151:25 -pub fn session_add_sync(id: String, is_file_transfer: bool, is_port_forward: bool) -> SyncReturn { +pub fn session_add_sync( + id: String, + is_file_transfer: bool, + is_port_forward: bool, +) -> SyncReturn { if let Err(e) = session_add(&id, is_file_transfer, is_port_forward) { SyncReturn(format!("Failed to add session with id {}, {}", &id, e)) } else { @@ -346,10 +352,8 @@ pub fn session_create_dir(id: String, act_id: i32, path: String, is_remote: bool } pub fn session_read_local_dir_sync(id: String, path: String, show_hidden: bool) -> String { - if let Some(session) = SESSIONS.read().unwrap().get(&id) { - if let Ok(fd) = fs::read_dir(&fs::get_path(&path), show_hidden) { - return make_fd_to_json(fd); - } + if let Ok(fd) = fs::read_dir(&fs::get_path(&path), show_hidden) { + return make_fd_to_json(fd.id, path, &fd.entries); } "".to_string() } diff --git a/src/ui/file_transfer.tis b/src/ui/file_transfer.tis index f32540b33..38c6321dc 100644 --- a/src/ui/file_transfer.tis +++ b/src/ui/file_transfer.tis @@ -695,7 +695,7 @@ handler.clearAllJobs = function() { file_transfer.job_table.clearAllJobs(); } -handler.addJob = function (id, path, to, file_num, show_hidden, is_remote) { +handler.addJob = function (id, path, to, file_num, show_hidden, is_remote) { // load last job // stdout.println("restore job: " + is_remote); file_transfer.job_table.addJob(id,path,to,file_num,show_hidden,is_remote); } diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 7e2c5cd9c..b377b8583 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -23,7 +23,9 @@ use clipboard::{ get_rx_clip_client, server_clip_file, }; -use hbb_common::{allow_err, log, message_proto::*, rendezvous_proto::ConnType}; +use hbb_common::{ + allow_err, fs::TransferJobMeta, log, message_proto::*, rendezvous_proto::ConnType, +}; #[cfg(windows)] use crate::clipboard_file::*; @@ -43,7 +45,6 @@ static mut IS_ALT_GR: bool = false; /// SciterHandler /// * element -/// * thread TODO check if flutter need /// * close_state for file path when close #[derive(Clone, Default)] pub struct SciterHandler { @@ -155,16 +156,36 @@ impl InvokeUi for SciterHandler { self.call("clearAllJobs", &make_args!()); } - fn add_job( + fn load_last_job(&self, cnt: i32, job_json: &str) { + let job: Result = serde_json::from_str(job_json); + if let Ok(job) = job { + let path; + let to; + if job.is_remote { + path = job.remote.clone(); + to = job.to.clone(); + } else { + path = job.to.clone(); + to = job.remote.clone(); + } + self.call( + "addJob", + &make_args!(cnt, path, to, job.file_num, job.show_hidden, job.is_remote), + ); + } + } + + fn update_folder_files( &self, id: i32, + entries: &Vec, path: String, - to: String, - file_num: i32, - show_hidden: bool, - is_remote: bool, + _is_local: bool, + only_count: bool, ) { - todo!() + let mut m = make_fd(id, entries, only_count); + m.set_item("path", path); + self.call("updateFolderFiles", &make_args!(m)); } fn update_transfer_list(&self) { @@ -686,15 +707,18 @@ impl SciterSession { } pub fn make_fd(id: i32, entries: &Vec, only_count: bool) -> Value { + log::debug!("make_fd"); let mut m = Value::map(); m.set_item("id", id); let mut a = Value::array(0); let mut n: u64 = 0; for entry in entries { + log::debug!("for"); n += entry.size; if only_count { continue; } + log::debug!("for1"); let mut e = Value::map(); e.set_item("name", entry.name.to_owned()); let tmp = entry.entry_type.value(); @@ -703,11 +727,11 @@ pub fn make_fd(id: i32, entries: &Vec, only_count: bool) -> Value { e.set_item("size", entry.size as f64); a.push(e); } - if only_count { - m.set_item("num_entries", entries.len() as i32); - } else { + if !only_count { m.set_item("entries", a); } + m.set_item("num_entries", entries.len() as i32); m.set_item("total_size", n as f64); + log::debug!("make_fd end"); m } diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 5ab6089a0..f117aae6d 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -1,11 +1,11 @@ -use crate::client::io_loop::Remote; -use crate::client::{ - check_if_retry, handle_hash, handle_login_from_ui, handle_test_delay, - input_os_password, load_config, send_mouse, start_video_audio_threads, FileManager, Key, - LoginConfigHandler, QualityStatus, KEY_MAP, SERVER_KEYBOARD_ENABLED, -}; #[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::client::get_key_state; +use crate::client::io_loop::Remote; +use crate::client::{ + check_if_retry, handle_hash, handle_login_from_ui, handle_test_delay, input_os_password, + load_config, send_mouse, start_video_audio_threads, FileManager, Key, LoginConfigHandler, + QualityStatus, KEY_MAP, SERVER_KEYBOARD_ENABLED, +}; use crate::common; use crate::{client::Data, client::Interface}; use async_trait::async_trait; @@ -19,7 +19,7 @@ use hbb_common::{allow_err, message_proto::*}; use hbb_common::{fs, get_version_number, log, Stream}; use std::collections::HashMap; use std::ops::{Deref, DerefMut}; -use std::sync::atomic::{AtomicUsize, Ordering, AtomicBool}; +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::{Arc, Mutex, RwLock}; /// IS_IN KEYBOARD_HOOKED sciter only @@ -556,18 +556,17 @@ pub trait InvokeUi: Send + Sync + Clone + 'static + Sized + Default { fn job_error(&self, id: i32, err: String, file_num: i32); fn job_done(&self, id: i32, file_num: i32); fn clear_all_jobs(&self); - fn add_job( - &self, - id: i32, - path: String, - to: String, - file_num: i32, - show_hidden: bool, - is_remote: bool, - ); fn new_message(&self, msg: String); fn update_transfer_list(&self); - // fn update_folder_files(&self); // TODO flutter with file_dir and update_folder_files + fn load_last_job(&self, cnt: i32, job_json: &str); + fn update_folder_files( + &self, + id: i32, + entries: &Vec, + path: String, + is_local: bool, + only_count: bool, + ); fn confirm_delete_files(&self, id: i32, i: i32, name: String); fn override_file_confirm(&self, id: i32, file_num: i32, to: String, is_upload: bool); fn update_block_input_state(&self, on: bool); @@ -604,11 +603,19 @@ impl Interface for Session { } fn is_file_transfer(&self) -> bool { - self.lc.read().unwrap().conn_type.eq(&ConnType::FILE_TRANSFER) + self.lc + .read() + .unwrap() + .conn_type + .eq(&ConnType::FILE_TRANSFER) } fn is_port_forward(&self) -> bool { - self.lc.read().unwrap().conn_type.eq(&ConnType::PORT_FORWARD) + self.lc + .read() + .unwrap() + .conn_type + .eq(&ConnType::PORT_FORWARD) } fn is_rdp(&self) -> bool { From 4c499ecf2e0b47ef634e71e0457983bc419b860a Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 5 Sep 2022 14:00:40 +0800 Subject: [PATCH 0384/2015] fix: linux wayland frame of subwindow exists Signed-off-by: Kingtous --- flutter/linux/my_application.cc | 2 ++ flutter/pubspec.lock | 4 ++-- flutter/pubspec.yaml | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/flutter/linux/my_application.cc b/flutter/linux/my_application.cc index 20513032d..deea3f549 100644 --- a/flutter/linux/my_application.cc +++ b/flutter/linux/my_application.cc @@ -19,6 +19,8 @@ static void my_application_activate(GApplication* application) { MyApplication* self = MY_APPLICATION(application); GtkWindow* window = GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + // we have custom window frame + gtk_window_set_decorated(window, FALSE); // Use a header bar when running in GNOME as this is the common style used // by applications and is the setup most users will be using (e.g. Ubuntu diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 61fbfc293..b1c603ff0 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -252,8 +252,8 @@ packages: dependency: "direct main" description: path: "." - ref: e0368a023ba195462acc00d33ab361b499f0e413 - resolved-ref: e0368a023ba195462acc00d33ab361b499f0e413 + ref: fee851fa43116e0b91c39acd0ec37063dc6015f8 + resolved-ref: fee851fa43116e0b91c39acd0ec37063dc6015f8 url: "https://github.com/Kingtous/rustdesk_desktop_multi_window" source: git version: "0.1.0" diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index f2d038af3..f86c59755 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -62,7 +62,7 @@ dependencies: desktop_multi_window: git: url: https://github.com/Kingtous/rustdesk_desktop_multi_window - ref: e0368a023ba195462acc00d33ab361b499f0e413 + ref: fee851fa43116e0b91c39acd0ec37063dc6015f8 freezed_annotation: ^2.0.3 tray_manager: git: From abf78ab6f725a66d43ee7878b23abdfe80446cfe Mon Sep 17 00:00:00 2001 From: asur4s Date: Mon, 5 Sep 2022 05:55:31 -0400 Subject: [PATCH 0385/2015] Refacotr env of keyboard && enter view --- flutter/pubspec.lock | 24 +++++++++++----------- src/common.rs | 10 ---------- src/ui/remote.rs | 13 ------------ src/ui_session_interface.rs | 40 ++++++++++++++++++++++++++++--------- 4 files changed, 43 insertions(+), 44 deletions(-) diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index cee8975ee..d9205b76c 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -49,7 +49,7 @@ packages: name: async url: "https://pub.flutter-io.cn" source: hosted - version: "2.8.2" + version: "2.9.0" back_button_interceptor: dependency: "direct main" description: @@ -147,7 +147,7 @@ packages: name: characters url: "https://pub.flutter-io.cn" source: hosted - version: "1.2.0" + version: "1.2.1" charcode: dependency: transitive description: @@ -168,7 +168,7 @@ packages: name: clock url: "https://pub.flutter-io.cn" source: hosted - version: "1.1.0" + version: "1.1.1" code_builder: dependency: transitive description: @@ -324,7 +324,7 @@ packages: name: fake_async url: "https://pub.flutter-io.cn" source: hosted - version: "1.3.0" + version: "1.3.1" ffi: dependency: "direct main" description: @@ -621,14 +621,14 @@ packages: name: matcher url: "https://pub.flutter-io.cn" source: hosted - version: "0.12.11" + version: "0.12.12" material_color_utilities: dependency: transitive description: name: material_color_utilities url: "https://pub.flutter-io.cn" source: hosted - version: "0.1.4" + version: "0.1.5" menu_base: dependency: transitive description: @@ -642,7 +642,7 @@ packages: name: meta url: "https://pub.flutter-io.cn" source: hosted - version: "1.7.0" + version: "1.8.0" mime: dependency: transitive description: @@ -719,7 +719,7 @@ packages: name: path url: "https://pub.flutter-io.cn" source: hosted - version: "1.8.1" + version: "1.8.2" path_provider: dependency: "direct main" description: @@ -971,7 +971,7 @@ packages: name: source_span url: "https://pub.flutter-io.cn" source: hosted - version: "1.8.2" + version: "1.9.0" sqflite: dependency: transitive description: @@ -1013,7 +1013,7 @@ packages: name: string_scanner url: "https://pub.flutter-io.cn" source: hosted - version: "1.1.0" + version: "1.1.1" synchronized: dependency: transitive description: @@ -1027,14 +1027,14 @@ packages: name: term_glyph url: "https://pub.flutter-io.cn" source: hosted - version: "1.2.0" + version: "1.2.1" test_api: dependency: transitive description: name: test_api url: "https://pub.flutter-io.cn" source: hosted - version: "0.4.9" + version: "0.4.12" timing: dependency: transitive description: diff --git a/src/common.rs b/src/common.rs index 137352052..3ffea492b 100644 --- a/src/common.rs +++ b/src/common.rs @@ -712,16 +712,6 @@ pub fn make_fd_flutter(id: i32, entries: &Vec, only_count: bool) -> S serde_json::to_string(&m).unwrap_or("".into()) } -pub fn get_keyboard_mode() -> String { - return std::env::var("KEYBOARD_MODE") - .unwrap_or(String::from("legacy")) - .to_lowercase(); -} - -pub fn save_keyboard_mode(value: String) { - std::env::set_var("KEYBOARD_MODE", value); -} - #[cfg(not(target_os = "linux"))] lazy_static::lazy_static! { pub static ref IS_X11: Mutex = Mutex::new(false); diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 26f323831..4fb6dd605 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -1,4 +1,3 @@ -use crate::common::{get_keyboard_mode, save_keyboard_mode}; use std::{ collections::HashMap, ops::{Deref, DerefMut}, @@ -540,18 +539,6 @@ impl SciterSession { self.close_state.insert(k, v); } - fn enter(&mut self) { - #[cfg(windows)] - crate::platform::windows::stop_system_key_propagate(true); - IS_IN.store(true, Ordering::SeqCst); - } - - fn leave(&mut self) { - #[cfg(windows)] - crate::platform::windows::stop_system_key_propagate(false); - IS_IN.store(false, Ordering::SeqCst); - } - fn get_key_event(&self, down_or_up: i32, name: &str, code: i32) -> Option { let mut key_event = KeyEvent::new(); if down_or_up == 2 { diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 312a0d21d..676933ccc 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -7,7 +7,6 @@ use crate::client::{ QualityStatus, KEY_MAP, SERVER_KEYBOARD_ENABLED, }; use crate::{client::Data, client::Interface}; -use crate::common::{get_keyboard_mode, save_keyboard_mode}; use async_trait::async_trait; use hbb_common::config::{Config, LocalConfig, PeerConfig}; @@ -61,11 +60,13 @@ impl Session { } pub fn get_keyboard_mode(&self) -> String { - return get_keyboard_mode(); + return std::env::var("KEYBOARD_MODE") + .unwrap_or(String::from("legacy")) + .to_lowercase(); } pub fn save_keyboard_mode(&self, value: String) { - save_keyboard_mode(value); + std::env::set_var("KEYBOARD_MODE", value); } pub fn save_view_style(&mut self, value: String) { @@ -726,8 +727,29 @@ impl Session { self.send_key_event(key_event, KeyboardMode::Legacy); } - pub fn handle_flutter_key_event(&self, name: &str, keycode: i32, scancode:i32, down_or_up: bool){ - if scancode < 0 || keycode < 0{ + pub fn enter(&self) { + #[cfg(windows)] + crate::platform::windows::stop_system_key_propagate(true); + IS_IN.store(true, Ordering::SeqCst); + } + + pub fn leave(&self) { + for key in TO_RELEASE.lock().unwrap().iter() { + self.map_keyboard_mode(false, *key, None) + } + #[cfg(windows)] + crate::platform::windows::stop_system_key_propagate(false); + IS_IN.store(false, Ordering::SeqCst); + } + + pub fn handle_flutter_key_event( + &self, + name: &str, + keycode: i32, + scancode: i32, + down_or_up: bool, + ) { + if scancode < 0 || keycode < 0 { return; } let keycode: u32 = keycode as u32; @@ -739,12 +761,12 @@ impl Session { #[cfg(target_os = "windows")] let key = rdev::get_win_key(keycode, scancode); - let event_type = if down_or_up{ + let event_type = if down_or_up { KeyPress(key) - }else{ + } else { KeyRelease(key) }; - let evt = Event{ + let evt = Event { time: std::time::SystemTime::now(), name: Option::Some(name.to_owned()), code: keycode as _, @@ -827,7 +849,7 @@ impl Session { } } } - + self.legacy_modifiers(&mut key_event, alt, ctrl, shift, command); if v == 1 { key_event.down = true; From 5dfd041d8f08fcc146811c380513679caa4f9238 Mon Sep 17 00:00:00 2001 From: asur4s Date: Mon, 5 Sep 2022 06:19:05 -0400 Subject: [PATCH 0386/2015] Opt: enter or leave --- flutter/lib/desktop/pages/remote_page.dart | 24 ++++++++++++++-------- flutter/lib/models/model.dart | 4 ++++ src/flutter_ffi.rs | 10 +++++++++ 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 51225672b..8c56f69d2 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -517,6 +517,19 @@ class _RemotePageState extends State } } + void enterView(PointerEnterEvent evt) { + if (!_imageFocused) { + _physicalFocusNode.requestFocus(); + } + _cursorOverImage.value = true; + _ffi.enterOrLeave(true); + } + + void leaveView(PointerExitEvent evt) { + _cursorOverImage.value = false; + _ffi.enterOrLeave(false); + } + Widget _buildImageListener(Widget child) { return Listener( onPointerHover: _onPointHoverImage, @@ -525,15 +538,8 @@ class _RemotePageState extends State onPointerMove: _onPointMoveImage, onPointerSignal: _onPointerSignalImage, child: MouseRegion( - onEnter: (evt) { - if (!_imageFocused) { - _physicalFocusNode.requestFocus(); - } - _cursorOverImage.value = true; - }, - onExit: (evt) { - _cursorOverImage.value = false; - }, + onEnter: enterView, + onExit: leaveView, child: child)); } diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index dcf9a7b22..bfa6f5297 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -1003,6 +1003,10 @@ class FFI { downOrUp: down); } + void enterOrLeave(bool enter) { + bind.sessionEnterOrLeave(id: id, enter: enter); + } + /// Send key stroke event. /// [down] indicates the key's state(down or up). /// [press] indicates a click event(down and up). diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 72ccbe603..e5616bee6 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -247,6 +247,16 @@ pub fn session_handle_flutter_key_event( } } +pub fn session_enter_or_leave(id: String, enter: bool) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + if enter { + session.enter(); + } else { + session.leave(); + } + } +} + pub fn session_input_key( id: String, name: String, From 7eeb0f733586d59dd4d45397b8b5e994b8680136 Mon Sep 17 00:00:00 2001 From: csf Date: Mon, 5 Sep 2022 19:41:09 +0800 Subject: [PATCH 0387/2015] refactor cm -> ui_cm_interface for sciter and flutter --- .../com/carriez/flutter_hbb/MainService.kt | 31 +- flutter/lib/models/model.dart | 12 +- flutter/lib/models/server_model.dart | 47 +- src/client/io_loop.rs | 6 +- src/flutter.rs | 657 ++--------------- src/flutter_ffi.rs | 27 +- src/lib.rs | 1 + src/ui.rs | 2 +- src/ui/cm.rs | 623 ++-------------- src/ui/remote.rs | 14 +- src/ui_cm_interface.rs | 665 ++++++++++++++++++ src/ui_session_interface.rs | 30 +- 12 files changed, 878 insertions(+), 1237 deletions(-) create mode 100644 src/ui_cm_interface.rs diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt index b9f9ff872..ac736ffdc 100644 --- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt +++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt @@ -105,43 +105,30 @@ class MainService : Service() { @Keep fun rustSetByName(name: String, arg1: String, arg2: String) { when (name) { - "try_start_without_auth" -> { - try { - val jsonObject = JSONObject(arg1) - val id = jsonObject["id"] as Int - val username = jsonObject["name"] as String - val peerId = jsonObject["peer_id"] as String - val type = if (jsonObject["is_file_transfer"] as Boolean) { - translate("File Connection") - } else { - translate("Screen Connection") - } - loginRequestNotification(id, type, username, peerId) - } catch (e: JSONException) { - e.printStackTrace() - } - } - "on_client_authorized" -> { - Log.d(logTag, "from rust:on_client_authorized") + "add_connection" -> { try { val jsonObject = JSONObject(arg1) val id = jsonObject["id"] as Int val username = jsonObject["name"] as String val peerId = jsonObject["peer_id"] as String + val authorized = jsonObject["authorized"] as Boolean val isFileTransfer = jsonObject["is_file_transfer"] as Boolean val type = if (isFileTransfer) { translate("File Connection") } else { translate("Screen Connection") } - if (!isFileTransfer && !isStart) { - startCapture() + if (authorized) { + if (!isFileTransfer && !isStart) { + startCapture() + } + onClientAuthorizedNotification(id, type, username, peerId) + } else { + loginRequestNotification(id, type, username, peerId) } - onClientAuthorizedNotification(id, type, username, peerId) } catch (e: JSONException) { e.printStackTrace() } - } "stop_capture" -> { Log.d(logTag, "from rust:stop_capture") diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 384d7692a..1993145e7 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -168,10 +168,8 @@ class FfiModel with ChangeNotifier { parent.target?.fileModel.loadLastJob(evt); } else if (name == 'update_folder_files') { parent.target?.fileModel.updateFolderFiles(evt); - } else if (name == 'try_start_without_auth') { - parent.target?.serverModel.loginRequest(evt); - } else if (name == 'on_client_authorized') { - parent.target?.serverModel.onClientAuthorized(evt); + } else if (name == 'add_connection') { + parent.target?.serverModel.addConnection(evt); } else if (name == 'on_client_remove') { parent.target?.serverModel.onClientRemove(evt); } else if (name == 'update_quality_status') { @@ -227,10 +225,8 @@ class FfiModel with ChangeNotifier { parent.target?.fileModel.loadLastJob(evt); } else if (name == 'update_folder_files') { parent.target?.fileModel.updateFolderFiles(evt); - } else if (name == 'try_start_without_auth') { - parent.target?.serverModel.loginRequest(evt); - } else if (name == 'on_client_authorized') { - parent.target?.serverModel.onClientAuthorized(evt); + } else if (name == 'add_connection') { + parent.target?.serverModel.addConnection(evt); } else if (name == 'on_client_remove') { parent.target?.serverModel.onClientRemove(evt); } else if (name == 'update_quality_status') { diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index f78f8cf70..9d921ef48 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -100,7 +100,7 @@ class ServerModel with ChangeNotifier { _connectStatus = status; notifyListeners(); } - final res = await bind.mainCheckClientsLength(length: _clients.length); + final res = await bind.cmCheckClientsLength(length: _clients.length); if (res != null) { debugPrint("clients not match!"); updateClientState(res); @@ -347,7 +347,7 @@ class ServerModel with ChangeNotifier { // force updateClientState([String? json]) async { - var res = await bind.mainGetClientsState(); + var res = await bind.cmGetClientsState(); try { final List clientsJson = jsonDecode(res); _clients.clear(); @@ -369,21 +369,40 @@ class ServerModel with ChangeNotifier { } } - void loginRequest(Map evt) { + void addConnection(Map evt) { try { final client = Client.fromJson(jsonDecode(evt["client"])); - if (_clients.any((c) => c.id == client.id)) { - return; + if (client.authorized) { + parent.target?.dialogManager.dismissByTag(getLoginDialogTag(client.id)); + final index = _clients.indexWhere((c) => c.id == client.id); + if (index < 0) { + _clients.add(client); + } else { + _clients[index].authorized = true; + } + tabController.add( + TabInfo( + key: client.id.toString(), + label: client.name, + closable: false, + page: Desktop.buildConnectionCard(client)), + authorized: true); + scrollToBottom(); + notifyListeners(); + } else { + if (_clients.any((c) => c.id == client.id)) { + return; + } + _clients.add(client); + tabController.add(TabInfo( + key: client.id.toString(), + label: client.name, + closable: false, + page: Desktop.buildConnectionCard(client))); + scrollToBottom(); + notifyListeners(); + if (isAndroid) showLoginDialog(client); } - _clients.add(client); - tabController.add(TabInfo( - key: client.id.toString(), - label: client.name, - closable: false, - page: Desktop.buildConnectionCard(client))); - scrollToBottom(); - notifyListeners(); - if (isAndroid) showLoginDialog(client); } catch (e) { debugPrint("Failed to call loginRequest,error:$e"); } diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 0913251dd..54c3be26e 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -6,7 +6,7 @@ use crate::common; #[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::common::{check_clipboard, update_clipboard, ClipboardContext, CLIPBOARD_INTERVAL}; -use crate::ui_session_interface::{InvokeUi, Session}; +use crate::ui_session_interface::{InvokeUiSession, Session}; use crate::{client::Data, client::Interface}; use hbb_common::config::{PeerConfig, TransferSerde}; @@ -29,7 +29,7 @@ use std::collections::HashMap; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::{Arc, Mutex}; -pub struct Remote { +pub struct Remote { handler: Session, video_sender: MediaSender, audio_sender: MediaSender, @@ -49,7 +49,7 @@ pub struct Remote { video_format: CodecFormat, } -impl Remote { +impl Remote { pub fn new( handler: Session, video_sender: MediaSender, diff --git a/src/flutter.rs b/src/flutter.rs index a2f03d2ff..53b79949a 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -10,7 +10,7 @@ use hbb_common::{ }; use serde_json::json; -use crate::ui_session_interface::{io_loop, InvokeUi, Session}; +use crate::ui_session_interface::{io_loop, InvokeUiSession, Session}; use crate::{client::*, flutter_ffi::EventToUI}; @@ -47,7 +47,7 @@ impl FlutterHandler { } } -impl InvokeUi for FlutterHandler { +impl InvokeUiSession for FlutterHandler { fn set_cursor_data(&self, cd: CursorData) { let colors = hbb_common::compress::decompress(&cd.colors); self.push_event( @@ -338,625 +338,82 @@ pub fn session_start_(id: &str, event_stream: StreamSink) -> ResultTy // Server Side #[cfg(not(any(target_os = "ios")))] pub mod connection_manager { - use std::{ - collections::HashMap, - iter::FromIterator, - sync::{ - atomic::{AtomicI64, Ordering}, - RwLock, - }, - }; + use std::collections::HashMap; - use serde_derive::Serialize; - - use hbb_common::{ - allow_err, - config::Config, - fs::is_write_need_confirmation, - fs::{self, get_string, new_send_confirm, DigestCheckResult}, - log, - message_proto::*, - protobuf::Message as _, - tokio::{ - self, - sync::mpsc::{self, UnboundedReceiver, UnboundedSender}, - task::spawn_blocking, - }, - }; #[cfg(any(target_os = "android"))] use scrap::android::call_main_service_set_by_name; - use crate::ipc::Data; - use crate::ipc::{self, new_listener, Connection}; + use crate::ui_cm_interface::InvokeUiCM; use super::GLOBAL_EVENT_STREAM; - #[derive(Debug, Serialize, Clone)] - struct Client { - id: i32, - pub authorized: bool, - is_file_transfer: bool, - name: String, - peer_id: String, - keyboard: bool, - clipboard: bool, - audio: bool, - file: bool, - restart: bool, - #[serde(skip)] - tx: UnboundedSender, + #[derive(Clone)] + struct FlutterHandler {} + + impl InvokeUiCM for FlutterHandler { + //TODO port_forward + fn add_connection(&self, client: &crate::ui_cm_interface::Client) { + let client_json = serde_json::to_string(&client).unwrap_or("".into()); + // send to Android service, active notification no matter UI is shown or not. + #[cfg(any(target_os = "android"))] + if let Err(e) = + call_main_service_set_by_name("add_connection", Some(&client_json), None) + { + log::debug!("call_service_set_by_name fail,{}", e); + } + // send to UI, refresh widget + self.push_event("add_connection", vec![("client", &client_json)]); // TODO use add_connection + } + + fn remove_connection(&self, id: i32) { + self.push_event("on_client_remove", vec![("id", &id.to_string())]); + } + + fn new_message(&self, id: i32, text: String) { + self.push_event( + "chat_server_mode", + vec![("id", &id.to_string()), ("text", &text)], + ); + } } - lazy_static::lazy_static! { - static ref CLIENTS: RwLock> = Default::default(); + impl FlutterHandler { + fn push_event(&self, name: &str, event: Vec<(&str, &str)>) { + 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_MAIN) + { + s.add(serde_json::ser::to_string(&h).unwrap_or("".to_owned())); + }; + } } - static CLICK_TIME: AtomicI64 = AtomicI64::new(0); - - // // TODO clipboard_file - // enum ClipboardFileData { - // #[cfg(windows)] - // Clip((i32, ipc::ClipbaordFile)), - // Enable((i32, bool)), - // } - #[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn start_listen_ipc_thread() { - std::thread::spawn(move || start_ipc()); - } + use crate::{ + ipc::start_pa, + ui_cm_interface::{start_ipc, ConnectionManager}, + }; - #[cfg(not(any(target_os = "android", target_os = "ios")))] - #[tokio::main(flavor = "current_thread")] - async fn start_ipc() { - // TODO clipboard_file - // let (tx_file, _rx_file) = mpsc::unbounded_channel::(); - // #[cfg(windows)] - // let cm_clip = cm.clone(); - // #[cfg(windows)] - // std::thread::spawn(move || start_clipboard_file(cm_clip, _rx_file)); + #[cfg(target_os = "linux")] + std::thread::spawn(start_pa); - #[cfg(windows)] - std::thread::spawn(move || { - log::info!("try create privacy mode window"); - #[cfg(windows)] - { - if let Err(e) = crate::platform::windows::check_update_broker_process() { - log::warn!( - "Failed to check update broker process. Privacy mode may not work properly. {}", - e - ); - } - } - allow_err!(crate::ui::win_privacy::start()); - }); - - match new_listener("_cm").await { - Ok(mut incoming) => { - while let Some(result) = incoming.next().await { - match result { - Ok(stream) => { - log::debug!("Got new connection"); - let mut stream = Connection::new(stream); - // let tx_file = tx_file.clone(); - tokio::spawn(async move { - // for tmp use, without real conn id - let conn_id_tmp = -1; - let mut conn_id: i32 = 0; - let (tx, mut rx) = mpsc::unbounded_channel::(); - let mut write_jobs: Vec = Vec::new(); - loop { - tokio::select! { - res = stream.next() => { - match res { - Err(err) => { - log::info!("cm ipc connection closed: {}", err); - break; - } - Ok(Some(data)) => { - match data { - Data::Login{id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, file_transfer_enabled, restart} => { - log::debug!("conn_id: {}", id); - conn_id = id; - // tx_file.send(ClipboardFileData::Enable((id, file_transfer_enabled))).ok(); - on_login(id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart, tx.clone()); - } - Data::Close => { - // tx_file.send(ClipboardFileData::Enable((conn_id, false))).ok(); - log::info!("cm ipc connection closed from connection request"); - break; - } - Data::PrivacyModeState((_, _)) => { - conn_id = conn_id_tmp; - allow_err!(tx.send(data)); - } - Data::ClickTime(ms) => { - CLICK_TIME.store(ms, Ordering::SeqCst); - } - Data::ChatMessage { text } => { - handle_chat(conn_id, text); - } - Data::FS(fs) => { - handle_fs(fs, &mut write_jobs, &tx).await; - } - // TODO ClipbaordFile - // #[cfg(windows)] - // Data::ClipbaordFile(_clip) => { - // tx_file - // .send(ClipboardFileData::Clip((id, _clip))) - // .ok(); - // } - // #[cfg(windows)] - // Data::ClipboardFileEnabled(enabled) => { - // tx_file - // .send(ClipboardFileData::Enable((id, enabled))) - // .ok(); - // } - _ => {} - } - } - _ => {} - } - } - Some(data) = rx.recv() => { - if stream.send(&data).await.is_err() { - break; - } - } - } - } - if conn_id != conn_id_tmp { - remove_connection(conn_id); - } - }); - } - Err(err) => { - log::error!("Couldn't get cm client: {:?}", err); - } - } - } - } - Err(err) => { - log::error!("Failed to start cm ipc server: {}", err); - } - } - // crate::platform::quit_gui(); - // TODO flutter quit_gui + let cm = ConnectionManager { + ui_handler: FlutterHandler {}, + }; + std::thread::spawn(move || start_ipc(cm)); } #[cfg(target_os = "android")] pub fn start_channel(rx: UnboundedReceiver, tx: UnboundedSender) { + use crate::ui_cm_interface::start_listen; std::thread::spawn(move || start_listen(rx, tx)); } - - #[cfg(target_os = "android")] - #[tokio::main(flavor = "current_thread")] - async fn start_listen(mut rx: UnboundedReceiver, tx: UnboundedSender) { - let mut current_id = 0; - let mut write_jobs: Vec = Vec::new(); - loop { - match rx.recv().await { - Some(Data::Login { - id, - is_file_transfer, - port_forward, - peer_id, - name, - authorized, - keyboard, - clipboard, - audio, - file, - restart, - .. - }) => { - current_id = id; - on_login( - id, - is_file_transfer, - port_forward, - peer_id, - name, - authorized, - keyboard, - clipboard, - audio, - file, - restart, - tx.clone(), - ); - } - Some(Data::ChatMessage { text }) => { - handle_chat(current_id, text); - } - Some(Data::FS(fs)) => { - handle_fs(fs, &mut write_jobs, &tx).await; - } - Some(Data::Close) => { - break; - } - None => { - break; - } - _ => {} - } - } - remove_connection(current_id); - } - - fn on_login( - id: i32, - is_file_transfer: bool, - _port_forward: String, - peer_id: String, - name: String, - authorized: bool, - keyboard: bool, - clipboard: bool, - audio: bool, - file: bool, - restart: bool, - tx: mpsc::UnboundedSender, - ) { - let mut client = Client { - id, - authorized, - is_file_transfer, - name: name.clone(), - peer_id: peer_id.clone(), - keyboard, - clipboard, - audio, - file, - restart, - tx, - }; - if authorized { - client.authorized = true; - let client_json = serde_json::to_string(&client).unwrap_or("".into()); - // send to Android service, active notification no matter UI is shown or not. - #[cfg(any(target_os = "android"))] - if let Err(e) = - call_main_service_set_by_name("on_client_authorized", Some(&client_json), None) - { - log::debug!("call_service_set_by_name fail,{}", e); - } - // send to UI, refresh widget - push_event("on_client_authorized", vec![("client", &client_json)]); - } else { - let client_json = serde_json::to_string(&client).unwrap_or("".into()); - // send to Android service, active notification no matter UI is shown or not. - #[cfg(any(target_os = "android"))] - if let Err(e) = - call_main_service_set_by_name("try_start_without_auth", Some(&client_json), None) - { - log::debug!("call_service_set_by_name fail,{}", e); - } - // send to UI, refresh widget - push_event("try_start_without_auth", vec![("client", &client_json)]); - } - CLIENTS.write().unwrap().insert(id, client); - } - - fn push_event(name: &str, event: Vec<(&str, &str)>) { - 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_MAIN) - { - s.add(serde_json::ser::to_string(&h).unwrap_or("".to_owned())); - }; - } - - pub fn get_click_time() -> i64 { - CLICK_TIME.load(Ordering::SeqCst) - } - - pub fn check_click_time(id: i32) { - if let Some(client) = CLIENTS.read().unwrap().get(&id) { - allow_err!(client.tx.send(Data::ClickTime(0))); - }; - } - - pub fn switch_permission(id: i32, name: String, enabled: bool) { - if let Some(client) = CLIENTS.read().unwrap().get(&id) { - allow_err!(client.tx.send(Data::SwitchPermission { name, enabled })); - }; - } - - pub fn get_clients_state() -> String { - let clients = CLIENTS.read().unwrap(); - let res = Vec::from_iter(clients.values().cloned()); - serde_json::to_string(&res).unwrap_or("".into()) - } - - pub fn get_clients_length() -> usize { - let clients = CLIENTS.read().unwrap(); - clients.len() - } - - pub fn close_conn(id: i32) { - if let Some(client) = CLIENTS.read().unwrap().get(&id) { - allow_err!(client.tx.send(Data::Close)); - }; - } - - pub fn on_login_res(id: i32, res: bool) { - if let Some(client) = CLIENTS.write().unwrap().get_mut(&id) { - if res { - allow_err!(client.tx.send(Data::Authorize)); - client.authorized = true; - } else { - allow_err!(client.tx.send(Data::Close)); - } - }; - } - - fn remove_connection(id: i32) { - let mut clients = CLIENTS.write().unwrap(); - clients.remove(&id); - - if clients - .iter() - .filter(|(_k, v)| !v.is_file_transfer) - .next() - .is_none() - { - #[cfg(any(target_os = "android"))] - if let Err(e) = call_main_service_set_by_name("stop_capture", None, None) { - log::debug!("stop_capture err:{}", e); - } - } - - push_event("on_client_remove", vec![("id", &id.to_string())]); - } - - // server mode handle chat from other peers - fn handle_chat(id: i32, text: String) { - push_event( - "chat_server_mode", - vec![("id", &id.to_string()), ("text", &text)], - ); - } - - // server mode send chat to peer - pub fn send_chat(id: i32, text: String) { - let clients = CLIENTS.read().unwrap(); - if let Some(client) = clients.get(&id) { - allow_err!(client.tx.send(Data::ChatMessage { text })); - } - } - - // handle FS server - async fn handle_fs( - fs: ipc::FS, - write_jobs: &mut Vec, - tx: &UnboundedSender, - ) { - match fs { - ipc::FS::ReadDir { - dir, - include_hidden, - } => { - read_dir(&dir, include_hidden, tx).await; - } - ipc::FS::RemoveDir { - path, - id, - recursive, - } => { - remove_dir(path, id, recursive, tx).await; - } - ipc::FS::RemoveFile { path, id, file_num } => { - remove_file(path, id, file_num, tx).await; - } - ipc::FS::CreateDir { path, id } => { - create_dir(path, id, tx).await; - } - ipc::FS::NewWrite { - path, - id, - file_num, - mut files, - overwrite_detection, - } => { - write_jobs.push(fs::TransferJob::new_write( - id, - "".to_string(), - path, - file_num, - false, - false, - files - .drain(..) - .map(|f| FileEntry { - name: f.0, - modified_time: f.1, - ..Default::default() - }) - .collect(), - overwrite_detection, - )); - } - ipc::FS::CancelWrite { id } => { - if let Some(job) = fs::get_job(id, write_jobs) { - job.remove_download_file(); - fs::remove_job(id, write_jobs); - } - } - ipc::FS::WriteDone { id, file_num } => { - if let Some(job) = fs::get_job(id, write_jobs) { - job.modify_time(); - send_raw(fs::new_done(id, file_num), tx); - fs::remove_job(id, write_jobs); - } - } - ipc::FS::WriteBlock { - id, - file_num, - data, - compressed, - } => { - if let Some(job) = fs::get_job(id, write_jobs) { - if let Err(err) = job - .write( - FileTransferBlock { - id, - file_num, - data, - compressed, - ..Default::default() - }, - None, - ) - .await - { - send_raw(fs::new_error(id, err, file_num), &tx); - } - } - } - ipc::FS::CheckDigest { - id, - file_num, - file_size, - last_modified, - is_upload, - } => { - if let Some(job) = fs::get_job(id, write_jobs) { - let mut req = FileTransferSendConfirmRequest { - id, - file_num, - union: Some(file_transfer_send_confirm_request::Union::OffsetBlk(0)), - ..Default::default() - }; - let digest = FileTransferDigest { - id, - file_num, - last_modified, - file_size, - ..Default::default() - }; - if let Some(file) = job.files().get(file_num as usize) { - let path = get_string(&job.join(&file.name)); - match is_write_need_confirmation(&path, &digest) { - Ok(digest_result) => { - match digest_result { - DigestCheckResult::IsSame => { - req.set_skip(true); - let msg_out = new_send_confirm(req); - send_raw(msg_out, &tx); - } - DigestCheckResult::NeedConfirm(mut digest) => { - // upload to server, but server has the same file, request - digest.is_upload = is_upload; - let mut msg_out = Message::new(); - let mut fr = FileResponse::new(); - fr.set_digest(digest); - msg_out.set_file_response(fr); - send_raw(msg_out, &tx); - } - DigestCheckResult::NoSuchFile => { - let msg_out = new_send_confirm(req); - send_raw(msg_out, &tx); - } - } - } - Err(err) => { - send_raw(fs::new_error(id, err, file_num), &tx); - } - } - } - } - } - _ => {} - } - } - - async fn read_dir(dir: &str, include_hidden: bool, tx: &UnboundedSender) { - let path = { - if dir.is_empty() { - Config::get_home() - } else { - fs::get_path(dir) - } - }; - if let Ok(Ok(fd)) = spawn_blocking(move || fs::read_dir(&path, include_hidden)).await { - let mut msg_out = Message::new(); - let mut file_response = FileResponse::new(); - file_response.set_dir(fd); - msg_out.set_file_response(file_response); - send_raw(msg_out, tx); - } - } - - async fn handle_result( - res: std::result::Result, S>, - id: i32, - file_num: i32, - tx: &UnboundedSender, - ) { - match res { - Err(err) => { - send_raw(fs::new_error(id, err, file_num), tx); - } - Ok(Err(err)) => { - send_raw(fs::new_error(id, err, file_num), tx); - } - Ok(Ok(())) => { - send_raw(fs::new_done(id, file_num), tx); - } - } - } - - async fn remove_file(path: String, id: i32, file_num: i32, tx: &UnboundedSender) { - handle_result( - spawn_blocking(move || fs::remove_file(&path)).await, - id, - file_num, - tx, - ) - .await; - } - - async fn create_dir(path: String, id: i32, tx: &UnboundedSender) { - handle_result( - spawn_blocking(move || fs::create_dir(&path)).await, - id, - 0, - tx, - ) - .await; - } - - async fn remove_dir(path: String, id: i32, recursive: bool, tx: &UnboundedSender) { - let path = fs::get_path(&path); - handle_result( - spawn_blocking(move || { - if recursive { - fs::remove_all_empty_dir(&path) - } else { - std::fs::remove_dir(&path).map_err(|err| err.into()) - } - }) - .await, - id, - 0, - tx, - ) - .await; - } - - fn send_raw(msg: Message, tx: &UnboundedSender) { - match msg.write_to_bytes() { - Ok(bytes) => { - allow_err!(tx.send(Data::RawMessage(bytes))); - } - err => allow_err!(err), - } - } } #[inline] diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 032be5b1f..2a41264f0 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -13,7 +13,6 @@ use hbb_common::{ fs, log, }; -use crate::flutter::connection_manager::{self, get_clients_length, get_clients_state}; use crate::flutter::{self, SESSIONS}; use crate::start_server; use crate::ui_interface; @@ -673,13 +672,13 @@ pub fn main_get_online_statue() -> i64 { ONLINE.lock().unwrap().values().max().unwrap_or(&0).clone() } -pub fn main_get_clients_state() -> String { - get_clients_state() +pub fn cm_get_clients_state() -> String { + crate::ui_cm_interface::get_clients_state() } -pub fn main_check_clients_length(length: usize) -> Option { - if length != get_clients_length() { - Some(get_clients_state()) +pub fn cm_check_clients_length(length: usize) -> Option { + if length != crate::ui_cm_interface::get_clients_length() { + Some(crate::ui_cm_interface::get_clients_state()) } else { None } @@ -791,27 +790,31 @@ pub fn main_get_mouse_time() -> f64 { } pub fn cm_send_chat(conn_id: i32, msg: String) { - connection_manager::send_chat(conn_id, msg); + crate::ui_cm_interface::send_chat(conn_id, msg); } pub fn cm_login_res(conn_id: i32, res: bool) { - connection_manager::on_login_res(conn_id, res); + if res { + crate::ui_cm_interface::authorize(conn_id); + } else { + crate::ui_cm_interface::close(conn_id); + } } pub fn cm_close_connection(conn_id: i32) { - connection_manager::close_conn(conn_id); + crate::ui_cm_interface::close(conn_id); } pub fn cm_check_click_time(conn_id: i32) { - connection_manager::check_click_time(conn_id) + crate::ui_cm_interface::check_click_time(conn_id) } pub fn cm_get_click_time() -> f64 { - connection_manager::get_click_time() as _ + crate::ui_cm_interface::get_click_time() as _ } pub fn cm_switch_permission(conn_id: i32, name: String, enabled: bool) { - connection_manager::switch_permission(conn_id, name, enabled) + crate::ui_cm_interface::switch_permission(conn_id, name, enabled) } pub fn main_get_icon() -> String { diff --git a/src/lib.rs b/src/lib.rs index f554d447e..b427c3301 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -49,6 +49,7 @@ mod tray; mod ui_interface; mod ui_session_interface; +mod ui_cm_interface; #[cfg(windows)] pub mod clipboard_file; diff --git a/src/ui.rs b/src/ui.rs index b66d1453b..b8b136c45 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -125,7 +125,7 @@ pub fn start(args: &mut [String]) { page = "install.html"; } else if args[0] == "--cm" { frame.register_behavior("connection-manager", move || { - Box::new(cm::ConnectionManager::new()) + Box::new(cm::SciterConnectionManager::new()) }); page = "cm.html"; } else if (args[0] == "--connect" diff --git a/src/ui/cm.rs b/src/ui/cm.rs index 222b9b5c9..2b1e3e791 100644 --- a/src/ui/cm.rs +++ b/src/ui/cm.rs @@ -1,60 +1,83 @@ #[cfg(target_os = "linux")] use crate::ipc::start_pa; -use crate::ipc::{self, new_listener, Connection, Data}; -use crate::VERSION; +use crate::ui_cm_interface::{start_ipc, ConnectionManager, InvokeUiCM}; + #[cfg(windows)] use clipboard::{ create_cliprdr_context, empty_clipboard, get_rx_clip_client, server_clip_file, set_conn_enabled, }; -use hbb_common::fs::{ - can_enable_overwrite_detection, get_string, is_write_need_confirmation, new_send_confirm, - DigestCheckResult, -}; -use hbb_common::{ - allow_err, - config::Config, - fs, get_version_number, log, - message_proto::*, - protobuf::Message as _, - tokio::{self, sync::mpsc, task::spawn_blocking}, -}; -use sciter::{make_args, Element, Value, HELEMENT}; -use std::{ - collections::HashMap, - ops::Deref, - sync::{Arc, RwLock}, -}; -pub struct ConnectionManagerInner { - root: Option, - senders: HashMap>, - click_time: i64, +use hbb_common::{allow_err, log}; +use sciter::{make_args, Element, Value, HELEMENT}; +use std::sync::Mutex; +use std::{ops::Deref, sync::Arc}; + +#[derive(Clone, Default)] +pub struct SciterHandler { + pub element: Arc>>, } -#[derive(Clone)] -pub struct ConnectionManager(Arc>); +impl InvokeUiCM for SciterHandler { + fn add_connection(&self, client: &crate::ui_cm_interface::Client) { + self.call( + "addConnection", + &make_args!( + client.id, + client.is_file_transfer, + client.port_forward.clone(), + client.peer_id.clone(), + client.name.clone(), + client.authorized, + client.keyboard, + client.clipboard, + client.audio, + client.file, + client.restart + ), + ); + } -impl Deref for ConnectionManager { - type Target = Arc>; + fn remove_connection(&self, id: i32) { + self.call("removeConnection", &make_args!(id)); + if crate::ui_cm_interface::get_clients_length().eq(&0) { + crate::platform::quit_gui(); + } + } + + fn new_message(&self, id: i32, text: String) { + self.call("newMessage", &make_args!(id, text)); + } +} + +impl SciterHandler { + #[inline] + fn call(&self, func: &str, args: &[Value]) { + if let Some(e) = self.element.lock().unwrap().as_ref() { + allow_err!(e.call_method(func, &super::value_crash_workaround(args)[..])); + } + } +} + +pub struct SciterConnectionManager(ConnectionManager); + +impl Deref for SciterConnectionManager { + type Target = ConnectionManager; fn deref(&self) -> &Self::Target { &self.0 } } -impl ConnectionManager { +impl SciterConnectionManager { pub fn new() -> Self { #[cfg(target_os = "linux")] std::thread::spawn(start_pa); - let inner = ConnectionManagerInner { - root: None, - senders: HashMap::new(), - click_time: Default::default(), + let cm = ConnectionManager { + ui_handler: SciterHandler::default(), }; - let cm = Self(Arc::new(RwLock::new(inner))); let cloned = cm.clone(); std::thread::spawn(move || start_ipc(cloned)); - cm + SciterConnectionManager(cm) } fn get_icon(&mut self) -> String { @@ -62,359 +85,27 @@ impl ConnectionManager { } fn check_click_time(&mut self, id: i32) { - let lock = self.read().unwrap(); - if let Some(s) = lock.senders.get(&id) { - allow_err!(s.send(Data::ClickTime(0))); - } + crate::ui_cm_interface::check_click_time(id); } fn get_click_time(&self) -> f64 { - self.read().unwrap().click_time as _ - } - - #[inline] - fn call(&self, func: &str, args: &[Value]) { - let r = self.read().unwrap(); - if let Some(ref e) = r.root { - allow_err!(e.call_method(func, &super::value_crash_workaround(args)[..])); - } - } - - fn add_connection( - &self, - id: i32, - is_file_transfer: bool, - port_forward: String, - peer_id: String, - name: String, - authorized: bool, - keyboard: bool, - clipboard: bool, - audio: bool, - file: bool, - restart: bool, - tx: mpsc::UnboundedSender, - ) { - self.call( - "addConnection", - &make_args!( - id, - is_file_transfer, - port_forward, - peer_id, - name, - authorized, - keyboard, - clipboard, - audio, - file, - restart - ), - ); - self.write().unwrap().senders.insert(id, tx); - } - - fn remove_connection(&self, id: i32) { - self.write().unwrap().senders.remove(&id); - if self.read().unwrap().senders.len() == 0 { - crate::platform::quit_gui(); - } - self.call("removeConnection", &make_args!(id)); - } - - async fn handle_data( - &self, - id: i32, - data: Data, - _tx_clip_file: &mpsc::UnboundedSender, - write_jobs: &mut Vec, - conn: &mut Connection, - ) { - match data { - Data::ChatMessage { text } => { - self.call("newMessage", &make_args!(id, text)); - } - Data::ClickTime(ms) => { - self.write().unwrap().click_time = ms; - } - Data::FS(v) => match v { - ipc::FS::ReadDir { - dir, - include_hidden, - } => { - Self::read_dir(&dir, include_hidden, conn).await; - } - ipc::FS::RemoveDir { - path, - id, - recursive, - } => { - Self::remove_dir(path, id, recursive, conn).await; - } - ipc::FS::RemoveFile { path, id, file_num } => { - Self::remove_file(path, id, file_num, conn).await; - } - ipc::FS::CreateDir { path, id } => { - Self::create_dir(path, id, conn).await; - } - ipc::FS::NewWrite { - path, - id, - file_num, - mut files, - overwrite_detection, - } => { - // cm has no show_hidden context - // dummy remote, show_hidden, is_remote - write_jobs.push(fs::TransferJob::new_write( - id, - "".to_string(), - path, - file_num, - false, - false, - files - .drain(..) - .map(|f| FileEntry { - name: f.0, - modified_time: f.1, - ..Default::default() - }) - .collect(), - overwrite_detection, - )); - } - ipc::FS::CancelWrite { id } => { - if let Some(job) = fs::get_job(id, write_jobs) { - job.remove_download_file(); - fs::remove_job(id, write_jobs); - } - } - ipc::FS::WriteDone { id, file_num } => { - if let Some(job) = fs::get_job(id, write_jobs) { - job.modify_time(); - Self::send(fs::new_done(id, file_num), conn).await; - fs::remove_job(id, write_jobs); - } - } - ipc::FS::CheckDigest { - id, - file_num, - file_size, - last_modified, - is_upload, - } => { - if let Some(job) = fs::get_job(id, write_jobs) { - let mut req = FileTransferSendConfirmRequest { - id, - file_num, - union: Some(file_transfer_send_confirm_request::Union::OffsetBlk(0)), - ..Default::default() - }; - let digest = FileTransferDigest { - id, - file_num, - last_modified, - file_size, - ..Default::default() - }; - if let Some(file) = job.files().get(file_num as usize) { - let path = get_string(&job.join(&file.name)); - match is_write_need_confirmation(&path, &digest) { - Ok(digest_result) => { - match digest_result { - DigestCheckResult::IsSame => { - req.set_skip(true); - let msg_out = new_send_confirm(req); - Self::send(msg_out, conn).await; - } - DigestCheckResult::NeedConfirm(mut digest) => { - // upload to server, but server has the same file, request - digest.is_upload = is_upload; - let mut msg_out = Message::new(); - let mut fr = FileResponse::new(); - fr.set_digest(digest); - msg_out.set_file_response(fr); - Self::send(msg_out, conn).await; - } - DigestCheckResult::NoSuchFile => { - let msg_out = new_send_confirm(req); - Self::send(msg_out, conn).await; - } - } - } - Err(err) => { - Self::send(fs::new_error(id, err, file_num), conn).await; - } - } - } - } - } - ipc::FS::WriteBlock { - id, - file_num, - data, - compressed, - } => { - let raw = if let Ok(bytes) = conn.next_raw().await { - Some(bytes) - } else { - None - }; - if let Some(job) = fs::get_job(id, write_jobs) { - if let Err(err) = job - .write( - FileTransferBlock { - id, - file_num, - data, - compressed, - ..Default::default() - }, - raw.as_ref().map(|x| &x[..]), - ) - .await - { - Self::send(fs::new_error(id, err, file_num), conn).await; - } - } - } - ipc::FS::WriteOffset { - id: _, - file_num: _, - offset_blk: _, - } => {} - }, - #[cfg(windows)] - Data::ClipbaordFile(_clip) => { - _tx_clip_file - .send(ClipboardFileData::Clip((id, _clip))) - .ok(); - } - #[cfg(windows)] - Data::ClipboardFileEnabled(enabled) => { - _tx_clip_file - .send(ClipboardFileData::Enable((id, enabled))) - .ok(); - } - _ => {} - } - } - - async fn read_dir(dir: &str, include_hidden: bool, conn: &mut Connection) { - let path = { - if dir.is_empty() { - Config::get_home() - } else { - fs::get_path(dir) - } - }; - if let Ok(Ok(fd)) = spawn_blocking(move || fs::read_dir(&path, include_hidden)).await { - let mut msg_out = Message::new(); - let mut file_response = FileResponse::new(); - file_response.set_dir(fd); - msg_out.set_file_response(file_response); - Self::send(msg_out, conn).await; - } - } - - async fn handle_result( - res: std::result::Result, S>, - id: i32, - file_num: i32, - conn: &mut Connection, - ) { - match res { - Err(err) => { - Self::send(fs::new_error(id, err, file_num), conn).await; - } - Ok(Err(err)) => { - Self::send(fs::new_error(id, err, file_num), conn).await; - } - Ok(Ok(())) => { - Self::send(fs::new_done(id, file_num), conn).await; - } - } - } - - async fn remove_file(path: String, id: i32, file_num: i32, conn: &mut Connection) { - Self::handle_result( - spawn_blocking(move || fs::remove_file(&path)).await, - id, - file_num, - conn, - ) - .await; - } - - async fn create_dir(path: String, id: i32, conn: &mut Connection) { - Self::handle_result( - spawn_blocking(move || fs::create_dir(&path)).await, - id, - 0, - conn, - ) - .await; - } - - async fn remove_dir(path: String, id: i32, recursive: bool, conn: &mut Connection) { - let path = fs::get_path(&path); - Self::handle_result( - spawn_blocking(move || { - if recursive { - fs::remove_all_empty_dir(&path) - } else { - std::fs::remove_dir(&path).map_err(|err| err.into()) - } - }) - .await, - id, - 0, - conn, - ) - .await; - } - - async fn send(msg: Message, conn: &mut Connection) { - match msg.write_to_bytes() { - Ok(bytes) => allow_err!(conn.send(&Data::RawMessage(bytes)).await), - err => allow_err!(err), - } + crate::ui_cm_interface::get_click_time() as _ } fn switch_permission(&self, id: i32, name: String, enabled: bool) { - let lock = self.read().unwrap(); - if let Some(s) = lock.senders.get(&id) { - allow_err!(s.send(Data::SwitchPermission { name, enabled })); - } + crate::ui_cm_interface::switch_permission(id, name, enabled); } fn close(&self, id: i32) { - let lock = self.read().unwrap(); - if let Some(s) = lock.senders.get(&id) { - allow_err!(s.send(Data::Close)); - } - } - - fn send_msg(&self, id: i32, text: String) { - let lock = self.read().unwrap(); - if let Some(s) = lock.senders.get(&id) { - allow_err!(s.send(Data::ChatMessage { text })); - } - } - - fn send_data(&self, id: i32, data: Data) { - let lock = self.read().unwrap(); - if let Some(s) = lock.senders.get(&id) { - allow_err!(s.send(data)); - } + crate::ui_cm_interface::close(id); } fn authorize(&self, id: i32) { - let lock = self.read().unwrap(); - if let Some(s) = lock.senders.get(&id) { - allow_err!(s.send(Data::Authorize)); - } + crate::ui_cm_interface::authorize(id); + } + + fn send_msg(&self, id: i32, text: String) { + crate::ui_cm_interface::send_chat(id, text); } fn t(&self, name: String) -> String { @@ -422,9 +113,9 @@ impl ConnectionManager { } } -impl sciter::EventHandler for ConnectionManager { +impl sciter::EventHandler for SciterConnectionManager { fn attached(&mut self, root: HELEMENT) { - self.write().unwrap().root = Some(Element::from(root)); + *self.ui_handler.element.lock().unwrap() = Some(Element::from(root)); } sciter::dispatch_script_call! { @@ -438,179 +129,3 @@ impl sciter::EventHandler for ConnectionManager { fn send_msg(i32, String); } } - -pub enum ClipboardFileData { - #[cfg(windows)] - Clip((i32, ipc::ClipbaordFile)), - Enable((i32, bool)), -} - -#[tokio::main(flavor = "current_thread")] -async fn start_ipc(cm: ConnectionManager) { - let (tx_file, _rx_file) = mpsc::unbounded_channel::(); - #[cfg(windows)] - let cm_clip = cm.clone(); - #[cfg(windows)] - std::thread::spawn(move || start_clipboard_file(cm_clip, _rx_file)); - - #[cfg(windows)] - std::thread::spawn(move || { - log::info!("try create privacy mode window"); - #[cfg(windows)] - { - if let Err(e) = crate::platform::windows::check_update_broker_process() { - log::warn!( - "Failed to check update broker process. Privacy mode may not work properly. {}", - e - ); - } - } - allow_err!(crate::ui::win_privacy::start()); - }); - - match new_listener("_cm").await { - Ok(mut incoming) => { - while let Some(result) = incoming.next().await { - match result { - Ok(stream) => { - log::debug!("Got new connection"); - let mut stream = Connection::new(stream); - let cm = cm.clone(); - let tx_file = tx_file.clone(); - tokio::spawn(async move { - // for tmp use, without real conn id - let conn_id_tmp = -1; - let mut conn_id: i32 = 0; - let (tx, mut rx) = mpsc::unbounded_channel::(); - let mut write_jobs: Vec = Vec::new(); - loop { - tokio::select! { - res = stream.next() => { - match res { - Err(err) => { - log::info!("cm ipc connection closed: {}", err); - break; - } - Ok(Some(data)) => { - match data { - Data::Login{id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, file_transfer_enabled, restart} => { - log::debug!("conn_id: {}", id); - conn_id = id; - tx_file.send(ClipboardFileData::Enable((id, file_transfer_enabled))).ok(); - cm.add_connection(id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart, tx.clone()); - } - Data::Close => { - tx_file.send(ClipboardFileData::Enable((conn_id, false))).ok(); - log::info!("cm ipc connection closed from connection request"); - break; - } - Data::PrivacyModeState((id, _)) => { - conn_id = conn_id_tmp; - cm.send_data(id, data) - } - _ => { - cm.handle_data(conn_id, data, &tx_file, &mut write_jobs, &mut stream).await; - } - } - } - _ => {} - } - } - Some(data) = rx.recv() => { - if stream.send(&data).await.is_err() { - break; - } - } - } - } - if conn_id != conn_id_tmp { - cm.remove_connection(conn_id); - } - }); - } - Err(err) => { - log::error!("Couldn't get cm client: {:?}", err); - } - } - } - } - Err(err) => { - log::error!("Failed to start cm ipc server: {}", err); - } - } - crate::platform::quit_gui(); -} - -#[cfg(windows)] -#[tokio::main(flavor = "current_thread")] -pub async fn start_clipboard_file( - cm: ConnectionManager, - mut rx: mpsc::UnboundedReceiver, -) { - let mut cliprdr_context = None; - let mut rx_clip_client = get_rx_clip_client().lock().await; - - loop { - tokio::select! { - clip_file = rx_clip_client.recv() => match clip_file { - Some((conn_id, clip)) => { - cmd_inner_send( - &cm, - conn_id, - Data::ClipbaordFile(clip) - ); - } - None => { - // - } - }, - server_msg = rx.recv() => match server_msg { - Some(ClipboardFileData::Clip((conn_id, clip))) => { - if let Some(ctx) = cliprdr_context.as_mut() { - server_clip_file(ctx, conn_id, clip); - } - } - Some(ClipboardFileData::Enable((id, enabled))) => { - if enabled && cliprdr_context.is_none() { - cliprdr_context = Some(match create_cliprdr_context(true, false) { - Ok(context) => { - log::info!("clipboard context for file transfer created."); - context - } - Err(err) => { - log::error!( - "Create clipboard context for file transfer: {}", - err.to_string() - ); - return; - } - }); - } - set_conn_enabled(id, enabled); - if !enabled { - if let Some(ctx) = cliprdr_context.as_mut() { - empty_clipboard(ctx, id); - } - } - } - None => { - break - } - } - } - } -} - -#[cfg(windows)] -fn cmd_inner_send(cm: &ConnectionManager, id: i32, data: Data) { - let lock = cm.read().unwrap(); - if id != 0 { - if let Some(s) = lock.senders.get(&id) { - allow_err!(s.send(data)); - } - } else { - for s in lock.senders.values() { - allow_err!(s.send(data.clone())); - } - } -} diff --git a/src/ui/remote.rs b/src/ui/remote.rs index b377b8583..f6b3acec6 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -2,7 +2,7 @@ use std::{ collections::HashMap, ops::{Deref, DerefMut}, sync::{ - atomic::{AtomicBool, Ordering}, + atomic::Ordering, Arc, Mutex, }, }; @@ -31,7 +31,7 @@ use hbb_common::{ use crate::clipboard_file::*; use crate::{ client::*, - ui_session_interface::{InvokeUi, Session, IS_IN}, + ui_session_interface::{InvokeUiSession, Session, IS_IN}, }; type Video = AssetPtr; @@ -68,7 +68,7 @@ impl SciterHandler { } } -impl InvokeUi for SciterHandler { +impl InvokeUiSession for SciterHandler { fn set_cursor_data(&self, cd: CursorData) { let mut colors = hbb_common::compress::decompress(&cd.colors); if colors.iter().filter(|x| **x != 0).next().is_none() { @@ -445,6 +445,14 @@ impl SciterSession { v } + pub fn t(&self, name: String) -> String { + crate::client::translate(name) + } + + pub fn get_icon(&self) -> String { + crate::get_icon() + } + fn supported_hwcodec(&self) -> Value { #[cfg(feature = "hwcodec")] { diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs new file mode 100644 index 000000000..8a26a9558 --- /dev/null +++ b/src/ui_cm_interface.rs @@ -0,0 +1,665 @@ +use std::ops::{Deref, DerefMut}; +use std::{ + collections::HashMap, + iter::FromIterator, + sync::{ + atomic::{AtomicI64, Ordering}, + RwLock, + }, +}; + +use serde_derive::Serialize; + +use crate::ipc::Data; +use crate::ipc::{self, new_listener, Connection}; +use hbb_common::{ + allow_err, + config::Config, + fs::is_write_need_confirmation, + fs::{self, get_string, new_send_confirm, DigestCheckResult}, + log, + message_proto::*, + protobuf::Message as _, + tokio::{ + self, + sync::mpsc::{self, UnboundedSender}, + task::spawn_blocking, + }, +}; + +#[derive(Serialize, Clone)] +pub struct Client { + pub id: i32, + pub authorized: bool, + pub is_file_transfer: bool, + pub port_forward: String, + pub name: String, + pub peer_id: String, + pub keyboard: bool, + pub clipboard: bool, + pub audio: bool, + pub file: bool, + pub restart: bool, + #[serde(skip)] + tx: UnboundedSender, +} + +lazy_static::lazy_static! { + static ref CLIENTS: RwLock> = Default::default(); + static ref CLICK_TIME: AtomicI64 = AtomicI64::new(0); +} + +#[derive(Clone)] +pub struct ConnectionManager { + pub ui_handler: T, +} + +pub trait InvokeUiCM: Send + Clone + 'static + Sized { + fn add_connection(&self, client: &Client); + + fn remove_connection(&self, id: i32); + + fn new_message(&self, id: i32, text: String); +} + +impl Deref for ConnectionManager { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.ui_handler + } +} + +impl DerefMut for ConnectionManager { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.ui_handler + } +} + +impl ConnectionManager { + fn add_connection( + &self, + id: i32, + is_file_transfer: bool, + port_forward: String, + peer_id: String, + name: String, + authorized: bool, + keyboard: bool, + clipboard: bool, + audio: bool, + file: bool, + restart: bool, + tx: mpsc::UnboundedSender, + ) { + let client = Client { + id, + authorized, + is_file_transfer, + port_forward, + name: name.clone(), + peer_id: peer_id.clone(), + keyboard, + clipboard, + audio, + file, + restart, + tx, + }; + self.ui_handler.add_connection(&client); + CLIENTS.write().unwrap().insert(id, client); + } + + fn remove_connection(&self, id: i32) { + CLIENTS.write().unwrap().remove(&id); + + #[cfg(any(target_os = "android"))] + if clients + .iter() + .filter(|(_k, v)| !v.is_file_transfer) + .next() + .is_none() + { + if let Err(e) = call_main_service_set_by_name("stop_capture", None, None) { + log::debug!("stop_capture err:{}", e); + } + } + + self.ui_handler.remove_connection(id); + } +} + +#[inline] +pub fn check_click_time(id: i32) { + if let Some(client) = CLIENTS.read().unwrap().get(&id) { + allow_err!(client.tx.send(Data::ClickTime(0))); + }; +} + +#[inline] +pub fn get_click_time() -> i64 { + CLICK_TIME.load(Ordering::SeqCst) +} + +#[inline] +pub fn authorize(id: i32) { + if let Some(client) = CLIENTS.write().unwrap().get_mut(&id) { + client.authorized = true; + allow_err!(client.tx.send(Data::Authorize)); + }; +} + +#[inline] +pub fn close(id: i32) { + if let Some(client) = CLIENTS.read().unwrap().get(&id) { + allow_err!(client.tx.send(Data::Close)); + }; +} + +// server mode send chat to peer +#[inline] +pub fn send_chat(id: i32, text: String) { + let clients = CLIENTS.read().unwrap(); + if let Some(client) = clients.get(&id) { + allow_err!(client.tx.send(Data::ChatMessage { text })); + } +} + +#[inline] +pub fn switch_permission(id: i32, name: String, enabled: bool) { + if let Some(client) = CLIENTS.read().unwrap().get(&id) { + allow_err!(client.tx.send(Data::SwitchPermission { name, enabled })); + }; +} + +#[inline] +pub fn get_clients_state() -> String { + let clients = CLIENTS.read().unwrap(); + let res = Vec::from_iter(clients.values().cloned()); + serde_json::to_string(&res).unwrap_or("".into()) +} + +#[inline] +pub fn get_clients_length() -> usize { + let clients = CLIENTS.read().unwrap(); + clients.len() +} + +pub enum ClipboardFileData { + #[cfg(windows)] + Clip((i32, ipc::ClipbaordFile)), + Enable((i32, bool)), +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +#[tokio::main(flavor = "current_thread")] +pub async fn start_ipc(cm: ConnectionManager) { + let (tx_file, _rx_file) = mpsc::unbounded_channel::(); + #[cfg(windows)] + let cm_clip = cm.clone(); + #[cfg(windows)] + std::thread::spawn(move || start_clipboard_file(cm_clip, _rx_file)); + + #[cfg(windows)] + std::thread::spawn(move || { + log::info!("try create privacy mode window"); + #[cfg(windows)] + { + if let Err(e) = crate::platform::windows::check_update_broker_process() { + log::warn!( + "Failed to check update broker process. Privacy mode may not work properly. {}", + e + ); + } + } + allow_err!(crate::ui::win_privacy::start()); + }); + + match new_listener("_cm").await { + Ok(mut incoming) => { + while let Some(result) = incoming.next().await { + match result { + Ok(stream) => { + log::debug!("Got new connection"); + let mut stream = Connection::new(stream); + let cm = cm.clone(); + let tx_file = tx_file.clone(); + tokio::spawn(async move { + // for tmp use, without real conn id + let conn_id_tmp = -1; + let mut conn_id: i32 = 0; + let (tx, mut rx) = mpsc::unbounded_channel::(); + let mut write_jobs: Vec = Vec::new(); + loop { + tokio::select! { + res = stream.next() => { + match res { + Err(err) => { + log::info!("cm ipc connection closed: {}", err); + break; + } + Ok(Some(data)) => { + match data { + Data::Login{id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, file_transfer_enabled, restart} => { + log::debug!("conn_id: {}", id); + conn_id = id; + tx_file.send(ClipboardFileData::Enable((id, file_transfer_enabled))).ok(); + cm.add_connection(id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart, tx.clone()); + } + Data::Close => { + tx_file.send(ClipboardFileData::Enable((conn_id, false))).ok(); + log::info!("cm ipc connection closed from connection request"); + break; + } + Data::PrivacyModeState((id, _)) => { + conn_id = conn_id_tmp; + allow_err!(tx.send(data)); + } + Data::ClickTime(ms) => { + CLICK_TIME.store(ms, Ordering::SeqCst); + } + Data::ChatMessage { text } => { + cm.new_message(conn_id, text); + } + Data::FS(fs) => { + handle_fs(fs, &mut write_jobs, &tx).await; + } + // TODO ClipbaordFile + // #[cfg(windows)] + // Data::ClipbaordFile(_clip) => { + // tx_file + // .send(ClipboardFileData::Clip((id, _clip))) + // .ok(); + // } + // #[cfg(windows)] + // Data::ClipboardFileEnabled(enabled) => { + // tx_file + // .send(ClipboardFileData::Enable((id, enabled))) + // .ok(); + // } + _ => { + + } + } + } + _ => {} + } + } + Some(data) = rx.recv() => { + if stream.send(&data).await.is_err() { + break; + } + } + } + } + if conn_id != conn_id_tmp { + cm.remove_connection(conn_id); + } + }); + } + Err(err) => { + log::error!("Couldn't get cm client: {:?}", err); + } + } + } + } + Err(err) => { + log::error!("Failed to start cm ipc server: {}", err); + } + } + crate::platform::quit_gui(); +} + +#[cfg(target_os = "android")] +#[tokio::main(flavor = "current_thread")] +pub async fn start_listen(mut rx: UnboundedReceiver, tx: UnboundedSender) { + let mut current_id = 0; + let mut write_jobs: Vec = Vec::new(); + loop { + match rx.recv().await { + Some(Data::Login { + id, + is_file_transfer, + port_forward, + peer_id, + name, + authorized, + keyboard, + clipboard, + audio, + file, + restart, + .. + }) => { + current_id = id; + on_login( + id, + is_file_transfer, + port_forward, + peer_id, + name, + authorized, + keyboard, + clipboard, + audio, + file, + restart, + tx.clone(), + ); + } + Some(Data::ChatMessage { text }) => { + cm.new_message(conn_id, text); + } + Some(Data::FS(fs)) => { + handle_fs(fs, &mut write_jobs, &tx).await; + } + Some(Data::Close) => { + break; + } + None => { + break; + } + _ => {} + } + } + remove_connection(current_id); +} + +async fn handle_fs(fs: ipc::FS, write_jobs: &mut Vec, tx: &UnboundedSender) { + match fs { + ipc::FS::ReadDir { + dir, + include_hidden, + } => { + read_dir(&dir, include_hidden, tx).await; + } + ipc::FS::RemoveDir { + path, + id, + recursive, + } => { + remove_dir(path, id, recursive, tx).await; + } + ipc::FS::RemoveFile { path, id, file_num } => { + remove_file(path, id, file_num, tx).await; + } + ipc::FS::CreateDir { path, id } => { + create_dir(path, id, tx).await; + } + ipc::FS::NewWrite { + path, + id, + file_num, + mut files, + overwrite_detection, + } => { + // cm has no show_hidden context + // dummy remote, show_hidden, is_remote + write_jobs.push(fs::TransferJob::new_write( + id, + "".to_string(), + path, + file_num, + false, + false, + files + .drain(..) + .map(|f| FileEntry { + name: f.0, + modified_time: f.1, + ..Default::default() + }) + .collect(), + overwrite_detection, + )); + } + ipc::FS::CancelWrite { id } => { + if let Some(job) = fs::get_job(id, write_jobs) { + job.remove_download_file(); + fs::remove_job(id, write_jobs); + } + } + ipc::FS::WriteDone { id, file_num } => { + if let Some(job) = fs::get_job(id, write_jobs) { + job.modify_time(); + send_raw(fs::new_done(id, file_num), tx); + fs::remove_job(id, write_jobs); + } + } + ipc::FS::WriteBlock { + id, + file_num, + data, + compressed, + } => { + if let Some(job) = fs::get_job(id, write_jobs) { + if let Err(err) = job + .write( + FileTransferBlock { + id, + file_num, + data, + compressed, + ..Default::default() + }, + None, + ) + .await + { + send_raw(fs::new_error(id, err, file_num), &tx); + } + } + } + ipc::FS::CheckDigest { + id, + file_num, + file_size, + last_modified, + is_upload, + } => { + if let Some(job) = fs::get_job(id, write_jobs) { + let mut req = FileTransferSendConfirmRequest { + id, + file_num, + union: Some(file_transfer_send_confirm_request::Union::OffsetBlk(0)), + ..Default::default() + }; + let digest = FileTransferDigest { + id, + file_num, + last_modified, + file_size, + ..Default::default() + }; + if let Some(file) = job.files().get(file_num as usize) { + let path = get_string(&job.join(&file.name)); + match is_write_need_confirmation(&path, &digest) { + Ok(digest_result) => { + match digest_result { + DigestCheckResult::IsSame => { + req.set_skip(true); + let msg_out = new_send_confirm(req); + send_raw(msg_out, &tx); + } + DigestCheckResult::NeedConfirm(mut digest) => { + // upload to server, but server has the same file, request + digest.is_upload = is_upload; + let mut msg_out = Message::new(); + let mut fr = FileResponse::new(); + fr.set_digest(digest); + msg_out.set_file_response(fr); + send_raw(msg_out, &tx); + } + DigestCheckResult::NoSuchFile => { + let msg_out = new_send_confirm(req); + send_raw(msg_out, &tx); + } + } + } + Err(err) => { + send_raw(fs::new_error(id, err, file_num), &tx); + } + } + } + } + } + _ => {} + } +} + +async fn read_dir(dir: &str, include_hidden: bool, tx: &UnboundedSender) { + let path = { + if dir.is_empty() { + Config::get_home() + } else { + fs::get_path(dir) + } + }; + if let Ok(Ok(fd)) = spawn_blocking(move || fs::read_dir(&path, include_hidden)).await { + let mut msg_out = Message::new(); + let mut file_response = FileResponse::new(); + file_response.set_dir(fd); + msg_out.set_file_response(file_response); + send_raw(msg_out, tx); + } +} + +async fn handle_result( + res: std::result::Result, S>, + id: i32, + file_num: i32, + tx: &UnboundedSender, +) { + match res { + Err(err) => { + send_raw(fs::new_error(id, err, file_num), tx); + } + Ok(Err(err)) => { + send_raw(fs::new_error(id, err, file_num), tx); + } + Ok(Ok(())) => { + send_raw(fs::new_done(id, file_num), tx); + } + } +} + +async fn remove_file(path: String, id: i32, file_num: i32, tx: &UnboundedSender) { + handle_result( + spawn_blocking(move || fs::remove_file(&path)).await, + id, + file_num, + tx, + ) + .await; +} + +async fn create_dir(path: String, id: i32, tx: &UnboundedSender) { + handle_result( + spawn_blocking(move || fs::create_dir(&path)).await, + id, + 0, + tx, + ) + .await; +} + +async fn remove_dir(path: String, id: i32, recursive: bool, tx: &UnboundedSender) { + let path = fs::get_path(&path); + handle_result( + spawn_blocking(move || { + if recursive { + fs::remove_all_empty_dir(&path) + } else { + std::fs::remove_dir(&path).map_err(|err| err.into()) + } + }) + .await, + id, + 0, + tx, + ) + .await; +} + +fn send_raw(msg: Message, tx: &UnboundedSender) { + match msg.write_to_bytes() { + Ok(bytes) => { + allow_err!(tx.send(Data::RawMessage(bytes))); + } + err => allow_err!(err), + } +} + +#[cfg(windows)] +#[tokio::main(flavor = "current_thread")] +pub async fn start_clipboard_file( + cm: ConnectionManager, + mut rx: mpsc::UnboundedReceiver, +) { + let mut cliprdr_context = None; + let mut rx_clip_client = get_rx_clip_client().lock().await; + + loop { + tokio::select! { + clip_file = rx_clip_client.recv() => match clip_file { + Some((conn_id, clip)) => { + cmd_inner_send( + &cm, + conn_id, + Data::ClipbaordFile(clip) + ); + } + None => { + // + } + }, + server_msg = rx.recv() => match server_msg { + Some(ClipboardFileData::Clip((conn_id, clip))) => { + if let Some(ctx) = cliprdr_context.as_mut() { + server_clip_file(ctx, conn_id, clip); + } + } + Some(ClipboardFileData::Enable((id, enabled))) => { + if enabled && cliprdr_context.is_none() { + cliprdr_context = Some(match create_cliprdr_context(true, false) { + Ok(context) => { + log::info!("clipboard context for file transfer created."); + context + } + Err(err) => { + log::error!( + "Create clipboard context for file transfer: {}", + err.to_string() + ); + return; + } + }); + } + set_conn_enabled(id, enabled); + if !enabled { + if let Some(ctx) = cliprdr_context.as_mut() { + empty_clipboard(ctx, id); + } + } + } + None => { + break + } + } + } + } +} + +#[cfg(windows)] +fn cmd_inner_send(cm: &ConnectionManager, id: i32, data: Data) { + let lock = cm.read().unwrap(); + if id != 0 { + if let Some(s) = lock.senders.get(&id) { + allow_err!(s.send(data)); + } + } else { + for s in lock.senders.values() { + allow_err!(s.send(data.clone())); + } + } +} diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index f117aae6d..9fca2dfbb 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -27,7 +27,7 @@ pub static IS_IN: AtomicBool = AtomicBool::new(false); static KEYBOARD_HOOKED: AtomicBool = AtomicBool::new(false); #[derive(Clone, Default)] -pub struct Session { +pub struct Session { pub cmd: String, pub id: String, pub password: String, @@ -38,7 +38,7 @@ pub struct Session { pub ui_handler: T, } -impl Session { +impl Session { pub fn get_view_style(&self) -> String { self.lc.read().unwrap().view_style.clone() } @@ -135,11 +135,6 @@ impl Session { self.send(Data::Message(msg)); } - #[cfg(not(any(target_os = "android", target_os = "ios")))] - pub fn t(&self, name: String) -> String { - crate::client::translate(name) - } - pub fn get_audit_server(&self) -> String { if self.lc.read().unwrap().conn_id <= 0 || LocalConfig::get_option("access_token").is_empty() @@ -327,11 +322,6 @@ impl Session { return "".to_owned(); } - #[cfg(not(any(target_os = "android", target_os = "ios")))] - pub fn get_icon(&self) -> String { - crate::get_icon() - } - pub fn send_chat(&self, text: String) { let mut misc = Misc::new(); misc.set_chat_message(ChatMessage { @@ -541,7 +531,7 @@ impl Session { } } -pub trait InvokeUi: Send + Sync + Clone + 'static + Sized + Default { +pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { fn set_cursor_data(&self, cd: CursorData); fn set_cursor_id(&self, id: String); fn set_cursor_position(&self, cp: CursorPosition); @@ -578,7 +568,7 @@ pub trait InvokeUi: Send + Sync + Clone + 'static + Sized + Default { fn clipboard(&self, content: String); } -impl Deref for Session { +impl Deref for Session { type Target = T; fn deref(&self) -> &Self::Target { @@ -586,16 +576,16 @@ impl Deref for Session { } } -impl DerefMut for Session { +impl DerefMut for Session { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.ui_handler } } -impl FileManager for Session {} +impl FileManager for Session {} #[async_trait] -impl Interface for Session { +impl Interface for Session { fn send(&self, data: Data) { if let Some(sender) = self.sender.read().unwrap().as_ref() { sender.send(data).ok(); @@ -723,7 +713,7 @@ impl Interface for Session { // TODO use event callbcak // sciter only #[cfg(not(any(target_os = "android", target_os = "ios")))] -impl Session { +impl Session { fn start_keyboard_hook(&self) { if self.is_port_forward() || self.is_file_transfer() { return; @@ -958,7 +948,7 @@ impl Session { } #[tokio::main(flavor = "current_thread")] -pub async fn io_loop(handler: Session) { +pub async fn io_loop(handler: Session) { let (sender, mut receiver) = mpsc::unbounded_channel::(); *handler.sender.write().unwrap() = Some(sender.clone()); let mut options = crate::ipc::get_options_async().await; @@ -1074,7 +1064,7 @@ pub async fn io_loop(handler: Session) { } #[cfg(not(any(target_os = "android", target_os = "ios")))] -async fn start_one_port_forward( +async fn start_one_port_forward( handler: Session, port: i32, remote_host: String, From a105aff2aaecbf1214ef9b9c45ab310054627bce Mon Sep 17 00:00:00 2001 From: asur4s Date: Mon, 5 Sep 2022 07:52:38 -0400 Subject: [PATCH 0388/2015] Get key state by read file --- src/client.rs | 12 ------------ src/server/input_service.rs | 4 ++-- src/ui_session_interface.rs | 39 +++++++++++++++++++++++++++++++++++-- 3 files changed, 39 insertions(+), 16 deletions(-) diff --git a/src/client.rs b/src/client.rs index 6cda7f9ad..37cea6b60 100644 --- a/src/client.rs +++ b/src/client.rs @@ -155,18 +155,6 @@ impl Client { } } Ok(x) => { - #[cfg(target_os = "linux")] - if !*crate::common::IS_X11.lock().unwrap() { - let keyboard = super::uinput::client::UInputKeyboard::new().await?; - log::info!("UInput keyboard created"); - let mouse = super::uinput::client::UInputMouse::new().await?; - log::info!("UInput mouse created"); - - let mut en = ENIGO.lock().unwrap(); - en.set_uinput_keyboard(Some(Box::new(keyboard))); - en.set_uinput_mouse(Some(Box::new(mouse))); - } - Ok(x)}, } } diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 19be48b8e..c2efb9947 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -665,7 +665,7 @@ fn map_keyboard_mode(evt: &KeyEvent) { en.key_down(enigo::Key::CapsLock); if evt.down { - en.key_down(enigo::Key::Raw(code)); + en.key_down(enigo::Key::Raw(code)).ok(); } else { en.key_up(enigo::Key::Raw(code)); } @@ -871,7 +871,7 @@ fn legacy_keyboard_mode(evt: &KeyEvent) { if *IS_X11.lock().unwrap() { tfc_key_down_or_up(key.clone(), true, false); } else { - allow_err!(en.key_down(key.clone())); + en.key_down(key.clone()).ok(); } KEYS_DOWN .lock() diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 676933ccc..254db238a 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -8,7 +8,7 @@ use crate::client::{ }; use crate::{client::Data, client::Interface}; use async_trait::async_trait; - +use std::io::Read; use hbb_common::config::{Config, LocalConfig, PeerConfig}; use hbb_common::rendezvous_proto::ConnType; use hbb_common::tokio::{self, sync::mpsc}; @@ -416,10 +416,20 @@ impl Session { { key_event.modifiers.push(ControlKey::Meta.into()); } - if get_key_state(enigo::Key::CapsLock) { + #[cfg(target_os = "linux")] + if get_led_state(enigo::Key::CapsLock){ key_event.modifiers.push(ControlKey::CapsLock.into()); } + #[cfg(not(target_os = "linux"))] + if get_key_state(enigo::Key::CapsLock) { + key_event.modifiers.push(ControlKey::CapsLock.into()); + } if self.peer_platform() != "Mac OS" { + #[cfg(target_os = "linux")] + if get_led_state(enigo::Key::NumLock){ + key_event.modifiers.push(ControlKey::NumLock.into()); + } + #[cfg(not(target_os = "linux"))] if get_key_state(enigo::Key::NumLock) { key_event.modifiers.push(ControlKey::NumLock.into()); } @@ -1362,3 +1372,28 @@ async fn send_note(url: String, id: String, conn_id: i32, note: String) { let body = serde_json::json!({ "id": id, "Id": conn_id, "note": note }); allow_err!(crate::post_request(url, body.to_string(), "").await); } + +fn get_led_state(key: enigo::Key) -> bool{ + let led_file = match key{ + enigo::Key::CapsLock => { + "/sys/class/leds/input1::capslock/brightness" + } + enigo::Key::NumLock => { + "/sys/class/leds/input1::numlock/brightness" + } + _ => { + return false; + } + }; + + let status = if let Ok(mut file) = std::fs::File::open(&led_file) { + let mut content = String::new(); + file.read_to_string(&mut content).ok(); + let status = content.trim_end().to_string().parse::().unwrap_or(0); + status + }else{ + 0 + }; + log::info!("get led state: {}", status); + status == 1 +} \ No newline at end of file From bd733bc108110bf4d0db829591e515d76a04b67b Mon Sep 17 00:00:00 2001 From: csf Date: Mon, 5 Sep 2022 20:05:23 +0800 Subject: [PATCH 0389/2015] mobile build --- src/client/file_trait.rs | 4 ++-- src/flutter.rs | 14 ++++++++++++-- src/flutter_ffi.rs | 4 ++-- src/ui_cm_interface.rs | 16 ++++++++++------ src/ui_interface.rs | 7 ++++--- 5 files changed, 30 insertions(+), 15 deletions(-) diff --git a/src/client/file_trait.rs b/src/client/file_trait.rs index d2f7b1648..b94177c51 100644 --- a/src/client/file_trait.rs +++ b/src/client/file_trait.rs @@ -22,9 +22,9 @@ pub trait FileManager: Interface { #[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))] fn read_dir(&self, path: &str, include_hidden: bool) -> String { - use crate::common::make_fd_to_json; + use crate::flutter::make_fd_to_json; match fs::read_dir(&fs::get_path(path), include_hidden) { - Ok(fd) => make_fd_to_json(fd), + Ok(fd) => make_fd_to_json(fd.id, fd.path, &fd.entries), Err(_) => "".into(), } } diff --git a/src/flutter.rs b/src/flutter.rs index 53b79949a..a3c7ea70f 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -340,6 +340,7 @@ pub fn session_start_(id: &str, event_stream: StreamSink) -> ResultTy pub mod connection_manager { use std::collections::HashMap; + use hbb_common::log; #[cfg(any(target_os = "android"))] use scrap::android::call_main_service_set_by_name; @@ -410,9 +411,18 @@ pub mod connection_manager { } #[cfg(target_os = "android")] - pub fn start_channel(rx: UnboundedReceiver, tx: UnboundedSender) { + use hbb_common::tokio::sync::mpsc::{UnboundedReceiver,UnboundedSender}; + + #[cfg(target_os = "android")] + pub fn start_channel( + rx: UnboundedReceiver, + tx: UnboundedSender, + ) { use crate::ui_cm_interface::start_listen; - std::thread::spawn(move || start_listen(rx, tx)); + let cm = crate::ui_cm_interface::ConnectionManager { + ui_handler: FlutterHandler {}, + }; + std::thread::spawn(move || start_listen(cm, rx, tx)); } } diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 2a41264f0..5da94c3c1 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -754,7 +754,7 @@ pub fn main_set_home_dir(home: String) { pub fn main_stop_service() { #[cfg(target_os = "android")] { - Config::set_option("stop-service".into(), "Y".into()); + config::Config::set_option("stop-service".into(), "Y".into()); crate::rendezvous_mediator::RendezvousMediator::restart(); } } @@ -762,7 +762,7 @@ pub fn main_stop_service() { pub fn main_start_service() { #[cfg(target_os = "android")] { - Config::set_option("stop-service".into(), "".into()); + config::Config::set_option("stop-service".into(), "".into()); crate::rendezvous_mediator::RendezvousMediator::restart(); } #[cfg(not(target_os = "android"))] diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index 8a26a9558..b9045532b 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -114,13 +114,17 @@ impl ConnectionManager { CLIENTS.write().unwrap().remove(&id); #[cfg(any(target_os = "android"))] - if clients + if CLIENTS + .read() + .unwrap() .iter() .filter(|(_k, v)| !v.is_file_transfer) .next() .is_none() { - if let Err(e) = call_main_service_set_by_name("stop_capture", None, None) { + if let Err(e) = + scrap::android::call_main_service_set_by_name("stop_capture", None, None) + { log::debug!("stop_capture err:{}", e); } } @@ -312,7 +316,7 @@ pub async fn start_ipc(cm: ConnectionManager) { #[cfg(target_os = "android")] #[tokio::main(flavor = "current_thread")] -pub async fn start_listen(mut rx: UnboundedReceiver, tx: UnboundedSender) { +pub async fn start_listen(cm: ConnectionManager, mut rx: mpsc::UnboundedReceiver, tx: mpsc::UnboundedSender) { let mut current_id = 0; let mut write_jobs: Vec = Vec::new(); loop { @@ -332,7 +336,7 @@ pub async fn start_listen(mut rx: UnboundedReceiver, tx: UnboundedSender { current_id = id; - on_login( + cm.add_connection( id, is_file_transfer, port_forward, @@ -348,7 +352,7 @@ pub async fn start_listen(mut rx: UnboundedReceiver, tx: UnboundedSender { - cm.new_message(conn_id, text); + cm.new_message(current_id, text); } Some(Data::FS(fs)) => { handle_fs(fs, &mut write_jobs, &tx).await; @@ -362,7 +366,7 @@ pub async fn start_listen(mut rx: UnboundedReceiver, tx: UnboundedSender {} } } - remove_connection(current_id); + cm.remove_connection(current_id); } async fn handle_fs(fs: ipc::FS, write_jobs: &mut Vec, tx: &UnboundedSender) { diff --git a/src/ui_interface.rs b/src/ui_interface.rs index a8e3be980..b8d59ac8f 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -372,10 +372,11 @@ pub fn get_mouse_time() -> f64 { return res; } -#[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn check_mouse_time() { - let sender = SENDER.lock().unwrap(); - allow_err!(sender.send(ipc::Data::MouseMoveTime(0))); + #[cfg(not(any(target_os = "android", target_os = "ios")))]{ + let sender = SENDER.lock().unwrap(); + allow_err!(sender.send(ipc::Data::MouseMoveTime(0))); + } } pub fn get_connect_status() -> Status { From 72d357e14b305e9186eb1a6c371a6899b50b9d22 Mon Sep 17 00:00:00 2001 From: asur4s Date: Mon, 5 Sep 2022 08:07:13 -0400 Subject: [PATCH 0390/2015] Refactor get led state --- libs/enigo/src/linux/nix_impl.rs | 27 ++++++++++++++++++++++- src/ui_session_interface.rs | 37 +------------------------------- 2 files changed, 27 insertions(+), 37 deletions(-) diff --git a/libs/enigo/src/linux/nix_impl.rs b/libs/enigo/src/linux/nix_impl.rs index 2e3fb8a24..895479481 100644 --- a/libs/enigo/src/linux/nix_impl.rs +++ b/libs/enigo/src/linux/nix_impl.rs @@ -1,5 +1,6 @@ use super::{xdo::EnigoXdo}; use crate::{Key, KeyboardControllable, MouseButton, MouseControllable}; +use std::io::Read; /// The main struct for handling the event emitting // #[derive(Default)] @@ -111,6 +112,30 @@ impl MouseControllable for Enigo { } } +fn get_led_state(key: Key) -> bool{ + let led_file = match key{ + Key::CapsLock => { + "/sys/class/leds/input1::capslock/brightness" + } + Key::NumLock => { + "/sys/class/leds/input1::numlock/brightness" + } + _ => { + return false; + } + }; + + let status = if let Ok(mut file) = std::fs::File::open(&led_file) { + let mut content = String::new(); + file.read_to_string(&mut content).ok(); + let status = content.trim_end().to_string().parse::().unwrap_or(0); + status + }else{ + 0 + }; + status == 1 +} + impl KeyboardControllable for Enigo { fn get_key_state(&mut self, key: Key) -> bool { if self.is_x11 { @@ -119,7 +144,7 @@ impl KeyboardControllable for Enigo { if let Some(keyboard) = &mut self.uinput_keyboard { keyboard.get_key_state(key) } else { - false + get_led_state(key) } } } diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 254db238a..d0142333d 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -330,7 +330,6 @@ impl Session { if get_key_state(enigo::Key::NumLock) { key_event.modifiers.push(ControlKey::NumLock.into()); } - log::info!("{:?}", get_key_state(enigo::Key::NumLock)); self.send_key_event(key_event, KeyboardMode::Map); } @@ -416,20 +415,11 @@ impl Session { { key_event.modifiers.push(ControlKey::Meta.into()); } - #[cfg(target_os = "linux")] - if get_led_state(enigo::Key::CapsLock){ - key_event.modifiers.push(ControlKey::CapsLock.into()); - } - #[cfg(not(target_os = "linux"))] + if get_key_state(enigo::Key::CapsLock) { key_event.modifiers.push(ControlKey::CapsLock.into()); } if self.peer_platform() != "Mac OS" { - #[cfg(target_os = "linux")] - if get_led_state(enigo::Key::NumLock){ - key_event.modifiers.push(ControlKey::NumLock.into()); - } - #[cfg(not(target_os = "linux"))] if get_key_state(enigo::Key::NumLock) { key_event.modifiers.push(ControlKey::NumLock.into()); } @@ -1371,29 +1361,4 @@ async fn start_one_port_forward( async fn send_note(url: String, id: String, conn_id: i32, note: String) { let body = serde_json::json!({ "id": id, "Id": conn_id, "note": note }); allow_err!(crate::post_request(url, body.to_string(), "").await); -} - -fn get_led_state(key: enigo::Key) -> bool{ - let led_file = match key{ - enigo::Key::CapsLock => { - "/sys/class/leds/input1::capslock/brightness" - } - enigo::Key::NumLock => { - "/sys/class/leds/input1::numlock/brightness" - } - _ => { - return false; - } - }; - - let status = if let Ok(mut file) = std::fs::File::open(&led_file) { - let mut content = String::new(); - file.read_to_string(&mut content).ok(); - let status = content.trim_end().to_string().parse::().unwrap_or(0); - status - }else{ - 0 - }; - log::info!("get led state: {}", status); - status == 1 } \ No newline at end of file From 948580b2882b43e937ab32b3746b0f8c46c16d34 Mon Sep 17 00:00:00 2001 From: csf Date: Mon, 5 Sep 2022 05:32:21 -0700 Subject: [PATCH 0391/2015] Windows build --- src/client/io_loop.rs | 44 +++++++++++++++++++++------- src/flutter.rs | 9 ++---- src/ui/remote.rs | 8 +---- src/ui_cm_interface.rs | 58 +++++++++++++++++++------------------ src/ui_session_interface.rs | 3 ++ 5 files changed, 71 insertions(+), 51 deletions(-) diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 54c3be26e..552fea7a8 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -43,7 +43,7 @@ pub struct Remote { last_update_jobs_status: (Instant, HashMap), first_frame: bool, #[cfg(windows)] - clipboard_file_context: Option>, + clipboard_file_context: Option>, data_count: Arc, frame_count: Arc, video_format: CodecFormat, @@ -107,7 +107,7 @@ impl Remote { #[cfg(not(windows))] let (_tx_holder, mut rx_clip_client) = mpsc::unbounded_channel::(); #[cfg(windows)] - let mut rx_clip_client = get_rx_clip_client().lock().await; + let mut rx_clip_client = clipboard::get_rx_clip_client().lock().await; let mut status_timer = time::interval(Duration::new(1, 0)); @@ -153,7 +153,7 @@ impl Remote { #[cfg(windows)] match _msg { Some((_, clip)) => { - allow_err!(peer.send(&clip_2_msg(clip)).await); + allow_err!(peer.send(&crate::clipboard_file::clip_2_msg(clip)).await); } None => { // unreachable!() @@ -357,7 +357,13 @@ impl Remote { to, job.files().len() ); - self.handler.update_folder_files(job.id(), job.files(), path,!is_remote, true); + self.handler.update_folder_files( + job.id(), + job.files(), + path, + !is_remote, + true, + ); #[cfg(not(windows))] let files = job.files().clone(); #[cfg(windows)] @@ -416,7 +422,13 @@ impl Remote { to, job.files().len() ); - self.handler.update_folder_files(job.id(), job.files(), path,!is_remote, true); + self.handler.update_folder_files( + job.id(), + job.files(), + path, + !is_remote, + true, + ); job.is_last_job = true; self.read_jobs.push(job); self.timer = time::interval(MILLI1); @@ -528,7 +540,13 @@ impl Remote { } else { match fs::get_recursive_files(&path, include_hidden) { Ok(entries) => { - self.handler.update_folder_files(id, &entries, path.clone(),!is_remote, false); + self.handler.update_folder_files( + id, + &entries, + path.clone(), + !is_remote, + false, + ); self.remove_jobs .insert(id, RemoveJob::new(entries, path, sep, is_remote)); } @@ -785,8 +803,8 @@ impl Remote { Some(message::Union::Cliprdr(clip)) => { if !self.handler.lc.read().unwrap().disable_clipboard { if let Some(context) = &mut self.clipboard_file_context { - if let Some(clip) = msg_2_clip(clip) { - server_clip_file(context, 0, clip); + if let Some(clip) = crate::clipboard_file::msg_2_clip(clip) { + clipboard::server_clip_file(context, 0, clip); } } } @@ -804,7 +822,13 @@ impl Remote { fs::transform_windows_path(&mut entries); } } - self.handler.update_folder_files(fd.id, &entries, fd.path, false, fd.id > 0); + self.handler.update_folder_files( + fd.id, + &entries, + fd.path, + false, + fd.id > 0, + ); if let Some(job) = fs::get_job(fd.id, &mut self.write_jobs) { log::info!("job set_files: {:?}", entries); job.set_files(entries); @@ -1132,7 +1156,7 @@ impl Remote { && self.handler.lc.read().unwrap().enable_file_transfer; if enabled == self.clipboard_file_context.is_none() { self.clipboard_file_context = if enabled { - match create_clipboard_file_context(true, false) { + match clipboard::create_cliprdr_context(true, false) { Ok(context) => { log::info!("clipboard context for file transfer created."); Some(context) diff --git a/src/flutter.rs b/src/flutter.rs index a3c7ea70f..1c4ed8869 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -396,13 +396,10 @@ pub mod connection_manager { #[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn start_listen_ipc_thread() { - use crate::{ - ipc::start_pa, - ui_cm_interface::{start_ipc, ConnectionManager}, - }; + use crate::ui_cm_interface::{start_ipc, ConnectionManager}; #[cfg(target_os = "linux")] - std::thread::spawn(start_pa); + std::thread::spawn(crate::ipc::start_pa); let cm = ConnectionManager { ui_handler: FlutterHandler {}, @@ -411,7 +408,7 @@ pub mod connection_manager { } #[cfg(target_os = "android")] - use hbb_common::tokio::sync::mpsc::{UnboundedReceiver,UnboundedSender}; + use hbb_common::tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; #[cfg(target_os = "android")] pub fn start_channel( diff --git a/src/ui/remote.rs b/src/ui/remote.rs index f6b3acec6..08430110c 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -1,10 +1,7 @@ use std::{ collections::HashMap, ops::{Deref, DerefMut}, - sync::{ - atomic::Ordering, - Arc, Mutex, - }, + sync::{atomic::Ordering, Arc, Mutex}, }; use sciter::{ @@ -40,9 +37,6 @@ lazy_static::lazy_static! { static ref VIDEO: Arc>> = Default::default(); } -#[cfg(windows)] -static mut IS_ALT_GR: bool = false; - /// SciterHandler /// * element /// * close_state for file path when close diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index b9045532b..d416fdd63 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -268,19 +268,18 @@ pub async fn start_ipc(cm: ConnectionManager) { Data::FS(fs) => { handle_fs(fs, &mut write_jobs, &tx).await; } - // TODO ClipbaordFile - // #[cfg(windows)] - // Data::ClipbaordFile(_clip) => { - // tx_file - // .send(ClipboardFileData::Clip((id, _clip))) - // .ok(); - // } - // #[cfg(windows)] - // Data::ClipboardFileEnabled(enabled) => { - // tx_file - // .send(ClipboardFileData::Enable((id, enabled))) - // .ok(); - // } + #[cfg(windows)] + Data::ClipbaordFile(_clip) => { + tx_file + .send(ClipboardFileData::Clip((conn_id, _clip))) + .ok(); + } + #[cfg(windows)] + Data::ClipboardFileEnabled(enabled) => { + tx_file + .send(ClipboardFileData::Enable((conn_id, enabled))) + .ok(); + } _ => { } @@ -316,7 +315,11 @@ pub async fn start_ipc(cm: ConnectionManager) { #[cfg(target_os = "android")] #[tokio::main(flavor = "current_thread")] -pub async fn start_listen(cm: ConnectionManager, mut rx: mpsc::UnboundedReceiver, tx: mpsc::UnboundedSender) { +pub async fn start_listen( + cm: ConnectionManager, + mut rx: mpsc::UnboundedReceiver, + tx: mpsc::UnboundedSender, +) { let mut current_id = 0; let mut write_jobs: Vec = Vec::new(); loop { @@ -596,19 +599,18 @@ fn send_raw(msg: Message, tx: &UnboundedSender) { #[cfg(windows)] #[tokio::main(flavor = "current_thread")] -pub async fn start_clipboard_file( - cm: ConnectionManager, +pub async fn start_clipboard_file( + cm: ConnectionManager, mut rx: mpsc::UnboundedReceiver, ) { let mut cliprdr_context = None; - let mut rx_clip_client = get_rx_clip_client().lock().await; + let mut rx_clip_client = clipboard::get_rx_clip_client().lock().await; loop { tokio::select! { clip_file = rx_clip_client.recv() => match clip_file { Some((conn_id, clip)) => { cmd_inner_send( - &cm, conn_id, Data::ClipbaordFile(clip) ); @@ -620,12 +622,12 @@ pub async fn start_clipboard_file( server_msg = rx.recv() => match server_msg { Some(ClipboardFileData::Clip((conn_id, clip))) => { if let Some(ctx) = cliprdr_context.as_mut() { - server_clip_file(ctx, conn_id, clip); + clipboard::server_clip_file(ctx, conn_id, clip); } } Some(ClipboardFileData::Enable((id, enabled))) => { if enabled && cliprdr_context.is_none() { - cliprdr_context = Some(match create_cliprdr_context(true, false) { + cliprdr_context = Some(match clipboard::create_cliprdr_context(true, false) { Ok(context) => { log::info!("clipboard context for file transfer created."); context @@ -639,10 +641,10 @@ pub async fn start_clipboard_file( } }); } - set_conn_enabled(id, enabled); + clipboard::set_conn_enabled(id, enabled); if !enabled { if let Some(ctx) = cliprdr_context.as_mut() { - empty_clipboard(ctx, id); + clipboard::empty_clipboard(ctx, id); } } } @@ -655,15 +657,15 @@ pub async fn start_clipboard_file( } #[cfg(windows)] -fn cmd_inner_send(cm: &ConnectionManager, id: i32, data: Data) { - let lock = cm.read().unwrap(); +fn cmd_inner_send(id: i32, data: Data) { + let lock = CLIENTS.read().unwrap(); if id != 0 { - if let Some(s) = lock.senders.get(&id) { - allow_err!(s.send(data)); + if let Some(s) = lock.get(&id) { + allow_err!(s.tx.send(data)); } } else { - for s in lock.senders.values() { - allow_err!(s.send(data.clone())); + for s in lock.values() { + allow_err!(s.tx.send(data.clone())); } } } diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 9fca2dfbb..717963561 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -26,6 +26,9 @@ use std::sync::{Arc, Mutex, RwLock}; pub static IS_IN: AtomicBool = AtomicBool::new(false); static KEYBOARD_HOOKED: AtomicBool = AtomicBool::new(false); +#[cfg(windows)] +static mut IS_ALT_GR: bool = false; + #[derive(Clone, Default)] pub struct Session { pub cmd: String, From 3d7377f9b6322b6da9f78ab82d60ac35b2db4867 Mon Sep 17 00:00:00 2001 From: asur4s Date: Mon, 5 Sep 2022 10:18:29 -0400 Subject: [PATCH 0392/2015] Opt: Change keyboard mode by ui --- flutter/lib/desktop/pages/remote_page.dart | 15 +++--- .../lib/desktop/widgets/remote_menubar.dart | 46 +++++++++++++++++++ flutter/lib/models/model.dart | 4 ++ src/flutter_ffi.rs | 13 ++++++ 4 files changed, 71 insertions(+), 7 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 8c56f69d2..c2bf4ea5b 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -41,6 +41,7 @@ class _RemotePageState extends State Timer? _timer; bool _showBar = !isWebDesktop; String _value = ''; + String keyboardMode = "legacy"; final _cursorOverImage = false.obs; final FocusNode _mobileFocusNode = FocusNode(); @@ -254,8 +255,11 @@ class _RemotePageState extends State } KeyEventResult handleRawKeyEvent(FocusNode data, RawKeyEvent e) { - String? keyboardMode = Platform.environment['KEYBOARD_MODE']; - keyboardMode ??= 'legacy'; + bind.sessionGetKeyboardName(id: widget.id).then((result) { + setState(() { + keyboardMode = result.toString(); + }); + }); if (keyboardMode == 'map') { mapKeyboardMode(e); @@ -285,7 +289,6 @@ class _RemotePageState extends State RawKeyEventDataLinux newData = e.data as RawKeyEventDataLinux; scanCode = newData.scanCode; keyCode = newData.keyCode; - debugPrint(newData.unicodeScalarValues.toString()); } else { scanCode = -1; keyCode = -1; @@ -537,10 +540,8 @@ class _RemotePageState extends State onPointerUp: _onPointUpImage, onPointerMove: _onPointMoveImage, onPointerSignal: _onPointerSignalImage, - child: MouseRegion( - onEnter: enterView, - onExit: leaveView, - child: child)); + child: + MouseRegion(onEnter: enterView, onExit: leaveView, child: child)); } Widget getBodyForDesktop(BuildContext context, bool keyboard) { diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 47536011d..4d27c107f 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -93,6 +93,7 @@ class _RemoteMenubarState extends State { menubarItems.add(_buildMonitor(context)); menubarItems.add(_buildControl(context)); menubarItems.add(_buildDisplay(context)); + menubarItems.add(_buildKeyboard(context)); if (!isWeb) { menubarItems.add(_buildChat(context)); } @@ -264,6 +265,29 @@ class _RemoteMenubarState extends State { ); } + Widget _buildKeyboard(BuildContext context) { + return mod_menu.PopupMenuButton( + padding: EdgeInsets.zero, + icon: const Icon( + Icons.keyboard, + color: _MenubarTheme.commonColor, + ), + tooltip: translate('Keyboard Settings'), + position: mod_menu.PopupMenuPosition.under, + onSelected: (String item) {}, + itemBuilder: (BuildContext context) => _getKeyboardMenu() + .map((entry) => entry.build( + context, + const MenuConfig( + commonColor: _MenubarTheme.commonColor, + height: _MenubarTheme.height, + dividerHeight: _MenubarTheme.dividerHeight, + ))) + .expand((i) => i) + .toList(), + ); + } + Widget _buildClose(BuildContext context) { return IconButton( tooltip: translate('Close'), @@ -555,6 +579,28 @@ class _RemoteMenubarState extends State { return displayMenu; } + List> _getKeyboardMenu() { + final keyboardMenu = [ + MenuEntryRadios( + text: translate('Ratio'), + optionsGetter: () => [ + MenuEntryRadioOption( + text: translate('Legacy mode'), value: 'legacy'), + MenuEntryRadioOption(text: translate('Map mode'), value: 'map'), + ], + curOptionGetter: () async { + return await bind.sessionGetKeyboardName(id: widget.id) ?? 'legacy'; + }, + optionSetter: (String oldValue, String newValue) async { + await bind.sessionSetKeyboardMode( + id: widget.id, keyboardMode: newValue); + widget.ffi.canvasModel.updateViewStyle(); + }) + ]; + + return keyboardMenu; + } + MenuEntrySwitch _createSwitchMenuEntry(String text, String option) { return MenuEntrySwitch( text: translate(text), diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index bfa6f5297..b1cc6192b 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -1003,6 +1003,10 @@ class FFI { downOrUp: down); } + Future getKeyboardMode(){ + return bind.sessionGetKeyboardName(id: id); + } + void enterOrLeave(bool enter) { bind.sessionEnterOrLeave(id: id, enter: enter); } diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index e5616bee6..d2d49fff0 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -298,6 +298,19 @@ pub fn session_get_peer_option(id: String, name: String) -> String { "".to_string() } +pub fn session_get_keyboard_name(id: String) -> String { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + return session.get_keyboard_mode(); + } + "legacy".to_string() +} + +pub fn session_set_keyboard_mode(id: String, keyboard_mode: String) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.save_keyboard_mode(keyboard_mode); + } +} + pub fn session_input_os_password(id: String, value: String) { if let Some(session) = SESSIONS.read().unwrap().get(&id) { session.input_os_password(value, true); From 2d7cd7c864fb57f14da68cccfb881597ac34f387 Mon Sep 17 00:00:00 2001 From: asur4s Date: Mon, 5 Sep 2022 11:50:42 -0400 Subject: [PATCH 0393/2015] Refactor: tfc --- Cargo.lock | 3 +- Cargo.toml | 2 - libs/enigo/Cargo.toml | 1 + libs/enigo/src/linux/nix_impl.rs | 135 ++++++++++++++++++++-- src/server/input_service.rs | 185 ++++--------------------------- 5 files changed, 147 insertions(+), 179 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 750ad4e8e..303d18f1e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1381,6 +1381,7 @@ dependencies = [ "rdev", "serde 1.0.144", "serde_derive", + "tfc", "unicode-segmentation", "winapi 0.3.9", ] @@ -4241,7 +4242,6 @@ dependencies = [ "mouce", "num_cpus", "objc", - "once_cell", "parity-tokio-ipc", "rdev", "repng", @@ -4261,7 +4261,6 @@ dependencies = [ "sys-locale", "sysinfo", "system_shutdown", - "tfc", "tray-item", "trayicon", "uuid", diff --git a/Cargo.toml b/Cargo.toml index 708e6660b..bf5ab9669 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -76,8 +76,6 @@ sys-locale = "0.2" enigo = { path = "libs/enigo", features = [ "with_serde" ] } clipboard = { path = "libs/clipboard" } rdev = { git = "https://github.com/asur4s/rdev" } -tfc = { git = "https://github.com/asur4s/The-Fat-Controller" } -once_cell = "1.13.0" ctrlc = "3.2" arboard = "2.0" #minreq = { version = "2.4", features = ["punycode", "https-native"] } diff --git a/libs/enigo/Cargo.toml b/libs/enigo/Cargo.toml index fec03d34e..83c79e064 100644 --- a/libs/enigo/Cargo.toml +++ b/libs/enigo/Cargo.toml @@ -23,6 +23,7 @@ serde = { version = "1.0", optional = true } serde_derive = { version = "1.0", optional = true } log = "0.4" rdev = { git = "https://github.com/asur4s/rdev" } +tfc = { git = "https://github.com/asur4s/The-Fat-Controller" } hbb_common = { path = "../hbb_common" } [features] diff --git a/libs/enigo/src/linux/nix_impl.rs b/libs/enigo/src/linux/nix_impl.rs index 895479481..5fb67c5ee 100644 --- a/libs/enigo/src/linux/nix_impl.rs +++ b/libs/enigo/src/linux/nix_impl.rs @@ -1,12 +1,14 @@ -use super::{xdo::EnigoXdo}; +use super::xdo::EnigoXdo; use crate::{Key, KeyboardControllable, MouseButton, MouseControllable}; use std::io::Read; +use tfc::{traits::*, Context as TFC_Context, Key as TFC_Key}; /// The main struct for handling the event emitting // #[derive(Default)] pub struct Enigo { xdo: EnigoXdo, is_x11: bool, + tfc: TFC_Context, uinput_keyboard: Option>, uinput_mouse: Option>, } @@ -31,12 +33,48 @@ impl Enigo { pub fn set_uinput_mouse(&mut self, uinput_mouse: Option>) { self.uinput_mouse = uinput_mouse } + + fn tfc_key_down_or_up(&mut self, key: Key, down: bool, up: bool) -> bool { + if let Key::Layout(chr) = key { + if down { + if let Err(_) = self.tfc.unicode_char_down(chr) { + return false; + } + } + if up { + if let Err(_) = self.tfc.unicode_char_up(chr) { + return false; + } + } + return true; + } + + let key = match convert_to_tfc_key(key) { + Some(key) => key, + None => { + return false; + } + }; + + if down { + if let Err(_) = self.tfc.key_down(key) { + return false; + } + }; + if up { + if let Err(_) = self.tfc.key_up(key) { + return false; + } + }; + return true; + } } impl Default for Enigo { fn default() -> Self { Self { is_x11: "x11" == hbb_common::platform::linux::get_display_server(), + tfc: TFC_Context::new().expect("kbd context error"), uinput_keyboard: None, uinput_mouse: None, xdo: EnigoXdo::default(), @@ -112,14 +150,10 @@ impl MouseControllable for Enigo { } } -fn get_led_state(key: Key) -> bool{ - let led_file = match key{ - Key::CapsLock => { - "/sys/class/leds/input1::capslock/brightness" - } - Key::NumLock => { - "/sys/class/leds/input1::numlock/brightness" - } +fn get_led_state(key: Key) -> bool { + let led_file = match key { + Key::CapsLock => "/sys/class/leds/input1::capslock/brightness", + Key::NumLock => "/sys/class/leds/input1::numlock/brightness", _ => { return false; } @@ -130,7 +164,7 @@ fn get_led_state(key: Key) -> bool{ file.read_to_string(&mut content).ok(); let status = content.trim_end().to_string().parse::().unwrap_or(0); status - }else{ + } else { 0 }; status == 1 @@ -161,7 +195,12 @@ impl KeyboardControllable for Enigo { fn key_down(&mut self, key: Key) -> crate::ResultType { if self.is_x11 { - self.xdo.key_down(key) + let has_down = self.tfc_key_down_or_up(key, true, false); + if !has_down { + self.xdo.key_down(key) + } else { + Ok(()) + } } else { if let Some(keyboard) = &mut self.uinput_keyboard { keyboard.key_down(key) @@ -172,7 +211,10 @@ impl KeyboardControllable for Enigo { } fn key_up(&mut self, key: Key) { if self.is_x11 { - self.xdo.key_up(key) + let has_down = self.tfc_key_down_or_up(key, false, true); + if !has_down { + self.xdo.key_up(key) + } } else { if let Some(keyboard) = &mut self.uinput_keyboard { keyboard.key_up(key) @@ -184,3 +226,72 @@ impl KeyboardControllable for Enigo { self.key_up(key); } } + +fn convert_to_tfc_key(key: Key) -> Option { + let key = match key { + Key::Alt => TFC_Key::Alt, + Key::Backspace => TFC_Key::DeleteOrBackspace, + Key::CapsLock => TFC_Key::CapsLock, + Key::Control => TFC_Key::Control, + Key::Delete => TFC_Key::ForwardDelete, + Key::DownArrow => TFC_Key::DownArrow, + Key::End => TFC_Key::End, + Key::Escape => TFC_Key::Escape, + Key::F1 => TFC_Key::F1, + Key::F10 => TFC_Key::F10, + Key::F11 => TFC_Key::F11, + Key::F12 => TFC_Key::F12, + Key::F2 => TFC_Key::F2, + Key::F3 => TFC_Key::F3, + Key::F4 => TFC_Key::F4, + Key::F5 => TFC_Key::F5, + Key::F6 => TFC_Key::F6, + Key::F7 => TFC_Key::F7, + Key::F8 => TFC_Key::F8, + Key::F9 => TFC_Key::F9, + Key::Home => TFC_Key::Home, + Key::LeftArrow => TFC_Key::LeftArrow, + Key::PageDown => TFC_Key::PageDown, + Key::PageUp => TFC_Key::PageUp, + Key::Return => TFC_Key::ReturnOrEnter, + Key::RightArrow => TFC_Key::RightArrow, + Key::Shift => TFC_Key::Shift, + Key::Space => TFC_Key::Space, + Key::Tab => TFC_Key::Tab, + Key::UpArrow => TFC_Key::UpArrow, + Key::Numpad0 => TFC_Key::N0, + Key::Numpad1 => TFC_Key::N1, + Key::Numpad2 => TFC_Key::N2, + Key::Numpad3 => TFC_Key::N3, + Key::Numpad4 => TFC_Key::N4, + Key::Numpad5 => TFC_Key::N5, + Key::Numpad6 => TFC_Key::N6, + Key::Numpad7 => TFC_Key::N7, + Key::Numpad8 => TFC_Key::N8, + Key::Numpad9 => TFC_Key::N9, + Key::Decimal => TFC_Key::NumpadDecimal, + Key::Clear => TFC_Key::NumpadClear, + Key::Pause => TFC_Key::PlayPause, + Key::Print => TFC_Key::Print, + Key::Snapshot => TFC_Key::PrintScreen, + Key::Insert => TFC_Key::Insert, + Key::Scroll => TFC_Key::ScrollLock, + Key::NumLock => TFC_Key::NumLock, + Key::RWin => TFC_Key::Meta, + Key::Apps => TFC_Key::Apps, + Key::Multiply => TFC_Key::NumpadMultiply, + Key::Add => TFC_Key::NumpadPlus, + Key::Subtract => TFC_Key::NumpadMinus, + Key::Divide => TFC_Key::NumpadDivide, + Key::Equals => TFC_Key::NumpadEquals, + Key::NumpadEnter => TFC_Key::NumpadEnter, + Key::RightShift => TFC_Key::RightShift, + Key::RightControl => TFC_Key::RightControl, + Key::RightAlt => TFC_Key::RightAlt, + Key::Command | Key::Super | Key::Windows | Key::Meta => TFC_Key::Meta, + _ => { + return None; + } + }; + Some(key) +} diff --git a/src/server/input_service.rs b/src/server/input_service.rs index c2efb9947..70af1e581 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -1,8 +1,8 @@ use super::*; +use crate::common::IS_X11; #[cfg(target_os = "macos")] use dispatch::Queue; use enigo::{Enigo, Key, KeyboardControllable, MouseButton, MouseControllable}; -use crate::common::IS_X11; use hbb_common::{config::COMPRESS_LEVEL, protobuf::EnumOrUnknown}; use rdev::{simulate, EventType, Key as RdevKey}; use std::{ @@ -10,7 +10,6 @@ use std::{ sync::atomic::{AtomicBool, Ordering}, time::Instant, }; -use tfc::{traits::*, Context as TFC_Context, Key as TFC_Key}; #[derive(Default)] struct StateCursor { @@ -172,7 +171,6 @@ lazy_static::lazy_static! { }; static ref KEYS_DOWN: Arc>> = Default::default(); static ref LATEST_INPUT: Arc> = Default::default(); - static ref TFC_CONTEXT: Mutex = Mutex::new(TFC_Context::new().expect("kbd context error")); } static EXITING: AtomicBool = AtomicBool::new(false); @@ -689,101 +687,6 @@ fn map_keyboard_mode(evt: &KeyEvent) { return; } -fn tfc_key_down_or_up(key: Key, down: bool, up: bool) { - if let Key::Layout(chr) = key { - log::info!("tfc_key_down_or_up :{:?}", chr); - if down { - if let Err(_) = TFC_CONTEXT.lock().unwrap().unicode_char_down(chr) { - log::error!("Failed to press char {:?}", chr); - }; - } - if up { - if let Err(_) = TFC_CONTEXT.lock().unwrap().unicode_char_down(chr) { - log::error!("Failed to press char {:?}", chr); - }; - } - return; - } - - let key = match key { - Key::Alt => TFC_Key::Alt, - Key::Backspace => TFC_Key::DeleteOrBackspace, - Key::CapsLock => TFC_Key::CapsLock, - Key::Control => TFC_Key::Control, - Key::Delete => TFC_Key::ForwardDelete, - Key::DownArrow => TFC_Key::DownArrow, - Key::End => TFC_Key::End, - Key::Escape => TFC_Key::Escape, - Key::F1 => TFC_Key::F1, - Key::F10 => TFC_Key::F10, - Key::F11 => TFC_Key::F11, - Key::F12 => TFC_Key::F12, - Key::F2 => TFC_Key::F2, - Key::F3 => TFC_Key::F3, - Key::F4 => TFC_Key::F4, - Key::F5 => TFC_Key::F5, - Key::F6 => TFC_Key::F6, - Key::F7 => TFC_Key::F7, - Key::F8 => TFC_Key::F8, - Key::F9 => TFC_Key::F9, - Key::Home => TFC_Key::Home, - Key::LeftArrow => TFC_Key::LeftArrow, - Key::PageDown => TFC_Key::PageDown, - Key::PageUp => TFC_Key::PageUp, - Key::Return => TFC_Key::ReturnOrEnter, - Key::RightArrow => TFC_Key::RightArrow, - Key::Shift => TFC_Key::Shift, - Key::Space => TFC_Key::Space, - Key::Tab => TFC_Key::Tab, - Key::UpArrow => TFC_Key::UpArrow, - Key::Numpad0 => TFC_Key::N0, - Key::Numpad1 => TFC_Key::N1, - Key::Numpad2 => TFC_Key::N2, - Key::Numpad3 => TFC_Key::N3, - Key::Numpad4 => TFC_Key::N4, - Key::Numpad5 => TFC_Key::N5, - Key::Numpad6 => TFC_Key::N6, - Key::Numpad7 => TFC_Key::N7, - Key::Numpad8 => TFC_Key::N8, - Key::Numpad9 => TFC_Key::N9, - Key::Decimal => TFC_Key::NumpadDecimal, - Key::Clear => TFC_Key::NumpadClear, - Key::Pause => TFC_Key::PlayPause, - Key::Print => TFC_Key::Print, - Key::Snapshot => TFC_Key::PrintScreen, - Key::Insert => TFC_Key::Insert, - Key::Scroll => TFC_Key::ScrollLock, - Key::NumLock => TFC_Key::NumLock, - Key::RWin => TFC_Key::Meta, - Key::Apps => TFC_Key::Apps, - Key::Multiply => TFC_Key::NumpadMultiply, - Key::Add => TFC_Key::NumpadPlus, - Key::Subtract => TFC_Key::NumpadMinus, - Key::Divide => TFC_Key::NumpadDivide, - Key::Equals => TFC_Key::NumpadEquals, - Key::NumpadEnter => TFC_Key::NumpadEnter, - Key::RightShift => TFC_Key::RightShift, - Key::RightControl => TFC_Key::RightControl, - Key::RightAlt => TFC_Key::RightAlt, - Key::Command | Key::Super | Key::Windows | Key::Meta => TFC_Key::Meta, - _ => { - return; - } - }; - - log::info!("tfc_key_down_or_up: {:?}", key); - if down { - if let Err(_) = TFC_CONTEXT.lock().unwrap().key_down(key) { - log::error!("Failed to press char {:?}", key); - }; - } - if up { - if let Err(_) = TFC_CONTEXT.lock().unwrap().key_up(key) { - log::error!("Failed to press char {:?}", key); - }; - } -} - fn legacy_keyboard_mode(evt: &KeyEvent) { let (click_capslock, click_numlock) = sync_status(evt); @@ -791,18 +694,10 @@ fn legacy_keyboard_mode(evt: &KeyEvent) { crate::platform::windows::try_change_desktop(); let mut en = ENIGO.lock().unwrap(); if click_capslock { - if *IS_X11.lock().unwrap() { - tfc_key_down_or_up(Key::CapsLock, true, true); - } else { - en.key_click(Key::CapsLock); - } + en.key_click(Key::CapsLock); } if click_numlock { - if *IS_X11.lock().unwrap() { - tfc_key_down_or_up(Key::NumLock, true, true); - } else { - en.key_click(Key::NumLock); - } + en.key_click(Key::NumLock); } // disable numlock if press home etc when numlock is on, // because we will get numpad value (7,8,9 etc) if not @@ -820,7 +715,7 @@ fn legacy_keyboard_mode(evt: &KeyEvent) { } #[cfg(not(target_os = "macos"))] let mut to_release = Vec::new(); - + if evt.down { let ck = if let Some(key_event::Union::ControlKey(ck)) = evt.union { ck.value() @@ -844,11 +739,7 @@ fn legacy_keyboard_mode(evt: &KeyEvent) { } #[cfg(not(target_os = "macos"))] if !get_modifier_state(key.clone(), &mut en) { - if *IS_X11.lock().unwrap() { - tfc_key_down_or_up(key.clone(), true, false); - } else { - en.key_down(key.clone()).ok(); - } + en.key_down(key.clone()).ok(); modifier_sleep(); to_release.push(key); } @@ -868,21 +759,13 @@ fn legacy_keyboard_mode(evt: &KeyEvent) { } } if evt.down { - if *IS_X11.lock().unwrap() { - tfc_key_down_or_up(key.clone(), true, false); - } else { - en.key_down(key.clone()).ok(); - } + en.key_down(key.clone()).ok(); KEYS_DOWN .lock() .unwrap() .insert(ck.value() as _, Instant::now()); } else { - if *IS_X11.lock().unwrap() { - tfc_key_down_or_up(key.clone(), false, true); - } else { - en.key_up(key.clone()); - } + en.key_up(key.clone()); KEYS_DOWN.lock().unwrap().remove(&(ck.value() as _)); } } else if ck.value() == ControlKey::CtrlAltDel.value() { @@ -896,24 +779,20 @@ fn legacy_keyboard_mode(evt: &KeyEvent) { } Some(key_event::Union::Chr(chr)) => { if evt.down { - if *IS_X11.lock().unwrap() { - tfc_key_down_or_up(get_layout(chr), true, false); + if en.key_down(get_layout(chr)).is_ok() { + KEYS_DOWN + .lock() + .unwrap() + .insert(chr as u64 + KEY_CHAR_START, Instant::now()); } else { - if en.key_down(get_layout(chr)).is_ok() { - KEYS_DOWN - .lock() - .unwrap() - .insert(chr as u64 + KEY_CHAR_START, Instant::now()); - } else { - if let Ok(chr) = char::try_from(chr) { - let mut x = chr.to_string(); - if get_modifier_state(Key::Shift, &mut en) - || get_modifier_state(Key::CapsLock, &mut en) - { - x = x.to_uppercase(); - } - en.key_sequence(&x); + if let Ok(chr) = char::try_from(chr) { + let mut x = chr.to_string(); + if get_modifier_state(Key::Shift, &mut en) + || get_modifier_state(Key::CapsLock, &mut en) + { + x = x.to_uppercase(); } + en.key_sequence(&x); } } KEYS_DOWN @@ -921,11 +800,7 @@ fn legacy_keyboard_mode(evt: &KeyEvent) { .unwrap() .insert(chr as u64 + KEY_CHAR_START, Instant::now()); } else { - if *IS_X11.lock().unwrap() { - tfc_key_down_or_up(get_layout(chr), false, true); - } else { - en.key_up(get_layout(chr)); - } + en.key_up(get_layout(chr)); KEYS_DOWN .lock() .unwrap() @@ -944,23 +819,7 @@ fn legacy_keyboard_mode(evt: &KeyEvent) { } #[cfg(not(target_os = "macos"))] for key in to_release { - if *IS_X11.lock().unwrap() { - tfc_key_down_or_up(key.clone(), false, true); - } else { - en.key_up(key.clone()); - } - } -} - -fn translate_keyboard_mode(evt: &KeyEvent) { - let chr = char::from_u32(evt.chr()).unwrap_or_default(); - // down(true)->press && press(false)-> release - if evt.down && !evt.press { - TFC_CONTEXT - .lock() - .unwrap() - .unicode_char(chr) - .expect("unicode_char_down error"); + en.key_up(key.clone()); } } @@ -977,7 +836,7 @@ fn handle_key_(evt: &KeyEvent) { map_keyboard_mode(evt); } KeyboardMode::Translate => { - translate_keyboard_mode(evt); + legacy_keyboard_mode(evt); } _ => { legacy_keyboard_mode(evt); From b6e0cc8e74188dc2ccc084adbe2bc49401518abb Mon Sep 17 00:00:00 2001 From: asur4s Date: Mon, 5 Sep 2022 12:39:11 -0400 Subject: [PATCH 0394/2015] Fix warning --- libs/hbb_common/src/platform/linux.rs | 1 - src/client/io_loop.rs | 1 + src/flutter.rs | 10 +++++++--- src/flutter_ffi.rs | 8 ++++---- src/rendezvous_mediator.rs | 3 ++- src/ui.rs | 1 + src/ui/remote.rs | 4 ++-- src/ui_interface.rs | 3 ++- src/ui_session_interface.rs | 3 +-- 9 files changed, 20 insertions(+), 14 deletions(-) diff --git a/libs/hbb_common/src/platform/linux.rs b/libs/hbb_common/src/platform/linux.rs index d4d44270a..865033204 100644 --- a/libs/hbb_common/src/platform/linux.rs +++ b/libs/hbb_common/src/platform/linux.rs @@ -1,5 +1,4 @@ use crate::ResultType; -use std::sync::Mutex; pub fn get_display_server() -> String { let session = get_value_of_seat0(0); diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index f1a481a36..d33e7b5ef 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -276,6 +276,7 @@ impl Remote { } // TODO + #[allow(dead_code)] fn load_last_jobs(&mut self) { log::info!("start load last jobs"); self.handler.clear_all_jobs(); diff --git a/src/flutter.rs b/src/flutter.rs index 7316dd2ed..ed42fbedb 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -12,7 +12,9 @@ use crate::ui_session_interface::{io_loop, InvokeUi, Session}; use crate::{client::*, flutter_ffi::EventToUI}; pub(super) const APP_TYPE_MAIN: &str = "main"; +#[allow(dead_code)] pub(super) const APP_TYPE_DESKTOP_REMOTE: &str = "remote"; +#[allow(dead_code)] pub(super) const APP_TYPE_DESKTOP_FILE_TRANSFER: &str = "file transfer"; lazy_static::lazy_static! { @@ -117,7 +119,7 @@ impl InvokeUi for FlutterHandler { ); } - fn job_error(&self, id: i32, err: String, file_num: i32) { + fn job_error(&self, id: i32, err: String, _file_num: i32) { self.push_event("job_error", vec![("id", &id.to_string()), ("err", &err)]); } @@ -132,6 +134,7 @@ impl InvokeUi for FlutterHandler { // todo!() } + #[allow(unused_variables)] fn add_job( &self, id: i32, @@ -148,6 +151,7 @@ impl InvokeUi for FlutterHandler { // todo!() } + #[allow(unused_variables)] fn confirm_delete_files(&self, id: i32, i: i32, name: String) { // todo!() } @@ -337,7 +341,7 @@ pub mod connection_manager { protobuf::Message as _, tokio::{ self, - sync::mpsc::{self, UnboundedReceiver, UnboundedSender}, + sync::mpsc::{self, UnboundedSender}, task::spawn_blocking, }, }; @@ -432,7 +436,7 @@ pub mod connection_manager { } Ok(Some(data)) => { match data { - Data::Login{id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, file_transfer_enabled, restart} => { + Data::Login{id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, file_transfer_enabled: _file_transfer_enabled, restart} => { log::debug!("conn_id: {}", id); conn_id = id; // tx_file.send(ClipboardFileData::Enable((id, file_transfer_enabled))).ok(); diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index d2d49fff0..be4cadc7e 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -106,9 +106,9 @@ pub fn stop_global_event_stream(app_type: String) { .remove(&app_type); } -pub fn host_stop_system_key_propagate(stopped: bool) { +pub fn host_stop_system_key_propagate(_stopped: bool) { #[cfg(windows)] - crate::platform::windows::stop_system_key_propagate(stopped); + crate::platform::windows::stop_system_key_propagate(_stopped); } // FIXME: -> ResultType<()> cannot be parsed by frb_codegen @@ -388,7 +388,7 @@ pub fn session_create_dir(id: String, act_id: i32, path: String, is_remote: bool } pub fn session_read_local_dir_sync(id: String, path: String, show_hidden: bool) -> String { - if let Some(session) = SESSIONS.read().unwrap().get(&id) { + if let Some(_) = SESSIONS.read().unwrap().get(&id) { if let Ok(fd) = fs::read_dir(&fs::get_path(&path), show_hidden) { return make_fd_to_json(fd); } @@ -404,7 +404,7 @@ pub fn session_get_platform(id: String, is_remote: bool) -> String { } pub fn session_load_last_transfer_jobs(id: String) { - if let Some(session) = SESSIONS.read().unwrap().get(&id) { + if let Some(_) = SESSIONS.read().unwrap().get(&id) { // return session.load_last_jobs(); } else { // a tip for flutter dev diff --git a/src/rendezvous_mediator.rs b/src/rendezvous_mediator.rs index 802fed1de..9fc59816f 100644 --- a/src/rendezvous_mediator.rs +++ b/src/rendezvous_mediator.rs @@ -568,6 +568,7 @@ pub fn get_mac() -> String { "".to_owned() } +#[allow(dead_code)] fn lan_discovery() -> ResultType<()> { let addr = SocketAddr::from(([0, 0, 0, 0], get_broadcast_port())); let socket = std::net::UdpSocket::bind(addr)?; @@ -637,7 +638,7 @@ pub async fn query_online_states, Vec)>(ids: Vec ResultType { - let (mut rendezvous_server, _servers, _contained) = crate::get_rendezvous_server(1_000).await; + let (rendezvous_server, _servers, _contained) = crate::get_rendezvous_server(1_000).await; let tmp: Vec<&str> = rendezvous_server.split(":").collect(); if tmp.len() != 2 { bail!("Invalid server address: {}", rendezvous_server); diff --git a/src/ui.rs b/src/ui.rs index 0e754e622..f71ff8601 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -763,6 +763,7 @@ fn get_sound_inputs() -> Vec { .collect() } +#[allow(dead_code)] fn check_connect_status( reconnect: bool, ) -> ( diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 4fb6dd605..4d2be50ed 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -2,7 +2,6 @@ use std::{ collections::HashMap, ops::{Deref, DerefMut}, sync::{ - atomic::{Ordering}, Arc, Mutex, }, }; @@ -29,7 +28,7 @@ use hbb_common::{allow_err, log, message_proto::*, rendezvous_proto::ConnType}; use crate::clipboard_file::*; use crate::{ client::*, - ui_session_interface::{InvokeUi, Session, IS_IN}, + ui_session_interface::{InvokeUi, Session}, }; type Video = AssetPtr; @@ -155,6 +154,7 @@ impl InvokeUi for SciterHandler { self.call("clearAllJobs", &make_args!()); } + #[allow(unused_variables)] fn add_job( &self, id: i32, diff --git a/src/ui_interface.rs b/src/ui_interface.rs index a8e3be980..75dead204 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -69,7 +69,7 @@ pub fn goto_install() { allow_err!(crate::run_me(vec!["--install"])); } -pub fn install_me(_options: String, _path: String, silent: bool, debug: bool) { +pub fn install_me(_options: String, _path: String, _silent: bool, _debug: bool) { #[cfg(windows)] std::thread::spawn(move || { allow_err!(crate::platform::windows::install_me( @@ -682,6 +682,7 @@ pub fn check_super_user_permission() -> bool { true } +#[allow(dead_code)] pub fn check_zombie(childs: Childs) { let mut deads = Vec::new(); loop { diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index d0142333d..3c6c21c3f 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -8,7 +8,6 @@ use crate::client::{ }; use crate::{client::Data, client::Interface}; use async_trait::async_trait; -use std::io::Read; use hbb_common::config::{Config, LocalConfig, PeerConfig}; use hbb_common::rendezvous_proto::ConnType; use hbb_common::tokio::{self, sync::mpsc}; @@ -1157,7 +1156,7 @@ impl Session { return; } log::info!("keyboard hooked"); - let mut me = self.clone(); + let me = self.clone(); #[cfg(windows)] crate::platform::windows::enable_lowlevel_keyboard(std::ptr::null_mut() as _); std::thread::spawn(move || { From d3d31ff0149a72949c5ec35ad40658753da927b7 Mon Sep 17 00:00:00 2001 From: asur4s Date: Mon, 5 Sep 2022 23:39:01 -0400 Subject: [PATCH 0395/2015] Fix modifier key status --- flutter/lib/models/model.dart | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index b1cc6192b..811608847 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -1003,11 +1003,18 @@ class FFI { downOrUp: down); } - Future getKeyboardMode(){ + Future getKeyboardMode() { return bind.sessionGetKeyboardName(id: id); } void enterOrLeave(bool enter) { + // Fix status + if (!enter) { + alt = false; + shift = false; + ctrl = false; + command = false; + } bind.sessionEnterOrLeave(id: id, enter: enter); } From afbdbe11fc2bafc3435198a4c06e354096d3dc57 Mon Sep 17 00:00:00 2001 From: Asura Date: Tue, 6 Sep 2022 14:02:12 +0800 Subject: [PATCH 0396/2015] Opt: wayland map mode --- src/server/input_service.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 70af1e581..0088f7dad 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -595,6 +595,7 @@ pub fn handle_key(evt: &KeyEvent) { QUEUE.exec_async(move || handle_key_(&evt)); return; } + log::info!("{:?}", evt); handle_key_(evt); } @@ -647,6 +648,7 @@ fn map_keyboard_mode(evt: &KeyEvent) { let (click_capslock, click_numlock) = sync_status(evt); // Wayland + #[cfg(target_os = "linux")] if !*IS_X11.lock().unwrap() { let mut en = ENIGO.lock().unwrap(); let code = evt.chr() as u16; From f20587cbc091f6b858aec2d8d47e7e19c01cdd44 Mon Sep 17 00:00:00 2001 From: asur4s Date: Tue, 6 Sep 2022 04:38:51 -0400 Subject: [PATCH 0397/2015] Fix release super key --- flutter/lib/desktop/pages/remote_page.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index c2bf4ea5b..381c6e54b 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -332,7 +332,7 @@ class _RemotePageState extends State key == LogicalKeyboardKey.shiftLeft) { _ffi.shift = false; } else if (key == LogicalKeyboardKey.metaLeft || - key == LogicalKeyboardKey.metaRight) { + key == LogicalKeyboardKey.metaRight || key == LogicalKeyboardKey.superKey) { _ffi.command = false; } sendRawKey(e); From 4eaa17017ce55dc86e6ba1bd295843032e8cad63 Mon Sep 17 00:00:00 2001 From: asur4s Date: Tue, 6 Sep 2022 04:39:24 -0400 Subject: [PATCH 0398/2015] Refacotr enter or leave --- flutter/lib/models/model.dart | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 811608847..81827fb81 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -1010,10 +1010,7 @@ class FFI { void enterOrLeave(bool enter) { // Fix status if (!enter) { - alt = false; - shift = false; - ctrl = false; - command = false; + resetModifiers(); } bind.sessionEnterOrLeave(id: id, enter: enter); } From 235eb5415e7acfc62d0414a7e6833e58f83e05eb Mon Sep 17 00:00:00 2001 From: csf Date: Tue, 6 Sep 2022 19:08:45 +0800 Subject: [PATCH 0399/2015] update file transfer and adjust icon size --- .../lib/desktop/pages/file_manager_page.dart | 51 ++++++++++++------- flutter/lib/models/file_model.dart | 8 +-- flutter/lib/models/model.dart | 15 ++++-- src/client.rs | 1 + src/client/io_loop.rs | 31 +---------- src/flutter_ffi.rs | 2 +- src/ui_session_interface.rs | 26 ++++++++++ 7 files changed, 76 insertions(+), 58 deletions(-) diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index b13f40a5f..be0fedc5c 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -59,8 +59,11 @@ class _FileManagerPageState extends State super.initState(); _ffi = FFI(); _ffi.connect(widget.id, isFileTransfer: true); + WidgetsBinding.instance.addPostFrameCallback((_) { + _ffi.dialogManager + .showLoading(translate('Connecting...'), onCancel: closeConnection); + }); Get.put(_ffi, tag: 'ft_${widget.id}'); - // _ffi.ffiModel.updateEventListener(widget.id); if (!Platform.isLinux) { Wakelock.enable(); } @@ -117,7 +120,8 @@ class _FileManagerPageState extends State Widget menu({bool isLocal = false}) { return PopupMenuButton( - icon: Icon(Icons.more_vert), + icon: const Icon(Icons.more_vert), + splashRadius: 20, itemBuilder: (context) { return [ PopupMenuItem( @@ -413,6 +417,7 @@ class _FileManagerPageState extends State /// watch transfer status Widget statusList() { return PreferredSize( + preferredSize: const Size(200, double.infinity), child: Container( margin: const EdgeInsets.only(top: 16.0, bottom: 16.0, right: 16.0), padding: const EdgeInsets.all(8.0), @@ -429,8 +434,8 @@ class _FileManagerPageState extends State children: [ Transform.rotate( angle: item.isRemote ? pi : 0, - child: Icon(Icons.send)), - SizedBox( + child: const Icon(Icons.send)), + const SizedBox( width: 16.0, ), Expanded( @@ -441,7 +446,7 @@ class _FileManagerPageState extends State Tooltip( message: item.jobName, child: Text( - '${item.jobName}', + item.jobName, maxLines: 1, overflow: TextOverflow.ellipsis, )), @@ -455,7 +460,7 @@ class _FileManagerPageState extends State offstage: item.state != JobState.inProgress, child: Text( - '${readableFileSize(item.speed) + "/s"} ')), + '${"${readableFileSize(item.speed)}/s"} ')), Offstage( offstage: item.totalSize <= 0, child: Text( @@ -475,10 +480,12 @@ class _FileManagerPageState extends State onPressed: () { model.resumeJob(item.id); }, - icon: Icon(Icons.restart_alt_rounded)), + splashRadius: 20, + icon: const Icon(Icons.restart_alt_rounded)), ), IconButton( - icon: Icon(Icons.delete), + icon: const Icon(Icons.delete), + splashRadius: 20, onPressed: () { model.jobTable.removeAt(index); model.cancelJob(item.id); @@ -500,8 +507,7 @@ class _FileManagerPageState extends State itemCount: model.jobTable.length, ), ), - ), - preferredSize: Size(200, double.infinity)); + )); } goBack({bool? isLocal}) { @@ -551,12 +557,15 @@ class _FileManagerPageState extends State Row( children: [ IconButton( - onPressed: () { - model.goHome(isLocal: isLocal); - }, - icon: Icon(Icons.home_outlined)), + onPressed: () { + model.goHome(isLocal: isLocal); + }, + icon: const Icon(Icons.home_outlined), + splashRadius: 20, + ), IconButton( - icon: Icon(Icons.arrow_upward), + icon: const Icon(Icons.arrow_upward), + splashRadius: 20, onPressed: () { goBack(isLocal: isLocal); }, @@ -622,13 +631,15 @@ class _FileManagerPageState extends State ), )) ], - child: Icon(Icons.search), + splashRadius: 20, + child: const Icon(Icons.search), ), IconButton( onPressed: () { model.refresh(isLocal: isLocal); }, - icon: Icon(Icons.refresh)), + splashRadius: 20, + icon: const Icon(Icons.refresh)), ], ), Row( @@ -686,6 +697,7 @@ class _FileManagerPageState extends State ); }); }, + splashRadius: 20, icon: const Icon(Icons.create_new_folder_outlined)), IconButton( onPressed: () async { @@ -695,7 +707,8 @@ class _FileManagerPageState extends State await (model.removeAction(items, isLocal: isLocal)); items.clear(); }, - icon: Icon(Icons.delete_forever_outlined)), + splashRadius: 20, + icon: const Icon(Icons.delete_forever_outlined)), ], ), ), @@ -707,7 +720,7 @@ class _FileManagerPageState extends State }, icon: Transform.rotate( angle: isLocal ? 0 : pi, - child: Icon( + child: const Icon( Icons.send, ), ), diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index dedca5efa..3246346be 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -360,9 +360,9 @@ class FileModel extends ChangeNotifier { Future refresh({bool? isLocal}) async { if (isDesktop) { isLocal = isLocal ?? _isLocal; - await isLocal - ? openDirectory(currentLocalDir.path, isLocal: isLocal) - : openDirectory(currentRemoteDir.path, isLocal: isLocal); + isLocal + ? await openDirectory(currentLocalDir.path, isLocal: isLocal) + : await openDirectory(currentRemoteDir.path, isLocal: isLocal); } else { await openDirectory(currentDir.path); } @@ -393,7 +393,7 @@ class FileModel extends ChangeNotifier { } notifyListeners(); } catch (e) { - debugPrint("Failed to openDirectory ${path} :$e"); + debugPrint("Failed to openDirectory $path: $e"); } } diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 1993145e7..7d8cdc203 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -326,8 +326,8 @@ class FfiModel with ChangeNotifier { await bind.sessionGetOption(id: peerId, arg: "touch-mode") != ''; } - if (evt['is_file_transfer'] == "true") { - // TODO is file transfer + if (parent.target != null && + parent.target!.connType == ConnType.fileTransfer) { parent.target?.fileModel.onReady(); } else { _pi.displays = []; @@ -916,6 +916,8 @@ extension ToString on MouseButtons { } } +enum ConnType { defaultConn, fileTransfer, portForward, rdp } + /// FFI class for communicating with the Rust core. class FFI { var id = ""; @@ -924,6 +926,7 @@ class FFI { var alt = false; var command = false; var version = ""; + var connType = ConnType.defaultConn; /// dialogManager use late to ensure init after main page binding [globalKey] late final dialogManager = OverlayDialogManager(); @@ -1055,9 +1058,11 @@ class FFI { double tabBarHeight = 0.0}) { assert(!(isFileTransfer && isPortForward), "more than one connect type"); if (isFileTransfer) { - id = 'ft_${id}'; + connType = ConnType.fileTransfer; + id = 'ft_$id'; } else if (isPortForward) { - id = 'pf_${id}'; + connType = ConnType.portForward; + id = 'pf_$id'; } else { chatModel.resetClientMode(); canvasModel.id = id; @@ -1086,7 +1091,7 @@ class FFI { // every instance will bind a stream this.id = id; if (isFileTransfer) { - this.fileModel.initFileFetcher(); + fileModel.initFileFetcher(); } } diff --git a/src/client.rs b/src/client.rs index 32c0003fd..a4a864846 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1665,6 +1665,7 @@ pub async fn handle_login_from_ui( /// Interface for client to send data and commands. #[async_trait] pub trait Interface: Send + Clone + 'static + Sized { + /// Send message data to remote peer. fn send(&self, data: Data); fn msgbox(&self, msgtype: &str, title: &str, text: &str); fn handle_login_error(&mut self, err: &str) -> bool; diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 552fea7a8..f33f740a3 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -269,33 +269,6 @@ impl Remote { Some(tx) } - // TODO - fn load_last_jobs(&mut self) { - self.handler.clear_all_jobs(); - let pc = self.handler.load_config(); - if pc.transfer.write_jobs.is_empty() && pc.transfer.read_jobs.is_empty() { - // no last jobs - return; - } - // TODO: can add a confirm dialog - let mut cnt = 1; - for job_str in pc.transfer.read_jobs.iter() { - if !job_str.is_empty() { - self.handler.load_last_job(cnt, job_str); - cnt += 1; - log::info!("restore read_job: {:?}", job_str); - } - } - for job_str in pc.transfer.write_jobs.iter() { - if !job_str.is_empty() { - self.handler.load_last_job(cnt, job_str); - cnt += 1; - log::info!("restore write_job: {:?}", job_str); - } - } - self.handler.update_transfer_list(); - } - async fn handle_msg_from_ui(&mut self, data: Data, peer: &mut Stream) -> bool { match data { Data::Close => { @@ -768,7 +741,7 @@ impl Remote { } if self.handler.is_file_transfer() { - self.load_last_jobs(); + self.handler.load_last_jobs(); } } _ => {} @@ -827,7 +800,7 @@ impl Remote { &entries, fd.path, false, - fd.id > 0, + false, ); if let Some(job) = fs::get_job(fd.id, &mut self.write_jobs) { log::info!("job set_files: {:?}", entries); diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 5da94c3c1..ef2aaeaa1 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -366,7 +366,7 @@ pub fn session_get_platform(id: String, is_remote: bool) -> String { pub fn session_load_last_transfer_jobs(id: String) { if let Some(session) = SESSIONS.read().unwrap().get(&id) { - // return session.load_last_jobs(); + return session.load_last_jobs(); } else { // a tip for flutter dev eprintln!( diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 717963561..f1444f4c3 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -532,6 +532,32 @@ impl Session { pub fn close(&self) { self.send(Data::Close); } + + pub fn load_last_jobs(&self) { + self.clear_all_jobs(); + let pc = self.load_config(); + if pc.transfer.write_jobs.is_empty() && pc.transfer.read_jobs.is_empty() { + // no last jobs + return; + } + // TODO: can add a confirm dialog + let mut cnt = 1; + for job_str in pc.transfer.read_jobs.iter() { + if !job_str.is_empty() { + self.load_last_job(cnt, job_str); + cnt += 1; + log::info!("restore read_job: {:?}", job_str); + } + } + for job_str in pc.transfer.write_jobs.iter() { + if !job_str.is_empty() { + self.load_last_job(cnt, job_str); + cnt += 1; + log::info!("restore write_job: {:?}", job_str); + } + } + self.update_transfer_list(); + } } pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { From cc4e6b591d90285c51d385ca4525dae99efc08ea Mon Sep 17 00:00:00 2001 From: asur4s Date: Tue, 6 Sep 2022 07:09:24 -0400 Subject: [PATCH 0400/2015] Convet numpad --- src/ui_session_interface.rs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 3c6c21c3f..d5ed36665 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -60,8 +60,8 @@ impl Session { pub fn get_keyboard_mode(&self) -> String { return std::env::var("KEYBOARD_MODE") - .unwrap_or(String::from("legacy")) - .to_lowercase(); + .unwrap_or(String::from("legacy")) + .to_lowercase(); } pub fn save_keyboard_mode(&self, value: String) { @@ -301,16 +301,15 @@ impl Session { } else { key }; + #[cfg(not(windows))] + let key = self.convert_numpad_keys(key); let peer = self.peer_platform(); - let mut key_event = KeyEvent::new(); // According to peer platform. let keycode: u32 = if peer == "Linux" { rdev::linux_keycode_from_key(key).unwrap_or_default().into() } else if peer == "Windows" { - #[cfg(not(windows))] - let key = self.convert_numpad_keys(key); rdev::win_keycode_from_key(key).unwrap_or_default().into() } else { // Without Clear Key on Mac OS @@ -417,7 +416,7 @@ impl Session { if get_key_state(enigo::Key::CapsLock) { key_event.modifiers.push(ControlKey::CapsLock.into()); - } + } if self.peer_platform() != "Mac OS" { if get_key_state(enigo::Key::NumLock) { key_event.modifiers.push(ControlKey::NumLock.into()); @@ -1360,4 +1359,4 @@ async fn start_one_port_forward( async fn send_note(url: String, id: String, conn_id: i32, note: String) { let body = serde_json::json!({ "id": id, "Id": conn_id, "note": note }); allow_err!(crate::post_request(url, body.to_string(), "").await); -} \ No newline at end of file +} From 05218ecabcdd019f1db42e082f800da727e7ebd5 Mon Sep 17 00:00:00 2001 From: csf Date: Tue, 6 Sep 2022 19:56:35 +0800 Subject: [PATCH 0401/2015] fix sciter confirm_delete_files bug --- src/client/io_loop.rs | 1 - src/ui/remote.rs | 4 ---- 2 files changed, 5 deletions(-) diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index f33f740a3..cc2ca1dae 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -452,7 +452,6 @@ impl Remote { file_num, job.files[i].name.clone(), ); - self.handler.confirm_delete_files(id, file_num); } } } diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 08430110c..c6a36f5c6 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -709,18 +709,15 @@ impl SciterSession { } pub fn make_fd(id: i32, entries: &Vec, only_count: bool) -> Value { - log::debug!("make_fd"); let mut m = Value::map(); m.set_item("id", id); let mut a = Value::array(0); let mut n: u64 = 0; for entry in entries { - log::debug!("for"); n += entry.size; if only_count { continue; } - log::debug!("for1"); let mut e = Value::map(); e.set_item("name", entry.name.to_owned()); let tmp = entry.entry_type.value(); @@ -734,6 +731,5 @@ pub fn make_fd(id: i32, entries: &Vec, only_count: bool) -> Value { } m.set_item("num_entries", entries.len() as i32); m.set_item("total_size", n as f64); - log::debug!("make_fd end"); m } From 468527775eb24e184400e3a6a850adeadeded992 Mon Sep 17 00:00:00 2001 From: csf Date: Tue, 6 Sep 2022 21:10:59 +0800 Subject: [PATCH 0402/2015] fix sciter can't update connect status bug --- src/ui.rs | 96 +----------------------------------------------- src/ui/index.tis | 1 + 2 files changed, 2 insertions(+), 95 deletions(-) diff --git a/src/ui.rs b/src/ui.rs index b8b136c45..96cf21f20 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -87,7 +87,7 @@ pub fn start(args: &mut [String]) { } #[cfg(windows)] if args.len() > 0 && args[0] == "--tray" { - let options = check_connect_status(false).1; + let options = crate::ui_interface::check_connect_status(false).1; crate::tray::start_tray(options); return; } @@ -664,79 +664,6 @@ pub fn check_zombie(childs: Childs) { } } -// notice: avoiding create ipc connecton repeatly, -// because windows named pipe has serious memory leak issue. -#[tokio::main(flavor = "current_thread")] -async fn check_connect_status_( - reconnect: bool, - status: Arc>, - options: Arc>>, - rx: mpsc::UnboundedReceiver, - password: Arc>, -) { - let mut key_confirmed = false; - let mut rx = rx; - let mut mouse_time = 0; - let mut id = "".to_owned(); - loop { - if let Ok(mut c) = ipc::connect(1000, "").await { - let mut timer = time::interval(time::Duration::from_secs(1)); - loop { - tokio::select! { - res = c.next() => { - match res { - Err(err) => { - log::error!("ipc connection closed: {}", err); - break; - } - Ok(Some(ipc::Data::MouseMoveTime(v))) => { - mouse_time = v; - status.lock().unwrap().2 = v; - } - Ok(Some(ipc::Data::Options(Some(v)))) => { - *options.lock().unwrap() = v - } - Ok(Some(ipc::Data::Config((name, Some(value))))) => { - if name == "id" { - id = value; - } else if name == "temporary-password" { - *password.lock().unwrap() = value; - } - } - Ok(Some(ipc::Data::OnlineStatus(Some((mut x, c))))) => { - if x > 0 { - x = 1 - } - key_confirmed = c; - *status.lock().unwrap() = (x as _, key_confirmed, mouse_time, id.clone()); - } - _ => {} - } - } - Some(data) = rx.recv() => { - allow_err!(c.send(&data).await); - } - _ = timer.tick() => { - c.send(&ipc::Data::OnlineStatus(None)).await.ok(); - c.send(&ipc::Data::Options(None)).await.ok(); - c.send(&ipc::Data::Config(("id".to_owned(), None))).await.ok(); - c.send(&ipc::Data::Config(("temporary-password".to_owned(), None))).await.ok(); - } - } - } - } - if !reconnect { - options - .lock() - .unwrap() - .insert("ipc-closed".to_owned(), "Y".to_owned()); - break; - } - *status.lock().unwrap() = (-1, key_confirmed, mouse_time, id.clone()); - sleep(1.).await; - } -} - #[cfg(not(target_os = "linux"))] fn get_sound_inputs() -> Vec { let mut out = Vec::new(); @@ -763,27 +690,6 @@ fn get_sound_inputs() -> Vec { .collect() } -fn check_connect_status( - reconnect: bool, -) -> ( - Arc>, - Arc>>, - mpsc::UnboundedSender, - Arc>, -) { - let status = Arc::new(Mutex::new((0, false, 0, "".to_owned()))); - let options = Arc::new(Mutex::new(Config::get_options())); - let cloned = status.clone(); - let cloned_options = options.clone(); - let (tx, rx) = mpsc::unbounded_channel::(); - let password = Arc::new(Mutex::new(String::default())); - let cloned_password = password.clone(); - std::thread::spawn(move || { - check_connect_status_(reconnect, cloned, cloned_options, rx, cloned_password) - }); - (status, options, tx, password) -} - const INVALID_FORMAT: &'static str = "Invalid format"; const UNKNOWN_ERROR: &'static str = "Unknown error"; diff --git a/src/ui/index.tis b/src/ui/index.tis index 256f00c44..d0a9d29a8 100644 --- a/src/ui/index.tis +++ b/src/ui/index.tis @@ -1055,6 +1055,7 @@ function showSettings() { } function checkConnectStatus() { + handler.check_mouse_time(); // trigger connection status updater self.timer(1s, function() { var tmp = !!handler.get_option("stop-service"); if (tmp != service_stopped) { From f1c8b59a91ea1f7fe2a36bc21e53c78061ef4934 Mon Sep 17 00:00:00 2001 From: asur4s Date: Wed, 7 Sep 2022 04:04:07 -0400 Subject: [PATCH 0403/2015] Update lock file --- flutter/pubspec.lock | 4 ++-- rust-toolchain.toml | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 rust-toolchain.toml diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index d9205b76c..08af15b8d 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -250,8 +250,8 @@ packages: dependency: "direct main" description: path: "." - ref: e0368a023ba195462acc00d33ab361b499f0e413 - resolved-ref: e0368a023ba195462acc00d33ab361b499f0e413 + ref: fee851fa43116e0b91c39acd0ec37063dc6015f8 + resolved-ref: fee851fa43116e0b91c39acd0ec37063dc6015f8 url: "https://github.com/Kingtous/rustdesk_desktop_multi_window" source: git version: "0.1.0" diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 000000000..24c182fd3 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "1.62.0" \ No newline at end of file From 4d3fa6955b6d52f64c19a4c4da09054ff2c8a19f Mon Sep 17 00:00:00 2001 From: Asura Date: Wed, 7 Sep 2022 16:57:27 +0800 Subject: [PATCH 0404/2015] Fix windows compile error --- flutter/pubspec.lock | 24 ++++++++++++------------ src/ui.rs | 23 ++++++++++++++++++++++- src/ui_interface.rs | 4 ++-- 3 files changed, 36 insertions(+), 15 deletions(-) diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 08af15b8d..586187be2 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -49,7 +49,7 @@ packages: name: async url: "https://pub.flutter-io.cn" source: hosted - version: "2.9.0" + version: "2.8.2" back_button_interceptor: dependency: "direct main" description: @@ -147,7 +147,7 @@ packages: name: characters url: "https://pub.flutter-io.cn" source: hosted - version: "1.2.1" + version: "1.2.0" charcode: dependency: transitive description: @@ -168,7 +168,7 @@ packages: name: clock url: "https://pub.flutter-io.cn" source: hosted - version: "1.1.1" + version: "1.1.0" code_builder: dependency: transitive description: @@ -324,7 +324,7 @@ packages: name: fake_async url: "https://pub.flutter-io.cn" source: hosted - version: "1.3.1" + version: "1.3.0" ffi: dependency: "direct main" description: @@ -621,14 +621,14 @@ packages: name: matcher url: "https://pub.flutter-io.cn" source: hosted - version: "0.12.12" + version: "0.12.11" material_color_utilities: dependency: transitive description: name: material_color_utilities url: "https://pub.flutter-io.cn" source: hosted - version: "0.1.5" + version: "0.1.4" menu_base: dependency: transitive description: @@ -642,7 +642,7 @@ packages: name: meta url: "https://pub.flutter-io.cn" source: hosted - version: "1.8.0" + version: "1.7.0" mime: dependency: transitive description: @@ -719,7 +719,7 @@ packages: name: path url: "https://pub.flutter-io.cn" source: hosted - version: "1.8.2" + version: "1.8.1" path_provider: dependency: "direct main" description: @@ -971,7 +971,7 @@ packages: name: source_span url: "https://pub.flutter-io.cn" source: hosted - version: "1.9.0" + version: "1.8.2" sqflite: dependency: transitive description: @@ -1013,7 +1013,7 @@ packages: name: string_scanner url: "https://pub.flutter-io.cn" source: hosted - version: "1.1.1" + version: "1.1.0" synchronized: dependency: transitive description: @@ -1027,14 +1027,14 @@ packages: name: term_glyph url: "https://pub.flutter-io.cn" source: hosted - version: "1.2.1" + version: "1.2.0" test_api: dependency: transitive description: name: test_api url: "https://pub.flutter-io.cn" source: hosted - version: "0.4.12" + version: "0.4.9" timing: dependency: transitive description: diff --git a/src/ui.rs b/src/ui.rs index 30cbec9c4..25ad18521 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -58,6 +58,27 @@ lazy_static::lazy_static! { struct UIHostHandler; +fn check_connect_status( + reconnect: bool, +) -> ( + Arc>, + Arc>>, + mpsc::UnboundedSender, + Arc>, +) { + let status = Arc::new(Mutex::new((0, false, 0, "".to_owned()))); + let options = Arc::new(Mutex::new(Config::get_options())); + let cloned = status.clone(); + let cloned_options = options.clone(); + let (tx, rx) = mpsc::unbounded_channel::(); + let password = Arc::new(Mutex::new(String::default())); + let cloned_password = password.clone(); + std::thread::spawn(move || { + crate::ui_interface::check_connect_status_(reconnect, rx) + }); + (status, options, tx, password) +} + pub fn start(args: &mut [String]) { #[cfg(target_os = "macos")] if args.len() == 1 && args[0] == "--server" { @@ -86,7 +107,7 @@ pub fn start(args: &mut [String]) { } #[cfg(windows)] if args.len() > 0 && args[0] == "--tray" { - let options = crate::ui_interface::check_connect_status(false).1; + let options = check_connect_status(false).1; crate::tray::start_tray(options); return; } diff --git a/src/ui_interface.rs b/src/ui_interface.rs index f239cc54e..186381ce4 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -69,7 +69,7 @@ pub fn goto_install() { allow_err!(crate::run_me(vec!["--install"])); } -pub fn install_me(_options: String, _path: String, _silent: bool, _debug: bool) { +pub fn install_me(_options: String, _path: String, silent: bool, debug: bool) { #[cfg(windows)] std::thread::spawn(move || { allow_err!(crate::platform::windows::install_me( @@ -715,7 +715,7 @@ pub(crate) fn check_connect_status(reconnect: bool) -> mpsc::UnboundedSender) { +pub(crate) async fn check_connect_status_(reconnect: bool, rx: mpsc::UnboundedReceiver) { let mut key_confirmed = false; let mut rx = rx; let mut mouse_time = 0; From afa9cda9bd68eca009bc3b659e30b3fe6915dc92 Mon Sep 17 00:00:00 2001 From: Asura Date: Wed, 7 Sep 2022 16:58:44 +0800 Subject: [PATCH 0405/2015] Hide translate mode --- src/ui/header.tis | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ui/header.tis b/src/ui/header.tis index 870167eae..b274b0464 100644 --- a/src/ui/header.tis +++ b/src/ui/header.tis @@ -151,7 +151,6 @@ class Header: Reactor.Component {
  • {svg_checkmark}{translate('Legacy mode')}
  • {svg_checkmark}{translate('Map mode')}
  • -
  • {svg_checkmark}{translate('Translate mode')}
  • ; } From a3279de93a8bcce1675605ebfa83547e67545cf5 Mon Sep 17 00:00:00 2001 From: Asura Date: Wed, 7 Sep 2022 17:07:36 +0800 Subject: [PATCH 0406/2015] Remove unnecessary log --- src/server/input_service.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 0088f7dad..d78441a18 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -595,7 +595,6 @@ pub fn handle_key(evt: &KeyEvent) { QUEUE.exec_async(move || handle_key_(&evt)); return; } - log::info!("{:?}", evt); handle_key_(evt); } From a50482af5caced4fde8d2db269d123534bef8eab Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 6 Sep 2022 02:08:59 -0700 Subject: [PATCH 0407/2015] flutter_desktop: WOL & menu, mid commit Signed-off-by: fufesou --- .../lib/desktop/pages/connection_page.dart | 6 +- .../lib/desktop/pages/desktop_home_page.dart | 273 +++++++++++++----- .../desktop/pages/desktop_setting_page.dart | 1 - .../lib/desktop/pages/file_manager_page.dart | 2 +- .../desktop/pages/file_manager_tab_page.dart | 4 +- flutter/lib/desktop/pages/remote_page.dart | 19 +- flutter/lib/desktop/pages/server_page.dart | 4 +- .../lib/desktop/widgets/peercard_widget.dart | 49 +++- flutter/lib/desktop/widgets/popup_menu.dart | 41 ++- .../lib/desktop/widgets/remote_menubar.dart | 28 +- flutter/pubspec.yaml | 4 + src/flutter_ffi.rs | 4 + 12 files changed, 311 insertions(+), 124 deletions(-) diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index e4f6527ca..b113d8a56 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -22,10 +22,10 @@ import '../../models/platform_model.dart'; /// Connection page for connecting to a remote peer. class ConnectionPage extends StatefulWidget { - ConnectionPage({Key? key}) : super(key: key); + const ConnectionPage({Key? key}) : super(key: key); @override - _ConnectionPageState createState() => _ConnectionPageState(); + State createState() => _ConnectionPageState(); } /// State for the connection page. @@ -101,7 +101,7 @@ class _ConnectionPageState extends State { ], ).marginSymmetric(horizontal: 22), ), - Divider(), + const Divider(), SizedBox(height: 50, child: Obx(() => buildStatus())) .paddingSymmetric(horizontal: 12.0) ]), diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 3ce956c23..545e9165c 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -19,8 +19,15 @@ import 'package:tray_manager/tray_manager.dart'; import 'package:url_launcher/url_launcher_string.dart'; import 'package:window_manager/window_manager.dart'; +class _PopupMenuTheme { + static const Color commonColor = MyTheme.accent; + // kMinInteractiveDimension + static const double height = 25.0; + static const double dividerHeight = 3.0; +} + class DesktopHomePage extends StatefulWidget { - DesktopHomePage({Key? key}) : super(key: key); + const DesktopHomePage({Key? key}) : super(key: key); @override State createState() => _DesktopHomePageState(); @@ -86,7 +93,7 @@ class _DesktopHomePageState extends State buildServerBoard(BuildContext context) { return Container( color: MyTheme.color(context).grayBg, - child: ConnectionPage(), + child: const ConnectionPage(), ); } @@ -154,8 +161,190 @@ class _DesktopHomePageState extends State ); } + // Future> _genSwitchEntry( + // String label, String key) async { + + // final v = await bind.mainGetOption(key: key); + // bool enable; + // if (key == "stop-service") { + // enable = v != "Y"; + // } else if (key.startsWith("allow-")) { + // enable = v == "Y"; + // } else { + // enable = v != "N"; + // } + + // return PopupMenuItem( + // child: Row( + // children: [ + // Icon(Icons.check, + // color: enable ? null : MyTheme.accent.withAlpha(00)), + // Text( + // label, + // style: genTextStyle(enable), + // ), + // ], + // ), + // value: key, + // ); + // } + + _popupMenu(BuildContext context, RelativeRect position) async { + TextStyle styleEnabled = const TextStyle( + color: Colors.black, + fontSize: MenuConfig.fontSize, + fontWeight: FontWeight.normal); + TextStyle styleDisabled = const TextStyle( + color: Colors.redAccent, + fontSize: MenuConfig.fontSize, + fontWeight: FontWeight.normal, + decoration: TextDecoration.lineThrough); + + enabledEntry(String label, String key) { + Rx textStyle = styleEnabled.obs; + return MenuEntrySwitch( + text: translate(label), + textStyle: textStyle, + getter: () async { + final opt = await bind.mainGetOption(key: key); + bool enabled; + if (key == 'stop-service') { + enabled = opt != 'Y'; + } else if (key.startsWith("allow-")) { + enabled = opt == 'Y'; + } else { + enabled = opt != 'N'; + } + textStyle.value = enabled ? styleEnabled : styleDisabled; + return enabled; + }, + setter: (bool v) async { + String opt; + if (key == 'stop-service') { + opt = v ? 'Y' : ''; + } else if (key.startsWith("allow-")) { + opt = v ? 'Y' : ''; + } else { + opt = v ? '' : 'N'; + } + await bind.mainSetOption(key: key, value: opt); + if (key == 'allow-darktheme') { + changeTheme(opt); + } + }, + dismissOnClicked: false, + ); + } + + final userName = await gFFI.userModel.getUserName(); + final enabledInput = await bind.mainGetOption(key: 'enable-audio'); + final defaultInput = await gFFI.getDefaultAudioInput(); + + final List> menu = >[ + enabledEntry('Enable Keyboard/Mouse', 'enable-keyboard'), + enabledEntry('Enable Clipboard', 'enable-clipboard'), + enabledEntry('Enable File Transfer', 'enable-file-transfer'), + enabledEntry('Enable TCP Tunneling', 'enable-tunnel'), + // TODO: audio sub menu? + // genAudioInputPopupMenuItem(enabledInput != "N", defaultInput), + MenuEntryDivider(), + MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('ID/Relay Server'), + style: style, + ), + proc: () { + changeServer(); + }, + dismissOnClicked: true, + ), + MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('IP Whitelisting'), + style: style, + ), + proc: () { + changeWhiteList(); + }, + dismissOnClicked: true, + ), + MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Socks5 Proxy'), + style: style, + ), + proc: () { + changeSocks5Proxy(); + }, + dismissOnClicked: true, + ), + MenuEntryDivider(), + enabledEntry('Enable Service', 'stop-service'), + enabledEntry('Always connected via relay', 'allow-always-relay'), + // FIXME: is this option correct? + enabledEntry('Start ID/relay service', 'stop-rendezvous-service'), + MenuEntryDivider(), + userName.isEmpty + ? MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Login'), + style: style, + ), + proc: () { + login(); + }, + dismissOnClicked: true, + ) + : MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Logout'), + style: style, + ), + proc: () { + logOut(); + }, + dismissOnClicked: true, + ), + MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Change ID'), + style: style, + ), + proc: () { + changeId(); + }, + dismissOnClicked: true, + ), + MenuEntryDivider(), + enabledEntry('Dark Theme', 'allow-darktheme'), + MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('About'), + style: style, + ), + proc: () { + about(); + }, + dismissOnClicked: true, + ), + ]; + + await mod_menu.showMenu( + context: context, + position: position, + items: menu + .map((e) => e.build( + context, + const MenuConfig( + commonColor: _PopupMenuTheme.commonColor, + height: _PopupMenuTheme.height, + dividerHeight: _PopupMenuTheme.dividerHeight))) + .expand((i) => i) + .toList()); + } + Widget buildPopupMenu(BuildContext context) { - var position; + RelativeRect position = const RelativeRect.fromLTRB(0.0, 0.0, 0.0, 0.0); RxBool hover = false.obs; return InkWell( onTapDown: (detail) { @@ -164,83 +353,7 @@ class _DesktopHomePageState extends State position = RelativeRect.fromLTRB(x, y, x, y); }, onTap: () async { - final userName = await gFFI.userModel.getUserName(); - final enabledInput = await bind.mainGetOption(key: 'enable-audio'); - final defaultInput = await gFFI.getDefaultAudioInput(); - var menu = [ - await genEnablePopupMenuItem( - translate("Enable Keyboard/Mouse"), - 'enable-keyboard', - ), - await genEnablePopupMenuItem( - translate("Enable Clipboard"), - 'enable-clipboard', - ), - await genEnablePopupMenuItem( - translate("Enable File Transfer"), - 'enable-file-transfer', - ), - await genEnablePopupMenuItem( - translate("Enable TCP Tunneling"), - 'enable-tunnel', - ), - genAudioInputPopupMenuItem(enabledInput != "N", defaultInput), - PopupMenuDivider(), - PopupMenuItem( - child: Text(translate("ID/Relay Server")), - value: 'custom-server', - ), - PopupMenuItem( - child: Text(translate("IP Whitelisting")), - value: 'whitelist', - ), - PopupMenuItem( - child: Text(translate("Socks5 Proxy")), - value: 'socks5-proxy', - ), - PopupMenuDivider(), - await genEnablePopupMenuItem( - translate("Enable Service"), - 'stop-service', - ), - // TODO: direct server - await genEnablePopupMenuItem( - translate("Always connected via relay"), - 'allow-always-relay', - ), - await genEnablePopupMenuItem( - translate("Start ID/relay service"), - 'stop-rendezvous-service', - ), - PopupMenuDivider(), - userName.isEmpty - ? PopupMenuItem( - child: Text(translate("Login")), - value: 'login', - ) - : PopupMenuItem( - child: Text("${translate("Logout")} $userName"), - value: 'logout', - ), - PopupMenuItem( - child: Text(translate("Change ID")), - value: 'change-id', - ), - PopupMenuDivider(), - await genEnablePopupMenuItem( - translate("Dark Theme"), - 'allow-darktheme', - ), - PopupMenuItem( - child: Text(translate("About")), - value: 'about', - ), - ]; - final v = - await showMenu(context: context, position: position, items: menu); - if (v != null) { - onSelectMenu(v); - } + await _popupMenu(context, position); }, child: Obx( () => CircleAvatar( diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index b4bb00ab8..35383a7e0 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -1,5 +1,4 @@ import 'dart:convert'; -import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index be0fedc5c..bd6e4cb63 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -17,7 +17,7 @@ import '../../models/platform_model.dart'; enum LocationStatus { bread, textField } class FileManagerPage extends StatefulWidget { - FileManagerPage({Key? key, required this.id}) : super(key: key); + const FileManagerPage({Key? key, required this.id}) : super(key: key); final String id; @override diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index 18ea039a7..d6f01e55f 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -21,8 +21,8 @@ class FileManagerTabPage extends StatefulWidget { class _FileManagerTabPageState extends State { DesktopTabController get tabController => Get.find(); - static final IconData selectedIcon = Icons.file_copy_sharp; - static final IconData unselectedIcon = Icons.file_copy_outlined; + static const IconData selectedIcon = Icons.file_copy_sharp; + static const IconData unselectedIcon = Icons.file_copy_outlined; _FileManagerTabPageState(Map params) { Get.put(DesktopTabController(tabType: DesktopTabType.fileTransfer)); diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 23e4e4900..9b0ce66c2 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -8,6 +8,7 @@ import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:wakelock/wakelock.dart'; +import 'package:flutter_custom_cursor/flutter_custom_cursor.dart'; // import 'package:window_manager/window_manager.dart'; @@ -22,7 +23,7 @@ import '../../common/shared_state.dart'; final initText = '\1' * 1024; class RemotePage extends StatefulWidget { - RemotePage({ + const RemotePage({ Key? key, required this.id, required this.tabBarHeight, @@ -32,7 +33,7 @@ class RemotePage extends StatefulWidget { final double tabBarHeight; @override - _RemotePageState createState() => _RemotePageState(); + State createState() => _RemotePageState(); } class _RemotePageState extends State @@ -483,6 +484,8 @@ class ImagePaint extends StatelessWidget { child: CustomPaint( painter: ImagePainter(image: m.image, x: 0, y: 0, scale: s), )); + + Rx pos = Rx(Offset(0.0, 0.0)); return Center( child: NotificationListener( onNotification: (notification) { @@ -498,9 +501,15 @@ class ImagePaint extends StatelessWidget { return false; }, child: Obx(() => MouseRegion( - cursor: (keyboardEnabled.isTrue && cursorOverImage.isTrue) - ? SystemMouseCursors.none - : MouseCursor.defer, + // cursor: (keyboardEnabled.isTrue && cursorOverImage.isTrue) + // ? SystemMouseCursors.none + // : MouseCursor.defer, + /// cursor: MouseCursor.defer, + cursor: FlutterCustomCursor( + path: "assets/pencil.png", x: 1.0, y: 8.0), + onHover: (evt) { + pos.value = evt.position; + }, child: _buildCrossScrollbar(_buildListener(imageWidget)))), ), ); diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index ac2fb7caa..08f3e5836 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -13,8 +13,10 @@ import '../../models/platform_model.dart'; import '../../models/server_model.dart'; class DesktopServerPage extends StatefulWidget { + const DesktopServerPage({Key? key}) : super(key: key); + @override - State createState() => _DesktopServerPageState(); + State createState() => _DesktopServerPageState(); } class _DesktopServerPageState extends State diff --git a/flutter/lib/desktop/widgets/peercard_widget.dart b/flutter/lib/desktop/widgets/peercard_widget.dart index 13ab92ffe..fb1c59891 100644 --- a/flutter/lib/desktop/widgets/peercard_widget.dart +++ b/flutter/lib/desktop/widgets/peercard_widget.dart @@ -427,7 +427,7 @@ abstract class BasePeerCard extends StatelessWidget { alignment: Alignment.centerRight, child: IconButton( padding: EdgeInsets.zero, - icon: Icon(Icons.edit), + icon: const Icon(Icons.edit), onPressed: () => _rdpDialog(id), ), )) @@ -440,6 +440,20 @@ abstract class BasePeerCard extends StatelessWidget { ); } + @protected + MenuEntryBase _wolAction(String id) { + return MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('WOL'), + style: style, + ), + proc: () { + bind.mainWol(id: id); + }, + dismissOnClicked: true, + ); + } + @protected Future> _forceAlwaysRelayAction(String id) async { const option = 'force-always-relay'; @@ -620,11 +634,16 @@ class RecentPeerCard extends BasePeerCard { _transferFileAction(context, peer.id), _tcpTunnelingAction(context, peer.id), ]; + MenuEntryBase? rdpAction; if (peer.platform == 'Windows') { - menuItems.add(_rdpAction(context, peer.id)); + rdpAction = _rdpAction(context, peer.id); } - menuItems.add(MenuEntryDivider()); menuItems.add(await _forceAlwaysRelayAction(peer.id)); + if (rdpAction != null) { + menuItems.add(rdpAction); + } + menuItems.add(_wolAction(peer.id)); + menuItems.add(MenuEntryDivider()); menuItems.add(_renameAction(peer.id, false)); menuItems.add(_removeAction(peer.id, () async { await bind.mainLoadRecentPeers(); @@ -647,10 +666,16 @@ class FavoritePeerCard extends BasePeerCard { _transferFileAction(context, peer.id), _tcpTunnelingAction(context, peer.id), ]; + MenuEntryBase? rdpAction; if (peer.platform == 'Windows') { - menuItems.add(_rdpAction(context, peer.id)); + rdpAction = _rdpAction(context, peer.id); } menuItems.add(await _forceAlwaysRelayAction(peer.id)); + if (rdpAction != null) { + menuItems.add(rdpAction); + } + menuItems.add(_wolAction(peer.id)); + menuItems.add(MenuEntryDivider()); menuItems.add(_renameAction(peer.id, false)); menuItems.add(_removeAction(peer.id, () async { await bind.mainLoadFavPeers(); @@ -673,10 +698,16 @@ class DiscoveredPeerCard extends BasePeerCard { _transferFileAction(context, peer.id), _tcpTunnelingAction(context, peer.id), ]; + MenuEntryBase? rdpAction; if (peer.platform == 'Windows') { - menuItems.add(_rdpAction(context, peer.id)); + rdpAction = _rdpAction(context, peer.id); } menuItems.add(await _forceAlwaysRelayAction(peer.id)); + if (rdpAction != null) { + menuItems.add(rdpAction); + } + menuItems.add(_wolAction(peer.id)); + menuItems.add(MenuEntryDivider()); menuItems.add(_renameAction(peer.id, false)); menuItems.add(_removeAction(peer.id, () async { await bind.mainLoadLanPeers(); @@ -698,10 +729,16 @@ class AddressBookPeerCard extends BasePeerCard { _transferFileAction(context, peer.id), _tcpTunnelingAction(context, peer.id), ]; + MenuEntryBase? rdpAction; if (peer.platform == 'Windows') { - menuItems.add(_rdpAction(context, peer.id)); + rdpAction = _rdpAction(context, peer.id); } menuItems.add(await _forceAlwaysRelayAction(peer.id)); + if (rdpAction != null) { + menuItems.add(rdpAction); + } + menuItems.add(_wolAction(peer.id)); + menuItems.add(MenuEntryDivider()); menuItems.add(_renameAction(peer.id, false)); menuItems.add(_removeAction(peer.id, () async {})); menuItems.add(_unrememberPasswordAction(peer.id)); diff --git a/flutter/lib/desktop/widgets/popup_menu.dart b/flutter/lib/desktop/widgets/popup_menu.dart index ea678673a..02e3512df 100644 --- a/flutter/lib/desktop/widgets/popup_menu.dart +++ b/flutter/lib/desktop/widgets/popup_menu.dart @@ -325,8 +325,10 @@ typedef SwitchSetter = Future Function(bool); abstract class MenuEntrySwitchBase extends MenuEntryBase { final String text; + final Rx? textStyle; - MenuEntrySwitchBase({required this.text, required dismissOnClicked}) + MenuEntrySwitchBase( + {required this.text, required dismissOnClicked, this.textStyle}) : super(dismissOnClicked: dismissOnClicked); RxBool get curOption; @@ -344,14 +346,23 @@ abstract class MenuEntrySwitchBase extends MenuEntryBase { alignment: AlignmentDirectional.centerStart, height: conf.height, child: Row(children: [ - // const SizedBox(width: MenuConfig.midPadding), - Text( - text, - style: TextStyle( - color: MyTheme.color(context).text, - fontSize: MenuConfig.fontSize, - fontWeight: FontWeight.normal), - ), + () { + if (textStyle != null) { + final style = textStyle!; + return Obx(() => Text( + text, + style: style.value, + )); + } else { + return Text( + text, + style: const TextStyle( + color: Colors.black, + fontSize: MenuConfig.fontSize, + fontWeight: FontWeight.normal), + ); + } + }(), Expanded( child: Align( alignment: Alignment.centerRight, @@ -388,8 +399,12 @@ class MenuEntrySwitch extends MenuEntrySwitchBase { {required String text, required this.getter, required this.setter, + Rx? textStyle, dismissOnClicked = false}) - : super(text: text, dismissOnClicked: dismissOnClicked) { + : super( + text: text, + textStyle: textStyle, + dismissOnClicked: dismissOnClicked) { () async { _curOption.value = await getter(); }(); @@ -418,8 +433,12 @@ class MenuEntrySwitch2 extends MenuEntrySwitchBase { {required String text, required this.getter, required this.setter, + Rx? textStyle, dismissOnClicked = false}) - : super(text: text, dismissOnClicked: dismissOnClicked); + : super( + text: text, + textStyle: textStyle, + dismissOnClicked: dismissOnClicked); @override RxBool get curOption => getter(); diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index c83f61a17..411c30e73 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -75,20 +75,20 @@ class _RemoteMenubarState extends State { final List menubarItems = []; if (!isWebDesktop) { menubarItems.add(_buildFullscreen(context)); - if (widget.ffi.ffiModel.isPeerAndroid) { - menubarItems.add(IconButton( - tooltip: translate('Mobile Actions'), - color: _MenubarTheme.commonColor, - icon: const Icon(Icons.build), - onPressed: () { - if (mobileActionsOverlayEntry == null) { - showMobileActionsOverlay(); - } else { - hideMobileActionsOverlay(); - } - }, - )); - } + //if (widget.ffi.ffiModel.isPeerAndroid) { + menubarItems.add(IconButton( + tooltip: translate('Mobile Actions'), + color: _MenubarTheme.commonColor, + icon: const Icon(Icons.build), + onPressed: () { + if (mobileActionsOverlayEntry == null) { + showMobileActionsOverlay(); + } else { + hideMobileActionsOverlay(); + } + }, + )); + //} } menubarItems.add(_buildMonitor(context)); menubarItems.add(_buildControl(context)); diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index f86c59755..1f4987e5c 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -68,6 +68,10 @@ dependencies: git: url: https://github.com/Kingtous/rustdesk_tray_manager ref: 3aa37c86e47ea748e7b5507cbe59f2c54ebdb23a + flutter_custom_cursor: + git: + url: https://github.com/Kingtous/rustdesk_flutter_custom_cursor + ref: 7fe78c139c711bafbae52d924e9caf18bd193e28 get: ^4.6.5 visibility_detector: ^0.3.3 contextmenu: ^3.0.0 diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index ef2aaeaa1..04d8619c1 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -789,6 +789,10 @@ pub fn main_get_mouse_time() -> f64 { get_mouse_time() } +pub fn main_wol(id: String) { + crate::lan::send_wol(id) +} + pub fn cm_send_chat(conn_id: i32, msg: String) { crate::ui_cm_interface::send_chat(conn_id, msg); } From 70c472676632144305164f1b9dfb54a26f5b9d8a Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 6 Sep 2022 21:20:53 -0700 Subject: [PATCH 0408/2015] flutter_desktop: password menu Signed-off-by: fufesou --- flutter/lib/common/shared_state.dart | 25 +- .../lib/desktop/pages/desktop_home_page.dart | 427 +++++++++--------- .../desktop/pages/desktop_setting_page.dart | 24 +- flutter/lib/desktop/pages/remote_page.dart | 24 +- .../widgets/material_mod_popup_menu.dart | 8 +- flutter/lib/desktop/widgets/popup_menu.dart | 236 ++++++---- flutter/lib/models/model.dart | 165 +++---- flutter/lib/models/server_model.dart | 10 +- 8 files changed, 501 insertions(+), 418 deletions(-) diff --git a/flutter/lib/common/shared_state.dart b/flutter/lib/common/shared_state.dart index 67752d888..9e741846f 100644 --- a/flutter/lib/common/shared_state.dart +++ b/flutter/lib/common/shared_state.dart @@ -1,7 +1,8 @@ import 'package:get/get.dart'; import '../consts.dart'; -import '../models/platform_model.dart'; + +// TODO: A lot of dup code. class PrivacyModeState { static String tag(String id) => 'privacy_mode_$id'; @@ -156,3 +157,25 @@ class KeyboardEnabledState { static RxBool find(String id) => Get.find(tag: tag(id)); } + +class RemoteCursorMovedState { + static String tag(String id) => 'remote_cursor_moved_$id'; + + static void init(String id) { + final key = tag(id); + if (!Get.isRegistered(tag: key)) { + // Server side, default true + final RxBool state = false.obs; + Get.put(state, tag: key); + } + } + + static void delete(String id) { + final key = tag(id); + if (Get.isRegistered(tag: key)) { + Get.delete(tag: key); + } + } + + static RxBool find(String id) => Get.find(tag: tag(id)); +} diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 545e9165c..3cda8aa60 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -19,18 +19,18 @@ import 'package:tray_manager/tray_manager.dart'; import 'package:url_launcher/url_launcher_string.dart'; import 'package:window_manager/window_manager.dart'; -class _PopupMenuTheme { +class _MenubarTheme { static const Color commonColor = MyTheme.accent; // kMinInteractiveDimension static const double height = 25.0; - static const double dividerHeight = 3.0; + static const double dividerHeight = 12.0; } class DesktopHomePage extends StatefulWidget { const DesktopHomePage({Key? key}) : super(key: key); @override - State createState() => _DesktopHomePageState(); + State createState() => _DesktopHomePageState(); } const borderColor = Color(0xFF2F65BA); @@ -93,7 +93,7 @@ class _DesktopHomePageState extends State buildServerBoard(BuildContext context) { return Container( color: MyTheme.color(context).grayBg, - child: const ConnectionPage(), + child: ConnectionPage(), ); } @@ -116,7 +116,7 @@ class _DesktopHomePageState extends State child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox( + Container( height: 25, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -142,11 +142,11 @@ class _DesktopHomePageState extends State child: TextFormField( controller: model.serverId, readOnly: true, - decoration: const InputDecoration( + decoration: InputDecoration( border: InputBorder.none, contentPadding: EdgeInsets.only(bottom: 20), ), - style: const TextStyle( + style: TextStyle( fontSize: 22, ), ), @@ -161,190 +161,8 @@ class _DesktopHomePageState extends State ); } - // Future> _genSwitchEntry( - // String label, String key) async { - - // final v = await bind.mainGetOption(key: key); - // bool enable; - // if (key == "stop-service") { - // enable = v != "Y"; - // } else if (key.startsWith("allow-")) { - // enable = v == "Y"; - // } else { - // enable = v != "N"; - // } - - // return PopupMenuItem( - // child: Row( - // children: [ - // Icon(Icons.check, - // color: enable ? null : MyTheme.accent.withAlpha(00)), - // Text( - // label, - // style: genTextStyle(enable), - // ), - // ], - // ), - // value: key, - // ); - // } - - _popupMenu(BuildContext context, RelativeRect position) async { - TextStyle styleEnabled = const TextStyle( - color: Colors.black, - fontSize: MenuConfig.fontSize, - fontWeight: FontWeight.normal); - TextStyle styleDisabled = const TextStyle( - color: Colors.redAccent, - fontSize: MenuConfig.fontSize, - fontWeight: FontWeight.normal, - decoration: TextDecoration.lineThrough); - - enabledEntry(String label, String key) { - Rx textStyle = styleEnabled.obs; - return MenuEntrySwitch( - text: translate(label), - textStyle: textStyle, - getter: () async { - final opt = await bind.mainGetOption(key: key); - bool enabled; - if (key == 'stop-service') { - enabled = opt != 'Y'; - } else if (key.startsWith("allow-")) { - enabled = opt == 'Y'; - } else { - enabled = opt != 'N'; - } - textStyle.value = enabled ? styleEnabled : styleDisabled; - return enabled; - }, - setter: (bool v) async { - String opt; - if (key == 'stop-service') { - opt = v ? 'Y' : ''; - } else if (key.startsWith("allow-")) { - opt = v ? 'Y' : ''; - } else { - opt = v ? '' : 'N'; - } - await bind.mainSetOption(key: key, value: opt); - if (key == 'allow-darktheme') { - changeTheme(opt); - } - }, - dismissOnClicked: false, - ); - } - - final userName = await gFFI.userModel.getUserName(); - final enabledInput = await bind.mainGetOption(key: 'enable-audio'); - final defaultInput = await gFFI.getDefaultAudioInput(); - - final List> menu = >[ - enabledEntry('Enable Keyboard/Mouse', 'enable-keyboard'), - enabledEntry('Enable Clipboard', 'enable-clipboard'), - enabledEntry('Enable File Transfer', 'enable-file-transfer'), - enabledEntry('Enable TCP Tunneling', 'enable-tunnel'), - // TODO: audio sub menu? - // genAudioInputPopupMenuItem(enabledInput != "N", defaultInput), - MenuEntryDivider(), - MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - translate('ID/Relay Server'), - style: style, - ), - proc: () { - changeServer(); - }, - dismissOnClicked: true, - ), - MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - translate('IP Whitelisting'), - style: style, - ), - proc: () { - changeWhiteList(); - }, - dismissOnClicked: true, - ), - MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - translate('Socks5 Proxy'), - style: style, - ), - proc: () { - changeSocks5Proxy(); - }, - dismissOnClicked: true, - ), - MenuEntryDivider(), - enabledEntry('Enable Service', 'stop-service'), - enabledEntry('Always connected via relay', 'allow-always-relay'), - // FIXME: is this option correct? - enabledEntry('Start ID/relay service', 'stop-rendezvous-service'), - MenuEntryDivider(), - userName.isEmpty - ? MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - translate('Login'), - style: style, - ), - proc: () { - login(); - }, - dismissOnClicked: true, - ) - : MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - translate('Logout'), - style: style, - ), - proc: () { - logOut(); - }, - dismissOnClicked: true, - ), - MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - translate('Change ID'), - style: style, - ), - proc: () { - changeId(); - }, - dismissOnClicked: true, - ), - MenuEntryDivider(), - enabledEntry('Dark Theme', 'allow-darktheme'), - MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - translate('About'), - style: style, - ), - proc: () { - about(); - }, - dismissOnClicked: true, - ), - ]; - - await mod_menu.showMenu( - context: context, - position: position, - items: menu - .map((e) => e.build( - context, - const MenuConfig( - commonColor: _PopupMenuTheme.commonColor, - height: _PopupMenuTheme.height, - dividerHeight: _PopupMenuTheme.dividerHeight))) - .expand((i) => i) - .toList()); - } - Widget buildPopupMenu(BuildContext context) { - RelativeRect position = const RelativeRect.fromLTRB(0.0, 0.0, 0.0, 0.0); + var position; RxBool hover = false.obs; return InkWell( onTapDown: (detail) { @@ -353,7 +171,83 @@ class _DesktopHomePageState extends State position = RelativeRect.fromLTRB(x, y, x, y); }, onTap: () async { - await _popupMenu(context, position); + final userName = await gFFI.userModel.getUserName(); + final enabledInput = await bind.mainGetOption(key: 'enable-audio'); + final defaultInput = await gFFI.getDefaultAudioInput(); + var menu = [ + await genEnablePopupMenuItem( + translate("Enable Keyboard/Mouse"), + 'enable-keyboard', + ), + await genEnablePopupMenuItem( + translate("Enable Clipboard"), + 'enable-clipboard', + ), + await genEnablePopupMenuItem( + translate("Enable File Transfer"), + 'enable-file-transfer', + ), + await genEnablePopupMenuItem( + translate("Enable TCP Tunneling"), + 'enable-tunnel', + ), + genAudioInputPopupMenuItem(enabledInput != "N", defaultInput), + PopupMenuDivider(), + PopupMenuItem( + child: Text(translate("ID/Relay Server")), + value: 'custom-server', + ), + PopupMenuItem( + child: Text(translate("IP Whitelisting")), + value: 'whitelist', + ), + PopupMenuItem( + child: Text(translate("Socks5 Proxy")), + value: 'socks5-proxy', + ), + PopupMenuDivider(), + await genEnablePopupMenuItem( + translate("Enable Service"), + 'stop-service', + ), + // TODO: direct server + await genEnablePopupMenuItem( + translate("Always connected via relay"), + 'allow-always-relay', + ), + await genEnablePopupMenuItem( + translate("Start ID/relay service"), + 'stop-rendezvous-service', + ), + PopupMenuDivider(), + userName.isEmpty + ? PopupMenuItem( + child: Text(translate("Login")), + value: 'login', + ) + : PopupMenuItem( + child: Text("${translate("Logout")} $userName"), + value: 'logout', + ), + PopupMenuItem( + child: Text(translate("Change ID")), + value: 'change-id', + ), + PopupMenuDivider(), + await genEnablePopupMenuItem( + translate("Dark Theme"), + 'allow-darktheme', + ), + PopupMenuItem( + child: Text(translate("About")), + value: 'about', + ), + ]; + final v = + await showMenu(context: context, position: position, items: menu); + if (v != null) { + onSelectMenu(v); + } }, child: Obx( () => CircleAvatar( @@ -435,18 +329,19 @@ class _DesktopHomePageState extends State onTap: () => bind.mainUpdateTemporaryPassword(), onHover: (value) => refreshHover.value = value, ), - FutureBuilder( - future: buildPasswordPopupMenu(context), - builder: (context, snapshot) { - if (snapshot.hasError) { - print("${snapshot.error}"); - } - if (snapshot.hasData) { - return snapshot.data!; - } else { - return Offstage(); - } - }) + const _PasswordPopupMenu(), + // FutureBuilder( + // future: buildPasswordPopupMenu(context), + // builder: (context, snapshot) { + // if (snapshot.hasError) { + // print("${snapshot.error}"); + // } + // if (snapshot.hasData) { + // return snapshot.data!; + // } else { + // return Offstage(); + // } + // }) ], ), ], @@ -479,7 +374,7 @@ class _DesktopHomePageState extends State ), ], ), - onTap: () => gFFI.serverModel.verificationMethod = value, + onTap: () => gFFI.serverModel.setVerificationMethod(value), ); final temporary_enabled = gFFI.serverModel.verificationMethod != kUsePermanentPassword; @@ -516,8 +411,11 @@ class _DesktopHomePageState extends State onTap: () { if (gFFI.serverModel.temporaryPasswordLength != e) { - gFFI.serverModel.temporaryPasswordLength = e; - bind.mainUpdateTemporaryPassword(); + () async { + await gFFI.serverModel + .setTemporaryPasswordLength(e); + await bind.mainUpdateTemporaryPassword(); + }(); } }, )) @@ -1148,3 +1046,120 @@ void setPasswordDialog() async { ); }); } + +class _PasswordPopupMenu extends StatefulWidget { + const _PasswordPopupMenu({Key? key}) : super(key: key); + + @override + State<_PasswordPopupMenu> createState() => _PasswordPopupMenuState(); +} + +class _PasswordPopupMenuState extends State<_PasswordPopupMenu> { + final RxBool _tempEnabled = true.obs; + final RxBool _permEnabled = true.obs; + + List> _buildMenus() { + return >[ + MenuEntryRadios( + text: translate('Password type'), + optionsGetter: () => [ + MenuEntryRadioOption( + text: translate('Use temporary password'), + value: kUseTemporaryPassword), + MenuEntryRadioOption( + text: translate('Use permanent password'), + value: kUsePermanentPassword), + MenuEntryRadioOption( + text: translate('Use both passwords'), + value: kUseBothPasswords), + ], + curOptionGetter: () async { + return gFFI.serverModel.verificationMethod; + }, + optionSetter: (String oldValue, String newValue) async { + await bind.mainSetOption( + key: "verification-method", value: newValue); + await gFFI.serverModel.updatePasswordModel(); + setState(() { + _tempEnabled.value = + gFFI.serverModel.verificationMethod != kUsePermanentPassword; + _permEnabled.value = + gFFI.serverModel.verificationMethod != kUseTemporaryPassword; + }); + }), + MenuEntryDivider(), + MenuEntryButton( + enabled: _permEnabled, + childBuilder: (TextStyle? style) => Text( + translate('Set permanent password'), + style: style, + ), + proc: () { + setPasswordDialog(); + }, + dismissOnClicked: true, + ), + MenuEntrySubMenu( + enabled: _tempEnabled, + text: translate('Set temporary password length'), + entries: [ + MenuEntryRadios( + enabled: _tempEnabled, + text: translate(''), + optionsGetter: () => [ + MenuEntryRadioOption( + text: translate('6'), + value: '6', + enabled: _tempEnabled, + ), + MenuEntryRadioOption( + text: translate('8'), + value: '8', + enabled: _tempEnabled, + ), + MenuEntryRadioOption( + text: translate('10'), + value: '10', + enabled: _tempEnabled, + ), + ], + curOptionGetter: () async { + return gFFI.serverModel.temporaryPasswordLength; + }, + optionSetter: (String oldValue, String newValue) async { + if (oldValue != newValue) { + await gFFI.serverModel.setTemporaryPasswordLength(newValue); + await gFFI.serverModel.updatePasswordModel(); + } + }), + ]) + ]; + } + + @override + Widget build(BuildContext context) { + final editHover = false.obs; + return mod_menu.PopupMenuButton( + padding: EdgeInsets.zero, + onHover: (v) => editHover.value = v, + tooltip: translate(''), + position: mod_menu.PopupMenuPosition.overSide, + itemBuilder: (BuildContext context) => _buildMenus() + .map((entry) => entry.build( + context, + const MenuConfig( + commonColor: _MenubarTheme.commonColor, + height: _MenubarTheme.height, + dividerHeight: _MenubarTheme.dividerHeight, + ))) + .expand((i) => i) + .toList(), + child: Obx(() => Icon(Icons.edit, + size: 22, + color: editHover.value + ? MyTheme.color(context).text + : const Color(0xFFDDDDDD)) + .marginOnly(bottom: 2)), + ); + } +} diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 35383a7e0..48fc0a5e7 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -313,8 +313,8 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { translate("Use permanent password"), translate("Use both passwords"), ]; - bool tmp_enabled = model.verificationMethod != kUsePermanentPassword; - bool perm_enabled = model.verificationMethod != kUseTemporaryPassword; + bool tmpEnabled = model.verificationMethod != kUsePermanentPassword; + bool permEnabled = model.verificationMethod != kUseTemporaryPassword; String currentValue = values[keys.indexOf(model.verificationMethod)]; List radios = values .map((value) => _Radio( @@ -323,16 +323,24 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { groupValue: currentValue, label: value, onChanged: ((value) { - model.verificationMethod = keys[values.indexOf(value)]; + () async { + await model + .setVerificationMethod(keys[values.indexOf(value)]); + await model.updatePasswordModel(); + }(); }), enabled: !locked, )) .toList(); - var onChanged = tmp_enabled && !locked + var onChanged = tmpEnabled && !locked ? (value) { - if (value != null) - model.temporaryPasswordLength = value.toString(); + if (value != null) { + () async { + await model.setTemporaryPasswordLength(value.toString()); + await model.updatePasswordModel(); + }(); + } } : null; List lengthRadios = ['6', '8', '10'] @@ -364,10 +372,10 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { ...lengthRadios, ], ), - enabled: tmp_enabled && !locked), + enabled: tmpEnabled && !locked), radios[1], _SubButton('Set permanent password', setPasswordDialog, - perm_enabled && !locked), + permEnabled && !locked), radios[2], ]); }))); diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 9b0ce66c2..3a34b44ef 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -42,6 +42,7 @@ class _RemotePageState extends State String _value = ''; final _cursorOverImage = false.obs; late RxBool _showRemoteCursor; + late RxBool _remoteCursorMoved; late RxBool _keyboardEnabled; final FocusNode _mobileFocusNode = FocusNode(); @@ -61,8 +62,10 @@ class _RemotePageState extends State CurrentDisplayState.init(id); KeyboardEnabledState.init(id); ShowRemoteCursorState.init(id); + RemoteCursorMovedState.init(id); _showRemoteCursor = ShowRemoteCursorState.find(id); _keyboardEnabled = KeyboardEnabledState.find(id); + _remoteCursorMoved = RemoteCursorMovedState.find(id); } void _removeStates(String id) { @@ -71,6 +74,7 @@ class _RemotePageState extends State CurrentDisplayState.delete(id); ShowRemoteCursorState.delete(id); KeyboardEnabledState.delete(id); + RemoteCursorMovedState.delete(id); } @override @@ -396,6 +400,7 @@ class _RemotePageState extends State id: widget.id, cursorOverImage: _cursorOverImage, keyboardEnabled: _keyboardEnabled, + remoteCursorMoved: _remoteCursorMoved, listenerBuilder: _buildImageListener, ); })) @@ -460,6 +465,7 @@ class ImagePaint extends StatelessWidget { final String id; final Rx cursorOverImage; final Rx keyboardEnabled; + final Rx remoteCursorMoved; final Widget Function(Widget)? listenerBuilder; final ScrollController _horizontal = ScrollController(); final ScrollController _vertical = ScrollController(); @@ -469,6 +475,7 @@ class ImagePaint extends StatelessWidget { required this.id, required this.cursorOverImage, required this.keyboardEnabled, + required this.remoteCursorMoved, this.listenerBuilder}) : super(key: key); @@ -476,6 +483,7 @@ class ImagePaint extends StatelessWidget { Widget build(BuildContext context) { final m = Provider.of(context); var c = Provider.of(context); + final cursor = Provider.of(context); final s = c.scale; if (c.scrollStyle == ScrollStyle.scrollbar) { final imageWidget = SizedBox( @@ -501,12 +509,16 @@ class ImagePaint extends StatelessWidget { return false; }, child: Obx(() => MouseRegion( - // cursor: (keyboardEnabled.isTrue && cursorOverImage.isTrue) - // ? SystemMouseCursors.none - // : MouseCursor.defer, - /// cursor: MouseCursor.defer, - cursor: FlutterCustomCursor( - path: "assets/pencil.png", x: 1.0, y: 8.0), + cursor: (cursorOverImage.isTrue && keyboardEnabled.isTrue) + ? (remoteCursorMoved.isTrue + ? SystemMouseCursors.none + : FlutterCustomMemoryImageCursor( + pixbuf: cursor.rgba!, + hotx: cursor.hotx, + hoty: cursor.hoty, + imageWidth: (cursor.image!.width * s).toInt(), + imageHeight: (cursor.image!.height * s).toInt())) + : MouseCursor.defer, onHover: (evt) { pos.value = evt.position; }, diff --git a/flutter/lib/desktop/widgets/material_mod_popup_menu.dart b/flutter/lib/desktop/widgets/material_mod_popup_menu.dart index a9aec932b..8b0acba9a 100644 --- a/flutter/lib/desktop/widgets/material_mod_popup_menu.dart +++ b/flutter/lib/desktop/widgets/material_mod_popup_menu.dart @@ -1031,6 +1031,7 @@ class PopupMenuButton extends StatefulWidget { Key? key, required this.itemBuilder, this.initialValue, + this.onHover, this.onSelected, this.onCanceled, this.tooltip, @@ -1061,6 +1062,9 @@ class PopupMenuButton extends StatefulWidget { /// The value of the menu item, if any, that should be highlighted when the menu opens. final T? initialValue; + /// Called when the user hovers this button. + final ValueChanged? onHover; + /// Called when the user selects a value from the popup menu created by this button. /// /// If the popup menu is dismissed without selecting a value, [onCanceled] is @@ -1273,18 +1277,20 @@ class PopupMenuButtonState extends State> { assert(debugCheckHasMaterialLocalizations(context)); - if (widget.child != null) + if (widget.child != null) { return Tooltip( message: widget.tooltip ?? MaterialLocalizations.of(context).showMenuTooltip, child: InkWell( onTap: widget.enabled ? showButtonMenu : null, + onHover: widget.onHover, canRequestFocus: _canRequestFocus, radius: widget.splashRadius, enableFeedback: enableFeedback, child: widget.child, ), ); + } return IconButton( icon: widget.icon ?? Icon(Icons.adaptive.more), diff --git a/flutter/lib/desktop/widgets/popup_menu.dart b/flutter/lib/desktop/widgets/popup_menu.dart index 02e3512df..0d469043f 100644 --- a/flutter/lib/desktop/widgets/popup_menu.dart +++ b/flutter/lib/desktop/widgets/popup_menu.dart @@ -12,7 +12,7 @@ class PopupMenuChildrenItem extends mod_menu.PopupMenuEntry { key, this.height = kMinInteractiveDimension, this.padding, - this.enable = true, + this.enabled, this.textStyle, this.onTap, this.position = mod_menu.PopupMenuPosition.overSide, @@ -25,7 +25,7 @@ class PopupMenuChildrenItem extends mod_menu.PopupMenuEntry { final Offset offset; final TextStyle? textStyle; final EdgeInsets? padding; - final bool enable; + final RxBool? enabled; final void Function()? onTap; final List> Function(BuildContext) itemBuilder; final Widget child; @@ -56,25 +56,27 @@ class MyPopupMenuItemState> TextStyle style = widget.textStyle ?? popupMenuTheme.textStyle ?? theme.textTheme.subtitle1!; - - return mod_menu.PopupMenuButton( - enabled: widget.enable, - position: widget.position, - offset: widget.offset, - onSelected: handleTap, - itemBuilder: widget.itemBuilder, - padding: EdgeInsets.zero, - child: AnimatedDefaultTextStyle( - style: style, - duration: kThemeChangeDuration, - child: Container( - alignment: AlignmentDirectional.centerStart, - constraints: BoxConstraints(minHeight: widget.height), - padding: widget.padding ?? const EdgeInsets.symmetric(horizontal: 16), - child: widget.child, + return Obx(() { + return mod_menu.PopupMenuButton( + enabled: widget.enabled != null ? widget.enabled!.value : true, + position: widget.position, + offset: widget.offset, + onSelected: handleTap, + itemBuilder: widget.itemBuilder, + padding: EdgeInsets.zero, + child: AnimatedDefaultTextStyle( + style: style, + duration: kThemeChangeDuration, + child: Container( + alignment: AlignmentDirectional.centerStart, + constraints: BoxConstraints(minHeight: widget.height), + padding: + widget.padding ?? const EdgeInsets.symmetric(horizontal: 16), + child: widget.child, + ), ), - ), - ); + ); + }); } } @@ -98,8 +100,12 @@ class MenuConfig { abstract class MenuEntryBase { bool dismissOnClicked; + RxBool? enabled; - MenuEntryBase({this.dismissOnClicked = false}); + MenuEntryBase({ + this.dismissOnClicked = false, + this.enabled, + }); List> build(BuildContext context, MenuConfig conf); } @@ -119,9 +125,14 @@ class MenuEntryRadioOption { String text; String value; bool dismissOnClicked; + RxBool? enabled; - MenuEntryRadioOption( - {required this.text, required this.value, this.dismissOnClicked = false}); + MenuEntryRadioOption({ + required this.text, + required this.value, + this.dismissOnClicked = false, + this.enabled, + }); } typedef RadioOptionsGetter = List Function(); @@ -138,13 +149,14 @@ class MenuEntryRadios extends MenuEntryBase { final RadioOptionSetter optionSetter; final RxString _curOption = "".obs; - MenuEntryRadios( - {required this.text, - required this.optionsGetter, - required this.curOptionGetter, - required this.optionSetter, - dismissOnClicked = false}) - : super(dismissOnClicked: dismissOnClicked) { + MenuEntryRadios({ + required this.text, + required this.optionsGetter, + required this.curOptionGetter, + required this.optionSetter, + dismissOnClicked = false, + RxBool? enabled, + }) : super(dismissOnClicked: dismissOnClicked, enabled: enabled) { () async { _curOption.value = await curOptionGetter(); }(); @@ -220,13 +232,17 @@ class MenuEntrySubRadios extends MenuEntryBase { final RadioOptionSetter optionSetter; final RxString _curOption = "".obs; - MenuEntrySubRadios( - {required this.text, - required this.optionsGetter, - required this.curOptionGetter, - required this.optionSetter, - dismissOnClicked = false}) - : super(dismissOnClicked: dismissOnClicked) { + MenuEntrySubRadios({ + required this.text, + required this.optionsGetter, + required this.curOptionGetter, + required this.optionSetter, + dismissOnClicked = false, + RxBool? enabled, + }) : super( + dismissOnClicked: dismissOnClicked, + enabled: enabled, + ) { () async { _curOption.value = await curOptionGetter(); }(); @@ -293,6 +309,7 @@ class MenuEntrySubRadios extends MenuEntryBase { BuildContext context, MenuConfig conf) { return [ PopupMenuChildrenItem( + enabled: super.enabled, padding: EdgeInsets.zero, height: conf.height, itemBuilder: (BuildContext context) => @@ -327,9 +344,12 @@ abstract class MenuEntrySwitchBase extends MenuEntryBase { final String text; final Rx? textStyle; - MenuEntrySwitchBase( - {required this.text, required dismissOnClicked, this.textStyle}) - : super(dismissOnClicked: dismissOnClicked); + MenuEntrySwitchBase({ + required this.text, + required dismissOnClicked, + this.textStyle, + RxBool? enabled, + }) : super(dismissOnClicked: dismissOnClicked, enabled: enabled); RxBool get curOption; Future setOption(bool option); @@ -395,16 +415,19 @@ class MenuEntrySwitch extends MenuEntrySwitchBase { final SwitchSetter setter; final RxBool _curOption = false.obs; - MenuEntrySwitch( - {required String text, - required this.getter, - required this.setter, - Rx? textStyle, - dismissOnClicked = false}) - : super( - text: text, - textStyle: textStyle, - dismissOnClicked: dismissOnClicked) { + MenuEntrySwitch({ + required String text, + required this.getter, + required this.setter, + Rx? textStyle, + dismissOnClicked = false, + RxBool? enabled, + }) : super( + text: text, + textStyle: textStyle, + dismissOnClicked: dismissOnClicked, + enabled: enabled, + ) { () async { _curOption.value = await getter(); }(); @@ -429,13 +452,14 @@ class MenuEntrySwitch2 extends MenuEntrySwitchBase { final Switch2Getter getter; final SwitchSetter setter; - MenuEntrySwitch2( - {required String text, - required this.getter, - required this.setter, - Rx? textStyle, - dismissOnClicked = false}) - : super( + MenuEntrySwitch2({ + required String text, + required this.getter, + required this.setter, + Rx? textStyle, + dismissOnClicked = false, + RxBool? enabled, + }) : super( text: text, textStyle: textStyle, dismissOnClicked: dismissOnClicked); @@ -452,13 +476,18 @@ class MenuEntrySubMenu extends MenuEntryBase { final String text; final List> entries; - MenuEntrySubMenu({required this.text, required this.entries}); + MenuEntrySubMenu({ + required this.text, + required this.entries, + RxBool? enabled, + }) : super(enabled: enabled); @override List> build( BuildContext context, MenuConfig conf) { return [ PopupMenuChildrenItem( + enabled: super.enabled, height: conf.height, padding: EdgeInsets.zero, position: mod_menu.PopupMenuPosition.overSide, @@ -468,20 +497,24 @@ class MenuEntrySubMenu extends MenuEntryBase { .toList(), child: Row(children: [ const SizedBox(width: MenuConfig.midPadding), - Text( - text, - style: TextStyle( - color: MyTheme.color(context).text, - fontSize: MenuConfig.fontSize, - fontWeight: FontWeight.normal), - ), + Obx(() => Text( + text, + style: TextStyle( + color: (super.enabled != null ? super.enabled!.value : true) + ? Colors.black + : Colors.grey, + fontSize: MenuConfig.fontSize, + fontWeight: FontWeight.normal), + )), Expanded( child: Align( alignment: Alignment.centerRight, - child: Icon( - Icons.keyboard_arrow_right, - color: conf.commonColor, - ), + child: Obx(() => Icon( + Icons.keyboard_arrow_right, + color: (super.enabled != null ? super.enabled!.value : true) + ? conf.commonColor + : Colors.grey, + )), )) ]), ) @@ -493,36 +526,57 @@ class MenuEntryButton extends MenuEntryBase { final Widget Function(TextStyle? style) childBuilder; Function() proc; - MenuEntryButton( - {required this.childBuilder, - required this.proc, - dismissOnClicked = false}) - : super(dismissOnClicked: dismissOnClicked); + MenuEntryButton({ + required this.childBuilder, + required this.proc, + dismissOnClicked = false, + RxBool? enabled, + }) : super( + dismissOnClicked: dismissOnClicked, + enabled: enabled, + ); + + Widget _buildChild(BuildContext context, MenuConfig conf) { + return Obx(() { + bool enabled = true; + if (super.enabled != null) { + enabled = super.enabled!.value; + } + const enabledStyle = TextStyle( + color: Colors.black, + fontSize: MenuConfig.fontSize, + fontWeight: FontWeight.normal); + const disabledStyle = TextStyle( + color: Colors.grey, + fontSize: MenuConfig.fontSize, + fontWeight: FontWeight.normal); + return TextButton( + onPressed: enabled + ? () { + if (super.dismissOnClicked && Navigator.canPop(context)) { + Navigator.pop(context); + } + proc(); + } + : null, + child: Container( + alignment: AlignmentDirectional.centerStart, + constraints: BoxConstraints(minHeight: conf.height), + child: childBuilder(enabled ? enabledStyle : disabledStyle), + ), + ); + }); + } @override List> build( BuildContext context, MenuConfig conf) { return [ mod_menu.PopupMenuItem( + enabled: super.enabled != null ? super.enabled!.value : true, padding: EdgeInsets.zero, height: conf.height, - child: TextButton( - child: Container( - alignment: AlignmentDirectional.centerStart, - constraints: BoxConstraints(minHeight: conf.height), - child: childBuilder( - TextStyle( - color: MyTheme.color(context).text, - fontSize: MenuConfig.fontSize, - fontWeight: FontWeight.normal), - )), - onPressed: () { - if (super.dismissOnClicked && Navigator.canPop(context)) { - Navigator.pop(context); - } - proc(); - }, - ), + child: _buildChild(context, conf), ) ]; } diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 7d8cdc203..51c0b4225 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -54,7 +54,7 @@ class FfiModel with ChangeNotifier { bool get touchMode => _touchMode; - bool get isPeerAndroid => _pi.platform == "Android"; + bool get isPeerAndroid => _pi.platform == 'Android'; set inputBlocked(v) { _inputBlocked = v; @@ -116,7 +116,7 @@ class FfiModel with ChangeNotifier { return null; } else { final icon = - '${secure == true ? "secure" : "insecure"}${direct == true ? "" : "_relay"}'; + '${secure == true ? 'secure' : 'insecure'}${direct == true ? '' : '_relay'}'; return Image.asset('assets/$icon.png', width: 48, height: 48); } } @@ -143,17 +143,17 @@ class FfiModel with ChangeNotifier { } else if (name == 'cursor_id') { parent.target?.cursorModel.updateCursorId(evt); } else if (name == 'cursor_position') { - parent.target?.cursorModel.updateCursorPosition(evt); + parent.target?.cursorModel.updateCursorPosition(evt, peerId); } else if (name == 'clipboard') { Clipboard.setData(ClipboardData(text: evt['content'])); } else if (name == 'permission') { parent.target?.ffiModel.updatePermission(evt, peerId); } else if (name == 'chat_client_mode') { parent.target?.chatModel - .receive(ChatModel.clientModeID, evt['text'] ?? ""); + .receive(ChatModel.clientModeID, evt['text'] ?? ''); } else if (name == 'chat_server_mode') { parent.target?.chatModel - .receive(int.parse(evt['id'] as String), evt['text'] ?? ""); + .receive(int.parse(evt['id'] as String), evt['text'] ?? ''); } else if (name == 'file_dir') { parent.target?.fileModel.receiveFileDir(evt); } else if (name == 'job_progress') { @@ -184,61 +184,7 @@ class FfiModel with ChangeNotifier { /// Bind the event listener to receive events from the Rust core. void updateEventListener(String peerId) { - cb(evt) { - var name = evt['name']; - if (name == 'msgbox') { - handleMsgBox(evt, peerId); - } else if (name == 'peer_info') { - handlePeerInfo(evt, peerId); - } else if (name == 'connection_ready') { - parent.target?.ffiModel.setConnectionType( - peerId, evt['secure'] == 'true', evt['direct'] == 'true'); - } else if (name == 'switch_display') { - handleSwitchDisplay(evt); - } else if (name == 'cursor_data') { - parent.target?.cursorModel.updateCursorData(evt); - } else if (name == 'cursor_id') { - parent.target?.cursorModel.updateCursorId(evt); - } else if (name == 'cursor_position') { - parent.target?.cursorModel.updateCursorPosition(evt); - } else if (name == 'clipboard') { - Clipboard.setData(ClipboardData(text: evt['content'])); - } else if (name == 'permission') { - parent.target?.ffiModel.updatePermission(evt, peerId); - } else if (name == 'chat_client_mode') { - parent.target?.chatModel - .receive(ChatModel.clientModeID, evt['text'] ?? ""); - } else if (name == 'chat_server_mode') { - parent.target?.chatModel - .receive(int.parse(evt['id'] as String), evt['text'] ?? ""); - } else if (name == 'file_dir') { - parent.target?.fileModel.receiveFileDir(evt); - } else if (name == 'job_progress') { - parent.target?.fileModel.tryUpdateJobProgress(evt); - } else if (name == 'job_done') { - parent.target?.fileModel.jobDone(evt); - } else if (name == 'job_error') { - parent.target?.fileModel.jobError(evt); - } else if (name == 'override_file_confirm') { - parent.target?.fileModel.overrideFileConfirm(evt); - } else if (name == 'load_last_job') { - parent.target?.fileModel.loadLastJob(evt); - } else if (name == 'update_folder_files') { - parent.target?.fileModel.updateFolderFiles(evt); - } else if (name == 'add_connection') { - parent.target?.serverModel.addConnection(evt); - } else if (name == 'on_client_remove') { - parent.target?.serverModel.onClientRemove(evt); - } else if (name == 'update_quality_status') { - parent.target?.qualityMonitorModel.updateQualityStatus(evt); - } else if (name == 'update_block_input_state') { - updateBlockInputState(evt, peerId); - } else if (name == 'update_privacy_mode') { - updatePrivacyMode(evt, peerId); - } - } - - platformFFI.setEventCallback(cb); + platformFFI.setEventCallback(startEventListener(peerId)); } void handleSwitchDisplay(Map evt) { @@ -249,8 +195,9 @@ class FfiModel with ChangeNotifier { _display.y = double.parse(evt['y']); _display.width = int.parse(evt['width']); _display.height = int.parse(evt['height']); - if (old != _pi.currentDisplay) + if (old != _pi.currentDisplay) { parent.target?.cursorModel.updateDisplayOrigin(_display.x, _display.y); + } // remote is mobile, and orientation changed if ((_display.width > _display.height) != oldOrientation) { @@ -307,7 +254,7 @@ class FfiModel with ChangeNotifier { _pi.username = evt['username']; _pi.hostname = evt['hostname']; _pi.platform = evt['platform']; - _pi.sasEnabled = evt['sas_enabled'] == "true"; + _pi.sasEnabled = evt['sas_enabled'] == 'true'; _pi.currentDisplay = int.parse(evt['current_display']); try { @@ -323,7 +270,7 @@ class FfiModel with ChangeNotifier { } } else { _touchMode = - await bind.sessionGetOption(id: peerId, arg: "touch-mode") != ''; + await bind.sessionGetOption(id: peerId, arg: 'touch-mode') != ''; } if (parent.target != null && @@ -381,7 +328,7 @@ class ImageModel with ChangeNotifier { ui.Image? get image => _image; - String _id = ""; + String _id = ''; WeakReference parent; @@ -426,7 +373,7 @@ class ImageModel with ChangeNotifier { } Future.delayed(Duration(milliseconds: 1), () { if (parent.target?.ffiModel.isPeerAndroid ?? false) { - bind.sessionPeerOption(id: _id, name: "view-style", value: "shrink"); + bind.sessionPeerOption(id: _id, name: 'view-style', value: 'shrink'); parent.target?.canvasModel.updateViewStyle(); } }); @@ -471,7 +418,7 @@ class CanvasModel with ChangeNotifier { // the tabbar over the image double tabBarHeight = 0.0; // TODO multi canvas model - String id = ""; + String id = ''; // scroll offset x percent double _scrollX = 0.0; // scroll offset y percent @@ -580,9 +527,16 @@ class CanvasModel with ChangeNotifier { } // If keyboard is not permitted, do not move cursor when mouse is moving. - if (parent.target != null) { - if (parent.target!.ffiModel.keyboard()) { + if (parent.target != null && parent.target!.ffiModel.keyboard()) { + // Draw cursor if is not desktop. + if (!isDesktop) { parent.target!.cursorModel.moveLocal(x, y); + } else { + try { + RemoteCursorMovedState.find(id).value = false; + } catch (e) { + // + } } } } @@ -641,17 +595,19 @@ class CanvasModel with ChangeNotifier { class CursorModel with ChangeNotifier { ui.Image? _image; - final _images = >{}; + Uint8List? _rgba; + final _images = >{}; double _x = -10000; double _y = -10000; double _hotx = 0; double _hoty = 0; double _displayOriginX = 0; double _displayOriginY = 0; - String id = ""; // TODO multi cursor model + String id = ''; // TODO multi cursor model WeakReference parent; ui.Image? get image => _image; + Uint8List? get rgba => _rgba; double get x => _x - _displayOriginX; @@ -803,7 +759,8 @@ class CursorModel with ChangeNotifier { (image) { if (parent.target?.id != pid) return; _image = image; - _images[id] = Tuple3(image, _hotx, _hoty); + _rgba = rgba; + _images[id] = Tuple4(rgba, image, _hotx, _hoty); try { // my throw exception, because the listener maybe already dispose notifyListeners(); @@ -816,17 +773,23 @@ class CursorModel with ChangeNotifier { void updateCursorId(Map evt) { final tmp = _images[int.parse(evt['id'])]; if (tmp != null) { - _image = tmp.item1; - _hotx = tmp.item2; - _hoty = tmp.item3; + _rgba = tmp.item1; + _image = tmp.item2; + _hotx = tmp.item3; + _hoty = tmp.item4; notifyListeners(); } } /// Update the cursor position. - void updateCursorPosition(Map evt) { + void updateCursorPosition(Map evt, String id) { _x = double.parse(evt['x']); _y = double.parse(evt['y']); + try { + RemoteCursorMovedState.find(id).value = false; + } catch (e) { + // + } notifyListeners(); } @@ -888,13 +851,15 @@ class QualityMonitorModel with ChangeNotifier { updateQualityStatus(Map evt) { try { - if ((evt["speed"] as String).isNotEmpty) _data.speed = evt["speed"]; - if ((evt["fps"] as String).isNotEmpty) _data.fps = evt["fps"]; - if ((evt["delay"] as String).isNotEmpty) _data.delay = evt["delay"]; - if ((evt["target_bitrate"] as String).isNotEmpty) - _data.targetBitrate = evt["target_bitrate"]; - if ((evt["codec_format"] as String).isNotEmpty) - _data.codecFormat = evt["codec_format"]; + if ((evt['speed'] as String).isNotEmpty) _data.speed = evt['speed']; + if ((evt['fps'] as String).isNotEmpty) _data.fps = evt['fps']; + if ((evt['delay'] as String).isNotEmpty) _data.delay = evt['delay']; + if ((evt['target_bitrate'] as String).isNotEmpty) { + _data.targetBitrate = evt['target_bitrate']; + } + if ((evt['codec_format'] as String).isNotEmpty) { + _data.codecFormat = evt['codec_format']; + } notifyListeners(); } catch (e) {} } @@ -907,11 +872,11 @@ extension ToString on MouseButtons { String get value { switch (this) { case MouseButtons.left: - return "left"; + return 'left'; case MouseButtons.right: - return "right"; + return 'right'; case MouseButtons.wheel: - return "wheel"; + return 'wheel'; } } } @@ -920,12 +885,12 @@ enum ConnType { defaultConn, fileTransfer, portForward, rdp } /// FFI class for communicating with the Rust core. class FFI { - var id = ""; + var id = ''; var shift = false; var ctrl = false; var alt = false; var command = false; - var version = ""; + var version = ''; var connType = ConnType.defaultConn; /// dialogManager use late to ensure init after main page binding [globalKey] @@ -1006,11 +971,11 @@ class FFI { // out['name'] = name; // // default: down = false // if (down == true) { - // out['down'] = "true"; + // out['down'] = 'true'; // } // // default: press = true // if (press != false) { - // out['press'] = "true"; + // out['press'] = 'true'; // } // setByName('input_key', json.encode(modify(out))); // TODO id @@ -1038,7 +1003,7 @@ class FFI { Future> peers() async { try { var str = await bind.mainGetRecentPeers(); - if (str == "") return []; + if (str == '') return []; List peers = json.decode(str); return peers .map((s) => s as List) @@ -1056,7 +1021,7 @@ class FFI { {bool isFileTransfer = false, bool isPortForward = false, double tabBarHeight = 0.0}) { - assert(!(isFileTransfer && isPortForward), "more than one connect type"); + assert(!(isFileTransfer && isPortForward), 'more than one connect type'); if (isFileTransfer) { connType = ConnType.fileTransfer; id = 'ft_$id'; @@ -1108,13 +1073,13 @@ class FFI { canvasModel.y, canvasModel.scale, ffiModel.pi.currentDisplay); } bind.sessionClose(id: id); - id = ""; + id = ''; imageModel.update(null, 0.0); cursorModel.clear(); ffiModel.clear(); canvasModel.clear(); resetModifiers(); - debugPrint("model $id closed"); + debugPrint('model $id closed'); } /// Send **get** command to the Rust core based on [name] and [arg]. @@ -1221,7 +1186,7 @@ class FFI { Future getDefaultAudioInput() async { final input = await bind.mainGetOption(key: 'audio-input'); if (input.isEmpty && Platform.isWindows) { - return "System Sound"; + return 'System Sound'; } return input; } @@ -1232,8 +1197,8 @@ class FFI { Future> getHttpHeaders() async { return { - "Authorization": - "Bearer " + await bind.mainGetLocalOption(key: "access_token") + 'Authorization': + 'Bearer ' + await bind.mainGetLocalOption(key: 'access_token') }; } } @@ -1246,10 +1211,10 @@ class Display { } class PeerInfo { - String version = ""; - String username = ""; - String hostname = ""; - String platform = ""; + String version = ''; + String username = ''; + String hostname = ''; + String platform = ''; bool sasEnabled = false; int currentDisplay = 0; List displays = []; diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index 9d921ef48..33ce7e707 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -35,7 +35,7 @@ class ServerModel with ChangeNotifier { final tabController = DesktopTabController(tabType: DesktopTabType.cm); - List _clients = []; + final List _clients = []; bool get isStart => _isStart; @@ -61,8 +61,8 @@ class ServerModel with ChangeNotifier { return _verificationMethod; } - set verificationMethod(String method) { - bind.mainSetOption(key: "verification-method", value: method); + setVerificationMethod(String method) async { + await bind.mainSetOption(key: "verification-method", value: method); } String get temporaryPasswordLength { @@ -73,8 +73,8 @@ class ServerModel with ChangeNotifier { return _temporaryPasswordLength; } - set temporaryPasswordLength(String length) { - bind.mainSetOption(key: "temporary-password-length", value: length); + setTemporaryPasswordLength(String length) async { + await bind.mainSetOption(key: "temporary-password-length", value: length); } TextEditingController get serverId => _serverId; From 1bf9700da661b6f0a0c4575e306eeac9ae2f82ca Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 7 Sep 2022 02:14:52 -0700 Subject: [PATCH 0409/2015] flutter_desktop: show cursor Signed-off-by: fufesou --- flutter/lib/desktop/pages/remote_page.dart | 20 +++++---- flutter/lib/models/model.dart | 48 +++++++++++++--------- 2 files changed, 41 insertions(+), 27 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 3a34b44ef..90c729abe 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:io'; +import 'dart:typed_data'; import 'dart:ui' as ui; import 'package:flutter/gestures.dart'; @@ -407,7 +408,7 @@ class _RemotePageState extends State ]; paints.add(Obx(() => Visibility( - visible: _keyboardEnabled.isTrue || _showRemoteCursor.isTrue, + visible: _showRemoteCursor.isTrue && _remoteCursorMoved.isTrue, child: CursorPaint( id: widget.id, )))); @@ -493,7 +494,7 @@ class ImagePaint extends StatelessWidget { painter: ImagePainter(image: m.image, x: 0, y: 0, scale: s), )); - Rx pos = Rx(Offset(0.0, 0.0)); + Rx pos = Rx(const Offset(0.0, 0.0)); return Center( child: NotificationListener( onNotification: (notification) { @@ -512,12 +513,15 @@ class ImagePaint extends StatelessWidget { cursor: (cursorOverImage.isTrue && keyboardEnabled.isTrue) ? (remoteCursorMoved.isTrue ? SystemMouseCursors.none - : FlutterCustomMemoryImageCursor( - pixbuf: cursor.rgba!, - hotx: cursor.hotx, - hoty: cursor.hoty, - imageWidth: (cursor.image!.width * s).toInt(), - imageHeight: (cursor.image!.height * s).toInt())) + : (cursor.pngData != null + ? FlutterCustomMemoryImageCursor( + pixbuf: cursor.pngData!, + hotx: cursor.hotx, + hoty: cursor.hoty, + imageWidth: (cursor.image!.width * s).toInt(), + imageHeight: (cursor.image!.height * s).toInt(), + ) + : MouseCursor.defer)) : MouseCursor.defer, onHover: (evt) { pos.value = evt.position; diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 51c0b4225..9f9899553 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'dart:math'; import 'dart:typed_data'; import 'dart:ui' as ui; +import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -595,8 +596,9 @@ class CanvasModel with ChangeNotifier { class CursorModel with ChangeNotifier { ui.Image? _image; - Uint8List? _rgba; - final _images = >{}; + final _images = >{}; + Uint8List? _pngData; + final _pngs = {}; double _x = -10000; double _y = -10000; double _hotx = 0; @@ -607,7 +609,7 @@ class CursorModel with ChangeNotifier { WeakReference parent; ui.Image? get image => _image; - Uint8List? get rgba => _rgba; + Uint8List? get pngData => _pngData; double get x => _x - _displayOriginX; @@ -757,26 +759,34 @@ class CursorModel with ChangeNotifier { var pid = parent.target?.id; ui.decodeImageFromPixels(rgba, width, height, ui.PixelFormat.rgba8888, (image) { - if (parent.target?.id != pid) return; - _image = image; - _rgba = rgba; - _images[id] = Tuple4(rgba, image, _hotx, _hoty); - try { - // my throw exception, because the listener maybe already dispose - notifyListeners(); - } catch (e) { - debugPrint('notify cursor: $e'); - } + () async { + if (parent.target?.id != pid) return; + _image = image; + _images[id] = Tuple3(image, _hotx, _hoty); + final data = await image.toByteData(format: ImageByteFormat.png); + if (data != null) { + _pngData = data.buffer.asUint8List(); + } else { + _pngData = null; + } + _pngs[id] = _pngData; + try { + // my throw exception, because the listener maybe already dispose + notifyListeners(); + } catch (e) { + debugPrint('notify cursor: $e'); + } + }(); }); } void updateCursorId(Map evt) { + _pngData = _pngs[int.parse(evt['id'])]; final tmp = _images[int.parse(evt['id'])]; if (tmp != null) { - _rgba = tmp.item1; - _image = tmp.item2; - _hotx = tmp.item3; - _hoty = tmp.item4; + _image = tmp.item1; + _hotx = tmp.item2; + _hoty = tmp.item3; notifyListeners(); } } @@ -786,7 +796,7 @@ class CursorModel with ChangeNotifier { _x = double.parse(evt['x']); _y = double.parse(evt['y']); try { - RemoteCursorMovedState.find(id).value = false; + RemoteCursorMovedState.find(id).value = true; } catch (e) { // } @@ -1011,7 +1021,7 @@ class FFI { Peer.fromJson(s[0] as String, s[1] as Map)) .toList(); } catch (e) { - print('peers(): $e'); + debugPrint('peers(): $e'); } return []; } From 09ad5e134cc575cf8d1357c1ed2f4505d26de8c7 Mon Sep 17 00:00:00 2001 From: Asura Date: Wed, 7 Sep 2022 18:04:43 +0800 Subject: [PATCH 0410/2015] doc: Add wayland instructions --- README.md | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6598dd51c..8abfbc9d3 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,9 @@ Please download sciter dynamic library yourself. ### 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 g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \ + libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake \ + libclang-dev ninja-build libayatana-appindicator3-1 libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libayatana-appindicator3-dev ``` ### Fedora 28 (CentOS 8) @@ -122,6 +124,30 @@ VCPKG_ROOT=$HOME/vcpkg cargo run RustDesk does not support Wayland. Check [this](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) to configuring Xorg as the default GNOME session. +## Wayland support + +Wayland does not seem to provide any API for sending keypresses to other windows. Therefore, the rustdesk uses an API from a lower level, namely the `/dev/uinput` device (Linux kernel level). + +When wayland is the controlled side, you have to start in the following way: +```bash +# Start uinput service +$ sudo rustdesk --service +$ rustdesk +``` +**Notice**: Wayland screen recording uses different interfaces, currently currently only supports 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 +# Not support +Error org.freedesktop.DBus.Error.InvalidArgs: No such interface “org.freedesktop.portal.ScreenCast” +# Support +method return time=1662544486.931020 sender=:1.54 -> destination=:1.139 serial=257 reply_serial=2 + variant uint32 4 +``` + ## How to build with Docker Begin by cloning the repository and building the docker container: From f91293bc12a1bc49be2b560490ef015d572d61fd Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 7 Sep 2022 22:00:42 +0800 Subject: [PATCH 0411/2015] 1.2.0 --- Cargo.toml | 2 +- flutter/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index bf5ab9669..88a7d963a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustdesk" -version = "1.1.10" +version = "1.2.0" authors = ["rustdesk "] edition = "2021" build= "build.rs" diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 1f4987e5c..601f1b94a 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # 1.1.9-1 works for android, but for ios it becomes 1.1.91, need to set it to 1.1.9-a.1 for iOS, will get 1.1.9.1, but iOS store not allow 4 numbers -version: 1.1.10-1+28 +version: 1.2.0 environment: sdk: ">=2.16.1" From e560a17d0592ab9e9cce01da05609cbfee724645 Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 7 Sep 2022 22:43:23 +0800 Subject: [PATCH 0412/2015] sciter: fix build Signed-off-by: fufesou --- src/ui.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/ui.rs b/src/ui.rs index 25ad18521..7457c2ff5 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -107,8 +107,7 @@ pub fn start(args: &mut [String]) { } #[cfg(windows)] if args.len() > 0 && args[0] == "--tray" { - let options = check_connect_status(false).1; - crate::tray::start_tray(options); + crate::tray::start_tray(crate::ui_interface::OPTIONS.clone()); return; } use sciter::SCRIPT_RUNTIME_FEATURES::*; From 9b694cbac07a703b5da93483df98cf01bbb894f2 Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 7 Sep 2022 07:06:05 -0700 Subject: [PATCH 0413/2015] flutter_desktop: cursor cache - linux Signed-off-by: fufesou --- flutter/lib/desktop/pages/remote_page.dart | 15 ++-- flutter/lib/models/model.dart | 84 ++++++++++++++++++---- flutter/pubspec.yaml | 2 +- 3 files changed, 80 insertions(+), 21 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 86372d868..cd23328a4 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -568,13 +568,16 @@ class ImagePaint extends StatelessWidget { cursor: (cursorOverImage.isTrue && keyboardEnabled.isTrue) ? (remoteCursorMoved.isTrue ? SystemMouseCursors.none - : (cursor.pngData != null + : (cursor.cacheLinux != null ? FlutterCustomMemoryImageCursor( - pixbuf: cursor.pngData!, - hotx: cursor.hotx, - hoty: cursor.hoty, - imageWidth: (cursor.image!.width * s).toInt(), - imageHeight: (cursor.image!.height * s).toInt(), + pixbuf: cursor.cacheLinux!.data, + key: cursor.cacheLinux!.key, + hotx: cursor.cacheLinux!.hotx, + hoty: cursor.cacheLinux!.hoty, + imageWidth: + (cursor.cacheLinux!.width * s).toInt(), + imageHeight: + (cursor.cacheLinux!.height * s).toInt(), ) : MouseCursor.defer)) : MouseCursor.defer, diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index c02cd1ee1..7cf54492a 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -17,6 +17,7 @@ import 'package:flutter_hbb/models/server_model.dart'; import 'package:flutter_hbb/models/user_model.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:tuple/tuple.dart'; +import 'package:flutter_custom_cursor/flutter_custom_cursor.dart'; import '../common.dart'; import '../common/shared_state.dart'; @@ -351,7 +352,7 @@ class ImageModel with ChangeNotifier { // my throw exception, because the listener maybe already dispose update(image, tabBarHeight); } catch (e) { - print('update image: $e'); + debugPrint('update image: $e'); } }); } @@ -594,11 +595,36 @@ class CanvasModel with ChangeNotifier { } } +// data for cursor +class CursorData { + final String peerId; + final int id; + final Uint8List? data; + final double hotx; + final double hoty; + final int width; + final int height; + late String key; + + CursorData({ + required this.peerId, + required this.id, + required this.data, + required this.hotx, + required this.hoty, + required this.width, + required this.height, + }) { + key = + '${peerId}_${id}_${(hotx * 10e6).round().toInt()}_${(hoty * 10e6).round().toInt()}_${width}_$height'; + } +} + class CursorModel with ChangeNotifier { ui.Image? _image; final _images = >{}; - Uint8List? _pngData; - final _pngs = {}; + CursorData? _cacheLinux; + final _cacheMapLinux = {}; double _x = -10000; double _y = -10000; double _hotx = 0; @@ -609,7 +635,7 @@ class CursorModel with ChangeNotifier { WeakReference parent; ui.Image? get image => _image; - Uint8List? get pngData => _pngData; + CursorData? get cacheLinux => _cacheLinux; double get x => _x - _displayOriginX; @@ -623,6 +649,9 @@ class CursorModel with ChangeNotifier { CursorModel(this.parent); + List get cachedKeysLinux => + _cacheMapLinux.values.map((v) => v.key).toList(); + // remote physical display coordinate Rect getVisibleRect() { final size = MediaQueryData.fromWindow(ui.window).size; @@ -763,13 +792,7 @@ class CursorModel with ChangeNotifier { if (parent.target?.id != pid) return; _image = image; _images[id] = Tuple3(image, _hotx, _hoty); - final data = await image.toByteData(format: ImageByteFormat.png); - if (data != null) { - _pngData = data.buffer.asUint8List(); - } else { - _pngData = null; - } - _pngs[id] = _pngData; + _updateCacheLinux(image, id, width, height); try { // my throw exception, because the listener maybe already dispose notifyListeners(); @@ -780,8 +803,28 @@ class CursorModel with ChangeNotifier { }); } + void _updateCacheLinux(ui.Image image, int id, int w, int h) async { + final data = await image.toByteData(format: ImageByteFormat.png); + late Uint8List? dataLinux; + if (data != null) { + dataLinux = data.buffer.asUint8List(); + } else { + dataLinux = null; + } + _cacheLinux = CursorData( + peerId: this.id, + data: dataLinux, + id: id, + hotx: _hotx, + hoty: _hoty, + width: w, + height: h, + ); + _cacheMapLinux[id] = _cacheLinux!; + } + void updateCursorId(Map evt) { - _pngData = _pngs[int.parse(evt['id'])]; + _cacheLinux = _cacheMapLinux[int.parse(evt['id'])]; final tmp = _images[int.parse(evt['id'])]; if (tmp != null) { _image = tmp.item1; @@ -828,6 +871,17 @@ class CursorModel with ChangeNotifier { _x = -10000; _image = null; _images.clear(); + + _clearCacheLinux(); + _cacheLinux = null; + _cacheMapLinux.clear(); + } + + void _clearCacheLinux() { + final cachedKeys = [...cachedKeysLinux]; + for (var key in cachedKeys) { + customCursorController.freeCache(key); + } } } @@ -871,7 +925,9 @@ class QualityMonitorModel with ChangeNotifier { _data.codecFormat = evt['codec_format']; } notifyListeners(); - } catch (e) {} + } catch (e) { + // + } } } @@ -1230,7 +1286,7 @@ class FFI { Future> getHttpHeaders() async { return { 'Authorization': - 'Bearer ' + await bind.mainGetLocalOption(key: 'access_token') + 'Bearer ${await bind.mainGetLocalOption(key: 'access_token')}' }; } } diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 601f1b94a..5556b0dd5 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -71,7 +71,7 @@ dependencies: flutter_custom_cursor: git: url: https://github.com/Kingtous/rustdesk_flutter_custom_cursor - ref: 7fe78c139c711bafbae52d924e9caf18bd193e28 + ref: 9021e21de36c84edf01d5034f38eda580463163b get: ^4.6.5 visibility_detector: ^0.3.3 contextmenu: ^3.0.0 From 41a5d53de6184c682efde6c0a13ea80c73127816 Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 7 Sep 2022 18:39:45 -0700 Subject: [PATCH 0414/2015] flutter_desktop: refactor GetX in popup menu Signed-off-by: fufesou --- flutter/lib/desktop/widgets/popup_menu.dart | 138 +++++++++--------- .../lib/desktop/widgets/remote_menubar.dart | 2 +- 2 files changed, 66 insertions(+), 74 deletions(-) diff --git a/flutter/lib/desktop/widgets/popup_menu.dart b/flutter/lib/desktop/widgets/popup_menu.dart index 0d469043f..02376ff71 100644 --- a/flutter/lib/desktop/widgets/popup_menu.dart +++ b/flutter/lib/desktop/widgets/popup_menu.dart @@ -8,7 +8,7 @@ import './material_mod_popup_menu.dart' as mod_menu; // https://stackoverflow.com/questions/68318314/flutter-popup-menu-inside-popup-menu class PopupMenuChildrenItem extends mod_menu.PopupMenuEntry { - const PopupMenuChildrenItem({ + PopupMenuChildrenItem({ key, this.height = kMinInteractiveDimension, this.padding, @@ -43,6 +43,16 @@ class PopupMenuChildrenItem extends mod_menu.PopupMenuEntry { class MyPopupMenuItemState> extends State { + RxBool enabled = true.obs; + + @override + void initState() { + super.initState(); + if (widget.enabled != null) { + enabled.value = widget.enabled!.value; + } + } + @protected void handleTap(T value) { widget.onTap?.call(); @@ -56,27 +66,25 @@ class MyPopupMenuItemState> TextStyle style = widget.textStyle ?? popupMenuTheme.textStyle ?? theme.textTheme.subtitle1!; - return Obx(() { - return mod_menu.PopupMenuButton( - enabled: widget.enabled != null ? widget.enabled!.value : true, - position: widget.position, - offset: widget.offset, - onSelected: handleTap, - itemBuilder: widget.itemBuilder, - padding: EdgeInsets.zero, - child: AnimatedDefaultTextStyle( - style: style, - duration: kThemeChangeDuration, - child: Container( - alignment: AlignmentDirectional.centerStart, - constraints: BoxConstraints(minHeight: widget.height), - padding: - widget.padding ?? const EdgeInsets.symmetric(horizontal: 16), - child: widget.child, + return Obx(() => mod_menu.PopupMenuButton( + enabled: enabled.value, + position: widget.position, + offset: widget.offset, + onSelected: handleTap, + itemBuilder: widget.itemBuilder, + padding: EdgeInsets.zero, + child: AnimatedDefaultTextStyle( + style: style, + duration: kThemeChangeDuration, + child: Container( + alignment: AlignmentDirectional.centerStart, + constraints: BoxConstraints(minHeight: widget.height), + padding: + widget.padding ?? const EdgeInsets.symmetric(horizontal: 16), + child: widget.child, + ), ), - ), - ); - }); + )); } } @@ -342,7 +350,7 @@ typedef SwitchSetter = Future Function(bool); abstract class MenuEntrySwitchBase extends MenuEntryBase { final String text; - final Rx? textStyle; + Rx? textStyle; MenuEntrySwitchBase({ required this.text, @@ -357,6 +365,11 @@ abstract class MenuEntrySwitchBase extends MenuEntryBase { @override List> build( BuildContext context, MenuConfig conf) { + textStyle ??= const TextStyle( + color: Colors.black, + fontSize: MenuConfig.fontSize, + fontWeight: FontWeight.normal) + .obs; return [ mod_menu.PopupMenuItem( padding: EdgeInsets.zero, @@ -366,23 +379,10 @@ abstract class MenuEntrySwitchBase extends MenuEntryBase { alignment: AlignmentDirectional.centerStart, height: conf.height, child: Row(children: [ - () { - if (textStyle != null) { - final style = textStyle!; - return Obx(() => Text( - text, - style: style.value, - )); - } else { - return Text( + Obx(() => Text( text, - style: const TextStyle( - color: Colors.black, - fontSize: MenuConfig.fontSize, - fontWeight: FontWeight.normal), - ); - } - }(), + style: textStyle!.value, + )), Expanded( child: Align( alignment: Alignment.centerRight, @@ -485,6 +485,7 @@ class MenuEntrySubMenu extends MenuEntryBase { @override List> build( BuildContext context, MenuConfig conf) { + super.enabled ??= true.obs; return [ PopupMenuChildrenItem( enabled: super.enabled, @@ -500,9 +501,7 @@ class MenuEntrySubMenu extends MenuEntryBase { Obx(() => Text( text, style: TextStyle( - color: (super.enabled != null ? super.enabled!.value : true) - ? Colors.black - : Colors.grey, + color: super.enabled!.value ? Colors.black : Colors.grey, fontSize: MenuConfig.fontSize, fontWeight: FontWeight.normal), )), @@ -511,9 +510,7 @@ class MenuEntrySubMenu extends MenuEntryBase { alignment: Alignment.centerRight, child: Obx(() => Icon( Icons.keyboard_arrow_right, - color: (super.enabled != null ? super.enabled!.value : true) - ? conf.commonColor - : Colors.grey, + color: super.enabled!.value ? conf.commonColor : Colors.grey, )), )) ]), @@ -537,35 +534,31 @@ class MenuEntryButton extends MenuEntryBase { ); Widget _buildChild(BuildContext context, MenuConfig conf) { - return Obx(() { - bool enabled = true; - if (super.enabled != null) { - enabled = super.enabled!.value; - } - const enabledStyle = TextStyle( - color: Colors.black, - fontSize: MenuConfig.fontSize, - fontWeight: FontWeight.normal); - const disabledStyle = TextStyle( - color: Colors.grey, - fontSize: MenuConfig.fontSize, - fontWeight: FontWeight.normal); - return TextButton( - onPressed: enabled - ? () { - if (super.dismissOnClicked && Navigator.canPop(context)) { - Navigator.pop(context); + const enabledStyle = TextStyle( + color: Colors.black, + fontSize: MenuConfig.fontSize, + fontWeight: FontWeight.normal); + const disabledStyle = TextStyle( + color: Colors.grey, + fontSize: MenuConfig.fontSize, + fontWeight: FontWeight.normal); + super.enabled ??= true.obs; + return Obx(() => TextButton( + onPressed: super.enabled!.value + ? () { + if (super.dismissOnClicked && Navigator.canPop(context)) { + Navigator.pop(context); + } + proc(); } - proc(); - } - : null, - child: Container( - alignment: AlignmentDirectional.centerStart, - constraints: BoxConstraints(minHeight: conf.height), - child: childBuilder(enabled ? enabledStyle : disabledStyle), - ), - ); - }); + : null, + child: Container( + alignment: AlignmentDirectional.centerStart, + constraints: BoxConstraints(minHeight: conf.height), + child: childBuilder( + super.enabled!.value ? enabledStyle : disabledStyle), + ), + )); } @override @@ -573,7 +566,6 @@ class MenuEntryButton extends MenuEntryBase { BuildContext context, MenuConfig conf) { return [ mod_menu.PopupMenuItem( - enabled: super.enabled != null ? super.enabled!.value : true, padding: EdgeInsets.zero, height: conf.height, child: _buildChild(context, conf), diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 2dcc81d4d..28085246f 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -611,7 +611,7 @@ class _RemoteMenubarState extends State { MenuEntryRadioOption(text: translate('Map mode'), value: 'map'), ], curOptionGetter: () async { - return await bind.sessionGetKeyboardName(id: widget.id) ?? 'legacy'; + return await bind.sessionGetKeyboardName(id: widget.id); }, optionSetter: (String oldValue, String newValue) async { await bind.sessionSetKeyboardMode( From 48481884b14404ed2751fb5d886d5d4c7cf6b457 Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 5 Sep 2022 16:01:53 +0800 Subject: [PATCH 0415/2015] fix closing PortForward page while closing msgbox Signed-off-by: 21pages --- flutter/lib/common.dart | 14 +++++++++++++- flutter/lib/main.dart | 6 ++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 309ae9892..da644c357 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -27,6 +27,7 @@ var isWeb = false; var isWebDesktop = false; var version = ""; int androidVersion = 0; +late final DesktopType? desktopType; typedef F = String Function(String); typedef FMethod = String Function(String, dynamic); @@ -42,6 +43,15 @@ late final iconFile = MemoryImage(Uint8List.fromList(base64Decode( late final iconRestart = MemoryImage(Uint8List.fromList(base64Decode( 'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAB7BAAAewQHDaVRTAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAbhJREFUWIXVlrFqFGEUhb+7UYxaWCQKlrKKxaZSQVGDJih2tj6MD2DnMwiWvoAIRnENIpZiYxEro6IooiS7SPwsMgNLkk3mjmYmnmb45/73nMNwz/x/qH3gMu2gH6rAU+Blw+Lngau4jpmGxVF7qp1iPWjaQKnZ2WnXbuP/NqAeUPc3ZkA9XDwvqc+BVWCgPlJ7tRwUKThZce819b46VH+pfXVRXVO/q2cSul3VOgZUl0ejq86r39TXI8mqZKDuDEwCw3IREQvAbWAGmMsQZQ0sAl3gHPB1Q+0e8BuYzRDuy2yOiFVgaUxtRf0ETGc4syk4rc6PqU0Cx9j8Zf6dAeAK8Fi9sUXtFjABvEgxJlNwRP2svlNPjbw/q35U36oTFbnyMSwabxb/gB/qA3VBHagrauV7RW0DRfP1IvMlXqkXkhz1DYyQTKtHa/Z2VVMx3IiI+PI3/bCHjuOpFrSnAMpL6QfgTcMGesDx0kBr2BMzsNyi/vtQu8CJlgwsRbZDnWP90NkKaxHxJMOXMqAeAn5u0ydwMCKGY+qbkB3C2W3EKWoXk5zVoHbUZ+6Mh7tl4G4F8RJ3qvL+AfV3r5Vdpj70AAAAAElFTkSuQmCC'))); +enum DesktopType { + main, + remote, + fileTransfer, + cm, + portForward, + rdp, +} + class IconFont { static const _family1 = 'Tabbar'; static const _family2 = 'PeerSearchbar'; @@ -478,7 +488,9 @@ void msgBox( submit() { dialogManager.dismissAll(); // https://github.com/fufesou/rustdesk/blob/5e9a31340b899822090a3731769ae79c6bf5f3e5/src/ui/common.tis#L263 - if (!type.contains("custom")) { + if (!type.contains("custom") && + !(desktopType == DesktopType.portForward || + desktopType == DesktopType.rdp)) { closeConnection(); } } diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 2f1d0680f..5ec2da9f8 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -43,12 +43,16 @@ Future main(List args) async { WindowType wType = type.windowType; switch (wType) { case WindowType.RemoteDesktop: + desktopType = DesktopType.remote; runRemoteScreen(argument); break; case WindowType.FileTransfer: + desktopType = DesktopType.fileTransfer; runFileTransferScreen(argument); break; case WindowType.PortForward: + desktopType = + argument['isRDP'] ? DesktopType.rdp : DesktopType.portForward; runPortForwardScreen(argument); break; default: @@ -56,9 +60,11 @@ Future main(List args) async { } } else if (args.isNotEmpty && args.first == '--cm') { print("--cm started"); + desktopType = DesktopType.cm; await windowManager.ensureInitialized(); runConnectionManagerScreen(); } else { + desktopType = DesktopType.main; await windowManager.ensureInitialized(); windowManager.setPreventClose(true); runMainApp(true); From 31550452c3957271f9cb54f736f6525bf098ca8e Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 5 Sep 2022 16:28:49 +0800 Subject: [PATCH 0416/2015] remove overlay of PortForward page because it will cause rebuilding when closing msgbox Signed-off-by: 21pages --- .../lib/desktop/pages/port_forward_page.dart | 69 +++++++++---------- 1 file changed, 31 insertions(+), 38 deletions(-) diff --git a/flutter/lib/desktop/pages/port_forward_page.dart b/flutter/lib/desktop/pages/port_forward_page.dart index 28ee0d70e..6cfd0cdb2 100644 --- a/flutter/lib/desktop/pages/port_forward_page.dart +++ b/flutter/lib/desktop/pages/port_forward_page.dart @@ -70,45 +70,38 @@ class _PortForwardPageState extends State @override Widget build(BuildContext context) { super.build(context); - return Overlay(initialEntries: [ - OverlayEntry(builder: (context) { - _ffi.dialogManager.setOverlayState(Overlay.of(context)); - return Scaffold( - backgroundColor: MyTheme.color(context).grayBg, - body: FutureBuilder(future: () async { - if (!isRdp) { - refreshTunnelConfig(); - } - }(), builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - return Container( - decoration: BoxDecoration( - border: Border.all( - width: 20, color: MyTheme.color(context).grayBg!)), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - buildPrompt(context), - Flexible( - child: Container( - decoration: BoxDecoration( - color: MyTheme.color(context).bg, - border: - Border.all(width: 1, color: MyTheme.border)), - child: widget.isRDP - ? buildRdp(context) - : buildTunnel(context), - ), - ), - ], + return Scaffold( + backgroundColor: MyTheme.color(context).grayBg, + body: FutureBuilder(future: () async { + if (!isRdp) { + refreshTunnelConfig(); + } + }(), builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + return Container( + decoration: BoxDecoration( + border: Border.all( + width: 20, color: MyTheme.color(context).grayBg!)), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + buildPrompt(context), + Flexible( + child: Container( + decoration: BoxDecoration( + color: MyTheme.color(context).bg, + border: Border.all(width: 1, color: MyTheme.border)), + child: + widget.isRDP ? buildRdp(context) : buildTunnel(context), + ), ), - ); - } - return const Offstage(); - }), - ); - }) - ]); + ], + ), + ); + } + return const Offstage(); + }), + ); } buildPrompt(BuildContext context) { From 59f82262c86a642f782ed5056329c404de307daf Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 6 Sep 2022 11:18:12 +0800 Subject: [PATCH 0417/2015] fix cm waiting page close button Signed-off-by: 21pages --- flutter/lib/desktop/pages/server_page.dart | 68 ++++++++++++---------- 1 file changed, 36 insertions(+), 32 deletions(-) diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index 08f3e5836..be14a8537 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; import 'package:flutter_hbb/mobile/pages/chat_page.dart'; import 'package:flutter_hbb/models/chat_model.dart'; @@ -63,20 +64,15 @@ class _DesktopServerPageState extends State border: Border.all(color: MyTheme.color(context).border!)), child: Scaffold( backgroundColor: MyTheme.color(context).bg, - body: Overlay(initialEntries: [ - OverlayEntry(builder: (context) { - gFFI.dialogManager.setOverlayState(Overlay.of(context)); - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Expanded(child: ConnectionManager()), - SizedBox.fromSize(size: Size(0, 15.0)), - ], - ), - ); - }) - ]), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Expanded(child: ConnectionManager()), + SizedBox.fromSize(size: Size(0, 15.0)), + ], + ), + ), )))); } @@ -111,7 +107,7 @@ class ConnectionManagerState extends State { return serverModel.clients.isEmpty ? Column( children: [ - buildTitleBar(Offstage()), + buildTitleBar(), Expanded( child: Center( child: Text(translate("Waiting")), @@ -134,20 +130,27 @@ class ConnectionManagerState extends State { ])); } - Widget buildTitleBar(Widget middle) { - return GestureDetector( - onPanDown: (d) { - windowManager.startDragging(); - }, + Widget buildTitleBar() { + return SizedBox( + height: kDesktopRemoteTabBarHeight, child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - _AppIcon(), - Expanded(child: middle), + const _AppIcon(), + Expanded( + child: GestureDetector( + onPanStart: (d) { + windowManager.startDragging(); + }, + child: Container( + color: MyTheme.color(context).bg, + ), + ), + ), const SizedBox( width: 4.0, ), - _CloseButton() + const _CloseButton() ], ), ); @@ -209,15 +212,16 @@ class _CloseButton extends StatelessWidget { @override Widget build(BuildContext context) { - return Ink( - child: InkWell( - onTap: () { - windowManager.close(); - }, - child: Icon( - Icons.close, - size: 30, - )), + return IconButton( + onPressed: () { + windowManager.close(); + }, + icon: const Icon( + IconFont.close, + size: 18, + ), + splashColor: Colors.transparent, + hoverColor: Colors.transparent, ); } } From a3c1e5ddb4301b4c76e90f9138cb94472faf4520 Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 6 Sep 2022 15:13:22 +0800 Subject: [PATCH 0418/2015] make os-password msgbox wordwrap Signed-off-by: 21pages --- src/ui/msgbox.tis | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/msgbox.tis b/src/ui/msgbox.tis index a622a45b8..b7df30717 100644 --- a/src/ui/msgbox.tis +++ b/src/ui/msgbox.tis @@ -63,7 +63,7 @@ class MsgboxComponent: Reactor.Component { var ts = this.auto_login ? { checked: true } : {}; return
    -
    {translate('Auto Login')}
    +
    {translate('Auto Login')}
    ; } return this.content; From 17a7cbf7bb18c9fa34982da70d2b3b4b0219c88f Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 6 Sep 2022 22:34:01 +0800 Subject: [PATCH 0419/2015] follow system theme at startup and changing Signed-off-by: 21pages --- flutter/lib/cm_main.dart | 2 +- flutter/lib/common.dart | 30 ++++++++++++++++ .../desktop/pages/desktop_setting_page.dart | 11 +++--- .../lib/desktop/pages/desktop_tab_page.dart | 1 - flutter/lib/main.dart | 35 +++++++++++++------ 5 files changed, 60 insertions(+), 19 deletions(-) diff --git a/flutter/lib/cm_main.dart b/flutter/lib/cm_main.dart index bf72849e8..a92edd194 100644 --- a/flutter/lib/cm_main.dart +++ b/flutter/lib/cm_main.dart @@ -25,6 +25,6 @@ void main(List args) async { .add(Client(3, false, false, "UserD", "441123123", true, false, false)); runApp(GetMaterialApp( debugShowCheckedModeBanner: false, - theme: getCurrentTheme(), + theme: MyTheme.initialTheme(), home: DesktopServerPage())); } diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index da644c357..bd16fa4c5 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -192,6 +192,28 @@ class MyTheme { ], ); + static changeTo(bool dark) { + Get.find().setString("darkTheme", dark ? "Y" : ""); + Get.changeTheme(dark ? MyTheme.darkTheme : MyTheme.lightTheme); + Get.forceAppUpdate(); + } + + static bool _themeInitialed = false; + + static ThemeData initialTheme({bool mainPage = false}) { + bool dark; + // Brightnesss is always light on windows, Flutter 3.0.5 + if (_themeInitialed || !mainPage || Platform.isWindows) { + dark = isDarkTheme(); + } else { + dark = WidgetsBinding.instance.platformDispatcher.platformBrightness == + Brightness.dark; + Get.find().setString("darkTheme", dark ? "Y" : ""); + } + _themeInitialed = true; + return dark ? MyTheme.darkTheme : MyTheme.lightTheme; + } + static ColorThemeExtension color(BuildContext context) { return Theme.of(context).extension()!; } @@ -201,6 +223,14 @@ class MyTheme { } } +class ThemeModeNotifier { + final ValueNotifier brightness; + ThemeModeNotifier(this.brightness); + changeThemeBrightness({required Brightness brightness}) { + this.brightness.value = brightness; + } +} + bool isDarkTheme() { final isDark = "Y" == Get.find().getString("darkTheme"); return isDark; diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 48fc0a5e7..a6ae89d71 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -224,21 +224,18 @@ class _UserInterfaceState extends State<_UserInterface> } Widget theme() { - var change = () { - bool dark = !isDarkTheme(); - Get.changeTheme(dark ? MyTheme.darkTheme : MyTheme.lightTheme); - Get.find().setString("darkTheme", dark ? "Y" : ""); - Get.forceAppUpdate(); - }; + change() { + MyTheme.changeTo(!isDarkTheme()); + } return GestureDetector( + onTap: change, child: Row( children: [ Checkbox(value: isDarkTheme(), onChanged: (_) => change()), Expanded(child: Text(translate('Dark Theme'))), ], ).marginOnly(left: _kCheckBoxLeftMargin), - onTap: change, ); } } diff --git a/flutter/lib/desktop/pages/desktop_tab_page.dart b/flutter/lib/desktop/pages/desktop_tab_page.dart index 87082284b..fde543a89 100644 --- a/flutter/lib/desktop/pages/desktop_tab_page.dart +++ b/flutter/lib/desktop/pages/desktop_tab_page.dart @@ -33,7 +33,6 @@ class _DesktopTabPageState extends State { @override Widget build(BuildContext context) { - final dark = isDarkTheme(); RxBool fullscreen = false.obs; Get.put(fullscreen, tag: 'fullscreen'); return Obx(() => DragToResizeArea( diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 5ec2da9f8..a25ed7027 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -71,10 +71,6 @@ Future main(List args) async { } } -ThemeData getCurrentTheme() { - return isDarkTheme() ? MyTheme.darkTheme : MyTheme.lightTheme; -} - Future initEnv(String appType) async { await platformFFI.init(appType); // global FFI, use this **ONLY** for global configuration @@ -117,7 +113,7 @@ void runRemoteScreen(Map argument) async { navigatorKey: globalKey, debugShowCheckedModeBanner: false, title: 'RustDesk - Remote Desktop', - theme: getCurrentTheme(), + theme: MyTheme.initialTheme(), home: DesktopRemoteScreen( params: argument, ), @@ -135,7 +131,7 @@ void runFileTransferScreen(Map argument) async { navigatorKey: globalKey, debugShowCheckedModeBanner: false, title: 'RustDesk - File Transfer', - theme: getCurrentTheme(), + theme: MyTheme.initialTheme(), home: DesktopFileTransferScreen(params: argument), navigatorObservers: [ // FirebaseAnalyticsObserver(analytics: analytics), @@ -152,7 +148,7 @@ void runPortForwardScreen(Map argument) async { navigatorKey: globalKey, debugShowCheckedModeBanner: false, title: 'RustDesk - Port Forward', - theme: getCurrentTheme(), + theme: MyTheme.initialTheme(), home: DesktopPortForwardScreen(params: argument), navigatorObservers: [ // FirebaseAnalyticsObserver(analytics: analytics), @@ -176,7 +172,7 @@ void runConnectionManagerScreen() async { ]); runApp(GetMaterialApp( debugShowCheckedModeBanner: false, - theme: getCurrentTheme(), + theme: MyTheme.initialTheme(), home: DesktopServerPage(), builder: _keepScaleBuilder())); } @@ -191,7 +187,26 @@ WindowOptions getHiddenTitleBarWindowOptions(Size size) { ); } -class App extends StatelessWidget { +class App extends StatefulWidget { + @override + State createState() => _AppState(); +} + +class _AppState extends State { + @override + void initState() { + super.initState(); + WidgetsBinding.instance.window.onPlatformBrightnessChanged = () { + WidgetsBinding.instance.handlePlatformBrightnessChanged(); + var system = + WidgetsBinding.instance.platformDispatcher.platformBrightness; + var current = isDarkTheme() ? Brightness.dark : Brightness.light; + if (current != system) { + MyTheme.changeTo(system == Brightness.dark); + } + }; + } + @override Widget build(BuildContext context) { // final analytics = FirebaseAnalytics.instance; @@ -210,7 +225,7 @@ class App extends StatelessWidget { navigatorKey: globalKey, debugShowCheckedModeBanner: false, title: 'RustDesk', - theme: getCurrentTheme(), + theme: MyTheme.initialTheme(mainPage: true), home: isDesktop ? const DesktopTabPage() : !isAndroid From b4e0101e3e7c9c1b995b372f7ab66368b3ba0ad8 Mon Sep 17 00:00:00 2001 From: 21pages Date: Wed, 7 Sep 2022 18:57:49 +0800 Subject: [PATCH 0420/2015] sync theme Signed-off-by: 21pages --- flutter/lib/cm_main.dart | 4 +--- flutter/lib/common.dart | 30 ++++++++++++++++++++++-------- flutter/lib/consts.dart | 2 ++ flutter/lib/main.dart | 21 ++++++++++++++++----- src/flutter.rs | 6 ++++++ src/flutter_ffi.rs | 19 ++++++++++++++++++- src/ipc.rs | 1 + src/ui/cm.rs | 4 ++++ src/ui_cm_interface.rs | 5 +++++ src/ui_interface.rs | 10 +++++++++- 10 files changed, 84 insertions(+), 18 deletions(-) diff --git a/flutter/lib/cm_main.dart b/flutter/lib/cm_main.dart index a92edd194..422e7aa26 100644 --- a/flutter/lib/cm_main.dart +++ b/flutter/lib/cm_main.dart @@ -24,7 +24,5 @@ void main(List args) async { gFFI.serverModel.clients .add(Client(3, false, false, "UserD", "441123123", true, false, false)); runApp(GetMaterialApp( - debugShowCheckedModeBanner: false, - theme: MyTheme.initialTheme(), - home: DesktopServerPage())); + debugShowCheckedModeBanner: false, home: DesktopServerPage())); } diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index bd16fa4c5..06d901368 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -193,25 +193,40 @@ class MyTheme { ); static changeTo(bool dark) { - Get.find().setString("darkTheme", dark ? "Y" : ""); - Get.changeTheme(dark ? MyTheme.darkTheme : MyTheme.lightTheme); - Get.forceAppUpdate(); + if (Get.isDarkMode != dark) { + Get.find().setString("darkTheme", dark ? "Y" : ""); + Get.changeThemeMode(dark ? ThemeMode.dark : ThemeMode.light); + if (desktopType == DesktopType.main) { + bind.mainChangeTheme(dark: dark); + } + } } static bool _themeInitialed = false; - static ThemeData initialTheme({bool mainPage = false}) { + static ThemeMode initialThemeMode({bool mainPage = false}) { bool dark; // Brightnesss is always light on windows, Flutter 3.0.5 if (_themeInitialed || !mainPage || Platform.isWindows) { - dark = isDarkTheme(); + dark = "Y" == Get.find().getString("darkTheme"); } else { dark = WidgetsBinding.instance.platformDispatcher.platformBrightness == Brightness.dark; Get.find().setString("darkTheme", dark ? "Y" : ""); } _themeInitialed = true; - return dark ? MyTheme.darkTheme : MyTheme.lightTheme; + return dark ? ThemeMode.dark : ThemeMode.light; + } + + static registerEventHandler() { + if (desktopType != DesktopType.main) { + platformFFI.registerEventHandler('theme', 'theme-$desktopType', (evt) { + String? dark = evt['dark']; + if (dark != null) { + changeTo(dark == 'true'); + } + }); + } } static ColorThemeExtension color(BuildContext context) { @@ -232,8 +247,7 @@ class ThemeModeNotifier { } bool isDarkTheme() { - final isDark = "Y" == Get.find().getString("darkTheme"); - return isDark; + return Get.isDarkMode; } final ButtonStyle flatButtonStyle = TextButton.styleFrom( diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 95a6faaa2..e48c85b0d 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -5,6 +5,8 @@ const String kAppTypeMain = "main"; const String kAppTypeDesktopRemote = "remote"; const String kAppTypeDesktopFileTransfer = "file transfer"; const String kAppTypeDesktopPortForward = "port forward"; +const String kAppTypeDesktopRDP = "rdp"; + const String kTabLabelHomePage = "Home"; const String kTabLabelSettingPage = "Settings"; diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index a25ed7027..2ca1d3e51 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -79,6 +79,7 @@ Future initEnv(String appType) async { await initGlobalFFI(); // await Firebase.initializeApp(); refreshCurrentUser(); + MyTheme.registerEventHandler(); } void runMainApp(bool startService) async { @@ -113,7 +114,9 @@ void runRemoteScreen(Map argument) async { navigatorKey: globalKey, debugShowCheckedModeBanner: false, title: 'RustDesk - Remote Desktop', - theme: MyTheme.initialTheme(), + theme: MyTheme.lightTheme, + darkTheme: MyTheme.darkTheme, + themeMode: MyTheme.initialThemeMode(), home: DesktopRemoteScreen( params: argument, ), @@ -131,7 +134,9 @@ void runFileTransferScreen(Map argument) async { navigatorKey: globalKey, debugShowCheckedModeBanner: false, title: 'RustDesk - File Transfer', - theme: MyTheme.initialTheme(), + theme: MyTheme.lightTheme, + darkTheme: MyTheme.darkTheme, + themeMode: MyTheme.initialThemeMode(), home: DesktopFileTransferScreen(params: argument), navigatorObservers: [ // FirebaseAnalyticsObserver(analytics: analytics), @@ -148,7 +153,9 @@ void runPortForwardScreen(Map argument) async { navigatorKey: globalKey, debugShowCheckedModeBanner: false, title: 'RustDesk - Port Forward', - theme: MyTheme.initialTheme(), + theme: MyTheme.lightTheme, + darkTheme: MyTheme.darkTheme, + themeMode: MyTheme.initialThemeMode(), home: DesktopPortForwardScreen(params: argument), navigatorObservers: [ // FirebaseAnalyticsObserver(analytics: analytics), @@ -172,7 +179,9 @@ void runConnectionManagerScreen() async { ]); runApp(GetMaterialApp( debugShowCheckedModeBanner: false, - theme: MyTheme.initialTheme(), + theme: MyTheme.lightTheme, + darkTheme: MyTheme.darkTheme, + themeMode: MyTheme.initialThemeMode(), home: DesktopServerPage(), builder: _keepScaleBuilder())); } @@ -225,7 +234,9 @@ class _AppState extends State { navigatorKey: globalKey, debugShowCheckedModeBanner: false, title: 'RustDesk', - theme: MyTheme.initialTheme(mainPage: true), + theme: MyTheme.lightTheme, + darkTheme: MyTheme.darkTheme, + themeMode: MyTheme.initialThemeMode(mainPage: true), home: isDesktop ? const DesktopTabPage() : !isAndroid diff --git a/src/flutter.rs b/src/flutter.rs index 4be998027..ffabef1a9 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -17,6 +17,8 @@ use crate::{client::*, flutter_ffi::EventToUI}; pub(super) const APP_TYPE_MAIN: &str = "main"; pub(super) const APP_TYPE_DESKTOP_REMOTE: &str = "remote"; pub(super) const APP_TYPE_DESKTOP_FILE_TRANSFER: &str = "file transfer"; +pub(super) const APP_TYPE_DESKTOP_PORT_FORWARD: &str = "port forward"; +pub(super) const APP_TYPE_DESKTOP_RDP: &str = "rdp"; lazy_static::lazy_static! { pub static ref SESSIONS: RwLock>> = Default::default(); @@ -376,6 +378,10 @@ pub mod connection_manager { vec![("id", &id.to_string()), ("text", &text)], ); } + + fn change_theme(&self, dark: bool) { + self.push_event("theme", vec![("dark", &dark.to_string())]); + } } impl FlutterHandler { diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 977c03197..13966e07c 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -23,7 +23,7 @@ use crate::ui_interface::{ get_app_name, get_async_job_status, get_connect_status, get_fav, get_id, get_lan_peers, get_langs, get_license, get_local_option, get_mouse_time, get_option, get_options, get_peer, get_peer_option, get_socks, get_sound_inputs, get_uuid, get_version, has_hwcodec, - has_rendezvous_service, post_request, set_local_option, set_option, set_options, + has_rendezvous_service, post_request, send_to_cm, set_local_option, set_option, set_options, set_peer_option, set_permanent_password, set_socks, store_fav, test_if_valid_server, update_temporary_password, using_public_server, }; @@ -659,6 +659,23 @@ pub fn main_load_lan_peers() { }; } +pub fn main_change_theme(dark: bool) { + let apps = vec![ + flutter::APP_TYPE_DESKTOP_REMOTE, + flutter::APP_TYPE_DESKTOP_FILE_TRANSFER, + flutter::APP_TYPE_DESKTOP_PORT_FORWARD, + flutter::APP_TYPE_DESKTOP_RDP, + ]; + + for app in apps { + if let Some(s) = flutter::GLOBAL_EVENT_STREAM.read().unwrap().get(app) { + let data = HashMap::from([("name", "theme".to_owned()), ("dark", dark.to_string())]); + s.add(serde_json::ser::to_string(&data).unwrap_or("".to_owned())); + }; + } + send_to_cm(&crate::ipc::Data::Theme(dark)); +} + pub fn session_add_port_forward( id: String, local_port: i32, diff --git a/src/ipc.rs b/src/ipc.rs index 0bdc3f43b..3d289e499 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -182,6 +182,7 @@ pub enum Data { #[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] Mouse(DataMouse), Control(DataControl), + Theme(bool), Empty, } diff --git a/src/ui/cm.rs b/src/ui/cm.rs index 2b1e3e791..615718e4a 100644 --- a/src/ui/cm.rs +++ b/src/ui/cm.rs @@ -47,6 +47,10 @@ impl InvokeUiCM for SciterHandler { fn new_message(&self, id: i32, text: String) { self.call("newMessage", &make_args!(id, text)); } + + fn change_theme(&self, _dark: bool) { + // TODO + } } impl SciterHandler { diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index d416fdd63..92724bb25 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -60,6 +60,8 @@ pub trait InvokeUiCM: Send + Clone + 'static + Sized { fn remove_connection(&self, id: i32); fn new_message(&self, id: i32, text: String); + + fn change_theme(&self, dark: bool); } impl Deref for ConnectionManager { @@ -280,6 +282,9 @@ pub async fn start_ipc(cm: ConnectionManager) { .send(ClipboardFileData::Enable((conn_id, enabled))) .ok(); } + Data::Theme(dark) => { + cm.change_theme(dark); + } _ => { } diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 186381ce4..c7b743ccb 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -373,7 +373,8 @@ pub fn get_mouse_time() -> f64 { } pub fn check_mouse_time() { - #[cfg(not(any(target_os = "android", target_os = "ios")))]{ + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { let sender = SENDER.lock().unwrap(); allow_err!(sender.send(ipc::Data::MouseMoveTime(0))); } @@ -779,6 +780,13 @@ pub(crate) async fn check_connect_status_(reconnect: bool, rx: mpsc::UnboundedRe } } +#[tokio::main(flavor = "current_thread")] +pub(crate) async fn send_to_cm(data: &ipc::Data) { + if let Ok(mut c) = ipc::connect(1000, "_cm").await { + c.send(data).await.ok(); + } +} + const INVALID_FORMAT: &'static str = "Invalid format"; const UNKNOWN_ERROR: &'static str = "Unknown error"; From d5d2a985729847ceea1a77fc4a554482c4665937 Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 8 Sep 2022 08:52:56 +0800 Subject: [PATCH 0421/2015] sync language Signed-off-by: 21pages --- flutter/lib/common.dart | 19 ------------------- .../desktop/pages/desktop_setting_page.dart | 1 + flutter/lib/main.dart | 16 +++++++++++++++- src/flutter.rs | 4 ++++ src/flutter_ffi.rs | 17 ++++++++++++++--- src/ipc.rs | 1 + src/ui/cm.rs | 4 ++++ src/ui_cm_interface.rs | 8 ++++++++ 8 files changed, 47 insertions(+), 23 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 06d901368..92ae17f9a 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -218,17 +218,6 @@ class MyTheme { return dark ? ThemeMode.dark : ThemeMode.light; } - static registerEventHandler() { - if (desktopType != DesktopType.main) { - platformFFI.registerEventHandler('theme', 'theme-$desktopType', (evt) { - String? dark = evt['dark']; - if (dark != null) { - changeTo(dark == 'true'); - } - }); - } - } - static ColorThemeExtension color(BuildContext context) { return Theme.of(context).extension()!; } @@ -238,14 +227,6 @@ class MyTheme { } } -class ThemeModeNotifier { - final ValueNotifier brightness; - ThemeModeNotifier(this.brightness); - changeThemeBrightness({required Brightness brightness}) { - this.brightness.value = brightness; - } -} - bool isDarkTheme() { return Get.isDarkMode; } diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index a6ae89d71..823a32fb6 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -218,6 +218,7 @@ class _UserInterfaceState extends State<_UserInterface> onChanged: (key) async { await bind.mainSetLocalOption(key: "lang", value: key); Get.forceAppUpdate(); + bind.mainChangeLanguage(lang: key); }, ).marginOnly(left: _kContentHMargin); }); diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 2ca1d3e51..8efc7c6ad 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -79,7 +79,7 @@ Future initEnv(String appType) async { await initGlobalFFI(); // await Firebase.initializeApp(); refreshCurrentUser(); - MyTheme.registerEventHandler(); + _registerEventHandler(); } void runMainApp(bool startService) async { @@ -270,3 +270,17 @@ _keepScaleBuilder() { ); }; } + +_registerEventHandler() { + if (desktopType != DesktopType.main) { + platformFFI.registerEventHandler('theme', 'theme', (evt) { + String? dark = evt['dark']; + if (dark != null) { + MyTheme.changeTo(dark == 'true'); + } + }); + platformFFI.registerEventHandler('language', 'language', (_) { + Get.forceAppUpdate(); + }); + } +} diff --git a/src/flutter.rs b/src/flutter.rs index ffabef1a9..b22c0da83 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -382,6 +382,10 @@ pub mod connection_manager { fn change_theme(&self, dark: bool) { self.push_event("theme", vec![("dark", &dark.to_string())]); } + + fn change_language(&self) { + self.push_event("language", vec![]); + } } impl FlutterHandler { diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 13966e07c..56d69f4c4 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -659,7 +659,7 @@ pub fn main_load_lan_peers() { }; } -pub fn main_change_theme(dark: bool) { +fn main_broadcast_message(data: &HashMap<&str, &str>) { let apps = vec![ flutter::APP_TYPE_DESKTOP_REMOTE, flutter::APP_TYPE_DESKTOP_FILE_TRANSFER, @@ -669,13 +669,24 @@ pub fn main_change_theme(dark: bool) { for app in apps { if let Some(s) = flutter::GLOBAL_EVENT_STREAM.read().unwrap().get(app) { - let data = HashMap::from([("name", "theme".to_owned()), ("dark", dark.to_string())]); - s.add(serde_json::ser::to_string(&data).unwrap_or("".to_owned())); + s.add(serde_json::ser::to_string(data).unwrap_or("".to_owned())); }; } +} + +pub fn main_change_theme(dark: bool) { + main_broadcast_message(&HashMap::from([ + ("name", "theme"), + ("dark", &dark.to_string()), + ])); send_to_cm(&crate::ipc::Data::Theme(dark)); } +pub fn main_change_language(lang: String) { + main_broadcast_message(&HashMap::from([("name", "language"), ("lang", &lang)])); + send_to_cm(&crate::ipc::Data::Language(lang)); +} + pub fn session_add_port_forward( id: String, local_port: i32, diff --git a/src/ipc.rs b/src/ipc.rs index 3d289e499..2d841755b 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -183,6 +183,7 @@ pub enum Data { Mouse(DataMouse), Control(DataControl), Theme(bool), + Language(String), Empty, } diff --git a/src/ui/cm.rs b/src/ui/cm.rs index 615718e4a..c5f64699a 100644 --- a/src/ui/cm.rs +++ b/src/ui/cm.rs @@ -51,6 +51,10 @@ impl InvokeUiCM for SciterHandler { fn change_theme(&self, _dark: bool) { // TODO } + + fn change_language(&self) { + // TODO + } } impl SciterHandler { diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index 92724bb25..a7082e9ee 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -62,6 +62,8 @@ pub trait InvokeUiCM: Send + Clone + 'static + Sized { fn new_message(&self, id: i32, text: String); fn change_theme(&self, dark: bool); + + fn change_language(&self); } impl Deref for ConnectionManager { @@ -200,6 +202,8 @@ pub enum ClipboardFileData { #[cfg(not(any(target_os = "android", target_os = "ios")))] #[tokio::main(flavor = "current_thread")] pub async fn start_ipc(cm: ConnectionManager) { + use hbb_common::config::LocalConfig; + let (tx_file, _rx_file) = mpsc::unbounded_channel::(); #[cfg(windows)] let cm_clip = cm.clone(); @@ -285,6 +289,10 @@ pub async fn start_ipc(cm: ConnectionManager) { Data::Theme(dark) => { cm.change_theme(dark); } + Data::Language(lang) => { + LocalConfig::set_option("lang".to_owned(), lang); + cm.change_language(); + } _ => { } From a9bb7c794728e87b419741307fc81aa32a8f3dbe Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 7 Sep 2022 19:52:30 -0700 Subject: [PATCH 0422/2015] flutter_destkop: fix cursor scale Signed-off-by: fufesou --- flutter/lib/desktop/pages/remote_page.dart | 34 +++++++++++++--------- flutter/lib/models/model.dart | 18 +++++++----- 2 files changed, 31 insertions(+), 21 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index cd23328a4..158028ab3 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -539,7 +539,7 @@ class ImagePaint extends StatelessWidget { Widget build(BuildContext context) { final m = Provider.of(context); var c = Provider.of(context); - final cursor = Provider.of(context); + final s = c.scale; if (c.scrollStyle == ScrollStyle.scrollbar) { final imageWidget = SizedBox( @@ -568,18 +568,7 @@ class ImagePaint extends StatelessWidget { cursor: (cursorOverImage.isTrue && keyboardEnabled.isTrue) ? (remoteCursorMoved.isTrue ? SystemMouseCursors.none - : (cursor.cacheLinux != null - ? FlutterCustomMemoryImageCursor( - pixbuf: cursor.cacheLinux!.data, - key: cursor.cacheLinux!.key, - hotx: cursor.cacheLinux!.hotx, - hoty: cursor.cacheLinux!.hoty, - imageWidth: - (cursor.cacheLinux!.width * s).toInt(), - imageHeight: - (cursor.cacheLinux!.height * s).toInt(), - ) - : MouseCursor.defer)) + : _buildCustomCursorLinux(context, s)) : MouseCursor.defer, onHover: (evt) { pos.value = evt.position; @@ -599,6 +588,25 @@ class ImagePaint extends StatelessWidget { } } + MouseCursor _buildCustomCursorLinux(BuildContext context, double scale) { + final cursor = Provider.of(context); + final cacheLinux = cursor.cacheLinux; + if (cacheLinux == null) { + return MouseCursor.defer; + } else { + final key = cacheLinux.key(scale); + cursor.addKeyLinux(key); + return FlutterCustomMemoryImageCursor( + pixbuf: cacheLinux.data, + key: key, + hotx: cacheLinux.hotx, + hoty: cacheLinux.hoty, + imageWidth: (cacheLinux.width * scale).toInt(), + imageHeight: (cacheLinux.height * scale).toInt(), + ); + } + } + Widget _buildCrossScrollbar(Widget child) { final physicsVertical = cursorOverImage.value ? const NeverScrollableScrollPhysics() : null; diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 7cf54492a..e96eb35a3 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -604,7 +604,6 @@ class CursorData { final double hoty; final int width; final int height; - late String key; CursorData({ required this.peerId, @@ -614,10 +613,12 @@ class CursorData { required this.hoty, required this.width, required this.height, - }) { - key = - '${peerId}_${id}_${(hotx * 10e6).round().toInt()}_${(hoty * 10e6).round().toInt()}_${width}_$height'; - } + }); + + int _doubleToInt(double v) => (v * 10e6).round().toInt(); + + String key(double scale) => + '${peerId}_${id}_${_doubleToInt(hotx)}_${_doubleToInt(hoty)}_${_doubleToInt(width * scale)}_${_doubleToInt(height * scale)}'; } class CursorModel with ChangeNotifier { @@ -625,6 +626,7 @@ class CursorModel with ChangeNotifier { final _images = >{}; CursorData? _cacheLinux; final _cacheMapLinux = {}; + final _cacheKeysLinux = {}; double _x = -10000; double _y = -10000; double _hotx = 0; @@ -649,8 +651,8 @@ class CursorModel with ChangeNotifier { CursorModel(this.parent); - List get cachedKeysLinux => - _cacheMapLinux.values.map((v) => v.key).toList(); + Set get cachedKeysLinux => _cacheKeysLinux; + addKeyLinux(String key) => _cacheKeysLinux.add(key); // remote physical display coordinate Rect getVisibleRect() { @@ -878,7 +880,7 @@ class CursorModel with ChangeNotifier { } void _clearCacheLinux() { - final cachedKeys = [...cachedKeysLinux]; + final cachedKeys = {...cachedKeysLinux}; for (var key in cachedKeys) { customCursorController.freeCache(key); } From e594657f979dcbd41c75f93b5d6e2664815c5fc5 Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 8 Sep 2022 12:20:33 +0800 Subject: [PATCH 0423/2015] fix linux RUSTDESK_LIB_PATH Signed-off-by: 21pages --- flutter/linux/main.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/linux/main.cc b/flutter/linux/main.cc index e2ad70957..d409bfd2b 100644 --- a/flutter/linux/main.cc +++ b/flutter/linux/main.cc @@ -1,7 +1,7 @@ #include #include "my_application.h" -#define RUSTDESK_LIB_PATH "ibrustdesk.so" +#define RUSTDESK_LIB_PATH "librustdesk.so" // #define RUSTDESK_LIB_PATH "/usr/lib/rustdesk/librustdesk.so" typedef bool (*RustDeskCoreMain)(); From 2d9346087343c817b465e56276fb829adbd1f25f Mon Sep 17 00:00:00 2001 From: Kingtous Date: Thu, 8 Sep 2022 18:21:17 +0800 Subject: [PATCH 0424/2015] feat: flatpak and flutter build --- .gitignore | 13 ++++++++++++- Cargo.lock | 2 +- build.rs | 1 + flatpak/rustdesk.yml | 31 +++++++++++++++++++++++++++++++ flatpak/shared-modules | 1 + flutter/PKGBUILD | 33 +++++++++++++++++++++++++++++++++ flutter/rustdesk.service.user | 15 +++++++++++++++ src/ui.rs | 2 +- src/ui_session_interface.rs | 2 +- 9 files changed, 96 insertions(+), 4 deletions(-) create mode 100644 flatpak/rustdesk.yml create mode 160000 flatpak/shared-modules create mode 100644 flutter/PKGBUILD create mode 100644 flutter/rustdesk.service.user diff --git a/.gitignore b/.gitignore index da52e1dc4..41ab9c504 100644 --- a/.gitignore +++ b/.gitignore @@ -21,7 +21,18 @@ sciter.dll src/bridge_generated.rs *deb rustdesk +*.cache # appimage appimage/AppDir appimage/*.AppImage -appimage/appimage-build \ No newline at end of file +appimage/appimage-build +# flutter +flutter/linux/build/** +flutter/linux/cmake-build-debug/** +# flatpak +flatpak/.flatpak-builder/** +flatpak/ccache/** +flatpak/.flatpak-builder/build/** +flatpak/.flatpak-builder/shared-modules/** +flatpak/.flatpak-builder/shared-modules/*.tar.xz +flatpak/.flatpak-builder/debian-binary \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 303d18f1e..5c3313336 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4201,7 +4201,7 @@ dependencies = [ [[package]] name = "rustdesk" -version = "1.1.10" +version = "1.2.0" dependencies = [ "android_logger 0.11.1", "arboard", diff --git a/build.rs b/build.rs index 01fe8ada1..a77d7100d 100644 --- a/build.rs +++ b/build.rs @@ -105,6 +105,7 @@ fn main() { // there is problem with cfg(target_os) in build.rs, so use our workaround // let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); // if target_os == "android" || target_os == "ios" { + #[cfg(feature = "flutter")] gen_flutter_rust_bridge(); // return; // } diff --git a/flatpak/rustdesk.yml b/flatpak/rustdesk.yml new file mode 100644 index 000000000..3d7936635 --- /dev/null +++ b/flatpak/rustdesk.yml @@ -0,0 +1,31 @@ +app-id: org.rustdesk.rustdesk +runtime: org.freedesktop.Platform +runtime-version: '21.08' +sdk: org.freedesktop.Sdk +command: rustdesk +modules: + # install appindicator + - shared-modules/libappindicator/libappindicator-gtk3-12.10.json + - name: rustdesk + buildsystem: simple + build-commands: + - bsdtar -zxvf rustdesk-1.2.0.deb + - tar -xvf ./data.tar.xz + - cp -r ./usr /app/ + - rm /app/usr/bin/rustdesk + - mkdir -p /app/bin && ln -s /app/usr/lib/rustdesk/flutter_hbb /app/bin/rustdesk + sources: + # Note: replace to deb files with url + - type: file + path: ../rustdesk-1.2.0.deb + +finish-args: + # X11 + XShm access + - --share=ipc + - --socket=x11 + # Wayland access + - --socket=wayland + # Needs to talk to the network: + - --share=network + # Needs to save files locally + - --filesystem=xdg-documents \ No newline at end of file diff --git a/flatpak/shared-modules b/flatpak/shared-modules new file mode 160000 index 000000000..cecc93886 --- /dev/null +++ b/flatpak/shared-modules @@ -0,0 +1 @@ +Subproject commit cecc93886ce839ec49b0041f072a573327acdf08 diff --git a/flutter/PKGBUILD b/flutter/PKGBUILD new file mode 100644 index 000000000..3d2981283 --- /dev/null +++ b/flutter/PKGBUILD @@ -0,0 +1,33 @@ +pkgname=rustdesk +pkgver=1.2.0 +pkgrel=0 +epoch= +pkgdesc="" +arch=('x86_64') +url="" +license=('GPL-3.0') +groups=() +depends=('gtk3' 'xdotool' 'libxcb' 'libxfixes' 'alsa-lib' 'pulseaudio' 'ttf-arphic-uming' 'python-pip' 'curl' 'libappindicator-gtk3') +makedepends=() +checkdepends=() +optdepends=() +provides=() +conflicts=() +replaces=() +backup=() +options=() +install=../pacman_install +changelog= +noextract=() +md5sums=() #generate with 'makepkg -g' + +package() { + mkdir -p "${pkgdir}/usr/lib/rustdesk" && cp -r ${HBB}/build/linux/x64/release/bundle/* -t "${pkgdir}/usr/lib/rustdesk" + cp ${HBB}/build/linux/x64/release/liblibrustdesk.so "${pkgdir}/usr/lib/rustdesk/librustdesk.so" + mkdir -p "${pkgdir}/usr/bin" + pushd ${pkgdir} && ln -s /usr/lib/rustdesk/flutter_hbb usr/bin/rustdesk && popd + install -Dm 644 $HBB/rustdesk.service -t "${pkgdir}/usr/share/rustdesk/files" + install -Dm 644 $HBB/rustdesk.desktop -t "${pkgdir}/usr/share/rustdesk/files" + # install -Dm 644 $HBB/../pynput_service.py -t "${pkgdir}/usr/share/rustdesk/files" + install -Dm 644 $HBB/../128x128@2x.png "${pkgdir}/usr/share/rustdesk/files/rustdesk.png" +} diff --git a/flutter/rustdesk.service.user b/flutter/rustdesk.service.user new file mode 100644 index 000000000..a349d0361 --- /dev/null +++ b/flutter/rustdesk.service.user @@ -0,0 +1,15 @@ +[Unit] +Description=RustDesk user service (--server) + +[Service] +Type=simple +ExecStart=/usr/lib/rustdesk/flutter_hbb --server +PIDFile=/run/rustdesk.user.pid +KillMode=mixed +TimeoutStopSec=30 +LimitNOFILE=100000 +Restart=on-failure +RestartSec=3 + +[Install] +WantedBy=multi-user.target diff --git a/src/ui.rs b/src/ui.rs index 7457c2ff5..fd4bfe1a8 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -39,7 +39,7 @@ use crate::ui_interface::{ mod cm; #[cfg(feature = "inline")] -mod inline; +pub mod inline; #[cfg(target_os = "macos")] mod macos; pub mod remote; diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 7eabe1510..43cd35754 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -680,7 +680,7 @@ impl Session { pub fn get_chatbox(&self) -> String { #[cfg(feature = "inline")] - return super::inline::get_chatbox(); + return crate::ui::inline::get_chatbox(); #[cfg(not(feature = "inline"))] return "".to_owned(); } From b93e59df21abf70595c92ec67cd559b0e512aa6c Mon Sep 17 00:00:00 2001 From: csf Date: Thu, 8 Sep 2022 19:26:55 +0800 Subject: [PATCH 0425/2015] confirm connection tab close --- .../desktop/pages/connection_tab_page.dart | 14 ++++++++- .../desktop/pages/file_manager_tab_page.dart | 4 +-- .../desktop/pages/port_forward_tab_page.dart | 3 +- .../lib/desktop/widgets/tabbar_widget.dart | 29 ++++++++++++++----- 4 files changed, 38 insertions(+), 12 deletions(-) diff --git a/flutter/lib/desktop/pages/connection_tab_page.dart b/flutter/lib/desktop/pages/connection_tab_page.dart index 8f5350792..bf12f443c 100644 --- a/flutter/lib/desktop/pages/connection_tab_page.dart +++ b/flutter/lib/desktop/pages/connection_tab_page.dart @@ -10,6 +10,8 @@ import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:get/get.dart'; +import '../../mobile/widgets/dialog.dart'; + class ConnectionTabPage extends StatefulWidget { final Map params; @@ -37,6 +39,11 @@ class _ConnectionTabPageState extends State { label: peerId, selectedIcon: selectedIcon, unselectedIcon: unselectedIcon, + onTabCloseButton: () { + debugPrint("onTabCloseButton"); + tabController.jumpBy(peerId); + clientClose(ffi(peerId).dialogManager); + }, page: Obx(() => RemotePage( key: ValueKey(peerId), id: peerId, @@ -69,6 +76,11 @@ class _ConnectionTabPageState extends State { label: id, selectedIcon: selectedIcon, unselectedIcon: unselectedIcon, + onTabCloseButton: () { + debugPrint("onTabCloseButton"); + tabController.jumpBy(id); + clientClose(ffi(id).dialogManager); + }, page: Obx(() => RemotePage( key: ValueKey(id), id: id, @@ -95,7 +107,7 @@ class _ConnectionTabPageState extends State { body: Obx(() => DesktopTab( controller: tabController, showTabBar: fullscreen.isFalse, - onClose: () { + onWindowCloseButton: () { tabController.clear(); }, tail: AddButton().paddingOnly(left: 10), diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index d6f01e55f..d33c0084c 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -71,10 +71,10 @@ class _FileManagerTabPageState extends State { backgroundColor: MyTheme.color(context).bg, body: DesktopTab( controller: tabController, - onClose: () { + onWindowCloseButton: () { tabController.clear(); }, - tail: AddButton().paddingOnly(left: 10), + tail: const AddButton().paddingOnly(left: 10), )), ), ); diff --git a/flutter/lib/desktop/pages/port_forward_tab_page.dart b/flutter/lib/desktop/pages/port_forward_tab_page.dart index e0384b614..5dd69e8eb 100644 --- a/flutter/lib/desktop/pages/port_forward_tab_page.dart +++ b/flutter/lib/desktop/pages/port_forward_tab_page.dart @@ -78,7 +78,7 @@ class _PortForwardTabPageState extends State { backgroundColor: MyTheme.color(context).bg, body: DesktopTab( controller: tabController, - onClose: () { + onWindowCloseButton: () { tabController.clear(); }, tail: AddButton().paddingOnly(left: 10), @@ -88,7 +88,6 @@ class _PortForwardTabPageState extends State { } void onRemoveId(String id) { - ffi("pf_$id").close(); if (tabController.state.value.tabs.isEmpty) { WindowController.fromWindowId(windowId()).hide(); } diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 5daa1aeb6..f2fc5f8ca 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -24,7 +24,8 @@ class TabInfo { final String label; final IconData? selectedIcon; final IconData? unselectedIcon; - final bool closable; + final bool closable; // + final VoidCallback? onTabCloseButton; final Widget page; TabInfo( @@ -33,6 +34,7 @@ class TabInfo { this.selectedIcon, this.unselectedIcon, this.closable = true, + this.onTabCloseButton, required this.page}); } @@ -137,16 +139,23 @@ class DesktopTabController { } } + void jumpBy(String key) { + if (!isDesktop) return; + final index = state.value.tabs.indexWhere((tab) => tab.key == key); + jumpTo(index); + } + void closeBy(String? key) { if (!isDesktop) return; + debugPrint("closeBy: $key"); assert(onRemove != null); if (key == null) { if (state.value.selected < state.value.tabs.length) { remove(state.value.selected); } } else { - state.value.tabs.indexWhere((tab) => tab.key == key); - remove(state.value.selected); + final index = state.value.tabs.indexWhere((tab) => tab.key == key); + remove(index); } } @@ -175,7 +184,7 @@ class DesktopTab extends StatelessWidget { final bool showClose; final Widget Function(Widget pageView)? pageViewBuilder; final Widget? tail; - final VoidCallback? onClose; + final VoidCallback? onWindowCloseButton; final TabBuilder? tabBuilder; final LabelGetter? labelGetter; @@ -196,7 +205,7 @@ class DesktopTab extends StatelessWidget { this.showClose = true, this.pageViewBuilder, this.tail, - this.onClose, + this.onWindowCloseButton, this.tabBuilder, this.labelGetter, }) : super(key: key) { @@ -333,7 +342,7 @@ class DesktopTab extends StatelessWidget { showMinimize: showMinimize, showMaximize: showMaximize, showClose: showClose, - onClose: onClose, + onClose: onWindowCloseButton, ) ], ); @@ -511,7 +520,13 @@ class _ListView extends StatelessWidget { unselectedIcon: tab.unselectedIcon, closable: tab.closable, selected: state.value.selected, - onClose: () => controller.remove(index), + onClose: () { + if (tab.onTabCloseButton != null) { + tab.onTabCloseButton!(); + } else { + controller.remove(index); + } + }, onSelected: () => controller.jumpTo(index), tabBuilder: tabBuilder == null ? null From 63cb816b7de0cd7685fb7c3a91760f839acb3feb Mon Sep 17 00:00:00 2001 From: csf Date: Thu, 8 Sep 2022 20:43:27 +0800 Subject: [PATCH 0426/2015] fix: close one connection tab will dispose all tabs (Obx) --- .../desktop/pages/connection_tab_page.dart | 122 +++++++++--------- 1 file changed, 59 insertions(+), 63 deletions(-) diff --git a/flutter/lib/desktop/pages/connection_tab_page.dart b/flutter/lib/desktop/pages/connection_tab_page.dart index bf12f443c..3dd825f36 100644 --- a/flutter/lib/desktop/pages/connection_tab_page.dart +++ b/flutter/lib/desktop/pages/connection_tab_page.dart @@ -44,12 +44,11 @@ class _ConnectionTabPageState extends State { tabController.jumpBy(peerId); clientClose(ffi(peerId).dialogManager); }, - page: Obx(() => RemotePage( - key: ValueKey(peerId), - id: peerId, - tabBarHeight: - fullscreen.isTrue ? 0 : kDesktopRemoteTabBarHeight, - )))); + page: RemotePage( + key: ValueKey(peerId), + id: peerId, + tabBarHeight: fullscreen.isTrue ? 0 : kDesktopRemoteTabBarHeight, + ))); } } @@ -81,12 +80,11 @@ class _ConnectionTabPageState extends State { tabController.jumpBy(id); clientClose(ffi(id).dialogManager); }, - page: Obx(() => RemotePage( - key: ValueKey(id), - id: id, - tabBarHeight: - fullscreen.isTrue ? 0 : kDesktopRemoteTabBarHeight, - )))); + page: RemotePage( + key: ValueKey(id), + id: id, + tabBarHeight: fullscreen.isTrue ? 0 : kDesktopRemoteTabBarHeight, + ))); } else if (call.method == "onDestroy") { tabController.clear(); } @@ -104,57 +102,55 @@ class _ConnectionTabPageState extends State { border: Border.all(color: MyTheme.color(context).border!)), child: Scaffold( backgroundColor: MyTheme.color(context).bg, - body: Obx(() => DesktopTab( - controller: tabController, - showTabBar: fullscreen.isFalse, - onWindowCloseButton: () { - tabController.clear(); - }, - tail: AddButton().paddingOnly(left: 10), - pageViewBuilder: (pageView) { - WindowController.fromWindowId(windowId()) - .setFullscreen(fullscreen.isTrue); - return pageView; - }, - tabBuilder: (key, icon, label, themeConf) => Obx(() { - final connectionType = ConnectionTypeState.find(key); - if (!connectionType.isValid()) { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - icon, - label, - ], - ); - } else { - final msgDirect = translate( - connectionType.direct.value == - ConnectionType.strDirect - ? 'Direct Connection' - : 'Relay Connection'); - final msgSecure = translate( - connectionType.secure.value == - ConnectionType.strSecure - ? 'Secure Connection' - : 'Insecure Connection'); - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - icon, - Tooltip( - message: '$msgDirect\n$msgSecure', - child: Image.asset( - 'assets/${connectionType.secure.value}${connectionType.direct.value}.png', - width: themeConf.iconSize, - height: themeConf.iconSize, - ).paddingOnly(right: 5), - ), - label, - ], - ); - } - }), - ))), + body: DesktopTab( + controller: tabController, + showTabBar: fullscreen.isFalse, + onWindowCloseButton: () { + tabController.clear(); + }, + tail: AddButton().paddingOnly(left: 10), + pageViewBuilder: (pageView) { + WindowController.fromWindowId(windowId()) + .setFullscreen(fullscreen.isTrue); + return pageView; + }, + tabBuilder: (key, icon, label, themeConf) => Obx(() { + final connectionType = ConnectionTypeState.find(key); + if (!connectionType.isValid()) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + icon, + label, + ], + ); + } else { + final msgDirect = translate(connectionType.direct.value == + ConnectionType.strDirect + ? 'Direct Connection' + : 'Relay Connection'); + final msgSecure = translate(connectionType.secure.value == + ConnectionType.strSecure + ? 'Secure Connection' + : 'Insecure Connection'); + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + icon, + Tooltip( + message: '$msgDirect\n$msgSecure', + child: Image.asset( + 'assets/${connectionType.secure.value}${connectionType.direct.value}.png', + width: themeConf.iconSize, + height: themeConf.iconSize, + ).paddingOnly(right: 5), + ), + label, + ], + ); + } + }), + )), ), )); } From 30156c694b6535fbe854f6ceaa35081cb5b31a7d Mon Sep 17 00:00:00 2001 From: csf Date: Thu, 8 Sep 2022 21:03:20 +0800 Subject: [PATCH 0427/2015] add file_transfer confirm close --- .../desktop/pages/connection_tab_page.dart | 22 ++++++++++--------- .../desktop/pages/file_manager_tab_page.dart | 14 ++++++++++++ .../lib/desktop/widgets/tabbar_widget.dart | 3 +-- 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/flutter/lib/desktop/pages/connection_tab_page.dart b/flutter/lib/desktop/pages/connection_tab_page.dart index 3dd825f36..e5caccd71 100644 --- a/flutter/lib/desktop/pages/connection_tab_page.dart +++ b/flutter/lib/desktop/pages/connection_tab_page.dart @@ -39,11 +39,7 @@ class _ConnectionTabPageState extends State { label: peerId, selectedIcon: selectedIcon, unselectedIcon: unselectedIcon, - onTabCloseButton: () { - debugPrint("onTabCloseButton"); - tabController.jumpBy(peerId); - clientClose(ffi(peerId).dialogManager); - }, + onTabCloseButton: () => handleTabCloseButton(peerId), page: RemotePage( key: ValueKey(peerId), id: peerId, @@ -75,11 +71,7 @@ class _ConnectionTabPageState extends State { label: id, selectedIcon: selectedIcon, unselectedIcon: unselectedIcon, - onTabCloseButton: () { - debugPrint("onTabCloseButton"); - tabController.jumpBy(id); - clientClose(ffi(id).dialogManager); - }, + onTabCloseButton: () => handleTabCloseButton(id), page: RemotePage( key: ValueKey(id), id: id, @@ -165,4 +157,14 @@ class _ConnectionTabPageState extends State { int windowId() { return widget.params["windowId"]; } + + void handleTabCloseButton(String peerId) { + final session = ffi(peerId); + if (session.ffiModel.pi.hostname.isNotEmpty) { + tabController.jumpBy(peerId); + clientClose(session.dialogManager); + } else { + tabController.closeBy(peerId); + } + } } diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index d33c0084c..42c50b927 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -8,6 +8,8 @@ import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:get/get.dart'; +import '../../mobile/widgets/dialog.dart'; + /// File Transfer for multi tabs class FileManagerTabPage extends StatefulWidget { final Map params; @@ -31,6 +33,7 @@ class _FileManagerTabPageState extends State { label: params['id'], selectedIcon: selectedIcon, unselectedIcon: unselectedIcon, + onTabCloseButton: () => handleTabCloseButton(params['id']), page: FileManagerPage(key: ValueKey(params['id']), id: params['id']))); } @@ -53,6 +56,7 @@ class _FileManagerTabPageState extends State { label: id, selectedIcon: selectedIcon, unselectedIcon: unselectedIcon, + onTabCloseButton: () => handleTabCloseButton(id), page: FileManagerPage(key: ValueKey(id), id: id))); } else if (call.method == "onDestroy") { tabController.clear(); @@ -89,4 +93,14 @@ class _FileManagerTabPageState extends State { int windowId() { return widget.params["windowId"]; } + + void handleTabCloseButton(String peerId) { + final session = ffi('ft_$peerId'); + if (session.ffiModel.pi.hostname.isNotEmpty) { + tabController.jumpBy(peerId); + clientClose(session.dialogManager); + } else { + tabController.closeBy(peerId); + } + } } diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index f2fc5f8ca..fef5fbf1f 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -24,7 +24,7 @@ class TabInfo { final String label; final IconData? selectedIcon; final IconData? unselectedIcon; - final bool closable; // + final bool closable; final VoidCallback? onTabCloseButton; final Widget page; @@ -147,7 +147,6 @@ class DesktopTabController { void closeBy(String? key) { if (!isDesktop) return; - debugPrint("closeBy: $key"); assert(onRemove != null); if (key == null) { if (state.value.selected < state.value.tabs.length) { From 36143c08807ef269ccea9414d812303da647c84a Mon Sep 17 00:00:00 2001 From: csf Date: Thu, 8 Sep 2022 21:12:35 +0800 Subject: [PATCH 0428/2015] update Cargo.lock pubspec.lock & rename connection_tab_page.dart -> remote_tab_page.dart --- Cargo.lock | 2 +- .../{connection_tab_page.dart => remote_tab_page.dart} | 0 flutter/lib/desktop/screen/desktop_remote_screen.dart | 2 +- flutter/pubspec.lock | 9 +++++++++ 4 files changed, 11 insertions(+), 2 deletions(-) rename flutter/lib/desktop/pages/{connection_tab_page.dart => remote_tab_page.dart} (100%) diff --git a/Cargo.lock b/Cargo.lock index 303d18f1e..5c3313336 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4201,7 +4201,7 @@ dependencies = [ [[package]] name = "rustdesk" -version = "1.1.10" +version = "1.2.0" dependencies = [ "android_logger 0.11.1", "arboard", diff --git a/flutter/lib/desktop/pages/connection_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart similarity index 100% rename from flutter/lib/desktop/pages/connection_tab_page.dart rename to flutter/lib/desktop/pages/remote_tab_page.dart diff --git a/flutter/lib/desktop/screen/desktop_remote_screen.dart b/flutter/lib/desktop/screen/desktop_remote_screen.dart index 5b5dd07c2..1dcb426df 100644 --- a/flutter/lib/desktop/screen/desktop_remote_screen.dart +++ b/flutter/lib/desktop/screen/desktop_remote_screen.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; -import 'package:flutter_hbb/desktop/pages/connection_tab_page.dart'; +import 'package:flutter_hbb/desktop/pages/remote_tab_page.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 586187be2..e6fa83b1d 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -414,6 +414,15 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "3.3.0" + flutter_custom_cursor: + dependency: "direct main" + description: + path: "." + ref: "9021e21de36c84edf01d5034f38eda580463163b" + resolved-ref: "9021e21de36c84edf01d5034f38eda580463163b" + url: "https://github.com/Kingtous/rustdesk_flutter_custom_cursor" + source: git + version: "0.0.1" flutter_launcher_icons: dependency: "direct dev" description: From d0c438268d3863cddc795d08ddc68f0ab46b12b0 Mon Sep 17 00:00:00 2001 From: csf Date: Thu, 8 Sep 2022 22:18:02 +0800 Subject: [PATCH 0429/2015] update overlay widgets on flutter desktop 1. add mobile actions 2. disable showChatIcon --- flutter/lib/common.dart | 59 ++++++++- flutter/lib/desktop/pages/remote_page.dart | 4 +- .../lib/desktop/widgets/remote_menubar.dart | 26 ++-- flutter/lib/mobile/pages/remote_page.dart | 19 +-- flutter/lib/mobile/widgets/overlay.dart | 121 ++++++------------ flutter/lib/models/chat_model.dart | 7 +- flutter/lib/models/model.dart | 7 +- 7 files changed, 126 insertions(+), 117 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 92ae17f9a..249b45a0c 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -14,6 +14,7 @@ import 'package:get/get.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:window_manager/window_manager.dart'; +import 'mobile/widgets/overlay.dart'; import 'models/model.dart'; import 'models/platform_model.dart'; @@ -294,9 +295,11 @@ class Dialog { class OverlayDialogManager { OverlayState? _overlayState; - Map _dialogs = Map(); + final Map _dialogs = {}; int _tagCount = 0; + OverlayEntry? _mobileActionsOverlayEntry; + /// By default OverlayDialogManager use global overlay OverlayDialogManager() { _overlayState = globalKey.currentState?.overlay; @@ -418,6 +421,60 @@ class OverlayDialogManager { ); }); } + + void resetMobileActionsOverlay({FFI? ffi}) { + if (_mobileActionsOverlayEntry == null) return; + hideMobileActionsOverlay(); + showMobileActionsOverlay(ffi: ffi); + } + + void showMobileActionsOverlay({FFI? ffi}) { + if (_mobileActionsOverlayEntry != null) return; + if (_overlayState == null) return; + + // compute overlay position + final screenW = MediaQuery.of(globalKey.currentContext!).size.width; + final screenH = MediaQuery.of(globalKey.currentContext!).size.height; + const double overlayW = 200; + const double overlayH = 45; + final left = (screenW - overlayW) / 2; + final top = screenH - overlayH - 80; + + final overlay = OverlayEntry(builder: (context) { + final session = ffi ?? gFFI; + return DraggableMobileActions( + position: Offset(left, top), + width: overlayW, + height: overlayH, + onBackPressed: () => session.tap(MouseButtons.right), + onHomePressed: () => session.tap(MouseButtons.wheel), + onRecentPressed: () async { + session.sendMouse('down', MouseButtons.wheel); + await Future.delayed(const Duration(milliseconds: 500)); + session.sendMouse('up', MouseButtons.wheel); + }, + onHidePressed: () => hideMobileActionsOverlay(), + ); + }); + _overlayState!.insert(overlay); + _mobileActionsOverlayEntry = overlay; + } + + void hideMobileActionsOverlay() { + if (_mobileActionsOverlayEntry != null) { + _mobileActionsOverlayEntry!.remove(); + _mobileActionsOverlayEntry = null; + return; + } + } + + void toggleMobileActionsOverlay({FFI? ffi}) { + if (_mobileActionsOverlayEntry == null) { + showMobileActionsOverlay(ffi: ffi); + } else { + hideMobileActionsOverlay(); + } + } } void showToast(String text, {Duration timeout = const Duration(seconds: 2)}) { diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 158028ab3..c03c2f3d4 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -1,6 +1,5 @@ import 'dart:async'; import 'dart:io'; -import 'dart:typed_data'; import 'dart:ui' as ui; import 'package:flutter/gestures.dart'; @@ -16,7 +15,6 @@ import 'package:flutter_custom_cursor/flutter_custom_cursor.dart'; import '../widgets/remote_menubar.dart'; import '../../common.dart'; import '../../mobile/widgets/dialog.dart'; -import '../../mobile/widgets/overlay.dart'; import '../../models/model.dart'; import '../../models/platform_model.dart'; import '../../common/shared_state.dart'; @@ -107,7 +105,7 @@ class _RemotePageState extends State @override void dispose() { debugPrint("REMOTE PAGE dispose ${widget.id}"); - hideMobileActionsOverlay(); + _ffi.dialogManager.hideMobileActionsOverlay(); _ffi.listenToMouse(false); _mobileFocusNode.dispose(); _physicalFocusNode.dispose(); diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 28085246f..c5e74be12 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -6,7 +6,6 @@ import 'package:rxdart/rxdart.dart' as rxdart; import '../../common.dart'; import '../../mobile/widgets/dialog.dart'; -import '../../mobile/widgets/overlay.dart'; import '../../models/model.dart'; import '../../models/platform_model.dart'; import '../../common/shared_state.dart'; @@ -75,20 +74,17 @@ class _RemoteMenubarState extends State { final List menubarItems = []; if (!isWebDesktop) { menubarItems.add(_buildFullscreen(context)); - //if (widget.ffi.ffiModel.isPeerAndroid) { - menubarItems.add(IconButton( - tooltip: translate('Mobile Actions'), - color: _MenubarTheme.commonColor, - icon: const Icon(Icons.build), - onPressed: () { - if (mobileActionsOverlayEntry == null) { - showMobileActionsOverlay(); - } else { - hideMobileActionsOverlay(); - } - }, - )); - //} + if (widget.ffi.ffiModel.isPeerAndroid) { + menubarItems.add(IconButton( + tooltip: translate('Mobile Actions'), + color: _MenubarTheme.commonColor, + icon: const Icon(Icons.build), + onPressed: () { + widget.ffi.dialogManager + .toggleMobileActionsOverlay(ffi: widget.ffi); + }, + )); + } } menubarItems.add(_buildMonitor(context)); menubarItems.add(_buildControl(context)); diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index ceb3df0ff..419a98f3a 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -14,7 +14,6 @@ import '../../models/model.dart'; import '../../models/platform_model.dart'; import '../widgets/dialog.dart'; import '../widgets/gestures.dart'; -import '../widgets/overlay.dart'; final initText = '\1' * 1024; @@ -64,7 +63,7 @@ class _RemotePageState extends State { @override void dispose() { - hideMobileActionsOverlay(); + gFFI.dialogManager.hideMobileActionsOverlay(); gFFI.listenToMouse(false); gFFI.invokeMethod("enable_soft_keyboard", true); _mobileFocusNode.dispose(); @@ -266,8 +265,9 @@ class _RemotePageState extends State { : SafeArea(child: OrientationBuilder(builder: (ctx, orientation) { if (_currentOrientation != orientation) { - Timer(Duration(milliseconds: 200), () { - resetMobileActionsOverlay(); + Timer(const Duration(milliseconds: 200), () { + gFFI.dialogManager + .resetMobileActionsOverlay(ffi: gFFI); _currentOrientation = orientation; gFFI.canvasModel.updateViewStyle(); }); @@ -422,14 +422,9 @@ class _RemotePageState extends State { ? [ IconButton( color: Colors.white, - icon: Icon(Icons.build), - onPressed: () { - if (mobileActionsOverlayEntry == null) { - showMobileActionsOverlay(); - } else { - hideMobileActionsOverlay(); - } - }, + icon: const Icon(Icons.build), + onPressed: () => gFFI.dialogManager + .toggleMobileActionsOverlay(ffi: gFFI), ) ] : [ diff --git a/flutter/lib/mobile/widgets/overlay.dart b/flutter/lib/mobile/widgets/overlay.dart index 976d9bb73..b8fd8f653 100644 --- a/flutter/lib/mobile/widgets/overlay.dart +++ b/flutter/lib/mobile/widgets/overlay.dart @@ -2,11 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; import '../../models/chat_model.dart'; -import '../../models/model.dart'; import '../pages/chat_page.dart'; -OverlayEntry? mobileActionsOverlayEntry; - class DraggableChatWindow extends StatelessWidget { DraggableChatWindow( {this.position = Offset.zero, @@ -99,6 +96,7 @@ class DraggableMobileActions extends StatelessWidget { this.onBackPressed, this.onRecentPressed, this.onHomePressed, + this.onHidePressed, required this.width, required this.height}); @@ -108,6 +106,7 @@ class DraggableMobileActions extends StatelessWidget { final VoidCallback? onBackPressed; final VoidCallback? onHomePressed; final VoidCallback? onRecentPressed; + final VoidCallback? onHidePressed; @override Widget build(BuildContext context) { @@ -118,89 +117,49 @@ class DraggableMobileActions extends StatelessWidget { builder: (_, onPanUpdate) { return GestureDetector( onPanUpdate: onPanUpdate, - child: Container( - decoration: BoxDecoration( - color: MyTheme.accent.withOpacity(0.4), - borderRadius: BorderRadius.all(Radius.circular(15))), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - IconButton( - color: MyTheme.white, - onPressed: onBackPressed, - icon: Icon(Icons.arrow_back)), - IconButton( - color: MyTheme.white, - onPressed: onHomePressed, - icon: Icon(Icons.home)), - IconButton( - color: MyTheme.white, - onPressed: onRecentPressed, - icon: Icon(Icons.more_horiz)), - VerticalDivider( - width: 0, - thickness: 2, - indent: 10, - endIndent: 10, + child: Card( + color: Colors.transparent, + shadowColor: Colors.transparent, + child: Container( + decoration: BoxDecoration( + color: MyTheme.accent.withOpacity(0.4), + borderRadius: BorderRadius.all(Radius.circular(15))), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + IconButton( + color: MyTheme.white, + onPressed: onBackPressed, + splashRadius: 20, + icon: const Icon(Icons.arrow_back)), + IconButton( + color: MyTheme.white, + onPressed: onHomePressed, + splashRadius: 20, + icon: const Icon(Icons.home)), + IconButton( + color: MyTheme.white, + onPressed: onRecentPressed, + splashRadius: 20, + icon: const Icon(Icons.more_horiz)), + const VerticalDivider( + width: 0, + thickness: 2, + indent: 10, + endIndent: 10, + ), + IconButton( + color: MyTheme.white, + onPressed: onHidePressed, + splashRadius: 20, + icon: const Icon(Icons.keyboard_arrow_down)), + ], ), - IconButton( - color: MyTheme.white, - onPressed: hideMobileActionsOverlay, - icon: Icon(Icons.keyboard_arrow_down)), - ], - ), - )); + ))); }); } } -resetMobileActionsOverlay() { - if (mobileActionsOverlayEntry == null) return; - hideMobileActionsOverlay(); - showMobileActionsOverlay(); -} - -showMobileActionsOverlay() { - if (mobileActionsOverlayEntry != null) return; - if (globalKey.currentContext == null || - globalKey.currentState == null || - globalKey.currentState!.overlay == null) return; - final globalOverlayState = globalKey.currentState!.overlay!; - - // compute overlay position - final screenW = MediaQuery.of(globalKey.currentContext!).size.width; - final screenH = MediaQuery.of(globalKey.currentContext!).size.height; - final double overlayW = 200; - final double overlayH = 45; - final left = (screenW - overlayW) / 2; - final top = screenH - overlayH - 80; - - final overlay = OverlayEntry(builder: (context) { - return DraggableMobileActions( - position: Offset(left, top), - width: overlayW, - height: overlayH, - onBackPressed: () => gFFI.tap(MouseButtons.right), - onHomePressed: () => gFFI.tap(MouseButtons.wheel), - onRecentPressed: () async { - gFFI.sendMouse('down', MouseButtons.wheel); - await Future.delayed(Duration(milliseconds: 500)); - gFFI.sendMouse('up', MouseButtons.wheel); - }, - ); - }); - globalOverlayState.insert(overlay); - mobileActionsOverlayEntry = overlay; -} - -hideMobileActionsOverlay() { - if (mobileActionsOverlayEntry != null) { - mobileActionsOverlayEntry!.remove(); - mobileActionsOverlayEntry = null; - return; - } -} - class Draggable extends StatefulWidget { Draggable( {this.checkKeyboard = false, diff --git a/flutter/lib/models/chat_model.dart b/flutter/lib/models/chat_model.dart index a9c791ef7..4bdf5826a 100644 --- a/flutter/lib/models/chat_model.dart +++ b/flutter/lib/models/chat_model.dart @@ -143,9 +143,12 @@ class ChatModel with ChangeNotifier { } toggleChatOverlay() { - if (chatIconOverlayEntry == null || chatWindowOverlayEntry == null) { + if ((!isDesktop && chatIconOverlayEntry == null) || + chatWindowOverlayEntry == null) { gFFI.invokeMethod("enable_soft_keyboard", true); - showChatIconOverlay(); + if (!isDesktop) { + showChatIconOverlay(); + } showChatWindowOverlay(); } else { hideChatIconOverlay(); diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index e96eb35a3..e907f3ded 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -22,7 +22,6 @@ import 'package:flutter_custom_cursor/flutter_custom_cursor.dart'; import '../common.dart'; import '../common/shared_state.dart'; import '../mobile/widgets/dialog.dart'; -import '../mobile/widgets/overlay.dart'; import 'peer_model.dart'; import 'platform_model.dart'; @@ -267,8 +266,10 @@ class FfiModel with ChangeNotifier { if (isPeerAndroid) { _touchMode = true; - if (parent.target?.ffiModel.permissions['keyboard'] != false) { - Timer(const Duration(milliseconds: 100), showMobileActionsOverlay); + if (parent.target != null && + parent.target!.ffiModel.permissions['keyboard'] != false) { + Timer(const Duration(milliseconds: 100), + parent.target!.dialogManager.showMobileActionsOverlay); } } else { _touchMode = From 121111b8648994c94d81cc03a91c445f2e1a45e5 Mon Sep 17 00:00:00 2001 From: csf Date: Wed, 7 Sep 2022 20:08:12 +0800 Subject: [PATCH 0430/2015] add flutter start_server & fix cm user environment from linux service --- src/core_main.rs | 12 ++++++++++-- src/platform/linux.rs | 27 +++++++++++++++++---------- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/src/core_main.rs b/src/core_main.rs index c780a1cb0..02ac5e646 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -1,6 +1,6 @@ use hbb_common::log; -use crate::{start_os_service, flutter::connection_manager}; +use crate::{start_os_service, flutter::connection_manager, start_server}; /// Main entry of the RustDesk Core. /// Return true if the app should continue running with UI(possibly Flutter), false if the app should exit. @@ -20,7 +20,15 @@ pub fn core_main() -> bool { return false; } if args[1] == "--server" { - // TODO: server + log::info!("start --server"); + #[cfg(not(target_os = "macos"))] + { + start_server(true); + } + #[cfg(target_os = "macos")] + { + std::thread::spawn(move || start_server(true)); + } return false; } } diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 0ead52f31..fe2673832 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -525,20 +525,27 @@ pub fn is_root() -> bool { crate::username() == "root" } +fn is_opensuse() -> bool { + if let Ok(res) = run_cmds("cat /etc/os-release | grep opensuse".to_owned()) { + if !res.is_empty() { + return true; + } + } + false +} + pub fn run_as_user(arg: &str) -> ResultType> { let uid = get_active_userid(); let cmd = std::env::current_exe()?; + let xdg = &format!("XDG_RUNTIME_DIR=/run/user/{}", uid) as &str; + let username = &get_active_username(); + let mut args = vec![xdg, "-u", username, cmd.to_str().unwrap_or(""), arg]; // -E required for opensuse - let task = std::process::Command::new("sudo") - .args(vec![ - "-E", - &format!("XDG_RUNTIME_DIR=/run/user/{}", uid) as &str, - "-u", - &get_active_username(), - cmd.to_str().unwrap_or(""), - arg, - ]) - .spawn()?; + if is_opensuse() { + args.insert(0, "-E"); + } + + let task = std::process::Command::new("sudo").args(args).spawn()?; Ok(Some(task)) } From bf7df67286b9f42585ae678a1f4bd1d0875d51b1 Mon Sep 17 00:00:00 2001 From: StephanStS Date: Fri, 9 Sep 2022 02:11:25 +0200 Subject: [PATCH 0431/2015] Added zip and make at Debian minimal installation Compile errors due to missing packages occured. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8abfbc9d3..8e484ef86 100644 --- a/README.md +++ b/README.md @@ -65,8 +65,8 @@ Please download sciter dynamic library yourself. ### 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 libayatana-appindicator3-1 libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libayatana-appindicator3-dev ``` From 21b277ea3fb2aabfd835eadfedadf81b50f4af3f Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 8 Sep 2022 00:35:19 -0700 Subject: [PATCH 0432/2015] flutter_desktop: check remote menu, mid commit Signed-off-by: fufesou --- .../lib/desktop/widgets/peercard_widget.dart | 35 +-- .../lib/desktop/widgets/remote_menubar.dart | 234 ++++++++++++++---- flutter/lib/desktop/widgets/utils.dart | 28 +++ src/flutter_ffi.rs | 30 ++- 4 files changed, 237 insertions(+), 90 deletions(-) create mode 100644 flutter/lib/desktop/widgets/utils.dart diff --git a/flutter/lib/desktop/widgets/peercard_widget.dart b/flutter/lib/desktop/widgets/peercard_widget.dart index fb1c59891..a809adf9c 100644 --- a/flutter/lib/desktop/widgets/peercard_widget.dart +++ b/flutter/lib/desktop/widgets/peercard_widget.dart @@ -1,7 +1,6 @@ import 'package:contextmenu/contextmenu.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:get/get.dart'; import '../../common.dart'; @@ -11,6 +10,7 @@ import '../../models/peer_model.dart'; import '../../models/platform_model.dart'; import './material_mod_popup_menu.dart' as mod_menu; import './popup_menu.dart'; +import './utils.dart'; class _PopupMenuTheme { static const Color commonColor = MyTheme.accent; @@ -32,7 +32,7 @@ class _PeerCard extends StatefulWidget { final Function(BuildContext, String) connect; final PopupMenuEntryBuilder popupMenuEntryBuilder; - _PeerCard( + const _PeerCard( {required this.peer, required this.alias, required this.connect, @@ -317,7 +317,7 @@ abstract class BasePeerCard extends StatelessWidget { return _PeerCard( peer: peer, alias: alias, - connect: (BuildContext context, String id) => _connect(context, id), + connect: (BuildContext context, String id) => connect(context, id), popupMenuEntryBuilder: _buildPopupMenuEntry, ); } @@ -337,31 +337,6 @@ abstract class BasePeerCard extends StatelessWidget { @protected Future>> _buildMenuItems(BuildContext context); - /// Connect to a peer with [id]. - /// If [isFileTransfer], starts a session only for file transfer. - /// If [isTcpTunneling], starts a session only for tcp tunneling. - /// If [isRDP], starts a session only for rdp. - void _connect(BuildContext context, String id, - {bool isFileTransfer = false, - bool isTcpTunneling = false, - bool isRDP = false}) async { - if (id == '') return; - id = id.replaceAll(' ', ''); - assert(!(isFileTransfer && isTcpTunneling && isRDP), - "more than one connect type"); - if (isFileTransfer) { - await rustDeskWinManager.newFileTransfer(id); - } else if (isTcpTunneling || isRDP) { - await rustDeskWinManager.newPortForward(id, isRDP); - } else { - await rustDeskWinManager.newRemoteDesktop(id); - } - FocusScopeNode currentFocus = FocusScope.of(context); - if (!currentFocus.hasPrimaryFocus) { - currentFocus.unfocus(); - } - } - MenuEntryBase _connectCommonAction( BuildContext context, String id, String title, {bool isFileTransfer = false, @@ -373,7 +348,7 @@ abstract class BasePeerCard extends StatelessWidget { style: style, ), proc: () { - _connect( + connect( context, peer.id, isFileTransfer: isFileTransfer, @@ -434,7 +409,7 @@ abstract class BasePeerCard extends StatelessWidget { ], )), proc: () { - _connect(context, id, isRDP: true); + connect(context, id, isRDP: true); }, dismissOnClicked: true, ); diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index c5e74be12..d7274b7ff 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -11,6 +11,7 @@ import '../../models/platform_model.dart'; import '../../common/shared_state.dart'; import './popup_menu.dart'; import './material_mod_popup_menu.dart' as mod_menu; +import './utils.dart'; class _MenubarTheme { static const Color commonColor = MyTheme.accent; @@ -225,7 +226,7 @@ class _RemoteMenubarState extends State { ), tooltip: translate('Control Actions'), position: mod_menu.PopupMenuPosition.under, - itemBuilder: (BuildContext context) => _getControlMenu() + itemBuilder: (BuildContext context) => _getControlMenu(context) .map((entry) => entry.build( context, const MenuConfig( @@ -297,66 +298,76 @@ class _RemoteMenubarState extends State { ); } - List> _getControlMenu() { + List> _getControlMenu(BuildContext context) { final pi = widget.ffi.ffiModel.pi; final perms = widget.ffi.ffiModel.permissions; final List> displayMenu = []; - - if (pi.version.isNotEmpty) { - displayMenu.add(MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - translate('Refresh'), - style: style, + displayMenu.addAll([ + MenuEntryButton( + childBuilder: (TextStyle? style) => Row( + children: [ + Text( + translate('OS Password'), + style: style, + ), + Expanded( + child: Align( + alignment: Alignment.centerRight, + child: IconButton( + padding: EdgeInsets.zero, + icon: const Icon(Icons.edit), + onPressed: () => showSetOSPassword( + widget.id, false, widget.ffi.dialogManager), + ), + )) + ], ), proc: () { - bind.sessionRefresh(id: widget.id); + showSetOSPassword(widget.id, false, widget.ffi.dialogManager); }, dismissOnClicked: true, - )); - } - displayMenu.add(MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - translate('OS Password'), - style: style, ), - proc: () { - showSetOSPassword(widget.id, false, widget.ffi.dialogManager); - }, - dismissOnClicked: true, - )); - - if (!isWebDesktop) { - if (perms['keyboard'] != false && perms['clipboard'] != false) { - displayMenu.add(MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - translate('Paste'), - style: style, - ), - proc: () { - () async { - ClipboardData? data = - await Clipboard.getData(Clipboard.kTextPlain); - if (data != null && data.text != null) { - bind.sessionInputString(id: widget.id, value: data.text ?? ""); - } - }(); - }, - dismissOnClicked: true, - )); - } - - displayMenu.add(MenuEntryButton( + MenuEntryButton( childBuilder: (TextStyle? style) => Text( - translate('Reset canvas'), + translate('Transfer File'), style: style, ), proc: () { - widget.ffi.cursorModel.reset(); + connect(context, widget.id, isFileTransfer: true); }, dismissOnClicked: true, - )); - } + ), + MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('TCP Tunneling'), + style: style, + ), + proc: () { + connect(context, widget.id, isTcpTunneling: true); + }, + dismissOnClicked: true, + ), + ]); + + // {handler.get_audit_server() &&
  • {translate('Note')}
  • } + final auditServer = bind.sessionGetAuditServerSync(id: widget.id); + //if (auditServer.isNotEmpty) { + displayMenu.add( + MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Note'), + style: style, + ), + proc: () { + showAuditDialog(widget.id, widget.ffi.dialogManager); + }, + dismissOnClicked: true, + ), + ); + //} + + displayMenu.add(MenuEntryDivider()); if (perms['keyboard'] != false) { if (pi.platform == 'Linux' || pi.sasEnabled) { @@ -371,7 +382,24 @@ class _RemoteMenubarState extends State { dismissOnClicked: true, )); } + } + if (gFFI.ffiModel.permissions["restart"] != false && + (pi.platform == "Linux" || + pi.platform == "Windows" || + pi.platform == "Mac OS")) { + displayMenu.add(MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Restart Remote Device'), + style: style, + ), + proc: () { + showRestartRemoteDevice(pi, widget.id, gFFI.dialogManager); + }, + dismissOnClicked: true, + )); + } + if (perms['keyboard'] != false) { displayMenu.add(MenuEntryButton( childBuilder: (TextStyle? style) => Text( translate('Insert Lock'), @@ -402,17 +430,46 @@ class _RemoteMenubarState extends State { } } - if (gFFI.ffiModel.permissions["restart"] != false && - (pi.platform == "Linux" || - pi.platform == "Windows" || - pi.platform == "Mac OS")) { + if (pi.version.isNotEmpty) { displayMenu.add(MenuEntryButton( childBuilder: (TextStyle? style) => Text( - translate('Restart Remote Device'), + translate('Refresh'), style: style, ), proc: () { - showRestartRemoteDevice(pi, widget.id, gFFI.dialogManager); + bind.sessionRefresh(id: widget.id); + }, + dismissOnClicked: true, + )); + } + + if (!isWebDesktop) { + // if (perms['keyboard'] != false && perms['clipboard'] != false) { + // displayMenu.add(MenuEntryButton( + // childBuilder: (TextStyle? style) => Text( + // translate('Paste'), + // style: style, + // ), + // proc: () { + // () async { + // ClipboardData? data = + // await Clipboard.getData(Clipboard.kTextPlain); + // if (data != null && data.text != null) { + // bind.sessionInputString(id: widget.id, value: data.text ?? ""); + // } + // }(); + // }, + // dismissOnClicked: true, + // )); + // } + + displayMenu.add(MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Reset canvas'), + style: style, + ), + proc: () { + widget.ffi.cursorModel.reset(); }, dismissOnClicked: true, )); @@ -684,3 +741,76 @@ void showSetOSPassword( ); }); } + +void showAuditDialog(String id, dialogManager) async { + final controller = TextEditingController(); + dialogManager.show((setState, close) { + submit() { + var text = controller.text.trim(); + if (text != "") { + bind.sessionSendNote(id: id, note: text); + } + close(); + } + + late final focusNode = FocusNode( + onKey: (FocusNode node, RawKeyEvent evt) { + if (evt.logicalKey.keyLabel == 'Enter') { + if (evt is RawKeyDownEvent) { + int pos = controller.selection.base.offset; + controller.text = + '${controller.text.substring(0, pos)}\n${controller.text.substring(pos)}'; + controller.selection = + TextSelection.fromPosition(TextPosition(offset: pos + 1)); + } + return KeyEventResult.handled; + } + if (evt.logicalKey.keyLabel == 'Esc') { + if (evt is RawKeyDownEvent) { + close(); + } + return KeyEventResult.handled; + } else { + return KeyEventResult.ignored; + } + }, + ); + + return CustomAlertDialog( + title: Text(translate('Note')), + content: SizedBox( + width: 250, + height: 120, + child: TextField( + autofocus: true, + keyboardType: TextInputType.multiline, + textInputAction: TextInputAction.newline, + decoration: const InputDecoration.collapsed( + hintText: "input note here", + ), + // inputFormatters: [ + // LengthLimitingTextInputFormatter(16), + // // FilteringTextInputFormatter(RegExp(r"[a-zA-z][a-zA-z0-9\_]*"), allow: true) + // ], + maxLines: null, + maxLength: 256, + controller: controller, + focusNode: focusNode, + )), + actions: [ + TextButton( + style: flatButtonStyle, + onPressed: close, + child: Text(translate('Cancel')), + ), + TextButton( + style: flatButtonStyle, + onPressed: submit, + child: Text(translate('OK')), + ), + ], + onSubmit: submit, + onCancel: close, + ); + }); +} diff --git a/flutter/lib/desktop/widgets/utils.dart b/flutter/lib/desktop/widgets/utils.dart new file mode 100644 index 000000000..2f555c239 --- /dev/null +++ b/flutter/lib/desktop/widgets/utils.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/utils/multi_window_manager.dart'; + +/// Connect to a peer with [id]. +/// If [isFileTransfer], starts a session only for file transfer. +/// If [isTcpTunneling], starts a session only for tcp tunneling. +/// If [isRDP], starts a session only for rdp. +void connect(BuildContext context, String id, + {bool isFileTransfer = false, + bool isTcpTunneling = false, + bool isRDP = false}) async { + if (id == '') return; + id = id.replaceAll(' ', ''); + assert(!(isFileTransfer && isTcpTunneling && isRDP), + "more than one connect type"); + + FocusScopeNode currentFocus = FocusScope.of(context); + if (isFileTransfer) { + await rustDeskWinManager.newFileTransfer(id); + } else if (isTcpTunneling || isRDP) { + await rustDeskWinManager.newPortForward(id, isRDP); + } else { + await rustDeskWinManager.newRemoteDesktop(id); + } + if (!currentFocus.hasPrimaryFocus) { + currentFocus.unfocus(); + } +} diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 56d69f4c4..10ab95487 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -17,15 +17,14 @@ use crate::flutter::{self, SESSIONS}; use crate::start_server; use crate::ui_interface; #[cfg(not(any(target_os = "android", target_os = "ios")))] -use crate::ui_interface::change_id; use crate::ui_interface::{ - check_mouse_time, check_super_user_permission, discover, forget_password, get_api_server, - get_app_name, get_async_job_status, get_connect_status, get_fav, get_id, get_lan_peers, - get_langs, get_license, get_local_option, get_mouse_time, get_option, get_options, get_peer, - get_peer_option, get_socks, get_sound_inputs, get_uuid, get_version, has_hwcodec, - has_rendezvous_service, post_request, send_to_cm, set_local_option, set_option, set_options, - set_peer_option, set_permanent_password, set_socks, store_fav, test_if_valid_server, - update_temporary_password, using_public_server, + change_id, check_mouse_time, check_super_user_permission, discover, forget_password, + get_api_server, get_app_name, get_async_job_status, get_connect_status, get_fav, get_id, + get_lan_peers, get_langs, get_license, get_local_option, get_mouse_time, get_option, + get_options, get_peer, get_peer_option, get_socks, get_sound_inputs, get_uuid, get_version, + has_hwcodec, has_rendezvous_service, post_request, send_to_cm, set_local_option, set_option, + set_options, set_peer_option, set_permanent_password, set_socks, store_fav, + test_if_valid_server, update_temporary_password, using_public_server, }; use crate::{ client::file_trait::FileManager, @@ -810,6 +809,21 @@ pub fn session_restart_remote_device(id: String) { } } +pub fn session_get_audit_server_sync(id: String) -> SyncReturn { + let res = if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.get_audit_server() + } else { + "".to_owned() + }; + SyncReturn(res) +} + +pub fn session_send_note(id: String, note: String) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.send_note(note) + } +} + pub fn main_set_home_dir(home: String) { *config::APP_HOME_DIR.write().unwrap() = home; } From 8d198a25548324eae151811e72c30feea433fba0 Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 8 Sep 2022 08:25:19 -0700 Subject: [PATCH 0433/2015] flutter_desktop: add action, allow file copy & paste Signed-off-by: fufesou --- .../lib/desktop/widgets/remote_menubar.dart | 40 ++++++++++++------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index d7274b7ff..327da889f 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hbb/models/chat_model.dart'; @@ -349,24 +351,22 @@ class _RemoteMenubarState extends State { dismissOnClicked: true, ), ]); - // {handler.get_audit_server() &&
  • {translate('Note')}
  • } final auditServer = bind.sessionGetAuditServerSync(id: widget.id); - //if (auditServer.isNotEmpty) { - displayMenu.add( - MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - translate('Note'), - style: style, + if (auditServer.isNotEmpty) { + displayMenu.add( + MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Note'), + style: style, + ), + proc: () { + showAuditDialog(widget.id, widget.ffi.dialogManager); + }, + dismissOnClicked: true, ), - proc: () { - showAuditDialog(widget.id, widget.ffi.dialogManager); - }, - dismissOnClicked: true, - ), - ); - //} - + ); + } displayMenu.add(MenuEntryDivider()); if (perms['keyboard'] != false) { @@ -599,6 +599,8 @@ class _RemoteMenubarState extends State { } }), MenuEntryDivider(), + // {show_codec ?
    {c.is_file_transfer || c.port_forward ? "" :
    {translate('Permissions')}
    } - {c.is_file_transfer || c.port_forward ? "" :
    + {c.is_file_transfer || c.port_forward ? "" :
    -
    } +
    +
    +
    + } {c.port_forward ?
    Port Forwarding: {c.port_forward}
    : ""}
    @@ -118,6 +121,15 @@ class Body: Reactor.Component }); } + event click $(icon.recording) { + var { cid, connection } = this; + checkClickTime(function() { + connection.recording = !connection.recording; + body.update(); + handler.switch_permission(cid, "recording", connection.recording); + }); + } + event click $(button#accept) { var { cid, connection } = this; checkClickTime(function() { @@ -276,7 +288,7 @@ function bring_to_top(idx=-1) { } } -handler.addConnection = function(id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart) { +handler.addConnection = function(id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart, recording) { stdout.println("new connection #" + id + ": " + peer_id); var conn; connections.map(function(c) { @@ -293,7 +305,7 @@ handler.addConnection = function(id, is_file_transfer, port_forward, peer_id, na port_forward: port_forward, name: name, authorized: authorized, time: new Date(), keyboard: keyboard, clipboard: clipboard, msgs: [], unreaded: 0, - audio: audio, file: file, restart: restart + audio: audio, file: file, restart: restart, recording: recording }); body.cur = connections.length - 1; bring_to_top(); diff --git a/src/ui/header.tis b/src/ui/header.tis index b8f1bdfd8..8f9fa8a32 100644 --- a/src/ui/header.tis +++ b/src/ui/header.tis @@ -144,7 +144,7 @@ class Header: Reactor.Component { {svg_action} {svg_display} {svg_keyboard} - {recording ? svg_recording_on : svg_recording_off} + {recording_enabled ? {recording ? svg_recording_on : svg_recording_off} : ""} {this.renderKeyboardPop()} {this.renderDisplayPop()} {this.renderActionPop()} diff --git a/src/ui/index.tis b/src/ui/index.tis index dc2f403fc..af69b450e 100644 --- a/src/ui/index.tis +++ b/src/ui/index.tis @@ -236,9 +236,11 @@ class Enhancements: Reactor.Component { } else if (v == 'screen-recording') { var dir = handler.get_option("video-save-directory"); if (!dir) dir = handler.default_video_save_directory(); + var ts0 = handler.get_option("enable-record-session") == '' ? { checked: true } : {}; var ts1 = handler.get_option("allow-auto-record-incoming") == 'Y' ? { checked: true } : {}; msgbox("custom-recording", translate('Recording'),
    +
    {translate('Enable Recording Session')}
    {translate('Automatically record incoming sessions')}
    {translate("Directory")}:  {dir}
    @@ -247,6 +249,7 @@ class Enhancements: Reactor.Component {
    , function(res=null) { if (!res) return; + handler.set_option("enable-record-session", res.enable_record_session ? '' : 'N'); handler.set_option("allow-auto-record-incoming", res.auto_record_incoming ? 'Y' : ''); handler.set_option("video-save-directory", $(#folderPath).text); }); diff --git a/src/ui/remote.tis b/src/ui/remote.tis index dc9d60d54..02f0de270 100644 --- a/src/ui/remote.tis +++ b/src/ui/remote.tis @@ -12,6 +12,7 @@ var clipboard_enabled = true; // server side var audio_enabled = true; // server side var file_enabled = true; // server side var restart_enabled = true; // server side +var recording_enabled = true; // server side var scroll_body = $(body); handler.setDisplay = function(x, y, w, h) { @@ -521,6 +522,7 @@ handler.setPermission = function(name, enabled) { if (name == "file") file_enabled = enabled; if (name == "clipboard") clipboard_enabled = enabled; if (name == "restart") restart_enabled = enabled; + if (name == "recording") recording_enabled = enabled; input_blocked = false; header.update(); }); diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index e4dbf80fb..3813760a0 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -40,6 +40,7 @@ pub struct Client { pub audio: bool, pub file: bool, pub restart: bool, + pub recording: bool, #[serde(skip)] tx: UnboundedSender, } @@ -94,6 +95,7 @@ impl ConnectionManager { audio: bool, file: bool, restart: bool, + recording: bool, tx: mpsc::UnboundedSender, ) { let client = Client { @@ -108,6 +110,7 @@ impl ConnectionManager { audio, file, restart, + recording, tx, }; self.ui_handler.add_connection(&client); @@ -250,11 +253,11 @@ pub async fn start_ipc(cm: ConnectionManager) { } Ok(Some(data)) => { match data { - Data::Login{id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, file_transfer_enabled, restart} => { + Data::Login{id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, file_transfer_enabled, restart, recording} => { log::debug!("conn_id: {}", id); conn_id = id; tx_file.send(ClipboardFileData::Enable((id, file_transfer_enabled))).ok(); - cm.add_connection(id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart, tx.clone()); + cm.add_connection(id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart, recording, tx.clone()); } Data::Close => { tx_file.send(ClipboardFileData::Enable((conn_id, false))).ok(); @@ -349,6 +352,7 @@ pub async fn start_listen( audio, file, restart, + recording, .. }) => { current_id = id; @@ -364,6 +368,7 @@ pub async fn start_listen( audio, file, restart, + recording, tx.clone(), ); } From aeeffad33bf3eb4e98e732a56b66edeb033a7c32 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 22 Sep 2022 15:59:51 +0800 Subject: [PATCH 0556/2015] fix peer widget overflow and tile bug, add more sync ffi --- Cargo.lock | 96 ++++++++++++++++++- .../lib/common/widgets/peercard_widget.dart | 59 ++++-------- src/flutter_ffi.rs | 53 ++++++++-- 3 files changed, 160 insertions(+), 48 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5627a861f..7dedfde5a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -58,6 +58,21 @@ dependencies = [ "atomic", ] +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + [[package]] name = "alsa" version = "0.6.0" @@ -420,6 +435,27 @@ dependencies = [ "once_cell", ] +[[package]] +name = "brotli" +version = "3.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ad2d4653bf5ca36ae797b1f4bb4dbddb60ce49ca4aed8a2ce4829f60425b80" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + [[package]] name = "bumpalo" version = "3.11.0" @@ -1359,6 +1395,19 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" +[[package]] +name = "embed-resource" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc24ff8d764818e9ab17963b0593c535f077a513f565e75e4352d758bc4d8c0" +dependencies = [ + "cc", + "rustc_version 0.4.0", + "toml", + "vswhom", + "winreg 0.10.1", +] + [[package]] name = "encoding_rs" version = "0.8.31" @@ -1548,7 +1597,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e1c54951450cbd39f3dbcf1005ac413b49487dabf18a720ad2383eccfeffb92" dependencies = [ "memoffset", - "rustc_version", + "rustc_version 0.3.3", ] [[package]] @@ -2822,6 +2871,12 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" +[[package]] +name = "md5" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" + [[package]] name = "memalloc" version = "0.1.0" @@ -4230,6 +4285,15 @@ dependencies = [ "semver 0.11.0", ] +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver 1.0.13", +] + [[package]] name = "rustdesk" version = "1.2.0" @@ -4305,6 +4369,16 @@ dependencies = [ "wol-rs", ] +[[package]] +name = "rustdesk-portable-packer" +version = "0.1.0" +dependencies = [ + "brotli", + "dirs", + "embed-resource", + "md5", +] + [[package]] name = "rustfft" version = "6.0.1" @@ -5374,6 +5448,26 @@ dependencies = [ "thiserror", ] +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22025f6d8eb903ebf920ea6933b70b1e495be37e2cb4099e62c80454aaf57c39" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "waker-fn" version = "1.1.0" diff --git a/flutter/lib/common/widgets/peercard_widget.dart b/flutter/lib/common/widgets/peercard_widget.dart index ecf89283a..1f30c89df 100644 --- a/flutter/lib/common/widgets/peercard_widget.dart +++ b/flutter/lib/common/widgets/peercard_widget.dart @@ -128,7 +128,8 @@ class _PeerCardState extends State<_PeerCard> Widget _buildPeerTile( BuildContext context, Peer peer, Rx deco) { final greyStyle = - TextStyle(fontSize: 12, color: MyTheme.color(context).lighterText); + TextStyle(fontSize: 11, color: MyTheme.color(context).lighterText); + final alias = bind.mainGetPeerOptionSync(id: peer.id, key: 'alias'); return Obx( () => Container( foregroundDecoration: deco.value, @@ -150,7 +151,6 @@ class _PeerCardState extends State<_PeerCard> children: [ Expanded( child: Column( - mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ Row(children: [ Padding( @@ -160,42 +160,21 @@ class _PeerCardState extends State<_PeerCard> backgroundColor: peer.online ? Colors.green : Colors.yellow)), - Text( - formatID(peer.id), + Expanded( + child: Text( + alias.isEmpty ? formatID(peer.id) : alias, style: const TextStyle(fontWeight: FontWeight.w400), - ), + overflow: TextOverflow.ellipsis, + )), ]), Align( alignment: Alignment.centerLeft, - child: FutureBuilder( - future: bind.mainGetPeerOption( - id: peer.id, key: 'alias'), - builder: (_, snapshot) { - if (snapshot.hasData) { - final name = snapshot.data!.isEmpty - ? '${peer.username}@${peer.hostname}' - : snapshot.data!; - return Tooltip( - message: name, - waitDuration: const Duration(seconds: 1), - child: Text( - name, - style: greyStyle, - textAlign: TextAlign.start, - overflow: TextOverflow.ellipsis, - ), - ); - } else { - // alias has not arrived - return Text( - '${peer.username}@${peer.hostname}', - style: greyStyle, - textAlign: TextAlign.start, - overflow: TextOverflow.ellipsis, - ); - } - }, + child: Text( + '${peer.username}@${peer.hostname}', + style: greyStyle, + textAlign: TextAlign.start, + overflow: TextOverflow.ellipsis, ), ), ], @@ -203,7 +182,7 @@ class _PeerCardState extends State<_PeerCard> ), _actionMore(peer), ], - ).paddingSymmetric(horizontal: 4.0), + ).paddingOnly(left: 10.0, top: 3.0), ), ) ], @@ -272,7 +251,8 @@ class _PeerCardState extends State<_PeerCard> child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row(children: [ + Expanded( + child: Row(children: [ Padding( padding: const EdgeInsets.fromLTRB(0, 4, 8, 4), child: CircleAvatar( @@ -280,9 +260,12 @@ class _PeerCardState extends State<_PeerCard> backgroundColor: peer.online ? Colors.green : Colors.yellow)), - Text( - peer.alias.isEmpty ? formatID(peer.id) : peer.alias) - ]).paddingSymmetric(vertical: 8), + Expanded( + child: Text( + peer.alias.isEmpty ? formatID(peer.id) : peer.alias, + overflow: TextOverflow.ellipsis, + )), + ]).paddingSymmetric(vertical: 8)), _actionMore(peer), ], ).paddingSymmetric(horizontal: 12.0), diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index f41108895..f859125bd 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -15,17 +15,18 @@ use hbb_common::{message_proto::Hash, ResultType}; use crate::flutter::{self, SESSIONS}; use crate::start_server; -use crate::ui_interface; #[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::ui_interface::get_sound_inputs; use crate::ui_interface::{ - change_id, check_mouse_time, check_super_user_permission, discover, forget_password, + self, change_id, check_mouse_time, check_super_user_permission, discover, forget_password, get_api_server, get_app_name, get_async_job_status, get_connect_status, get_fav, get_id, get_lan_peers, get_langs, get_license, get_local_option, get_mouse_time, get_option, get_options, get_peer, get_peer_option, get_socks, get_uuid, get_version, has_hwcodec, - has_rendezvous_service, post_request, send_to_cm, set_local_option, set_option, set_options, - set_peer_option, set_permanent_password, set_socks, store_fav, test_if_valid_server, - update_temporary_password, using_public_server, + has_rendezvous_service, is_can_screen_recording, is_installed, is_installed_daemon, + is_installed_lower_version, is_process_trusted, is_rdp_service_open, is_share_rdp, + post_request, send_to_cm, set_local_option, set_option, set_options, set_peer_option, + set_permanent_password, set_socks, store_fav, test_if_valid_server, update_temporary_password, + using_public_server, }; use crate::{ client::file_trait::FileManager, @@ -557,10 +558,19 @@ pub fn main_get_peer_option(id: String, key: String) -> String { get_peer_option(id, key) } +pub fn main_get_peer_option_sync(id: String, key: String) -> SyncReturn { + SyncReturn(get_peer_option(id, key)) +} + pub fn main_set_peer_option(id: String, key: String, value: String) { set_peer_option(id, key, value) } +pub fn main_set_peer_option_sync(id: String, key: String, value: String) -> SyncReturn { + set_peer_option(id, key, value); + SyncReturn(true) +} + pub fn main_forget_password(id: String) { forget_password(id) } @@ -693,10 +703,7 @@ fn main_broadcast_message(data: &HashMap<&str, &str>) { } pub fn main_change_theme(dark: String) { - main_broadcast_message(&HashMap::from([ - ("name", "theme"), - ("dark", &dark), - ])); + main_broadcast_message(&HashMap::from([("name", "theme"), ("dark", &dark)])); send_to_cm(&crate::ipc::Data::Theme(dark)); } @@ -972,6 +979,34 @@ pub fn query_onlines(ids: Vec) { crate::rendezvous_mediator::query_online_states(ids, handle_query_onlines) } +pub fn main_is_installed() -> SyncReturn { + SyncReturn(is_installed()) +} + +pub fn main_is_installed_lower_version() -> SyncReturn { + SyncReturn(is_installed_lower_version()) +} + +pub fn main_is_installed_daemon(prompt: bool) -> SyncReturn { + SyncReturn(is_installed_daemon(prompt)) +} + +pub fn main_is_process_trusted(prompt: bool) -> SyncReturn { + SyncReturn(is_process_trusted(prompt)) +} + +pub fn main_is_can_screen_recording(prompt: bool) -> SyncReturn { + SyncReturn(is_can_screen_recording(prompt)) +} + +pub fn main_is_share_rdp() -> SyncReturn { + SyncReturn(is_share_rdp()) +} + +pub fn main_is_rdp_service_open() -> SyncReturn { + SyncReturn(is_rdp_service_open()) +} + #[cfg(target_os = "android")] pub mod server_side { use jni::{ From 78efa66378c53e0ec33eefa83e8ee48d3c1fe741 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 22 Sep 2022 16:18:06 +0800 Subject: [PATCH 0557/2015] locked only if installed, to-do: need refine here --- 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 5ab3b9a51..60bc96b47 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -331,7 +331,7 @@ class _Safety extends StatefulWidget { class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { @override bool get wantKeepAlive => true; - bool locked = true; + bool locked = bind.mainIsInstalled(); final scrollController = ScrollController(); @override From 36cd2622273345b31badf91245700f9015c340c5 Mon Sep 17 00:00:00 2001 From: csf Date: Thu, 22 Sep 2022 15:12:23 +0800 Subject: [PATCH 0558/2015] mobile dark theme options --- flutter/lib/mobile/pages/settings_page.dart | 56 ++++++++++++++------- 1 file changed, 39 insertions(+), 17 deletions(-) diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index 6f986ee78..93120427a 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -25,14 +25,13 @@ class SettingsPage extends StatefulWidget implements PageShape { final appBarActions = [ScanButton()]; @override - _SettingsState createState() => _SettingsState(); + State createState() => _SettingsState(); } const url = 'https://rustdesk.com/'; final _hasIgnoreBattery = androidVersion >= 26; var _ignoreBatteryOpt = false; var _enableAbr = false; -var _isDarkMode = false; class _SettingsState extends State with WidgetsBindingObserver { String? username; @@ -60,8 +59,6 @@ class _SettingsState extends State with WidgetsBindingObserver { _enableAbr = enableAbrRes; } - // _isDarkMode = MyTheme.currentDarkMode(); // TODO - if (update) { setState(() {}); } @@ -100,7 +97,7 @@ class _SettingsState extends State with WidgetsBindingObserver { Provider.of(context); final enhancementsTiles = [ SettingsTile.switchTile( - title: Text(translate('Adaptive Bitrate') + ' (beta)'), + title: Text('${translate('Adaptive Bitrate')} (beta)'), initialValue: _enableAbr, onToggle: (v) { bind.mainSetOption(key: "enable-abr", value: v ? "" : "N"); @@ -152,7 +149,7 @@ class _SettingsState extends State with WidgetsBindingObserver { SettingsTile.navigation( title: Text(username == null ? translate("Login") - : translate("Logout") + ' ($username)'), + : '${translate("Logout")} ($username)'), leading: Icon(Icons.person), onPressed: (context) { if (username == null) { @@ -177,15 +174,11 @@ class _SettingsState extends State with WidgetsBindingObserver { onPressed: (context) { showLanguageSettings(gFFI.dialogManager); }), - SettingsTile.switchTile( + SettingsTile.navigation( title: Text(translate('Dark Theme')), leading: Icon(Icons.dark_mode), - initialValue: _isDarkMode, - onToggle: (v) { - setState(() { - _isDarkMode = !_isDarkMode; - // MyTheme.changeDarkMode(_isDarkMode); // TODO - }); + onPressed: (context) { + showThemeSettings(gFFI.dialogManager); }, ) ]), @@ -232,7 +225,7 @@ void showLanguageSettings(OverlayDialogManager dialogManager) async { final langs = json.decode(await bind.mainGetLangs()) as List; var lang = await bind.mainGetLocalOption(key: "lang"); dialogManager.show((setState, close) { - final setLang = (v) { + setLang(v) { if (lang != v) { setState(() { lang = v; @@ -241,7 +234,8 @@ void showLanguageSettings(OverlayDialogManager dialogManager) async { HomePage.homeKey.currentState?.refreshPages(); Future.delayed(Duration(milliseconds: 200), close); } - }; + } + return CustomAlertDialog( title: SizedBox.shrink(), content: Column( @@ -257,13 +251,41 @@ void showLanguageSettings(OverlayDialogManager dialogManager) async { ), actions: []); }, backDismiss: true, clickMaskDismiss: true); - } catch (_e) {} + } catch (e) { + // + } +} + +void showThemeSettings(OverlayDialogManager dialogManager) async { + var themeMode = MyTheme.getThemeModePreference(); + + dialogManager.show((setState, close) { + setTheme(v) { + if (themeMode != v) { + setState(() { + themeMode = v; + }); + MyTheme.changeDarkMode(themeMode); + Future.delayed(Duration(milliseconds: 200), close); + } + } + + return CustomAlertDialog( + title: SizedBox.shrink(), + contentPadding: 10, + content: Column(children: [ + getRadio('Light', ThemeMode.light, themeMode, setTheme), + getRadio('Dark', ThemeMode.dark, themeMode, setTheme), + getRadio('Follow System', ThemeMode.system, themeMode, setTheme) + ]), + actions: []); + }, backDismiss: true, clickMaskDismiss: true); } void showAbout(OverlayDialogManager dialogManager) { dialogManager.show((setState, close) { return CustomAlertDialog( - title: Text(translate('About') + ' RustDesk'), + title: Text('${translate('About')} RustDesk'), content: Wrap(direction: Axis.vertical, spacing: 12, children: [ Text('Version: $version'), InkWell( From 9bbc3376a42ca3112293818ec3873206c75659b0 Mon Sep 17 00:00:00 2001 From: csf Date: Thu, 22 Sep 2022 15:35:46 +0800 Subject: [PATCH 0559/2015] refactor: rename to peer_card.dart and peers_view.dart --- flutter/lib/common/widgets/address_book.dart | 4 +- .../{peercard_widget.dart => peer_card.dart} | 1 - flutter/lib/common/widgets/peer_tab_page.dart | 4 +- .../{peer_widget.dart => peers_view.dart} | 56 +++++++++---------- .../lib/desktop/pages/connection_page.dart | 8 +-- flutter/lib/mobile/pages/connection_page.dart | 8 +-- 6 files changed, 40 insertions(+), 41 deletions(-) rename flutter/lib/common/widgets/{peercard_widget.dart => peer_card.dart} (99%) rename flutter/lib/common/widgets/{peer_widget.dart => peers_view.dart} (86%) diff --git a/flutter/lib/common/widgets/address_book.dart b/flutter/lib/common/widgets/address_book.dart index beecf47f2..ca5e85f56 100644 --- a/flutter/lib/common/widgets/address_book.dart +++ b/flutter/lib/common/widgets/address_book.dart @@ -1,6 +1,6 @@ import 'package:contextmenu/contextmenu.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_hbb/common/widgets/peer_widget.dart'; +import 'package:flutter_hbb/common/widgets/peers_view.dart'; import 'package:flutter_hbb/models/ab_model.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; @@ -174,7 +174,7 @@ class _AddressBookState extends State { Expanded( child: Align( alignment: Alignment.topLeft, - child: AddressBookPeerWidget()), + child: AddressBookPeersView()), ) ], )); diff --git a/flutter/lib/common/widgets/peercard_widget.dart b/flutter/lib/common/widgets/peer_card.dart similarity index 99% rename from flutter/lib/common/widgets/peercard_widget.dart rename to flutter/lib/common/widgets/peer_card.dart index 1f30c89df..bf0c93de5 100644 --- a/flutter/lib/common/widgets/peercard_widget.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -489,7 +489,6 @@ abstract class BasePeerCard extends StatelessWidget { await bind.mainRemovePeer(id: id); removePreference(id); await reloadFunc(); - // Get.forceAppUpdate(); // TODO use inner model / state }(); }, dismissOnClicked: true, diff --git a/flutter/lib/common/widgets/peer_tab_page.dart b/flutter/lib/common/widgets/peer_tab_page.dart index fefe74671..2a1fe9909 100644 --- a/flutter/lib/common/widgets/peer_tab_page.dart +++ b/flutter/lib/common/widgets/peer_tab_page.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:flutter_hbb/common/widgets/peer_widget.dart'; -import 'package:flutter_hbb/common/widgets/peercard_widget.dart'; +import 'package:flutter_hbb/common/widgets/peers_view.dart'; +import 'package:flutter_hbb/common/widgets/peer_card.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:get/get.dart'; diff --git a/flutter/lib/common/widgets/peer_widget.dart b/flutter/lib/common/widgets/peers_view.dart similarity index 86% rename from flutter/lib/common/widgets/peer_widget.dart rename to flutter/lib/common/widgets/peers_view.dart index e6236ff4e..cf9c4299a 100644 --- a/flutter/lib/common/widgets/peer_widget.dart +++ b/flutter/lib/common/widgets/peers_view.dart @@ -11,34 +11,34 @@ import 'package:window_manager/window_manager.dart'; import '../../common.dart'; import '../../models/peer_model.dart'; import '../../models/platform_model.dart'; -import 'peercard_widget.dart'; +import 'peer_card.dart'; typedef OffstageFunc = bool Function(Peer peer); -typedef PeerCardWidgetFunc = Widget Function(Peer peer); +typedef PeerCardBuilder = BasePeerCard Function(Peer peer); /// for peer search text, global obs value final peerSearchText = "".obs; final peerSearchTextController = TextEditingController(text: peerSearchText.value); -class _PeerWidget extends StatefulWidget { +class _PeersView extends StatefulWidget { final Peers peers; final OffstageFunc offstageFunc; - final PeerCardWidgetFunc peerCardWidgetFunc; + final PeerCardBuilder peerCardBuilder; - const _PeerWidget( + const _PeersView( {required this.peers, required this.offstageFunc, - required this.peerCardWidgetFunc, + required this.peerCardBuilder, Key? key}) : super(key: key); @override - _PeerWidgetState createState() => _PeerWidgetState(); + _PeersViewState createState() => _PeersViewState(); } /// State for the peer widget. -class _PeerWidgetState extends State<_PeerWidget> with WindowListener { +class _PeersViewState extends State<_PeersView> with WindowListener { static const int _maxQueryCount = 3; final space = isDesktop ? 12.0 : 8.0; final _curPeers = {}; @@ -60,7 +60,7 @@ class _PeerWidgetState extends State<_PeerWidget> with WindowListener { return width; }(); - _PeerWidgetState() { + _PeersViewState() { _startCheckOnlines(); } @@ -119,7 +119,7 @@ class _PeerWidgetState extends State<_PeerWidget> with WindowListener { } _lastChangeTime = DateTime.now(); }, - child: widget.peerCardWidgetFunc(peer), + child: widget.peerCardBuilder(peer), ); cards.add(Offstage( key: ValueKey("off${peer.id}"), @@ -198,39 +198,39 @@ class _PeerWidgetState extends State<_PeerWidget> with WindowListener { } } -abstract class BasePeerWidget extends StatelessWidget { +abstract class BasePeersView extends StatelessWidget { final String name; final String loadEvent; final OffstageFunc offstageFunc; - final PeerCardWidgetFunc peerCardWidgetFunc; + final PeerCardBuilder peerCardBuilder; final List initPeers; - const BasePeerWidget({ + const BasePeersView({ Key? key, required this.name, required this.loadEvent, required this.offstageFunc, - required this.peerCardWidgetFunc, + required this.peerCardBuilder, required this.initPeers, }) : super(key: key); @override Widget build(BuildContext context) { - return _PeerWidget( + return _PeersView( peers: Peers(name: name, loadEvent: loadEvent, peers: initPeers), offstageFunc: offstageFunc, - peerCardWidgetFunc: peerCardWidgetFunc); + peerCardBuilder: peerCardBuilder); } } -class RecentPeerWidget extends BasePeerWidget { - RecentPeerWidget({Key? key}) +class RecentPeersView extends BasePeersView { + RecentPeersView({Key? key}) : super( key: key, name: 'recent peer', loadEvent: 'load_recent_peers', offstageFunc: (Peer peer) => false, - peerCardWidgetFunc: (Peer peer) => RecentPeerCard( + peerCardBuilder: (Peer peer) => RecentPeerCard( peer: peer, ), initPeers: [], @@ -244,14 +244,14 @@ class RecentPeerWidget extends BasePeerWidget { } } -class FavoritePeerWidget extends BasePeerWidget { - FavoritePeerWidget({Key? key}) +class FavoritePeersView extends BasePeersView { + FavoritePeersView({Key? key}) : super( key: key, name: 'favorite peer', loadEvent: 'load_fav_peers', offstageFunc: (Peer peer) => false, - peerCardWidgetFunc: (Peer peer) => FavoritePeerCard( + peerCardBuilder: (Peer peer) => FavoritePeerCard( peer: peer, ), initPeers: [], @@ -265,14 +265,14 @@ class FavoritePeerWidget extends BasePeerWidget { } } -class DiscoveredPeerWidget extends BasePeerWidget { - DiscoveredPeerWidget({Key? key}) +class DiscoveredPeersView extends BasePeersView { + DiscoveredPeersView({Key? key}) : super( key: key, name: 'discovered peer', loadEvent: 'load_lan_peers', offstageFunc: (Peer peer) => false, - peerCardWidgetFunc: (Peer peer) => DiscoveredPeerCard( + peerCardBuilder: (Peer peer) => DiscoveredPeerCard( peer: peer, ), initPeers: [], @@ -286,15 +286,15 @@ class DiscoveredPeerWidget extends BasePeerWidget { } } -class AddressBookPeerWidget extends BasePeerWidget { - AddressBookPeerWidget({Key? key}) +class AddressBookPeersView extends BasePeersView { + AddressBookPeersView({Key? key}) : super( key: key, name: 'address book peer', loadEvent: 'load_address_book_peers', offstageFunc: (Peer peer) => !_hitTag(gFFI.abModel.selectedTags, peer.tags), - peerCardWidgetFunc: (Peer peer) => AddressBookPeerCard( + peerCardBuilder: (Peer peer) => AddressBookPeerCard( peer: peer, ), initPeers: _loadPeers(), diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index ad8e430f4..6a8c58f7b 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -11,7 +11,7 @@ import 'package:url_launcher/url_launcher_string.dart'; import '../../common.dart'; import '../../common/formatter/id_formatter.dart'; import '../../common/widgets/peer_tab_page.dart'; -import '../../common/widgets/peer_widget.dart'; +import '../../common/widgets/peers_view.dart'; import '../../models/platform_model.dart'; /// Connection page for connecting to a remote peer. @@ -74,9 +74,9 @@ class _ConnectionPageState extends State { translate('Address Book') ], children: [ - RecentPeerWidget(), - FavoritePeerWidget(), - DiscoveredPeerWidget(), + RecentPeersView(), + FavoritePeersView(), + DiscoveredPeersView(), const AddressBook(), ], )), diff --git a/flutter/lib/mobile/pages/connection_page.dart b/flutter/lib/mobile/pages/connection_page.dart index edc2f5f6d..0778bec4e 100644 --- a/flutter/lib/mobile/pages/connection_page.dart +++ b/flutter/lib/mobile/pages/connection_page.dart @@ -10,7 +10,7 @@ import 'package:url_launcher/url_launcher.dart'; import '../../common.dart'; import '../../common/widgets/address_book.dart'; import '../../common/widgets/peer_tab_page.dart'; -import '../../common/widgets/peer_widget.dart'; +import '../../common/widgets/peers_view.dart'; import '../../consts.dart'; import '../../models/model.dart'; import '../../models/platform_model.dart'; @@ -84,9 +84,9 @@ class _ConnectionPageState extends State { translate('Address Book') ], children: [ - RecentPeerWidget(), - FavoritePeerWidget(), - DiscoveredPeerWidget(), + RecentPeersView(), + FavoritePeersView(), + DiscoveredPeersView(), const AddressBook(), ], )), From 00077676f4d0fac4b56eb8cb6a9a3767f81b9bba Mon Sep 17 00:00:00 2001 From: csf Date: Thu, 22 Sep 2022 16:45:14 +0800 Subject: [PATCH 0560/2015] 1. new mobile connect. 2. _forceAlwaysRelayAction dismissOnClicked: false. 3. no tcp tunneling on mobile 4. adjust peer tab border on mobile --- flutter/lib/common.dart | 38 ++++++++++++--- flutter/lib/common/widgets/peer_card.dart | 48 ++++++++----------- flutter/lib/common/widgets/peer_tab_page.dart | 2 +- flutter/lib/mobile/pages/connection_page.dart | 35 +------------- 4 files changed, 55 insertions(+), 68 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 365ce3dd5..5cd54a0a8 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -17,6 +17,8 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'package:window_manager/window_manager.dart'; import 'common/widgets/overlay.dart'; +import 'mobile/pages/file_manager_page.dart'; +import 'mobile/pages/remote_page.dart'; import 'models/model.dart'; import 'models/platform_model.dart'; @@ -1071,14 +1073,38 @@ void connect(BuildContext context, String id, assert(!(isFileTransfer && isTcpTunneling && isRDP), "more than one connect type"); - FocusScopeNode currentFocus = FocusScope.of(context); - if (isFileTransfer) { - await rustDeskWinManager.newFileTransfer(id); - } else if (isTcpTunneling || isRDP) { - await rustDeskWinManager.newPortForward(id, isRDP); + if (isDesktop) { + if (isFileTransfer) { + await rustDeskWinManager.newFileTransfer(id); + } else if (isTcpTunneling || isRDP) { + await rustDeskWinManager.newPortForward(id, isRDP); + } else { + await rustDeskWinManager.newRemoteDesktop(id); + } } else { - await rustDeskWinManager.newRemoteDesktop(id); + if (isFileTransfer) { + if (!await PermissionManager.check("file")) { + if (!await PermissionManager.request("file")) { + return; + } + } + Navigator.push( + context, + MaterialPageRoute( + builder: (BuildContext context) => FileManagerPage(id: id), + ), + ); + } else { + Navigator.push( + context, + MaterialPageRoute( + builder: (BuildContext context) => RemotePage(id: id), + ), + ); + } } + + FocusScopeNode currentFocus = FocusScope.of(context); if (!currentFocus.hasPrimaryFocus) { currentFocus.unfocus(); } diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index bf0c93de5..7eae2aa5f 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -77,8 +77,11 @@ class _PeerCardState extends State<_PeerCard> subtitle: Text('${peer.username}@${peer.hostname}'), title: Text(peer.alias.isEmpty ? formatID(peer.id) : peer.alias), leading: Container( + decoration: BoxDecoration( + color: str2color('${peer.id}${peer.platform}', 0x7f), + borderRadius: BorderRadius.circular(4), + ), padding: const EdgeInsets.all(6), - color: str2color('${peer.id}${peer.platform}', 0x7f), child: getPlatformImage(peer.platform)), trailing: InkWell( child: const Padding( @@ -458,7 +461,7 @@ abstract class BasePeerCard extends StatelessWidget { } await bind.mainSetPeerOption(id: id, key: option, value: value); }, - dismissOnClicked: true, + dismissOnClicked: false, ); } @@ -543,7 +546,6 @@ abstract class BasePeerCard extends StatelessWidget { if (favs.remove(id)) { await bind.mainStoreFav(favs: favs); await reloadFunc(); - // Get.forceAppUpdate(); // TODO use inner model / state } }(); }, @@ -624,15 +626,13 @@ class RecentPeerCard extends BasePeerCard { final List> menuItems = [ _connectAction(context, peer), _transferFileAction(context, peer.id), - _tcpTunnelingAction(context, peer.id), ]; - MenuEntryBase? rdpAction; - if (peer.platform == 'Windows') { - rdpAction = _rdpAction(context, peer.id); + if (isDesktop) { + menuItems.add(_tcpTunnelingAction(context, peer.id)); } menuItems.add(await _forceAlwaysRelayAction(peer.id)); - if (rdpAction != null) { - menuItems.add(rdpAction); + if (peer.platform == 'Windows') { + menuItems.add(_rdpAction(context, peer.id)); } menuItems.add(_wolAction(peer.id)); menuItems.add(MenuEntryDivider()); @@ -656,15 +656,13 @@ class FavoritePeerCard extends BasePeerCard { final List> menuItems = [ _connectAction(context, peer), _transferFileAction(context, peer.id), - _tcpTunnelingAction(context, peer.id), ]; - MenuEntryBase? rdpAction; - if (peer.platform == 'Windows') { - rdpAction = _rdpAction(context, peer.id); + if (isDesktop) { + menuItems.add(_tcpTunnelingAction(context, peer.id)); } menuItems.add(await _forceAlwaysRelayAction(peer.id)); - if (rdpAction != null) { - menuItems.add(rdpAction); + if (peer.platform == 'Windows') { + menuItems.add(_rdpAction(context, peer.id)); } menuItems.add(_wolAction(peer.id)); menuItems.add(MenuEntryDivider()); @@ -690,15 +688,13 @@ class DiscoveredPeerCard extends BasePeerCard { final List> menuItems = [ _connectAction(context, peer), _transferFileAction(context, peer.id), - _tcpTunnelingAction(context, peer.id), ]; - MenuEntryBase? rdpAction; - if (peer.platform == 'Windows') { - rdpAction = _rdpAction(context, peer.id); + if (isDesktop) { + menuItems.add(_tcpTunnelingAction(context, peer.id)); } menuItems.add(await _forceAlwaysRelayAction(peer.id)); - if (rdpAction != null) { - menuItems.add(rdpAction); + if (peer.platform == 'Windows') { + menuItems.add(_rdpAction(context, peer.id)); } menuItems.add(_wolAction(peer.id)); menuItems.add(MenuEntryDivider()); @@ -721,15 +717,13 @@ class AddressBookPeerCard extends BasePeerCard { final List> menuItems = [ _connectAction(context, peer), _transferFileAction(context, peer.id), - _tcpTunnelingAction(context, peer.id), ]; - MenuEntryBase? rdpAction; - if (peer.platform == 'Windows') { - rdpAction = _rdpAction(context, peer.id); + if (isDesktop) { + menuItems.add(_tcpTunnelingAction(context, peer.id)); } menuItems.add(await _forceAlwaysRelayAction(peer.id)); - if (rdpAction != null) { - menuItems.add(rdpAction); + if (peer.platform == 'Windows') { + menuItems.add(_rdpAction(context, peer.id)); } menuItems.add(_wolAction(peer.id)); menuItems.add(MenuEntryDivider()); diff --git a/flutter/lib/common/widgets/peer_tab_page.dart b/flutter/lib/common/widgets/peer_tab_page.dart index 2a1fe9909..3ed3dc11d 100644 --- a/flutter/lib/common/widgets/peer_tab_page.dart +++ b/flutter/lib/common/widgets/peer_tab_page.dart @@ -113,7 +113,7 @@ class _PeerTabPageState extends State color: _tabIndex.value == t.key ? MyTheme.color(context).bg : null, - borderRadius: BorderRadius.circular(2), + borderRadius: BorderRadius.circular(isDesktop ? 2 : 6), ), child: Align( alignment: Alignment.center, diff --git a/flutter/lib/mobile/pages/connection_page.dart b/flutter/lib/mobile/pages/connection_page.dart index 0778bec4e..6156223b5 100644 --- a/flutter/lib/mobile/pages/connection_page.dart +++ b/flutter/lib/mobile/pages/connection_page.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common/formatter/id_formatter.dart'; -import 'package:flutter_hbb/mobile/pages/file_manager_page.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -15,7 +14,6 @@ import '../../consts.dart'; import '../../models/model.dart'; import '../../models/platform_model.dart'; import 'home_page.dart'; -import 'remote_page.dart'; import 'scan_page.dart'; import 'settings_page.dart'; @@ -97,38 +95,7 @@ class _ConnectionPageState extends State { /// Connects to the selected peer. void onConnect() { var id = _idController.id; - connect(id); - } - - /// Connect to a peer with [id]. - /// If [isFileTransfer], starts a session only for file transfer. - void connect(String id, {bool isFileTransfer = false}) async { - if (id == '') return; - id = id.replaceAll(' ', ''); - if (isFileTransfer) { - if (!await PermissionManager.check("file")) { - if (!await PermissionManager.request("file")) { - return; - } - } - Navigator.push( - context, - MaterialPageRoute( - builder: (BuildContext context) => FileManagerPage(id: id), - ), - ); - } else { - Navigator.push( - context, - MaterialPageRoute( - builder: (BuildContext context) => RemotePage(id: id), - ), - ); - } - FocusScopeNode currentFocus = FocusScope.of(context); - if (!currentFocus.hasPrimaryFocus) { - currentFocus.unfocus(); - } + connect(context, id); } /// UI for software update. From 51b02353c9177a73944cc71d8e42943791f64f17 Mon Sep 17 00:00:00 2001 From: csf Date: Thu, 22 Sep 2022 17:38:18 +0800 Subject: [PATCH 0561/2015] 1. mobile ab login. 2. typos 3. del rename dialog body padding --- flutter/lib/common/widgets/address_book.dart | 16 ++++++---- flutter/lib/common/widgets/peer_card.dart | 2 -- .../lib/desktop/pages/desktop_home_page.dart | 7 +++-- .../desktop/pages/desktop_setting_page.dart | 30 ++++++++++--------- flutter/lib/mobile/pages/settings_page.dart | 2 +- flutter/lib/mobile/widgets/dialog.dart | 18 ++++++----- 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/fr.rs | 2 +- src/lang/hu.rs | 2 +- src/lang/id.rs | 2 +- src/lang/it.rs | 2 +- src/lang/ja.rs | 2 +- src/lang/ko.rs | 2 +- src/lang/pl.rs | 2 +- src/lang/pt_PT.rs | 2 +- src/lang/ptbr.rs | 2 +- src/lang/ru.rs | 2 +- src/lang/sk.rs | 2 +- src/lang/template.rs | 2 +- src/lang/tr.rs | 2 +- src/lang/tw.rs | 2 +- src/lang/vn.rs | 2 +- 27 files changed, 64 insertions(+), 53 deletions(-) diff --git a/flutter/lib/common/widgets/address_book.dart b/flutter/lib/common/widgets/address_book.dart index ca5e85f56..9fac81723 100644 --- a/flutter/lib/common/widgets/address_book.dart +++ b/flutter/lib/common/widgets/address_book.dart @@ -7,6 +7,7 @@ import 'package:provider/provider.dart'; import '../../common.dart'; import '../../desktop/pages/desktop_home_page.dart'; +import '../../mobile/pages/settings_page.dart'; import '../../models/platform_model.dart'; class AddressBook extends StatefulWidget { @@ -37,11 +38,16 @@ class _AddressBookState extends State { }); handleLogin() { - loginDialog().then((success) { - if (success) { - setState(() {}); - } - }); + // TODO refactor login dialog for desktop and mobile + if (isDesktop) { + loginDialog().then((success) { + if (success) { + setState(() {}); + } + }); + } else { + showLogin(gFFI.dialogManager); + } } Future buildAddressBook(BuildContext context) async { diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index 7eae2aa5f..9c0c997bc 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -590,8 +590,6 @@ abstract class BasePeerCard extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( - padding: - const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), child: Form( child: TextFormField( controller: controller, diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index edae7deeb..833a914cd 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -331,7 +331,7 @@ Future loginDialog() async { var userNameMsg = ""; String pass = ""; var passMsg = ""; - var userContontroller = TextEditingController(text: userName); + var userController = TextEditingController(text: userName); var pwdController = TextEditingController(text: pass); var isInProgress = false; @@ -349,7 +349,7 @@ Future loginDialog() async { }); } - userName = userContontroller.text; + userName = userController.text; pass = pwdController.text; if (userName.isEmpty) { userNameMsg = translate("Username missed"); @@ -385,6 +385,7 @@ Future loginDialog() async { close(); } + // 登录dialog return CustomAlertDialog( title: Text(translate("Login")), content: ConstrainedBox( @@ -411,7 +412,7 @@ Future loginDialog() async { decoration: InputDecoration( border: const OutlineInputBorder(), errorText: userNameMsg.isNotEmpty ? userNameMsg : null), - controller: userContontroller, + controller: userController, focusNode: FocusNode()..requestFocus(), ), ), diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 60bc96b47..0918fc59b 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -48,7 +48,7 @@ class _DesktopSettingPageState extends State _TabInfo('Security', Icons.enhanced_encryption_outlined, Icons.enhanced_encryption), _TabInfo('Network', Icons.link_outlined, Icons.link), - _TabInfo('Acount', Icons.person_outline, Icons.person), + _TabInfo('Account', Icons.person_outline, Icons.person), _TabInfo('About', Icons.info_outline, Icons.info) ]; @@ -92,7 +92,7 @@ class _DesktopSettingPageState extends State _General(), _Safety(), _Network(), - _Acount(), + _Account(), _About(), ], )), @@ -641,14 +641,14 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin { } } -class _Acount extends StatefulWidget { - const _Acount({Key? key}) : super(key: key); +class _Account extends StatefulWidget { + const _Account({Key? key}) : super(key: key); @override - State<_Acount> createState() => _AcountState(); + State<_Account> createState() => _AccountState(); } -class _AcountState extends State<_Acount> { +class _AccountState extends State<_Account> { @override Widget build(BuildContext context) { final scrollController = ScrollController(); @@ -658,12 +658,12 @@ class _AcountState extends State<_Acount> { physics: NeverScrollableScrollPhysics(), controller: scrollController, children: [ - _Card(title: 'Acount', children: [login()]), + _Card(title: 'Account', children: [accountAction()]), ], ).marginOnly(bottom: _kListViewBottomMargin)); } - Widget login() { + Widget accountAction() { return _futureBuilder(future: () async { return await gFFI.userModel.getUserName(); }(), hasData: (data) { @@ -671,12 +671,14 @@ class _AcountState extends State<_Acount> { return _Button( username.isEmpty ? 'Login' : 'Logout', () => { - loginDialog().then((success) { - if (success) { - // refresh frame - setState(() {}); - } - }) + username.isEmpty + ? loginDialog().then((success) { + if (success) { + // refresh frame + setState(() {}); + } + }) + : gFFI.userModel.logOut() }); }); } diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index 93120427a..985fe2df0 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -451,7 +451,7 @@ void showLogin(OverlayDialogManager dialogManager) { ), controller: nameController, ), - PasswordWidget(controller: passwordController), + PasswordWidget(controller: passwordController, autoFocus: false), ]), actions: (loading ? [CircularProgressIndicator()] diff --git a/flutter/lib/mobile/widgets/dialog.dart b/flutter/lib/mobile/widgets/dialog.dart index 503b82c50..c17045236 100644 --- a/flutter/lib/mobile/widgets/dialog.dart +++ b/flutter/lib/mobile/widgets/dialog.dart @@ -6,8 +6,7 @@ import '../../models/model.dart'; import '../../models/platform_model.dart'; void clientClose(OverlayDialogManager dialogManager) { - msgBox('', 'Close', 'Are you sure to close the connection?', - dialogManager); + msgBox('', 'Close', 'Are you sure to close the connection?', dialogManager); } void showSuccess() { @@ -131,7 +130,7 @@ void setTemporaryPasswordLengthDialog( if (index < 0) index = 0; length = lengths[index]; dialogManager.show((setState, close) { - final setLength = (newValue) { + setLength(newValue) { final oldValue = length; if (oldValue == newValue) return; setState(() { @@ -143,7 +142,8 @@ void setTemporaryPasswordLengthDialog( close(); showSuccess(); }); - }; + } + return CustomAlertDialog( title: Text(translate("Set temporary password length")), content: Column( @@ -230,12 +230,14 @@ void wrongPasswordDialog(String id, OverlayDialogManager dialogManager) { } class PasswordWidget extends StatefulWidget { - PasswordWidget({Key? key, required this.controller}) : super(key: key); + PasswordWidget({Key? key, required this.controller, this.autoFocus = true}) + : super(key: key); final TextEditingController controller; + final bool autoFocus; @override - _PasswordWidgetState createState() => _PasswordWidgetState(); + State createState() => _PasswordWidgetState(); } class _PasswordWidgetState extends State { @@ -245,7 +247,9 @@ class _PasswordWidgetState extends State { @override void initState() { super.initState(); - Timer(Duration(milliseconds: 50), () => _focusNode.requestFocus()); + if (widget.autoFocus) { + Timer(Duration(milliseconds: 50), () => _focusNode.requestFocus()); + } } @override diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 664d6f05b..47f3c0870 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -324,7 +324,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "适应窗口"), ("General", "常规"), ("Security", "安全"), - ("Acount", "账户"), + ("Account", "账户"), ("Theme", "主题"), ("Dark Theme", "暗黑主题"), ("Enable hardware codec", "使用硬件编解码"), diff --git a/src/lang/cs.rs b/src/lang/cs.rs index ace56788f..9d203f9ce 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -324,7 +324,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Měřítko adaptivní"), ("General", ""), ("Security", ""), - ("Acount", ""), + ("Account", ""), ("Theme", ""), ("Dark Theme", ""), ("Enable hardware codec", ""), diff --git a/src/lang/da.rs b/src/lang/da.rs index 27724f7b3..a07539719 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -324,7 +324,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Skala adaptiv"), ("General", ""), ("Security", ""), - ("Acount", ""), + ("Account", ""), ("Theme", ""), ("Dark Theme", ""), ("Enable hardware codec", ""), diff --git a/src/lang/de.rs b/src/lang/de.rs index 8d90be381..fa589a564 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -324,7 +324,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Adaptiv skalieren"), ("General", ""), ("Security", ""), - ("Acount", ""), + ("Account", ""), ("Theme", ""), ("Dark Theme", ""), ("Enable hardware codec", ""), diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 6c7bb5aa8..cc28525e5 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -324,7 +324,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Skalo adapta"), ("General", ""), ("Security", ""), - ("Acount", ""), + ("Account", ""), ("Theme", ""), ("Dark Theme", ""), ("Enable hardware codec", ""), diff --git a/src/lang/es.rs b/src/lang/es.rs index c8296ced5..9704a3f84 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -337,7 +337,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Adaptable a escala"), ("General", ""), ("Security", ""), - ("Acount", ""), + ("Account", ""), ("Theme", ""), ("Dark Theme", ""), ("Enable hardware codec", ""), diff --git a/src/lang/fr.rs b/src/lang/fr.rs index d9a42e934..8276a54f2 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -324,7 +324,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Échelle adaptative"), ("General", ""), ("Security", ""), - ("Acount", ""), + ("Account", ""), ("Theme", ""), ("Dark Theme", ""), ("Enable hardware codec", ""), diff --git a/src/lang/hu.rs b/src/lang/hu.rs index b35224c03..e322053ac 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -324,7 +324,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Skála adaptív"), ("General", ""), ("Security", ""), - ("Acount", ""), + ("Account", ""), ("Theme", ""), ("Dark Theme", ""), ("Enable hardware codec", ""), diff --git a/src/lang/id.rs b/src/lang/id.rs index 657014141..a285e15de 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -337,7 +337,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Skala adaptif"), ("General", ""), ("Security", ""), - ("Acount", ""), + ("Account", ""), ("Theme", ""), ("Dark Theme", ""), ("Enable hardware codec", ""), diff --git a/src/lang/it.rs b/src/lang/it.rs index 8f6dfb3d9..917d5e9b2 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -323,7 +323,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Translate mode", ""), ("General", ""), ("Security", ""), - ("Acount", ""), + ("Account", ""), ("Theme", ""), ("Dark Theme", ""), ("Enable hardware codec", ""), diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 6d0a2a2f7..446bbc944 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -321,7 +321,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "フィットウィンドウ"), ("General", ""), ("Security", ""), - ("Acount", ""), + ("Account", ""), ("Theme", ""), ("Dark Theme", ""), ("Enable hardware codec", ""), diff --git a/src/lang/ko.rs b/src/lang/ko.rs index ca939e2b8..cb223f77d 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -318,7 +318,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "맞는 창"), ("General", ""), ("Security", ""), - ("Acount", ""), + ("Account", ""), ("Theme", ""), ("Dark Theme", ""), ("Enable hardware codec", ""), diff --git a/src/lang/pl.rs b/src/lang/pl.rs index fe45ddf3e..e6696fed5 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -322,7 +322,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Skala adaptacyjna"), ("General", ""), ("Security", ""), - ("Acount", ""), + ("Account", ""), ("Theme", ""), ("Dark Theme", ""), ("Enable hardware codec", ""), diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 858afd8a1..783d93635 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -318,7 +318,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Escala adaptável"), ("General", ""), ("Security", ""), - ("Acount", ""), + ("Account", ""), ("Theme", ""), ("Dark Theme", ""), ("Enable hardware codec", ""), diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index af4f0b52e..ac1688d13 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -324,7 +324,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", ""), ("General", ""), ("Security", ""), - ("Acount", ""), + ("Account", ""), ("Theme", ""), ("Dark Theme", ""), ("Enable hardware codec", ""), diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 04cfed485..a005dc6ad 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -324,7 +324,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Масштаб адаптивный"), ("General", ""), ("Security", ""), - ("Acount", ""), + ("Account", ""), ("Theme", ""), ("Dark Theme", ""), ("Enable hardware codec", ""), diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 8ae17b1ad..837e491ce 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -324,7 +324,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Prispôsobivá mierka"), ("General", ""), ("Security", ""), - ("Acount", ""), + ("Account", ""), ("Theme", ""), ("Dark Theme", ""), ("Enable hardware codec", ""), diff --git a/src/lang/template.rs b/src/lang/template.rs index 914b103df..5c68cef37 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -324,7 +324,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", ""), ("General", ""), ("Security", ""), - ("Acount", ""), + ("Account", ""), ("Theme", ""), ("Dark Theme", ""), ("Enable hardware codec", ""), diff --git a/src/lang/tr.rs b/src/lang/tr.rs index b1b029b39..a0cd3ed8d 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -337,7 +337,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Ölçek uyarlanabilir"), ("General", ""), ("Security", ""), - ("Acount", ""), + ("Account", ""), ("Theme", ""), ("Dark Theme", ""), ("Enable hardware codec", ""), diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 764f666e7..c3c0849f0 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -324,7 +324,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "適應窗口"), ("General", "常規"), ("Security", "安全"), - ("Acount", "賬戶"), + ("Account", "賬戶"), ("Theme", "主題"), ("Dark Theme", "暗黑主題"), ("Enable hardware codec", "使用硬件編解碼"), diff --git a/src/lang/vn.rs b/src/lang/vn.rs index f177581f9..88aa79dbf 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -324,7 +324,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Quy mô thích ứng"), ("General", ""), ("Security", ""), - ("Acount", ""), + ("Account", ""), ("Theme", ""), ("Dark Theme", ""), ("Enable hardware codec", ""), From 204eab4b81baa681f77aa570bcc0c6ba052f4d40 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 22 Sep 2022 23:22:31 +0800 Subject: [PATCH 0562/2015] add margin to app icon --- .../AppIcon.appiconset/app_icon_1024.png | Bin 15146 -> 15849 bytes .../AppIcon.appiconset/app_icon_128.png | Bin 1975 -> 1570 bytes .../AppIcon.appiconset/app_icon_16.png | Bin 383 -> 354 bytes .../AppIcon.appiconset/app_icon_256.png | Bin 3938 -> 3330 bytes .../AppIcon.appiconset/app_icon_32.png | Bin 624 -> 569 bytes .../AppIcon.appiconset/app_icon_512.png | Bin 8587 -> 6852 bytes .../AppIcon.appiconset/app_icon_64.png | Bin 1023 -> 909 bytes flutter/pubspec.lock | 10 +++++----- res/icon-margin.png | Bin 0 -> 15172 bytes 9 files changed, 5 insertions(+), 5 deletions(-) create mode 100644 res/icon-margin.png 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 6b4737dd5d7abd614f667696c8fbc698ae90556c..9a3310e01c49f8b36849b6a24d12dc4325827e2a 100644 GIT binary patch literal 15849 zcmb`ucT^Nj^etKw$vNji$&w@^!9lVhAW1TVs6@$v2*MCV6bS;7BrA$2hy(#i11MQQ z$vGz_C&_*1`@Q%6ee15fZm(HuYEGZpwa-4)HQiO!&x{PTsmR&M0RU9GIvU0RAc;jJ zAVm$89WXqd#~5 zM4_ssMDesrxEcPg#oE~^MXe!@H&T1>-Z@{Gx4*ke+m4+an)iz-tr>T4bK!r+464&N0ZvvE*@Rh8(2)9vOv= zD@}bQwJh8?#^2~TTUe9}WEtxembq1v(`{37LxJg2@p_cdQBoe`(-JX@!CfX^>(5S2 zu7B?NoFr9t4G1BIydRlnpLR^uJX(QIG54Nt(fProM{^)OmeRsUr}s9eQs|@dgz)#? ziiMLiqIL;HRpwU|Z0fuNUH(}?3H<_!PHjzBBg*<&yrkE=>#wSp+w@kDOnuMFwnu-s zd@*-92X_xyGFX{J9t4^Mt;UioAOrbEMJTO;<)lh$<2`*^tb z&ZTcNFcFRg)5y18n{V)3{I|e4qfIk(_02LT0lM0;2LBYA_yHV1e7@8;$WUq-oI|?z zSiE!&1T6*pVJCJX8iB%T5O2yV?r+W=5&ThTVh71#>GqkR@kRc%tl_JqgDjn<=Yn}l zW#NmV4Ek2p`-WwQO7b&z(&-My~7e)8FHc~}+)c5dI6?CzPm7s`1OU>{Rv zQKrB^(y?vu@_t}G)adHTe@6(h!?X0w>LM@?W6(pcY1}A}6 z0Y(L|AQ&e+{Q4{nLhRFBY(eG7U$sdTa6~p`HdDy zuUUurE+qMuOp#s`^v!@z6o5};*t$vdh>AAxr}j7*j*av!c{QU3zyiDg8t2cCpfA=q zp_EL8bu3wZAqAuW1MmRAi3M62uz|$;&2FGe={Yxe$6UQ&07e|nX^D0J--5Foks1SV zmi(VD3>`J^>dR=fb$sG+O z&FvOmpJ>;2IRpfJ?B8%-_n+RU{<{0@>N{ob$3cTCN{WhsCv!(_!)HzV=dJ#B4>{7L zV7XRyj2t99B~iW9O3Bw(`hUx^{c^Y&slC__hmQVgU60ehx4l=T|aFsAS6m%>N z3@lo35%S_<1Un?Al5U?5R3Kav3Hc2UOH!k0|Jkcj5gQxTWe?sKU`K?&f|x1ec>08g zkNBzK(l2%Y%X=P0UeEPQlxR3#Yr$vpL4rM_49|hm^phmka`rL`hwUDGlFCW#nK~Jh zR`WF&=JP;sDK|B~r8Ii&f@>{B@QbF&Z?^f*uogt@WrtAVG(#HnmZhCJ-aQnYzHklT zPWID!9Y$*eNJYLebf1->PNP!S-~+v83h{&6jmYehzI!vqj9^r_d%UNXO`*U0&T?}H z5KmYwsiJ;^w26Xa6vZqgy1Y~!OTO&pmWNR+rtFGM(pHnAuvUeV_Xj3qD64e>dwbqY zv3k|~si^$DQZzFv1QM@l2B_{+i`%&0G#z0}&Zo(HSTglbo1K3~nGp!*CDIJVIFbH% z6ZInJ5k=mYKlWb*_-8m70o#WuZ-0F0g1`%&M;xRk9wQ?6*k(0h2~<+W)pp6e8ji8v zE!Wwa?=n3aue)xKAv}d+STy&)))%4jI*VDW+!>mO(^Uuk#>lsFLAyprNroOv^O78! z{C$#P+`uryYkIFM(yt?!7)yh_aVVi(kXPqUyGxI zqu~jztq${x150l#C+6NE91MT~88w54K5kPr7Y*k1M*{ z@2wrHPwZXc5vFIV*8A?Ji#jl`^gWVVAu*N*__Qmbc3{~}kPU{{p!AkqI zn=6a#JPw{9Qpg-|H#&PiT^jgC>mC(cdxMu30w3logQI`Elm-&CuOD?eE-;}mDn{et zw$W`2_F&-h_qMEqRdd26a;%lm=v+0WcgU3{|L{eQFMN-d#c0?cpdvYV%P5R zmw*@FFz7}X>5iL#p3-dj3pu=6x(05qm>0)4 zH*|@(U;5m+u#|cYfblV{_PwgaKq@RuN2JncGaU0S6IK|Z^DRd*9ib$nYofEKqQ7O^!%Zq8@d>O2sZ-1WHJ(a z;KHCIq9BF#X=vjB0}p6_;ij*}h+kmam%^l6+d+W7`(1}RY48NeLK2G0)X+5RfH5s7 z{Ca(di3lf}<|&6&h&4sdoXBCn5g`+Q1kt%1=ad1UKQ59)fx@U66}yYl(mDa|NGd>? z%~#8)0n?!#I1Uf-`ON^@l{@$EJ22wE6Mo+~m4HkdP=~G65}g%jVgOOn%!Z&M0Toz120Y!eRnM>& zW?N?QZ(Eh5Pay8ua%blsuLo zaRsEU+d-p?jqA)2Ui{npGll_{0-s$#`;Vh-YeXJ{A_}W4qo8{Hn3qUNwB$u(D#W8d z0mrzrV=5bZ$#P*BChd=g=*V$@Tqh?7U%{n#mS`g8k6-!=TRGZX%juj*3mERYN~i?N z6|{f<;h4ERJrSi(z1E3ynSQABA1a@dYt%9ebk;-lF_ID_csSJrjQ)0B>FVC4d(;zRxM+QmF} zkmwbH^o2rOo_E|p;W~fKUvGGTb^)Z#+lqE!6KRGlDHC@jqu{FVl)IwEHK+w_*3U zsXR@)W%S9v60x9c8`9q(0TcQ(NF-z{uEVlaa)ufm!bwzvFW!V|Mt!?Bp@S_o6+Q8R zN3VsLC1)7Vvq6%S32*K*4no>9mLFIu`~sCx(ELX6wRd7(G~Z|+N(B5>zifoSc-xM^ zXKs<0>5b1Q4?HT>E|5@8V<>_pNS0u(lCA!O-ET9{Lr=BF1Y;P_Fu){0^r zZ0xvn7Do<|=fV<%db(foM0APw4;edlFSXrBMZw?05(;O?eU+ES+NAmq6g)6br^iGM zM`QrKnbtb^7GU+xY1S)_Gjy5;-b8|O1ANgJC#XRZxBqIog0zBWW>~e$=tXCC&@t;3 zsQ~vXo+^}7vESzOBX5pKlG?k3M`Mlg+JG(T^`8AZbkB!X%7eyIxlgo`RQT^+OqKN6 zN|qYkds6Po&T#uDasmO_7i`#p?rfnWrofQ>2Y&U3+<$Dxf z2aoJ1R|@VH&6`YE!zx8KE)!e17LLNn>u=0#l-^Fa%PzR)`Q%tPa)Oxs1WIng*;xPj zyq$c7K14FU0 zaLp>Ve41_lIc!}^nah6O3>9azjBo*bbLjU6XG<$}JYVjPF?HQ+`~_3u9eo{UK=KRd zfJXK7=V$L!ltkZ(_8WYm%3nQPK>W7;{S!no^Q$Iudmb4T)cj|-Pk5)R-Jx_*8njVU3-mqQmmhL3X!Q)ed#IRa#gpQa@H%_ z3o}>f=`yHgKtpMOptB27jS>I(@?gK@np*-tqBD-p60ON#w0+L<%u>d?Nn-oWd%NC& zP-M!mo>{uP5v zZF2M=&$vTwD`n^)SbAalXuM@`af-Y?SgLfxhn5U)^x@J33R^;#EIYRX>8zfz)~S~c z7%2I_1f+w46XaNg_d0a_e1ra*Vxdpum54`4NNjaQycMk4wkJ;;67*Q|K-E3ZFS+Xk z=L6T?#$l&nB@g21zrL{P6#j<~iZD*eB1-_I6dN(sAJhc%MA@s7#Yu8?k8>S{n{Z_K ztrd>;8*53i53GmdN;Yttlq39{F*+bp>nGhb?4#jpx!Cjg2@NNzF=pKGI+hrT?i#6q ztK^q^+fb&DbO2TN65-X2Tanj6eBMaM{z0V!%h2~$vh zF(%}Lr>@~pv@J|5CId#~n*X>W$Mr|m*47w>NV(c4k~Q+7?vH$+dDgtC1g8JsqrcyI zUGLzabj2P1PJd2oBB^Wq8)Vm~t3Em2moVGf_OP(BG_iaINZXJ_?pYlAqV{Ld4W6vL z2NtQpZS+K{?e_Eu$7P%^XCW4Oh6(zV-8}xBQY4mj5ABjQR;^>zc=9jvdO!niANf#E z@0|rb;5E9D%T4(GjqS-~<8m^4$d5neB{rjUWyCW&h27iUl2B{$vpEv6c0)t^3v&;wH@f$@Avkk zny!vb%VIEGTIXldYi}547a{wlR7nv1FKf(Yka{4k;Bn;Zzje``(AD?kYAKk9s;BB|^g!zaZ?h~I z&!Rf{_4EzBJZ6!g)d~}Y;Y_apXSl(ncEjnv@ayp>kpQwNMdEL2;!UuXnp4lR_E_JX z9a9`n=)nnmTw9x90REA?NP%%O%$e-U6#f>RA5a==o^#=?XwMFE#kmI)aoZ3N zAIz13Bpr#rT=ei--Sw`t5{PG&0^fB_yN_2ChI=vf@OzO=Z097zlhUxXNm_J^3r7!H z?-)eCs)}(qg9;X+>MxC)L`~2Tox5$HS;Wqa8>#2g-#prYOgT&lK5cpIMO=qC6L6QDQ=BXo!go^{MmbE#`RBCjtpEJs51=y8j~gu z>fd_0es_gglMxetVNyVKiu8>3(F%P(=UJn|1SK(%8a0~1_tYnDI0=aK^Ba7~MqGyz zqHjDKF)g`s8mjrzFuI?8AB#ug$&T2*bR674Y*D#(Tc;?ZvR>%|))~8jXI*j|g5aNS zmEL#fKe$97i8*>T@<}>ZovJoC_1u0Thb##wC|8YL5RWZ-0Rw!A@U!YIb zyCd!{0w?hxi(hqGGW8xkAbBr!>Gs@>q_`1n{te&n?|w&S!gT#?h`pj;}7BmqfilJ$J1-5I{TlFr{)9w6cDqD~xpZMAx+ z_>H(PbiQF6*y%ggFxt{AUJJdrJ}jQw)GaTq+uQhLL8+rY;NC_)^(EQVau^$!)8ZJZ z-cbpgitzHHxwK!~&WpyEZXMCf2~0+x^K&*fbr!eD0t7aBR;YC75(|KsJI9e)Qw>VW zBsf6&v7x0^UL5=;E$3j>Wv0%dMPSX{%{+!e{0;-XU4r2kO`iI)vx2g@Z8Wph#VTp1 zx-T5VW4_gB9Ec|WDVh%C(WPag7c)D8bF2!lHXB)2hpck{tAPepUa0{TagHTG(f@B3l{d-fg&sv4VVm zVOb}J+^l(a{ecFJ9!bE*W!sGXxmwEB_UVe8bxS)N8qfOCA-5xa_+@{{lQ#J@i=c5N zF(&fik)}|u?M&-?0rJ#!_lKp3&)WML9)ZZ}+QTnY6j?bvDQ=WN@k>Wty4*4djWoM8 zQ(KPUK;s!da;Rya!VaUoMzkLNsOQ1MFap7LYSTX_hACcg45lJ$g`Cxi3h9>?!i(ZB z9H+I&TX^}?wgXxdu50$8tz38-5l@HNPqTlXT?UaOdoXvco`Yl`KEG)kPSUsXzcADP z<$9Fz{33a>Bu(ia{J=vRe5bVTOF8^2-u3dZngl^{t4)p)2rn`ji+I^umPEEP8DIM2 z=Y_^6Y!y&F2q;k%s9}g#>KpbQq$4)#uq#zrTXK-(BbesrX`X#Sn-v(_D(n;2b~j^CvRr{Bok95OkPLtwRnx0%L04^lYd5ExQ+6 zO%tHK{Fk>re@Z%C&!Lg#LGKNDns^vy?gQNR^~=|{gqE-Ol2zCq*K|IEVe*pwzq?a`p-8jpV%<~KsRW~BUAef;|kHhBc5wy!R{B;|8c;t_MkX5WzFi%cB2 z!_w=S``0hW3HZ!;bGb13&^(1D*q-);_h zqP{XAW~y!@vCf%l!yi6(-Sv^M!;Z(M+G5ezd)w#2ek$eUI;N+65oNl=9T(zY36{Yl zVMRTL=fq2i9nx)iju(r;05p+_lSb3u`_eaCamfJ~) zXgNbhm6nf2)x_i4sFRC1R72)jK{{7uUQvIn&d4ty%(%@*B%FT0J#*vY1&5plwb6x_ zu+m#Eh<~%Wu3cIf??K*VvrIq5V{{a#N6Nh_qRKo~OGs{m)7zVNrN5)XCF49JBqFGoVi=LHE*O2- z6jUuTW}1Ywz9AClGnLZ2k3y_PJE@u>qP|D`);>g&UPnrOciJLy9}<}KBdD2Gy^`~d1m)B z|H~YBSl`QKv1CFys4Kt9v{Kjag@+k^i};WpQ!9Wu+4g9(;iPB>g?Bjgl9~R>jCzIG zG?||i&H_Yk;{Ed{1*b_cowrA%))DWc5R~}m?wd`<0Ta108C{(3tiwXS>kIdQNllDq79~FXO#Jty z5S0GCWv(C1kAFD;Q@J>YKfiwb`I2@;dgRK#Qsy+6(dtCYz|*He0)?>@BV)T>Ul|a& zJ8wu#NI1!pyQ}n)8C)&piI#FIlS?%5v1*Tb-h9F`gtLKHvmj;XS%kGf(ULE<$>w`% ze}5?;A}D6@Xy5yVss5-XwaI;&a^m%Z+8uPf--FcAJ~m0qXi%)qAo4mg&CO1FmdM+2 z&iU>ew?V*dAd|r&B-UrSbGhSy@+&hX_9tR%o&6oRO0Z&F=V|aQQ8s)A32vF3{|UPw zlgU?Km~|Vq>K`dUzT^wDze0(XvRp=e4N*$lX$3-ev@a$zx#YX+rym&;C`eSC0ApDQ z%{z}8dWF0tn7hiAhxv{~(=6w^*1>7Xunty!6#Fi*o`oT*sk6y#zWNTzdU<@%-Vlvd z6R4GZU~xvpkY?eDPE$;y?EymVx*REAvp+UEz>|`g_1E`5Hf9qe;v7Tg?i>B*sXrz; z=v!J912ak(1Sw;glo<(Um$Hsb_J@;*rYd34^+zLE?o{QU4u6I%|HmlAD$KPLMW!F6 z@`*)qg$OqB;EOq-@kN+!Xe4}_dzGl6qNFM{9;Tz~QG#=wnJExQhiQce#=me z@lxo8Yi0@g%*79dh=ax$myC-1QC)XUFEgpc?7e$CQfdPJBBkbgCSg~xm10UJX>si|+nL86Yj4ri!-x2{+0W zyhA@oX2=Y}w#-Qe=UFJQ-ksK7#q!Mferm!F+%#gmxAAuVqMo+bVg>;q-|$42jWm;zMwAMze#C5Yy0BQF$ibRz%#2tE8H91$i8E_8f_74=M|hs)H? zd<#4~y$hHD+rcwT?5l9?wwJEMO@osbz(56*GuqHF1@24YcGtV zIsVABC7+rC-yXZ&1pbPg-2iKkid*xGQt@C}f&FX6t0vuy(gFY&V=%9c+|f%>%21aZ}foN{jv0{*;0C$e--)@gn948U_9cB8$yN6gK**D*BrW z*RX-ch5$w{RpcI?8SvUSpsk_QI5$D=junKZ=GTw?a15h)ZaFWur%0*v#sg(k1<-lo zK>pQUTn#t);hng>WnC(( zb)DS&OP#pf^LU3I0@y2nxXh{HEf!LgWsP3Of671oHY5V>hY(o{@0PSlMtt^)6X`y<7okzX0%GEp2y|J+$Fikcp;9l2QEppJ(vF+ zmVavqzP38jZ`M7KGOpAk@)uA@szp7U?pf8tQl^iSayoibd}Rkt?)B)z=RK(CrjK%J zJ{SM8<`Nz6>`Ir`{jtFpkIvsp@Y9;RRt)nJ97gL2xpuzr+GW>yw{ze|+^g}h#i>;E z`CN#V@#1@&OH2@gn9e(f%(&H+_t&bDU9>SSl#A;_M6Ju@QQwoM^aPAYAECayJ)KHH z6L>dEP#N+LRr+_zeyCcUa)gEQQsMPRxt*I0;h}bOce1bGTqenab^3wA1MaYU0B=)!gGYT10;Cub=!UkA|XLV(HFfWmDlHNrld-%D8FE{?3w=42DsFt;l zaC*sdm?#`w{}I#R7eaeS9Wj$8W{Yyl_Dio!Twc!;G^E3{Z+uUaFO5hJZ8Jm6q=*?{O%FwPG)>?q1!Vb7VQu(WqFl?xS1{z8 zvBw;*bMP*K7j{SW|7%6I$xrO>c#`qR+Ayg`ScUft2RryD<*AzX?HlEm{6F5b45?Hg z3&;we;zUSo@8_583#6UOf2gXI1wD6DbII>|C`ga~rZlDU?#Ii^09{s%T-WO-$CR&s z{%ew@t5qZ>xhLiQjMeqWvkRr8y(pvNMb%^%K<~8?(HS~O78W`(5lEt>yDQm8MTHM~ zU9)SWX3YYJszcGE#RL3UD(nTHc#PXb*ICEc#EL$sV^<1GV@R8tQ5?e|6L?Y)M zdc64I@kZVI|$YSzaGl&_iK`SAuhj|VvrtMJ(q z`@dHX3;oGJeBU#Oqfc-3d{`fJ!Umw!PKvEI3D^u%?FyttaD)f{xB_Z`=Ey5!D%909RM}ie50}?mkZ|Msns!EPU zj3dg>FYkHj#USxdooAsug5dg>@K81`_zw>0SopqNt{53GyvcZ%EO$Sig9B>ZF4wv1 zm0hh`-?Ra?<<^ggvom!`VuzZ&SZ4HQ%oxaI< z1?D=s5AiK+%rM5+7tNqsohCmbkoe^y$t)MQC%2Q^x>h5ieg5I%E&g6fj zIhES;p0mc|_;u!7V|pN9p_qKcC~<1>cH|aZl%(Vk5|UalaU#x12WI! zZsvb4eMe_2J?2V0!Ta2tB-oVWJCkEr?UkO6>D00ts*Rz<311<^EfSmbtll^JZODYA z4}cj;GXt-XcwPe@N9;<@wSD96ITo1t$oe&{Kb-;QuRqZPG@v4Vyto%;D~uOX)%bfl zp25`eoQ@2rdnCjaZ5FEBu{i508mN0(ll{mq<|?TlaTcD@N5JT*S-^lmiMsOn{SE`O zAju@=X(PAohY4=Px$r*J?Y7P&+(Afp9y8YOcb(S6(RB1)VDURX(EZ^;WA$F|Q)5PD zcr^VW-N5Xh;Sz?C-Yps;5!8lkh5P$Q`S{UiE?lR}y(_eR#N4Yzj?mRzoaAmrB*elr zdVY*D?ahVqk33+MCN7Zlak>8OZ2i>doF!CY2rJ7^_n@lYfj+erk{rH7;&mB=2$N(Z zLh>yXr70 z#ICC6AFp)8X(AHuJu9smGgj%-Al*nh62%PH@Wz9EV>e${~S5PMY}ufrlI=}6LE%~!&4K4jtM`m1Z>i(fOh3F_Hs=}@lH`Dw z^1^0Ft~gkrAEs<18*BW@6;)F8++8^cGKn5$EM?&X!D?*Tckz{d_oeTBXyF~_`dU-+ z%siYbTM4g64^&-zT)hJqD}30@NqnV{*{}BK);Z^uQi#=NGrdC=U2Hk{s7=T*?;};_ z|6UX$1W!Z6N_nQ5<;<&<&2Lp>>1~JH@(T7cJz-c~aQMR~Fq1OG-hq{WUpJZ4dtyIS zc;`}UAPfVWd2Ot=d$4}qzw0Sp^FDXz4RRiiCk``))$I@G-PYx_|1dar2TI=&%W*h6 zPX${yXIPdNze0H%iDMj#V;QyX4EKC~+YxEC)bXrz2$dYP)>D5o-uOoJN!{h~s ztS*%1h3!JX(k4w4*(UtIEB9Q)TjjLu^54xG4jtWlh^tdxUEZWK)MOy^$+CV_WW#5p z{Im1DXGigi3#!($kI{#piH=6WG4*VRAGD)BMARQ$ z_HjULDN*5(j6ZXq&hpL!S#Z0L_8|Ndlcz`&d8vF}^1V1ppi0~e09T?{5Z-|s zX`R)WunWc&hf?AB^!iTJtm+KsktTifG}I;knP$Om(N;W@9q^D;zxBv#5fCKUf#n?a z!i!VgSmMr;1#w)+^33rtXs6$#qab9WsL|0$y+_3{f9(+%3N>cT@B%8=aXVP$j;#&E z4qS}!sCLSSutIBvf~?p7`5646-eB?h)sE%MJn(n-Dba;QB~0w%eaJ?J<^w@CW8o8h5T-^D)@ zBeqb_Wst6I9|XZx-~6Cq;>}%6VU(}4zqOYPD9-cFL}NeU#wsz#&tAh;%riqy<+TtP zw*4_o)f0PjMOa}t5;MJ_*hthp@#YcU+rz{5!4?^2cV^`fBKDV2p>(xiS@%~DB!FUS zTWB^hvu_IHyc9}L&OvTBEVH)$4AMYgxPD|Ank#DotN`qDMjp$55!fkkTCSFKd7v

    {L9VNI(BtAdZ*hFwy1fde$^fC}+3%eX%qS^anxDTK zJ$P}8XnK_DV~_NLV~)*d`XAA=5_dlt8A&lHG3>o$!7E)FFza7$fjpf7-pKXhej@EX zaA5xS^ZJMFI5g0)uE&N<$|2-g-TpD;x;fDhAXciG2dXF>Te zp#HM`4113_I~=EQ@7QT!p4g>w$vlSwFWrb?!As0d-OC>|BCVo_2ENk;$W zz!2Fz*TZFbDu8*8{w+?qbOVmAWVod3VRx{z3cW-Fy7cmfPXS@wz?AR4(~`EH#Ps=D zE)19z#ynzCm_yBcy8EMDg)z2Q1|$f5Dn_jr-01uS7=L8C>BP&SXyl>LUx1LmcCAPI zd#&Z|v;KnMt(le*C*}XiIzDFZwOMOt=^QHeTN(XZ?TGf>#m_LT)?ie^Sw)w~CGbS= z65(hhC#I2je+**h9IU^qW0iPY66m)a@WI2qzov&}z)bj`;>dNpss!*mbqnidxqDSR z)V?3&zVRBndXAgP!$$FrjLvsfx=nGNLYJuUi2NQt&-lN*b10lgyzp6U{Yl?__UL&9 zk1uNaynRqq*819{427Tz`lZo!Qi^wgaBWuHn5#7IB-use~+T;!h zQD*+&%thr7&-v67u%eOnre978ucdhzwAov(7^iAd#6U{4pxCR7C5Jiz|Ah#YBtT*L zXAcDa}87Z(e{YBOzN4_+$XzGtrSyh%mfLVCH zKz!HMp-)fiAl=iGh&HQywQeU=9wWy`b13;EnCa?kGyO20P+3y!TQ7&9s$J8JqYw5h zZ{wW4g~Xko4+=?KK7GXt=vrzjE+rYfI&#AK?%;Xk)2}u#n#)^$I#p2v`8p|ao{yE7 zqCJs;w{P6;COI?v=uG!80%#;9^lTmY_Vhjt8(i!iX(;;m<^ftdbdwfeNZ;HY=i#=# z*P!mz)ba6V(mJo5tu2e5#o;Cj7A-DbZN`41C&NB1VBI`iS{1rmd_m3FVT=G z&tA(&pWC1DmEQ0;92VF|zJKifao8)MUfUt0)?1(A&9prYRXqlryzEH&>zTt|d!aA8 z>Q7=Y@34}+v};^#|4+s2y?;D#5a7itWXahh-dl9}!1K!zE{P>GxLL10aAV3c)IqUE z(VHm$IR*o^XlzJbmV4u!CJib%4a={-9k)*%-=$<(;4wJ*xwFB;_s0!04{Li-;bb>UYe@1GJA1=@FXBA z&P!GSPw~x`EDit+c*p?=sIw{L;i4FfVBkrmQXirYN?TqJM`dTv7yx{ho$}?qNIf;+ z|7%Ms!9Kh00vMTnRU(bwT$>#Cn3H5g8GYoF6!Fu-IG;&EP*%N`tFZ1v)u7_gQl>U1 z&lqIO^TeAt4ln|&PR7GD%{Boa+`;T8-^_yAB~np#@*H5L6o6~0F%zHxyMLCz0t{z1 z$%$76pM*0JY3_zxY2G4As6uwy6f-fz9+q_C{T|oM(|Qfs?uTPlJNk&`8

    YV5r>{ zuNJ*Q0X$FkB(j+b5>X{{W+mOY8A|G0rsOx%nRm`L^S)Rgb1Ie01JAIz;tug|~3=*AyK<@>$t)EZ=8>k0fV!0$(3DB*(0^8!Dy?5sfb}EyB4wq2h7BTWf z5;FgI!X91(%%-6fxSr}&>@<=aOvM_9_0i=0ZM+}PHODny0@eZUtm^AuTJZw(cMZuj z*bohAQYJU{4DTgYNYY0Qh9IiiHtLt$#gP04oa}*nh5t+P3%?Wk5p^qSilku%FWImi zxb~l597Z@ErovWtQ-ev6FaeWv@+85u3#@QSZlan4EH}ZFTLpr4uH#>$3HTBWjlmS8uAA|mXKR(xed^ya+-z`)!rsTzlcspq0^wQEWoqIquGcN z8<7P3$>KE@mRfdsoGvZa{=QG1I&9@D={!E(*0u;ikLPj5LbZwL-9~+i<-%_005wzn z2@1WnKSgzi<=fOTq;V3?rEqwXlk>5Tt$B$m1md|wq6Bldl1>&>HP2w~Ov(B@)L0AS zOx|d;!?YEg$j7?XDbd|={1&u#l5>4j)vlDchUxsh=@q&=>J8a~0%qE~@X=`NPlo#I z!w-Lv)RcUVgxFLX?Mki6pou@M`*4dlq9;(69XJ`9{D?V$63W(@ z5Ed`BHzy#D2|8T7-+3zS?bo{$%&&Yoh;IVXH;eI~z(n8btulW)=YLgs22!?q&MFrg z;luf?k+2XW?&sGw?2|-Vos#a8OMcGu-F@*Zvr(3u_eE-Q;w40PuCB0cD`xMr)tRIPd@f literal 15146 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 v+}mnSJ(2Y4Epz4PxKDgp&l+LfBy`4 z|MvR-6MX+5gZ~eD|G3!yH<14@jQ`E#|5Bg-?e+iP?f=y0|0sz6>GS`q(f`8U|GwM* zv(^8Axc^+F|4p3#Jd^(gbpOcV|D?_Tm&N~MsQ=^e|Bk@_dA0vRmH+Sd|JmyQq09eB zf0_SsvHxwZ|D4GGXsiEJp#O-y|5l;@PC?{@0000KbW%=J0QNuo$#oa;@8!{%OEn`7 z2v&$!FX9(?000E&NklMvuH47QzW+y3CmwGa zAT}xJ&pv;$L7pUU(lCo z#TTw~Aer2Cfm`8S9#SFyj;r_&g}0CrZwntd%Kw;y)X052ZcxZUisTAM`~NMZ%G)Dx zaFK_U$zL4q07#v}!3Ma7)VV&?1ot^eo!tEafg3mhHwT_Sc{l<2{ZFR5D>wmHe|LKY zZZF{kT;A>#c=H-g!0R{S0>-pGT-mUrHyW*b#_0r%r%hlHLh3t%PK^+v1)C@16auQZ z4T|JDEBJ=}K)zE9#%@1l0oC2q$WQ7jd?oztlge<~Pg5Wq*T`>_m6r(6$ZyOxUTOkL zG$)9L313-S1o3%NN=Lw16ba_xf2E`@{jpy5(h!)w#2Qotn(f9T2)u*0VYO!7V88@SRkTXw&}rMy0LI=iwYR@6QFLu z0lWd-T%n-AqfV7?!$Cq?xGg4NQ#EQhaD-l!iULDOz%Swkb>S$Hh3CU(5rO^?|JWlO zM_EvFfhr&n;v}GD0tX)ce^S*E5QuP=u>>x*xY_*V0_qSqfC5~2`1w3eE?~^5aN?t1 z$1|6bSYT>V1B#PK$3c)-U`6FWJj8P(t(RC}gHo&x@M7ZWtCLi~Y@#)n2i)MPV3I^2 z9)nc_xM}0QODYi7s8t9z5j7Vk6mT_aG;!nWX#VIY6zFNxDukP5e-W!E6qs6x1uQ87 zJu7J!bmJ``9s`3CaPzFAwo4iV9!kjP25_^+Xf z6Hmnr>UVgtacUeT6;OvH**J933dx!#6@UreehYz<75-krd?3F6wFW+vGPRqSNFY88 zNYex^dNbjikX;q4e+xXQBb*gKB@~DU0V$hY4N#G{F7PV2G7%i7)RMR|Qm7xBdvKJA zPWOO-0Cb83(X!zP{*2nrwWTi4$>vBHEa@%fmE;WyeN;MFf4cm@5frc+grXk8KFmpl zP`+#7O)B3a6hTJ;CT>e76b)NgKv@tPtJN(rPSenN5?as;e+fVskhnr);-Y9Y{6&q# z8LJWmz}E^B9lFx)Pq{(VP)=OqdwEB<+1Om zz{TaQ_*JgH9eTfNYkIDu05?~6fA3|svtQ>s|Aof0MPKPW`=!?NU+X>l#pd&0?LPnI z_OoB_KmQ93f59srsl4QJfAMd<=97_;k&*Eq@dfZRM$1E3`Gf!f002ovPDHLkV1gyQ B;DP`E delta 1901 zcmV-z2a@=r47U%EBna?OOjJbx005CeCy`Jae*ka)1atrU{r?Mh|Jv*S2zCG8?f)l- z|2L5T4|@M^um2f;{}g@y>-7K6<^RIn|9Z9mB82}Qg8%*g|MB?$@Am&wpa19b|EbUa zWU2o#jsGo*|MmL+N*%|7ffKTBQF&mjA}z|GnD(jllm* zf1LlM&HsqK|2vZZy4nA&(f^#t|B}N0guDNKxc@(t|CYu7VW|H|ng5{5|DS`#;s5{u z6?9TgQvi$_^_ThT;@;7~XHYXL6bJN{udb$kpRfP`24P7=K~#9!)thNw8Zi)u(c*2j z+D`tv-1jB7g2*8#sNnhizfJ2=R3@8le*$IO=biqPeMlygV>7-_tnRLCZ+xM@#+Qxl zmEBdJ6JJ-h_`?@3v9{QIAR?>jA> z?;nD3e;Lo`2K&?g_me01f5*Yj8hOIn&fECgB#+p99gTkS4F601TPM$0f6j?3f8-%6 zzf!;!dC1nPZ}$s%$d}z^_Wz!MHhtn!inoVf94fHFDRVo zzm5W{DIB@VVLk!$!eR!N2dE;P>E|$y0HQ(R%#de7PBQ^>0YuWmnZ^a13uIizU@CxQ zQz)nee$R$&p`b9dOa-j5aHc#?VA@bOW#P>61Iz?aZ3qWa%~t(GvWHb~^^3xZa1AB` zsD+(AaLAS&Kp;AF4A|2cfAj?qxE4}QCOLpOKrZW`P>7odRBdVKU)J9r&lSc+d7IUt zD}el&^y^oZ6Ab7=K5%a=?G~6wvwsw%203Y7EcT!$fcl9ntL5Ng?KHLp=Q^a>0s~nv zyISNRFenJd(H(RI5D5!LhRv-`1~;-OAT5EcU<7L*cOY>k3Ev3Ze+wX)=51I4F?4cR zTt}e9<7i-!dl2Uz&=0s5ur9?c={$4$$Gi{R3v>%?1jImE zgid)Xuv~Wm8%70<25=0cU>@!SkYv-qSwdTY52WLsTY=G$WcjHJ20RGd3UnOYHN&vM zlA+rexMyQ9X&{gffAih6I|07edm;luS1bw>a1>Ch$3gmF%u-R^oj|lWn}sfn1mY}| z?%WAn*t7ExTL+nC0=wi+pvv|j5hH;S%f?lA0xhfLxd4N2OIldKY%K0cA)wxwUImDu zzcJ`QA)sDP7Pb2TwKD6-kxD?lGZO>S1h+!9xlqS|#al0|u_obXTzoLA1<(?V327jW zqe2y~1!^gNf51_L)=U_7J|a;IIK2rJm5_S_1S-mf{ ztT+-B5`raCD=zW(W&&`f9wuOS0L6GQqm7e2L(`MP!>{} zF>L|DT}nu?+pUPj9Dq=C5EgREBalMCiEKeQaB|)G!U4NQWFUapwQ%O- zCQ4JR$4XiKhErF7zqsX;tJCM-9t#EO9LPw3&?^fCsXM&=8QX)g6mBL!=qf*Y0)8aP zHA@9=0-OKF&0=eEp=P`*7CkmZx*YIlfhROAQyug29!nXGX4xX2IcwXeVd(J)lX%=hs!Vf{kICAf3bJCywdN( ny??RzeOms1K!1$`UjF|R?7e%eE*Oe`00000NkvXXu0mjfS$C?{ 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 c78490fc442c3640a519ee94a3faa81d087cacf5..a529bd927a24075adffda5dc990b1ba4ce54914e 100644 GIT binary patch delta 327 zcmey*^oVJKN_|CuPlzi65ETALLHQsq5PbXff9<9JycPeSef;0D`+v>$|3w@A2hIO) zJ?X!3-+zVX|LkS|&p!UY`O5z}C;mq){;%EnU#i_o?)C=YS?Pe4=6I()5S4F z;&N{POTHrt94+CwjamL%pWplbd;h~lf8O}{@W@Y)Kt?(uN<}!iF)& z5S2ewL8$VRbP0l+XkKqj9Vp delta 356 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 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 883dbd709db9f3298e1af6c1bc676525cb19f260..2ce17295714ca1c69895b842b8245aa2d84c8fed 100644 GIT binary patch delta 544 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 literal 8587 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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 056497e14981860529b98b67d73d953dcdabd9fd..c9bf7b6bb844a1cce7ca10f364d964aa68aec5e7 100644 GIT binary patch delta 863 zcmV-l1EBo>2aN}iBnY=qOjJbx005Ce6_HQ|kx&MI0CE5S|Nj7Q{}g@y@b~`?dH?$S z{{(aY33mVW`v0@l|Bb-^*y{g0lm97*|K;)j(&qn6oBuYC|KRTb&gB0rivJ{q|LODp ztkM6X%>RG5|7EKG8i4=A-~Xx4|Ao8%LYDtAjQ^Fy|A@W+bF%+%u>WnY|5Bd+9)kb; z{{QTM^#8uw|G3!yo5=rPrvF)@|97(&Ky zuRYllYpZt!5GXSM053oT2v>j>Wx4G5Du^)O2Qm%OVgf*S?2)N0MNqAyGO`^5>Ht7= z7K(JyNCh`aekhZb2;2aW<98|5K!$ZfN)WaToB#+d3Dr*lsE;vl0l%tpE6VeUXS_yze638C&qf~<%GD(0&s^o zb>RY#s0N?-Gzy@?0vl&;RJaOkzOgU11rVYE6I@RSb9Ale0u%6x?j!CdgcZIIZ%lxJ zdyUJI#LioRuVulR0|S76lPsN59S;30Cju4#U9hB85aS~q(|i`dCXS*A zop53SHrbpR74U_FOaR-0YQUD^13jw}$1{f$7qlqIgLKSXu>gA#>oOov@Eu_R#!(ik zGX)S!sa{kDAdj_V8BltU2HB9!PNwf{mUbIRlINz+2Dl!kqXXBCVrG&Z@D-GQ{_jQt z#!f-gPEb+O!afqBWtbrV3}>R@Q%i-aqIx!qqP#bfS~RmrZ}9@C>M@4fDlpN;sw|CL8%xnv!xq9{i_yA~x zKOJgmbCpMz%@4Ng3zh{CO1Yy(hx<5AGS9JhE7~DRs5<6BB62OO*Z82>^r+kTu-){y pPy66_s`0_m=#!_{x%=1O(hoT9j-;$0e%$~7002ovPDHLkV1g4MvxEQu delta 966 zcmV;%13CPS2mc3;K2#|H0k=x7Yu&)c=^q|BJu>g1Y}dl>aG+|JUjNrq2J5!T)Nk|6Zm4Lze&Y zfB66D^Z$jr|7NQHE{p#kg8y={|0{|AP@ey^RavtD000hjQchC<`|8ke9tXXVVPFov zkm~p0Hvj+v+DSw~R9M5cm*;w_KoEvOO)NQQXDOlxBG`M2HHjv<|C>652!nA4jXdZ3 zwQPB}4LdVZpod&OBeP#f&iu(eNTD8!e_47o&BzuXr9e*w9R52*;VGEE!0uMzYYj#D zet=x`&o9eTDElDgi6h04;EYxn8Gd;0%1r=!2$9;1X=VJJe|+2k za6LE!C}lvEcUJ*h9b#(ufMLACv#tQ#v+?x>V|)x}ghe92_8=yGruubOxf-uDI|%@7 zgdMA9%i!O);;vo*wNh!oD|Q=fG_2=FzmDWL5@6s&%^!xx-2q<3kpS>Mgz77o=7HDQ zA^?US%`Y;!g%>TMA_mN&4jm@ae_Jg)1y=+>o2_k`Q0h3^p%7q&6ZBLj)@RtGtcd`x zXuo2iRqyRf04~t$FiLAUX)6F!o==Tql>}@?ftXsS() zfQw6L(SS}je473TU>9y-O#=>k!WIk%8!7E-^daK%Oxg==60Q;7f5#>PfBhsw;w!jv zifC(&Apo%V5MXKqpi#vhmjFD)YMb!aLqKB{0MuUepbFh6kAByhB?RCKVWNpPun(Ju ziWs1o`1W5Actelg^#=8<;}W1}{B-%~7)A;OP@52`J2UWIIBraAaV*+19R`5Xd+kcF zcx(C$F}-pcz+4C7>U|N~e|3m!q9nTa_Zq}h{bWoXLR_N`O9b%N*?f(Y{o?rLwFrf` zbwF)=K>S>q#~Xqy%cO!VeXiUT<{j5BqA;&E-rX8#a(mkSK13_}MVzrK_7~z{n=d!b zXJRaC_&+{=%{qKpE}`4o${nlx};K?Fto3fKJoPR^fz7V o>#w17U-DfF=D#=n&!5u&f9hKM9u7#i&;S4c07*qoM6N<$f(|b48UO$Q diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 79c25ee5b..f1921779e 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -140,7 +140,7 @@ packages: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.2.1" charcode: dependency: transitive description: @@ -161,7 +161,7 @@ packages: name: clock url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.1.1" code_builder: dependency: transitive description: @@ -588,7 +588,7 @@ packages: name: material_color_utilities url: "https://pub.dartlang.org" source: hosted - version: "0.1.4" + version: "0.1.5" menu_base: dependency: transitive description: @@ -602,7 +602,7 @@ packages: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.7.0" + version: "1.8.0" mime: dependency: transitive description: @@ -679,7 +679,7 @@ packages: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.1" + version: "1.8.2" path_provider: dependency: "direct main" description: diff --git a/res/icon-margin.png b/res/icon-margin.png new file mode 100644 index 0000000000000000000000000000000000000000..dfc650e9de412b3dd87a0941713d3787b971ce4c GIT binary patch literal 15172 zcmbWeby!sIw>P{e=w|4Up+QhuN?~XOL_j2@5$O_1K^Qs&1Q8^pLqH^?yAh?kQ$V^z z8s^=6&+mDk-}9X7eXn!Q{%6hltX}upa}5{k4%bjuBEqM`2LOQRp|ZRd0APV1u>f2c zNcbd3IzbYnp{A_>Qs5;9eDK^cPct!ts~D>4015Ml@-o_<)7!27H`$^yt_r9Npf?}?XOHReBI+N@ZcHVa_S66I6Pzl6 zgwkLKAdh}8xH`CgL;ja@IxTCBBW_Td_dgGlT*iK>Gy>pJ5yhAe-wN+KwB}mqj;XBiG+1q^pe_I$0l_bh&QHODrjsQaV_QEZ}&@Dpe z7cF;Tvs$L_AiLb99l$%nNmKN1ikf+VkK4Ht5>4Y=28pJ1{sNhQvZw<4WY+QqTTIoz zhtN3v>WHmL>+v!iFl|#A4>1){r~P4~z#TXx zxgDf*9Z0^EhRIMo9q`j&n>}RG#`C+xdC?Y+$xyG^XQIGwzQl=_U6$seyu_&tz+~u< zA}jE(1E00}Y4Dtq*kWO1zAj4VQ^Ik--U#=ZM$gSY6T`uSqQUM?vhK+O7&r)|Au`o|Hi3k)sIh$}3nGZ@ z7%5WwhWs0cks1FNctigGxPO`dpDOJ#1(9@wF#`TQDT~>Zb zunfTxkr1A$k*;l%L>)J>@z?#4?y|n*tq&J(YSlkJWjQk8FGFm;wS6b$ zUzF zO?VOFr(LnEGQ9ZpgtdFsxg>ECkGn$Uv%zfUt#JB}geS=liVZT;zyyaaE1H`n+Wl+x z9l}U{9J6Lh-GDiav-eJ8w6;^0e^s6Nu;-+Pr^e9j)U|vUF`wEboAbGCe4BX;Ou#jt zn6^&*?bFAcdNGWrjbWBxYw&%zIXzWdZ`T;LydQDn%=rNJ$h!Ut&%LNK*#i1jeU+Ly z3N#HU{D~+!6DBus(Ywc8%ak7js)>;VLUqQzP6~v=ru<<6(_q%Yg8iYTUrBl1(>dPn zKdbz78;l^cot#dWZA-roSlxTlugqg^&bhMi`y(F1RK>87R(md1HEay5;?Us($&&ir zxxS_8=Rb2x@j*-D2ZVw(e-$Jo+ZuP05X1Oj0Iv$kfqnX)#*jGLhHh*hBEq*23*hSE zTFHK9e00}-ZqwmM7G?o7VFurr+uLSMS5m{aY*v!sb~At$C7mGZ`RYS-^uBFdyfYUe z61Lu6gI20^)ow5EOL`?B!w;Ih`e7R2VKWW)kp)DrRoMXS7>KL=)A4-=w$w;6c>$`! zNHAXNYg@~GCuFLR9P+b>DD27{bROz1zDs>=K8oEs8j8d0M}xv1OqwI|SIdo~hfZ&w z@v#2`#i~Yo+tea>9S7D8 zJj+A+p~g1OG9id{*bT1}JYe!&riIKk(a4KoneGOvw3U+a*OpMu?6e=qq^Z6Mp0&b$ zdgdSea9!kRGC?gv6)e|!zz4!kJS>CvC~ZQv|EAhI*@U*j!>zjOf)9fkZdxQ+$V!o< zf+K4EWIr#FX?u=HXIg_4wIqa~#Ol!E$)F@taQHfUwO9OgJR&B@!dT3Tqj7=d?#fkP zG${%?8DwK(J>BIXvwwgAREX(VW>0YyVy~S^l1pz zSU;5?M1tT>-mT)RwUmgd%DVCAkXQR_dtN&7S}%HMzLkOZXR=OGaQJ8lSA z21AUGxV?Vj-WQelz=q*d*h6`+R7QekrF7}!@F@?X6(K)p9N05GIgcEf$j)s=IWu;C zEC?Gq{>5{mU&fIQQE)SM=g&8;H(Q5#-+lEBn-Yz(>`g-PfJyNry9&oNC1RbYU*w%| z@iU|0rSCgyLtNM_nWN8&Htyr`f`+qqhj+hYXi;OY@;LOtP&GzWC3w~56e8%?KaNac zpquJCm#h7!O|JwFMX6I8V;EV`aOafFfdVGDv)ntSZ<`=rYDiU%0~3f~KsD0f+uj5k z{j3~{(c$gnas{4V_+U@Sf`uhL`2Jj4W#_HtK5$5DNCK7-%winjIfBSuhm1aV53~bk zp>*)0X=pk^Gc!qLKV=BN~G2z13lD! zZMXJ0zya)mA;SK)#t0H(6=uQ$iOym*tOoZ)&i`i>7L7ogK?#w@m3P`LLx;e z3VQFb{`z^NlSMSc6b>O2rHcC+LjfLUlU?Vrk~uI_20wS~Z^#h@q2feq1K!TWpB2zu9+^} z6u$U&T&eMmYZ!ZJQ>`l*%->rcPy|#=q+HKljW9`lTpMv4mEVg1A^}&i6SHy6s$y|B z@{?uv8?EzaSI@k(s_6gNGF<7+umA^;b=wn6U;HaG=>#Q zN{^0C$MiOR32|C8W?NK&BSnBWn4fze`u40fo+>Rpg(HQ5R7^KteBYL1Z$b$^9}AEK zSzo^#G9LK3ws%`gO}?2AN=l2~O@FEK;?Gn#lflai1%Mnjh+*y5nrhII-HULf;f9jZ zpos_49B8S3uGNq__lTWCC?VYhD9oAF=byhDi1Rjcw3S^0xQ#xhULNRI*3`!wrn#GV zhO6`DHeWDBJ+2j(Bs(XG!x>_i$ed+Kb5t2~ail3PSR_wX&J$v#$;WOJ+&B5-UB)iq zPe{Y(@wu=alXx2POV&DEodIiQ&M{Mhbm0fjZcxH{hm2RHq}GG#kZ+{=S%{DY=;b3$ zxiJVGJkmS}7o`qJ(RV2St!c(5SAFTk0ti5G^$(mZdN847pDKdl*E+%3C;Nw<|eV?{J zDUm!Cpz0dZf?)Fg!ja5?H0}4tB0v>Ub(=konyX96yEDLke7BGcYdt;BZpQ$^ zF5xBG$REukk|UgU>)?rVR@|%3vX_ygr2WgLW6Jl#)%!K+Na@lo>xI&kme?fD#s#7u z>P9Vn2z6Lwj8JuTi7JQK-`n`2iM6prd%?^$^rQU648J9b?R551Z>af0R~LKSO*e_O zLN9i!;N)kdsp~5=>x_!iCfer@s58sw>$b%GI7vBbF@l&qycA&Au3WqY(Cu^Vb2 z=iwnWb-rp+_%5~gNkEDsDI8K^H(ok2^4*M|7RBCQLWI_zF$Sd;L)sbT^&=}%3{PHi=w`39 zTl1^c1m;b%{V=HdES9a{ouoGVXo+Q3MvEEqb?u$5Sp9TmD<)z>#Iz=m?cmR|rY}^d z6p!WJQJJOKwK(G_46~9Zzng=mTb~~$uRJ>6C*4Knmw0PzeRWdV_nk6lm3~oXHB7;< zo1s*N$5sSZ&Z{#y=bAwBB&rw{@h5yv&e?9{#UC>E)?h|dr~)j01m0SeXYn<0;=`y& zO=)9w-a1u0my+#{&cQyB;65L4((3)w;mCWbY*-T*vLQxRPj%f^y2zyAO{Bj>OqUpH+W7N> z@4NMpiQgO9NlJ!Ps93Dh6ze&j1^z27_JNKgi*~Tj`1tdCUt|RK_U0UMX1V{0%Lhwx zWi-Ah%uo0A^Uh0qkEF*Y^dKF4{3)5w*=0<%L2-~`7l}TKZp^LwBF2$|NDo{!-}L^7 zrC|ms;!x}(y0rJ=bmtR^OI{*A65~O-&orkOR`@^alTfaKHM%Vexpy})RGIGRj}`?6 zNtULtOMS(PBzkxe$i9VKBHli|M}g>TA2m2|gi>C}v@v!c{6!b}eDhO!prNjuFZZ|x zRz35AWh7>&P@=Oa4Hqw(m%wAO>zH_0Pgvdkn?cuZs$?UVS`Dny>YIgw(YN=fesw|q ziF_J}T~t(2^O4Kt)sxVYKqWqW#BR$rhky;fF>|A8m%D%LuxJ>bhpA#^u%5%qgO7KO z*h;bQED?9)v+%RNYxU!tE++q|YmMiz7%vc|`|Q_pOu#msuET9GQpuyN*o))q>yi7K zXZ`bx$XrUe3{9Fbo4J`eTmj3R-_|BpuNJf867T`ym4ivTDmJZkno8PuI8MYkhPM?_ z?skz$;&(~8%gjGf&0B~Vx67unnt$yDywc9lFUESPnVR2Z*7b1ju#bdc5?h4kqiw8= zHs|eLuCdy>lgL~2-z7$H?i3|tR`X|qU0|`AXuMxCOJ6UYH%ig8<%*@ELMmhXT>s`o58pXw!$q|x#3D~ zva*H%lkw<|h5CkrNlqW;Nw&)Hg^g3hCLFG;u?YhFoTdpHiTUdH(FCT_nB(9x%1><6U?T4@Sf%NH z7ulcCCLd(AIX`*565aZ{Kr+QrXXYa8rRdgH)(qSo-ci3_CC<$9SB+NQEek}7FOI88 zQh>Z=9eQ@svGA@3;*MS=yY1@9&AK3u*YyCW^mj8u*Jn$oBxV|sLPVsuv*r8UFbqiy zi6>7}zB=#YwRdu~*PLoKew)v{sZ6vvss4D&L|8FXAFo~~EY`9XB7Sz)%ke5%1--Sq zFna0H_I5L9nK9)$frGujy~~#^XyJf%3~#lTHict4{#R=gV+>}6Y%IK^PSSn~mO--n z&FdE*PWYz0Ib%@rqQdOJqdy_?Ng2F+5+$Z%5ecM=(em04( zlH2lRQSw)TYk}^uaY@=WMSIOuG2@i43XcJXunTSeMndv2yyq+R_q;_kMM+PR#7+Zx z^VMm(ME#jILxjGi;d&vSX>&%teDycqQJHUi0KIJ!ldn$E^nkxy3L?n0)#GQkpg=Vi zzHBArHqDxWvo;5aAoR64!|Z0#Wg20xJtz9-(Chem+MKvE3^bR#K)@eCA6F6|ke?v$ z&e}ssL$^2v2Ug~gnyEDQAI*+&gMQJ}`a+h%aY4W^#cU%^gTY|I9(Bm0tj!t3GeX`? zA%&~pGGyCk7p$JVMfvLRyho&0IQs(P zd-AK%?Fw!z`@H^_W_ymV$q@m_=so-JC~|n2j&-X%`)#JSi{?}*g+0IB?LAt@;aBQb zR_}zMF3;$<)r(YX3ta_)J8g$on?`S}^IzStEwe=Ts287P8)IFU&*AEASS)@GFH%B1 z+SyJ1aePl>qoZ1G7wR=GCgLcNu)|n098NM7Sej1Wg5}(fE}mJsoq8Ix(D3K-v(TnS z%@!!f+bi^U6iGl|F?IYUHrxnO`UIIf=P;Rjz%Qg(jDhJ&ERl_r2*Ij9F-0A5sm80j zb_moozQW3@Cn~?SP=WX|9KNgjr&k);lexA$Q%=_~jOPf8Y@++XPaQAI$X3Chsh*2kg3-gBs-fl6l z<>=<9cy()&lrEv2N~sRkj9>i9xd=MP&5hT@Xu=NNmh7b!7GmJDHSN+?(+Snu&&bxZF*T=?z!PTs%Pc3h!mRdhYK7mKIr-YV!!;4(ovhK+H z4QHIq2s@fQ%tefUSA1D?uJ^#0ylaQ7bU1vor%bb7*x6I{$=rK%yt;Pjo5Tq^R85<5 zweh72=k3XRF?cep2NGt*7P-UW5tM%x`fsywOxNVhh&ksXD2nhb1^G2ZiJl+lYD>s8 z3jees%9*Ru@0Eb?q-N;3eY7-jBR3Rjns^RI9tjayJIjJ#sS?!O!7gw$H@^dylCnlQ z5cj!obH=Ny6$x5byV_Aavl&eJq%9#(mIgFBikMl~%QT#m-cC)_I7!~4GpuZ5ahk(h#eJkO2Kej_F73^i+SZ-%$sz@LaYR?a>l5 z92=Kg(T2xEBe%r&WF=urUzKzvSu@*==%E&Y3VCZE1M^d3@8YoWOlaW#t^XwMLRu+A zw#Do`n(NN`?9OJ~tNhe(4FZ8|b>aBCrgOCqpWMi8i%+((aAwkCKG11MA~Nd$r^ZFS zy^ytO@zH)%HSDnKZ8Rl)RAHyCBrNl8Wi^2`a9e~QQ6X!xLwF>He8}4PEMN6ke)TU~ zE97um=pz|UR4Q$jI+3ka8ceI_EQ)&JDg!?Dxgga`LwEYSF+@2uBnjuchUA|Z^TG56 z@)oKbCb8h@q|gfT2kZP#h^lB;LZ9TPdMSTTDixWiyo#9q6YgtC1`Jl%f(?49wlZ6PA9GTh`EG#ZFwo1KXV=H7tW#WZVK#qd7n+1 zQFccSv}M?#G>wLhn#Uw+Syu`XSvR*ELpUHr1C~-^L7;VysXabF-WLjy)%^_j5a!id zb49P7SKpuSz8p7U3T4YxRNf(kJqtS;=lRi& z&fx_QvK<}LqHFw6!%0T#{KhtesDwVS|Iiop56hclxsd8_AtPd*%NwUDS&mlp;!fXx zeIU$69i8EB32B6@i+2?HS-)FKW02YDh1%X*V0NY(HI9ivW=toozUHqY;OX0g;Nd|F zQ}t1eO;*PGj@WD~m;!f2=G?PpuNUI;{@74rfdCVqy1bNToVPl zG+J>QDq*OdNa(X|=jHe{SFyW{G9ahY8pL2W?pMm7rpR76O}pyIFMGF)`p=)FS~qB= z5dJ##t@26dMie8`vOo34x~@a~NCjp)^2iH6%bkPSt<~bPWr9aLY>~v<CIW z3%h`*JZlIWO;AP#?rS$p{hy(&p^vQ}o43!$`A7M!&b8|eu?jZiW$9o?Vr}7a&2$~V z2y^ogcpF5!2M9jn7Hr4^l> zoznNXuk2%BvN~Ia0O}n{qF9G5ydm~~@vPh63Ey`3_9QP4fons!|7;1Tl6J&AIwKF! zt^dk1vaE}F@+s-t%0iJ;l=7>=TD;$3!phTDf*K=uL8I#zkCsA@;9lF>`5W-VMseP> zc~7h=`nhGsXP~%Zltb5_F>|*~f3$Vf6?!(!&`jwYP1j*wV*7|QCUi|hjKv7MiiQoe zg(fc_CYnc$rLUE)w-;`}T_$KaBpc0Njxb2S05ic|XNmi+(69H#Cc611@`}>)#G=aR zPK8%`M>e4ADG|l#()+QSrtUXV_|cIu7lZ$N<`owV%o?@8M@8bqtMQrZitUD1r*B{W z!%mf-i+{_6>_#5EMujt3hI*vs^@(-D75b3X*9X@&VgC91o5Yz zOful19^4#2y?R|S6~OU5AdiK#F0h+6y&vlszy>hl z!jGuMg7g2w5dz)iwpM0Z+4_$_7}r75>XwtCsM0P}k`$K%CrQK&a)s&u|G(;ciT&+u`u;vKlSj@YzFXs3Y6O?z}| zq_pmrdnidhQjnntv3gV>9E6Kcbh`{~4ljIz`7V?Q1RFSB$LlW$ed_I>^0rLKOz$&0 zh|vL?^?_iv8efhng|&5^82-&Z`bjYJFs<#7DmY3jPOt1KLQoUkw?^c_Mk-_Yc!HTV ziq!TjZpL(CO-zHvmh^2f-Hq(ArwN7cIFWgob~Si>1>sxrOTX)6c;92$N%-5Ru<~Rw zI?#126MhAUY>0tMbn>7}Gw?FjKUp2A+h;b^Nmo*;C2$dVbDGQ!WSx~-k*edj->a7AUwatQg+PWWZfI^a3p2aKky9|O7H{2mi53b} zY>fNzXR<*Oo7|f3_lj=#ds(mTg(9enOl{)!K7-#NHq046_|&$=?PDH;vBOt#WO&Vu ze$m=9H~pXl&yajnBJrrTMy3+-dRtZ*3~$RA`JLD5zJF>u$Hl0vL~Loa^V>I$2g}7W z#4j>If3^ui7pAirRgmP_j8Rt>ai{kL!Iaw6k>A-2@iR1(+grS_@0HislfHXcLxIZ7 z=-;Ogjtk|ejSEgz=gQX?i}PqWeK#(*D1iGO%0)5CYgj^|sNryQtDuD&T?!4ewOIEK zjx~x)6xJMbm1S1Qo|%3AoploHn-Yg1KC%e-R{YYJ+ovd{gO!{ci&O{DY)zyMDQ`P> zS3A#m0t6p6Y98X72f4jj9+rPL5{^|lbf*NVUiIi(d}y*dZ@zZSJhwe>%1Y?{QCD-B zT!};rgYVf`TfbX#i1@KIc=HYUQj9|~8vTzHZ`n$eWxO{sa$%IM5G73!_z9X9DYn>m z9KM`oTd>H=LlA5$Sr+iOzLqLKbGEJX+nys(H_#*uG8-@qr@#-&)2LM8D%mG5x!Q|% zxI|nSw8xedzk8s>TcoV*WJMtXwqJkSO7r3?=~$|%R3tgMow#Ep z`73XoIlPdup8kzh*Y6R-QAq(wP_9Jpay0}$)Shtqt*o|3fPY^r$ilW1HWGOgxzRPy z?$=Y691yt{Uxua+S)gz81RJQL=}rvJTcS}@QOx=g1Wtw0`ac&oZqO@-Lert)zs(;^8j)Jwyx-z2@Dj|(Z}Mt4A!>FtPKFJ)${ zPOtckE?T>BrWuS}j)Nm+ZhxQ~zexN)+~T7P^~2syG(PLH_J_9%Lj@Yif_ zxl`B) zLP_Q!V=r9$UDuYkePNR9W2uSmUE=klrmgHsU6sIRAR_!@D1SKa{RWqh3&Ww}3!!47)vN8}e9iaYQ!iwZ-88NgFzy2Qc!(?dK9rQGf^;bw4aMJlE zOL#0bADx6gzPr)4)vh?3V7SWoZc|;mg}LNM(2^cnZg63HZpsC5^Ln_2#*{vVlueN* zXjm|l9$q^siN_P%tqt30RysFseL|P6smgh9y$)K}5MYD?i&cZ|-(Tr`+EmwTN!+@` z>Zsu#j9=FFXk$@tU;QfijBD1RrQmxYC>B+W=sD*j-3YeO2eNU1c4eyKxDykhM$X=%K0%#y?^-0g7N|EWBJJ6}G=iBOQD z9!qUOd$c=z`n`JfakVjaI6+1gEc-6HyGYJ*-tA!%WTK^GKIPdQo_(Ls@YUS|yAv1E z6JYwfKz=N>TPoQmp>3P!9ur;No$5LStZ!-T<6@Ia)#bJ zGRL|Kg}XsrhGC``Z@Y{c_rS6s9GAoUtB^blnzm|P{fr) z(JtJ>s<4ILc~~YCE%b58Ne$9LjoQw%I!8)+1Uw*Sxc$_Pvqr>~GhycsE#7`#QA>zlrEg5?RYyFX!y}nFD>qT2akd&Cr4^fD zq5!*l%k7lg6Dl>t_nh(T4L=`WtNsKBSdM89y8onjQ{5+YEm>~gv8aVf_R_Exe8=DL z>uuwrsGO;g?qrcU+-N*3XN5W7+e?Ym#jmXIf<8CHzDQyro3Gypyj=fUzm&K1oDWtj zip8G=1RzYyZ~5VjQ)BncM;Xj|b_20QIz(`Kej5lM>73>(AQ^ zD7Vk>^?SZQav z052UHn+N1kpe{ALAB)_Yu{*ch6PAKJo#XDX+71>YiH;VzH4(vxCNT-B6u{!o12*3+ z%|BvYiG3q%bgf!a_XPhnd6(2D#qR9aHg&Tz{7h<)UT*XeTQ3W-JljIl#|PNFHu$C* zkQ`2ygh2#66DNIGG;=-$N}~;6rH7yQ%+Fb~)5>eoq&52UtD*Mv`kKVRni6E7=JuB_v1_e>>(LKo1Bhj!G{x075{g zi%p1Hi`5kKQM0@RUtP0>cw$E;3XtX z9Qr!u3jO#G{?H1=){PRKGV`RnQ&vS|rocaLgJIn8nd?ught=a|6`qQcH=aWUh=;|x z`*01C346)bZ>(+&>YKgJst6vk3Y355rDMyPMwX{%z2_ExOC%)H5AJiGJF1TQy_U$t z^T3?x=&j|z^CEv63xEZ-56d%gUwu1l!hbv)G6OqR#YPGO4;c|vCRVCPCTi1|ORpA% zK~X{Q|E!ZTbF{S|cliBW+a|B6k5MMJoH@_4)nFpgjYYcoThcS-rgJ;F?&hzbf8Y(i z)CnCKZK~4d1%XYvwflTfCYn~?E_=SLH<7vS0W6P*g`0jWEIu&$dDu*JfuA?3uECj? z^y2YRo}8BG?`MyPK1_$>3zJs6=Tub5eYp;y4>0%vtY9zVEHdTGToYaA{^^v@3|Z6> z1ZpAqxn7nfA9CKNg&#w&edDL`zO;W~p7u@k&T><;yRH||zMt#9+*a8pOl<2EaUI69 zsN~=uD+v66a7|KdOlx-V4UZ?}KcdPsFwG%ExdQL0U~9~!0*dnn!c9C`T9UD?RNQm5 zg?~7lU0xKO<+9waMCjv8sa1W$J@eZnGa4sPznv_mx)an@6ex_HNk!na^sBNpPi9~n zEWQ1H+_i?E-V=cwp$;l(I?dlSsyiKB)6sqdG7lVvE(o#}UgN!z!$v*;V*emLUb~G3 zdL+g%d7PUudETu=FkaUGmDtwZ8pk?6EO}X&ud*XAdT-|P_BfB^NowlABM;J(s;W_S zPo!BNaPOP&qu;w* z`bR|m(u=GDOG69WGzeh38yd~zA1brV=SYx|L)bzJ5o3O*=Wt5)yVyoB()onijp*nr zVN3e<+*_?_6iw&B19}QvQRV33$-f+vy6bp~$TwK#` z≧j@y~irmk083($cF&nNVId17yV$ufLbN71PSzw=dch^BCe|1!2Qm@&YZH~UG#*;Eg2o-XK|#Fv! z+LQqK2q-WP5E^mUc-z+Qo1t6bvr4)BNut!+Q|hEqW35zl+LgqwK>Rl(^$r2@G0-lJ zXc$?%Ei?9Ye974DD3|8wlf+;clqvFWfzBS)80A=kTD|e%t(JiT*gK`J>e47p$>Nn{ zWr}LAs{4MfCO^^}w5d>efcbX#qnW1$)Op3}Cvls8-dw#67!JQkngrfpr*7lMT%Kem*7h_uqMVtCLNII`|3pN{fBwUsZVY!H%zMSC6M zV>i;Ovu-vY-WB;l2)XQmc2l7mJc&5H2<^G8AwUi|kM2cLeOE!w#56m(-pXt%fTiM;{902MwdGJPSz!Uj4+1 zY-!NEz~sXfRm^ROzo|X^E2TnZv-9RhFRU@z=`j^*99W+1(a$yvxJrS$kvx4#g_;JIr+RAT zyejT}Xr`BodV=%Jh|kLZo3fPb#l00Vw~X9sQmuK~yPE_$Qe#i2A*p2mIgW-ibH@bY zgY4m%z9R%)3JBt89NcmAwUM4nlgKobHDMt@x&aUORHWNvu9f%y_>beo5D_3ffK2yj zY07N4bR$EQ>Y`LB@E%9QPFN=|c$wvV=5LOYse?Z70~VGvpMEHgujK>ykQoB%`oI*7 ztA=7$*34y~57vpb_yH?M3ccI>Ge6Gf>|!NAWbC8JEBQFKuxNZgH-pW-5a0mW>`w@Ul-lLS|y4Wm6x zHc*MP#-1bY7US&wE7&kIOxcz=avQrP5=(;(9Tr~&yQe9?9Q=tWv#%g9EO}z6^ST1N zb+*S5Jzom^gp)q!dyc+(7}dtK@ei^I*RYmzWmIVJq->t5FaKH*fQ)QA+YM)zK^QR^IALq>x_97GAlRug>Q5 z@s$GPfjBIU#wV(avc~$+5$g-|(8N1vI%z#0-9web=+Vmk2q2FjgTsiRguR4eYky+_ z2b#!-w*78GBouEF&`jddCJ9aCN8{mp-fcduH`U@p87Tll*!;c_l2sjj*H=pjh#GCn zKGyb62m988Ddpr|*dsvN|dF-pchKL1>~ddWeAQ^ZJ%XlAnPBP>!v!Gx$06 z%^92tODfOr3$E!jU`u|cc%*0i=L<<;!nCIL;nkJf5uk%)Ob3S8$05#gGi1DjLO&2CO>z@SYL+)mqUTNZ!+xEC{p+*IoSP`^M$)NI$ND}W=pb=z~fVNP>}0 znaAB|pxp;fjG*v(M7)aY)+)k=aonTI?Jwiyf}e);nh% z`r!-9r`g!xj-2k1F)!Q+XuluPab$1@bWr-a)3q?z0muKwJ{Tu5#N6|Pv30*d6-;lcWWEJ;hCka#%zLXblFFiJ6x&bsPubvIdI$}L;clA2= zd3*!88SPu|%Q}+i6k<#Joq&6ci|(SU*M!F1JA*%6-S2F_1@mJpOrR`eu9n>cQoo(g z-;|ju5qt|zc;#Lt`2&};sX!2s-kzItA(tUVnbqkdRrRh7zcI7i&WwZ$?_t~%o~_2? zANBVhQ~dT0Fid?WC^Ux=9r#0S#%(F?`&VlJcp?P!Aat&89C5dE`>o|~FTR^4(EXx% z!UgOUSmF{-B9YTVFW83rCH;FUrreg2eLeJX={q3OARjx^Qxi_JT1DHqyrV=9)?~ag z@~nI~Ow-bCDK8y<2iz6)%=})omg7sAT~x*Llv9T1Mv0^RT}u+pY6Ydu&&A(WH=bts z(+!3s1!in-iCq$*!Hjr%%e^?<$+PVf0zS-M5!^sucm-*xL@3G|Ef-%gT5%0nI&nYKsu#)LJvuQX^9lbls>+eIImO!mZS)!_^{!xkI40u<<&sPoDKwnkvzxU?5vn!mnbQMi#yh(|` zBp?*?>T+G?YYrT~KYD+Mfq4>UHvg#<&QTEd>FfBtE_xSq*1pcfz+U?P9{ykRMoRgc zby%*yN}>jG*yg{jxVw|BoQA->X!@I8zHiamZh-GcU9zJPuCymNk#mHOJG36~rk4E}~|n*t-chdh<;>jg{m zCbFQNqNdLXPpvqdYGc>QFHvAmoEyxF)$63lC`nmTa;NKx)yWqDHiAES;)im{k{QoA zAK!HFU%ePQutc)4`hnM2)O`MabrYlah@9r@Zjv+X)Q# z^wauL@;dQ(Lbqv;XKBGGzYK_QUMIxal#IHu$_}y$C^M~)04Qu1u>?p+LpPp_R z?v3NUU#GUa;xxfPKM1ER?UERtZyAHAR?a8@C&*s)ud#}@qI-|ulMtaM}KZMFqvYN0&-)Qtw5$_|x!%@cajmEUKn!XS8sfr#jUAlQGL09 z*dAL+pR!QwWo*qE7BqDb3>7}4E|MGOx;&#(1;J7Ei&xze!a2G=H1ntPrHk{{=DWCR z8bwC8@{b>iR?<*ivC)}~gio>Zx2H6W&l(tu=cMD;roW31CGw*pMVUrFo`PRR+t^!E z#LoN@=y@Y!iUomNig!t#v<&1s5l40E9|%F&q`%-I`QQE0{r!n)Q~6zSclXQCNa+$#JPMob1w`4c%f9+febtWX$Q~Fpq2My(!CF`JM6v0Q$>8 zYtO~K`jXYv;~?C4{mbRkkgXTF(eQf+lxfuS%?lM3X%}l~aCO~;`0wx(BOZOz`4bXc zB#>C(#(G?OQWY$iYylEwO-YEG8vx1FRwl%aZxe;dX72Z(H8>4vNPh-NV+Y+{N`_=A zpAMikSZ4*1A8=0yaVgtav4fyX0xOm zRTt|)m#T>kpi5OeLm_6d`y5zp%kyhH{+0(< z@idDGnAe2PkNk|VYX(iyA-<<2Lx9k^oe28+HBm;hF>JkL0yy6S*@%DG$s3mE9{TD= zY7X-$dx8Vrz(W!S`e25!X@gpP&0yQQ_fY?f*aTLvI+40&$h;=jR_=j-=lS1y_WyOG z6NtgsvE~>4j~^9pXXTr~$aRdJ6U~1sh5t~dA Date: Fri, 23 Sep 2022 10:18:30 +0800 Subject: [PATCH 0563/2015] opt: center/align topright when toggle chat --- flutter/lib/models/chat_model.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/flutter/lib/models/chat_model.dart b/flutter/lib/models/chat_model.dart index e9952db1e..dbf5ac753 100644 --- a/flutter/lib/models/chat_model.dart +++ b/flutter/lib/models/chat_model.dart @@ -167,8 +167,10 @@ class ChatModel with ChangeNotifier { _isShowChatPage = !_isShowChatPage; notifyListeners(); await windowManager.setSize(Size(400, 600)); + await windowManager.setAlignment(Alignment.topRight); } else { await windowManager.setSize(Size(800, 600)); + await windowManager.center(); await Future.delayed(Duration(milliseconds: 100)); _isShowChatPage = !_isShowChatPage; notifyListeners(); From d939fdac7258b42efe1915c860b840e09beb63b1 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 23 Sep 2022 11:01:33 +0800 Subject: [PATCH 0564/2015] opt: hide home button when it only exists on tab --- .../lib/desktop/widgets/tabbar_widget.dart | 74 +++++++++++-------- 1 file changed, 43 insertions(+), 31 deletions(-) diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index dc9bdccb8..1b82b6b55 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -490,6 +490,15 @@ class _ListView extends StatelessWidget { this.tabBuilder, this.labelGetter}); + /// Check whether to show ListView + /// + /// Conditions: + /// - hide single item when only has one item (home) on [DesktopTabPage]. + bool isHideSingleItem() { + return state.value.tabs.length == 1 && + controller.tabType == DesktopTabType.main; + } + @override Widget build(BuildContext context) { return Obx(() => ListView( @@ -497,38 +506,41 @@ class _ListView extends StatelessWidget { scrollDirection: Axis.horizontal, shrinkWrap: true, physics: const BouncingScrollPhysics(), - children: state.value.tabs.asMap().entries.map((e) { - final index = e.key; - final tab = e.value; - return _Tab( - index: index, - label: labelGetter == null - ? Rx(tab.label) - : labelGetter!(tab.label), - selectedIcon: tab.selectedIcon, - unselectedIcon: tab.unselectedIcon, - closable: tab.closable, - selected: state.value.selected, - onClose: () { - if (tab.onTabCloseButton != null) { - tab.onTabCloseButton!(); - } else { - controller.remove(index); - } - }, - onSelected: () => controller.jumpTo(index), - tabBuilder: tabBuilder == null - ? null - : (Widget icon, Widget labelWidget, TabThemeConf themeConf) { - return tabBuilder!( - tab.label, - icon, - labelWidget, - themeConf, - ); + children: isHideSingleItem() + ? List.empty() + : state.value.tabs.asMap().entries.map((e) { + final index = e.key; + final tab = e.value; + return _Tab( + index: index, + label: labelGetter == null + ? Rx(tab.label) + : labelGetter!(tab.label), + selectedIcon: tab.selectedIcon, + unselectedIcon: tab.unselectedIcon, + closable: tab.closable, + selected: state.value.selected, + onClose: () { + if (tab.onTabCloseButton != null) { + tab.onTabCloseButton!(); + } else { + controller.remove(index); + } }, - ); - }).toList())); + onSelected: () => controller.jumpTo(index), + tabBuilder: tabBuilder == null + ? null + : (Widget icon, Widget labelWidget, + TabThemeConf themeConf) { + return tabBuilder!( + tab.label, + icon, + labelWidget, + themeConf, + ); + }, + ); + }).toList())); } } From b8a382a0d837fe3116d0d2dfa5a8f18f4806fac4 Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 23 Sep 2022 12:20:40 +0800 Subject: [PATCH 0565/2015] flutter_desktop: remove animation & adjust popup menu Signed-off-by: fufesou --- flutter/lib/common.dart | 12 + flutter/lib/common/widgets/address_book.dart | 7 +- flutter/lib/common/widgets/peer_card.dart | 52 +- flutter/lib/common/widgets/peers_view.dart | 12 +- .../lib/desktop/pages/connection_page.dart | 16 +- .../widgets/material_mod_popup_menu.dart | 5 +- flutter/lib/desktop/widgets/popup_menu.dart | 147 +++-- .../lib/desktop/widgets/remote_menubar.dart | 506 ++++++++++-------- 8 files changed, 478 insertions(+), 279 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 5cd54a0a8..8f8220db1 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -177,6 +177,12 @@ class MyTheme { ), splashColor: Colors.transparent, highlightColor: Colors.transparent, + splashFactory: isDesktop ? NoSplash.splashFactory : null, + textButtonTheme: isDesktop + ? TextButtonThemeData( + style: ButtonStyle(splashFactory: NoSplash.splashFactory), + ) + : null, ).copyWith( extensions: >[ ColorThemeExtension.light, @@ -192,6 +198,12 @@ class MyTheme { ), splashColor: Colors.transparent, highlightColor: Colors.transparent, + splashFactory: isDesktop ? NoSplash.splashFactory : null, + textButtonTheme: isDesktop + ? TextButtonThemeData( + style: ButtonStyle(splashFactory: NoSplash.splashFactory), + ) + : null, ).copyWith( extensions: >[ ColorThemeExtension.dark, diff --git a/flutter/lib/common/widgets/address_book.dart b/flutter/lib/common/widgets/address_book.dart index 9fac81723..ab5f924dd 100644 --- a/flutter/lib/common/widgets/address_book.dart +++ b/flutter/lib/common/widgets/address_book.dart @@ -11,7 +11,8 @@ import '../../mobile/pages/settings_page.dart'; import '../../models/platform_model.dart'; class AddressBook extends StatefulWidget { - const AddressBook({Key? key}) : super(key: key); + final EdgeInsets? menuPadding; + const AddressBook({Key? key, this.menuPadding}) : super(key: key); @override State createState() { @@ -180,7 +181,9 @@ class _AddressBookState extends State { Expanded( child: Align( alignment: Alignment.topLeft, - child: AddressBookPeersView()), + child: AddressBookPeersView( + menuPadding: widget.menuPadding, + )), ) ], )); diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index 9c0c997bc..2f6421880 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -14,7 +14,7 @@ import '../../desktop/widgets/popup_menu.dart'; class _PopupMenuTheme { static const Color commonColor = MyTheme.accent; // kMinInteractiveDimension - static const double height = 25.0; + static const double height = 20.0; static const double dividerHeight = 3.0; } @@ -319,8 +319,10 @@ class _PeerCardState extends State<_PeerCard> abstract class BasePeerCard extends StatelessWidget { final Peer peer; + final EdgeInsets? menuPadding; - BasePeerCard({required this.peer, Key? key}) : super(key: key); + BasePeerCard({required this.peer, this.menuPadding, Key? key}) + : super(key: key); @override Widget build(BuildContext context) { @@ -365,6 +367,7 @@ abstract class BasePeerCard extends StatelessWidget { isRDP: isRDP, ); }, + padding: menuPadding, dismissOnClicked: true, ); } @@ -414,17 +417,25 @@ abstract class BasePeerCard extends StatelessWidget { Expanded( child: Align( alignment: Alignment.centerRight, - child: IconButton( - padding: EdgeInsets.zero, - icon: const Icon(Icons.edit), - onPressed: () => _rdpDialog(id), - ), + child: Transform.scale( + scale: 0.8, + child: IconButton( + icon: const Icon(Icons.edit), + padding: EdgeInsets.zero, + onPressed: () { + if (Navigator.canPop(context)) { + Navigator.pop(context); + } + _rdpDialog(id); + }, + )), )) ], )), proc: () { connect(context, id, isRDP: true); }, + padding: menuPadding, dismissOnClicked: true, ); } @@ -439,6 +450,7 @@ abstract class BasePeerCard extends StatelessWidget { proc: () { bind.mainWol(id: id); }, + padding: menuPadding, dismissOnClicked: true, ); } @@ -447,6 +459,7 @@ abstract class BasePeerCard extends StatelessWidget { Future> _forceAlwaysRelayAction(String id) async { const option = 'force-always-relay'; return MenuEntrySwitch( + switchType: SwitchType.scheckbox, text: translate('Always connect via relay'), getter: () async { return (await bind.mainGetPeerOption(id: id, key: option)).isNotEmpty; @@ -461,7 +474,8 @@ abstract class BasePeerCard extends StatelessWidget { } await bind.mainSetPeerOption(id: id, key: option, value: value); }, - dismissOnClicked: false, + padding: menuPadding, + dismissOnClicked: true, ); } @@ -475,6 +489,7 @@ abstract class BasePeerCard extends StatelessWidget { proc: () { _rename(id, isAddressBook); }, + padding: menuPadding, dismissOnClicked: true, ); } @@ -494,6 +509,7 @@ abstract class BasePeerCard extends StatelessWidget { await reloadFunc(); }(); }, + padding: menuPadding, dismissOnClicked: true, ); } @@ -508,6 +524,7 @@ abstract class BasePeerCard extends StatelessWidget { proc: () { bind.mainForgetPassword(id: id); }, + padding: menuPadding, dismissOnClicked: true, ); } @@ -528,6 +545,7 @@ abstract class BasePeerCard extends StatelessWidget { } }(); }, + padding: menuPadding, dismissOnClicked: true, ); } @@ -549,6 +567,7 @@ abstract class BasePeerCard extends StatelessWidget { } }(); }, + padding: menuPadding, dismissOnClicked: true, ); } @@ -616,7 +635,8 @@ abstract class BasePeerCard extends StatelessWidget { } class RecentPeerCard extends BasePeerCard { - RecentPeerCard({required Peer peer, Key? key}) : super(peer: peer, key: key); + RecentPeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key}) + : super(peer: peer, menuPadding: menuPadding, key: key); @override Future>> _buildMenuItems( @@ -645,8 +665,8 @@ class RecentPeerCard extends BasePeerCard { } class FavoritePeerCard extends BasePeerCard { - FavoritePeerCard({required Peer peer, Key? key}) - : super(peer: peer, key: key); + FavoritePeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key}) + : super(peer: peer, menuPadding: menuPadding, key: key); @override Future>> _buildMenuItems( @@ -677,8 +697,8 @@ class FavoritePeerCard extends BasePeerCard { } class DiscoveredPeerCard extends BasePeerCard { - DiscoveredPeerCard({required Peer peer, Key? key}) - : super(peer: peer, key: key); + DiscoveredPeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key}) + : super(peer: peer, menuPadding: menuPadding, key: key); @override Future>> _buildMenuItems( @@ -706,8 +726,8 @@ class DiscoveredPeerCard extends BasePeerCard { } class AddressBookPeerCard extends BasePeerCard { - AddressBookPeerCard({required Peer peer, Key? key}) - : super(peer: peer, key: key); + AddressBookPeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key}) + : super(peer: peer, menuPadding: menuPadding, key: key); @override Future>> _buildMenuItems( @@ -748,6 +768,7 @@ class AddressBookPeerCard extends BasePeerCard { await gFFI.abModel.updateAb(); }(); }, + padding: super.menuPadding, dismissOnClicked: true, ); } @@ -762,6 +783,7 @@ class AddressBookPeerCard extends BasePeerCard { proc: () { _abEditTag(id); }, + padding: super.menuPadding, dismissOnClicked: true, ); } diff --git a/flutter/lib/common/widgets/peers_view.dart b/flutter/lib/common/widgets/peers_view.dart index cf9c4299a..63c29af6d 100644 --- a/flutter/lib/common/widgets/peers_view.dart +++ b/flutter/lib/common/widgets/peers_view.dart @@ -224,7 +224,7 @@ abstract class BasePeersView extends StatelessWidget { } class RecentPeersView extends BasePeersView { - RecentPeersView({Key? key}) + RecentPeersView({Key? key, EdgeInsets? menuPadding}) : super( key: key, name: 'recent peer', @@ -232,6 +232,7 @@ class RecentPeersView extends BasePeersView { offstageFunc: (Peer peer) => false, peerCardBuilder: (Peer peer) => RecentPeerCard( peer: peer, + menuPadding: menuPadding, ), initPeers: [], ); @@ -245,7 +246,7 @@ class RecentPeersView extends BasePeersView { } class FavoritePeersView extends BasePeersView { - FavoritePeersView({Key? key}) + FavoritePeersView({Key? key, EdgeInsets? menuPadding}) : super( key: key, name: 'favorite peer', @@ -253,6 +254,7 @@ class FavoritePeersView extends BasePeersView { offstageFunc: (Peer peer) => false, peerCardBuilder: (Peer peer) => FavoritePeerCard( peer: peer, + menuPadding: menuPadding, ), initPeers: [], ); @@ -266,7 +268,7 @@ class FavoritePeersView extends BasePeersView { } class DiscoveredPeersView extends BasePeersView { - DiscoveredPeersView({Key? key}) + DiscoveredPeersView({Key? key, EdgeInsets? menuPadding}) : super( key: key, name: 'discovered peer', @@ -274,6 +276,7 @@ class DiscoveredPeersView extends BasePeersView { offstageFunc: (Peer peer) => false, peerCardBuilder: (Peer peer) => DiscoveredPeerCard( peer: peer, + menuPadding: menuPadding, ), initPeers: [], ); @@ -287,7 +290,7 @@ class DiscoveredPeersView extends BasePeersView { } class AddressBookPeersView extends BasePeersView { - AddressBookPeersView({Key? key}) + AddressBookPeersView({Key? key, EdgeInsets? menuPadding}) : super( key: key, name: 'address book peer', @@ -296,6 +299,7 @@ class AddressBookPeersView extends BasePeersView { !_hitTag(gFFI.abModel.selectedTags, peer.tags), peerCardBuilder: (Peer peer) => AddressBookPeerCard( peer: peer, + menuPadding: menuPadding, ), initPeers: _loadPeers(), ); diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 6a8c58f7b..9ff7befd7 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -74,10 +74,18 @@ class _ConnectionPageState extends State { translate('Address Book') ], children: [ - RecentPeersView(), - FavoritePeersView(), - DiscoveredPeersView(), - const AddressBook(), + RecentPeersView( + menuPadding: EdgeInsets.only(left: 12.0, right: 3.0), + ), + FavoritePeersView( + menuPadding: EdgeInsets.only(left: 12.0, right: 3.0), + ), + DiscoveredPeersView( + menuPadding: EdgeInsets.only(left: 12.0, right: 3.0), + ), + const AddressBook( + menuPadding: EdgeInsets.only(left: 12.0, right: 3.0), + ), ], )), ], diff --git a/flutter/lib/desktop/widgets/material_mod_popup_menu.dart b/flutter/lib/desktop/widgets/material_mod_popup_menu.dart index 1345f72f1..776a2b756 100644 --- a/flutter/lib/desktop/widgets/material_mod_popup_menu.dart +++ b/flutter/lib/desktop/widgets/material_mod_popup_menu.dart @@ -14,7 +14,8 @@ import 'package:flutter/material.dart'; // void setState(VoidCallback fn) { } // enum Menu { itemOne, itemTwo, itemThree, itemFour } -const Duration _kMenuDuration = Duration(milliseconds: 300); +// const Duration _kMenuDuration = Duration(milliseconds: 300); +const Duration _kMenuDuration = Duration(milliseconds: 0); const double _kMenuCloseIntervalEnd = 2.0 / 3.0; const double _kMenuHorizontalPadding = 16.0; const double _kMenuDividerHeight = 16.0; @@ -22,7 +23,7 @@ const double _kMenuDividerHeight = 16.0; const double _kMenuMinWidth = 2.0 * _kMenuWidthStep; const double _kMenuMaxWidth = double.infinity; // const double _kMenuVerticalPadding = 8.0; -const double _kMenuVerticalPadding = 0.0; +const double _kMenuVerticalPadding = 8.0; const double _kMenuWidthStep = 0.0; //const double _kMenuScreenPadding = 8.0; const double _kMenuScreenPadding = 0.0; diff --git a/flutter/lib/desktop/widgets/popup_menu.dart b/flutter/lib/desktop/widgets/popup_menu.dart index 3814561ee..84fd69b0e 100644 --- a/flutter/lib/desktop/widgets/popup_menu.dart +++ b/flutter/lib/desktop/widgets/popup_menu.dart @@ -78,7 +78,8 @@ class MyPopupMenuItemState> duration: kThemeChangeDuration, child: Container( alignment: AlignmentDirectional.centerStart, - constraints: BoxConstraints(minHeight: widget.height), + constraints: BoxConstraints( + minHeight: widget.height, maxHeight: widget.height), padding: widget.padding ?? const EdgeInsets.symmetric(horizontal: 16), child: widget.child, @@ -156,12 +157,14 @@ class MenuEntryRadios extends MenuEntryBase { final RadioCurOptionGetter curOptionGetter; final RadioOptionSetter optionSetter; final RxString _curOption = "".obs; + final EdgeInsets? padding; MenuEntryRadios({ required this.text, required this.optionsGetter, required this.curOptionGetter, required this.optionSetter, + this.padding, dismissOnClicked = false, RxBool? enabled, }) : super(dismissOnClicked: dismissOnClicked, enabled: enabled) { @@ -189,8 +192,10 @@ class MenuEntryRadios extends MenuEntryBase { height: conf.height, child: TextButton( child: Container( + padding: padding, alignment: AlignmentDirectional.centerStart, - constraints: BoxConstraints(minHeight: conf.height), + constraints: + BoxConstraints(minHeight: conf.height, maxHeight: conf.height), child: Row( children: [ Text( @@ -202,17 +207,22 @@ class MenuEntryRadios extends MenuEntryBase { ), Expanded( child: Align( - alignment: Alignment.centerRight, - child: SizedBox( - width: 20.0, - height: 20.0, - child: Obx(() => opt.value == curOption.value - ? Icon( - Icons.check, - color: conf.commonColor, - ) - : const SizedBox.shrink())), - )), + alignment: Alignment.centerRight, + child: Transform.scale( + scale: MenuConfig.iconScale, + child: Obx(() => opt.value == curOption.value + ? IconButton( + padding: const EdgeInsets.fromLTRB( + 8.0, 0.0, 8.0, 0.0), + hoverColor: Colors.transparent, + focusColor: Colors.transparent, + onPressed: () {}, + icon: Icon( + Icons.check, + color: conf.commonColor, + )) + : const SizedBox.shrink()), + ))), ], ), ), @@ -239,12 +249,14 @@ class MenuEntrySubRadios extends MenuEntryBase { final RadioCurOptionGetter curOptionGetter; final RadioOptionSetter optionSetter; final RxString _curOption = "".obs; + final EdgeInsets? padding; MenuEntrySubRadios({ required this.text, required this.optionsGetter, required this.curOptionGetter, required this.optionSetter, + this.padding, dismissOnClicked = false, RxBool? enabled, }) : super( @@ -275,8 +287,10 @@ class MenuEntrySubRadios extends MenuEntryBase { height: conf.height, child: TextButton( child: Container( + padding: padding, alignment: AlignmentDirectional.centerStart, - constraints: BoxConstraints(minHeight: conf.height), + constraints: + BoxConstraints(minHeight: conf.height, maxHeight: conf.height), child: Row( children: [ Text( @@ -289,14 +303,18 @@ class MenuEntrySubRadios extends MenuEntryBase { Expanded( child: Align( alignment: Alignment.centerRight, - child: SizedBox( - width: 20.0, - height: 20.0, + child: Transform.scale( + scale: MenuConfig.iconScale, child: Obx(() => opt.value == curOption.value - ? Icon( - Icons.check, - color: conf.commonColor, - ) + ? IconButton( + padding: EdgeInsets.zero, + hoverColor: Colors.transparent, + focusColor: Colors.transparent, + onPressed: () {}, + icon: Icon( + Icons.check, + color: conf.commonColor, + )) : const SizedBox.shrink())), )), ], @@ -318,7 +336,7 @@ class MenuEntrySubRadios extends MenuEntryBase { return [ PopupMenuChildrenItem( enabled: super.enabled, - padding: EdgeInsets.zero, + padding: padding, height: conf.height, itemBuilder: (BuildContext context) => options.map((opt) => _buildSecondMenu(context, conf, opt)).toList(), @@ -345,22 +363,31 @@ class MenuEntrySubRadios extends MenuEntryBase { } } +enum SwitchType { + sswitch, + scheckbox, +} + typedef SwitchGetter = Future Function(); typedef SwitchSetter = Future Function(bool); abstract class MenuEntrySwitchBase extends MenuEntryBase { + final SwitchType switchType; final String text; + final EdgeInsets? padding; Rx? textStyle; MenuEntrySwitchBase({ + required this.switchType, required this.text, required dismissOnClicked, this.textStyle, + this.padding, RxBool? enabled, }) : super(dismissOnClicked: dismissOnClicked, enabled: enabled); RxBool get curOption; - Future setOption(bool option); + Future setOption(bool? option); @override List> build( @@ -376,6 +403,7 @@ abstract class MenuEntrySwitchBase extends MenuEntryBase { height: conf.height, child: TextButton( child: Container( + padding: padding, alignment: AlignmentDirectional.centerStart, height: conf.height, child: Row(children: [ @@ -386,16 +414,33 @@ abstract class MenuEntrySwitchBase extends MenuEntryBase { Expanded( child: Align( alignment: Alignment.centerRight, - child: Obx(() => Switch( - value: curOption.value, - onChanged: (v) { - if (super.dismissOnClicked && - Navigator.canPop(context)) { - Navigator.pop(context); - } - setOption(v); - }, - )), + child: Transform.scale( + scale: MenuConfig.iconScale, + child: Obx(() { + if (switchType == SwitchType.sswitch) { + return Switch( + value: curOption.value, + onChanged: (v) { + if (super.dismissOnClicked && + Navigator.canPop(context)) { + Navigator.pop(context); + } + setOption(v); + }, + ); + } else { + return Checkbox( + value: curOption.value, + onChanged: (v) { + if (super.dismissOnClicked && + Navigator.canPop(context)) { + Navigator.pop(context); + } + setOption(v); + }, + ); + } + })), )) ])), onPressed: () { @@ -416,15 +461,19 @@ class MenuEntrySwitch extends MenuEntrySwitchBase { final RxBool _curOption = false.obs; MenuEntrySwitch({ + required SwitchType switchType, required String text, required this.getter, required this.setter, Rx? textStyle, + EdgeInsets? padding, dismissOnClicked = false, RxBool? enabled, }) : super( + switchType: switchType, text: text, textStyle: textStyle, + padding: padding, dismissOnClicked: dismissOnClicked, enabled: enabled, ) { @@ -436,11 +485,13 @@ class MenuEntrySwitch extends MenuEntrySwitchBase { @override RxBool get curOption => _curOption; @override - setOption(bool option) async { - await setter(option); - final opt = await getter(); - if (_curOption.value != opt) { - _curOption.value = opt; + setOption(bool? option) async { + if (option != null) { + await setter(option); + final opt = await getter(); + if (_curOption.value != opt) { + _curOption.value = opt; + } } } } @@ -453,32 +504,40 @@ class MenuEntrySwitch2 extends MenuEntrySwitchBase { final SwitchSetter setter; MenuEntrySwitch2({ + required SwitchType switchType, required String text, required this.getter, required this.setter, Rx? textStyle, + EdgeInsets? padding, dismissOnClicked = false, RxBool? enabled, }) : super( + switchType: switchType, text: text, textStyle: textStyle, + padding: padding, dismissOnClicked: dismissOnClicked); @override RxBool get curOption => getter(); @override - setOption(bool option) async { - await setter(option); + setOption(bool? option) async { + if (option != null) { + await setter(option); + } } } class MenuEntrySubMenu extends MenuEntryBase { final String text; final List> entries; + final EdgeInsets? padding; MenuEntrySubMenu({ required this.text, required this.entries, + this.padding, RxBool? enabled, }) : super(enabled: enabled); @@ -490,7 +549,7 @@ class MenuEntrySubMenu extends MenuEntryBase { PopupMenuChildrenItem( enabled: super.enabled, height: conf.height, - padding: EdgeInsets.zero, + padding: padding, position: mod_menu.PopupMenuPosition.overSide, itemBuilder: (BuildContext context) => entries .map((entry) => entry.build(context, conf)) @@ -522,10 +581,12 @@ class MenuEntrySubMenu extends MenuEntryBase { class MenuEntryButton extends MenuEntryBase { final Widget Function(TextStyle? style) childBuilder; Function() proc; + final EdgeInsets? padding; MenuEntryButton({ required this.childBuilder, required this.proc, + this.padding, dismissOnClicked = false, RxBool? enabled, }) : super( @@ -553,8 +614,10 @@ class MenuEntryButton extends MenuEntryBase { } : null, child: Container( + padding: padding, alignment: AlignmentDirectional.centerStart, - constraints: BoxConstraints(minHeight: conf.height), + constraints: + BoxConstraints(minHeight: conf.height, maxHeight: conf.height), child: childBuilder( super.enabled!.value ? enabledStyle : disabledStyle), ), diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 070ad217b..67e69a1d9 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -19,7 +19,7 @@ import './material_mod_popup_menu.dart' as mod_menu; class _MenubarTheme { static const Color commonColor = MyTheme.accent; // kMinInteractiveDimension - static const double height = 25.0; + static const double height = 20.0; static const double dividerHeight = 12.0; } @@ -367,31 +367,41 @@ class _RemoteMenubarState extends State { List> _getControlMenu(BuildContext context) { final pi = widget.ffi.ffiModel.pi; final perms = widget.ffi.ffiModel.permissions; - + const EdgeInsets padding = EdgeInsets.only(left: 14.0, right: 5.0); final List> displayMenu = []; displayMenu.addAll([ MenuEntryButton( - childBuilder: (TextStyle? style) => Row( - children: [ - Text( - translate('OS Password'), - style: style, - ), - Expanded( - child: Align( - alignment: Alignment.centerRight, - child: IconButton( - padding: EdgeInsets.zero, - icon: const Icon(Icons.edit), - onPressed: () => showSetOSPassword( - widget.id, false, widget.ffi.dialogManager), - ), - )) - ], - ), + childBuilder: (TextStyle? style) => Container( + alignment: AlignmentDirectional.center, + height: _MenubarTheme.height, + child: Row( + children: [ + Text( + translate('OS Password'), + style: style, + ), + Expanded( + child: Align( + alignment: Alignment.centerRight, + child: Transform.scale( + scale: 0.8, + child: IconButton( + padding: EdgeInsets.zero, + icon: const Icon(Icons.edit), + onPressed: () { + if (Navigator.canPop(context)) { + Navigator.pop(context); + } + showSetOSPassword( + widget.id, false, widget.ffi.dialogManager); + })), + )) + ], + )), proc: () { showSetOSPassword(widget.id, false, widget.ffi.dialogManager); }, + padding: padding, dismissOnClicked: true, ), MenuEntryButton( @@ -402,6 +412,7 @@ class _RemoteMenubarState extends State { proc: () { connect(context, widget.id, isFileTransfer: true); }, + padding: padding, dismissOnClicked: true, ), MenuEntryButton( @@ -409,6 +420,7 @@ class _RemoteMenubarState extends State { translate('TCP Tunneling'), style: style, ), + padding: padding, proc: () { connect(context, widget.id, isTcpTunneling: true); }, @@ -427,6 +439,7 @@ class _RemoteMenubarState extends State { proc: () { showAuditDialog(widget.id, widget.ffi.dialogManager); }, + padding: padding, dismissOnClicked: true, ), ); @@ -443,6 +456,7 @@ class _RemoteMenubarState extends State { proc: () { bind.sessionCtrlAltDel(id: widget.id); }, + padding: padding, dismissOnClicked: true, )); } @@ -459,6 +473,7 @@ class _RemoteMenubarState extends State { proc: () { showRestartRemoteDevice(pi, widget.id, gFFI.dialogManager); }, + padding: padding, dismissOnClicked: true, )); } @@ -472,6 +487,7 @@ class _RemoteMenubarState extends State { proc: () { bind.sessionLockScreen(id: widget.id); }, + padding: padding, dismissOnClicked: true, )); @@ -489,6 +505,7 @@ class _RemoteMenubarState extends State { value: '${blockInput.value ? "un" : ""}block-input'); blockInput.value = !blockInput.value; }, + padding: padding, dismissOnClicked: true, )); } @@ -503,6 +520,7 @@ class _RemoteMenubarState extends State { proc: () { bind.sessionRefresh(id: widget.id); }, + padding: padding, dismissOnClicked: true, )); } @@ -523,6 +541,7 @@ class _RemoteMenubarState extends State { // } // }(); // }, + // padding: padding, // dismissOnClicked: true, // )); // } @@ -535,6 +554,7 @@ class _RemoteMenubarState extends State { proc: () { widget.ffi.cursorModel.reset(); }, + padding: padding, dismissOnClicked: true, )); } @@ -543,125 +563,155 @@ class _RemoteMenubarState extends State { } List> _getDisplayMenu(dynamic futureData) { + const EdgeInsets padding = EdgeInsets.only(left: 18.0, right: 8.0); final displayMenu = [ MenuEntryRadios( - text: translate('Ratio'), - optionsGetter: () => [ - MenuEntryRadioOption( - text: translate('Scale original'), value: 'original'), - MenuEntryRadioOption( - text: translate('Scale adaptive'), value: 'adaptive'), - ], - curOptionGetter: () async { - return await bind.sessionGetOption( - id: widget.id, arg: 'view-style') ?? - 'adaptive'; - }, - optionSetter: (String oldValue, String newValue) async { - await bind.sessionPeerOption( - id: widget.id, name: "view-style", value: newValue); - widget.ffi.canvasModel.updateViewStyle(); - }), + text: translate('Ratio'), + optionsGetter: () => [ + MenuEntryRadioOption( + text: translate('Scale original'), + value: 'original', + dismissOnClicked: true, + ), + MenuEntryRadioOption( + text: translate('Scale adaptive'), + value: 'adaptive', + dismissOnClicked: true, + ), + ], + curOptionGetter: () async { + return await bind.sessionGetOption( + id: widget.id, arg: 'view-style') ?? + 'adaptive'; + }, + optionSetter: (String oldValue, String newValue) async { + await bind.sessionPeerOption( + id: widget.id, name: "view-style", value: newValue); + widget.ffi.canvasModel.updateViewStyle(); + }, + padding: padding, + dismissOnClicked: true, + ), MenuEntryDivider(), MenuEntryRadios( - text: translate('Scroll Style'), - optionsGetter: () => [ - MenuEntryRadioOption( - text: translate('ScrollAuto'), value: 'scrollauto'), - MenuEntryRadioOption( - text: translate('Scrollbar'), value: 'scrollbar'), - ], - curOptionGetter: () async { - return await bind.sessionGetOption( - id: widget.id, arg: 'scroll-style') ?? - ''; - }, - optionSetter: (String oldValue, String newValue) async { - await bind.sessionPeerOption( - id: widget.id, name: "scroll-style", value: newValue); - widget.ffi.canvasModel.updateScrollStyle(); - }), + text: translate('Scroll Style'), + optionsGetter: () => [ + MenuEntryRadioOption( + text: translate('ScrollAuto'), + value: 'scrollauto', + dismissOnClicked: true, + ), + MenuEntryRadioOption( + text: translate('Scrollbar'), + value: 'scrollbar', + dismissOnClicked: true, + ), + ], + curOptionGetter: () async { + return await bind.sessionGetOption( + id: widget.id, arg: 'scroll-style') ?? + ''; + }, + optionSetter: (String oldValue, String newValue) async { + await bind.sessionPeerOption( + id: widget.id, name: "scroll-style", value: newValue); + widget.ffi.canvasModel.updateScrollStyle(); + }, + padding: padding, + dismissOnClicked: true, + ), MenuEntryDivider(), MenuEntryRadios( - text: translate('Image Quality'), - optionsGetter: () => [ - MenuEntryRadioOption( - text: translate('Good image quality'), value: 'best'), - MenuEntryRadioOption( - text: translate('Balanced'), value: 'balanced'), - MenuEntryRadioOption( - text: translate('Optimize reaction time'), value: 'low'), - MenuEntryRadioOption( - text: translate('Custom'), - value: 'custom', - dismissOnClicked: true), - ], - curOptionGetter: () async { - String quality = - await bind.sessionGetImageQuality(id: widget.id) ?? 'balanced'; - if (quality == '') quality = 'balanced'; - return quality; - }, - optionSetter: (String oldValue, String newValue) async { - if (oldValue != newValue) { - await bind.sessionSetImageQuality(id: widget.id, value: newValue); - } + text: translate('Image Quality'), + optionsGetter: () => [ + MenuEntryRadioOption( + text: translate('Good image quality'), + value: 'best', + dismissOnClicked: true, + ), + MenuEntryRadioOption( + text: translate('Balanced'), + value: 'balanced', + dismissOnClicked: true, + ), + MenuEntryRadioOption( + text: translate('Optimize reaction time'), + value: 'low', + dismissOnClicked: true, + ), + MenuEntryRadioOption( + text: translate('Custom'), + value: 'custom', + dismissOnClicked: true), + ], + curOptionGetter: () async { + String quality = + await bind.sessionGetImageQuality(id: widget.id) ?? 'balanced'; + if (quality == '') quality = 'balanced'; + return quality; + }, + optionSetter: (String oldValue, String newValue) async { + if (oldValue != newValue) { + await bind.sessionSetImageQuality(id: widget.id, value: newValue); + } - if (newValue == 'custom') { - final btnCancel = msgBoxButton(translate('Close'), () { - widget.ffi.dialogManager.dismissAll(); - }); - final quality = - await bind.sessionGetCustomImageQuality(id: widget.id); - double initValue = quality != null && quality.isNotEmpty - ? quality[0].toDouble() - : 50.0; - const minValue = 10.0; - const maxValue = 100.0; - if (initValue < minValue) { - initValue = minValue; - } - if (initValue > maxValue) { - initValue = maxValue; - } - final RxDouble sliderValue = RxDouble(initValue); - final rxReplay = rxdart.ReplaySubject(); - rxReplay - .throttleTime(const Duration(milliseconds: 1000), - trailing: true, leading: false) - .listen((double v) { - () async { - await bind.sessionSetCustomImageQuality( - id: widget.id, value: v.toInt()); - }(); - }); - final slider = Obx(() { - return Slider( - value: sliderValue.value, - min: minValue, - max: maxValue, - divisions: 90, - onChanged: (double value) { - sliderValue.value = value; - rxReplay.add(value); - }, - ); - }); - final content = Row( - children: [ - slider, - SizedBox( - width: 90, - child: Obx(() => Text( - '${sliderValue.value.round()}% Bitrate', - style: const TextStyle(fontSize: 15), - ))) - ], - ); - msgBoxCommon(widget.ffi.dialogManager, 'Custom Image Quality', - content, [btnCancel]); + if (newValue == 'custom') { + final btnCancel = msgBoxButton(translate('Close'), () { + widget.ffi.dialogManager.dismissAll(); + }); + final quality = + await bind.sessionGetCustomImageQuality(id: widget.id); + double initValue = quality != null && quality.isNotEmpty + ? quality[0].toDouble() + : 50.0; + const minValue = 10.0; + const maxValue = 100.0; + if (initValue < minValue) { + initValue = minValue; } - }), + if (initValue > maxValue) { + initValue = maxValue; + } + final RxDouble sliderValue = RxDouble(initValue); + final rxReplay = rxdart.ReplaySubject(); + rxReplay + .throttleTime(const Duration(milliseconds: 1000), + trailing: true, leading: false) + .listen((double v) { + () async { + await bind.sessionSetCustomImageQuality( + id: widget.id, value: v.toInt()); + }(); + }); + final slider = Obx(() { + return Slider( + value: sliderValue.value, + min: minValue, + max: maxValue, + divisions: 90, + onChanged: (double value) { + sliderValue.value = value; + rxReplay.add(value); + }, + ); + }); + final content = Row( + children: [ + slider, + SizedBox( + width: 90, + child: Obx(() => Text( + '${sliderValue.value.round()}% Bitrate', + style: const TextStyle(fontSize: 15), + ))) + ], + ); + msgBoxCommon(widget.ffi.dialogManager, 'Custom Image Quality', + content, [btnCancel]); + } + }, + padding: padding, + ), MenuEntryDivider(), ]; @@ -677,30 +727,49 @@ class _RemoteMenubarState extends State { } finally {} if (codecs.length == 2 && (codecs[0] || codecs[1])) { displayMenu.add(MenuEntryRadios( - text: translate('Codec Preference'), - optionsGetter: () { - final list = [ - MenuEntryRadioOption(text: translate('Auto'), value: 'auto'), - MenuEntryRadioOption(text: 'VP9', value: 'vp9'), - ]; - if (codecs[0]) { - list.add(MenuEntryRadioOption(text: 'H264', value: 'h264')); - } - if (codecs[1]) { - list.add(MenuEntryRadioOption(text: 'H265', value: 'h265')); - } - return list; - }, - curOptionGetter: () async { - return await bind.sessionGetOption( - id: widget.id, arg: 'codec-preference') ?? - 'auto'; - }, - optionSetter: (String oldValue, String newValue) async { - await bind.sessionPeerOption( - id: widget.id, name: "codec-preference", value: newValue); - bind.sessionChangePreferCodec(id: widget.id); - })); + text: translate('Codec Preference'), + optionsGetter: () { + final list = [ + MenuEntryRadioOption( + text: translate('Auto'), + value: 'auto', + dismissOnClicked: true, + ), + MenuEntryRadioOption( + text: 'VP9', + value: 'vp9', + dismissOnClicked: true, + ), + ]; + if (codecs[0]) { + list.add(MenuEntryRadioOption( + text: 'H264', + value: 'h264', + dismissOnClicked: true, + )); + } + if (codecs[1]) { + list.add(MenuEntryRadioOption( + text: 'H265', + value: 'h265', + dismissOnClicked: true, + )); + } + return list; + }, + curOptionGetter: () async { + return await bind.sessionGetOption( + id: widget.id, arg: 'codec-preference') ?? + 'auto'; + }, + optionSetter: (String oldValue, String newValue) async { + await bind.sessionPeerOption( + id: widget.id, name: "codec-preference", value: newValue); + bind.sessionChangePreferCodec(id: widget.id); + }, + padding: padding, + dismissOnClicked: true, + )); } } @@ -708,62 +777,74 @@ class _RemoteMenubarState extends State { displayMenu.add(() { final state = ShowRemoteCursorState.find(widget.id); return MenuEntrySwitch2( - text: translate('Show remote cursor'), - getter: () { - return state; - }, - setter: (bool v) async { - state.value = v; - await bind.sessionToggleOption( - id: widget.id, value: 'show-remote-cursor'); - }); + switchType: SwitchType.scheckbox, + text: translate('Show remote cursor'), + getter: () { + return state; + }, + setter: (bool v) async { + state.value = v; + await bind.sessionToggleOption( + id: widget.id, value: 'show-remote-cursor'); + }, + padding: padding, + dismissOnClicked: true, + ); }()); /// Show quality monitor displayMenu.add(MenuEntrySwitch( - text: translate('Show quality monitor'), - getter: () async { - return bind.sessionGetToggleOptionSync( - id: widget.id, arg: 'show-quality-monitor'); - }, - setter: (bool v) async { - await bind.sessionToggleOption( - id: widget.id, value: 'show-quality-monitor'); - widget.ffi.qualityMonitorModel.checkShowQualityMonitor(widget.id); - })); + switchType: SwitchType.scheckbox, + text: translate('Show quality monitor'), + getter: () async { + return bind.sessionGetToggleOptionSync( + id: widget.id, arg: 'show-quality-monitor'); + }, + setter: (bool v) async { + await bind.sessionToggleOption( + id: widget.id, value: 'show-quality-monitor'); + widget.ffi.qualityMonitorModel.checkShowQualityMonitor(widget.id); + }, + padding: padding, + dismissOnClicked: true, + )); final perms = widget.ffi.ffiModel.permissions; final pi = widget.ffi.ffiModel.pi; if (perms['audio'] != false) { - displayMenu.add(_createSwitchMenuEntry('Mute', 'disable-audio')); + displayMenu + .add(_createSwitchMenuEntry('Mute', 'disable-audio', padding, true)); } if (Platform.isWindows && pi.platform == 'Windows' && perms['file'] != false) { displayMenu.add(_createSwitchMenuEntry( - 'Allow file copy and paste', 'enable-file-transfer')); + 'Allow file copy and paste', 'enable-file-transfer', padding, true)); } if (perms['keyboard'] != false) { if (perms['clipboard'] != false) { - displayMenu.add( - _createSwitchMenuEntry('Disable clipboard', 'disable-clipboard')); + displayMenu.add(_createSwitchMenuEntry( + 'Disable clipboard', 'disable-clipboard', padding, true)); } displayMenu.add(_createSwitchMenuEntry( - 'Lock after session end', 'lock-after-session-end')); + 'Lock after session end', 'lock-after-session-end', padding, true)); if (pi.platform == 'Windows') { displayMenu.add(MenuEntrySwitch2( - dismissOnClicked: true, - text: translate('Privacy mode'), - getter: () { - return PrivacyModeState.find(widget.id); - }, - setter: (bool v) async { - await bind.sessionToggleOption( - id: widget.id, value: 'privacy-mode'); - })); + switchType: SwitchType.scheckbox, + text: translate('Privacy mode'), + getter: () { + return PrivacyModeState.find(widget.id); + }, + setter: (bool v) async { + await bind.sessionToggleOption( + id: widget.id, value: 'privacy-mode'); + }, + padding: padding, + dismissOnClicked: true, + )); } } return displayMenu; @@ -772,34 +853,39 @@ class _RemoteMenubarState extends State { List> _getKeyboardMenu() { final keyboardMenu = [ MenuEntryRadios( - text: translate('Ratio'), - optionsGetter: () => [ - MenuEntryRadioOption( - text: translate('Legacy mode'), value: 'legacy'), - MenuEntryRadioOption(text: translate('Map mode'), value: 'map'), - ], - curOptionGetter: () async { - return await bind.sessionGetKeyboardName(id: widget.id); - }, - optionSetter: (String oldValue, String newValue) async { - await bind.sessionSetKeyboardMode( - id: widget.id, keyboardMode: newValue); - widget.ffi.canvasModel.updateViewStyle(); - }) + text: translate('Ratio'), + optionsGetter: () => [ + MenuEntryRadioOption(text: translate('Legacy mode'), value: 'legacy'), + MenuEntryRadioOption(text: translate('Map mode'), value: 'map'), + ], + curOptionGetter: () async { + return await bind.sessionGetKeyboardName(id: widget.id); + }, + optionSetter: (String oldValue, String newValue) async { + await bind.sessionSetKeyboardMode( + id: widget.id, keyboardMode: newValue); + widget.ffi.canvasModel.updateViewStyle(); + }, + ) ]; return keyboardMenu; } - MenuEntrySwitch _createSwitchMenuEntry(String text, String option) { + MenuEntrySwitch _createSwitchMenuEntry( + String text, String option, EdgeInsets? padding, bool dismissOnClicked) { return MenuEntrySwitch( - text: translate(text), - getter: () async { - return bind.sessionGetToggleOptionSync(id: widget.id, arg: option); - }, - setter: (bool v) async { - await bind.sessionToggleOption(id: widget.id, value: option); - }); + switchType: SwitchType.scheckbox, + text: translate(text), + getter: () async { + return bind.sessionGetToggleOptionSync(id: widget.id, arg: option); + }, + setter: (bool v) async { + await bind.sessionToggleOption(id: widget.id, value: option); + }, + padding: padding, + dismissOnClicked: dismissOnClicked, + ); } } From 4f9255539914518a8cff7711d61bd48b4d844f1e Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 23 Sep 2022 15:12:50 +0800 Subject: [PATCH 0566/2015] fix connect status colors --- flutter/lib/common/widgets/peer_card.dart | 27 +++++++++---------- flutter/lib/consts.dart | 2 ++ .../lib/desktop/pages/connection_page.dart | 7 ++++- .../desktop/pages/desktop_setting_page.dart | 3 ++- 4 files changed, 23 insertions(+), 16 deletions(-) diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index 2f6421880..f00a6864d 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -1,6 +1,7 @@ import 'package:contextmenu/contextmenu.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_hbb/consts.dart'; import 'package:get/get.dart'; import '../../common.dart'; @@ -156,13 +157,7 @@ class _PeerCardState extends State<_PeerCard> child: Column( children: [ Row(children: [ - Padding( - padding: const EdgeInsets.fromLTRB(0, 4, 4, 4), - child: CircleAvatar( - radius: 5, - backgroundColor: peer.online - ? Colors.green - : Colors.yellow)), + getOnline(4, peer.online), Expanded( child: Text( alias.isEmpty ? formatID(peer.id) : alias, @@ -256,13 +251,7 @@ class _PeerCardState extends State<_PeerCard> children: [ Expanded( child: Row(children: [ - Padding( - padding: const EdgeInsets.fromLTRB(0, 4, 8, 4), - child: CircleAvatar( - radius: 5, - backgroundColor: peer.online - ? Colors.green - : Colors.yellow)), + getOnline(4, peer.online), Expanded( child: Text( peer.alias.isEmpty ? formatID(peer.id) : peer.alias, @@ -991,3 +980,13 @@ void _rdpDialog(String id) async { ); }); } + +Widget getOnline(int rightMargin, bool online) { + return Tooltip( + message: translate(online ? 'Online' : 'Offline'), + waitDuration: const Duration(seconds: 1), + child: Padding( + padding: const EdgeInsets.fromLTRB(0, 4, 8, 4), + child: CircleAvatar( + radius: 3, backgroundColor: online ? Colors.green : kColorWarn))); +} diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index ff15173d3..ccacab5fb 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -11,6 +11,8 @@ const String kAppTypeDesktopPortForward = "port forward"; const String kTabLabelHomePage = "Home"; const String kTabLabelSettingPage = "Settings"; +const Color kColorWarn = Color.fromARGB(255, 245, 133, 59); + const int kMobileDefaultDisplayWidth = 720; const int kMobileDefaultDisplayHeight = 1280; diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 9ff7befd7..261b23f53 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -5,6 +5,7 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common/widgets/address_book.dart'; +import 'package:flutter_hbb/consts.dart'; import 'package:get/get.dart'; import 'package:url_launcher/url_launcher_string.dart'; @@ -297,7 +298,11 @@ class _ConnectionPageState extends State { width: 8, decoration: BoxDecoration( borderRadius: BorderRadius.circular(20), - color: svcStopped.value ? Colors.redAccent : Colors.green, + color: svcStopped.value || svcStatusCode.value == 0 + ? kColorWarn + : (svcStatusCode.value == 1 + ? Color.fromARGB(255, 50, 190, 166) + : Color.fromARGB(255, 224, 79, 95)), ), ).paddingSymmetric(horizontal: 12.0); if (svcStopped.value) { diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 0918fc59b..02c2b5354 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/server_model.dart'; @@ -474,7 +475,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { _OptionCheckBox(context, 'Deny remote access', 'stop-service', checkedIcon: const Icon( Icons.warning_amber_rounded, - color: Color.fromARGB(255, 255, 204, 0), + color: kColorWarn, ), enabled: enabled), Offstage( From e8587436d6e19b0493b697c38fc3d2becd76ae37 Mon Sep 17 00:00:00 2001 From: csf Date: Fri, 23 Sep 2022 16:31:50 +0800 Subject: [PATCH 0567/2015] refactor ThemeData --- flutter/lib/common.dart | 58 +++++-------------- flutter/lib/common/widgets/address_book.dart | 6 +- flutter/lib/common/widgets/overlay.dart | 16 ++--- flutter/lib/common/widgets/peer_card.dart | 32 ++++++---- flutter/lib/common/widgets/peer_tab_page.dart | 31 +++++----- .../lib/desktop/pages/connection_page.dart | 21 ++++--- .../lib/desktop/pages/desktop_home_page.dart | 41 +++++++------ .../desktop/pages/desktop_setting_page.dart | 8 ++- .../lib/desktop/pages/desktop_tab_page.dart | 2 +- .../lib/desktop/pages/file_manager_page.dart | 2 +- .../desktop/pages/file_manager_tab_page.dart | 2 +- .../lib/desktop/pages/port_forward_page.dart | 33 ++++++----- .../desktop/pages/port_forward_tab_page.dart | 2 +- flutter/lib/desktop/pages/remote_page.dart | 15 +++-- .../lib/desktop/pages/remote_tab_page.dart | 2 +- flutter/lib/desktop/pages/server_page.dart | 4 +- flutter/lib/desktop/widgets/popup_menu.dart | 19 +++--- flutter/lib/mobile/pages/chat_page.dart | 16 +++-- .../lib/mobile/pages/file_manager_page.dart | 12 ++-- flutter/lib/mobile/pages/home_page.dart | 6 +- flutter/lib/mobile/pages/remote_page.dart | 14 +++-- 21 files changed, 178 insertions(+), 164 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 8f8220db1..170ac597b 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -76,59 +76,22 @@ class IconFont { class ColorThemeExtension extends ThemeExtension { const ColorThemeExtension({ - required this.bg, - required this.grayBg, - required this.text, - required this.lightText, - required this.lighterText, - required this.placeholder, required this.border, }); - final Color? bg; - final Color? grayBg; - final Color? text; - final Color? lightText; - final Color? lighterText; - final Color? placeholder; final Color? border; static const light = ColorThemeExtension( - bg: Color(0xFFFFFFFF), - grayBg: Color(0xFFEEEEEE), - text: Color(0xFF222222), - lightText: Color(0xFF666666), - lighterText: Color(0xFF888888), - placeholder: Color(0xFFAAAAAA), border: Color(0xFFCCCCCC), ); static const dark = ColorThemeExtension( - bg: Color(0xFF252525), - grayBg: Color(0xFF141414), - text: Color(0xFFFFFFFF), - lightText: Color(0xFF999999), - lighterText: Color(0xFF777777), - placeholder: Color(0xFF555555), border: Color(0xFF555555), ); @override - ThemeExtension copyWith( - {Color? bg, - Color? grayBg, - Color? text, - Color? lightText, - Color? lighterText, - Color? placeholder, - Color? border}) { + ThemeExtension copyWith({Color? border}) { return ColorThemeExtension( - bg: bg ?? this.bg, - grayBg: grayBg ?? this.grayBg, - text: text ?? this.text, - lightText: lightText ?? this.lightText, - lighterText: lighterText ?? this.lighterText, - placeholder: placeholder ?? this.placeholder, border: border ?? this.border, ); } @@ -140,12 +103,6 @@ class ColorThemeExtension extends ThemeExtension { return this; } return ColorThemeExtension( - bg: Color.lerp(bg, other.bg, t), - grayBg: Color.lerp(grayBg, other.grayBg, t), - text: Color.lerp(text, other.text, t), - lightText: Color.lerp(lightText, other.lightText, t), - lighterText: Color.lerp(lighterText, other.lighterText, t), - placeholder: Color.lerp(placeholder, other.placeholder, t), border: Color.lerp(border, other.border, t), ); } @@ -170,6 +127,13 @@ class MyTheme { static ThemeData lightTheme = ThemeData( brightness: Brightness.light, + backgroundColor: Color(0xFFFFFFFF), + scaffoldBackgroundColor: Color(0xFFEEEEEE), + textTheme: const TextTheme( + titleLarge: TextStyle(fontSize: 19, color: Colors.black87), + bodySmall: + TextStyle(fontSize: 12, color: Colors.black54, height: 1.25)), + hintColor: Color(0xFFAAAAAA), primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, tabBarTheme: const TabBarTheme( @@ -191,6 +155,12 @@ class MyTheme { ); static ThemeData darkTheme = ThemeData( brightness: Brightness.dark, + backgroundColor: Color(0xFF252525), + scaffoldBackgroundColor: Color(0xFF141414), + textTheme: const TextTheme( + titleLarge: TextStyle(fontSize: 19), + bodySmall: TextStyle(fontSize: 12, height: 1.25)), + hintColor: Color(0xFF555555), primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, tabBarTheme: const TabBarTheme( diff --git a/flutter/lib/common/widgets/address_book.dart b/flutter/lib/common/widgets/address_book.dart index ab5f924dd..47a992bd3 100644 --- a/flutter/lib/common/widgets/address_book.dart +++ b/flutter/lib/common/widgets/address_book.dart @@ -115,7 +115,8 @@ class _AddressBookState extends State { Card( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), - side: const BorderSide(color: MyTheme.grayBg)), + side: BorderSide( + color: Theme.of(context).scaffoldBackgroundColor)), child: Container( width: 200, height: double.infinity, @@ -215,7 +216,8 @@ class _AddressBookState extends State { child: Text( tagName, style: TextStyle( - color: rxTags.contains(tagName) ? MyTheme.white : null), + color: + rxTags.contains(tagName) ? Colors.white : null), // TODO ), ), ), diff --git a/flutter/lib/common/widgets/overlay.dart b/flutter/lib/common/widgets/overlay.dart index 0e0a6ce2d..89dbe2ea6 100644 --- a/flutter/lib/common/widgets/overlay.dart +++ b/flutter/lib/common/widgets/overlay.dart @@ -26,7 +26,7 @@ class DraggableChatWindow extends StatelessWidget { position: position, width: width, height: height, - builder: (_, onPanUpdate) { + builder: (context, onPanUpdate) { return isIOS ? ChatPage(chatModel: chatModel) : Scaffold( @@ -35,16 +35,16 @@ class DraggableChatWindow extends StatelessWidget { onPanUpdate: onPanUpdate, appBar: isDesktop ? _buildDesktopAppBar() - : _buildMobileAppBar(), + : _buildMobileAppBar(context), ), body: ChatPage(chatModel: chatModel), ); }); } - Widget _buildMobileAppBar() { + Widget _buildMobileAppBar(BuildContext context) { return Container( - color: MyTheme.accent50, + color: Theme.of(context).colorScheme.primary, height: 50, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -169,17 +169,17 @@ class DraggableMobileActions extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ IconButton( - color: MyTheme.white, + color: Colors.white, onPressed: onBackPressed, splashRadius: 20, icon: const Icon(Icons.arrow_back)), IconButton( - color: MyTheme.white, + color: Colors.white, onPressed: onHomePressed, splashRadius: 20, icon: const Icon(Icons.home)), IconButton( - color: MyTheme.white, + color: Colors.white, onPressed: onRecentPressed, splashRadius: 20, icon: const Icon(Icons.more_horiz)), @@ -190,7 +190,7 @@ class DraggableMobileActions extends StatelessWidget { endIndent: 10, ), IconButton( - color: MyTheme.white, + color: Colors.white, onPressed: onHidePressed, splashRadius: 20, icon: const Icon(Icons.keyboard_arrow_down)), diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index 2f6421880..169e10428 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -108,7 +108,9 @@ class _PeerCardState extends State<_PeerCard> return MouseRegion( onEnter: (evt) { deco.value = BoxDecoration( - border: Border.all(color: MyTheme.button, width: _borderWidth), + border: Border.all( + color: Theme.of(context).colorScheme.secondary, + width: _borderWidth), borderRadius: peerCardUiType.value == PeerUiType.grid ? BorderRadius.circular(_cardRadis) : null); @@ -130,8 +132,9 @@ class _PeerCardState extends State<_PeerCard> Widget _buildPeerTile( BuildContext context, Peer peer, Rx deco) { - final greyStyle = - TextStyle(fontSize: 11, color: MyTheme.color(context).lighterText); + final greyStyle = TextStyle( + fontSize: 11, + color: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6)); final alias = bind.mainGetPeerOptionSync(id: peer.id, key: 'alias'); return Obx( () => Container( @@ -148,7 +151,8 @@ class _PeerCardState extends State<_PeerCard> ), Expanded( child: Container( - decoration: BoxDecoration(color: MyTheme.color(context).bg), + decoration: + BoxDecoration(color: Theme.of(context).backgroundColor), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -250,7 +254,7 @@ class _PeerCardState extends State<_PeerCard> ), ), Container( - color: MyTheme.color(context).bg, + color: Theme.of(context).backgroundColor, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -294,13 +298,21 @@ class _PeerCardState extends State<_PeerCard> child: CircleAvatar( radius: 14, backgroundColor: _iconMoreHover.value - ? MyTheme.color(context).grayBg! - : MyTheme.color(context).bg!, + ? Theme.of(context).scaffoldBackgroundColor + : Theme.of(context).backgroundColor, + // ? Theme.of(context).scaffoldBackgroundColor! + // : Theme.of(context).backgroundColor!, child: Icon(Icons.more_vert, size: 18, color: _iconMoreHover.value - ? MyTheme.color(context).text - : MyTheme.color(context).lightText)))); + ? Theme.of(context).textTheme.titleLarge?.color + : Theme.of(context) + .textTheme + .titleLarge + ?.color + ?.withOpacity(0.5))))); + // ? MyTheme.color(context).text + // : MyTheme.color(context).lightText)))); /// Show the peer menu and handle user's choice. /// User might remove the peer or send a file to the peer. @@ -865,7 +877,7 @@ class AddressBookPeerCard extends BasePeerCard { child: Text( tagName, style: TextStyle( - color: rxTags.contains(tagName) ? MyTheme.white : null), + color: rxTags.contains(tagName) ? Colors.white : null), ), ), ), diff --git a/flutter/lib/common/widgets/peer_tab_page.dart b/flutter/lib/common/widgets/peer_tab_page.dart index 3ed3dc11d..089e6acb5 100644 --- a/flutter/lib/common/widgets/peer_tab_page.dart +++ b/flutter/lib/common/widgets/peer_tab_page.dart @@ -101,6 +101,7 @@ class _PeerTabPageState extends State } Widget _createTabBar(BuildContext context) { + final textColor = Theme.of(context).textTheme.titleLarge?.color; return ListView( scrollDirection: Axis.horizontal, shrinkWrap: true, @@ -111,7 +112,7 @@ class _PeerTabPageState extends State padding: const EdgeInsets.symmetric(horizontal: 8), decoration: BoxDecoration( color: _tabIndex.value == t.key - ? MyTheme.color(context).bg + ? Theme.of(context).backgroundColor : null, borderRadius: BorderRadius.circular(isDesktop ? 2 : 6), ), @@ -123,9 +124,9 @@ class _PeerTabPageState extends State style: TextStyle( height: 1, fontSize: 14, - color: _tabIndex.value == t.key - ? MyTheme.color(context).text - : MyTheme.color(context).lightText), + color: + _tabIndex.value == t.key ? textColor : textColor + ?..withOpacity(0.5)), ), )), onTap: () async => await _handleTabSelection(t.key), @@ -147,7 +148,8 @@ class _PeerTabPageState extends State } Widget _createPeerViewTypeSwitch(BuildContext context) { - final activeDeco = BoxDecoration(color: MyTheme.color(context).bg); + final textColor = Theme.of(context).textTheme.titleLarge?.color; + final activeDeco = BoxDecoration(color: Theme.of(context).backgroundColor); return Row( children: [PeerUiType.grid, PeerUiType.list] .map((type) => Obx( @@ -166,9 +168,9 @@ class _PeerTabPageState extends State ? Icons.grid_view_rounded : Icons.list, size: 18, - color: peerCardUiType.value == type - ? MyTheme.color(context).text - : MyTheme.color(context).lightText, + color: + peerCardUiType.value == type ? textColor : textColor + ?..withOpacity(0.5), )), ), )) @@ -212,7 +214,7 @@ class _PeerSearchBarState extends State { return Container( width: 120, decoration: BoxDecoration( - color: MyTheme.color(context).bg, + color: Theme.of(context).backgroundColor, borderRadius: BorderRadius.circular(6), ), child: Obx(() => Row( @@ -222,7 +224,7 @@ class _PeerSearchBarState extends State { children: [ Icon( Icons.search_rounded, - color: MyTheme.color(context).placeholder, + color: Theme.of(context).hintColor, ).marginSymmetric(horizontal: 4), Expanded( child: TextField( @@ -234,7 +236,11 @@ class _PeerSearchBarState extends State { focusNode: focusNode, textAlign: TextAlign.start, maxLines: 1, - cursorColor: MyTheme.color(context).lightText, + cursorColor: Theme.of(context) + .textTheme + .titleLarge + ?.color + ?.withOpacity(0.5), cursorHeight: 18, cursorWidth: 1, style: const TextStyle(fontSize: 14), @@ -244,8 +250,7 @@ class _PeerSearchBarState extends State { hintText: focused.value ? null : translate("Search ID"), hintStyle: TextStyle( - fontSize: 14, - color: MyTheme.color(context).placeholder), + fontSize: 14, color: Theme.of(context).hintColor), border: InputBorder.none, isDense: true, ), diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 9ff7befd7..cdd4cc286 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -121,7 +121,7 @@ class _ConnectionPageState extends State { width: 320 + 20 * 2, padding: const EdgeInsets.fromLTRB(20, 24, 20, 22), decoration: BoxDecoration( - color: MyTheme.color(context).bg, + color: Theme.of(context).backgroundColor, borderRadius: const BorderRadius.all(Radius.circular(13)), ), child: Ink( @@ -131,7 +131,10 @@ class _ConnectionPageState extends State { children: [ Text( translate('Control Remote Desktop'), - style: const TextStyle(fontSize: 19, height: 1), + style: Theme.of(context) + .textTheme + .titleLarge + ?.merge(TextStyle(height: 1)), ), ], ).marginOnly(bottom: 15), @@ -150,13 +153,12 @@ class _ConnectionPageState extends State { height: 1, ), maxLines: 1, - cursorColor: MyTheme.color(context).text!, + cursorColor: + Theme.of(context).textTheme.titleLarge?.color, decoration: InputDecoration( hintText: inputFocused.value ? null : translate('Enter Remote ID'), - hintStyle: TextStyle( - color: MyTheme.color(context).placeholder), border: OutlineInputBorder( borderRadius: BorderRadius.zero, borderSide: BorderSide( @@ -219,8 +221,11 @@ class _ConnectionPageState extends State { style: TextStyle( fontSize: 12, color: ftPressed.value - ? MyTheme.color(context).bg - : MyTheme.color(context).text), + ? Theme.of(context).backgroundColor + : Theme.of(context) + .textTheme + .titleLarge + ?.color), ).marginSymmetric(horizontal: 12), ), )), @@ -260,7 +265,7 @@ class _ConnectionPageState extends State { ), style: TextStyle( fontSize: 12, - color: MyTheme.color(context).bg), + color: Theme.of(context).backgroundColor), ), ).marginSymmetric(horizontal: 12), )), diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 833a914cd..6c4af5cf8 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -68,7 +68,7 @@ class _DesktopHomePageState extends State value: gFFI.serverModel, child: Container( width: 200, - color: MyTheme.color(context).bg, + color: Theme.of(context).backgroundColor, child: Column( children: [ buildTip(context), @@ -82,7 +82,7 @@ class _DesktopHomePageState extends State buildRightPane(BuildContext context) { return Container( - color: MyTheme.color(context).grayBg, + color: Theme.of(context).scaffoldBackgroundColor, child: ConnectionPage(), ); } @@ -116,7 +116,11 @@ class _DesktopHomePageState extends State translate("ID"), style: TextStyle( fontSize: 14, - color: MyTheme.color(context).lightText), + color: Theme.of(context) + .textTheme + .titleLarge + ?.color + ?.withOpacity(0.5)), ).marginOnly(top: 5), buildPopupMenu(context) ], @@ -152,6 +156,7 @@ class _DesktopHomePageState extends State } Widget buildPopupMenu(BuildContext context) { + final textColor = Theme.of(context).textTheme.titleLarge?.color; RxBool hover = false.obs; return InkWell( onTap: () async {}, @@ -159,14 +164,12 @@ class _DesktopHomePageState extends State () => CircleAvatar( radius: 15, backgroundColor: hover.value - ? MyTheme.color(context).grayBg! - : MyTheme.color(context).bg!, + ? Theme.of(context).scaffoldBackgroundColor + : Theme.of(context).backgroundColor, child: Icon( Icons.more_vert_outlined, size: 20, - color: hover.value - ? MyTheme.color(context).text - : MyTheme.color(context).lightText, + color: hover.value ? textColor : textColor?.withOpacity(0.5), ), ), ), @@ -178,6 +181,7 @@ class _DesktopHomePageState extends State final model = gFFI.serverModel; RxBool refreshHover = false.obs; RxBool editHover = false.obs; + final textColor = Theme.of(context).textTheme.titleLarge?.color; return Container( margin: EdgeInsets.only(left: 20.0, right: 16, top: 13, bottom: 13), child: Row( @@ -198,7 +202,7 @@ class _DesktopHomePageState extends State Text( translate("Password"), style: TextStyle( - fontSize: 14, color: MyTheme.color(context).lightText), + fontSize: 14, color: textColor?.withOpacity(0.5)), ), Row( children: [ @@ -228,8 +232,8 @@ class _DesktopHomePageState extends State () => Icon( Icons.refresh, color: refreshHover.value - ? MyTheme.color(context).text - : Color(0xFFDDDDDD), + ? textColor + : Color(0xFFDDDDDD), // TODO size: 22, ).marginOnly(right: 8, bottom: 2), ), @@ -241,8 +245,8 @@ class _DesktopHomePageState extends State () => Icon( Icons.edit, color: editHover.value - ? MyTheme.color(context).text - : Color(0xFFDDDDDD), + ? textColor + : Color(0xFFDDDDDD), // TODO size: 22, ).marginOnly(right: 8, bottom: 2), ), @@ -270,7 +274,11 @@ class _DesktopHomePageState extends State children: [ Text( translate("Your Desktop"), - style: TextStyle(fontWeight: FontWeight.normal, fontSize: 19), + style: Theme.of(context).textTheme.titleLarge, + // style: TextStyle( + // // color: MyTheme.color(context).text, + // fontWeight: FontWeight.normal, + // fontSize: 19), ), SizedBox( height: 10.0, @@ -278,10 +286,7 @@ class _DesktopHomePageState extends State Text( translate("desk_tip"), overflow: TextOverflow.clip, - style: TextStyle( - fontSize: 12, - color: MyTheme.color(context).lighterText, - height: 1.25), + style: Theme.of(context).textTheme.bodySmall, ) ], ), diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 0918fc59b..ec386a8e2 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -68,7 +68,7 @@ class _DesktopSettingPageState extends State Widget build(BuildContext context) { super.build(context); return Scaffold( - backgroundColor: MyTheme.color(context).bg, + backgroundColor: Theme.of(context).backgroundColor, body: Row( children: [ SizedBox( @@ -83,7 +83,7 @@ class _DesktopSettingPageState extends State const VerticalDivider(thickness: 1, width: 1), Expanded( child: Container( - color: MyTheme.color(context).grayBg, + color: Theme.of(context).scaffoldBackgroundColor, child: DesktopScrollWrapper( scrollController: controller, child: PageView( @@ -802,7 +802,9 @@ Widget _Card({required String title, required List children}) { } Color? _disabledTextColor(BuildContext context, bool enabled) { - return enabled ? null : MyTheme.color(context).lighterText; + return enabled + ? null + : Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6); } // ignore: non_constant_identifier_names diff --git a/flutter/lib/desktop/pages/desktop_tab_page.dart b/flutter/lib/desktop/pages/desktop_tab_page.dart index 2f24ddbde..cd79e127b 100644 --- a/flutter/lib/desktop/pages/desktop_tab_page.dart +++ b/flutter/lib/desktop/pages/desktop_tab_page.dart @@ -42,7 +42,7 @@ class _DesktopTabPageState extends State { OverlayEntry(builder: (context) { gFFI.dialogManager.setOverlayState(Overlay.of(context)); return Scaffold( - backgroundColor: MyTheme.color(context).bg, + backgroundColor: Theme.of(context).backgroundColor, body: DesktopTab( controller: tabController, tail: ActionIcon( diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index ced61e8d9..f44088d6e 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -104,7 +104,7 @@ class _FileManagerPageState extends State return false; }, child: Scaffold( - backgroundColor: MyTheme.color(context).bg, + backgroundColor: Theme.of(context).backgroundColor, body: Row( children: [ Flexible(flex: 3, child: body(isLocal: true)), diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index 9b8060bb7..24a36eddb 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -72,7 +72,7 @@ class _FileManagerTabPageState extends State { decoration: BoxDecoration( border: Border.all(color: MyTheme.color(context).border!)), child: Scaffold( - backgroundColor: MyTheme.color(context).bg, + backgroundColor: Theme.of(context).backgroundColor, body: DesktopTab( controller: tabController, onWindowCloseButton: handleWindowCloseButton, diff --git a/flutter/lib/desktop/pages/port_forward_page.dart b/flutter/lib/desktop/pages/port_forward_page.dart index 28aa8d3cf..ccbf1805e 100644 --- a/flutter/lib/desktop/pages/port_forward_page.dart +++ b/flutter/lib/desktop/pages/port_forward_page.dart @@ -70,7 +70,7 @@ class _PortForwardPageState extends State Widget build(BuildContext context) { super.build(context); return Scaffold( - backgroundColor: MyTheme.color(context).grayBg, + backgroundColor: Theme.of(context).scaffoldBackgroundColor, body: FutureBuilder(future: () async { if (!widget.isRDP) { refreshTunnelConfig(); @@ -80,7 +80,8 @@ class _PortForwardPageState extends State return Container( decoration: BoxDecoration( border: Border.all( - width: 20, color: MyTheme.color(context).grayBg!)), + width: 20, + color: Theme.of(context).scaffoldBackgroundColor)), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ @@ -88,7 +89,7 @@ class _PortForwardPageState extends State Flexible( child: Container( decoration: BoxDecoration( - color: MyTheme.color(context).bg, + color: Theme.of(context).backgroundColor, border: Border.all(width: 1, color: MyTheme.border)), child: widget.isRDP ? buildRdp(context) : buildTunnel(context), @@ -131,7 +132,7 @@ class _PortForwardPageState extends State return Theme( data: Theme.of(context) - .copyWith(backgroundColor: MyTheme.color(context).bg), + .copyWith(backgroundColor: Theme.of(context).backgroundColor), child: Obx(() => ListView.builder( controller: ScrollController(), itemCount: pfs.length + 2, @@ -139,7 +140,7 @@ class _PortForwardPageState extends State if (index == 0) { return Container( height: 25, - color: MyTheme.color(context).grayBg, + color: Theme.of(context).scaffoldBackgroundColor, child: Row(children: [ text('Local Port'), const SizedBox(width: _kColumn1Width), @@ -166,7 +167,7 @@ class _PortForwardPageState extends State return Container( height: _kRowHeight, - decoration: BoxDecoration(color: MyTheme.color(context).bg), + decoration: BoxDecoration(color: Theme.of(context).backgroundColor), child: Row(children: [ buildTunnelInputCell(context, controller: localPortController, @@ -216,11 +217,12 @@ class _PortForwardPageState extends State {required TextEditingController controller, List? inputFormatters, String? hint}) { + final textColor = Theme.of(context).textTheme.titleLarge?.color; return Expanded( child: TextField( controller: controller, inputFormatters: inputFormatters, - cursorColor: MyTheme.color(context).text, + cursorColor: textColor, cursorHeight: 20, cursorWidth: 1, decoration: InputDecoration( @@ -228,12 +230,12 @@ class _PortForwardPageState extends State borderSide: BorderSide(color: MyTheme.color(context).border!)), focusedBorder: OutlineInputBorder( borderSide: BorderSide(color: MyTheme.color(context).border!)), - fillColor: MyTheme.color(context).bg, + fillColor: Theme.of(context).backgroundColor, contentPadding: const EdgeInsets.all(10), hintText: hint, - hintStyle: TextStyle( - color: MyTheme.color(context).placeholder, fontSize: 16)), - style: TextStyle(color: MyTheme.color(context).text, fontSize: 16), + hintStyle: + TextStyle(color: Theme.of(context).hintColor, fontSize: 16)), + style: TextStyle(color: textColor, fontSize: 16), ).marginAll(10), ); } @@ -250,7 +252,7 @@ class _PortForwardPageState extends State ? MyTheme.currentThemeMode() == ThemeMode.dark ? const Color(0xFF202020) : const Color(0xFFF4F5F6) - : MyTheme.color(context).bg), + : Theme.of(context).backgroundColor), child: Row(children: [ text(pf.localPort.toString()), const SizedBox(width: _kColumn1Width), @@ -292,7 +294,7 @@ class _PortForwardPageState extends State ).marginOnly(left: _kTextLeftMargin)); return Theme( data: Theme.of(context) - .copyWith(backgroundColor: MyTheme.color(context).bg), + .copyWith(backgroundColor: Theme.of(context).backgroundColor), child: ListView.builder( controller: ScrollController(), itemCount: 2, @@ -300,7 +302,7 @@ class _PortForwardPageState extends State if (index == 0) { return Container( height: 25, - color: MyTheme.color(context).grayBg, + color: Theme.of(context).scaffoldBackgroundColor, child: Row(children: [ text1('Local Port'), const SizedBox(width: _kColumn1Width), @@ -311,7 +313,8 @@ class _PortForwardPageState extends State } else { return Container( height: _kRowHeight, - decoration: BoxDecoration(color: MyTheme.color(context).bg), + decoration: + BoxDecoration(color: Theme.of(context).backgroundColor), child: Row(children: [ Expanded( child: Align( diff --git a/flutter/lib/desktop/pages/port_forward_tab_page.dart b/flutter/lib/desktop/pages/port_forward_tab_page.dart index d4f17aaef..6c6865718 100644 --- a/flutter/lib/desktop/pages/port_forward_tab_page.dart +++ b/flutter/lib/desktop/pages/port_forward_tab_page.dart @@ -80,7 +80,7 @@ class _PortForwardTabPageState extends State { decoration: BoxDecoration( border: Border.all(color: MyTheme.color(context).border!)), child: Scaffold( - backgroundColor: MyTheme.color(context).bg, + backgroundColor: Theme.of(context).backgroundColor, body: DesktopTab( controller: tabController, onWindowCloseButton: () async { diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index dad286ee7..74ff9b7af 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -11,7 +11,6 @@ import 'package:provider/provider.dart'; import 'package:wakelock/wakelock.dart'; import 'package:flutter_custom_cursor/flutter_custom_cursor.dart'; -import '../../consts.dart'; import '../widgets/remote_menubar.dart'; import '../../common.dart'; import '../../mobile/widgets/dialog.dart'; @@ -45,7 +44,6 @@ class _RemotePageState extends State late RxBool _keyboardEnabled; final FocusNode _rawKeyFocusNode = FocusNode(); - var _isPhysicalMouse = false; var _imageFocused = false; Function(bool)? _onEnterOrLeaveImage4Menubar; @@ -138,7 +136,7 @@ class _RemotePageState extends State Widget buildBody(BuildContext context) { return Scaffold( - backgroundColor: MyTheme.color(context).bg, + backgroundColor: Theme.of(context).backgroundColor, body: Overlay( initialEntries: [ OverlayEntry(builder: (context) { @@ -443,6 +441,7 @@ class ImagePainter extends CustomPainter { } class QualityMonitor extends StatelessWidget { + static const textStyle = TextStyle(color: MyTheme.grayBg); final QualityMonitorModel qualityMonitorModel; QualityMonitor(this.qualityMonitorModel); @@ -462,23 +461,23 @@ class QualityMonitor extends StatelessWidget { children: [ Text( "Speed: ${qualityMonitorModel.data.speed ?? ''}", - style: const TextStyle(color: MyTheme.grayBg), + style: textStyle, ), Text( "FPS: ${qualityMonitorModel.data.fps ?? ''}", - style: const TextStyle(color: MyTheme.grayBg), + style: textStyle, ), Text( "Delay: ${qualityMonitorModel.data.delay ?? ''} ms", - style: const TextStyle(color: MyTheme.grayBg), + style: textStyle, ), Text( "Target Bitrate: ${qualityMonitorModel.data.targetBitrate ?? ''}kb", - style: const TextStyle(color: MyTheme.grayBg), + style: textStyle, ), Text( "Codec: ${qualityMonitorModel.data.codecFormat ?? ''}", - style: const TextStyle(color: MyTheme.grayBg), + style: textStyle, ), ], ), diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index b086a2e35..e84b574fd 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -91,7 +91,7 @@ class _ConnectionTabPageState extends State { decoration: BoxDecoration( border: Border.all(color: MyTheme.color(context).border!)), child: Scaffold( - backgroundColor: MyTheme.color(context).bg, + backgroundColor: Theme.of(context).backgroundColor, body: DesktopTab( controller: tabController, showTabBar: fullscreen.isFalse, diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index 9ca446dcb..68d5c3b33 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -69,7 +69,7 @@ class _DesktopServerPageState extends State OverlayEntry(builder: (context) { gFFI.dialogManager.setOverlayState(Overlay.of(context)); return Scaffold( - backgroundColor: MyTheme.color(context).bg, + backgroundColor: Theme.of(context).backgroundColor, body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.start, @@ -145,7 +145,7 @@ class ConnectionManagerState extends State { windowManager.startDragging(); }, child: Container( - color: MyTheme.color(context).bg, + color: Theme.of(context).backgroundColor, ), ), ), diff --git a/flutter/lib/desktop/widgets/popup_menu.dart b/flutter/lib/desktop/widgets/popup_menu.dart index 84fd69b0e..71d1ec417 100644 --- a/flutter/lib/desktop/widgets/popup_menu.dart +++ b/flutter/lib/desktop/widgets/popup_menu.dart @@ -1,7 +1,6 @@ import 'dart:core'; import 'package:flutter/material.dart'; -import 'package:flutter_hbb/common.dart'; import 'package:get/get.dart'; import './material_mod_popup_menu.dart' as mod_menu; @@ -201,7 +200,7 @@ class MenuEntryRadios extends MenuEntryBase { Text( opt.text, style: TextStyle( - color: MyTheme.color(context).text, + color: Theme.of(context).textTheme.titleLarge?.color, fontSize: MenuConfig.fontSize, fontWeight: FontWeight.normal), ), @@ -296,7 +295,7 @@ class MenuEntrySubRadios extends MenuEntryBase { Text( opt.text, style: TextStyle( - color: MyTheme.color(context).text, + color: Theme.of(context).textTheme.titleLarge?.color, fontSize: MenuConfig.fontSize, fontWeight: FontWeight.normal), ), @@ -345,7 +344,7 @@ class MenuEntrySubRadios extends MenuEntryBase { Text( text, style: TextStyle( - color: MyTheme.color(context).text, + color: Theme.of(context).textTheme.titleLarge?.color, fontSize: MenuConfig.fontSize, fontWeight: FontWeight.normal), ), @@ -392,8 +391,8 @@ abstract class MenuEntrySwitchBase extends MenuEntryBase { @override List> build( BuildContext context, MenuConfig conf) { - textStyle ??= const TextStyle( - color: Colors.black, + textStyle ??= TextStyle( + color: Theme.of(context).textTheme.titleLarge?.color, fontSize: MenuConfig.fontSize, fontWeight: FontWeight.normal) .obs; @@ -560,7 +559,9 @@ class MenuEntrySubMenu extends MenuEntryBase { Obx(() => Text( text, style: TextStyle( - color: super.enabled!.value ? Colors.black : Colors.grey, + color: super.enabled!.value + ? Theme.of(context).textTheme.titleLarge?.color + : Colors.grey, fontSize: MenuConfig.fontSize, fontWeight: FontWeight.normal), )), @@ -595,8 +596,8 @@ class MenuEntryButton extends MenuEntryBase { ); Widget _buildChild(BuildContext context, MenuConfig conf) { - const enabledStyle = TextStyle( - color: Colors.black, + final enabledStyle = TextStyle( + color: Theme.of(context).textTheme.titleLarge?.color, fontSize: MenuConfig.fontSize, fontWeight: FontWeight.normal); const disabledStyle = TextStyle( diff --git a/flutter/lib/mobile/pages/chat_page.dart b/flutter/lib/mobile/pages/chat_page.dart index 2151f17be..8ac5ce313 100644 --- a/flutter/lib/mobile/pages/chat_page.dart +++ b/flutter/lib/mobile/pages/chat_page.dart @@ -45,7 +45,7 @@ class ChatPage extends StatelessWidget implements PageShape { return ChangeNotifierProvider.value( value: chatModel, child: Container( - color: MyTheme.color(context).grayBg, + color: Theme.of(context).scaffoldBackgroundColor, child: Consumer(builder: (context, chatModel, child) { final currentUser = chatModel.currentUser; return Stack( @@ -62,11 +62,17 @@ class ChatPage extends StatelessWidget implements PageShape { inputOptions: InputOptions( sendOnEnter: true, inputDecoration: defaultInputDecoration( - fillColor: MyTheme.color(context).bg), + fillColor: Theme.of(context).backgroundColor), sendButtonBuilder: defaultSendButton( - color: MyTheme.color(context).text!), - inputTextStyle: - TextStyle(color: MyTheme.color(context).text)), + color: Theme.of(context) + .textTheme + .titleLarge! + .color!), + inputTextStyle: TextStyle( + color: Theme.of(context) + .textTheme + .titleLarge + ?.color)), messageOptions: MessageOptions( showOtherUsersAvatar: false, showTime: true, diff --git a/flutter/lib/mobile/pages/file_manager_page.dart b/flutter/lib/mobile/pages/file_manager_page.dart index dd1cbb83f..13059d188 100644 --- a/flutter/lib/mobile/pages/file_manager_page.dart +++ b/flutter/lib/mobile/pages/file_manager_page.dart @@ -58,7 +58,7 @@ class _FileManagerPageState extends State { return false; }, child: Scaffold( - backgroundColor: MyTheme.grayBg, + // backgroundColor: MyTheme.grayBg, appBar: AppBar( leading: Row(children: [ IconButton( @@ -69,7 +69,7 @@ class _FileManagerPageState extends State { title: ToggleSwitch( initialLabelIndex: model.isLocal ? 0 : 1, activeBgColor: [MyTheme.idColor], - inactiveBgColor: MyTheme.grayBg, + // inactiveBgColor: MyTheme.grayBg, inactiveFgColor: Colors.black54, totalSwitches: 2, minWidth: 100, @@ -465,6 +465,9 @@ class _FileManagerPageState extends State { ); case JobState.none: break; + case JobState.paused: + // TODO: Handle this case. + break; } return null; } @@ -530,8 +533,7 @@ class BottomSheetBody extends StatelessWidget { children: [ Text(title, style: TextStyle(fontSize: 18)), Text(text, - style: TextStyle( - fontSize: 14, color: MyTheme.grayBg)) + style: TextStyle(fontSize: 14)) // TODO color ], ) ], @@ -548,7 +550,7 @@ class BottomSheetBody extends StatelessWidget { )); }, onClosing: () {}, - backgroundColor: MyTheme.grayBg, + // backgroundColor: MyTheme.grayBg, enableDrag: false, ); } diff --git a/flutter/lib/mobile/pages/home_page.dart b/flutter/lib/mobile/pages/home_page.dart index 05a6d6b51..31240b895 100644 --- a/flutter/lib/mobile/pages/home_page.dart +++ b/flutter/lib/mobile/pages/home_page.dart @@ -59,7 +59,7 @@ class _HomePageState extends State { return false; }, child: Scaffold( - backgroundColor: MyTheme.grayBg, + // backgroundColor: MyTheme.grayBg, appBar: AppBar( centerTitle: true, title: Text("RustDesk"), @@ -73,7 +73,7 @@ class _HomePageState extends State { .toList(), currentIndex: _selectedIndex, type: BottomNavigationBarType.fixed, - selectedItemColor: MyTheme.accent, + selectedItemColor: MyTheme.accent, // unselectedItemColor: MyTheme.darkGray, onTap: (index) => setState(() { // close chat overlay when go chat page @@ -95,7 +95,7 @@ class WebHomePage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: MyTheme.grayBg, + // backgroundColor: MyTheme.grayBg, appBar: AppBar( centerTitle: true, title: Text("RustDesk" + (isWeb ? " (Beta) " : "")), diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 94f584109..e16035175 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -752,7 +752,7 @@ class _RemotePageState extends State { void changeTouchMode() { setState(() => _showEdit = false); showModalBottomSheet( - backgroundColor: MyTheme.grayBg, + // backgroundColor: MyTheme.grayBg, isScrollControlled: true, context: context, shape: const RoundedRectangleBorder( @@ -968,7 +968,9 @@ class ImagePainter extends CustomPainter { } } +// TODO global widget class QualityMonitor extends StatelessWidget { + static final textColor = Colors.grey.shade200; @override Widget build(BuildContext context) => ChangeNotifierProvider.value( value: gFFI.qualityMonitorModel, @@ -985,23 +987,23 @@ class QualityMonitor extends StatelessWidget { children: [ Text( "Speed: ${qualityMonitorModel.data.speed ?? ''}", - style: TextStyle(color: MyTheme.grayBg), + style: TextStyle(color: textColor), ), Text( "FPS: ${qualityMonitorModel.data.fps ?? ''}", - style: TextStyle(color: MyTheme.grayBg), + style: TextStyle(color: textColor), ), Text( "Delay: ${qualityMonitorModel.data.delay ?? ''} ms", - style: TextStyle(color: MyTheme.grayBg), + style: TextStyle(color: textColor), ), Text( "Target Bitrate: ${qualityMonitorModel.data.targetBitrate ?? ''}kb", - style: TextStyle(color: MyTheme.grayBg), + style: TextStyle(color: textColor), ), Text( "Codec: ${qualityMonitorModel.data.codecFormat ?? ''}", - style: TextStyle(color: MyTheme.grayBg), + style: TextStyle(color: textColor), ), ], ), From 02adf7104dee54766863c368e21fa5218b223edf Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 23 Sep 2022 16:37:17 +0800 Subject: [PATCH 0568/2015] feat: add shadow on linux --- flutter/linux/my_application.cc | 7 ++++++- flutter/pubspec.yaml | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/flutter/linux/my_application.cc b/flutter/linux/my_application.cc index 6d101687b..05d420342 100644 --- a/flutter/linux/my_application.cc +++ b/flutter/linux/my_application.cc @@ -59,7 +59,12 @@ static void my_application_activate(GApplication* application) { FlView* view = fl_view_new(project); gtk_widget_show(GTK_WIDGET(view)); - gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + auto border_frame = gtk_frame_new(nullptr); + gtk_frame_set_shadow_type(GTK_FRAME(border_frame), GTK_SHADOW_ETCHED_IN); + gtk_container_add(GTK_CONTAINER(border_frame), GTK_WIDGET(view)); + gtk_widget_show(GTK_WIDGET(border_frame)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(border_frame)); fl_register_plugins(FL_PLUGIN_REGISTRY(view)); diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 05c711dcf..c87bb02fb 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -64,7 +64,7 @@ dependencies: desktop_multi_window: git: url: https://github.com/Kingtous/rustdesk_desktop_multi_window - ref: fee851fa43116e0b91c39acd0ec37063dc6015f8 + ref: 1818097611168f6148317f4c527aa45ff29d5850 freezed_annotation: ^2.0.3 tray_manager: git: From cf31ec3a53d3946e9ee630c3497f73075aeb3b00 Mon Sep 17 00:00:00 2001 From: csf Date: Fri, 23 Sep 2022 16:56:28 +0800 Subject: [PATCH 0569/2015] fix mobile build --- src/ui_interface.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/ui_interface.rs b/src/ui_interface.rs index dc3a02c7a..3d076255f 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -338,10 +338,13 @@ pub fn set_socks(proxy: String, username: String, password: String) { .ok(); } -#[cfg(not(any(target_os = "android", target_os = "ios")))] #[inline] pub fn is_installed() -> bool { - crate::platform::is_installed() + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + return crate::platform::is_installed(); + } + false } #[inline] From d2d531516aa2bd5813a8518472f55dc5bf90a017 Mon Sep 17 00:00:00 2001 From: csf Date: Fri, 23 Sep 2022 17:16:25 +0800 Subject: [PATCH 0570/2015] opt mobile dark theme --- flutter/lib/common.dart | 2 +- flutter/lib/common/widgets/peer_tab_page.dart | 8 +++---- flutter/lib/mobile/pages/connection_page.dart | 4 ++-- flutter/lib/mobile/pages/server_page.dart | 21 +++++++++++-------- 4 files changed, 19 insertions(+), 16 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 170ac597b..dcdc191ff 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -160,7 +160,7 @@ class MyTheme { textTheme: const TextTheme( titleLarge: TextStyle(fontSize: 19), bodySmall: TextStyle(fontSize: 12, height: 1.25)), - hintColor: Color(0xFF555555), + cardColor: Color(0xFF252525), primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, tabBarTheme: const TabBarTheme( diff --git a/flutter/lib/common/widgets/peer_tab_page.dart b/flutter/lib/common/widgets/peer_tab_page.dart index 089e6acb5..835849ae4 100644 --- a/flutter/lib/common/widgets/peer_tab_page.dart +++ b/flutter/lib/common/widgets/peer_tab_page.dart @@ -201,9 +201,9 @@ class _PeerSearchBarState extends State { drawer = true; }); }, - icon: const Icon( + icon: Icon( Icons.search_rounded, - color: MyTheme.dark, + color: Theme.of(context).hintColor, )); } @@ -267,9 +267,9 @@ class _PeerSearchBarState extends State { drawer = false; }); }, - icon: const Icon( + icon: Icon( Icons.close, - color: MyTheme.dark, + color: Theme.of(context).hintColor, )), ], ), diff --git a/flutter/lib/mobile/pages/connection_page.dart b/flutter/lib/mobile/pages/connection_page.dart index 6156223b5..af84a8a47 100644 --- a/flutter/lib/mobile/pages/connection_page.dart +++ b/flutter/lib/mobile/pages/connection_page.dart @@ -128,8 +128,8 @@ class _ConnectionPageState extends State { child: Padding( padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 2), child: Ink( - decoration: const BoxDecoration( - color: MyTheme.white, + decoration: BoxDecoration( + color: Theme.of(context).cardColor, borderRadius: BorderRadius.all(Radius.circular(13)), ), child: Row( diff --git a/flutter/lib/mobile/pages/server_page.dart b/flutter/lib/mobile/pages/server_page.dart index 4fdd00ede..bed043b25 100644 --- a/flutter/lib/mobile/pages/server_page.dart +++ b/flutter/lib/mobile/pages/server_page.dart @@ -170,7 +170,7 @@ class ServerInfo extends StatelessWidget { icon: const Icon(Icons.perm_identity), labelText: translate("ID"), labelStyle: const TextStyle( - fontWeight: FontWeight.bold, color: MyTheme.accent50), + fontWeight: FontWeight.bold, color: MyTheme.accent80), ), onSaved: (String? value) {}, ), @@ -185,7 +185,7 @@ class ServerInfo extends StatelessWidget { icon: const Icon(Icons.lock), labelText: translate("Password"), labelStyle: const TextStyle( - fontWeight: FontWeight.bold, color: MyTheme.accent50), + fontWeight: FontWeight.bold, color: MyTheme.accent80), suffix: isPermanent ? null : IconButton( @@ -213,7 +213,7 @@ class ServerInfo extends StatelessWidget { fontFamily: 'WorkSans', fontWeight: FontWeight.bold, fontSize: 18, - color: MyTheme.accent80, + color: MyTheme.accent, ), )) ], @@ -304,7 +304,8 @@ class _PermissionCheckerState extends State { softWrap: true, style: const TextStyle( fontSize: 14.0, - color: MyTheme.accent50))) + fontWeight: FontWeight.w500, + color: MyTheme.accent80))) ], ) : const SizedBox.shrink()) @@ -334,7 +335,9 @@ class PermissionRow extends StatelessWidget { alignment: Alignment.centerLeft, child: Text(name, style: const TextStyle( - fontSize: 16.0, color: MyTheme.accent50)))), + fontSize: 16.0, + fontWeight: FontWeight.bold, + color: MyTheme.accent80)))), Expanded( flex: 2, child: FittedBox( @@ -398,7 +401,7 @@ class ConnectionManager extends StatelessWidget { }, icon: const Icon( Icons.chat, - color: MyTheme.accent80, + color: MyTheme.accent, ))) ], ), @@ -460,8 +463,8 @@ class PaddingCard extends StatelessWidget { titleIcon != null ? Padding( padding: const EdgeInsets.only(right: 10), - child: Icon(titleIcon, - color: MyTheme.accent80, size: 30)) + child: + Icon(titleIcon, color: MyTheme.accent, size: 30)) : const SizedBox.shrink(), Text( title!, @@ -469,7 +472,7 @@ class PaddingCard extends StatelessWidget { fontFamily: 'WorkSans', fontWeight: FontWeight.bold, fontSize: 20, - color: MyTheme.accent80, + color: MyTheme.accent, ), ) ], From 2e9ff13ed488c695ef8153b21289e4f28f9ed949 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 23 Sep 2022 17:28:22 +0800 Subject: [PATCH 0571/2015] button widget and preparing help cards --- .../lib/desktop/pages/connection_page.dart | 88 ++----------------- .../lib/desktop/pages/desktop_home_page.dart | 46 ++++++++++ flutter/lib/desktop/widgets/button.dart | 73 +++++++++++++++ 3 files changed, 128 insertions(+), 79 deletions(-) create mode 100644 flutter/lib/desktop/widgets/button.dart diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 261b23f53..0b17c5f47 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -14,6 +14,7 @@ import '../../common/formatter/id_formatter.dart'; import '../../common/widgets/peer_tab_page.dart'; import '../../common/widgets/peers_view.dart'; import '../../models/platform_model.dart'; +import '../widgets/button.dart'; /// Connection page for connecting to a remote peer. class ConnectionPage extends StatefulWidget { @@ -109,10 +110,6 @@ class _ConnectionPageState extends State { /// UI for the remote ID TextField. /// Search for a peer and connect to it if the id exists. Widget _buildRemoteIDTextField(BuildContext context) { - RxBool ftHover = false.obs; - RxBool ftPressed = false.obs; - RxBool connHover = false.obs; - RxBool connPressed = false.obs; RxBool inputFocused = false.obs; FocusNode focusNode = FocusNode(); focusNode.addListener(() { @@ -189,84 +186,17 @@ class _ConnectionPageState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ - Obx(() => InkWell( - onTapDown: (_) => ftPressed.value = true, - onTapUp: (_) => ftPressed.value = false, - onTapCancel: () => ftPressed.value = false, - onHover: (value) => ftHover.value = value, - onTap: () { - onConnect(isFileTransfer: true); - }, - child: Container( - height: 27, - alignment: Alignment.center, - decoration: BoxDecoration( - color: ftPressed.value - ? MyTheme.accent - : Colors.transparent, - border: Border.all( - color: ftPressed.value - ? MyTheme.accent - : ftHover.value - ? MyTheme.hoverBorder - : MyTheme.border, - ), - borderRadius: BorderRadius.circular(5), - ), - child: Text( - translate( - "Transfer File", - ), - style: TextStyle( - fontSize: 12, - color: ftPressed.value - ? MyTheme.color(context).bg - : MyTheme.color(context).text), - ).marginSymmetric(horizontal: 12), - ), - )), + Button( + isOutline: true, + onTap: () { + onConnect(isFileTransfer: true); + }, + text: "Transfer File", + ), const SizedBox( width: 17, ), - Obx( - () => InkWell( - onTapDown: (_) => connPressed.value = true, - onTapUp: (_) => connPressed.value = false, - onTapCancel: () => connPressed.value = false, - onHover: (value) => connHover.value = value, - onTap: onConnect, - child: ConstrainedBox( - constraints: BoxConstraints( - minWidth: 80.0, - ), - child: Container( - height: 27, - decoration: BoxDecoration( - color: connPressed.value - ? MyTheme.accent - : MyTheme.button, - border: Border.all( - color: connPressed.value - ? MyTheme.accent - : connHover.value - ? MyTheme.hoverBorder - : MyTheme.button, - ), - borderRadius: BorderRadius.circular(5), - ), - child: Center( - child: Text( - translate( - "Connect", - ), - style: TextStyle( - fontSize: 12, - color: MyTheme.color(context).bg), - ), - ).marginSymmetric(horizontal: 12), - )), - ), - ), + Button(onTap: onConnect, text: "Connect"), ], ), ) diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 833a914cd..e39e4f372 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -26,6 +26,7 @@ class _DesktopHomePageState extends State with TrayListener, WindowListener, AutomaticKeepAliveClientMixin { @override bool get wantKeepAlive => true; + var updateUrl = ''; @override void onWindowClose() async { @@ -74,6 +75,7 @@ class _DesktopHomePageState extends State buildTip(context), buildIDBoard(context), buildPasswordBoard(context), + buildHelpCards(), ], ), ), @@ -288,6 +290,46 @@ class _DesktopHomePageState extends State ); } + Widget buildHelpCards() { + if (Platform.isWindows) { + if (!bind.mainIsInstalled()) { + return buildInstallCard(); + } else if (bind.mainIsInstalledLowerVersion()) { + return buildUpgradeCard(); + } + } + if (updateUrl.isNotEmpty) { + return buildUpdateCard(); + } + if (Platform.isMacOS) {} + if (bind.mainIsInstalledLowerVersion()) {} + return Container(); + } + + Widget buildUpdateCard() { + return Container(); + } + + Widget buildUpgradeCard() { + return Container(); + } + + Widget buildInstallCard() { + return Container( + margin: EdgeInsets.only(top: 20), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + translate("install_tip"), + style: TextStyle(fontWeight: FontWeight.normal, fontSize: 19), + ), + ], + ), + ); + } + @override void onTrayMenuItemClick(MenuItem menuItem) { debugPrint('click ${menuItem.key}'); @@ -305,6 +347,10 @@ class _DesktopHomePageState extends State @override void initState() { super.initState(); + Timer(const Duration(seconds: 5), () async { + updateUrl = await bind.mainGetSoftwareUpdateUrl(); + if (updateUrl.isNotEmpty) setState(() {}); + }); trayManager.addListener(this); windowManager.addListener(this); rustDeskWinManager.setMethodHandler((call, fromWindowId) async { diff --git a/flutter/lib/desktop/widgets/button.dart b/flutter/lib/desktop/widgets/button.dart new file mode 100644 index 000000000..dc0cc6a2c --- /dev/null +++ b/flutter/lib/desktop/widgets/button.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import '../../common.dart'; + +class Button extends StatefulWidget { + GestureTapCallback onTap; + String text; + double? minWidth; + bool isOutline; + + Button({ + Key? key, + this.minWidth, + this.isOutline = false, + required this.onTap, + required this.text, + }) : super(key: key); + + @override + State} {auth ? "" : } - {auth ? : ""} + {auth && !disconnected ? : ""} + {auth && disconnected ? : ""}

    {c.is_file_transfer || c.port_forward ? "" :
    {svg_chat}
    }
    @@ -155,6 +157,25 @@ class Body: Reactor.Component handler.close(cid); }); } + + event click $(button#close) { + var cid = this.cid; + if (this.cur >= 0 && this.cur < connections.length){ + handler.remove_disconnected_connection(cid); + connections.splice(this.cur, 1); + if (connections.length > 0) { + if (this.cur > 0) + this.cur -= 1; + else + this.cur = connections.length - 1; + header.update(); + body.update(); + } else { + handler.quit(); + } + } + + } } $(body).content(); @@ -299,15 +320,26 @@ handler.addConnection = function(id, is_file_transfer, port_forward, peer_id, na update(); return; } + var idx = -1; + connections.map(function(c, i) { + if (c.disconnected && c.peer_id == peer_id) idx = i; + }); if (!name) name = "NA"; - connections.push({ + conn = { id: id, is_file_transfer: is_file_transfer, peer_id: peer_id, port_forward: port_forward, - name: name, authorized: authorized, time: new Date(), + name: name, authorized: authorized, time: new Date(), now: new Date(), keyboard: keyboard, clipboard: clipboard, msgs: [], unreaded: 0, - audio: audio, file: file, restart: restart, recording: recording - }); - body.cur = connections.length - 1; + audio: audio, file: file, restart: restart, recording: recording, + disconnected: false + }; + if (idx < 0) { + connections.push(conn); + body.cur = connections.length - 1; + } else { + connections[idx] = conn; + body.cur = idx; + } bring_to_top(); update(); self.timer(1ms, adjustHeader); @@ -318,15 +350,20 @@ handler.addConnection = function(id, is_file_transfer, port_forward, peer_id, na } } -handler.removeConnection = function(id) { +handler.removeConnection = function(id, close) { var i = -1; connections.map(function(c, idx) { if (c.id == id) i = idx; }); if (i < 0) return; - connections.splice(i, 1); + if (close) { + connections.splice(i, 1); + } else { + var conn = connections[i]; + conn.disconnected = true; + } if (connections.length > 0) { - if (body.cur >= i && body.cur > 0) body.cur -= 1; + if (body.cur >= i && body.cur > 0 && close) body.cur -= 1; update(); } } @@ -361,8 +398,7 @@ function self.ready() { view.move(sw - w, 0, w, h); } -function getElaspsed(time) { - var now = new Date(); +function getElaspsed(time, now) { var seconds = Date.diff(time, now, #seconds); var hours = seconds / 3600; var days = hours / 24; @@ -378,11 +414,15 @@ function getElaspsed(time) { function updateTime() { self.timer(1s, function() { + var now = new Date(); + connections.map(function(c) { + if (!c.disconnected) c.now = now; + }); var el = $(#time); if (el) { var c = connections[body.cur]; - if (c) { - el.text = getElaspsed(c.time); + if (c && !c.disconnected) { + el.text = getElaspsed(c.time, c.now); } } updateTime(); diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index 3813760a0..bd05f3bce 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -31,6 +31,7 @@ use hbb_common::{ pub struct Client { pub id: i32, pub authorized: bool, + pub disconnected: bool, pub is_file_transfer: bool, pub port_forward: String, pub name: String, @@ -58,7 +59,7 @@ pub struct ConnectionManager { pub trait InvokeUiCM: Send + Clone + 'static + Sized { fn add_connection(&self, client: &Client); - fn remove_connection(&self, id: i32); + fn remove_connection(&self, id: i32, close: bool); fn new_message(&self, id: i32, text: String); @@ -101,6 +102,7 @@ impl ConnectionManager { let client = Client { id, authorized, + disconnected: false, is_file_transfer, port_forward, name: name.clone(), @@ -113,12 +115,24 @@ impl ConnectionManager { recording, tx, }; + CLIENTS + .write() + .unwrap() + .retain(|_, c| !(c.disconnected && c.peer_id == client.peer_id)); + CLIENTS.write().unwrap().insert(id, client.clone()); self.ui_handler.add_connection(&client); - CLIENTS.write().unwrap().insert(id, client); } - fn remove_connection(&self, id: i32) { - CLIENTS.write().unwrap().remove(&id); + fn remove_connection(&self, id: i32, close: bool) { + if close { + CLIENTS.write().unwrap().remove(&id); + } else { + CLIENTS + .write() + .unwrap() + .get_mut(&id) + .map(|c| c.disconnected = true); + } #[cfg(any(target_os = "android"))] if CLIENTS @@ -136,7 +150,7 @@ impl ConnectionManager { } } - self.ui_handler.remove_connection(id); + self.ui_handler.remove_connection(id, close); } } @@ -167,6 +181,11 @@ pub fn close(id: i32) { }; } +#[inline] +pub fn remove(id: i32) { + CLIENTS.write().unwrap().remove(&id); +} + // server mode send chat to peer #[inline] pub fn send_chat(id: i32, text: String) { @@ -243,6 +262,7 @@ pub async fn start_ipc(cm: ConnectionManager) { let mut conn_id: i32 = 0; let (tx, mut rx) = mpsc::unbounded_channel::(); let mut write_jobs: Vec = Vec::new(); + let mut close = true; loop { tokio::select! { res = stream.next() => { @@ -264,6 +284,12 @@ pub async fn start_ipc(cm: ConnectionManager) { log::info!("cm ipc connection closed from connection request"); break; } + Data::Disconnected => { + close = false; + tx_file.send(ClipboardFileData::Enable((conn_id, false))).ok(); + log::info!("cm ipc connection disconnect"); + break; + } Data::PrivacyModeState((id, _)) => { conn_id = conn_id_tmp; allow_err!(tx.send(data)); @@ -312,7 +338,7 @@ pub async fn start_ipc(cm: ConnectionManager) { } } if conn_id != conn_id_tmp { - cm.remove_connection(conn_id); + cm.remove_connection(conn_id, close); } }); } From 4b72b57428dcc1708bc40763c025ebfea2a12938 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sun, 9 Oct 2022 17:13:14 +0800 Subject: [PATCH 0654/2015] fix: scroll alignment in remote page --- flutter/lib/desktop/pages/remote_page.dart | 153 ++++++++++++++------- 1 file changed, 105 insertions(+), 48 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index eb070a676..52f474fbd 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -286,30 +286,35 @@ class ImagePaint extends StatelessWidget { child: child)); if (c.scrollStyle == ScrollStyle.scrollbar) { + final imageWidth = c.getDisplayWidth() * s; + final imageHeight = c.getDisplayHeight() * s; final imageWidget = SizedBox( - width: c.getDisplayWidth() * s, - height: c.getDisplayHeight() * s, + width: imageWidth, + height: imageHeight, child: CustomPaint( painter: ImagePainter(image: m.image, x: 0, y: 0, scale: s), )); - return Center( - child: NotificationListener( - onNotification: (notification) { - final percentX = _horizontal.position.extentBefore / - (_horizontal.position.extentBefore + - _horizontal.position.extentInside + - _horizontal.position.extentAfter); - final percentY = _vertical.position.extentBefore / - (_vertical.position.extentBefore + - _vertical.position.extentInside + - _vertical.position.extentAfter); - c.setScrollPercent(percentX, percentY); - return false; - }, - child: mouseRegion( - child: _buildCrossScrollbar(_buildListener(imageWidget))), - ), + return NotificationListener( + onNotification: (notification) { + final percentX = _horizontal.hasClients + ? _horizontal.position.extentBefore / + (_horizontal.position.extentBefore + + _horizontal.position.extentInside + + _horizontal.position.extentAfter) + : 0.0; + final percentY = _vertical.hasClients + ? _vertical.position.extentBefore / + (_vertical.position.extentBefore + + _vertical.position.extentInside + + _vertical.position.extentAfter) + : 0.0; + c.setScrollPercent(percentX, percentY); + return false; + }, + child: mouseRegion( + child: _buildCrossScrollbar(context, _buildListener(imageWidget), + Size(imageWidth, imageHeight))), ); } else { final imageWidget = SizedBox( @@ -342,44 +347,96 @@ class ImagePaint extends StatelessWidget { } } - Widget _buildCrossScrollbar(Widget child) { + Widget _buildCrossScrollbarFromLayout( + BuildContext context, Widget child, Size layoutSize, Size size) { final scrollConfig = CustomMouseWheelScrollConfig( scrollDuration: kDefaultScrollDuration, scrollCurve: Curves.linearToEaseOut, mouseWheelTurnsThrottleTimeMs: kDefaultMouseWheelThrottleDuration.inMilliseconds, scrollAmountMultiplier: kDefaultScrollAmountMultiplier); - return Obx(() => ImprovedScrolling( + var widget = child; + if (layoutSize.width < size.width) { + widget = ScrollConfiguration( + behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false), + child: SingleChildScrollView( + controller: _horizontal, + scrollDirection: Axis.horizontal, + physics: cursorOverImage.isTrue + ? const NeverScrollableScrollPhysics() + : null, + child: widget, + ), + ); + } else { + widget = Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [widget], + ); + } + if (layoutSize.height < size.height) { + widget = ScrollConfiguration( + behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false), + child: SingleChildScrollView( + controller: _vertical, + physics: cursorOverImage.isTrue + ? const NeverScrollableScrollPhysics() + : null, + child: widget, + ), + ); + } else { + widget = Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [widget], + ); + } + if (layoutSize.width < size.width) { + widget = ImprovedScrolling( + scrollController: _horizontal, + enableCustomMouseWheelScrolling: cursorOverImage.isFalse, + customMouseWheelScrollConfig: scrollConfig, + child: RawScrollbar( + thumbColor: Colors.grey, + controller: _horizontal, + thumbVisibility: false, + trackVisibility: false, + notificationPredicate: (notification) => notification.depth == 1, + child: widget, + ), + ); + } + if (layoutSize.height < size.height) { + widget = ImprovedScrolling( scrollController: _vertical, enableCustomMouseWheelScrolling: cursorOverImage.isFalse, customMouseWheelScrollConfig: scrollConfig, - child: ImprovedScrolling( - scrollController: _horizontal, - enableCustomMouseWheelScrolling: cursorOverImage.isFalse, - customMouseWheelScrollConfig: scrollConfig, - child: Scrollbar( - controller: _vertical, - thumbVisibility: false, - trackVisibility: false, - child: Scrollbar( - controller: _horizontal, - thumbVisibility: false, - trackVisibility: false, - notificationPredicate: (notif) => notif.depth == 1, - child: SingleChildScrollView( - controller: _vertical, - physics: cursorOverImage.isTrue - ? const NeverScrollableScrollPhysics() - : null, - child: SingleChildScrollView( - controller: _horizontal, - scrollDirection: Axis.horizontal, - physics: cursorOverImage.isTrue - ? const NeverScrollableScrollPhysics() - : null, - child: child, - ), - )))))); + child: RawScrollbar( + thumbColor: Colors.grey, + controller: _vertical, + thumbVisibility: false, + trackVisibility: false, + child: widget, + ), + ); + } + + return widget; + } + + Widget _buildCrossScrollbar(BuildContext context, Widget child, Size size) { + var layoutSize = MediaQuery.of(context).size; + layoutSize = Size( + layoutSize.width - kWindowBorderWidth * 2, + layoutSize.height - + kWindowBorderWidth * 2 - + kDesktopRemoteTabBarHeight); + bool overflow = + layoutSize.width < size.width || layoutSize.height < size.height; + return overflow + ? Obx(() => + _buildCrossScrollbarFromLayout(context, child, layoutSize, size)) + : _buildCrossScrollbarFromLayout(context, child, layoutSize, size); } Widget _buildListener(Widget child) { From f3a60a0448b521366e229c9163b2b755bc92cca9 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sun, 9 Oct 2022 18:13:15 +0800 Subject: [PATCH 0655/2015] opt: remove debug output from custom cursor --- flutter/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 1865dfec0..fc62df908 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -73,7 +73,7 @@ dependencies: flutter_custom_cursor: git: url: https://github.com/Kingtous/rustdesk_flutter_custom_cursor - ref: 527821d676017387be024dffd61898ff79b14c41 + ref: 4a950fd3a5a228bf5381070a4c803919d5787c07 get: ^4.6.5 visibility_detector: ^0.3.3 contextmenu: ^3.0.0 From 8d23c11312cd001cb905ef631cc971595a72b27c Mon Sep 17 00:00:00 2001 From: csf Date: Sun, 9 Oct 2022 19:41:50 +0900 Subject: [PATCH 0656/2015] fix abModel multi request and state didn't refresh bug --- flutter/lib/common/widgets/address_book.dart | 62 +++---- flutter/lib/common/widgets/peer_tab_page.dart | 3 +- .../lib/desktop/pages/desktop_home_page.dart | 1 - .../desktop/pages/desktop_setting_page.dart | 14 +- flutter/lib/main.dart | 3 - flutter/lib/mobile/pages/connection_page.dart | 16 +- flutter/lib/mobile/pages/scan_page.dart | 7 +- flutter/lib/mobile/pages/settings_page.dart | 168 ++---------------- flutter/lib/models/ab_model.dart | 3 +- flutter/lib/models/model.dart | 4 - flutter/lib/models/user_model.dart | 60 ++++++- 11 files changed, 111 insertions(+), 230 deletions(-) diff --git a/flutter/lib/common/widgets/address_book.dart b/flutter/lib/common/widgets/address_book.dart index 49d2eaf04..52189c8b1 100644 --- a/flutter/lib/common/widgets/address_book.dart +++ b/flutter/lib/common/widgets/address_book.dart @@ -9,7 +9,6 @@ import 'package:get/get.dart'; import '../../common.dart'; import '../../desktop/pages/desktop_home_page.dart'; import '../../mobile/pages/settings_page.dart'; -import '../../models/platform_model.dart'; class AddressBook extends StatefulWidget { final EdgeInsets? menuPadding; @@ -30,7 +29,7 @@ class _AddressBookState extends State { @override Widget build(BuildContext context) => FutureBuilder( - future: buildAddressBook(context), + future: buildBody(context), builder: (context, snapshot) { if (snapshot.hasData) { return snapshot.data!; @@ -44,7 +43,7 @@ class _AddressBookState extends State { if (isDesktop) { loginDialog().then((success) { if (success) { - setState(() {}); + gFFI.abModel.pullAb(); } }); } else { @@ -52,41 +51,30 @@ class _AddressBookState extends State { } } - Future buildAddressBook(BuildContext context) async { - final token = await bind.mainGetLocalOption(key: 'access_token'); - if (token.trim().isEmpty) { - return Center( - child: InkWell( - onTap: handleLogin, - child: Text( - translate("Login"), - style: const TextStyle(decoration: TextDecoration.underline), + Future buildBody(BuildContext context) async { + return Obx(() { + if (gFFI.userModel.userName.value.isEmpty) { + return Center( + child: InkWell( + onTap: handleLogin, + child: Text( + translate("Login"), + style: const TextStyle(decoration: TextDecoration.underline), + ), ), - ), - ); - } - final model = gFFI.abModel; - return FutureBuilder( - future: model.pullAb(), - builder: (context, snapshot) { - if (snapshot.hasData) { - return _buildAddressBook(context); - } else if (snapshot.hasError) { - return _buildShowError(snapshot.error.toString()); - } else { - return Obx(() { - if (model.abLoading.value) { - return const Center( - child: CircularProgressIndicator(), - ); - } else if (model.abError.isNotEmpty) { - return _buildShowError(model.abError.value); - } else { - return const Offstage(); - } - }); - } - }); + ); + } else { + if (gFFI.abModel.abLoading.value) { + return const Center( + child: CircularProgressIndicator(), + ); + } + if (gFFI.abModel.abError.isNotEmpty) { + return _buildShowError(gFFI.abModel.abError.value); + } + return _buildAddressBook(context); + } + }); } Widget _buildShowError(String error) { diff --git a/flutter/lib/common/widgets/peer_tab_page.dart b/flutter/lib/common/widgets/peer_tab_page.dart index 9a5503e26..81559a3d3 100644 --- a/flutter/lib/common/widgets/peer_tab_page.dart +++ b/flutter/lib/common/widgets/peer_tab_page.dart @@ -54,7 +54,8 @@ class _PeerTabPageState extends State bind.mainDiscover(); break; case 3: - gFFI.abModel.pullAb(); + + /// AddressBook initState will refresh ab state break; } } diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index fcc8c4991..6cb78b6a7 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -500,7 +500,6 @@ Future loginDialog() async { close(); } - // 登录dialog return CustomAlertDialog( title: Text(translate("Login")), content: ConstrainedBox( diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 1c28fdd98..1534f9394 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -761,20 +761,18 @@ class _AccountState extends State<_Account> { Widget accountAction() { return _futureBuilder(future: () async { return await gFFI.userModel.getUserName(); - }(), hasData: (data) { - String username = data as String; - return _Button( - username.isEmpty ? 'Login' : 'Logout', + }(), hasData: (_) { + return Obx(() => _Button( + gFFI.userModel.userName.value.isEmpty ? 'Login' : 'Logout', () => { - username.isEmpty + gFFI.userModel.userName.value.isEmpty ? loginDialog().then((success) { if (success) { - // refresh frame - setState(() {}); + gFFI.abModel.pullAb(); } }) : gFFI.userModel.logOut() - }); + })); }); } } diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 230199431..0d1123e05 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -20,7 +20,6 @@ import 'common.dart'; import 'consts.dart'; import 'mobile/pages/home_page.dart'; import 'mobile/pages/server_page.dart'; -import 'mobile/pages/settings_page.dart'; import 'models/platform_model.dart'; int? windowId; @@ -82,7 +81,6 @@ Future initEnv(String appType) async { // focus on multi-ffi on desktop first await initGlobalFFI(); // await Firebase.initializeApp(); - refreshCurrentUser(); _registerEventHandler(); } @@ -267,7 +265,6 @@ class _AppState extends State { ChangeNotifierProvider.value(value: gFFI.imageModel), ChangeNotifierProvider.value(value: gFFI.cursorModel), ChangeNotifierProvider.value(value: gFFI.canvasModel), - ChangeNotifierProvider.value(value: gFFI.userModel), ], child: GetMaterialApp( navigatorKey: globalKey, diff --git a/flutter/lib/mobile/pages/connection_page.dart b/flutter/lib/mobile/pages/connection_page.dart index 9d105ade6..e99226c4d 100644 --- a/flutter/lib/mobile/pages/connection_page.dart +++ b/flutter/lib/mobile/pages/connection_page.dart @@ -206,20 +206,14 @@ class WebMenu extends StatefulWidget { } class _WebMenuState extends State { - String? username; String url = ""; @override void initState() { super.initState(); () async { - final usernameRes = await getUsername(); - final urlRes = await getUrl(); + final urlRes = await bind.mainGetApiServer(); var update = false; - if (usernameRes != username) { - username = usernameRes; - update = true; - } if (urlRes != url) { url = urlRes; update = true; @@ -256,9 +250,9 @@ class _WebMenuState extends State { : [ PopupMenuItem( value: "login", - child: Text(username == null + child: Text(gFFI.userModel.userName.value.isEmpty ? translate("Login") - : '${translate("Logout")} ($username)'), + : '${translate("Logout")} (${gFFI.userModel.userName.value})'), ) ]) + [ @@ -276,10 +270,10 @@ class _WebMenuState extends State { showAbout(gFFI.dialogManager); } if (value == 'login') { - if (username == null) { + if (gFFI.userModel.userName.value.isEmpty) { showLogin(gFFI.dialogManager); } else { - logout(gFFI.dialogManager); + gFFI.userModel.logOut(); } } if (value == 'scan') { diff --git a/flutter/lib/mobile/pages/scan_page.dart b/flutter/lib/mobile/pages/scan_page.dart index 2487c0f58..3bd381d92 100644 --- a/flutter/lib/mobile/pages/scan_page.dart +++ b/flutter/lib/mobile/pages/scan_page.dart @@ -263,12 +263,13 @@ void showServerSettingsWithValue(String id, String relay, String key, if (id != id0) { bind.mainSetOption(key: "custom-rendezvous-server", value: id); } - if (relay != relay0) + if (relay != relay0) { bind.mainSetOption(key: "relay-server", value: relay); + } if (key != key0) bind.mainSetOption(key: "key", value: key); - if (api != api0) + if (api != api0) { bind.mainSetOption(key: "api-server", value: api); - gFFI.ffiModel.updateUser(); + } close(); } setState(() { diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index 4f1640a03..c555314d0 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -3,7 +3,6 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:http/http.dart' as http; import 'package:provider/provider.dart'; import 'package:settings_ui/settings_ui.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -41,8 +40,6 @@ var _localIP = ""; var _directAccessPort = ""; class _SettingsState extends State with WidgetsBindingObserver { - String? username; - @override void initState() { super.initState(); @@ -54,12 +51,6 @@ class _SettingsState extends State with WidgetsBindingObserver { update = await updateIgnoreBatteryStatus(); } - final usernameRes = await getUsername(); - if (usernameRes != username) { - update = true; - username = usernameRes; - } - final enableAbrRes = await bind.mainGetOption(key: "enable-abr") != "N"; if (enableAbrRes != _enableAbr) { update = true; @@ -273,15 +264,15 @@ class _SettingsState extends State with WidgetsBindingObserver { title: Text(translate("Account")), tiles: [ SettingsTile.navigation( - title: Text(username == null + title: Obx(() => Text(gFFI.userModel.userName.value.isEmpty ? translate("Login") - : '${translate("Logout")} ($username)'), + : '${translate("Logout")} (${gFFI.userModel.userName.value})')), leading: Icon(Icons.person), onPressed: (context) { - if (username == null) { + if (gFFI.userModel.userName.value.isEmpty) { showLogin(gFFI.dialogManager); } else { - logout(gFFI.dialogManager); + gFFI.userModel.logOut(); } }, ), @@ -438,130 +429,6 @@ void showAbout(OverlayDialogManager dialogManager) { }, clickMaskDismiss: true, backDismiss: true); } -Future login(String name, String pass) async { -/* js test CORS -const data = { username: 'example', password: 'xx' }; - -fetch('http://localhost:21114/api/login', { - method: 'POST', // or 'PUT' - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(data), -}) -.then(response => response.json()) -.then(data => { - console.log('Success:', data); -}) -.catch((error) => { - console.error('Error:', error); -}); -*/ - final url = getUrl(); - final body = { - 'username': name, - 'password': pass, - 'id': bind.mainGetMyId(), - 'uuid': bind.mainGetUuid() - }; - try { - final response = await http.post(Uri.parse('$url/api/login'), - headers: {"Content-Type": "application/json"}, body: json.encode(body)); - return parseResp(response.body); - } catch (e) { - print(e); - return 'Failed to access $url'; - } -} - -String parseResp(String body) { - final data = json.decode(body); - final error = data['error']; - if (error != null) { - return error!; - } - final token = data['access_token']; - if (token != null) { - bind.mainSetOption(key: "access_token", value: token); - } - final info = data['user']; - if (info != null) { - final value = json.encode(info); - bind.mainSetOption(key: "user_info", value: value); - gFFI.ffiModel.updateUser(); - } - return ''; -} - -void refreshCurrentUser() async { - final token = await bind.mainGetOption(key: "access_token"); - if (token == '') return; - final url = getUrl(); - final body = {'id': bind.mainGetMyId(), 'uuid': bind.mainGetUuid()}; - try { - final response = await http.post(Uri.parse('$url/api/currentUser'), - headers: { - "Content-Type": "application/json", - "Authorization": "Bearer $token" - }, - body: json.encode(body)); - final status = response.statusCode; - if (status == 401 || status == 400) { - resetToken(); - return; - } - parseResp(response.body); - } catch (e) { - print('$e'); - } -} - -void logout(OverlayDialogManager dialogManager) async { - final token = await bind.mainGetOption(key: "access_token"); - if (token == '') return; - final url = getUrl(); - final body = {'id': bind.mainGetMyId(), 'uuid': bind.mainGetUuid()}; - try { - await http.post(Uri.parse('$url/api/logout'), - headers: { - "Content-Type": "application/json", - "Authorization": "Bearer $token" - }, - body: json.encode(body)); - } catch (e) { - showToast('Failed to access $url'); - } - resetToken(); -} - -void resetToken() async { - await bind.mainSetOption(key: "access_token", value: ""); - await bind.mainSetOption(key: "user_info", value: ""); - gFFI.ffiModel.updateUser(); -} - -Future getUrl() async { - var url = await bind.mainGetOption(key: "api-server"); - if (url == '') { - url = await bind.mainGetOption(key: "custom-rendezvous-server"); - if (url != '') { - if (url.contains(':')) { - final tmp = url.split(':'); - if (tmp.length == 2) { - var port = int.parse(tmp[1]) - 2; - url = 'http://${tmp[0]}:$port'; - } - } else { - url = 'http://$url:21114'; - } - } - } - if (url == '') { - url = 'https://admin.rustdesk.com'; - } - return url; -} - void showLogin(OverlayDialogManager dialogManager) { final passwordController = TextEditingController(); final nameController = TextEditingController(); @@ -615,15 +482,17 @@ void showLogin(OverlayDialogManager dialogManager) { setState(() { loading = true; }); - final e = await login(name, pass); + final resp = await gFFI.userModel.login(name, pass); setState(() { loading = false; - error = e; }); - if (e == "") { - close(); + if (resp.containsKey('error')) { + error = resp['error']; + return; } + gFFI.abModel.pullAb(); } + close(); }, child: Text(translate('OK')), ), @@ -632,23 +501,6 @@ void showLogin(OverlayDialogManager dialogManager) { }); } -Future getUsername() async { - final token = await bind.mainGetOption(key: "access_token"); - String? username; - if (token != "") { - final info = await bind.mainGetOption(key: "user_info"); - if (info != "") { - try { - Map tmp = json.decode(info); - username = tmp["name"]; - } catch (e) { - print('$e'); - } - } - } - return username; -} - class ScanButton extends StatelessWidget { @override Widget build(BuildContext context) { diff --git a/flutter/lib/models/ab_model.dart b/flutter/lib/models/ab_model.dart index ae41e07e6..5a055fd14 100644 --- a/flutter/lib/models/ab_model.dart +++ b/flutter/lib/models/ab_model.dart @@ -24,8 +24,9 @@ class AbModel { FFI? get _ffi => parent.target; Future pullAb() async { + if (_ffi!.userModel.userName.isEmpty) return; abLoading.value = true; - // request + abError.value = ""; final api = "${await bind.mainGetApiServer()}/api/ab/get"; try { final resp = diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 833ab32a0..c646f5285 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -82,10 +82,6 @@ class FfiModel with ChangeNotifier { notifyListeners(); } - updateUser() { - notifyListeners(); - } - bool keyboard() => _permissions['keyboard'] != false; clear() { diff --git a/flutter/lib/models/user_model.dart b/flutter/lib/models/user_model.dart index e195c205d..e9990efa9 100644 --- a/flutter/lib/models/user_model.dart +++ b/flutter/lib/models/user_model.dart @@ -9,11 +9,65 @@ import '../common.dart'; import 'model.dart'; import 'platform_model.dart'; -class UserModel extends ChangeNotifier { +class UserModel { var userName = "".obs; WeakReference parent; - UserModel(this.parent); + UserModel(this.parent) { + refreshCurrentUser(); + } + + void refreshCurrentUser() async { + await getUserName(); + final token = await bind.mainGetLocalOption(key: "access_token"); + if (token == '') return; + final url = await bind.mainGetApiServer(); + final body = { + 'id': await bind.mainGetMyId(), + 'uuid': await bind.mainGetUuid() + }; + try { + final response = await http.post(Uri.parse('$url/api/currentUser'), + headers: { + "Content-Type": "application/json", + "Authorization": "Bearer $token" + }, + body: json.encode(body)); + final status = response.statusCode; + if (status == 401 || status == 400) { + resetToken(); + return; + } + await _parseResp(response.body); + } catch (e) { + print('Failed to refreshCurrentUser: $e'); + } + } + + void resetToken() async { + await bind.mainSetLocalOption(key: "access_token", value: ""); + await bind.mainSetLocalOption(key: "user_info", value: ""); + userName.value = ""; + } + + Future _parseResp(String body) async { + final data = json.decode(body); + final error = data['error']; + if (error != null) { + return error!; + } + final token = data['access_token']; + if (token != null) { + await bind.mainSetLocalOption(key: "access_token", value: token); + } + final info = data['user']; + if (info != null) { + final value = json.encode(info); + await bind.mainSetOption(key: "user_info", value: value); + userName.value = info["name"]; + } + return ''; + } Future getUserName() async { if (userName.isNotEmpty) { @@ -29,6 +83,7 @@ class UserModel extends ChangeNotifier { } Future logOut() async { + // TODO show toast debugPrint("start logout"); final url = await bind.mainGetApiServer(); final _ = await http.post(Uri.parse("$url/api/logout"), @@ -44,7 +99,6 @@ class UserModel extends ChangeNotifier { ]); parent.target?.abModel.clear(); userName.value = ""; - notifyListeners(); } Future> login(String userName, String pass) async { From 715d837f5424b99a21ef1866ec3d764f7f4de693 Mon Sep 17 00:00:00 2001 From: csf Date: Sun, 9 Oct 2022 19:57:38 +0900 Subject: [PATCH 0657/2015] logOut show loading --- flutter/lib/common.dart | 26 +++++++++++++++----------- flutter/lib/models/user_model.dart | 5 ++--- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 2ff3d4477..ce341d160 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -383,22 +383,23 @@ class OverlayDialogManager { "[OverlayDialogManager] Failed to show dialog, _overlayState is null, call [setOverlayState] first"); } - final _tag; + final String dialogTag; if (tag != null) { - _tag = tag; + dialogTag = tag; } else { - _tag = _tagCount.toString(); + dialogTag = _tagCount.toString(); _tagCount++; } final dialog = Dialog(); - _dialogs[_tag] = dialog; + _dialogs[dialogTag] = dialog; - final close = ([res]) { - _dialogs.remove(_tag); + close([res]) { + _dialogs.remove(dialogTag); dialog.complete(res); - BackButtonInterceptor.removeByName(_tag); - }; + BackButtonInterceptor.removeByName(dialogTag); + } + dialog.entry = OverlayEntry(builder: (_) { bool innerClicked = false; return Listener( @@ -423,14 +424,16 @@ class OverlayDialogManager { close(); } return true; - }, name: _tag); + }, name: dialogTag); return dialog.completer.future; } - void showLoading(String text, + String showLoading(String text, {bool clickMaskDismiss = false, bool showCancel = true, VoidCallback? onCancel}) { + final tag = _tagCount.toString(); + _tagCount++; show((setState, close) { cancel() { dismissAll(); @@ -465,7 +468,8 @@ class OverlayDialogManager { ])), onCancel: showCancel ? cancel : null, ); - }); + }, tag: tag); + return tag; } void resetMobileActionsOverlay({FFI? ffi}) { diff --git a/flutter/lib/models/user_model.dart b/flutter/lib/models/user_model.dart index e9990efa9..163efaebc 100644 --- a/flutter/lib/models/user_model.dart +++ b/flutter/lib/models/user_model.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'dart:convert'; -import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:http/http.dart' as http; @@ -83,8 +82,7 @@ class UserModel { } Future logOut() async { - // TODO show toast - debugPrint("start logout"); + final tag = gFFI.dialogManager.showLoading(translate('Waiting')); final url = await bind.mainGetApiServer(); final _ = await http.post(Uri.parse("$url/api/logout"), body: { @@ -99,6 +97,7 @@ class UserModel { ]); parent.target?.abModel.clear(); userName.value = ""; + gFFI.dialogManager.dismissByTag(tag); } Future> login(String userName, String pass) async { From 55e30fa51bbecc96f3bc08b97abd410cb9213128 Mon Sep 17 00:00:00 2001 From: RelatedTitle Date: Sun, 9 Oct 2022 20:25:13 -0500 Subject: [PATCH 0658/2015] Update Spanish README Updated Spanish README to match the current state of the main README (updated server list, added Wayland support section, among other things). Reworded certain parts for clarity. Ensured the tone was consistent. (Some parts used the informal tone while others used the formal tone, even in the same sentence) Corrected small mistakes. Signed-off-by: RelatedTitle --- docs/README-ES.md | 79 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 55 insertions(+), 24 deletions(-) diff --git a/docs/README-ES.md b/docs/README-ES.md index d2d97b23c..f647ecbfd 100644 --- a/docs/README-ES.md +++ b/docs/README-ES.md @@ -4,7 +4,7 @@ CompilarDockerEstructura • - Captura de pantalla
    + Capturas de pantalla
    [English] | [Українська] | [česky] | [中文] | [Magyar] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
    Necesitamos tu ayuda para traducir este README a tu idioma

    @@ -13,27 +13,34 @@ Chatea con nosotros: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https: [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) -Otro software de escritorio remoto, escrito en Rust. Funciona de forma inmediata, sin necesidad de configuración. Tienes el control total de sus datos, sin preocupaciones sobre la seguridad. Puedes utilizar nuestro servidor de rendezvous/relay, [set up your own](https://rustdesk.com/server), o [escribir tu propio servidor rendezvous/relay](https://github.com/rustdesk/rustdesk-server-demo). +Otro software de escritorio remoto, escrito en Rust. Funciona de forma inmediata, sin necesidad de configuración. Tienes el control total de tus datos, sin preocupaciones sobre la seguridad. Puedes utilizar nuestro servidor de rendezvous/relay, [instalar el tuyo](https://rustdesk.com/server), o [escribir tu propio servidor rendezvous/relay](https://github.com/rustdesk/rustdesk-server-demo). -RustDesk agradece la contribución de todo el mundo. Ve [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) para ayuda inicial. +RustDesk agradece la contribución de todo el mundo. Lee [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) para ayuda para empezar. + +[**¿Cómo funciona rustdesk?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F) [**DESCARGA DE BINARIOS**](https://github.com/rustdesk/rustdesk/releases) +[Get it on F-Droid](https://f-droid.org/en/packages/com.carriez.flutter_hbb) + ## Servidores gratis de uso público -A continuación se muestran los servidores que está utilizando de forma gratuita, puede cambiar en algún momento. Si no estás cerca de uno de ellos, tu red puede ser lenta. +A continuación se muestran los servidores gratuitos, pueden cambiar a medida que pasa el tiempo. Si no estás cerca de uno de ellos, tu conexión puede ser lenta. -| Ubicación | Vendedor | Especificación | +| Ubicación | Compañía | Especificación | | --------- | ------------- | ------------------ | -| Seoul | AWS lightsail | 1 VCPU / 0.5GB RAM | -| Singapore | Vultr | 1 VCPU / 1GB RAM | -| Dallas | Vultr | 1 VCPU / 1GB RAM | | -| Germany | Hetzner | 2 VCPU / 4GB RAM | -| Germany | Codext | 4 VCPU / 8GB RAM | +| Seúl | AWS lightsail | 1 vCPU / 0.5GB RAM | +| Singapur | Vultr | 1 vCPU / 1GB RAM | +| Alemania | Hetzner | 2 vCPU / 4GB RAM | +| Alemania | Codext | 4 vCPU / 8GB RAM | -## Dependencies +## Dependencias -La versión Desktop usa [sciter](https://sciter.com/) para GUI, por favor bajate la librería sciter tu mismo.. +La versión Desktop usa [Sciter](https://sciter.com/) o Flutter para el GUI, este tutorial es solo para Sciter. + +Por favor descarga la librería dinámica de Sciter tu mismo. [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,14 +48,14 @@ La versión Desktop usa [sciter](https://sciter.com/) para GUI, por favor bajate ## Pasos para compilar desde el inicio -- Prepara el entono de desarrollo de Rust y el entorno de compilación de C++ y Rust. +- Prepara el entorno de desarrollo de Rust y el entorno de compilación de C++ y Rust. - Instala [vcpkg](https://github.com/microsoft/vcpkg), y configura la variable de entono `VCPKG_ROOT` correctamente. - Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static - Linux/Osx: vcpkg install libvpx libyuv opus -- run `cargo run` +- Corre `cargo run` ## Como compilar en linux @@ -70,7 +77,7 @@ sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb- sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire ``` -### Install vcpkg +### Instala vcpkg ```sh git clone https://github.com/microsoft/vcpkg @@ -82,7 +89,7 @@ export VCPKG_ROOT=$HOME/vcpkg vcpkg/vcpkg install libvpx libyuv opus ``` -### Soluciona libvpx (Para Fedora) +### Arregla libvpx (Para Fedora) ```sh cd vcpkg/buildtrees/libvpx/src @@ -110,7 +117,31 @@ cargo run ### Cambia Wayland a X11 (Xorg) -RustDesk no soporta Wayland. Comprueba [aquí](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) para configurar Xorg en la sesión por defecto de GNOME. +RustDesk no soporta Wayland. Lee [esto](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) para configurar Xorg en la sesión por defecto de GNOME. + +## Soporte para Wayland + +Wayland no parece proporcionar ninguna API para enviar pulsaciones de teclas a otras ventanas. Por lo tanto, rustdesk usa una API de nivel bajo, a saber, el dispositivo `/dev/uinput` (a nivel del kernel de Linux). + +Cuando wayland esta del lado controlado, hay que iniciar de la siguiente manera: +```bash +# Empezar el servicio uinput +$ sudo rustdesk --service +$ rustdesk +``` +**Aviso**: La grabación de pantalla de Wayland utiliza diferentes interfaces. RustDesk actualmente sólo soporta 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 +# No soportado +Error org.freedesktop.DBus.Error.InvalidArgs: No such interface “org.freedesktop.portal.ScreenCast” +# Soportado +method return time=1662544486.931020 sender=:1.54 -> destination=:1.139 serial=257 reply_serial=2 + variant uint32 4 +``` ## Como compilar con Docker @@ -128,7 +159,7 @@ Entonces, cada vez que necesites compilar una modificación, ejecuta el siguient 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 ``` -Ten en cuenta que la primera compilación puede tardar más tiempo antes de que las dependencias se almacenen en la caché, las siguientes compilaciones serán más rápidas. Además, si necesitas especificar diferentes argumentos a la orden de compilación, puede hacerlo al final de la linea de comandos en el apartado ``. Por ejemplo, si desea compilar una versión optimizada para publicación, deberá ejecutar el comando anterior seguido de `--release`. El ejecutable resultante estará disponible en la carpeta de destino en su sistema, y puede ser ejecutado con: +Ten en cuenta que la primera compilación puede tardar más tiempo antes de que las dependencias se almacenen en la caché, las siguientes compilaciones serán más rápidas. Además, si necesitas especificar diferentes argumentos al comando de compilación, puedes hacerlo al final del comando en la posición ``. Por ejemplo, si deseas compilar una versión optimizada para publicación, deberas ejecutar el comando anterior seguido de `--release`. El ejecutable resultante estará disponible en la carpeta de destino en tu sistema, y puede ser ejecutado con: ```sh target/debug/rustdesk @@ -140,22 +171,22 @@ O si estas ejecutando una versión para su publicación: target/release/rustdesk ``` -Por favor, asegurate de que estás ejecutando estos comandos desde la raíz del repositorio de RustDesk, de lo contrario la aplicación puede ser incapaz de encontrar los recursos necesarios. También hay que tener en cuenta que otros subcomandos de carga como `install` o `run` no estan actualmente soportados via este metodo y podrían requerir ser instalados dentro del contenedor y no en el host. +Por favor, asegurate de que estás ejecutando estos comandos desde la raíz del repositorio de RustDesk, de lo contrario la aplicación puede ser incapaz de encontrar los recursos necesarios. También ten en cuenta que otros subcomandos de cargo como `install` o `run` no estan actualmente soportados usando este metodo, ya que instalarían o ejecutarían el programa dentro del contenedor en lugar del host. ## Estructura de archivos -- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: video codec, configuración, tcp/udp wrapper, protobuf, fs funciones para transferencia de ficheros, y alguna función de utilidad. +- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: codec de video, configuración, tcp/udp wrapper, protobuf, funciones para transferencia de archivos, y otras funciones de utilidad. - **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: captura de pantalla -- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: control específico por cada plataforma para el teclado/ratón +- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: control del teclado/mouse especificos de cada plataforma - **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI -- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: sonido/portapapeles/entrada/servicios de video, y conexiones de red +- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: sonido/portapapeles/input/servicios de video, y conexiones de red - **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: iniciar una conexión "peer to peer" - **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Comunicación con [rustdesk-server](https://github.com/rustdesk/rustdesk-server), esperar la conexión remota directa ("TCP hole punching") o conexión indirecta ("relayed") - **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: código específico de cada plataforma - **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter, código para moviles -- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Javascript para cliente web Flutter +- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Javascript para el cliente web Flutter -## Captura de pantalla +## Capturas de pantalla ![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) From 03439831a7c137664a970dda7b08d3a77c4d8a21 Mon Sep 17 00:00:00 2001 From: fufesou Date: Sat, 8 Oct 2022 17:27:30 +0800 Subject: [PATCH 0659/2015] flutter_desktop: adjust window Signed-off-by: fufesou --- flutter/lib/common/shared_state.dart | 22 +++++ .../lib/desktop/pages/desktop_tab_page.dart | 3 + flutter/lib/desktop/pages/remote_page.dart | 3 + .../lib/desktop/pages/remote_tab_page.dart | 12 ++- .../lib/desktop/widgets/remote_menubar.dart | 91 +++++++++++++++---- .../lib/desktop/widgets/tabbar_widget.dart | 2 + 6 files changed, 112 insertions(+), 21 deletions(-) diff --git a/flutter/lib/common/shared_state.dart b/flutter/lib/common/shared_state.dart index 9e741846f..4cfe219e4 100644 --- a/flutter/lib/common/shared_state.dart +++ b/flutter/lib/common/shared_state.dart @@ -179,3 +179,25 @@ class RemoteCursorMovedState { static RxBool find(String id) => Get.find(tag: tag(id)); } + +class RemoteCountState { + static String tag() => 'remote_count_'; + + static void init() { + final key = tag(); + if (!Get.isRegistered(tag: key)) { + // Server side, default true + final RxInt state = 1.obs; + Get.put(state, tag: key); + } + } + + static void delete() { + final key = tag(); + if (Get.isRegistered(tag: key)) { + Get.delete(tag: key); + } + } + + static RxInt find() => Get.find(tag: tag()); +} diff --git a/flutter/lib/desktop/pages/desktop_tab_page.dart b/flutter/lib/desktop/pages/desktop_tab_page.dart index 82a9227c7..8672a1dad 100644 --- a/flutter/lib/desktop/pages/desktop_tab_page.dart +++ b/flutter/lib/desktop/pages/desktop_tab_page.dart @@ -9,6 +9,8 @@ import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; import 'package:get/get.dart'; import 'package:window_manager/window_manager.dart'; +import '../../common/shared_state.dart'; + class DesktopTabPage extends StatefulWidget { const DesktopTabPage({Key? key}) : super(key: key); @@ -40,6 +42,7 @@ class _DesktopTabPageState extends State { void initState() { super.initState(); Get.put(tabController); + RemoteCountState.init(); tabController.add(TabInfo( key: kTabLabelHomePage, label: kTabLabelHomePage, diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 52f474fbd..396e736e0 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -28,11 +28,13 @@ class RemotePage extends StatefulWidget { const RemotePage({ Key? key, required this.id, + required this.windowId, required this.tabBarHeight, required this.windowBorderWidth, }) : super(key: key); final String id; + final int windowId; final double tabBarHeight; final double windowBorderWidth; @@ -239,6 +241,7 @@ class _RemotePageState extends State paints.add(QualityMonitor(_ffi.qualityMonitorModel)); paints.add(RemoteMenubar( id: widget.id, + windowId: widget.windowId, ffi: _ffi, onEnterOrLeaveImageSetter: (func) => _onEnterOrLeaveImage4Menubar = func, onEnterOrLeaveImageCleaner: () => _onEnterOrLeaveImage4Menubar = null, diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index 7b38488fb..352e4682a 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -32,6 +32,7 @@ class _ConnectionTabPageState extends State { var connectionMap = RxList.empty(growable: true); _ConnectionTabPageState(Map params) { + RemoteCountState.init(); final RxBool fullscreen = Get.find(tag: 'fullscreen'); final peerId = params['id']; if (peerId != null) { @@ -45,10 +46,12 @@ class _ConnectionTabPageState extends State { page: Obx(() => RemotePage( key: ValueKey(peerId), id: peerId, + windowId: windowId(), tabBarHeight: fullscreen.isTrue ? 0 : kDesktopRemoteTabBarHeight, windowBorderWidth: fullscreen.isTrue ? 0 : kWindowBorderWidth, )))); + _update_remote_count(); } } @@ -79,6 +82,7 @@ class _ConnectionTabPageState extends State { page: Obx(() => RemotePage( key: ValueKey(id), id: id, + windowId: windowId(), tabBarHeight: fullscreen.isTrue ? 0 : kDesktopRemoteTabBarHeight, windowBorderWidth: fullscreen.isTrue ? 0 : kWindowBorderWidth, @@ -86,6 +90,7 @@ class _ConnectionTabPageState extends State { } else if (call.method == "onDestroy") { tabController.clear(); } + _update_remote_count(); }); } @@ -161,6 +166,7 @@ class _ConnectionTabPageState extends State { WindowController.fromWindowId(windowId()).hide(); } ConnectionTypeState.delete(id); + _update_remote_count(); } int windowId() { @@ -178,7 +184,7 @@ class _ConnectionTabPageState extends State { } Future handleWindowCloseButton() async { - final connLength = tabController.state.value.tabs.length; + final connLength = tabController.length; if (connLength < 1) { return true; } else if (connLength == 1) { @@ -189,8 +195,12 @@ class _ConnectionTabPageState extends State { final res = await closeConfirmDialog(); if (res) { tabController.clear(); + _update_remote_count(); } return res; } } + + _update_remote_count() => + RemoteCountState.find().value = tabController.length; } diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index d524ef279..e06af22da 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -8,6 +8,7 @@ import 'package:flutter_hbb/models/chat_model.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:rxdart/rxdart.dart' as rxdart; +import 'package:desktop_multi_window/desktop_multi_window.dart'; import '../../common.dart'; import '../../mobile/widgets/dialog.dart'; @@ -26,6 +27,7 @@ class _MenubarTheme { class RemoteMenubar extends StatefulWidget { final String id; + final int windowId; final FFI ffi; final Function(Function(bool)) onEnterOrLeaveImageSetter; final Function() onEnterOrLeaveImageCleaner; @@ -33,6 +35,7 @@ class RemoteMenubar extends StatefulWidget { const RemoteMenubar({ Key? key, required this.id, + required this.windowId, required this.ffi, required this.onEnterOrLeaveImageSetter, required this.onEnterOrLeaveImageCleaner, @@ -306,25 +309,29 @@ class _RemoteMenubarState extends State { return {'supportedHwcodec': supportedHwcodec}; }(), builder: (context, snapshot) { if (snapshot.hasData) { - return mod_menu.PopupMenuButton( - padding: EdgeInsets.zero, - icon: const Icon( - Icons.tv, - color: _MenubarTheme.commonColor, - ), - tooltip: translate('Display Settings'), - position: mod_menu.PopupMenuPosition.under, - itemBuilder: (BuildContext context) => _getDisplayMenu(snapshot.data!) - .map((entry) => entry.build( - context, - const MenuConfig( - commonColor: _MenubarTheme.commonColor, - height: _MenubarTheme.height, - dividerHeight: _MenubarTheme.dividerHeight, - ))) - .expand((i) => i) - .toList(), - ); + return Obx(() { + final remoteCount = RemoteCountState.find().value; + return mod_menu.PopupMenuButton( + padding: EdgeInsets.zero, + icon: const Icon( + Icons.tv, + color: _MenubarTheme.commonColor, + ), + tooltip: translate('Display Settings'), + position: mod_menu.PopupMenuPosition.under, + itemBuilder: (BuildContext context) => + _getDisplayMenu(snapshot.data!, remoteCount) + .map((entry) => entry.build( + context, + const MenuConfig( + commonColor: _MenubarTheme.commonColor, + height: _MenubarTheme.height, + dividerHeight: _MenubarTheme.dividerHeight, + ))) + .expand((i) => i) + .toList(), + ); + }); } else { return const Offstage(); } @@ -586,7 +593,15 @@ class _RemoteMenubarState extends State { return displayMenu; } - List> _getDisplayMenu(dynamic futureData) { + bool _isWindowCanBeAdjusted(int remoteCount) { + final RxBool fullscreen = Get.find(tag: 'fullscreen'); + return remoteCount == 1 && + fullscreen.isFalse && + widget.ffi.canvasModel.scale > 1.0; + } + + List> _getDisplayMenu( + dynamic futureData, int remoteCount) { const EdgeInsets padding = EdgeInsets.only(left: 18.0, right: 8.0); final displayMenu = [ MenuEntryRadios( @@ -739,6 +754,42 @@ class _RemoteMenubarState extends State { MenuEntryDivider(), ]; + if (_isWindowCanBeAdjusted(remoteCount)) { + displayMenu.insert( + 0, + MenuEntryDivider(), + ); + displayMenu.insert( + 0, + MenuEntryButton( + childBuilder: (TextStyle? style) => Container( + alignment: AlignmentDirectional.center, + height: _MenubarTheme.height, + child: Text( + translate('Adjust Window'), + style: style, + )), + proc: () { + () async { + final wndRect = + await WindowController.fromWindowId(widget.windowId) + .getFrame(); + final canvasModel = widget.ffi.canvasModel; + final width = + canvasModel.size.width + canvasModel.windowBorderWidth * 2; + final height = canvasModel.size.height + + canvasModel.tabBarHeight + + canvasModel.windowBorderWidth * 2; + await WindowController.fromWindowId(widget.windowId).setFrame( + Rect.fromLTWH(wndRect.left, wndRect.top, width, height)); + }(); + }, + padding: padding, + dismissOnClicked: true, + ), + ); + } + /// Show Codec Preference if (bind.mainHasHwcodec()) { final List codecs = []; diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 9bfc010ec..a98b3af20 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -69,6 +69,8 @@ class DesktopTabController { DesktopTabController({required this.tabType}); + int get length => state.value.tabs.length; + void add(TabInfo tab, {bool authorized = false}) { if (!isDesktop) return; final index = state.value.tabs.indexWhere((e) => e.key == tab.key); From 2cc92c199c7d33baae491e76da73410b0b89d54e Mon Sep 17 00:00:00 2001 From: fufesou Date: Sun, 9 Oct 2022 19:27:30 +0800 Subject: [PATCH 0660/2015] flutter_desktop: adjust window Signed-off-by: fufesou --- flutter/lib/common.dart | 94 +++- flutter/lib/consts.dart | 6 + .../lib/desktop/pages/desktop_home_page.dart | 23 + .../lib/desktop/widgets/remote_menubar.dart | 117 ++++- flutter/pubspec.lock | 410 +++++++++--------- flutter/pubspec.yaml | 7 +- 6 files changed, 426 insertions(+), 231 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 2ff3d4477..d68467d91 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; +import 'dart:ui'; import 'package:back_button_interceptor/back_button_interceptor.dart'; import 'package:desktop_multi_window/desktop_multi_window.dart'; @@ -15,6 +16,7 @@ import 'package:get/get.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:window_manager/window_manager.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:window_size/window_size.dart' as window_size; import 'common/widgets/overlay.dart'; import 'mobile/pages/file_manager_page.dart'; @@ -23,6 +25,8 @@ import 'models/input_model.dart'; import 'models/model.dart'; import 'models/platform_model.dart'; +import '../consts.dart'; + final globalKey = GlobalKey(); final navigationBarKey = GlobalKey(); @@ -1022,6 +1026,85 @@ Future saveWindowPosition(WindowType type, {int? windowId}) async { } } +_adjustRestoreMainWindowSize(double? width, double? height) async { + const double minWidth = 600; + const double minHeight = 100; + double maxWidth = ((isDesktop || isWebDesktop) + ? kDesktopMaxDisplayWidth + : kMobileMaxDisplayWidth) as double; + double maxHeight = ((isDesktop || isWebDesktop) + ? kDesktopMaxDisplayHeight + : kMobileMaxDisplayHeight) as double; + + if (isDesktop || isWebDesktop) { + final screen = (await window_size.getWindowInfo()).screen; + if (screen != null) { + maxWidth = screen.visibleFrame.width; + maxHeight = screen.visibleFrame.height; + } + } + + final defaultWidth = ((isDesktop || isWebDesktop) + ? 1280 + : kMobileDefaultDisplayWidth) as double; + final defaultHeight = ((isDesktop || isWebDesktop) + ? 720 + : kMobileDefaultDisplayHeight) as double; + double restoreWidth = width ?? defaultWidth; + double restoreHeight = height ?? defaultHeight; + + if (restoreWidth < minWidth) { + restoreWidth = minWidth; + } + if (restoreHeight < minHeight) { + restoreHeight = minHeight; + } + if (restoreWidth > maxWidth) { + restoreWidth = maxWidth; + } + if (restoreHeight > maxHeight) { + restoreWidth = maxHeight; + } + await windowManager.setSize(Size(restoreWidth, restoreHeight)); +} + +_adjustRestoreMainWindowOffset(double? left, double? top) async { + if (left == null || top == null) { + await windowManager.center(); + } else { + double windowLeft = left; + double windowTop = top; + + double frameLeft = 0; + double frameTop = 0; + double frameRight = ((isDesktop || isWebDesktop) + ? kDesktopMaxDisplayWidth + : kMobileMaxDisplayWidth) as double; + double frameBottom = ((isDesktop || isWebDesktop) + ? kDesktopMaxDisplayHeight + : kMobileMaxDisplayHeight) as double; + + if (isDesktop || isWebDesktop) { + final screen = (await window_size.getWindowInfo()).screen; + if (screen != null) { + frameLeft = screen.visibleFrame.left; + frameTop = screen.visibleFrame.top; + frameRight = screen.visibleFrame.right; + frameBottom = screen.visibleFrame.bottom; + } + } + + if (windowLeft < frameLeft || + windowLeft > frameRight || + windowTop < frameTop || + windowTop > frameBottom) { + await windowManager.center(); + } else { + await windowManager.setPosition(Offset(windowLeft, windowTop)); + } + } +} + /// Save window position and size on exit /// Note that windowId must be provided if it's subwindow Future restoreWindowPosition(WindowType type, {int? windowId}) async { @@ -1042,13 +1125,10 @@ Future restoreWindowPosition(WindowType type, {int? windowId}) async { debugPrint("window position saved, but cannot be parsed"); return false; } - await windowManager.setSize(Size(lpos.width ?? 1280, lpos.height ?? 720)); - if (lpos.offsetWidth == null || lpos.offsetHeight == null) { - await windowManager.center(); - } else { - await windowManager - .setPosition(Offset(lpos.offsetWidth!, lpos.offsetHeight!)); - } + + await _adjustRestoreMainWindowSize(lpos.width, lpos.height); + await _adjustRestoreMainWindowOffset(lpos.offsetWidth, lpos.offsetHeight); + return true; default: // TODO: implement subwindow diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 9e46db0d2..d31f77111 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -20,6 +20,12 @@ const int kMobileDefaultDisplayHeight = 1280; const int kDesktopDefaultDisplayWidth = 1080; const int kDesktopDefaultDisplayHeight = 720; +const int kMobileMaxDisplayWidth = 720; +const int kMobileMaxDisplayHeight = 1280; + +const int kDesktopMaxDisplayWidth = 1920; +const int kDesktopMaxDisplayHeight = 1080; + 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/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index fcc8c4991..3b5f4426b 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:io'; +import 'dart:convert'; import 'package:flutter/material.dart' hide MenuItem; import 'package:flutter/services.dart'; @@ -16,6 +17,7 @@ import 'package:provider/provider.dart'; import 'package:tray_manager/tray_manager.dart'; import 'package:window_manager/window_manager.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'package:window_size/window_size.dart' as window_size; import '../widgets/button.dart'; @@ -427,6 +429,27 @@ class _DesktopHomePageState extends State "call ${call.method} with args ${call.arguments} from window $fromWindowId"); if (call.method == "main_window_on_top") { window_on_top(null); + } else if (call.method == "get_window_info") { + final screen = (await window_size.getWindowInfo()).screen; + if (screen == null) { + return ""; + } else { + return jsonEncode({ + 'frame': { + 'l': screen.frame.left, + 't': screen.frame.top, + 'r': screen.frame.right, + 'b': screen.frame.bottom, + }, + 'visibleFrame': { + 'l': screen.visibleFrame.left, + 't': screen.visibleFrame.top, + 'r': screen.visibleFrame.right, + 'b': screen.visibleFrame.bottom, + }, + 'scaleFactor': screen.scaleFactor, + }); + } } }); } diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index e06af22da..529c2657f 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:io'; import 'dart:math' as math; +import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -9,6 +10,7 @@ import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:rxdart/rxdart.dart' as rxdart; import 'package:desktop_multi_window/desktop_multi_window.dart'; +import 'package:window_size/window_size.dart' as window_size; import '../../common.dart'; import '../../mobile/widgets/dialog.dart'; @@ -51,6 +53,7 @@ class _RemoteMenubarState extends State { final _rxHideReplay = rxdart.ReplaySubject(); final _pinMenubar = false.obs; bool _isCursorOverImage = false; + window_size.Screen? _screen; bool get isFullscreen => Get.find(tag: 'fullscreen').isTrue; void _setFullscreen(bool v) { @@ -108,12 +111,34 @@ class _RemoteMenubarState extends State { }, onPressed: () { _show.value = !_show.value; + if (_show.isTrue) { + _updateScreen(); + } }, child: Obx(() => Container( color: _hideColor.value, ).marginOnly(bottom: 8.0)))))); } + _updateScreen() async { + final v = await DesktopMultiWindow.invokeMethod(0, "get_window_info", ""); + final String valueStr = v; + if (valueStr.isEmpty) { + _screen = null; + } else { + final screenMap = jsonDecode(valueStr); + _screen = window_size.Screen( + Rect.fromLTRB(screenMap['frame']['l'], screenMap['frame']['t'], + screenMap['frame']['r'], screenMap['frame']['b']), + Rect.fromLTRB( + screenMap['visibleFrame']['l'], + screenMap['visibleFrame']['t'], + screenMap['visibleFrame']['r'], + screenMap['visibleFrame']['b']), + screenMap['scaleFactor']); + } + } + Widget _buildMenubar(BuildContext context) { final List menubarItems = []; if (!isWebDesktop) { @@ -594,10 +619,30 @@ class _RemoteMenubarState extends State { } bool _isWindowCanBeAdjusted(int remoteCount) { + if (remoteCount != 1) { + return false; + } + if (_screen == null) { + return false; + } + double scale = _screen!.scaleFactor; + double selfWidth = _screen!.frame.width; + double selfHeight = _screen!.frame.height; final RxBool fullscreen = Get.find(tag: 'fullscreen'); - return remoteCount == 1 && - fullscreen.isFalse && - widget.ffi.canvasModel.scale > 1.0; + if (fullscreen.isFalse) { + selfWidth = _screen!.visibleFrame.width; + selfHeight = _screen!.visibleFrame.height; + } + + final canvasModel = widget.ffi.canvasModel; + final displayWidth = canvasModel.getDisplayWidth(); + final displayHeight = canvasModel.getDisplayHeight(); + final requiredWidth = displayWidth + + (canvasModel.tabBarHeight + canvasModel.windowBorderWidth * 2); + final requiredHeight = displayHeight + + (canvasModel.tabBarHeight + canvasModel.windowBorderWidth * 2); + return selfWidth > (requiredWidth * scale) && + selfHeight > (requiredHeight * scale); } List> _getDisplayMenu( @@ -763,25 +808,59 @@ class _RemoteMenubarState extends State { 0, MenuEntryButton( childBuilder: (TextStyle? style) => Container( - alignment: AlignmentDirectional.center, - height: _MenubarTheme.height, child: Text( - translate('Adjust Window'), - style: style, - )), + translate('Adjust Window'), + style: style, + )), proc: () { () async { - final wndRect = - await WindowController.fromWindowId(widget.windowId) - .getFrame(); - final canvasModel = widget.ffi.canvasModel; - final width = - canvasModel.size.width + canvasModel.windowBorderWidth * 2; - final height = canvasModel.size.height + - canvasModel.tabBarHeight + - canvasModel.windowBorderWidth * 2; - await WindowController.fromWindowId(widget.windowId).setFrame( - Rect.fromLTWH(wndRect.left, wndRect.top, width, height)); + await _updateScreen(); + if (_screen != null) { + double scale = _screen!.scaleFactor; + final wndRect = + await WindowController.fromWindowId(widget.windowId) + .getFrame(); + final mediaSize = MediaQueryData.fromWindow(ui.window).size; + // On windows, wndRect is equal to GetWindowRect and mediaSize is equal to GetClientRect. + // https://stackoverflow.com/a/7561083 + double magicWidth = + wndRect.right - wndRect.left - mediaSize.width * scale; + double magicHeight = + wndRect.bottom - wndRect.top - mediaSize.height * scale; + + final canvasModel = widget.ffi.canvasModel; + final width = (canvasModel.getDisplayWidth() + + canvasModel.windowBorderWidth * 2) * + scale + + magicWidth; + final height = (canvasModel.getDisplayHeight() + + canvasModel.tabBarHeight + + canvasModel.windowBorderWidth * 2) * + scale + + magicHeight; + double left = wndRect.left + (wndRect.width - width) / 2; + double top = wndRect.top + (wndRect.height - height) / 2; + + Rect frameRect = _screen!.frame; + final RxBool fullscreen = Get.find(tag: 'fullscreen'); + if (fullscreen.isFalse) { + frameRect = _screen!.visibleFrame; + } + if (left < frameRect.left) { + left = frameRect.left; + } + if (top < frameRect.top) { + top = frameRect.top; + } + if ((left + width) > frameRect.right) { + left = frameRect.right - width; + } + if ((top + height) > frameRect.bottom) { + top = frameRect.bottom - height; + } + await WindowController.fromWindowId(widget.windowId) + .setFrame(Rect.fromLTWH(left, top, width, height)); + } }(); }, padding: padding, diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 755d551db..b3308bb9d 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -5,246 +5,246 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "47.0.0" + version: "49.0.0" after_layout: dependency: transitive description: name: after_layout - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.0" analyzer: dependency: transitive description: name: analyzer - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "4.7.0" + version: "5.1.0" animations: dependency: transitive description: name: animations - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "2.0.4" + version: "2.0.7" archive: dependency: transitive description: name: archive - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.3.1" args: dependency: transitive description: name: args - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.3.1" async: dependency: transitive description: name: async - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.9.0" back_button_interceptor: dependency: "direct main" description: name: back_button_interceptor - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "6.0.1" build: dependency: transitive description: name: build - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.3.1" build_config: dependency: transitive description: name: build_config - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.0" build_daemon: dependency: transitive description: name: build_daemon - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.0" build_resolvers: dependency: transitive description: name: build_resolvers - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.10" build_runner: dependency: "direct dev" description: name: build_runner - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.1" build_runner_core: dependency: transitive description: name: build_runner_core - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "7.2.4" built_collection: dependency: transitive description: name: built_collection - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "5.1.1" built_value: dependency: transitive description: name: built_value - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "8.4.1" cached_network_image: dependency: transitive description: name: cached_network_image - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.2.1" cached_network_image_platform_interface: dependency: transitive description: name: cached_network_image_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.0" cached_network_image_web: dependency: transitive description: name: cached_network_image_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.1" characters: dependency: transitive description: name: characters - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.0" charcode: dependency: transitive description: name: charcode - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.1" checked_yaml: dependency: transitive description: name: checked_yaml - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.1" clock: dependency: transitive description: name: clock - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.0" code_builder: dependency: transitive description: name: code_builder - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "4.3.0" collection: dependency: transitive description: name: collection - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.16.0" contextmenu: dependency: "direct main" description: name: contextmenu - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.0" convert: dependency: transitive description: name: convert - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.2" cross_file: dependency: transitive description: name: cross_file - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.3.3+2" crypto: dependency: transitive description: name: crypto - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.2" csslib: dependency: transitive description: name: csslib - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.17.2" cupertino_icons: dependency: "direct main" description: name: cupertino_icons - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.5" dart_style: dependency: transitive description: name: dart_style - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.4" dash_chat_2: dependency: "direct main" description: name: dash_chat_2 - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "0.0.14" + version: "0.0.15" desktop_drop: dependency: "direct main" description: name: desktop_drop - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.3.3" desktop_multi_window: dependency: "direct main" description: path: "." - ref: "74c10aa49fecc088992a9edef1f6da39f83df505" - resolved-ref: "74c10aa49fecc088992a9edef1f6da39f83df505" + ref: "0019311e8aba4e84ffd44c57ba1834cc76924f2a" + resolved-ref: "0019311e8aba4e84ffd44c57ba1834cc76924f2a" url: "https://github.com/Kingtous/rustdesk_desktop_multi_window" source: git version: "0.1.0" @@ -252,91 +252,91 @@ packages: dependency: "direct main" description: name: device_info_plus - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "4.1.2" + version: "4.1.3" device_info_plus_linux: dependency: transitive description: name: device_info_plus_linux - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.0" device_info_plus_macos: dependency: transitive description: name: device_info_plus_macos - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.0" device_info_plus_platform_interface: dependency: transitive description: name: device_info_plus_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.0" device_info_plus_web: dependency: transitive description: name: device_info_plus_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.0" device_info_plus_windows: dependency: transitive description: name: device_info_plus_windows - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "4.1.0" draggable_float_widget: dependency: "direct main" description: name: draggable_float_widget - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.0.2" event_bus: dependency: transitive description: name: event_bus - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.0" external_path: dependency: "direct main" description: name: external_path - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.3" ffi: dependency: "direct main" description: name: ffi - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.1" file: dependency: transitive description: name: file - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "6.1.4" file_picker: dependency: "direct main" description: name: file_picker - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "5.1.0" + version: "5.2.0+1" fixnum: dependency: transitive description: name: fixnum - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.1" flutter: @@ -348,29 +348,29 @@ packages: dependency: transitive description: name: flutter_blurhash - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.7.0" flutter_breadcrumb: dependency: "direct main" description: name: flutter_breadcrumb - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.1" flutter_cache_manager: dependency: transitive description: name: flutter_cache_manager - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.3.0" flutter_custom_cursor: dependency: "direct main" description: path: "." - ref: "527821d676017387be024dffd61898ff79b14c41" - resolved-ref: "527821d676017387be024dffd61898ff79b14c41" + ref: "4a950fd3a5a228bf5381070a4c803919d5787c07" + resolved-ref: "4a950fd3a5a228bf5381070a4c803919d5787c07" url: "https://github.com/Kingtous/rustdesk_flutter_custom_cursor" source: git version: "0.0.1" @@ -378,14 +378,14 @@ packages: dependency: "direct main" description: name: flutter_improved_scrolling - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.0.3" flutter_lints: dependency: "direct dev" description: name: flutter_lints - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.1" flutter_localizations: @@ -397,14 +397,14 @@ packages: dependency: transitive description: name: flutter_parsed_text - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.1" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.7" flutter_rust_bridge: @@ -420,7 +420,7 @@ packages: dependency: "direct main" description: name: flutter_svg - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.5" flutter_web_plugins: @@ -432,413 +432,406 @@ packages: dependency: "direct dev" description: name: freezed - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "2.1.0+1" + version: "2.1.1" freezed_annotation: dependency: "direct main" description: name: freezed_annotation - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0" frontend_server_client: dependency: transitive description: name: frontend_server_client - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.3" get: dependency: "direct main" description: name: get - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "4.6.5" glob: dependency: transitive description: name: glob - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0" graphs: dependency: transitive description: name: graphs - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0" html: dependency: transitive description: name: html - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.15.0" http: dependency: "direct main" description: name: http - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.13.5" http_multi_server: dependency: transitive description: name: http_multi_server - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.2.1" http_parser: dependency: transitive description: name: http_parser - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "4.0.1" icons_launcher: dependency: "direct dev" description: name: icons_launcher - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.4" image: dependency: "direct main" description: name: image - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.2.0" image_picker: dependency: "direct main" description: name: image_picker - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "0.8.5+3" + version: "0.8.6" image_picker_android: dependency: transitive description: name: image_picker_android - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.8.5+3" image_picker_for_web: dependency: transitive description: name: image_picker_for_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "2.1.8" + version: "2.1.10" image_picker_ios: dependency: transitive description: name: image_picker_ios - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "0.8.6" + version: "0.8.6+1" image_picker_platform_interface: dependency: transitive description: name: image_picker_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "2.6.1" + version: "2.6.2" intl: dependency: transitive description: name: intl - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.17.0" io: dependency: transitive description: name: io - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.3" js: dependency: transitive description: name: js - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.6.4" json_annotation: dependency: transitive description: name: json_annotation - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "4.6.0" + version: "4.7.0" lints: dependency: transitive description: name: lints - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.0" logging: dependency: transitive description: name: logging - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "1.0.2" + version: "1.1.0" matcher: dependency: transitive description: name: matcher - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.12.12" material_color_utilities: dependency: transitive description: name: material_color_utilities - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.1.4" menu_base: dependency: transitive description: name: menu_base - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.1.1" meta: dependency: transitive description: name: meta - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.7.0" mime: dependency: transitive description: name: mime - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.2" nested: dependency: transitive description: name: nested - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.0" octo_image: dependency: transitive description: name: octo_image - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.2" package_config: dependency: transitive description: name: package_config - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0" package_info_plus: dependency: "direct main" description: name: package_info_plus - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.4.3+1" package_info_plus_linux: dependency: transitive description: name: package_info_plus_linux - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.5" package_info_plus_macos: dependency: transitive description: name: package_info_plus_macos - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.0" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.2" package_info_plus_web: dependency: transitive description: name: package_info_plus_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "1.0.5" + version: "1.0.6" package_info_plus_windows: dependency: transitive description: name: package_info_plus_windows - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0" path: dependency: transitive description: name: path - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.8.1" path_drawing: dependency: transitive description: name: path_drawing - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.1" path_parsing: dependency: transitive description: name: path_parsing - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.1" path_provider: dependency: "direct main" description: name: path_provider - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.11" path_provider_android: dependency: transitive description: name: path_provider_android - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.20" path_provider_ios: dependency: transitive description: name: path_provider_ios - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.11" path_provider_linux: dependency: transitive description: name: path_provider_linux - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.7" path_provider_macos: dependency: transitive description: name: path_provider_macos - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.6" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "2.0.4" + version: "2.0.5" path_provider_windows: dependency: transitive description: name: path_provider_windows - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.3" pedantic: dependency: transitive description: name: pedantic - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.11.1" petitparser: dependency: transitive description: name: petitparser - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "5.0.0" platform: dependency: transitive description: name: platform - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.0" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.3" pool: dependency: transitive description: name: pool - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.5.1" process: dependency: transitive description: name: process - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "4.2.4" provider: dependency: "direct main" description: name: provider - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "6.0.3" pub_semver: dependency: transitive description: name: pub_semver - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.1" pubspec_parse: dependency: transitive description: name: pubspec_parse - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.1" qr_code_scanner: dependency: "direct main" description: name: qr_code_scanner - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.1" - quiver: - dependency: transitive - description: - name: quiver - url: "https://pub.dartlang.org" - source: hosted - version: "3.1.0" rxdart: dependency: "direct main" description: name: rxdart - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.27.5" screen_retriever: @@ -854,91 +847,91 @@ packages: dependency: "direct main" description: name: scroll_pos - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.3.0" settings_ui: dependency: "direct main" description: name: settings_ui - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.2" shared_preferences: dependency: "direct main" description: name: shared_preferences - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.15" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.13" shared_preferences_ios: dependency: transitive description: name: shared_preferences_ios - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.1" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.1" shared_preferences_macos: dependency: transitive description: name: shared_preferences_macos - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.4" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.4" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.1" shelf: dependency: transitive description: name: shelf - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "1.3.2" + version: "1.4.0" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.2" shortid: dependency: transitive description: name: shortid - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.1.2" sky_engine: @@ -950,84 +943,84 @@ packages: dependency: transitive description: name: source_gen - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "1.2.3" + version: "1.2.5" source_span: dependency: transitive description: name: source_span - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.9.1" sqflite: dependency: transitive description: name: sqflite - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.3+1" sqflite_common: dependency: transitive description: name: sqflite_common - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "2.2.1+1" + version: "2.3.0" stack_trace: dependency: transitive description: name: stack_trace - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.10.0" stream_channel: dependency: transitive description: name: stream_channel - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "2.1.0" + version: "2.1.1" stream_transform: dependency: transitive description: name: stream_transform - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "2.0.0" + version: "2.0.1" string_scanner: dependency: transitive description: name: string_scanner - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.1" synchronized: dependency: transitive description: name: synchronized - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.0+3" term_glyph: dependency: transitive description: name: term_glyph - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.1" timing: dependency: transitive description: name: timing - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.0" toggle_switch: dependency: "direct main" description: name: toggle_switch - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.4.0" tray_manager: @@ -1043,189 +1036,189 @@ packages: dependency: "direct main" description: name: tuple - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "2.0.0" + version: "2.0.1" typed_data: dependency: transitive description: name: typed_data - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.1" universal_io: dependency: transitive description: name: universal_io - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.4" url_launcher: dependency: "direct main" description: name: url_launcher - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "6.1.5" + version: "6.1.6" url_launcher_android: dependency: transitive description: name: url_launcher_android - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "6.0.19" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "6.0.17" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.1" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "2.1.0" + version: "2.1.1" url_launcher_web: dependency: transitive description: name: url_launcher_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.13" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.1" uuid: dependency: transitive description: name: uuid - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.6" vector_math: dependency: transitive description: name: vector_math - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.2" video_player: dependency: transitive description: name: video_player - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.4.7" video_player_android: dependency: transitive description: name: video_player_android - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.3.9" video_player_avfoundation: dependency: transitive description: name: video_player_avfoundation - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "2.3.5" + version: "2.3.7" video_player_platform_interface: dependency: transitive description: name: video_player_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "5.1.4" video_player_web: dependency: transitive description: name: video_player_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.12" visibility_detector: dependency: "direct main" description: name: visibility_detector - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.3.3" wakelock: dependency: "direct main" description: name: wakelock - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.6.2" wakelock_macos: dependency: transitive description: name: wakelock_macos - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.4.0" wakelock_platform_interface: dependency: transitive description: name: wakelock_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.3.0" wakelock_web: dependency: transitive description: name: wakelock_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.4.0" wakelock_windows: dependency: transitive description: name: wakelock_windows - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.2.1" watcher: dependency: transitive description: name: watcher - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.1" web_socket_channel: dependency: transitive description: name: web_socket_channel - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.0" win32: dependency: transitive description: name: win32 - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.0" window_manager: @@ -1237,32 +1230,41 @@ packages: url: "https://github.com/Kingtous/rustdesk_window_manager" source: git version: "0.2.7" + window_size: + dependency: "direct main" + description: + path: "plugins/window_size" + ref: a738913c8ce2c9f47515382d40827e794a334274 + resolved-ref: a738913c8ce2c9f47515382d40827e794a334274 + url: "https://github.com/google/flutter-desktop-embedding.git" + source: git + version: "0.1.0" xdg_directories: dependency: transitive description: name: xdg_directories - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.2.0+2" xml: dependency: transitive description: name: xml - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "6.1.0" yaml: dependency: transitive description: name: yaml - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.1" zxing2: dependency: "direct main" description: name: zxing2 - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.1.0" sdks: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index fc62df908..36feee2a5 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -73,7 +73,12 @@ dependencies: flutter_custom_cursor: git: url: https://github.com/Kingtous/rustdesk_flutter_custom_cursor - ref: 4a950fd3a5a228bf5381070a4c803919d5787c07 + ref: 4a950fd3a5a228bf5381070a4c803919d5787c07 + window_size: + git: + url: https://github.com/google/flutter-desktop-embedding.git + path: plugins/window_size + ref: a738913c8ce2c9f47515382d40827e794a334274 get: ^4.6.5 visibility_detector: ^0.3.3 contextmenu: ^3.0.0 From 8ec565d5a071ecb933ecb8edb0ca0a99a9c236a1 Mon Sep 17 00:00:00 2001 From: fufesou Date: Mon, 10 Oct 2022 09:56:27 +0800 Subject: [PATCH 0661/2015] flutter_desktop: adjust window, debug done Signed-off-by: fufesou --- flutter/lib/common.dart | 34 +++++++++++-------- .../lib/desktop/widgets/remote_menubar.dart | 4 +-- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index d68467d91..8b8403954 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1029,12 +1029,14 @@ Future saveWindowPosition(WindowType type, {int? windowId}) async { _adjustRestoreMainWindowSize(double? width, double? height) async { const double minWidth = 600; const double minHeight = 100; - double maxWidth = ((isDesktop || isWebDesktop) - ? kDesktopMaxDisplayWidth - : kMobileMaxDisplayWidth) as double; + double maxWidth = (((isDesktop || isWebDesktop) + ? kDesktopMaxDisplayWidth + : kMobileMaxDisplayWidth)) + .toDouble(); double maxHeight = ((isDesktop || isWebDesktop) - ? kDesktopMaxDisplayHeight - : kMobileMaxDisplayHeight) as double; + ? kDesktopMaxDisplayHeight + : kMobileMaxDisplayHeight) + .toDouble(); if (isDesktop || isWebDesktop) { final screen = (await window_size.getWindowInfo()).screen; @@ -1044,12 +1046,12 @@ _adjustRestoreMainWindowSize(double? width, double? height) async { } } - final defaultWidth = ((isDesktop || isWebDesktop) - ? 1280 - : kMobileDefaultDisplayWidth) as double; - final defaultHeight = ((isDesktop || isWebDesktop) - ? 720 - : kMobileDefaultDisplayHeight) as double; + final defaultWidth = + ((isDesktop || isWebDesktop) ? 1280 : kMobileDefaultDisplayWidth) + .toDouble(); + final defaultHeight = + ((isDesktop || isWebDesktop) ? 720 : kMobileDefaultDisplayHeight) + .toDouble(); double restoreWidth = width ?? defaultWidth; double restoreHeight = height ?? defaultHeight; @@ -1078,11 +1080,13 @@ _adjustRestoreMainWindowOffset(double? left, double? top) async { double frameLeft = 0; double frameTop = 0; double frameRight = ((isDesktop || isWebDesktop) - ? kDesktopMaxDisplayWidth - : kMobileMaxDisplayWidth) as double; + ? kDesktopMaxDisplayWidth + : kMobileMaxDisplayWidth) + .toDouble(); double frameBottom = ((isDesktop || isWebDesktop) - ? kDesktopMaxDisplayHeight - : kMobileMaxDisplayHeight) as double; + ? kDesktopMaxDisplayHeight + : kMobileMaxDisplayHeight) + .toDouble(); if (isDesktop || isWebDesktop) { final screen = (await window_size.getWindowInfo()).screen; diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 529c2657f..13aa1932f 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -628,8 +628,7 @@ class _RemoteMenubarState extends State { double scale = _screen!.scaleFactor; double selfWidth = _screen!.frame.width; double selfHeight = _screen!.frame.height; - final RxBool fullscreen = Get.find(tag: 'fullscreen'); - if (fullscreen.isFalse) { + if (isFullscreen) { selfWidth = _screen!.visibleFrame.width; selfHeight = _screen!.visibleFrame.height; } @@ -816,6 +815,7 @@ class _RemoteMenubarState extends State { () async { await _updateScreen(); if (_screen != null) { + _setFullscreen(false); double scale = _screen!.scaleFactor; final wndRect = await WindowController.fromWindowId(widget.windowId) From 87ee35953633ec770f65fb25ddbea8624eb3043a Mon Sep 17 00:00:00 2001 From: Chieh Wang Date: Fri, 7 Oct 2022 13:48:46 +0800 Subject: [PATCH 0662/2015] Feat: Grab hot key --- Cargo.lock | 83 ++++++++++++++++- src/ui_session_interface.rs | 176 ++++++++++++++++++++++++++++++++---- 2 files changed, 238 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 55949fe8b..4348e05cb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1514,6 +1514,16 @@ dependencies = [ "termcolor", ] +[[package]] +name = "epoll" +version = "4.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20df693c700404f7e19d4d6fae6b15215d2913c27955d2b9d6f2c0f537511cd0" +dependencies = [ + "bitflags", + "libc", +] + [[package]] name = "err-derive" version = "0.3.1" @@ -1569,6 +1579,29 @@ dependencies = [ "nix 0.23.1", ] +[[package]] +name = "evdev-rs" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46504075975d14f0463e5a41efa06820c94d4c04fecd01f70b95365d60de1caf" +dependencies = [ + "bitflags", + "evdev-sys", + "libc", + "log", +] + +[[package]] +name = "evdev-sys" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14ead42b547b15d47089c1243d907bcf0eb94e457046d3b315a26ac9c9e9ea6d" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "event-listener" version = "2.5.3" @@ -2574,6 +2607,26 @@ dependencies = [ "hashbrown 0.12.3", ] +[[package]] +name = "inotify" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abf888f9575c290197b2c948dc9e9ff10bd1a39ad1ea8585f734585fa6b9d3f9" +dependencies = [ + "bitflags", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + [[package]] name = "instant" version = "0.1.12" @@ -4073,15 +4126,20 @@ dependencies = [ [[package]] name = "rdev" version = "0.5.0-2" -source = "git+https://github.com/asur4s/rdev#bff57a29e3f14d032ab7441b2d6cf029df8adaca" +source = "git+https://github.com/asur4s/rdev#71a2b9e014b5aaeb85d7bb4a5b7562e3a68cc509" dependencies = [ "cocoa", "core-foundation 0.9.3", "core-foundation-sys 0.8.3", "core-graphics 0.22.3", "enum-map", + "epoll", + "evdev-rs", + "inotify", "lazy_static", "libc", + "strum 0.24.1", + "strum_macros 0.24.3", "widestring 1.0.2", "winapi 0.3.9", "x11 2.20.0", @@ -4899,6 +4957,12 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57bd81eb48f4c437cadc685403cad539345bf703d78e63707418431cecd4522b" +[[package]] +name = "strum" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" + [[package]] name = "strum_macros" version = "0.18.0" @@ -4911,6 +4975,19 @@ dependencies = [ "syn", ] +[[package]] +name = "strum_macros" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" +dependencies = [ + "heck 0.4.0", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + [[package]] name = "syn" version = "1.0.99" @@ -4993,8 +5070,8 @@ checksum = "0f3ecc17269a19353b3558b313bba738b25d82993e30d62a18406a24aba4649b" dependencies = [ "heck 0.3.3", "pkg-config", - "strum", - "strum_macros", + "strum 0.18.0", + "strum_macros 0.18.0", "thiserror", "toml", "version-compare 0.0.10", diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 9e2a09545..8288adbe0 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -13,7 +13,7 @@ use hbb_common::rendezvous_proto::ConnType; use hbb_common::tokio::{self, sync::mpsc}; #[cfg(not(any(target_os = "android", target_os = "ios")))] use rdev::Keyboard as RdevKeyboard; -use rdev::{Event, EventType::*, Key as RdevKey, KeyboardState}; +use rdev::{Event, EventType, EventType::*, Key as RdevKey, KeyboardState}; use hbb_common::{allow_err, message_proto::*}; use hbb_common::{fs, get_version_number, log, Stream}; @@ -25,7 +25,9 @@ use std::sync::{Arc, Mutex, RwLock}; /// IS_IN KEYBOARD_HOOKED sciter only pub static IS_IN: AtomicBool = AtomicBool::new(false); pub static KEYBOARD_HOOKED: AtomicBool = AtomicBool::new(true); - +pub static HOTKEY_HOOK_ENABLED: AtomicBool = AtomicBool::new(false); +#[cfg(target_os = "linux")] +use rdev::IS_GRAB; #[cfg(windows)] static mut IS_ALT_GR: bool = false; @@ -38,6 +40,21 @@ lazy_static::lazy_static! { static ref KEYBOARD: Arc> = Arc::new(Mutex::new(RdevKeyboard::new().unwrap())); } +lazy_static::lazy_static! { + static ref MUTEX_SPECIAL_KEYS: Mutex> = { + let mut m = HashMap::new(); + m.insert(RdevKey::ShiftLeft, false); + m.insert(RdevKey::ShiftRight, false); + m.insert(RdevKey::ControlLeft, false); + m.insert(RdevKey::ControlRight, false); + m.insert(RdevKey::Alt, false); + m.insert(RdevKey::AltGr, false); + m.insert(RdevKey::MetaLeft, false); + m.insert(RdevKey::MetaRight, false); + Mutex::new(m) + }; +} + #[derive(Clone, Default)] pub struct Session { pub id: String, @@ -146,7 +163,12 @@ impl Session { let decoder = scrap::codec::Decoder::video_codec_state(&self.id); let mut h264 = decoder.score_h264 > 0; let mut h265 = decoder.score_h265 > 0; - let (encoding_264, encoding_265) = self.lc.read().unwrap().supported_encoding.unwrap_or_default(); + let (encoding_264, encoding_265) = self + .lc + .read() + .unwrap() + .supported_encoding + .unwrap_or_default(); h264 = h264 && encoding_264; h265 = h265 && encoding_265; return (h264, h265); @@ -622,6 +644,7 @@ impl Session { RdevKey::Quote => '\'', RdevKey::LeftBracket => '[', RdevKey::RightBracket => ']', + RdevKey::Slash => '/', RdevKey::BackSlash => '\\', RdevKey::Minus => '-', RdevKey::Equal => '=', @@ -746,12 +769,24 @@ impl Session { } pub fn enter(&self) { + HOTKEY_HOOK_ENABLED.store(true, Ordering::SeqCst); + #[cfg(target_os = "linux")] + unsafe { + IS_GRAB.store(true, Ordering::SeqCst); + } + #[cfg(windows)] crate::platform::windows::stop_system_key_propagate(true); IS_IN.store(true, Ordering::SeqCst); } pub fn leave(&self) { + HOTKEY_HOOK_ENABLED.store(false, Ordering::SeqCst); + #[cfg(target_os = "linux")] + unsafe { + IS_GRAB.store(false, Ordering::SeqCst); + } + for key in TO_RELEASE.lock().unwrap().iter() { self.map_keyboard_mode(false, *key, None) } @@ -865,7 +900,7 @@ impl Session { ControlKey::Numpad9 => ControlKey::PageUp, _ => key, } - }else{ + } else { key }; key_event.set_control_key(key.clone()); @@ -886,6 +921,16 @@ impl Session { } } + #[cfg(not(any(target_os = "android", target_os = "ios")))] + let ctrl = + get_hotkey_state(RdevKey::ControlLeft) || get_hotkey_state(RdevKey::ControlRight); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + let shift = get_hotkey_state(RdevKey::ShiftLeft) || get_hotkey_state(RdevKey::ShiftRight); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + let command = get_hotkey_state(RdevKey::MetaLeft) || get_hotkey_state(RdevKey::MetaRight); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + let alt = get_hotkey_state(RdevKey::Alt) || get_hotkey_state(RdevKey::AltGr); + self.legacy_modifiers(&mut key_event, alt, ctrl, shift, command); if v == 1 { key_event.down = true; @@ -915,6 +960,16 @@ impl Session { } } + #[cfg(not(any(target_os = "android", target_os = "ios")))] + let ctrl = + get_hotkey_state(RdevKey::ControlLeft) || get_hotkey_state(RdevKey::ControlRight); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + let shift = get_hotkey_state(RdevKey::ShiftLeft) || get_hotkey_state(RdevKey::ShiftRight); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + let command = get_hotkey_state(RdevKey::MetaLeft) || get_hotkey_state(RdevKey::MetaRight); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + let alt = get_hotkey_state(RdevKey::Alt) || get_hotkey_state(RdevKey::AltGr); + send_mouse(mask, x, y, alt, ctrl, shift, command, self); // on macos, ctrl + left button down = right button down, up won't emit, so we need to // emit up myself if peer is not macos @@ -1164,8 +1219,11 @@ impl Interface for Session { crate::platform::windows::add_recent_document(&path); } } + // rdev::grab and rdev::listen use the same api on macOS #[cfg(not(any(target_os = "android", target_os = "ios")))] self.start_keyboard_hook(); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + self.start_hotkey_grab(); } async fn handle_hash(&mut self, pass: &str, hash: Hash, peer: &mut Stream) { @@ -1208,6 +1266,93 @@ impl Interface for Session { #[cfg(not(any(target_os = "android", target_os = "ios")))] impl Session { + fn send_hotkey(&self, key: RdevKey, is_press: bool) { + log::info!("{:?} {:?}", key, is_press); + } + + fn handle_hot_key_event(&self, event: Event) { + // keyboard long press + match event.event_type { + EventType::KeyPress(k) => { + if MUTEX_SPECIAL_KEYS.lock().unwrap().contains_key(&k) { + if *MUTEX_SPECIAL_KEYS.lock().unwrap().get(&k).unwrap() { + return; + } + MUTEX_SPECIAL_KEYS.lock().unwrap().insert(k, true); + } + } + EventType::KeyRelease(k) => { + if MUTEX_SPECIAL_KEYS.lock().unwrap().contains_key(&k) { + MUTEX_SPECIAL_KEYS.lock().unwrap().insert(k, false); + } + } + _ => return, + }; + + // keyboard short press + match event.event_type { + EventType::KeyPress(key) => { + self.send_hotkey(key, true); + self.key_down_or_up(true, key, event); + } + EventType::KeyRelease(key) => { + self.send_hotkey(key, false); + self.key_down_or_up(false, key, event); + } + _ => {} + } + } + + fn start_hotkey_grab(&self) { + if self.is_port_forward() || self.is_file_transfer() { + return; + } + let me = self.clone(); + + log::info!("hotkey grabing"); + std::thread::spawn(move || { + std::env::set_var("KEYBOARD_ONLY", "y"); + + let func = move |event: Event| { + #[cfg(any(target_os = "windows", target_os = "macos"))] + if !HOTKEY_HOOK_ENABLED.load(Ordering::SeqCst) { + return Some(event); + }; + match event.event_type { + EventType::KeyPress(key) | EventType::KeyRelease(key) => { + #[cfg(any(target_os = "windows", target_os = "macos"))] + if MUTEX_SPECIAL_KEYS.lock().unwrap().contains_key(&key) { + me.handle_hot_key_event(event); + return None; + } + + #[cfg(target_os = "linux")] + me.handle_hot_key_event(event); + + None + } + _ => Some(event), + } + }; + + #[cfg(target_os = "linux")] + { + use rdev::GRABED_KEYS; + GRABED_KEYS.lock().unwrap().insert(RdevKey::ShiftLeft); + GRABED_KEYS.lock().unwrap().insert(RdevKey::ShiftRight); + GRABED_KEYS.lock().unwrap().insert(RdevKey::ControlLeft); + GRABED_KEYS.lock().unwrap().insert(RdevKey::ControlRight); + GRABED_KEYS.lock().unwrap().insert(RdevKey::Alt); + GRABED_KEYS.lock().unwrap().insert(RdevKey::AltGr); + GRABED_KEYS.lock().unwrap().insert(RdevKey::MetaLeft); + GRABED_KEYS.lock().unwrap().insert(RdevKey::MetaRight); + } + if let Err(error) = rdev::grab(func) { + log::error!("Error: {:?}", error) + } + }); + } + fn start_keyboard_hook(&self) { if self.is_port_forward() || self.is_file_transfer() { return; @@ -1215,6 +1360,11 @@ impl Session { if !KEYBOARD_HOOKED.load(Ordering::SeqCst) { return; } + // rdev::grab and rdev::listen use the same api on macOS + #[cfg(target_os = "macos")] + if HOTKEY_HOOK_ENABLED.load(Ordering::SeqCst) { + return; + } log::info!("keyboard hooked"); let me = self.clone(); #[cfg(windows)] @@ -1222,20 +1372,6 @@ impl Session { std::thread::spawn(move || { // This will block. std::env::set_var("KEYBOARD_ONLY", "y"); - lazy_static::lazy_static! { - static ref MUTEX_SPECIAL_KEYS: Mutex> = { - let mut m = HashMap::new(); - m.insert(RdevKey::ShiftLeft, false); - m.insert(RdevKey::ShiftRight, false); - m.insert(RdevKey::ControlLeft, false); - m.insert(RdevKey::ControlRight, false); - m.insert(RdevKey::Alt, false); - m.insert(RdevKey::AltGr, false); - m.insert(RdevKey::MetaLeft, false); - m.insert(RdevKey::MetaRight, false); - Mutex::new(m) - }; - } let func = move |evt: Event| { if !IS_IN.load(Ordering::SeqCst) || !SERVER_KEYBOARD_ENABLED.load(Ordering::SeqCst) @@ -1421,3 +1557,7 @@ async fn send_note(url: String, id: String, conn_id: i32, note: String) { let body = serde_json::json!({ "id": id, "Id": conn_id, "note": note }); allow_err!(crate::post_request(url, body.to_string(), "").await); } + +fn get_hotkey_state(key: RdevKey) -> bool { + *MUTEX_SPECIAL_KEYS.lock().unwrap().get(&key).unwrap() +} From 01875a562c2dd1ff700373834d4241625dfd3efb Mon Sep 17 00:00:00 2001 From: Asura Date: Sun, 9 Oct 2022 18:29:21 -0700 Subject: [PATCH 0663/2015] Fix backquote error --- src/ui_session_interface.rs | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 8288adbe0..003394f7c 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -906,18 +906,7 @@ impl Session { key_event.set_control_key(key.clone()); } Key::_Raw(raw) => { - if raw > 'z' as u32 || raw < 'a' as u32 { - key_event.set_unicode(raw); - // TODO - // if down_or_up == 0 { - // // ignore up, avoiding trigger twice - // return; - // } - // down_or_up = 1; // if press, turn into down for avoiding trigger twice on server side - } else { - // to make ctrl+c works on windows - key_event.set_chr(raw); - } + key_event.set_chr(raw); } } From f2a5b77d7a310dd57d1c5dbdd7176bacd3aba9d3 Mon Sep 17 00:00:00 2001 From: Asura Date: Sun, 9 Oct 2022 19:27:19 -0700 Subject: [PATCH 0664/2015] set map as default keyboard mode --- Cargo.lock | 2 +- src/client.rs | 5 ++++- src/ui_session_interface.rs | 16 ++++++++++++---- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4348e05cb..807f072f7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4126,7 +4126,7 @@ dependencies = [ [[package]] name = "rdev" version = "0.5.0-2" -source = "git+https://github.com/asur4s/rdev#71a2b9e014b5aaeb85d7bb4a5b7562e3a68cc509" +source = "git+https://github.com/asur4s/rdev#22c8a6474065f03ecbddef7f47de0539ff3f5c4f" dependencies = [ "cocoa", "core-foundation 0.9.3", diff --git a/src/client.rs b/src/client.rs index e65c16f33..df18d9b0a 100644 --- a/src/client.rs +++ b/src/client.rs @@ -48,7 +48,7 @@ pub use super::lang::*; pub mod file_trait; pub mod helper; pub mod io_loop; - +use crate::ui_session_interface::global_save_keyboard_mode; 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); @@ -1294,6 +1294,9 @@ impl LoginConfigHandler { if !pi.version.is_empty() { self.version = hbb_common::get_version_number(&pi.version); } + if hbb_common::get_version_number(&pi.version) < hbb_common::get_version_number("1.2.0") { + global_save_keyboard_mode("legacy".to_owned()); + } self.features = pi.features.clone().into_option(); let serde = PeerInfoSerde { username: pi.username.clone(), diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 003394f7c..3753932d8 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -80,13 +80,11 @@ impl Session { } pub fn get_keyboard_mode(&self) -> String { - return std::env::var("KEYBOARD_MODE") - .unwrap_or(String::from("legacy")) - .to_lowercase(); + global_get_keyboard_mode() } pub fn save_keyboard_mode(&self, value: String) { - std::env::set_var("KEYBOARD_MODE", value); + global_save_keyboard_mode(value); } pub fn save_view_style(&mut self, value: String) { @@ -1550,3 +1548,13 @@ async fn send_note(url: String, id: String, conn_id: i32, note: String) { fn get_hotkey_state(key: RdevKey) -> bool { *MUTEX_SPECIAL_KEYS.lock().unwrap().get(&key).unwrap() } + +pub fn global_get_keyboard_mode() -> String { + return std::env::var("KEYBOARD_MODE") + .unwrap_or(String::from("map")) + .to_lowercase(); +} + +pub fn global_save_keyboard_mode(value: String) { + std::env::set_var("KEYBOARD_MODE", value); +} From 2252d6345aec1b9bd64eb990afc7159dbf741c45 Mon Sep 17 00:00:00 2001 From: Asura Date: Sun, 9 Oct 2022 20:51:46 -0700 Subject: [PATCH 0665/2015] refactor grab hot key add compile condition --- Cargo.lock | 26 +----------------- src/ui_session_interface.rs | 54 ++++++++++++++++++++----------------- 2 files changed, 30 insertions(+), 50 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 807f072f7..48bfd0e20 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1579,29 +1579,6 @@ dependencies = [ "nix 0.23.1", ] -[[package]] -name = "evdev-rs" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46504075975d14f0463e5a41efa06820c94d4c04fecd01f70b95365d60de1caf" -dependencies = [ - "bitflags", - "evdev-sys", - "libc", - "log", -] - -[[package]] -name = "evdev-sys" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14ead42b547b15d47089c1243d907bcf0eb94e457046d3b315a26ac9c9e9ea6d" -dependencies = [ - "cc", - "libc", - "pkg-config", -] - [[package]] name = "event-listener" version = "2.5.3" @@ -4126,7 +4103,7 @@ dependencies = [ [[package]] name = "rdev" version = "0.5.0-2" -source = "git+https://github.com/asur4s/rdev#22c8a6474065f03ecbddef7f47de0539ff3f5c4f" +source = "git+https://github.com/asur4s/rdev#ea223720532f32652dab803db43f9ce437f2b019" dependencies = [ "cocoa", "core-foundation 0.9.3", @@ -4134,7 +4111,6 @@ dependencies = [ "core-graphics 0.22.3", "enum-map", "epoll", - "evdev-rs", "inotify", "lazy_static", "libc", diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 3753932d8..f5cc0499d 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -6,6 +6,7 @@ use crate::client::{ load_config, send_mouse, start_video_audio_threads, FileManager, Key, LoginConfigHandler, QualityStatus, KEY_MAP, SERVER_KEYBOARD_ENABLED, }; +use crate::common::IS_X11; use crate::{client::Data, client::Interface}; use async_trait::async_trait; use hbb_common::config::{Config, LocalConfig, PeerConfig}; @@ -909,14 +910,7 @@ impl Session { } #[cfg(not(any(target_os = "android", target_os = "ios")))] - let ctrl = - get_hotkey_state(RdevKey::ControlLeft) || get_hotkey_state(RdevKey::ControlRight); - #[cfg(not(any(target_os = "android", target_os = "ios")))] - let shift = get_hotkey_state(RdevKey::ShiftLeft) || get_hotkey_state(RdevKey::ShiftRight); - #[cfg(not(any(target_os = "android", target_os = "ios")))] - let command = get_hotkey_state(RdevKey::MetaLeft) || get_hotkey_state(RdevKey::MetaRight); - #[cfg(not(any(target_os = "android", target_os = "ios")))] - let alt = get_hotkey_state(RdevKey::Alt) || get_hotkey_state(RdevKey::AltGr); + let (alt, ctrl, shift, command) = get_all_hotkey_state(alt, ctrl, shift, command); self.legacy_modifiers(&mut key_event, alt, ctrl, shift, command); if v == 1 { @@ -948,14 +942,7 @@ impl Session { } #[cfg(not(any(target_os = "android", target_os = "ios")))] - let ctrl = - get_hotkey_state(RdevKey::ControlLeft) || get_hotkey_state(RdevKey::ControlRight); - #[cfg(not(any(target_os = "android", target_os = "ios")))] - let shift = get_hotkey_state(RdevKey::ShiftLeft) || get_hotkey_state(RdevKey::ShiftRight); - #[cfg(not(any(target_os = "android", target_os = "ios")))] - let command = get_hotkey_state(RdevKey::MetaLeft) || get_hotkey_state(RdevKey::MetaRight); - #[cfg(not(any(target_os = "android", target_os = "ios")))] - let alt = get_hotkey_state(RdevKey::Alt) || get_hotkey_state(RdevKey::AltGr); + let (alt, ctrl, shift, command) = get_all_hotkey_state(alt, ctrl, shift, command); send_mouse(mask, x, y, alt, ctrl, shift, command, self); // on macos, ctrl + left button down = right button down, up won't emit, so we need to @@ -1206,7 +1193,7 @@ impl Interface for Session { crate::platform::windows::add_recent_document(&path); } } - // rdev::grab and rdev::listen use the same api on macOS + #[cfg(not(any(target_os = "android", target_os = "ios")))] self.start_keyboard_hook(); #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -1253,10 +1240,6 @@ impl Interface for Session { #[cfg(not(any(target_os = "android", target_os = "ios")))] impl Session { - fn send_hotkey(&self, key: RdevKey, is_press: bool) { - log::info!("{:?} {:?}", key, is_press); - } - fn handle_hot_key_event(&self, event: Event) { // keyboard long press match event.event_type { @@ -1279,11 +1262,9 @@ impl Session { // keyboard short press match event.event_type { EventType::KeyPress(key) => { - self.send_hotkey(key, true); self.key_down_or_up(true, key, event); } EventType::KeyRelease(key) => { - self.send_hotkey(key, false); self.key_down_or_up(false, key, event); } _ => {} @@ -1291,6 +1272,10 @@ impl Session { } fn start_hotkey_grab(&self) { + #[cfg(target_os = "linux")] + if !*IS_X11.lock().unwrap() { + return; + } if self.is_port_forward() || self.is_file_transfer() { return; } @@ -1306,11 +1291,13 @@ impl Session { return Some(event); }; match event.event_type { - EventType::KeyPress(key) | EventType::KeyRelease(key) => { + EventType::KeyPress(_key) | EventType::KeyRelease(_key) => { #[cfg(any(target_os = "windows", target_os = "macos"))] - if MUTEX_SPECIAL_KEYS.lock().unwrap().contains_key(&key) { + if MUTEX_SPECIAL_KEYS.lock().unwrap().contains_key(&_key) { me.handle_hot_key_event(event); return None; + } else { + return Some(event); } #[cfg(target_os = "linux")] @@ -1549,6 +1536,23 @@ fn get_hotkey_state(key: RdevKey) -> bool { *MUTEX_SPECIAL_KEYS.lock().unwrap().get(&key).unwrap() } +fn get_all_hotkey_state( + alt: bool, + ctrl: bool, + shift: bool, + command: bool, +) -> (bool, bool, bool, bool) { + let ctrl = + get_hotkey_state(RdevKey::ControlLeft) || get_hotkey_state(RdevKey::ControlRight) || ctrl; + let shift = + get_hotkey_state(RdevKey::ShiftLeft) || get_hotkey_state(RdevKey::ShiftRight) || shift; + let command = + get_hotkey_state(RdevKey::MetaLeft) || get_hotkey_state(RdevKey::MetaRight) || command; + let alt = get_hotkey_state(RdevKey::Alt) || get_hotkey_state(RdevKey::AltGr) || alt; + + (alt, ctrl, shift, command) +} + pub fn global_get_keyboard_mode() -> String { return std::env::var("KEYBOARD_MODE") .unwrap_or(String::from("map")) From 1ce8b1fee558822d06d9f6e96af0effd44960057 Mon Sep 17 00:00:00 2001 From: csf Date: Mon, 10 Oct 2022 18:27:26 +0900 Subject: [PATCH 0666/2015] mobile tag actions --- flutter/lib/common/widgets/address_book.dart | 130 +++++++++++++------ 1 file changed, 89 insertions(+), 41 deletions(-) diff --git a/flutter/lib/common/widgets/address_book.dart b/flutter/lib/common/widgets/address_book.dart index 52189c8b1..570ff6e95 100644 --- a/flutter/lib/common/widgets/address_book.dart +++ b/flutter/lib/common/widgets/address_book.dart @@ -21,6 +21,8 @@ class AddressBook extends StatefulWidget { } class _AddressBookState extends State { + var menuPos = RelativeRect.fill; + @override void initState() { super.initState(); @@ -72,7 +74,9 @@ class _AddressBookState extends State { if (gFFI.abModel.abError.isNotEmpty) { return _buildShowError(gFFI.abModel.abError.value); } - return _buildAddressBook(context); + return isDesktop + ? _buildAddressBookDesktop() + : _buildAddressBookMobile(); } }); } @@ -92,8 +96,7 @@ class _AddressBookState extends State { )); } - Widget _buildAddressBook(BuildContext context) { - var pos = RelativeRect.fill; + Widget _buildAddressBookDesktop() { return Row( children: [ Card( @@ -109,20 +112,7 @@ class _AddressBookState extends State { const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), child: Column( children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(translate('Tags')), - GestureDetector( - onTapDown: (e) { - final x = e.globalPosition.dx; - final y = e.globalPosition.dy; - pos = RelativeRect.fromLTRB(x, y, x, y); - }, - onTap: () => _showMenu(pos), - child: ActionMore()), - ], - ), + _buildTagHeader(), Expanded( child: Container( width: double.infinity, @@ -130,40 +120,98 @@ class _AddressBookState extends State { decoration: BoxDecoration( border: Border.all(color: MyTheme.darkGray), borderRadius: BorderRadius.circular(2)), - child: Obx( - () => Wrap( - children: gFFI.abModel.tags - .map((e) => AddressBookTag( - name: e, - tags: gFFI.abModel.selectedTags, - onTap: () { - if (gFFI.abModel.selectedTags.contains(e)) { - gFFI.abModel.selectedTags.remove(e); - } else { - gFFI.abModel.selectedTags.add(e); - } - })) - .toList(), - ), - ), + child: _buildTags(), ).marginSymmetric(vertical: 8.0), ) ], ), ), ).marginOnly(right: 8.0), - Expanded( - child: Align( - alignment: Alignment.topLeft, - child: Obx(() => AddressBookPeersView( - menuPadding: widget.menuPadding, - initPeers: gFFI.abModel.peers.value, - ))), - ) + _buildPeersViews() ], ); } + Widget _buildAddressBookMobile() { + return Column( + children: [ + Card( + margin: EdgeInsets.symmetric(horizontal: 1.0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6), + side: + BorderSide(color: Theme.of(context).scaffoldBackgroundColor)), + child: Container( + padding: + const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _buildTagHeader(), + Container( + width: double.infinity, + decoration: BoxDecoration( + border: Border.all(color: MyTheme.darkGray), + borderRadius: BorderRadius.circular(4)), + child: _buildTags(), + ).marginSymmetric(vertical: 8.0), + ], + ), + ), + ), + Divider(), + _buildPeersViews() + ], + ); + } + + Widget _buildTagHeader() { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(translate('Tags')), + GestureDetector( + onTapDown: (e) { + final x = e.globalPosition.dx; + final y = e.globalPosition.dy; + menuPos = RelativeRect.fromLTRB(x, y, x, y); + }, + onTap: () => _showMenu(menuPos), + child: ActionMore()), + ], + ); + } + + Widget _buildTags() { + return Obx( + () => Wrap( + children: gFFI.abModel.tags + .map((e) => AddressBookTag( + name: e, + tags: gFFI.abModel.selectedTags, + onTap: () { + if (gFFI.abModel.selectedTags.contains(e)) { + gFFI.abModel.selectedTags.remove(e); + } else { + gFFI.abModel.selectedTags.add(e); + } + })) + .toList(), + ), + ); + } + + Widget _buildPeersViews() { + return Expanded( + child: Align( + alignment: Alignment.topLeft, + child: Obx(() => AddressBookPeersView( + menuPadding: widget.menuPadding, + initPeers: gFFI.abModel.peers.value, + ))), + ); + } + void _showMenu(RelativeRect pos) { final items = [ getEntry(translate("Add ID"), abAddId), From efacc7362adc4dd7a4f6466aa4e7ba1e7b3fcde3 Mon Sep 17 00:00:00 2001 From: csf Date: Mon, 10 Oct 2022 21:10:31 +0900 Subject: [PATCH 0667/2015] fix hit tag empty space bug --- flutter/lib/common.dart | 17 --------- flutter/lib/common/widgets/peers_view.dart | 44 ++++++++++++++++++---- 2 files changed, 36 insertions(+), 25 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index ce341d160..c4615c336 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -936,23 +936,6 @@ Future matchPeer(String searchText, Peer peer) async { return alias.toLowerCase().contains(searchText); } -Future>? matchPeers(String searchText, List peers) async { - searchText = searchText.trim(); - if (searchText.isEmpty) { - return peers; - } - searchText = searchText.toLowerCase(); - final matches = - await Future.wait(peers.map((peer) => matchPeer(searchText, peer))); - final filteredList = List.empty(growable: true); - for (var i = 0; i < peers.length; i++) { - if (matches[i]) { - filteredList.add(peers[i]); - } - } - return filteredList; -} - /// Get the image for the current [platform]. Widget getPlatformImage(String platform, {double size = 50}) { platform = platform.toLowerCase(); diff --git a/flutter/lib/common/widgets/peers_view.dart b/flutter/lib/common/widgets/peers_view.dart index 03a2436f2..6e52bfeb8 100644 --- a/flutter/lib/common/widgets/peers_view.dart +++ b/flutter/lib/common/widgets/peers_view.dart @@ -13,6 +13,7 @@ import '../../models/peer_model.dart'; import '../../models/platform_model.dart'; import 'peer_card.dart'; +typedef PeerFilter = bool Function(Peer peer); typedef PeerCardBuilder = Widget Function(Peer peer); /// for peer search text, global obs value @@ -22,10 +23,14 @@ final peerSearchTextController = class _PeersView extends StatefulWidget { final Peers peers; + final PeerFilter? peerFilter; final PeerCardBuilder peerCardBuilder; const _PeersView( - {required this.peers, required this.peerCardBuilder, Key? key}) + {required this.peers, + required this.peerCardBuilder, + this.peerFilter, + Key? key}) : super(key: key); @override @@ -173,11 +178,33 @@ class _PeersViewState extends State<_PeersView> with WindowListener { } }(); } + + Future>? matchPeers(String searchText, List peers) async { + if (widget.peerFilter != null) { + peers = peers.where((peer) => widget.peerFilter!(peer)).toList(); + } + + searchText = searchText.trim(); + if (searchText.isEmpty) { + return peers; + } + searchText = searchText.toLowerCase(); + final matches = + await Future.wait(peers.map((peer) => matchPeer(searchText, peer))); + final filteredList = List.empty(growable: true); + for (var i = 0; i < peers.length; i++) { + if (matches[i]) { + filteredList.add(peers[i]); + } + } + return filteredList; + } } abstract class BasePeersView extends StatelessWidget { final String name; final String loadEvent; + final PeerFilter? peerFilter; final PeerCardBuilder peerCardBuilder; final List initPeers; @@ -185,6 +212,7 @@ abstract class BasePeersView extends StatelessWidget { Key? key, required this.name, required this.loadEvent, + this.peerFilter, required this.peerCardBuilder, required this.initPeers, }) : super(key: key); @@ -193,6 +221,7 @@ abstract class BasePeersView extends StatelessWidget { Widget build(BuildContext context) { return _PeersView( peers: Peers(name: name, loadEvent: loadEvent, peers: initPeers), + peerFilter: peerFilter, peerCardBuilder: peerCardBuilder); } } @@ -273,13 +302,12 @@ class AddressBookPeersView extends BasePeersView { key: key, name: 'address book peer', loadEvent: 'load_address_book_peers', - peerCardBuilder: (Peer peer) => Obx(() => Offstage( - key: ValueKey("off${peer.id}"), - offstage: !_hitTag(gFFI.abModel.selectedTags, peer.tags), - child: AddressBookPeerCard( - peer: peer, - menuPadding: menuPadding, - ))), + peerFilter: (Peer peer) => + _hitTag(gFFI.abModel.selectedTags, peer.tags), + peerCardBuilder: (Peer peer) => AddressBookPeerCard( + peer: peer, + menuPadding: menuPadding, + ), initPeers: initPeers, ); From abbf56f2abff609987676a5980a1763d61dafd0d Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 10 Oct 2022 10:53:10 +0800 Subject: [PATCH 0668/2015] fix: use rawRGBa cursor workaround --- flutter/lib/desktop/pages/remote_page.dart | 4 +++- flutter/lib/models/model.dart | 8 +++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 396e736e0..3757d96e6 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -404,7 +404,9 @@ class ImagePaint extends StatelessWidget { controller: _horizontal, thumbVisibility: false, trackVisibility: false, - notificationPredicate: (notification) => notification.depth == 1, + notificationPredicate: layoutSize.height < size.height + ? (notification) => notification.depth == 1 + : defaultScrollNotificationPredicate, child: widget, ), ); diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index c646f5285..6c800147e 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:io'; import 'dart:math'; import 'dart:typed_data'; import 'dart:ui' as ui; @@ -848,7 +849,12 @@ class CursorModel with ChangeNotifier { } _updateCacheLinux(ui.Image image, int id, int w, int h) async { - final data = await image.toByteData(format: ui.ImageByteFormat.png); + ByteData? data; + if (Platform.isWindows) { + data = await image.toByteData(format: ui.ImageByteFormat.rawRgba); + } else { + data = await image.toByteData(format: ui.ImageByteFormat.png); + } _cacheLinux = CursorData( peerId: this.id, data: data?.buffer.asUint8List(), From ed9ce650e07a11b8d4d19ad074ee2fbbc6d35e93 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 10 Oct 2022 22:23:24 +0800 Subject: [PATCH 0669/2015] opt: update upstream flutter_custom_cursor --- flutter/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 36feee2a5..530d74b82 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -73,7 +73,7 @@ dependencies: flutter_custom_cursor: git: url: https://github.com/Kingtous/rustdesk_flutter_custom_cursor - ref: 4a950fd3a5a228bf5381070a4c803919d5787c07 + ref: dec2166e881c47d922e1edc484d10d2cd5c2103b window_size: git: url: https://github.com/google/flutter-desktop-embedding.git From 607bf331623a181bc0ef8827d5cc7b6781135342 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 11 Oct 2022 06:28:46 +0800 Subject: [PATCH 0670/2015] deb bug --- build.py | 2 -- res/DEBIAN/postinst | 3 ++- res/PKGBUILD | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/build.py b/build.py index 16f625197..54f50a0b8 100755 --- a/build.py +++ b/build.py @@ -157,8 +157,6 @@ def build_flutter_deb(version): os.system('rm tmpdeb/usr/bin/rustdesk') os.system( 'cp -r build/linux/x64/release/bundle/* tmpdeb/usr/lib/rustdesk/') - os.system( - 'cp ../target/release/liblibrustdesk.so tmpdeb/usr/lib/rustdesk/librustdesk.so') os.system( 'cp ../res/rustdesk.service tmpdeb/usr/share/rustdesk/files/systemd/') os.system( diff --git a/res/DEBIAN/postinst b/res/DEBIAN/postinst index b30664ce0..44cb88c33 100755 --- a/res/DEBIAN/postinst +++ b/res/DEBIAN/postinst @@ -7,13 +7,14 @@ if [ "$1" = configure ]; then INITSYS=$(ls -al /proc/1/exe | awk -F' ' '{print $NF}' | awk -F'/' '{print $NF}') if [ "systemd" == "$INITSYS" ]; then - ln -s /usr/lib/rustdesk/flutter_hbb /usr/bin/rustdesk + ln -s /usr/lib/rustdesk/rustdesk /usr/bin/rustdesk if [ -e /etc/systemd/system/rustdesk.service ]; then rm /etc/systemd/system/rustdesk.service /usr/lib/systemd/system/rustdesk.service /usr/lib/systemd/user/rustdesk.service >/dev/null 2>&1 fi version=$(python3 -V 2>&1 | grep -Po '(?<=Python )(.+)') parsedVersion=$(echo "${version//./}") + mkdir -p /usr/lib/systemd/system/ cp /usr/share/rustdesk/files/systemd/rustdesk.service /usr/lib/systemd/system/rustdesk.service systemctl daemon-reload systemctl enable rustdesk diff --git a/res/PKGBUILD b/res/PKGBUILD index bd950fa67..90e00df19 100644 --- a/res/PKGBUILD +++ b/res/PKGBUILD @@ -27,7 +27,7 @@ package() { cp ${HBB}/flutter/build/linux/x64/release/liblibrustdesk.so "${pkgdir}/usr/lib/rustdesk/librustdesk.so" fi mkdir -p "${pkgdir}/usr/bin" - pushd ${pkgdir} && ln -s /usr/lib/rustdesk/flutter_hbb usr/bin/rustdesk && popd + pushd ${pkgdir} && ln -s /usr/lib/rustdesk/rustdesk usr/bin/rustdesk && popd install -Dm 644 $HBB/res/rustdesk.service -t "${pkgdir}/usr/share/rustdesk/files" install -Dm 644 $HBB/res/rustdesk.desktop -t "${pkgdir}/usr/share/rustdesk/files" install -Dm 644 $HBB/res/128x128@2x.png "${pkgdir}/usr/share/rustdesk/files/rustdesk.png" From c1b9a3f53da0c25e07de1dfbab9b0bb981f74b2c Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 11 Oct 2022 14:56:08 +0800 Subject: [PATCH 0671/2015] unsafe --- libs/hbb_common/src/config.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index d04db5245..0ef507a13 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -284,7 +284,7 @@ impl Config { log::debug!("Configuration path: {}", file.display()); let cfg = load_path(file); if suffix.is_empty() { - log::debug!("{:?}", cfg); + log::trace!("{:?}", cfg); } cfg } From 089cf41a2ff86b71696e2591ac74daf0cfef17eb Mon Sep 17 00:00:00 2001 From: 21pages Date: Sun, 9 Oct 2022 20:32:28 +0800 Subject: [PATCH 0672/2015] add install page Signed-off-by: 21pages --- flutter/lib/consts.dart | 2 +- flutter/lib/desktop/pages/install_page.dart | 198 ++++++++++++++++++++ flutter/lib/main.dart | 27 +++ src/flutter_ffi.rs | 16 ++ 4 files changed, 242 insertions(+), 1 deletion(-) create mode 100644 flutter/lib/desktop/pages/install_page.dart diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index d31f77111..ce6c93c00 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -3,7 +3,7 @@ import 'dart:io'; const double kDesktopRemoteTabBarHeight = 28.0; -/// [kAppTypeMain] used by 'Desktop Main Page' , 'Mobile (Client and Server)' , 'Desktop CM Page' +/// [kAppTypeMain] used by 'Desktop Main Page' , 'Mobile (Client and Server)' , 'Desktop CM Page', "Install Page" const String kAppTypeMain = "main"; const String kAppTypeDesktopRemote = "remote"; const String kAppTypeDesktopFileTransfer = "file transfer"; diff --git a/flutter/lib/desktop/pages/install_page.dart b/flutter/lib/desktop/pages/install_page.dart new file mode 100644 index 000000000..b7b3f982d --- /dev/null +++ b/flutter/lib/desktop/pages/install_page.dart @@ -0,0 +1,198 @@ +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; +import 'package:get/get.dart'; +import 'package:url_launcher/url_launcher_string.dart'; +import 'package:window_manager/window_manager.dart'; + +class InstallPage extends StatefulWidget { + const InstallPage({Key? key}) : super(key: key); + + @override + State createState() => _InstallPageState(); +} + +class _InstallPageState extends State with WindowListener { + late final TextEditingController controller; + final RxBool startmenu = true.obs; + final RxBool desktopicon = true.obs; + final RxBool showProgress = false.obs; + final RxBool btnEnabled = true.obs; + + @override + void initState() { + windowManager.addListener(this); + controller = TextEditingController(text: bind.installInstallPath()); + super.initState(); + } + + @override + void dispose() { + windowManager.removeListener(this); + super.dispose(); + } + + @override + void onWindowClose() { + gFFI.close(); + super.onWindowClose(); + windowManager.setPreventClose(false); + windowManager.close(); + } + + @override + Widget build(BuildContext context) { + final double em = 13; + final btnFontSize = 0.9 * em; + final double button_radius = 6; + final buttonStyle = OutlinedButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(button_radius)), + )); + final inputBorder = OutlineInputBorder( + borderRadius: BorderRadius.zero, + borderSide: BorderSide(color: Colors.black12)); + return Scaffold( + backgroundColor: Colors.white, + body: SingleChildScrollView( + child: Column( + children: [ + Row( + children: [ + Text( + translate('Installation'), + style: TextStyle( + fontSize: 2 * em, fontWeight: FontWeight.w500), + ), + ], + ), + Row( + children: [ + Text('${translate('Installation Path')}: '), + Expanded( + child: TextField( + controller: controller, + readOnly: true, + style: TextStyle( + fontSize: 1.5 * em, fontWeight: FontWeight.w400), + decoration: InputDecoration( + isDense: true, + contentPadding: EdgeInsets.all(0.75 * em), + enabledBorder: inputBorder, + border: inputBorder, + focusedBorder: inputBorder, + constraints: BoxConstraints(maxHeight: 3 * em), + ), + )), + Obx(() => OutlinedButton( + onPressed: + btnEnabled.value ? selectInstallPath : null, + style: buttonStyle, + child: Text(translate('Change Path'), + style: TextStyle( + color: Colors.black87, + fontSize: btnFontSize))) + .marginOnly(left: em)) + ], + ).marginSymmetric(vertical: 2 * em), + Row( + children: [ + Obx(() => Checkbox( + value: startmenu.value, + onChanged: (b) { + if (b != null) startmenu.value = b; + })), + Text(translate('Create start menu shortcuts')) + ], + ), + Row( + children: [ + Obx(() => Checkbox( + value: desktopicon.value, + onChanged: (b) { + if (b != null) desktopicon.value = b; + })), + Text(translate('Create desktop icon')) + ], + ), + GestureDetector( + onTap: () => launchUrlString('http://rustdesk.com/privacy'), + child: Row( + children: [ + Text(translate('End-user license agreement'), + style: const TextStyle( + decoration: TextDecoration.underline)) + ], + )).marginOnly(top: 2 * em), + Row(children: [Text(translate('agreement_tip'))]) + .marginOnly(top: em), + Divider(color: Colors.black87) + .marginSymmetric(vertical: 0.5 * em), + Row( + children: [ + Expanded( + child: Obx(() => Offstage( + offstage: !showProgress.value, + child: LinearProgressIndicator(), + ))), + Obx(() => OutlinedButton( + onPressed: btnEnabled.value + ? () => windowManager.close() + : null, + style: buttonStyle, + child: Text(translate('Cancel'), + style: TextStyle( + color: Colors.black87, + fontSize: btnFontSize))) + .marginOnly(right: 2 * em)), + Obx(() => ElevatedButton( + onPressed: btnEnabled.value ? install : null, + style: ElevatedButton.styleFrom( + primary: MyTheme.button, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(button_radius)), + )), + child: Text( + translate('Accept and Install'), + style: TextStyle(fontSize: btnFontSize), + ))), + Offstage( + offstage: bind.installShowRunWithoutInstall(), + child: Obx(() => OutlinedButton( + onPressed: btnEnabled.value + ? () => bind.installRunWithoutInstall() + : null, + style: buttonStyle, + child: Text(translate('Run without install'), + style: TextStyle( + color: Colors.black87, + fontSize: btnFontSize))) + .marginOnly(left: 2 * em)), + ), + ], + ) + ], + ).paddingSymmetric(horizontal: 8 * em, vertical: 2 * em), + )); + } + + void install() { + btnEnabled.value = false; + showProgress.value = true; + String args = ''; + if (startmenu.value) args += 'startmenu '; + if (desktopicon.value) args += 'desktopicon '; + bind.installInstallMe(options: args, path: controller.text); + } + + void selectInstallPath() async { + String? install_path = await FilePicker.platform + .getDirectoryPath(initialDirectory: controller.text); + if (install_path != null) { + install_path = '$install_path\\${await bind.mainGetAppName()}'; + controller.text = install_path; + } + } +} diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 0d1123e05..27b14be09 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -4,6 +4,7 @@ import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart'; import 'package:flutter_hbb/desktop/pages/server_page.dart'; +import 'package:flutter_hbb/desktop/pages/install_page.dart'; import 'package:flutter_hbb/desktop/screen/desktop_file_transfer_screen.dart'; import 'package:flutter_hbb/desktop/screen/desktop_port_forward_screen.dart'; import 'package:flutter_hbb/desktop/screen/desktop_remote_screen.dart'; @@ -64,6 +65,8 @@ Future main(List args) async { desktopType = DesktopType.cm; await windowManager.ensureInitialized(); runConnectionManagerScreen(); + } else if (args.contains('--install')) { + runInstallPage(); } else { desktopType = DesktopType.main; await windowManager.ensureInitialized(); @@ -215,6 +218,30 @@ void runConnectionManagerScreen() async { }); } +void runInstallPage() async { + await windowManager.ensureInitialized(); + await initEnv(kAppTypeMain); + runApp(GetMaterialApp( + debugShowCheckedModeBanner: false, + theme: MyTheme.lightTheme, + themeMode: ThemeMode.light, + localizationsDelegates: const [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: supportedLocales, + home: const InstallPage(), + builder: _keepScaleBuilder())); + windowManager.waitUntilReadyToShow( + WindowOptions(size: Size(800, 600), center: true), () async { + windowManager.show(); + windowManager.focus(); + windowManager.setOpacity(1); + windowManager.setAlignment(Alignment.center); // ensure + }); +} + WindowOptions getHiddenTitleBarWindowOptions({Size? size}) { return WindowOptions( size: size, diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 9ce92aeb2..b33c9490f 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1041,6 +1041,22 @@ pub fn main_update_me() -> SyncReturn { SyncReturn(true) } +pub fn install_show_run_without_install() -> SyncReturn { + SyncReturn(show_run_without_install()) +} + +pub fn install_run_without_install() { + run_without_install(); +} + +pub fn install_install_me(options: String, path: String) { + install_me(options, path, false, false); +} + +pub fn install_install_path() -> SyncReturn { + SyncReturn(install_path()) +} + #[cfg(target_os = "android")] pub mod server_side { use jni::{ From 2ced73cddaf16da44389de01a0d51a953b789cd7 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sun, 9 Oct 2022 21:10:41 +0800 Subject: [PATCH 0673/2015] pass rust args to flutter Signed-off-by: 21pages --- flutter/windows/runner/main.cpp | 18 +++++++- src/flutter.rs | 74 +++++++++++++++++++++++++++++++++ src/flutter_ffi.rs | 10 ----- 3 files changed, 90 insertions(+), 12 deletions(-) diff --git a/flutter/windows/runner/main.cpp b/flutter/windows/runner/main.cpp index efa26314e..3921e03dd 100644 --- a/flutter/windows/runner/main.cpp +++ b/flutter/windows/runner/main.cpp @@ -7,7 +7,8 @@ #include "utils.h" // #include -typedef bool (*FUNC_RUSTDESK_CORE_MAIN)(void); +typedef char** (*FUNC_RUSTDESK_CORE_MAIN)(int*); +typedef void (*FUNC_RUSTDESK_FREE_ARGS)( char**, int); // auto bdw = bitsdojo_window_configure(BDW_CUSTOM_FRAME | BDW_HIDE_ON_STARTUP); int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, @@ -26,11 +27,23 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, std::cout << "Failed to get rustdesk_core_main" << std::endl; return EXIT_FAILURE; } - if (!rustdesk_core_main()) + FUNC_RUSTDESK_FREE_ARGS free_c_args = + (FUNC_RUSTDESK_FREE_ARGS)GetProcAddress(hInstance, "free_c_args"); + if (!free_c_args) + { + std::cout << "Failed to get free_c_args" << std::endl; + return EXIT_FAILURE; + } + + int args_len = 0; + char** c_args = rustdesk_core_main(&args_len); + if (!c_args) { std::cout << "Rustdesk core returns false, exiting without launching Flutter app" << std::endl; return EXIT_SUCCESS; } + std::vector rust_args(c_args, c_args + args_len); + free_c_args(c_args, args_len); // Attach to console when present (e.g., 'flutter run') or create a // new console when running with a debugger. @@ -48,6 +61,7 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, std::vector command_line_arguments = GetCommandLineArguments(); + command_line_arguments.insert(command_line_arguments.end(), rust_args.begin(), rust_args.end()); project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); FlutterWindow window(project); diff --git a/src/flutter.rs b/src/flutter.rs index bf758e31c..b65ce4412 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -1,5 +1,7 @@ use std::{ collections::HashMap, + ffi::CString, + os::raw::{c_char, c_int}, sync::{Arc, RwLock}, }; @@ -24,6 +26,78 @@ lazy_static::lazy_static! { pub static ref GLOBAL_EVENT_STREAM: RwLock>> = Default::default(); // rust to dart event channel } +/// FFI for rustdesk core's main entry. +/// Return true if the app should continue running with UI(possibly Flutter), false if the app should exit. +#[cfg(not(windows))] +#[no_mangle] +pub extern "C" fn rustdesk_core_main() -> bool { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + return crate::core_main::core_main().is_some(); + #[cfg(any(target_os = "android", target_os = "ios"))] + false +} + +#[cfg(windows)] +#[no_mangle] +pub extern "C" fn rustdesk_core_main(args_len: *mut c_int) -> *mut *mut c_char { + unsafe { std::ptr::write(args_len, 0) }; + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + if let Some(args) = crate::core_main::core_main() { + return rust_args_to_c_args(args, args_len); + } + return std::ptr::null_mut() as _; + } + #[cfg(any(target_os = "android", target_os = "ios"))] + return std::ptr::null_mut() as _; +} + +// https://gist.github.com/iskakaushik/1c5b8aa75c77479c33c4320913eebef6 +fn rust_args_to_c_args(args: Vec, outlen: *mut c_int) -> *mut *mut c_char { + let mut v = vec![]; + + // Let's fill a vector with null-terminated strings + for s in args { + v.push(CString::new(s).unwrap()); + } + + // Turning each null-terminated string into a pointer. + // `into_raw` takes ownershop, gives us the pointer and does NOT drop the data. + let mut out = v.into_iter().map(|s| s.into_raw()).collect::>(); + + // Make sure we're not wasting space. + out.shrink_to_fit(); + assert!(out.len() == out.capacity()); + + // Get the pointer to our vector. + let len = out.len(); + let ptr = out.as_mut_ptr(); + std::mem::forget(out); + + // Let's write back the length the caller can expect + unsafe { std::ptr::write(outlen, len as c_int) }; + + // Finally return the data + ptr +} + +#[no_mangle] +pub unsafe extern "C" fn free_c_args(ptr: *mut *mut c_char, len: c_int) { + let len = len as usize; + + // Get back our vector. + // Previously we shrank to fit, so capacity == length. + let v = Vec::from_raw_parts(ptr, len, len); + + // Now drop one string at a time. + for elem in v { + let s = CString::from_raw(elem); + std::mem::drop(s); + } + + // Afterwards the vector will be dropped and thus freed. +} + #[derive(Default, Clone)] pub struct FlutterHandler { pub event_stream: Arc>>>, diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index b33c9490f..f788c679d 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -43,16 +43,6 @@ fn initialize(app_dir: &str) { } } -/// FFI for rustdesk core's main entry. -/// Return true if the app should continue running with UI(possibly Flutter), false if the app should exit. -#[no_mangle] -pub extern "C" fn rustdesk_core_main() -> bool { - #[cfg(not(any(target_os = "android", target_os = "ios")))] - return crate::core_main::core_main().is_some(); - #[cfg(any(target_os = "android", target_os = "ios"))] - false -} - pub enum EventToUI { Event(String), Rgba(ZeroCopyBuffer>), From e2924f0d4152649025bd1c0fbe9e39bcd7a761a3 Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 11 Oct 2022 14:52:46 +0800 Subject: [PATCH 0674/2015] build windows install Signed-off-by: 21pages --- build.py | 10 ++-- flutter/lib/desktop/pages/install_page.dart | 6 +-- libs/portable/src/main.rs | 53 +++++++++++++++------ src/common.rs | 2 +- src/core_main.rs | 15 ++++-- src/platform/windows.rs | 14 ++++++ 6 files changed, 74 insertions(+), 26 deletions(-) diff --git a/build.py b/build.py index 54f50a0b8..85dab7090 100755 --- a/build.py +++ b/build.py @@ -189,7 +189,7 @@ def build_flutter_arch_manjaro(): os.chdir('..') os.system('HBB=`pwd` FLUTTER=1 makepkg -f') -def build_flutter_windows_portable(): +def build_flutter_windows(version): os.system("cargo build --lib --features flutter --release") os.chdir('flutter') os.system("flutter build windows --release") @@ -203,6 +203,8 @@ def build_flutter_windows_portable(): else: os.rename("./target/release/rustdesk-portable-packer.exe", "./rustdesk_portable.exe") print(f"output location: {os.path.abspath(os.curdir)}/rustdesk_portable.exe") + os.system(f"cp -rf ./rustdesk_portable.exe ./rustdesk-{version}-install.exe") + print(f"output location: {os.path.abspath(os.curdir)}/rustdesk-{version}-install.exe") def main(): parser = make_parser() @@ -227,8 +229,8 @@ def main(): os.system('python3 res/inline-sciter.py') portable = args.portable if windows: - if portable: - build_flutter_windows_portable() + if flutter: + build_flutter_windows(version) return os.system('cargo build --release --features ' + features) # os.system('upx.exe target/release/rustdesk.exe') @@ -239,7 +241,7 @@ def main(): 'target\\release\\rustdesk.exe') else: print('Not signed') - os.system(f'cp -rf target/release/RustDesk.exe rustdesk-{version}-setdown.exe') + os.system(f'cp -rf target/release/RustDesk.exe rustdesk-{version}-win7-install.exe') elif os.path.isfile('/usr/bin/pacman'): # pacman -S -needed base-devel os.system("sed -i 's/pkgver=.*/pkgver=%s/g' PKGBUILD" % version) diff --git a/flutter/lib/desktop/pages/install_page.dart b/flutter/lib/desktop/pages/install_page.dart index b7b3f982d..73ad8769d 100644 --- a/flutter/lib/desktop/pages/install_page.dart +++ b/flutter/lib/desktop/pages/install_page.dart @@ -181,9 +181,9 @@ class _InstallPageState extends State with WindowListener { void install() { btnEnabled.value = false; showProgress.value = true; - String args = ''; - if (startmenu.value) args += 'startmenu '; - if (desktopicon.value) args += 'desktopicon '; + String args = '--flutter'; + if (startmenu.value) args += ' startmenu'; + if (desktopicon.value) args += ' desktopicon'; bind.installInstallMe(options: args, path: controller.text); } diff --git a/libs/portable/src/main.rs b/libs/portable/src/main.rs index 614c4c17c..1c9dc11ef 100644 --- a/libs/portable/src/main.rs +++ b/libs/portable/src/main.rs @@ -12,29 +12,37 @@ pub mod bin_reader; const APP_PREFIX: &str = "rustdesk"; const APPNAME_RUNTIME_ENV_KEY: &str = "RUSTDESK_APPNAME"; -fn setup(reader: BinaryReader) -> Option { - // home dir - if let Some(dir) = dirs::data_local_dir() { - let dir = dir.join(APP_PREFIX); - for file in reader.files.iter() { - file.write_to_file(&dir); - } - #[cfg(unix)] - reader.configure_permission(&dir); - Some(dir.join(&reader.exe)) +fn setup(reader: BinaryReader, dir: Option, clear: bool) -> Option { + let dir = if let Some(dir) = dir { + dir } else { - eprintln!("not found data local dir"); - None + // home dir + if let Some(dir) = dirs::data_local_dir() { + dir.join(APP_PREFIX) + } else { + eprintln!("not found data local dir"); + return None; + } + }; + if clear { + std::fs::remove_dir_all(&dir).ok(); } + for file in reader.files.iter() { + file.write_to_file(&dir); + } + #[cfg(unix)] + reader.configure_permission(&dir); + Some(dir.join(&reader.exe)) } -fn execute(path: PathBuf) { +fn execute(path: PathBuf, args: Vec) { println!("executing {}", path.display()); // setup env let exe = std::env::current_exe().unwrap(); let exe_name = exe.file_name().unwrap(); // run executable Command::new(path) + .args(args) .env(APPNAME_RUNTIME_ENV_KEY, exe_name) .stdin(Stdio::inherit()) .stdout(Stdio::inherit()) @@ -43,9 +51,24 @@ fn execute(path: PathBuf) { .expect(&format!("failed to execute {:?}", exe_name)); } +fn is_setup(name: &str) -> bool { + name.to_lowercase().ends_with("install.exe") || name.to_lowercase().ends_with("安装.exe") +} + fn main() { + let is_setup = is_setup( + &std::env::current_exe() + .unwrap() + .to_string_lossy() + .to_string(), + ); let reader = BinaryReader::default(); - if let Some(exe) = setup(reader) { - execute(exe); + if let Some(exe) = setup(reader, None, is_setup) { + let args = if is_setup { + vec!["--install".to_owned()] + } else { + vec![] + }; + execute(exe, args); } } diff --git a/src/common.rs b/src/common.rs index dd792362a..129e948cf 100644 --- a/src/common.rs +++ b/src/common.rs @@ -544,7 +544,7 @@ pub fn is_ip(id: &str) -> bool { } pub fn is_setup(name: &str) -> bool { - name.to_lowercase().ends_with("setdown.exe") || name.to_lowercase().ends_with("安装.exe") + name.to_lowercase().ends_with("install.exe") || name.to_lowercase().ends_with("安装.exe") } pub fn get_custom_rendezvous_server(custom: String) -> String { diff --git a/src/core_main.rs b/src/core_main.rs index d159e115e..ac05b20bf 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -6,6 +6,7 @@ pub fn core_main() -> Option> { // though async logger more efficient, but it also causes more problems, disable it for now // let mut _async_logger_holder: Option = None; let mut args = Vec::new(); + let mut flutter_args = Vec::new(); let mut i = 0; let mut is_setup = false; let mut _is_elevate = false; @@ -25,13 +26,18 @@ pub fn core_main() -> Option> { } i += 1; } + if args.contains(&"--install".to_string()) { + is_setup = true; + } if is_setup { if args.is_empty() { args.push("--install".to_owned()); - } else if args[0] == "--noinstall" { - args.clear(); + flutter_args.push("--install".to_string()); } } + if args.contains(&"--noinstall".to_string()) { + args.clear(); + } if args.len() > 0 && args[0] == "--version" { println!("{}", crate::VERSION); return None; @@ -171,7 +177,10 @@ pub fn core_main() -> Option> { } } //_async_logger_holder.map(|x| x.flush()); - Some(args) + #[cfg(feature = "flutter")] + return Some(flutter_args); + #[cfg(not(feature = "flutter"))] + return Some(args); } fn import_config(path: &str) { diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 10b0dcee6..6732f2c36 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -1025,6 +1025,18 @@ copy /Y \"{tmp_path}\\Uninstall {app_name}.lnk\" \"{start_menu}\\\" app_name = crate::get_app_name(), ); } + let mut flutter_copy = Default::default(); + if options.contains("--flutter") { + flutter_copy = format!( + "XCOPY \"{}\" \"{}\" /Y /E /H /C /I /K /R /Z", + std::env::current_exe()? + .parent() + .unwrap() + .to_string_lossy() + .to_string(), + path + ); + } let meta = std::fs::symlink_metadata(std::env::current_exe()?)?; let size = meta.len() / 1024; @@ -1052,6 +1064,7 @@ if exist \"{tmp_path}\\{app_name} Tray.lnk\" del /f /q \"{tmp_path}\\{app_name} {uninstall_str} chcp 65001 md \"{path}\" +{flutter_copy} copy /Y \"{src_exe}\" \"{exe}\" copy /Y \"{ORIGIN_PROCESS_EXE}\" \"{path}\\{broker_exe}\" \"{src_exe}\" --extract \"{path}\" @@ -1114,6 +1127,7 @@ sc delete {app_name} } else { &dels }, + flutter_copy = flutter_copy, ); run_cmds(cmds, debug, "install")?; std::thread::sleep(std::time::Duration::from_millis(2000)); From 3d7736836f372f77063468a2873798b5ab13e223 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 11 Oct 2022 19:52:03 +0800 Subject: [PATCH 0675/2015] feat: add dbus and cli connect support --- Cargo.lock | 11 +++ Cargo.toml | 2 + build.py | 4 + flutter/lib/common.dart | 14 +++ .../lib/desktop/pages/desktop_home_page.dart | 3 + flutter/lib/main.dart | 2 + flutter/lib/models/model.dart | 4 + flutter/lib/models/native_model.dart | 4 + flutter/pubspec.lock | 14 +-- res/PKGBUILD | 1 + res/pacman_install | 3 + res/rpm-suse.spec | 4 + res/rpm.spec | 4 + res/rustdesk-link.desktop | 11 +++ src/core_main.rs | 42 ++++++++- src/flutter_ffi.rs | 11 +++ src/server.rs | 2 + src/server/dbus.rs | 92 +++++++++++++++++++ 18 files changed, 219 insertions(+), 9 deletions(-) create mode 100644 res/rustdesk-link.desktop create mode 100644 src/server/dbus.rs diff --git a/Cargo.lock b/Cargo.lock index 48bfd0e20..159eee6e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1229,6 +1229,15 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "dbus-crossroads" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "554114296d012b33fdaf362a733db6dc5f73c4c9348b8b620ddd42e61b406e30" +dependencies = [ + "dbus", +] + [[package]] name = "dconf_rs" version = "0.3.0" @@ -4361,6 +4370,8 @@ dependencies = [ "ctrlc", "dark-light", "dasp", + "dbus", + "dbus-crossroads", "default-net", "dispatch", "enigo", diff --git a/Cargo.toml b/Cargo.toml index f04b36b5c..8cf338647 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -110,6 +110,8 @@ rust-pulsectl = { git = "https://github.com/open-trade/pulsectl" } async-process = "1.3" mouce = { git="https://github.com/fufesou/mouce.git" } evdev = { git="https://github.com/fufesou/evdev" } +dbus = "0.9" +dbus-crossroads = "0.5" [target.'cfg(target_os = "android")'.dependencies] diff --git a/build.py b/build.py index 85dab7090..3e9fe813d 100755 --- a/build.py +++ b/build.py @@ -165,6 +165,8 @@ def build_flutter_deb(version): 'cp ../res/128x128@2x.png tmpdeb/usr/share/rustdesk/files/rustdesk.png') os.system( 'cp ../res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop') + os.system( + 'cp ../res/rustdesk-link.desktop tmpdeb/usr/share/applications/rustdesk-link.desktop') os.system( 'cp ../res/com.rustdesk.RustDesk.policy tmpdeb/usr/share/polkit-1/actions/') os.system("echo \"#!/bin/sh\" >> tmpdeb/usr/share/rustdesk/files/polkit && chmod a+x tmpdeb/usr/share/rustdesk/files/polkit") @@ -333,6 +335,8 @@ def main(): 'cp res/128x128@2x.png tmpdeb/usr/share/rustdesk/files/rustdesk.png') os.system( 'cp res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop') + os.system( + 'cp res/rustdesk-link.desktop tmpdeb/usr/share/applications/rustdesk-link.desktop') os.system('cp -a res/DEBIAN/* tmpdeb/DEBIAN/') os.system('strip tmpdeb/usr/bin/rustdesk') os.system('mkdir -p tmpdeb/usr/lib/rustdesk') diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 7be5807a6..6f41aa27a 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -10,6 +10,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; +import 'package:flutter_hbb/main.dart'; import 'package:flutter_hbb/models/peer_model.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:get/get.dart'; @@ -1128,6 +1129,19 @@ Future restoreWindowPosition(WindowType type, {int? windowId}) async { return false; } +void checkArguments() { + // check connect args + final connectIndex = bootArgs.indexOf("--connect"); + if (connectIndex == -1) { + return; + } + String? peerId = bootArgs.length < connectIndex + 1 ? null: bootArgs[connectIndex + 1]; + if (peerId != null) { + rustDeskWinManager.newRemoteDesktop(peerId); + bootArgs.removeAt(connectIndex); bootArgs.removeAt(connectIndex); + } +} + /// Connect to a peer with [id]. /// If [isFileTransfer], starts a session only for file transfer. /// If [isTcpTunneling], starts a session only for tcp tunneling. diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 017bf5436..4bdbbbaac 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -452,6 +452,9 @@ class _DesktopHomePageState extends State } } }); + Future.delayed(Duration.zero, () { + checkArguments(); + }); } @override diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 27b14be09..549a07517 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -24,10 +24,12 @@ import 'mobile/pages/server_page.dart'; import 'models/platform_model.dart'; int? windowId; +late List bootArgs; Future main(List args) async { WidgetsFlutterBinding.ensureInitialized(); debugPrint("launch args: $args"); + bootArgs = args; if (!isDesktop) { runMobileApp(); diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 6c800147e..b57d1fbc1 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -14,6 +14,7 @@ import 'package:flutter_hbb/models/chat_model.dart'; import 'package:flutter_hbb/models/file_model.dart'; import 'package:flutter_hbb/models/server_model.dart'; import 'package:flutter_hbb/models/user_model.dart'; +import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:tuple/tuple.dart'; import 'package:flutter_custom_cursor/flutter_custom_cursor.dart'; @@ -176,6 +177,9 @@ class FfiModel with ChangeNotifier { updateBlockInputState(evt, peerId); } else if (name == 'update_privacy_mode') { updatePrivacyMode(evt, peerId); + } else if (name == 'new_connection') { + final remoteId = evt['peer_id']; + rustDeskWinManager.newRemoteDesktop(remoteId); } }; } diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index 54895f947..5c724f59b 100644 --- a/flutter/lib/models/native_model.dart +++ b/flutter/lib/models/native_model.dart @@ -113,6 +113,10 @@ class PlatformFFI { debugPrint('Failed to get documents directory: $e'); } _ffiBind = RustdeskImpl(dylib); + if (Platform.isLinux) { + // start dbus service, no need to await + await _ffiBind.mainStartDbusServer(); + } _startListenEvent(_ffiBind); // global event try { if (isAndroid) { diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index b3308bb9d..b7971eb92 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -140,7 +140,7 @@ packages: name: characters url: "https://pub.flutter-io.cn" source: hosted - version: "1.2.0" + version: "1.2.1" charcode: dependency: transitive description: @@ -161,7 +161,7 @@ packages: name: clock url: "https://pub.flutter-io.cn" source: hosted - version: "1.1.0" + version: "1.1.1" code_builder: dependency: transitive description: @@ -369,8 +369,8 @@ packages: dependency: "direct main" description: path: "." - ref: "4a950fd3a5a228bf5381070a4c803919d5787c07" - resolved-ref: "4a950fd3a5a228bf5381070a4c803919d5787c07" + ref: dec2166e881c47d922e1edc484d10d2cd5c2103b + resolved-ref: dec2166e881c47d922e1edc484d10d2cd5c2103b url: "https://github.com/Kingtous/rustdesk_flutter_custom_cursor" source: git version: "0.0.1" @@ -602,7 +602,7 @@ packages: name: material_color_utilities url: "https://pub.flutter-io.cn" source: hosted - version: "0.1.4" + version: "0.1.5" menu_base: dependency: transitive description: @@ -616,7 +616,7 @@ packages: name: meta url: "https://pub.flutter-io.cn" source: hosted - version: "1.7.0" + version: "1.8.0" mime: dependency: transitive description: @@ -693,7 +693,7 @@ packages: name: path url: "https://pub.flutter-io.cn" source: hosted - version: "1.8.1" + version: "1.8.2" path_drawing: dependency: transitive description: diff --git a/res/PKGBUILD b/res/PKGBUILD index 90e00df19..ff9ab1a28 100644 --- a/res/PKGBUILD +++ b/res/PKGBUILD @@ -30,5 +30,6 @@ package() { pushd ${pkgdir} && ln -s /usr/lib/rustdesk/rustdesk usr/bin/rustdesk && popd install -Dm 644 $HBB/res/rustdesk.service -t "${pkgdir}/usr/share/rustdesk/files" install -Dm 644 $HBB/res/rustdesk.desktop -t "${pkgdir}/usr/share/rustdesk/files" + install -Dm 644 $HBB/res/rustdesk-link.desktop -t "${pkgdir}/usr/share/rustdesk/files" install -Dm 644 $HBB/res/128x128@2x.png "${pkgdir}/usr/share/rustdesk/files/rustdesk.png" } diff --git a/res/pacman_install b/res/pacman_install index eeef34028..bcd69077d 100644 --- a/res/pacman_install +++ b/res/pacman_install @@ -7,6 +7,7 @@ post_install() { # do something here cp /usr/share/rustdesk/files/rustdesk.service /etc/systemd/system/rustdesk.service cp /usr/share/rustdesk/files/rustdesk.desktop /usr/share/applications/ + cp /usr/share/rustdesk/files/rustdesk-link.desktop /usr/share/applications/ systemctl daemon-reload systemctl enable rustdesk systemctl start rustdesk @@ -24,6 +25,7 @@ pre_upgrade() { post_upgrade() { cp /usr/share/rustdesk/files/rustdesk.service /etc/systemd/system/rustdesk.service cp /usr/share/rustdesk/files/rustdesk.desktop /usr/share/applications/ + cp /usr/share/rustdesk/files/rustdesk-link.desktop /usr/share/applications/ systemctl daemon-reload systemctl enable rustdesk systemctl start rustdesk @@ -40,5 +42,6 @@ pre_remove() { # arg 1: the old package version post_remove() { rm /usr/share/applications/rustdesk.desktop || true + rm /usr/share/applications/rustdesk-link.desktop || true update-desktop-database } diff --git a/res/rpm-suse.spec b/res/rpm-suse.spec index a22171242..85f01e37e 100644 --- a/res/rpm-suse.spec +++ b/res/rpm-suse.spec @@ -25,6 +25,7 @@ install $HBB/libsciter-gtk.so %{buildroot}/usr/lib/rustdesk/libsciter-gtk.so install $HBB/res/rustdesk.service %{buildroot}/usr/share/rustdesk/files/ install $HBB/res/128x128@2x.png %{buildroot}/usr/share/rustdesk/files/rustdesk.png install $HBB/res/rustdesk.desktop %{buildroot}/usr/share/rustdesk/files/ +install $HBB/res/rustdesk-link.desktop %{buildroot}/usr/share/rustdesk/files/ %files /usr/bin/rustdesk @@ -32,6 +33,7 @@ install $HBB/res/rustdesk.desktop %{buildroot}/usr/share/rustdesk/files/ /usr/share/rustdesk/files/rustdesk.service /usr/share/rustdesk/files/rustdesk.png /usr/share/rustdesk/files/rustdesk.desktop +/usr/share/rustdesk/files/rustdesk-link.desktop %changelog # let's skip this for now @@ -52,6 +54,7 @@ esac %post cp /usr/share/rustdesk/files/rustdesk.service /etc/systemd/system/rustdesk.service cp /usr/share/rustdesk/files/rustdesk.desktop /usr/share/applications/ +cp /usr/share/rustdesk/files/rustdesk-link.desktop /usr/share/applications/ systemctl daemon-reload systemctl enable rustdesk systemctl start rustdesk @@ -75,6 +78,7 @@ case "$1" in 0) # for uninstall rm /usr/share/applications/rustdesk.desktop || true + rm /usr/share/applications/rustdesk-link.desktop || true update-desktop-database ;; 1) diff --git a/res/rpm.spec b/res/rpm.spec index 8e04eaac5..80887de96 100644 --- a/res/rpm.spec +++ b/res/rpm.spec @@ -25,6 +25,7 @@ install $HBB/libsciter-gtk.so %{buildroot}/usr/lib/rustdesk/libsciter-gtk.so install $HBB/res/rustdesk.service %{buildroot}/usr/share/rustdesk/files/ install $HBB/res/128x128@2x.png %{buildroot}/usr/share/rustdesk/files/rustdesk.png install $HBB/res/rustdesk.desktop %{buildroot}/usr/share/rustdesk/files/ +install $HBB/res/rustdesk-link.desktop %{buildroot}/usr/share/rustdesk/files/ %files /usr/bin/rustdesk @@ -32,6 +33,7 @@ install $HBB/res/rustdesk.desktop %{buildroot}/usr/share/rustdesk/files/ /usr/share/rustdesk/files/rustdesk.service /usr/share/rustdesk/files/rustdesk.png /usr/share/rustdesk/files/rustdesk.desktop +/usr/share/rustdesk/files/rustdesk-link.desktop /usr/share/rustdesk/files/__pycache__/* %changelog @@ -53,6 +55,7 @@ esac %post cp /usr/share/rustdesk/files/rustdesk.service /etc/systemd/system/rustdesk.service cp /usr/share/rustdesk/files/rustdesk.desktop /usr/share/applications/ +cp /usr/share/rustdesk/files/rustdesk-link.desktop /usr/share/applications/ systemctl daemon-reload systemctl enable rustdesk systemctl start rustdesk @@ -76,6 +79,7 @@ case "$1" in 0) # for uninstall rm /usr/share/applications/rustdesk.desktop || true + rm /usr/share/applications/rustdesk-link.desktop || true update-desktop-database ;; 1) diff --git a/res/rustdesk-link.desktop b/res/rustdesk-link.desktop new file mode 100644 index 000000000..9ab6f495e --- /dev/null +++ b/res/rustdesk-link.desktop @@ -0,0 +1,11 @@ +[Desktop Entry] +Name=RustDeskURL Scheme Handler +NoDisplay=true +MimeType=x-scheme-handler/rustdesk; +TryExec=rustdesk +Exec=rustdesk --connect "%u" +Icon=rustdesk +Terminal=false +Type=Application +StartupNotify=false +Version=1.5 diff --git a/src/core_main.rs b/src/core_main.rs index ac05b20bf..416c2965f 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -1,3 +1,5 @@ +use std::env::Args; + use hbb_common::log; // shared by flutter and sciter main function @@ -11,6 +13,7 @@ pub fn core_main() -> Option> { let mut is_setup = false; let mut _is_elevate = false; let mut _is_run_as_system = false; + let mut _is_connect = false; for arg in std::env::args() { // to-do: how to pass to flutter? if i == 0 && crate::common::is_setup(&arg) { @@ -20,14 +23,16 @@ pub fn core_main() -> Option> { _is_elevate = true; } else if arg == "--run-as-system" { _is_run_as_system = true; + } else if arg == "--connect" { + _is_connect = true; } else { args.push(arg); } } i += 1; } - if args.contains(&"--install".to_string()) { - is_setup = true; + if _is_connect { + return core_main_invoke_new_connection(std::env::args()); } if is_setup { if args.is_empty() { @@ -208,3 +213,36 @@ fn import_config(path: &str) { } } } + +/// invoke a new connection +/// +/// [Note] +/// this is for invoke new connection from dbus +fn core_main_invoke_new_connection(mut args: Args) -> Option> { + args + .position(|element| { + return element == "--connect"; + }) + .unwrap(); + let peer_id = args.next().unwrap_or("".to_string()); + if peer_id.is_empty() { + eprintln!("please provide a valid peer id"); + return None; + } + #[cfg(target_os = "linux")] + { + use crate::dbus::invoke_new_connection; + + match invoke_new_connection(peer_id) { + Ok(()) => { + return None; + } + Err(err) => { + log::error!("{}", err.as_ref()); + // return Some to invoke this new connection by self + return Some(Vec::new()); + } + } + } + return None; +} diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index f788c679d..873248cca 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -801,6 +801,17 @@ pub fn main_is_release() -> bool { is_release() } +pub fn main_start_dbus_server() { + #[cfg(target_os = "linux")] + { + use crate::dbus::start_dbus_server; + // spawn new thread to start dbus server + std::thread::spawn(|| { + let _ = start_dbus_server(); + }); + } +} + pub fn session_send_mouse(id: String, msg: String) { if let Ok(m) = serde_json::from_str::>(&msg) { let alt = m.get("alt").is_some(); diff --git a/src/server.rs b/src/server.rs index 12b5afe56..cb3fd7c9d 100644 --- a/src/server.rs +++ b/src/server.rs @@ -30,6 +30,8 @@ mod clipboard_service; mod wayland; #[cfg(target_os = "linux")] pub mod uinput; +#[cfg(target_os = "linux")] +pub mod dbus; pub mod input_service; } else { mod clipboard_service { diff --git a/src/server/dbus.rs b/src/server/dbus.rs new file mode 100644 index 000000000..a7b21f0dc --- /dev/null +++ b/src/server/dbus.rs @@ -0,0 +1,92 @@ +/// Url handler based on dbus +/// +/// Note: +/// On linux, we use dbus to communicate multiple rustdesk process. +/// [Flutter]: handle uni links for linux +use dbus::blocking::Connection; +use dbus_crossroads::{Crossroads, IfaceBuilder}; +use hbb_common::{log}; +use std::{error::Error, fmt, time::Duration, collections::HashMap}; + +const DBUS_NAME: &str = "org.rustdesk.rustdesk"; +const DBUS_PREFIX: &str = "/dbus"; +const DBUS_METHOD_NEW_CONNECTION: &str = "NewConnection"; +const DBUS_METHOD_NEW_CONNECTION_ID: &str = "id"; +const DBUS_METHOD_RETURN: &str = "ret"; +const DBUS_METHOD_RETURN_SUCCESS: &str = "ok"; +const DBUS_TIMEOUT: Duration = Duration::from_secs(5); + +#[derive(Debug)] +struct DbusError(String); + +impl fmt::Display for DbusError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "RustDesk DBus Error: {}", self.0) + } +} + +impl Error for DbusError {} + +/// invoke new connection from dbus +/// +/// [Tips]: +/// How to test by CLI: +/// - use dbus-send command: +/// `dbus-send --session --print-reply --dest=org.rustdesk.rustdesk /dbus org.rustdesk.rustdesk.NewConnection string:'PEER_ID'` +pub fn invoke_new_connection(peer_id: String) -> Result<(), Box> { + let conn = Connection::new_session()?; + let proxy = conn.with_proxy(DBUS_NAME, DBUS_PREFIX, DBUS_TIMEOUT); + let (ret,): (String,) = proxy.method_call(DBUS_NAME, DBUS_METHOD_NEW_CONNECTION, (peer_id,))?; + if ret != DBUS_METHOD_RETURN_SUCCESS { + log::error!("error on call new connection to dbus server"); + return Err(Box::new(DbusError("not success".to_string()))); + } + Ok(()) +} + +/// start dbus server +/// +/// [Blocking]: +/// The function will block current thread to serve dbus server. +/// So it's suitable to spawn a new thread dedicated to dbus server. +pub fn start_dbus_server() -> Result<(), Box> { + let conn: Connection = Connection::new_session()?; + let _ = conn.request_name(DBUS_NAME, false, true, false)?; + let mut cr = Crossroads::new(); + let token = cr.register(DBUS_NAME, handle_client_message); + cr.insert(DBUS_PREFIX, &[token], ()); + cr.serve(&conn)?; + Ok(()) +} + +fn handle_client_message(builder: &mut IfaceBuilder<()>) { + // register new connection dbus + builder.method( + DBUS_METHOD_NEW_CONNECTION, + (DBUS_METHOD_NEW_CONNECTION_ID,), + (DBUS_METHOD_RETURN,), + move |_, _, (peer_id,): (String,)| { + #[cfg(feature = "flutter")] + { + use crate::flutter::{self, APP_TYPE_MAIN}; + + if let Some(stream) = flutter::GLOBAL_EVENT_STREAM + .write() + .unwrap() + .get(APP_TYPE_MAIN) + { + let data = HashMap::from([ + ("name", "new_connection"), + ("peer_id", peer_id.as_str()) + ]); + if !stream.add(serde_json::ser::to_string(&data).unwrap_or("".to_string())) { + log::error!("failed to add dbus message to flutter global dbus stream."); + } + } else { + log::error!("failed to find main event stream"); + } + } + return Ok((DBUS_METHOD_RETURN_SUCCESS.to_string(),)); + }, + ); +} From 150057f92ded133b0e2b32a7331d8303ab9930a4 Mon Sep 17 00:00:00 2001 From: 21pages Date: Wed, 12 Oct 2022 16:06:15 +0800 Subject: [PATCH 0676/2015] fix default video save directory Signed-off-by: 21pages --- .../desktop/pages/desktop_setting_page.dart | 6 +-- libs/scrap/src/common/record.rs | 43 ++++++++----------- src/client.rs | 3 +- src/core_main.rs | 3 ++ src/platform/linux.rs | 12 ++++++ src/platform/macos.rs | 12 ++++++ src/platform/windows.rs | 13 ++++++ src/server/video_service.rs | 3 +- src/ui_interface.rs | 24 ++++++++++- 9 files changed, 87 insertions(+), 32 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 1534f9394..7dc18679d 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -338,8 +338,8 @@ class _GeneralState extends State<_General> { } else { dir = defaultDirectory; } - final canlaunch = await canLaunchUrl(Uri.file(dir)); - return {'dir': dir, 'canlaunch': canlaunch}; + // canLaunchUrl blocked on windows portable, user SYSTEM + return {'dir': dir, 'canlaunch': true}; }(), hasData: (data) { Map map = data as Map; String dir = map['dir']!; @@ -703,7 +703,7 @@ class _Network extends StatefulWidget { class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin { @override bool get wantKeepAlive => true; - bool locked = true; + bool locked = bind.mainIsInstalled(); @override Widget build(BuildContext context) { diff --git a/libs/scrap/src/common/record.rs b/libs/scrap/src/common/record.rs index e8bbacf02..83bd9eee7 100644 --- a/libs/scrap/src/common/record.rs +++ b/libs/scrap/src/common/record.rs @@ -3,7 +3,7 @@ use hbb_common::anyhow::anyhow; use hbb_common::{ bail, chrono, config::Config, - directories_next, + log, message_proto::{message, video_frame, EncodedVideoFrame, Message}, ResultType, }; @@ -23,7 +23,7 @@ use webm::mux::{self, Segment, Track, VideoTrack, Writer}; const MIN_SECS: u64 = 1; #[derive(Debug, Clone, PartialEq)] -pub enum RecodeCodecID { +pub enum RecordCodecID { VP9, H264, H265, @@ -32,10 +32,11 @@ pub enum RecodeCodecID { #[derive(Debug, Clone)] pub struct RecorderContext { pub id: String, + pub default_dir: String, pub filename: String, pub width: usize, pub height: usize, - pub codec_id: RecodeCodecID, + pub codec_id: RecordCodecID, } impl RecorderContext { @@ -46,30 +47,22 @@ impl RecorderContext { std::fs::create_dir_all(&dir)?; } } else { - dir = Self::default_save_directory(); + dir = self.default_dir.clone(); if !dir.is_empty() && !PathBuf::from(&dir).exists() { std::fs::create_dir_all(&dir)?; } } let file = self.id.clone() + &chrono::Local::now().format("_%Y%m%d%H%M%S").to_string() - + if self.codec_id == RecodeCodecID::VP9 { + + if self.codec_id == RecordCodecID::VP9 { ".webm" } else { ".mp4" }; self.filename = PathBuf::from(&dir).join(file).to_string_lossy().to_string(); + log::info!("video save to:{}", self.filename); Ok(()) } - - pub fn default_save_directory() -> String { - if let Some(user) = directories_next::UserDirs::new() { - if let Some(video_dir) = user.video_dir() { - return video_dir.join("RustDesk").to_string_lossy().to_string(); - } - } - "".to_owned() - } } unsafe impl Send for Recorder {} @@ -105,7 +98,7 @@ impl Recorder { pub fn new(mut ctx: RecorderContext) -> ResultType { ctx.set_filename()?; let recorder = match ctx.codec_id { - RecodeCodecID::VP9 => Recorder { + RecordCodecID::VP9 => Recorder { inner: Box::new(WebmRecorder::new(ctx.clone())?), ctx, }, @@ -123,7 +116,7 @@ impl Recorder { fn change(&mut self, mut ctx: RecorderContext) -> ResultType<()> { ctx.set_filename()?; self.inner = match ctx.codec_id { - RecodeCodecID::VP9 => Box::new(WebmRecorder::new(ctx.clone())?), + RecordCodecID::VP9 => Box::new(WebmRecorder::new(ctx.clone())?), #[cfg(feature = "hwcodec")] _ => Box::new(HwRecorder::new(ctx.clone())?), #[cfg(not(feature = "hwcodec"))] @@ -144,9 +137,9 @@ impl Recorder { pub fn write_frame(&mut self, frame: &video_frame::Union) -> ResultType<()> { match frame { video_frame::Union::Vp9s(vp9s) => { - if self.ctx.codec_id != RecodeCodecID::VP9 { + if self.ctx.codec_id != RecordCodecID::VP9 { self.change(RecorderContext { - codec_id: RecodeCodecID::VP9, + codec_id: RecordCodecID::VP9, ..self.ctx.clone() })?; } @@ -154,25 +147,25 @@ impl Recorder { } #[cfg(feature = "hwcodec")] video_frame::Union::H264s(h264s) => { - if self.ctx.codec_id != RecodeCodecID::H264 { + if self.ctx.codec_id != RecordCodecID::H264 { self.change(RecorderContext { - codec_id: RecodeCodecID::H264, + codec_id: RecordCodecID::H264, ..self.ctx.clone() })?; } - if self.ctx.codec_id == RecodeCodecID::H264 { + if self.ctx.codec_id == RecordCodecID::H264 { h264s.frames.iter().map(|f| self.write_video(f)).count(); } } #[cfg(feature = "hwcodec")] video_frame::Union::H265s(h265s) => { - if self.ctx.codec_id != RecodeCodecID::H265 { + if self.ctx.codec_id != RecordCodecID::H265 { self.change(RecorderContext { - codec_id: RecodeCodecID::H265, + codec_id: RecordCodecID::H265, ..self.ctx.clone() })?; } - if self.ctx.codec_id == RecodeCodecID::H265 { + if self.ctx.codec_id == RecordCodecID::H265 { h265s.frames.iter().map(|f| self.write_video(f)).count(); } } @@ -266,7 +259,7 @@ impl RecorderApi for HwRecorder { filename: ctx.filename.clone(), width: ctx.width, height: ctx.height, - is265: ctx.codec_id == RecodeCodecID::H265, + is265: ctx.codec_id == RecordCodecID::H265, framerate: crate::hwcodec::DEFAULT_TIME_BASE[1] as _, }) .map_err(|_| anyhow!("Failed to create hardware muxer"))?; diff --git a/src/client.rs b/src/client.rs index df18d9b0a..8723480f3 100644 --- a/src/client.rs +++ b/src/client.rs @@ -860,10 +860,11 @@ impl VideoHandler { if start { self.recorder = Recorder::new(RecorderContext { id, + default_dir: crate::ui_interface::default_video_save_directory(), filename: "".to_owned(), width: w as _, height: h as _, - codec_id: scrap::record::RecodeCodecID::VP9, + codec_id: scrap::record::RecordCodecID::VP9, }) .map_or(Default::default(), |r| Arc::new(Mutex::new(Some(r)))); } else { diff --git a/src/core_main.rs b/src/core_main.rs index 416c2965f..5a72cea9c 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -33,6 +33,9 @@ pub fn core_main() -> Option> { } if _is_connect { return core_main_invoke_new_connection(std::env::args()); + } + if args.contains(&"--install".to_string()) { + is_setup = true; } if is_setup { if args.is_empty() { diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 89e2d296d..5ff69d732 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -4,6 +4,7 @@ use hbb_common::{allow_err, bail, log}; use libc::{c_char, c_int, c_void}; use std::{ cell::RefCell, + path::PathBuf, sync::{ atomic::{AtomicBool, Ordering}, Arc, @@ -516,6 +517,17 @@ pub fn get_active_username() -> String { get_value_of_seat0(2) } +pub fn get_active_user_home() -> Option { + let username = get_active_username(); + if !username.is_empty() { + let home = PathBuf::from(format!("/home/{}", username)); + if home.exists() { + return Some(home); + } + } + None +} + pub fn is_prelogin() -> bool { let n = get_active_userid().len(); n < 4 && n > 1 diff --git a/src/platform/macos.rs b/src/platform/macos.rs index 1a8096587..3273a22a5 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -20,6 +20,7 @@ use hbb_common::{bail, log}; use include_dir::{include_dir, Dir}; use objc::{class, msg_send, sel, sel_impl}; use scrap::{libc::c_void, quartz::ffi::*}; +use std::path::PathBuf; static PRIVILEGES_SCRIPTS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/src/platform/privileges_scripts"); @@ -374,6 +375,17 @@ pub fn get_active_userid() -> String { get_active_user("-n") } +pub fn get_active_user_home() -> Option { + let username = get_active_username(); + if !username.is_empty() { + let home = PathBuf::from(format!("/Users/{}", username)); + if home.exists() { + return Some(home); + } + } + None +} + pub fn is_prelogin() -> bool { get_active_userid() == "0" } diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 6732f2c36..190a49a16 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -10,6 +10,7 @@ use std::io::prelude::*; use std::{ ffi::{CString, OsString}, fs, io, mem, + path::PathBuf, sync::{Arc, Mutex}, time::{Duration, Instant}, }; @@ -734,6 +735,18 @@ pub fn get_active_username() -> String { .to_owned() } +pub fn get_active_user_home() -> Option { + let username = get_active_username(); + if !username.is_empty() { + let drive = std::env::var("SystemDrive").unwrap_or("C:".to_owned()); + let home = PathBuf::from(format!("{}\\Users\\{}", drive, username)); + if home.exists() { + return Some(home); + } + } + None +} + pub fn is_prelogin() -> bool { let username = get_active_username(); username.is_empty() || username == "SYSTEM" diff --git a/src/server/video_service.rs b/src/server/video_service.rs index ad6f5f620..fad66ffb4 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -443,10 +443,11 @@ fn run(sp: GenericService) -> ResultType<()> { let recorder = if !Config::get_option("allow-auto-record-incoming").is_empty() { Recorder::new(RecorderContext { id: "local".to_owned(), + default_dir: crate::ui_interface::default_video_save_directory(), filename: "".to_owned(), width: c.width, height: c.height, - codec_id: scrap::record::RecodeCodecID::VP9, + codec_id: scrap::record::RecordCodecID::VP9, }) .map_or(Default::default(), |r| Arc::new(Mutex::new(Some(r)))) } else { diff --git a/src/ui_interface.rs b/src/ui_interface.rs index c42a99037..9952a5b35 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -10,6 +10,7 @@ use hbb_common::password_security; use hbb_common::{ allow_err, config::{self, Config, LocalConfig, PeerConfig, RENDEZVOUS_PORT, RENDEZVOUS_TIMEOUT}, + directories_next, futures::future::join_all, log, protobuf::Message as _, @@ -19,8 +20,8 @@ use hbb_common::{ tokio::{self, sync::mpsc, time}, }; -use crate::common::SOFTWARE_UPDATE_URL; use crate::ipc; +use crate::{common::SOFTWARE_UPDATE_URL, platform}; type Message = RendezvousMessage; @@ -731,7 +732,26 @@ pub fn get_langs() -> String { #[inline] pub fn default_video_save_directory() -> String { - scrap::record::RecorderContext::default_save_directory() + let appname = crate::get_app_name(); + if let Some(user) = directories_next::UserDirs::new() { + if let Some(video_dir) = user.video_dir() { + return video_dir.join(appname).to_string_lossy().to_string(); + } + } + if let Some(home) = platform::get_active_user_home() { + let name = if cfg!(target_os = "macos") { + "Movies" + } else { + "Videos" + }; + return home.join(name).join(appname).to_string_lossy().to_string(); + } + if let Ok(exe) = std::env::current_exe() { + if let Some(dir) = exe.parent() { + return dir.join("videos").to_string_lossy().to_string(); + } + } + "".to_owned() } #[inline] From 2eab0d8832a161fee85c7f269514b6641c6fdc89 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Wed, 12 Oct 2022 21:57:19 +0800 Subject: [PATCH 0677/2015] feat: add rustdesk uni links protocol --- flutter/lib/common.dart | 34 ++++++++++++++++++++++++++++++---- flutter/lib/consts.dart | 3 +++ flutter/lib/main.dart | 2 +- flutter/lib/models/model.dart | 10 ++++++++-- 4 files changed, 42 insertions(+), 7 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 6f41aa27a..890feac6b 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1135,10 +1135,36 @@ void checkArguments() { if (connectIndex == -1) { return; } - String? peerId = bootArgs.length < connectIndex + 1 ? null: bootArgs[connectIndex + 1]; - if (peerId != null) { - rustDeskWinManager.newRemoteDesktop(peerId); - bootArgs.removeAt(connectIndex); bootArgs.removeAt(connectIndex); + String? arg = + bootArgs.length < connectIndex + 1 ? null : bootArgs[connectIndex + 1]; + if (arg != null) { + if (arg.startsWith(kUniLinksPrefix)) { + parseRustdeskUri(arg); + } else { + // fallback to peer id + rustDeskWinManager.newRemoteDesktop(arg); + bootArgs.removeAt(connectIndex); + bootArgs.removeAt(connectIndex); + } + } +} + +/// Parse `rustdesk://` unilinks +/// +/// [Functions] +/// 1. New Connection: rustdesk://connection/new/your_peer_id +void parseRustdeskUri(String uriPath) { + final uri = Uri.tryParse(uriPath); + if (uri == null) { + print("uri is not valid: $uriPath"); + return; + } + // new connection + if (uri.authority == "connection" && uri.path.startsWith("/new/")) { + final peerId = uri.path.substring("/new/".length); + Future.delayed(Duration.zero, () { + rustDeskWinManager.newRemoteDesktop(peerId); + }); } } diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index ce6c93c00..056cc000c 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -9,6 +9,9 @@ const String kAppTypeDesktopRemote = "remote"; const String kAppTypeDesktopFileTransfer = "file transfer"; const String kAppTypeDesktopPortForward = "port forward"; +const String kUniLinksPrefix = "rustdesk://"; +const String kActionNewConnection = "connection/new/"; + const String kTabLabelHomePage = "Home"; const String kTabLabelSettingPage = "Settings"; diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 549a07517..43f4b9c24 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -29,7 +29,7 @@ late List bootArgs; Future main(List args) async { WidgetsFlutterBinding.ensureInitialized(); debugPrint("launch args: $args"); - bootArgs = args; + bootArgs = List.from(args); if (!isDesktop) { runMobileApp(); diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index b57d1fbc1..8b4d89461 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -178,8 +178,14 @@ class FfiModel with ChangeNotifier { } else if (name == 'update_privacy_mode') { updatePrivacyMode(evt, peerId); } else if (name == 'new_connection') { - final remoteId = evt['peer_id']; - rustDeskWinManager.newRemoteDesktop(remoteId); + var arg = evt['peer_id'].toString(); + if (arg.startsWith(kUniLinksPrefix)) { + parseRustdeskUri(arg); + } else { + Future.delayed(Duration.zero, () { + rustDeskWinManager.newRemoteDesktop(arg); + }); + } } }; } From 953613ba9409b913ac10208b99c2da88704716a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Schultz=20Madsen?= Date: Wed, 12 Oct 2022 16:54:04 +0200 Subject: [PATCH 0678/2015] Adding Danish translations. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: René Schultz Madsen --- README.md | 4 +- docs/README-DA.md | 187 ++++++++++++++++++++++++++++++++++++++++++++++ src/lang/da.rs | 110 +++++++++++++-------------- 3 files changed, 244 insertions(+), 57 deletions(-) create mode 100644 docs/README-DA.md diff --git a/README.md b/README.md index d294f6f1e..313b1e18b 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@

    - RustDesk - Your remote desktop
    + RustDesk - Dit fjernskrivebord
    ServersBuildDockerStructureSnapshot
    - [Українська] | [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
    + [Українська] | [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt] | [Dansk]
    We need your help to translate this README, RustDesk UI and Doc to your native language

    diff --git a/docs/README-DA.md b/docs/README-DA.md new file mode 100644 index 000000000..6887aefa0 --- /dev/null +++ b/docs/README-DA.md @@ -0,0 +1,187 @@ +

    + RustDesk - Your remote desktop
    + Servere • + Byg • + Docker • + Filstruktur • + Skærmbilleder
    + [English] | [Українська] | [česky] | [中文] | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
    + Vi har brug for din hjælp til at oversætte denne README, RustDesk UI og Dokument til dit modersmål +

    + +Chat med os: [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) + +Endnu en fjernskrivebordssoftware, skrevet i Rust. Fungerer ud af æsken, ingen konfiguration påkrævet. Du har fuld kontrol over dine data uden bekymringer om sikkerhed. Du kan bruge vores rendezvous/relay-server, [opsætte din egen](https://rustdesk.com/server), eller [skrive din egen rendezvous/relay-server](https://github.com/rustdesk/rustdesk- server-demo). + +RustDesk hilser bidrag fra alle velkommen. Se [`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md) for at få hjælp til at komme i gang. + +[**PROGRAM DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases) + +## Gratis offentlige servere + +Nedenfor er de servere, du bruger gratis, det kan ændre sig med tiden. Hvis du ikke er tæt på en af disse, kan dit netværk være langsomt. + +| Beliggenhed | Udbyder | Specifikation | +| ---------- | ------------- | ------------------ | +| Seoul | AWS lightsail | 1 vCPU / 0,5 GB RAM | +| Singapore | Vultr | 1 vCPU / 1 GB RAM | +| Tyskland | Hetzner | 2 vCPU / 4 GB RAM | +| Tyskland | Codext | 4 vCPU / 8 GB RAM | + +## Afhængigheder + +Desktopversioner bruger [sciter](https://sciter.com/) eller Flutter til GUI, denne vejledning er kun for Sciter. + +Hent venligst sciter dynamic library selv. + +[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) + +## Rå trin til at bygge + +- Forbered din Rust-udviklings-env og C++ build-env + +- Installer [vcpkg](https://github.com/microsoft/vcpkg), og indstil env-variabelen "VCPKG_ROOT" korrekt + + - Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static + - Linux/MacOS: vcpkg install libvpx libyuv opus + +- kør `cargo run` + +## [Byg](https://rustdesk.com/docs/en/dev/build/) + +## Sådan bygger du på 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 +``` + +### Fedora 28 (CentOS 8) + +```sh +sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel +``` + +### Arch (Manjaro) + +```sh +sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire +``` + +### vcpkg installation + +```sh +git clone https://github.com/microsoft/vcpkg +cd vcpkg +git checkout 2021.12.01 +cd .. +vcpkg/bootstrap-vcpkg.sh +export VCPKG_ROOT=$HOME/vcpkg +vcpkg/vcpkg install libvpx libyuv opus +``` + +### libvpx rettelse (For Fedora) + +```sh +cd vcpkg/buildtrees/libvpx/src +cd * +./configure +sed -i 's/CFLAGS+=-I/CFLAGS+=-fPIC -I/g' Makefile +sed -i 's/CXXFLAGS+=-I/CXXFLAGS+=-fPIC -I/g' Makefile +make +cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/ +cd +``` + +### Byg + +```sh +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +source $HOME/.cargo/env +git clone https://github.com/rustdesk/rustdesk +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 +``` + +### Skift Wayland til X11 (Xorg) + +RustDesk understøtter ikke Wayland. Tjek [dette](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) for at konfigurere Xorg som standard GNOME-session. + +## Wayland-support + +Wayland ser ikke ud til at levere nogen API til at sende tastetryk til andre vinduer. Derfor bruger rustdesk et API fra et lavere niveau, nemlig `/dev/uinput`-enheden (Linux-kerneniveau). + +Når wayland er den kontrollerede side, skal du starte på følgende måde: +```bash +# Start uinput service +$ sudo rustdesk --service +$ rustdesk +``` +**Bemærk**: Wayland-skærmoptagelse bruger forskellige grænseflader. RustDesk understøtter i øjeblikket kun 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 +# Not support +Error org.freedesktop.DBus.Error.InvalidArgs: No such interface “org.freedesktop.portal.ScreenCast” +# Support +method return time=1662544486.931020 sender=:1.54 -> destination=:1.139 serial=257 reply_serial=2 + variant uint32 4 +``` +## Sådan bygger du med Docker + +```sh +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +docker build -t "rustdesk-builder" . +``` + +Kør derefter følgende kommando, hver gang du skal bygge applikationen: +```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 +``` + +Bemærk, at den første bygning kan tage længere tid, før afhængigheder cachelagres, efterfølgende bygninger vil være hurtigere. Derudover, hvis du har brug for at angive forskellige argumenter til bygge-kommandoen, kan du gøre det i slutningen af kommandoen i ``-positionen. For eksempel, hvis du ville bygge en optimeret udgivelsesversion, ville du køre kommandoen ovenfor efterfulgt af `--release`. Den resulterende eksekverbare vil være tilgængelig i målmappen på dit system og kan køres med: + +```sh +target/debug/rustdesk +``` + +Eller, hvis du kører en udgivelses eksekverbar: + +```sh +target/release/rustdesk +``` + +Sørg for, at du kører disse kommandoer fra roden af RustDesk-lageret, ellers kan applikationen muligvis ikke finde de nødvendige ressourcer. Bemærk også, at andre cargo underkommandoer såsom 'install' eller 'run' i øjeblikket ikke understøttes via denne metode, da de ville installere eller køre programmet inde i containeren i stedet for værten. + +## Filstruktur + +- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: video codec, config, tcp/udp wrapper, protobuf, fs funktioner til filoverførsel og nogle andre hjælpefunktioner +- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: Skærmbillede +- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: platform specifik tastatur/mus kontrol +- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI +- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: lyd/udklipsholder/input/videotjenester og netværksforbindelser +- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: starte en peer-forbindelse +- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Kommuniker med [rustdesk-server](https://github.com/rustdesk/rustdesk-server), vent på direkte fjernforbindelse (TCP-hulning) eller relæforbindelse +- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: Javascript til Flutter webklient + +## Skærmbilleder + +![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) + +![image](https://user-images.githubusercontent.com/71636191/113112619-f705a480-923b-11eb-911d-97e984ef52b6.png) + +![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png) + +![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png) diff --git a/src/lang/da.rs b/src/lang/da.rs index 073fda6a4..c5dc87d49 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -287,26 +287,26 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Turned off", "Slukket"), ("In privacy mode", "I databeskyttelsestilstand"), ("Out privacy mode", "Databeskyttelsestilstand fra"), - ("Language", ""), - ("Keep RustDesk background service", ""), - ("Ignore Battery Optimizations", ""), + ("Language", "Sprog"), + ("Keep RustDesk background service", "Behold RustDesk baggrundstjeneste"), + ("Ignore Battery Optimizations", "Ignorer betteri optimeringer"), ("android_open_battery_optimizations_tip", ""), - ("Connection not allowed", ""), - ("Legacy mode", ""), + ("Connection not allowed", "Forbindelse ikke tilladt"), + ("Legacy mode", "Bagudkompatibilitetstilstand"), ("Map mode", ""), ("Translate mode", ""), - ("Use temporary password", ""), - ("Use permanent password", ""), - ("Use both passwords", ""), - ("Set permanent password", ""), - ("Set temporary password length", ""), - ("Enable Remote Restart", ""), - ("Allow remote restart", ""), - ("Restart Remote Device", ""), - ("Are you sure you want to restart", ""), - ("Restarting Remote Device", ""), + ("Use temporary password", "Brug midlertidig adgangskode"), + ("Use permanent password", "Brug permanent adgangskode"), + ("Use both passwords", "Bug begge adgangskoder"), + ("Set permanent password", "Sæt permanent adgangskode"), + ("Set temporary password length", "Sæt midlertidig adgangskode"), + ("Enable Remote Restart", "Aktiver fjerngenstart"), + ("Allow remote restart", "Tillad fjerngenstart"), + ("Restart Remote Device", "Genstart fjernenhed"), + ("Are you sure you want to restart", "Er du sikker på at du vil genstarte"), + ("Restarting Remote Device", "Genstarter fjernenhed"), ("remote_restarting_tip", ""), - ("Copied", ""), + ("Copied", "Kopierét"), ("Exit Fullscreen", "Afslut fuldskærm"), ("Fullscreen", "Fuld skærm"), ("Mobile Actions", "Mobile handlinger"), @@ -322,50 +322,50 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Relay Connection", "Relæforbindelse"), ("Secure Connection", "Sikker forbindelse"), ("Insecure Connection", "Usikker forbindelse"), - ("Scale original", "Skala original"), - ("Scale adaptive", "Skala adaptiv"), - ("General", ""), - ("Security", ""), - ("Account", ""), - ("Theme", ""), - ("Dark Theme", ""), - ("Dark", ""), - ("Light", ""), - ("Follow System", ""), - ("Enable hardware codec", ""), - ("Unlock Security Settings", ""), - ("Enable Audio", ""), - ("Temporary Password Length", ""), - ("Unlock Network Settings", ""), - ("Server", ""), - ("Direct IP Access", ""), - ("Proxy", ""), - ("Port", ""), - ("Apply", ""), - ("Disconnect all devices?", ""), - ("Clear", ""), - ("Audio Input Device", ""), - ("Deny remote access", ""), - ("Use IP Whitelisting", ""), - ("Network", ""), - ("Enable RDP", ""), + ("Scale original", "Skaler original"), + ("Scale adaptive", "Skaler adaptiv"), + ("General", "Generelt"), + ("Security", "Sikkerhed"), + ("Account", "Konto"), + ("Theme", "Thema"), + ("Dark Theme", "Mørk Tema"), + ("Dark", "Mørk"), + ("Light", "Lys"), + ("Follow System", "Følg System"), + ("Enable hardware codec", "Aktiver hardware-codec"), + ("Unlock Security Settings", "Lås op for sikkerhedsinstillinger"), + ("Enable Audio", "Aktiver Lyd"), + ("Temporary Password Length", "Midlertidig Adgangskode Længde"), + ("Unlock Network Settings", "Lås op for Netværksinstillinger"), + ("Server", "Server"), + ("Direct IP Access", "Direkte IP Adgang"), + ("Proxy", "Proxy"), + ("Port", "Port"), + ("Apply", "Anvend"), + ("Disconnect all devices?", "Afbryd alle enheder?"), + ("Clear", "Nulstil"), + ("Audio Input Device", "Lydindgangsenhed"), + ("Deny remote access", "Afvis fjeradgang"), + ("Use IP Whitelisting", "Brug IP Whitelisting"), + ("Network", "Netværk"), + ("Enable RDP", "Aktiver RDP"), ("Pin menubar", "Fastgør menulinjen"), ("Unpin menubar", "Frigør menulinjen"), - ("Recording", ""), - ("Directory", ""), - ("Automatically record incoming sessions", ""), - ("Change", ""), - ("Start session recording", ""), - ("Stop session recording", ""), - ("Enable Recording Session", ""), - ("Allow recording session", ""), - ("Enable LAN Discovery", ""), - ("Deny LAN Discovery", ""), - ("Write a message", ""), + ("Recording", "Optager"), + ("Directory", "Mappe"), + ("Automatically record incoming sessions", "Optag automatisk indgående sessioner"), + ("Change", "Ændre"), + ("Start session recording", "Start sessionsoptagelse"), + ("Stop session recording", "Stop sessionsoptagelse"), + ("Enable Recording Session", "Aktiver optagelsessession"), + ("Allow recording session", "Tillad optagelsessession"), + ("Enable LAN Discovery", "Aktiver LAN Discovery"), + ("Deny LAN Discovery", "Afvis LAN Discovery"), + ("Write a message", "Skriv en besked"), ("Prompt", ""), ("elevation_prompt", ""), ("uac_warning", ""), ("elevated_foreground_window_warning", ""), - ("Disconnected", ""), + ("Disconnected", "Afbrudt"), ].iter().cloned().collect(); } From ed688bc06daa28aee689a07a5d4c85c11487961d Mon Sep 17 00:00:00 2001 From: Kingtous Date: Thu, 13 Oct 2022 09:58:46 +0800 Subject: [PATCH 0679/2015] fix: avoid corrupt with --connect in sciter --- src/core_main.rs | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/src/core_main.rs b/src/core_main.rs index 5a72cea9c..daa97ceb1 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -13,28 +13,34 @@ pub fn core_main() -> Option> { let mut is_setup = false; let mut _is_elevate = false; let mut _is_run_as_system = false; - let mut _is_connect = false; + let mut _is_flutter_connect = false; for arg in std::env::args() { // to-do: how to pass to flutter? if i == 0 && crate::common::is_setup(&arg) { is_setup = true; } else if i > 0 { + #[cfg(feature = "flutter")] + if arg == "--connect" { + _is_flutter_connect = true; + } if arg == "--elevate" { _is_elevate = true; } else if arg == "--run-as-system" { _is_run_as_system = true; - } else if arg == "--connect" { - _is_connect = true; } else { args.push(arg); } } i += 1; } - if _is_connect { + if args.contains(&"--install".to_string()) { + is_setup = true; + } + #[cfg(feature = "flutter")] + if _is_flutter_connect { return core_main_invoke_new_connection(std::env::args()); } - if args.contains(&"--install".to_string()) { + if args.contains(&"--install".to_string()) { is_setup = true; } if is_setup { @@ -220,13 +226,13 @@ fn import_config(path: &str) { /// invoke a new connection /// /// [Note] -/// this is for invoke new connection from dbus +/// this is for invoke new connection from dbus. +#[cfg(feature = "flutter")] fn core_main_invoke_new_connection(mut args: Args) -> Option> { - args - .position(|element| { - return element == "--connect"; - }) - .unwrap(); + args.position(|element| { + return element == "--connect"; + }) + .unwrap(); let peer_id = args.next().unwrap_or("".to_string()); if peer_id.is_empty() { eprintln!("please provide a valid peer id"); From 9ac07b518d56b5c76be49b859dc16a5e4b4672d2 Mon Sep 17 00:00:00 2001 From: Cooper Liu Date: Thu, 13 Oct 2022 10:23:52 +0800 Subject: [PATCH 0680/2015] add button for importing server config --- .../desktop/pages/desktop_setting_page.dart | 150 +++++++++++++++++- src/lang/cn.rs | 4 + src/lang/cs.rs | 4 + src/lang/da.rs | 4 + src/lang/de.rs | 4 + src/lang/eo.rs | 4 + src/lang/es.rs | 4 + src/lang/fr.rs | 4 + src/lang/hu.rs | 4 + src/lang/id.rs | 4 + src/lang/it.rs | 4 + src/lang/ja.rs | 4 + src/lang/ko.rs | 4 + src/lang/kz.rs | 4 + src/lang/pl.rs | 4 + src/lang/pt_PT.rs | 4 + src/lang/ptbr.rs | 4 + src/lang/ru.rs | 4 + src/lang/sk.rs | 4 + src/lang/template.rs | 4 + src/lang/tr.rs | 4 + src/lang/tw.rs | 4 + src/lang/ua.rs | 4 + src/lang/vn.rs | 4 + 24 files changed, 241 insertions(+), 1 deletion(-) diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 7dc18679d..45b1b9a85 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -723,8 +723,10 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin { AbsorbPointer( absorbing: locked, child: Column(children: [ - _Card(title: 'Server', children: [ + _CardRow(title: 'Server', children: [ _Button('ID/Relay Server', changeServer, enabled: enabled), + _Button('Import Server Conf', importServer, + enabled: enabled), ]), _Card(title: 'Proxy', children: [ _Button('Socks5 Proxy', changeSocks5Proxy, @@ -894,6 +896,38 @@ Widget _Card({required String title, required List children}) { ); } +Widget _CardRow({required String title, required List children}) { + return Row( + children: [ + SizedBox( + width: _kCardFixedWidth, + child: Card( + child: Column( + children: [ + Row( + children: [ + Text( + translate(title), + textAlign: TextAlign.start, + style: const TextStyle( + fontSize: _kTitleFontSize, + ), + ), + const Spacer(), + ], + ).marginOnly(left: _kContentHMargin, top: 10, bottom: 10), + Row(children: [ + ...children + .map((e) => e.marginOnly(top: 4, right: _kContentHMargin)), + ]), + ], + ).marginOnly(bottom: 10), + ).marginOnly(left: _kCardLeftMargin, top: 15), + ), + ], + ); +} + Color? _disabledTextColor(BuildContext context, bool enabled) { return enabled ? null @@ -1377,6 +1411,120 @@ void changeServer() async { }); } +void importServer() async { + Future importServerShow(String content) async { + gFFI.dialogManager.show((setState, close) { + return CustomAlertDialog( + title: Text(content), + content: ConstrainedBox( + constraints: const BoxConstraints(minWidth: 500), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + height: 4.0, + ), + ], + ), + ), + actions: [ + TextButton(onPressed: close, child: Text(translate("OK"))), + ], + onCancel: close, + ); + }); + } + + Future submit( + String idServer, String relayServer, String apiServer, String key) async { + Map oldOptions = jsonDecode(await bind.mainGetOptions()); + var idServerMsg = ""; + var relayServerMsg = ""; + if (idServer.isNotEmpty) { + idServerMsg = + translate(await bind.mainTestIfValidServer(server: idServer)); + if (idServerMsg.isEmpty) { + oldOptions['custom-rendezvous-server'] = idServer; + } else { + print('ID Server invalid return'); + return false; + } + } else { + oldOptions['custom-rendezvous-server'] = ""; + } + + if (relayServer.isNotEmpty) { + relayServerMsg = + translate(await bind.mainTestIfValidServer(server: relayServer)); + if (relayServerMsg.isEmpty) { + oldOptions['relay-server'] = relayServer; + } else { + print('Relay Server invalid return'); + return false; + } + } else { + oldOptions['relay-server'] = ""; + } + + if (apiServer.isNotEmpty) { + if (apiServer.startsWith('http://') || apiServer.startsWith("https://")) { + oldOptions['api-server'] = apiServer; + return false; + } else { + print('invalid_http'); + return false; + } + } else { + oldOptions['api-server'] = ""; + } + // ok + oldOptions['key'] = key; + await bind.mainSetOptions(json: jsonEncode(oldOptions)); + print("set ID/Realy Server Ok"); + return true; + } + + Clipboard.getData(Clipboard.kTextPlain).then((value) { + TextEditingController mytext = TextEditingController(); + String? aNullableString = ""; + aNullableString = value?.text; + mytext.text = aNullableString.toString(); + if (mytext.text.isNotEmpty) { + print('Clipboard is not empty'); + try { + Map config = jsonDecode(mytext.text); + print(config); + if (config.containsKey('IdServer') && + config.containsKey('RelayServer')) { + print('IdServer: ${config['IdServer']}'); + print('RelayServer: ${config['RelayServer']}'); + print('ApiServer: ${config['ApiServer']}'); + print('Key: ${config['Key']}'); + Future success = submit(config['IdServer'], + config['RelayServer'], config['ApiServer'], config['Key']); + success.then((value) { + if (value) { + importServerShow( + translate('Import server configuration successfully')); + } else { + importServerShow(translate('Invalid server configuration')); + } + }); + } else { + print('invalid config info'); + importServerShow(translate("Invalid server configuration")); + } + } catch (e) { + print('invalid config info'); + importServerShow(translate("Invalid server configuration")); + } + } else { + print('Clipboard is empty'); + importServerShow(translate("Clipboard is empty")); + } + }); +} + void changeSocks5Proxy() async { var socks = await bind.mainGetSocks(); diff --git a/src/lang/cn.rs b/src/lang/cn.rs index e5f01b185..35e77990c 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -29,6 +29,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable TCP Tunneling", "允许建立TCP隧道"), ("IP Whitelisting", "IP白名单"), ("ID/Relay Server", "ID/中继服务器"), + ("Import Server Conf", "导入服务器配置"), + ("Import server configuration successfully", "导入服务器配置信息成功"), + ("Invalid server configuration", "无效服务器配置,请修改后重新拷贝配置信息到剪贴板后点击此按钮"), + ("Clipboard is empty", "拷贝配置信息到剪贴板后点击此按钮,可以自动导入配置"), ("Stop service", "停止服务"), ("Change ID", "改变ID"), ("Website", "网站"), diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 9186454d1..e849dfa3f 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -29,6 +29,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable TCP Tunneling", "Povolit TCP tunelování"), ("IP Whitelisting", "Povolování pouze z daných IP adres)"), ("ID/Relay Server", "Identifikátor / předávací (relay) server"), + ("Import Server Conf", "Importovat konfiguraci serveru"), + ("Import server configuration successfully", "Konfigurace serveru úspěšně importována"), + ("Invalid server configuration", "Neplatná konfigurace serveru"), + ("Clipboard is empty", "Schránka je prázdná"), ("Stop service", "Zastavit službu"), ("Change ID", "Změnit identifikátor"), ("Website", "Webové stránky"), diff --git a/src/lang/da.rs b/src/lang/da.rs index c5dc87d49..3dd2e0a51 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -29,6 +29,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable TCP Tunneling", "Slå TCP-tunneling til"), ("IP Whitelisting", "IP-udgivelsesliste"), ("ID/Relay Server", "ID/forbindelsesserver"), + ("Import Server Conf", "Importér serverkonfiguration"), + ("Import server configuration successfully", "Importér serverkonfigurationen"), + ("Invalid server configuration", "Ugyldig serverkonfiguration"), + ("Clipboard is empty", "Udklipsholderen er tom"), ("Stop service", "Sluk for forbindelsesserveren"), ("Change ID", "Ændre ID"), ("Website", "Hjemmeside"), diff --git a/src/lang/de.rs b/src/lang/de.rs index 255ae932d..38c0f657a 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -29,6 +29,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable TCP Tunneling", "TCP-Tunnel aktivieren"), ("IP Whitelisting", "IP-Whitelist"), ("ID/Relay Server", "ID/Vermittlungsserver"), + ("Import Server Conf", "Serverkonfiguration importieren"), + ("Import server configuration successfully", "Serverkonfiguration erfolgreich importiert"), + ("Invalid server configuration", "Ungültige Serverkonfiguration"), + ("Clipboard is empty", "Zwischenablage ist leer"), ("Stop service", "Vermittlungsdienst deaktivieren"), ("Change ID", "ID ändern"), ("Website", "Webseite"), diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 18d4e0ba2..cee1975b0 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -29,6 +29,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable TCP Tunneling", "Ebligi tunelado TCP"), ("IP Whitelisting", "Listo de IP akceptataj"), ("ID/Relay Server", "Identigila/Relajsa servilo"), + ("Import Server Conf", "Enporti servilan agordon"), + ("Import server configuration successfully", "Importi servilan agordon sukcese"), + ("Invalid server configuration", "Nevalida servila agordo"), + ("Clipboard is empty", "La poŝo estas malplena"), ("Stop service", "Haltu servon"), ("Change ID", "Ŝanĝi identigilon"), ("Website", "Retejo"), diff --git a/src/lang/es.rs b/src/lang/es.rs index f6e8b803b..d3da5b402 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -29,6 +29,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable TCP Tunneling", "Habilitar tunel TCP"), ("IP Whitelisting", "Lista blanca de IP"), ("ID/Relay Server", "Servidor ID/Relay"), + ("Import Server Conf", "Importar configuración de servidor"), + ("Import server configuration successfully", "Configuración de servidor importada con éxito"), + ("Invalid server configuration", "Configuración de servidor inválida"), + ("Clipboard is empty", "El portapapeles está vacío"), ("Stop service", "Parar servicio"), ("Change ID", "Cambiar ID"), ("Website", "Sitio web"), diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 3dae5053c..70a62e49d 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -29,6 +29,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable TCP Tunneling", "Activer le tunneling TCP"), ("IP Whitelisting", "Liste blanche IP"), ("ID/Relay Server", "ID/Serveur Relais"), + ("Import Server Conf", "Importer la configuration du serveur"), + ("Import server configuration successfully", "Configuration du serveur importée avec succès"), + ("Invalid server configuration", "Configuration du serveur non valide"), + ("Clipboard is empty", "Presse-papier vide"), ("Stop service", "Arrêter le service"), ("Change ID", "Changer d'ID"), ("Website", "Site Web"), diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 14bd1a895..191165b65 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -29,6 +29,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable TCP Tunneling", "TCP Tunneling bekapcsolása"), ("IP Whitelisting", "IP Fehérlista"), ("ID/Relay Server", "ID/Relay Szerver"), + ("Import Server Conf", "Szerver Konfiguráció Importálása"), + ("Import server configuration successfully", "Szerver konfiguráció sikeresen importálva"), + ("Invalid server configuration", "Érvénytelen szerver konfiguráció"), + ("Clipboard is empty", "A vágólap üres"), ("Stop service", "Szolgáltatás Kikapcsolása"), ("Change ID", "ID Megváltoztatása"), ("Website", "Weboldal"), diff --git a/src/lang/id.rs b/src/lang/id.rs index d260e0add..96ac3892b 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -29,6 +29,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable TCP Tunneling", "Aktifkan TCP Tunneling"), ("IP Whitelisting", "Daftar Putih IP"), ("ID/Relay Server", "ID/Relay Server"), + ("Import Server Conf", "Impor Konfigurasi Server"), + ("Import server configuration successfully", "Impor konfigurasi server berhasil"), + ("Invalid server configuration", "Konfigurasi server tidak valid"), + ("Clipboard is empty", "Papan klip kosong"), ("Stop service", "Hentikan Layanan"), ("Change ID", "Ubah ID"), ("Website", "Website"), diff --git a/src/lang/it.rs b/src/lang/it.rs index fb8d80435..1954eeaae 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -29,6 +29,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable TCP Tunneling", "Abilita tunnel TCP"), ("IP Whitelisting", "IP autorizzati"), ("ID/Relay Server", "Server ID/Relay"), + ("Import Server Conf", "Importa configurazione Server"), + ("Import server configuration successfully", "Configurazione Server importata con successo"), + ("Invalid server configuration", "Configurazione Server non valida"), + ("Clipboard is empty", "Gli appunti sono vuoti"), ("Stop service", "Arresta servizio"), ("Change ID", "Cambia ID"), ("Website", "Sito web"), diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 4107760c1..0c11db665 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -29,6 +29,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable TCP Tunneling", "TCPトンネリングを有効化"), ("IP Whitelisting", "IPホワイトリスト"), ("ID/Relay Server", "認証・中継サーバー"), + ("Import Server Conf", "サーバー設定をインポート"), + ("Import server configuration successfully", "サーバー設定をインポートしました"), + ("Invalid server configuration", "無効なサーバー設定です"), + ("Clipboard is empty", "クリップボードは空です"), ("Stop service", "サービスを停止"), ("Change ID", "IDを変更"), ("Website", "公式サイト"), diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 9a8ef361d..d59d71dc9 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -29,6 +29,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable TCP Tunneling", "TCP 터널링 활성화"), ("IP Whitelisting", "IP 화이트리스트"), ("ID/Relay Server", "ID/Relay 서버"), + ("Import Server Conf", "서버 설정 가져오기"), + ("Import server configuration successfully", "서버 설정 가져오기 성공"), + ("Invalid server configuration", "잘못된 서버 설정"), + ("Clipboard is empty", "클립보드가 비어있습니다"), ("Stop service", "서비스 중단"), ("Change ID", "ID 변경"), ("Website", "웹사이트"), diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 517157c83..b4f29894d 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -29,6 +29,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable TCP Tunneling", "TCP тунелдеуді қосу"), ("IP Whitelisting", "IP Ақ-тізімі"), ("ID/Relay Server", "ID/Relay сербері"), + ("Import Server Conf", "Серверді импорттау"), + ("Import server configuration successfully", "Сервердің конфигурациясы сәтті импортталды"), + ("Invalid server configuration", "Жарамсыз сервердің конфигурациясы"), + ("Clipboard is empty", "Көшіру-тақта бос"), ("Stop service", "Сербесті тоқтату"), ("Change ID", "ID ауыстыру"), ("Website", "Web-сайт"), diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 14b95a979..03f030507 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -29,6 +29,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable TCP Tunneling", "Włącz tunelowanie TCP"), ("IP Whitelisting", "Biała lista IP"), ("ID/Relay Server", "Serwer ID/Pośredniczący"), + ("Import Server Conf", "Importuj konfigurację serwera"), + ("Import server configuration successfully", "Importowanie konfiguracji serwera powiodło się"), + ("Invalid server configuration", "Nieprawidłowa konfiguracja serwera"), + ("Clipboard is empty", "Schowek jest pusty"), ("Stop service", "Zatrzymaj usługę"), ("Change ID", "Zmień ID"), ("Website", "Strona internetowa"), diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 8798c90d9..835fae801 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -29,6 +29,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable TCP Tunneling", "Activar Túnel TCP"), ("IP Whitelisting", "Whitelist de IP"), ("ID/Relay Server", "Servidor ID/Relay"), + ("Import Server Conf", "Importar Configuração do Servidor"), + ("Import server configuration successfully", "Configuração do servidor importada com sucesso"), + ("Invalid server configuration", "Configuração do servidor inválida"), + ("Clipboard is empty", "A área de transferência está vazia"), ("Stop service", "Parar serviço"), ("Change ID", "Alterar ID"), ("Website", "Website"), diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index d59eb0624..280390d7f 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -29,6 +29,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable TCP Tunneling", "Habilitar Tunelamento TCP"), ("IP Whitelisting", "Whitelist de IP"), ("ID/Relay Server", "Servidor ID/Relay"), + ("Import Server Conf", "Importar Configuração do Servidor"), + ("Import server configuration successfully", "Configuração do servidor importada com sucesso"), + ("Invalid server configuration", "Configuração do servidor inválida"), + ("Clipboard is empty", "A área de transferência está vazia"), ("Stop service", "Parar serviço"), ("Change ID", "Alterar ID"), ("Website", "Website"), diff --git a/src/lang/ru.rs b/src/lang/ru.rs index b04a80de0..7784d74f0 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -29,6 +29,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable TCP Tunneling", "Включить туннелирование TCP"), ("IP Whitelisting", "Список разрешенных IP-адресов"), ("ID/Relay Server", "ID/Сервер ретрансляции"), + ("Import Server Conf", "Импортировать конфигурацию сервера"), + ("Import server configuration successfully", "Конфигурация сервера успешно импортирована"), + ("Invalid server configuration", "Недопустимая конфигурация сервера"), + ("Clipboard is empty", "Буфер обмена пуст"), ("Stop service", "Остановить службу"), ("Change ID", "Изменить ID"), ("Website", "Веб-сайт"), diff --git a/src/lang/sk.rs b/src/lang/sk.rs index ec93850fe..0319ddc68 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -29,6 +29,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable TCP Tunneling", "Povoliť TCP tunelovanie"), ("IP Whitelisting", "Zoznam povolených IP adries"), ("ID/Relay Server", "ID/Prepojovací server"), + ("Import Server Conf", "Importovať konfiguráciu servera"), + ("Import server configuration successfully", "Konfigurácia servera bola úspešne importovaná"), + ("Invalid server configuration", "Neplatná konfigurácia servera"), + ("Clipboard is empty", "Schránka je prázdna"), ("Stop service", "Zastaviť službu"), ("Change ID", "Zmeniť ID"), ("Website", "Webová stránka"), diff --git a/src/lang/template.rs b/src/lang/template.rs index 666e1cf02..b292f917c 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -29,6 +29,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable TCP Tunneling", ""), ("IP Whitelisting", ""), ("ID/Relay Server", ""), + ("Import server configuration successfully", ""), + ("Import Server Conf", ""), + ("Invalid server configuration", ""), + ("Clipboard is empty", ""), ("Stop service", ""), ("Change ID", ""), ("Website", ""), diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 935faea84..1a7912f23 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -29,6 +29,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable TCP Tunneling", "TCP Tüneline izin ver"), ("IP Whitelisting", "İzinli IP listesi"), ("ID/Relay Server", "ID/Relay Sunucusu"), + ("Import Server Conf", "Sunucu ayarlarını içe aktar"), + ("Import server configuration successfully", "Sunucu ayarları başarıyla içe aktarıldı"), + ("Invalid server configuration", "Geçersiz sunucu ayarı"), + ("Clipboard is empty", "Kopyalanan geçici veri boş"), ("Stop service", "Servisi Durdur"), ("Change ID", "ID Değiştir"), ("Website", "Website"), diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 9e3a040da..b588f2a33 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -29,6 +29,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable TCP Tunneling", "啟用 TCP 通道"), ("IP Whitelisting", "IP 白名單"), ("ID/Relay Server", "ID/轉送伺服器"), + ("Import Server Conf", "匯入伺服器設定"), + ("Import server configuration successfully", "匯入伺服器設定成功"), + ("Invalid server configuration", "無效的伺服器設定"), + ("Clipboard is empty", "剪貼簿是空的"), ("Stop service", "停止服務"), ("Change ID", "更改 ID"), ("Website", "網站"), diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 32f818911..3a5c4afd6 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -29,6 +29,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable TCP Tunneling", "Увімкнути тунелювання TCP"), ("IP Whitelisting", "Список дозволених IP-адрес"), ("ID/Relay Server", "ID/Сервер ретрансляції"), + ("Import Server Conf", "Імпортувати конфігурацію сервера"), + ("Import server configuration successfully", "Конфігурацію сервера успішно імпортовано"), + ("Invalid server configuration", "Недійсна конфігурація сервера"), + ("Clipboard is empty", "Буфер обміну порожній"), ("Stop service", "Зупинити службу"), ("Change ID", "Змінити ID"), ("Website", "Веб-сайт"), diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 9310c90b7..c245bdd75 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -29,6 +29,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable TCP Tunneling", "Cho phép TCP Tunneling"), ("IP Whitelisting", "Cho phép IP"), ("ID/Relay Server", "Máy chủ ID/Relay"), + ("Import Server Conf", "Nhập cấu hình máy chủ"), + ("Import server configuration successfully", "Nhập cấu hình máy chủ thành công"), + ("Invalid server configuration", "Cấu hình máy chủ không hợp lệ"), + ("Clipboard is empty", "Khay nhớ tạm trống"), ("Stop service", "Dừng dịch vụ"), ("Change ID", "Thay đổi ID"), ("Website", "Trang web"), From 9a5c0b610bda1412252d66e7d64a3281931df422 Mon Sep 17 00:00:00 2001 From: Cooper Liu Date: Thu, 13 Oct 2022 11:29:35 +0800 Subject: [PATCH 0681/2015] modify print to debugPrint --- .../desktop/pages/desktop_setting_page.dart | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 45b1b9a85..bc0eb7b67 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -1446,7 +1446,7 @@ void importServer() async { if (idServerMsg.isEmpty) { oldOptions['custom-rendezvous-server'] = idServer; } else { - print('ID Server invalid return'); + debugPrint('ID Server invalid return'); return false; } } else { @@ -1459,7 +1459,7 @@ void importServer() async { if (relayServerMsg.isEmpty) { oldOptions['relay-server'] = relayServer; } else { - print('Relay Server invalid return'); + debugPrint('Relay Server invalid return'); return false; } } else { @@ -1471,7 +1471,7 @@ void importServer() async { oldOptions['api-server'] = apiServer; return false; } else { - print('invalid_http'); + debugPrint('invalid_http'); return false; } } else { @@ -1480,7 +1480,7 @@ void importServer() async { // ok oldOptions['key'] = key; await bind.mainSetOptions(json: jsonEncode(oldOptions)); - print("set ID/Realy Server Ok"); + debugPrint("set ID/Realy Server Ok"); return true; } @@ -1490,16 +1490,15 @@ void importServer() async { aNullableString = value?.text; mytext.text = aNullableString.toString(); if (mytext.text.isNotEmpty) { - print('Clipboard is not empty'); + debugPrint('Clipboard is not empty'); try { Map config = jsonDecode(mytext.text); - print(config); if (config.containsKey('IdServer') && config.containsKey('RelayServer')) { - print('IdServer: ${config['IdServer']}'); - print('RelayServer: ${config['RelayServer']}'); - print('ApiServer: ${config['ApiServer']}'); - print('Key: ${config['Key']}'); + debugPrint('IdServer: ${config['IdServer']}'); + debugPrint('RelayServer: ${config['RelayServer']}'); + debugPrint('ApiServer: ${config['ApiServer']}'); + debugPrint('Key: ${config['Key']}'); Future success = submit(config['IdServer'], config['RelayServer'], config['ApiServer'], config['Key']); success.then((value) { @@ -1511,15 +1510,15 @@ void importServer() async { } }); } else { - print('invalid config info'); + debugPrint('invalid config info'); importServerShow(translate("Invalid server configuration")); } } catch (e) { - print('invalid config info'); + debugPrint('invalid config info'); importServerShow(translate("Invalid server configuration")); } } else { - print('Clipboard is empty'); + debugPrint('Clipboard is empty'); importServerShow(translate("Clipboard is empty")); } }); From 168b47469ebe43c67370243b6de1d17cf35ab4fe Mon Sep 17 00:00:00 2001 From: csf Date: Tue, 11 Oct 2022 09:59:27 +0900 Subject: [PATCH 0682/2015] fix file transfer search feature, opt UI style --- .../lib/desktop/pages/file_manager_page.dart | 70 ++++++++++--------- flutter/lib/desktop/pages/server_page.dart | 2 +- 2 files changed, 37 insertions(+), 35 deletions(-) diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index cc91a698f..212148c36 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -95,7 +95,7 @@ class _FileManagerPageState extends State _ffi.dialogManager.setOverlayState(Overlay.of(context)); return ChangeNotifierProvider.value( value: _ffi.fileModel, - child: Consumer(builder: (_context, _model, _child) { + child: Consumer(builder: (context, model, child) { return WillPopScope( onWillPop: () async { if (model.selectMode) { @@ -187,13 +187,9 @@ class _FileManagerPageState extends State controller: ScrollController(), child: ObxValue( (searchText) { - final filteredEntries = searchText.isEmpty + final filteredEntries = searchText.isNotEmpty ? entries.where((element) { - if (searchText.isEmpty) { - return true; - } else { - return element.name.contains(searchText.value); - } + return element.name.contains(searchText.value); }).toList(growable: false) : entries; return DataTable( @@ -273,22 +269,18 @@ class _FileManagerPageState extends State } } else { // Perform file-related tasks. - final _selectedItems = + final selectedItems = getSelectedItem(isLocal); - if (_selectedItems.contains(entry)) { - _selectedItems.remove(entry); + if (selectedItems.contains(entry)) { + selectedItems.remove(entry); } else { - _selectedItems.add(isLocal, entry); + selectedItems.add(isLocal, entry); } setState(() {}); } }), DataCell(Text( - entry - .lastModified() - .toString() - .replaceAll(".000", "") + - " ", + "${entry.lastModified().toString().replaceAll(".000", "")} ", style: TextStyle( fontSize: 12, color: MyTheme.darkGray), )), @@ -415,10 +407,11 @@ class _FileManagerPageState extends State } Widget headTools(bool isLocal) { - final _locationStatus = + final locationStatus = isLocal ? _locationStatusLocal : _locationStatusRemote; - final _locationFocus = isLocal ? _locationNodeLocal : _locationNodeRemote; - final _searchTextObs = isLocal ? _searchTextLocal : _searchTextRemote; + final locationFocus = isLocal ? _locationNodeLocal : _locationNodeRemote; + final searchTextObs = isLocal ? _searchTextLocal : _searchTextRemote; + final searchController = TextEditingController(text: searchTextObs.value); return Container( child: Column( children: [ @@ -476,13 +469,13 @@ class _FileManagerPageState extends State Expanded( child: GestureDetector( onTap: () { - _locationStatus.value = - _locationStatus.value == LocationStatus.bread + locationStatus.value = + locationStatus.value == LocationStatus.bread ? LocationStatus.textField : LocationStatus.bread; Future.delayed(Duration.zero, () { - if (_locationStatus.value == LocationStatus.textField) { - _locationFocus.requestFocus(); + if (locationStatus.value == LocationStatus.textField) { + locationFocus.requestFocus(); } }); }, @@ -493,7 +486,7 @@ class _FileManagerPageState extends State children: [ Expanded( child: Obx(() => - _locationStatus.value == LocationStatus.bread + locationStatus.value == LocationStatus.bread ? buildBread(isLocal) : buildPathLocation(isLocal))), DropdownButton( @@ -521,18 +514,28 @@ class _FileManagerPageState extends State child: ConstrainedBox( constraints: BoxConstraints(minWidth: 200), child: TextField( - controller: - TextEditingController(text: _searchTextObs.value), + textAlignVertical: TextAlignVertical.center, + controller: searchController, autofocus: true, - decoration: - InputDecoration(prefixIcon: Icon(Icons.search)), + decoration: InputDecoration( + prefixIcon: Icon(Icons.search), + suffix: IconButton( + icon: Icon(Icons.clear), + splashRadius: 20, + onPressed: () { + searchController.clear(); + searchTextObs.value = ""; + Get.back(); + }, + ), + ), onChanged: (searchText) => onSearchText(searchText, isLocal), ), )) ], splashRadius: 20, - child: const Icon(Icons.search), + icon: const Icon(Icons.search), ), IconButton( onPressed: () { @@ -690,9 +693,8 @@ class _FileManagerPageState extends State breadCrumbScrollToEnd(bool isLocal) { Future.delayed(Duration(milliseconds: 200), () { - final _breadCrumbScroller = getBreadCrumbScrollController(isLocal); - _breadCrumbScroller.animateTo( - _breadCrumbScroller.position.maxScrollExtent, + final breadCrumbScroller = getBreadCrumbScrollController(isLocal); + breadCrumbScroller.animateTo(breadCrumbScroller.position.maxScrollExtent, duration: Duration(milliseconds: 200), curve: Curves.fastLinearToSlowEaseIn); }); @@ -734,7 +736,7 @@ class _FileManagerPageState extends State return; } var items = SelectedItems(); - details.files.forEach((file) { + for (var file in details.files) { final f = File(file.path); items.add( true, @@ -743,7 +745,7 @@ class _FileManagerPageState extends State ..name = file.name ..size = FileSystemEntity.isDirectorySync(f.path) ? 0 : f.lengthSync()); - }); + } model.sendFiles(items, isRemote: false); } } diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index 3710b2932..e69557981 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -407,7 +407,7 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> { setState(() { client.recording = enabled; }); - }, translate('Allow reco)rding session')) + }, translate('Allow recording session')) ], )), ], From 5b1a12c6a7f4f735d293f1c03833c28b6ead0ab8 Mon Sep 17 00:00:00 2001 From: csf Date: Tue, 11 Oct 2022 17:25:34 +0900 Subject: [PATCH 0683/2015] feat file transfer history (goBack) --- .../lib/desktop/pages/file_manager_page.dart | 18 +- flutter/lib/models/file_model.dart | 39 +- flutter/pubspec.lock | 356 +++++++++--------- 3 files changed, 224 insertions(+), 189 deletions(-) diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 212148c36..08f13fd16 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -202,7 +202,7 @@ class _FileManagerPageState extends State sortColumnIndex: sortIndex, sortAscending: sortAscending, columns: [ - DataColumn(label: Text(translate(" "))), // icon + DataColumn(label: Text(" ")), // icon DataColumn( label: Text( translate("Name"), @@ -402,10 +402,6 @@ class _FileManagerPageState extends State )); } - goBack({bool? isLocal}) { - model.goToParentDirectory(isLocal: isLocal); - } - Widget headTools(bool isLocal) { final locationStatus = isLocal ? _locationStatusLocal : _locationStatusRemote; @@ -456,14 +452,20 @@ class _FileManagerPageState extends State icon: const Icon(Icons.home_outlined), splashRadius: 20, ), + IconButton( + icon: const Icon(Icons.arrow_back), + splashRadius: 20, + onPressed: () { + model.goBack(isLocal: isLocal); + }, + ), IconButton( icon: const Icon(Icons.arrow_upward), splashRadius: 20, onPressed: () { - goBack(isLocal: isLocal); + model.goToParentDirectory(isLocal: isLocal); }, ), - menu(isLocal: isLocal), ], ), Expanded( @@ -508,6 +510,7 @@ class _FileManagerPageState extends State )), )), PopupMenuButton( + tooltip: "", itemBuilder: (context) => [ PopupMenuItem( enabled: false, @@ -612,6 +615,7 @@ class _FileManagerPageState extends State }, splashRadius: 20, icon: const Icon(Icons.delete_forever_outlined)), + menu(isLocal: isLocal), ], ), ), diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index 3fdbe7099..1ba66b864 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -16,12 +16,15 @@ class FileModel extends ChangeNotifier { var _isLocal = false; var _selectMode = false; - var _localOption = DirectoryOption(); - var _remoteOption = DirectoryOption(); + final _localOption = DirectoryOption(); + final _remoteOption = DirectoryOption(); + + List localHistory = []; + List remoteHistory = []; var _jobId = 0; - var _jobProgress = JobProgress(); // from rust update + final _jobProgress = JobProgress(); // from rust update /// JobTable final _jobTable = List.empty(growable: true).obs; @@ -368,8 +371,11 @@ class FileModel extends ChangeNotifier { } } - openDirectory(String path, {bool? isLocal}) async { + openDirectory(String path, {bool? isLocal, bool isBack = false}) async { isLocal = isLocal ?? _isLocal; + if (!isBack) { + pushHistory(isLocal); + } final showHidden = isLocal ? _localOption.showHidden : _remoteOption.showHidden; final isWindows = @@ -397,11 +403,34 @@ class FileModel extends ChangeNotifier { } } + void pushHistory(bool isLocal) { + final history = isLocal ? localHistory : remoteHistory; + final currPath = isLocal ? currentLocalDir.path : currentRemoteDir.path; + if (history.isNotEmpty && history.last == currPath) { + return; + } + history.add(currPath); + } + goHome({bool? isLocal}) { isLocal = isLocal ?? _isLocal; openDirectory(getCurrentHome(isLocal), isLocal: isLocal); } + goBack({bool? isLocal}) { + isLocal = isLocal ?? _isLocal; + final history = isLocal ? localHistory : remoteHistory; + if (history.isEmpty) return; + final path = history.removeAt(history.length - 1); + if (path.isEmpty) return; + final currPath = isLocal ? currentLocalDir.path : currentRemoteDir.path; + if (currPath == path) { + goBack(isLocal: isLocal); + return; + } + openDirectory(path, isLocal: isLocal, isBack: true); + } + goToParentDirectory({bool? isLocal}) { isLocal = isLocal ?? _isLocal; final isWindows = @@ -685,6 +714,8 @@ class FileModel extends ChangeNotifier { } sendRemoveEmptyDir(String path, int fileNum, bool isLocal) { + final history = isLocal ? localHistory : remoteHistory; + history.removeWhere((element) => element.contains(path)); bind.sessionRemoveAllEmptyDirs( id: '${parent.target?.id}', actId: _jobId, diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index b7971eb92..fb28840b4 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -5,238 +5,238 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "49.0.0" after_layout: dependency: transitive description: name: after_layout - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.2.0" analyzer: dependency: transitive description: name: analyzer - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "5.1.0" animations: dependency: transitive description: name: animations - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.7" archive: dependency: transitive description: name: archive - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.3.1" args: dependency: transitive description: name: args - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.3.1" async: dependency: transitive description: name: async - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.9.0" back_button_interceptor: dependency: "direct main" description: name: back_button_interceptor - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "6.0.1" build: dependency: transitive description: name: build - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.3.1" build_config: dependency: transitive description: name: build_config - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.1.0" build_daemon: dependency: transitive description: name: build_daemon - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.1.0" build_resolvers: dependency: transitive description: name: build_resolvers - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.10" build_runner: dependency: "direct dev" description: name: build_runner - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.2.1" build_runner_core: dependency: transitive description: name: build_runner_core - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "7.2.4" built_collection: dependency: transitive description: name: built_collection - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "5.1.1" built_value: dependency: transitive description: name: built_value - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "8.4.1" cached_network_image: dependency: transitive description: name: cached_network_image - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "3.2.1" + version: "3.2.2" cached_network_image_platform_interface: dependency: transitive description: name: cached_network_image_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "2.0.0" cached_network_image_web: dependency: transitive description: name: cached_network_image_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "1.0.2" characters: dependency: transitive description: name: characters - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.2.1" charcode: dependency: transitive description: name: charcode - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.3.1" checked_yaml: dependency: transitive description: name: checked_yaml - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.1" clock: dependency: transitive description: name: clock - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.1.1" code_builder: dependency: transitive description: name: code_builder - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "4.3.0" collection: dependency: transitive description: name: collection - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.16.0" contextmenu: dependency: "direct main" description: name: contextmenu - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.0" convert: dependency: transitive description: name: convert - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.2" cross_file: dependency: transitive description: name: cross_file - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.3.3+2" crypto: dependency: transitive description: name: crypto - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.2" csslib: dependency: transitive description: name: csslib - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.17.2" cupertino_icons: dependency: "direct main" description: name: cupertino_icons - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.5" dart_style: dependency: transitive description: name: dart_style - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.2.4" dash_chat_2: dependency: "direct main" description: name: dash_chat_2 - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.0.15" desktop_drop: dependency: "direct main" description: name: desktop_drop - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.3.3" desktop_multi_window: @@ -252,91 +252,91 @@ packages: dependency: "direct main" description: name: device_info_plus - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "4.1.3" device_info_plus_linux: dependency: transitive description: name: device_info_plus_linux - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.0" device_info_plus_macos: dependency: transitive description: name: device_info_plus_macos - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.0" device_info_plus_platform_interface: dependency: transitive description: name: device_info_plus_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.0" device_info_plus_web: dependency: transitive description: name: device_info_plus_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.0" device_info_plus_windows: dependency: transitive description: name: device_info_plus_windows - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "4.1.0" draggable_float_widget: dependency: "direct main" description: name: draggable_float_widget - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.0.2" event_bus: dependency: transitive description: name: event_bus - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.0" external_path: dependency: "direct main" description: name: external_path - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.3" ffi: dependency: "direct main" description: name: ffi - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.1" file: dependency: transitive description: name: file - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "6.1.4" file_picker: dependency: "direct main" description: name: file_picker - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "5.2.0+1" + version: "5.2.1" fixnum: dependency: transitive description: name: fixnum - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.1" flutter: @@ -348,21 +348,21 @@ packages: dependency: transitive description: name: flutter_blurhash - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.7.0" flutter_breadcrumb: dependency: "direct main" description: name: flutter_breadcrumb - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.1" flutter_cache_manager: dependency: transitive description: name: flutter_cache_manager - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.3.0" flutter_custom_cursor: @@ -378,14 +378,14 @@ packages: dependency: "direct main" description: name: flutter_improved_scrolling - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.0.3" flutter_lints: dependency: "direct dev" description: name: flutter_lints - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.1" flutter_localizations: @@ -397,14 +397,14 @@ packages: dependency: transitive description: name: flutter_parsed_text - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.2.1" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.7" flutter_rust_bridge: @@ -420,7 +420,7 @@ packages: dependency: "direct main" description: name: flutter_svg - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.1.5" flutter_web_plugins: @@ -432,406 +432,406 @@ packages: dependency: "direct dev" description: name: freezed - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.1" freezed_annotation: dependency: "direct main" description: name: freezed_annotation - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.0" frontend_server_client: dependency: transitive description: name: frontend_server_client - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.3" get: dependency: "direct main" description: name: get - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "4.6.5" glob: dependency: transitive description: name: glob - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.0" graphs: dependency: transitive description: name: graphs - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.0" html: dependency: transitive description: name: html - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.15.0" http: dependency: "direct main" description: name: http - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.13.5" http_multi_server: dependency: transitive description: name: http_multi_server - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.2.1" http_parser: dependency: transitive description: name: http_parser - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "4.0.1" icons_launcher: dependency: "direct dev" description: name: icons_launcher - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "2.0.4" + version: "2.0.5" image: dependency: "direct main" description: name: image - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.2.0" image_picker: dependency: "direct main" description: name: image_picker - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.8.6" image_picker_android: dependency: transitive description: name: image_picker_android - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.8.5+3" image_picker_for_web: dependency: transitive description: name: image_picker_for_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.10" image_picker_ios: dependency: transitive description: name: image_picker_ios - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.8.6+1" image_picker_platform_interface: dependency: transitive description: name: image_picker_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.6.2" intl: dependency: transitive description: name: intl - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.17.0" io: dependency: transitive description: name: io - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.3" js: dependency: transitive description: name: js - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.6.4" json_annotation: dependency: transitive description: name: json_annotation - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "4.7.0" lints: dependency: transitive description: name: lints - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.0" logging: dependency: transitive description: name: logging - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.1.0" matcher: dependency: transitive description: name: matcher - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.12.12" material_color_utilities: dependency: transitive description: name: material_color_utilities - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.1.5" menu_base: dependency: transitive description: name: menu_base - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.1.1" meta: dependency: transitive description: name: meta - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.8.0" mime: dependency: transitive description: name: mime - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.2" nested: dependency: transitive description: name: nested - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.0" octo_image: dependency: transitive description: name: octo_image - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.2" package_config: dependency: transitive description: name: package_config - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.0" package_info_plus: dependency: "direct main" description: name: package_info_plus - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.4.3+1" package_info_plus_linux: dependency: transitive description: name: package_info_plus_linux - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.5" package_info_plus_macos: dependency: transitive description: name: package_info_plus_macos - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.3.0" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.2" package_info_plus_web: dependency: transitive description: name: package_info_plus_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.6" package_info_plus_windows: dependency: transitive description: name: package_info_plus_windows - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.0" path: dependency: transitive description: name: path - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.8.2" path_drawing: dependency: transitive description: name: path_drawing - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.1" path_parsing: dependency: transitive description: name: path_parsing - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.1" path_provider: dependency: "direct main" description: name: path_provider - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.11" path_provider_android: dependency: transitive description: name: path_provider_android - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.20" path_provider_ios: dependency: transitive description: name: path_provider_ios - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.11" path_provider_linux: dependency: transitive description: name: path_provider_linux - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.7" path_provider_macos: dependency: transitive description: name: path_provider_macos - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.6" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.5" path_provider_windows: dependency: transitive description: name: path_provider_windows - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.3" pedantic: dependency: transitive description: name: pedantic - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.11.1" petitparser: dependency: transitive description: name: petitparser - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "5.0.0" platform: dependency: transitive description: name: platform - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.1.0" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.3" pool: dependency: transitive description: name: pool - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.5.1" process: dependency: transitive description: name: process - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "4.2.4" provider: dependency: "direct main" description: name: provider - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "6.0.3" pub_semver: dependency: transitive description: name: pub_semver - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.1" pubspec_parse: dependency: transitive description: name: pubspec_parse - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.2.1" qr_code_scanner: dependency: "direct main" description: name: qr_code_scanner - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.1" rxdart: dependency: "direct main" description: name: rxdart - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.27.5" screen_retriever: @@ -847,91 +847,91 @@ packages: dependency: "direct main" description: name: scroll_pos - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.3.0" settings_ui: dependency: "direct main" description: name: settings_ui - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.2" shared_preferences: dependency: "direct main" description: name: shared_preferences - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.15" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.13" shared_preferences_ios: dependency: transitive description: name: shared_preferences_ios - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.1" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.1" shared_preferences_macos: dependency: transitive description: name: shared_preferences_macos - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.4" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.0" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.4" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.1" shelf: dependency: transitive description: name: shelf - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.4.0" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.2" shortid: dependency: transitive description: name: shortid - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.1.2" sky_engine: @@ -943,84 +943,84 @@ packages: dependency: transitive description: name: source_gen - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.2.5" source_span: dependency: transitive description: name: source_span - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.9.1" sqflite: dependency: transitive description: name: sqflite - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "2.0.3+1" + version: "2.1.0+1" sqflite_common: dependency: transitive description: name: sqflite_common - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.3.0" stack_trace: dependency: transitive description: name: stack_trace - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.10.0" stream_channel: dependency: transitive description: name: stream_channel - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.1" stream_transform: dependency: transitive description: name: stream_transform - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.1" string_scanner: dependency: transitive description: name: string_scanner - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.1.1" synchronized: dependency: transitive description: name: synchronized - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.0+3" term_glyph: dependency: transitive description: name: term_glyph - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.2.1" timing: dependency: transitive description: name: timing - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.0" toggle_switch: dependency: "direct main" description: name: toggle_switch - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.4.0" tray_manager: @@ -1036,189 +1036,189 @@ packages: dependency: "direct main" description: name: tuple - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.1" typed_data: dependency: transitive description: name: typed_data - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.3.1" universal_io: dependency: transitive description: name: universal_io - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.4" url_launcher: dependency: "direct main" description: name: url_launcher - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "6.1.6" url_launcher_android: dependency: transitive description: name: url_launcher_android - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "6.0.19" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "6.0.17" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.1" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.1" url_launcher_web: dependency: transitive description: name: url_launcher_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.13" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.1" uuid: dependency: transitive description: name: uuid - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.6" vector_math: dependency: transitive description: name: vector_math - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.2" video_player: dependency: transitive description: name: video_player - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.4.7" video_player_android: dependency: transitive description: name: video_player_android - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.3.9" video_player_avfoundation: dependency: transitive description: name: video_player_avfoundation - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.3.7" video_player_platform_interface: dependency: transitive description: name: video_player_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "5.1.4" video_player_web: dependency: transitive description: name: video_player_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.12" visibility_detector: dependency: "direct main" description: name: visibility_detector - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.3.3" wakelock: dependency: "direct main" description: name: wakelock - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.6.2" wakelock_macos: dependency: transitive description: name: wakelock_macos - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.4.0" wakelock_platform_interface: dependency: transitive description: name: wakelock_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.3.0" wakelock_web: dependency: transitive description: name: wakelock_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.4.0" wakelock_windows: dependency: transitive description: name: wakelock_windows - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.2.1" watcher: dependency: transitive description: name: watcher - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.1" web_socket_channel: dependency: transitive description: name: web_socket_channel - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.2.0" win32: dependency: transitive description: name: win32 - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.0" window_manager: @@ -1243,30 +1243,30 @@ packages: dependency: transitive description: name: xdg_directories - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.2.0+2" xml: dependency: transitive description: name: xml - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "6.1.0" yaml: dependency: transitive description: name: yaml - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.1.1" zxing2: dependency: "direct main" description: name: zxing2 - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "0.1.0" + version: "0.1.1" sdks: - dart: ">=2.17.1 <3.0.0" - flutter: ">=3.0.0" + dart: ">=2.18.0 <3.0.0" + flutter: ">=3.3.0" From baf437e6f0a8fd62d4b31a4d741bebd86b7cfea9 Mon Sep 17 00:00:00 2001 From: csf Date: Thu, 13 Oct 2022 10:17:20 +0900 Subject: [PATCH 0684/2015] integrate file search bar into path location --- .../lib/desktop/pages/file_manager_page.dart | 229 ++++++++++-------- 1 file changed, 131 insertions(+), 98 deletions(-) diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 08f13fd16..0c60bf22d 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -14,7 +14,17 @@ import '../../common.dart'; import '../../models/model.dart'; import '../../models/platform_model.dart'; -enum LocationStatus { bread, textField } +/// status of location bar +enum LocationStatus { + /// normal bread crumb bar + bread, + + /// show path text field + pathLocation, + + /// show file search bar text field + fileSearchBar +} class FileManagerPage extends StatefulWidget { const FileManagerPage({Key? key, required this.id}) : super(key: key); @@ -40,7 +50,7 @@ class _FileManagerPageState extends State final _breadCrumbScrollerLocal = ScrollController(); final _breadCrumbScrollerRemote = ScrollController(); - final _dropMaskVisible = false.obs; + final _dropMaskVisible = false.obs; // TODO impl drop mask ScrollController getBreadCrumbScrollController(bool isLocal) { return isLocal ? _breadCrumbScrollerLocal : _breadCrumbScrollerRemote; @@ -84,6 +94,8 @@ class _FileManagerPageState extends State Get.delete(tag: 'ft_${widget.id}'); _locationNodeLocal.removeListener(onLocalLocationFocusChanged); _locationNodeRemote.removeListener(onRemoteLocationFocusChanged); + _locationNodeLocal.dispose(); + _locationNodeRemote.dispose(); super.dispose(); } @@ -96,23 +108,16 @@ class _FileManagerPageState extends State return ChangeNotifierProvider.value( value: _ffi.fileModel, child: Consumer(builder: (context, model, child) { - return WillPopScope( - onWillPop: () async { - if (model.selectMode) { - model.toggleSelectMode(); - } - return false; - }, - child: Scaffold( - backgroundColor: Theme.of(context).backgroundColor, - body: Row( - children: [ - Flexible(flex: 3, child: body(isLocal: true)), - Flexible(flex: 3, child: body(isLocal: false)), - Flexible(flex: 2, child: statusList()) - ], - ), - )); + return Scaffold( + backgroundColor: Theme.of(context).backgroundColor, + body: Row( + children: [ + Flexible(flex: 3, child: body(isLocal: true)), + Flexible(flex: 3, child: body(isLocal: false)), + Flexible(flex: 2, child: statusList()) + ], + ), + ); })); }) ]); @@ -406,8 +411,6 @@ class _FileManagerPageState extends State final locationStatus = isLocal ? _locationStatusLocal : _locationStatusRemote; final locationFocus = isLocal ? _locationNodeLocal : _locationNodeRemote; - final searchTextObs = isLocal ? _searchTextLocal : _searchTextRemote; - final searchController = TextEditingController(text: searchTextObs.value); return Container( child: Column( children: [ @@ -473,73 +476,62 @@ class _FileManagerPageState extends State onTap: () { locationStatus.value = locationStatus.value == LocationStatus.bread - ? LocationStatus.textField + ? LocationStatus.pathLocation : LocationStatus.bread; Future.delayed(Duration.zero, () { - if (locationStatus.value == LocationStatus.textField) { + if (locationStatus.value == LocationStatus.pathLocation) { locationFocus.requestFocus(); } }); }, - child: Container( - decoration: - BoxDecoration(border: Border.all(color: Colors.black12)), + child: Obx(() => Container( + decoration: BoxDecoration( + border: Border.all( + color: locationStatus.value == LocationStatus.bread + ? Colors.black12 + : Theme.of(context) + .colorScheme + .primary + .withOpacity(0.5))), child: Row( children: [ Expanded( - child: Obx(() => - locationStatus.value == LocationStatus.bread - ? buildBread(isLocal) - : buildPathLocation(isLocal))), - DropdownButton( - isDense: true, - underline: Offstage(), - items: [ - // TODO: favourite - DropdownMenuItem( - child: Text('/'), - value: '/', - ) - ], - onChanged: (path) { - if (path is String && path.isNotEmpty) { - openDirectory(path, isLocal: isLocal); - } - }) + child: locationStatus.value == LocationStatus.bread + ? buildBread(isLocal) + : buildPathLocation(isLocal)), ], - )), + ))), )), - PopupMenuButton( - tooltip: "", - itemBuilder: (context) => [ - PopupMenuItem( - enabled: false, - child: ConstrainedBox( - constraints: BoxConstraints(minWidth: 200), - child: TextField( - textAlignVertical: TextAlignVertical.center, - controller: searchController, - autofocus: true, - decoration: InputDecoration( - prefixIcon: Icon(Icons.search), - suffix: IconButton( - icon: Icon(Icons.clear), - splashRadius: 20, - onPressed: () { - searchController.clear(); - searchTextObs.value = ""; - Get.back(); - }, - ), - ), - onChanged: (searchText) => - onSearchText(searchText, isLocal), - ), - )) - ], - splashRadius: 20, - icon: const Icon(Icons.search), - ), + Obx(() { + switch (locationStatus.value) { + case LocationStatus.bread: + return IconButton( + onPressed: () { + locationStatus.value = LocationStatus.fileSearchBar; + final focusNode = + isLocal ? _locationNodeLocal : _locationNodeRemote; + Future.delayed( + Duration.zero, () => focusNode.requestFocus()); + }, + splashRadius: 20, + icon: Icon(Icons.search)); + case LocationStatus.pathLocation: + return IconButton( + color: Theme.of(context).disabledColor, + onPressed: null, + splashRadius: 20, + icon: Icon(Icons.close)); + case LocationStatus.fileSearchBar: + return IconButton( + color: Theme.of(context).disabledColor, + onPressed: () { + onSearchText("", isLocal); + locationStatus.value = LocationStatus.bread; + }, + splashRadius: 1, + icon: Icon(Icons.close)); + } + }), IconButton( onPressed: () { model.refresh(isLocal: isLocal); @@ -649,7 +641,9 @@ class _FileManagerPageState extends State // ignore } else { // lost focus, change to bread - _locationStatusLocal.value = LocationStatus.bread; + if (_locationStatusLocal.value != LocationStatus.fileSearchBar) { + _locationStatusLocal.value = LocationStatus.bread; + } } } @@ -659,7 +653,9 @@ class _FileManagerPageState extends State // ignore } else { // lost focus, change to bread - _locationStatusRemote.value = LocationStatus.bread; + if (_locationStatusRemote.value != LocationStatus.fileSearchBar) { + _locationStatusRemote.value = LocationStatus.bread; + } } } @@ -671,14 +667,33 @@ class _FileManagerPageState extends State } openDirectory(path, isLocal: isLocal); }); + breadCrumbScrollToEnd(isLocal); return items.isEmpty ? Offstage() - : BreadCrumb( - items: items, - divider: Text("/").paddingSymmetric(horizontal: 4.0), - overflow: ScrollableOverflow( - controller: getBreadCrumbScrollController(isLocal)), - ); + : Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + Expanded( + child: BreadCrumb( + items: items, + divider: Text("/").paddingSymmetric(horizontal: 4.0), + overflow: ScrollableOverflow( + controller: getBreadCrumbScrollController(isLocal)), + )), + DropdownButton( + isDense: true, + underline: Offstage(), + items: [ + // TODO: favourite + DropdownMenuItem( + child: Text('/'), + value: '/', + ) + ], + onChanged: (path) { + if (path is String && path.isNotEmpty) { + openDirectory(path, isLocal: isLocal); + } + }) + ]); } List getPathBreadCrumbItems( @@ -705,19 +720,37 @@ class _FileManagerPageState extends State } Widget buildPathLocation(bool isLocal) { - return TextField( - focusNode: isLocal ? _locationNodeLocal : _locationNodeRemote, - decoration: InputDecoration( - border: InputBorder.none, - isDense: true, - prefix: Padding(padding: EdgeInsets.only(left: 4.0)), - ), - controller: - TextEditingController(text: model.getCurrentDir(isLocal).path), - onSubmitted: (path) { - openDirectory(path, isLocal: isLocal); - }, - ); + final searchTextObs = isLocal ? _searchTextLocal : _searchTextRemote; + final locationStatus = + isLocal ? _locationStatusLocal : _locationStatusRemote; + final focusNode = isLocal ? _locationNodeLocal : _locationNodeRemote; + return Row(children: [ + Icon( + locationStatus.value == LocationStatus.pathLocation + ? Icons.folder + : Icons.search, + color: Theme.of(context).hintColor, + ).paddingSymmetric(horizontal: 2), + Expanded( + child: TextField( + focusNode: focusNode, + decoration: InputDecoration( + border: InputBorder.none, + isDense: true, + prefix: Padding(padding: EdgeInsets.only(left: 4.0))), + controller: locationStatus.value == LocationStatus.pathLocation + ? TextEditingController(text: model.getCurrentDir(isLocal).path) + : TextEditingController(text: searchTextObs.value) + ..selection = + TextSelection.collapsed(offset: searchTextObs.value.length), + onSubmitted: (path) { + openDirectory(path, isLocal: isLocal); + }, + onChanged: locationStatus.value == LocationStatus.fileSearchBar + ? (searchText) => onSearchText(searchText, isLocal) + : null, + )) + ]); } onSearchText(String searchText, bool isLocal) { From eaf7fd320cfd7decb7234c9a0c1585e1ca4da41d Mon Sep 17 00:00:00 2001 From: csf Date: Thu, 13 Oct 2022 19:57:59 +0900 Subject: [PATCH 0685/2015] update file transfer UI --- .../lib/desktop/pages/file_manager_page.dart | 54 +++++++++++-------- 1 file changed, 33 insertions(+), 21 deletions(-) diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 0c60bf22d..19a7a3e56 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -158,13 +158,13 @@ class _FileManagerPageState extends State final sortIndex = (SortBy style) { switch (style) { case SortBy.Name: - return 1; + return 0; case SortBy.Type: return 0; case SortBy.Modified: - return 2; + return 1; case SortBy.Size: - return 3; + return 2; } }(model.getSortStyle(isLocal)); final sortAscending = @@ -202,16 +202,16 @@ class _FileManagerPageState extends State showCheckboxColumn: true, dataRowHeight: 25, headingRowHeight: 30, + horizontalMargin: 8, columnSpacing: 8, showBottomBorder: true, sortColumnIndex: sortIndex, sortAscending: sortAscending, columns: [ - DataColumn(label: Text(" ")), // icon DataColumn( label: Text( translate("Name"), - ), + ).marginSymmetric(horizontal: 4), onSort: (columnIndex, ascending) { model.changeSortStyle(SortBy.Name, isLocal: isLocal, ascending: ascending); @@ -251,19 +251,28 @@ class _FileManagerPageState extends State selected: getSelectedItem(isLocal).contains(entry), cells: [ - DataCell(Icon( - entry.isFile - ? Icons.feed_outlined - : Icons.folder, - size: 25)), DataCell( ConstrainedBox( constraints: - BoxConstraints(maxWidth: 100), + BoxConstraints(maxWidth: 180), child: Tooltip( message: entry.name, - child: Text(entry.name, - overflow: TextOverflow.ellipsis), + child: Row(children: [ + 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)) + ]), )), onTap: () { if (entry.isDirectory) { openDirectory(entry.path, isLocal: isLocal); @@ -284,15 +293,17 @@ class _FileManagerPageState extends State setState(() {}); } }), - DataCell(Text( + DataCell(FittedBox( + child: Text( "${entry.lastModified().toString().replaceAll(".000", "")} ", style: TextStyle( fontSize: 12, color: MyTheme.darkGray), - )), + ))), DataCell(Text( sizeStr, + overflow: TextOverflow.ellipsis, style: TextStyle( - fontSize: 12, color: MyTheme.darkGray), + fontSize: 10, color: MyTheme.darkGray), )), ]); }).toList(growable: false), @@ -724,6 +735,11 @@ class _FileManagerPageState extends State final locationStatus = isLocal ? _locationStatusLocal : _locationStatusRemote; final focusNode = isLocal ? _locationNodeLocal : _locationNodeRemote; + final text = locationStatus.value == LocationStatus.pathLocation + ? model.getCurrentDir(isLocal).path + : searchTextObs.value; + final textController = TextEditingController(text: text) + ..selection = TextSelection.collapsed(offset: text.length); return Row(children: [ Icon( locationStatus.value == LocationStatus.pathLocation @@ -738,11 +754,7 @@ class _FileManagerPageState extends State border: InputBorder.none, isDense: true, prefix: Padding(padding: EdgeInsets.only(left: 4.0))), - controller: locationStatus.value == LocationStatus.pathLocation - ? TextEditingController(text: model.getCurrentDir(isLocal).path) - : TextEditingController(text: searchTextObs.value) - ..selection = - TextSelection.collapsed(offset: searchTextObs.value.length), + controller: textController, onSubmitted: (path) { openDirectory(path, isLocal: isLocal); }, From da18e69258c1a1133db5a7be3204b616cf084cc2 Mon Sep 17 00:00:00 2001 From: csf Date: Thu, 13 Oct 2022 20:22:11 +0900 Subject: [PATCH 0686/2015] update file transfer pop menu style / fixed file name width --- flutter/lib/common/widgets/peer_card.dart | 7 -- .../lib/desktop/pages/file_manager_page.dart | 82 ++++++++++++------- flutter/lib/desktop/widgets/popup_menu.dart | 8 ++ 3 files changed, 59 insertions(+), 38 deletions(-) diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index 19d28513c..c99cf2e68 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -12,13 +12,6 @@ import '../../models/platform_model.dart'; import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu; import '../../desktop/widgets/popup_menu.dart'; -class CustomPopupMenuTheme { - static const Color commonColor = MyTheme.accent; - // kMinInteractiveDimension - static const double height = 20.0; - static const double dividerHeight = 3.0; -} - typedef PopupMenuEntryBuilder = Future>> Function(BuildContext); diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 19a7a3e56..f6fae1e31 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -9,10 +9,13 @@ import 'package:flutter_hbb/models/file_model.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:wakelock/wakelock.dart'; +import '../../consts.dart'; +import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu; import '../../common.dart'; import '../../models/model.dart'; import '../../models/platform_model.dart'; +import '../widgets/popup_menu.dart'; /// status of location bar enum LocationStatus { @@ -124,32 +127,47 @@ class _FileManagerPageState extends State } Widget menu({bool isLocal = false}) { - return PopupMenuButton( - icon: const Icon(Icons.more_vert), - splashRadius: 20, - itemBuilder: (context) { - return [ - PopupMenuItem( - child: Row( - children: [ - Icon( - model.getCurrentShowHidden(isLocal) - ? Icons.check_box_outlined - : Icons.check_box_outline_blank, - color: Colors.black), - SizedBox(width: 5), - Text(translate("Show Hidden Files")) - ], - ), - value: "hidden", - ) - ]; + var menuPos = RelativeRect.fill; + + final items = [ + MenuEntrySwitch( + switchType: SwitchType.scheckbox, + text: translate("Show Hidden Files"), + getter: () async { + return model.getCurrentShowHidden(isLocal); }, - onSelected: (v) { - if (v == "hidden") { - model.toggleShowHidden(local: isLocal); - } - }); + setter: (bool v) async { + model.toggleShowHidden(local: isLocal); + }, + padding: kDesktopMenuPadding, + dismissOnClicked: true, + ), + ]; + + return Listener( + onPointerDown: (e) { + final x = e.position.dx; + final y = e.position.dy; + menuPos = RelativeRect.fromLTRB(x, y, x, y); + }, + child: IconButton( + icon: const Icon(Icons.more_vert), + splashRadius: 20, + onPressed: () => mod_menu.showMenu( + context: context, + position: menuPos, + items: items + .map((e) => e.build( + context, + MenuConfig( + commonColor: CustomPopupMenuTheme.commonColor, + height: CustomPopupMenuTheme.height, + dividerHeight: CustomPopupMenuTheme.dividerHeight))) + .expand((i) => i) + .toList(), + elevation: 8, + ), + )); } Widget body({bool isLocal = false}) { @@ -252,9 +270,8 @@ class _FileManagerPageState extends State getSelectedItem(isLocal).contains(entry), cells: [ DataCell( - ConstrainedBox( - constraints: - BoxConstraints(maxWidth: 180), + Container( + width: 180, child: Tooltip( message: entry.name, child: Row(children: [ @@ -724,9 +741,12 @@ class _FileManagerPageState extends State breadCrumbScrollToEnd(bool isLocal) { Future.delayed(Duration(milliseconds: 200), () { final breadCrumbScroller = getBreadCrumbScrollController(isLocal); - breadCrumbScroller.animateTo(breadCrumbScroller.position.maxScrollExtent, - duration: Duration(milliseconds: 200), - curve: Curves.fastLinearToSlowEaseIn); + if (breadCrumbScroller.hasClients) { + breadCrumbScroller.animateTo( + breadCrumbScroller.position.maxScrollExtent, + duration: Duration(milliseconds: 200), + curve: Curves.fastLinearToSlowEaseIn); + } }); } diff --git a/flutter/lib/desktop/widgets/popup_menu.dart b/flutter/lib/desktop/widgets/popup_menu.dart index 71d1ec417..5f06aebfe 100644 --- a/flutter/lib/desktop/widgets/popup_menu.dart +++ b/flutter/lib/desktop/widgets/popup_menu.dart @@ -3,6 +3,7 @@ import 'dart:core'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import '../../common.dart'; import './material_mod_popup_menu.dart' as mod_menu; // https://stackoverflow.com/questions/68318314/flutter-popup-menu-inside-popup-menu @@ -637,3 +638,10 @@ class MenuEntryButton extends MenuEntryBase { ]; } } + +class CustomPopupMenuTheme { + static const Color commonColor = MyTheme.accent; + // kMinInteractiveDimension + static const double height = 20.0; + static const double dividerHeight = 3.0; +} From 44662dc50bab9766751de4bc777623bf2bf10b86 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Thu, 13 Oct 2022 20:40:02 +0800 Subject: [PATCH 0687/2015] [Linux] feat: add window oriented listener support --- flutter/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 530d74b82..c2f9b960a 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -64,7 +64,7 @@ dependencies: desktop_multi_window: git: url: https://github.com/Kingtous/rustdesk_desktop_multi_window - ref: 0019311e8aba4e84ffd44c57ba1834cc76924f2a + ref: c09d65018f402dd0d6073149fe6705185101a270 freezed_annotation: ^2.0.3 tray_manager: git: From 67a5cf97718ed44c190a898ff363db4be84d0a59 Mon Sep 17 00:00:00 2001 From: csf Date: Thu, 13 Oct 2022 21:19:05 +0900 Subject: [PATCH 0688/2015] add confirm before closing multiple tabs --- .../desktop/pages/desktop_setting_page.dart | 8 +++-- .../desktop/pages/file_manager_tab_page.dart | 31 +++++++----------- .../lib/desktop/pages/remote_tab_page.dart | 32 +++++++------------ .../lib/desktop/widgets/tabbar_widget.dart | 29 +++++++++++++++-- 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/fr.rs | 6 ++-- src/lang/hu.rs | 2 ++ src/lang/id.rs | 2 ++ src/lang/it.rs | 2 ++ src/lang/ja.rs | 2 ++ src/lang/ko.rs | 2 ++ src/lang/kz.rs | 2 ++ src/lang/pl.rs | 2 ++ src/lang/pt_PT.rs | 2 ++ src/lang/ptbr.rs | 2 ++ src/lang/ru.rs | 2 ++ src/lang/sk.rs | 2 ++ src/lang/template.rs | 2 ++ src/lang/tr.rs | 2 ++ src/lang/tw.rs | 2 ++ src/lang/ua.rs | 10 +++--- src/lang/vn.rs | 2 ++ 27 files changed, 108 insertions(+), 50 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index bc0eb7b67..55fd93bcc 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -232,11 +232,11 @@ class _GeneralState extends State<_General> { controller: scrollController, children: [ theme(), - abr(), hwcodec(), audio(context), record(context), _Card(title: 'Language', children: [language()]), + other() ], ).marginOnly(bottom: _kListViewBottomMargin)); } @@ -267,8 +267,10 @@ class _GeneralState extends State<_General> { ]); } - Widget abr() { - return _Card(title: 'Adaptive Bitrate', children: [ + Widget other() { + return _Card(title: 'Other', children: [ + _OptionCheckBox(context, 'Confirm before closing multiple tabs', + 'enable-confirm-closing-tabs'), _OptionCheckBox(context, 'Adaptive Bitrate', 'enable-abr'), ]); } diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index 24a36eddb..f844edc36 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -10,7 +10,7 @@ import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:get/get.dart'; -import '../../mobile/widgets/dialog.dart'; +import '../../models/platform_model.dart'; /// File Transfer for multi tabs class FileManagerTabPage extends StatefulWidget { @@ -35,7 +35,7 @@ class _FileManagerTabPageState extends State { label: params['id'], selectedIcon: selectedIcon, unselectedIcon: unselectedIcon, - onTabCloseButton: () => handleTabCloseButton(params['id']), + onTabCloseButton: () => () => tabController.closeBy(params['id']), page: FileManagerPage(key: ValueKey(params['id']), id: params['id']))); } @@ -58,7 +58,7 @@ class _FileManagerTabPageState extends State { label: id, selectedIcon: selectedIcon, unselectedIcon: unselectedIcon, - onTabCloseButton: () => handleTabCloseButton(id), + onTabCloseButton: () => tabController.closeBy(id), page: FileManagerPage(key: ValueKey(id), id: id))); } else if (call.method == "onDestroy") { tabController.clear(); @@ -98,26 +98,19 @@ class _FileManagerTabPageState extends State { return widget.params["windowId"]; } - void handleTabCloseButton(String peerId) { - final session = ffi('ft_$peerId'); - if (session.ffiModel.pi.hostname.isNotEmpty) { - tabController.jumpBy(peerId); - clientClose(session.dialogManager); - } else { - tabController.closeBy(peerId); - } - } - Future handleWindowCloseButton() async { final connLength = tabController.state.value.tabs.length; - if (connLength < 1) { + if (connLength <= 1) { + tabController.clear(); return true; - } else if (connLength == 1) { - final currentConn = tabController.state.value.tabs[0]; - handleTabCloseButton(currentConn.key); - return false; } else { - final res = await closeConfirmDialog(); + final opt = "enable-confirm-closing-tabs"; + final bool res; + if (!option2bool(opt, await bind.mainGetOption(key: opt))) { + res = true; + } else { + res = await closeConfirmDialog(); + } if (res) { tabController.clear(); } diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index 352e4682a..1d3daf7b3 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -12,7 +12,7 @@ import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; -import '../../mobile/widgets/dialog.dart'; +import '../../models/platform_model.dart'; class ConnectionTabPage extends StatefulWidget { final Map params; @@ -42,7 +42,7 @@ class _ConnectionTabPageState extends State { label: peerId, selectedIcon: selectedIcon, unselectedIcon: unselectedIcon, - onTabCloseButton: () => handleTabCloseButton(peerId), + onTabCloseButton: () => tabController.closeBy(peerId), page: Obx(() => RemotePage( key: ValueKey(peerId), id: peerId, @@ -78,7 +78,7 @@ class _ConnectionTabPageState extends State { label: id, selectedIcon: selectedIcon, unselectedIcon: unselectedIcon, - onTabCloseButton: () => handleTabCloseButton(id), + onTabCloseButton: () => tabController.closeBy(id), page: Obx(() => RemotePage( key: ValueKey(id), id: id, @@ -173,29 +173,21 @@ class _ConnectionTabPageState extends State { return widget.params["windowId"]; } - void handleTabCloseButton(String peerId) { - final session = ffi(peerId); - if (session.ffiModel.pi.hostname.isNotEmpty) { - tabController.jumpBy(peerId); - clientClose(session.dialogManager); - } else { - tabController.closeBy(peerId); - } - } - Future handleWindowCloseButton() async { final connLength = tabController.length; - if (connLength < 1) { + if (connLength <= 1) { + tabController.clear(); return true; - } else if (connLength == 1) { - final currentConn = tabController.state.value.tabs[0]; - handleTabCloseButton(currentConn.key); - return false; } else { - final res = await closeConfirmDialog(); + final opt = "enable-confirm-closing-tabs"; + final bool res; + if (!option2bool(opt, await bind.mainGetOption(key: opt))) { + res = true; + } else { + res = await closeConfirmDialog(); + } if (res) { tabController.clear(); - _update_remote_count(); } return res; } diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index a98b3af20..e3fcf0f53 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -456,8 +456,15 @@ class WindowActionPanel extends StatelessWidget { } Future closeConfirmDialog() async { + var confirm = true; final res = await gFFI.dialogManager.show((setState, close) { - submit() => close(true); + submit() { + final opt = "enable-confirm-closing-tabs"; + String value = bool2option(opt, confirm); + bind.mainSetOption(key: opt, value: value); + close(true); + } + return CustomAlertDialog( title: Row(children: [ const Icon(Icons.warning_amber_sharp, @@ -465,7 +472,25 @@ Future closeConfirmDialog() async { const SizedBox(width: 10), Text(translate("Warning")), ]), - content: Text(translate("Disconnect all devices?")), + content: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(translate("Disconnect all devices?")), + CheckboxListTile( + contentPadding: const EdgeInsets.all(0), + dense: true, + controlAffinity: ListTileControlAffinity.leading, + title: Text( + translate("Confirm before closing multiple tabs"), + ), + value: confirm, + onChanged: (v) { + if (v == null) return; + setState(() => confirm = v); + }, + ) + ]), // confirm checkbox actions: [ TextButton(onPressed: close, child: Text(translate("Cancel"))), ElevatedButton(onPressed: submit, child: Text(translate("OK"))), diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 35e77990c..0d0b9e208 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -371,5 +371,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("uac_warning", "暂时无法访问远端设备,因为远端设备正在请求用户账户权限,请等待对方关闭UAC窗口。为避免这个问题,建议在远端设备上安装或者以管理员权限运行本软件。"), ("elevated_foreground_window_warning", "暂时无法使用鼠标键盘,因为远端桌面的当前窗口需要更高的权限才能操作, 可以请求对方最小化当前窗口。为避免这个问题,建议在远端设备上安装或者以管理员权限运行本软件。"), ("Disconnected", "会话已结束"), + ("Other", "其他"), + ("Confirm before closing multiple tabs", "关闭多个标签页时向您确认"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index e849dfa3f..9ca84a28b 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -371,5 +371,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("uac_warning", ""), ("elevated_foreground_window_warning", ""), ("Disconnected", ""), + ("Other", ""), + ("Confirm before closing multiple tabs", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 3dd2e0a51..10cd64dd4 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -371,5 +371,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("uac_warning", ""), ("elevated_foreground_window_warning", ""), ("Disconnected", "Afbrudt"), + ("Other", ""), + ("Confirm before closing multiple tabs", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 38c0f657a..f45a583f9 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -371,5 +371,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("uac_warning", ""), ("elevated_foreground_window_warning", ""), ("Disconnected", ""), + ("Other", ""), + ("Confirm before closing multiple tabs", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index cee1975b0..f243eb28a 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -371,5 +371,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("uac_warning", ""), ("elevated_foreground_window_warning", ""), ("Disconnected", ""), + ("Other", ""), + ("Confirm before closing multiple tabs", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index d3da5b402..413cef26b 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -371,5 +371,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("uac_warning", ""), ("elevated_foreground_window_warning", ""), ("Disconnected", ""), + ("Other", ""), + ("Confirm before closing multiple tabs", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 70a62e49d..79c93a4d9 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -193,7 +193,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Reboot required", "Redémarrage pour prendre effet"), ("Unsupported display server ", "Le serveur d'affichage actuel n'est pas pris en charge"), ("x11 expected", "Veuillez passer à x11"), - ("Port", ""), + ("Port", "Port"), ("Settings", "Paramètres"), ("Username", " Nom d'utilisateur"), ("Invalid port", "Port invalide"), @@ -274,7 +274,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_stop_service_tip", "La fermeture du service fermera automatiquement toutes les connexions établies."), ("android_version_audio_tip", "La version actuelle d'Android ne prend pas en charge la capture audio, veuillez passer à Android 10 ou supérieur."), ("android_start_service_tip", "Appuyez sur [Démarrer le service] ou sur l'autorisation OUVRIR [Capture d'écran] pour démarrer le service de partage d'écran."), - ("Account", ""), + ("Account", "Compte"), ("Overwrite", "Écraser"), ("This file exists, skip or overwrite this file?", "Ce fichier existe, ignorer ou écraser ce fichier ?"), ("Quit", "Quitter"), @@ -371,5 +371,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("uac_warning", ""), ("elevated_foreground_window_warning", ""), ("Disconnected", ""), + ("Other", ""), + ("Confirm before closing multiple tabs", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 191165b65..b4ddebc94 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -371,5 +371,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("uac_warning", ""), ("elevated_foreground_window_warning", ""), ("Disconnected", ""), + ("Other", ""), + ("Confirm before closing multiple tabs", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 96ac3892b..a751c11c0 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -371,5 +371,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("uac_warning", ""), ("elevated_foreground_window_warning", ""), ("Disconnected", ""), + ("Other", ""), + ("Confirm before closing multiple tabs", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 1954eeaae..ae98a5690 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -371,5 +371,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("uac_warning", ""), ("elevated_foreground_window_warning", ""), ("Disconnected", ""), + ("Other", ""), + ("Confirm before closing multiple tabs", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 0c11db665..46428d74c 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -371,5 +371,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("uac_warning", ""), ("elevated_foreground_window_warning", ""), ("Disconnected", ""), + ("Other", "他の"), + ("Confirm before closing multiple tabs", "同時に複数のタブを閉じる前に確認する"), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index d59d71dc9..3c857424e 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -371,5 +371,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("uac_warning", ""), ("elevated_foreground_window_warning", ""), ("Disconnected", ""), + ("Other", ""), + ("Confirm before closing multiple tabs", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index b4f29894d..f83b8b04b 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -371,5 +371,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("uac_warning", ""), ("elevated_foreground_window_warning", ""), ("Disconnected", ""), + ("Other", ""), + ("Confirm before closing multiple tabs", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 03f030507..9130d6b9a 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -371,5 +371,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("uac_warning", "Ostrzeżenie UAC"), ("elevated_foreground_window_warning", "Pierwszoplanowe okno ostrzeżenia o podwyższeniu uprawnień"), ("Disconnected", ""), + ("Other", ""), + ("Confirm before closing multiple tabs", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 835fae801..0fd796cb3 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -371,5 +371,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("uac_warning", ""), ("elevated_foreground_window_warning", ""), ("Disconnected", ""), + ("Other", ""), + ("Confirm before closing multiple tabs", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 280390d7f..a20fdd557 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -371,5 +371,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("uac_warning", ""), ("elevated_foreground_window_warning", ""), ("Disconnected", ""), + ("Other", ""), + ("Confirm before closing multiple tabs", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 7784d74f0..c86d5cc31 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -371,5 +371,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("uac_warning", ""), ("elevated_foreground_window_warning", ""), ("Disconnected", ""), + ("Other", ""), + ("Confirm before closing multiple tabs", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 0319ddc68..8d4105118 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -371,5 +371,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("uac_warning", ""), ("elevated_foreground_window_warning", ""), ("Disconnected", ""), + ("Other", ""), + ("Confirm before closing multiple tabs", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index b292f917c..02bfc95c0 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -371,5 +371,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("uac_warning", ""), ("elevated_foreground_window_warning", ""), ("Disconnected", ""), + ("Other", ""), + ("Confirm before closing multiple tabs", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 1a7912f23..e4ee58e23 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -371,5 +371,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("uac_warning", ""), ("elevated_foreground_window_warning", ""), ("Disconnected", ""), + ("Other", ""), + ("Confirm before closing multiple tabs", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index b588f2a33..5b0dc48aa 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -371,5 +371,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("uac_warning", "暂时无法访问远端设备,因为远端设备正在请求用户账户权限,请等待对方关闭UAC窗口。为避免这个问题,建议在远端设备上安装或者以管理员权限运行本软件。"), ("elevated_foreground_window_warning", "暫時無法使用鼠標鍵盤,因為遠端桌面的當前窗口需要更高的權限才能操作, 可以請求對方最小化當前窗口。為避免這個問題,建議在遠端設備上安裝或者以管理員權限運行本軟件。"), ("Disconnected", "會話已結束"), + ("Other", "其他"), + ("Confirm before closing multiple tabs", "關閉多個分頁前跟我確認"), ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 3a5c4afd6..7d5037807 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -191,9 +191,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Warning", "Попередження"), ("Login screen using Wayland is not supported", "Вхід у систему з використанням Wayland не підтримується"), ("Reboot required", "Потрібне перезавантаження"), - ("Unsupported display server", "Непідтримуваний сервер дисплея"), + ("Unsupported display server ", ""), ("x11 expected", "Очікується X11"), - ("Port", ""), + ("Port", "Порт"), ("Settings", "Налаштування"), ("Username", "Ім'я користувача"), ("Invalid port", "Неправильний порт"), @@ -274,7 +274,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_stop_service_tip", "Закриття служби автоматично закриє всі встановлені з'єднання."), ("android_version_audio_tip", "Поточна версія Android не підтримує захоплення звуку, оновіть її до Android 10 або вище."), ("android_start_service_tip", "Натисніть [Запуск проміжного сервера] або ВІДКРИТИ роздільну здатність [Захоплення екрана], щоб запустити службу демонстрації екрана."), - ("Account", ""), + ("Account", "Акаунт"), ("Overwrite", "Перезаписати"), ("This file exists, skip or overwrite this file?", "Цей файл існує, пропустити або перезаписати файл?"), ("Quit", "Вийти"), @@ -298,7 +298,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Connection not allowed", "Підключення не дозволено"), ("Legacy mode", ""), ("Map mode", ""), - ("Режим перекладу", ""), + ("Translate mode", ""), ("Use temporary password", "Використовувати тимчасовий пароль"), ("Use permanent password", "Використовувати постійний пароль"), ("Use both passwords", "Використовувати обидва паролі"), @@ -371,5 +371,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("uac_warning", ""), ("elevated_foreground_window_warning", ""), ("Disconnected", ""), + ("Other", ""), + ("Confirm before closing multiple tabs", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index c245bdd75..4e441abff 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -371,5 +371,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("uac_warning", ""), ("elevated_foreground_window_warning", ""), ("Disconnected", ""), + ("Other", ""), + ("Confirm before closing multiple tabs", ""), ].iter().cloned().collect(); } From 614e0d40bf405bd45d041d5345e8727f0cd15853 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 14 Oct 2022 14:00:54 +0800 Subject: [PATCH 0689/2015] feat: add window event on macos, windows --- flutter/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index c2f9b960a..190a6ffa4 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -64,7 +64,7 @@ dependencies: desktop_multi_window: git: url: https://github.com/Kingtous/rustdesk_desktop_multi_window - ref: c09d65018f402dd0d6073149fe6705185101a270 + ref: f25487b8aacfcc9d22b86a84e97eda1a5c07ccaf freezed_annotation: ^2.0.3 tray_manager: git: From cf73c04cb3ed6f389d5818859950b2a1abc9e4e6 Mon Sep 17 00:00:00 2001 From: csf Date: Fri, 14 Oct 2022 10:48:33 +0900 Subject: [PATCH 0690/2015] drag to Un/Maximize update icon state --- .../lib/desktop/widgets/tabbar_widget.dart | 138 ++++++++++++------ flutter/pubspec.lock | 4 +- 2 files changed, 94 insertions(+), 48 deletions(-) diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index e3fcf0f53..d92af9447 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -350,7 +350,7 @@ class DesktopTab extends StatelessWidget { } } -class WindowActionPanel extends StatelessWidget { +class WindowActionPanel extends StatefulWidget { final bool mainTab; final DesktopTabType tabType; final Rx state; @@ -371,17 +371,79 @@ class WindowActionPanel extends StatelessWidget { this.onClose}) : super(key: key); + @override + State createState() { + return WindowActionPanelState(); + } +} + +class WindowActionPanelState extends State + with MultiWindowListener, WindowListener { + bool isMaximized = false; + + @override + void initState() { + super.initState(); + DesktopMultiWindow.addListener(this); + windowManager.addListener(this); + + // TODO init window can't detect isMaximized + if (widget.mainTab) { + windowManager.isMaximized().then((maximized) { + debugPrint("init main maximized: $maximized"); + if (isMaximized != maximized) { + WidgetsBinding.instance.addPostFrameCallback( + (_) => setState(() => isMaximized = maximized)); + } + }); + } else { + final wc = WindowController.fromWindowId(windowId!); + wc.isMaximized().then((maximized) { + debugPrint("init sun maximized: $maximized"); + if (isMaximized != maximized) { + WidgetsBinding.instance.addPostFrameCallback( + (_) => setState(() => isMaximized = maximized)); + } + }); + } + } + + @override + void dispose() { + DesktopMultiWindow.removeListener(this); + windowManager.removeListener(this); + super.dispose(); + } + + @override + void onWindowMaximize() { + // catch maximize from system + if (!isMaximized) { + setState(() => isMaximized = true); + } + super.onWindowMaximize(); + } + + @override + void onWindowUnmaximize() { + // catch unmaximize from system + if (isMaximized) { + setState(() => isMaximized = false); + } + super.onWindowUnmaximize(); + } + @override Widget build(BuildContext context) { return Row( children: [ Offstage( - offstage: !showMinimize, + offstage: !widget.showMinimize, child: ActionIcon( message: 'Minimize', icon: IconFont.min, onTap: () { - if (mainTab) { + if (widget.mainTab) { windowManager.minimize(); } else { WindowController.fromWindowId(windowId!).minimize(); @@ -389,56 +451,40 @@ class WindowActionPanel extends StatelessWidget { }, isClose: false, )), - // TODO: drag makes window restore Offstage( - offstage: !showMaximize, - child: FutureBuilder(builder: (context, snapshot) { - RxBool isMaximized = false.obs; - if (mainTab) { - windowManager.isMaximized().then((maximized) { - isMaximized.value = maximized; - }); - } else { - final wc = WindowController.fromWindowId(windowId!); - wc.isMaximized().then((maximized) { - isMaximized.value = maximized; - }); - } - return Obx( - () => ActionIcon( - message: isMaximized.value ? "Restore" : "Maximize", - icon: isMaximized.value ? IconFont.restore : IconFont.max, - onTap: () { - if (mainTab) { - if (isMaximized.value) { - windowManager.unmaximize(); - } else { - windowManager.maximize(); - } - } else { - // TODO: subwindow is maximized but first query result is not maximized. - final wc = WindowController.fromWindowId(windowId!); - if (isMaximized.value) { - wc.unmaximize(); - } else { - wc.maximize(); - } - } - isMaximized.value = !isMaximized.value; - }, - isClose: false, - ), - ); - })), + offstage: !widget.showMaximize, + child: ActionIcon( + message: isMaximized ? "Restore" : "Maximize", + icon: isMaximized ? IconFont.restore : IconFont.max, + onTap: () { + if (widget.mainTab) { + if (isMaximized) { + windowManager.unmaximize(); + } else { + windowManager.maximize(); + } + } else { + final wc = WindowController.fromWindowId(windowId!); + if (isMaximized) { + wc.unmaximize(); + } else { + wc.maximize(); + } + } + // setState for sub window, wc.unmaximize/maximize() will not invoke onWindowMaximize/Unmaximize + setState(() => isMaximized = !isMaximized); + }, + isClose: false, + )), Offstage( - offstage: !showClose, + offstage: !widget.showClose, child: ActionIcon( message: 'Close', icon: IconFont.close, onTap: () async { - final res = await onClose?.call() ?? true; + final res = await widget.onClose?.call() ?? true; if (res) { - if (mainTab) { + if (widget.mainTab) { windowManager.close(); } else { // only hide for multi window, not close diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index fb28840b4..d5f46f2f6 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -243,8 +243,8 @@ packages: dependency: "direct main" description: path: "." - ref: "0019311e8aba4e84ffd44c57ba1834cc76924f2a" - resolved-ref: "0019311e8aba4e84ffd44c57ba1834cc76924f2a" + ref: c09d65018f402dd0d6073149fe6705185101a270 + resolved-ref: c09d65018f402dd0d6073149fe6705185101a270 url: "https://github.com/Kingtous/rustdesk_desktop_multi_window" source: git version: "0.1.0" From c01b9d5d7d846ab8b5d99daa24ad8b1524060411 Mon Sep 17 00:00:00 2001 From: csf Date: Fri, 14 Oct 2022 19:48:41 +0900 Subject: [PATCH 0691/2015] restoreWindowPosition for sub window and add restore maximize --- flutter/lib/common.dart | 110 ++++++++++++------ flutter/lib/consts.dart | 2 + .../lib/desktop/widgets/tabbar_widget.dart | 9 +- flutter/lib/main.dart | 4 +- flutter/lib/utils/multi_window_manager.dart | 2 + flutter/pubspec.lock | 4 +- 6 files changed, 89 insertions(+), 42 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 890feac6b..6e32ad09c 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -38,7 +38,6 @@ var isWeb = false; var isWebDesktop = false; var version = ""; int androidVersion = 0; -const windowPrefix = "wm_"; DesktopType? desktopType; typedef F = String Function(String); @@ -957,16 +956,18 @@ class LastWindowPosition { double? height; double? offsetWidth; double? offsetHeight; + bool? isMaximized; - LastWindowPosition( - this.width, this.height, this.offsetWidth, this.offsetHeight); + LastWindowPosition(this.width, this.height, this.offsetWidth, + this.offsetHeight, this.isMaximized); Map toJson() { return { "width": width, "height": height, "offsetWidth": offsetWidth, - "offsetHeight": offsetHeight + "offsetHeight": offsetHeight, + "isMaximized": isMaximized, }; } @@ -981,8 +982,8 @@ class LastWindowPosition { } try { final m = jsonDecode(content); - return LastWindowPosition( - m["width"], m["height"], m["offsetWidth"], m["offsetHeight"]); + return LastWindowPosition(m["width"], m["height"], m["offsetWidth"], + m["offsetHeight"], m["isMaximized"]); } catch (e) { debugPrint(e.toString()); return null; @@ -999,22 +1000,29 @@ Future saveWindowPosition(WindowType type, {int? windowId}) async { } switch (type) { case WindowType.Main: - List resp = await Future.wait( - [windowManager.getPosition(), windowManager.getSize()]); - Offset position = resp[0]; - Size sz = resp[1]; - final pos = - LastWindowPosition(sz.width, sz.height, position.dx, position.dy); + final position = await windowManager.getPosition(); + final sz = await windowManager.getSize(); + final isMaximized = await windowManager.isMaximized(); + final pos = LastWindowPosition( + sz.width, sz.height, position.dx, position.dy, isMaximized); await Get.find() - .setString(windowPrefix + type.name, pos.toString()); + .setString(kWindowPrefix + type.name, pos.toString()); break; default: - // TODO: implement window + final wc = WindowController.fromWindowId(windowId!); + final frame = await wc.getFrame(); + final position = frame.topLeft; + final sz = frame.size; + final isMaximized = await wc.isMaximized(); + final pos = LastWindowPosition( + sz.width, sz.height, position.dx, position.dy, isMaximized); + await Get.find() + .setString(kWindowPrefix + type.name, pos.toString()); break; } } -_adjustRestoreMainWindowSize(double? width, double? height) async { +Future _adjustRestoreMainWindowSize(double? width, double? height) async { const double minWidth = 600; const double minHeight = 100; double maxWidth = (((isDesktop || isWebDesktop) @@ -1055,10 +1063,12 @@ _adjustRestoreMainWindowSize(double? width, double? height) async { if (restoreHeight > maxHeight) { restoreWidth = maxHeight; } - await windowManager.setSize(Size(restoreWidth, restoreHeight)); + return Size(restoreWidth, restoreHeight); } -_adjustRestoreMainWindowOffset(double? left, double? top) async { +/// return null means center +Future _adjustRestoreMainWindowOffset( + double? left, double? top) async { if (left == null || top == null) { await windowManager.center(); } else { @@ -1090,40 +1100,68 @@ _adjustRestoreMainWindowOffset(double? left, double? top) async { windowLeft > frameRight || windowTop < frameTop || windowTop > frameBottom) { - await windowManager.center(); + return null; } else { - await windowManager.setPosition(Offset(windowLeft, windowTop)); + return Offset(windowLeft, windowTop); } } + return null; } -/// Save window position and size on exit +/// Restore window position and size on start /// Note that windowId must be provided if it's subwindow Future restoreWindowPosition(WindowType type, {int? windowId}) async { if (type != WindowType.Main && windowId == null) { debugPrint( "Error: windowId cannot be null when saving positions for sub window"); } + final pos = + Get.find().getString(kWindowPrefix + type.name); + + if (pos == null) { + debugPrint("no window position saved, ignore restore"); + return false; + } + var lpos = LastWindowPosition.loadFromString(pos); + if (lpos == null) { + debugPrint("window position saved, but cannot be parsed"); + return false; + } + switch (type) { case WindowType.Main: - var pos = - Get.find().getString(windowPrefix + type.name); - if (pos == null) { - debugPrint("no window position saved, ignore restore"); - return false; + if (lpos.isMaximized == true) { + await windowManager.maximize(); + } else { + final size = + await _adjustRestoreMainWindowSize(lpos.width, lpos.height); + final offset = await _adjustRestoreMainWindowOffset( + lpos.offsetWidth, lpos.offsetHeight); + await windowManager.setSize(size); + if (offset == null) { + await windowManager.center(); + } else { + await windowManager.setPosition(offset); + } } - var lpos = LastWindowPosition.loadFromString(pos); - if (lpos == null) { - debugPrint("window position saved, but cannot be parsed"); - return false; - } - - await _adjustRestoreMainWindowSize(lpos.width, lpos.height); - await _adjustRestoreMainWindowOffset(lpos.offsetWidth, lpos.offsetHeight); - return true; default: - // TODO: implement subwindow + final wc = WindowController.fromWindowId(windowId!); + if (lpos.isMaximized == true) { + await wc.maximize(); + } else { + final size = + await _adjustRestoreMainWindowSize(lpos.width, lpos.height); + final offset = await _adjustRestoreMainWindowOffset( + lpos.offsetWidth, lpos.offsetHeight); + if (offset == null) { + await wc.center(); + } else { + final frame = + Rect.fromLTWH(offset.dx, offset.dy, size.width, size.height); + await wc.setFrame(frame); + } + } break; } return false; @@ -1150,7 +1188,7 @@ void checkArguments() { } /// Parse `rustdesk://` unilinks -/// +/// /// [Functions] /// 1. New Connection: rustdesk://connection/new/your_peer_id void parseRustdeskUri(String uriPath) { diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 056cc000c..f43c20cc6 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -15,6 +15,8 @@ const String kActionNewConnection = "connection/new/"; const String kTabLabelHomePage = "Home"; const String kTabLabelSettingPage = "Settings"; +const String kWindowPrefix = "wm_"; + const Color kColorWarn = Color.fromARGB(255, 245, 133, 59); const int kMobileDefaultDisplayWidth = 720; diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index d92af9447..b11ded495 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -387,10 +387,8 @@ class WindowActionPanelState extends State DesktopMultiWindow.addListener(this); windowManager.addListener(this); - // TODO init window can't detect isMaximized if (widget.mainTab) { windowManager.isMaximized().then((maximized) { - debugPrint("init main maximized: $maximized"); if (isMaximized != maximized) { WidgetsBinding.instance.addPostFrameCallback( (_) => setState(() => isMaximized = maximized)); @@ -399,7 +397,6 @@ class WindowActionPanelState extends State } else { final wc = WindowController.fromWindowId(windowId!); wc.isMaximized().then((maximized) { - debugPrint("init sun maximized: $maximized"); if (isMaximized != maximized) { WidgetsBinding.instance.addPostFrameCallback( (_) => setState(() => isMaximized = maximized)); @@ -433,6 +430,12 @@ class WindowActionPanelState extends State super.onWindowUnmaximize(); } + @override + void onWindowClose() { + debugPrint("onWindowClose : is Main : ${widget.mainTab}"); + super.onWindowClose(); + } + @override Widget build(BuildContext context) { return Row( diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 43f4b9c24..835606eb1 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -45,7 +45,6 @@ Future main(List args) async { int type = argument['type'] ?? -1; argument['windowId'] = windowId; WindowType wType = type.windowType; - restoreWindowPosition(wType, windowId: windowId); switch (wType) { case WindowType.RemoteDesktop: desktopType = DesktopType.remote; @@ -118,6 +117,7 @@ void runMobileApp() async { void runRemoteScreen(Map argument) async { await initEnv(kAppTypeDesktopRemote); + await restoreWindowPosition(WindowType.RemoteDesktop, windowId: windowId); runApp(GetMaterialApp( navigatorKey: globalKey, debugShowCheckedModeBanner: false, @@ -143,6 +143,7 @@ void runRemoteScreen(Map argument) async { void runFileTransferScreen(Map argument) async { await initEnv(kAppTypeDesktopFileTransfer); + await restoreWindowPosition(WindowType.FileTransfer, windowId: windowId); runApp( GetMaterialApp( navigatorKey: globalKey, @@ -168,6 +169,7 @@ void runFileTransferScreen(Map argument) async { void runPortForwardScreen(Map argument) async { await initEnv(kAppTypeDesktopPortForward); + await restoreWindowPosition(WindowType.PortForward, windowId: windowId); runApp( GetMaterialApp( navigatorKey: globalKey, diff --git a/flutter/lib/utils/multi_window_manager.dart b/flutter/lib/utils/multi_window_manager.dart index 97d5a5e23..8fd71540d 100644 --- a/flutter/lib/utils/multi_window_manager.dart +++ b/flutter/lib/utils/multi_window_manager.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_hbb/common.dart'; /// must keep the order enum WindowType { Main, RemoteDesktop, FileTransfer, PortForward, Unknown } @@ -153,6 +154,7 @@ class RustDeskMultiWindowManager { int? wId = findWindowByType(type); if (wId != null) { debugPrint("closing multi window: ${type.toString()}"); + saveWindowPosition(type, windowId: wId); try { final ids = await DesktopMultiWindow.getAllSubWindowIds(); if (!ids.contains(wId)) { diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index d5f46f2f6..4b78bab93 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -243,8 +243,8 @@ packages: dependency: "direct main" description: path: "." - ref: c09d65018f402dd0d6073149fe6705185101a270 - resolved-ref: c09d65018f402dd0d6073149fe6705185101a270 + ref: f25487b8aacfcc9d22b86a84e97eda1a5c07ccaf + resolved-ref: f25487b8aacfcc9d22b86a84e97eda1a5c07ccaf url: "https://github.com/Kingtous/rustdesk_desktop_multi_window" source: git version: "0.1.0" From 06844f2f4f0407931f62c31b3e444b82ae0b547b Mon Sep 17 00:00:00 2001 From: csf Date: Fri, 14 Oct 2022 20:44:57 +0900 Subject: [PATCH 0692/2015] double click toggle maximize --- .../lib/desktop/widgets/tabbar_widget.dart | 173 ++++++++++-------- 1 file changed, 95 insertions(+), 78 deletions(-) diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index b11ded495..3eadf75fd 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -289,57 +289,48 @@ class DesktopTab extends StatelessWidget { Widget _buildBar() { return Row( children: [ - Expanded( - child: Row( - children: [ - Offstage( - offstage: !Platform.isMacOS, - child: const SizedBox( - width: 78, - )), - Row(children: [ - Offstage( - offstage: !showLogo, - child: SvgPicture.asset( - 'assets/logo.svg', - width: 16, - height: 16, - )), - Offstage( - offstage: !showTitle, - child: const Text( - "RustDesk", - style: TextStyle(fontSize: 13), - ).marginOnly(left: 2)) - ]).marginOnly( - left: 5, - right: 10, - ), - Expanded( - child: GestureDetector( - onPanStart: (_) { - if (isMainWindow) { - windowManager.startDragging(); - } else { - WindowController.fromWindowId(windowId!) - .startDragging(); - } - }, - child: _ListView( - controller: controller, - onTabClose: onTabClose, - tabBuilder: tabBuilder, - labelGetter: labelGetter, - )), - ), - ], - ), + Row( + children: [ + Offstage( + offstage: !Platform.isMacOS, + child: const SizedBox( + width: 78, + )), + GestureDetector( + onDoubleTap: () => + showMaximize ? toggleMaximize(isMainWindow) : null, + onPanStart: (_) => startDragging(isMainWindow), + child: Row(children: [ + Offstage( + offstage: !showLogo, + child: SvgPicture.asset( + 'assets/logo.svg', + width: 16, + height: 16, + )), + Offstage( + offstage: !showTitle, + child: const Text( + "RustDesk", + style: TextStyle(fontSize: 13), + ).marginOnly(left: 2)) + ]).marginOnly( + left: 5, + right: 10, + )), + _ListView( + controller: controller, + onTabClose: onTabClose, + tabBuilder: tabBuilder, + labelGetter: labelGetter, + ), + ], ), - Offstage(offstage: tail == null, child: tail), WindowActionPanel( - mainTab: isMainWindow, + isMainWindow: isMainWindow, tabType: tabType, state: state, + tail: tail, showMinimize: showMinimize, showMaximize: showMaximize, showClose: showClose, @@ -351,20 +342,22 @@ class DesktopTab extends StatelessWidget { } class WindowActionPanel extends StatefulWidget { - final bool mainTab; + final bool isMainWindow; final DesktopTabType tabType; final Rx state; final bool showMinimize; final bool showMaximize; final bool showClose; + final Widget? tail; final Future Function()? onClose; const WindowActionPanel( {Key? key, - required this.mainTab, + required this.isMainWindow, required this.tabType, required this.state, + this.tail, this.showMinimize = true, this.showMaximize = true, this.showClose = true, @@ -387,7 +380,7 @@ class WindowActionPanelState extends State DesktopMultiWindow.addListener(this); windowManager.addListener(this); - if (widget.mainTab) { + if (widget.isMainWindow) { windowManager.isMaximized().then((maximized) { if (isMaximized != maximized) { WidgetsBinding.instance.addPostFrameCallback( @@ -430,23 +423,24 @@ class WindowActionPanelState extends State super.onWindowUnmaximize(); } - @override - void onWindowClose() { - debugPrint("onWindowClose : is Main : ${widget.mainTab}"); - super.onWindowClose(); - } - @override Widget build(BuildContext context) { - return Row( + return Expanded( + child: Row( children: [ + Expanded( + child: GestureDetector( + onDoubleTap: widget.showMaximize ? _toggleMaximize : null, + onPanStart: (_) => startDragging(widget.isMainWindow), + )), + Offstage(offstage: widget.tail == null, child: widget.tail), Offstage( offstage: !widget.showMinimize, child: ActionIcon( message: 'Minimize', icon: IconFont.min, onTap: () { - if (widget.mainTab) { + if (widget.isMainWindow) { windowManager.minimize(); } else { WindowController.fromWindowId(windowId!).minimize(); @@ -459,24 +453,7 @@ class WindowActionPanelState extends State child: ActionIcon( message: isMaximized ? "Restore" : "Maximize", icon: isMaximized ? IconFont.restore : IconFont.max, - onTap: () { - if (widget.mainTab) { - if (isMaximized) { - windowManager.unmaximize(); - } else { - windowManager.maximize(); - } - } else { - final wc = WindowController.fromWindowId(windowId!); - if (isMaximized) { - wc.unmaximize(); - } else { - wc.maximize(); - } - } - // setState for sub window, wc.unmaximize/maximize() will not invoke onWindowMaximize/Unmaximize - setState(() => isMaximized = !isMaximized); - }, + onTap: _toggleMaximize, isClose: false, )), Offstage( @@ -487,7 +464,7 @@ class WindowActionPanelState extends State onTap: () async { final res = await widget.onClose?.call() ?? true; if (res) { - if (widget.mainTab) { + if (widget.isMainWindow) { windowManager.close(); } else { // only hide for multi window, not close @@ -500,7 +477,47 @@ class WindowActionPanelState extends State isClose: true, )), ], - ); + )); + } + + void _toggleMaximize() { + toggleMaximize(widget.isMainWindow).then((maximize) { + if (isMaximized != maximize) { + // setState for sub window, wc.unmaximize/maximize() will not invoke onWindowMaximize/Unmaximize + setState(() => isMaximized = !isMaximized); + } + }); + } +} + +void startDragging(bool isMainWindow) { + if (isMainWindow) { + windowManager.startDragging(); + } else { + WindowController.fromWindowId(windowId!).startDragging(); + } +} + +/// return true -> window will be maximize +/// return false -> window will be unmaximize +Future toggleMaximize(bool isMainWindow) async { + if (isMainWindow) { + if (await windowManager.isMaximized()) { + windowManager.unmaximize(); + return false; + } else { + windowManager.maximize(); + return true; + } + } else { + final wc = WindowController.fromWindowId(windowId!); + if (await wc.isMaximized()) { + wc.unmaximize(); + return false; + } else { + wc.maximize(); + return true; + } } } From 516ff4221b4c8ae50c70ab97abba891f031b2614 Mon Sep 17 00:00:00 2001 From: csf Date: Fri, 14 Oct 2022 23:50:13 +0900 Subject: [PATCH 0693/2015] opt desktop chat page style --- flutter/lib/common/widgets/overlay.dart | 38 +++++++++---------- .../lib/desktop/widgets/tabbar_widget.dart | 20 +++++----- flutter/lib/mobile/pages/chat_page.dart | 35 ++++++++++++----- 3 files changed, 56 insertions(+), 37 deletions(-) diff --git a/flutter/lib/common/widgets/overlay.dart b/flutter/lib/common/widgets/overlay.dart index c5a3ad8b6..9680d1bf3 100644 --- a/flutter/lib/common/widgets/overlay.dart +++ b/flutter/lib/common/widgets/overlay.dart @@ -36,7 +36,7 @@ class DraggableChatWindow extends StatelessWidget { appBar: CustomAppBar( onPanUpdate: onPanUpdate, appBar: isDesktop - ? _buildDesktopAppBar() + ? _buildDesktopAppBar(context) : _buildMobileAppBar(context), ), body: ChatPage(chatModel: chatModel), @@ -82,33 +82,33 @@ class DraggableChatWindow extends StatelessWidget { ); } - Widget _buildDesktopAppBar() { + Widget _buildDesktopAppBar(BuildContext context) { return Container( - color: MyTheme.accent50, - height: 35, + decoration: BoxDecoration( + border: Border( + 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), - child: Text( - translate("Chat"), - style: const TextStyle( - color: Colors.white, - fontFamily: 'WorkSans', - fontWeight: FontWeight.bold), - )), - Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - ActionIcon( + 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, - ) - ], - ) + size: 32, + )) ], ), ); diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 3eadf75fd..9e191ac28 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -799,13 +799,15 @@ class ActionIcon extends StatelessWidget { final IconData icon; final Function() onTap; final bool isClose; - const ActionIcon({ - Key? key, - required this.message, - required this.icon, - required this.onTap, - required this.isClose, - }) : super(key: key); + final double? size; + const ActionIcon( + {Key? key, + required this.message, + required this.icon, + required this.onTap, + required this.isClose, + this.size}) + : super(key: key); @override Widget build(BuildContext context) { @@ -820,8 +822,8 @@ class ActionIcon extends StatelessWidget { onHover: (value) => hover.value = value, onTap: onTap, child: SizedBox( - height: _kTabBarHeight - 1, - width: _kTabBarHeight - 1, + height: size ?? (_kTabBarHeight - 1), + width: size ?? (_kTabBarHeight - 1), child: Icon( icon, color: hover.value && isClose diff --git a/flutter/lib/mobile/pages/chat_page.dart b/flutter/lib/mobile/pages/chat_page.dart index b7cf28c9d..11794cb3d 100644 --- a/flutter/lib/mobile/pages/chat_page.dart +++ b/flutter/lib/mobile/pages/chat_page.dart @@ -61,19 +61,36 @@ class ChatPage extends StatelessWidget implements PageShape { [], inputOptions: InputOptions( sendOnEnter: true, - inputDecoration: defaultInputDecoration( - hintText: "${translate('Write a message')}...", - fillColor: Theme.of(context).backgroundColor), - sendButtonBuilder: defaultSendButton( - color: Theme.of(context) - .textTheme - .titleLarge! - .color!), inputTextStyle: TextStyle( + fontSize: 14, color: Theme.of(context) .textTheme .titleLarge - ?.color)), + ?.color), + inputDecoration: isDesktop + ? InputDecoration( + isDense: true, + hintText: + "${translate('Write a message')}...", + filled: true, + fillColor: Theme.of(context).backgroundColor, + contentPadding: EdgeInsets.all(10), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(6), + borderSide: const BorderSide( + width: 0, + style: BorderStyle.none, + ), + ), + ) + : defaultInputDecoration( + hintText: + "${translate('Write a message')}...", + fillColor: Theme.of(context).backgroundColor), + sendButtonBuilder: defaultSendButton( + padding: EdgeInsets.symmetric( + horizontal: 6, vertical: 0), + color: Theme.of(context).colorScheme.primary)), messageOptions: MessageOptions( showOtherUsersAvatar: false, showTime: true, From e667dad144579e86bf14f529f0a95b3b8807b967 Mon Sep 17 00:00:00 2001 From: sandroid Date: Sun, 16 Oct 2022 21:45:59 +0200 Subject: [PATCH 0694/2015] Fix usage of loginctl inside flatpak Signed-off-by: sandroid --- libs/hbb_common/Cargo.toml | 1 + libs/hbb_common/src/platform/linux.rs | 36 +++++++++++++++++++-------- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/libs/hbb_common/Cargo.toml b/libs/hbb_common/Cargo.toml index e7377608b..59f0896cc 100644 --- a/libs/hbb_common/Cargo.toml +++ b/libs/hbb_common/Cargo.toml @@ -38,6 +38,7 @@ machine-uid = "0.2" [features] quic = [] +flatpak = [] [build-dependencies] protobuf-codegen = { version = "3.1" } diff --git a/libs/hbb_common/src/platform/linux.rs b/libs/hbb_common/src/platform/linux.rs index 865033204..08e091337 100644 --- a/libs/hbb_common/src/platform/linux.rs +++ b/libs/hbb_common/src/platform/linux.rs @@ -6,9 +6,7 @@ pub fn get_display_server() -> String { } fn get_display_server_of_session(session: &str) -> String { - if let Ok(output) = std::process::Command::new("loginctl") - .args(vec!["show-session", "-p", "Type", session]) - .output() + if let Ok(output) = run_loginctl(Some(vec!["show-session", "-p", "Type", session])) // Check session type of the session { let display_server = String::from_utf8_lossy(&output.stdout) @@ -17,9 +15,7 @@ fn get_display_server_of_session(session: &str) -> String { .into(); if display_server == "tty" { // If the type is tty... - if let Ok(output) = std::process::Command::new("loginctl") - .args(vec!["show-session", "-p", "TTY", session]) - .output() + if let Ok(output) = run_loginctl(Some(vec!["show-session", "-p", "TTY", session])) // Get the tty number { let tty: String = String::from_utf8_lossy(&output.stdout) @@ -56,7 +52,7 @@ fn get_display_server_of_session(session: &str) -> String { } pub fn get_value_of_seat0(i: usize) -> String { - if let Ok(output) = std::process::Command::new("loginctl").output() { + if let Ok(output) = run_loginctl(None) { for line in String::from_utf8_lossy(&output.stdout).lines() { if line.contains("seat0") { if let Some(sid) = line.split_whitespace().nth(0) { @@ -71,7 +67,7 @@ pub fn get_value_of_seat0(i: usize) -> String { } // some case, there is no seat0 https://github.com/rustdesk/rustdesk/issues/73 - if let Ok(output) = std::process::Command::new("loginctl").output() { + if let Ok(output) = run_loginctl(None) { for line in String::from_utf8_lossy(&output.stdout).lines() { if let Some(sid) = line.split_whitespace().nth(0) { let d = get_display_server_of_session(sid); @@ -93,9 +89,7 @@ pub fn get_value_of_seat0(i: usize) -> String { } fn is_active(sid: &str) -> bool { - if let Ok(output) = std::process::Command::new("loginctl") - .args(vec!["show-session", "-p", "State", sid]) - .output() + if let Ok(output) = run_loginctl(Some(vec!["show-session", "-p", "State", sid])) { String::from_utf8_lossy(&output.stdout).contains("active") } else { @@ -109,3 +103,23 @@ pub fn run_cmds(cmds: String) -> ResultType { .output()?; Ok(String::from_utf8_lossy(&output.stdout).to_string()) } + +#[cfg(not(feature = "flatpak"))] +fn run_loginctl(args: Option>) -> std::io::Result { + let mut cmd = std::process::Command::new("loginctl"); + if let Some(a) = args { + return cmd.args(a).output(); + } + cmd.output() +} + +#[cfg(feature = "flatpak")] +fn run_loginctl(args: Option>) -> std::io::Result { + let mut l_args = String::from("loginctl"); + if let Some(a) = args { + l_args = format!("{} {}", l_args, a.join(" ")); + } + std::process::Command::new("flatpak-spawn") + .args(vec![String::from("--host"), l_args]) + .output() +} From 52a21234d4f2bf95ce6a8de568c2d4338bda6471 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sun, 16 Oct 2022 12:29:51 +0800 Subject: [PATCH 0695/2015] show disabled cursor on the control end Signed-off-by: 21pages --- flutter/lib/desktop/pages/remote_page.dart | 32 +++++++++++++++++++--- flutter/lib/models/model.dart | 11 +++++++- 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 3757d96e6..01229ab2b 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -280,10 +280,12 @@ class ImagePaint extends StatelessWidget { final s = c.scale; mouseRegion({child}) => Obx(() => MouseRegion( - cursor: (cursorOverImage.isTrue && keyboardEnabled.isTrue) - ? (remoteCursorMoved.isTrue - ? SystemMouseCursors.none - : _buildCustomCursorLinux(context, s)) + cursor: cursorOverImage.isTrue + ? keyboardEnabled.isTrue + ? (remoteCursorMoved.isTrue + ? SystemMouseCursors.none + : _buildCustomCursorLinux(context, s)) + : _buildDisabledCursor(context, s) : MouseCursor.defer, onHover: (evt) {}, child: child)); @@ -350,6 +352,28 @@ class ImagePaint extends StatelessWidget { } } + MouseCursor _buildDisabledCursor(BuildContext context, double scale) { + final cursor = Provider.of(context); + final cacheLinux = cursor.cacheLinux; + if (cacheLinux == null) { + return MouseCursor.defer; + } else { + if (cursor.cachedForbidmemoryCursorData == null) { + cursor.updateForbiddenCursorBuffer(); + } + final key = 'disabled_cursor_key'; + cursor.addKeyLinux(key); + return FlutterCustomMemoryImageCursor( + pixbuf: cursor.cachedForbidmemoryCursorData, + key: key, + hotx: cacheLinux.hotx, + hoty: cacheLinux.hoty, + imageWidth: 32, + imageHeight: 32, + ); + } + } + Widget _buildCrossScrollbarFromLayout( BuildContext context, Widget child, Size layoutSize, Size size) { final scrollConfig = CustomMouseWheelScrollConfig( diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 8b4d89461..1fd33bc77 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -79,7 +79,10 @@ class FfiModel with ChangeNotifier { if (k == 'name' || k.isEmpty) return; _permissions[k] = v == 'true'; }); - KeyboardEnabledState.find(id).value = _permissions['keyboard'] != false; + // Only inited at remote page + if (desktopType == DesktopType.remote) { + KeyboardEnabledState.find(id).value = _permissions['keyboard'] != false; + } debugPrint('$_permissions'); notifyListeners(); } @@ -938,6 +941,12 @@ class CursorModel with ChangeNotifier { customCursorController.freeCache(key); } } + + Uint8List? cachedForbidmemoryCursorData; + void updateForbiddenCursorBuffer() { + cachedForbidmemoryCursorData ??= base64Decode( + 'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAAXNSR0IB2cksfwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAkZQTFRFAAAA2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4GWAwCAAAAAAAA2B4GAAAAMTExAAAAAAAA2B4G2B4G2B4GAAAAmZmZkZGRAQEBAAAA2B4G2B4G2B4G////oKCgAwMDag8D2B4G2B4G2B4Gra2tBgYGbg8D2B4G2B4Gubm5CQkJTwsCVgwC2B4GxcXFDg4OAAAAAAAA2B4G2B4Gz8/PFBQUAAAAAAAA2B4G2B4G2B4G2B4G2B4G2B4G2B4GDgIA2NjYGxsbAAAAAAAA2B4GFwMB4eHhIyMjAAAAAAAA2B4G6OjoLCwsAAAAAAAA2B4G2B4G2B4G2B4G2B4GCQEA4ODgv7+/iYmJY2NjAgICAAAA9PT0Ojo6AAAAAAAAAAAA+/v7SkpKhYWFr6+vAAAAAAAA8/PzOTk5ERER9fX1KCgoAAAAgYGBKioqAAAAAAAApqamlpaWAAAAAAAAAAAAAAAAAAAAAAAALi4u/v7+GRkZAAAAAAAAAAAAAAAAAAAAfn5+AAAAAAAAV1dXkJCQAAAAAAAAAQEBAAAAAAAAAAAA7Hz6BAAAAMJ0Uk5TAAIWEwEynNz6//fVkCAatP2fDUHs6cDD8d0mPfT5fiEskiIR584A0gejr3AZ+P4plfALf5ZiTL85a4ziD6697fzN3UYE4v/4TwrNHuT///tdRKZh///+1U/ZBv///yjb///eAVL//50Cocv//6oFBbPvpGZCbfT//7cIhv///8INM///zBEcWYSZmO7//////1P////ts/////8vBv//////gv//R/z///QQz9sevP///2waXhNO/+fc//8mev/5gAe2r90MAAAByUlEQVR4nGNggANGJmYWBpyAlY2dg5OTi5uHF6s0H78AJxRwCAphyguLgKRExcQlQLSkFLq8tAwnp6ycPNABjAqKQKNElVDllVU4OVVhVquJA81Q10BRoAkUUYbJa4Edoo0sr6PLqaePLG/AyWlohKTAmJPTBFnelAFoixmSAnNOTgsUeQZLTk4rJAXWnJw2EHlbiDyDPCenHZICe04HFrh+RydnBgYWPU5uJAWinJwucPNd3dw9GDw5Ob2QFHBzcnrD7ffx9fMPCOTkDEINhmC4+3x8Q0LDwlEDIoKTMzIKKg9SEBIdE8sZh6SAJZ6Tkx0qD1YQkpCYlIwclCng0AXLQxSEpKalZyCryATKZwkhKQjJzsnNQ1KQXwBUUVhUXBJYWgZREFJeUVmFpMKlWg+anmqgCkJq6+obkG1pLEBTENLU3NKKrIKhrb2js8u4G6Kgpze0r3/CRAZMAHbkpJDJU6ZMmTqtFbuC6TNmhsyaMnsOFlmwgrnzpsxfELJwEXZ5Bp/FS3yWLlsesmLlKuwKVk9Ys5Zh3foN0zduwq5g85atDAzbpqSGbN9RhV0FGOzctWH3lD14FOzdt3H/gQw8Cg4u2gQPAwBYDXXdIH+wqAAAAABJRU5ErkJggg=='); + } } class QualityMonitorData { From cba450b32f6d1048b713dd3d61305306763f64fe Mon Sep 17 00:00:00 2001 From: 21pages Date: Sun, 16 Oct 2022 12:32:52 +0800 Subject: [PATCH 0696/2015] fix id input focus problem Signed-off-by: 21pages --- flutter/lib/desktop/pages/connection_page.dart | 13 +++++++------ flutter/lib/desktop/pages/desktop_setting_page.dart | 8 +++++--- flutter/lib/desktop/widgets/remote_menubar.dart | 6 +++++- flutter/lib/models/server_model.dart | 3 +++ 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/fr.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/ru.rs | 1 + src/lang/sk.rs | 1 + src/lang/template.rs | 1 + src/lang/tr.rs | 1 + src/lang/tw.rs | 1 + src/lang/ua.rs | 1 + src/lang/vn.rs | 1 + 27 files changed, 43 insertions(+), 10 deletions(-) diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 9a606d122..2e7026071 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -36,6 +36,9 @@ class _ConnectionPageState extends State Timer? _updateTimer; + final RxBool _idInputFocused = false.obs; + final FocusNode _idFocusNode = FocusNode(); + @override void initState() { super.initState(); @@ -121,10 +124,8 @@ class _ConnectionPageState extends State /// UI for the remote ID TextField. /// Search for a peer and connect to it if the id exists. Widget _buildRemoteIDTextField(BuildContext context) { - RxBool inputFocused = false.obs; - FocusNode focusNode = FocusNode(); - focusNode.addListener(() { - inputFocused.value = focusNode.hasFocus; + _idFocusNode.addListener(() { + _idInputFocused.value = _idFocusNode.hasFocus; }); var w = Container( width: 320 + 20 * 2, @@ -155,7 +156,7 @@ class _ConnectionPageState extends State autocorrect: false, enableSuggestions: false, keyboardType: TextInputType.visiblePassword, - focusNode: focusNode, + focusNode: _idFocusNode, style: const TextStyle( fontFamily: 'WorkSans', fontSize: 22, @@ -165,7 +166,7 @@ class _ConnectionPageState extends State cursorColor: Theme.of(context).textTheme.titleLarge?.color, decoration: InputDecoration( - hintText: inputFocused.value + hintText: _idInputFocused.value ? null : translate('Enter Remote ID'), border: OutlineInputBorder( diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 55fd93bcc..0c4cc0aec 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -977,9 +977,11 @@ Widget _OptionCheckBox(BuildContext context, String label, String key, ], ), ).marginOnly(left: _kCheckBoxLeftMargin), - onTap: () { - onChanged(!ref.value); - }, + onTap: enabled + ? () { + onChanged(!ref.value); + } + : null, ); }); } diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 13aa1932f..72cc56cad 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -364,6 +364,10 @@ class _RemoteMenubarState extends State { } Widget _buildKeyboard(BuildContext context) { + FfiModel ffiModel = Provider.of(context); + if (ffiModel.permissions['keyboard'] == false) { + return Offstage(); + } return mod_menu.PopupMenuButton( padding: EdgeInsets.zero, icon: const Icon( @@ -517,7 +521,7 @@ class _RemoteMenubarState extends State { )); } } - if (gFFI.ffiModel.permissions["restart"] != false && + if (perms["restart"] != false && (pi.platform == "Linux" || pi.platform == "Windows" || pi.platform == "Mac OS")) { diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index decb10e54..87ac9c8ea 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -556,6 +556,9 @@ class Client { data['keyboard'] = keyboard; data['clipboard'] = clipboard; data['audio'] = audio; + data['file'] = file; + data['restart'] = restart; + data['recording'] = recording; data['disconnected'] = disconnected; return data; } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 0d0b9e208..47b5f0bcb 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -373,5 +373,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Disconnected", "会话已结束"), ("Other", "其他"), ("Confirm before closing multiple tabs", "关闭多个标签页时向您确认"), + ("Keyboard Settings", "键盘设置"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 9ca84a28b..2515f2231 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -373,5 +373,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Disconnected", ""), ("Other", ""), ("Confirm before closing multiple tabs", ""), + ("Keyboard Settings", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 10cd64dd4..df1a06f6d 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -373,5 +373,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Disconnected", "Afbrudt"), ("Other", ""), ("Confirm before closing multiple tabs", ""), + ("Keyboard Settings", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index f45a583f9..1b9973957 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -373,5 +373,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Disconnected", ""), ("Other", ""), ("Confirm before closing multiple tabs", ""), + ("Keyboard Settings", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index f243eb28a..3c6b3b771 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -373,5 +373,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Disconnected", ""), ("Other", ""), ("Confirm before closing multiple tabs", ""), + ("Keyboard Settings", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 413cef26b..3826650af 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -373,5 +373,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Disconnected", ""), ("Other", ""), ("Confirm before closing multiple tabs", ""), + ("Keyboard Settings", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 79c93a4d9..4b76b8941 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -373,5 +373,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Disconnected", ""), ("Other", ""), ("Confirm before closing multiple tabs", ""), + ("Keyboard Settings", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index b4ddebc94..98e15397c 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -373,5 +373,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Disconnected", ""), ("Other", ""), ("Confirm before closing multiple tabs", ""), + ("Keyboard Settings", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index a751c11c0..437ec52cd 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -373,5 +373,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Disconnected", ""), ("Other", ""), ("Confirm before closing multiple tabs", ""), + ("Keyboard Settings", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index ae98a5690..6856eca60 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -373,5 +373,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Disconnected", ""), ("Other", ""), ("Confirm before closing multiple tabs", ""), + ("Keyboard Settings", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 46428d74c..77f47964a 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -373,5 +373,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Disconnected", ""), ("Other", "他の"), ("Confirm before closing multiple tabs", "同時に複数のタブを閉じる前に確認する"), + ("Keyboard Settings", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 3c857424e..4d0ff5f3e 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -373,5 +373,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Disconnected", ""), ("Other", ""), ("Confirm before closing multiple tabs", ""), + ("Keyboard Settings", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index f83b8b04b..602413ba9 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -373,5 +373,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Disconnected", ""), ("Other", ""), ("Confirm before closing multiple tabs", ""), + ("Keyboard Settings", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 9130d6b9a..dc6cdb750 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -373,5 +373,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Disconnected", ""), ("Other", ""), ("Confirm before closing multiple tabs", ""), + ("Keyboard Settings", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 0fd796cb3..ff3d31b83 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -373,5 +373,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Disconnected", ""), ("Other", ""), ("Confirm before closing multiple tabs", ""), + ("Keyboard Settings", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index a20fdd557..0c0150e88 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -373,5 +373,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Disconnected", ""), ("Other", ""), ("Confirm before closing multiple tabs", ""), + ("Keyboard Settings", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index c86d5cc31..4192f8490 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -373,5 +373,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Disconnected", ""), ("Other", ""), ("Confirm before closing multiple tabs", ""), + ("Keyboard Settings", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 8d4105118..819eee109 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -373,5 +373,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Disconnected", ""), ("Other", ""), ("Confirm before closing multiple tabs", ""), + ("Keyboard Settings", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 02bfc95c0..c1d93b224 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -373,5 +373,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Disconnected", ""), ("Other", ""), ("Confirm before closing multiple tabs", ""), + ("Keyboard Settings", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index e4ee58e23..7761c1ff3 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -373,5 +373,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Disconnected", ""), ("Other", ""), ("Confirm before closing multiple tabs", ""), + ("Keyboard Settings", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 5b0dc48aa..ab175cf11 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -373,5 +373,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Disconnected", "會話已結束"), ("Other", "其他"), ("Confirm before closing multiple tabs", "關閉多個分頁前跟我確認"), + ("Keyboard Settings", "鍵盤設置"), ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 7d5037807..0399705b9 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -373,5 +373,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Disconnected", ""), ("Other", ""), ("Confirm before closing multiple tabs", ""), + ("Keyboard Settings", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 4e441abff..db09d03a3 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -373,5 +373,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Disconnected", ""), ("Other", ""), ("Confirm before closing multiple tabs", ""), + ("Keyboard Settings", ""), ].iter().cloned().collect(); } From 0522471b39ebd71a41953a93e8cf9eaa9a9924d3 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 17 Oct 2022 11:53:15 +0800 Subject: [PATCH 0697/2015] fix: cm window block on setSize --- flutter/lib/main.dart | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 835606eb1..38417f25f 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -194,13 +194,10 @@ void runPortForwardScreen(Map argument) async { } void runConnectionManagerScreen() async { + await initEnv(kAppTypeMain); // initialize window WindowOptions windowOptions = getHiddenTitleBarWindowOptions(size: kConnectionManagerWindowSize); - // ensure initial window size to be changed - await windowManager.setSize(kConnectionManagerWindowSize); - await Future.wait( - [windowManager.setAlignment(Alignment.topRight), initEnv(kAppTypeMain)]); runApp(GetMaterialApp( debugShowCheckedModeBanner: false, theme: MyTheme.lightTheme, @@ -215,10 +212,16 @@ void runConnectionManagerScreen() async { home: const DesktopServerPage(), builder: _keepScaleBuilder())); windowManager.waitUntilReadyToShow(windowOptions, () async { - windowManager.show(); - windowManager.focus(); - windowManager.setOpacity(1); - windowManager.setAlignment(Alignment.topRight); // ensure + await windowManager.show(); + // ensure initial window size to be changed + await windowManager.setSize(kConnectionManagerWindowSize); + await Future.wait([ + windowManager.setAlignment(Alignment.topRight), + windowManager.focus(), + windowManager.setOpacity(1) + ]); + // ensure + windowManager.setAlignment(Alignment.topRight); }); } From 688519320e63eb8ce3bd40fede2217644f7392b1 Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 17 Oct 2022 14:35:44 +0800 Subject: [PATCH 0698/2015] add access mode Signed-off-by: 21pages --- .../lib/desktop/pages/connection_page.dart | 1 + .../desktop/pages/desktop_setting_page.dart | 148 ++++++++++++++---- .../lib/desktop/widgets/tabbar_widget.dart | 6 +- 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 | 3 + src/lang/fr.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/ru.rs | 3 + src/lang/sk.rs | 3 + src/lang/template.rs | 3 + src/lang/tr.rs | 3 + src/lang/tw.rs | 3 + src/lang/ua.rs | 3 + src/lang/vn.rs | 3 + src/server/connection.rs | 32 ++-- 27 files changed, 218 insertions(+), 38 deletions(-) diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 2e7026071..318843699 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -260,6 +260,7 @@ class _ConnectionPageState extends State bool checked = await bind.mainCheckSuperUserPermission(); if (checked) { bind.mainSetOption(key: "stop-service", value: ""); + bind.mainSetOption(key: "access-mode", value: ""); } }, child: Text(translate("Start Service"), style: textStyle)) diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 0c4cc0aec..9787048fa 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -413,6 +413,13 @@ class _GeneralState extends State<_General> { } } +enum _AccessMode { + custom, + full, + view, + deny, +} + class _Safety extends StatefulWidget { const _Safety({Key? key}) : super(key: key); @@ -459,26 +466,113 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { Widget permissions(context) { bool enabled = !locked; - return _Card(title: 'Permissions', children: [ - _OptionCheckBox(context, 'Enable Keyboard/Mouse', 'enable-keyboard', - enabled: enabled), - _OptionCheckBox(context, 'Enable Clipboard', 'enable-clipboard', - enabled: enabled), - _OptionCheckBox(context, 'Enable File Transfer', 'enable-file-transfer', - enabled: enabled), - _OptionCheckBox(context, 'Enable Audio', 'enable-audio', - enabled: enabled), - _OptionCheckBox(context, 'Enable TCP Tunneling', 'enable-tunnel', - enabled: enabled), - _OptionCheckBox(context, 'Enable Remote Restart', 'enable-remote-restart', - enabled: enabled), - _OptionCheckBox( - context, 'Enable Recording Session', 'enable-record-session', - enabled: enabled), - _OptionCheckBox(context, 'Enable remote configuration modification', - 'allow-remote-config-modification', - enabled: enabled), - ]); + + return _futureBuilder(future: () async { + bool stopService = option2bool( + 'stop-service', await bind.mainGetOption(key: 'stop-service')); + final accessMode = await bind.mainGetOption(key: 'access-mode'); + return {'stopService': stopService, 'accessMode': accessMode}; + }(), hasData: (data) { + var map = data! as Map; + bool stopService = map['stopService'] as bool; + String accessMode = map['accessMode'] as String; + _AccessMode mode; + if (stopService) { + mode = _AccessMode.deny; + } else { + if (accessMode == 'full') { + mode = _AccessMode.full; + } else if (accessMode == 'view') { + mode = _AccessMode.view; + } else { + mode = _AccessMode.custom; + } + } + String initialKey; + bool? fakeValue; + switch (mode) { + case _AccessMode.custom: + initialKey = ''; + fakeValue = null; + break; + case _AccessMode.full: + initialKey = 'full'; + fakeValue = true; + break; + case _AccessMode.view: + initialKey = 'view'; + fakeValue = false; + break; + case _AccessMode.deny: + initialKey = 'deny'; + fakeValue = false; + break; + } + + return _Card(title: 'Permissions', children: [ + _ComboBox( + keys: [ + '', + 'full', + 'view', + 'deny' + ], + values: [ + translate('Custom'), + translate('Full Access'), + translate('Screen Share'), + translate('Deny remote access'), + ], + initialKey: initialKey, + onChanged: (mode) async { + String modeValue; + bool stopService; + if (mode == 'deny') { + modeValue = ''; + stopService = true; + } else { + modeValue = mode; + stopService = false; + } + await bind.mainSetOption(key: 'access-mode', value: modeValue); + await bind.mainSetOption( + key: 'stop-service', + value: bool2option('stop-service', stopService)); + setState(() {}); + }).marginOnly(left: _kContentHMargin), + Offstage( + offstage: mode == _AccessMode.deny, + child: Column( + children: [ + _OptionCheckBox( + context, 'Enable Keyboard/Mouse', 'enable-keyboard', + enabled: enabled, fakeValue: fakeValue), + _OptionCheckBox(context, 'Enable Clipboard', 'enable-clipboard', + enabled: enabled, fakeValue: fakeValue), + _OptionCheckBox( + context, 'Enable File Transfer', 'enable-file-transfer', + enabled: enabled, fakeValue: fakeValue), + _OptionCheckBox(context, 'Enable Audio', 'enable-audio', + enabled: enabled, fakeValue: fakeValue), + _OptionCheckBox(context, 'Enable TCP Tunneling', 'enable-tunnel', + enabled: enabled, fakeValue: fakeValue), + _OptionCheckBox( + context, 'Enable Remote Restart', 'enable-remote-restart', + enabled: enabled, fakeValue: fakeValue), + _OptionCheckBox( + context, 'Enable Recording Session', 'enable-record-session', + enabled: enabled, fakeValue: fakeValue), + _OptionCheckBox( + context, + 'Enable remote configuration modification', + 'allow-remote-config-modification', + enabled: enabled, + fakeValue: fakeValue), + ], + ), + ) + ]); + }); } Widget password(BuildContext context) { @@ -566,12 +660,6 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { Widget more(BuildContext context) { bool enabled = !locked; return _Card(title: 'Security', children: [ - _OptionCheckBox(context, 'Deny remote access', 'stop-service', - checkedIcon: const Icon( - Icons.warning_amber_rounded, - color: kColorWarn, - ), - enabled: enabled), Offstage( offstage: !Platform.isWindows, child: _OptionCheckBox(context, 'Enable RDP', 'enable-rdp', @@ -941,7 +1029,8 @@ Widget _OptionCheckBox(BuildContext context, String label, String key, {Function()? update, bool reverse = false, bool enabled = true, - Icon? checkedIcon}) { + Icon? checkedIcon, + bool? fakeValue}) { return _futureBuilder( future: bind.mainGetOption(key: key), hasData: (data) { @@ -958,6 +1047,11 @@ Widget _OptionCheckBox(BuildContext context, String label, String key, } } + if (fakeValue != null) { + ref.value = fakeValue; + enabled = false; + } + return GestureDetector( child: Obx( () => Row( diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 3eadf75fd..e146a4bee 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -249,10 +249,12 @@ class DesktopTab extends StatelessWidget { var block = false.obs; return Obx(() => MouseRegion( onEnter: (_) async { - if (!option2bool( + var access_mode = await bind.mainGetOption(key: 'access-mode'); + var option = option2bool( 'allow-remote-config-modification', await bind.mainGetOption( - key: 'allow-remote-config-modification'))) { + key: 'allow-remote-config-modification')); + if (access_mode == 'view' || (access_mode.isEmpty && !option)) { var time0 = DateTime.now().millisecondsSinceEpoch; await bind.mainCheckMouseTime(); Timer(const Duration(milliseconds: 120), () async { diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 47b5f0bcb..c88b0182c 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -374,5 +374,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", "其他"), ("Confirm before closing multiple tabs", "关闭多个标签页时向您确认"), ("Keyboard Settings", "键盘设置"), + ("Custom", "自定义"), + ("Full Access", "完全访问"), + ("Screen Share", "仅共享屏幕"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 2515f2231..f616d518b 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -374,5 +374,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", ""), ("Confirm before closing multiple tabs", ""), ("Keyboard Settings", ""), + ("Custom", ""), + ("Full Access", ""), + ("Screen Share", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index df1a06f6d..3fe49d051 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -374,5 +374,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", ""), ("Confirm before closing multiple tabs", ""), ("Keyboard Settings", ""), + ("Custom", ""), + ("Full Access", ""), + ("Screen Share", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 1b9973957..4942b3171 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -374,5 +374,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", ""), ("Confirm before closing multiple tabs", ""), ("Keyboard Settings", ""), + ("Custom", ""), + ("Full Access", ""), + ("Screen Share", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 3c6b3b771..9bfea5440 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -374,5 +374,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", ""), ("Confirm before closing multiple tabs", ""), ("Keyboard Settings", ""), + ("Custom", ""), + ("Full Access", ""), + ("Screen Share", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 3826650af..3b6419572 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -374,5 +374,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", ""), ("Confirm before closing multiple tabs", ""), ("Keyboard Settings", ""), + ("Custom", ""), + ("Full Access", ""), + ("Screen Share", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 4b76b8941..b7dbd5636 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -374,5 +374,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", ""), ("Confirm before closing multiple tabs", ""), ("Keyboard Settings", ""), + ("Custom", ""), + ("Full Access", ""), + ("Screen Share", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 98e15397c..aa5e34d09 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -374,5 +374,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", ""), ("Confirm before closing multiple tabs", ""), ("Keyboard Settings", ""), + ("Custom", ""), + ("Full Access", ""), + ("Screen Share", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 437ec52cd..1ab7fd134 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -374,5 +374,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", ""), ("Confirm before closing multiple tabs", ""), ("Keyboard Settings", ""), + ("Custom", ""), + ("Full Access", ""), + ("Screen Share", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 6856eca60..65c16567c 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -374,5 +374,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", ""), ("Confirm before closing multiple tabs", ""), ("Keyboard Settings", ""), + ("Custom", ""), + ("Full Access", ""), + ("Screen Share", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 77f47964a..75a9c0c9b 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -374,5 +374,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", "他の"), ("Confirm before closing multiple tabs", "同時に複数のタブを閉じる前に確認する"), ("Keyboard Settings", ""), + ("Custom", ""), + ("Full Access", ""), + ("Screen Share", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 4d0ff5f3e..eccb4d836 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -374,5 +374,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", ""), ("Confirm before closing multiple tabs", ""), ("Keyboard Settings", ""), + ("Custom", ""), + ("Full Access", ""), + ("Screen Share", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 602413ba9..9a1e23c23 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -374,5 +374,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", ""), ("Confirm before closing multiple tabs", ""), ("Keyboard Settings", ""), + ("Custom", ""), + ("Full Access", ""), + ("Screen Share", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index dc6cdb750..e96061729 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -374,5 +374,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", ""), ("Confirm before closing multiple tabs", ""), ("Keyboard Settings", ""), + ("Custom", ""), + ("Full Access", ""), + ("Screen Share", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index ff3d31b83..e17967998 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -374,5 +374,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", ""), ("Confirm before closing multiple tabs", ""), ("Keyboard Settings", ""), + ("Custom", ""), + ("Full Access", ""), + ("Screen Share", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 0c0150e88..c2965405d 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -374,5 +374,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", ""), ("Confirm before closing multiple tabs", ""), ("Keyboard Settings", ""), + ("Custom", ""), + ("Full Access", ""), + ("Screen Share", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 4192f8490..95c59dede 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -374,5 +374,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", ""), ("Confirm before closing multiple tabs", ""), ("Keyboard Settings", ""), + ("Custom", ""), + ("Full Access", ""), + ("Screen Share", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 819eee109..5064e1069 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -374,5 +374,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", ""), ("Confirm before closing multiple tabs", ""), ("Keyboard Settings", ""), + ("Custom", ""), + ("Full Access", ""), + ("Screen Share", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index c1d93b224..e08a2ba76 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -374,5 +374,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", ""), ("Confirm before closing multiple tabs", ""), ("Keyboard Settings", ""), + ("Custom", ""), + ("Full Access", ""), + ("Screen Share", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 7761c1ff3..117a5683b 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -374,5 +374,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", ""), ("Confirm before closing multiple tabs", ""), ("Keyboard Settings", ""), + ("Custom", ""), + ("Full Access", ""), + ("Screen Share", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index ab175cf11..d0bce5d77 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -374,5 +374,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", "其他"), ("Confirm before closing multiple tabs", "關閉多個分頁前跟我確認"), ("Keyboard Settings", "鍵盤設置"), + ("Custom", "自定義"), + ("Full Access", "完全訪問"), + ("Screen Share", "僅共享屏幕"), ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 0399705b9..f17e17a93 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -374,5 +374,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", ""), ("Confirm before closing multiple tabs", ""), ("Keyboard Settings", ""), + ("Custom", ""), + ("Full Access", ""), + ("Screen Share", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index db09d03a3..30e32a82c 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -374,5 +374,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", ""), ("Confirm before closing multiple tabs", ""), ("Keyboard Settings", ""), + ("Custom", ""), + ("Full Access", ""), + ("Screen Share", ""), ].iter().cloned().collect(); } diff --git a/src/server/connection.rs b/src/server/connection.rs index 14c906bcf..d111757bf 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -167,12 +167,12 @@ impl Connection { port_forward_address: "".to_owned(), tx_to_cm, authorized: false, - keyboard: Config::get_option("enable-keyboard").is_empty(), - clipboard: Config::get_option("enable-clipboard").is_empty(), - audio: Config::get_option("enable-audio").is_empty(), - file: Config::get_option("enable-file-transfer").is_empty(), - restart: Config::get_option("enable-remote-restart").is_empty(), - recording: Config::get_option("enable-record-session").is_empty(), + keyboard: Connection::permission("enable-keyboard"), + clipboard: Connection::permission("enable-clipboard"), + audio: Connection::permission("enable-audio"), + file: Connection::permission("enable-file-transfer"), + restart: Connection::permission("enable-remote-restart"), + recording: Connection::permission("enable-record-session"), last_test_delay: 0, lock_after_session_end: false, show_remote_cursor: false, @@ -922,6 +922,20 @@ impl Connection { false } + pub fn permission(enable_prefix_option: &str) -> bool { + #[cfg(feature = "flutter")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + let access_mode = Config::get_option("access-mode"); + if access_mode == "full" { + return true; + } else if access_mode == "view" { + return false; + } + } + return Config::get_option(enable_prefix_option).is_empty(); + } + async fn on_message(&mut self, msg: Message) -> bool { if let Some(message::Union::LoginRequest(lr)) = msg.union { self.lr = lr.clone(); @@ -950,7 +964,7 @@ impl Connection { } match lr.union { Some(login_request::Union::FileTransfer(ft)) => { - if !Config::get_option("enable-file-transfer").is_empty() { + if !Connection::permission("enable-file-transfer") { self.send_login_error("No permission of file transfer") .await; sleep(1.).await; @@ -965,8 +979,8 @@ impl Connection { pf.port = 3389; is_rdp = true; } - if is_rdp && !Config::get_option("enable-rdp").is_empty() - || !is_rdp && !Config::get_option("enable-tunnel").is_empty() + if is_rdp && !Connection::permission("enable-rdp") + || !is_rdp && !Connection::permission("enable-tunnel") { if is_rdp { self.send_login_error("No permission of RDP").await; From 76581d46f2c90562e386f580b5a7512ce2df631e Mon Sep 17 00:00:00 2001 From: csf Date: Mon, 17 Oct 2022 19:37:00 +0900 Subject: [PATCH 0699/2015] fix can't update isMaximized IconButton via double click title logo --- .../lib/desktop/widgets/tabbar_widget.dart | 76 +++++++++++-------- 1 file changed, 43 insertions(+), 33 deletions(-) diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 9e191ac28..98eea7595 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -191,6 +191,8 @@ class DesktopTab extends StatelessWidget { final DesktopTabController controller; Rx get state => controller.state; + final isMaximized = false.obs; + late final DesktopTabType tabType; late final bool isMainWindow; @@ -297,8 +299,10 @@ class DesktopTab extends StatelessWidget { width: 78, )), GestureDetector( - onDoubleTap: () => - showMaximize ? toggleMaximize(isMainWindow) : null, + onDoubleTap: showMaximize + ? () => toggleMaximize(isMainWindow) + .then((value) => isMaximized.value = value) + : null, onPanStart: (_) => startDragging(isMainWindow), child: Row(children: [ Offstage( @@ -331,6 +335,7 @@ class DesktopTab extends StatelessWidget { tabType: tabType, state: state, tail: tail, + isMaximized: isMaximized, showMinimize: showMinimize, showMaximize: showMaximize, showClose: showClose, @@ -345,6 +350,7 @@ class WindowActionPanel extends StatefulWidget { final bool isMainWindow; final DesktopTabType tabType; final Rx state; + final RxBool isMaximized; final bool showMinimize; final bool showMaximize; @@ -357,6 +363,7 @@ class WindowActionPanel extends StatefulWidget { required this.isMainWindow, required this.tabType, required this.state, + required this.isMaximized, this.tail, this.showMinimize = true, this.showMaximize = true, @@ -372,30 +379,31 @@ class WindowActionPanel extends StatefulWidget { class WindowActionPanelState extends State with MultiWindowListener, WindowListener { - bool isMaximized = false; - @override void initState() { super.initState(); DesktopMultiWindow.addListener(this); windowManager.addListener(this); - if (widget.isMainWindow) { - windowManager.isMaximized().then((maximized) { - if (isMaximized != maximized) { - WidgetsBinding.instance.addPostFrameCallback( - (_) => setState(() => isMaximized = maximized)); - } - }); - } else { - final wc = WindowController.fromWindowId(windowId!); - wc.isMaximized().then((maximized) { - if (isMaximized != maximized) { - WidgetsBinding.instance.addPostFrameCallback( - (_) => setState(() => isMaximized = maximized)); - } - }); - } + Future.delayed(Duration(milliseconds: 500), () { + if (widget.isMainWindow) { + windowManager.isMaximized().then((maximized) { + if (widget.isMaximized.value != maximized) { + WidgetsBinding.instance.addPostFrameCallback( + (_) => setState(() => widget.isMaximized.value = maximized)); + } + }); + } else { + final wc = WindowController.fromWindowId(windowId!); + wc.isMaximized().then((maximized) { + debugPrint("isMaximized $maximized"); + if (widget.isMaximized.value != maximized) { + WidgetsBinding.instance.addPostFrameCallback( + (_) => setState(() => widget.isMaximized.value = maximized)); + } + }); + } + }); } @override @@ -408,8 +416,8 @@ class WindowActionPanelState extends State @override void onWindowMaximize() { // catch maximize from system - if (!isMaximized) { - setState(() => isMaximized = true); + if (!widget.isMaximized.value) { + widget.isMaximized.value = true; } super.onWindowMaximize(); } @@ -417,8 +425,8 @@ class WindowActionPanelState extends State @override void onWindowUnmaximize() { // catch unmaximize from system - if (isMaximized) { - setState(() => isMaximized = false); + if (widget.isMaximized.value) { + widget.isMaximized.value = false; } super.onWindowUnmaximize(); } @@ -450,12 +458,14 @@ class WindowActionPanelState extends State )), Offstage( offstage: !widget.showMaximize, - child: ActionIcon( - message: isMaximized ? "Restore" : "Maximize", - icon: isMaximized ? IconFont.restore : IconFont.max, - onTap: _toggleMaximize, - isClose: false, - )), + child: Obx(() => ActionIcon( + message: widget.isMaximized.value ? "Restore" : "Maximize", + icon: widget.isMaximized.value + ? IconFont.restore + : IconFont.max, + onTap: _toggleMaximize, + isClose: false, + ))), Offstage( offstage: !widget.showClose, child: ActionIcon( @@ -482,9 +492,9 @@ class WindowActionPanelState extends State void _toggleMaximize() { toggleMaximize(widget.isMainWindow).then((maximize) { - if (isMaximized != maximize) { - // setState for sub window, wc.unmaximize/maximize() will not invoke onWindowMaximize/Unmaximize - setState(() => isMaximized = !isMaximized); + if (widget.isMaximized.value != maximize) { + // update state for sub window, wc.unmaximize/maximize() will not invoke onWindowMaximize/Unmaximize + widget.isMaximized.value = maximize; } }); } From 7e7214bd07a2516b927e1fd161f9e5f46771e023 Mon Sep 17 00:00:00 2001 From: csf Date: Mon, 17 Oct 2022 22:26:18 +0900 Subject: [PATCH 0700/2015] desktop file transfer ctrl + click multi selection --- flutter/lib/consts.dart | 2 + .../lib/desktop/pages/file_manager_page.dart | 320 ++++++++++-------- 2 files changed, 182 insertions(+), 140 deletions(-) diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index f43c20cc6..726ae24be 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -31,6 +31,8 @@ const int kMobileMaxDisplayHeight = 1280; const int kDesktopMaxDisplayWidth = 1920; const int kDesktopMaxDisplayHeight = 1080; +const int kDesktopDoubleClickTimeMilli = 200; + 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 f6fae1e31..6c2e20e78 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -1,8 +1,10 @@ +import 'dart:async'; import 'dart:io'; import 'dart:math'; import 'package:desktop_drop/desktop_drop.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_breadcrumb/flutter_breadcrumb.dart'; import 'package:flutter_hbb/mobile/pages/file_manager_page.dart'; import 'package:flutter_hbb/models/file_model.dart'; @@ -44,15 +46,17 @@ class _FileManagerPageState extends State final _locationStatusLocal = LocationStatus.bread.obs; final _locationStatusRemote = LocationStatus.bread.obs; - final FocusNode _locationNodeLocal = - FocusNode(debugLabel: "locationNodeLocal"); - final FocusNode _locationNodeRemote = - FocusNode(debugLabel: "locationNodeRemote"); + final _locationNodeLocal = FocusNode(debugLabel: "locationNodeLocal"); + final _locationNodeRemote = FocusNode(debugLabel: "locationNodeRemote"); final _searchTextLocal = "".obs; final _searchTextRemote = "".obs; final _breadCrumbScrollerLocal = ScrollController(); final _breadCrumbScrollerRemote = ScrollController(); + /// [_lastClickTime], [_lastClickEntry] help to handle double click + int _lastClickTime = DateTime.now().millisecondsSinceEpoch; + Entry? _lastClickEntry; + final _dropMaskVisible = false.obs; // TODO impl drop mask ScrollController getBreadCrumbScrollController(bool isLocal) { @@ -171,22 +175,6 @@ class _FileManagerPageState extends State } Widget body({bool isLocal = false}) { - 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; return Container( decoration: BoxDecoration(border: Border.all(color: Colors.black26)), margin: const EdgeInsets.all(16.0), @@ -208,126 +196,7 @@ class _FileManagerPageState extends State Expanded( child: SingleChildScrollView( controller: ScrollController(), - child: ObxValue( - (searchText) { - final filteredEntries = searchText.isNotEmpty - ? entries.where((element) { - return element.name.contains(searchText.value); - }).toList(growable: false) - : entries; - return DataTable( - key: ValueKey(isLocal ? 0 : 1), - showCheckboxColumn: true, - dataRowHeight: 25, - 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 sizeStr = entry.isFile - ? readableFileSize(entry.size.toDouble()) - : ""; - return DataRow( - key: ValueKey(entry.name), - onSelectChanged: (s) { - if (s != null) { - if (s) { - getSelectedItem(isLocal) - .add(isLocal, entry); - } else { - getSelectedItem(isLocal).remove(entry); - } - setState(() {}); - } - }, - selected: - getSelectedItem(isLocal).contains(entry), - cells: [ - DataCell( - Container( - width: 180, - child: Tooltip( - message: entry.name, - child: Row(children: [ - 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)) - ]), - )), onTap: () { - if (entry.isDirectory) { - openDirectory(entry.path, isLocal: isLocal); - if (isLocal) { - _localSelectedItems.clear(); - } else { - _remoteSelectedItems.clear(); - } - } else { - // Perform file-related tasks. - final selectedItems = - getSelectedItem(isLocal); - if (selectedItems.contains(entry)) { - selectedItems.remove(entry); - } else { - selectedItems.add(isLocal, entry); - } - setState(() {}); - } - }), - DataCell(FittedBox( - child: Text( - "${entry.lastModified().toString().replaceAll(".000", "")} ", - style: TextStyle( - fontSize: 12, color: MyTheme.darkGray), - ))), - DataCell(Text( - sizeStr, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 10, color: MyTheme.darkGray), - )), - ]); - }).toList(growable: false), - ); - }, - isLocal ? _searchTextLocal : _searchTextRemote, - ), + child: _buildDataTable(context, isLocal), ), ) ], @@ -337,6 +206,176 @@ class _FileManagerPageState extends State ); } + Widget _buildDataTable(BuildContext context, bool isLocal) { + 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; + + return ObxValue( + (searchText) { + final filteredEntries = searchText.isNotEmpty + ? entries.where((element) { + return element.name.contains(searchText.value); + }).toList(growable: false) + : entries; + return DataTable( + key: ValueKey(isLocal ? 0 : 1), + showCheckboxColumn: false, + dataRowHeight: 25, + 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 sizeStr = + entry.isFile ? readableFileSize(entry.size.toDouble()) : ""; + final lastModifiedStr = + "${entry.lastModified().toString().replaceAll(".000", "")} "; + return DataRow( + key: ValueKey(entry.name), + onSelectChanged: (s) { + final isCtrlDown = RawKeyboard.instance.keysPressed + .contains(LogicalKeyboardKey.controlLeft); + final items = getSelectedItem(isLocal); + if (isCtrlDown) { + if (s != null) { + if (s) { + items.add(isLocal, entry); + } else { + items.remove(entry); + } + } + } else { + items.clear(); + items.add(isLocal, entry); + } + setState(() {}); + }, + selected: getSelectedItem(isLocal).contains(entry), + cells: [ + DataCell( + Container( + width: 200, + child: Tooltip( + waitDuration: Duration(milliseconds: 500), + message: entry.name, + child: Row(children: [ + 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)) + ]), + )), + onTap: () { + final items = getSelectedItem(isLocal); + + // handle double click + if (_checkDoubleClick(entry)) { + openDirectory(entry.path, isLocal: isLocal); + items.clear(); + return; + } + + final isCtrlDown = RawKeyboard.instance.keysPressed + .contains(LogicalKeyboardKey.controlLeft); + if (isCtrlDown) { + if (items.contains(entry)) { + items.remove(entry); + } else { + items.add(isLocal, entry); + } + } else { + items.clear(); + items.add(isLocal, entry); + } + setState(() {}); + }, + ), + 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), + ); + }, + isLocal ? _searchTextLocal : _searchTextRemote, + ); + } + + bool _checkDoubleClick(Entry entry) { + final current = DateTime.now().millisecondsSinceEpoch; + final elapsed = current - _lastClickTime; + _lastClickTime = current; + if (_lastClickEntry == entry) { + if (elapsed < kDesktopDoubleClickTimeMilli) { + return true; + } + } else { + _lastClickEntry = entry; + } + return false; + } + /// transfer status list /// watch transfer status Widget statusList() { @@ -369,6 +408,7 @@ class _FileManagerPageState extends State crossAxisAlignment: CrossAxisAlignment.start, children: [ Tooltip( + waitDuration: Duration(milliseconds: 500), message: item.jobName, child: Text( item.jobName, From 75590af0d7be6165ba531ea26cc8f2496e04ef0d Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 11 Oct 2022 20:34:58 -0700 Subject: [PATCH 0701/2015] build: trivial changes build.py Signed-off-by: fufesou --- build.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/build.py b/build.py index 3e9fe813d..7780ecfd0 100755 --- a/build.py +++ b/build.py @@ -144,7 +144,9 @@ Description: A remote control software. file.close() def build_flutter_deb(version): - os.system('cargo build --features flutter --lib --release') + os.system('cargo build --features default,flutter --lib --release') + # workaround ffigen + os.system('sed -i "s/ffi.NativeFunction Date: Tue, 11 Oct 2022 20:35:30 -0700 Subject: [PATCH 0702/2015] flutter: msgbox selectable Signed-off-by: fufesou --- flutter/lib/common.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 6e32ad09c..686ef3144 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -656,7 +656,8 @@ void msgBox( } dialogManager.show((setState, close) => CustomAlertDialog( title: _msgBoxTitle(title), - content: Text(translate(text), style: const TextStyle(fontSize: 15)), + content: SelectableText(translate(text), + style: const TextStyle(fontSize: 15)), actions: buttons, onSubmit: hasOk ? submit : null, onCancel: hasCancel == true ? cancel : null, From 2da5401fd48664d40a984d7a9839849877e169db Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 11 Oct 2022 20:36:19 -0700 Subject: [PATCH 0703/2015] add global init and update wayland error map Signed-off-by: fufesou --- libs/hbb_common/src/platform/linux.rs | 31 +++++++++++++- libs/scrap/src/common/mod.rs | 1 + libs/scrap/src/common/wayland.rs | 16 ++++++- src/common.rs | 35 ++++++++++++++- src/main.rs | 12 ++++++ src/server.rs | 2 +- src/server/connection.rs | 2 +- src/server/wayland.rs | 62 +++++++++++++++++++++++++-- src/ui_interface.rs | 2 +- 9 files changed, 151 insertions(+), 12 deletions(-) diff --git a/libs/hbb_common/src/platform/linux.rs b/libs/hbb_common/src/platform/linux.rs index 08e091337..0473a3bbc 100644 --- a/libs/hbb_common/src/platform/linux.rs +++ b/libs/hbb_common/src/platform/linux.rs @@ -1,5 +1,31 @@ use crate::ResultType; +lazy_static::lazy_static! { + pub static ref DISTRO: Disto = Disto::new(); +} + +pub struct Disto { + pub name: String, + pub version_id: String, +} + +impl Disto { + fn new() -> Self { + let name = run_cmds("awk -F'=' '/^NAME=/ {print $2}' /etc/os-release".to_owned()) + .unwrap_or_default() + .trim() + .trim_matches('"') + .to_string(); + let version_id = + run_cmds("awk -F'=' '/^VERSION_ID=/ {print $2}' /etc/os-release".to_owned()) + .unwrap_or_default() + .trim() + .trim_matches('"') + .to_string(); + Self { name, version_id } + } +} + pub fn get_display_server() -> String { let session = get_value_of_seat0(0); get_display_server_of_session(&session) @@ -81,8 +107,9 @@ pub fn get_value_of_seat0(i: usize) -> String { } // loginctl has not given the expected output. try something else. - if let Ok(sid) = std::env::var("XDG_SESSION_ID") { // could also execute "cat /proc/self/sessionid" - return sid.to_owned(); + if let Ok(sid) = std::env::var("XDG_SESSION_ID") { + // could also execute "cat /proc/self/sessionid" + return sid.to_owned(); } return "".to_owned(); diff --git a/libs/scrap/src/common/mod.rs b/libs/scrap/src/common/mod.rs index fe817c00a..468efb88e 100644 --- a/libs/scrap/src/common/mod.rs +++ b/libs/scrap/src/common/mod.rs @@ -12,6 +12,7 @@ cfg_if! { mod x11; pub use self::linux::*; pub use self::x11::Frame; + pub use self::wayland::set_map_err; } else { mod x11; pub use self::x11::*; diff --git a/libs/scrap/src/common/wayland.rs b/libs/scrap/src/common/wayland.rs index e33cbe745..6ad2d84cb 100644 --- a/libs/scrap/src/common/wayland.rs +++ b/libs/scrap/src/common/wayland.rs @@ -1,11 +1,23 @@ use crate::common::{x11::Frame, TraitCapturer}; use crate::wayland::{capturable::*, *}; -use std::{io, time::Duration}; +use std::{io, sync::RwLock, time::Duration}; pub struct Capturer(Display, Box, bool, Vec); +lazy_static::lazy_static! { + static ref MAP_ERR: RwLock io::Error>> = Default::default(); +} + +pub fn set_map_err(f: fn(err: String) -> io::Error) { + *MAP_ERR.write().unwrap() = Some(f); +} + fn map_err(err: E) -> io::Error { - io::Error::new(io::ErrorKind::Other, err.to_string()) + if let Some(f) = *MAP_ERR.read().unwrap() { + f(err.to_string()) + } else { + io::Error::new(io::ErrorKind::Other, err.to_string()) + } } impl Capturer { diff --git a/src/common.rs b/src/common.rs index 129e948cf..9beed2e90 100644 --- a/src/common.rs +++ b/src/common.rs @@ -1,4 +1,7 @@ -use std::sync::{Arc, Mutex}; +use std::{ + collections::HashMap, + sync::{Arc, Mutex}, +}; #[cfg(not(any(target_os = "android", target_os = "ios")))] pub use arboard::Clipboard as ClipboardContext; @@ -31,6 +34,36 @@ lazy_static::lazy_static! { pub static ref DEVICE_NAME: Arc> = Default::default(); } +lazy_static::lazy_static! { + static ref GLOBAL_INIT_FUNCS: Mutex bool>> = Default::default(); + static ref GLOBAL_CLEAN_FUNCS: Mutex> = Default::default(); +} + +pub fn reg_global_init(key: String, f: fn() -> bool) { + GLOBAL_INIT_FUNCS.lock().unwrap().insert(key, f); +} + +pub fn reg_global_clean(key: String, f: fn()) { + GLOBAL_CLEAN_FUNCS.lock().unwrap().insert(key, f); +} + +pub fn global_init() -> bool { + for (k, f) in GLOBAL_INIT_FUNCS.lock().unwrap().iter() { + println!("Init {}", k); + if !f() { + return false; + } + } + true +} + +pub fn global_clean() { + for (k, f) in GLOBAL_CLEAN_FUNCS.lock().unwrap().iter() { + println!("Clean {}", k); + f(); + } +} + #[inline] pub fn valid_for_numlock(evt: &KeyEvent) -> bool { if let Some(key_event::Union::ControlKey(ck)) = evt.union { diff --git a/src/main.rs b/src/main.rs index 23559ed8a..ac8fd5219 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,21 +6,32 @@ use librustdesk::*; #[cfg(any(target_os = "android", target_os = "ios"))] fn main() { + if !common::global_init() { + return; + } common::test_rendezvous_server(); common::test_nat_type(); #[cfg(target_os = "android")] crate::common::check_software_update(); + common::global_clean(); } #[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] fn main() { + if !common::global_init() { + return; + } if let Some(args) = crate::core_main::core_main().as_mut() { ui::start(args); } + common::global_clean(); } #[cfg(feature = "cli")] fn main() { + if !common::global_init() { + return; + } use hbb_common::log; use clap::App; let args = format!( @@ -64,4 +75,5 @@ fn main() { let token = LocalConfig::get_option("access_token"); cli::start_one_port_forward(options[0].clone(), port, remote_host, remote_port, key, token); } + common::global_clean(); } \ No newline at end of file diff --git a/src/server.rs b/src/server.rs index cb3fd7c9d..58aab8fd1 100644 --- a/src/server.rs +++ b/src/server.rs @@ -27,7 +27,7 @@ cfg_if::cfg_if! { if #[cfg(not(any(target_os = "android", target_os = "ios")))] { mod clipboard_service; #[cfg(target_os = "linux")] -mod wayland; +pub(crate) mod wayland; #[cfg(target_os = "linux")] pub mod uinput; #[cfg(target_os = "linux")] diff --git a/src/server/connection.rs b/src/server/connection.rs index d111757bf..8e3f80bb9 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -745,7 +745,7 @@ impl Connection { try_activate_screen(); match super::video_service::get_displays().await { Err(err) => { - res.set_error(format!("X11 error: {}", err)); + res.set_error(format!("Error: {}", err)); } Ok((current, displays)) => { pi.displays = displays.into(); diff --git a/src/server/wayland.rs b/src/server/wayland.rs index 1ac2c18be..c12d1e8da 100644 --- a/src/server/wayland.rs +++ b/src/server/wayland.rs @@ -1,10 +1,53 @@ use super::*; -use hbb_common::allow_err; -use scrap::{Capturer, Display, Frame, TraitCapturer}; -use std::io::Result; +use hbb_common::{allow_err, platform::linux::DISTRO}; +use scrap::{set_map_err, Capturer, Display, Frame, TraitCapturer}; +use std::io; + +pub const SCRAP_UBUNTU_HIGHER_REQUIRED: &str = "Wayland requires Ubuntu 21.04 or higher version."; +pub const SCRAP_OTHER_VERSION_OR_X11_REQUIRED: &str = + "Wayland requires higher version of linux distro. Please try X11 desktop or change your OS."; +pub const SCRAP_X11_REQUIRED: &str = "X11 is required"; +pub const SCRAP_X11_REF_URL: &str = "https://rustdesk.com/docs/en/manual/linux/#x11-required"; lazy_static::lazy_static! { static ref CAP_DISPLAY_INFO: RwLock = RwLock::new(0); + static ref LOG_SCRAP_COUNT: Mutex = Mutex::new(0); + static ref GLOBAL_INIT_REG_HELPER: u8 = { + set_map_err(map_err_scrap); + 0u8 + }; +} + +pub fn map_err_scrap(err: String) -> io::Error { + if DISTRO.name.to_uppercase() == "Ubuntu".to_uppercase() { + if DISTRO.version_id < "21".to_owned() { + io::Error::new(io::ErrorKind::Other, SCRAP_UBUNTU_HIGHER_REQUIRED) + } else { + try_log(&err); + io::Error::new(io::ErrorKind::Other, err) + } + } else { + try_log(&err); + if err.contains("org.freedesktop.portal") + || err.contains("pipewire") + || err.contains("dbus") + { + io::Error::new(io::ErrorKind::Other, SCRAP_OTHER_VERSION_OR_X11_REQUIRED) + } else { + io::Error::new(io::ErrorKind::Other, SCRAP_X11_REQUIRED) + } + } +} + +fn try_log(err: &String) { + let mut lock_count = LOG_SCRAP_COUNT.lock().unwrap(); + if *lock_count >= 1000000 { + return; + } + if *lock_count % 10000 == 0 { + log::error!("Failed scrap {}", err); + } + *lock_count += 1; } struct CapturerPtr(*mut Capturer); @@ -16,7 +59,7 @@ impl Clone for CapturerPtr { } impl TraitCapturer for CapturerPtr { - fn frame<'a>(&'a mut self, timeout: Duration) -> Result> { + fn frame<'a>(&'a mut self, timeout: Duration) -> io::Result> { unsafe { (*self.0).frame(timeout) } } @@ -187,3 +230,14 @@ pub(super) fn get_capturer() -> ResultType { bail!("Failed to get capturer display info"); } } + +pub fn common_get_error() -> String { + if DISTRO.name.to_uppercase() == "Ubuntu".to_uppercase() { + if DISTRO.version_id < "21".to_owned() { + return "".to_owned(); + } + } else { + // to-do: check other distros + } + return "".to_owned(); +} diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 9952a5b35..9ef512fd7 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -547,7 +547,7 @@ pub fn get_error() -> String { { let dtype = crate::platform::linux::get_display_server(); if "wayland" == dtype { - return "".to_owned(); + return crate::server::wayland::common_get_error(); } if dtype != "x11" { return format!( From 3c9ac9e4d77e374e282b9d7aacfb6f53ee5ef6d8 Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 12 Oct 2022 20:05:30 -0700 Subject: [PATCH 0704/2015] wayland: fix enigo init Signed-off-by: fufesou --- libs/enigo/src/linux/nix_impl.rs | 73 ++++++++++++++++++-------------- 1 file changed, 41 insertions(+), 32 deletions(-) diff --git a/libs/enigo/src/linux/nix_impl.rs b/libs/enigo/src/linux/nix_impl.rs index 5fb67c5ee..e09f826dc 100644 --- a/libs/enigo/src/linux/nix_impl.rs +++ b/libs/enigo/src/linux/nix_impl.rs @@ -8,7 +8,7 @@ use tfc::{traits::*, Context as TFC_Context, Key as TFC_Key}; pub struct Enigo { xdo: EnigoXdo, is_x11: bool, - tfc: TFC_Context, + tfc: Option, uinput_keyboard: Option>, uinput_mouse: Option>, } @@ -35,46 +35,55 @@ impl Enigo { } fn tfc_key_down_or_up(&mut self, key: Key, down: bool, up: bool) -> bool { - if let Key::Layout(chr) = key { - if down { - if let Err(_) = self.tfc.unicode_char_down(chr) { - return false; + match &mut self.tfc { + None => false, + Some(tfc) => { + if let Key::Layout(chr) = key { + if down { + if let Err(_) = tfc.unicode_char_down(chr) { + return false; + } + } + if up { + if let Err(_) = tfc.unicode_char_up(chr) { + return false; + } + } + return true; } + let key = match convert_to_tfc_key(key) { + Some(key) => key, + None => { + return false; + } + }; + + if down { + if let Err(_) = tfc.key_down(key) { + return false; + } + }; + if up { + if let Err(_) = tfc.key_up(key) { + return false; + } + }; + return true; } - if up { - if let Err(_) = self.tfc.unicode_char_up(chr) { - return false; - } - } - return true; } - - let key = match convert_to_tfc_key(key) { - Some(key) => key, - None => { - return false; - } - }; - - if down { - if let Err(_) = self.tfc.key_down(key) { - return false; - } - }; - if up { - if let Err(_) = self.tfc.key_up(key) { - return false; - } - }; - return true; } } impl Default for Enigo { fn default() -> Self { + let is_x11 = "x11" == hbb_common::platform::linux::get_display_server(); Self { - is_x11: "x11" == hbb_common::platform::linux::get_display_server(), - tfc: TFC_Context::new().expect("kbd context error"), + is_x11, + tfc: if is_x11 { + Some(TFC_Context::new().expect("kbd context error")) + } else { + None + }, uinput_keyboard: None, uinput_mouse: None, xdo: EnigoXdo::default(), From 5ddb10366e5fe0c66187a44a45f07e43e1dfa0d9 Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 13 Oct 2022 06:34:50 -0700 Subject: [PATCH 0705/2015] wayland: fix enigo crash & mid commit Signed-off-by: fufesou --- build.py | 1 + res/DEBIAN/postinst | 2 +- src/common.rs | 25 ++++-------------------- src/server/connection.rs | 1 - src/server/input_service.rs | 3 ++- src/server/video_service.rs | 26 ++++++++++++++++++++++--- src/server/wayland.rs | 39 ++++++++++++++++++++++++++++++++----- 7 files changed, 65 insertions(+), 32 deletions(-) diff --git a/build.py b/build.py index 7780ecfd0..dbc4f6255 100755 --- a/build.py +++ b/build.py @@ -342,6 +342,7 @@ def main(): os.system('cp -a res/DEBIAN/* tmpdeb/DEBIAN/') os.system('strip tmpdeb/usr/bin/rustdesk') os.system('mkdir -p tmpdeb/usr/lib/rustdesk') + os.system('mv tmpdeb/usr/bin/rustdesk tmpdeb/usr/lib/rustdesk/') os.system('cp libsciter-gtk.so tmpdeb/usr/lib/rustdesk/') md5_file('usr/share/rustdesk/files/systemd/rustdesk.service') md5_file('usr/share/rustdesk/files/systemd/rustdesk.service.user') diff --git a/res/DEBIAN/postinst b/res/DEBIAN/postinst index 44cb88c33..95222564d 100755 --- a/res/DEBIAN/postinst +++ b/res/DEBIAN/postinst @@ -14,7 +14,7 @@ if [ "$1" = configure ]; then fi version=$(python3 -V 2>&1 | grep -Po '(?<=Python )(.+)') parsedVersion=$(echo "${version//./}") - mkdir -p /usr/lib/systemd/system/ + mkdir -p /usr/lib/systemd/system/ cp /usr/share/rustdesk/files/systemd/rustdesk.service /usr/lib/systemd/system/rustdesk.service systemctl daemon-reload systemctl enable rustdesk diff --git a/src/common.rs b/src/common.rs index 9beed2e90..26903eacf 100644 --- a/src/common.rs +++ b/src/common.rs @@ -34,34 +34,17 @@ lazy_static::lazy_static! { pub static ref DEVICE_NAME: Arc> = Default::default(); } -lazy_static::lazy_static! { - static ref GLOBAL_INIT_FUNCS: Mutex bool>> = Default::default(); - static ref GLOBAL_CLEAN_FUNCS: Mutex> = Default::default(); -} - -pub fn reg_global_init(key: String, f: fn() -> bool) { - GLOBAL_INIT_FUNCS.lock().unwrap().insert(key, f); -} - -pub fn reg_global_clean(key: String, f: fn()) { - GLOBAL_CLEAN_FUNCS.lock().unwrap().insert(key, f); -} - pub fn global_init() -> bool { - for (k, f) in GLOBAL_INIT_FUNCS.lock().unwrap().iter() { - println!("Init {}", k); - if !f() { - return false; + #[cfg(target_os = "linux")] + { + if !scrap::is_x11() { + crate::server::wayland::set_wayland_scrap_map_err(); } } true } pub fn global_clean() { - for (k, f) in GLOBAL_CLEAN_FUNCS.lock().unwrap().iter() { - println!("Clean {}", k); - f(); - } } #[inline] diff --git a/src/server/connection.rs b/src/server/connection.rs index 8e3f80bb9..c451d5a1c 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -666,7 +666,6 @@ impl Connection { #[allow(unused_mut)] let mut username = crate::platform::get_active_username(); let mut res = LoginResponse::new(); - let mut pi = PeerInfo { username: username.clone(), conn_id: self.inner.id, diff --git a/src/server/input_service.rs b/src/server/input_service.rs index f36f2c50e..7dbba0e05 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -190,7 +190,8 @@ pub async fn set_uinput() -> ResultType<()> { let mouse = super::uinput::client::UInputMouse::new().await?; log::info!("UInput mouse created"); - let mut en = ENIGO.lock().unwrap(); + let xxx = ENIGO.lock(); + let mut en = xxx.unwrap(); en.set_uinput_keyboard(Some(Box::new(keyboard))); en.set_uinput_mouse(Some(Box::new(mouse))); Ok(()) diff --git a/src/server/video_service.rs b/src/server/video_service.rs index fad66ffb4..2b94077be 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -379,6 +379,10 @@ fn run(sp: GenericService) -> ResultType<()> { #[cfg(windows)] ensure_close_virtual_device()?; + // ensure_inited() is needed because release_resouce() may be called. + #[cfg(target_os = "linux")] + super::wayland::ensure_inited()?; + let mut c = get_capturer(true)?; let mut video_qos = VIDEO_QOS.lock().unwrap(); @@ -458,6 +462,8 @@ fn run(sp: GenericService) -> ResultType<()> { #[cfg(windows)] start_uac_elevation_check(); + let mut would_block_count = 0u32; + while sp.ok() { #[cfg(windows)] check_uac_switch(c.privacy_mode_id, c._captuerer_privacy_mode_id)?; @@ -547,8 +553,7 @@ fn run(sp: GenericService) -> ResultType<()> { }; match res { - Err(ref e) if e.kind() == WouldBlock => - { + Err(ref e) if e.kind() == WouldBlock => { #[cfg(windows)] if try_gdi > 0 && !c.is_gdi() { if try_gdi > 3 { @@ -558,6 +563,19 @@ fn run(sp: GenericService) -> ResultType<()> { } try_gdi += 1; } + + would_block_count += 1; + #[cfg(target_os = "linux")] + { + if !scrap::is_x11() { + if would_block_count >= 100 { + // For now, the user should choose and agree screen sharing agiain. + // to-do: Remember choice, attendless... + super::wayland::release_resouce(); + bail!("Wayland capturer none 100 times, try restart captuere"); + } + } + } } Err(err) => { if check_display_changed(c.ndisplay, c.current, c.width, c.height) { @@ -575,7 +593,9 @@ fn run(sp: GenericService) -> ResultType<()> { return Err(err.into()); } - _ => {} + _ => { + would_block_count = 0; + } } let mut fetched_conn_ids = HashSet::new(); diff --git a/src/server/wayland.rs b/src/server/wayland.rs index c12d1e8da..6d4a85399 100644 --- a/src/server/wayland.rs +++ b/src/server/wayland.rs @@ -12,13 +12,22 @@ pub const SCRAP_X11_REF_URL: &str = "https://rustdesk.com/docs/en/manual/linux/# lazy_static::lazy_static! { static ref CAP_DISPLAY_INFO: RwLock = RwLock::new(0); static ref LOG_SCRAP_COUNT: Mutex = Mutex::new(0); - static ref GLOBAL_INIT_REG_HELPER: u8 = { - set_map_err(map_err_scrap); - 0u8 - }; } -pub fn map_err_scrap(err: String) -> io::Error { +pub fn set_wayland_scrap_map_err() { + set_map_err(map_err_scrap); +} + +fn map_err_scrap(err: String) -> io::Error { + // REMOVE ME ===================================== uncomment to handle error + // // to-do: Handle error better, do not restart server + // if err.starts_with("Did not receive a reply") { + // log::error!("Fatal pipewire error, {}", &err); + // std::process::exit(-1); + // } + + log::error!("REMOVE ME ===================================== wayland scrap error {}", &err); + if DISTRO.name.to_uppercase() == "Ubuntu".to_uppercase() { if DISTRO.version_id < "21".to_owned() { io::Error::new(io::ErrorKind::Other, SCRAP_UBUNTU_HIGHER_REQUIRED) @@ -79,6 +88,11 @@ struct CapDisplayInfo { capturer: CapturerPtr, } +#[tokio::main(flavor = "current_thread")] +pub(super) async fn ensure_inited() -> ResultType<()> { + check_init().await +} + async fn check_init() -> ResultType<()> { if !scrap::is_x11() { let mut minx = 0; @@ -205,6 +219,21 @@ pub(super) fn get_display_num() -> ResultType { } } +pub(super) fn release_resouce() { + if scrap::is_x11() { + return; + } + let mut write_lock = CAP_DISPLAY_INFO.write().unwrap(); + if *write_lock != 0 { + let cap_display_info: *mut CapDisplayInfo = *write_lock as _; + unsafe { + let box_capturer = Box::from_raw((*cap_display_info).capturer.0); + let box_cap_display_info = Box::from_raw(cap_display_info); + *write_lock = 0; + } + } +} + pub(super) fn get_capturer() -> ResultType { if scrap::is_x11() { bail!("Do not call this function if not wayland"); From 77de0d05f95d45d3f84e28df8b43fb2fd1328f1f Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 14 Oct 2022 11:19:49 +0800 Subject: [PATCH 0706/2015] msgbox & translations Signed-off-by: fufesou --- flutter/lib/common.dart | 14 ++++- .../lib/desktop/pages/desktop_home_page.dart | 2 +- flutter/lib/mobile/widgets/dialog.dart | 2 +- flutter/lib/models/model.dart | 17 +++--- libs/hbb_common/protos/message.proto | 14 +++++ libs/hbb_common/src/config.rs | 10 +++- src/client.rs | 19 ++++-- src/client/io_loop.rs | 59 ++++++++++++------- src/common.rs | 6 +- src/flutter.rs | 3 +- src/lang/cn.rs | 4 ++ src/lang/cs.rs | 4 ++ src/lang/da.rs | 4 ++ src/lang/de.rs | 4 ++ src/lang/en.rs | 1 + src/lang/eo.rs | 4 ++ src/lang/es.rs | 4 ++ src/lang/fr.rs | 4 ++ src/lang/hu.rs | 4 ++ src/lang/id.rs | 4 ++ src/lang/it.rs | 4 ++ src/lang/ja.rs | 4 ++ src/lang/ko.rs | 4 ++ src/lang/kz.rs | 4 ++ src/lang/pl.rs | 4 ++ src/lang/pt_PT.rs | 4 ++ src/lang/ptbr.rs | 4 ++ src/lang/ru.rs | 4 ++ src/lang/sk.rs | 4 ++ src/lang/template.rs | 4 ++ src/lang/tr.rs | 4 ++ src/lang/tw.rs | 4 ++ src/lang/ua.rs | 4 ++ src/lang/vn.rs | 4 ++ src/port_forward.rs | 4 +- src/server/connection.rs | 4 ++ src/server/video_service.rs | 16 ++++- src/server/wayland.rs | 34 +++++++++-- src/ui/common.tis | 12 ++-- src/ui/msgbox.tis | 12 ++++ src/ui/remote.rs | 4 +- src/ui_session_interface.rs | 10 ++-- 42 files changed, 271 insertions(+), 64 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 686ef3144..caa143632 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -18,6 +18,7 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'package:window_manager/window_manager.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:window_size/window_size.dart' as window_size; +import 'package:url_launcher/url_launcher.dart'; import 'common/widgets/overlay.dart'; import 'mobile/pages/file_manager_page.dart'; @@ -618,8 +619,8 @@ class CustomAlertDialog extends StatelessWidget { } } -void msgBox( - String type, String title, String text, OverlayDialogManager dialogManager, +void msgBox(String type, String title, String text, String link, + OverlayDialogManager dialogManager, {bool? hasCancel}) { dialogManager.dismissAll(); List buttons = []; @@ -636,6 +637,12 @@ void msgBox( dialogManager.dismissAll(); } + jumplink() { + if (link.startsWith('http')) { + launchUrl(Uri.parse(link)); + } + } + if (type != "connecting" && type != "success" && !type.contains("nook")) { hasOk = true; buttons.insert(0, msgBoxButton(translate('OK'), submit)); @@ -654,6 +661,9 @@ void msgBox( dialogManager.dismissAll(); })); } + if (link.isNotEmpty) { + buttons.insert(0, msgBoxButton(translate('JumpLink'), jumplink)); + } dialogManager.show((setState, close) => CustomAlertDialog( title: _msgBoxTitle(title), content: SelectableText(translate(text), diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 4bdbbbaac..a31a71802 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -414,7 +414,7 @@ class _DesktopHomePageState extends State final root = await bind.mainIsRoot(); final release = await bind.mainIsRelease(); if (Platform.isWindows && release && !installed && !root) { - msgBox('custom-elevation-nocancel', 'Prompt', 'elevation_prompt', + msgBox('custom-elevation-nocancel', 'Prompt', 'elevation_prompt', '', gFFI.dialogManager); } }); diff --git a/flutter/lib/mobile/widgets/dialog.dart b/flutter/lib/mobile/widgets/dialog.dart index 455922e34..ca82cbed9 100644 --- a/flutter/lib/mobile/widgets/dialog.dart +++ b/flutter/lib/mobile/widgets/dialog.dart @@ -6,7 +6,7 @@ import '../../models/model.dart'; import '../../models/platform_model.dart'; void clientClose(OverlayDialogManager dialogManager) { - msgBox('', 'Close', 'Are you sure to close the connection?', dialogManager); + msgBox('', 'Close', 'Are you sure to close the connection?', '', dialogManager); } void showSuccess() { diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 1fd33bc77..79cd7ad54 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -222,26 +222,27 @@ class FfiModel with ChangeNotifier { handleMsgBox(Map evt, String id) { if (parent.target == null) return; final dialogManager = parent.target!.dialogManager; - var type = evt['type']; - var title = evt['title']; - var text = evt['text']; + final type = evt['type']; + final title = evt['title']; + final text = evt['text']; + final link = evt['link']; if (type == 're-input-password') { wrongPasswordDialog(id, dialogManager); } else if (type == 'input-password') { enterPasswordDialog(id, dialogManager); } else if (type == 'restarting') { - showMsgBox(id, type, title, text, false, dialogManager, hasCancel: false); + showMsgBox(id, type, title, text, link, false, dialogManager, hasCancel: false); } else { var hasRetry = evt['hasRetry'] == 'true'; - showMsgBox(id, type, title, text, hasRetry, dialogManager); + showMsgBox(id, type, title, text, link, hasRetry, dialogManager); } } /// Show a message box with [type], [title] and [text]. - showMsgBox(String id, String type, String title, String text, bool hasRetry, - OverlayDialogManager dialogManager, + showMsgBox(String id, String type, String title, String text, String link, + bool hasRetry, OverlayDialogManager dialogManager, {bool? hasCancel}) { - msgBox(type, title, text, dialogManager, hasCancel: hasCancel); + msgBox(type, title, text, link, dialogManager, hasCancel: hasCancel); _timer?.cancel(); if (hasRetry) { _timer = Timer(Duration(seconds: _reconnects), () { diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index 1f3d24157..a48ec9d14 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -506,6 +506,19 @@ message AudioFrame { int64 timestamp = 2; } +// Notify peer to show message box. +message MessageBox { + // Message type. Refer to flutter/lib/commom.dart/msgBox(). + string msgtype = 1; + string title = 2; + // English + string text = 3; + // If not empty, msgbox provides a button to following the link. + // The link here can't be directly http url. + // It must be the key of http url configed in peer side or "rustdesk://*" (jump in app). + string link = 4; +} + message BackNotification { // no need to consider block input by someone else enum BlockInputState { @@ -581,5 +594,6 @@ message Message { FileResponse file_response = 18; Misc misc = 19; Cliprdr cliprdr = 20; + MessageBox message_box = 21; } } diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 0ef507a13..ad0585549 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -53,11 +53,19 @@ lazy_static::lazy_static! { static ref HW_CODEC_CONFIG: Arc> = Arc::new(RwLock::new(HwCodecConfig::load())); } -// #[cfg(any(target_os = "android", target_os = "ios"))] lazy_static::lazy_static! { pub static ref APP_DIR: Arc> = Default::default(); pub static ref APP_HOME_DIR: Arc> = Default::default(); } + +// #[cfg(any(target_os = "android", target_os = "ios"))] +lazy_static::lazy_static! { + pub static ref HELPER_URL: HashMap<&'static str, &'static str> = HashMap::from([ + ("rustdesk docs home", "https://rustdesk.com/docs/en/"), + ("rustdesk docs x11-required", "https://rustdesk.com/docs/en/manual/linux/#x11-required"), + ]); +} + const CHARS: &'static [char] = &[ '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'm', 'n', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', diff --git a/src/client.rs b/src/client.rs index 8723480f3..6b3917790 100644 --- a/src/client.rs +++ b/src/client.rs @@ -48,7 +48,10 @@ pub use super::lang::*; pub mod file_trait; pub mod helper; pub mod io_loop; -use crate::ui_session_interface::global_save_keyboard_mode; +use crate::{ + server::video_service::{SCRAP_X11_REF_URL, SCRAP_X11_REQUIRED}, + ui_session_interface::global_save_keyboard_mode, +}; 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); @@ -1263,10 +1266,14 @@ impl LoginConfigHandler { pub fn handle_login_error(&mut self, err: &str, interface: &impl Interface) -> bool { if err == "Wrong Password" { self.password = Default::default(); - interface.msgbox("re-input-password", err, "Do you want to enter again?"); + interface.msgbox("re-input-password", err, "Do you want to enter again?", ""); true } else { - interface.msgbox("error", "Login Error", err); + if err.contains(SCRAP_X11_REQUIRED) { + interface.msgbox("error", "Login Error", err, SCRAP_X11_REF_URL); + } else { + interface.msgbox("error", "Login Error", err, ""); + } false } } @@ -1636,7 +1643,7 @@ pub async fn handle_hash( if password.is_empty() { // login without password, the remote side can click accept send_login(lc.clone(), Vec::new(), peer).await; - interface.msgbox("input-password", "Password Required", ""); + interface.msgbox("input-password", "Password Required", "", ""); } else { let mut hasher = Sha256::new(); hasher.update(&password); @@ -1689,7 +1696,7 @@ pub async fn handle_login_from_ui( pub trait Interface: Send + Clone + 'static + Sized { /// Send message data to remote peer. fn send(&self, data: Data); - fn msgbox(&self, msgtype: &str, title: &str, text: &str); + fn msgbox(&self, msgtype: &str, title: &str, text: &str, link: &str); fn handle_login_error(&mut self, err: &str) -> bool; fn handle_peer_info(&mut self, pi: PeerInfo); fn set_force_relay(&mut self, direct: bool, received: bool); @@ -1697,7 +1704,7 @@ pub trait Interface: Send + Clone + 'static + Sized { fn is_port_forward(&self) -> bool; fn is_rdp(&self) -> bool; fn on_error(&self, err: &str) { - self.msgbox("error", "Error", err); + self.msgbox("error", "Error", err, ""); } fn is_force_relay(&self) -> bool; async fn handle_hash(&mut self, pass: &str, hash: Hash, peer: &mut Stream); diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 7e3bbb3dc..a415576c8 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -119,7 +119,7 @@ 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()); + self.handler.msgbox("error", "Connection Error", &err.to_string(), ""); break; } Ok(ref bytes) => { @@ -134,10 +134,10 @@ impl Remote { } else { if self.handler.is_restarting_remote_device() { log::info!("Restart remote device"); - self.handler.msgbox("restarting", "Restarting Remote Device", "remote_restarting_tip"); + self.handler.msgbox("restarting", "Restarting Remote Device", "remote_restarting_tip", ""); } else { log::info!("Reset by the peer"); - self.handler.msgbox("error", "Connection Error", "Reset by the peer"); + self.handler.msgbox("error", "Connection Error", "Reset by the peer", ""); } break; } @@ -162,12 +162,12 @@ impl Remote { } _ = self.timer.tick() => { if last_recv_time.elapsed() >= SEC30 { - self.handler.msgbox("error", "Connection Error", "Timeout"); + self.handler.msgbox("error", "Connection Error", "Timeout", ""); break; } if !self.read_jobs.is_empty() { if let Err(err) = fs::handle_read_jobs(&mut self.read_jobs, &mut peer).await { - self.handler.msgbox("error", "Connection Error", &err.to_string()); + self.handler.msgbox("error", "Connection Error", &err.to_string(), ""); break; } self.update_jobs_status(); @@ -191,7 +191,7 @@ impl Remote { } Err(err) => { self.handler - .msgbox("error", "Connection Error", &err.to_string()); + .msgbox("error", "Connection Error", &err.to_string(), ""); } } if let Some(stop) = stop_clipboard { @@ -971,7 +971,7 @@ impl Remote { } } Some(misc::Union::CloseReason(c)) => { - self.handler.msgbox("error", "Connection Error", &c); + self.handler.msgbox("error", "Connection Error", &c, ""); return false; } Some(misc::Union::BackNotification(notification)) => { @@ -981,8 +981,12 @@ impl Remote { } Some(misc::Union::Uac(uac)) => { if uac { - self.handler - .msgbox("custom-uac-nocancel", "Warning", "uac_warning"); + self.handler.msgbox( + "custom-uac-nocancel", + "Warning", + "uac_warning", + "", + ); } } Some(misc::Union::ForegroundWindowElevated(elevated)) => { @@ -991,6 +995,7 @@ impl Remote { "custom-elevated-foreground-nocancel", "Warning", "elevated_foreground_window_warning", + "", ); } } @@ -1012,6 +1017,19 @@ impl Remote { } _ => {} }, + Some(message::Union::MessageBox(msgbox)) => { + let mut link = msgbox.link; + if !link.starts_with("rustdesk://") { + if let Some(v) = hbb_common::config::HELPER_URL.get(&link as &str) { + link = v.to_string(); + } else { + log::warn!("Message box ignore link {} for security", &link); + link = "".to_string(); + } + } + self.handler + .msgbox(&msgbox.msgtype, &msgbox.title, &msgbox.text, &link); + } _ => {} } } @@ -1053,7 +1071,7 @@ impl Remote { } back_notification::BlockInputState::BlkOnFailed => { self.handler - .msgbox("custom-error", "Block user input", "Failed"); + .msgbox("custom-error", "Block user input", "Failed", ""); self.update_block_input_state(false); } back_notification::BlockInputState::BlkOffSucceeded => { @@ -1061,7 +1079,7 @@ impl Remote { } back_notification::BlockInputState::BlkOffFailed => { self.handler - .msgbox("custom-error", "Unblock user input", "Failed"); + .msgbox("custom-error", "Unblock user input", "Failed", ""); } _ => {} } @@ -1086,51 +1104,52 @@ impl Remote { "error", "Connecting...", "Someone turns on privacy mode, exit", + "", ); return false; } back_notification::PrivacyModeState::PrvNotSupported => { self.handler - .msgbox("custom-error", "Privacy mode", "Unsupported"); + .msgbox("custom-error", "Privacy mode", "Unsupported", ""); self.update_privacy_mode(false); } back_notification::PrivacyModeState::PrvOnSucceeded => { self.handler - .msgbox("custom-nocancel", "Privacy mode", "In privacy mode"); + .msgbox("custom-nocancel", "Privacy mode", "In privacy mode", ""); self.update_privacy_mode(true); } back_notification::PrivacyModeState::PrvOnFailedDenied => { self.handler - .msgbox("custom-error", "Privacy mode", "Peer denied"); + .msgbox("custom-error", "Privacy mode", "Peer denied", ""); self.update_privacy_mode(false); } back_notification::PrivacyModeState::PrvOnFailedPlugin => { self.handler - .msgbox("custom-error", "Privacy mode", "Please install plugins"); + .msgbox("custom-error", "Privacy mode", "Please install plugins", ""); self.update_privacy_mode(false); } back_notification::PrivacyModeState::PrvOnFailed => { self.handler - .msgbox("custom-error", "Privacy mode", "Failed"); + .msgbox("custom-error", "Privacy mode", "Failed", ""); self.update_privacy_mode(false); } back_notification::PrivacyModeState::PrvOffSucceeded => { self.handler - .msgbox("custom-nocancel", "Privacy mode", "Out privacy mode"); + .msgbox("custom-nocancel", "Privacy mode", "Out privacy mode", ""); self.update_privacy_mode(false); } back_notification::PrivacyModeState::PrvOffByPeer => { self.handler - .msgbox("custom-error", "Privacy mode", "Peer exit"); + .msgbox("custom-error", "Privacy mode", "Peer exit", ""); self.update_privacy_mode(false); } back_notification::PrivacyModeState::PrvOffFailed => { self.handler - .msgbox("custom-error", "Privacy mode", "Failed to turn off"); + .msgbox("custom-error", "Privacy mode", "Failed to turn off", ""); } back_notification::PrivacyModeState::PrvOffUnknown => { self.handler - .msgbox("custom-error", "Privacy mode", "Turned off"); + .msgbox("custom-error", "Privacy mode", "Turned off", ""); // log::error!("Privacy mode is turned off with unknown reason"); self.update_privacy_mode(false); } diff --git a/src/common.rs b/src/common.rs index 26903eacf..3022e5b56 100644 --- a/src/common.rs +++ b/src/common.rs @@ -1,5 +1,6 @@ use std::{ collections::HashMap, + future::Future, sync::{Arc, Mutex}, }; @@ -21,6 +22,8 @@ use hbb_common::{ // #[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))] use hbb_common::{config::RENDEZVOUS_PORT, futures::future::join_all}; +pub type NotifyMessageBox = fn(String, String, String, String) -> dyn Future; + pub const CLIPBOARD_NAME: &'static str = "clipboard"; pub const CLIPBOARD_INTERVAL: u64 = 333; @@ -44,8 +47,7 @@ pub fn global_init() -> bool { true } -pub fn global_clean() { -} +pub fn global_clean() {} #[inline] pub fn valid_for_numlock(evt: &KeyEvent) -> bool { diff --git a/src/flutter.rs b/src/flutter.rs index b65ce4412..2b95f9cfb 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -310,7 +310,7 @@ impl InvokeUiSession for FlutterHandler { ); } - fn msgbox(&self, msgtype: &str, title: &str, text: &str, retry: bool) { + fn msgbox(&self, msgtype: &str, title: &str, text: &str, link: &str, retry: bool) { let has_retry = if retry { "true" } else { "" }; self.push_event( "msgbox", @@ -318,6 +318,7 @@ impl InvokeUiSession for FlutterHandler { ("type", msgtype), ("title", title), ("text", text), + ("link", link), ("hasRetry", has_retry), ], ); diff --git a/src/lang/cn.rs b/src/lang/cn.rs index c88b0182c..cf2575a7f 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -377,5 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Custom", "自定义"), ("Full Access", "完全访问"), ("Screen Share", "仅共享屏幕"), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland 需要 Ubuntu 21.04 或更高版本。"), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland 需要更高版本的 linux 发行版。 请尝试 X11 桌面或更改您的操作系统。"), + ("JumpLink", "查看"), + ("Please Select the screen to be shared(Operate on the peer side).", "请选择要分享的画面(对端操作)。"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index f616d518b..162fdc1ed 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -377,5 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Custom", ""), ("Full Access", ""), ("Screen Share", ""), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland vyžaduje Ubuntu 21.04 nebo vyšší verzi."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland vyžaduje vyšší verzi linuxové distribuce. Zkuste prosím X11 desktop nebo změňte OS."), + ("JumpLink", "View"), + ("Please Select the screen to be shared(Operate on the peer side).", "Vyberte prosím obrazovku, kterou chcete sdílet (Ovládejte na straně protějšku)."), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 3fe49d051..df9929d1f 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -377,5 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Custom", ""), ("Full Access", ""), ("Screen Share", ""), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland kræver Ubuntu 21.04 eller nyere version."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland kræver en højere version af linux distro. Prøv venligst X11 desktop eller skift dit OS."), + ("JumpLink", "View"), + ("Please Select the screen to be shared(Operate on the peer side).", "Vælg venligst den skærm, der skal deles (Betjen på peer-siden)."), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 4942b3171..b0f6d0ba3 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -377,5 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Custom", ""), ("Full Access", ""), ("Screen Share", ""), + ("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 Bildschirm aus, der freigegeben werden soll (auf der Peer-Seite arbeiten)."), ].iter().cloned().collect(); } diff --git a/src/lang/en.rs b/src/lang/en.rs index 279f26cd1..7c95f8abb 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -33,5 +33,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("elevation_prompt", "Running software without privilege elevation may cause problems when remote users operate certain windows."), ("uac_warning", "Temporarily denied access due to elevation request, please wait for the remote user to accept the UAC dialog. To avoid this problem, it is recommended to install the software on the remote device or run it with administrator privileges."), ("elevated_foreground_window_warning", "Temporarily unable to use the mouse and keyboard, because the current window of the remote desktop requires higher privilege to operate, you can request the remote user to minimize the current window. To avoid this problem, it is recommended to install the software on the remote device or run it with administrator privileges."), + ("JumpLink", "View"), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 9bfea5440..ca0f12bcd 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -377,5 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Custom", ""), ("Full Access", ""), ("Screen Share", ""), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland postulas Ubuntu 21.04 aŭ pli altan version."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland postulas pli altan version de linuksa distro. Bonvolu provi X11-labortablon aŭ ŝanĝi vian OS."), + ("JumpLink", "View"), + ("Please Select the screen to be shared(Operate on the peer side).", "Bonvolu Elekti la ekranon por esti dividita (Funkciu ĉe la sama flanko)."), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 3b6419572..3be5f920e 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -377,5 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Custom", ""), ("Full Access", ""), ("Screen Share", ""), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland requiere Ubuntu 21.04 o una versión superior."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland requiere una versión superior de la distribución de Linux. Pruebe el escritorio X11 o cambie su sistema operativo."), + ("JumpLink", "View"), + ("Please Select the screen to be shared(Operate on the peer side).", "Seleccione la pantalla que se compartirá (Operar en el lado del compañero)."), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index b7dbd5636..484c415fc 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -377,5 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Custom", ""), ("Full Access", ""), ("Screen Share", ""), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland nécessite Ubuntu 21.04 ou une version supérieure."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland nécessite une version supérieure de la distribution Linux. Veuillez essayer le bureau X11 ou changer votre système d'exploitation."), + ("JumpLink", "View"), + ("Please Select the screen to be shared(Operate on the peer side).", "Veuillez sélectionner l'écran à partager (opérer du côté pair)."), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index aa5e34d09..c2d5903e1 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -377,5 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Custom", ""), ("Full Access", ""), ("Screen Share", ""), + ("Wayland requires Ubuntu 21.04 or higher version.", "A Waylandhoz Ubuntu 21.04 vagy újabb verzió szükséges."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "A Wayland a Linux disztró magasabb verzióját igényli. Próbálja ki az X11 desktopot, vagy változtassa meg az operációs rendszert."), + ("JumpLink", "View"), + ("Please Select the screen to be shared(Operate on the peer side).", "Kérjük, válassza ki a megosztani kívánt képernyőt (a társoldalon működjön)."), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 1ab7fd134..33aae1579 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -377,5 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Custom", ""), ("Full Access", ""), ("Screen Share", ""), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland membutuhkan Ubuntu 21.04 atau versi yang lebih tinggi."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland membutuhkan versi distro linux yang lebih tinggi. Silakan coba desktop X11 atau ubah OS Anda."), + ("JumpLink", "View"), + ("Please Select the screen to be shared(Operate on the peer side).", "Silakan Pilih layar yang akan dibagikan (Operasi di sisi rekan)."), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 65c16567c..5b37e9291 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -377,5 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Custom", ""), ("Full Access", ""), ("Screen Share", ""), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland richiede Ubuntu 21.04 o versione successiva."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland richiede una versione superiore della distribuzione Linux. Prova X11 desktop o cambia il tuo sistema operativo."), + ("JumpLink", "View"), + ("Please Select the screen to be shared(Operate on the peer side).", "Seleziona lo schermo da condividere (opera sul lato peer)."), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 75a9c0c9b..593ef0186 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -377,5 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Custom", ""), ("Full Access", ""), ("Screen Share", ""), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland には、Ubuntu 21.04 以降のバージョンが必要です。"), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland には、より高いバージョンの Linux ディストリビューションが必要です。 X11 デスクトップを試すか、OS を変更してください。"), + ("JumpLink", "View"), + ("Please Select the screen to be shared(Operate on the peer side).", "共有する画面を選択してください(ピア側で操作)。"), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index eccb4d836..1b668462e 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -377,5 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Custom", ""), ("Full Access", ""), ("Screen Share", ""), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland는 Ubuntu 21.04 이상 버전이 필요합니다."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland에는 더 높은 버전의 Linux 배포판이 필요합니다. X11 데스크탑을 시도하거나 OS를 변경하십시오."), + ("JumpLink", "View"), + ("Please Select the screen to be shared(Operate on the peer side).", "공유할 화면을 선택하십시오(피어 측에서 작동)."), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 9a1e23c23..e55b0a3e3 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -377,5 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Custom", ""), ("Full Access", ""), ("Screen Share", ""), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland Ubuntu 21.04 немесе одан жоғары нұсқасын қажет етеді."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland linux дистрибутивінің жоғарырақ нұсқасын қажет етеді. X11 жұмыс үстелін қолданып көріңіз немесе операциялық жүйеңізді өзгертіңіз."), + ("JumpLink", "View"), + ("Please Select the screen to be shared(Operate on the peer side).", "Бөлісетін экранды таңдаңыз (бірдей жағынан жұмыс жасаңыз)."), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index e96061729..16f1a5ff3 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -377,5 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Custom", ""), ("Full Access", ""), ("Screen Share", ""), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland wymaga Ubuntu 21.04 lub nowszego."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland wymaga wyższej wersji dystrybucji Linuksa. Wypróbuj pulpit X11 lub zmień system operacyjny."), + ("JumpLink", "View"), + ("Please Select the screen to be shared(Operate on the peer side).", "Wybierz ekran do udostępnienia (działaj po stronie równorzędnej)."), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index e17967998..ea90329a5 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -377,5 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Custom", ""), ("Full Access", ""), ("Screen Share", ""), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland requer Ubuntu 21.04 ou versão superior."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland requer uma versão superior da distribuição linux. Por favor, tente o desktop X11 ou mude seu sistema operacional."), + ("JumpLink", "View"), + ("Please Select the screen to be shared(Operate on the peer side).", "Por favor, selecione a tela a ser compartilhada (operar no lado do peer)."), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index c2965405d..8a6280aed 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -377,5 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Custom", ""), ("Full Access", ""), ("Screen Share", ""), + ("Wayland requires Ubuntu 21.04 or higher version.", ""), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", ""), + ("JumpLink", "View"), + ("Please Select the screen to be shared(Operate on the peer side).", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 95c59dede..5fe9c89ec 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -377,5 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Custom", ""), ("Full Access", ""), ("Screen Share", ""), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland требует Ubuntu 21.04 или более позднюю версию."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Для Wayland требуется более поздняя версия дистрибутива Linux. Пожалуйста, попробуйте рабочий стол X11 или смените ОС."), + ("JumpLink", "View"), + ("Please Select the screen to be shared(Operate on the peer side).", "Пожалуйста, выберите экран для совместного использования (работайте на одноранговой стороне)."), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 5064e1069..23c8b7220 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -377,5 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Custom", ""), ("Full Access", ""), ("Screen Share", ""), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland vyžaduje Ubuntu 21.04 alebo vyššiu verziu."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland vyžaduje vyššiu verziu linuxovej distribúcie. Skúste X11 desktop alebo zmeňte OS."), + ("JumpLink", "View"), + ("Please Select the screen to be shared(Operate on the peer side).", "Vyberte obrazovku, ktorú chcete zdieľať (Ovládajte na strane partnera)."), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index e08a2ba76..6d549b02a 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -377,5 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Custom", ""), ("Full Access", ""), ("Screen Share", ""), + ("Wayland requires Ubuntu 21.04 or higher version.", ""), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", ""), + ("JumpLink", ""), + ("Please Select the screen to be shared(Operate on the peer side).", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 117a5683b..1cf50b2ca 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -377,5 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Custom", ""), ("Full Access", ""), ("Screen Share", ""), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland, Ubuntu 21.04 veya daha yüksek bir sürüm gerektirir."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland, linux dağıtımının daha yüksek bir sürümünü gerektirir. Lütfen X11 masaüstünü deneyin veya işletim sisteminizi değiştirin."), + ("JumpLink", "View"), + ("Please Select the screen to be shared(Operate on the peer side).", "Lütfen paylaşılacak ekranı seçiniz (Ekran tarafında çalıştırın)."), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index d0bce5d77..91d0ab169 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -377,5 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Custom", "自定義"), ("Full Access", "完全訪問"), ("Screen Share", "僅共享屏幕"), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland 需要 Ubuntu 21.04 或更高版本。"), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland 需要更高版本的 linux 發行版。 請嘗試 X11 桌面或更改您的操作系統。"), + ("JumpLink", "查看"), + ("Please Select the screen to be shared(Operate on the peer side).", "請選擇要分享的畫面(在對端操作)。"), ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index f17e17a93..528b919b8 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -377,5 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Custom", ""), ("Full Access", ""), ("Screen Share", ""), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland потребує Ubuntu 21.04 або новішої версії."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Для Wayland потрібна новіша версія дистрибутива Linux. Будь ласка, спробуйте робочий стіл X11 або змініть свою ОС."), + ("JumpLink", "View"), + ("Please Select the screen to be shared(Operate on the peer side).", "Будь ласка, виберіть екран, до якого потрібно надати доступ (працюйте на стороні однорангового пристрою)."), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 30e32a82c..7f368126f 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -377,5 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Custom", ""), ("Full Access", ""), ("Screen Share", ""), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland yêu cầu phiên bản Ubuntu 21.04 trở lên."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland yêu cầu phiên bản distro linux cao hơn. Vui lòng thử máy tính để bàn X11 hoặc thay đổi hệ điều hành của bạn."), + ("JumpLink", "View"), + ("Please Select the screen to be shared(Operate on the peer side).", "Vui lòng Chọn màn hình để chia sẻ (Hoạt động ở phía ngang hàng)."), ].iter().cloned().collect(); } diff --git a/src/port_forward.rs b/src/port_forward.rs index 934743edc..f50f40db8 100644 --- a/src/port_forward.rs +++ b/src/port_forward.rs @@ -75,13 +75,13 @@ pub async fn listen( let interface = interface.clone(); tokio::spawn(async move { if let Err(err) = run_forward(forward, stream).await { - interface.msgbox("error", "Error", &err.to_string()); + interface.msgbox("error", "Error", &err.to_string(), ""); } log::info!("connection from {:?} closed", addr); }); } Err(err) => { - interface.msgbox("error", "Error", &err.to_string()); + interface.msgbox("error", "Error", &err.to_string(), ""); } _ => {} } diff --git a/src/server/connection.rs b/src/server/connection.rs index c451d5a1c..dca05e088 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -742,6 +742,10 @@ impl Connection { res.set_peer_info(pi); } else { try_activate_screen(); + if let Some(msg_out) = super::video_service::is_inited_msg() { + self.send(msg_out).await; + } + match super::video_service::get_displays().await { Err(err) => { res.set_error(format!("Error: {}", err)); diff --git a/src/server/video_service.rs b/src/server/video_service.rs index 2b94077be..d43996559 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -39,6 +39,12 @@ use std::{ #[cfg(windows)] use virtual_display; +pub const SCRAP_UBUNTU_HIGHER_REQUIRED: &str = "Wayland requires Ubuntu 21.04 or higher version."; +pub const SCRAP_OTHER_VERSION_OR_X11_REQUIRED: &str = + "Wayland requires higher version of linux distro. Please try X11 desktop or change your OS."; +pub const SCRAP_X11_REQUIRED: &str = "x11 expected"; +pub const SCRAP_X11_REF_URL: &str = "https://rustdesk.com/docs/en/manual/linux/#x11-required"; + pub const NAME: &'static str = "video"; lazy_static::lazy_static! { @@ -569,7 +575,7 @@ fn run(sp: GenericService) -> ResultType<()> { { if !scrap::is_x11() { if would_block_count >= 100 { - // For now, the user should choose and agree screen sharing agiain. + // For now, the user should choose and agree screen sharing agiain. // to-do: Remember choice, attendless... super::wayland::release_resouce(); bail!("Wayland capturer none 100 times, try restart captuere"); @@ -747,6 +753,14 @@ pub(super) fn get_displays_2(all: &Vec) -> (usize, Vec) { (*lock, displays) } +pub fn is_inited_msg() -> Option { + #[cfg(target_os = "linux")] + if !scrap::is_x11() { + return super::wayland::is_inited(); + } + None +} + pub async fn get_displays() -> ResultType<(usize, Vec)> { #[cfg(target_os = "linux")] { diff --git a/src/server/wayland.rs b/src/server/wayland.rs index 6d4a85399..8cf1623c5 100644 --- a/src/server/wayland.rs +++ b/src/server/wayland.rs @@ -3,11 +3,9 @@ use hbb_common::{allow_err, platform::linux::DISTRO}; use scrap::{set_map_err, Capturer, Display, Frame, TraitCapturer}; use std::io; -pub const SCRAP_UBUNTU_HIGHER_REQUIRED: &str = "Wayland requires Ubuntu 21.04 or higher version."; -pub const SCRAP_OTHER_VERSION_OR_X11_REQUIRED: &str = - "Wayland requires higher version of linux distro. Please try X11 desktop or change your OS."; -pub const SCRAP_X11_REQUIRED: &str = "X11 is required"; -pub const SCRAP_X11_REF_URL: &str = "https://rustdesk.com/docs/en/manual/linux/#x11-required"; +use super::video_service::{ + SCRAP_OTHER_VERSION_OR_X11_REQUIRED, SCRAP_UBUNTU_HIGHER_REQUIRED, SCRAP_X11_REQUIRED, +}; lazy_static::lazy_static! { static ref CAP_DISPLAY_INFO: RwLock = RwLock::new(0); @@ -26,7 +24,10 @@ fn map_err_scrap(err: String) -> io::Error { // std::process::exit(-1); // } - log::error!("REMOVE ME ===================================== wayland scrap error {}", &err); + log::error!( + "REMOVE ME ===================================== wayland scrap error {}", + &err + ); if DISTRO.name.to_uppercase() == "Ubuntu".to_uppercase() { if DISTRO.version_id < "21".to_owned() { @@ -93,6 +94,27 @@ pub(super) async fn ensure_inited() -> ResultType<()> { check_init().await } +pub(super) fn is_inited() -> Option { + if scrap::is_x11() { + None + } else { + if *CAP_DISPLAY_INFO.read().unwrap() == 0 { + let mut msg_out = Message::new(); + let res = MessageBox { + msgtype: "nook-nocancel-hasclose".to_owned(), + title: "Wayland".to_owned(), + text: "Please Select the screen to be shared(Operate on the peer side).".to_owned(), + link: "".to_owned(), + ..Default::default() + }; + msg_out.set_message_box(res); + Some(msg_out) + } else { + None + } + } +} + async fn check_init() -> ResultType<()> { if !scrap::is_x11() { let mut minx = 0; diff --git a/src/ui/common.tis b/src/ui/common.tis index 69e5565f0..e591f45a3 100644 --- a/src/ui/common.tis +++ b/src/ui/common.tis @@ -232,7 +232,7 @@ class ChatBox: Reactor.Component { /******************** start of msgbox ****************************************/ var remember_password = false; -function msgbox(type, title, content, callback=null, height=180, width=500, hasRetry=false, contentStyle="") { +function msgbox(type, title, content, link, callback=null, height=180, width=500, hasRetry=false, contentStyle="") { $(body).scrollTo(0, 0); if (!type) { closeMsgbox(); @@ -264,21 +264,21 @@ function msgbox(type, title, content, callback=null, height=180, width=500, hasR } else if (type.indexOf("custom") < 0 && !is_port_forward && !callback) { callback = function() { view.close(); } } - $(#msgbox).content(); + $(#msgbox).content(); } function connecting() { handler.msgbox("connecting", "Connecting...", "Connection in progress. Please wait."); } -handler.msgbox = function(type, title, text, hasRetry=false) { +handler.msgbox = function(type, title, text, link = "", hasRetry=false) { // crash somehow (when input wrong password), even with small time, for example, 1ms - self.timer(60ms, function() { msgbox(type, title, text, null, 180, 500, hasRetry); }); + self.timer(60ms, function() { msgbox(type, title, text, link, null, 180, 500, hasRetry); }); } var reconnectTimeout = 1000; -handler.msgbox_retry = function(type, title, text, hasRetry) { - handler.msgbox(type, title, text, hasRetry); +handler.msgbox_retry = function(type, title, text, link, hasRetry) { + handler.msgbox(type, title, text, link, hasRetry); if (hasRetry) { self.timer(0, retryConnect); self.timer(reconnectTimeout, retryConnect); diff --git a/src/ui/msgbox.tis b/src/ui/msgbox.tis index 7d1430cb0..3f40c367d 100644 --- a/src/ui/msgbox.tis +++ b/src/ui/msgbox.tis @@ -1,3 +1,5 @@ +import * as env from "@env"; + function translate_text(text) { if (text.indexOf('Failed') == 0 && text.indexOf(': ') > 0) { var fds = text.split(': '); @@ -22,6 +24,7 @@ class MsgboxComponent: Reactor.Component { this.type = params.type; this.title = params.title; this.content = params.content; + this.link = params.link; this.remember = params.remember; this.callback = params.callback; this.hasRetry = params.hasRetry; @@ -93,6 +96,7 @@ class MsgboxComponent: Reactor.Component { var content = this.getContent(); var hasCancel = this.type.indexOf("error") < 0 && this.type.indexOf("nocancel") < 0 && this.type != "restarting"; var hasOk = this.type != "connecting" && this.type != "success" && this.type.indexOf("nook") < 0; + var hasLink = this.link != ""; var hasClose = this.type.indexOf("hasclose") >= 0; var show_progress = this.type == "connecting"; var me = this; @@ -121,6 +125,7 @@ class MsgboxComponent: Reactor.Component { {hasCancel || this.hasRetry ? : ""} {this.hasSkip() ? : ""} {hasOk || this.hasRetry ? : ""} + {hasLink ? : ""} {hasClose ? : ""}
    @@ -155,6 +160,13 @@ class MsgboxComponent: Reactor.Component { if (this.callback) this.callback(values); if (this.close) this.close(); } + + event click $(button#jumplink) { + stdout.println("REMOVE ME ================================= jump link" + this.link); + if (this.link.indexOf("http") == 0) { + env.launch(this.link); + } + } event click $(button#submit) { if (this.type == "error") { diff --git a/src/ui/remote.rs b/src/ui/remote.rs index b3f443dda..dfd6394a5 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -238,8 +238,8 @@ impl InvokeUiSession for SciterHandler { self.call("updatePi", &make_args!(pi_sciter)); } - fn msgbox(&self, msgtype: &str, title: &str, text: &str, retry: bool) { - self.call2("msgbox_retry", &make_args!(msgtype, title, text, retry)); + fn msgbox(&self, msgtype: &str, title: &str, text: &str, link: &str, retry: bool) { + self.call2("msgbox_retry", &make_args!(msgtype, title, text, link, retry)); } fn new_message(&self, msg: String) { diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index f5cc0499d..cf550ed63 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -1088,7 +1088,7 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { 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 msgbox(&self, msgtype: &str, title: &str, text: &str, retry: bool); + 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); } @@ -1137,9 +1137,9 @@ impl Interface for Session { self.lc.read().unwrap().conn_type.eq(&ConnType::RDP) } - fn msgbox(&self, msgtype: &str, title: &str, text: &str) { + fn msgbox(&self, msgtype: &str, title: &str, text: &str, link: &str) { let retry = check_if_retry(msgtype, title, text); - self.ui_handler.msgbox(msgtype, title, text, retry); + self.ui_handler.msgbox(msgtype, title, text, link, retry); } fn handle_login_error(&mut self, err: &str) -> bool { @@ -1164,7 +1164,7 @@ impl Interface for Session { if pi.displays.is_empty() { self.lc.write().unwrap().handle_peer_info(&pi); self.update_privacy_mode(); - self.msgbox("error", "Remote Error", "No Display"); + self.msgbox("error", "Remote Error", "No Display", ""); return; } let p = self.lc.read().unwrap().should_auto_login(); @@ -1181,7 +1181,7 @@ impl Interface for Session { if self.is_file_transfer() { self.close_success(); } else if !self.is_port_forward() { - self.msgbox("success", "Successful", "Connected, waiting for image..."); + self.msgbox("success", "Successful", "Connected, waiting for image...", ""); } #[cfg(windows)] { From 9385e95b4eb653581184ae81e4cce92a4505363c Mon Sep 17 00:00:00 2001 From: fufesou Date: Mon, 17 Oct 2022 09:41:02 +0800 Subject: [PATCH 0707/2015] debug msgbox in sciter ui Signed-off-by: fufesou --- src/server/connection.rs | 2 +- src/ui/ab.tis | 50 ++++++++++++++++++++++------------------ src/ui/common.tis | 2 +- src/ui/file_transfer.tis | 8 +++---- src/ui/header.tis | 20 ++++++++++------ src/ui/index.tis | 18 +++++++-------- src/ui/msgbox.tis | 5 +--- 7 files changed, 57 insertions(+), 48 deletions(-) diff --git a/src/server/connection.rs b/src/server/connection.rs index dca05e088..e865a8b4f 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -748,7 +748,7 @@ impl Connection { match super::video_service::get_displays().await { Err(err) => { - res.set_error(format!("Error: {}", err)); + res.set_error(format!("{}", err)); } Ok((current, displays)) => { pi.displays = displays.into(); diff --git a/src/ui/ab.tis b/src/ui/ab.tis index ac2efb7dd..acee472a8 100644 --- a/src/ui/ab.tis +++ b/src/ui/ab.tis @@ -76,28 +76,34 @@ class AddressBook: Reactor.Component event click $(#add-id) (_, __) { var me = this; - msgbox("custom-add-id", translate("Add ID"),
    + msgbox( + "custom-add-id", + translate("Add ID"), +
    {translate("whitelist_sep")}
    -
    , function(res=null) { - if (!res) return; - var value = (res.text || "").trim(); - var values = value.split(/[\s,;\n]+/g); - if (values.length == 0) return; - for (var v in values) { - var found; - for (var i = 0; i < ab.peers.length; ++i) { - if (ab.peers[i].id == v) { - found = true; - break; +
    , + "", + function(res=null) { + if (!res) return; + var value = (res.text || "").trim(); + var values = value.split(/[\s,;\n]+/g); + if (values.length == 0) return; + for (var v in values) { + var found; + for (var i = 0; i < ab.peers.length; ++i) { + if (ab.peers[i].id == v) { + found = true; + break; + } } + if (found) continue; + ab.peers.push({ id: v }); } - if (found) continue; - ab.peers.push({ id: v }); - } - updateAb(); - me.update(); - }, 300); + updateAb(); + me.update(); + }, + 300); } event click $(#add-tag) (_, __) { @@ -105,7 +111,7 @@ class AddressBook: Reactor.Component msgbox("custom-add-tag", translate("Add Tag"),
    {translate("whitelist_sep")}
    -
    , function(res=null) { +
    + // MenuEntryDivider(), () { final state = ShowRemoteCursorState.find(widget.id); return MenuEntrySwitch2( @@ -631,6 +633,14 @@ class _RemoteMenubarState extends State { if (perms['audio'] != false) { displayMenu.add(_createSwitchMenuEntry('Mute', 'disable-audio')); } + + if (Platform.isWindows && + pi.platform == 'Windows' && + perms['file'] != false) { + displayMenu.add(_createSwitchMenuEntry( + 'Allow file copy and paste', 'enable-file-transfer')); + } + if (perms['keyboard'] != false) { if (perms['clipboard'] != false) { displayMenu.add( From adafa38cfadc263255be9bd38b7d43339ed77238 Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 8 Sep 2022 21:03:20 -0700 Subject: [PATCH 0434/2015] flutter_desktop: change cursor on scroll auto Signed-off-by: fufesou --- flutter/lib/desktop/pages/remote_page.dart | 28 +++++++++++---------- flutter/lib/desktop/widgets/popup_menu.dart | 2 +- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index c03c2f3d4..96ade40f4 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -537,8 +537,17 @@ class ImagePaint extends StatelessWidget { Widget build(BuildContext context) { final m = Provider.of(context); var c = Provider.of(context); - final s = c.scale; + + mouseRegion({child}) => Obx(() => MouseRegion( + cursor: (cursorOverImage.isTrue && keyboardEnabled.isTrue) + ? (remoteCursorMoved.isTrue + ? SystemMouseCursors.none + : _buildCustomCursorLinux(context, s)) + : MouseCursor.defer, + onHover: (evt) {}, + child: child)); + if (c.scrollStyle == ScrollStyle.scrollbar) { final imageWidget = SizedBox( width: c.getDisplayWidth() * s, @@ -547,7 +556,6 @@ class ImagePaint extends StatelessWidget { painter: ImagePainter(image: m.image, x: 0, y: 0, scale: s), )); - Rx pos = Rx(const Offset(0.0, 0.0)); return Center( child: NotificationListener( onNotification: (notification) { @@ -562,16 +570,8 @@ class ImagePaint extends StatelessWidget { c.setScrollPercent(percentX, percentY); return false; }, - child: Obx(() => MouseRegion( - cursor: (cursorOverImage.isTrue && keyboardEnabled.isTrue) - ? (remoteCursorMoved.isTrue - ? SystemMouseCursors.none - : _buildCustomCursorLinux(context, s)) - : MouseCursor.defer, - onHover: (evt) { - pos.value = evt.position; - }, - child: _buildCrossScrollbar(_buildListener(imageWidget)))), + child: mouseRegion( + child: _buildCrossScrollbar(_buildListener(imageWidget))), ), ); } else { @@ -582,7 +582,7 @@ class ImagePaint extends StatelessWidget { painter: ImagePainter(image: m.image, x: c.x / s, y: c.y / s, scale: s), )); - return _buildListener(imageWidget); + return mouseRegion(child: _buildListener(imageWidget)); } } @@ -594,6 +594,8 @@ class ImagePaint extends StatelessWidget { } else { final key = cacheLinux.key(scale); cursor.addKeyLinux(key); + // debugPrint( + // 'REMOVE ME ================================= linux curor key: $key'); return FlutterCustomMemoryImageCursor( pixbuf: cacheLinux.data, key: key, diff --git a/flutter/lib/desktop/widgets/popup_menu.dart b/flutter/lib/desktop/widgets/popup_menu.dart index 02376ff71..3814561ee 100644 --- a/flutter/lib/desktop/widgets/popup_menu.dart +++ b/flutter/lib/desktop/widgets/popup_menu.dart @@ -8,7 +8,7 @@ import './material_mod_popup_menu.dart' as mod_menu; // https://stackoverflow.com/questions/68318314/flutter-popup-menu-inside-popup-menu class PopupMenuChildrenItem extends mod_menu.PopupMenuEntry { - PopupMenuChildrenItem({ + const PopupMenuChildrenItem({ key, this.height = kMinInteractiveDimension, this.padding, From ef0980a9b1535e02cfbf1c9774d49b1e0fc0275c Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 8 Sep 2022 21:05:26 -0700 Subject: [PATCH 0435/2015] flutter_desktop: fix local cursor (hotx,hoty) offset Signed-off-by: fufesou --- flutter/lib/desktop/pages/remote_page.dart | 4 ++-- flutter/lib/models/model.dart | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 96ade40f4..3d47dd1ce 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -599,8 +599,8 @@ class ImagePaint extends StatelessWidget { return FlutterCustomMemoryImageCursor( pixbuf: cacheLinux.data, key: key, - hotx: cacheLinux.hotx, - hoty: cacheLinux.hoty, + hotx: 0.0, + hoty: 0.0, imageWidth: (cacheLinux.width * scale).toInt(), imageHeight: (cacheLinux.height * scale).toInt(), ); diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index e907f3ded..6d8d68281 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -619,7 +619,7 @@ class CursorData { int _doubleToInt(double v) => (v * 10e6).round().toInt(); String key(double scale) => - '${peerId}_${id}_${_doubleToInt(hotx)}_${_doubleToInt(hoty)}_${_doubleToInt(width * scale)}_${_doubleToInt(height * scale)}'; + '${peerId}_${id}_${_doubleToInt(width * scale)}_${_doubleToInt(height * scale)}'; } class CursorModel with ChangeNotifier { From 1f591e0a6632aee4782b0803f9aeb3982f0f5dbe Mon Sep 17 00:00:00 2001 From: sandroid Date: Sat, 10 Sep 2022 00:44:35 +0200 Subject: [PATCH 0436/2015] Added flatpak feature Signed-off-by: sandroid --- Cargo.toml | 1 + src/ui.rs | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 88a7d963a..da36ca5b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ hbbs = [] cli = [] with_rc = ["simple_rc"] appimage = [] +flatpak = [] use_samplerate = ["samplerate"] use_rubato = ["rubato"] use_dasp = ["dasp"] diff --git a/src/ui.rs b/src/ui.rs index fd4bfe1a8..77d983e56 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -93,7 +93,11 @@ pub fn start(args: &mut [String]) { let prefix = std::env::var("APPDIR").unwrap_or("".to_string()); #[cfg(not(feature = "appimage"))] let prefix = "".to_string(); - sciter::set_library(&(prefix + "/usr/lib/rustdesk/libsciter-gtk.so")).ok(); + #[cfg(feature = "flatpak")] + let dir = "/app"; + #[cfg(not(feature = "flatpak"))] + let dir = "/usr"; + sciter::set_library(&(prefix + dir + "/lib/rustdesk/libsciter-gtk.so")).ok(); } // https://github.com/c-smile/sciter-sdk/blob/master/include/sciter-x-types.h // https://github.com/rustdesk/rustdesk/issues/132#issuecomment-886069737 From aa6e747e8aebb707ad756d2b5c432e235a32a0e8 Mon Sep 17 00:00:00 2001 From: fufesou Date: Sat, 10 Sep 2022 19:50:48 -0700 Subject: [PATCH 0437/2015] flutter_desktop: cursor image cache mismatch Signed-off-by: fufesou --- flutter/lib/common.dart | 2 + flutter/lib/desktop/pages/remote_page.dart | 4 +- flutter/lib/models/model.dart | 63 ++++++++++------------ flutter/lib/models/native_model.dart | 26 ++++----- flutter/lib/utils/image.dart | 49 +++++++++++++++++ 5 files changed, 94 insertions(+), 50 deletions(-) create mode 100644 flutter/lib/utils/image.dart diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 249b45a0c..73334baae 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -33,6 +33,8 @@ late final DesktopType? desktopType; typedef F = String Function(String); typedef FMethod = String Function(String, dynamic); +typedef StreamEventHandler = Future Function(Map); + late final iconKeyboard = MemoryImage(Uint8List.fromList(base64Decode( "iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAgVBMVEUAAAD///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////9d3yJTAAAAKnRSTlMA0Gd/0y8ILZgbJffDPUwV2nvzt+TMqZxyU7CMb1pYQyzsvKunkXE4AwJnNC24AAAA+0lEQVQ4y83O2U7DMBCF4ZMxk9rZk26kpQs7nPd/QJy4EiLbLf01N5Y/2YP/qxDFQvGB5NPC/ZpVnfJx4b5xyGfF95rkHvNCWH1u+N6J6T0sC7gqRy8uGPfBLEbozPXUjlkQKwGaFPNizwQbwkx0TDvhCii34ExZCSQVBdzIOEOyeclSHgBGXkpeygXSQgStACtWx4Z8rr8COHOvfEP/IbbsQAToFUAAV1M408IIjIGYAPoCSNRP7DQutfQTqxuAiH7UUg1FaJR2AGrrx52sK2ye28LZ0wBAEyR6y8X+NADhm1B4fgiiHXbRrTrxpwEY9RdM9wsepnvFHfUDwYEeiwAJr/gAAAAASUVORK5CYII="))); late final iconClipboard = MemoryImage(Uint8List.fromList(base64Decode( diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 3d47dd1ce..4321376a2 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -594,8 +594,8 @@ class ImagePaint extends StatelessWidget { } else { final key = cacheLinux.key(scale); cursor.addKeyLinux(key); - // debugPrint( - // 'REMOVE ME ================================= linux curor key: $key'); + debugPrint( + 'REMOVE ME ================================= linux curor key: $key'); return FlutterCustomMemoryImageCursor( pixbuf: cacheLinux.data, key: key, diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 6d8d68281..303157247 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -4,7 +4,6 @@ import 'dart:io'; import 'dart:math'; import 'dart:typed_data'; import 'dart:ui' as ui; -import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -21,6 +20,7 @@ import 'package:flutter_custom_cursor/flutter_custom_cursor.dart'; import '../common.dart'; import '../common/shared_state.dart'; +import '../utils/image.dart' as img; import '../mobile/widgets/dialog.dart'; import 'peer_model.dart'; import 'platform_model.dart'; @@ -127,8 +127,8 @@ class FfiModel with ChangeNotifier { _permissions.clear(); } - void Function(Map) startEventListener(String peerId) { - return (evt) { + StreamEventHandler startEventListener(String peerId) { + return (evt) async { var name = evt['name']; if (name == 'msgbox') { handleMsgBox(evt, peerId); @@ -140,11 +140,11 @@ class FfiModel with ChangeNotifier { } else if (name == 'switch_display') { handleSwitchDisplay(evt); } else if (name == 'cursor_data') { - parent.target?.cursorModel.updateCursorData(evt); + await parent.target?.cursorModel.updateCursorData(evt); } else if (name == 'cursor_id') { - parent.target?.cursorModel.updateCursorId(evt); + await parent.target?.cursorModel.updateCursorId(evt); } else if (name == 'cursor_position') { - parent.target?.cursorModel.updateCursorPosition(evt, peerId); + await parent.target?.cursorModel.updateCursorPosition(evt, peerId); } else if (name == 'clipboard') { Clipboard.setData(ClipboardData(text: evt['content'])); } else if (name == 'permission') { @@ -780,7 +780,7 @@ class CursorModel with ChangeNotifier { notifyListeners(); } - void updateCursorData(Map evt) { + updateCursorData(Map evt) async { var id = int.parse(evt['id']); _hotx = double.parse(evt['hotx']); _hoty = double.parse(evt['hoty']); @@ -789,34 +789,26 @@ class CursorModel with ChangeNotifier { List colors = json.decode(evt['colors']); final rgba = Uint8List.fromList(colors.map((s) => s as int).toList()); var pid = parent.target?.id; - ui.decodeImageFromPixels(rgba, width, height, ui.PixelFormat.rgba8888, - (image) { - () async { - if (parent.target?.id != pid) return; - _image = image; - _images[id] = Tuple3(image, _hotx, _hoty); - _updateCacheLinux(image, id, width, height); - try { - // my throw exception, because the listener maybe already dispose - notifyListeners(); - } catch (e) { - debugPrint('notify cursor: $e'); - } - }(); - }); + + final image = await img.decodeImageFromPixels( + rgba, width, height, ui.PixelFormat.rgba8888); + if (parent.target?.id != pid) return; + _image = image; + _images[id] = Tuple3(image, _hotx, _hoty); + await _updateCacheLinux(image, id, width, height); + try { + // my throw exception, because the listener maybe already dispose + notifyListeners(); + } catch (e) { + debugPrint('notify cursor: $e'); + } } - void _updateCacheLinux(ui.Image image, int id, int w, int h) async { - final data = await image.toByteData(format: ImageByteFormat.png); - late Uint8List? dataLinux; - if (data != null) { - dataLinux = data.buffer.asUint8List(); - } else { - dataLinux = null; - } + _updateCacheLinux(ui.Image image, int id, int w, int h) async { + final data = await image.toByteData(format: ui.ImageByteFormat.png); _cacheLinux = CursorData( peerId: this.id, - data: dataLinux, + data: data?.buffer.asUint8List(), id: id, hotx: _hotx, hoty: _hoty, @@ -826,9 +818,10 @@ class CursorModel with ChangeNotifier { _cacheMapLinux[id] = _cacheLinux!; } - void updateCursorId(Map evt) { - _cacheLinux = _cacheMapLinux[int.parse(evt['id'])]; - final tmp = _images[int.parse(evt['id'])]; + updateCursorId(Map evt) async { + final id = int.parse(evt['id']); + _cacheLinux = _cacheMapLinux[id]; + final tmp = _images[id]; if (tmp != null) { _image = tmp.item1; _hotx = tmp.item2; @@ -838,7 +831,7 @@ class CursorModel with ChangeNotifier { } /// Update the cursor position. - void updateCursorPosition(Map evt, String id) { + updateCursorPosition(Map evt, String id) async { _x = double.parse(evt['x']); _y = double.parse(evt['y']); try { diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index 0d356a7c2..a9081a160 100644 --- a/flutter/lib/models/native_model.dart +++ b/flutter/lib/models/native_model.dart @@ -30,15 +30,15 @@ class PlatformFFI { String _dir = ''; String _homeDir = ''; F2? _translate; - final _eventHandlers = Map>(); + final _eventHandlers = >{}; late RustdeskImpl _ffiBind; late String _appType; - void Function(Map)? _eventCallback; + StreamEventHandler? _eventCallback; PlatformFFI._(); static final PlatformFFI instance = PlatformFFI._(); - final _toAndroidChannel = MethodChannel("mChannel"); + final _toAndroidChannel = const MethodChannel("mChannel"); RustdeskImpl get ffiBind => _ffiBind; @@ -88,7 +88,7 @@ class PlatformFFI { } /// Init the FFI class, loads the native Rust core library. - Future init(String appType) async { + Future init(String appType) async { _appType = appType; // if (isDesktop) { // // TODO @@ -117,7 +117,7 @@ class PlatformFFI { _homeDir = (await getDownloadsDirectory())?.path ?? ""; } } catch (e) { - print("initialize failed: $e"); + debugPrint('initialize failed: $e'); } String id = 'NA'; String name = 'Flutter'; @@ -144,14 +144,14 @@ class PlatformFFI { name = macOsInfo.computerName; id = macOsInfo.systemGUID ?? ""; } - print( - "_appType:$_appType,info1-id:$id,info2-name:$name,dir:$_dir,homeDir:$_homeDir"); + debugPrint( + '_appType:$_appType,info1-id:$id,info2-name:$name,dir:$_dir,homeDir:$_homeDir'); await _ffiBind.mainDeviceId(id: id); await _ffiBind.mainDeviceName(name: name); await _ffiBind.mainSetHomeDir(home: _homeDir); await _ffiBind.mainInit(appDir: _dir); } catch (e) { - print("initialize failed: $e"); + debugPrint('initialize failed: $e'); } version = await getVersion(); } @@ -162,9 +162,9 @@ class PlatformFFI { final handlers = _eventHandlers[name]; if (handlers != null) { if (handlers.isNotEmpty) { - handlers.values.forEach((handler) { + for (var handler in handlers.values) { handler(evt); - }); + } return true; } } @@ -182,17 +182,17 @@ class PlatformFFI { // _tryHandle here may be more flexible than _eventCallback if (!_tryHandle(event)) { if (_eventCallback != null) { - _eventCallback!(event); + await _eventCallback!(event); } } } catch (e) { - print('json.decode fail(): $e'); + debugPrint('json.decode fail(): $e'); } } }(); } - void setEventCallback(void Function(Map) fun) async { + void setEventCallback(StreamEventHandler fun) async { _eventCallback = fun; } diff --git a/flutter/lib/utils/image.dart b/flutter/lib/utils/image.dart new file mode 100644 index 000000000..e92fdc6e5 --- /dev/null +++ b/flutter/lib/utils/image.dart @@ -0,0 +1,49 @@ +import 'dart:typed_data'; +import 'dart:ui' as ui; + +Future decodeImageFromPixels( + Uint8List pixels, + int width, + int height, + ui.PixelFormat format, { + int? rowBytes, + int? targetWidth, + int? targetHeight, + bool allowUpscaling = true, +}) async { + if (targetWidth != null) { + assert(allowUpscaling || targetWidth <= width); + } + if (targetHeight != null) { + assert(allowUpscaling || targetHeight <= height); + } + + final ui.ImmutableBuffer buffer = + await ui.ImmutableBuffer.fromUint8List(pixels); + final ui.ImageDescriptor descriptor = ui.ImageDescriptor.raw( + buffer, + width: width, + height: height, + rowBytes: rowBytes, + pixelFormat: format, + ); + if (!allowUpscaling) { + if (targetWidth != null && targetWidth! > descriptor.width) { + targetWidth = descriptor.width; + } + if (targetHeight != null && targetHeight! > descriptor.height) { + targetHeight = descriptor.height; + } + } + + final ui.Codec codec = await descriptor.instantiateCodec( + targetWidth: targetWidth, + targetHeight: targetHeight, + ); + + final ui.FrameInfo frameInfo = await codec.getNextFrame(); + codec.dispose(); + buffer.dispose(); + descriptor.dispose(); + return frameInfo.image; +} From efe6d080f37b9d371f95224257f098a284df95fa Mon Sep 17 00:00:00 2001 From: fufesou Date: Sun, 11 Sep 2022 19:52:38 -0700 Subject: [PATCH 0438/2015] flutter_desktop: set event func to async Signed-off-by: fufesou --- flutter/lib/desktop/pages/remote_page.dart | 2 - flutter/lib/main.dart | 4 +- flutter/lib/mobile/pages/remote_page.dart | 5 +- flutter/lib/models/model.dart | 55 ++++++++++------------ flutter/lib/models/native_model.dart | 8 ++-- flutter/lib/models/peer_model.dart | 4 +- flutter/lib/utils/image.dart | 4 +- 7 files changed, 37 insertions(+), 45 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 4321376a2..34c365d68 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -594,8 +594,6 @@ class ImagePaint extends StatelessWidget { } else { final key = cacheLinux.key(scale); cursor.addKeyLinux(key); - debugPrint( - 'REMOVE ME ================================= linux curor key: $key'); return FlutterCustomMemoryImageCursor( pixbuf: cacheLinux.data, key: key, diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 8efc7c6ad..0ae5b583d 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -273,13 +273,13 @@ _keepScaleBuilder() { _registerEventHandler() { if (desktopType != DesktopType.main) { - platformFFI.registerEventHandler('theme', 'theme', (evt) { + platformFFI.registerEventHandler('theme', 'theme', (evt) async { String? dark = evt['dark']; if (dark != null) { MyTheme.changeTo(dark == 'true'); } }); - platformFFI.registerEventHandler('language', 'language', (_) { + platformFFI.registerEventHandler('language', 'language', (_) async { Get.forceAppUpdate(); }); } diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 419a98f3a..dd3742d32 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -1085,9 +1085,8 @@ void showOptions(String id, OverlayDialogManager dialogManager) async { mainAxisSize: MainAxisSize.min, children: displays + [ - getRadio('Original', 'original', viewStyle, setViewStyle), - getRadio('Shrink', 'shrink', viewStyle, setViewStyle), - getRadio('Stretch', 'stretch', viewStyle, setViewStyle), + getRadio('Scale Original', 'original', viewStyle, setViewStyle), + getRadio('Scale adaptive', 'adaptive', viewStyle, setViewStyle), Divider(color: MyTheme.border), getRadio('Good image quality', 'best', quality, setQuality), getRadio('Balanced', 'balanced', quality, setQuality), diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 303157247..8416c5a8c 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -358,11 +358,11 @@ class ImageModel with ChangeNotifier { }); } - void update(ui.Image? image, double tabBarHeight) { + void update(ui.Image? image, double tabBarHeight) async { if (_image == null && image != null) { if (isWebDesktop || isDesktop) { - parent.target?.canvasModel.updateViewStyle(); - parent.target?.canvasModel.updateScrollStyle(); + await parent.target?.canvasModel.updateViewStyle(); + await parent.target?.canvasModel.updateScrollStyle(); } else { final size = MediaQueryData.fromWindow(ui.window).size; final canvasWidth = size.width; @@ -372,14 +372,12 @@ class ImageModel with ChangeNotifier { parent.target?.canvasModel.scale = min(xscale, yscale); } if (parent.target != null) { - initializeCursorAndCanvas(parent.target!); + await initializeCursorAndCanvas(parent.target!); + } + if (parent.target?.ffiModel.isPeerAndroid ?? false) { + bind.sessionPeerOption(id: _id, name: 'view-style', value: 'adaptive'); + parent.target?.canvasModel.updateViewStyle(); } - Future.delayed(Duration(milliseconds: 1), () { - if (parent.target?.ffiModel.isPeerAndroid ?? false) { - bind.sessionPeerOption(id: _id, name: 'view-style', value: 'shrink'); - parent.target?.canvasModel.updateViewStyle(); - } - }); } _image = image; if (image != null) notifyListeners(); @@ -445,7 +443,7 @@ class CanvasModel with ChangeNotifier { double get scrollX => _scrollX; double get scrollY => _scrollY; - void updateViewStyle() async { + updateViewStyle() async { final style = await bind.sessionGetOption(id: id, arg: 'view-style'); if (style == null) { return; @@ -457,7 +455,6 @@ class CanvasModel with ChangeNotifier { final s2 = size.height / getDisplayHeight(); _scale = s1 < s2 ? s1 : s2; } - _x = (size.width - getDisplayWidth() * _scale) / 2; _y = (size.height - getDisplayHeight() * _scale) / 2; notifyListeners(); @@ -475,7 +472,7 @@ class CanvasModel with ChangeNotifier { notifyListeners(); } - void update(double x, double y, double scale) { + update(double x, double y, double scale) { _x = x; _y = y; _scale = scale; @@ -508,19 +505,9 @@ class CanvasModel with ChangeNotifier { var dxOffset = 0; var dyOffset = 0; if (dw > size.width) { - final X_debugNanOrInfinite = x - dw * (x / size.width) - _x; - if (X_debugNanOrInfinite.isInfinite || X_debugNanOrInfinite.isNaN) { - debugPrint( - 'REMOVE ME ============================ X_debugNanOrInfinite $x,$dw,$_scale,${size.width},$_x'); - } dxOffset = (x - dw * (x / size.width) - _x).toInt(); } if (dh > size.height) { - final Y_debugNanOrInfinite = y - dh * (y / size.height) - _y; - if (Y_debugNanOrInfinite.isInfinite || Y_debugNanOrInfinite.isNaN) { - debugPrint( - 'REMOVE ME ============================ Y_debugNanOrInfinite $y,$dh,$_scale,${size.height},$_y'); - } dyOffset = (y - dh * (y / size.height) - _y).toInt(); } _x += dxOffset; @@ -789,7 +776,6 @@ class CursorModel with ChangeNotifier { List colors = json.decode(evt['colors']); final rgba = Uint8List.fromList(colors.map((s) => s as int).toList()); var pid = parent.target?.id; - final image = await img.decodeImageFromPixels( rgba, width, height, ui.PixelFormat.rgba8888); if (parent.target?.id != pid) return; @@ -1128,9 +1114,9 @@ class FFI { if (message is Event) { try { Map event = json.decode(message.field0); - cb(event); + await cb(event); } catch (e) { - print('json.decode fail(): $e'); + debugPrint('json.decode fail(): $e'); } } else if (message is Rgba) { imageModel.onRgba(message.field0, tabBarHeight); @@ -1292,6 +1278,15 @@ class Display { double y = 0; int width = 0; int height = 0; + + Display() { + width = (isDesktop || isWebDesktop) + ? kDesktopDefaultDisplayWidth + : kMobileDefaultDisplayWidth; + height = (isDesktop || isWebDesktop) + ? kDesktopDefaultDisplayHeight + : kMobileDefaultDisplayHeight; + } } class PeerInfo { @@ -1304,8 +1299,8 @@ class PeerInfo { List displays = []; } -Future savePreference(String id, double xCursor, double yCursor, - double xCanvas, double yCanvas, double scale, int currentDisplay) async { +savePreference(String id, double xCursor, double yCursor, double xCanvas, + double yCanvas, double scale, int currentDisplay) async { SharedPreferences prefs = await SharedPreferences.getInstance(); final p = {}; p['xCursor'] = xCursor; @@ -1326,12 +1321,12 @@ Future?> getPreference(String id) async { return m; } -void removePreference(String id) async { +removePreference(String id) async { SharedPreferences prefs = await SharedPreferences.getInstance(); prefs.remove('peer$id'); } -void initializeCursorAndCanvas(FFI ffi) async { +initializeCursorAndCanvas(FFI ffi) async { var p = await getPreference(ffi.id); int currentDisplay = 0; if (p != null) { diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index a9081a160..b1091c8ba 100644 --- a/flutter/lib/models/native_model.dart +++ b/flutter/lib/models/native_model.dart @@ -22,7 +22,7 @@ class RgbaFrame extends Struct { typedef F2 = Pointer Function(Pointer, Pointer); typedef F3 = void Function(Pointer, Pointer); -typedef HandleEvent = void Function(Map evt); +typedef HandleEvent = Future Function(Map evt); /// FFI wrapper around the native Rust core. /// Hides the platform differences. @@ -156,14 +156,14 @@ class PlatformFFI { version = await getVersion(); } - bool _tryHandle(Map evt) { + Future _tryHandle(Map evt) async { final name = evt['name']; if (name != null) { final handlers = _eventHandlers[name]; if (handlers != null) { if (handlers.isNotEmpty) { for (var handler in handlers.values) { - handler(evt); + await handler(evt); } return true; } @@ -180,7 +180,7 @@ class PlatformFFI { try { Map event = json.decode(message); // _tryHandle here may be more flexible than _eventCallback - if (!_tryHandle(event)) { + if (!await _tryHandle(event)) { if (_eventCallback != null) { await _eventCallback!(event); } diff --git a/flutter/lib/models/peer_model.dart b/flutter/lib/models/peer_model.dart index 79b71e6db..0857bfcf8 100644 --- a/flutter/lib/models/peer_model.dart +++ b/flutter/lib/models/peer_model.dart @@ -40,10 +40,10 @@ class Peers extends ChangeNotifier { static const _cbQueryOnlines = 'callback_query_onlines'; Peers({required this.name, required this.peers, required this.loadEvent}) { - platformFFI.registerEventHandler(_cbQueryOnlines, name, (evt) { + platformFFI.registerEventHandler(_cbQueryOnlines, name, (evt) async { _updateOnlineState(evt); }); - platformFFI.registerEventHandler(loadEvent, name, (evt) { + platformFFI.registerEventHandler(loadEvent, name, (evt) async { _updatePeers(evt); }); } diff --git a/flutter/lib/utils/image.dart b/flutter/lib/utils/image.dart index e92fdc6e5..1f0d5b0cd 100644 --- a/flutter/lib/utils/image.dart +++ b/flutter/lib/utils/image.dart @@ -28,10 +28,10 @@ Future decodeImageFromPixels( pixelFormat: format, ); if (!allowUpscaling) { - if (targetWidth != null && targetWidth! > descriptor.width) { + if (targetWidth != null && targetWidth > descriptor.width) { targetWidth = descriptor.width; } - if (targetHeight != null && targetHeight! > descriptor.height) { + if (targetHeight != null && targetHeight > descriptor.height) { targetHeight = descriptor.height; } } From 86a9060e169545666216db6f93ffdcc9f9deeb35 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Mon, 12 Sep 2022 12:31:02 +0800 Subject: [PATCH 0439/2015] remove flutter from default features --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 88a7d963a..d778cc279 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,7 @@ use_samplerate = ["samplerate"] use_rubato = ["rubato"] use_dasp = ["dasp"] flutter = ["flutter_rust_bridge"] -default = ["use_dasp","flutter"] +default = ["use_dasp"] hwcodec = ["scrap/hwcodec"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From ed58f0745bac2fc2300e7a7dd5e7e8debdafb929 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Mon, 12 Sep 2022 12:50:51 +0800 Subject: [PATCH 0440/2015] revert back to no flutter version --- .github/workflows/ci.yml | 66 ++++++++++------------------------------ 1 file changed, 16 insertions(+), 50 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 39fca8c5d..cd8282187 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -78,45 +78,10 @@ jobs: shell: bash run: | case ${{ matrix.job.target }} in - x86_64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev;; + x86_64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake ;; # arm-unknown-linux-*) sudo apt-get -y update ; sudo apt-get -y install gcc-arm-linux-gnueabihf ;; # aarch64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt-get -y install gcc-aarch64-linux-gnu ;; esac - - - name: Install flutter - uses: subosito/flutter-action@v2 - with: - channel: 'stable' - - - name: Install Rust toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - target: ${{ matrix.job.target }} - override: true - profile: minimal # minimal component installation (ie, no documentation) - - - name: Install flutter rust bridge deps - run: | - dart pub global activate ffigen --version 5.0.1 - # flutter_rust_bridge - pushd /tmp && git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1 && popd - pushd /tmp/flutter_rust_bridge/frb_codegen && cargo install --path . && popd - pushd flutter && flutter pub get && popd - ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart - - - name: Install corrosion - run: | - mkdir /tmp/corrosion - pushd /tmp/corrosion - git clone https://github.com/corrosion-rs/corrosion.git - # Optionally, specify -DCMAKE_INSTALL_PREFIX=. You can install Corrosion anyway - cmake -Scorrosion -Bbuild -DCMAKE_BUILD_TYPE=Release - cmake --build build --config Release - # This next step may require sudo or admin privileges if you're installing to a system location, - # which is the default. - sudo cmake --install build --config Release - popd - name: Restore from cache and install vcpkg uses: lukka/run-vcpkg@v7 @@ -127,7 +92,15 @@ jobs: - name: Install vcpkg dependencies run: | $VCPKG_ROOT/vcpkg install libvpx libyuv opus - shell: bash + shell: bash + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: ${{ matrix.job.target }} + override: true + profile: minimal # minimal component installation (ie, no documentation) - name: Show version information (Rust, cargo, GCC) shell: bash @@ -141,19 +114,12 @@ jobs: - uses: Swatinem/rust-cache@v1 -# - name: Build -# uses: actions-rs/cargo@v1 -# with: -# use-cross: ${{ matrix.job.use-cross }} -# command: build -# args: --locked --release --target=${{ matrix.job.target }} --features flutter -v - - - name: Build Flutter - run: | - pushd flutter - flutter pub get - flutter build linux --release -v - popd + - name: Build + uses: actions-rs/cargo@v1 + with: + use-cross: ${{ matrix.job.use-cross }} + command: build + args: --locked --release --target=${{ matrix.job.target }} # - name: Strip debug information from executable # id: strip From 5926892734a6a01578e37ff5bf1bcfd76e41ab99 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Mon, 12 Sep 2022 13:06:58 +0800 Subject: [PATCH 0441/2015] miss libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev; --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cd8282187..7cdca74ac 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -78,7 +78,7 @@ jobs: shell: bash run: | case ${{ matrix.job.target }} in - x86_64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake ;; + x86_64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev;; # arm-unknown-linux-*) sudo apt-get -y update ; sudo apt-get -y install gcc-arm-linux-gnueabihf ;; # aarch64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt-get -y install gcc-aarch64-linux-gnu ;; esac From c479e0871d71badaf638110d863815c3c965835c Mon Sep 17 00:00:00 2001 From: fufesou Date: Mon, 12 Sep 2022 01:35:56 -0700 Subject: [PATCH 0442/2015] flutter_desktop: fix scroll to center when mouse hover menu bar Signed-off-by: fufesou --- flutter/lib/desktop/pages/remote_page.dart | 8 +- flutter/lib/models/model.dart | 92 +++++++++++----------- flutter/lib/models/native_model.dart | 16 ++-- 3 files changed, 59 insertions(+), 57 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 34c365d68..29e99e6a7 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -612,12 +612,12 @@ class ImagePaint extends StatelessWidget { cursorOverImage.value ? const NeverScrollableScrollPhysics() : null; return Scrollbar( controller: _vertical, - thumbVisibility: true, - trackVisibility: true, + thumbVisibility: false, + trackVisibility: false, child: Scrollbar( controller: _horizontal, - thumbVisibility: true, - trackVisibility: true, + thumbVisibility: false, + trackVisibility: false, notificationPredicate: (notif) => notif.depth == 1, child: SingleChildScrollView( controller: _vertical, diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 8416c5a8c..7fe2b6767 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -25,7 +25,7 @@ import '../mobile/widgets/dialog.dart'; import 'peer_model.dart'; import 'platform_model.dart'; -typedef HandleMsgBox = void Function(Map evt, String id); +typedef HandleMsgBox = Function(Map evt, String id); bool _waitForImage = false; class FfiModel with ChangeNotifier { @@ -65,14 +65,14 @@ class FfiModel with ChangeNotifier { clear(); } - void toggleTouchMode() { + toggleTouchMode() { if (!isPeerAndroid) { _touchMode = !_touchMode; notifyListeners(); } } - void updatePermission(Map evt, String id) { + updatePermission(Map evt, String id) { evt.forEach((k, v) { if (k == 'name' || k.isEmpty) return; _permissions[k] = v == 'true'; @@ -82,13 +82,13 @@ class FfiModel with ChangeNotifier { notifyListeners(); } - void updateUser() { + updateUser() { notifyListeners(); } bool keyboard() => _permissions['keyboard'] != false; - void clear() { + clear() { _pi = PeerInfo(); _display = Display(); _waitForImage = false; @@ -100,7 +100,7 @@ class FfiModel with ChangeNotifier { clearPermissions(); } - void setConnectionType(String peerId, bool secure, bool direct) { + setConnectionType(String peerId, bool secure, bool direct) { _secure = secure; _direct = direct; try { @@ -122,7 +122,7 @@ class FfiModel with ChangeNotifier { } } - void clearPermissions() { + clearPermissions() { _inputBlocked = false; _permissions.clear(); } @@ -184,11 +184,11 @@ class FfiModel with ChangeNotifier { } /// Bind the event listener to receive events from the Rust core. - void updateEventListener(String peerId) { + updateEventListener(String peerId) { platformFFI.setEventCallback(startEventListener(peerId)); } - void handleSwitchDisplay(Map evt) { + handleSwitchDisplay(Map evt) { final oldOrientation = _display.width > _display.height; var old = _pi.currentDisplay; _pi.currentDisplay = int.parse(evt['display']); @@ -208,7 +208,7 @@ class FfiModel with ChangeNotifier { } /// Handle the message box event based on [evt] and [id]. - void handleMsgBox(Map evt, String id) { + handleMsgBox(Map evt, String id) { if (parent.target == null) return; final dialogManager = parent.target!.dialogManager; var type = evt['type']; @@ -227,8 +227,8 @@ class FfiModel with ChangeNotifier { } /// Show a message box with [type], [title] and [text]. - void showMsgBox(String id, String type, String title, String text, - bool hasRetry, OverlayDialogManager dialogManager, + showMsgBox(String id, String type, String title, String text, bool hasRetry, + OverlayDialogManager dialogManager, {bool? hasCancel}) { msgBox(type, title, text, dialogManager, hasCancel: hasCancel); _timer?.cancel(); @@ -246,7 +246,7 @@ class FfiModel with ChangeNotifier { } /// Handle the peer info event based on [evt]. - void handlePeerInfo(Map evt, String peerId) async { + 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) bind.mainLoadRecentPeers(); @@ -337,7 +337,7 @@ class ImageModel with ChangeNotifier { ImageModel(this.parent); - void onRgba(Uint8List rgba, double tabBarHeight) { + onRgba(Uint8List rgba, double tabBarHeight) { if (_waitForImage) { _waitForImage = false; parent.target?.dialogManager.dismissAll(); @@ -358,7 +358,7 @@ class ImageModel with ChangeNotifier { }); } - void update(ui.Image? image, double tabBarHeight) async { + update(ui.Image? image, double tabBarHeight) async { if (_image == null && image != null) { if (isWebDesktop || isDesktop) { await parent.target?.canvasModel.updateViewStyle(); @@ -425,6 +425,7 @@ class CanvasModel with ChangeNotifier { // scroll offset y percent double _scrollY = 0.0; ScrollStyle _scrollStyle = ScrollStyle.scrollauto; + String? _viewStyle; WeakReference parent; @@ -445,7 +446,7 @@ class CanvasModel with ChangeNotifier { updateViewStyle() async { final style = await bind.sessionGetOption(id: id, arg: 'view-style'); - if (style == null) { + if (style == null || _viewStyle == style) { return; } @@ -455,6 +456,7 @@ class CanvasModel with ChangeNotifier { final s2 = size.height / getDisplayHeight(); _scale = s1 < s2 ? s1 : s2; } + _viewStyle = style; _x = (size.width - getDisplayWidth() * _scale) / 2; _y = (size.height - getDisplayHeight() * _scale) / 2; notifyListeners(); @@ -498,7 +500,7 @@ class CanvasModel with ChangeNotifier { return Size(size.width, size.height - tabBarHeight); } - void moveDesktopMouse(double x, double y) { + moveDesktopMouse(double x, double y) { // On mobile platforms, move the canvas with the cursor. final dw = getDisplayWidth() * _scale; final dh = getDisplayHeight() * _scale; @@ -536,12 +538,12 @@ class CanvasModel with ChangeNotifier { notifyListeners(); } - void panX(double dx) { + panX(double dx) { _x += dx; notifyListeners(); } - void resetOffset() { + resetOffset() { if (isWebDesktop) { updateViewStyle(); } else { @@ -551,12 +553,12 @@ class CanvasModel with ChangeNotifier { notifyListeners(); } - void panY(double dy) { + panY(double dy) { _y += dy; notifyListeners(); } - void updateScale(double v) { + updateScale(double v) { if (parent.target?.imageModel.image == null) return; final offset = parent.target?.cursorModel.offset ?? const Offset(0, 0); var r = parent.target?.cursorModel.getVisibleRect() ?? Rect.zero; @@ -575,7 +577,7 @@ class CanvasModel with ChangeNotifier { notifyListeners(); } - void clear([bool notify = false]) { + clear([bool notify = false]) { _x = 0; _y = 0; _scale = 1.0; @@ -664,18 +666,18 @@ class CursorModel with ChangeNotifier { return h - thresh; } - void touch(double x, double y, MouseButtons button) { + touch(double x, double y, MouseButtons button) { moveLocal(x, y); parent.target?.moveMouse(_x, _y); parent.target?.tap(button); } - void move(double x, double y) { + move(double x, double y) { moveLocal(x, y); parent.target?.moveMouse(_x, _y); } - void moveLocal(double x, double y) { + moveLocal(double x, double y) { final scale = parent.target?.canvasModel.scale ?? 1.0; final xoffset = parent.target?.canvasModel.x ?? 0; final yoffset = parent.target?.canvasModel.y ?? 0; @@ -684,7 +686,7 @@ class CursorModel with ChangeNotifier { notifyListeners(); } - void reset() { + reset() { _x = _displayOriginX; _y = _displayOriginY; parent.target?.moveMouse(_x, _y); @@ -692,7 +694,7 @@ class CursorModel with ChangeNotifier { notifyListeners(); } - void updatePan(double dx, double dy, bool touchMode) { + updatePan(double dx, double dy, bool touchMode) { if (parent.target?.imageModel.image == null) return; if (touchMode) { final scale = parent.target?.canvasModel.scale ?? 1.0; @@ -828,7 +830,7 @@ class CursorModel with ChangeNotifier { notifyListeners(); } - void updateDisplayOrigin(double x, double y) { + updateDisplayOrigin(double x, double y) { _displayOriginX = x; _displayOriginY = y; _x = x + 1; @@ -838,7 +840,7 @@ class CursorModel with ChangeNotifier { notifyListeners(); } - void updateDisplayOriginWithCursor( + updateDisplayOriginWithCursor( double x, double y, double xCursor, double yCursor) { _displayOriginX = x; _displayOriginY = y; @@ -848,7 +850,7 @@ class CursorModel with ChangeNotifier { notifyListeners(); } - void clear() { + clear() { _x = -10000; _x = -10000; _image = null; @@ -859,7 +861,7 @@ class CursorModel with ChangeNotifier { _cacheMapLinux.clear(); } - void _clearCacheLinux() { + _clearCacheLinux() { final cachedKeys = {...cachedKeysLinux}; for (var key in cachedKeys) { customCursorController.freeCache(key); @@ -969,13 +971,13 @@ class FFI { } /// Send a mouse tap event(down and up). - void tap(MouseButtons button) { + tap(MouseButtons button) { sendMouse('down', button); sendMouse('up', button); } /// Send scroll event with scroll distance [y]. - void scroll(int y) { + scroll(int y) { bind.sessionSendMouse( id: id, msg: json @@ -983,13 +985,13 @@ class FFI { } /// Reconnect to the remote peer. - // static void reconnect() { + // static reconnect() { // setByName('reconnect'); // parent.target?.ffiModel.clearPermissions(); // } /// Reset key modifiers to false, including [shift], [ctrl], [alt] and [command]. - void resetModifiers() { + resetModifiers() { shift = ctrl = alt = command = false; } @@ -1003,7 +1005,7 @@ class FFI { } /// Send mouse press event. - void sendMouse(String type, MouseButtons button) { + sendMouse(String type, MouseButtons button) { if (!ffiModel.keyboard()) return; bind.sessionSendMouse( id: id, @@ -1011,7 +1013,7 @@ class FFI { } // Raw Key - void inputRawKey(String name, int keyCode, int scanCode, bool down) { + inputRawKey(String name, int keyCode, int scanCode, bool down) { bind.sessionHandleFlutterKeyEvent( id: id, name: name, @@ -1024,7 +1026,7 @@ class FFI { return bind.sessionGetKeyboardName(id: id); } - void enterOrLeave(bool enter) { + enterOrLeave(bool enter) { // Fix status if (!enter) { resetModifiers(); @@ -1035,7 +1037,7 @@ class FFI { /// Send key stroke event. /// [down] indicates the key's state(down or up). /// [press] indicates a click event(down and up). - void inputKey(String name, {bool? down, bool? press}) { + inputKey(String name, {bool? down, bool? press}) { if (!ffiModel.keyboard()) return; // final Map out = Map(); // out['name'] = name; @@ -1061,7 +1063,7 @@ class FFI { } /// Send mouse movement event with distance in [x] and [y]. - void moveMouse(double x, double y) { + moveMouse(double x, double y) { if (!ffiModel.keyboard()) return; var x2 = x.toInt(); var y2 = y.toInt(); @@ -1087,7 +1089,7 @@ class FFI { } /// Connect with the given [id]. Only transfer file if [isFileTransfer], only port forward if [isPortForward]. - void connect(String id, + connect(String id, {bool isFileTransfer = false, bool isPortForward = false, double tabBarHeight = 0.0}) { @@ -1131,7 +1133,7 @@ class FFI { } /// Login with [password], choose if the client should [remember] it. - void login(String id, String password, bool remember) { + login(String id, String password, bool remember) { bind.sessionLogin(id: id, password: password, remember: remember); } @@ -1159,7 +1161,7 @@ class FFI { // } /// Send **set** command to the Rust core based on [name] and [value]. - // void setByName(String name, [String value = '']) { + // setByName(String name, [String value = '']) { // platformFFI.setByName(name, value); // } @@ -1241,7 +1243,7 @@ class FFI { } } - void setMethodCallHandler(FMethod callback) { + setMethodCallHandler(FMethod callback) { platformFFI.setMethodCallHandler(callback); } @@ -1261,7 +1263,7 @@ class FFI { return input; } - void setDefaultAudioInput(String input) { + setDefaultAudioInput(String input) { bind.mainSetOption(key: 'audio-input', value: input); } diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index b1091c8ba..964979731 100644 --- a/flutter/lib/models/native_model.dart +++ b/flutter/lib/models/native_model.dart @@ -38,7 +38,7 @@ class PlatformFFI { PlatformFFI._(); static final PlatformFFI instance = PlatformFFI._(); - final _toAndroidChannel = const MethodChannel("mChannel"); + final _toAndroidChannel = const MethodChannel('mChannel'); RustdeskImpl get ffiBind => _ffiBind; @@ -97,13 +97,13 @@ class PlatformFFI { final dylib = Platform.isAndroid ? DynamicLibrary.open('librustdesk.so') : Platform.isLinux - ? DynamicLibrary.open("librustdesk.so") + ? DynamicLibrary.open('librustdesk.so') : Platform.isWindows - ? DynamicLibrary.open("librustdesk.dll") + ? DynamicLibrary.open('librustdesk.dll') : Platform.isMacOS - ? DynamicLibrary.open("librustdesk.dylib") + ? DynamicLibrary.open('librustdesk.dylib') : DynamicLibrary.process(); - debugPrint('initializing FFI ${_appType}'); + debugPrint('initializing FFI $_appType'); try { _translate = dylib.lookupFunction('translate'); _dir = (await getApplicationDocumentsDirectory()).path; @@ -114,7 +114,7 @@ class PlatformFFI { // only support for android _homeDir = (await ExternalPath.getExternalStorageDirectories())[0]; } else { - _homeDir = (await getDownloadsDirectory())?.path ?? ""; + _homeDir = (await getDownloadsDirectory())?.path ?? ''; } } catch (e) { debugPrint('initialize failed: $e'); @@ -129,7 +129,7 @@ class PlatformFFI { androidVersion = androidInfo.version.sdkInt ?? 0; } else if (Platform.isIOS) { IosDeviceInfo iosInfo = await deviceInfo.iosInfo; - name = iosInfo.utsname.machine ?? ""; + name = iosInfo.utsname.machine ?? ''; id = iosInfo.identifierForVendor.hashCode.toString(); } else if (Platform.isLinux) { LinuxDeviceInfo linuxInfo = await deviceInfo.linuxInfo; @@ -142,7 +142,7 @@ class PlatformFFI { } else if (Platform.isMacOS) { MacOsDeviceInfo macOsInfo = await deviceInfo.macOsInfo; name = macOsInfo.computerName; - id = macOsInfo.systemGUID ?? ""; + id = macOsInfo.systemGUID ?? ''; } debugPrint( '_appType:$_appType,info1-id:$id,info2-name:$name,dir:$_dir,homeDir:$_homeDir'); From a957f894b754b8a79678ccb5367edbd13711a909 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 12 Sep 2022 15:05:04 +0800 Subject: [PATCH 0443/2015] add: dl libs fix: flutter ci Signed-off-by: Kingtous add: flutter ci Signed-off-by: Kingtous --- .github/workflows/flutter-ci.yml | 97 ++++++++++++++++++++++++++++++++ flutter/linux/CMakeLists.txt | 31 +++------- 2 files changed, 106 insertions(+), 22 deletions(-) create mode 100644 .github/workflows/flutter-ci.yml diff --git a/.github/workflows/flutter-ci.yml b/.github/workflows/flutter-ci.yml new file mode 100644 index 000000000..9cdd1fb91 --- /dev/null +++ b/.github/workflows/flutter-ci.yml @@ -0,0 +1,97 @@ +name: Flutter CI + +on: + workflow_dispatch: + pull_request: + push: + branches: + - master + tags: + - '*' + +jobs: + build: + name: ${{ matrix.job.target }} (${{ matrix.job.os }}) + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: false + matrix: + job: + # - { target: aarch64-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } + # - { target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true } + # - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true } + # - { target: i686-pc-windows-msvc , os: windows-2019 } + # - { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } + # - { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } + # - { target: x86_64-apple-darwin , os: macos-10.15 } + # - { target: x86_64-pc-windows-gnu , os: windows-2019 } + # - { target: x86_64-pc-windows-msvc , os: windows-2019 } + - { target: x86_64-unknown-linux-gnu , os: ubuntu-20.04 } + # - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } + steps: + - name: Checkout source code + uses: actions/checkout@v2 + + - name: Install prerequisites + shell: bash + run: | + case ${{ matrix.job.target }} in + x86_64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev;; + # arm-unknown-linux-*) sudo apt-get -y update ; sudo apt-get -y install gcc-arm-linux-gnueabihf ;; + # aarch64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt-get -y install gcc-aarch64-linux-gnu ;; + esac + + - name: Install flutter + uses: subosito/flutter-action@v2 + with: + channel: 'stable' + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: ${{ matrix.job.target }} + override: true + profile: minimal # minimal component installation (ie, no documentation) + + - uses: Swatinem/rust-cache@v1 + + - name: Install flutter rust bridge deps + run: | + dart pub global activate ffigen --version 5.0.1 + # flutter_rust_bridge + pushd /tmp && git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1 && popd + pushd /tmp/flutter_rust_bridge/frb_codegen && cargo install --path . && popd + pushd flutter && flutter pub get && popd + ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart + + - name: Restore from cache and install vcpkg + uses: lukka/run-vcpkg@v7 + with: + setupOnly: true + vcpkgGitCommitId: '1d4128f08e30cec31b94500840c7eca8ebc579cb' + + - name: Install vcpkg dependencies + run: | + $VCPKG_ROOT/vcpkg install libvpx libyuv opus + shell: bash + + - name: Show version information (Rust, cargo, GCC) + shell: bash + run: | + gcc --version || true + rustup -V + rustup toolchain list + rustup default + cargo -V + rustc -V + + - name: Build rustdesk ffi lib + run: cargo build --features flutter --lib + + - name: Build Flutter + run: | + pushd flutter + flutter pub get + flutter build linux --debug -v + popd \ No newline at end of file diff --git a/flutter/linux/CMakeLists.txt b/flutter/linux/CMakeLists.txt index 8484ca5b6..08d557f1b 100644 --- a/flutter/linux/CMakeLists.txt +++ b/flutter/linux/CMakeLists.txt @@ -56,26 +56,6 @@ pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") -# flutter_rust_bridge -find_package(Corrosion REQUIRED) - -corrosion_import_crate(MANIFEST_PATH ../../Cargo.toml - # Equivalent to --all-features passed to cargo build -# [ALL_FEATURES] - # Equivalent to --no-default-features passed to cargo build -# [NO_DEFAULT_FEATURES] - # Disable linking of standard libraries (required for no_std crates). -# [NO_STD] - # Specify cargo build profile (e.g. release or a custom profile) -# [PROFILE ] - # Only import the specified crates from a workspace -# [CRATES ... ] - # Enable the specified features -# [FEATURES ... ] -) - -set(BASE_RUSTDESK "librustdesk") - # Define the application target. To change its name, change BINARY_NAME above, # not the value here, or `flutter run` will no longer work. # @@ -93,7 +73,7 @@ apply_standard_settings(${BINARY_NAME}) # Add dependency libraries. Add any application-specific dependencies here. target_link_libraries(${BINARY_NAME} PRIVATE flutter) target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) -target_link_libraries(${BINARY_NAME} PRIVATE ${BASE_RUSTDESK}) +target_link_libraries(${BINARY_NAME} PRIVATE ${CMAKE_DL_LIBS}) # target_link_libraries(${BINARY_NAME} PRIVATE librustdesk) # Run the Flutter tool portions of the build. This must not be removed. @@ -144,7 +124,14 @@ foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) COMPONENT Runtime) endforeach(bundled_library) -install(FILES $ DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime RENAME librustdesk.so) +# flutter_rust_bridge +set(RUSTDESK_LIB_BUILD_TYPE debug) +string(TOLOWER ${CMAKE_BUILD_TYPE} ${RUSTDESK_LIB_BUILD_TYPE}) +message(STATUS "rustdesk lib build type: ${RUSTDESK_LIB_BUILD_TYPE}") + +set(RUSTDESK_LIB "../../target/${RUSTDESK_LIB_BUILD_TYPE}/liblibrustdesk.so") +install(FILES "${RUSTDESK_LIB}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" +COMPONENT Runtime RENAME librustdesk.so) # Fully re-copy the assets directory on each build to avoid having stale files # from a previous install. From 74201b71b4962f4b5b75dcaf3b81f08300b884bb Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 12 Sep 2022 18:04:00 +0800 Subject: [PATCH 0444/2015] opt: windows build type & use generator expression --- flutter/linux/CMakeLists.txt | 2 +- flutter/windows/CMakeLists.txt | 7 +++++++ flutter/windows/runner/CMakeLists.txt | 19 ------------------- 3 files changed, 8 insertions(+), 20 deletions(-) diff --git a/flutter/linux/CMakeLists.txt b/flutter/linux/CMakeLists.txt index 08d557f1b..9a4e0527b 100644 --- a/flutter/linux/CMakeLists.txt +++ b/flutter/linux/CMakeLists.txt @@ -125,7 +125,7 @@ foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) endforeach(bundled_library) # flutter_rust_bridge -set(RUSTDESK_LIB_BUILD_TYPE debug) +set(RUSTDESK_LIB_BUILD_TYPE $,debug,release>) string(TOLOWER ${CMAKE_BUILD_TYPE} ${RUSTDESK_LIB_BUILD_TYPE}) message(STATUS "rustdesk lib build type: ${RUSTDESK_LIB_BUILD_TYPE}") diff --git a/flutter/windows/CMakeLists.txt b/flutter/windows/CMakeLists.txt index 3d4e30586..89f0925f3 100644 --- a/flutter/windows/CMakeLists.txt +++ b/flutter/windows/CMakeLists.txt @@ -86,6 +86,13 @@ if(PLUGIN_BUNDLED_LIBRARIES) COMPONENT Runtime) endif() +# flutter_rust_bridge +set(RUSTDESK_LIB_BUILD_TYPE $,debug,release>) +message(STATUS "rustdesk lib build type: ${RUSTDESK_LIB_BUILD_TYPE}") +set(RUSTDESK_LIB "../../target/${RUSTDESK_LIB_BUILD_TYPE}/librustdesk.dll") +install(FILES "${RUSTDESK_LIB}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" +COMPONENT Runtime RENAME librustdesk.dll) + # Fully re-copy the assets directory on each build to avoid having stale files # from a previous install. set(FLUTTER_ASSET_DIR_NAME "flutter_assets") diff --git a/flutter/windows/runner/CMakeLists.txt b/flutter/windows/runner/CMakeLists.txt index bcaa06d73..b9e550fba 100644 --- a/flutter/windows/runner/CMakeLists.txt +++ b/flutter/windows/runner/CMakeLists.txt @@ -16,25 +16,6 @@ add_executable(${BINARY_NAME} WIN32 "runner.exe.manifest" ) -# flutter_rust_bridge with Corrosion -find_package(Corrosion REQUIRED) - -corrosion_import_crate(MANIFEST_PATH ../../../Cargo.toml - # Equivalent to --all-features passed to cargo build -# [ALL_FEATURES] - # Equivalent to --no-default-features passed to cargo build -# [NO_DEFAULT_FEATURES] - # Disable linking of standard libraries (required for no_std crates). -# [NO_STD] - # Specify cargo build profile (e.g. release or a custom profile) -# [PROFILE ] - # Only import the specified crates from a workspace -# [CRATES ... ] - # Enable the specified features -# [FEATURES ... ] -) -target_link_libraries(${BINARY_NAME} PRIVATE librustdesk) - # Apply the standard set of build settings. This can be removed for applications # that need different build settings. apply_standard_settings(${BINARY_NAME}) From 4b0d12f16ee1aee358360c6fe860a57d78318315 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Mon, 12 Sep 2022 18:45:28 +0800 Subject: [PATCH 0445/2015] change flutter_hbb name later --- flutter/rustdesk.desktop | 19 ------------------- flutter/rustdesk.service | 16 ---------------- flutter/rustdesk.service.user | 15 --------------- 3 files changed, 50 deletions(-) delete mode 100644 flutter/rustdesk.desktop delete mode 100644 flutter/rustdesk.service delete mode 100644 flutter/rustdesk.service.user diff --git a/flutter/rustdesk.desktop b/flutter/rustdesk.desktop deleted file mode 100644 index c94285bbd..000000000 --- a/flutter/rustdesk.desktop +++ /dev/null @@ -1,19 +0,0 @@ -[Desktop Entry] -Version=1.1.10 -Name=RustDesk -GenericName=Remote Desktop -Comment=Remote Desktop -Exec=/usr/lib/rustdesk/flutter_hbb %u -Icon=/usr/share/rustdesk/files/rustdesk.png -Terminal=false -Type=Application -StartupNotify=true -Categories=Network;RemoteAccess;GTK; -Keywords=internet; -Actions=new-window; - -X-Desktop-File-Install-Version=0.23 - -[Desktop Action new-window] -Name=Open a New Window - diff --git a/flutter/rustdesk.service b/flutter/rustdesk.service deleted file mode 100644 index 422d9e387..000000000 --- a/flutter/rustdesk.service +++ /dev/null @@ -1,16 +0,0 @@ -[Unit] -Description=RustDesk -Requires=network.target -After=systemd-user-sessions.service - -[Service] -Type=simple -ExecStart=/usr/lib/rustdesk/flutter_hbb --service -PIDFile=/run/rustdesk.pid -KillMode=mixed -TimeoutStopSec=30 -User=root -LimitNOFILE=100000 - -[Install] -WantedBy=multi-user.target diff --git a/flutter/rustdesk.service.user b/flutter/rustdesk.service.user deleted file mode 100644 index a349d0361..000000000 --- a/flutter/rustdesk.service.user +++ /dev/null @@ -1,15 +0,0 @@ -[Unit] -Description=RustDesk user service (--server) - -[Service] -Type=simple -ExecStart=/usr/lib/rustdesk/flutter_hbb --server -PIDFile=/run/rustdesk.user.pid -KillMode=mixed -TimeoutStopSec=30 -LimitNOFILE=100000 -Restart=on-failure -RestartSec=3 - -[Install] -WantedBy=multi-user.target From d939a5ebc66cbbfa81f536e64fe869cf7c50e891 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Mon, 12 Sep 2022 19:17:46 +0800 Subject: [PATCH 0446/2015] remove flutter/PKGBUILD, and modify build.py, not tested yet --- PKGBUILD | 14 +++++++++----- build.py | 21 ++++++++------------- flatpak/shared-modules | 1 - flutter/PKGBUILD | 33 --------------------------------- 4 files changed, 17 insertions(+), 52 deletions(-) delete mode 160000 flatpak/shared-modules delete mode 100644 flutter/PKGBUILD diff --git a/PKGBUILD b/PKGBUILD index 1d1956ede..38004f66c 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,13 +1,13 @@ pkgname=rustdesk -pkgver=1.1.10 +pkgver=1.2.0 pkgrel=0 epoch= pkgdesc="" arch=('x86_64') url="" -license=('GPL-3.0') +license=('AGPL-3.0') groups=() -depends=('gtk3' 'xdotool' 'libxcb' 'libxfixes' 'alsa-lib' 'pulseaudio' 'ttf-arphic-uming' 'python-pip' 'curl') +depends=('gtk3' 'xdotool' 'libxcb' 'libxfixes' 'alsa-lib' 'pulseaudio' 'ttf-arphic-uming' 'python-pip' 'curl' 'libappindicator-gtk3') makedepends=() checkdepends=() optdepends=() @@ -22,8 +22,12 @@ noextract=() md5sums=() #generate with 'makepkg -g' package() { - install -Dm 755 ${HBB}/target/release/${pkgname} -t "${pkgdir}/usr/bin" - install -Dm 644 ${HBB}/libsciter-gtk.so -t "${pkgdir}/usr/lib/rustdesk" + if [[ ${FLUTTER} ]]; then + mkdir -p "${pkgdir}/usr/lib/rustdesk" && cp -r ${HBB}/flutter/build/linux/x64/release/bundle/* -t "${pkgdir}/usr/lib/rustdesk" + cp ${HBB}/flutter/build/linux/x64/release/liblibrustdesk.so "${pkgdir}/usr/lib/rustdesk/librustdesk.so" + fi + mkdir -p "${pkgdir}/usr/bin" + pushd ${pkgdir} && ln -s /usr/lib/rustdesk/flutter_hbb usr/bin/rustdesk && popd install -Dm 644 $HBB/rustdesk.service -t "${pkgdir}/usr/share/rustdesk/files" install -Dm 644 $HBB/rustdesk.desktop -t "${pkgdir}/usr/share/rustdesk/files" install -Dm 644 $HBB/128x128@2x.png "${pkgdir}/usr/share/rustdesk/files/rustdesk.png" diff --git a/build.py b/build.py index 095f8b7f1..cdca16645 100755 --- a/build.py +++ b/build.py @@ -153,16 +153,12 @@ def build_flutter_deb(version): os.chdir("..") -def build_flutter_arch_manjaro(version): +def build_flutter_arch_manjaro(): os.chdir('flutter') os.system('flutter build linux --release') os.system('strip build/linux/x64/release/liblibrustdesk.so') - os.system("sed -i 's/pkgver=.*/pkgver=%s/g' PKGBUILD" % version) - # pacman -S -needed base-devel - os.system('HBB=`pwd` makepkg -f') - os.system( - 'mv rustdesk-%s-0-x86_64.pkg.tar.zst ../rustdesk-%s-manjaro-arch.pkg.tar.zst' % (version, version)) os.chdir('..') + os.system('HBB=`pwd` FLUTTER=1 makepkg -f') def main(): @@ -196,18 +192,17 @@ def main(): print('Not signed') os.system(f'cp -rf target/release/RustDesk.exe rustdesk-{version}-setdown.exe') elif os.path.isfile('/usr/bin/pacman1'): + # pacman -S -needed base-devel + os.system("sed -i 's/pkgver=.*/pkgver=%s/g' PKGBUILD" % version) if flutter: - build_flutter_arch_manjaro(version) + build_flutter_arch_manjaro() else: - # os.system('cargo build --release --features ' + features) + os.system('cargo build --release --features ' + features) os.system('git checkout src/ui/common.tis') os.system('strip target/release/rustdesk') - os.system("sed -i 's/pkgver=.*/pkgver=%s/g' PKGBUILD" % version) - # pacman -S -needed base-devel os.system('HBB=`pwd` makepkg -f') - os.system( - 'mv rustdesk-%s-0-x86_64.pkg.tar.zst rustdesk-%s-manjaro-arch.pkg.tar.zst' % (version, version)) - # pacman -U ./rustdesk.pkg.tar.zst + os.system('mv rustdesk-%s-0-x86_64.pkg.tar.zst rustdesk-%s-manjaro-arch.pkg.tar.zst' % (version, version)) + # pacman -U ./rustdesk.pkg.tar.zst elif os.path.isfile('/usr/bin/yum'): os.system('cargo build --release --features ' + features) os.system('strip target/release/rustdesk') diff --git a/flatpak/shared-modules b/flatpak/shared-modules deleted file mode 160000 index cecc93886..000000000 --- a/flatpak/shared-modules +++ /dev/null @@ -1 +0,0 @@ -Subproject commit cecc93886ce839ec49b0041f072a573327acdf08 diff --git a/flutter/PKGBUILD b/flutter/PKGBUILD deleted file mode 100644 index 3d2981283..000000000 --- a/flutter/PKGBUILD +++ /dev/null @@ -1,33 +0,0 @@ -pkgname=rustdesk -pkgver=1.2.0 -pkgrel=0 -epoch= -pkgdesc="" -arch=('x86_64') -url="" -license=('GPL-3.0') -groups=() -depends=('gtk3' 'xdotool' 'libxcb' 'libxfixes' 'alsa-lib' 'pulseaudio' 'ttf-arphic-uming' 'python-pip' 'curl' 'libappindicator-gtk3') -makedepends=() -checkdepends=() -optdepends=() -provides=() -conflicts=() -replaces=() -backup=() -options=() -install=../pacman_install -changelog= -noextract=() -md5sums=() #generate with 'makepkg -g' - -package() { - mkdir -p "${pkgdir}/usr/lib/rustdesk" && cp -r ${HBB}/build/linux/x64/release/bundle/* -t "${pkgdir}/usr/lib/rustdesk" - cp ${HBB}/build/linux/x64/release/liblibrustdesk.so "${pkgdir}/usr/lib/rustdesk/librustdesk.so" - mkdir -p "${pkgdir}/usr/bin" - pushd ${pkgdir} && ln -s /usr/lib/rustdesk/flutter_hbb usr/bin/rustdesk && popd - install -Dm 644 $HBB/rustdesk.service -t "${pkgdir}/usr/share/rustdesk/files" - install -Dm 644 $HBB/rustdesk.desktop -t "${pkgdir}/usr/share/rustdesk/files" - # install -Dm 644 $HBB/../pynput_service.py -t "${pkgdir}/usr/share/rustdesk/files" - install -Dm 644 $HBB/../128x128@2x.png "${pkgdir}/usr/share/rustdesk/files/rustdesk.png" -} From f5dba0f7aa2b15b7d81b9b4a6c23a5ed86dce9b4 Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 8 Sep 2022 17:22:24 +0800 Subject: [PATCH 0447/2015] rdp and tcpTunnel use same tabPage Signed-off-by: 21pages --- flutter/lib/common.dart | 5 +---- flutter/lib/consts.dart | 1 - flutter/lib/desktop/pages/port_forward_page.dart | 11 +++++------ flutter/lib/desktop/pages/port_forward_tab_page.dart | 9 +++++++-- flutter/lib/desktop/widgets/tabbar_widget.dart | 1 - flutter/lib/main.dart | 3 +-- src/flutter.rs | 1 - src/flutter_ffi.rs | 7 ++++++- 8 files changed, 20 insertions(+), 18 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 73334baae..53b23c5cf 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -52,7 +52,6 @@ enum DesktopType { fileTransfer, cm, portForward, - rdp, } class IconFont { @@ -572,9 +571,7 @@ void msgBox( submit() { dialogManager.dismissAll(); // https://github.com/fufesou/rustdesk/blob/5e9a31340b899822090a3731769ae79c6bf5f3e5/src/ui/common.tis#L263 - if (!type.contains("custom") && - !(desktopType == DesktopType.portForward || - desktopType == DesktopType.rdp)) { + if (!type.contains("custom") && desktopType != DesktopType.portForward) { closeConnection(); } } diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index e48c85b0d..70fc9f065 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -5,7 +5,6 @@ const String kAppTypeMain = "main"; const String kAppTypeDesktopRemote = "remote"; const String kAppTypeDesktopFileTransfer = "file transfer"; const String kAppTypeDesktopPortForward = "port forward"; -const String kAppTypeDesktopRDP = "rdp"; const String kTabLabelHomePage = "Home"; const String kTabLabelSettingPage = "Settings"; diff --git a/flutter/lib/desktop/pages/port_forward_page.dart b/flutter/lib/desktop/pages/port_forward_page.dart index 6cfd0cdb2..06ee7bd94 100644 --- a/flutter/lib/desktop/pages/port_forward_page.dart +++ b/flutter/lib/desktop/pages/port_forward_page.dart @@ -37,7 +37,6 @@ class PortForwardPage extends StatefulWidget { class _PortForwardPageState extends State with AutomaticKeepAliveClientMixin { - final bool isRdp = false; final TextEditingController localPortController = TextEditingController(); final TextEditingController remoteHostController = TextEditingController(); final TextEditingController remotePortController = TextEditingController(); @@ -53,7 +52,7 @@ class _PortForwardPageState extends State if (!Platform.isLinux) { Wakelock.enable(); } - print("init success with id ${widget.id}"); + debugPrint("init success with id ${widget.id}"); } @override @@ -73,7 +72,7 @@ class _PortForwardPageState extends State return Scaffold( backgroundColor: MyTheme.color(context).grayBg, body: FutureBuilder(future: () async { - if (!isRdp) { + if (!widget.isRDP) { refreshTunnelConfig(); } }(), builder: (context, snapshot) { @@ -288,7 +287,7 @@ class _PortForwardPageState extends State text2(String lable) => Expanded( child: Text( lable, - style: TextStyle(fontSize: 20), + style: const TextStyle(fontSize: 20), ).marginOnly(left: _kTextLeftMargin)); return Theme( data: Theme.of(context) @@ -321,10 +320,10 @@ class _PortForwardPageState extends State style: ElevatedButton.styleFrom( elevation: 0, side: const BorderSide(color: MyTheme.border)), - onPressed: () {}, + onPressed: () => bind.sessionNewRdp(id: widget.id), child: Text( translate('New RDP'), - style: TextStyle( + style: const TextStyle( fontWeight: FontWeight.w300, fontSize: 14), ), ).marginSymmetric(vertical: 10), diff --git a/flutter/lib/desktop/pages/port_forward_tab_page.dart b/flutter/lib/desktop/pages/port_forward_tab_page.dart index 5dd69e8eb..d078af458 100644 --- a/flutter/lib/desktop/pages/port_forward_tab_page.dart +++ b/flutter/lib/desktop/pages/port_forward_tab_page.dart @@ -26,8 +26,8 @@ class _PortForwardTabPageState extends State { _PortForwardTabPageState(Map params) { isRDP = params['isRDP']; - tabController = Get.put(DesktopTabController( - tabType: isRDP ? DesktopTabType.rdp : DesktopTabType.portForward)); + tabController = + Get.put(DesktopTabController(tabType: DesktopTabType.portForward)); tabController.add(TabInfo( key: params['id'], label: params['id'], @@ -55,6 +55,11 @@ class _PortForwardTabPageState extends State { final id = args['id']; final isRDP = args['isRDP']; window_on_top(windowId()); + if (tabController.state.value.tabs.indexWhere((e) => e.key == id) >= + 0) { + debugPrint("port forward $id exists"); + return; + } tabController.add(TabInfo( key: id, label: id, diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index fef5fbf1f..fb7989108 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -44,7 +44,6 @@ enum DesktopTabType { remoteScreen, fileTransfer, portForward, - rdp, } class DesktopTabState { diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 0ae5b583d..98ac20bfe 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -51,8 +51,7 @@ Future main(List args) async { runFileTransferScreen(argument); break; case WindowType.PortForward: - desktopType = - argument['isRDP'] ? DesktopType.rdp : DesktopType.portForward; + desktopType = DesktopType.portForward; runPortForwardScreen(argument); break; default: diff --git a/src/flutter.rs b/src/flutter.rs index b22c0da83..eb66260c9 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -18,7 +18,6 @@ pub(super) const APP_TYPE_MAIN: &str = "main"; pub(super) const APP_TYPE_DESKTOP_REMOTE: &str = "remote"; pub(super) const APP_TYPE_DESKTOP_FILE_TRANSFER: &str = "file transfer"; pub(super) const APP_TYPE_DESKTOP_PORT_FORWARD: &str = "port forward"; -pub(super) const APP_TYPE_DESKTOP_RDP: &str = "rdp"; lazy_static::lazy_static! { pub static ref SESSIONS: RwLock>> = Default::default(); diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 10ab95487..f3c7b3735 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -663,7 +663,6 @@ fn main_broadcast_message(data: &HashMap<&str, &str>) { flutter::APP_TYPE_DESKTOP_REMOTE, flutter::APP_TYPE_DESKTOP_FILE_TRANSFER, flutter::APP_TYPE_DESKTOP_PORT_FORWARD, - flutter::APP_TYPE_DESKTOP_RDP, ]; for app in apps { @@ -703,6 +702,12 @@ pub fn session_remove_port_forward(id: String, local_port: i32) { } } +pub fn session_new_rdp(id: String) { + if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) { + session.new_rdp(); + } +} + pub fn main_get_last_remote_id() -> String { // if !config::APP_DIR.read().unwrap().is_empty() { // res = LocalConfig::get_remote_id(); From 302a43d68c7e786f02ba9c2b64b4a2b96ac91c0c Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 8 Sep 2022 21:40:43 +0800 Subject: [PATCH 0448/2015] update setting page, add option enable-rdp Signed-off-by: 21pages --- flutter/lib/common.dart | 6 +- .../lib/desktop/pages/connection_page.dart | 8 +- .../lib/desktop/pages/desktop_home_page.dart | 81 +- .../desktop/pages/desktop_setting_page.dart | 760 +++++++++++------- .../lib/desktop/pages/port_forward_page.dart | 4 +- flutter/lib/desktop/pages/server_page.dart | 35 +- src/lang/cn.rs | 22 + src/lang/cs.rs | 22 + src/lang/da.rs | 22 + src/lang/de.rs | 22 + src/lang/eo.rs | 22 + src/lang/es.rs | 22 + src/lang/fr.rs | 22 + src/lang/hu.rs | 22 + src/lang/id.rs | 22 + src/lang/it.rs | 22 + src/lang/ja.rs | 22 + src/lang/ko.rs | 24 +- src/lang/pl.rs | 22 + src/lang/pt_PT.rs | 22 + src/lang/ptbr.rs | 22 + src/lang/ru.rs | 22 + src/lang/sk.rs | 22 + src/lang/template.rs | 22 + src/lang/tr.rs | 22 + src/lang/tw.rs | 22 + src/lang/vn.rs | 22 + src/server/connection.rs | 16 +- src/ui_interface.rs | 2 +- 29 files changed, 956 insertions(+), 420 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 53b23c5cf..59e3bf891 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -195,7 +195,7 @@ class MyTheme { ); static changeTo(bool dark) { - if (Get.isDarkMode != dark) { + if (isDarkTheme() != dark) { Get.find().setString("darkTheme", dark ? "Y" : ""); Get.changeThemeMode(dark ? ThemeMode.dark : ThemeMode.light); if (desktopType == DesktopType.main) { @@ -210,7 +210,7 @@ class MyTheme { bool dark; // Brightnesss is always light on windows, Flutter 3.0.5 if (_themeInitialed || !mainPage || Platform.isWindows) { - dark = "Y" == Get.find().getString("darkTheme"); + dark = isDarkTheme(); } else { dark = WidgetsBinding.instance.platformDispatcher.platformBrightness == Brightness.dark; @@ -230,7 +230,7 @@ class MyTheme { } bool isDarkTheme() { - return Get.isDarkMode; + return "Y" == Get.find().getString("darkTheme"); } final ButtonStyle flatButtonStyle = TextButton.styleFrom( diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index b113d8a56..a39877b64 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -428,8 +428,12 @@ class _ConnectionPageState extends State { light, Text(translate("Service is not running")), TextButton( - onPressed: () => - bind.mainSetOption(key: "stop-service", value: ""), + onPressed: () async { + bool checked = await bind.mainCheckSuperUserPermission(); + if (checked) { + bind.mainSetOption(key: "stop-service", value: ""); + } + }, child: Text(translate("Start Service"))) ], ); diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 3cda8aa60..47f1cc026 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -525,7 +525,7 @@ class _DesktopHomePageState extends State final option = await bind.mainGetOption(key: key); bind.mainSetOption(key: key, value: option == "Y" ? "" : "Y"); } else if (key == "change-id") { - changeId(); + changeIdDialog(); } else if (key == "custom-server") { changeServer(); } else if (key == "whitelist") { @@ -648,85 +648,6 @@ class _DesktopHomePageState extends State ); } - /// change local ID - void changeId() { - var newId = ""; - var msg = ""; - var isInProgress = false; - TextEditingController controller = TextEditingController(); - gFFI.dialogManager.show((setState, close) { - submit() async { - newId = controller.text.trim(); - setState(() { - msg = ""; - isInProgress = true; - bind.mainChangeId(newId: newId); - }); - - var status = await bind.mainGetAsyncStatus(); - while (status == " ") { - await Future.delayed(const Duration(milliseconds: 100)); - status = await bind.mainGetAsyncStatus(); - } - if (status.isEmpty) { - // ok - close(); - return; - } - setState(() { - isInProgress = false; - msg = translate(status); - }); - } - - return CustomAlertDialog( - title: Text(translate("Change ID")), - content: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(translate("id_change_tip")), - const SizedBox( - height: 8.0, - ), - Row( - children: [ - const Text("ID:").marginOnly(bottom: 16.0), - const SizedBox( - width: 24.0, - ), - Expanded( - child: TextField( - decoration: InputDecoration( - border: const OutlineInputBorder(), - errorText: msg.isEmpty ? null : translate(msg)), - inputFormatters: [ - LengthLimitingTextInputFormatter(16), - // FilteringTextInputFormatter(RegExp(r"[a-zA-z][a-zA-z0-9\_]*"), allow: true) - ], - maxLength: 16, - controller: controller, - focusNode: FocusNode()..requestFocus(), - ), - ), - ], - ), - const SizedBox( - height: 4.0, - ), - Offstage( - offstage: !isInProgress, child: const LinearProgressIndicator()) - ], - ), - actions: [ - TextButton(onPressed: close, child: Text(translate("Cancel"))), - TextButton(onPressed: submit, child: Text(translate("OK"))), - ], - onSubmit: submit, - onCancel: close, - ); - }); - } - void about() async { final appName = await bind.mainGetAppName(); final license = await bind.mainGetLicense(); diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 823a32fb6..87d12c9c3 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -8,7 +9,6 @@ import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/server_model.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import 'package:url_launcher/url_launcher_string.dart'; const double _kTabWidth = 235; @@ -32,7 +32,7 @@ class _TabInfo { } class DesktopSettingPage extends StatefulWidget { - DesktopSettingPage({Key? key}) : super(key: key); + const DesktopSettingPage({Key? key}) : super(key: key); @override State createState() => _DesktopSettingPageState(); @@ -40,19 +40,18 @@ class DesktopSettingPage extends StatefulWidget { class _DesktopSettingPageState extends State with TickerProviderStateMixin, AutomaticKeepAliveClientMixin { - final List<_TabInfo> _setting_tabs = <_TabInfo>[ - _TabInfo('User Interface', Icons.language_outlined, Icons.language_sharp), + final List<_TabInfo> settingTabs = <_TabInfo>[ + _TabInfo('General', Icons.settings_outlined, Icons.settings), + _TabInfo('Language', Icons.language_outlined, Icons.language), _TabInfo('Security', Icons.enhanced_encryption_outlined, - Icons.enhanced_encryption_sharp), - _TabInfo( - 'Display', Icons.desktop_windows_outlined, Icons.desktop_windows_sharp), - _TabInfo('Audio', Icons.volume_up_outlined, Icons.volume_up_sharp), - _TabInfo('Connection', Icons.link_outlined, Icons.link_sharp), - _TabInfo('About', Icons.info_outline, Icons.info_sharp) + Icons.enhanced_encryption), + _TabInfo('Network', Icons.link_outlined, Icons.link), + _TabInfo('Acount', Icons.person_outline, Icons.person), + _TabInfo('About', Icons.info_outline, Icons.info) ]; late PageController controller; - RxInt _selectedIndex = 0.obs; + RxInt selectedIndex = 0.obs; @override bool get wantKeepAlive => true; @@ -70,12 +69,12 @@ class _DesktopSettingPageState extends State backgroundColor: MyTheme.color(context).bg, body: Row( children: [ - Container( + SizedBox( width: _kTabWidth, child: Column( children: [ _header(), - Flexible(child: _listView(tabs: _setting_tabs)), + Flexible(child: _listView(tabs: settingTabs)), ], ), ), @@ -85,12 +84,12 @@ class _DesktopSettingPageState extends State color: MyTheme.color(context).grayBg, child: PageView( controller: controller, - children: [ - _UserInterface(), + children: const [ + _General(), + _Language(), _Safety(), - _Display(), - _Audio(), - _Connection(), + _Network(), + _Acount(), _About(), ], ), @@ -109,14 +108,14 @@ class _DesktopSettingPageState extends State child: Text( translate('Settings'), textAlign: TextAlign.left, - style: TextStyle( + style: const TextStyle( color: _accentColor, fontSize: _kTitleFontSize, fontWeight: FontWeight.w400, ), ), ).marginOnly(left: 20, top: 10), - Spacer(), + const Spacer(), ], ); } @@ -133,16 +132,16 @@ class _DesktopSettingPageState extends State Widget _listItem({required _TabInfo tab, required int index}) { return Obx(() { - bool selected = index == _selectedIndex.value; - return Container( + bool selected = index == selectedIndex.value; + return SizedBox( width: _kTabWidth, height: _kTabHeight, child: InkWell( onTap: () { - if (_selectedIndex.value != index) { + if (selectedIndex.value != index) { controller.jumpToPage(index); } - _selectedIndex.value = index; + selectedIndex.value = index; }, child: Row(children: [ Container( @@ -171,14 +170,128 @@ class _DesktopSettingPageState extends State //#region pages -class _UserInterface extends StatefulWidget { - _UserInterface({Key? key}) : super(key: key); +class _General extends StatefulWidget { + const _General({Key? key}) : super(key: key); @override - State<_UserInterface> createState() => _UserInterfaceState(); + State<_General> createState() => _GeneralState(); } -class _UserInterfaceState extends State<_UserInterface> +class _GeneralState extends State<_General> { + @override + Widget build(BuildContext context) { + return ListView( + children: [ + theme(), + abr(), + hwcodec(), + audio(context), + ], + ).marginOnly(bottom: _kListViewBottomMargin); + } + + Widget theme() { + change() { + MyTheme.changeTo(!isDarkTheme()); + setState(() {}); + } + + return _Card(title: 'Theme', children: [ + GestureDetector( + onTap: change, + child: Row( + children: [ + Checkbox(value: isDarkTheme(), onChanged: (_) => change()) + .marginOnly(right: 5), + Expanded(child: Text(translate('Dark Theme'))), + ], + ).marginOnly(left: _kCheckBoxLeftMargin), + ) + ]); + } + + Widget abr() { + return _Card(title: 'Adaptive Bitrate', children: [ + _OptionCheckBox(context, 'Adaptive Bitrate', 'enable-abr'), + ]); + } + + Widget hwcodec() { + return _futureBuilder( + future: bind.mainHasHwcodec(), + hasData: (data) { + return Offstage( + offstage: !(data as bool), + child: _Card(title: 'Hardware Codec', children: [ + _OptionCheckBox( + context, 'Enable hardware codec', 'enable-hwcodec'), + ]), + ); + }); + } + + Widget audio(BuildContext context) { + String getDefault() { + if (Platform.isWindows) return "System Sound"; + return ""; + } + + Future getValue() async { + String device = await bind.mainGetOption(key: 'audio-input'); + if (device.isNotEmpty) { + return device; + } else { + return getDefault(); + } + } + + setDevice(String device) { + if (device == getDefault()) device = ""; + bind.mainSetOption(key: 'audio-input', value: device); + } + + return _futureBuilder(future: () async { + List devices = (await bind.mainGetSoundInputs()).toList(); + if (Platform.isWindows) { + devices.insert(0, 'System Sound'); + } + String current = await getValue(); + return {'devices': devices, 'current': current}; + }(), hasData: (data) { + String currentDevice = data['current']; + List devices = data['devices'] as List; + if (devices.isEmpty) { + return const Offstage(); + } + List keys = devices.toList(); + List values = devices.toList(); + // TODO + if (!devices.contains(currentDevice)) { + currentDevice = ""; + keys.insert(0, currentDevice); + values.insert(0, 'default'); + } + return _Card(title: 'Audio Input Device', children: [ + _ComboBox( + keys: keys, + values: values, + initialKey: currentDevice, + onChanged: (key) { + setDevice(key); + }).marginOnly(left: _kContentHMargin), + ]); + }); + } +} + +class _Language extends StatefulWidget { + const _Language({Key? key}) : super(key: key); + + @override + State<_Language> createState() => _LanguageState(); +} + +class _LanguageState extends State<_Language> with AutomaticKeepAliveClientMixin { @override bool get wantKeepAlive => true; @@ -189,7 +302,6 @@ class _UserInterfaceState extends State<_UserInterface> return ListView( children: [ _Card(title: 'Language', children: [language()]), - _Card(title: 'Theme', children: [theme()]), ], ).marginOnly(bottom: _kListViewBottomMargin); } @@ -223,22 +335,6 @@ class _UserInterfaceState extends State<_UserInterface> ).marginOnly(left: _kContentHMargin); }); } - - Widget theme() { - change() { - MyTheme.changeTo(!isDarkTheme()); - } - - return GestureDetector( - onTap: change, - child: Row( - children: [ - Checkbox(value: isDarkTheme(), onChanged: (_) => change()), - Expanded(child: Text(translate('Dark Theme'))), - ], - ).marginOnly(left: _kCheckBoxLeftMargin), - ); - } } class _Safety extends StatefulWidget { @@ -269,7 +365,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { child: Column(children: [ permissions(context), password(context), - whitelist(), + connection(context), ]), ), ], @@ -379,73 +475,32 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { }))); } - Widget whitelist() { - return _Card(title: 'IP Whitelisting', children: [ - _Button('IP Whitelisting', changeWhiteList, - tip: 'whitelist_tip', enabled: !locked) + Widget connection(BuildContext context) { + bool enabled = !locked; + return _Card(title: 'Connection', children: [ + _OptionCheckBox(context, 'Deny remote access', 'stop-service', + checkedIcon: const Icon( + Icons.warning, + color: Colors.yellowAccent, + ), + enabled: enabled), + _OptionCheckBox(context, 'Enable TCP Tunneling', 'enable-tunnel', + enabled: enabled), + Offstage( + offstage: !Platform.isWindows, + child: _OptionCheckBox(context, 'Enable RDP', 'enable-rdp', + enabled: enabled), + ), + ...directIp(context), + whitelist(), ]); } -} -class _Connection extends StatefulWidget { - const _Connection({Key? key}) : super(key: key); - - @override - State<_Connection> createState() => _ConnectionState(); -} - -class _ConnectionState extends State<_Connection> - with AutomaticKeepAliveClientMixin { - @override - bool get wantKeepAlive => true; - bool locked = true; - - @override - Widget build(BuildContext context) { - super.build(context); - bool enabled = !locked; - return ListView(children: [ - Column( - children: [ - _lock(locked, 'Unlock Connection Settings', () { - locked = false; - setState(() => {}); - }), - AbsorbPointer( - absorbing: locked, - child: Column(children: [ - _Card(title: 'Server', children: [ - _Button('ID/Relay Server', changeServer, enabled: enabled), - ]), - _Card(title: 'Service', children: [ - _OptionCheckBox(context, 'Enable Service', 'stop-service', - reverse: true, enabled: enabled), - // TODO: Not implemented - // _option_check('Always connected via relay', 'allow-always-relay', enabled: enabled), - // _option_check('Start ID/relay service', 'stop-rendezvous-service', - // reverse: true, enabled: enabled), - ]), - _Card(title: 'TCP Tunneling', children: [ - _OptionCheckBox( - context, 'Enable TCP Tunneling', 'enable-tunnel', - enabled: enabled), - ]), - direct_ip(context), - _Card(title: 'Proxy', children: [ - _Button('Socks5 Proxy', changeSocks5Proxy, enabled: enabled), - ]), - ]), - ), - ], - ) - ]).marginOnly(bottom: _kListViewBottomMargin); - } - - Widget direct_ip(BuildContext context) { + List directIp(BuildContext context) { TextEditingController controller = TextEditingController(); - var update = () => setState(() {}); - RxBool apply_enabled = false.obs; - return _Card(title: 'Direct IP Access', children: [ + update() => setState(() {}); + RxBool applyEnabled = false.obs; + return [ _OptionCheckBox(context, 'Enable Direct IP Access', 'direct-server', update: update, enabled: !locked), _futureBuilder( @@ -457,194 +512,178 @@ class _ConnectionState extends State<_Connection> hasData: (data) { bool enabled = option2bool('direct-server', data['enabled'].toString()); - if (!enabled) apply_enabled.value = false; + if (!enabled) applyEnabled.value = false; controller.text = data['port'].toString(); - return Row(children: [ - _SubLabeledWidget( - 'Port', - Container( - width: 80, - child: TextField( - controller: controller, - enabled: enabled && !locked, - onChanged: (_) => apply_enabled.value = true, - inputFormatters: [ - FilteringTextInputFormatter.allow(RegExp( - '\^([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])\$')), - ], - textAlign: TextAlign.end, - decoration: InputDecoration( - hintText: '21118', - border: InputBorder.none, - contentPadding: EdgeInsets.only(right: 5), - isCollapsed: true, + return Offstage( + offstage: !enabled, + child: Row(children: [ + _SubLabeledWidget( + 'Port', + SizedBox( + width: 80, + child: TextField( + controller: controller, + enabled: enabled && !locked, + onChanged: (_) => applyEnabled.value = true, + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp( + 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])$')), + ], + textAlign: TextAlign.end, + decoration: const InputDecoration( + hintText: '21118', + border: InputBorder.none, + contentPadding: EdgeInsets.only(right: 5), + isCollapsed: true, + ), ), ), - ), - enabled: enabled && !locked, - ).marginOnly(left: 5), - Obx(() => ElevatedButton( - onPressed: apply_enabled.value && enabled && !locked - ? () async { - apply_enabled.value = false; - await bind.mainSetOption( - key: 'direct-access-port', - value: controller.text); - } - : null, - child: Text( - translate('Apply'), - ), - ).marginOnly(left: 20)) - ]); + 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)) + ]), + ); }, ), - ]); + ]; + } + + Widget whitelist() { + bool enabled = !locked; + return _futureBuilder(future: () async { + return await bind.mainGetOption(key: 'whitelist'); + }(), hasData: (data) { + RxBool hasWhitelist = (data as String).isNotEmpty.obs; + update() async { + hasWhitelist.value = + (await bind.mainGetOption(key: 'whitelist')).isNotEmpty; + } + + onChanged(bool? checked) async { + changeWhiteList(callback: update); + } + + return GestureDetector( + child: Tooltip( + message: translate('whitelist_tip'), + child: Obx(() => Row( + children: [ + Checkbox( + value: hasWhitelist.value, + onChanged: enabled ? onChanged : null) + .marginOnly(right: 5), + Offstage( + offstage: !hasWhitelist.value, + child: const Icon(Icons.warning, color: Colors.yellowAccent) + .marginOnly(right: 5), + ), + Expanded( + child: Text( + translate('Use IP Whitelisting'), + style: + TextStyle(color: _disabledTextColor(context, enabled)), + )) + ], + )), + ), + onTap: () { + onChanged(!hasWhitelist.value); + }, + ).marginOnly(left: _kCheckBoxLeftMargin); + }); } } -class _Display extends StatefulWidget { - const _Display({Key? key}) : super(key: key); +class _Network extends StatefulWidget { + const _Network({Key? key}) : super(key: key); @override - State<_Display> createState() => _DisplayState(); + State<_Network> createState() => _NetworkState(); } -class _DisplayState extends State<_Display> with AutomaticKeepAliveClientMixin { +class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin { @override bool get wantKeepAlive => true; + bool locked = true; @override Widget build(BuildContext context) { super.build(context); + bool enabled = !locked; + return ListView(children: [ + Column( + children: [ + _lock(locked, 'Unlock Network Settings', () { + locked = false; + setState(() => {}); + }), + AbsorbPointer( + absorbing: locked, + child: Column(children: [ + _Card(title: 'Server', children: [ + _Button('ID/Relay Server', changeServer, enabled: enabled), + ]), + _Card(title: 'Proxy', children: [ + _Button('Socks5 Proxy', changeSocks5Proxy, enabled: enabled), + ]), + ]), + ), + ], + ) + ]).marginOnly(bottom: _kListViewBottomMargin); + } +} + +class _Acount extends StatefulWidget { + const _Acount({Key? key}) : super(key: key); + + @override + State<_Acount> createState() => _AcountState(); +} + +class _AcountState extends State<_Acount> { + @override + Widget build(BuildContext context) { return ListView( children: [ - _Card(title: 'Adaptive Bitrate', children: [ - _OptionCheckBox(context, 'Adaptive Bitrate', 'enable-abr'), - ]), - hwcodec(), + _Card(title: 'Acount', children: [login()]), + _Card(title: 'ID', children: [changeId()]), ], ).marginOnly(bottom: _kListViewBottomMargin); } - Widget hwcodec() { - return _futureBuilder( - future: bind.mainHasHwcodec(), - hasData: (data) { - return Offstage( - offstage: !(data as bool), - child: _Card(title: 'Hardware Codec', children: [ - _OptionCheckBox( - context, 'Enable hardware codec', 'enable-hwcodec'), - ]), - ); - }); + Widget login() { + return _futureBuilder(future: () async { + return await gFFI.userModel.getUserName(); + }(), hasData: (data) { + String username = data as String; + return _Button( + username.isEmpty ? 'Login' : 'Logout', + () => { + loginDialog().then((success) { + if (success) { + // refresh frame + setState(() {}); + } + }) + }); + }); } -} -class _Audio extends StatefulWidget { - const _Audio({Key? key}) : super(key: key); - - @override - State<_Audio> createState() => _AudioState(); -} - -enum _AudioInputType { - Mute, - Standard, - Specify, -} - -class _AudioState extends State<_Audio> with AutomaticKeepAliveClientMixin { - @override - bool get wantKeepAlive => true; - - @override - Widget build(BuildContext context) { - super.build(context); - var update = () => setState(() {}); - var set_enabled = (bool enabled) => bind.mainSetOption( - key: 'enable-audio', value: bool2option('enable-audio', enabled)); - var set_device = (String device) => - bind.mainSetOption(key: 'audio-input', value: device); - return ListView(children: [ - _Card( - title: 'Audio Input', - children: [ - _futureBuilder(future: () async { - List devices = await bind.mainGetSoundInputs(); - String current = await bind.mainGetOption(key: 'audio-input'); - String enabled = await bind.mainGetOption(key: 'enable-audio'); - return {'devices': devices, 'current': current, 'enabled': enabled}; - }(), hasData: (data) { - bool mute = - !option2bool('enable-audio', data['enabled'].toString()); - String currentDevice = data['current']; - List devices = (data['devices'] as List).toList(); - _AudioInputType groupValue; - if (mute) { - groupValue = _AudioInputType.Mute; - } else if (devices.contains(currentDevice)) { - groupValue = _AudioInputType.Specify; - } else { - groupValue = _AudioInputType.Standard; - } - List deviceWidget = [].toList(); - if (devices.isNotEmpty) { - var combo = _ComboBox( - keys: devices, - values: devices, - initialKey: devices.contains(currentDevice) - ? currentDevice - : devices[0], - onChanged: (key) { - set_device(key); - }, - enabled: groupValue == _AudioInputType.Specify, - ); - deviceWidget.addAll([ - _Radio<_AudioInputType>( - context, - value: _AudioInputType.Specify, - groupValue: groupValue, - label: 'Specify device', - onChanged: (value) { - set_device(combo.current); - set_enabled(true); - update(); - }, - ), - combo.marginOnly(left: _kContentHSubMargin, top: 5), - ]); - } - return Column(children: [ - _Radio<_AudioInputType>( - context, - value: _AudioInputType.Mute, - groupValue: groupValue, - label: 'Mute', - onChanged: (value) { - set_enabled(false); - update(); - }, - ), - _Radio( - context, - value: _AudioInputType.Standard, - groupValue: groupValue, - label: 'Use standard device', - onChanged: (value) { - set_device(''); - set_enabled(true); - update(); - }, - ), - ...deviceWidget, - ]); - }), - ], - ) - ]).marginOnly(bottom: _kListViewBottomMargin); + Widget changeId() { + return _Button('Change ID', changeIdDialog); } } @@ -665,13 +704,13 @@ class _AboutState extends State<_About> { }(), hasData: (data) { final license = data['license'].toString(); final version = data['version'].toString(); - final linkStyle = TextStyle(decoration: TextDecoration.underline); + const linkStyle = TextStyle(decoration: TextDecoration.underline); return ListView(children: [ _Card(title: "About RustDesk", children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox( + const SizedBox( height: 8.0, ), Text("Version: $version").marginSymmetric(vertical: 4.0), @@ -679,7 +718,7 @@ class _AboutState extends State<_About> { onTap: () { launchUrlString("https://rustdesk.com/privacy"); }, - child: Text( + child: const Text( "Privacy Statement", style: linkStyle, ).marginSymmetric(vertical: 4.0)), @@ -687,13 +726,14 @@ class _AboutState extends State<_About> { onTap: () { launchUrlString("https://rustdesk.com"); }, - child: Text( + child: const Text( "Website", style: linkStyle, ).marginSymmetric(vertical: 4.0)), Container( - decoration: BoxDecoration(color: Color(0xFF2c8cff)), - padding: EdgeInsets.symmetric(vertical: 24, horizontal: 8), + decoration: const BoxDecoration(color: Color(0xFF2c8cff)), + padding: + const EdgeInsets.symmetric(vertical: 24, horizontal: 8), child: Row( children: [ Expanded( @@ -702,9 +742,9 @@ class _AboutState extends State<_About> { children: [ Text( "Copyright © 2022 Purslane Ltd.\n$license", - style: TextStyle(color: Colors.white), + style: const TextStyle(color: Colors.white), ), - Text( + const Text( "Made with heart in this chaotic world!", style: TextStyle( fontWeight: FontWeight.w800, @@ -728,10 +768,11 @@ class _AboutState extends State<_About> { //#region components +// ignore: non_constant_identifier_names Widget _Card({required String title, required List children}) { return Row( children: [ - Container( + SizedBox( width: _kCardFixedWidth, child: Card( child: Column( @@ -741,11 +782,11 @@ Widget _Card({required String title, required List children}) { Text( translate(title), textAlign: TextAlign.start, - style: TextStyle( + style: const TextStyle( fontSize: _kTitleFontSize, ), ), - Spacer(), + const Spacer(), ], ).marginOnly(left: _kContentHMargin, top: 10, bottom: 10), ...children @@ -762,15 +803,19 @@ Color? _disabledTextColor(BuildContext context, bool enabled) { return enabled ? null : MyTheme.color(context).lighterText; } +// ignore: non_constant_identifier_names Widget _OptionCheckBox(BuildContext context, String label, String key, - {Function()? update = null, bool reverse = false, bool enabled = true}) { + {Function()? update, + bool reverse = false, + bool enabled = true, + Icon? checkedIcon}) { return _futureBuilder( future: bind.mainGetOption(key: key), hasData: (data) { bool value = option2bool(key, data.toString()); if (reverse) value = !value; var ref = value.obs; - var onChanged = (option) async { + onChanged(option) async { if (option != null) { ref.value = option; if (reverse) option = !option; @@ -778,14 +823,19 @@ Widget _OptionCheckBox(BuildContext context, String label, String key, bind.mainSetOption(key: key, value: value); update?.call(); } - }; + } + return GestureDetector( child: Obx( () => Row( children: [ Checkbox( value: ref.value, onChanged: enabled ? onChanged : null) - .marginOnly(right: 10), + .marginOnly(right: 5), + Offstage( + offstage: !ref.value || checkedIcon == null, + child: checkedIcon?.marginOnly(right: 5), + ), Expanded( child: Text( translate(label), @@ -801,13 +851,14 @@ Widget _OptionCheckBox(BuildContext context, String label, String key, }); } +// ignore: non_constant_identifier_names Widget _Radio(BuildContext context, {required T value, required T groupValue, required String label, required Function(T value) onChanged, bool enabled = true}) { - var on_change = enabled + var onChange = enabled ? (T? value) { if (value != null) { onChanged(value); @@ -817,7 +868,7 @@ Widget _Radio(BuildContext context, return GestureDetector( child: Row( children: [ - Radio(value: value, groupValue: groupValue, onChanged: on_change), + Radio(value: value, groupValue: groupValue, onChanged: onChange), Expanded( child: Text(translate(label), style: TextStyle( @@ -827,10 +878,11 @@ Widget _Radio(BuildContext context, ), ], ).marginOnly(left: _kRadioLeftMargin), - onTap: () => on_change?.call(value), + onTap: () => onChange?.call(value), ); } +// ignore: non_constant_identifier_names Widget _Button(String label, Function() onPressed, {bool enabled = true, String? tip}) { var button = ElevatedButton( @@ -840,7 +892,7 @@ Widget _Button(String label, Function() onPressed, translate(label), ).marginSymmetric(horizontal: 15), )); - var child; + StatefulWidget child; if (tip == null) { child = button; } else { @@ -851,6 +903,7 @@ Widget _Button(String label, Function() onPressed, ]).marginOnly(left: _kContentHMargin); } +// ignore: non_constant_identifier_names Widget _SubButton(String label, Function() onPressed, [bool enabled = true]) { return Row( children: [ @@ -865,6 +918,7 @@ Widget _SubButton(String label, Function() onPressed, [bool enabled = true]) { ).marginOnly(left: _kContentHSubMargin); } +// ignore: non_constant_identifier_names Widget _SubLabeledWidget(String label, Widget child, {bool enabled = true}) { RxBool hover = false.obs; return Row( @@ -879,23 +933,23 @@ Widget _SubLabeledWidget(String label, Widget child, {bool enabled = true}) { decoration: BoxDecoration( border: Border.all( color: hover.value && enabled - ? Color(0xFFD7D7D7) - : Color(0xFFCBCBCB), + ? const Color(0xFFD7D7D7) + : const Color(0xFFCBCBCB), width: hover.value && enabled ? 2 : 1)), child: Row( children: [ Container( height: 28, color: (hover.value && enabled) - ? Color(0xFFD7D7D7) - : Color(0xFFCBCBCB), - child: Text( - label + ': ', - style: TextStyle(fontWeight: FontWeight.w300), - ), + ? const Color(0xFFD7D7D7) + : const Color(0xFFCBCBCB), alignment: Alignment.center, - padding: - EdgeInsets.symmetric(horizontal: 5, vertical: 2), + padding: const EdgeInsets.symmetric( + horizontal: 5, vertical: 2), + child: Text( + '${translate(label)}: ', + style: const TextStyle(fontWeight: FontWeight.w300), + ), ).paddingAll(2), child, ], @@ -915,7 +969,7 @@ Widget _futureBuilder( return hasData(snapshot.data!); } else { if (snapshot.hasError) { - print(snapshot.error.toString()); + debugPrint(snapshot.error.toString()); } return Container(); } @@ -931,16 +985,16 @@ Widget _lock( offstage: !locked, child: Row( children: [ - Container( + SizedBox( width: _kCardFixedWidth, child: Card( child: ElevatedButton( - child: Container( + child: SizedBox( height: 25, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( + const Icon( Icons.security_sharp, size: 20, ), @@ -996,7 +1050,7 @@ class _ComboBox extends StatelessWidget { underline: Container( height: 25, ), - icon: Icon( + icon: const Icon( Icons.expand_more_sharp, size: 20, ), @@ -1014,7 +1068,7 @@ class _ComboBox extends StatelessWidget { value: value, child: Text( value, - style: TextStyle(fontSize: _kContentFontSize), + style: const TextStyle(fontSize: _kContentFontSize), overflow: TextOverflow.ellipsis, ).marginOnly(left: 5), ); @@ -1047,9 +1101,9 @@ void changeServer() async { gFFI.dialogManager.show((setState, close) { submit() async { setState(() { - [idServerMsg, relayServerMsg, apiServerMsg].forEach((element) { - element = ""; - }); + idServerMsg = ""; + relayServerMsg = ""; + apiServerMsg = ""; isInProgress = true; }); cancel() { @@ -1224,7 +1278,7 @@ void changeServer() async { }); } -void changeWhiteList() async { +void changeWhiteList({Function()? callback}) async { Map oldOptions = jsonDecode(await bind.mainGetOptions()); var newWhiteList = ((oldOptions['whitelist'] ?? "") as String).split(','); var newWhiteListField = newWhiteList.join('\n'); @@ -1263,11 +1317,14 @@ void changeWhiteList() async { ], ), actions: [ + TextButton(onPressed: close, child: Text(translate("Cancel"))), TextButton( - onPressed: () { + onPressed: () async { + await bind.mainSetOption(key: 'whitelist', value: ''); + callback?.call(); close(); }, - child: Text(translate("Cancel"))), + child: Text(translate("Clear"))), TextButton( onPressed: () async { setState(() { @@ -1296,6 +1353,7 @@ void changeWhiteList() async { } oldOptions['whitelist'] = newWhiteList; await bind.mainSetOptions(json: jsonEncode(oldOptions)); + callback?.call(); close(); }, child: Text(translate("OK"))), @@ -1444,4 +1502,82 @@ void changeSocks5Proxy() async { }); } +void changeIdDialog() { + var newId = ""; + var msg = ""; + var isInProgress = false; + TextEditingController controller = TextEditingController(); + gFFI.dialogManager.show((setState, close) { + submit() async { + newId = controller.text.trim(); + setState(() { + msg = ""; + isInProgress = true; + bind.mainChangeId(newId: newId); + }); + + var status = await bind.mainGetAsyncStatus(); + while (status == " ") { + await Future.delayed(const Duration(milliseconds: 100)); + status = await bind.mainGetAsyncStatus(); + } + if (status.isEmpty) { + // ok + close(); + return; + } + setState(() { + isInProgress = false; + msg = translate(status); + }); + } + + return CustomAlertDialog( + title: Text(translate("Change ID")), + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(translate("id_change_tip")), + const SizedBox( + height: 8.0, + ), + Row( + children: [ + const Text("ID:").marginOnly(bottom: 16.0), + const SizedBox( + width: 24.0, + ), + Expanded( + child: TextField( + decoration: InputDecoration( + border: const OutlineInputBorder(), + errorText: msg.isEmpty ? null : translate(msg)), + inputFormatters: [ + LengthLimitingTextInputFormatter(16), + // FilteringTextInputFormatter(RegExp(r"[a-zA-z][a-zA-z0-9\_]*"), allow: true) + ], + maxLength: 16, + controller: controller, + focusNode: FocusNode()..requestFocus(), + ), + ), + ], + ), + const SizedBox( + height: 4.0, + ), + Offstage( + offstage: !isInProgress, child: const LinearProgressIndicator()) + ], + ), + actions: [ + TextButton(onPressed: close, child: Text(translate("Cancel"))), + TextButton(onPressed: submit, child: Text(translate("OK"))), + ], + onSubmit: submit, + onCancel: close, + ); + }); +} + //#endregion diff --git a/flutter/lib/desktop/pages/port_forward_page.dart b/flutter/lib/desktop/pages/port_forward_page.dart index 06ee7bd94..aa108c2f5 100644 --- a/flutter/lib/desktop/pages/port_forward_page.dart +++ b/flutter/lib/desktop/pages/port_forward_page.dart @@ -282,8 +282,8 @@ class _PortForwardPageState extends State } buildRdp(BuildContext context) { - text1(String lable) => - Expanded(child: Text(lable).marginOnly(left: _kTextLeftMargin)); + text1(String lable) => Expanded( + child: Text(translate(lable)).marginOnly(left: _kTextLeftMargin)); text2(String lable) => Expanded( child: Text( lable, diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index be14a8537..b49bfdc27 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -60,20 +60,27 @@ class _DesktopServerPageState extends State ], child: Consumer( builder: (context, serverModel, child) => Container( - decoration: BoxDecoration( - border: Border.all(color: MyTheme.color(context).border!)), - child: Scaffold( - backgroundColor: MyTheme.color(context).bg, - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Expanded(child: ConnectionManager()), - SizedBox.fromSize(size: Size(0, 15.0)), - ], - ), - ), - )))); + 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: MyTheme.color(context).bg, + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Expanded(child: ConnectionManager()), + SizedBox.fromSize(size: Size(0, 15.0)), + ], + ), + ), + ); + }) + ]), + ))); } @override diff --git a/src/lang/cn.rs b/src/lang/cn.rs index ae55b9fab..abb772d51 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -322,5 +322,27 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Insecure Connection", "非安全连接"), ("Scale original", "原始尺寸"), ("Scale adaptive", "适应窗口"), + ("General", "常规"), + ("Security", "安全"), + ("Acount", "账户"), + ("Theme", "主题"), + ("Dark Theme", "暗黑主题"), + ("Enable hardware codec", "使用硬件编解码"), + ("Unlock Security Settings", "解锁安全设置"), + ("Enable Audio", "允许传输音频"), + ("Temporary Password Length", "临时密码长度"), + ("Unlock Network Settings", "解锁网络设置"), + ("Server", "服务器"), + ("Direct IP Access", "IP直接访问"), + ("Proxy", "代理"), + ("Port", "端口"), + ("Apply", "应用"), + ("Disconnect all devices?", "断开所有远程连接?"), + ("Clear", "清空"), + ("Audio Input Device", "音频输入设备"), + ("Deny remote access", "拒绝远程访问"), + ("Use IP Whitelisting", "只允许白名单上的IP访问"), + ("Network", "网络"), + ("Enable RDP", "允许RDP访问"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 4f49cb113..5061fbb88 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -322,5 +322,27 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Insecure Connection", "Nezabezpečené připojení"), ("Scale original", "Měřítko původní"), ("Scale adaptive", "Měřítko adaptivní"), + ("General", ""), + ("Security", ""), + ("Acount", ""), + ("Theme", ""), + ("Dark Theme", ""), + ("Enable hardware codec", ""), + ("Unlock Security Settings", ""), + ("Enable Audio", ""), + ("Temporary Password Length", ""), + ("Unlock Network Settings", ""), + ("Server", ""), + ("Direct IP Access", ""), + ("Proxy", ""), + ("Port", ""), + ("Apply", ""), + ("Disconnect all devices?", ""), + ("Clear", ""), + ("Audio Input Device", ""), + ("Deny remote access", ""), + ("Use IP Whitelisting", ""), + ("Network", ""), + ("Enable RDP", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 78db47875..ed5e3425b 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -322,5 +322,27 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Insecure Connection", "Usikker forbindelse"), ("Scale original", "Skala original"), ("Scale adaptive", "Skala adaptiv"), + ("General", ""), + ("Security", ""), + ("Acount", ""), + ("Theme", ""), + ("Dark Theme", ""), + ("Enable hardware codec", ""), + ("Unlock Security Settings", ""), + ("Enable Audio", ""), + ("Temporary Password Length", ""), + ("Unlock Network Settings", ""), + ("Server", ""), + ("Direct IP Access", ""), + ("Proxy", ""), + ("Port", ""), + ("Apply", ""), + ("Disconnect all devices?", ""), + ("Clear", ""), + ("Audio Input Device", ""), + ("Deny remote access", ""), + ("Use IP Whitelisting", ""), + ("Network", ""), + ("Enable RDP", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 60caecdd4..43f0b2f8e 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -322,5 +322,27 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Insecure Connection", "Unsichere Verbindung"), ("Scale original", "Original skalieren"), ("Scale adaptive", "Adaptiv skalieren"), + ("General", ""), + ("Security", ""), + ("Acount", ""), + ("Theme", ""), + ("Dark Theme", ""), + ("Enable hardware codec", ""), + ("Unlock Security Settings", ""), + ("Enable Audio", ""), + ("Temporary Password Length", ""), + ("Unlock Network Settings", ""), + ("Server", ""), + ("Direct IP Access", ""), + ("Proxy", ""), + ("Port", ""), + ("Apply", ""), + ("Disconnect all devices?", ""), + ("Clear", ""), + ("Audio Input Device", ""), + ("Deny remote access", ""), + ("Use IP Whitelisting", ""), + ("Network", ""), + ("Enable RDP", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 95fda5e90..d7f038b41 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -322,5 +322,27 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Insecure Connection", "Nesekura Konekto"), ("Scale original", "Skalo originalo"), ("Scale adaptive", "Skalo adapta"), + ("General", ""), + ("Security", ""), + ("Acount", ""), + ("Theme", ""), + ("Dark Theme", ""), + ("Enable hardware codec", ""), + ("Unlock Security Settings", ""), + ("Enable Audio", ""), + ("Temporary Password Length", ""), + ("Unlock Network Settings", ""), + ("Server", ""), + ("Direct IP Access", ""), + ("Proxy", ""), + ("Port", ""), + ("Apply", ""), + ("Disconnect all devices?", ""), + ("Clear", ""), + ("Audio Input Device", ""), + ("Deny remote access", ""), + ("Use IP Whitelisting", ""), + ("Network", ""), + ("Enable RDP", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 17147a0cf..7c13e849c 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -335,5 +335,27 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Insecure Connection", "Conexión insegura"), ("Scale original", "escala originales"), ("Scale adaptive", "Adaptable a escala"), + ("General", ""), + ("Security", ""), + ("Acount", ""), + ("Theme", ""), + ("Dark Theme", ""), + ("Enable hardware codec", ""), + ("Unlock Security Settings", ""), + ("Enable Audio", ""), + ("Temporary Password Length", ""), + ("Unlock Network Settings", ""), + ("Server", ""), + ("Direct IP Access", ""), + ("Proxy", ""), + ("Port", ""), + ("Apply", ""), + ("Disconnect all devices?", ""), + ("Clear", ""), + ("Audio Input Device", ""), + ("Deny remote access", ""), + ("Use IP Whitelisting", ""), + ("Network", ""), + ("Enable RDP", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 8df98de48..3e46a6b40 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -322,5 +322,27 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Insecure Connection", "Connexion non sécurisée"), ("Scale original", "Échelle d'origine"), ("Scale adaptive", "Échelle adaptative"), + ("General", ""), + ("Security", ""), + ("Acount", ""), + ("Theme", ""), + ("Dark Theme", ""), + ("Enable hardware codec", ""), + ("Unlock Security Settings", ""), + ("Enable Audio", ""), + ("Temporary Password Length", ""), + ("Unlock Network Settings", ""), + ("Server", ""), + ("Direct IP Access", ""), + ("Proxy", ""), + ("Port", ""), + ("Apply", ""), + ("Disconnect all devices?", ""), + ("Clear", ""), + ("Audio Input Device", ""), + ("Deny remote access", ""), + ("Use IP Whitelisting", ""), + ("Network", ""), + ("Enable RDP", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 84d030eb0..78089075c 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -322,5 +322,27 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Insecure Connection", "Nem biztonságos kapcsolat"), ("Scale original", "Eredeti méretarány"), ("Scale adaptive", "Skála adaptív"), + ("General", ""), + ("Security", ""), + ("Acount", ""), + ("Theme", ""), + ("Dark Theme", ""), + ("Enable hardware codec", ""), + ("Unlock Security Settings", ""), + ("Enable Audio", ""), + ("Temporary Password Length", ""), + ("Unlock Network Settings", ""), + ("Server", ""), + ("Direct IP Access", ""), + ("Proxy", ""), + ("Port", ""), + ("Apply", ""), + ("Disconnect all devices?", ""), + ("Clear", ""), + ("Audio Input Device", ""), + ("Deny remote access", ""), + ("Use IP Whitelisting", ""), + ("Network", ""), + ("Enable RDP", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index a80dd6c22..a9cc27767 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -335,5 +335,27 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Insecure Connection", "Koneksi Tidak Aman"), ("Scale original", "Skala asli"), ("Scale adaptive", "Skala adaptif"), + ("General", ""), + ("Security", ""), + ("Acount", ""), + ("Theme", ""), + ("Dark Theme", ""), + ("Enable hardware codec", ""), + ("Unlock Security Settings", ""), + ("Enable Audio", ""), + ("Temporary Password Length", ""), + ("Unlock Network Settings", ""), + ("Server", ""), + ("Direct IP Access", ""), + ("Proxy", ""), + ("Port", ""), + ("Apply", ""), + ("Disconnect all devices?", ""), + ("Clear", ""), + ("Audio Input Device", ""), + ("Deny remote access", ""), + ("Use IP Whitelisting", ""), + ("Network", ""), + ("Enable RDP", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 8bf455d8e..8749e2976 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -321,5 +321,27 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Legacy mode", ""), ("Map mode", ""), ("Translate mode", ""), + ("General", ""), + ("Security", ""), + ("Acount", ""), + ("Theme", ""), + ("Dark Theme", ""), + ("Enable hardware codec", ""), + ("Unlock Security Settings", ""), + ("Enable Audio", ""), + ("Temporary Password Length", ""), + ("Unlock Network Settings", ""), + ("Server", ""), + ("Direct IP Access", ""), + ("Proxy", ""), + ("Port", ""), + ("Apply", ""), + ("Disconnect all devices?", ""), + ("Clear", ""), + ("Audio Input Device", ""), + ("Deny remote access", ""), + ("Use IP Whitelisting", ""), + ("Network", ""), + ("Enable RDP", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 0c9b6c54d..30181b7a5 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -319,5 +319,27 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Insecure Connection", "安全でない接続"), ("Scale original", "オリジナルサイズ"), ("Scale adaptive", "フィットウィンドウ"), + ("General", ""), + ("Security", ""), + ("Acount", ""), + ("Theme", ""), + ("Dark Theme", ""), + ("Enable hardware codec", ""), + ("Unlock Security Settings", ""), + ("Enable Audio", ""), + ("Temporary Password Length", ""), + ("Unlock Network Settings", ""), + ("Server", ""), + ("Direct IP Access", ""), + ("Proxy", ""), + ("Port", ""), + ("Apply", ""), + ("Disconnect all devices?", ""), + ("Clear", ""), + ("Audio Input Device", ""), + ("Deny remote access", ""), + ("Use IP Whitelisting", ""), + ("Network", ""), + ("Enable RDP", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 44ba589f6..7b8377206 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -316,5 +316,27 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Insecure Connection", "안전하지 않은 연결"), ("Scale original", "원래 크기"), ("Scale adaptive", "맞는 창"), + ("General", ""), + ("Security", ""), + ("Acount", ""), + ("Theme", ""), + ("Dark Theme", ""), + ("Enable hardware codec", ""), + ("Unlock Security Settings", ""), + ("Enable Audio", ""), + ("Temporary Password Length", ""), + ("Unlock Network Settings", ""), + ("Server", ""), + ("Direct IP Access", ""), + ("Proxy", ""), + ("Port", ""), + ("Apply", ""), + ("Disconnect all devices?", ""), + ("Clear", ""), + ("Audio Input Device", ""), + ("Deny remote access", ""), + ("Use IP Whitelisting", ""), + ("Network", ""), + ("Enable RDP", ""), ].iter().cloned().collect(); -} \ No newline at end of file +} diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 42bd49bbb..1088cfa72 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -320,5 +320,27 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Insecure Connection", "Niepewne połączenie"), ("Scale original", "Skala oryginalna"), ("Scale adaptive", "Skala adaptacyjna"), + ("General", ""), + ("Security", ""), + ("Acount", ""), + ("Theme", ""), + ("Dark Theme", ""), + ("Enable hardware codec", ""), + ("Unlock Security Settings", ""), + ("Enable Audio", ""), + ("Temporary Password Length", ""), + ("Unlock Network Settings", ""), + ("Server", ""), + ("Direct IP Access", ""), + ("Proxy", ""), + ("Port", ""), + ("Apply", ""), + ("Disconnect all devices?", ""), + ("Clear", ""), + ("Audio Input Device", ""), + ("Deny remote access", ""), + ("Use IP Whitelisting", ""), + ("Network", ""), + ("Enable RDP", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index e8c62d78a..cb6fcf548 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -316,5 +316,27 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Insecure Connection", "Conexão insegura"), ("Scale original", "Escala original"), ("Scale adaptive", "Escala adaptável"), + ("General", ""), + ("Security", ""), + ("Acount", ""), + ("Theme", ""), + ("Dark Theme", ""), + ("Enable hardware codec", ""), + ("Unlock Security Settings", ""), + ("Enable Audio", ""), + ("Temporary Password Length", ""), + ("Unlock Network Settings", ""), + ("Server", ""), + ("Direct IP Access", ""), + ("Proxy", ""), + ("Port", ""), + ("Apply", ""), + ("Disconnect all devices?", ""), + ("Clear", ""), + ("Audio Input Device", ""), + ("Deny remote access", ""), + ("Use IP Whitelisting", ""), + ("Network", ""), + ("Enable RDP", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index b9d3cad70..cba544380 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -322,5 +322,27 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Insecure Connection", ""), ("Scale original", ""), ("Scale adaptive", ""), + ("General", ""), + ("Security", ""), + ("Acount", ""), + ("Theme", ""), + ("Dark Theme", ""), + ("Enable hardware codec", ""), + ("Unlock Security Settings", ""), + ("Enable Audio", ""), + ("Temporary Password Length", ""), + ("Unlock Network Settings", ""), + ("Server", ""), + ("Direct IP Access", ""), + ("Proxy", ""), + ("Port", ""), + ("Apply", ""), + ("Disconnect all devices?", ""), + ("Clear", ""), + ("Audio Input Device", ""), + ("Deny remote access", ""), + ("Use IP Whitelisting", ""), + ("Network", ""), + ("Enable RDP", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 4a0c8413c..6fb7078ac 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -322,5 +322,27 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Insecure Connection", "Небезопасное соединение"), ("Scale original", "Оригинал масштаба"), ("Scale adaptive", "Масштаб адаптивный"), + ("General", ""), + ("Security", ""), + ("Acount", ""), + ("Theme", ""), + ("Dark Theme", ""), + ("Enable hardware codec", ""), + ("Unlock Security Settings", ""), + ("Enable Audio", ""), + ("Temporary Password Length", ""), + ("Unlock Network Settings", ""), + ("Server", ""), + ("Direct IP Access", ""), + ("Proxy", ""), + ("Port", ""), + ("Apply", ""), + ("Disconnect all devices?", ""), + ("Clear", ""), + ("Audio Input Device", ""), + ("Deny remote access", ""), + ("Use IP Whitelisting", ""), + ("Network", ""), + ("Enable RDP", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 1d083d2cc..ebc9571af 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -322,5 +322,27 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Insecure Connection", "Nezabezpečené pripojenie"), ("Scale original", "Pôvodná mierka"), ("Scale adaptive", "Prispôsobivá mierka"), + ("General", ""), + ("Security", ""), + ("Acount", ""), + ("Theme", ""), + ("Dark Theme", ""), + ("Enable hardware codec", ""), + ("Unlock Security Settings", ""), + ("Enable Audio", ""), + ("Temporary Password Length", ""), + ("Unlock Network Settings", ""), + ("Server", ""), + ("Direct IP Access", ""), + ("Proxy", ""), + ("Port", ""), + ("Apply", ""), + ("Disconnect all devices?", ""), + ("Clear", ""), + ("Audio Input Device", ""), + ("Deny remote access", ""), + ("Use IP Whitelisting", ""), + ("Network", ""), + ("Enable RDP", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index dcb2a9566..5d3061a05 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -322,5 +322,27 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Insecure Connection", ""), ("Scale original", ""), ("Scale adaptive", ""), + ("General", ""), + ("Security", ""), + ("Acount", ""), + ("Theme", ""), + ("Dark Theme", ""), + ("Enable hardware codec", ""), + ("Unlock Security Settings", ""), + ("Enable Audio", ""), + ("Temporary Password Length", ""), + ("Unlock Network Settings", ""), + ("Server", ""), + ("Direct IP Access", ""), + ("Proxy", ""), + ("Port", ""), + ("Apply", ""), + ("Disconnect all devices?", ""), + ("Clear", ""), + ("Audio Input Device", ""), + ("Deny remote access", ""), + ("Use IP Whitelisting", ""), + ("Network", ""), + ("Enable RDP", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 01de89909..6cf6c1298 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -335,5 +335,27 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Insecure Connection", "Güvenli Bağlantı"), ("Scale original", "Orijinali ölçeklendir"), ("Scale adaptive", "Ölçek uyarlanabilir"), + ("General", ""), + ("Security", ""), + ("Acount", ""), + ("Theme", ""), + ("Dark Theme", ""), + ("Enable hardware codec", ""), + ("Unlock Security Settings", ""), + ("Enable Audio", ""), + ("Temporary Password Length", ""), + ("Unlock Network Settings", ""), + ("Server", ""), + ("Direct IP Access", ""), + ("Proxy", ""), + ("Port", ""), + ("Apply", ""), + ("Disconnect all devices?", ""), + ("Clear", ""), + ("Audio Input Device", ""), + ("Deny remote access", ""), + ("Use IP Whitelisting", ""), + ("Network", ""), + ("Enable RDP", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 84002b163..f312c7f47 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -322,5 +322,27 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Insecure Connection", "非安全連接"), ("Scale original", "原始尺寸"), ("Scale adaptive", "適應窗口"), + ("General", "常規"), + ("Security", "安全"), + ("Acount", "賬戶"), + ("Theme", "主題"), + ("Dark Theme", "暗黑主題"), + ("Enable hardware codec", "使用硬件編解碼"), + ("Unlock Security Settings", "解鎖安全設置"), + ("Enable Audio", "允許傳輸音頻"), + ("Temporary Password Length", "临时密码长度"), + ("Unlock Network Settings", "臨時密碼長度"), + ("Server", "服務器"), + ("Direct IP Access", "IP直接訪問"), + ("Proxy", "代理"), + ("Port", "端口"), + ("Apply", "應用"), + ("Disconnect all devices?", "斷開所有遠程連接?"), + ("Clear", "清空"), + ("Audio Input Device", "音頻輸入設備"), + ("Deny remote access", "拒絕遠程訪問"), + ("Use IP Whitelisting", "只允許白名單上的IP訪問"), + ("Network", "網絡"), + ("Enable RDP", "允許RDP訪問"), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index a9bc04946..94beafb67 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -322,5 +322,27 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Insecure Connection", "Kết nối không an toàn"), ("Scale original", "Quy mô gốc"), ("Scale adaptive", "Quy mô thích ứng"), + ("General", ""), + ("Security", ""), + ("Acount", ""), + ("Theme", ""), + ("Dark Theme", ""), + ("Enable hardware codec", ""), + ("Unlock Security Settings", ""), + ("Enable Audio", ""), + ("Temporary Password Length", ""), + ("Unlock Network Settings", ""), + ("Server", ""), + ("Direct IP Access", ""), + ("Proxy", ""), + ("Port", ""), + ("Apply", ""), + ("Disconnect all devices?", ""), + ("Clear", ""), + ("Audio Input Device", ""), + ("Deny remote access", ""), + ("Use IP Whitelisting", ""), + ("Network", ""), + ("Enable RDP", ""), ].iter().cloned().collect(); } diff --git a/src/server/connection.rs b/src/server/connection.rs index d93d6d775..42f4bc940 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -921,17 +921,23 @@ impl Connection { self.file_transfer = Some((ft.dir, ft.show_hidden)); } Some(login_request::Union::PortForward(mut pf)) => { - if !Config::get_option("enable-tunnel").is_empty() { - self.send_login_error("No permission of IP tunneling").await; - sleep(1.).await; - return false; - } let mut is_rdp = false; if pf.host == "RDP" && pf.port == 0 { pf.host = "localhost".to_owned(); pf.port = 3389; is_rdp = true; } + if is_rdp && !Config::get_option("enable-rdp").is_empty() + || !is_rdp && !Config::get_option("enable-tunnel").is_empty() + { + if is_rdp { + self.send_login_error("No permission of RDP").await; + } else { + self.send_login_error("No permission of IP tunneling").await; + } + sleep(1.).await; + return false; + } if pf.host.is_empty() { pf.host = "localhost".to_owned(); } diff --git a/src/ui_interface.rs b/src/ui_interface.rs index c7b743ccb..0b95cd588 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -206,7 +206,7 @@ pub fn test_if_valid_server(host: String) -> String { pub fn get_sound_inputs() -> Vec { let mut a = Vec::new(); - #[cfg(windows)] + #[cfg(not(target_os = "linux"))] { // TODO TEST fn get_sound_inputs_() -> Vec { From 42d17f9d2bd3a0ec2b39623290da44bf020b90f0 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sun, 11 Sep 2022 21:46:53 +0800 Subject: [PATCH 0449/2015] fix audio no sound, add missing VideoFrame timestamp move get_time to hbb_common Signed-off-by: 21pages --- libs/hbb_common/src/lib.rs | 8 ++++++++ libs/scrap/src/common/hwcodec.rs | 6 ++++-- libs/scrap/src/common/vpxcodec.rs | 5 +++-- src/common.rs | 10 +--------- src/server/audio_service.rs | 5 +++-- src/server/connection.rs | 14 +++++++------- src/server/input_service.rs | 6 +++--- src/server/video_service.rs | 2 +- 8 files changed, 30 insertions(+), 26 deletions(-) diff --git a/libs/hbb_common/src/lib.rs b/libs/hbb_common/src/lib.rs index 48fbfe23c..50fcc07b2 100644 --- a/libs/hbb_common/src/lib.rs +++ b/libs/hbb_common/src/lib.rs @@ -228,6 +228,14 @@ pub fn get_uuid() -> Vec { Config::get_key_pair().1 } +#[inline] +pub fn get_time() -> i64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_millis()) + .unwrap_or(0) as _ +} + #[cfg(test)] mod tests { use super::*; diff --git a/libs/scrap/src/common/hwcodec.rs b/libs/scrap/src/common/hwcodec.rs index 7431bc952..ee81627d8 100644 --- a/libs/scrap/src/common/hwcodec.rs +++ b/libs/scrap/src/common/hwcodec.rs @@ -4,10 +4,11 @@ use crate::{ }; use hbb_common::{ anyhow::{anyhow, Context}, + bytes::Bytes, config::HwCodecConfig, - lazy_static, log, + get_time, lazy_static, log, message_proto::{EncodedVideoFrame, EncodedVideoFrames, Message, VideoFrame}, - ResultType, bytes::Bytes, + ResultType, }; use hwcodec::{ decode::{DecodeContext, DecodeFrame, Decoder}, @@ -105,6 +106,7 @@ impl EncoderApi for HwEncoder { DataFormat::H264 => vf.set_h264s(frames), DataFormat::H265 => vf.set_h265s(frames), } + vf.timestamp = get_time(); msg_out.set_video_frame(vf); Ok(msg_out) } else { diff --git a/libs/scrap/src/common/vpxcodec.rs b/libs/scrap/src/common/vpxcodec.rs index 0fda53fa3..47b3df3a6 100644 --- a/libs/scrap/src/common/vpxcodec.rs +++ b/libs/scrap/src/common/vpxcodec.rs @@ -4,15 +4,15 @@ use hbb_common::anyhow::{anyhow, Context}; use hbb_common::message_proto::{EncodedVideoFrame, EncodedVideoFrames, Message, VideoFrame}; -use hbb_common::ResultType; +use hbb_common::{ResultType, get_time}; use crate::codec::EncoderApi; use crate::STRIDE_ALIGN; use super::vpx::{vp8e_enc_control_id::*, vpx_codec_err_t::*, *}; +use hbb_common::bytes::Bytes; use std::os::raw::{c_int, c_uint}; use std::{ptr, slice}; -use hbb_common::bytes::Bytes; #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] pub enum VpxVideoCodecId { @@ -285,6 +285,7 @@ impl VpxEncoder { frames: vp9s.into(), ..Default::default() }); + vf.timestamp = get_time(); msg_out.set_video_frame(vf); msg_out } diff --git a/src/common.rs b/src/common.rs index 68656853a..d5ccf0e1b 100644 --- a/src/common.rs +++ b/src/common.rs @@ -426,14 +426,6 @@ pub fn refresh_rendezvous_server() { }); } -#[inline] -pub fn get_time() -> i64 { - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|d| d.as_millis()) - .unwrap_or(0) as _ -} - pub fn run_me>(args: Vec) -> std::io::Result { #[cfg(not(feature = "appimage"))] { @@ -676,4 +668,4 @@ lazy_static::lazy_static! { #[cfg(target_os = "linux")] lazy_static::lazy_static! { pub static ref IS_X11: Mutex = Mutex::new("x11" == hbb_common::platform::linux::get_display_server()); -} \ No newline at end of file +} diff --git a/src/server/audio_service.rs b/src/server/audio_service.rs index addc06644..c7a720ded 100644 --- a/src/server/audio_service.rs +++ b/src/server/audio_service.rs @@ -13,6 +13,7 @@ // https://github.com/krruzic/pulsectl use super::*; +use hbb_common::get_time; use magnum_opus::{Application::*, Channels::*, Encoder}; use std::sync::atomic::{AtomicBool, Ordering}; @@ -348,7 +349,7 @@ fn send_f32(data: &[f32], encoder: &mut Encoder, sp: &GenericService) { let mut msg_out = Message::new(); msg_out.set_audio_frame(AudioFrame { data: data.into(), - timestamp: crate::common::get_time(), + timestamp: get_time(), ..Default::default() }); sp.send(msg_out); @@ -368,7 +369,7 @@ fn send_f32(data: &[f32], encoder: &mut Encoder, sp: &GenericService) { let mut msg_out = Message::new(); msg_out.set_audio_frame(AudioFrame { data: data.into(), - timestamp: crate::common::get_time(), + timestamp: get_time(), ..Default::default() }); sp.send(msg_out); diff --git a/src/server/connection.rs b/src/server/connection.rs index 42f4bc940..161c058f8 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -12,7 +12,7 @@ use hbb_common::{ fs, fs::can_enable_overwrite_detection, futures::{SinkExt, StreamExt}, - get_version_number, + get_time, get_version_number, message_proto::{option_message::BoolOption, permission_info::Permission}, password_security as password, sleep, timeout, tokio::{ @@ -397,7 +397,7 @@ impl Connection { conn.on_close("Timeout", true).await; break; } - let time = crate::get_time(); + let time = get_time(); if time > 0 && conn.last_test_delay == 0 { conn.last_test_delay = time; let mut msg_out = Message::new(); @@ -983,7 +983,7 @@ impl Connection { .get(&self.ip) .map(|x| x.clone()) .unwrap_or((0, 0, 0)); - let time = (crate::get_time() / 60_000) as i32; + let time = (get_time() / 60_000) as i32; if failure.2 > 30 { self.send_login_error("Too many wrong password attempts") .await; @@ -1022,7 +1022,7 @@ impl Connection { self.inner.send(msg_out.into()); } else { self.last_test_delay = 0; - let new_delay = (crate::get_time() - t.time) as u32; + let new_delay = (get_time() - t.time) as u32; video_service::VIDEO_QOS .lock() .unwrap() @@ -1038,9 +1038,9 @@ impl Connection { #[cfg(not(any(target_os = "android", target_os = "ios")))] if self.keyboard { if is_left_up(&me) { - CLICK_TIME.store(crate::get_time(), Ordering::SeqCst); + CLICK_TIME.store(get_time(), Ordering::SeqCst); } else { - MOUSE_MOVE_TIME.store(crate::get_time(), Ordering::SeqCst); + MOUSE_MOVE_TIME.store(get_time(), Ordering::SeqCst); } self.input_mouse(me, self.inner.id()); } @@ -1049,7 +1049,7 @@ impl Connection { #[cfg(not(any(target_os = "android", target_os = "ios")))] if self.keyboard { if is_enter(&me) { - CLICK_TIME.store(crate::get_time(), Ordering::SeqCst); + CLICK_TIME.store(get_time(), Ordering::SeqCst); } // handle all down as press // fix unexpected repeating key on remote linux, seems also fix abnormal alt/shift, which diff --git a/src/server/input_service.rs b/src/server/input_service.rs index d78441a18..f36f2c50e 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -3,7 +3,7 @@ use crate::common::IS_X11; #[cfg(target_os = "macos")] use dispatch::Queue; use enigo::{Enigo, Key, KeyboardControllable, MouseButton, MouseControllable}; -use hbb_common::{config::COMPRESS_LEVEL, protobuf::EnumOrUnknown}; +use hbb_common::{config::COMPRESS_LEVEL, get_time, protobuf::EnumOrUnknown}; use rdev::{simulate, EventType, Key as RdevKey}; use std::{ convert::TryFrom, @@ -111,7 +111,7 @@ fn run_pos(sp: GenericService, state: &mut StatePos) -> ResultType<()> { ..Default::default() }); let exclude = { - let now = crate::get_time(); + let now = get_time(); let lock = LATEST_INPUT.lock().unwrap(); if now - lock.time < 300 { lock.conn @@ -365,7 +365,7 @@ fn handle_mouse_(evt: &MouseEvent, conn: i32) { let buttons = evt.mask >> 3; let evt_type = evt.mask & 0x7; if evt_type == 0 { - let time = crate::get_time(); + let time = get_time(); *LATEST_INPUT.lock().unwrap() = Input { time, conn }; } let mut en = ENIGO.lock().unwrap(); diff --git a/src/server/video_service.rs b/src/server/video_service.rs index b22418398..eee9e4255 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -601,7 +601,7 @@ fn create_msg(vp9s: Vec) -> Message { frames: vp9s.into(), ..Default::default() }); - vf.timestamp = crate::common::get_time(); + vf.timestamp = hbb_common::get_time(); msg_out.set_video_frame(vf); msg_out } From 76e7bf529341eaefc954453123f42eeaf52970b8 Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 12 Sep 2022 11:23:45 +0800 Subject: [PATCH 0450/2015] add ScrollController to each ScrollView fix "The provided ScrollController is currently attached to more than one ScrollPosition" Signed-off-by: 21pages --- flutter/lib/desktop/pages/connection_page.dart | 1 + flutter/lib/desktop/pages/desktop_setting_page.dart | 9 +++++++-- flutter/lib/desktop/pages/file_manager_page.dart | 3 +++ flutter/lib/desktop/pages/port_forward_page.dart | 2 ++ flutter/lib/desktop/widgets/material_mod_popup_menu.dart | 1 + flutter/lib/desktop/widgets/peer_widget.dart | 1 + flutter/lib/mobile/pages/connection_page.dart | 1 + flutter/lib/mobile/pages/file_manager_page.dart | 1 + flutter/lib/mobile/pages/remote_page.dart | 1 + 9 files changed, 18 insertions(+), 2 deletions(-) diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index a39877b64..64a74d22a 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -1023,6 +1023,7 @@ class _PeerTabbedPageState extends State<_PeerTabbedPage> return ListView( scrollDirection: Axis.horizontal, shrinkWrap: true, + controller: ScrollController(), children: super.widget.tabs.asMap().entries.map((t) { return Obx(() => GestureDetector( child: Container( diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 87d12c9c3..9aae9dc29 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -122,6 +122,7 @@ class _DesktopSettingPageState extends State Widget _listView({required List<_TabInfo> tabs}) { return ListView( + controller: ScrollController(), children: tabs .asMap() .entries @@ -181,6 +182,7 @@ class _GeneralState extends State<_General> { @override Widget build(BuildContext context) { return ListView( + controller: ScrollController(), children: [ theme(), abr(), @@ -300,6 +302,7 @@ class _LanguageState extends State<_Language> Widget build(BuildContext context) { super.build(context); return ListView( + controller: ScrollController(), children: [ _Card(title: 'Language', children: [language()]), ], @@ -353,6 +356,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { Widget build(BuildContext context) { super.build(context); return ListView( + controller: ScrollController(), children: [ Column( children: [ @@ -622,7 +626,7 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin { Widget build(BuildContext context) { super.build(context); bool enabled = !locked; - return ListView(children: [ + return ListView(controller: ScrollController(), children: [ Column( children: [ _lock(locked, 'Unlock Network Settings', () { @@ -657,6 +661,7 @@ class _AcountState extends State<_Acount> { @override Widget build(BuildContext context) { return ListView( + controller: ScrollController(), children: [ _Card(title: 'Acount', children: [login()]), _Card(title: 'ID', children: [changeId()]), @@ -705,7 +710,7 @@ class _AboutState extends State<_About> { final license = data['license'].toString(); final version = data['version'].toString(); const linkStyle = TextStyle(decoration: TextDecoration.underline); - return ListView(children: [ + return ListView(controller: ScrollController(), children: [ _Card(title: "About RustDesk", children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index bd6e4cb63..eee3c226b 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -184,6 +184,7 @@ class _FileManagerPageState extends State children: [ Expanded( child: SingleChildScrollView( + controller: ScrollController(), child: ObxValue( (searchText) { final filteredEntries = searchText.isEmpty @@ -309,6 +310,7 @@ class _FileManagerPageState extends State // Center(child: listTail(isLocal: isLocal)), // Expanded( // child: ListView.builder( + // controller: ScrollController(), // itemCount: entries.length + 1, // itemBuilder: (context, index) { // if (index >= entries.length) { @@ -424,6 +426,7 @@ class _FileManagerPageState extends State decoration: BoxDecoration(border: Border.all(color: Colors.grey)), child: Obx( () => ListView.builder( + controller: ScrollController(), itemBuilder: (BuildContext context, int index) { final item = model.jobTable[index]; return Column( diff --git a/flutter/lib/desktop/pages/port_forward_page.dart b/flutter/lib/desktop/pages/port_forward_page.dart index aa108c2f5..f3e988744 100644 --- a/flutter/lib/desktop/pages/port_forward_page.dart +++ b/flutter/lib/desktop/pages/port_forward_page.dart @@ -133,6 +133,7 @@ class _PortForwardPageState extends State data: Theme.of(context) .copyWith(backgroundColor: MyTheme.color(context).bg), child: Obx(() => ListView.builder( + controller: ScrollController(), itemCount: pfs.length + 2, itemBuilder: ((context, index) { if (index == 0) { @@ -293,6 +294,7 @@ class _PortForwardPageState extends State data: Theme.of(context) .copyWith(backgroundColor: MyTheme.color(context).bg), child: ListView.builder( + controller: ScrollController(), itemCount: 2, itemBuilder: ((context, index) { if (index == 0) { diff --git a/flutter/lib/desktop/widgets/material_mod_popup_menu.dart b/flutter/lib/desktop/widgets/material_mod_popup_menu.dart index 8b0acba9a..1345f72f1 100644 --- a/flutter/lib/desktop/widgets/material_mod_popup_menu.dart +++ b/flutter/lib/desktop/widgets/material_mod_popup_menu.dart @@ -614,6 +614,7 @@ class _PopupMenu extends StatelessWidget { padding: const EdgeInsets.symmetric( vertical: _kMenuVerticalPadding, ), + controller: ScrollController(), child: ListBody(children: children), ), ), diff --git a/flutter/lib/desktop/widgets/peer_widget.dart b/flutter/lib/desktop/widgets/peer_widget.dart index 02b5b9f00..32976fb5b 100644 --- a/flutter/lib/desktop/widgets/peer_widget.dart +++ b/flutter/lib/desktop/widgets/peer_widget.dart @@ -85,6 +85,7 @@ class _PeerWidgetState extends State<_PeerWidget> with WindowListener { child: Text(translate("Empty")), ) : SingleChildScrollView( + controller: ScrollController(), child: ObxValue((searchText) { return FutureBuilder>( builder: (context, snapshot) { diff --git a/flutter/lib/mobile/pages/connection_page.dart b/flutter/lib/mobile/pages/connection_page.dart index ba34b31e8..7549bbae7 100644 --- a/flutter/lib/mobile/pages/connection_page.dart +++ b/flutter/lib/mobile/pages/connection_page.dart @@ -66,6 +66,7 @@ class _ConnectionPageState extends State { Widget build(BuildContext context) { Provider.of(context); return SingleChildScrollView( + controller: ScrollController(), child: Column( mainAxisAlignment: MainAxisAlignment.start, mainAxisSize: MainAxisSize.max, diff --git a/flutter/lib/mobile/pages/file_manager_page.dart b/flutter/lib/mobile/pages/file_manager_page.dart index 87169b987..dd1cbb83f 100644 --- a/flutter/lib/mobile/pages/file_manager_page.dart +++ b/flutter/lib/mobile/pages/file_manager_page.dart @@ -203,6 +203,7 @@ class _FileManagerPageState extends State { headTools(), Expanded( child: ListView.builder( + controller: ScrollController(), itemCount: entries.length + 1, itemBuilder: (context, index) { if (index >= entries.length) { diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index dd3742d32..a9a03c04d 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -759,6 +759,7 @@ class _RemotePageState extends State { expand: false, builder: (context, scrollController) { return SingleChildScrollView( + controller: ScrollController(), padding: EdgeInsets.symmetric(vertical: 10), child: GestureHelp( touchMode: gFFI.ffiModel.touchMode, From 7fce018eeab7329a7f4f1c900b709ad9f9ee99fa Mon Sep 17 00:00:00 2001 From: csf Date: Fri, 9 Sep 2022 19:29:19 +0800 Subject: [PATCH 0451/2015] optimize closeConfirmDialog by using async onWindowCloseButton --- .../desktop/pages/file_manager_tab_page.dart | 21 ++++++- .../desktop/pages/port_forward_tab_page.dart | 3 +- .../lib/desktop/pages/remote_tab_page.dart | 21 ++++++- .../lib/desktop/widgets/tabbar_widget.dart | 59 ++++++++----------- 4 files changed, 63 insertions(+), 41 deletions(-) diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index 42c50b927..de874b42d 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -75,9 +75,7 @@ class _FileManagerTabPageState extends State { backgroundColor: MyTheme.color(context).bg, body: DesktopTab( controller: tabController, - onWindowCloseButton: () { - tabController.clear(); - }, + onWindowCloseButton: handleWindowCloseButton, tail: const AddButton().paddingOnly(left: 10), )), ), @@ -103,4 +101,21 @@ class _FileManagerTabPageState extends State { tabController.closeBy(peerId); } } + + Future handleWindowCloseButton() async { + final connLength = tabController.state.value.tabs.length; + if (connLength < 1) { + return true; + } else if (connLength == 1) { + final currentConn = tabController.state.value.tabs[0]; + handleTabCloseButton(currentConn.key); + return false; + } else { + final res = await closeConfirmDialog(); + if (res) { + tabController.clear(); + } + return res; + } + } } diff --git a/flutter/lib/desktop/pages/port_forward_tab_page.dart b/flutter/lib/desktop/pages/port_forward_tab_page.dart index 5dd69e8eb..dffe7856b 100644 --- a/flutter/lib/desktop/pages/port_forward_tab_page.dart +++ b/flutter/lib/desktop/pages/port_forward_tab_page.dart @@ -78,8 +78,9 @@ class _PortForwardTabPageState extends State { backgroundColor: MyTheme.color(context).bg, body: DesktopTab( controller: tabController, - onWindowCloseButton: () { + onWindowCloseButton: () async { tabController.clear(); + return true; }, tail: AddButton().paddingOnly(left: 10), )), diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index e5caccd71..2f87c51fb 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -97,9 +97,7 @@ class _ConnectionTabPageState extends State { body: DesktopTab( controller: tabController, showTabBar: fullscreen.isFalse, - onWindowCloseButton: () { - tabController.clear(); - }, + onWindowCloseButton: handleWindowCloseButton, tail: AddButton().paddingOnly(left: 10), pageViewBuilder: (pageView) { WindowController.fromWindowId(windowId()) @@ -167,4 +165,21 @@ class _ConnectionTabPageState extends State { tabController.closeBy(peerId); } } + + Future handleWindowCloseButton() async { + final connLength = tabController.state.value.tabs.length; + if (connLength < 1) { + return true; + } else if (connLength == 1) { + final currentConn = tabController.state.value.tabs[0]; + handleTabCloseButton(currentConn.key); + return false; + } else { + final res = await closeConfirmDialog(); + if (res) { + tabController.clear(); + } + return res; + } + } } diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index fef5fbf1f..e812b3e7c 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -183,7 +183,7 @@ class DesktopTab extends StatelessWidget { final bool showClose; final Widget Function(Widget pageView)? pageViewBuilder; final Widget? tail; - final VoidCallback? onWindowCloseButton; + final Future Function()? onWindowCloseButton; final TabBuilder? tabBuilder; final LabelGetter? labelGetter; @@ -356,7 +356,7 @@ class WindowActionPanel extends StatelessWidget { final bool showMinimize; final bool showMaximize; final bool showClose; - final VoidCallback? onClose; + final Future Function()? onClose; const WindowActionPanel( {Key? key, @@ -434,7 +434,8 @@ class WindowActionPanel extends StatelessWidget { message: 'Close', icon: IconFont.close, onTap: () async { - action() { + final res = await onClose?.call() ?? true; + if (res) { if (mainTab) { windowManager.close(); } else { @@ -443,14 +444,6 @@ class WindowActionPanel extends StatelessWidget { WindowController.fromWindowId(windowId!).hide(); }); } - onClose?.call(); - } - - if (tabType != DesktopTabType.main && - state.value.tabs.length > 1) { - closeConfirmDialog(action); - } else { - action(); } }, isClose: true, @@ -458,30 +451,28 @@ class WindowActionPanel extends StatelessWidget { ], ); } +} - closeConfirmDialog(Function() callback) async { - final res = await gFFI.dialogManager.show((setState, close) { - submit() => close(true); - return CustomAlertDialog( - title: Row(children: [ - const Icon(Icons.warning_amber_sharp, - color: Colors.redAccent, size: 28), - const SizedBox(width: 10), - Text(translate("Warning")), - ]), - content: Text(translate("Disconnect all devices?")), - actions: [ - TextButton(onPressed: close, child: Text(translate("Cancel"))), - ElevatedButton(onPressed: submit, child: Text(translate("OK"))), - ], - onSubmit: submit, - onCancel: close, - ); - }); - if (res == true) { - callback(); - } - } +Future closeConfirmDialog() async { + final res = await gFFI.dialogManager.show((setState, close) { + submit() => close(true); + return CustomAlertDialog( + title: Row(children: [ + const Icon(Icons.warning_amber_sharp, + color: Colors.redAccent, size: 28), + const SizedBox(width: 10), + Text(translate("Warning")), + ]), + content: Text(translate("Disconnect all devices?")), + actions: [ + TextButton(onPressed: close, child: Text(translate("Cancel"))), + ElevatedButton(onPressed: submit, child: Text(translate("OK"))), + ], + onSubmit: submit, + onCancel: close, + ); + }); + return res == true; } // ignore: must_be_immutable From f6055130e47dd1530dac8ad0282d4d6f7f21b7d3 Mon Sep 17 00:00:00 2001 From: csf Date: Tue, 13 Sep 2022 09:03:34 +0800 Subject: [PATCH 0452/2015] mv overlay.dart --- flutter/lib/common.dart | 2 +- flutter/lib/{mobile => common}/widgets/overlay.dart | 2 +- flutter/lib/models/chat_model.dart | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename flutter/lib/{mobile => common}/widgets/overlay.dart (99%) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 73334baae..711bdadc6 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -14,7 +14,7 @@ import 'package:get/get.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:window_manager/window_manager.dart'; -import 'mobile/widgets/overlay.dart'; +import 'common/widgets/overlay.dart'; import 'models/model.dart'; import 'models/platform_model.dart'; diff --git a/flutter/lib/mobile/widgets/overlay.dart b/flutter/lib/common/widgets/overlay.dart similarity index 99% rename from flutter/lib/mobile/widgets/overlay.dart rename to flutter/lib/common/widgets/overlay.dart index b8fd8f653..d21bbde96 100644 --- a/flutter/lib/mobile/widgets/overlay.dart +++ b/flutter/lib/common/widgets/overlay.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; +import '../../mobile/pages/chat_page.dart'; import '../../models/chat_model.dart'; -import '../pages/chat_page.dart'; class DraggableChatWindow extends StatelessWidget { DraggableChatWindow( diff --git a/flutter/lib/models/chat_model.dart b/flutter/lib/models/chat_model.dart index 4bdf5826a..cad9a818f 100644 --- a/flutter/lib/models/chat_model.dart +++ b/flutter/lib/models/chat_model.dart @@ -4,8 +4,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:window_manager/window_manager.dart'; -import '../../mobile/widgets/overlay.dart'; import '../common.dart'; +import '../common/widgets/overlay.dart'; import 'model.dart'; class MessageBody { From 062a9d2635b8762ccacb2baf7f3d49586ba6d015 Mon Sep 17 00:00:00 2001 From: csf Date: Tue, 13 Sep 2022 09:29:19 +0800 Subject: [PATCH 0453/2015] update flutter desktop, chat page (in remote page) style --- flutter/lib/common/widgets/overlay.dart | 128 ++++++++++++++++-------- flutter/lib/models/chat_model.dart | 5 +- 2 files changed, 89 insertions(+), 44 deletions(-) diff --git a/flutter/lib/common/widgets/overlay.dart b/flutter/lib/common/widgets/overlay.dart index d21bbde96..0e0a6ce2d 100644 --- a/flutter/lib/common/widgets/overlay.dart +++ b/flutter/lib/common/widgets/overlay.dart @@ -1,15 +1,18 @@ import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; +import '../../desktop/widgets/tabbar_widget.dart'; import '../../mobile/pages/chat_page.dart'; import '../../models/chat_model.dart'; class DraggableChatWindow extends StatelessWidget { - DraggableChatWindow( - {this.position = Offset.zero, + const DraggableChatWindow( + {Key? key, + this.position = Offset.zero, required this.width, required this.height, - required this.chatModel}); + required this.chatModel}) + : super(key: key); final Offset position; final double width; @@ -30,46 +33,84 @@ class DraggableChatWindow extends StatelessWidget { resizeToAvoidBottomInset: false, appBar: CustomAppBar( onPanUpdate: onPanUpdate, - appBar: Container( - color: MyTheme.accent50, - height: 50, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: EdgeInsets.symmetric(horizontal: 15), - child: Text( - translate("Chat"), - style: TextStyle( - color: Colors.white, - fontFamily: 'WorkSans', - fontWeight: FontWeight.bold, - fontSize: 20), - )), - Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - IconButton( - onPressed: () { - chatModel.hideChatWindowOverlay(); - }, - icon: Icon(Icons.keyboard_arrow_down)), - IconButton( - onPressed: () { - chatModel.hideChatWindowOverlay(); - chatModel.hideChatIconOverlay(); - }, - icon: Icon(Icons.close)) - ], - ) - ], - ), - ), + appBar: isDesktop + ? _buildDesktopAppBar() + : _buildMobileAppBar(), ), body: ChatPage(chatModel: chatModel), ); }); } + + Widget _buildMobileAppBar() { + return Container( + color: MyTheme.accent50, + height: 50, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: Text( + translate("Chat"), + style: const TextStyle( + color: Colors.white, + fontFamily: 'WorkSans', + fontWeight: FontWeight.bold, + fontSize: 20), + )), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + IconButton( + onPressed: () { + chatModel.hideChatWindowOverlay(); + }, + icon: const Icon(Icons.keyboard_arrow_down)), + IconButton( + onPressed: () { + chatModel.hideChatWindowOverlay(); + chatModel.hideChatIconOverlay(); + }, + icon: const Icon(Icons.close)) + ], + ) + ], + ), + ); + } + + Widget _buildDesktopAppBar() { + return Container( + color: MyTheme.accent50, + height: 35, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: Text( + translate("Chat"), + style: const TextStyle( + color: Colors.white, + fontFamily: 'WorkSans', + fontWeight: FontWeight.bold), + )), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + ActionIcon( + message: 'Close', + icon: IconFont.close, + onTap: chatModel.hideChatWindowOverlay, + isClose: true, + ) + ], + ) + ], + ), + ); + } } class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { @@ -86,7 +127,7 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { } @override - Size get preferredSize => new Size.fromHeight(kToolbarHeight); + Size get preferredSize => const Size.fromHeight(kToolbarHeight); } /// floating buttons of back/home/recent actions for android @@ -161,13 +202,15 @@ class DraggableMobileActions extends StatelessWidget { } class Draggable extends StatefulWidget { - Draggable( - {this.checkKeyboard = false, + const Draggable( + {Key? key, + this.checkKeyboard = false, this.checkScreenSize = false, this.position = Offset.zero, required this.width, required this.height, - required this.builder}); + required this.builder}) + : super(key: key); final bool checkKeyboard; final bool checkScreenSize; @@ -224,7 +267,6 @@ class _DraggableState extends State { final bottomHeight = MediaQuery.of(context).viewInsets.bottom; final currentVisible = bottomHeight != 0; - debugPrint(bottomHeight.toString() + currentVisible.toString()); // save if (!_keyboardVisible && currentVisible) { _saveHeight = _position.dy; diff --git a/flutter/lib/models/chat_model.dart b/flutter/lib/models/chat_model.dart index cad9a818f..e9952db1e 100644 --- a/flutter/lib/models/chat_model.dart +++ b/flutter/lib/models/chat_model.dart @@ -128,7 +128,10 @@ class ChatModel with ChangeNotifier { if (overlayState == null) return; final overlay = OverlayEntry(builder: (context) { return DraggableChatWindow( - position: Offset(20, 80), width: 250, height: 350, chatModel: this); + position: const Offset(20, 80), + width: 250, + height: 350, + chatModel: this); }); overlayState.insert(overlay); chatWindowOverlayEntry = overlay; From a505b73a56a53ebb7da36f6f462818fc54b807e7 Mon Sep 17 00:00:00 2001 From: Asura Date: Mon, 12 Sep 2022 23:27:07 -0700 Subject: [PATCH 0454/2015] Fix build flutter deb --- DEBIAN/postinst | 13 ++++++++++--- DEBIAN/prerm | 7 ++++++- build.py | 35 +++++++++++++++++++++++++++++------ flutter/linux/CMakeLists.txt | 2 +- 4 files changed, 46 insertions(+), 11 deletions(-) diff --git a/DEBIAN/postinst b/DEBIAN/postinst index d643c5caf..b30664ce0 100755 --- a/DEBIAN/postinst +++ b/DEBIAN/postinst @@ -5,8 +5,10 @@ set -e if [ "$1" = configure ]; then INITSYS=$(ls -al /proc/1/exe | awk -F' ' '{print $NF}' | awk -F'/' '{print $NF}') - + if [ "systemd" == "$INITSYS" ]; then + ln -s /usr/lib/rustdesk/flutter_hbb /usr/bin/rustdesk + if [ -e /etc/systemd/system/rustdesk.service ]; then rm /etc/systemd/system/rustdesk.service /usr/lib/systemd/system/rustdesk.service /usr/lib/systemd/user/rustdesk.service >/dev/null 2>&1 fi @@ -18,7 +20,12 @@ if [ "$1" = configure ]; then systemctl start rustdesk cp /usr/share/rustdesk/files/systemd/rustdesk.service.user /usr/lib/systemd/user/rustdesk.service - curUser=$(who | awk '{print $1}' | head -1) - systemctl --machine=${curUser}@.host --user daemon-reload + ubuntuVersion=$(grep -oP 'VERSION_ID="\K[\d]+' /etc/os-release | bc -l) + waylandSupportVersion=21 + if [ "$ubuntuVersion" -ge "$waylandSupportVersion" ] + then + curUser=$(who | awk '{print $1}' | head -1) + systemctl --machine=${curUser}@.host --user daemon-reload + fi fi fi diff --git a/DEBIAN/prerm b/DEBIAN/prerm index 3bb453198..e9e3931ac 100755 --- a/DEBIAN/prerm +++ b/DEBIAN/prerm @@ -5,12 +5,17 @@ set -e case $1 in remove|upgrade) INITSYS=$(ls -al /proc/1/exe | awk -F' ' '{print $NF}' | awk -F'/' '{print $NF}') + if [ "systemd" == "${INITSYS}" ]; then + rm /usr/bin/rustdesk + systemctl stop rustdesk || true systemctl disable rustdesk || true serverUser=$(ps -ef | grep -E 'rustdesk +--server' | awk '{print $1}' | head -1) - if [ "$serverUser" != "" ] && [ "$serverUser" != "root" ] + ubuntuVersion=$(grep -oP 'VERSION_ID="\K[\d]+' /etc/os-release | bc -l) + waylandSupportVersion=21 + if [ "$serverUser" != "" ] && [ "$serverUser" != "root" ] && [ "$ubuntuVersion" -ge "$waylandSupportVersion" ] then systemctl --machine=${serverUser}@.host --user stop rustdesk || true fi diff --git a/build.py b/build.py index cdca16645..e56d6a41e 100755 --- a/build.py +++ b/build.py @@ -121,15 +121,34 @@ def get_features(args): print("features:", features) return features +def generate_control_file(version): + control_file_path = "../DEBIAN/control" + os.system('/bin/rm -rf %s' % control_file_path) + + content = """Package: rustdesk +Version: %s +Architecture: amd64 +Maintainer: open-trade +Homepage: https://rustdesk.com +Depends: libgtk-3-0, libxcb-randr0, libxdo3, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2, libsystemd0, pulseaudio, curl +Description: A remote control software. + +""" % version + file = open(control_file_path, "w") + file.write(content) + file.close() def build_flutter_deb(version): os.chdir('flutter') os.system('dpkg-deb -R rustdesk.deb tmpdeb') - # os.system('flutter build linux --release') - os.system('rm tmpdeb/usr/bin/rustdesk') + os.system('flutter build linux --release') os.system('strip build/linux/x64/release/liblibrustdesk.so') + + os.system('mkdir -p tmpdeb/usr/bin/') os.system('mkdir -p tmpdeb/usr/lib/rustdesk') os.system('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/') + os.system('mkdir -p tmpdeb/usr/share/applications/') + os.system( 'cp -r build/linux/x64/release/bundle/* tmpdeb/usr/lib/rustdesk/') os.system( @@ -137,18 +156,22 @@ def build_flutter_deb(version): os.system( 'cp build/linux/x64/release/liblibrustdesk.so tmpdeb/usr/lib/rustdesk/librustdesk.so') os.system( - 'cp rustdesk.service tmpdeb/usr/share/rustdesk/files/systemd/') + 'cp ../rustdesk.service tmpdeb/usr/share/rustdesk/files/systemd/') os.system( - 'cp rustdesk.service.user tmpdeb/usr/share/rustdesk/files/systemd/') + 'cp ../rustdesk.service.user tmpdeb/usr/share/rustdesk/files/systemd/') os.system( 'cp ../128x128@2x.png tmpdeb/usr/share/rustdesk/files/rustdesk.png') os.system( - 'cp rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop') + 'cp ../rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop') os.system('mkdir -p tmpdeb/DEBIAN') + generate_control_file(version) os.system('cp -a ../DEBIAN/* tmpdeb/DEBIAN/') md5_file('usr/share/rustdesk/files/systemd/rustdesk.service') md5_file('usr/share/rustdesk/files/systemd/rustdesk.service.user') - os.system('dpkg-deb -b tmpdeb rustdesk.deb; /bin/rm -rf tmpdeb/') + os.system('dpkg-deb -b tmpdeb rustdesk.deb;') + + os.system('/bin/rm -rf tmpdeb/') + os.system('/bin/rm -rf ../DEBIAN/control') os.rename('rustdesk.deb', '../rustdesk-%s.deb' % version) os.chdir("..") diff --git a/flutter/linux/CMakeLists.txt b/flutter/linux/CMakeLists.txt index 9a4e0527b..83778e83d 100644 --- a/flutter/linux/CMakeLists.txt +++ b/flutter/linux/CMakeLists.txt @@ -129,7 +129,7 @@ set(RUSTDESK_LIB_BUILD_TYPE $,debug,release>) string(TOLOWER ${CMAKE_BUILD_TYPE} ${RUSTDESK_LIB_BUILD_TYPE}) message(STATUS "rustdesk lib build type: ${RUSTDESK_LIB_BUILD_TYPE}") -set(RUSTDESK_LIB "../../target/${RUSTDESK_LIB_BUILD_TYPE}/liblibrustdesk.so") +set(RUSTDESK_LIB "../build/linux/x64/${RUSTDESK_LIB_BUILD_TYPE}/liblibrustdesk.so") install(FILES "${RUSTDESK_LIB}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime RENAME librustdesk.so) From 675e199b351c379f4cefc5aeccb51fd721ac0443 Mon Sep 17 00:00:00 2001 From: Asura Date: Tue, 13 Sep 2022 01:50:22 -0700 Subject: [PATCH 0455/2015] Disable keyboard listen --- src/client.rs | 6 +++++- src/flutter.rs | 1 + src/ui_session_interface.rs | 4 ++-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/client.rs b/src/client.rs index a625efb31..cc1f78b78 100644 --- a/src/client.rs +++ b/src/client.rs @@ -4,7 +4,7 @@ use std::{ ops::{Deref, Not}, sync::{atomic::AtomicBool, mpsc, Arc, Mutex, RwLock}, }; - +use std::sync::atomic::Ordering; pub use async_trait::async_trait; #[cfg(not(any(target_os = "android", target_os = "linux")))] use cpal::{ @@ -1891,3 +1891,7 @@ fn decode_id_pk(signed: &[u8], key: &sign::PublicKey) -> ResultType<(String, [u8 bail!("Wrong public length"); } } + +pub fn disable_keyboard_listening() { + crate::ui_session_interface::KEYBOARD_HOOKED.store(false, Ordering::SeqCst); +} \ No newline at end of file diff --git a/src/flutter.rs b/src/flutter.rs index eb66260c9..81b455b04 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -328,6 +328,7 @@ pub fn session_start_(id: &str, event_stream: StreamSink) -> ResultTy *session.event_stream.write().unwrap() = Some(event_stream); let session = session.clone(); std::thread::spawn(move || { + crate::client::disable_keyboard_listening(); io_loop(session); }); Ok(()) diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 43cd35754..7af3a2fbd 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -22,7 +22,7 @@ use std::sync::{Arc, Mutex, RwLock}; /// IS_IN KEYBOARD_HOOKED sciter only pub static IS_IN: AtomicBool = AtomicBool::new(false); -static KEYBOARD_HOOKED: AtomicBool = AtomicBool::new(false); +pub static KEYBOARD_HOOKED: AtomicBool = AtomicBool::new(true); #[cfg(windows)] static mut IS_ALT_GR: bool = false; @@ -1166,7 +1166,7 @@ impl Session { if self.is_port_forward() || self.is_file_transfer() { return; } - if KEYBOARD_HOOKED.swap(true, Ordering::SeqCst) { + if !KEYBOARD_HOOKED.load(Ordering::SeqCst){ return; } log::info!("keyboard hooked"); From 69c49073c6597b376e9b71cbc8e470ca093c256d Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 13 Sep 2022 17:00:59 +0800 Subject: [PATCH 0456/2015] kz --- flutter/pubspec.lock | 384 +++++++++++++++++++++---------------------- src/lang.rs | 3 + 2 files changed, 195 insertions(+), 192 deletions(-) diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index e6fa83b1d..251d3dacb 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -5,245 +5,245 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "46.0.0" + version: "47.0.0" after_layout: dependency: transitive description: name: after_layout - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.2.0" analyzer: dependency: transitive description: name: analyzer - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "4.6.0" + version: "4.7.0" animations: dependency: transitive description: name: animations - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.4" archive: dependency: transitive description: name: archive - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.3.1" args: dependency: transitive description: name: args - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.3.1" async: dependency: transitive description: name: async - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.8.2" back_button_interceptor: dependency: "direct main" description: name: back_button_interceptor - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "6.0.1" boolean_selector: dependency: transitive description: name: boolean_selector - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.0" build: dependency: transitive description: name: build - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.3.0" build_config: dependency: transitive description: name: build_config - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.1.0" build_daemon: dependency: transitive description: name: build_daemon - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.1.0" build_resolvers: dependency: transitive description: name: build_resolvers - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.9" build_runner: dependency: "direct dev" description: name: build_runner - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.2.0" build_runner_core: dependency: transitive description: name: build_runner_core - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "7.2.3" built_collection: dependency: transitive description: name: built_collection - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "5.1.1" built_value: dependency: transitive description: name: built_value - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "8.4.1" cached_network_image: dependency: transitive description: name: cached_network_image - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.2.1" cached_network_image_platform_interface: dependency: transitive description: name: cached_network_image_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.0" cached_network_image_web: dependency: transitive description: name: cached_network_image_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.1" characters: dependency: transitive description: name: characters - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.2.0" charcode: dependency: transitive description: name: charcode - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.3.1" checked_yaml: dependency: transitive description: name: checked_yaml - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.1" clock: dependency: transitive description: name: clock - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.1.0" code_builder: dependency: transitive description: name: code_builder - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "4.2.0" collection: dependency: transitive description: name: collection - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.16.0" contextmenu: dependency: "direct main" description: name: contextmenu - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.0" convert: dependency: transitive description: name: convert - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.2" cross_file: dependency: transitive description: name: cross_file - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.3.3+1" crypto: dependency: transitive description: name: crypto - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.2" csslib: dependency: transitive description: name: csslib - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.17.2" cupertino_icons: dependency: "direct main" description: name: cupertino_icons - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.5" dart_style: dependency: transitive description: name: dart_style - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "2.2.3" + version: "2.2.4" dash_chat_2: dependency: "direct main" description: name: dash_chat_2 - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.0.14" desktop_drop: dependency: "direct main" description: name: desktop_drop - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.3.3" desktop_multi_window: @@ -259,133 +259,133 @@ packages: dependency: "direct main" description: name: device_info_plus - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "4.1.2" device_info_plus_linux: dependency: transitive description: name: device_info_plus_linux - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.0" device_info_plus_macos: dependency: transitive description: name: device_info_plus_macos - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.0" device_info_plus_platform_interface: dependency: transitive description: name: device_info_plus_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.0" device_info_plus_web: dependency: transitive description: name: device_info_plus_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.0" device_info_plus_windows: dependency: transitive description: name: device_info_plus_windows - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "4.0.0" + version: "4.1.0" draggable_float_widget: dependency: "direct main" description: name: draggable_float_widget - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.0.2" event_bus: dependency: transitive description: name: event_bus - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.0" external_path: dependency: "direct main" description: name: external_path - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "1.0.3" fake_async: dependency: transitive description: name: fake_async - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.3.0" ffi: dependency: "direct main" description: name: ffi - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.1" file: dependency: transitive description: name: file - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "6.1.4" firebase_analytics: dependency: "direct main" description: name: firebase_analytics - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "9.3.3" + version: "9.3.4" firebase_analytics_platform_interface: dependency: transitive description: name: firebase_analytics_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "3.3.3" + version: "3.3.4" firebase_analytics_web: dependency: transitive description: name: firebase_analytics_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "0.4.2+3" + version: "0.4.2+4" firebase_core: dependency: transitive description: name: firebase_core - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "1.21.1" + version: "1.22.0" firebase_core_platform_interface: dependency: transitive description: name: firebase_core_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "4.5.1" firebase_core_web: dependency: transitive description: name: firebase_core_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.7.2" fixnum: dependency: transitive description: name: fixnum - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.1" flutter: @@ -397,21 +397,21 @@ packages: dependency: transitive description: name: flutter_blurhash - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.7.0" flutter_breadcrumb: dependency: "direct main" description: name: flutter_breadcrumb - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.1" flutter_cache_manager: dependency: transitive description: name: flutter_cache_manager - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.3.0" flutter_custom_cursor: @@ -427,28 +427,28 @@ packages: dependency: "direct dev" description: name: flutter_launcher_icons - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.9.3" flutter_lints: dependency: "direct dev" description: name: flutter_lints - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.1" flutter_parsed_text: dependency: transitive description: name: flutter_parsed_text - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.2.1" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.7" flutter_rust_bridge: @@ -474,392 +474,392 @@ packages: dependency: "direct dev" description: name: freezed - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.0+1" freezed_annotation: dependency: "direct main" description: name: freezed_annotation - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.0" frontend_server_client: dependency: transitive description: name: frontend_server_client - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.3" get: dependency: "direct main" description: name: get - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "4.6.5" glob: dependency: transitive description: name: glob - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.0" graphs: dependency: transitive description: name: graphs - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.0" html: dependency: transitive description: name: html - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.15.0" http: dependency: "direct main" description: name: http - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.13.5" http_multi_server: dependency: transitive description: name: http_multi_server - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.2.1" http_parser: dependency: transitive description: name: http_parser - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "4.0.1" image: dependency: "direct main" description: name: image - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.2.0" image_picker: dependency: "direct main" description: name: image_picker - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.8.5+3" image_picker_android: dependency: transitive description: name: image_picker_android - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "0.8.5+2" + version: "0.8.5+3" image_picker_for_web: dependency: transitive description: name: image_picker_for_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.8" image_picker_ios: dependency: transitive description: name: image_picker_ios - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "0.8.5+6" + version: "0.8.6" image_picker_platform_interface: dependency: transitive description: name: image_picker_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.6.1" intl: dependency: transitive description: name: intl - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.17.0" io: dependency: transitive description: name: io - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.3" js: dependency: transitive description: name: js - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.6.4" json_annotation: dependency: transitive description: name: json_annotation - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "4.6.0" lints: dependency: transitive description: name: lints - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.0" logging: dependency: transitive description: name: logging - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.2" matcher: dependency: transitive description: name: matcher - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.12.11" material_color_utilities: dependency: transitive description: name: material_color_utilities - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.1.4" menu_base: dependency: transitive description: name: menu_base - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.1.1" meta: dependency: transitive description: name: meta - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.7.0" mime: dependency: transitive description: name: mime - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.2" nested: dependency: transitive description: name: nested - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.0" octo_image: dependency: transitive description: name: octo_image - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.2" package_config: dependency: transitive description: name: package_config - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.0" package_info_plus: dependency: "direct main" description: name: package_info_plus - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.4.3+1" package_info_plus_linux: dependency: transitive description: name: package_info_plus_linux - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.5" package_info_plus_macos: dependency: transitive description: name: package_info_plus_macos - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.3.0" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.2" package_info_plus_web: dependency: transitive description: name: package_info_plus_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.5" package_info_plus_windows: dependency: transitive description: name: package_info_plus_windows - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.1.0" path: dependency: transitive description: name: path - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.8.1" path_provider: dependency: "direct main" description: name: path_provider - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.11" path_provider_android: dependency: transitive description: name: path_provider_android - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.20" path_provider_ios: dependency: transitive description: name: path_provider_ios - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.11" path_provider_linux: dependency: transitive description: name: path_provider_linux - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.7" path_provider_macos: dependency: transitive description: name: path_provider_macos - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.6" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.4" path_provider_windows: dependency: transitive description: name: path_provider_windows - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "2.1.2" + version: "2.1.3" pedantic: dependency: transitive description: name: pedantic - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.11.1" petitparser: dependency: transitive description: name: petitparser - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "5.0.0" platform: dependency: transitive description: name: platform - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.1.0" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.2" pool: dependency: transitive description: name: pool - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.5.1" process: dependency: transitive description: name: process - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "4.2.4" provider: dependency: "direct main" description: name: provider - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "6.0.3" pub_semver: dependency: transitive description: name: pub_semver - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.1" pubspec_parse: dependency: transitive description: name: pubspec_parse - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.2.1" qr_code_scanner: dependency: "direct main" description: name: qr_code_scanner - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.1" quiver: dependency: transitive description: name: quiver - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.1.0" rxdart: dependency: "direct main" description: name: rxdart - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.27.5" screen_retriever: @@ -875,91 +875,91 @@ packages: dependency: "direct main" description: name: scroll_pos - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.3.0" settings_ui: dependency: "direct main" description: name: settings_ui - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.2" shared_preferences: dependency: "direct main" description: name: shared_preferences - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.15" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "2.0.12" + version: "2.0.13" shared_preferences_ios: dependency: transitive description: name: shared_preferences_ios - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.1" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.1" shared_preferences_macos: dependency: transitive description: name: shared_preferences_macos - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.4" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.1.0" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.4" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.1" shelf: dependency: transitive description: name: shelf - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.3.2" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.2" shortid: dependency: transitive description: name: shortid - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.1.2" sky_engine: @@ -971,91 +971,91 @@ packages: dependency: transitive description: name: source_gen - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.2.2" source_span: dependency: transitive description: name: source_span - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.8.2" sqflite: dependency: transitive description: name: sqflite - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.3+1" sqflite_common: dependency: transitive description: name: sqflite_common - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.2.1+1" stack_trace: dependency: transitive description: name: stack_trace - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.10.0" stream_channel: dependency: transitive description: name: stream_channel - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.0" stream_transform: dependency: transitive description: name: stream_transform - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.0" string_scanner: dependency: transitive description: name: string_scanner - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.1.0" synchronized: dependency: transitive description: name: synchronized - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "3.0.0+2" + version: "3.0.0+3" term_glyph: dependency: transitive description: name: term_glyph - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.2.0" test_api: dependency: transitive description: name: test_api - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.4.9" timing: dependency: transitive description: name: timing - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.0" toggle_switch: dependency: "direct main" description: name: toggle_switch - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.4.0" tray_manager: @@ -1071,184 +1071,184 @@ packages: dependency: "direct main" description: name: tuple - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.0" typed_data: dependency: transitive description: name: typed_data - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.3.1" url_launcher: dependency: "direct main" description: name: url_launcher - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "6.1.5" url_launcher_android: dependency: transitive description: name: url_launcher_android - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "6.0.17" + version: "6.0.19" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "6.0.17" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.1" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.0" url_launcher_web: dependency: transitive description: name: url_launcher_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.13" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.1" uuid: dependency: transitive description: name: uuid - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.6" vector_math: dependency: transitive description: name: vector_math - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.2" video_player: dependency: transitive description: name: video_player - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.4.7" video_player_android: dependency: transitive description: name: video_player_android - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.3.9" video_player_avfoundation: dependency: transitive description: name: video_player_avfoundation - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.3.5" video_player_platform_interface: dependency: transitive description: name: video_player_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "5.1.4" video_player_web: dependency: transitive description: name: video_player_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.12" visibility_detector: dependency: "direct main" description: name: visibility_detector - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.3.3" wakelock: dependency: "direct main" description: name: wakelock - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.5.6" wakelock_macos: dependency: transitive description: name: wakelock_macos - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.4.0" wakelock_platform_interface: dependency: transitive description: name: wakelock_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.3.0" wakelock_web: dependency: transitive description: name: wakelock_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.4.0" wakelock_windows: dependency: transitive description: name: wakelock_windows - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "0.2.0" + version: "0.2.1" watcher: dependency: transitive description: name: watcher - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.1" web_socket_channel: dependency: transitive description: name: web_socket_channel - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.2.0" win32: dependency: transitive description: name: win32 - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "2.7.0" + version: "3.0.0" window_manager: dependency: "direct main" description: @@ -1262,28 +1262,28 @@ packages: dependency: transitive description: name: xdg_directories - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.2.0+2" xml: dependency: transitive description: name: xml - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "6.1.0" yaml: dependency: transitive description: name: yaml - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.1.1" zxing2: dependency: "direct main" description: name: zxing2 - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.1.0" sdks: diff --git a/src/lang.rs b/src/lang.rs index 7157f9b42..7ac893ca8 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -21,6 +21,7 @@ mod sk; mod tr; mod tw; mod vn; +mod kz; lazy_static::lazy_static! { pub static ref LANGS: Value = @@ -45,6 +46,7 @@ lazy_static::lazy_static! { ("pl", "Polski"), ("ja", "日本語"), ("ko", "한국어"), + ("kz", "Қазақша"), ]); } @@ -93,6 +95,7 @@ pub fn translate_locale(name: String, locale: &str) -> String { "pl" => pl::T.deref(), "ja" => ja::T.deref(), "ko" => ko::T.deref(), + "kz" => kz::T.deref(), _ => en::T.deref(), }; if let Some(v) = m.get(&name as &str) { From 203d9e39a05da90bb340a22f406717832508bdff Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 13 Sep 2022 18:10:20 +0800 Subject: [PATCH 0457/2015] add polkit for custom authentication && update build.rs Signed-off-by: 21pages --- build.py | 18 ++++++++++-------- com.rustdesk.RustDesk.policy | 22 ++++++++++++++++++++++ src/platform/linux.rs | 10 ++++++++-- 3 files changed, 40 insertions(+), 10 deletions(-) create mode 100644 com.rustdesk.RustDesk.policy diff --git a/build.py b/build.py index cdca16645..5be59503c 100755 --- a/build.py +++ b/build.py @@ -71,7 +71,7 @@ def make_parser(): parser.add_argument( '--hwcodec', action='store_true', - help='Enable feature hwcodec, windows only.' + help='Enable feature hwcodec' ) return parser @@ -124,26 +124,28 @@ def get_features(args): def build_flutter_deb(version): os.chdir('flutter') + os.system('/bin/rm -rf tmpdeb/') os.system('dpkg-deb -R rustdesk.deb tmpdeb') - # os.system('flutter build linux --release') + os.system('flutter build linux --release') os.system('rm tmpdeb/usr/bin/rustdesk') - os.system('strip build/linux/x64/release/liblibrustdesk.so') os.system('mkdir -p tmpdeb/usr/lib/rustdesk') os.system('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/') + os.system('mkdir -p tmpdeb/usr/share/polkit-1/actions') os.system( 'cp -r build/linux/x64/release/bundle/* tmpdeb/usr/lib/rustdesk/') os.system( 'pushd tmpdeb && ln -s /usr/lib/rustdesk/flutter_hbb usr/bin/rustdesk && popd') os.system( - 'cp build/linux/x64/release/liblibrustdesk.so tmpdeb/usr/lib/rustdesk/librustdesk.so') + 'cp ../rustdesk.service tmpdeb/usr/share/rustdesk/files/systemd/') os.system( - 'cp rustdesk.service tmpdeb/usr/share/rustdesk/files/systemd/') - os.system( - 'cp rustdesk.service.user tmpdeb/usr/share/rustdesk/files/systemd/') + 'cp ../rustdesk.service.user tmpdeb/usr/share/rustdesk/files/systemd/') os.system( 'cp ../128x128@2x.png tmpdeb/usr/share/rustdesk/files/rustdesk.png') os.system( - 'cp rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop') + 'cp ../rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop') + os.system( + 'cp ../com.rustdesk.RustDesk.policy tmpdeb/usr/share/polkit-1/actions/') + os.system("echo \"#!/bin/sh\" >> tmpdeb/usr/share/rustdesk/files/polkit && chmod a+x tmpdeb/usr/share/rustdesk/files/polkit") os.system('mkdir -p tmpdeb/DEBIAN') os.system('cp -a ../DEBIAN/* tmpdeb/DEBIAN/') md5_file('usr/share/rustdesk/files/systemd/rustdesk.service') diff --git a/com.rustdesk.RustDesk.policy b/com.rustdesk.RustDesk.policy new file mode 100644 index 000000000..a77223120 --- /dev/null +++ b/com.rustdesk.RustDesk.policy @@ -0,0 +1,22 @@ + + + + RustDesk + https://rustdesk.com/ + rustdesk + + Change RustDesk options + Authentication is required to change RustDesk options + 要更改RustDesk选项, 需要您先通过身份验证 + 要變更RustDesk選項, 需要您先通過身份驗證 + /usr/share/rustdesk/files/polkit + true + + auth_admin + auth_admin + auth_admin + + + diff --git a/src/platform/linux.rs b/src/platform/linux.rs index fe2673832..89e2d296d 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -638,7 +638,13 @@ pub fn quit_gui() { } pub fn check_super_user_permission() -> ResultType { - // TODO: replace echo with a rustdesk's program, which is location-fixed and non-gui. - let status = std::process::Command::new("pkexec").arg("echo").status()?; + let file = "/usr/share/rustdesk/files/polkit"; + let arg; + if std::path::Path::new(file).is_file() { + arg = file; + } else { + arg = "echo"; + } + let status = std::process::Command::new("pkexec").arg(arg).status()?; Ok(status.success() && status.code() == Some(0)) } From 2f1092afaf31fa8390e785470beac91327c7c808 Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 13 Sep 2022 19:38:50 +0800 Subject: [PATCH 0458/2015] refactor audio setting Signed-off-by: 21pages --- .../desktop/pages/desktop_setting_page.dart | 24 +++++++------------ 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 9aae9dc29..effc26b39 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -265,22 +265,14 @@ class _GeneralState extends State<_General> { if (devices.isEmpty) { return const Offstage(); } - List keys = devices.toList(); - List values = devices.toList(); - // TODO - if (!devices.contains(currentDevice)) { - currentDevice = ""; - keys.insert(0, currentDevice); - values.insert(0, 'default'); - } return _Card(title: 'Audio Input Device', children: [ - _ComboBox( - keys: keys, - values: values, - initialKey: currentDevice, - onChanged: (key) { - setDevice(key); - }).marginOnly(left: _kContentHMargin), + ...devices.map((device) => _Radio(context, + value: device, + groupValue: currentDevice, + label: device, onChanged: (value) { + setDevice(value); + setState(() {}); + })) ]); }); } @@ -876,6 +868,8 @@ Widget _Radio(BuildContext context, Radio(value: value, groupValue: groupValue, onChanged: onChange), Expanded( child: Text(translate(label), + maxLines: 1, + overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: _kContentFontSize, color: _disabledTextColor(context, enabled))) From a8e501cb79beef47bad7fd6f0a69dd0298a02f5b Mon Sep 17 00:00:00 2001 From: Asura Date: Tue, 13 Sep 2022 02:20:25 -0700 Subject: [PATCH 0459/2015] Fix compile error on Android --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index d778cc279..95ae98409 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,6 +61,7 @@ default-net = "0.11.0" wol-rs = "0.9.1" flutter_rust_bridge = { git = "https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge", optional = true } errno = "0.2.8" +rdev = { git = "https://github.com/asur4s/rdev" } [target.'cfg(not(target_os = "linux"))'.dependencies] reqwest = { version = "0.11", features = ["json", "rustls-tls"], default-features=false } @@ -75,7 +76,6 @@ sciter-rs = { git = "https://github.com/open-trade/rust-sciter", branch = "dyn" sys-locale = "0.2" enigo = { path = "libs/enigo", features = [ "with_serde" ] } clipboard = { path = "libs/clipboard" } -rdev = { git = "https://github.com/asur4s/rdev" } ctrlc = "3.2" arboard = "2.0" #minreq = { version = "2.4", features = ["punycode", "https-native"] } From 7ffa407604b991847b23f5998da082ea8632d4e0 Mon Sep 17 00:00:00 2001 From: Asura Date: Tue, 13 Sep 2022 06:19:08 -0700 Subject: [PATCH 0460/2015] Fix logger without ui --- Cargo.lock | 2 +- flutter/linux/CMakeLists.txt | 2 +- src/core_main.rs | 5 ++++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5c3313336..3a6abcaf7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3975,7 +3975,7 @@ dependencies = [ [[package]] name = "rdev" version = "0.5.0-2" -source = "git+https://github.com/asur4s/rdev#895c8fb1a6106714793e8877d35d2b7a1c57ce9c" +source = "git+https://github.com/asur4s/rdev#0ad53987fa6f0e37a7bc000358f71c3802de4e7c" dependencies = [ "cocoa", "core-foundation 0.9.3", diff --git a/flutter/linux/CMakeLists.txt b/flutter/linux/CMakeLists.txt index 83778e83d..9a4e0527b 100644 --- a/flutter/linux/CMakeLists.txt +++ b/flutter/linux/CMakeLists.txt @@ -129,7 +129,7 @@ set(RUSTDESK_LIB_BUILD_TYPE $,debug,release>) string(TOLOWER ${CMAKE_BUILD_TYPE} ${RUSTDESK_LIB_BUILD_TYPE}) message(STATUS "rustdesk lib build type: ${RUSTDESK_LIB_BUILD_TYPE}") -set(RUSTDESK_LIB "../build/linux/x64/${RUSTDESK_LIB_BUILD_TYPE}/liblibrustdesk.so") +set(RUSTDESK_LIB "../../target/${RUSTDESK_LIB_BUILD_TYPE}/liblibrustdesk.so") install(FILES "${RUSTDESK_LIB}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime RENAME librustdesk.so) diff --git a/src/core_main.rs b/src/core_main.rs index 02ac5e646..cecbf4115 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -1,6 +1,6 @@ use hbb_common::log; -use crate::{start_os_service, flutter::connection_manager, start_server}; +use crate::{flutter::connection_manager, start_os_service, start_server}; /// Main entry of the RustDesk Core. /// Return true if the app should continue running with UI(possibly Flutter), false if the app should exit. @@ -14,6 +14,9 @@ pub fn core_main() -> bool { connection_manager::start_listen_ipc_thread(); return true; } + + use hbb_common::env_logger::*; + init_from_env(Env::default().filter_or(DEFAULT_FILTER_ENV, "info")); if args[1] == "--service" { log::info!("start --service"); start_os_service(); From a075385a114530314d1f8d133de73494e31a16ec Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 13 Sep 2022 06:59:06 -0700 Subject: [PATCH 0461/2015] flutter_desktop: fix resize scale && Pin peer menu bar Signed-off-by: fufesou --- .../lib/desktop/pages/desktop_home_page.dart | 100 +----------------- flutter/lib/desktop/pages/remote_page.dart | 13 ++- .../lib/desktop/widgets/remote_menubar.dart | 48 ++++++++- flutter/lib/models/model.dart | 80 ++++++++++++-- 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/fr.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/pl.rs | 1 + src/lang/pt_PT.rs | 1 + src/lang/ptbr.rs | 1 + src/lang/ru.rs | 1 + src/lang/sk.rs | 1 + src/lang/template.rs | 1 + src/lang/tr.rs | 1 + src/lang/tw.rs | 1 + src/lang/vn.rs | 1 + 25 files changed, 146 insertions(+), 116 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 47f1cc026..25d1ae66d 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -330,18 +330,6 @@ class _DesktopHomePageState extends State onHover: (value) => refreshHover.value = value, ), const _PasswordPopupMenu(), - // FutureBuilder( - // future: buildPasswordPopupMenu(context), - // builder: (context, snapshot) { - // if (snapshot.hasError) { - // print("${snapshot.error}"); - // } - // if (snapshot.hasData) { - // return snapshot.data!; - // } else { - // return Offstage(); - // } - // }) ], ), ], @@ -353,92 +341,6 @@ class _DesktopHomePageState extends State ); } - Future buildPasswordPopupMenu(BuildContext context) async { - var position; - RxBool editHover = false.obs; - return InkWell( - onTapDown: (detail) { - final x = detail.globalPosition.dx; - final y = detail.globalPosition.dy; - position = RelativeRect.fromLTRB(x, y, x, y); - }, - onTap: () async { - var method = (String text, String value) => PopupMenuItem( - child: Row( - children: [ - Offstage( - offstage: gFFI.serverModel.verificationMethod != value, - child: Icon(Icons.check)), - Text( - text, - ), - ], - ), - onTap: () => gFFI.serverModel.setVerificationMethod(value), - ); - final temporary_enabled = - gFFI.serverModel.verificationMethod != kUsePermanentPassword; - var menu = [ - method(translate("Use temporary password"), kUseTemporaryPassword), - method(translate("Use permanent password"), kUsePermanentPassword), - method(translate("Use both passwords"), kUseBothPasswords), - PopupMenuDivider(), - PopupMenuItem( - child: Text(translate("Set permanent password")), - value: 'set-permanent-password', - enabled: gFFI.serverModel.verificationMethod != - kUseTemporaryPassword), - PopupMenuItem( - child: PopupMenuButton( - padding: EdgeInsets.zero, - child: Text( - translate("Set temporary password length"), - ), - itemBuilder: (context) => ["6", "8", "10"] - .map((e) => PopupMenuItem( - child: Row( - children: [ - Offstage( - offstage: gFFI.serverModel - .temporaryPasswordLength != - e, - child: Icon(Icons.check)), - Text( - e, - ), - ], - ), - onTap: () { - if (gFFI.serverModel.temporaryPasswordLength != - e) { - () async { - await gFFI.serverModel - .setTemporaryPasswordLength(e); - await bind.mainUpdateTemporaryPassword(); - }(); - } - }, - )) - .toList(), - enabled: temporary_enabled, - ), - enabled: temporary_enabled), - ]; - final v = - await showMenu(context: context, position: position, items: menu); - if (v == "set-permanent-password") { - setPasswordDialog(); - } - }, - onHover: (value) => editHover.value = value, - child: Obx(() => Icon(Icons.edit, - size: 22, - color: editHover.value - ? MyTheme.color(context).text - : Color(0xFFDDDDDD)) - .marginOnly(bottom: 2))); - } - buildTip(BuildContext context) { return Padding( padding: @@ -469,7 +371,7 @@ class _DesktopHomePageState extends State @override void onTrayMenuItemClick(MenuItem menuItem) { - print("click ${menuItem.key}"); + print('click ${menuItem.key}'); switch (menuItem.key) { case "quit": exit(0); diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 29e99e6a7..35ab548c3 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -50,6 +50,8 @@ class _RemotePageState extends State var _isPhysicalMouse = false; var _imageFocused = false; + final _onEnterOrLeaveImage = []; + late FFI _ffi; void _updateTabBarHeight() { @@ -421,11 +423,17 @@ class _RemotePageState extends State _physicalFocusNode.requestFocus(); } _cursorOverImage.value = true; + for (var f in _onEnterOrLeaveImage) { + f(true); + } _ffi.enterOrLeave(true); } void leaveView(PointerExitEvent evt) { _cursorOverImage.value = false; + for (var f in _onEnterOrLeaveImage) { + f(false); + } _ffi.enterOrLeave(false); } @@ -469,6 +477,7 @@ class _RemotePageState extends State paints.add(RemoteMenubar( id: widget.id, ffi: _ffi, + onEnterOrLeaveImage: _onEnterOrLeaveImage, )); return Stack( children: paints, @@ -597,8 +606,8 @@ class ImagePaint extends StatelessWidget { return FlutterCustomMemoryImageCursor( pixbuf: cacheLinux.data, key: key, - hotx: 0.0, - hoty: 0.0, + hotx: cacheLinux.hotx, + hoty: cacheLinux.hoty, imageWidth: (cacheLinux.width * scale).toInt(), imageHeight: (cacheLinux.height * scale).toInt(), ); diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 327da889f..47ac2094b 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -25,11 +25,13 @@ class _MenubarTheme { class RemoteMenubar extends StatefulWidget { final String id; final FFI ffi; + final List onEnterOrLeaveImage; const RemoteMenubar({ Key? key, required this.id, required this.ffi, + required this.onEnterOrLeaveImage, }) : super(key: key); @override @@ -39,12 +41,38 @@ class RemoteMenubar extends StatefulWidget { class _RemoteMenubarState extends State { final RxBool _show = false.obs; final Rx _hideColor = Colors.white12.obs; + final _rxHideReplay = rxdart.ReplaySubject(); + final _pinMenubar = false.obs; + bool _isCursorOverImage = false; bool get isFullscreen => Get.find(tag: 'fullscreen').isTrue; - void setFullscreen(bool v) { + void _setFullscreen(bool v) { Get.find(tag: 'fullscreen').value = v; } + @override + void initState() { + super.initState(); + + widget.onEnterOrLeaveImage.add((enter) { + if (enter) { + _rxHideReplay.add(0); + _isCursorOverImage = true; + } else { + _isCursorOverImage = false; + } + }); + + _rxHideReplay + .throttleTime(const Duration(milliseconds: 5000), + trailing: true, leading: false) + .listen((int v) { + if (_pinMenubar.isFalse && _show.isTrue && _isCursorOverImage) { + _show.value = false; + } + }); + } + @override Widget build(BuildContext context) { return Align( @@ -76,6 +104,7 @@ class _RemoteMenubarState extends State { Widget _buildMenubar(BuildContext context) { final List menubarItems = []; if (!isWebDesktop) { + menubarItems.add(_buildPinMenubar(context)); menubarItems.add(_buildFullscreen(context)); if (widget.ffi.ffiModel.isPeerAndroid) { menubarItems.add(IconButton( @@ -111,11 +140,24 @@ class _RemoteMenubarState extends State { ])); } + Widget _buildPinMenubar(BuildContext context) { + return IconButton( + tooltip: translate('Pin menubar'), + onPressed: () { + _pinMenubar.value = !_pinMenubar.value; + }, + icon: Obx(() => Icon( + Icons.push_pin, + color: _pinMenubar.isTrue ? _MenubarTheme.commonColor : Colors.grey, + )), + ); + } + Widget _buildFullscreen(BuildContext context) { return IconButton( tooltip: translate(isFullscreen ? 'Exit Fullscreen' : 'Fullscreen'), onPressed: () { - setFullscreen(!isFullscreen); + _setFullscreen(!isFullscreen); }, icon: Obx(() => isFullscreen ? const Icon( @@ -250,7 +292,6 @@ class _RemoteMenubarState extends State { ), tooltip: translate('Display Settings'), position: mod_menu.PopupMenuPosition.under, - onSelected: (String item) {}, itemBuilder: (BuildContext context) => _getDisplayMenu() .map((entry) => entry.build( context, @@ -273,7 +314,6 @@ class _RemoteMenubarState extends State { ), tooltip: translate('Keyboard Settings'), position: mod_menu.PopupMenuPosition.under, - onSelected: (String item) {}, itemBuilder: (BuildContext context) => _getKeyboardMenu() .map((entry) => entry.build( context, diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 7fe2b6767..c37ae1046 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -409,6 +409,56 @@ enum ScrollStyle { scrollauto, } +class ViewStyle { + final String style; + final double width; + final double height; + final int displayWidth; + final int displayHeight; + ViewStyle({ + this.style = '', + this.width = 0.0, + this.height = 0.0, + this.displayWidth = 0, + this.displayHeight = 0, + }); + + static int _double2Int(double v) => (v * 100).round().toInt(); + + @override + bool operator ==(Object other) => + other is ViewStyle && + other.runtimeType == runtimeType && + _innerEqual(other); + + bool _innerEqual(ViewStyle other) { + return style == other.style && + ViewStyle._double2Int(other.width) == ViewStyle._double2Int(width) && + ViewStyle._double2Int(other.height) == ViewStyle._double2Int(height) && + other.displayWidth == displayWidth && + other.displayHeight == displayHeight; + } + + @override + int get hashCode => Object.hash( + style, + ViewStyle._double2Int(width), + ViewStyle._double2Int(height), + displayWidth, + displayHeight, + ).hashCode; + + double get scale { + double s = 1.0; + if (style == 'adaptive') { + final s1 = width / displayWidth; + final s2 = height / displayHeight; + s = s1 < s2 ? s1 : s2; + } + return s; + } +} + class CanvasModel with ChangeNotifier { // image offset of canvas double _x = 0; @@ -425,7 +475,7 @@ class CanvasModel with ChangeNotifier { // scroll offset y percent double _scrollY = 0.0; ScrollStyle _scrollStyle = ScrollStyle.scrollauto; - String? _viewStyle; + ViewStyle _lastViewStyle = ViewStyle(); WeakReference parent; @@ -446,19 +496,27 @@ class CanvasModel with ChangeNotifier { updateViewStyle() async { final style = await bind.sessionGetOption(id: id, arg: 'view-style'); - if (style == null || _viewStyle == style) { + if (style == null) { return; } - - _scale = 1.0; - if (style == 'adaptive') { - final s1 = size.width / getDisplayWidth(); - final s2 = size.height / getDisplayHeight(); - _scale = s1 < s2 ? s1 : s2; + final sizeWidth = size.width; + final sizeHeight = size.height; + final displayWidth = getDisplayWidth(); + final displayHeight = getDisplayHeight(); + final viewStyle = ViewStyle( + style: style, + width: sizeWidth, + height: sizeHeight, + displayWidth: displayWidth, + displayHeight: displayHeight, + ); + if (_lastViewStyle == viewStyle) { + return; } - _viewStyle = style; - _x = (size.width - getDisplayWidth() * _scale) / 2; - _y = (size.height - getDisplayHeight() * _scale) / 2; + _lastViewStyle = viewStyle; + _scale = viewStyle.scale; + _x = (sizeWidth - displayWidth * _scale) / 2; + _y = (sizeHeight - displayHeight * _scale) / 2; notifyListeners(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index abb772d51..62d2cce23 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -344,5 +344,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Use IP Whitelisting", "只允许白名单上的IP访问"), ("Network", "网络"), ("Enable RDP", "允许RDP访问"), + ("Pin menubar", "固定菜单栏"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 5061fbb88..3b317f864 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -344,5 +344,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Use IP Whitelisting", ""), ("Network", ""), ("Enable RDP", ""), + ("Pin menubar", "Připnout panel nabídek"), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index ed5e3425b..1180a43d4 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -344,5 +344,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Use IP Whitelisting", ""), ("Network", ""), ("Enable RDP", ""), + ("Pin menubar", "Fastgør menulinjen"), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 43f0b2f8e..f5278d2d0 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -344,5 +344,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Use IP Whitelisting", ""), ("Network", ""), ("Enable RDP", ""), + ("Pin menubar", "Pin-Menüleiste"), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index d7f038b41..58143d80d 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -344,5 +344,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Use IP Whitelisting", ""), ("Network", ""), ("Enable RDP", ""), + ("Pin menubar", "Alpingla menubreto"), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 7c13e849c..0674840c7 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -357,5 +357,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Use IP Whitelisting", ""), ("Network", ""), ("Enable RDP", ""), + ("Pin menubar", "Pin barra de menú"), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 3e46a6b40..e4387d773 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -344,5 +344,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Use IP Whitelisting", ""), ("Network", ""), ("Enable RDP", ""), + ("Pin menubar", "Épingler la barre de menus"), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 78089075c..8830bdfc0 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -344,5 +344,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Use IP Whitelisting", ""), ("Network", ""), ("Enable RDP", ""), + ("Pin menubar", "Menüsor rögzítése"), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index a9cc27767..16fa641f2 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -357,5 +357,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Use IP Whitelisting", ""), ("Network", ""), ("Enable RDP", ""), + ("Pin menubar", "Pin menubar"), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 8749e2976..c5a3a3330 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -343,5 +343,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Use IP Whitelisting", ""), ("Network", ""), ("Enable RDP", ""), + ("Pin menubar", "Aggiungi barra dei menu"), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 30181b7a5..2a160f744 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -341,5 +341,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Use IP Whitelisting", ""), ("Network", ""), ("Enable RDP", ""), + ("Pin menubar", "メニューバーを固定する"), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 7b8377206..6b866a1ee 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -338,5 +338,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Use IP Whitelisting", ""), ("Network", ""), ("Enable RDP", ""), + ("Pin menubar", "핀 메뉴 바"), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 1088cfa72..3f8f81e2b 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -342,5 +342,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Use IP Whitelisting", ""), ("Network", ""), ("Enable RDP", ""), + ("Pin menubar", "Przypnij pasek menu"), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index cb6fcf548..aeceec759 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -338,5 +338,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Use IP Whitelisting", ""), ("Network", ""), ("Enable RDP", ""), + ("Pin menubar", "Fixar barra de menu"), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index cba544380..3dc651743 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -344,5 +344,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Use IP Whitelisting", ""), ("Network", ""), ("Enable RDP", ""), + ("Pin menubar", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 6fb7078ac..9e890e7d8 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -344,5 +344,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Use IP Whitelisting", ""), ("Network", ""), ("Enable RDP", ""), + ("Pin menubar", "Закрепить строку меню"), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index ebc9571af..74bc27b26 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -344,5 +344,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Use IP Whitelisting", ""), ("Network", ""), ("Enable RDP", ""), + ("Pin menubar", "Pripnúť panel s ponukami"), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 5d3061a05..9a6dc431f 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -344,5 +344,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Use IP Whitelisting", ""), ("Network", ""), ("Enable RDP", ""), + ("Pin menubar", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 6cf6c1298..823d31043 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -357,5 +357,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Use IP Whitelisting", ""), ("Network", ""), ("Enable RDP", ""), + ("Pin menubar", "Menü çubuğunu sabitle"), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index f312c7f47..dd7178122 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -344,5 +344,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Use IP Whitelisting", "只允許白名單上的IP訪問"), ("Network", "網絡"), ("Enable RDP", "允許RDP訪問"), + ("Pin menubar", "固定菜單欄"), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 94beafb67..5d11ca6b9 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -344,5 +344,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Use IP Whitelisting", ""), ("Network", ""), ("Enable RDP", ""), + ("Pin menubar", "Ghim thanh menu"), ].iter().cloned().collect(); } From 27e0bdca97bc95ac998e3c48ccf5a9f8bc102ebb Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 13 Sep 2022 07:24:06 -0700 Subject: [PATCH 0462/2015] flutter_desktop: pin menubar, rotate icon Signed-off-by: fufesou --- .../lib/desktop/widgets/remote_menubar.dart | 26 ++++++++++++------- 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/fr.rs | 1 + src/lang/hu.rs | 1 + src/lang/id.rs | 1 + src/lang/it.rs | 3 ++- src/lang/ja.rs | 1 + src/lang/ko.rs | 1 + src/lang/kz.rs | 2 ++ src/lang/pl.rs | 1 + src/lang/pt_PT.rs | 1 + src/lang/ptbr.rs | 1 + src/lang/ru.rs | 1 + src/lang/sk.rs | 1 + src/lang/template.rs | 1 + src/lang/tr.rs | 1 + src/lang/tw.rs | 1 + src/lang/vn.rs | 1 + 23 files changed, 40 insertions(+), 11 deletions(-) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 47ac2094b..7c8fd20b5 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -141,16 +142,21 @@ class _RemoteMenubarState extends State { } Widget _buildPinMenubar(BuildContext context) { - return IconButton( - tooltip: translate('Pin menubar'), - onPressed: () { - _pinMenubar.value = !_pinMenubar.value; - }, - icon: Obx(() => Icon( - Icons.push_pin, - color: _pinMenubar.isTrue ? _MenubarTheme.commonColor : Colors.grey, - )), - ); + return Obx(() => IconButton( + tooltip: + translate(_pinMenubar.isTrue ? 'Unpin menubar' : 'Pin menubar'), + onPressed: () { + _pinMenubar.value = !_pinMenubar.value; + }, + icon: Obx(() => Transform.rotate( + angle: _pinMenubar.isTrue ? math.pi / 4 : 0, + child: Icon( + Icons.push_pin, + color: _pinMenubar.isTrue + ? _MenubarTheme.commonColor + : Colors.grey, + ))), + )); } Widget _buildFullscreen(BuildContext context) { diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 62d2cce23..738595aa9 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -345,5 +345,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Network", "网络"), ("Enable RDP", "允许RDP访问"), ("Pin menubar", "固定菜单栏"), + ("Unpin menubar", "取消固定菜单栏"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 3b317f864..ace56788f 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -345,5 +345,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Network", ""), ("Enable RDP", ""), ("Pin menubar", "Připnout panel nabídek"), + ("Unpin menubar", "Odepnout panel nabídek"), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 1180a43d4..27724f7b3 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -345,5 +345,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Network", ""), ("Enable RDP", ""), ("Pin menubar", "Fastgør menulinjen"), + ("Unpin menubar", "Frigør menulinjen"), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index f5278d2d0..8d90be381 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -345,5 +345,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Network", ""), ("Enable RDP", ""), ("Pin menubar", "Pin-Menüleiste"), + ("Unpin menubar", "Menüleiste lösen"), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 58143d80d..6c7bb5aa8 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -345,5 +345,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Network", ""), ("Enable RDP", ""), ("Pin menubar", "Alpingla menubreto"), + ("Unpin menubar", "Malfiksi menubreton"), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 0674840c7..c8296ced5 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -358,5 +358,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Network", ""), ("Enable RDP", ""), ("Pin menubar", "Pin barra de menú"), + ("Unpin menubar", "Desbloquear barra de menú"), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index e4387d773..d9a42e934 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -345,5 +345,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Network", ""), ("Enable RDP", ""), ("Pin menubar", "Épingler la barre de menus"), + ("Unpin menubar", "Détacher la barre de menu"), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 8830bdfc0..b35224c03 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -345,5 +345,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Network", ""), ("Enable RDP", ""), ("Pin menubar", "Menüsor rögzítése"), + ("Unpin menubar", "Menüsor rögzítésének feloldása"), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 16fa641f2..657014141 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -358,5 +358,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Network", ""), ("Enable RDP", ""), ("Pin menubar", "Pin menubar"), + ("Unpin menubar", "Unpin menubar"), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index c5a3a3330..8f6dfb3d9 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -343,6 +343,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Use IP Whitelisting", ""), ("Network", ""), ("Enable RDP", ""), - ("Pin menubar", "Aggiungi barra dei menu"), + ("Pin menubar", "Blocca la barra dei menu"), + ("Unpin menubar", "Sblocca la barra dei menu"), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 2a160f744..6d0a2a2f7 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -342,5 +342,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Network", ""), ("Enable RDP", ""), ("Pin menubar", "メニューバーを固定する"), + ("Unpin menubar", "メニューバーのピン留めを外す"), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 6b866a1ee..ca939e2b8 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -339,5 +339,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Network", ""), ("Enable RDP", ""), ("Pin menubar", "핀 메뉴 바"), + ("Unpin menubar", "메뉴 모음 고정 해제"), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 8b7b3402d..720b7109f 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -319,5 +319,7 @@ lazy_static::lazy_static! { ("Insecure Connection", "Қатерлі Қосылым"), ("Scale original", "Scale original"), ("Scale adaptive", "Scale adaptive"), + ("Pin menubar", "Мәзір жолағын бекіту"), + ("Unpin menubar", "Мәзір жолағын босату"), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 3f8f81e2b..fe45ddf3e 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -343,5 +343,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Network", ""), ("Enable RDP", ""), ("Pin menubar", "Przypnij pasek menu"), + ("Unpin menubar", "Odepnij pasek menu"), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index aeceec759..858afd8a1 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -339,5 +339,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Network", ""), ("Enable RDP", ""), ("Pin menubar", "Fixar barra de menu"), + ("Unpin menubar", "Desenganxa la barra de menús"), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 3dc651743..af4f0b52e 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -345,5 +345,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Network", ""), ("Enable RDP", ""), ("Pin menubar", ""), + ("Unpin menubar", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 9e890e7d8..04cfed485 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -345,5 +345,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Network", ""), ("Enable RDP", ""), ("Pin menubar", "Закрепить строку меню"), + ("Unpin menubar", "Открепить строку меню"), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 74bc27b26..8ae17b1ad 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -345,5 +345,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Network", ""), ("Enable RDP", ""), ("Pin menubar", "Pripnúť panel s ponukami"), + ("Unpin menubar", "Uvoľniť panel s ponukami"), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 9a6dc431f..914b103df 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -345,5 +345,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Network", ""), ("Enable RDP", ""), ("Pin menubar", ""), + ("Unpin menubar", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 823d31043..b1b029b39 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -358,5 +358,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Network", ""), ("Enable RDP", ""), ("Pin menubar", "Menü çubuğunu sabitle"), + ("Unpin menubar", "Menü çubuğunun sabitlemesini kaldır"), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index dd7178122..764f666e7 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -345,5 +345,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Network", "網絡"), ("Enable RDP", "允許RDP訪問"), ("Pin menubar", "固定菜單欄"), + ("Unpin menubar", "取消固定菜單欄"), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 5d11ca6b9..f177581f9 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -345,5 +345,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Network", ""), ("Enable RDP", ""), ("Pin menubar", "Ghim thanh menu"), + ("Unpin menubar", "Bỏ ghim thanh menu"), ].iter().cloned().collect(); } From ccb60ace8fde3fde2d4bd9bde93b90e3994a8674 Mon Sep 17 00:00:00 2001 From: csf Date: Tue, 13 Sep 2022 16:28:22 +0800 Subject: [PATCH 0463/2015] fix mouse out of bounds --- flutter/lib/models/model.dart | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index c37ae1046..c6f38534a 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -1240,8 +1240,8 @@ class FFI { return; } evt['type'] = type; - var x = evt['x']; - var y = max(0.0, (evt['y'] as double) - tabBarHeight); + double x = evt['x']; + double y = max(0.0, (evt['y'] as double) - tabBarHeight); if (isMove) { canvasModel.moveDesktopMouse(x, y); } @@ -1264,9 +1264,6 @@ class FFI { y -= canvasModel.y; } - if (!isMove && (x < 0 || x > d.width || y < 0 || y > d.height)) { - return; - } x /= canvasModel.scale; y /= canvasModel.scale; x += d.x; @@ -1275,6 +1272,9 @@ class FFI { x = 0; y = 0; } + // fix mouse out of bounds + x = min(max(0.0, x), d.width.toDouble()); + y = min(max(0.0, y), d.height.toDouble()); evt['x'] = '${x.round()}'; evt['y'] = '${y.round()}'; var buttons = ''; From d3eac8539d46167b885d6c0945179c1f79189878 Mon Sep 17 00:00:00 2001 From: csf Date: Tue, 13 Sep 2022 17:39:24 +0800 Subject: [PATCH 0464/2015] fix android no input permission --- flutter/lib/models/server_model.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index 33ce7e707..737d0b22b 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -133,8 +133,6 @@ class ServerModel with ChangeNotifier { _fileOk = fileOption.isEmpty; } - // input (mouse control) false by default - bind.mainSetOption(key: "enable-keyboard", value: "N"); notifyListeners(); } From a28fd5d772dc95adac2102216e3feffd1b7f7e6f Mon Sep 17 00:00:00 2001 From: csf Date: Tue, 13 Sep 2022 21:36:38 +0800 Subject: [PATCH 0465/2015] refactor: del unused or dead code and optimize reusable code --- flutter/lib/common.dart | 11 + flutter/lib/consts.dart | 230 +++++++++++ .../lib/desktop/pages/connection_page.dart | 210 +--------- .../lib/desktop/pages/file_manager_page.dart | 140 +------ .../lib/desktop/pages/port_forward_page.dart | 2 +- flutter/lib/desktop/pages/remote_page.dart | 373 +----------------- flutter/lib/desktop/pages/server_page.dart | 7 - .../lib/desktop/widgets/peercard_widget.dart | 16 +- flutter/lib/mobile/pages/connection_page.dart | 9 - flutter/lib/mobile/pages/remote_page.dart | 235 +---------- 10 files changed, 259 insertions(+), 974 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 9c3a5724f..aed6d6fd8 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -898,3 +898,14 @@ Future>? matchPeers(String searchText, List peers) async { } return filteredList; } + +/// Get the image for the current [platform]. +Widget getPlatformImage(String platform, {double size = 50}) { + platform = platform.toLowerCase(); + if (platform == 'mac os') { + platform = 'mac'; + } else if (platform != 'linux' && platform != 'android') { + platform = 'win'; + } + return Image.asset('assets/$platform.png', height: size, width: size); +} diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 70fc9f065..8986f05ec 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -16,3 +16,233 @@ const int kDesktopDefaultDisplayWidth = 1080; const int kDesktopDefaultDisplayHeight = 720; const kInvalidValueStr = "InvalidValueStr"; + +/// flutter/packages/flutter/lib/src/services/keyboard_key.dart -> _keyLabels +/// see [LogicalKeyboardKey.keyLabel] +const Map logicalKeyMap = { + 0x00000000020: 'VK_SPACE', + 0x00000000022: 'VK_QUOTE', + 0x0000000002c: 'VK_COMMA', + 0x0000000002d: 'VK_MINUS', + 0x0000000002f: 'VK_SLASH', + 0x00000000030: 'VK_0', + 0x00000000031: 'VK_1', + 0x00000000032: 'VK_2', + 0x00000000033: 'VK_3', + 0x00000000034: 'VK_4', + 0x00000000035: 'VK_5', + 0x00000000036: 'VK_6', + 0x00000000037: 'VK_7', + 0x00000000038: 'VK_8', + 0x00000000039: 'VK_9', + 0x0000000003b: 'VK_SEMICOLON', + 0x0000000003d: 'VK_PLUS', // it is = + 0x0000000005b: 'VK_LBRACKET', + 0x0000000005c: 'VK_BACKSLASH', + 0x0000000005d: 'VK_RBRACKET', + 0x00000000061: 'VK_A', + 0x00000000062: 'VK_B', + 0x00000000063: 'VK_C', + 0x00000000064: 'VK_D', + 0x00000000065: 'VK_E', + 0x00000000066: 'VK_F', + 0x00000000067: 'VK_G', + 0x00000000068: 'VK_H', + 0x00000000069: 'VK_I', + 0x0000000006a: 'VK_J', + 0x0000000006b: 'VK_K', + 0x0000000006c: 'VK_L', + 0x0000000006d: 'VK_M', + 0x0000000006e: 'VK_N', + 0x0000000006f: 'VK_O', + 0x00000000070: 'VK_P', + 0x00000000071: 'VK_Q', + 0x00000000072: 'VK_R', + 0x00000000073: 'VK_S', + 0x00000000074: 'VK_T', + 0x00000000075: 'VK_U', + 0x00000000076: 'VK_V', + 0x00000000077: 'VK_W', + 0x00000000078: 'VK_X', + 0x00000000079: 'VK_Y', + 0x0000000007a: 'VK_Z', + 0x00100000008: 'VK_BACK', + 0x00100000009: 'VK_TAB', + 0x0010000000d: 'VK_ENTER', + 0x0010000001b: 'VK_ESCAPE', + 0x0010000007f: 'VK_DELETE', + 0x00100000104: 'VK_CAPITAL', + 0x00100000301: 'VK_DOWN', + 0x00100000302: 'VK_LEFT', + 0x00100000303: 'VK_RIGHT', + 0x00100000304: 'VK_UP', + 0x00100000305: 'VK_END', + 0x00100000306: 'VK_HOME', + 0x00100000307: 'VK_NEXT', + 0x00100000308: 'VK_PRIOR', + 0x00100000401: 'VK_CLEAR', + 0x00100000407: 'VK_INSERT', + 0x00100000504: 'VK_CANCEL', + 0x00100000506: 'VK_EXECUTE', + 0x00100000508: 'VK_HELP', + 0x00100000509: 'VK_PAUSE', + 0x0010000050c: 'VK_SELECT', + 0x00100000608: 'VK_PRINT', + 0x00100000705: 'VK_CONVERT', + 0x00100000706: 'VK_FINAL', + 0x00100000711: 'VK_HANGUL', + 0x00100000712: 'VK_HANJA', + 0x00100000713: 'VK_JUNJA', + 0x00100000718: 'VK_KANA', + 0x00100000719: 'VK_KANJI', + 0x00100000801: 'VK_F1', + 0x00100000802: 'VK_F2', + 0x00100000803: 'VK_F3', + 0x00100000804: 'VK_F4', + 0x00100000805: 'VK_F5', + 0x00100000806: 'VK_F6', + 0x00100000807: 'VK_F7', + 0x00100000808: 'VK_F8', + 0x00100000809: 'VK_F9', + 0x0010000080a: 'VK_F10', + 0x0010000080b: 'VK_F11', + 0x0010000080c: 'VK_F12', + 0x00100000d2b: 'Apps', + 0x00200000002: 'VK_SLEEP', + 0x00200000100: 'VK_CONTROL', + 0x00200000101: 'RControl', + 0x00200000102: 'VK_SHIFT', + 0x00200000103: 'RShift', + 0x00200000104: 'VK_MENU', + 0x00200000105: 'RAlt', + 0x002000001f0: 'VK_CONTROL', + 0x002000001f2: 'VK_SHIFT', + 0x002000001f4: 'VK_MENU', + 0x002000001f6: 'Meta', + 0x0020000022a: 'VK_MULTIPLY', + 0x0020000022b: 'VK_ADD', + 0x0020000022d: 'VK_SUBTRACT', + 0x0020000022e: 'VK_DECIMAL', + 0x0020000022f: 'VK_DIVIDE', + 0x00200000230: 'VK_NUMPAD0', + 0x00200000231: 'VK_NUMPAD1', + 0x00200000232: 'VK_NUMPAD2', + 0x00200000233: 'VK_NUMPAD3', + 0x00200000234: 'VK_NUMPAD4', + 0x00200000235: 'VK_NUMPAD5', + 0x00200000236: 'VK_NUMPAD6', + 0x00200000237: 'VK_NUMPAD7', + 0x00200000238: 'VK_NUMPAD8', + 0x00200000239: 'VK_NUMPAD9', +}; + +/// flutter/packages/flutter/lib/src/services/keyboard_key.dart -> _debugName +/// see [PhysicalKeyboardKey.debugName] -> _debugName +const Map physicalKeyMap = { + 0x00010082: 'VK_SLEEP', + 0x00070004: 'VK_A', + 0x00070005: 'VK_B', + 0x00070006: 'VK_C', + 0x00070007: 'VK_D', + 0x00070008: 'VK_E', + 0x00070009: 'VK_F', + 0x0007000a: 'VK_G', + 0x0007000b: 'VK_H', + 0x0007000c: 'VK_I', + 0x0007000d: 'VK_J', + 0x0007000e: 'VK_K', + 0x0007000f: 'VK_L', + 0x00070010: 'VK_M', + 0x00070011: 'VK_N', + 0x00070012: 'VK_O', + 0x00070013: 'VK_P', + 0x00070014: 'VK_Q', + 0x00070015: 'VK_R', + 0x00070016: 'VK_S', + 0x00070017: 'VK_T', + 0x00070018: 'VK_U', + 0x00070019: 'VK_V', + 0x0007001a: 'VK_W', + 0x0007001b: 'VK_X', + 0x0007001c: 'VK_Y', + 0x0007001d: 'VK_Z', + 0x0007001e: 'VK_1', + 0x0007001f: 'VK_2', + 0x00070020: 'VK_3', + 0x00070021: 'VK_4', + 0x00070022: 'VK_5', + 0x00070023: 'VK_6', + 0x00070024: 'VK_7', + 0x00070025: 'VK_8', + 0x00070026: 'VK_9', + 0x00070027: 'VK_0', + 0x00070028: 'VK_ENTER', + 0x00070029: 'VK_ESCAPE', + 0x0007002a: 'VK_BACK', + 0x0007002b: 'VK_TAB', + 0x0007002c: 'VK_SPACE', + 0x0007002d: 'VK_MINUS', + 0x0007002e: 'VK_PLUS', // it is = + 0x0007002f: 'VK_LBRACKET', + 0x00070030: 'VK_RBRACKET', + 0x00070033: 'VK_SEMICOLON', + 0x00070034: 'VK_QUOTE', + 0x00070036: 'VK_COMMA', + 0x00070038: 'VK_SLASH', + 0x00070039: 'VK_CAPITAL', + 0x0007003a: 'VK_F1', + 0x0007003b: 'VK_F2', + 0x0007003c: 'VK_F3', + 0x0007003d: 'VK_F4', + 0x0007003e: 'VK_F5', + 0x0007003f: 'VK_F6', + 0x00070040: 'VK_F7', + 0x00070041: 'VK_F8', + 0x00070042: 'VK_F9', + 0x00070043: 'VK_F10', + 0x00070044: 'VK_F11', + 0x00070045: 'VK_F12', + 0x00070049: 'VK_INSERT', + 0x0007004a: 'VK_HOME', + 0x0007004b: 'VK_PRIOR', // Page Up + 0x0007004c: 'VK_DELETE', + 0x0007004d: 'VK_END', + 0x0007004e: 'VK_NEXT', // Page Down + 0x0007004f: 'VK_RIGHT', + 0x00070050: 'VK_LEFT', + 0x00070051: 'VK_DOWN', + 0x00070052: 'VK_UP', + 0x00070053: 'Num Lock', // TODO rust not impl + 0x00070054: 'VK_DIVIDE', // numpad + 0x00070055: 'VK_MULTIPLY', + 0x00070056: 'VK_SUBTRACT', + 0x00070057: 'VK_ADD', + 0x00070058: 'VK_ENTER', // num enter + 0x00070059: 'VK_NUMPAD0', + 0x0007005a: 'VK_NUMPAD1', + 0x0007005b: 'VK_NUMPAD2', + 0x0007005c: 'VK_NUMPAD3', + 0x0007005d: 'VK_NUMPAD4', + 0x0007005e: 'VK_NUMPAD5', + 0x0007005f: 'VK_NUMPAD6', + 0x00070060: 'VK_NUMPAD7', + 0x00070061: 'VK_NUMPAD8', + 0x00070062: 'VK_NUMPAD9', + 0x00070063: 'VK_DECIMAL', + 0x00070075: 'VK_HELP', + 0x00070077: 'VK_SELECT', + 0x00070088: 'VK_KANA', + 0x0007008a: 'VK_CONVERT', + 0x000700e0: 'VK_CONTROL', + 0x000700e1: 'VK_SHIFT', + 0x000700e2: 'VK_MENU', + 0x000700e3: 'Meta', + 0x000700e4: 'RControl', + 0x000700e5: 'RShift', + 0x000700e6: 'RAlt', + 0x000700e7: 'RWin', + 0x000c00b1: 'VK_PAUSE', + 0x000c00cd: 'VK_PAUSE', + 0x000c019e: 'LOCK_SCREEN', + 0x000c0208: 'VK_PRINT', +}; diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 64a74d22a..134fe4219 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -8,18 +8,12 @@ import 'package:flutter_hbb/desktop/widgets/peer_widget.dart'; import 'package:flutter_hbb/desktop/widgets/peercard_widget.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:get/get.dart'; -import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher_string.dart'; import '../../common.dart'; import '../../common/formatter/id_formatter.dart'; -import '../../mobile/pages/scan_page.dart'; -import '../../mobile/pages/settings_page.dart'; -import '../../models/model.dart'; import '../../models/platform_model.dart'; -// enum RemoteType { recently, favorite, discovered, addressBook } - /// Connection page for connecting to a remote peer. class ConnectionPage extends StatefulWidget { const ConnectionPage({Key? key}) : super(key: key); @@ -33,9 +27,6 @@ class _ConnectionPageState extends State { /// Controller for the id input bar. final _idController = IDTextEditingController(); - /// Update url. If it's not null, means an update is available. - final _updateUrl = ''; - Timer? _updateTimer; @override @@ -67,7 +58,6 @@ class _ConnectionPageState extends State { Expanded( child: Column( children: [ - getUpdateUI(), Row( children: [ getSearchBarUI(context), @@ -131,28 +121,6 @@ class _ConnectionPageState extends State { } } - /// UI for software update. - /// If [_updateUrl] is not empty, shows a button to update the software. - Widget getUpdateUI() { - return _updateUrl.isEmpty - ? SizedBox(height: 0) - : InkWell( - onTap: () async { - final url = _updateUrl + '.apk'; - if (await canLaunchUrlString(url)) { - await launchUrlString(url); - } - }, - child: Container( - alignment: AlignmentDirectional.center, - width: double.infinity, - color: Colors.pinkAccent, - padding: EdgeInsets.symmetric(vertical: 12), - child: Text(translate('Download new version'), - style: TextStyle( - color: Colors.white, fontWeight: FontWeight.bold)))); - } - /// UI for the search bar. /// Search for a peer and connect to it if the id exists. Widget getSearchBarUI(BuildContext context) { @@ -328,86 +296,6 @@ class _ConnectionPageState extends State { super.dispose(); } - /// Get the image for the current [platform]. - Widget getPlatformImage(String platform) { - platform = platform.toLowerCase(); - if (platform == 'mac os') - platform = 'mac'; - else if (platform != 'linux' && platform != 'android') platform = 'win'; - return Image.asset('assets/$platform.png', height: 50); - } - - bool hitTag(List selectedTags, List idents) { - if (selectedTags.isEmpty) { - return true; - } - if (idents.isEmpty) { - return false; - } - for (final tag in selectedTags) { - if (!idents.contains(tag)) { - return false; - } - } - return true; - } - - // /// Show the peer menu and handle user's choice. - // /// User might remove the peer or send a file to the peer. - // void showPeerMenu(BuildContext context, String id, RemoteType rType) async { - // var items = [ - // PopupMenuItem( - // child: Text(translate('Connect')), value: 'connect'), - // PopupMenuItem( - // child: Text(translate('Transfer File')), value: 'file'), - // PopupMenuItem( - // child: Text(translate('TCP Tunneling')), value: 'tcp-tunnel'), - // PopupMenuItem(child: Text(translate('Rename')), value: 'rename'), - // rType == RemoteType.addressBook - // ? PopupMenuItem( - // child: Text(translate('Remove')), value: 'ab-delete') - // : PopupMenuItem( - // child: Text(translate('Remove')), value: 'remove'), - // PopupMenuItem( - // child: Text(translate('Unremember Password')), - // value: 'unremember-password'), - // ]; - // if (rType == RemoteType.favorite) { - // items.add(PopupMenuItem( - // child: Text(translate('Remove from Favorites')), - // value: 'remove-fav')); - // } else if (rType != RemoteType.addressBook) { - // items.add(PopupMenuItem( - // child: Text(translate('Add to Favorites')), value: 'add-fav')); - // } else { - // items.add(PopupMenuItem( - // child: Text(translate('Edit Tag')), value: 'ab-edit-tag')); - // } - // var value = await showMenu( - // context: context, - // position: this._menuPos, - // items: items, - // elevation: 8, - // ); - // if (value == 'remove') { - // setState(() => gFFI.setByName('remove', '$id')); - // () async { - // removePreference(id); - // }(); - // } else if (value == 'file') { - // connect(id, isFileTransfer: true); - // } else if (value == 'add-fav') { - // } else if (value == 'connect') { - // connect(id, isFileTransfer: false); - // } else if (value == 'ab-delete') { - // gFFI.abModel.deletePeer(id); - // await gFFI.abModel.updateAb(); - // setState(() {}); - // } else if (value == 'ab-edit-tag') { - // abEditTag(id); - // } - // } - var svcStopped = false.obs; var svcStatusCode = 0.obs; var svcIsUsingPublicServer = true.obs; @@ -454,7 +342,7 @@ class _ConnectionPageState extends State { crossAxisAlignment: CrossAxisAlignment.center, children: [ light, - Text("${translate('Ready')}"), + Text(translate('Ready')), svcIsUsingPublicServer.value ? InkWell( onTap: onUsePublicServerGuide, @@ -469,7 +357,7 @@ class _ConnectionPageState extends State { } void onUsePublicServerGuide() { - final url = "https://rustdesk.com/blog/id-relay-set/"; + const url = "https://rustdesk.com/blog/id-relay-set/"; canLaunchUrlString(url).then((can) { if (can) { launchUrlString(url); @@ -857,100 +745,6 @@ class _ConnectionPageState extends State { } } -class WebMenu extends StatefulWidget { - @override - _WebMenuState createState() => _WebMenuState(); -} - -class _WebMenuState extends State { - String? username; - String url = ""; - - @override - void initState() { - super.initState(); - () async { - final usernameRes = await getUsername(); - final urlRes = await getUrl(); - var update = false; - if (usernameRes != username) { - username = usernameRes; - update = true; - } - if (urlRes != url) { - url = urlRes; - update = true; - } - - if (update) { - setState(() {}); - } - }(); - } - - @override - Widget build(BuildContext context) { - Provider.of(context); - return PopupMenuButton( - icon: Icon(Icons.more_vert), - itemBuilder: (context) { - return (isIOS - ? [ - PopupMenuItem( - child: Icon(Icons.qr_code_scanner, color: Colors.black), - value: "scan", - ) - ] - : >[]) + - [ - PopupMenuItem( - child: Text(translate('ID/Relay Server')), - value: "server", - ) - ] + - (url.contains('admin.rustdesk.com') - ? >[] - : [ - PopupMenuItem( - child: Text(username == null - ? translate("Login") - : translate("Logout") + ' ($username)'), - value: "login", - ) - ]) + - [ - PopupMenuItem( - child: Text(translate('About') + ' RustDesk'), - value: "about", - ) - ]; - }, - onSelected: (value) { - if (value == 'server') { - showServerSettings(gFFI.dialogManager); - } - if (value == 'about') { - showAbout(gFFI.dialogManager); - } - if (value == 'login') { - if (username == null) { - showLogin(gFFI.dialogManager); - } else { - logout(gFFI.dialogManager); - } - } - if (value == 'scan') { - Navigator.push( - context, - MaterialPageRoute( - builder: (BuildContext context) => ScanPage(), - ), - ); - } - }); - } -} - class _PeerTabbedPage extends StatefulWidget { final List tabs; final List children; diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index eee3c226b..ced61e8d9 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -67,7 +67,7 @@ class _FileManagerPageState extends State if (!Platform.isLinux) { Wakelock.enable(); } - print("init success with id ${widget.id}"); + debugPrint("File manager page init success with id ${widget.id}"); // register location listener _locationNodeLocal.addListener(onLocalLocationFocusChanged); _locationNodeRemote.addListener(onRemoteLocationFocusChanged); @@ -307,109 +307,6 @@ class _FileManagerPageState extends State ) ], )), - // Center(child: listTail(isLocal: isLocal)), - // Expanded( - // child: ListView.builder( - // controller: ScrollController(), - // itemCount: entries.length + 1, - // itemBuilder: (context, index) { - // if (index >= entries.length) { - // return listTail(isLocal: isLocal); - // } - // var selected = false; - // if (model.selectMode) { - // selected = _selectedItems.contains(entries[index]); - // } - // - // final sizeStr = entries[index].isFile - // ? readableFileSize(entries[index].size.toDouble()) - // : ""; - // return Card( - // child: ListTile( - // leading: Icon( - // entries[index].isFile ? Icons.feed_outlined : Icons.folder, - // size: 40), - // title: Text(entries[index].name), - // selected: selected, - // subtitle: Text( - // entries[index] - // .lastModified() - // .toString() - // .replaceAll(".000", "") + - // " " + - // sizeStr, - // style: TextStyle(fontSize: 12, color: MyTheme.darkGray), - // ), - // trailing: needShowCheckBox() - // ? Checkbox( - // value: selected, - // onChanged: (v) { - // if (v == null) return; - // if (v && !selected) { - // _selectedItems.add(isLocal, entries[index]); - // } else if (!v && selected) { - // _selectedItems.remove(entries[index]); - // } - // setState(() {}); - // }) - // : PopupMenuButton( - // icon: Icon(Icons.more_vert), - // itemBuilder: (context) { - // return [ - // PopupMenuItem( - // child: Text(translate("Delete")), - // value: "delete", - // ), - // PopupMenuItem( - // child: Text(translate("Multi Select")), - // value: "multi_select", - // ), - // PopupMenuItem( - // child: Text(translate("Properties")), - // value: "properties", - // enabled: false, - // ) - // ]; - // }, - // onSelected: (v) { - // if (v == "delete") { - // final items = SelectedItems(); - // items.add(isLocal, entries[index]); - // model.removeAction(items); - // } else if (v == "multi_select") { - // _selectedItems.clear(); - // model.toggleSelectMode(); - // } - // }), - // onTap: () { - // if (model.selectMode && !_selectedItems.isOtherPage(isLocal)) { - // if (selected) { - // _selectedItems.remove(entries[index]); - // } else { - // _selectedItems.add(isLocal, entries[index]); - // } - // setState(() {}); - // return; - // } - // if (entries[index].isDirectory) { - // openDirectory(entries[index].path, isLocal: isLocal); - // breadCrumbScrollToEnd(isLocal); - // } else { - // // Perform file-related tasks. - // } - // }, - // onLongPress: () { - // _selectedItems.clear(); - // model.toggleSelectMode(); - // if (model.selectMode) { - // _selectedItems.add(isLocal, entries[index]); - // } - // setState(() {}); - // }, - // ), - // ); - // }, - // )) ]), ), ); @@ -736,43 +633,9 @@ class _FileManagerPageState extends State )); } - Widget listTail({bool isLocal = false}) { - final dir = isLocal ? model.currentLocalDir : model.currentRemoteDir; - return Container( - height: 100, - child: Column( - children: [ - Padding( - padding: EdgeInsets.fromLTRB(30, 5, 30, 0), - child: Text( - dir.path, - style: TextStyle(color: MyTheme.darkGray), - ), - ), - Padding( - padding: EdgeInsets.all(2), - child: Text( - "${translate("Total")}: ${dir.entries.length} ${translate("items")}", - style: TextStyle(color: MyTheme.darkGray), - ), - ) - ], - ), - ); - } - @override bool get wantKeepAlive => true; - /// Get the image for the current [platform]. - Widget getPlatformImage(String platform) { - platform = platform.toLowerCase(); - if (platform == 'mac os') - platform = 'mac'; - else if (platform != 'linux' && platform != 'android') platform = 'win'; - return Image.asset('assets/$platform.png', width: 25, height: 25); - } - void onLocalLocationFocusChanged() { debugPrint("focus changed on local"); if (_locationNodeLocal.hasFocus) { @@ -861,7 +724,6 @@ class _FileManagerPageState extends State openDirectory(String path, {bool isLocal = false}) { model.openDirectory(path, isLocal: isLocal).then((_) { - print("scroll"); breadCrumbScrollToEnd(isLocal); }); } diff --git a/flutter/lib/desktop/pages/port_forward_page.dart b/flutter/lib/desktop/pages/port_forward_page.dart index f3e988744..49946cc56 100644 --- a/flutter/lib/desktop/pages/port_forward_page.dart +++ b/flutter/lib/desktop/pages/port_forward_page.dart @@ -52,7 +52,7 @@ class _PortForwardPageState extends State if (!Platform.isLinux) { Wakelock.enable(); } - debugPrint("init success with id ${widget.id}"); + debugPrint("Port forward page init success with id ${widget.id}"); } @override diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 35ab548c3..43d48740e 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -10,8 +10,7 @@ import 'package:provider/provider.dart'; import 'package:wakelock/wakelock.dart'; import 'package:flutter_custom_cursor/flutter_custom_cursor.dart'; -// import 'package:window_manager/window_manager.dart'; - +import '../../consts.dart'; import '../widgets/remote_menubar.dart'; import '../../common.dart'; import '../../mobile/widgets/dialog.dart'; @@ -19,8 +18,6 @@ import '../../models/model.dart'; import '../../models/platform_model.dart'; import '../../common/shared_state.dart'; -final initText = '\1' * 1024; - class RemotePage extends StatefulWidget { const RemotePage({ Key? key, @@ -38,15 +35,13 @@ class RemotePage extends StatefulWidget { class _RemotePageState extends State with AutomaticKeepAliveClientMixin { Timer? _timer; - String _value = ''; String keyboardMode = "legacy"; final _cursorOverImage = false.obs; late RxBool _showRemoteCursor; late RxBool _remoteCursorMoved; late RxBool _keyboardEnabled; - final FocusNode _mobileFocusNode = FocusNode(); - final FocusNode _physicalFocusNode = FocusNode(); + final FocusNode _rawKeyFocusNode = FocusNode(); var _isPhysicalMouse = false; var _imageFocused = false; @@ -95,11 +90,9 @@ class _RemotePageState extends State if (!Platform.isLinux) { Wakelock.enable(); } - _physicalFocusNode.requestFocus(); + _rawKeyFocusNode.requestFocus(); _ffi.ffiModel.updateEventListener(widget.id); - _ffi.listenToMouse(true); _ffi.qualityMonitorModel.checkShowQualityMonitor(widget.id); - // WindowManager.instance.addListener(this); _showRemoteCursor.value = bind.sessionGetToggleOptionSync( id: widget.id, arg: 'show-remote-cursor'); } @@ -108,9 +101,7 @@ class _RemotePageState extends State void dispose() { debugPrint("REMOTE PAGE dispose ${widget.id}"); _ffi.dialogManager.hideMobileActionsOverlay(); - _ffi.listenToMouse(false); - _mobileFocusNode.dispose(); - _physicalFocusNode.dispose(); + _rawKeyFocusNode.dispose(); _ffi.close(); _timer?.cancel(); _ffi.dialogManager.dismissAll(); @@ -119,7 +110,6 @@ class _RemotePageState extends State if (!Platform.isLinux) { Wakelock.disable(); } - // WindowManager.instance.removeListener(this); Get.delete(tag: widget.id); super.dispose(); _removeStates(widget.id); @@ -129,87 +119,10 @@ class _RemotePageState extends State _ffi.resetModifiers(); } - // handle mobile virtual keyboard - void handleInput(String newValue) { - var oldValue = _value; - _value = newValue; - if (isIOS) { - var i = newValue.length - 1; - for (; i >= 0 && newValue[i] != '\1'; --i) {} - var j = oldValue.length - 1; - for (; j >= 0 && oldValue[j] != '\1'; --j) {} - if (i < j) j = i; - newValue = newValue.substring(j + 1); - oldValue = oldValue.substring(j + 1); - var common = 0; - for (; - common < oldValue.length && - common < newValue.length && - newValue[common] == oldValue[common]; - ++common) {} - for (i = 0; i < oldValue.length - common; ++i) { - _ffi.inputKey('VK_BACK'); - } - if (newValue.length > common) { - var s = newValue.substring(common); - if (s.length > 1) { - bind.sessionInputString(id: widget.id, value: s); - } else { - inputChar(s); - } - } - return; - } - if (oldValue.isNotEmpty && - newValue.isNotEmpty && - oldValue[0] == '\1' && - newValue[0] != '\1') { - // clipboard - oldValue = ''; - } - if (newValue.length == oldValue.length) { - // ? - } else if (newValue.length < oldValue.length) { - const char = 'VK_BACK'; - _ffi.inputKey(char); - } else { - final content = newValue.substring(oldValue.length); - if (content.length > 1) { - if (oldValue != '' && - content.length == 2 && - (content == '""' || - content == '()' || - content == '[]' || - content == '<>' || - content == "{}" || - content == '”“' || - content == '《》' || - content == '()' || - content == '【】')) { - // can not only input content[0], because when input ], [ are also auo insert, which cause ] never be input - bind.sessionInputString(id: widget.id, value: content); - return; - } - bind.sessionInputString(id: widget.id, value: content); - } else { - inputChar(content); - } - } - } - - void inputChar(String char) { - if (char == '\n') { - char = 'VK_RETURN'; - } else if (char == ' ') { - char = 'VK_SPACE'; - } - _ffi.inputKey(char); - } - void sendRawKey(RawKeyEvent e, {bool? down, bool? press}) { // for maximum compatibility - final label = _logicalKeyMap[e.logicalKey.keyId] ?? - _physicalKeyMap[e.physicalKey.usbHidUsage] ?? + final label = logicalKeyMap[e.logicalKey.keyId] ?? + physicalKeyMap[e.physicalKey.usbHidUsage] ?? e.logicalKey.keyLabel; _ffi.inputKey(label, down: down, press: press ?? false); } @@ -339,7 +252,7 @@ class _RemotePageState extends State child: Focus( autofocus: true, canRequestFocus: true, - focusNode: _physicalFocusNode, + focusNode: _rawKeyFocusNode, onFocusChange: (bool v) { _imageFocused = v; }, @@ -347,15 +260,6 @@ class _RemotePageState extends State child: child)); } - /// touchMode only: - /// LongPress -> right click - /// OneFingerPan -> start/end -> left down start/end - /// onDoubleTapDown -> move to - /// onLongPressDown => move to - /// - /// mouseMode only: - /// DoubleFiner -> right click - /// HoldDrag -> left drag void _onPointHoverImage(PointerHoverEvent e) { if (e.kind != ui.PointerDeviceKind.mouse) return; if (!_isPhysicalMouse) { @@ -420,7 +324,7 @@ class _RemotePageState extends State void enterView(PointerEnterEvent evt) { if (!_imageFocused) { - _physicalFocusNode.requestFocus(); + _rawKeyFocusNode.requestFocus(); } _cursorOverImage.value = true; for (var f in _onEnterOrLeaveImage) { @@ -505,21 +409,6 @@ class _RemotePageState extends State return out; } - @override - void onWindowEvent(String eventName) { - print("window event: $eventName"); - switch (eventName) { - case 'resize': - _ffi.canvasModel.updateViewStyle(); - break; - case 'maximize': - Future.delayed(const Duration(milliseconds: 100), () { - _ffi.canvasModel.updateViewStyle(); - }); - break; - } - } - @override bool get wantKeepAlive => true; } @@ -747,249 +636,3 @@ class QualityMonitor extends StatelessWidget { ) : const SizedBox.shrink()))); } - -void sendPrompt(String id, bool isMac, String key) { - FFI _ffi = ffi(id); - final old = isMac ? _ffi.command : _ffi.ctrl; - if (isMac) { - _ffi.command = true; - } else { - _ffi.ctrl = true; - } - _ffi.inputKey(key); - if (isMac) { - _ffi.command = old; - } else { - _ffi.ctrl = old; - } -} - -/// flutter/packages/flutter/lib/src/services/keyboard_key.dart -> _keyLabels -/// see [LogicalKeyboardKey.keyLabel] -const Map _logicalKeyMap = { - 0x00000000020: 'VK_SPACE', - 0x00000000022: 'VK_QUOTE', - 0x0000000002c: 'VK_COMMA', - 0x0000000002d: 'VK_MINUS', - 0x0000000002f: 'VK_SLASH', - 0x00000000030: 'VK_0', - 0x00000000031: 'VK_1', - 0x00000000032: 'VK_2', - 0x00000000033: 'VK_3', - 0x00000000034: 'VK_4', - 0x00000000035: 'VK_5', - 0x00000000036: 'VK_6', - 0x00000000037: 'VK_7', - 0x00000000038: 'VK_8', - 0x00000000039: 'VK_9', - 0x0000000003b: 'VK_SEMICOLON', - 0x0000000003d: 'VK_PLUS', // it is = - 0x0000000005b: 'VK_LBRACKET', - 0x0000000005c: 'VK_BACKSLASH', - 0x0000000005d: 'VK_RBRACKET', - 0x00000000061: 'VK_A', - 0x00000000062: 'VK_B', - 0x00000000063: 'VK_C', - 0x00000000064: 'VK_D', - 0x00000000065: 'VK_E', - 0x00000000066: 'VK_F', - 0x00000000067: 'VK_G', - 0x00000000068: 'VK_H', - 0x00000000069: 'VK_I', - 0x0000000006a: 'VK_J', - 0x0000000006b: 'VK_K', - 0x0000000006c: 'VK_L', - 0x0000000006d: 'VK_M', - 0x0000000006e: 'VK_N', - 0x0000000006f: 'VK_O', - 0x00000000070: 'VK_P', - 0x00000000071: 'VK_Q', - 0x00000000072: 'VK_R', - 0x00000000073: 'VK_S', - 0x00000000074: 'VK_T', - 0x00000000075: 'VK_U', - 0x00000000076: 'VK_V', - 0x00000000077: 'VK_W', - 0x00000000078: 'VK_X', - 0x00000000079: 'VK_Y', - 0x0000000007a: 'VK_Z', - 0x00100000008: 'VK_BACK', - 0x00100000009: 'VK_TAB', - 0x0010000000d: 'VK_ENTER', - 0x0010000001b: 'VK_ESCAPE', - 0x0010000007f: 'VK_DELETE', - 0x00100000104: 'VK_CAPITAL', - 0x00100000301: 'VK_DOWN', - 0x00100000302: 'VK_LEFT', - 0x00100000303: 'VK_RIGHT', - 0x00100000304: 'VK_UP', - 0x00100000305: 'VK_END', - 0x00100000306: 'VK_HOME', - 0x00100000307: 'VK_NEXT', - 0x00100000308: 'VK_PRIOR', - 0x00100000401: 'VK_CLEAR', - 0x00100000407: 'VK_INSERT', - 0x00100000504: 'VK_CANCEL', - 0x00100000506: 'VK_EXECUTE', - 0x00100000508: 'VK_HELP', - 0x00100000509: 'VK_PAUSE', - 0x0010000050c: 'VK_SELECT', - 0x00100000608: 'VK_PRINT', - 0x00100000705: 'VK_CONVERT', - 0x00100000706: 'VK_FINAL', - 0x00100000711: 'VK_HANGUL', - 0x00100000712: 'VK_HANJA', - 0x00100000713: 'VK_JUNJA', - 0x00100000718: 'VK_KANA', - 0x00100000719: 'VK_KANJI', - 0x00100000801: 'VK_F1', - 0x00100000802: 'VK_F2', - 0x00100000803: 'VK_F3', - 0x00100000804: 'VK_F4', - 0x00100000805: 'VK_F5', - 0x00100000806: 'VK_F6', - 0x00100000807: 'VK_F7', - 0x00100000808: 'VK_F8', - 0x00100000809: 'VK_F9', - 0x0010000080a: 'VK_F10', - 0x0010000080b: 'VK_F11', - 0x0010000080c: 'VK_F12', - 0x00100000d2b: 'Apps', - 0x00200000002: 'VK_SLEEP', - 0x00200000100: 'VK_CONTROL', - 0x00200000101: 'RControl', - 0x00200000102: 'VK_SHIFT', - 0x00200000103: 'RShift', - 0x00200000104: 'VK_MENU', - 0x00200000105: 'RAlt', - 0x002000001f0: 'VK_CONTROL', - 0x002000001f2: 'VK_SHIFT', - 0x002000001f4: 'VK_MENU', - 0x002000001f6: 'Meta', - 0x0020000022a: 'VK_MULTIPLY', - 0x0020000022b: 'VK_ADD', - 0x0020000022d: 'VK_SUBTRACT', - 0x0020000022e: 'VK_DECIMAL', - 0x0020000022f: 'VK_DIVIDE', - 0x00200000230: 'VK_NUMPAD0', - 0x00200000231: 'VK_NUMPAD1', - 0x00200000232: 'VK_NUMPAD2', - 0x00200000233: 'VK_NUMPAD3', - 0x00200000234: 'VK_NUMPAD4', - 0x00200000235: 'VK_NUMPAD5', - 0x00200000236: 'VK_NUMPAD6', - 0x00200000237: 'VK_NUMPAD7', - 0x00200000238: 'VK_NUMPAD8', - 0x00200000239: 'VK_NUMPAD9', -}; - -/// flutter/packages/flutter/lib/src/services/keyboard_key.dart -> _debugName -/// see [PhysicalKeyboardKey.debugName] -> _debugName -const Map _physicalKeyMap = { - 0x00010082: 'VK_SLEEP', - 0x00070004: 'VK_A', - 0x00070005: 'VK_B', - 0x00070006: 'VK_C', - 0x00070007: 'VK_D', - 0x00070008: 'VK_E', - 0x00070009: 'VK_F', - 0x0007000a: 'VK_G', - 0x0007000b: 'VK_H', - 0x0007000c: 'VK_I', - 0x0007000d: 'VK_J', - 0x0007000e: 'VK_K', - 0x0007000f: 'VK_L', - 0x00070010: 'VK_M', - 0x00070011: 'VK_N', - 0x00070012: 'VK_O', - 0x00070013: 'VK_P', - 0x00070014: 'VK_Q', - 0x00070015: 'VK_R', - 0x00070016: 'VK_S', - 0x00070017: 'VK_T', - 0x00070018: 'VK_U', - 0x00070019: 'VK_V', - 0x0007001a: 'VK_W', - 0x0007001b: 'VK_X', - 0x0007001c: 'VK_Y', - 0x0007001d: 'VK_Z', - 0x0007001e: 'VK_1', - 0x0007001f: 'VK_2', - 0x00070020: 'VK_3', - 0x00070021: 'VK_4', - 0x00070022: 'VK_5', - 0x00070023: 'VK_6', - 0x00070024: 'VK_7', - 0x00070025: 'VK_8', - 0x00070026: 'VK_9', - 0x00070027: 'VK_0', - 0x00070028: 'VK_ENTER', - 0x00070029: 'VK_ESCAPE', - 0x0007002a: 'VK_BACK', - 0x0007002b: 'VK_TAB', - 0x0007002c: 'VK_SPACE', - 0x0007002d: 'VK_MINUS', - 0x0007002e: 'VK_PLUS', // it is = - 0x0007002f: 'VK_LBRACKET', - 0x00070030: 'VK_RBRACKET', - 0x00070033: 'VK_SEMICOLON', - 0x00070034: 'VK_QUOTE', - 0x00070036: 'VK_COMMA', - 0x00070038: 'VK_SLASH', - 0x00070039: 'VK_CAPITAL', - 0x0007003a: 'VK_F1', - 0x0007003b: 'VK_F2', - 0x0007003c: 'VK_F3', - 0x0007003d: 'VK_F4', - 0x0007003e: 'VK_F5', - 0x0007003f: 'VK_F6', - 0x00070040: 'VK_F7', - 0x00070041: 'VK_F8', - 0x00070042: 'VK_F9', - 0x00070043: 'VK_F10', - 0x00070044: 'VK_F11', - 0x00070045: 'VK_F12', - 0x00070049: 'VK_INSERT', - 0x0007004a: 'VK_HOME', - 0x0007004b: 'VK_PRIOR', // Page Up - 0x0007004c: 'VK_DELETE', - 0x0007004d: 'VK_END', - 0x0007004e: 'VK_NEXT', // Page Down - 0x0007004f: 'VK_RIGHT', - 0x00070050: 'VK_LEFT', - 0x00070051: 'VK_DOWN', - 0x00070052: 'VK_UP', - 0x00070053: 'Num Lock', // TODO rust not impl - 0x00070054: 'VK_DIVIDE', // numpad - 0x00070055: 'VK_MULTIPLY', - 0x00070056: 'VK_SUBTRACT', - 0x00070057: 'VK_ADD', - 0x00070058: 'VK_ENTER', // num enter - 0x00070059: 'VK_NUMPAD0', - 0x0007005a: 'VK_NUMPAD1', - 0x0007005b: 'VK_NUMPAD2', - 0x0007005c: 'VK_NUMPAD3', - 0x0007005d: 'VK_NUMPAD4', - 0x0007005e: 'VK_NUMPAD5', - 0x0007005f: 'VK_NUMPAD6', - 0x00070060: 'VK_NUMPAD7', - 0x00070061: 'VK_NUMPAD8', - 0x00070062: 'VK_NUMPAD9', - 0x00070063: 'VK_DECIMAL', - 0x00070075: 'VK_HELP', - 0x00070077: 'VK_SELECT', - 0x00070088: 'VK_KANA', - 0x0007008a: 'VK_CONVERT', - 0x000700e0: 'VK_CONTROL', - 0x000700e1: 'VK_SHIFT', - 0x000700e2: 'VK_MENU', - 0x000700e3: 'Meta', - 0x000700e4: 'RControl', - 0x000700e5: 'RShift', - 0x000700e6: 'RAlt', - 0x000700e7: 'RWin', - 0x000c00b1: 'VK_PAUSE', - 0x000c00cd: 'VK_PAUSE', - 0x000c019e: 'LOCK_SCREEN', - 0x000c0208: 'VK_PRINT', -}; diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index b49bfdc27..62e60ba26 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -98,13 +98,6 @@ class ConnectionManagerState extends State { gFFI.serverModel.updateClientState(); gFFI.serverModel.tabController.onSelected = (index) => gFFI.chatModel.changeCurrentID(gFFI.serverModel.clients[index].id); - // test - // gFFI.serverModel.clients.forEach((client) { - // DesktopTabBar.onAdd( - // gFFI.serverModel.tabs, - // TabInfo( - // key: client.id.toString(), label: client.name, closable: false)); - // }); super.initState(); } diff --git a/flutter/lib/desktop/widgets/peercard_widget.dart b/flutter/lib/desktop/widgets/peercard_widget.dart index a809adf9c..d9a87cd7b 100644 --- a/flutter/lib/desktop/widgets/peercard_widget.dart +++ b/flutter/lib/desktop/widgets/peercard_widget.dart @@ -99,7 +99,7 @@ class _PeerCardState extends State<_PeerCard> color: str2color('${peer.id}${peer.platform}', 0x7f), ), alignment: Alignment.center, - child: _getPlatformImage('${peer.platform}', 30).paddingAll(6), + child: getPlatformImage(peer.platform, size: 30).paddingAll(6), ), Expanded( child: Container( @@ -196,7 +196,8 @@ class _PeerCardState extends State<_PeerCard> children: [ Container( padding: const EdgeInsets.all(6), - child: _getPlatformImage(peer.platform, 60), + child: + getPlatformImage(peer.platform, size: 60), ), Row( children: [ @@ -287,17 +288,6 @@ class _PeerCardState extends State<_PeerCard> ); } - /// Get the image for the current [platform]. - Widget _getPlatformImage(String platform, double size) { - platform = platform.toLowerCase(); - if (platform == 'mac os') { - platform = 'mac'; - } else if (platform != 'linux' && platform != 'android') { - platform = 'win'; - } - return Image.asset('assets/$platform.png', height: size, width: size); - } - @override bool get wantKeepAlive => true; } diff --git a/flutter/lib/mobile/pages/connection_page.dart b/flutter/lib/mobile/pages/connection_page.dart index 7549bbae7..e6eddd087 100644 --- a/flutter/lib/mobile/pages/connection_page.dart +++ b/flutter/lib/mobile/pages/connection_page.dart @@ -215,15 +215,6 @@ class _ConnectionPageState extends State { super.dispose(); } - /// Get the image for the current [platform]. - Widget getPlatformImage(String platform) { - platform = platform.toLowerCase(); - if (platform == 'mac os') - platform = 'mac'; - else if (platform != 'linux' && platform != 'android') platform = 'win'; - return Image.asset('assets/$platform.png', width: 24, height: 24); - } - /// Get all the saved peers. Widget getPeers() { final windowWidth = MediaQuery.of(context).size.width; diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index a9a03c04d..d9943a62d 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -10,6 +10,7 @@ import 'package:provider/provider.dart'; import 'package:wakelock/wakelock.dart'; import '../../common.dart'; +import '../../consts.dart'; import '../../models/model.dart'; import '../../models/platform_model.dart'; import '../widgets/dialog.dart'; @@ -211,8 +212,8 @@ class _RemotePageState extends State { void sendRawKey(RawKeyEvent e, {bool? down, bool? press}) { // for maximum compatibility - final label = _logicalKeyMap[e.logicalKey.keyId] ?? - _physicalKeyMap[e.physicalKey.usbHidUsage] ?? + final label = logicalKeyMap[e.logicalKey.keyId] ?? + physicalKeyMap[e.physicalKey.usbHidUsage] ?? e.logicalKey.keyLabel; gFFI.inputKey(label, down: down, press: press ?? false); } @@ -1170,233 +1171,3 @@ void sendPrompt(bool isMac, String key) { gFFI.ctrl = old; } } - -/// flutter/packages/flutter/lib/src/services/keyboard_key.dart -> _keyLabels -/// see [LogicalKeyboardKey.keyLabel] -const Map _logicalKeyMap = { - 0x00000000020: 'VK_SPACE', - 0x00000000022: 'VK_QUOTE', - 0x0000000002c: 'VK_COMMA', - 0x0000000002d: 'VK_MINUS', - 0x0000000002f: 'VK_SLASH', - 0x00000000030: 'VK_0', - 0x00000000031: 'VK_1', - 0x00000000032: 'VK_2', - 0x00000000033: 'VK_3', - 0x00000000034: 'VK_4', - 0x00000000035: 'VK_5', - 0x00000000036: 'VK_6', - 0x00000000037: 'VK_7', - 0x00000000038: 'VK_8', - 0x00000000039: 'VK_9', - 0x0000000003b: 'VK_SEMICOLON', - 0x0000000003d: 'VK_PLUS', // it is = - 0x0000000005b: 'VK_LBRACKET', - 0x0000000005c: 'VK_BACKSLASH', - 0x0000000005d: 'VK_RBRACKET', - 0x00000000061: 'VK_A', - 0x00000000062: 'VK_B', - 0x00000000063: 'VK_C', - 0x00000000064: 'VK_D', - 0x00000000065: 'VK_E', - 0x00000000066: 'VK_F', - 0x00000000067: 'VK_G', - 0x00000000068: 'VK_H', - 0x00000000069: 'VK_I', - 0x0000000006a: 'VK_J', - 0x0000000006b: 'VK_K', - 0x0000000006c: 'VK_L', - 0x0000000006d: 'VK_M', - 0x0000000006e: 'VK_N', - 0x0000000006f: 'VK_O', - 0x00000000070: 'VK_P', - 0x00000000071: 'VK_Q', - 0x00000000072: 'VK_R', - 0x00000000073: 'VK_S', - 0x00000000074: 'VK_T', - 0x00000000075: 'VK_U', - 0x00000000076: 'VK_V', - 0x00000000077: 'VK_W', - 0x00000000078: 'VK_X', - 0x00000000079: 'VK_Y', - 0x0000000007a: 'VK_Z', - 0x00100000008: 'VK_BACK', - 0x00100000009: 'VK_TAB', - 0x0010000000d: 'VK_ENTER', - 0x0010000001b: 'VK_ESCAPE', - 0x0010000007f: 'VK_DELETE', - 0x00100000104: 'VK_CAPITAL', - 0x00100000301: 'VK_DOWN', - 0x00100000302: 'VK_LEFT', - 0x00100000303: 'VK_RIGHT', - 0x00100000304: 'VK_UP', - 0x00100000305: 'VK_END', - 0x00100000306: 'VK_HOME', - 0x00100000307: 'VK_NEXT', - 0x00100000308: 'VK_PRIOR', - 0x00100000401: 'VK_CLEAR', - 0x00100000407: 'VK_INSERT', - 0x00100000504: 'VK_CANCEL', - 0x00100000506: 'VK_EXECUTE', - 0x00100000508: 'VK_HELP', - 0x00100000509: 'VK_PAUSE', - 0x0010000050c: 'VK_SELECT', - 0x00100000608: 'VK_PRINT', - 0x00100000705: 'VK_CONVERT', - 0x00100000706: 'VK_FINAL', - 0x00100000711: 'VK_HANGUL', - 0x00100000712: 'VK_HANJA', - 0x00100000713: 'VK_JUNJA', - 0x00100000718: 'VK_KANA', - 0x00100000719: 'VK_KANJI', - 0x00100000801: 'VK_F1', - 0x00100000802: 'VK_F2', - 0x00100000803: 'VK_F3', - 0x00100000804: 'VK_F4', - 0x00100000805: 'VK_F5', - 0x00100000806: 'VK_F6', - 0x00100000807: 'VK_F7', - 0x00100000808: 'VK_F8', - 0x00100000809: 'VK_F9', - 0x0010000080a: 'VK_F10', - 0x0010000080b: 'VK_F11', - 0x0010000080c: 'VK_F12', - 0x00100000d2b: 'Apps', - 0x00200000002: 'VK_SLEEP', - 0x00200000100: 'VK_CONTROL', - 0x00200000101: 'RControl', - 0x00200000102: 'VK_SHIFT', - 0x00200000103: 'RShift', - 0x00200000104: 'VK_MENU', - 0x00200000105: 'RAlt', - 0x002000001f0: 'VK_CONTROL', - 0x002000001f2: 'VK_SHIFT', - 0x002000001f4: 'VK_MENU', - 0x002000001f6: 'Meta', - 0x0020000022a: 'VK_MULTIPLY', - 0x0020000022b: 'VK_ADD', - 0x0020000022d: 'VK_SUBTRACT', - 0x0020000022e: 'VK_DECIMAL', - 0x0020000022f: 'VK_DIVIDE', - 0x00200000230: 'VK_NUMPAD0', - 0x00200000231: 'VK_NUMPAD1', - 0x00200000232: 'VK_NUMPAD2', - 0x00200000233: 'VK_NUMPAD3', - 0x00200000234: 'VK_NUMPAD4', - 0x00200000235: 'VK_NUMPAD5', - 0x00200000236: 'VK_NUMPAD6', - 0x00200000237: 'VK_NUMPAD7', - 0x00200000238: 'VK_NUMPAD8', - 0x00200000239: 'VK_NUMPAD9', -}; - -/// flutter/packages/flutter/lib/src/services/keyboard_key.dart -> _debugName -/// see [PhysicalKeyboardKey.debugName] -> _debugName -const Map _physicalKeyMap = { - 0x00010082: 'VK_SLEEP', - 0x00070004: 'VK_A', - 0x00070005: 'VK_B', - 0x00070006: 'VK_C', - 0x00070007: 'VK_D', - 0x00070008: 'VK_E', - 0x00070009: 'VK_F', - 0x0007000a: 'VK_G', - 0x0007000b: 'VK_H', - 0x0007000c: 'VK_I', - 0x0007000d: 'VK_J', - 0x0007000e: 'VK_K', - 0x0007000f: 'VK_L', - 0x00070010: 'VK_M', - 0x00070011: 'VK_N', - 0x00070012: 'VK_O', - 0x00070013: 'VK_P', - 0x00070014: 'VK_Q', - 0x00070015: 'VK_R', - 0x00070016: 'VK_S', - 0x00070017: 'VK_T', - 0x00070018: 'VK_U', - 0x00070019: 'VK_V', - 0x0007001a: 'VK_W', - 0x0007001b: 'VK_X', - 0x0007001c: 'VK_Y', - 0x0007001d: 'VK_Z', - 0x0007001e: 'VK_1', - 0x0007001f: 'VK_2', - 0x00070020: 'VK_3', - 0x00070021: 'VK_4', - 0x00070022: 'VK_5', - 0x00070023: 'VK_6', - 0x00070024: 'VK_7', - 0x00070025: 'VK_8', - 0x00070026: 'VK_9', - 0x00070027: 'VK_0', - 0x00070028: 'VK_ENTER', - 0x00070029: 'VK_ESCAPE', - 0x0007002a: 'VK_BACK', - 0x0007002b: 'VK_TAB', - 0x0007002c: 'VK_SPACE', - 0x0007002d: 'VK_MINUS', - 0x0007002e: 'VK_PLUS', // it is = - 0x0007002f: 'VK_LBRACKET', - 0x00070030: 'VK_RBRACKET', - 0x00070033: 'VK_SEMICOLON', - 0x00070034: 'VK_QUOTE', - 0x00070036: 'VK_COMMA', - 0x00070038: 'VK_SLASH', - 0x00070039: 'VK_CAPITAL', - 0x0007003a: 'VK_F1', - 0x0007003b: 'VK_F2', - 0x0007003c: 'VK_F3', - 0x0007003d: 'VK_F4', - 0x0007003e: 'VK_F5', - 0x0007003f: 'VK_F6', - 0x00070040: 'VK_F7', - 0x00070041: 'VK_F8', - 0x00070042: 'VK_F9', - 0x00070043: 'VK_F10', - 0x00070044: 'VK_F11', - 0x00070045: 'VK_F12', - 0x00070049: 'VK_INSERT', - 0x0007004a: 'VK_HOME', - 0x0007004b: 'VK_PRIOR', // Page Up - 0x0007004c: 'VK_DELETE', - 0x0007004d: 'VK_END', - 0x0007004e: 'VK_NEXT', // Page Down - 0x0007004f: 'VK_RIGHT', - 0x00070050: 'VK_LEFT', - 0x00070051: 'VK_DOWN', - 0x00070052: 'VK_UP', - 0x00070053: 'Num Lock', // TODO rust not impl - 0x00070054: 'VK_DIVIDE', // numpad - 0x00070055: 'VK_MULTIPLY', - 0x00070056: 'VK_SUBTRACT', - 0x00070057: 'VK_ADD', - 0x00070058: 'VK_ENTER', // num enter - 0x00070059: 'VK_NUMPAD0', - 0x0007005a: 'VK_NUMPAD1', - 0x0007005b: 'VK_NUMPAD2', - 0x0007005c: 'VK_NUMPAD3', - 0x0007005d: 'VK_NUMPAD4', - 0x0007005e: 'VK_NUMPAD5', - 0x0007005f: 'VK_NUMPAD6', - 0x00070060: 'VK_NUMPAD7', - 0x00070061: 'VK_NUMPAD8', - 0x00070062: 'VK_NUMPAD9', - 0x00070063: 'VK_DECIMAL', - 0x00070075: 'VK_HELP', - 0x00070077: 'VK_SELECT', - 0x00070088: 'VK_KANA', - 0x0007008a: 'VK_CONVERT', - 0x000700e0: 'VK_CONTROL', - 0x000700e1: 'VK_SHIFT', - 0x000700e2: 'VK_MENU', - 0x000700e3: 'Meta', - 0x000700e4: 'RControl', - 0x000700e5: 'RShift', - 0x000700e6: 'RAlt', - 0x000700e7: 'RWin', - 0x000c00b1: 'VK_PAUSE', - 0x000c00cd: 'VK_PAUSE', - 0x000c019e: 'LOCK_SCREEN', - 0x000c0208: 'VK_PRINT', -}; From e9d94fdb245a56e88a53208feab844e7e7c70f18 Mon Sep 17 00:00:00 2001 From: csf Date: Tue, 13 Sep 2022 21:38:47 +0800 Subject: [PATCH 0466/2015] mv lib/cm_main.dart to test/cm_test.dart --- flutter/{lib/cm_main.dart => test/cm_test.dart} | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) rename flutter/{lib/cm_main.dart => test/cm_test.dart} (79%) diff --git a/flutter/lib/cm_main.dart b/flutter/test/cm_test.dart similarity index 79% rename from flutter/lib/cm_main.dart rename to flutter/test/cm_test.dart index 422e7aa26..704124781 100644 --- a/flutter/lib/cm_main.dart +++ b/flutter/test/cm_test.dart @@ -1,18 +1,17 @@ import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/consts.dart'; +import 'package:flutter_hbb/desktop/pages/server_page.dart'; import 'package:flutter_hbb/main.dart'; +import 'package:flutter_hbb/models/server_model.dart'; import 'package:get/get.dart'; import 'package:window_manager/window_manager.dart'; -import 'common.dart'; -import 'desktop/pages/server_page.dart'; -import 'models/server_model.dart'; - /// -t lib/cm_main.dart to test cm void main(List args) async { WidgetsFlutterBinding.ensureInitialized(); await windowManager.ensureInitialized(); - await windowManager.setSize(Size(400, 600)); + await windowManager.setSize(const Size(400, 600)); await windowManager.setAlignment(Alignment.topRight); await initEnv(kAppTypeMain); gFFI.serverModel.clients @@ -23,6 +22,6 @@ void main(List args) async { .add(Client(2, false, false, "UserC", "331123123", true, false, false)); gFFI.serverModel.clients .add(Client(3, false, false, "UserD", "441123123", true, false, false)); - runApp(GetMaterialApp( + runApp(const GetMaterialApp( debugShowCheckedModeBanner: false, home: DesktopServerPage())); } From 2e2bf3b8fbe35017eb7d028134fbde6cc21f7bf0 Mon Sep 17 00:00:00 2001 From: csf Date: Tue, 13 Sep 2022 21:52:22 +0800 Subject: [PATCH 0467/2015] optimize model.dart --- flutter/lib/models/model.dart | 49 ++++++----------------------------- 1 file changed, 8 insertions(+), 41 deletions(-) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index c6f38534a..b9225414d 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -331,7 +331,7 @@ class ImageModel with ChangeNotifier { ui.Image? get image => _image; - String _id = ''; + String id = ''; WeakReference parent; @@ -375,7 +375,7 @@ class ImageModel with ChangeNotifier { await initializeCursorAndCanvas(parent.target!); } if (parent.target?.ffiModel.isPeerAndroid ?? false) { - bind.sessionPeerOption(id: _id, name: 'view-style', value: 'adaptive'); + bind.sessionPeerOption(id: id, name: 'view-style', value: 'adaptive'); parent.target?.canvasModel.updateViewStyle(); } } @@ -468,7 +468,7 @@ class CanvasModel with ChangeNotifier { double _scale = 1.0; // the tabbar over the image double tabBarHeight = 0.0; - // TODO multi canvas model + // remote id String id = ''; // scroll offset x percent double _scrollX = 0.0; @@ -681,7 +681,7 @@ class CursorModel with ChangeNotifier { double _hoty = 0; double _displayOriginX = 0; double _displayOriginY = 0; - String id = ''; // TODO multi cursor model + String id = ''; WeakReference parent; ui.Image? get image => _image; @@ -991,7 +991,7 @@ extension ToString on MouseButtons { enum ConnType { defaultConn, fileTransfer, portForward, rdp } -/// FFI class for communicating with the Rust core. +/// Flutter state manager and data communication with the Rust core. class FFI { var id = ''; var shift = false; @@ -1020,7 +1020,7 @@ class FFI { ffiModel = FfiModel(WeakReference(this)); cursorModel = CursorModel(WeakReference(this)); canvasModel = CanvasModel(WeakReference(this)); - serverModel = ServerModel(WeakReference(this)); // use global FFI + serverModel = ServerModel(WeakReference(this)); chatModel = ChatModel(WeakReference(this)); fileModel = FileModel(WeakReference(this)); abModel = AbModel(WeakReference(this)); @@ -1042,12 +1042,6 @@ class FFI { .encode(modify({'id': id, 'type': 'wheel', 'y': y.toString()}))); } - /// Reconnect to the remote peer. - // static reconnect() { - // setByName('reconnect'); - // parent.target?.ffiModel.clearPermissions(); - // } - /// Reset key modifiers to false, including [shift], [ctrl], [alt] and [command]. resetModifiers() { shift = ctrl = alt = command = false; @@ -1070,7 +1064,7 @@ class FFI { msg: json.encode(modify({'type': type, 'buttons': button.value}))); } - // Raw Key + /// Send raw Key Event inputRawKey(String name, int keyCode, int scanCode, bool down) { bind.sessionHandleFlutterKeyEvent( id: id, @@ -1080,10 +1074,6 @@ class FFI { downOrUp: down); } - Future getKeyboardMode() { - return bind.sessionGetKeyboardName(id: id); - } - enterOrLeave(bool enter) { // Fix status if (!enter) { @@ -1097,18 +1087,6 @@ class FFI { /// [press] indicates a click event(down and up). inputKey(String name, {bool? down, bool? press}) { if (!ffiModel.keyboard()) return; - // final Map out = Map(); - // out['name'] = name; - // // default: down = false - // if (down == true) { - // out['down'] = 'true'; - // } - // // default: press = true - // if (press != false) { - // out['press'] = 'true'; - // } - // setByName('input_key', json.encode(modify(out))); - // TODO id bind.sessionInputKey( id: id, name: name, @@ -1161,7 +1139,7 @@ class FFI { } else { chatModel.resetClientMode(); canvasModel.id = id; - imageModel._id = id; + imageModel.id = id; cursorModel.id = id; } // ignore: unused_local_variable @@ -1212,17 +1190,6 @@ class FFI { debugPrint('model $id closed'); } - /// Send **get** command to the Rust core based on [name] and [arg]. - /// Return the result as a string. - // String getByName(String name, [String arg = '']) { - // return platformFFI.getByName(name, arg); - // } - - /// Send **set** command to the Rust core based on [name] and [value]. - // setByName(String name, [String value = '']) { - // platformFFI.setByName(name, value); - // } - handleMouse(Map evt, {double tabBarHeight = 0.0}) { var type = ''; var isMove = false; From 583ccb4b667f272b9085321b5cc9c580d2d07df1 Mon Sep 17 00:00:00 2001 From: csf Date: Tue, 13 Sep 2022 22:37:16 +0800 Subject: [PATCH 0468/2015] del finished TODOs --- src/flutter.rs | 2 +- src/flutter_ffi.rs | 4 ---- src/ui/remote.rs | 1 - src/ui_interface.rs | 7 +------ src/ui_session_interface.rs | 6 +----- 5 files changed, 3 insertions(+), 17 deletions(-) diff --git a/src/flutter.rs b/src/flutter.rs index 81b455b04..566576b5d 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -365,7 +365,7 @@ pub mod connection_manager { log::debug!("call_service_set_by_name fail,{}", e); } // send to UI, refresh widget - self.push_event("add_connection", vec![("client", &client_json)]); // TODO use add_connection + self.push_event("add_connection", vec![("client", &client_json)]); } fn remove_connection(&self, id: i32) { diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index f3c7b3735..568096b1c 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -575,7 +575,6 @@ pub fn main_forget_password(id: String) { forget_password(id) } -// TODO APP_DIR & ui_interface pub fn main_get_recent_peers() -> String { if !config::APP_DIR.read().unwrap().is_empty() { let peers: Vec<(String, config::PeerInfoSerde)> = PeerConfig::peers() @@ -709,9 +708,6 @@ pub fn session_new_rdp(id: String) { } pub fn main_get_last_remote_id() -> String { - // if !config::APP_DIR.read().unwrap().is_empty() { - // res = LocalConfig::get_remote_id(); - // } LocalConfig::get_remote_id() } diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 9cf04b69a..76e1ee96c 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -411,7 +411,6 @@ impl sciter::EventHandler for SciterSession { impl SciterSession { pub fn new(cmd: String, id: String, password: String, args: Vec) -> Self { let session: Session = Session { - cmd: cmd.clone(), id: id.clone(), password: password.clone(), args, diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 0b95cd588..133dd24b0 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -139,10 +139,6 @@ pub fn get_license() -> String { pub fn get_option(key: String) -> String { get_option_(&key) - // #[cfg(any(target_os = "android", target_os = "ios"))] - // return Config::get_option(&key); - // #[cfg(not(any(target_os = "android", target_os = "ios")))] - // return get_option_(&key); } fn get_option_(key: &str) -> String { @@ -208,7 +204,6 @@ pub fn get_sound_inputs() -> Vec { let mut a = Vec::new(); #[cfg(not(target_os = "linux"))] { - // TODO TEST fn get_sound_inputs_() -> Vec { let mut out = Vec::new(); use cpal::traits::{DeviceTrait, HostTrait}; @@ -236,7 +231,7 @@ pub fn get_sound_inputs() -> Vec { a.push(name); } } - #[cfg(target_os = "linux")] // TODO + #[cfg(target_os = "linux")] { let inputs: Vec = crate::platform::linux::get_pa_sources() .drain(..) diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 7af3a2fbd..945c45385 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -35,7 +35,6 @@ lazy_static::lazy_static! { #[derive(Clone, Default)] pub struct Session { - pub cmd: String, pub id: String, pub password: String, pub args: Vec, @@ -53,7 +52,7 @@ impl Session { pub fn get_image_quality(&self) -> String { self.lc.read().unwrap().image_quality.clone() } - /// Get custom image quality. + pub fn get_custom_image_quality(&self) -> Vec { self.lc.read().unwrap().custom_image_quality.clone() } @@ -1115,7 +1114,6 @@ impl Interface for Session { crate::platform::windows::add_recent_document(&path); } } - // TODO use event callbcak #[cfg(not(any(target_os = "android", target_os = "ios")))] self.start_keyboard_hook(); } @@ -1158,8 +1156,6 @@ impl Interface for Session { } } -// TODO use event callbcak -// sciter only #[cfg(not(any(target_os = "android", target_os = "ios")))] impl Session { fn start_keyboard_hook(&self) { From d92bbc045ab76e74b9c52dbaa4e5b7c05f236379 Mon Sep 17 00:00:00 2001 From: csf Date: Tue, 13 Sep 2022 22:48:14 +0800 Subject: [PATCH 0469/2015] add main ui interface #[inline] --- src/ui_interface.rs | 78 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 133dd24b0..af7e8ba05 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -40,6 +40,7 @@ lazy_static::lazy_static! { pub static ref SENDER : Mutex> = Mutex::new(check_connect_status(true)); } +#[inline] pub fn recent_sessions_updated() -> bool { let mut childs = CHILDS.lock().unwrap(); if childs.0 { @@ -50,6 +51,7 @@ pub fn recent_sessions_updated() -> bool { } } +#[inline] pub fn get_id() -> String { #[cfg(any(target_os = "android", target_os = "ios"))] return Config::get_id(); @@ -57,18 +59,22 @@ 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"])); } +#[inline] pub fn install_me(_options: String, _path: String, silent: bool, debug: bool) { #[cfg(windows)] std::thread::spawn(move || { @@ -79,6 +85,7 @@ pub fn install_me(_options: String, _path: String, silent: bool, debug: bool) { }); } +#[inline] pub fn update_me(_path: String) { #[cfg(target_os = "linux")] { @@ -105,11 +112,13 @@ pub fn update_me(_path: String) { } } +#[inline] pub fn run_without_install() { crate::run_me(vec!["--noinstall"]).ok(); std::process::exit(0); } +#[inline] pub fn show_run_without_install() -> bool { let mut it = std::env::args(); if let Some(tmp) = it.next() { @@ -120,12 +129,14 @@ pub fn show_run_without_install() -> bool { false } +#[inline] pub fn has_rendezvous_service() -> bool { #[cfg(all(windows, feature = "hbbs"))] return crate::platform::is_win_server() && crate::platform::windows::get_license().is_some(); return false; } +#[inline] pub fn get_license() -> String { #[cfg(windows)] if let Some(lic) = crate::platform::windows::get_license() { @@ -137,10 +148,12 @@ pub fn get_license() -> String { Default::default() } +#[inline] pub fn get_option(key: String) -> String { get_option_(&key) } +#[inline] fn get_option_(key: &str) -> String { let map = OPTIONS.lock().unwrap(); if let Some(v) = map.get(key) { @@ -150,29 +163,35 @@ fn get_option_(key: &str) -> String { } } +#[inline] pub fn get_local_option(key: String) -> String { LocalConfig::get_option(&key) } +#[inline] pub fn set_local_option(key: String, value: String) { LocalConfig::set_option(key, value); } +#[inline] pub fn peer_has_password(id: String) -> bool { !PeerConfig::load(&id).password.is_empty() } +#[inline] pub fn forget_password(id: String) { let mut c = PeerConfig::load(&id); c.password.clear(); c.store(&id); } +#[inline] pub fn get_peer_option(id: String, name: String) -> String { let c = PeerConfig::load(&id); c.options.get(&name).unwrap_or(&"".to_owned()).to_owned() } +#[inline] pub fn set_peer_option(id: String, name: String, value: String) { let mut c = PeerConfig::load(&id); if value.is_empty() { @@ -183,10 +202,12 @@ pub fn set_peer_option(id: String, name: String, value: String) { c.store(&id); } +#[inline] pub fn using_public_server() -> bool { crate::get_custom_rendezvous_server(get_option_("custom-rendezvous-server")).is_empty() } +#[inline] pub fn get_options() -> String { let options = OPTIONS.lock().unwrap(); let mut m = serde_json::Map::new(); @@ -196,10 +217,12 @@ pub fn get_options() -> String { serde_json::to_string(&m).unwrap() } +#[inline] pub fn test_if_valid_server(host: String) -> String { hbb_common::socket_client::test_if_valid_server(&host) } +#[inline] pub fn get_sound_inputs() -> Vec { let mut a = Vec::new(); #[cfg(not(target_os = "linux"))] @@ -245,6 +268,7 @@ pub fn get_sound_inputs() -> Vec { a } +#[inline] pub fn set_options(m: HashMap) { *OPTIONS.lock().unwrap() = m.clone(); #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -253,6 +277,7 @@ pub fn set_options(m: HashMap) { Config::set_options(m); } +#[inline] pub fn set_option(key: String, value: String) { let mut options = OPTIONS.lock().unwrap(); #[cfg(target_os = "macos")] @@ -273,6 +298,7 @@ pub fn set_option(key: String, value: String) { Config::set_option(key, value); } +#[inline] pub fn install_path() -> String { #[cfg(windows)] return crate::platform::windows::get_install_info().1; @@ -280,6 +306,7 @@ pub fn install_path() -> String { return "".to_owned(); } +#[inline] pub fn get_socks() -> Vec { #[cfg(any(target_os = "android", target_os = "ios"))] return Vec::new(); @@ -299,6 +326,7 @@ pub fn get_socks() -> Vec { } } +#[inline] pub fn set_socks(proxy: String, username: String, password: String) { #[cfg(not(any(target_os = "android", target_os = "ios")))] ipc::set_socks(config::Socks5Server { @@ -310,10 +338,12 @@ pub fn set_socks(proxy: String, username: String, password: String) { } #[cfg(not(any(target_os = "android", target_os = "ios")))] +#[inline] pub fn is_installed() -> bool { crate::platform::is_installed() } +#[inline] pub fn is_rdp_service_open() -> bool { #[cfg(windows)] return is_installed() && crate::platform::windows::is_rdp_service_open(); @@ -321,6 +351,7 @@ pub fn is_rdp_service_open() -> bool { return false; } +#[inline] pub fn is_share_rdp() -> bool { #[cfg(windows)] return crate::platform::windows::is_share_rdp(); @@ -328,11 +359,13 @@ pub fn is_share_rdp() -> bool { return false; } +#[inline] pub fn set_share_rdp(_enable: bool) { #[cfg(windows)] crate::platform::windows::set_share_rdp(_enable); } +#[inline] pub fn is_installed_lower_version() -> bool { #[cfg(not(windows))] return false; @@ -345,12 +378,14 @@ 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(); @@ -361,12 +396,14 @@ pub fn get_size() -> Vec { v } +#[inline] pub fn get_mouse_time() -> f64 { let ui_status = UI_STATUS.lock().unwrap(); let res = ui_status.2 as f64; return res; } +#[inline] pub fn check_mouse_time() { #[cfg(not(any(target_os = "android", target_os = "ios")))] { @@ -375,12 +412,14 @@ pub fn check_mouse_time() { } } +#[inline] pub fn get_connect_status() -> Status { let ui_statue = UI_STATUS.lock().unwrap(); let res = ui_statue.clone(); res } +#[inline] pub fn temporary_password() -> String { #[cfg(any(target_os = "android", target_os = "ios"))] return password_security::temporary_password(); @@ -388,6 +427,7 @@ pub fn temporary_password() -> String { return TEMPORARY_PASSWD.lock().unwrap().clone(); } +#[inline] pub fn update_temporary_password() { #[cfg(any(target_os = "android", target_os = "ios"))] password_security::update_temporary_password(); @@ -395,6 +435,7 @@ pub fn update_temporary_password() { allow_err!(ipc::update_temporary_password()); } +#[inline] pub fn permanent_password() -> String { #[cfg(any(target_os = "android", target_os = "ios"))] return Config::get_permanent_password(); @@ -402,6 +443,7 @@ pub fn permanent_password() -> String { return ipc::get_permanent_password(); } +#[inline] pub fn set_permanent_password(password: String) { #[cfg(any(target_os = "android", target_os = "ios"))] Config::set_permanent_password(&password); @@ -409,31 +451,38 @@ pub fn set_permanent_password(password: String) { allow_err!(ipc::set_permanent_password(password)); } +#[inline] pub fn get_peer(id: String) -> PeerConfig { PeerConfig::load(&id) } +#[inline] pub fn get_fav() -> Vec { LocalConfig::get_fav() } +#[inline] 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 = CHILDS.lock().unwrap(); let args = vec![format!("--{}", remote_type), id.clone()]; @@ -462,6 +511,7 @@ pub fn new_remote(id: String, remote_type: String) { } } +#[inline] pub fn is_process_trusted(_prompt: bool) -> bool { #[cfg(target_os = "macos")] return crate::platform::macos::is_process_trusted(_prompt); @@ -469,6 +519,7 @@ pub fn is_process_trusted(_prompt: bool) -> bool { return true; } +#[inline] pub fn is_can_screen_recording(_prompt: bool) -> bool { #[cfg(target_os = "macos")] return crate::platform::macos::is_can_screen_recording(_prompt); @@ -476,6 +527,7 @@ pub fn is_can_screen_recording(_prompt: bool) -> bool { return true; } +#[inline] pub fn is_installed_daemon(_prompt: bool) -> bool { #[cfg(target_os = "macos")] return crate::platform::macos::is_installed_daemon(_prompt); @@ -483,6 +535,7 @@ pub fn is_installed_daemon(_prompt: bool) -> bool { return true; } +#[inline] pub fn get_error() -> String { #[cfg(not(any(feature = "cli")))] #[cfg(target_os = "linux")] @@ -503,6 +556,7 @@ pub fn get_error() -> String { return "".to_owned(); } +#[inline] pub fn is_login_wayland() -> bool { #[cfg(target_os = "linux")] return crate::platform::linux::is_login_wayland(); @@ -510,11 +564,13 @@ pub fn is_login_wayland() -> bool { return false; } +#[inline] pub fn fix_login_wayland() { #[cfg(target_os = "linux")] crate::platform::linux::fix_login_wayland(); } +#[inline] pub fn current_is_wayland() -> bool { #[cfg(target_os = "linux")] return crate::platform::linux::current_is_wayland(); @@ -522,6 +578,7 @@ pub fn current_is_wayland() -> bool { return false; } +#[inline] pub fn modify_default_login() -> String { #[cfg(target_os = "linux")] return crate::platform::linux::modify_default_login(); @@ -529,22 +586,27 @@ pub fn modify_default_login() -> String { return "".to_owned(); } +#[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()) } +#[inline] pub fn get_version() -> String { crate::VERSION.to_owned() } +#[inline] 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)] @@ -556,6 +618,7 @@ pub fn get_software_ext() -> String { 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(); @@ -570,17 +633,20 @@ pub fn get_software_store_path() -> String { format!("{}.{}", p.to_string_lossy(), get_software_ext()) } +#[inline] pub fn create_shortcut(_id: String) { #[cfg(windows)] crate::platform::windows::create_shortcut(&_id).ok(); } +#[inline] pub fn discover() { std::thread::spawn(move || { allow_err!(crate::lan::discover()); }); } +#[inline] pub fn get_lan_peers() -> Vec<(String, config::PeerInfoSerde)> { config::LanPeers::load() .peers @@ -598,10 +664,12 @@ pub fn get_lan_peers() -> Vec<(String, config::PeerInfoSerde)> { .collect() } +#[inline] 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)] @@ -617,6 +685,7 @@ pub fn open_url(url: String) { allow_err!(std::process::Command::new(p).arg(url).spawn()); } +#[inline] #[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn change_id(id: String) { *ASYNC_JOB_STATUS.lock().unwrap() = " ".to_owned(); @@ -626,6 +695,7 @@ pub fn change_id(id: String) { }); } +#[inline] pub fn post_request(url: String, body: String, header: String) { *ASYNC_JOB_STATUS.lock().unwrap() = " ".to_owned(); std::thread::spawn(move || { @@ -636,28 +706,34 @@ 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() } +#[inline] pub fn is_xfce() -> bool { crate::platform::is_xfce() } +#[inline] pub fn get_api_server() -> String { crate::get_api_server( get_option_("api-server"), @@ -665,6 +741,7 @@ pub fn get_api_server() -> String { ) } +#[inline] pub fn has_hwcodec() -> bool { #[cfg(not(feature = "hwcodec"))] return false; @@ -672,6 +749,7 @@ pub fn has_hwcodec() -> bool { return true; } +#[inline] pub fn check_super_user_permission() -> bool { #[cfg(any(windows, target_os = "linux"))] return crate::platform::check_super_user_permission().unwrap_or(false); From c5a78ce107eade7e79bd82d1101f8507b7920b8f Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 13 Sep 2022 19:10:55 -0700 Subject: [PATCH 0470/2015] flutter_desktop: update custom cursor lib & menubar margin & better callback for pinning menubar Signed-off-by: fufesou --- flutter/lib/desktop/pages/remote_page.dart | 31 ++++++++++--- .../lib/desktop/pages/remote_tab_page.dart | 2 +- .../lib/desktop/widgets/remote_menubar.dart | 46 +++++++++++-------- flutter/pubspec.yaml | 2 +- 4 files changed, 54 insertions(+), 27 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 43d48740e..d8243700c 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -18,6 +18,8 @@ import '../../models/model.dart'; import '../../models/platform_model.dart'; import '../../common/shared_state.dart'; +bool _isCustomCursorInited = false; + class RemotePage extends StatefulWidget { const RemotePage({ Key? key, @@ -45,7 +47,7 @@ class _RemotePageState extends State var _isPhysicalMouse = false; var _imageFocused = false; - final _onEnterOrLeaveImage = []; + Function(bool)? _onEnterOrLeaveImage4Menubar; late FFI _ffi; @@ -95,6 +97,14 @@ class _RemotePageState extends State _ffi.qualityMonitorModel.checkShowQualityMonitor(widget.id); _showRemoteCursor.value = bind.sessionGetToggleOptionSync( id: widget.id, arg: 'show-remote-cursor'); + + if (!_isCustomCursorInited) { + customCursorController.registerNeedUpdateCursorCallback( + (String? lastKey, String? currentKey) async { + return lastKey == null || lastKey != currentKey; + }); + _isCustomCursorInited = true; + } } @override @@ -327,16 +337,24 @@ class _RemotePageState extends State _rawKeyFocusNode.requestFocus(); } _cursorOverImage.value = true; - for (var f in _onEnterOrLeaveImage) { - f(true); + if (_onEnterOrLeaveImage4Menubar != null) { + try { + _onEnterOrLeaveImage4Menubar!(true); + } catch (e) { + // + } } _ffi.enterOrLeave(true); } void leaveView(PointerExitEvent evt) { _cursorOverImage.value = false; - for (var f in _onEnterOrLeaveImage) { - f(false); + if (_onEnterOrLeaveImage4Menubar != null) { + try { + _onEnterOrLeaveImage4Menubar!(false); + } catch (e) { + // + } } _ffi.enterOrLeave(false); } @@ -381,7 +399,8 @@ class _RemotePageState extends State paints.add(RemoteMenubar( id: widget.id, ffi: _ffi, - onEnterOrLeaveImage: _onEnterOrLeaveImage, + onEnterOrLeaveImageSetter: (func) => _onEnterOrLeaveImage4Menubar = func, + onEnterOrLeaveImageCleaner: () => _onEnterOrLeaveImage4Menubar = null, )); return Stack( children: paints, diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index 2f87c51fb..4d4c7e6e1 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -98,7 +98,7 @@ class _ConnectionTabPageState extends State { controller: tabController, showTabBar: fullscreen.isFalse, onWindowCloseButton: handleWindowCloseButton, - tail: AddButton().paddingOnly(left: 10), + tail: const AddButton().paddingOnly(left: 10), pageViewBuilder: (pageView) { WindowController.fromWindowId(windowId()) .setFullscreen(fullscreen.isTrue); diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 7c8fd20b5..ef20352dc 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -26,13 +26,15 @@ class _MenubarTheme { class RemoteMenubar extends StatefulWidget { final String id; final FFI ffi; - final List onEnterOrLeaveImage; + final Function(Function(bool)) onEnterOrLeaveImageSetter; + final Function() onEnterOrLeaveImageCleaner; const RemoteMenubar({ Key? key, required this.id, required this.ffi, - required this.onEnterOrLeaveImage, + required this.onEnterOrLeaveImageSetter, + required this.onEnterOrLeaveImageCleaner, }) : super(key: key); @override @@ -52,10 +54,10 @@ class _RemoteMenubarState extends State { } @override - void initState() { + initState() { super.initState(); - widget.onEnterOrLeaveImage.add((enter) { + widget.onEnterOrLeaveImageSetter((enter) { if (enter) { _rxHideReplay.add(0); _isCursorOverImage = true; @@ -74,6 +76,13 @@ class _RemoteMenubarState extends State { }); } + @override + dispose() { + super.dispose(); + + widget.onEnterOrLeaveImageCleaner(); + } + @override Widget build(BuildContext context) { return Align( @@ -85,21 +94,20 @@ class _RemoteMenubarState extends State { Widget _buildShowHide(BuildContext context) { return Obx(() => Tooltip( - message: translate(_show.value ? "Hide Menubar" : "Show Menubar"), - child: SizedBox( - width: 100, - height: 5, - child: TextButton( - onHover: (bool v) { - _hideColor.value = v ? Colors.white60 : Colors.white24; - }, - onPressed: () { - _show.value = !_show.value; - }, - child: Obx(() => Container( - color: _hideColor.value, - )))), - )); + message: translate(_show.value ? "Hide Menubar" : "Show Menubar"), + child: SizedBox( + width: 100, + height: 13, + child: TextButton( + onHover: (bool v) { + _hideColor.value = v ? Colors.white60 : Colors.white24; + }, + onPressed: () { + _show.value = !_show.value; + }, + child: Obx(() => Container( + color: _hideColor.value, + ).marginOnly(bottom: 8.0)))))); } Widget _buildMenubar(BuildContext context) { diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 5556b0dd5..e3b1c883a 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -71,7 +71,7 @@ dependencies: flutter_custom_cursor: git: url: https://github.com/Kingtous/rustdesk_flutter_custom_cursor - ref: 9021e21de36c84edf01d5034f38eda580463163b + ref: 47179378523c993092f70d95f93d53f40af01f02 get: ^4.6.5 visibility_detector: ^0.3.3 contextmenu: ^3.0.0 From 4d2feb60303ed24452e8c0bb2d3d4068608455c9 Mon Sep 17 00:00:00 2001 From: Asura Date: Tue, 13 Sep 2022 19:30:57 -0700 Subject: [PATCH 0471/2015] Add polkit to build.py --- build.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/build.py b/build.py index 27b5af744..97b1c02f9 100755 --- a/build.py +++ b/build.py @@ -142,12 +142,12 @@ def build_flutter_deb(version): os.chdir('flutter') os.system('dpkg-deb -R rustdesk.deb tmpdeb') os.system('flutter build linux --release') - os.system('strip build/linux/x64/release/liblibrustdesk.so') os.system('mkdir -p tmpdeb/usr/bin/') os.system('mkdir -p tmpdeb/usr/lib/rustdesk') os.system('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/') os.system('mkdir -p tmpdeb/usr/share/applications/') + os.system('mkdir -p tmpdeb/usr/share/polkit-1/actions') os.system( 'cp -r build/linux/x64/release/bundle/* tmpdeb/usr/lib/rustdesk/') @@ -163,6 +163,10 @@ def build_flutter_deb(version): 'cp ../128x128@2x.png tmpdeb/usr/share/rustdesk/files/rustdesk.png') os.system( 'cp ../rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop') + os.system( + 'cp ../com.rustdesk.RustDesk.policy tmpdeb/usr/share/polkit-1/actions/') + os.system("echo \"#!/bin/sh\" >> tmpdeb/usr/share/rustdesk/files/polkit && chmod a+x tmpdeb/usr/share/rustdesk/files/polkit") + os.system('mkdir -p tmpdeb/DEBIAN') generate_control_file(version) os.system('cp -a ../DEBIAN/* tmpdeb/DEBIAN/') From e6b1b007a6efe615f60ff0b31b60fd9c3c387730 Mon Sep 17 00:00:00 2001 From: Asura Date: Wed, 14 Sep 2022 02:37:52 -0700 Subject: [PATCH 0472/2015] Fix compile on android --- Cargo.lock | 2 +- flutter/pubspec.lock | 14 +++++++------- src/flutter_ffi.rs | 20 ++++++++++++-------- src/ui_interface.rs | 1 + src/ui_session_interface.rs | 30 +++++++++++++++++++++++------- 5 files changed, 44 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3a6abcaf7..aa6d98497 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3975,7 +3975,7 @@ dependencies = [ [[package]] name = "rdev" version = "0.5.0-2" -source = "git+https://github.com/asur4s/rdev#0ad53987fa6f0e37a7bc000358f71c3802de4e7c" +source = "git+https://github.com/asur4s/rdev#22c737fe4dadb7f16c8ded2c13bd6f578a1db4ce" dependencies = [ "cocoa", "core-foundation 0.9.3", diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 251d3dacb..c0004499e 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -70,7 +70,7 @@ packages: name: build url: "https://pub.dartlang.org" source: hosted - version: "2.3.0" + version: "2.3.1" build_config: dependency: transitive description: @@ -91,21 +91,21 @@ packages: name: build_resolvers url: "https://pub.dartlang.org" source: hosted - version: "2.0.9" + version: "2.0.10" build_runner: dependency: "direct dev" description: name: build_runner url: "https://pub.dartlang.org" source: hosted - version: "2.2.0" + version: "2.2.1" build_runner_core: dependency: transitive description: name: build_runner_core url: "https://pub.dartlang.org" source: hosted - version: "7.2.3" + version: "7.2.4" built_collection: dependency: transitive description: @@ -418,8 +418,8 @@ packages: dependency: "direct main" description: path: "." - ref: "9021e21de36c84edf01d5034f38eda580463163b" - resolved-ref: "9021e21de36c84edf01d5034f38eda580463163b" + ref: "47179378523c993092f70d95f93d53f40af01f02" + resolved-ref: "47179378523c993092f70d95f93d53f40af01f02" url: "https://github.com/Kingtous/rustdesk_flutter_custom_cursor" source: git version: "0.0.1" @@ -973,7 +973,7 @@ packages: name: source_gen url: "https://pub.dartlang.org" source: hosted - version: "1.2.2" + version: "1.2.3" source_span: dependency: transitive description: diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 568096b1c..c3b71e4c9 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -17,14 +17,15 @@ use crate::flutter::{self, SESSIONS}; use crate::start_server; use crate::ui_interface; #[cfg(not(any(target_os = "android", target_os = "ios")))] +use crate::ui_interface::{change_id, get_sound_inputs}; use crate::ui_interface::{ - change_id, check_mouse_time, check_super_user_permission, discover, forget_password, - get_api_server, get_app_name, get_async_job_status, get_connect_status, get_fav, get_id, - get_lan_peers, get_langs, get_license, get_local_option, get_mouse_time, get_option, - get_options, get_peer, get_peer_option, get_socks, get_sound_inputs, get_uuid, get_version, - has_hwcodec, has_rendezvous_service, post_request, send_to_cm, set_local_option, set_option, - set_options, set_peer_option, set_permanent_password, set_socks, store_fav, - test_if_valid_server, update_temporary_password, using_public_server, + check_mouse_time, check_super_user_permission, discover, forget_password, get_api_server, + get_app_name, get_async_job_status, get_connect_status, get_fav, get_id, get_lan_peers, + get_langs, get_license, get_local_option, get_mouse_time, get_option, get_options, get_peer, + get_peer_option, get_socks, get_uuid, get_version, has_hwcodec, has_rendezvous_service, + post_request, send_to_cm, set_local_option, set_option, set_options, set_peer_option, + set_permanent_password, set_socks, store_fav, test_if_valid_server, update_temporary_password, + using_public_server, }; use crate::{ client::file_trait::FileManager, @@ -431,7 +432,10 @@ pub fn session_resume_job(id: String, act_id: i32, is_remote: bool) { } pub fn main_get_sound_inputs() -> Vec { - get_sound_inputs() + #[cfg(not(any(target_os = "android", target_os = "ios")))] + return get_sound_inputs(); + #[cfg(any(target_os = "android", target_os = "linux"))] + vec![String::from("")] } pub fn main_change_id(new_id: String) { diff --git a/src/ui_interface.rs b/src/ui_interface.rs index af7e8ba05..90afb8c43 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -223,6 +223,7 @@ pub fn test_if_valid_server(host: String) -> String { } #[inline] +#[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn get_sound_inputs() -> Vec { let mut a = Vec::new(); #[cfg(not(target_os = "linux"))] diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 945c45385..692b592d4 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -11,7 +11,9 @@ use async_trait::async_trait; use hbb_common::config::{Config, LocalConfig, PeerConfig}; use hbb_common::rendezvous_proto::ConnType; use hbb_common::tokio::{self, sync::mpsc}; -use rdev::{Event, EventType::*, Key as RdevKey, Keyboard as RdevKeyboard, KeyboardState}; +use rdev::{Event, EventType::*, Key as RdevKey, KeyboardState}; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use rdev::Keyboard as RdevKeyboard; use hbb_common::{allow_err, message_proto::*}; use hbb_common::{fs, get_version_number, log, Stream}; @@ -27,9 +29,12 @@ pub static KEYBOARD_HOOKED: AtomicBool = AtomicBool::new(true); #[cfg(windows)] static mut IS_ALT_GR: bool = false; -#[cfg(not(any(target_os = "android", target_os = "ios")))] lazy_static::lazy_static! { static ref TO_RELEASE: Arc>> = Arc::new(Mutex::new(HashSet::::new())); +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +lazy_static::lazy_static! { static ref KEYBOARD: Arc> = Arc::new(Mutex::new(RdevKeyboard::new().unwrap())); } @@ -268,6 +273,7 @@ impl Session { #[allow(dead_code)] fn convert_numpad_keys(&self, key: RdevKey) -> RdevKey { + #[cfg(not(any(target_os = "android", target_os = "ios")))] if get_key_state(enigo::Key::NumLock) { return key; } @@ -315,10 +321,11 @@ impl Session { key_event.set_chr(keycode); key_event.down = down_or_up; - + #[cfg(not(any(target_os = "android", target_os = "ios")))] if get_key_state(enigo::Key::CapsLock) { key_event.modifiers.push(ControlKey::CapsLock.into()); } + #[cfg(not(any(target_os = "android", target_os = "ios")))] if get_key_state(enigo::Key::NumLock) { key_event.modifiers.push(ControlKey::NumLock.into()); } @@ -326,6 +333,7 @@ impl Session { self.send_key_event(key_event, KeyboardMode::Map); } + #[cfg(not(any(target_os = "android", target_os = "ios")))] fn translate_keyboard_mode(&self, down_or_up: bool, key: RdevKey, evt: Event) { // translate mode(2): locally generated characters are send to the peer. @@ -407,10 +415,11 @@ impl Session { { key_event.modifiers.push(ControlKey::Meta.into()); } - + #[cfg(not(any(target_os = "android", target_os = "ios")))] if get_key_state(enigo::Key::CapsLock) { key_event.modifiers.push(ControlKey::CapsLock.into()); } + #[cfg(not(any(target_os = "android", target_os = "ios")))] if self.peer_platform() != "Mac OS" { if get_key_state(enigo::Key::NumLock) { key_event.modifiers.push(ControlKey::NumLock.into()); @@ -418,6 +427,7 @@ impl Session { } } + #[cfg(not(any(target_os = "android", target_os = "ios")))] fn legacy_keyboard_mode(&self, down_or_up: bool, key: RdevKey, evt: Event) { // legacy mode(0): Generate characters locally, look for keycode on other side. let peer = self.peer_platform(); @@ -648,11 +658,18 @@ impl Session { } self.map_keyboard_mode(down_or_up, key, Some(evt)); } - KeyboardMode::Legacy => self.legacy_keyboard_mode(down_or_up, key, evt), + KeyboardMode::Legacy => { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + self.legacy_keyboard_mode(down_or_up, key, evt) + }, KeyboardMode::Translate => { + #[cfg(not(any(target_os = "android", target_os = "ios")))] self.translate_keyboard_mode(down_or_up, key, evt); } - _ => self.legacy_keyboard_mode(down_or_up, key, evt), + _ => { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + self.legacy_keyboard_mode(down_or_up, key, evt) + }, } } @@ -742,7 +759,6 @@ impl Session { let keycode: u32 = keycode as u32; let scancode: u32 = scancode as u32; - #[cfg(not(target_os = "windows"))] let key = rdev::key_from_scancode(scancode) as RdevKey; // Windows requires special handling #[cfg(target_os = "windows")] From 9502a2eddc3e3906cdfeacbafa65e1f04aedb8a0 Mon Sep 17 00:00:00 2001 From: Asura Date: Wed, 14 Sep 2022 05:31:19 -0700 Subject: [PATCH 0473/2015] Fix altgr of Korean --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index aa6d98497..e4b5d2792 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3975,7 +3975,7 @@ dependencies = [ [[package]] name = "rdev" version = "0.5.0-2" -source = "git+https://github.com/asur4s/rdev#22c737fe4dadb7f16c8ded2c13bd6f578a1db4ce" +source = "git+https://github.com/asur4s/rdev#bff57a29e3f14d032ab7441b2d6cf029df8adaca" dependencies = [ "cocoa", "core-foundation 0.9.3", From 088e31d80f10d4955f01b31029078c38bf02c565 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Thu, 15 Sep 2022 11:06:44 +0800 Subject: [PATCH 0474/2015] fix: add null catch on address book request --- .../lib/desktop/pages/connection_page.dart | 2 +- flutter/lib/desktop/widgets/peer_widget.dart | 2 +- flutter/lib/models/ab_model.dart | 24 +++++++++++-------- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 134fe4219..b7aa92c0f 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -414,7 +414,7 @@ class _ConnectionPageState extends State { ); } else { if (model.abLoading) { - return Center( + return const Center( child: CircularProgressIndicator(), ); } else if (model.abError.isNotEmpty) { diff --git a/flutter/lib/desktop/widgets/peer_widget.dart b/flutter/lib/desktop/widgets/peer_widget.dart index 32976fb5b..9511fcec8 100644 --- a/flutter/lib/desktop/widgets/peer_widget.dart +++ b/flutter/lib/desktop/widgets/peer_widget.dart @@ -266,7 +266,7 @@ class AddressBookPeerWidget extends BasePeerWidget { loadEvent: 'load_address_book_peers', offstageFunc: (Peer peer) => !_hitTag(gFFI.abModel.selectedTags, peer.tags), - peerCardWidgetFunc: (Peer peer) => DiscoveredPeerCard( + peerCardWidgetFunc: (Peer peer) => AddressBookPeerCard( peer: peer, ), initPeers: _loadPeers(), diff --git a/flutter/lib/models/ab_model.dart b/flutter/lib/models/ab_model.dart index 18bb73c3f..2749e972f 100644 --- a/flutter/lib/models/ab_model.dart +++ b/flutter/lib/models/ab_model.dart @@ -28,21 +28,26 @@ class AbModel with ChangeNotifier { try { final resp = await http.post(Uri.parse(api), headers: await _getHeaders()); - Map json = jsonDecode(resp.body); - if (json.containsKey('error')) { - abError = json['error']; - } else if (json.containsKey('data')) { - final data = jsonDecode(json['data']); - tags.value = data['tags']; - peers.value = data['peers']; + if (resp.body.isNotEmpty && resp.body.toLowerCase() != "null") { + Map json = jsonDecode(resp.body); + if (json.containsKey('error')) { + abError = json['error']; + } else if (json.containsKey('data')) { + final data = jsonDecode(json['data']); + tags.value = data['tags']; + peers.value = data['peers']; + } + notifyListeners(); + return resp.body; + } else { + return ""; } - return resp.body; } catch (err) { abError = err.toString(); } finally { + notifyListeners(); abLoading = false; } - notifyListeners(); return null; } @@ -60,7 +65,6 @@ class AbModel with ChangeNotifier { return _ffi?.getHttpHeaders(); } - /// void addId(String id) async { if (idContainBy(id)) { return; From af656f948936e1a1ca2ecebb744b031391871a15 Mon Sep 17 00:00:00 2001 From: csf Date: Sun, 17 Jul 2022 23:00:57 +0800 Subject: [PATCH 0475/2015] add mediacodec.rs --- libs/scrap/src/common/mediacodec.rs | 187 ++++++++++++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 libs/scrap/src/common/mediacodec.rs diff --git a/libs/scrap/src/common/mediacodec.rs b/libs/scrap/src/common/mediacodec.rs new file mode 100644 index 000000000..6a0282b49 --- /dev/null +++ b/libs/scrap/src/common/mediacodec.rs @@ -0,0 +1,187 @@ +use std::{io::Write, time::Duration}; + +use hbb_common::{bail, ResultType}; +#[cfg(target_os = "android")] +use ndk::media::media_codec::{MediaCodec, MediaCodecDirection, MediaFormat}; + +use crate::{ + codec::{EncoderApi, EncoderCfg}, + I420ToARGB, +}; + +pub struct MediaCodecEncoder { + encoder: MediaCodec, +} + +impl EncoderApi for MediaCodecEncoder { + fn new(cfg: EncoderCfg) -> ResultType + where + Self: Sized, + { + if let EncoderCfg::HW(cfg) = cfg { + create_media_codec(&cfg.codec_name, MediaCodecDirection::Encoder) + } else { + bail!("encoder type mismatch") + } + } + + fn encode_to_message( + &mut self, + frame: &[u8], + ms: i64, + ) -> ResultType { + todo!() + } + + fn use_yuv(&self) -> bool { + todo!() + } + + fn set_bitrate(&mut self, bitrate: u32) -> ResultType<()> { + todo!() + } +} + +pub struct MediaCodecDecoder { + decoder: MediaCodec, + // pub info: CodecInfo, +} + +pub struct MediaCodecDecoders { + pub h264: Option, + pub h265: Option, +} + +// "video/x-vnd.on2.vp8" - VP8 video (i.e. video in .webm) +// "video/x-vnd.on2.vp9" - VP9 video (i.e. video in .webm) +// "video/avc" - H.264/AVC video +// "video/hevc" - H.265/HEVC video + +impl MediaCodecDecoder { + pub fn new_decoders() -> MediaCodecDecoders { + // 直接生成 h264 和 h265 + // 264 + let h264 = create_media_codec("video/avc", MediaCodecDirection::Decoder) + .map(|decoder| MediaCodecDecoder { decoder }); + let h265 = create_media_codec("video/hevc", MediaCodecDirection::Decoder) + .map(|decoder| MediaCodecDecoder { decoder }); + + MediaCodecDecoders { h264, h265 } + } + + pub fn decode(&mut self, data: &[u8], rgb: &mut Vec) -> ResultType { + log::debug!("start dequeue_input"); + + match self + .decoder + .dequeue_input_buffer(Duration::from_millis(10)) + .unwrap() + { + Some(mut input_buffer) => { + let mut buf = input_buffer.buffer_mut(); + log::debug!( + "dequeue_input success:buf ptr:{:?},len:{}", + buf.as_ptr(), + buf.len() + ); + if data.len() > buf.len() { + log::error!("break! res.len()>buf.len()"); + bail!("break! res.len()>buf.len()"); + } + buf.write_all(&data).unwrap(); + if let Err(e) = self + .decoder + .queue_input_buffer(input_buffer, 0, data.len(), 0, 0) + { + log::debug!("debug queue_input_buffer:{:?}", e); + }; + } + None => { + log::debug!("dequeue_input_buffer fail :None"); + } + }; + + return match self + .decoder + .dequeue_output_buffer(Duration::from_millis(100)) + { + Ok(Some(output_buffer)) => { + log::debug!("dequeue_output success"); + // let res_format = output_buffer.format(); + let res_format = self.decoder.output_format(); + log::debug!("res_format:{:?}", res_format.str("mime")); + log::debug!("res_color:{:?}", res_format.i32("color-format")); + log::debug!("stride:{:?}", res_format.i32("stride")); + let w = res_format.i32("width").unwrap() as usize; + let h = res_format.i32("height").unwrap() as usize; + let stride = res_format.i32("stride").unwrap(); // todo + + // let w = 1920; + // let h = 1080; + // let stride = 1920; // todo + + let buf = output_buffer.buffer(); + log::debug!("output_buffer ptr:{:?} len:{}", buf.as_ptr(), buf.len()); + let bps = 4; + let u = buf.len() * 2 / 3; + let v = buf.len() * 5 / 6; + rgb.resize(h * w * bps, 0); + log::debug!("start I420ToARGB,u:{},v:{},w:{},h:{}", u, v, w, h); + let y_ptr = buf.as_ptr(); + let u_ptr = buf[u..].as_ptr(); + let v_ptr = buf[v..].as_ptr(); + log::debug!("ptr,y:{:?},u:{:?},v:{:?}", y_ptr, u_ptr, v_ptr); + unsafe { + I420ToARGB( + y_ptr, + stride, + u_ptr, + stride / 2, + v_ptr, + stride / 2, + rgb.as_mut_ptr(), + (w * bps) as _, + w as _, + h as _, + ); + } + log::debug!("end I420ToARGB"); + log::debug!("release_output_buffer"); + self.decoder + .release_output_buffer(output_buffer, false) + .unwrap(); + log::debug!("return true"); + Ok(true) + } + Ok(None) => { + log::debug!("dequeue_output fail :None"); + Ok(false) + } + Err(e) => { + log::debug!("dequeue_output fail :error:{:?}", e); + Ok(false) + } + }; + } +} + +fn create_media_codec(name: &str, direction: MediaCodecDirection) -> Option { + let codec = MediaCodec::from_decoder_type(name).unwrap(); + log::debug!("start init"); + let media_format = MediaFormat::new(); + media_format.set_str("mime", name); + media_format.set_i32("width", 0); + media_format.set_i32("height", 0); + media_format.set_i32("color-format", 19); // COLOR_FormatYUV420Planar + if let Err(e) = codec.configure(&media_format, None, direction) { + log::error!("failed to decoder.init:{:?}", e); + return None; + }; + log::error!("decoder init success"); + if let Err(e) = codec.start() { + log::error!("failed to decoder.start:{:?}", e); + return None; + }; + log::debug!("init decoder successed!:{:?}", name); + return Some(codec); +} From f17198cd2a3205d890087ae41b6aa6d8a844f222 Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 14 Sep 2022 22:22:23 -0700 Subject: [PATCH 0476/2015] flutter_desktop: fix remove fav peer && remove unused code Signed-off-by: fufesou --- .../lib/desktop/pages/connection_page.dart | 1 + flutter/lib/desktop/widgets/peer_widget.dart | 19 ++++-- .../lib/desktop/widgets/peercard_widget.dart | 66 ++++++------------- 3 files changed, 34 insertions(+), 52 deletions(-) diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index b7aa92c0f..9024c996f 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -782,6 +782,7 @@ class _PeerTabbedPageState extends State<_PeerTabbedPage> bind.mainDiscover(); break; case 3: + gFFI.abModel.updateAb(); break; } } diff --git a/flutter/lib/desktop/widgets/peer_widget.dart b/flutter/lib/desktop/widgets/peer_widget.dart index 9511fcec8..f137241a9 100644 --- a/flutter/lib/desktop/widgets/peer_widget.dart +++ b/flutter/lib/desktop/widgets/peer_widget.dart @@ -43,7 +43,7 @@ class _PeerWidgetState extends State<_PeerWidget> with WindowListener { final _curPeers = {}; var _lastChangeTime = DateTime.now(); var _lastQueryPeers = {}; - var _lastQueryTime = DateTime.now().subtract(Duration(hours: 1)); + var _lastQueryTime = DateTime.now().subtract(const Duration(hours: 1)); var _queryCoun = 0; var _exit = false; @@ -143,8 +143,8 @@ class _PeerWidgetState extends State<_PeerWidget> with WindowListener { while (!_exit) { final now = DateTime.now(); if (!setEquals(_curPeers, _lastQueryPeers)) { - if (now.difference(_lastChangeTime) > Duration(seconds: 1)) { - if (_curPeers.length > 0) { + if (now.difference(_lastChangeTime) > const Duration(seconds: 1)) { + if (_curPeers.isNotEmpty) { platformFFI.ffiBind .queryOnlines(ids: _curPeers.toList(growable: false)); _lastQueryPeers = {..._curPeers}; @@ -154,8 +154,8 @@ class _PeerWidgetState extends State<_PeerWidget> with WindowListener { } } else { if (_queryCoun < _maxQueryCount) { - if (now.difference(_lastQueryTime) > Duration(seconds: 20)) { - if (_curPeers.length > 0) { + if (now.difference(_lastQueryTime) > const Duration(seconds: 20)) { + if (_curPeers.isNotEmpty) { platformFFI.ffiBind .queryOnlines(ids: _curPeers.toList(growable: false)); _lastQueryTime = DateTime.now(); @@ -164,7 +164,7 @@ class _PeerWidgetState extends State<_PeerWidget> with WindowListener { } } } - await Future.delayed(Duration(milliseconds: 300)); + await Future.delayed(const Duration(milliseconds: 300)); } }(); } @@ -292,4 +292,11 @@ class AddressBookPeerWidget extends BasePeerWidget { } return true; } + + @override + Widget build(BuildContext context) { + final widget = super.build(context); + gFFI.abModel.updateAb(); + return widget; + } } diff --git a/flutter/lib/desktop/widgets/peercard_widget.dart b/flutter/lib/desktop/widgets/peercard_widget.dart index d9a87cd7b..fc93c59c6 100644 --- a/flutter/lib/desktop/widgets/peercard_widget.dart +++ b/flutter/lib/desktop/widgets/peercard_widget.dart @@ -113,15 +113,16 @@ class _PeerCardState extends State<_PeerCard> children: [ Row(children: [ Padding( - padding: EdgeInsets.fromLTRB(0, 4, 4, 4), + padding: const EdgeInsets.fromLTRB(0, 4, 4, 4), child: CircleAvatar( radius: 5, backgroundColor: peer.online ? Colors.green : Colors.yellow)), Text( - formatID('${peer.id}'), - style: TextStyle(fontWeight: FontWeight.w400), + formatID(peer.id), + style: + const TextStyle(fontWeight: FontWeight.w400), ), ]), Align( @@ -136,7 +137,7 @@ class _PeerCardState extends State<_PeerCard> : snapshot.data!; return Tooltip( message: name, - waitDuration: Duration(seconds: 1), + waitDuration: const Duration(seconds: 1), child: Text( name, style: greyStyle, @@ -208,10 +209,11 @@ class _PeerCardState extends State<_PeerCard> : widget.alias.value; return Tooltip( message: name, - waitDuration: Duration(seconds: 1), + waitDuration: + const Duration(seconds: 1), child: Text( name, - style: TextStyle( + style: const TextStyle( color: Colors.white70, fontSize: 12), textAlign: TextAlign.center, @@ -236,7 +238,7 @@ class _PeerCardState extends State<_PeerCard> children: [ Row(children: [ Padding( - padding: EdgeInsets.fromLTRB(0, 4, 8, 4), + padding: const EdgeInsets.fromLTRB(0, 4, 8, 4), child: CircleAvatar( radius: 5, backgroundColor: peer.online @@ -501,7 +503,7 @@ abstract class BasePeerCard extends StatelessWidget { final favs = (await bind.mainGetFav()).toList(); if (!favs.contains(id)) { favs.add(id); - bind.mainStoreFav(favs: favs); + await bind.mainStoreFav(favs: favs); } }(); }, @@ -510,7 +512,8 @@ abstract class BasePeerCard extends StatelessWidget { } @protected - MenuEntryBase _rmFavAction(String id) { + MenuEntryBase _rmFavAction( + String id, Future Function() reloadFunc) { return MenuEntryButton( childBuilder: (TextStyle? style) => Text( translate('Remove from Favorites'), @@ -520,8 +523,9 @@ abstract class BasePeerCard extends StatelessWidget { () async { final favs = (await bind.mainGetFav()).toList(); if (favs.remove(id)) { - bind.mainStoreFav(favs: favs); - Get.forceAppUpdate(); // TODO use inner model / state + await bind.mainStoreFav(favs: favs); + await reloadFunc(); + // Get.forceAppUpdate(); // TODO use inner model / state } }(); }, @@ -646,7 +650,9 @@ class FavoritePeerCard extends BasePeerCard { await bind.mainLoadFavPeers(); })); menuItems.add(_unrememberPasswordAction(peer.id)); - menuItems.add(_rmFavAction(peer.id)); + menuItems.add(_rmFavAction(peer.id, () async { + await bind.mainLoadFavPeers(); + })); return menuItems; } } @@ -817,8 +823,8 @@ class AddressBookPeerCard extends BasePeerCard { color: rxTags.contains(tagName) ? Colors.blue : null, border: Border.all(color: MyTheme.darkGray), borderRadius: BorderRadius.circular(10)), - margin: EdgeInsets.symmetric(horizontal: 4.0, vertical: 8.0), - padding: EdgeInsets.symmetric(vertical: 2.0, horizontal: 8.0), + margin: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 8.0), + padding: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 8.0), child: Text( tagName, style: TextStyle( @@ -831,38 +837,6 @@ class AddressBookPeerCard extends BasePeerCard { } } -Future> _forceAlwaysRelayMenuItem(String id) async { - bool force_always_relay = - (await bind.mainGetPeerOption(id: id, key: 'force-always-relay')) - .isNotEmpty; - return PopupMenuItem( - child: Row( - children: [ - Offstage( - offstage: !force_always_relay, - child: Icon(Icons.check), - ), - Text(translate('Always connect via relay')), - ], - ), - value: 'force-always-relay'); -} - -PopupMenuItem _rdpMenuItem(String id) { - return PopupMenuItem( - child: Row( - children: [ - Text('RDP'), - SizedBox(width: 20), - IconButton( - icon: Icon(Icons.edit), - onPressed: () => _rdpDialog(id), - ) - ], - ), - value: 'RDP'); -} - void _rdpDialog(String id) async { final portController = TextEditingController( text: await bind.mainGetPeerOption(id: id, key: 'rdp_port')); From 25fd0d6148f2ea059f2ce72dec4ede00f3f5a3ea Mon Sep 17 00:00:00 2001 From: Asura Date: Wed, 14 Sep 2022 23:49:59 -0700 Subject: [PATCH 0477/2015] Refactor: input mode of desktop --- flutter/lib/desktop/pages/remote_page.dart | 198 ++------------------ flutter/lib/models/input_model.dart | 199 +++++++++++++++++++++ 2 files changed, 213 insertions(+), 184 deletions(-) create mode 100644 flutter/lib/models/input_model.dart diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index d8243700c..dad286ee7 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -5,6 +5,7 @@ import 'dart:ui' as ui; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_hbb/models/input_model.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:wakelock/wakelock.dart'; @@ -50,9 +51,12 @@ class _RemotePageState extends State Function(bool)? _onEnterOrLeaveImage4Menubar; late FFI _ffi; + late Keyboard _keyboard_input; + late Mouse _mouse_input; void _updateTabBarHeight() { _ffi.canvasModel.tabBarHeight = widget.tabBarHeight; + _mouse_input.tabBarHeight = widget.tabBarHeight; } void _initStates(String id) { @@ -80,7 +84,11 @@ class _RemotePageState extends State void initState() { super.initState(); _initStates(widget.id); + _ffi = FFI(); + _keyboard_input = Keyboard(_ffi, widget.id); + _mouse_input = Mouse(_ffi, widget.id, widget.tabBarHeight); + _updateTabBarHeight(); Get.put(_ffi, tag: widget.id); _ffi.connect(widget.id, tabBarHeight: super.widget.tabBarHeight); @@ -97,7 +105,6 @@ class _RemotePageState extends State _ffi.qualityMonitorModel.checkShowQualityMonitor(widget.id); _showRemoteCursor.value = bind.sessionGetToggleOptionSync( id: widget.id, arg: 'show-remote-cursor'); - if (!_isCustomCursorInited) { customCursorController.registerNeedUpdateCursorCallback( (String? lastKey, String? currentKey) async { @@ -129,14 +136,6 @@ class _RemotePageState extends State _ffi.resetModifiers(); } - void sendRawKey(RawKeyEvent e, {bool? down, bool? press}) { - // for maximum compatibility - final label = logicalKeyMap[e.logicalKey.keyId] ?? - physicalKeyMap[e.physicalKey.usbHidUsage] ?? - e.logicalKey.keyLabel; - _ffi.inputKey(label, down: down, press: press ?? false); - } - Widget buildBody(BuildContext context) { return Scaffold( backgroundColor: MyTheme.color(context).bg, @@ -170,92 +169,6 @@ class _RemotePageState extends State ], child: buildBody(context))); } - KeyEventResult handleRawKeyEvent(FocusNode data, RawKeyEvent e) { - bind.sessionGetKeyboardName(id: widget.id).then((result) { - setState(() { - keyboardMode = result.toString(); - }); - }); - - if (keyboardMode == 'map') { - mapKeyboardMode(e); - } else if (keyboardMode == 'translate') { - legacyKeyboardMode(e); - } else { - legacyKeyboardMode(e); - } - - return KeyEventResult.handled; - } - - void mapKeyboardMode(RawKeyEvent e) { - int scanCode; - int keyCode; - bool down; - - if (e.data is RawKeyEventDataMacOs) { - RawKeyEventDataMacOs newData = e.data as RawKeyEventDataMacOs; - scanCode = newData.keyCode; - keyCode = newData.keyCode; - } else if (e.data is RawKeyEventDataWindows) { - RawKeyEventDataWindows newData = e.data as RawKeyEventDataWindows; - scanCode = newData.scanCode; - keyCode = newData.keyCode; - } else if (e.data is RawKeyEventDataLinux) { - RawKeyEventDataLinux newData = e.data as RawKeyEventDataLinux; - scanCode = newData.scanCode; - keyCode = newData.keyCode; - } else { - scanCode = -1; - keyCode = -1; - } - - if (e is RawKeyDownEvent) { - down = true; - } else { - down = false; - } - - _ffi.inputRawKey(e.character ?? "", keyCode, scanCode, down); - } - - void legacyKeyboardMode(RawKeyEvent e) { - final key = e.logicalKey; - if (e is RawKeyDownEvent) { - if (e.repeat) { - sendRawKey(e, press: true); - } else { - if (e.isAltPressed && !_ffi.alt) { - _ffi.alt = true; - } else if (e.isControlPressed && !_ffi.ctrl) { - _ffi.ctrl = true; - } else if (e.isShiftPressed && !_ffi.shift) { - _ffi.shift = true; - } else if (e.isMetaPressed && !_ffi.command) { - _ffi.command = true; - } - sendRawKey(e, down: true); - } - } - if (e is RawKeyUpEvent) { - if (key == LogicalKeyboardKey.altLeft || - key == LogicalKeyboardKey.altRight) { - _ffi.alt = false; - } else if (key == LogicalKeyboardKey.controlLeft || - key == LogicalKeyboardKey.controlRight) { - _ffi.ctrl = false; - } else if (key == LogicalKeyboardKey.shiftRight || - key == LogicalKeyboardKey.shiftLeft) { - _ffi.shift = false; - } else if (key == LogicalKeyboardKey.metaLeft || - key == LogicalKeyboardKey.metaRight || - key == LogicalKeyboardKey.superKey) { - _ffi.command = false; - } - sendRawKey(e); - } - } - Widget getRawPointerAndKeyBody(Widget child) { return FocusScope( autofocus: true, @@ -266,72 +179,10 @@ class _RemotePageState extends State onFocusChange: (bool v) { _imageFocused = v; }, - onKey: handleRawKeyEvent, + onKey: _keyboard_input.handleRawKeyEvent, child: child)); } - void _onPointHoverImage(PointerHoverEvent e) { - if (e.kind != ui.PointerDeviceKind.mouse) return; - if (!_isPhysicalMouse) { - setState(() { - _isPhysicalMouse = true; - }); - } - if (_isPhysicalMouse) { - _ffi.handleMouse(getEvent(e, 'mousemove'), - tabBarHeight: widget.tabBarHeight); - } - } - - void _onPointDownImage(PointerDownEvent e) { - if (e.kind != ui.PointerDeviceKind.mouse) { - if (_isPhysicalMouse) { - setState(() { - _isPhysicalMouse = false; - }); - } - } - if (_isPhysicalMouse) { - _ffi.handleMouse(getEvent(e, 'mousedown'), - tabBarHeight: widget.tabBarHeight); - } - } - - void _onPointUpImage(PointerUpEvent e) { - if (e.kind != ui.PointerDeviceKind.mouse) return; - if (_isPhysicalMouse) { - _ffi.handleMouse(getEvent(e, 'mouseup'), - tabBarHeight: widget.tabBarHeight); - } - } - - void _onPointMoveImage(PointerMoveEvent e) { - if (e.kind != ui.PointerDeviceKind.mouse) return; - if (_isPhysicalMouse) { - _ffi.handleMouse(getEvent(e, 'mousemove'), - tabBarHeight: widget.tabBarHeight); - } - } - - void _onPointerSignalImage(PointerSignalEvent e) { - if (e is PointerScrollEvent) { - var dx = e.scrollDelta.dx.toInt(); - var dy = e.scrollDelta.dy.toInt(); - if (dx > 0) { - dx = -1; - } else if (dx < 0) { - dx = 1; - } - if (dy > 0) { - dy = -1; - } else if (dy < 0) { - dy = 1; - } - bind.sessionSendMouse( - id: widget.id, msg: '{"type": "wheel", "x": "$dx", "y": "$dy"}'); - } - } - void enterView(PointerEnterEvent evt) { if (!_imageFocused) { _rawKeyFocusNode.requestFocus(); @@ -361,11 +212,11 @@ class _RemotePageState extends State Widget _buildImageListener(Widget child) { return Listener( - onPointerHover: _onPointHoverImage, - onPointerDown: _onPointDownImage, - onPointerUp: _onPointUpImage, - onPointerMove: _onPointMoveImage, - onPointerSignal: _onPointerSignalImage, + onPointerHover: _mouse_input.onPointHoverImage, + onPointerDown: _mouse_input.onPointDownImage, + onPointerUp: _mouse_input.onPointUpImage, + onPointerMove: _mouse_input.onPointMoveImage, + onPointerSignal: _mouse_input.onPointerSignalImage, child: MouseRegion(onEnter: enterView, onExit: leaveView, child: child)); } @@ -407,27 +258,6 @@ class _RemotePageState extends State ); } - int lastMouseDownButtons = 0; - - Map getEvent(PointerEvent evt, String type) { - final Map out = {}; - out['type'] = type; - out['x'] = evt.position.dx; - out['y'] = evt.position.dy; - if (_ffi.alt) out['alt'] = 'true'; - if (_ffi.shift) out['shift'] = 'true'; - if (_ffi.ctrl) out['ctrl'] = 'true'; - if (_ffi.command) out['command'] = 'true'; - out['buttons'] = evt - .buttons; // left button: 1, right button: 2, middle button: 4, 1 | 2 = 3 (left + right) - if (evt.buttons != 0) { - lastMouseDownButtons = evt.buttons; - } else { - out['buttons'] = lastMouseDownButtons; - } - return out; - } - @override bool get wantKeepAlive => true; } diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart new file mode 100644 index 000000000..f996daddf --- /dev/null +++ b/flutter/lib/models/input_model.dart @@ -0,0 +1,199 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; + +import '../../models/model.dart'; +import '../../models/platform_model.dart'; +import '../consts.dart'; +import 'dart:ui' as ui; + +class Keyboard { + late FFI _ffi; + late String _id; + String keyboardMode = "legacy"; + + Keyboard(FFI ffi, String id) { + _ffi = ffi; + _id = id; + } + + KeyEventResult handleRawKeyEvent(FocusNode data, RawKeyEvent e) { + bind.sessionGetKeyboardName(id: _id).then((result) { + keyboardMode = result.toString(); + }); + + if (keyboardMode == 'map') { + mapKeyboardMode(e); + } else if (keyboardMode == 'translate') { + legacyKeyboardMode(e); + } else { + legacyKeyboardMode(e); + } + + return KeyEventResult.handled; + } + + void mapKeyboardMode(RawKeyEvent e) { + int scanCode; + int keyCode; + bool down; + + if (e.data is RawKeyEventDataMacOs) { + RawKeyEventDataMacOs newData = e.data as RawKeyEventDataMacOs; + scanCode = newData.keyCode; + keyCode = newData.keyCode; + } else if (e.data is RawKeyEventDataWindows) { + RawKeyEventDataWindows newData = e.data as RawKeyEventDataWindows; + scanCode = newData.scanCode; + keyCode = newData.keyCode; + } else if (e.data is RawKeyEventDataLinux) { + RawKeyEventDataLinux newData = e.data as RawKeyEventDataLinux; + scanCode = newData.scanCode; + keyCode = newData.keyCode; + } else { + scanCode = -1; + keyCode = -1; + } + + if (e is RawKeyDownEvent) { + down = true; + } else { + down = false; + } + + _ffi.inputRawKey(e.character ?? "", keyCode, scanCode, down); + } + + void legacyKeyboardMode(RawKeyEvent e) { + final key = e.logicalKey; + if (e is RawKeyDownEvent) { + if (e.repeat) { + sendRawKey(e, press: true); + } else { + if (e.isAltPressed && !_ffi.alt) { + _ffi.alt = true; + } else if (e.isControlPressed && !_ffi.ctrl) { + _ffi.ctrl = true; + } else if (e.isShiftPressed && !_ffi.shift) { + _ffi.shift = true; + } else if (e.isMetaPressed && !_ffi.command) { + _ffi.command = true; + } + sendRawKey(e, down: true); + } + } + if (e is RawKeyUpEvent) { + if (key == LogicalKeyboardKey.altLeft || + key == LogicalKeyboardKey.altRight) { + _ffi.alt = false; + } else if (key == LogicalKeyboardKey.controlLeft || + key == LogicalKeyboardKey.controlRight) { + _ffi.ctrl = false; + } else if (key == LogicalKeyboardKey.shiftRight || + key == LogicalKeyboardKey.shiftLeft) { + _ffi.shift = false; + } else if (key == LogicalKeyboardKey.metaLeft || + key == LogicalKeyboardKey.metaRight || + key == LogicalKeyboardKey.superKey) { + _ffi.command = false; + } + sendRawKey(e); + } + } + + void sendRawKey(RawKeyEvent e, {bool? down, bool? press}) { + // for maximum compatibility + final label = logicalKeyMap[e.logicalKey.keyId] ?? + physicalKeyMap[e.physicalKey.usbHidUsage] ?? + e.logicalKey.keyLabel; + _ffi.inputKey(label, down: down, press: press ?? false); + } +} + +class Mouse { + var _isPhysicalMouse = false; + int _lastMouseDownButtons = 0; + + late FFI _ffi; + late String _id; + late double tabBarHeight; + + Mouse(FFI ffi, String id, double tabBarHeight_) { + _ffi = ffi; + _id = id; + tabBarHeight = tabBarHeight_; + } + + Map getEvent(PointerEvent evt, String type) { + final Map out = {}; + out['type'] = type; + out['x'] = evt.position.dx; + out['y'] = evt.position.dy; + if (_ffi.alt) out['alt'] = 'true'; + if (_ffi.shift) out['shift'] = 'true'; + if (_ffi.ctrl) out['ctrl'] = 'true'; + if (_ffi.command) out['command'] = 'true'; + out['buttons'] = evt + .buttons; // left button: 1, right button: 2, middle button: 4, 1 | 2 = 3 (left + right) + if (evt.buttons != 0) { + _lastMouseDownButtons = evt.buttons; + } else { + out['buttons'] = _lastMouseDownButtons; + } + return out; + } + + void onPointHoverImage(PointerHoverEvent e) { + if (e.kind != ui.PointerDeviceKind.mouse) return; + if (!_isPhysicalMouse) { + _isPhysicalMouse = true; + } + if (_isPhysicalMouse) { + _ffi.handleMouse(getEvent(e, 'mousemove'), tabBarHeight: tabBarHeight); + } + } + + void onPointDownImage(PointerDownEvent e) { + if (e.kind != ui.PointerDeviceKind.mouse) { + if (_isPhysicalMouse) { + _isPhysicalMouse = false; + } + } + if (_isPhysicalMouse) { + _ffi.handleMouse(getEvent(e, 'mousedown'), tabBarHeight: tabBarHeight); + } + } + + void onPointUpImage(PointerUpEvent e) { + if (e.kind != ui.PointerDeviceKind.mouse) return; + if (_isPhysicalMouse) { + _ffi.handleMouse(getEvent(e, 'mouseup'), tabBarHeight: tabBarHeight); + } + } + + void onPointMoveImage(PointerMoveEvent e) { + if (e.kind != ui.PointerDeviceKind.mouse) return; + if (_isPhysicalMouse) { + _ffi.handleMouse(getEvent(e, 'mousemove'), tabBarHeight: tabBarHeight); + } + } + + void onPointerSignalImage(PointerSignalEvent e) { + if (e is PointerScrollEvent) { + var dx = e.scrollDelta.dx.toInt(); + var dy = e.scrollDelta.dy.toInt(); + if (dx > 0) { + dx = -1; + } else if (dx < 0) { + dx = 1; + } + if (dy > 0) { + dy = -1; + } else if (dy < 0) { + dy = 1; + } + bind.sessionSendMouse( + id: _id, msg: '{"type": "wheel", "x": "$dx", "y": "$dy"}'); + } + } +} From 1e9e00ec51500728336d09d44839b1a6a1729838 Mon Sep 17 00:00:00 2001 From: csf Date: Thu, 15 Sep 2022 16:09:07 +0800 Subject: [PATCH 0478/2015] put video_handler thread & update android build --- flutter/android/app/build.gradle | 2 +- flutter/lib/main.dart | 2 +- src/client.rs | 4 +--- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/flutter/android/app/build.gradle b/flutter/android/app/build.gradle index a2a1a02a3..94fc645af 100644 --- a/flutter/android/app/build.gradle +++ b/flutter/android/app/build.gradle @@ -32,7 +32,7 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 32 + compileSdkVersion 33 sourceSets { main.java.srcDirs += 'src/main/kotlin' } diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 98ac20bfe..d5837c09c 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -271,7 +271,7 @@ _keepScaleBuilder() { } _registerEventHandler() { - if (desktopType != DesktopType.main) { + if (isDesktop && desktopType != DesktopType.main) { platformFFI.registerEventHandler('theme', 'theme', (evt) async { String? dark = evt['dark']; if (dark != null) { diff --git a/src/client.rs b/src/client.rs index cc1f78b78..baf06833a 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1415,11 +1415,9 @@ where let latency_controller = LatencyController::new(); let latency_controller_cl = latency_controller.clone(); - // Create video_handler out of the thread below to ensure that the handler exists before client start. - // It will take a few tenths of a second for the first time, and then tens of milliseconds. - let mut video_handler = VideoHandler::new(latency_controller); std::thread::spawn(move || { + let mut video_handler = VideoHandler::new(latency_controller); loop { if let Ok(data) = video_receiver.recv() { match data { From d3bc4a7dc6c1614d2752aa35fa80c10306d40ec1 Mon Sep 17 00:00:00 2001 From: csf Date: Thu, 15 Sep 2022 16:36:52 +0800 Subject: [PATCH 0479/2015] fix desktopType for mobile --- 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 aed6d6fd8..f7c423dbc 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -28,7 +28,7 @@ var isWeb = false; var isWebDesktop = false; var version = ""; int androidVersion = 0; -late final DesktopType? desktopType; +DesktopType? desktopType; typedef F = String Function(String); typedef FMethod = String Function(String, dynamic); From f4871a992fec480a501fb0c826938521192ce16d Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 15 Sep 2022 17:41:10 +0800 Subject: [PATCH 0480/2015] refactor core_main, also fix windows flutter restart repeated fatal error crash --- flutter/lib/desktop/pages/server_page.dart | 2 + flutter/lib/models/server_model.dart | 2 - flutter/pubspec.lock | 28 +-- src/common.rs | 1 - src/core_main.rs | 193 ++++++++++++++++++--- src/flutter_ffi.rs | 19 +- src/lib.rs | 13 +- src/main.rs | 179 +------------------ 8 files changed, 197 insertions(+), 240 deletions(-) diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index 62e60ba26..9ca446dcb 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -1,3 +1,5 @@ +// original cm window in Sciter version. + import 'dart:async'; import 'package:flutter/material.dart'; diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index 737d0b22b..a26fe5062 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -259,7 +259,6 @@ class ServerModel with ChangeNotifier { Future startService() async { _isStart = true; notifyListeners(); - // TODO parent.target?.ffiModel.updateEventListener(""); await parent.target?.invokeMethod("init_service"); await bind.mainStartService(); @@ -274,7 +273,6 @@ class ServerModel with ChangeNotifier { /// Stop the screen sharing service. Future stopService() async { _isStart = false; - // TODO closeAll(); await parent.target?.invokeMethod("stop_service"); await bind.mainStopService(); diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 251d3dacb..dbdf1637b 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -49,7 +49,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.8.2" + version: "2.9.0" back_button_interceptor: dependency: "direct main" description: @@ -147,7 +147,7 @@ packages: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.2.1" charcode: dependency: transitive description: @@ -168,7 +168,7 @@ packages: name: clock url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.1.1" code_builder: dependency: transitive description: @@ -324,7 +324,7 @@ packages: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.3.1" ffi: dependency: "direct main" description: @@ -418,8 +418,8 @@ packages: dependency: "direct main" description: path: "." - ref: "9021e21de36c84edf01d5034f38eda580463163b" - resolved-ref: "9021e21de36c84edf01d5034f38eda580463163b" + ref: "47179378523c993092f70d95f93d53f40af01f02" + resolved-ref: "47179378523c993092f70d95f93d53f40af01f02" url: "https://github.com/Kingtous/rustdesk_flutter_custom_cursor" source: git version: "0.0.1" @@ -630,14 +630,14 @@ packages: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.11" + version: "0.12.12" material_color_utilities: dependency: transitive description: name: material_color_utilities url: "https://pub.dartlang.org" source: hosted - version: "0.1.4" + version: "0.1.5" menu_base: dependency: transitive description: @@ -651,7 +651,7 @@ packages: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.7.0" + version: "1.8.0" mime: dependency: transitive description: @@ -728,7 +728,7 @@ packages: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.1" + version: "1.8.2" path_provider: dependency: "direct main" description: @@ -980,7 +980,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.2" + version: "1.9.0" sqflite: dependency: transitive description: @@ -1022,7 +1022,7 @@ packages: name: string_scanner url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.1.1" synchronized: dependency: transitive description: @@ -1036,14 +1036,14 @@ packages: name: term_glyph url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.2.1" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.9" + version: "0.4.12" timing: dependency: transitive description: diff --git a/src/common.rs b/src/common.rs index d5ccf0e1b..37057b809 100644 --- a/src/common.rs +++ b/src/common.rs @@ -2,7 +2,6 @@ use std::sync::{Arc, Mutex}; #[cfg(not(any(target_os = "android", target_os = "ios")))] pub use arboard::Clipboard as ClipboardContext; -use serde_json::json; use hbb_common::{ allow_err, diff --git a/src/core_main.rs b/src/core_main.rs index cecbf4115..3c1d858fb 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -1,39 +1,188 @@ use hbb_common::log; -use crate::{flutter::connection_manager, start_os_service, start_server}; - -/// Main entry of the RustDesk Core. -/// Return true if the app should continue running with UI(possibly Flutter), false if the app should exit. -pub fn core_main() -> bool { - let args = std::env::args().collect::>(); - // TODO: implement core_main() - if args.len() > 1 { - if args[1] == "--cm" { - // call connection manager to establish connections - // meanwhile, return true to call flutter window to show control panel - connection_manager::start_listen_ipc_thread(); - return true; +// shared by flutter and sciter main function +pub fn core_main() -> Option> { + // https://docs.rs/flexi_logger/latest/flexi_logger/error_info/index.html#write + // though async logger more efficient, but it also causes more problems, disable it for now + // let mut _async_logger_holder: Option = None; + let mut args = Vec::new(); + let mut i = 0; + let mut is_setup = false; + for arg in std::env::args() { + // to-do: how to pass to flutter? + if i == 0 && crate::common::is_setup(&arg) { + is_setup = true; + } else if i > 0 { + args.push(arg); } - + i += 1; + } + if is_setup { + if args.is_empty() { + args.push("--install".to_owned()); + } else if args[0] == "--noinstall" { + args.clear(); + } + } + if args.len() > 0 && args[0] == "--version" { + println!("{}", crate::VERSION); + return None; + } + #[cfg(debug_assertions)] + { use hbb_common::env_logger::*; init_from_env(Env::default().filter_or(DEFAULT_FILTER_ENV, "info")); - if args[1] == "--service" { - log::info!("start --service"); - start_os_service(); - return false; + } + #[cfg(not(debug_assertions))] + { + let mut path = hbb_common::config::Config::log_path(); + if args.len() > 0 && args[0].starts_with("--") { + let name = args[0].replace("--", ""); + if !name.is_empty() { + path.push(name); + } } - if args[1] == "--server" { + use flexi_logger::*; + if let Ok(x) = Logger::try_with_env_or_str("debug") { + // _async_logger_holder = + x.log_to_file(FileSpec::default().directory(path)) + //.write_mode(WriteMode::Async) + .format(opt_format) + .rotate( + Criterion::Age(Age::Day), + Naming::Timestamps, + Cleanup::KeepLogFiles(6), + ) + .start() + .ok(); + } + } + if args.is_empty() { + std::thread::spawn(move || crate::start_server(false)); + } else { + #[cfg(windows)] + { + use crate::platform; + if args[0] == "--uninstall" { + if let Err(err) = platform::uninstall_me() { + log::error!("Failed to uninstall: {}", err); + } + return None; + } else if args[0] == "--after-install" { + if let Err(err) = platform::run_after_install() { + log::error!("Failed to after-install: {}", err); + } + return None; + } else if args[0] == "--before-uninstall" { + if let Err(err) = platform::run_before_uninstall() { + log::error!("Failed to before-uninstall: {}", err); + } + return None; + } else if args[0] == "--update" { + hbb_common::allow_err!(platform::update_me()); + return None; + } else if args[0] == "--reinstall" { + hbb_common::allow_err!(platform::uninstall_me()); + hbb_common::allow_err!(platform::install_me( + "desktopicon startmenu", + "".to_owned(), + false, + false, + )); + return None; + } else if args[0] == "--silent-install" { + hbb_common::allow_err!(platform::install_me( + "desktopicon startmenu", + "".to_owned(), + true, + args.len() > 1, + )); + return None; + } else if args[0] == "--extract" { + #[cfg(feature = "with_rc")] + hbb_common::allow_err!(crate::rc::extract_resources(&args[1])); + return None; + } + } + if args[0] == "--remove" { + if args.len() == 2 { + // sleep a while so that process of removed exe exit + std::thread::sleep(std::time::Duration::from_secs(1)); + std::fs::remove_file(&args[1]).ok(); + 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"); #[cfg(not(target_os = "macos"))] { - start_server(true); + crate::start_server(true); + return None; } #[cfg(target_os = "macos")] { std::thread::spawn(move || start_server(true)); + // to-do: for flutter, starting tray not ready yet, or we can reuse sciter's tray implementation. } - return false; + } else if args[0] == "--import-config" { + if args.len() == 2 { + let filepath; + let path = std::path::Path::new(&args[1]); + if !path.is_absolute() { + let mut cur = std::env::current_dir().unwrap(); + cur.push(path); + filepath = cur.to_str().unwrap().to_string(); + } else { + filepath = path.to_str().unwrap().to_string(); + } + import_config(&filepath); + } + return None; + } else if args[0] == "--password" { + if args.len() == 2 { + crate::ipc::set_permanent_password(args[1].to_owned()).unwrap(); + } + return None; + } else if args[0] == "--check-hwcodec-config" { + #[cfg(feature = "hwcodec")] + scrap::hwcodec::check_config(); + return None; + } 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(); + } + } + //_async_logger_holder.map(|x| x.flush()); + Some(args) +} + +fn import_config(path: &str) { + use hbb_common::{config::*, get_exe_time, get_modified_time}; + let path2 = path.replace(".toml", "2.toml"); + let path2 = std::path::Path::new(&path2); + let path = std::path::Path::new(path); + log::info!("import config from {:?} and {:?}", path, path2); + let config: Config = load_path(path.into()); + if config.is_empty() { + log::info!("Empty source config, skipped"); + return; + } + if get_modified_time(&path) > get_modified_time(&Config::file()) + && get_modified_time(&path) < get_exe_time() + { + if store_path(Config::file(), config).is_err() { + log::info!("config written"); + } + } + let config2: Config2 = load_path(path2.into()); + if get_modified_time(&path2) > get_modified_time(&Config2::file()) { + if store_path(Config2::file(), config2).is_err() { + log::info!("config2 written"); } } - true } diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 568096b1c..6f73aa768 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -33,14 +33,6 @@ use crate::{ fn initialize(app_dir: &str) { *config::APP_DIR.write().unwrap() = app_dir.to_owned(); - #[cfg(feature = "cli")] - { - #[cfg(any(target_os = "android", target_os = "ios"))] - { - crate::common::test_rendezvous_server(); - crate::common::test_nat_type(); - } - } #[cfg(target_os = "android")] { android_logger::init_once( @@ -58,13 +50,6 @@ fn initialize(app_dir: &str) { { crate::common::check_software_update(); } - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] - { - use hbb_common::env_logger::*; - if let Err(e) = try_init_from_env(Env::default().filter_or(DEFAULT_FILTER_ENV, "debug")) { - log::debug!("{}", e); - } - } } /// FFI for rustdesk core's main entry. @@ -72,7 +57,7 @@ fn initialize(app_dir: &str) { #[no_mangle] pub extern "C" fn rustdesk_core_main() -> bool { #[cfg(not(any(target_os = "android", target_os = "ios")))] - return crate::core_main::core_main(); + return crate::core_main::core_main().is_some(); #[cfg(any(target_os = "android", target_os = "ios"))] false } @@ -843,8 +828,6 @@ pub fn main_start_service() { config::Config::set_option("stop-service".into(), "".into()); crate::rendezvous_mediator::RendezvousMediator::restart(); } - #[cfg(not(target_os = "android"))] - std::thread::spawn(move || start_server(true)); } pub fn main_update_temporary_password() { diff --git a/src/lib.rs b/src/lib.rs index b427c3301..a5041e9f8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,10 +10,10 @@ mod server; pub use self::server::*; mod client; #[cfg(not(any(target_os = "ios")))] -mod rendezvous_mediator; -#[cfg(not(any(target_os = "ios")))] mod lan; #[cfg(not(any(target_os = "ios")))] +mod rendezvous_mediator; +#[cfg(not(any(target_os = "ios")))] pub use self::rendezvous_mediator::*; /// cbindgen:ignore pub mod common; @@ -30,13 +30,10 @@ pub mod flutter; #[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] pub mod flutter_ffi; use common::*; -#[cfg(all( - not(any(target_os = "android", target_os = "ios")), - feature = "flutter" -))] -pub mod core_main; #[cfg(feature = "cli")] pub mod cli; +#[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] +pub mod core_main; #[cfg(all(windows, feature = "hbbs"))] mod hbbs; mod lang; @@ -47,9 +44,9 @@ mod port_forward; #[cfg(windows)] mod tray; +mod ui_cm_interface; mod ui_interface; mod ui_session_interface; -mod ui_cm_interface; #[cfg(windows)] pub mod clipboard_file; diff --git a/src/main.rs b/src/main.rs index 0acdde68f..23559ed8a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,6 @@ // Requires Rust 1.18. //#![windows_subsystem = "windows"] -use hbb_common::log; use librustdesk::*; #[cfg(any(target_os = "android", target_os = "ios"))] @@ -15,184 +14,14 @@ fn main() { #[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] fn main() { - // https://docs.rs/flexi_logger/latest/flexi_logger/error_info/index.html#write - let mut _async_logger_holder: Option = None; - let mut args = Vec::new(); - let mut i = 0; - let mut is_setup = false; - for arg in std::env::args() { - if i == 0 && common::is_setup(&arg) { - is_setup = true; - } else if i > 0 { - args.push(arg); - } - i += 1; - } - if is_setup { - if args.is_empty() { - args.push("--install".to_owned()); - } else if args[0] == "--noinstall" { - args.clear(); - } - } - if args.len() > 0 && args[0] == "--version" { - println!("{}", crate::VERSION); - return; - } - #[cfg(not(feature = "inline"))] - { - use hbb_common::env_logger::*; - init_from_env(Env::default().filter_or(DEFAULT_FILTER_ENV, "info")); - } - #[cfg(feature = "inline")] - { - let mut path = hbb_common::config::Config::log_path(); - if args.len() > 0 && args[0].starts_with("--") { - let name = args[0].replace("--", ""); - if !name.is_empty() { - path.push(name); - } - } - use flexi_logger::*; - if let Ok(x) = Logger::try_with_env_or_str("debug") { - _async_logger_holder = x - .log_to_file(FileSpec::default().directory(path)) - .write_mode(WriteMode::Async) - .format(opt_format) - .rotate( - Criterion::Age(Age::Day), - Naming::Timestamps, - Cleanup::KeepLogFiles(6), - ) - .start() - .ok(); - } - } - if args.is_empty() { - std::thread::spawn(move || start_server(false)); - } else { - #[cfg(windows)] - { - if args[0] == "--uninstall" { - if let Err(err) = platform::uninstall_me() { - log::error!("Failed to uninstall: {}", err); - } - return; - } else if args[0] == "--after-install" { - if let Err(err) = platform::run_after_install() { - log::error!("Failed to after-install: {}", err); - } - return; - } else if args[0] == "--before-uninstall" { - if let Err(err) = platform::run_before_uninstall() { - log::error!("Failed to before-uninstall: {}", err); - } - return; - } else if args[0] == "--update" { - hbb_common::allow_err!(platform::update_me()); - return; - } else if args[0] == "--reinstall" { - hbb_common::allow_err!(platform::uninstall_me()); - hbb_common::allow_err!(platform::install_me( - "desktopicon startmenu", - "".to_owned(), - false, - false, - )); - return; - } else if args[0] == "--silent-install" { - hbb_common::allow_err!(platform::install_me( - "desktopicon startmenu", - "".to_owned(), - true, - args.len() > 1, - )); - return; - } else if args[0] == "--extract" { - #[cfg(feature = "with_rc")] - hbb_common::allow_err!(crate::rc::extract_resources(&args[1])); - return; - } - } - if args[0] == "--remove" { - if args.len() == 2 { - // sleep a while so that process of removed exe exit - std::thread::sleep(std::time::Duration::from_secs(1)); - std::fs::remove_file(&args[1]).ok(); - return; - } - } else if args[0] == "--service" { - log::info!("start --service"); - start_os_service(); - return; - } else if args[0] == "--server" { - log::info!("start --server"); - #[cfg(not(target_os = "macos"))] - { - start_server(true); - return; - } - #[cfg(target_os = "macos")] - { - std::thread::spawn(move || start_server(true)); - } - } else if args[0] == "--import-config" { - if args.len() == 2 { - let filepath; - let path = std::path::Path::new(&args[1]); - if !path.is_absolute() { - let mut cur = std::env::current_dir().unwrap(); - cur.push(path); - filepath = cur.to_str().unwrap().to_string(); - } else { - filepath = path.to_str().unwrap().to_string(); - } - import_config(&filepath); - } - return; - } else if args[0] == "--password" { - if args.len() == 2 { - ipc::set_permanent_password(args[1].to_owned()).unwrap(); - } - return; - } else if args[0] == "--check-hwcodec-config" { - #[cfg(feature = "hwcodec")] - scrap::hwcodec::check_config(); - return; - } - } - ui::start(&mut args[..]); - _async_logger_holder.map(|x| x.flush()); -} - -fn import_config(path: &str) { - use hbb_common::{config::*, get_exe_time, get_modified_time}; - let path2 = path.replace(".toml", "2.toml"); - let path2 = std::path::Path::new(&path2); - let path = std::path::Path::new(path); - log::info!("import config from {:?} and {:?}", path, path2); - let config: Config = load_path(path.into()); - if config.is_empty() { - log::info!("Empty source config, skipped"); - return; - } - if get_modified_time(&path) > get_modified_time(&Config::file()) - && get_modified_time(&path) < get_exe_time() - { - if store_path(Config::file(), config).is_err() { - log::info!("config written"); - } - } - let config2: Config2 = load_path(path2.into()); - if get_modified_time(&path2) > get_modified_time(&Config2::file()) { - if store_path(Config2::file(), config2).is_err() { - log::info!("config2 written"); - } + if let Some(args) = crate::core_main::core_main().as_mut() { + ui::start(args); } } #[cfg(feature = "cli")] fn main() { + use hbb_common::log; use clap::App; let args = format!( "-p, --port-forward=[PORT-FORWARD-OPTIONS] 'Format: remote-id:local-port:remote-port[:remote-host]' @@ -235,4 +64,4 @@ fn main() { let token = LocalConfig::get_option("access_token"); cli::start_one_port_forward(options[0].clone(), port, remote_host, remote_port, key, token); } -} +} \ No newline at end of file From a73fab575a5130e9c2427c91d93a5923dad66c4b Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 15 Sep 2022 18:27:10 +0800 Subject: [PATCH 0481/2015] run.sh --- flutter/run.sh | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 flutter/run.sh diff --git a/flutter/run.sh b/flutter/run.sh new file mode 100644 index 000000000..cb6e0f9cb --- /dev/null +++ b/flutter/run.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +dart pub global activate ffigen --version 5.0.1 +flutter pub get +# call `flutter clean` if cargo build fails +cargo build --features flutter +flutter run $@ From f310251cfc23266702bcc558d0f746cbf3e05f04 Mon Sep 17 00:00:00 2001 From: csf Date: Thu, 15 Sep 2022 20:40:29 +0800 Subject: [PATCH 0482/2015] feat mediacodec: Android H264/H265 decoder support --- Cargo.lock | 35 +++++- Cargo.toml | 1 + libs/scrap/Cargo.toml | 2 + libs/scrap/src/common/codec.rs | 65 ++++++++-- libs/scrap/src/common/mediacodec.rs | 178 +++++++++++----------------- libs/scrap/src/common/mod.rs | 2 + src/flutter_ffi.rs | 2 + 7 files changed, 168 insertions(+), 117 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e4b5d2792..c4941a1b7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3007,6 +3007,20 @@ dependencies = [ "thiserror", ] +[[package]] +name = "ndk" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "451422b7e4718271c8b5b3aadf5adedba43dc76312454b387e98fae0fc951aa0" +dependencies = [ + "bitflags", + "jni-sys", + "ndk-sys 0.4.0", + "num_enum", + "raw-window-handle 0.5.0", + "thiserror", +] + [[package]] name = "ndk-context" version = "0.1.1" @@ -3071,6 +3085,15 @@ dependencies = [ "jni-sys", ] +[[package]] +name = "ndk-sys" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21d83ec9c63ec5bf950200a8e508bdad6659972187b625469f58ef8c08e29046" +dependencies = [ + "jni-sys", +] + [[package]] name = "net2" version = "0.2.37" @@ -3948,6 +3971,15 @@ dependencies = [ "cty", ] +[[package]] +name = "raw-window-handle" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed7e3d950b66e19e0c372f3fa3fbbcf85b1746b571f74e0c2af6042a5c93420a" +dependencies = [ + "cty", +] + [[package]] name = "rayon" version = "1.5.3" @@ -4412,6 +4444,7 @@ dependencies = [ "lazy_static", "libc", "log", + "ndk 0.7.0", "num_cpus", "quest", "repng", @@ -5857,7 +5890,7 @@ dependencies = [ "objc", "parking_lot 0.11.2", "percent-encoding", - "raw-window-handle", + "raw-window-handle 0.4.3", "smithay-client-toolkit", "wasm-bindgen", "wayland-client", diff --git a/Cargo.toml b/Cargo.toml index f3a0377d4..c65e73d82 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ use_dasp = ["dasp"] flutter = ["flutter_rust_bridge"] default = ["use_dasp"] hwcodec = ["scrap/hwcodec"] +mediacodec = ["scrap/mediacodec"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/libs/scrap/Cargo.toml b/libs/scrap/Cargo.toml index d40eb0cfd..c980d9d49 100644 --- a/libs/scrap/Cargo.toml +++ b/libs/scrap/Cargo.toml @@ -11,6 +11,7 @@ edition = "2018" [features] wayland = ["gstreamer", "gstreamer-app", "gstreamer-video", "dbus", "tracing"] +mediacodec = ["ndk"] [dependencies] block = "0.1" @@ -31,6 +32,7 @@ jni = "0.19" lazy_static = "1.4" log = "0.4" serde_json = "1.0" +ndk = { version = "0.7", features = ["media"], optional = true} [target.'cfg(not(target_os = "android"))'.dev-dependencies] repng = "0.2" diff --git a/libs/scrap/src/common/codec.rs b/libs/scrap/src/common/codec.rs index f0bd1c5f7..d729342d6 100644 --- a/libs/scrap/src/common/codec.rs +++ b/libs/scrap/src/common/codec.rs @@ -7,6 +7,10 @@ use std::{ #[cfg(feature = "hwcodec")] use crate::hwcodec::*; +#[cfg(feature = "mediacodec")] +use crate::mediacodec::{ + MediaCodecDecoder, MediaCodecDecoders, H264_DECODER_SUPPORT, H265_DECODER_SUPPORT, +}; use crate::vpxcodec::*; use hbb_common::{ @@ -15,7 +19,7 @@ use hbb_common::{ message_proto::{video_frame, EncodedVideoFrames, Message, VideoCodecState}, ResultType, }; -#[cfg(feature = "hwcodec")] +#[cfg(any(feature = "hwcodec", feature = "mediacodec"))] use hbb_common::{ config::{Config2, PeerConfig}, lazy_static, @@ -82,6 +86,8 @@ pub struct Decoder { hw: HwDecoders, #[cfg(feature = "hwcodec")] i420: Vec, + #[cfg(feature = "mediacodec")] + media_codec: MediaCodecDecoders, } #[derive(Debug, Clone)] @@ -242,20 +248,34 @@ impl Decoder { #[cfg(feature = "hwcodec")] if check_hwcodec_config() { let best = HwDecoder::best(); - VideoCodecState { + return VideoCodecState { score_vpx: SCORE_VPX, score_h264: best.h264.map_or(0, |c| c.score), score_h265: best.h265.map_or(0, |c| c.score), perfer: Self::codec_preference(_id).into(), ..Default::default() - } - } else { + }; + } + #[cfg(feature = "mediacodec")] + if check_hwcodec_config() { + let score_h264 = if H264_DECODER_SUPPORT.load(std::sync::atomic::Ordering::SeqCst) { + 92 + } else { + 0 + }; + let score_h265 = if H265_DECODER_SUPPORT.load(std::sync::atomic::Ordering::SeqCst) { + 94 + } else { + 0 + }; return VideoCodecState { score_vpx: SCORE_VPX, + score_h264, + score_h265, + perfer: Self::codec_preference(_id).into(), ..Default::default() }; } - #[cfg(not(feature = "hwcodec"))] VideoCodecState { score_vpx: SCORE_VPX, ..Default::default() @@ -270,6 +290,8 @@ impl Decoder { hw: HwDecoder::new_decoders(), #[cfg(feature = "hwcodec")] i420: vec![], + #[cfg(feature = "mediacodec")] + media_codec: MediaCodecDecoder::new_decoders(), } } @@ -298,6 +320,22 @@ impl Decoder { Err(anyhow!("don't support h265!")) } } + #[cfg(feature = "mediacodec")] + video_frame::Union::H264s(h264s) => { + if let Some(decoder) = &mut self.media_codec.h264 { + Decoder::handle_mediacodec_video_frame(decoder, h264s, rgb) + } else { + Err(anyhow!("don't support h264!")) + } + } + #[cfg(feature = "mediacodec")] + video_frame::Union::H265s(h265s) => { + if let Some(decoder) = &mut self.media_codec.h265 { + Decoder::handle_mediacodec_video_frame(decoder, h265s, rgb) + } else { + Err(anyhow!("don't support h265!")) + } + } _ => Err(anyhow!("unsupported video frame type!")), } } @@ -345,7 +383,20 @@ impl Decoder { return Ok(ret); } - #[cfg(feature = "hwcodec")] + #[cfg(feature = "mediacodec")] + fn handle_mediacodec_video_frame( + decoder: &mut MediaCodecDecoder, + frames: &EncodedVideoFrames, + rgb: &mut Vec, + ) -> ResultType { + let mut ret = false; + for h264 in frames.frames.iter() { + return decoder.decode(&h264.data, rgb); + } + return Ok(false); + } + + #[cfg(any(feature = "hwcodec", feature = "mediacodec"))] fn codec_preference(id: &str) -> PerferCodec { let codec = PeerConfig::load(id) .options @@ -363,7 +414,7 @@ impl Decoder { } } -#[cfg(feature = "hwcodec")] +#[cfg(any(feature = "hwcodec", feature = "mediacodec"))] fn check_hwcodec_config() -> bool { if let Some(v) = Config2::get().options.get("enable-hwcodec") { return v != "N"; diff --git a/libs/scrap/src/common/mediacodec.rs b/libs/scrap/src/common/mediacodec.rs index 6a0282b49..fa821246c 100644 --- a/libs/scrap/src/common/mediacodec.rs +++ b/libs/scrap/src/common/mediacodec.rs @@ -1,50 +1,40 @@ -use std::{io::Write, time::Duration}; - +use hbb_common::anyhow::Error; use hbb_common::{bail, ResultType}; -#[cfg(target_os = "android")] use ndk::media::media_codec::{MediaCodec, MediaCodecDirection, MediaFormat}; +use std::ops::Deref; +use std::{ + io::Write, + sync::atomic::{AtomicBool, Ordering}, + time::Duration, +}; use crate::{ codec::{EncoderApi, EncoderCfg}, I420ToARGB, }; -pub struct MediaCodecEncoder { - encoder: MediaCodec, -} +/// MediaCodec mime type name +const H264_MIME_TYPE: &str = "video/avc"; +const H265_MIME_TYPE: &str = "video/hevc"; +// const VP8_MIME_TYPE: &str = "video/x-vnd.on2.vp8"; +// const VP9_MIME_TYPE: &str = "video/x-vnd.on2.vp9"; -impl EncoderApi for MediaCodecEncoder { - fn new(cfg: EncoderCfg) -> ResultType - where - Self: Sized, - { - if let EncoderCfg::HW(cfg) = cfg { - create_media_codec(&cfg.codec_name, MediaCodecDirection::Encoder) - } else { - bail!("encoder type mismatch") - } - } +// TODO MediaCodecEncoder - fn encode_to_message( - &mut self, - frame: &[u8], - ms: i64, - ) -> ResultType { - todo!() - } - - fn use_yuv(&self) -> bool { - todo!() - } - - fn set_bitrate(&mut self, bitrate: u32) -> ResultType<()> { - todo!() - } -} +pub static H264_DECODER_SUPPORT: AtomicBool = AtomicBool::new(false); +pub static H265_DECODER_SUPPORT: AtomicBool = AtomicBool::new(false); pub struct MediaCodecDecoder { decoder: MediaCodec, - // pub info: CodecInfo, + name: String, +} + +impl Deref for MediaCodecDecoder { + type Target = MediaCodec; + + fn deref(&self) -> &Self::Target { + &self.decoder + } } pub struct MediaCodecDecoders { @@ -52,85 +42,50 @@ pub struct MediaCodecDecoders { pub h265: Option, } -// "video/x-vnd.on2.vp8" - VP8 video (i.e. video in .webm) -// "video/x-vnd.on2.vp9" - VP9 video (i.e. video in .webm) -// "video/avc" - H.264/AVC video -// "video/hevc" - H.265/HEVC video - impl MediaCodecDecoder { pub fn new_decoders() -> MediaCodecDecoders { - // 直接生成 h264 和 h265 - // 264 - let h264 = create_media_codec("video/avc", MediaCodecDirection::Decoder) - .map(|decoder| MediaCodecDecoder { decoder }); - let h265 = create_media_codec("video/hevc", MediaCodecDirection::Decoder) - .map(|decoder| MediaCodecDecoder { decoder }); - + let h264 = create_media_codec(H264_MIME_TYPE, MediaCodecDirection::Decoder); + let h265 = create_media_codec(H265_MIME_TYPE, MediaCodecDirection::Decoder); MediaCodecDecoders { h264, h265 } } pub fn decode(&mut self, data: &[u8], rgb: &mut Vec) -> ResultType { - log::debug!("start dequeue_input"); - - match self - .decoder - .dequeue_input_buffer(Duration::from_millis(10)) - .unwrap() - { + match self.dequeue_input_buffer(Duration::from_millis(10))? { Some(mut input_buffer) => { let mut buf = input_buffer.buffer_mut(); - log::debug!( - "dequeue_input success:buf ptr:{:?},len:{}", - buf.as_ptr(), - buf.len() - ); if data.len() > buf.len() { - log::error!("break! res.len()>buf.len()"); - bail!("break! res.len()>buf.len()"); + log::error!("Failed to decode, the input data size is bigger than input buf"); + bail!("The input data size is bigger than input buf"); } - buf.write_all(&data).unwrap(); - if let Err(e) = self - .decoder - .queue_input_buffer(input_buffer, 0, data.len(), 0, 0) - { - log::debug!("debug queue_input_buffer:{:?}", e); - }; + buf.write_all(&data)?; + self.queue_input_buffer(input_buffer, 0, data.len(), 0, 0)?; } None => { - log::debug!("dequeue_input_buffer fail :None"); + log::debug!("Failed to dequeue_input_buffer: No available input_buffer"); } }; - return match self - .decoder - .dequeue_output_buffer(Duration::from_millis(100)) - { - Ok(Some(output_buffer)) => { - log::debug!("dequeue_output success"); - // let res_format = output_buffer.format(); - let res_format = self.decoder.output_format(); - log::debug!("res_format:{:?}", res_format.str("mime")); - log::debug!("res_color:{:?}", res_format.i32("color-format")); - log::debug!("stride:{:?}", res_format.i32("stride")); - let w = res_format.i32("width").unwrap() as usize; - let h = res_format.i32("height").unwrap() as usize; - let stride = res_format.i32("stride").unwrap(); // todo - - // let w = 1920; - // let h = 1080; - // let stride = 1920; // todo - + return match self.dequeue_output_buffer(Duration::from_millis(100))? { + Some(output_buffer) => { + let res_format = self.output_format(); + let w = res_format + .i32("width") + .ok_or(Error::msg("Failed to dequeue_output_buffer, width is None"))? + as usize; + let h = res_format.i32("height").ok_or(Error::msg( + "Failed to dequeue_output_buffer, height is None", + ))? as usize; + let stride = res_format.i32("stride").ok_or(Error::msg( + "Failed to dequeue_output_buffer, stride is None", + ))?; let buf = output_buffer.buffer(); - log::debug!("output_buffer ptr:{:?} len:{}", buf.as_ptr(), buf.len()); let bps = 4; let u = buf.len() * 2 / 3; let v = buf.len() * 5 / 6; rgb.resize(h * w * bps, 0); - log::debug!("start I420ToARGB,u:{},v:{},w:{},h:{}", u, v, w, h); let y_ptr = buf.as_ptr(); let u_ptr = buf[u..].as_ptr(); let v_ptr = buf[v..].as_ptr(); - log::debug!("ptr,y:{:?},u:{:?},v:{:?}", y_ptr, u_ptr, v_ptr); unsafe { I420ToARGB( y_ptr, @@ -145,43 +100,48 @@ impl MediaCodecDecoder { h as _, ); } - log::debug!("end I420ToARGB"); - log::debug!("release_output_buffer"); - self.decoder - .release_output_buffer(output_buffer, false) - .unwrap(); - log::debug!("return true"); + self.release_output_buffer(output_buffer, false)?; Ok(true) } - Ok(None) => { - log::debug!("dequeue_output fail :None"); - Ok(false) - } - Err(e) => { - log::debug!("dequeue_output fail :error:{:?}", e); + None => { + log::debug!("Failed to dequeue_output: No available dequeue_output"); Ok(false) } }; } } -fn create_media_codec(name: &str, direction: MediaCodecDirection) -> Option { - let codec = MediaCodec::from_decoder_type(name).unwrap(); - log::debug!("start init"); +fn create_media_codec(name: &str, direction: MediaCodecDirection) -> Option { + let codec = MediaCodec::from_decoder_type(name)?; let media_format = MediaFormat::new(); media_format.set_str("mime", name); media_format.set_i32("width", 0); media_format.set_i32("height", 0); media_format.set_i32("color-format", 19); // COLOR_FormatYUV420Planar if let Err(e) = codec.configure(&media_format, None, direction) { - log::error!("failed to decoder.init:{:?}", e); + log::error!("Failed to init decoder:{:?}", e); return None; }; log::error!("decoder init success"); if let Err(e) = codec.start() { - log::error!("failed to decoder.start:{:?}", e); + log::error!("Failed to start decoder:{:?}", e); return None; }; - log::debug!("init decoder successed!:{:?}", name); - return Some(codec); + log::debug!("Init decoder successed!: {:?}", name); + return Some(MediaCodecDecoder { + decoder: codec, + name: name.to_owned(), + }); +} + +pub fn check_mediacodec() { + std::thread::spawn(move || { + // check decoders + let decoders = MediaCodecDecoder::new_decoders(); + H264_DECODER_SUPPORT.swap(decoders.h264.is_some(), Ordering::SeqCst); + H265_DECODER_SUPPORT.swap(decoders.h265.is_some(), Ordering::SeqCst); + decoders.h264.map(|d| d.stop()); + decoders.h265.map(|d| d.stop()); + // TODO encoders + }); } diff --git a/libs/scrap/src/common/mod.rs b/libs/scrap/src/common/mod.rs index 8ee22ada6..78ea7c888 100644 --- a/libs/scrap/src/common/mod.rs +++ b/libs/scrap/src/common/mod.rs @@ -32,6 +32,8 @@ pub mod codec; mod convert; #[cfg(feature = "hwcodec")] pub mod hwcodec; +#[cfg(feature = "mediacodec")] +pub mod mediacodec; pub mod vpxcodec; pub use self::convert::*; pub const STRIDE_ALIGN: usize = 64; // commonly used in libvpx vpx_img_alloc caller diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index c3b71e4c9..bc082aedf 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -49,6 +49,8 @@ fn initialize(app_dir: &str) { .with_min_level(log::Level::Debug) // limit log level .with_tag("ffi"), // logs will show under mytag tag ); + #[cfg(feature = "mediacodec")] + scrap::mediacodec::check_mediacodec(); } #[cfg(target_os = "ios")] { From ac7a8cfc2dd366ec760cf05641c464827055fb77 Mon Sep 17 00:00:00 2001 From: meisenger <51264035+meisenger@users.noreply.github.com> Date: Thu, 15 Sep 2022 20:38:03 +0500 Subject: [PATCH 0483/2015] Kazakh language name correction --- src/lang.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang.rs b/src/lang.rs index 7ac893ca8..4da3bf5d3 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -46,7 +46,7 @@ lazy_static::lazy_static! { ("pl", "Polski"), ("ja", "日本語"), ("ko", "한국어"), - ("kz", "Қазақша"), + ("kz", "Қазақ"), ]); } From ccdd01eed051b83ccf02de70eebe9e1fee22d432 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 16 Sep 2022 00:32:34 +0800 Subject: [PATCH 0484/2015] fix mac flutter build issue, ipc not working yet --- flutter/macos/Podfile.lock | 134 +----------------- .../macos/rustdesk.xcodeproj/project.pbxproj | 1 - flutter/pubspec.lock | 64 ++------- flutter/pubspec.yaml | 6 +- flutter/run.sh | 1 + src/core_main.rs | 2 +- 6 files changed, 22 insertions(+), 186 deletions(-) diff --git a/flutter/macos/Podfile.lock b/flutter/macos/Podfile.lock index e417eb188..812fbf8b3 100644 --- a/flutter/macos/Podfile.lock +++ b/flutter/macos/Podfile.lock @@ -5,112 +5,16 @@ PODS: - FlutterMacOS - device_info_plus_macos (0.0.1): - FlutterMacOS - - Firebase/Analytics (9.4.0): - - Firebase/Core - - Firebase/Core (9.4.0): - - Firebase/CoreOnly - - FirebaseAnalytics (~> 9.4.0) - - Firebase/CoreOnly (9.4.0): - - FirebaseCore (= 9.4.0) - - firebase_analytics (9.3.3): - - Firebase/Analytics (= 9.4.0) - - firebase_core + - flutter_custom_cursor (0.0.1): - FlutterMacOS - - firebase_core (1.21.1): - - Firebase/CoreOnly (~> 9.4.0) - - FlutterMacOS - - FirebaseAnalytics (9.4.0): - - FirebaseAnalytics/AdIdSupport (= 9.4.0) - - FirebaseCore (~> 9.0) - - FirebaseInstallations (~> 9.0) - - GoogleUtilities/AppDelegateSwizzler (~> 7.7) - - GoogleUtilities/MethodSwizzler (~> 7.7) - - GoogleUtilities/Network (~> 7.7) - - "GoogleUtilities/NSData+zlib (~> 7.7)" - - nanopb (< 2.30910.0, >= 2.30908.0) - - FirebaseAnalytics/AdIdSupport (9.4.0): - - FirebaseCore (~> 9.0) - - FirebaseInstallations (~> 9.0) - - GoogleAppMeasurement (= 9.4.0) - - GoogleUtilities/AppDelegateSwizzler (~> 7.7) - - GoogleUtilities/MethodSwizzler (~> 7.7) - - GoogleUtilities/Network (~> 7.7) - - "GoogleUtilities/NSData+zlib (~> 7.7)" - - nanopb (< 2.30910.0, >= 2.30908.0) - - FirebaseCore (9.4.0): - - FirebaseCoreDiagnostics (~> 9.0) - - FirebaseCoreInternal (~> 9.0) - - GoogleUtilities/Environment (~> 7.7) - - GoogleUtilities/Logger (~> 7.7) - - FirebaseCoreDiagnostics (9.5.0): - - GoogleDataTransport (< 10.0.0, >= 9.1.4) - - GoogleUtilities/Environment (~> 7.7) - - GoogleUtilities/Logger (~> 7.7) - - nanopb (< 2.30910.0, >= 2.30908.0) - - FirebaseCoreInternal (9.5.0): - - "GoogleUtilities/NSData+zlib (~> 7.7)" - - FirebaseInstallations (9.5.0): - - FirebaseCore (~> 9.0) - - GoogleUtilities/Environment (~> 7.7) - - GoogleUtilities/UserDefaults (~> 7.7) - - PromisesObjC (~> 2.1) - FlutterMacOS (1.0.0) - FMDB (2.7.5): - FMDB/standard (= 2.7.5) - FMDB/standard (2.7.5) - - GoogleAppMeasurement (9.4.0): - - GoogleAppMeasurement/AdIdSupport (= 9.4.0) - - GoogleUtilities/AppDelegateSwizzler (~> 7.7) - - GoogleUtilities/MethodSwizzler (~> 7.7) - - GoogleUtilities/Network (~> 7.7) - - "GoogleUtilities/NSData+zlib (~> 7.7)" - - nanopb (< 2.30910.0, >= 2.30908.0) - - GoogleAppMeasurement/AdIdSupport (9.4.0): - - GoogleAppMeasurement/WithoutAdIdSupport (= 9.4.0) - - GoogleUtilities/AppDelegateSwizzler (~> 7.7) - - GoogleUtilities/MethodSwizzler (~> 7.7) - - GoogleUtilities/Network (~> 7.7) - - "GoogleUtilities/NSData+zlib (~> 7.7)" - - nanopb (< 2.30910.0, >= 2.30908.0) - - GoogleAppMeasurement/WithoutAdIdSupport (9.4.0): - - GoogleUtilities/AppDelegateSwizzler (~> 7.7) - - GoogleUtilities/MethodSwizzler (~> 7.7) - - GoogleUtilities/Network (~> 7.7) - - "GoogleUtilities/NSData+zlib (~> 7.7)" - - nanopb (< 2.30910.0, >= 2.30908.0) - - GoogleDataTransport (9.2.0): - - GoogleUtilities/Environment (~> 7.7) - - nanopb (< 2.30910.0, >= 2.30908.0) - - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/AppDelegateSwizzler (7.7.0): - - GoogleUtilities/Environment - - GoogleUtilities/Logger - - GoogleUtilities/Network - - GoogleUtilities/Environment (7.7.0): - - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/Logger (7.7.0): - - GoogleUtilities/Environment - - GoogleUtilities/MethodSwizzler (7.7.0): - - GoogleUtilities/Logger - - GoogleUtilities/Network (7.7.0): - - GoogleUtilities/Logger - - "GoogleUtilities/NSData+zlib" - - GoogleUtilities/Reachability - - "GoogleUtilities/NSData+zlib (7.7.0)" - - GoogleUtilities/Reachability (7.7.0): - - GoogleUtilities/Logger - - GoogleUtilities/UserDefaults (7.7.0): - - GoogleUtilities/Logger - - nanopb (2.30909.0): - - nanopb/decode (= 2.30909.0) - - nanopb/encode (= 2.30909.0) - - nanopb/decode (2.30909.0) - - nanopb/encode (2.30909.0) - package_info_plus_macos (0.0.1): - FlutterMacOS - path_provider_macos (0.0.1): - FlutterMacOS - - PromisesObjC (2.1.1) - screen_retriever (0.0.1): - FlutterMacOS - shared_preferences_macos (0.0.1): @@ -131,8 +35,7 @@ DEPENDENCIES: - desktop_drop (from `Flutter/ephemeral/.symlinks/plugins/desktop_drop/macos`) - desktop_multi_window (from `Flutter/ephemeral/.symlinks/plugins/desktop_multi_window/macos`) - device_info_plus_macos (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus_macos/macos`) - - firebase_analytics (from `Flutter/ephemeral/.symlinks/plugins/firebase_analytics/macos`) - - firebase_core (from `Flutter/ephemeral/.symlinks/plugins/firebase_core/macos`) + - flutter_custom_cursor (from `Flutter/ephemeral/.symlinks/plugins/flutter_custom_cursor/macos`) - FlutterMacOS (from `Flutter/ephemeral`) - package_info_plus_macos (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus_macos/macos`) - path_provider_macos (from `Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos`) @@ -146,18 +49,7 @@ DEPENDENCIES: SPEC REPOS: trunk: - - Firebase - - FirebaseAnalytics - - FirebaseCore - - FirebaseCoreDiagnostics - - FirebaseCoreInternal - - FirebaseInstallations - FMDB - - GoogleAppMeasurement - - GoogleDataTransport - - GoogleUtilities - - nanopb - - PromisesObjC EXTERNAL SOURCES: desktop_drop: @@ -166,10 +58,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/desktop_multi_window/macos device_info_plus_macos: :path: Flutter/ephemeral/.symlinks/plugins/device_info_plus_macos/macos - firebase_analytics: - :path: Flutter/ephemeral/.symlinks/plugins/firebase_analytics/macos - firebase_core: - :path: Flutter/ephemeral/.symlinks/plugins/firebase_core/macos + flutter_custom_cursor: + :path: Flutter/ephemeral/.symlinks/plugins/flutter_custom_cursor/macos FlutterMacOS: :path: Flutter/ephemeral package_info_plus_macos: @@ -195,23 +85,11 @@ SPEC CHECKSUMS: desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898 desktop_multi_window: 566489c048b501134f9d7fb6a2354c60a9126486 device_info_plus_macos: 1ad388a1ef433505c4038e7dd9605aadd1e2e9c7 - Firebase: 7703fc4022824b6d6db1bf7bea58d13b8e17ec46 - firebase_analytics: 57144bae6cd39d3be367a8767a1b8857a037cee5 - firebase_core: 822a1076483bf9764284322c9310daa98e1e6817 - FirebaseAnalytics: a1a24e72b7ba7f47045a4633f1abb545c07bd29c - FirebaseCore: 9a2b10270a854731c4d4d8a97d0aa8380ec3458d - FirebaseCoreDiagnostics: 17cbf4e72b1dbd64bfdc33d4b1f07bce4f16f1d8 - FirebaseCoreInternal: 50a8e39cae8abf72d5145d07ea34c3244f70862b - FirebaseInstallations: 41f811b530c41dd90973d0174381cdb3fcb5e839 - FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424 + flutter_custom_cursor: 629957115075c672287bd0fa979d863ccf6024f7 + FlutterMacOS: ae6af50a8ea7d6103d888583d46bd8328a7e9811 FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a - GoogleAppMeasurement: 5d69e04287fc2c10cc43724bfa4bf31fc12c3dff - GoogleDataTransport: 1c8145da7117bd68bbbed00cf304edb6a24de00f - GoogleUtilities: e0913149f6b0625b553d70dae12b49fc62914fd1 - nanopb: b552cce312b6c8484180ef47159bc0f65a1f0431 package_info_plus_macos: f010621b07802a241d96d01876d6705f15e77c1c path_provider_macos: 3c0c3b4b0d4a76d2bf989a913c2de869c5641a19 - PromisesObjC: ab77feca74fa2823e7af4249b8326368e61014cb screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38 shared_preferences_macos: a64dc611287ed6cbe28fd1297898db1336975727 sqflite: a5789cceda41d54d23f31d6de539d65bb14100ea diff --git a/flutter/macos/rustdesk.xcodeproj/project.pbxproj b/flutter/macos/rustdesk.xcodeproj/project.pbxproj index 5e9d16659..7aacb5f05 100644 --- a/flutter/macos/rustdesk.xcodeproj/project.pbxproj +++ b/flutter/macos/rustdesk.xcodeproj/project.pbxproj @@ -33,7 +33,6 @@ /* Begin PBXFileReference section */ ADDEDBA66A6E1 /* libresolv.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libresolv.tbd; path = usr/lib/libresolv.tbd; sourceTree = SDKROOT; }; - CA603C4309E13EF4668187A5 /* Cargo.toml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = Cargo.toml; path = /Users/mac/Documents/project/rustdesk/Cargo.toml; sourceTree = ""; }; CA604C7415FB2A3731F5016A /* liblibrustdesk_static.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = liblibrustdesk_static.a; sourceTree = BUILT_PRODUCTS_DIR; }; CA6071B5A0F5A7A3EF2297AA /* librustdesk.dylib */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.dylib"; includeInIndex = 0; path = librustdesk.dylib; sourceTree = BUILT_PRODUCTS_DIR; }; CA60D3BC5386B357B2AB834F /* rustdesk */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = rustdesk; sourceTree = BUILT_PRODUCTS_DIR; }; diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index acbd45269..a871bb928 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -126,21 +126,21 @@ packages: name: cached_network_image url: "https://pub.dartlang.org" source: hosted - version: "3.2.1" + version: "3.2.2" cached_network_image_platform_interface: dependency: transitive description: name: cached_network_image_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "2.0.0" cached_network_image_web: dependency: transitive description: name: cached_network_image_web url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "1.0.2" characters: dependency: transitive description: @@ -175,7 +175,7 @@ packages: name: code_builder url: "https://pub.dartlang.org" source: hosted - version: "4.2.0" + version: "4.3.0" collection: dependency: transitive description: @@ -203,7 +203,7 @@ packages: name: cross_file url: "https://pub.dartlang.org" source: hosted - version: "0.3.3+1" + version: "0.3.3+2" crypto: dependency: transitive description: @@ -339,48 +339,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "6.1.4" - firebase_analytics: - dependency: "direct main" - description: - name: firebase_analytics - url: "https://pub.dartlang.org" - source: hosted - version: "9.3.4" - firebase_analytics_platform_interface: - dependency: transitive - description: - name: firebase_analytics_platform_interface - url: "https://pub.dartlang.org" - source: hosted - version: "3.3.4" - firebase_analytics_web: - dependency: transitive - description: - name: firebase_analytics_web - url: "https://pub.dartlang.org" - source: hosted - version: "0.4.2+4" - firebase_core: - dependency: transitive - description: - name: firebase_core - url: "https://pub.dartlang.org" - source: hosted - version: "1.22.0" - firebase_core_platform_interface: - dependency: transitive - description: - name: firebase_core_platform_interface - url: "https://pub.dartlang.org" - source: hosted - version: "4.5.1" - firebase_core_web: - dependency: transitive - description: - name: firebase_core_web - url: "https://pub.dartlang.org" - source: hosted - version: "1.7.2" fixnum: dependency: transitive description: @@ -418,8 +376,8 @@ packages: dependency: "direct main" description: path: "." - ref: "47179378523c993092f70d95f93d53f40af01f02" - resolved-ref: "47179378523c993092f70d95f93d53f40af01f02" + ref: "527821d676017387be024dffd61898ff79b14c41" + resolved-ref: "527821d676017387be024dffd61898ff79b14c41" url: "https://github.com/Kingtous/rustdesk_flutter_custom_cursor" source: git version: "0.0.1" @@ -1253,8 +1211,8 @@ packages: dependency: "direct main" description: path: "." - ref: "4627ba808ed08ff0c08706b01a7f9cc8b747accd" - resolved-ref: "4627ba808ed08ff0c08706b01a7f9cc8b747accd" + ref: "88487257cbafc501599ab4f82ec343b46acec020" + resolved-ref: "88487257cbafc501599ab4f82ec343b46acec020" url: "https://github.com/Kingtous/rustdesk_window_manager" source: git version: "0.2.7" @@ -1287,5 +1245,5 @@ packages: source: hosted version: "0.1.0" sdks: - dart: ">=2.17.0 <3.0.0" - flutter: ">=3.0.0" + dart: ">=2.18.0 <3.0.0" + flutter: ">=3.3.0" diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index e3b1c883a..e3458b57a 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -35,7 +35,7 @@ dependencies: tuple: ^2.0.0 wakelock: ^0.5.2 device_info_plus: ^4.1.2 - firebase_analytics: ^9.1.5 + #firebase_analytics: ^9.1.5 package_info_plus: ^1.4.2 url_launcher: ^6.0.9 shared_preferences: ^2.0.6 @@ -58,7 +58,7 @@ dependencies: window_manager: git: url: https://github.com/Kingtous/rustdesk_window_manager - ref: 4627ba808ed08ff0c08706b01a7f9cc8b747accd + ref: 88487257cbafc501599ab4f82ec343b46acec020 desktop_multi_window: git: url: https://github.com/Kingtous/rustdesk_desktop_multi_window @@ -71,7 +71,7 @@ dependencies: flutter_custom_cursor: git: url: https://github.com/Kingtous/rustdesk_flutter_custom_cursor - ref: 47179378523c993092f70d95f93d53f40af01f02 + ref: 527821d676017387be024dffd61898ff79b14c41 get: ^4.6.5 visibility_detector: ^0.3.3 contextmenu: ^3.0.0 diff --git a/flutter/run.sh b/flutter/run.sh index cb6e0f9cb..f1066306a 100644 --- a/flutter/run.sh +++ b/flutter/run.sh @@ -3,5 +3,6 @@ dart pub global activate ffigen --version 5.0.1 flutter pub get # call `flutter clean` if cargo build fails +# export LLVM_HOME=/Library/Developer/CommandLineTools/usr/ cargo build --features flutter flutter run $@ diff --git a/src/core_main.rs b/src/core_main.rs index 3c1d858fb..f514cd790 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -124,7 +124,7 @@ pub fn core_main() -> Option> { } #[cfg(target_os = "macos")] { - std::thread::spawn(move || start_server(true)); + std::thread::spawn(move || crate::start_server(true)); // to-do: for flutter, starting tray not ready yet, or we can reuse sciter's tray implementation. } } else if args[0] == "--import-config" { From 3ef1adf796a92ca933f1e7512fc3fa1d5a8a62e2 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 16 Sep 2022 10:32:41 +0800 Subject: [PATCH 0485/2015] opt: icons for all platform --- .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 1889 -> 2521 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 1274 -> 1681 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 2554 -> 3274 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 3822 -> 4804 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 5160 -> 6268 bytes .../AppIcon.appiconset/Contents.json | 162 +++++++++--------- .../Icon-App-1024x1024@1x.png | Bin 32008 -> 61199 bytes .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin 558 -> 618 bytes .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin 1034 -> 1209 bytes .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin 1594 -> 1798 bytes .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin 790 -> 893 bytes .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin 1531 -> 1787 bytes .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin 2331 -> 2621 bytes .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin 1034 -> 1209 bytes .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin 2105 -> 2401 bytes .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin 3176 -> 3502 bytes .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin 3176 -> 3502 bytes .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin 4814 -> 4996 bytes .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin 1969 -> 2270 bytes .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin 4010 -> 4317 bytes .../Icon-App-83.5x83.5@2x.png | Bin 4461 -> 4702 bytes .../AppIcon.appiconset/Contents.json | 90 +++++----- flutter/pubspec.yaml | 19 +- flutter/windows/runner/resources/app_icon.ico | Bin 33772 -> 21592 bytes 24 files changed, 140 insertions(+), 131 deletions(-) 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 9a6ce011d32bea90d6d91918a89da549727cb8de..b3dc255d52198ca510462a80f9cb43f084f97996 100644 GIT binary patch literal 2521 zcmV;~2`2W5P)7!gZjL0j-pwVU5@|3bkkJ3jV@_r!$-H~tyvb0=cg{N}^X9+r-uZIx z{nvLL@Q0Y>?&v*r3Zj4d6ZzCtt`>gf|9?;-*CFI*X5#BjZy{^;=Gp`uHQwzrZ4mm+ zdJIh>2Fa%eK^kNz5iVCRL=E6I8U538u=~&Ruxri|loXVE3VIV1-YyJDYo3-y597)t zre=g827RY5M9i3pNKZ*Zo~zLPRTV)bXY}S55fMKrB!j#h&^&qol2&iQI_K@kb)Dd? zDuR;MJbP7><3{6F@ff>(D;7m0%5p0;2+?As*W0gD}M6*2OciZd954woQTIHv&UqfLpr*QSxso zy8ujI4ZQHNw`|8&z+3l9NZ${v&j!w2sH;)e=8M^gW{rSZLxl+0 z>B1X&f7eKQpPK|M$^0#F;X81n^5)3fE{@RzL6= zMMI6YQ8L>$-w!O1+4S&Qpya%ITNT(w(2v^7#yZj1D*{FNMROTXaG!cWmh#U<2_!EA zKFbY!3~ty$5I59}siG4bS!;jhocCv|v!F{GnN8vsB+ds8e{1a-z6hA0&N8TXCRrzk zs{Y7n?|Y{L*mT=80oG0yQ{n|f;{}R<2x=fxdwGhOn|`#88Q$9md|&2$Z^t1+16oCF zq-@$#fSaC|X9u_^Stf|<{-yCUh)&kpfBKEgyf3O7iNt&V0dDyRFsUCfQZ!=gCi>4O zN%KYmQ&w3UpBt75x-%ZQCsDuM+d|4?8-N2R{mbn*Xj~1i0q*JxJoPiNk6P*Vb^ich z&0b*Re*JN}VTmBB^JnhRYvYn1`^Y+A*HOK8=gWbYKLOtOOlFjr56=u&pHZCU%Vd_v z%>_;t>-Fn~C4%_h?ATgee)(>3A70Z}-<~*M0W21mBqK*=gxEdTw^eILgOXM^XRQ@z z6VyZOoGAmQ_#Pp*G|;%`1RLMUlKJCCU{SKUEW<8Cx&iC5_4?2vh)$>q z{`x_1LlB;LX{V4hOwCwjur6o>(WRh6YKC(%g_rKs8$=+Wc6(}z7@T9xWpD1H|9oYF z26k4HTP23zaT|Of9W2`g%pX}jV(E*x_C^?{yh8oi$^^xCP?LN0Q}xd60%O!PKyNY0 z&oJ&pl6L2*KU7ru_1@!&{O{D{I%qn)$^@BhfP~%gU>%$-GnXNnX$JO`31V=}4w+D+fo7S8 zGC^hK)w2)2XlgO+Y6nz9zNMO3S!IIg`WW5zW-(bmQI11o6RbO#)Ti$hlXA zVlV2fsGYtkGMDj$GC_HwCAziqFGDMJV3*)!IsGA4!A>J`3iO_|Cu#-E7R!%LyUkpV4k2UL z43q~Gw7X{Ct|m(#al=0SKC}p8vC_;T=JpW5Y0ngL<`Mltsp93kM0+U&@rZZ-?Y|zf zHfRwR(1O z(JJDjr0;P+LbQ6D3yfI@kbbh1dj8EjVtaH~Pd_u4&YH#F4Kh-|tlrQ;r`AS2VoIWV z($Wnoiai<;L|<4Uh%w*?g{0N@>4i9Y%M4gQ*)SK@3rQ<>1KVm*z4ZH74oo#p_k~p+ zy7=(0Xx)wa?L1+*{Dt|Y)RjikPN!g#pe>a;z_?hsaqpYn-zQurM%{yfg-;HNVi^C5 zvBUyFKow>$?lc_OK&ru5WAwn=-Vs680|i1(L9zIaq8M@&!?6AkaE&4h6j<3hGS*sq zngfw7#1ts3y4P8;w52969qn9dBoORCzy#687$;MCP zqlXj4hKrFV|K;S~MgbG6MBFhY&VnU7z26lh=*rOF>vYLd=X+tyr*F$K3PSgrln~p9 z%7XOgqS6&|*AX zi09|jqY5YYtU#VC*C2=|FU5>WLGx(E8u}|9#c)mkJ^wHW;;~feDNZD<-i*er`h;|7 zp+^xi)11h4RjP<8Ly^Z)@#Lkk+uyG|Kj-Si*-CPtL5B~YZCk)4n(qZ_@Ifn00000NkvXXu0mjfvdO^- delta 1885 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 W(*Rc8R`UP=002ovP6b4+LSTYtWk4kW delta 1265 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 IM6N<$f~yE8CjbBd literal 2554 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 Q^Z)<=07*qoM6N<$g1?mA00000 diff --git a/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index 638a672f9fd88ce12ccd1eec924d52fd7554a91e..8bb8d570f0dd7a72fdddc94020282353a6f3375f 100644 GIT binary patch literal 4804 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 } @@ -232,6 +233,23 @@ class Enhancements: Reactor.Component { var v = me.id; if (v.indexOf("enable-") == 0) { handler.set_option(v, handler.get_option(v) != 'N' ? 'N' : ''); + } else if (v == 'screen-recording') { + var dir = handler.get_option("video-save-directory"); + if (!dir) dir = handler.default_video_save_directory(); + var ts1 = handler.get_option("allow-auto-record-incoming") == 'Y' ? { checked: true } : {}; + msgbox("custom-recording", translate('Recording'), +
    +
    {translate('Automatically record incoming sessions')}
    +
    +
    {translate("Directory")}:  {dir}
    +
    +
    +
    + , function(res=null) { + if (!res) return; + handler.set_option("allow-auto-record-incoming", res.auto_record_incoming ? 'Y' : ''); + handler.set_option("video-save-directory", $(#folderPath).text); + }); } this.toggleMenuState(); } diff --git a/src/ui/msgbox.tis b/src/ui/msgbox.tis index b7df30717..7d1430cb0 100644 --- a/src/ui/msgbox.tis +++ b/src/ui/msgbox.tis @@ -192,6 +192,14 @@ class MsgboxComponent: Reactor.Component { } } } + + event click $(button#select_directory) { + var folder = view.selectFolder(translate("Change"), $(#folderPath).text); + if (folder) { + if (folder.indexOf("file://") == 0) folder = folder.substring(7); + $(#folderPath).text = folder; + } + } function show_progress(show=1, err="") { if (show == -1) { diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 97e130d09..b3f443dda 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -394,6 +394,7 @@ impl sciter::EventHandler for SciterSession { fn save_image_quality(String); fn save_custom_image_quality(i32); fn refresh_video(); + fn record_screen(bool, i32, i32); fn get_toggle_option(String); fn is_privacy_mode_supported(); fn toggle_option(String); diff --git a/src/ui/remote.tis b/src/ui/remote.tis index 835136442..393c83384 100644 --- a/src/ui/remote.tis +++ b/src/ui/remote.tis @@ -20,6 +20,9 @@ handler.setDisplay = function(x, y, w, h) { display_origin_x = x; display_origin_y = y; adaptDisplay(); + + if (recording && !recording_refresh) handler.record_screen(true, w, h); + recording_refresh = false; } // in case toolbar not shown correclty diff --git a/src/ui_interface.rs b/src/ui_interface.rs index dc3a02c7a..419a89676 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -726,6 +726,11 @@ pub fn get_langs() -> String { crate::lang::LANGS.to_string() } +#[inline] +pub fn default_video_save_directory() -> String { + scrap::record::RecorderContext::default_save_directory() +} + #[inline] pub fn is_xfce() -> bool { crate::platform::is_xfce() diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 2feceb8fe..9f7391dac 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -98,6 +98,10 @@ impl Session { self.send(Data::Message(LoginConfigHandler::refresh())); } + pub fn record_screen(&self, start: bool, w: i32, h: i32) { + self.send(Data::RecordScreen(start, w, h, self.id.clone())); + } + pub fn save_custom_image_quality(&mut self, custom_image_quality: i32) { let msg = self .lc From eff5dd2e0340ba743df7623872fbf329e9b0f700 Mon Sep 17 00:00:00 2001 From: 21pages Date: Wed, 21 Sep 2022 13:27:18 +0800 Subject: [PATCH 0553/2015] ensure first mux frame is key frame Signed-off-by: 21pages --- libs/scrap/src/common/hwcodec.rs | 1 + libs/scrap/src/common/record.rs | 23 +++++++++++++++++------ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/libs/scrap/src/common/hwcodec.rs b/libs/scrap/src/common/hwcodec.rs index 32bcbd4a2..166f7516c 100644 --- a/libs/scrap/src/common/hwcodec.rs +++ b/libs/scrap/src/common/hwcodec.rs @@ -94,6 +94,7 @@ impl EncoderApi for HwEncoder { frames.push(EncodedVideoFrame { data: Bytes::from(frame.data), pts: frame.pts as _, + key:frame.key == 1, ..Default::default() }); } diff --git a/libs/scrap/src/common/record.rs b/libs/scrap/src/common/record.rs index d757fe8cd..836f759c2 100644 --- a/libs/scrap/src/common/record.rs +++ b/libs/scrap/src/common/record.rs @@ -161,7 +161,7 @@ impl Recorder { })?; } if self.ctx.codec_id == RecodeCodecID::H264 { - h264s.frames.last().map(|f| self.write_video(f)); + h264s.frames.iter().map(|f| self.write_video(f)).count(); } } #[cfg(feature = "hwcodec")] @@ -173,7 +173,7 @@ impl Recorder { })?; } if self.ctx.codec_id == RecodeCodecID::H265 { - h265s.frames.last().map(|f| self.write_video(f)); + h265s.frames.iter().map(|f| self.write_video(f)).count(); } } _ => bail!("unsupported frame type"), @@ -255,6 +255,7 @@ struct HwRecorder { muxer: Muxer, ctx: RecorderContext, written: bool, + key: bool, start: Instant, } @@ -273,25 +274,35 @@ impl RecorderApi for HwRecorder { muxer, ctx, written: false, + key: false, start: Instant::now(), }) } fn write_video(&mut self, frame: &EncodedVideoFrame) -> bool { - let ok = self.muxer.write_video(&frame.data, frame.pts).is_ok(); - if ok { - self.written = true; + if frame.key { + self.key = true; + } + if self.key { + let ok = self.muxer.write_video(&frame.data, frame.key).is_ok(); + if ok { + self.written = true; + } + ok + } else { + false } - ok } } #[cfg(feature = "hwcodec")] impl Drop for HwRecorder { fn drop(&mut self) { + log::info!("DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD {}", self.ctx.filename); self.muxer.write_tail().ok(); if !self.written || self.start.elapsed().as_secs() < MIN_SECS { std::fs::remove_file(&self.ctx.filename).ok(); } + log::info!("DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD ok"); } } From e74f155cb680f0bdacb95491c7b5794aac54e1eb Mon Sep 17 00:00:00 2001 From: 21pages Date: Wed, 21 Sep 2022 16:03:08 +0800 Subject: [PATCH 0554/2015] fix recording start stop Signed-off-by: 21pages --- Cargo.lock | 2 +- flutter/lib/desktop/pages/remote_page.dart | 1 + .../lib/desktop/widgets/remote_menubar.dart | 4 +-- flutter/lib/models/model.dart | 32 ++++++++++--------- libs/scrap/src/common/record.rs | 2 -- src/flutter.rs | 4 +-- src/ui/header.tis | 7 ++-- src/ui/remote.tis | 5 ++- 8 files changed, 28 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c43bccbbe..60852eba1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2444,7 +2444,7 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hwcodec" version = "0.1.0" -source = "git+https://github.com/21pages/hwcodec#097a476a0ee249e28d99573899ed4c9c0c01f884" +source = "git+https://github.com/21pages/hwcodec#1f03d203eca24dc976c21a47228f3bc31484c2bc" dependencies = [ "bindgen", "cc", diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 1225e5a66..4c13f4cde 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -118,6 +118,7 @@ class _RemotePageState extends State void dispose() { debugPrint("REMOTE PAGE dispose ${widget.id}"); _ffi.dialogManager.hideMobileActionsOverlay(); + _ffi.recordingModel.onClose(); _rawKeyFocusNode.dispose(); _ffi.close(); _timer?.cancel(); diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index dd0a0bf05..785471740 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -359,9 +359,7 @@ class _RemoteMenubarState extends State { tooltip: value.start ? translate('Stop session recording') : translate('Start session recording'), - onPressed: () async { - await value.toggle(); - }, + onPressed: () => value.toggle(), icon: Icon( value.start ? Icons.pause_circle_filled diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 8bc3e8083..c90b07daf 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -197,13 +197,13 @@ class FfiModel with ChangeNotifier { _display.height = int.parse(evt['height']); if (old != _pi.currentDisplay) { parent.target?.cursorModel.updateDisplayOrigin(_display.x, _display.y); - parent.target?.recordingModel.switchDisplay(); } // remote is mobile, and orientation changed if ((_display.width > _display.height) != oldOrientation) { gFFI.canvasModel.updateViewStyle(); } + parent.target?.recordingModel.onSwitchDisplay(); notifyListeners(); } @@ -979,33 +979,35 @@ class RecordingModel with ChangeNotifier { bool _start = false; get start => _start; - switchDisplay() { + onSwitchDisplay() { if (!isDesktop || !_start) return; var id = parent.target?.id; int? width = parent.target?.canvasModel.getDisplayWidth(); int? height = parent.target?.canvasModel.getDisplayWidth(); if (id == null || width == null || height == null) return; - bind.sessionRecordScreen( - id: id, start: _start, width: width, height: height); + bind.sessionRecordScreen(id: id, start: true, width: width, height: height); } - Future toggle() async { + toggle() { if (!isDesktop) return; var id = parent.target?.id; - int? width = parent.target?.canvasModel.getDisplayWidth(); - int? height = parent.target?.canvasModel.getDisplayWidth(); - if (id == null || width == null || height == null) return; - - await bind.sessionRecordScreen( - id: id, start: !_start, width: width, height: height); + if (id == null) return; _start = !_start; notifyListeners(); if (_start) { - Future.delayed(const Duration(milliseconds: 100), () { - bind.sessionRefresh(id: id); - }); + bind.sessionRefresh(id: id); + } else { + bind.sessionRecordScreen(id: id, start: false, width: 0, height: 0); } } + + onClose() { + if (!isDesktop) return; + var id = parent.target?.id; + if (id == null) return; + _start = false; + bind.sessionRecordScreen(id: id, start: false, width: 0, height: 0); + } } /// Mouse button enum. @@ -1174,7 +1176,7 @@ class FFI { Map event = json.decode(message.field0); await cb(event); } catch (e) { - debugPrint('json.decode fail(): $e'); + debugPrint('json.decode fail1(): $e, ${message.field0}'); } } else if (message is Rgba) { imageModel.onRgba(message.field0, tabBarHeight); diff --git a/libs/scrap/src/common/record.rs b/libs/scrap/src/common/record.rs index 836f759c2..e8bbacf02 100644 --- a/libs/scrap/src/common/record.rs +++ b/libs/scrap/src/common/record.rs @@ -298,11 +298,9 @@ impl RecorderApi for HwRecorder { #[cfg(feature = "hwcodec")] impl Drop for HwRecorder { fn drop(&mut self) { - log::info!("DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD {}", self.ctx.filename); self.muxer.write_tail().ok(); if !self.written || self.start.elapsed().as_secs() < MIN_SECS { std::fs::remove_file(&self.ctx.filename).ok(); } - log::info!("DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD ok"); } } diff --git a/src/flutter.rs b/src/flutter.rs index fea412c23..755e245fe 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -257,7 +257,7 @@ impl InvokeUiSession for FlutterHandler { self.push_event( "switch_display", vec![ - ("display", &display.to_string()), + ("display", &display.display.to_string()), ("x", &display.x.to_string()), ("y", &display.y.to_string()), ("width", &display.width.to_string()), @@ -485,4 +485,4 @@ pub fn make_fd_flutter(id: i32, entries: &Vec, only_count: bool) -> S } m.insert("total_size".into(), json!(n as f64)); serde_json::to_string(&m).unwrap_or("".into()) -} \ No newline at end of file +} diff --git a/src/ui/header.tis b/src/ui/header.tis index a997ce4b3..b8f1bdfd8 100644 --- a/src/ui/header.tis +++ b/src/ui/header.tis @@ -93,7 +93,6 @@ function editOSPassword(login=false) { } var recording = false; -var recording_refresh = false; class Header: Reactor.Component { this var conn_note = ""; @@ -286,10 +285,12 @@ class Header: Reactor.Component { } event click $(span#recording) (_, me) { - handler.record_screen(!recording, display_width, display_height); recording = !recording; header.update(); - if (recording) self.timer(100ms, function() { recording_refresh = true; handler.refresh_video(); }); + if (recording) + handler.refresh_video(); + else + handler.record_screen(false, display_width, display_height); } event click $(#screen) (_, me) { diff --git a/src/ui/remote.tis b/src/ui/remote.tis index 393c83384..dc9d60d54 100644 --- a/src/ui/remote.tis +++ b/src/ui/remote.tis @@ -20,9 +20,7 @@ handler.setDisplay = function(x, y, w, h) { display_origin_x = x; display_origin_y = y; adaptDisplay(); - - if (recording && !recording_refresh) handler.record_screen(true, w, h); - recording_refresh = false; + if (recording) handler.record_screen(true, w, h); } // in case toolbar not shown correclty @@ -470,6 +468,7 @@ function self.closing() { var (x, y, w, h) = view.box(#rectw, #border, #screen); if (is_file_transfer) save_file_transfer_close_state(); if (is_file_transfer || is_port_forward || size_adapted) handler.save_size(x, y, w, h); + if (recording) handler.record_screen(false, display_width, display_height); } var qualityMonitor; From e7e3494dc9e196d548f5c42f6ac7add6e756f828 Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 22 Sep 2022 09:55:34 +0800 Subject: [PATCH 0555/2015] record permission Signed-off-by: 21pages --- flutter/lib/common.dart | 2 ++ .../desktop/pages/desktop_setting_page.dart | 3 ++ flutter/lib/desktop/pages/server_page.dart | 7 ++++ .../lib/desktop/widgets/remote_menubar.dart | 32 +++++++++++-------- flutter/lib/models/server_model.dart | 2 ++ libs/hbb_common/protos/message.proto | 1 + src/client/io_loop.rs | 3 ++ src/ipc.rs | 1 + 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/fr.rs | 2 ++ src/lang/hu.rs | 2 ++ src/lang/id.rs | 2 ++ src/lang/it.rs | 2 ++ src/lang/ja.rs | 2 ++ src/lang/ko.rs | 2 ++ src/lang/kz.rs | 2 ++ src/lang/pl.rs | 2 ++ src/lang/pt_PT.rs | 2 ++ src/lang/ptbr.rs | 2 ++ src/lang/ru.rs | 2 ++ src/lang/sk.rs | 2 ++ src/lang/template.rs | 2 ++ src/lang/tr.rs | 2 ++ src/lang/tw.rs | 2 ++ src/lang/vn.rs | 2 ++ src/server/connection.rs | 9 ++++++ src/ui/cm.css | 4 +++ src/ui/cm.rs | 3 +- src/ui/cm.tis | 20 +++++++++--- src/ui/header.tis | 2 +- src/ui/index.tis | 3 ++ src/ui/remote.tis | 2 ++ src/ui_cm_interface.rs | 9 ++++-- 38 files changed, 126 insertions(+), 21 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 365ce3dd5..6166f8121 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -48,6 +48,8 @@ late final iconFile = MemoryImage(Uint8List.fromList(base64Decode( 'iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAAUVBMVEUAAAD///////////////////////////////////////////////////////////////////////////////////////////////////////8IN+deAAAAGnRSTlMAH+CAESEN8jyZkcIb5N/ONy3vmHhmiGjUm7UwS+YAAAHZSURBVGje7dnbboMwDIBhBwgQoFAO7Ta//4NOqCAXYZQstatq4r+r5ubrgQSpg8iyC4ZURa+PlIpQYGiwrzyeHtYZjAL8T05O4H8BbbKvFgRa4NoBU8pXeYEkDDgaaLQBcwJrmeErJQB/7wes3QBWGnCIX0+AQycL1PO6BMwPa0nA4ZxbgTvOjUYMGPHRnZkQAY4mxPZBjmy53E7ukSkFKYB/D4XsWZQx64sCeYebOogGsoOBYvv6/UCb8F0IOBZ0TlP6lEYdANY350AJqB9/qPVuOI5evw4A1hgLigAlepnyxW80bcCcwN++A2s82Vcu02ta+ceq9BoL5KGTTRwQPlpqA3gCnwWU2kCDgeWRQPj2jAPCDxgCMjhI6uZnToDpvd/BJeFrJQB/fsAa02gCt3mi1wNuy8GgBNDZlysBNNSrADVSjcJl6vCpUn6jOdx0kz0q6PMhQRa4465SFKhx35cgUCBTwj2/NHwZAb71qR8GEP2H1XcmAtBPTEO67GP6FUUAIKGABbDLQ0EArhN2sAIGesRO+iyy+RMAjckVTlMCKFVAbh/4Af9OPgG61SkDVco3BQGT3GXaDAnTIAcYZDuBTwGsAGDxuBFeAQqIqwoFMlAVLrHr/wId5MPt0nilGgAAAABJRU5ErkJggg=='))); late final iconRestart = MemoryImage(Uint8List.fromList(base64Decode( 'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAB7BAAAewQHDaVRTAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAbhJREFUWIXVlrFqFGEUhb+7UYxaWCQKlrKKxaZSQVGDJih2tj6MD2DnMwiWvoAIRnENIpZiYxEro6IooiS7SPwsMgNLkk3mjmYmnmb45/73nMNwz/x/qH3gMu2gH6rAU+Blw+Lngau4jpmGxVF7qp1iPWjaQKnZ2WnXbuP/NqAeUPc3ZkA9XDwvqc+BVWCgPlJ7tRwUKThZce819b46VH+pfXVRXVO/q2cSul3VOgZUl0ejq86r39TXI8mqZKDuDEwCw3IREQvAbWAGmMsQZQ0sAl3gHPB1Q+0e8BuYzRDuy2yOiFVgaUxtRf0ETGc4syk4rc6PqU0Cx9j8Zf6dAeAK8Fi9sUXtFjABvEgxJlNwRP2svlNPjbw/q35U36oTFbnyMSwabxb/gB/qA3VBHagrauV7RW0DRfP1IvMlXqkXkhz1DYyQTKtHa/Z2VVMx3IiI+PI3/bCHjuOpFrSnAMpL6QfgTcMGesDx0kBr2BMzsNyi/vtQu8CJlgwsRbZDnWP90NkKaxHxJMOXMqAeAn5u0ydwMCKGY+qbkB3C2W3EKWoXk5zVoHbUZ+6Mh7tl4G4F8RJ3qvL+AfV3r5Vdpj70AAAAAElFTkSuQmCC'))); +late final iconRecording = MemoryImage(Uint8List.fromList(base64Decode( + 'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAANpJREFUWEftltENAiEMhtsJ1NcynG6gI+gGugEOR591gppeQoIYSDBILxEeydH/57u2FMF4obE+TAOTwLoIhBDOAHBExG2n6rgR0akW640AM0sn4SWMiDycc7s8JjN7Ijro/k8NqAAR5RoeAPZxv2ggP9hCJiWZxtGbq3hqbJiBVHy4gVx8qAER8Yi4JFy6huVAKXemgb8icI+1b5KEitq0DOO/Nm1EEX1TK27p/bVvv36MOhl4EtHHbFF7jq8AoG1z08OAiFycczrkFNe6RrIet26NMQlMAuYEXiayryF/QQktAAAAAElFTkSuQmCC'))); enum DesktopType { main, diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 231e001a2..119abda08 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -436,6 +436,9 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { enabled: enabled), _OptionCheckBox(context, 'Enable Remote Restart', 'enable-remote-restart', enabled: enabled), + _OptionCheckBox( + context, 'Enable Recording Session', 'enable-record-session', + enabled: enabled), _OptionCheckBox(context, 'Enable remote configuration modification', 'allow-remote-config-modification', enabled: enabled), diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index 9ca446dcb..400def7f0 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -411,6 +411,13 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> { client.restart = enabled; }); }, null), + buildPermissionIcon(client.recording, iconRecording, (enabled) { + bind.cmSwitchPermission( + connId: client.id, name: "recording", enabled: enabled); + setState(() { + client.recording = enabled; + }); + }, null), ], )), ], diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 785471740..6e137fa6e 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -354,19 +354,25 @@ class _RemoteMenubarState extends State { } Widget _buildRecording(BuildContext context) { - return Consumer( - builder: (context, value, child) => IconButton( - tooltip: value.start - ? 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, - ), - )); + 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: Icon( + value.start + ? Icons.pause_circle_filled + : Icons.videocam_outlined, + color: _MenubarTheme.commonColor, + ), + )); + } else { + return Offstage(); + } + })); } Widget _buildClose(BuildContext context) { diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index a26fe5062..10cc8e0b1 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -544,6 +544,7 @@ class Client { bool audio = false; bool file = false; bool restart = false; + bool recording = false; Client(this.id, this.authorized, this.isFileTransfer, this.name, this.peerId, this.keyboard, this.clipboard, this.audio); @@ -559,6 +560,7 @@ class Client { audio = json['audio']; file = json['file']; restart = json['restart']; + recording = json['recording']; } Map toJson() { diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index e711f5826..8fb67e5c1 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -436,6 +436,7 @@ message PermissionInfo { Audio = 3; File = 4; Restart = 5; + Recording = 6; } Permission permission = 1; diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 1cf89f173..cf6168834 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -958,6 +958,9 @@ impl Remote { Permission::Restart => { self.handler.set_permission("restart", p.enabled); } + Permission::Recording => { + self.handler.set_permission("recording", p.enabled); + } } } Some(misc::Union::SwitchDisplay(s)) => { diff --git a/src/ipc.rs b/src/ipc.rs index 36f6b9c1f..709384bb6 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -146,6 +146,7 @@ pub enum Data { file: bool, file_transfer_enabled: bool, restart: bool, + recording: bool, }, ChatMessage { text: String, diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 65c7b529d..cd534858b 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -352,5 +352,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change", "更改"), ("Start session recording", "开始录屏"), ("Stop session recording", "结束录屏"), + ("Enable Recording Session", "允许录制会话"), + ("Allow recording session", "允许录制会话"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index a271e2446..ba6d20fab 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -352,5 +352,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change", ""), ("Start session recording", ""), ("Stop session recording", ""), + ("Enable Recording Session", ""), + ("Allow recording session", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 77f585390..82cb403dc 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -352,5 +352,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change", ""), ("Start session recording", ""), ("Stop session recording", ""), + ("Enable Recording Session", ""), + ("Allow recording session", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 27e04717e..16e625927 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -352,5 +352,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change", ""), ("Start session recording", ""), ("Stop session recording", ""), + ("Enable Recording Session", ""), + ("Allow recording session", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index a3add20f9..dee425de2 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -352,5 +352,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change", ""), ("Start session recording", ""), ("Stop session recording", ""), + ("Enable Recording Session", ""), + ("Allow recording session", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 049a8c428..211d7bb92 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -365,5 +365,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change", ""), ("Start session recording", ""), ("Stop session recording", ""), + ("Enable Recording Session", ""), + ("Allow recording session", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 033e08c4c..694131ddd 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -352,5 +352,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change", ""), ("Start session recording", ""), ("Stop session recording", ""), + ("Enable Recording Session", ""), + ("Allow recording session", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 4cff53b2d..d5d6cb1f2 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -352,5 +352,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change", ""), ("Start session recording", ""), ("Stop session recording", ""), + ("Enable Recording Session", ""), + ("Allow recording session", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index dd4adf2bc..992a03ac2 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -365,5 +365,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change", ""), ("Start session recording", ""), ("Stop session recording", ""), + ("Enable Recording Session", ""), + ("Allow recording session", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index dd941b30b..98730c8d1 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -351,5 +351,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change", ""), ("Start session recording", ""), ("Stop session recording", ""), + ("Enable Recording Session", ""), + ("Allow recording session", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 2d76c93db..e25207ce8 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -349,5 +349,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change", ""), ("Start session recording", ""), ("Stop session recording", ""), + ("Enable Recording Session", ""), + ("Allow recording session", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 5897dc690..0a8a6555c 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -346,5 +346,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change", ""), ("Start session recording", ""), ("Stop session recording", ""), + ("Enable Recording Session", ""), + ("Allow recording session", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index c2f5f2cf0..b24708933 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -327,5 +327,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change", ""), ("Start session recording", ""), ("Stop session recording", ""), + ("Enable Recording Session", ""), + ("Allow recording session", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index b54218d56..d6a3ee01f 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -350,5 +350,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change", ""), ("Start session recording", ""), ("Stop session recording", ""), + ("Enable Recording Session", ""), + ("Allow recording session", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 7b386c3bf..b27da9a29 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -346,5 +346,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change", ""), ("Start session recording", ""), ("Stop session recording", ""), + ("Enable Recording Session", ""), + ("Allow recording session", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 099ecb2bb..a7b4fa51a 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -352,5 +352,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change", ""), ("Start session recording", ""), ("Stop session recording", ""), + ("Enable Recording Session", ""), + ("Allow recording session", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index c001b8770..fbe49d9e0 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -352,5 +352,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change", ""), ("Start session recording", ""), ("Stop session recording", ""), + ("Enable Recording Session", ""), + ("Allow recording session", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 2a1de16aa..b075af3a8 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -352,5 +352,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change", ""), ("Start session recording", ""), ("Stop session recording", ""), + ("Enable Recording Session", ""), + ("Allow recording session", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 5896d4336..1aa7009c7 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -352,5 +352,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change", ""), ("Start session recording", ""), ("Stop session recording", ""), + ("Enable Recording Session", ""), + ("Allow recording session", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 9e9475ead..e8ac92743 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -365,5 +365,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change", ""), ("Start session recording", ""), ("Stop session recording", ""), + ("Enable Recording Session", ""), + ("Allow recording session", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index dc7ab8a59..190beca3c 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -352,5 +352,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change", "變更"), ("Start session recording", "開始錄屏"), ("Stop session recording", "結束錄屏"), + ("Enable Recording Session", "允許錄製會話"), + ("Allow recording session", "允許錄製會話"), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index ebd44d8d5..394b97c2b 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -352,5 +352,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change", ""), ("Start session recording", ""), ("Stop session recording", ""), + ("Enable Recording Session", ""), + ("Allow recording session", ""), ].iter().cloned().collect(); } diff --git a/src/server/connection.rs b/src/server/connection.rs index 161c058f8..15d313fbe 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -81,6 +81,7 @@ pub struct Connection { audio: bool, file: bool, restart: bool, + recording: bool, last_test_delay: i64, lock_after_session_end: bool, show_remote_cursor: bool, // by peer @@ -169,6 +170,7 @@ impl Connection { audio: Config::get_option("enable-audio").is_empty(), file: Config::get_option("enable-file-transfer").is_empty(), restart: Config::get_option("enable-remote-restart").is_empty(), + recording: Config::get_option("enable-record-session").is_empty(), last_test_delay: 0, lock_after_session_end: false, show_remote_cursor: false, @@ -210,6 +212,9 @@ impl Connection { if !conn.restart { conn.send_permission(Permission::Restart, false).await; } + if !conn.recording { + conn.send_permission(Permission::Recording, false).await; + } let mut test_delay_timer = time::interval_at(Instant::now() + TEST_DELAY_TIMEOUT, TEST_DELAY_TIMEOUT); let mut last_recv_time = Instant::now(); @@ -290,6 +295,9 @@ impl Connection { } else if &name == "restart" { conn.restart = enabled; conn.send_permission(Permission::Restart, enabled).await; + } else if &name == "recording" { + conn.recording = enabled; + conn.send_permission(Permission::Recording, enabled).await; } } ipc::Data::RawMessage(bytes) => { @@ -777,6 +785,7 @@ impl Connection { file: self.file, file_transfer_enabled: self.file_transfer_enabled(), restart: self.restart, + recording: self.recording, }); } diff --git a/src/ui/cm.css b/src/ui/cm.css index 0832c6251..fbbd58961 100644 --- a/src/ui/cm.css +++ b/src/ui/cm.css @@ -108,6 +108,10 @@ icon.restart { background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAB7BAAAewQHDaVRTAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAbhJREFUWIXVlrFqFGEUhb+7UYxaWCQKlrKKxaZSQVGDJih2tj6MD2DnMwiWvoAIRnENIpZiYxEro6IooiS7SPwsMgNLkk3mjmYmnmb45/73nMNwz/x/qH3gMu2gH6rAU+Blw+Lngau4jpmGxVF7qp1iPWjaQKnZ2WnXbuP/NqAeUPc3ZkA9XDwvqc+BVWCgPlJ7tRwUKThZce819b46VH+pfXVRXVO/q2cSul3VOgZUl0ejq86r39TXI8mqZKDuDEwCw3IREQvAbWAGmMsQZQ0sAl3gHPB1Q+0e8BuYzRDuy2yOiFVgaUxtRf0ETGc4syk4rc6PqU0Cx9j8Zf6dAeAK8Fi9sUXtFjABvEgxJlNwRP2svlNPjbw/q35U36oTFbnyMSwabxb/gB/qA3VBHagrauV7RW0DRfP1IvMlXqkXkhz1DYyQTKtHa/Z2VVMx3IiI+PI3/bCHjuOpFrSnAMpL6QfgTcMGesDx0kBr2BMzsNyi/vtQu8CJlgwsRbZDnWP90NkKaxHxJMOXMqAeAn5u0ydwMCKGY+qbkB3C2W3EKWoXk5zVoHbUZ+6Mh7tl4G4F8RJ3qvL+AfV3r5Vdpj70AAAAAElFTkSuQmCC'); } +icon.recording { + background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAANpJREFUWEftltENAiEMhtsJ1NcynG6gI+gGugEOR591gppeQoIYSDBILxEeydH/57u2FMF4obE+TAOTwLoIhBDOAHBExG2n6rgR0akW640AM0sn4SWMiDycc7s8JjN7Ijro/k8NqAAR5RoeAPZxv2ggP9hCJiWZxtGbq3hqbJiBVHy4gVx8qAER8Yi4JFy6huVAKXemgb8icI+1b5KEitq0DOO/Nm1EEX1TK27p/bVvv36MOhl4EtHHbFF7jq8AoG1z08OAiFycczrkFNe6RrIet26NMQlMAuYEXiayryF/QQktAAAAAElFTkSuQmCC'); +} + div.buttons { width: *; border-spacing: 0.5em; diff --git a/src/ui/cm.rs b/src/ui/cm.rs index 959141da6..e0fea8bf3 100644 --- a/src/ui/cm.rs +++ b/src/ui/cm.rs @@ -32,7 +32,8 @@ impl InvokeUiCM for SciterHandler { client.clipboard, client.audio, client.file, - client.restart + client.restart, + client.recording ), ); } diff --git a/src/ui/cm.tis b/src/ui/cm.tis index 4708acea5..c6664b50b 100644 --- a/src/ui/cm.tis +++ b/src/ui/cm.tis @@ -41,13 +41,16 @@ class Body: Reactor.Component
  • {v?hPuWKmS$V$-_uKFy!hK{!^Musbr_5g@ zX)OLx+8smmR%XPXb&c~5GDnNWyrlI0hGtAHh(dkHd-6SQ%U#T(FOdA1`D=Z4tp*C0 zieMc%uE_7~iR&!#C?6O;90Vox2&e8SdnZj|;c*b|KGN-?W!WuhotEm)#D5&xV|xg^ zvS_ZxK&6K5vMqvsPYd68RB|~#iOT6SYsL*OMKS#RZqv|+J7QKFij6X>5|CI(zcG>f z^~*E7n{JCj0`9`$(Bs&sq@MrccE`80cFv*Br6AZFNm9x>I1#3w(soVFb<=6#m6x)U zP#-EBBjWXUrC6n)TF8Zkw#FmfIFT z)aKi^=~oWiV_v19Ktxx1*2>7)Yuhhd)zepl9>1kUa5@(LBO9s#vj+)slB9E({bNWs zt#Re&O0`K@!AU2L)cbD#j2*%lFDWL5?89%J^wys(!#?2|IG;Hv5P@EL^?to(pG{; z@8H>#edNL$(_o)TRLFm}Gm|wLHVQ}Q_FAS!T9(-U`}zE`o9>4yD$KP<%dRAbwHA%L z{S_lXw0jnK58UH(q$%UCyyB(qztL2SGAnu*UG~vm{AzN&6u8y@~T`}JX)>J($faGA_>Eoi$|vw9uT9+Xt^N2hs4vIZ9`_R z8yOy?akypJG7t*HhwHmZw*RYN0}iutvlDW)@oKFK9dn5fBy$StekX1nC|_eHhZiLq zJ(_k{utUpVBvGYKJ5Wx& z7-OQEOJFbV@%Hcw`n&qQ=U1FRXNDoL*<)4S^m>ksx{fH#CpPKV)sYtyHa9tj&K=5S zF{C{*8^X z2hmi&{DD@rj3MnciRC$yd)Q#b$R|n@5&*E9!{8mQXJ?cDi zhWl$~Ki9Y$nln=Cv4f0N;aD%^_Eiql^?p$iUOK6{dobiMT>E@Y{h_+$8IXHu!Ou$bK;ir!KfAN>?KpT7e zK7P2jzBZ!fk^Qv8b!p?`^xn0DG?~&D8ehJmC9gKd_kI$_F0dk)YV&?p@;^(Q%rAbX zQR&Zcz&`T^C;JzbNZqzg@iC~VoK4Gm4qrxioQ*k!hf9WFUi#qqWTvagZ%1Vbt5^P< z*iL?M2#l+~96#Q>v9Ws#>Ay5_oG-HFproghqv4Kez;;Q6$UQpnejLV64W?RdlhBTh z@-*5vy!R>>x94kfQHQsCo6BBG%|i^%(VGmKsjyS-$dhZfJ)&H*;f(7qRs13+*c*M?lXNdh0Lo@}!p}E&JvCqly_pE8#4bD%6SX z3h(mIlyGp6w@ovJF{2knc17|UZJo^V>LX9La$GpQFXv@Ue1PE8*$@1VL`h|d zfKFRU50e7^wKpj*ovL$A^e=CGxc0ZkUe9mb`=ERz>)wEi7WdzOHTxAqc!_ah0$%9R zFnPUDL|Q}ZW}Pnun>sLGT)>M2?FQf+y?$Ow6@9!a;o2<$rfhG5*M(w{;h??zttJBe#p%rE(&Btf@Q~BR zhc_tGnvZ@UP$7DEDwFFErp5w#y!JP2yNai}kXwN&i%Kd>4 z9G5>bG~S@BA$_h1)(XhfwcJ{(_^)tL+=e~N;Y(ZoyjSqt^!ml%78gAI2QN@Yen)Tr zyAIilgxOwQSySaeVA>BcKf3hsk-2z9hrhz_!XK90-sto(cOD4k{v??X#GTL>u$^zU zb{)GjZ>F7V+MVJ|($IdX-`8ujODO%za^yB6oUx0QRoPuLPJqPrJ=Z<;(mAXP27gI5dsj6f1zk(y=Hn4K}COaoQ z+NQS)S2o6RQ^Ai^29MHr&w((aw!hm>w0i8@Bs;UM3}1O9k82Nq(BA#UW!V~jbBqV2 zn3cc2AAtK5nLm%qa0&3q-D>N6&`t;%{(K_&^9;Jzg8Qb)d-RqQne0TK74FpjDx#GV ze~=Yt&9KzL+*_jJOH^+^`F|fiOP2ru diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png index b17866fe40a146d18921e2ba11afbcbc873454d8..627a5900c45b687f543d5003cecea324fefa79ea 100644 GIT binary patch delta 594 zcmV-Y0tQd{&CLZ-sfGG-OiBBIPD zGNTa+7ZF6R3XCFZ6FpFiSr`-*S+ppEh)M>M8Z8m&1*I7g6!ub^5_Ckgai;S*!`Qud zf}uZmaW41#zVrCbxoR>LKJLZS&d4|kDkGJ>4(hj{ggo&uQGYC=vEO)}kdlm&nE$7X zohWOzv(l+q!`edj++?Gjayz$QbGMsN*x(kJK$*y@6*N_nm#yuSt!yZwdX;`&e!+d6 z_dg8I2Dqk-D?3R_()L73>I;6j^@DWs$)jX3j&fe{Wr;Qm`0 zDsU{KActGKHGdz=e;Pizp3F3D9J)t?Z~FL-k2ufc+mB-*WeadE=gu1=tdglL)<*9D zXWPsz91rRf+iJDF)e0BoGc#QqExqQh#^OsC(iBVQT567vqQF1qY%;88L*Phh2KeV_ z^nFQQd^k5F8o(d0z#sgYw`EbWj5%5ON6oOiBzlEieSa2M>TG_-0Vg>#Xs%}aRh?u^ z(&tmolSZFzWZgR4rKF~4pZdr2^v8pT#&BJxbsw`c zS#0C=w#RGsKbpH9BDDsVi?a?UnyzS^R%s4Z=BR3*CYBqtmnMxDW*$Qve|!^ z*EhtDYfI#3V=th)pJQGwb@BW&!H~Iy0xJ0@$Yz@)D=|2AC9)W?-7rbYQSvvD3}v%r gKNFP16!M&lKSwjW9)EAUh5!Hn07*qoM6N<$f+sT~e*gdg delta 534 zcmV+x0_pwg1g->-BYy$)NklEE`#tHN8kBO{A&SJfu<`(mF_@ z!!o8`@}P$d>LPaNkU~(9XaofnQVu#um#D)Qffgag3W^08REU-3zoBEc&CRuE{!o5@ zycm7?9lj6m`}sZZ^S;mfnz$Hetun2d{15PUGfjs$yoa-oaDNn0Zl(V{Pkb!u>eIpv zc5c;@u}OSxCKXnkcIxfC9i!gG=gHOK=2GsQA|*w9FwE}&qkd(mIEPo~DeK|Ov=N+d z;if~#Xp9aI-7g7*x%5od++D+wTr8V$H3}cs! z11}ejN_+J>arQ}t%Sv=xXQh}(ls>NxPN>sEZ4o&cIDZQ9wQ+Zdu0d?sv>l`F0H$>^ z=jw}$+5d;(=lz}ceV_Mvo^uqp+YOI4RwK6(wSO%bB8`heW(rnLLx{qc zeCq1Kf_&_494e%$dM94pgD-1QvJ?)h2kZxGuV_Zob5XMb3eB+JsH$k_Yb&78y82&* zhoKCeHw~Z$r)Q0giyU%#JVF(GUH6}5avWku;lL@hw3%ZY1y8vN%O+!P62gK(8*t_Z z>^p(Da2#BQKz|Fax>4PX{2FXKg3f>bCvbQe@)qI2@m@~;!3%D~_AE*7q|04=?&y}> zqR}s(!f)`%X1E6dPOzikQ90Gwj#AIoufKrCRwOzkZGRq4=KcCciAk_Em}hK6s9+?Y zDs4huHNI^`r;9CZ-AG!EsW&4#6^oJ)6ezS`bsvuWg5sadfLBb_kAoI0`*Ys-I*~t@ zpW@?_N)$bT>owj$b534a_747X83Wrw@WQ=Y}OD9Js9utsRK6c`Y#`C4bHIVBOxm;zoCmIWRnNO$#0#YyjMI zRlIHCRA`nR79{p53)*_{#SVRoTkJS?-UOAh^=L7vIQ#S|0iU?2Z;>8v4m=@-=SizZ zrvdQcpSju%)tD*ttg9F(&*-Gh+^Q_1SWf@O z8Gp<-m_!wf7ED?*4`nCOF3Y}J1zAv-37fbtzB(pvECClE$I%wt9__WnN22^WEG)p; zOZ`%BxDHzuvqNY_A#Z&@;Z_2sO!v|*lsqFCQ*Pww|6UnBtI>NtWwRxzSTzF?Vrn71 zRod^n%JVTL{13{JlP(xX4#VpE@$PKwX@5Z0HeBjt@BQ>4PAb+ANhdq%ux7uU+Ao;& z;x5ERU|}B@rASU5Fh2r)Cx3a@ z^4H6JZq;}hl}A*OylGuS@_lzbA6ikg8HC10Ldi_wKLmaj#b9OSG+sh~ekIe_&CAoFP@XSY%Ks${ zPWZJ`srI(3KVKYH{u{Ekg59!V$RF^ut~A3mR-%&r1!t^paPjYDi~s-t07*qoM6N<$ Ef;9U))Bpeg delta 1013 zcmVnNpUTW?|8NnNdklY6nzM zd3D%96e5&%d?^b05Ps;Q=-NopE&`>}%8ZW1BU6eFDmPTpQc^41R#Q{A=WV3jcXr&J zF{>~BkzwbVXZ_7Q`@a9@83$}L;(y5DzfgO@y9(6`yCd0!S+A=)- z@OQuqgRpBJX8Xg=j-6J0K~5}et(Otv*7N8m~^%p6ocmmK(k7k`mzRhbnL^E_b9H!G#`na$VvF{6-n0> zf(JSxJqCU3xuDUA%oj+$hoZ{P$k%2}Kf;z+!fdRa27CT4x6j309d5r-0zMFdVXj=H z;X77eK-MdXd#L$BIr%+NQeeZ&( z3?avmp8!8IcHm&dPQm%d^1z#dq2`MmpJ9V4s9MjeqK)s|>%ae-T=2??+$kYbi)~W{ zbeToDJRy*|@V!cxRw%e|3XstioNP21Pp zTY|h~fPY7NaHp;9sQjV~tZzY`0bT=4k)bXUX<~5yKHO>Z4|HQBqg`MQ?x&UkZsYfy zlbRLHit|aziNAYU`?$W5ziiL;W%(y1R5e@1=Bl$;(h^`@oj7BTzcMhbVUc(0>Sch7 ztI*Z~2eWUI^`yw&$N1ILM0)g22ADj#paOFOOn(s?0qCK%<+7+_ybwK^i@f=WGC5C7 zB8M{vlmDHMLhupPHCpC!a&g4PSh3w|ilmbzz!#rj?*fb%WQzC?LhcqU*5G|ZmsGO6 zj5tj2=7P1~aJoP?m`sDZLe@sk>#6C!*nz_jnQy=2EoF!c>ja6R?1@F%l?{W`lp+l# z-G6+|7~4a+pld(u568YR*4`u2@VN<&4!F7&5kcZ*EQl(XBGs3yfk``Wux_zrtT;|( zm_Hg}qjBjeTMCOU5(M3Ui}jbgq*?>F8?ho4DN$^CvXn4)TGmz&wS?$2v{)UmtxcKq z&8RfiVh=8Yb6<&sCX3&XTzbzf9M#%(cy8MR{fRteJZF>oy6J3a6+{yxsYY)~t2ZFE z1cx55IYf?Ry}}S6@j7;AW0oIl*km#}zh*~zUy8fLoS6o${0e4y@}=OVjhf7ZGWKUK jMQ++naL=k2{Ll0g3@j(^y~}aP00000NkvXXu0mjfe;@Ob diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png index fa5fb621a78f9606976374b825f82492f0a7689d..fec31995671bdae66b2206e4fdc327d46478a3fd 100644 GIT binary patch delta 1782 zcmVeF)wV7%T8Xu8*jh9eO+_jiaf@1Iu~bCItx8c5Tu@Yy#f6?bGj(S0 z-nTKS@MV77IrqN%zPxwtIp4YW>1t|fV1d(xSa}rtFQVunUVk~7Iw8~tlY=p;rZ-uss@G-A#5A>Cy`HS&2(`UKu}Wfd#{`{4f?D zK=m^%cUMi?#Q_PCnDUlzKoduEyRl$;7e~w+hCy8rnTV3dYAL8B-M<48qnHmU#B<5s zL7kB|2OlLP^_(geYDkkxOpAr1z09yjRpxeim3W*1ZJQ%yDkl7bZGR~zqL?&!YSKh` z#)Nal=6`nK1Ng|j7FxvX#<8o;6e;AQj5N9CuBm90i9G4C0T|;i?4@Gun}b-e4^IS(?tkfX?7hH#pYFoEaD4JM>>Ke%hIC>f z<;yf%ke)L{*hOnDItja!Dcc8?h~JHq1=ul8uSf9hkE=0!oodELP|d+&+$6rAP}*90 zNPkA3NRb}U5q-S*T}uz+7qPOT_l<%AVoT=7vD~`+UjG92@0@i zKVtjwtH>Ogv=AK#vsg?s7+LoJk6VmJ9o5T`5D7uX|pt&sE^?(lBOu~UpoIIfl< zA1MOT0Uj*fl!+yPko3S#eEZpAo9#E86o2>t1(xn@kSX>MA?a@JeEaD_Rm7xFSjuWM zODX#T9b0N5-MJOte!WyJHz8^a`xt($IbG5^M@U+v6C_+s11LkOy1NmoD;H4))`muQ ze7nZvQu&l`Z|S0#kpj3m^X(PYe7g}s(zgYwMYKRQK-Y*VQZA%5laO@&J-)rKH-CGl z&+EQy<9peL zbeJ#Se*TtKb^J1fq$z^t6`^NaP8X@i#p&`=T^gRQc)tg~>vz4(wNfE3=}&w&l4GYF zHhTywL+XMxk!T_a#&ln4NRj^e6n`rMxPf`*0Bp*#+AUCpxB+-q@R-Rr^5pgsq(~P% z#IL9MLd@9#snalUHEyck-X7f(t40ZXx8=ysAd?a2rZGw0l=mJxzJ{#NFms2BO)Yyn zd=ZL;!-WMo$@H}+m; zUn8$QmLFl^!oyWKfc$4{0QR0|zO?fUs;u)ry`_mkYz@503q+Pk(_?eCJYy&!U0Q*N zjYyv%r8*q#nHSM-;a!YakIVm631j`S{sXx3wy|r0(!zu!MU0_P<>lPKJL_>cj;%jw zkP46O%e=*iufi2E{`)S5{eQ@wjzTDvrc_7FNXF(U^GSsWmM_Fxpoc7Rzk;2^-dD^R zs`NC4ZHn1z#>i~=x>%G?qA2kK`6bNv)W&;tMZm^PWM5}dc5oL(v`k=?-!8P~Aolz9v#i}o?> zVP{>#(JL%>p4BQ>tVm1m<;a3uGyM|#aX~B0Z;AHFxo~lSF4U(XeeVH7y`!5P+`vdj z_(4bR=s{+v8AB@ Y7uKB%1pM)ERR91007*qoM6N<$f^*z<$N&HU delta 1577 zcmV+^2G;q84!R7GB!2-(L_t(|0qvQ2P*qhN$G^Zs1VsZ;$OUmBG=bbgToN}lDfiSA zbxZ`)8JpCctQe;pb;ikXD~&QO1%-6b+!zgK1T&)|L`gPHO<7zs+td)xvnp;5H8Xnz!KEgD5zi>F((lRaMa z#JNJ;xo^_$>j9k|G7C{&*-$_A$f8>`#gv|S%^yKs(9|A9#Td8=f84H)E**+BufoO# z_a5SSF7{+#-#PqK_V`45IOE-62n&R>L!HJJnAH~_9{t=%f z&lqDOVZ?ip}#QaZ<`&gV+S2ie~XX^Sy!CywQ4 zQlUlkL_fsMg1w#OG#Yb8^ra>b-W=*Jhvs7H4mLp5qkmTPpiYRLAy-Uf6x4pcdKYfZ zBCVBQQxBvI^`@$BtiXTi!~*=1K5tE4h%~M#60Ljza$5 zYUHR8ZEJ&=8ED;H>72ZqSf7IUblf(bS$?<#|F!7Z7PA8|r=Ll3tOwJZGW$=v1)^+s4fY)zFal8JV4#EQ|v z=!IJtyA@eCMj*?Qztt6rs1+f9ng}TJxPf>JFs@8=4 znHc>Aj?RNq6Hb&SrilKCN|xM2Npw>ko4U9ZKYze0TQw^-Kpj2&Tf~I&`^o%)SeGp6 z*-D}(`lFQ~VJ$hpp0vg6`ip!pY^n;E788u_i=|AidxX7!ZmUt$&ui-PB8xK=!|VOMd7^iPD8a(KNKV1c;Zk zTJiG`E`^j=yF$_Di}>w6ZQ$jC%Ywh8iQ&)j+wYgwmV3AeMW_DBZzpRO+6SwW)N2zm ztvA2@m#dO`h)^_ngG+bN$(0i#ze<+(5$8oRHoe;MLrJ-kYn4(Sd5k4PIPGm)vVS7o zieu{3@u038HI5&us*-yfCDGebu{elt)!!e1#4Pq6Eo1E%_zRZk)Gg0nmt3wSnl4Vv zMo4!~h&;`AVHolma(2td<}m{TlYF@VyezY1i=OX@ATcz>l_Rj-)rS?Psh`l4Mpp8%*tmRUz==jcfyz zRam%>^=T<`@3HhCf_+%Al&{7X;enVFz@GP>48*3@F97&@V0v$sB|QbU(a8E_wUsfs zXfk3Gx3iw0P#+@cG*i&Ad?bc#K~9YoHtqySq5r(>uqwq+>GCoPF!^g7n13s`c$5ui z_CaJ~^;c2ysYhlZCTwG+K$WPqrblxyE0z^2bTZwpH8zkQzq{E^fH_&$F)A8&AE*?< z(BP-z&#+bPu9^I`h7re_TdH`(IG%?o-_+j_lcH@Aa5xKn*C94kY5T%Rc_m6KggY3e z<^Po=lPuYG(Y$iWyNUiAuzz3>mIuR)SN|=pL(6tuN{{}4IN2GkbA%z`1JTTIS72qg!P@;`V zt34+?o=t&dkJ9s5m4ob?URK8ae?=Sly@(xLvTtBy^rQbmeV*gq+nQ@fqx@tP!`JD=0wzLa11x9 zP~M7~cyKh+d4bNVxf_?CfurTmyYd^s8QoLWLU=l%ri0loOJQw+*WH*_8|uzL;E!>lKLFiYp;lvWQ3L9fZdwA=>n4s0-KuD7#b7qu9Il73I3 zwgY=N@I{IpS?Y0leS z1NxH?ap~N=7|13)6 z(1s4P(>rm-ALcV%-}T^TjsC#;=fl-9_^e@Pu76fR>)AuG7n*|)y~qk@gK;`;K*z$N z0*UR?=aT0}@2sAt?CFRXUAWtX+s|1j%Za1Q1m%56 zRbaKVhbBt?G^Ais*MuxiD<$1aC{592!hqD^p@5+Y>MC+GILeQ7Lur=TrvDJ- xz2snUlq2ZcLpKx(3d;L)5iOr)N>B$Y`~j@$3al@Zu}1&^002ovPDHLkV1j$|x-0+y delta 767 zcmVC4fR#I9~NkmbB5QRYs30jC9pcE3MRCZw+oeD}CQ4w_MS{dc$k=L$0uQSfHYbbo;1Xry>S&9vmh~+O){f^n$cd^%qDc<7C z`APWj1Gis0;(zWjB0fyKmewPy9IYMvHKz(^s*xIt)2rY$j(I5cMH?{l6gqpAxgKsf zwp6?l;y{HokO3>sKSkXqJj`V8V<_Fb49Iz)%-uK}6Lh@Y(1L?Eq)XFt#R?Y3A&9%0CynJyfut}v|#fiFonh%cL;+HR{ zU_A~eVEcTw95_+-F4fuPl1|*il5iNN^H)mCquqG(4T}rd+P%CP$5fnkdF$7XU6dfO6_)3K`6BEq` zJ`*32nyF1?PBZyOW}1zoS!$-KsHEj13pHOTqiCA8;sXqoA{8*y1XK{@<@P(ev3J>X z&fUA)4EW9cXAd+xXAocsMA_v#D=0~AQgz(ze%{y<4N>VFoIL6{VQ5uMQ3)nwGw zoZT(M&;+Dr*8wvwhzT8wQm}0Ty0kDFjdYJ?W6)ZZRMZ(c5wiY4*b=N6g{eK+PezyK zwSICn!U5BEAjB6@!H?`v+USAE@NgMN6s4?G)kT7R1;jNWdwTOAxQxC8o!VATnH znu3A{T5qnAu8%wRO+iGUu!D<}VrVCRA6I9*7lpXzFnB#qU)OYhEp)s3V|P6Kn#lT8 zJu;nFNS`g=Mo6B9DciCAy!8~UrW@pq1Jl^-%As7}!cqi!$vS(uVrv{oNOoAw*ebe# zo;W%~oqsDy*pKO^pFd@wvme{95>V^9QOGUADeHEHRdm;nfxoA$1L@SaXRz`Y{C?A9 zoS%%tS(w=e!#kpZ_&zkpd)`3kXSid+eEi- zYrZ`tODi`%_Fv-DZP!e8m5^>n-cBH754 z+byGHC4_X9g|r$}RBP5+5$+3g(9K15mA3F)$9wo}gQHDD3F%Uh)wn@zBWVR`&D}Cv z?o! z+Vz&Q;_c3g3>0hIgGGb*IUJ=VXY*ti3h9d+iJCnI_d z9%`v8w((&F08wEj@eD4j)y`ye$=}Z1hb`mzt&u@Uor&0uD$-Mf-mS2GBA)T!w^Bwu z@1Xi1Do*&jvxw<{v0?mHO0Cl8VEt+AOlQ@uM+QzGXGcVbV#f1K79}MT%76Z?`P=F- zL;O}II>9`bfyeRx>?n3nma!}q%a5B#yXx@K09KI}<2aNh<6WOM)>Ef z3?oUWl-X!ml`Pgi@+U@a(p*5-7M}lAfZofodL&{y^E*^2wqQv)Ka(jg$A^3I)d?7Q zN+O5mcN65vXjZ!Vvx0qWC%Bjokgv$(?v&tJ^E~lZF*fO$>&mmWs()hRo1e5iCKt2Z zSTSZqC-%0&#ffRi4;M^*793)JfxLx`z9Ulk~C>+qy&Rdkiz9FgS zAfGzAGjxqz*|#}iOJ&$MB=s4eL?g8M<427==bBOMdzX5PsXg#cBs(!G1I0}tzU+IK z{QGrD%M!E)XkMRig;L0(f>{kdQO~|1(dV#l9qJaO1N3asex*H7gMR^kPXj(!L;nW= O0000b%yCfQ)wfS@35E#{iIf{LKBNx$RM?(@Ch zId^%Fnnu3!{dwP$<%6gI zJlqq-Cy~BOYj5X^*^#Kbj$e-Ar^BeObNx~-;CcWfdt>roJk|%k-dg)V#H$hz8+7xy zvKn9S$LD$Y@sQ;U3R%RZ$GPwc1b239j||XoL%L|~t(zl0MEnYlRd$cH5JT~K^brSUvHcdiwFQ(VC8Ro|# z<};m1RJadz64sGb<{qS&VYTT>EgoNv9W%tWYwk?SX>77I(QaECvOYcY@!>W zu!Q`JOj6u|-TC${zgoa!wCxWDmqfeG3U1$fnZ~+$vGUxal2d#;xeSGPT(4$g4LR#v zm48TP+#>SZto&Q6M)9~Uym{|HhSOcTEQt_Wdh_izNfv`-H#r+i%c2)(=E}E5v8@ye zHZ(AX#-B57*2G##Qk60;<8kX7L?Y!pBL#b1{P5RB3wVwS@aNmlR;orxEdxa)>;s?} zEz=#igCwEZ;WE`IDQ-zALc22UB4gWQ3V)0dWt}5>^IiLthEj^V;|L}WV%o>{$IL8L z)|kR`)W|?|;0zYU_&blQ21;?iIVc8uI_+OSU(AfaJ2@60HN7#MZ_hs_5>kbvxTi0R zB0y9Rrfbe{k?kBUXS$4tqC|?HSY_%3PpWu#X&#T;$_J|xMEZKoe3dS^GnS0yyMM@f z+E)Zwsc;XLK(G? z*6qRae0^3!xD+(Tr6PB_&Q-~dz8xbx{=(dwEi2-s7QkM6Phr9uBd9`#YGh=(pG_A< z4R5`OtY1g%IE-_O4x-HO(NCqpq_d1xAhY&j^io0G)>n1fUAM|gbYqHYseeu66h<%8 zw+8kyQQzJ28-^{xhKcAQ38r8d#n#sIPSGZcPHA_(yxz3sHdj5TL@i{2Sfek7^Crpu z^G;umN$VFczxi%%mCl~DVF>G>*0T)NxE+p)4h6xOZD z;+LO4%P@JZDCbasHl{DaA7c=S#-a0AW#>ibevhS3i&&8;bAHFV67hk}MY;4D@X|G` z*eQOv;SL-sQE7#uVnDgR8xvBuePm^;X0DXdSlxNwg#S%7;Wnn4a2r#91L=jU%qYq@ Q@Bjb+07*qoM6N<$f{9M`p8x;= diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png index 295741a1bdbfc40b3cfd1ee49470b7f01bb43f36..e8b1c78167efafa9589a5ea838c1970820826a0b 100644 GIT binary patch delta 2613 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 XmQn9uDIjD<00000NkvXXu0mjfvbqC3 delta 2321 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 Ef;9U))Bpeg delta 1013 zcmVnNpUTW?|8NnNdklY6nzM zd3D%96e5&%d?^b05Ps;Q=-NopE&`>}%8ZW1BU6eFDmPTpQc^41R#Q{A=WV3jcXr&J zF{>~BkzwbVXZ_7Q`@a9@83$}L;(y5DzfgO@y9(6`yCd0!S+A=)- z@OQuqgRpBJX8Xg=j-6J0K~5}et(Otv*7N8m~^%p6ocmmK(k7k`mzRhbnL^E_b9H!G#`na$VvF{6-n0> zf(JSxJqCU3xuDUA%oj+$hoZ{P$k%2}Kf;z+!fdRa27CT4x6j309d5r-0zMFdVXj=H z;X77eK-MdXd#L$BIr%+NQeeZ&( z3?avmp8!8IcHm&dPQm%d^1z#dq2`MmpJ9V4s9MjeqK)s|>%ae-T=2??+$kYbi)~W{ zbeToDJRy*|@V!cxRw%e|3XstioNP21Pp zTY|h~fPY7NaHp;9sQjV~tZzY`0bT=4k)bXUX<~5yKHO>Z4|HQBqg`MQ?x&UkZsYfy zlbRLHit|aziNAYU`?$W5ziiL;W%(y1R5e@1=Bl$;(h^`@oj7BTzcMhbVUc(0>Sch7 ztI*Z~2eWUI^`yw&$N1ILM0)g22ADj#paOFOOn(s?0qCK%<+7+_ybwK^i@f=WGC5C7 zB8M{vlmDHMLhupPHCpC!a&g4PSh3w|ilmbzz!#rj?*fb%WQzC?LhcqU*5G|ZmsGO6 zj5tj2=7P1~aJoP?m`sDZLe@sk>#6C!*nz_jnQy=2EoF!c>ja6R?1@F%l?{W`lp+l# z-G6+|7~4a+pld(u568YR*4`u2@VN<&4!F7&5kcZ*EQl(XBGs3yfk``Wux_zrtT;|( zm_Hg}qjBjeTMCOU5(M3Ui}jbgq*?>F8?ho4DN$^CvXn4)TGmz&wS?$2v{)UmtxcKq z&8RfiVh=8Yb6<&sCX3&XTzbzf9M#%(cy8MR{fRteJZF>oy6J3a6+{yxsYY)~t2ZFE z1cx55IYf?Ry}}S6@j7;AW0oIl*km#}zh*~zUy8fLoS6o${0e4y@}=OVjhf7ZGWKUK jMQ++naL=k2{Ll0g3@j(^y~}aP00000NkvXXu0mjfe;@Ob diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png index 6207bba1bc37cbfbebca8879feedb103294a40e8..8eb9e99e962b20c69f1ef32b335cdc65dd042a27 100644 GIT binary patch delta 2391 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 baY6nM#h?T*Ar6;{00000NkvXXu0mjfL~PnP literal 3176 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 baY6nM#h?T*Ar6;{00000NkvXXu0mjfL~PnP literal 3176 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 Xk49Q;y(j+WR76JlW_qnU?n(ax0O(Aj diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png index 563187384fd29a83eb0bfcfea65c3d7c71f9789c..4f71e4267d8480fedb16920bf1218545b362c8b5 100644 GIT binary patch delta 2259 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 literal 4461 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 TT4rwED7UoL_0_6WY(oAAreLo; diff --git a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json index a2ec33f19..7b4d860d6 100644 --- a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,68 +1,68 @@ { - "images" : [ + "images": [ { - "size" : "16x16", - "idiom" : "mac", - "filename" : "app_icon_16.png", - "scale" : "1x" + "filename": "app_icon_16.png", + "idiom": "mac", + "scale": "1x", + "size": "16x16" }, { - "size" : "16x16", - "idiom" : "mac", - "filename" : "app_icon_32.png", - "scale" : "2x" + "filename": "app_icon_32.png", + "idiom": "mac", + "scale": "2x", + "size": "16x16" }, { - "size" : "32x32", - "idiom" : "mac", - "filename" : "app_icon_32.png", - "scale" : "1x" + "filename": "app_icon_32.png", + "idiom": "mac", + "scale": "1x", + "size": "32x32" }, { - "size" : "32x32", - "idiom" : "mac", - "filename" : "app_icon_64.png", - "scale" : "2x" + "filename": "app_icon_64.png", + "idiom": "mac", + "scale": "2x", + "size": "32x32" }, { - "size" : "128x128", - "idiom" : "mac", - "filename" : "app_icon_128.png", - "scale" : "1x" + "filename": "app_icon_128.png", + "idiom": "mac", + "scale": "1x", + "size": "128x128" }, { - "size" : "128x128", - "idiom" : "mac", - "filename" : "app_icon_256.png", - "scale" : "2x" + "filename": "app_icon_256.png", + "idiom": "mac", + "scale": "2x", + "size": "128x128" }, { - "size" : "256x256", - "idiom" : "mac", - "filename" : "app_icon_256.png", - "scale" : "1x" + "filename": "app_icon_256.png", + "idiom": "mac", + "scale": "1x", + "size": "256x256" }, { - "size" : "256x256", - "idiom" : "mac", - "filename" : "app_icon_512.png", - "scale" : "2x" + "filename": "app_icon_512.png", + "idiom": "mac", + "scale": "2x", + "size": "256x256" }, { - "size" : "512x512", - "idiom" : "mac", - "filename" : "app_icon_512.png", - "scale" : "1x" + "filename": "app_icon_512.png", + "idiom": "mac", + "scale": "1x", + "size": "512x512" }, { - "size" : "512x512", - "idiom" : "mac", - "filename" : "app_icon_1024.png", - "scale" : "2x" + "filename": "app_icon_1024.png", + "idiom": "mac", + "scale": "2x", + "size": "512x512" } ], - "info" : { - "version" : 1, - "author" : "xcode" + "info": { + "author": "icons_launcher", + "version": 1 } -} +} \ No newline at end of file diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index e3458b57a..a05cd779a 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -80,7 +80,7 @@ dependencies: rxdart: ^0.27.5 dev_dependencies: - flutter_launcher_icons: ^0.9.1 + icons_launcher: ^2.0.5 flutter_test: sdk: flutter build_runner: ^2.1.11 @@ -88,10 +88,19 @@ dev_dependencies: flutter_lints: ^2.0.0 # rerun: flutter pub run flutter_launcher_icons:main -flutter_icons: - android: "ic_launcher" - ios: true - image_path: "../1024-rec.png" +icons_launcher: + image_path: "../128x128@2x.png" + platforms: + android: + enable: true + ios: + enable: true + windows: + enable: true + macos: + enable: true + linux: + enable: true # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/flutter/windows/runner/resources/app_icon.ico b/flutter/windows/runner/resources/app_icon.ico index c04e20caf6370ebb9253ad831cc31de4a9c965f6..9b52c497eeb83afb5e271c8d9e33caf0675949ab 100644 GIT binary patch literal 21592 zcmag_WmFu|(gq6841>D`cXt9I1ef3r!GaUq-EGj|?ry=|oq-S}xVyW1aJZavzVF{% z_w7HutEzWZ_u5sppQ^PR0006I02mm6_lq2W4FUi{-beWO|D%Or0D!c29y$5{Xc900 zum}qPu(SV+ zm2n3A_N=I3r6)1#bP$rsQp3o0;aNEHfnkW$h<%8d)^U@x)Ju(aQ>K_=0iR(uWd_AX zkuw}HIH$st45h?>(MmEhV1EnY-S#oM?#DxT*>qUnUVS^y^coW&O#Ab&z^_=FoCAzs z%%Kb_?)RA7ns9bn6^!~wbM9`P?JFN+t&|SS`k}X+hrXfLpZa3__jzB`)-2YvZ7Bra zbx?wLn5p?cXHA?2lOFb8 zTDMFCQe7aSJ#YAue+lFUv_r0vy;0l9;dy`Bq&CgIhu<600u927_&YKy3hUP)yJc& z>q$K|ro|!7QLc%j0hELVME#&X!dQoEK}ODG8P{>iXdS3z`On&1VaFg7^X3L;*K$We zdQnpv8GP!!o>%A?4mlqwekFYGKj3ndurjaGHR+dCfQ}Dv0dD3cex}QK3{Jr$4jcG` z1tON;e~n%E=VI~#sObm1pE*n$D1at=2!H<^JSkb>a(6koNVVQhj^v(-%=`(c-`;_d z`aveU8hc0JMc6{XKBV_@9u7wku;vZ#$_BSZb)P-tT*kyBY#GL(gXB+zqwhCRsoBPW z@=JKy>@rLDY?qd@a3Nz3&??cDO^j--0ALj)5QBKsdAHTLt;8m@_sSFxwWCo>fUfB- z8#DVTz*9!RD5_5yz0q}Hz^@}#M(n5a(ggUZd^y<ZS)Wtv>~Xf9kwy{R0aJ`E3nKH!x z&y*5M=Bm_=p?B13ggRA>1KVtAzU4;l_fBI@VDe>n_C-Tr_@c4_Y*KU*r+Goh{p{v1YWl4bx`CcrIyb+$-0$cl-!&)Q4ZrwI8dF=!K{2%c{{w^o~Z~QoUdfvRp&(8ni z=T8c%b+pENO5L?kL*zo8o{{`5vwO=UDj`!u_{nf|Y1WhqTcyWaojQS~r?;;yO6rKI zUOG87IsS3Mv?-S`Z?Av_^>Q|hJnFM~+IZITK*oNn&w6vG9p0&s$5q!p|DE8z-#O1+ zrV~rYLCPg5%swmiUrwk>B zg5_hxi$z*;1^=(G$m391T{#a4f12g4+@UBh3f5-5hYk<&CN&g%_{?Ht?@#z^r?BPd zzt^x1RuO$uQ8bvy6?eQnvb)%eFbvZoJ(-ZYvZ!m9@I*zeSLy7Q!TtR`d;%x?MvKE& zGWS;+_lJoDfpCa&q+spoJo(frbo53)?b!#>X0(g#pRrSl2rnq;Z!ctEQ4)n~?Qe(@cJKq2M_xWA6Oz95W`jDNOz4e~&o;GLO_IyaIsFs@+L z2#@h&;^aQdaynt8zIAmDG-P*V*|3A^Kie6W1QI4PAh@`?)Hs!Nlo=kEOgr}K18wi z87#T)@$E>IabyLNjqnR*Jx$mse$lbQ@R4HL8Kk#N$e?WIA6k{VNt^d4+NrUpU(G)I zeGNa8;vI`oT|D7}Q$*blyP!cYSS?Q&9F7|)rH=zMt*m;KOz~gR*yLR{T|z(j*~R%% zFpM?3Gg#9@ey0Rd=f*mrdCCN9bBWO9y`mplwo743rG%J>cD&h@)K$7x2+l0Wdb9Yf zcUo$uY&i?-xns+w=BSl_`R<3qaZMBPS?MdG_0Z=WNJ0F(2HkJ5nCV=1Z5EdwEA+9o zIb=0f{)jlhS)JKsf|S)HQoD@RbDDwWT^Fyo7ll@tA?`u~0r$x&ph#|iS!a3aaXe6U zAVat0n6Xd%`YM3qC|yUzcu4%ReW_i`N!VK3QFfII?~h&jv#&b?x9nn5puOJ`Rx3TJ zUG(vT-+>XSC_nJY_Wwu@|3P%@|4j}WpogCT0G#IklEbW)mr{~A&Koc%hYC;nQ39N79Lu<7|K z59HPPq2ZJ+W4d+_@Ah@yJC49-(=Pv3Gq>*RWdW~$ne{pTf7t^dgNR=zY>rfeZaq_d z5%UJf8YBD;9eR>7qBAZ(;TfdoBjr`Zk z;1j|?O=OhjWNE}^lZWI^?$X@Q=OZp_SudA>&9xdAT+FbreiFNH+?%1%oASge2}VuU zD49%EuU_U3&jiWisjV6tm~jPDA8BQzv!~M9a5;1J}AWwAP*CK75Sk7Nzny& zd;k#|KmzsB_MzIs_1}PlmO0>t4=uSP4DM75phe~-$rjD>I*t*^caXEkju?A{v*Y4E zzx3$~3AwbFBn$tO+qgaVziSR}=|Qu`#FuEw<_Y7qI7#19xxBM1by!>PXUA{U-c&&nh~0!>Y)ajw3L z9J%t~=$Q}$RNORPI`6zU(z<>)xuK`rnK@~vqu}U(n)Z*)i=+*tu}C~wcCNhf*poWO zl7&Wg%`9NomYB08`2Z2v{;YX>HZal!H^qlOcE=ucvLK&jL^8IouE5hMliq=2gJs0r zbxCMj7}O|hbUBC{Hv3eu%C-I0JsGSQoiN-rOvT+Y)kW6r_%-%5vg5mq;`L`X6-a&T zSePSnb0IZ`1iYfChSTDq>>9&)PvtVS&pB7pN(S`1YhgzxX!8u3ZxMLB?d6q5S>+w z5l`-{um;!$P1NaH5`~SVafaw8L+wsB!hdu~+?77bAeKC{!RvD&rj=?P_QYbfc;3IQO0)VNtzH!rBNje$&T6YOZAUiWS|$Pj7_rTUMSX*3}NH9l1ER*pNtT z9f8@E9+`#V1fif^^%ozOl=PqYUo|+r7#_dMfUM{_)3MRPsm6wxnapdjClhk~BhaD* zEF)H<>&=}(BpDE_ICNeTqD=b{Ghyr5{YGY^c$;u(4VJaiKKzJHTG``Pjjjeh`2m{A zY+%fNJvt)oKKIGMl=fms;W(vLUka)F0h~5Q-g;Pbotp8BCtEErut7QH8N~kxoGj;Rrx`9cC%?j6m2~)@3>5HaN_?MV^|U0%yOQccBgX zXJeMMm6*QKJ}W6t7jFxMo3QYi#2mZAcKC1I5Ie9+=9u1ZD*%7pHrcG4W5Ce4`T+{! zNGK$={ByL5IAOb{9`juft?&$JY6A|(xlBQ6vMSKoCTrJ99Pqdf;reX1KeP}o|G5F4 zpJIA0o{rB{BLayXh^EQ)p_OrA+4?Um?JZ2v04R$Ix2&b&pmk(pYU^REOziHJLFSm? z=@80zr{^)3g=p`)JIjK)%rtL%Z^$-VA64T~h3BCpfz;1}^~=4?x==^!T~o&yoq1f&d|qQT>_ za;=alTr|Ub`;2pMxWLd1rpH5u>2!Ldu6|;V6}qDg+qc<4&rL2y%;X7zJGjoYL&Kdm z3a>HzyrA=Up;E*!lAq?I9%xVW?pKOJKc8UMJ;+;s=D#N0^aCnQ1Jqa5um6t?N%&nZ z{ND|!%)(&}06-xA--fiS?WU9{iSw3MY;e917h2ngrH#XE-j8TNQJb5uJSj%kWKJ9C zN3B5F1m+^t&hN*sHH`X^Q%j^ev5=o%i$V0+x;BFAympP4c%ZpmgB+$i&F<{1z4^4c z-EQ5h-70_ur*tcN$>;0FbS*ajF?T*U?!l1Vw|{RDIG$A$4+j*~{T1S(Gf4a`sXthZ z|Hh8Esk+@u$b}Fwnu@dNBDi^Wl)^k5irPGqUSd*V2zgp)Q6K}){#=n8&&y$vCeCt= zV%h(E6c?K=`8QBD_chk>9Jjj(BQ}93=R@`_yo$EIk#u0s^EZqQKE_WPpVm-p%g;+z zKanJ7sNiPX^`S577Bj;)^H4DMi!F6op((jP+JB|7Z4XNn9XP3yZSr5T^{gM;S5R91 zj;KC(=4njD$PQ#}yjVG~&>=8?LyKyef+p&|W3EG!t9{rmOBhvVG~zat6Mp#2*2BMA z-^0hu5qS}3hXFYcKmkI~ZiLpFpC(ZWf%NAipW$|kEQtST-yN`P>$=ayU{b{3)pui$ zN@I2^nc;-o(i3E~49YqNdaQ7~>d(NKAHnuz)^uE7kI`!s6SL>RU|=GQP+|4k!Bz>` zyV_()L4Hbq@=oo>pOT>e&i-dLLhfe=|UB_)=tSKrI6rAC1%s=K!Z|h>2vMo0YW?vU?Ql(k!S~ zY8gIZ(pZJ~yB^J;Is(npk8Dq$DbWmw9Sp=Xy#JczD5;to)J+1=lfhZNPJu3AZdXPf3mFOE79;`g|l5S?8t1VnPTNZ5xWJ* z;KdiG;iFXW!12$hmS}-%^0upD1DMvh3ElH*I~IHU6NDoUKZ#3iYcQKKYgZw#2cZ-p z^4PuEdaxoTgv?JR?c z1eKWb&*8_ZfX?W_gpqS5X1`@c=nlb>h2Wa0O{wTg{B8nN>fu68<>n5Zy(!)Q7~uC| z<+x!@Yei`8<DgKTBo0 zsEjaBe4K;c(#CNwsE?R^4;}Hubx#Gk%Yf3n#$j@*$K3zUD1SN4-0&fc{yi7_V}&ZZ z^9oTpM$WOh3Gtp>d{*w{dBLxbqgl_WTM=GITrW1$Rqy^po+J4^!RXWd0FziokcT|r zujZr#$;W=0Ke?>xL5cZoQf*E~p>Sl5w8WCd&?17rEEfTKe1R( zE(`{Q-C?Ib!kxQCmZ)@ z5S^I8eN&s}`Yq3n87@u^tGw`it+e$OUX2^q2x-n&=qwXBFDwOf+S>cF)C)Y3ety`4J+SPN}s+A`9Bl|`_xf$}&&;du`W@Plfzn#He8QkOAe}Op^A`g5ya;do+031%3DFD* z=xdnp0pq`PixI8{gD=4BTmLV#oz5>=>94-C82Y}-4bGoTj8-ox+1>Ci`W2j#slOtc zPh#9#a!g3&m7YJIUhUhy=2!W}_v;;^x70@YY|$M(Hp2c!FxP}%;7~Z-!Dd}&_N%H; zEd(`)kPQQqsmspWsZl5(IOUaW_N}@!V>)xmtK<5+iXt|_dEml=4x#ssd*egAapG1{ zCgf=Le#gzCs?BWUci_^#g?=a>QjnX9m%yX`8p2jzwY2iG+s8!g3r20Bc7`Q;#9(Vj z^lUgm88~&)x|h!GzS@^P6!s}H1oN?T12s){@Io5!*;!sU=Z}&Mz#{eSq@bv}kIdiY zr0PkZ(Eic!&w97-&#*ScXY>&5w1im3;{7ch2pTx8;0&} z@i>RD)kd!e%uAf^z}2I{C@W#@rjKuNr^twoFNwh%AXC(h533LuvKza;Tc;K_QJV19PkMjM{|0))u-U-~zI@Pp ztguON6EE5BmBQ~!ZZ%yL-0h}*zj*WEiXsxZ)I7ux)MX(^QvST~9 z+YcGf_*T}JV(u6T=KnhT&P_W*e}G38dtl+dOJNFQp%1N6BIpghbf{BASj_l*fU)4b zv6?WZ3qPokpp2(9P?daHqwk%fHu$N_VBNo^O3H4B_l*?(z>cq$ujU@obSTq

    qBHf!`sN{X^^8`|Z%7S+=4vmx1fjEsF@{4Li4=H+xS(_pVi8*?zU z`I+hC$2M(QWW?N5lFr-#HHqI`NVPxx{qAO~|9oSPXAVLt&zJT4{Vxg*NOvu!ZW@t|UGb_57( z6=yc8W)_P4T~D4)c;}3pb4nPzvq0AwBzNR_0j459Y`Av`Mkt|2Q9`C;i4?5IGhUyR zg%lg#qELX+5iyhqw%D0O=f)qGL+SHrD8v9Bp*NJxSVON(_7wVl8Im+40;1p<*U9z0 z<^KorI`949eU#$g=XB=?^$0*nX+FH4Y3)VPg&}0l==1*vOF^O}%XpLW{1_ zrQ2Uo>Yx?t79{4%y2}DlaOf9Gep1K}vonxkqK55|?zd2gEGV5U*iXYX)O~9!)ttpXArmh(BK}>!SgGat5H?qZ6igjFPVyOxuyAr=0E3utdX&!U zQy%N`hJ;I5<|{zd9zHi4l6z5UEZTwrH0Eqo%N3fp@)q8uF&H7cX{H#@wuV+=01Im{ zt`!gEKAx_iTwDUCn9{x#B@vB}%}1!hDwFi0rL7V?B9hCU0mW=^I?F!hBEZMxZmkLp z>T9P091TV=5Tpy0k}Wc#q?xdQNuThUYMwF7(TpkNghgp%Y|E?b=a2}hX&P@V$GakGSaHtsjCI35BM_Dqtlg>|iG`L{(y%PJ>hW z+;e!#YwuH_$*?s(%jbunMuwx29BpX}VahbR#1Tn?Bvn%3J|Y?rBUYUtvH84M zg}^E^&R7Z36xetVm{a^w{z|&`AGIG@#+crlmG{I4amRs>q1{@WXTm}CgLU0OW%6{A_6_6_@(8v zG>1OV=lLF0eUBt^ONG?-rgFf7cN-}Or6dz>hVXA^D@9WaZ#zQ1{d5i&5wxrp&*d%n zngWAZJaeau^7ZemwO~K0E%MK@(-eeXB`0+moDUh|+d4u;FA%MmEk-A(di}Ys`+AlFEEZ;wL#xSsGq*;^El8LBC@)Fkal|9nN#yKcO z5`L=}VDB(re^j+?qHR}%iAvA$%)9+m>Ui_b`O_{NoowIJaWI1kQ};dEMGV{rl)dAQN!Yae`v6=nE!5k z>~#Lf@NM1LX&FV0bka(^GhdnOG6cetafM57mS!oLgDW+n1S=cvmcYbjOf%l_piSO9 zyFfLhBv&?PJXz;Z3>D;HNDmxABBQ=)U$>lyMK-|1+(9k4GENZM#zimPAj9=)gk@YS z-bKh0$fZoU2Nuc6uY;%wOH>haA+*_3`#2i+Gw|uET4)Djg<1uP0C}~R0|AlsLW|N?8@l3u0I|g`&62fd`CKOH}^+s{CBr0N3%? zjX1=v{P*V_%p&xWkMEJrc(Bi8%P20@mhZnXipDX~SNMrmIYE6DDhw2D8>r>il=$8sQ7c_J!$0@>Eiy zwk#6s9{)E#+SKLj$7J4LB({#b75yFMZrM0v#M_E`4oC_kYM0{5jvEH=MNdJw;zTgPYM4%t0xrXXXLW^cZ*9=`~qc$T+C{(de*lK~6ud*53FFUIbegqF!Na)5lVEc(9OVVXdkaG-4~sHK2(LhFUu!S`UlQ8|X?v>JRxd9m#0 z)U44&U-RBV-ir5o_De(dU$#6vZgqt=*(FJR2G(-}ywHYyNSi~y09BcRC^jf7Nx2g2 zG?h7uSggRd@}aYe7)L*a6kan8U;3&tF#n{MK2sM!PYb4_9`}m}V@I8PR468t#sq>n z3h64_8fOzOJysji258_Ju+HR%bakm11)EJl<~goxO*%sNVWCA2Y}_z`M2B5nxqaV9 zTzXrdoVM#6%V?zFNG;GJ_23G0$&dWi54~{=b)UR4d~gQXyNWh(~}&H!AM=E z7Wzh0dUkILc9SSRlR;!f1PDw6tj~<|=!U~Ie#C%0+7kWQP}Yr{@rJkED2@)^dm_bk zgHc0mFUYRBpQ^hx~sE6H3&h~~oa<2Q~Hu9*i11B?`%TW1e;oRM8>(7b$ z-k?+j_UE`XqK$nTu(&s3kAVHS?Rbp;Vxa zu9ux?)AhjXqm((&^i1P%tbjJoXb}31n)ayrnX2ePih=8d%ra*?%xF=nSB%mZN{B`q z$Rs?y_T?JUprfqBOd~x_#pVId-5&E|2dT43=td$pGjDvVftOvZF+M>fqm~S58+7np zvE3xiVDhtpJ!8$AB)#FrCyt%J?`|rWs7hmJA02`BR#qF8ENy&_paM`!P0edv zp`nwIvA=7FJt5kM*7=+|k-O#Dzgpe2LN!bq!ih$hbnJ|fpBc;74T!L-)mq&Zd#CF| zOQUQzI9>ldHe9%3?7V(eoRV4scv%77b^3F*^pS`Vm~QxZW1FgFm=)etBCCU-RnaF6 z|I(qQ7!f2_7H3`?KHV`>TYPN5YCd%qullLF+v_KRgKI3Ep6w58ZrC)%t<7cXztm|A zj+-4ne>D^X+*A|INR6!}!yMVV|M5!7$;z`UT@?A)8A|;pa1^p;H@Ju#j0v0wXR_hcfX=hkApu8 zQ9Hrdf0*6otv?2BfpUL;ot@%nYQbv$@iUU8c)>^WT;L)T0v4c>2RM7hL!PcOy&~mRAt!zXF2nHShR)ED7`~hmo&woLe!FTVGg{M1# zsz>%vpD8xj9Y2}Ey>ihvKy72lai_IA7LSlXxUC)0AOmAJnY6nITopmiq&F);b$n-@=Pyl!ED z2o4Lg5gi{TC^oeFdOhKvjXQ3s4{BIcQ)Xyt;ums5I)t6pfG|`JOrTW7>hw#PXn2S& zG!qB!D*AO*n2CWBq962Vmrce`%|Ct%!U{D0@gtL(62LG{!b&e*0ANJSiso=2mk6R3P5jzbjpBY-EJ7^J6p`mX$h5z`%~qqXgYQh6ds0x+d#2Oaa8oS;6?wyU+H+_^HtjnzGU{$QL82x8;41hG5WJ9?j&>FXsWd|Y&)VKw@P1h?k z_|s*?;8cL-!=ZkuV0DO(b|m!n-fImJ_8Ue^GX*ZS(V{B>UmnJ)fli&g1b`p$TIpMs zJ%fs2^jBU7nHp*pFcc-7ts%wYAAX|4QTiYvS{z9c)bLp!aiOk@hyi5KbT(T?WbWAN zqirop=M3iUFkMfcmOIWLX6uO`r(_lzDJS3nk)K)}La;gKh8LVb5|Rm#k?9(3+K7Fy z?0%f=Rh>HgtI{Mc?+$`Y5l_vy`KHUx!8re#R<}m|9*onVuZ8@c36TQ4nXE_1pR#x4 zXtfwCdRQlM6Qiu*Q&gbZ(nk>Yfj{5q3}yv*71*g9Jc2)PxeyYUwuFjgBY(jrn$#>=FjEJ?91ZUw zZeV>(;BfOHk*XgA8WSqG3^YyYW;rk1+eIH?cqkai0d)R&o#@WSxWqROQzwkH4)7U| zaJZo*+Rr)4j~G@GX?$udpQooP>3d{5dg zZFb9^q-uF?^G!nL4qCM@c(N!rdOc&zZOdws53&CSBG%i%yzuNwyoQ z0Q{xGZ8>tEP-|LDCi)u_UI!n1S1+5bZAX=%VP=8pYM5dbPKe z0lB}8D;uGmR#_de(3%s4n#D1Ayxn+c72eUZdtT(Ubxa`g%w|r;!M^CRPo3&k zw=}4I>xi%d-l=eAJ*H-B<4E&hVcP0R((3xQj2uVYW(){6<&N3ur?03%0yvnnYCsH$ z={-Rqw;4u!guoAdWdVx#(Mnyqs5eXUDG>zp!N(wSloA~~V)p73xrxI7sFYq33it=L zQ_|qm__Zk#@jI@XcxoPs@Qg_E1y9M>C3WTmiME>jR3N$Q+*XJ`w!e%7ASeyE6b-XfQYfPjism>D0v{VkGN(f-mtOCu@9R4Qz})#|gy zBtDaqwEhGTxizYu)V%HFh^XL=gqN*`x_^mZ?V`{%`|-k^bAN&qlFR;gAb9ACSqq7u zObMp+5xUV2J1p1sr?)b*r_}wEKzB#?w`z-;sE%-zYllAC9^WtOYBYv>bpg}di6GORZ@`gxs6&T#~v4}WkU zOsVwjZM2?(My0Ohn!t}*!KP%qmC7B;(PurgYDr=qw2DPk?;2#y1{U5Qc$s_nu+aXG zKftjhtmTfJG%b0eZY&anM!s3dGjz+o81Kc!#i>JZRsXtknR+E?tmzogg65dzwn zr^6ZS(>?f>Qmdg48w~%E>X12K-nH+Cz5!JkH6owglNh+{%qWgP>Kn+uonFv3_b8i1 z7>*Fq#S!B8?a@8)rQ%8RL{X^JH?2U+AJaBva6m+J|9yty+sE&S2C0vuB5v|zcsy}x z`v|N6lgQNwI3rVXI2c`)g1+nV7<+ zk0P_oE9GqgI2z%*+b9*=BR6kPG%pV5RrsvL@mC)Qk<^+49bd1lfyPB|ArI0+GT_n+aSWCUC6^_Na$6@1PogiJFJ`v^ zu2N`nYO5mp+kdV<4ls<2Sk8V`6&zYchp^$3O`{K40J+#R5&T>+EQp5)FLn>Eoqn7m zh{9Zk4XavR6k4J_LiL%icE5e`J^?L{XkUJ?d6F zO`eg&QP}=A!^O}th>iB(1Y58fSxbePavzE%0@_MQFUxV6r5cI+U1;~~%4V9bd%`bm zljCr(p2~_Zhp%mAX1hy4@PX`GYC(YDHn@lTUx|ZQ6pZDG`nD5SQ)#VQ)a|E^ znV2>dCX$2l+hLY+z1l|4#8L?-t|Rc{V?gfDzsfO}+2f%P)~;u?xBHG zF~Ls<$=r2LZe=m!B_#$w+VVkfuuz9*uh{~4X;Uwe3lQxX=HDDr^&jk_h&>41!BBnR z;Fx&o8}m>k8-i~Dac}oE?cB)<(9(i^w%YC_24K9euHhG**E@tNg7FL%Wfyg^E3iXJfcW!mS`f>gUL026d1Ea$&?^Di85*T{DR6 z=>Q7AaX#v&`+(dG+rJUVOgYxOMY;4YK>N8T9_leR1RV%lm0DL?oTVN)*MDAX@k}(l z(i#gaBDCP4UFzKsh2?)cB*@?av^Vv2@Di5Pe{=-D`b`HB* z>>t_z%X|PBKhbQ`A<+8oLizr>Wh6H6Db)cIiyZ!AtVo&8FXQ!0Og~W*kV%{69AIvf{$DTH9_x+Y7rP_wIgxAt zg#$KtFK`bU$CbiSzXiLBDj>7sZGyZ43rxYDt=6SmxT^VHQl@(h`gW5qh|hYjlM=tg z)PgofUhm+{6Uh;J9ICG_Q>#SZSvMC6i~1NRg^pb@F_F(-H)(|53{Ek@6b2jH4q&Lo zDof`xxW~3^@8R)6CE_yW03r7$Z6H_#;lsUWBH+tV_wgq}>i193N!h=CcOOSEw6#jM z>=jHwtU7r2z!OI!yuu+Of>8l0_@)muubRdZ!2r>X@5}DHJ;hzW24JurLGLhuGt)L< zdLvffgRZADJ?<>o#|=#eZRy_>>w-zk5Wk67gXr70; zT@A78CX8Kcxg2)HS8`A;LC`;4!jg+u%KQ*N$2$RXoa%LnM4k}yKWKg@gv&!=>?;_U zK|QBopU)}MZy6?sb3*a$#lXTa2K#d_7FWwW&ca|E`li6oE?oj3qB;kmo*M<1dXqT+Z z-Epx3bqskes-dqWp@2e6>PfhU$ebQUyiicAoHNl64FRs!odSIWHy~!u_GxIgzVs*S zG-mVwF)-Aurb`k5b)Z^DAqKUP3A57vP|VDdmjE>Bp(HN7sDTybmt#Kh12cde19*Ws zav#&=U}~6^d79VqbOIoOBj`==y{g+J>RQ-SMZzmTp&-zM=_HN(u z{YI$Lui|VYPDH^_Sk3)6>*fMMiw%OkbEbk`okf#?gdCC~tpN$ygtk(ukr>j_JE- z8^gaBc~gaqlMUM8)lvIUX;M=MwsF(|D>lb5DHQJv^FhA0%8z825FBCpa5*kFh0EBW zJ`15U@}u5!&?J;)y0C8nnGzrkp=z0Kzm&fEl~4zVAPiQ;ts)b?f$HSPF*$1+(NY3v zAaXYeryfada%MtXY#M=NR11)WU!*Xr1N1; zSe>cVrIK_gK+Io7uYyi~K(=dZg6wrM4;#m9Q|IeXJQ}uG1Wbsx+~Srj+|40&W|bJ( z%2(%6u|-B^G)%zN+-o>hH?iZ}N}%L(eIN||g4Qpw+!yen0ddR(GGOj$#qyzZ>rWUu z>Fqcg>xT1QKHse42JE4*YU%_MKt^x#*~aRysn4(D%OwyQ7OrTc_)fIiq5Ni-| zxJ{N1NWp+yHX$UR zUT6fF=NqXe(YC!jzHpZCSf!Xo46@l`%`FImCb-Ra+(e0Qf2%$QcfM|RKXris5EgZJ z)X2}EJ#M0QulNAaiT%jLM7+s7_j(Y$4ZlG7wVg#hxe!zsjO#-8`ZuCf%df(6*=WZXRhEnjnd*?j8fg3uE~ZK>P3 zVJCIve^Y=t+puRn82P0|m|%0Ae2SA&qR79-5FGt#6sz`4f1MAcA}54G$8VEE6g2d- zQ`8P%{0D0n=~t7%fi573t>9tMj0uz?xi0bRRR0${J3QkwU+Cm(|sh>|a&>?H2VhE~YaD)2q3<;kNFeoLorqGOA z8VLTD^L?{r_rq|2R$v&Mj`)p1h=+D)s!puFsBVzW`;&?doJ=t?`#UB5L>#=}3i1^fnn{u4JzOPg~_h zv;*iO=KpEp%EO`T-v5~~!`L#$zE6l`EG2JbXULWE-Qxem)DHz!-)vZI1l_Ox^~ zf_ry1hof%p4p}Si<7;MlLqP0~^r@-qvGifvB+mUdz~8Fe*9#tFUmEWCGbvPnm^t2A zrBUDBM+JU)p`0>{Cm+#o^AoLFf(a?pC8 zBULATT~#akq$UD;F@IE~Yu>Tn+RqlPP7FuyVzB$2jBnL-T=+RMV5`WP%MYuLin%OS z3*s22M1$0fYPsGx@eRC6&%Ed`#;j@T%Bry==zEHSP|F{IPd={yMOw8nMNsZm?Dm%@ z&kx-F$nwh*UOtquJ512U)yc9pS{1FK-f#o2wZgk=V*IogsE$gt^@)R5u((?cH)Vkl z_`sp|BzMOd_vAn(Z|R7P^_S@0vg7a>f$q{f76D}kZe>G+2WS{qcyZgjuMO&7!=g#i zj>}-;H1ko{E9g!Mfw|#3o_L1I%bX-b-Ju$~aVAQ&pRcVM=-W?pcur+{CSRw_*L+;r zih(RaDZKQ|6PfRK5&Gck3$9g`YB;Ru(|xJe+}9HNxnS!ol_l98M~-#{RQNL`hy5oR z;A|mM4}dbiBnjZlxblZfw(pK6365h#@`^_u$HVpBWFgpjOg?Py}? zzp0N}ambm<(gvB6PJI%S+^7?+27vP13Svbg2%NGnn$cIYE|0P6VL^6r*s;NvdCN7T z%43Y6ve<5BrxWuvVx0TNgk4{PrG`!|@_oArs7U|cLFE5#TlewGnGedEx&bZk=mu6Vvx^Yh>^)epUhsv@R%OPaC_q4r?kwXvZshvZW~6x^MSWA zlmqcP8g$Gm#o!T%Pg3+V5FQDh6CfK6}43&=0|(j z|G5D|-R(kSmX<97Jgalucp^Na`JteM%L$Mj>b`g;NP+QvYYFS9m!2dC2jqm(!GMyYBzwvn zPLC{Opvgl#A93UsA$u1s=wUqq?Tp5mtX>R++c|T-f0AV2t$aLCNtZCaI!w^IQ-J{{ zGC9QlY^e+tRA1T`I}d$IEzQ?9Nw5qt(~}1!fw;dTcpingF@N4qR#|Bvr0XOcB)mKc zOQOLN@~5sqzsfh27+?`aWh?)8AK#w7T%)Z|Z^90iuNYrBUx@);qbl~Hxn9hX*KgWc z)gqCe*ZfVLMWJ0CYlteA;^>M%&$hG?G?r`d`iNJlix%3)h&stq6&$3G@d)6%$2^#H z*Vp#AI9XQ_N(0v{7?o9EV{Nz)kFaMbaQBP{w8iP=7+BpRhwE~TJAN6q7v92J4i#Ub z>DANAHQ2y0MoL#B6!4rlI*E{csIn9$hMFKXgO6n?Ua8;!zO!1wGopHalWP1ifQbCs z%Q&>G2gRBuC&d6%uxOTiH~Eh4V2V7-S%ylV^kK)98myuX6R><6tTTDJcb9XS?UaHk-C9Ck0H}heTG}5=&D78wf_QWiLows%u5ztOq7pdR^Nma>7e^BEE&`^AK$yY&P^a%5vquzNB%pLM-6+(<@(uVOtf>UZ{Uo;r4{*nG zfZkHNt63Yad|@2s)t6#~vtCd!Ei;}+w#|DN{MajY&XuoOi$bvVCAeE$H^QvL;(&!* z#<$?jx6fL7QdxM%utUsyrH0}d&k0shPC~A2LG0KPtHv{f333z3$~#OWwC?SvNvmhoNUwhC58kl#36zv89=3b)fDVS#w6?0eh%yeJDGz1K|cG|pFuc=3O?!G7wo(^5% z+_M_uq8p(ntkl#0OUc_{ru4jRTw^>p5FZZeKP!2hmz4~|%jrk5I%uEWxkpQh%vpJv zS;cykGL^ElmF30!DQXZ7lF-==kEX4zy$@GsFm-&<=JM@i|LeD6qNHLqVp`zJp8ytH z57R+Y)p#B-aKuU;QH(j20TDgHXkjtY?v_tCBOXRm3iTe_%p*{1)+ z17ec7bnnMAU33akUKiT-zM%)pSn)#|pF~o~=QOepU;Fi5TunoqSEpKTSPPDzK%Hb% z@eS6k$0i$^udt%@?m7u3Z?zVfvn)?ESUuPq9epF&A$QDfLnwWd@Dc&?C%gzX%?ccp z8aHuAQaRo;sz}~DH}-uM*W>x-7M1bdC=^*m6sz6a|eyj=rtNEl#kwVZyMJW zaY!S96YFSktTMyHrf_%c;y>gDof!Ya<+b2v0zWA)58m`}ni*j_Lm!PWfu%EP?%yZ< zC6IIXo4V!*F+s+=JNMSrjFqo{W6=K{c#xSH3-yN8b~Hv0LP*|0<_aojIXFzgC0GuZ zKPG8x@UG2927uXhM@i*d^YXnOdm7}PN7Tm;W#lS?8BXwD|MNPN_f?^za~TgHttw%z zZ~a6?p|X7{gJH!!Mek~e$VD18o_RERH6T(OfXcbBr(SwG=C8AFr_dH10@9&Y8`(D< zRCaIj-WN1K47i_+HEDML;H0@LtwWwR^M$7@&oF@GZ%1RJ3NZ#; zY0sz)I7rMC>HBG29?2SM0yO8_j@jx`;yA2vP@kS19eHqOs|#ZY1MKVp-??d+h;OPW z5;diGf*Ntl?(m!9}$ubPx=T9rM`44X)x8UI`jWcBfE5R(rliWD+k#aG*tN zcnJ#$ilKMr*`Sm~2j|}Dv9{1cV3{IgQGB;Kxi8AMbx*Z7$Pn3- zWAlq^F#gAa{rRtH?GImN49dOfFC&irYScpH^9}@5waSyF>_6U$vg*{qtk;SijR6wOUaKd8!!IT_-8_e&`|&N#x!ll{Ag95p>kxG2V$RE%N|!HFU9zq zB!)g(-I9+JaXJN7{Mm2x4~9v_5~4vJsOryAi0Lf5BfB#{60ps~W<)yZMF77)5!?oK zd9rw*N;+8RJ>6$@C1PT;MD)vH6>U-YMhXJo`0Clui9o`6$-fj_^Ze;LRO>?#h<_uw zy6J3hZU>u4y;chS4KZ)Y?#Gv?#-<0r)kKc5Ql{~s>I4_j!@J`SnQx_IBLX^%!HSOr zlYzIx95d)!h0YxYWYjLY)Y385=98bCJIA5cXxFg4q>aEPP%@KnO;CNG;FlxQ;XBIi z6xAkeDr@j_GWN;Z?Z%A`#mbx)0w_wT;;E&7I$Y78O!m9^>in5sur}JQ9^v9xT14Qa zMy}iGQ~f7cjxFQN%m)jr@7P4K2V+670Rk&;R&(C7L;*`*ehXtCeB3;4c`BVzKX+CS z`_A>VutJmL%ZJjn3xwNvgNfU3=23_ll9zNFQwN|?lhuB>4oAN-Gm2bTR1ta1j!;t1 z)$q)GH+!~aG_9XK;rkQ2Fhq9y(YnC5ZI$e6In~pK0p5TC3%Y3!m;O;|DeJx~EO>!l zUvxynHo+&lG*8RdkdGXDBU^!HJM6vIyk=6DD?3A3N+hkcRq}J#KPRUud|*E)JUm!& z%&Go!{@ap3ONLkD!%6}8dObg1gA99Ycmr&b%D?g69pDMR%r;0d*T5T`yy4;w4} zkC3n6f>${FN60(hyUFjceBndiBt8Yj87cpUu|=c=VP_%=!qE~lTSqcThq6xl-=6%CSaFiV`%2H_{zCj0_Jhw#89Oio~neKd4P3J*&(vn(=0 zBj)N~udlLwJ%@F>OH7Z$$6++|Oeg75d z6i^~(#W|31T?&hm7&_OV5qEr}8T47?D;hk2aNS#h>e-|eIeKNi=3(n$No83aO5%;d z{tr@0OYwCW(I0uWCU zMPct5QE-ug&$U*S&S7VNykh8L{?$Vu=c=U^X5?d+u3w!pvBxmJ3#j7&Gfgv-r}%Bk z)gMWe@N!0CwH`Q5r|j$-wKW4gG4=uR-5PI76|8^_Ub8Vp#`BLl3lbZ zvPO!8k!2X>cg~Elr=IVxo~J*a`+9wR=A83c-k-DFd(XM&UI1VKCqM@V;DDtJ09WB} zRaHKiW(GT00brH|0EeTeKVbpbGZg?nK6-j827q-+NFM34gXjqWxJ*a#{b_apGN<-L_m3#8Z26atkEn& ze87Bvv^6vVmM+p+cQ~{u%=NJF>#(d;8{7Q{^rWKWNtf14H}>#&y7$lqmY6xmZryI& z($uy?c5-+cPnt2%)R&(KIWEXww>Cnz{OUpT>W$CbO$h1= z#4BPMkFG1Y)x}Ui+WXr?Z!w!t_hjRq8qTaWpu}FH{MsHlU{>;08goVLm{V<&`itk~ zE_Ys=D(hjiy+5=?=$HGii=Y5)jMe9|wWoD_K07(}edAxh`~LBorOJ!Cf@f{_gNCC| z%{*04ViE!#>@hc1t5bb+NO>ncf@@Dv01K!NxH$3Eg1%)|wLyMDF8^d44lV!_Sr}iEWefOaL z8f?ud3Q%Sen39u|%00W<#!E=-RpGa+H8}{ulxVl4mwpjaU+%2pzmi{3HM)%8vb*~-M9rPUAfGCSos8GUXp02|o~0BTV2l#`>>aFV&_P$ejS;nGwSVP8 zMbOaG7<7eKD>c12VdGH;?2@q7535sa7MN*L@&!m?L`ASG%boY7(&L5imY#EQ$KrBB z4@_tfP5m50(T--qv1BJcD&aiH#b-QC>8#7Fx@3yXlonJI#aEIi=8&ChiVpc#N=5le zM*?rDIdcpawoc5kizv$GEjnveyrp3sY>+5_R5;>`>erS%JolimF=A^EIsAK zsPoVyyUHCgf0aYr&alx`<)eb6Be$m&`JYSuBu=p8j%QlNNp$-5C{b4#RubPb|CAIS zGE=9OFLP7?Hgc{?k45)84biT0k&-C6C%Q}aI~q<(7BL`C#<6HyxaR%!dFx7*o^laG z=!GBF^cwK$IA(sn9y6>60Rw{mYRYkp%$jH z*xQM~+bp)G$_RhtFPYx2HTsWk80+p(uqv9@I9)y{b$7NK53rYL$ezbmRjdXS?V}fj zWxX_feWoLFNm3MG7pMUuFPs$qrQWO9!l2B(SIuy2}S|lHNbHzoE+M2|Zxhjq9+Ws8c{*}x^VAib7SbxJ*Q3EnY5lgI9 z=U^f3IW6T=TWaVj+2N%K3<%Un;CF(wUp`TC&Y|ZjyFu6co^uqDDB#EP?DV5v_dw~E zIRK*BoY9y-G_ToU2V_XCX4nJ32~`czdjT!zwme zGgJ0nOk3U4@IE5JwtM}pwimLjk{ln^*4HMU%Fl4~n(cnsLB}Ja-jUM>xIB%aY;Nq8 z)Fp8dv1tkqKanv<68o@cN|%thj$+f;zGSO7H#b+eMAV8xH$hLggtt?O?;oYEgbq@= zV(u9bbd12^%;?nyk6&$GPI%|+<_mEpJGNfl*`!KV;VfmZWw{n{rnZ51?}FDh8we_L z8OI9nE31skDqJ5Oa_ybn7|5@ui>aC`s34p4ZEu6-s!%{uU45$Zd1=p$^^dZBh zu<*pDDPLW+c>iWO$&Z_*{VSQKg7=YEpS3PssPn1U!lSm6eZIho*{@&20e4Y_lRklKDTUCKI%o4Pc<|G^Xgu$J^Q|B87U;`c1zGwf^-zH*VQ^x+i^OUWE0yd z;{FJq)2w!%`x7yg@>uGFFf-XJl4H`YtUG%0slGKOlXV`q?RP>AEWg#x!b{0RicxGhS!3$p7 zij;{gm!_u@D4$Ox%>>bPtLJ> zwKtYz?T_DR1jN>DkkfGU^<#6sGz|~p*I{y`aZ>^Di#TC|Z!7j_O1=Wo8thuit?WxR zh9_S>kw^{V^|g}HRUF=dcq>?q(pHxw!8rx4dC6vbQVmIhmICF#zU!HkHpQ>9S%Uo( zMw{eC+`&pb=GZRou|3;Po1}m46H6NGd$t<2mQh}kaK-WFfmj_66_17BX0|j-E2fe3Jat}ijpc53 zJV$$;PC<5aW`{*^Z6e5##^`Ed#a0nwJDT#Qq~^e8^JTA=z^Kl>La|(UQ!bI@#ge{Dzz@61p-I)kc2?ZxFt^QQ}f%ldLjO*GPj(5)V9IyuUakJX=~GnTgZ4$5!3E=V#t`yOG4U z(gphZB6u2zsj=qNFLYShhg$}lNpO`P9xOSnO*$@@UdMYES*{jJVj|9z-}F^riksLK zbsU+4-{281P9e2UjY6tse^&a)WM1MFw;p#_dHhWI7p&U*9TR0zKdVuQed%6{otTsq z$f~S!;wg#Bd9kez=Br{m|66Wv z#g1xMup<0)H;c2ZO6su_ii&m8j&+jJz4iKnGZ&wxoQX|5a>v&_e#6WA!MB_4asTxLRGQCC5cI(em z%$ZfeqP>!*q5kU>a+BO&ln=4Jm>Ef(QE8o&RgLkk%2}4Tf}U%IFP&uS7}&|Q-)`5< z+e>;s#4cJ-z%&-^&!xsYx777Wt(wZY9(3(avmr|gRe4cD+a8&!LY`1^T?7x{E<=kdY9NYw>A;FtTvQ=Y&1M%lyZPl$ss1oY^Sl8we}n}Aob#6 zl4jERwnt9BlSoWb@3HxYgga(752Vu6Y)k4yk9u~Kw>cA5&LHcrvn1Y-HoIuFWg~}4 zEw4bR`mXZQIyOAzo)FYqg?$5W<;^+XX%Uz61{-L6@eP|lLH%|w?g=rFc;OvEW;^qh z&iYXGhVt(G-q<+_j}CTbPS_=K>RKN0&;dubh0NxJyDOHFF;<1k!{k#7b{|Qok9hac z;gHz}6>H6C6RnB`Tt#oaSrX0p-j-oRJ;_WvS-qS--P*8}V943RT6kou-G=A+7QPGQ z!ze^UGxtW3FC0$|(lY9^L!Lx^?Q8cny(rR`es5U;-xBhphF%_WNu|aO<+e9%6LuZq zt(0PoagJG<%hyuf;te}n+qIl_Ej;czWdc{LX^pS>77s9t*2b4s5dvP_!L^3cwlc)E!(!kGrg~FescVT zZCLeua3f4;d;Tk4iXzt}g}O@nlK3?_o91_~@UMIl?@77Qc$IAlLE95#Z=TES>2E%z zxUKpK{_HvGF;5%Q7n&vA?`{%8ohlYT_?(3A$cZSi)MvIJygXD}TS-3UwyUxGLGiJP znblO~G|*uA^|ac8E-w#}uBtg|s_~s&t>-g0X%zIZ@;o_wNMr_;{KDg^O=rg`fhDZu zFp(VKd1Edj%F zWHPl+)FGj%J1BO3bOHVfH^3d1F{)*PL&sRX`~(-Zy3&9UQX)Z;c51tvaI2E*E7!)q zcz|{vpK7bjxix(k&6=OEIBJC!9lTkUbgg?4-yE{9+pFS)$Ar@vrIf`D0Bnsed(Cf? zObt2CJ>BKOl>q8PyFO6w)+6Iz`LW%T5^R`U_NIW0r1dWv6OY=TVF?N=EfA(k(~7VBW(S;Tu5m4Lg8emDG-(mOSSs=M9Q&N8jc^Y4&9RqIsk(yO_P(mcCr}rCs%1MW1VBrn=0-oQN(Xj!k%iKV zb%ricBF3G4S1;+8lzg5PbZ|$Se$)I=PwiK=cDpHYdov2QO1_a-*dL4KUi|g&oh>(* zq$<`dQ^fat`+VW?m)?_KLn&mp^-@d=&7yGDt<=XwZZC=1scwxO2^RRI7n@g-1o8ps z)&+et_~)vr8aIF1VY1Qrq~Xe``KJrQSnAZ{CSq3yP;V*JC;mmCT6oRLSs7=GA?@6g zUooM}@tKtx(^|aKK8vbaHlUQqwE0}>j&~YlN3H#vKGm@u)xxS?n9XrOWUfCRa< z`20Fld2f&;gg7zpo{Adh+mqNntMc-D$N^yWZAZRI+u1T1zWHPxk{+?vcS1D>08>@6 zLhE@`gt1Y9mAK6Z4p|u(5I%EkfU7rKFSM=E4?VG9tI;a*@?6!ey{lzN5=Y-!$WFSe z&2dtO>^0@V4WRc#L&P%R(?@KfSblMS+N+?xUN$u3K4Ys%OmEh+tq}fnU}i>6YHM?< zlnL2gl~sF!j!Y4E;j3eIU-lfa`RsOL*Tt<%EFC0gPzoHfNWAfKFIKZN8}w~(Yi~=q z>=VNLO2|CjkxP}RkutxjV#4fWYR1KNrPYq5ha9Wl+u>ipsk*I(HS@iLnmGH9MFlTU zaFZ*KSR0px>o+pL7BbhB2EC1%PJ{67_ z#kY&#O4@P=OV#-79y_W>Gv2dxL*@G7%LksNSqgId9v;2xJ zrh8uR!F-eU$NMx@S*+sk=C~Dxr9Qn7TfWnTupuHKuQ$;gGiBcU>GF5sWx(~4IP3`f zWE;YFO*?jGwYh%C3X<>RKHC-DZ!*r;cIr}GLOno^3U4tFSSoJp%oHPiSa%nh=Zgn% z14+8v@ygy0>UgEN1bczD6wK45%M>psM)y^)IfG*>3ItX|TzV*0i%@>L(VN!zdKb8S?Qf7BhjNpziA zR}?={-eu>9JDcl*R=OP9B8N$IcCETXah9SUDhr{yrld{G;PnCWRsPD7!eOOFBTWUQ=LrA_~)mFf&!zJX!Oc-_=kT<}m|K52 z)M=G#;p;Rdb@~h5D{q^K;^fX-m5V}L%!wVC2iZ1uu401Ll}#rocTeK|7FAeBRhNdQ zCc2d^aQnQp=MpOmak60N$OgS}a;p(l9CL`o4r(e-nN}mQ?M&isv-P&d$!8|1D1I(3-z!wi zTgoo)*Mv`gC?~bm?S|@}I|m-E2yqPEvYybiD5azInexpK8?9q*$9Yy9-t%5jU8~ym zgZDx>!@ujQ=|HJnwp^wv-FdD{RtzO9SnyfB{mH_(c!jHL*$>0o-(h(eqe*ZwF6Lvu z{7rkk%PEqaA>o+f{H02tzZ@TWy&su?VNw43! z-X+rN`6llvpUms3ZiSt)JMeztB~>9{J8SPmYs&qohxdYFi!ra8KR$35Zp9oR)eFC4 zE;P31#3V)n`w$fZ|4X-|%MX`xZDM~gJyl2W;O$H25*=+1S#%|53>|LyH za@yh+;325%Gq3;J&a)?%7X%t@WXcWL*BaaR*7UEZad4I8iDt7^R_Fd`XeUo256;sAo2F!HcIQKk;h})QxEsPE5BcKc7WyerTchgKmrfRX z!x#H_%cL#B9TWAqkA4I$R^8{%do3Y*&(;WFmJ zU7Dih{t1<{($VtJRl9|&EB?|cJ)xse!;}>6mSO$o5XIx@V|AA8ZcoD88ZM?C*;{|f zZVmf94_l1OmaICt`2sTyG!$^UeTHx9YuUP!omj(r|7zpm5475|yXI=rR>>fteLI+| z)MoiGho0oEt=*J(;?VY0QzwCqw@cVm?d7Y!z0A@u#H?sCJ*ecvyhj& z-F77lO;SH^dmf?L>3i>?Z*U}Em4ZYV_CjgfvzYsRZ+1B!Uo6H6mbS<-FFL`ytqvb& zE7+)2ahv-~dz(Hs+f})z{*4|{)b=2!RZK;PWwOnO=hG7xG`JU5>bAvUbdYd_CjvtHBHgtGdlO+s^9ca^Bv3`t@VRX2_AD$Ckg36OcQRF zXD6QtGfHdw*hx~V(MV-;;ZZF#dJ-piEF+s27z4X1qi5$!o~xBnvf=uopcn7ftfsZc zy@(PuOk`4GL_n(H9(E2)VUjqRCk9kR?w)v@xO6Jm_Mx})&WGEl=GS0#)0FAq^J*o! zAClhvoTsNP*-b~rN{8Yym3g{01}Ep^^Omf=SKqvN?{Q*C4HNNAcrowIa^mf+3PRy! z*_G-|3i8a;+q;iP@~Of_$(vtFkB8yOyWt2*K)vAn9El>=D;A$CEx6b*XF@4y_6M+2 zpeW`RHoI_p(B{%(&jTHI->hmNmZjHUj<@;7w0mx3&koy!2$@cfX{sN19Y}euYJFn& z1?)+?HCkD0MRI$~uB2UWri})0bru_B;klFdwsLc!ne4YUE;t41JqfG# zZJq6%vbsdx!wYeE<~?>o4V`A3?lN%MnKQ`z=uUivQN^vzJ|C;sdQ37Qn?;lpzg})y z)_2~rUdH}zNwX;Tp0tJ78+&I=IwOQ-fl30R79O8@?Ub8IIA(6I`yHn%lARVL`%b8+ z4$8D-|MZZWxc_)vu6@VZN!HsI$*2NOV&uMxBNzIbRgy%ob_ zhwEH{J9r$!dEix9XM7n&c{S(h>nGm?el;gaX0@|QnzFD@bne`el^CO$yXC?BDJ|Qg z+y$GRoR`?ST1z^e*>;!IS@5Ovb7*RlN>BV_UC!7E_F;N#ky%1J{+iixp(dUJj93aK zzHNN>R-oN7>kykHClPnoPTIj7zc6KM(Pnlb(|s??)SMb)4!sMHU^-ntJwY5Big7xv zb1Ew`Xj;|D2kzGja*C$eS44(d&RMU~c_Y14V9_TLTz0J#uHlsx`S6{nhsA0dWZ#cG zJ?`fO50E>*X4TQLv#nl%3GOk*UkAgt=IY+u0LNXqeln3Z zv$~&Li`ZJOKkFuS)dJRA>)b_Da%Q~axwA_8zNK{BH{#}#m}zGcuckz}riDE-z_Ms> zR8-EqAMcfyGJCtvTpaUVQtajhUS%c@Yj}&6Zz;-M7MZzqv3kA7{SuW$oW#=0az2wQ zg-WG@Vb4|D`pl~Il54N7Hmsauc_ne-a!o5#j3WaBBh@Wuefb!QJIOn5;d)%A#s+5% zuD$H=VNux9bE-}1&bcYGZ+>1Fo;3Z@e&zX^n!?JK*adSbONm$XW9z;Q^L>9U!}Toj2WdafJ%oL#h|yWWwyAGxzfrAWdDTtaKl zK4`5tDpPg5>z$MNv=X0LZ0d6l%D{(D8oT@+w0?ce$DZ6pv>{1&Ok67Ix1 zH}3=IEhPJEhItCC8E=`T`N5(k?G=B4+xzZ?<4!~ ze~z6Wk9!CHTI(0rLJ4{JU?E-puc;xusR?>G?;4vt;q~iI9=kDL=z0Rr%O$vU`30X$ zDZRFyZ`(omOy@u|i6h;wtJlP;+}$|Ak|k2dea7n?U1*$T!sXqqOjq^NxLPMmk~&qI zYg0W?yK8T(6+Ea+$YyspKK?kP$+B`~t3^Pib_`!6xCs32!i@pqXfFV6PmBIR<-QW= zN8L{pt0Vap0x`Gzn#E@zh@H)0FfVfA_Iu4fjYZ+umO1LXIbVc$pY+E234u)ttcrl$ z>s92z4vT%n6cMb>=XT6;l0+9e(|CZG)$@C7t7Z7Ez@a)h)!hyuV&B5K%%)P5?Lk|C zZZSVzdXp{@OXSP0hoU-gF8s8Um(#xzjP2Vem zec#-^JqTa&Y#QJ>-FBxd7tf`XB6e^JPUgagB8iBSEps;92KG`!#mvVcPQ5yNC-GEG zTiHEDYfH+0O15}r^+ z#jxj=@x8iNHWALe!P3R67TwmhItn**0JwnzSV2O&KE8KcT+0hWH^OPD1pwiuyx=b@ zNf5Jh0{9X)8;~Es)$t@%(3!OnbY+`@?i{mGX7Yy}8T_*0a6g;kaFPq;*=px5EhO{Cp%1kI<0?*|h8v!6WnO3cCJRF2-CRrU3JiLJnj@6;L)!0kWYAc_}F{2P))3HmCrz zQ&N&gE70;`!6*eJ4^1IR{f6j4(-l&X!tjHxkbHA^Zhrnhr9g{exN|xrS`5Pq=#Xf& zG%P=#ra-TyVFfgW%cZo5OSIwFL9WtXAlFOa+ubmI5t*3=g#Y zF%;70p5;{ZeFL}&}yOY1N1*Q;*<(kTB!7vM$QokF)yr2FlIU@$Ph58$Bz z0J?xQG=MlS4L6jA22eS42g|9*9pX@$#*sUeM(z+t?hr@r5J&D1rx}2pW&m*_`VDCW zUYY@v-;bAO0HqoAgbbiGGC<=ryf96}3pouhy3XJrX+!!u*O_>Si38V{uJmQ&USptX zKp#l(?>%^7;2%h(q@YWS#9;a!JhKlkR#Vd)ERILlgu!Hr@jA@V;sk4BJ-H#p*4EqC zDGjC*tl=@3Oi6)Bn^QwFpul18fpkbpg0+peH$xyPBqb%`$OUhPKyWb32o7clB*9Z< zN=i~NLjavrLtwgJ01bufP+>p-jR2I95|TpmKpQL2!oV>g(4RvS2pK4*ou%m(h6r3A zX#s&`9LU1ZG&;{CkOK!4fLDTnBys`M!vuz>Q&9OZ0hGQl!~!jSDg|~s*w52opC{sB ze|Cf2luD(*G13LcOAGA!s2FjSK8&IE5#W%J25w!vM0^VyQM!t)inj&RTiJ!wXzFgz z3^IqzB7I0L$llljsGq})thBy9UOyjtFO_*hYM_sgcMk>44jeH0V1FDyELc{S1F-;A zS;T^k^~4biG&V*Irq}O;e}j$$+E_#G?HKIn05iP3j|87TkGK~SqG!-KBg5+mN(aLm z8ybhIM`%C19UX$H$KY6JgXbY$0AT%rEpHC;u`rQ$Y=rxUdsc5*Kvc8jaYaO$^)cI6){P6K0r)I6DY4Wr4&B zLQUBraey#0HV|&c4v7PVo3n$zHj99(TZO^3?Ly%C4nYvJTL9eLBLHsM3WKKD>5!B` zQ=BsR3aR6PD(Fa>327E2HAu5TM~Wusc!)>~(gM)+3~m;92Jd;FnSib=M5d6;;5{%R zb4V7DEJ0V!CP-F*oU?gkc>ksUtAYP&V4ND5J>J2^jt*vcFflQWCrB&fLdT%O59PVJ zhid#toR=FNgD!q3&r8#wEBr`!wzvQu5zX?Q>nlSJ4i@WC*CN*-xU66F^V5crWevQ9gsq$I@z1o(a=k7LL~ z7m_~`o;_Ozha1$8Q}{WBehvAlO4EL60y5}8GDrZ< zXh&F}71JbW2A~8KfEWj&UWV#4+Z4p`b{uAj4&WC zha`}X@3~+Iz^WRlOHU&KngK>#j}+_o@LdBC1H-`gT+krWX3-;!)6?{FBp~%20a}FL zFP9%Emqcwa#(`=G>BBZ0qZDQhmZKJg_g8<=bBFKWr!dyg(YkpE+|R*SGpDVU!+VlU zFC54^DLv}`qa%49T>nNiA9Q7Ips#!Xx90tCU2gvK`(F+GPcL=J^>No{)~we#o@&mUb6c$ zCc*<|NJBk-#+{j9xkQ&ujB zI~`#kN~7W!f*-}wkG~Ld!JqZ@tK}eeSnsS5J1fMFXm|`LJx&}5`@dK3W^7#Wnm+_P zBZkp&j1fa2Y=eIjJ0}gh85jt43kaIXXv?xmo@eHrka!Z|vQv12HN#+!I5E z`(fbuW>gFiJL|uXJ!vKt#z3e3HlVdboH7;e#i3(2<)Fg-I@BR!qY#eof3MFZ&*Y@l zI|KJf&ge@p2Dq09Vu$$Qxb7!}{m-iRk@!)%KL)txi3;~Z4Pb}u@GsW;ELiWeG9V51 znX#}B&4Y2E7-H=OpNE@q{%hFLxwIpBF2t{vPREa8_{linXT;#1vMRWjOzLOP$-hf( z>=?$0;~~PnkqY;~K{EM6Vo-T(0K{A0}VUGmu*hR z{tw3hvBN%N3G3Yw`X5Te+F{J`(3w1s3-+1EbnFQKcrgrX1Jqvs@ADGe%M0s$EbK$$ zK)=y=upBc6SjGYAACCcI=Y*6Fi8_jgwZlLxD26fnQfJmb8^gHRN5(TemhX@0e=vr> zg`W}6U>x6VhoA3DqsGGD9uL1DhB3!OXO=k}59TqD@(0Nb{)Ut_luTioK_>7wjc!5C zIr@w}b`Fez3)0wQfKl&bae7;PcTA7%?f2xucM0G)wt_KO!Ewx>F~;=BI0j=Fb4>pp zv}0R^xM4eti~+^+gE$6b81p(kwzuDti(-K9bc|?+pJEl@H+jSYuxZQV8rl8 zjp@M{#%qItIUFN~KcO9Hed*`$5A-2~pAo~K&<-Q+`9`$CK>rzqAI4w~$F%vs9s{~x zg4BP%Gy*@m?;D6=SRX?888Q6peF@_4Z->8wAH~Cn!R$|Hhq2cIzFYqT_+cDourHbY z0qroxJnrZ4Gh+Ay+F`_c%+KRT>y3qw{)89?=hJ@=KO=@ep)aBJ$c!JHfBMJpsP*3G za7|)VJJ8B;4?n{~ldJF7%jmb`-ftIvNd~ekoufG(`K(3=LNc;HBY& z(lp#q8XAD#cIf}k49zX_i`*fO+#!zKA&%T3j@%)R+#yag067CU%yUEe47>wzGU8^` z1EXFT^@I!{J!F8!X?S6ph8J=gUi5tl93*W>7}_uR<2N2~e}FaG?}KPyugQ=-OGEZs z!GBoyYY+H*ANn4?Z)X4l+7H%`17i5~zRlRIX?t)6_eu=g2Q`3WBhxSUeea+M-S?RL zX9oBGKn%a!H+*hx4d2(I!gsi+@SQK%<{X22M~2tMulJoa)0*+z9=-YO+;DFEm5eE1U9b^B(Z}2^9!Qk`!A$wUE z7$Ar5?NRg2&G!AZqnmE64eh^Anss3i!{}%6@Et+4rr!=}!SBF8eZ2*J3ujCWbl;3; z48H~goPSv(8X61fKKdpP!Z7$88NL^Z?j`!^*I?-P4X^pMxyWz~@$(UeAcTSDd(`vO z{~rc;9|GfMJcApU3k}22a!&)k4{CU!e_ny^Y3cO;tOvOMKEyWz!vG(Kp*;hB?d|R3`2X~=5a6#^o5@qn?J-bI8Ppip{-yG z!k|VcGsq!jF~}7DMr49Wap-s&>o=U^T0!Lcy}!(bhtYsPQy z4|EJe{12QL#=c(suQ89Mhw9<`bui%nx7Nep`C&*M3~vMEACmcRYYRGtANq$F%zh&V zc)cEVeHz*Z1N)L7k-(k3np#{GcDh2Q@ya0YHl*n7fl*ZPAsbU-a94MYYtA#&!c`xGIaV;yzsmrjfieTEtqB_WgZp2*NplHx=$O{M~2#i_vJ{ps-NgK zQsxKK_CBM2PP_je+Xft`(vYfXXgIUr{=PA=7a8`2EHk)Ym2QKIforz# tySWtj{oF3N9@_;i*Fv5S)9x^z=nlWP>jpp-9)52ZmLVA=i*%6g{{fxOO~wEK From 0e260958522e55d8b4f6c7ea2548f23e93c8cf8a Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 16 Sep 2022 12:14:03 +0800 Subject: [PATCH 0486/2015] opt: main window save/restore offset/position --- flutter/lib/common.dart | 102 +++++++++++++++++- .../lib/desktop/pages/desktop_home_page.dart | 5 +- flutter/lib/main.dart | 13 ++- 3 files changed, 113 insertions(+), 7 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index f7c423dbc..c07764e32 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; +import 'dart:ui'; import 'package:back_button_interceptor/back_button_interceptor.dart'; import 'package:desktop_multi_window/desktop_multi_window.dart'; @@ -10,6 +11,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; import 'package:flutter_hbb/models/peer_model.dart'; +import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:get/get.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:window_manager/window_manager.dart'; @@ -28,6 +30,7 @@ var isWeb = false; var isWebDesktop = false; var version = ""; int androidVersion = 0; +const windowPrefix = "wm_"; DesktopType? desktopType; typedef F = String Function(String); @@ -821,8 +824,6 @@ Future initGlobalFFI() async { debugPrint("_globalFFI init end"); // after `put`, can also be globally found by Get.find(); Get.put(_globalFFI, permanent: true); - // global shared preference - await Get.putAsync(() => SharedPreferences.getInstance()); } String translate(String name) { @@ -909,3 +910,100 @@ Widget getPlatformImage(String platform, {double size = 50}) { } return Image.asset('assets/$platform.png', height: size, width: size); } + +class LastWindowPosition { + double? width; + double? height; + double? offsetWidth; + double? offsetHeight; + + LastWindowPosition( + this.width, this.height, this.offsetWidth, this.offsetHeight); + + Map toJson() { + return { + "width": width, + "height": height, + "offsetWidth": offsetWidth, + "offsetHeight": offsetHeight + }; + } + + @override + String toString() { + return jsonEncode(toJson()); + } + + static LastWindowPosition? loadFromString(String content) { + if (content.isEmpty) { + return null; + } + try { + final m = jsonDecode(content); + return LastWindowPosition( + m["width"], m["height"], m["offsetWidth"], m["offsetHeight"]); + } catch (e) { + debugPrint(e.toString()); + return null; + } + } +} + +/// Save window position and size on exit +/// Note that windowId must be provided if it's subwindow +Future saveWindowPosition(WindowType type, {int? windowId}) async { + if (type != WindowType.Main && windowId == null) { + debugPrint( + "Error: windowId cannot be null when saving positions for sub window"); + } + switch (type) { + case WindowType.Main: + List resp = await Future.wait( + [windowManager.getPosition(), windowManager.getSize()]); + Offset position = resp[0]; + Size sz = resp[1]; + final pos = + LastWindowPosition(sz.width, sz.height, position.dx, position.dy); + await Get.find() + .setString(windowPrefix + type.name, pos.toString()); + break; + default: + // TODO: implement window + break; + } +} + +/// Save window position and size on exit +/// Note that windowId must be provided if it's subwindow +Future restoreWindowPosition(WindowType type, {int? windowId}) async { + if (type != WindowType.Main && windowId == null) { + debugPrint( + "Error: windowId cannot be null when saving positions for sub window"); + } + switch (type) { + case WindowType.Main: + var pos = + Get.find().getString(windowPrefix + type.name); + if (pos == null) { + debugPrint("no window position saved, ignore restore"); + return false; + } + var lpos = LastWindowPosition.loadFromString(pos); + if (lpos == null) { + debugPrint("window position saved, but cannot be parsed"); + return false; + } + await windowManager.setSize(Size(lpos.width ?? 1280, lpos.height ?? 720)); + if (lpos.offsetWidth == null || lpos.offsetHeight == null) { + await windowManager.center(); + } else { + await windowManager + .setPosition(Offset(lpos.offsetWidth!, lpos.offsetHeight!)); + } + return true; + default: + // TODO: implement subwindow + break; + } + return false; +} diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 25d1ae66d..bbdd89b15 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -46,7 +46,10 @@ class _DesktopHomePageState extends State // close all sub windows if (await windowManager.isPreventClose()) { try { - await rustDeskWinManager.closeAllSubWindows(); + await Future.wait([ + saveWindowPosition(WindowType.Main), + rustDeskWinManager.closeAllSubWindows() + ]); } catch (err) { debugPrint("$err"); } finally { diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index d5837c09c..7968bbad7 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -10,6 +10,7 @@ import 'package:flutter_hbb/desktop/screen/desktop_remote_screen.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:window_manager/window_manager.dart'; // import 'package:window_manager/window_manager.dart'; @@ -41,6 +42,7 @@ Future main(List args) async { int type = argument['type'] ?? -1; argument['windowId'] = windowId; WindowType wType = type.windowType; + restoreWindowPosition(wType, windowId: windowId); switch (wType) { case WindowType.RemoteDesktop: desktopType = DesktopType.remote; @@ -71,6 +73,8 @@ Future main(List args) async { } Future initEnv(String appType) async { + // global shared preference + await Get.putAsync(() => SharedPreferences.getInstance()); await platformFFI.init(appType); // global FFI, use this **ONLY** for global configuration // for convenience, use global FFI on mobile platform @@ -93,9 +97,9 @@ void runMainApp(bool startService) async { } runApp(App()); // set window option - WindowOptions windowOptions = - getHiddenTitleBarWindowOptions(const Size(1280, 720)); + WindowOptions windowOptions = getHiddenTitleBarWindowOptions(); windowManager.waitUntilReadyToShow(windowOptions, () async { + restoreWindowPosition(WindowType.Main); await windowManager.show(); await windowManager.focus(); }); @@ -166,7 +170,8 @@ void runPortForwardScreen(Map argument) async { void runConnectionManagerScreen() async { // initialize window - WindowOptions windowOptions = getHiddenTitleBarWindowOptions(Size(300, 400)); + WindowOptions windowOptions = + getHiddenTitleBarWindowOptions(size: const Size(300, 400)); await Future.wait([ initEnv(kAppTypeMain), windowManager.waitUntilReadyToShow(windowOptions, () async { @@ -185,7 +190,7 @@ void runConnectionManagerScreen() async { builder: _keepScaleBuilder())); } -WindowOptions getHiddenTitleBarWindowOptions(Size size) { +WindowOptions getHiddenTitleBarWindowOptions({Size? size}) { return WindowOptions( size: size, center: true, From 921c321a713722346dc2f4ed1c1a100ba63a2eaa Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 16 Sep 2022 17:14:32 +0800 Subject: [PATCH 0487/2015] remove flutter_test to resolve version conflict with icon_launcher --- flutter/lib/main.dart | 18 ++++++------ flutter/pubspec.lock | 67 ++++++++++++++++--------------------------- flutter/pubspec.yaml | 4 +-- 3 files changed, 35 insertions(+), 54 deletions(-) diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 7968bbad7..816870984 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -24,9 +24,9 @@ import 'models/platform_model.dart'; int? windowId; -Future main(List args) async { +Future main(List args) async { WidgetsFlutterBinding.ensureInitialized(); - print("launch args: $args"); + debugPrint("launch args: $args"); if (!isDesktop) { runMobileApp(); @@ -37,7 +37,7 @@ Future main(List args) async { windowId = int.parse(args[1]); WindowController.fromWindowId(windowId!).showTitleBar(false); final argument = args[2].isEmpty - ? Map() + ? {} : jsonDecode(args[2]) as Map; int type = argument['type'] ?? -1; argument['windowId'] = windowId; @@ -60,7 +60,7 @@ Future main(List args) async { break; } } else if (args.isNotEmpty && args.first == '--cm') { - print("--cm started"); + debugPrint("--cm started"); desktopType = DesktopType.cm; await windowManager.ensureInitialized(); runConnectionManagerScreen(); @@ -123,7 +123,7 @@ void runRemoteScreen(Map argument) async { home: DesktopRemoteScreen( params: argument, ), - navigatorObservers: [ + navigatorObservers: const [ // FirebaseAnalyticsObserver(analytics: analytics), ], builder: _keepScaleBuilder(), @@ -141,7 +141,7 @@ void runFileTransferScreen(Map argument) async { darkTheme: MyTheme.darkTheme, themeMode: MyTheme.initialThemeMode(), home: DesktopFileTransferScreen(params: argument), - navigatorObservers: [ + navigatorObservers: const [ // FirebaseAnalyticsObserver(analytics: analytics), ], builder: _keepScaleBuilder(), @@ -160,7 +160,7 @@ void runPortForwardScreen(Map argument) async { darkTheme: MyTheme.darkTheme, themeMode: MyTheme.initialThemeMode(), home: DesktopPortForwardScreen(params: argument), - navigatorObservers: [ + navigatorObservers: const [ // FirebaseAnalyticsObserver(analytics: analytics), ], builder: _keepScaleBuilder(), @@ -186,7 +186,7 @@ void runConnectionManagerScreen() async { theme: MyTheme.lightTheme, darkTheme: MyTheme.darkTheme, themeMode: MyTheme.initialThemeMode(), - home: DesktopServerPage(), + home: const DesktopServerPage(), builder: _keepScaleBuilder())); } @@ -246,7 +246,7 @@ class _AppState extends State { : !isAndroid ? WebHomePage() : HomePage(), - navigatorObservers: [ + navigatorObservers: const [ // FirebaseAnalyticsObserver(analytics: analytics), ], builder: isAndroid diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index a871bb928..b012f0435 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -57,13 +57,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "6.0.1" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0" build: dependency: transitive description: @@ -126,28 +119,28 @@ packages: name: cached_network_image url: "https://pub.dartlang.org" source: hosted - version: "3.2.2" + version: "3.2.1" cached_network_image_platform_interface: dependency: transitive description: name: cached_network_image_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "1.0.0" cached_network_image_web: dependency: transitive description: name: cached_network_image_web url: "https://pub.dartlang.org" source: hosted - version: "1.0.2" + version: "1.0.1" characters: dependency: transitive description: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.2.1" + version: "1.2.0" charcode: dependency: transitive description: @@ -318,13 +311,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.3" - fake_async: - dependency: transitive - description: - name: fake_async - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.1" ffi: dependency: "direct main" description: @@ -381,13 +367,6 @@ packages: url: "https://github.com/Kingtous/rustdesk_flutter_custom_cursor" source: git version: "0.0.1" - flutter_launcher_icons: - dependency: "direct dev" - description: - name: flutter_launcher_icons - url: "https://pub.dartlang.org" - source: hosted - version: "0.9.3" flutter_lints: dependency: "direct dev" description: @@ -418,11 +397,6 @@ packages: url: "https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge" source: git version: "1.32.0" - flutter_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" flutter_web_plugins: dependency: transitive description: flutter @@ -498,6 +472,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "4.0.1" + icons_launcher: + dependency: "direct dev" + description: + name: icons_launcher + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.5" image: dependency: "direct main" description: @@ -595,7 +576,7 @@ packages: name: material_color_utilities url: "https://pub.dartlang.org" source: hosted - version: "0.1.5" + version: "0.1.4" menu_base: dependency: transitive description: @@ -609,7 +590,7 @@ packages: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.8.0" + version: "1.7.0" mime: dependency: transitive description: @@ -763,7 +744,7 @@ packages: name: plugin_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.1.2" + version: "2.1.3" pool: dependency: transitive description: @@ -938,7 +919,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.9.0" + version: "1.9.1" sqflite: dependency: transitive description: @@ -995,13 +976,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.2.1" - test_api: - dependency: transitive - description: - name: test_api - url: "https://pub.dartlang.org" - source: hosted - version: "0.4.12" timing: dependency: transitive description: @@ -1039,6 +1013,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.3.1" + universal_io: + dependency: transitive + description: + name: universal_io + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.4" url_launcher: dependency: "direct main" description: @@ -1245,5 +1226,5 @@ packages: source: hosted version: "0.1.0" sdks: - dart: ">=2.18.0 <3.0.0" - flutter: ">=3.3.0" + dart: ">=2.17.1 <3.0.0" + flutter: ">=3.0.0" diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index a05cd779a..1cb72677c 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -81,8 +81,8 @@ dependencies: dev_dependencies: icons_launcher: ^2.0.5 - flutter_test: - sdk: flutter + #flutter_test: + #sdk: flutter build_runner: ^2.1.11 freezed: ^2.0.3 flutter_lints: ^2.0.0 From 402e1c587c866866efa46f50db46ecb212a13b4f Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 16 Sep 2022 16:19:15 +0800 Subject: [PATCH 0488/2015] fix: cm stuck at boot up, revert to flutter 3.0.5 --- flutter/lib/main.dart | 16 +++++++--------- flutter/lib/models/native_model.dart | 12 +++++++++--- flutter/pubspec.yaml | 2 +- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 816870984..43069bf79 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -172,15 +172,7 @@ void runConnectionManagerScreen() async { // initialize window WindowOptions windowOptions = getHiddenTitleBarWindowOptions(size: const Size(300, 400)); - await Future.wait([ - initEnv(kAppTypeMain), - windowManager.waitUntilReadyToShow(windowOptions, () async { - await windowManager.setAlignment(Alignment.topRight); - await windowManager.show(); - await windowManager.focus(); - await windowManager.setAlignment(Alignment.topRight); // ensure - }) - ]); + await initEnv(kAppTypeMain); runApp(GetMaterialApp( debugShowCheckedModeBanner: false, theme: MyTheme.lightTheme, @@ -188,6 +180,12 @@ void runConnectionManagerScreen() async { themeMode: MyTheme.initialThemeMode(), home: const DesktopServerPage(), builder: _keepScaleBuilder())); + windowManager.waitUntilReadyToShow(windowOptions, () async { + await windowManager.setAlignment(Alignment.topRight); + await windowManager.show(); + await windowManager.focus(); + await windowManager.setAlignment(Alignment.topRight); // ensure + }); } WindowOptions getHiddenTitleBarWindowOptions({Size? size}) { diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index 964979731..666116d78 100644 --- a/flutter/lib/models/native_model.dart +++ b/flutter/lib/models/native_model.dart @@ -136,9 +136,15 @@ class PlatformFFI { name = linuxInfo.name; id = linuxInfo.machineId ?? linuxInfo.id; } else if (Platform.isWindows) { - WindowsDeviceInfo winInfo = await deviceInfo.windowsInfo; - name = winInfo.computerName; - id = winInfo.computerName; + try { + WindowsDeviceInfo winInfo = await deviceInfo.windowsInfo; + name = winInfo.computerName; + id = winInfo.computerName; + } catch (e) { + debugPrint("$e"); + name = "unknown"; + id = "unknown"; + } } else if (Platform.isMacOS) { MacOsDeviceInfo macOsInfo = await deviceInfo.macOsInfo; name = macOsInfo.computerName; diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 1cb72677c..02621ed66 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -80,7 +80,7 @@ dependencies: rxdart: ^0.27.5 dev_dependencies: - icons_launcher: ^2.0.5 + icons_launcher: ^2.0.4 #flutter_test: #sdk: flutter build_runner: ^2.1.11 From 76ad796c6a9d2abc5de5a085d462c00e9eebf9d3 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 16 Sep 2022 17:42:11 +0800 Subject: [PATCH 0489/2015] opt: [windows] hide window on start --- flutter/windows/runner/win32_window.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/windows/runner/win32_window.cpp b/flutter/windows/runner/win32_window.cpp index c10f08dc7..3273c2c08 100644 --- a/flutter/windows/runner/win32_window.cpp +++ b/flutter/windows/runner/win32_window.cpp @@ -117,7 +117,7 @@ bool Win32Window::CreateAndShow(const std::wstring& title, double scale_factor = dpi / 96.0; HWND window = CreateWindow( - window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE, + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), Scale(size.width, scale_factor), Scale(size.height, scale_factor), nullptr, nullptr, GetModuleHandle(nullptr), this); From e3a5218eb18a3430577e87627223e3790bcdf9d5 Mon Sep 17 00:00:00 2001 From: csf Date: Fri, 16 Sep 2022 19:41:04 +0800 Subject: [PATCH 0490/2015] global HW_CODEC_CONFIG --- libs/hbb_common/src/config.rs | 11 +++++++++++ libs/scrap/src/common/hwcodec.rs | 5 +++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 9354b4079..5fc974462 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -50,6 +50,7 @@ lazy_static::lazy_static! { pub static ref PROD_RENDEZVOUS_SERVER: Arc> = Default::default(); pub static ref APP_NAME: Arc> = Arc::new(RwLock::new("RustDesk".to_owned())); static ref KEY_PAIR: Arc, Vec)>>> = Default::default(); + static ref HW_CODEC_CONFIG: Arc> = Arc::new(RwLock::new(HwCodecConfig::load())); } // #[cfg(any(target_os = "android", target_os = "ios"))] @@ -1023,6 +1024,16 @@ impl HwCodecConfig { pub fn remove() { std::fs::remove_file(Config::file_("_hwcodec")).ok(); } + + /// refresh current global HW_CODEC_CONFIG, usually uesd after HwCodecConfig::remove() + pub fn refresh() { + *HW_CODEC_CONFIG.write().unwrap() = HwCodecConfig::load(); + log::debug!("HW_CODEC_CONFIG refreshed successfully"); + } + + pub fn get() -> HwCodecConfig { + return HW_CODEC_CONFIG.read().unwrap().clone(); + } } #[cfg(test)] diff --git a/libs/scrap/src/common/hwcodec.rs b/libs/scrap/src/common/hwcodec.rs index ee81627d8..4fdef5462 100644 --- a/libs/scrap/src/common/hwcodec.rs +++ b/libs/scrap/src/common/hwcodec.rs @@ -267,7 +267,7 @@ impl HwDecoderImage<'_> { } fn get_config(k: &str) -> ResultType { - let v = HwCodecConfig::load() + let v = HwCodecConfig::get() .options .get(k) .unwrap_or(&"".to_owned()) @@ -323,7 +323,8 @@ pub fn check_config_process(force_reset: bool) { std::process::Command::new(exe) .arg("--check-hwcodec-config") .status() - .ok() + .ok(); + HwCodecConfig::refresh(); }); }; } From c6e1e84c72524904519b7b64653b3ddf359b310d Mon Sep 17 00:00:00 2001 From: csf Date: Fri, 16 Sep 2022 19:43:28 +0800 Subject: [PATCH 0491/2015] flutter desktop Codec Preference --- .../desktop/pages/desktop_setting_page.dart | 17 +-- .../lib/desktop/widgets/remote_menubar.dart | 138 ++++++++++++------ src/flutter_ffi.rs | 20 ++- src/ui/remote.rs | 28 +--- src/ui_session_interface.rs | 33 ++++- 5 files changed, 152 insertions(+), 84 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index effc26b39..0693554d9 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -219,17 +219,12 @@ class _GeneralState extends State<_General> { } Widget hwcodec() { - return _futureBuilder( - future: bind.mainHasHwcodec(), - hasData: (data) { - return Offstage( - offstage: !(data as bool), - child: _Card(title: 'Hardware Codec', children: [ - _OptionCheckBox( - context, 'Enable hardware codec', 'enable-hwcodec'), - ]), - ); - }); + return Offstage( + offstage: !bind.mainHasHwcodec(), + child: _Card(title: 'Hardware Codec', children: [ + _OptionCheckBox(context, 'Enable hardware codec', 'enable-hwcodec'), + ]), + ); } Widget audio(BuildContext context) { diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index ef20352dc..092ea7da8 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'dart:io'; import 'dart:math' as math; @@ -298,25 +299,35 @@ class _RemoteMenubarState extends State { } Widget _buildDisplay(BuildContext context) { - return mod_menu.PopupMenuButton( - padding: EdgeInsets.zero, - icon: const Icon( - Icons.tv, - color: _MenubarTheme.commonColor, - ), - tooltip: translate('Display Settings'), - position: mod_menu.PopupMenuPosition.under, - itemBuilder: (BuildContext context) => _getDisplayMenu() - .map((entry) => entry.build( - context, - const MenuConfig( - commonColor: _MenubarTheme.commonColor, - height: _MenubarTheme.height, - dividerHeight: _MenubarTheme.dividerHeight, - ))) - .expand((i) => i) - .toList(), - ); + return FutureBuilder(future: () async { + final supportedHwcodec = + await bind.sessionSupportedHwcodec(id: widget.id); + return {'supportedHwcodec': supportedHwcodec}; + }(), builder: (context, snapshot) { + if (snapshot.hasData) { + return mod_menu.PopupMenuButton( + padding: EdgeInsets.zero, + icon: const Icon( + Icons.tv, + color: _MenubarTheme.commonColor, + ), + tooltip: translate('Display Settings'), + position: mod_menu.PopupMenuPosition.under, + itemBuilder: (BuildContext context) => _getDisplayMenu(snapshot.data!) + .map((entry) => entry.build( + context, + const MenuConfig( + commonColor: _MenubarTheme.commonColor, + height: _MenubarTheme.height, + dividerHeight: _MenubarTheme.dividerHeight, + ))) + .expand((i) => i) + .toList(), + ); + } else { + return const Offstage(); + } + }); } Widget _buildKeyboard(BuildContext context) { @@ -532,7 +543,7 @@ class _RemoteMenubarState extends State { return displayMenu; } - List> _getDisplayMenu() { + List> _getDisplayMenu(dynamic futureData) { final displayMenu = [ MenuEntryRadios( text: translate('Ratio'), @@ -653,33 +664,74 @@ class _RemoteMenubarState extends State { } }), MenuEntryDivider(), - // {show_codec ?

    - // MenuEntryDivider(), - () { - final state = ShowRemoteCursorState.find(widget.id); - return MenuEntrySwitch2( - text: translate('Show remote cursor'), - getter: () { - return state; + ]; + + /// Show Codec Preference + if (bind.mainHasHwcodec()) { + final List codecs = []; + try { + final Map codecsJson = jsonDecode(futureData['supportedHwcodec']); + final h264 = codecsJson['h264'] ?? false; + final h265 = codecsJson['h265'] ?? false; + codecs.add(h264); + codecs.add(h265); + } finally {} + if (codecs.length == 2 && (codecs[0] || codecs[1])) { + displayMenu.add(MenuEntryRadios( + text: translate('Codec Preference'), + optionsGetter: () { + final list = [ + MenuEntryRadioOption(text: translate('Auto'), value: 'auto'), + MenuEntryRadioOption(text: 'VP9', value: 'vp9'), + ]; + if (codecs[0]) { + list.add(MenuEntryRadioOption(text: 'H264', value: 'h264')); + } + if (codecs[1]) { + list.add(MenuEntryRadioOption(text: 'H265', value: 'h265')); + } + return list; }, - setter: (bool v) async { - state.value = v; - await bind.sessionToggleOption( - id: widget.id, value: 'show-remote-cursor'); - }); - }(), - MenuEntrySwitch( - text: translate('Show quality monitor'), - getter: () async { - return bind.sessionGetToggleOptionSync( - id: widget.id, arg: 'show-quality-monitor'); + curOptionGetter: () async { + return await bind.sessionGetOption( + id: widget.id, arg: 'codec-preference') ?? + 'auto'; + }, + optionSetter: (String oldValue, String newValue) async { + await bind.sessionPeerOption( + id: widget.id, name: "codec-preference", value: newValue); + bind.sessionChangePreferCodec(id: widget.id); + })); + } + } + + /// Show remote cursor + displayMenu.add(() { + final state = ShowRemoteCursorState.find(widget.id); + return MenuEntrySwitch2( + text: translate('Show remote cursor'), + getter: () { + return state; }, setter: (bool v) async { + state.value = v; await bind.sessionToggleOption( - id: widget.id, value: 'show-quality-monitor'); - widget.ffi.qualityMonitorModel.checkShowQualityMonitor(widget.id); - }), - ]; + id: widget.id, value: 'show-remote-cursor'); + }); + }()); + + /// Show quality monitor + displayMenu.add(MenuEntrySwitch( + text: translate('Show quality monitor'), + getter: () async { + return bind.sessionGetToggleOptionSync( + id: widget.id, arg: 'show-quality-monitor'); + }, + setter: (bool v) async { + await bind.sessionToggleOption( + id: widget.id, value: 'show-quality-monitor'); + widget.ffi.qualityMonitorModel.checkShowQualityMonitor(widget.id); + })); final perms = widget.ffi.ffiModel.permissions; final pi = widget.ffi.ffiModel.pi; diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index e1a806bf4..d68d030be 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -754,8 +754,8 @@ pub fn main_remove_peer(id: String) { PeerConfig::remove(&id); } -pub fn main_has_hwcodec() -> bool { - has_hwcodec() +pub fn main_has_hwcodec() -> SyncReturn { + SyncReturn(has_hwcodec()) } pub fn session_send_mouse(id: String, msg: String) { @@ -816,6 +816,22 @@ pub fn session_send_note(id: String, note: String) { } } +pub fn session_supported_hwcodec(id: String) -> String { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + let (h264, h265) = session.supported_hwcodec(); + let msg = HashMap::from([("h264", h264), ("h265", h265)]); + serde_json::ser::to_string(&msg).unwrap_or("".to_owned()) + } else { + String::new() + } +} + +pub fn session_change_prefer_codec(id: String) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.change_prefer_codec(); + } +} + pub fn main_set_home_dir(home: String) { *config::APP_HOME_DIR.write().unwrap() = home; } diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 76e1ee96c..97dfe1d02 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -449,27 +449,11 @@ impl SciterSession { } fn supported_hwcodec(&self) -> Value { - #[cfg(feature = "hwcodec")] - { - let mut v = Value::array(0); - let decoder = scrap::codec::Decoder::video_codec_state(&self.id); - let mut h264 = decoder.score_h264 > 0; - let mut h265 = decoder.score_h265 > 0; - if let Some((encoding_264, encoding_265)) = self.lc.read().unwrap().supported_encoding { - h264 = h264 && encoding_264; - h265 = h265 && encoding_265; - } - v.push(h264); - v.push(h265); - v - } - #[cfg(not(feature = "hwcodec"))] - { - let mut v = Value::array(0); - v.push(false); - v.push(false); - v - } + let (h264, h265) = self.0.supported_hwcodec(); + let mut v = Value::array(0); + v.push(h264); + v.push(h265); + v } fn save_size(&mut self, x: i32, y: i32, w: i32, h: i32) { @@ -721,4 +705,4 @@ pub fn make_fd(id: i32, entries: &Vec, only_count: bool) -> Value { m.set_item("num_entries", entries.len() as i32); m.set_item("total_size", n as f64); m -} \ No newline at end of file +} diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 692b592d4..c6254b719 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -11,9 +11,9 @@ use async_trait::async_trait; use hbb_common::config::{Config, LocalConfig, PeerConfig}; use hbb_common::rendezvous_proto::ConnType; use hbb_common::tokio::{self, sync::mpsc}; -use rdev::{Event, EventType::*, Key as RdevKey, KeyboardState}; #[cfg(not(any(target_os = "android", target_os = "ios")))] use rdev::Keyboard as RdevKeyboard; +use rdev::{Event, EventType::*, Key as RdevKey, KeyboardState}; use hbb_common::{allow_err, message_proto::*}; use hbb_common::{fs, get_version_number, log, Stream}; @@ -143,6 +143,25 @@ impl Session { return true; } + pub fn supported_hwcodec(&self) -> (bool, bool) { + #[cfg(feature = "hwcodec")] + { + let decoder = scrap::codec::Decoder::video_codec_state(&self.id); + let mut h264 = decoder.score_h264 > 0; + let mut h265 = decoder.score_h265 > 0; + if let Some((encoding_264, encoding_265)) = self.lc.read().unwrap().supported_encoding { + h264 = h264 && encoding_264; + h265 = h265 && encoding_265; + } + return (h264, h265); + } + #[cfg(feature = "mediacodec")] + { + todo!(); + } + (false, false) + } + pub fn change_prefer_codec(&self) { let msg = self.lc.write().unwrap().change_prefer_codec(); self.send(Data::Message(msg)); @@ -658,18 +677,20 @@ impl Session { } self.map_keyboard_mode(down_or_up, key, Some(evt)); } - KeyboardMode::Legacy => { + KeyboardMode::Legacy => + { #[cfg(not(any(target_os = "android", target_os = "ios")))] self.legacy_keyboard_mode(down_or_up, key, evt) - }, + } KeyboardMode::Translate => { #[cfg(not(any(target_os = "android", target_os = "ios")))] self.translate_keyboard_mode(down_or_up, key, evt); } - _ => { + _ => + { #[cfg(not(any(target_os = "android", target_os = "ios")))] self.legacy_keyboard_mode(down_or_up, key, evt) - }, + } } } @@ -1178,7 +1199,7 @@ impl Session { if self.is_port_forward() || self.is_file_transfer() { return; } - if !KEYBOARD_HOOKED.load(Ordering::SeqCst){ + if !KEYBOARD_HOOKED.load(Ordering::SeqCst) { return; } log::info!("keyboard hooked"); From 6f92edca5c44ebdd74a6b1c94bb4cda4ec99be91 Mon Sep 17 00:00:00 2001 From: csf Date: Fri, 16 Sep 2022 20:31:01 +0800 Subject: [PATCH 0492/2015] feat: Android Codec Preference --- flutter/lib/mobile/pages/remote_page.dart | 100 ++++++++++++++++------ src/ui_interface.rs | 4 +- src/ui_session_interface.rs | 13 +-- 3 files changed, 76 insertions(+), 41 deletions(-) diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index d9943a62d..94f584109 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:ui' as ui; import 'package:flutter/gestures.dart'; @@ -1011,17 +1012,22 @@ class QualityMonitor extends StatelessWidget { void showOptions(String id, OverlayDialogManager dialogManager) async { String quality = await bind.sessionGetImageQuality(id: id) ?? 'balanced'; if (quality == '') quality = 'balanced'; + String codec = + await bind.sessionGetOption(id: id, arg: 'codec-preference') ?? 'auto'; + if (codec == '') codec = 'auto'; String viewStyle = await bind.sessionGetOption(id: id, arg: 'view-style') ?? ''; + var displays = []; final pi = gFFI.ffiModel.pi; final image = gFFI.ffiModel.getConnectionImage(); - if (image != null) + if (image != null) { displays.add(Padding(padding: const EdgeInsets.only(top: 8), child: image)); + } if (pi.displays.length > 1) { final cur = pi.currentDisplay; final children = []; - for (var i = 0; i < pi.displays.length; ++i) + for (var i = 0; i < pi.displays.length; ++i) { children.add(InkWell( onTap: () { if (i == cur) return; @@ -1038,6 +1044,7 @@ void showOptions(String id, OverlayDialogManager dialogManager) async { child: Text((i + 1).toString(), style: TextStyle( color: i == cur ? Colors.white : Colors.black87)))))); + } displays.add(Padding( padding: const EdgeInsets.only(top: 8), child: Wrap( @@ -1047,9 +1054,21 @@ void showOptions(String id, OverlayDialogManager dialogManager) async { ))); } if (displays.isNotEmpty) { - displays.add(Divider(color: MyTheme.border)); + displays.add(const Divider(color: MyTheme.border)); } final perms = gFFI.ffiModel.permissions; + final hasHwcodec = bind.mainHasHwcodec(); + final List codecs = []; + if (hasHwcodec) { + try { + final Map codecsJson = + jsonDecode(await bind.sessionSupportedHwcodec(id: id)); + final h264 = codecsJson['h264'] ?? false; + final h265 = codecsJson['h265'] ?? false; + codecs.add(h264); + codecs.add(h265); + } finally {} + } dialogManager.show((setState, close) { final more = []; @@ -1057,50 +1076,77 @@ void showOptions(String id, OverlayDialogManager dialogManager) async { more.add(getToggle(id, setState, 'disable-audio', 'Mute')); } if (perms['keyboard'] != false) { - if (perms['clipboard'] != false) + if (perms['clipboard'] != false) { more.add( getToggle(id, setState, 'disable-clipboard', 'Disable clipboard')); + } more.add(getToggle( id, setState, 'lock-after-session-end', 'Lock after session end')); if (pi.platform == 'Windows') { more.add(getToggle(id, setState, 'privacy-mode', 'Privacy mode')); } } - var setQuality = (String? value) { + setQuality(String? value) { if (value == null) return; setState(() { quality = value; bind.sessionSetImageQuality(id: id, value: value); }); - }; - var setViewStyle = (String? value) { + } + + setViewStyle(String? value) { if (value == null) return; setState(() { viewStyle = value; - bind.sessionPeerOption(id: id, name: "view-style", value: value); - gFFI.canvasModel.updateViewStyle(); + bind + .sessionPeerOption(id: id, name: "view-style", value: value) + .then((_) => gFFI.canvasModel.updateViewStyle()); }); - }; + } + + setCodec(String? value) { + if (value == null) return; + setState(() { + codec = value; + bind + .sessionPeerOption(id: id, name: "codec-preference", value: value) + .then((_) => bind.sessionChangePreferCodec(id: id)); + }); + } + + final radios = [ + getRadio('Scale original', 'original', viewStyle, setViewStyle), + getRadio('Scale adaptive', 'adaptive', viewStyle, setViewStyle), + const Divider(color: MyTheme.border), + getRadio('Good image quality', 'best', quality, setQuality), + getRadio('Balanced', 'balanced', quality, setQuality), + getRadio('Optimize reaction time', 'low', quality, setQuality), + const Divider(color: MyTheme.border) + ]; + + if (hasHwcodec && codecs.length == 2 && (codecs[0] || codecs[1])) { + radios.addAll([ + getRadio(translate('Auto'), 'auto', codec, setCodec), + getRadio('VP9', 'vp9', codec, setCodec), + ]); + if (codecs[0]) { + radios.add(getRadio('H264', 'h264', codec, setCodec)); + } + if (codecs[1]) { + radios.add(getRadio('H265', 'h265', codec, setCodec)); + } + radios.add(const Divider(color: MyTheme.border)); + } + + final toggles = [ + getToggle(id, setState, 'show-remote-cursor', 'Show remote cursor'), + getToggle(id, setState, 'show-quality-monitor', 'Show quality monitor'), + ]; + return CustomAlertDialog( - title: SizedBox.shrink(), content: Column( mainAxisSize: MainAxisSize.min, - children: displays + - [ - getRadio('Scale Original', 'original', viewStyle, setViewStyle), - getRadio('Scale adaptive', 'adaptive', viewStyle, setViewStyle), - Divider(color: MyTheme.border), - getRadio('Good image quality', 'best', quality, setQuality), - getRadio('Balanced', 'balanced', quality, setQuality), - getRadio('Optimize reaction time', 'low', quality, setQuality), - Divider(color: MyTheme.border), - getToggle( - id, setState, 'show-remote-cursor', 'Show remote cursor'), - getToggle(id, setState, 'show-quality-monitor', - 'Show quality monitor'), - ] + - more), - actions: [], + children: displays + radios + toggles + more), contentPadding: 0, ); }, clickMaskDismiss: true, backDismiss: true); diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 90afb8c43..fb2ab7472 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -744,9 +744,9 @@ pub fn get_api_server() -> String { #[inline] pub fn has_hwcodec() -> bool { - #[cfg(not(feature = "hwcodec"))] + #[cfg(not(any(feature = "hwcodec", feature = "mediacodec")))] return false; - #[cfg(feature = "hwcodec")] + #[cfg(any(feature = "hwcodec", feature = "mediacodec"))] return true; } diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index c6254b719..2feceb8fe 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -136,15 +136,8 @@ impl Session { true } - pub fn has_hwcodec(&self) -> bool { - #[cfg(not(feature = "hwcodec"))] - return false; - #[cfg(feature = "hwcodec")] - return true; - } - pub fn supported_hwcodec(&self) -> (bool, bool) { - #[cfg(feature = "hwcodec")] + #[cfg(any(feature = "hwcodec", feature = "mediacodec"))] { let decoder = scrap::codec::Decoder::video_codec_state(&self.id); let mut h264 = decoder.score_h264 > 0; @@ -155,10 +148,6 @@ impl Session { } return (h264, h265); } - #[cfg(feature = "mediacodec")] - { - todo!(); - } (false, false) } From e0302de8082375227620353905dbb217c25748bf Mon Sep 17 00:00:00 2001 From: csf Date: Fri, 16 Sep 2022 20:43:15 +0800 Subject: [PATCH 0493/2015] Android server_page.dart fix verificationMethod onSelected color & follow lint --- flutter/lib/mobile/pages/server_page.dart | 152 ++++++++++++---------- 1 file changed, 82 insertions(+), 70 deletions(-) diff --git a/flutter/lib/mobile/pages/server_page.dart b/flutter/lib/mobile/pages/server_page.dart index 00c433fd8..e3be5060f 100644 --- a/flutter/lib/mobile/pages/server_page.dart +++ b/flutter/lib/mobile/pages/server_page.dart @@ -12,51 +12,50 @@ class ServerPage extends StatefulWidget implements PageShape { final title = translate("Share Screen"); @override - final icon = Icon(Icons.mobile_screen_share); + final icon = const Icon(Icons.mobile_screen_share); @override final appBarActions = [ PopupMenuButton( - icon: Icon(Icons.more_vert), + icon: const Icon(Icons.more_vert), itemBuilder: (context) { return [ PopupMenuItem( - child: Text(translate("Change ID")), - padding: EdgeInsets.symmetric(horizontal: 16.0), + padding: const EdgeInsets.symmetric(horizontal: 16.0), value: "changeID", enabled: false, + child: Text(translate("Change ID")), ), PopupMenuItem( - child: Text(translate("Set permanent password")), - padding: EdgeInsets.symmetric(horizontal: 16.0), + padding: const EdgeInsets.symmetric(horizontal: 16.0), value: "setPermanentPassword", enabled: gFFI.serverModel.verificationMethod != kUseTemporaryPassword, + child: Text(translate("Set permanent password")), ), PopupMenuItem( - child: Text(translate("Set temporary password length")), - padding: EdgeInsets.symmetric(horizontal: 16.0), + padding: const EdgeInsets.symmetric(horizontal: 16.0), value: "setTemporaryPasswordLength", enabled: gFFI.serverModel.verificationMethod != kUsePermanentPassword, + child: Text(translate("Set temporary password length")), ), const PopupMenuDivider(), PopupMenuItem( - padding: EdgeInsets.symmetric(horizontal: 0.0), + padding: const EdgeInsets.symmetric(horizontal: 0.0), value: kUseTemporaryPassword, - child: Container( - child: ListTile( - title: Text(translate("Use temporary password")), - trailing: Icon( - Icons.check, - color: gFFI.serverModel.verificationMethod == - kUseTemporaryPassword - ? null - : Color(0xFFFFFFFF), - ))), + child: ListTile( + title: Text(translate("Use temporary password")), + trailing: Icon( + Icons.check, + color: gFFI.serverModel.verificationMethod == + kUseTemporaryPassword + ? null + : Colors.transparent, + )), ), PopupMenuItem( - padding: EdgeInsets.symmetric(horizontal: 0.0), + padding: const EdgeInsets.symmetric(horizontal: 0.0), value: kUsePermanentPassword, child: ListTile( title: Text(translate("Use permanent password")), @@ -65,11 +64,11 @@ class ServerPage extends StatefulWidget implements PageShape { color: gFFI.serverModel.verificationMethod == kUsePermanentPassword ? null - : Color(0xFFFFFFFF), + : Colors.transparent, )), ), PopupMenuItem( - padding: EdgeInsets.symmetric(horizontal: 0.0), + padding: const EdgeInsets.symmetric(horizontal: 0.0), value: kUseBothPasswords, child: ListTile( title: Text(translate("Use both passwords")), @@ -80,7 +79,7 @@ class ServerPage extends StatefulWidget implements PageShape { gFFI.serverModel.verificationMethod != kUsePermanentPassword ? null - : Color(0xFFFFFFFF), + : Colors.transparent, )), ), ]; @@ -101,6 +100,8 @@ class ServerPage extends StatefulWidget implements PageShape { }) ]; + ServerPage({Key? key}) : super(key: key); + @override State createState() => _ServerPageState(); } @@ -125,9 +126,9 @@ class _ServerPageState extends State { mainAxisAlignment: MainAxisAlignment.start, children: [ ServerInfo(), - PermissionChecker(), - ConnectionManager(), - SizedBox.fromSize(size: Size(0, 15.0)), + const PermissionChecker(), + const ConnectionManager(), + SizedBox.fromSize(size: const Size(0, 15.0)), ], ), ), @@ -148,6 +149,8 @@ class ServerInfo extends StatelessWidget { final model = gFFI.serverModel; final emptyController = TextEditingController(text: "-"); + ServerInfo({Key? key}) : super(key: key); + @override Widget build(BuildContext context) { final isPermanent = model.verificationMethod == kUsePermanentPassword; @@ -158,7 +161,7 @@ class ServerInfo extends StatelessWidget { children: [ TextFormField( readOnly: true, - style: TextStyle( + style: const TextStyle( fontSize: 25.0, fontWeight: FontWeight.bold, color: MyTheme.accent), @@ -166,14 +169,14 @@ class ServerInfo extends StatelessWidget { decoration: InputDecoration( icon: const Icon(Icons.perm_identity), labelText: translate("ID"), - labelStyle: TextStyle( + labelStyle: const TextStyle( fontWeight: FontWeight.bold, color: MyTheme.accent50), ), onSaved: (String? value) {}, ), TextFormField( readOnly: true, - style: TextStyle( + style: const TextStyle( fontSize: 25.0, fontWeight: FontWeight.bold, color: MyTheme.accent), @@ -181,7 +184,7 @@ class ServerInfo extends StatelessWidget { decoration: InputDecoration( icon: const Icon(Icons.lock), labelText: translate("Password"), - labelStyle: TextStyle( + labelStyle: const TextStyle( fontWeight: FontWeight.bold, color: MyTheme.accent50), suffix: isPermanent ? null @@ -200,13 +203,13 @@ class ServerInfo extends StatelessWidget { Center( child: Row( children: [ - Icon(Icons.warning_amber_sharp, + const Icon(Icons.warning_amber_sharp, color: Colors.redAccent, size: 24), - SizedBox(width: 10), + const SizedBox(width: 10), Expanded( child: Text( translate("Service is not running"), - style: TextStyle( + style: const TextStyle( fontFamily: 'WorkSans', fontWeight: FontWeight.bold, fontSize: 18, @@ -215,11 +218,11 @@ class ServerInfo extends StatelessWidget { )) ], )), - SizedBox(height: 5), + const SizedBox(height: 5), Center( child: Text( translate("android_start_service_tip"), - style: TextStyle(fontSize: 12, color: MyTheme.darkGray), + style: const TextStyle(fontSize: 12, color: MyTheme.darkGray), )) ], )); @@ -227,8 +230,10 @@ class ServerInfo extends StatelessWidget { } class PermissionChecker extends StatefulWidget { + const PermissionChecker({Key? key}) : super(key: key); + @override - _PermissionCheckerState createState() => _PermissionCheckerState(); + State createState() => _PermissionCheckerState(); } class _PermissionCheckerState extends State { @@ -236,7 +241,7 @@ class _PermissionCheckerState extends State { Widget build(BuildContext context) { final serverModel = Provider.of(context); final hasAudioPermission = androidVersion >= 30; - final status; + final String status; if (serverModel.connectStatus == -1) { status = 'not_ready_status'; } else if (serverModel.connectStatus == 0) { @@ -260,9 +265,9 @@ class _PermissionCheckerState extends State { serverModel.toggleAudio) : Text( "* ${translate("android_version_audio_tip")}", - style: TextStyle(color: MyTheme.darkGray), + style: const TextStyle(color: MyTheme.darkGray), ), - SizedBox(height: 8), + const SizedBox(height: 8), Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ @@ -273,11 +278,11 @@ class _PermissionCheckerState extends State { style: ButtonStyle( backgroundColor: MaterialStateProperty.all(Colors.red)), - icon: Icon(Icons.stop), + icon: const Icon(Icons.stop), onPressed: serverModel.toggleService, label: Text(translate("Stop service"))) : ElevatedButton.icon( - icon: Icon(Icons.play_arrow), + icon: const Icon(Icons.play_arrow), onPressed: serverModel.toggleService, label: Text(translate("Start Service")))), Expanded( @@ -287,8 +292,8 @@ class _PermissionCheckerState extends State { Expanded( flex: 0, child: Padding( - padding: - EdgeInsets.only(left: 20, right: 5), + padding: const EdgeInsets.only( + left: 20, right: 5), child: Icon(Icons.circle, color: serverModel.connectStatus > 0 ? Colors.greenAccent @@ -297,12 +302,12 @@ class _PermissionCheckerState extends State { Expanded( child: Text(translate(status), softWrap: true, - style: TextStyle( + style: const TextStyle( fontSize: 14.0, color: MyTheme.accent50))) ], ) - : SizedBox.shrink()) + : const SizedBox.shrink()) ], ), ], @@ -311,7 +316,8 @@ class _PermissionCheckerState extends State { } class PermissionRow extends StatelessWidget { - PermissionRow(this.name, this.isOk, this.onPressed); + const PermissionRow(this.name, this.isOk, this.onPressed, {Key? key}) + : super(key: key); final String name; final bool isOk; @@ -327,8 +333,8 @@ class PermissionRow extends StatelessWidget { fit: BoxFit.scaleDown, alignment: Alignment.centerLeft, child: Text(name, - style: - TextStyle(fontSize: 16.0, color: MyTheme.accent50)))), + style: const TextStyle( + fontSize: 16.0, color: MyTheme.accent50)))), Expanded( flex: 2, child: FittedBox( @@ -347,7 +353,7 @@ class PermissionRow extends StatelessWidget { onPressed: onPressed, child: Text( translate(isOk ? "CLOSE" : "OPEN"), - style: TextStyle(fontWeight: FontWeight.bold), + style: const TextStyle(fontWeight: FontWeight.bold), )))), ], ); @@ -355,6 +361,8 @@ class PermissionRow extends StatelessWidget { } class ConnectionManager extends StatelessWidget { + const ConnectionManager({Key? key}) : super(key: key); + @override Widget build(BuildContext context) { final serverModel = Provider.of(context); @@ -377,7 +385,7 @@ class ConnectionManager extends StatelessWidget { Expanded( flex: -1, child: client.isFileTransfer || !client.authorized - ? SizedBox.shrink() + ? const SizedBox.shrink() : IconButton( onPressed: () { gFFI.chatModel.changeCurrentID(client.id); @@ -388,24 +396,24 @@ class ConnectionManager extends StatelessWidget { bar.onTap!(1); } }, - icon: Icon( + icon: const Icon( Icons.chat, color: MyTheme.accent80, ))) ], ), client.authorized - ? SizedBox.shrink() + ? const SizedBox.shrink() : Text( translate("android_new_connection_tip"), - style: TextStyle(color: Colors.black54), + style: const TextStyle(color: Colors.black54), ), client.authorized ? ElevatedButton.icon( style: ButtonStyle( backgroundColor: MaterialStateProperty.all(Colors.red)), - icon: Icon(Icons.close), + icon: const Icon(Icons.close), onPressed: () { bind.cmCloseConnection(connId: client.id); gFFI.invokeMethod( @@ -418,7 +426,7 @@ class ConnectionManager extends StatelessWidget { onPressed: () { serverModel.sendLoginResponse(client, false); }), - SizedBox(width: 20), + const SizedBox(width: 20), ElevatedButton( child: Text(translate("Accept")), onPressed: () { @@ -432,7 +440,8 @@ class ConnectionManager extends StatelessWidget { } class PaddingCard extends StatelessWidget { - PaddingCard({required this.child, this.title, this.titleIcon}); + const PaddingCard({Key? key, required this.child, this.title, this.titleIcon}) + : super(key: key); final String? title; final IconData? titleIcon; @@ -445,18 +454,18 @@ class PaddingCard extends StatelessWidget { children.insert( 0, Padding( - padding: EdgeInsets.symmetric(vertical: 5.0), + padding: const EdgeInsets.symmetric(vertical: 5.0), child: Row( children: [ titleIcon != null ? Padding( - padding: EdgeInsets.only(right: 10), + padding: const EdgeInsets.only(right: 10), child: Icon(titleIcon, color: MyTheme.accent80, size: 30)) - : SizedBox.shrink(), + : const SizedBox.shrink(), Text( title!, - style: TextStyle( + style: const TextStyle( fontFamily: 'WorkSans', fontWeight: FontWeight.bold, fontSize: 20, @@ -466,12 +475,13 @@ class PaddingCard extends StatelessWidget { ], ))); } - return Container( + return SizedBox( width: double.maxFinite, child: Card( - margin: EdgeInsets.fromLTRB(15.0, 15.0, 15.0, 0), + margin: const EdgeInsets.fromLTRB(15.0, 15.0, 15.0, 0), child: Padding( - padding: EdgeInsets.symmetric(vertical: 15.0, horizontal: 30.0), + padding: + const EdgeInsets.symmetric(vertical: 15.0, horizontal: 30.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: children, @@ -483,27 +493,29 @@ class PaddingCard extends StatelessWidget { Widget clientInfo(Client client) { return Padding( - padding: EdgeInsets.symmetric(vertical: 8), + padding: const EdgeInsets.symmetric(vertical: 8), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( flex: -1, child: Padding( - padding: EdgeInsets.only(right: 12), + padding: const EdgeInsets.only(right: 12), child: CircleAvatar( - child: Text(client.name[0]), - backgroundColor: MyTheme.border))), + backgroundColor: MyTheme.border, + child: Text(client.name[0])))), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [ Text(client.name, - style: TextStyle(color: MyTheme.idColor, fontSize: 18)), - SizedBox(width: 8), + style: const TextStyle( + color: MyTheme.idColor, fontSize: 18)), + const SizedBox(width: 8), Text(client.peerId, - style: TextStyle(color: MyTheme.idColor, fontSize: 10)) + style: + const TextStyle(color: MyTheme.idColor, fontSize: 10)) ])) ], ), From e32a019a29a72c80d1f31c0808616bdb508e6f93 Mon Sep 17 00:00:00 2001 From: csf Date: Fri, 16 Sep 2022 21:52:08 +0800 Subject: [PATCH 0494/2015] feat: Android change id --- flutter/lib/common/widgets/dialog.dart | 74 +++++++++++++++++ .../lib/desktop/pages/desktop_home_page.dart | 2 + .../desktop/pages/desktop_setting_page.dart | 80 +------------------ flutter/lib/mobile/pages/server_page.dart | 4 +- src/flutter_ffi.rs | 17 ++-- src/ui_interface.rs | 22 ++++- 6 files changed, 108 insertions(+), 91 deletions(-) create mode 100644 flutter/lib/common/widgets/dialog.dart diff --git a/flutter/lib/common/widgets/dialog.dart b/flutter/lib/common/widgets/dialog.dart new file mode 100644 index 000000000..82e4fb5cc --- /dev/null +++ b/flutter/lib/common/widgets/dialog.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import '../../common.dart'; +import '../../models/platform_model.dart'; + +void changeIdDialog() { + var newId = ""; + var msg = ""; + var isInProgress = false; + TextEditingController controller = TextEditingController(); + gFFI.dialogManager.show((setState, close) { + submit() async { + debugPrint("onSubmit"); + newId = controller.text.trim(); + setState(() { + msg = ""; + isInProgress = true; + bind.mainChangeId(newId: newId); + }); + + var status = await bind.mainGetAsyncStatus(); + while (status == " ") { + await Future.delayed(const Duration(milliseconds: 100)); + status = await bind.mainGetAsyncStatus(); + } + if (status.isEmpty) { + // ok + close(); + return; + } + setState(() { + isInProgress = false; + msg = translate(status); + }); + } + + return CustomAlertDialog( + title: Text(translate("Change ID")), + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(translate("id_change_tip")), + const SizedBox( + height: 12.0, + ), + TextField( + decoration: InputDecoration( + border: const OutlineInputBorder(), + errorText: msg.isEmpty ? null : translate(msg)), + inputFormatters: [ + LengthLimitingTextInputFormatter(16), + // FilteringTextInputFormatter(RegExp(r"[a-zA-z][a-zA-z0-9\_]*"), allow: true) + ], + maxLength: 16, + controller: controller, + focusNode: FocusNode()..requestFocus(), + ), + const SizedBox( + height: 4.0, + ), + Offstage( + offstage: !isInProgress, child: const LinearProgressIndicator()) + ], + ), + actions: [ + TextButton(onPressed: close, child: Text(translate("Cancel"))), + TextButton(onPressed: submit, child: Text(translate("OK"))), + ], + onSubmit: submit, + onCancel: close, + ); + }); +} diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index bbdd89b15..c8706d5a0 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -19,6 +19,8 @@ import 'package:tray_manager/tray_manager.dart'; import 'package:url_launcher/url_launcher_string.dart'; import 'package:window_manager/window_manager.dart'; +import '../../common/widgets/dialog.dart'; + class _MenubarTheme { static const Color commonColor = MyTheme.accent; // kMinInteractiveDimension diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 0693554d9..4aedc1385 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -11,6 +11,8 @@ import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher_string.dart'; +import '../../common/widgets/dialog.dart'; + const double _kTabWidth = 235; const double _kTabHeight = 42; const double _kCardFixedWidth = 560; @@ -1496,82 +1498,4 @@ void changeSocks5Proxy() async { }); } -void changeIdDialog() { - var newId = ""; - var msg = ""; - var isInProgress = false; - TextEditingController controller = TextEditingController(); - gFFI.dialogManager.show((setState, close) { - submit() async { - newId = controller.text.trim(); - setState(() { - msg = ""; - isInProgress = true; - bind.mainChangeId(newId: newId); - }); - - var status = await bind.mainGetAsyncStatus(); - while (status == " ") { - await Future.delayed(const Duration(milliseconds: 100)); - status = await bind.mainGetAsyncStatus(); - } - if (status.isEmpty) { - // ok - close(); - return; - } - setState(() { - isInProgress = false; - msg = translate(status); - }); - } - - return CustomAlertDialog( - title: Text(translate("Change ID")), - content: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(translate("id_change_tip")), - const SizedBox( - height: 8.0, - ), - Row( - children: [ - const Text("ID:").marginOnly(bottom: 16.0), - const SizedBox( - width: 24.0, - ), - Expanded( - child: TextField( - decoration: InputDecoration( - border: const OutlineInputBorder(), - errorText: msg.isEmpty ? null : translate(msg)), - inputFormatters: [ - LengthLimitingTextInputFormatter(16), - // FilteringTextInputFormatter(RegExp(r"[a-zA-z][a-zA-z0-9\_]*"), allow: true) - ], - maxLength: 16, - controller: controller, - focusNode: FocusNode()..requestFocus(), - ), - ), - ], - ), - const SizedBox( - height: 4.0, - ), - Offstage( - offstage: !isInProgress, child: const LinearProgressIndicator()) - ], - ), - actions: [ - TextButton(onPressed: close, child: Text(translate("Cancel"))), - TextButton(onPressed: submit, child: Text(translate("OK"))), - ], - onSubmit: submit, - onCancel: close, - ); - }); -} - //#endregion diff --git a/flutter/lib/mobile/pages/server_page.dart b/flutter/lib/mobile/pages/server_page.dart index e3be5060f..4fdd00ede 100644 --- a/flutter/lib/mobile/pages/server_page.dart +++ b/flutter/lib/mobile/pages/server_page.dart @@ -3,6 +3,7 @@ import 'package:flutter_hbb/mobile/widgets/dialog.dart'; import 'package:provider/provider.dart'; import '../../common.dart'; +import '../../common/widgets/dialog.dart'; import '../../models/platform_model.dart'; import '../../models/server_model.dart'; import 'home_page.dart'; @@ -23,7 +24,6 @@ class ServerPage extends StatefulWidget implements PageShape { PopupMenuItem( padding: const EdgeInsets.symmetric(horizontal: 16.0), value: "changeID", - enabled: false, child: Text(translate("Change ID")), ), PopupMenuItem( @@ -86,7 +86,7 @@ class ServerPage extends StatefulWidget implements PageShape { }, onSelected: (value) { if (value == "changeID") { - // TODO + changeIdDialog(); } else if (value == "setPermanentPassword") { setPermanentPasswordDialog(gFFI.dialogManager); } else if (value == "setTemporaryPasswordLength") { diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index d68d030be..253855e3f 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -17,15 +17,15 @@ use crate::flutter::{self, SESSIONS}; use crate::start_server; use crate::ui_interface; #[cfg(not(any(target_os = "android", target_os = "ios")))] -use crate::ui_interface::{change_id, get_sound_inputs}; +use crate::ui_interface::get_sound_inputs; use crate::ui_interface::{ - check_mouse_time, check_super_user_permission, discover, forget_password, get_api_server, - get_app_name, get_async_job_status, get_connect_status, get_fav, get_id, get_lan_peers, - get_langs, get_license, get_local_option, get_mouse_time, get_option, get_options, get_peer, - get_peer_option, get_socks, get_uuid, get_version, has_hwcodec, has_rendezvous_service, - post_request, send_to_cm, set_local_option, set_option, set_options, set_peer_option, - set_permanent_password, set_socks, store_fav, test_if_valid_server, update_temporary_password, - using_public_server, + change_id, check_mouse_time, check_super_user_permission, discover, forget_password, + get_api_server, get_app_name, get_async_job_status, get_connect_status, get_fav, get_id, + get_lan_peers, get_langs, get_license, get_local_option, get_mouse_time, get_option, + get_options, get_peer, get_peer_option, get_socks, get_uuid, get_version, has_hwcodec, + has_rendezvous_service, post_request, send_to_cm, set_local_option, set_option, set_options, + set_peer_option, set_permanent_password, set_socks, store_fav, test_if_valid_server, + update_temporary_password, using_public_server, }; use crate::{ client::file_trait::FileManager, @@ -426,7 +426,6 @@ pub fn main_get_sound_inputs() -> Vec { } pub fn main_change_id(new_id: String) { - #[cfg(not(any(target_os = "android", target_os = "ios")))] change_id(new_id) } diff --git a/src/ui_interface.rs b/src/ui_interface.rs index fb2ab7472..7242a35dc 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -687,7 +687,6 @@ pub fn open_url(url: String) { } #[inline] -#[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn change_id(id: String) { *ASYNC_JOB_STATUS.lock().unwrap() = " ".to_owned(); let old_id = get_id(); @@ -864,17 +863,27 @@ pub(crate) async fn send_to_cm(data: &ipc::Data) { const INVALID_FORMAT: &'static str = "Invalid format"; const UNKNOWN_ERROR: &'static str = "Unknown error"; -#[cfg(not(any(target_os = "android", target_os = "ios")))] #[tokio::main(flavor = "current_thread")] async fn change_id_(id: String, old_id: String) -> &'static str { if !hbb_common::is_valid_custom_id(&id) { return INVALID_FORMAT; } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] let uuid = machine_uid::get().unwrap_or("".to_owned()); + #[cfg(any(target_os = "android", target_os = "ios"))] + let uuid = base64::encode(hbb_common::get_uuid()); + if uuid.is_empty() { + log::error!("Failed to change id, uuid is_empty"); return UNKNOWN_ERROR; } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] let rendezvous_servers = crate::ipc::get_rendezvous_servers(1_000).await; + #[cfg(any(target_os = "android", target_os = "ios"))] + let rendezvous_servers = Config::get_rendezvous_servers(); + let mut futs = Vec::new(); let err: Arc> = Default::default(); for rendezvous_server in rendezvous_servers { @@ -892,7 +901,13 @@ async fn change_id_(id: String, old_id: String) -> &'static str { join_all(futs).await; let err = *err.lock().unwrap(); if err.is_empty() { + #[cfg(not(any(target_os = "android", target_os = "ios")))] crate::ipc::set_config_async("id", id.to_owned()).await.ok(); + #[cfg(any(target_os = "android", target_os = "ios"))] + { + Config::set_key_confirmed(false); + Config::set_id(&id); + } } err } @@ -937,6 +952,9 @@ async fn check_id( register_pk_response::Result::NOT_SUPPORT => { return "server_not_support"; } + register_pk_response::Result::SERVER_ERROR => { + return "Server error"; + } register_pk_response::Result::INVALID_ID_FORMAT => { return INVALID_FORMAT; } From d80b5c35d40c9c3844b43f8491fc129e4e864138 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 16 Sep 2022 21:53:34 +0800 Subject: [PATCH 0495/2015] add icon files for mac flutter --- .../AppIcon.appiconset/app_icon_1024.png | Bin 0 -> 15146 bytes .../AppIcon.appiconset/app_icon_128.png | Bin 0 -> 1975 bytes .../AppIcon.appiconset/app_icon_16.png | Bin 0 -> 383 bytes .../AppIcon.appiconset/app_icon_256.png | Bin 0 -> 3938 bytes .../AppIcon.appiconset/app_icon_32.png | Bin 0 -> 624 bytes .../AppIcon.appiconset/app_icon_512.png | Bin 0 -> 8587 bytes .../AppIcon.appiconset/app_icon_64.png | Bin 0 -> 1023 bytes 7 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png create mode 100644 flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png create mode 100644 flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png create mode 100644 flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png create mode 100644 flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png create mode 100644 flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png create mode 100644 flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png 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 new file mode 100644 index 0000000000000000000000000000000000000000..6b4737dd5d7abd614f667696c8fbc698ae90556c GIT binary patch literal 15146 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 literal 0 HcmV?d00001 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 new file mode 100644 index 0000000000000000000000000000000000000000..c78490fc442c3640a519ee94a3faa81d087cacf5 GIT binary patch literal 383 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbK}dw@@fD?{Ob1j_#p1gs_httb8e z`|tnv-~ZEA{kNO)U%KJ{{kQ**-21=l=Ksc>|I0W3=d1kBRsNqL@Bg!p|8Ktfzwh?{ z%~$`gx%hv@`Tvs+|1aM7KX={#kOlwsy8kOQ{}-$M|L*JmH(&l=eD?pqo&QVD{O{WL zKVjMbxMlx+X8-q`_1~=j|D?Vi4xp2mOM?7@|DCqK?Xq(Zkh{dw#W6(Ua_af(e1{D< zSOcut-oLS(U9$fF|9x!Xue>XplofaAJo5R|^|7NcOF^V`j^V89M^}eC3Z-RA*S;@( z?ZJ`OoIZ8FL6`$WdsKhiiSnjQL50Ipu4$K=@Rd!F^%IrV5Z=P_lr@)O<|Cg2Y%+~` zzuCl@Hbhu6OprX5-LUqk#qBEzC3636yjSb!mpi+5iM_|4D?9)Env(cTakfZM&EdaC a|1<0>=H$9?WVRg8y$qhNelF{r5}E)qM8D7g literal 0 HcmV?d00001 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 new file mode 100644 index 0000000000000000000000000000000000000000..fb7dc702a35996c766f5404c4850d2b461322fde GIT binary patch literal 3938 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 literal 0 HcmV?d00001 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 new file mode 100644 index 0000000000000000000000000000000000000000..883dbd709db9f3298e1af6c1bc676525cb19f260 GIT binary patch literal 624 zcmV-$0+0QPP)W19|2>oc`TYOP!~bio|5Bg-F^&HwhX3dC|Ksof z#ozz7*Z-!@|DVeLgS!8Fw*Pmt|7EHFN}K=c^#9!K|JUjN!`=V6*#EQC|EbUaLYMzH zkpC`={}z4!-Ry$d00008bW%=J`Ojtu_JsiVG)Z`K)XOsG}QMM7r&Zx)iVKAe(mx&O+tOU{SoRLbsCeO4T+ME>V0 zmn_?TDcdsg1CmxryS5pA$X2vZF+9xUtAX4BoR3u@W@rjg>#Th|0PC?J)!h|uwO)Wf zK;j8jA8V0o8o&gwsj_u6A0mi$IWhMQ06~Z2&Mq!oAXqZpj-dgnj5N4aMo`SeB!mJG za)2%wlp;EN8x6EP3Yls@GHz+10nV`FvX$h4Tb6gA!5O6TD$B&b7un~=3&4Uboe(O* zfR5k32@DDmu%zQSGi%rvYe?Jln_>;%GAecvd~uGeA|N?~cTxbUkzZ7ZUw_l%wtNjB z;}Erai)@g?KKs2C=55Ao6V>hs*>Nja4Ow^(a@o$!Jv7DbBSZm!Oh(3w@*?spK5(R)3-<%;um{C@zw-Yu?4)IjY30000< KMNUMnLSTYQPCUi{ literal 0 HcmV?d00001 diff --git a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 0000000000000000000000000000000000000000..f60f4744288d02147a58a566c3ec33f0e3043145 GIT binary patch literal 8587 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 literal 0 HcmV?d00001 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 new file mode 100644 index 0000000000000000000000000000000000000000..056497e14981860529b98b67d73d953dcdabd9fd GIT binary patch literal 1023 zcma)*|34IV9LGQAYd3C}Ey9kd89Sv#-F3cnIz+x*H)V1-r1R~1AY#6Sp%7-v#Fd=z zXl93Dhia}itQjGXu5N^zFFEAfalRby=|AZ4cs*awpI<+`vT5$8G%>gph`_lRbUM zly|5p?(`jg+U$-xX|A3QJ4R9Po&f;G%YaJ&OdMz>!`uoypMs+WpmP=E+t51>YjSWd z1#%&*Y`}sHg6@MC7sT^W+YgDq0hx zx50^QeX8^VE_~R`*#xA7!jclM9%hho?rjbTs)OfL&0U)xtSeju}mw}j9w<@ z9?dpx1r9kw?b|5TJnN~==7?|j8detc@@ov+{f+)kz4E6vPF($5?*z-9zqAahC?3k6 zZd=)pnciv_ygfG-uHmqrlFE=iGw-IGZ0?&Fw?xN3pYZ--qRN#_as2+d!iDUx{wvGV zMkYOSM- z>W$cQt-5uVR=W^81OM~I?$HsxnCFL@>7fF`6}Z*FCb74|L@sg|PL$)Sw_sG>QV}-2) zR576?IwvGN-w@TyaLh2J@-rR!#mKrNjI$u5QiArNK|g6ZX4s@4MOgZo(kLdNBKuhV zN1SUpd&^7KR{e4D)PmP5Dl4`BX_m9(G3#&kI%1S*9NbS<77MurvFA6dd>P9*V&&fDdj028~h`ZxDuTJDm^obD`DdbCy2OPlI$+Ad00q(ooI nOdU7If5{g!*-qSgc6R`-O3rWUW1aYY@1E@J;BL=5$xQzrRq+A3 literal 0 HcmV?d00001 From 2e0f71fb35b9753c1684d424a32626957ada468f Mon Sep 17 00:00:00 2001 From: csf Date: Fri, 16 Sep 2022 21:55:51 +0800 Subject: [PATCH 0496/2015] fix sciter has_hwcodec --- src/ui/remote.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 97dfe1d02..97e130d09 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -28,6 +28,7 @@ use hbb_common::{ use crate::clipboard_file::*; use crate::{ client::*, + ui_interface::has_hwcodec, ui_session_interface::{InvokeUiSession, Session}, }; @@ -440,6 +441,10 @@ impl SciterSession { v } + fn has_hwcodec(&self) -> bool { + has_hwcodec() + } + pub fn t(&self, name: String) -> String { crate::client::translate(name) } From dd2315a5186ff23950efc7afaa36548513a2c7f9 Mon Sep 17 00:00:00 2001 From: songwei163 Date: Fri, 16 Sep 2022 22:22:17 +0800 Subject: [PATCH 0497/2015] fix m1 pro scrap compile error --- libs/scrap/build.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/libs/scrap/build.rs b/libs/scrap/build.rs index b59dc03f3..8939cd214 100644 --- a/libs/scrap/build.rs +++ b/libs/scrap/build.rs @@ -13,7 +13,13 @@ fn link_vcpkg(mut path: PathBuf, name: &str) -> PathBuf { target_arch = "arm64".to_owned(); } let mut target = if target_os == "macos" { - "x64-osx".to_owned() + if target_arch == "x64" { + "x64-osx".to_owned() + } else if target_arch == "arm64"{ + "arm64-osx".to_owned() + } else { + format!("{}-{}", target_arch, target_os) + } } else if target_os == "windows" { "x64-windows-static".to_owned() } else { From 4b451b25e96d98449c5a54d49819028d515bd3cc Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 16 Sep 2022 22:38:55 +0800 Subject: [PATCH 0498/2015] disable macos sandbox, we are not normal app --- flutter/macos/Runner/DebugProfile.entitlements | 2 +- flutter/macos/Runner/Release.entitlements | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/flutter/macos/Runner/DebugProfile.entitlements b/flutter/macos/Runner/DebugProfile.entitlements index dddb8a30c..9f56413f3 100644 --- a/flutter/macos/Runner/DebugProfile.entitlements +++ b/flutter/macos/Runner/DebugProfile.entitlements @@ -3,7 +3,7 @@ com.apple.security.app-sandbox - + com.apple.security.cs.allow-jit com.apple.security.network.server diff --git a/flutter/macos/Runner/Release.entitlements b/flutter/macos/Runner/Release.entitlements index ee95ab7e5..08ba3a3fa 100644 --- a/flutter/macos/Runner/Release.entitlements +++ b/flutter/macos/Runner/Release.entitlements @@ -3,7 +3,7 @@ com.apple.security.app-sandbox - + com.apple.security.network.client From 3a04991c776b76ee4c350611fdbf3599dfe4e273 Mon Sep 17 00:00:00 2001 From: Reza <3228126+i2@users.noreply.github.com> Date: Fri, 16 Sep 2022 11:46:21 -0300 Subject: [PATCH 0499/2015] 1. Moved *.md files to docs folder 2. currently was used twice --- README.md | 8 +- CODE_OF_CONDUCT.md => docs/CODE_OF_CONDUCT.md | 0 CONTRIBUTING.md => docs/CONTRIBUTING.md | 2 +- README-AR.md => docs/README-AR.md | 4 +- README-CS.md => docs/README-CS.md | 4 +- README-DE.md => docs/README-DE.md | 4 +- README-EO.md => docs/README-EO.md | 4 +- README-ES.md => docs/README-ES.md | 332 +++++++++--------- README-FA.md => docs/README-FA.md | 4 +- README-FI.md => docs/README-FI.md | 4 +- README-FR.md => docs/README-FR.md | 4 +- README-HU.md => docs/README-HU.md | 4 +- README-ID.md => docs/README-ID.md | 4 +- README-IT.md => docs/README-IT.md | 4 +- README-JP.md => docs/README-JP.md | 4 +- README-KR.md => docs/README-KR.md | 4 +- README-ML.md => docs/README-ML.md | 4 +- README-NL.md => docs/README-NL.md | 4 +- README-PL.md => docs/README-PL.md | 4 +- README-PTBR.md => docs/README-PTBR.md | 4 +- README-RU.md => docs/README-RU.md | 4 +- README-VN.md => docs/README-VN.md | 4 +- README-ZH.md => docs/README-ZH.md | 4 +- SECURITY.md => docs/SECURITY.md | 0 24 files changed, 209 insertions(+), 209 deletions(-) rename CODE_OF_CONDUCT.md => docs/CODE_OF_CONDUCT.md (100%) rename CONTRIBUTING.md => docs/CONTRIBUTING.md (96%) rename README-AR.md => docs/README-AR.md (88%) rename README-CS.md => docs/README-CS.md (89%) rename README-DE.md => docs/README-DE.md (87%) rename README-EO.md => docs/README-EO.md (87%) rename README-ES.md => docs/README-ES.md (87%) rename README-FA.md => docs/README-FA.md (90%) rename README-FI.md => docs/README-FI.md (87%) rename README-FR.md => docs/README-FR.md (87%) rename README-HU.md => docs/README-HU.md (89%) rename README-ID.md => docs/README-ID.md (87%) rename README-IT.md => docs/README-IT.md (87%) rename README-JP.md => docs/README-JP.md (89%) rename README-KR.md => docs/README-KR.md (88%) rename README-ML.md => docs/README-ML.md (91%) rename README-NL.md => docs/README-NL.md (87%) rename README-PL.md => docs/README-PL.md (87%) rename README-PTBR.md => docs/README-PTBR.md (87%) rename README-RU.md => docs/README-RU.md (90%) rename README-VN.md => docs/README-VN.md (90%) rename README-ZH.md => docs/README-ZH.md (89%) rename SECURITY.md => docs/SECURITY.md (100%) diff --git a/README.md b/README.md index 8e484ef86..4c76e8ec4 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ DockerStructureSnapshot
    - [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
    + [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
    We need your help to translate this README, RustDesk UI and Doc to your native language

    @@ -17,7 +17,7 @@ Yet another remote desktop software, written in Rust. Works out of the box, no c ![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) -RustDesk welcomes contribution from everyone. See [`CONTRIBUTING.md`](CONTRIBUTING.md) for help getting started. +RustDesk welcomes contribution from everyone. See [`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md) for help getting started. [**How does RustDesk work?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F) @@ -39,7 +39,7 @@ Below are the servers you are using for free, it may change along the time. If y ## Dependencies -Desktop versions use [sciter](https://sciter.com/) or Flutter for GUI, this tutorial is for Sciter only. +Desktop versions use [sciter](https://sciter.com/) or Flutter for GUI, this tutorial is for Sciter only. Please download sciter dynamic library yourself. @@ -134,7 +134,7 @@ When wayland is the controlled side, you have to start in the following way: $ sudo rustdesk --service $ rustdesk ``` -**Notice**: Wayland screen recording uses different interfaces, currently currently only supports org.freedesktop.portal.ScreenCast. +**Notice**: Wayland screen recording uses different interfaces. RustDesk currently only supports org.freedesktop.portal.ScreenCast. ```bash $ dbus-send --session --print-reply \ --dest=org.freedesktop.portal.Desktop \ diff --git a/CODE_OF_CONDUCT.md b/docs/CODE_OF_CONDUCT.md similarity index 100% rename from CODE_OF_CONDUCT.md rename to docs/CODE_OF_CONDUCT.md diff --git a/CONTRIBUTING.md b/docs/CONTRIBUTING.md similarity index 96% rename from CONTRIBUTING.md rename to docs/CONTRIBUTING.md index 79e3ba4a2..5229d3d36 100644 --- a/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -39,7 +39,7 @@ For specific git instructions, see [GitHub workflow 101](https://github.com/serv ## Conduct -https://github.com/rustdesk/rustdesk/blob/master/CODE_OF_CONDUCT.md +https://github.com/rustdesk/rustdesk/blob/master/docs/CODE_OF_CONDUCT.md ## Communication diff --git a/README-AR.md b/docs/README-AR.md similarity index 88% rename from README-AR.md rename to docs/README-AR.md index c0186037e..62867faca 100644 --- a/README-AR.md +++ b/docs/README-AR.md @@ -5,7 +5,7 @@ DockerStructureSnapshot
    - [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
    + [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
    لغتك الأم, Doc و RustDesk UI, README نحن بحاجة إلى مساعدتك لترجمة هذا

    @@ -21,7 +21,7 @@ ![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) -لمساعدتك على ذلك [`CONTRIBUTING.md`](CONTRIBUTING.md) يرحب بمساهمة الجميع. اطلع على RustDesk. +لمساعدتك على ذلك [`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md) يرحب بمساهمة الجميع. اطلع على RustDesk. [**؟ RustDesk كيفية يعمل**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F) diff --git a/README-CS.md b/docs/README-CS.md similarity index 89% rename from README-CS.md rename to docs/README-CS.md index 7ad86e08b..eebcfc46b 100644 --- a/README-CS.md +++ b/docs/README-CS.md @@ -5,7 +5,7 @@ DockerStrukturaUkázky
    - [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
    + [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
    Potřebujeme Vaši pomoc s překláním textů tohoto ČTIMNE, uživatelského rozhraní aplikace RustDesk a dokumentace k ní do vašeho jazyka

    @@ -16,7 +16,7 @@ Dopisujte si s námi: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https Zase další software pro přístup k ploše na dálku, naprogramovaný v jazyce Rust. Funguje hned tak, jak je – není třeba žádného nastavování. Svá data máte ve svých rukách, bez obav o zabezpečení. Je možné používat námi poskytovaný propojovací/předávací (relay) server, [vytvořit si svůj vlastní](https://rustdesk.com/server), nebo [si dokonce svůj vlastní naprogramovat](https://github.com/rustdesk/rustdesk-server-demo), budete-li chtít. -Projekt RustDesk vítá přiložení ruky k dílu od každého. Jak začít se dozvíte z [`CONTRIBUTING.md`](CONTRIBUTING.md). +Projekt RustDesk vítá přiložení ruky k dílu od každého. Jak začít se dozvíte z [`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md). [**Jak RustDesk funguje?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F) diff --git a/README-DE.md b/docs/README-DE.md similarity index 87% rename from README-DE.md rename to docs/README-DE.md index eb468d569..dafc28d7e 100644 --- a/README-DE.md +++ b/docs/README-DE.md @@ -5,7 +5,7 @@ DockerDateistrukturScreenshots
    - [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
    + [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [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

    @@ -15,7 +15,7 @@ Rede mit uns: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitt 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 heißt jegliche Mitarbeit willkommen. Schau dir [`CONTRIBUTING.md`](CONTRIBUTING.md) an, wenn du Hilfe brauchst für den Start. +RustDesk heißt jegliche Mitarbeit willkommen. Schau dir [`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md) an, wenn du Hilfe brauchst für den Start. [**PROGRAMM DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases) diff --git a/README-EO.md b/docs/README-EO.md similarity index 87% rename from README-EO.md rename to docs/README-EO.md index b9af26102..321a46ca4 100644 --- a/README-EO.md +++ b/docs/README-EO.md @@ -5,7 +5,7 @@ DockerStrukturoEkrankopio
    - [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
    + [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
    Ni bezonas helpon traduki tiun README kaj la interfacon al via denaska lingvo

    @@ -15,7 +15,7 @@ Babili kun ni: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twit Denove alia fora labortabla programo, skribita en Rust. Ĝi funkcias elskatole, ne bezonas konfiguraĵon. Vi havas la tutan kontrolon sur viaj datumoj, sen zorgo pri sekureco. Vi povas uzi nian servilon rendezvous/relajsan, [agordi vian propran](https://rustdesk.com/server), aŭ [skribi vian propran servilon rendezvous/relajsan](https://github.com/rustdesk/rustdesk-server-demo). -RustDesk bonvenigas kontribuon de ĉiuj. Vidu [`CONTRIBUTING.md`](CONTRIBUTING.md) por helpo komenci. +RustDesk bonvenigas kontribuon de ĉiuj. Vidu [`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md) por helpo komenci. [**BINARA ELŜUTO**](https://github.com/rustdesk/rustdesk/releases) diff --git a/README-ES.md b/docs/README-ES.md similarity index 87% rename from README-ES.md rename to docs/README-ES.md index 7ceed149a..f14d96b78 100644 --- a/README-ES.md +++ b/docs/README-ES.md @@ -1,166 +1,166 @@ -

    - RustDesk - Your remote desktop
    - Servidores • - Compilar • - Docker • - Estructura • - Captura de pantalla
    - [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
    - Necesitamos tu ayuda para traducir este README a tu idioma -

    - -Chatea con nosotros: [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) - -Otro software de escritorio remoto, escrito en Rust. Funciona de forma inmediata, sin necesidad de configuración. Tienes el control total de sus datos, sin preocupaciones sobre la seguridad. Puedes utilizar nuestro servidor de rendezvous/relay, [set up your own](https://rustdesk.com/server), o [escribir tu propio servidor rendezvous/relay](https://github.com/rustdesk/rustdesk-server-demo). - -RustDesk agradece la contribución de todo el mundo. Ve [`CONTRIBUTING.md`](CONTRIBUTING.md) para ayuda inicial. - -[**DESCARGA DE BINARIOS**](https://github.com/rustdesk/rustdesk/releases) - -## Servidores gratis de uso público - -A continuación se muestran los servidores que está utilizando de forma gratuita, puede cambiar en algún momento. Si no estás cerca de uno de ellos, tu red puede ser lenta. - -| Ubicación | Vendedor | Especificación | -| --------- | ------------- | ------------------ | -| Seoul | AWS lightsail | 1 VCPU / 0.5GB RAM | -| Singapore | Vultr | 1 VCPU / 1GB RAM | -| Dallas | Vultr | 1 VCPU / 1GB RAM | | -| Germany | Hetzner | 2 VCPU / 4GB RAM | -| Germany | Codext | 4 VCPU / 8GB RAM | - -## Dependencies - -La versión Desktop usa [sciter](https://sciter.com/) para GUI, por favor bajate la librería sciter tu mismo.. - -[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) - -## Pasos para compilar desde el inicio - -- Prepara el entono de desarrollo de Rust y el entorno de compilación de C++ y Rust. - -- Instala [vcpkg](https://github.com/microsoft/vcpkg), y configura la variable de entono `VCPKG_ROOT` correctamente. - - - Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static - - Linux/Osx: vcpkg install libvpx libyuv opus - -- run `cargo run` - -## Como compilar en 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 -``` - -### Fedora 28 (CentOS 8) - -```sh -sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel -``` - -### Arch (Manjaro) - -```sh -sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pulseaudio -``` - -### Install vcpkg - -```sh -git clone https://github.com/microsoft/vcpkg -cd vcpkg -git checkout 2021.12.01 -cd .. -vcpkg/bootstrap-vcpkg.sh -export VCPKG_ROOT=$HOME/vcpkg -vcpkg/vcpkg install libvpx libyuv opus -``` - -### Soluciona libvpx (Para Fedora) - -```sh -cd vcpkg/buildtrees/libvpx/src -cd * -./configure -sed -i 's/CFLAGS+=-I/CFLAGS+=-fPIC -I/g' Makefile -sed -i 's/CXXFLAGS+=-I/CXXFLAGS+=-fPIC -I/g' Makefile -make -cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/ -cd -``` - -### Compila - -```sh -curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -source $HOME/.cargo/env -git clone https://github.com/rustdesk/rustdesk -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 -``` - -### Cambia Wayland a X11 (Xorg) - -RustDesk no soporta Wayland. Comprueba [aquí](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) para configurar Xorg en la sesión por defecto de GNOME. - -## Como compilar con Docker - -Empieza clonando el repositorio y compilando el contenedor de docker: - -```sh -git clone https://github.com/rustdesk/rustdesk -cd rustdesk -docker build -t "rustdesk-builder" . -``` - -Entonces, cada vez que necesites compilar una modificación, ejecuta el siguiente comando: - -```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 -``` - -Ten en cuenta que la primera compilación puede tardar más tiempo antes de que las dependencias se almacenen en la caché, las siguientes compilaciones serán más rápidas. Además, si necesitas especificar diferentes argumentos a la orden de compilación, puede hacerlo al final de la linea de comandos en el apartado ``. Por ejemplo, si desea compilar una versión optimizada para publicación, deberá ejecutar el comando anterior seguido de `--release`. El ejecutable resultante estará disponible en la carpeta de destino en su sistema, y puede ser ejecutado con: - -```sh -target/debug/rustdesk -``` - -O si estas ejecutando una versión para su publicación: - -```sh -target/release/rustdesk -``` - -Por favor, asegurate de que estás ejecutando estos comandos desde la raíz del repositorio de RustDesk, de lo contrario la aplicación puede ser incapaz de encontrar los recursos necesarios. También hay que tener en cuenta que otros subcomandos de carga como `install` o `run` no estan actualmente soportados via este metodo y podrían requerir ser instalados dentro del contenedor y no en el host. - -## Estructura de archivos - -- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: video codec, configuración, tcp/udp wrapper, protobuf, fs funciones para transferencia de ficheros, y alguna función de utilidad. -- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: captura de pantalla -- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: control específico por cada plataforma para el teclado/ratón -- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI -- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: sonido/portapapeles/entrada/servicios de video, y conexiones de red -- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: iniciar una conexión "peer to peer" -- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Comunicación con [rustdesk-server](https://github.com/rustdesk/rustdesk-server), esperar la conexión remota directa ("TCP hole punching") o conexión indirecta ("relayed") -- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: código específico de cada plataforma -- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter, código para moviles -- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Javascript para cliente web Flutter - -## Captura de pantalla - -![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) - -![image](https://user-images.githubusercontent.com/71636191/113112619-f705a480-923b-11eb-911d-97e984ef52b6.png) - -![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png) - -![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png) +

    + RustDesk - Your remote desktop
    + Servidores • + Compilar • + Docker • + Estructura • + Captura de pantalla
    + [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
    + Necesitamos tu ayuda para traducir este README a tu idioma +

    + +Chatea con nosotros: [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) + +Otro software de escritorio remoto, escrito en Rust. Funciona de forma inmediata, sin necesidad de configuración. Tienes el control total de sus datos, sin preocupaciones sobre la seguridad. Puedes utilizar nuestro servidor de rendezvous/relay, [set up your own](https://rustdesk.com/server), o [escribir tu propio servidor rendezvous/relay](https://github.com/rustdesk/rustdesk-server-demo). + +RustDesk agradece la contribución de todo el mundo. Ve [`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md) para ayuda inicial. + +[**DESCARGA DE BINARIOS**](https://github.com/rustdesk/rustdesk/releases) + +## Servidores gratis de uso público + +A continuación se muestran los servidores que está utilizando de forma gratuita, puede cambiar en algún momento. Si no estás cerca de uno de ellos, tu red puede ser lenta. + +| Ubicación | Vendedor | Especificación | +| --------- | ------------- | ------------------ | +| Seoul | AWS lightsail | 1 VCPU / 0.5GB RAM | +| Singapore | Vultr | 1 VCPU / 1GB RAM | +| Dallas | Vultr | 1 VCPU / 1GB RAM | | +| Germany | Hetzner | 2 VCPU / 4GB RAM | +| Germany | Codext | 4 VCPU / 8GB RAM | + +## Dependencies + +La versión Desktop usa [sciter](https://sciter.com/) para GUI, por favor bajate la librería sciter tu mismo.. + +[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) + +## Pasos para compilar desde el inicio + +- Prepara el entono de desarrollo de Rust y el entorno de compilación de C++ y Rust. + +- Instala [vcpkg](https://github.com/microsoft/vcpkg), y configura la variable de entono `VCPKG_ROOT` correctamente. + + - Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static + - Linux/Osx: vcpkg install libvpx libyuv opus + +- run `cargo run` + +## Como compilar en 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 +``` + +### Fedora 28 (CentOS 8) + +```sh +sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel +``` + +### Arch (Manjaro) + +```sh +sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pulseaudio +``` + +### Install vcpkg + +```sh +git clone https://github.com/microsoft/vcpkg +cd vcpkg +git checkout 2021.12.01 +cd .. +vcpkg/bootstrap-vcpkg.sh +export VCPKG_ROOT=$HOME/vcpkg +vcpkg/vcpkg install libvpx libyuv opus +``` + +### Soluciona libvpx (Para Fedora) + +```sh +cd vcpkg/buildtrees/libvpx/src +cd * +./configure +sed -i 's/CFLAGS+=-I/CFLAGS+=-fPIC -I/g' Makefile +sed -i 's/CXXFLAGS+=-I/CXXFLAGS+=-fPIC -I/g' Makefile +make +cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/ +cd +``` + +### Compila + +```sh +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +source $HOME/.cargo/env +git clone https://github.com/rustdesk/rustdesk +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 +``` + +### Cambia Wayland a X11 (Xorg) + +RustDesk no soporta Wayland. Comprueba [aquí](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) para configurar Xorg en la sesión por defecto de GNOME. + +## Como compilar con Docker + +Empieza clonando el repositorio y compilando el contenedor de docker: + +```sh +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +docker build -t "rustdesk-builder" . +``` + +Entonces, cada vez que necesites compilar una modificación, ejecuta el siguiente comando: + +```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 +``` + +Ten en cuenta que la primera compilación puede tardar más tiempo antes de que las dependencias se almacenen en la caché, las siguientes compilaciones serán más rápidas. Además, si necesitas especificar diferentes argumentos a la orden de compilación, puede hacerlo al final de la linea de comandos en el apartado ``. Por ejemplo, si desea compilar una versión optimizada para publicación, deberá ejecutar el comando anterior seguido de `--release`. El ejecutable resultante estará disponible en la carpeta de destino en su sistema, y puede ser ejecutado con: + +```sh +target/debug/rustdesk +``` + +O si estas ejecutando una versión para su publicación: + +```sh +target/release/rustdesk +``` + +Por favor, asegurate de que estás ejecutando estos comandos desde la raíz del repositorio de RustDesk, de lo contrario la aplicación puede ser incapaz de encontrar los recursos necesarios. También hay que tener en cuenta que otros subcomandos de carga como `install` o `run` no estan actualmente soportados via este metodo y podrían requerir ser instalados dentro del contenedor y no en el host. + +## Estructura de archivos + +- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: video codec, configuración, tcp/udp wrapper, protobuf, fs funciones para transferencia de ficheros, y alguna función de utilidad. +- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: captura de pantalla +- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: control específico por cada plataforma para el teclado/ratón +- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI +- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: sonido/portapapeles/entrada/servicios de video, y conexiones de red +- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: iniciar una conexión "peer to peer" +- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Comunicación con [rustdesk-server](https://github.com/rustdesk/rustdesk-server), esperar la conexión remota directa ("TCP hole punching") o conexión indirecta ("relayed") +- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: código específico de cada plataforma +- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter, código para moviles +- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Javascript para cliente web Flutter + +## Captura de pantalla + +![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) + +![image](https://user-images.githubusercontent.com/71636191/113112619-f705a480-923b-11eb-911d-97e984ef52b6.png) + +![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png) + +![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png) diff --git a/README-FA.md b/docs/README-FA.md similarity index 90% rename from README-FA.md rename to docs/README-FA.md index f7de9aa87..73a8d6914 100644 --- a/README-FA.md +++ b/docs/README-FA.md @@ -5,7 +5,7 @@ داکرساختسرور
    - [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
    + [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
    برای ترجمه این RustDesk UI ،README و Doc به زبان مادری شما به کمکتون نیاز داریم

    @@ -18,7 +18,7 @@ می‌توانید از سرور rendezvous/relay ما استفاده کنید، [سرور خودتان را راه‌اندازی کنید](https://rustdesk.com/server) یا [ سرورrendezvous/relay خود را بنویسید](https://github.com/rustdesk/rustdesk). -‫راست دسک (RustDesk) از مشارکت همه استقبال می کند. برای راهنمایی جهت مشارکت به [`CONTRIBUTING.md`](CONTRIBUTING.md) مراجعه کنید. +‫راست دسک (RustDesk) از مشارکت همه استقبال می کند. برای راهنمایی جهت مشارکت به [`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md) مراجعه کنید. [راست دسک چطور کار می کند؟](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F) diff --git a/README-FI.md b/docs/README-FI.md similarity index 87% rename from README-FI.md rename to docs/README-FI.md index 2e4c99ba6..910b787f2 100644 --- a/README-FI.md +++ b/docs/README-FI.md @@ -5,7 +5,7 @@ DockerRakenneTilannevedos
    - [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
    + [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
    Tarvitsemme apua tämän README-tiedoston kääntämiseksi äidinkielellesi

    @@ -15,7 +15,7 @@ Juttele meidän kanssa: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](htt Vielä yksi etätyöpöytäohjelmisto, ohjelmoitu Rust-kielellä. Toimii suoraan pakkauksesta, ei tarvitse asetusta. Hallitset täysin tietojasi, ei tarvitse murehtia turvallisuutta. Voit käyttää meidän rendezvous/relay-palvelinta, [aseta omasi](https://rustdesk.com/server), tai [kirjoittaa oma rendezvous/relay-palvelin](https://github.com/rustdesk/rustdesk-server-demo). -RustDesk toivottaa avustukset tervetulleiksi kaikilta. Katso lisätietoja [`CONTRIBUTING.md`](CONTRIBUTING.md) avun saamiseksi. +RustDesk toivottaa avustukset tervetulleiksi kaikilta. Katso lisätietoja [`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md) avun saamiseksi. [**BINAARILATAUS**](https://github.com/rustdesk/rustdesk/releases) diff --git a/README-FR.md b/docs/README-FR.md similarity index 87% rename from README-FR.md rename to docs/README-FR.md index 3e33cb322..2dab81b7a 100644 --- a/README-FR.md +++ b/docs/README-FR.md @@ -5,7 +5,7 @@ Docker - Structure - Images
    - [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
    + [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
    Nous avons besoin de votre aide pour traduire ce README dans votre langue maternelle.

    @@ -15,7 +15,7 @@ Chattez avec nous : [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https:/ Encore un autre logiciel de bureau à distance, écrit en Rust. Fonctionne directement, aucune configuration n'est nécessaire. Vous avez le contrôle total de vos données, sans aucun souci de sécurité. Vous pouvez utiliser notre serveur de rendez-vous/relais, [configurer le vôtre](https://rustdesk.com/server), ou [écrire votre propre serveur de rendez-vous/relais](https://github.com/rustdesk/rustdesk-server-demo). -RustDesk accueille les contributions de tout le monde. Voir [`CONTRIBUTING.md`](CONTRIBUTING.md) pour plus d'informations. +RustDesk accueille les contributions de tout le monde. Voir [`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md) pour plus d'informations. [**TÉLÉCHARGEMENT BINAIRE**](https://github.com/rustdesk/rustdesk/releases) diff --git a/README-HU.md b/docs/README-HU.md similarity index 89% rename from README-HU.md rename to docs/README-HU.md index cfc6c793d..a682a23c4 100644 --- a/README-HU.md +++ b/docs/README-HU.md @@ -5,7 +5,7 @@ DockerStruktúraKépernyőképek
    - [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
    + [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
    Kell a segítséged, hogy lefordítsuk ezt a README-t, a RustDesk UI-t és a Dokumentációt az anyanyelvedre

    @@ -17,7 +17,7 @@ A RustDesk egy távoli elérésű asztali szoftver, Rust-ban írva. Működik mi ![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) -A RustDesk szívesen fogad minden contributiont, támogatást mindenkitől. Lásd a [`CONTRIBUTING.md`](CONTRIBUTING.md) fájlt a kezdéshez. +A RustDesk szívesen fogad minden contributiont, támogatást mindenkitől. Lásd a [`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md) fájlt a kezdéshez. [**Hogyan működik a RustDesk?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F) diff --git a/README-ID.md b/docs/README-ID.md similarity index 87% rename from README-ID.md rename to docs/README-ID.md index 3ea9bc454..2a6c13ce0 100644 --- a/README-ID.md +++ b/docs/README-ID.md @@ -5,7 +5,7 @@ DockerStructureSnapshot
    - [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
    + [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
    Kami membutuhkan bantuan Anda untuk menerjemahkan README ini dan RustDesk UI ke bahasa asli anda

    @@ -15,7 +15,7 @@ Birbincang bersama kami: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](ht Perangkat lunak desktop jarak jauh lainnya, ditulis dengan Rust. Bekerja begitu saja, tidak memerlukan konfigurasi. Anda memiliki kendali penuh atas data Anda, tanpa khawatir tentang keamanan. Anda dapat menggunakan server rendezvous/relay kami, [konfigurasi server sendiri](https://rustdesk.com/server), or [tulis rendezvous/relay server anda sendiri](https://github.com/rustdesk/rustdesk-server-demo). -RustDesk menyambut baik kontribusi dari semua orang. Lihat [`CONTRIBUTING.md`](CONTRIBUTING.md) untuk membantu sebelum memulai. +RustDesk menyambut baik kontribusi dari semua orang. Lihat [`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md) untuk membantu sebelum memulai. [**BINARY DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases) diff --git a/README-IT.md b/docs/README-IT.md similarity index 87% rename from README-IT.md rename to docs/README-IT.md index a79c28153..5ca2af3f7 100644 --- a/README-IT.md +++ b/docs/README-IT.md @@ -5,7 +5,7 @@ DockerStrutturaScreenshots
    - [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
    + [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
    Abbiamo bisogno del tuo aiuto per tradurre questo README e la RustDesk UI nella tua lingua nativa

    @@ -15,7 +15,7 @@ Chatta con noi: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twi Ancora un altro software per il controllo remoto del desktop, scritto in Rust. Funziona immediatamente, nessuna configurazione richiesta. Hai il pieno controllo dei tuoi dati, senza preoccupazioni per la sicurezza. Puoi utilizzare il nostro server rendezvous/relay, [configurare il tuo](https://rustdesk.com/server) o [scrivere il tuo rendezvous/relay server](https://github.com/rustdesk/rustdesk-server-demo). -RustDesk accoglie il contributo di tutti. Per ulteriori informazioni su come inizare a contribuire, vedere [`CONTRIBUTING.md`](CONTRIBUTING.md). +RustDesk accoglie il contributo di tutti. Per ulteriori informazioni su come inizare a contribuire, vedere [`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md). [**BINARY DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases) diff --git a/README-JP.md b/docs/README-JP.md similarity index 89% rename from README-JP.md rename to docs/README-JP.md index 394fbc52a..a36258925 100644 --- a/README-JP.md +++ b/docs/README-JP.md @@ -5,7 +5,7 @@ DockerStructureSnapshot
    - [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
    + [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
    このREADMEをあなたの母国語に翻訳するために、あなたの助けが必要です。

    @@ -18,7 +18,7 @@ Rustで書かれた、設定不要ですぐに使えるリモートデスクト ![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) -RustDeskは誰からの貢献も歓迎します。 貢献するには [`CONTRIBUTING.md`](CONTRIBUTING.md) を参照してください。 +RustDeskは誰からの貢献も歓迎します。 貢献するには [`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md) を参照してください。 [**RustDeskはどの様に動くのか?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F) diff --git a/README-KR.md b/docs/README-KR.md similarity index 88% rename from README-KR.md rename to docs/README-KR.md index 9a87d8ab1..89e22860a 100644 --- a/README-KR.md +++ b/docs/README-KR.md @@ -5,7 +5,7 @@ DockerStructureSnapshot
    - [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
    + [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
    README를 모국어로 번역하기 위한 당신의 도움의 필요합니다.

    @@ -18,7 +18,7 @@ Rust로 작성되었고, 설정없이 바로 사용할 수 있는 원격 데스 ![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) -RustDesk는 모든 기여를 환영합니다. 기여하고자 한다면 [`CONTRIBUTING.md`](CONTRIBUTING.md)를 참조해주세요. +RustDesk는 모든 기여를 환영합니다. 기여하고자 한다면 [`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md)를 참조해주세요. [**RustDesk는 어떻게 작동하는가?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F) diff --git a/README-ML.md b/docs/README-ML.md similarity index 91% rename from README-ML.md rename to docs/README-ML.md index 45496b129..435174165 100644 --- a/README-ML.md +++ b/docs/README-ML.md @@ -5,7 +5,7 @@ DockerStructureSnapshot
    - [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
    + [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
    ഈ README നിങ്ങളുടെ മാതൃഭാഷയിലേക്ക് വിവർത്തനം ചെയ്യാൻ ഞങ്ങൾക്ക് നിങ്ങളുടെ സഹായം ആവശ്യമാണ്

    @@ -15,7 +15,7 @@ റസ്റ്റിൽ എഴുതിയ മറ്റൊരു റിമോട്ട് ഡെസ്ക്ടോപ്പ് സോഫ്റ്റ്‌വെയർ. ബോക്‌സിന് പുറത്ത് പ്രവർത്തിക്കുന്നു, കോൺഫിഗറേഷൻ ആവശ്യമില്ല. സുരക്ഷയെക്കുറിച്ച് ആശങ്കകളൊന്നുമില്ലാതെ, നിങ്ങളുടെ ഡാറ്റയുടെ പൂർണ്ണ നിയന്ത്രണം നിങ്ങൾക്കുണ്ട്. നിങ്ങൾക്ക് ഞങ്ങളുടെ rendezvous/relay സെർവർ ഉപയോഗിക്കാം, [സ്വന്തമായി സജ്ജീകരിക്കുക](https://rustdesk.com/server), അല്ലെങ്കിൽ [നിങ്ങളുടെ സ്വന്തം rendezvous/relay സെർവർ എഴുതുക](https://github.com/rustdesk/rustdesk-server-demo). -എല്ലാവരുടെയും സംഭാവനയെ RustDesk സ്വാഗതം ചെയ്യുന്നു. ആരംഭിക്കുന്നതിനുള്ള സഹായത്തിന് [`CONTRIBUTING.md`](CONTRIBUTING.md) കാണുക. +എല്ലാവരുടെയും സംഭാവനയെ RustDesk സ്വാഗതം ചെയ്യുന്നു. ആരംഭിക്കുന്നതിനുള്ള സഹായത്തിന് [`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md) കാണുക. [**BINARY DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases) diff --git a/README-NL.md b/docs/README-NL.md similarity index 87% rename from README-NL.md rename to docs/README-NL.md index 8a4a119fc..0e0314b50 100644 --- a/README-NL.md +++ b/docs/README-NL.md @@ -5,7 +5,7 @@ DockerStructuurSnapshot
    - [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
    + [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
    We hebben je hulp nodig om deze README te vertalen naar jouw moedertaal

    @@ -15,7 +15,7 @@ Praat met ons: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twit Nog weer een applicatie voor toegang op afstand, geschreven in Rust. Werkt meteen, geen configuratie nodig. Je hebt volledig beheer over je data, zonder na te hoeven denken over veiligheid. Je kunt onze rendez-vous/relay-server gebruiken, [je eigen server opzetten](https://rustdesk.com/blog/id-relay-set), of [je eigen rendez-vous/relay-server schrijven](https://github.com/rustdesk/rustdesk-server-demo). -RustDesk verwelkomt bijdragen van iedereen. Zie [`CONTRIBUTING.md`](CONTRIBUTING.md) om te lezen hoe je van start kunt gaan. +RustDesk verwelkomt bijdragen van iedereen. Zie [`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md) om te lezen hoe je van start kunt gaan. [**BINARY DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases) diff --git a/README-PL.md b/docs/README-PL.md similarity index 87% rename from README-PL.md rename to docs/README-PL.md index d3b298d5a..8464401b2 100644 --- a/README-PL.md +++ b/docs/README-PL.md @@ -5,7 +5,7 @@ DockerStrukturaSnapshot
    - [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
    + [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
    Potrzebujemy twojej pomocy w tłumaczeniu README na twój ojczysty język

    @@ -15,7 +15,7 @@ Porozmawiaj z nami na: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](http Kolejny program do zdalnego pulpitu, napisany w Rust. Działa od samego początku, nie wymaga konfiguracji. Masz pełną kontrolę nad swoimi danymi, bez obaw o bezpieczeństwo. Możesz skorzystać z naszego darmowego serwera publicznego , [skonfigurować własny](https://rustdesk.com/server), lub [napisać własny serwer rendezvous/relay server](https://github.com/rustdesk/rustdesk-server-demo). -RustDesk zaprasza do współpracy każdego. Zobacz [`CONTRIBUTING.md`](CONTRIBUTING.md) pomoc w uruchomieniu programu. +RustDesk zaprasza do współpracy każdego. Zobacz [`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md) pomoc w uruchomieniu programu. [**POBIERZ KOMPILACJE**](https://github.com/rustdesk/rustdesk/releases) diff --git a/README-PTBR.md b/docs/README-PTBR.md similarity index 87% rename from README-PTBR.md rename to docs/README-PTBR.md index 020941831..d575f7a19 100644 --- a/README-PTBR.md +++ b/docs/README-PTBR.md @@ -5,7 +5,7 @@ DockerEstruturaScreenshots
    - [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
    + [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
    Precisamos de sua ajuda para traduzir este README e a UI do RustDesk para sua língua nativa

    @@ -15,7 +15,7 @@ Converse conosco: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://t Mais um software de desktop remoto, escrito em Rust. Funciona por padrão, sem necessidade de configuração. Você tem completo controle de seus dados, sem se preocupar com segurança. Você pode usar nossos servidores de rendezvous/relay, [configurar seu próprio](https://rustdesk.com/server), ou [escrever seu próprio servidor de rendezvous/relay](https://github.com/rustdesk/rustdesk-server-demo). -RustDesk acolhe contribuições de todos. Leia [`CONTRIBUTING.md`](CONTRIBUTING.md) para ver como começar. +RustDesk acolhe contribuições de todos. Leia [`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md) para ver como começar. [**DOWNLOAD DE BINÁRIOS**](https://github.com/rustdesk/rustdesk/releases) diff --git a/README-RU.md b/docs/README-RU.md similarity index 90% rename from README-RU.md rename to docs/README-RU.md index d610f2dc5..1e5e35f02 100644 --- a/README-RU.md +++ b/docs/README-RU.md @@ -5,7 +5,7 @@ DockerStructureSnapshot
    - [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
    + [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
    Нам нужна ваша помощь для перевода этого README и RustDesk UI на ваш родной язык

    @@ -17,7 +17,7 @@ ![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) -RustDesk приветствует вклад каждого. Смотрите [`CONTRIBUTING.md`](CONTRIBUTING.md) для помощи в начале работы. +RustDesk приветствует вклад каждого. Смотрите [`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md) для помощи в начале работы. [**Как работает RustDesk?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F) diff --git a/README-VN.md b/docs/README-VN.md similarity index 90% rename from README-VN.md rename to docs/README-VN.md index b7b683e4f..6b6eb0532 100644 --- a/README-VN.md +++ b/docs/README-VN.md @@ -5,7 +5,7 @@ DockerCấu trúc tệp tinSnapshot
    - [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
    + [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
    Chúng tôi cần sự gíup đỡ của bạn để dịch trang README này, RustDesk UItài liệu sang ngôn ngữ bản địa của bạn

    @@ -17,7 +17,7 @@ Một phần mềm điểu khiển máy tính từ xa, đuợc lập trình bằ ![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) -Mọi người đều đuợc chào đón để đóng góp vào RustDesk. Để bắt đầu, hãy đọc [`CONTRIBUTING.md`](CONTRIBUTING.md). +Mọi người đều đuợc chào đón để đóng góp vào RustDesk. Để bắt đầu, hãy đọc [`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md). [**RustDesk hoạt động như thế nào?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F) diff --git a/README-ZH.md b/docs/README-ZH.md similarity index 89% rename from README-ZH.md rename to docs/README-ZH.md index e17254670..e224a3bf3 100644 --- a/README-ZH.md +++ b/docs/README-ZH.md @@ -5,7 +5,7 @@ Docker结构截图
    - [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
    + [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]

    Chat with us: [知乎](https://www.zhihu.com/people/rustdesk) | [Discord](https://discord.gg/nDceKgxnkV) | [Reddit](https://www.reddit.com/r/rustdesk) @@ -16,7 +16,7 @@ Chat with us: [知乎](https://www.zhihu.com/people/rustdesk) | [Discord](https: 或者[自己设置](https://rustdesk.com/server), 亦或者[开发您的版本](https://github.com/rustdesk/rustdesk-server-demo)。 -欢迎大家贡献代码, 请看 [`CONTRIBUTING.md`](CONTRIBUTING.md). +欢迎大家贡献代码, 请看 [`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md). [**可执行程序下载**](https://github.com/rustdesk/rustdesk/releases) diff --git a/SECURITY.md b/docs/SECURITY.md similarity index 100% rename from SECURITY.md rename to docs/SECURITY.md From 9bb52db1cd92f797c2764e187784e5ecdec5e8c7 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 16 Sep 2022 22:55:21 +0800 Subject: [PATCH 0500/2015] fix logo path --- docs/README-AR.md | 2 +- docs/README-CS.md | 2 +- docs/README-DE.md | 2 +- docs/README-EO.md | 2 +- docs/README-ES.md | 2 +- docs/README-FA.md | 2 +- docs/README-FI.md | 2 +- docs/README-FR.md | 2 +- docs/README-HU.md | 2 +- docs/README-ID.md | 2 +- docs/README-IT.md | 2 +- docs/README-JP.md | 2 +- docs/README-KR.md | 2 +- docs/README-ML.md | 2 +- docs/README-NL.md | 2 +- docs/README-PL.md | 2 +- docs/README-PTBR.md | 2 +- docs/README-RU.md | 2 +- docs/README-VN.md | 2 +- docs/README-ZH.md | 2 +- 20 files changed, 20 insertions(+), 20 deletions(-) diff --git a/docs/README-AR.md b/docs/README-AR.md index 62867faca..0344c90e1 100644 --- a/docs/README-AR.md +++ b/docs/README-AR.md @@ -1,5 +1,5 @@

    - RustDesk - Your remote desktop
    + RustDesk - Your remote desktop
    ServersBuildDocker • diff --git a/docs/README-CS.md b/docs/README-CS.md index eebcfc46b..809cd0694 100644 --- a/docs/README-CS.md +++ b/docs/README-CS.md @@ -1,5 +1,5 @@

    - RustDesk – vaše vzdálená plocha
    + RustDesk – vaše vzdálená plocha
    ServerySestavení ze zdrojových kódůDocker • diff --git a/docs/README-DE.md b/docs/README-DE.md index dafc28d7e..b927075b6 100644 --- a/docs/README-DE.md +++ b/docs/README-DE.md @@ -1,5 +1,5 @@

    - RustDesk - Your remote desktop
    + RustDesk - Your remote desktop
    ServerKompilierenDocker • diff --git a/docs/README-EO.md b/docs/README-EO.md index 321a46ca4..8e8669890 100644 --- a/docs/README-EO.md +++ b/docs/README-EO.md @@ -1,5 +1,5 @@

    - RustDesk - Your remote desktop
    + RustDesk - Your remote desktop
    ServilojKompiliDocker • diff --git a/docs/README-ES.md b/docs/README-ES.md index f14d96b78..e6a8ec8c9 100644 --- a/docs/README-ES.md +++ b/docs/README-ES.md @@ -1,5 +1,5 @@

    - RustDesk - Your remote desktop
    + RustDesk - Your remote desktop
    ServidoresCompilarDocker • diff --git a/docs/README-FA.md b/docs/README-FA.md index 73a8d6914..16c9f196e 100644 --- a/docs/README-FA.md +++ b/docs/README-FA.md @@ -1,5 +1,5 @@

    - RustDesk - Your remote desktop
    + RustDesk - Your remote desktop
    اسنپ شاتساختارداکر • diff --git a/docs/README-FI.md b/docs/README-FI.md index 910b787f2..795b8b883 100644 --- a/docs/README-FI.md +++ b/docs/README-FI.md @@ -1,5 +1,5 @@

    - RustDesk - Etätyöpöytäsi
    + RustDesk - Etätyöpöytäsi
    PalvelimetRakennaDocker • diff --git a/docs/README-FR.md b/docs/README-FR.md index 2dab81b7a..fa1b32f18 100644 --- a/docs/README-FR.md +++ b/docs/README-FR.md @@ -1,5 +1,5 @@

    - RustDesk - Your remote desktop
    + RustDesk - Your remote desktop
    Serveurs - Build - Docker - diff --git a/docs/README-HU.md b/docs/README-HU.md index a682a23c4..bae9b3c7a 100644 --- a/docs/README-HU.md +++ b/docs/README-HU.md @@ -1,5 +1,5 @@

    - RustDesk - Your remote desktop
    + RustDesk - Your remote desktop
    SzerverekÉpítésDocker • diff --git a/docs/README-ID.md b/docs/README-ID.md index 2a6c13ce0..e7a84dfed 100644 --- a/docs/README-ID.md +++ b/docs/README-ID.md @@ -1,5 +1,5 @@

    - RustDesk - Your remote desktop
    + RustDesk - Your remote desktop
    ServersBuildDocker • diff --git a/docs/README-IT.md b/docs/README-IT.md index 5ca2af3f7..9eaa89ecf 100644 --- a/docs/README-IT.md +++ b/docs/README-IT.md @@ -1,5 +1,5 @@

    - RustDesk - Your remote desktop
    + RustDesk - Your remote desktop
    ServersCompilazioneDocker • diff --git a/docs/README-JP.md b/docs/README-JP.md index a36258925..8e29892bc 100644 --- a/docs/README-JP.md +++ b/docs/README-JP.md @@ -1,5 +1,5 @@

    - RustDesk - Your remote desktop
    + RustDesk - Your remote desktop
    ServersBuildDocker • diff --git a/docs/README-KR.md b/docs/README-KR.md index 89e22860a..d4779c60f 100644 --- a/docs/README-KR.md +++ b/docs/README-KR.md @@ -1,5 +1,5 @@

    - RustDesk - Your remote desktop
    + RustDesk - Your remote desktop
    ServersBuildDocker • diff --git a/docs/README-ML.md b/docs/README-ML.md index 435174165..b5badba4f 100644 --- a/docs/README-ML.md +++ b/docs/README-ML.md @@ -1,5 +1,5 @@

    - RustDesk - Your remote desktop
    + RustDesk - Your remote desktop
    ServersBuildDocker • diff --git a/docs/README-NL.md b/docs/README-NL.md index 0e0314b50..8cd9c8b0e 100644 --- a/docs/README-NL.md +++ b/docs/README-NL.md @@ -1,5 +1,5 @@

    - RustDesk - Jouw verbinding op afstand
    + RustDesk - Jouw verbinding op afstand
    ServersBouwenDocker • diff --git a/docs/README-PL.md b/docs/README-PL.md index 8464401b2..a755c8ded 100644 --- a/docs/README-PL.md +++ b/docs/README-PL.md @@ -1,5 +1,5 @@

    - RustDesk - Your remote desktop
    + RustDesk - Your remote desktop
    SerweryKompilacjaDocker • diff --git a/docs/README-PTBR.md b/docs/README-PTBR.md index d575f7a19..a2c6e8a4a 100644 --- a/docs/README-PTBR.md +++ b/docs/README-PTBR.md @@ -1,5 +1,5 @@

    - RustDesk - Seu desktop remoto
    + RustDesk - Seu desktop remoto
    ServidoresCompilarDocker • diff --git a/docs/README-RU.md b/docs/README-RU.md index 1e5e35f02..f89454cc6 100644 --- a/docs/README-RU.md +++ b/docs/README-RU.md @@ -1,5 +1,5 @@

    - RustDesk - Ваш удаленый рабочий стол
    + RustDesk - Ваш удаленый рабочий стол
    ServersBuildDocker • diff --git a/docs/README-VN.md b/docs/README-VN.md index 6b6eb0532..d3999fc50 100644 --- a/docs/README-VN.md +++ b/docs/README-VN.md @@ -1,5 +1,5 @@

    - RustDesk - Phần mềm điểu khiển máy tính từ xa dành cho bạn
    + RustDesk - Phần mềm điểu khiển máy tính từ xa dành cho bạn
    Máy chủBuildDocker • diff --git a/docs/README-ZH.md b/docs/README-ZH.md index e224a3bf3..2a0f47f27 100644 --- a/docs/README-ZH.md +++ b/docs/README-ZH.md @@ -1,5 +1,5 @@

    - RustDesk - Your remote desktop
    + RustDesk - Your remote desktop
    服务器编译Docker • From bcf52d2cd87d09d9714c60ea54bc8403d39f88c0 Mon Sep 17 00:00:00 2001 From: LaLucid <55886143+VoidxHoshi@users.noreply.github.com> Date: Sat, 17 Sep 2022 17:40:50 +0800 Subject: [PATCH 0501/2015] Create dependabot.yml --- .github/dependabot.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..761e23e0c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,27 @@ +version: 2 +updates: + + - package-ecosystem: "cargo" + directory: "/" + schedule: + interval: "daily" + + - package-ecosystem: "docker" + directory: "/" + schedule: + interval: "daily" + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + + - package-ecosystem: "gradle" + directory: "/flutter/android" + schedule: + interval: "daily" + + - package-ecosystem: "pub" + directory: "/flutter" + schedule: + interval: "daily" From b57fcb188096b4b3fa5c0e30d2e8280bf9b2be2e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 17 Sep 2022 11:26:01 +0000 Subject: [PATCH 0502/2015] Bump actions/checkout from 2 to 3 Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v2...v3) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 6 +++--- .github/workflows/flutter-ci.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7cdca74ac..235b14be7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,7 +24,7 @@ jobs: # default: true # profile: minimal # components: rustfmt - # - uses: actions/checkout@v2 + # - uses: actions/checkout@v3 # - run: cargo fmt -- --check # min_version: @@ -32,7 +32,7 @@ jobs: # runs-on: ubuntu-20.04 # steps: # - name: Checkout source code - # uses: actions/checkout@v2 + # uses: actions/checkout@v3 # - name: Install rust toolchain (v${{ env.MIN_SUPPORTED_RUST_VERSION }}) # uses: actions-rs/toolchain@v1 @@ -72,7 +72,7 @@ jobs: # - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } steps: - name: Checkout source code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Install prerequisites shell: bash diff --git a/.github/workflows/flutter-ci.yml b/.github/workflows/flutter-ci.yml index 9cdd1fb91..98eb3ab6a 100644 --- a/.github/workflows/flutter-ci.yml +++ b/.github/workflows/flutter-ci.yml @@ -30,7 +30,7 @@ jobs: # - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } steps: - name: Checkout source code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Install prerequisites shell: bash From ec0e5f52f99537820139427d464f21c3b2726a7e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 17 Sep 2022 11:26:30 +0000 Subject: [PATCH 0503/2015] Bump hound from 3.4.0 to 3.5.0 Bumps [hound](https://github.com/ruuda/hound) from 3.4.0 to 3.5.0. - [Release notes](https://github.com/ruuda/hound/releases) - [Changelog](https://github.com/ruuda/hound/blob/master/changelog.md) - [Commits](https://github.com/ruuda/hound/commits) --- updated-dependencies: - dependency-name: hound dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c4941a1b7..ef85d46dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2345,9 +2345,9 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hound" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a164bb2ceaeff4f42542bdb847c41517c78a60f5649671b2a07312b6e117549" +checksum = "4d13cdbd5dbb29f9c88095bbdc2590c9cba0d0a1269b983fef6b2cdd7e9f4db1" [[package]] name = "http" diff --git a/Cargo.toml b/Cargo.toml index c65e73d82..90af17138 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -137,7 +137,7 @@ simple_rc = { path = "libs/simple_rc", optional = true } flutter_rust_bridge_codegen = { git = "https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge" } [dev-dependencies] -hound = "3.4" +hound = "3.5" [package.metadata.bundle] name = "RustDesk" From 519ebf4fee5658f44b7aadc6818b38ced82feecc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 17 Sep 2022 11:26:32 +0000 Subject: [PATCH 0504/2015] Bump kotlin-gradle-plugin from 1.6.10 to 1.7.10 in /flutter/android Bumps [kotlin-gradle-plugin](https://github.com/JetBrains/kotlin) from 1.6.10 to 1.7.10. - [Release notes](https://github.com/JetBrains/kotlin/releases) - [Changelog](https://github.com/JetBrains/kotlin/blob/v1.7.10/ChangeLog.md) - [Commits](https://github.com/JetBrains/kotlin/compare/v1.6.10...v1.7.10) --- updated-dependencies: - dependency-name: org.jetbrains.kotlin:kotlin-gradle-plugin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- flutter/android/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/android/build.gradle b/flutter/android/build.gradle index e34fecc69..c1f65e3b6 100644 --- a/flutter/android/build.gradle +++ b/flutter/android/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.6.10' + ext.kotlin_version = '1.7.10' repositories { google() jcenter() From 827c53549033ff333f8b0b82c14f3198eb2a0be0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 17 Sep 2022 11:26:34 +0000 Subject: [PATCH 0505/2015] Bump gradle from 7.0.2 to 7.3.0 in /flutter/android Bumps gradle from 7.0.2 to 7.3.0. --- updated-dependencies: - dependency-name: com.android.tools.build:gradle dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- flutter/android/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/android/build.gradle b/flutter/android/build.gradle index e34fecc69..02f30d4de 100644 --- a/flutter/android/build.gradle +++ b/flutter/android/build.gradle @@ -7,7 +7,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:7.0.2' + classpath 'com.android.tools.build:gradle:7.3.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath 'com.google.gms:google-services:4.3.3' } From 191e7cf0bab5fe24323848e8c9c5f495730d325e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 17 Sep 2022 11:26:35 +0000 Subject: [PATCH 0506/2015] Bump google-services from 4.3.3 to 4.3.14 in /flutter/android Bumps google-services from 4.3.3 to 4.3.14. --- updated-dependencies: - dependency-name: com.google.gms:google-services dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- flutter/android/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/android/build.gradle b/flutter/android/build.gradle index e34fecc69..bd7315a7f 100644 --- a/flutter/android/build.gradle +++ b/flutter/android/build.gradle @@ -9,7 +9,7 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:7.0.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - classpath 'com.google.gms:google-services:4.3.3' + classpath 'com.google.gms:google-services:4.3.14' } } From 8be67360767a10b1759f652bccfc458f3a900ced Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 17 Sep 2022 11:26:37 +0000 Subject: [PATCH 0507/2015] Bump media from 1.4.3 to 1.6.0 in /flutter/android Bumps media from 1.4.3 to 1.6.0. --- updated-dependencies: - dependency-name: androidx.media:media dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- flutter/android/app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/android/app/build.gradle b/flutter/android/app/build.gradle index 94fc645af..10f0f4298 100644 --- a/flutter/android/app/build.gradle +++ b/flutter/android/app/build.gradle @@ -74,7 +74,7 @@ flutter { } dependencies { - implementation "androidx.media:media:1.4.3" + implementation "androidx.media:media:1.6.0" implementation 'com.github.getActivity:XXPermissions:13.2' implementation("org.jetbrains.kotlin:kotlin-stdlib") { version { strictly("$kotlin_version") } } } From e871663a86afa600d27bf7029b4c911e81f2e07c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 17 Sep 2022 11:26:44 +0000 Subject: [PATCH 0508/2015] Bump sha2 from 0.10.2 to 0.10.6 Bumps [sha2](https://github.com/RustCrypto/hashes) from 0.10.2 to 0.10.6. - [Release notes](https://github.com/RustCrypto/hashes/releases) - [Commits](https://github.com/RustCrypto/hashes/compare/sha2-v0.10.2...sha2-v0.10.6) --- updated-dependencies: - dependency-name: sha2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c4941a1b7..eadb8557c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1237,9 +1237,9 @@ checksum = "21d8ad60dd5b13a4ee6bd8fa2d5d88965c597c67bce32b5fc49c94f55cb50810" [[package]] name = "digest" -version = "0.10.3" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" +checksum = "adfbc57365a37acbd2ebf2b64d7e69bb766e2fea813521ed536f5d0520dcf86c" dependencies = [ "block-buffer", "crypto-common", @@ -4639,9 +4639,9 @@ checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" [[package]] name = "sha2" -version = "0.10.2" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55deaec60f81eefe3cce0dc50bda92d6d8e88f2a27df7c5033b42afeb1ed2676" +checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" dependencies = [ "cfg-if 1.0.0", "cpufeatures", From 7d7f0e623839221257b3c1ea3a9fba2d4e6c2e51 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 17 Sep 2022 11:26:44 +0000 Subject: [PATCH 0509/2015] Bump tokio-util from 0.7.3 to 0.7.4 Bumps [tokio-util](https://github.com/tokio-rs/tokio) from 0.7.3 to 0.7.4. - [Release notes](https://github.com/tokio-rs/tokio/releases) - [Commits](https://github.com/tokio-rs/tokio/compare/tokio-util-0.7.3...tokio-util-0.7.4) --- updated-dependencies: - dependency-name: tokio-util dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c4941a1b7..e9f9b25f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5108,9 +5108,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.20.1" +version = "1.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a8325f63a7d4774dd041e363b2409ed1c5cbbd0f867795e661df066b2b0a581" +checksum = "0020c875007ad96677dcc890298f4b942882c5d4eb7cc8f439fc3bf813dc9c95" dependencies = [ "autocfg 1.1.0", "bytes", @@ -5167,9 +5167,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc463cd8deddc3770d20f9852143d50bf6094e640b485cb2e189a2099085ff45" +checksum = "0bb2e075f03b3d66d8d8785356224ba688d2906a371015e225beeb65ca92c740" dependencies = [ "bytes", "futures-core", From e9610a76890ee517d576468b0e80e44f093aff8e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 17 Sep 2022 13:47:36 +0000 Subject: [PATCH 0510/2015] Bump XXPermissions from 13.2 to 16.2 in /flutter/android Bumps [XXPermissions](https://github.com/getActivity/XXPermissions) from 13.2 to 16.2. - [Release notes](https://github.com/getActivity/XXPermissions/releases) - [Commits](https://github.com/getActivity/XXPermissions/compare/13.2...16.2) --- updated-dependencies: - dependency-name: com.github.getActivity:XXPermissions dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- flutter/android/app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/android/app/build.gradle b/flutter/android/app/build.gradle index 10f0f4298..326689e5e 100644 --- a/flutter/android/app/build.gradle +++ b/flutter/android/app/build.gradle @@ -75,7 +75,7 @@ flutter { dependencies { implementation "androidx.media:media:1.6.0" - implementation 'com.github.getActivity:XXPermissions:13.2' + implementation 'com.github.getActivity:XXPermissions:16.2' implementation("org.jetbrains.kotlin:kotlin-stdlib") { version { strictly("$kotlin_version") } } } From ad942b21389beaf72dc1539c24ddacc31ce4a3d6 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sat, 17 Sep 2022 21:57:56 +0800 Subject: [PATCH 0511/2015] anonying --- .github/dependabot.yml | 27 --------------------------- 1 file changed, 27 deletions(-) delete mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 761e23e0c..000000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,27 +0,0 @@ -version: 2 -updates: - - - package-ecosystem: "cargo" - directory: "/" - schedule: - interval: "daily" - - - package-ecosystem: "docker" - directory: "/" - schedule: - interval: "daily" - - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "daily" - - - package-ecosystem: "gradle" - directory: "/flutter/android" - schedule: - interval: "daily" - - - package-ecosystem: "pub" - directory: "/flutter" - schedule: - interval: "daily" From f64d2a3983ac913be3f24afef1c9b10b4327f0d4 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sun, 18 Sep 2022 11:02:27 +0800 Subject: [PATCH 0512/2015] fix translation --- flutter/lib/mobile/widgets/dialog.dart | 2 +- src/lang/en.rs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/flutter/lib/mobile/widgets/dialog.dart b/flutter/lib/mobile/widgets/dialog.dart index 4169eecdf..503b82c50 100644 --- a/flutter/lib/mobile/widgets/dialog.dart +++ b/flutter/lib/mobile/widgets/dialog.dart @@ -6,7 +6,7 @@ import '../../models/model.dart'; import '../../models/platform_model.dart'; void clientClose(OverlayDialogManager dialogManager) { - msgBox('', 'Close', 'Are you sure you want to close the connection?', + msgBox('', 'Close', 'Are you sure to close the connection?', dialogManager); } diff --git a/src/lang/en.rs b/src/lang/en.rs index 8b21af4ef..14232c4e2 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -29,5 +29,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("server_not_support", "Not yet supported by the server"), ("android_open_battery_optimizations_tip", "If you want to disable this feature, please go to the next RustDesk application settings page, find and enter [Battery], Uncheck [Unrestricted]"), ("remote_restarting_tip", "Remote device is restarting, please close this message box and reconnect with permanent password after a while"), + ("Are you sure to close the connection?", "Are you sure you want to close the connection?"), ].iter().cloned().collect(); } From 2567256dcbd0318a5f1a97075dbf64f2841ad4e7 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sun, 18 Sep 2022 11:22:30 +0800 Subject: [PATCH 0513/2015] moved all png to res --- Cargo.toml | 4 +- .../Icon-App-1024x1024@1x.png | Bin 61199 -> 12076 bytes .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin 618 -> 364 bytes .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin 1209 -> 574 bytes .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin 1798 -> 811 bytes .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin 893 -> 467 bytes .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin 1787 -> 806 bytes .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin 2621 -> 1101 bytes .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin 1209 -> 574 bytes .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin 2401 -> 997 bytes .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin 3502 -> 1411 bytes .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin 3502 -> 1411 bytes .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin 4996 -> 2037 bytes .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin 2270 -> 939 bytes .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin 4317 -> 1735 bytes .../Icon-App-83.5x83.5@2x.png | Bin 4702 -> 1901 bytes flutter/pubspec.lock | 6 +- flutter/pubspec.yaml | 2 +- 128x128.png => res/128x128.png | Bin 128x128@2x.png => res/128x128@2x.png | Bin 32x32.png => res/32x32.png | Bin res/icon.png | Bin 0 -> 15849 bytes mac-tray-dark.png => res/mac-tray-dark.png | Bin mac-tray-light.png => res/mac-tray-light.png | Bin setup.nsi => res/setup.nsi | 224 +++++++++--------- {src => res}/tray-icon.ico | Bin src/tray.rs | 2 +- 27 files changed, 119 insertions(+), 119 deletions(-) rename 128x128.png => res/128x128.png (100%) rename 128x128@2x.png => res/128x128@2x.png (100%) rename 32x32.png => res/32x32.png (100%) create mode 100644 res/icon.png rename mac-tray-dark.png => res/mac-tray-dark.png (100%) rename mac-tray-light.png => res/mac-tray-light.png (100%) rename setup.nsi => res/setup.nsi (96%) rename {src => res}/tray-icon.ico (100%) diff --git a/Cargo.toml b/Cargo.toml index 90af17138..7ac714c04 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -142,10 +142,10 @@ hound = "3.5" [package.metadata.bundle] name = "RustDesk" identifier = "com.carriez.rustdesk" -icon = ["32x32.png", "128x128.png", "128x128@2x.png"] +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", "pulseaudio", "python3-pip", "curl"] osx_minimum_system_version = "10.14" -resources = ["mac-tray-light.png","mac-tray-dark.png"] +resources = ["res/mac-tray-light.png","res/mac-tray-dark.png"] #https://github.com/johnthagen/min-sized-rust [profile.release] diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png index 5216876c6753bc6e87705a06fa8698ce768d5068..d4ae9af185b3becf4062488b688aa74cdd78a6d0 100644 GIT binary patch literal 12076 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 delta 1793 zcmV+c2mbi028Irh8Gix*006a~P9*>U2F*!CK~#7F)mmvxR96)KkO2f2#9CAkaRV)C zL6EvqH6UsutrfLW72HCV#BN0U!&al!wk|PRiM4LnS~M0-MJgI`i&|x|R7A(EN>LG9 zP*jk`g`PVzb!PD1w=t>kWq#Z__rCkSym#(7-?{hcYHDg=fq&D5Sa}rtFQVunUOAdN zA=C$xgE6Y7m0kCu^{5h)wj$;1|IJJSWmSw1{ro+!JsMrzO?DIM(hBrhiA#5089!;{ z-bC*eI5rCbp6o9pP0zSKGd7~E8X*Zd5{Do!18MS#dd}Dg@|MWW$eRrvt-N~W$I>Xc zk5oN$6e}7QwtwY7N71ceA)^4g`Vfy*;cmG_oUS{(-Pnxf)mFyljp3xjoiZEV#}=e} zd16v;M07`&RxAqj{~jgfrmiqw?1*8!$xeQloQvaEQ6o8<5@;vw=FGT82P4oEc6KJC z@9X$D1oQWq{>~4Ft21j*GNa$D5L~>C4Vm~UyH3wuBY)}W-dH`FJ;Ima)xz9v*P?bW zQC}8;1;eoXFcu#`^)oGZS54Z*0SS?q@|JKw6GwBqv0!=^N6Z_BL0u4;h?2)@DX1jf zzXKAZm=7q#bIIO8osl;OA0;F8oGKP-NRvuTi-n`T%&%Tn13{RYSKh`#)Nal=62x&_{hB$TEy$dv8&D$DdeJzG`Z%ksc79yek5tgO_}Du zi_+1}9c`M+Tt#9(bvgo9m}8|h( z7#Dy@e>8Iv&d}ZszfD2d4~pW6Jn69k7~?POrGH}Wn}b-e4^IS(?&)*vy}*8-?!vrq zeDXHz8}UbmbYdap%QRb%o-;()MQbiP3A>aj+Xt11-;I+6*g6pog0&Gp2nokf{77ah z8PfD-sZ!wLVI{)XBJ-LoTPy6y#gk`9j^Xoe?0}eHEIA}Ii45tlNAT^Bt1*0?YQ{!T z&40mS+$6rAP}*90NJgJXksiSKxt3b1HDV*Byq$$LHOKzgJf-=3I-d+G&4EtFPbH~C2rrz_MK*dukVkn|hw z@NUVmQ;bVEu9hJmDFV^~9xUCIi6wxL^nbukeEZpAo9#E86!-uImhNqkDfSQ{>2B_P z`{_be#H3JI%4#!9Df7tmC0=PNz?G@F0yAeXtw*{(2v_LgL*N7=nE`Ovo zlaO@&J-)rKH+!eg>%Mtm*l%zW-2Y06)P+LQ`dfUvt26p_z#sX_xd{>6o^PioqA%um z86hN{Emm;jd)bC`m@nUc{+3mB{4#{3DT3w|p=Vo87pcd^>GD!t8lJ9rzX!kTcfHKD zQXwztPkc9$W2YQ8dk8B->Vh?qXn!II#&ln4NRj^e6e|L_fqCWtY|66QEl`EH0eDyN zn8`Qt6F%pJ<_rNy*+8gs$fseke(yy0E3 zWTfdJP=xpt+)=P!Bdv1@atv_jy3Xko}ynn@rufi2E{`)S5{m7nqqDDeXMCCvBK#(Q-|z{X5uUuRKva2G}6 zsm$S)fG||`G#jN=QSl-H)s@Scn;5bN)B9q1gz4m}7A#3nn_KPCvVZ$L1;@3jE{&&K z8#1snmu2JFezk@Ch1=%#)qAy10*L6h2atAN+igwIevU{>XYm2HIUVN1KGz|U-N6kR z*RbW3c?>0s_A%^XXI;b5D=c@O)hbu4NK5bK$bwum{Sy0eK`YE}iT264aB+Yx)TbeR z?*T)-qnjMuz(_~;Gyljrd@%S*1HB2FoWbTnTrU6Jm*F%t3&@(nNhGm=%@?u|)rRrJ1lTpCFk$L5y j=ti-ERv!7WrJ(;8)}0Fk{PA#A00000NkvXXu0mjfJ>qzQ diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png index 1dfae6a194c0f3aafbb29b1a64e2fc1678e37d92..ff419814de4dc3af5a029e96da9dacd44f5fd4b8 100644 GIT binary patch delta 452 zcmV;#0XzQv2GawO8Gi!+003c4mpuRg0EbXaR7C)B|NsC09D)D){r?1W{|tEl_4)tv z`2Xhd|9Q3l@%R7E;Ks4 z|Ht6}NSXgenE%wima6~&0S8G$K~yNueUk~3f-nq)6DVydAjsi*udb`^_5Xk0Cd^<1 zQr|EzFW-ldpV^ delta 881 zcmV-%1CIRD1N{b&8Gix*007zX@K^u<14KzgK~#7Ft(Iv>6;Tw&|N17id}^;;+bl~= zGlH-zGnF7kEk9JE5iKTWh$Stbf(q1(2n`8buu{ROAd<+y6!!(vq%5+?h0wB0Gjl7~ z*SYVF&YRcFdy)PbhI8-S`@74WbI;|}YW3tal1q?Lhu#s)MSs@jh~9>zU|5=gBWO?- z%G>5d)M{`HH>yzHikf(EG}C#3&Z@Z^m!E;7<}{{q*w#o4W`Do_*gaIr{2K|bnF5cZ5b7bSoR|{cS~;sw5{u*aQPA{n+?)szbZQzE zZ76=v?6fK*_`%ka|Fl4XJCXSOlQl5HJ?w=eUP7_(4bDEs$9}$;T#D-zh}w!PAy~4A zN9P@c$2E+I_gciII7tl?ikbC@d#FE=u}Ngrqq-fnr+=W*pjWAIE&z!o^4xtJVW;Hn zwmu{j>O*JM(t`_6kr~bxPi#k033Po=#0_>A?6gYJU;0~ljY!$UyqmP#2JQ}QFlny0 zvtSps7=n_1PolO1dpGb!iXB<*%Ei1v;q+<12$$6PL$K|YqE8~OiOGzi35|(qBV39@ zIf=M^!+&tJ<_B=K6Rk^96w|Z%HZ0F2kd{jdbTh)W;NY`bD8BtF&n;`kE?3@;+rcu( zxV%fm`ao&U+g$_tlMr#~+`JgbCPJcp$T)zQ2ar4w{ala|A{5iBp%%9jaVgR%eI(Cx zi1x+`8(bJZ!__kQtYK%aRzd68L$Mc{gAcvP z3TK0HI&MJ6!l43*?b7Fx=SJkCCSh!2P^ymtm+D^FOsoG00000NkvXX Hu0mjfu#>%d diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png index 626a8a31ba14d65f8466e8c863321ce789dd194a..b3cdc7920e6528bb0bf6b299dc28c7f7d9575bd7 100644 GIT binary patch delta 794 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 diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png index e8b1c78167efafa9589a5ea838c1970820826a0b..15b2e086da558ddc8b8f9e2e3fa6472e0ffc961c 100644 GIT binary patch delta 1091 zcmV-J1ibsb6wL^b8Gi!+002f7DP8~o0Hsh&R7C)B|NsC0{r>*~a{mo@|M2(!26X@X z{r~j&{~vi^H={|a~i>h%BN?*Cq;|52X*5PJXR@&Cr(|CPl5fw})0fB*RV z|LyhvrOp3|z5jHw|75BEOPl{UkN@55|JCUK%i{mM+W)Q5|9_y$|Cq)9dbR&mp#MFS z|1pgJoyq?ziT@>r|F_rwk-`6Num5VS|Bk@_S)%_%m;bNR|ESLYOLh_D000AHNkl*K>35qx8yR_*TCmAvT=*+F}vL;@V zjpi9TJI4*07Jt%^-xPV=frqqM^GjT#i8K|e=eSLzt8j`tK-xx!xPgbX2s^w(ge=}63lsktb6W2CEu1v{-sj{4ulpL+=wOY5F&#Mlmi5|H3pqOT zj~|riEVHfPY11xUxW`XIx~Mktc#1bPh_UfkhM2I8Cx7gVbmO>KE^$UZ`3jC5lE*#j zERf76#aWj;aG9?}`(Q|M&ap0w^ip6kEOl{^qgMi#Ni@d6ibS;~^hAa@sj_)GU>X?` zboDlJrAD`m{^a_f_T*dm+E3`{$D$5qxV5Mou_05awdh8;hiF!cWx|Gv6%aSLUr}bm zAg-ydGk?v@t#1O?EXdgbF?Goy)w~J3PLkb{tzGI}$o2T9&B}jTml%1~E%=YyK(0+p zL%V{zCKUK?%R$CxBMVTO@Y?WXT?*ei>b-9BijSo2<<%T&b2h##8OJY!6R7QYWolk> zvQS%UE?mg_1r27EE&|Cghn=+0(P10n@wT*tpMRy2yE(8Q@z|&y7_G|ilyUXv+v9Py z@(_a2s?Yr6bu(DIXhMzmKaONrkZ}Kgu!$5kmp6jio*x2|aRWAPo3Jq&YL4?0Zr`Tv z2{41olAkV9**Z9hx}ydgUCry(k6VG9$9up&WH!Y7Ch6%n@(o$#M^+m$Q=VPNgH)ZE zqJJzCm5`Vp$juC5avzVw`ovA4@Izeh(To=%aod>v~Z)Edkq#ch+9Sa&}F2!Z$%YT0y)+o-=yn`zkByjhq3y4FaH8gEWloJ{k zb{)}V4oHyU=ZDHab4m{ zOFSW8=Bs1~&ozRUiRVRr6jTe25`!#aDv#0Z*O`7Gi~cS+|zvi8{r+=nByH*$mEeeGX!mrz6pVf7Sd+>1fn6*);PV-Y9U>baXgA! zEum002ov JPDHLkV1gL&M*aW* literal 2621 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 IM6N<$f-Lg}DgXcg literal 2401 zcmV-n37+UgR4mCU z6C1S&X9yuOGBq^>!>JSr6+wLME%e|G+;h%-_iAFl^%jfw-oEFa@7*)(z0bMVVzpY~ zin*!S@C!B{bou3fnm}I!`650H9h$rN*5drp)9aD28K`4=%}gTgFuxMEAh07+;^6IR z_pP1aUyfnKA{;q$4XdqAS{{1OLCQn~b*lQXN^pK5!WZD=*(+Z$6CpXl^Raq7#spM; zQYo0OZS7NQBYHU!i!nKb|F0BGExER;tzgPJ^l#5?n+4M-#r?Ln6P<+oS(gQ0(_^wc zbWu|ez(Vuw^`N$bHQBI`cX9o&Jr|a1(U;QAPX(?#t=i51f(^yD1xEJ9T|ME~60^R- z${!sK`nJQoNHlGTow?X@2w(5R>GP)6$Q;1~+T+3g7}bY$+DMLpX9FxwtNNs;H`1oT zqdxynUIBSR7cU_*AM1Wba;90Xfx&_!`{0G!S+Fy8>R{Z>c7j8@RI072O zBP9p3H}WuJEQSg0+6s%uVpta~o7Ro(-Z}(s)u~&D8whoWu^Ai;2Y{8C6blqSri1{ z*Bd>&#bzgpF=`2R6THcm>sYW&>aC*&_sI z69#gmO}~*OvVv#c&as;Y|(6i3TCA6jOEox%VunV(SoD;vHPw8IeLlPWKXF?(FH8uh3TPMHWA&?(1MbS zQUyv07EThYvvJsj%Ui)mSpvc2(uZ`$H~XbZ6APxOU3<79ALG?;q)w)fAM&uffYW^% zPz?4K@b`iYXH$N!cpN-UV-+isdO6L}_*Zk=w(7x2_<($vEb{UueDSnz>A1-|qx zou$s?4nl8_LYE6hMN2tH1qQ>qaDXA%h4c~8LtwC5R7o3R!5R4i0~)9)Mqid8drDO( zbh)#qz$X2$Y-wV_Sx0zGBF9aGXK%%anPyE+_xBTYVB!nujJ#Mdg}ZBi!qi)|Z2WzA zvjdr#!Hh70!TyuH@*;^;#$$E1Krk^q)D*!n0|b#UA)nBKq~PtpW8VpM*UZ8X^}{2B z4DQKYTJl1nHq-!@?@}mJHck81HYAP~*ihy69OsOTA+&0Y&mZG0T-s#h@x&>Qtl-6I z+?P6OhKl6we*FaRN=VL9sDVpDJ7ehs=-g6lxnR3m$+CiJTA#Za^G6BH=z^?>VMpbO zHL`hAXK1{5Gg2xJ+j~O!$5mKcTAGfT!==_TCk*0cx7d~3!juw4QPF=)FOR#XqtI0 z+)nV=06l6?79)J2{)Ia|yLf1vh)%-xskm9O9s1Z0c=FkE8{}q8T;rOu6M=gTl= z8MaN88nkUVY%XmomuIw5R*tw;_|(0-QuJ>SWrFL(NB!<}|g&3cV@7=zzHjLRIUQmSL@8HQ`JbxQ+x~Pd`_LoR|H!Zc{H&S`4 z-q}(uGgS<`1)%)$t_O-S9_)`v!MwTH8HX@E^N8!BXmm(OMba+i{lzjEe3J*ckV}5x z*Psq~pg)Fp;~}kL5hUj*-nNfyQV(F!PD~2o|E*Sz1X2&;(;RHtuk0bqW7d638Tpmj zzee@p>%{_G?pC9qirOvBPxP&+c~2XbKwsX*YaTSJX>Sd@0?W%b{C!{vGFk9{3hG~$ z_%MC~#B~Bi+LjK@`FR&<=I_(E`dE39ga@sZIDXzm?SAM1v3df$n_L#`?a5E~2%dkn zYh)(zp_PyFsGOF9I`Pvzq7`?SUn@}8of}#Cf7LupKR{wJKkuTJLD$x%77mWbmZm_gmXTX)18^w5V<8&tj)1%&#p8-ojw8Gi!+000iU#^3+|0HRP#R7C)B|NsC0{QdtAdH(}){|9yd&gB0X ze*f$A|45nt^!fji!v8vw|EJFXhrIu9um5DJ|22>QC58VCcmMeN|L5}m-R%Fk*Z+UG z|0#(76MX;g_W$GX|JmyQs?h&apZ_t8|JLaLEsFoe-~XM+|9{fv|FP8nqRjtvvj0w; z|Cq-AjKBXvmj51t|KRQaz1shTyZ>se|6Qg3dbR&pqW}8*|B>(ussI24oJmAMRA}Dp z*IRR{IdPSOaMrhl$xB)Ky2WulSPmTud`<#_V}zr5lg<$qD^2Dk{bXYt1j2P^*2cBz50 z>&j#^Mp@)m~CyV=tlL&cpdc9p+52H>610=s1(d8T0zsip1b&rDy%UL1R%voyqf&+cYavdmHvrNmiaTKl-mhz#%(^zQGX&cDsyD^Jt zH;`s7Sfn+vk!QYJEM%FG^~3_2tD{6ZW046Y-Du2P^ij_27L^PF$Qd(l9^zN+xc+26 zD|Ibc)SfR(Z8hwKIS-J~X3htomaI)t-Hsv`)qhN5sZz0;qB{c>r4+RcROM3ONVKa% z(IYA{W0?aMy$mRrE05O5goIZLoT}v;3f>??cPMy9H6w94LP*&tUf2NzUC8k$7^9LN z1p(w3LuL(8$%b8t5C-7CVTgzuD#j(QVB8`p*Y|(5 zQ*|n&6lRFiqww8pLqkD#55uTyS%G)ytZ@P_o^ZzRoK%mWaE%w7n=636KIi7zC?B-^ z>bTE((AJ^NrF&~u;p0PBh4n@s^6O&tv46APPH*P3z&fksvMQ$c7TLU+%Mp5F;yvzL zWSHv~vqugot*}|}tzni%Ev{H(E;0X*b(VUMly=~Ei*&-HD%l@*3}#%t z3LfJ`losJ(vBe(I?I^=Ho7*;oW^bJ6Ie1)~c#*%1C%7xhTG_~FCyC-u6A%Zo|9_Yx zPdFio6Z9NNA{ca`GHM1S5eevV-bu#Y*pRW$F3AK#=xPTfN**34xYG2O77n)ZP%*FlBSZA6b&lY z34Cpj67t4ISftW|iTy|ZGH#a?0)HQ7hN@i7a|3%-n2Mvk6>~vSixB2SEhjJnl4x}C z6x(lQs?np2-UZKM*DO^9H_`b1gk!!+g|@o#u3;5I10(deY}#Q>L%cv|aFf?*w)me6 zV!TSb`T29g;eEejy&dMzWJAlSx~Y9_1rlPmTyFLw=(5ZEAhd;2_~@e)Yjj*~lt8#3 zA0?;~=o2OA6NpYIT%8C+u8o2@i~oUXje<3kEh!rmYDP(5>w*I9z2CrO9Z{H>cel}M zX`>KMGrhHJyhQ=(3(01>Xy)LhM72-9KsUUp|FZ3aZGY|3jek>^L^X4MEM@=z002ov JPDHLkV1j$l(;NT* literal 3502 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 diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png index 8213d5d2ed573ed8c52fd4f14afba4ce03fa5198..3cf8a0dc28bbb331115a6660482c4c357e6f875c 100644 GIT binary patch delta 1403 zcmV->1%&#p8-ojw8Gi!+000iU#^3+|0HRP#R7C)B|NsC0{QdtAdH(}){|9yd&gB0X ze*f$A|45nt^!fji!v8vw|EJFXhrIu9um5DJ|22>QC58VCcmMeN|L5}m-R%Fk*Z+UG z|0#(76MX;g_W$GX|JmyQs?h&apZ_t8|JLaLEsFoe-~XM+|9{fv|FP8nqRjtvvj0w; z|Cq-AjKBXvmj51t|KRQaz1shTyZ>se|6Qg3dbR&pqW}8*|B>(ussI24oJmAMRA}Dp z*IRR{IdPSOaMrhl$xB)Ky2WulSPmTud`<#_V}zr5lg<$qD^2Dk{bXYt1j2P^*2cBz50 z>&j#^Mp@)m~CyV=tlL&cpdc9p+52H>610=s1(d8T0zsip1b&rDy%UL1R%voyqf&+cYavdmHvrNmiaTKl-mhz#%(^zQGX&cDsyD^Jt zH;`s7Sfn+vk!QYJEM%FG^~3_2tD{6ZW046Y-Du2P^ij_27L^PF$Qd(l9^zN+xc+26 zD|Ibc)SfR(Z8hwKIS-J~X3htomaI)t-Hsv`)qhN5sZz0;qB{c>r4+RcROM3ONVKa% z(IYA{W0?aMy$mRrE05O5goIZLoT}v;3f>??cPMy9H6w94LP*&tUf2NzUC8k$7^9LN z1p(w3LuL(8$%b8t5C-7CVTgzuD#j(QVB8`p*Y|(5 zQ*|n&6lRFiqww8pLqkD#55uTyS%G)ytZ@P_o^ZzRoK%mWaE%w7n=636KIi7zC?B-^ z>bTE((AJ^NrF&~u;p0PBh4n@s^6O&tv46APPH*P3z&fksvMQ$c7TLU+%Mp5F;yvzL zWSHv~vqugot*}|}tzni%Ev{H(E;0X*b(VUMly=~Ei*&-HD%l@*3}#%t z3LfJ`losJ(vBe(I?I^=Ho7*;oW^bJ6Ie1)~c#*%1C%7xhTG_~FCyC-u6A%Zo|9_Yx zPdFio6Z9NNA{ca`GHM1S5eevV-bu#Y*pRW$F3AK#=xPTfN**34xYG2O77n)ZP%*FlBSZA6b&lY z34Cpj67t4ISftW|iTy|ZGH#a?0)HQ7hN@i7a|3%-n2Mvk6>~vSixB2SEhjJnl4x}C z6x(lQs?np2-UZKM*DO^9H_`b1gk!!+g|@o#u3;5I10(deY}#Q>L%cv|aFf?*w)me6 zV!TSb`T29g;eEejy&dMzWJAlSx~Y9_1rlPmTyFLw=(5ZEAhd;2_~@e)Yjj*~lt8#3 zA0?;~=o2OA6NpYIT%8C+u8o2@i~oUXje<3kEh!rmYDP(5>w*I9z2CrO9Z{H>cel}M zX`>KMGrhHJyhQ=(3(01>Xy)LhM72-9KsUUp|FZ3aZGY|3jek>^L^X4MEM@=z002ov JPDHLkV1j$l(;NT* literal 3502 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 diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png index 5750709c5f86072a7bd49d278bffe7b9369480e5..6e21d8ee9ece9294df06b5cf624ae2635bc463b0 100644 GIT binary patch delta 2034 zcmV;Jgd|FhNqX{-N6nE&|v|MU6(q|E=H%Kvk*|9>2S|G?b;o5%l!yZ_|y z|JLaL$l?E%#s5H+|IFk6uG0TAjsNZS|Ge4%Zm$0(hW{CV|Ig(AT?7ty000LINkl2lgI6b0aOyZ|k7T*;EkPt02XO zc2wwb6RDOfUWP8^kaAA{bLdj)4vkAz|2*_K_0Rb?t9TmPOZj^K@MfQcPW8EQs`)?k z9+?q-g!WQfQ!W*j|3Y_>A??4Ri|On)M81a2#V}r~oBShx(&_L!4k%Ie;Yvu9f%NGH^{Tc)d2cP7^{UEHTaqn2oZs78_At)yypw%T)>M{X4-7Cf=)re#EOx;D=&EgThGo_nvSVB+{O@%ULAM|EDXTQfXV zHDH8i9DlVK!7VrOFo%GVLL8MDtvtg+%;qCTo1PA#0;ABu-C56LifnOIqL5v|o#jgu z+n>&$5e&<|fC)||W| z^pf#jEhNkDGhTqBCn^~DNRsnT1$!L5Q^m$Xa(|o`swfL8qS#AvseO57dG8??_(;fr zYQEtpq@pd7F{h%=N$I4jIg$`kQ3?O$T&ilpyed@?(DRIqF15ve&JKVUPq8L!{)A>`NlE@QcCm{YJBV>Im`Mt(YC zoPQ?=#%viQI6y&ed&dP*rQEU(Mb#B!tYL;C& zqt;1ia$7^wk|J(9sm@>uJwpnGiIv%!4}UFXimWW?nIu(Ud`~h|X?pW05f@(+2I~w#LP9n zsMi$O!;rvyQS4Em0Xh9&csG8vyR%?OhXNzZbhA{3F}CTQ_@=Q-v>T8rZquN^0e{9+ zD9|%J6V+f&o&v9i_u&i7u||wGz-@}c2G5|C*)z{_HC~QrcM40~B&RXr>)%j#xJi=& zbr{s5fN!?*sKTHg1!iVnSqx#&PqW3Ks&kj3KZSNQ3S!csD|ZS#O!1TcRo$H}Ck_Kq z0KHeV6#OKMB)?iTVO9?Wj@#)V9I zQ%d+|r@T!e^K`@P;xZ~?MWitMrY3?xzxlltexyNrTT}HNxHf*qDz5C8Yvf)k@vutSAD40)wOd?lbExbvuR15<_` z)nT`w;73&2H(<<;YR>hGfRyM)9@Va3(40DYcU^&RxPx`Cg!0dAkAHfc%~{yT>y_>g zH(*qsy22D+Y;;1l^cC9~b$MO+*2w;Br`|BAx0a1nsWrBuLtO*O!c;;H_A#cWS^iRU z&IPP!gv?!`-!8pEfBQf!YrOMpmjtT?PR`-kqLy$Ai~hvaFvp3j_eZv1mHSGyBwx@G zY;)&S>f{+7-c-OkF@IfB;Rz>#D$WZEu~=ybjV6`3T0w%2?(l$9{?K@L)zfkmpGZew*}S0owC(F7$C{gB@oD0bni=Ky6ICp;Hq zo`3XqF3L6;#pfk%(kF3wGpN9KP2Z%`#N)f>0G|YVzH6_$Eq_#q+kJlI8}Hu77ir8l zz4_)eT~z8@-|leGA2)DjG23)~nB-%O+F4+S+Bu*`U2{(E!SbigyxL&xx*G%p?@%){ zg1x$pkB}hysFjoi8KPDq5_Rft_9i_w3)`YLMEpN!MyQP`PeF5mn(&(+K|7Ud@gOHc zegw_-5j7wZerpbz^NOTFTBlXuDeB5EjN+M}g09C15ed(Orc>$0pMEH5b9`Uf7HwkM zq$FtWE1EH;ac1l8q=I3mk+Mm8dsnHk9n3;f9`pw%nvE;azsX-5?%VvvKhuP9!+?#T Qn*aa+07*qoM6N<$f`uOkO#lD@ literal 4996 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 Y1AvlToan6R0RR9107*qoM6N<$f*sdyMF0Q* literal 4317 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 diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index b012f0435..7c0b90e54 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -140,7 +140,7 @@ packages: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.2.1" charcode: dependency: transitive description: @@ -576,7 +576,7 @@ packages: name: material_color_utilities url: "https://pub.dartlang.org" source: hosted - version: "0.1.4" + version: "0.1.5" menu_base: dependency: transitive description: @@ -590,7 +590,7 @@ packages: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.7.0" + version: "1.8.0" mime: dependency: transitive description: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 02621ed66..09025ad0e 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -89,7 +89,7 @@ dev_dependencies: # rerun: flutter pub run flutter_launcher_icons:main icons_launcher: - image_path: "../128x128@2x.png" + image_path: "../res/icon.png" platforms: android: enable: true diff --git a/128x128.png b/res/128x128.png similarity index 100% rename from 128x128.png rename to res/128x128.png diff --git a/128x128@2x.png b/res/128x128@2x.png similarity index 100% rename from 128x128@2x.png rename to res/128x128@2x.png diff --git a/32x32.png b/res/32x32.png similarity index 100% rename from 32x32.png rename to res/32x32.png diff --git a/res/icon.png b/res/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..573165a06f786b223571eb346610042777c466c5 GIT binary patch literal 15849 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 { let event_loop = EventLoop::::with_user_event(); let proxy = event_loop.create_proxy(); - let icon = include_bytes!("./tray-icon.ico"); + let icon = include_bytes!("../res/tray-icon.ico"); let mut tray_icon = TrayIconBuilder::new() .sender_winit(proxy) .icon_from_buffer(icon) From 12fa8d370029b1e8a3dfe8558484e1930b1b905e Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sun, 18 Sep 2022 11:23:08 +0800 Subject: [PATCH 0514/2015] move setup.nsi to res --- res/setup.nsi | 224 +++++++++++++++++++++++++------------------------- 1 file changed, 112 insertions(+), 112 deletions(-) diff --git a/res/setup.nsi b/res/setup.nsi index 0d8636592..5410e0ff5 100644 --- a/res/setup.nsi +++ b/res/setup.nsi @@ -1,112 +1,112 @@ -Unicode true - -#################################################################### -# Includes - -!include nsDialogs.nsh -!include MUI2.nsh -!include x64.nsh -!include LogicLib.nsh - -#################################################################### -# File Info - -!define PRODUCT_NAME "RustDesk" -!define PRODUCT_DESCRIPTION "Installer for ${PRODUCT_NAME}" -!define COPYRIGHT "Copyright © 2021" -!define VERSION "1.1.6" - -VIProductVersion "${VERSION}.0" -VIAddVersionKey "ProductName" "${PRODUCT_NAME}" -VIAddVersionKey "ProductVersion" "${VERSION}" -VIAddVersionKey "FileDescription" "${PRODUCT_DESCRIPTION}" -VIAddVersionKey "LegalCopyright" "${COPYRIGHT}" -VIAddVersionKey "FileVersion" "${VERSION}.0" - -#################################################################### -# Installer Attributes - -Name "${PRODUCT_NAME}" -Outfile "rustdesk-${VERSION}-setup.exe" -Caption "Setup - ${PRODUCT_NAME}" -BrandingText "${PRODUCT_NAME}" - -ShowInstDetails show -RequestExecutionLevel admin -SetOverwrite on - -InstallDir "$PROGRAMFILES64\${PRODUCT_NAME}" - -#################################################################### -# Pages - -!define MUI_ICON "tray-icon.ico" -!define MUI_ABORTWARNING -!define MUI_LANGDLL_ALLLANGUAGES -!define MUI_FINISHPAGE_SHOWREADME "" -!define MUI_FINISHPAGE_SHOWREADME_NOTCHECKED -!define MUI_FINISHPAGE_SHOWREADME_TEXT "Create Desktop Shortcut" -!define MUI_FINISHPAGE_SHOWREADME_FUNCTION CreateDesktopShortcut -!define MUI_FINISHPAGE_RUN "$INSTDIR\${PRODUCT_NAME}.exe" - -!insertmacro MUI_PAGE_DIRECTORY -!insertmacro MUI_PAGE_INSTFILES -!insertmacro MUI_PAGE_FINISH - -#################################################################### -# Language - -!insertmacro MUI_LANGUAGE "English" -!insertmacro MUI_LANGUAGE "SimpChinese" - -#################################################################### -# Sections - -Section "Install" - SetOutPath $INSTDIR - - # Regkeys - WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "DisplayIcon" "$INSTDIR\${PRODUCT_NAME}.exe" - WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "DisplayName" "${PRODUCT_NAME} (x64)" - WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "DisplayVersion" "${VERSION}" - WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "UninstallString" '"$INSTDIR\${PRODUCT_NAME}.exe" --uninstall' - WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "InstallLocation" "$INSTDIR" - WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "Publisher" "Carriez, Inc." - WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "HelpLink" "https://www.rustdesk.com/" - WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "URLInfoAbout" "https://www.rustdesk.com/" - WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "URLUpdateInfo" "https://www.rustdesk.com/" - - nsExec::Exec "taskkill /F /IM ${PRODUCT_NAME}.exe" - Sleep 500 ; Give time for process to be completely killed - File "${PRODUCT_NAME}.exe" - - SetShellVarContext all - CreateShortCut "$INSTDIR\Uninstall ${PRODUCT_NAME}.lnk" "$INSTDIR\${PRODUCT_NAME}.exe" "--uninstall" "msiexec.exe" - CreateDirectory "$SMPROGRAMS\${PRODUCT_NAME}" - CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}\${PRODUCT_NAME}.lnk" "$INSTDIR\${PRODUCT_NAME}.exe" - CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}\Uninstall ${PRODUCT_NAME}.lnk" "$INSTDIR\${PRODUCT_NAME}.exe" "--uninstall" "msiexec.exe" - CreateShortCut "$SMSTARTUP\${PRODUCT_NAME} Tray.lnk" "$INSTDIR\${PRODUCT_NAME}.exe" "--tray" - - nsExec::Exec 'sc create ${PRODUCT_NAME} start=auto DisplayName="${PRODUCT_NAME} Service" binPath= "\"$INSTDIR\${PRODUCT_NAME}.exe\" --service"' - nsExec::Exec 'netsh advfirewall firewall add rule name="${PRODUCT_NAME} Service" dir=in action=allow program="$INSTDIR\${PRODUCT_NAME}.exe" enable=yes' - nsExec::Exec 'sc start ${PRODUCT_NAME}' -SectionEnd - -#################################################################### -# Functions - -Function .onInit - # RustDesk is 64-bit only - ${IfNot} ${RunningX64} - MessageBox MB_ICONSTOP "${PRODUCT_NAME} is 64-bit only!" - Quit - ${EndIf} - ${DisableX64FSRedirection} - SetRegView 64 - - !insertmacro MUI_LANGDLL_DISPLAY -FunctionEnd - -Function CreateDesktopShortcut - CreateShortCut "$DESKTOP\${PRODUCT_NAME}.lnk" "$INSTDIR\${PRODUCT_NAME}.exe" -FunctionEnd +Unicode true + +#################################################################### +# Includes + +!include nsDialogs.nsh +!include MUI2.nsh +!include x64.nsh +!include LogicLib.nsh + +#################################################################### +# File Info + +!define PRODUCT_NAME "RustDesk" +!define PRODUCT_DESCRIPTION "Installer for ${PRODUCT_NAME}" +!define COPYRIGHT "Copyright © 2021" +!define VERSION "1.1.6" + +VIProductVersion "${VERSION}.0" +VIAddVersionKey "ProductName" "${PRODUCT_NAME}" +VIAddVersionKey "ProductVersion" "${VERSION}" +VIAddVersionKey "FileDescription" "${PRODUCT_DESCRIPTION}" +VIAddVersionKey "LegalCopyright" "${COPYRIGHT}" +VIAddVersionKey "FileVersion" "${VERSION}.0" + +#################################################################### +# Installer Attributes + +Name "${PRODUCT_NAME}" +Outfile "rustdesk-${VERSION}-setup.exe" +Caption "Setup - ${PRODUCT_NAME}" +BrandingText "${PRODUCT_NAME}" + +ShowInstDetails show +RequestExecutionLevel admin +SetOverwrite on + +InstallDir "$PROGRAMFILES64\${PRODUCT_NAME}" + +#################################################################### +# Pages + +!define MUI_ICON "tray-icon.ico" +!define MUI_ABORTWARNING +!define MUI_LANGDLL_ALLLANGUAGES +!define MUI_FINISHPAGE_SHOWREADME "" +!define MUI_FINISHPAGE_SHOWREADME_NOTCHECKED +!define MUI_FINISHPAGE_SHOWREADME_TEXT "Create Desktop Shortcut" +!define MUI_FINISHPAGE_SHOWREADME_FUNCTION CreateDesktopShortcut +!define MUI_FINISHPAGE_RUN "$INSTDIR\${PRODUCT_NAME}.exe" + +!insertmacro MUI_PAGE_DIRECTORY +!insertmacro MUI_PAGE_INSTFILES +!insertmacro MUI_PAGE_FINISH + +#################################################################### +# Language + +!insertmacro MUI_LANGUAGE "English" +!insertmacro MUI_LANGUAGE "SimpChinese" + +#################################################################### +# Sections + +Section "Install" + SetOutPath $INSTDIR + + # Regkeys + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "DisplayIcon" "$INSTDIR\${PRODUCT_NAME}.exe" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "DisplayName" "${PRODUCT_NAME} (x64)" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "DisplayVersion" "${VERSION}" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "UninstallString" '"$INSTDIR\${PRODUCT_NAME}.exe" --uninstall' + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "InstallLocation" "$INSTDIR" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "Publisher" "Carriez, Inc." + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "HelpLink" "https://www.rustdesk.com/" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "URLInfoAbout" "https://www.rustdesk.com/" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "URLUpdateInfo" "https://www.rustdesk.com/" + + nsExec::Exec "taskkill /F /IM ${PRODUCT_NAME}.exe" + Sleep 500 ; Give time for process to be completely killed + File "${PRODUCT_NAME}.exe" + + SetShellVarContext all + CreateShortCut "$INSTDIR\Uninstall ${PRODUCT_NAME}.lnk" "$INSTDIR\${PRODUCT_NAME}.exe" "--uninstall" "msiexec.exe" + CreateDirectory "$SMPROGRAMS\${PRODUCT_NAME}" + CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}\${PRODUCT_NAME}.lnk" "$INSTDIR\${PRODUCT_NAME}.exe" + CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}\Uninstall ${PRODUCT_NAME}.lnk" "$INSTDIR\${PRODUCT_NAME}.exe" "--uninstall" "msiexec.exe" + CreateShortCut "$SMSTARTUP\${PRODUCT_NAME} Tray.lnk" "$INSTDIR\${PRODUCT_NAME}.exe" "--tray" + + nsExec::Exec 'sc create ${PRODUCT_NAME} start=auto DisplayName="${PRODUCT_NAME} Service" binPath= "\"$INSTDIR\${PRODUCT_NAME}.exe\" --service"' + nsExec::Exec 'netsh advfirewall firewall add rule name="${PRODUCT_NAME} Service" dir=in action=allow program="$INSTDIR\${PRODUCT_NAME}.exe" enable=yes' + nsExec::Exec 'sc start ${PRODUCT_NAME}' +SectionEnd + +#################################################################### +# Functions + +Function .onInit + # RustDesk is 64-bit only + ${IfNot} ${RunningX64} + MessageBox MB_ICONSTOP "${PRODUCT_NAME} is 64-bit only!" + Quit + ${EndIf} + ${DisableX64FSRedirection} + SetRegView 64 + + !insertmacro MUI_LANGDLL_DISPLAY +FunctionEnd + +Function CreateDesktopShortcut + CreateShortCut "$DESKTOP\${PRODUCT_NAME}.lnk" "$INSTDIR\${PRODUCT_NAME}.exe" +FunctionEnd From 6db730cbfbb576b4b9e528030792638a064874c1 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sun, 18 Sep 2022 11:23:36 +0800 Subject: [PATCH 0515/2015] remove appimage, stupid package --- appimage/AppImageBuilder.yml | 96 ------------------------------------ appimage/README.md | 20 -------- appimage/rustdesk.desktop | 19 ------- build_appimage.py | 23 --------- 4 files changed, 158 deletions(-) delete mode 100644 appimage/AppImageBuilder.yml delete mode 100644 appimage/README.md delete mode 100644 appimage/rustdesk.desktop delete mode 100755 build_appimage.py diff --git a/appimage/AppImageBuilder.yml b/appimage/AppImageBuilder.yml deleted file mode 100644 index cdaead908..000000000 --- a/appimage/AppImageBuilder.yml +++ /dev/null @@ -1,96 +0,0 @@ -# appimage-builder recipe see https://appimage-builder.readthedocs.io for details -# Please build this AppImage on Ubuntu 18.04 -version: 1 -script: - # Remove any previous build - - rm -rf AppDir | true - # Install application dependencies - - pip3 install --upgrade pip && pip3 install --ignore-installed --prefix=/usr --root=AppDir -r ./requirements.txt - # Download sciter.so - - mkdir -p AppDir/usr/lib/rustdesk/ - - pushd AppDir/usr/lib/rustdesk && wget https://github.com/c-smile/sciter-sdk/raw/29a598b6d20220b93848b5e8abab704619296857/bin.lnx/x64/libsciter-gtk.so && popd - # Build rustdesk - - pushd .. && python3 inline-sciter.py && cargo build --features inline,appimage --release && popd - - mkdir -p AppDir/usr/bin - - cp ../target/release/rustdesk AppDir/usr/bin/rustdesk - # Make usr and icons dirs - - mkdir -p AppDir/usr/share/icons/hicolor/128x128 && cp ../128x128.png AppDir/usr/share/icons/hicolor/128x128/rustdesk.png - - mkdir -p AppDir/usr/share/icons/hicolor/32x32 && cp ../32x32.png AppDir/usr/share/icons/hicolor/32x32/rustdesk.png - - cp rustdesk.desktop AppDir/ - -AppDir: - path: ./AppDir - app_info: - id: rustdesk - name: RustDesk - icon: rustdesk - version: 1.1.10 - exec: usr/bin/rustdesk - exec_args: $@ - apt: - arch: - - amd64 - allow_unauthenticated: true - sources: - - sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic main restricted - - sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-updates main restricted - - sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic universe - - sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-updates universe - - sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic multiverse - - sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-updates multiverse - - sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-backports main restricted universe multiverse - - sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-security main restricted - - sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-security universe - - sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-security multiverse - include: - - libc6:amd64 - - libgcc1:amd64 - - libgcrypt20:amd64 - - libgtk-3-0:amd64 - - liblz4-1:amd64 - - liblzma5:amd64 - - libpcre3:amd64 - - libpulse0:amd64 - - libsystemd0:amd64 - - libxau6:amd64 - - libxcb-randr0:amd64 - - libxdmcp6:amd64 - - libxdo3:amd64 - - libxext6:amd64 - - libxfixes3:amd64 - - libxinerama1:amd64 - - libxrender1:amd64 - - libxtst6:amd64 - - python3:amd64 - - python3-pkg-resources:amd64 - files: - include: [] - exclude: - - usr/share/man - - usr/share/doc/*/README.* - - usr/share/doc/*/changelog.* - - usr/share/doc/*/NEWS.* - - usr/share/doc/*/TODO.* - runtime: - env: - PYTHONHOME: '${APPDIR}/usr' - PYTHONPATH: '${APPDIR}/usr/lib/python3.6/site-packages' - test: - fedora-30: - image: appimagecrafters/tests-env:fedora-30 - command: ./AppRun - debian-stable: - image: appimagecrafters/tests-env:debian-stable - command: ./AppRun - archlinux-latest: - image: appimagecrafters/tests-env:archlinux-latest - command: ./AppRun - centos-7: - image: appimagecrafters/tests-env:centos-7 - command: ./AppRun - ubuntu-xenial: - image: appimagecrafters/tests-env:ubuntu-xenial - command: ./AppRun -AppImage: - arch: x86_64 - update-information: guess diff --git a/appimage/README.md b/appimage/README.md deleted file mode 100644 index 1dcfa0b35..000000000 --- a/appimage/README.md +++ /dev/null @@ -1,20 +0,0 @@ -# How to build and run RustDesk in AppImage - -Begin by installing `appimage-builder` and predependencies mentioned in official website. - -Assume that `appimage-builder` is setup correctly, run commands below, `bash` or `zsh` is recommended: - -```bash -cd /path/to/rustdesk_root -./build_appimage.py -``` - -After a success package, you can see the message in console like: - -```shell -INFO:root:AppImage created successfully -``` - -The AppImage package is shown in `./appimage/RustDesk-VERSION-TARGET_PLATFORM.AppImage`. - -Note: AppImage version of rustdesk is an early version which requires more test. If you find problems, please open an issue. \ No newline at end of file diff --git a/appimage/rustdesk.desktop b/appimage/rustdesk.desktop deleted file mode 100644 index a0227f256..000000000 --- a/appimage/rustdesk.desktop +++ /dev/null @@ -1,19 +0,0 @@ -[Desktop Entry] -Version=1.1.10 -Name=RustDesk -GenericName=Remote Desktop -Comment=Remote Desktop -Exec=rustdesk -Icon=rustdesk -Terminal=false -Type=Application -StartupNotify=true -Categories=Other; -Keywords=internet; -Actions=new-window; - -X-Desktop-File-Install-Version=0.23 - -[Desktop Action new-window] -Name=Open a New Window - diff --git a/build_appimage.py b/build_appimage.py deleted file mode 100755 index 1c7ae2443..000000000 --- a/build_appimage.py +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/python3 -import os - -def get_version(): - with open("Cargo.toml") as fh: - for line in fh: - if line.startswith("version"): - return line.replace("version", "").replace("=", "").replace('"', '').strip() - return '' - -if __name__ == '__main__': - # check version - version = get_version() - os.chdir("appimage") - os.system("sed -i 's/^Version=.*/Version=%s/g' rustdesk.desktop" % version) - os.system("sed -i 's/^ version: .*/ version: %s/g' AppImageBuilder.yml" % version) - # build appimage - ret = os.system("appimage-builder --recipe AppImageBuilder.yml --skip-test") - if ret == 0: - print("RustDesk AppImage build success :)") - print("Check AppImage in '/path/to/rustdesk/appimage/RustDesk-VERSION-TARGET_PLATFORM.AppImage'") - else: - print("RustDesk AppImage build failed :(") From 1db743affa90bceb2161527dba41cff4b9c63bb9 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sun, 18 Sep 2022 11:24:02 +0800 Subject: [PATCH 0516/2015] remove snap, as bad as appimage --- snap/README.md | 25 ------ snap/gui/rustdesk.desktop | 19 ----- snap/gui/rustdesk.png | Bin 3042 -> 0 bytes snap/snapcraft.yaml | 157 -------------------------------------- 4 files changed, 201 deletions(-) delete mode 100644 snap/README.md delete mode 100644 snap/gui/rustdesk.desktop delete mode 100644 snap/gui/rustdesk.png delete mode 100644 snap/snapcraft.yaml diff --git a/snap/README.md b/snap/README.md deleted file mode 100644 index e785d4657..000000000 --- a/snap/README.md +++ /dev/null @@ -1,25 +0,0 @@ -## How to build and run with Snap - -Begin by cloning the repository and make sure snapcraft is installed in your Linux. - -```sh -git clone https://github.com/rustdesk/rustdesk -# if snapcraft is installed, please skip this -sudo snap install snapcraft --classic -# build rustdesk snap package -snapcraft --use-lxd -# install rustdesk snap package, `--dangerous` flag must exists if u manually build and install rustdesk -sudo snap install rustdesk_xxx.snap --dangerous -``` - -Note: Some of interfaces needed by RustDesk cannot automatically connected by Snap. Please **manually** connect them by executing: -```sh -# record system audio -snap connect rustdesk:audio-record -snap connect rustdesk:pulseaudio -# observe loginctl session -snap connect rustdesk:login-session-observe -``` - -After steps above, RustDesk can be found in System App Menu. - diff --git a/snap/gui/rustdesk.desktop b/snap/gui/rustdesk.desktop deleted file mode 100644 index a4cfe8963..000000000 --- a/snap/gui/rustdesk.desktop +++ /dev/null @@ -1,19 +0,0 @@ -[Desktop Entry] -Version=1.0 -Name=RustDesk -GenericName=Remote Desktop -Comment=Remote Desktop -Exec=rustdesk %u -Icon=${SNAP}/meta/gui/rustdesk.png -Terminal=false -Type=Application -StartupNotify=true -Categories=Other; -Keywords=internet; -Actions=new-window; - -X-Desktop-File-Install-Version=0.23 - -[Desktop Action new-window] -Name=Open a New Window - diff --git a/snap/gui/rustdesk.png b/snap/gui/rustdesk.png deleted file mode 100644 index 3da699f1d711a027e315d45dc5ada04bdfa2e114..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3042 zcmb7`_dnH-ATYsJmXs0dftTV$m2vLltusLV@J!Yv~s*A?DY zT{GKF;ws5XlDs~B|Ag=3aUPH7`SJO~`QfD5TA$(K5aj>>fXl+%LtXQbS2dK{ z1|3U;#+Xp>OX%|kH1iF5*b7C!hLkfPgKX%0DP)ufwN688KOyEe)c6syD~7Z)A<7t( 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 Date: Sun, 18 Sep 2022 11:26:10 +0800 Subject: [PATCH 0517/2015] move icon.ico to res --- build.rs | 6 +++--- icon.ico => res/icon.ico | Bin manifest.xml => res/manifest.xml | 0 3 files changed, 3 insertions(+), 3 deletions(-) rename icon.ico => res/icon.ico (100%) rename manifest.xml => res/manifest.xml (100%) diff --git a/build.rs b/build.rs index a77d7100d..c47ef2b1c 100644 --- a/build.rs +++ b/build.rs @@ -11,12 +11,12 @@ fn build_manifest() { use std::io::Write; if std::env::var("PROFILE").unwrap() == "release" { let mut res = winres::WindowsResource::new(); - res.set_icon("icon.ico") + res.set_icon("res/icon.ico") .set_language(winapi::um::winnt::MAKELANGID( winapi::um::winnt::LANG_ENGLISH, winapi::um::winnt::SUBLANG_ENGLISH_US, )) - .set_manifest_file("manifest.xml"); + .set_manifest_file("res/manifest.xml"); match res.compile() { Err(e) => { write!(std::io::stderr(), "{}", e).unwrap(); @@ -117,4 +117,4 @@ fn main() { build_windows(); #[cfg(target_os = "macos")] println!("cargo:rustc-link-lib=framework=ApplicationServices"); -} \ No newline at end of file +} diff --git a/icon.ico b/res/icon.ico similarity index 100% rename from icon.ico rename to res/icon.ico diff --git a/manifest.xml b/res/manifest.xml similarity index 100% rename from manifest.xml rename to res/manifest.xml From f0c53bc1265976c3fca231a3acbd3ecdcc1c9ab5 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sun, 18 Sep 2022 11:28:05 +0800 Subject: [PATCH 0518/2015] mv svg to res --- README.md | 2 +- docs/README-AR.md | 2 +- docs/README-CS.md | 2 +- docs/README-DE.md | 2 +- docs/README-EO.md | 2 +- docs/README-ES.md | 2 +- docs/README-FA.md | 2 +- docs/README-FI.md | 2 +- docs/README-FR.md | 2 +- docs/README-HU.md | 2 +- docs/README-ID.md | 2 +- docs/README-IT.md | 2 +- docs/README-JP.md | 2 +- docs/README-KR.md | 2 +- docs/README-ML.md | 2 +- docs/README-NL.md | 2 +- docs/README-PL.md | 2 +- docs/README-PTBR.md | 2 +- docs/README-RU.md | 2 +- docs/README-VN.md | 2 +- docs/README-ZH.md | 2 +- logo-header.svg => res/logo-header.svg | 0 logo.svg => res/logo.svg | 0 23 files changed, 21 insertions(+), 21 deletions(-) rename logo-header.svg => res/logo-header.svg (100%) rename logo.svg => res/logo.svg (100%) diff --git a/README.md b/README.md index 4c76e8ec4..09af44cc7 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

    - RustDesk - Your remote desktop
    + RustDesk - Your remote desktop
    ServersBuildDocker • diff --git a/docs/README-AR.md b/docs/README-AR.md index 0344c90e1..c1290da09 100644 --- a/docs/README-AR.md +++ b/docs/README-AR.md @@ -1,5 +1,5 @@

    - RustDesk - Your remote desktop
    + RustDesk - Your remote desktop
    ServersBuildDocker • diff --git a/docs/README-CS.md b/docs/README-CS.md index 809cd0694..a6465f18f 100644 --- a/docs/README-CS.md +++ b/docs/README-CS.md @@ -1,5 +1,5 @@

    - RustDesk – vaše vzdálená plocha
    + RustDesk – vaše vzdálená plocha
    ServerySestavení ze zdrojových kódůDocker • diff --git a/docs/README-DE.md b/docs/README-DE.md index b927075b6..2f2558f37 100644 --- a/docs/README-DE.md +++ b/docs/README-DE.md @@ -1,5 +1,5 @@

    - RustDesk - Your remote desktop
    + RustDesk - Your remote desktop
    ServerKompilierenDocker • diff --git a/docs/README-EO.md b/docs/README-EO.md index 8e8669890..ce564fb3a 100644 --- a/docs/README-EO.md +++ b/docs/README-EO.md @@ -1,5 +1,5 @@

    - RustDesk - Your remote desktop
    + RustDesk - Your remote desktop
    ServilojKompiliDocker • diff --git a/docs/README-ES.md b/docs/README-ES.md index e6a8ec8c9..b484d54c3 100644 --- a/docs/README-ES.md +++ b/docs/README-ES.md @@ -1,5 +1,5 @@

    - RustDesk - Your remote desktop
    + RustDesk - Your remote desktop
    ServidoresCompilarDocker • diff --git a/docs/README-FA.md b/docs/README-FA.md index 16c9f196e..9a0799777 100644 --- a/docs/README-FA.md +++ b/docs/README-FA.md @@ -1,5 +1,5 @@

    - RustDesk - Your remote desktop
    + RustDesk - Your remote desktop
    اسنپ شاتساختارداکر • diff --git a/docs/README-FI.md b/docs/README-FI.md index 795b8b883..3353b7dcd 100644 --- a/docs/README-FI.md +++ b/docs/README-FI.md @@ -1,5 +1,5 @@

    - RustDesk - Etätyöpöytäsi
    + RustDesk - Etätyöpöytäsi
    PalvelimetRakennaDocker • diff --git a/docs/README-FR.md b/docs/README-FR.md index fa1b32f18..ce1d7be8c 100644 --- a/docs/README-FR.md +++ b/docs/README-FR.md @@ -1,5 +1,5 @@

    - RustDesk - Your remote desktop
    + RustDesk - Your remote desktop
    Serveurs - Build - Docker - diff --git a/docs/README-HU.md b/docs/README-HU.md index bae9b3c7a..cb3eace09 100644 --- a/docs/README-HU.md +++ b/docs/README-HU.md @@ -1,5 +1,5 @@

    - RustDesk - Your remote desktop
    + RustDesk - Your remote desktop
    SzerverekÉpítésDocker • diff --git a/docs/README-ID.md b/docs/README-ID.md index e7a84dfed..9cd2ed4af 100644 --- a/docs/README-ID.md +++ b/docs/README-ID.md @@ -1,5 +1,5 @@

    - RustDesk - Your remote desktop
    + RustDesk - Your remote desktop
    ServersBuildDocker • diff --git a/docs/README-IT.md b/docs/README-IT.md index 9eaa89ecf..39bc75a9e 100644 --- a/docs/README-IT.md +++ b/docs/README-IT.md @@ -1,5 +1,5 @@

    - RustDesk - Your remote desktop
    + RustDesk - Your remote desktop
    ServersCompilazioneDocker • diff --git a/docs/README-JP.md b/docs/README-JP.md index 8e29892bc..119190397 100644 --- a/docs/README-JP.md +++ b/docs/README-JP.md @@ -1,5 +1,5 @@

    - RustDesk - Your remote desktop
    + RustDesk - Your remote desktop
    ServersBuildDocker • diff --git a/docs/README-KR.md b/docs/README-KR.md index d4779c60f..7a0447759 100644 --- a/docs/README-KR.md +++ b/docs/README-KR.md @@ -1,5 +1,5 @@

    - RustDesk - Your remote desktop
    + RustDesk - Your remote desktop
    ServersBuildDocker • diff --git a/docs/README-ML.md b/docs/README-ML.md index b5badba4f..6987883c0 100644 --- a/docs/README-ML.md +++ b/docs/README-ML.md @@ -1,5 +1,5 @@

    - RustDesk - Your remote desktop
    + RustDesk - Your remote desktop
    ServersBuildDocker • diff --git a/docs/README-NL.md b/docs/README-NL.md index 8cd9c8b0e..3dad84b84 100644 --- a/docs/README-NL.md +++ b/docs/README-NL.md @@ -1,5 +1,5 @@

    - RustDesk - Jouw verbinding op afstand
    + RustDesk - Jouw verbinding op afstand
    ServersBouwenDocker • diff --git a/docs/README-PL.md b/docs/README-PL.md index a755c8ded..c5dee351c 100644 --- a/docs/README-PL.md +++ b/docs/README-PL.md @@ -1,5 +1,5 @@

    - RustDesk - Your remote desktop
    + RustDesk - Your remote desktop
    SerweryKompilacjaDocker • diff --git a/docs/README-PTBR.md b/docs/README-PTBR.md index a2c6e8a4a..7781b7468 100644 --- a/docs/README-PTBR.md +++ b/docs/README-PTBR.md @@ -1,5 +1,5 @@

    - RustDesk - Seu desktop remoto
    + RustDesk - Seu desktop remoto
    ServidoresCompilarDocker • diff --git a/docs/README-RU.md b/docs/README-RU.md index f89454cc6..f2a0f9a5e 100644 --- a/docs/README-RU.md +++ b/docs/README-RU.md @@ -1,5 +1,5 @@

    - RustDesk - Ваш удаленый рабочий стол
    + RustDesk - Ваш удаленый рабочий стол
    ServersBuildDocker • diff --git a/docs/README-VN.md b/docs/README-VN.md index d3999fc50..869753d14 100644 --- a/docs/README-VN.md +++ b/docs/README-VN.md @@ -1,5 +1,5 @@

    - RustDesk - Phần mềm điểu khiển máy tính từ xa dành cho bạn
    + RustDesk - Phần mềm điểu khiển máy tính từ xa dành cho bạn
    Máy chủBuildDocker • diff --git a/docs/README-ZH.md b/docs/README-ZH.md index 991a047f6..e7a598541 100644 --- a/docs/README-ZH.md +++ b/docs/README-ZH.md @@ -1,5 +1,5 @@

    - RustDesk - Your remote desktop
    + RustDesk - Your remote desktop
    服务器编译Docker • diff --git a/logo-header.svg b/res/logo-header.svg similarity index 100% rename from logo-header.svg rename to res/logo-header.svg diff --git a/logo.svg b/res/logo.svg similarity index 100% rename from logo.svg rename to res/logo.svg From 69ceb7f1e8311d758eb844c37d9373087e784b90 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sun, 18 Sep 2022 11:32:15 +0800 Subject: [PATCH 0519/2015] move com.rustdesk.RustDesk.policy to res --- PKGBUILD | 2 +- build.py | 4 ++-- .../com.rustdesk.RustDesk.policy | 0 rpm-suse.spec | 2 +- rpm.spec | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) rename com.rustdesk.RustDesk.policy => res/com.rustdesk.RustDesk.policy (100%) diff --git a/PKGBUILD b/PKGBUILD index 38004f66c..90eafe4ff 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -30,5 +30,5 @@ package() { pushd ${pkgdir} && ln -s /usr/lib/rustdesk/flutter_hbb usr/bin/rustdesk && popd install -Dm 644 $HBB/rustdesk.service -t "${pkgdir}/usr/share/rustdesk/files" install -Dm 644 $HBB/rustdesk.desktop -t "${pkgdir}/usr/share/rustdesk/files" - install -Dm 644 $HBB/128x128@2x.png "${pkgdir}/usr/share/rustdesk/files/rustdesk.png" + install -Dm 644 $HBB/res/128x128@2x.png "${pkgdir}/usr/share/rustdesk/files/rustdesk.png" } diff --git a/build.py b/build.py index 97b1c02f9..522a74532 100755 --- a/build.py +++ b/build.py @@ -160,11 +160,11 @@ def build_flutter_deb(version): os.system( 'cp ../rustdesk.service.user tmpdeb/usr/share/rustdesk/files/systemd/') os.system( - 'cp ../128x128@2x.png tmpdeb/usr/share/rustdesk/files/rustdesk.png') + 'cp ../res/128x128@2x.png tmpdeb/usr/share/rustdesk/files/rustdesk.png') os.system( 'cp ../rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop') os.system( - 'cp ../com.rustdesk.RustDesk.policy tmpdeb/usr/share/polkit-1/actions/') + 'cp ../res/com.rustdesk.RustDesk.policy tmpdeb/usr/share/polkit-1/actions/') os.system("echo \"#!/bin/sh\" >> tmpdeb/usr/share/rustdesk/files/polkit && chmod a+x tmpdeb/usr/share/rustdesk/files/polkit") os.system('mkdir -p tmpdeb/DEBIAN') diff --git a/com.rustdesk.RustDesk.policy b/res/com.rustdesk.RustDesk.policy similarity index 100% rename from com.rustdesk.RustDesk.policy rename to res/com.rustdesk.RustDesk.policy diff --git a/rpm-suse.spec b/rpm-suse.spec index db4bfe66f..bc2a3f056 100644 --- a/rpm-suse.spec +++ b/rpm-suse.spec @@ -23,7 +23,7 @@ mkdir -p %{buildroot}/usr/share/rustdesk/files/ install -m 755 $HBB/target/release/rustdesk %{buildroot}/usr/bin/rustdesk install $HBB/libsciter-gtk.so %{buildroot}/usr/lib/rustdesk/libsciter-gtk.so install $HBB/rustdesk.service %{buildroot}/usr/share/rustdesk/files/ -install $HBB/128x128@2x.png %{buildroot}/usr/share/rustdesk/files/rustdesk.png +install $HBB/res/128x128@2x.png %{buildroot}/usr/share/rustdesk/files/rustdesk.png install $HBB/rustdesk.desktop %{buildroot}/usr/share/rustdesk/files/ %files diff --git a/rpm.spec b/rpm.spec index 37e8ea4cc..3a6eb57e0 100644 --- a/rpm.spec +++ b/rpm.spec @@ -23,7 +23,7 @@ mkdir -p %{buildroot}/usr/share/rustdesk/files/ install -m 755 $HBB/target/release/rustdesk %{buildroot}/usr/bin/rustdesk install $HBB/libsciter-gtk.so %{buildroot}/usr/lib/rustdesk/libsciter-gtk.so install $HBB/rustdesk.service %{buildroot}/usr/share/rustdesk/files/ -install $HBB/128x128@2x.png %{buildroot}/usr/share/rustdesk/files/rustdesk.png +install $HBB/res/128x128@2x.png %{buildroot}/usr/share/rustdesk/files/rustdesk.png install $HBB/rustdesk.desktop %{buildroot}/usr/share/rustdesk/files/ %files From 5751b23a97696cc663f75c8de5c3eaae4e658610 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sun, 18 Sep 2022 11:50:23 +0800 Subject: [PATCH 0520/2015] mv some linux package files to res --- PKGBUILD | 4 ++-- build.py | 22 +++++++++++-------- {DEBIAN => res/DEBIAN}/postinst | 0 {DEBIAN => res/DEBIAN}/postrm | 0 {DEBIAN => res/DEBIAN}/preinst | 0 {DEBIAN => res/DEBIAN}/prerm | 0 rustdesk.desktop => res/rustdesk.desktop | 0 rustdesk.service => res/rustdesk.service | 0 .../rustdesk.service.user | 0 rpm-suse.spec | 4 ++-- rpm.spec | 4 ++-- 11 files changed, 19 insertions(+), 15 deletions(-) rename {DEBIAN => res/DEBIAN}/postinst (100%) mode change 100755 => 100644 rename {DEBIAN => res/DEBIAN}/postrm (100%) mode change 100755 => 100644 rename {DEBIAN => res/DEBIAN}/preinst (100%) mode change 100755 => 100644 rename {DEBIAN => res/DEBIAN}/prerm (100%) mode change 100755 => 100644 rename rustdesk.desktop => res/rustdesk.desktop (100%) rename rustdesk.service => res/rustdesk.service (100%) rename rustdesk.service.user => res/rustdesk.service.user (100%) diff --git a/PKGBUILD b/PKGBUILD index 90eafe4ff..4d6057ab0 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -28,7 +28,7 @@ package() { fi mkdir -p "${pkgdir}/usr/bin" pushd ${pkgdir} && ln -s /usr/lib/rustdesk/flutter_hbb usr/bin/rustdesk && popd - install -Dm 644 $HBB/rustdesk.service -t "${pkgdir}/usr/share/rustdesk/files" - install -Dm 644 $HBB/rustdesk.desktop -t "${pkgdir}/usr/share/rustdesk/files" + install -Dm 644 $HBB/res/rustdesk.service -t "${pkgdir}/usr/share/rustdesk/files" + install -Dm 644 $HBB/res/rustdesk.desktop -t "${pkgdir}/usr/share/rustdesk/files" install -Dm 644 $HBB/res/128x128@2x.png "${pkgdir}/usr/share/rustdesk/files/rustdesk.png" } diff --git a/build.py b/build.py index 522a74532..eadd547e5 100755 --- a/build.py +++ b/build.py @@ -122,7 +122,7 @@ def get_features(args): return features def generate_control_file(version): - control_file_path = "../DEBIAN/control" + control_file_path = "../res/DEBIAN/control" os.system('/bin/rm -rf %s' % control_file_path) content = """Package: rustdesk @@ -156,26 +156,26 @@ def build_flutter_deb(version): os.system( 'cp build/linux/x64/release/liblibrustdesk.so tmpdeb/usr/lib/rustdesk/librustdesk.so') os.system( - 'cp ../rustdesk.service tmpdeb/usr/share/rustdesk/files/systemd/') + 'cp ../res/rustdesk.service tmpdeb/usr/share/rustdesk/files/systemd/') os.system( - 'cp ../rustdesk.service.user tmpdeb/usr/share/rustdesk/files/systemd/') + 'cp ../res/rustdesk.service.user tmpdeb/usr/share/rustdesk/files/systemd/') os.system( 'cp ../res/128x128@2x.png tmpdeb/usr/share/rustdesk/files/rustdesk.png') os.system( - 'cp ../rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop') + 'cp ../res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop') os.system( 'cp ../res/com.rustdesk.RustDesk.policy tmpdeb/usr/share/polkit-1/actions/') os.system("echo \"#!/bin/sh\" >> tmpdeb/usr/share/rustdesk/files/polkit && chmod a+x tmpdeb/usr/share/rustdesk/files/polkit") os.system('mkdir -p tmpdeb/DEBIAN') generate_control_file(version) - os.system('cp -a ../DEBIAN/* tmpdeb/DEBIAN/') + os.system('cp -a ../res/DEBIAN/* tmpdeb/DEBIAN/') md5_file('usr/share/rustdesk/files/systemd/rustdesk.service') md5_file('usr/share/rustdesk/files/systemd/rustdesk.service.user') os.system('dpkg-deb -b tmpdeb rustdesk.deb;') os.system('/bin/rm -rf tmpdeb/') - os.system('/bin/rm -rf ../DEBIAN/control') + os.system('/bin/rm -rf ../res/DEBIAN/control') os.rename('rustdesk.deb', '../rustdesk-%s.deb' % version) os.chdir("..") @@ -301,10 +301,14 @@ def main(): os.system('dpkg-deb -R rustdesk.deb tmpdeb') os.system('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/') os.system( - 'cp rustdesk.service tmpdeb/usr/share/rustdesk/files/systemd/') + 'cp res/rustdesk.service tmpdeb/usr/share/rustdesk/files/systemd/') os.system( - 'cp rustdesk.service.user tmpdeb/usr/share/rustdesk/files/systemd/') - os.system('cp -a DEBIAN/* tmpdeb/DEBIAN/') + 'cp res/rustdesk.service.user tmpdeb/usr/share/rustdesk/files/systemd/') + os.system( + 'cp res/128x128@2x.png tmpdeb/usr/share/rustdesk/files/rustdesk.png') + os.system( + 'cp res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop') + os.system('cp -a res/DEBIAN/* tmpdeb/DEBIAN/') os.system('strip tmpdeb/usr/bin/rustdesk') os.system('mkdir -p tmpdeb/usr/lib/rustdesk') os.system('cp libsciter-gtk.so tmpdeb/usr/lib/rustdesk/') diff --git a/DEBIAN/postinst b/res/DEBIAN/postinst old mode 100755 new mode 100644 similarity index 100% rename from DEBIAN/postinst rename to res/DEBIAN/postinst diff --git a/DEBIAN/postrm b/res/DEBIAN/postrm old mode 100755 new mode 100644 similarity index 100% rename from DEBIAN/postrm rename to res/DEBIAN/postrm diff --git a/DEBIAN/preinst b/res/DEBIAN/preinst old mode 100755 new mode 100644 similarity index 100% rename from DEBIAN/preinst rename to res/DEBIAN/preinst diff --git a/DEBIAN/prerm b/res/DEBIAN/prerm old mode 100755 new mode 100644 similarity index 100% rename from DEBIAN/prerm rename to res/DEBIAN/prerm diff --git a/rustdesk.desktop b/res/rustdesk.desktop similarity index 100% rename from rustdesk.desktop rename to res/rustdesk.desktop diff --git a/rustdesk.service b/res/rustdesk.service similarity index 100% rename from rustdesk.service rename to res/rustdesk.service diff --git a/rustdesk.service.user b/res/rustdesk.service.user similarity index 100% rename from rustdesk.service.user rename to res/rustdesk.service.user diff --git a/rpm-suse.spec b/rpm-suse.spec index bc2a3f056..8b820beca 100644 --- a/rpm-suse.spec +++ b/rpm-suse.spec @@ -22,9 +22,9 @@ mkdir -p %{buildroot}/usr/lib/rustdesk/ mkdir -p %{buildroot}/usr/share/rustdesk/files/ install -m 755 $HBB/target/release/rustdesk %{buildroot}/usr/bin/rustdesk install $HBB/libsciter-gtk.so %{buildroot}/usr/lib/rustdesk/libsciter-gtk.so -install $HBB/rustdesk.service %{buildroot}/usr/share/rustdesk/files/ +install $HBB/res/rustdesk.service %{buildroot}/usr/share/rustdesk/files/ install $HBB/res/128x128@2x.png %{buildroot}/usr/share/rustdesk/files/rustdesk.png -install $HBB/rustdesk.desktop %{buildroot}/usr/share/rustdesk/files/ +install $HBB/res/rustdesk.desktop %{buildroot}/usr/share/rustdesk/files/ %files /usr/bin/rustdesk diff --git a/rpm.spec b/rpm.spec index 3a6eb57e0..e34997d2a 100644 --- a/rpm.spec +++ b/rpm.spec @@ -22,9 +22,9 @@ mkdir -p %{buildroot}/usr/lib/rustdesk/ mkdir -p %{buildroot}/usr/share/rustdesk/files/ install -m 755 $HBB/target/release/rustdesk %{buildroot}/usr/bin/rustdesk install $HBB/libsciter-gtk.so %{buildroot}/usr/lib/rustdesk/libsciter-gtk.so -install $HBB/rustdesk.service %{buildroot}/usr/share/rustdesk/files/ +install $HBB/res/rustdesk.service %{buildroot}/usr/share/rustdesk/files/ install $HBB/res/128x128@2x.png %{buildroot}/usr/share/rustdesk/files/rustdesk.png -install $HBB/rustdesk.desktop %{buildroot}/usr/share/rustdesk/files/ +install $HBB/res/rustdesk.desktop %{buildroot}/usr/share/rustdesk/files/ %files /usr/bin/rustdesk From b7e54081b8377711fdd9003efbbc5d1775f02dc6 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sun, 18 Sep 2022 11:53:15 +0800 Subject: [PATCH 0521/2015] move lang.py and inlinee-sciter.py to res --- build.py | 2 +- res/gen_icon.sh | 7 +++++++ inline-sciter.py => res/inline-sciter.py | 0 lang.py => res/lang.py | 0 4 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 res/gen_icon.sh rename inline-sciter.py => res/inline-sciter.py (100%) rename lang.py => res/lang.py (100%) diff --git a/build.py b/build.py index eadd547e5..97942a319 100755 --- a/build.py +++ b/build.py @@ -201,7 +201,7 @@ def main(): '//#![windows_subsystem', '#![windows_subsystem')) if os.path.exists(exe_path): os.unlink(exe_path) - os.system('python3 inline-sciter.py') + os.system('python3 res/inline-sciter.py') if os.path.isfile('/usr/bin/pacman'): os.system('git checkout src/ui/common.tis') version = get_version() diff --git a/res/gen_icon.sh b/res/gen_icon.sh new file mode 100644 index 000000000..40b67aa53 --- /dev/null +++ b/res/gen_icon.sh @@ -0,0 +1,7 @@ +#!/bin/bash +for size in 16 32 64 128 256 512 1024; do + #inkscape -z -o $size.png -w $size -h $size icon.svg >/dev/null 2>/dev/null + convert icon.png -resize ${size}x${size} app_icon_$size.png +done +# from ImageMagick +#/bin/rm 16.png 32.png 48.png 128.png 256.png diff --git a/inline-sciter.py b/res/inline-sciter.py similarity index 100% rename from inline-sciter.py rename to res/inline-sciter.py diff --git a/lang.py b/res/lang.py similarity index 100% rename from lang.py rename to res/lang.py From 3e22893bc81570f302539dc482756c6951187769 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sun, 18 Sep 2022 12:00:45 +0800 Subject: [PATCH 0522/2015] remove python dep --- Cargo.toml | 2 +- PKGBUILD | 2 +- pacman_install | 1 - rpm-suse.spec | 2 +- rpm.spec | 2 +- 5 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7ac714c04..8670ade40 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -143,7 +143,7 @@ hound = "3.5" name = "RustDesk" identifier = "com.carriez.rustdesk" icon = ["res/32x32.png", "res/128x128.png", "res/128x128@2x.png"] -deb_depends = ["libgtk-3-0", "libxcb-randr0", "libxdo3", "libxfixes3", "libxcb-shape0", "libxcb-xfixes0", "libasound2", "libsystemd0", "pulseaudio", "python3-pip", "curl"] +deb_depends = ["libgtk-3-0", "libxcb-randr0", "libxdo3", "libxfixes3", "libxcb-shape0", "libxcb-xfixes0", "libasound2", "libsystemd0", "pulseaudio", "curl"] osx_minimum_system_version = "10.14" resources = ["res/mac-tray-light.png","res/mac-tray-dark.png"] diff --git a/PKGBUILD b/PKGBUILD index 4d6057ab0..575887a7b 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -7,7 +7,7 @@ arch=('x86_64') url="" license=('AGPL-3.0') groups=() -depends=('gtk3' 'xdotool' 'libxcb' 'libxfixes' 'alsa-lib' 'pulseaudio' 'ttf-arphic-uming' 'python-pip' 'curl' 'libappindicator-gtk3') +depends=('gtk3' 'xdotool' 'libxcb' 'libxfixes' 'alsa-lib' 'pulseaudio' 'ttf-arphic-uming' 'curl' 'libappindicator-gtk3') makedepends=() checkdepends=() optdepends=() diff --git a/pacman_install b/pacman_install index cfd3bdd60..eeef34028 100644 --- a/pacman_install +++ b/pacman_install @@ -7,7 +7,6 @@ post_install() { # do something here cp /usr/share/rustdesk/files/rustdesk.service /etc/systemd/system/rustdesk.service cp /usr/share/rustdesk/files/rustdesk.desktop /usr/share/applications/ - sudo -H pip3 install systemctl daemon-reload systemctl enable rustdesk systemctl start rustdesk diff --git a/rpm-suse.spec b/rpm-suse.spec index 8b820beca..a22171242 100644 --- a/rpm-suse.spec +++ b/rpm-suse.spec @@ -3,7 +3,7 @@ Version: 1.1.9 Release: 0 Summary: RPM package License: GPL-3.0 -Requires: gtk3 libxcb1 xdotool libXfixes3 pulseaudio-utils alsa-utils arphic-uming-fonts python3-pip curl libXtst6 python3-devel +Requires: gtk3 libxcb1 xdotool libXfixes3 pulseaudio-utils alsa-utils arphic-uming-fonts curl libXtst6 %description The best open-source remote desktop client software, written in Rust. diff --git a/rpm.spec b/rpm.spec index e34997d2a..8e04eaac5 100644 --- a/rpm.spec +++ b/rpm.spec @@ -3,7 +3,7 @@ Version: 1.1.9 Release: 0 Summary: RPM package License: GPL-3.0 -Requires: gtk3 libxcb libxdo libXfixes pulseaudio-libs alsa-lib cjkuni-uming-fonts python3-pip curl +Requires: gtk3 libxcb libxdo libXfixes pulseaudio-libs alsa-lib cjkuni-uming-fonts curl %description The best open-source remote desktop client software, written in Rust. From 49491823c359ddfe9ecdb0503aa6cc69b90a1059 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sun, 18 Sep 2022 12:07:22 +0800 Subject: [PATCH 0523/2015] mv the other linux package files to res --- build.py | 9 +++++---- PKGBUILD => res/PKGBUILD | 0 pacman_install => res/pacman_install | 0 rpm-suse.spec => res/rpm-suse.spec | 0 rpm.spec => res/rpm.spec | 0 5 files changed, 5 insertions(+), 4 deletions(-) rename PKGBUILD => res/PKGBUILD (100%) rename pacman_install => res/pacman_install (100%) rename rpm-suse.spec => res/rpm-suse.spec (100%) rename rpm.spec => res/rpm.spec (100%) diff --git a/build.py b/build.py index 97942a319..52ccbe41e 100755 --- a/build.py +++ b/build.py @@ -227,22 +227,23 @@ def main(): os.system('cargo build --release --features ' + features) os.system('git checkout src/ui/common.tis') os.system('strip target/release/rustdesk') + os.system('ln -s res/pacman_install && ln -s res/PKGBUILD') os.system('HBB=`pwd` makepkg -f') os.system('mv rustdesk-%s-0-x86_64.pkg.tar.zst rustdesk-%s-manjaro-arch.pkg.tar.zst' % (version, version)) # pacman -U ./rustdesk.pkg.tar.zst elif os.path.isfile('/usr/bin/yum'): os.system('cargo build --release --features ' + features) os.system('strip target/release/rustdesk') - os.system("sed -i 's/Version: .*/Version: %s/g' rpm.spec" % version) - os.system('HBB=`pwd` rpmbuild -ba rpm.spec') + os.system("sed -i 's/Version: .*/Version: %s/g' res/rpm.spec" % version) + os.system('HBB=`pwd` rpmbuild -ba res/rpm.spec') os.system('mv $HOME/rpmbuild/RPMS/x86_64/rustdesk-%s-0.x86_64.rpm ./rustdesk-%s-fedora28-centos8.rpm' % ( version, version)) # yum localinstall rustdesk.rpm elif os.path.isfile('/usr/bin/zypper'): os.system('cargo build --release --features ' + features) os.system('strip target/release/rustdesk') - os.system("sed -i 's/Version: .*/Version: %s/g' rpm-suse.spec" % version) - os.system('HBB=`pwd` rpmbuild -ba rpm-suse.spec') + os.system("sed -i 's/Version: .*/Version: %s/g' res/rpm-suse.spec" % version) + os.system('HBB=`pwd` rpmbuild -ba res/rpm-suse.spec') os.system('mv $HOME/rpmbuild/RPMS/x86_64/rustdesk-%s-0.x86_64.rpm ./rustdesk-%s-suse.rpm' % (version, version)) # yum localinstall rustdesk.rpm else: diff --git a/PKGBUILD b/res/PKGBUILD similarity index 100% rename from PKGBUILD rename to res/PKGBUILD diff --git a/pacman_install b/res/pacman_install similarity index 100% rename from pacman_install rename to res/pacman_install diff --git a/rpm-suse.spec b/res/rpm-suse.spec similarity index 100% rename from rpm-suse.spec rename to res/rpm-suse.spec diff --git a/rpm.spec b/res/rpm.spec similarity index 100% rename from rpm.spec rename to res/rpm.spec From f0208c759bc7e496b90d29af435cafa0e98c0599 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sun, 18 Sep 2022 13:13:45 +0800 Subject: [PATCH 0524/2015] https://github.com/rustdesk/rustdesk/pull/1562 --- Cargo.lock | 23 ----------------------- build.rs | 1 + libs/hbb_common/Cargo.toml | 1 - libs/hbb_common/src/config.rs | 9 +++++++-- 4 files changed, 8 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f2a260a9b..5627a861f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2302,7 +2302,6 @@ dependencies = [ "serde 1.0.144", "serde_derive", "serde_json 1.0.85", - "serde_with", "socket2 0.3.19", "sodiumoxide", "tokio", @@ -4588,28 +4587,6 @@ dependencies = [ "serde 1.0.144", ] -[[package]] -name = "serde_with" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "678b5a069e50bf00ecd22d0cd8ddf7c236f68581b03db652061ed5eb13a312ff" -dependencies = [ - "serde 1.0.144", - "serde_with_macros", -] - -[[package]] -name = "serde_with_macros" -version = "1.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082" -dependencies = [ - "darling", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "serde_yaml" version = "0.8.26" diff --git a/build.rs b/build.rs index c47ef2b1c..67e40752c 100644 --- a/build.rs +++ b/build.rs @@ -76,6 +76,7 @@ fn install_oboe() { //cc::Build::new().file("oboe.cc").include(include).compile("oboe_wrapper"); } +#[cfg(feature = "flutter")] fn gen_flutter_rust_bridge() { let llvm_path = match std::env::var("LLVM_HOME") { Ok(path) => Some(vec![path]), diff --git a/libs/hbb_common/Cargo.toml b/libs/hbb_common/Cargo.toml index 6773c0f53..8d4f36b2d 100644 --- a/libs/hbb_common/Cargo.toml +++ b/libs/hbb_common/Cargo.toml @@ -23,7 +23,6 @@ directories-next = "2.0" rand = "0.8" serde_derive = "1.0" serde = "1.0" -serde_with = "1.14.0" lazy_static = "1.4" confy = { git = "https://github.com/open-trade/confy" } dirs-next = "2.0" diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 5fc974462..2ad8bee47 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -956,13 +956,18 @@ impl LocalConfig { #[derive(Debug, Default, Serialize, Deserialize, Clone)] pub struct DiscoveryPeer { + #[serde(default)] pub id: String, - #[serde(with = "serde_with::rust::map_as_tuple_list")] - pub ip_mac: HashMap, + #[serde(default)] pub username: String, + #[serde(default)] pub hostname: String, + #[serde(default)] pub platform: String, + #[serde(default)] pub online: bool, + #[serde(default)] + pub ip_mac: HashMap, } impl DiscoveryPeer { From 91829c73f384251c104d8911d24061d72c1ae22d Mon Sep 17 00:00:00 2001 From: xxrl <837951112@qq.com> Date: Sun, 18 Sep 2022 17:38:16 +0800 Subject: [PATCH 0525/2015] fix chinese version of doc_mac_permission url --- src/lang/cn.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 738595aa9..920013be3 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -272,7 +272,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Overwrite", "覆盖"), ("This file exists, skip or overwrite this file?", "这个文件/文件夹已存在,跳过/覆盖?"), ("Quit", "退出"), - ("doc_mac_permission", "https://rustdesk.com/docs/zh-cn/manual/mac/#启用权限"), + ("doc_mac_permission", "https://rustdesk.com/docs/zh-cn/manual/mac#%E5%90%AF%E7%94%A8%E6%9D%83%E9%99%90"), ("Help", "帮助"), ("Failed", "失败"), ("Succeeded", "成功"), From 429d72c9c343539ea7d02babae4b00942fc00dca Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 19 Sep 2022 10:05:53 +0800 Subject: [PATCH 0526/2015] refactor: change binary name to rustdesk --- flutter/linux/CMakeLists.txt | 2 +- flutter/macos/Runner/Configs/AppInfo.xcconfig | 2 +- flutter/windows/CMakeLists.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/flutter/linux/CMakeLists.txt b/flutter/linux/CMakeLists.txt index 9a4e0527b..9391ed97e 100644 --- a/flutter/linux/CMakeLists.txt +++ b/flutter/linux/CMakeLists.txt @@ -4,7 +4,7 @@ project(runner LANGUAGES CXX) # The name of the executable created for the application. Change this to change # the on-disk name of your application. -set(BINARY_NAME "flutter_hbb") +set(BINARY_NAME "rustdesk") # The unique GTK application identifier for this application. See: # https://wiki.gnome.org/HowDoI/ChooseApplicationID set(APPLICATION_ID "com.carriez.flutter_hbb") diff --git a/flutter/macos/Runner/Configs/AppInfo.xcconfig b/flutter/macos/Runner/Configs/AppInfo.xcconfig index 3c862dee9..bf05a4caa 100644 --- a/flutter/macos/Runner/Configs/AppInfo.xcconfig +++ b/flutter/macos/Runner/Configs/AppInfo.xcconfig @@ -5,7 +5,7 @@ // 'flutter create' template. // The application's name. By default this is also the title of the Flutter window. -PRODUCT_NAME = flutter_hbb +PRODUCT_NAME = rustdesk // The application's bundle identifier PRODUCT_BUNDLE_IDENTIFIER = com.carriez.flutterHbb diff --git a/flutter/windows/CMakeLists.txt b/flutter/windows/CMakeLists.txt index 89f0925f3..c6192b584 100644 --- a/flutter/windows/CMakeLists.txt +++ b/flutter/windows/CMakeLists.txt @@ -4,7 +4,7 @@ project(flutter_hbb LANGUAGES CXX) # The name of the executable created for the application. Change this to change # the on-disk name of your application. -set(BINARY_NAME "flutter_hbb") +set(BINARY_NAME "rustdesk") # Explicitly opt in to modern CMake behaviors to avoid warnings with recent # versions of CMake. From ef80dab48e8177d1091adbe15db03e4fbd73659d Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 19 Sep 2022 10:14:14 +0800 Subject: [PATCH 0527/2015] opt: remove drag to resize widget on macOS --- .../lib/desktop/pages/desktop_tab_page.dart | 48 +++---- .../desktop/pages/file_manager_tab_page.dart | 30 +++-- .../desktop/pages/port_forward_tab_page.dart | 36 +++--- .../lib/desktop/pages/remote_tab_page.dart | 117 +++++++++--------- 4 files changed, 123 insertions(+), 108 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_tab_page.dart b/flutter/lib/desktop/pages/desktop_tab_page.dart index fde543a89..725babdd8 100644 --- a/flutter/lib/desktop/pages/desktop_tab_page.dart +++ b/flutter/lib/desktop/pages/desktop_tab_page.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/consts.dart'; @@ -35,28 +37,30 @@ class _DesktopTabPageState extends State { Widget build(BuildContext context) { RxBool fullscreen = false.obs; Get.put(fullscreen, tag: 'fullscreen'); - return Obx(() => DragToResizeArea( - resizeEdgeSize: fullscreen.value ? 1.0 : 8.0, - 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: MyTheme.color(context).bg, - body: DesktopTab( - controller: tabController, - tail: ActionIcon( - message: 'Settings', - icon: IconFont.menu, - onTap: onAddSetting, - isClose: false, - ), - )); - }) - ]), - ))); + final tabWidget = 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: MyTheme.color(context).bg, + body: DesktopTab( + controller: tabController, + tail: ActionIcon( + message: 'Settings', + icon: IconFont.menu, + onTap: onAddSetting, + isClose: false, + ), + )); + }) + ]), + ); + return Obx(() => Platform.isMacOS + ? tabWidget + : DragToResizeArea( + resizeEdgeSize: fullscreen.value ? 1.0 : 8.0, child: tabWidget)); } void onAddSetting() { diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index de874b42d..bfaf33327 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:io'; import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/material.dart'; @@ -66,20 +67,23 @@ class _FileManagerTabPageState extends State { @override Widget build(BuildContext context) { - return SubWindowDragToResizeArea( - windowId: windowId(), - child: Container( - decoration: BoxDecoration( - border: Border.all(color: MyTheme.color(context).border!)), - child: Scaffold( - backgroundColor: MyTheme.color(context).bg, - body: DesktopTab( - controller: tabController, - onWindowCloseButton: handleWindowCloseButton, - tail: const AddButton().paddingOnly(left: 10), - )), - ), + final tabWidget = Container( + decoration: BoxDecoration( + border: Border.all(color: MyTheme.color(context).border!)), + child: Scaffold( + backgroundColor: MyTheme.color(context).bg, + body: DesktopTab( + controller: tabController, + onWindowCloseButton: handleWindowCloseButton, + tail: const AddButton().paddingOnly(left: 10), + )), ); + return Platform.isMacOS + ? tabWidget + : SubWindowDragToResizeArea( + windowId: windowId(), + child: tabWidget, + ); } void onRemoveId(String id) { diff --git a/flutter/lib/desktop/pages/port_forward_tab_page.dart b/flutter/lib/desktop/pages/port_forward_tab_page.dart index e4aac06fe..f142508de 100644 --- a/flutter/lib/desktop/pages/port_forward_tab_page.dart +++ b/flutter/lib/desktop/pages/port_forward_tab_page.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:io'; import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/material.dart'; @@ -74,23 +75,26 @@ class _PortForwardTabPageState extends State { @override Widget build(BuildContext context) { - return SubWindowDragToResizeArea( - windowId: windowId(), - child: Container( - decoration: BoxDecoration( - border: Border.all(color: MyTheme.color(context).border!)), - child: Scaffold( - backgroundColor: MyTheme.color(context).bg, - body: DesktopTab( - controller: tabController, - onWindowCloseButton: () async { - tabController.clear(); - return true; - }, - tail: AddButton().paddingOnly(left: 10), - )), - ), + final tabWidget = Container( + decoration: BoxDecoration( + border: Border.all(color: MyTheme.color(context).border!)), + child: Scaffold( + backgroundColor: MyTheme.color(context).bg, + body: DesktopTab( + controller: tabController, + onWindowCloseButton: () async { + tabController.clear(); + return true; + }, + tail: AddButton().paddingOnly(left: 10), + )), ); + return Platform.isMacOS + ? tabWidget + : SubWindowDragToResizeArea( + windowId: windowId(), + child: tabWidget, + ); } void onRemoveId(String id) { diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index 4d4c7e6e1..2bef85b60 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:io'; import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/material.dart'; @@ -86,63 +87,65 @@ class _ConnectionTabPageState extends State { @override Widget build(BuildContext context) { final RxBool fullscreen = Get.find(tag: 'fullscreen'); - return Obx(() => SubWindowDragToResizeArea( - resizeEdgeSize: fullscreen.value ? 1.0 : 8.0, - windowId: windowId(), - child: Container( - decoration: BoxDecoration( - border: Border.all(color: MyTheme.color(context).border!)), - child: Scaffold( - backgroundColor: MyTheme.color(context).bg, - body: DesktopTab( - controller: tabController, - showTabBar: fullscreen.isFalse, - onWindowCloseButton: handleWindowCloseButton, - tail: const AddButton().paddingOnly(left: 10), - pageViewBuilder: (pageView) { - WindowController.fromWindowId(windowId()) - .setFullscreen(fullscreen.isTrue); - return pageView; - }, - tabBuilder: (key, icon, label, themeConf) => Obx(() { - final connectionType = ConnectionTypeState.find(key); - if (!connectionType.isValid()) { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - icon, - label, - ], - ); - } else { - final msgDirect = translate(connectionType.direct.value == - ConnectionType.strDirect - ? 'Direct Connection' - : 'Relay Connection'); - final msgSecure = translate(connectionType.secure.value == - ConnectionType.strSecure - ? 'Secure Connection' - : 'Insecure Connection'); - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - icon, - Tooltip( - message: '$msgDirect\n$msgSecure', - child: Image.asset( - 'assets/${connectionType.secure.value}${connectionType.direct.value}.png', - width: themeConf.iconSize, - height: themeConf.iconSize, - ).paddingOnly(right: 5), - ), - label, - ], - ); - } - }), - )), - ), - )); + final tabWidget = Container( + decoration: BoxDecoration( + border: Border.all(color: MyTheme.color(context).border!)), + child: Scaffold( + backgroundColor: MyTheme.color(context).bg, + body: DesktopTab( + controller: tabController, + showTabBar: fullscreen.isFalse, + onWindowCloseButton: handleWindowCloseButton, + tail: const AddButton().paddingOnly(left: 10), + pageViewBuilder: (pageView) { + WindowController.fromWindowId(windowId()) + .setFullscreen(fullscreen.isTrue); + return pageView; + }, + tabBuilder: (key, icon, label, themeConf) => Obx(() { + final connectionType = ConnectionTypeState.find(key); + if (!connectionType.isValid()) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + icon, + label, + ], + ); + } else { + final msgDirect = translate( + connectionType.direct.value == ConnectionType.strDirect + ? 'Direct Connection' + : 'Relay Connection'); + final msgSecure = translate( + connectionType.secure.value == ConnectionType.strSecure + ? 'Secure Connection' + : 'Insecure Connection'); + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + icon, + Tooltip( + message: '$msgDirect\n$msgSecure', + child: Image.asset( + 'assets/${connectionType.secure.value}${connectionType.direct.value}.png', + width: themeConf.iconSize, + height: themeConf.iconSize, + ).paddingOnly(right: 5), + ), + label, + ], + ); + } + }), + )), + ); + return Obx(() => Platform.isMacOS + ? tabWidget + : SubWindowDragToResizeArea( + resizeEdgeSize: fullscreen.value ? 1.0 : 8.0, + windowId: windowId(), + child: tabWidget)); } void onRemoveId(String id) { From 910fb84857f0bdeebe0a5b661fc3684e20e080cb Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 19 Sep 2022 10:22:40 +0800 Subject: [PATCH 0528/2015] opt: more error catch on address book --- flutter/lib/models/ab_model.dart | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/flutter/lib/models/ab_model.dart b/flutter/lib/models/ab_model.dart index 2749e972f..161a4d8a5 100644 --- a/flutter/lib/models/ab_model.dart +++ b/flutter/lib/models/ab_model.dart @@ -45,8 +45,8 @@ class AbModel with ChangeNotifier { } catch (err) { abError = err.toString(); } finally { - notifyListeners(); abLoading = false; + notifyListeners(); } return null; } @@ -98,12 +98,18 @@ class AbModel with ChangeNotifier { final body = jsonEncode({ "data": jsonEncode({"tags": tags, "peers": peers}) }); - final resp = - await http.post(Uri.parse(api), headers: authHeaders, body: body); - abLoading = false; - await getAb(); + try { + final resp = + await http.post(Uri.parse(api), headers: authHeaders, body: body); + abError = ""; + await getAb(); + debugPrint("resp: ${resp.body}"); + } catch (e) { + abError = e.toString(); + } finally { + abLoading = false; + } notifyListeners(); - debugPrint("resp: ${resp.body}"); } bool idContainBy(String id) { From 225d5a098339e9e1fd6f24670698074382cf3370 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 19 Sep 2022 11:09:29 +0800 Subject: [PATCH 0529/2015] fix: place obx down --- flutter/lib/desktop/pages/desktop_tab_page.dart | 4 ++-- flutter/lib/desktop/pages/remote_tab_page.dart | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_tab_page.dart b/flutter/lib/desktop/pages/desktop_tab_page.dart index 725babdd8..a117bab12 100644 --- a/flutter/lib/desktop/pages/desktop_tab_page.dart +++ b/flutter/lib/desktop/pages/desktop_tab_page.dart @@ -57,9 +57,9 @@ class _DesktopTabPageState extends State { }) ]), ); - return Obx(() => Platform.isMacOS + return Platform.isMacOS ? tabWidget - : DragToResizeArea( + : Obx(() => DragToResizeArea( resizeEdgeSize: fullscreen.value ? 1.0 : 8.0, child: tabWidget)); } diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index 2bef85b60..5ea94f4a9 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -140,9 +140,9 @@ class _ConnectionTabPageState extends State { }), )), ); - return Obx(() => Platform.isMacOS + return Platform.isMacOS ? tabWidget - : SubWindowDragToResizeArea( + : Obx(() => SubWindowDragToResizeArea( resizeEdgeSize: fullscreen.value ? 1.0 : 8.0, windowId: windowId(), child: tabWidget)); From f1a3a8ca0187d6b52f1ef6cf98d79d60b6254ca3 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 19 Sep 2022 15:46:09 +0800 Subject: [PATCH 0530/2015] opt: add support locales --- flutter/lib/common.dart | 25 +++++++++++++++++++++++++ flutter/lib/main.dart | 41 ++++++++++++++++++++++++++++++++++++----- flutter/pubspec.yaml | 2 ++ 3 files changed, 63 insertions(+), 5 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index c07764e32..3e08d1e0c 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -244,6 +244,31 @@ final ButtonStyle flatButtonStyle = TextButton.styleFrom( ), ); +List supportedLocales = const [ + // specify CN/TW to fix CJK issue in flutter + Locale('zh', 'CN'), + Locale('zh', 'TW'), + Locale('fr'), + Locale('de'), + Locale('it'), + Locale('ja'), + Locale('cs'), + Locale('pl'), + Locale('ko'), + Locale('hu'), + Locale('pt'), + Locale('ru'), + Locale('sk'), + Locale('id'), + Locale('da'), + Locale('eo'), + Locale('tr'), + Locale('vi'), + Locale('pl'), + Locale('kz'), + Locale('en', 'US'), +]; + String formatDurationToTime(Duration duration) { var totalTime = duration.inSeconds; final secs = totalTime % 60; diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 43069bf79..8f04846e9 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -8,6 +8,7 @@ import 'package:flutter_hbb/desktop/screen/desktop_file_transfer_screen.dart'; import 'package:flutter_hbb/desktop/screen/desktop_port_forward_screen.dart'; import 'package:flutter_hbb/desktop/screen/desktop_remote_screen.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -123,6 +124,12 @@ void runRemoteScreen(Map argument) async { home: DesktopRemoteScreen( params: argument, ), + localizationsDelegates: const [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: supportedLocales, navigatorObservers: const [ // FirebaseAnalyticsObserver(analytics: analytics), ], @@ -141,6 +148,12 @@ void runFileTransferScreen(Map argument) async { darkTheme: MyTheme.darkTheme, themeMode: MyTheme.initialThemeMode(), home: DesktopFileTransferScreen(params: argument), + localizationsDelegates: const [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: supportedLocales, navigatorObservers: const [ // FirebaseAnalyticsObserver(analytics: analytics), ], @@ -160,6 +173,12 @@ void runPortForwardScreen(Map argument) async { darkTheme: MyTheme.darkTheme, themeMode: MyTheme.initialThemeMode(), home: DesktopPortForwardScreen(params: argument), + localizationsDelegates: const [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: supportedLocales, navigatorObservers: const [ // FirebaseAnalyticsObserver(analytics: analytics), ], @@ -178,14 +197,20 @@ void runConnectionManagerScreen() async { theme: MyTheme.lightTheme, darkTheme: MyTheme.darkTheme, themeMode: MyTheme.initialThemeMode(), + localizationsDelegates: const [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: supportedLocales, home: const DesktopServerPage(), builder: _keepScaleBuilder())); windowManager.waitUntilReadyToShow(windowOptions, () async { - await windowManager.setAlignment(Alignment.topRight); - await windowManager.show(); - await windowManager.focus(); - await windowManager.setAlignment(Alignment.topRight); // ensure - }); + await windowManager.setAlignment(Alignment.topRight); + await windowManager.show(); + await windowManager.focus(); + await windowManager.setAlignment(Alignment.topRight); // ensure + }); } WindowOptions getHiddenTitleBarWindowOptions({Size? size}) { @@ -247,6 +272,12 @@ class _AppState extends State { navigatorObservers: const [ // FirebaseAnalyticsObserver(analytics: analytics), ], + localizationsDelegates: const [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: supportedLocales, builder: isAndroid ? (context, child) => AccessibilityListener( child: MediaQuery( diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 09025ad0e..9248fe80e 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -24,6 +24,8 @@ environment: dependencies: flutter: sdk: flutter + flutter_localizations: + sdk: flutter # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. From 21eb7bd1651e586d2573b667d3fe7ba3f54091cf Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 19 Sep 2022 16:06:03 +0800 Subject: [PATCH 0531/2015] opt: more configurable scroll logic & edge size --- flutter/lib/common.dart | 1 + .../lib/desktop/pages/desktop_tab_page.dart | 2 +- .../desktop/pages/file_manager_tab_page.dart | 1 + .../desktop/pages/port_forward_tab_page.dart | 1 + .../lib/desktop/pages/remote_tab_page.dart | 2 +- flutter/lib/desktop/widgets/peer_widget.dart | 97 ++++++++++--------- .../lib/desktop/widgets/scroll_wrapper.dart | 21 ++++ flutter/pubspec.yaml | 1 + 8 files changed, 79 insertions(+), 47 deletions(-) create mode 100644 flutter/lib/desktop/widgets/scroll_wrapper.dart diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 3e08d1e0c..dfe96c903 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -248,6 +248,7 @@ List supportedLocales = const [ // specify CN/TW to fix CJK issue in flutter Locale('zh', 'CN'), Locale('zh', 'TW'), + Locale('zh', 'SG'), Locale('fr'), Locale('de'), Locale('it'), diff --git a/flutter/lib/desktop/pages/desktop_tab_page.dart b/flutter/lib/desktop/pages/desktop_tab_page.dart index a117bab12..8a49f4cde 100644 --- a/flutter/lib/desktop/pages/desktop_tab_page.dart +++ b/flutter/lib/desktop/pages/desktop_tab_page.dart @@ -60,7 +60,7 @@ class _DesktopTabPageState extends State { return Platform.isMacOS ? tabWidget : Obx(() => DragToResizeArea( - resizeEdgeSize: fullscreen.value ? 1.0 : 8.0, child: tabWidget)); + resizeEdgeSize: fullscreen.value ? 1.0 : 4.0, child: tabWidget)); } void onAddSetting() { diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index bfaf33327..086f3b184 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -81,6 +81,7 @@ class _FileManagerTabPageState extends State { return Platform.isMacOS ? tabWidget : SubWindowDragToResizeArea( + resizeEdgeSize: 4.0, windowId: windowId(), child: tabWidget, ); diff --git a/flutter/lib/desktop/pages/port_forward_tab_page.dart b/flutter/lib/desktop/pages/port_forward_tab_page.dart index f142508de..b92943f13 100644 --- a/flutter/lib/desktop/pages/port_forward_tab_page.dart +++ b/flutter/lib/desktop/pages/port_forward_tab_page.dart @@ -92,6 +92,7 @@ class _PortForwardTabPageState extends State { return Platform.isMacOS ? tabWidget : SubWindowDragToResizeArea( + resizeEdgeSize: 4.0, windowId: windowId(), child: tabWidget, ); diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index 5ea94f4a9..70003483a 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -143,7 +143,7 @@ class _ConnectionTabPageState extends State { return Platform.isMacOS ? tabWidget : Obx(() => SubWindowDragToResizeArea( - resizeEdgeSize: fullscreen.value ? 1.0 : 8.0, + resizeEdgeSize: fullscreen.value ? 1.0 : 4.0, windowId: windowId(), child: tabWidget)); } diff --git a/flutter/lib/desktop/widgets/peer_widget.dart b/flutter/lib/desktop/widgets/peer_widget.dart index f137241a9..07a621add 100644 --- a/flutter/lib/desktop/widgets/peer_widget.dart +++ b/flutter/lib/desktop/widgets/peer_widget.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:visibility_detector/visibility_detector.dart'; @@ -41,6 +42,7 @@ class _PeerWidgetState extends State<_PeerWidget> with WindowListener { static const int _maxQueryCount = 3; final _curPeers = {}; + final _scrollController = ScrollController(); var _lastChangeTime = DateTime.now(); var _lastQueryPeers = {}; var _lastQueryTime = DateTime.now().subtract(const Duration(hours: 1)); @@ -84,53 +86,58 @@ class _PeerWidgetState extends State<_PeerWidget> with WindowListener { ? Center( child: Text(translate("Empty")), ) - : SingleChildScrollView( - controller: ScrollController(), - child: ObxValue((searchText) { - return FutureBuilder>( - builder: (context, snapshot) { - if (snapshot.hasData) { - final peers = snapshot.data!; - final cards = []; - for (final peer in peers) { - cards.add(Offstage( - key: ValueKey("off${peer.id}"), - offstage: widget.offstageFunc(peer), - child: Obx( - () => SizedBox( - width: 220, - height: - peerCardUiType.value == PeerUiType.grid - ? 140 - : 42, - child: VisibilityDetector( - key: ValueKey(peer.id), - onVisibilityChanged: (info) { - final peerId = - (info.key as ValueKey).value; - if (info.visibleFraction > 0.00001) { - _curPeers.add(peerId); - } else { - _curPeers.remove(peerId); - } - _lastChangeTime = DateTime.now(); - }, - child: widget.peerCardWidgetFunc(peer), + : DesktopScrollWrapper( + scrollController: _scrollController, + child: SingleChildScrollView( + controller: _scrollController, + child: ObxValue((searchText) { + return FutureBuilder>( + builder: (context, snapshot) { + if (snapshot.hasData) { + final peers = snapshot.data!; + final cards = []; + for (final peer in peers) { + cards.add(Offstage( + key: ValueKey("off${peer.id}"), + offstage: widget.offstageFunc(peer), + child: Obx( + () => SizedBox( + width: 220, + height: + peerCardUiType.value == PeerUiType.grid + ? 140 + : 42, + child: VisibilityDetector( + key: ValueKey(peer.id), + onVisibilityChanged: (info) { + final peerId = + (info.key as ValueKey).value; + if (info.visibleFraction > 0.00001) { + _curPeers.add(peerId); + } else { + _curPeers.remove(peerId); + } + _lastChangeTime = DateTime.now(); + }, + child: widget.peerCardWidgetFunc(peer), + ), ), - ), - ))); + ))); + } + return Wrap( + spacing: space, + runSpacing: space, + children: cards); + } else { + return const Center( + child: CircularProgressIndicator(), + ); } - return Wrap( - spacing: space, runSpacing: space, children: cards); - } else { - return const Center( - child: CircularProgressIndicator(), - ); - } - }, - future: matchPeers(searchText.value, peers.peers), - ); - }, peerSearchText), + }, + future: matchPeers(searchText.value, peers.peers), + ); + }, peerSearchText), + ), ), ), ); diff --git a/flutter/lib/desktop/widgets/scroll_wrapper.dart b/flutter/lib/desktop/widgets/scroll_wrapper.dart new file mode 100644 index 000000000..dc333205f --- /dev/null +++ b/flutter/lib/desktop/widgets/scroll_wrapper.dart @@ -0,0 +1,21 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_improved_scrolling/flutter_improved_scrolling.dart'; + +class DesktopScrollWrapper extends StatelessWidget { + final ScrollController scrollController; + final Widget child; + const DesktopScrollWrapper( + {Key? key, required this.scrollController, required this.child}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return ImprovedScrolling( + scrollController: scrollController, + enableCustomMouseWheelScrolling: false, + customMouseWheelScrollConfig: + const CustomMouseWheelScrollConfig(scrollAmountMultiplier: 3.0), + child: child, + ); + } +} diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 9248fe80e..9bc8816ef 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -80,6 +80,7 @@ dependencies: desktop_drop: ^0.3.3 scroll_pos: ^0.3.0 rxdart: ^0.27.5 + flutter_improved_scrolling: ^0.0.3 dev_dependencies: icons_launcher: ^2.0.4 From 0c407994cd67191ec04d8325ca91ec03429be38f Mon Sep 17 00:00:00 2001 From: csf Date: Mon, 19 Sep 2022 17:03:12 +0800 Subject: [PATCH 0532/2015] fix android deps build --- flutter/android/gradle/wrapper/gradle-wrapper.properties | 2 +- flutter/pubspec.lock | 2 +- flutter/pubspec.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/flutter/android/gradle/wrapper/gradle-wrapper.properties b/flutter/android/gradle/wrapper/gradle-wrapper.properties index b8793d3c0..cc5527d78 100644 --- a/flutter/android/gradle/wrapper/gradle-wrapper.properties +++ b/flutter/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 7c0b90e54..bb27d243e 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -1138,7 +1138,7 @@ packages: name: wakelock url: "https://pub.dartlang.org" source: hosted - version: "0.5.6" + version: "0.6.2" wakelock_macos: dependency: transitive description: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 09025ad0e..91c3b4164 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -33,7 +33,7 @@ dependencies: external_path: ^1.0.1 provider: ^6.0.3 tuple: ^2.0.0 - wakelock: ^0.5.2 + wakelock: ^0.6.2 device_info_plus: ^4.1.2 #firebase_analytics: ^9.1.5 package_info_plus: ^1.4.2 From c2f516f57fc8f49330bea14542ee4382e27282c6 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 19 Sep 2022 17:10:12 +0800 Subject: [PATCH 0533/2015] opt: use const variable --- flutter/lib/consts.dart | 5 +++++ flutter/lib/desktop/pages/desktop_tab_page.dart | 4 +++- flutter/lib/desktop/pages/file_manager_tab_page.dart | 3 ++- flutter/lib/desktop/pages/port_forward_tab_page.dart | 3 ++- flutter/lib/desktop/pages/remote_tab_page.dart | 3 ++- flutter/lib/desktop/widgets/scroll_wrapper.dart | 5 +++-- 6 files changed, 17 insertions(+), 6 deletions(-) diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 8986f05ec..0d93df778 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -15,6 +15,11 @@ const int kMobileDefaultDisplayHeight = 1280; const int kDesktopDefaultDisplayWidth = 1080; const int kDesktopDefaultDisplayHeight = 720; +/// [kDefaultScrollAmountMultiplier] indicates how many rows can be scrolled after a minimum scroll action of mouse +const kDefaultScrollAmountMultiplier = 3.0; +const kFullScreenEdgeSize = 1.0; +const kWindowEdgeSize = 4.0; + const kInvalidValueStr = "InvalidValueStr"; /// flutter/packages/flutter/lib/src/services/keyboard_key.dart -> _keyLabels diff --git a/flutter/lib/desktop/pages/desktop_tab_page.dart b/flutter/lib/desktop/pages/desktop_tab_page.dart index 8a49f4cde..58ed34947 100644 --- a/flutter/lib/desktop/pages/desktop_tab_page.dart +++ b/flutter/lib/desktop/pages/desktop_tab_page.dart @@ -60,7 +60,9 @@ class _DesktopTabPageState extends State { return Platform.isMacOS ? tabWidget : Obx(() => DragToResizeArea( - resizeEdgeSize: fullscreen.value ? 1.0 : 4.0, child: tabWidget)); + resizeEdgeSize: + fullscreen.value ? kFullScreenEdgeSize : kWindowEdgeSize, + child: tabWidget)); } void onAddSetting() { diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index 086f3b184..9b8060bb7 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/desktop/pages/file_manager_page.dart'; import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; @@ -81,7 +82,7 @@ class _FileManagerTabPageState extends State { return Platform.isMacOS ? tabWidget : SubWindowDragToResizeArea( - resizeEdgeSize: 4.0, + resizeEdgeSize: kWindowEdgeSize, windowId: windowId(), child: tabWidget, ); diff --git a/flutter/lib/desktop/pages/port_forward_tab_page.dart b/flutter/lib/desktop/pages/port_forward_tab_page.dart index b92943f13..d4f17aaef 100644 --- a/flutter/lib/desktop/pages/port_forward_tab_page.dart +++ b/flutter/lib/desktop/pages/port_forward_tab_page.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/desktop/pages/port_forward_page.dart'; import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; @@ -92,7 +93,7 @@ class _PortForwardTabPageState extends State { return Platform.isMacOS ? tabWidget : SubWindowDragToResizeArea( - resizeEdgeSize: 4.0, + resizeEdgeSize: kWindowEdgeSize, windowId: windowId(), child: tabWidget, ); diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index 70003483a..b086a2e35 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -143,7 +143,8 @@ class _ConnectionTabPageState extends State { return Platform.isMacOS ? tabWidget : Obx(() => SubWindowDragToResizeArea( - resizeEdgeSize: fullscreen.value ? 1.0 : 4.0, + resizeEdgeSize: + fullscreen.value ? kFullScreenEdgeSize : kWindowEdgeSize, windowId: windowId(), child: tabWidget)); } diff --git a/flutter/lib/desktop/widgets/scroll_wrapper.dart b/flutter/lib/desktop/widgets/scroll_wrapper.dart index dc333205f..96eb9f735 100644 --- a/flutter/lib/desktop/widgets/scroll_wrapper.dart +++ b/flutter/lib/desktop/widgets/scroll_wrapper.dart @@ -1,4 +1,5 @@ import 'package:flutter/widgets.dart'; +import 'package:flutter_hbb/consts.dart'; import 'package:flutter_improved_scrolling/flutter_improved_scrolling.dart'; class DesktopScrollWrapper extends StatelessWidget { @@ -13,8 +14,8 @@ class DesktopScrollWrapper extends StatelessWidget { return ImprovedScrolling( scrollController: scrollController, enableCustomMouseWheelScrolling: false, - customMouseWheelScrollConfig: - const CustomMouseWheelScrollConfig(scrollAmountMultiplier: 3.0), + customMouseWheelScrollConfig: const CustomMouseWheelScrollConfig( + scrollAmountMultiplier: kDefaultScrollAmountMultiplier), child: child, ); } From e0d759c3bb47af8eae2641ac74a3319688538892 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Mon, 19 Sep 2022 18:38:19 +0800 Subject: [PATCH 0534/2015] remove menu from desktop home page --- flutter/analysis_options.yaml | 29 +- .../lib/desktop/pages/connection_page.dart | 2 + .../lib/desktop/pages/desktop_home_page.dart | 471 +----------------- flutter/pubspec.lock | 24 +- flutter/test/widget_test.dart | 30 -- 5 files changed, 42 insertions(+), 514 deletions(-) delete mode 100644 flutter/test/widget_test.dart diff --git a/flutter/analysis_options.yaml b/flutter/analysis_options.yaml index 61b6c4de1..a679f5774 100644 --- a/flutter/analysis_options.yaml +++ b/flutter/analysis_options.yaml @@ -1,29 +1,6 @@ -# This file configures the analyzer, which statically analyzes Dart code to -# check for errors, warnings, and lints. -# -# The issues identified by the analyzer are surfaced in the UI of Dart-enabled -# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be -# invoked from the command line by running `flutter analyze`. - -# The following line activates a set of recommended lints for Flutter apps, -# packages, and plugins designed to encourage good coding practices. -include: package:flutter_lints/flutter.yaml +include: package:lints/recommended.yaml linter: - # The lint rules applied to this project can be customized in the - # section below to disable rules from the `package:flutter_lints/flutter.yaml` - # included above or to enable additional rules. A list of all available lints - # and their documentation is published at - # https://dart-lang.github.io/linter/lints/index.html. - # - # Instead of disabling a lint rule for the entire project in the - # section below, it can also be suppressed for a single line of code - # or a specific dart file by using the `// ignore: name_of_lint` and - # `// ignore_for_file: name_of_lint` syntax on the line or in the file - # producing the lint. rules: - # avoid_print: false # Uncomment to disable the `avoid_print` rule - # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule - -# Additional information about this file can be found at -# https://dart.dev/guides/language/analysis-options + non_constant_identifier_names: false + sort_child_properties_last: false diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 9024c996f..e857df271 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -1,3 +1,5 @@ +// main window right pane + import 'dart:async'; import 'dart:convert'; diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index c8706d5a0..b574900fe 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -5,29 +5,14 @@ import 'package:flutter/material.dart' hide MenuItem; import 'package:flutter/services.dart'; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/desktop/pages/connection_page.dart'; -import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart'; -import 'package:flutter_hbb/desktop/widgets/popup_menu.dart'; -import 'package:flutter_hbb/desktop/widgets/material_mod_popup_menu.dart' - as mod_menu; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/server_model.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import 'package:tray_manager/tray_manager.dart'; -import 'package:url_launcher/url_launcher_string.dart'; import 'package:window_manager/window_manager.dart'; -import '../../common/widgets/dialog.dart'; - -class _MenubarTheme { - static const Color commonColor = MyTheme.accent; - // kMinInteractiveDimension - static const double height = 25.0; - static const double dividerHeight = 12.0; -} - class DesktopHomePage extends StatefulWidget { const DesktopHomePage({Key? key}) : super(key: key); @@ -66,19 +51,19 @@ class _DesktopHomePageState extends State super.build(context); return Row( children: [ - buildServerInfo(context), + buildLeftPane(context), const VerticalDivider( width: 1, thickness: 1, ), Expanded( - child: buildServerBoard(context), + child: buildRightPane(context), ), ], ); } - buildServerInfo(BuildContext context) { + buildLeftPane(BuildContext context) { return ChangeNotifierProvider.value( value: gFFI.serverModel, child: Container( @@ -95,7 +80,7 @@ class _DesktopHomePageState extends State ); } - buildServerBoard(BuildContext context) { + buildRightPane(BuildContext context) { return Container( color: MyTheme.color(context).grayBg, child: ConnectionPage(), @@ -167,93 +152,9 @@ class _DesktopHomePageState extends State } Widget buildPopupMenu(BuildContext context) { - var position; RxBool hover = false.obs; return InkWell( - onTapDown: (detail) { - final x = detail.globalPosition.dx; - final y = detail.globalPosition.dy; - position = RelativeRect.fromLTRB(x, y, x, y); - }, - onTap: () async { - final userName = await gFFI.userModel.getUserName(); - final enabledInput = await bind.mainGetOption(key: 'enable-audio'); - final defaultInput = await gFFI.getDefaultAudioInput(); - var menu = [ - await genEnablePopupMenuItem( - translate("Enable Keyboard/Mouse"), - 'enable-keyboard', - ), - await genEnablePopupMenuItem( - translate("Enable Clipboard"), - 'enable-clipboard', - ), - await genEnablePopupMenuItem( - translate("Enable File Transfer"), - 'enable-file-transfer', - ), - await genEnablePopupMenuItem( - translate("Enable TCP Tunneling"), - 'enable-tunnel', - ), - genAudioInputPopupMenuItem(enabledInput != "N", defaultInput), - PopupMenuDivider(), - PopupMenuItem( - child: Text(translate("ID/Relay Server")), - value: 'custom-server', - ), - PopupMenuItem( - child: Text(translate("IP Whitelisting")), - value: 'whitelist', - ), - PopupMenuItem( - child: Text(translate("Socks5 Proxy")), - value: 'socks5-proxy', - ), - PopupMenuDivider(), - await genEnablePopupMenuItem( - translate("Enable Service"), - 'stop-service', - ), - // TODO: direct server - await genEnablePopupMenuItem( - translate("Always connected via relay"), - 'allow-always-relay', - ), - await genEnablePopupMenuItem( - translate("Start ID/relay service"), - 'stop-rendezvous-service', - ), - PopupMenuDivider(), - userName.isEmpty - ? PopupMenuItem( - child: Text(translate("Login")), - value: 'login', - ) - : PopupMenuItem( - child: Text("${translate("Logout")} $userName"), - value: 'logout', - ), - PopupMenuItem( - child: Text(translate("Change ID")), - value: 'change-id', - ), - PopupMenuDivider(), - await genEnablePopupMenuItem( - translate("Dark Theme"), - 'allow-darktheme', - ), - PopupMenuItem( - child: Text(translate("About")), - value: 'about', - ), - ]; - final v = - await showMenu(context: context, position: position, items: menu); - if (v != null) { - onSelectMenu(v); - } - }, + onTap: () async {}, child: Obx( () => CircleAvatar( radius: 15, @@ -276,6 +177,7 @@ class _DesktopHomePageState extends State buildPasswordBoard(BuildContext context) { final model = gFFI.serverModel; RxBool refreshHover = false.obs; + RxBool editHover = false.obs; return Container( margin: EdgeInsets.only(left: 20.0, right: 16, top: 13, bottom: 13), child: Row( @@ -334,7 +236,19 @@ class _DesktopHomePageState extends State onTap: () => bind.mainUpdateTemporaryPassword(), onHover: (value) => refreshHover.value = value, ), - const _PasswordPopupMenu(), + InkWell( + child: Obx( + () => Icon( + Icons.edit, + color: editHover.value + ? MyTheme.color(context).text + : Color(0xFFDDDDDD), + size: 22, + ).marginOnly(right: 8, bottom: 2), + ), + onTap: () => {}, + onHover: (value) => editHover.value = value, + ), ], ), ], @@ -408,236 +322,6 @@ class _DesktopHomePageState extends State windowManager.removeListener(this); super.dispose(); } - - void changeTheme(String choice) async { - if (choice == "Y") { - Get.changeTheme(MyTheme.darkTheme); - } else { - Get.changeTheme(MyTheme.lightTheme); - } - Get.find().setString("darkTheme", choice); - Get.forceAppUpdate(); - } - - void onSelectMenu(String key) async { - if (key.startsWith('enable-')) { - final option = await bind.mainGetOption(key: key); - bind.mainSetOption(key: key, value: option == "N" ? "" : "N"); - } else if (key.startsWith('allow-')) { - final option = await bind.mainGetOption(key: key); - final choice = option == "Y" ? "" : "Y"; - bind.mainSetOption(key: key, value: choice); - if (key == "allow-darktheme") changeTheme(choice); - } else if (key == "stop-service") { - final option = await bind.mainGetOption(key: key); - bind.mainSetOption(key: key, value: option == "Y" ? "" : "Y"); - } else if (key == "change-id") { - changeIdDialog(); - } else if (key == "custom-server") { - changeServer(); - } else if (key == "whitelist") { - changeWhiteList(); - } else if (key == "socks5-proxy") { - changeSocks5Proxy(); - } else if (key == "about") { - about(); - } else if (key == "logout") { - logOut(); - } else if (key == "login") { - login(); - } - } - - Future> genEnablePopupMenuItem( - String label, String key) async { - final v = await bind.mainGetOption(key: key); - bool enable; - if (key == "stop-service") { - enable = v != "Y"; - } else if (key.startsWith("allow-")) { - enable = v == "Y"; - } else { - enable = v != "N"; - } - - return PopupMenuItem( - child: Row( - children: [ - Icon(Icons.check, - color: enable ? null : MyTheme.accent.withAlpha(00)), - Text( - label, - style: genTextStyle(enable), - ), - ], - ), - value: key, - ); - } - - TextStyle genTextStyle(bool isPositive) { - return isPositive - ? TextStyle() - : TextStyle( - color: Colors.redAccent, decoration: TextDecoration.lineThrough); - } - - PopupMenuItem genAudioInputPopupMenuItem( - bool enableInput, String defaultAudioInput) { - final defaultInput = defaultAudioInput.obs; - final enabled = enableInput.obs; - - return PopupMenuItem( - child: FutureBuilder>( - future: gFFI.getAudioInputs(), - builder: (context, snapshot) { - if (snapshot.hasData) { - final inputs = snapshot.data!.toList(); - if (Platform.isWindows) { - inputs.insert(0, translate("System Sound")); - } - var inputList = inputs - .map((e) => PopupMenuItem( - child: Row( - children: [ - Obx(() => Offstage( - offstage: defaultInput.value != e, - child: Icon(Icons.check))), - Expanded( - child: Tooltip( - message: e, - child: Text( - "$e", - maxLines: 1, - overflow: TextOverflow.ellipsis, - ))), - ], - ), - value: e, - )) - .toList(); - inputList.insert( - 0, - PopupMenuItem( - child: Row( - children: [ - Obx(() => Offstage( - offstage: enabled.value, child: Icon(Icons.check))), - Expanded(child: Text(translate("Mute"))), - ], - ), - value: "Mute", - )); - return PopupMenuButton( - padding: EdgeInsets.zero, - child: Container( - alignment: Alignment.centerLeft, - child: Text(translate("Audio Input"))), - itemBuilder: (context) => inputList, - onSelected: (dev) async { - if (dev == "Mute") { - await bind.mainSetOption( - key: 'enable-audio', value: enabled.value ? '' : 'N'); - enabled.value = - await bind.mainGetOption(key: 'enable-audio') != 'N'; - } else if (dev != await gFFI.getDefaultAudioInput()) { - gFFI.setDefaultAudioInput(dev); - defaultInput.value = dev; - } - }, - ); - } else { - return Text("..."); - } - }, - ), - value: 'audio-input', - ); - } - - void about() async { - final appName = await bind.mainGetAppName(); - final license = await bind.mainGetLicense(); - final version = await bind.mainGetVersion(); - const linkStyle = TextStyle(decoration: TextDecoration.underline); - gFFI.dialogManager.show((setState, close) { - return CustomAlertDialog( - title: Text("About $appName"), - content: ConstrainedBox( - constraints: const BoxConstraints(minWidth: 500), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox( - height: 8.0, - ), - Text("Version: $version").marginSymmetric(vertical: 4.0), - InkWell( - onTap: () { - launchUrlString("https://rustdesk.com/privacy"); - }, - child: const Text( - "Privacy Statement", - style: linkStyle, - ).marginSymmetric(vertical: 4.0)), - InkWell( - onTap: () { - launchUrlString("https://rustdesk.com"); - }, - child: const Text( - "Website", - style: linkStyle, - ).marginSymmetric(vertical: 4.0)), - Container( - decoration: const BoxDecoration(color: Color(0xFF2c8cff)), - padding: - const EdgeInsets.symmetric(vertical: 24, horizontal: 8), - child: Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Copyright © 2022 Purslane Ltd.\n$license", - style: const TextStyle(color: Colors.white), - ), - const Text( - "Made with heart in this chaotic world!", - style: TextStyle( - fontWeight: FontWeight.w800, - color: Colors.white), - ) - ], - ), - ), - ], - ), - ).marginSymmetric(vertical: 4.0) - ], - ), - ), - actions: [ - TextButton(onPressed: close, child: Text(translate("OK"))), - ], - onSubmit: close, - onCancel: close, - ); - }); - } - - void login() { - loginDialog().then((success) { - if (success) { - // refresh frame - setState(() {}); - } - }); - } - - void logOut() { - gFFI.userModel.logOut().then((_) => {setState(() {})}); - } } /// common login dialog for desktop @@ -874,120 +558,3 @@ void setPasswordDialog() async { ); }); } - -class _PasswordPopupMenu extends StatefulWidget { - const _PasswordPopupMenu({Key? key}) : super(key: key); - - @override - State<_PasswordPopupMenu> createState() => _PasswordPopupMenuState(); -} - -class _PasswordPopupMenuState extends State<_PasswordPopupMenu> { - final RxBool _tempEnabled = true.obs; - final RxBool _permEnabled = true.obs; - - List> _buildMenus() { - return >[ - MenuEntryRadios( - text: translate('Password type'), - optionsGetter: () => [ - MenuEntryRadioOption( - text: translate('Use temporary password'), - value: kUseTemporaryPassword), - MenuEntryRadioOption( - text: translate('Use permanent password'), - value: kUsePermanentPassword), - MenuEntryRadioOption( - text: translate('Use both passwords'), - value: kUseBothPasswords), - ], - curOptionGetter: () async { - return gFFI.serverModel.verificationMethod; - }, - optionSetter: (String oldValue, String newValue) async { - await bind.mainSetOption( - key: "verification-method", value: newValue); - await gFFI.serverModel.updatePasswordModel(); - setState(() { - _tempEnabled.value = - gFFI.serverModel.verificationMethod != kUsePermanentPassword; - _permEnabled.value = - gFFI.serverModel.verificationMethod != kUseTemporaryPassword; - }); - }), - MenuEntryDivider(), - MenuEntryButton( - enabled: _permEnabled, - childBuilder: (TextStyle? style) => Text( - translate('Set permanent password'), - style: style, - ), - proc: () { - setPasswordDialog(); - }, - dismissOnClicked: true, - ), - MenuEntrySubMenu( - enabled: _tempEnabled, - text: translate('Set temporary password length'), - entries: [ - MenuEntryRadios( - enabled: _tempEnabled, - text: translate(''), - optionsGetter: () => [ - MenuEntryRadioOption( - text: translate('6'), - value: '6', - enabled: _tempEnabled, - ), - MenuEntryRadioOption( - text: translate('8'), - value: '8', - enabled: _tempEnabled, - ), - MenuEntryRadioOption( - text: translate('10'), - value: '10', - enabled: _tempEnabled, - ), - ], - curOptionGetter: () async { - return gFFI.serverModel.temporaryPasswordLength; - }, - optionSetter: (String oldValue, String newValue) async { - if (oldValue != newValue) { - await gFFI.serverModel.setTemporaryPasswordLength(newValue); - await gFFI.serverModel.updatePasswordModel(); - } - }), - ]) - ]; - } - - @override - Widget build(BuildContext context) { - final editHover = false.obs; - return mod_menu.PopupMenuButton( - padding: EdgeInsets.zero, - onHover: (v) => editHover.value = v, - tooltip: translate(''), - position: mod_menu.PopupMenuPosition.overSide, - itemBuilder: (BuildContext context) => _buildMenus() - .map((entry) => entry.build( - context, - const MenuConfig( - commonColor: _MenubarTheme.commonColor, - height: _MenubarTheme.height, - dividerHeight: _MenubarTheme.dividerHeight, - ))) - .expand((i) => i) - .toList(), - child: Obx(() => Icon(Icons.edit, - size: 22, - color: editHover.value - ? MyTheme.color(context).text - : const Color(0xFFDDDDDD)) - .marginOnly(bottom: 2)), - ); - } -} diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 7c0b90e54..c6caebc77 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -140,7 +140,7 @@ packages: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.2.1" + version: "1.2.0" charcode: dependency: transitive description: @@ -161,7 +161,7 @@ packages: name: clock url: "https://pub.dartlang.org" source: hosted - version: "1.1.1" + version: "1.1.0" code_builder: dependency: transitive description: @@ -367,6 +367,13 @@ packages: url: "https://github.com/Kingtous/rustdesk_flutter_custom_cursor" source: git version: "0.0.1" + flutter_improved_scrolling: + dependency: "direct main" + description: + name: flutter_improved_scrolling + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.3" flutter_lints: dependency: "direct dev" description: @@ -374,6 +381,11 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.1" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" flutter_parsed_text: dependency: transitive description: @@ -478,7 +490,7 @@ packages: name: icons_launcher url: "https://pub.dartlang.org" source: hosted - version: "2.0.5" + version: "2.0.4" image: dependency: "direct main" description: @@ -576,7 +588,7 @@ packages: name: material_color_utilities url: "https://pub.dartlang.org" source: hosted - version: "0.1.5" + version: "0.1.4" menu_base: dependency: transitive description: @@ -590,7 +602,7 @@ packages: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.8.0" + version: "1.7.0" mime: dependency: transitive description: @@ -667,7 +679,7 @@ packages: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.2" + version: "1.8.1" path_provider: dependency: "direct main" description: diff --git a/flutter/test/widget_test.dart b/flutter/test/widget_test.dart deleted file mode 100644 index 3e7534740..000000000 --- a/flutter/test/widget_test.dart +++ /dev/null @@ -1,30 +0,0 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility that Flutter provides. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:flutter_hbb/main.dart'; - -void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(App()); - - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); - - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); - - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); - }); -} From e1ab01a97f6d769c96c41a16ec43665b990edfd5 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 19 Sep 2022 19:18:05 +0800 Subject: [PATCH 0535/2015] opt: use custom scroll feature --- flutter/lib/consts.dart | 2 +- flutter/lib/desktop/widgets/peer_widget.dart | 1 + flutter/lib/desktop/widgets/scroll_wrapper.dart | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 0d93df778..3ed080206 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -16,7 +16,7 @@ const int kDesktopDefaultDisplayWidth = 1080; const int kDesktopDefaultDisplayHeight = 720; /// [kDefaultScrollAmountMultiplier] indicates how many rows can be scrolled after a minimum scroll action of mouse -const kDefaultScrollAmountMultiplier = 3.0; +const kDefaultScrollAmountMultiplier = 10.0; const kFullScreenEdgeSize = 1.0; const kWindowEdgeSize = 4.0; diff --git a/flutter/lib/desktop/widgets/peer_widget.dart b/flutter/lib/desktop/widgets/peer_widget.dart index 07a621add..1b0626198 100644 --- a/flutter/lib/desktop/widgets/peer_widget.dart +++ b/flutter/lib/desktop/widgets/peer_widget.dart @@ -89,6 +89,7 @@ class _PeerWidgetState extends State<_PeerWidget> with WindowListener { : DesktopScrollWrapper( scrollController: _scrollController, child: SingleChildScrollView( + physics: const NeverScrollableScrollPhysics(), controller: _scrollController, child: ObxValue((searchText) { return FutureBuilder>( diff --git a/flutter/lib/desktop/widgets/scroll_wrapper.dart b/flutter/lib/desktop/widgets/scroll_wrapper.dart index 96eb9f735..6ad63b99c 100644 --- a/flutter/lib/desktop/widgets/scroll_wrapper.dart +++ b/flutter/lib/desktop/widgets/scroll_wrapper.dart @@ -13,7 +13,7 @@ class DesktopScrollWrapper extends StatelessWidget { Widget build(BuildContext context) { return ImprovedScrolling( scrollController: scrollController, - enableCustomMouseWheelScrolling: false, + enableCustomMouseWheelScrolling: true, customMouseWheelScrollConfig: const CustomMouseWheelScrollConfig( scrollAmountMultiplier: kDefaultScrollAmountMultiplier), child: child, From 0679d01a63cd9a4680534be65c8f4806a8a5a79f Mon Sep 17 00:00:00 2001 From: rustdesk Date: Mon, 19 Sep 2022 19:24:51 +0800 Subject: [PATCH 0536/2015] fix connection status style --- .../lib/desktop/pages/connection_page.dart | 32 ++++++++++++------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index e857df271..c989badb8 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -94,8 +94,8 @@ class _ConnectionPageState extends State { ).marginSymmetric(horizontal: 22), ), const Divider(), - SizedBox(height: 50, child: Obx(() => buildStatus())) - .paddingSymmetric(horizontal: 12.0) + SizedBox(child: Obx(() => buildStatus())) + .paddingOnly(bottom: 12, top: 6), ]), ); } @@ -303,6 +303,8 @@ class _ConnectionPageState extends State { var svcIsUsingPublicServer = true.obs; Widget buildStatus() { + final fontSize = 14.0; + final textStyle = TextStyle(fontSize: fontSize); final light = Container( height: 8, width: 8, @@ -310,13 +312,13 @@ class _ConnectionPageState extends State { borderRadius: BorderRadius.circular(20), color: svcStopped.value ? Colors.redAccent : Colors.green, ), - ).paddingSymmetric(horizontal: 10.0); + ).paddingSymmetric(horizontal: 12.0); if (svcStopped.value) { return Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ light, - Text(translate("Service is not running")), + Text(translate("Service is not running"), style: textStyle), TextButton( onPressed: () async { bool checked = await bind.mainCheckSuperUserPermission(); @@ -324,19 +326,25 @@ class _ConnectionPageState extends State { bind.mainSetOption(key: "stop-service", value: ""); } }, - child: Text(translate("Start Service"))) + child: Text(translate("Start Service"), style: textStyle)) ], ); } else { if (svcStatusCode.value == 0) { return Row( crossAxisAlignment: CrossAxisAlignment.center, - children: [light, Text(translate("connecting_status"))], + children: [ + light, + Text(translate("connecting_status"), style: textStyle) + ], ); } else if (svcStatusCode.value == -1) { return Row( crossAxisAlignment: CrossAxisAlignment.center, - children: [light, Text(translate("not_ready_status"))], + children: [ + light, + Text(translate("not_ready_status"), style: textStyle) + ], ); } } @@ -344,13 +352,15 @@ class _ConnectionPageState extends State { crossAxisAlignment: CrossAxisAlignment.center, children: [ light, - Text(translate('Ready')), + Text(translate('Ready'), style: textStyle), + Text(', ', style: textStyle), svcIsUsingPublicServer.value ? InkWell( onTap: onUsePublicServerGuide, child: Text( - ', ${translate('setup_server_tip')}', - style: TextStyle(decoration: TextDecoration.underline), + translate('setup_server_tip'), + style: TextStyle( + decoration: TextDecoration.underline, fontSize: fontSize), ), ) : Offstage() @@ -424,7 +434,7 @@ class _ConnectionPageState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text(translate("${model.abError}")), + Text(translate(model.abError)), TextButton( onPressed: () { setState(() {}); From 19586f28bd4cba89de12b053cada52e70ea9627b Mon Sep 17 00:00:00 2001 From: rustdesk Date: Mon, 19 Sep 2022 19:42:13 +0800 Subject: [PATCH 0537/2015] save peer_tab_index --- flutter/lib/desktop/pages/connection_page.dart | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index c989badb8..198f64bcc 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -768,20 +768,33 @@ class _PeerTabbedPage extends StatefulWidget { class _PeerTabbedPageState extends State<_PeerTabbedPage> with SingleTickerProviderStateMixin { - late PageController _pageController = PageController(); - RxInt _tabIndex = 0.obs; + late final PageController _pageController = PageController(); + final RxInt _tabIndex = 0.obs; @override void initState() { + () async { + await bind.mainGetLocalOption(key: 'peer_tab_index').then((value) { + if (value == '') return; + final tab = int.parse(value); + _tabIndex.value = tab; + _pageController.jumpToPage(tab); + }); + }(); super.initState(); } // hard code for now void _handleTabSelection(int index) { + if (index == _tabIndex.value) return; // reset search text peerSearchText.value = ""; peerSearchTextController.clear(); _tabIndex.value = index; + () async { + await bind.mainSetLocalOption( + key: 'peer_tab_index', value: index.toString()); + }(); _pageController.jumpToPage(index); switch (index) { case 0: From df5a2ab5569cae8ad9c8a29c49631f5dc96b5a5e Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 19 Sep 2022 21:09:54 +0800 Subject: [PATCH 0538/2015] opt: custom scroll for better scroll and add trackpad support --- flutter/lib/consts.dart | 4 +++- flutter/lib/desktop/pages/connection_page.dart | 4 +++- flutter/lib/desktop/widgets/peer_widget.dart | 2 +- flutter/lib/desktop/widgets/scroll_wrapper.dart | 6 +++++- flutter/pubspec.yaml | 5 ++++- 5 files changed, 16 insertions(+), 5 deletions(-) diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 3ed080206..e7c506ecc 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -16,7 +16,9 @@ const int kDesktopDefaultDisplayWidth = 1080; const int kDesktopDefaultDisplayHeight = 720; /// [kDefaultScrollAmountMultiplier] indicates how many rows can be scrolled after a minimum scroll action of mouse -const kDefaultScrollAmountMultiplier = 10.0; +const kDefaultScrollAmountMultiplier = 5.0; +const kDefaultScrollDuration = Duration(milliseconds: 50); +const kDefaultMouseWhellThrottleDuration = Duration(milliseconds: 50); const kFullScreenEdgeSize = 1.0; const kWindowEdgeSize = 4.0; diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index e857df271..3c7994861 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -852,7 +852,9 @@ class _PeerTabbedPageState extends State<_PeerTabbedPage> Widget _createTabBarView() { return Expanded( child: PageView( - controller: _pageController, children: super.widget.children) + physics: NeverScrollableScrollPhysics(), + controller: _pageController, + children: super.widget.children) .marginSymmetric(vertical: 12)); } diff --git a/flutter/lib/desktop/widgets/peer_widget.dart b/flutter/lib/desktop/widgets/peer_widget.dart index 1b0626198..a7edc0b93 100644 --- a/flutter/lib/desktop/widgets/peer_widget.dart +++ b/flutter/lib/desktop/widgets/peer_widget.dart @@ -89,7 +89,7 @@ class _PeerWidgetState extends State<_PeerWidget> with WindowListener { : DesktopScrollWrapper( scrollController: _scrollController, child: SingleChildScrollView( - physics: const NeverScrollableScrollPhysics(), + physics: NeverScrollableScrollPhysics(), controller: _scrollController, child: ObxValue((searchText) { return FutureBuilder>( diff --git a/flutter/lib/desktop/widgets/scroll_wrapper.dart b/flutter/lib/desktop/widgets/scroll_wrapper.dart index 6ad63b99c..aaa3aa403 100644 --- a/flutter/lib/desktop/widgets/scroll_wrapper.dart +++ b/flutter/lib/desktop/widgets/scroll_wrapper.dart @@ -14,7 +14,11 @@ class DesktopScrollWrapper extends StatelessWidget { return ImprovedScrolling( scrollController: scrollController, enableCustomMouseWheelScrolling: true, - customMouseWheelScrollConfig: const CustomMouseWheelScrollConfig( + customMouseWheelScrollConfig: CustomMouseWheelScrollConfig( + scrollDuration: kDefaultScrollDuration, + scrollCurve: Curves.linearToEaseOut, + mouseWheelTurnsThrottleTimeMs: + kDefaultMouseWhellThrottleDuration.inMilliseconds, scrollAmountMultiplier: kDefaultScrollAmountMultiplier), child: child, ); diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 9bc8816ef..696f2a5d2 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -80,7 +80,10 @@ dependencies: desktop_drop: ^0.3.3 scroll_pos: ^0.3.0 rxdart: ^0.27.5 - flutter_improved_scrolling: ^0.0.3 + flutter_improved_scrolling: + git: + url: https://github.com/Kingtous/flutter_improved_scrolling + ref: 62f09545149f320616467c306c8c5f71714a18e6 dev_dependencies: icons_launcher: ^2.0.4 From e5a292ef26af89ece90aa56a336f9610ed9e41f7 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 20 Sep 2022 10:25:18 +0800 Subject: [PATCH 0539/2015] opt: flutter_improved_scrolling doc & remove border --- flutter/lib/desktop/pages/desktop_tab_page.dart | 2 -- flutter/pubspec.yaml | 12 ++++++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_tab_page.dart b/flutter/lib/desktop/pages/desktop_tab_page.dart index 58ed34947..2f24ddbde 100644 --- a/flutter/lib/desktop/pages/desktop_tab_page.dart +++ b/flutter/lib/desktop/pages/desktop_tab_page.dart @@ -38,8 +38,6 @@ class _DesktopTabPageState extends State { RxBool fullscreen = false.obs; Get.put(fullscreen, tag: 'fullscreen'); final tabWidget = Container( - decoration: BoxDecoration( - border: Border.all(color: MyTheme.color(context).border!)), child: Overlay(initialEntries: [ OverlayEntry(builder: (context) { gFFI.dialogManager.setOverlayState(Overlay.of(context)); diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 696f2a5d2..7eb074e4d 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -80,10 +80,14 @@ dependencies: desktop_drop: ^0.3.3 scroll_pos: ^0.3.0 rxdart: ^0.27.5 - flutter_improved_scrolling: - git: - url: https://github.com/Kingtous/flutter_improved_scrolling - ref: 62f09545149f320616467c306c8c5f71714a18e6 + flutter_improved_scrolling: ^0.0.3 + # currently, we use flutter 3.0.5 for windows build, latest for other builds. + # + # for flutter 3.0.5, please use official version(just comment code below). + # if build rustdesk by flutter >=3.3, please use our custom pub below (uncomment code below). + # git: + # url: https://github.com/Kingtous/flutter_improved_scrolling + # ref: 62f09545149f320616467c306c8c5f71714a18e6 dev_dependencies: icons_launcher: ^2.0.4 From 13fe2164d47cef8637524b168c2ebe270567cd60 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 20 Sep 2022 18:09:02 +0800 Subject: [PATCH 0540/2015] more style bug fix --- flutter/lib/consts.dart | 4 +- .../lib/desktop/pages/connection_page.dart | 82 ++++++++----------- src/lang/cn.rs | 2 +- 3 files changed, 39 insertions(+), 49 deletions(-) diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index e7c506ecc..55d13f10e 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -19,8 +19,8 @@ const int kDesktopDefaultDisplayHeight = 720; const kDefaultScrollAmountMultiplier = 5.0; const kDefaultScrollDuration = Duration(milliseconds: 50); const kDefaultMouseWhellThrottleDuration = Duration(milliseconds: 50); -const kFullScreenEdgeSize = 1.0; -const kWindowEdgeSize = 4.0; +const kFullScreenEdgeSize = 0.0; +const kWindowEdgeSize = 1.0; const kInvalidValueStr = "InvalidValueStr"; diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 05cb3858a..5c127354e 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -774,27 +774,31 @@ class _PeerTabbedPageState extends State<_PeerTabbedPage> @override void initState() { () async { - await bind.mainGetLocalOption(key: 'peer_tab_index').then((value) { + await bind.mainGetLocalOption(key: 'peer-tab-index').then((value) { if (value == '') return; final tab = int.parse(value); _tabIndex.value = tab; _pageController.jumpToPage(tab); }); + await bind.mainGetLocalOption(key: 'peer-card-ui-type').then((value) { + if (value == '') return; + final tab = int.parse(value); + peerCardUiType.value = + tab == PeerUiType.list.index ? PeerUiType.list : PeerUiType.grid; + }); }(); super.initState(); } // hard code for now - void _handleTabSelection(int index) { + Future _handleTabSelection(int index) async { if (index == _tabIndex.value) return; // reset search text peerSearchText.value = ""; peerSearchTextController.clear(); _tabIndex.value = index; - () async { - await bind.mainSetLocalOption( - key: 'peer_tab_index', value: index.toString()); - }(); + await bind.mainSetLocalOption( + key: 'peer-tab-index', value: index.toString()); _pageController.jumpToPage(index); switch (index) { case 0: @@ -845,7 +849,7 @@ class _PeerTabbedPageState extends State<_PeerTabbedPage> shrinkWrap: true, controller: ScrollController(), children: super.widget.tabs.asMap().entries.map((t) { - return Obx(() => GestureDetector( + return Obx(() => InkWell( child: Container( padding: EdgeInsets.symmetric(horizontal: 8), decoration: BoxDecoration( @@ -867,7 +871,7 @@ class _PeerTabbedPageState extends State<_PeerTabbedPage> : MyTheme.color(context).lightText), ), )), - onTap: () => _handleTabSelection(t.key), + onTap: () async => await _handleTabSelection(t.key), )); }).toList()); } @@ -959,44 +963,30 @@ class _PeerTabbedPageState extends State<_PeerTabbedPage> _createPeerViewTypeSwitch(BuildContext context) { final activeDeco = BoxDecoration(color: MyTheme.color(context).bg); return Row( - children: [ - Obx( - () => Container( - padding: EdgeInsets.all(4.0), - decoration: - peerCardUiType.value == PeerUiType.grid ? activeDeco : null, - child: InkWell( - onTap: () { - peerCardUiType.value = PeerUiType.grid; - }, - child: Icon( - Icons.grid_view_rounded, - size: 18, - color: peerCardUiType.value == PeerUiType.grid - ? MyTheme.color(context).text - : MyTheme.color(context).lightText, - )), - ), - ), - Obx( - () => Container( - padding: EdgeInsets.all(4.0), - decoration: - peerCardUiType.value == PeerUiType.list ? activeDeco : null, - child: InkWell( - onTap: () { - peerCardUiType.value = PeerUiType.list; - }, - child: Icon( - Icons.list, - size: 18, - color: peerCardUiType.value == PeerUiType.list - ? MyTheme.color(context).text - : MyTheme.color(context).lightText, - )), - ), - ), - ], + children: [PeerUiType.grid, PeerUiType.list] + .map((type) => Obx( + () => Container( + padding: EdgeInsets.all(4.0), + decoration: peerCardUiType.value == type ? activeDeco : null, + child: InkWell( + onTap: () async { + await bind.mainSetLocalOption( + key: 'peer-card-ui-type', + value: type.index.toString()); + peerCardUiType.value = type; + }, + child: Icon( + type == PeerUiType.grid + ? Icons.grid_view_rounded + : Icons.list, + size: 18, + color: peerCardUiType.value == type + ? MyTheme.color(context).text + : MyTheme.color(context).lightText, + )), + ), + )) + .toList(), ); } } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 920013be3..664d6f05b 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -189,7 +189,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("x11 expected", "请切换到 x11"), ("Port", "端口"), ("Settings", "设置"), - ("Username", " 用户名"), + ("Username", "用户名"), ("Invalid port", "无效端口"), ("Closed manually by the peer", "被对方手动关闭"), ("Enable remote configuration modification", "允许远程修改配置"), From 3101c4e11982bb3abd254403562d62f0d9b2d4fb Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 20 Sep 2022 19:31:32 +0800 Subject: [PATCH 0541/2015] fix formatId, right panel button style, default windows size (windows, Linux, no idea about Mac, need to check with xcode) --- flutter/lib/common.dart | 6 ++- .../lib/common/formatter/id_formatter.dart | 1 + .../lib/desktop/pages/connection_page.dart | 53 ++++++++++--------- .../lib/desktop/pages/desktop_home_page.dart | 9 ++-- flutter/linux/my_application.cc | 2 +- flutter/windows/runner/main.cpp | 2 +- 6 files changed, 40 insertions(+), 33 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index dfe96c903..f268286a9 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -760,8 +760,9 @@ class PermissionManager { if (isDesktop) { return Future.value(true); } - if (!permissions.contains(type)) + if (!permissions.contains(type)) { return Future.error("Wrong permission!$type"); + } return gFFI.invokeMethod("check_permission", type); } @@ -769,8 +770,9 @@ class PermissionManager { if (isDesktop) { return Future.value(true); } - if (!permissions.contains(type)) + if (!permissions.contains(type)) { return Future.error("Wrong permission!$type"); + } gFFI.invokeMethod("request_permission", type); if (type == "ignore_battery_optimizations") { diff --git a/flutter/lib/common/formatter/id_formatter.dart b/flutter/lib/common/formatter/id_formatter.dart index c7ce14da4..a9e4893a6 100644 --- a/flutter/lib/common/formatter/id_formatter.dart +++ b/flutter/lib/common/formatter/id_formatter.dart @@ -33,6 +33,7 @@ class IDTextInputFormatter extends TextInputFormatter { String formatID(String id) { String id2 = id.replaceAll(' ', ''); + if (int.tryParse(id2) == null) return id; String newID = ''; if (id2.length <= 3) { newID = id2; diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 5c127354e..f7fdd3e5f 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -215,7 +215,7 @@ class _ConnectionPageState extends State { onConnect(isFileTransfer: true); }, child: Container( - height: 24, + height: 27, alignment: Alignment.center, decoration: BoxDecoration( color: ftPressed.value @@ -252,31 +252,36 @@ class _ConnectionPageState extends State { onTapCancel: () => connPressed.value = false, onHover: (value) => connHover.value = value, onTap: onConnect, - child: Container( - height: 24, - decoration: BoxDecoration( - color: connPressed.value - ? MyTheme.accent - : MyTheme.button, - border: Border.all( - color: connPressed.value - ? MyTheme.accent - : connHover.value - ? MyTheme.hoverBorder - : MyTheme.button, + child: ConstrainedBox( + constraints: BoxConstraints( + minWidth: 80.0, ), - borderRadius: BorderRadius.circular(5), - ), - child: Center( - child: Text( - translate( - "Connect", + child: Container( + height: 27, + decoration: BoxDecoration( + color: connPressed.value + ? MyTheme.accent + : MyTheme.button, + border: Border.all( + color: connPressed.value + ? MyTheme.accent + : connHover.value + ? MyTheme.hoverBorder + : MyTheme.button, + ), + borderRadius: BorderRadius.circular(5), ), - style: TextStyle( - fontSize: 12, color: MyTheme.color(context).bg), - ), - ).marginSymmetric(horizontal: 12), - ), + child: Center( + child: Text( + translate( + "Connect", + ), + style: TextStyle( + fontSize: 12, + color: MyTheme.color(context).bg), + ), + ).marginSymmetric(horizontal: 12), + )), ), ), ], diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index b574900fe..edae7deeb 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -290,7 +290,7 @@ class _DesktopHomePageState extends State @override void onTrayMenuItemClick(MenuItem menuItem) { - print('click ${menuItem.key}'); + debugPrint('click ${menuItem.key}'); switch (menuItem.key) { case "quit": exit(0); @@ -308,8 +308,8 @@ class _DesktopHomePageState extends State trayManager.addListener(this); windowManager.addListener(this); rustDeskWinManager.setMethodHandler((call, fromWindowId) async { - print( - "call ${call.method} with args ${call.arguments} from window ${fromWindowId}"); + debugPrint( + "call ${call.method} with args ${call.arguments} from window $fromWindowId"); if (call.method == "main_window_on_top") { window_on_top(null); } @@ -373,8 +373,7 @@ Future loginDialog() async { debugPrint("$resp"); completer.complete(true); } catch (err) { - // ignore: avoid_print - print(err.toString()); + debugPrint(err.toString()); cancel(); return; } diff --git a/flutter/linux/my_application.cc b/flutter/linux/my_application.cc index deea3f549..6d101687b 100644 --- a/flutter/linux/my_application.cc +++ b/flutter/linux/my_application.cc @@ -51,7 +51,7 @@ static void my_application_activate(GApplication* application) { // auto bdw = bitsdojo_window_from(window); // <--- add this line // bdw->setCustomFrame(true); // <-- add this line - gtk_window_set_default_size(window, 1280, 720); // <-- comment this line + gtk_window_set_default_size(window, 800, 600); // <-- comment this line gtk_widget_show(GTK_WIDGET(window)); g_autoptr(FlDartProject) project = fl_dart_project_new(); diff --git a/flutter/windows/runner/main.cpp b/flutter/windows/runner/main.cpp index f84fc1861..0724ace8a 100644 --- a/flutter/windows/runner/main.cpp +++ b/flutter/windows/runner/main.cpp @@ -52,7 +52,7 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, FlutterWindow window(project); Win32Window::Point origin(10, 10); - Win32Window::Size size(1280, 720); + Win32Window::Size size(800, 600); if (!window.CreateAndShow(L"flutter_hbb", origin, size)) { return EXIT_FAILURE; From 8c10675d8a2638a6ea2d497ab7392733b02dd255 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Wed, 21 Sep 2022 11:28:28 +0800 Subject: [PATCH 0542/2015] feat: windows portable build script --- Cargo.toml | 2 +- build.py | 27 ++- libs/portable/.gitignore | 3 + libs/portable/Cargo.lock | 285 ++++++++++++++++++++++++++++++++ libs/portable/Cargo.toml | 16 ++ libs/portable/build.rs | 5 + libs/portable/generate.py | 88 ++++++++++ libs/portable/icon.rc | 1 + libs/portable/requirements.txt | 1 + libs/portable/src/bin_reader.rs | 134 +++++++++++++++ libs/portable/src/main.rs | 51 ++++++ 11 files changed, 611 insertions(+), 2 deletions(-) create mode 100644 libs/portable/.gitignore create mode 100644 libs/portable/Cargo.lock create mode 100644 libs/portable/Cargo.toml create mode 100644 libs/portable/build.rs create mode 100644 libs/portable/generate.py create mode 100644 libs/portable/icon.rc create mode 100644 libs/portable/requirements.txt create mode 100644 libs/portable/src/bin_reader.rs create mode 100644 libs/portable/src/main.rs diff --git a/Cargo.toml b/Cargo.toml index 8670ade40..062e32abb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -119,7 +119,7 @@ jni = "0.19" flutter_rust_bridge = { git = "https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge" } [workspace] -members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "libs/virtual_display", "libs/simple_rc"] +members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "libs/virtual_display", "libs/simple_rc", "libs/portable"] [package.metadata.winres] LegalCopyright = "Copyright © 2022 Purslane, Inc." diff --git a/build.py b/build.py index 52ccbe41e..2c08e5af0 100755 --- a/build.py +++ b/build.py @@ -73,6 +73,11 @@ def make_parser(): action='store_true', help='Enable feature hwcodec' ) + parser.add_argument( + '--portable', + action='store_true', + help='Build windows portable' + ) return parser @@ -187,6 +192,20 @@ def build_flutter_arch_manjaro(): os.chdir('..') os.system('HBB=`pwd` FLUTTER=1 makepkg -f') +def build_flutter_windows_portable(): + os.system("cargo build --lib --features flutter --release") + os.chdir('flutter') + os.system("flutter build windows --release") + os.chdir('..') + os.chdir("libs/portable") + os.system("pip3 install -r requirements.txt") + os.system("python3 .\\generate.py -f ..\\..\\flutter\\build\\windows\\runner\Release\ -o . -e ..\\..\\flutter\\build\\windows\\runner\\Release\\rustdesk.exe") + os.chdir("../..") + if os.path.exists("./rustdesk_portable.exe"): + os.replace("./target/release/rustdesk-portable-packer.exe", "./rustdesk_portable.exe") + else: + os.rename("./target/release/rustdesk-portable-packer.exe", "./rustdesk_portable.exe") + print(f"output location: {os.path.abspath(os.curdir)}/rustdesk_portable.exe") def main(): parser = make_parser() @@ -201,13 +220,19 @@ def main(): '//#![windows_subsystem', '#![windows_subsystem')) if os.path.exists(exe_path): os.unlink(exe_path) - os.system('python3 res/inline-sciter.py') if os.path.isfile('/usr/bin/pacman'): os.system('git checkout src/ui/common.tis') version = get_version() features = ",".join(get_features(args)) flutter = args.flutter + if not flutter: + # not flutter, is sciter + os.system('python3 res/inline-sciter.py') + portable = args.portable if windows: + if portable: + build_flutter_windows_portable() + return os.system('cargo build --release --features ' + features) # os.system('upx.exe target/release/rustdesk.exe') os.system('mv target/release/rustdesk.exe target/release/RustDesk.exe') diff --git a/libs/portable/.gitignore b/libs/portable/.gitignore new file mode 100644 index 000000000..8dfaeb73d --- /dev/null +++ b/libs/portable/.gitignore @@ -0,0 +1,3 @@ +/target +*.exe +*.bin \ No newline at end of file diff --git a/libs/portable/Cargo.lock b/libs/portable/Cargo.lock new file mode 100644 index 000000000..623d64918 --- /dev/null +++ b/libs/portable/Cargo.lock @@ -0,0 +1,285 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "brotli" +version = "3.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ad2d4653bf5ca36ae797b1f4bb4dbddb60ce49ca4aed8a2ce4829f60425b80" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "cc" +version = "1.0.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "embed-resource" +version = "1.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "936c1354206a875581696369aef920e12396e93bbd251c43a7a3f3fa85023a7d" +dependencies = [ + "cc", + "rustc_version", + "toml", + "vswhom", + "winreg", +] + +[[package]] +name = "getrandom" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "libc" +version = "0.2.133" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f80d65747a3e43d1596c7c5492d95d5edddaabd45a7fcdb02b95f644164966" + +[[package]] +name = "md5" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" + +[[package]] +name = "proc-macro2" +version = "1.0.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +dependencies = [ + "getrandom", + "redox_syscall", + "thiserror", +] + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "rustdesk-portable-packer" +version = "0.1.0" +dependencies = [ + "brotli", + "dirs", + "embed-resource", + "md5", +] + +[[package]] +name = "semver" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e25dfac463d778e353db5be2449d1cce89bd6fd23c9f1ea21310ce6e5a1b29c4" + +[[package]] +name = "serde" +version = "1.0.144" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860" + +[[package]] +name = "syn" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52205623b1b0f064a4e71182c3b18ae902267282930c6d5462c91b859668426e" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c53f98874615aea268107765aa1ed8f6116782501d18e53d08b471733bea6c85" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8b463991b4eab2d801e724172285ec4195c650e8ec79b149e6c2a8e6dd3f783" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "toml" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +dependencies = [ + "serde", +] + +[[package]] +name = "unicode-ident" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22025f6d8eb903ebf920ea6933b70b1e495be37e2cb4099e62c80454aaf57c39" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] diff --git a/libs/portable/Cargo.toml b/libs/portable/Cargo.toml new file mode 100644 index 000000000..bdd7b23ca --- /dev/null +++ b/libs/portable/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "rustdesk-portable-packer" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +build = "build.rs" + +[build-dependencies] +embed-resource = "1.7" + +[dependencies] +brotli = "3.3.4" +dirs = "4.0.0" +md5 = "0.7.0" diff --git a/libs/portable/build.rs b/libs/portable/build.rs new file mode 100644 index 000000000..2fedd1d9c --- /dev/null +++ b/libs/portable/build.rs @@ -0,0 +1,5 @@ +extern crate embed_resource; + +fn main() { + embed_resource::compile("icon.rc"); +} diff --git a/libs/portable/generate.py b/libs/portable/generate.py new file mode 100644 index 000000000..640f2ae6a --- /dev/null +++ b/libs/portable/generate.py @@ -0,0 +1,88 @@ +from ast import parse +import os +import optparse +from hashlib import md5 +import brotli + +# file compress level(0-11) +compress_level = 11 +# 4GB maximum +length_count = 4 +# encoding +encoding = 'utf-8' + +# output: {path: (compressed_data, file_md5)} + + +def generate_md5_table(folder: str) -> dict: + res: dict = dict() + curdir = os.curdir + os.chdir(folder) + for root, _, files in os.walk('.'): + # remove ./ + for f in files: + md5_generator = md5() + full_path = os.path.join(root, f) + print(f"processing {full_path}...") + f = open(full_path, "rb") + content = f.read() + content_compressed = brotli.compress( + content, quality=compress_level) + md5_generator.update(content) + md5_code = md5_generator.hexdigest().encode(encoding=encoding) + res[full_path] = (content_compressed, md5_code) + os.chdir(curdir) + return res + + +def write_metadata(md5_table: dict, output_folder: str, exe: str): + output_path = os.path.join(output_folder, "data.bin") + with open(output_path, "wb") as f: + f.write("rustdesk".encode(encoding=encoding)) + for path in md5_table.keys(): + (compressed_data, md5_code) = md5_table[path] + data_length = len(compressed_data) + path = path.encode(encoding=encoding) + # path length & path + f.write((len(path)).to_bytes(length=length_count, byteorder='big')) + f.write(path) + # data length & compressed data + f.write((data_length).to_bytes( + length=length_count, byteorder='big')) + f.write(compressed_data) + # md5 code + f.write(md5_code) + # end + f.write("rustdesk".encode(encoding=encoding)) + # executable + f.write(exe.encode(encoding='utf-8')) + print(f"metadata had written to {output_path}") + + +def build_portable(output_folder: str): + os.chdir(output_folder) + os.system("cargo build --release") + +# Linux: python3 generate.py -f ../rustdesk-portable-packer/test -o . -e ./test/main.py +# Windows: python3 .\generate.py -f ..\rustdesk\flutter\build\windows\runner\Debug\ -o . -e ..\rustdesk\flutter\build\windows\runner\Debug\rustdesk.exe +if __name__ == '__main__': + parser = optparse.OptionParser() + parser.add_option("-f", "--folder", dest="folder", + help="folder to compress") + parser.add_option("-o", "--output", dest="output_folder", + help="the root of portable packer project") + parser.add_option("-e", "--executable", dest="executable", + help="specify startup file") + (options, args) = parser.parse_args() + folder = options.folder + output_folder = os.path.abspath(options.output_folder) + + exe: str = os.path.abspath(options.executable) + if not exe.startswith(os.path.abspath(folder)): + print("the executable must locate in source folder") + exit(-1) + exe = '.' + exe[len(os.path.abspath(folder)):] + print("executable path: " + exe) + md5_table = generate_md5_table(folder) + write_metadata(md5_table, output_folder, exe) + build_portable(output_folder) diff --git a/libs/portable/icon.rc b/libs/portable/icon.rc new file mode 100644 index 000000000..2f41e79d8 --- /dev/null +++ b/libs/portable/icon.rc @@ -0,0 +1 @@ +rustdesk_icon ICON "../../res/icon.ico" \ No newline at end of file diff --git a/libs/portable/requirements.txt b/libs/portable/requirements.txt new file mode 100644 index 000000000..ac6cebc82 --- /dev/null +++ b/libs/portable/requirements.txt @@ -0,0 +1 @@ +brotli \ No newline at end of file diff --git a/libs/portable/src/bin_reader.rs b/libs/portable/src/bin_reader.rs new file mode 100644 index 000000000..499c18e2c --- /dev/null +++ b/libs/portable/src/bin_reader.rs @@ -0,0 +1,134 @@ +use std::{ + fs::{self}, + io::{Cursor, Read}, + path::PathBuf, +}; + +const BIN_DATA: &[u8] = include_bytes!("../data.bin"); +// 4bytes +const LENGTH: usize = 4; +const IDENTIFIER_LENGTH: usize = 8; +const MD5_LENGTH: usize = 32; +const BUF_SIZE: usize = 4096; + +pub(crate) struct BinaryData { + pub md5_code: &'static [u8], + // compressed gzip data + pub raw: &'static [u8], + pub path: String, +} + +pub(crate) struct BinaryReader { + pub files: Vec, + pub exe: String, +} + +impl Default for BinaryReader { + fn default() -> Self { + let (files, exe) = BinaryReader::read(); + Self { files, exe } + } +} + +impl BinaryData { + fn decompress(&self) -> Vec { + let cursor = Cursor::new(self.raw); + let mut decoder = brotli::Decompressor::new(cursor, BUF_SIZE); + let mut buf = Vec::new(); + decoder.read_to_end(&mut buf).unwrap(); + buf + } + + pub fn write_to_file(&self, prefix: &PathBuf) { + let p = prefix.join(&self.path); + if let Some(parent) = p.parent() { + if !parent.exists() { + let _ = fs::create_dir_all(parent); + } + } + if p.exists() { + // check md5 + let f = fs::read(p.clone()).unwrap(); + let digest = format!("{:x}", md5::compute(&f)); + let md5_record = String::from_utf8_lossy(self.md5_code); + if digest == md5_record { + // same, skip this file + println!("skip {}", &self.path); + return; + } else { + println!("writing {}", p.display()); + println!("{} -> {}", md5_record, digest) + } + } + let _ = fs::write(p, self.decompress()); + } +} + +impl BinaryReader { + fn read() -> (Vec, String) { + let mut base: usize = 0; + let mut parsed = vec![]; + assert!(BIN_DATA.len() > IDENTIFIER_LENGTH, "bin data invalid!"); + let mut iden = String::from_utf8_lossy(&BIN_DATA[base..base + IDENTIFIER_LENGTH]); + if iden != "rustdesk" { + panic!("bin file is not vaild!"); + } + base += IDENTIFIER_LENGTH; + loop { + iden = String::from_utf8_lossy(&BIN_DATA[base..base + IDENTIFIER_LENGTH]); + if iden == "rustdesk" { + base += IDENTIFIER_LENGTH; + break; + } + // start reading + let mut offset = 0; + let path_length = u32::from_be_bytes([ + BIN_DATA[base + offset], + BIN_DATA[base + offset + 1], + BIN_DATA[base + offset + 2], + BIN_DATA[base + offset + 3], + ]) as usize; + offset += LENGTH; + let path = + String::from_utf8_lossy(&BIN_DATA[base + offset..base + offset + path_length]) + .to_string(); + offset += path_length; + // file sz + let file_length = u32::from_be_bytes([ + BIN_DATA[base + offset], + BIN_DATA[base + offset + 1], + BIN_DATA[base + offset + 2], + BIN_DATA[base + offset + 3], + ]) as usize; + offset += LENGTH; + let raw = &BIN_DATA[base + offset..base + offset + file_length]; + offset += file_length; + // md5 + let md5 = &BIN_DATA[base + offset..base + offset + MD5_LENGTH]; + offset += MD5_LENGTH; + parsed.push(BinaryData { + md5_code: md5, + raw: raw, + path: path, + }); + base += offset; + } + // executable + let executable = String::from_utf8_lossy(&BIN_DATA[base..]).to_string(); + (parsed, executable) + } + + #[cfg(unix)] + pub fn configure_permission(&self, prefix: &PathBuf) { + use std::os::unix::prelude::PermissionsExt; + + let exe_path = prefix.join(&self.exe); + if exe_path.exists() { + let f = File::open(exe_path).unwrap(); + let meta = f.metadata().unwrap(); + let mut permissions = meta.permissions(); + permissions.set_mode(0o755); + f.set_permissions(permissions).unwrap(); + } + } +} diff --git a/libs/portable/src/main.rs b/libs/portable/src/main.rs new file mode 100644 index 000000000..614c4c17c --- /dev/null +++ b/libs/portable/src/main.rs @@ -0,0 +1,51 @@ +#![windows_subsystem = "windows"] + +use std::{ + path::PathBuf, + process::{Command, Stdio}, +}; + +use bin_reader::BinaryReader; + +pub mod bin_reader; + +const APP_PREFIX: &str = "rustdesk"; +const APPNAME_RUNTIME_ENV_KEY: &str = "RUSTDESK_APPNAME"; + +fn setup(reader: BinaryReader) -> Option { + // home dir + if let Some(dir) = dirs::data_local_dir() { + let dir = dir.join(APP_PREFIX); + for file in reader.files.iter() { + file.write_to_file(&dir); + } + #[cfg(unix)] + reader.configure_permission(&dir); + Some(dir.join(&reader.exe)) + } else { + eprintln!("not found data local dir"); + None + } +} + +fn execute(path: PathBuf) { + println!("executing {}", path.display()); + // setup env + let exe = std::env::current_exe().unwrap(); + let exe_name = exe.file_name().unwrap(); + // run executable + Command::new(path) + .env(APPNAME_RUNTIME_ENV_KEY, exe_name) + .stdin(Stdio::inherit()) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .output() + .expect(&format!("failed to execute {:?}", exe_name)); +} + +fn main() { + let reader = BinaryReader::default(); + if let Some(exe) = setup(reader) { + execute(exe); + } +} From 9e6e842247070662cdeb208f4fe9f37c51c2ad6c Mon Sep 17 00:00:00 2001 From: csf Date: Mon, 19 Sep 2022 20:26:39 +0800 Subject: [PATCH 0543/2015] refactor: move peer_widget / peercard_widget / peer_tab_page & move connect new address_book class; add peer tab onPageChanged android settings_page.dart add dark mode opt peer_tab_page search bar, add mobile peer_tab support --- flutter/lib/common.dart | 26 + flutter/lib/common/widgets/address_book.dart | 415 ++++++++++++ flutter/lib/common/widgets/peer_tab_page.dart | 271 ++++++++ .../widgets/peer_widget.dart | 67 +- .../widgets/peercard_widget.dart | 53 +- flutter/lib/consts.dart | 4 + .../lib/desktop/pages/connection_page.dart | 638 +----------------- .../lib/desktop/widgets/remote_menubar.dart | 1 - flutter/lib/desktop/widgets/utils.dart | 28 - flutter/lib/mobile/pages/connection_page.dart | 274 +++----- flutter/lib/mobile/pages/settings_page.dart | 16 +- flutter/lib/models/ab_model.dart | 2 +- 12 files changed, 927 insertions(+), 868 deletions(-) create mode 100644 flutter/lib/common/widgets/address_book.dart create mode 100644 flutter/lib/common/widgets/peer_tab_page.dart rename flutter/lib/{desktop => common}/widgets/peer_widget.dart (80%) rename flutter/lib/{desktop => common}/widgets/peercard_widget.dart (94%) delete mode 100644 flutter/lib/desktop/widgets/utils.dart diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index c07764e32..70e10a9da 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1007,3 +1007,29 @@ Future restoreWindowPosition(WindowType type, {int? windowId}) async { } return false; } + +/// Connect to a peer with [id]. +/// If [isFileTransfer], starts a session only for file transfer. +/// If [isTcpTunneling], starts a session only for tcp tunneling. +/// If [isRDP], starts a session only for rdp. +void connect(BuildContext context, String id, + {bool isFileTransfer = false, + bool isTcpTunneling = false, + bool isRDP = false}) async { + if (id == '') return; + id = id.replaceAll(' ', ''); + assert(!(isFileTransfer && isTcpTunneling && isRDP), + "more than one connect type"); + + FocusScopeNode currentFocus = FocusScope.of(context); + if (isFileTransfer) { + await rustDeskWinManager.newFileTransfer(id); + } else if (isTcpTunneling || isRDP) { + await rustDeskWinManager.newPortForward(id, isRDP); + } else { + await rustDeskWinManager.newRemoteDesktop(id); + } + if (!currentFocus.hasPrimaryFocus) { + currentFocus.unfocus(); + } +} diff --git a/flutter/lib/common/widgets/address_book.dart b/flutter/lib/common/widgets/address_book.dart new file mode 100644 index 000000000..beecf47f2 --- /dev/null +++ b/flutter/lib/common/widgets/address_book.dart @@ -0,0 +1,415 @@ +import 'package:contextmenu/contextmenu.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common/widgets/peer_widget.dart'; +import 'package:flutter_hbb/models/ab_model.dart'; +import 'package:get/get.dart'; +import 'package:provider/provider.dart'; + +import '../../common.dart'; +import '../../desktop/pages/desktop_home_page.dart'; +import '../../models/platform_model.dart'; + +class AddressBook extends StatefulWidget { + const AddressBook({Key? key}) : super(key: key); + + @override + State createState() { + return _AddressBookState(); + } +} + +class _AddressBookState extends State { + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) => gFFI.abModel.getAb()); + } + + @override + Widget build(BuildContext context) => FutureBuilder( + future: buildAddressBook(context), + builder: (context, snapshot) { + if (snapshot.hasData) { + return snapshot.data!; + } else { + return const Offstage(); + } + }); + + handleLogin() { + loginDialog().then((success) { + if (success) { + setState(() {}); + } + }); + } + + Future buildAddressBook(BuildContext context) async { + final token = await bind.mainGetLocalOption(key: 'access_token'); + if (token.trim().isEmpty) { + return Center( + child: InkWell( + onTap: handleLogin, + child: Text( + translate("Login"), + style: const TextStyle(decoration: TextDecoration.underline), + ), + ), + ); + } + final model = gFFI.abModel; + return FutureBuilder( + future: model.getAb(), + builder: (context, snapshot) { + if (snapshot.hasData) { + return _buildAddressBook(context); + } else if (snapshot.hasError) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(translate("${snapshot.error}")), + TextButton( + onPressed: () { + setState(() {}); + }, + child: Text(translate("Retry"))) + ], + ); + } else { + if (model.abLoading) { + return const Center( + child: CircularProgressIndicator(), + ); + } else if (model.abError.isNotEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(translate(model.abError)), + TextButton( + onPressed: () { + setState(() {}); + }, + child: Text(translate("Retry"))) + ], + ), + ); + } else { + return const Offstage(); + } + } + }); + } + + Widget _buildAddressBook(BuildContext context) { + return Consumer( + builder: (context, model, child) => Row( + children: [ + Card( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + side: const BorderSide(color: MyTheme.grayBg)), + child: Container( + width: 200, + height: double.infinity, + padding: const EdgeInsets.symmetric( + horizontal: 12.0, vertical: 8.0), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(translate('Tags')), + InkWell( + child: PopupMenuButton( + itemBuilder: (context) => [ + PopupMenuItem( + value: 'add-id', + child: Text(translate("Add ID")), + ), + PopupMenuItem( + value: 'add-tag', + child: Text(translate("Add Tag")), + ), + PopupMenuItem( + value: 'unset-all-tag', + child: Text( + translate("Unselect all tags")), + ), + ], + onSelected: handleAbOp, + child: const Icon(Icons.more_vert_outlined)), + ) + ], + ), + Expanded( + child: Container( + width: double.infinity, + height: double.infinity, + decoration: BoxDecoration( + border: Border.all(color: MyTheme.darkGray)), + child: Obx( + () => Wrap( + children: gFFI.abModel.tags + .map((e) => + buildTag(e, gFFI.abModel.selectedTags, + onTap: () { + // + if (gFFI.abModel.selectedTags + .contains(e)) { + gFFI.abModel.selectedTags.remove(e); + } else { + gFFI.abModel.selectedTags.add(e); + } + })) + .toList(), + ), + ), + ).marginSymmetric(vertical: 8.0), + ) + ], + ), + ), + ).marginOnly(right: 8.0), + Expanded( + child: Align( + alignment: Alignment.topLeft, + child: AddressBookPeerWidget()), + ) + ], + )); + } + + Widget buildTag(String tagName, RxList rxTags, {Function()? onTap}) { + return ContextMenuArea( + width: 100, + builder: (context) => [ + ListTile( + title: Text(translate("Delete")), + onTap: () { + gFFI.abModel.deleteTag(tagName); + gFFI.abModel.updateAb(); + Future.delayed(Duration.zero, () => Get.back()); + }, + ) + ], + child: GestureDetector( + onTap: onTap, + child: Obx( + () => Container( + decoration: BoxDecoration( + color: rxTags.contains(tagName) ? Colors.blue : null, + border: Border.all(color: MyTheme.darkGray), + borderRadius: BorderRadius.circular(10)), + margin: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 8.0), + padding: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 8.0), + child: Text( + tagName, + style: TextStyle( + color: rxTags.contains(tagName) ? MyTheme.white : null), + ), + ), + ), + ), + ); + } + + /// tag operation + void handleAbOp(String value) { + if (value == 'add-id') { + abAddId(); + } else if (value == 'add-tag') { + abAddTag(); + } else if (value == 'unset-all-tag') { + gFFI.abModel.unsetSelectedTags(); + } + } + + void abAddId() async { + var field = ""; + var msg = ""; + var isInProgress = false; + TextEditingController controller = TextEditingController(text: field); + + gFFI.dialogManager.show((setState, close) { + submit() async { + setState(() { + msg = ""; + isInProgress = true; + }); + field = controller.text.trim(); + if (field.isEmpty) { + // pass + } else { + final ids = field.trim().split(RegExp(r"[\s,;\n]+")); + field = ids.join(','); + for (final newId in ids) { + if (gFFI.abModel.idContainBy(newId)) { + continue; + } + gFFI.abModel.addId(newId); + } + await gFFI.abModel.updateAb(); + this.setState(() {}); + // final currentPeers + } + close(); + } + + return CustomAlertDialog( + title: Text(translate("Add ID")), + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(translate("whitelist_sep")), + const SizedBox( + height: 8.0, + ), + Row( + children: [ + Expanded( + child: TextField( + maxLines: null, + decoration: InputDecoration( + border: const OutlineInputBorder(), + errorText: msg.isEmpty ? null : translate(msg), + ), + controller: controller, + focusNode: FocusNode()..requestFocus()), + ), + ], + ), + const SizedBox( + height: 4.0, + ), + Offstage( + offstage: !isInProgress, child: const LinearProgressIndicator()) + ], + ), + actions: [ + TextButton(onPressed: close, child: Text(translate("Cancel"))), + TextButton(onPressed: submit, child: Text(translate("OK"))), + ], + onSubmit: submit, + onCancel: close, + ); + }); + } + + void abAddTag() async { + var field = ""; + var msg = ""; + var isInProgress = false; + TextEditingController controller = TextEditingController(text: field); + gFFI.dialogManager.show((setState, close) { + submit() async { + setState(() { + msg = ""; + isInProgress = true; + }); + field = controller.text.trim(); + if (field.isEmpty) { + // pass + } else { + final tags = field.trim().split(RegExp(r"[\s,;\n]+")); + field = tags.join(','); + for (final tag in tags) { + gFFI.abModel.addTag(tag); + } + await gFFI.abModel.updateAb(); + // final currentPeers + } + close(); + } + + return CustomAlertDialog( + title: Text(translate("Add Tag")), + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(translate("whitelist_sep")), + const SizedBox( + height: 8.0, + ), + Row( + children: [ + Expanded( + child: TextField( + maxLines: null, + decoration: InputDecoration( + border: const OutlineInputBorder(), + errorText: msg.isEmpty ? null : translate(msg), + ), + controller: controller, + focusNode: FocusNode()..requestFocus(), + ), + ), + ], + ), + const SizedBox( + height: 4.0, + ), + Offstage( + offstage: !isInProgress, child: const LinearProgressIndicator()) + ], + ), + actions: [ + TextButton(onPressed: close, child: Text(translate("Cancel"))), + TextButton(onPressed: submit, child: Text(translate("OK"))), + ], + onSubmit: submit, + onCancel: close, + ); + }); + } + + void abEditTag(String id) { + var isInProgress = false; + + final tags = List.of(gFFI.abModel.tags); + var selectedTag = gFFI.abModel.getPeerTags(id).obs; + + gFFI.dialogManager.show((setState, close) { + submit() async { + setState(() { + isInProgress = true; + }); + gFFI.abModel.changeTagForPeer(id, selectedTag); + await gFFI.abModel.updateAb(); + close(); + } + + return CustomAlertDialog( + title: Text(translate("Edit Tag")), + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: + const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + child: Wrap( + children: tags + .map((e) => buildTag(e, selectedTag, onTap: () { + if (selectedTag.contains(e)) { + selectedTag.remove(e); + } else { + selectedTag.add(e); + } + })) + .toList(growable: false), + ), + ), + Offstage( + offstage: !isInProgress, child: const LinearProgressIndicator()) + ], + ), + actions: [ + TextButton(onPressed: close, child: Text(translate("Cancel"))), + TextButton(onPressed: submit, child: Text(translate("OK"))), + ], + onSubmit: submit, + onCancel: close, + ); + }); + } +} diff --git a/flutter/lib/common/widgets/peer_tab_page.dart b/flutter/lib/common/widgets/peer_tab_page.dart new file mode 100644 index 000000000..fbda91649 --- /dev/null +++ b/flutter/lib/common/widgets/peer_tab_page.dart @@ -0,0 +1,271 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common/widgets/peer_widget.dart'; +import 'package:flutter_hbb/common/widgets/peercard_widget.dart'; +import 'package:flutter_hbb/consts.dart'; +import 'package:get/get.dart'; + +import '../../common.dart'; +import '../../models/platform_model.dart'; + +class PeerTabPage extends StatefulWidget { + final List tabs; + final List children; + const PeerTabPage({required this.tabs, required this.children, Key? key}) + : super(key: key); + @override + State createState() => _PeerTabPageState(); +} + +class _PeerTabPageState extends State + with SingleTickerProviderStateMixin { + late final PageController _pageController = PageController(); + final RxInt _tabIndex = 0.obs; + + @override + void initState() { + super.initState(); + } + + // hard code for now + void _handleTabSelection(int index) { + // reset search text + peerSearchText.value = ""; + peerSearchTextController.clear(); + _tabIndex.value = index; + _pageController.jumpToPage(index); + switch (index) { + case 0: + bind.mainLoadRecentPeers(); + break; + case 1: + bind.mainLoadFavPeers(); + break; + case 2: + bind.mainDiscover(); + break; + case 3: + gFFI.abModel.getAb(); + break; + } + } + + @override + void dispose() { + _pageController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + textBaseline: TextBaseline.ideographic, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: 28, + child: Container( + constraints: isDesktop ? null : kMobilePageConstraints, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded(child: _createTabBar(context)), + const SizedBox(width: 10), + const PeerSearchBar(), + Offstage( + offstage: !isDesktop, + child: _createPeerViewTypeSwitch(context) + .marginOnly(left: 13)), + ], + )), + ), + _createTabBarView(), + ], + ); + } + + Widget _createTabBar(BuildContext context) { + return ListView( + scrollDirection: Axis.horizontal, + shrinkWrap: true, + controller: ScrollController(), + children: super.widget.tabs.asMap().entries.map((t) { + return Obx(() => GestureDetector( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8), + decoration: BoxDecoration( + color: _tabIndex.value == t.key + ? MyTheme.color(context).bg + : null, + borderRadius: BorderRadius.circular(2), + ), + child: Align( + alignment: Alignment.center, + child: Text( + t.value, + textAlign: TextAlign.center, + style: TextStyle( + height: 1, + fontSize: 14, + color: _tabIndex.value == t.key + ? MyTheme.color(context).text + : MyTheme.color(context).lightText), + ), + )), + onTap: () => _handleTabSelection(t.key), + )); + }).toList()); + } + + Widget _createTabBarView() { + final verticalMargin = isDesktop ? 12.0 : 6.0; + return Expanded( + child: PageView( + physics: const BouncingScrollPhysics(), + controller: _pageController, + children: super.widget.children, + onPageChanged: (to) => _tabIndex.value = to) + .marginSymmetric(vertical: verticalMargin)); + } + + Widget _createPeerViewTypeSwitch(BuildContext context) { + final activeDeco = BoxDecoration(color: MyTheme.color(context).bg); + return Row( + children: [ + Obx( + () => Container( + padding: const EdgeInsets.all(4.0), + decoration: + peerCardUiType.value == PeerUiType.grid ? activeDeco : null, + child: InkWell( + onTap: () { + peerCardUiType.value = PeerUiType.grid; + }, + child: Icon( + Icons.grid_view_rounded, + size: 18, + color: peerCardUiType.value == PeerUiType.grid + ? MyTheme.color(context).text + : MyTheme.color(context).lightText, + )), + ), + ), + Obx( + () => Container( + padding: const EdgeInsets.all(4.0), + decoration: + peerCardUiType.value == PeerUiType.list ? activeDeco : null, + child: InkWell( + onTap: () { + peerCardUiType.value = PeerUiType.list; + }, + child: Icon( + Icons.list, + size: 18, + color: peerCardUiType.value == PeerUiType.list + ? MyTheme.color(context).text + : MyTheme.color(context).lightText, + )), + ), + ), + ], + ); + } +} + +class PeerSearchBar extends StatefulWidget { + const PeerSearchBar({Key? key}) : super(key: key); + + @override + State createState() => _PeerSearchBarState(); +} + +class _PeerSearchBarState extends State { + var drawer = false; + + @override + Widget build(BuildContext context) { + return drawer + ? _buildSearchBar() + : IconButton( + alignment: Alignment.centerRight, + padding: const EdgeInsets.only(right: 2), + onPressed: () { + setState(() { + drawer = true; + }); + }, + icon: const Icon( + Icons.search_rounded, + color: MyTheme.dark, + )); + } + + Widget _buildSearchBar() { + RxBool focused = false.obs; + FocusNode focusNode = FocusNode(); + focusNode.addListener(() => focused.value = focusNode.hasFocus); + return Container( + width: 120, + decoration: BoxDecoration( + color: MyTheme.color(context).bg, + borderRadius: BorderRadius.circular(6), + ), + child: Obx(() => Row( + children: [ + Expanded( + child: Row( + children: [ + Icon( + Icons.search_rounded, + color: MyTheme.color(context).placeholder, + ).marginSymmetric(horizontal: 4), + Expanded( + child: TextField( + autofocus: true, + controller: peerSearchTextController, + onChanged: (searchText) { + peerSearchText.value = searchText; + }, + focusNode: focusNode, + textAlign: TextAlign.start, + maxLines: 1, + cursorColor: MyTheme.color(context).lightText, + cursorHeight: 18, + cursorWidth: 1, + style: const TextStyle(fontSize: 14), + decoration: InputDecoration( + contentPadding: + const EdgeInsets.symmetric(vertical: 6), + hintText: + focused.value ? null : translate("Search ID"), + hintStyle: TextStyle( + fontSize: 14, + color: MyTheme.color(context).placeholder), + border: InputBorder.none, + isDense: true, + ), + ), + ), + // Icon(Icons.close), + IconButton( + alignment: Alignment.centerRight, + padding: const EdgeInsets.only(right: 2), + onPressed: () { + setState(() { + peerSearchTextController.clear(); + peerSearchText.value = ""; + drawer = false; + }); + }, + icon: const Icon( + Icons.close, + color: MyTheme.dark, + )), + ], + ), + ) + ], + )), + ); + } +} diff --git a/flutter/lib/desktop/widgets/peer_widget.dart b/flutter/lib/common/widgets/peer_widget.dart similarity index 80% rename from flutter/lib/desktop/widgets/peer_widget.dart rename to flutter/lib/common/widgets/peer_widget.dart index f137241a9..9ae1a6340 100644 --- a/flutter/lib/desktop/widgets/peer_widget.dart +++ b/flutter/lib/common/widgets/peer_widget.dart @@ -39,7 +39,7 @@ class _PeerWidget extends StatefulWidget { /// State for the peer widget. class _PeerWidgetState extends State<_PeerWidget> with WindowListener { static const int _maxQueryCount = 3; - + final space = isDesktop ? 12.0 : 8.0; final _curPeers = {}; var _lastChangeTime = DateTime.now(); var _lastQueryPeers = {}; @@ -47,6 +47,17 @@ class _PeerWidgetState extends State<_PeerWidget> with WindowListener { var _queryCoun = 0; var _exit = false; + late final mobileWidth = () { + const minWidth = 320.0; + final windowWidth = MediaQuery.of(context).size.width; + var width = windowWidth - 2 * space; + if (windowWidth > minWidth + 2 * space) { + final n = (windowWidth / (minWidth + 2 * space)).floor(); + width = windowWidth / n - 2 * space; + } + return width; + }(); + _PeerWidgetState() { _startCheckOnlines(); } @@ -76,7 +87,6 @@ class _PeerWidgetState extends State<_PeerWidget> with WindowListener { @override Widget build(BuildContext context) { - const space = 12.0; return ChangeNotifierProvider( create: (context) => widget.peers, child: Consumer( @@ -93,32 +103,36 @@ class _PeerWidgetState extends State<_PeerWidget> with WindowListener { final peers = snapshot.data!; final cards = []; for (final peer in peers) { + final visibilityChild = VisibilityDetector( + key: ValueKey(peer.id), + onVisibilityChanged: (info) { + final peerId = (info.key as ValueKey).value; + if (info.visibleFraction > 0.00001) { + _curPeers.add(peerId); + } else { + _curPeers.remove(peerId); + } + _lastChangeTime = DateTime.now(); + }, + child: widget.peerCardWidgetFunc(peer), + ); cards.add(Offstage( key: ValueKey("off${peer.id}"), offstage: widget.offstageFunc(peer), - child: Obx( - () => SizedBox( - width: 220, - height: - peerCardUiType.value == PeerUiType.grid - ? 140 - : 42, - child: VisibilityDetector( - key: ValueKey(peer.id), - onVisibilityChanged: (info) { - final peerId = - (info.key as ValueKey).value; - if (info.visibleFraction > 0.00001) { - _curPeers.add(peerId); - } else { - _curPeers.remove(peerId); - } - _lastChangeTime = DateTime.now(); - }, - child: widget.peerCardWidgetFunc(peer), - ), - ), - ))); + child: isDesktop + ? Obx( + () => SizedBox( + width: 220, + height: peerCardUiType.value == + PeerUiType.grid + ? 140 + : 42, + child: visibilityChild, + ), + ) + : SizedBox( + width: mobileWidth, + child: visibilityChild))); } return Wrap( spacing: space, runSpacing: space, children: cards); @@ -273,6 +287,7 @@ class AddressBookPeerWidget extends BasePeerWidget { ); static List _loadPeers() { + debugPrint("_loadPeers : ${gFFI.abModel.peers.toString()}"); return gFFI.abModel.peers.map((e) { return Peer.fromJson(e['id'], e); }).toList(); @@ -296,7 +311,7 @@ class AddressBookPeerWidget extends BasePeerWidget { @override Widget build(BuildContext context) { final widget = super.build(context); - gFFI.abModel.updateAb(); + // gFFI.abModel.updateAb(); return widget; } } diff --git a/flutter/lib/desktop/widgets/peercard_widget.dart b/flutter/lib/common/widgets/peercard_widget.dart similarity index 94% rename from flutter/lib/desktop/widgets/peercard_widget.dart rename to flutter/lib/common/widgets/peercard_widget.dart index fc93c59c6..2cb451974 100644 --- a/flutter/lib/desktop/widgets/peercard_widget.dart +++ b/flutter/lib/common/widgets/peercard_widget.dart @@ -8,9 +8,8 @@ import '../../common/formatter/id_formatter.dart'; import '../../models/model.dart'; import '../../models/peer_model.dart'; import '../../models/platform_model.dart'; -import './material_mod_popup_menu.dart' as mod_menu; -import './popup_menu.dart'; -import './utils.dart'; +import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu; +import '../../desktop/widgets/popup_menu.dart'; class _PopupMenuTheme { static const Color commonColor = MyTheme.accent; @@ -55,6 +54,50 @@ class _PeerCardState extends State<_PeerCard> @override Widget build(BuildContext context) { super.build(context); + if (isDesktop) { + return _buildDesktop(); + } else { + return _buildMobile(); + } + } + + Widget _buildMobile() { + final peer = super.widget.peer; + return Card( + margin: EdgeInsets.zero, + child: GestureDetector( + onTap: !isWebDesktop ? () => connect(context, peer.id) : null, + onDoubleTap: isWebDesktop ? () => connect(context, peer.id) : null, + onLongPressStart: (details) { + final x = details.globalPosition.dx; + final y = details.globalPosition.dy; + _menuPos = RelativeRect.fromLTRB(x, y, x, y); + _showPeerMenu(peer.id); + }, + child: ListTile( + contentPadding: const EdgeInsets.only(left: 12), + subtitle: Text('${peer.username}@${peer.hostname}'), + title: Text(peer.id), + leading: Container( + padding: const EdgeInsets.all(6), + color: str2color('${peer.id}${peer.platform}', 0x7f), + child: getPlatformImage(peer.platform)), + trailing: InkWell( + child: const Padding( + padding: EdgeInsets.all(12), + child: Icon(Icons.more_vert)), + onTapDown: (e) { + final x = e.globalPosition.dx; + final y = e.globalPosition.dy; + _menuPos = RelativeRect.fromLTRB(x, y, x, y); + }, + onTap: () { + _showPeerMenu(peer.id); + }), + ))); + } + + Widget _buildDesktop() { final peer = super.widget.peer; var deco = Rx(BoxDecoration( border: Border.all(color: Colors.transparent, width: _borderWidth), @@ -264,7 +307,7 @@ class _PeerCardState extends State<_PeerCard> final y = e.position.dy; _menuPos = RelativeRect.fromLTRB(x, y, x, y); }, - onPointerUp: (_) => _showPeerMenu(context, peer.id), + onPointerUp: (_) => _showPeerMenu(peer.id), child: MouseRegion( onEnter: (_) => _iconMoreHover.value = true, onExit: (_) => _iconMoreHover.value = false, @@ -281,7 +324,7 @@ class _PeerCardState extends State<_PeerCard> /// Show the peer menu and handle user's choice. /// User might remove the peer or send a file to the peer. - void _showPeerMenu(BuildContext context, String id) async { + void _showPeerMenu(String id) async { await mod_menu.showMenu( context: context, position: _menuPos, diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 8986f05ec..4f710ea2c 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -1,3 +1,5 @@ +import 'package:flutter/material.dart'; + const double kDesktopRemoteTabBarHeight = 28.0; /// [kAppTypeMain] used by 'Desktop Main Page' , 'Mobile (Client and Server)' , 'Desktop CM Page' @@ -17,6 +19,8 @@ const int kDesktopDefaultDisplayHeight = 720; const kInvalidValueStr = "InvalidValueStr"; +const kMobilePageConstraints = BoxConstraints(maxWidth: 600); + /// flutter/packages/flutter/lib/src/services/keyboard_key.dart -> _keyLabels /// see [LogicalKeyboardKey.keyLabel] const Map logicalKeyMap = { diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 9024c996f..faf798f0d 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -1,17 +1,15 @@ import 'dart:async'; import 'dart:convert'; -import 'package:contextmenu/contextmenu.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart'; -import 'package:flutter_hbb/desktop/widgets/peer_widget.dart'; -import 'package:flutter_hbb/desktop/widgets/peercard_widget.dart'; -import 'package:flutter_hbb/utils/multi_window_manager.dart'; +import 'package:flutter_hbb/common/widgets/address_book.dart'; import 'package:get/get.dart'; import 'package:url_launcher/url_launcher_string.dart'; import '../../common.dart'; import '../../common/formatter/id_formatter.dart'; +import '../../common/widgets/peer_tab_page.dart'; +import '../../common/widgets/peer_widget.dart'; import '../../models/platform_model.dart'; /// Connection page for connecting to a remote peer. @@ -66,7 +64,7 @@ class _ConnectionPageState extends State { SizedBox(height: 12), Divider(), Expanded( - child: _PeerTabbedPage( + child: PeerTabPage( tabs: [ translate('Recent Sessions'), translate('Favorites'), @@ -77,15 +75,7 @@ class _ConnectionPageState extends State { RecentPeerWidget(), FavoritePeerWidget(), DiscoveredPeerWidget(), - FutureBuilder( - future: buildAddressBook(context), - builder: (context, snapshot) { - if (snapshot.hasData) { - return snapshot.data!; - } else { - return const Offstage(); - } - }), + const AddressBook(), ], )), ], @@ -102,23 +92,7 @@ class _ConnectionPageState extends State { /// Connects to the selected peer. void onConnect({bool isFileTransfer = false}) { final id = _idController.id; - connect(id, isFileTransfer: isFileTransfer); - } - - /// Connect to a peer with [id]. - /// If [isFileTransfer], starts a session only for file transfer. - void connect(String id, {bool isFileTransfer = false}) async { - if (id == '') return; - id = id.replaceAll(' ', ''); - if (isFileTransfer) { - await rustDeskWinManager.newFileTransfer(id); - } else { - await rustDeskWinManager.newRemoteDesktop(id); - } - FocusScopeNode currentFocus = FocusScope.of(context); - if (!currentFocus.hasPrimaryFocus) { - currentFocus.unfocus(); - } + connect(context, id, isFileTransfer: isFileTransfer); } /// UI for the search bar. @@ -372,604 +346,4 @@ class _ConnectionPageState extends State { svcStatusCode.value = status["status_num"]; svcIsUsingPublicServer.value = await bind.mainIsUsingPublicServer(); } - - handleLogin() { - loginDialog().then((success) { - if (success) { - setState(() {}); - } - }); - } - - Future buildAddressBook(BuildContext context) async { - final token = await bind.mainGetLocalOption(key: 'access_token'); - if (token.trim().isEmpty) { - return Center( - child: InkWell( - onTap: handleLogin, - child: Text( - translate("Login"), - style: TextStyle(decoration: TextDecoration.underline), - ), - ), - ); - } - final model = gFFI.abModel; - return FutureBuilder( - future: model.getAb(), - builder: (context, snapshot) { - if (snapshot.hasData) { - return _buildAddressBook(context); - } else if (snapshot.hasError) { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text(translate("${snapshot.error}")), - TextButton( - onPressed: () { - setState(() {}); - }, - child: Text(translate("Retry"))) - ], - ); - } else { - if (model.abLoading) { - return const Center( - child: CircularProgressIndicator(), - ); - } else if (model.abError.isNotEmpty) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text(translate("${model.abError}")), - TextButton( - onPressed: () { - setState(() {}); - }, - child: Text(translate("Retry"))) - ], - ), - ); - } else { - return Offstage(); - } - } - }); - } - - Widget _buildAddressBook(BuildContext context) { - return Row( - children: [ - Card( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - side: BorderSide(color: MyTheme.grayBg)), - child: Container( - width: 200, - height: double.infinity, - padding: EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(translate('Tags')), - InkWell( - child: PopupMenuButton( - itemBuilder: (context) => [ - PopupMenuItem( - child: Text(translate("Add ID")), - value: 'add-id', - ), - PopupMenuItem( - child: Text(translate("Add Tag")), - value: 'add-tag', - ), - PopupMenuItem( - child: Text(translate("Unselect all tags")), - value: 'unset-all-tag', - ), - ], - onSelected: handleAbOp, - child: Icon(Icons.more_vert_outlined)), - ) - ], - ), - Expanded( - child: Container( - width: double.infinity, - height: double.infinity, - decoration: BoxDecoration( - border: Border.all(color: MyTheme.darkGray)), - child: Obx( - () => Wrap( - children: gFFI.abModel.tags - .map((e) => buildTag(e, gFFI.abModel.selectedTags, - onTap: () { - // - if (gFFI.abModel.selectedTags.contains(e)) { - gFFI.abModel.selectedTags.remove(e); - } else { - gFFI.abModel.selectedTags.add(e); - } - })) - .toList(), - ), - ), - ).marginSymmetric(vertical: 8.0), - ) - ], - ), - ), - ).marginOnly(right: 8.0), - Expanded( - child: Align( - alignment: Alignment.topLeft, child: AddressBookPeerWidget()), - ) - ], - ); - } - - Widget buildTag(String tagName, RxList rxTags, {Function()? onTap}) { - return ContextMenuArea( - width: 100, - builder: (context) => [ - ListTile( - title: Text(translate("Delete")), - onTap: () { - gFFI.abModel.deleteTag(tagName); - gFFI.abModel.updateAb(); - Future.delayed(Duration.zero, () => Get.back()); - }, - ) - ], - child: GestureDetector( - onTap: onTap, - child: Obx( - () => Container( - decoration: BoxDecoration( - color: rxTags.contains(tagName) ? Colors.blue : null, - border: Border.all(color: MyTheme.darkGray), - borderRadius: BorderRadius.circular(10)), - margin: EdgeInsets.symmetric(horizontal: 4.0, vertical: 8.0), - padding: EdgeInsets.symmetric(vertical: 2.0, horizontal: 8.0), - child: Text( - tagName, - style: TextStyle( - color: rxTags.contains(tagName) ? MyTheme.white : null), - ), - ), - ), - ), - ); - } - - /// tag operation - void handleAbOp(String value) { - if (value == 'add-id') { - abAddId(); - } else if (value == 'add-tag') { - abAddTag(); - } else if (value == 'unset-all-tag') { - gFFI.abModel.unsetSelectedTags(); - } - } - - void abAddId() async { - var field = ""; - var msg = ""; - var isInProgress = false; - TextEditingController controller = TextEditingController(text: field); - - gFFI.dialogManager.show((setState, close) { - submit() async { - setState(() { - msg = ""; - isInProgress = true; - }); - field = controller.text.trim(); - if (field.isEmpty) { - // pass - } else { - final ids = field.trim().split(RegExp(r"[\s,;\n]+")); - field = ids.join(','); - for (final newId in ids) { - if (gFFI.abModel.idContainBy(newId)) { - continue; - } - gFFI.abModel.addId(newId); - } - await gFFI.abModel.updateAb(); - this.setState(() {}); - // final currentPeers - } - close(); - } - - return CustomAlertDialog( - title: Text(translate("Add ID")), - content: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(translate("whitelist_sep")), - const SizedBox( - height: 8.0, - ), - Row( - children: [ - Expanded( - child: TextField( - maxLines: null, - decoration: InputDecoration( - border: const OutlineInputBorder(), - errorText: msg.isEmpty ? null : translate(msg), - ), - controller: controller, - focusNode: FocusNode()..requestFocus()), - ), - ], - ), - const SizedBox( - height: 4.0, - ), - Offstage( - offstage: !isInProgress, child: const LinearProgressIndicator()) - ], - ), - actions: [ - TextButton(onPressed: close, child: Text(translate("Cancel"))), - TextButton(onPressed: submit, child: Text(translate("OK"))), - ], - onSubmit: submit, - onCancel: close, - ); - }); - } - - void abAddTag() async { - var field = ""; - var msg = ""; - var isInProgress = false; - TextEditingController controller = TextEditingController(text: field); - gFFI.dialogManager.show((setState, close) { - submit() async { - setState(() { - msg = ""; - isInProgress = true; - }); - field = controller.text.trim(); - if (field.isEmpty) { - // pass - } else { - final tags = field.trim().split(RegExp(r"[\s,;\n]+")); - field = tags.join(','); - for (final tag in tags) { - gFFI.abModel.addTag(tag); - } - await gFFI.abModel.updateAb(); - // final currentPeers - } - close(); - } - - return CustomAlertDialog( - title: Text(translate("Add Tag")), - content: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(translate("whitelist_sep")), - const SizedBox( - height: 8.0, - ), - Row( - children: [ - Expanded( - child: TextField( - maxLines: null, - decoration: InputDecoration( - border: const OutlineInputBorder(), - errorText: msg.isEmpty ? null : translate(msg), - ), - controller: controller, - focusNode: FocusNode()..requestFocus(), - ), - ), - ], - ), - const SizedBox( - height: 4.0, - ), - Offstage( - offstage: !isInProgress, child: const LinearProgressIndicator()) - ], - ), - actions: [ - TextButton(onPressed: close, child: Text(translate("Cancel"))), - TextButton(onPressed: submit, child: Text(translate("OK"))), - ], - onSubmit: submit, - onCancel: close, - ); - }); - } - - void abEditTag(String id) { - var isInProgress = false; - - final tags = List.of(gFFI.abModel.tags); - var selectedTag = gFFI.abModel.getPeerTags(id).obs; - - gFFI.dialogManager.show((setState, close) { - submit() async { - setState(() { - isInProgress = true; - }); - gFFI.abModel.changeTagForPeer(id, selectedTag); - await gFFI.abModel.updateAb(); - close(); - } - - return CustomAlertDialog( - title: Text(translate("Edit Tag")), - content: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - padding: - const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), - child: Wrap( - children: tags - .map((e) => buildTag(e, selectedTag, onTap: () { - if (selectedTag.contains(e)) { - selectedTag.remove(e); - } else { - selectedTag.add(e); - } - })) - .toList(growable: false), - ), - ), - Offstage( - offstage: !isInProgress, child: const LinearProgressIndicator()) - ], - ), - actions: [ - TextButton(onPressed: close, child: Text(translate("Cancel"))), - TextButton(onPressed: submit, child: Text(translate("OK"))), - ], - onSubmit: submit, - onCancel: close, - ); - }); - } -} - -class _PeerTabbedPage extends StatefulWidget { - final List tabs; - final List children; - const _PeerTabbedPage({required this.tabs, required this.children, Key? key}) - : super(key: key); - @override - _PeerTabbedPageState createState() => _PeerTabbedPageState(); -} - -class _PeerTabbedPageState extends State<_PeerTabbedPage> - with SingleTickerProviderStateMixin { - late PageController _pageController = PageController(); - RxInt _tabIndex = 0.obs; - - @override - void initState() { - super.initState(); - } - - // hard code for now - void _handleTabSelection(int index) { - // reset search text - peerSearchText.value = ""; - peerSearchTextController.clear(); - _tabIndex.value = index; - _pageController.jumpToPage(index); - switch (index) { - case 0: - bind.mainLoadRecentPeers(); - break; - case 1: - bind.mainLoadFavPeers(); - break; - case 2: - bind.mainDiscover(); - break; - case 3: - gFFI.abModel.updateAb(); - break; - } - } - - @override - void dispose() { - _pageController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Column( - textBaseline: TextBaseline.ideographic, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - height: 28, - child: Row( - children: [ - Expanded(child: _createTabBar(context)), - _createSearchBar(context), - _createPeerViewTypeSwitch(context), - ], - ), - ), - _createTabBarView(), - ], - ); - } - - Widget _createTabBar(BuildContext context) { - return ListView( - scrollDirection: Axis.horizontal, - shrinkWrap: true, - controller: ScrollController(), - children: super.widget.tabs.asMap().entries.map((t) { - return Obx(() => GestureDetector( - child: Container( - padding: EdgeInsets.symmetric(horizontal: 8), - decoration: BoxDecoration( - color: _tabIndex.value == t.key - ? MyTheme.color(context).bg - : null, - borderRadius: BorderRadius.circular(2), - ), - child: Align( - alignment: Alignment.center, - child: Text( - t.value, - textAlign: TextAlign.center, - style: TextStyle( - height: 1, - fontSize: 14, - color: _tabIndex.value == t.key - ? MyTheme.color(context).text - : MyTheme.color(context).lightText), - ), - )), - onTap: () => _handleTabSelection(t.key), - )); - }).toList()); - } - - Widget _createTabBarView() { - return Expanded( - child: PageView( - controller: _pageController, children: super.widget.children) - .marginSymmetric(vertical: 12)); - } - - _createSearchBar(BuildContext context) { - RxBool focused = false.obs; - FocusNode focusNode = FocusNode(); - focusNode.addListener(() => focused.value = focusNode.hasFocus); - RxBool rowHover = false.obs; - RxBool clearHover = false.obs; - return Container( - width: 120, - height: 25, - margin: EdgeInsets.only(right: 13), - decoration: BoxDecoration(color: MyTheme.color(context).bg), - child: Obx(() => Row( - children: [ - Expanded( - child: MouseRegion( - onEnter: (_) => rowHover.value = true, - onExit: (_) => rowHover.value = false, - child: Row( - children: [ - Icon( - IconFont.search, - size: 16, - color: MyTheme.color(context).placeholder, - ).marginSymmetric(horizontal: 4), - Expanded( - child: TextField( - controller: peerSearchTextController, - onChanged: (searchText) { - peerSearchText.value = searchText; - }, - focusNode: focusNode, - textAlign: TextAlign.start, - maxLines: 1, - cursorColor: MyTheme.color(context).lightText, - cursorHeight: 18, - cursorWidth: 1, - style: TextStyle(fontSize: 14), - decoration: InputDecoration( - contentPadding: EdgeInsets.symmetric(vertical: 6), - hintText: - focused.value ? null : translate("Search ID"), - hintStyle: TextStyle( - fontSize: 14, - color: MyTheme.color(context).placeholder), - border: InputBorder.none, - isDense: true, - ), - ), - ), - ], - ), - ), - ), - Offstage( - offstage: !(peerSearchText.value.isNotEmpty && - (rowHover.value || clearHover.value)), - child: InkWell( - onHover: (value) => clearHover.value = value, - child: Icon( - IconFont.round_close, - size: 16, - color: clearHover.value - ? MyTheme.color(context).text - : MyTheme.color(context).placeholder, - ).marginSymmetric(horizontal: 4), - onTap: () { - peerSearchTextController.clear(); - peerSearchText.value = ""; - }), - ) - ], - )), - ); - } - - _createPeerViewTypeSwitch(BuildContext context) { - final activeDeco = BoxDecoration(color: MyTheme.color(context).bg); - return Row( - children: [ - Obx( - () => Container( - padding: EdgeInsets.all(4.0), - decoration: - peerCardUiType.value == PeerUiType.grid ? activeDeco : null, - child: InkWell( - onTap: () { - peerCardUiType.value = PeerUiType.grid; - }, - child: Icon( - Icons.grid_view_rounded, - size: 18, - color: peerCardUiType.value == PeerUiType.grid - ? MyTheme.color(context).text - : MyTheme.color(context).lightText, - )), - ), - ), - Obx( - () => Container( - padding: EdgeInsets.all(4.0), - decoration: - peerCardUiType.value == PeerUiType.list ? activeDeco : null, - child: InkWell( - onTap: () { - peerCardUiType.value = PeerUiType.list; - }, - child: Icon( - Icons.list, - size: 18, - color: peerCardUiType.value == PeerUiType.list - ? MyTheme.color(context).text - : MyTheme.color(context).lightText, - )), - ), - ), - ], - ); - } } diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 092ea7da8..070ad217b 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -15,7 +15,6 @@ import '../../models/platform_model.dart'; import '../../common/shared_state.dart'; import './popup_menu.dart'; import './material_mod_popup_menu.dart' as mod_menu; -import './utils.dart'; class _MenubarTheme { static const Color commonColor = MyTheme.accent; diff --git a/flutter/lib/desktop/widgets/utils.dart b/flutter/lib/desktop/widgets/utils.dart deleted file mode 100644 index 2f555c239..000000000 --- a/flutter/lib/desktop/widgets/utils.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_hbb/utils/multi_window_manager.dart'; - -/// Connect to a peer with [id]. -/// If [isFileTransfer], starts a session only for file transfer. -/// If [isTcpTunneling], starts a session only for tcp tunneling. -/// If [isRDP], starts a session only for rdp. -void connect(BuildContext context, String id, - {bool isFileTransfer = false, - bool isTcpTunneling = false, - bool isRDP = false}) async { - if (id == '') return; - id = id.replaceAll(' ', ''); - assert(!(isFileTransfer && isTcpTunneling && isRDP), - "more than one connect type"); - - FocusScopeNode currentFocus = FocusScope.of(context); - if (isFileTransfer) { - await rustDeskWinManager.newFileTransfer(id); - } else if (isTcpTunneling || isRDP) { - await rustDeskWinManager.newPortForward(id, isRDP); - } else { - await rustDeskWinManager.newRemoteDesktop(id); - } - if (!currentFocus.hasPrimaryFocus) { - currentFocus.unfocus(); - } -} diff --git a/flutter/lib/mobile/pages/connection_page.dart b/flutter/lib/mobile/pages/connection_page.dart index e6eddd087..83e26547a 100644 --- a/flutter/lib/mobile/pages/connection_page.dart +++ b/flutter/lib/mobile/pages/connection_page.dart @@ -2,12 +2,16 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/mobile/pages/file_manager_page.dart'; +import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; import '../../common.dart'; +import '../../common/widgets/address_book.dart'; +import '../../common/widgets/peer_tab_page.dart'; +import '../../common/widgets/peer_widget.dart'; +import '../../consts.dart'; import '../../models/model.dart'; -import '../../models/peer_model.dart'; import '../../models/platform_model.dart'; import 'home_page.dart'; import 'remote_page.dart'; @@ -19,16 +23,16 @@ class ConnectionPage extends StatefulWidget implements PageShape { ConnectionPage({Key? key}) : super(key: key); @override - final icon = Icon(Icons.connected_tv); + final icon = const Icon(Icons.connected_tv); @override final title = translate("Connection"); @override - final appBarActions = !isAndroid ? [WebMenu()] : []; + final appBarActions = !isAndroid ? [const WebMenu()] : []; @override - _ConnectionPageState createState() => _ConnectionPageState(); + State createState() => _ConnectionPageState(); } /// State for the connection page. @@ -38,7 +42,6 @@ class _ConnectionPageState extends State { /// Update url. If it's not null, means an update is available. var _updateUrl = ''; - var _menuPos; @override void initState() { @@ -54,9 +57,8 @@ class _ConnectionPageState extends State { }(); } if (isAndroid) { - Timer(Duration(seconds: 5), () async { + Timer(const Duration(seconds: 5), () async { _updateUrl = await bind.mainGetSoftwareUpdateUrl(); - ; if (_updateUrl.isNotEmpty) setState(() {}); }); } @@ -65,19 +67,29 @@ class _ConnectionPageState extends State { @override Widget build(BuildContext context) { Provider.of(context); - return SingleChildScrollView( - controller: ScrollController(), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - getUpdateUI(), - getSearchBarUI(), - Container(height: 12), - getPeers(), - ]), - ); + return Column( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + getUpdateUI(), + getSearchBarUI(), + Expanded( + child: PeerTabPage( + tabs: [ + translate('Recent Sessions'), + translate('Favorites'), + translate('Discovered'), + translate('Address Book') + ], + children: [ + RecentPeerWidget(), + FavoritePeerWidget(), + DiscoveredPeerWidget(), + const AddressBook(), + ], + )), + ]).marginOnly(top: 2, left: 12, right: 12); } /// Callback for the connect button. @@ -122,10 +134,10 @@ class _ConnectionPageState extends State { /// If [_updateUrl] is not empty, shows a button to update the software. Widget getUpdateUI() { return _updateUrl.isEmpty - ? SizedBox(height: 0) + ? const SizedBox(height: 0) : InkWell( onTap: () async { - final url = _updateUrl + '.apk'; + final url = '$_updateUrl.apk'; if (await canLaunchUrl(Uri.parse(url))) { await launchUrl(Uri.parse(url)); } @@ -134,79 +146,77 @@ class _ConnectionPageState extends State { alignment: AlignmentDirectional.center, width: double.infinity, color: Colors.pinkAccent, - padding: EdgeInsets.symmetric(vertical: 12), + padding: const EdgeInsets.symmetric(vertical: 12), child: Text(translate('Download new version'), - style: TextStyle( + style: const TextStyle( color: Colors.white, fontWeight: FontWeight.bold)))); } /// UI for the search bar. /// Search for a peer and connect to it if the id exists. Widget getSearchBarUI() { - var w = Padding( - padding: const EdgeInsets.fromLTRB(16.0, 8.0, 16.0, 0.0), - child: Container( - height: 84, - child: Padding( - padding: const EdgeInsets.only(top: 8, bottom: 8), - child: Ink( - decoration: BoxDecoration( - color: MyTheme.white, - borderRadius: const BorderRadius.all(Radius.circular(13)), - ), - child: Row( - children: [ - Expanded( - child: Container( - padding: const EdgeInsets.only(left: 16, right: 16), - child: TextField( - autocorrect: false, - enableSuggestions: false, - keyboardType: TextInputType.visiblePassword, - // keyboardType: TextInputType.number, - style: TextStyle( - fontFamily: 'WorkSans', - fontWeight: FontWeight.bold, - fontSize: 30, - color: MyTheme.idColor, - ), - decoration: InputDecoration( - labelText: translate('Remote ID'), - // hintText: 'Enter your remote ID', - border: InputBorder.none, - helperStyle: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - color: MyTheme.darkGray, - ), - labelStyle: TextStyle( - fontWeight: FontWeight.w600, - fontSize: 16, - letterSpacing: 0.2, - color: MyTheme.darkGray, - ), - ), - controller: _idController, + final w = SizedBox( + height: 84, + child: Padding( + padding: const EdgeInsets.only(top: 8, bottom: 8), + child: Ink( + decoration: const BoxDecoration( + color: MyTheme.white, + borderRadius: BorderRadius.all(Radius.circular(13)), + ), + child: Row( + children: [ + Expanded( + child: Container( + padding: const EdgeInsets.only(left: 16, right: 16), + child: TextField( + autocorrect: false, + enableSuggestions: false, + keyboardType: TextInputType.visiblePassword, + // keyboardType: TextInputType.number, + style: const TextStyle( + fontFamily: 'WorkSans', + fontWeight: FontWeight.bold, + fontSize: 30, + color: MyTheme.idColor, ), + decoration: InputDecoration( + labelText: translate('Remote ID'), + // hintText: 'Enter your remote ID', + border: InputBorder.none, + helperStyle: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + color: MyTheme.darkGray, + ), + labelStyle: const TextStyle( + fontWeight: FontWeight.w600, + fontSize: 16, + letterSpacing: 0.2, + color: MyTheme.darkGray, + ), + ), + controller: _idController, ), ), - SizedBox( - width: 60, - height: 60, - child: IconButton( - icon: Icon(Icons.arrow_forward, - color: MyTheme.darkGray, size: 45), - onPressed: onConnect, - ), + ), + SizedBox( + width: 60, + height: 60, + child: IconButton( + icon: const Icon(Icons.arrow_forward, + color: MyTheme.darkGray, size: 45), + onPressed: onConnect, ), - ], - ), + ), + ], ), ), ), ); - return Center( - child: Container(constraints: BoxConstraints(maxWidth: 600), child: w)); + return Align( + alignment: Alignment.topLeft, + child: Container(constraints: kMobilePageConstraints, child: w)); } @override @@ -214,97 +224,13 @@ class _ConnectionPageState extends State { _idController.dispose(); super.dispose(); } - - /// Get all the saved peers. - Widget getPeers() { - final windowWidth = MediaQuery.of(context).size.width; - final space = 8.0; - var width = windowWidth - 2 * space; - final minWidth = 320.0; - if (windowWidth > minWidth + 2 * space) { - final n = (windowWidth / (minWidth + 2 * space)).floor(); - width = windowWidth / n - 2 * space; - } - return FutureBuilder>( - future: gFFI.peers(), - builder: (context, snapshot) { - final cards = []; - if (snapshot.hasData) { - final peers = snapshot.data!; - peers.forEach((p) { - cards.add(Container( - width: width, - child: Card( - child: GestureDetector( - onTap: - !isWebDesktop ? () => connect('${p.id}') : null, - onDoubleTap: - isWebDesktop ? () => connect('${p.id}') : null, - onLongPressStart: (details) { - final x = details.globalPosition.dx; - final y = details.globalPosition.dy; - _menuPos = RelativeRect.fromLTRB(x, y, x, y); - showPeerMenu(context, p.id); - }, - child: ListTile( - contentPadding: const EdgeInsets.only(left: 12), - subtitle: Text('${p.username}@${p.hostname}'), - title: Text('${p.id}'), - leading: Container( - padding: const EdgeInsets.all(6), - child: getPlatformImage('${p.platform}'), - color: str2color('${p.id}${p.platform}', 0x7f)), - trailing: InkWell( - child: Padding( - padding: const EdgeInsets.all(12), - child: Icon(Icons.more_vert)), - onTapDown: (e) { - final x = e.globalPosition.dx; - final y = e.globalPosition.dy; - _menuPos = RelativeRect.fromLTRB(x, y, x, y); - }, - onTap: () { - showPeerMenu(context, p.id); - }), - ))))); - }); - } - return Wrap(children: cards, spacing: space, runSpacing: space); - }); - } - - /// Show the peer menu and handle user's choice. - /// User might remove the peer or send a file to the peer. - void showPeerMenu(BuildContext context, String id) async { - var value = await showMenu( - context: context, - position: this._menuPos, - items: [ - PopupMenuItem( - child: Text(translate('Remove')), value: 'remove') - ] + - (!isAndroid - ? [] - : [ - PopupMenuItem( - child: Text(translate('Transfer File')), value: 'file') - ]), - elevation: 8, - ); - if (value == 'remove') { - setState(() => bind.mainRemovePeer(id: id)); - () async { - removePreference(id); - }(); - } else if (value == 'file') { - connect(id, isFileTransfer: true); - } - } } class WebMenu extends StatefulWidget { + const WebMenu({Key? key}) : super(key: key); + @override - _WebMenuState createState() => _WebMenuState(); + State createState() => _WebMenuState(); } class _WebMenuState extends State { @@ -337,36 +263,36 @@ class _WebMenuState extends State { Widget build(BuildContext context) { Provider.of(context); return PopupMenuButton( - icon: Icon(Icons.more_vert), + icon: const Icon(Icons.more_vert), itemBuilder: (context) { return (isIOS ? [ - PopupMenuItem( - child: Icon(Icons.qr_code_scanner, color: Colors.black), + const PopupMenuItem( value: "scan", + child: Icon(Icons.qr_code_scanner, color: Colors.black), ) ] : >[]) + [ PopupMenuItem( - child: Text(translate('ID/Relay Server')), value: "server", + child: Text(translate('ID/Relay Server')), ) ] + (url.contains('admin.rustdesk.com') ? >[] : [ PopupMenuItem( + value: "login", child: Text(username == null ? translate("Login") - : translate("Logout") + ' ($username)'), - value: "login", + : '${translate("Logout")} ($username)'), ) ]) + [ PopupMenuItem( - child: Text(translate('About') + ' RustDesk'), value: "about", + child: Text('${translate('About')} RustDesk'), ) ]; }, diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index be8403427..dc3a153d7 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -32,6 +32,7 @@ const url = 'https://rustdesk.com/'; final _hasIgnoreBattery = androidVersion >= 26; var _ignoreBatteryOpt = false; var _enableAbr = false; +var _isDarkMode = false; class _SettingsState extends State with WidgetsBindingObserver { String? username; @@ -59,6 +60,8 @@ class _SettingsState extends State with WidgetsBindingObserver { _enableAbr = enableAbrRes; } + _enableAbr = isDarkTheme(); + if (update) { setState(() {}); } @@ -173,7 +176,18 @@ class _SettingsState extends State with WidgetsBindingObserver { leading: Icon(Icons.translate), onPressed: (context) { showLanguageSettings(gFFI.dialogManager); - }) + }), + SettingsTile.switchTile( + title: Text(translate('Dark Theme')), + leading: Icon(Icons.dark_mode), + initialValue: _isDarkMode, + onToggle: (v) { + setState(() { + _isDarkMode = !_isDarkMode; + MyTheme.changeTo(_isDarkMode); + }); + }, + ) ]), SettingsSection( title: Text(translate("Enhancements")), diff --git a/flutter/lib/models/ab_model.dart b/flutter/lib/models/ab_model.dart index 2749e972f..e0467565c 100644 --- a/flutter/lib/models/ab_model.dart +++ b/flutter/lib/models/ab_model.dart @@ -101,7 +101,7 @@ class AbModel with ChangeNotifier { final resp = await http.post(Uri.parse(api), headers: authHeaders, body: body); abLoading = false; - await getAb(); + // await getAb(); // TODO notifyListeners(); debugPrint("resp: ${resp.body}"); } From 5625a061a4b15c5606c525c855186b6d9929fc85 Mon Sep 17 00:00:00 2001 From: csf Date: Wed, 21 Sep 2022 14:56:01 +0800 Subject: [PATCH 0544/2015] merge master peer_tab_page.dart peer_widget.dart --- flutter/lib/common/widgets/peer_tab_page.dart | 88 ++++++------ flutter/lib/common/widgets/peer_widget.dart | 128 ++++++++++-------- 2 files changed, 115 insertions(+), 101 deletions(-) diff --git a/flutter/lib/common/widgets/peer_tab_page.dart b/flutter/lib/common/widgets/peer_tab_page.dart index fbda91649..c1151088d 100644 --- a/flutter/lib/common/widgets/peer_tab_page.dart +++ b/flutter/lib/common/widgets/peer_tab_page.dart @@ -23,15 +23,31 @@ class _PeerTabPageState extends State @override void initState() { + () async { + await bind.mainGetLocalOption(key: 'peer-tab-index').then((value) { + if (value == '') return; + final tab = int.parse(value); + _tabIndex.value = tab; + _pageController.jumpToPage(tab); + }); + await bind.mainGetLocalOption(key: 'peer-card-ui-type').then((value) { + if (value == '') return; + final tab = int.parse(value); + peerCardUiType.value = + tab == PeerUiType.list.index ? PeerUiType.list : PeerUiType.grid; + }); + }(); super.initState(); } // hard code for now - void _handleTabSelection(int index) { + Future _handleTabSelection(int index) async { // reset search text peerSearchText.value = ""; peerSearchTextController.clear(); _tabIndex.value = index; + await bind.mainSetLocalOption( + key: 'peer-tab-index', value: index.toString()); _pageController.jumpToPage(index); switch (index) { case 0: @@ -89,7 +105,7 @@ class _PeerTabPageState extends State shrinkWrap: true, controller: ScrollController(), children: super.widget.tabs.asMap().entries.map((t) { - return Obx(() => GestureDetector( + return Obx(() => InkWell( child: Container( padding: const EdgeInsets.symmetric(horizontal: 8), decoration: BoxDecoration( @@ -111,7 +127,7 @@ class _PeerTabPageState extends State : MyTheme.color(context).lightText), ), )), - onTap: () => _handleTabSelection(t.key), + onTap: () async => await _handleTabSelection(t.key), )); }).toList()); } @@ -120,7 +136,9 @@ class _PeerTabPageState extends State final verticalMargin = isDesktop ? 12.0 : 6.0; return Expanded( child: PageView( - physics: const BouncingScrollPhysics(), + physics: isDesktop + ? NeverScrollableScrollPhysics() + : BouncingScrollPhysics(), controller: _pageController, children: super.widget.children, onPageChanged: (to) => _tabIndex.value = to) @@ -130,44 +148,30 @@ class _PeerTabPageState extends State Widget _createPeerViewTypeSwitch(BuildContext context) { final activeDeco = BoxDecoration(color: MyTheme.color(context).bg); return Row( - children: [ - Obx( - () => Container( - padding: const EdgeInsets.all(4.0), - decoration: - peerCardUiType.value == PeerUiType.grid ? activeDeco : null, - child: InkWell( - onTap: () { - peerCardUiType.value = PeerUiType.grid; - }, - child: Icon( - Icons.grid_view_rounded, - size: 18, - color: peerCardUiType.value == PeerUiType.grid - ? MyTheme.color(context).text - : MyTheme.color(context).lightText, - )), - ), - ), - Obx( - () => Container( - padding: const EdgeInsets.all(4.0), - decoration: - peerCardUiType.value == PeerUiType.list ? activeDeco : null, - child: InkWell( - onTap: () { - peerCardUiType.value = PeerUiType.list; - }, - child: Icon( - Icons.list, - size: 18, - color: peerCardUiType.value == PeerUiType.list - ? MyTheme.color(context).text - : MyTheme.color(context).lightText, - )), - ), - ), - ], + children: [PeerUiType.grid, PeerUiType.list] + .map((type) => Obx( + () => Container( + padding: EdgeInsets.all(4.0), + decoration: peerCardUiType.value == type ? activeDeco : null, + child: InkWell( + onTap: () async { + await bind.mainSetLocalOption( + key: 'peer-card-ui-type', + value: type.index.toString()); + peerCardUiType.value = type; + }, + child: Icon( + type == PeerUiType.grid + ? Icons.grid_view_rounded + : Icons.list, + size: 18, + color: peerCardUiType.value == type + ? MyTheme.color(context).text + : MyTheme.color(context).lightText, + )), + ), + )) + .toList(), ); } } diff --git a/flutter/lib/common/widgets/peer_widget.dart b/flutter/lib/common/widgets/peer_widget.dart index f32b8a2f1..bdfbe53e5 100644 --- a/flutter/lib/common/widgets/peer_widget.dart +++ b/flutter/lib/common/widgets/peer_widget.dart @@ -92,68 +92,78 @@ class _PeerWidgetState extends State<_PeerWidget> with WindowListener { return ChangeNotifierProvider( create: (context) => widget.peers, child: Consumer( - builder: (context, peers, child) => peers.peers.isEmpty - ? Center( - child: Text(translate("Empty")), - ) - : DesktopScrollWrapper( - scrollController: _scrollController, - child: SingleChildScrollView( - physics: NeverScrollableScrollPhysics(), - controller: _scrollController, - child: ObxValue((searchText) { - return FutureBuilder>( - builder: (context, snapshot) { - if (snapshot.hasData) { - final peers = snapshot.data!; - final cards = []; - for (final peer in peers) { - cards.add(Offstage( - key: ValueKey("off${peer.id}"), - offstage: widget.offstageFunc(peer), - child: Obx( - () => SizedBox( - width: 220, - height: - peerCardUiType.value == PeerUiType.grid - ? 140 - : 42, - child: VisibilityDetector( - key: ValueKey(peer.id), - onVisibilityChanged: (info) { - final peerId = - (info.key as ValueKey).value; - if (info.visibleFraction > 0.00001) { - _curPeers.add(peerId); - } else { - _curPeers.remove(peerId); - } - _lastChangeTime = DateTime.now(); - }, - child: widget.peerCardWidgetFunc(peer), - ), - ), - ))); - } - return Wrap( - spacing: space, - runSpacing: space, - children: cards); - } else { - return const Center( - child: CircularProgressIndicator(), - ); - } - }, - future: matchPeers(searchText.value, peers.peers), - ); - }, peerSearchText), - ), - ), - ), + builder: (context, peers, child) => peers.peers.isEmpty + ? Center( + child: Text(translate("Empty")), + ) + : _buildPeersView(peers)), ); } + Widget _buildPeersView(Peers peers) { + final body = ObxValue((searchText) { + return FutureBuilder>( + builder: (context, snapshot) { + if (snapshot.hasData) { + final peers = snapshot.data!; + final cards = []; + for (final peer in peers) { + final visibilityChild = VisibilityDetector( + key: ValueKey(peer.id), + onVisibilityChanged: (info) { + final peerId = (info.key as ValueKey).value; + if (info.visibleFraction > 0.00001) { + _curPeers.add(peerId); + } else { + _curPeers.remove(peerId); + } + _lastChangeTime = DateTime.now(); + }, + child: widget.peerCardWidgetFunc(peer), + ); + cards.add(Offstage( + key: ValueKey("off${peer.id}"), + offstage: widget.offstageFunc(peer), + child: isDesktop + ? Obx( + () => SizedBox( + width: 220, + height: peerCardUiType.value == PeerUiType.grid + ? 140 + : 42, + child: visibilityChild, + ), + ) + : SizedBox(width: mobileWidth, child: visibilityChild))); + } + return Wrap(spacing: space, runSpacing: space, children: cards); + } else { + return const Center( + child: CircularProgressIndicator(), + ); + } + }, + future: matchPeers(searchText.value, peers.peers), + ); + }, peerSearchText); + + if (isDesktop) { + return DesktopScrollWrapper( + scrollController: _scrollController, + child: SingleChildScrollView( + physics: NeverScrollableScrollPhysics(), + controller: _scrollController, + child: body), + ); + } else { + return SingleChildScrollView( + physics: BouncingScrollPhysics(), + controller: _scrollController, + child: body, + ); + } + } + // ignore: todo // TODO: variables walk through async tasks? void _startCheckOnlines() { From 285d415a5aed2ac86906675e479ad0dba269a776 Mon Sep 17 00:00:00 2001 From: csf Date: Wed, 21 Sep 2022 17:16:09 +0800 Subject: [PATCH 0545/2015] mobile peers tab padding --- flutter/lib/common/widgets/peer_tab_page.dart | 1 + flutter/lib/common/widgets/peercard_widget.dart | 2 +- flutter/lib/mobile/pages/connection_page.dart | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/flutter/lib/common/widgets/peer_tab_page.dart b/flutter/lib/common/widgets/peer_tab_page.dart index c1151088d..fefe74671 100644 --- a/flutter/lib/common/widgets/peer_tab_page.dart +++ b/flutter/lib/common/widgets/peer_tab_page.dart @@ -80,6 +80,7 @@ class _PeerTabPageState extends State SizedBox( height: 28, child: Container( + padding: isDesktop ? null : EdgeInsets.symmetric(horizontal: 2), constraints: isDesktop ? null : kMobilePageConstraints, child: Row( crossAxisAlignment: CrossAxisAlignment.center, diff --git a/flutter/lib/common/widgets/peercard_widget.dart b/flutter/lib/common/widgets/peercard_widget.dart index 2cb451974..2e9c1c080 100644 --- a/flutter/lib/common/widgets/peercard_widget.dart +++ b/flutter/lib/common/widgets/peercard_widget.dart @@ -64,7 +64,7 @@ class _PeerCardState extends State<_PeerCard> Widget _buildMobile() { final peer = super.widget.peer; return Card( - margin: EdgeInsets.zero, + margin: EdgeInsets.symmetric(horizontal: 2), child: GestureDetector( onTap: !isWebDesktop ? () => connect(context, peer.id) : null, onDoubleTap: isWebDesktop ? () => connect(context, peer.id) : null, diff --git a/flutter/lib/mobile/pages/connection_page.dart b/flutter/lib/mobile/pages/connection_page.dart index 83e26547a..1377088c7 100644 --- a/flutter/lib/mobile/pages/connection_page.dart +++ b/flutter/lib/mobile/pages/connection_page.dart @@ -89,7 +89,7 @@ class _ConnectionPageState extends State { const AddressBook(), ], )), - ]).marginOnly(top: 2, left: 12, right: 12); + ]).marginOnly(top: 2, left: 10, right: 10); } /// Callback for the connect button. @@ -158,7 +158,7 @@ class _ConnectionPageState extends State { final w = SizedBox( height: 84, child: Padding( - padding: const EdgeInsets.only(top: 8, bottom: 8), + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 2), child: Ink( decoration: const BoxDecoration( color: MyTheme.white, From 725c0689e275878dde23fdec51ddf57953c8cbd7 Mon Sep 17 00:00:00 2001 From: csf Date: Wed, 21 Sep 2022 17:54:47 +0800 Subject: [PATCH 0546/2015] mobile id text format --- .../lib/common/widgets/peercard_widget.dart | 2 +- .../lib/desktop/pages/connection_page.dart | 6 +++--- flutter/lib/mobile/pages/connection_page.dart | 20 ++++++++++--------- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/flutter/lib/common/widgets/peercard_widget.dart b/flutter/lib/common/widgets/peercard_widget.dart index 2e9c1c080..e3bc81af2 100644 --- a/flutter/lib/common/widgets/peercard_widget.dart +++ b/flutter/lib/common/widgets/peercard_widget.dart @@ -77,7 +77,7 @@ class _PeerCardState extends State<_PeerCard> child: ListTile( contentPadding: const EdgeInsets.only(left: 12), subtitle: Text('${peer.username}@${peer.hostname}'), - title: Text(peer.id), + title: Text(formatID(peer.id)), leading: Container( padding: const EdgeInsets.all(6), color: str2color('${peer.id}${peer.platform}', 0x7f), diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 6e1eac814..ad8e430f4 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -60,7 +60,7 @@ class _ConnectionPageState extends State { children: [ Row( children: [ - getSearchBarUI(context), + _buildRemoteIDTextField(context), ], ).marginOnly(top: 22), SizedBox(height: 12), @@ -97,9 +97,9 @@ class _ConnectionPageState extends State { connect(context, id, isFileTransfer: isFileTransfer); } - /// UI for the search bar. + /// UI for the remote ID TextField. /// Search for a peer and connect to it if the id exists. - Widget getSearchBarUI(BuildContext context) { + Widget _buildRemoteIDTextField(BuildContext context) { RxBool ftHover = false.obs; RxBool ftPressed = false.obs; RxBool connHover = false.obs; diff --git a/flutter/lib/mobile/pages/connection_page.dart b/flutter/lib/mobile/pages/connection_page.dart index 1377088c7..edc2f5f6d 100644 --- a/flutter/lib/mobile/pages/connection_page.dart +++ b/flutter/lib/mobile/pages/connection_page.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common/formatter/id_formatter.dart'; import 'package:flutter_hbb/mobile/pages/file_manager_page.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; @@ -38,7 +39,7 @@ class ConnectionPage extends StatefulWidget implements PageShape { /// State for the connection page. class _ConnectionPageState extends State { /// Controller for the id input bar. - final _idController = TextEditingController(); + final _idController = IDTextEditingController(); /// Update url. If it's not null, means an update is available. var _updateUrl = ''; @@ -49,9 +50,9 @@ class _ConnectionPageState extends State { if (_idController.text.isEmpty) { () async { final lastRemoteId = await bind.mainGetLastRemoteId(); - if (lastRemoteId != _idController.text) { + if (lastRemoteId != _idController.id) { setState(() { - _idController.text = lastRemoteId; + _idController.id = lastRemoteId; }); } }(); @@ -72,8 +73,8 @@ class _ConnectionPageState extends State { mainAxisSize: MainAxisSize.max, crossAxisAlignment: CrossAxisAlignment.center, children: [ - getUpdateUI(), - getSearchBarUI(), + _buildUpdateUI(), + _buildRemoteIDTextField(), Expanded( child: PeerTabPage( tabs: [ @@ -95,7 +96,7 @@ class _ConnectionPageState extends State { /// Callback for the connect button. /// Connects to the selected peer. void onConnect() { - var id = _idController.text.trim(); + var id = _idController.id; connect(id); } @@ -132,7 +133,7 @@ class _ConnectionPageState extends State { /// UI for software update. /// If [_updateUrl] is not empty, shows a button to update the software. - Widget getUpdateUI() { + Widget _buildUpdateUI() { return _updateUrl.isEmpty ? const SizedBox(height: 0) : InkWell( @@ -152,9 +153,9 @@ class _ConnectionPageState extends State { color: Colors.white, fontWeight: FontWeight.bold)))); } - /// UI for the search bar. + /// UI for the remote ID TextField. /// Search for a peer and connect to it if the id exists. - Widget getSearchBarUI() { + Widget _buildRemoteIDTextField() { final w = SizedBox( height: 84, child: Padding( @@ -197,6 +198,7 @@ class _ConnectionPageState extends State { ), ), controller: _idController, + inputFormatters: [IDTextInputFormatter()], ), ), ), From 4377baf0628521a2e4ad8beea5e82ed50a414309 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 21 Sep 2022 18:49:28 +0800 Subject: [PATCH 0547/2015] imporove setting page --- .../desktop/pages/desktop_setting_page.dart | 323 +++++++++--------- 1 file changed, 161 insertions(+), 162 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 4aedc1385..151498fb7 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -10,6 +10,7 @@ import 'package:flutter_hbb/models/server_model.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher_string.dart'; +import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart'; import '../../common/widgets/dialog.dart'; @@ -44,7 +45,6 @@ class _DesktopSettingPageState extends State with TickerProviderStateMixin, AutomaticKeepAliveClientMixin { final List<_TabInfo> settingTabs = <_TabInfo>[ _TabInfo('General', Icons.settings_outlined, Icons.settings), - _TabInfo('Language', Icons.language_outlined, Icons.language), _TabInfo('Security', Icons.enhanced_encryption_outlined, Icons.enhanced_encryption), _TabInfo('Network', Icons.link_outlined, Icons.link), @@ -84,17 +84,18 @@ class _DesktopSettingPageState extends State Expanded( child: Container( color: MyTheme.color(context).grayBg, - child: PageView( - controller: controller, - children: const [ - _General(), - _Language(), - _Safety(), - _Network(), - _Acount(), - _About(), - ], - ), + child: DesktopScrollWrapper( + scrollController: controller, + child: PageView( + controller: controller, + children: const [ + _General(), + _Safety(), + _Network(), + _Acount(), + _About(), + ], + )), ), ) ], @@ -123,14 +124,18 @@ class _DesktopSettingPageState extends State } Widget _listView({required List<_TabInfo> tabs}) { - return ListView( - controller: ScrollController(), - children: tabs - .asMap() - .entries - .map((tab) => _listItem(tab: tab.value, index: tab.key)) - .toList(), - ); + final scrollController = ScrollController(); + return DesktopScrollWrapper( + scrollController: scrollController, + child: ListView( + physics: NeverScrollableScrollPhysics(), + controller: scrollController, + children: tabs + .asMap() + .entries + .map((tab) => _listItem(tab: tab.value, index: tab.key)) + .toList(), + )); } Widget _listItem({required _TabInfo tab, required int index}) { @@ -183,15 +188,20 @@ class _General extends StatefulWidget { class _GeneralState extends State<_General> { @override Widget build(BuildContext context) { - return ListView( - controller: ScrollController(), - children: [ - theme(), - abr(), - hwcodec(), - audio(context), - ], - ).marginOnly(bottom: _kListViewBottomMargin); + final scrollController = ScrollController(); + return DesktopScrollWrapper( + scrollController: scrollController, + child: ListView( + physics: NeverScrollableScrollPhysics(), + controller: scrollController, + children: [ + theme(), + abr(), + hwcodec(), + audio(context), + _Card(title: 'Language', children: [language()]), + ], + ).marginOnly(bottom: _kListViewBottomMargin)); } Widget theme() { @@ -273,30 +283,6 @@ class _GeneralState extends State<_General> { ]); }); } -} - -class _Language extends StatefulWidget { - const _Language({Key? key}) : super(key: key); - - @override - State<_Language> createState() => _LanguageState(); -} - -class _LanguageState extends State<_Language> - with AutomaticKeepAliveClientMixin { - @override - bool get wantKeepAlive => true; - - @override - Widget build(BuildContext context) { - super.build(context); - return ListView( - controller: ScrollController(), - children: [ - _Card(title: 'Language', children: [language()]), - ], - ).marginOnly(bottom: _kListViewBottomMargin); - } Widget language() { return _futureBuilder(future: () async { @@ -340,31 +326,37 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { @override bool get wantKeepAlive => true; bool locked = true; + final scrollController = ScrollController(); @override Widget build(BuildContext context) { super.build(context); - return ListView( - controller: ScrollController(), - children: [ - Column( - children: [ - _lock(locked, 'Unlock Security Settings', () { - locked = false; - setState(() => {}); - }), - AbsorbPointer( - absorbing: locked, - child: Column(children: [ - permissions(context), - password(context), - connection(context), - ]), - ), - ], - ) - ], - ).marginOnly(bottom: _kListViewBottomMargin); + return DesktopScrollWrapper( + scrollController: scrollController, + child: SingleChildScrollView( + physics: NeverScrollableScrollPhysics(), + controller: scrollController, + child: Column( + children: [ + _lock(locked, 'Unlock Security Settings', () { + locked = false; + setState(() => {}); + }), + AbsorbPointer( + absorbing: locked, + child: Column(children: [ + permissions(context), + password(context), + _Card(title: 'ID', children: [changeId()]), + connection(context), + ]), + ), + ], + )).marginOnly(bottom: _kListViewBottomMargin)); + } + + Widget changeId() { + return _Button('Change ID', changeIdDialog, enabled: !locked); } Widget permissions(context) { @@ -378,6 +370,8 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { enabled: enabled), _OptionCheckBox(context, 'Enable Audio', 'enable-audio', enabled: enabled), + _OptionCheckBox(context, 'Enable TCP Tunneling', 'enable-tunnel', + enabled: enabled), _OptionCheckBox(context, 'Enable Remote Restart', 'enable-remote-restart', enabled: enabled), _OptionCheckBox(context, 'Enable remote configuration modification', @@ -470,15 +464,13 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { Widget connection(BuildContext context) { bool enabled = !locked; - return _Card(title: 'Connection', children: [ + return _Card(title: 'Security', children: [ _OptionCheckBox(context, 'Deny remote access', 'stop-service', checkedIcon: const Icon( Icons.warning, color: Colors.yellowAccent, ), enabled: enabled), - _OptionCheckBox(context, 'Enable TCP Tunneling', 'enable-tunnel', - enabled: enabled), Offstage( offstage: !Platform.isWindows, child: _OptionCheckBox(context, 'Enable RDP', 'enable-rdp', @@ -615,27 +607,30 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin { Widget build(BuildContext context) { super.build(context); bool enabled = !locked; - return ListView(controller: ScrollController(), children: [ - Column( - children: [ - _lock(locked, 'Unlock Network Settings', () { - locked = false; - setState(() => {}); - }), - AbsorbPointer( - absorbing: locked, - child: Column(children: [ - _Card(title: 'Server', children: [ - _Button('ID/Relay Server', changeServer, enabled: enabled), - ]), - _Card(title: 'Proxy', children: [ - _Button('Socks5 Proxy', changeSocks5Proxy, enabled: enabled), - ]), - ]), - ), - ], - ) - ]).marginOnly(bottom: _kListViewBottomMargin); + final scrollController = ScrollController(); + return DesktopScrollWrapper( + scrollController: scrollController, + child: ListView( + controller: scrollController, + physics: NeverScrollableScrollPhysics(), + children: [ + _lock(locked, 'Unlock Network Settings', () { + locked = false; + setState(() => {}); + }), + AbsorbPointer( + absorbing: locked, + child: Column(children: [ + _Card(title: 'Server', children: [ + _Button('ID/Relay Server', changeServer, enabled: enabled), + ]), + _Card(title: 'Proxy', children: [ + _Button('Socks5 Proxy', changeSocks5Proxy, + enabled: enabled), + ]), + ]), + ), + ]).marginOnly(bottom: _kListViewBottomMargin)); } } @@ -649,13 +644,16 @@ class _Acount extends StatefulWidget { class _AcountState extends State<_Acount> { @override Widget build(BuildContext context) { - return ListView( - controller: ScrollController(), - children: [ - _Card(title: 'Acount', children: [login()]), - _Card(title: 'ID', children: [changeId()]), - ], - ).marginOnly(bottom: _kListViewBottomMargin); + final scrollController = ScrollController(); + return DesktopScrollWrapper( + scrollController: scrollController, + child: ListView( + physics: NeverScrollableScrollPhysics(), + controller: scrollController, + children: [ + _Card(title: 'Acount', children: [login()]), + ], + ).marginOnly(bottom: _kListViewBottomMargin)); } Widget login() { @@ -675,10 +673,6 @@ class _AcountState extends State<_Acount> { }); }); } - - Widget changeId() { - return _Button('Change ID', changeIdDialog); - } } class _About extends StatefulWidget { @@ -699,61 +693,66 @@ class _AboutState extends State<_About> { final license = data['license'].toString(); final version = data['version'].toString(); const linkStyle = TextStyle(decoration: TextDecoration.underline); - return ListView(controller: ScrollController(), children: [ - _Card(title: "About RustDesk", children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox( - height: 8.0, - ), - Text("Version: $version").marginSymmetric(vertical: 4.0), - InkWell( - onTap: () { - launchUrlString("https://rustdesk.com/privacy"); - }, - child: const Text( - "Privacy Statement", - style: linkStyle, - ).marginSymmetric(vertical: 4.0)), - InkWell( - onTap: () { - launchUrlString("https://rustdesk.com"); - }, - child: const Text( - "Website", - style: linkStyle, - ).marginSymmetric(vertical: 4.0)), - Container( - decoration: const BoxDecoration(color: Color(0xFF2c8cff)), - padding: - const EdgeInsets.symmetric(vertical: 24, horizontal: 8), - child: Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Copyright © 2022 Purslane Ltd.\n$license", - style: const TextStyle(color: Colors.white), + final scrollController = ScrollController(); + return DesktopScrollWrapper( + scrollController: scrollController, + child: SingleChildScrollView( + controller: scrollController, + physics: NeverScrollableScrollPhysics(), + child: _Card(title: "About RustDesk", children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + height: 8.0, + ), + Text("Version: $version").marginSymmetric(vertical: 4.0), + InkWell( + onTap: () { + launchUrlString("https://rustdesk.com/privacy"); + }, + child: const Text( + "Privacy Statement", + style: linkStyle, + ).marginSymmetric(vertical: 4.0)), + InkWell( + onTap: () { + launchUrlString("https://rustdesk.com"); + }, + child: const Text( + "Website", + style: linkStyle, + ).marginSymmetric(vertical: 4.0)), + Container( + decoration: const BoxDecoration(color: Color(0xFF2c8cff)), + padding: + const EdgeInsets.symmetric(vertical: 24, horizontal: 8), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Copyright © 2022 Purslane Ltd.\n$license", + style: const TextStyle(color: Colors.white), + ), + const Text( + "Made with heart in this chaotic world!", + style: TextStyle( + fontWeight: FontWeight.w800, + color: Colors.white), + ) + ], ), - const Text( - "Made with heart in this chaotic world!", - style: TextStyle( - fontWeight: FontWeight.w800, - color: Colors.white), - ) - ], - ), + ), + ], ), - ], - ), - ).marginSymmetric(vertical: 4.0) - ], - ).marginOnly(left: _kContentHMargin) - ]), - ]); + ).marginSymmetric(vertical: 4.0) + ], + ).marginOnly(left: _kContentHMargin) + ]), + )); }); } } From 715a780edd4f97a77cae6226aba9388590f61e59 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 21 Sep 2022 19:08:36 +0800 Subject: [PATCH 0548/2015] fix default lang bug --- flutter/lib/desktop/pages/desktop_setting_page.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 151498fb7..d59320504 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -295,11 +295,11 @@ class _GeneralState extends State<_General> { Map langsMap = {for (var v in langsList) v[0]: v[1]}; List keys = langsMap.keys.toList(); List values = langsMap.values.toList(); - keys.insert(0, "default"); + keys.insert(0, ""); values.insert(0, "Default"); String currentKey = data["lang"]!; if (!keys.contains(currentKey)) { - currentKey = "default"; + currentKey = ""; } return _ComboBox( keys: keys, @@ -1023,6 +1023,7 @@ class _ComboBox extends StatelessWidget { required this.values, required this.initialKey, required this.onChanged, + // ignore: unused_element this.enabled = true, }) : super(key: key); @@ -1030,7 +1031,6 @@ class _ComboBox extends StatelessWidget { Widget build(BuildContext context) { var index = keys.indexOf(initialKey); if (index < 0) { - assert(false); index = 0; } var ref = values[index].obs; From 7cecf32d9ef8279bc5feca8670a38e860f3faa19 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 21 Sep 2022 19:18:40 +0800 Subject: [PATCH 0549/2015] better warn icon --- flutter/lib/desktop/pages/desktop_setting_page.dart | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index d59320504..f79d58b50 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -348,7 +348,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { permissions(context), password(context), _Card(title: 'ID', children: [changeId()]), - connection(context), + more(context), ]), ), ], @@ -462,13 +462,13 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { }))); } - Widget connection(BuildContext context) { + Widget more(BuildContext context) { bool enabled = !locked; return _Card(title: 'Security', children: [ _OptionCheckBox(context, 'Deny remote access', 'stop-service', checkedIcon: const Icon( - Icons.warning, - color: Colors.yellowAccent, + Icons.warning_amber_rounded, + color: Color.fromARGB(255, 255, 204, 0), ), enabled: enabled), Offstage( @@ -571,7 +571,8 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { .marginOnly(right: 5), Offstage( offstage: !hasWhitelist.value, - child: const Icon(Icons.warning, color: Colors.yellowAccent) + child: const Icon(Icons.warning_amber_rounded, + color: Color.fromARGB(255, 255, 204, 0)) .marginOnly(right: 5), ), Expanded( From 5a4806e9b2bf34ec1dfb6b8d40679f1cc5690a3b Mon Sep 17 00:00:00 2001 From: csf Date: Wed, 21 Sep 2022 21:20:19 +0800 Subject: [PATCH 0550/2015] refactor peer alias --- flutter/lib/common/widgets/peer_widget.dart | 2 +- .../lib/common/widgets/peercard_widget.dart | 78 +++++++++---------- flutter/lib/models/model.dart | 18 ----- flutter/lib/models/peer_model.dart | 17 ++-- src/flutter_ffi.rs | 47 +++++++++-- src/ui.rs | 17 ++-- src/ui_interface.rs | 16 ++-- 7 files changed, 106 insertions(+), 89 deletions(-) diff --git a/flutter/lib/common/widgets/peer_widget.dart b/flutter/lib/common/widgets/peer_widget.dart index bdfbe53e5..e6236ff4e 100644 --- a/flutter/lib/common/widgets/peer_widget.dart +++ b/flutter/lib/common/widgets/peer_widget.dart @@ -303,7 +303,7 @@ class AddressBookPeerWidget extends BasePeerWidget { static List _loadPeers() { debugPrint("_loadPeers : ${gFFI.abModel.peers.toString()}"); return gFFI.abModel.peers.map((e) { - return Peer.fromJson(e['id'], e); + return Peer.fromJson(e); }).toList(); } diff --git a/flutter/lib/common/widgets/peercard_widget.dart b/flutter/lib/common/widgets/peercard_widget.dart index e3bc81af2..ecf89283a 100644 --- a/flutter/lib/common/widgets/peercard_widget.dart +++ b/flutter/lib/common/widgets/peercard_widget.dart @@ -27,13 +27,11 @@ final peerCardUiType = PeerUiType.grid.obs; class _PeerCard extends StatefulWidget { final Peer peer; - final RxString alias; final Function(BuildContext, String) connect; final PopupMenuEntryBuilder popupMenuEntryBuilder; const _PeerCard( {required this.peer, - required this.alias, required this.connect, required this.popupMenuEntryBuilder, Key? key}) @@ -77,7 +75,7 @@ class _PeerCardState extends State<_PeerCard> child: ListTile( contentPadding: const EdgeInsets.only(left: 12), subtitle: Text('${peer.username}@${peer.hostname}'), - title: Text(formatID(peer.id)), + title: Text(peer.alias.isEmpty ? formatID(peer.id) : peer.alias), leading: Container( padding: const EdgeInsets.all(6), color: str2color('${peer.id}${peer.platform}', 0x7f), @@ -216,6 +214,7 @@ class _PeerCardState extends State<_PeerCard> Widget _buildPeerCard( BuildContext context, Peer peer, Rx deco) { + final name = '${peer.username}@${peer.hostname}'; return Card( color: Colors.transparent, elevation: 0, @@ -246,24 +245,18 @@ class _PeerCardState extends State<_PeerCard> Row( children: [ Expanded( - child: Obx(() { - final name = widget.alias.value.isEmpty - ? '${peer.username}@${peer.hostname}' - : widget.alias.value; - return Tooltip( - message: name, - waitDuration: - const Duration(seconds: 1), - child: Text( - name, - style: const TextStyle( - color: Colors.white70, - fontSize: 12), - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - ), - ); - }), + child: Tooltip( + message: name, + waitDuration: const Duration(seconds: 1), + child: Text( + name, + style: const TextStyle( + color: Colors.white70, + fontSize: 12), + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + ), + ), ), ], ), @@ -287,7 +280,8 @@ class _PeerCardState extends State<_PeerCard> backgroundColor: peer.online ? Colors.green : Colors.yellow)), - Text(formatID(peer.id)) + Text( + peer.alias.isEmpty ? formatID(peer.id) : peer.alias) ]).paddingSymmetric(vertical: 8), _actionMore(peer), ], @@ -338,20 +332,14 @@ class _PeerCardState extends State<_PeerCard> } abstract class BasePeerCard extends StatelessWidget { - final RxString alias = ''.obs; final Peer peer; - BasePeerCard({required this.peer, Key? key}) : super(key: key) { - bind - .mainGetPeerOption(id: peer.id, key: 'alias') - .then((value) => alias.value = value); - } + BasePeerCard({required this.peer, Key? key}) : super(key: key); @override Widget build(BuildContext context) { return _PeerCard( peer: peer, - alias: alias, connect: (BuildContext context, String id) => connect(context, id), popupMenuEntryBuilder: _buildPopupMenuEntry, ); @@ -379,7 +367,7 @@ abstract class BasePeerCard extends StatelessWidget { bool isRDP = false}) { return MenuEntryButton( childBuilder: (TextStyle? style) => Text( - translate(title), + title, style: style, ), proc: () { @@ -396,8 +384,13 @@ abstract class BasePeerCard extends StatelessWidget { } @protected - MenuEntryBase _connectAction(BuildContext context, String id) { - return _connectCommonAction(context, id, 'Connect'); + MenuEntryBase _connectAction(BuildContext context, Peer peer) { + return _connectCommonAction( + context, + peer.id, + peer.alias.isEmpty + ? translate('Connect') + : "${translate('Connect')} ${peer.id}"); } @protected @@ -405,7 +398,7 @@ abstract class BasePeerCard extends StatelessWidget { return _connectCommonAction( context, id, - 'Transfer File', + translate('Transfer File'), isFileTransfer: true, ); } @@ -415,7 +408,7 @@ abstract class BasePeerCard extends StatelessWidget { return _connectCommonAction( context, id, - 'TCP Tunneling', + translate('TCP Tunneling'), isTcpTunneling: true, ); } @@ -578,7 +571,7 @@ abstract class BasePeerCard extends StatelessWidget { void _rename(String id, bool isAddressBook) async { RxBool isInProgress = false.obs; - var name = await bind.mainGetPeerOption(id: id, key: 'alias'); + var name = peer.alias; var controller = TextEditingController(text: name); if (isAddressBook) { final peer = gFFI.abModel.peers.firstWhere((p) => id == p['id']); @@ -597,7 +590,12 @@ abstract class BasePeerCard extends StatelessWidget { gFFI.abModel.setPeerOption(id, 'alias', name); await gFFI.abModel.updateAb(); } - alias.value = await bind.mainGetPeerOption(id: peer.id, key: 'alias'); + if (isAddressBook) { + gFFI.abModel.getAb(); + } else { + bind.mainLoadRecentPeers(); + bind.mainLoadFavPeers(); + } close(); isInProgress.value = false; } @@ -642,7 +640,7 @@ class RecentPeerCard extends BasePeerCard { Future>> _buildMenuItems( BuildContext context) async { final List> menuItems = [ - _connectAction(context, peer.id), + _connectAction(context, peer), _transferFileAction(context, peer.id), _tcpTunnelingAction(context, peer.id), ]; @@ -674,7 +672,7 @@ class FavoritePeerCard extends BasePeerCard { Future>> _buildMenuItems( BuildContext context) async { final List> menuItems = [ - _connectAction(context, peer.id), + _connectAction(context, peer), _transferFileAction(context, peer.id), _tcpTunnelingAction(context, peer.id), ]; @@ -708,7 +706,7 @@ class DiscoveredPeerCard extends BasePeerCard { Future>> _buildMenuItems( BuildContext context) async { final List> menuItems = [ - _connectAction(context, peer.id), + _connectAction(context, peer), _transferFileAction(context, peer.id), _tcpTunnelingAction(context, peer.id), ]; @@ -739,7 +737,7 @@ class AddressBookPeerCard extends BasePeerCard { Future>> _buildMenuItems( BuildContext context) async { final List> menuItems = [ - _connectAction(context, peer.id), + _connectAction(context, peer), _transferFileAction(context, peer.id), _tcpTunnelingAction(context, peer.id), ]; diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index b9225414d..237378166 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -22,7 +22,6 @@ import '../common.dart'; import '../common/shared_state.dart'; import '../utils/image.dart' as img; import '../mobile/widgets/dialog.dart'; -import 'peer_model.dart'; import 'platform_model.dart'; typedef HandleMsgBox = Function(Map evt, String id); @@ -1107,23 +1106,6 @@ class FFI { id: id, msg: json.encode(modify({'x': '$x2', 'y': '$y2'}))); } - /// List the saved peers. - Future> peers() async { - try { - var str = await bind.mainGetRecentPeers(); - if (str == '') return []; - List peers = json.decode(str); - return peers - .map((s) => s as List) - .map((s) => - Peer.fromJson(s[0] as String, s[1] as Map)) - .toList(); - } catch (e) { - debugPrint('peers(): $e'); - } - return []; - } - /// Connect with the given [id]. Only transfer file if [isFileTransfer], only port forward if [isPortForward]. connect(String id, {bool isFileTransfer = false, diff --git a/flutter/lib/models/peer_model.dart b/flutter/lib/models/peer_model.dart index 0857bfcf8..c68ca26df 100644 --- a/flutter/lib/models/peer_model.dart +++ b/flutter/lib/models/peer_model.dart @@ -7,13 +7,16 @@ class Peer { final String username; final String hostname; final String platform; + final String alias; final List tags; bool online = false; - Peer.fromJson(this.id, Map json) - : username = json['username'] ?? '', + Peer.fromJson(Map json) + : id = json['id'] ?? '', + username = json['username'] ?? '', hostname = json['hostname'] ?? '', platform = json['platform'] ?? '', + alias = json['alias'] ?? '', tags = json['tags'] ?? []; Peer({ @@ -21,6 +24,7 @@ class Peer { required this.username, required this.hostname, required this.platform, + required this.alias, required this.tags, }); @@ -30,6 +34,7 @@ class Peer { username: '...', hostname: '...', platform: '...', + alias: '', tags: []); } @@ -109,11 +114,9 @@ class Peers extends ChangeNotifier { try { if (peersStr == "") return []; List peers = json.decode(peersStr); - return peers - .map((s) => s as List) - .map((s) => - Peer.fromJson(s[0] as String, s[1] as Map)) - .toList(); + return peers.map((peer) { + return Peer.fromJson(peer as Map); + }).toList(); } catch (e) { debugPrint('peers(): $e'); } diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 253855e3f..0579ac272 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -7,11 +7,11 @@ use std::{ use flutter_rust_bridge::{StreamSink, SyncReturn, ZeroCopyBuffer}; use serde_json::json; -use hbb_common::ResultType; use hbb_common::{ config::{self, LocalConfig, PeerConfig, ONLINE}, fs, log, }; +use hbb_common::{message_proto::Hash, ResultType}; use crate::flutter::{self, SESSIONS}; use crate::start_server; @@ -567,9 +567,20 @@ pub fn main_forget_password(id: String) { pub fn main_get_recent_peers() -> String { if !config::APP_DIR.read().unwrap().is_empty() { - let peers: Vec<(String, config::PeerInfoSerde)> = PeerConfig::peers() + let peers: Vec> = PeerConfig::peers() .drain(..) - .map(|(id, _, p)| (id, p.info)) + .map(|(id, _, p)| { + HashMap::<&str, String>::from_iter([ + ("id", id), + ("username", p.info.username.clone()), + ("hostname", p.info.hostname.clone()), + ("platform", p.info.platform.clone()), + ( + "alias", + p.options.get("alias").unwrap_or(&"".to_owned()).to_owned(), + ), + ]) + }) .collect(); serde_json::ser::to_string(&peers).unwrap_or("".to_owned()) } else { @@ -579,9 +590,20 @@ pub fn main_get_recent_peers() -> String { pub fn main_load_recent_peers() { if !config::APP_DIR.read().unwrap().is_empty() { - let peers: Vec<(String, config::PeerInfoSerde)> = PeerConfig::peers() + let peers: Vec> = PeerConfig::peers() .drain(..) - .map(|(id, _, p)| (id, p.info)) + .map(|(id, _, p)| { + HashMap::<&str, String>::from_iter([ + ("id", id), + ("username", p.info.username.clone()), + ("hostname", p.info.hostname.clone()), + ("platform", p.info.platform.clone()), + ( + "alias", + p.options.get("alias").unwrap_or(&"".to_owned()).to_owned(), + ), + ]) + }) .collect(); if let Some(s) = flutter::GLOBAL_EVENT_STREAM .read() @@ -603,11 +625,20 @@ pub fn main_load_recent_peers() { pub fn main_load_fav_peers() { if !config::APP_DIR.read().unwrap().is_empty() { let favs = get_fav(); - let peers: Vec<(String, config::PeerInfoSerde)> = PeerConfig::peers() + let peers: Vec> = PeerConfig::peers() .into_iter() - .filter_map(|(id, _, peer)| { + .filter_map(|(id, _, p)| { if favs.contains(&id) { - Some((id, peer.info)) + Some(HashMap::<&str, String>::from_iter([ + ("id", id), + ("username", p.info.username.clone()), + ("hostname", p.info.hostname.clone()), + ("platform", p.info.platform.clone()), + ( + "alias", + p.options.get("alias").unwrap_or(&"".to_owned()).to_owned(), + ), + ])) } else { None } diff --git a/src/ui.rs b/src/ui.rs index 77d983e56..d1c669848 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -18,7 +18,7 @@ use hbb_common::{ tokio::{self, sync::mpsc, time}, }; -use crate::common::{get_app_name}; +use crate::common::get_app_name; use crate::ipc; use crate::ui_interface::{ check_mouse_time, closing, create_shortcut, current_is_wayland, fix_login_wayland, @@ -73,9 +73,7 @@ fn check_connect_status( let (tx, rx) = mpsc::unbounded_channel::(); let password = Arc::new(Mutex::new(String::default())); let cloned_password = password.clone(); - std::thread::spawn(move || { - crate::ui_interface::check_connect_status_(reconnect, rx) - }); + std::thread::spawn(move || crate::ui_interface::check_connect_status_(reconnect, rx)); (status, options, tx, password) } @@ -525,9 +523,16 @@ impl UI { fn get_lan_peers(&self) -> String { let peers = get_lan_peers() .into_iter() - .map(|(id, peer)| (id, peer.username, peer.hostname, peer.platform)) + .map(|mut peer| { + ( + peer.remove("id").unwrap_or_default(), + peer.remove("username").unwrap_or_default(), + peer.remove("hostname").unwrap_or_default(), + peer.remove("platform").unwrap_or_default(), + ) + }) .collect::>(); - serde_json::to_string(&peers).unwrap_or_default() + serde_json::to_string(&get_lan_peers()).unwrap_or_default() } fn get_uuid(&self) -> String { diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 7242a35dc..dc3a02c7a 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -648,19 +648,17 @@ pub fn discover() { } #[inline] -pub fn get_lan_peers() -> Vec<(String, config::PeerInfoSerde)> { +pub fn get_lan_peers() -> Vec> { config::LanPeers::load() .peers .iter() .map(|peer| { - ( - peer.id.clone(), - config::PeerInfoSerde { - username: peer.username.clone(), - hostname: peer.hostname.clone(), - platform: peer.platform.clone(), - }, - ) + HashMap::<&str, String>::from_iter([ + ("id", peer.id.clone()), + ("username", peer.username.clone()), + ("hostname", peer.hostname.clone()), + ("platform", peer.platform.clone()), + ]) }) .collect() } From 86d83e12b0675f6a454846f95949ae999d188a12 Mon Sep 17 00:00:00 2001 From: csf Date: Wed, 21 Sep 2022 23:32:59 +0800 Subject: [PATCH 0551/2015] opt: dark theme, add follow system mode --- flutter/lib/common.dart | 60 +++++++++++++------ .../desktop/pages/desktop_setting_page.dart | 30 ++++++---- .../lib/desktop/pages/port_forward_page.dart | 2 +- flutter/lib/main.dart | 31 ++++++---- flutter/lib/mobile/pages/settings_page.dart | 4 +- src/flutter.rs | 4 +- src/flutter_ffi.rs | 4 +- src/ipc.rs | 2 +- src/ui/cm.rs | 2 +- src/ui_cm_interface.rs | 2 +- 10 files changed, 89 insertions(+), 52 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 0b56e9f1a..365ce3dd5 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -197,30 +197,39 @@ class MyTheme { ], ); - static changeTo(bool dark) { - if (isDarkTheme() != dark) { - Get.find().setString("darkTheme", dark ? "Y" : ""); - Get.changeThemeMode(dark ? ThemeMode.dark : ThemeMode.light); + static ThemeMode getThemeModePreference() { + return themeModeFromString( + Get.find().getString("themeMode") ?? ""); + } + + static void changeDarkMode(ThemeMode mode) { + final preference = getThemeModePreference(); + if (preference != mode) { + if (mode == ThemeMode.system) { + Get.find().setString("themeMode", ""); + } else { + Get.find() + .setString("themeMode", mode.toShortString()); + } + Get.changeThemeMode(mode); if (desktopType == DesktopType.main) { - bind.mainChangeTheme(dark: dark); + bind.mainChangeTheme(dark: currentThemeMode().toShortString()); } } } - static bool _themeInitialed = false; - - static ThemeMode initialThemeMode({bool mainPage = false}) { - bool dark; - // Brightnesss is always light on windows, Flutter 3.0.5 - if (_themeInitialed || !mainPage || Platform.isWindows) { - dark = isDarkTheme(); + static ThemeMode currentThemeMode() { + final preference = getThemeModePreference(); + if (preference == ThemeMode.system) { + if (WidgetsBinding.instance.platformDispatcher.platformBrightness == + Brightness.light) { + return ThemeMode.light; + } else { + return ThemeMode.dark; + } } else { - dark = WidgetsBinding.instance.platformDispatcher.platformBrightness == - Brightness.dark; - Get.find().setString("darkTheme", dark ? "Y" : ""); + return preference; } - _themeInitialed = true; - return dark ? ThemeMode.dark : ThemeMode.light; } static ColorThemeExtension color(BuildContext context) { @@ -230,10 +239,23 @@ class MyTheme { static TabbarTheme tabbar(BuildContext context) { return Theme.of(context).extension()!; } + + static ThemeMode themeModeFromString(String v) { + switch (v) { + case "light": + return ThemeMode.light; + case "dark": + return ThemeMode.dark; + default: + return ThemeMode.system; + } + } } -bool isDarkTheme() { - return "Y" == Get.find().getString("darkTheme"); +extension ParseToString on ThemeMode { + String toShortString() { + return toString().split('.').last; + } } final ButtonStyle flatButtonStyle = TextButton.styleFrom( diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index f79d58b50..5ab3b9a51 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -205,22 +205,28 @@ class _GeneralState extends State<_General> { } Widget theme() { - change() { - MyTheme.changeTo(!isDarkTheme()); + final current = MyTheme.getThemeModePreference().toShortString(); + onChanged(String value) { + MyTheme.changeDarkMode(MyTheme.themeModeFromString(value)); setState(() {}); } return _Card(title: 'Theme', children: [ - GestureDetector( - onTap: change, - child: Row( - children: [ - Checkbox(value: isDarkTheme(), onChanged: (_) => change()) - .marginOnly(right: 5), - Expanded(child: Text(translate('Dark Theme'))), - ], - ).marginOnly(left: _kCheckBoxLeftMargin), - ) + _Radio(context, + value: "light", + groupValue: current, + label: "Light", + onChanged: onChanged), + _Radio(context, + value: "dark", + groupValue: current, + label: "Dark", + onChanged: onChanged), + _Radio(context, + value: "system", + groupValue: current, + label: "Follow System", + onChanged: onChanged), ]); } diff --git a/flutter/lib/desktop/pages/port_forward_page.dart b/flutter/lib/desktop/pages/port_forward_page.dart index 49946cc56..28aa8d3cf 100644 --- a/flutter/lib/desktop/pages/port_forward_page.dart +++ b/flutter/lib/desktop/pages/port_forward_page.dart @@ -247,7 +247,7 @@ class _PortForwardPageState extends State height: _kRowHeight, decoration: BoxDecoration( color: index % 2 == 0 - ? isDarkTheme() + ? MyTheme.currentThemeMode() == ThemeMode.dark ? const Color(0xFF202020) : const Color(0xFFF4F5F6) : MyTheme.color(context).bg), diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 8f04846e9..a8df504d1 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -120,7 +120,7 @@ void runRemoteScreen(Map argument) async { title: 'RustDesk - Remote Desktop', theme: MyTheme.lightTheme, darkTheme: MyTheme.darkTheme, - themeMode: MyTheme.initialThemeMode(), + themeMode: MyTheme.currentThemeMode(), home: DesktopRemoteScreen( params: argument, ), @@ -146,7 +146,7 @@ void runFileTransferScreen(Map argument) async { title: 'RustDesk - File Transfer', theme: MyTheme.lightTheme, darkTheme: MyTheme.darkTheme, - themeMode: MyTheme.initialThemeMode(), + themeMode: MyTheme.currentThemeMode(), home: DesktopFileTransferScreen(params: argument), localizationsDelegates: const [ GlobalMaterialLocalizations.delegate, @@ -171,7 +171,7 @@ void runPortForwardScreen(Map argument) async { title: 'RustDesk - Port Forward', theme: MyTheme.lightTheme, darkTheme: MyTheme.darkTheme, - themeMode: MyTheme.initialThemeMode(), + themeMode: MyTheme.currentThemeMode(), home: DesktopPortForwardScreen(params: argument), localizationsDelegates: const [ GlobalMaterialLocalizations.delegate, @@ -196,7 +196,7 @@ void runConnectionManagerScreen() async { debugShowCheckedModeBanner: false, theme: MyTheme.lightTheme, darkTheme: MyTheme.darkTheme, - themeMode: MyTheme.initialThemeMode(), + themeMode: MyTheme.currentThemeMode(), localizationsDelegates: const [ GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, @@ -233,12 +233,21 @@ class _AppState extends State { void initState() { super.initState(); WidgetsBinding.instance.window.onPlatformBrightnessChanged = () { + final userPreference = MyTheme.getThemeModePreference(); + if (userPreference != ThemeMode.system) return; WidgetsBinding.instance.handlePlatformBrightnessChanged(); - var system = - WidgetsBinding.instance.platformDispatcher.platformBrightness; - var current = isDarkTheme() ? Brightness.dark : Brightness.light; - if (current != system) { - MyTheme.changeTo(system == Brightness.dark); + final systemIsDark = + WidgetsBinding.instance.platformDispatcher.platformBrightness == + Brightness.dark; + final ThemeMode to; + if (systemIsDark) { + to = ThemeMode.dark; + } else { + to = ThemeMode.light; + } + Get.changeThemeMode(to); + if (desktopType == DesktopType.main) { + bind.mainChangeTheme(dark: to.toShortString()); } }; } @@ -263,7 +272,7 @@ class _AppState extends State { title: 'RustDesk', theme: MyTheme.lightTheme, darkTheme: MyTheme.darkTheme, - themeMode: MyTheme.initialThemeMode(mainPage: true), + themeMode: MyTheme.currentThemeMode(), home: isDesktop ? const DesktopTabPage() : !isAndroid @@ -309,7 +318,7 @@ _registerEventHandler() { platformFFI.registerEventHandler('theme', 'theme', (evt) async { String? dark = evt['dark']; if (dark != null) { - MyTheme.changeTo(dark == 'true'); + MyTheme.changeDarkMode(MyTheme.themeModeFromString(dark)); } }); platformFFI.registerEventHandler('language', 'language', (_) async { diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index dc3a153d7..6f986ee78 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -60,7 +60,7 @@ class _SettingsState extends State with WidgetsBindingObserver { _enableAbr = enableAbrRes; } - _enableAbr = isDarkTheme(); + // _isDarkMode = MyTheme.currentDarkMode(); // TODO if (update) { setState(() {}); @@ -184,7 +184,7 @@ class _SettingsState extends State with WidgetsBindingObserver { onToggle: (v) { setState(() { _isDarkMode = !_isDarkMode; - MyTheme.changeTo(_isDarkMode); + // MyTheme.changeDarkMode(_isDarkMode); // TODO }); }, ) diff --git a/src/flutter.rs b/src/flutter.rs index 566576b5d..fea412c23 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -379,8 +379,8 @@ pub mod connection_manager { ); } - fn change_theme(&self, dark: bool) { - self.push_event("theme", vec![("dark", &dark.to_string())]); + fn change_theme(&self, dark: String) { + self.push_event("theme", vec![("dark", &dark)]); } fn change_language(&self) { diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 0579ac272..f41108895 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -692,10 +692,10 @@ fn main_broadcast_message(data: &HashMap<&str, &str>) { } } -pub fn main_change_theme(dark: bool) { +pub fn main_change_theme(dark: String) { main_broadcast_message(&HashMap::from([ ("name", "theme"), - ("dark", &dark.to_string()), + ("dark", &dark), ])); send_to_cm(&crate::ipc::Data::Theme(dark)); } diff --git a/src/ipc.rs b/src/ipc.rs index 2d841755b..36f6b9c1f 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -182,7 +182,7 @@ pub enum Data { #[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] Mouse(DataMouse), Control(DataControl), - Theme(bool), + Theme(String), Language(String), Empty, } diff --git a/src/ui/cm.rs b/src/ui/cm.rs index c5f64699a..959141da6 100644 --- a/src/ui/cm.rs +++ b/src/ui/cm.rs @@ -48,7 +48,7 @@ impl InvokeUiCM for SciterHandler { self.call("newMessage", &make_args!(id, text)); } - fn change_theme(&self, _dark: bool) { + fn change_theme(&self, _dark: String) { // TODO } diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index a7082e9ee..e4dbf80fb 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -61,7 +61,7 @@ pub trait InvokeUiCM: Send + Clone + 'static + Sized { fn new_message(&self, id: i32, text: String); - fn change_theme(&self, dark: bool); + fn change_theme(&self, dark: String); fn change_language(&self); } From 9489877c7810329e5e43ed577e3b96f126991ec7 Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 15 Sep 2022 17:31:28 +0800 Subject: [PATCH 0552/2015] video record Signed-off-by: 21pages --- Cargo.lock | 125 +++- .../desktop/pages/desktop_setting_page.dart | 56 ++ flutter/lib/desktop/pages/remote_page.dart | 1 + .../lib/desktop/widgets/remote_menubar.dart | 20 + flutter/lib/models/model.dart | 38 + flutter/pubspec.lock | 17 +- flutter/pubspec.yaml | 1 + libs/hbb_common/Cargo.toml | 1 + libs/hbb_common/src/lib.rs | 2 + libs/scrap/Cargo.toml | 2 +- libs/scrap/src/common/hwcodec.rs | 2 +- libs/scrap/src/common/mod.rs | 1 + libs/scrap/src/common/record.rs | 297 ++++++++ src/client.rs | 80 ++- src/client/io_loop.rs | 14 +- src/flutter_ffi.rs | 24 +- 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/fr.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 | 654 +++++++++--------- src/lang/pl.rs | 6 + src/lang/pt_PT.rs | 6 + src/lang/ptbr.rs | 6 + src/lang/ru.rs | 6 + src/lang/sk.rs | 6 + src/lang/template.rs | 6 + src/lang/tr.rs | 6 + src/lang/tw.rs | 6 + src/lang/vn.rs | 6 + src/server/video_service.rs | 29 +- src/ui.rs | 33 +- src/ui/common.css | 9 + src/ui/header.tis | 13 + src/ui/index.tis | 18 + src/ui/msgbox.tis | 8 + src/ui/remote.rs | 1 + src/ui/remote.tis | 3 + src/ui_interface.rs | 5 + src/ui_session_interface.rs | 4 + 48 files changed, 1186 insertions(+), 398 deletions(-) create mode 100644 libs/scrap/src/common/record.rs diff --git a/Cargo.lock b/Cargo.lock index 5627a861f..c43bccbbe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -58,6 +58,21 @@ dependencies = [ "atomic", ] +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + [[package]] name = "alsa" version = "0.6.0" @@ -420,6 +435,27 @@ dependencies = [ "once_cell", ] +[[package]] +name = "brotli" +version = "3.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ad2d4653bf5ca36ae797b1f4bb4dbddb60ce49ca4aed8a2ce4829f60425b80" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + [[package]] name = "bumpalo" version = "3.11.0" @@ -589,8 +625,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1" dependencies = [ "iana-time-zone", + "js-sys", "num-integer", "num-traits 0.2.15", + "time 0.1.44", + "wasm-bindgen", "winapi 0.3.9", ] @@ -1359,6 +1398,19 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" +[[package]] +name = "embed-resource" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc24ff8d764818e9ab17963b0593c535f077a513f565e75e4352d758bc4d8c0" +dependencies = [ + "cc", + "rustc_version 0.4.0", + "toml", + "vswhom", + "winreg 0.10.1", +] + [[package]] name = "encoding_rs" version = "0.8.31" @@ -1548,7 +1600,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e1c54951450cbd39f3dbcf1005ac413b49487dabf18a720ad2383eccfeffb92" dependencies = [ "memoffset", - "rustc_version", + "rustc_version 0.3.3", ] [[package]] @@ -1590,7 +1642,7 @@ dependencies = [ "regex", "rustversion", "thiserror", - "time", + "time 0.3.9", ] [[package]] @@ -1895,7 +1947,7 @@ checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" dependencies = [ "cfg-if 1.0.0", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", ] [[package]] @@ -2283,6 +2335,7 @@ version = "0.1.0" dependencies = [ "anyhow", "bytes", + "chrono", "confy", "directories-next", "dirs-next", @@ -2391,7 +2444,7 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hwcodec" version = "0.1.0" -source = "git+https://github.com/21pages/hwcodec#890204e0703a3d361fc7a45f035fe75c0575bb1d" +source = "git+https://github.com/21pages/hwcodec#097a476a0ee249e28d99573899ed4c9c0c01f884" dependencies = [ "bindgen", "cc", @@ -2822,6 +2875,12 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" +[[package]] +name = "md5" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" + [[package]] name = "memalloc" version = "0.1.0" @@ -2919,7 +2978,7 @@ checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf" dependencies = [ "libc", "log", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.36.1", ] @@ -4230,6 +4289,15 @@ dependencies = [ "semver 0.11.0", ] +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver 1.0.13", +] + [[package]] name = "rustdesk" version = "1.2.0" @@ -4305,6 +4373,16 @@ dependencies = [ "wol-rs", ] +[[package]] +name = "rustdesk-portable-packer" +version = "0.1.0" +dependencies = [ + "brotli", + "dirs", + "embed-resource", + "md5", +] + [[package]] name = "rustfft" version = "6.0.1" @@ -5050,6 +5128,17 @@ dependencies = [ "weezl", ] +[[package]] +name = "time" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi 0.3.9", +] + [[package]] name = "time" version = "0.3.9" @@ -5374,6 +5463,26 @@ dependencies = [ "thiserror", ] +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22025f6d8eb903ebf920ea6933b70b1e495be37e2cb4099e62c80454aaf57c39" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "waker-fn" version = "1.1.0" @@ -5401,6 +5510,12 @@ dependencies = [ "try-lock", ] +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 5ab3b9a51..231e001a2 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:io'; +import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hbb/common.dart'; @@ -9,6 +10,7 @@ import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/server_model.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; +import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher_string.dart'; import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart'; @@ -199,6 +201,7 @@ class _GeneralState extends State<_General> { abr(), hwcodec(), audio(context), + record(context), _Card(title: 'Language', children: [language()]), ], ).marginOnly(bottom: _kListViewBottomMargin)); @@ -290,6 +293,59 @@ class _GeneralState extends State<_General> { }); } + Widget record(BuildContext context) { + return _futureBuilder(future: () async { + String customDirectory = + await bind.mainGetOption(key: 'video-save-directory'); + String defaultDirectory = await bind.mainDefaultVideoSaveDirectory(); + String dir; + if (customDirectory.isNotEmpty) { + dir = customDirectory; + } else { + dir = defaultDirectory; + } + final canlaunch = await canLaunchUrl(Uri.file(dir)); + return {'dir': dir, 'canlaunch': canlaunch}; + }(), hasData: (data) { + Map map = data as Map; + String dir = map['dir']!; + bool canlaunch = map['canlaunch']! as bool; + + return _Card(title: 'Recording', children: [ + _OptionCheckBox(context, 'Automatically record incoming sessions', + 'allow-auto-record-incoming'), + Row( + children: [ + Text('${translate('Directory')}:'), + Expanded( + child: GestureDetector( + onTap: canlaunch ? () => launchUrl(Uri.file(dir)) : null, + child: Text( + dir, + softWrap: true, + style: + const TextStyle(decoration: TextDecoration.underline), + )).marginOnly(left: 10), + ), + ElevatedButton( + onPressed: () async { + String? selectedDirectory = await FilePicker.platform + .getDirectoryPath(initialDirectory: dir); + if (selectedDirectory != null) { + await bind.mainSetOption( + key: 'video-save-directory', + value: selectedDirectory); + setState(() {}); + } + }, + child: Text(translate('Change'))) + .marginOnly(left: 5), + ], + ).marginOnly(left: _kContentHMargin), + ]); + }); + } + Widget language() { return _futureBuilder(future: () async { String langs = await bind.mainGetLangs(); diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index dad286ee7..1225e5a66 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -166,6 +166,7 @@ class _RemotePageState extends State ChangeNotifierProvider.value(value: _ffi.imageModel), ChangeNotifierProvider.value(value: _ffi.cursorModel), ChangeNotifierProvider.value(value: _ffi.canvasModel), + ChangeNotifierProvider.value(value: _ffi.recordingModel), ], child: buildBody(context))); } diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 070ad217b..dd0a0bf05 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hbb/models/chat_model.dart'; import 'package:get/get.dart'; +import 'package:provider/provider.dart'; import 'package:rxdart/rxdart.dart' as rxdart; import '../../common.dart'; @@ -134,6 +135,7 @@ class _RemoteMenubarState extends State { if (!isWeb) { menubarItems.add(_buildChat(context)); } + menubarItems.add(_buildRecording(context)); menubarItems.add(_buildClose(context)); return PopupMenuTheme( data: const PopupMenuThemeData( @@ -351,6 +353,24 @@ class _RemoteMenubarState extends State { ); } + Widget _buildRecording(BuildContext context) { + return Consumer( + builder: (context, value, child) => IconButton( + tooltip: value.start + ? translate('Stop session recording') + : translate('Start session recording'), + onPressed: () async { + await value.toggle(); + }, + icon: Icon( + value.start + ? Icons.pause_circle_filled + : Icons.videocam_outlined, + color: _MenubarTheme.commonColor, + ), + )); + } + Widget _buildClose(BuildContext context) { return IconButton( tooltip: translate('Close'), diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 237378166..8bc3e8083 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -197,6 +197,7 @@ class FfiModel with ChangeNotifier { _display.height = int.parse(evt['height']); if (old != _pi.currentDisplay) { parent.target?.cursorModel.updateDisplayOrigin(_display.x, _display.y); + parent.target?.recordingModel.switchDisplay(); } // remote is mobile, and orientation changed @@ -972,6 +973,41 @@ class QualityMonitorModel with ChangeNotifier { } } +class RecordingModel with ChangeNotifier { + WeakReference parent; + RecordingModel(this.parent); + bool _start = false; + get start => _start; + + switchDisplay() { + if (!isDesktop || !_start) return; + var id = parent.target?.id; + int? width = parent.target?.canvasModel.getDisplayWidth(); + int? height = parent.target?.canvasModel.getDisplayWidth(); + if (id == null || width == null || height == null) return; + bind.sessionRecordScreen( + id: id, start: _start, width: width, height: height); + } + + Future toggle() async { + if (!isDesktop) return; + var id = parent.target?.id; + int? width = parent.target?.canvasModel.getDisplayWidth(); + int? height = parent.target?.canvasModel.getDisplayWidth(); + if (id == null || width == null || height == null) return; + + await bind.sessionRecordScreen( + id: id, start: !_start, width: width, height: height); + _start = !_start; + notifyListeners(); + if (_start) { + Future.delayed(const Duration(milliseconds: 100), () { + bind.sessionRefresh(id: id); + }); + } + } +} + /// Mouse button enum. enum MouseButtons { left, right, wheel } @@ -1013,6 +1049,7 @@ class FFI { late final AbModel abModel; // global late final UserModel userModel; // global late final QualityMonitorModel qualityMonitorModel; // session + late final RecordingModel recordingModel; // recording FFI() { imageModel = ImageModel(WeakReference(this)); @@ -1025,6 +1062,7 @@ class FFI { abModel = AbModel(WeakReference(this)); userModel = UserModel(WeakReference(this)); qualityMonitorModel = QualityMonitorModel(WeakReference(this)); + recordingModel = RecordingModel(WeakReference(this)); } /// Send a mouse tap event(down and up). diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 79c25ee5b..a97c9adfd 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -140,7 +140,7 @@ packages: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.2.1" charcode: dependency: transitive description: @@ -161,7 +161,7 @@ packages: name: clock url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.1.1" code_builder: dependency: transitive description: @@ -325,6 +325,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "6.1.4" + file_picker: + dependency: "direct main" + description: + name: file_picker + url: "https://pub.dartlang.org" + source: hosted + version: "5.1.0" fixnum: dependency: transitive description: @@ -588,7 +595,7 @@ packages: name: material_color_utilities url: "https://pub.dartlang.org" source: hosted - version: "0.1.4" + version: "0.1.5" menu_base: dependency: transitive description: @@ -602,7 +609,7 @@ packages: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.7.0" + version: "1.8.0" mime: dependency: transitive description: @@ -679,7 +686,7 @@ packages: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.1" + version: "1.8.2" path_provider: dependency: "direct main" description: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 05c711dcf..d50f6f970 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -80,6 +80,7 @@ dependencies: desktop_drop: ^0.3.3 scroll_pos: ^0.3.0 rxdart: ^0.27.5 + file_picker: ^5.1.0 flutter_improved_scrolling: ^0.0.3 # currently, we use flutter 3.0.5 for windows build, latest for other builds. # diff --git a/libs/hbb_common/Cargo.toml b/libs/hbb_common/Cargo.toml index 8d4f36b2d..e7377608b 100644 --- a/libs/hbb_common/Cargo.toml +++ b/libs/hbb_common/Cargo.toml @@ -30,6 +30,7 @@ filetime = "0.2" sodiumoxide = "0.2" regex = "1.4" tokio-socks = { git = "https://github.com/open-trade/tokio-socks" } +chrono = "0.4" [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 50fcc07b2..02acfd9ff 100644 --- a/libs/hbb_common/src/lib.rs +++ b/libs/hbb_common/src/lib.rs @@ -38,6 +38,8 @@ pub use tokio_socks; pub use tokio_socks::IntoTargetAddr; pub use tokio_socks::TargetAddr; pub mod password_security; +pub use chrono; +pub use directories_next; #[cfg(feature = "quic")] pub type Stream = quic::Connection; diff --git a/libs/scrap/Cargo.toml b/libs/scrap/Cargo.toml index c980d9d49..e2eb43177 100644 --- a/libs/scrap/Cargo.toml +++ b/libs/scrap/Cargo.toml @@ -20,6 +20,7 @@ libc = "0.2" num_cpus = "1.13" lazy_static = "1.4" hbb_common = { path = "../hbb_common" } +webm = "1.0" [dependencies.winapi] version = "0.3" @@ -37,7 +38,6 @@ ndk = { version = "0.7", features = ["media"], optional = true} [target.'cfg(not(target_os = "android"))'.dev-dependencies] repng = "0.2" docopt = "1.1" -webm = "1.0" serde = {version="1.0", features=["derive"]} quest = "0.3" diff --git a/libs/scrap/src/common/hwcodec.rs b/libs/scrap/src/common/hwcodec.rs index 4fdef5462..32bcbd4a2 100644 --- a/libs/scrap/src/common/hwcodec.rs +++ b/libs/scrap/src/common/hwcodec.rs @@ -28,7 +28,7 @@ const CFG_KEY_ENCODER: &str = "bestHwEncoders"; const CFG_KEY_DECODER: &str = "bestHwDecoders"; const DEFAULT_PIXFMT: AVPixelFormat = AVPixelFormat::AV_PIX_FMT_YUV420P; -const DEFAULT_TIME_BASE: [i32; 2] = [1, 30]; +pub const DEFAULT_TIME_BASE: [i32; 2] = [1, 30]; const DEFAULT_GOP: i32 = 60; const DEFAULT_HW_QUALITY: Quality = Quality_Default; const DEFAULT_RC: RateContorl = RC_DEFAULT; diff --git a/libs/scrap/src/common/mod.rs b/libs/scrap/src/common/mod.rs index 78ea7c888..fe817c00a 100644 --- a/libs/scrap/src/common/mod.rs +++ b/libs/scrap/src/common/mod.rs @@ -39,6 +39,7 @@ pub use self::convert::*; pub const STRIDE_ALIGN: usize = 64; // commonly used in libvpx vpx_img_alloc caller pub const HW_STRIDE_ALIGN: usize = 0; // recommended by av_frame_get_buffer +pub mod record; mod vpx; #[inline] diff --git a/libs/scrap/src/common/record.rs b/libs/scrap/src/common/record.rs new file mode 100644 index 000000000..d757fe8cd --- /dev/null +++ b/libs/scrap/src/common/record.rs @@ -0,0 +1,297 @@ +#[cfg(feature = "hwcodec")] +use hbb_common::anyhow::anyhow; +use hbb_common::{ + bail, chrono, + config::Config, + directories_next, + message_proto::{message, video_frame, EncodedVideoFrame, Message}, + ResultType, +}; +#[cfg(feature = "hwcodec")] +use hwcodec::mux::{MuxContext, Muxer}; +use std::{ + fs::{File, OpenOptions}, + io, + time::Instant, +}; +use std::{ + ops::{Deref, DerefMut}, + path::PathBuf, +}; +use webm::mux::{self, Segment, Track, VideoTrack, Writer}; + +const MIN_SECS: u64 = 1; + +#[derive(Debug, Clone, PartialEq)] +pub enum RecodeCodecID { + VP9, + H264, + H265, +} + +#[derive(Debug, Clone)] +pub struct RecorderContext { + pub id: String, + pub filename: String, + pub width: usize, + pub height: usize, + pub codec_id: RecodeCodecID, +} + +impl RecorderContext { + pub fn set_filename(&mut self) -> ResultType<()> { + let mut dir = Config::get_option("video-save-directory"); + if !dir.is_empty() { + if !PathBuf::from(&dir).exists() { + std::fs::create_dir_all(&dir)?; + } + } else { + dir = Self::default_save_directory(); + if !dir.is_empty() && !PathBuf::from(&dir).exists() { + std::fs::create_dir_all(&dir)?; + } + } + let file = self.id.clone() + + &chrono::Local::now().format("_%Y%m%d%H%M%S").to_string() + + if self.codec_id == RecodeCodecID::VP9 { + ".webm" + } else { + ".mp4" + }; + self.filename = PathBuf::from(&dir).join(file).to_string_lossy().to_string(); + Ok(()) + } + + pub fn default_save_directory() -> String { + if let Some(user) = directories_next::UserDirs::new() { + if let Some(video_dir) = user.video_dir() { + return video_dir.join("RustDesk").to_string_lossy().to_string(); + } + } + "".to_owned() + } +} + +unsafe impl Send for Recorder {} +unsafe impl Sync for Recorder {} + +pub trait RecorderApi { + fn new(ctx: RecorderContext) -> ResultType + where + Self: Sized; + fn write_video(&mut self, frame: &EncodedVideoFrame) -> bool; +} + +pub struct Recorder { + pub inner: Box, + ctx: RecorderContext, +} + +impl Deref for Recorder { + type Target = Box; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl DerefMut for Recorder { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} + +impl Recorder { + pub fn new(mut ctx: RecorderContext) -> ResultType { + ctx.set_filename()?; + let recorder = match ctx.codec_id { + RecodeCodecID::VP9 => Recorder { + inner: Box::new(WebmRecorder::new(ctx.clone())?), + ctx, + }, + #[cfg(feature = "hwcodec")] + _ => Recorder { + inner: Box::new(HwRecorder::new(ctx.clone())?), + ctx, + }, + #[cfg(not(feature = "hwcodec"))] + _ => bail!("unsupported codec type"), + }; + Ok(recorder) + } + + fn change(&mut self, mut ctx: RecorderContext) -> ResultType<()> { + ctx.set_filename()?; + self.inner = match ctx.codec_id { + RecodeCodecID::VP9 => Box::new(WebmRecorder::new(ctx.clone())?), + #[cfg(feature = "hwcodec")] + _ => Box::new(HwRecorder::new(ctx.clone())?), + #[cfg(not(feature = "hwcodec"))] + _ => bail!("unsupported codec type"), + }; + self.ctx = ctx; + Ok(()) + } + + pub fn write_message(&mut self, msg: &Message) { + if let Some(message::Union::VideoFrame(vf)) = &msg.union { + if let Some(frame) = &vf.union { + self.write_frame(frame).ok(); + } + } + } + + pub fn write_frame(&mut self, frame: &video_frame::Union) -> ResultType<()> { + match frame { + video_frame::Union::Vp9s(vp9s) => { + if self.ctx.codec_id != RecodeCodecID::VP9 { + self.change(RecorderContext { + codec_id: RecodeCodecID::VP9, + ..self.ctx.clone() + })?; + } + vp9s.frames.iter().map(|f| self.write_video(f)).count(); + } + #[cfg(feature = "hwcodec")] + video_frame::Union::H264s(h264s) => { + if self.ctx.codec_id != RecodeCodecID::H264 { + self.change(RecorderContext { + codec_id: RecodeCodecID::H264, + ..self.ctx.clone() + })?; + } + if self.ctx.codec_id == RecodeCodecID::H264 { + h264s.frames.last().map(|f| self.write_video(f)); + } + } + #[cfg(feature = "hwcodec")] + video_frame::Union::H265s(h265s) => { + if self.ctx.codec_id != RecodeCodecID::H265 { + self.change(RecorderContext { + codec_id: RecodeCodecID::H265, + ..self.ctx.clone() + })?; + } + if self.ctx.codec_id == RecodeCodecID::H265 { + h265s.frames.last().map(|f| self.write_video(f)); + } + } + _ => bail!("unsupported frame type"), + } + Ok(()) + } +} + +struct WebmRecorder { + vt: VideoTrack, + webm: Option>>, + ctx: RecorderContext, + key: bool, + written: bool, + start: Instant, +} + +impl RecorderApi for WebmRecorder { + fn new(ctx: RecorderContext) -> ResultType { + let out = match { + OpenOptions::new() + .write(true) + .create_new(true) + .open(&ctx.filename) + } { + Ok(file) => file, + Err(ref e) if e.kind() == io::ErrorKind::AlreadyExists => File::create(&ctx.filename)?, + Err(e) => return Err(e.into()), + }; + let mut webm = match mux::Segment::new(mux::Writer::new(out)) { + Some(v) => v, + None => bail!("Failed to create webm mux"), + }; + let vt = webm.add_video_track( + ctx.width as _, + ctx.height as _, + None, + mux::VideoCodecId::VP9, + ); + Ok(WebmRecorder { + vt, + webm: Some(webm), + ctx, + key: false, + written: false, + start: Instant::now(), + }) + } + + fn write_video(&mut self, frame: &EncodedVideoFrame) -> bool { + if frame.key { + self.key = true; + } + if self.key { + let ok = self + .vt + .add_frame(&frame.data, frame.pts as u64 * 1_000_000, frame.key); + if ok { + self.written = true; + } + ok + } else { + false + } + } +} + +impl Drop for WebmRecorder { + fn drop(&mut self) { + std::mem::replace(&mut self.webm, None).map_or(false, |webm| webm.finalize(None)); + if !self.written || self.start.elapsed().as_secs() < MIN_SECS { + std::fs::remove_file(&self.ctx.filename).ok(); + } + } +} + +#[cfg(feature = "hwcodec")] +struct HwRecorder { + muxer: Muxer, + ctx: RecorderContext, + written: bool, + start: Instant, +} + +#[cfg(feature = "hwcodec")] +impl RecorderApi for HwRecorder { + fn new(ctx: RecorderContext) -> ResultType { + let muxer = Muxer::new(MuxContext { + filename: ctx.filename.clone(), + width: ctx.width, + height: ctx.height, + is265: ctx.codec_id == RecodeCodecID::H265, + framerate: crate::hwcodec::DEFAULT_TIME_BASE[1] as _, + }) + .map_err(|_| anyhow!("Failed to create hardware muxer"))?; + Ok(HwRecorder { + muxer, + ctx, + written: false, + start: Instant::now(), + }) + } + + fn write_video(&mut self, frame: &EncodedVideoFrame) -> bool { + let ok = self.muxer.write_video(&frame.data, frame.pts).is_ok(); + if ok { + self.written = true; + } + ok + } +} + +#[cfg(feature = "hwcodec")] +impl Drop for HwRecorder { + fn drop(&mut self) { + self.muxer.write_tail().ok(); + if !self.written || self.start.elapsed().as_secs() < MIN_SECS { + std::fs::remove_file(&self.ctx.filename).ok(); + } + } +} diff --git a/src/client.rs b/src/client.rs index baf06833a..c70956b63 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,10 +1,3 @@ -use std::{ - collections::HashMap, - net::SocketAddr, - ops::{Deref, Not}, - sync::{atomic::AtomicBool, mpsc, Arc, Mutex, RwLock}, -}; -use std::sync::atomic::Ordering; pub use async_trait::async_trait; #[cfg(not(any(target_os = "android", target_os = "linux")))] use cpal::{ @@ -13,6 +6,13 @@ use cpal::{ }; use magnum_opus::{Channels::*, Decoder as AudioDecoder}; use sha2::{Digest, Sha256}; +use std::sync::atomic::Ordering; +use std::{ + collections::HashMap, + net::SocketAddr, + ops::{Deref, Not}, + sync::{atomic::AtomicBool, mpsc, Arc, Mutex, RwLock}, +}; use uuid::Uuid; pub use file_trait::FileManager; @@ -39,6 +39,7 @@ pub use helper::LatencyController; pub use helper::*; use scrap::{ codec::{Decoder, DecoderCfg}, + record::{Recorder, RecorderContext}, VpxDecoderConfig, VpxVideoCodecId, }; @@ -154,8 +155,7 @@ impl Client { return Err(err); } } - Ok(x) => { - Ok(x)}, + Ok(x) => Ok(x), } } @@ -798,6 +798,8 @@ pub struct VideoHandler { decoder: Decoder, latency_controller: Arc>, pub rgb: Vec, + recorder: Arc>>, + record: bool, } impl VideoHandler { @@ -812,6 +814,8 @@ impl VideoHandler { }), latency_controller, rgb: Default::default(), + recorder: Default::default(), + record: false, } } @@ -825,32 +829,21 @@ impl VideoHandler { .update_video(vf.timestamp); } match &vf.union { - Some(frame) => self.decoder.handle_video_frame(frame, &mut self.rgb), + Some(frame) => { + let res = self.decoder.handle_video_frame(frame, &mut self.rgb); + if self.record { + self.recorder + .lock() + .unwrap() + .as_mut() + .map(|r| r.write_frame(frame)); + } + res + } _ => Ok(false), } } - /// Handle a VP9S frame. - // pub fn handle_vp9s(&mut self, vp9s: &VP9s) -> ResultType { - // let mut last_frame = Image::new(); - // for vp9 in vp9s.frames.iter() { - // for frame in self.decoder.decode(&vp9.data)? { - // drop(last_frame); - // last_frame = frame; - // } - // } - // for frame in self.decoder.flush()? { - // drop(last_frame); - // last_frame = frame; - // } - // if last_frame.is_null() { - // Ok(false) - // } else { - // last_frame.rgb(1, true, &mut self.rgb); - // Ok(true) - // } - // } - /// Reset the decoder. pub fn reset(&mut self) { self.decoder = Decoder::new(DecoderCfg { @@ -860,6 +853,24 @@ impl VideoHandler { }, }); } + + /// Start or stop screen record. + pub fn record_screen(&mut self, start: bool, w: i32, h: i32, id: String) { + self.record = false; + if start { + self.recorder = Recorder::new(RecorderContext { + id, + filename: "".to_owned(), + width: w as _, + height: h as _, + codec_id: scrap::record::RecodeCodecID::VP9, + }) + .map_or(Default::default(), |r| Arc::new(Mutex::new(Some(r)))); + } else { + self.recorder = Default::default(); + } + self.record = start; + } } /// Login config handler for [`Client`]. @@ -1395,6 +1406,7 @@ pub enum MediaData { AudioFrame(AudioFrame), AudioFormat(AudioFormat), Reset, + RecordScreen(bool, i32, i32, String), } pub type MediaSender = mpsc::Sender; @@ -1429,6 +1441,9 @@ where MediaData::Reset => { video_handler.reset(); } + MediaData::RecordScreen(start, w, h, id) => { + video_handler.record_screen(start, w, h, id) + } _ => {} } } else { @@ -1703,6 +1718,7 @@ pub enum Data { SetConfirmOverrideFile((i32, i32, bool, bool, bool)), AddJob((i32, String, String, i32, bool, bool)), ResumeJob((i32, bool)), + RecordScreen(bool, i32, i32, String), } /// Keycode for key events. @@ -1892,4 +1908,4 @@ fn decode_id_pk(signed: &[u8], key: &sign::PublicKey) -> ResultType<(String, [u8 pub fn disable_keyboard_listening() { crate::ui_session_interface::KEYBOARD_HOOKED.store(false, Ordering::SeqCst); -} \ No newline at end of file +} diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index cc2ca1dae..1cf89f173 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -601,6 +601,11 @@ impl Remote { } } } + Data::RecordScreen(start, w, h, id) => { + let _ = self + .video_sender + .send(MediaData::RecordScreen(start, w, h, id)); + } _ => {} } true @@ -794,13 +799,8 @@ impl Remote { fs::transform_windows_path(&mut entries); } } - self.handler.update_folder_files( - fd.id, - &entries, - fd.path, - false, - false, - ); + self.handler + .update_folder_files(fd.id, &entries, fd.path, false, false); if let Some(job) = fs::get_job(fd.id, &mut self.write_jobs) { log::info!("job set_files: {:?}", entries); job.set_files(entries); diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index f41108895..1404627f5 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -19,13 +19,13 @@ use crate::ui_interface; #[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::ui_interface::get_sound_inputs; use crate::ui_interface::{ - change_id, check_mouse_time, check_super_user_permission, discover, forget_password, - get_api_server, get_app_name, get_async_job_status, get_connect_status, get_fav, get_id, - get_lan_peers, get_langs, get_license, get_local_option, get_mouse_time, get_option, - get_options, get_peer, get_peer_option, get_socks, get_uuid, get_version, has_hwcodec, - has_rendezvous_service, post_request, send_to_cm, set_local_option, set_option, set_options, - set_peer_option, set_permanent_password, set_socks, store_fav, test_if_valid_server, - update_temporary_password, using_public_server, + change_id, check_mouse_time, check_super_user_permission, default_video_save_directory, + discover, forget_password, get_api_server, get_app_name, get_async_job_status, + get_connect_status, get_fav, get_id, get_lan_peers, get_langs, get_license, get_local_option, + get_mouse_time, get_option, get_options, get_peer, get_peer_option, get_socks, get_uuid, + get_version, has_hwcodec, has_rendezvous_service, post_request, send_to_cm, set_local_option, + set_option, set_options, set_peer_option, set_permanent_password, set_socks, store_fav, + test_if_valid_server, update_temporary_password, using_public_server, }; use crate::{ client::file_trait::FileManager, @@ -162,6 +162,12 @@ pub fn session_refresh(id: String) { } } +pub fn session_record_screen(id: String, start: bool, width: usize, height: usize) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.record_screen(start, width as _, height as _); + } +} + pub fn session_reconnect(id: String) { if let Some(session) = SESSIONS.read().unwrap().get(&id) { session.reconnect(); @@ -705,6 +711,10 @@ pub fn main_change_language(lang: String) { send_to_cm(&crate::ipc::Data::Language(lang)); } +pub fn main_default_video_save_directory() -> String { + default_video_save_directory() +} + pub fn session_add_port_forward( id: String, local_port: i32, diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 664d6f05b..65c7b529d 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -346,5 +346,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable RDP", "允许RDP访问"), ("Pin menubar", "固定菜单栏"), ("Unpin menubar", "取消固定菜单栏"), + ("Recording", "录屏"), + ("Directory", "目录"), + ("Automatically record incoming sessions", "自动录制来访会话"), + ("Change", "更改"), + ("Start session recording", "开始录屏"), + ("Stop session recording", "结束录屏"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index ace56788f..a271e2446 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -346,5 +346,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable RDP", ""), ("Pin menubar", "Připnout panel nabídek"), ("Unpin menubar", "Odepnout panel nabídek"), + ("Recording", ""), + ("Directory", ""), + ("Automatically record incoming sessions", ""), + ("Change", ""), + ("Start session recording", ""), + ("Stop session recording", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 27724f7b3..77f585390 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -346,5 +346,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable RDP", ""), ("Pin menubar", "Fastgør menulinjen"), ("Unpin menubar", "Frigør menulinjen"), + ("Recording", ""), + ("Directory", ""), + ("Automatically record incoming sessions", ""), + ("Change", ""), + ("Start session recording", ""), + ("Stop session recording", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 8d90be381..27e04717e 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -346,5 +346,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable RDP", ""), ("Pin menubar", "Pin-Menüleiste"), ("Unpin menubar", "Menüleiste lösen"), + ("Recording", ""), + ("Directory", ""), + ("Automatically record incoming sessions", ""), + ("Change", ""), + ("Start session recording", ""), + ("Stop session recording", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 6c7bb5aa8..a3add20f9 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -346,5 +346,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable RDP", ""), ("Pin menubar", "Alpingla menubreto"), ("Unpin menubar", "Malfiksi menubreton"), + ("Recording", ""), + ("Directory", ""), + ("Automatically record incoming sessions", ""), + ("Change", ""), + ("Start session recording", ""), + ("Stop session recording", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index c8296ced5..049a8c428 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -359,5 +359,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable RDP", ""), ("Pin menubar", "Pin barra de menú"), ("Unpin menubar", "Desbloquear barra de menú"), + ("Recording", ""), + ("Directory", ""), + ("Automatically record incoming sessions", ""), + ("Change", ""), + ("Start session recording", ""), + ("Stop session recording", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index d9a42e934..033e08c4c 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -346,5 +346,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable RDP", ""), ("Pin menubar", "Épingler la barre de menus"), ("Unpin menubar", "Détacher la barre de menu"), + ("Recording", ""), + ("Directory", ""), + ("Automatically record incoming sessions", ""), + ("Change", ""), + ("Start session recording", ""), + ("Stop session recording", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index b35224c03..4cff53b2d 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -346,5 +346,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable RDP", ""), ("Pin menubar", "Menüsor rögzítése"), ("Unpin menubar", "Menüsor rögzítésének feloldása"), + ("Recording", ""), + ("Directory", ""), + ("Automatically record incoming sessions", ""), + ("Change", ""), + ("Start session recording", ""), + ("Stop session recording", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 657014141..dd4adf2bc 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -359,5 +359,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable RDP", ""), ("Pin menubar", "Pin menubar"), ("Unpin menubar", "Unpin menubar"), + ("Recording", ""), + ("Directory", ""), + ("Automatically record incoming sessions", ""), + ("Change", ""), + ("Start session recording", ""), + ("Stop session recording", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 8f6dfb3d9..dd941b30b 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -345,5 +345,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable RDP", ""), ("Pin menubar", "Blocca la barra dei menu"), ("Unpin menubar", "Sblocca la barra dei menu"), + ("Recording", ""), + ("Directory", ""), + ("Automatically record incoming sessions", ""), + ("Change", ""), + ("Start session recording", ""), + ("Stop session recording", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 6d0a2a2f7..2d76c93db 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -343,5 +343,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable RDP", ""), ("Pin menubar", "メニューバーを固定する"), ("Unpin menubar", "メニューバーのピン留めを外す"), + ("Recording", ""), + ("Directory", ""), + ("Automatically record incoming sessions", ""), + ("Change", ""), + ("Start session recording", ""), + ("Stop session recording", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index ca939e2b8..5897dc690 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -340,5 +340,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable RDP", ""), ("Pin menubar", "핀 메뉴 바"), ("Unpin menubar", "메뉴 모음 고정 해제"), + ("Recording", ""), + ("Directory", ""), + ("Automatically record incoming sessions", ""), + ("Change", ""), + ("Start session recording", ""), + ("Stop session recording", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 720b7109f..c2f5f2cf0 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -1,325 +1,331 @@ lazy_static::lazy_static! { - pub static ref T: std::collections::HashMap<&'static str, &'static str> = - [ - ("Status", "Күй"), - ("Your Desktop", "Сіздің Жұмыс үстеліңіз"), - ("desk_tip", "Сіздің Жұмыс үстеліңіз осы ID мен құпия сөз арқылы қолжетімді"), - ("Password", "Құпия сөз"), - ("Ready", "Дайын"), - ("Established", "Қосылды"), - ("connecting_status", "RustDesk желісіне қосылуда..."), - ("Enable Service", "Сербесті қосу"), - ("Start Service", "Сербесті іске қосу"), - ("Service is running", "Сербес істеуде"), - ("Service is not running", "Сербес істемеуде"), - ("not_ready_status", "Дайын емес. Қосылымды тексеруді өтінеміз"), - ("Control Remote Desktop", "Қашықтағы Жұмыс үстелін Басқару"), - ("Transfer File", "Файыл Тасымалдау"), - ("Connect", "Қосылу"), - ("Recent Sessions", "Соңғы Сештер"), - ("Address Book", "Мекенжай Кітабы"), - ("Confirmation", "Мақұлдау"), - ("TCP Tunneling", "TCP тунелдеу"), - ("Remove", "Жою"), - ("Refresh random password", "Кездейсоқ құпия сөзді жаңарту"), - ("Set your own password", "Өз құпия сөзіңізді орнатыңыз"), - ("Enable Keyboard/Mouse", "Пернетақта/Тінтуірді қосу"), - ("Enable Clipboard", "Көшіру-тақтасын қосу"), - ("Enable File Transfer", "Файыл Тасымалдауды қосу"), - ("Enable TCP Tunneling", "TCP тунелдеуді қосу"), - ("IP Whitelisting", "IP Ақ-тізімі"), - ("ID/Relay Server", "ID/Relay сербері"), - ("Stop service", "Сербесті тоқтату"), - ("Change ID", "ID ауыстыру"), - ("Website", "Web-сайт"), - ("About", "Туралы"), - ("Mute", "Дыбыссыздандыру"), - ("Audio Input", "Аудио Еңгізу"), - ("Enhancements", "Жақсартулар"), - ("Hardware Codec", "Hardware Codec"), - ("Adaptive Bitrate", "Adaptive Bitrate"), - ("ID Server", "ID Сербері"), - ("Relay Server", "Relay Сербері"), - ("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", "Қолжетімсіз"), - ("Too frequent", "Тым жиі"), - ("Cancel", "Болдырмау"), - ("Skip", "Өткізіп жіберу"), - ("Close", "Жабу"), - ("Retry", "Қайтадан көру"), - ("OK", "OK"), - ("Password Required", "Құпия сөз Қажет"), - ("Please enter your password", "Құпия сөзіңізді еңгізуді өтінеміз"), - ("Remember password", "Құпия сөзді есте сақтау"), - ("Wrong Password", "Бұрыс Құпия сөз"), - ("Do you want to enter again?", "Қайтадан кіргіңіз келеді ме?"), - ("Connection Error", "Қосылым Қатесі"), - ("Error", "Қате"), - ("Reset by the peer", "Пир қалпына келтірді"), - ("Connecting...", "Қосылуда..."), - ("Connection in progress. Please wait.", "Қосылым барысында. Күтуді өтінеміз"), - ("Please try 1 minute later", "1 минуттан соң қайта көріңіз"), - ("Login Error", "Кіру Қатесі"), - ("Successful", "Сәтті"), - ("Connected, waiting for image...", "Қосылды, сурет күтілуде..."), - ("Name", "Ат"), - ("Type", "Түр"), - ("Modified", "Өзгертілді"), - ("Size", "Өлшем"), - ("Show Hidden Files", "Жасырын Файылдарды Көрсету"), - ("Receive", "Қабылдау"), - ("Send", "Жіберу"), - ("Refresh File", "Файылды жаңарту"), - ("Local", "Лақал"), - ("Remote", "Қашықтағы"), - ("Remote Computer", "Қашықтағы Қампұтыр"), - ("Local Computer", "Лақал Қампұтыр"), - ("Confirm Delete", "Жоюды Растау"), - ("Delete", "Жою"), - ("Properties", "Қасиеттер"), - ("Multi Select", "Көптік таңдау"), - ("Empty Directory", "Бос Бума"), - ("Not an empty directory", "Бос бума емес"), - ("Are you sure you want to delete this file?", "Бұл файылды жоюға сенімдісіз бе?"), - ("Are you sure you want to delete this empty directory?", "Бұл бос буманы жоюға сенімдісіз бе?"), - ("Are you sure you want to delete the file of this directory?", "Бұл буманың файылын жоюға сенімдісіз бе?"), - ("Do this for all conflicts", "Мұны барлық қанпілектер үшін жасау"), - ("This is irreversible!", "Бұл қайтымсыз!"), - ("Deleting", "Жойылу"), - ("files", "файылдар"), - ("Waiting", "Күту"), - ("Finished", "Аяқталды"), - ("Speed", "Жылдамдық"), - ("Custom Image Quality", "Теңшеулі Сурет Сапасы"), - ("Privacy mode", "Құпиялылық Модасы"), - ("Block user input", "Қолданушы еңгізуін бұғаттау"), - ("Unblock user input", "Қолданушы еңгізуін бұғаттан шығару"), - ("Adjust Window", "Терезені Реттеу"), - ("Original", "Түпнұсқа"), - ("Shrink", "Қысу"), - ("Stretch", "Созу"), - ("Scrollbar", "Scrollbar"), - ("ScrollAuto", "ScrollAuto"), - ("Good image quality", "Жақсы сурет сапасы"), - ("Balanced", "Теңдестірілген"), - ("Optimize reaction time", "Реакция уақытын оңтайландыру"), - ("Custom", "Теңшеулі"), - ("Show remote cursor", "Қашықтағы курсорды көрсету"), - ("Show quality monitor", "Сапа мониторын көрсету"), - ("Disable clipboard", "Көшіру-тақтасын өшіру"), - ("Lock after session end", "Сеш аяқталған соң құлыптау"), - ("Insert", "Кірістіру"), - ("Insert Lock", "Кірістіруді Құлыптау"), - ("Refresh", "Жаңарту"), - ("ID does not exist", "ID табылмады"), - ("Failed to connect to rendezvous server", "Rendezvous серберіне қосылу сәтсіз"), - ("Please try later", "Кейінірек қайта көруді өтінеміз"), - ("Remote desktop is offline", "Қашықтағы жұмыс үстелі офлайн күйінде"), - ("Key mismatch", "Кілт сәйкессіздігі"), - ("Timeout", "Үзіліс"), - ("Failed to connect to relay server", "Relay серберіне қосылу сәтсіз"), - ("Failed to connect via rendezvous server", "Rendezvous сербері арқылы қосылу сәтсіз"), - ("Failed to connect via relay server", "Relay сербері арқылы қосылу сәтсіз"), - ("Failed to make direct connection to remote desktop", "Қашықтағы жұмыс үстеліне тікелей қосылым жасау сәтсіз"), - ("Set Password", "Құпия сөзді Орнату"), - ("OS Password", "OS Құпия сөзі"), - ("install_tip", "UAC кесірінен, RustDesk кейбірде қашықтағы жақ ретінде дұрыс жұмыс істей алмайды. UAC'пен қиындықты болдырмау үшін, төмендегі батырманы басып RustDesk'ті жүйеге орнатыңыз."), - ("Click to upgrade", "Жаңғырту үшін басыңыз"), - ("Click to download", "Жүктеу үшін басыңыз"), - ("Click to update", "Жаңарту үшін басыңыз"), - ("Configure", "Қалыптау"), - ("config_acc", "Сіздің Жұмыс үстеліңізді қашықтан басқару үшін, RustDesk'ке \"Қолжетімділік\" рұқсаттарын беруіңіз керек."), - ("config_screen", "Сіздің Жұмыс үстеліңізге қашықтан қол жеткізу үшін, RustDesk'ке \"Екіренді Жазу\" рұқсаттарын беруіңіз керек."), - ("Installing ...", "Орнатылу..."), - ("Install", "Орнату"), - ("Installation", "Орнатылу"), - ("Installation Path", "Орнатылу Жолы"), - ("Create start menu shortcuts", "Бастау мәзірі белгішесің жасау"), - ("Create desktop icon", "Жұмыс үстелі белгішесің жасау"), - ("agreement_tip", "Орнатуды бастасаңыз, сіз лисензе келісімін қабылдайсыз."), - ("Accept and Install", "Қабылдау және Орнату"), - ("End-user license agreement", "Түпкі қолданушының лисензе келісімі"), - ("Generating ...", "Генератталуда..."), - ("Your installation is lower version.", "Сіздің орнатуыныз төменгі нұсқа."), - ("not_close_tcp_tip", "Тунел қолдану кезінде бұл терезені жаппаңыз"), - ("Listening ...", "Тыңдау ..."), - ("Remote Host", "Қашықтағы Хост"), - ("Remote Port", "Қашықтағы Порт"), - ("Action", "Әрекет"), - ("Add", "Қосу"), - ("Local Port", "Лақал Порт"), - ("setup_server_tip", "Тез қосылым үшін өз серберіңізді орнатуды өтінеміз"), - ("Too short, at least 6 characters.", "Тым қысқа, кемінде 6 таңба."), - ("The confirmation is not identical.", "Растау сәйкес келмейді."), - ("Permissions", "Рұқсаттар"), - ("Accept", "Қабылдау"), - ("Dismiss", "Босату"), - ("Disconnect", "Ажырату"), - ("Allow using keyboard and mouse", "Пернетақта мен тінтуірді қолдануды рұқсат ету"), - ("Allow using clipboard", "Көшіру-тақтасын рұқсат ету"), - ("Allow hearing sound", "Дыбыс естуді рұқсат ету"), - ("Allow file copy and paste", "Файылды көшіру мен қоюды рұқсат ету"), - ("Connected", "Қосылды"), - ("Direct and encrypted connection", "Тікелей және кіриптелген қосылым"), - ("Relayed and encrypted connection", "Релайданған және кіриптелген қосылым"), - ("Direct and unencrypted connection", "Тікелей және кіриптелмеген қосылым"), - ("Relayed and unencrypted connection", "Релайданған және кіриптелмеген қосылым"), - ("Enter Remote ID", "Қашықтағы ID еңгізіңіз"), - ("Enter your password", "Құпия сөзіңізді енгізіңіз"), - ("Logging in...", "Кіруде..."), - ("Enable RDP session sharing", "RDP сешті бөлісуді іске қосу"), - ("Auto Login", "Ауты Кіру (\"Сеш аяқталған соң құлыптау\"'ды орнатқанда ғана жарамды)"), - ("Enable Direct IP Access", "Тікелей IP Қолжетімді іске қосу"), - ("Rename", "Атын өзгерту"), - ("Space", "Орын"), - ("Create Desktop Shortcut", "Жұмыс үстелі Таңбашасын Жасау"), - ("Change Path", "Жолды өзгерту"), - ("Create Folder", "Бума жасау"), - ("Please enter the folder name", "Буманың атауын еңгізуді өтінеміз"), - ("Fix it", "Түзету"), - ("Warning", "Ескерту"), - ("Login screen using Wayland is not supported", "Wayland қолданған Кіру екіреніне қолдау көрсетілмейді"), - ("Reboot required", "Қайта-қосу қажет"), - ("Unsupported display server ", "Қолдаусыз дисплей сербері"), - ("x11 expected", "x11 күтілген"), - ("Port", "Порт"), - ("Settings", "Орнатпалар"), - ("Username", "Қолданушы аты"), - ("Invalid port", "Бұрыс порт"), - ("Closed manually by the peer", "Пир қолымен жабылған"), - ("Enable remote configuration modification", "Қашықтан қалыптарды өзгертуді іске қосу"), - ("Run without install", "Орнатпай-ақ Іске қосу"), - ("Always connected via relay", "Әрқашан да релай сербері арқылы қосулы"), - ("Always connect via relay", "Әрқашан да релай сербері арқылы қосылу"), - ("whitelist_tip", "Маған тек ақ-тізімделген IP қол жеткізе алады"), - ("Login", "Кіру"), - ("Logout", "Шығу"), - ("Tags", "Тақтар"), - ("Search ID", "ID Іздеу"), - ("Current Wayland display server is not supported", "Ағымдағы Wayland дисплей серберіне қолдау көрсетілмейді"), - ("whitelist_sep", "Үтір, нүктелі үтір, бос орын және жаңа жолал арқылы бөлінеді"), - ("Add ID", "ID Қосу"), - ("Add Tag", "Тақ Қосу"), - ("Unselect all tags", "Барлық тақтардың таңдауын алып тастау"), - ("Network error", "Желі қатесі"), - ("Username missed", "Қолданушы аты бос"), - ("Password missed", "Құпия сөз бос"), - ("Wrong credentials", "Бұрыс тіркелгі деректер"), - ("Edit Tag", "Тақты Өндеу"), - ("Unremember Password", "Құпия сөзді Ұмыту"), - ("Favorites", "Таңдаулылар"), - ("Add to Favorites", "Таңдаулыларға Қосу"), - ("Remove from Favorites", "Таңдаулылардан алып тастау"), - ("Empty", "Бос"), - ("Invalid folder name", "Бұрыс бума атауы"), - ("Socks5 Proxy", "Socks5 Proxy"), - ("Hostname", "Хост атауы"), - ("Discovered", "Табылды"), - ("install_daemon_tip", "Бут кезінде қосылу үшін жүйелік сербесті орнатуыныз керек."), - ("Remote ID", "Қашықтағы ID"), - ("Paste", "Қою"), - ("Paste here?", "Осында қою керек пе?"), - ("Are you sure to close the connection?", "Қосылымды жабуға сенімдісіз бе?"), - ("Download new version", "Жаңа нұсқаны жүктеу"), - ("Touch mode", "Жанасатын мода"), - ("Mouse mode", "Тінтуірлі мода"), - ("One-Finger Tap", "Бір-Саусақпен Түрту"), - ("Left Mouse", "Солақ Тінтуір"), - ("One-Long Tap", "Бір-Ұзақ Түрту"), - ("Two-Finger Tap", "Екі-Саусақпен Түрту"), - ("Right Mouse", "Оңақ Тінтуір"), - ("One-Finger Move", "Бір-Саусақпен Жылжыту"), - ("Double Tap & Move", "Екі-рет Түртіп Жылжыту"), - ("Mouse Drag", "Тінтуір Тартуы"), - ("Three-Finger vertically", "Үш-Саусақпен тік-бағытты"), - ("Mouse Wheel", "Тінтуір Дөңгелегі"), - ("Two-Finger Move", "Екі-Саусақпен Жылжыту"), - ("Canvas Move", "Кенеп Жылжуы"), - ("Pinch to Zoom", "Зумдау үшін Шымшыңыз"), - ("Canvas Zoom", "Кенеп Зумы"), - ("Reset canvas", "Кенепті қалпына келтіру"), - ("No permission of file transfer", "Файыл алмасуға рұқсат берілмеген"), - ("Note", "Нота"), - ("Connection", "Қосылым"), - ("Share Screen", "Екіренді Бөлісу"), - ("CLOSE", "ЖАБУ"), - ("OPEN", "АШУ"), - ("Chat", "Чат"), - ("Total", "Барлығы"), - ("items", "зат"), - ("Selected", "Таңдалған"), - ("Screen Capture", "Екіренді Түсіру"), - ("Input Control", "Еңгізуді Басқару/Қадағалау"), - ("Audio Capture", "Аудио Түсіру"), - ("File Connection", "Файыл Қосылымы"), - ("Screen Connection", "Екірен Қосылымы"), - ("Do you accept?", "Қабылдайсыз ба?"), - ("Open System Setting", "Жүйе Орнатпаларын Ашу"), - ("How to get Android input permission?", "Android еңгізу рұқсатын қалай алуға болады?"), - ("android_input_permission_tip1", "Қашықтағы құрылғы сіздің Android құрылғыңызды тінтуір немесе түрту арқылы басқару үшін, RustDesk'ке \"Қолжетімділік\" сербесін қолдануға рұқсат беруініз керек."), - ("android_input_permission_tip2", "Келесі Жүйе Орнатпалары бетіне барып, [Орнатылған Сербестер]'ді тауып кіріңіз, сосын [RustDesk Еңгізу] сербесін іске қосыңыз."), - ("android_new_connection_tip", "Сіздің ағымдағы құрылғыңызды басқаруды қалайтын жаңа басқару сұранысы түсті."), - ("android_service_will_start_tip", "\"Екіренді Тұсіру\" қосылған кезде сербес аутыматты іске қосылып, басқа құрылғыларға сіздің құрылғыға қосылым сұраныстауға мүмкіндің береді."), - ("android_stop_service_tip", "Сербесті жабу аутыматты түрде барлық орнатылған қосылымдарды жабады."), - ("android_version_audio_tip", "Ағымдағы Android нұсқасы аудионы түсіруді қолдамайды, Android 10 не жоғарғысына жаңғыртуды өтінеміз."), - ("android_start_service_tip", "[Сербесті Іске қосу]'ды түртіңіз не [Екіренді Түсіру] рұқсатын АШУ арқылы екіренді бөлісу сербесін іске қосыңыз."), - ("Account", "Есепкі"), - ("Overwrite", "Үстінен қайта жазу"), - ("This file exists, skip or overwrite this file?", "Бұл файыл бар, өткізіп жіберу әлде үстінен қайта жазу керек пе?"), - ("Quit", "Шығу"), - ("doc_mac_permission", ""), - ("Help", "Көмек"), - ("Failed", "Сәтсіз"), - ("Succeeded", "Сәтті"), - ("Someone turns on privacy mode, exit", "Біреу құпиялылық модасын қосты, шығу"), - ("Unsupported", "Қолдаусыз"), - ("Peer denied", "Пир қабылдамады"), - ("Please install plugins", "Плагиндерді орнатуды өтінеміз"), - ("Peer exit", "Пирдің шығуы"), - ("Failed to turn off", "Сөндіру сәтсіз болды"), - ("Turned off", "Өшірілген"), - ("In privacy mode", "Құпиялылық модасында"), - ("Out privacy mode", "Құпиялылық модасынан Шығу"), - ("Language", "Тіл"), - ("Keep RustDesk background service", "Артжақтағы RustDesk сербесін сақтап тұру"), - ("Ignore Battery Optimizations", "Бәтері Оңтайландыруларын Елемеу"), - ("android_open_battery_optimizations_tip", "Егер де бұл ерекшелікті өшіруді қаласаңыз, келесі RustDesk апылқат орнатпалары бетіне барып, [Бәтері]'ні тауып кіріңіз де [Шектеусіз]'ден құсбелгіні алып тастауды өтінеміз"), - ("Connection not allowed", "Қосылу рұқсат етілмеген"), - ("Use temporary password", "Уақытша құпия сөзді қолдану"), - ("Use permanent password", "Тұрақты құпия сөзді қолдану"), - ("Use both passwords", "Қос құпия сөзді қолдану"), - ("Set permanent password", "Тұрақты құпия сөзді орнату"), - ("Set temporary password length", "Уақытша құпия сөздің ұзындығын орнату"), - ("Enable Remote Restart", "Қашықтан қайта-қосуды іске қосу"), - ("Allow remote restart", "Қашықтан қайта-қосуды рұқсат ету"), - ("Restart Remote Device", "Қашықтағы құрылғыны қайта-қосу"), - ("Are you sure you want to restart", "Қайта-қосуға сенімдісіз бе?"), - ("Restarting Remote Device", "Қашықтағы Құрылғыны қайта-қосуда"), - ("remote_restarting_tip", "Қашықтағы құрылғы қайта-қосылуда, бұл хабар терезесін жабып, біраздан соң тұрақты құпия сөзбен қайта қосылуды өтінеміз"), - ("Copied", "Көшірілді"), - ("Exit Fullscreen", "Толық екіреннен Шығу"), - ("Fullscreen", "Толық екірен"), - ("Mobile Actions", "Мабыл Әрекеттері"), - ("Select Monitor", "Мониторды Таңдау"), - ("Control Actions", "Басқару Әрекеттері"), - ("Display Settings", "Дисплей Орнатпалары"), - ("Ratio", "Арақатынас"), - ("Image Quality", "Сурет Сапасы"), - ("Scroll Style", "Scroll Теңшетұрі"), - ("Show Menubar", "Мәзір жолағын көрсету"), - ("Hide Menubar", "Мәзір жолағын жасыру"), - ("Direct Connection", "Тікелей Қосылым"), - ("Relay Connection", "Релай Қосылым"), - ("Secure Connection", "Қауіпсіз Қосылым"), - ("Insecure Connection", "Қатерлі Қосылым"), - ("Scale original", "Scale original"), - ("Scale adaptive", "Scale adaptive"), - ("Pin menubar", "Мәзір жолағын бекіту"), - ("Unpin menubar", "Мәзір жолағын босату"), - ].iter().cloned().collect(); - } +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "Күй"), + ("Your Desktop", "Сіздің Жұмыс үстеліңіз"), + ("desk_tip", "Сіздің Жұмыс үстеліңіз осы ID мен құпия сөз арқылы қолжетімді"), + ("Password", "Құпия сөз"), + ("Ready", "Дайын"), + ("Established", "Қосылды"), + ("connecting_status", "RustDesk желісіне қосылуда..."), + ("Enable Service", "Сербесті қосу"), + ("Start Service", "Сербесті іске қосу"), + ("Service is running", "Сербес істеуде"), + ("Service is not running", "Сербес істемеуде"), + ("not_ready_status", "Дайын емес. Қосылымды тексеруді өтінеміз"), + ("Control Remote Desktop", "Қашықтағы Жұмыс үстелін Басқару"), + ("Transfer File", "Файыл Тасымалдау"), + ("Connect", "Қосылу"), + ("Recent Sessions", "Соңғы Сештер"), + ("Address Book", "Мекенжай Кітабы"), + ("Confirmation", "Мақұлдау"), + ("TCP Tunneling", "TCP тунелдеу"), + ("Remove", "Жою"), + ("Refresh random password", "Кездейсоқ құпия сөзді жаңарту"), + ("Set your own password", "Өз құпия сөзіңізді орнатыңыз"), + ("Enable Keyboard/Mouse", "Пернетақта/Тінтуірді қосу"), + ("Enable Clipboard", "Көшіру-тақтасын қосу"), + ("Enable File Transfer", "Файыл Тасымалдауды қосу"), + ("Enable TCP Tunneling", "TCP тунелдеуді қосу"), + ("IP Whitelisting", "IP Ақ-тізімі"), + ("ID/Relay Server", "ID/Relay сербері"), + ("Stop service", "Сербесті тоқтату"), + ("Change ID", "ID ауыстыру"), + ("Website", "Web-сайт"), + ("About", "Туралы"), + ("Mute", "Дыбыссыздандыру"), + ("Audio Input", "Аудио Еңгізу"), + ("Enhancements", "Жақсартулар"), + ("Hardware Codec", "Hardware Codec"), + ("Adaptive Bitrate", "Adaptive Bitrate"), + ("ID Server", "ID Сербері"), + ("Relay Server", "Relay Сербері"), + ("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", "Қолжетімсіз"), + ("Too frequent", "Тым жиі"), + ("Cancel", "Болдырмау"), + ("Skip", "Өткізіп жіберу"), + ("Close", "Жабу"), + ("Retry", "Қайтадан көру"), + ("OK", "OK"), + ("Password Required", "Құпия сөз Қажет"), + ("Please enter your password", "Құпия сөзіңізді еңгізуді өтінеміз"), + ("Remember password", "Құпия сөзді есте сақтау"), + ("Wrong Password", "Бұрыс Құпия сөз"), + ("Do you want to enter again?", "Қайтадан кіргіңіз келеді ме?"), + ("Connection Error", "Қосылым Қатесі"), + ("Error", "Қате"), + ("Reset by the peer", "Пир қалпына келтірді"), + ("Connecting...", "Қосылуда..."), + ("Connection in progress. Please wait.", "Қосылым барысында. Күтуді өтінеміз"), + ("Please try 1 minute later", "1 минуттан соң қайта көріңіз"), + ("Login Error", "Кіру Қатесі"), + ("Successful", "Сәтті"), + ("Connected, waiting for image...", "Қосылды, сурет күтілуде..."), + ("Name", "Ат"), + ("Type", "Түр"), + ("Modified", "Өзгертілді"), + ("Size", "Өлшем"), + ("Show Hidden Files", "Жасырын Файылдарды Көрсету"), + ("Receive", "Қабылдау"), + ("Send", "Жіберу"), + ("Refresh File", "Файылды жаңарту"), + ("Local", "Лақал"), + ("Remote", "Қашықтағы"), + ("Remote Computer", "Қашықтағы Қампұтыр"), + ("Local Computer", "Лақал Қампұтыр"), + ("Confirm Delete", "Жоюды Растау"), + ("Delete", "Жою"), + ("Properties", "Қасиеттер"), + ("Multi Select", "Көптік таңдау"), + ("Empty Directory", "Бос Бума"), + ("Not an empty directory", "Бос бума емес"), + ("Are you sure you want to delete this file?", "Бұл файылды жоюға сенімдісіз бе?"), + ("Are you sure you want to delete this empty directory?", "Бұл бос буманы жоюға сенімдісіз бе?"), + ("Are you sure you want to delete the file of this directory?", "Бұл буманың файылын жоюға сенімдісіз бе?"), + ("Do this for all conflicts", "Мұны барлық қанпілектер үшін жасау"), + ("This is irreversible!", "Бұл қайтымсыз!"), + ("Deleting", "Жойылу"), + ("files", "файылдар"), + ("Waiting", "Күту"), + ("Finished", "Аяқталды"), + ("Speed", "Жылдамдық"), + ("Custom Image Quality", "Теңшеулі Сурет Сапасы"), + ("Privacy mode", "Құпиялылық Модасы"), + ("Block user input", "Қолданушы еңгізуін бұғаттау"), + ("Unblock user input", "Қолданушы еңгізуін бұғаттан шығару"), + ("Adjust Window", "Терезені Реттеу"), + ("Original", "Түпнұсқа"), + ("Shrink", "Қысу"), + ("Stretch", "Созу"), + ("Scrollbar", "Scrollbar"), + ("ScrollAuto", "ScrollAuto"), + ("Good image quality", "Жақсы сурет сапасы"), + ("Balanced", "Теңдестірілген"), + ("Optimize reaction time", "Реакция уақытын оңтайландыру"), + ("Custom", "Теңшеулі"), + ("Show remote cursor", "Қашықтағы курсорды көрсету"), + ("Show quality monitor", "Сапа мониторын көрсету"), + ("Disable clipboard", "Көшіру-тақтасын өшіру"), + ("Lock after session end", "Сеш аяқталған соң құлыптау"), + ("Insert", "Кірістіру"), + ("Insert Lock", "Кірістіруді Құлыптау"), + ("Refresh", "Жаңарту"), + ("ID does not exist", "ID табылмады"), + ("Failed to connect to rendezvous server", "Rendezvous серберіне қосылу сәтсіз"), + ("Please try later", "Кейінірек қайта көруді өтінеміз"), + ("Remote desktop is offline", "Қашықтағы жұмыс үстелі офлайн күйінде"), + ("Key mismatch", "Кілт сәйкессіздігі"), + ("Timeout", "Үзіліс"), + ("Failed to connect to relay server", "Relay серберіне қосылу сәтсіз"), + ("Failed to connect via rendezvous server", "Rendezvous сербері арқылы қосылу сәтсіз"), + ("Failed to connect via relay server", "Relay сербері арқылы қосылу сәтсіз"), + ("Failed to make direct connection to remote desktop", "Қашықтағы жұмыс үстеліне тікелей қосылым жасау сәтсіз"), + ("Set Password", "Құпия сөзді Орнату"), + ("OS Password", "OS Құпия сөзі"), + ("install_tip", "UAC кесірінен, RustDesk кейбірде қашықтағы жақ ретінде дұрыс жұмыс істей алмайды. UAC'пен қиындықты болдырмау үшін, төмендегі батырманы басып RustDesk'ті жүйеге орнатыңыз."), + ("Click to upgrade", "Жаңғырту үшін басыңыз"), + ("Click to download", "Жүктеу үшін басыңыз"), + ("Click to update", "Жаңарту үшін басыңыз"), + ("Configure", "Қалыптау"), + ("config_acc", "Сіздің Жұмыс үстеліңізді қашықтан басқару үшін, RustDesk'ке \"Қолжетімділік\" рұқсаттарын беруіңіз керек."), + ("config_screen", "Сіздің Жұмыс үстеліңізге қашықтан қол жеткізу үшін, RustDesk'ке \"Екіренді Жазу\" рұқсаттарын беруіңіз керек."), + ("Installing ...", "Орнатылу..."), + ("Install", "Орнату"), + ("Installation", "Орнатылу"), + ("Installation Path", "Орнатылу Жолы"), + ("Create start menu shortcuts", "Бастау мәзірі белгішесің жасау"), + ("Create desktop icon", "Жұмыс үстелі белгішесің жасау"), + ("agreement_tip", "Орнатуды бастасаңыз, сіз лисензе келісімін қабылдайсыз."), + ("Accept and Install", "Қабылдау және Орнату"), + ("End-user license agreement", "Түпкі қолданушының лисензе келісімі"), + ("Generating ...", "Генератталуда..."), + ("Your installation is lower version.", "Сіздің орнатуыныз төменгі нұсқа."), + ("not_close_tcp_tip", "Тунел қолдану кезінде бұл терезені жаппаңыз"), + ("Listening ...", "Тыңдау ..."), + ("Remote Host", "Қашықтағы Хост"), + ("Remote Port", "Қашықтағы Порт"), + ("Action", "Әрекет"), + ("Add", "Қосу"), + ("Local Port", "Лақал Порт"), + ("setup_server_tip", "Тез қосылым үшін өз серберіңізді орнатуды өтінеміз"), + ("Too short, at least 6 characters.", "Тым қысқа, кемінде 6 таңба."), + ("The confirmation is not identical.", "Растау сәйкес келмейді."), + ("Permissions", "Рұқсаттар"), + ("Accept", "Қабылдау"), + ("Dismiss", "Босату"), + ("Disconnect", "Ажырату"), + ("Allow using keyboard and mouse", "Пернетақта мен тінтуірді қолдануды рұқсат ету"), + ("Allow using clipboard", "Көшіру-тақтасын рұқсат ету"), + ("Allow hearing sound", "Дыбыс естуді рұқсат ету"), + ("Allow file copy and paste", "Файылды көшіру мен қоюды рұқсат ету"), + ("Connected", "Қосылды"), + ("Direct and encrypted connection", "Тікелей және кіриптелген қосылым"), + ("Relayed and encrypted connection", "Релайданған және кіриптелген қосылым"), + ("Direct and unencrypted connection", "Тікелей және кіриптелмеген қосылым"), + ("Relayed and unencrypted connection", "Релайданған және кіриптелмеген қосылым"), + ("Enter Remote ID", "Қашықтағы ID еңгізіңіз"), + ("Enter your password", "Құпия сөзіңізді енгізіңіз"), + ("Logging in...", "Кіруде..."), + ("Enable RDP session sharing", "RDP сешті бөлісуді іске қосу"), + ("Auto Login", "Ауты Кіру (\"Сеш аяқталған соң құлыптау\"'ды орнатқанда ғана жарамды)"), + ("Enable Direct IP Access", "Тікелей IP Қолжетімді іске қосу"), + ("Rename", "Атын өзгерту"), + ("Space", "Орын"), + ("Create Desktop Shortcut", "Жұмыс үстелі Таңбашасын Жасау"), + ("Change Path", "Жолды өзгерту"), + ("Create Folder", "Бума жасау"), + ("Please enter the folder name", "Буманың атауын еңгізуді өтінеміз"), + ("Fix it", "Түзету"), + ("Warning", "Ескерту"), + ("Login screen using Wayland is not supported", "Wayland қолданған Кіру екіреніне қолдау көрсетілмейді"), + ("Reboot required", "Қайта-қосу қажет"), + ("Unsupported display server ", "Қолдаусыз дисплей сербері"), + ("x11 expected", "x11 күтілген"), + ("Port", "Порт"), + ("Settings", "Орнатпалар"), + ("Username", "Қолданушы аты"), + ("Invalid port", "Бұрыс порт"), + ("Closed manually by the peer", "Пир қолымен жабылған"), + ("Enable remote configuration modification", "Қашықтан қалыптарды өзгертуді іске қосу"), + ("Run without install", "Орнатпай-ақ Іске қосу"), + ("Always connected via relay", "Әрқашан да релай сербері арқылы қосулы"), + ("Always connect via relay", "Әрқашан да релай сербері арқылы қосылу"), + ("whitelist_tip", "Маған тек ақ-тізімделген IP қол жеткізе алады"), + ("Login", "Кіру"), + ("Logout", "Шығу"), + ("Tags", "Тақтар"), + ("Search ID", "ID Іздеу"), + ("Current Wayland display server is not supported", "Ағымдағы Wayland дисплей серберіне қолдау көрсетілмейді"), + ("whitelist_sep", "Үтір, нүктелі үтір, бос орын және жаңа жолал арқылы бөлінеді"), + ("Add ID", "ID Қосу"), + ("Add Tag", "Тақ Қосу"), + ("Unselect all tags", "Барлық тақтардың таңдауын алып тастау"), + ("Network error", "Желі қатесі"), + ("Username missed", "Қолданушы аты бос"), + ("Password missed", "Құпия сөз бос"), + ("Wrong credentials", "Бұрыс тіркелгі деректер"), + ("Edit Tag", "Тақты Өндеу"), + ("Unremember Password", "Құпия сөзді Ұмыту"), + ("Favorites", "Таңдаулылар"), + ("Add to Favorites", "Таңдаулыларға Қосу"), + ("Remove from Favorites", "Таңдаулылардан алып тастау"), + ("Empty", "Бос"), + ("Invalid folder name", "Бұрыс бума атауы"), + ("Socks5 Proxy", "Socks5 Proxy"), + ("Hostname", "Хост атауы"), + ("Discovered", "Табылды"), + ("install_daemon_tip", "Бут кезінде қосылу үшін жүйелік сербесті орнатуыныз керек."), + ("Remote ID", "Қашықтағы ID"), + ("Paste", "Қою"), + ("Paste here?", "Осында қою керек пе?"), + ("Are you sure to close the connection?", "Қосылымды жабуға сенімдісіз бе?"), + ("Download new version", "Жаңа нұсқаны жүктеу"), + ("Touch mode", "Жанасатын мода"), + ("Mouse mode", "Тінтуірлі мода"), + ("One-Finger Tap", "Бір-Саусақпен Түрту"), + ("Left Mouse", "Солақ Тінтуір"), + ("One-Long Tap", "Бір-Ұзақ Түрту"), + ("Two-Finger Tap", "Екі-Саусақпен Түрту"), + ("Right Mouse", "Оңақ Тінтуір"), + ("One-Finger Move", "Бір-Саусақпен Жылжыту"), + ("Double Tap & Move", "Екі-рет Түртіп Жылжыту"), + ("Mouse Drag", "Тінтуір Тартуы"), + ("Three-Finger vertically", "Үш-Саусақпен тік-бағытты"), + ("Mouse Wheel", "Тінтуір Дөңгелегі"), + ("Two-Finger Move", "Екі-Саусақпен Жылжыту"), + ("Canvas Move", "Кенеп Жылжуы"), + ("Pinch to Zoom", "Зумдау үшін Шымшыңыз"), + ("Canvas Zoom", "Кенеп Зумы"), + ("Reset canvas", "Кенепті қалпына келтіру"), + ("No permission of file transfer", "Файыл алмасуға рұқсат берілмеген"), + ("Note", "Нота"), + ("Connection", "Қосылым"), + ("Share Screen", "Екіренді Бөлісу"), + ("CLOSE", "ЖАБУ"), + ("OPEN", "АШУ"), + ("Chat", "Чат"), + ("Total", "Барлығы"), + ("items", "зат"), + ("Selected", "Таңдалған"), + ("Screen Capture", "Екіренді Түсіру"), + ("Input Control", "Еңгізуді Басқару/Қадағалау"), + ("Audio Capture", "Аудио Түсіру"), + ("File Connection", "Файыл Қосылымы"), + ("Screen Connection", "Екірен Қосылымы"), + ("Do you accept?", "Қабылдайсыз ба?"), + ("Open System Setting", "Жүйе Орнатпаларын Ашу"), + ("How to get Android input permission?", "Android еңгізу рұқсатын қалай алуға болады?"), + ("android_input_permission_tip1", "Қашықтағы құрылғы сіздің Android құрылғыңызды тінтуір немесе түрту арқылы басқару үшін, RustDesk'ке \"Қолжетімділік\" сербесін қолдануға рұқсат беруініз керек."), + ("android_input_permission_tip2", "Келесі Жүйе Орнатпалары бетіне барып, [Орнатылған Сербестер]'ді тауып кіріңіз, сосын [RustDesk Еңгізу] сербесін іске қосыңыз."), + ("android_new_connection_tip", "Сіздің ағымдағы құрылғыңызды басқаруды қалайтын жаңа басқару сұранысы түсті."), + ("android_service_will_start_tip", "\"Екіренді Тұсіру\" қосылған кезде сербес аутыматты іске қосылып, басқа құрылғыларға сіздің құрылғыға қосылым сұраныстауға мүмкіндің береді."), + ("android_stop_service_tip", "Сербесті жабу аутыматты түрде барлық орнатылған қосылымдарды жабады."), + ("android_version_audio_tip", "Ағымдағы Android нұсқасы аудионы түсіруді қолдамайды, Android 10 не жоғарғысына жаңғыртуды өтінеміз."), + ("android_start_service_tip", "[Сербесті Іске қосу]'ды түртіңіз не [Екіренді Түсіру] рұқсатын АШУ арқылы екіренді бөлісу сербесін іске қосыңыз."), + ("Account", "Есепкі"), + ("Overwrite", "Үстінен қайта жазу"), + ("This file exists, skip or overwrite this file?", "Бұл файыл бар, өткізіп жіберу әлде үстінен қайта жазу керек пе?"), + ("Quit", "Шығу"), + ("doc_mac_permission", ""), + ("Help", "Көмек"), + ("Failed", "Сәтсіз"), + ("Succeeded", "Сәтті"), + ("Someone turns on privacy mode, exit", "Біреу құпиялылық модасын қосты, шығу"), + ("Unsupported", "Қолдаусыз"), + ("Peer denied", "Пир қабылдамады"), + ("Please install plugins", "Плагиндерді орнатуды өтінеміз"), + ("Peer exit", "Пирдің шығуы"), + ("Failed to turn off", "Сөндіру сәтсіз болды"), + ("Turned off", "Өшірілген"), + ("In privacy mode", "Құпиялылық модасында"), + ("Out privacy mode", "Құпиялылық модасынан Шығу"), + ("Language", "Тіл"), + ("Keep RustDesk background service", "Артжақтағы RustDesk сербесін сақтап тұру"), + ("Ignore Battery Optimizations", "Бәтері Оңтайландыруларын Елемеу"), + ("android_open_battery_optimizations_tip", "Егер де бұл ерекшелікті өшіруді қаласаңыз, келесі RustDesk апылқат орнатпалары бетіне барып, [Бәтері]'ні тауып кіріңіз де [Шектеусіз]'ден құсбелгіні алып тастауды өтінеміз"), + ("Connection not allowed", "Қосылу рұқсат етілмеген"), + ("Use temporary password", "Уақытша құпия сөзді қолдану"), + ("Use permanent password", "Тұрақты құпия сөзді қолдану"), + ("Use both passwords", "Қос құпия сөзді қолдану"), + ("Set permanent password", "Тұрақты құпия сөзді орнату"), + ("Set temporary password length", "Уақытша құпия сөздің ұзындығын орнату"), + ("Enable Remote Restart", "Қашықтан қайта-қосуды іске қосу"), + ("Allow remote restart", "Қашықтан қайта-қосуды рұқсат ету"), + ("Restart Remote Device", "Қашықтағы құрылғыны қайта-қосу"), + ("Are you sure you want to restart", "Қайта-қосуға сенімдісіз бе?"), + ("Restarting Remote Device", "Қашықтағы Құрылғыны қайта-қосуда"), + ("remote_restarting_tip", "Қашықтағы құрылғы қайта-қосылуда, бұл хабар терезесін жабып, біраздан соң тұрақты құпия сөзбен қайта қосылуды өтінеміз"), + ("Copied", "Көшірілді"), + ("Exit Fullscreen", "Толық екіреннен Шығу"), + ("Fullscreen", "Толық екірен"), + ("Mobile Actions", "Мабыл Әрекеттері"), + ("Select Monitor", "Мониторды Таңдау"), + ("Control Actions", "Басқару Әрекеттері"), + ("Display Settings", "Дисплей Орнатпалары"), + ("Ratio", "Арақатынас"), + ("Image Quality", "Сурет Сапасы"), + ("Scroll Style", "Scroll Теңшетұрі"), + ("Show Menubar", "Мәзір жолағын көрсету"), + ("Hide Menubar", "Мәзір жолағын жасыру"), + ("Direct Connection", "Тікелей Қосылым"), + ("Relay Connection", "Релай Қосылым"), + ("Secure Connection", "Қауіпсіз Қосылым"), + ("Insecure Connection", "Қатерлі Қосылым"), + ("Scale original", "Scale original"), + ("Scale adaptive", "Scale adaptive"), + ("Pin menubar", "Мәзір жолағын бекіту"), + ("Unpin menubar", "Мәзір жолағын босату"), + ("Recording", ""), + ("Directory", ""), + ("Automatically record incoming sessions", ""), + ("Change", ""), + ("Start session recording", ""), + ("Stop session recording", ""), + ].iter().cloned().collect(); +} diff --git a/src/lang/pl.rs b/src/lang/pl.rs index fe45ddf3e..b54218d56 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -344,5 +344,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable RDP", ""), ("Pin menubar", "Przypnij pasek menu"), ("Unpin menubar", "Odepnij pasek menu"), + ("Recording", ""), + ("Directory", ""), + ("Automatically record incoming sessions", ""), + ("Change", ""), + ("Start session recording", ""), + ("Stop session recording", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 858afd8a1..7b386c3bf 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -340,5 +340,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable RDP", ""), ("Pin menubar", "Fixar barra de menu"), ("Unpin menubar", "Desenganxa la barra de menús"), + ("Recording", ""), + ("Directory", ""), + ("Automatically record incoming sessions", ""), + ("Change", ""), + ("Start session recording", ""), + ("Stop session recording", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index af4f0b52e..099ecb2bb 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -346,5 +346,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable RDP", ""), ("Pin menubar", ""), ("Unpin menubar", ""), + ("Recording", ""), + ("Directory", ""), + ("Automatically record incoming sessions", ""), + ("Change", ""), + ("Start session recording", ""), + ("Stop session recording", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 04cfed485..c001b8770 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -346,5 +346,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable RDP", ""), ("Pin menubar", "Закрепить строку меню"), ("Unpin menubar", "Открепить строку меню"), + ("Recording", ""), + ("Directory", ""), + ("Automatically record incoming sessions", ""), + ("Change", ""), + ("Start session recording", ""), + ("Stop session recording", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 8ae17b1ad..2a1de16aa 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -346,5 +346,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable RDP", ""), ("Pin menubar", "Pripnúť panel s ponukami"), ("Unpin menubar", "Uvoľniť panel s ponukami"), + ("Recording", ""), + ("Directory", ""), + ("Automatically record incoming sessions", ""), + ("Change", ""), + ("Start session recording", ""), + ("Stop session recording", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 914b103df..5896d4336 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -346,5 +346,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable RDP", ""), ("Pin menubar", ""), ("Unpin menubar", ""), + ("Recording", ""), + ("Directory", ""), + ("Automatically record incoming sessions", ""), + ("Change", ""), + ("Start session recording", ""), + ("Stop session recording", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index b1b029b39..9e9475ead 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -359,5 +359,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable RDP", ""), ("Pin menubar", "Menü çubuğunu sabitle"), ("Unpin menubar", "Menü çubuğunun sabitlemesini kaldır"), + ("Recording", ""), + ("Directory", ""), + ("Automatically record incoming sessions", ""), + ("Change", ""), + ("Start session recording", ""), + ("Stop session recording", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 764f666e7..dc7ab8a59 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -346,5 +346,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable RDP", "允許RDP訪問"), ("Pin menubar", "固定菜單欄"), ("Unpin menubar", "取消固定菜單欄"), + ("Recording", "錄屏"), + ("Directory", "目錄"), + ("Automatically record incoming sessions", "自動錄製來訪會話"), + ("Change", "變更"), + ("Start session recording", "開始錄屏"), + ("Stop session recording", "結束錄屏"), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index f177581f9..ebd44d8d5 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -346,5 +346,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable RDP", ""), ("Pin menubar", "Ghim thanh menu"), ("Unpin menubar", "Bỏ ghim thanh menu"), + ("Recording", ""), + ("Directory", ""), + ("Automatically record incoming sessions", ""), + ("Change", ""), + ("Start session recording", ""), + ("Stop session recording", ""), ].iter().cloned().collect(); } diff --git a/src/server/video_service.rs b/src/server/video_service.rs index eee9e4255..272bcf8d5 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -25,6 +25,7 @@ use hbb_common::tokio::sync::{ }; use scrap::{ codec::{Encoder, EncoderCfg, HwEncoderConfig}, + record::{Recorder, RecorderContext}, vpxcodec::{VpxEncoderConfig, VpxVideoCodecId}, Capturer, Display, TraitCapturer, }; @@ -435,6 +436,21 @@ fn run(sp: GenericService) -> ResultType<()> { #[cfg(windows)] log::info!("gdi: {}", c.is_gdi()); let codec_name = Encoder::current_hw_encoder_name(); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + let recorder = if !Config::get_option("allow-auto-record-incoming").is_empty() { + Recorder::new(RecorderContext { + id: "local".to_owned(), + filename: "".to_owned(), + width: c.width, + height: c.height, + codec_id: scrap::record::RecodeCodecID::VP9, + }) + .map_or(Default::default(), |r| Arc::new(Mutex::new(Some(r)))) + } else { + Default::default() + }; + #[cfg(any(target_os = "android", target_os = "ios"))] + let recorder: Arc>> = Default::default(); while sp.ok() { #[cfg(windows)] @@ -495,7 +511,8 @@ fn run(sp: GenericService) -> ResultType<()> { } scrap::Frame::RAW(data) => { if (data.len() != 0) { - let send_conn_ids = handle_one_frame(&sp, data, ms, &mut encoder)?; + let send_conn_ids = + handle_one_frame(&sp, data, ms, &mut encoder, recorder.clone())?; frame_controller.set_send(now, send_conn_ids); } } @@ -511,7 +528,8 @@ fn run(sp: GenericService) -> ResultType<()> { Ok(frame) => { let time = now - start; let ms = (time.as_secs() * 1000 + time.subsec_millis() as u64) as i64; - let send_conn_ids = handle_one_frame(&sp, &frame, ms, &mut encoder)?; + let send_conn_ids = + handle_one_frame(&sp, &frame, ms, &mut encoder, recorder.clone())?; frame_controller.set_send(now, send_conn_ids); #[cfg(windows)] { @@ -612,6 +630,7 @@ fn handle_one_frame( frame: &[u8], ms: i64, encoder: &mut Encoder, + recorder: Arc>>, ) -> ResultType> { sp.snapshot(|sps| { // so that new sub and old sub share the same encoder after switch @@ -623,6 +642,12 @@ fn handle_one_frame( let mut send_conn_ids: HashSet = Default::default(); if let Ok(msg) = encoder.encode_to_message(frame, ms) { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + recorder + .lock() + .unwrap() + .as_mut() + .map(|r| r.write_message(&msg)); send_conn_ids = sp.send_video_frame(msg); } Ok(send_conn_ids) diff --git a/src/ui.rs b/src/ui.rs index d1c669848..095559811 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -21,20 +21,20 @@ use hbb_common::{ use crate::common::get_app_name; use crate::ipc; use crate::ui_interface::{ - check_mouse_time, closing, create_shortcut, current_is_wayland, fix_login_wayland, - forget_password, get_api_server, get_async_job_status, get_connect_status, get_error, get_fav, - get_icon, get_lan_peers, get_langs, get_license, get_local_option, get_mouse_time, - get_new_version, get_option, get_options, get_peer, get_peer_option, get_recent_sessions, - get_remote_id, get_size, get_socks, get_software_ext, get_software_store_path, - get_software_update_url, get_uuid, get_version, goto_install, has_hwcodec, - has_rendezvous_service, install_me, install_path, is_can_screen_recording, is_installed, - is_installed_daemon, is_installed_lower_version, is_login_wayland, is_ok_change_id, - is_process_trusted, is_rdp_service_open, is_share_rdp, is_xfce, modify_default_login, - new_remote, open_url, peer_has_password, permanent_password, post_request, - recent_sessions_updated, remove_peer, run_without_install, set_local_option, set_option, - set_options, set_peer_option, set_permanent_password, set_remote_id, set_share_rdp, set_socks, - show_run_without_install, store_fav, t, temporary_password, test_if_valid_server, update_me, - update_temporary_password, using_public_server, + check_mouse_time, closing, create_shortcut, current_is_wayland, default_video_save_directory, + fix_login_wayland, forget_password, get_api_server, get_async_job_status, get_connect_status, + get_error, get_fav, get_icon, get_lan_peers, get_langs, get_license, get_local_option, + get_mouse_time, get_new_version, get_option, get_options, get_peer, get_peer_option, + get_recent_sessions, get_remote_id, get_size, get_socks, get_software_ext, + get_software_store_path, get_software_update_url, get_uuid, get_version, goto_install, + has_hwcodec, has_rendezvous_service, install_me, install_path, is_can_screen_recording, + is_installed, is_installed_daemon, is_installed_lower_version, is_login_wayland, + is_ok_change_id, is_process_trusted, is_rdp_service_open, is_share_rdp, is_xfce, + modify_default_login, new_remote, open_url, peer_has_password, permanent_password, + post_request, recent_sessions_updated, remove_peer, run_without_install, set_local_option, + set_option, set_options, set_peer_option, set_permanent_password, set_remote_id, set_share_rdp, + set_socks, show_run_without_install, store_fav, t, temporary_password, test_if_valid_server, + update_me, update_temporary_password, using_public_server, }; mod cm; @@ -579,6 +579,10 @@ impl UI { fn get_langs(&self) -> String { get_langs() } + + fn default_video_save_directory(&self) -> String { + default_video_save_directory() + } } impl sciter::EventHandler for UI { @@ -661,6 +665,7 @@ impl sciter::EventHandler for UI { fn get_uuid(); fn has_hwcodec(); fn get_langs(); + fn default_video_save_directory(); } } diff --git a/src/ui/common.css b/src/ui/common.css index c3f3706ef..1814ad32d 100644 --- a/src/ui/common.css +++ b/src/ui/common.css @@ -70,6 +70,15 @@ button.button:hover, button.outline:hover { border-color: color(hover-border); } +button.link { + background: none !important; + border: none; + padding: 0 !important; + color: color(button); + text-decoration: underline; + cursor: pointer; +} + input[type=text], input[type=password], input[type=number] { width: *; font-size: 1.5em; diff --git a/src/ui/header.tis b/src/ui/header.tis index b274b0464..a997ce4b3 100644 --- a/src/ui/header.tis +++ b/src/ui/header.tis @@ -14,6 +14,8 @@ var svg_secure = var svg_insecure = ; var svg_insecure_relay = ; var svg_secure_relay = ; +var svg_recording_off = ; +var svg_recording_on = ; var cur_window_state = view.windowState; function check_state_change() { @@ -90,6 +92,9 @@ function editOSPassword(login=false) { }); } +var recording = false; +var recording_refresh = false; + class Header: Reactor.Component { this var conn_note = ""; @@ -140,6 +145,7 @@ class Header: Reactor.Component { {svg_action} {svg_display} {svg_keyboard} + {recording ? svg_recording_on : svg_recording_off} {this.renderKeyboardPop()} {this.renderDisplayPop()} {this.renderActionPop()} @@ -279,6 +285,13 @@ class Header: Reactor.Component { me.popup(menu); } + event click $(span#recording) (_, me) { + handler.record_screen(!recording, display_width, display_height); + recording = !recording; + header.update(); + if (recording) self.timer(100ms, function() { recording_refresh = true; handler.refresh_video(); }); + } + event click $(#screen) (_, me) { if (pi.current_display == me.index) return; handler.switch_display(me.index); diff --git a/src/ui/index.tis b/src/ui/index.tis index d0a9d29a8..dc2f403fc 100644 --- a/src/ui/index.tis +++ b/src/ui/index.tis @@ -214,6 +214,7 @@ class Enhancements: Reactor.Component {

    {has_hwcodec ?
  • {svg_checkmark}{translate("Hardware Codec")} (beta)
  • : ""}
  • {svg_checkmark}{translate("Adaptive Bitrate")} (beta)
  • +
  • {translate("Recording")}
  • , "", function(res=null) { if (!res) return; var value = (res.text || "").trim(); var values = value.split(/[\s,;\n]+/g); @@ -483,7 +489,7 @@ class SessionList: Reactor.Component { msgbox("custom-rename", "Rename", "
    \
    \
    \ - ", function(res=null) { + ", "", function(res=null) { if (!res) return; var name = (res.name || "").trim(); if (name != old_name) { @@ -506,7 +512,7 @@ class SessionList: Reactor.Component { } } if (!peer) return; - msgbox("custom-edit-tag", "Edit Tag", , function(res=null) { + msgbox("custom-edit-tag", "Edit Tag", , "", function(res=null) { if (!res) return; peer.tags = selectTags.tags; updateAb(); @@ -738,7 +744,7 @@ function editRdpPort() {
    {translate('Port')}:{port}
    {translate('Username')}:
    {translate('Password')}:
    - , function(res=null) { + , "", function(res=null) { if (!res) return; var p = (res.port || '').trim(); if (p != p0) { diff --git a/src/ui/common.tis b/src/ui/common.tis index e591f45a3..76e0fb84e 100644 --- a/src/ui/common.tis +++ b/src/ui/common.tis @@ -232,7 +232,7 @@ class ChatBox: Reactor.Component { /******************** start of msgbox ****************************************/ var remember_password = false; -function msgbox(type, title, content, link, callback=null, height=180, width=500, hasRetry=false, contentStyle="") { +function msgbox(type, title, content, link="", callback=null, height=180, width=500, hasRetry=false, contentStyle="") { $(body).scrollTo(0, 0); if (!type) { closeMsgbox(); diff --git a/src/ui/file_transfer.tis b/src/ui/file_transfer.tis index 38c6321dc..451117403 100644 --- a/src/ui/file_transfer.tis +++ b/src/ui/file_transfer.tis @@ -535,7 +535,7 @@ class FolderView : Reactor.Component { msgbox("custom", translate("Create Folder"), "
    \
    " + translate("Please enter the folder name") + ":
    \
    \ -
    ", function(res=null) { + ", "", function(res=null) { if (!res) return; if (!res.name) return; var name = res.name.trim(); @@ -716,7 +716,7 @@ function confirmDelete(id ,path, is_remote) { msgbox("custom-skip", "Confirm Delete", "
    \
    " + translate('Are you sure you want to delete this file?') + "
    \ " + path + "
    \ - ", function(res=null) { + ", "", function(res=null) { if (!res) { file_transfer.job_table.updateJobStatus(id, -1, "cancel"); file_transfer.job_table.cancelDeletePolling(); @@ -746,7 +746,7 @@ handler.confirmDeleteFiles = function(id, i, name) {
    " + translate('Are you sure you want to delete this file?') + "
    \ " + file_path + " \
    " + translate('Do this for all conflicts') + "
    \ - ", function(res=null) { + ", "", function(res=null) { if (!res) { jt.updateJobStatus(id, i - 1, "cancel"); file_transfer.job_table.cancelDeletePolling(); @@ -778,7 +778,7 @@ handler.overrideFileConfirm = function(id, file_num, to, is_upload) {
    " + translate('This file exists, skip or overwrite this file?') + "
    \ " + to + " \
    " + translate('Do this for all conflicts') + "
    \ - ", function(res=null) { + ", "", function(res=null) { if (!res) { jt.updateJobStatus(id, -1, "cancel"); handler.cancel_job(id); diff --git a/src/ui/header.tis b/src/ui/header.tis index 8f9fa8a32..01699a583 100644 --- a/src/ui/header.tis +++ b/src/ui/header.tis @@ -78,7 +78,7 @@ class EditOsPassword: Reactor.Component { function editOSPassword(login=false) { var p0 = handler.get_option('os-password'); - msgbox("custom-os-password", 'OS Password', p0, function(res=null) { + msgbox("custom-os-password", 'OS Password', p0, "", function(res=null) { if (!res) return; var a0 = handler.get_option('auto-login') != ''; var p = (res.password || '').trim(); @@ -320,7 +320,7 @@ class Header: Reactor.Component { var self = this; msgbox("custom", "Note",
    -
    , function(res=null) { + , "", function(res=null) { if (!res) return; if (!res.text) return; self.conn_note = res.text; @@ -333,9 +333,15 @@ class Header: Reactor.Component { } event click $(#restart_remote_device) { - msgbox("restart-confirmation", translate("Restart Remote Device"), translate("Are you sure you want to restart") + " " + pi.username + "@" + pi.hostname + "(" + get_id() + ") ?", function(res=null) { - if (res != null) handler.restart_remote_device(); - }); + msgbox( + "restart-confirmation", + translate("Restart Remote Device"), + translate("Are you sure you want to restart") + " " + pi.username + "@" + pi.hostname + "(" + get_id() + ") ?", + "", + function(res=null) { + if (res != null) handler.restart_remote_device(); + } + ); } event click $(#lock-screen) { @@ -400,7 +406,7 @@ function handle_custom_image_quality() { var bitrate = (tmp[0] || 50); msgbox("custom", "Custom Image Quality", "
    \
    x% Bitrate
    \ -
    ", function(res=null) { + ", "", function(res=null) { if (!res) return; if (!res.bitrate) return; handler.save_custom_image_quality(res.bitrate); @@ -489,7 +495,7 @@ handler.updatePrivacyMode = updatePrivacyMode; function togglePrivacyMode(privacy_id) { var supported = handler.is_privacy_mode_supported(); if (!supported) { - msgbox("nocancel", translate("Privacy mode"), translate("Unsupported"), function() { }); + msgbox("nocancel", translate("Privacy mode"), translate("Unsupported"), "", function() { }); } else { handler.toggle_option(privacy_id); } diff --git a/src/ui/index.tis b/src/ui/index.tis index a2d895733..45e881096 100644 --- a/src/ui/index.tis +++ b/src/ui/index.tis @@ -247,7 +247,7 @@ class Enhancements: Reactor.Component {
    - , function(res=null) { + , "", function(res=null) { if (!res) return; handler.set_option("enable-record-session", res.enable_record_session ? '' : 'N'); handler.set_option("allow-auto-record-incoming", res.auto_record_incoming ? 'Y' : ''); @@ -369,7 +369,7 @@ class MyIdMenu: Reactor.Component {
    " + handler.get_license() + " \

    Made with heart in this chaotic world!

    \ \ - ", function(el) { + ", "", function(el) { if (el && el.attributes) { handler.open_url(el.attributes['url']); }; @@ -389,7 +389,7 @@ class MyIdMenu: Reactor.Component {
    " + translate("whitelist_sep") + "
    \ \ \ - ", function(res=null) { + ", "", function(res=null) { if (!res) return; var value = (res.text || "").trim(); if (value) { @@ -417,7 +417,7 @@ class MyIdMenu: Reactor.Component {
    " + translate("API Server") + ":
    \
    " + translate("Key") + ":
    \ \ - ", function(res=null) { + ", "", function(res=null) { if (!res) return; var id = (res.id || "").trim(); var relay = (res.relay || "").trim(); @@ -453,7 +453,7 @@ class MyIdMenu: Reactor.Component {
    {translate("Username")}:
    {translate("Password")}:
    - , function(res=null) { + , "", function(res=null) { if (!res) return; var proxy = (res.proxy || "").trim(); var username = (res.username || "").trim(); @@ -474,7 +474,7 @@ class MyIdMenu: Reactor.Component {
    " + translate('id_change_tip') + "
    \
    ID:
    \ \ - ", function(res=null, show_progress) { + ", "", function(res=null, show_progress) { if (!res) return; show_progress(); var id = (res.id || "").trim(); @@ -520,7 +520,7 @@ function editDirectAccessPort() { ; msgbox("custom-direct-access-port", translate('Direct IP Access Settings'),
    {translate('Port')}:{port}
    -
    , function(res=null) { + , "", function(res=null) { if (!res) return; var p = (res.port || '').trim(); if (p) { @@ -934,7 +934,7 @@ class PasswordArea: Reactor.Component {
    " + translate('Password') + ":
    \
    " + translate('Confirmation') + ":
    \ \ - ", function(res=null) { + ", "", function(res=null) { if (!res) return; var p0 = (res.password || "").trim(); var p1 = (res.confirmation || "").trim(); @@ -1161,7 +1161,7 @@ function login() { msgbox("custom-login", translate('Login'),
    {translate('Username')}:
    {translate('Password')}:
    -
    , function(res=null, show_progress) { + , "", function(res=null, show_progress) { if (!res) return; show_progress(); var name = (res.username || '').trim(); diff --git a/src/ui/msgbox.tis b/src/ui/msgbox.tis index 3f40c367d..d5c60d95c 100644 --- a/src/ui/msgbox.tis +++ b/src/ui/msgbox.tis @@ -1,5 +1,3 @@ -import * as env from "@env"; - function translate_text(text) { if (text.indexOf('Failed') == 0 && text.indexOf(': ') > 0) { var fds = text.split(': '); @@ -162,9 +160,8 @@ class MsgboxComponent: Reactor.Component { } event click $(button#jumplink) { - stdout.println("REMOVE ME ================================= jump link" + this.link); if (this.link.indexOf("http") == 0) { - env.launch(this.link); + Sciter.launch(this.link); } } From c48ed06d93b0ca072e9f4db605c2a849fcbb37fd Mon Sep 17 00:00:00 2001 From: fufesou Date: Mon, 17 Oct 2022 03:06:06 -0700 Subject: [PATCH 0708/2015] wayland: remove user service Signed-off-by: fufesou --- build.py | 6 - res/DEBIAN/postinst | 9 -- res/DEBIAN/preinst | 7 -- res/DEBIAN/prerm | 5 +- res/rustdesk.service.user | 15 --- src/platform/linux.rs | 229 +++++++++++++++++++------------------- 6 files changed, 117 insertions(+), 154 deletions(-) delete mode 100644 res/rustdesk.service.user diff --git a/build.py b/build.py index dbc4f6255..0cf62daf1 100755 --- a/build.py +++ b/build.py @@ -161,8 +161,6 @@ def build_flutter_deb(version): 'cp -r build/linux/x64/release/bundle/* tmpdeb/usr/lib/rustdesk/') os.system( 'cp ../res/rustdesk.service tmpdeb/usr/share/rustdesk/files/systemd/') - os.system( - 'cp ../res/rustdesk.service.user tmpdeb/usr/share/rustdesk/files/systemd/') os.system( 'cp ../res/128x128@2x.png tmpdeb/usr/share/rustdesk/files/rustdesk.png') os.system( @@ -177,7 +175,6 @@ def build_flutter_deb(version): generate_control_file(version) os.system('cp -a ../res/DEBIAN/* tmpdeb/DEBIAN/') md5_file('usr/share/rustdesk/files/systemd/rustdesk.service') - md5_file('usr/share/rustdesk/files/systemd/rustdesk.service.user') os.system('dpkg-deb -b tmpdeb rustdesk.deb;') os.system('/bin/rm -rf tmpdeb/') @@ -331,8 +328,6 @@ def main(): os.system('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/') os.system( 'cp res/rustdesk.service tmpdeb/usr/share/rustdesk/files/systemd/') - os.system( - 'cp res/rustdesk.service.user tmpdeb/usr/share/rustdesk/files/systemd/') os.system( 'cp res/128x128@2x.png tmpdeb/usr/share/rustdesk/files/rustdesk.png') os.system( @@ -345,7 +340,6 @@ def main(): os.system('mv tmpdeb/usr/bin/rustdesk tmpdeb/usr/lib/rustdesk/') os.system('cp libsciter-gtk.so tmpdeb/usr/lib/rustdesk/') md5_file('usr/share/rustdesk/files/systemd/rustdesk.service') - md5_file('usr/share/rustdesk/files/systemd/rustdesk.service.user') md5_file('usr/lib/rustdesk/libsciter-gtk.so') os.system('dpkg-deb -b tmpdeb rustdesk.deb; /bin/rm -rf tmpdeb/') os.rename('rustdesk.deb', 'rustdesk-%s.deb' % version) diff --git a/res/DEBIAN/postinst b/res/DEBIAN/postinst index 95222564d..5b37e2f51 100755 --- a/res/DEBIAN/postinst +++ b/res/DEBIAN/postinst @@ -19,14 +19,5 @@ if [ "$1" = configure ]; then systemctl daemon-reload systemctl enable rustdesk systemctl start rustdesk - - cp /usr/share/rustdesk/files/systemd/rustdesk.service.user /usr/lib/systemd/user/rustdesk.service - ubuntuVersion=$(grep -oP 'VERSION_ID="\K[\d]+' /etc/os-release | bc -l) - waylandSupportVersion=21 - if [ "$ubuntuVersion" -ge "$waylandSupportVersion" ] - then - curUser=$(who | awk '{print $1}' | head -1) - systemctl --machine=${curUser}@.host --user daemon-reload - fi fi fi diff --git a/res/DEBIAN/preinst b/res/DEBIAN/preinst index 7fbedca4a..8b73e9962 100755 --- a/res/DEBIAN/preinst +++ b/res/DEBIAN/preinst @@ -7,13 +7,6 @@ case $1 in INITSYS=$(ls -al /proc/1/exe | awk -F' ' '{print $NF}' | awk -F'/' '{print $NF}') if [ "systemd" == "${INITSYS}" ]; then service rustdesk stop || true - - serverUser=$(ps -ef | grep -E 'rustdesk +--server' | awk '{print $1}' | head -1) - if [ "$serverUser" != "" ] && [ "$serverUser" != "root" ] - then - systemctl --machine=${serverUser}@.host --user stop rustdesk || true - fi - sleep 1 rm -rf /usr/bin/libsciter-gtk.so fi diff --git a/res/DEBIAN/prerm b/res/DEBIAN/prerm index e9e3931ac..6d5026991 100755 --- a/res/DEBIAN/prerm +++ b/res/DEBIAN/prerm @@ -11,7 +11,9 @@ case $1 in systemctl stop rustdesk || true systemctl disable rustdesk || true + rm /etc/systemd/system/rustdesk.service /usr/lib/systemd/system/rustdesk.service || true + # workaround temp dev build between 1.1.9 and 1.2.0 serverUser=$(ps -ef | grep -E 'rustdesk +--server' | awk '{print $1}' | head -1) ubuntuVersion=$(grep -oP 'VERSION_ID="\K[\d]+' /etc/os-release | bc -l) waylandSupportVersion=21 @@ -19,8 +21,7 @@ case $1 in then systemctl --machine=${serverUser}@.host --user stop rustdesk || true fi - - rm /etc/systemd/system/rustdesk.service /usr/lib/systemd/system/rustdesk.service /usr/lib/systemd/user/rustdesk.service || true + rm /usr/lib/systemd/user/rustdesk.service >/dev/null 2>/dev/null || true fi ;; esac diff --git a/res/rustdesk.service.user b/res/rustdesk.service.user deleted file mode 100644 index f6c7454c9..000000000 --- a/res/rustdesk.service.user +++ /dev/null @@ -1,15 +0,0 @@ -[Unit] -Description=RustDesk user service (--server) - -[Service] -Type=simple -ExecStart=/usr/bin/rustdesk --server -PIDFile=/run/rustdesk.user.pid -KillMode=mixed -TimeoutStopSec=30 -LimitNOFILE=100000 -Restart=on-failure -RestartSec=3 - -[Install] -WantedBy=multi-user.target diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 5ff69d732..2efe06faf 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -161,45 +161,6 @@ fn start_uinput_service() { }); } -fn try_start_user_service(username: &str) { - if username == "" || username == "root" { - return; - } - - if let Ok(mut cur_username) = - run_cmds("ps -ef | grep -E 'rustdesk +--server' | awk '{print $1}' | head -1".to_owned()) - { - cur_username = cur_username.trim().to_owned(); - if cur_username != "root" && cur_username != username { - let _ = run_cmds(format!( - "systemctl --machine={}@.host --user stop rustdesk", - &cur_username - )); - } else if cur_username == username { - return; - } - } - - let _ = run_cmds(format!( - "systemctl --machine={}@.host --user start rustdesk", - username - )); -} - -fn try_stop_user_service() { - if let Ok(mut username) = - run_cmds("ps -ef | grep -E 'rustdesk +--server' | awk '{print $1}' | head -1".to_owned()) - { - username = username.trim().to_owned(); - if username != "root" { - let _ = run_cmds(format!( - "systemctl --machine={}@.host --user stop rustdesk", - &username - )); - } - } -} - fn stop_server(server: &mut Option) { if let Some(mut ps) = server.take() { allow_err!(ps.kill()); @@ -214,13 +175,106 @@ fn stop_server(server: &mut Option) { } } +fn set_x11_env(uid: &str) { + log::info!("uid of seat0: {}", uid); + let gdm = format!("/run/user/{}/gdm/Xauthority", uid); + let mut auth = get_env_tries("XAUTHORITY", uid, 10); + if auth.is_empty() { + auth = if std::path::Path::new(&gdm).exists() { + gdm + } else { + let username = get_active_username(); + if username == "root" { + format!("/{}/.Xauthority", username) + } else { + let tmp = format!("/home/{}/.Xauthority", username); + if std::path::Path::new(&tmp).exists() { + tmp + } else { + format!("/var/lib/{}/.Xauthority", username) + } + } + }; + } + let mut d = get_env("DISPLAY", uid); + if d.is_empty() { + d = get_display(); + } + if d.is_empty() { + d = ":0".to_owned(); + } + d = d.replace(&whoami::hostname(), "").replace("localhost", ""); + log::info!("DISPLAY: {}", d); + log::info!("XAUTHORITY: {}", auth); + std::env::set_var("XAUTHORITY", auth); + std::env::set_var("DISPLAY", d); +} + +fn stop_rustdesk_servers() { + let _ = run_cmds(format!( + r##"ps -ef | grep -E 'rustdesk +--server' | awk '{{printf("kill -9 %d\n", $2)}}' | bash"##, + )); +} + +fn should_start_server( + try_x11: bool, + uid: &mut String, + cm0: &mut bool, + last_restart: &mut std::time::Instant, + server: &mut Option, +) -> bool { + let cm = get_cm(); + let tmp = get_active_userid(); + let mut start_new = false; + if tmp != *uid && !tmp.is_empty() { + *uid = tmp; + if try_x11 { + set_x11_env(&uid); + } + if let Some(ps) = server.as_mut() { + allow_err!(ps.kill()); + std::thread::sleep(std::time::Duration::from_millis(30)); + *last_restart = std::time::Instant::now(); + } + } else if !cm + && ((*cm0 && last_restart.elapsed().as_secs() > 60) + || last_restart.elapsed().as_secs() > 3600) + { + // restart server if new connections all closed, or every one hour, + // as a workaround to resolve "SpotUdp" (dns resolve) + // and x server get displays failure issue + if let Some(ps) = server.as_mut() { + allow_err!(ps.kill()); + std::thread::sleep(std::time::Duration::from_millis(30)); + *last_restart = std::time::Instant::now(); + log::info!("restart server"); + } + } + if let Some(ps) = server.as_mut() { + match ps.try_wait() { + Ok(Some(_)) => { + *server = None; + start_new = true; + } + _ => {} + } + } else { + start_new = true; + } + *cm0 = cm; + start_new +} + pub fn start_os_service() { + stop_rustdesk_servers(); + start_uinput_service(); let running = Arc::new(AtomicBool::new(true)); let r = running.clone(); let mut uid = "".to_owned(); let mut server: Option = None; + let mut user_server: Option = None; if let Err(err) = ctrlc::set_handler(move || { r.store(false, Ordering::SeqCst); }) { @@ -234,78 +288,9 @@ pub fn start_os_service() { let is_wayland = current_is_wayland(); if username == "root" || !is_wayland { - // try stop user service - try_stop_user_service(); - + stop_server(&mut user_server); // try start subprocess "--server" - let cm = get_cm(); - let tmp = get_active_userid(); - let mut start_new = false; - if tmp != uid && !tmp.is_empty() { - uid = tmp; - log::info!("uid of seat0: {}", uid); - let gdm = format!("/run/user/{}/gdm/Xauthority", uid); - let mut auth = get_env_tries("XAUTHORITY", &uid, 10); - if auth.is_empty() { - auth = if std::path::Path::new(&gdm).exists() { - gdm - } else { - let username = get_active_username(); - if username == "root" { - format!("/{}/.Xauthority", username) - } else { - let tmp = format!("/home/{}/.Xauthority", username); - if std::path::Path::new(&tmp).exists() { - tmp - } else { - format!("/var/lib/{}/.Xauthority", username) - } - } - }; - } - let mut d = get_env("DISPLAY", &uid); - if d.is_empty() { - d = get_display(); - } - if d.is_empty() { - d = ":0".to_owned(); - } - d = d.replace(&whoami::hostname(), "").replace("localhost", ""); - log::info!("DISPLAY: {}", d); - log::info!("XAUTHORITY: {}", auth); - std::env::set_var("XAUTHORITY", auth); - std::env::set_var("DISPLAY", d); - if let Some(ps) = server.as_mut() { - allow_err!(ps.kill()); - std::thread::sleep(std::time::Duration::from_millis(30)); - last_restart = std::time::Instant::now(); - } - } else if !cm - && ((cm0 && last_restart.elapsed().as_secs() > 60) - || last_restart.elapsed().as_secs() > 3600) - { - // restart server if new connections all closed, or every one hour, - // as a workaround to resolve "SpotUdp" (dns resolve) - // and x server get displays failure issue - if let Some(ps) = server.as_mut() { - allow_err!(ps.kill()); - std::thread::sleep(std::time::Duration::from_millis(30)); - last_restart = std::time::Instant::now(); - log::info!("restart server"); - } - } - if let Some(ps) = server.as_mut() { - match ps.try_wait() { - Ok(Some(_)) => { - server = None; - start_new = true; - } - _ => {} - } - } else { - start_new = true; - } - if start_new { + if should_start_server(true, &mut uid, &mut cm0, &mut last_restart, &mut server) { match crate::run_me(vec!["--server"]) { Ok(ps) => server = Some(ps), Err(err) => { @@ -313,23 +298,37 @@ pub fn start_os_service() { } } } - cm0 = cm; } else if username != "" { if username != "gdm" { // try kill subprocess "--server" stop_server(&mut server); - // try start user service - try_start_user_service(&username); + // try start subprocess "--server" + if should_start_server( + false, + &mut uid, + &mut cm0, + &mut last_restart, + &mut user_server, + ) { + match run_as_user("--server") { + Ok(ps) => user_server = ps, + Err(err) => { + log::error!("Failed to start server: {}", err); + } + } + } } } else { - try_stop_user_service(); + stop_server(&mut user_server); stop_server(&mut server); } std::thread::sleep(std::time::Duration::from_millis(super::SERVICE_INTERVAL)); } - try_stop_user_service(); + if let Some(ps) = user_server.take().as_mut() { + allow_err!(ps.kill()); + } if let Some(ps) = server.take().as_mut() { allow_err!(ps.kill()); } From b265d25dcb1ff3709d5f530f0184c6e4058523df Mon Sep 17 00:00:00 2001 From: csf Date: Mon, 17 Oct 2022 23:07:40 +0900 Subject: [PATCH 0709/2015] desktop file transfer shift + click multi selection --- .../lib/desktop/pages/file_manager_page.dart | 66 ++++++++++--------- .../lib/mobile/pages/file_manager_page.dart | 47 ------------- flutter/lib/models/file_model.dart | 46 ++++++++++++- 3 files changed, 81 insertions(+), 78 deletions(-) diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 6c2e20e78..3ffca3f91 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -6,7 +6,6 @@ import 'package:desktop_drop/desktop_drop.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_breadcrumb/flutter_breadcrumb.dart'; -import 'package:flutter_hbb/mobile/pages/file_manager_page.dart'; import 'package:flutter_hbb/models/file_model.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; @@ -273,21 +272,8 @@ class _FileManagerPageState extends State return DataRow( key: ValueKey(entry.name), onSelectChanged: (s) { - final isCtrlDown = RawKeyboard.instance.keysPressed - .contains(LogicalKeyboardKey.controlLeft); - final items = getSelectedItem(isLocal); - if (isCtrlDown) { - if (s != null) { - if (s) { - items.add(isLocal, entry); - } else { - items.remove(entry); - } - } - } else { - items.clear(); - items.add(isLocal, entry); - } + _onSelectedChanged(getSelectedItem(isLocal), filteredEntries, + entry, isLocal); setState(() {}); }, selected: getSelectedItem(isLocal).contains(entry), @@ -321,20 +307,8 @@ class _FileManagerPageState extends State items.clear(); return; } - - final isCtrlDown = RawKeyboard.instance.keysPressed - .contains(LogicalKeyboardKey.controlLeft); - if (isCtrlDown) { - if (items.contains(entry)) { - items.remove(entry); - } else { - items.add(isLocal, entry); - } - } else { - items.clear(); - items.add(isLocal, entry); - } - setState(() {}); + _onSelectedChanged( + items, filteredEntries, entry, isLocal); }, ), DataCell(FittedBox( @@ -362,6 +336,38 @@ class _FileManagerPageState extends State ); } + void _onSelectedChanged(SelectedItems selectedItems, List entries, + Entry entry, bool isLocal) { + final isCtrlDown = RawKeyboard.instance.keysPressed + .contains(LogicalKeyboardKey.controlLeft); + final isShiftDown = + RawKeyboard.instance.keysPressed.contains(LogicalKeyboardKey.shiftLeft); + if (isCtrlDown) { + if (selectedItems.contains(entry)) { + selectedItems.remove(entry); + } else { + selectedItems.add(isLocal, entry); + } + } else if (isShiftDown) { + final List indexGroup = []; + for (var selected in selectedItems.items) { + indexGroup.add(entries.indexOf(selected)); + } + indexGroup.add(entries.indexOf(entry)); + indexGroup.removeWhere((e) => e == -1); + final maxIndex = indexGroup.reduce(max); + final minIndex = indexGroup.reduce(min); + selectedItems.clear(); + entries + .getRange(minIndex, maxIndex + 1) + .forEach((e) => selectedItems.add(isLocal, e)); + } else { + selectedItems.clear(); + selectedItems.add(isLocal, entry); + } + setState(() {}); + } + bool _checkDoubleClick(Entry entry) { final current = DateTime.now().millisecondsSinceEpoch; final elapsed = current - _lastClickTime; diff --git a/flutter/lib/mobile/pages/file_manager_page.dart b/flutter/lib/mobile/pages/file_manager_page.dart index ce270ffea..0ee0e0f8d 100644 --- a/flutter/lib/mobile/pages/file_manager_page.dart +++ b/flutter/lib/mobile/pages/file_manager_page.dart @@ -555,50 +555,3 @@ class BottomSheetBody extends StatelessWidget { ); } } - -class SelectedItems { - bool? _isLocal; - final List _items = []; - - List get items => _items; - - int get length => _items.length; - - bool? get isLocal => _isLocal; - - add(bool isLocal, Entry e) { - if (_isLocal == null) { - _isLocal = isLocal; - } - if (_isLocal != null && _isLocal != isLocal) { - return; - } - if (!_items.contains(e)) { - _items.add(e); - } - } - - bool contains(Entry e) { - return _items.contains(e); - } - - remove(Entry e) { - _items.remove(e); - if (_items.length == 0) { - _isLocal = null; - } - } - - bool isOtherPage(bool currentIsLocal) { - if (_isLocal == null) { - return false; - } else { - return _isLocal != currentIsLocal; - } - } - - clear() { - _items.clear(); - _isLocal = null; - } -} diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index 1ba66b864..8fb29bd41 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -3,7 +3,6 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; -import 'package:flutter_hbb/mobile/pages/file_manager_page.dart'; import 'package:get/get.dart'; import 'package:path/path.dart' as Path; @@ -1123,6 +1122,51 @@ class DirectoryOption { } } +class SelectedItems { + bool? _isLocal; + final List _items = []; + + List get items => _items; + + int get length => _items.length; + + bool? get isLocal => _isLocal; + + add(bool isLocal, Entry e) { + _isLocal ??= isLocal; + if (_isLocal != null && _isLocal != isLocal) { + return; + } + if (!_items.contains(e)) { + _items.add(e); + } + } + + bool contains(Entry e) { + return _items.contains(e); + } + + remove(Entry e) { + _items.remove(e); + if (_items.length == 0) { + _isLocal = null; + } + } + + bool isOtherPage(bool currentIsLocal) { + if (_isLocal == null) { + return false; + } else { + return _isLocal != currentIsLocal; + } + } + + clear() { + _items.clear(); + _isLocal = null; + } +} + // code from file_manager pkg after edit List _sortList(List list, SortBy sortType, bool ascending) { if (sortType == SortBy.Name) { From 64c44e1be6855808d875cce52efec39372cc702e Mon Sep 17 00:00:00 2001 From: csf Date: Mon, 17 Oct 2022 23:09:38 +0900 Subject: [PATCH 0710/2015] file transfer clear selected items onSearchText --- flutter/lib/desktop/pages/file_manager_page.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 3ffca3f91..f9521f4dc 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -833,8 +833,10 @@ class _FileManagerPageState extends State onSearchText(String searchText, bool isLocal) { if (isLocal) { + _localSelectedItems.clear(); _searchTextLocal.value = searchText; } else { + _remoteSelectedItems.clear(); _searchTextRemote.value = searchText; } } From 9a9a8197aec79ba97229127bfb3310c38d414da6 Mon Sep 17 00:00:00 2001 From: fufesou Date: Mon, 17 Oct 2022 07:55:14 -0700 Subject: [PATCH 0711/2015] fix linux uid username mismatch Signed-off-by: fufesou --- libs/hbb_common/src/platform/linux.rs | 40 ++++++++++++++-------- src/platform/linux.rs | 49 ++++++++++++++++++--------- src/server/connection.rs | 9 ++++- 3 files changed, 66 insertions(+), 32 deletions(-) diff --git a/libs/hbb_common/src/platform/linux.rs b/libs/hbb_common/src/platform/linux.rs index 0473a3bbc..1a20ed0e1 100644 --- a/libs/hbb_common/src/platform/linux.rs +++ b/libs/hbb_common/src/platform/linux.rs @@ -27,7 +27,18 @@ impl Disto { } pub fn get_display_server() -> String { - let session = get_value_of_seat0(0); + let mut session = get_values_of_seat0([0].to_vec())[0].clone(); + if session.is_empty() { + // loginctl has not given the expected output. try something else. + if let Ok(sid) = std::env::var("XDG_SESSION_ID") { + // could also execute "cat /proc/self/sessionid" + session = sid.to_owned(); + } + if session.is_empty() { + session = run_cmds("cat /proc/self/sessionid".to_owned()).unwrap_or_default(); + } + } + get_display_server_of_session(&session) } @@ -77,15 +88,16 @@ fn get_display_server_of_session(session: &str) -> String { } } -pub fn get_value_of_seat0(i: usize) -> String { +pub fn get_values_of_seat0(indices: Vec) -> Vec { if let Ok(output) = run_loginctl(None) { for line in String::from_utf8_lossy(&output.stdout).lines() { if line.contains("seat0") { if let Some(sid) = line.split_whitespace().nth(0) { if is_active(sid) { - if let Some(uid) = line.split_whitespace().nth(i) { - return uid.to_owned(); - } + return indices + .into_iter() + .map(|idx| line.split_whitespace().nth(idx).unwrap_or("").to_owned()) + .collect::>(); } } } @@ -98,21 +110,19 @@ pub fn get_value_of_seat0(i: usize) -> String { if let Some(sid) = line.split_whitespace().nth(0) { let d = get_display_server_of_session(sid); if is_active(sid) && d != "tty" { - if let Some(uid) = line.split_whitespace().nth(i) { - return uid.to_owned(); - } + return indices + .into_iter() + .map(|idx| line.split_whitespace().nth(idx).unwrap_or("").to_owned()) + .collect::>(); } } } } - // loginctl has not given the expected output. try something else. - if let Ok(sid) = std::env::var("XDG_SESSION_ID") { - // could also execute "cat /proc/self/sessionid" - return sid.to_owned(); - } - - return "".to_owned(); + return indices + .iter() + .map(|_x| "".to_owned()) + .collect::>(); } fn is_active(sid: &str) -> bool { diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 2efe06faf..b72c15f54 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -219,15 +219,15 @@ fn stop_rustdesk_servers() { fn should_start_server( try_x11: bool, uid: &mut String, + cur_uid: String, cm0: &mut bool, last_restart: &mut std::time::Instant, server: &mut Option, ) -> bool { let cm = get_cm(); - let tmp = get_active_userid(); let mut start_new = false; - if tmp != *uid && !tmp.is_empty() { - *uid = tmp; + if cur_uid != *uid && !cur_uid.is_empty() { + *uid = cur_uid; if try_x11 { set_x11_env(&uid); } @@ -267,7 +267,6 @@ fn should_start_server( pub fn start_os_service() { stop_rustdesk_servers(); - start_uinput_service(); let running = Arc::new(AtomicBool::new(true)); @@ -284,13 +283,20 @@ pub fn start_os_service() { let mut cm0 = false; let mut last_restart = std::time::Instant::now(); while running.load(Ordering::SeqCst) { - let username = get_active_username(); + let (cur_uid, cur_user) = get_active_user_id_name(); let is_wayland = current_is_wayland(); - if username == "root" || !is_wayland { + if cur_user == "root" || !is_wayland { stop_server(&mut user_server); // try start subprocess "--server" - if should_start_server(true, &mut uid, &mut cm0, &mut last_restart, &mut server) { + if should_start_server( + true, + &mut uid, + cur_uid, + &mut cm0, + &mut last_restart, + &mut server, + ) { match crate::run_me(vec!["--server"]) { Ok(ps) => server = Some(ps), Err(err) => { @@ -298,8 +304,8 @@ pub fn start_os_service() { } } } - } else if username != "" { - if username != "gdm" { + } else if cur_user != "" { + if cur_user != "gdm" { // try kill subprocess "--server" stop_server(&mut server); @@ -307,11 +313,12 @@ pub fn start_os_service() { if should_start_server( false, &mut uid, + cur_uid.clone(), &mut cm0, &mut last_restart, &mut user_server, ) { - match run_as_user("--server") { + match run_as_user("--server", Some((cur_uid, cur_user))) { Ok(ps) => user_server = ps, Err(err) => { log::error!("Failed to start server: {}", err); @@ -335,8 +342,13 @@ pub fn start_os_service() { log::info!("Exit"); } +pub fn get_active_user_id_name() -> (String, String) { + let vec_id_name = get_values_of_seat0([1, 2].to_vec()); + (vec_id_name[0].clone(), vec_id_name[1].clone()) +} + pub fn get_active_userid() -> String { - get_value_of_seat0(1) + get_values_of_seat0([1].to_vec())[0].clone() } fn get_cm() -> bool { @@ -513,7 +525,7 @@ fn _get_display_manager() -> String { } pub fn get_active_username() -> String { - get_value_of_seat0(2) + get_values_of_seat0([2].to_vec())[0].clone() } pub fn get_active_user_home() -> Option { @@ -545,12 +557,17 @@ fn is_opensuse() -> bool { false } -pub fn run_as_user(arg: &str) -> ResultType> { - let uid = get_active_userid(); +pub fn run_as_user( + arg: &str, + user: Option<(String, String)>, +) -> ResultType> { + let (uid, username) = match user { + Some(id_name) => id_name, + None => get_active_user_id_name(), + }; let cmd = std::env::current_exe()?; let xdg = &format!("XDG_RUNTIME_DIR=/run/user/{}", uid) as &str; - let username = &get_active_username(); - let mut args = vec![xdg, "-u", username, cmd.to_str().unwrap_or(""), arg]; + let mut args = vec![xdg, "-u", &username, cmd.to_str().unwrap_or(""), arg]; // -E required for opensuse if is_opensuse() { args.insert(0, "-E"); diff --git a/src/server/connection.rs b/src/server/connection.rs index e865a8b4f..4f122b59d 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1514,7 +1514,14 @@ async fn start_ipc( if crate::platform::is_root() { let mut res = Ok(None); for _ in 0..10 { - res = crate::platform::run_as_user("--cm"); + #[cfg(not(target_os = "linux"))] + { + res = crate::platform::run_as_user("--cm"); + } + #[cfg(target_os = "linux")] + { + res = crate::platform::run_as_user("--cm", None); + } if res.is_ok() { break; } From b2688da10f71878eabc9b8753912cf6e1bdbf742 Mon Sep 17 00:00:00 2001 From: fufesou Date: Mon, 17 Oct 2022 08:17:03 -0700 Subject: [PATCH 0712/2015] workaround of handle subprocess from run_as_user Signed-off-by: fufesou --- src/platform/linux.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/platform/linux.rs b/src/platform/linux.rs index b72c15f54..508ad456c 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -297,6 +297,10 @@ pub fn start_os_service() { &mut last_restart, &mut server, ) { + // to-do: stop_server(&mut user_server); may not stop child correctly + // stop_rustdesk_servers() is just a temp solution here. + stop_rustdesk_servers(); + std::thread::sleep(std::time::Duration::from_millis(super::SERVICE_INTERVAL)); match crate::run_me(vec!["--server"]) { Ok(ps) => server = Some(ps), Err(err) => { @@ -327,6 +331,7 @@ pub fn start_os_service() { } } } else { + stop_rustdesk_servers(); stop_server(&mut user_server); stop_server(&mut server); } From 2f33e9dfac4817c0072c77a104e47e51f2756080 Mon Sep 17 00:00:00 2001 From: fufesou Date: Mon, 17 Oct 2022 08:45:58 -0700 Subject: [PATCH 0713/2015] linux workaround --server orphan Signed-off-by: fufesou --- src/platform/linux.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 508ad456c..a02c4618b 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -322,6 +322,8 @@ pub fn start_os_service() { &mut last_restart, &mut user_server, ) { + stop_rustdesk_servers(); + std::thread::sleep(std::time::Duration::from_millis(super::SERVICE_INTERVAL)); match run_as_user("--server", Some((cur_uid, cur_user))) { Ok(ps) => user_server = ps, Err(err) => { @@ -332,6 +334,7 @@ pub fn start_os_service() { } } else { stop_rustdesk_servers(); + std::thread::sleep(std::time::Duration::from_millis(super::SERVICE_INTERVAL)); stop_server(&mut user_server); stop_server(&mut server); } From c24d7196002dc31c6a373a83201cba28a039e2f9 Mon Sep 17 00:00:00 2001 From: fufesou Date: Mon, 17 Oct 2022 18:27:14 -0700 Subject: [PATCH 0714/2015] debian prerm, fix condition expression Signed-off-by: fufesou --- res/DEBIAN/prerm | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/res/DEBIAN/prerm b/res/DEBIAN/prerm index 6d5026991..c7177e250 100755 --- a/res/DEBIAN/prerm +++ b/res/DEBIAN/prerm @@ -14,14 +14,17 @@ case $1 in rm /etc/systemd/system/rustdesk.service /usr/lib/systemd/system/rustdesk.service || true # workaround temp dev build between 1.1.9 and 1.2.0 - serverUser=$(ps -ef | grep -E 'rustdesk +--server' | awk '{print $1}' | head -1) ubuntuVersion=$(grep -oP 'VERSION_ID="\K[\d]+' /etc/os-release | bc -l) waylandSupportVersion=21 - if [ "$serverUser" != "" ] && [ "$serverUser" != "root" ] && [ "$ubuntuVersion" -ge "$waylandSupportVersion" ] + if [ "$ubuntuVersion" != "" ] && [ "$ubuntuVersion" -ge "$waylandSupportVersion" ] then - systemctl --machine=${serverUser}@.host --user stop rustdesk || true - fi - rm /usr/lib/systemd/user/rustdesk.service >/dev/null 2>/dev/null || true + serverUser=$(ps -ef | grep -E 'rustdesk +--server' | grep -v 'sudo ' | awk '{print $1}' | head -1) + if [ "$serverUser" != "" ] && [ "$serverUser" != "root" ] + then + systemctl --machine=${serverUser}@.host --user stop rustdesk >/dev/null 2>&1 || true + fi + fi + rm /usr/lib/systemd/user/rustdesk.service >/dev/null 2>&1 || true fi ;; esac From 0c976a6644aa275a6bfe71e1766c6ed13ed80033 Mon Sep 17 00:00:00 2001 From: csf Date: Tue, 18 Oct 2022 23:56:36 +0900 Subject: [PATCH 0715/2015] file transfer search bar pop_menu show Windows drives --- flutter/lib/common/widgets/overlay.dart | 2 +- .../lib/desktop/pages/file_manager_page.dart | 117 ++++++-- flutter/lib/desktop/widgets/popup_menu.dart | 280 +++++++++--------- .../lib/desktop/widgets/tabbar_widget.dart | 14 +- flutter/lib/main.dart | 2 +- flutter/lib/models/file_model.dart | 4 + 6 files changed, 251 insertions(+), 168 deletions(-) diff --git a/flutter/lib/common/widgets/overlay.dart b/flutter/lib/common/widgets/overlay.dart index 9680d1bf3..97815a998 100644 --- a/flutter/lib/common/widgets/overlay.dart +++ b/flutter/lib/common/widgets/overlay.dart @@ -107,7 +107,7 @@ class DraggableChatWindow extends StatelessWidget { icon: IconFont.close, onTap: chatModel.hideChatWindowOverlay, isClose: true, - size: 32, + boxSize: 32, )) ], ), diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index f9521f4dc..f55619ddf 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -6,6 +6,7 @@ import 'package:desktop_drop/desktop_drop.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_breadcrumb/flutter_breadcrumb.dart'; +import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; import 'package:flutter_hbb/models/file_model.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; @@ -455,7 +456,7 @@ class _FileManagerPageState extends State icon: const Icon(Icons.restart_alt_rounded)), ), IconButton( - icon: const Icon(Icons.delete), + icon: const Icon(Icons.delete_forever_outlined), splashRadius: 20, onPressed: () { model.jobTable.removeAt(index); @@ -742,32 +743,98 @@ class _FileManagerPageState extends State openDirectory(path, isLocal: isLocal); }); breadCrumbScrollToEnd(isLocal); + final locationBarKey = GlobalKey(debugLabel: "locationBarKey"); + return items.isEmpty ? Offstage() - : Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Expanded( - child: BreadCrumb( - items: items, - divider: Text("/").paddingSymmetric(horizontal: 4.0), - overflow: ScrollableOverflow( - controller: getBreadCrumbScrollController(isLocal)), - )), - DropdownButton( - isDense: true, - underline: Offstage(), - items: [ - // TODO: favourite - DropdownMenuItem( - child: Text('/'), - value: '/', - ) - ], - onChanged: (path) { - if (path is String && path.isNotEmpty) { - openDirectory(path, isLocal: isLocal); - } - }) - ]); + : Row( + key: locationBarKey, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: BreadCrumb( + items: items, + divider: Text("/", + style: TextStyle(color: Theme.of(context).hintColor)) + .paddingSymmetric(horizontal: 2.0), + overflow: ScrollableOverflow( + controller: getBreadCrumbScrollController(isLocal)), + )), + ActionIcon( + message: "", + icon: Icons.arrow_drop_down, + onTap: () async { + final renderBox = locationBarKey.currentContext + ?.findRenderObject() as RenderBox; + locationBarKey.currentContext?.size; + + final size = renderBox.size; + final offset = renderBox.localToGlobal(Offset.zero); + + final x = offset.dx; + final y = offset.dy + size.height + 1; + + final peerPlatform = (await bind.sessionGetPlatform( + id: _ffi.id, isRemote: !isLocal)) + .toLowerCase(); + final List menuItems; + if (peerPlatform == "windows") { + menuItems = []; + final loadingTag = + _ffi.dialogManager.showLoading("Waiting"); + try { + final fd = + await model.fetchDirectory("/", isLocal, false); + for (var entry in fd.entries) { + menuItems.add(MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + entry.name, + style: style, + ), + proc: () { + openDirectory(entry.name, isLocal: isLocal); + Get.back(); + })); + menuItems.add(MenuEntryDivider()); + } + } finally { + _ffi.dialogManager.dismissByTag(loadingTag); + } + } else { + menuItems = [ + MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + '/', + style: style, + ), + proc: () { + openDirectory('/', isLocal: isLocal); + Get.back(); + }), + MenuEntryDivider() + ]; + } + + mod_menu.showMenu( + context: context, + position: RelativeRect.fromLTRB(x, y, x, y), + elevation: 4, + items: menuItems + .map((e) => e.build( + context, + MenuConfig( + commonColor: + CustomPopupMenuTheme.commonColor, + height: CustomPopupMenuTheme.height, + dividerHeight: + CustomPopupMenuTheme.dividerHeight, + boxWidth: size.width))) + .expand((i) => i) + .toList()); + }, + iconSize: 20, + ) + ]); } List getPathBreadCrumbItems( diff --git a/flutter/lib/desktop/widgets/popup_menu.dart b/flutter/lib/desktop/widgets/popup_menu.dart index 5f06aebfe..20ab31ed9 100644 --- a/flutter/lib/desktop/widgets/popup_menu.dart +++ b/flutter/lib/desktop/widgets/popup_menu.dart @@ -99,12 +99,14 @@ class MenuConfig { final double height; final double dividerHeight; + final double? boxWidth; final Color commonColor; const MenuConfig( {required this.commonColor, this.height = kMinInteractiveDimension, - this.dividerHeight = 16.0}); + this.dividerHeight = 16.0, + this.boxWidth}); } abstract class MenuEntryBase { @@ -190,49 +192,51 @@ class MenuEntryRadios extends MenuEntryBase { return mod_menu.PopupMenuItem( padding: EdgeInsets.zero, height: conf.height, - child: TextButton( - child: Container( - padding: padding, - alignment: AlignmentDirectional.centerStart, - constraints: - BoxConstraints(minHeight: conf.height, maxHeight: conf.height), - child: Row( - children: [ - Text( - opt.text, - style: TextStyle( - color: Theme.of(context).textTheme.titleLarge?.color, - fontSize: MenuConfig.fontSize, - fontWeight: FontWeight.normal), + child: Container( + width: conf.boxWidth, + child: TextButton( + child: Container( + padding: padding, + alignment: AlignmentDirectional.centerStart, + constraints: BoxConstraints( + minHeight: conf.height, maxHeight: conf.height), + child: Row( + children: [ + Text( + opt.text, + style: TextStyle( + color: Theme.of(context).textTheme.titleLarge?.color, + fontSize: MenuConfig.fontSize, + fontWeight: FontWeight.normal), + ), + Expanded( + child: Align( + alignment: Alignment.centerRight, + child: Transform.scale( + scale: MenuConfig.iconScale, + child: Obx(() => opt.value == curOption.value + ? IconButton( + padding: const EdgeInsets.fromLTRB( + 8.0, 0.0, 8.0, 0.0), + hoverColor: Colors.transparent, + focusColor: Colors.transparent, + onPressed: () {}, + icon: Icon( + Icons.check, + color: conf.commonColor, + )) + : const SizedBox.shrink()), + ))), + ], ), - Expanded( - child: Align( - alignment: Alignment.centerRight, - child: Transform.scale( - scale: MenuConfig.iconScale, - child: Obx(() => opt.value == curOption.value - ? IconButton( - padding: const EdgeInsets.fromLTRB( - 8.0, 0.0, 8.0, 0.0), - hoverColor: Colors.transparent, - focusColor: Colors.transparent, - onPressed: () {}, - icon: Icon( - Icons.check, - color: conf.commonColor, - )) - : const SizedBox.shrink()), - ))), - ], - ), - ), - onPressed: () { - if (opt.dismissOnClicked && Navigator.canPop(context)) { - Navigator.pop(context); - } - setOption(opt.value); - }, - ), + ), + onPressed: () { + if (opt.dismissOnClicked && Navigator.canPop(context)) { + Navigator.pop(context); + } + setOption(opt.value); + }, + )), ); } @@ -285,48 +289,50 @@ class MenuEntrySubRadios extends MenuEntryBase { return mod_menu.PopupMenuItem( padding: EdgeInsets.zero, height: conf.height, - child: TextButton( - child: Container( - padding: padding, - alignment: AlignmentDirectional.centerStart, - constraints: - BoxConstraints(minHeight: conf.height, maxHeight: conf.height), - child: Row( - children: [ - Text( - opt.text, - style: TextStyle( - color: Theme.of(context).textTheme.titleLarge?.color, - fontSize: MenuConfig.fontSize, - fontWeight: FontWeight.normal), + child: Container( + width: conf.boxWidth, + child: TextButton( + child: Container( + padding: padding, + alignment: AlignmentDirectional.centerStart, + constraints: BoxConstraints( + minHeight: conf.height, maxHeight: conf.height), + child: Row( + children: [ + Text( + opt.text, + style: TextStyle( + color: Theme.of(context).textTheme.titleLarge?.color, + fontSize: MenuConfig.fontSize, + fontWeight: FontWeight.normal), + ), + Expanded( + child: Align( + alignment: Alignment.centerRight, + child: Transform.scale( + scale: MenuConfig.iconScale, + child: Obx(() => opt.value == curOption.value + ? IconButton( + padding: EdgeInsets.zero, + hoverColor: Colors.transparent, + focusColor: Colors.transparent, + onPressed: () {}, + icon: Icon( + Icons.check, + color: conf.commonColor, + )) + : const SizedBox.shrink())), + )), + ], ), - Expanded( - child: Align( - alignment: Alignment.centerRight, - child: Transform.scale( - scale: MenuConfig.iconScale, - child: Obx(() => opt.value == curOption.value - ? IconButton( - padding: EdgeInsets.zero, - hoverColor: Colors.transparent, - focusColor: Colors.transparent, - onPressed: () {}, - icon: Icon( - Icons.check, - color: conf.commonColor, - )) - : const SizedBox.shrink())), - )), - ], - ), - ), - onPressed: () { - if (opt.dismissOnClicked && Navigator.canPop(context)) { - Navigator.pop(context); - } - setOption(opt.value); - }, - ), + ), + onPressed: () { + if (opt.dismissOnClicked && Navigator.canPop(context)) { + Navigator.pop(context); + } + setOption(opt.value); + }, + )), ); } @@ -401,55 +407,57 @@ abstract class MenuEntrySwitchBase extends MenuEntryBase { mod_menu.PopupMenuItem( padding: EdgeInsets.zero, height: conf.height, - child: TextButton( - child: Container( - padding: padding, - alignment: AlignmentDirectional.centerStart, - height: conf.height, - child: Row(children: [ - Obx(() => Text( - text, - style: textStyle!.value, - )), - Expanded( - child: Align( - alignment: Alignment.centerRight, - child: Transform.scale( - scale: MenuConfig.iconScale, - child: Obx(() { - if (switchType == SwitchType.sswitch) { - return Switch( - value: curOption.value, - onChanged: (v) { - if (super.dismissOnClicked && - Navigator.canPop(context)) { - Navigator.pop(context); - } - setOption(v); - }, - ); - } else { - return Checkbox( - value: curOption.value, - onChanged: (v) { - if (super.dismissOnClicked && - Navigator.canPop(context)) { - Navigator.pop(context); - } - setOption(v); - }, - ); - } - })), - )) - ])), - onPressed: () { - if (super.dismissOnClicked && Navigator.canPop(context)) { - Navigator.pop(context); - } - setOption(!curOption.value); - }, - ), + child: Container( + width: conf.boxWidth, + child: TextButton( + child: Container( + padding: padding, + alignment: AlignmentDirectional.centerStart, + height: conf.height, + child: Row(children: [ + Obx(() => Text( + text, + style: textStyle!.value, + )), + Expanded( + child: Align( + alignment: Alignment.centerRight, + child: Transform.scale( + scale: MenuConfig.iconScale, + child: Obx(() { + if (switchType == SwitchType.sswitch) { + return Switch( + value: curOption.value, + onChanged: (v) { + if (super.dismissOnClicked && + Navigator.canPop(context)) { + Navigator.pop(context); + } + setOption(v); + }, + ); + } else { + return Checkbox( + value: curOption.value, + onChanged: (v) { + if (super.dismissOnClicked && + Navigator.canPop(context)) { + Navigator.pop(context); + } + setOption(v); + }, + ); + } + })), + )) + ])), + onPressed: () { + if (super.dismissOnClicked && Navigator.canPop(context)) { + Navigator.pop(context); + } + setOption(!curOption.value); + }, + )), ) ]; } @@ -606,7 +614,9 @@ class MenuEntryButton extends MenuEntryBase { fontSize: MenuConfig.fontSize, fontWeight: FontWeight.normal); super.enabled ??= true.obs; - return Obx(() => TextButton( + return Obx(() => Container( + width: conf.boxWidth, + child: TextButton( onPressed: super.enabled!.value ? () { if (super.dismissOnClicked && Navigator.canPop(context)) { @@ -623,7 +633,7 @@ class MenuEntryButton extends MenuEntryBase { child: childBuilder( super.enabled!.value ? enabledStyle : disabledStyle), ), - )); + ))); } @override diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 50c238a36..1d774143c 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -811,14 +811,16 @@ class ActionIcon extends StatelessWidget { final IconData icon; final Function() onTap; final bool isClose; - final double? size; + final double iconSize; + final double boxSize; const ActionIcon( {Key? key, required this.message, required this.icon, required this.onTap, - required this.isClose, - this.size}) + this.isClose = false, + this.iconSize = _kActionIconSize, + this.boxSize = _kTabBarHeight - 1}) : super(key: key); @override @@ -834,14 +836,14 @@ class ActionIcon extends StatelessWidget { onHover: (value) => hover.value = value, onTap: onTap, child: SizedBox( - height: size ?? (_kTabBarHeight - 1), - width: size ?? (_kTabBarHeight - 1), + height: boxSize, + width: boxSize, child: Icon( icon, color: hover.value && isClose ? Colors.white : MyTheme.tabbar(context).unSelectedIconColor, - size: _kActionIconSize, + size: iconSize, ), ), ), diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 38417f25f..5009e68a1 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -221,7 +221,7 @@ void runConnectionManagerScreen() async { windowManager.setOpacity(1) ]); // ensure - windowManager.setAlignment(Alignment.topRight); + windowManager.setAlignment(Alignment.topRight); }); } diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index 8fb29bd41..d60858c6e 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -402,6 +402,10 @@ class FileModel extends ChangeNotifier { } } + Future fetchDirectory(path, isLocal, showHidden) async { + return await _fileFetcher.fetchDirectory(path, isLocal, showHidden); + } + void pushHistory(bool isLocal) { final history = isLocal ? localHistory : remoteHistory; final currPath = isLocal ? currentLocalDir.path : currentRemoteDir.path; From 0bced441264ed91a42e7fdff1ef3ed3c32f0077a Mon Sep 17 00:00:00 2001 From: csf Date: Wed, 19 Oct 2022 10:52:29 +0900 Subject: [PATCH 0716/2015] fix globalKey / handle Windows drive fd type / add un/select all --- .../lib/desktop/pages/file_manager_page.dart | 95 +++++++++++++------ flutter/lib/models/file_model.dart | 9 +- src/lang/cn.rs | 2 + src/lang/cs.rs | 4 +- src/lang/da.rs | 8 +- src/lang/de.rs | 4 +- src/lang/eo.rs | 4 +- src/lang/es.rs | 4 +- src/lang/fr.rs | 4 +- src/lang/hu.rs | 4 +- src/lang/id.rs | 4 +- src/lang/it.rs | 4 +- src/lang/ja.rs | 4 +- src/lang/ko.rs | 4 +- src/lang/kz.rs | 4 +- src/lang/pl.rs | 4 +- src/lang/pt_PT.rs | 4 +- src/lang/ptbr.rs | 4 +- src/lang/ru.rs | 4 +- src/lang/sk.rs | 4 +- src/lang/template.rs | 4 +- src/lang/tr.rs | 4 +- src/lang/tw.rs | 4 +- src/lang/ua.rs | 4 +- src/lang/vn.rs | 4 +- 25 files changed, 144 insertions(+), 54 deletions(-) diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index f55619ddf..a225b55b0 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -48,6 +48,8 @@ class _FileManagerPageState extends State final _locationStatusRemote = LocationStatus.bread.obs; final _locationNodeLocal = FocusNode(debugLabel: "locationNodeLocal"); final _locationNodeRemote = FocusNode(debugLabel: "locationNodeRemote"); + final _locationBarKeyLocal = GlobalKey(debugLabel: "locationBarKeyLocal"); + final _locationBarKeyRemote = GlobalKey(debugLabel: "locationBarKeyRemote"); final _searchTextLocal = "".obs; final _searchTextRemote = "".obs; final _breadCrumbScrollerLocal = ScrollController(); @@ -63,11 +65,15 @@ class _FileManagerPageState extends State return isLocal ? _breadCrumbScrollerLocal : _breadCrumbScrollerRemote; } + GlobalKey getLocationBarKey(bool isLocal) { + return isLocal ? _locationBarKeyLocal : _locationBarKeyRemote; + } + late FFI _ffi; FileModel get model => _ffi.fileModel; - SelectedItems getSelectedItem(bool isLocal) { + SelectedItems getSelectedItems(bool isLocal) { return isLocal ? _localSelectedItems : _remoteSelectedItems; } @@ -133,7 +139,7 @@ class _FileManagerPageState extends State Widget menu({bool isLocal = false}) { var menuPos = RelativeRect.fill; - final items = [ + final List> items = [ MenuEntrySwitch( switchType: SwitchType.scheckbox, text: translate("Show Hidden Files"), @@ -146,6 +152,18 @@ class _FileManagerPageState extends State padding: kDesktopMenuPadding, dismissOnClicked: true, ), + MenuEntryButton( + childBuilder: (style) => Text(translate("Select All"), style: style), + proc: () => setState(() => getSelectedItems(isLocal) + .selectAll(model.getCurrentDir(isLocal).entries)), + padding: kDesktopMenuPadding, + dismissOnClicked: true), + MenuEntryButton( + childBuilder: (style) => + Text(translate("Unselect All"), style: style), + proc: () => setState(() => getSelectedItems(isLocal).clear()), + padding: kDesktopMenuPadding, + dismissOnClicked: true) ]; return Listener( @@ -273,11 +291,10 @@ class _FileManagerPageState extends State return DataRow( key: ValueKey(entry.name), onSelectChanged: (s) { - _onSelectedChanged(getSelectedItem(isLocal), filteredEntries, + _onSelectedChanged(getSelectedItems(isLocal), filteredEntries, entry, isLocal); - setState(() {}); }, - selected: getSelectedItem(isLocal).contains(entry), + selected: getSelectedItems(isLocal).contains(entry), cells: [ DataCell( Container( @@ -287,7 +304,11 @@ class _FileManagerPageState extends State message: entry.name, child: Row(children: [ Icon( - entry.isFile ? Icons.feed_outlined : Icons.folder, + entry.isFile + ? Icons.feed_outlined + : entry.isDrive + ? Icons.computer + : Icons.folder, size: 20, color: Theme.of(context) .iconTheme @@ -300,7 +321,7 @@ class _FileManagerPageState extends State ]), )), onTap: () { - final items = getSelectedItem(isLocal); + final items = getSelectedItems(isLocal); // handle double click if (_checkDoubleClick(entry)) { @@ -486,6 +507,7 @@ class _FileManagerPageState extends State final locationStatus = isLocal ? _locationStatusLocal : _locationStatusRemote; final locationFocus = isLocal ? _locationNodeLocal : _locationNodeRemote; + final selectedItems = getSelectedItems(isLocal); return Container( child: Column( children: [ @@ -534,6 +556,7 @@ class _FileManagerPageState extends State icon: const Icon(Icons.arrow_back), splashRadius: 20, onPressed: () { + selectedItems.clear(); model.goBack(isLocal: isLocal); }, ), @@ -541,6 +564,7 @@ class _FileManagerPageState extends State icon: const Icon(Icons.arrow_upward), splashRadius: 20, onPressed: () { + selectedItems.clear(); model.goToParentDirectory(isLocal: isLocal); }, ), @@ -609,6 +633,7 @@ class _FileManagerPageState extends State }), IconButton( onPressed: () { + breadCrumbScrollToEnd(isLocal); model.refresh(isLocal: isLocal); }, splashRadius: 20, @@ -673,13 +698,13 @@ class _FileManagerPageState extends State splashRadius: 20, icon: const Icon(Icons.create_new_folder_outlined)), IconButton( - onPressed: () async { - final items = isLocal - ? _localSelectedItems - : _remoteSelectedItems; - await (model.removeAction(items, isLocal: isLocal)); - items.clear(); - }, + onPressed: validItems(selectedItems) + ? () async { + await (model.removeAction(selectedItems, + isLocal: isLocal)); + selectedItems.clear(); + } + : null, splashRadius: 20, icon: const Icon(Icons.delete_forever_outlined)), menu(isLocal: isLocal), @@ -687,11 +712,12 @@ class _FileManagerPageState extends State ), ), TextButton.icon( - onPressed: () { - final items = getSelectedItem(isLocal); - model.sendFiles(items, isRemote: !isLocal); - items.clear(); - }, + onPressed: validItems(selectedItems) + ? () { + model.sendFiles(selectedItems, isRemote: !isLocal); + selectedItems.clear(); + } + : null, icon: Transform.rotate( angle: isLocal ? 0 : pi, child: const Icon( @@ -707,6 +733,14 @@ class _FileManagerPageState extends State )); } + bool validItems(SelectedItems items) { + if (items.length > 0) { + // exclude DirDrive type + return items.items.any((item) => !item.isDrive); + } + return false; + } + @override bool get wantKeepAlive => true; @@ -742,8 +776,7 @@ class _FileManagerPageState extends State } openDirectory(path, isLocal: isLocal); }); - breadCrumbScrollToEnd(isLocal); - final locationBarKey = GlobalKey(debugLabel: "locationBarKey"); + final locationBarKey = getLocationBarKey(isLocal); return items.isEmpty ? Offstage() @@ -780,11 +813,13 @@ class _FileManagerPageState extends State final List menuItems; if (peerPlatform == "windows") { menuItems = []; - final loadingTag = - _ffi.dialogManager.showLoading("Waiting"); + var loadingTag = ""; + if (!isLocal) { + loadingTag = _ffi.dialogManager.showLoading("Waiting"); + } try { final fd = - await model.fetchDirectory("/", isLocal, false); + await model.fetchDirectory("/", isLocal, isLocal); for (var entry in fd.entries) { menuItems.add(MenuEntryButton( childBuilder: (TextStyle? style) => Text( @@ -793,12 +828,14 @@ class _FileManagerPageState extends State ), proc: () { openDirectory(entry.name, isLocal: isLocal); - Get.back(); - })); + }, + dismissOnClicked: true)); menuItems.add(MenuEntryDivider()); } } finally { - _ffi.dialogManager.dismissByTag(loadingTag); + if (!isLocal) { + _ffi.dialogManager.dismissByTag(loadingTag); + } } } else { menuItems = [ @@ -809,8 +846,8 @@ class _FileManagerPageState extends State ), proc: () { openDirectory('/', isLocal: isLocal); - Get.back(); - }), + }, + dismissOnClicked: true), MenuEntryDivider() ]; } diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index d60858c6e..a95e44ddf 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -1031,7 +1031,9 @@ class Entry { bool get isFile => entryType > 3; - bool get isDirectory => entryType <= 3; + bool get isDirectory => entryType < 3; + + bool get isDrive => entryType == 3; DateTime lastModified() { return DateTime.fromMillisecondsSinceEpoch(modifiedTime * 1000); @@ -1169,6 +1171,11 @@ class SelectedItems { _items.clear(); _isLocal = null; } + + void selectAll(List entries) { + _items.clear(); + _items.addAll(entries); + } } // code from file_manager pkg after edit diff --git a/src/lang/cn.rs b/src/lang/cn.rs index cf2575a7f..32e30f81c 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -87,6 +87,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Delete", "删除"), ("Properties", "属性"), ("Multi Select", "多选"), + ("Select All", "全选"), + ("Unselect All", "取消全选"), ("Empty Directory", "空文件夹"), ("Not an empty directory", "这不是一个空文件夹"), ("Are you sure you want to delete this file?", "是否删除此文件?"), diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 162fdc1ed..6610c312b 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -87,6 +87,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Delete", "Smazat"), ("Properties", "Vlastnosti"), ("Multi Select", "Vícenásobný výběr"), + ("Select All", ""), + ("Unselect All", ""), ("Empty Directory", "Prázdná složka"), ("Not an empty directory", "Neprázdná složka"), ("Are you sure you want to delete this file?", "Opravdu chcete tento soubor vymazat?"), @@ -112,7 +114,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Good image quality", "Dobrá kvalita obrazu"), ("Balanced", "Vyvážené"), ("Optimize reaction time", "Optimalizovat pro co nejnižší prodlevu odezvy"), - ("Custom", "Uživatelsky určené"), + ("Custom", ""), ("Show remote cursor", "Zobrazovat ukazatel myši z protějšku"), ("Show quality monitor", ""), ("Disable clipboard", "Vypnout schránku"), diff --git a/src/lang/da.rs b/src/lang/da.rs index df9929d1f..3bf05a3d6 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -87,6 +87,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Delete", "Slet"), ("Properties", "Egenskaber"), ("Multi Select", "Flere valg"), + ("Select All", ""), + ("Unselect All", ""), ("Empty Directory", "Tom bibliotek"), ("Not an empty directory", "Intet tomt bibliotek"), ("Are you sure you want to delete this file?", "Er du sikker på, at du vil slette denne fil?"), @@ -112,7 +114,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Good image quality", "God billedkvalitet"), ("Balanced", "Afbalanceret"), ("Optimize reaction time", "Optimeret responstid"), - ("Custom", "Brugerdefineret"), + ("Custom", ""), ("Show remote cursor", "Vis fjernbetjeningskontrolleret markør"), ("Show quality monitor", ""), ("Disable clipboard", "Deaktiver udklipsholder"), @@ -193,7 +195,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Reboot required", "Genstart krævet"), ("Unsupported display server ", "Ikke-understøttet displayserver"), ("x11 expected", "X11 Forventet"), - ("Port", ""), + ("Port", "Port"), ("Settings", "Indstillinger"), ("Username", " Brugernavn"), ("Invalid port", "Ugyldig port"), @@ -274,7 +276,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_stop_service_tip", "Ved at lukke tjenesten lukkes alle fremstillede forbindelser automatisk."), ("android_version_audio_tip", "Den aktuelle Android -version understøtter ikke lydoptagelse, skal du opdatere om Android 10 eller højere."), ("android_start_service_tip", "Tryk på [Start Service] eller åbn autorisationen [skærmoptagelse] for at starte skærmudgivelsen."), - ("Account", ""), + ("Account", "Konto"), ("Overwrite", "Overskriv"), ("This file exists, skip or overwrite this file?", "Denne fil findes, springer over denne fil eller overskriver?"), ("Quit", "Afslut"), diff --git a/src/lang/de.rs b/src/lang/de.rs index b0f6d0ba3..095206f8e 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -87,6 +87,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Delete", "Löschen"), ("Properties", "Eigenschaften"), ("Multi Select", "Mehrfachauswahl"), + ("Select All", ""), + ("Unselect All", ""), ("Empty Directory", "Leerer Ordner"), ("Not an empty directory", "Ordner ist nicht leer"), ("Are you sure you want to delete this file?", "Sind Sie sicher, dass Sie diese Datei löschen wollen?"), @@ -112,7 +114,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Good image quality", "Qualität"), ("Balanced", "Ausgeglichen"), ("Optimize reaction time", "Geschwindigkeit"), - ("Custom", "Benutzerdefiniert"), + ("Custom", ""), ("Show remote cursor", "Entfernten Cursor anzeigen"), ("Show quality monitor", "Qualitätsüberwachung anzeigen"), ("Disable clipboard", "Zwischenablage deaktivieren"), diff --git a/src/lang/eo.rs b/src/lang/eo.rs index ca0f12bcd..1b2a391d9 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -87,6 +87,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Delete", ""), ("Properties", ""), ("Multi Select", ""), + ("Select All", ""), + ("Unselect All", ""), ("Empty Directory", ""), ("Not an empty directory", ""), ("Are you sure you want to delete this file?", "Ĉu vi vere volas forigi tiun dosieron?"), @@ -112,7 +114,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Good image quality", "Bona bilda kvalito"), ("Balanced", "Normala bilda kvalito"), ("Optimize reaction time", "Optimigi reakcia tempo"), - ("Custom", "Personigi bilda kvalito"), + ("Custom", ""), ("Show remote cursor", "Montri foran kursoron"), ("Show quality monitor", ""), ("Disable clipboard", "Malebligi poŝon"), diff --git a/src/lang/es.rs b/src/lang/es.rs index 3be5f920e..ef90bb711 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -87,6 +87,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Delete", "Borrar"), ("Properties", "Propiedades"), ("Multi Select", "Selección múltiple"), + ("Select All", ""), + ("Unselect All", ""), ("Empty Directory", "Directorio vacío"), ("Not an empty directory", "No es un directorio vacío"), ("Are you sure you want to delete this file?", "Estás seguro de que quieres eliminar este archivo?"), @@ -112,7 +114,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Good image quality", "Buena calidad de imagen"), ("Balanced", "Equilibrado"), ("Optimize reaction time", "Optimizar el tiempo de reacción"), - ("Custom", "Personalizado"), + ("Custom", ""), ("Show remote cursor", "Mostrar cursor remoto"), ("Show quality monitor", "Mostrar calidad del monitor"), ("Disable clipboard", "Deshabilitar portapapeles"), diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 484c415fc..8b6420823 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -87,6 +87,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Delete", "Supprimer"), ("Properties", "Propriétés"), ("Multi Select", "Choix multiple"), + ("Select All", ""), + ("Unselect All", ""), ("Empty Directory", "Répertoire vide"), ("Not an empty directory", "Pas un répertoire vide"), ("Are you sure you want to delete this file?", "Voulez-vous vraiment supprimer ce fichier?"), @@ -112,7 +114,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Good image quality", "Bonne qualité d'image"), ("Balanced", "Qualité d'image normale"), ("Optimize reaction time", "Optimiser le temps de réaction"), - ("Custom", "Qualité d'image personnalisée"), + ("Custom", ""), ("Show remote cursor", "Afficher le curseur distant"), ("Show quality monitor", ""), ("Disable clipboard", "Désactiver le presse-papier"), diff --git a/src/lang/hu.rs b/src/lang/hu.rs index c2d5903e1..bbc0594a5 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -87,6 +87,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Delete", "Törlés"), ("Properties", "Tulajdonságok"), ("Multi Select", "Több fájl kiválasztása"), + ("Select All", ""), + ("Unselect All", ""), ("Empty Directory", "Üres Könyvtár"), ("Not an empty directory", "Nem egy üres könyvtár"), ("Are you sure you want to delete this file?", "Biztosan törölni szeretnéd ezt a fájlt?"), @@ -112,7 +114,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Good image quality", "Jó képminőség"), ("Balanced", "Balanszolt"), ("Optimize reaction time", "Válaszidő optimializálása"), - ("Custom", "Egyedi"), + ("Custom", ""), ("Show remote cursor", "Távoli kurzor mutatása"), ("Show quality monitor", "Minőségi monitor mutatása"), ("Disable clipboard", "Vágólap Kikapcsolása"), diff --git a/src/lang/id.rs b/src/lang/id.rs index 33aae1579..fd0b3bbf8 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -87,6 +87,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Delete", "Hapus"), ("Properties", "Properti"), ("Multi Select", "Pilih Beberapa"), + ("Select All", ""), + ("Unselect All", ""), ("Empty Directory", "Folder Kosong"), ("Not an empty directory", "Folder tidak kosong"), ("Are you sure you want to delete this file?", "Apakah anda yakin untuk menghapus file ini?"), @@ -112,7 +114,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Good image quality", "Kualitas Gambar Baik"), ("Balanced", "Seimbang"), ("Optimize reaction time", "Optimalkan waktu reaksi"), - ("Custom", "Custom"), + ("Custom", ""), ("Show remote cursor", "Tampilkan remote kursor"), ("Show quality monitor", ""), ("Disable clipboard", "Matikan papan klip"), diff --git a/src/lang/it.rs b/src/lang/it.rs index 5b37e9291..bf2c76fe8 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -87,6 +87,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Delete", "Eliminare"), ("Properties", "Proprietà"), ("Multi Select", "Selezione multipla"), + ("Select All", ""), + ("Unselect All", ""), ("Empty Directory", "Directory vuota"), ("Not an empty directory", "Non una directory vuota"), ("Are you sure you want to delete this file?", "Vuoi davvero eliminare questo file?"), @@ -112,7 +114,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Good image quality", "Buona qualità immagine"), ("Balanced", "Bilanciato"), ("Optimize reaction time", "Ottimizza il tempo di reazione"), - ("Custom", "Personalizzato"), + ("Custom", ""), ("Show remote cursor", "Mostra il cursore remoto"), ("Show quality monitor", ""), ("Disable clipboard", "Disabilita appunti"), diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 593ef0186..38e4c0c7d 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -87,6 +87,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Delete", "削除"), ("Properties", "プロパティ"), ("Multi Select", "複数選択"), + ("Select All", ""), + ("Unselect All", ""), ("Empty Directory", "空のディレクトリ"), ("Not an empty directory", "空ではないディレクトリ"), ("Are you sure you want to delete this file?", "本当にこのファイルを削除しますか?"), @@ -112,7 +114,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Good image quality", "画質優先"), ("Balanced", "バランス"), ("Optimize reaction time", "速度優先"), - ("Custom", "カスタム"), + ("Custom", ""), ("Show remote cursor", "リモート側のカーソルを表示"), ("Show quality monitor", "品質モニターを表示"), ("Disable clipboard", "クリップボードを無効化"), diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 1b668462e..740465323 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -87,6 +87,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Delete", "삭제"), ("Properties", "속성"), ("Multi Select", "다중 선택"), + ("Select All", ""), + ("Unselect All", ""), ("Empty Directory", "빈 디렉터리"), ("Not an empty directory", "디렉터리가 비어있지 않습니다"), ("Are you sure you want to delete this file?", "정말로 해당 파일을 삭제하시겠습니까?"), @@ -112,7 +114,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Good image quality", "최적 이미지 품질"), ("Balanced", "균형"), ("Optimize reaction time", "반응 시간 최적화"), - ("Custom", "커스텀"), + ("Custom", ""), ("Show remote cursor", "원격 커서 보이기"), ("Show quality monitor", "품질 모니터 띄우기"), ("Disable clipboard", "클립보드 비활성화"), diff --git a/src/lang/kz.rs b/src/lang/kz.rs index e55b0a3e3..a4f25dbf8 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -87,6 +87,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Delete", "Жою"), ("Properties", "Қасиеттер"), ("Multi Select", "Көптік таңдау"), + ("Select All", ""), + ("Unselect All", ""), ("Empty Directory", "Бос Бума"), ("Not an empty directory", "Бос бума емес"), ("Are you sure you want to delete this file?", "Бұл файылды жоюға сенімдісіз бе?"), @@ -112,7 +114,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Good image quality", "Жақсы сурет сапасы"), ("Balanced", "Теңдестірілген"), ("Optimize reaction time", "Реакция уақытын оңтайландыру"), - ("Custom", "Теңшеулі"), + ("Custom", ""), ("Show remote cursor", "Қашықтағы курсорды көрсету"), ("Show quality monitor", "Сапа мониторын көрсету"), ("Disable clipboard", "Көшіру-тақтасын өшіру"), diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 16f1a5ff3..e46796148 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -87,6 +87,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Delete", "Usuń"), ("Properties", "Właściwości"), ("Multi Select", "Wielokrotny wybór"), + ("Select All", ""), + ("Unselect All", ""), ("Empty Directory", "Pusty katalog"), ("Not an empty directory", "Katalog nie jest pusty"), ("Are you sure you want to delete this file?", "Czy na pewno chcesz usunąć ten plik?"), @@ -112,7 +114,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Good image quality", "Dobra jakość obrazu"), ("Balanced", "Zrównoważony"), ("Optimize reaction time", "Zoptymalizuj czas reakcji"), - ("Custom", "Niestandardowy"), + ("Custom", ""), ("Show remote cursor", "Pokazuj zdalny kursor"), ("Show quality monitor", "Pokazuj jakość monitora"), ("Disable clipboard", "Wyłącz schowek"), diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index ea90329a5..69b61d622 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -87,6 +87,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Delete", "Apagar"), ("Properties", "Propriedades"), ("Multi Select", "Selecção Múltipla"), + ("Select All", ""), + ("Unselect All", ""), ("Empty Directory", "Directório Vazio"), ("Not an empty directory", "Directório não está vazio"), ("Are you sure you want to delete this file?", "Tem certeza que deseja apagar este ficheiro?"), @@ -112,7 +114,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Good image quality", "Qualidade visual boa"), ("Balanced", "Equilibrada"), ("Optimize reaction time", "Optimizar tempo de reacção"), - ("Custom", "Personalizado"), + ("Custom", ""), ("Show remote cursor", "Mostrar cursor remoto"), ("Show quality monitor", ""), ("Disable clipboard", "Desabilitar área de transferência"), diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 8a6280aed..e93cbef33 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -87,6 +87,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Delete", "Apagar"), ("Properties", "Propriedades"), ("Multi Select", "Seleção Múltipla"), + ("Select All", ""), + ("Unselect All", ""), ("Empty Directory", "Diretório Vazio"), ("Not an empty directory", "Diretório não está vazio"), ("Are you sure you want to delete this file?", "Tem certeza que deseja apagar este arquivo?"), @@ -112,7 +114,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Good image quality", "Qualidade visual boa"), ("Balanced", "Balanceada"), ("Optimize reaction time", "Otimizar tempo de reação"), - ("Custom", "Personalizado"), + ("Custom", ""), ("Show remote cursor", "Mostrar cursor remoto"), ("Show quality monitor", ""), ("Disable clipboard", "Desabilitar área de transferência"), diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 5fe9c89ec..eccc28602 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -87,6 +87,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Delete", "Удалить"), ("Properties", "Свойства"), ("Multi Select", "Многоэлементный выбор"), + ("Select All", ""), + ("Unselect All", ""), ("Empty Directory", "Пустая папка"), ("Not an empty directory", "Папка не пуста"), ("Are you sure you want to delete this file?", "Вы уверены, что хотите удалить этот файл?"), @@ -112,7 +114,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Good image quality", "Хорошее качество изображения"), ("Balanced", "Сбалансированный"), ("Optimize reaction time", "Оптимизировать время реакции"), - ("Custom", "Пользовательский"), + ("Custom", ""), ("Show remote cursor", "Показать удаленный курсор"), ("Show quality monitor", "Показать качество"), ("Disable clipboard", "Отключить буфер обмена"), diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 23c8b7220..20e096f0e 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -87,6 +87,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Delete", "Zmazať"), ("Properties", "Vlastnosti"), ("Multi Select", "Viacnásobný výber"), + ("Select All", ""), + ("Unselect All", ""), ("Empty Directory", "Prázdny adresár"), ("Not an empty directory", "Nie prázdny adresár"), ("Are you sure you want to delete this file?", "Ste si istý, že chcete zmazať tento súbor?"), @@ -112,7 +114,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Good image quality", "Dobrá kvalita obrazu"), ("Balanced", "Vyvážené"), ("Optimize reaction time", "Optimalizované pre čas odozvy"), - ("Custom", "Vlastné"), + ("Custom", ""), ("Show remote cursor", "Zobrazovať vzdialený ukazovateľ myši"), ("Show quality monitor", ""), ("Disable clipboard", "Vypnúť schránku"), diff --git a/src/lang/template.rs b/src/lang/template.rs index 6d549b02a..87dcc3a48 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -29,8 +29,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable TCP Tunneling", ""), ("IP Whitelisting", ""), ("ID/Relay Server", ""), - ("Import server configuration successfully", ""), ("Import Server Conf", ""), + ("Import server configuration successfully", ""), ("Invalid server configuration", ""), ("Clipboard is empty", ""), ("Stop service", ""), @@ -87,6 +87,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Delete", ""), ("Properties", ""), ("Multi Select", ""), + ("Select All", ""), + ("Unselect All", ""), ("Empty Directory", ""), ("Not an empty directory", ""), ("Are you sure you want to delete this file?", ""), diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 1cf50b2ca..f8d8b5fff 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -87,6 +87,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Delete", "Sil"), ("Properties", "Özellikler"), ("Multi Select", "Çoklu Seçim"), + ("Select All", ""), + ("Unselect All", ""), ("Empty Directory", "Boş Klasör"), ("Not an empty directory", "Klasör boş değil"), ("Are you sure you want to delete this file?", "Bu dosyayı silmek istediğinize emin misiniz?"), @@ -112,7 +114,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Good image quality", "İyi görüntü kalitesi"), ("Balanced", "Dengelenmiş"), ("Optimize reaction time", "Tepki süresini optimize et"), - ("Custom", "Özel"), + ("Custom", ""), ("Show remote cursor", "Uzaktaki fare imlecini göster"), ("Show quality monitor", ""), ("Disable clipboard", "Hafızadaki kopyalanmışları engelle"), diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 91d0ab169..0c085775e 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -87,6 +87,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Delete", "刪除"), ("Properties", "屬性"), ("Multi Select", "多選"), + ("Select All", "全選"), + ("Unselect All", "取消全選"), ("Empty Directory", "空文件夾"), ("Not an empty directory", "不是一個空文件夾"), ("Are you sure you want to delete this file?", "您確定要刪除此檔案嗎?"), @@ -112,7 +114,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Good image quality", "畫面品質良好"), ("Balanced", "平衡"), ("Optimize reaction time", "回應速度最佳化"), - ("Custom", "自訂"), + ("Custom", "自定義"), ("Show remote cursor", "顯示遠端游標"), ("Show quality monitor", "顯示質量監測"), ("Disable clipboard", "停用剪貼簿"), diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 528b919b8..8d3451db0 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -87,6 +87,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Delete", "Видалити"), ("Properties", "Властивості"), ("Multi Select", "Багатоелементний вибір"), + ("Select All", ""), + ("Unselect All", ""), ("Empty Directory", "Порожня папка"), ("Not an empty directory", "Папка не порожня"), ("Are you sure you want to delete this file?", "Ви впевнені, що хочете видалити цей файл?"), @@ -112,7 +114,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Good image quality", "Хороша якість зображення"), ("Balanced", "Збалансований"), ("Optimize reaction time", "Оптимізувати час реакції"), - ("Custom", "Користувацький"), + ("Custom", ""), ("Show remote cursor", "Показати віддалений курсор"), ("Show quality monitor", "Показати якість"), ("Disable clipboard", "Відключити буфер обміну"), diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 7f368126f..81428a8ec 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -87,6 +87,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Delete", "Xóa"), ("Properties", "Thuộc tính"), ("Multi Select", "Chọn nhiều"), + ("Select All", ""), + ("Unselect All", ""), ("Empty Directory", "Thư mục rỗng"), ("Not an empty directory", "Không phải thư mục rỗng"), ("Are you sure you want to delete this file?", "Bạn chắc bạn có muốn xóa tệp tin này không?"), @@ -112,7 +114,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Good image quality", "Chất lượng hình ảnh tốt"), ("Balanced", "Cân bằng"), ("Optimize reaction time", "Thời gian phản ứng tối ưu"), - ("Custom", "Custom"), + ("Custom", ""), ("Show remote cursor", "Hiển thị con trỏ từ máy từ xa"), ("Show quality monitor", "Hiện thị chất lượng của màn hình"), ("Disable clipboard", "Tắt clipboard"), From ec698e688519f248dbf585277c7bf32148c70a0b Mon Sep 17 00:00:00 2001 From: csf Date: Wed, 19 Oct 2022 11:24:44 +0900 Subject: [PATCH 0717/2015] fix didn't show drives on '/' page --- .../lib/desktop/pages/file_manager_page.dart | 32 +++++++++---------- flutter/lib/models/file_model.dart | 6 ++-- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index a225b55b0..fc7b6676e 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -286,8 +286,9 @@ class _FileManagerPageState extends State rows: filteredEntries.map((entry) { final sizeStr = entry.isFile ? readableFileSize(entry.size.toDouble()) : ""; - final lastModifiedStr = - "${entry.lastModified().toString().replaceAll(".000", "")} "; + final lastModifiedStr = entry.isDrive + ? " " + : "${entry.lastModified().toString().replaceAll(".000", "")} "; return DataRow( key: ValueKey(entry.name), onSelectChanged: (s) { @@ -810,9 +811,19 @@ class _FileManagerPageState extends State final peerPlatform = (await bind.sessionGetPlatform( id: _ffi.id, isRemote: !isLocal)) .toLowerCase(); - final List menuItems; + final List menuItems = [ + MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + '/', + style: style, + ), + proc: () { + openDirectory('/', isLocal: isLocal); + }, + dismissOnClicked: true), + MenuEntryDivider() + ]; if (peerPlatform == "windows") { - menuItems = []; var loadingTag = ""; if (!isLocal) { loadingTag = _ffi.dialogManager.showLoading("Waiting"); @@ -837,19 +848,6 @@ class _FileManagerPageState extends State _ffi.dialogManager.dismissByTag(loadingTag); } } - } else { - menuItems = [ - MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - '/', - style: style, - ), - proc: () { - openDirectory('/', isLocal: isLocal); - }, - dismissOnClicked: true), - MenuEntryDivider() - ]; } mod_menu.showMenu( diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index a95e44ddf..c7d712eb5 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -1178,11 +1178,13 @@ class SelectedItems { } } -// code from file_manager pkg after edit +// edited from [https://github.com/DevsOnFlutter/file_manager/blob/c1bf7f0225b15bcb86eba602c60acd5c4da90dd8/lib/file_manager.dart#L22] List _sortList(List list, SortBy sortType, bool ascending) { if (sortType == SortBy.Name) { // making list of only folders. - final dirs = list.where((element) => element.isDirectory).toList(); + final dirs = list + .where((element) => element.isDirectory || element.isDrive) + .toList(); // sorting folder list by name. dirs.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase())); From d10748a67beefcc4d16e80d5bb146d52995e5143 Mon Sep 17 00:00:00 2001 From: csf Date: Wed, 19 Oct 2022 11:49:32 +0900 Subject: [PATCH 0718/2015] mobile file transfer handle driver & update UI theme --- .../lib/mobile/pages/file_manager_page.dart | 141 ++++++++++-------- flutter/lib/models/file_model.dart | 1 + 2 files changed, 78 insertions(+), 64 deletions(-) diff --git a/flutter/lib/mobile/pages/file_manager_page.dart b/flutter/lib/mobile/pages/file_manager_page.dart index 0ee0e0f8d..0ff6c83da 100644 --- a/flutter/lib/mobile/pages/file_manager_page.dart +++ b/flutter/lib/mobile/pages/file_manager_page.dart @@ -69,8 +69,14 @@ class _FileManagerPageState extends State { title: ToggleSwitch( initialLabelIndex: model.isLocal ? 0 : 1, activeBgColor: [MyTheme.idColor], - // inactiveBgColor: MyTheme.grayBg, - inactiveFgColor: Colors.black54, + inactiveBgColor: + Theme.of(context).brightness == Brightness.light + ? MyTheme.grayBg + : null, + inactiveFgColor: + Theme.of(context).brightness == Brightness.light + ? Colors.black54 + : null, totalSwitches: 2, minWidth: 100, fontSize: 15, @@ -92,7 +98,8 @@ class _FileManagerPageState extends State { PopupMenuItem( child: Row( children: [ - Icon(Icons.refresh, color: Colors.black), + Icon(Icons.refresh, + color: Theme.of(context).iconTheme.color), SizedBox(width: 5), Text(translate("Refresh File")) ], @@ -102,7 +109,8 @@ class _FileManagerPageState extends State { PopupMenuItem( child: Row( children: [ - Icon(Icons.check, color: Colors.black), + Icon(Icons.check, + color: Theme.of(context).iconTheme.color), SizedBox(width: 5), Text(translate("Multi Select")) ], @@ -113,7 +121,7 @@ class _FileManagerPageState extends State { child: Row( children: [ Icon(Icons.folder_outlined, - color: Colors.black), + color: Theme.of(context).iconTheme.color), SizedBox(width: 5), Text(translate("Create Folder")) ], @@ -127,7 +135,7 @@ class _FileManagerPageState extends State { model.currentShowHidden ? Icons.check_box_outlined : Icons.check_box_outline_blank, - color: Colors.black), + color: Theme.of(context).iconTheme.color), SizedBox(width: 5), Text(translate("Show Hidden Files")) ], @@ -188,7 +196,7 @@ class _FileManagerPageState extends State { )); })); - bool needShowCheckBox() { + bool showCheckBox() { if (!model.selectMode) { return false; } @@ -220,60 +228,63 @@ class _FileManagerPageState extends State { return Card( child: ListTile( leading: Icon( - entries[index].isFile ? Icons.feed_outlined : Icons.folder, + entries[index].isFile + ? Icons.feed_outlined + : entries[index].isDrive + ? Icons.computer + : Icons.folder, size: 40), title: Text(entries[index].name), selected: selected, subtitle: Text( - entries[index] - .lastModified() - .toString() - .replaceAll(".000", "") + - " " + - sizeStr, + entries[index].isDrive + ? "" + : "${entries[index].lastModified().toString().replaceAll(".000", "")} $sizeStr", style: TextStyle(fontSize: 12, color: MyTheme.darkGray), ), - trailing: needShowCheckBox() - ? Checkbox( - value: selected, - onChanged: (v) { - if (v == null) return; - if (v && !selected) { - _selectedItems.add(isLocal, entries[index]); - } else if (!v && selected) { - _selectedItems.remove(entries[index]); - } - setState(() {}); - }) - : PopupMenuButton( - icon: Icon(Icons.more_vert), - itemBuilder: (context) { - return [ - PopupMenuItem( - child: Text(translate("Delete")), - value: "delete", - ), - PopupMenuItem( - child: Text(translate("Multi Select")), - value: "multi_select", - ), - PopupMenuItem( - child: Text(translate("Properties")), - value: "properties", - enabled: false, - ) - ]; - }, - onSelected: (v) { - if (v == "delete") { - final items = SelectedItems(); - items.add(isLocal, entries[index]); - model.removeAction(items); - } else if (v == "multi_select") { - _selectedItems.clear(); - model.toggleSelectMode(); - } - }), + trailing: entries[index].isDrive + ? null + : showCheckBox() + ? Checkbox( + value: selected, + onChanged: (v) { + if (v == null) return; + if (v && !selected) { + _selectedItems.add(isLocal, entries[index]); + } else if (!v && selected) { + _selectedItems.remove(entries[index]); + } + setState(() {}); + }) + : PopupMenuButton( + icon: Icon(Icons.more_vert), + itemBuilder: (context) { + return [ + PopupMenuItem( + child: Text(translate("Delete")), + value: "delete", + ), + PopupMenuItem( + child: Text(translate("Multi Select")), + value: "multi_select", + ), + PopupMenuItem( + child: Text(translate("Properties")), + value: "properties", + enabled: false, + ) + ]; + }, + onSelected: (v) { + if (v == "delete") { + final items = SelectedItems(); + items.add(isLocal, entries[index]); + model.removeAction(items); + } else if (v == "multi_select") { + _selectedItems.clear(); + model.toggleSelectMode(); + } + }), onTap: () { if (model.selectMode && !_selectedItems.isOtherPage(isLocal)) { if (selected) { @@ -284,21 +295,23 @@ class _FileManagerPageState extends State { setState(() {}); return; } - if (entries[index].isDirectory) { + if (entries[index].isDirectory || entries[index].isDrive) { model.openDirectory(entries[index].path); breadCrumbScrollToEnd(); } else { // Perform file-related tasks. } }, - onLongPress: () { - _selectedItems.clear(); - model.toggleSelectMode(); - if (model.selectMode) { - _selectedItems.add(isLocal, entries[index]); - } - setState(() {}); - }, + onLongPress: entries[index].isDrive + ? null + : () { + _selectedItems.clear(); + model.toggleSelectMode(); + if (model.selectMode) { + _selectedItems.add(isLocal, entries[index]); + } + setState(() {}); + }, ), ); }, diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index c7d712eb5..1ea9f65a1 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -1139,6 +1139,7 @@ class SelectedItems { bool? get isLocal => _isLocal; add(bool isLocal, Entry e) { + if (e.isDrive) return; _isLocal ??= isLocal; if (_isLocal != null && _isLocal != isLocal) { return; From e50271cbb65e836797d405a1334f6c83d3d11543 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 17 Oct 2022 11:53:15 +0800 Subject: [PATCH 0719/2015] fix: cm window block on setSize --- flutter/lib/main.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 5009e68a1..0f7c29705 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -194,6 +194,7 @@ void runPortForwardScreen(Map argument) async { } void runConnectionManagerScreen() async { + await initEnv(kAppTypeMain); await initEnv(kAppTypeMain); // initialize window WindowOptions windowOptions = From 5fff68011aef83f8f78614fa31bf50198e228ae4 Mon Sep 17 00:00:00 2001 From: kingtous Date: Tue, 18 Oct 2022 10:29:33 +0800 Subject: [PATCH 0720/2015] wip: uni links --- flutter/lib/common.dart | 49 +++++++++++++++++++ .../lib/desktop/pages/desktop_home_page.dart | 3 ++ flutter/lib/main.dart | 4 ++ flutter/macos/Runner/Info.plist | 13 +++++ flutter/pubspec.yaml | 2 + flutter/windows/runner/main.cpp | 11 +++++ 6 files changed, 82 insertions(+) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index caa143632..12222727b 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -15,6 +15,8 @@ import 'package:flutter_hbb/models/peer_model.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:get/get.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:uni_links/uni_links.dart'; +import 'package:uni_links_desktop/uni_links_desktop.dart'; import 'package:window_manager/window_manager.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:window_size/window_size.dart' as window_size; @@ -1178,6 +1180,48 @@ Future restoreWindowPosition(WindowType type, {int? windowId}) async { return false; } +/// Initialize uni links for macos/windows +/// +/// [Availability] +/// initUniLinks should only be used on macos/windows. +/// we use dbus for linux currently. +Future initUniLinks() async { + if (!Platform.isWindows && !Platform.isMacOS) { + return; + } + if (Platform.isWindows) { + registerProtocol('rustdesk'); + } + // check cold boot + try { + final initialLink = await getInitialLink(); + // TODO: parse link + print("${initialLink}"); + } on PlatformException { + // Handle exception by warning the user their action did not succeed + // return? + } +} + +StreamSubscription listenUniLinks() { + if (Platform.isWindows || Platform.isMacOS) { + final sub = uriLinkStream.listen((Uri? uri) { + if (uri != null) { + callUniLinksUriHandler(uri); + } else { + print("uni listen error: uri is empty."); + } + }, onError: (err) { + print("uni links error: $err"); + }); + return sub; + } else { + // return empty stream subscription for uniform logic + final stream = Stream.empty(); + return stream.listen((event) {/*ignore*/}); + } +} + void checkArguments() { // check connect args final connectIndex = bootArgs.indexOf("--connect"); @@ -1208,6 +1252,11 @@ void parseRustdeskUri(String uriPath) { print("uri is not valid: $uriPath"); return; } + callUniLinksUriHandler(uri); +} + +/// uri handler +void callUniLinksUriHandler(Uri uri) { // new connection if (uri.authority == "connection" && uri.path.startsWith("/new/")) { final peerId = uri.path.substring("/new/".length); diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index a31a71802..d22f740cb 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -37,6 +37,7 @@ class _DesktopHomePageState extends State @override bool get wantKeepAlive => true; var updateUrl = ''; + StreamSubscription? _uniLinksSubscription; @override void onWindowClose() async { @@ -455,12 +456,14 @@ class _DesktopHomePageState extends State Future.delayed(Duration.zero, () { checkArguments(); }); + _uniLinksSubscription = listenUniLinks(); } @override void dispose() { trayManager.removeListener(this); windowManager.removeListener(this); + _uniLinksSubscription?.cancel(); super.dispose(); } } diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 0f7c29705..0e3362a45 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:io'; import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/material.dart'; @@ -13,6 +14,7 @@ import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:uni_links_desktop/uni_links_desktop.dart'; import 'package:window_manager/window_manager.dart'; // import 'package:window_manager/window_manager.dart'; @@ -89,6 +91,8 @@ Future initEnv(String appType) async { } void runMainApp(bool startService) async { + // register uni links + initUniLinks(); await initEnv(kAppTypeMain); // trigger connection status updater await bind.mainCheckConnectStatus(); diff --git a/flutter/macos/Runner/Info.plist b/flutter/macos/Runner/Info.plist index 4789daa6a..8245f21a0 100644 --- a/flutter/macos/Runner/Info.plist +++ b/flutter/macos/Runner/Info.plist @@ -28,5 +28,18 @@ MainMenu NSPrincipalClass NSApplication + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLName + + CFBundleURLSchemes + + rustdesk + + + diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 190a6ffa4..8c1dc9815 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -95,6 +95,8 @@ dependencies: # git: # url: https://github.com/Kingtous/flutter_improved_scrolling # ref: 62f09545149f320616467c306c8c5f71714a18e6 + uni_links: ^0.5.1 + uni_links_desktop: ^0.1.3 dev_dependencies: icons_launcher: ^2.0.4 diff --git a/flutter/windows/runner/main.cpp b/flutter/windows/runner/main.cpp index 3921e03dd..66194ed42 100644 --- a/flutter/windows/runner/main.cpp +++ b/flutter/windows/runner/main.cpp @@ -7,6 +7,8 @@ #include "utils.h" // #include +#include + typedef char** (*FUNC_RUSTDESK_CORE_MAIN)(int*); typedef void (*FUNC_RUSTDESK_FREE_ARGS)( char**, int); @@ -14,6 +16,15 @@ typedef void (*FUNC_RUSTDESK_FREE_ARGS)( char**, int); int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, _In_ wchar_t *command_line, _In_ int show_command) { + // uni links dispatch + HWND hwnd = ::FindWindow(L"FLUTTER_RUNNER_WIN32_WINDOW", L"rustdesk"); + if (hwnd != NULL) { + DispatchToUniLinksDesktop(hwnd); + + ::ShowWindow(hwnd, SW_NORMAL); + ::SetForegroundWindow(hwnd); + return EXIT_FAILURE; + } HINSTANCE hInstance = LoadLibraryA("librustdesk.dll"); if (!hInstance) { From bf7597ec7c15df9c448e5a75e9320ac7fb817ed8 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Wed, 19 Oct 2022 09:54:04 +0800 Subject: [PATCH 0721/2015] feat: add window size plugin injection on windows --- flutter/lib/common.dart | 12 +++++++----- flutter/lib/main.dart | 1 - flutter/pubspec.yaml | 2 +- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 12222727b..968ae34e8 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1195,11 +1195,12 @@ Future initUniLinks() async { // check cold boot try { final initialLink = await getInitialLink(); - // TODO: parse link - print("${initialLink}"); - } on PlatformException { - // Handle exception by warning the user their action did not succeed - // return? + if (initialLink == null) { + return; + } + parseRustdeskUri(initialLink); + } catch (err) { + debugPrint("$err"); } } @@ -1257,6 +1258,7 @@ void parseRustdeskUri(String uriPath) { /// uri handler void callUniLinksUriHandler(Uri uri) { + debugPrint("uni links called: $uri"); // new connection if (uri.authority == "connection" && uri.path.startsWith("/new/")) { final peerId = uri.path.substring("/new/".length); diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 0e3362a45..6f69a9c2b 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -198,7 +198,6 @@ void runPortForwardScreen(Map argument) async { } void runConnectionManagerScreen() async { - await initEnv(kAppTypeMain); await initEnv(kAppTypeMain); // initialize window WindowOptions windowOptions = diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 8c1dc9815..da086aab1 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -64,7 +64,7 @@ dependencies: desktop_multi_window: git: url: https://github.com/Kingtous/rustdesk_desktop_multi_window - ref: f25487b8aacfcc9d22b86a84e97eda1a5c07ccaf + ref: 318ebd0a70cc5868911591c04f84bf1541f1bf4e freezed_annotation: ^2.0.3 tray_manager: git: From 62c53f0343c4bb4b94cfa805874bb2e358bdb07d Mon Sep 17 00:00:00 2001 From: csf Date: Wed, 19 Oct 2022 22:52:02 +0900 Subject: [PATCH 0722/2015] mobile file transfer disable actions on drivers --- flutter/lib/mobile/pages/file_manager_page.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/flutter/lib/mobile/pages/file_manager_page.dart b/flutter/lib/mobile/pages/file_manager_page.dart index 0ff6c83da..97d7798d7 100644 --- a/flutter/lib/mobile/pages/file_manager_page.dart +++ b/flutter/lib/mobile/pages/file_manager_page.dart @@ -46,7 +46,7 @@ class _FileManagerPageState extends State { @override Widget build(BuildContext context) => ChangeNotifierProvider.value( - value: gFFI.fileModel, + value: model, child: Consumer(builder: (_context, _model, _child) { return WillPopScope( onWillPop: () async { @@ -107,6 +107,7 @@ class _FileManagerPageState extends State { value: "refresh", ), PopupMenuItem( + enabled: model.currentDir.path != "/", child: Row( children: [ Icon(Icons.check, @@ -118,6 +119,7 @@ class _FileManagerPageState extends State { value: "select", ), PopupMenuItem( + enabled: model.currentDir.path != "/", child: Row( children: [ Icon(Icons.folder_outlined, @@ -129,6 +131,7 @@ class _FileManagerPageState extends State { value: "folder", ), PopupMenuItem( + enabled: model.currentDir.path != "/", child: Row( children: [ Icon( From 04398ef54eb32c716c7e41b8c4a6fc906cfa1b68 Mon Sep 17 00:00:00 2001 From: csf Date: Wed, 19 Oct 2022 23:29:45 +0900 Subject: [PATCH 0723/2015] file model handle path `.` and `..` , opt follow lint --- .../lib/desktop/pages/file_manager_page.dart | 14 +- flutter/lib/models/file_model.dart | 152 +++++++++--------- flutter/pubspec.yaml | 1 + 3 files changed, 87 insertions(+), 80 deletions(-) diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index fc7b6676e..bbd84170e 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -229,13 +229,13 @@ class _FileManagerPageState extends State final entries = fd.entries; final sortIndex = (SortBy style) { switch (style) { - case SortBy.Name: + case SortBy.name: return 0; - case SortBy.Type: + case SortBy.type: return 0; - case SortBy.Modified: + case SortBy.modified: return 1; - case SortBy.Size: + case SortBy.size: return 2; } }(model.getSortStyle(isLocal)); @@ -265,7 +265,7 @@ class _FileManagerPageState extends State translate("Name"), ).marginSymmetric(horizontal: 4), onSort: (columnIndex, ascending) { - model.changeSortStyle(SortBy.Name, + model.changeSortStyle(SortBy.name, isLocal: isLocal, ascending: ascending); }), DataColumn( @@ -273,13 +273,13 @@ class _FileManagerPageState extends State translate("Modified"), ), onSort: (columnIndex, ascending) { - model.changeSortStyle(SortBy.Modified, + model.changeSortStyle(SortBy.modified, isLocal: isLocal, ascending: ascending); }), DataColumn( label: Text(translate("Size")), onSort: (columnIndex, ascending) { - model.changeSortStyle(SortBy.Size, + model.changeSortStyle(SortBy.size, isLocal: isLocal, ascending: ascending); }), ], diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index 1ea9f65a1..2a14071fd 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -4,15 +4,18 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; import 'package:get/get.dart'; -import 'package:path/path.dart' as Path; +import 'package:path/path.dart' as path; import 'model.dart'; import 'platform_model.dart'; -enum SortBy { Name, Type, Modified, Size } +enum SortBy { name, type, modified, size } class FileModel extends ChangeNotifier { - var _isLocal = false; + /// mobile, current selected page show on mobile screen + var _isSelectedLocal = false; + + /// mobile, select mode state var _selectMode = false; final _localOption = DirectoryOption(); @@ -30,7 +33,7 @@ class FileModel extends ChangeNotifier { RxList get jobTable => _jobTable; - bool get isLocal => _isLocal; + bool get isLocal => _isSelectedLocal; bool get selectMode => _selectMode; @@ -38,17 +41,17 @@ class FileModel extends ChangeNotifier { JobState get jobState => _jobProgress.state; - SortBy _sortStyle = SortBy.Name; + SortBy _sortStyle = SortBy.name; SortBy get sortStyle => _sortStyle; - SortBy _localSortStyle = SortBy.Name; + SortBy _localSortStyle = SortBy.name; bool _localSortAscending = true; bool _remoteSortAscending = true; - SortBy _remoteSortStyle = SortBy.Name; + SortBy _remoteSortStyle = SortBy.name; bool get localSortAscending => _localSortAscending; @@ -64,7 +67,8 @@ class FileModel extends ChangeNotifier { FileDirectory get currentRemoteDir => _currentRemoteDir; - FileDirectory get currentDir => _isLocal ? currentLocalDir : currentRemoteDir; + FileDirectory get currentDir => + _isSelectedLocal ? currentLocalDir : currentRemoteDir; FileDirectory getCurrentDir(bool isLocal) { return isLocal ? currentLocalDir : currentRemoteDir; @@ -75,7 +79,7 @@ class FileModel extends ChangeNotifier { final currentHome = getCurrentHome(isLocal); if (currentDir.path.startsWith(currentHome)) { var path = currentDir.path.replaceFirst(currentHome, ""); - if (path.length == 0) return ""; + if (path.isEmpty) return ""; if (path[0] == "/" || path[0] == "\\") { // remove more '/' or '\' path = path.replaceFirst(path[0], ""); @@ -86,7 +90,8 @@ class FileModel extends ChangeNotifier { } } - String get currentHome => _isLocal ? _localOption.home : _remoteOption.home; + String get currentHome => + _isSelectedLocal ? _localOption.home : _remoteOption.home; String getCurrentHome(bool isLocal) { return isLocal ? _localOption.home : _remoteOption.home; @@ -99,7 +104,7 @@ class FileModel extends ChangeNotifier { String get currentShortPath { if (currentDir.path.startsWith(currentHome)) { var path = currentDir.path.replaceFirst(currentHome, ""); - if (path.length == 0) return ""; + if (path.isEmpty) return ""; if (path[0] == "/" || path[0] == "\\") { // remove more '/' or '\' path = path.replaceFirst(path[0], ""); @@ -114,7 +119,7 @@ class FileModel extends ChangeNotifier { final dir = isLocal ? currentLocalDir : currentRemoteDir; if (dir.path.startsWith(currentHome)) { var path = dir.path.replaceFirst(currentHome, ""); - if (path.length == 0) return ""; + if (path.isEmpty) return ""; if (path[0] == "/" || path[0] == "\\") { // remove more '/' or '\' path = path.replaceFirst(path[0], ""); @@ -126,14 +131,14 @@ class FileModel extends ChangeNotifier { } bool get currentShowHidden => - _isLocal ? _localOption.showHidden : _remoteOption.showHidden; + _isSelectedLocal ? _localOption.showHidden : _remoteOption.showHidden; bool getCurrentShowHidden(bool isLocal) { return isLocal ? _localOption.showHidden : _remoteOption.showHidden; } bool get currentIsWindows => - _isLocal ? _localOption.isWindows : _remoteOption.isWindows; + _isSelectedLocal ? _localOption.isWindows : _remoteOption.isWindows; bool getCurrentIsWindows(bool isLocal) { return isLocal ? _localOption.isWindows : _remoteOption.isWindows; @@ -156,12 +161,12 @@ class FileModel extends ChangeNotifier { } togglePage() { - _isLocal = !_isLocal; + _isSelectedLocal = !_isSelectedLocal; notifyListeners(); } toggleShowHidden({bool? showHidden, bool? local}) { - final isLocal = local ?? _isLocal; + final isLocal = local ?? _isSelectedLocal; if (isLocal) { _localOption.showHidden = showHidden ?? !_localOption.showHidden; } else { @@ -187,7 +192,7 @@ class FileModel extends ChangeNotifier { job.fileNum = int.parse(evt['file_num']); job.speed = double.parse(evt['speed']); job.finishedSize = int.parse(evt['finished_size']); - debugPrint("update job ${id} with ${evt}"); + debugPrint("update job $id with $evt"); } } notifyListeners(); @@ -197,7 +202,7 @@ class FileModel extends ChangeNotifier { } receiveFileDir(Map evt) { - debugPrint("recv file dir:${evt}"); + debugPrint("recv file dir:$evt"); if (evt['is_local'] == "false") { // init remote home, the connection will automatic read remote home when established, try { @@ -209,9 +214,9 @@ class FileModel extends ChangeNotifier { final job = jobTable[jobIndex]; var totalSize = 0; var fileCount = fd.entries.length; - fd.entries.forEach((element) { + for (var element in fd.entries) { totalSize += element.size; - }); + } job.totalSize = totalSize; job.fileCount = fileCount; debugPrint("update receive details:${fd.path}"); @@ -343,7 +348,7 @@ class FileModel extends ChangeNotifier { jobReset(); // save config - Map msgMap = Map(); + Map msgMap = {}; msgMap["local_dir"] = _currentLocalDir.path; msgMap["local_show_hidden"] = _localOption.showHidden ? "Y" : ""; @@ -361,7 +366,7 @@ class FileModel extends ChangeNotifier { Future refresh({bool? isLocal}) async { if (isDesktop) { - isLocal = isLocal ?? _isLocal; + isLocal = isLocal ?? _isSelectedLocal; isLocal ? await openDirectory(currentLocalDir.path, isLocal: isLocal) : await openDirectory(currentRemoteDir.path, isLocal: isLocal); @@ -371,7 +376,15 @@ class FileModel extends ChangeNotifier { } openDirectory(String path, {bool? isLocal, bool isBack = false}) async { - isLocal = isLocal ?? _isLocal; + isLocal = isLocal ?? _isSelectedLocal; + if (path == ".") { + refresh(isLocal: isLocal); + return; + } + if (path == "..") { + goToParentDirectory(isLocal: isLocal); + return; + } if (!isBack) { pushHistory(isLocal); } @@ -385,7 +398,7 @@ class FileModel extends ChangeNotifier { : _remoteOption.isWindows && path.length > 1 && path[0] == '/') { path = path.substring(1); if (path[path.length - 1] != '\\') { - path = path + "\\"; + path = "$path\\"; } } try { @@ -416,12 +429,12 @@ class FileModel extends ChangeNotifier { } goHome({bool? isLocal}) { - isLocal = isLocal ?? _isLocal; + isLocal = isLocal ?? _isSelectedLocal; openDirectory(getCurrentHome(isLocal), isLocal: isLocal); } goBack({bool? isLocal}) { - isLocal = isLocal ?? _isLocal; + isLocal = isLocal ?? _isSelectedLocal; final history = isLocal ? localHistory : remoteHistory; if (history.isEmpty) return; final path = history.removeAt(history.length - 1); @@ -435,7 +448,7 @@ class FileModel extends ChangeNotifier { } goToParentDirectory({bool? isLocal}) { - isLocal = isLocal ?? _isLocal; + isLocal = isLocal ?? _isSelectedLocal; final isWindows = isLocal ? _localOption.isWindows : _remoteOption.isWindows; final currDir = isLocal ? currentLocalDir : currentRemoteDir; @@ -457,7 +470,7 @@ class FileModel extends ChangeNotifier { isRemote ? _localOption.isWindows : _remoteOption.isWindows; final showHidden = isRemote ? _localOption.showHidden : _remoteOption.showHidden; - items.items.forEach((from) async { + for (var from in items.items) { final jobId = ++_jobId; _jobTable.add(JobProgress() ..jobName = from.path @@ -473,9 +486,9 @@ class FileModel extends ChangeNotifier { fileNum: 0, includeHidden: showHidden, isRemote: isRemote); - print( - "path:${from.path}, toPath:${toPath}, to:${PathUtil.join(toPath, from.name, isWindows)}"); - }); + debugPrint( + "path:${from.path}, toPath:$toPath, to:${PathUtil.join(toPath, from.name, isWindows)}"); + } } else { if (items.isLocal == null) { debugPrint("Failed to sendFiles ,wrong path state"); @@ -505,7 +518,7 @@ class FileModel extends ChangeNotifier { bool removeCheckboxRemember = false; removeAction(SelectedItems items, {bool? isLocal}) async { - isLocal = isLocal ?? _isLocal; + isLocal = isLocal ?? _isSelectedLocal; removeCheckboxRemember = false; if (items.isLocal == null) { debugPrint("Failed to removeFile, wrong path state"); @@ -520,7 +533,7 @@ class FileModel extends ChangeNotifier { late final List entries; if (item.isFile) { title = translate("Are you sure you want to delete this file?"); - content = "${item.name}"; + content = item.name; entries = [item]; } else if (item.isDirectory) { title = translate("Not an empty directory"); @@ -553,7 +566,7 @@ class FileModel extends ChangeNotifier { ? "${translate("Are you sure you want to delete the file of this directory?")}\n" : ""; final count = entries.length > 1 ? "${i + 1}/${entries.length}" : ""; - content = dirShow + "$count \n${entries[i].path}"; + content = "$dirShow$count \n${entries[i].path}"; final confirm = await showRemoveDialog(title, content, item.isDirectory); try { @@ -580,7 +593,7 @@ class FileModel extends ChangeNotifier { break; } } catch (e) { - print("remove error: ${e}"); + print("remove error: $e"); } } }); @@ -779,14 +792,14 @@ class FileModel extends ChangeNotifier { job.fileCount = num_entries; job.totalSize = total_size.toInt(); } - debugPrint("update folder files: ${info}"); + debugPrint("update folder files: $info"); notifyListeners(); } bool get remoteSortAscending => _remoteSortAscending; void loadLastJob(Map evt) { - debugPrint("load last job: ${evt}"); + debugPrint("load last job: $evt"); Map jobDetail = json.decode(evt['value']); // int id = int.parse(jobDetail['id']); String remote = jobDetail['remote']; @@ -824,7 +837,7 @@ class FileModel extends ChangeNotifier { id: '${parent.target?.id}', actId: job.id, isRemote: job.isRemote); job.state = JobState.inProgress; } else { - debugPrint("jobId ${jobId} is not exists"); + debugPrint("jobId $jobId is not exists"); } notifyListeners(); } @@ -833,7 +846,7 @@ class FileModel extends ChangeNotifier { class JobResultListener { Completer? _completer; Timer? _timer; - int _timeoutSecond = 5; + final int _timeoutSecond = 5; bool get isListening => _completer != null; @@ -871,20 +884,14 @@ class JobResultListener { } class FileFetcher { - // Map> localTasks = Map(); // now we only use read local dir sync - Map> remoteTasks = Map(); - Map> readRecursiveTasks = Map(); + // Map> localTasks = {}; // now we only use read local dir sync + Map> remoteTasks = {}; + Map> readRecursiveTasks = {}; - String? _id; - - String? get id => _id; - - set id(String? id) { - _id = id; - } + String? id; // if id == null, means to fetch global FFI - FFI get _ffi => ffi(_id ?? ""); + FFI get _ffi => ffi(id ?? ""); Future registerReadTask(bool isLocal, String path) { // final jobs = isLocal?localJobs:remoteJobs; // maybe we will use read local dir async later @@ -921,7 +928,7 @@ class FileFetcher { tryCompleteTask(String? msg, String? isLocalStr) { if (msg == null || isLocalStr == null) return; - late final tasks; + late final Map> tasks; try { final fd = FileDirectory.fromJson(jsonDecode(msg)); if (fd.id > 0) { @@ -988,15 +995,15 @@ class FileDirectory { id = json['id']; path = json['path']; json['entries'].forEach((v) { - entries.add(new Entry.fromJson(v)); + entries.add(Entry.fromJson(v)); }); } // generate full path for every entry , init sort style if need. format(bool isWindows, {SortBy? sort}) { - entries.forEach((entry) { + for (var entry in entries) { entry.path = PathUtil.join(path, entry.name, isWindows); - }); + } if (sort != null) { changeSortStyle(sort); } @@ -1094,8 +1101,8 @@ class _PathStat { } class PathUtil { - static final windowsContext = Path.Context(style: Path.Style.windows); - static final posixContext = Path.Context(style: Path.Style.posix); + static final windowsContext = path.Context(style: path.Style.windows); + static final posixContext = path.Context(style: path.Style.posix); static String join(String path1, String path2, bool isWindows) { final pathUtil = isWindows ? windowsContext : posixContext; @@ -1155,7 +1162,7 @@ class SelectedItems { remove(Entry e) { _items.remove(e); - if (_items.length == 0) { + if (_items.isEmpty) { _isLocal = null; } } @@ -1181,7 +1188,7 @@ class SelectedItems { // edited from [https://github.com/DevsOnFlutter/file_manager/blob/c1bf7f0225b15bcb86eba602c60acd5c4da90dd8/lib/file_manager.dart#L22] List _sortList(List list, SortBy sortType, bool ascending) { - if (sortType == SortBy.Name) { + if (sortType == SortBy.name) { // making list of only folders. final dirs = list .where((element) => element.isDirectory || element.isDrive) @@ -1198,22 +1205,22 @@ List _sortList(List list, SortBy sortType, bool ascending) { return ascending ? [...dirs, ...files] : [...dirs.reversed.toList(), ...files.reversed.toList()]; - } else if (sortType == SortBy.Modified) { + } else if (sortType == SortBy.modified) { // making the list of Path & DateTime - List<_PathStat> _pathStat = []; + List<_PathStat> pathStat = []; for (Entry e in list) { - _pathStat.add(_PathStat(e.name, e.lastModified())); + pathStat.add(_PathStat(e.name, e.lastModified())); } // sort _pathStat according to date - _pathStat.sort((b, a) => a.dateTime.compareTo(b.dateTime)); + pathStat.sort((b, a) => a.dateTime.compareTo(b.dateTime)); // sorting [list] according to [_pathStat] - list.sort((a, b) => _pathStat + list.sort((a, b) => pathStat .indexWhere((element) => element.path == a.name) - .compareTo(_pathStat.indexWhere((element) => element.path == b.name))); + .compareTo(pathStat.indexWhere((element) => element.path == b.name))); return ascending ? list : list.reversed.toList(); - } else if (sortType == SortBy.Type) { + } else if (sortType == SortBy.type) { // making list of only folders. final dirs = list.where((element) => element.isDirectory).toList(); @@ -1232,11 +1239,11 @@ List _sortList(List list, SortBy sortType, bool ascending) { return ascending ? [...dirs, ...files] : [...dirs.reversed.toList(), ...files.reversed.toList()]; - } else if (sortType == SortBy.Size) { + } else if (sortType == SortBy.size) { // create list of path and size - Map _sizeMap = {}; + Map sizeMap = {}; for (Entry e in list) { - _sizeMap[e.name] = e.size; + sizeMap[e.name] = e.size; } // making list of only folders. @@ -1248,14 +1255,13 @@ List _sortList(List list, SortBy sortType, bool ascending) { final files = list.where((element) => element.isFile).toList(); // creating sorted list of [_sizeMapList] by size. - final List> _sizeMapList = _sizeMap.entries.toList(); - _sizeMapList.sort((b, a) => a.value.compareTo(b.value)); + final List> sizeMapList = sizeMap.entries.toList(); + sizeMapList.sort((b, a) => a.value.compareTo(b.value)); // sort [list] according to [_sizeMapList] - files.sort((a, b) => _sizeMapList + files.sort((a, b) => sizeMapList .indexWhere((element) => element.key == a.name) - .compareTo( - _sizeMapList.indexWhere((element) => element.key == b.name))); + .compareTo(sizeMapList.indexWhere((element) => element.key == b.name))); return ascending ? [...dirs, ...files] : [...dirs.reversed.toList(), ...files.reversed.toList()]; diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index da086aab1..471a2ffdc 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -97,6 +97,7 @@ dependencies: # ref: 62f09545149f320616467c306c8c5f71714a18e6 uni_links: ^0.5.1 uni_links_desktop: ^0.1.3 + path: ^1.8.2 dev_dependencies: icons_launcher: ^2.0.4 From 9623123e9f212da050b572faa853aaa66a8fd67c Mon Sep 17 00:00:00 2001 From: csf Date: Wed, 19 Oct 2022 23:59:02 +0900 Subject: [PATCH 0724/2015] desktop file transfer update UI style --- .../lib/desktop/pages/file_manager_page.dart | 44 +++++++++++-------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index bbd84170e..deacff80c 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -546,13 +546,6 @@ class _FileManagerPageState extends State children: [ Row( children: [ - IconButton( - onPressed: () { - model.goHome(isLocal: isLocal); - }, - icon: const Icon(Icons.home_outlined), - splashRadius: 20, - ), IconButton( icon: const Icon(Icons.arrow_back), splashRadius: 20, @@ -649,6 +642,13 @@ class _FileManagerPageState extends State mainAxisAlignment: isLocal ? MainAxisAlignment.start : MainAxisAlignment.end, children: [ + IconButton( + onPressed: () { + model.goHome(isLocal: isLocal); + }, + icon: const Icon(Icons.home_outlined), + splashRadius: 20, + ), IconButton( onPressed: () { final name = TextEditingController(); @@ -789,8 +789,7 @@ class _FileManagerPageState extends State child: BreadCrumb( items: items, divider: Text("/", - style: TextStyle(color: Theme.of(context).hintColor)) - .paddingSymmetric(horizontal: 2.0), + style: TextStyle(color: Theme.of(context).hintColor)), overflow: ScrollableOverflow( controller: getBreadCrumbScrollController(isLocal)), )), @@ -833,15 +832,21 @@ class _FileManagerPageState extends State await model.fetchDirectory("/", isLocal, isLocal); for (var entry in fd.entries) { menuItems.add(MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - entry.name, - style: style, - ), + childBuilder: (TextStyle? style) => + Row(children: [ + Icon(Icons.computer, + color: style?.color, + size: style?.fontSize), + SizedBox(width: 10), + Text( + entry.name, + style: style, + ) + ]), proc: () { openDirectory(entry.name, isLocal: isLocal); }, dismissOnClicked: true)); - menuItems.add(MenuEntryDivider()); } } finally { if (!isLocal) { @@ -849,7 +854,7 @@ class _FileManagerPageState extends State } } } - + menuItems.add(MenuEntryDivider()); mod_menu.showMenu( context: context, position: RelativeRect.fromLTRB(x, y, x, y), @@ -879,10 +884,11 @@ class _FileManagerPageState extends State final breadCrumbList = List.empty(growable: true); breadCrumbList.addAll(list.asMap().entries.map((e) => BreadCrumbItem( content: TextButton( - child: Text(e.value), - style: - ButtonStyle(minimumSize: MaterialStateProperty.all(Size(0, 0))), - onPressed: () => onPressed(list.sublist(0, e.key + 1)))))); + child: Text(e.value), + style: ButtonStyle( + minimumSize: MaterialStateProperty.all(Size(0, 0))), + onPressed: () => onPressed(list.sublist(0, e.key + 1))) + .marginSymmetric(horizontal: 4)))); return breadCrumbList; } From bab826e9a3f160adce9df441d99167daac88d749 Mon Sep 17 00:00:00 2001 From: csf Date: Thu, 20 Oct 2022 10:31:31 +0900 Subject: [PATCH 0725/2015] [change dart SDK version to >=2.17.0] update drive icon and mobile style --- flutter/lib/common.dart | 14 ++++--- .../lib/desktop/pages/file_manager_page.dart | 41 ++++++++++++------- .../lib/mobile/pages/file_manager_page.dart | 35 +++++++++------- flutter/lib/models/file_model.dart | 13 +++++- flutter/pubspec.lock | 41 +++++++++++++++++-- flutter/pubspec.yaml | 2 +- 6 files changed, 106 insertions(+), 40 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 968ae34e8..255cb564c 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -48,18 +48,20 @@ typedef FMethod = String Function(String, dynamic); typedef StreamEventHandler = Future Function(Map); -late final iconKeyboard = MemoryImage(Uint8List.fromList(base64Decode( +final iconKeyboard = MemoryImage(Uint8List.fromList(base64Decode( "iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAgVBMVEUAAAD///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////9d3yJTAAAAKnRSTlMA0Gd/0y8ILZgbJffDPUwV2nvzt+TMqZxyU7CMb1pYQyzsvKunkXE4AwJnNC24AAAA+0lEQVQ4y83O2U7DMBCF4ZMxk9rZk26kpQs7nPd/QJy4EiLbLf01N5Y/2YP/qxDFQvGB5NPC/ZpVnfJx4b5xyGfF95rkHvNCWH1u+N6J6T0sC7gqRy8uGPfBLEbozPXUjlkQKwGaFPNizwQbwkx0TDvhCii34ExZCSQVBdzIOEOyeclSHgBGXkpeygXSQgStACtWx4Z8rr8COHOvfEP/IbbsQAToFUAAV1M408IIjIGYAPoCSNRP7DQutfQTqxuAiH7UUg1FaJR2AGrrx52sK2ye28LZ0wBAEyR6y8X+NADhm1B4fgiiHXbRrTrxpwEY9RdM9wsepnvFHfUDwYEeiwAJr/gAAAAASUVORK5CYII="))); -late final iconClipboard = MemoryImage(Uint8List.fromList(base64Decode( +final iconClipboard = MemoryImage(Uint8List.fromList(base64Decode( 'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAjVBMVEUAAAD///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8DizOFAAAALnRSTlMAnIsyZy8YZF3NSAuabRL34cq6trCScyZ4qI9CQDwV+fPl2tnTwzkeB+m/pIFK/Xx0ewAAAQlJREFUOMudktduhDAQRWep69iY3tle0+7/f16Qg7MsJUQ5Dwh8jzRzhemJPIaf3GiW7eFQfOwDPp1ek/iMnKgBi5PrhJAhZAa1lCxE9pw5KWMswOMAQXuQOvqTB7tLFJ36wimKLrufZTzUaoRtdthqRA2vEwS+tR4qguiElRKk1YMrYfUQRkwLmwVBYDMvJKF8R0o3V2MOhNrfo+hXSYYjPn1L/S+n438t8gWh+q1F+cYFBMm1Jh8Ia7y2OWXQxMMRLqr2eTc1crSD84cWfEGwYM4LlaACEee2ZjsQXJxR3qmYb+GpC8ZfNM5oh3yxxbxgQE7lEkb3ZvvH1BiRHn1bu02ICcKGWr4AudUkyYxmvywAAAAASUVORK5CYII='))); -late final iconAudio = MemoryImage(Uint8List.fromList(base64Decode( +final iconAudio = MemoryImage(Uint8List.fromList(base64Decode( 'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAk1BMVEUAAAD////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////ROyVeAAAAMHRSTlMAgfz08DDqCAThvraZjEcoGA751JxzbGdfTRP25NrIpaGTcEM+HAvMuKinhXhWNx9Yzm/gAAABFUlEQVQ4y82S2XLCMAxFheMsQNghCQFalkL39vz/11V4GpNk0r629+Va1pmxPFfyh1ravOP2Y1ydJmBO0lYP3r+PyQ62s2Y7fgF6VRXOYdToT++ogIuoVhCUtX7YpwJG3F8f6V8rr3WABwwUahlEvr8y3IBniGKdKYBQ5OGQpukQakBpIVcfwptIhJcf8hWGakdndAAhBInIGHbdQGJg6jjbDUgEE5EpmB+AAM4uj6gb+AQT6wdhITLvAHJ4VCtgoAlG1tpNA0gWON/f4ioHdSADc1bfgt+PZFkDlD6ojWF+kVoaHlhvFjPHuVRrefohY1GdcFm1N8JvwEyrJ/X2Th2rIoVgIi3Fo6Xf0z5k8psKu5f/oi+nHjjI92o36AAAAABJRU5ErkJggg=='))); -late final iconFile = MemoryImage(Uint8List.fromList(base64Decode( +final iconFile = MemoryImage(Uint8List.fromList(base64Decode( 'iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAAUVBMVEUAAAD///////////////////////////////////////////////////////////////////////////////////////////////////////8IN+deAAAAGnRSTlMAH+CAESEN8jyZkcIb5N/ONy3vmHhmiGjUm7UwS+YAAAHZSURBVGje7dnbboMwDIBhBwgQoFAO7Ta//4NOqCAXYZQstatq4r+r5ubrgQSpg8iyC4ZURa+PlIpQYGiwrzyeHtYZjAL8T05O4H8BbbKvFgRa4NoBU8pXeYEkDDgaaLQBcwJrmeErJQB/7wes3QBWGnCIX0+AQycL1PO6BMwPa0nA4ZxbgTvOjUYMGPHRnZkQAY4mxPZBjmy53E7ukSkFKYB/D4XsWZQx64sCeYebOogGsoOBYvv6/UCb8F0IOBZ0TlP6lEYdANY350AJqB9/qPVuOI5evw4A1hgLigAlepnyxW80bcCcwN++A2s82Vcu02ta+ceq9BoL5KGTTRwQPlpqA3gCnwWU2kCDgeWRQPj2jAPCDxgCMjhI6uZnToDpvd/BJeFrJQB/fsAa02gCt3mi1wNuy8GgBNDZlysBNNSrADVSjcJl6vCpUn6jOdx0kz0q6PMhQRa4465SFKhx35cgUCBTwj2/NHwZAb71qR8GEP2H1XcmAtBPTEO67GP6FUUAIKGABbDLQ0EArhN2sAIGesRO+iyy+RMAjckVTlMCKFVAbh/4Af9OPgG61SkDVco3BQGT3GXaDAnTIAcYZDuBTwGsAGDxuBFeAQqIqwoFMlAVLrHr/wId5MPt0nilGgAAAABJRU5ErkJggg=='))); -late final iconRestart = MemoryImage(Uint8List.fromList(base64Decode( +final iconRestart = MemoryImage(Uint8List.fromList(base64Decode( 'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAB7BAAAewQHDaVRTAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAbhJREFUWIXVlrFqFGEUhb+7UYxaWCQKlrKKxaZSQVGDJih2tj6MD2DnMwiWvoAIRnENIpZiYxEro6IooiS7SPwsMgNLkk3mjmYmnmb45/73nMNwz/x/qH3gMu2gH6rAU+Blw+Lngau4jpmGxVF7qp1iPWjaQKnZ2WnXbuP/NqAeUPc3ZkA9XDwvqc+BVWCgPlJ7tRwUKThZce819b46VH+pfXVRXVO/q2cSul3VOgZUl0ejq86r39TXI8mqZKDuDEwCw3IREQvAbWAGmMsQZQ0sAl3gHPB1Q+0e8BuYzRDuy2yOiFVgaUxtRf0ETGc4syk4rc6PqU0Cx9j8Zf6dAeAK8Fi9sUXtFjABvEgxJlNwRP2svlNPjbw/q35U36oTFbnyMSwabxb/gB/qA3VBHagrauV7RW0DRfP1IvMlXqkXkhz1DYyQTKtHa/Z2VVMx3IiI+PI3/bCHjuOpFrSnAMpL6QfgTcMGesDx0kBr2BMzsNyi/vtQu8CJlgwsRbZDnWP90NkKaxHxJMOXMqAeAn5u0ydwMCKGY+qbkB3C2W3EKWoXk5zVoHbUZ+6Mh7tl4G4F8RJ3qvL+AfV3r5Vdpj70AAAAAElFTkSuQmCC'))); -late final iconRecording = MemoryImage(Uint8List.fromList(base64Decode( +final iconRecording = MemoryImage(Uint8List.fromList(base64Decode( 'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAANpJREFUWEftltENAiEMhtsJ1NcynG6gI+gGugEOR591gppeQoIYSDBILxEeydH/57u2FMF4obE+TAOTwLoIhBDOAHBExG2n6rgR0akW640AM0sn4SWMiDycc7s8JjN7Ijro/k8NqAAR5RoeAPZxv2ggP9hCJiWZxtGbq3hqbJiBVHy4gVx8qAER8Yi4JFy6huVAKXemgb8icI+1b5KEitq0DOO/Nm1EEX1TK27p/bVvv36MOhl4EtHHbFF7jq8AoG1z08OAiFycczrkFNe6RrIet26NMQlMAuYEXiayryF/QQktAAAAAElFTkSuQmCC'))); +final iconHardDrive = MemoryImage(Uint8List.fromList(base64Decode( + 'iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAMAAACahl6sAAAAmVBMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAjHWqVAAAAMnRSTlMAv0BmzLJNXlhiUu2fxXDgu7WuSUUe29LJvpqUjX53VTstD7ilNujCqTEk5IYH+vEoFjKvAagAAAPpSURBVHja7d0JbhpBEIXhB3jYzb5vBgzYgO04df/DJXGUKMwU9ECmZ6pQfSfw028LCXW3YYwxxhhjjDHGGGOM0eZ9VV1MckdKWLM1bRQ/35GW/WxHHu1me6ShuyHvNl34VhlTKsYVeDWj1EzgUZ1S1DrAk/UDparZgxd9Sl0BHnxSBhpI3jfKQG2FpLUpE69I2ILikv1nsvygjBwPSNKYMlNHggqUoSKS80AZCnwHqQ1zCRvW+CRegwRFeFAMKKrtM8gTPJlzSfwFgT9dJom3IDN4VGaSeAryAK8m0SSeghTg1ZYiql6CjBDhO8mzlyAVhKhIwgXxrh5NojGIhyRckEdwpCdhgpSQgiWTRGMQNonGIGySp0SDvMDBX5KWxiB8Eo1BgE00SYJBykhNnkmSWJAcLpGaJNMgfJKyxiDAK4WNEwryhMtkJsk8CJtEYxA+icYgQIfCcgkEqcJNXhIRQdgkGoPwSTQG+e8khdu/7JOVREwQIKCwF41B2CQljUH4JLcH6SI+OUlEBQHa0SQag/BJNAbhkjxqDMIn0RgEeI4muSlID9eSkERgEKAVTaIxCJ9EYxA2ydVB8hCASVLRGAQYR5NoDMIn0RgEyFHYSGMQPonGII4kziCNvBgNJonEk4u3GAk8Sprk6eYaqbMDY0oKvUm5jfC/viGiSypV7+M3i2iDsAGpNEDYjlTa3W8RdR/r544g50ilnA0RxoZIE2NIXqQbhkAkGyKNDZHGhkhjQ6SxIdLYEGlsiDQ2JGTVeD0264U9zipPh7XOooffpA6pfNCXjxl4/c3pUzlChwzor53zwYYVfpI5pOV6LWFF/2jiJ5FDSs5jdY/0rwUAkUMeXWdBqnSqD0DikBqdqCHsjTvELm9In0IOri/0pwAEDtlSyNaRjAIAAoesKWTtuusxByBwCJp0oomwBXcYUuCQgE50ENajE4OvZAKHLB1/68Br5NqiyCGYOY8YRd77kTkEb64n7lZN+mOIX4QOwb5FX0ZVx3uOxwW+SB0CbBubemWP8/rlaaeRX+M3uUOuZENsiA25zIbYkPsZElBIHwL13U/PTjJ/cyOOEoVM3I+hziDQlELm7pPxw3eI8/7gPh1fpLA6xGnEeDDgO0UcIAzzM35HxLPIq5SXe9BLzOsj9eUaQqyXzxS1QFSfWM2cCANiHcAISJ0AnCKpUwTuIkkA3EeSInAXSQKcs1V18e24wlllUmQp9v9zXKeHi+akRAMOPVKhAqdPBZeUmnnEsO6QcJ0+4qmOSbBxFfGVRiTUqITrdKcCbyYO3/K4wX4+aQ+FfNjXhu3JfAVjjDHGGGOMMcYYY4xIPwCgfqT6TbhCLAAAAABJRU5ErkJggg=='))); enum DesktopType { main, diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index deacff80c..622a86fb7 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -304,18 +304,25 @@ class _FileManagerPageState extends State waitDuration: Duration(milliseconds: 500), message: entry.name, child: Row(children: [ - Icon( - entry.isFile - ? Icons.feed_outlined - : entry.isDrive - ? Icons.computer - : Icons.folder, - size: 20, - color: Theme.of(context) - .iconTheme - .color - ?.withOpacity(0.7), - ).marginSymmetric(horizontal: 2), + 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)) @@ -834,9 +841,13 @@ class _FileManagerPageState extends State menuItems.add(MenuEntryButton( childBuilder: (TextStyle? style) => Row(children: [ - Icon(Icons.computer, - color: style?.color, - size: style?.fontSize), + Image( + image: iconHardDrive, + fit: BoxFit.scaleDown, + color: Theme.of(context) + .iconTheme + .color + ?.withOpacity(0.7)), SizedBox(width: 10), Text( entry.name, diff --git a/flutter/lib/mobile/pages/file_manager_page.dart b/flutter/lib/mobile/pages/file_manager_page.dart index 97d7798d7..982b8ffe3 100644 --- a/flutter/lib/mobile/pages/file_manager_page.dart +++ b/flutter/lib/mobile/pages/file_manager_page.dart @@ -230,21 +230,29 @@ class _FileManagerPageState extends State { : ""; return Card( child: ListTile( - leading: Icon( - entries[index].isFile - ? Icons.feed_outlined - : entries[index].isDrive - ? Icons.computer + leading: entries[index].isDrive + ? Padding( + padding: EdgeInsets.symmetric(vertical: 8), + child: Image( + image: iconHardDrive, + fit: BoxFit.scaleDown, + color: Theme.of(context) + .iconTheme + .color + ?.withOpacity(0.7))) + : Icon( + entries[index].isFile + ? Icons.feed_outlined : Icons.folder, - size: 40), + size: 40), title: Text(entries[index].name), selected: selected, - subtitle: Text( - entries[index].isDrive - ? "" - : "${entries[index].lastModified().toString().replaceAll(".000", "")} $sizeStr", - style: TextStyle(fontSize: 12, color: MyTheme.darkGray), - ), + subtitle: entries[index].isDrive + ? null + : Text( + "${entries[index].lastModified().toString().replaceAll(".000", "")} $sizeStr", + style: TextStyle(fontSize: 12, color: MyTheme.darkGray), + ), trailing: entries[index].isDrive ? null : showCheckBox() @@ -369,8 +377,7 @@ class _FileManagerPageState extends State { itemBuilder: (context) { return SortBy.values .map((e) => PopupMenuItem( - child: - Text(translate(e.toString().split(".").last)), + child: Text(translate(e.toString())), value: e, )) .toList(); diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index 2a14071fd..ed52d03ee 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -9,7 +9,18 @@ import 'package:path/path.dart' as path; import 'model.dart'; import 'platform_model.dart'; -enum SortBy { name, type, modified, size } +enum SortBy { + name, + type, + modified, + size; + + @override + String toString() { + final str = this.name.toString(); + return "${str[0].toUpperCase()}${str.substring(1)}"; + } +} class FileModel extends ChangeNotifier { /// mobile, current selected page show on mobile screen diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 4b78bab93..4692b3019 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -243,8 +243,8 @@ packages: dependency: "direct main" description: path: "." - ref: f25487b8aacfcc9d22b86a84e97eda1a5c07ccaf - resolved-ref: f25487b8aacfcc9d22b86a84e97eda1a5c07ccaf + ref: "318ebd0a70cc5868911591c04f84bf1541f1bf4e" + resolved-ref: "318ebd0a70cc5868911591c04f84bf1541f1bf4e" url: "https://github.com/Kingtous/rustdesk_desktop_multi_window" source: git version: "0.1.0" @@ -688,7 +688,7 @@ packages: source: hosted version: "2.1.0" path: - dependency: transitive + dependency: "direct main" description: name: path url: "https://pub.dartlang.org" @@ -1046,6 +1046,34 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.3.1" + uni_links: + dependency: "direct main" + description: + name: uni_links + url: "https://pub.dartlang.org" + source: hosted + version: "0.5.1" + uni_links_desktop: + dependency: "direct main" + description: + name: uni_links_desktop + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.3" + uni_links_platform_interface: + dependency: transitive + description: + name: uni_links_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + uni_links_web: + dependency: transitive + description: + name: uni_links_web + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.0" universal_io: dependency: transitive description: @@ -1221,6 +1249,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.0.0" + win32_registry: + dependency: transitive + description: + name: win32_registry + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" window_manager: dependency: "direct main" description: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 471a2ffdc..6d2cb31b7 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -19,7 +19,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev version: 1.2.0 environment: - sdk: ">=2.16.1" + sdk: ">=2.17.0" dependencies: flutter: From bd68969daced696648c647b009340956ee721f76 Mon Sep 17 00:00:00 2001 From: csf Date: Thu, 20 Oct 2022 11:20:41 +0900 Subject: [PATCH 0726/2015] file transfer BreadCrumb handle mouse wheel --- .../lib/desktop/pages/file_manager_page.dart | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 622a86fb7..e31a0e1d9 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'dart:math'; import 'package:desktop_drop/desktop_drop.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_breadcrumb/flutter_breadcrumb.dart'; @@ -793,13 +794,23 @@ class _FileManagerPageState extends State mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( - child: BreadCrumb( - items: items, - divider: Text("/", - style: TextStyle(color: Theme.of(context).hintColor)), - overflow: ScrollableOverflow( - controller: getBreadCrumbScrollController(isLocal)), - )), + child: Listener( + // handle mouse wheel + onPointerSignal: (e) { + if (e is PointerScrollEvent) { + final sc = getBreadCrumbScrollController(isLocal); + sc.jumpTo(sc.offset + e.scrollDelta.dy / 4); + } + }, + child: BreadCrumb( + items: items, + divider: Text("/", + style: TextStyle( + color: Theme.of(context).hintColor)), + overflow: ScrollableOverflow( + controller: + getBreadCrumbScrollController(isLocal)), + ))), ActionIcon( message: "", icon: Icons.arrow_drop_down, From 32ad458b25e6f79b7d3ff63b7981a9560a967d51 Mon Sep 17 00:00:00 2001 From: 21pages Date: Wed, 19 Oct 2022 10:19:49 +0800 Subject: [PATCH 0727/2015] user fps adjust Signed-off-by: 21pages --- .../lib/desktop/widgets/remote_menubar.dart | 156 ++++++++++++++---- libs/hbb_common/protos/message.proto | 1 + src/client.rs | 28 +++- src/flutter_ffi.rs | 10 ++ src/server/connection.rs | 13 +- src/server/video_qos.rs | 43 ++++- src/ui_session_interface.rs | 12 +- 7 files changed, 218 insertions(+), 45 deletions(-) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 72cc56cad..e670e5d80 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -742,59 +742,147 @@ class _RemoteMenubarState extends State { await bind.sessionSetImageQuality(id: widget.id, value: newValue); } + double qualityInitValue = 50; + double fpsInitValue = 30; + bool qualitySet = false; + bool fpsSet = false; + setCustomValues({double? quality, double? fps}) async { + if (quality != null) { + qualitySet = true; + await bind.sessionSetCustomImageQuality( + id: widget.id, value: quality.toInt()); + } + if (fps != null) { + fpsSet = true; + await bind.sessionSetCustomFps(id: widget.id, fps: fps.toInt()); + } + if (!qualitySet) { + qualitySet = true; + await bind.sessionSetCustomImageQuality( + id: widget.id, value: qualityInitValue.toInt()); + } + if (!fpsSet) { + fpsSet = true; + await bind.sessionSetCustomFps( + id: widget.id, fps: fpsInitValue.toInt()); + } + } + if (newValue == 'custom') { - final btnCancel = msgBoxButton(translate('Close'), () { + final btnClose = msgBoxButton(translate('Close'), () async { + await setCustomValues(); widget.ffi.dialogManager.dismissAll(); }); + + // quality final quality = await bind.sessionGetCustomImageQuality(id: widget.id); - double initValue = quality != null && quality.isNotEmpty + qualityInitValue = quality != null && quality.isNotEmpty ? quality[0].toDouble() : 50.0; - const minValue = 10.0; - const maxValue = 100.0; - if (initValue < minValue) { - initValue = minValue; + const qualityMinValue = 10.0; + const qualityMaxValue = 100.0; + if (qualityInitValue < qualityMinValue) { + qualityInitValue = qualityMinValue; } - if (initValue > maxValue) { - initValue = maxValue; + if (qualityInitValue > qualityMaxValue) { + qualityInitValue = qualityMaxValue; } - final RxDouble sliderValue = RxDouble(initValue); - final rxReplay = rxdart.ReplaySubject(); - rxReplay + final RxDouble qualitySliderValue = RxDouble(qualityInitValue); + final qualityRxReplay = rxdart.ReplaySubject(); + qualityRxReplay .throttleTime(const Duration(milliseconds: 1000), trailing: true, leading: false) .listen((double v) { () async { - await bind.sessionSetCustomImageQuality( - id: widget.id, value: v.toInt()); + await setCustomValues(quality: v); }(); }); - final slider = Obx(() { - return Slider( - value: sliderValue.value, - min: minValue, - max: maxValue, - divisions: 90, - onChanged: (double value) { - sliderValue.value = value; - rxReplay.add(value); - }, - ); + final qualitySlider = Obx(() => Row( + children: [ + Slider( + value: qualitySliderValue.value, + min: qualityMinValue, + max: qualityMaxValue, + divisions: 90, + onChanged: (double value) { + qualitySliderValue.value = value; + qualityRxReplay.add(value); + }, + ), + SizedBox( + width: 90, + child: Obx(() => Text( + '${qualitySliderValue.value.round()}% Bitrate', + style: const TextStyle(fontSize: 15), + ))) + ], + )); + // fps + final fpsOption = + await bind.sessionGetOption(id: widget.id, arg: 'custom-fps'); + fpsInitValue = + fpsOption == null ? 30 : double.tryParse(fpsOption) ?? 30; + if (fpsInitValue < 10 || fpsInitValue > 120) { + fpsInitValue = 30; + } + final RxDouble fpsSliderValue = RxDouble(fpsInitValue); + final fpsRxReplay = rxdart.ReplaySubject(); + fpsRxReplay + .throttleTime(const Duration(milliseconds: 1000), + trailing: true, leading: false) + .listen((double v) { + () async { + await setCustomValues(fps: v); + }(); }); - final content = Row( - children: [ - slider, - SizedBox( - width: 90, - child: Obx(() => Text( - '${sliderValue.value.round()}% Bitrate', + bool? direct; + try { + direct = ConnectionTypeState.find(widget.id).direct.value == + ConnectionType.strDirect; + } catch (_) {} + final fpsSlider = Offstage( + offstage: + (await bind.mainIsUsingPublicServer() && direct != true) || + (await bind.versionToNumber( + v: widget.ffi.ffiModel.pi.version) < + await bind.versionToNumber(v: '1.2.0')), + child: Row( + children: [ + Obx((() => Slider( + value: fpsSliderValue.value, + min: 10, + max: 120, + divisions: 22, + onChanged: (double value) { + fpsSliderValue.value = value; + fpsRxReplay.add(value); + }, + ))), + SizedBox( + width: 90, + child: Obx(() { + final fps = fpsSliderValue.value.round(); + String text; + if (fps < 100) { + text = '$fps FPS'; + } else { + text = '$fps FPS'; + } + return Text( + text, style: const TextStyle(fontSize: 15), - ))) - ], + ); + })) + ], + ), + ); + + final content = Column( + children: [qualitySlider, fpsSlider], ); msgBoxCommon(widget.ffi.dialogManager, 'Custom Image Quality', - content, [btnCancel]); + content, [btnClose]); } }, padding: padding, diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index a48ec9d14..4bb015866 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -480,6 +480,7 @@ message OptionMessage { BoolOption disable_clipboard = 8; BoolOption enable_file_transfer = 9; VideoCodecState video_codec_state = 10; + int32 custom_fps = 11; } message TestDelay { diff --git a/src/client.rs b/src/client.rs index 6b3917790..9e2627d55 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1092,7 +1092,12 @@ impl LoginConfigHandler { n += 1; } else if q == "custom" { let config = PeerConfig::load(&self.id); - msg.custom_image_quality = config.custom_image_quality[0] << 8; + let quality = if config.custom_image_quality.is_empty() { + 50 + } else { + config.custom_image_quality[0] + }; + msg.custom_image_quality = quality << 8; n += 1; } if self.get_toggle_option("show-remote-cursor") { @@ -1253,6 +1258,27 @@ impl LoginConfigHandler { res } + /// Create a [`Message`] for saving custom fps. + /// + /// # Arguments + /// + /// * `fps` - The given fps. + pub fn set_custom_fps(&mut self, fps: i32) -> Message { + let mut misc = Misc::new(); + misc.set_option(OptionMessage { + custom_fps: fps, + ..Default::default() + }); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + let mut config = self.load_config(); + config + .options + .insert("custom-fps".to_owned(), fps.to_string()); + self.save_config(config); + msg_out + } + pub fn get_option(&self, k: &str) -> String { if let Some(v) = self.config.options.get(k) { v.clone() diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 873248cca..5fdb3122c 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -186,6 +186,12 @@ pub fn session_set_custom_image_quality(id: String, value: i32) { } } +pub fn session_set_custom_fps(id: String, fps: i32) { + if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) { + session.set_custom_fps(fps); + } +} + pub fn session_lock_screen(id: String) { if let Some(session) = SESSIONS.read().unwrap().get(&id) { session.lock_screen(); @@ -1000,6 +1006,10 @@ pub fn query_onlines(ids: Vec) { crate::rendezvous_mediator::query_online_states(ids, handle_query_onlines) } +pub fn version_to_number(v: String) -> i64 { + hbb_common::get_version_number(&v) +} + pub fn main_is_installed() -> SyncReturn { SyncReturn(is_installed()) } diff --git a/src/server/connection.rs b/src/server/connection.rs index 4f122b59d..c4dc615be 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1316,16 +1316,25 @@ impl Connection { if o.custom_image_quality > 0 { image_quality = o.custom_image_quality; } else { - image_quality = ImageQuality::Balanced.value(); + image_quality = -1; } } else { image_quality = q.value(); } + if image_quality > 0 { + video_service::VIDEO_QOS + .lock() + .unwrap() + .update_image_quality(image_quality); + } + } + if o.custom_fps > 0 { video_service::VIDEO_QOS .lock() .unwrap() - .update_image_quality(image_quality); + .update_user_fps(o.custom_fps as _); } + if let Ok(q) = o.lock_after_session_end.enum_value() { if q != BoolOption::NotSet { self.lock_after_session_end = q == BoolOption::Yes; diff --git a/src/server/video_qos.rs b/src/server/video_qos.rs index b0e06bc03..7aaf12d92 100644 --- a/src/server/video_qos.rs +++ b/src/server/video_qos.rs @@ -1,6 +1,8 @@ use super::*; use std::time::Duration; const FPS: u8 = 30; +const MIN_FPS: u8 = 10; +const MAX_FPS: u8 = 120; trait Percent { fn as_percent(&self) -> u32; } @@ -23,7 +25,8 @@ pub struct VideoQoS { current_image_quality: u32, enable_abr: bool, pub current_delay: u32, - pub fps: u8, // abr + pub fps: u8, // abr + pub user_fps: u8, pub target_bitrate: u32, // abr updated: bool, state: DelayState, @@ -56,6 +59,7 @@ impl Default for VideoQoS { fn default() -> Self { VideoQoS { fps: FPS, + user_fps: FPS, user_image_quality: ImageQuality::Balanced.as_percent(), current_image_quality: ImageQuality::Balanced.as_percent(), enable_abr: false, @@ -80,12 +84,19 @@ impl VideoQoS { } pub fn spf(&mut self) -> Duration { - if self.fps <= 0 { - self.fps = FPS; + if self.fps < MIN_FPS || self.fps > MAX_FPS { + self.fps = self.base_fps(); } Duration::from_secs_f32(1. / (self.fps as f32)) } + fn base_fps(&self) -> u8 { + if self.user_fps >= MIN_FPS && self.user_fps <= MAX_FPS { + return self.user_fps; + } + return FPS; + } + // update_network_delay periodically // decrease the bitrate when the delay gets bigger pub fn update_network_delay(&mut self, delay: u32) { @@ -124,19 +135,19 @@ impl VideoQoS { fn refresh_quality(&mut self) { match self.state { DelayState::Normal => { - self.fps = FPS; + self.fps = self.base_fps(); self.current_image_quality = self.user_image_quality; } DelayState::LowDelay => { - self.fps = FPS; + self.fps = self.base_fps(); self.current_image_quality = std::cmp::min(self.user_image_quality, 50); } DelayState::HighDelay => { - self.fps = FPS / 2; + self.fps = self.base_fps() / 2; self.current_image_quality = std::cmp::min(self.user_image_quality, 25); } DelayState::Broken => { - self.fps = FPS / 4; + self.fps = self.base_fps() / 4; self.current_image_quality = 10; } } @@ -146,6 +157,14 @@ impl VideoQoS { // handle image_quality change from peer pub fn update_image_quality(&mut self, image_quality: i32) { + if image_quality == ImageQuality::Low.value() + || image_quality == ImageQuality::Balanced.value() + || image_quality == ImageQuality::Best.value() + { + // not custom + self.user_fps = FPS; + self.fps = FPS; + } let image_quality = Self::convert_quality(image_quality) as _; if self.current_image_quality != image_quality { self.current_image_quality = image_quality; @@ -156,6 +175,16 @@ impl VideoQoS { self.user_image_quality = self.current_image_quality; } + pub fn update_user_fps(&mut self, fps: u8) { + if fps >= MIN_FPS && fps <= MAX_FPS { + if self.user_fps != fps { + self.user_fps = fps; + self.fps = fps; + self.updated = true; + } + } + } + pub fn generate_bitrate(&mut self) -> ResultType { // https://www.nvidia.com/en-us/geforce/guides/broadcasting-guide/ if self.width == 0 || self.height == 0 { diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index cf550ed63..f95a743c2 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -134,6 +134,11 @@ impl Session { } } + pub fn set_custom_fps(&mut self, custom_fps: i32) { + let msg = self.lc.write().unwrap().set_custom_fps(custom_fps); + self.send(Data::Message(msg)); + } + pub fn get_remember(&self) -> bool { self.lc.read().unwrap().remember } @@ -1181,7 +1186,12 @@ impl Interface for Session { if self.is_file_transfer() { self.close_success(); } else if !self.is_port_forward() { - self.msgbox("success", "Successful", "Connected, waiting for image...", ""); + self.msgbox( + "success", + "Successful", + "Connected, waiting for image...", + "", + ); } #[cfg(windows)] { From 4a2307de2f0b31d4f4bfa48d2d0ac34ead7f22f3 Mon Sep 17 00:00:00 2001 From: 21pages Date: Wed, 19 Oct 2022 20:53:05 +0800 Subject: [PATCH 0728/2015] fix status bar height && status only update after mouse hover Signed-off-by: 21pages --- .../lib/desktop/pages/connection_page.dart | 151 +++++++++--------- 1 file changed, 72 insertions(+), 79 deletions(-) diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 318843699..f2b5cd9f4 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -55,6 +55,9 @@ class _ConnectionPageState extends State _updateTimer = Timer.periodic(Duration(seconds: 1), (timer) { updateStatus(); }); + _idFocusNode.addListener(() { + _idInputFocused.value = _idFocusNode.hasFocus; + }); } @override @@ -107,9 +110,8 @@ class _ConnectionPageState extends State ).paddingOnly(left: 12.0), ), ), - const Divider(), - SizedBox(child: Obx(() => buildStatus())) - .paddingOnly(bottom: 12, top: 6), + const Divider(height: 1), + buildStatus() ], ); } @@ -124,9 +126,6 @@ class _ConnectionPageState extends State /// UI for the remote ID TextField. /// Search for a peer and connect to it if the id exists. Widget _buildRemoteIDTextField(BuildContext context) { - _idFocusNode.addListener(() { - _idInputFocused.value = _idFocusNode.hasFocus; - }); var w = Container( width: 320 + 20 * 2, padding: const EdgeInsets.fromLTRB(20, 24, 20, 22), @@ -235,79 +234,73 @@ class _ConnectionPageState extends State var svcIsUsingPublicServer = true.obs; Widget buildStatus() { - final fontSize = 14.0; - final textStyle = TextStyle(fontSize: fontSize); - final light = Container( - height: 8, - width: 8, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - color: svcStopped.value || svcStatusCode.value == 0 - ? kColorWarn - : (svcStatusCode.value == 1 - ? Color.fromARGB(255, 50, 190, 166) - : Color.fromARGB(255, 224, 79, 95)), - ), - ).paddingSymmetric(horizontal: 12.0); - if (svcStopped.value) { - return Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - light, - Text(translate("Service is not running"), style: textStyle), - TextButton( - onPressed: () async { - bool checked = await bind.mainCheckSuperUserPermission(); - if (checked) { - bind.mainSetOption(key: "stop-service", value: ""); - bind.mainSetOption(key: "access-mode", value: ""); - } - }, - child: Text(translate("Start Service"), style: textStyle)) - ], - ); - } else { - if (svcStatusCode.value == 0) { - return Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - light, - Text(translate("connecting_status"), style: textStyle) - ], - ); - } else if (svcStatusCode.value == -1) { - return Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - light, - Text(translate("not_ready_status"), style: textStyle) - ], - ); - } - } - return Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - light, - Text(translate('Ready'), style: textStyle), - Offstage( - offstage: !svcIsUsingPublicServer.value, - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text(', ', style: textStyle), - InkWell( - onTap: onUsePublicServerGuide, - child: Text( - translate('setup_server_tip'), - style: TextStyle( - decoration: TextDecoration.underline, - fontSize: fontSize), - ), - ) - ], - )) - ], + final em = 14.0; + return ConstrainedBox( + constraints: BoxConstraints.tightFor(height: 3 * em), + child: Obx(() => Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + height: 8, + width: 8, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: svcStopped.value || svcStatusCode.value == 0 + ? kColorWarn + : (svcStatusCode.value == 1 + ? Color.fromARGB(255, 50, 190, 166) + : Color.fromARGB(255, 224, 79, 95)), + ), + ).marginSymmetric(horizontal: em), + Text( + svcStopped.value + ? translate("Service is not running") + : svcStatusCode.value == 0 + ? translate("connecting_status") + : svcStatusCode.value == -1 + ? translate("not_ready_status") + : translate('Ready'), + style: TextStyle(fontSize: em)), + // stop + Offstage( + offstage: !svcStopped.value, + child: GestureDetector( + onTap: () async { + bool checked = + await bind.mainCheckSuperUserPermission(); + if (checked) { + bind.mainSetOption(key: "stop-service", value: ""); + bind.mainSetOption(key: "access-mode", value: ""); + } + }, + child: Text(translate("Start Service"), + style: TextStyle( + decoration: TextDecoration.underline, + fontSize: em))) + .marginOnly(left: em), + ), + // ready && public + Offstage( + offstage: !(!svcStopped.value && + svcStatusCode.value == 1 && + svcIsUsingPublicServer.value), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(', ', style: TextStyle(fontSize: em)), + InkWell( + onTap: onUsePublicServerGuide, + child: Text( + translate('setup_server_tip'), + style: TextStyle( + decoration: TextDecoration.underline, fontSize: em), + ), + ) + ], + ), + ) + ], + )), ); } From c2287214f884115642df8d685f6b2866ce4a6c65 Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 20 Oct 2022 09:21:02 +0800 Subject: [PATCH 0729/2015] sync setting page service status Signed-off-by: 21pages --- build.py | 2 +- .../lib/desktop/pages/connection_page.dart | 24 ++++++++++--------- .../desktop/pages/desktop_setting_page.dart | 15 ++++++------ 3 files changed, 21 insertions(+), 20 deletions(-) diff --git a/build.py b/build.py index 0cf62daf1..a81a9a999 100755 --- a/build.py +++ b/build.py @@ -71,7 +71,7 @@ def make_parser(): parser.add_argument( '--hwcodec', action='store_true', - help='Enable feature hwcodec' + help='Enable feature hwcodec' + ('' if windows or osx else ', need libva-dev, libvdpau-dev.') ) parser.add_argument( '--portable', diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index f2b5cd9f4..8964191f8 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -39,6 +39,10 @@ class _ConnectionPageState extends State final RxBool _idInputFocused = false.obs; final FocusNode _idFocusNode = FocusNode(); + var svcStopped = false.obs; + var svcStatusCode = 0.obs; + var svcIsUsingPublicServer = true.obs; + @override void initState() { super.initState(); @@ -58,6 +62,15 @@ class _ConnectionPageState extends State _idFocusNode.addListener(() { _idInputFocused.value = _idFocusNode.hasFocus; }); + Get.put(svcStopped, tag: 'service-stop'); + } + + @override + void dispose() { + _idController.dispose(); + _updateTimer?.cancel(); + Get.delete(tag: 'service-stop'); + super.dispose(); } @override @@ -222,17 +235,6 @@ class _ConnectionPageState extends State constraints: const BoxConstraints(maxWidth: 600), child: w)); } - @override - void dispose() { - _idController.dispose(); - _updateTimer?.cancel(); - super.dispose(); - } - - var svcStopped = false.obs; - var svcStatusCode = 0.obs; - var svcIsUsingPublicServer = true.obs; - Widget buildStatus() { final em = 14.0; return ConstrainedBox( diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 9787048fa..23d832580 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -432,6 +432,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { bool get wantKeepAlive => true; bool locked = bind.mainIsInstalled(); final scrollController = ScrollController(); + final RxBool serviceStop = Get.find(tag: 'service-stop'); @override Widget build(BuildContext context) { @@ -465,17 +466,15 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { } Widget permissions(context) { - bool enabled = !locked; + return Obx(() => _permissions(context, serviceStop.value)); + } + Widget _permissions(context, bool stopService) { + bool enabled = !locked; return _futureBuilder(future: () async { - bool stopService = option2bool( - 'stop-service', await bind.mainGetOption(key: 'stop-service')); - final accessMode = await bind.mainGetOption(key: 'access-mode'); - return {'stopService': stopService, 'accessMode': accessMode}; + return await bind.mainGetOption(key: 'access-mode'); }(), hasData: (data) { - var map = data! as Map; - bool stopService = map['stopService'] as bool; - String accessMode = map['accessMode'] as String; + String accessMode = data! as String; _AccessMode mode; if (stopService) { mode = _AccessMode.deny; From 7bd0843bcd964e8e5e1f73deb3c9edd03b91733c Mon Sep 17 00:00:00 2001 From: Kingtous Date: Thu, 20 Oct 2022 17:16:15 +0800 Subject: [PATCH 0730/2015] feat: add flutter nightly ci --- .github/workflows/ci.yml | 2 + .github/workflows/flutter-ci.yml | 2 + .github/workflows/flutter-nightly.yml | 151 ++++++++++++++++++++++++++ build.py | 4 +- 4 files changed, 156 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/flutter-nightly.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 235b14be7..2e1702a60 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,6 +12,8 @@ on: - master tags: - '*' + paths-ignore: + - ".github/**" jobs: # ensure_cargo_fmt: diff --git a/.github/workflows/flutter-ci.yml b/.github/workflows/flutter-ci.yml index ac7e9b27d..8b58db83f 100644 --- a/.github/workflows/flutter-ci.yml +++ b/.github/workflows/flutter-ci.yml @@ -8,6 +8,8 @@ on: - master tags: - '*' + paths-ignore: + - ".github/**" jobs: build: diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml new file mode 100644 index 000000000..79ae62e3d --- /dev/null +++ b/.github/workflows/flutter-nightly.yml @@ -0,0 +1,151 @@ +name: Flutter Nightly Build + +on: + schedule: + - cron: "* 0 * * *" + workflow_dispatch: + +jobs: + build: + name: ${{ matrix.job.target }} (${{ matrix.job.os }}) + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: false + matrix: + job: + # - { target: aarch64-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } + # - { target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true } + # - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true } + # - { target: i686-pc-windows-msvc , os: windows-2019 } + # - { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } + # - { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } + # - { target: x86_64-apple-darwin , os: macos-10.15 } + # - { target: x86_64-pc-windows-gnu , os: windows-2019 } + - { target: x86_64-pc-windows-msvc , os: windows-2019 } + - { target: x86_64-unknown-linux-gnu , os: ubuntu-20.04 } + # - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } + steps: + - name: Checkout source code + uses: actions/checkout@v3 + + - name: Install prerequisites + shell: bash + if: startsWith(matrix.job.os, 'ubuntu') + run: | + case ${{ matrix.job.target }} in + x86_64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libayatana-appindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev;; + # arm-unknown-linux-*) sudo apt-get -y update ; sudo apt-get -y install gcc-arm-linux-gnueabihf ;; + # aarch64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt-get -y install gcc-aarch64-linux-gnu ;; + esac + + - name: Install LLVM and Clang + if: startsWith(matrix.job.os, 'windows') + uses: KyleMayes/install-llvm-action@v1 + with: + version: "13.0" + + - name: Install flutter + uses: subosito/flutter-action@v2 + with: + channel: 'stable' + flutter-version: '3.0.5' + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: ${{ matrix.job.target }} + override: true + profile: minimal # minimal component installation (ie, no documentation) + + - uses: Swatinem/rust-cache@v1 + + - name: Install flutter rust bridge deps for linux + if: startsWith(matrix.job.os, 'ubuntu') + run: | + dart pub global activate ffigen --version 5.0.1 + # flutter_rust_bridge + if [[ ! -e $HOME/.cargo/bin/flutter_rust_bridge_codegen ]]; then  ✔ + pushd /tmp && git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1 && popd + pushd /tmp/flutter_rust_bridge/frb_codegen && cargo install --path . && popd + fi + pushd flutter && flutter pub get && popd + ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart + + - name: Install flutter rust bridge deps for windows + if: startsWith(matrix.job.os, 'windows') + run: | + dart pub global activate ffigen --version 5.0.1 + $exists = Test-Path ~/.cargo/bin/flutter_rust_bridge_codegen.exe + If ( ! $exists -eq $True ) + { + Push-Location .. + git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1 + Push-Location flutter_rust_bridge/frb_codegen ; cargo install --path . ; Pop-Location + Pop-Location + } + Push-Location flutter ; flutter pub get ; Pop-Location + ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart + + - name: Restore from cache and install vcpkg + uses: lukka/run-vcpkg@v7 + with: + setupOnly: true + vcpkgGitCommitId: '6ca56aeb457f033d344a7106cb3f9f1abf8f4e98' + + - name: Install vcpkg dependencies for linux + if: startsWith(matrix.job.os, 'ubuntu') + run: | + $VCPKG_ROOT/vcpkg install libvpx libyuv opus + shell: bash + + - name: Install vcpkg dependencies for windows + if: startsWith(matrix.job.os, 'windows') + run: | + $VCPKG_ROOT/vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static + shell: bash + + - name: Install cargo bundle tools + if: startsWith(matrix.job.os, 'ubuntu') + run: | + cargo install cargo-bundle + + - name: Show version information (Rust, cargo, GCC) + shell: bash + run: | + gcc --version || true + rustup -V + rustup toolchain list + rustup default + cargo -V + rustc -V + + - name: Build rustdesk for linux + if: startsWith(matrix.job.os, 'ubuntu') + run: ./build.py --flutter + + - name: Build rustdesk for windows + if: startsWith(matrix.job.os, 'windows') + run: python3 .\build.py --portable + + - name: Update nightly release for linux + if: startsWith(matrix.job.os, 'ubuntu') + uses: "marvinpinto/action-automatic-releases@latest" + with: + repo_token: "${{ secrets.GITHUB_TOKEN }}" + automatic_release_tag: "nightly" + prerelease: true + title: "Nightly Build" + files: | + rustdesk-*.deb + + - name: Update nightly release for windows + if: startsWith(matrix.job.os, 'windows') + uses: "marvinpinto/action-automatic-releases@latest" + with: + repo_token: "${{ secrets.GITHUB_TOKEN }}" + automatic_release_tag: "nightly" + prerelease: true + title: "Nightly Build" + files: | + rustdesk-*.exe diff --git a/build.py b/build.py index a81a9a999..e7deb52ba 100755 --- a/build.py +++ b/build.py @@ -225,9 +225,7 @@ def main(): version = get_version() features = ",".join(get_features(args)) flutter = args.flutter - if not flutter: - # not flutter, is sciter - os.system('python3 res/inline-sciter.py') + os.system('python3 res/inline-sciter.py') portable = args.portable if windows: if flutter: From afa94d59072257d2195b3bde0a8a0f64f68148ea Mon Sep 17 00:00:00 2001 From: csf Date: Thu, 20 Oct 2022 23:05:34 +0900 Subject: [PATCH 0731/2015] add test mode, update cm_test --- flutter/lib/common.dart | 3 ++ flutter/lib/models/server_model.dart | 2 + flutter/test/cm_test.dart | 57 +++++++++++++++++++++++----- 3 files changed, 52 insertions(+), 10 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 255cb564c..8544fc240 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -43,6 +43,9 @@ var version = ""; int androidVersion = 0; DesktopType? desktopType; +/// * debug or test only, DO NOT enable in release build +bool isTest = false; + typedef F = String Function(String); typedef FMethod = String Function(String, dynamic); diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index 87ac9c8ea..c37af81f5 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -92,6 +92,7 @@ class ServerModel with ChangeNotifier { _serverId = IDTextEditingController(text: _emptyIdShow); Timer.periodic(Duration(seconds: 1), (timer) async { + if (isTest) return timer.cancel(); var status = await bind.mainGetOnlineStatue(); if (status > 0) { status = 1; @@ -343,6 +344,7 @@ class ServerModel with ChangeNotifier { // force updateClientState([String? json]) async { + if (isTest) return; var res = await bind.cmGetClientsState(); try { final List clientsJson = jsonDecode(res); diff --git a/flutter/test/cm_test.dart b/flutter/test/cm_test.dart index 704124781..c709d618a 100644 --- a/flutter/test/cm_test.dart +++ b/flutter/test/cm_test.dart @@ -2,26 +2,63 @@ import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/desktop/pages/server_page.dart'; +import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; import 'package:flutter_hbb/main.dart'; import 'package:flutter_hbb/models/server_model.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:get/get.dart'; import 'package:window_manager/window_manager.dart'; +final testClients = [ + Client(0, false, false, "UserAAAAAA", "123123123", true, false, false), + Client(1, false, false, "UserBBBBB", "221123123", true, false, false), + Client(2, false, false, "UserC", "331123123", true, false, false), + Client(3, false, false, "UserDDDDDDDDDDDd", "441123123", true, false, false) +]; + /// -t lib/cm_main.dart to test cm void main(List args) async { + isTest = true; WidgetsFlutterBinding.ensureInitialized(); await windowManager.ensureInitialized(); await windowManager.setSize(const Size(400, 600)); await windowManager.setAlignment(Alignment.topRight); await initEnv(kAppTypeMain); - gFFI.serverModel.clients - .add(Client(0, false, false, "UserA", "123123123", true, false, false)); - gFFI.serverModel.clients - .add(Client(1, false, false, "UserB", "221123123", true, false, false)); - gFFI.serverModel.clients - .add(Client(2, false, false, "UserC", "331123123", true, false, false)); - gFFI.serverModel.clients - .add(Client(3, false, false, "UserD", "441123123", true, false, false)); - runApp(const GetMaterialApp( - debugShowCheckedModeBanner: false, home: DesktopServerPage())); + for (var client in testClients) { + gFFI.serverModel.clients.add(client); + gFFI.serverModel.tabController.add( + TabInfo( + key: client.id.toString(), + label: client.name, + closable: false, + page: buildConnectionCard(client)), + authorized: client.authorized); + } + + runApp(GetMaterialApp( + debugShowCheckedModeBanner: false, + theme: MyTheme.lightTheme, + darkTheme: MyTheme.darkTheme, + themeMode: MyTheme.currentThemeMode(), + localizationsDelegates: const [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: supportedLocales, + home: const DesktopServerPage())); + WindowOptions windowOptions = + getHiddenTitleBarWindowOptions(size: kConnectionManagerWindowSize); + windowManager.waitUntilReadyToShow(windowOptions, () async { + await windowManager.show(); + // ensure initial window size to be changed + await windowManager.setSize(kConnectionManagerWindowSize); + await Future.wait([ + windowManager.setAlignment(Alignment.topRight), + windowManager.focus(), + windowManager.setOpacity(1) + ]); + // ensure + windowManager.setAlignment(Alignment.topRight); + }); } From 94c8b117ef9d67d742024a8f0476b709a8d4cfd0 Mon Sep 17 00:00:00 2001 From: csf Date: Thu, 20 Oct 2022 23:10:26 +0900 Subject: [PATCH 0732/2015] opt: DesktopTab tabs handle mouse wheel, add maxLabelWidth constraint, update cm --- flutter/lib/desktop/pages/server_page.dart | 1 + .../lib/desktop/widgets/tabbar_widget.dart | 149 +++++++++++------- 2 files changed, 94 insertions(+), 56 deletions(-) diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index e69557981..570c3e68e 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -124,6 +124,7 @@ class ConnectionManagerState extends State { showMinimize: true, showClose: true, controller: serverModel.tabController, + maxLabelWidth: 100, pageViewBuilder: (pageView) => Row(children: [ Expanded(child: pageView), Consumer( diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 1d774143c..cf866c481 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -3,12 +3,14 @@ import 'dart:async'; import 'dart:math'; import 'package:desktop_multi_window/desktop_multi_window.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart' hide TabBarTheme; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/main.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:get/get.dart'; +import 'package:get/get_rx/src/rx_workers/utils/debouncer.dart'; import 'package:scroll_pos/scroll_pos.dart'; import 'package:window_manager/window_manager.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -132,7 +134,8 @@ class DesktopTabController { if (val.scrollController.hasClients && val.scrollController.canScroll && val.scrollController.itemCount > index) { - val.scrollController.scrollToItem(index, center: true, animate: true); + val.scrollController + .scrollToItem(index, center: false, animate: true); } })); }); @@ -188,10 +191,15 @@ class DesktopTab extends StatelessWidget { final Future Function()? onWindowCloseButton; final TabBuilder? tabBuilder; final LabelGetter? labelGetter; + final double? maxLabelWidth; final DesktopTabController controller; Rx get state => controller.state; final isMaximized = false.obs; + final _scrollDebounce = Debouncer(delay: Duration(milliseconds: 50)); + + /// [_lastClickTime], help to handle double click + int _lastClickTime = DateTime.now().millisecondsSinceEpoch; late final DesktopTabType tabType; late final bool isMainWindow; @@ -211,6 +219,7 @@ class DesktopTab extends StatelessWidget { this.onWindowCloseButton, this.tabBuilder, this.labelGetter, + this.maxLabelWidth, }) : super(key: key) { tabType = controller.tabType; isMainWindow = @@ -292,46 +301,72 @@ class DesktopTab extends StatelessWidget { Widget _buildBar() { return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row( - children: [ - Offstage( - offstage: !Platform.isMacOS, - child: const SizedBox( - width: 78, - )), - GestureDetector( - onDoubleTap: showMaximize - ? () => toggleMaximize(isMainWindow) - .then((value) => isMaximized.value = value) + Expanded( + child: GestureDetector( + // custom double tap handler + onTap: showMaximize + ? () { + final current = DateTime.now().millisecondsSinceEpoch; + final elapsed = current - _lastClickTime; + _lastClickTime = current; + if (elapsed < kDesktopDoubleClickTimeMilli) { + // onDoubleTap + toggleMaximize(isMainWindow) + .then((value) => isMaximized.value = value); + } + } : null, onPanStart: (_) => startDragging(isMainWindow), - child: Row(children: [ - Offstage( - offstage: !showLogo, - child: SvgPicture.asset( - 'assets/logo.svg', - width: 16, - height: 16, - )), - Offstage( - offstage: !showTitle, - child: const Text( - "RustDesk", - style: TextStyle(fontSize: 13), - ).marginOnly(left: 2)) - ]).marginOnly( - left: 5, - right: 10, - )), - _ListView( - controller: controller, - onTabClose: onTabClose, - tabBuilder: tabBuilder, - labelGetter: labelGetter, - ), - ], - ), + child: Row( + children: [ + Offstage( + offstage: !Platform.isMacOS, + child: const SizedBox( + width: 78, + )), + Row(children: [ + Offstage( + offstage: !showLogo, + child: SvgPicture.asset( + 'assets/logo.svg', + width: 16, + height: 16, + )), + Offstage( + offstage: !showTitle, + child: const Text( + "RustDesk", + style: TextStyle(fontSize: 13), + ).marginOnly(left: 2)) + ]).marginOnly( + left: 5, + right: 10, + ), + Expanded( + child: Listener( + // handle mouse wheel + onPointerSignal: (e) { + if (e is PointerScrollEvent) { + final sc = + controller.state.value.scrollController; + if (!sc.canScroll) return; + _scrollDebounce.call(() { + sc.animateTo(sc.offset + e.scrollDelta.dy, + duration: Duration(milliseconds: 200), + curve: Curves.ease); + }); + } + }, + child: _ListView( + controller: controller, + onTabClose: onTabClose, + tabBuilder: tabBuilder, + labelGetter: labelGetter, + maxLabelWidth: maxLabelWidth))), + ], + ))), WindowActionPanel( isMainWindow: isMainWindow, tabType: tabType, @@ -435,14 +470,9 @@ class WindowActionPanelState extends State @override Widget build(BuildContext context) { - return Expanded( - child: Row( + return Row( + mainAxisAlignment: MainAxisAlignment.end, children: [ - Expanded( - child: GestureDetector( - onDoubleTap: widget.showMaximize ? _toggleMaximize : null, - onPanStart: (_) => startDragging(widget.isMainWindow), - )), Offstage(offstage: widget.tail == null, child: widget.tail), Offstage( offstage: !widget.showMinimize, @@ -489,7 +519,7 @@ class WindowActionPanelState extends State isClose: true, )), ], - )); + ); } void _toggleMaximize() { @@ -580,13 +610,13 @@ Future closeConfirmDialog() async { return res == true; } -// ignore: must_be_immutable class _ListView extends StatelessWidget { final DesktopTabController controller; final Function(String key)? onTabClose; final TabBuilder? tabBuilder; final LabelGetter? labelGetter; + final double? maxLabelWidth; Rx get state => controller.state; @@ -594,7 +624,8 @@ class _ListView extends StatelessWidget { {required this.controller, required this.onTabClose, this.tabBuilder, - this.labelGetter}); + this.labelGetter, + this.maxLabelWidth}); /// Check whether to show ListView /// @@ -645,6 +676,7 @@ class _ListView extends StatelessWidget { themeConf, ); }, + maxLabelWidth: maxLabelWidth, ); }).toList())); } @@ -661,6 +693,7 @@ class _Tab extends StatefulWidget { final Function() onSelected; final Widget Function(Widget icon, Widget label, TabThemeConf themeConf)? tabBuilder; + final double? maxLabelWidth; const _Tab({ Key? key, @@ -673,6 +706,7 @@ class _Tab extends StatefulWidget { required this.selected, required this.onClose, required this.onSelected, + this.maxLabelWidth, }) : super(key: key); @override @@ -697,14 +731,17 @@ class _TabState extends State<_Tab> with RestorationMixin { : MyTheme.tabbar(context).unSelectedTabIconColor, ).paddingOnly(right: 5)); final labelWidget = Obx(() { - return Text( - translate(widget.label.value), - textAlign: TextAlign.center, - style: TextStyle( - color: isSelected - ? MyTheme.tabbar(context).selectedTextColor - : MyTheme.tabbar(context).unSelectedTextColor), - ); + return ConstrainedBox( + constraints: BoxConstraints(maxWidth: widget.maxLabelWidth ?? 200), + child: Text( + translate(widget.label.value), + textAlign: TextAlign.center, + style: TextStyle( + color: isSelected + ? MyTheme.tabbar(context).selectedTextColor + : MyTheme.tabbar(context).unSelectedTextColor), + overflow: TextOverflow.ellipsis, + )); }); if (widget.tabBuilder == null) { From 6e6a359809d1f3cf702b8750f71b7e983329543a Mon Sep 17 00:00:00 2001 From: csf Date: Thu, 20 Oct 2022 23:22:02 +0900 Subject: [PATCH 0733/2015] cm add multi clients scroll controller arrow actions --- flutter/lib/desktop/pages/server_page.dart | 16 ++++++++++++++++ flutter/lib/desktop/widgets/tabbar_widget.dart | 6 +++--- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index 570c3e68e..277839a21 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -125,6 +125,7 @@ class ConnectionManagerState extends State { showClose: true, controller: serverModel.tabController, maxLabelWidth: 100, + tail: buildScrollJumper(), pageViewBuilder: (pageView) => Row(children: [ Expanded(child: pageView), Consumer( @@ -159,6 +160,21 @@ class ConnectionManagerState extends State { ), ); } + + Widget buildScrollJumper() { + final offstage = gFFI.serverModel.clients.length < 2; + final sc = gFFI.serverModel.tabController.state.value.scrollController; + return Offstage( + offstage: offstage, + child: Row( + children: [ + ActionIcon( + icon: Icons.arrow_left, iconSize: 22, onTap: sc.backward), + ActionIcon( + icon: Icons.arrow_right, iconSize: 22, onTap: sc.forward), + ], + )); + } } Widget buildConnectionCard(Client client) { diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index cf866c481..50ae8cf26 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -844,7 +844,7 @@ class _CloseButton extends StatelessWidget { } class ActionIcon extends StatelessWidget { - final String message; + final String? message; final IconData icon; final Function() onTap; final bool isClose; @@ -852,7 +852,7 @@ class ActionIcon extends StatelessWidget { final double boxSize; const ActionIcon( {Key? key, - required this.message, + this.message, required this.icon, required this.onTap, this.isClose = false, @@ -864,7 +864,7 @@ class ActionIcon extends StatelessWidget { Widget build(BuildContext context) { RxBool hover = false.obs; return Obx(() => Tooltip( - message: translate(message), + message: message != null ? translate(message!) : "", waitDuration: const Duration(seconds: 1), child: InkWell( hoverColor: isClose From 13e4435089b4d05fd12a01354de70fe82aac01e4 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Thu, 20 Oct 2022 21:11:23 +0800 Subject: [PATCH 0734/2015] opt: split per item --- .github/workflows/flutter-nightly.yml | 150 +++++++++++++++----------- 1 file changed, 85 insertions(+), 65 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 79ae62e3d..6a6fae7bc 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -6,38 +6,20 @@ on: workflow_dispatch: jobs: - build: + build-for-windows: name: ${{ matrix.job.target }} (${{ matrix.job.os }}) runs-on: ${{ matrix.job.os }} strategy: fail-fast: false matrix: job: - # - { target: aarch64-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } - # - { target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true } - # - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true } # - { target: i686-pc-windows-msvc , os: windows-2019 } - # - { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } - # - { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } - # - { target: x86_64-apple-darwin , os: macos-10.15 } # - { target: x86_64-pc-windows-gnu , os: windows-2019 } - - { target: x86_64-pc-windows-msvc , os: windows-2019 } - - { target: x86_64-unknown-linux-gnu , os: ubuntu-20.04 } - # - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } + - { target: x86_64-pc-windows-msvc , os: windows-2019 } steps: - name: Checkout source code uses: actions/checkout@v3 - - name: Install prerequisites - shell: bash - if: startsWith(matrix.job.os, 'ubuntu') - run: | - case ${{ matrix.job.target }} in - x86_64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libayatana-appindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev;; - # arm-unknown-linux-*) sudo apt-get -y update ; sudo apt-get -y install gcc-arm-linux-gnueabihf ;; - # aarch64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt-get -y install gcc-aarch64-linux-gnu ;; - esac - - name: Install LLVM and Clang if: startsWith(matrix.job.os, 'windows') uses: KyleMayes/install-llvm-action@v1 @@ -60,20 +42,7 @@ jobs: - uses: Swatinem/rust-cache@v1 - - name: Install flutter rust bridge deps for linux - if: startsWith(matrix.job.os, 'ubuntu') - run: | - dart pub global activate ffigen --version 5.0.1 - # flutter_rust_bridge - if [[ ! -e $HOME/.cargo/bin/flutter_rust_bridge_codegen ]]; then  ✔ - pushd /tmp && git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1 && popd - pushd /tmp/flutter_rust_bridge/frb_codegen && cargo install --path . && popd - fi - pushd flutter && flutter pub get && popd - ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart - - - name: Install flutter rust bridge deps for windows - if: startsWith(matrix.job.os, 'windows') + - name: Install flutter rust bridge deps run: | dart pub global activate ffigen --version 5.0.1 $exists = Test-Path ~/.cargo/bin/flutter_rust_bridge_codegen.exe @@ -93,20 +62,89 @@ jobs: setupOnly: true vcpkgGitCommitId: '6ca56aeb457f033d344a7106cb3f9f1abf8f4e98' - - name: Install vcpkg dependencies for linux - if: startsWith(matrix.job.os, 'ubuntu') - run: | - $VCPKG_ROOT/vcpkg install libvpx libyuv opus - shell: bash - - - name: Install vcpkg dependencies for windows - if: startsWith(matrix.job.os, 'windows') + - name: Install vcpkg dependencies run: | $VCPKG_ROOT/vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static shell: bash + - name: Build rustdesk + run: python3 .\build.py --portable + + - name: Publish Release + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: "nightly" + files: | + rustdesk-*.exe + + build-for-linux: + name: ${{ matrix.job.target }} (${{ matrix.job.os }}) + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: false + matrix: + job: + # - { target: aarch64-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } + # - { target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true } + # - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true } + # - { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } + # - { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } + # - { target: x86_64-apple-darwin , os: macos-10.15 } + - { target: x86_64-unknown-linux-gnu , os: ubuntu-20.04 } + # - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } + steps: + - name: Checkout source code + uses: actions/checkout@v3 + + - name: Install prerequisites + run: | + case ${{ matrix.job.target }} in + x86_64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libayatana-appindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev;; + # arm-unknown-linux-*) sudo apt-get -y update ; sudo apt-get -y install gcc-arm-linux-gnueabihf ;; + # aarch64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt-get -y install gcc-aarch64-linux-gnu ;; + esac + + - name: Install flutter + uses: subosito/flutter-action@v2 + with: + channel: 'stable' + flutter-version: '3.0.5' + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: ${{ matrix.job.target }} + override: true + profile: minimal # minimal component installation (ie, no documentation) + + - uses: Swatinem/rust-cache@v1 + + - name: Install flutter rust bridge deps + shell: bash + run: | + dart pub global activate ffigen --version 5.0.1 + # flutter_rust_bridge + if [[ ! -e $HOME/.cargo/bin/flutter_rust_bridge_codegen ]]; then + pushd /tmp && git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1 && popd + pushd /tmp/flutter_rust_bridge/frb_codegen && cargo install --path . && popd + fi + pushd flutter && flutter pub get && popd + ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart + + - name: Restore from cache and install vcpkg + uses: lukka/run-vcpkg@v7 + with: + setupOnly: true + vcpkgGitCommitId: '6ca56aeb457f033d344a7106cb3f9f1abf8f4e98' + + - name: Install vcpkg dependencies + run: | + $VCPKG_ROOT/vcpkg install libvpx libyuv opus + shell: bash + - name: Install cargo bundle tools - if: startsWith(matrix.job.os, 'ubuntu') run: | cargo install cargo-bundle @@ -120,32 +158,14 @@ jobs: cargo -V rustc -V - - name: Build rustdesk for linux - if: startsWith(matrix.job.os, 'ubuntu') + - name: Build rustdesk run: ./build.py --flutter - - name: Build rustdesk for windows - if: startsWith(matrix.job.os, 'windows') - run: python3 .\build.py --portable - - - name: Update nightly release for linux - if: startsWith(matrix.job.os, 'ubuntu') - uses: "marvinpinto/action-automatic-releases@latest" + - name: Publish Release + uses: softprops/action-gh-release@v1 with: - repo_token: "${{ secrets.GITHUB_TOKEN }}" - automatic_release_tag: "nightly" prerelease: true - title: "Nightly Build" + tag_name: "nightly" files: | rustdesk-*.deb - - name: Update nightly release for windows - if: startsWith(matrix.job.os, 'windows') - uses: "marvinpinto/action-automatic-releases@latest" - with: - repo_token: "${{ secrets.GITHUB_TOKEN }}" - automatic_release_tag: "nightly" - prerelease: true - title: "Nightly Build" - files: | - rustdesk-*.exe From ee744d7de3d82f6d7fbb4f9b73038c5642902475 Mon Sep 17 00:00:00 2001 From: csf Date: Thu, 20 Oct 2022 23:56:23 +0900 Subject: [PATCH 0735/2015] cm tabs add tooltips and selected color --- flutter/lib/desktop/pages/server_page.dart | 14 +++ .../lib/desktop/widgets/tabbar_widget.dart | 93 ++++++++++++------- 2 files changed, 71 insertions(+), 36 deletions(-) diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index 277839a21..d721cda4d 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -126,6 +126,20 @@ class ConnectionManagerState extends State { controller: serverModel.tabController, maxLabelWidth: 100, tail: buildScrollJumper(), + selectedTabBackgroundColor: + Theme.of(context).hintColor.withOpacity(0.2), + tabBuilder: (key, icon, label, themeConf) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + icon, + Tooltip( + message: key, + waitDuration: Duration(seconds: 1), + child: label), + ], + ); + }, pageViewBuilder: (pageView) => Row(children: [ Expanded(child: pageView), Consumer( diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 50ae8cf26..03a8b6f01 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -178,6 +178,9 @@ typedef TabBuilder = Widget Function( String key, Widget icon, Widget label, TabThemeConf themeConf); typedef LabelGetter = Rx Function(String key); +/// [_lastClickTime], help to handle double click +int _lastClickTime = DateTime.now().millisecondsSinceEpoch; + class DesktopTab extends StatelessWidget { final Function(String)? onTabClose; final bool showTabBar; @@ -192,15 +195,14 @@ class DesktopTab extends StatelessWidget { final TabBuilder? tabBuilder; final LabelGetter? labelGetter; final double? maxLabelWidth; + final Color? selectedTabBackgroundColor; + final Color? unSelectedTabBackgroundColor; final DesktopTabController controller; Rx get state => controller.state; final isMaximized = false.obs; final _scrollDebounce = Debouncer(delay: Duration(milliseconds: 50)); - /// [_lastClickTime], help to handle double click - int _lastClickTime = DateTime.now().millisecondsSinceEpoch; - late final DesktopTabType tabType; late final bool isMainWindow; @@ -220,6 +222,8 @@ class DesktopTab extends StatelessWidget { this.tabBuilder, this.labelGetter, this.maxLabelWidth, + this.selectedTabBackgroundColor, + this.unSelectedTabBackgroundColor, }) : super(key: key) { tabType = controller.tabType; isMainWindow = @@ -364,7 +368,11 @@ class DesktopTab extends StatelessWidget { onTabClose: onTabClose, tabBuilder: tabBuilder, labelGetter: labelGetter, - maxLabelWidth: maxLabelWidth))), + maxLabelWidth: maxLabelWidth, + selectedTabBackgroundColor: + selectedTabBackgroundColor, + unSelectedTabBackgroundColor: + unSelectedTabBackgroundColor))), ], ))), WindowActionPanel( @@ -617,6 +625,8 @@ class _ListView extends StatelessWidget { final TabBuilder? tabBuilder; final LabelGetter? labelGetter; final double? maxLabelWidth; + final Color? selectedTabBackgroundColor; + final Color? unSelectedTabBackgroundColor; Rx get state => controller.state; @@ -625,7 +635,9 @@ class _ListView extends StatelessWidget { required this.onTabClose, this.tabBuilder, this.labelGetter, - this.maxLabelWidth}); + this.maxLabelWidth, + this.selectedTabBackgroundColor, + this.unSelectedTabBackgroundColor}); /// Check whether to show ListView /// @@ -667,7 +679,7 @@ class _ListView extends StatelessWidget { onSelected: () => controller.jumpTo(index), tabBuilder: tabBuilder == null ? null - : (Widget icon, Widget labelWidget, + : (String key, Widget icon, Widget labelWidget, TabThemeConf themeConf) { return tabBuilder!( tab.label, @@ -677,6 +689,8 @@ class _ListView extends StatelessWidget { ); }, maxLabelWidth: maxLabelWidth, + selectedTabBackgroundColor: selectedTabBackgroundColor, + unSelectedTabBackgroundColor: unSelectedTabBackgroundColor, ); }).toList())); } @@ -691,9 +705,10 @@ class _Tab extends StatefulWidget { final int selected; final Function() onClose; final Function() onSelected; - final Widget Function(Widget icon, Widget label, TabThemeConf themeConf)? - tabBuilder; + final TabBuilder? tabBuilder; final double? maxLabelWidth; + final Color? selectedTabBackgroundColor; + final Color? unSelectedTabBackgroundColor; const _Tab({ Key? key, @@ -707,6 +722,8 @@ class _Tab extends StatefulWidget { required this.onClose, required this.onSelected, this.maxLabelWidth, + this.selectedTabBackgroundColor, + this.unSelectedTabBackgroundColor, }) : super(key: key); @override @@ -753,8 +770,8 @@ class _TabState extends State<_Tab> with RestorationMixin { ], ); } else { - return widget.tabBuilder!( - icon, labelWidget, TabThemeConf(iconSize: _kIconSize)); + return widget.tabBuilder!(widget.label.value, icon, labelWidget, + TabThemeConf(iconSize: _kIconSize)); } } @@ -771,32 +788,36 @@ class _TabState extends State<_Tab> with RestorationMixin { restoreHover.value = value; }, onTap: () => widget.onSelected(), - child: Row( - children: [ - SizedBox( - height: _kTabBarHeight, - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - _buildTabContent(), - Obx((() => _CloseButton( - visiable: hover.value && widget.closable, - tabSelected: isSelected, - onClose: () => widget.onClose(), - ))) - ])).paddingSymmetric(horizontal: 10), - Offstage( - offstage: !showDivider, - child: VerticalDivider( - width: 1, - indent: _kDividerIndent, - endIndent: _kDividerIndent, - color: MyTheme.tabbar(context).dividerColor, - thickness: 1, - ), - ) - ], - ), + child: Container( + color: isSelected + ? widget.selectedTabBackgroundColor + : widget.unSelectedTabBackgroundColor, + child: Row( + children: [ + SizedBox( + height: _kTabBarHeight, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + _buildTabContent(), + Obx((() => _CloseButton( + visiable: hover.value && widget.closable, + tabSelected: isSelected, + onClose: () => widget.onClose(), + ))) + ])).paddingSymmetric(horizontal: 10), + Offstage( + offstage: !showDivider, + child: VerticalDivider( + width: 1, + indent: _kDividerIndent, + endIndent: _kDividerIndent, + color: MyTheme.tabbar(context).dividerColor, + thickness: 1, + ), + ) + ], + )), ), ); } From 2c2ab097a351e13963d8e47f938fef9849d36f27 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 21 Oct 2022 00:45:28 +0800 Subject: [PATCH 0736/2015] refactor: ci change to ubuntu 18.04 --- .github/workflows/flutter-nightly.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 6a6fae7bc..5e677c4db 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -91,7 +91,7 @@ jobs: # - { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } # - { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } # - { target: x86_64-apple-darwin , os: macos-10.15 } - - { target: x86_64-unknown-linux-gnu , os: ubuntu-20.04 } + - { target: x86_64-unknown-linux-gnu , os: ubuntu-18.04 } # - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } steps: - name: Checkout source code From 1e86f96827c12fe2e7d4df1512be050b5049ccc7 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 21 Oct 2022 08:39:37 +0800 Subject: [PATCH 0737/2015] refactor: remove flutter_rust_bridge compilation speedup workaround chagne crontab to 0:00 --- .github/workflows/flutter-nightly.yml | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 5e677c4db..692bfcfb7 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -2,7 +2,8 @@ name: Flutter Nightly Build on: schedule: - - cron: "* 0 * * *" + # schedule build every night + - cron: "0 0 * * *" workflow_dispatch: jobs: @@ -46,13 +47,10 @@ jobs: run: | dart pub global activate ffigen --version 5.0.1 $exists = Test-Path ~/.cargo/bin/flutter_rust_bridge_codegen.exe - If ( ! $exists -eq $True ) - { - Push-Location .. - git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1 - Push-Location flutter_rust_bridge/frb_codegen ; cargo install --path . ; Pop-Location - Pop-Location - } + Push-Location .. + git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1 + Push-Location flutter_rust_bridge/frb_codegen ; cargo install --path . ; Pop-Location + Pop-Location Push-Location flutter ; flutter pub get ; Pop-Location ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart @@ -126,10 +124,8 @@ jobs: run: | dart pub global activate ffigen --version 5.0.1 # flutter_rust_bridge - if [[ ! -e $HOME/.cargo/bin/flutter_rust_bridge_codegen ]]; then - pushd /tmp && git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1 && popd - pushd /tmp/flutter_rust_bridge/frb_codegen && cargo install --path . && popd - fi + pushd /tmp && git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1 && popd + pushd /tmp/flutter_rust_bridge/frb_codegen && cargo install --path . && popd pushd flutter && flutter pub get && popd ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart From 3818a0ddd05f6a1449058ce8aa9b9bb99f7c7dab Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 21 Oct 2022 08:41:57 +0800 Subject: [PATCH 0738/2015] feat: add hwcodec --- .github/workflows/flutter-nightly.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 692bfcfb7..f9a7e4fb2 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -66,7 +66,7 @@ jobs: shell: bash - name: Build rustdesk - run: python3 .\build.py --portable + run: python3 .\build.py --portable --hwcodec - name: Publish Release uses: softprops/action-gh-release@v1 @@ -98,7 +98,7 @@ jobs: - name: Install prerequisites run: | case ${{ matrix.job.target }} in - x86_64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libayatana-appindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev;; + x86_64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libayatana-appindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libvdpau-dev libva-dev;; # arm-unknown-linux-*) sudo apt-get -y update ; sudo apt-get -y install gcc-arm-linux-gnueabihf ;; # aarch64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt-get -y install gcc-aarch64-linux-gnu ;; esac @@ -155,7 +155,7 @@ jobs: rustc -V - name: Build rustdesk - run: ./build.py --flutter + run: ./build.py --flutter --hwcodec - name: Publish Release uses: softprops/action-gh-release@v1 From 0115774ee4fc041dc7658081de37f10d58a31c9c Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 21 Oct 2022 13:38:44 +0800 Subject: [PATCH 0739/2015] ci windows add feature PrivacyMode Signed-off-by: fufesou --- build.py | 56 ++++++++++++++++++++++++++++++++++---------------------- 1 file changed, 34 insertions(+), 22 deletions(-) diff --git a/build.py b/build.py index e7deb52ba..c024775e3 100755 --- a/build.py +++ b/build.py @@ -25,23 +25,23 @@ def get_version(): def parse_rc_features(feature): available_features = { 'IddDriver': { - 'zip_url': 'https://github.com/fufesou/RustDeskIddDriver/releases/download/v0.1/RustDeskIddDriver_x64.zip', - 'checksum_url': 'https://github.com/fufesou/RustDeskIddDriver/releases/download/v0.1' - '/RustDeskIddDriver_x64.zip.checksum_md5', + 'zip_url': 'https://github.com/fufesou/RustDeskIddDriver/releases/download/v0.1/RustDeskIddDriver_x64_pic_en.zip', + 'checksum_url': 'https://github.com/fufesou/RustDeskTempTopMostWindow/releases/download/v0.1/checksum_md5', }, 'PrivacyMode': { 'zip_url': 'https://github.com/fufesou/RustDeskTempTopMostWindow/releases/download/v0.1' - '/TempTopMostWindow_x64.zip', - 'checksum_url': 'https://github.com/fufesou/RustDeskTempTopMostWindow/releases/download/v0.1' - '/TempTopMostWindow_x64.zip.checksum_md5', + '/TempTopMostWindow_x64_pic_en.zip', + 'checksum_url': 'https://github.com/fufesou/RustDeskTempTopMostWindow/releases/download/v0.1/checksum_md5', } } apply_features = {} if not feature: - return apply_features - elif isinstance(feature, str) and feature.upper() == 'ALL': + feature = [] + if isinstance(feature, str) and feature.upper() == 'ALL': return available_features elif isinstance(feature, list): + # force add PrivacyMode + feature.append('PrivacyMode') for feat in feature: if isinstance(feat, str) and feat.upper() == 'ALL': return available_features @@ -82,23 +82,35 @@ def make_parser(): def download_extract_features(features, res_dir): + proxy = '' + def req(url): + if not proxy: + return url + else: + r = urllib.request.Request(url) + r.set_proxy(proxy, 'http') + r.set_proxy(proxy, 'https') + return r + for (feat, feat_info) in features.items(): print(f'{feat} download begin') - checksum_md5_response = urllib.request.urlopen(feat_info['checksum_url']) - checksum_md5 = checksum_md5_response.read().decode('utf-8').split()[0] download_filename = feat_info['zip_url'].split('/')[-1] - filename, _headers = urllib.request.urlretrieve(feat_info['zip_url'], download_filename) - md5 = hashlib.md5(open(filename, 'rb').read()).hexdigest() - if checksum_md5 != md5: - raise Exception(f'{feat} download failed') - print(f'{feat} download end. extract bein') - zip_file = zipfile.ZipFile(filename) - zip_list = zip_file.namelist() - for f in zip_list: - zip_file.extract(f, res_dir) - zip_file.close() - os.remove(download_filename) - print(f'{feat} extract end') + checksum_md5_response = urllib.request.urlopen(req(feat_info['checksum_url'])) + for line in checksum_md5_response.read().decode('utf-8').splitlines(): + if line.split()[1] == download_filename: + checksum_md5 = line.split()[0] + filename, _headers = urllib.request.urlretrieve(feat_info['zip_url'], download_filename) + md5 = hashlib.md5(open(filename, 'rb').read()).hexdigest() + if checksum_md5 != md5: + raise Exception(f'{feat} download failed') + print(f'{feat} download end. extract bein') + zip_file = zipfile.ZipFile(filename) + zip_list = zip_file.namelist() + for f in zip_list: + zip_file.extract(f, res_dir) + zip_file.close() + os.remove(download_filename) + print(f'{feat} extract end') def get_rc_features(args): From f0f3a2027cb9c96398f7641dd0b20dc6fb1559d2 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 21 Oct 2022 08:51:20 +0800 Subject: [PATCH 0740/2015] opt: ci opt: use force to prevent reuse newer bundle tools --- .github/workflows/flutter-nightly.yml | 51 +++++++++++++++++++-------- build.py | 2 +- flutter/pubspec.yaml | 2 +- 3 files changed, 39 insertions(+), 16 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index f9a7e4fb2..5374ec72d 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -6,6 +6,12 @@ on: - cron: "0 0 * * *" workflow_dispatch: +env: + LLVM_VERSION: "10.0" + FLUTTER_VERSION: "3.0.5" + TAG_NAME: "nightly" + VCPKG_COMMIT_ID: '6ca56aeb457f033d344a7106cb3f9f1abf8f4e98' + jobs: build-for-windows: name: ${{ matrix.job.target }} (${{ matrix.job.os }}) @@ -22,16 +28,15 @@ jobs: uses: actions/checkout@v3 - name: Install LLVM and Clang - if: startsWith(matrix.job.os, 'windows') uses: KyleMayes/install-llvm-action@v1 with: - version: "13.0" + version: ${{ env.LLVM_VERSION }} - name: Install flutter uses: subosito/flutter-action@v2 with: channel: 'stable' - flutter-version: '3.0.5' + flutter-version: ${{ env.FLUTTER_VERSION }} - name: Install Rust toolchain uses: actions-rs/toolchain@v1 @@ -41,7 +46,9 @@ jobs: override: true profile: minimal # minimal component installation (ie, no documentation) - - uses: Swatinem/rust-cache@v1 + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: ${{ matrix.job.os }} - name: Install flutter rust bridge deps run: | @@ -58,7 +65,7 @@ jobs: uses: lukka/run-vcpkg@v7 with: setupOnly: true - vcpkgGitCommitId: '6ca56aeb457f033d344a7106cb3f9f1abf8f4e98' + vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} - name: Install vcpkg dependencies run: | @@ -66,13 +73,20 @@ jobs: shell: bash - name: Build rustdesk - run: python3 .\build.py --portable --hwcodec + run: python3 .\build.py --portable --hwcodec --flutter + + - name: Rename rustdesk + shell: bash + run: | + for name in rustdesk*??.exe; do + mv "$name" "${name%%.exe}-${{ matrix.job.target }}.exe" + done - name: Publish Release uses: softprops/action-gh-release@v1 with: prerelease: true - tag_name: "nightly" + tag_name: ${{ env.TAG_NAME }} files: | rustdesk-*.exe @@ -98,7 +112,7 @@ jobs: - name: Install prerequisites run: | case ${{ matrix.job.target }} in - x86_64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libayatana-appindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libvdpau-dev libva-dev;; + x86_64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libayatana-appindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libvdpau-dev libva-dev libclang-dev llvm-dev libclang-10-dev llvm-10-dev;; # arm-unknown-linux-*) sudo apt-get -y update ; sudo apt-get -y install gcc-arm-linux-gnueabihf ;; # aarch64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt-get -y install gcc-aarch64-linux-gnu ;; esac @@ -107,7 +121,7 @@ jobs: uses: subosito/flutter-action@v2 with: channel: 'stable' - flutter-version: '3.0.5' + flutter-version: ${{ env.FLUTTER_VERSION }} - name: Install Rust toolchain uses: actions-rs/toolchain@v1 @@ -117,7 +131,9 @@ jobs: override: true profile: minimal # minimal component installation (ie, no documentation) - - uses: Swatinem/rust-cache@v1 + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: ${{ matrix.job.os }} - name: Install flutter rust bridge deps shell: bash @@ -133,7 +149,7 @@ jobs: uses: lukka/run-vcpkg@v7 with: setupOnly: true - vcpkgGitCommitId: '6ca56aeb457f033d344a7106cb3f9f1abf8f4e98' + vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} - name: Install vcpkg dependencies run: | @@ -142,7 +158,7 @@ jobs: - name: Install cargo bundle tools run: | - cargo install cargo-bundle + cargo install cargo-bundle --force - name: Show version information (Rust, cargo, GCC) shell: bash @@ -157,11 +173,18 @@ jobs: - name: Build rustdesk run: ./build.py --flutter --hwcodec + - name: Rename rustdesk + shell: bash + run: | + for name in rustdesk*??.deb; do + mv "$name" "${name%%.deb}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb" + done + - name: Publish Release uses: softprops/action-gh-release@v1 with: prerelease: true - tag_name: "nightly" + tag_name: ${{ env.TAG_NAME }} files: | - rustdesk-*.deb + rustdesk*.deb diff --git a/build.py b/build.py index e7deb52ba..403154d85 100755 --- a/build.py +++ b/build.py @@ -204,7 +204,7 @@ def build_flutter_windows(version): else: os.rename("./target/release/rustdesk-portable-packer.exe", "./rustdesk_portable.exe") print(f"output location: {os.path.abspath(os.curdir)}/rustdesk_portable.exe") - os.system(f"cp -rf ./rustdesk_portable.exe ./rustdesk-{version}-install.exe") + os.rename("./rustdesk_portable.exe", f"./rustdesk-{version}-install.exe") print(f"output location: {os.path.abspath(os.curdir)}/rustdesk-{version}-install.exe") def main(): diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 6d2cb31b7..f77fe9894 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -97,7 +97,7 @@ dependencies: # ref: 62f09545149f320616467c306c8c5f71714a18e6 uni_links: ^0.5.1 uni_links_desktop: ^0.1.3 - path: ^1.8.2 + path: ^1.8.1 dev_dependencies: icons_launcher: ^2.0.4 From 79e86df91eadb9ea8faf3181c70551dc8a2b3abd Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 21 Oct 2022 17:24:07 +0800 Subject: [PATCH 0741/2015] build.py no with_rc when build flutter Signed-off-by: fufesou --- build.py | 66 ++++++++++++++++++++++++++++++++++---------------------- 1 file changed, 40 insertions(+), 26 deletions(-) diff --git a/build.py b/build.py index 54e63b31b..7a0dc54fb 100755 --- a/build.py +++ b/build.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import os +import pathlib import platform import zipfile import urllib.request @@ -12,7 +13,7 @@ windows = platform.platform().startswith('Windows') osx = platform.platform().startswith('Darwin') or platform.platform().startswith("macOS") hbb_name = 'rustdesk' + ('.exe' if windows else '') exe_path = 'target/release/' + hbb_name - +flutter_win_target_dir = 'flutter/build/windows/runner/Release/' def get_version(): with open("Cargo.toml") as fh: @@ -114,17 +115,30 @@ def download_extract_features(features, res_dir): def get_rc_features(args): + flutter = args.flutter features = parse_rc_features(args.feature) - if features: - print(f'Build with features {list(features.keys())}') - res_dir = 'resources' - if os.path.isdir(res_dir) and not os.path.islink(res_dir): - shutil.rmtree(res_dir) - elif os.path.exists(res_dir): - raise Exception(f'Find file {res_dir}, not a directory') - os.makedirs(res_dir, exist_ok=True) - download_extract_features(features, res_dir) - return ['with_rc'] if features else [] + if not features: + return [] + + print(f'Build with features {list(features.keys())}') + res_dir = 'resources' + if os.path.isdir(res_dir) and not os.path.islink(res_dir): + shutil.rmtree(res_dir) + elif os.path.exists(res_dir): + raise Exception(f'Find file {res_dir}, not a directory') + os.makedirs(res_dir, exist_ok=True) + download_extract_features(features, res_dir) + if flutter: + os.makedirs(flutter_win_target_dir, exist_ok=True) + for f in pathlib.Path(res_dir).iterdir(): + print(f'{f}') + if f.is_file(): + shutil.copy2(f, flutter_win_target_dir) + else: + shutil.copytree(f, f'{flutter_win_target_dir}{f.stem}') + return [] + else: + return ['with_rc'] def get_features(args): @@ -203,28 +217,28 @@ def build_flutter_arch_manjaro(): os.system('HBB=`pwd` FLUTTER=1 makepkg -f') def build_flutter_windows(version): - os.system("cargo build --lib --features flutter --release") + os.system('cargo build --lib --features flutter --release') os.chdir('flutter') - os.system("flutter build windows --release") + os.system('flutter build windows --release') os.chdir('..') - os.chdir("libs/portable") - os.system("pip3 install -r requirements.txt") - os.system("python3 .\\generate.py -f ..\\..\\flutter\\build\\windows\\runner\Release\ -o . -e ..\\..\\flutter\\build\\windows\\runner\\Release\\rustdesk.exe") - os.chdir("../..") - if os.path.exists("./rustdesk_portable.exe"): - os.replace("./target/release/rustdesk-portable-packer.exe", "./rustdesk_portable.exe") + os.chdir('libs/portable') + os.system('pip3 install -r requirements.txt') + os.system(f'python3 ./generate.py -f ../../{flutter_win_target_dir} -o . -e ../../{flutter_win_target_dir}/rustdesk.exe') + os.chdir('../..') + if os.path.exists('./rustdesk_portable.exe'): + os.replace('./target/release/rustdesk-portable-packer.exe', './rustdesk_portable.exe') else: - os.rename("./target/release/rustdesk-portable-packer.exe", "./rustdesk_portable.exe") - print(f"output location: {os.path.abspath(os.curdir)}/rustdesk_portable.exe") - os.rename("./rustdesk_portable.exe", f"./rustdesk-{version}-install.exe") - print(f"output location: {os.path.abspath(os.curdir)}/rustdesk-{version}-install.exe") + os.rename('./target/release/rustdesk-portable-packer.exe', './rustdesk_portable.exe') + print(f'output location: {os.path.abspath(os.curdir)}/rustdesk_portable.exe') + os.rename('./rustdesk_portable.exe', f'./rustdesk-{version}-install.exe') + print(f'output location: {os.path.abspath(os.curdir)}/rustdesk-{version}-install.exe') def main(): parser = make_parser() args = parser.parse_args() - os.system("cp Cargo.toml Cargo.toml.bk") - os.system("cp src/main.rs src/main.rs.bk") + shutil.copy2('Cargo.toml', 'Cargo.toml.bk') + shutil.copy2('src/main.rs', 'src/main.rs.bk') if windows: txt = open('src/main.rs', encoding='utf8').read() with open('src/main.rs', 'wt', encoding='utf8') as fh: @@ -235,7 +249,7 @@ def main(): if os.path.isfile('/usr/bin/pacman'): os.system('git checkout src/ui/common.tis') version = get_version() - features = ",".join(get_features(args)) + features = ','.join(get_features(args)) flutter = args.flutter os.system('python3 res/inline-sciter.py') portable = args.portable From 2504ef7af7bf2234866ad50abf961223c9173543 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sat, 22 Oct 2022 12:41:37 +0800 Subject: [PATCH 0742/2015] feat: add nightly arch package --- .github/workflows/flutter-nightly.yml | 49 +++++++++++- build.py | 105 ++++++++++++++++++++------ res/PKGBUILD | 3 +- 3 files changed, 127 insertions(+), 30 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 5374ec72d..706e0b247 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -78,8 +78,8 @@ jobs: - name: Rename rustdesk shell: bash run: | - for name in rustdesk*??.exe; do - mv "$name" "${name%%.exe}-${{ matrix.job.target }}.exe" + for name in rustdesk*??-install.exe; do + mv "$name" "${name%%-install.exe}-${{ matrix.job.target }}.exe" done - name: Publish Release @@ -103,7 +103,7 @@ jobs: # - { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } # - { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } # - { target: x86_64-apple-darwin , os: macos-10.15 } - - { target: x86_64-unknown-linux-gnu , os: ubuntu-18.04 } + - { target: x86_64-unknown-linux-gnu , os: ubuntu-18.04} # - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } steps: - name: Checkout source code @@ -180,7 +180,7 @@ jobs: mv "$name" "${name%%.deb}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb" done - - name: Publish Release + - name: Publish debian package uses: softprops/action-gh-release@v1 with: prerelease: true @@ -188,3 +188,44 @@ jobs: files: | rustdesk*.deb + - name: Build archlinux package + uses: vufa/arch-makepkg-action@master + with: + packages: > + llvm + clang + libva + libvdpau + rust + gstreamer + unzip + git + cmake + gcc + curl + wget + yasm + nasm + zip + make + pkg-config + clang + gtk3 + xdotool + libxcb + libxfixes + alsa-lib + pipewire + python + ttf-arphic-uming + libappindicator-gtk3 + scripts: | + cd res && HBB=`pwd`/.. FLUTTER=1 makepkg -f + + - name: Publish archlinux package + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + res/rustdesk*.zst diff --git a/build.py b/build.py index 7a0dc54fb..920832909 100755 --- a/build.py +++ b/build.py @@ -15,6 +15,7 @@ hbb_name = 'rustdesk' + ('.exe' if windows else '') exe_path = 'target/release/' + hbb_name flutter_win_target_dir = 'flutter/build/windows/runner/Release/' + def get_version(): with open("Cargo.toml") as fh: for line in fh: @@ -72,7 +73,8 @@ def make_parser(): parser.add_argument( '--hwcodec', action='store_true', - help='Enable feature hwcodec' + ('' if windows or osx else ', need libva-dev, libvdpau-dev.') + help='Enable feature hwcodec' + ( + '' if windows or osx else ', need libva-dev, libvdpau-dev.') ) parser.add_argument( '--portable', @@ -82,8 +84,44 @@ def make_parser(): return parser +# Generate build script for docker +# +# it assumes all build dependencies are installed in environments +# Note: do not use it in bare metal, or may break build environments +def generate_build_script_for_docker(): + with open("/tmp/build.sh", "w") as f: + f.write(''' + #!/bin/bash + # environment + export CPATH="$(clang -v 2>&1 | grep "Selected GCC installation: " | cut -d' ' -f4-)/include" + # flutter + pushd /opt + wget https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.0.5-stable.tar.xz + tar -xvf flutter_linux_3.0.5-stable.tar.xz + export PATH=`pwd`/flutter/bin:$PATH + popd + # flutter_rust_bridge + dart pub global activate ffigen --version 5.0.1 + pushd /tmp && git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1 && popd + pushd /tmp/flutter_rust_bridge/frb_codegen && cargo install --path . && popd + pushd flutter && flutter pub get && popd + ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart + # install vcpkg + pushd /opt + export VCPKG_ROOT=`pwd`/vcpkg + git clone https://github.com/microsoft/vcpkg + vcpkg/bootstrap-vcpkg.sh + vcpkg/vcpkg install libvpx libyuv opus + popd + # build rustdesk + ./build.py --flutter --hwcodec + ''') + os.system("chmod +x /tmp/build.sh") + os.system("bash /tmp/build.sh") + def download_extract_features(features, res_dir): proxy = '' + def req(url): if not proxy: return url @@ -100,7 +138,8 @@ def download_extract_features(features, res_dir): for line in checksum_md5_response.read().decode('utf-8').splitlines(): if line.split()[1] == download_filename: checksum_md5 = line.split()[0] - filename, _headers = urllib.request.urlretrieve(feat_info['zip_url'], download_filename) + filename, _headers = urllib.request.urlretrieve(feat_info['zip_url'], + download_filename) md5 = hashlib.md5(open(filename, 'rb').read()).hexdigest() if checksum_md5 != md5: raise Exception(f'{feat} download failed') @@ -139,7 +178,7 @@ def get_rc_features(args): return [] else: return ['with_rc'] - + def get_features(args): features = ['inline'] @@ -152,6 +191,7 @@ def get_features(args): print("features:", features) return features + def generate_control_file(version): control_file_path = "../res/DEBIAN/control" os.system('/bin/rm -rf %s' % control_file_path) @@ -169,10 +209,16 @@ Description: A remote control software. file.write(content) file.close() -def build_flutter_deb(version): - os.system('cargo build --features default,flutter --lib --release') + +def ffi_bindgen_function_refactor(): # workaround ffigen - os.system('sed -i "s/ffi.NativeFunction> tmpdeb/usr/share/rustdesk/files/polkit && chmod a+x tmpdeb/usr/share/rustdesk/files/polkit") + os.system( + "echo \"#!/bin/sh\" >> tmpdeb/usr/share/rustdesk/files/polkit && chmod a+x tmpdeb/usr/share/rustdesk/files/polkit") os.system('mkdir -p tmpdeb/DEBIAN') generate_control_file(version) @@ -209,21 +256,25 @@ def build_flutter_deb(version): os.chdir("..") -def build_flutter_arch_manjaro(): +def build_flutter_arch_manjaro(version, features): + os.system(f'cargo build --features {features} --lib --release') + ffi_bindgen_function_refactor() os.chdir('flutter') os.system('flutter build linux --release') - os.system('strip build/linux/x64/release/liblibrustdesk.so') - os.chdir('..') - os.system('HBB=`pwd` FLUTTER=1 makepkg -f') + os.system('strip build/linux/x64/release/bundle/lib/librustdesk.so') + os.chdir('../res') + os.system('HBB=`pwd`/.. FLUTTER=1 makepkg -f') -def build_flutter_windows(version): - os.system('cargo build --lib --features flutter --release') + +def build_flutter_windows(version, features): + os.system(f'cargo build --features {features} --lib --release') os.chdir('flutter') os.system('flutter build windows --release') os.chdir('..') os.chdir('libs/portable') os.system('pip3 install -r requirements.txt') - os.system(f'python3 ./generate.py -f ../../{flutter_win_target_dir} -o . -e ../../{flutter_win_target_dir}/rustdesk.exe') + os.system( + f'python3 ./generate.py -f ../../{flutter_win_target_dir} -o . -e ../../{flutter_win_target_dir}/rustdesk.exe') os.chdir('../..') if os.path.exists('./rustdesk_portable.exe'): os.replace('./target/release/rustdesk-portable-packer.exe', './rustdesk_portable.exe') @@ -233,6 +284,7 @@ def build_flutter_windows(version): os.rename('./rustdesk_portable.exe', f'./rustdesk-{version}-install.exe') print(f'output location: {os.path.abspath(os.curdir)}/rustdesk-{version}-install.exe') + def main(): parser = make_parser() args = parser.parse_args() @@ -255,45 +307,50 @@ def main(): portable = args.portable if windows: if flutter: - build_flutter_windows(version) + build_flutter_windows(version, features) return os.system('cargo build --release --features ' + features) # os.system('upx.exe target/release/rustdesk.exe') os.system('mv target/release/rustdesk.exe target/release/RustDesk.exe') pa = os.environ.get('P') if pa: - os.system(f'signtool sign /a /v /p {pa} /debug /f .\\cert.pfx /t http://timestamp.digicert.com ' - 'target\\release\\rustdesk.exe') + os.system( + f'signtool sign /a /v /p {pa} /debug /f .\\cert.pfx /t http://timestamp.digicert.com ' + 'target\\release\\rustdesk.exe') else: print('Not signed') os.system(f'cp -rf target/release/RustDesk.exe rustdesk-{version}-win7-install.exe') elif os.path.isfile('/usr/bin/pacman'): # pacman -S -needed base-devel - os.system("sed -i 's/pkgver=.*/pkgver=%s/g' PKGBUILD" % version) + os.system("sed -i 's/pkgver=.*/pkgver=%s/g' res/PKGBUILD" % version) if flutter: - build_flutter_arch_manjaro() + build_flutter_arch_manjaro(version, features) else: os.system('cargo build --release --features ' + features) os.system('git checkout src/ui/common.tis') os.system('strip target/release/rustdesk') os.system('ln -s res/pacman_install && ln -s res/PKGBUILD') os.system('HBB=`pwd` makepkg -f') - os.system('mv rustdesk-%s-0-x86_64.pkg.tar.zst rustdesk-%s-manjaro-arch.pkg.tar.zst' % (version, version)) + os.system('mv rustdesk-%s-0-x86_64.pkg.tar.zst rustdesk-%s-manjaro-arch.pkg.tar.zst' % ( + version, version)) # pacman -U ./rustdesk.pkg.tar.zst elif os.path.isfile('/usr/bin/yum'): os.system('cargo build --release --features ' + features) os.system('strip target/release/rustdesk') os.system("sed -i 's/Version: .*/Version: %s/g' res/rpm.spec" % version) os.system('HBB=`pwd` rpmbuild -ba res/rpm.spec') - os.system('mv $HOME/rpmbuild/RPMS/x86_64/rustdesk-%s-0.x86_64.rpm ./rustdesk-%s-fedora28-centos8.rpm' % ( - version, version)) + os.system( + 'mv $HOME/rpmbuild/RPMS/x86_64/rustdesk-%s-0.x86_64.rpm ./rustdesk-%s-fedora28-centos8.rpm' % ( + version, version)) # yum localinstall rustdesk.rpm elif os.path.isfile('/usr/bin/zypper'): os.system('cargo build --release --features ' + features) os.system('strip target/release/rustdesk') os.system("sed -i 's/Version: .*/Version: %s/g' res/rpm-suse.spec" % version) os.system('HBB=`pwd` rpmbuild -ba res/rpm-suse.spec') - os.system('mv $HOME/rpmbuild/RPMS/x86_64/rustdesk-%s-0.x86_64.rpm ./rustdesk-%s-suse.rpm' % (version, version)) + os.system( + 'mv $HOME/rpmbuild/RPMS/x86_64/rustdesk-%s-0.x86_64.rpm ./rustdesk-%s-suse.rpm' % ( + version, version)) # yum localinstall rustdesk.rpm else: os.system('cargo bundle --release --features ' + features) @@ -304,7 +361,7 @@ def main(): else: os.system( 'mv target/release/bundle/deb/rustdesk*.deb ./flutter/rustdesk.deb') - build_flutter_deb(version) + build_flutter_deb(version, features) else: if osx: os.system( diff --git a/res/PKGBUILD b/res/PKGBUILD index ff9ab1a28..2ab82f9ea 100644 --- a/res/PKGBUILD +++ b/res/PKGBUILD @@ -7,7 +7,7 @@ arch=('x86_64') url="" license=('AGPL-3.0') groups=() -depends=('gtk3' 'xdotool' 'libxcb' 'libxfixes' 'alsa-lib' 'pipewire' 'ttf-arphic-uming' 'curl' 'libappindicator-gtk3') +depends=('gtk3' 'xdotool' 'libxcb' 'libxfixes' 'alsa-lib' 'pipewire' 'ttf-arphic-uming' 'curl' 'libappindicator-gtk3' 'libva' 'libvdpau' 'libayatana-appindicator') makedepends=() checkdepends=() optdepends=() @@ -24,7 +24,6 @@ md5sums=() #generate with 'makepkg -g' package() { if [[ ${FLUTTER} ]]; then mkdir -p "${pkgdir}/usr/lib/rustdesk" && cp -r ${HBB}/flutter/build/linux/x64/release/bundle/* -t "${pkgdir}/usr/lib/rustdesk" - cp ${HBB}/flutter/build/linux/x64/release/liblibrustdesk.so "${pkgdir}/usr/lib/rustdesk/librustdesk.so" fi mkdir -p "${pkgdir}/usr/bin" pushd ${pkgdir} && ln -s /usr/lib/rustdesk/rustdesk usr/bin/rustdesk && popd From 74b03afe5ca495ba70c5f284d102dbfeee04afb2 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sat, 22 Oct 2022 22:43:26 +0800 Subject: [PATCH 0743/2015] fix: sub window failed to start or freeze issue --- flutter/lib/desktop/pages/file_manager_tab_page.dart | 3 +++ flutter/lib/desktop/pages/port_forward_tab_page.dart | 3 +++ flutter/lib/desktop/pages/remote_tab_page.dart | 3 +++ flutter/lib/main.dart | 3 --- flutter/pubspec.yaml | 2 +- 5 files changed, 10 insertions(+), 4 deletions(-) diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index f844edc36..85a2ab197 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -64,6 +64,9 @@ class _FileManagerTabPageState extends State { tabController.clear(); } }); + Future.delayed(Duration.zero, () { + restoreWindowPosition(WindowType.FileTransfer, windowId: windowId()); + }); } @override diff --git a/flutter/lib/desktop/pages/port_forward_tab_page.dart b/flutter/lib/desktop/pages/port_forward_tab_page.dart index 6c6865718..64a67121b 100644 --- a/flutter/lib/desktop/pages/port_forward_tab_page.dart +++ b/flutter/lib/desktop/pages/port_forward_tab_page.dart @@ -72,6 +72,9 @@ class _PortForwardTabPageState extends State { tabController.clear(); } }); + Future.delayed(Duration.zero, () { + restoreWindowPosition(WindowType.PortForward, windowId: windowId()); + }); } @override diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index 1d3daf7b3..ea0658b6e 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -92,6 +92,9 @@ class _ConnectionTabPageState extends State { } _update_remote_count(); }); + Future.delayed(Duration.zero, () { + restoreWindowPosition(WindowType.RemoteDesktop, windowId: windowId()); + }); } @override diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 6f69a9c2b..1b896c781 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -121,7 +121,6 @@ void runMobileApp() async { void runRemoteScreen(Map argument) async { await initEnv(kAppTypeDesktopRemote); - await restoreWindowPosition(WindowType.RemoteDesktop, windowId: windowId); runApp(GetMaterialApp( navigatorKey: globalKey, debugShowCheckedModeBanner: false, @@ -147,7 +146,6 @@ void runRemoteScreen(Map argument) async { void runFileTransferScreen(Map argument) async { await initEnv(kAppTypeDesktopFileTransfer); - await restoreWindowPosition(WindowType.FileTransfer, windowId: windowId); runApp( GetMaterialApp( navigatorKey: globalKey, @@ -173,7 +171,6 @@ void runFileTransferScreen(Map argument) async { void runPortForwardScreen(Map argument) async { await initEnv(kAppTypeDesktopPortForward); - await restoreWindowPosition(WindowType.PortForward, windowId: windowId); runApp( GetMaterialApp( navigatorKey: globalKey, diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index f77fe9894..baa921339 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -64,7 +64,7 @@ dependencies: desktop_multi_window: git: url: https://github.com/Kingtous/rustdesk_desktop_multi_window - ref: 318ebd0a70cc5868911591c04f84bf1541f1bf4e + ref: 541f05f766c3f72984ff40b70dd3c7d061f2ce61 freezed_annotation: ^2.0.3 tray_manager: git: From aee596439e4822431331d52e757a43412f6cfa4d Mon Sep 17 00:00:00 2001 From: Wahyu Kristianto Date: Sun, 23 Oct 2022 04:45:55 +0700 Subject: [PATCH 0744/2015] Update lang id.rs Signed-off-by: Wahyu Kristianto --- src/lang/id.rs | 108 ++++++++++++++++++++++++------------------------- 1 file changed, 54 insertions(+), 54 deletions(-) diff --git a/src/lang/id.rs b/src/lang/id.rs index fd0b3bbf8..fc1ab88c2 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -87,8 +87,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Delete", "Hapus"), ("Properties", "Properti"), ("Multi Select", "Pilih Beberapa"), - ("Select All", ""), - ("Unselect All", ""), + ("Select All", "Pilih Semua"), + ("Unselect All", "Batalkan Pilihan Semua"), ("Empty Directory", "Folder Kosong"), ("Not an empty directory", "Folder tidak kosong"), ("Are you sure you want to delete this file?", "Apakah anda yakin untuk menghapus file ini?"), @@ -114,9 +114,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Good image quality", "Kualitas Gambar Baik"), ("Balanced", "Seimbang"), ("Optimize reaction time", "Optimalkan waktu reaksi"), - ("Custom", ""), + ("Custom", "Kustom"), ("Show remote cursor", "Tampilkan remote kursor"), - ("Show quality monitor", ""), + ("Show quality monitor", "Tampilkan kualitas monitor"), ("Disable clipboard", "Matikan papan klip"), ("Lock after session end", "Kunci setelah sesi berakhir"), ("Insert", "Menyisipkan"), @@ -159,8 +159,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Action", "Aksi"), ("Add", "Tambah"), ("Local Port", "Port Lokal"), - ("Local Address", ""), - ("Change Local Port", ""), + ("Local Address", "Alamat lokal"), + ("Change Local Port", "Ubah Port Lokal"), ("setup_server_tip", "Untuk koneksi yang lebih cepat, silakan atur server Anda sendiri"), ("Too short, at least 6 characters.", "Terlalu pendek, setidaknya 6 karekter."), ("The confirmation is not identical.", "Konfirmasi tidak identik."), @@ -195,7 +195,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Reboot required", "Diperlukan boot ulang"), ("Unsupported display server ", "Server tampilan tidak didukung "), ("x11 expected", "x11 diharapkan"), - ("Port", ""), + ("Port", "Port"), ("Settings", "Pengaturan"), ("Username", "Username"), ("Invalid port", "Kesalahan port"), @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logout", "Keluar"), ("Tags", "Tag"), ("Search ID", "Cari ID"), - ("Current Wayland display server is not supported", ""), + ("Current Wayland display server is not supported", "Server tampilan Wayland saat ini tidak didukung"), ("whitelist_sep", "Dipisahkan dengan koma, titik koma, spasi, atau baris baru"), ("Add ID", "Tambah ID"), ("Add Tag", "Tambah Tag"), @@ -276,7 +276,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_stop_service_tip", "Menutup layanan akan secara otomatis menutup semua koneksi yang dibuat."), ("android_version_audio_tip", "Versi Android saat ini tidak mendukung pengambilan audio, harap tingkatkan ke Android 10 atau lebih tinggi."), ("android_start_service_tip", "Ketuk izin [Mulai Layanan] atau BUKA [Tangkapan Layar] untuk memulai layanan berbagi layar."), - ("Account", ""), + ("Account", "Akun"), ("Overwrite", "Timpa"), ("This file exists, skip or overwrite this file?", "File ini sudah ada, lewati atau timpa file ini?"), ("Quit", "Keluar"), @@ -298,9 +298,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Ignore Battery Optimizations", "Abaikan Pengoptimalan Baterai"), ("android_open_battery_optimizations_tip", ""), ("Connection not allowed", "Koneksi tidak dijinkan"), - ("Legacy mode", ""), - ("Map mode", ""), - ("Translate mode", ""), + ("Legacy mode", "Mode lama"), + ("Map mode", "Mode peta"), + ("Translate mode", "Mode terjemahan"), ("Use temporary password", "Gunakan kata sandi sementara"), ("Use permanent password", "Gunakan kata sandi permanaen"), ("Use both passwords", "Gunakan kedua kata sandi "), @@ -312,7 +312,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Are you sure you want to restart", "Apakah Anda yakin untuk memulai ulang"), ("Restarting Remote Device", "Memulai Ulang Perangkat Jarak Jauh"), ("remote_restarting_tip", ""), - ("Copied", ""), + ("Copied", "Disalin"), ("Exit Fullscreen", "Keluar dari Layar Penuh"), ("Fullscreen", "Layar penuh"), ("Mobile Actions", "Tindakan Seluler"), @@ -330,55 +330,55 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Insecure Connection", "Koneksi Tidak Aman"), ("Scale original", "Skala asli"), ("Scale adaptive", "Skala adaptif"), - ("General", ""), - ("Security", ""), - ("Account", ""), - ("Theme", ""), - ("Dark Theme", ""), - ("Dark", ""), - ("Light", ""), - ("Follow System", ""), - ("Enable hardware codec", ""), - ("Unlock Security Settings", ""), - ("Enable Audio", ""), - ("Temporary Password Length", ""), - ("Unlock Network Settings", ""), - ("Server", ""), - ("Direct IP Access", ""), - ("Proxy", ""), - ("Port", ""), - ("Apply", ""), - ("Disconnect all devices?", ""), + ("General", "Umum"), + ("Security", "Keamanan"), + ("Account", "Akun"), + ("Theme", "Tema"), + ("Dark Theme", "Tema gelap"), + ("Dark", "Gelap"), + ("Light", "Terang"), + ("Follow System", "Ikuti sistem"), + ("Enable hardware codec", "Aktifkan codec perangkat keras"), + ("Unlock Security Settings", "Buka Kunci Pengaturan Keamanan"), + ("Enable Audio", "Aktifkan Audio"), + ("Temporary Password Length", "Panjang Kata Sandi Sementara"), + ("Unlock Network Settings", "Buka Kunci Pengaturan Jaringan"), + ("Server", "Server"), + ("Direct IP Access", "Direct IP Access"), + ("Proxy", "Proxy"), + ("Port", "Port"), + ("Apply", "Terapkan"), + ("Disconnect all devices?", "Putuskan sambungan semua perangkat?"), ("Clear", ""), ("Audio Input Device", ""), - ("Deny remote access", ""), - ("Use IP Whitelisting", ""), - ("Network", ""), - ("Enable RDP", ""), + ("Deny remote access", "Tolak akses jarak jauh"), + ("Use IP Whitelisting", "Gunakan Daftar Putih IP"), + ("Network", "Jaringan"), + ("Enable RDP", "Aktifkan RDP"), ("Pin menubar", "Pin menubar"), ("Unpin menubar", "Unpin menubar"), - ("Recording", ""), - ("Directory", ""), - ("Automatically record incoming sessions", ""), - ("Change", ""), - ("Start session recording", ""), - ("Stop session recording", ""), - ("Enable Recording Session", ""), - ("Allow recording session", ""), - ("Enable LAN Discovery", ""), - ("Deny LAN Discovery", ""), - ("Write a message", ""), + ("Recording", "Rekaman"), + ("Directory", "Direktori"), + ("Automatically record incoming sessions", "Secara otomatis merekam sesi masuk"), + ("Change", "Mengubah"), + ("Start session recording", "Mulai sesi perekaman"), + ("Stop session recording", "Hentikan sesi perekaman"), + ("Enable Recording Session", "Aktifkan Sesi Perekaman"), + ("Allow recording session", "Izinkan sesi perekaman"), + ("Enable LAN Discovery", "Aktifkan Penemuan LAN"), + ("Deny LAN Discovery", "Tolak Penemuan LAN"), + ("Write a message", "Menulis pesan"), ("Prompt", ""), ("elevation_prompt", ""), ("uac_warning", ""), ("elevated_foreground_window_warning", ""), - ("Disconnected", ""), - ("Other", ""), - ("Confirm before closing multiple tabs", ""), - ("Keyboard Settings", ""), - ("Custom", ""), - ("Full Access", ""), - ("Screen Share", ""), + ("Disconnected", "Terputus"), + ("Other", "Lainnya"), + ("Confirm before closing multiple tabs", "Konfirmasi sebelum menutup banyak tab"), + ("Keyboard Settings", "Pengaturan Papan Ketik"), + ("Custom", "Kustom"), + ("Full Access", "Akses penuh"), + ("Screen Share", "Berbagi Layar"), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland membutuhkan Ubuntu 21.04 atau versi yang lebih tinggi."), ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland membutuhkan versi distro linux yang lebih tinggi. Silakan coba desktop X11 atau ubah OS Anda."), ("JumpLink", "View"), From 81221bfa67058fdb873cc81b05272935f6829346 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sun, 23 Oct 2022 09:27:45 +0800 Subject: [PATCH 0745/2015] fix: add ayatana deps for nightly arch ci --- .github/workflows/flutter-nightly.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 706e0b247..efaa407e3 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -219,6 +219,7 @@ jobs: python ttf-arphic-uming libappindicator-gtk3 + libayatana-appindicator scripts: | cd res && HBB=`pwd`/.. FLUTTER=1 makepkg -f From b14f4574747ea34aa54330607913749c6604e76e Mon Sep 17 00:00:00 2001 From: 21pages Date: Sat, 22 Oct 2022 12:03:57 +0800 Subject: [PATCH 0746/2015] auto new line && add server conf export Signed-off-by: 21pages --- .../lib/desktop/pages/connection_page.dart | 45 +- .../desktop/pages/desktop_setting_page.dart | 658 +++++++----------- src/lang/cn.rs | 4 +- src/lang/cs.rs | 4 +- src/lang/da.rs | 4 +- src/lang/de.rs | 4 +- src/lang/eo.rs | 4 +- src/lang/es.rs | 4 +- src/lang/fr.rs | 4 +- src/lang/hu.rs | 4 +- src/lang/id.rs | 4 +- src/lang/it.rs | 4 +- src/lang/ja.rs | 4 +- src/lang/ko.rs | 4 +- src/lang/kz.rs | 4 +- src/lang/pl.rs | 4 +- src/lang/pt_PT.rs | 4 +- src/lang/ptbr.rs | 4 +- src/lang/ru.rs | 4 +- src/lang/sk.rs | 4 +- src/lang/template.rs | 4 +- src/lang/tr.rs | 4 +- src/lang/tw.rs | 5 +- src/lang/ua.rs | 4 +- src/lang/vn.rs | 4 +- 25 files changed, 351 insertions(+), 445 deletions(-) diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 8964191f8..30b685347 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -282,23 +282,34 @@ class _ConnectionPageState extends State .marginOnly(left: em), ), // ready && public - Offstage( - offstage: !(!svcStopped.value && - svcStatusCode.value == 1 && - svcIsUsingPublicServer.value), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text(', ', style: TextStyle(fontSize: em)), - InkWell( - onTap: onUsePublicServerGuide, - child: Text( - translate('setup_server_tip'), - style: TextStyle( - decoration: TextDecoration.underline, fontSize: em), - ), - ) - ], + Flexible( + child: Offstage( + offstage: !(!svcStopped.value && + svcStatusCode.value == 1 && + svcIsUsingPublicServer.value), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(', ', style: TextStyle(fontSize: em)), + Flexible( + child: InkWell( + onTap: onUsePublicServerGuide, + child: Row( + children: [ + Flexible( + child: Text( + translate('setup_server_tip'), + style: TextStyle( + decoration: TextDecoration.underline, + fontSize: em), + ), + ), + ], + ), + ), + ) + ], + ), ), ) ], diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 23d832580..22247c033 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -321,6 +321,7 @@ class _GeneralState extends State<_General> { ...devices.map((device) => _Radio(context, value: device, groupValue: currentDevice, + autoNewLine: false, label: device, onChanged: (value) { setDevice(value); setState(() {}); @@ -812,11 +813,7 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin { AbsorbPointer( absorbing: locked, child: Column(children: [ - _CardRow(title: 'Server', children: [ - _Button('ID/Relay Server', changeServer, enabled: enabled), - _Button('Import Server Conf', importServer, - enabled: enabled), - ]), + server(enabled), _Card(title: 'Proxy', children: [ _Button('Socks5 Proxy', changeSocks5Proxy, enabled: enabled), @@ -825,6 +822,156 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin { ), ]).marginOnly(bottom: _kListViewBottomMargin)); } + + server(bool enabled) { + return _futureBuilder(future: () async { + return await bind.mainGetOptions(); + }(), hasData: (data) { + // Setting page is not modal, oldOptions should only be used when getting options, never when setting. + Map oldOptions = jsonDecode(data! as String); + old(String key) { + return (oldOptions[key] ?? "").trim(); + } + + RxString idErrMsg = "".obs; + RxString relayErrMsg = "".obs; + RxString apiErrMsg = "".obs; + var idController = + TextEditingController(text: old('custom-rendezvous-server')); + var relayController = TextEditingController(text: old('relay-server')); + var apiController = TextEditingController(text: old('api-server')); + var keyController = TextEditingController(text: old('key')); + + set(String idServer, String relayServer, String apiServer, + String key) async { + idServer = idServer.trim(); + relayServer = relayServer.trim(); + apiServer = apiServer.trim(); + key = key.trim(); + if (idServer.isNotEmpty) { + idErrMsg.value = + translate(await bind.mainTestIfValidServer(server: idServer)); + if (idErrMsg.isNotEmpty) { + return false; + } + } + if (relayServer.isNotEmpty) { + relayErrMsg.value = + translate(await bind.mainTestIfValidServer(server: relayServer)); + if (relayErrMsg.isNotEmpty) { + return false; + } + } + if (apiServer.isNotEmpty) { + if (!apiServer.startsWith('http://') || + !apiServer.startsWith("https://")) { + apiErrMsg.value = + "${translate("API Server")}: ${translate("invalid_http")}"; + return false; + } + } + // should set one by one + await bind.mainSetOption( + key: 'custom-rendezvous-server', value: idServer); + await bind.mainSetOption(key: 'relay-server', value: relayServer); + await bind.mainSetOption(key: 'api-server', value: apiServer); + await bind.mainSetOption(key: 'key', value: key); + return true; + } + + submit() async { + bool result = await set(idController.text, relayController.text, + apiController.text, keyController.text); + if (result) { + setState(() {}); + showToast(translate('Successful')); + } else { + showToast(translate('Failed')); + } + } + + import() { + Clipboard.getData(Clipboard.kTextPlain).then((value) { + TextEditingController mytext = TextEditingController(); + String? aNullableString = ""; + aNullableString = value?.text; + mytext.text = aNullableString.toString(); + if (mytext.text.isNotEmpty) { + try { + Map config = jsonDecode(mytext.text); + if (config.containsKey('IdServer')) { + String id = config['IdServer'] ?? ''; + String relay = config['RelayServer'] ?? ''; + String api = config['ApiServer'] ?? ''; + String key = config['Key'] ?? ''; + idController.text = id; + relayController.text = relay; + apiController.text = api; + keyController.text = key; + Future success = set(id, relay, api, key); + success.then((value) { + if (value) { + showToast( + translate('Import server configuration successfully')); + } else { + showToast(translate('Invalid server configuration')); + } + }); + } else { + showToast(translate("Invalid server configuration")); + } + } catch (e) { + showToast(translate("Invalid server configuration")); + } + } else { + showToast(translate("Clipboard is empty")); + } + }); + } + + export() { + Map config = {}; + config['IdServer'] = idController.text.trim(); + config['RelayServer'] = relayController.text.trim(); + config['ApiServer'] = apiController.text.trim(); + config['Key'] = keyController.text.trim(); + Clipboard.setData(ClipboardData(text: jsonEncode(config))); + showToast(translate("Export server configuration successfully")); + } + + bool secure = !enabled; + return _Card(title: 'ID/Relay Server', title_suffix: [ + Tooltip( + message: translate('Import Server Config'), + child: IconButton( + icon: Icon(Icons.paste, color: Colors.grey), + onPressed: enabled ? import : null), + ), + Tooltip( + message: translate('Export Server Config'), + child: IconButton( + icon: Icon(Icons.copy, color: Colors.grey), + onPressed: enabled ? export : null)), + ], children: [ + Column( + children: [ + Obx(() => _LabeledTextField(context, 'ID Server', idController, + idErrMsg.value, enabled, secure)), + Obx(() => _LabeledTextField(context, 'Relay Server', + relayController, relayErrMsg.value, enabled, secure)), + Obx(() => _LabeledTextField(context, 'API Server', apiController, + apiErrMsg.value, enabled, secure)), + _LabeledTextField( + context, 'Key', keyController, "", enabled, secure), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [_Button('Apply', submit, enabled: enabled)], + ).marginOnly(top: 15), + ], + ) + ]); + }); + } } class _Account extends StatefulWidget { @@ -955,63 +1102,37 @@ class _AboutState extends State<_About> { //#region components // ignore: non_constant_identifier_names -Widget _Card({required String title, required List children}) { +Widget _Card( + {required String title, + required List children, + List? title_suffix}) { return Row( children: [ - SizedBox( - width: _kCardFixedWidth, - child: Card( - child: Column( - children: [ - Row( - children: [ - Text( - translate(title), - textAlign: TextAlign.start, - style: const TextStyle( - fontSize: _kTitleFontSize, - ), - ), - const Spacer(), - ], - ).marginOnly(left: _kContentHMargin, top: 10, bottom: 10), - ...children - .map((e) => e.marginOnly(top: 4, right: _kContentHMargin)), - ], - ).marginOnly(bottom: 10), - ).marginOnly(left: _kCardLeftMargin, top: 15), - ), - ], - ); -} - -Widget _CardRow({required String title, required List children}) { - return Row( - children: [ - SizedBox( - width: _kCardFixedWidth, - child: Card( - child: Column( - children: [ - Row( - children: [ - Text( - translate(title), - textAlign: TextAlign.start, - style: const TextStyle( - fontSize: _kTitleFontSize, - ), - ), - const Spacer(), - ], - ).marginOnly(left: _kContentHMargin, top: 10, bottom: 10), - Row(children: [ + Flexible( + child: SizedBox( + width: _kCardFixedWidth, + child: Card( + child: Column( + children: [ + Row( + children: [ + Expanded( + child: Text( + translate(title), + textAlign: TextAlign.start, + style: const TextStyle( + fontSize: _kTitleFontSize, + ), + )), + ...?title_suffix + ], + ).marginOnly(left: _kContentHMargin, top: 10, bottom: 10), ...children .map((e) => e.marginOnly(top: 4, right: _kContentHMargin)), - ]), - ], - ).marginOnly(bottom: 10), - ).marginOnly(left: _kCardLeftMargin, top: 15), + ], + ).marginOnly(bottom: 10), + ).marginOnly(left: _kCardLeftMargin, top: 15), + ), ), ], ); @@ -1085,6 +1206,7 @@ Widget _Radio(BuildContext context, required T groupValue, required String label, required Function(T value) onChanged, + bool autoNewLine = true, bool enabled = true}) { var onChange = enabled ? (T? value) { @@ -1099,8 +1221,7 @@ Widget _Radio(BuildContext context, Radio(value: value, groupValue: groupValue, onChanged: onChange), Expanded( child: Text(translate(label), - maxLines: 1, - overflow: TextOverflow.ellipsis, + overflow: autoNewLine ? null : TextOverflow.ellipsis, style: TextStyle( fontSize: _kContentFontSize, color: _disabledTextColor(context, enabled))) @@ -1116,12 +1237,11 @@ Widget _Radio(BuildContext context, Widget _Button(String label, Function() onPressed, {bool enabled = true, String? tip}) { var button = ElevatedButton( - onPressed: enabled ? onPressed : null, - child: Container( - child: Text( - translate(label), - ).marginSymmetric(horizontal: 15), - )); + onPressed: enabled ? onPressed : null, + child: Text( + translate(label), + ).marginSymmetric(horizontal: 15), + ); StatefulWidget child; if (tip == null) { child = button; @@ -1138,12 +1258,11 @@ Widget _SubButton(String label, Function() onPressed, [bool enabled = true]) { return Row( children: [ ElevatedButton( - onPressed: enabled ? onPressed : null, - child: Container( - child: Text( - translate(label), - ).marginSymmetric(horizontal: 15), - )), + onPressed: enabled ? onPressed : null, + child: Text( + translate(label), + ).marginSymmetric(horizontal: 15), + ), ], ).marginOnly(left: _kContentHSubMargin); } @@ -1215,34 +1334,72 @@ Widget _lock( offstage: !locked, child: Row( children: [ - SizedBox( - width: _kCardFixedWidth, - child: Card( - child: ElevatedButton( - child: SizedBox( - height: 25, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon( - Icons.security_sharp, - size: 20, - ), - Text(translate(label)).marginOnly(left: 5), - ]).marginSymmetric(vertical: 2)), - onPressed: () async { - bool checked = await bind.mainCheckSuperUserPermission(); - if (checked) { - onUnlock(); - } - }, - ).marginSymmetric(horizontal: 2, vertical: 4), - ).marginOnly(left: _kCardLeftMargin), - ).marginOnly(top: 10), + Flexible( + child: SizedBox( + width: _kCardFixedWidth, + child: Card( + child: ElevatedButton( + child: SizedBox( + height: 25, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.security_sharp, + size: 20, + ), + Text(translate(label)).marginOnly(left: 5), + ]).marginSymmetric(vertical: 2)), + onPressed: () async { + bool checked = await bind.mainCheckSuperUserPermission(); + if (checked) { + onUnlock(); + } + }, + ).marginSymmetric(horizontal: 2, vertical: 4), + ).marginOnly(left: _kCardLeftMargin), + ).marginOnly(top: 10), + ), ], )); } +_LabeledTextField( + BuildContext context, + String lable, + TextEditingController controller, + String errorText, + bool enabled, + bool secure) { + return Row( + children: [ + Spacer(flex: 1), + Expanded( + flex: 4, + child: Text( + '${translate(lable)}:', + 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( + errorText: errorText.isNotEmpty ? errorText : null), + style: TextStyle( + color: _disabledTextColor(context, enabled), + )), + ), + Spacer(flex: 1), + ], + ); +} + // ignore: must_be_immutable class _ComboBox extends StatelessWidget { late final List keys; @@ -1312,315 +1469,6 @@ class _ComboBox extends StatelessWidget { //#region dialogs -void changeServer() async { - Map oldOptions = jsonDecode(await bind.mainGetOptions()); - String idServer = oldOptions['custom-rendezvous-server'] ?? ""; - var idServerMsg = ""; - String relayServer = oldOptions['relay-server'] ?? ""; - var relayServerMsg = ""; - String apiServer = oldOptions['api-server'] ?? ""; - var apiServerMsg = ""; - var key = oldOptions['key'] ?? ""; - var idController = TextEditingController(text: idServer); - var relayController = TextEditingController(text: relayServer); - var apiController = TextEditingController(text: apiServer); - var keyController = TextEditingController(text: key); - - var isInProgress = false; - - gFFI.dialogManager.show((setState, close) { - submit() async { - setState(() { - idServerMsg = ""; - relayServerMsg = ""; - apiServerMsg = ""; - isInProgress = true; - }); - cancel() { - setState(() { - isInProgress = false; - }); - } - - idServer = idController.text.trim(); - relayServer = relayController.text.trim(); - apiServer = apiController.text.trim().toLowerCase(); - key = keyController.text.trim(); - - if (idServer.isNotEmpty) { - idServerMsg = - translate(await bind.mainTestIfValidServer(server: idServer)); - if (idServerMsg.isEmpty) { - oldOptions['custom-rendezvous-server'] = idServer; - } else { - cancel(); - return; - } - } else { - oldOptions['custom-rendezvous-server'] = ""; - } - - if (relayServer.isNotEmpty) { - relayServerMsg = - translate(await bind.mainTestIfValidServer(server: relayServer)); - if (relayServerMsg.isEmpty) { - oldOptions['relay-server'] = relayServer; - } else { - cancel(); - return; - } - } else { - oldOptions['relay-server'] = ""; - } - - if (apiServer.isNotEmpty) { - if (apiServer.startsWith('http://') || - apiServer.startsWith("https://")) { - oldOptions['api-server'] = apiServer; - return; - } else { - apiServerMsg = translate("invalid_http"); - cancel(); - return; - } - } else { - oldOptions['api-server'] = ""; - } - // ok - oldOptions['key'] = key; - await bind.mainSetOptions(json: jsonEncode(oldOptions)); - close(); - } - - return CustomAlertDialog( - title: Text(translate("ID/Relay Server")), - content: ConstrainedBox( - constraints: const BoxConstraints(minWidth: 500), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox( - height: 8.0, - ), - Row( - children: [ - ConstrainedBox( - constraints: const BoxConstraints(minWidth: 100), - child: Text("${translate('ID Server')}:") - .marginOnly(bottom: 16.0)), - const SizedBox( - width: 24.0, - ), - Expanded( - child: TextField( - decoration: InputDecoration( - border: const OutlineInputBorder(), - errorText: idServerMsg.isNotEmpty ? idServerMsg : null), - controller: idController, - focusNode: FocusNode()..requestFocus(), - ), - ), - ], - ), - const SizedBox( - height: 8.0, - ), - Row( - children: [ - ConstrainedBox( - constraints: const BoxConstraints(minWidth: 100), - child: Text("${translate('Relay Server')}:") - .marginOnly(bottom: 16.0)), - const SizedBox( - width: 24.0, - ), - Expanded( - child: TextField( - decoration: InputDecoration( - border: const OutlineInputBorder(), - errorText: - relayServerMsg.isNotEmpty ? relayServerMsg : null), - controller: relayController, - ), - ), - ], - ), - const SizedBox( - height: 8.0, - ), - Row( - children: [ - ConstrainedBox( - constraints: const BoxConstraints(minWidth: 100), - child: Text("${translate('API Server')}:") - .marginOnly(bottom: 16.0)), - const SizedBox( - width: 24.0, - ), - Expanded( - child: TextField( - decoration: InputDecoration( - border: const OutlineInputBorder(), - errorText: - apiServerMsg.isNotEmpty ? apiServerMsg : null), - controller: apiController, - ), - ), - ], - ), - const SizedBox( - height: 8.0, - ), - Row( - children: [ - ConstrainedBox( - constraints: const BoxConstraints(minWidth: 100), - child: - Text("${translate('Key')}:").marginOnly(bottom: 16.0)), - const SizedBox( - width: 24.0, - ), - Expanded( - child: TextField( - decoration: const InputDecoration( - border: OutlineInputBorder(), - ), - controller: keyController, - ), - ), - ], - ), - const SizedBox( - height: 4.0, - ), - Offstage( - offstage: !isInProgress, child: const LinearProgressIndicator()) - ], - ), - ), - actions: [ - TextButton(onPressed: close, child: Text(translate("Cancel"))), - TextButton(onPressed: submit, child: Text(translate("OK"))), - ], - onSubmit: submit, - onCancel: close, - ); - }); -} - -void importServer() async { - Future importServerShow(String content) async { - gFFI.dialogManager.show((setState, close) { - return CustomAlertDialog( - title: Text(content), - content: ConstrainedBox( - constraints: const BoxConstraints(minWidth: 500), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox( - height: 4.0, - ), - ], - ), - ), - actions: [ - TextButton(onPressed: close, child: Text(translate("OK"))), - ], - onCancel: close, - ); - }); - } - - Future submit( - String idServer, String relayServer, String apiServer, String key) async { - Map oldOptions = jsonDecode(await bind.mainGetOptions()); - var idServerMsg = ""; - var relayServerMsg = ""; - if (idServer.isNotEmpty) { - idServerMsg = - translate(await bind.mainTestIfValidServer(server: idServer)); - if (idServerMsg.isEmpty) { - oldOptions['custom-rendezvous-server'] = idServer; - } else { - debugPrint('ID Server invalid return'); - return false; - } - } else { - oldOptions['custom-rendezvous-server'] = ""; - } - - if (relayServer.isNotEmpty) { - relayServerMsg = - translate(await bind.mainTestIfValidServer(server: relayServer)); - if (relayServerMsg.isEmpty) { - oldOptions['relay-server'] = relayServer; - } else { - debugPrint('Relay Server invalid return'); - return false; - } - } else { - oldOptions['relay-server'] = ""; - } - - if (apiServer.isNotEmpty) { - if (apiServer.startsWith('http://') || apiServer.startsWith("https://")) { - oldOptions['api-server'] = apiServer; - return false; - } else { - debugPrint('invalid_http'); - return false; - } - } else { - oldOptions['api-server'] = ""; - } - // ok - oldOptions['key'] = key; - await bind.mainSetOptions(json: jsonEncode(oldOptions)); - debugPrint("set ID/Realy Server Ok"); - return true; - } - - Clipboard.getData(Clipboard.kTextPlain).then((value) { - TextEditingController mytext = TextEditingController(); - String? aNullableString = ""; - aNullableString = value?.text; - mytext.text = aNullableString.toString(); - if (mytext.text.isNotEmpty) { - debugPrint('Clipboard is not empty'); - try { - Map config = jsonDecode(mytext.text); - if (config.containsKey('IdServer') && - config.containsKey('RelayServer')) { - debugPrint('IdServer: ${config['IdServer']}'); - debugPrint('RelayServer: ${config['RelayServer']}'); - debugPrint('ApiServer: ${config['ApiServer']}'); - debugPrint('Key: ${config['Key']}'); - Future success = submit(config['IdServer'], - config['RelayServer'], config['ApiServer'], config['Key']); - success.then((value) { - if (value) { - importServerShow( - translate('Import server configuration successfully')); - } else { - importServerShow(translate('Invalid server configuration')); - } - }); - } else { - debugPrint('invalid config info'); - importServerShow(translate("Invalid server configuration")); - } - } catch (e) { - debugPrint('invalid config info'); - importServerShow(translate("Invalid server configuration")); - } - } else { - debugPrint('Clipboard is empty'); - importServerShow(translate("Clipboard is empty")); - } - }); -} - void changeSocks5Proxy() async { var socks = await bind.mainGetSocks(); diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 32e30f81c..ca69ef4e4 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -29,8 +29,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable TCP Tunneling", "允许建立TCP隧道"), ("IP Whitelisting", "IP白名单"), ("ID/Relay Server", "ID/中继服务器"), - ("Import Server Conf", "导入服务器配置"), + ("Import Server Config", "导入服务器配置"), + ("Export Server Config", "导出服务器配置"), ("Import server configuration successfully", "导入服务器配置信息成功"), + ("Export server configuration successfully", "导出服务器配置信息成功"), ("Invalid server configuration", "无效服务器配置,请修改后重新拷贝配置信息到剪贴板后点击此按钮"), ("Clipboard is empty", "拷贝配置信息到剪贴板后点击此按钮,可以自动导入配置"), ("Stop service", "停止服务"), diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 6610c312b..84a5c590e 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -29,8 +29,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable TCP Tunneling", "Povolit TCP tunelování"), ("IP Whitelisting", "Povolování pouze z daných IP adres)"), ("ID/Relay Server", "Identifikátor / předávací (relay) server"), - ("Import Server Conf", "Importovat konfiguraci serveru"), + ("Import Server Config", "Importovat konfiguraci serveru"), + ("Export Server Config", ""), ("Import server configuration successfully", "Konfigurace serveru úspěšně importována"), + ("Export server configuration successfully", ""), ("Invalid server configuration", "Neplatná konfigurace serveru"), ("Clipboard is empty", "Schránka je prázdná"), ("Stop service", "Zastavit službu"), diff --git a/src/lang/da.rs b/src/lang/da.rs index 3bf05a3d6..1210c2dba 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -29,8 +29,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable TCP Tunneling", "Slå TCP-tunneling til"), ("IP Whitelisting", "IP-udgivelsesliste"), ("ID/Relay Server", "ID/forbindelsesserver"), - ("Import Server Conf", "Importér serverkonfiguration"), + ("Import Server Config", "Importér serverkonfiguration"), + ("Export Server Config", ""), ("Import server configuration successfully", "Importér serverkonfigurationen"), + ("Export server configuration successfully", ""), ("Invalid server configuration", "Ugyldig serverkonfiguration"), ("Clipboard is empty", "Udklipsholderen er tom"), ("Stop service", "Sluk for forbindelsesserveren"), diff --git a/src/lang/de.rs b/src/lang/de.rs index 095206f8e..95d0c3e5b 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -29,8 +29,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable TCP Tunneling", "TCP-Tunnel aktivieren"), ("IP Whitelisting", "IP-Whitelist"), ("ID/Relay Server", "ID/Vermittlungsserver"), - ("Import Server Conf", "Serverkonfiguration importieren"), + ("Import Server Config", "Serverkonfiguration importieren"), + ("Export Server Config", ""), ("Import server configuration successfully", "Serverkonfiguration erfolgreich importiert"), + ("Export server configuration successfully", ""), ("Invalid server configuration", "Ungültige Serverkonfiguration"), ("Clipboard is empty", "Zwischenablage ist leer"), ("Stop service", "Vermittlungsdienst deaktivieren"), diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 1b2a391d9..57955b9b8 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -29,8 +29,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable TCP Tunneling", "Ebligi tunelado TCP"), ("IP Whitelisting", "Listo de IP akceptataj"), ("ID/Relay Server", "Identigila/Relajsa servilo"), - ("Import Server Conf", "Enporti servilan agordon"), + ("Import Server Config", "Enporti servilan agordon"), + ("Export Server Config", ""), ("Import server configuration successfully", "Importi servilan agordon sukcese"), + ("Export server configuration successfully", ""), ("Invalid server configuration", "Nevalida servila agordo"), ("Clipboard is empty", "La poŝo estas malplena"), ("Stop service", "Haltu servon"), diff --git a/src/lang/es.rs b/src/lang/es.rs index ef90bb711..39e904eda 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -29,8 +29,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable TCP Tunneling", "Habilitar tunel TCP"), ("IP Whitelisting", "Lista blanca de IP"), ("ID/Relay Server", "Servidor ID/Relay"), - ("Import Server Conf", "Importar configuración de servidor"), + ("Import Server Config", "Importar configuración de servidor"), + ("Export Server Config", ""), ("Import server configuration successfully", "Configuración de servidor importada con éxito"), + ("Export server configuration successfully", ""), ("Invalid server configuration", "Configuración de servidor inválida"), ("Clipboard is empty", "El portapapeles está vacío"), ("Stop service", "Parar servicio"), diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 8b6420823..8e75eec3a 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -29,8 +29,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable TCP Tunneling", "Activer le tunneling TCP"), ("IP Whitelisting", "Liste blanche IP"), ("ID/Relay Server", "ID/Serveur Relais"), - ("Import Server Conf", "Importer la configuration du serveur"), + ("Import Server Config", "Importer la configuration du serveur"), + ("Export Server Config", ""), ("Import server configuration successfully", "Configuration du serveur importée avec succès"), + ("Export server configuration successfully", ""), ("Invalid server configuration", "Configuration du serveur non valide"), ("Clipboard is empty", "Presse-papier vide"), ("Stop service", "Arrêter le service"), diff --git a/src/lang/hu.rs b/src/lang/hu.rs index bbc0594a5..e26cdeff7 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -29,8 +29,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable TCP Tunneling", "TCP Tunneling bekapcsolása"), ("IP Whitelisting", "IP Fehérlista"), ("ID/Relay Server", "ID/Relay Szerver"), - ("Import Server Conf", "Szerver Konfiguráció Importálása"), + ("Import Server Config", "Szerver Konfiguráció Importálása"), + ("Export Server Config", ""), ("Import server configuration successfully", "Szerver konfiguráció sikeresen importálva"), + ("Export server configuration successfully", ""), ("Invalid server configuration", "Érvénytelen szerver konfiguráció"), ("Clipboard is empty", "A vágólap üres"), ("Stop service", "Szolgáltatás Kikapcsolása"), diff --git a/src/lang/id.rs b/src/lang/id.rs index fc1ab88c2..8486e9e64 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -29,8 +29,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable TCP Tunneling", "Aktifkan TCP Tunneling"), ("IP Whitelisting", "Daftar Putih IP"), ("ID/Relay Server", "ID/Relay Server"), - ("Import Server Conf", "Impor Konfigurasi Server"), + ("Import Server Config", "Impor Konfigurasi Server"), + ("Export Server Config", ""), ("Import server configuration successfully", "Impor konfigurasi server berhasil"), + ("Export server configuration successfully", ""), ("Invalid server configuration", "Konfigurasi server tidak valid"), ("Clipboard is empty", "Papan klip kosong"), ("Stop service", "Hentikan Layanan"), diff --git a/src/lang/it.rs b/src/lang/it.rs index bf2c76fe8..57ba210c3 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -29,8 +29,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable TCP Tunneling", "Abilita tunnel TCP"), ("IP Whitelisting", "IP autorizzati"), ("ID/Relay Server", "Server ID/Relay"), - ("Import Server Conf", "Importa configurazione Server"), + ("Import Server Config", "Importa configurazione Server"), + ("Export Server Config", ""), ("Import server configuration successfully", "Configurazione Server importata con successo"), + ("Export server configuration successfully", ""), ("Invalid server configuration", "Configurazione Server non valida"), ("Clipboard is empty", "Gli appunti sono vuoti"), ("Stop service", "Arresta servizio"), diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 38e4c0c7d..207a874ec 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -29,8 +29,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable TCP Tunneling", "TCPトンネリングを有効化"), ("IP Whitelisting", "IPホワイトリスト"), ("ID/Relay Server", "認証・中継サーバー"), - ("Import Server Conf", "サーバー設定をインポート"), + ("Import Server Config", "サーバー設定をインポート"), + ("Export Server Config", ""), ("Import server configuration successfully", "サーバー設定をインポートしました"), + ("Export server configuration successfully", ""), ("Invalid server configuration", "無効なサーバー設定です"), ("Clipboard is empty", "クリップボードは空です"), ("Stop service", "サービスを停止"), diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 740465323..5ed580ff3 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -29,8 +29,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable TCP Tunneling", "TCP 터널링 활성화"), ("IP Whitelisting", "IP 화이트리스트"), ("ID/Relay Server", "ID/Relay 서버"), - ("Import Server Conf", "서버 설정 가져오기"), + ("Import Server Config", "서버 설정 가져오기"), + ("Export Server Config", ""), ("Import server configuration successfully", "서버 설정 가져오기 성공"), + ("Export server configuration successfully", ""), ("Invalid server configuration", "잘못된 서버 설정"), ("Clipboard is empty", "클립보드가 비어있습니다"), ("Stop service", "서비스 중단"), diff --git a/src/lang/kz.rs b/src/lang/kz.rs index a4f25dbf8..ad7bdb476 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -29,8 +29,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable TCP Tunneling", "TCP тунелдеуді қосу"), ("IP Whitelisting", "IP Ақ-тізімі"), ("ID/Relay Server", "ID/Relay сербері"), - ("Import Server Conf", "Серверді импорттау"), + ("Import Server Config", "Серверді импорттау"), + ("Export Server Config", ""), ("Import server configuration successfully", "Сервердің конфигурациясы сәтті импортталды"), + ("Export server configuration successfully", ""), ("Invalid server configuration", "Жарамсыз сервердің конфигурациясы"), ("Clipboard is empty", "Көшіру-тақта бос"), ("Stop service", "Сербесті тоқтату"), diff --git a/src/lang/pl.rs b/src/lang/pl.rs index e46796148..2a08479aa 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -29,8 +29,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable TCP Tunneling", "Włącz tunelowanie TCP"), ("IP Whitelisting", "Biała lista IP"), ("ID/Relay Server", "Serwer ID/Pośredniczący"), - ("Import Server Conf", "Importuj konfigurację serwera"), + ("Import Server Config", "Importuj konfigurację serwera"), + ("Export Server Config", ""), ("Import server configuration successfully", "Importowanie konfiguracji serwera powiodło się"), + ("Export server configuration successfully", ""), ("Invalid server configuration", "Nieprawidłowa konfiguracja serwera"), ("Clipboard is empty", "Schowek jest pusty"), ("Stop service", "Zatrzymaj usługę"), diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 69b61d622..fc40cbfb8 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -29,8 +29,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable TCP Tunneling", "Activar Túnel TCP"), ("IP Whitelisting", "Whitelist de IP"), ("ID/Relay Server", "Servidor ID/Relay"), - ("Import Server Conf", "Importar Configuração do Servidor"), + ("Import Server Config", "Importar Configuração do Servidor"), + ("Export Server Config", ""), ("Import server configuration successfully", "Configuração do servidor importada com sucesso"), + ("Export server configuration successfully", ""), ("Invalid server configuration", "Configuração do servidor inválida"), ("Clipboard is empty", "A área de transferência está vazia"), ("Stop service", "Parar serviço"), diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index e93cbef33..adeee81a4 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -29,8 +29,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable TCP Tunneling", "Habilitar Tunelamento TCP"), ("IP Whitelisting", "Whitelist de IP"), ("ID/Relay Server", "Servidor ID/Relay"), - ("Import Server Conf", "Importar Configuração do Servidor"), + ("Import Server Config", "Importar Configuração do Servidor"), + ("Export Server Config", ""), ("Import server configuration successfully", "Configuração do servidor importada com sucesso"), + ("Export server configuration successfully", ""), ("Invalid server configuration", "Configuração do servidor inválida"), ("Clipboard is empty", "A área de transferência está vazia"), ("Stop service", "Parar serviço"), diff --git a/src/lang/ru.rs b/src/lang/ru.rs index eccc28602..3ddc40d53 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -29,8 +29,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable TCP Tunneling", "Включить туннелирование TCP"), ("IP Whitelisting", "Список разрешенных IP-адресов"), ("ID/Relay Server", "ID/Сервер ретрансляции"), - ("Import Server Conf", "Импортировать конфигурацию сервера"), + ("Import Server Config", "Импортировать конфигурацию сервера"), + ("Export Server Config", ""), ("Import server configuration successfully", "Конфигурация сервера успешно импортирована"), + ("Export server configuration successfully", ""), ("Invalid server configuration", "Недопустимая конфигурация сервера"), ("Clipboard is empty", "Буфер обмена пуст"), ("Stop service", "Остановить службу"), diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 20e096f0e..107f38bf8 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -29,8 +29,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable TCP Tunneling", "Povoliť TCP tunelovanie"), ("IP Whitelisting", "Zoznam povolených IP adries"), ("ID/Relay Server", "ID/Prepojovací server"), - ("Import Server Conf", "Importovať konfiguráciu servera"), + ("Import Server Config", "Importovať konfiguráciu servera"), + ("Export Server Config", ""), ("Import server configuration successfully", "Konfigurácia servera bola úspešne importovaná"), + ("Export server configuration successfully", ""), ("Invalid server configuration", "Neplatná konfigurácia servera"), ("Clipboard is empty", "Schránka je prázdna"), ("Stop service", "Zastaviť službu"), diff --git a/src/lang/template.rs b/src/lang/template.rs index 87dcc3a48..a08eec33d 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -29,8 +29,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable TCP Tunneling", ""), ("IP Whitelisting", ""), ("ID/Relay Server", ""), - ("Import Server Conf", ""), + ("Import Server Config", ""), + ("Export Server Config", ""), ("Import server configuration successfully", ""), + ("Export server configuration successfully", ""), ("Invalid server configuration", ""), ("Clipboard is empty", ""), ("Stop service", ""), diff --git a/src/lang/tr.rs b/src/lang/tr.rs index f8d8b5fff..99fad87bc 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -29,8 +29,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable TCP Tunneling", "TCP Tüneline izin ver"), ("IP Whitelisting", "İzinli IP listesi"), ("ID/Relay Server", "ID/Relay Sunucusu"), - ("Import Server Conf", "Sunucu ayarlarını içe aktar"), + ("Import Server Config", "Sunucu ayarlarını içe aktar"), + ("Export Server Config", ""), ("Import server configuration successfully", "Sunucu ayarları başarıyla içe aktarıldı"), + ("Export server configuration successfully", ""), ("Invalid server configuration", "Geçersiz sunucu ayarı"), ("Clipboard is empty", "Kopyalanan geçici veri boş"), ("Stop service", "Servisi Durdur"), diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 0c085775e..d20888aeb 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -29,8 +29,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable TCP Tunneling", "啟用 TCP 通道"), ("IP Whitelisting", "IP 白名單"), ("ID/Relay Server", "ID/轉送伺服器"), - ("Import Server Conf", "匯入伺服器設定"), + ("Import Server Config", "匯入伺服器設定"), + ("Export Server Config", "導出服務器配置"), ("Import server configuration successfully", "匯入伺服器設定成功"), + ("Export server configuration successfully", ""), + ("Export server configuration successfully", "導出服務器配置信息成功"), ("Invalid server configuration", "無效的伺服器設定"), ("Clipboard is empty", "剪貼簿是空的"), ("Stop service", "停止服務"), diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 8d3451db0..f6c389533 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -29,8 +29,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable TCP Tunneling", "Увімкнути тунелювання TCP"), ("IP Whitelisting", "Список дозволених IP-адрес"), ("ID/Relay Server", "ID/Сервер ретрансляції"), - ("Import Server Conf", "Імпортувати конфігурацію сервера"), + ("Import Server Config", "Імпортувати конфігурацію сервера"), + ("Export Server Config", ""), ("Import server configuration successfully", "Конфігурацію сервера успішно імпортовано"), + ("Export server configuration successfully", ""), ("Invalid server configuration", "Недійсна конфігурація сервера"), ("Clipboard is empty", "Буфер обміну порожній"), ("Stop service", "Зупинити службу"), diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 81428a8ec..f4823dbfd 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -29,8 +29,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable TCP Tunneling", "Cho phép TCP Tunneling"), ("IP Whitelisting", "Cho phép IP"), ("ID/Relay Server", "Máy chủ ID/Relay"), - ("Import Server Conf", "Nhập cấu hình máy chủ"), + ("Import Server Config", "Nhập cấu hình máy chủ"), + ("Export Server Config", ""), ("Import server configuration successfully", "Nhập cấu hình máy chủ thành công"), + ("Export server configuration successfully", ""), ("Invalid server configuration", "Cấu hình máy chủ không hợp lệ"), ("Clipboard is empty", "Khay nhớ tạm trống"), ("Stop service", "Dừng dịch vụ"), From 41893e2ac2a81e10d29fe704a277de74826a4367 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sat, 22 Oct 2022 16:56:21 +0800 Subject: [PATCH 0747/2015] replace cmd.exe with rustdek.exe when check uac Signed-off-by: 21pages --- src/platform/windows.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 190a49a16..278d79d92 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -1478,7 +1478,13 @@ pub fn run_uac(exe: &str, arg: &str) -> ResultType { } pub fn check_super_user_permission() -> ResultType { - run_uac("cmd", "/c /q") + run_uac( + std::env::current_exe()? + .to_string_lossy() + .to_string() + .as_str(), + "--version", + ) } pub fn elevate(arg: &str) -> ResultType { From d48a94e5309cda5124befa3bee5dfd52383454ea Mon Sep 17 00:00:00 2001 From: 21pages Date: Sat, 22 Oct 2022 21:55:36 +0800 Subject: [PATCH 0748/2015] forceUpdate when unminisized on windows Signed-off-by: 21pages --- .../lib/desktop/pages/connection_page.dart | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 30b685347..506a03b7a 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common/widgets/address_book.dart'; @@ -9,6 +10,7 @@ import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart'; import 'package:get/get.dart'; import 'package:url_launcher/url_launcher_string.dart'; +import 'package:window_manager/window_manager.dart'; import '../../common.dart'; import '../../common/formatter/id_formatter.dart'; @@ -27,7 +29,7 @@ class ConnectionPage extends StatefulWidget { /// State for the connection page. class _ConnectionPageState extends State - with SingleTickerProviderStateMixin { + with SingleTickerProviderStateMixin, WindowListener { /// Controller for the id input bar. final _idController = IDTextEditingController(); @@ -43,6 +45,8 @@ class _ConnectionPageState extends State var svcStatusCode = 0.obs; var svcIsUsingPublicServer = true.obs; + bool isWindowMinisized = false; + @override void initState() { super.initState(); @@ -63,6 +67,7 @@ class _ConnectionPageState extends State _idInputFocused.value = _idFocusNode.hasFocus; }); Get.put(svcStopped, tag: 'service-stop'); + windowManager.addListener(this); } @override @@ -70,9 +75,24 @@ class _ConnectionPageState extends State _idController.dispose(); _updateTimer?.cancel(); Get.delete(tag: 'service-stop'); + windowManager.removeListener(this); super.dispose(); } + @override + void onWindowEvent(String eventName) { + super.onWindowEvent(eventName); + if (eventName == 'minimize') { + isWindowMinisized = true; + } else if (eventName == 'maximize' || eventName == 'restore') { + if (isWindowMinisized && Platform.isWindows) { + // windows can't update when minisized. + Get.forceAppUpdate(); + } + isWindowMinisized = false; + } + } + @override Widget build(BuildContext context) { return Column( From 894fe69285725e02d1cb2536c3a4302404acb16f Mon Sep 17 00:00:00 2001 From: 21pages Date: Sun, 23 Oct 2022 19:52:30 +0800 Subject: [PATCH 0749/2015] fix flutter upgrade Signed-off-by: 21pages --- flutter/lib/desktop/pages/install_page.dart | 2 +- flutter/windows/runner/main.cpp | 21 +++++----- src/platform/windows.rs | 44 ++++++++++++--------- 3 files changed, 38 insertions(+), 29 deletions(-) diff --git a/flutter/lib/desktop/pages/install_page.dart b/flutter/lib/desktop/pages/install_page.dart index 73ad8769d..e7bb28813 100644 --- a/flutter/lib/desktop/pages/install_page.dart +++ b/flutter/lib/desktop/pages/install_page.dart @@ -181,7 +181,7 @@ class _InstallPageState extends State with WindowListener { void install() { btnEnabled.value = false; showProgress.value = true; - String args = '--flutter'; + String args = ''; if (startmenu.value) args += ' startmenu'; if (desktopicon.value) args += ' desktopicon'; bind.installInstallMe(options: args, path: controller.text); diff --git a/flutter/windows/runner/main.cpp b/flutter/windows/runner/main.cpp index 66194ed42..fed399c9a 100644 --- a/flutter/windows/runner/main.cpp +++ b/flutter/windows/runner/main.cpp @@ -16,15 +16,6 @@ typedef void (*FUNC_RUSTDESK_FREE_ARGS)( char**, int); int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, _In_ wchar_t *command_line, _In_ int show_command) { - // uni links dispatch - HWND hwnd = ::FindWindow(L"FLUTTER_RUNNER_WIN32_WINDOW", L"rustdesk"); - if (hwnd != NULL) { - DispatchToUniLinksDesktop(hwnd); - - ::ShowWindow(hwnd, SW_NORMAL); - ::SetForegroundWindow(hwnd); - return EXIT_FAILURE; - } HINSTANCE hInstance = LoadLibraryA("librustdesk.dll"); if (!hInstance) { @@ -56,6 +47,16 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, std::vector rust_args(c_args, c_args + args_len); free_c_args(c_args, args_len); + // uni links dispatch + HWND hwnd = ::FindWindow(L"FLUTTER_RUNNER_WIN32_WINDOW", L"rustdesk"); + if (hwnd != NULL) { + DispatchToUniLinksDesktop(hwnd); + + ::ShowWindow(hwnd, SW_NORMAL); + ::SetForegroundWindow(hwnd); + return EXIT_FAILURE; + } + // Attach to console when present (e.g., 'flutter run') or create a // new console when running with a debugger. if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) @@ -78,7 +79,7 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, FlutterWindow window(project); Win32Window::Point origin(10, 10); Win32Window::Size size(800, 600); - if (!window.CreateAndShow(L"rustdesk", origin, size)) + if (!window.CreateAndShow(L"RustDesk", origin, size)) { return EXIT_FAILURE; } diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 278d79d92..53fb60bd8 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -878,6 +878,25 @@ fn get_install_info_with_subkey(subkey: String) -> (String, String, String, Stri (subkey, path, start_menu, exe) } +pub fn copy_exe_cmd(src_exe: &str, _exe: &str, _path: &str) -> String { + #[cfg(feature = "flutter")] + return format!( + "XCOPY \"{}\" \"{}\" /Y /E /H /C /I /K /R /Z", + PathBuf::from(src_exe) + .parent() + .unwrap() + .to_string_lossy() + .to_string(), + _path + ); + #[cfg(not(feature = "flutter"))] + return format!( + "copy /Y \"{src_exe}\" \"{exe}\"", + src_exe = src_exe, + exe = _exe + ); +} + pub fn update_me() -> ResultType<()> { let (_, path, _, exe) = get_install_info(); let src_exe = std::env::current_exe()?.to_str().unwrap_or("").to_owned(); @@ -887,13 +906,13 @@ pub fn update_me() -> ResultType<()> { sc stop {app_name} taskkill /F /IM {broker_exe} taskkill /F /IM {app_name}.exe - copy /Y \"{src_exe}\" \"{exe}\" + {copy_exe} \"{src_exe}\" --extract \"{path}\" sc start {app_name} {lic} ", src_exe = src_exe, - exe = exe, + copy_exe = copy_exe_cmd(&src_exe, &exe, &path), broker_exe = crate::ui::win_privacy::INJECTED_PROCESS_EXE, path = path, app_name = crate::get_app_name(), @@ -1038,18 +1057,6 @@ copy /Y \"{tmp_path}\\Uninstall {app_name}.lnk\" \"{start_menu}\\\" app_name = crate::get_app_name(), ); } - let mut flutter_copy = Default::default(); - if options.contains("--flutter") { - flutter_copy = format!( - "XCOPY \"{}\" \"{}\" /Y /E /H /C /I /K /R /Z", - std::env::current_exe()? - .parent() - .unwrap() - .to_string_lossy() - .to_string(), - path - ); - } let meta = std::fs::symlink_metadata(std::env::current_exe()?)?; let size = meta.len() / 1024; @@ -1072,13 +1079,14 @@ if exist \"{tmp_path}\\{app_name} Tray.lnk\" del /f /q \"{tmp_path}\\{app_name} tmp_path = tmp_path, app_name = crate::get_app_name(), ); + let src_exe = std::env::current_exe()?.to_str().unwrap_or("").to_string(); + let cmds = format!( " {uninstall_str} chcp 65001 md \"{path}\" -{flutter_copy} -copy /Y \"{src_exe}\" \"{exe}\" +{copy_exe} copy /Y \"{ORIGIN_PROCESS_EXE}\" \"{path}\\{broker_exe}\" \"{src_exe}\" --extract \"{path}\" reg add {subkey} /f @@ -1111,7 +1119,7 @@ sc delete {app_name} ", uninstall_str=uninstall_str, path=path, - src_exe=std::env::current_exe()?.to_str().unwrap_or(""), + src_exe=src_exe, exe=exe, ORIGIN_PROCESS_EXE = crate::ui::win_privacy::ORIGIN_PROCESS_EXE, broker_exe=crate::ui::win_privacy::INJECTED_PROCESS_EXE, @@ -1140,7 +1148,7 @@ sc delete {app_name} } else { &dels }, - flutter_copy = flutter_copy, + copy_exe = copy_exe_cmd(&src_exe, &exe, &path), ); run_cmds(cmds, debug, "install")?; std::thread::sleep(std::time::Duration::from_millis(2000)); From 63add105b7740926ac6710a21f95b963fc8683b8 Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Mon, 24 Oct 2022 13:06:07 +0800 Subject: [PATCH 0750/2015] Update README.md --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 313b1e18b..640f255de 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,9 @@ RustDesk welcomes contribution from everyone. See [`docs/CONTRIBUTING.md`](docs/ [**How does RustDesk work?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F) -[**BINARY DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases) +[**NIGHTLY BUILD**](https://github.com/rustdesk/rustdesk/releases) + +https://github.com/rustdesk/rustdesk/releases/tag/nightly [Get it on F-Droid Date: Mon, 24 Oct 2022 13:07:16 +0800 Subject: [PATCH 0751/2015] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 640f255de..aa058480d 100644 --- a/README.md +++ b/README.md @@ -21,9 +21,9 @@ RustDesk welcomes contribution from everyone. See [`docs/CONTRIBUTING.md`](docs/ [**How does RustDesk work?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F) -[**NIGHTLY BUILD**](https://github.com/rustdesk/rustdesk/releases) +[**BINARY DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases) -https://github.com/rustdesk/rustdesk/releases/tag/nightly +[**NIGHTLY BUILD**](https://github.com/rustdesk/rustdesk/releases/tag/nightly) [Get it on F-Droid Date: Mon, 24 Oct 2022 15:58:24 +0800 Subject: [PATCH 0752/2015] add auto-size-text for installCard button Signed-off-by: 21pages --- .../lib/desktop/pages/desktop_home_page.dart | 3 +- flutter/lib/desktop/widgets/button.dart | 105 ++++++++++++++++-- flutter/pubspec.lock | 37 +++--- flutter/pubspec.yaml | 1 + 4 files changed, 121 insertions(+), 25 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index d22f740cb..7d2690626 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -377,7 +377,8 @@ class _DesktopHomePageState extends State fontSize: 13), ).marginOnly(bottom: 20), Row(mainAxisAlignment: MainAxisAlignment.center, children: [ - Button( + FixedWidthButton( + width: 150, padding: 8, isOutline: true, text: translate(btnText), diff --git a/flutter/lib/desktop/widgets/button.dart b/flutter/lib/desktop/widgets/button.dart index 695302a9e..0c09f7c77 100644 --- a/flutter/lib/desktop/widgets/button.dart +++ b/flutter/lib/desktop/widgets/button.dart @@ -1,18 +1,19 @@ +import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import '../../common.dart'; class Button extends StatefulWidget { - GestureTapCallback onTap; - String text; - double? textSize; - double? minWidth; - bool isOutline; - double? padding; - Color? textColor; - double? radius; - Color? borderColor; + final GestureTapCallback onTap; + final String text; + final double? textSize; + final double? minWidth; + final bool isOutline; + final double? padding; + final Color? textColor; + final double? radius; + final Color? borderColor; Button({ Key? key, @@ -82,3 +83,89 @@ class _ButtonState extends State} - {auth ? "" : } - {auth && !disconnected ? : ""} - {auth && disconnected ? : ""} + {!auth && show_elevation_btn ? : "" } + {auth ? "" : } + {auth ? "" : } + {auth && !disconnected && show_elevation_btn ? : "" } + {auth && !disconnected ? : ""} + {auth && disconnected ? : ""} {c.is_file_transfer || c.port_forward ? "" :
    {svg_chat}
    } @@ -144,6 +148,32 @@ class Body: Reactor.Component }); } + event click $(button#elevate_accept) { + var { cid, connection } = this; + checkClickTime(function() { + connection.authorized = true; + show_elevation = false; + body.update(); + handler.elevate_portable(cid); + handler.authorize(cid); + self.timer(30ms, function() { + view.windowState = View.WINDOW_MINIMIZED; + }); + }); + } + + event click $(button#elevate) { + var { cid, connection } = this; + checkClickTime(function() { + show_elevation = false; + body.update(); + handler.elevate_portable(cid); + self.timer(30ms, function() { + view.windowState = View.WINDOW_MINIMIZED; + }); + }); + } + event click $(button#dismiss) { var cid = this.cid; checkClickTime(function() { @@ -386,6 +416,13 @@ handler.newMessage = function(id, text) { update(); } +handler.showElevation = function(show) { + if (show != show_elevation) { + show_elevation = show; + update(); + } +} + view << event statechange { adjustBorder(); } diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index 72225b3fb..b1e4db7f8 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -85,6 +85,8 @@ pub trait InvokeUiCM: Send + Clone + 'static + Sized { fn change_theme(&self, dark: String); fn change_language(&self); + + fn show_elevation(&self, show: bool); } impl Deref for ConnectionManager { @@ -171,6 +173,10 @@ impl ConnectionManager { self.ui_handler.remove_connection(id, close); } + + fn show_elevation(&self, show: bool) { + self.ui_handler.show_elevation(show); + } } #[inline] @@ -362,6 +368,9 @@ impl IpcTaskRunner { LocalConfig::set_option("lang".to_owned(), lang); self.cm.change_language(); } + Data::DataPortableService(ipc::DataPortableService::CmShowElevation(show)) => { + self.cm.show_elevation(show); + } _ => { } @@ -757,3 +766,28 @@ fn cm_inner_send(id: i32, data: Data) { } } } + +pub fn can_elevate() -> bool { + #[cfg(windows)] + { + use crate::portable_service::client::{ + PortableServiceStatus::NotStarted, PORTABLE_SERVICE_STATUS, + }; + return !crate::platform::is_installed() + && PORTABLE_SERVICE_STATUS.lock().unwrap().clone() == NotStarted; + } + #[cfg(not(windows))] + return false; +} + +pub fn elevate_portable(id: i32) { + #[cfg(windows)] + { + let lock = CLIENTS.read().unwrap(); + if let Some(s) = lock.get(&id) { + allow_err!(s.tx.send(ipc::Data::DataPortableService( + ipc::DataPortableService::RequestStart + ))); + } + } +} diff --git a/src/ui_interface.rs b/src/ui_interface.rs index a334fb6fb..0e443ad61 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -10,8 +10,7 @@ use hbb_common::password_security; use hbb_common::{ allow_err, config::{self, Config, LocalConfig, PeerConfig}, - directories_next, log, - sleep, + directories_next, log, sleep, tokio::{self, sync::mpsc, time}, }; @@ -376,7 +375,7 @@ pub fn is_installed() -> bool { #[cfg(any(target_os = "android", target_os = "ios"))] #[inline] -pub fn is_installed() -> bool { +pub fn is_installed() -> bool { false } From e186eec5df9b238def11dcf81af06cf26ae6dc55 Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 10 Nov 2022 17:06:27 +0800 Subject: [PATCH 0933/2015] portable service: optimize flutter cm buttons Signed-off-by: 21pages --- flutter/lib/desktop/pages/server_page.dart | 264 +++++++++------------ 1 file changed, 112 insertions(+), 152 deletions(-) diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index aac6ee017..16abe0b64 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -76,7 +76,6 @@ class _DesktopServerPageState extends State mainAxisAlignment: MainAxisAlignment.start, children: [ Expanded(child: ConnectionManager()), - SizedBox.fromSize(size: Size(0, 15.0)), ], ), ), @@ -486,6 +485,8 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> { } } +const double bigMargin = 15; + class _CmControlPanel extends StatelessWidget { final Client client; @@ -503,180 +504,139 @@ class _CmControlPanel extends StatelessWidget { buildAuthorized(BuildContext context) { final bool canElevate = bind.cmCanElevate(); final model = Provider.of(context); - final offstage = !(canElevate && model.showElevation); - final width = offstage ? 200.0 : 100.0; - return Row( - mainAxisAlignment: MainAxisAlignment.center, + final showElevation = canElevate && model.showElevation; + return Column( + mainAxisAlignment: MainAxisAlignment.end, children: [ Offstage( - offstage: offstage, - child: Ink( - width: width, - height: 40, - decoration: BoxDecoration( - color: Colors.green[700], - borderRadius: BorderRadius.circular(10)), - child: InkWell( - onTap: () => - checkClickTime(client.id, () => handleElevate(context)), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.security_sharp, - color: Colors.white, - ), - Text( - translate("Elevate"), - style: TextStyle(color: Colors.white), - ), - ], - )), - ), + offstage: !showElevation, + child: buildButton(context, color: Colors.green[700], onClick: () { + handleElevate(context); + windowManager.minimize(); + }, + icon: Icon( + Icons.security_sharp, + color: Colors.white, + ), + text: 'Elevate', + textColor: Colors.white), ), - Offstage( - offstage: offstage, - child: SizedBox( - width: 30, - )), - Ink( - width: width, - height: 40, - decoration: BoxDecoration( - color: Colors.redAccent, borderRadius: BorderRadius.circular(10)), - child: InkWell( - onTap: () => - checkClickTime(client.id, () => handleDisconnect(context)), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - translate("Disconnect"), - style: TextStyle(color: Colors.white), - ), - ], - )), + Row( + children: [ + Expanded( + child: buildButton(context, + color: Colors.redAccent, + onClick: handleDisconnect, + text: 'Disconnect', + textColor: Colors.white)), + ], ) ], - ); + ) + .marginOnly(bottom: showElevation ? 0 : bigMargin) + .marginSymmetric(horizontal: showElevation ? 0 : bigMargin); } buildDisconnected(BuildContext context) { return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Ink( - width: 200, - height: 40, - decoration: BoxDecoration( - color: MyTheme.accent, borderRadius: BorderRadius.circular(10)), - child: InkWell( - onTap: () => handleClose(context), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - translate("Close"), - style: TextStyle(color: Colors.white), - ), - ], - )), - ) + Expanded( + child: buildButton(context, + color: MyTheme.accent, + onClick: handleClose, + text: 'Close', + textColor: Colors.white)), ], - ); + ).marginOnly(bottom: 15).marginSymmetric(horizontal: bigMargin); } buildUnAuthorized(BuildContext context) { final bool canElevate = bind.cmCanElevate(); final model = Provider.of(context); - final offstage = !(canElevate && model.showElevation); - final width = offstage ? 100.0 : 85.0; - final spacerWidth = offstage ? 30.0 : 5.0; - return Row( - mainAxisAlignment: MainAxisAlignment.center, + final showElevation = canElevate && model.showElevation; + return Column( + mainAxisAlignment: MainAxisAlignment.end, children: [ Offstage( - offstage: offstage, - child: Ink( - height: 40, - width: width, - decoration: BoxDecoration( - color: Colors.green[700], - borderRadius: BorderRadius.circular(10)), - child: InkWell( - onTap: () => checkClickTime(client.id, () { - handleAccept(context); - handleElevate(context); - windowManager.minimize(); - }), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.security_sharp, - color: Colors.white, - ), - Text( - translate("Accept"), - style: TextStyle(color: Colors.white), - ), - ], - )), - ), + offstage: !showElevation, + child: buildButton(context, color: Colors.green[700], onClick: () { + handleAccept(context); + handleElevate(context); + windowManager.minimize(); + }, + text: 'Accept', + icon: Icon( + Icons.security_sharp, + color: Colors.white, + ), + textColor: Colors.white), ), - Offstage( - offstage: offstage, - child: SizedBox( - width: spacerWidth, - )), - Ink( - width: width, - height: 40, - decoration: BoxDecoration( - color: MyTheme.accent, borderRadius: BorderRadius.circular(10)), - child: InkWell( - onTap: () => checkClickTime(client.id, () { - handleAccept(context); - windowManager.minimize(); - }), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - translate("Accept"), - style: TextStyle(color: Colors.white), - ), - ], - )), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: buildButton(context, color: MyTheme.accent, onClick: () { + handleAccept(context); + windowManager.minimize(); + }, text: 'Accept', textColor: Colors.white)), + Expanded( + child: buildButton(context, + color: Colors.transparent, + border: Border.all(color: Colors.grey), + onClick: handleDisconnect, + text: 'Cancel', + textColor: null)), + ], ), - SizedBox( - width: spacerWidth, - ), - Ink( - width: width, - height: 40, - decoration: BoxDecoration( - color: Colors.transparent, - borderRadius: BorderRadius.circular(10), - border: Border.all(color: Colors.grey)), - child: InkWell( - onTap: () => - checkClickTime(client.id, () => handleDisconnect(context)), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - translate("Cancel"), - style: TextStyle(), - ), - ], - )), - ) ], - ); + ) + .marginOnly(bottom: showElevation ? 0 : bigMargin) + .marginSymmetric(horizontal: showElevation ? 0 : bigMargin); } - void handleDisconnect(BuildContext context) { + buildButton( + BuildContext context, { + required Color? color, + required Function() onClick, + Icon? icon, + BoxBorder? border, + required String text, + required Color? textColor, + }) { + Widget textWidget; + if (icon != null) { + textWidget = Text( + translate(text), + style: TextStyle(color: textColor), + textAlign: TextAlign.center, + ); + } else { + textWidget = Expanded( + child: Text( + translate(text), + style: TextStyle(color: textColor), + textAlign: TextAlign.center, + ), + ); + } + return Container( + height: 35, + decoration: BoxDecoration( + color: color, borderRadius: BorderRadius.circular(4), border: border), + child: InkWell( + onTap: () => checkClickTime(client.id, onClick), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Offstage(offstage: icon == null, child: icon), + textWidget, + ], + )), + ).marginAll(4); + } + + void handleDisconnect() { bind.cmCloseConnection(connId: client.id); } @@ -691,7 +651,7 @@ class _CmControlPanel extends StatelessWidget { bind.cmElevatePortable(connId: client.id); } - void handleClose(BuildContext context) async { + void handleClose() async { await bind.cmRemoveDisconnectedConnection(connId: client.id); if (await bind.cmGetClientsLength() == 0) { windowManager.close(); From 9f73b89f217f20924a4767f822fb0c0e11973dd3 Mon Sep 17 00:00:00 2001 From: 21pages Date: Fri, 11 Nov 2022 11:40:23 +0800 Subject: [PATCH 0934/2015] portable-service: exchange ipc server/client Signed-off-by: 21pages --- src/server/connection.rs | 48 ++-- src/server/portable_service.rs | 426 +++++++++++++++------------------ src/server/video_service.rs | 38 +-- src/ui_cm_interface.rs | 8 +- 4 files changed, 239 insertions(+), 281 deletions(-) diff --git a/src/server/connection.rs b/src/server/connection.rs index c77f6ebd7..8674c6d9d 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -429,31 +429,31 @@ impl Connection { _ = second_timer.tick() => { #[cfg(windows)] { - use crate::portable_service::client::{PORTABLE_SERVICE_STATUS, PortableServiceStatus::*}; - let uac = crate::video_service::IS_UAC_RUNNING.lock().unwrap().clone(); - if last_uac != uac { - last_uac = uac; - if PORTABLE_SERVICE_STATUS.lock().unwrap().clone() == NotStarted { - let mut misc = Misc::new(); - misc.set_uac(uac); - let mut msg = Message::new(); - msg.set_misc(misc); - conn.inner.send(msg.into()); - } - } - let foreground_window_elevated = crate::video_service::IS_FOREGROUND_WINDOW_ELEVATED.lock().unwrap().clone(); - if last_foreground_window_elevated != foreground_window_elevated { - last_foreground_window_elevated = foreground_window_elevated; - if PORTABLE_SERVICE_STATUS.lock().unwrap().clone() == NotStarted { - let mut misc = Misc::new(); - misc.set_foreground_window_elevated(foreground_window_elevated); - let mut msg = Message::new(); - msg.set_misc(misc); - conn.inner.send(msg.into()); - } - } if !is_installed { - let show_elevation = PORTABLE_SERVICE_STATUS.lock().unwrap().clone() == NotStarted; + let portable_service_running = crate::portable_service::client::PORTABLE_SERVICE_RUNNING.lock().unwrap().clone(); + let uac = crate::video_service::IS_UAC_RUNNING.lock().unwrap().clone(); + if last_uac != uac { + last_uac = uac; + if !portable_service_running { + let mut misc = Misc::new(); + misc.set_uac(uac); + let mut msg = Message::new(); + msg.set_misc(misc); + conn.inner.send(msg.into()); + } + } + let foreground_window_elevated = crate::video_service::IS_FOREGROUND_WINDOW_ELEVATED.lock().unwrap().clone(); + if last_foreground_window_elevated != foreground_window_elevated { + last_foreground_window_elevated = foreground_window_elevated; + if !portable_service_running { + let mut misc = Misc::new(); + misc.set_foreground_window_elevated(foreground_window_elevated); + let mut msg = Message::new(); + msg.set_misc(misc); + conn.inner.send(msg.into()); + } + } + let show_elevation = !portable_service_running; conn.send_to_cm(ipc::Data::DataPortableService(ipc::DataPortableService::CmShowElevation(show_elevation))); } diff --git a/src/server/portable_service.rs b/src/server/portable_service.rs index 1de2a1c8b..a666b56d5 100644 --- a/src/server/portable_service.rs +++ b/src/server/portable_service.rs @@ -7,7 +7,6 @@ use hbb_common::{ log, message_proto::{KeyEvent, MouseEvent}, protobuf::Message, - sleep, tokio::{self, sync::mpsc}, ResultType, }; @@ -17,7 +16,7 @@ use std::{ mem::size_of, ops::{Deref, DerefMut}, sync::{Arc, Mutex}, - time::Duration, + time::{Duration, Instant}, }; use winapi::{ shared::minwindef::{BOOL, FALSE, TRUE}, @@ -48,18 +47,6 @@ const ADDR_CAPTURE_FRAME: usize = const IPC_PROFIX: &str = "_portable_service"; pub const SHMEM_NAME: &str = "_portable_service"; const MAX_NACK: usize = 3; -const IPC_CONN_TIMEOUT: Duration = Duration::from_secs(3); - -pub enum PortableServiceStatus { - NonStart, - Running, -} - -impl Default for PortableServiceStatus { - fn default() -> Self { - Self::NonStart - } -} pub struct SharedMemory { inner: Shmem, @@ -200,8 +187,6 @@ mod utils { // functions called in seperate SYSTEM user process. pub mod server { - use hbb_common::tokio::time::Instant; - use super::*; lazy_static::lazy_static! { @@ -220,7 +205,7 @@ pub mod server { run_capture(shmem2); })); threads.push(std::thread::spawn(|| { - run_ipc_server(); + run_ipc_client(); })); threads.push(std::thread::spawn(|| { run_exit_check(); @@ -270,7 +255,7 @@ pub mod server { if EXIT.lock().unwrap().clone() { break; } - let start = std::time::Instant::now(); + let start = Instant::now(); unsafe { let para_ptr = shmem.as_ptr().add(ADDR_CAPTURER_PARA); let para = para_ptr as *const CapturerPara; @@ -278,7 +263,6 @@ pub mod server { let use_yuv = (*para).use_yuv; let timeout_ms = (*para).timeout_ms; if c.is_none() { - let use_yuv = true; *crate::video_service::CURRENT_DISPLAY.lock().unwrap() = current_display; let (_, _current, display) = get_current_display().unwrap(); match Capturer::new(display, use_yuv) { @@ -348,114 +332,78 @@ pub mod server { } #[tokio::main(flavor = "current_thread")] - async fn run_ipc_server() { + async fn run_ipc_client() { use DataPortableService::*; let postfix = IPC_PROFIX; - let last_recv_time = Arc::new(Mutex::new(Instant::now())); - let mut interval = tokio::time::interval(Duration::from_secs(1)); - match new_listener(postfix).await { - Ok(mut incoming) => loop { - tokio::select! { - Some(result) = incoming.next() => { - match result { - Ok(stream) => { - log::info!("Got new connection"); - let last_recv_time_cloned = last_recv_time.clone(); - tokio::spawn(async move { - let mut stream = Connection::new(stream); - let postfix = postfix.to_owned(); - let mut timer = tokio::time::interval(Duration::from_secs(1)); - let mut nack = 0; - let mut old_conn_count = 0; - loop { - tokio::select! { - res = stream.next() => { - if res.is_ok() { - *last_recv_time_cloned.lock().unwrap() = Instant::now(); - } - match res { - Err(err) => { - log::error!( - "ipc{} connection closed: {}", - postfix, - err - ); - *EXIT.lock().unwrap() = true; - break; - } - Ok(Some(Data::DataPortableService(data))) => match data { - Ping => { - allow_err!( - stream - .send(&Data::DataPortableService(Pong)) - .await - ); - } - Pong => { - nack = 0; - } - ConnCount(Some(n)) => { - if old_conn_count != 0 && n == 0 { - log::info!("Connection count decrease to 0, exit"); - stream.send(&Data::DataPortableService(WillClose)).await.ok(); - *EXIT.lock().unwrap() = true; - break; - } - old_conn_count = n; - } - Mouse(v) => { - if let Ok(evt) = MouseEvent::parse_from_bytes(&v) { - crate::input_service::handle_mouse_(&evt); - } - } - Key(v) => { - if let Ok(evt) = KeyEvent::parse_from_bytes(&v) { - crate::input_service::handle_key_(&evt); - } - } - _ => {} - }, - _ => {} - } - } - _ = timer.tick() => { - nack+=1; - if nack > MAX_NACK { - log::info!("max ping nack, exit"); - *EXIT.lock().unwrap() = true; - break; - } - stream.send(&Data::DataPortableService(Ping)).await.ok(); - stream.send(&Data::DataPortableService(ConnCount(None))).await.ok(); - } + match ipc::connect(1000, postfix).await { + Ok(mut stream) => { + let mut timer = tokio::time::interval(Duration::from_secs(1)); + let mut nack = 0; + loop { + tokio::select! { + res = stream.next() => { + match res { + Err(err) => { + log::error!( + "ipc{} connection closed: {}", + postfix, + err + ); + break; + } + Ok(Some(Data::DataPortableService(data))) => match data { + Ping => { + allow_err!( + stream + .send(&Data::DataPortableService(Pong)) + .await + ); + } + Pong => { + nack = 0; + } + ConnCount(Some(n)) => { + if n == 0 { + log::info!("Connnection count equals 0, exit"); + stream.send(&Data::DataPortableService(WillClose)).await.ok(); + break; } } - }); - } - Err(err) => { - log::error!("Couldn't get portable client: {:?}", err); - *EXIT.lock().unwrap() = true; + Mouse(v) => { + if let Ok(evt) = MouseEvent::parse_from_bytes(&v) { + crate::input_service::handle_mouse_(&evt); + } + } + Key(v) => { + if let Ok(evt) = KeyEvent::parse_from_bytes(&v) { + crate::input_service::handle_key_(&evt); + } + } + _ => {} + }, + _ => {} } } - } - _ = interval.tick() => { - if last_recv_time.lock().unwrap().elapsed() > IPC_CONN_TIMEOUT { - log::error!("receive data timeout"); - *EXIT.lock().unwrap() = true; - } - if EXIT.lock().unwrap().clone() { - break; + _ = timer.tick() => { + nack+=1; + if nack > MAX_NACK { + log::info!("max ping nack, exit"); + break; + } + stream.send(&Data::DataPortableService(Ping)).await.ok(); + stream.send(&Data::DataPortableService(ConnCount(None))).await.ok(); } } } - }, - Err(err) => { - log::error!("Failed to start cm ipc server: {}", err); - *EXIT.lock().unwrap() = true; + } + Err(e) => { + log::error!("Failed to connect portable service ipc:{:?}", e); } } + + *EXIT.lock().unwrap() = true; } } @@ -466,54 +414,46 @@ pub mod client { use super::*; lazy_static::lazy_static! { - pub static ref SHMEM: Arc>> = Default::default(); - pub static ref PORTABLE_SERVICE_STATUS: Arc> = Default::default(); - static ref SENDER : Mutex> = Mutex::new(client::start_ipc_client()); - } - - #[derive(Debug, Clone, Copy, PartialEq, Eq)] - pub enum PortableServiceStatus { - NotStarted, - Starting, - Running, - } - - impl Default for PortableServiceStatus { - fn default() -> Self { - Self::NotStarted - } + pub static ref PORTABLE_SERVICE_RUNNING: Arc> = Default::default(); + static ref SHMEM: Arc>> = Default::default(); + static ref SENDER : Mutex> = Mutex::new(client::start_ipc_server()); } pub(crate) fn start_portable_service() -> ResultType<()> { - if PORTABLE_SERVICE_STATUS.lock().unwrap().clone() == PortableServiceStatus::NotStarted { - if SHMEM.lock().unwrap().is_none() { - let displays = scrap::Display::all()?; - if displays.is_empty() { - bail!("no display available!"); - } - let mut max_pixel = 0; - let align = 64; - for d in displays { - let pixel = utils::align(d.width(), align) * utils::align(d.height(), align); - if max_pixel < pixel { - max_pixel = pixel; - } - } - let shmem_size = utils::align(ADDR_CAPTURE_FRAME + max_pixel * 4, align); - // os error 112, no enough space - *SHMEM.lock().unwrap() = Some(crate::portable_service::SharedMemory::create( - crate::portable_service::SHMEM_NAME, - shmem_size, - )?); - shutdown_hooks::add_shutdown_hook(drop_shmem); - } - if crate::common::run_me(vec!["--portable-service"]).is_err() { - *SHMEM.lock().unwrap() = None; - bail!("Failed to run portable service process"); - } - *PORTABLE_SERVICE_STATUS.lock().unwrap() = PortableServiceStatus::Starting; - let _sender = SENDER.lock().unwrap(); + if PORTABLE_SERVICE_RUNNING.lock().unwrap().clone() { + bail!("already running"); } + if SHMEM.lock().unwrap().is_none() { + let displays = scrap::Display::all()?; + if displays.is_empty() { + bail!("no display available!"); + } + let mut max_pixel = 0; + let align = 64; + for d in displays { + let pixel = utils::align(d.width(), align) * utils::align(d.height(), align); + if max_pixel < pixel { + max_pixel = pixel; + } + } + let shmem_size = utils::align(ADDR_CAPTURE_FRAME + max_pixel * 4, align); + // os error 112, no enough space + *SHMEM.lock().unwrap() = Some(crate::portable_service::SharedMemory::create( + crate::portable_service::SHMEM_NAME, + shmem_size, + )?); + shutdown_hooks::add_shutdown_hook(drop_shmem); + } + let mut option = SHMEM.lock().unwrap(); + let shmem = option.as_mut().unwrap(); + unsafe { + libc::memset(shmem.as_ptr() as _, 0, shmem.len() as _); + } + if crate::common::run_me(vec!["--portable-service"]).is_err() { + *SHMEM.lock().unwrap() = None; + bail!("Failed to run portable service process"); + } + let _sender = SENDER.lock().unwrap(); Ok(()) } @@ -613,94 +553,98 @@ pub mod client { } } - pub(super) fn start_ipc_client() -> mpsc::UnboundedSender { + pub(super) fn start_ipc_server() -> mpsc::UnboundedSender { let (tx, rx) = mpsc::unbounded_channel::(); - std::thread::spawn(move || start_ipc_client_async(rx)); + std::thread::spawn(move || start_ipc_server_async(rx)); tx } #[tokio::main(flavor = "current_thread")] - async fn start_ipc_client_async(rx: mpsc::UnboundedReceiver) { + async fn start_ipc_server_async(rx: mpsc::UnboundedReceiver) { use DataPortableService::*; - let mut rx = rx; - let mut connect_failed = 0; - loop { - if PORTABLE_SERVICE_STATUS.lock().unwrap().clone() == PortableServiceStatus::NotStarted - { - sleep(1.).await; - continue; - } - if let Ok(mut c) = ipc::connect(1000, IPC_PROFIX).await { - let mut nack = 0; - let mut timer = tokio::time::interval(Duration::from_secs(1)); - loop { - tokio::select! { - res = c.next() => { - match res { - Err(err) => { - log::error!("ipc connection closed: {}", err); - break; - } - Ok(Some(Data::DataPortableService(data))) => { - match data { - Ping => { - c.send(&Data::DataPortableService(Pong)).await.ok(); - } - Pong => { - nack = 0; - *PORTABLE_SERVICE_STATUS.lock().unwrap() = PortableServiceStatus::Running; - }, - ConnCount(None) => { - let cnt = crate::server::CONN_COUNT.lock().unwrap().clone(); - c.send(&Data::DataPortableService(ConnCount(Some(cnt)))).await.ok(); - }, - WillClose => { - log::info!("portable service will close, set status to not started"); - *PORTABLE_SERVICE_STATUS.lock().unwrap() = PortableServiceStatus::NotStarted; - break; - } - _=>{} - } - } - _ => {} - } - } - _ = timer.tick() => { - nack+=1; - if nack > MAX_NACK { - // In fact, this will not happen, ipc will be closed before max nack. - log::error!("max ipc nack, set status to not started"); - *PORTABLE_SERVICE_STATUS.lock().unwrap() = PortableServiceStatus::NotStarted; - break; - } - c.send(&Data::DataPortableService(Ping)).await.ok(); - } - Some(data) = rx.recv() => { - allow_err!(c.send(&data).await); - } + let rx = Arc::new(tokio::sync::Mutex::new(rx)); + let postfix = IPC_PROFIX; + match new_listener(postfix).await { + Ok(mut incoming) => loop { + { + tokio::select! { + Some(result) = incoming.next() => { + match result { + Ok(stream) => { + log::info!("Got portable service ipc connection"); + let rx_clone = rx.clone(); + tokio::spawn(async move { + let mut stream = Connection::new(stream); + let postfix = postfix.to_owned(); + let mut timer = tokio::time::interval(Duration::from_secs(1)); + let mut nack = 0; + let mut rx = rx_clone.lock().await; + loop { + tokio::select! { + res = stream.next() => { + match res { + Err(err) => { + log::info!( + "ipc{} connection closed: {}", + postfix, + err + ); + break; + } + Ok(Some(Data::DataPortableService(data))) => match data { + Ping => { + stream.send(&Data::DataPortableService(Pong)).await.ok(); + } + Pong => { + nack = 0; + *PORTABLE_SERVICE_RUNNING.lock().unwrap() = true; + }, + ConnCount(None) => { + let cnt = crate::server::CONN_COUNT.lock().unwrap().clone(); + stream.send(&Data::DataPortableService(ConnCount(Some(cnt)))).await.ok(); + }, + WillClose => { + log::info!("portable service will close"); + break; + } + _=>{} + } + _=>{} + } + } + _ = timer.tick() => { + nack+=1; + if nack > MAX_NACK { + // In fact, this will not happen, ipc will be closed before max nack. + log::error!("max ipc nack"); + break; + } + stream.send(&Data::DataPortableService(Ping)).await.ok(); + } + Some(data) = rx.recv() => { + allow_err!(stream.send(&data).await); + } + } + } + *PORTABLE_SERVICE_RUNNING.lock().unwrap() = false; + }); + } + Err(err) => { + log::error!("Couldn't get portable client: {:?}", err); + } + } + } } } - } else { - connect_failed += 1; - if connect_failed > IPC_CONN_TIMEOUT.as_secs() { - connect_failed = 0; - *PORTABLE_SERVICE_STATUS.lock().unwrap() = PortableServiceStatus::NotStarted; - log::info!( - "connect failed {} times, set status to not started", - connect_failed - ); - } - log::info!( - "client ip connect failed, status:{:?}", - PORTABLE_SERVICE_STATUS.lock().unwrap().clone(), - ); + }, + Err(err) => { + log::error!("Failed to start portable service ipc server: {}", err); } - sleep(1.).await; } } - fn client_ipc_send(data: Data) -> ResultType<()> { + fn ipc_send(data: Data) -> ResultType<()> { let sender = SENDER.lock().unwrap(); sender .send(data) @@ -721,21 +665,25 @@ pub mod client { fn handle_mouse_(evt: &MouseEvent) -> ResultType<()> { let mut v = vec![]; evt.write_to_vec(&mut v)?; - client_ipc_send(Data::DataPortableService(DataPortableService::Mouse(v))) + ipc_send(Data::DataPortableService(DataPortableService::Mouse(v))) } fn handle_key_(evt: &KeyEvent) -> ResultType<()> { let mut v = vec![]; evt.write_to_vec(&mut v)?; - client_ipc_send(Data::DataPortableService(DataPortableService::Key(v))) + ipc_send(Data::DataPortableService(DataPortableService::Key(v))) } pub fn create_capturer( current_display: usize, display: scrap::Display, use_yuv: bool, + portable_service_running: bool, ) -> ResultType> { - if PORTABLE_SERVICE_STATUS.lock().unwrap().clone() == PortableServiceStatus::Running { + if portable_service_running != PORTABLE_SERVICE_RUNNING.lock().unwrap().clone() { + log::info!("portable service status mismatch"); + } + if portable_service_running { log::info!("Create shared memeory capturer"); return Ok(Box::new(CapturerPortable::new(current_display, use_yuv))); } else { @@ -747,7 +695,7 @@ pub mod client { } pub fn get_cursor_info(pci: PCURSORINFO) -> BOOL { - if PORTABLE_SERVICE_STATUS.lock().unwrap().clone() == PortableServiceStatus::Running { + if PORTABLE_SERVICE_RUNNING.lock().unwrap().clone() { get_cursor_info_(&mut SHMEM.lock().unwrap().as_mut().unwrap(), pci) } else { unsafe { winuser::GetCursorInfo(pci) } @@ -755,7 +703,7 @@ pub mod client { } pub fn handle_mouse(evt: &MouseEvent) { - if PORTABLE_SERVICE_STATUS.lock().unwrap().clone() == PortableServiceStatus::Running { + if PORTABLE_SERVICE_RUNNING.lock().unwrap().clone() { handle_mouse_(evt).ok(); } else { crate::input_service::handle_mouse_(evt); @@ -763,7 +711,7 @@ pub mod client { } pub fn handle_key(evt: &KeyEvent) { - if PORTABLE_SERVICE_STATUS.lock().unwrap().clone() == PortableServiceStatus::Running { + if PORTABLE_SERVICE_RUNNING.lock().unwrap().clone() { handle_key_(evt).ok(); } else { crate::input_service::handle_key_(evt); diff --git a/src/server/video_service.rs b/src/server/video_service.rs index f48fefeec..43ce013f5 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -20,7 +20,7 @@ use super::{video_qos::VideoQoS, *}; #[cfg(windows)] -use crate::portable_service::client::{PortableServiceStatus, PORTABLE_SERVICE_STATUS}; +use crate::portable_service::client::PORTABLE_SERVICE_RUNNING; use hbb_common::tokio::sync::{ mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}, Mutex as TokioMutex, @@ -191,6 +191,7 @@ fn create_capturer( display: Display, use_yuv: bool, current: usize, + _portable_service_running: bool, ) -> ResultType> { #[cfg(not(windows))] let c: Option> = None; @@ -252,7 +253,12 @@ fn create_capturer( None => { log::debug!("Create capturer dxgi|gdi"); #[cfg(windows)] - return crate::portable_service::client::create_capturer(current, display, use_yuv); + return crate::portable_service::client::create_capturer( + current, + display, + use_yuv, + _portable_service_running, + ); #[cfg(not(windows))] return Ok(Box::new( Capturer::new(display, use_yuv).with_context(|| "Failed to create capturer")?, @@ -282,7 +288,7 @@ pub fn test_create_capturer(privacy_mode_id: i32, timeout_millis: u64) -> bool { let test_begin = Instant::now(); while test_begin.elapsed().as_millis() < timeout_millis as _ { if let Ok((_, current, display)) = get_current_display() { - if let Ok(_) = create_capturer(privacy_mode_id, display, true, current) { + if let Ok(_) = create_capturer(privacy_mode_id, display, true, current, false) { return true; } } @@ -331,7 +337,7 @@ impl DerefMut for CapturerInfo { } } -fn get_capturer(use_yuv: bool) -> ResultType { +fn get_capturer(use_yuv: bool, portable_service_running: bool) -> ResultType { #[cfg(target_os = "linux")] { if !scrap::is_x11() { @@ -373,7 +379,13 @@ fn get_capturer(use_yuv: bool) -> ResultType { } else { log::info!("In privacy mode, the peer side cannot watch the screen"); } - let capturer = create_capturer(captuerer_privacy_mode_id, display, use_yuv, current)?; + let capturer = create_capturer( + captuerer_privacy_mode_id, + display, + use_yuv, + current, + portable_service_running, + )?; Ok(CapturerInfo { origin, width, @@ -393,8 +405,12 @@ fn run(sp: GenericService) -> ResultType<()> { // ensure_inited() is needed because release_resouce() may be called. #[cfg(target_os = "linux")] super::wayland::ensure_inited()?; + #[cfg(windows)] + let last_portable_service_running = PORTABLE_SERVICE_RUNNING.lock().unwrap().clone(); + #[cfg(not(windows))] + let last_portable_service_running = false; - let mut c = get_capturer(true)?; + let mut c = get_capturer(true, last_portable_service_running)?; let mut video_qos = VIDEO_QOS.lock().unwrap(); video_qos.set_size(c.width as _, c.height as _); @@ -472,11 +488,6 @@ fn run(sp: GenericService) -> ResultType<()> { let recorder: Arc>> = Default::default(); #[cfg(windows)] start_uac_elevation_check(); - #[cfg(windows)] - let portable_service_status = crate::portable_service::client::PORTABLE_SERVICE_STATUS - .lock() - .unwrap() - .clone(); #[cfg(target_os = "linux")] let mut would_block_count = 0u32; @@ -508,15 +519,14 @@ fn run(sp: GenericService) -> ResultType<()> { bail!("SWITCH"); } #[cfg(windows)] - if portable_service_status != PORTABLE_SERVICE_STATUS.lock().unwrap().clone() { + if last_portable_service_running != PORTABLE_SERVICE_RUNNING.lock().unwrap().clone() { bail!("SWITCH"); } check_privacy_mode_changed(&sp, c.privacy_mode_id)?; #[cfg(windows)] { if crate::platform::windows::desktop_changed() - && PORTABLE_SERVICE_STATUS.lock().unwrap().clone() - == PortableServiceStatus::NotStarted + && !PORTABLE_SERVICE_RUNNING.lock().unwrap().clone() { bail!("Desktop changed"); } diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index b1e4db7f8..26b26bf92 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -770,11 +770,11 @@ fn cm_inner_send(id: i32, data: Data) { pub fn can_elevate() -> bool { #[cfg(windows)] { - use crate::portable_service::client::{ - PortableServiceStatus::NotStarted, PORTABLE_SERVICE_STATUS, - }; return !crate::platform::is_installed() - && PORTABLE_SERVICE_STATUS.lock().unwrap().clone() == NotStarted; + && !crate::portable_service::client::PORTABLE_SERVICE_RUNNING + .lock() + .unwrap() + .clone(); } #[cfg(not(windows))] return false; From bee19bfe176c7032d59248182e1d7767037f7446 Mon Sep 17 00:00:00 2001 From: 21pages Date: Fri, 11 Nov 2022 20:44:16 +0800 Subject: [PATCH 0935/2015] portable-service: optimize sciter cm buttons Signed-off-by: 21pages --- src/ui/cm.css | 42 +++++++++++++++++++++++++++++++++--------- src/ui/cm.tis | 18 +++++++++++------- 2 files changed, 44 insertions(+), 16 deletions(-) diff --git a/src/ui/cm.css b/src/ui/cm.css index fccdb155f..ff4d422e4 100644 --- a/src/ui/cm.css +++ b/src/ui/cm.css @@ -112,20 +112,44 @@ icon.recording { background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAANpJREFUWEftltENAiEMhtsJ1NcynG6gI+gGugEOR591gppeQoIYSDBILxEeydH/57u2FMF4obE+TAOTwLoIhBDOAHBExG2n6rgR0akW640AM0sn4SWMiDycc7s8JjN7Ijro/k8NqAAR5RoeAPZxv2ggP9hCJiWZxtGbq3hqbJiBVHy4gVx8qAER8Yi4JFy6huVAKXemgb8icI+1b5KEitq0DOO/Nm1EEX1TK27p/bVvv36MOhl4EtHHbFF7jq8AoG1z08OAiFycczrkFNe6RrIet26NMQlMAuYEXiayryF/QQktAAAAAElFTkSuQmCC'); } -div.buttons { - width: *; - border-spacing: 0.5em; - text-align: center; +div.outer_buttons { + flow:vertical; + border-spacing:8; } -div.buttons button { - width: 80px; - height: 40px; - margin: 0.5em; +div.inner_buttons { + flow:horizontal; + border-spacing:8; +} + +button.control { + width: *; +} + +button.elevate { + background:green; +} + +button.elevate:active { + background: rgb(2, 104, 2); + border-color: color(hover-border); +} + +button.elevate>span { + flow:horizontal; + width: *; +} + +button.elevate>span>span { + margin-left:*; + margin-right:*; +} + +button.elevate>span>span>span { + vertical-align: middle; } button#disconnect { - width: 160px; background: color(blood-red); border: none; } diff --git a/src/ui/cm.tis b/src/ui/cm.tis index 5238ab91a..035b58650 100644 --- a/src/ui/cm.tis +++ b/src/ui/cm.tis @@ -4,6 +4,7 @@ var body; var connections = []; var show_chat = false; var show_elevation = true; +var svg_elevate = ; class Body: Reactor.Component { @@ -56,14 +57,17 @@ class Body: Reactor.Component } {c.port_forward ?
    Port Forwarding: {c.port_forward}
    : ""}
    -
    - {!auth && show_elevation_btn ? : "" } - {auth ? "" : } - {auth ? "" : } - {auth && !disconnected && show_elevation_btn ? : "" } - {auth && !disconnected ? : ""} - {auth && disconnected ? : ""} +
    + {!auth && !disconnected && show_elevation_btn ? : "" } + {auth && !disconnected && show_elevation_btn ? : "" } +
    + {!auth ? : "" } + {!auth ? : "" } +
    + {auth && !disconnected ? : "" } + {auth && disconnected ? : "" }
    +
    {c.is_file_transfer || c.port_forward ? "" :
    {svg_chat}
    }
    From 4d492cb2c6bf83f904625a9c87de99115b23f905 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sat, 12 Nov 2022 17:18:26 +0800 Subject: [PATCH 0936/2015] portable-service: fix set capture para dead lock Signed-off-by: 21pages --- src/server/portable_service.rs | 45 ++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/src/server/portable_service.rs b/src/server/portable_service.rs index a666b56d5..d78038ecd 100644 --- a/src/server/portable_service.rs +++ b/src/server/portable_service.rs @@ -469,17 +469,20 @@ pub mod client { where Self: Sized, { - Self::set_para(CapturerPara { - current_display, - use_yuv, - timeout_ms: 33, - }); + let mut option = SHMEM.lock().unwrap(); + let shmem = option.as_mut().unwrap(); + Self::set_para( + shmem, + CapturerPara { + current_display, + use_yuv, + timeout_ms: 33, + }, + ); CapturerPortable {} } - fn set_para(para: CapturerPara) { - let mut option = SHMEM.lock().unwrap(); - let shmem = option.as_mut().unwrap(); + fn set_para(shmem: &mut SharedMemory, para: CapturerPara) { let para_ptr = ¶ as *const CapturerPara as *const u8; let para_data; unsafe { @@ -497,11 +500,14 @@ pub mod client { let para_ptr = shmem.as_ptr().add(ADDR_CAPTURER_PARA); let para = para_ptr as *const CapturerPara; if use_yuv != (*para).use_yuv { - Self::set_para(CapturerPara { - current_display: (*para).current_display, - use_yuv, - timeout_ms: (*para).timeout_ms, - }); + Self::set_para( + shmem, + CapturerPara { + current_display: (*para).current_display, + use_yuv, + timeout_ms: (*para).timeout_ms, + }, + ); } } } @@ -514,11 +520,14 @@ pub mod client { let para_ptr = base.add(ADDR_CAPTURER_PARA); let para = para_ptr as *const CapturerPara; if timeout.as_millis() != (*para).timeout_ms as _ { - Self::set_para(CapturerPara { - current_display: (*para).current_display, - use_yuv: (*para).use_yuv, - timeout_ms: timeout.as_millis() as _, - }); + Self::set_para( + shmem, + CapturerPara { + current_display: (*para).current_display, + use_yuv: (*para).use_yuv, + timeout_ms: timeout.as_millis() as _, + }, + ); } if utils::counter_ready(base.add(ADDR_CAPTURE_FRAME_COUNTER)) { let frame_len_ptr = base.add(ADDR_CAPTURE_FRAME_SIZE); From 8c529a1159dc98b2c3d9866a03c15d56b062023d Mon Sep 17 00:00:00 2001 From: 21pages Date: Sun, 13 Nov 2022 16:33:22 +0800 Subject: [PATCH 0937/2015] portable-service: add yuv set flag to fix start splash Signed-off-by: 21pages --- src/server/portable_service.rs | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/src/server/portable_service.rs b/src/server/portable_service.rs index d78038ecd..9256c9a71 100644 --- a/src/server/portable_service.rs +++ b/src/server/portable_service.rs @@ -261,7 +261,13 @@ pub mod server { let para = para_ptr as *const CapturerPara; let current_display = (*para).current_display; let use_yuv = (*para).use_yuv; + let use_yuv_set = (*para).use_yuv_set; let timeout_ms = (*para).timeout_ms; + if !use_yuv_set { + c = None; + std::thread::sleep(spf); + continue; + } if c.is_none() { *crate::video_service::CURRENT_DISPLAY.lock().unwrap() = current_display; let (_, _current, display) = get_current_display().unwrap(); @@ -476,6 +482,7 @@ pub mod client { CapturerPara { current_display, use_yuv, + use_yuv_set: false, timeout_ms: 33, }, ); @@ -499,16 +506,15 @@ pub mod client { unsafe { let para_ptr = shmem.as_ptr().add(ADDR_CAPTURER_PARA); let para = para_ptr as *const CapturerPara; - if use_yuv != (*para).use_yuv { - Self::set_para( - shmem, - CapturerPara { - current_display: (*para).current_display, - use_yuv, - timeout_ms: (*para).timeout_ms, - }, - ); - } + Self::set_para( + shmem, + CapturerPara { + current_display: (*para).current_display, + use_yuv, + use_yuv_set: true, + timeout_ms: (*para).timeout_ms, + }, + ); } } @@ -525,6 +531,7 @@ pub mod client { CapturerPara { current_display: (*para).current_display, use_yuv: (*para).use_yuv, + use_yuv_set: (*para).use_yuv_set, timeout_ms: timeout.as_millis() as _, }, ); @@ -732,5 +739,6 @@ pub mod client { struct CapturerPara { current_display: usize, use_yuv: bool, + use_yuv_set: bool, timeout_ms: i32, } From 3f77b6bc6498e92f5b132bbdefba7059c21c2325 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sun, 13 Nov 2022 17:01:09 +0800 Subject: [PATCH 0938/2015] portable service: sync capture counter to make continuous frame, which will decrease fps Signed-off-by: 21pages --- src/server/portable_service.rs | 41 +++++++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/src/server/portable_service.rs b/src/server/portable_service.rs index 9256c9a71..9e3f11bc7 100644 --- a/src/server/portable_service.rs +++ b/src/server/portable_service.rs @@ -172,11 +172,28 @@ mod utils { } } - pub fn increase_counter(ptr: *mut u8) { + pub fn counter_equal(counter: *const u8) -> bool { unsafe { - let i = ptr_to_i32(ptr); - let v = i32_to_vec(i + 1); - std::ptr::copy_nonoverlapping(v.as_ptr(), ptr, size_of::()); + let wptr = counter; + let rptr = counter.add(size_of::()); + let iw = ptr_to_i32(wptr); + let ir = ptr_to_i32(rptr); + iw == ir + } + } + + pub fn increase_counter(counter: *mut u8) { + unsafe { + let wptr = counter; + let rptr = counter.add(size_of::()); + let iw = ptr_to_i32(counter); + let ir = ptr_to_i32(counter); + let v = i32_to_vec(iw + 1); + std::ptr::copy_nonoverlapping(v.as_ptr(), wptr, size_of::()); + if ir == iw + 1 { + let v = i32_to_vec(iw); + std::ptr::copy_nonoverlapping(v.as_ptr(), rptr, size_of::()); + } } } @@ -212,7 +229,7 @@ pub mod server { })); for th in threads.drain(..) { th.join().unwrap(); - log::info!("all thread joined"); + log::info!("thread joined"); } } @@ -251,11 +268,11 @@ pub mod server { let mut last_use_yuv = false; let mut last_timeout_ms: i32 = 33; let mut spf = Duration::from_millis(last_timeout_ms as _); + let mut first_frame_captured = false; loop { if EXIT.lock().unwrap().clone() { break; } - let start = Instant::now(); unsafe { let para_ptr = shmem.as_ptr().add(ADDR_CAPTURER_PARA); let para = para_ptr as *const CapturerPara; @@ -276,6 +293,7 @@ pub mod server { c = { last_current_display = current_display; last_use_yuv = use_yuv; + first_frame_captured = false; // dxgi failed at loadFrame on my PC. // to-do: try dxgi on another PC. v.set_gdi(); @@ -308,6 +326,12 @@ pub mod server { spf = Duration::from_millis(timeout_ms as _); } } + if first_frame_captured { + if !utils::counter_equal(shmem.as_ptr().add(ADDR_CAPTURE_FRAME_COUNTER)) { + std::thread::sleep(spf); + continue; + } + } match c.as_mut().unwrap().frame(spf) { Ok(f) => { let len = f.0.len(); @@ -316,6 +340,7 @@ pub mod server { shmem.write(ADDR_CAPTURE_FRAME, f.0); shmem.write(ADDR_CAPTURE_WOULDBLOCK, &utils::i32_to_vec(TRUE)); utils::increase_counter(shmem.as_ptr().add(ADDR_CAPTURE_FRAME_COUNTER)); + first_frame_captured = true; } Err(e) => { if e.kind() != std::io::ErrorKind::WouldBlock { @@ -330,10 +355,6 @@ pub mod server { } } } - let elapsed = start.elapsed(); - if elapsed < spf { - std::thread::sleep(spf - elapsed); - } } } From 45bfb0e22e4440080bfdd7927ff3358adf0a288e Mon Sep 17 00:00:00 2001 From: 21pages Date: Sun, 13 Nov 2022 18:38:24 +0800 Subject: [PATCH 0939/2015] portable-service: run background Signed-off-by: 21pages --- src/platform/windows.rs | 21 +++++++++++++++++++++ src/server/portable_service.rs | 7 ++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/platform/windows.rs b/src/platform/windows.rs index b418d0904..34f132894 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -1480,6 +1480,27 @@ pub fn get_user_token(session_id: u32, as_user: bool) -> HANDLE { } } +pub fn run_background(exe: &str, arg: &str) -> ResultType { + let wexe = wide_string(exe); + let warg; + unsafe { + let ret = ShellExecuteW( + NULL as _, + NULL as _, + wexe.as_ptr() as _, + if arg.is_empty() { + NULL as _ + } else { + warg = wide_string(arg); + warg.as_ptr() as _ + }, + NULL as _, + SW_HIDE, + ); + return Ok(ret as i32 > 32); + } +} + pub fn run_uac(exe: &str, arg: &str) -> ResultType { let wop = wide_string("runas"); let wexe = wide_string(exe); diff --git a/src/server/portable_service.rs b/src/server/portable_service.rs index 9e3f11bc7..54f7a92dd 100644 --- a/src/server/portable_service.rs +++ b/src/server/portable_service.rs @@ -476,7 +476,12 @@ pub mod client { unsafe { libc::memset(shmem.as_ptr() as _, 0, shmem.len() as _); } - if crate::common::run_me(vec!["--portable-service"]).is_err() { + if crate::platform::run_background( + &std::env::current_exe()?.to_string_lossy().to_string(), + "--portable-service", + ) + .is_err() + { *SHMEM.lock().unwrap() = None; bail!("Failed to run portable service process"); } From abd665153b331de07646eb3499130a4af7906627 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sun, 13 Nov 2022 20:15:38 +0800 Subject: [PATCH 0940/2015] portable-service: try dxgi before gdi, which not controlled by video_service Signed-off-by: 21pages --- src/server/portable_service.rs | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/src/server/portable_service.rs b/src/server/portable_service.rs index 54f7a92dd..48cdcbe51 100644 --- a/src/server/portable_service.rs +++ b/src/server/portable_service.rs @@ -47,6 +47,7 @@ const ADDR_CAPTURE_FRAME: usize = const IPC_PROFIX: &str = "_portable_service"; pub const SHMEM_NAME: &str = "_portable_service"; const MAX_NACK: usize = 3; +const MAX_DXGI_FAIL_TIME: usize = 5; pub struct SharedMemory { inner: Shmem, @@ -269,6 +270,7 @@ pub mod server { let mut last_timeout_ms: i32 = 33; let mut spf = Duration::from_millis(last_timeout_ms as _); let mut first_frame_captured = false; + let mut dxgi_failed_times = 0; loop { if EXIT.lock().unwrap().clone() { break; @@ -294,9 +296,10 @@ pub mod server { last_current_display = current_display; last_use_yuv = use_yuv; first_frame_captured = false; - // dxgi failed at loadFrame on my PC. - // to-do: try dxgi on another PC. - v.set_gdi(); + if dxgi_failed_times > MAX_DXGI_FAIL_TIME { + dxgi_failed_times = 0; + v.set_gdi(); + } Some(v) } } @@ -341,14 +344,26 @@ pub mod server { shmem.write(ADDR_CAPTURE_WOULDBLOCK, &utils::i32_to_vec(TRUE)); utils::increase_counter(shmem.as_ptr().add(ADDR_CAPTURE_FRAME_COUNTER)); first_frame_captured = true; + dxgi_failed_times = 0; } Err(e) => { if e.kind() != std::io::ErrorKind::WouldBlock { - log::error!("capture frame failed:{:?}", e); - crate::platform::try_change_desktop(); - c = None; - shmem.write(ADDR_CAPTURE_WOULDBLOCK, &utils::i32_to_vec(FALSE)); - continue; + // DXGI_ERROR_INVALID_CALL after each success on Microsoft GPU driver + // log::error!("capture frame failed:{:?}", e); + if crate::platform::windows::desktop_changed() { + crate::platform::try_change_desktop(); + c = None; + std::thread::sleep(spf); + continue; + } + if !c.as_ref().unwrap().is_gdi() { + dxgi_failed_times += 1; + } + if dxgi_failed_times > MAX_DXGI_FAIL_TIME { + c = None; + shmem.write(ADDR_CAPTURE_WOULDBLOCK, &utils::i32_to_vec(FALSE)); + std::thread::sleep(spf); + } } else { shmem.write(ADDR_CAPTURE_WOULDBLOCK, &utils::i32_to_vec(TRUE)); } @@ -512,6 +527,7 @@ pub mod client { timeout_ms: 33, }, ); + shmem.write(ADDR_CAPTURE_WOULDBLOCK, &utils::i32_to_vec(TRUE)); CapturerPortable {} } @@ -586,6 +602,7 @@ pub mod client { } } + // control by itself fn is_gdi(&self) -> bool { true } From ca8cb5a3b0f74af7c2c9764905b0239a27135963 Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 15 Nov 2022 16:49:55 +0800 Subject: [PATCH 0941/2015] portable-service: better prompt message Signed-off-by: 21pages --- flutter/lib/common.dart | 21 ++++++---- flutter/lib/desktop/pages/remote_page.dart | 2 +- .../lib/desktop/widgets/remote_menubar.dart | 2 +- .../lib/mobile/pages/file_manager_page.dart | 3 +- flutter/lib/mobile/pages/remote_page.dart | 4 +- flutter/lib/mobile/widgets/dialog.dart | 6 +-- flutter/lib/models/model.dart | 11 ++++- src/client/io_loop.rs | 42 ++++++++++--------- src/flutter.rs | 7 +++- src/lang/cn.rs | 5 +-- src/lang/cs.rs | 5 +-- src/lang/da.rs | 5 +-- src/lang/de.rs | 5 +-- src/lang/en.rs | 4 +- src/lang/eo.rs | 5 +-- src/lang/es.rs | 5 +-- src/lang/fa.rs | 5 +-- src/lang/fr.rs | 5 +-- src/lang/hu.rs | 5 +-- src/lang/id.rs | 5 +-- src/lang/it.rs | 5 +-- src/lang/ja.rs | 5 +-- src/lang/ko.rs | 5 +-- src/lang/kz.rs | 5 +-- src/lang/pl.rs | 5 +-- src/lang/pt_PT.rs | 5 +-- src/lang/ptbr.rs | 5 +-- src/lang/ru.rs | 5 +-- src/lang/sk.rs | 5 +-- src/lang/template.rs | 5 +-- src/lang/tr.rs | 5 +-- src/lang/tw.rs | 5 +-- src/lang/ua.rs | 5 +-- src/lang/vn.rs | 5 +-- src/server/connection.rs | 9 ++-- src/ui/common.tis | 8 ++++ src/ui/remote.rs | 9 +++- src/ui_session_interface.rs | 7 ++-- 38 files changed, 131 insertions(+), 124 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 93fe0fee5..483aba384 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -627,7 +627,7 @@ class CustomAlertDialog extends StatelessWidget { } } -void msgBox(String type, String title, String text, String link, +void msgBox(String id, String type, String title, String text, String link, OverlayDialogManager dialogManager, {bool? hasCancel}) { dialogManager.dismissAll(); @@ -672,14 +672,17 @@ void msgBox(String type, String title, String text, String link, if (link.isNotEmpty) { buttons.insert(0, msgBoxButton(translate('JumpLink'), jumplink)); } - dialogManager.show((setState, close) => CustomAlertDialog( - title: _msgBoxTitle(title), - content: SelectableText(translate(text), - style: const TextStyle(fontSize: 15)), - actions: buttons, - onSubmit: hasOk ? submit : null, - onCancel: hasCancel == true ? cancel : null, - )); + dialogManager.show( + (setState, close) => CustomAlertDialog( + title: _msgBoxTitle(title), + content: + SelectableText(translate(text), style: const TextStyle(fontSize: 15)), + actions: buttons, + onSubmit: hasOk ? submit : null, + onCancel: hasCancel == true ? cancel : null, + ), + tag: '$id-$type-$title-$text-$link', + ); } Widget msgBoxButton(String text, void Function() onPressed) { diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 2b8c99940..dae3fa612 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -163,7 +163,7 @@ class _RemotePageState extends State super.build(context); return WillPopScope( onWillPop: () async { - clientClose(_ffi.dialogManager); + clientClose(widget.id, _ffi.dialogManager); return false; }, child: MultiProvider(providers: [ diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 75311c4c5..cdbeb0bed 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -489,7 +489,7 @@ class _RemoteMenubarState extends State { return IconButton( tooltip: translate('Close'), onPressed: () { - clientClose(widget.ffi.dialogManager); + clientClose(widget.id, widget.ffi.dialogManager); }, icon: const Icon( Icons.close, diff --git a/flutter/lib/mobile/pages/file_manager_page.dart b/flutter/lib/mobile/pages/file_manager_page.dart index 3221cdbaa..6e5c91484 100644 --- a/flutter/lib/mobile/pages/file_manager_page.dart +++ b/flutter/lib/mobile/pages/file_manager_page.dart @@ -63,7 +63,8 @@ class _FileManagerPageState extends State { leading: Row(children: [ IconButton( icon: Icon(Icons.close), - onPressed: () => clientClose(gFFI.dialogManager)), + onPressed: () => + clientClose(widget.id, gFFI.dialogManager)), ]), centerTitle: true, title: ToggleSwitch( diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 07304d2d3..719b7dc28 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -223,7 +223,7 @@ class _RemotePageState extends State { return WillPopScope( onWillPop: () async { - clientClose(gFFI.dialogManager); + clientClose(widget.id, gFFI.dialogManager); return false; }, child: getRawPointerAndKeyBody(Scaffold( @@ -304,7 +304,7 @@ class _RemotePageState extends State { color: Colors.white, icon: Icon(Icons.clear), onPressed: () { - clientClose(gFFI.dialogManager); + clientClose(widget.id, gFFI.dialogManager); }, ) ] + diff --git a/flutter/lib/mobile/widgets/dialog.dart b/flutter/lib/mobile/widgets/dialog.dart index 03b36ecf3..96f96658a 100644 --- a/flutter/lib/mobile/widgets/dialog.dart +++ b/flutter/lib/mobile/widgets/dialog.dart @@ -5,9 +5,9 @@ import '../../common.dart'; import '../../models/model.dart'; import '../../models/platform_model.dart'; -void clientClose(OverlayDialogManager dialogManager) { - msgBox( - '', 'Close', 'Are you sure to close the connection?', '', dialogManager); +void clientClose(String id, OverlayDialogManager dialogManager) { + msgBox(id, '', 'Close', 'Are you sure to close the connection?', '', + dialogManager); } void showSuccess() { diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index fb4f8b4f4..a39bc7d08 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -195,6 +195,8 @@ class FfiModel with ChangeNotifier { } else if (name == 'show_elevation') { final show = evt['show'].toString() == 'true'; parent.target?.serverModel.setShowElevation(show); + } else if (name == 'cancel_msgbox') { + cancelMsgBox(evt, peerId); } }; } @@ -231,6 +233,13 @@ class FfiModel with ChangeNotifier { notifyListeners(); } + cancelMsgBox(Map evt, String id) { + if (parent.target == null) return; + final dialogManager = parent.target!.dialogManager; + final tag = '$id-${evt['tag']}'; + dialogManager.dismissByTag(tag); + } + /// Handle the message box event based on [evt] and [id]. handleMsgBox(Map evt, String id) { if (parent.target == null) return; @@ -256,7 +265,7 @@ class FfiModel with ChangeNotifier { showMsgBox(String id, String type, String title, String text, String link, bool hasRetry, OverlayDialogManager dialogManager, {bool? hasCancel}) { - msgBox(type, title, text, link, dialogManager, hasCancel: hasCancel); + msgBox(id, type, title, text, link, dialogManager, hasCancel: hasCancel); _timer?.cancel(); if (hasRetry) { _timer = Timer(Duration(seconds: _reconnects), () { diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 04d1d4d29..6eb443103 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -20,18 +20,14 @@ use hbb_common::fs::{ 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}, }; -#[cfg(windows)] -use hbb_common::tokio::sync::Mutex as TokioMutex; -use hbb_common::{ - allow_err, - message_proto::*, - sleep, -}; +use hbb_common::{allow_err, message_proto::*, sleep}; use hbb_common::{fs, log, Stream}; use std::collections::HashMap; @@ -998,23 +994,31 @@ impl Remote { } } Some(misc::Union::Uac(uac)) => { + let msgtype = "custom-uac-nocancel"; + let title = "Prompt"; + let text = "Please wait for confirmation of UAC..."; + let link = ""; if uac { - self.handler.msgbox( - "custom-uac-nocancel", - "Warning", - "uac_warning", - "", - ); + self.handler.msgbox(msgtype, title, text, link); + } else { + self.handler + .cancel_msgbox( + &format!("{}-{}-{}-{}", msgtype, title, text, link,), + ); } } Some(misc::Union::ForegroundWindowElevated(elevated)) => { + let msgtype = "custom-elevated-foreground-nocancel"; + let title = "Prompt"; + let text = "elevated_foreground_window_tip"; + let link = ""; if elevated { - self.handler.msgbox( - "custom-elevated-foreground-nocancel", - "Warning", - "elevated_foreground_window_warning", - "", - ); + self.handler.msgbox(msgtype, title, text, link); + } else { + self.handler + .cancel_msgbox( + &format!("{}-{}-{}-{}", msgtype, title, text, link,), + ); } } _ => {} diff --git a/src/flutter.rs b/src/flutter.rs index a69473e5a..9c4208625 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -228,8 +228,7 @@ impl InvokeUiSession for FlutterHandler { id: i32, entries: &Vec, path: String, - #[allow(unused_variables)] - is_local: bool, + #[allow(unused_variables)] is_local: bool, only_count: bool, ) { // TODO opt @@ -327,6 +326,10 @@ impl InvokeUiSession for FlutterHandler { ); } + fn cancel_msgbox(&self, tag: &str) { + self.push_event("cancel_msgbox", vec![("tag", tag)]); + } + fn new_message(&self, msg: String) { self.push_event("chat_client_mode", vec![("text", &msg)]); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index b622123a0..68c5dbf60 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -371,9 +371,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Deny LAN Discovery", "拒绝局域网发现"), ("Write a message", "输入聊天消息"), ("Prompt", "提示"), - ("elevation_prompt", "以当前用户权限运行软件,可能导致远端在访问本机时,没有足够的权限来操作部分窗口。"), - ("uac_warning", "暂时无法访问远端设备,因为远端设备正在请求用户账户权限,请等待对方关闭UAC窗口。为避免这个问题,建议在远端设备上安装或者以管理员权限运行本软件。"), - ("elevated_foreground_window_warning", "暂时无法使用鼠标键盘,因为远端桌面的当前窗口需要更高的权限才能操作, 可以请求对方最小化当前窗口。为避免这个问题,建议在远端设备上安装或者以管理员权限运行本软件。"), + ("Please wait for confirmation of UAC...", "请等待对方确认UAC..."), + ("elevated_foreground_window_tip", "远端桌面的当前窗口需要更高的权限才能操作, 暂时无法使用鼠标键盘, 可以请求对方最小化当前窗口, 或者在连接管理窗口点击提升。为避免这个问题,建议在远端设备上安装本软件。"), ("Disconnected", "会话已结束"), ("Other", "其他"), ("Confirm before closing multiple tabs", "关闭多个标签页时向您确认"), diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 5c086bfb2..98bb9c5d3 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -371,9 +371,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Deny LAN Discovery", ""), ("Write a message", ""), ("Prompt", ""), - ("elevation_prompt", ""), - ("uac_warning", ""), - ("elevated_foreground_window_warning", ""), + ("Please wait for confirmation of UAC...", ""), + ("elevated_foreground_window_tip", ""), ("Disconnected", ""), ("Other", ""), ("Confirm before closing multiple tabs", ""), diff --git a/src/lang/da.rs b/src/lang/da.rs index 90670804a..b98e5ba3f 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -371,9 +371,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Deny LAN Discovery", "Afvis LAN Discovery"), ("Write a message", "Skriv en besked"), ("Prompt", ""), - ("elevation_prompt", ""), - ("uac_warning", ""), - ("elevated_foreground_window_warning", ""), + ("Please wait for confirmation of UAC...", ""), + ("elevated_foreground_window_tip", ""), ("Disconnected", "Afbrudt"), ("Other", ""), ("Confirm before closing multiple tabs", ""), diff --git a/src/lang/de.rs b/src/lang/de.rs index 6ebea6b2e..1e8083915 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -371,9 +371,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Deny LAN Discovery", "LAN-Erkennung verbieten"), ("Write a message", "Nachricht schreiben"), ("Prompt", ""), - ("elevation_prompt", ""), - ("uac_warning", ""), - ("elevated_foreground_window_warning", ""), + ("Please wait for confirmation of UAC...", ""), + ("elevated_foreground_window_tip", ""), ("Disconnected", ""), ("Other", ""), ("Confirm before closing multiple tabs", ""), diff --git a/src/lang/en.rs b/src/lang/en.rs index 3415fa463..ee68d4431 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -30,9 +30,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_open_battery_optimizations_tip", "If you want to disable this feature, please go to the next RustDesk application settings page, find and enter [Battery], Uncheck [Unrestricted]"), ("remote_restarting_tip", "Remote device is restarting, please close this message box and reconnect with permanent password after a while"), ("Are you sure to close the connection?", "Are you sure you want to close the connection?"), - ("elevation_prompt", "Running software without privilege elevation may cause problems when remote users operate certain windows."), - ("uac_warning", "Temporarily denied access due to elevation request, please wait for the remote user to accept the UAC dialog. To avoid this problem, it is recommended to install the software on the remote device or run it with administrator privileges."), - ("elevated_foreground_window_warning", "Temporarily unable to use the mouse and keyboard, because the current window of the remote desktop requires higher privilege to operate, you can request the remote user to minimize the current window. To avoid this problem, it is recommended to install the software on the remote device or run it with administrator privileges."), + ("elevated_foreground_window_tip", "The current window of the remote desktop requires higher privilege to operate, so it's unable to use the mouse and keyboard temporarily. You can request the remote user to minimize the current window, or click elevation button on the connection management window. To avoid this problem, it is recommended to install the software on the remote device."), ("JumpLink", "View"), ("Stop service", "Stop Service"), ("or", ""), diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 2dce72f6e..93394a91f 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -371,9 +371,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Deny LAN Discovery", ""), ("Write a message", ""), ("Prompt", ""), - ("elevation_prompt", ""), - ("uac_warning", ""), - ("elevated_foreground_window_warning", ""), + ("Please wait for confirmation of UAC...", ""), + ("elevated_foreground_window_tip", ""), ("Disconnected", ""), ("Other", ""), ("Confirm before closing multiple tabs", ""), diff --git a/src/lang/es.rs b/src/lang/es.rs index 2e70a5194..a00009b78 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -371,9 +371,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Deny LAN Discovery", "Denegar descubrimiento de LAN"), ("Write a message", "Escribir un mensaje"), ("Prompt", ""), - ("elevation_prompt", ""), - ("uac_warning", ""), - ("elevated_foreground_window_warning", ""), + ("Please wait for confirmation of UAC...", ""), + ("elevated_foreground_window_tip", ""), ("Disconnected", "Desconectado"), ("Other", "Otro"), ("Confirm before closing multiple tabs", "Confirmar antes de cerrar múltiples pestañas"), diff --git a/src/lang/fa.rs b/src/lang/fa.rs index f9a15dc9e..b6c3d002b 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -371,9 +371,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Deny LAN Discovery", "غیر فعالسازی جستجو در شبکه"), ("Write a message", "یک پیام بنویسید"), ("Prompt", ""), - ("elevation_prompt", "اجرای نرم‌افزار بدون افزایش امتیاز می‌تواند باعث ایجاد مشکلاتی در هنگام کار کردن کاربران راه دور با ویندوزهای خاص شود"), - ("uac_warning", "به دلیل درخواست دسترسی سطح بالا، به طور موقت از دسترسی رد شد. منتظر بمانید تا کاربر راه دور گفتگوی UAC را بپذیرد. برای جلوگیری از این مشکل، توصیه می شود نرم افزار را روی دستگاه از راه دور نصب کنید یا آن را با دسترسی مدیر اجرا کنید."), - ("elevated_foreground_window_warning", "به طور موقت استفاده از ماوس و صفحه کلید امکان پذیر نیست زیرا پنجره دسکتاپ از راه دور فعلی برای کار کردن به دسترسی های بالاتر نیاز دارد، می توانید از کاربر راه دور بخواهید که پنجره فعلی را به حداقل برساند. برای جلوگیری از این مشکل، توصیه می شود نرم افزار را روی یک دستگاه راه دور نصب کنید یا آن را با دسترسی مدیر اجرا کنید"), + ("Please wait for confirmation of UAC...", ""), + ("elevated_foreground_window_tip", ""), ("Disconnected", "قطع ارتباط"), ("Other", "دیگر"), ("Confirm before closing multiple tabs", "بستن چندین برگه را تأیید کنید"), diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 811ec911d..ff4e2e083 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -371,9 +371,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Deny LAN Discovery", "Interdir la découverte réseau local"), ("Write a message", "Ecrire un message"), ("Prompt", ""), - ("elevation_prompt", ""), - ("uac_warning", ""), - ("elevated_foreground_window_warning", ""), + ("Please wait for confirmation of UAC...", ""), + ("elevated_foreground_window_tip", ""), ("Disconnected", "Déconnecté"), ("Other", "Divers"), ("Confirm before closing multiple tabs", "Confirmer avant de fermer plusieurs onglets"), diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 863fa3739..9bd5de216 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -371,9 +371,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Deny LAN Discovery", "Felfedezés tiltása"), ("Write a message", "Üzenet írása"), ("Prompt", ""), - ("elevation_prompt", "A szoftver jogosultságnövelés nélküli futtatása problémákat okozhat, ha távoli felhasználók bizonyos ablakokat működtetnek."), - ("uac_warning", "Kérjük, várja meg, amíg a távoli felhasználó elfogadja az UAC párbeszédpanelt. A probléma elkerülése érdekében javasoljuk, hogy telepítse a szoftvert a távoli eszközre, vagy futtassa rendszergazdai jogosultságokkal."), - ("elevated_foreground_window_warning", "Átmenetileg nem tudja használni az egeret és a billentyűzetet, mert a távoli asztal aktuális ablakának működéséhez magasabb jogosultság szükséges, ezért kérheti a távoli felhasználót, hogy minimalizálja az aktuális ablakot. A probléma elkerülése érdekében javasoljuk, hogy telepítse a szoftvert a távoli eszközre, vagy futtassa rendszergazdai jogosultságokkal."), + ("Please wait for confirmation of UAC...", ""), + ("elevated_foreground_window_tip", ""), ("Disconnected", "Szétkapcsolva"), ("Other", "Egyéb"), ("Confirm before closing multiple tabs", "Biztos, hogy bezárja az összes lapot?"), diff --git a/src/lang/id.rs b/src/lang/id.rs index 78fccc9f3..7d4ae1634 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -371,9 +371,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Deny LAN Discovery", "Tolak Penemuan LAN"), ("Write a message", "Menulis pesan"), ("Prompt", ""), - ("elevation_prompt", ""), - ("uac_warning", ""), - ("elevated_foreground_window_warning", ""), + ("Please wait for confirmation of UAC...", ""), + ("elevated_foreground_window_tip", ""), ("Disconnected", "Terputus"), ("Other", "Lainnya"), ("Confirm before closing multiple tabs", "Konfirmasi sebelum menutup banyak tab"), diff --git a/src/lang/it.rs b/src/lang/it.rs index 35c09a0b2..3490a9b77 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -371,9 +371,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Deny LAN Discovery", ""), ("Write a message", ""), ("Prompt", ""), - ("elevation_prompt", ""), - ("uac_warning", ""), - ("elevated_foreground_window_warning", ""), + ("Please wait for confirmation of UAC...", ""), + ("elevated_foreground_window_tip", ""), ("Disconnected", ""), ("Other", ""), ("Confirm before closing multiple tabs", ""), diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 21344eb11..2953f80cd 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -371,9 +371,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Deny LAN Discovery", ""), ("Write a message", ""), ("Prompt", ""), - ("elevation_prompt", ""), - ("uac_warning", ""), - ("elevated_foreground_window_warning", ""), + ("Please wait for confirmation of UAC...", ""), + ("elevated_foreground_window_tip", ""), ("Disconnected", ""), ("Other", "他の"), ("Confirm before closing multiple tabs", "同時に複数のタブを閉じる前に確認する"), diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 3b7269023..2473ef2de 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -371,9 +371,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Deny LAN Discovery", ""), ("Write a message", ""), ("Prompt", ""), - ("elevation_prompt", ""), - ("uac_warning", ""), - ("elevated_foreground_window_warning", ""), + ("Please wait for confirmation of UAC...", ""), + ("elevated_foreground_window_tip", ""), ("Disconnected", ""), ("Other", ""), ("Confirm before closing multiple tabs", ""), diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 1dc505807..1ce728db3 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -371,9 +371,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Deny LAN Discovery", ""), ("Write a message", ""), ("Prompt", ""), - ("elevation_prompt", ""), - ("uac_warning", ""), - ("elevated_foreground_window_warning", ""), + ("Please wait for confirmation of UAC...", ""), + ("elevated_foreground_window_tip", ""), ("Disconnected", ""), ("Other", ""), ("Confirm before closing multiple tabs", ""), diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 7ed98913c..3bcc464c6 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -371,9 +371,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Deny LAN Discovery", "Zablokuj Wykrywanie LAN"), ("Write a message", "Napisz wiadomość"), ("Prompt", "Monit"), - ("elevation_prompt", "Monit o podwyższeniu uprawnień"), - ("uac_warning", "Ostrzeżenie UAC"), - ("elevated_foreground_window_warning", "Pierwszoplanowe okno ostrzeżenia o podwyższeniu uprawnień"), + ("Please wait for confirmation of UAC...", ""), + ("elevated_foreground_window_tip", ""), ("Disconnected", "Rozłączone"), ("Other", "Inne"), ("Confirm before closing multiple tabs", "Potwierdź przed zamknięciem wielu kart"), diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 8b4c980f6..e7bb0e73c 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -371,9 +371,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Deny LAN Discovery", ""), ("Write a message", ""), ("Prompt", ""), - ("elevation_prompt", ""), - ("uac_warning", ""), - ("elevated_foreground_window_warning", ""), + ("Please wait for confirmation of UAC...", ""), + ("elevated_foreground_window_tip", ""), ("Disconnected", "Desconectado"), ("Other", "Outro"), ("Confirm before closing multiple tabs", "Confirme antes de fechar vários separadores"), diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 0d5594ea9..bc35cfcb2 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -371,9 +371,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Deny LAN Discovery", "Negar descoberta da LAN"), ("Write a message", "Escrever uma mensagem"), ("Prompt", "Prompt de comando"), - ("elevation_prompt", "Prompt de comando (Admin)"), - ("uac_warning", "Aviso UAC"), - ("elevated_foreground_window_warning", "Aviso de janela de primeiro plano elevado"), + ("Please wait for confirmation of UAC...", ""), + ("elevated_foreground_window_tip", ""), ("Disconnected", "Desconectado"), ("Other", "Outro"), ("Confirm before closing multiple tabs", "Confirmar antes de fechar múltiplas abas"), diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 22c246f86..9e9a60829 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -371,9 +371,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Deny LAN Discovery", "Запретить обнаружение в локальной сети"), ("Write a message", "Написать сообщение"), ("Prompt", "Подсказка"), - ("elevation_prompt", "Запуск программного обеспечения без повышения привилегий может вызвать проблемы, когда удалённые пользователи работают с определёнными окнами."), - ("uac_warning", "Временно отказано в доступе из-за запроса на повышение прав. Подождите, пока удалённый пользователь примет диалоговое окно UAC. Чтобы избежать этой проблемы, рекомендуется устанавливать программное обеспечение на удалённое устройство или запускать его с правами администратора."), - ("elevated_foreground_window_warning", "Временно невозможно использовать мышь и клавиатуру, поскольку текущее окно удалённого рабочего стола требует более высоких привилегий для работы, вы можете попросить удалённого пользователя свернуть текущее окно. Чтобы избежать этой проблемы, рекомендуется устанавливать программное обеспечение на удалённое устройство или запускать его с правами администратора."), + ("Please wait for confirmation of UAC...", ""), + ("elevated_foreground_window_tip", ""), ("Disconnected", "Отключено"), ("Other", "Другое"), ("Confirm before closing multiple tabs", "Подтверждение закрытия несколько вкладок"), diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 618ede5cd..3970ef3b9 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -371,9 +371,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Deny LAN Discovery", ""), ("Write a message", ""), ("Prompt", ""), - ("elevation_prompt", ""), - ("uac_warning", ""), - ("elevated_foreground_window_warning", ""), + ("Please wait for confirmation of UAC...", ""), + ("elevated_foreground_window_tip", ""), ("Disconnected", ""), ("Other", ""), ("Confirm before closing multiple tabs", ""), diff --git a/src/lang/template.rs b/src/lang/template.rs index 4ff8f9b87..d9fd7b9bb 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -371,9 +371,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Deny LAN Discovery", ""), ("Write a message", ""), ("Prompt", ""), - ("elevation_prompt", ""), - ("uac_warning", ""), - ("elevated_foreground_window_warning", ""), + ("Please wait for confirmation of UAC...", ""), + ("elevated_foreground_window_tip", ""), ("Disconnected", ""), ("Other", ""), ("Confirm before closing multiple tabs", ""), diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 6fb89a09f..cd2c8b269 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -371,9 +371,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Deny LAN Discovery", ""), ("Write a message", ""), ("Prompt", ""), - ("elevation_prompt", ""), - ("uac_warning", ""), - ("elevated_foreground_window_warning", ""), + ("Please wait for confirmation of UAC...", ""), + ("elevated_foreground_window_tip", ""), ("Disconnected", ""), ("Other", ""), ("Confirm before closing multiple tabs", ""), diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 1150199cd..da57cf07c 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -371,9 +371,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Deny LAN Discovery", "拒絕局域網發現"), ("Write a message", "輸入聊天消息"), ("Prompt", "提示"), - ("elevation_prompt", "以當前用戶權限運行軟件,可能導致遠端在訪問本機時,沒有足夠的權限來操作部分窗口。"), - ("uac_warning", "暂时无法访问远端设备,因为远端设备正在请求用户账户权限,请等待对方关闭UAC窗口。为避免这个问题,建议在远端设备上安装或者以管理员权限运行本软件。"), - ("elevated_foreground_window_warning", "暫時無法使用鼠標鍵盤,因為遠端桌面的當前窗口需要更高的權限才能操作, 可以請求對方最小化當前窗口。為避免這個問題,建議在遠端設備上安裝或者以管理員權限運行本軟件。"), + ("Please wait for confirmation of UAC...", "請等待對方確認UAC"), + ("elevated_foreground_window_tip", "遠端桌面的當前窗口需要更高的權限才能操作, 暫時無法使用鼠標鍵盤, 可以請求對方最小化當前窗口, 或者在連接管理窗口點擊提升。為避免這個問題,建議在遠端設備上安裝本軟件。"), ("Disconnected", "會話已結束"), ("Other", "其他"), ("Confirm before closing multiple tabs", "關閉多個分頁前跟我確認"), diff --git a/src/lang/ua.rs b/src/lang/ua.rs index cc0ef6536..10119d1e2 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -371,9 +371,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Deny LAN Discovery", "Заборонити виявлення локальної мережі"), ("Write a message", "Написати повідомлення"), ("Prompt", ""), - ("elevation_prompt", ""), - ("uac_warning", ""), - ("elevated_foreground_window_warning", ""), + ("Please wait for confirmation of UAC...", ""), + ("elevated_foreground_window_tip", ""), ("Disconnected", ""), ("Other", ""), ("Confirm before closing multiple tabs", ""), diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 9773d7655..8bb164a8f 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -371,9 +371,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Deny LAN Discovery", ""), ("Write a message", ""), ("Prompt", ""), - ("elevation_prompt", ""), - ("uac_warning", ""), - ("elevated_foreground_window_warning", ""), + ("Please wait for confirmation of UAC...", ""), + ("elevated_foreground_window_tip", ""), ("Disconnected", ""), ("Other", ""), ("Confirm before closing multiple tabs", ""), diff --git a/src/server/connection.rs b/src/server/connection.rs index 8674c6d9d..cb2ddc2c6 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -26,12 +26,9 @@ use hbb_common::{ use scrap::android::call_main_service_mouse_input; use serde_json::{json, value::Value}; use sha2::{Digest, Sha256}; -use std::sync::{ - atomic::AtomicI64, - mpsc as std_mpsc, -}; #[cfg(not(any(target_os = "android", target_os = "ios")))] use std::sync::atomic::Ordering; +use std::sync::{atomic::AtomicI64, mpsc as std_mpsc}; #[cfg(not(any(target_os = "android", target_os = "ios")))] use system_shutdown; @@ -434,7 +431,7 @@ impl Connection { let uac = crate::video_service::IS_UAC_RUNNING.lock().unwrap().clone(); if last_uac != uac { last_uac = uac; - if !portable_service_running { + if !uac || !portable_service_running{ let mut misc = Misc::new(); misc.set_uac(uac); let mut msg = Message::new(); @@ -445,7 +442,7 @@ impl Connection { let foreground_window_elevated = crate::video_service::IS_FOREGROUND_WINDOW_ELEVATED.lock().unwrap().clone(); if last_foreground_window_elevated != foreground_window_elevated { last_foreground_window_elevated = foreground_window_elevated; - if !portable_service_running { + if !foreground_window_elevated || !portable_service_running { let mut misc = Misc::new(); misc.set_foreground_window_elevated(foreground_window_elevated); let mut msg = Message::new(); diff --git a/src/ui/common.tis b/src/ui/common.tis index 76e0fb84e..7507d4895 100644 --- a/src/ui/common.tis +++ b/src/ui/common.tis @@ -232,6 +232,7 @@ class ChatBox: Reactor.Component { /******************** start of msgbox ****************************************/ var remember_password = false; +var last_msgbox_tag = ""; function msgbox(type, title, content, link="", callback=null, height=180, width=500, hasRetry=false, contentStyle="") { $(body).scrollTo(0, 0); if (!type) { @@ -264,6 +265,7 @@ function msgbox(type, title, content, link="", callback=null, height=180, width= } else if (type.indexOf("custom") < 0 && !is_port_forward && !callback) { callback = function() { view.close(); } } + last_msgbox_tag = type + "-" + title + "-" + content + "-" + link; $(#msgbox).content(); } @@ -276,6 +278,12 @@ handler.msgbox = function(type, title, text, link = "", hasRetry=false) { self.timer(60ms, function() { msgbox(type, title, text, link, null, 180, 500, hasRetry); }); } +handler.cancel_msgbox = function(tag) { + if (last_msgbox_tag == tag) { + closeMsgbox(); + } +} + var reconnectTimeout = 1000; handler.msgbox_retry = function(type, title, text, link, hasRetry) { handler.msgbox(type, title, text, link, hasRetry); diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 62df85250..66b46cf85 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -231,7 +231,14 @@ impl InvokeUiSession for SciterHandler { } fn msgbox(&self, msgtype: &str, title: &str, text: &str, link: &str, retry: bool) { - self.call2("msgbox_retry", &make_args!(msgtype, title, text, link, retry)); + self.call2( + "msgbox_retry", + &make_args!(msgtype, title, text, link, retry), + ); + } + + fn cancel_msgbox(&self, tag: &str) { + self.call("cancel_msgbox", &make_args!(tag)); } fn new_message(&self, msg: String) { diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 4467fee00..5119a6e1b 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -1,11 +1,11 @@ -#[cfg(not(any(target_os = "android", target_os = "ios")))] -use crate::client::{get_key_state, SERVER_KEYBOARD_ENABLED}; use crate::client::io_loop::Remote; use crate::client::{ check_if_retry, handle_hash, handle_login_from_ui, handle_test_delay, input_os_password, load_config, send_mouse, start_video_audio_threads, FileManager, Key, LoginConfigHandler, QualityStatus, KEY_MAP, }; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use crate::client::{get_key_state, SERVER_KEYBOARD_ENABLED}; #[cfg(target_os = "linux")] use crate::common::IS_X11; use crate::{client::Data, client::Interface}; @@ -15,9 +15,9 @@ 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, EventType::*, Key as RdevKey}; #[cfg(not(any(target_os = "android", target_os = "ios")))] use rdev::{Keyboard as RdevKeyboard, KeyboardState}; -use rdev::{Event, EventType, EventType::*, Key as RdevKey}; use std::collections::{HashMap, HashSet}; use std::ops::{Deref, DerefMut}; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; @@ -1120,6 +1120,7 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { 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); + fn cancel_msgbox(&self, tag: &str); } impl Deref for Session { From 8328869c11b80b3dc568e2f35fcd8d05d0f31f40 Mon Sep 17 00:00:00 2001 From: Peeveck <118300943+Peeveck@users.noreply.github.com> Date: Tue, 15 Nov 2022 21:35:39 +0100 Subject: [PATCH 0942/2015] Update pl.rs --- src/lang/pl.rs | 52 +++++++++++++++++++++++++------------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 382b254f0..2e7e7dc46 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -7,7 +7,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Password", "Hasło"), ("Ready", "Gotowe"), ("Established", "Nawiązano"), - ("connecting_status", "Status połączenia"), + ("connecting_status", "Łączenie"), ("Enable Service", "Włącz usługę"), ("Start Service", "Uruchom usługę"), ("Service is running", "Usługa uruchomiona"), @@ -30,9 +30,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("IP Whitelisting", "Biała lista IP"), ("ID/Relay Server", "Serwer ID/Pośredniczący"), ("Import Server Config", "Importuj konfigurację serwera"), - ("Export Server Config", ""), - ("Import server configuration successfully", "Importowanie konfiguracji serwera powiodło się"), - ("Export server configuration successfully", ""), + ("Export Server Config", "Eksportuj konfigurację serwera"), + ("Import server configuration successfully", "Import konfiguracji serwera zakończono pomyślnie"), + ("Export server configuration successfully", "Eksport konfiguracji serwera zakończono pomyślnie"), ("Invalid server configuration", "Nieprawidłowa konfiguracja serwera"), ("Clipboard is empty", "Schowek jest pusty"), ("Stop service", "Zatrzymaj usługę"), @@ -47,7 +47,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ID Server", "Serwer ID"), ("Relay Server", "Serwer pośredniczący"), ("API Server", "Serwer API"), - ("invalid_http", "Nieprawidłowy żądanie http"), + ("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"), @@ -72,7 +72,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please try 1 minute later", "Spróbuj za minutę"), ("Login Error", "Błąd logowania"), ("Successful", "Sukces"), - ("Connected, waiting for image...", "Połączono, czekam na obraz..."), + ("Connected, waiting for image...", "Połączono, oczekiwanie na obraz..."), ("Name", "Nazwa"), ("Type", "Typ"), ("Modified", "Zmodyfikowany"), @@ -95,8 +95,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Not an empty directory", "Katalog nie jest pusty"), ("Are you sure you want to delete this file?", "Czy na pewno chcesz usunąć ten plik?"), ("Are you sure you want to delete this empty directory?", "Czy na pewno chcesz usunać ten pusty katalog?"), - ("Are you sure you want to delete the file of this directory?", "Czy na pewno chcesz usunąć pliki z tego katalog?"), - ("Do this for all conflicts", "Zrób to dla wszystkich konfliktów"), + ("Are you sure you want to delete the file of this directory?", "Czy na pewno chcesz usunąć pliki z tego katalogu?"), + ("Do this for all conflicts", "wykonaj dla wszystkich konfliktów"), ("This is irreversible!", "To jest nieodwracalne!"), ("Deleting", "Usuwanie"), ("files", "pliki"), @@ -121,15 +121,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Pokazuj jakość monitora"), ("Disable clipboard", "Wyłącz schowek"), ("Lock after session end", "Zablokuj po zakończeniu sesji"), - ("Insert", "Wstaw"), - ("Insert Lock", "Wstaw blokadę"), + ("Insert", "Wyślij"), + ("Insert Lock", "Wyślij Zablokuj"), ("Refresh", "Odśwież"), ("ID does not exist", "ID nie istnieje"), ("Failed to connect to rendezvous server", "Nie udało się połączyć z serwerem połączeń"), ("Please try later", "Spróbuj później"), ("Remote desktop is offline", "Zdalny pulpit jest offline"), ("Key mismatch", "Niezgodność klucza"), - ("Timeout", "Przekroczenie czasu"), + ("Timeout", "Przekroczono czas oczekiwania"), ("Failed to connect to relay server", "Nie udało się połączyć z serwerem pośredniczącym"), ("Failed to connect via rendezvous server", "Nie udało się połączyć przez serwer połączeń"), ("Failed to connect via relay server", "Nie udało się połączyć przez serwer pośredniczący"), @@ -195,7 +195,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Warning", "Ostrzeżenie"), ("Login screen using Wayland is not supported", "Ekran logowania korzystający z Wayland nie jest obsługiwany"), ("Reboot required", "Wymagany ponowne uruchomienie"), - ("Unsupported display server ", "Nieobsługiwany serwer wyświetlania "), + ("Unsupported display server ", "Nieobsługiwany serwer wyświetlania"), ("x11 expected", "Wymagany jest X11"), ("Port", "Port"), ("Settings", "Ustawienia"), @@ -206,19 +206,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Run without install", "Uruchom bez instalacji"), ("Always connected via relay", "Zawsze połączony pośrednio"), ("Always connect via relay", "Zawsze łącz pośrednio"), - ("whitelist_tip", "Podpowiedź do białej listy"), + ("whitelist_tip", "Zezwlaj na łączenie z tym komputerem tylko z adresów IP znajdujących się na białej liście"), ("Login", "Zaloguj"), ("Logout", "Wyloguj"), ("Tags", "Tagi"), ("Search ID", "Szukaj ID"), ("Current Wayland display server is not supported", "Obecny serwer wyświetlania Wayland nie jest obsługiwany"), - ("whitelist_sep", "Seperator białej listy"), + ("whitelist_sep", "Oddzielone przecinkiem, średnikiem, spacją lub w nowej linii"), ("Add ID", "Dodaj ID"), ("Add Tag", "Dodaj Tag"), ("Unselect all tags", "Odznacz wszystkie tagi"), ("Network error", "Błąd sieci"), - ("Username missed", "Brak użytkownika"), - ("Password missed", "Brak hasła"), + ("Username missed", "Nieprawidłowe nazwa użytkownika"), + ("Password missed", "Nieprawidłowe hasło"), ("Wrong credentials", "Błędne dane uwierzytelniające"), ("Edit Tag", "Edytuj tag"), ("Unremember Password", "Zapomnij hasło"), @@ -226,7 +226,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Favorites", "Dodaj do ulubionych"), ("Remove from Favorites", "Usuń z ulubionych"), ("Empty", "Pusty"), - ("Invalid folder name", "Błędna nazwa folderu"), + ("Invalid folder name", "Nieprawidłowa nazwa folderu"), ("Socks5 Proxy", "Socks5 Proxy"), ("Hostname", "Nazwa hosta"), ("Discovered", "Wykryte"), @@ -242,12 +242,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Left Mouse", "Lewy klik myszy"), ("One-Long Tap", "Przytrzymaj jednym palcem"), ("Two-Finger Tap", "Dotknij dwoma palcami"), - ("Right Mouse", "Prawy klik myszy"), - ("One-Finger Move", "Ruch jednym palcem"), + ("Right Mouse", "Prawy przycisk myszy"), + ("One-Finger Move", "Przesuń jednym palcem"), ("Double Tap & Move", "Dotknij dwukrotnie i przesuń"), ("Mouse Drag", "Przeciągnij myszą"), - ("Three-Finger vertically", "Trzy palce wertykalnie"), - ("Mouse Wheel", "Skrol myszy"), + ("Three-Finger vertically", "Trzy palce pionowo"), + ("Mouse Wheel", "Kółko myszy"), ("Two-Finger Move", "Ruch dwoma palcami"), ("Canvas Move", "Ruch ekranu"), ("Pinch to Zoom", "Uszczypnij, aby powiększyć"), @@ -289,7 +289,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Someone turns on privacy mode, exit", "Ktoś włącza tryb prywatności, wyjdź"), ("Unsupported", "Niewspierane"), ("Peer denied", "Odmowa dostępu"), - ("Please install plugins", "Zainstaluj plugin"), + ("Please install plugins", "Zainstaluj wtyczkę"), ("Peer exit", "Wyjście peer"), ("Failed to turn off", "Nie udało się wyłączyć"), ("Turned off", "Wyłączony"), @@ -385,9 +385,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland wymaga wyższej wersji dystrybucji Linuksa. Wypróbuj pulpit X11 lub zmień system operacyjny."), ("JumpLink", "View"), ("Please Select the screen to be shared(Operate on the peer side).", "Wybierz ekran do udostępnienia (działaj po stronie równorzędnej)."), - ("Show RustDesk", ""), - ("This PC", ""), - ("or", ""), - ("Continue with", ""), + ("Show RustDesk", "Pokaż RustDesk"), + ("This PC", "Ten komputer"), + ("or", "albo"), + ("Continue with", "Kontynuuj z"), ].iter().cloned().collect(); } From 2a65d948aabbd9ac807ecfac9be999a8afc2aadc Mon Sep 17 00:00:00 2001 From: 21pages Date: Wed, 16 Nov 2022 13:15:05 +0800 Subject: [PATCH 0943/2015] portable-service: little fix Signed-off-by: 21pages --- src/lang/pl.rs | 2 +- src/server/input_service.rs | 14 +++++++------- src/server/portable_service.rs | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 0e2616a80..ffe94432d 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -387,7 +387,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show RustDesk", "Pokaż RustDesk"), ("This PC", "Ten komputer"), ("or", "albo"), - ("Continue with", "Kontynuuj z"),""), + ("Continue with", "Kontynuuj z"), ("Elevate", ""), ].iter().cloned().collect(); } diff --git a/src/server/input_service.rs b/src/server/input_service.rs index af4b7d853..af9441ae4 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -280,13 +280,6 @@ fn get_modifier_state(key: Key, en: &mut Enigo) -> bool { } pub fn handle_mouse(evt: &MouseEvent, conn: i32) { - #[cfg(target_os = "macos")] - if !*IS_SERVER { - // having GUI, run main GUI thread, otherwise crash - let evt = evt.clone(); - QUEUE.exec_async(move || handle_mouse_(&evt, conn)); - return; - } if !active_mouse_(conn) { return; } @@ -300,6 +293,13 @@ pub fn handle_mouse(evt: &MouseEvent, conn: i32) { y: evt.y, }; } + #[cfg(target_os = "macos")] + if !*IS_SERVER { + // having GUI, run main GUI thread, otherwise crash + let evt = evt.clone(); + QUEUE.exec_async(move || handle_mouse_(&evt)); + return; + } #[cfg(windows)] crate::portable_service::client::handle_mouse(evt); #[cfg(not(windows))] diff --git a/src/server/portable_service.rs b/src/server/portable_service.rs index 48cdcbe51..21b501f1e 100644 --- a/src/server/portable_service.rs +++ b/src/server/portable_service.rs @@ -16,7 +16,7 @@ use std::{ mem::size_of, ops::{Deref, DerefMut}, sync::{Arc, Mutex}, - time::{Duration, Instant}, + time::Duration, }; use winapi::{ shared::minwindef::{BOOL, FALSE, TRUE}, From 44b89f574b9a320dd9e5a6bcfc728db0c7f2149b Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 15 Nov 2022 22:35:10 +0800 Subject: [PATCH 0944/2015] fix cursor image && hotx,hoty, debug win Signed-off-by: fufesou --- flutter/lib/models/model.dart | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index a39bc7d08..1c7c6cafa 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -696,6 +696,8 @@ class CursorData { final img2.Image? image; double scale; Uint8List? data; + final double hotxOrigin; + final double hotyOrigin; double hotx; double hoty; final int width; @@ -707,11 +709,12 @@ class CursorData { required this.image, required this.scale, required this.data, - required this.hotx, - required this.hoty, + required this.hotxOrigin, + required this.hotyOrigin, required this.width, required this.height, - }); + }) : hotx = hotxOrigin * scale, + hoty = hotxOrigin * scale; int _doubleToInt(double v) => (v * 10e6).round().toInt(); @@ -731,16 +734,14 @@ class CursorData { image!, width: (width * scale).toInt(), height: (height * scale).toInt(), + interpolation: img2.Interpolation.cubic, ) .getBytes(format: img2.Format.bgra); } } this.scale = scale; - if (hotx > 0 && hoty > 0) { - // default cursor data - hotx = (width * scale) / 2; - hoty = (height * scale) / 2; - } + hotx = hotxOrigin * scale; + hoty = hotyOrigin * scale; return scale; } @@ -811,8 +812,6 @@ class CursorModel with ChangeNotifier { if (_defaultCache == null) { Uint8List data; double scale = 1.0; - double hotx = (defaultCursorImage!.width * scale) / 2; - double hoty = (defaultCursorImage!.height * scale) / 2; if (Platform.isWindows) { data = defaultCursorImage!.getBytes(format: img2.Format.bgra); } else { @@ -825,8 +824,8 @@ class CursorModel with ChangeNotifier { image: defaultCursorImage?.clone(), scale: scale, data: data, - hotx: hotx, - hoty: hoty, + hotxOrigin: defaultCursorImage!.width / 2, + hotyOrigin: defaultCursorImage!.height / 2, width: defaultCursorImage!.width, height: defaultCursorImage!.height, ); @@ -996,10 +995,8 @@ class CursorModel with ChangeNotifier { image: Platform.isWindows ? img2.Image.fromBytes(w, h, data) : null, scale: 1.0, data: data, - hotx: 0, - hoty: 0, - // hotx: _hotx, - // hoty: _hoty, + hotxOrigin: _hotx, + hotyOrigin: _hoty, width: w, height: h, ); From 46423614c803778ab73b1c5d949acc58270eff8b Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 15 Nov 2022 23:02:50 +0800 Subject: [PATCH 0945/2015] change cursor resize interpolation to linear 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 1c7c6cafa..6734bd8fb 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -734,7 +734,7 @@ class CursorData { image!, width: (width * scale).toInt(), height: (height * scale).toInt(), - interpolation: img2.Interpolation.cubic, + interpolation: img2.Interpolation.linear, ) .getBytes(format: img2.Format.bgra); } From 658c539730668f1ff74551308322f93c9a04c8a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=BCseyin=20Fahri=20Uzun?= Date: Wed, 16 Nov 2022 12:11:24 +0300 Subject: [PATCH 0946/2015] Updated tr.rs --- src/lang/tr.rs | 134 ++++++++++++++++++++++++------------------------- 1 file changed, 67 insertions(+), 67 deletions(-) diff --git a/src/lang/tr.rs b/src/lang/tr.rs index cd2c8b269..01aa7ff07 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -41,9 +41,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "Hakkında"), ("Mute", "Sustur"), ("Audio Input", "Ses Girişi"), - ("Enhancements", ""), - ("Hardware Codec", ""), - ("Adaptive Bitrate", ""), + ("Enhancements", "Geliştirmeler"), + ("Hardware Codec", "Donanımsal Codec"), + ("Adaptive Bitrate", "Uyarlanabilir Bit Hızı"), ("ID Server", "ID Sunucu"), ("Relay Server", "Relay Sunucu"), ("API Server", "API Sunucu"), @@ -89,8 +89,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Delete", "Sil"), ("Properties", "Özellikler"), ("Multi Select", "Çoklu Seçim"), - ("Select All", ""), - ("Unselect All", ""), + ("Select All", "Tümünü Seç"), + ("Unselect All", "Tüm Seçimi Kaldır"), ("Empty Directory", "Boş Klasör"), ("Not an empty directory", "Klasör boş değil"), ("Are you sure you want to delete this file?", "Bu dosyayı silmek istediğinize emin misiniz?"), @@ -116,9 +116,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Good image quality", "İyi görüntü kalitesi"), ("Balanced", "Dengelenmiş"), ("Optimize reaction time", "Tepki süresini optimize et"), - ("Custom", ""), + ("Custom", "Özel"), ("Show remote cursor", "Uzaktaki fare imlecini göster"), - ("Show quality monitor", ""), + ("Show quality monitor", "Kalite monitörünü göster"), ("Disable clipboard", "Hafızadaki kopyalanmışları engelle"), ("Lock after session end", "Bağlantıdan sonra kilitle"), ("Insert", "Ekle"), @@ -161,8 +161,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Action", "Eylem"), ("Add", "Ekle"), ("Local Port", "Yerel Port"), - ("Local Address", ""), - ("Change Local Port", ""), + ("Local Address", "Yerel Adres"), + ("Change Local Port", "Yerel Port'u Değiştir"), ("setup_server_tip", "Daha hızlı bağlantı için kendi sunucunuzu kurun"), ("Too short, at least 6 characters.", "Çok kısa en az 6 karakter gerekli."), ("The confirmation is not identical.", "Doğrulama yapılamadı."), @@ -197,7 +197,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Reboot required", "Yeniden başlatma gerekli"), ("Unsupported display server ", "Desteklenmeyen görüntü sunucusu"), ("x11 expected", "x11 bekleniyor"), - ("Port", ""), + ("Port", "Port"), ("Settings", "Ayarlar"), ("Username", "Kullanıcı Adı"), ("Invalid port", "Geçersiz port"), @@ -278,7 +278,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_stop_service_tip", "Hizmetin kapatılması, kurulan tüm bağlantıları otomatik olarak kapatacaktır."), ("android_version_audio_tip", "Mevcut Android sürümü ses yakalamayı desteklemiyor, lütfen Android 10 veya sonraki bir sürüme yükseltin."), ("android_start_service_tip", "Ekran paylaşım hizmetini başlatmak için [Hizmeti Başlat] veya AÇ [Ekran Yakalama] iznine dokunun."), - ("Account", ""), + ("Account", "Hesap"), ("Overwrite", "üzerine yaz"), ("This file exists, skip or overwrite this file?", "Bu dosya var, bu dosya atlansın veya üzerine yazılsın mı?"), ("Quit", "Çıkış"), @@ -300,21 +300,21 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Ignore Battery Optimizations", ""), ("android_open_battery_optimizations_tip", ""), ("Connection not allowed", "bağlantıya izin verilmedi"), - ("Legacy mode", ""), - ("Map mode", ""), - ("Translate mode", ""), + ("Legacy mode", "Eski mod"), + ("Map mode", "Haritalama modu"), + ("Translate mode", "Çeviri modu"), ("Use temporary password", "Geçici şifre kullan"), ("Use permanent password", "Kalıcı şifre kullan"), ("Use both passwords", "İki şifreyide kullan"), ("Set permanent password", "Kalıcı şifre oluştur"), - ("Set temporary password length", ""), + ("Set temporary password length", "Geçici şifre oluştur"), ("Enable Remote Restart", "Uzaktan yeniden başlatmayı aktif et"), ("Allow remote restart", "Uzaktan yeniden başlatmaya izin ver"), ("Restart Remote Device", "Uzaktaki cihazı yeniden başlat"), ("Are you sure you want to restart", "Yeniden başlatmak istediğinize emin misin?"), ("Restarting Remote Device", "Uzaktan yeniden başlatılıyor"), - ("remote_restarting_tip", ""), - ("Copied", ""), + ("remote_restarting_tip", "remote_restarting_tip"), + ("Copied", "Kopyalandı"), ("Exit Fullscreen", "Tam ekrandan çık"), ("Fullscreen", "Tam ekran"), ("Mobile Actions", "Mobil İşlemler"), @@ -332,62 +332,62 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Insecure Connection", "Güvenli Bağlantı"), ("Scale original", "Orijinali ölçeklendir"), ("Scale adaptive", "Ölçek uyarlanabilir"), - ("General", ""), - ("Security", ""), - ("Account", ""), - ("Theme", ""), - ("Dark Theme", ""), - ("Dark", ""), - ("Light", ""), - ("Follow System", ""), - ("Enable hardware codec", ""), - ("Unlock Security Settings", ""), - ("Enable Audio", ""), - ("Temporary Password Length", ""), - ("Unlock Network Settings", ""), - ("Server", ""), - ("Direct IP Access", ""), - ("Proxy", ""), - ("Port", ""), - ("Apply", ""), - ("Disconnect all devices?", ""), - ("Clear", ""), - ("Audio Input Device", ""), - ("Deny remote access", ""), - ("Use IP Whitelisting", ""), - ("Network", ""), - ("Enable RDP", ""), + ("General", "Genel"), + ("Security", "Güvenlik"), + ("Account", "Hesap"), + ("Theme", "Tema"), + ("Dark Theme", "Koyu Tema"), + ("Dark", "Koyu"), + ("Light", "Açık"), + ("Follow System", "Sisteme Uy"), + ("Enable hardware codec", "Donanımsal codec aktif et"), + ("Unlock Security Settings", "Güvenlik Ayarlarını Aç"), + ("Enable Audio", "Sesi Aktif Et"), + ("Temporary Password Length", "Geçici Şifre Uzunluğu"), + ("Unlock Network Settings", "Ağ Ayarlarını Aç"), + ("Server", "Sunucu"), + ("Direct IP Access", "Direk IP Erişimi"), + ("Proxy", "Vekil"), + ("Port", "Port"), + ("Apply", "Uygula"), + ("Disconnect all devices?", "Tüm cihazların bağlantısını kes?"), + ("Clear", "Temizle"), + ("Audio Input Device", "Ses Giriş Aygıtı"), + ("Deny remote access", "Uzak erişime izin verme"), + ("Use IP Whitelisting", "IP Beyaz Listeyi Kullan"), + ("Network", "Ağ"), + ("Enable RDP", "RDP Aktif Et"), ("Pin menubar", "Menü çubuğunu sabitle"), ("Unpin menubar", "Menü çubuğunun sabitlemesini kaldır"), - ("Recording", ""), - ("Directory", ""), - ("Automatically record incoming sessions", ""), - ("Change", ""), - ("Start session recording", ""), - ("Stop session recording", ""), - ("Enable Recording Session", ""), - ("Allow recording session", ""), - ("Enable LAN Discovery", ""), - ("Deny LAN Discovery", ""), - ("Write a message", ""), + ("Recording", "Kayıt Ediliyor"), + ("Directory", "Klasör"), + ("Automatically record incoming sessions", "Gelen oturumları otomatik olarak kayıt et"), + ("Change", "Değiştir"), + ("Start session recording", "Oturum kaydını başlat"), + ("Stop session recording", "Oturum kaydını sonlandır"), + ("Enable Recording Session", "Kayıt Oturumunu Aktif Et"), + ("Allow recording session", "Oturum kaydına izin ver"), + ("Enable LAN Discovery", "Yerel Ağ Keşfine İzin Ver"), + ("Deny LAN Discovery", "Yerl Ağ Keşfine İzin Verme"), + ("Write a message", "Bir mesaj yazın"), ("Prompt", ""), - ("Please wait for confirmation of UAC...", ""), - ("elevated_foreground_window_tip", ""), - ("Disconnected", ""), - ("Other", ""), - ("Confirm before closing multiple tabs", ""), - ("Keyboard Settings", ""), - ("Custom", ""), - ("Full Access", ""), - ("Screen Share", ""), + ("Please wait for confirmation of UAC...", "UAC onayı için lütfen bekleyiniz..."), + ("elevated_foreground_window_tip", "elevated_foreground_window_tip"), + ("Disconnected", "Bağlantı Kesildi"), + ("Other", "Diğer"), + ("Confirm before closing multiple tabs", "Çoklu sekmeleri kapatmadan önce onayla"), + ("Keyboard Settings", "Klavye Ayarları"), + ("Custom", "Özel"), + ("Full Access", "Tam Erişim"), + ("Screen Share", "Ekran Paylaşımı"), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland, Ubuntu 21.04 veya daha yüksek bir sürüm gerektirir."), ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland, linux dağıtımının daha yüksek bir sürümünü gerektirir. Lütfen X11 masaüstünü deneyin veya işletim sisteminizi değiştirin."), ("JumpLink", "View"), ("Please Select the screen to be shared(Operate on the peer side).", "Lütfen paylaşılacak ekranı seçiniz (Ekran tarafında çalıştırın)."), - ("Show RustDesk", ""), - ("This PC", ""), - ("or", ""), - ("Continue with", ""), - ("Elevate", ""), + ("Show RustDesk", "RustDesk'i Göster"), + ("This PC", "Bu PC"), + ("or", "veya"), + ("Continue with", "bununla devam et"), + ("Elevate", "Yükseltme"), ].iter().cloned().collect(); } From 50dc2a4d73cb300d0994dd9cd64ca017e108383c Mon Sep 17 00:00:00 2001 From: csf Date: Wed, 16 Nov 2022 18:53:32 +0900 Subject: [PATCH 0947/2015] fix: sending file from local to remote (keep send_raw) --- libs/hbb_common/src/fs.rs | 15 +++++---------- src/client/io_loop.rs | 2 +- src/server/connection.rs | 3 ++- src/ui_cm_interface.rs | 32 ++++++++++++++++++-------------- 4 files changed, 26 insertions(+), 26 deletions(-) diff --git a/libs/hbb_common/src/fs.rs b/libs/hbb_common/src/fs.rs index 6cc795a0d..dd8a7530e 100644 --- a/libs/hbb_common/src/fs.rs +++ b/libs/hbb_common/src/fs.rs @@ -380,7 +380,7 @@ impl TransferJob { } } - pub async fn write(&mut self, block: FileTransferBlock, raw: Option<&[u8]>) -> ResultType<()> { + pub async fn write(&mut self, block: FileTransferBlock) -> ResultType<()> { if block.id != self.id { bail!("Wrong id"); } @@ -402,20 +402,15 @@ impl TransferJob { let path = format!("{}.download", get_string(&path)); self.file = Some(File::create(&path).await?); } - let data = if let Some(data) = raw { - data - } else { - &block.data - }; if block.compressed { - let tmp = decompress(data); + let tmp = decompress(&block.data); self.file.as_mut().unwrap().write_all(&tmp).await?; self.finished_size += tmp.len() as u64; } else { - self.file.as_mut().unwrap().write_all(data).await?; - self.finished_size += data.len() as u64; + self.file.as_mut().unwrap().write_all(&block.data).await?; + self.finished_size += block.data.len() as u64; } - self.transferred += data.len() as u64; + self.transferred += block.data.len() as u64; Ok(()) } diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 04d1d4d29..447b89290 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -925,7 +925,7 @@ impl Remote { block.file_num ); if let Some(job) = fs::get_job(block.id, &mut self.write_jobs) { - if let Err(_err) = job.write(block, None).await { + if let Err(_err) = job.write(block).await { // to-do: add "skip" for writing job } self.update_jobs_status(); diff --git a/src/server/connection.rs b/src/server/connection.rs index 3644066e2..891c48888 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1609,7 +1609,8 @@ async fn start_ipc( file_num, data, compressed}) = data { - stream.send(&Data::FS(ipc::FS::WriteBlock{id, file_num, data, compressed})).await?; + stream.send(&Data::FS(ipc::FS::WriteBlock{id, file_num, data: Bytes::new(), compressed})).await?; + stream.send_raw(data).await?; } else { stream.send(&data).await?; } diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index 72225b3fb..e8668a2ab 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -1,3 +1,5 @@ +#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] +use std::iter::FromIterator; #[cfg(windows)] use std::sync::Arc; use std::{ @@ -8,8 +10,6 @@ use std::{ RwLock, }, }; -#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] -use std::iter::FromIterator; #[cfg(windows)] use clipboard::{cliprdr::CliprdrClientContext, empty_clipboard, set_conn_enabled, ContextSend}; @@ -337,8 +337,15 @@ impl IpcTaskRunner { Data::ChatMessage { text } => { self.cm.new_message(self.conn_id, text); } - Data::FS(fs) => { - handle_fs(fs, &mut write_jobs, &self.tx).await; + Data::FS(mut fs) => { + if let ipc::FS::WriteBlock { id, file_num, data: _, compressed } = fs { + if let Ok(bytes) = self.stream.next_raw().await { + fs = ipc::FS::WriteBlock{id, file_num, data:bytes.into(), compressed}; + handle_fs(fs, &mut write_jobs, &self.tx).await; + } + } else { + handle_fs(fs, &mut write_jobs, &self.tx).await; + } } #[cfg(windows)] Data::ClipbaordFile(_clip) => { @@ -588,16 +595,13 @@ async fn handle_fs(fs: ipc::FS, write_jobs: &mut Vec, tx: &Unbo } => { if let Some(job) = fs::get_job(id, write_jobs) { if let Err(err) = job - .write( - FileTransferBlock { - id, - file_num, - data, - compressed, - ..Default::default() - }, - None, - ) + .write(FileTransferBlock { + id, + file_num, + data, + compressed, + ..Default::default() + }) .await { send_raw(fs::new_error(id, err, file_num), &tx); From a98e655cc765f9599918b74bfa2d8a5374ad5f2d Mon Sep 17 00:00:00 2001 From: csf Date: Wed, 16 Nov 2022 18:59:13 +0900 Subject: [PATCH 0948/2015] remove unused log --- src/client/io_loop.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 447b89290..3e691da95 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -919,11 +919,6 @@ impl Remote { } } Some(file_response::Union::Block(block)) => { - log::info!( - "file response block, file id:{}, file num: {}", - block.id, - block.file_num - ); if let Some(job) = fs::get_job(block.id, &mut self.write_jobs) { if let Err(_err) = job.write(block).await { // to-do: add "skip" for writing job From 9a70725090501cf8e27f3bf7c52abd20a7572459 Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 16 Nov 2022 18:07:58 +0800 Subject: [PATCH 0949/2015] Add peer option: zoom cursor & show menubar on conn Signed-off-by: fufesou --- flutter/lib/common/shared_state.dart | 22 ++++++++++ flutter/lib/desktop/pages/remote_page.dart | 41 +++++++++++++++---- .../lib/desktop/widgets/remote_menubar.dart | 22 +++++++++- flutter/lib/models/model.dart | 35 ++++++++++------ 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/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/ru.rs | 1 + src/lang/sk.rs | 1 + src/lang/template.rs | 1 + src/lang/tr.rs | 1 + src/lang/tw.rs | 1 + src/lang/ua.rs | 1 + src/lang/vn.rs | 1 + 28 files changed, 121 insertions(+), 23 deletions(-) diff --git a/flutter/lib/common/shared_state.dart b/flutter/lib/common/shared_state.dart index 5ae618dfe..ebac18dac 100644 --- a/flutter/lib/common/shared_state.dart +++ b/flutter/lib/common/shared_state.dart @@ -202,6 +202,28 @@ class RemoteCountState { static RxInt find() => Get.find(tag: tag()); } +class PeerBoolOption { + static String tag(String id, String opt) => 'peer_{$opt}_$id'; + + static void init(String id, String opt, bool Function() init_getter) { + final key = tag(id, opt); + if (!Get.isRegistered(tag: key)) { + final RxBool value = RxBool(init_getter()); + Get.put(value, tag: key); + } + } + + static void delete(String id, String opt) { + final key = tag(id, opt); + if (Get.isRegistered(tag: key)) { + Get.delete(tag: key); + } + } + + static RxBool find(String id, String opt) => + Get.find(tag: tag(id, opt)); +} + class PeerStringOption { static String tag(String id, String opt) => 'peer_{$opt}_$id'; diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index dae3fa612..117a0ab02 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -51,6 +51,7 @@ class _RemotePageState extends State String keyboardMode = "legacy"; final _cursorOverImage = false.obs; late RxBool _showRemoteCursor; + late RxBool _zoomCursor; late RxBool _remoteCursorMoved; late RxBool _keyboardEnabled; @@ -68,6 +69,10 @@ class _RemotePageState extends State KeyboardEnabledState.init(id); ShowRemoteCursorState.init(id); RemoteCursorMovedState.init(id); + final optZoomCursor = 'zoom-cursor'; + PeerBoolOption.init(id, optZoomCursor, + () => bind.sessionGetToggleOptionSync(id: id, arg: optZoomCursor)); + _zoomCursor = PeerBoolOption.find(id, optZoomCursor); _showRemoteCursor = ShowRemoteCursorState.find(id); _keyboardEnabled = KeyboardEnabledState.find(id); _remoteCursorMoved = RemoteCursorMovedState.find(id); @@ -216,6 +221,7 @@ class _RemotePageState extends State }); return ImagePaint( id: widget.id, + zoomCursor: _zoomCursor, cursorOverImage: _cursorOverImage, keyboardEnabled: _keyboardEnabled, remoteCursorMoved: _remoteCursorMoved, @@ -233,6 +239,7 @@ class _RemotePageState extends State visible: _showRemoteCursor.isTrue && _remoteCursorMoved.isTrue, child: CursorPaint( id: widget.id, + zoomCursor: _zoomCursor, )))); paints.add(QualityMonitor(_ffi.qualityMonitorModel)); paints.add(RemoteMenubar( @@ -253,6 +260,7 @@ class _RemotePageState extends State class ImagePaint extends StatefulWidget { final String id; + final Rx zoomCursor; final Rx cursorOverImage; final Rx keyboardEnabled; final Rx remoteCursorMoved; @@ -261,6 +269,7 @@ class ImagePaint extends StatefulWidget { ImagePaint( {Key? key, required this.id, + required this.zoomCursor, required this.cursorOverImage, required this.keyboardEnabled, required this.remoteCursorMoved, @@ -277,6 +286,7 @@ class _ImagePaintState extends State { final ScrollController _vertical = ScrollController(); String get id => widget.id; + Rx get zoomCursor => widget.zoomCursor; Rx get cursorOverImage => widget.cursorOverImage; Rx get keyboardEnabled => widget.keyboardEnabled; Rx get remoteCursorMoved => widget.remoteCursorMoved; @@ -357,7 +367,7 @@ class _ImagePaintState extends State { if (cache == null) { return MouseCursor.defer; } else { - final key = cache.updateGetKey(scale); + final key = cache.updateGetKey(scale, zoomCursor.value); cursor.addKey(key); return FlutterCustomMemoryImageCursor( pixbuf: cache.data, @@ -500,8 +510,13 @@ class _ImagePaintState extends State { class CursorPaint extends StatelessWidget { final String id; + final RxBool zoomCursor; - const CursorPaint({Key? key, required this.id}) : super(key: key); + const CursorPaint({ + Key? key, + required this.id, + required this.zoomCursor, + }) : super(key: key); @override Widget build(BuildContext context) { @@ -516,13 +531,21 @@ class CursorPaint extends StatelessWidget { hoty = m.defaultImage!.height / 2; } } - return CustomPaint( - painter: ImagePainter( - image: m.image ?? m.defaultImage, - x: m.x - hotx + c.x / c.scale, - y: m.y - hoty + c.y / c.scale, - scale: c.scale), - ); + return zoomCursor.isTrue + ? CustomPaint( + painter: ImagePainter( + image: m.image ?? m.defaultImage, + x: m.x - hotx + c.x / c.scale, + y: m.y - hoty + c.y / c.scale, + scale: c.scale), + ) + : CustomPaint( + painter: ImagePainter( + image: m.image ?? m.defaultImage, + x: (m.x - hotx) * c.scale + c.x, + y: (m.y - hoty) * c.scale + c.y, + scale: 1.0), + ); } } diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index cdbeb0bed..6db5a7fb7 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -47,7 +47,8 @@ class MenubarState { } _initSet(bool s, bool p) { - show = RxBool(s); + // Show remubar when connection is established. + show = RxBool(true); _pin = RxBool(p); } @@ -1109,6 +1110,25 @@ class _RemoteMenubarState extends State { ); }()); + /// Show remote cursor + displayMenu.add(() { + final opt = 'zoom-cursor'; + final state = PeerBoolOption.find(widget.id, opt); + return MenuEntrySwitch2( + switchType: SwitchType.scheckbox, + text: translate('Zoom cursor'), + getter: () { + return state; + }, + setter: (bool v) async { + state.value = v; + await bind.sessionToggleOption(id: widget.id, value: opt); + }, + padding: padding, + dismissOnClicked: true, + ); + }()); + /// Show quality monitor displayMenu.add(MenuEntrySwitch( switchType: SwitchType.scheckbox, diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 6734bd8fb..a074bf266 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -718,35 +718,44 @@ class CursorData { int _doubleToInt(double v) => (v * 10e6).round().toInt(); - double _checkUpdateScale(double scale) { - // Update data if scale changed. - if (Platform.isWindows) { - final tgtWidth = (width * scale).toInt(); - final tgtHeight = (width * scale).toInt(); - if (tgtWidth < kMinCursorSize || tgtHeight < kMinCursorSize) { - double sw = kMinCursorSize.toDouble() / width; - double sh = kMinCursorSize.toDouble() / height; - scale = sw < sh ? sh : sw; + double _checkUpdateScale(double scale, bool shouldScale) { + double oldScale = this.scale; + if (!shouldScale) { + scale = 1.0; + } else { + // Update data if scale changed. + if (Platform.isWindows) { + final tgtWidth = (width * scale).toInt(); + final tgtHeight = (width * scale).toInt(); + if (tgtWidth < kMinCursorSize || tgtHeight < kMinCursorSize) { + double sw = kMinCursorSize.toDouble() / width; + double sh = kMinCursorSize.toDouble() / height; + scale = sw < sh ? sh : sw; + } } - if (_doubleToInt(this.scale) != _doubleToInt(scale)) { + } + + if (Platform.isWindows) { + if (_doubleToInt(oldScale) != _doubleToInt(scale)) { data = img2 .copyResize( image!, width: (width * scale).toInt(), height: (height * scale).toInt(), - interpolation: img2.Interpolation.linear, + interpolation: img2.Interpolation.average, ) .getBytes(format: img2.Format.bgra); } } + this.scale = scale; hotx = hotxOrigin * scale; hoty = hotyOrigin * scale; return scale; } - String updateGetKey(double scale) { - scale = _checkUpdateScale(scale); + String updateGetKey(double scale, bool shouldScale) { + scale = _checkUpdateScale(scale, shouldScale); return '${peerId}_${id}_${_doubleToInt(width * scale)}_${_doubleToInt(height * scale)}'; } } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 68c5dbf60..16bbdb590 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -389,5 +389,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("or", "或"), ("Continue with", "使用"), ("Elevate", "提权"), + ("Zoom cursor", "缩放鼠标"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 98bb9c5d3..0f262cd25 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -389,5 +389,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("or", ""), ("Continue with", ""), ("Elevate", ""), + ("Zoom cursor", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index b98e5ba3f..c7362e26f 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -389,5 +389,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("or", ""), ("Continue with", ""), ("Elevate", ""), + ("Zoom cursor", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 1e8083915..acc22a461 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -389,5 +389,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("or", ""), ("Continue with", ""), ("Elevate", ""), + ("Zoom cursor", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 93394a91f..206229859 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -389,5 +389,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("or", ""), ("Continue with", ""), ("Elevate", ""), + ("Zoom cursor", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index a00009b78..d0c569eff 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -389,5 +389,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("or", "o"), ("Continue with", "Continuar con"), ("Elevate", ""), + ("Zoom cursor", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index b6c3d002b..b602bd404 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -388,5 +388,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("This PC", "This PC"), ("or", "یا"), ("Continue with", "ادامه با"), + ("Zoom cursor", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index ff4e2e083..f4ff46cdf 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -389,5 +389,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("or", "ou"), ("Continue with", "Continuer avec"), ("Elevate", ""), + ("Zoom cursor", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 9bd5de216..aaad2e9f9 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -389,5 +389,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("or", "vagy"), ("Continue with", "Folytatás a következővel"), ("Elevate", ""), + ("Zoom cursor", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 7d4ae1634..96e7d38f4 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -389,5 +389,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("or", ""), ("Continue with", ""), ("Elevate", ""), + ("Zoom cursor", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 3490a9b77..fc6b936bf 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -389,5 +389,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("or", ""), ("Continue with", ""), ("Elevate", ""), + ("Zoom cursor", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 2953f80cd..1ff301bba 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -389,5 +389,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("or", ""), ("Continue with", ""), ("Elevate", ""), + ("Zoom cursor", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 2473ef2de..aa6c01e3d 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -389,5 +389,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("or", ""), ("Continue with", ""), ("Elevate", ""), + ("Zoom cursor", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 1ce728db3..05055d1a3 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -389,5 +389,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("or", ""), ("Continue with", ""), ("Elevate", ""), + ("Zoom cursor", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index ffe94432d..c62007778 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -389,5 +389,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("or", "albo"), ("Continue with", "Kontynuuj z"), ("Elevate", ""), + ("Zoom cursor", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index e7bb0e73c..ca0fcead9 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -389,5 +389,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("or", ""), ("Continue with", ""), ("Elevate", ""), + ("Zoom cursor", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index bc35cfcb2..41459404b 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -389,5 +389,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("or", "ou"), ("Continue with", "Continuar com"), ("Elevate", ""), + ("Zoom cursor", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 9e9a60829..b5d747f9d 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -389,5 +389,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("or", "или"), ("Continue with", "Продолжить с"), ("Elevate", ""), + ("Zoom cursor", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 3970ef3b9..0844c8442 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -389,5 +389,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("or", ""), ("Continue with", ""), ("Elevate", ""), + ("Zoom cursor", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index d9fd7b9bb..9f1779119 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -389,5 +389,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("or", ""), ("Continue with", ""), ("Elevate", ""), + ("Zoom cursor", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index cd2c8b269..6b625b63a 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -389,5 +389,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("or", ""), ("Continue with", ""), ("Elevate", ""), + ("Zoom cursor", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index da57cf07c..900f25ccd 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -389,5 +389,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("or", ""), ("Continue with", ""), ("Elevate", "提權"), + ("Zoom cursor", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 10119d1e2..b336b1e36 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -389,5 +389,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("or", ""), ("Continue with", ""), ("Elevate", ""), + ("Zoom cursor", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 8bb164a8f..c21fe8aa8 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -389,5 +389,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("or", ""), ("Continue with", ""), ("Elevate", ""), + ("Zoom cursor", ""), ].iter().cloned().collect(); } From dcd2d93fcb9c864775cb591d67f9fcf6d9a050c8 Mon Sep 17 00:00:00 2001 From: ivanbea Date: Wed, 16 Nov 2022 11:26:53 +0100 Subject: [PATCH 0950/2015] add catalan translation ca.rs add catalan translation ca.rs --- src/lang/ca.rs | 393 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 393 insertions(+) create mode 100644 src/lang/ca.rs diff --git a/src/lang/ca.rs b/src/lang/ca.rs new file mode 100644 index 000000000..1958b5b94 --- /dev/null +++ b/src/lang/ca.rs @@ -0,0 +1,393 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "Estat"), + ("Your Desktop", "EL teu escriptori"), + ("desk_tip", "Pots accedir al teu escriptori amb aquest ID i contrasenya."), + ("Password", "Contrasenya"), + ("Ready", "Llest"), + ("Established", "Establert"), + ("connecting_status", "Connexió a la xarxa RustDesk en progrés..."), + ("Enable Service", "Habilitar Servei"), + ("Start Service", "Iniciar Servei"), + ("Service is running", "El servei s'està executant"), + ("Service is not running", "El servei no s'està executant"), + ("not_ready_status", "No està llest. Comprova la teva connexió"), + ("Control Remote Desktop", "Controlar escriptori remot"), + ("Transfer File", "Transferir arxiu"), + ("Connect", "Connectar"), + ("Recent Sessions", "Sessions recents"), + ("Address Book", "Directori"), + ("Confirmation", "Confirmació"), + ("TCP Tunneling", "Túnel TCP"), + ("Remove", "Eliminar"), + ("Refresh random password", "Actualitzar contrasenya aleatòria"), + ("Set your own password", "Estableix la teva pròpia contrasenya"), + ("Enable Keyboard/Mouse", "Habilitar teclat/ratolí"), + ("Enable Clipboard", "Habilitar portapapers"), + ("Enable File Transfer", "Habilitar transferència d'arxius"), + ("Enable TCP Tunneling", "Habilitar túnel TCP"), + ("IP Whitelisting", "Direccions IP admeses"), + ("ID/Relay Server", "Servidor ID/Relay"), + ("Import Server Config", "Importar configuració de servidor"), + ("Export Server Config", "Exportar configuració del servidor"), + ("Import server configuration successfully", "Configuració de servidor importada amb èxit"), + ("Export server configuration successfully", "Configuració de servidor exportada con èxit"), + ("Invalid server configuration", "Configuració de servidor incorrecta"), + ("Clipboard is empty", "El portapapers està buit"), + ("Stop service", "Aturar servei"), + ("Change ID", "Canviar ID"), + ("Website", "Lloc web"), + ("About", "Sobre"), + ("Mute", "Silenciar"), + ("Audio Input", "Entrada d'àudio"), + ("Enhancements", "Millores"), + ("Hardware Codec", "Còdec de hardware"), + ("Adaptive Bitrate", "Tasa de bits adaptativa"), + ("ID Server", "Servidor de IDs"), + ("Relay Server", "Servidor Relay"), + ("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"), + ("Too frequent", "Massa comú"), + ("Cancel", "Cancel·lar"), + ("Skip", "Saltar"), + ("Close", "Tancar"), + ("Retry", "Reintentar"), + ("OK", ""), + ("Password Required", "Es necessita la contrasenya"), + ("Please enter your password", "Si us plau, introdueixi la seva contrasenya"), + ("Remember password", "Recordar contrasenya"), + ("Wrong Password", "Contrasenya incorrecta"), + ("Do you want to enter again?", "Vol tornar a entrar?"), + ("Connection Error", "Error de connexió"), + ("Error", ""), + ("Reset by the peer", "Reestablert pel peer"), + ("Connecting...", "Connectant..."), + ("Connection in progress. Please wait.", "Connexió en procés. Esperi."), + ("Please try 1 minute later", "Torni a provar-ho d'aquí un minut"), + ("Login Error", "Error d'inicio de sessió"), + ("Successful", "Exitós"), + ("Connected, waiting for image...", "Connectant, esperant imatge..."), + ("Name", "Nom"), + ("Type", "Tipus"), + ("Modified", "Modificat"), + ("Size", "Grandària"), + ("Show Hidden Files", "Mostrar arxius ocults"), + ("Receive", "Rebre"), + ("Send", "Enviar"), + ("Refresh File", "Actualitzar arxiu"), + ("Local", ""), + ("Remote", "Remot"), + ("Remote Computer", "Ordinador remot"), + ("Local Computer", "Ordinador local"), + ("Confirm Delete", "Confirma eliminació"), + ("Delete", "Eliminar"), + ("Properties", "Propietats"), + ("Multi Select", "Selecció múltiple"), + ("Select All", "Selecciona-ho Tot"), + ("Unselect All", "Deselecciona-ho Tot"), + ("Empty Directory", "Directori buit"), + ("Not an empty directory", "No és un directori buit"), + ("Are you sure you want to delete this file?", "Estàs segur que vols eliminar aquest arxiu?"), + ("Are you sure you want to delete this empty directory?", "Estàs segur que vols eliminar aquest directori buit?"), + ("Are you sure you want to delete the file of this directory?", "Estàs segur que vols eliminar aquest arxiu d'aquest directori?"), + ("Do this for all conflicts", "Fes això per a tots els conflictes"), + ("This is irreversible!", "Això és irreversible!"), + ("Deleting", "Eliminant"), + ("files", "arxius"), + ("Waiting", "Esperant"), + ("Finished", "Acabat"), + ("Speed", "Velocitat"), + ("Custom Image Quality", "Qualitat d'imatge personalitzada"), + ("Privacy mode", "Mode privat"), + ("Block user input", "Bloquejar entrada d'usuari"), + ("Unblock user input", "Desbloquejar entrada d'usuari"), + ("Adjust Window", "Ajustar finestra"), + ("Original", "Original"), + ("Shrink", "Reduir"), + ("Stretch", "Estirar"), + ("Scrollbar", "Barra de desplaçament"), + ("ScrollAuto", "Desplaçament automàtico"), + ("Good image quality", "Bona qualitat d'imatge"), + ("Balanced", "Equilibrat"), + ("Optimize reaction time", "Optimitzar el temps de reacció"), + ("Custom", "Personalitzat"), + ("Show remote cursor", "Mostrar cursor remot"), + ("Show quality monitor", "Mostrar qualitat del monitor"), + ("Disable clipboard", "Deshabilitar portapapers"), + ("Lock after session end", "Bloquejar després del final de la sessió"), + ("Insert", "Inserir"), + ("Insert Lock", "Inserir bloqueig"), + ("Refresh", "Actualitzar"), + ("ID does not exist", "L'ID no existeix"), + ("Failed to connect to rendezvous server", "No es pot connectar al servidor rendezvous"), + ("Please try later", "Siusplau provi-ho més tard"), + ("Remote desktop is offline", "L'escriptori remot està desconecctat"), + ("Key mismatch", "La clau no coincideix"), + ("Timeout", "Temps esgotat"), + ("Failed to connect to relay server", "No es pot connectar al servidor de relay"), + ("Failed to connect via rendezvous server", "No es pot connectar a través del servidor de rendezvous"), + ("Failed to connect via relay server", "No es pot connectar a través del servidor de relay"), + ("Failed to make direct connection to remote desktop", "No s'ha pogut establir una connexió directa amb l'escriptori remot"), + ("Set Password", "Configurar la contrasenya"), + ("OS Password", "contrasenya del sistema operatiu"), + ("install_tip", ""), + ("Click to upgrade", "Clicar per actualitzar"), + ("Click to download", "Clicar per descarregar"), + ("Click to update", "Clicar per refrescar"), + ("Configure", "Configurar"), + ("config_acc", ""), + ("config_screen", ""), + ("Installing ...", "Instal·lant ..."), + ("Install", "Instal·lar"), + ("Installation", "Instal·lació"), + ("Installation Path", "Ruta d'instal·lació"), + ("Create start menu shortcuts", "Crear accessos directes al menú d'inici"), + ("Create desktop icon", "Crear icona d'escriptori"), + ("agreement_tip", ""), + ("Accept and Install", "Acceptar i instal·lar"), + ("End-user license agreement", "Acord de llicència d'usuario final"), + ("Generating ...", "Generant ..."), + ("Your installation is lower version.", "La seva instal·lació és una versión inferior."), + ("not_close_tcp_tip", ""), + ("Listening ...", "Escoltant..."), + ("Remote Host", "Hoste remot"), + ("Remote Port", "Port remot"), + ("Action", "Acció"), + ("Add", "Afegirr"), + ("Local Port", "Port local"), + ("Local Address", "Adreça Local"), + ("Change Local Port", "Canviar Port Local"), + ("setup_server_tip", ""), + ("Too short, at least 6 characters.", "Massa curt, almenys 6 caràcters."), + ("The confirmation is not identical.", "La confirmación no coincideix."), + ("Permissions", "Permisos"), + ("Accept", "Acceptar"), + ("Dismiss", "Cancel·lar"), + ("Disconnect", "Desconnectar"), + ("Allow using keyboard and mouse", "Permetre l'ús del teclat i ratolí"), + ("Allow using clipboard", "Permetre usar portapapers"), + ("Allow hearing sound", "Permetre escoltar so"), + ("Allow file copy and paste", "Permetre copiar i enganxar arxius"), + ("Connected", "Connectat"), + ("Direct and encrypted connection", "Connexió directa i xifrada"), + ("Relayed and encrypted connection", "connexió retransmesa i xifrada"), + ("Direct and unencrypted connection", "connexió directa i sense xifrar"), + ("Relayed and unencrypted connection", "connexió retransmesa i sense xifrar"), + ("Enter Remote ID", "Introduixi l'ID remot"), + ("Enter your password", "Introdueixi la seva contrasenya"), + ("Logging in...", "Iniciant sessió..."), + ("Enable RDP session sharing", "Habilitar l'ús compartit de sessions RDP"), + ("Auto Login", "Inici de sessió automàtic"), + ("Enable Direct IP Access", "Habilitar accés IP directe"), + ("Rename", "Renombrar"), + ("Space", "Espai"), + ("Create Desktop Shortcut", "Crear accés directe a l'escriptori"), + ("Change Path", "Cnviar ruta"), + ("Create Folder", "Crear carpeta"), + ("Please enter the folder name", "Indiqui el nom de la carpeta"), + ("Fix it", "Soluciona-ho"), + ("Warning", "Avís"), + ("Login screen using Wayland is not supported", "La pantalla d'inici de sessió amb Wayland no és compatible"), + ("Reboot required", "Cal reiniciar"), + ("Unsupported display server ", "Servidor de visualització no compatible"), + ("x11 expected", "x11 necessari"), + ("Port", ""), + ("Settings", "Ajustaments"), + ("Username", " Nom d'usuari"), + ("Invalid port", "Port incorrecte"), + ("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"), + ("Always connect via relay", "Connecta sempre a través de relay"), + ("whitelist_tip", ""), + ("Login", "Inicia sessió"), + ("Logout", "Sortir"), + ("Tags", ""), + ("Search ID", "Cerca ID"), + ("Current Wayland display server is not supported", "El servidor de visualització actual de Wayland no és compatible"), + ("whitelist_sep", ""), + ("Add ID", "Afegir ID"), + ("Add Tag", "Afegir tag"), + ("Unselect all tags", "Deseleccionar tots els tags"), + ("Network error", "Error de xarxa"), + ("Username missed", "Nom d'usuari oblidat"), + ("Password missed", "Contrasenya oblidada"), + ("Wrong credentials", "Credencials incorrectes"), + ("Edit Tag", "Editar tag"), + ("Unremember Password", "Contrasenya oblidada"), + ("Favorites", "Preferits"), + ("Add to Favorites", "Afegir a preferits"), + ("Remove from Favorites", "Treure de preferits"), + ("Empty", "Buit"), + ("Invalid folder name", "Nom de carpeta incorrecte"), + ("Socks5 Proxy", "Proxy Socks5"), + ("Hostname", ""), + ("Discovered", "Descobert"), + ("install_daemon_tip", ""), + ("Remote ID", "ID remot"), + ("Paste", "Enganxar"), + ("Paste here?", "Enganxar aquí?"), + ("Are you sure to close the connection?", "Estàs segur que vols tancar la connexió?"), + ("Download new version", "Descarregar nova versió"), + ("Touch mode", "Mode tàctil"), + ("Mouse mode", "Mode ratolí"), + ("One-Finger Tap", "Toqui amb un dit"), + ("Left Mouse", "Ratolí esquerra"), + ("One-Long Tap", "Toc llarg"), + ("Two-Finger Tap", "Toqui amb dos dits"), + ("Right Mouse", "Botó dret"), + ("One-Finger Move", "Moviment amb un dir"), + ("Double Tap & Move", "Toqui dos cops i mogui"), + ("Mouse Drag", "Arrastri amb el ratolí"), + ("Three-Finger vertically", "Tres dits verticalment"), + ("Mouse Wheel", "Roda del ratolí"), + ("Two-Finger Move", "Moviment amb dos dits"), + ("Canvas Move", "Moviment del llenç"), + ("Pinch to Zoom", "Pessiga per fer zoom"), + ("Canvas Zoom", "Ampliar llenç"), + ("Reset canvas", "Reestablir llenç"), + ("No permission of file transfer", "No tens permís de transferència de fitxers"), + ("Note", "Nota"), + ("Connection", "connexió"), + ("Share Screen", "Compartir pantalla"), + ("CLOSE", "TANCAR"), + ("OPEN", "OBRIR"), + ("Chat", "Xat"), + ("Total", "Total"), + ("items", "ítems"), + ("Selected", "Seleccionat"), + ("Screen Capture", "Captura de pantalla"), + ("Input Control", "Control d'entrada"), + ("Audio Capture", "Captura d'àudio"), + ("File Connection", "connexió d'arxius"), + ("Screen Connection", "connexió de pantalla"), + ("Do you accept?", "Acceptes?"), + ("Open System Setting", "Configuració del sistema obert"), + ("How to get Android input permission?", "Com obtenir el permís d'entrada d'Android?"), + ("android_input_permission_tip1", "Per a que un dispositiu remot controli el seu dispositiu Android amb el ratolí o tocs, cal permetre que RustDesk utilitzi el servei d' \"Accesibilitat\"."), + ("android_input_permission_tip2", "Vagi a la pàgina de [Serveis instal·lats], activi el servici [RustDesk Input]."), + ("android_new_connection_tip", "S'ha rebut una nova sol·licitud de control per al dispositiu actual."), + ("android_service_will_start_tip", "Habilitar la captura de pantalla iniciarà el servei automàticament, i permetrà que altres dispositius sol·licitin una connexió des d'aquest dispositiu."), + ("android_stop_service_tip", "Tancar el servei tancarà totes les connexions establertes."), + ("android_version_audio_tip", "La versión actual de Android no admet la captura d'àudio, actualizi a Android 10 o superior."), + ("android_start_service_tip", "Toqui el permís [Iniciar servei] o OBRIR [Captura de pantalla] per iniciar el servei d'ús compartit de pantalla."), + ("Account", "Compte"), + ("Overwrite", "Sobreescriure"), + ("This file exists, skip or overwrite this file?", "Aquest arxiu ja existeix, ometre o sobreescriure l'arxiu?"), + ("Quit", "Sortir"), + ("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"), + ("Help", "Ajuda"), + ("Failed", "Ha fallat"), + ("Succeeded", "Aconseguit"), + ("Someone turns on privacy mode, exit", "Algú ha activat el mode de privacitat, surti"), + ("Unsupported", "No suportat"), + ("Peer denied", "Peer denegat"), + ("Please install plugins", "Instal·li complements"), + ("Peer exit", "El peer ha sortit"), + ("Failed to turn off", "Error en apagar"), + ("Turned off", "Apagat"), + ("In privacy mode", "En mode de privacitat"), + ("Out privacy mode", "Fora del mode de privacitat"), + ("Language", "Idioma"), + ("Keep RustDesk background service", "Mantenir RustDesk com a servei en segon pla"), + ("Ignore Battery Optimizations", "Ignorar optimizacions de la bateria"), + ("android_open_battery_optimizations_tip", ""), + ("Connection not allowed", "Connexió no disponible"), + ("Legacy mode", "Mode heretat"), + ("Map mode", "Mode mapa"), + ("Translate mode", "Mode traduit"), + ("Use temporary password", "Utilitzar contrasenya temporal"), + ("Use permanent password", "Utilitzar contrasenya permament"), + ("Use both passwords", "Utilitzar ambdues contrasenyas"), + ("Set permanent password", "Establir contrasenya permament"), + ("Set temporary password length", "Establir llargada de la contrasenya temporal"), + ("Enable Remote Restart", "Activar reinici remot"), + ("Allow remote restart", "Permetre reinici remot"), + ("Restart Remote Device", "Reiniciar dispositiu"), + ("Are you sure you want to restart", "Està segur que vol reiniciar?"), + ("Restarting Remote Device", "Reiniciant dispositiu remot"), + ("remote_restarting_tip", "Dispositiu remot reiniciant, tanqui aquest missatge i tornis a connectar amb la contrasenya."), + ("Copied", "Copiat"), + ("Exit Fullscreen", "Sortir de la pantalla completa"), + ("Fullscreen", "Pantalla completa"), + ("Mobile Actions", "Accions mòbils"), + ("Select Monitor", "Seleccionar monitor"), + ("Control Actions", "Accions de control"), + ("Display Settings", "Configuració de pantalla"), + ("Ratio", "Relació"), + ("Image Quality", "Qualitat d'imatge"), + ("Scroll Style", "Estil de desplaçament"), + ("Show Menubar", "Mostra barra de menú"), + ("Hide Menubar", "Amaga barra de menú"), + ("Direct Connection", "Connexió directa"), + ("Relay Connection", "Connexió Relay"), + ("Secure Connection", "Connexió segura"), + ("Insecure Connection", "Connexió insegura"), + ("Scale original", "Escala original"), + ("Scale adaptive", "Escala adaptativa"), + ("General", ""), + ("Security", "Seguritat"), + ("Account", "Compte"), + ("Theme", "Tema"), + ("Dark Theme", "Tema Fosc"), + ("Dark", "Fosc"), + ("Light", "Clar"), + ("Follow System", "Tema del sistema"), + ("Enable hardware codec", "Habilitar còdec per hardware"), + ("Unlock Security Settings", "Desbloquejar ajustaments de seguritat"), + ("Enable Audio", "Habilitar àudio"), + ("Temporary Password Length", "Longitut de Contrasenya Temporal"), + ("Unlock Network Settings", "Desbloquejar Ajustaments de Xarxa"), + ("Server", "Servidor"), + ("Direct IP Access", "Accés IP Directe"), + ("Proxy", ""), + ("Port", ""), + ("Apply", "Aplicar"), + ("Disconnect all devices?", "Desconnectar tots els dispositius?"), + ("Clear", "Netejar"), + ("Audio Input Device", "Dispositiu d'entrada d'àudio"), + ("Deny remote access", "Denegar accés remot"), + ("Use IP Whitelisting", "Utilitza llista de IPs admeses"), + ("Network", "Xarxa"), + ("Enable RDP", "Habilitar RDP"), + ("Pin menubar", "Bloqueja barra de menú"), + ("Unpin menubar", "Desbloquejar barra de menú"), + ("Recording", "Gravant"), + ("Directory", "Directori"), + ("Automatically record incoming sessions", "Gravació automàtica de sessions entrants"), + ("Change", "Canviar"), + ("Start session recording", "Començar gravació de sessió"), + ("Stop session recording", "Aturar gravació de sessió"), + ("Enable Recording Session", "Habilitar gravació de sessió"), + ("Allow recording session", "Permetre gravació de sessió"), + ("Enable LAN Discovery", "Habilitar descobriment de LAN"), + ("Deny LAN Discovery", "Denegar descobriment de LAN"), + ("Write a message", "Escriure un missatge"), + ("Prompt", ""), + ("elevation_prompt", ""), + ("uac_warning", ""), + ("elevated_foreground_window_warning", ""), + ("Disconnected", "Desconnectat"), + ("Other", "Altre"), + ("Confirm before closing multiple tabs", "Confirmar abans de tancar múltiples pestanyes"), + ("Keyboard Settings", "Ajustaments de teclat"), + ("Custom", "Personalitzat"), + ("Full Access", "Acces complet"), + ("Screen Share", "Compartir pantalla"), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland requereix Ubuntu 21.04 o una versió superior."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland requereix una versió superior de la distribución de Linux. Provi l'escriptori X11 o canvïi el seu sistema operatiu."), + ("JumpLink", "Veure"), + ("Please Select the screen to be shared(Operate on the peer side).", "Seleccioni la pantalla que es compartirà (Operar al costat del peer)."), + ("Show RustDesk", "Mostrar RustDesk"), + ("This PC", "Aquest PC"), + ("or", "o"), + ("Continue with", "Continuar amb"), + ].iter().cloned().collect(); +} From 0c854e8c121ebdbfbc8297d72580eabcf6ff4c9c Mon Sep 17 00:00:00 2001 From: ivanbea Date: Wed, 16 Nov 2022 11:28:37 +0100 Subject: [PATCH 0951/2015] update lang.rs to add catalan update lang.rs to add catalan --- src/lang.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/lang.rs b/src/lang.rs index db59e9f54..4579ca893 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -24,6 +24,7 @@ mod vn; mod kz; mod ua; mod fa; +mod ca; lazy_static::lazy_static! { pub static ref LANGS: Value = @@ -51,6 +52,7 @@ lazy_static::lazy_static! { ("kz", "Қазақ"), ("ua", "Українська"), ("fa", "فارسی"), + ("ca", "català"), ]); } @@ -102,6 +104,7 @@ pub fn translate_locale(name: String, locale: &str) -> String { "kz" => kz::T.deref(), "ua" => ua::T.deref(), "fa" => fa::T.deref(), + "ca" => ca::T.deref(), _ => en::T.deref(), }; if let Some(v) = m.get(&name as &str) { From 628dcf89746bd43b3530be3d1c953b7ccc2ccf3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Be=C3=A0?= Date: Wed, 16 Nov 2022 13:36:57 +0100 Subject: [PATCH 0952/2015] Update lang.rs --- src/lang.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang.rs b/src/lang.rs index 4579ca893..30ce2d372 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -52,7 +52,7 @@ lazy_static::lazy_static! { ("kz", "Қазақ"), ("ua", "Українська"), ("fa", "فارسی"), - ("ca", "català"), + ("ca", "Català"), ]); } From c802b1e95dbbe4f329969c1a9d03cad296ed7853 Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 16 Nov 2022 19:49:52 +0800 Subject: [PATCH 0953/2015] fix window border width whenn fullscreen Signed-off-by: fufesou --- flutter/lib/desktop/pages/remote_tab_page.dart | 14 +++++++++----- flutter/lib/models/model.dart | 2 +- flutter/lib/models/state_model.dart | 4 +++- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index 5e49895f4..f6ebc0f86 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -116,12 +116,14 @@ class _ConnectionTabPageState extends State { @override Widget build(BuildContext context) { - final tabWidget = Container( - decoration: BoxDecoration( + final tabWidget = Obx( + () => Container( + decoration: BoxDecoration( border: Border.all( color: MyTheme.color(context).border!, - width: kWindowBorderWidth)), - child: Scaffold( + width: stateGlobal.windowBorderWidth.value), + ), + child: Scaffold( backgroundColor: Theme.of(context).backgroundColor, body: DesktopTab( controller: tabController, @@ -182,7 +184,9 @@ class _ConnectionTabPageState extends State { ); } }), - )), + ), + ), + ), ); return Platform.isMacOS ? tabWidget diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index a074bf266..eca94d5e5 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -593,7 +593,7 @@ class CanvasModel with ChangeNotifier { return parent.target?.ffiModel.display.height ?? defaultHeight; } - double get windowBorderWidth => stateGlobal.windowBorderWidth; + double get windowBorderWidth => stateGlobal.windowBorderWidth.value; double get tabBarHeight => stateGlobal.tabBarHeight; Size get size { diff --git a/flutter/lib/models/state_model.dart b/flutter/lib/models/state_model.dart index dab21fcb6..1ceee2fe0 100644 --- a/flutter/lib/models/state_model.dart +++ b/flutter/lib/models/state_model.dart @@ -8,14 +8,15 @@ class StateGlobal { bool _fullscreen = false; final RxBool _showTabBar = true.obs; final RxDouble _resizeEdgeSize = 8.0.obs; + final RxDouble _windowBorderWidth = RxDouble(kWindowBorderWidth); final RxBool showRemoteMenuBar = false.obs; int get windowId => _windowId; bool get fullscreen => _fullscreen; double get tabBarHeight => fullscreen ? 0 : kDesktopRemoteTabBarHeight; - double get windowBorderWidth => fullscreen ? 0 : kWindowBorderWidth; RxBool get showTabBar => _showTabBar; RxDouble get resizeEdgeSize => _resizeEdgeSize; + RxDouble get windowBorderWidth => _windowBorderWidth; setWindowId(int id) => _windowId = id; setFullscreen(bool v) { @@ -24,6 +25,7 @@ class StateGlobal { _showTabBar.value = !_fullscreen; _resizeEdgeSize.value = fullscreen ? kFullScreenEdgeSize : kWindowEdgeSize; + _windowBorderWidth.value = fullscreen ? 0 : kWindowBorderWidth; WindowController.fromWindowId(windowId).setFullscreen(_fullscreen); } } From 9946a5fc63c58efe218f2c8b5afe516cadc46d0f Mon Sep 17 00:00:00 2001 From: "Miguel F. G" <116861809+flusheDData@users.noreply.github.com> Date: Wed, 16 Nov 2022 17:41:29 +0100 Subject: [PATCH 0954/2015] Update es.rs New items. --- 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 d0c569eff..ca866641a 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -388,7 +388,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("This PC", "Este PC"), ("or", "o"), ("Continue with", "Continuar con"), - ("Elevate", ""), - ("Zoom cursor", ""), + ("Elevate", "Elevar"), + ("Zoom cursor", "Ampliar cursor"), ].iter().cloned().collect(); } From 2e58f072b0d2afbfc841238576b9d06c1af766de Mon Sep 17 00:00:00 2001 From: Kingtous Date: Thu, 17 Nov 2022 10:58:23 +0800 Subject: [PATCH 0955/2015] feat: add RPM build --- .github/workflows/flutter-nightly.yml | 36 ++++++++++++++++++--------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index fdbced00a..62637a259 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -120,6 +120,13 @@ jobs: - name: Checkout source code uses: actions/checkout@v3 + - name: Get build target triple + uses: jungwinter/split@v2 + id: build-target-triple + with: + separator: '-' + msg: ${{ matrix.job.target }} + - name: Install prerequisites run: | case ${{ matrix.job.target }} in @@ -250,19 +257,24 @@ jobs: files: | res/rustdesk*.zst - # - name: build RPM package - # id: rpm - # uses: Kingtous/rustdesk-rpmbuild@master - # with: - # spec_file: "res/rpm-flutter.spec" + - name: Make RPM package + shell: bash + if: ${{ matrix.job.extra-build-args == '' }} + run: | + sudo apt install -y rpm + pushd ~/rpmbuild/RPMS/${{ steps.build-target-triple.outputs._0 }} + for name in rustdesk*??.rpm; do + mv "$name" "${name%%.rpm}-fedora28-centos8.rpm" + done - # - name: Publish fedora28/centos8 package - # uses: softprops/action-gh-release@v1 - # with: - # prerelease: true - # tag_name: ${{ env.TAG_NAME }} - # files: | - # ${{ steps.rpm.outputs.rpm_dir_path }}/* + - name: Publish fedora28/centos8 package + if: ${{ matrix.job.extra-build-args == '' }} + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + ~/rpmbuild/RPMS/${{ steps.build-target-triple.outputs._0 }}/* build-flatpak: name: Build Flatpak From 19ea51d07cb51aab531f6611231825ce40848b43 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Thu, 17 Nov 2022 11:16:27 +0800 Subject: [PATCH 0956/2015] fix: rpm flutter build fix: fedora nightly build --- .github/workflows/flutter-nightly.yml | 3 ++- res/rpm-flutter.spec | 21 ++++++++++----------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 62637a259..f2391e77e 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -262,6 +262,7 @@ jobs: if: ${{ matrix.job.extra-build-args == '' }} run: | sudo apt install -y rpm + HBB=`pwd` rpmbuild ./res/rpm-flutter.spec -bb pushd ~/rpmbuild/RPMS/${{ steps.build-target-triple.outputs._0 }} for name in rustdesk*??.rpm; do mv "$name" "${name%%.rpm}-fedora28-centos8.rpm" @@ -274,7 +275,7 @@ jobs: prerelease: true tag_name: ${{ env.TAG_NAME }} files: | - ~/rpmbuild/RPMS/${{ steps.build-target-triple.outputs._0 }}/* + /home/runner/rpmbuild/RPMS/${{ steps.build-target-triple.outputs._0 }}/*.rpm build-flatpak: name: Build Flatpak diff --git a/res/rpm-flutter.spec b/res/rpm-flutter.spec index a01926baa..2ba1cbfa4 100644 --- a/res/rpm-flutter.spec +++ b/res/rpm-flutter.spec @@ -3,8 +3,8 @@ Version: 1.2.0 Release: 0 Summary: RPM package License: GPL-3.0 -Requires: gtk3 libxcb libxdo libXfixes pipewire alsa-lib curl libappindicator libvdpau1 libva2 - +Requires: gtk3 libxcb libxdo libXfixes pipewire alsa-lib curl libappindicator-gtk3 libvdpau libva +Provides: libdesktop_drop_plugin.so()(64bit), libdesktop_multi_window_plugin.so()(64bit), libflutter_custom_cursor_plugin.so()(64bit), libflutter_linux_gtk.so()(64bit), libscreen_retriever_plugin.so()(64bit), libtray_manager_plugin.so()(64bit), liburl_launcher_linux_plugin.so()(64bit), libwindow_manager_plugin.so()(64bit), libwindow_size_plugin.so()(64bit) %description The best open-source remote desktop client software, written in Rust. @@ -19,17 +19,14 @@ The best open-source remote desktop client software, written in Rust. %install -mkdir -p "${buildroot}/usr/lib/rustdesk" && cp -r ${HBB}/flutter/build/linux/x64/release/bundle/* -t "${buildroot}/usr/lib/rustdesk" -mkdir -p "${buildroot}/usr/bin" -pushd ${buildroot} && ln -s /usr/lib/rustdesk/rustdesk usr/bin/rustdesk && popd -install -Dm 644 $HBB/res/rustdesk.service -t "${buildroot}/usr/share/rustdesk/files" -install -Dm 644 $HBB/res/rustdesk.desktop -t "${buildroot}/usr/share/rustdesk/files" -install -Dm 644 $HBB/res/rustdesk-link.desktop -t "${buildroot}/usr/share/rustdesk/files" -install -Dm 644 $HBB/res/128x128@2x.png "${buildroot}/usr/share/rustdesk/files/rustdesk.png" - +mkdir -p "%{buildroot}/usr/lib/rustdesk" && cp -r ${HBB}/flutter/build/linux/x64/release/bundle/* -t "%{buildroot}/usr/lib/rustdesk" +mkdir -p "%{buildroot}/usr/bin" +install -Dm 644 $HBB/res/rustdesk.service -t "%{buildroot}/usr/share/rustdesk/files" +install -Dm 644 $HBB/res/rustdesk.desktop -t "%{buildroot}/usr/share/rustdesk/files" +install -Dm 644 $HBB/res/rustdesk-link.desktop -t "%{buildroot}/usr/share/rustdesk/files" +install -Dm 644 $HBB/res/128x128@2x.png "%{buildroot}/usr/share/rustdesk/files/rustdesk.png" %files -/usr/bin/rustdesk /usr/lib/rustdesk/* /usr/share/rustdesk/files/rustdesk.service /usr/share/rustdesk/files/rustdesk.png @@ -56,6 +53,7 @@ esac cp /usr/share/rustdesk/files/rustdesk.service /etc/systemd/system/rustdesk.service cp /usr/share/rustdesk/files/rustdesk.desktop /usr/share/applications/ cp /usr/share/rustdesk/files/rustdesk-link.desktop /usr/share/applications/ +ln -s /usr/lib/rustdesk/rustdesk /usr/bin/rustdesk systemctl daemon-reload systemctl enable rustdesk systemctl start rustdesk @@ -80,6 +78,7 @@ case "$1" in # for uninstall rm /usr/share/applications/rustdesk.desktop || true rm /usr/share/applications/rustdesk-link.desktop || true + rm /usr/bin/rustdesk || true update-desktop-database ;; 1) From 403861c3cef67e3b5fe776e677581bf7c2db210f Mon Sep 17 00:00:00 2001 From: Kingtous Date: Thu, 17 Nov 2022 15:54:33 +0800 Subject: [PATCH 0957/2015] fix: multi window existence on taskbar --- flutter/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index ed7fad5dd..e76855f62 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -63,7 +63,7 @@ dependencies: desktop_multi_window: git: url: https://github.com/Kingtous/rustdesk_desktop_multi_window - ref: fc14252a5e32236b68a91df0be12d9950c525069 + ref: 9b4c5ac1aec2c4d1bdabb4dd29e4bc3b75c76a79 freezed_annotation: ^2.0.3 tray_manager: git: From 1e2429b2a9f14da690c29df30eec19f123b9a1b8 Mon Sep 17 00:00:00 2001 From: KoalaBear84 Date: Thu, 17 Nov 2022 08:57:53 +0100 Subject: [PATCH 0958/2015] Update desktop_setting_page.dart MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use real copyright symbol © --- 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 ab8f76ca5..009b784ad 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -1075,7 +1075,7 @@ class _AboutState extends State<_About> { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Copyright © 2022 Purslane Ltd.\n$license', + 'Copyright © 2022 Purslane Ltd.\n$license', style: const TextStyle(color: Colors.white), ), const Text( From d24e7b25ab0cad68f3d1acb2d2ddc83c6ce9aaa5 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Thu, 17 Nov 2022 16:36:07 +0800 Subject: [PATCH 0959/2015] feat: add build date --- Cargo.toml | 1 + flutter/lib/desktop/pages/desktop_setting_page.dart | 5 ++++- libs/hbb_common/src/lib.rs | 10 +++++++--- src/flutter_ffi.rs | 4 ++++ 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 689e1a989..836bd07d4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -67,6 +67,7 @@ rdev = { git = "https://github.com/asur4s/rdev" } url = { version = "2.1", features = ["serde"] } reqwest = { version = "0.11", features = ["blocking", "json", "rustls-tls"], default-features=false } +chrono = "0.4.23" [target.'cfg(not(any(target_os = "android", target_os = "linux")))'.dependencies] cpal = "0.13.5" diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 009b784ad..b4c69a6a6 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -1029,10 +1029,12 @@ class _AboutState extends State<_About> { return _futureBuilder(future: () async { final license = await bind.mainGetLicense(); final version = await bind.mainGetVersion(); - return {'license': license, 'version': version}; + final buildDate = await bind.mainGetBuildDate(); + return {'license': license, 'version': version, 'buildDate': buildDate}; }(), hasData: (data) { final license = data['license'].toString(); final version = data['version'].toString(); + final buildDate = data['buildDate'].toString(); const linkStyle = TextStyle(decoration: TextDecoration.underline); final scrollController = ScrollController(); return DesktopScrollWrapper( @@ -1048,6 +1050,7 @@ class _AboutState extends State<_About> { height: 8.0, ), Text('Version: $version').marginSymmetric(vertical: 4.0), + Text('Build Date: $buildDate').marginSymmetric(vertical: 4.0), InkWell( onTap: () { launchUrlString('https://rustdesk.com/privacy'); diff --git a/libs/hbb_common/src/lib.rs b/libs/hbb_common/src/lib.rs index 02acfd9ff..ae564685f 100644 --- a/libs/hbb_common/src/lib.rs +++ b/libs/hbb_common/src/lib.rs @@ -161,19 +161,23 @@ pub fn get_version_from_url(url: &str) -> String { } pub fn gen_version() { + use std::io::prelude::*; let mut file = File::create("./src/version.rs").unwrap(); for line in read_lines("Cargo.toml").unwrap() { if let Ok(line) = line { let ab: Vec<&str> = line.split("=").map(|x| x.trim()).collect(); if ab.len() == 2 && ab[0] == "version" { - use std::io::prelude::*; - file.write_all(format!("pub const VERSION: &str = {};", ab[1]).as_bytes()) + file.write_all(format!("pub const VERSION: &str = {};\n", ab[1]).as_bytes()) .ok(); - file.sync_all().ok(); break; } } } + // generate build date + let build_date = format!("{}", chrono::Local::now().format("%Y-%m-%d %H:%M")); + file.write_all(format!("pub const BUILD_DATE: &str = \"{}\";", build_date).as_bytes()) + .ok(); + file.sync_all().ok(); } fn read_lines

    (filename: P) -> io::Result>> diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 856c4ed21..8627168a4 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1025,6 +1025,10 @@ pub fn main_get_icon() -> String { return String::new(); } +pub fn main_get_build_date() -> String { + crate::BUILD_DATE.to_string() +} + #[no_mangle] unsafe extern "C" fn translate(name: *const c_char, locale: *const c_char) -> *const c_char { let name = CStr::from_ptr(name); From d804ae4e230c5290e335613b548175422bdf7243 Mon Sep 17 00:00:00 2001 From: neoGalaxy88 Date: Thu, 17 Nov 2022 11:53:49 +0100 Subject: [PATCH 0960/2015] Update it.rs --- src/lang/it.rs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/lang/it.rs b/src/lang/it.rs index fc6b936bf..731b5643d 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -113,12 +113,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stretch", "Allarga"), ("Scrollbar", "Barra di scorrimento"), ("ScrollAuto", "Scorri automaticamente"), - ("Good image quality", "Buona qualità immagine"), + ("Good image quality", "Qualità immagine migliore"), ("Balanced", "Bilanciato"), ("Optimize reaction time", "Ottimizza il tempo di reazione"), - ("Custom", ""), + ("Custom", "Personalizza"), ("Show remote cursor", "Mostra il cursore remoto"), - ("Show quality monitor", ""), + ("Show quality monitor", "Visualizza qualità video"), ("Disable clipboard", "Disabilita appunti"), ("Lock after session end", "Blocca al termine della sessione"), ("Insert", "Inserisci"), @@ -161,8 +161,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Action", "Azione"), ("Add", "Aggiungi"), ("Local Port", "Porta locale"), - ("Local Address", ""), - ("Change Local Port", ""), + ("Local Address", "Indirizzo locale"), + ("Change Local Port", "Cambia porta locale"), ("setup_server_tip", "Per una connessione più veloce, configura un tuo server"), ("Too short, at least 6 characters.", "Troppo breve, almeno 6 caratteri"), ("The confirmation is not identical.", "La conferma non corrisponde"), @@ -197,7 +197,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Reboot required", "Riavvio necessario"), ("Unsupported display server ", "Display server non supportato"), ("x11 expected", "x11 necessario"), - ("Port", ""), + ("Port", "Porta"), ("Settings", "Impostazioni"), ("Username", " Nome utente"), ("Invalid port", "Porta non valida"), @@ -314,7 +314,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Are you sure you want to restart", "Sei sicuro di voler riavviare?"), ("Restarting Remote Device", "Il dispositivo remoto si sta riavviando"), ("remote_restarting_tip", "Riavviare il dispositivo remoto"), - ("Copied", ""), + ("Copied", "Copiato"), ("Exit Fullscreen", "Esci dalla modalità schermo intero"), ("Fullscreen", "A schermo intero"), ("Mobile Actions", "Azioni mobili"), @@ -332,11 +332,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Insecure Connection", "Connessione insicura"), ("Scale original", "Scala originale"), ("Scale adaptive", "Scala adattiva"), - ("General", ""), - ("Security", ""), - ("Account", ""), - ("Theme", ""), - ("Dark Theme", ""), + ("General", "Generale"), + ("Security", "Sicurezza"), + ("Account", "Account"), + ("Theme", "Tema"), + ("Dark Theme", "Tema Scuro"), ("Dark", ""), ("Light", ""), ("Follow System", ""), From 75d8168070b76c4f9051817b6b6eea3abdc05a2e Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 17 Nov 2022 18:52:27 +0800 Subject: [PATCH 0961/2015] enable rust default option Signed-off-by: fufesou --- .../lib/desktop/pages/remote_tab_page.dart | 11 +-- .../lib/desktop/widgets/remote_menubar.dart | 96 +++++++------------ flutter/lib/mobile/pages/remote_page.dart | 4 +- flutter/lib/models/model.dart | 6 +- libs/hbb_common/src/config.rs | 62 +++++++++++- src/client.rs | 15 ++- src/flutter_ffi.rs | 30 +++++- src/server/video_service.rs | 8 +- src/ui_session_interface.rs | 12 ++- 9 files changed, 162 insertions(+), 82 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index f6ebc0f86..df8256496 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -246,13 +246,12 @@ class _ConnectionTabPageState extends State { dismissOnClicked: true, ), ], - curOptionGetter: () async { - return await bind.sessionGetOption(id: key, arg: 'view-style') ?? - 'adaptive'; - }, + curOptionGetter: () async => + // null means peer id is not found, which there's no need to care about + await bind.sessionGetViewStyle(id: key) ?? '', optionSetter: (String oldValue, String newValue) async { - await bind.sessionPeerOption( - id: key, name: "view-style", value: newValue); + await bind.sessionSetViewStyle( + id: key, value: newValue); ffi.canvasModel.updateViewStyle(); cancelFunc(); }, diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 6db5a7fb7..ed69f3e65 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -22,7 +22,7 @@ import './popup_menu.dart'; import './material_mod_popup_menu.dart' as mod_menu; class MenubarState { - final kStoreKey = "remoteMenubarState"; + final kStoreKey = 'remoteMenubarState'; late RxBool show; late RxBool _pin; @@ -195,7 +195,7 @@ class _RemoteMenubarState extends State { } _updateScreen() async { - final v = await DesktopMultiWindow.invokeMethod(0, "get_window_info", ""); + final v = await DesktopMultiWindow.invokeMethod(0, 'get_window_info', ''); final String valueStr = v; if (valueStr.isEmpty) { _screen = null; @@ -322,7 +322,7 @@ class _RemoteMenubarState extends State { child: Obx(() { RxInt display = CurrentDisplayState.find(widget.id); return Text( - "${display.value + 1}/${pi.displays.length}", + '${display.value + 1}/${pi.displays.length}', style: const TextStyle( color: _MenubarTheme.commonColor, fontSize: 8), ); @@ -595,10 +595,10 @@ class _RemoteMenubarState extends State { )); } } - if (perms["restart"] != false && - (pi.platform == "Linux" || - pi.platform == "Windows" || - pi.platform == "Mac OS")) { + if (perms['restart'] != false && + (pi.platform == 'Linux' || + pi.platform == 'Windows' || + pi.platform == 'Mac OS')) { displayMenu.add(MenuEntryButton( childBuilder: (TextStyle? style) => Text( translate('Restart Remote Device'), @@ -629,14 +629,14 @@ class _RemoteMenubarState extends State { displayMenu.add(MenuEntryButton( childBuilder: (TextStyle? style) => Obx(() => Text( translate( - '${BlockInputState.find(widget.id).value ? "Unb" : "B"}lock user input'), + '${BlockInputState.find(widget.id).value ? 'Unb' : 'B'}lock user input'), style: style, )), proc: () { RxBool blockInput = BlockInputState.find(widget.id); bind.sessionToggleOption( id: widget.id, - value: '${blockInput.value ? "un" : ""}block-input'); + value: '${blockInput.value ? 'un' : ''}block-input'); blockInput.value = !blockInput.value; }, padding: padding, @@ -671,7 +671,7 @@ class _RemoteMenubarState extends State { // ClipboardData? data = // await Clipboard.getData(Clipboard.kTextPlain); // if (data != null && data.text != null) { - // bind.sessionInputString(id: widget.id, value: data.text ?? ""); + // bind.sessionInputString(id: widget.id, value: data.text ?? ''); // } // }(); // }, @@ -679,18 +679,6 @@ class _RemoteMenubarState extends State { // dismissOnClicked: true, // )); // } - - displayMenu.add(MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - translate('Reset canvas'), - style: style, - ), - proc: () { - widget.ffi.cursorModel.reset(); - }, - padding: padding, - dismissOnClicked: true, - )); } return displayMenu; @@ -740,14 +728,11 @@ class _RemoteMenubarState extends State { dismissOnClicked: true, ), ], - curOptionGetter: () async { - return await bind.sessionGetOption( - id: widget.id, arg: 'view-style') ?? - 'adaptive'; - }, + curOptionGetter: () async => + // null means peer id is not found, which there's no need to care about + await bind.sessionGetViewStyle(id: widget.id) ?? '', optionSetter: (String oldValue, String newValue) async { - await bind.sessionPeerOption( - id: widget.id, name: "view-style", value: newValue); + await bind.sessionSetViewStyle(id: widget.id, value: newValue); widget.ffi.canvasModel.updateViewStyle(); }, padding: padding, @@ -768,14 +753,11 @@ class _RemoteMenubarState extends State { dismissOnClicked: true, ), ], - curOptionGetter: () async { - return await bind.sessionGetOption( - id: widget.id, arg: 'scroll-style') ?? - ''; - }, + curOptionGetter: () async => + // null means peer id is not found, which there's no need to care about + await bind.sessionGetScrollStyle(id: widget.id) ?? '', optionSetter: (String oldValue, String newValue) async { - await bind.sessionPeerOption( - id: widget.id, name: "scroll-style", value: newValue); + await bind.sessionSetScrollStyle(id: widget.id, value: newValue); widget.ffi.canvasModel.updateScrollStyle(); }, padding: padding, @@ -805,12 +787,9 @@ class _RemoteMenubarState extends State { value: 'custom', dismissOnClicked: true), ], - curOptionGetter: () async { - String quality = - await bind.sessionGetImageQuality(id: widget.id) ?? 'balanced'; - if (quality == '') quality = 'balanced'; - return quality; - }, + curOptionGetter: () async => + // null means peer id is not found, which there's no need to care about + await bind.sessionGetImageQuality(id: widget.id) ?? '', optionSetter: (String oldValue, String newValue) async { if (oldValue != newValue) { await bind.sessionSetImageQuality(id: widget.id, value: newValue); @@ -1075,14 +1054,14 @@ class _RemoteMenubarState extends State { } return list; }, - curOptionGetter: () async { - return await bind.sessionGetOption( - id: widget.id, arg: 'codec-preference') ?? - 'auto'; - }, + curOptionGetter: () async => + // null means peer id is not found, which there's no need to care about + await bind.sessionGetOption( + id: widget.id, arg: 'codec-preference') ?? + '', optionSetter: (String oldValue, String newValue) async { await bind.sessionPeerOption( - id: widget.id, name: "codec-preference", value: newValue); + id: widget.id, name: 'codec-preference', value: newValue); bind.sessionChangePreferCodec(id: widget.id); }, padding: padding, @@ -1195,9 +1174,8 @@ class _RemoteMenubarState extends State { MenuEntryRadioOption(text: translate('Legacy mode'), value: 'legacy'), MenuEntryRadioOption(text: translate('Map mode'), value: 'map'), ], - curOptionGetter: () async { - return await bind.sessionGetKeyboardName(id: widget.id); - }, + curOptionGetter: () async => + await bind.sessionGetKeyboardName(id: widget.id), optionSetter: (String oldValue, String newValue) async { await bind.sessionSetKeyboardMode( id: widget.id, keyboardMode: newValue); @@ -1229,16 +1207,16 @@ class _RemoteMenubarState extends State { void showSetOSPassword( String id, bool login, OverlayDialogManager dialogManager) async { final controller = TextEditingController(); - var password = await bind.sessionGetOption(id: id, arg: "os-password") ?? ""; - var autoLogin = await bind.sessionGetOption(id: id, arg: "auto-login") != ""; + var password = await bind.sessionGetOption(id: id, arg: 'os-password') ?? ''; + var autoLogin = await bind.sessionGetOption(id: id, arg: 'auto-login') != ''; controller.text = password; dialogManager.show((setState, close) { submit() { var text = controller.text.trim(); - bind.sessionPeerOption(id: id, name: "os-password", value: text); + bind.sessionPeerOption(id: id, name: 'os-password', value: text); bind.sessionPeerOption( - id: id, name: "auto-login", value: autoLogin ? 'Y' : ''); - if (text != "" && login) { + id: id, name: 'auto-login', value: autoLogin ? 'Y' : ''); + if (text != '' && login) { bind.sessionInputOsPassword(id: id, value: text); } close(); @@ -1285,7 +1263,7 @@ void showAuditDialog(String id, dialogManager) async { dialogManager.show((setState, close) { submit() { var text = controller.text.trim(); - if (text != "") { + if (text != '') { bind.sessionSendNote(id: id, note: text); } close(); @@ -1324,11 +1302,11 @@ void showAuditDialog(String id, dialogManager) async { keyboardType: TextInputType.multiline, textInputAction: TextInputAction.newline, decoration: const InputDecoration.collapsed( - hintText: "input note here", + hintText: 'input note here', ), // inputFormatters: [ // LengthLimitingTextInputFormatter(16), - // // FilteringTextInputFormatter(RegExp(r"[a-zA-z][a-zA-z0-9\_]*"), allow: true) + // // FilteringTextInputFormatter(RegExp(r'[a-zA-z][a-zA-z0-9\_]*'), allow: true) // ], maxLines: null, maxLength: 256, diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 719b7dc28..b48e9960a 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -474,7 +474,7 @@ class _RemotePageState extends State { }, onTwoFingerScaleEnd: (d) { _scale = 1; - bind.sessionPeerOption(id: widget.id, name: "view-style", value: ""); + bind.sessionSetViewStyle(id: widget.id, value: ""); }, onThreeFingerVerticalDragUpdate: gFFI.ffiModel.isPeerAndroid ? null @@ -1001,7 +1001,7 @@ void showOptions( setState(() { viewStyle = value; bind - .sessionPeerOption(id: id, name: "view-style", value: value) + .sessionSetViewStyle(id: id, value: value) .then((_) => gFFI.canvasModel.updateViewStyle()); }); } diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index eca94d5e5..cae4485cb 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -413,7 +413,7 @@ class ImageModel with ChangeNotifier { await initializeCursorAndCanvas(parent.target!); } if (parent.target?.ffiModel.isPeerAndroid ?? false) { - bind.sessionPeerOption(id: id, name: 'view-style', value: 'adaptive'); + bind.sessionSetViewStyle(id: id, value: 'adaptive'); parent.target?.canvasModel.updateViewStyle(); } } @@ -535,7 +535,7 @@ class CanvasModel with ChangeNotifier { double get scrollY => _scrollY; updateViewStyle() async { - final style = await bind.sessionGetOption(id: id, arg: 'view-style'); + final style = await bind.sessionGetViewStyle(id: id); if (style == null) { return; } @@ -561,7 +561,7 @@ class CanvasModel with ChangeNotifier { } updateScrollStyle() async { - final style = await bind.sessionGetOption(id: id, arg: 'scroll-style'); + final style = await bind.sessionGetScrollStyle(id: id); if (style == 'scrollbar') { _scrollStyle = ScrollStyle.scrollbar; _scrollX = 0.0; diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index a7836cef7..f4b6661b4 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -9,6 +9,7 @@ use std::{ use anyhow::Result; use rand::Rng; +use serde as de; use serde_derive::{Deserialize, Serialize}; use sodiumoxide::crypto::sign; @@ -79,6 +80,26 @@ pub const RS_PUB_KEY: &'static str = "OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmB pub const RENDEZVOUS_PORT: i32 = 21116; pub const RELAY_PORT: i32 = 21117; +macro_rules! serde_field_string { + ($default_func:ident, $de_func:ident, $default_expr:expr) => { + fn $default_func() -> String { + $default_expr + } + + fn $de_func<'de, D>(deserializer: D) -> Result + where + D: de::Deserializer<'de>, + { + let s: &str = de::Deserialize::deserialize(deserializer)?; + Ok(if s.is_empty() { + Self::$default_func() + } else { + s.to_owned() + }) + } + }; +} + #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum NetworkType { Direct, @@ -141,9 +162,20 @@ pub struct PeerConfig { pub size_ft: Size, #[serde(default)] pub size_pf: Size, - #[serde(default)] - pub view_style: String, // original (default), scale - #[serde(default)] + #[serde( + default = "PeerConfig::default_view_style", + deserialize_with = "PeerConfig::deserialize_view_style" + )] + pub view_style: String, + #[serde( + default = "PeerConfig::default_scroll_style", + deserialize_with = "PeerConfig::deserialize_scroll_style" + )] + pub scroll_style: String, + #[serde( + default = "PeerConfig::default_image_quality", + deserialize_with = "PeerConfig::deserialize_image_quality" + )] pub image_quality: String, #[serde(default)] pub custom_image_quality: Vec, @@ -167,7 +199,10 @@ pub struct PeerConfig { pub show_quality_monitor: bool, // The other scalar value must before this - #[serde(default)] + #[serde( + default, + deserialize_with = "PeerConfig::deserialize_options" + )] pub options: HashMap, // Various data for flutter ui #[serde(default)] @@ -400,7 +435,9 @@ impl Config { #[cfg(target_os = "macos")] let org = ORG.read().unwrap().clone(); // /var/root for root - if let Some(project) = directories_next::ProjectDirs::from("", &org, &*APP_NAME.read().unwrap()) { + if let Some(project) = + directories_next::ProjectDirs::from("", &org, &*APP_NAME.read().unwrap()) + { let mut path = patch(project.config_dir().to_path_buf()); path.push(p); return path; @@ -896,6 +933,21 @@ impl PeerConfig { } Default::default() } + + serde_field_string!(default_view_style, deserialize_view_style, "original".to_owned()); + serde_field_string!(default_scroll_style, deserialize_scroll_style, "scrollauto".to_owned()); + serde_field_string!(default_image_quality, deserialize_image_quality, "balanced".to_owned()); + + fn deserialize_options<'de, D>(deserializer: D) -> Result, D::Error> + where + D: de::Deserializer<'de>, + { + let mut mp: HashMap = de::Deserialize::deserialize(deserializer)?; + if !mp.contains_key("codec-preference") { + mp.insert("codec-preference".to_owned(), "auto".to_owned()); + } + Ok(mp) + } } #[derive(Debug, Default, Serialize, Deserialize, Clone)] diff --git a/src/client.rs b/src/client.rs index a938702b3..657c50572 100644 --- a/src/client.rs +++ b/src/client.rs @@ -974,6 +974,8 @@ impl LoginConfigHandler { self.save_config(config); } + //to-do: too many dup code below. + /// Save view style to the current config. /// /// # Arguments @@ -985,13 +987,24 @@ impl LoginConfigHandler { self.save_config(config); } + /// Save scroll style to the current config. + /// + /// # Arguments + /// + /// * `value` - The view style to be saved. + pub fn save_scroll_style(&mut self, value: String) { + let mut config = self.load_config(); + config.scroll_style = value; + self.save_config(config); + } + /// Set a ui config of flutter for handler's [`PeerConfig`]. /// /// # Arguments /// /// * `k` - key of option /// * `v` - value of option - pub fn set_ui_flutter(&mut self, k: String, v: String) { + pub fn save_ui_flutter(&mut self, k: String, v: String) { let mut config = self.load_config(); config.ui_flutter.insert(k, v); self.save_config(config); diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 8627168a4..520efea70 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -170,7 +170,7 @@ pub fn session_get_flutter_config(id: String, k: String) -> Option { pub fn session_set_flutter_config(id: String, k: String, v: String) { if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) { - session.set_flutter_config(k, v); + session.save_flutter_config(k, v); } } @@ -182,6 +182,34 @@ pub fn set_local_flutter_config(k: String, v: String) { ui_interface::set_local_flutter_config(k, v); } +pub fn session_get_view_style(id: String) -> Option { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + Some(session.get_view_style()) + } else { + None + } +} + +pub fn session_set_view_style(id: String, value: String) { + if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) { + session.save_view_style(value); + } +} + +pub fn session_get_scroll_style(id: String) -> Option { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + Some(session.get_scroll_style()) + } else { + None + } +} + +pub fn session_set_scroll_style(id: String, value: String) { + if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) { + session.save_scroll_style(value); + } +} + pub fn session_get_image_quality(id: String) -> Option { if let Some(session) = SESSIONS.read().unwrap().get(&id) { Some(session.get_image_quality()) diff --git a/src/server/video_service.rs b/src/server/video_service.rs index 43ce013f5..3ccc3af39 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -25,12 +25,16 @@ use hbb_common::tokio::sync::{ mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}, Mutex as TokioMutex, }; +#[cfg(not(windows))] +use scrap::Capturer; use scrap::{ codec::{Encoder, EncoderCfg, HwEncoderConfig}, record::{Recorder, RecorderContext}, vpxcodec::{VpxEncoderConfig, VpxVideoCodecId}, - Capturer, Display, TraitCapturer, + Display, TraitCapturer, }; +#[cfg(windows)] +use std::sync::Once; use std::{ collections::HashSet, io::ErrorKind::WouldBlock, @@ -38,8 +42,6 @@ use std::{ time::{self, Duration, Instant}, }; #[cfg(windows)] -use std::sync::Once; -#[cfg(windows)] use virtual_display; pub const SCRAP_UBUNTU_HIGHER_REQUIRED: &str = "Wayland requires Ubuntu 21.04 or higher version."; diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 5119a6e1b..9b00730e5 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -79,6 +79,10 @@ impl Session { self.lc.read().unwrap().view_style.clone() } + pub fn get_scroll_style(&self) -> String { + self.lc.read().unwrap().scroll_style.clone() + } + pub fn get_image_quality(&self) -> String { self.lc.read().unwrap().image_quality.clone() } @@ -99,8 +103,12 @@ impl Session { self.lc.write().unwrap().save_view_style(value); } - pub fn set_flutter_config(&mut self, k: String, v: String) { - self.lc.write().unwrap().set_ui_flutter(k, v); + pub fn save_scroll_style(&mut self, value: String) { + self.lc.write().unwrap().save_scroll_style(value); + } + + pub fn save_flutter_config(&mut self, k: String, v: String) { + self.lc.write().unwrap().save_ui_flutter(k, v); } pub fn get_flutter_config(&self, k: String) -> String { From 4963dcf673e886923b3dd3351434b0e10042d3fa Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 17 Nov 2022 19:32:59 +0800 Subject: [PATCH 0962/2015] fix init custom_fps option Signed-off-by: fufesou --- src/client.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/client.rs b/src/client.rs index 657c50572..c98561967 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1140,6 +1140,9 @@ impl LoginConfigHandler { msg.custom_image_quality = quality << 8; n += 1; } + if let Some(custom_fps) = self.options.get("custom-fps") { + msg.custom_fps = custom_fps.parse().unwrap_or(30); + } if self.get_toggle_option("show-remote-cursor") { msg.show_remote_cursor = BoolOption::Yes.into(); n += 1; From 4bd1a39ac461605e27144cacb0401a40740e7f03 Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 17 Nov 2022 20:24:17 +0800 Subject: [PATCH 0963/2015] fix big init resizeEdgeSize Signed-off-by: fufesou --- Cargo.lock | 5 +++-- flutter/lib/models/state_model.dart | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 67a471cef..49fcc3dce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -620,9 +620,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.22" +version = "0.4.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1" +checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" dependencies = [ "iana-time-zone", "js-sys", @@ -4362,6 +4362,7 @@ dependencies = [ "bytes", "cc", "cfg-if 1.0.0", + "chrono", "clap 3.2.17", "clipboard", "cocoa", diff --git a/flutter/lib/models/state_model.dart b/flutter/lib/models/state_model.dart index 1ceee2fe0..bc80fdf0e 100644 --- a/flutter/lib/models/state_model.dart +++ b/flutter/lib/models/state_model.dart @@ -7,7 +7,7 @@ class StateGlobal { int _windowId = -1; bool _fullscreen = false; final RxBool _showTabBar = true.obs; - final RxDouble _resizeEdgeSize = 8.0.obs; + final RxDouble _resizeEdgeSize = RxDouble(kWindowEdgeSize); final RxDouble _windowBorderWidth = RxDouble(kWindowBorderWidth); final RxBool showRemoteMenuBar = false.obs; From eab059d3de7dc11a03722430dbff56bff7280fa6 Mon Sep 17 00:00:00 2001 From: solokot Date: Thu, 17 Nov 2022 16:06:36 +0300 Subject: [PATCH 0964/2015] Updated ru.rs --- src/lang/ru.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/lang/ru.rs b/src/lang/ru.rs index b5d747f9d..4920c2826 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -136,7 +136,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to make direct connection to remote desktop", "Не удалось установить прямое подключение к удалённому рабочему столу"), ("Set Password", "Установить пароль"), ("OS Password", "Пароль ОС"), - ("install_tip", "В некоторых случаях из-за UAC RustDesk может работать некорректно на удалённом узле. Чтобы избежать UAC, нажмите кнопку ниже, чтобы установить RustDesk в системе."), + ("install_tip", "В некоторых случаях из-за UAC RustDesk может работать неправильно на удалённом узле. Чтобы избежать UAC, нажмите кнопку ниже, чтобы установить RustDesk в системе."), ("Click to upgrade", "Нажмите, чтобы проверить наличие обновлений"), ("Click to download", "Нажмите, чтобы скачать"), ("Click to update", "Нажмите, чтобы обновить"), @@ -371,8 +371,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Deny LAN Discovery", "Запретить обнаружение в локальной сети"), ("Write a message", "Написать сообщение"), ("Prompt", "Подсказка"), - ("Please wait for confirmation of UAC...", ""), - ("elevated_foreground_window_tip", ""), + ("Please wait for confirmation of UAC...", "Дождитесь подтверждения UAC..."), + ("elevated_foreground_window_tip", "Текущее окно удалённого рабочего стола требует более высоких привилегий для работы, поэтому временно невозможно использовать мышь и клавиатуру. Можно попросить удалённого пользователя свернуть текущее окно или нажать кнопку повышения прав в окне управления подключением. Чтобы избежать этой проблемы в дальнейшем, рекомендуется выполнить установку программного обеспечения на удалённом устройстве."), ("Disconnected", "Отключено"), ("Other", "Другое"), ("Confirm before closing multiple tabs", "Подтверждение закрытия несколько вкладок"), @@ -388,7 +388,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("This PC", "Этот компьютер"), ("or", "или"), ("Continue with", "Продолжить с"), - ("Elevate", ""), - ("Zoom cursor", ""), + ("Elevate", "Повысить"), + ("Zoom cursor", "Масштабировать курсор"), ].iter().cloned().collect(); } From 752d84ffb0fca52bd9bb1f2691c685fab5c7712e Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 18 Nov 2022 10:19:55 +0800 Subject: [PATCH 0965/2015] fix remember peer card view type Signed-off-by: fufesou --- flutter/lib/common/widgets/peer_tab_page.dart | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/flutter/lib/common/widgets/peer_tab_page.dart b/flutter/lib/common/widgets/peer_tab_page.dart index 9129e4711..523230810 100644 --- a/flutter/lib/common/widgets/peer_tab_page.dart +++ b/flutter/lib/common/widgets/peer_tab_page.dart @@ -28,14 +28,16 @@ class _PeerTabPageState extends State setPeer() { final index = bind.getLocalFlutterConfig(k: 'peer-tab-index'); - if (index == '') return; - _tabIndex.value = int.parse(index); + if (index != '') { + _tabIndex.value = int.parse(index); + } final uiType = bind.getLocalFlutterConfig(k: 'peer-card-ui-type'); - if (uiType == '') return; - peerCardUiType.value = int.parse(uiType) == PeerUiType.list.index - ? PeerUiType.list - : PeerUiType.grid; + if (uiType != '') { + peerCardUiType.value = int.parse(uiType) == PeerUiType.list.index + ? PeerUiType.list + : PeerUiType.grid; + } } // hard code for now From ea5bff63fc8b3718fe9bf70500cf094879e58e85 Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 18 Nov 2022 10:21:25 +0800 Subject: [PATCH 0966/2015] remove unused sized box widget Signed-off-by: fufesou --- flutter/lib/desktop/pages/remote_page.dart | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 117a0ab02..1aca198c2 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -321,12 +321,10 @@ class _ImagePaintState extends State { if (c.scrollStyle == ScrollStyle.scrollbar) { final imageWidth = c.getDisplayWidth() * s; final imageHeight = c.getDisplayHeight() * s; - final imageWidget = SizedBox( - width: imageWidth, - height: imageHeight, - child: CustomPaint( - painter: ImagePainter(image: m.image, x: 0, y: 0, scale: s), - )); + final imageWidget = CustomPaint( + size: Size(imageWidth, imageHeight), + painter: ImagePainter(image: m.image, x: 0, y: 0, scale: s), + ); return NotificationListener( onNotification: (notification) { @@ -350,13 +348,10 @@ class _ImagePaintState extends State { Size(imageWidth, imageHeight))), ); } else { - final imageWidget = SizedBox( - width: c.size.width, - height: c.size.height, - child: CustomPaint( - painter: - ImagePainter(image: m.image, x: c.x / s, y: c.y / s, scale: s), - )); + final imageWidget = CustomPaint( + size: Size(c.size.width, c.size.height), + painter: ImagePainter(image: m.image, x: c.x / s, y: c.y / s, scale: s), + ); return mouseRegion(child: _buildListener(imageWidget)); } } From 3ad46908e9b874fd0b1fef0b1125e4e01828e4c5 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Thu, 17 Nov 2022 17:26:46 +0800 Subject: [PATCH 0967/2015] fix: window overflow after restore from fullscreen related: https://github.com/leanflutter/window_manager/issues/131 --- flutter/lib/models/state_model.dart | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/flutter/lib/models/state_model.dart b/flutter/lib/models/state_model.dart index bc80fdf0e..53f1a19b1 100644 --- a/flutter/lib/models/state_model.dart +++ b/flutter/lib/models/state_model.dart @@ -1,4 +1,7 @@ +import 'dart:io'; + import 'package:desktop_multi_window/desktop_multi_window.dart'; +import 'package:flutter/material.dart'; import 'package:get/get.dart'; import '../consts.dart'; @@ -26,7 +29,20 @@ class StateGlobal { _resizeEdgeSize.value = fullscreen ? kFullScreenEdgeSize : kWindowEdgeSize; _windowBorderWidth.value = fullscreen ? 0 : kWindowBorderWidth; - WindowController.fromWindowId(windowId).setFullscreen(_fullscreen); + WindowController.fromWindowId(windowId) + .setFullscreen(_fullscreen) + .then((_) { + // https://github.com/leanflutter/window_manager/issues/131#issuecomment-1111587982 + if (Platform.isWindows && !v) { + Future.delayed(Duration.zero, () async { + final frame = + await WindowController.fromWindowId(windowId).getFrame(); + final newRect = Rect.fromLTWH( + frame.left, frame.top, frame.width + 1, frame.height + 1); + await WindowController.fromWindowId(windowId).setFrame(newRect); + }); + } + }); } } From 931ebc86bc2f0a25f5a819109de35d608ce84fbb Mon Sep 17 00:00:00 2001 From: 21pages Date: Fri, 18 Nov 2022 11:56:32 +0800 Subject: [PATCH 0968/2015] fix api server setting error Signed-off-by: 21pages --- 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 b4c69a6a6..c03371a64 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -864,7 +864,7 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin { } } if (apiServer.isNotEmpty) { - if (!apiServer.startsWith('http://') || + if (!apiServer.startsWith('http://') && !apiServer.startsWith('https://')) { apiErrMsg.value = '${translate("API Server")}: ${translate("invalid_http")}'; From 01997704542aa9c49274f0dde91196a99bcbec96 Mon Sep 17 00:00:00 2001 From: 21pages Date: Wed, 16 Nov 2022 20:32:22 +0800 Subject: [PATCH 0969/2015] portable-service: add quick_start feature and ci Signed-off-by: 21pages --- .github/workflows/flutter-nightly.yml | 9 +++++---- Cargo.toml | 1 + build.py | 7 +++++++ src/core_main.rs | 9 ++++++++- src/platform/windows.rs | 7 +++++++ src/server/portable_service.rs | 7 +++++-- 6 files changed, 33 insertions(+), 7 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index f2391e77e..074eafe08 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -15,7 +15,7 @@ env: jobs: build-for-windows: - name: ${{ matrix.job.target }} (${{ matrix.job.os }}) + name: ${{ matrix.job.target }} (${{ matrix.job.os }}) ${{ matrix.job.suffix }} runs-on: ${{ matrix.job.os }} strategy: fail-fast: false @@ -23,7 +23,8 @@ jobs: job: # - { target: i686-pc-windows-msvc , os: windows-2019 } # - { target: x86_64-pc-windows-gnu , os: windows-2019 } - - { target: x86_64-pc-windows-msvc , os: windows-2019 } + - { target: x86_64-pc-windows-msvc , os: windows-2019, suffix: "" , extra-build-args: "" } + - { target: x86_64-pc-windows-msvc , os: windows-2019, suffix: "-qs", extra-build-args: "--quick_start" } steps: - name: Checkout source code uses: actions/checkout@v3 @@ -83,13 +84,13 @@ jobs: shell: bash - name: Build rustdesk - run: python3 .\build.py --portable --hwcodec --flutter + run: python3 .\build.py --portable --hwcodec --flutter ${{ matrix.job.extra-build-args }} - name: Rename rustdesk shell: bash run: | for name in rustdesk*??-install.exe; do - mv "$name" "${name%%-install.exe}-${{ matrix.job.target }}.exe" + mv "$name" "${name%%-install.exe}-${{ matrix.job.target }}${{ matrix.job.suffix }}.exe" done - name: Publish Release diff --git a/Cargo.toml b/Cargo.toml index 836bd07d4..44df74952 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ flutter = ["flutter_rust_bridge"] default = ["use_dasp"] hwcodec = ["scrap/hwcodec"] mediacodec = ["scrap/mediacodec"] +quick_start = [] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/build.py b/build.py index c907334ce..a887ff070 100755 --- a/build.py +++ b/build.py @@ -81,6 +81,11 @@ def make_parser(): action='store_true', help='Build windows portable' ) + parser.add_argument( + '--quick_start', + action='store_true', + help='Windows quick start portable' + ) parser.add_argument( '--flatpak', action='store_true', @@ -189,6 +194,8 @@ def get_features(args): features = ['inline'] if windows: features.extend(get_rc_features(args)) + if args.quick_start: + features.append('quick_start') if args.hwcodec: features.append('hwcodec') if args.flutter: diff --git a/src/core_main.rs b/src/core_main.rs index 889015c0d..bd9680ffb 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -81,6 +81,13 @@ pub fn core_main() -> Option> { } } #[cfg(windows)] + #[cfg(feature = "quick_start")] + if !crate::platform::is_installed() && args.is_empty() && !_is_elevate && !_is_run_as_system { + if let Err(e) = crate::portable_service::client::start_portable_service() { + log::error!("Failed to start portable service:{:?}", e); + } + } + #[cfg(windows)] if !crate::platform::is_installed() && (_is_elevate || _is_run_as_system) { crate::platform::elevate_or_run_as_system(click_setup, _is_elevate, _is_run_as_system); return None; @@ -276,7 +283,7 @@ fn core_main_invoke_new_connection(mut args: std::env::Args) -> Option ResultType<()> { pub fn elevate_or_run_as_system(is_setup: bool, is_elevate: bool, is_run_as_system: bool) { // avoid possible run recursively due to failed run. + log::info!( + "elevate:{}->{:?}, run_as_system:{}->{}", + is_elevate, + is_elevated(None), + is_run_as_system, + crate::username(), + ); let arg_elevate = if is_setup { "--noinstall --elevate" } else { diff --git a/src/server/portable_service.rs b/src/server/portable_service.rs index 21b501f1e..64d6c017b 100644 --- a/src/server/portable_service.rs +++ b/src/server/portable_service.rs @@ -406,8 +406,9 @@ pub mod server { Pong => { nack = 0; } - ConnCount(Some(n)) => { - if n == 0 { + ConnCount(Some(_n)) => { + #[cfg(not(feature = "quick_start"))] + if _n == 0 { log::info!("Connnection count equals 0, exit"); stream.send(&Data::DataPortableService(WillClose)).await.ok(); break; @@ -435,6 +436,7 @@ pub mod server { break; } stream.send(&Data::DataPortableService(Ping)).await.ok(); + #[cfg(not(feature = "quick_start"))] stream.send(&Data::DataPortableService(ConnCount(None))).await.ok(); } } @@ -462,6 +464,7 @@ pub mod client { } pub(crate) fn start_portable_service() -> ResultType<()> { + log::info!("start portable service"); if PORTABLE_SERVICE_RUNNING.lock().unwrap().clone() { bail!("already running"); } From f986236a61309df89f2287561ce0b441a09c9aec Mon Sep 17 00:00:00 2001 From: 21pages Date: Fri, 18 Nov 2022 17:07:03 +0800 Subject: [PATCH 0970/2015] portable service: fix clean shared memory, at_exit not called at flutter Signed-off-by: 21pages --- flutter/lib/desktop/pages/connection_page.dart | 3 +++ src/flutter_ffi.rs | 11 ++++++++++- src/server/portable_service.rs | 4 ++-- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index fc5b8e574..f73c6b0da 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -90,6 +90,9 @@ class _ConnectionPageState extends State Get.forceAppUpdate(); } isWindowMinisized = false; + } else if (eventName == 'close') { + // called more then one time + bind.mainOnMainWindowClose(); } } diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 520efea70..b947fad47 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -619,7 +619,11 @@ pub fn main_set_peer_option_sync(id: String, key: String, value: String) -> Sync } pub fn main_set_peer_alias(id: String, alias: String) { - main_broadcast_message(&HashMap::from([("name", "alias"), ("id", &id), ("alias", &alias)])); + main_broadcast_message(&HashMap::from([ + ("name", "alias"), + ("id", &id), + ("alias", &alias), + ])); set_peer_option(id, "alias".to_owned(), alias) } @@ -1173,6 +1177,11 @@ pub fn main_account_auth_result() -> String { account_auth_result() } +pub fn main_on_main_window_close() { + #[cfg(windows)] + crate::portable_service::client::drop_portable_service_shared_memory(); +} + #[cfg(target_os = "android")] pub mod server_side { use jni::{ diff --git a/src/server/portable_service.rs b/src/server/portable_service.rs index 64d6c017b..861b04b3d 100644 --- a/src/server/portable_service.rs +++ b/src/server/portable_service.rs @@ -487,7 +487,7 @@ pub mod client { crate::portable_service::SHMEM_NAME, shmem_size, )?); - shutdown_hooks::add_shutdown_hook(drop_shmem); + shutdown_hooks::add_shutdown_hook(drop_portable_service_shared_memory); } let mut option = SHMEM.lock().unwrap(); let shmem = option.as_mut().unwrap(); @@ -507,7 +507,7 @@ pub mod client { Ok(()) } - extern "C" fn drop_shmem() { + pub extern "C" fn drop_portable_service_shared_memory() { log::info!("drop shared memory"); *SHMEM.lock().unwrap() = None; } From 26e8355528d91e186a6d48de8dfbcbb1a947b2af Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 18 Nov 2022 13:33:54 +0800 Subject: [PATCH 0971/2015] dynamic library - win virtual display Signed-off-by: fufesou --- Cargo.lock | 2 +- Cargo.toml | 2 +- build.py | 6 ++++ libs/clipboard/build.rs | 2 +- libs/virtual_display/Cargo.toml | 4 +++ libs/virtual_display/build.rs | 4 +-- libs/virtual_display/src/lib.rs | 9 +++++ src/server.rs | 2 ++ src/server/video_service.rs | 2 +- src/server/virtual_display.rs | 64 +++++++++++++++++++++++++++++++++ 10 files changed, 91 insertions(+), 6 deletions(-) create mode 100644 src/server/virtual_display.rs diff --git a/Cargo.lock b/Cargo.lock index 49fcc3dce..0e892aa31 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4391,6 +4391,7 @@ dependencies = [ "lazy_static", "libappindicator", "libc", + "libloading", "libpulse-binding", "libpulse-simple-binding", "mac_address", @@ -4424,7 +4425,6 @@ dependencies = [ "trayicon", "url", "uuid", - "virtual_display", "whoami", "winapi 0.3.9", "windows-service", diff --git a/Cargo.toml b/Cargo.toml index 836bd07d4..a3758c861 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -65,6 +65,7 @@ flutter_rust_bridge = { git = "https://github.com/SoLongAndThanksForAllThePizza/ errno = "0.2.8" rdev = { git = "https://github.com/asur4s/rdev" } url = { version = "2.1", features = ["serde"] } +libloading = "0.7" reqwest = { version = "0.11", features = ["blocking", "json", "rustls-tls"], default-features=false } chrono = "0.4.23" @@ -91,7 +92,6 @@ winit = "0.26" winapi = { version = "0.3", features = ["winuser"] } winreg = "0.10" windows-service = "0.4" -virtual_display = { path = "libs/virtual_display" } impersonate_system = { git = "https://github.com/21pages/impersonate-system" } shared_memory = "0.12.4" shutdown_hooks = "0.1.0" diff --git a/build.py b/build.py index c907334ce..48eaf1bc0 100755 --- a/build.py +++ b/build.py @@ -280,6 +280,7 @@ def build_flutter_windows(version, features): exit(-1) os.chdir('flutter') os.system('flutter build windows --release') + shutil.copy2('target/release/deps/virtual_display.dll', flutter_win_target_dir) os.chdir('..') os.chdir('libs/portable') os.system('pip3 install -r requirements.txt') @@ -316,6 +317,11 @@ def main(): os.system('python3 res/inline-sciter.py') portable = args.portable if windows: + # build virtual display dynamic library + os.chdir('libs/virtual_display') + os.system('cargo build --release') + os.chdir('../..') + if flutter: build_flutter_windows(version, features) return diff --git a/libs/clipboard/build.rs b/libs/clipboard/build.rs index b5c547637..7eb52c75b 100644 --- a/libs/clipboard/build.rs +++ b/libs/clipboard/build.rs @@ -18,7 +18,7 @@ fn build_c_impl() { if build.get_compiler().is_like_msvc() { build.define("WIN32", ""); // build.define("_AMD64_", ""); - build.flag("-Zi"); + build.flag("-Z7"); build.flag("-GR-"); // build.flag("-std:c++11"); } else { diff --git a/libs/virtual_display/Cargo.toml b/libs/virtual_display/Cargo.toml index 8d0b65171..d0b63b454 100644 --- a/libs/virtual_display/Cargo.toml +++ b/libs/virtual_display/Cargo.toml @@ -3,6 +3,10 @@ name = "virtual_display" version = "0.1.0" edition = "2021" +[lib] +name = "virtual_display" +crate-type = ["cdylib", "staticlib", "rlib"] + # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [build-dependencies] diff --git a/libs/virtual_display/build.rs b/libs/virtual_display/build.rs index 177d92371..29c3dd5d4 100644 --- a/libs/virtual_display/build.rs +++ b/libs/virtual_display/build.rs @@ -13,7 +13,7 @@ fn build_c_impl() { if build.get_compiler().is_like_msvc() { build.define("WIN32", ""); - build.flag("-Zi"); + build.flag("-Z7"); build.flag("-GR-"); // build.flag("-std:c++11"); } else { @@ -24,7 +24,7 @@ fn build_c_impl() { } #[cfg(target_os = "windows")] - build.compile("xxx"); + build.compile("win_virtual_display"); #[cfg(target_os = "windows")] println!("cargo:rerun-if-changed=src/win10/IddController.c"); diff --git a/libs/virtual_display/src/lib.rs b/libs/virtual_display/src/lib.rs index 9f71fd6da..7ffcc679f 100644 --- a/libs/virtual_display/src/lib.rs +++ b/libs/virtual_display/src/lib.rs @@ -11,6 +11,7 @@ lazy_static::lazy_static! { static ref MONITOR_PLUGIN: Mutex> = Mutex::new(Vec::new()); } +#[no_mangle] pub fn download_driver() -> ResultType<()> { #[cfg(windows)] let _download_url = win10::DRIVER_DOWNLOAD_URL; @@ -22,6 +23,7 @@ pub fn download_driver() -> ResultType<()> { Ok(()) } +#[no_mangle] pub fn install_update_driver(_reboot_required: &mut bool) -> ResultType<()> { #[cfg(windows)] let install_path = win10::DRIVER_INSTALL_PATH; @@ -62,6 +64,7 @@ pub fn install_update_driver(_reboot_required: &mut bool) -> ResultType<()> { Ok(()) } +#[no_mangle] pub fn uninstall_driver(_reboot_required: &mut bool) -> ResultType<()> { #[cfg(windows)] let install_path = win10::DRIVER_INSTALL_PATH; @@ -96,6 +99,7 @@ pub fn uninstall_driver(_reboot_required: &mut bool) -> ResultType<()> { Ok(()) } +#[no_mangle] pub fn is_device_created() -> bool { #[cfg(windows)] return *H_SW_DEVICE.lock().unwrap() != 0; @@ -103,6 +107,7 @@ pub fn is_device_created() -> bool { return false; } +#[no_mangle] pub fn create_device() -> ResultType<()> { if is_device_created() { return Ok(()); @@ -120,6 +125,7 @@ pub fn create_device() -> ResultType<()> { Ok(()) } +#[no_mangle] pub fn close_device() { #[cfg(windows)] unsafe { @@ -129,6 +135,7 @@ pub fn close_device() { } } +#[no_mangle] pub fn plug_in_monitor() -> ResultType<()> { #[cfg(windows)] unsafe { @@ -149,6 +156,7 @@ pub fn plug_in_monitor() -> ResultType<()> { Ok(()) } +#[no_mangle] pub fn plug_out_monitor() -> ResultType<()> { #[cfg(windows)] unsafe { @@ -169,6 +177,7 @@ pub fn plug_out_monitor() -> ResultType<()> { Ok(()) } +#[no_mangle] pub fn update_monitor_modes() -> ResultType<()> { #[cfg(windows)] unsafe { diff --git a/src/server.rs b/src/server.rs index 7e00532fe..bf81468ce 100644 --- a/src/server.rs +++ b/src/server.rs @@ -53,6 +53,8 @@ mod connection; pub mod portable_service; mod service; mod video_qos; +#[cfg(windows)] +mod virtual_display; pub mod video_service; use hbb_common::tcp::new_listener; diff --git a/src/server/video_service.rs b/src/server/video_service.rs index 3ccc3af39..0597ac956 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -42,7 +42,7 @@ use std::{ time::{self, Duration, Instant}, }; #[cfg(windows)] -use virtual_display; +use super::virtual_display; pub const SCRAP_UBUNTU_HIGHER_REQUIRED: &str = "Wayland requires Ubuntu 21.04 or higher version."; pub const SCRAP_OTHER_VERSION_OR_X11_REQUIRED: &str = diff --git a/src/server/virtual_display.rs b/src/server/virtual_display.rs new file mode 100644 index 000000000..23071326b --- /dev/null +++ b/src/server/virtual_display.rs @@ -0,0 +1,64 @@ +#![allow(dead_code)] + +use hbb_common::{bail, ResultType}; +use std::sync::{Arc, Mutex}; + +const LIB_NAME_VIRTUAL_DISPLAY: &str = "virtual_display"; + +lazy_static::lazy_static! { + static ref LIB_VIRTUAL_DISPLAY: Arc>> = { + #[cfg(target_os = "windows")] + let libname = format!("{}.dll", LIB_NAME_VIRTUAL_DISPLAY); + #[cfg(target_os = "linux")] + let libname = format!("lib{}.so", LIB_NAME_VIRTUAL_DISPLAY); + #[cfg(target_os = "macos")] + let libname = format!("lib{}.dylib", LIB_NAME_VIRTUAL_DISPLAY); + Arc::new(Mutex::new(unsafe { libloading::Library::new(libname) })) + }; +} + +pub(super) fn is_device_created() -> bool { + match &*LIB_VIRTUAL_DISPLAY.lock().unwrap() { + Ok(lib) => unsafe { + match lib.get:: bool>>(b"is_device_created") { + Ok(func) => func(), + Err(..) => false, + } + }, + Err(..) => false, + } +} + +macro_rules! def_func_result { + ($func:ident, $name: tt) => { + pub(super) fn $func() -> ResultType<()> { + match &*LIB_VIRTUAL_DISPLAY.lock().unwrap() { + Ok(lib) => unsafe { + match lib.get:: ResultType<()>>>($name.as_bytes()) { + Ok(func) => func(), + Err(..) => bail!("Failed to load func {}", $name), + } + }, + Err(e) => bail!("Failed to load library {}, {}", LIB_NAME_VIRTUAL_DISPLAY, e), + } + } + }; +} + +def_func_result!(create_device, "create_device"); + +pub(super) fn close_device() { + match &*LIB_VIRTUAL_DISPLAY.lock().unwrap() { + Ok(lib) => unsafe { + match lib.get::>(b"close_device") { + Ok(func) => func(), + Err(..) => {}, + } + }, + Err(..) => {}, + } +} + +def_func_result!(plug_in_monitor, "plug_in_monitor"); +def_func_result!(plug_out_monitor, "plug_out_monitor"); +def_func_result!(update_monitor_modes, "update_monitor_modes"); From 27e7b5722255ab297077d9f3252f2099b00ef2ff Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 18 Nov 2022 14:52:01 +0800 Subject: [PATCH 0972/2015] move virtual display to lib workspace Signed-off-by: fufesou --- Cargo.lock | 20 +- Cargo.toml | 4 +- build.py | 6 +- libs/virtual_display/Cargo.lock | 1358 +++++++++++++++++ libs/virtual_display/Cargo.toml | 12 +- libs/virtual_display/README.md | 31 +- libs/virtual_display/dylib/Cargo.toml | 19 + libs/virtual_display/dylib/README.md | 32 + libs/virtual_display/{ => dylib}/build.rs | 0 libs/virtual_display/dylib/src/lib.rs | 201 +++ .../{ => dylib}/src/win10/IddController.c | 0 .../{ => dylib}/src/win10/IddController.h | 0 .../{ => dylib}/src/win10/Public.h | 0 .../{ => dylib}/src/win10/idd.rs | 0 .../{ => dylib}/src/win10/mod.rs | 0 libs/virtual_display/src/lib.rs | 248 +-- src/server.rs | 2 - src/server/video_service.rs | 2 +- src/server/virtual_display.rs | 64 - 19 files changed, 1702 insertions(+), 297 deletions(-) create mode 100644 libs/virtual_display/Cargo.lock create mode 100644 libs/virtual_display/dylib/Cargo.toml create mode 100644 libs/virtual_display/dylib/README.md rename libs/virtual_display/{ => dylib}/build.rs (100%) create mode 100644 libs/virtual_display/dylib/src/lib.rs rename libs/virtual_display/{ => dylib}/src/win10/IddController.c (100%) rename libs/virtual_display/{ => dylib}/src/win10/IddController.h (100%) rename libs/virtual_display/{ => dylib}/src/win10/Public.h (100%) rename libs/virtual_display/{ => dylib}/src/win10/idd.rs (100%) rename libs/virtual_display/{ => dylib}/src/win10/mod.rs (100%) delete mode 100644 src/server/virtual_display.rs diff --git a/Cargo.lock b/Cargo.lock index 0e892aa31..07cb346b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1392,6 +1392,18 @@ version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0" +[[package]] +name = "dylib_virtual_display" +version = "0.1.0" +dependencies = [ + "cc", + "hbb_common", + "lazy_static", + "serde 1.0.144", + "serde_derive", + "thiserror", +] + [[package]] name = "ed25519" version = "1.5.2" @@ -4391,7 +4403,6 @@ dependencies = [ "lazy_static", "libappindicator", "libc", - "libloading", "libpulse-binding", "libpulse-simple-binding", "mac_address", @@ -4425,6 +4436,7 @@ dependencies = [ "trayicon", "url", "uuid", + "virtual_display", "whoami", "winapi 0.3.9", "windows-service", @@ -5555,12 +5567,10 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" name = "virtual_display" version = "0.1.0" dependencies = [ - "cc", + "dylib_virtual_display", "hbb_common", "lazy_static", - "serde 1.0.144", - "serde_derive", - "thiserror", + "libloading", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index a3758c861..6375bce26 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -65,7 +65,6 @@ flutter_rust_bridge = { git = "https://github.com/SoLongAndThanksForAllThePizza/ errno = "0.2.8" rdev = { git = "https://github.com/asur4s/rdev" } url = { version = "2.1", features = ["serde"] } -libloading = "0.7" reqwest = { version = "0.11", features = ["blocking", "json", "rustls-tls"], default-features=false } chrono = "0.4.23" @@ -92,6 +91,7 @@ winit = "0.26" winapi = { version = "0.3", features = ["winuser"] } winreg = "0.10" windows-service = "0.4" +virtual_display = { path = "libs/virtual_display" } impersonate_system = { git = "https://github.com/21pages/impersonate-system" } shared_memory = "0.12.4" shutdown_hooks = "0.1.0" @@ -126,7 +126,7 @@ jni = "0.19" flutter_rust_bridge = { git = "https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge" } [workspace] -members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "libs/virtual_display", "libs/simple_rc", "libs/portable"] +members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "libs/virtual_display", "libs/virtual_display/dylib", "libs/simple_rc", "libs/portable"] [package.metadata.winres] LegalCopyright = "Copyright © 2022 Purslane, Inc." diff --git a/build.py b/build.py index 48eaf1bc0..a95c64dc1 100755 --- a/build.py +++ b/build.py @@ -280,8 +280,8 @@ def build_flutter_windows(version, features): exit(-1) os.chdir('flutter') os.system('flutter build windows --release') - shutil.copy2('target/release/deps/virtual_display.dll', flutter_win_target_dir) os.chdir('..') + shutil.copy2('target/release/deps/dylib_virtual_display.dll', flutter_win_target_dir) os.chdir('libs/portable') os.system('pip3 install -r requirements.txt') os.system( @@ -318,9 +318,9 @@ def main(): portable = args.portable if windows: # build virtual display dynamic library - os.chdir('libs/virtual_display') + os.chdir('libs/virtual_display/dylib') os.system('cargo build --release') - os.chdir('../..') + os.chdir('../../..') if flutter: build_flutter_windows(version, features) diff --git a/libs/virtual_display/Cargo.lock b/libs/virtual_display/Cargo.lock new file mode 100644 index 000000000..22fa681b2 --- /dev/null +++ b/libs/virtual_display/Cargo.lock @@ -0,0 +1,1358 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4f55bd91a0978cbfd91c457a164bab8b4001c833b7f323132c0a4e1922dd44e" +dependencies = [ + "memchr", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bumpalo" +version = "3.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" + +[[package]] +name = "bytes" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db" +dependencies = [ + "serde", +] + +[[package]] +name = "cc" +version = "1.0.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a284da2e6fe2092f2353e51713435363112dfd60030e22add80be333fb928f" +dependencies = [ + "jobserver", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-integer", + "num-traits", + "time", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + +[[package]] +name = "confy" +version = "0.4.0" +source = "git+https://github.com/open-trade/confy#630cc28a396cb7d01eefdd9f3824486fe4d8554b" +dependencies = [ + "directories-next", + "serde", + "thiserror", + "toml", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + +[[package]] +name = "cxx" +version = "1.0.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97abf9f0eca9e52b7f81b945524e76710e6cb2366aead23b7d4fbf72e281f888" +dependencies = [ + "cc", + "cxxbridge-flags", + "cxxbridge-macro", + "link-cplusplus", +] + +[[package]] +name = "cxx-build" +version = "1.0.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cc32cc5fea1d894b77d269ddb9f192110069a8a9c1f1d441195fba90553dea3" +dependencies = [ + "cc", + "codespan-reporting", + "once_cell", + "proc-macro2", + "quote", + "scratch", + "syn", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca220e4794c934dc6b1207c3b42856ad4c302f2df1712e9f8d2eec5afaacf1f" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b846f081361125bfc8dc9d3940c84e1fd83ba54bbca7b17cd29483c828be0704" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "directories-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "dylib_virtual_display" +version = "0.1.0" +dependencies = [ + "cc", + "hbb_common", + "lazy_static", + "serde", + "serde_derive", + "thiserror", +] + +[[package]] +name = "ed25519" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9c280362032ea4203659fc489832d0204ef09f247a0506f170dafcac08c369" +dependencies = [ + "signature", +] + +[[package]] +name = "either" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" + +[[package]] +name = "env_logger" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "fastrand" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" +dependencies = [ + "instant", +] + +[[package]] +name = "filetime" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b9663d381d07ae25dc88dbdf27df458faa83a9b25336bcac83d5e452b5fc9d3" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "windows-sys", +] + +[[package]] +name = "futures" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38390104763dc37a5145a53c29c63c1290b5d316d6086ec32c293f6736051bb0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ba265a92256105f45b719605a571ffe2d1f0fea3807304b522c1d778f79eed" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac" + +[[package]] +name = "futures-executor" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7acc85df6714c176ab5edf386123fafe217be88c0840ec11f199441134a074e2" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00f5fb52a06bdcadeb54e8d3671f8888a39697dcb0b81b23b55174030427f4eb" + +[[package]] +name = "futures-macro" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdfb8ce053d86b91919aad980c220b1fb8401a9394410e1c289ed7e66b61835d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39c15cf1a4aa79df40f1bb462fb39676d0ad9e366c2a33b590d7c66f4f81fcf9" + +[[package]] +name = "futures-task" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ffb393ac5d9a6eaa9d3fdf37ae2776656b706e200c8e16b1bdb227f5198e6ea" + +[[package]] +name = "futures-util" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "197676987abd2f9cadff84926f410af1c183608d36641465df73ae8211dc65d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] + +[[package]] +name = "hbb_common" +version = "0.1.0" +dependencies = [ + "anyhow", + "bytes", + "chrono", + "confy", + "directories-next", + "dirs-next", + "env_logger", + "filetime", + "futures", + "futures-util", + "lazy_static", + "log", + "mac_address", + "machine-uid", + "protobuf", + "protobuf-codegen", + "rand", + "regex", + "serde", + "serde_derive", + "socket2 0.3.19", + "sodiumoxide", + "tokio", + "tokio-socks", + "tokio-util", + "winapi", + "zstd", +] + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "iana-time-zone" +version = "0.1.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" +dependencies = [ + "cxx", + "cxx-build", +] + +[[package]] +name = "indexmap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "jobserver" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "068b1ee6743e4d11fb9c6a1e6064b3693a1b600e7f5f5988047d98b3dc9fb90b" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.137" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89" + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libsodium-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b779387cd56adfbc02ea4a668e704f729be8d6a6abd2c27ca5ee537849a92fd" +dependencies = [ + "cc", + "libc", + "pkg-config", + "walkdir", +] + +[[package]] +name = "link-cplusplus" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9272ab7b96c9046fbc5bc56c06c117cb639fe2d509df0c421cad82d2915cf369" +dependencies = [ + "cc", +] + +[[package]] +name = "lock_api" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "mac_address" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b238e3235c8382b7653c6408ed1b08dd379bdb9fdf990fb0bbae3db2cc0ae963" +dependencies = [ + "nix", + "winapi", +] + +[[package]] +name = "machine-uid" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f1595709b0a7386bcd56ba34d250d626e5503917d05d32cdccddcd68603e212" +dependencies = [ + "winreg", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mio" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de" +dependencies = [ + "libc", + "log", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys", +] + +[[package]] +name = "nix" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f866317acbd3a240710c63f065ffb1e4fd466259045ccb504130b7f668f35c6" +dependencies = [ + "bitflags", + "cc", + "cfg-if", + "libc", + "memoffset", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dc9e0dc2adc1c69d09143aff38d3d30c5c3f0df0dad82e6d25547af174ebec0" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-sys", +] + +[[package]] +name = "pin-project" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro2" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "protobuf" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b55bad9126f378a853655831eb7363b7b01b81d19f8cb1218861086ca4a1a61e" +dependencies = [ + "bytes", + "once_cell", + "protobuf-support", + "thiserror", +] + +[[package]] +name = "protobuf-codegen" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd418ac3c91caa4032d37cb80ff0d44e2ebe637b2fb243b6234bf89cdac4901" +dependencies = [ + "anyhow", + "once_cell", + "protobuf", + "protobuf-parse", + "regex", + "tempfile", + "thiserror", +] + +[[package]] +name = "protobuf-parse" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d39b14605eaa1f6a340aec7f320b34064feb26c93aec35d6a9a2272a8ddfa49" +dependencies = [ + "anyhow", + "indexmap", + "log", + "protobuf", + "protobuf-support", + "tempfile", + "thiserror", + "which", +] + +[[package]] +name = "protobuf-support" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d4d7b8601c814cfb36bcebb79f0e61e45e1e93640cf778837833bbed05c372" +dependencies = [ + "thiserror", +] + +[[package]] +name = "quote" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +dependencies = [ + "getrandom", + "redox_syscall", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "scratch" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8132065adcfd6e02db789d9285a0deb2f3fcb04002865ab67d5fb103533898" + +[[package]] +name = "serde" +version = "1.0.147" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d193d69bae983fc11a79df82342761dfbf28a99fc8d203dca4c3c1b590948965" + +[[package]] +name = "serde_derive" +version = "1.0.147" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f1d362ca8fc9c3e3a7484440752472d68a6caa98f1ab81d99b5dfe517cec852" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +dependencies = [ + "libc", +] + +[[package]] +name = "signature" +version = "1.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" + +[[package]] +name = "slab" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" + +[[package]] +name = "socket2" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "122e570113d28d773067fab24266b66753f6ea915758651696b6e35e49f88d6e" +dependencies = [ + "cfg-if", + "libc", + "winapi", +] + +[[package]] +name = "socket2" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "sodiumoxide" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e26be3acb6c2d9a7aac28482586a7856436af4cfe7100031d219de2d2ecb0028" +dependencies = [ + "ed25519", + "libc", + "libsodium-sys", + "serde", +] + +[[package]] +name = "syn" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +dependencies = [ + "cfg-if", + "fastrand", + "libc", + "redox_syscall", + "remove_dir_all", + "winapi", +] + +[[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + +[[package]] +name = "tokio" +version = "1.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e03c497dc955702ba729190dc4aac6f2a0ce97f913e5b1b5912fc5039d9099" +dependencies = [ + "autocfg", + "bytes", + "libc", + "memchr", + "mio", + "num_cpus", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.4.7", + "tokio-macros", + "winapi", +] + +[[package]] +name = "tokio-macros" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-socks" +version = "0.5.1-1" +source = "git+https://github.com/open-trade/tokio-socks#7034e79263ce25c348be072808d7601d82cd892d" +dependencies = [ + "bytes", + "either", + "futures-core", + "futures-sink", + "futures-util", + "pin-project", + "thiserror", + "tokio", + "tokio-util", +] + +[[package]] +name = "tokio-util" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb2e075f03b3d66d8d8785356224ba688d2906a371015e225beeb65ca92c740" +dependencies = [ + "bytes", + "futures-core", + "futures-io", + "futures-sink", + "futures-util", + "hashbrown", + "pin-project-lite", + "slab", + "tokio", + "tracing", +] + +[[package]] +name = "toml" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" +dependencies = [ + "serde", +] + +[[package]] +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "unicode-ident" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" + +[[package]] +name = "unicode-width" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "virtual_display" +version = "0.1.0" +dependencies = [ + "dylib_virtual_display", + "hbb_common", + "lazy_static", + "libloading", +] + +[[package]] +name = "walkdir" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +dependencies = [ + "same-file", + "winapi", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" + +[[package]] +name = "which" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c831fbbee9e129a8cf93e7747a82da9d95ba8e16621cae60ec2cdc849bacb7b" +dependencies = [ + "either", + "libc", + "once_cell", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" + +[[package]] +name = "winreg" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2986deb581c4fe11b621998a5e53361efe6b48a151178d0cd9eeffa4dc6acc9" +dependencies = [ + "winapi", +] + +[[package]] +name = "zstd" +version = "0.9.2+zstd.1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2390ea1bf6c038c39674f22d95f0564725fc06034a47129179810b2fc58caa54" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "4.1.3+zstd.1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e99d81b99fb3c2c2c794e3fe56c305c63d5173a16a46b5850b07c935ffc7db79" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "1.6.2+zstd.1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2daf2f248d9ea44454bfcb2516534e8b8ad2fc91bf818a1885495fc42bc8ac9f" +dependencies = [ + "cc", + "libc", +] diff --git a/libs/virtual_display/Cargo.toml b/libs/virtual_display/Cargo.toml index d0b63b454..88e065206 100644 --- a/libs/virtual_display/Cargo.toml +++ b/libs/virtual_display/Cargo.toml @@ -3,18 +3,10 @@ name = "virtual_display" version = "0.1.0" edition = "2021" -[lib] -name = "virtual_display" -crate-type = ["cdylib", "staticlib", "rlib"] - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html -[build-dependencies] -cc = "1.0" - [dependencies] -thiserror = "1.0.30" lazy_static = "1.4" -serde = "1.0" -serde_derive = "1.0" +libloading = "0.7" hbb_common = { path = "../hbb_common" } +dylib_virtual_display = { path = "./dylib" } diff --git a/libs/virtual_display/README.md b/libs/virtual_display/README.md index d5a1a0862..86b7b2ea2 100644 --- a/libs/virtual_display/README.md +++ b/libs/virtual_display/README.md @@ -1,32 +1,3 @@ # virtual display -Virtual display may be used on computers that do not have a monitor. - -[Development reference](https://github.com/pavlobu/deskreen/discussions/86) - -## windows - -### win10 - -Win10 provides [Indirect Display Driver Model](https://msdn.microsoft.com/en-us/library/windows/hardware/mt761968(v=vs.85).aspx). - -This lib uses [this project](https://github.com/fufesou/RustDeskIddDriver) as the driver. - - -**NOTE**: Versions before Win10 1607. Try follow [this method](https://github.com/fanxiushu/xdisp_virt/tree/master/indirect_display). - - -#### tested platforms - -- [x] 19041 -- [x] 19043 - -### win7 - -TODO - -[WDDM](https://docs.microsoft.com/en-us/windows-hardware/drivers/display/windows-vista-display-driver-model-design-guide). - -## X11 - -## OSX +[doc](./dylib/README.md) diff --git a/libs/virtual_display/dylib/Cargo.toml b/libs/virtual_display/dylib/Cargo.toml new file mode 100644 index 000000000..fee4e3e4f --- /dev/null +++ b/libs/virtual_display/dylib/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "dylib_virtual_display" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib", "staticlib", "rlib"] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[build-dependencies] +cc = "1.0" + +[dependencies] +thiserror = "1.0.30" +lazy_static = "1.4" +serde = "1.0" +serde_derive = "1.0" +hbb_common = { path = "../../hbb_common" } diff --git a/libs/virtual_display/dylib/README.md b/libs/virtual_display/dylib/README.md new file mode 100644 index 000000000..d5a1a0862 --- /dev/null +++ b/libs/virtual_display/dylib/README.md @@ -0,0 +1,32 @@ +# virtual display + +Virtual display may be used on computers that do not have a monitor. + +[Development reference](https://github.com/pavlobu/deskreen/discussions/86) + +## windows + +### win10 + +Win10 provides [Indirect Display Driver Model](https://msdn.microsoft.com/en-us/library/windows/hardware/mt761968(v=vs.85).aspx). + +This lib uses [this project](https://github.com/fufesou/RustDeskIddDriver) as the driver. + + +**NOTE**: Versions before Win10 1607. Try follow [this method](https://github.com/fanxiushu/xdisp_virt/tree/master/indirect_display). + + +#### tested platforms + +- [x] 19041 +- [x] 19043 + +### win7 + +TODO + +[WDDM](https://docs.microsoft.com/en-us/windows-hardware/drivers/display/windows-vista-display-driver-model-design-guide). + +## X11 + +## OSX diff --git a/libs/virtual_display/build.rs b/libs/virtual_display/dylib/build.rs similarity index 100% rename from libs/virtual_display/build.rs rename to libs/virtual_display/dylib/build.rs diff --git a/libs/virtual_display/dylib/src/lib.rs b/libs/virtual_display/dylib/src/lib.rs new file mode 100644 index 000000000..7ffcc679f --- /dev/null +++ b/libs/virtual_display/dylib/src/lib.rs @@ -0,0 +1,201 @@ +#[cfg(windows)] +pub mod win10; + +use hbb_common::{bail, lazy_static, ResultType}; +use std::{path::Path, sync::Mutex}; + +lazy_static::lazy_static! { + // If device is uninstalled though "Device Manager" Window. + // Rustdesk is unable to handle device any more... + static ref H_SW_DEVICE: Mutex = Mutex::new(0); + static ref MONITOR_PLUGIN: Mutex> = Mutex::new(Vec::new()); +} + +#[no_mangle] +pub fn download_driver() -> ResultType<()> { + #[cfg(windows)] + let _download_url = win10::DRIVER_DOWNLOAD_URL; + #[cfg(target_os = "linux")] + let _download_url = ""; + + // process download and report progress + + Ok(()) +} + +#[no_mangle] +pub fn install_update_driver(_reboot_required: &mut bool) -> ResultType<()> { + #[cfg(windows)] + let install_path = win10::DRIVER_INSTALL_PATH; + #[cfg(not(windows))] + let install_path = ""; + + let abs_path = Path::new(install_path).canonicalize()?; + if !abs_path.exists() { + bail!("{} not exists", install_path) + } + + #[cfg(windows)] + unsafe { + { + // Device must be created before install driver. + // https://github.com/fufesou/RustDeskIddDriver/issues/1 + if let Err(e) = create_device() { + bail!("{}", e); + } + + let full_install_path: Vec = abs_path + .to_string_lossy() + .as_ref() + .encode_utf16() + .chain(Some(0).into_iter()) + .collect(); + + let mut reboot_required_tmp = win10::idd::FALSE; + if win10::idd::InstallUpdate(full_install_path.as_ptr() as _, &mut reboot_required_tmp) + == win10::idd::FALSE + { + bail!("{}", win10::get_last_msg()?); + } + *_reboot_required = reboot_required_tmp == win10::idd::TRUE; + } + } + + Ok(()) +} + +#[no_mangle] +pub fn uninstall_driver(_reboot_required: &mut bool) -> ResultType<()> { + #[cfg(windows)] + let install_path = win10::DRIVER_INSTALL_PATH; + #[cfg(not(windows))] + let install_path = ""; + + let abs_path = Path::new(install_path).canonicalize()?; + if !abs_path.exists() { + bail!("{} not exists", install_path) + } + + #[cfg(windows)] + unsafe { + { + let full_install_path: Vec = abs_path + .to_string_lossy() + .as_ref() + .encode_utf16() + .chain(Some(0).into_iter()) + .collect(); + + let mut reboot_required_tmp = win10::idd::FALSE; + if win10::idd::Uninstall(full_install_path.as_ptr() as _, &mut reboot_required_tmp) + == win10::idd::FALSE + { + bail!("{}", win10::get_last_msg()?); + } + *_reboot_required = reboot_required_tmp == win10::idd::TRUE; + } + } + + Ok(()) +} + +#[no_mangle] +pub fn is_device_created() -> bool { + #[cfg(windows)] + return *H_SW_DEVICE.lock().unwrap() != 0; + #[cfg(not(windows))] + return false; +} + +#[no_mangle] +pub fn create_device() -> ResultType<()> { + if is_device_created() { + return Ok(()); + } + #[cfg(windows)] + unsafe { + let mut lock_device = H_SW_DEVICE.lock().unwrap(); + let mut h_sw_device = *lock_device as win10::idd::HSWDEVICE; + if win10::idd::DeviceCreate(&mut h_sw_device) == win10::idd::FALSE { + bail!("{}", win10::get_last_msg()?); + } else { + *lock_device = h_sw_device as u64; + } + } + Ok(()) +} + +#[no_mangle] +pub fn close_device() { + #[cfg(windows)] + unsafe { + win10::idd::DeviceClose(*H_SW_DEVICE.lock().unwrap() as win10::idd::HSWDEVICE); + *H_SW_DEVICE.lock().unwrap() = 0; + MONITOR_PLUGIN.lock().unwrap().clear(); + } +} + +#[no_mangle] +pub fn plug_in_monitor() -> ResultType<()> { + #[cfg(windows)] + unsafe { + let monitor_index = 0 as u32; + let mut plug_in_monitors = MONITOR_PLUGIN.lock().unwrap(); + for i in 0..plug_in_monitors.len() { + if let Some(d) = plug_in_monitors.get(i) { + if *d == monitor_index { + return Ok(()); + } + }; + } + if win10::idd::MonitorPlugIn(monitor_index, 0, 30) == win10::idd::FALSE { + bail!("{}", win10::get_last_msg()?); + } + (*plug_in_monitors).push(monitor_index); + } + Ok(()) +} + +#[no_mangle] +pub fn plug_out_monitor() -> ResultType<()> { + #[cfg(windows)] + unsafe { + let monitor_index = 0 as u32; + if win10::idd::MonitorPlugOut(monitor_index) == win10::idd::FALSE { + bail!("{}", win10::get_last_msg()?); + } + let mut plug_in_monitors = MONITOR_PLUGIN.lock().unwrap(); + for i in 0..plug_in_monitors.len() { + if let Some(d) = plug_in_monitors.get(i) { + if *d == monitor_index { + plug_in_monitors.remove(i); + break; + } + }; + } + } + Ok(()) +} + +#[no_mangle] +pub fn update_monitor_modes() -> ResultType<()> { + #[cfg(windows)] + unsafe { + let monitor_index = 0 as u32; + let mut modes = vec![win10::idd::MonitorMode { + width: 1920, + height: 1080, + sync: 60, + }]; + if win10::idd::FALSE + == win10::idd::MonitorModesUpdate( + monitor_index as win10::idd::UINT, + modes.len() as win10::idd::UINT, + modes.as_mut_ptr(), + ) + { + bail!("{}", win10::get_last_msg()?); + } + } + Ok(()) +} diff --git a/libs/virtual_display/src/win10/IddController.c b/libs/virtual_display/dylib/src/win10/IddController.c similarity index 100% rename from libs/virtual_display/src/win10/IddController.c rename to libs/virtual_display/dylib/src/win10/IddController.c diff --git a/libs/virtual_display/src/win10/IddController.h b/libs/virtual_display/dylib/src/win10/IddController.h similarity index 100% rename from libs/virtual_display/src/win10/IddController.h rename to libs/virtual_display/dylib/src/win10/IddController.h diff --git a/libs/virtual_display/src/win10/Public.h b/libs/virtual_display/dylib/src/win10/Public.h similarity index 100% rename from libs/virtual_display/src/win10/Public.h rename to libs/virtual_display/dylib/src/win10/Public.h diff --git a/libs/virtual_display/src/win10/idd.rs b/libs/virtual_display/dylib/src/win10/idd.rs similarity index 100% rename from libs/virtual_display/src/win10/idd.rs rename to libs/virtual_display/dylib/src/win10/idd.rs diff --git a/libs/virtual_display/src/win10/mod.rs b/libs/virtual_display/dylib/src/win10/mod.rs similarity index 100% rename from libs/virtual_display/src/win10/mod.rs rename to libs/virtual_display/dylib/src/win10/mod.rs diff --git a/libs/virtual_display/src/lib.rs b/libs/virtual_display/src/lib.rs index 7ffcc679f..47b11d74a 100644 --- a/libs/virtual_display/src/lib.rs +++ b/libs/virtual_display/src/lib.rs @@ -1,201 +1,89 @@ #[cfg(windows)] -pub mod win10; +pub use dylib_virtual_display::win10; -use hbb_common::{bail, lazy_static, ResultType}; -use std::{path::Path, sync::Mutex}; +use hbb_common::{bail, ResultType}; +use std::sync::{Arc, Mutex}; + +const LIB_NAME_VIRTUAL_DISPLAY: &str = "virtual_display"; lazy_static::lazy_static! { - // If device is uninstalled though "Device Manager" Window. - // Rustdesk is unable to handle device any more... - static ref H_SW_DEVICE: Mutex = Mutex::new(0); - static ref MONITOR_PLUGIN: Mutex> = Mutex::new(Vec::new()); + static ref LIB_VIRTUAL_DISPLAY: Arc>> = { + #[cfg(target_os = "windows")] + let libname = format!("{}.dll", LIB_NAME_VIRTUAL_DISPLAY); + #[cfg(target_os = "linux")] + let libname = format!("lib{}.so", LIB_NAME_VIRTUAL_DISPLAY); + #[cfg(target_os = "macos")] + let libname = format!("lib{}.dylib", LIB_NAME_VIRTUAL_DISPLAY); + Arc::new(Mutex::new(unsafe { libloading::Library::new(libname) })) + }; } -#[no_mangle] -pub fn download_driver() -> ResultType<()> { - #[cfg(windows)] - let _download_url = win10::DRIVER_DOWNLOAD_URL; - #[cfg(target_os = "linux")] - let _download_url = ""; - - // process download and report progress - - Ok(()) -} - -#[no_mangle] -pub fn install_update_driver(_reboot_required: &mut bool) -> ResultType<()> { - #[cfg(windows)] - let install_path = win10::DRIVER_INSTALL_PATH; - #[cfg(not(windows))] - let install_path = ""; - - let abs_path = Path::new(install_path).canonicalize()?; - if !abs_path.exists() { - bail!("{} not exists", install_path) - } - - #[cfg(windows)] - unsafe { - { - // Device must be created before install driver. - // https://github.com/fufesou/RustDeskIddDriver/issues/1 - if let Err(e) = create_device() { - bail!("{}", e); - } - - let full_install_path: Vec = abs_path - .to_string_lossy() - .as_ref() - .encode_utf16() - .chain(Some(0).into_iter()) - .collect(); - - let mut reboot_required_tmp = win10::idd::FALSE; - if win10::idd::InstallUpdate(full_install_path.as_ptr() as _, &mut reboot_required_tmp) - == win10::idd::FALSE - { - bail!("{}", win10::get_last_msg()?); - } - *_reboot_required = reboot_required_tmp == win10::idd::TRUE; - } - } - - Ok(()) -} - -#[no_mangle] -pub fn uninstall_driver(_reboot_required: &mut bool) -> ResultType<()> { - #[cfg(windows)] - let install_path = win10::DRIVER_INSTALL_PATH; - #[cfg(not(windows))] - let install_path = ""; - - let abs_path = Path::new(install_path).canonicalize()?; - if !abs_path.exists() { - bail!("{} not exists", install_path) - } - - #[cfg(windows)] - unsafe { - { - let full_install_path: Vec = abs_path - .to_string_lossy() - .as_ref() - .encode_utf16() - .chain(Some(0).into_iter()) - .collect(); - - let mut reboot_required_tmp = win10::idd::FALSE; - if win10::idd::Uninstall(full_install_path.as_ptr() as _, &mut reboot_required_tmp) - == win10::idd::FALSE - { - bail!("{}", win10::get_last_msg()?); - } - *_reboot_required = reboot_required_tmp == win10::idd::TRUE; - } - } - - Ok(()) -} - -#[no_mangle] pub fn is_device_created() -> bool { - #[cfg(windows)] - return *H_SW_DEVICE.lock().unwrap() != 0; - #[cfg(not(windows))] - return false; + match &*LIB_VIRTUAL_DISPLAY.lock().unwrap() { + Ok(lib) => unsafe { + match lib.get:: bool>>(b"is_device_created") { + Ok(func) => func(), + Err(..) => false, + } + }, + Err(..) => false, + } } -#[no_mangle] -pub fn create_device() -> ResultType<()> { - if is_device_created() { - return Ok(()); - } - #[cfg(windows)] - unsafe { - let mut lock_device = H_SW_DEVICE.lock().unwrap(); - let mut h_sw_device = *lock_device as win10::idd::HSWDEVICE; - if win10::idd::DeviceCreate(&mut h_sw_device) == win10::idd::FALSE { - bail!("{}", win10::get_last_msg()?); - } else { - *lock_device = h_sw_device as u64; - } - } - Ok(()) -} - -#[no_mangle] pub fn close_device() { - #[cfg(windows)] - unsafe { - win10::idd::DeviceClose(*H_SW_DEVICE.lock().unwrap() as win10::idd::HSWDEVICE); - *H_SW_DEVICE.lock().unwrap() = 0; - MONITOR_PLUGIN.lock().unwrap().clear(); + match &*LIB_VIRTUAL_DISPLAY.lock().unwrap() { + Ok(lib) => unsafe { + match lib.get::>(b"close_device") { + Ok(func) => func(), + Err(..) => {} + } + }, + Err(..) => {} } } -#[no_mangle] -pub fn plug_in_monitor() -> ResultType<()> { - #[cfg(windows)] - unsafe { - let monitor_index = 0 as u32; - let mut plug_in_monitors = MONITOR_PLUGIN.lock().unwrap(); - for i in 0..plug_in_monitors.len() { - if let Some(d) = plug_in_monitors.get(i) { - if *d == monitor_index { - return Ok(()); - } - }; +macro_rules! def_func_result { + ($func:ident, $name: tt) => { + pub fn $func() -> ResultType<()> { + match &*LIB_VIRTUAL_DISPLAY.lock().unwrap() { + Ok(lib) => unsafe { + match lib.get:: ResultType<()>>>($name.as_bytes()) { + Ok(func) => func(), + Err(..) => bail!("Failed to load func {}", $name), + } + }, + Err(e) => bail!("Failed to load library {}, {}", LIB_NAME_VIRTUAL_DISPLAY, e), + } } - if win10::idd::MonitorPlugIn(monitor_index, 0, 30) == win10::idd::FALSE { - bail!("{}", win10::get_last_msg()?); - } - (*plug_in_monitors).push(monitor_index); - } - Ok(()) + }; } -#[no_mangle] -pub fn plug_out_monitor() -> ResultType<()> { - #[cfg(windows)] - unsafe { - let monitor_index = 0 as u32; - if win10::idd::MonitorPlugOut(monitor_index) == win10::idd::FALSE { - bail!("{}", win10::get_last_msg()?); - } - let mut plug_in_monitors = MONITOR_PLUGIN.lock().unwrap(); - for i in 0..plug_in_monitors.len() { - if let Some(d) = plug_in_monitors.get(i) { - if *d == monitor_index { - plug_in_monitors.remove(i); - break; - } - }; - } +pub fn install_update_driver(reboot_required: &mut bool) -> ResultType<()> { + match &*LIB_VIRTUAL_DISPLAY.lock().unwrap() { + Ok(lib) => unsafe { + match lib.get:: ResultType<()>>>(b"install_update_driver") { + Ok(func) => func(reboot_required), + Err(..) => bail!("Failed to load func install_update_driver"), + } + }, + Err(e) => bail!("Failed to load library {}, {}", LIB_NAME_VIRTUAL_DISPLAY, e), } - Ok(()) } -#[no_mangle] -pub fn update_monitor_modes() -> ResultType<()> { - #[cfg(windows)] - unsafe { - let monitor_index = 0 as u32; - let mut modes = vec![win10::idd::MonitorMode { - width: 1920, - height: 1080, - sync: 60, - }]; - if win10::idd::FALSE - == win10::idd::MonitorModesUpdate( - monitor_index as win10::idd::UINT, - modes.len() as win10::idd::UINT, - modes.as_mut_ptr(), - ) - { - bail!("{}", win10::get_last_msg()?); - } +pub fn uninstall_driver(reboot_required: &mut bool) -> ResultType<()> { + match &*LIB_VIRTUAL_DISPLAY.lock().unwrap() { + Ok(lib) => unsafe { + match lib.get:: ResultType<()>>>(b"uninstall_driver") { + Ok(func) => func(reboot_required), + Err(..) => bail!("Failed to load func uninstall_driver"), + } + }, + Err(e) => bail!("Failed to load library {}, {}", LIB_NAME_VIRTUAL_DISPLAY, e), } - Ok(()) } + +def_func_result!(download_driver, "download_driver"); +def_func_result!(create_device, "create_device"); +def_func_result!(plug_in_monitor, "plug_in_monitor"); +def_func_result!(plug_out_monitor, "plug_out_monitor"); +def_func_result!(update_monitor_modes, "update_monitor_modes"); diff --git a/src/server.rs b/src/server.rs index bf81468ce..7e00532fe 100644 --- a/src/server.rs +++ b/src/server.rs @@ -53,8 +53,6 @@ mod connection; pub mod portable_service; mod service; mod video_qos; -#[cfg(windows)] -mod virtual_display; pub mod video_service; use hbb_common::tcp::new_listener; diff --git a/src/server/video_service.rs b/src/server/video_service.rs index 0597ac956..3ccc3af39 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -42,7 +42,7 @@ use std::{ time::{self, Duration, Instant}, }; #[cfg(windows)] -use super::virtual_display; +use virtual_display; pub const SCRAP_UBUNTU_HIGHER_REQUIRED: &str = "Wayland requires Ubuntu 21.04 or higher version."; pub const SCRAP_OTHER_VERSION_OR_X11_REQUIRED: &str = diff --git a/src/server/virtual_display.rs b/src/server/virtual_display.rs deleted file mode 100644 index 23071326b..000000000 --- a/src/server/virtual_display.rs +++ /dev/null @@ -1,64 +0,0 @@ -#![allow(dead_code)] - -use hbb_common::{bail, ResultType}; -use std::sync::{Arc, Mutex}; - -const LIB_NAME_VIRTUAL_DISPLAY: &str = "virtual_display"; - -lazy_static::lazy_static! { - static ref LIB_VIRTUAL_DISPLAY: Arc>> = { - #[cfg(target_os = "windows")] - let libname = format!("{}.dll", LIB_NAME_VIRTUAL_DISPLAY); - #[cfg(target_os = "linux")] - let libname = format!("lib{}.so", LIB_NAME_VIRTUAL_DISPLAY); - #[cfg(target_os = "macos")] - let libname = format!("lib{}.dylib", LIB_NAME_VIRTUAL_DISPLAY); - Arc::new(Mutex::new(unsafe { libloading::Library::new(libname) })) - }; -} - -pub(super) fn is_device_created() -> bool { - match &*LIB_VIRTUAL_DISPLAY.lock().unwrap() { - Ok(lib) => unsafe { - match lib.get:: bool>>(b"is_device_created") { - Ok(func) => func(), - Err(..) => false, - } - }, - Err(..) => false, - } -} - -macro_rules! def_func_result { - ($func:ident, $name: tt) => { - pub(super) fn $func() -> ResultType<()> { - match &*LIB_VIRTUAL_DISPLAY.lock().unwrap() { - Ok(lib) => unsafe { - match lib.get:: ResultType<()>>>($name.as_bytes()) { - Ok(func) => func(), - Err(..) => bail!("Failed to load func {}", $name), - } - }, - Err(e) => bail!("Failed to load library {}, {}", LIB_NAME_VIRTUAL_DISPLAY, e), - } - } - }; -} - -def_func_result!(create_device, "create_device"); - -pub(super) fn close_device() { - match &*LIB_VIRTUAL_DISPLAY.lock().unwrap() { - Ok(lib) => unsafe { - match lib.get::>(b"close_device") { - Ok(func) => func(), - Err(..) => {}, - } - }, - Err(..) => {}, - } -} - -def_func_result!(plug_in_monitor, "plug_in_monitor"); -def_func_result!(plug_out_monitor, "plug_out_monitor"); -def_func_result!(update_monitor_modes, "update_monitor_modes"); From aa3b8ca084babcb451ea73dc18843a49ff1d88eb Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 18 Nov 2022 15:51:33 +0800 Subject: [PATCH 0973/2015] virtual display remove static links Signed-off-by: fufesou --- Cargo.lock | 1 - build.py | 5 -- libs/virtual_display/Cargo.toml | 1 - .../{ => dylib}/examples/idd_controller.rs | 2 +- libs/virtual_display/dylib/src/lib.rs | 6 ++ libs/virtual_display/src/lib.rs | 68 ++++++++++++++----- 6 files changed, 59 insertions(+), 24 deletions(-) rename libs/virtual_display/{ => dylib}/examples/idd_controller.rs (98%) diff --git a/Cargo.lock b/Cargo.lock index 07cb346b8..9eb270683 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5567,7 +5567,6 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" name = "virtual_display" version = "0.1.0" dependencies = [ - "dylib_virtual_display", "hbb_common", "lazy_static", "libloading", diff --git a/build.py b/build.py index a95c64dc1..07c571b11 100755 --- a/build.py +++ b/build.py @@ -317,11 +317,6 @@ def main(): os.system('python3 res/inline-sciter.py') portable = args.portable if windows: - # build virtual display dynamic library - os.chdir('libs/virtual_display/dylib') - os.system('cargo build --release') - os.chdir('../../..') - if flutter: build_flutter_windows(version, features) return diff --git a/libs/virtual_display/Cargo.toml b/libs/virtual_display/Cargo.toml index 88e065206..c700bd12a 100644 --- a/libs/virtual_display/Cargo.toml +++ b/libs/virtual_display/Cargo.toml @@ -9,4 +9,3 @@ edition = "2021" lazy_static = "1.4" libloading = "0.7" hbb_common = { path = "../hbb_common" } -dylib_virtual_display = { path = "./dylib" } diff --git a/libs/virtual_display/examples/idd_controller.rs b/libs/virtual_display/dylib/examples/idd_controller.rs similarity index 98% rename from libs/virtual_display/examples/idd_controller.rs rename to libs/virtual_display/dylib/examples/idd_controller.rs index 7d5677724..c9a3fbbab 100644 --- a/libs/virtual_display/examples/idd_controller.rs +++ b/libs/virtual_display/dylib/examples/idd_controller.rs @@ -1,5 +1,5 @@ #[cfg(windows)] -use virtual_display::win10::{idd, DRIVER_INSTALL_PATH}; +use dylib_virtual_display::win10::{idd, DRIVER_INSTALL_PATH}; #[cfg(windows)] use std::{ diff --git a/libs/virtual_display/dylib/src/lib.rs b/libs/virtual_display/dylib/src/lib.rs index 7ffcc679f..4a95e3461 100644 --- a/libs/virtual_display/dylib/src/lib.rs +++ b/libs/virtual_display/dylib/src/lib.rs @@ -11,6 +11,12 @@ lazy_static::lazy_static! { static ref MONITOR_PLUGIN: Mutex> = Mutex::new(Vec::new()); } +#[no_mangle] +#[cfg(windows)] +pub fn get_dirver_install_path() -> &'static str { + win10::DRIVER_INSTALL_PATH +} + #[no_mangle] pub fn download_driver() -> ResultType<()> { #[cfg(windows)] diff --git a/libs/virtual_display/src/lib.rs b/libs/virtual_display/src/lib.rs index 47b11d74a..cd9402c69 100644 --- a/libs/virtual_display/src/lib.rs +++ b/libs/virtual_display/src/lib.rs @@ -1,24 +1,52 @@ -#[cfg(windows)] -pub use dylib_virtual_display::win10; - use hbb_common::{bail, ResultType}; use std::sync::{Arc, Mutex}; -const LIB_NAME_VIRTUAL_DISPLAY: &str = "virtual_display"; +const LIB_NAME_VIRTUAL_DISPLAY: &str = "dylib_virtual_display"; lazy_static::lazy_static! { static ref LIB_VIRTUAL_DISPLAY: Arc>> = { - #[cfg(target_os = "windows")] - let libname = format!("{}.dll", LIB_NAME_VIRTUAL_DISPLAY); - #[cfg(target_os = "linux")] - let libname = format!("lib{}.so", LIB_NAME_VIRTUAL_DISPLAY); - #[cfg(target_os = "macos")] - let libname = format!("lib{}.dylib", LIB_NAME_VIRTUAL_DISPLAY); - Arc::new(Mutex::new(unsafe { libloading::Library::new(libname) })) + Arc::new(Mutex::new(unsafe { libloading::Library::new(get_lib_name()) })) }; } +#[cfg(target_os = "windows")] +fn get_lib_name() -> String { + format!("{}.dll", LIB_NAME_VIRTUAL_DISPLAY) +} + +#[cfg(target_os = "linux")] +fn get_lib_name() -> String { + format!("lib{}.so", LIB_NAME_VIRTUAL_DISPLAY) +} + +#[cfg(target_os = "macos")] +fn get_lib_name() -> String { + format!("lib{}.dylib", LIB_NAME_VIRTUAL_DISPLAY) +} + +fn try_reload_lib() { + let mut lock = LIB_VIRTUAL_DISPLAY.lock().unwrap(); + if lock.is_err() { + *lock = unsafe { libloading::Library::new(get_lib_name()) }; + } +} + +#[cfg(windows)] +pub fn get_dirver_install_path() -> ResultType<&'static str> { + try_reload_lib(); + match &*LIB_VIRTUAL_DISPLAY.lock().unwrap() { + Ok(lib) => unsafe { + match lib.get:: &'static str>>(b"get_dirver_install_path") { + Ok(func) => Ok(func()), + Err(e) => bail!("Failed to load func get_dirver_install_path, {}", e), + } + }, + Err(e) => bail!("Failed to load library {}, {}", LIB_NAME_VIRTUAL_DISPLAY, e), + } +} + pub fn is_device_created() -> bool { + try_reload_lib(); match &*LIB_VIRTUAL_DISPLAY.lock().unwrap() { Ok(lib) => unsafe { match lib.get:: bool>>(b"is_device_created") { @@ -31,6 +59,7 @@ pub fn is_device_created() -> bool { } pub fn close_device() { + try_reload_lib(); match &*LIB_VIRTUAL_DISPLAY.lock().unwrap() { Ok(lib) => unsafe { match lib.get::>(b"close_device") { @@ -45,11 +74,12 @@ pub fn close_device() { macro_rules! def_func_result { ($func:ident, $name: tt) => { pub fn $func() -> ResultType<()> { + try_reload_lib(); match &*LIB_VIRTUAL_DISPLAY.lock().unwrap() { Ok(lib) => unsafe { match lib.get:: ResultType<()>>>($name.as_bytes()) { Ok(func) => func(), - Err(..) => bail!("Failed to load func {}", $name), + Err(e) => bail!("Failed to load func {}, {}", $name, e), } }, Err(e) => bail!("Failed to load library {}, {}", LIB_NAME_VIRTUAL_DISPLAY, e), @@ -59,11 +89,14 @@ macro_rules! def_func_result { } pub fn install_update_driver(reboot_required: &mut bool) -> ResultType<()> { + try_reload_lib(); match &*LIB_VIRTUAL_DISPLAY.lock().unwrap() { Ok(lib) => unsafe { - match lib.get:: ResultType<()>>>(b"install_update_driver") { + match lib.get:: ResultType<()>>>( + b"install_update_driver", + ) { Ok(func) => func(reboot_required), - Err(..) => bail!("Failed to load func install_update_driver"), + Err(e) => bail!("Failed to load func install_update_driver, {}", e), } }, Err(e) => bail!("Failed to load library {}, {}", LIB_NAME_VIRTUAL_DISPLAY, e), @@ -71,11 +104,14 @@ pub fn install_update_driver(reboot_required: &mut bool) -> ResultType<()> { } pub fn uninstall_driver(reboot_required: &mut bool) -> ResultType<()> { + try_reload_lib(); match &*LIB_VIRTUAL_DISPLAY.lock().unwrap() { Ok(lib) => unsafe { - match lib.get:: ResultType<()>>>(b"uninstall_driver") { + match lib + .get:: ResultType<()>>>(b"uninstall_driver") + { Ok(func) => func(reboot_required), - Err(..) => bail!("Failed to load func uninstall_driver"), + Err(e) => bail!("Failed to load func uninstall_driver, {}", e), } }, Err(e) => bail!("Failed to load library {}, {}", LIB_NAME_VIRTUAL_DISPLAY, e), From 3e9d992db333869bccf0ce94239758ea1d2c1bac Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 18 Nov 2022 17:09:52 +0800 Subject: [PATCH 0974/2015] build dylib Signed-off-by: fufesou --- build.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/build.py b/build.py index 07c571b11..a95c64dc1 100755 --- a/build.py +++ b/build.py @@ -317,6 +317,11 @@ def main(): os.system('python3 res/inline-sciter.py') portable = args.portable if windows: + # build virtual display dynamic library + os.chdir('libs/virtual_display/dylib') + os.system('cargo build --release') + os.chdir('../../..') + if flutter: build_flutter_windows(version, features) return From 03ae220f714b504db38481b6a928af4f7ef64464 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 18 Nov 2022 18:36:25 +0800 Subject: [PATCH 0975/2015] macos tray --- src/core_main.rs | 3 ++- src/tray.rs | 32 ++++++++++++++++++++++++++++++++ src/ui.rs | 7 +------ src/ui/macos.rs | 28 +--------------------------- 4 files changed, 36 insertions(+), 34 deletions(-) diff --git a/src/core_main.rs b/src/core_main.rs index 889015c0d..31b2ae118 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -163,7 +163,8 @@ pub fn core_main() -> Option> { #[cfg(target_os = "macos")] { std::thread::spawn(move || crate::start_server(true)); - // to-do: for flutter, starting tray not ready yet, or we can reuse sciter's tray implementation. + crate::tray::make_tray(); + return None; } #[cfg(target_os = "linux")] { diff --git a/src/tray.rs b/src/tray.rs index 30bdc5a59..5064d96dc 100644 --- a/src/tray.rs +++ b/src/tray.rs @@ -173,3 +173,35 @@ fn is_service_stoped() -> bool { false } } + +#[cfg(target_os = "macos")] +pub fn make_tray() { + use tray_item::TrayItem; + let mode = dark_light::detect(); + let mut icon_path = ""; + match mode { + dark_light::Mode::Dark => { + icon_path = "mac-tray-light.png"; + }, + dark_light::Mode::Light => { + icon_path = "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(); + + 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)); + } + } +} + diff --git a/src/ui.rs b/src/ui.rs index 2d7f8d70a..904d3ae2b 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -63,12 +63,7 @@ fn check_connect_status( pub fn start(args: &mut [String]) { #[cfg(target_os = "macos")] - if args.len() == 1 && args[0] == "--server" { - macos::make_tray(); - return; - } else { - macos::show_dock(); - } + macos::show_dock(); #[cfg(all(target_os = "linux", feature = "inline"))] { #[cfg(feature = "appimage")] diff --git a/src/ui/macos.rs b/src/ui/macos.rs index 3c7a7dcd0..488d1afc8 100644 --- a/src/ui/macos.rs +++ b/src/ui/macos.rs @@ -233,33 +233,7 @@ pub fn make_tray() { unsafe { set_delegate(None); } - use tray_item::TrayItem; - let mode = dark_light::detect(); - let mut icon_path = ""; - match mode { - dark_light::Mode::Dark => { - icon_path = "mac-tray-light.png"; - }, - dark_light::Mode::Light => { - icon_path = "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(); - - 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)); - } - } + crate::tray::make_tray(); } pub fn check_main_window() { From af68800f45357de52662f8144d09696c58c42c10 Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 18 Nov 2022 18:46:00 +0800 Subject: [PATCH 0976/2015] remove redundant conditions Signed-off-by: fufesou --- src/client/io_loop.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 86a9e2f2e..6ad4f96d6 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -1187,7 +1187,7 @@ impl Remote { #[cfg(windows)] fn handle_cliprdr_msg(&self, clip: hbb_common::message_proto::Cliprdr) { if !self.handler.lc.read().unwrap().disable_clipboard { - #[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] + #[cfg(feature = "flutter")] if let Some(hbb_common::message_proto::cliprdr::Union::FormatList(_)) = &clip.union { if self.client_conn_id != clipboard::get_client_conn_id(&crate::flutter::get_cur_session_id()) From 1af6f81e409908826dab5fdfdb5314d34c04bcaa Mon Sep 17 00:00:00 2001 From: NicKoehler <53040044+NicKoehler@users.noreply.github.com> Date: Fri, 18 Nov 2022 12:50:39 +0100 Subject: [PATCH 0977/2015] Update it.rs added italian strings --- src/lang/it.rs | 108 ++++++++++++++++++++++++------------------------- 1 file changed, 54 insertions(+), 54 deletions(-) diff --git a/src/lang/it.rs b/src/lang/it.rs index 731b5643d..469a9c5ae 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -30,9 +30,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("IP Whitelisting", "IP autorizzati"), ("ID/Relay Server", "Server ID/Relay"), ("Import Server Config", "Importa configurazione Server"), - ("Export Server Config", ""), + ("Export Server Config", "Esporta configurazione Server"), ("Import server configuration successfully", "Configurazione Server importata con successo"), - ("Export server configuration successfully", ""), + ("Export server configuration successfully", "Configurazione Server esportata con successo"), ("Invalid server configuration", "Configurazione Server non valida"), ("Clipboard is empty", "Gli appunti sono vuoti"), ("Stop service", "Arresta servizio"), @@ -89,8 +89,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Delete", "Eliminare"), ("Properties", "Proprietà"), ("Multi Select", "Selezione multipla"), - ("Select All", ""), - ("Unselect All", ""), + ("Select All", "Seleziona tutto"), + ("Unselect All", "Deseleziona tutto"), ("Empty Directory", "Directory vuota"), ("Not an empty directory", "Non una directory vuota"), ("Are you sure you want to delete this file?", "Vuoi davvero eliminare questo file?"), @@ -296,13 +296,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("In privacy mode", "In modalità privacy"), ("Out privacy mode", "Fuori modalità privacy"), ("Language", "Linguaggio"), - ("Keep RustDesk background service", ""), - ("Ignore Battery Optimizations", ""), + ("Keep RustDesk background service", "Mantieni il servizio di RustDesk in background"), + ("Ignore Battery Optimizations", "Ignora le ottimizzazioni della batteria"), ("android_open_battery_optimizations_tip", ""), ("Connection not allowed", "Connessione non consentita"), - ("Legacy mode", ""), - ("Map mode", ""), - ("Translate mode", ""), + ("Legacy mode", "Modalità legacy"), + ("Map mode", "Modalità mappa"), + ("Translate mode", "Modalità di traduzione"), ("Use temporary password", "Usa password temporanea"), ("Use permanent password", "Usa password permanente"), ("Use both passwords", "Usa entrambe le password"), @@ -318,7 +318,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Exit Fullscreen", "Esci dalla modalità schermo intero"), ("Fullscreen", "A schermo intero"), ("Mobile Actions", "Azioni mobili"), - ("Select Monitor", "Seleziona Monitora"), + ("Select Monitor", "Seleziona schermo"), ("Control Actions", "Azioni di controllo"), ("Display Settings", "Impostazioni di visualizzazione"), ("Ratio", "Rapporto"), @@ -337,58 +337,58 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Account", "Account"), ("Theme", "Tema"), ("Dark Theme", "Tema Scuro"), - ("Dark", ""), - ("Light", ""), - ("Follow System", ""), - ("Enable hardware codec", ""), - ("Unlock Security Settings", ""), - ("Enable Audio", ""), - ("Temporary Password Length", ""), - ("Unlock Network Settings", ""), + ("Dark", "Scuro"), + ("Light", "Chiaro"), + ("Follow System", "Segui il sistema"), + ("Enable hardware codec", "Abilita codec hardware"), + ("Unlock Security Settings", "Sblocca impostazioni di sicurezza"), + ("Enable Audio", "Abilita audio"), + ("Temporary Password Length", "Lunghezza password temporanea"), + ("Unlock Network Settings", "Sblocca impostazioni di rete"), ("Server", ""), - ("Direct IP Access", ""), + ("Direct IP Access", "Accesso IP diretto"), ("Proxy", ""), - ("Port", ""), - ("Apply", ""), - ("Disconnect all devices?", ""), - ("Clear", ""), - ("Audio Input Device", ""), - ("Deny remote access", ""), - ("Use IP Whitelisting", ""), - ("Network", ""), - ("Enable RDP", ""), + ("Port", "Porta"), + ("Apply", "Applica"), + ("Disconnect all devices?", "Disconnettere tutti i dispositivi?"), + ("Clear", "Ripulisci"), + ("Audio Input Device", "Dispositivo di inpit audio"), + ("Deny remote access", "Nega accesso remoto"), + ("Use IP Whitelisting", "Utilizza la whitelist di IP"), + ("Network", "Rete"), + ("Enable RDP", "Abilita RDP"), ("Pin menubar", "Blocca la barra dei menu"), ("Unpin menubar", "Sblocca la barra dei menu"), - ("Recording", ""), - ("Directory", ""), - ("Automatically record incoming sessions", ""), - ("Change", ""), - ("Start session recording", ""), - ("Stop session recording", ""), - ("Enable Recording Session", ""), - ("Allow recording session", ""), - ("Enable LAN Discovery", ""), - ("Deny LAN Discovery", ""), - ("Write a message", ""), + ("Recording", "Registrazione"), + ("Directory", "Cartella"), + ("Automatically record incoming sessions", "Registra automaticamente le sessioni in entrata"), + ("Change", "Cambia"), + ("Start session recording", "Inizia registrazione della sessione"), + ("Stop session recording", "Ferma registrazione della sessione"), + ("Enable Recording Session", "Abilita registrazione della sessione"), + ("Allow recording session", "Permetti di registrare la sessione"), + ("Enable LAN Discovery", "Abilita il rilevamento della LAN"), + ("Deny LAN Discovery", "Nega il rilevamento della LAN"), + ("Write a message", "Scrivi un messaggio"), ("Prompt", ""), - ("Please wait for confirmation of UAC...", ""), + ("Please wait for confirmation of UAC...", "Attendi la conferma dell'UAC..."), ("elevated_foreground_window_tip", ""), - ("Disconnected", ""), - ("Other", ""), - ("Confirm before closing multiple tabs", ""), - ("Keyboard Settings", ""), - ("Custom", ""), - ("Full Access", ""), - ("Screen Share", ""), - ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland richiede Ubuntu 21.04 o versione successiva."), + ("Disconnected", "Disconnesso"), + ("Other", "Altro"), + ("Confirm before closing multiple tabs", "Conferma prima di chiudere più schede"), + ("Keyboard Settings", "Impostazioni tastiera"), + ("Custom", "Personalizzato"), + ("Full Access", "Accesso completo"), + ("Screen Share", "Condivisione dello schermo"), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland richiede Ubuntu 21.04 o successiva."), ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland richiede una versione superiore della distribuzione Linux. Prova X11 desktop o cambia il tuo sistema operativo."), ("JumpLink", "View"), ("Please Select the screen to be shared(Operate on the peer side).", "Seleziona lo schermo da condividere (opera sul lato peer)."), - ("Show RustDesk", ""), - ("This PC", ""), - ("or", ""), - ("Continue with", ""), - ("Elevate", ""), - ("Zoom cursor", ""), + ("Show RustDesk", "Mostra RustDesk"), + ("This PC", "Questo PC"), + ("or", "O"), + ("Continue with", "Continua con"), + ("Elevate", "Eleva"), + ("Zoom cursor", "Cursore zoom"), ].iter().cloned().collect(); } From 3adeba65d871fe8740466e603e4a64826e2295a8 Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 18 Nov 2022 22:36:51 +0800 Subject: [PATCH 0978/2015] fix blurry screen when scale original Signed-off-by: fufesou --- flutter/lib/desktop/pages/remote_page.dart | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 1aca198c2..84ea36d78 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -565,9 +565,11 @@ class ImagePainter extends CustomPainter { // https://github.com/flutter/flutter/issues/76187#issuecomment-784628161 // https://api.flutter-io.cn/flutter/dart-ui/FilterQuality.html var paint = Paint(); - paint.filterQuality = FilterQuality.medium; - if (scale > 10.00000) { - paint.filterQuality = FilterQuality.high; + if ((scale - 1.0).abs() > 0.001) { + paint.filterQuality = FilterQuality.medium; + if (scale > 10.00000) { + paint.filterQuality = FilterQuality.high; + } } canvas.drawImage(image!, Offset(x, y), paint); } From 042a2bff904a82d812011a39ab6b48fe29240d40 Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 18 Nov 2022 23:00:15 +0800 Subject: [PATCH 0979/2015] trivial changes Signed-off-by: fufesou --- flutter/lib/desktop/pages/remote_page.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 84ea36d78..c1837cb21 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -561,11 +561,11 @@ class ImagePainter extends CustomPainter { 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) { + canvas.scale(scale, scale); paint.filterQuality = FilterQuality.medium; if (scale > 10.00000) { paint.filterQuality = FilterQuality.high; From 44e45082ba94812cdb22aef3038a16ba4e9464af Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Fri, 18 Nov 2022 23:13:14 +0800 Subject: [PATCH 0980/2015] Revert "trivial changes" --- flutter/lib/desktop/pages/remote_page.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index c1837cb21..84ea36d78 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -561,11 +561,11 @@ class ImagePainter extends CustomPainter { 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) { - canvas.scale(scale, scale); paint.filterQuality = FilterQuality.medium; if (scale > 10.00000) { paint.filterQuality = FilterQuality.high; From eb673c8c78738f0091ffe9ff65ceec056efb1064 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sat, 19 Nov 2022 10:57:17 +0800 Subject: [PATCH 0981/2015] portable-service: enable quick_support by rename as xxxqs.exe Signed-off-by: 21pages --- .github/workflows/flutter-nightly.yml | 9 +++--- Cargo.toml | 1 - build.py | 7 ----- .../lib/desktop/pages/connection_page.dart | 9 ++++-- libs/portable/src/main.rs | 5 +++- src/core_main.rs | 16 +++++++++-- src/flutter_ffi.rs | 1 + src/server/portable_service.rs | 28 +++++++++++++------ src/ui.rs | 19 +------------ 9 files changed, 49 insertions(+), 46 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 074eafe08..f2391e77e 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -15,7 +15,7 @@ env: jobs: build-for-windows: - name: ${{ matrix.job.target }} (${{ matrix.job.os }}) ${{ matrix.job.suffix }} + name: ${{ matrix.job.target }} (${{ matrix.job.os }}) runs-on: ${{ matrix.job.os }} strategy: fail-fast: false @@ -23,8 +23,7 @@ jobs: job: # - { target: i686-pc-windows-msvc , os: windows-2019 } # - { target: x86_64-pc-windows-gnu , os: windows-2019 } - - { target: x86_64-pc-windows-msvc , os: windows-2019, suffix: "" , extra-build-args: "" } - - { target: x86_64-pc-windows-msvc , os: windows-2019, suffix: "-qs", extra-build-args: "--quick_start" } + - { target: x86_64-pc-windows-msvc , os: windows-2019 } steps: - name: Checkout source code uses: actions/checkout@v3 @@ -84,13 +83,13 @@ jobs: shell: bash - name: Build rustdesk - run: python3 .\build.py --portable --hwcodec --flutter ${{ matrix.job.extra-build-args }} + run: python3 .\build.py --portable --hwcodec --flutter - name: Rename rustdesk shell: bash run: | for name in rustdesk*??-install.exe; do - mv "$name" "${name%%-install.exe}-${{ matrix.job.target }}${{ matrix.job.suffix }}.exe" + mv "$name" "${name%%-install.exe}-${{ matrix.job.target }}.exe" done - name: Publish Release diff --git a/Cargo.toml b/Cargo.toml index 44df74952..836bd07d4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,6 @@ flutter = ["flutter_rust_bridge"] default = ["use_dasp"] hwcodec = ["scrap/hwcodec"] mediacodec = ["scrap/mediacodec"] -quick_start = [] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/build.py b/build.py index a887ff070..c907334ce 100755 --- a/build.py +++ b/build.py @@ -81,11 +81,6 @@ def make_parser(): action='store_true', help='Build windows portable' ) - parser.add_argument( - '--quick_start', - action='store_true', - help='Windows quick start portable' - ) parser.add_argument( '--flatpak', action='store_true', @@ -194,8 +189,6 @@ def get_features(args): features = ['inline'] if windows: features.extend(get_rc_features(args)) - if args.quick_start: - features.append('quick_start') if args.hwcodec: features.append('hwcodec') if args.flutter: diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index f73c6b0da..671335bfd 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -90,12 +90,15 @@ class _ConnectionPageState extends State Get.forceAppUpdate(); } isWindowMinisized = false; - } else if (eventName == 'close') { - // called more then one time - bind.mainOnMainWindowClose(); } } + @override + void onWindowClose() { + super.onWindowClose(); + bind.mainOnMainWindowClose(); + } + @override Widget build(BuildContext context) { return Column( diff --git a/libs/portable/src/main.rs b/libs/portable/src/main.rs index ad05e7376..edcbdd1fd 100644 --- a/libs/portable/src/main.rs +++ b/libs/portable/src/main.rs @@ -64,6 +64,7 @@ fn main() { i += 1; } let click_setup = args.is_empty() && arg_exe.to_lowercase().ends_with("install.exe"); + let quick_support = args.is_empty() && arg_exe.to_lowercase().ends_with("qs.exe"); let reader = BinaryReader::default(); if let Some(exe) = setup( @@ -72,7 +73,9 @@ fn main() { click_setup || args.contains(&"--silent-install".to_owned()), ) { if click_setup { - args = vec!["--install".to_owned()] + args = vec!["--install".to_owned()]; + } else if quick_support { + args = vec!["--quick_support".to_owned()]; } execute(exe, args); } diff --git a/src/core_main.rs b/src/core_main.rs index bd9680ffb..95331a184 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -14,6 +14,7 @@ pub fn core_main() -> Option> { let mut i = 0; let mut _is_elevate = false; let mut _is_run_as_system = false; + let mut _is_quick_support = false; let mut _is_flutter_connect = false; let mut arg_exe = Default::default(); for arg in std::env::args() { @@ -29,6 +30,8 @@ pub fn core_main() -> Option> { _is_elevate = true; } else if arg == "--run-as-system" { _is_run_as_system = true; + } else if arg == "--quick_support" { + _is_quick_support = true; } else { args.push(arg); } @@ -40,6 +43,11 @@ pub fn core_main() -> Option> { return core_main_invoke_new_connection(std::env::args()); } let click_setup = cfg!(windows) && args.is_empty() && crate::common::is_setup(&arg_exe); + #[cfg(not(feature = "flutter"))] + { + _is_quick_support = + cfg!(windows) && args.is_empty() && arg_exe.to_lowercase().ends_with("qs.exe"); + } if click_setup { args.push("--install".to_owned()); flutter_args.push("--install".to_string()); @@ -81,8 +89,12 @@ pub fn core_main() -> Option> { } } #[cfg(windows)] - #[cfg(feature = "quick_start")] - if !crate::platform::is_installed() && args.is_empty() && !_is_elevate && !_is_run_as_system { + if !crate::platform::is_installed() + && args.is_empty() + && _is_quick_support + && !_is_elevate + && !_is_run_as_system + { if let Err(e) = crate::portable_service::client::start_portable_service() { log::error!("Failed to start portable service:{:?}", e); } diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index b947fad47..36e38f86e 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1178,6 +1178,7 @@ pub fn main_account_auth_result() -> String { } pub fn main_on_main_window_close() { + // may called more than one times #[cfg(windows)] crate::portable_service::client::drop_portable_service_shared_memory(); } diff --git a/src/server/portable_service.rs b/src/server/portable_service.rs index 861b04b3d..ace70e1bd 100644 --- a/src/server/portable_service.rs +++ b/src/server/portable_service.rs @@ -237,11 +237,10 @@ pub mod server { fn run_exit_check() { loop { if EXIT.lock().unwrap().clone() { - std::thread::sleep(Duration::from_secs(1)); - log::info!("exit from seperate check thread"); + std::thread::sleep(Duration::from_millis(50)); std::process::exit(0); } - std::thread::sleep(Duration::from_secs(1)); + std::thread::sleep(Duration::from_millis(50)); } } @@ -406,9 +405,8 @@ pub mod server { Pong => { nack = 0; } - ConnCount(Some(_n)) => { - #[cfg(not(feature = "quick_start"))] - if _n == 0 { + ConnCount(Some(n)) => { + if n == 0 { log::info!("Connnection count equals 0, exit"); stream.send(&Data::DataPortableService(WillClose)).await.ok(); break; @@ -436,7 +434,6 @@ pub mod server { break; } stream.send(&Data::DataPortableService(Ping)).await.ok(); - #[cfg(not(feature = "quick_start"))] stream.send(&Data::DataPortableService(ConnCount(None))).await.ok(); } } @@ -626,6 +623,17 @@ pub mod client { use DataPortableService::*; let rx = Arc::new(tokio::sync::Mutex::new(rx)); let postfix = IPC_PROFIX; + #[cfg(feature = "flutter")] + let quick_support = { + let args: Vec<_> = std::env::args().collect(); + args.contains(&"--quick_support".to_string()) + }; + #[cfg(not(feature = "flutter"))] + let quick_support = std::env::current_exe() + .unwrap_or("".into()) + .to_string_lossy() + .to_lowercase() + .ends_with("qs.exe"); match new_listener(postfix).await { Ok(mut incoming) => loop { @@ -663,8 +671,10 @@ pub mod client { *PORTABLE_SERVICE_RUNNING.lock().unwrap() = true; }, ConnCount(None) => { - let cnt = crate::server::CONN_COUNT.lock().unwrap().clone(); - stream.send(&Data::DataPortableService(ConnCount(Some(cnt)))).await.ok(); + if !quick_support { + let cnt = crate::server::CONN_COUNT.lock().unwrap().clone(); + stream.send(&Data::DataPortableService(ConnCount(Some(cnt)))).await.ok(); + } }, WillClose => { log::info!("portable service will close"); diff --git a/src/ui.rs b/src/ui.rs index 2d7f8d70a..991832627 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -15,7 +15,7 @@ use hbb_common::{ protobuf::Message as _, rendezvous_proto::*, tcp::FramedStream, - tokio::{self, sync::mpsc}, + tokio, }; use crate::common::get_app_name; @@ -44,23 +44,6 @@ lazy_static::lazy_static! { struct UIHostHandler; -// to-do: dead code? -fn check_connect_status( - reconnect: bool, -) -> ( - Arc>, - Arc>>, - mpsc::UnboundedSender, - Arc>, -) { - let status = Arc::new(Mutex::new((0, false, 0, "".to_owned()))); - let options = Arc::new(Mutex::new(Config::get_options())); - let (tx, rx) = mpsc::unbounded_channel::(); - let password = Arc::new(Mutex::new(String::default())); - std::thread::spawn(move || crate::ui_interface::check_connect_status_(reconnect, rx)); - (status, options, tx, password) -} - pub fn start(args: &mut [String]) { #[cfg(target_os = "macos")] if args.len() == 1 && args[0] == "--server" { From 9a9d0117e25018ef23e8041fc6666d053eb5e75a Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sat, 19 Nov 2022 16:14:56 +0800 Subject: [PATCH 0982/2015] opt: reduce white screen flickering on sub window --- flutter/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index e76855f62..a05898468 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -63,7 +63,7 @@ dependencies: desktop_multi_window: git: url: https://github.com/Kingtous/rustdesk_desktop_multi_window - ref: 9b4c5ac1aec2c4d1bdabb4dd29e4bc3b75c76a79 + ref: 8ee8eb59cabf6ac83a13fe002de7d4a231263a58 freezed_annotation: ^2.0.3 tray_manager: git: From 2ccf9726da426ca6b3464f81fd01299b119eea32 Mon Sep 17 00:00:00 2001 From: neoGalaxy88 Date: Sat, 19 Nov 2022 11:23:34 +0100 Subject: [PATCH 0983/2015] 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 469a9c5ae..61f289ad6 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -352,7 +352,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Apply", "Applica"), ("Disconnect all devices?", "Disconnettere tutti i dispositivi?"), ("Clear", "Ripulisci"), - ("Audio Input Device", "Dispositivo di inpit audio"), + ("Audio Input Device", "Dispositivo di input audio"), ("Deny remote access", "Nega accesso remoto"), ("Use IP Whitelisting", "Utilizza la whitelist di IP"), ("Network", "Rete"), From 6d0039b104c3e35c52839500200d326a9dd9d9dc Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sat, 19 Nov 2022 20:06:17 +0800 Subject: [PATCH 0984/2015] opt: stop show tray icon in root mode --- src/core_main.rs | 10 ++++++++-- src/tray.rs | 7 +++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/core_main.rs b/src/core_main.rs index 1f13172dd..13a2ef91c 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -188,8 +188,14 @@ pub fn core_main() -> Option> { #[cfg(target_os = "linux")] { let handler = std::thread::spawn(move || crate::start_server(true)); - crate::tray::start_tray(); - // revent server exit when encountering errors from tray + // 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(); + } + // prevent server exit when encountering errors from tray hbb_common::allow_err!(handler.join()); } } else if args[0] == "--import-config" { diff --git a/src/tray.rs b/src/tray.rs index 5064d96dc..3658739a4 100644 --- a/src/tray.rs +++ b/src/tray.rs @@ -150,6 +150,13 @@ fn get_default_app_indicator() -> Option { 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); From 05c549a5fea36ba7786e530872b1fe059b2eb3ec Mon Sep 17 00:00:00 2001 From: 21pages Date: Sun, 20 Nov 2022 15:53:08 +0800 Subject: [PATCH 0985/2015] approve mode, cm sync option Signed-off-by: 21pages --- .../desktop/pages/desktop_setting_page.dart | 70 ++++++++++++++----- .../lib/desktop/pages/remote_tab_page.dart | 1 - flutter/lib/desktop/pages/server_page.dart | 21 ++++-- flutter/lib/models/model.dart | 3 + flutter/lib/models/server_model.dart | 27 +++++-- src/client.rs | 15 ++-- src/core_main.rs | 1 + src/flutter_ffi.rs | 2 +- src/lang/ca.rs | 4 ++ src/lang/cn.rs | 4 ++ src/lang/cs.rs | 4 ++ src/lang/da.rs | 4 ++ src/lang/de.rs | 4 ++ src/lang/eo.rs | 4 ++ src/lang/es.rs | 4 ++ src/lang/fa.rs | 4 ++ src/lang/fr.rs | 4 ++ src/lang/hu.rs | 4 ++ src/lang/id.rs | 4 ++ src/lang/it.rs | 4 ++ src/lang/ja.rs | 4 ++ src/lang/ko.rs | 4 ++ src/lang/kz.rs | 4 ++ src/lang/pl.rs | 4 ++ src/lang/pt_PT.rs | 4 ++ src/lang/ptbr.rs | 4 ++ src/lang/ru.rs | 4 ++ src/lang/sk.rs | 4 ++ src/lang/template.rs | 4 ++ src/lang/tr.rs | 4 ++ src/lang/tw.rs | 4 ++ src/lang/ua.rs | 4 ++ src/lang/vn.rs | 4 ++ src/server/connection.rs | 8 +++ src/ui/cm.rs | 5 ++ src/ui/cm.tis | 21 +++++- src/ui/common.tis | 7 ++ src/ui/index.tis | 64 +++++++++++++---- src/ui_interface.rs | 9 ++- 39 files changed, 298 insertions(+), 56 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index c03371a64..64ebc712e 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -580,20 +580,21 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { return ChangeNotifierProvider.value( value: gFFI.serverModel, child: Consumer(builder: ((context, model, child) { - List keys = [ + List passwordKeys = [ kUseTemporaryPassword, kUsePermanentPassword, kUseBothPasswords, ]; - List values = [ + List passwordValues = [ translate('Use temporary password'), translate('Use permanent password'), translate('Use both passwords'), ]; bool tmpEnabled = model.verificationMethod != kUsePermanentPassword; bool permEnabled = model.verificationMethod != kUseTemporaryPassword; - String currentValue = values[keys.indexOf(model.verificationMethod)]; - List radios = values + String currentValue = + passwordValues[passwordKeys.indexOf(model.verificationMethod)]; + List radios = passwordValues .map((value) => _Radio( context, value: value, @@ -601,8 +602,8 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { label: value, onChanged: ((value) { () async { - await model - .setVerificationMethod(keys[values.indexOf(value)]); + await model.setVerificationMethod( + passwordKeys[passwordValues.indexOf(value)]); await model.updatePasswordModel(); }(); }), @@ -640,20 +641,51 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { )) .toList(); + final modeKeys = ['password', 'click', '']; + final modeValues = [ + translate('Accept sessions via password'), + translate('Accept sessions via click'), + translate('Accept sessions via both'), + ]; + var modeInitialKey = model.approveMode; + if (!modeKeys.contains(modeInitialKey)) modeInitialKey = ''; + final usePassword = model.approveMode != 'click'; + return _Card(title: 'Password', children: [ - radios[0], - _SubLabeledWidget( - 'Temporary Password Length', - Row( - children: [ - ...lengthRadios, - ], - ), - enabled: tmpEnabled && !locked), - radios[1], - _SubButton('Set permanent password', setPasswordDialog, - permEnabled && !locked), - radios[2], + _ComboBox( + keys: modeKeys, + values: modeValues, + initialKey: modeInitialKey, + onChanged: (key) => model.setApproveMode(key), + ).marginOnly(left: _kContentHMargin), + Offstage( + offstage: !usePassword, + child: radios[0], + ), + Offstage( + offstage: !usePassword, + child: _SubLabeledWidget( + 'Temporary Password Length', + Row( + children: [ + ...lengthRadios, + ], + ), + enabled: tmpEnabled && !locked), + ), + Offstage( + offstage: !usePassword, + child: radios[1], + ), + Offstage( + offstage: !usePassword, + child: _SubButton('Set permanent password', setPasswordDialog, + permEnabled && !locked), + ), + Offstage( + offstage: !usePassword, + child: radios[2], + ), ]); }))); } diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index df8256496..1e942272c 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -84,7 +84,6 @@ class _ConnectionTabPageState extends State { if (call.method == "new_remote_desktop") { final args = jsonDecode(call.arguments); final id = args['id']; - ConnectionTypeState.init(id); window_on_top(windowId()); ConnectionTypeState.init(id); tabController.add(TabInfo( diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index 16abe0b64..ae69497bc 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -555,11 +555,12 @@ class _CmControlPanel extends StatelessWidget { final bool canElevate = bind.cmCanElevate(); final model = Provider.of(context); final showElevation = canElevate && model.showElevation; + final showAccept = model.approveMode != 'password'; return Column( mainAxisAlignment: MainAxisAlignment.end, children: [ Offstage( - offstage: !showElevation, + offstage: !showElevation || !showAccept, child: buildButton(context, color: Colors.green[700], onClick: () { handleAccept(context); handleElevate(context); @@ -575,11 +576,17 @@ class _CmControlPanel extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Expanded( - child: buildButton(context, color: MyTheme.accent, onClick: () { - handleAccept(context); - windowManager.minimize(); - }, text: 'Accept', textColor: Colors.white)), + if (showAccept) + Expanded( + child: Column( + children: [ + buildButton(context, color: MyTheme.accent, onClick: () { + handleAccept(context); + windowManager.minimize(); + }, text: 'Accept', textColor: Colors.white), + ], + ), + ), Expanded( child: buildButton(context, color: Colors.transparent, @@ -621,7 +628,7 @@ class _CmControlPanel extends StatelessWidget { ); } return Container( - height: 35, + height: 30, decoration: BoxDecoration( color: color, borderRadius: BorderRadius.circular(4), border: border), child: InkWell( diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index cae4485cb..a7cd7a11c 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -255,6 +255,9 @@ class FfiModel with ChangeNotifier { } else if (type == 'restarting') { showMsgBox(id, type, title, text, link, false, dialogManager, hasCancel: false); + } else if (type == 'wait-remote-accept-nook') { + msgBoxCommon(dialogManager, title, Text(translate(text)), + [msgBoxButton("Cancel", closeConnection)]); } else { var hasRetry = evt['hasRetry'] == 'true'; showMsgBox(id, type, title, text, link, hasRetry, dialogManager); diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index 1c0d1cbdd..a00630b22 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -31,6 +31,7 @@ class ServerModel with ChangeNotifier { int _connectStatus = 0; // Rendezvous Server status String _verificationMethod = ""; String _temporaryPasswordLength = ""; + String _approveMode = ""; late String _emptyIdShow; late final IDTextEditingController _serverId; @@ -68,6 +69,8 @@ class ServerModel with ChangeNotifier { return _verificationMethod; } + String get approveMode => _approveMode; + setVerificationMethod(String method) async { await bind.mainSetOption(key: "verification-method", value: method); } @@ -84,6 +87,10 @@ class ServerModel with ChangeNotifier { await bind.mainSetOption(key: "temporary-password-length", value: length); } + setApproveMode(String mode) async { + await bind.mainSetOption(key: 'approve-mode', value: mode); + } + TextEditingController get serverId => _serverId; TextEditingController get serverPasswd => _serverPasswd; @@ -98,8 +105,7 @@ class ServerModel with ChangeNotifier { _emptyIdShow = translate("Generating ..."); _serverId = IDTextEditingController(text: _emptyIdShow); - Timer.periodic(Duration(seconds: 1), (timer) async { - if (isTest) return timer.cancel(); + timerCallback() async { var status = await bind.mainGetOnlineStatue(); if (status > 0) { status = 1; @@ -115,7 +121,14 @@ class ServerModel with ChangeNotifier { } updatePasswordModel(); - }); + } + + if (!isTest) { + Future.delayed(Duration.zero, timerCallback); + Timer.periodic(Duration(milliseconds: 500), (timer) async { + await timerCallback(); + }); + } } /// 1. check android permission @@ -151,11 +164,17 @@ class ServerModel with ChangeNotifier { await bind.mainGetOption(key: "verification-method"); final temporaryPasswordLength = await bind.mainGetOption(key: "temporary-password-length"); + final approveMode = await bind.mainGetOption(key: 'approve-mode'); + if (_approveMode != approveMode) { + _approveMode = approveMode; + update = true; + } final oldPwdText = _serverPasswd.text; if (_serverPasswd.text != temporaryPassword) { _serverPasswd.text = temporaryPassword; } - if (verificationMethod == kUsePermanentPassword) { + if (verificationMethod == kUsePermanentPassword || + _approveMode == 'click') { _serverPasswd.text = '-'; } if (oldPwdText != _serverPasswd.text) { diff --git a/src/client.rs b/src/client.rs index c98561967..672a35693 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1337,6 +1337,15 @@ impl LoginConfigHandler { self.password = Default::default(); interface.msgbox("re-input-password", err, "Do you want to enter again?", ""); true + } else if err == "No Password Access" { + self.password = Default::default(); + interface.msgbox( + "wait-remote-accept-nook", + "Prompt", + "Please wait for the remote side to accept your session request...", + "", + ); + true } else { if err.contains(SCRAP_X11_REQUIRED) { interface.msgbox("error", "Login Error", err, SCRAP_X11_REF_URL); @@ -1434,11 +1443,7 @@ impl LoginConfigHandler { username: self.id.clone(), password: password.into(), my_id, - my_name: if cfg!(windows) { - crate::platform::get_active_username() - } else { - crate::username() - }, + my_name: crate::username(), option: self.get_option_message(true).into(), session_id: self.session_id, version: crate::VERSION.to_string(), diff --git a/src/core_main.rs b/src/core_main.rs index 13a2ef91c..f890a9525 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -226,6 +226,7 @@ pub fn core_main() -> Option> { // 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(); } } //_async_logger_holder.map(|x| x.flush()); diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 36e38f86e..928bca26b 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -562,7 +562,7 @@ pub fn main_get_connect_status() -> String { pub fn main_check_connect_status() { #[cfg(not(any(target_os = "android", target_os = "ios")))] - check_mouse_time(); // avoid multi calls + start_option_status_sync(); // avoid multi calls } pub fn main_is_using_public_server() -> bool { diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 1958b5b94..a70fce1c9 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -389,5 +389,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("This PC", "Aquest PC"), ("or", "o"), ("Continue with", "Continuar amb"), + ("Accept sessions via password", ""), + ("Accept sessions via click", ""), + ("Accept sessions via both", ""), + ("Please wait for the remote side to accept your session request...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 16bbdb590..4588a148f 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -390,5 +390,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Continue with", "使用"), ("Elevate", "提权"), ("Zoom cursor", "缩放鼠标"), + ("Accept sessions via password", "只允许密码访问"), + ("Accept sessions via click", "只允许点击访问"), + ("Accept sessions via both", "允许密码或点击访问"), + ("Please wait for the remote side to accept your session request...", "请等待对方接受你的连接..."), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 0f262cd25..46817bbc4 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -390,5 +390,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Continue with", ""), ("Elevate", ""), ("Zoom cursor", ""), + ("Accept sessions via password", ""), + ("Accept sessions via click", ""), + ("Accept sessions via both", ""), + ("Please wait for the remote side to accept your session request...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index c7362e26f..6678e734a 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -390,5 +390,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Continue with", ""), ("Elevate", ""), ("Zoom cursor", ""), + ("Accept sessions via password", ""), + ("Accept sessions via click", ""), + ("Accept sessions via both", ""), + ("Please wait for the remote side to accept your session request...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index acc22a461..28d220933 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -390,5 +390,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Continue with", ""), ("Elevate", ""), ("Zoom cursor", ""), + ("Accept sessions via password", ""), + ("Accept sessions via click", ""), + ("Accept sessions via both", ""), + ("Please wait for the remote side to accept your session request...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 206229859..2fc63ecc2 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -390,5 +390,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Continue with", ""), ("Elevate", ""), ("Zoom cursor", ""), + ("Accept sessions via password", ""), + ("Accept sessions via click", ""), + ("Accept sessions via both", ""), + ("Please wait for the remote side to accept your session request...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index ca866641a..4e04fa2a0 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -390,5 +390,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Continue with", "Continuar con"), ("Elevate", "Elevar"), ("Zoom cursor", "Ampliar cursor"), + ("Accept sessions via password", ""), + ("Accept sessions via click", ""), + ("Accept sessions via both", ""), + ("Please wait for the remote side to accept your session request...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index b602bd404..040dcf204 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -389,5 +389,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("or", "یا"), ("Continue with", "ادامه با"), ("Zoom cursor", ""), + ("Accept sessions via password", ""), + ("Accept sessions via click", ""), + ("Accept sessions via both", ""), + ("Please wait for the remote side to accept your session request...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index f4ff46cdf..926ad26fc 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -390,5 +390,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Continue with", "Continuer avec"), ("Elevate", ""), ("Zoom cursor", ""), + ("Accept sessions via password", ""), + ("Accept sessions via click", ""), + ("Accept sessions via both", ""), + ("Please wait for the remote side to accept your session request...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index aaad2e9f9..2cdb56b9b 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -390,5 +390,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Continue with", "Folytatás a következővel"), ("Elevate", ""), ("Zoom cursor", ""), + ("Accept sessions via password", ""), + ("Accept sessions via click", ""), + ("Accept sessions via both", ""), + ("Please wait for the remote side to accept your session request...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 96e7d38f4..65d447021 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -390,5 +390,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Continue with", ""), ("Elevate", ""), ("Zoom cursor", ""), + ("Accept sessions via password", ""), + ("Accept sessions via click", ""), + ("Accept sessions via both", ""), + ("Please wait for the remote side to accept your session request...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 61f289ad6..58d493785 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -390,5 +390,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Continue with", "Continua con"), ("Elevate", "Eleva"), ("Zoom cursor", "Cursore zoom"), + ("Accept sessions via password", ""), + ("Accept sessions via click", ""), + ("Accept sessions via both", ""), + ("Please wait for the remote side to accept your session request...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 1ff301bba..c0e6b667d 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -390,5 +390,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Continue with", ""), ("Elevate", ""), ("Zoom cursor", ""), + ("Accept sessions via password", ""), + ("Accept sessions via click", ""), + ("Accept sessions via both", ""), + ("Please wait for the remote side to accept your session request...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index aa6c01e3d..3f0e10645 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -390,5 +390,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Continue with", ""), ("Elevate", ""), ("Zoom cursor", ""), + ("Accept sessions via password", ""), + ("Accept sessions via click", ""), + ("Accept sessions via both", ""), + ("Please wait for the remote side to accept your session request...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 05055d1a3..a2d6a6dce 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -390,5 +390,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Continue with", ""), ("Elevate", ""), ("Zoom cursor", ""), + ("Accept sessions via password", ""), + ("Accept sessions via click", ""), + ("Accept sessions via both", ""), + ("Please wait for the remote side to accept your session request...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index c62007778..d440b06f1 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -390,5 +390,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Continue with", "Kontynuuj z"), ("Elevate", ""), ("Zoom cursor", ""), + ("Accept sessions via password", ""), + ("Accept sessions via click", ""), + ("Accept sessions via both", ""), + ("Please wait for the remote side to accept your session request...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index ca0fcead9..71d3ffb53 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -390,5 +390,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Continue with", ""), ("Elevate", ""), ("Zoom cursor", ""), + ("Accept sessions via password", ""), + ("Accept sessions via click", ""), + ("Accept sessions via both", ""), + ("Please wait for the remote side to accept your session request...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 41459404b..8a213d51c 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -390,5 +390,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Continue with", "Continuar com"), ("Elevate", ""), ("Zoom cursor", ""), + ("Accept sessions via password", ""), + ("Accept sessions via click", ""), + ("Accept sessions via both", ""), + ("Please wait for the remote side to accept your session request...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 4920c2826..db0209fa8 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -390,5 +390,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Continue with", "Продолжить с"), ("Elevate", "Повысить"), ("Zoom cursor", "Масштабировать курсор"), + ("Accept sessions via password", ""), + ("Accept sessions via click", ""), + ("Accept sessions via both", ""), + ("Please wait for the remote side to accept your session request...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 0844c8442..6d4a9d382 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -390,5 +390,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Continue with", ""), ("Elevate", ""), ("Zoom cursor", ""), + ("Accept sessions via password", ""), + ("Accept sessions via click", ""), + ("Accept sessions via both", ""), + ("Please wait for the remote side to accept your session request...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 9f1779119..22bf2ac23 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -390,5 +390,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Continue with", ""), ("Elevate", ""), ("Zoom cursor", ""), + ("Accept sessions via password", ""), + ("Accept sessions via click", ""), + ("Accept sessions via both", ""), + ("Please wait for the remote side to accept your session request...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 6f3c9ba92..bd439676e 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -390,5 +390,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Continue with", "bununla devam et"), ("Elevate", "Yükseltme"), ("Zoom cursor", ""), + ("Accept sessions via password", ""), + ("Accept sessions via click", ""), + ("Accept sessions via both", ""), + ("Please wait for the remote side to accept your session request...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 900f25ccd..ff544c1d8 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -390,5 +390,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Continue with", ""), ("Elevate", "提權"), ("Zoom cursor", ""), + ("Accept sessions via password", "只允許密碼訪問"), + ("Accept sessions via click", "只允許點擊訪問"), + ("Accept sessions via both", "允許密碼或點擊訪問"), + ("Please wait for the remote side to accept your session request...", "請等待對方接受你的連接..."), ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index b336b1e36..d7efe411b 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -390,5 +390,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Continue with", ""), ("Elevate", ""), ("Zoom cursor", ""), + ("Accept sessions via password", ""), + ("Accept sessions via click", ""), + ("Accept sessions via both", ""), + ("Please wait for the remote side to accept your session request...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index c21fe8aa8..96ff195b9 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -390,5 +390,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Continue with", ""), ("Elevate", ""), ("Zoom cursor", ""), + ("Accept sessions via password", ""), + ("Accept sessions via click", ""), + ("Accept sessions via both", ""), + ("Please wait for the remote side to accept your session request...", ""), ].iter().cloned().collect(); } diff --git a/src/server/connection.rs b/src/server/connection.rs index cb2ddc2c6..f49e293a2 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1046,6 +1046,14 @@ impl Connection { } if !crate::is_ip(&lr.username) && lr.username != Config::get_id() { self.send_login_error("Offline").await; + } else if Config::get_option("approve-mode") == "click" { + self.try_start_cm(lr.my_id, lr.my_name, false); + if hbb_common::get_version_number(&lr.version) + >= hbb_common::get_version_number("1.2.0") + { + self.send_login_error("No Password Access").await; + } + return true; } else if self.is_of_recent_session() { self.try_start_cm(lr.my_id, lr.my_name, true); self.send_logon_response().await; diff --git a/src/ui/cm.rs b/src/ui/cm.rs index 7c0e3fe24..2bd8824db 100644 --- a/src/ui/cm.rs +++ b/src/ui/cm.rs @@ -135,6 +135,10 @@ impl SciterConnectionManager { fn elevate_portable(&self, id: i32) { crate::ui_cm_interface::elevate_portable(id); } + + fn get_option(&self, key: String) -> String { + crate::ui_interface::get_option(key) + } } impl sciter::EventHandler for SciterConnectionManager { @@ -155,5 +159,6 @@ impl sciter::EventHandler for SciterConnectionManager { fn send_msg(i32, String); fn can_elevate(); fn elevate_portable(i32); + fn get_option(String); } } diff --git a/src/ui/cm.tis b/src/ui/cm.tis index 035b58650..4ecbe706c 100644 --- a/src/ui/cm.tis +++ b/src/ui/cm.tis @@ -30,6 +30,7 @@ class Body: Reactor.Component var right_style = show_chat ? "" : "display: none"; var disconnected = c.disconnected; var show_elevation_btn = handler.can_elevate() && show_elevation; + var show_accept_btn = handler.get_option('approve-mode') != 'password'; // below size:* is work around for Linux, it alreayd set in css, but not work, shit sciter return

    @@ -58,16 +59,15 @@ class Body: Reactor.Component {c.port_forward ?
    Port Forwarding: {c.port_forward}
    : ""}
    - {!auth && !disconnected && show_elevation_btn ? : "" } + {!auth && !disconnected && show_elevation_btn && show_accept_btn ? : "" } {auth && !disconnected && show_elevation_btn ? : "" }
    - {!auth ? : "" } + {!auth && show_accept_btn ? : "" } {!auth ? : "" }
    {auth && !disconnected ? : "" } {auth && disconnected ? : "" }
    -
    {c.is_file_transfer || c.port_forward ? "" :
    {svg_chat}
    }
    @@ -453,6 +453,21 @@ function getElaspsed(time, now) { return out; } +var ui_status_cache = [""]; +function check_update_ui() { + self.timer(1s, function() { + var approve_mode = handler.get_option('approve-mode'); + var changed = false; + if (ui_status_cache[0] != approve_mode) { + ui_status_cache[0] = approve_mode; + changed = true; + } + if (changed) update(); + check_update_ui(); + }); +} +check_update_ui(); + function updateTime() { self.timer(1s, function() { var now = new Date(); diff --git a/src/ui/common.tis b/src/ui/common.tis index 7507d4895..b6723b131 100644 --- a/src/ui/common.tis +++ b/src/ui/common.tis @@ -264,6 +264,13 @@ function msgbox(type, title, content, link="", callback=null, height=180, width= }; } else if (type.indexOf("custom") < 0 && !is_port_forward && !callback) { callback = function() { view.close(); } + } else if (type == 'wait-remote-accept-nook') { + callback = function (res) { + if (!res) { + view.close(); + return; + } + }; } last_msgbox_tag = type + "-" + title + "-" + content + "-" + link; $(#msgbox).content(); diff --git a/src/ui/index.tis b/src/ui/index.tis index 31781c35e..840198896 100644 --- a/src/ui/index.tis +++ b/src/ui/index.tis @@ -824,7 +824,8 @@ function watch_screen_recording() { class PasswordEyeArea : Reactor.Component { render() { var method = handler.get_option('verification-method'); - var value = method != 'use-permanent-password' ? password_cache[0] : "-"; + var mode= handler.get_option('approve-mode'); + var value = mode == 'click' || method == 'use-permanent-password' ? "-" : password_cache[0]; return
    @@ -901,27 +902,46 @@ class PasswordArea: Reactor.Component { function renderPop() { var method = handler.get_option('verification-method'); + var approve_mode= handler.get_option('approve-mode'); + var show_password = approve_mode != 'click'; return -
  • {svg_checkmark}{translate('Use temporary password')}
  • -
  • {svg_checkmark}{translate('Use permanent password')}
  • -
  • {svg_checkmark}{translate('Use both passwords')}
  • -
    -
  • {translate('Set permanent password')}
  • - +
  • {svg_checkmark}{translate('Accept sessions via password')}
  • +
  • {svg_checkmark}{translate('Accept sessions via click')}
  • +
  • {svg_checkmark}{translate('Accept sessions via both')}
  • + { !show_password ? '' :
    } + { !show_password ? '' :
  • {svg_checkmark}{translate('Use temporary password')}
  • } + { !show_password ? '' :
  • {svg_checkmark}{translate('Use permanent password')}
  • } + { !show_password ? '' :
  • {svg_checkmark}{translate('Use both passwords')}
  • } + { !show_password ? '' :
    } + { !show_password ? '' :
  • {translate('Set permanent password')}
  • } + { !show_password ? '' : } ; } function toggleMenuState() { - var id = handler.get_option('verification-method'); - if (id != 'use-temporary-password' && id != 'use-permanent-password') - id = 'use-both-passwords'; - for (var el in [this.$(li#use-temporary-password), this.$(li#use-permanent-password), this.$(li#use-both-passwords)]) { - el.attributes.toggleClass("selected", el.id == id); + var mode= handler.get_option('approve-mode'); + var mode_id; + if (mode == 'password') + mode_id = 'approve-mode-password'; + else if (mode == 'click') + mode_id = 'approve-mode-click'; + else + mode_id = 'approve-mode-both'; + var pwd_id = handler.get_option('verification-method'); + if (pwd_id != 'use-temporary-password' && pwd_id != 'use-permanent-password') + pwd_id = 'use-both-passwords'; + for (var el in this.$$(menu#edit-password-context>li)) { + if (el.id.indexOf("approve-mode-") == 0) + el.attributes.toggleClass("selected", el.id == mode_id); + if (el.id.indexOf("use-") == 0) + el.attributes.toggleClass("selected", el.id == pwd_id); } } event click $(svg#edit) (_, me) { - temporaryPasswordLengthMenu.update({show: true }); + var approve_mode= handler.get_option('approve-mode'); + var show_password = approve_mode != 'click'; + if(show_password && temporaryPasswordLengthMenu) temporaryPasswordLengthMenu.update({show: true }); var menu = $(menu#edit-password-context); me.popup(menu); } @@ -954,16 +974,28 @@ class PasswordArea: Reactor.Component { handler.set_option('verification-method', me.id); this.toggleMenuState(); passwordArea.update(); + } else if (me.id.indexOf('approve-mode') == 0) { + var approve_mode; + if (me.id == 'approve-mode-password') + approve_mode = 'password'; + else if (me.id == 'approve-mode-click') + approve_mode = 'click'; + else + approve_mode = ''; + handler.set_option('approve-mode', approve_mode); + this.toggleMenuState(); + passwordArea.update(); } } } -var password_cache = ["","",""]; +var password_cache = ["","","",""]; function updatePasswordArea() { self.timer(1s, function() { var temporary_password = handler.temporary_password(); var verification_method = handler.get_option('verification-method'); var temporary_password_length = handler.get_option('temporary-password-length'); + var approve_mode = handler.get_option('approve-mode'); var update = false; if (password_cache[0] != temporary_password) { password_cache[0] = temporary_password; @@ -977,6 +1009,10 @@ function updatePasswordArea() { password_cache[2] = temporary_password_length; update = true; } + if (password_cache[3] != approve_mode) { + password_cache[3] = approve_mode; + update = true; + } if (update) passwordArea.update(); updatePasswordArea(); }); diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 0e443ad61..e1dc3005e 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -874,7 +874,12 @@ pub fn check_zombie(children: Children) { } } -pub(crate) fn check_connect_status(reconnect: bool) -> mpsc::UnboundedSender { +pub fn start_option_status_sync() { + let _sender = SENDER.lock().unwrap(); +} + +// not call directly +fn check_connect_status(reconnect: bool) -> mpsc::UnboundedSender { let (tx, rx) = mpsc::unbounded_channel::(); std::thread::spawn(move || check_connect_status_(reconnect, rx)); tx @@ -898,7 +903,7 @@ pub fn account_auth_result() -> String { // notice: avoiding create ipc connecton repeatly, // because windows named pipe has serious memory leak issue. #[tokio::main(flavor = "current_thread")] -pub(crate) async fn check_connect_status_(reconnect: bool, rx: mpsc::UnboundedReceiver) { +async fn check_connect_status_(reconnect: bool, rx: mpsc::UnboundedReceiver) { let mut key_confirmed = false; let mut rx = rx; let mut mouse_time = 0; From 2952f151a006c51bf7120f7769c80b8813ead1ed Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sun, 20 Nov 2022 21:10:40 +0800 Subject: [PATCH 0986/2015] modify copyright in mac --- flutter/macos/Runner/Configs/AppInfo.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/macos/Runner/Configs/AppInfo.xcconfig b/flutter/macos/Runner/Configs/AppInfo.xcconfig index bf05a4caa..389ae0a70 100644 --- a/flutter/macos/Runner/Configs/AppInfo.xcconfig +++ b/flutter/macos/Runner/Configs/AppInfo.xcconfig @@ -11,4 +11,4 @@ PRODUCT_NAME = rustdesk PRODUCT_BUNDLE_IDENTIFIER = com.carriez.flutterHbb // The copyright displayed in application information -PRODUCT_COPYRIGHT = Copyright © 2022 com.carriez. All rights reserved. +PRODUCT_COPYRIGHT = Copyright © 2022 Purslane Ltd. All rights reserved. From 13fd55557be8a0f68dcdd0ed2f31d009ae4e7fb0 Mon Sep 17 00:00:00 2001 From: xxrl <837951112@qq.com> Date: Sun, 20 Nov 2022 22:46:27 +0800 Subject: [PATCH 0987/2015] feat: support track pad scroll on flutter --- flutter/lib/common/widgets/remote_input.dart | 3 ++ flutter/lib/models/input_model.dart | 57 ++++++++++++++++++++ src/client.rs | 11 ++-- src/flutter_ffi.rs | 2 + src/server/input_service.rs | 2 +- 5 files changed, 71 insertions(+), 4 deletions(-) diff --git a/flutter/lib/common/widgets/remote_input.dart b/flutter/lib/common/widgets/remote_input.dart index ad50d4839..89443e14f 100644 --- a/flutter/lib/common/widgets/remote_input.dart +++ b/flutter/lib/common/widgets/remote_input.dart @@ -51,6 +51,9 @@ class RawPointerMouseRegion extends StatelessWidget { onPointerUp: inputModel.onPointUpImage, onPointerMove: inputModel.onPointMoveImage, onPointerSignal: inputModel.onPointerSignalImage, + onPointerPanZoomStart: inputModel.onPointerPanZoomStart, + onPointerPanZoomUpdate: inputModel.onPointerPanZoomUpdate, + onPointerPanZoomEnd: inputModel.onPointerPanZoomEnd, child: MouseRegion( cursor: cursor ?? MouseCursor.defer, onEnter: onEnter, diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index 83171514d..eb1b4a3ff 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:convert'; import 'dart:math'; import 'dart:ui' as ui; @@ -39,6 +40,10 @@ class InputModel { var alt = false; var command = false; + // trackpad + var trackpadScrollDistance = Offset.zero; + Timer? _flingTimer; + // mouse final isPhysicalMouse = false.obs; int _lastMouseDownButtons = 0; @@ -236,6 +241,7 @@ class InputModel { if (!enter) { resetModifiers(); } + _flingTimer?.cancel(); bind.sessionEnterOrLeave(id: id, enter: enter); } @@ -258,6 +264,57 @@ class InputModel { } } + int _signOrZero(num x) { + if (x == 0) { + return 0; + } else { + return x > 0 ? 1 : -1; + } + } + + void onPointerPanZoomStart(PointerPanZoomStartEvent e) {} + + // https://docs.flutter.dev/release/breaking-changes/trackpad-gestures + // TODO(support zoom in/out) + void onPointerPanZoomUpdate(PointerPanZoomUpdateEvent e) { + var delta = e.panDelta; + trackpadScrollDistance += delta; + bind.sessionSendMouse( + id: id, + msg: + '{"type": "trackpad", "x": "${delta.dx.toInt()}", "y": "${delta.dy.toInt()}"}'); + } + + // Simple simulation for fling. + void _scheduleFling(var x, y, dx, dy) { + if (dx <= 0 && dy <= 0) { + return; + } + _flingTimer = Timer(Duration(milliseconds: 10), () { + bind.sessionSendMouse( + id: id, msg: '{"type": "trackpad", "x": "$x", "y": "$y"}'); + dx--; + dy--; + if (dx == 0) { + x = 0; + } + if (dy == 0) { + y = 0; + } + _scheduleFling(x, y, dx, dy); + }); + } + + void onPointerPanZoomEnd(PointerPanZoomEndEvent e) { + var x = _signOrZero(trackpadScrollDistance.dx); + var y = _signOrZero(trackpadScrollDistance.dy); + var dx = trackpadScrollDistance.dx.abs() ~/ 40; + var dy = trackpadScrollDistance.dy.abs() ~/ 40; + _scheduleFling(x, y, dx, dy); + + trackpadScrollDistance = Offset.zero; + } + void onPointDownImage(PointerDownEvent e) { debugPrint("onPointDownImage"); if (e.kind != ui.PointerDeviceKind.mouse) { diff --git a/src/client.rs b/src/client.rs index 3f738a369..b3be51cb4 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1572,9 +1572,14 @@ pub async fn handle_test_delay(t: TestDelay, peer: &mut Stream) { } } +/// Whether is track pad scrolling. #[inline] -#[cfg(all(target_os = "macos", not(feature = "flutter")))] +#[cfg(all(target_os = "macos"))] fn check_scroll_on_mac(mask: i32, x: i32, y: i32) -> bool { + // flutter version we set mask type bit to 4 when track pad scrolling. + if mask & 7 == 4 { + return true; + } if mask & 3 != 3 { return false; } @@ -1597,7 +1602,7 @@ fn check_scroll_on_mac(mask: i32, x: i32, y: i32) -> bool { /// /// * `mask` - Mouse event. /// * mask = buttons << 3 | type -/// * type, 1: down, 2: up, 3: wheel +/// * type, 1: down, 2: up, 3: wheel, 4: trackpad /// * buttons, 1: left, 2: right, 4: middle /// * `x` - X coordinate. /// * `y` - Y coordinate. @@ -1636,7 +1641,7 @@ pub fn send_mouse( if command { mouse_event.modifiers.push(ControlKey::Meta.into()); } - #[cfg(all(target_os = "macos", not(feature = "flutter")))] + #[cfg(all(target_os = "macos"))] if check_scroll_on_mac(mask, x, y) { mouse_event.modifiers.push(ControlKey::Scroll.into()); } diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 36e38f86e..bfee92f88 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -156,6 +156,7 @@ pub fn session_reconnect(id: String) { pub fn session_toggle_option(id: String, value: String) { if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) { + log::warn!("toggle option {}", value); session.toggle_option(value); } } @@ -907,6 +908,7 @@ pub fn session_send_mouse(id: String, msg: String) { "down" => 1, "up" => 2, "wheel" => 3, + "trackpad" => 4, _ => 0, }; } diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 005c828bc..ca63fed94 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -513,7 +513,7 @@ pub fn handle_mouse_(evt: &MouseEvent) { } _ => {} }, - 3 => { + 3 | 4 => { #[allow(unused_mut)] let mut x = evt.x; #[allow(unused_mut)] From 6f390759f301d39549cd800f44291a305700e193 Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 21 Nov 2022 14:06:32 +0800 Subject: [PATCH 0988/2015] rename temporary password to one-time password Signed-off-by: 21pages --- flutter/lib/desktop/pages/desktop_home_page.dart | 6 ++++-- flutter/lib/desktop/pages/desktop_setting_page.dart | 4 ++-- flutter/lib/models/server_model.dart | 6 ++++-- 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/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/pl.rs | 6 +++--- src/lang/pt_PT.rs | 6 +++--- src/lang/ptbr.rs | 6 +++--- src/lang/ru.rs | 6 +++--- src/lang/sk.rs | 6 +++--- src/lang/template.rs | 6 +++--- src/lang/tr.rs | 6 +++--- src/lang/tw.rs | 6 +++--- src/lang/ua.rs | 6 +++--- src/lang/vn.rs | 6 +++--- src/ui/index.tis | 6 +++--- 29 files changed, 88 insertions(+), 84 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index e7d6f50e8..244d315ce 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:io'; import 'dart:convert'; +import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart' hide MenuItem; import 'package:flutter/services.dart'; import 'package:flutter_hbb/common.dart'; @@ -202,10 +203,11 @@ class _DesktopHomePageState extends State child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - translate("Password"), + AutoSizeText( + translate("One-time Password"), style: TextStyle( fontSize: 14, color: textColor?.withOpacity(0.5)), + maxLines: 1, ), Row( children: [ diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 64ebc712e..b5aeaa7c3 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -586,7 +586,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { kUseBothPasswords, ]; List passwordValues = [ - translate('Use temporary password'), + translate('Use one-time password'), translate('Use permanent password'), translate('Use both passwords'), ]; @@ -665,7 +665,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { Offstage( offstage: !usePassword, child: _SubLabeledWidget( - 'Temporary Password Length', + 'One-time password length', Row( children: [ ...lengthRadios, diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index a00630b22..be3f02b5d 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -35,7 +35,8 @@ class ServerModel with ChangeNotifier { late String _emptyIdShow; late final IDTextEditingController _serverId; - final _serverPasswd = TextEditingController(text: ""); + final _serverPasswd = + TextEditingController(text: translate("Generating ...")); final tabController = DesktopTabController(tabType: DesktopTabType.cm); @@ -170,7 +171,8 @@ class ServerModel with ChangeNotifier { update = true; } final oldPwdText = _serverPasswd.text; - if (_serverPasswd.text != temporaryPassword) { + if (_serverPasswd.text != temporaryPassword && + temporaryPassword.isNotEmpty) { _serverPasswd.text = temporaryPassword; } if (verificationMethod == kUsePermanentPassword || diff --git a/src/lang/ca.rs b/src/lang/ca.rs index a70fce1c9..99150acd0 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -303,11 +303,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Legacy mode", "Mode heretat"), ("Map mode", "Mode mapa"), ("Translate mode", "Mode traduit"), - ("Use temporary password", "Utilitzar contrasenya temporal"), ("Use permanent password", "Utilitzar contrasenya permament"), ("Use both passwords", "Utilitzar ambdues contrasenyas"), ("Set permanent password", "Establir contrasenya permament"), - ("Set temporary password length", "Establir llargada de la contrasenya temporal"), ("Enable Remote Restart", "Activar reinici remot"), ("Allow remote restart", "Permetre reinici remot"), ("Restart Remote Device", "Reiniciar dispositiu"), @@ -343,7 +341,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable hardware codec", "Habilitar còdec per hardware"), ("Unlock Security Settings", "Desbloquejar ajustaments de seguritat"), ("Enable Audio", "Habilitar àudio"), - ("Temporary Password Length", "Longitut de Contrasenya Temporal"), ("Unlock Network Settings", "Desbloquejar Ajustaments de Xarxa"), ("Server", "Servidor"), ("Direct IP Access", "Accés IP Directe"), @@ -393,5 +390,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Accept sessions via click", ""), ("Accept sessions via both", ""), ("Please wait for the remote side to accept your session request...", ""), + ("One-time Password", ""), + ("Use one-time password", ""), + ("One-time password length", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 4588a148f..5e38c3d27 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -303,11 +303,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Legacy mode", "传统模式"), ("Map mode", "1:1传输"), ("Translate mode", "翻译模式"), - ("Use temporary password", "使用临时密码"), ("Use permanent password", "使用固定密码"), ("Use both passwords", "同时使用两种密码"), ("Set permanent password", "设置固定密码"), - ("Set temporary password length", "设置临时密码长度"), ("Enable Remote Restart", "允许远程重启"), ("Allow remote restart", "允许远程重启"), ("Restart Remote Device", "重启远程电脑"), @@ -343,7 +341,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable hardware codec", "使用硬件编解码"), ("Unlock Security Settings", "解锁安全设置"), ("Enable Audio", "允许传输音频"), - ("Temporary Password Length", "临时密码长度"), ("Unlock Network Settings", "解锁网络设置"), ("Server", "服务器"), ("Direct IP Access", "IP直接访问"), @@ -394,5 +391,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Accept sessions via click", "只允许点击访问"), ("Accept sessions via both", "允许密码或点击访问"), ("Please wait for the remote side to accept your session request...", "请等待对方接受你的连接..."), + ("One-time Password", "一次性密码"), + ("Use one-time password", "使用一次性密码"), + ("One-time password length", "一次性密码长度"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 46817bbc4..6081e4212 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -303,11 +303,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Legacy mode", ""), ("Map mode", ""), ("Translate mode", ""), - ("Use temporary password", ""), ("Use permanent password", ""), ("Use both passwords", ""), ("Set permanent password", ""), - ("Set temporary password length", ""), ("Enable Remote Restart", ""), ("Allow remote restart", ""), ("Restart Remote Device", ""), @@ -343,7 +341,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable hardware codec", ""), ("Unlock Security Settings", ""), ("Enable Audio", ""), - ("Temporary Password Length", ""), ("Unlock Network Settings", ""), ("Server", ""), ("Direct IP Access", ""), @@ -394,5 +391,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Accept sessions via click", ""), ("Accept sessions via both", ""), ("Please wait for the remote side to accept your session request...", ""), + ("One-time Password", ""), + ("Use one-time password", ""), + ("One-time password length", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 6678e734a..ab1dbff38 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -303,11 +303,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Legacy mode", "Bagudkompatibilitetstilstand"), ("Map mode", ""), ("Translate mode", ""), - ("Use temporary password", "Brug midlertidig adgangskode"), ("Use permanent password", "Brug permanent adgangskode"), ("Use both passwords", "Bug begge adgangskoder"), ("Set permanent password", "Sæt permanent adgangskode"), - ("Set temporary password length", "Sæt midlertidig adgangskode"), ("Enable Remote Restart", "Aktiver fjerngenstart"), ("Allow remote restart", "Tillad fjerngenstart"), ("Restart Remote Device", "Genstart fjernenhed"), @@ -343,7 +341,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable hardware codec", "Aktiver hardware-codec"), ("Unlock Security Settings", "Lås op for sikkerhedsinstillinger"), ("Enable Audio", "Aktiver Lyd"), - ("Temporary Password Length", "Midlertidig Adgangskode Længde"), ("Unlock Network Settings", "Lås op for Netværksinstillinger"), ("Server", "Server"), ("Direct IP Access", "Direkte IP Adgang"), @@ -394,5 +391,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Accept sessions via click", ""), ("Accept sessions via both", ""), ("Please wait for the remote side to accept your session request...", ""), + ("One-time Password", ""), + ("Use one-time password", ""), + ("One-time password length", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 28d220933..328c5128b 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -303,11 +303,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Legacy mode", "Kompatibilitätsmodus"), ("Map mode", ""), ("Translate mode", "Übersetzungsmodus"), - ("Use temporary password", "Temporäres Passwort verwenden"), ("Use permanent password", "Dauerhaftes Passwort verwenden"), ("Use both passwords", "Beide Passwörter verwenden"), ("Set permanent password", "Dauerhaftes Passwort setzen"), - ("Set temporary password length", "Länge des temporären Passworts setzen"), ("Enable Remote Restart", "Entfernten Neustart aktivieren"), ("Allow remote restart", "Entfernten Neustart erlauben"), ("Restart Remote Device", "Entferntes Gerät neu starten"), @@ -343,7 +341,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable hardware codec", "Hardware-Codec aktivieren"), ("Unlock Security Settings", "Sicherheitseinstellungen entsperren"), ("Enable Audio", "Audio aktivieren"), - ("Temporary Password Length", "Länge des temporären Passworts"), ("Unlock Network Settings", "Netzwerkeinstellungen entsperren"), ("Server", "Server"), ("Direct IP Access", "Direkter IP-Zugriff"), @@ -394,5 +391,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Accept sessions via click", ""), ("Accept sessions via both", ""), ("Please wait for the remote side to accept your session request...", ""), + ("One-time Password", ""), + ("Use one-time password", ""), + ("One-time password length", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 2fc63ecc2..9cd6eacd1 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -303,11 +303,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Legacy mode", ""), ("Map mode", ""), ("Translate mode", ""), - ("Use temporary password", ""), ("Use permanent password", ""), ("Use both passwords", ""), ("Set permanent password", ""), - ("Set temporary password length", ""), ("Enable Remote Restart", ""), ("Allow remote restart", ""), ("Restart Remote Device", ""), @@ -343,7 +341,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable hardware codec", ""), ("Unlock Security Settings", ""), ("Enable Audio", ""), - ("Temporary Password Length", ""), ("Unlock Network Settings", ""), ("Server", ""), ("Direct IP Access", ""), @@ -394,5 +391,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Accept sessions via click", ""), ("Accept sessions via both", ""), ("Please wait for the remote side to accept your session request...", ""), + ("One-time Password", ""), + ("Use one-time password", ""), + ("One-time password length", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 4e04fa2a0..210dc4352 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -303,11 +303,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Legacy mode", "Modo heredado"), ("Map mode", "Modo mapa"), ("Translate mode", "Modo traducido"), - ("Use temporary password", "Usar contraseña temporal"), ("Use permanent password", "Usar contraseña permamente"), ("Use both passwords", "Usar ambas contraseñas"), ("Set permanent password", "Establecer contraseña permamente"), - ("Set temporary password length", "Establecer largo de contraseña temporal"), ("Enable Remote Restart", "Activar reinicio remoto"), ("Allow remote restart", "Permitir reinicio remoto"), ("Restart Remote Device", "Reiniciar dispositivo"), @@ -343,7 +341,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable hardware codec", "Habilitar códec por hardware"), ("Unlock Security Settings", "Desbloquear ajustes de seguridad"), ("Enable Audio", "Habilitar Audio"), - ("Temporary Password Length", "Longitud de Contraseña Temporal"), ("Unlock Network Settings", "Desbloquear Ajustes de Red"), ("Server", "Servidor"), ("Direct IP Access", "Acceso IP Directo"), @@ -394,5 +391,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Accept sessions via click", ""), ("Accept sessions via both", ""), ("Please wait for the remote side to accept your session request...", ""), + ("One-time Password", ""), + ("Use one-time password", ""), + ("One-time password length", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 040dcf204..c6b5f60a6 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -303,11 +303,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Legacy mode", "پشتیبانی موارد قدیمی"), ("Map mode", "حالت نقشه"), ("Translate mode", "حالت ترجمه"), - ("Use temporary password", "از رمز عبور موقت استفاده کنید"), ("Use permanent password", "از رمز عبور دائمی استفاده کنید"), ("Use both passwords", "از هر دو رمز عبور استفاده کنید"), ("Set permanent password", "یک رمز عبور دائمی تنظیم کنید"), - ("Set temporary password length", "تنظیم طول رمز عبور موقت"), ("Enable Remote Restart", "فعال کردن راه‌اندازی مجدد از راه دور"), ("Allow remote restart", "اجازه راه اندازی مجدد از راه دور"), ("Restart Remote Device", "راه‌اندازی مجدد دستگاه از راه دور"), @@ -343,7 +341,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable hardware codec", "از کدک سخت افزاری استفاده کنید"), ("Unlock Security Settings", "تنظیمات امنیتی را باز کنید"), ("Enable Audio", "صدا را روشن کنید"), - ("Temporary Password Length", "طول رمز عبور موقت"), ("Unlock Network Settings", "باز کردن قفل تنظیمات شبکه"), ("Server", "سرور"), ("Direct IP Access", "دسترسی مستقیم به IP"), @@ -393,5 +390,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Accept sessions via click", ""), ("Accept sessions via both", ""), ("Please wait for the remote side to accept your session request...", ""), + ("One-time Password", ""), + ("Use one-time password", ""), + ("One-time password length", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 926ad26fc..b24fb71da 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -303,11 +303,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Legacy mode", "Mode hérité"), ("Map mode", ""), ("Translate mode", ""), - ("Use temporary password", "Utiliser un mot de passe temporaire"), ("Use permanent password", "Utiliser un mot de passe permanent"), ("Use both passwords", "Utiliser les mots de passe temporaire et permanent"), ("Set permanent password", "Définir le mot de passe permanent"), - ("Set temporary password length", "Définir la longueur du mot de passe temporaire"), ("Enable Remote Restart", "Activer le redémarrage à distance"), ("Allow remote restart", "Autoriser le redémarrage à distance"), ("Restart Remote Device", "Redémarrer l'appareil à distance"), @@ -343,7 +341,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable hardware codec", "Activer le transcodage matériel"), ("Unlock Security Settings", "Déverrouiller les configurations de sécurité"), ("Enable Audio", "Activer l'audio"), - ("Temporary Password Length", "Longueur mot de passe temporaire"), ("Unlock Network Settings", "Déverrouiller les configurations réseau"), ("Server", "Serveur"), ("Direct IP Access", "Accès IP direct"), @@ -394,5 +391,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Accept sessions via click", ""), ("Accept sessions via both", ""), ("Please wait for the remote side to accept your session request...", ""), + ("One-time Password", ""), + ("Use one-time password", ""), + ("One-time password length", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 2cdb56b9b..b65ff2579 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -303,11 +303,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Legacy mode", ""), ("Map mode", ""), ("Translate mode", "Fordító mód"), - ("Use temporary password", "Ideiglenes jelszó használata"), ("Use permanent password", "Állandó jelszó használata"), ("Use both passwords", "Mindkét jelszó használata"), ("Set permanent password", "Állandó jelszó beállítása"), - ("Set temporary password length", "Ideiglenes jelszó hosszának beállítása"), ("Enable Remote Restart", "Távoli újraindítás engedélyezése"), ("Allow remote restart", "Távoli újraindítás engedélyezése"), ("Restart Remote Device", "Távoli eszköz újraindítása"), @@ -343,7 +341,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable hardware codec", "Hardveres kodek engedélyezése"), ("Unlock Security Settings", "Biztonsági beállítások feloldása"), ("Enable Audio", "Hang engedélyezése"), - ("Temporary Password Length", "Ideiglenes jelszó hossza"), ("Unlock Network Settings", "Hálózati beállítások feloldása"), ("Server", "Szerver"), ("Direct IP Access", "Közvetlen IP hozzáférés"), @@ -394,5 +391,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Accept sessions via click", ""), ("Accept sessions via both", ""), ("Please wait for the remote side to accept your session request...", ""), + ("One-time Password", ""), + ("Use one-time password", ""), + ("One-time password length", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 65d447021..0fb623606 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -303,11 +303,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Legacy mode", "Mode lama"), ("Map mode", "Mode peta"), ("Translate mode", "Mode terjemahan"), - ("Use temporary password", "Gunakan kata sandi sementara"), ("Use permanent password", "Gunakan kata sandi permanaen"), ("Use both passwords", "Gunakan kedua kata sandi "), ("Set permanent password", "Setel kata sandi permanen"), - ("Set temporary password length", "Setel panjang kata sandi sementara"), ("Enable Remote Restart", "Aktifkan Restart Jarak Jauh"), ("Allow remote restart", "Ijinkan Restart Jarak Jauh"), ("Restart Remote Device", "Restart Perangkat Jarak Jauh"), @@ -343,7 +341,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable hardware codec", "Aktifkan codec perangkat keras"), ("Unlock Security Settings", "Buka Kunci Pengaturan Keamanan"), ("Enable Audio", "Aktifkan Audio"), - ("Temporary Password Length", "Panjang Kata Sandi Sementara"), ("Unlock Network Settings", "Buka Kunci Pengaturan Jaringan"), ("Server", "Server"), ("Direct IP Access", "Direct IP Access"), @@ -394,5 +391,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Accept sessions via click", ""), ("Accept sessions via both", ""), ("Please wait for the remote side to accept your session request...", ""), + ("One-time Password", ""), + ("Use one-time password", ""), + ("One-time password length", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 58d493785..47c186fe5 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -303,11 +303,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Legacy mode", "Modalità legacy"), ("Map mode", "Modalità mappa"), ("Translate mode", "Modalità di traduzione"), - ("Use temporary password", "Usa password temporanea"), ("Use permanent password", "Usa password permanente"), ("Use both passwords", "Usa entrambe le password"), ("Set permanent password", "Imposta password permanente"), - ("Set temporary password length", "Imposta lunghezza passwod temporanea"), ("Enable Remote Restart", "Abilita riavvio da remoto"), ("Allow remote restart", "Consenti riavvio da remoto"), ("Restart Remote Device", "Riavvia dispositivo remoto"), @@ -343,7 +341,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable hardware codec", "Abilita codec hardware"), ("Unlock Security Settings", "Sblocca impostazioni di sicurezza"), ("Enable Audio", "Abilita audio"), - ("Temporary Password Length", "Lunghezza password temporanea"), ("Unlock Network Settings", "Sblocca impostazioni di rete"), ("Server", ""), ("Direct IP Access", "Accesso IP diretto"), @@ -394,5 +391,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Accept sessions via click", ""), ("Accept sessions via both", ""), ("Please wait for the remote side to accept your session request...", ""), + ("One-time Password", ""), + ("Use one-time password", ""), + ("One-time password length", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index c0e6b667d..fcd579c9f 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -303,11 +303,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Legacy mode", ""), ("Map mode", ""), ("Translate mode", ""), - ("Use temporary password", "使い捨てのパスワードを使用"), ("Use permanent password", "固定のパスワードを使用"), ("Use both passwords", "どちらのパスワードも使用"), ("Set permanent password", "固定のパスワードを設定"), - ("Set temporary password length", "使い捨てのパスワードの長さを設定"), ("Enable Remote Restart", "リモートからの再起動を有効化"), ("Allow remote restart", "リモートからの再起動を許可"), ("Restart Remote Device", "リモートの端末を再起動"), @@ -343,7 +341,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable hardware codec", ""), ("Unlock Security Settings", ""), ("Enable Audio", ""), - ("Temporary Password Length", ""), ("Unlock Network Settings", ""), ("Server", ""), ("Direct IP Access", ""), @@ -394,5 +391,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Accept sessions via click", ""), ("Accept sessions via both", ""), ("Please wait for the remote side to accept your session request...", ""), + ("One-time Password", ""), + ("Use one-time password", ""), + ("One-time password length", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 3f0e10645..ee28e05eb 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -303,11 +303,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Legacy mode", ""), ("Map mode", ""), ("Translate mode", ""), - ("Use temporary password", "임시 비밀번호 사용"), ("Use permanent password", "영구 비밀번호 사용"), ("Use both passwords", "두 비밀번호 (임시/영구) 사용"), ("Set permanent password", "영구 비밀번호 설정"), - ("Set temporary password length", "임시 비밀번호 길이 설정"), ("Enable Remote Restart", "원격지 재시작 활성화"), ("Allow remote restart", "원격지 재시작 허용"), ("Restart Remote Device", "원격 기기 재시작"), @@ -343,7 +341,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable hardware codec", ""), ("Unlock Security Settings", ""), ("Enable Audio", ""), - ("Temporary Password Length", ""), ("Unlock Network Settings", ""), ("Server", ""), ("Direct IP Access", ""), @@ -394,5 +391,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Accept sessions via click", ""), ("Accept sessions via both", ""), ("Please wait for the remote side to accept your session request...", ""), + ("One-time Password", ""), + ("Use one-time password", ""), + ("One-time password length", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index a2d6a6dce..768e735b8 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -303,11 +303,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Legacy mode", ""), ("Map mode", ""), ("Translate mode", ""), - ("Use temporary password", "Уақытша құпия сөзді қолдану"), ("Use permanent password", "Тұрақты құпия сөзді қолдану"), ("Use both passwords", "Қос құпия сөзді қолдану"), ("Set permanent password", "Тұрақты құпия сөзді орнату"), - ("Set temporary password length", "Уақытша құпия сөздің ұзындығын орнату"), ("Enable Remote Restart", "Қашықтан қайта-қосуды іске қосу"), ("Allow remote restart", "Қашықтан қайта-қосуды рұқсат ету"), ("Restart Remote Device", "Қашықтағы құрылғыны қайта-қосу"), @@ -343,7 +341,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable hardware codec", ""), ("Unlock Security Settings", ""), ("Enable Audio", ""), - ("Temporary Password Length", ""), ("Unlock Network Settings", ""), ("Server", ""), ("Direct IP Access", ""), @@ -394,5 +391,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Accept sessions via click", ""), ("Accept sessions via both", ""), ("Please wait for the remote side to accept your session request...", ""), + ("One-time Password", ""), + ("Use one-time password", ""), + ("One-time password length", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index d440b06f1..c6ba009d3 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -303,11 +303,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Legacy mode", "Tryb kompatybilności wstecznej (legacy)"), ("Map mode", "Tryb mapowania"), ("Translate mode", "Tryb translacji"), - ("Use temporary password", "Użyj tymczasowego hasła"), ("Use permanent password", "Użyj hasła permanentnego"), ("Use both passwords", "Użyj obu haseł"), ("Set permanent password", "Ustaw hasło permanentne"), - ("Set temporary password length", "Ustaw długość hasła tymczasowego"), ("Enable Remote Restart", "Włącz Zdalne Restartowanie"), ("Allow remote restart", "Zezwól na zdalne restartowanie"), ("Restart Remote Device", "Zrestartuj Zdalne Urządzenie"), @@ -343,7 +341,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable hardware codec", "Włącz wsparcie sprzętowe dla kodeków"), ("Unlock Security Settings", "Odblokuj Ustawienia Zabezpieczeń"), ("Enable Audio", "Włącz Dźwięk"), - ("Temporary Password Length", "Długość hasła tymaczowego"), ("Unlock Network Settings", "Odblokuj ustawienia Sieciowe"), ("Server", "Serwer"), ("Direct IP Access", "Bezpośredni Adres IP"), @@ -394,5 +391,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Accept sessions via click", ""), ("Accept sessions via both", ""), ("Please wait for the remote side to accept your session request...", ""), + ("One-time Password", ""), + ("Use one-time password", ""), + ("One-time password length", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 71d3ffb53..97800a8f7 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -303,11 +303,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Legacy mode", ""), ("Map mode", ""), ("Translate mode", ""), - ("Use temporary password", "Utilizar palavra-chave temporária"), ("Use permanent password", "Utilizar palavra-chave permanente"), ("Use both passwords", "Utilizar ambas as palavras-chave"), ("Set permanent password", "Definir palavra-chave permanente"), - ("Set temporary password length", "Definir tamanho de palavra-chave temporária"), ("Enable Remote Restart", "Activar reiniciar remoto"), ("Allow remote restart", "Permitir reiniciar remoto"), ("Restart Remote Device", "Reiniciar Dispositivo Remoto"), @@ -343,7 +341,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable hardware codec", ""), ("Unlock Security Settings", ""), ("Enable Audio", ""), - ("Temporary Password Length", ""), ("Unlock Network Settings", ""), ("Server", ""), ("Direct IP Access", ""), @@ -394,5 +391,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Accept sessions via click", ""), ("Accept sessions via both", ""), ("Please wait for the remote side to accept your session request...", ""), + ("One-time Password", ""), + ("Use one-time password", ""), + ("One-time password length", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 8a213d51c..f773a0a93 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -303,11 +303,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Legacy mode", "Modo legado"), ("Map mode", "Modo mapa"), ("Translate mode", "Modo traduzido"), - ("Use temporary password", "Utilizar senha temporária"), ("Use permanent password", "Utilizar senha permanente"), ("Use both passwords", "Utilizar ambas as senhas"), ("Set permanent password", "Configurar senha permanente"), - ("Set temporary password length", "Configurar extensão da senha temporária"), ("Enable Remote Restart", "Habilitar reinicialização remota"), ("Allow remote restart", "Permitir reinicialização remota"), ("Restart Remote Device", "Reiniciar dispositivo remoto"), @@ -343,7 +341,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable hardware codec", "Habilitar codec de hardware"), ("Unlock Security Settings", "Desabilitar configurações de segurança"), ("Enable Audio", "Habilitar áudio"), - ("Temporary Password Length", "Extensão da senha temporária"), ("Unlock Network Settings", "Desbloquear configurações de rede"), ("Server", "Servidor"), ("Direct IP Access", "Acesso direto por IP"), @@ -394,5 +391,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Accept sessions via click", ""), ("Accept sessions via both", ""), ("Please wait for the remote side to accept your session request...", ""), + ("One-time Password", ""), + ("Use one-time password", ""), + ("One-time password length", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index db0209fa8..fdee117c5 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -303,11 +303,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Legacy mode", "Устаревший режим"), ("Map mode", "Режим сопоставления"), ("Translate mode", "Режим перевода"), - ("Use temporary password", "Использовать временный пароль"), ("Use permanent password", "Использовать постоянный пароль"), ("Use both passwords", "Использовать оба пароля"), ("Set permanent password", "Установить постоянный пароль"), - ("Set temporary password length", "Длина временного пароля"), ("Enable Remote Restart", "Включить удалённый перезапуск"), ("Allow remote restart", "Разрешить удалённый перезапуск"), ("Restart Remote Device", "Перезапустить удалённое устройство"), @@ -343,7 +341,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable hardware codec", "Использовать аппаратный кодек"), ("Unlock Security Settings", "Разблокировать настройки безопасности"), ("Enable Audio", "Включить звук"), - ("Temporary Password Length", "Длинна временного пароля"), ("Unlock Network Settings", "Разблокировать настройки соединения"), ("Server", "Сервер"), ("Direct IP Access", "Прямой IP-доступ"), @@ -394,5 +391,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Accept sessions via click", ""), ("Accept sessions via both", ""), ("Please wait for the remote side to accept your session request...", ""), + ("One-time Password", ""), + ("Use one-time password", ""), + ("One-time password length", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 6d4a9d382..4013a95c0 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -303,11 +303,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Legacy mode", ""), ("Map mode", ""), ("Translate mode", ""), - ("Use temporary password", ""), ("Use permanent password", ""), ("Use both passwords", ""), ("Set permanent password", ""), - ("Set temporary password length", ""), ("Enable Remote Restart", ""), ("Allow remote restart", ""), ("Restart Remote Device", ""), @@ -343,7 +341,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable hardware codec", ""), ("Unlock Security Settings", ""), ("Enable Audio", ""), - ("Temporary Password Length", ""), ("Unlock Network Settings", ""), ("Server", ""), ("Direct IP Access", ""), @@ -394,5 +391,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Accept sessions via click", ""), ("Accept sessions via both", ""), ("Please wait for the remote side to accept your session request...", ""), + ("One-time Password", ""), + ("Use one-time password", ""), + ("One-time password length", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 22bf2ac23..bd0f87519 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -303,11 +303,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Legacy mode", ""), ("Map mode", ""), ("Translate mode", ""), - ("Use temporary password", ""), ("Use permanent password", ""), ("Use both passwords", ""), ("Set permanent password", ""), - ("Set temporary password length", ""), ("Enable Remote Restart", ""), ("Allow remote restart", ""), ("Restart Remote Device", ""), @@ -343,7 +341,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable hardware codec", ""), ("Unlock Security Settings", ""), ("Enable Audio", ""), - ("Temporary Password Length", ""), ("Unlock Network Settings", ""), ("Server", ""), ("Direct IP Access", ""), @@ -394,5 +391,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Accept sessions via click", ""), ("Accept sessions via both", ""), ("Please wait for the remote side to accept your session request...", ""), + ("One-time Password", ""), + ("Use one-time password", ""), + ("One-time password length", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index bd439676e..5aef77c22 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -303,11 +303,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Legacy mode", "Eski mod"), ("Map mode", "Haritalama modu"), ("Translate mode", "Çeviri modu"), - ("Use temporary password", "Geçici şifre kullan"), ("Use permanent password", "Kalıcı şifre kullan"), ("Use both passwords", "İki şifreyide kullan"), ("Set permanent password", "Kalıcı şifre oluştur"), - ("Set temporary password length", "Geçici şifre oluştur"), ("Enable Remote Restart", "Uzaktan yeniden başlatmayı aktif et"), ("Allow remote restart", "Uzaktan yeniden başlatmaya izin ver"), ("Restart Remote Device", "Uzaktaki cihazı yeniden başlat"), @@ -343,7 +341,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable hardware codec", "Donanımsal codec aktif et"), ("Unlock Security Settings", "Güvenlik Ayarlarını Aç"), ("Enable Audio", "Sesi Aktif Et"), - ("Temporary Password Length", "Geçici Şifre Uzunluğu"), ("Unlock Network Settings", "Ağ Ayarlarını Aç"), ("Server", "Sunucu"), ("Direct IP Access", "Direk IP Erişimi"), @@ -394,5 +391,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Accept sessions via click", ""), ("Accept sessions via both", ""), ("Please wait for the remote side to accept your session request...", ""), + ("One-time Password", ""), + ("Use one-time password", ""), + ("One-time password length", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index ff544c1d8..7e3c451f7 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -303,11 +303,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Legacy mode", "傳統模式"), ("Map mode", "1:1傳輸"), ("Translate mode", "翻譯模式"), - ("Use temporary password", "使用臨時密碼"), ("Use permanent password", "使用固定密碼"), ("Use both passwords", "同時使用兩種密碼"), ("Set permanent password", "設定固定密碼"), - ("Set temporary password length", "設定臨時密碼長度"), ("Enable Remote Restart", "允許遠程重啓"), ("Allow remote restart", "允許遠程重啓"), ("Restart Remote Device", "重啓遠程電腦"), @@ -343,7 +341,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable hardware codec", "使用硬件編解碼"), ("Unlock Security Settings", "解鎖安全設置"), ("Enable Audio", "允許傳輸音頻"), - ("Temporary Password Length", "臨時密碼長度"), ("Unlock Network Settings", "解鎖網絡設置"), ("Server", "服務器"), ("Direct IP Access", "IP直接訪問"), @@ -394,5 +391,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Accept sessions via click", "只允許點擊訪問"), ("Accept sessions via both", "允許密碼或點擊訪問"), ("Please wait for the remote side to accept your session request...", "請等待對方接受你的連接..."), + ("One-time Password", "一次性密碼"), + ("Use one-time password", "使用一次性密碼"), + ("One-time password length", "一次性密碼長度"), ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index d7efe411b..656d7665b 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -303,11 +303,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Legacy mode", ""), ("Map mode", ""), ("Translate mode", ""), - ("Use temporary password", "Використовувати тимчасовий пароль"), ("Use permanent password", "Використовувати постійний пароль"), ("Use both passwords", "Використовувати обидва паролі"), ("Set permanent password", "Встановити постійний пароль"), - ("Set temporary password length", "Довжина тимчасового пароля"), ("Enable Remote Restart", "Увімкнути віддалений перезапуск"), ("Allow remote restart", "Дозволити віддалений перезапуск"), ("Restart Remote Device", "Перезапустити віддалений пристрій"), @@ -343,7 +341,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable hardware codec", "Увімкнути апаратний кодек"), ("Unlock Security Settings", "Розблокувати налаштування безпеки"), ("Enable Audio", "Вімкнути аудіо"), - ("Temporary Password Length", "Довжина тимчасового пароля"), ("Unlock Network Settings", "Розблокувати мережеві налаштування"), ("Server", "Сервер"), ("Direct IP Access", "Прямий IP доступ"), @@ -394,5 +391,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Accept sessions via click", ""), ("Accept sessions via both", ""), ("Please wait for the remote side to accept your session request...", ""), + ("One-time Password", ""), + ("Use one-time password", ""), + ("One-time password length", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 96ff195b9..a5dc66db8 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -303,11 +303,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Legacy mode", ""), ("Map mode", ""), ("Translate mode", ""), - ("Use temporary password", "Sử dụng mật khẩu tạm thời"), ("Use permanent password", "Sử dụng mật khẩu vĩnh viễn"), ("Use both passwords", "Sử dụng cả hai mật khẩu"), ("Set permanent password", "Đặt mật khẩu vĩnh viễn"), - ("Set temporary password length", "Đặt chiều dài của mật khẩu tạm thời"), ("Enable Remote Restart", "Bật khởi động lại từ xa"), ("Allow remote restart", "Cho phép khởi động lại từ xa"), ("Restart Remote Device", "Khởi động lại thiết bị từ xa"), @@ -343,7 +341,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable hardware codec", ""), ("Unlock Security Settings", ""), ("Enable Audio", ""), - ("Temporary Password Length", ""), ("Unlock Network Settings", ""), ("Server", ""), ("Direct IP Access", ""), @@ -394,5 +391,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Accept sessions via click", ""), ("Accept sessions via both", ""), ("Please wait for the remote side to accept your session request...", ""), + ("One-time Password", ""), + ("Use one-time password", ""), + ("One-time password length", ""), ].iter().cloned().collect(); } diff --git a/src/ui/index.tis b/src/ui/index.tis index 840198896..81cb588bc 100644 --- a/src/ui/index.tis +++ b/src/ui/index.tis @@ -850,7 +850,7 @@ class TemporaryPasswordLengthMenu: Reactor.Component { var me = this; var method = handler.get_option('verification-method'); self.timer(1ms, function() { me.toggleMenuState() }); - return
  • {translate("Set temporary password length")} + return
  • {translate("One-time password length")}
  • {svg_checkmark}6
  • {svg_checkmark}8
  • @@ -891,7 +891,7 @@ class PasswordArea: Reactor.Component { self.timer(1ms, function() { me.toggleMenuState() }); return
    -
    {translate('Password')}
    +
    {translate('One-time Password')}
    {this.renderPop()} @@ -909,7 +909,7 @@ class PasswordArea: Reactor.Component {
  • {svg_checkmark}{translate('Accept sessions via click')}
  • {svg_checkmark}{translate('Accept sessions via both')}
  • { !show_password ? '' :
    } - { !show_password ? '' :
  • {svg_checkmark}{translate('Use temporary password')}
  • } + { !show_password ? '' :
  • {svg_checkmark}{translate('Use one-time password')}
  • } { !show_password ? '' :
  • {svg_checkmark}{translate('Use permanent password')}
  • } { !show_password ? '' :
  • {svg_checkmark}{translate('Use both passwords')}
  • } { !show_password ? '' :
    } From 617e64d01f010d65f84b24a5999c3ac0b6dfcaf2 Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 21 Nov 2022 15:29:00 +0800 Subject: [PATCH 0989/2015] fix approve mode judgement Signed-off-by: 21pages --- libs/hbb_common/src/password_security.rs | 18 ++++++++++++++++++ src/server/connection.rs | 16 ++++++++++------ 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/libs/hbb_common/src/password_security.rs b/libs/hbb_common/src/password_security.rs index 55a6825fa..adaafebb3 100644 --- a/libs/hbb_common/src/password_security.rs +++ b/libs/hbb_common/src/password_security.rs @@ -13,6 +13,13 @@ enum VerificationMethod { UseBothPasswords, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ApproveMode { + Both, + Password, + Click, +} + // Should only be called in server pub fn update_temporary_password() { *TEMPORARY_PASSWORD.write().unwrap() = Config::get_auto_password(temporary_password_length()); @@ -58,6 +65,17 @@ pub fn has_valid_password() -> bool { || permanent_enabled() && !Config::get_permanent_password().is_empty() } +pub fn approve_mode() -> ApproveMode { + let mode = Config::get_option("approve-mode"); + if mode == "password" { + ApproveMode::Password + } else if mode == "click" { + ApproveMode::Click + } else { + ApproveMode::Both + } +} + const VERSION_LEN: usize = 2; pub fn encrypt_str_or_original(s: &str, version: &str) -> String { diff --git a/src/server/connection.rs b/src/server/connection.rs index f49e293a2..11960be8a 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -14,7 +14,8 @@ use hbb_common::{ futures::{SinkExt, StreamExt}, get_time, get_version_number, message_proto::{option_message::BoolOption, permission_info::Permission}, - password_security as password, sleep, timeout, + password_security::{self as password, ApproveMode}, + sleep, timeout, tokio::{ net::TcpStream, sync::mpsc, @@ -1046,7 +1047,9 @@ impl Connection { } if !crate::is_ip(&lr.username) && lr.username != Config::get_id() { self.send_login_error("Offline").await; - } else if Config::get_option("approve-mode") == "click" { + } else if password::approve_mode() == ApproveMode::Click + || password::approve_mode() == ApproveMode::Both && !password::has_valid_password() + { self.try_start_cm(lr.my_id, lr.my_name, false); if hbb_common::get_version_number(&lr.version) >= hbb_common::get_version_number("1.2.0") @@ -1054,6 +1057,11 @@ impl Connection { self.send_login_error("No Password Access").await; } return true; + } else if password::approve_mode() == ApproveMode::Password + && !password::has_valid_password() + { + self.send_login_error("Connection not allowed").await; + return false; } else if self.is_of_recent_session() { self.try_start_cm(lr.my_id, lr.my_name, true); self.send_logon_response().await; @@ -1063,10 +1071,6 @@ impl Connection { } else if lr.password.is_empty() { self.try_start_cm(lr.my_id, lr.my_name, false); } else { - if !password::has_valid_password() { - self.send_login_error("Connection not allowed").await; - return false; - } let mut failure = LOGIN_FAILURES .lock() .unwrap() From be9d04ff246a4a6bbe75029fb42871c4124c8620 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Mon, 21 Nov 2022 16:29:29 +0800 Subject: [PATCH 0990/2015] remove trackpad support in 3.0.5, will revert once upgrade to 3.3 --- flutter/lib/common/widgets/remote_input.dart | 2 ++ flutter/lib/models/input_model.dart | 2 ++ 2 files changed, 4 insertions(+) diff --git a/flutter/lib/common/widgets/remote_input.dart b/flutter/lib/common/widgets/remote_input.dart index 89443e14f..3a79d24fb 100644 --- a/flutter/lib/common/widgets/remote_input.dart +++ b/flutter/lib/common/widgets/remote_input.dart @@ -51,9 +51,11 @@ class RawPointerMouseRegion extends StatelessWidget { onPointerUp: inputModel.onPointUpImage, onPointerMove: inputModel.onPointMoveImage, onPointerSignal: inputModel.onPointerSignalImage, + /* onPointerPanZoomStart: inputModel.onPointerPanZoomStart, onPointerPanZoomUpdate: inputModel.onPointerPanZoomUpdate, onPointerPanZoomEnd: inputModel.onPointerPanZoomEnd, + */ child: MouseRegion( cursor: cursor ?? MouseCursor.defer, onEnter: onEnter, diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index eb1b4a3ff..bd1131c7a 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -264,6 +264,7 @@ class InputModel { } } +/* int _signOrZero(num x) { if (x == 0) { return 0; @@ -314,6 +315,7 @@ class InputModel { trackpadScrollDistance = Offset.zero; } +*/ void onPointDownImage(PointerDownEvent e) { debugPrint("onPointDownImage"); From 1f3042007148ab847da650c5ec41ec0be2ae68c2 Mon Sep 17 00:00:00 2001 From: mehdi-song Date: Mon, 21 Nov 2022 12:24:04 +0330 Subject: [PATCH 0991/2015] Update fa.rs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update fa.rs ("Accept sessions via password", "قبول درخواست با رمز عبور"), ("Accept sessions via click", "قبول درخواست با کلیک موس"), ("Accept sessions via both", "قبول درخواست با هر دو"), ("Please wait for the remote side to accept your session request...", "لطفا صبر کنید تا میزبان درخواست شما را قبول کند..."), ("One-time Password", "رمز عبور یکبار مصرف"), ("Use one-time password", "استفاده از رمز عبور یکبار مصرف"), ("One-time password length", "طول رمز عبور یکبار مصرف"), --- src/lang/fa.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/lang/fa.rs b/src/lang/fa.rs index c6b5f60a6..7432dd8a1 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -385,13 +385,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("This PC", "This PC"), ("or", "یا"), ("Continue with", "ادامه با"), - ("Zoom cursor", ""), - ("Accept sessions via password", ""), - ("Accept sessions via click", ""), - ("Accept sessions via both", ""), - ("Please wait for the remote side to accept your session request...", ""), - ("One-time Password", ""), - ("Use one-time password", ""), - ("One-time password length", ""), + ("Zoom cursor", "نشانگر بزرگنمایی"), + ("Accept sessions via password", "قبول درخواست با رمز عبور"), + ("Accept sessions via click", "قبول درخواست با کلیک موس"), + ("Accept sessions via both", "قبول درخواست با هر دو"), + ("Please wait for the remote side to accept your session request...", "لطفا صبر کنید تا میزبان درخواست شما را قبول کند..."), + ("One-time Password", "رمز عبور یکبار مصرف"), + ("Use one-time password", "استفاده از رمز عبور یکبار مصرف"), + ("One-time password length", "طول رمز عبور یکبار مصرف"), ].iter().cloned().collect(); } From 2d528dde6fc2976808f540ff29a339172c53299b Mon Sep 17 00:00:00 2001 From: NicKoehler <53040044+NicKoehler@users.noreply.github.com> Date: Mon, 21 Nov 2022 11:43:20 +0100 Subject: [PATCH 0992/2015] Update it.rs --- src/lang/it.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/lang/it.rs b/src/lang/it.rs index 47c186fe5..af49382c0 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -298,7 +298,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Language", "Linguaggio"), ("Keep RustDesk background service", "Mantieni il servizio di RustDesk in background"), ("Ignore Battery Optimizations", "Ignora le ottimizzazioni della batteria"), - ("android_open_battery_optimizations_tip", ""), + ("android_open_battery_optimizations_tip", "Se si desidera disabilitare questa funzione, andare nelle impostazioni dell'applicazione RustDesk, aprire la sezione [Batteria] e deselezionare [Senza restrizioni]."), ("Connection not allowed", "Connessione non consentita"), ("Legacy mode", "Modalità legacy"), ("Map mode", "Modalità mappa"), @@ -369,7 +369,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Write a message", "Scrivi un messaggio"), ("Prompt", ""), ("Please wait for confirmation of UAC...", "Attendi la conferma dell'UAC..."), - ("elevated_foreground_window_tip", ""), + ("elevated_foreground_window_tip", "La finestra corrente del desktop remoto richiede privilegi più elevati per funzionare, quindi non è in grado di utilizzare temporaneamente il mouse e la tastiera. È possibile chiedere all'utente remoto di ridurre a icona la finestra corrente o di fare clic sul pulsante di elevazione nella finestra di gestione della connessione. Per evitare questo problema, si consiglia di installare il software sul dispositivo remoto."), ("Disconnected", "Disconnesso"), ("Other", "Altro"), ("Confirm before closing multiple tabs", "Conferma prima di chiudere più schede"), @@ -387,12 +387,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Continue with", "Continua con"), ("Elevate", "Eleva"), ("Zoom cursor", "Cursore zoom"), - ("Accept sessions via password", ""), - ("Accept sessions via click", ""), - ("Accept sessions via both", ""), - ("Please wait for the remote side to accept your session request...", ""), - ("One-time Password", ""), - ("Use one-time password", ""), - ("One-time password length", ""), + ("Accept sessions via password", "Accetta sessioni via password"), + ("Accept sessions via click", "Accetta sessioni via click"), + ("Accept sessions via both", "Accetta sessioni con entrambi"), + ("Please wait for the remote side to accept your session request...", "Attendere che il lato remoto accetti la richiesta di sessione..."), + ("One-time Password", "Password monouso"), + ("Use one-time password", "Usa password monouso"), + ("One-time password length", "Lunghezza password monouso"), ].iter().cloned().collect(); } From 16165dae27cc85f7fd738d95c46b1f0d87ea68ae Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 21 Nov 2022 16:37:15 +0800 Subject: [PATCH 0993/2015] allow set empty permanent password to delete it Signed-off-by: 21pages --- flutter/lib/desktop/pages/desktop_home_page.dart | 2 +- src/ui/index.tis | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 244d315ce..f7b07cf3a 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -486,7 +486,7 @@ void setPasswordDialog() async { errMsg1 = ""; }); final pass = p0.text.trim(); - if (pass.length < 6) { + if (pass.length < 6 && pass.isNotEmpty) { setState(() { errMsg0 = translate("Too short, at least 6 characters."); }); diff --git a/src/ui/index.tis b/src/ui/index.tis index 81cb588bc..8e2238b2d 100644 --- a/src/ui/index.tis +++ b/src/ui/index.tis @@ -958,7 +958,7 @@ class PasswordArea: Reactor.Component { if (!res) return; var p0 = (res.password || "").trim(); var p1 = (res.confirmation || "").trim(); - if (p0.length < 6) { + if (p0.length < 6 && p0.length != 0) { return translate("Too short, at least 6 characters."); } if (p0 != p1) { From 048fcf4016238979fa7bd4e5bf1de386406ead26 Mon Sep 17 00:00:00 2001 From: fufesou Date: Mon, 21 Nov 2022 18:56:27 +0800 Subject: [PATCH 0994/2015] fix win cursor color Signed-off-by: fufesou --- flutter/lib/models/model.dart | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index a7cd7a11c..7a08fc671 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -975,7 +975,7 @@ class CursorModel with ChangeNotifier { final image = await img.decodeImageFromPixels( rgba, width, height, ui.PixelFormat.rgba8888); _image = image; - if (await _updateCache(image, id, width, height)) { + if (await _updateCache(rgba, image, id, width, height)) { _images[id] = Tuple3(image, _hotx, _hoty); } else { _hotx = 0; @@ -989,22 +989,25 @@ class CursorModel with ChangeNotifier { } } - Future _updateCache(ui.Image image, int id, int w, int h) async { - ui.ImageByteFormat imgFormat = ui.ImageByteFormat.png; + Future _updateCache( + Uint8List rgba, ui.Image image, int id, int w, int h) async { + Uint8List? data; + img2.Image? imgOrigin; if (Platform.isWindows) { - imgFormat = ui.ImageByteFormat.rawRgba; + imgOrigin = img2.Image.fromBytes(w, h, rgba, format: img2.Format.rgba); + data = imgOrigin.getBytes(format: img2.Format.bgra); + } else { + ByteData? imgBytes = + await image.toByteData(format: ui.ImageByteFormat.png); + if (imgBytes == null) { + return false; + } + data = imgBytes.buffer.asUint8List(); } - - ByteData? imgBytes = await image.toByteData(format: imgFormat); - if (imgBytes == null) { - return false; - } - - Uint8List? data = imgBytes.buffer.asUint8List(); _cache = CursorData( peerId: this.id, id: id, - image: Platform.isWindows ? img2.Image.fromBytes(w, h, data) : null, + image: imgOrigin, scale: 1.0, data: data, hotxOrigin: _hotx, From a9773035c927222e5a1eeee905292ccd24adb8af Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 21 Nov 2022 18:45:36 +0800 Subject: [PATCH 0995/2015] cm show requesting rather than connected when not authorized Signed-off-by: 21pages --- flutter/lib/desktop/pages/server_page.dart | 17 +++++++++++------ 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/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/ru.rs | 1 + src/lang/sk.rs | 1 + src/lang/template.rs | 1 + src/lang/tr.rs | 1 + src/lang/tw.rs | 1 + src/lang/ua.rs | 1 + src/lang/vn.rs | 1 + src/ui/cm.tis | 8 ++++++-- 27 files changed, 42 insertions(+), 8 deletions(-) diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index ae69497bc..aae6da8fc 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -304,7 +304,9 @@ class _CmHeaderState extends State<_CmHeader> void initState() { super.initState(); _timer = Timer.periodic(Duration(seconds: 1), (_) { - if (!client.disconnected) _time.value = _time.value + 1; + if (client.authorized && !client.disconnected) { + _time.value = _time.value + 1; + } }); } @@ -358,12 +360,15 @@ class _CmHeaderState extends State<_CmHeader> FittedBox( child: Row( children: [ - Text(client.disconnected - ? translate("Disconnected") - : translate("Connected")) + Text(client.authorized + ? client.disconnected + ? translate("Disconnected") + : translate("Connected") + : "${translate("Request access to your device")}...") .marginOnly(right: 8.0), - Obx(() => Text( - formatDurationToTime(Duration(seconds: _time.value)))) + if (client.authorized) + Obx(() => Text( + formatDurationToTime(Duration(seconds: _time.value)))) ], )) ], diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 99150acd0..f5684b4c4 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -393,5 +393,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("One-time Password", ""), ("Use one-time password", ""), ("One-time password length", ""), + ("Request access to your device", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 5e38c3d27..7256c5d1f 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -394,5 +394,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("One-time Password", "一次性密码"), ("Use one-time password", "使用一次性密码"), ("One-time password length", "一次性密码长度"), + ("Request access to your device", "请求访问你的设备"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 6081e4212..63fac7288 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -394,5 +394,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("One-time Password", ""), ("Use one-time password", ""), ("One-time password length", ""), + ("Request access to your device", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index ab1dbff38..4278cdc20 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -394,5 +394,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("One-time Password", ""), ("Use one-time password", ""), ("One-time password length", ""), + ("Request access to your device", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 328c5128b..5d82f84d1 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -394,5 +394,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("One-time Password", ""), ("Use one-time password", ""), ("One-time password length", ""), + ("Request access to your device", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 9cd6eacd1..3c7ac806a 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -394,5 +394,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("One-time Password", ""), ("Use one-time password", ""), ("One-time password length", ""), + ("Request access to your device", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 210dc4352..5dd471a4b 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -394,5 +394,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("One-time Password", ""), ("Use one-time password", ""), ("One-time password length", ""), + ("Request access to your device", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 7432dd8a1..0ea1f6f55 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -393,5 +393,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("One-time Password", "رمز عبور یکبار مصرف"), ("Use one-time password", "استفاده از رمز عبور یکبار مصرف"), ("One-time password length", "طول رمز عبور یکبار مصرف"), + ("Request access to your device", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index b24fb71da..4b8d0d83e 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -394,5 +394,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("One-time Password", ""), ("Use one-time password", ""), ("One-time password length", ""), + ("Request access to your device", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index b65ff2579..9802ddb6f 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -394,5 +394,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("One-time Password", ""), ("Use one-time password", ""), ("One-time password length", ""), + ("Request access to your device", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 0fb623606..002789066 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -394,5 +394,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("One-time Password", ""), ("Use one-time password", ""), ("One-time password length", ""), + ("Request access to your device", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 47c186fe5..46602dfd4 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -394,5 +394,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("One-time Password", ""), ("Use one-time password", ""), ("One-time password length", ""), + ("Request access to your device", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index fcd579c9f..b032fbe7e 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -394,5 +394,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("One-time Password", ""), ("Use one-time password", ""), ("One-time password length", ""), + ("Request access to your device", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index ee28e05eb..338fc7ffe 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -394,5 +394,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("One-time Password", ""), ("Use one-time password", ""), ("One-time password length", ""), + ("Request access to your device", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 768e735b8..3a343da21 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -394,5 +394,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("One-time Password", ""), ("Use one-time password", ""), ("One-time password length", ""), + ("Request access to your device", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index c6ba009d3..b4f760a35 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -394,5 +394,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("One-time Password", ""), ("Use one-time password", ""), ("One-time password length", ""), + ("Request access to your device", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 97800a8f7..dbb5fbbe9 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -394,5 +394,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("One-time Password", ""), ("Use one-time password", ""), ("One-time password length", ""), + ("Request access to your device", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index f773a0a93..7d9c0b270 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -394,5 +394,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("One-time Password", ""), ("Use one-time password", ""), ("One-time password length", ""), + ("Request access to your device", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index fdee117c5..ded00af23 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -394,5 +394,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("One-time Password", ""), ("Use one-time password", ""), ("One-time password length", ""), + ("Request access to your device", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 4013a95c0..8da18e035 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -394,5 +394,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("One-time Password", ""), ("Use one-time password", ""), ("One-time password length", ""), + ("Request access to your device", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index bd0f87519..bc9bc95e3 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -394,5 +394,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("One-time Password", ""), ("Use one-time password", ""), ("One-time password length", ""), + ("Request access to your device", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 5aef77c22..86283557f 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -394,5 +394,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("One-time Password", ""), ("Use one-time password", ""), ("One-time password length", ""), + ("Request access to your device", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 7e3c451f7..cc8f65e1c 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -394,5 +394,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("One-time Password", "一次性密碼"), ("Use one-time password", "使用一次性密碼"), ("One-time password length", "一次性密碼長度"), + ("Request access to your device", "請求訪問你的設備"), ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 656d7665b..7d14ee7e0 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -394,5 +394,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("One-time Password", ""), ("Use one-time password", ""), ("One-time password length", ""), + ("Request access to your device", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index a5dc66db8..3ddacdf8d 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -394,5 +394,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("One-time Password", ""), ("Use one-time password", ""), ("One-time password length", ""), + ("Request access to your device", ""), ].iter().cloned().collect(); } diff --git a/src/ui/cm.tis b/src/ui/cm.tis index 4ecbe706c..2cfc14bf1 100644 --- a/src/ui/cm.tis +++ b/src/ui/cm.tis @@ -41,7 +41,10 @@ class Body: Reactor.Component
    {c.name}
    ({c.peer_id})
    -
    {disconnected ? translate('Disconnected') : translate('Connected')} {" "} {getElaspsed(c.time, c.now)}
    +
    {auth + ? {disconnected ? translate('Disconnected') : translate('Connected')}{" "}{getElaspsed(c.time, c.now)} + : {translate('Request access to your device')}{"..."}} +
    @@ -472,12 +475,13 @@ function updateTime() { self.timer(1s, function() { var now = new Date(); connections.map(function(c) { + if (!c.authorized) c.time = now; if (!c.disconnected) c.now = now; }); var el = $(#time); if (el) { var c = connections[body.cur]; - if (c && !c.disconnected) { + if (c && c.authorized && !c.disconnected) { el.text = getElaspsed(c.time, c.now); } } From ff9a538e2d07acfc73bf257eedadd1e998fc85f9 Mon Sep 17 00:00:00 2001 From: NicKoehler <53040044+NicKoehler@users.noreply.github.com> Date: Mon, 21 Nov 2022 15:24:30 +0100 Subject: [PATCH 0996/2015] 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 61f9cad5e..3ca466f22 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -394,6 +394,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("One-time Password", "Password monouso"), ("Use one-time password", "Usa password monouso"), ("One-time password length", "Lunghezza password monouso"), - ("Request access to your device", ""), + ("Request access to your device", "Richiedi accesso al tuo dispositivo"), ].iter().cloned().collect(); } From 90ea283746b9b1cfb7c2fe2d6024c6cf37cfd4ed Mon Sep 17 00:00:00 2001 From: KrystianGraba Date: Mon, 21 Nov 2022 18:32:53 +0100 Subject: [PATCH 0997/2015] Update pl.rs --- src/lang/pl.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/lang/pl.rs b/src/lang/pl.rs index b4f760a35..ecc09ce2a 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -385,15 +385,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("This PC", "Ten komputer"), ("or", "albo"), ("Continue with", "Kontynuuj z"), - ("Elevate", ""), - ("Zoom cursor", ""), - ("Accept sessions via password", ""), - ("Accept sessions via click", ""), - ("Accept sessions via both", ""), - ("Please wait for the remote side to accept your session request...", ""), - ("One-time Password", ""), - ("Use one-time password", ""), - ("One-time password length", ""), - ("Request access to your device", ""), + ("Elevate", "Podwyższ"), + ("Zoom cursor", "Zoom kursora"), + ("Accept sessions via password", "Akceptuj sesje używając hasła"), + ("Accept sessions via click", "Akceptuj sesję klikając"), + ("Accept sessions via both", "Akceptuj sesjęna dwa sposoby"), + ("Please wait for the remote side to accept your session request...", "Proszę czekać aż zdalny host zaakceptuje Twoją prośbę..."), + ("One-time Password", "Hasło jednorazowe"), + ("Use one-time password", "Użyj hasła jednorazowego"), + ("One-time password length", "Długość hasła jednorazowego"), + ("Request access to your device", "Żądanie dostępu do Twojego urządzenia"), ].iter().cloned().collect(); } From 6dfe68be9be26f70447e6f886a7fa4b7f469e88d Mon Sep 17 00:00:00 2001 From: "Miguel F. G" <116861809+flusheDData@users.noreply.github.com> Date: Mon, 21 Nov 2022 20:14:23 +0100 Subject: [PATCH 0998/2015] Update es.rs New terms translated. --- src/lang/es.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/lang/es.rs b/src/lang/es.rs index 5dd471a4b..cdebf7c59 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -387,13 +387,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Continue with", "Continuar con"), ("Elevate", "Elevar"), ("Zoom cursor", "Ampliar cursor"), - ("Accept sessions via password", ""), - ("Accept sessions via click", ""), - ("Accept sessions via both", ""), - ("Please wait for the remote side to accept your session request...", ""), - ("One-time Password", ""), - ("Use one-time password", ""), - ("One-time password length", ""), - ("Request access to your device", ""), + ("Accept sessions via password", "Aceptar sesiones a través de contraseña"), + ("Accept sessions via click", "Aceptar sesiones a través de clic"), + ("Accept sessions via both", "Aceptar sesiones a través de ambos"), + ("Please wait for the remote side to accept your session request...", "Por favor, esperar a que el lado remoto acepte la solicitud de sesión"), + ("One-time Password", "Constaseña de un solo uso"), + ("Use one-time password", "Usar contraseña de un solo uso"), + ("One-time password length", "Longitud de la contraseña de un solo uso"), + ("Request access to your device", "Solicitud de acceso a al dispositivo"), ].iter().cloned().collect(); } From 25286408e1d580c2129cd12821ae6e01926cc9b5 Mon Sep 17 00:00:00 2001 From: "Miguel F. G" <116861809+flusheDData@users.noreply.github.com> Date: Mon, 21 Nov 2022 20:16:47 +0100 Subject: [PATCH 0999/2015] 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 cdebf7c59..a59437581 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -390,10 +390,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Accept sessions via password", "Aceptar sesiones a través de contraseña"), ("Accept sessions via click", "Aceptar sesiones a través de clic"), ("Accept sessions via both", "Aceptar sesiones a través de ambos"), - ("Please wait for the remote side to accept your session request...", "Por favor, esperar a que el lado remoto acepte la solicitud de sesión"), + ("Please wait for the remote side to accept your session request...", "Por favor, espere a que el lado remoto acepte su solicitud de sesión"), ("One-time Password", "Constaseña de un solo uso"), ("Use one-time password", "Usar contraseña de un solo uso"), ("One-time password length", "Longitud de la contraseña de un solo uso"), - ("Request access to your device", "Solicitud de acceso a al dispositivo"), + ("Request access to your device", "Solicitud de acceso a su dispositivo"), ].iter().cloned().collect(); } From 32af62cccd924498f4d1251b72c3c8e0e48cd7c1 Mon Sep 17 00:00:00 2001 From: neoGalaxy88 Date: Tue, 22 Nov 2022 09:48:07 +0100 Subject: [PATCH 1000/2015] 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 61f9cad5e..76e034450 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -394,6 +394,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("One-time Password", "Password monouso"), ("Use one-time password", "Usa password monouso"), ("One-time password length", "Lunghezza password monouso"), - ("Request access to your device", ""), + ("Request access to your device", "Richiedi l'accesso al tuo dispositivo"), ].iter().cloned().collect(); } From edab4fd62d23be7695573e2ccd825a1987968958 Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 22 Nov 2022 21:34:53 +0800 Subject: [PATCH 1001/2015] fix predefined win forbidden cursor Signed-off-by: fufesou --- flutter/lib/desktop/pages/remote_page.dart | 41 +++---- flutter/lib/mobile/pages/remote_page.dart | 8 +- flutter/lib/models/model.dart | 123 ++++++++++++--------- 3 files changed, 88 insertions(+), 84 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 84ea36d78..9e775f86f 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -356,9 +356,8 @@ class _ImagePaintState extends State { } } - MouseCursor _buildCustomCursor(BuildContext context, double scale) { - final cursor = Provider.of(context); - final cache = cursor.cache ?? cursor.defaultCache; + MouseCursor _buildCursorOfCache( + CursorModel cursor, double scale, CursorData? cache) { if (cache == null) { return MouseCursor.defer; } else { @@ -375,26 +374,16 @@ class _ImagePaintState extends State { } } + MouseCursor _buildCustomCursor(BuildContext context, double scale) { + final cursor = Provider.of(context); + final cache = cursor.cache ?? preDefaultCursor.cache; + return _buildCursorOfCache(cursor, scale, cache); + } + MouseCursor _buildDisabledCursor(BuildContext context, double scale) { final cursor = Provider.of(context); - final cache = cursor.cache; - if (cache == null) { - return MouseCursor.defer; - } else { - if (cursor.cachedForbidmemoryCursorData == null) { - cursor.updateForbiddenCursorBuffer(); - } - final key = 'disabled_cursor_key'; - cursor.addKey(key); - return FlutterCustomMemoryImageCursor( - pixbuf: cursor.cachedForbidmemoryCursorData, - key: key, - hotx: 0, - hoty: 0, - imageWidth: 32, - imageHeight: 32, - ); - } + final cache = preForbiddenCursor.cache; + return _buildCursorOfCache(cursor, scale, cache); } Widget _buildCrossScrollbarFromLayout( @@ -521,22 +510,22 @@ class CursorPaint extends StatelessWidget { double hotx = m.hotx; double hoty = m.hoty; if (m.image == null) { - if (m.defaultCache != null) { - hotx = m.defaultImage!.width / 2; - hoty = m.defaultImage!.height / 2; + if (preDefaultCursor.image != null) { + hotx = preDefaultCursor.image!.width / 2; + hoty = preDefaultCursor.image!.height / 2; } } return zoomCursor.isTrue ? CustomPaint( painter: ImagePainter( - image: m.image ?? m.defaultImage, + image: m.image ?? preDefaultCursor.image, x: m.x - hotx + c.x / c.scale, y: m.y - hoty + c.y / c.scale, scale: c.scale), ) : CustomPaint( painter: ImagePainter( - image: m.image ?? m.defaultImage, + image: m.image ?? preDefaultCursor.image, x: (m.x - hotx) * c.scale + c.x, y: (m.y - hoty) * c.scale + c.y, scale: 1.0), diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index b48e9960a..b17f0ef54 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -865,14 +865,14 @@ class CursorPaint extends StatelessWidget { double hotx = m.hotx; double hoty = m.hoty; if (m.image == null) { - if (m.defaultCache != null) { - hotx = m.defaultImage!.width / 2; - hoty = m.defaultImage!.height / 2; + if (preDefaultCursor.image != null) { + hotx = preDefaultCursor.image!.width / 2; + hoty = preDefaultCursor.image!.height / 2; } } return CustomPaint( painter: ImagePainter( - image: m.image ?? m.defaultImage, + image: m.image ?? preDefaultCursor.image, x: m.x * s - hotx * s + c.x, y: m.y * s - hoty * s + c.y - adjust, scale: 1), diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 7a08fc671..b7d72f878 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -763,13 +763,78 @@ class CursorData { } } +const _forbiddenCursorPng = + 'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAAXNSR0IB2cksfwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAkZQTFRFAAAA2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4GWAwCAAAAAAAA2B4GAAAAMTExAAAAAAAA2B4G2B4G2B4GAAAAmZmZkZGRAQEBAAAA2B4G2B4G2B4G////oKCgAwMDag8D2B4G2B4G2B4Gra2tBgYGbg8D2B4G2B4Gubm5CQkJTwsCVgwC2B4GxcXFDg4OAAAAAAAA2B4G2B4Gz8/PFBQUAAAAAAAA2B4G2B4G2B4G2B4G2B4G2B4G2B4GDgIA2NjYGxsbAAAAAAAA2B4GFwMB4eHhIyMjAAAAAAAA2B4G6OjoLCwsAAAAAAAA2B4G2B4G2B4G2B4G2B4GCQEA4ODgv7+/iYmJY2NjAgICAAAA9PT0Ojo6AAAAAAAAAAAA+/v7SkpKhYWFr6+vAAAAAAAA8/PzOTk5ERER9fX1KCgoAAAAgYGBKioqAAAAAAAApqamlpaWAAAAAAAAAAAAAAAAAAAAAAAALi4u/v7+GRkZAAAAAAAAAAAAAAAAAAAAfn5+AAAAAAAAV1dXkJCQAAAAAAAAAQEBAAAAAAAAAAAA7Hz6BAAAAMJ0Uk5TAAIWEwEynNz6//fVkCAatP2fDUHs6cDD8d0mPfT5fiEskiIR584A0gejr3AZ+P4plfALf5ZiTL85a4ziD6697fzN3UYE4v/4TwrNHuT///tdRKZh///+1U/ZBv///yjb///eAVL//50Cocv//6oFBbPvpGZCbfT//7cIhv///8INM///zBEcWYSZmO7//////1P////ts/////8vBv//////gv//R/z///QQz9sevP///2waXhNO/+fc//8mev/5gAe2r90MAAAByUlEQVR4nGNggANGJmYWBpyAlY2dg5OTi5uHF6s0H78AJxRwCAphyguLgKRExcQlQLSkFLq8tAwnp6ycPNABjAqKQKNElVDllVU4OVVhVquJA81Q10BRoAkUUYbJa4Edoo0sr6PLqaePLG/AyWlohKTAmJPTBFnelAFoixmSAnNOTgsUeQZLTk4rJAXWnJw2EHlbiDyDPCenHZICe04HFrh+RydnBgYWPU5uJAWinJwucPNd3dw9GDw5Ob2QFHBzcnrD7ffx9fMPCOTkDEINhmC4+3x8Q0LDwlEDIoKTMzIKKg9SEBIdE8sZh6SAJZ6Tkx0qD1YQkpCYlIwclCng0AXLQxSEpKalZyCryATKZwkhKQjJzsnNQ1KQXwBUUVhUXBJYWgZREFJeUVmFpMKlWg+anmqgCkJq6+obkG1pLEBTENLU3NKKrIKhrb2js8u4G6Kgpze0r3/CRAZMAHbkpJDJU6ZMmTqtFbuC6TNmhsyaMnsOFlmwgrnzpsxfELJwEXZ5Bp/FS3yWLlsesmLlKuwKVk9Ys5Zh3foN0zduwq5g85atDAzbpqSGbN9RhV0FGOzctWH3lD14FOzdt3H/gQw8Cg4u2gQPAwBYDXXdIH+wqAAAAABJRU5ErkJggg=='; +const _defaultCursorPng = + 'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAARzQklUCAgICHwIZIgAAAFmSURBVFiF7dWxSlxREMbx34QFDRowYBchZSxSCWlMCOwD5FGEFHap06UI7KPsAyyEEIQFqxRaCqYTsqCJFsKkuAeRXb17wrqV918dztw55zszc2fo6Oh47MR/e3zO1/iAHWmznHKGQwx9ip/LEbCfazbsoY8j/JLOhcC6sCW9wsjEwJf483AC9nPNc1+lFRwI13d+l3rYFS799rFGxJMqARv2pBXh+72XQ7gWvklPS7TmMl9Ak/M+DqrENvxAv/guKKApuKPWl0/TROK4+LbSqzhuB+OZ3fRSeFPWY+Fkyn56Y29hfgTSpnQ+s98cvorVey66uPlNFxKwZOYLCGfCs5n9NMYVrsp6mvXSoFqpqYFDvMBkStgJJe93dZOwVXxbqUnBENulydSReqUrDhcX0PT2EXarBYS3GNXMhboinBgIl9K71kg0L3+PvyYGdVpruT2MwrF0iotiXfIwus0Dj+OOjo6Of+e7ab74RkpgAAAAAElFTkSuQmCC'; + +final preForbiddenCursor = PredefinedCursor( + png: _forbiddenCursorPng, + id: -2, +); +final preDefaultCursor = PredefinedCursor( + png: _defaultCursorPng, + id: -1, + hotxGetter: (double w) => w / 2, + hotyGetter: (double h) => h / 2, +); + +class PredefinedCursor { + ui.Image? _image; + img2.Image? _image2; + CursorData? _cache; + String png; + int id; + double Function(double)? hotxGetter; + double Function(double)? hotyGetter; + + PredefinedCursor( + {required this.png, required this.id, this.hotxGetter, this.hotyGetter}) { + init(); + } + + ui.Image? get image => _image; + CursorData? get cache => _cache; + + init() { + _image2 = img2.decodePng(base64Decode(png)); + if (_image2 != null) { + () async { + final defaultImg = _image2!; + // This function is called only one time, no need to care about the performance. + Uint8List data = defaultImg.getBytes(format: img2.Format.rgba); + _image = await img.decodeImageFromPixels( + data, defaultImg.width, defaultImg.height, ui.PixelFormat.rgba8888); + + double scale = 1.0; + if (Platform.isWindows) { + data = _image2!.getBytes(format: img2.Format.bgra); + } else { + data = Uint8List.fromList(img2.encodePng(_image2!)); + } + + _cache = CursorData( + peerId: '', + id: id, + image: _image2?.clone(), + scale: scale, + data: data, + hotxOrigin: + hotxGetter != null ? hotxGetter!(_image2!.width.toDouble()) : 0, + hotyOrigin: + hotyGetter != null ? hotyGetter!(_image2!.height.toDouble()) : 0, + width: _image2!.width, + height: _image2!.height, + ); + }(); + } + } +} + class CursorModel with ChangeNotifier { ui.Image? _image; - ui.Image? _defaultImage; final _images = >{}; CursorData? _cache; - final _defaultCacheId = -1; - CursorData? _defaultCache; final _cacheMap = {}; final _cacheKeys = {}; double _x = -10000; @@ -785,9 +850,7 @@ class CursorModel with ChangeNotifier { WeakReference parent; ui.Image? get image => _image; - ui.Image? get defaultImage => _defaultImage; CursorData? get cache => _cache; - CursorData? get defaultCache => _getDefaultCache(); double get x => _x - _displayOriginX; double get y => _y - _displayOriginY; @@ -801,50 +864,11 @@ class CursorModel with ChangeNotifier { DateTime.now().difference(_lastPeerMouse).inMilliseconds < kMouseControlTimeoutMSec; - CursorModel(this.parent) { - _getDefaultImage(); - _getDefaultCache(); - } + CursorModel(this.parent); Set get cachedKeys => _cacheKeys; addKey(String key) => _cacheKeys.add(key); - Future _getDefaultImage() async { - if (_defaultImage == null) { - final defaultImg = defaultCursorImage!; - // This function is called only one time, no need to care about the performance. - Uint8List data = defaultImg.getBytes(format: img2.Format.rgba); - _defaultImage = await img.decodeImageFromPixels( - data, defaultImg.width, defaultImg.height, ui.PixelFormat.rgba8888); - } - return _defaultImage; - } - - CursorData? _getDefaultCache() { - if (_defaultCache == null) { - Uint8List data; - double scale = 1.0; - if (Platform.isWindows) { - data = defaultCursorImage!.getBytes(format: img2.Format.bgra); - } else { - data = Uint8List.fromList(img2.encodePng(defaultCursorImage!)); - } - - _defaultCache = CursorData( - peerId: id, - id: _defaultCacheId, - image: defaultCursorImage?.clone(), - scale: scale, - data: data, - hotxOrigin: defaultCursorImage!.width / 2, - hotyOrigin: defaultCursorImage!.height / 2, - width: defaultCursorImage!.width, - height: defaultCursorImage!.height, - ); - } - return _defaultCache; - } - // remote physical display coordinate Rect getVisibleRect() { final size = MediaQueryData.fromWindow(ui.window).size; @@ -1085,15 +1109,6 @@ class CursorModel with ChangeNotifier { customCursorController.freeCache(k); } } - - Uint8List? cachedForbidmemoryCursorData; - void updateForbiddenCursorBuffer() { - cachedForbidmemoryCursorData ??= base64Decode( - 'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAAXNSR0IB2cksfwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAkZQTFRFAAAA2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4G2B4GWAwCAAAAAAAA2B4GAAAAMTExAAAAAAAA2B4G2B4G2B4GAAAAmZmZkZGRAQEBAAAA2B4G2B4G2B4G////oKCgAwMDag8D2B4G2B4G2B4Gra2tBgYGbg8D2B4G2B4Gubm5CQkJTwsCVgwC2B4GxcXFDg4OAAAAAAAA2B4G2B4Gz8/PFBQUAAAAAAAA2B4G2B4G2B4G2B4G2B4G2B4G2B4GDgIA2NjYGxsbAAAAAAAA2B4GFwMB4eHhIyMjAAAAAAAA2B4G6OjoLCwsAAAAAAAA2B4G2B4G2B4G2B4G2B4GCQEA4ODgv7+/iYmJY2NjAgICAAAA9PT0Ojo6AAAAAAAAAAAA+/v7SkpKhYWFr6+vAAAAAAAA8/PzOTk5ERER9fX1KCgoAAAAgYGBKioqAAAAAAAApqamlpaWAAAAAAAAAAAAAAAAAAAAAAAALi4u/v7+GRkZAAAAAAAAAAAAAAAAAAAAfn5+AAAAAAAAV1dXkJCQAAAAAAAAAQEBAAAAAAAAAAAA7Hz6BAAAAMJ0Uk5TAAIWEwEynNz6//fVkCAatP2fDUHs6cDD8d0mPfT5fiEskiIR584A0gejr3AZ+P4plfALf5ZiTL85a4ziD6697fzN3UYE4v/4TwrNHuT///tdRKZh///+1U/ZBv///yjb///eAVL//50Cocv//6oFBbPvpGZCbfT//7cIhv///8INM///zBEcWYSZmO7//////1P////ts/////8vBv//////gv//R/z///QQz9sevP///2waXhNO/+fc//8mev/5gAe2r90MAAAByUlEQVR4nGNggANGJmYWBpyAlY2dg5OTi5uHF6s0H78AJxRwCAphyguLgKRExcQlQLSkFLq8tAwnp6ycPNABjAqKQKNElVDllVU4OVVhVquJA81Q10BRoAkUUYbJa4Edoo0sr6PLqaePLG/AyWlohKTAmJPTBFnelAFoixmSAnNOTgsUeQZLTk4rJAXWnJw2EHlbiDyDPCenHZICe04HFrh+RydnBgYWPU5uJAWinJwucPNd3dw9GDw5Ob2QFHBzcnrD7ffx9fMPCOTkDEINhmC4+3x8Q0LDwlEDIoKTMzIKKg9SEBIdE8sZh6SAJZ6Tkx0qD1YQkpCYlIwclCng0AXLQxSEpKalZyCryATKZwkhKQjJzsnNQ1KQXwBUUVhUXBJYWgZREFJeUVmFpMKlWg+anmqgCkJq6+obkG1pLEBTENLU3NKKrIKhrb2js8u4G6Kgpze0r3/CRAZMAHbkpJDJU6ZMmTqtFbuC6TNmhsyaMnsOFlmwgrnzpsxfELJwEXZ5Bp/FS3yWLlsesmLlKuwKVk9Ys5Zh3foN0zduwq5g85atDAzbpqSGbN9RhV0FGOzctWH3lD14FOzdt3H/gQw8Cg4u2gQPAwBYDXXdIH+wqAAAAABJRU5ErkJggg=='); - } - - img2.Image? defaultCursorImage = img2.decodePng(base64Decode( - 'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAARzQklUCAgICHwIZIgAAAFmSURBVFiF7dWxSlxREMbx34QFDRowYBchZSxSCWlMCOwD5FGEFHap06UI7KPsAyyEEIQFqxRaCqYTsqCJFsKkuAeRXb17wrqV918dztw55zszc2fo6Oh47MR/e3zO1/iAHWmznHKGQwx9ip/LEbCfazbsoY8j/JLOhcC6sCW9wsjEwJf483AC9nPNc1+lFRwI13d+l3rYFS799rFGxJMqARv2pBXh+72XQ7gWvklPS7TmMl9Ak/M+DqrENvxAv/guKKApuKPWl0/TROK4+LbSqzhuB+OZ3fRSeFPWY+Fkyn56Y29hfgTSpnQ+s98cvorVey66uPlNFxKwZOYLCGfCs5n9NMYVrsp6mvXSoFqpqYFDvMBkStgJJe93dZOwVXxbqUnBENulydSReqUrDhcX0PT2EXarBYS3GNXMhboinBgIl9K71kg0L3+PvyYGdVpruT2MwrF0iotiXfIwus0Dj+OOjo6Of+e7ab74RkpgAAAAAElFTkSuQmCC')); } class QualityMonitorData { From db18f4ab26c2cdf5a403a7277b8c9534414d5bda Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 22 Nov 2022 22:12:10 +0800 Subject: [PATCH 1002/2015] fix session option zoom cursor Signed-off-by: fufesou --- flutter/lib/desktop/pages/remote_page.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 9e775f86f..c0a05c6d5 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -70,8 +70,7 @@ class _RemotePageState extends State ShowRemoteCursorState.init(id); RemoteCursorMovedState.init(id); final optZoomCursor = 'zoom-cursor'; - PeerBoolOption.init(id, optZoomCursor, - () => bind.sessionGetToggleOptionSync(id: id, arg: optZoomCursor)); + PeerBoolOption.init(id, optZoomCursor, () => false); _zoomCursor = PeerBoolOption.find(id, optZoomCursor); _showRemoteCursor = ShowRemoteCursorState.find(id); _keyboardEnabled = KeyboardEnabledState.find(id); @@ -91,9 +90,7 @@ class _RemotePageState extends State void initState() { super.initState(); _initStates(widget.id); - _ffi = FFI(); - Get.put(_ffi, tag: widget.id); _ffi.start(widget.id); WidgetsBinding.instance.addPostFrameCallback((_) { @@ -107,8 +104,11 @@ class _RemotePageState extends State _rawKeyFocusNode.requestFocus(); _ffi.ffiModel.updateEventListener(widget.id); _ffi.qualityMonitorModel.checkShowQualityMonitor(widget.id); + // Session option should be set after models.dart/FFI.start _showRemoteCursor.value = bind.sessionGetToggleOptionSync( id: widget.id, arg: 'show-remote-cursor'); + _zoomCursor.value = + bind.sessionGetToggleOptionSync(id: widget.id, arg: 'zoom-cursor'); if (!_isCustomCursorInited) { customCursorController.registerNeedUpdateCursorCallback( (String? lastKey, String? currentKey) async { From cea402ffcc379e294cf24454dcb1cca403cb22ad Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sun, 20 Nov 2022 16:40:59 +0800 Subject: [PATCH 1003/2015] feat: initial macos ci --- .github/workflows/flutter-nightly.yml | 80 +++++++++++++++++++++++++++ build.py | 10 +++- 2 files changed, 89 insertions(+), 1 deletion(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index f2391e77e..246c717f4 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -100,6 +100,86 @@ jobs: files: | rustdesk-*.exe + builf-for-macOS: + name: ${{ matrix.job.target }} (${{ matrix.job.os }},${{ matrix.job.extra-build-args }}) + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: false + matrix: + job: + - { target: x86_64-apple-darwin , os: macos-10.15, extra-build-args: ""} + steps: + - name: Checkout source code + uses: actions/checkout@v3 + + - name: Get build target triple + uses: jungwinter/split@v2 + id: build-target-triple + with: + separator: '-' + msg: ${{ matrix.job.target }} + + - name: Install build runtime + run: | + brew install llvm create-dmg + + - name: Install flutter + uses: subosito/flutter-action@v2 + with: + channel: 'stable' + flutter-version: ${{ env.FLUTTER_VERSION }} + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: ${{ matrix.job.target }} + override: true + profile: minimal # minimal component installation (ie, no documentation) + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: ${{ matrix.job.os }} + + - name: Install flutter rust bridge deps + shell: bash + run: | + dart pub global activate ffigen --version 5.0.1 + # flutter_rust_bridge + pushd /tmp && git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1 && popd + pushd /tmp/flutter_rust_bridge/frb_codegen && cargo install --path . && popd + pushd flutter && flutter pub get && popd + ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart + + - name: Restore from cache and install vcpkg + uses: lukka/run-vcpkg@v7 + with: + setupOnly: true + vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} + + - name: Install vcpkg dependencies + run: | + $VCPKG_ROOT/vcpkg install libvpx libyuv opus + shell: bash + + - name: Install cargo bundle tools + run: | + cargo install cargo-bundle + + - name: Show version information (Rust, cargo, Clang) + shell: bash + run: | + clang --version || true + rustup -V + rustup toolchain list + rustup default + cargo -V + rustc -V + + - name: Build rustdesk + run: ./build.py --flutter --hwcodec ${{ matrix.job.extra-build-args }} + + build-for-linux: name: ${{ matrix.job.target }} (${{ matrix.job.os }},${{ matrix.job.extra-build-args }}) runs-on: ${{ matrix.job.os }} diff --git a/build.py b/build.py index a95c64dc1..595c719d5 100755 --- a/build.py +++ b/build.py @@ -263,6 +263,14 @@ def build_flutter_deb(version, features): os.chdir("..") +def build_flutter_dmg(version, features): + os.system(f'cargo build --features {features} --lib --release') + ffi_bindgen_function_refactor() + os.chdir('flutter') + os.system('flutter build macos --release') + # TODO: pass + + def build_flutter_arch_manjaro(version, features): os.system(f'cargo build --features {features} --lib --release') ffi_bindgen_function_refactor() @@ -372,7 +380,7 @@ def main(): os.system('cargo bundle --release --features ' + features) if flutter: if osx: - # todo: OSX build + build_flutter_dmg(version, features) pass else: os.system( From 592a609fb6234b87aac5ca3012da8b5d707f9cd2 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sun, 20 Nov 2022 16:42:46 +0800 Subject: [PATCH 1004/2015] fix: ci --- .github/workflows/flutter-nightly.yml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 246c717f4..b11bc4fcd 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -112,13 +112,6 @@ jobs: - name: Checkout source code uses: actions/checkout@v3 - - name: Get build target triple - uses: jungwinter/split@v2 - id: build-target-triple - with: - separator: '-' - msg: ${{ matrix.job.target }} - - name: Install build runtime run: | brew install llvm create-dmg From 8b15174ca68055c69ce933e7d4e7a5ce334f3621 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sun, 20 Nov 2022 16:54:54 +0800 Subject: [PATCH 1005/2015] update: deps wip: add arm,arm64 triplet opt: macos nightly ci --- .github/workflows/flutter-nightly.yml | 46 +- build.py | 55 +- flutter/lib/models/native_model.dart | 4 +- flutter/macos/Podfile.lock | 20 +- .../macos/Runner.xcodeproj/project.pbxproj | 138 ++--- .../xcshareddata/xcschemes/Runner.xcscheme | 8 +- flutter/macos/Runner/Info.plist | 20 +- .../macos/rustdesk.xcodeproj/project.pbxproj | 309 ----------- flutter/pubspec.lock | 486 ++++++++---------- 9 files changed, 358 insertions(+), 728 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index b11bc4fcd..0b49b9893 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -114,7 +114,7 @@ jobs: - name: Install build runtime run: | - brew install llvm create-dmg + brew install llvm create-dmg nasm yasm cmake gcc wget ninja - name: Install flutter uses: subosito/flutter-action@v2 @@ -153,7 +153,6 @@ jobs: - name: Install vcpkg dependencies run: | $VCPKG_ROOT/vcpkg install libvpx libyuv opus - shell: bash - name: Install cargo bundle tools run: | @@ -170,8 +169,23 @@ jobs: rustc -V - name: Build rustdesk - run: ./build.py --flutter --hwcodec ${{ matrix.job.extra-build-args }} - + run: | + # --hwcodec not supported on macos yet + ./build.py --flutter ${{ matrix.job.extra-build-args }} + + - name: Rename rustdesk + run: | + for name in rustdesk*??.dmg; do + mv "$name" "${name%%.dmg}-untested-${{ matrix.job.target }}.dmg" + done + + - name: Publish DMG package + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + rustdesk*-${{ matrix.job.target }}.dmg build-for-linux: name: ${{ matrix.job.target }} (${{ matrix.job.os }},${{ matrix.job.extra-build-args }}) @@ -180,12 +194,11 @@ jobs: fail-fast: false matrix: job: - # - { target: aarch64-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } + # - { target: aarch64-unknown-linux-gnu , os: ubuntu-18.04, use-cross: true } # - { target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true } # - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true } # - { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } # - { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } - # - { target: x86_64-apple-darwin , os: macos-10.15 } - { target: x86_64-unknown-linux-gnu , os: ubuntu-18.04, extra-build-args: ""} - { target: x86_64-unknown-linux-gnu , os: ubuntu-18.04, extra-build-args: "--flatpak"} # - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } @@ -203,10 +216,12 @@ jobs: - name: Install prerequisites run: | case ${{ matrix.job.target }} in - x86_64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libvdpau-dev libva-dev libclang-dev llvm-dev libclang-10-dev llvm-10-dev;; - # arm-unknown-linux-*) sudo apt-get -y update ; sudo apt-get -y install gcc-arm-linux-gnueabihf ;; - # aarch64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt-get -y install gcc-aarch64-linux-gnu ;; + x86_64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt install -y g++ gcc;; + arm-unknown-linux-*) sudo apt-get -y update ; sudo apt-get -y install gcc-arm-linux-gnueabihf ;; + aarch64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt-get -y install gcc-aarch64-linux-gnu ;; esac + # common package + sudo apt install -y git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libvdpau-dev libva-dev libclang-dev llvm-dev libclang-10-dev llvm-10-dev - name: Install flutter uses: subosito/flutter-action@v2 @@ -244,7 +259,11 @@ jobs: - name: Install vcpkg dependencies run: | - $VCPKG_ROOT/vcpkg install libvpx libyuv opus + case ${{ matrix.job.target }} in + x86_64-unknown-linux-gnu) $VCPKG_ROOT/vcpkg install libvpx libyuv opus;; + arm-unknown-linux-*) $VCPKG_ROOT/vcpkg install libvpx:arm-linux libyuv:arm-linux opus:arm-linux;; + aarch64-unknown-linux-gnu) $VCPKG_ROOT/vcpkg install libvpx:arm64-linux libyuv:arm64-linux opus:arm64-linux;; + esac shell: bash - name: Install cargo bundle tools @@ -286,6 +305,11 @@ jobs: name: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb path: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb + - name: Patch archlinux PKGBUILD + if: ${{ matrix.job.extra-build-args == '' }} + run: | + sed -i "s/arch=('x86_64')/arch=('${{ steps.build-target-triple.outputs._0 }}')/g" res/PKGBUILD + - name: Build archlinux package if: ${{ matrix.job.extra-build-args == '' }} uses: vufa/arch-makepkg-action@master @@ -358,7 +382,7 @@ jobs: fail-fast: false matrix: job: - # - { target: aarch64-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } + - { target: aarch64-unknown-linux-gnu , os: ubuntu-18.04, use-cross: true } # - { target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true } # - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true } # - { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } diff --git a/build.py b/build.py index 595c719d5..445eb15bf 100755 --- a/build.py +++ b/build.py @@ -10,7 +10,8 @@ import hashlib import argparse windows = platform.platform().startswith('Windows') -osx = platform.platform().startswith('Darwin') or platform.platform().startswith("macOS") +osx = platform.platform().startswith( + 'Darwin') or platform.platform().startswith("macOS") hbb_name = 'rustdesk' + ('.exe' if windows else '') exe_path = 'target/release/' + hbb_name flutter_win_target_dir = 'flutter/build/windows/runner/Release/' @@ -124,6 +125,7 @@ def generate_build_script_for_docker(): os.system("chmod +x /tmp/build.sh") os.system("bash /tmp/build.sh") + def download_extract_features(features, res_dir): proxy = '' @@ -139,7 +141,8 @@ def download_extract_features(features, res_dir): for (feat, feat_info) in features.items(): print(f'{feat} download begin') download_filename = feat_info['zip_url'].split('/')[-1] - checksum_md5_response = urllib.request.urlopen(req(feat_info['checksum_url'])) + checksum_md5_response = urllib.request.urlopen( + req(feat_info['checksum_url'])) for line in checksum_md5_response.read().decode('utf-8').splitlines(): if line.split()[1] == download_filename: checksum_md5 = line.split()[0] @@ -186,7 +189,7 @@ def get_rc_features(args): def get_features(args): - features = ['inline'] + features = ['inline'] if not args.flutter else [] if windows: features.extend(get_rc_features(args)) if args.hwcodec: @@ -227,7 +230,6 @@ def build_flutter_deb(version, features): os.system(f'cargo build --features {features} --lib --release') ffi_bindgen_function_refactor() os.chdir('flutter') - os.system('dpkg-deb -R rustdesk.deb tmpdeb') os.system('flutter build linux --release') os.system('mkdir -p tmpdeb/usr/bin/') os.system('mkdir -p tmpdeb/usr/lib/rustdesk') @@ -265,10 +267,18 @@ def build_flutter_deb(version, features): def build_flutter_dmg(version, features): os.system(f'cargo build --features {features} --lib --release') - ffi_bindgen_function_refactor() + # copy dylib + os.system( + "cp target/release/liblibrustdesk.dylib target/release/librustdesk.dylib") + # ffi_bindgen_function_refactor() + # limitations from flutter rust bridge + 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') - # TODO: pass + os.system( + "create-dmg rustdesk.dmg ./build/macos/Build/Products/Release/rustdesk.app") + os.rename("rustdesk.dmg", f"../rustdesk-{version}.dmg") + os.chdir("..") def build_flutter_arch_manjaro(version, features): @@ -289,19 +299,24 @@ def build_flutter_windows(version, features): os.chdir('flutter') os.system('flutter build windows --release') os.chdir('..') - shutil.copy2('target/release/deps/dylib_virtual_display.dll', flutter_win_target_dir) + shutil.copy2('target/release/deps/dylib_virtual_display.dll', + flutter_win_target_dir) os.chdir('libs/portable') os.system('pip3 install -r requirements.txt') os.system( f'python3 ./generate.py -f ../../{flutter_win_target_dir} -o . -e ../../{flutter_win_target_dir}/rustdesk.exe') os.chdir('../..') if os.path.exists('./rustdesk_portable.exe'): - os.replace('./target/release/rustdesk-portable-packer.exe', './rustdesk_portable.exe') + os.replace('./target/release/rustdesk-portable-packer.exe', + './rustdesk_portable.exe') else: - os.rename('./target/release/rustdesk-portable-packer.exe', './rustdesk_portable.exe') - print(f'output location: {os.path.abspath(os.curdir)}/rustdesk_portable.exe') + os.rename('./target/release/rustdesk-portable-packer.exe', + './rustdesk_portable.exe') + print( + f'output location: {os.path.abspath(os.curdir)}/rustdesk_portable.exe') os.rename('./rustdesk_portable.exe', f'./rustdesk-{version}-install.exe') - print(f'output location: {os.path.abspath(os.curdir)}/rustdesk-{version}-install.exe') + print( + f'output location: {os.path.abspath(os.curdir)}/rustdesk-{version}-install.exe') def main(): @@ -322,7 +337,8 @@ def main(): version = get_version() features = ','.join(get_features(args)) flutter = args.flutter - os.system('python3 res/inline-sciter.py') + if not flutter: + os.system('python3 res/inline-sciter.py') portable = args.portable if windows: # build virtual display dynamic library @@ -343,7 +359,8 @@ def main(): 'target\\release\\rustdesk.exe') else: print('Not signed') - os.system(f'cp -rf target/release/RustDesk.exe rustdesk-{version}-win7-install.exe') + os.system( + f'cp -rf target/release/RustDesk.exe rustdesk-{version}-win7-install.exe') elif os.path.isfile('/usr/bin/pacman'): # pacman -S -needed base-devel os.system("sed -i 's/pkgver=.*/pkgver=%s/g' res/PKGBUILD" % version) @@ -356,12 +373,13 @@ def main(): os.system('ln -s res/pacman_install && ln -s res/PKGBUILD') os.system('HBB=`pwd` makepkg -f') os.system('mv rustdesk-%s-0-x86_64.pkg.tar.zst rustdesk-%s-manjaro-arch.pkg.tar.zst' % ( - version, version)) + version, version)) # pacman -U ./rustdesk.pkg.tar.zst elif os.path.isfile('/usr/bin/yum'): os.system('cargo build --release --features ' + features) os.system('strip target/release/rustdesk') - os.system("sed -i 's/Version: .*/Version: %s/g' res/rpm.spec" % version) + os.system( + "sed -i 's/Version: .*/Version: %s/g' res/rpm.spec" % version) os.system('HBB=`pwd` rpmbuild -ba res/rpm.spec') os.system( 'mv $HOME/rpmbuild/RPMS/x86_64/rustdesk-%s-0.x86_64.rpm ./rustdesk-%s-fedora28-centos8.rpm' % ( @@ -370,14 +388,14 @@ def main(): elif os.path.isfile('/usr/bin/zypper'): os.system('cargo build --release --features ' + features) os.system('strip target/release/rustdesk') - os.system("sed -i 's/Version: .*/Version: %s/g' res/rpm-suse.spec" % version) + os.system( + "sed -i 's/Version: .*/Version: %s/g' res/rpm-suse.spec" % version) os.system('HBB=`pwd` rpmbuild -ba res/rpm-suse.spec') os.system( 'mv $HOME/rpmbuild/RPMS/x86_64/rustdesk-%s-0.x86_64.rpm ./rustdesk-%s-suse.rpm' % ( - version, version)) + version, version)) # yum localinstall rustdesk.rpm else: - os.system('cargo bundle --release --features ' + features) if flutter: if osx: build_flutter_dmg(version, features) @@ -387,6 +405,7 @@ def main(): 'mv target/release/bundle/deb/rustdesk*.deb ./flutter/rustdesk.deb') build_flutter_deb(version, features) else: + os.system('cargo bundle --release --features ' + features) if osx: os.system( 'strip target/release/bundle/osx/RustDesk.app/Contents/MacOS/rustdesk') diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index 3af6f4fd7..e8aad8638 100644 --- a/flutter/lib/models/native_model.dart +++ b/flutter/lib/models/native_model.dart @@ -96,9 +96,7 @@ class PlatformFFI { ? DynamicLibrary.open('librustdesk.so') : Platform.isWindows ? DynamicLibrary.open('librustdesk.dll') - : Platform.isMacOS - ? DynamicLibrary.open('librustdesk.dylib') - : DynamicLibrary.process(); + : DynamicLibrary.process(); debugPrint('initializing FFI $_appType'); try { _translate = dylib.lookupFunction('translate'); diff --git a/flutter/macos/Podfile.lock b/flutter/macos/Podfile.lock index 812fbf8b3..952996e5f 100644 --- a/flutter/macos/Podfile.lock +++ b/flutter/macos/Podfile.lock @@ -17,19 +17,21 @@ PODS: - FlutterMacOS - screen_retriever (0.0.1): - FlutterMacOS - - shared_preferences_macos (0.0.1): - - FlutterMacOS - sqflite (0.0.2): - FlutterMacOS - FMDB (>= 2.7.5) - tray_manager (0.0.1): - FlutterMacOS + - uni_links_desktop (0.0.1): + - FlutterMacOS - url_launcher_macos (0.0.1): - FlutterMacOS - wakelock_macos (0.0.1): - FlutterMacOS - window_manager (0.2.0): - FlutterMacOS + - window_size (0.0.2): + - FlutterMacOS DEPENDENCIES: - desktop_drop (from `Flutter/ephemeral/.symlinks/plugins/desktop_drop/macos`) @@ -40,12 +42,13 @@ DEPENDENCIES: - package_info_plus_macos (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus_macos/macos`) - path_provider_macos (from `Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos`) - screen_retriever (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos`) - - shared_preferences_macos (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_macos/macos`) - sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/macos`) - tray_manager (from `Flutter/ephemeral/.symlinks/plugins/tray_manager/macos`) + - uni_links_desktop (from `Flutter/ephemeral/.symlinks/plugins/uni_links_desktop/macos`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) - wakelock_macos (from `Flutter/ephemeral/.symlinks/plugins/wakelock_macos/macos`) - window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`) + - window_size (from `Flutter/ephemeral/.symlinks/plugins/window_size/macos`) SPEC REPOS: trunk: @@ -68,35 +71,38 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos screen_retriever: :path: Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos - shared_preferences_macos: - :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_macos/macos sqflite: :path: Flutter/ephemeral/.symlinks/plugins/sqflite/macos tray_manager: :path: Flutter/ephemeral/.symlinks/plugins/tray_manager/macos + uni_links_desktop: + :path: Flutter/ephemeral/.symlinks/plugins/uni_links_desktop/macos url_launcher_macos: :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos wakelock_macos: :path: Flutter/ephemeral/.symlinks/plugins/wakelock_macos/macos window_manager: :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos + window_size: + :path: Flutter/ephemeral/.symlinks/plugins/window_size/macos SPEC CHECKSUMS: desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898 desktop_multi_window: 566489c048b501134f9d7fb6a2354c60a9126486 device_info_plus_macos: 1ad388a1ef433505c4038e7dd9605aadd1e2e9c7 flutter_custom_cursor: 629957115075c672287bd0fa979d863ccf6024f7 - FlutterMacOS: ae6af50a8ea7d6103d888583d46bd8328a7e9811 + FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424 FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a package_info_plus_macos: f010621b07802a241d96d01876d6705f15e77c1c path_provider_macos: 3c0c3b4b0d4a76d2bf989a913c2de869c5641a19 screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38 - shared_preferences_macos: a64dc611287ed6cbe28fd1297898db1336975727 sqflite: a5789cceda41d54d23f31d6de539d65bb14100ea tray_manager: 9064e219c56d75c476e46b9a21182087930baf90 + uni_links_desktop: 45900fb319df48fcdea2df0756e9c2626696b026 url_launcher_macos: 597e05b8e514239626bcf4a850fcf9ef5c856ec3 wakelock_macos: bc3f2a9bd8d2e6c89fee1e1822e7ddac3bd004a9 window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 + window_size: 339dafa0b27a95a62a843042038fa6c3c48de195 PODFILE CHECKSUM: c7161fcf45d4fd9025dc0f48a76d6e64e52f8176 diff --git a/flutter/macos/Runner.xcodeproj/project.pbxproj b/flutter/macos/Runner.xcodeproj/project.pbxproj index 23549954b..8a44007a9 100644 --- a/flutter/macos/Runner.xcodeproj/project.pbxproj +++ b/flutter/macos/Runner.xcodeproj/project.pbxproj @@ -26,9 +26,8 @@ 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 */; }; + 84010B42292B58A400152837 /* liblibrustdesk.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 84010B41292B585400152837 /* liblibrustdesk.a */; }; C5E54335B73C89F72DB1B606 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 26C84465887F29AE938039CB /* Pods_Runner.framework */; }; - CC13D44B2847D53E00EF8B54 /* librustdesk.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = CC13D4362847C8C200EF8B54 /* librustdesk.dylib */; }; - CC13D4502847D5E800EF8B54 /* librustdesk.dylib in Bundle Framework */ = {isa = PBXBuildFile; fileRef = CC13D4362847C8C200EF8B54 /* librustdesk.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -39,53 +38,17 @@ remoteGlobalIDString = 33CC111A2044C6BA0003C045; remoteInfo = FLX; }; - CC13D4352847C8C200EF8B54 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = CC13D42E2847C8C200EF8B54 /* rustdesk.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = CA6071B5A0F5A7A3EF2297AA; - remoteInfo = "librustdesk-cdylib"; - }; - CC13D4372847C8C200EF8B54 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = CC13D42E2847C8C200EF8B54 /* rustdesk.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = CA604C7415FB2A3731F5016A; - remoteInfo = "librustdesk-staticlib"; - }; - CC13D4392847C8C200EF8B54 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = CC13D42E2847C8C200EF8B54 /* rustdesk.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = CA60D3BC5386D3D7DBD96893; - remoteInfo = "naming-bin"; - }; - CC13D43B2847C8C200EF8B54 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = CC13D42E2847C8C200EF8B54 /* rustdesk.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = CA60D3BC5386B357B2AB834F; - remoteInfo = "rustdesk-bin"; - }; - CC13D43D2847C8CB00EF8B54 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = CC13D42E2847C8C200EF8B54 /* rustdesk.xcodeproj */; - proxyType = 1; - remoteGlobalIDString = CA6071B5A0F5D6691E4C3FF1; - remoteInfo = "librustdesk-cdylib"; - }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ - 33CC110E2044A8840003C045 /* Bundle Framework */ = { + 840109CF292B240500152837 /* Embed Libraries */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( - CC13D4502847D5E800EF8B54 /* librustdesk.dylib in Bundle Framework */, ); - name = "Bundle Framework"; + name = "Embed Libraries"; runOnlyForDeploymentPostprocessing = 0; }; /* End PBXCopyFilesBuildPhase section */ @@ -95,7 +58,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 /* flutter_hbb.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = flutter_hbb.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 = ""; }; @@ -109,9 +72,9 @@ 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 = ""; }; + 84010B41292B585400152837 /* liblibrustdesk.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = liblibrustdesk.a; path = ../../target/release/liblibrustdesk.a; 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 = ""; }; - CC13D42E2847C8C200EF8B54 /* rustdesk.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; path = rustdesk.xcodeproj; sourceTree = SOURCE_ROOT; }; CCB6FE9A2848A6B800E58D48 /* bridge_generated.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = bridge_generated.h; path = Runner/bridge_generated.h; sourceTree = ""; }; /* End PBXFileReference section */ @@ -120,8 +83,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - CC13D44B2847D53E00EF8B54 /* librustdesk.dylib in Frameworks */, C5E54335B73C89F72DB1B606 /* Pods_Runner.framework in Frameworks */, + 84010B42292B58A400152837 /* liblibrustdesk.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -154,7 +117,7 @@ 33CC10EE2044A3C60003C045 /* Products */ = { isa = PBXGroup; children = ( - 33CC10ED2044A3C60003C045 /* flutter_hbb.app */, + 33CC10ED2044A3C60003C045 /* rustdesk.app */, ); name = Products; sourceTree = ""; @@ -184,7 +147,6 @@ 33FAB671232836740065AC1E /* Runner */ = { isa = PBXGroup; children = ( - CC13D42E2847C8C200EF8B54 /* rustdesk.xcodeproj */, 33CC10F02044A3C60003C045 /* AppDelegate.swift */, 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, 33E51913231747F40026EE4D /* DebugProfile.entitlements */, @@ -205,20 +167,10 @@ path = Pods; sourceTree = ""; }; - CC13D42F2847C8C200EF8B54 /* Products */ = { - isa = PBXGroup; - children = ( - CC13D4362847C8C200EF8B54 /* librustdesk.dylib */, - CC13D4382847C8C200EF8B54 /* liblibrustdesk_static.a */, - CC13D43A2847C8C200EF8B54 /* naming */, - CC13D43C2847C8C200EF8B54 /* rustdesk */, - ); - name = Products; - sourceTree = ""; - }; D73912EC22F37F3D000D13A0 /* Frameworks */ = { isa = PBXGroup; children = ( + 84010B41292B585400152837 /* liblibrustdesk.a */, 26C84465887F29AE938039CB /* Pods_Runner.framework */, ); name = Frameworks; @@ -235,19 +187,18 @@ 33CC10E92044A3C60003C045 /* Sources */, 33CC10EA2044A3C60003C045 /* Frameworks */, 33CC10EB2044A3C60003C045 /* Resources */, - 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, + 840109CF292B240500152837 /* Embed Libraries */, 4688A20DD8E4F3E900927B2C /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); dependencies = ( - CC13D43E2847C8CB00EF8B54 /* PBXTargetDependency */, 33CC11202044C79F0003C045 /* PBXTargetDependency */, ); name = Runner; productName = Runner; - productReference = 33CC10ED2044A3C60003C045 /* flutter_hbb.app */; + productReference = 33CC10ED2044A3C60003C045 /* rustdesk.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ @@ -287,12 +238,6 @@ mainGroup = 33CC10E42044A3C60003C045; productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; projectDirPath = ""; - projectReferences = ( - { - ProductGroup = CC13D42F2847C8C200EF8B54 /* Products */; - ProjectRef = CC13D42E2847C8C200EF8B54 /* rustdesk.xcodeproj */; - }, - ); projectRoot = ""; targets = ( 33CC10EC2044A3C60003C045 /* Runner */, @@ -301,37 +246,6 @@ }; /* End PBXProject section */ -/* Begin PBXReferenceProxy section */ - CC13D4362847C8C200EF8B54 /* librustdesk.dylib */ = { - isa = PBXReferenceProxy; - fileType = "compiled.mach-o.dylib"; - path = librustdesk.dylib; - remoteRef = CC13D4352847C8C200EF8B54 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - CC13D4382847C8C200EF8B54 /* liblibrustdesk_static.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = liblibrustdesk_static.a; - remoteRef = CC13D4372847C8C200EF8B54 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - CC13D43A2847C8C200EF8B54 /* naming */ = { - isa = PBXReferenceProxy; - fileType = "compiled.mach-o.executable"; - path = naming; - remoteRef = CC13D4392847C8C200EF8B54 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - CC13D43C2847C8C200EF8B54 /* rustdesk */ = { - isa = PBXReferenceProxy; - fileType = "compiled.mach-o.executable"; - path = rustdesk; - remoteRef = CC13D43B2847C8C200EF8B54 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; -/* End PBXReferenceProxy section */ - /* Begin PBXResourcesBuildPhase section */ 33CC10EB2044A3C60003C045 /* Resources */ = { isa = PBXResourcesBuildPhase; @@ -442,11 +356,6 @@ target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; }; - CC13D43E2847C8CB00EF8B54 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - name = "librustdesk-cdylib"; - targetProxy = CC13D43D2847C8CB00EF8B54 /* PBXContainerItemProxy */; - }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ @@ -467,6 +376,7 @@ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ARCHS = x86_64; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -502,6 +412,7 @@ GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 10.11; MTL_ENABLE_DEBUG_INFO = NO; + ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; @@ -522,6 +433,12 @@ "$(inherited)", "@executable_path/../Frameworks", ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + ../../target/profile, + ); + MACOSX_DEPLOYMENT_TARGET = 10.15; + PRODUCT_BUNDLE_IDENTIFIER = com.carriez.rustdesk; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; @@ -540,6 +457,7 @@ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ARCHS = x86_64; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -579,7 +497,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -593,6 +511,7 @@ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ARCHS = x86_64; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -626,8 +545,9 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = NO; + ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; @@ -648,6 +568,12 @@ "$(inherited)", "@executable_path/../Frameworks", ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + ../../target/debug, + ); + MACOSX_DEPLOYMENT_TARGET = 10.15; + PRODUCT_BUNDLE_IDENTIFIER = com.carriez.rustdesk; PROVISIONING_PROFILE_SPECIFIER = ""; "SWIFT_OBJC_BRIDGING_HEADER[arch=*]" = Runner/bridge_generated.h; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -669,6 +595,12 @@ "$(inherited)", "@executable_path/../Frameworks", ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + ../../target/release, + ); + MACOSX_DEPLOYMENT_TARGET = 10.15; + PRODUCT_BUNDLE_IDENTIFIER = com.carriez.rustdesk; PROVISIONING_PROFILE_SPECIFIER = ""; "SWIFT_OBJC_BRIDGING_HEADER[arch=*]" = Runner/bridge_generated.h; SWIFT_VERSION = 5.0; diff --git a/flutter/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/flutter/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 85831efcf..898fbe4e7 100644 --- a/flutter/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/flutter/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -15,7 +15,7 @@ @@ -31,7 +31,7 @@ @@ -54,7 +54,7 @@ @@ -71,7 +71,7 @@ diff --git a/flutter/macos/Runner/Info.plist b/flutter/macos/Runner/Info.plist index 8245f21a0..7b985c870 100644 --- a/flutter/macos/Runner/Info.plist +++ b/flutter/macos/Runner/Info.plist @@ -18,16 +18,6 @@ APPL CFBundleShortVersionString $(FLUTTER_BUILD_NAME) - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - LSMinimumSystemVersion - $(MACOSX_DEPLOYMENT_TARGET) - NSHumanReadableCopyright - $(PRODUCT_COPYRIGHT) - NSMainNibFile - MainMenu - NSPrincipalClass - NSApplication CFBundleURLTypes @@ -41,5 +31,15 @@ + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication diff --git a/flutter/macos/rustdesk.xcodeproj/project.pbxproj b/flutter/macos/rustdesk.xcodeproj/project.pbxproj index 7aacb5f05..e334f0ac5 100644 --- a/flutter/macos/rustdesk.xcodeproj/project.pbxproj +++ b/flutter/macos/rustdesk.xcodeproj/project.pbxproj @@ -6,37 +6,8 @@ objectVersion = 53; objects = { -/* Begin PBXBuildFile section */ - CA6061C6409F12977AAB839F /* Cargo.toml in Sources */ = {isa = PBXBuildFile; fileRef = CA603C4309E13EF4668187A5 /* Cargo.toml */; settings = {COMPILER_FLAGS = "--lib"; }; }; - CA6061C6409FC858B7409EE3 /* Cargo.toml in Sources */ = {isa = PBXBuildFile; fileRef = CA603C4309E13EF4668187A5 /* Cargo.toml */; settings = {COMPILER_FLAGS = "--bin naming"; }; }; - CA6061C6409FC9FA710A2219 /* Cargo.toml in Sources */ = {isa = PBXBuildFile; fileRef = CA603C4309E13EF4668187A5 /* Cargo.toml */; settings = {COMPILER_FLAGS = "--bin rustdesk"; }; }; - CA6061C6409FD6691E4C3FF1 /* Cargo.toml in Sources */ = {isa = PBXBuildFile; fileRef = CA603C4309E13EF4668187A5 /* Cargo.toml */; settings = {COMPILER_FLAGS = "--lib"; }; }; -/* End PBXBuildFile section */ - -/* Begin PBXBuildRule section */ - CA603C4309E1AC6C1400ACA8 /* PBXBuildRule */ = { - isa = PBXBuildRule; - compilerSpec = com.apple.compilers.proxy.script; - dependencyFile = "$(DERIVED_FILE_DIR)/$(CARGO_XCODE_TARGET_ARCH)-$(EXECUTABLE_NAME).d"; - filePatterns = "*/Cargo.toml"; - fileType = pattern.proxy; - inputFiles = ( - ); - isEditable = 0; - name = "Cargo project build"; - outputFiles = ( - "$(OBJECT_FILE_DIR)/$(CARGO_XCODE_TARGET_ARCH)-$(EXECUTABLE_NAME)", - ); - script = "# generated with cargo-xcode 1.4.1\n\nset -eu; export PATH=$PATH:~/.cargo/bin:/usr/local/bin;\nif [ \"${IS_MACCATALYST-NO}\" = YES ]; then\n CARGO_XCODE_TARGET_TRIPLE=\"${CARGO_XCODE_TARGET_ARCH}-apple-ios-macabi\"\nelse\n CARGO_XCODE_TARGET_TRIPLE=\"${CARGO_XCODE_TARGET_ARCH}-apple-${CARGO_XCODE_TARGET_OS}\"\nfi\nif [ \"$CARGO_XCODE_TARGET_OS\" != \"darwin\" ]; then\n PATH=\"${PATH/\\/Contents\\/Developer\\/Toolchains\\/XcodeDefault.xctoolchain\\/usr\\/bin:/xcode-provided-ld-cant-link-lSystem-for-the-host-build-script:}\"\nfi\nPATH=\"$PATH:/opt/homebrew/bin\" # Rust projects often depend on extra tools like nasm, which Xcode lacks\nif [ \"$CARGO_XCODE_BUILD_MODE\" == release ]; then\n OTHER_INPUT_FILE_FLAGS=\"${OTHER_INPUT_FILE_FLAGS} --release\"\nfi\nif command -v rustup &> /dev/null; then\n if ! rustup target list --installed | egrep -q \"${CARGO_XCODE_TARGET_TRIPLE}\"; then\n echo \"warning: this build requires rustup toolchain for $CARGO_XCODE_TARGET_TRIPLE, but it isn't installed\"\n rustup target add \"${CARGO_XCODE_TARGET_TRIPLE}\" || echo >&2 \"warning: can't install $CARGO_XCODE_TARGET_TRIPLE\"\n fi\nfi\nif [ \"$ACTION\" = clean ]; then\n ( set -x; cargo clean --manifest-path=\"$SCRIPT_INPUT_FILE\" ${OTHER_INPUT_FILE_FLAGS} --target=\"${CARGO_XCODE_TARGET_TRIPLE}\"; );\nelse\n ( set -x; cargo build --manifest-path=\"$SCRIPT_INPUT_FILE\" --features=\"${CARGO_XCODE_FEATURES:-}\" ${OTHER_INPUT_FILE_FLAGS} --target=\"${CARGO_XCODE_TARGET_TRIPLE}\"; );\nfi\n# it's too hard to explain Cargo's actual exe path to Xcode build graph, so hardlink to a known-good path instead\nBUILT_SRC=\"${CARGO_TARGET_DIR}/${CARGO_XCODE_TARGET_TRIPLE}/${CARGO_XCODE_BUILD_MODE}/${CARGO_XCODE_CARGO_FILE_NAME}\"\nln -f -- \"$BUILT_SRC\" \"$SCRIPT_OUTPUT_FILE_0\"\n\n# xcode generates dep file, but for its own path, so append our rename to it\nDEP_FILE_SRC=\"${CARGO_TARGET_DIR}/${CARGO_XCODE_TARGET_TRIPLE}/${CARGO_XCODE_BUILD_MODE}/${CARGO_XCODE_CARGO_DEP_FILE_NAME}\"\nif [ -f \"$DEP_FILE_SRC\" ]; then\n DEP_FILE_DST=\"${DERIVED_FILE_DIR}/${CARGO_XCODE_TARGET_ARCH}-${EXECUTABLE_NAME}.d\"\n cp -f \"$DEP_FILE_SRC\" \"$DEP_FILE_DST\"\n echo >> \"$DEP_FILE_DST\" \"$SCRIPT_OUTPUT_FILE_0: $BUILT_SRC\"\nfi\n\n# lipo script needs to know all the platform-specific files that have been built\n# archs is in the file name, so that paths don't stay around after archs change\n# must match input for LipoScript\nFILE_LIST=\"${DERIVED_FILE_DIR}/${ARCHS}-${EXECUTABLE_NAME}.xcfilelist\"\ntouch \"$FILE_LIST\"\nif ! egrep -q \"$SCRIPT_OUTPUT_FILE_0\" \"$FILE_LIST\" ; then\n echo >> \"$FILE_LIST\" \"$SCRIPT_OUTPUT_FILE_0\"\nfi\n"; - }; -/* End PBXBuildRule section */ - /* Begin PBXFileReference section */ ADDEDBA66A6E1 /* libresolv.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libresolv.tbd; path = usr/lib/libresolv.tbd; sourceTree = SDKROOT; }; - CA604C7415FB2A3731F5016A /* liblibrustdesk_static.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = liblibrustdesk_static.a; sourceTree = BUILT_PRODUCTS_DIR; }; - CA6071B5A0F5A7A3EF2297AA /* librustdesk.dylib */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.dylib"; includeInIndex = 0; path = librustdesk.dylib; sourceTree = BUILT_PRODUCTS_DIR; }; - CA60D3BC5386B357B2AB834F /* rustdesk */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = rustdesk; sourceTree = BUILT_PRODUCTS_DIR; }; - CA60D3BC5386D3D7DBD96893 /* naming */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = naming; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXGroup section */ @@ -51,10 +22,6 @@ CA603C4309E122869D176AE5 /* Products */ = { isa = PBXGroup; children = ( - CA6071B5A0F5A7A3EF2297AA /* librustdesk.dylib */, - CA604C7415FB2A3731F5016A /* liblibrustdesk_static.a */, - CA60D3BC5386D3D7DBD96893 /* naming */, - CA60D3BC5386B357B2AB834F /* rustdesk */, ); name = Products; sourceTree = ""; @@ -70,7 +37,6 @@ CA603C4309E1D65BC3C892A8 = { isa = PBXGroup; children = ( - CA603C4309E13EF4668187A5 /* Cargo.toml */, CA603C4309E122869D176AE5 /* Products */, CA603C4309E198AF0B5890DB /* Frameworks */, ); @@ -78,100 +44,11 @@ }; /* End PBXGroup section */ -/* Begin PBXNativeTarget section */ - CA604C7415FB12977AAB839F /* librustdesk-staticlib */ = { - isa = PBXNativeTarget; - buildConfigurationList = CA6028B9540B12977AAB839F /* Build configuration list for PBXNativeTarget "librustdesk-staticlib" */; - buildPhases = ( - CA6033723F8212977AAB839F /* Sources */, - CA603C4309E1AF6EBB7F357C /* Universal Binary lipo */, - ); - buildRules = ( - CA603C4309E1AC6C1400ACA8 /* PBXBuildRule */, - ); - dependencies = ( - ); - name = "librustdesk-staticlib"; - productName = liblibrustdesk_static.a; - productReference = CA604C7415FB2A3731F5016A /* liblibrustdesk_static.a */; - productType = "com.apple.product-type.library.static"; - }; - CA6071B5A0F5D6691E4C3FF1 /* librustdesk-cdylib */ = { - isa = PBXNativeTarget; - buildConfigurationList = CA6028B9540BD6691E4C3FF1 /* Build configuration list for PBXNativeTarget "librustdesk-cdylib" */; - buildPhases = ( - CA6033723F82D6691E4C3FF1 /* Sources */, - CA603C4309E1AF6EBB7F357C /* Universal Binary lipo */, - ); - buildRules = ( - CA603C4309E1AC6C1400ACA8 /* PBXBuildRule */, - ); - dependencies = ( - ); - name = "librustdesk-cdylib"; - productName = librustdesk.dylib; - productReference = CA6071B5A0F5A7A3EF2297AA /* librustdesk.dylib */; - productType = "com.apple.product-type.library.dynamic"; - }; - CA60D3BC5386C858B7409EE3 /* naming-bin */ = { - isa = PBXNativeTarget; - buildConfigurationList = CA6028B9540BC858B7409EE3 /* Build configuration list for PBXNativeTarget "naming-bin" */; - buildPhases = ( - CA6033723F82C858B7409EE3 /* Sources */, - CA603C4309E1AF6EBB7F357C /* Universal Binary lipo */, - ); - buildRules = ( - CA603C4309E1AC6C1400ACA8 /* PBXBuildRule */, - ); - dependencies = ( - ); - name = "naming-bin"; - productName = naming; - productReference = CA60D3BC5386D3D7DBD96893 /* naming */; - productType = "com.apple.product-type.tool"; - }; - CA60D3BC5386C9FA710A2219 /* rustdesk-bin */ = { - isa = PBXNativeTarget; - buildConfigurationList = CA6028B9540BC9FA710A2219 /* Build configuration list for PBXNativeTarget "rustdesk-bin" */; - buildPhases = ( - CA6033723F82C9FA710A2219 /* Sources */, - CA603C4309E1AF6EBB7F357C /* Universal Binary lipo */, - ); - buildRules = ( - CA603C4309E1AC6C1400ACA8 /* PBXBuildRule */, - ); - dependencies = ( - ); - name = "rustdesk-bin"; - productName = rustdesk; - productReference = CA60D3BC5386B357B2AB834F /* rustdesk */; - productType = "com.apple.product-type.tool"; - }; -/* End PBXNativeTarget section */ - /* Begin PBXProject section */ CA603C4309E1E04653AD465F /* Project object */ = { isa = PBXProject; attributes = { LastUpgradeCheck = 1300; - TargetAttributes = { - CA604C7415FB12977AAB839F = { - CreatedOnToolsVersion = 9.2; - ProvisioningStyle = Automatic; - }; - CA6071B5A0F5D6691E4C3FF1 = { - CreatedOnToolsVersion = 9.2; - ProvisioningStyle = Automatic; - }; - CA60D3BC5386C858B7409EE3 = { - CreatedOnToolsVersion = 9.2; - ProvisioningStyle = Automatic; - }; - CA60D3BC5386C9FA710A2219 = { - CreatedOnToolsVersion = 9.2; - ProvisioningStyle = Automatic; - }; - }; }; buildConfigurationList = CA603C4309E180E02D6C7F57 /* Build configuration list for PBXProject "rustdesk" */; compatibilityVersion = "Xcode 11.4"; @@ -186,161 +63,11 @@ projectDirPath = ""; projectRoot = ""; targets = ( - CA6071B5A0F5D6691E4C3FF1 /* librustdesk-cdylib */, - CA604C7415FB12977AAB839F /* librustdesk-staticlib */, - CA60D3BC5386C858B7409EE3 /* naming-bin */, - CA60D3BC5386C9FA710A2219 /* rustdesk-bin */, ); }; /* End PBXProject section */ -/* Begin PBXShellScriptBuildPhase section */ - CA603C4309E1AF6EBB7F357C /* Universal Binary lipo */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "$(DERIVED_FILE_DIR)/$(ARCHS)-$(EXECUTABLE_NAME).xcfilelist", - ); - name = "Universal Binary lipo"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(TARGET_BUILD_DIR)/$(EXECUTABLE_PATH)", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "# generated with cargo-xcode 1.4.1\nset -eux; cat \"$DERIVED_FILE_DIR/$ARCHS-$EXECUTABLE_NAME.xcfilelist\" | tr '\\n' '\\0' | xargs -0 lipo -create -output \"$TARGET_BUILD_DIR/$EXECUTABLE_PATH\""; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - CA6033723F8212977AAB839F /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - CA6061C6409F12977AAB839F /* Cargo.toml in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - CA6033723F82C858B7409EE3 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - CA6061C6409FC858B7409EE3 /* Cargo.toml in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - CA6033723F82C9FA710A2219 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - CA6061C6409FC9FA710A2219 /* Cargo.toml in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - CA6033723F82D6691E4C3FF1 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - CA6061C6409FD6691E4C3FF1 /* Cargo.toml in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - /* Begin XCBuildConfiguration section */ - CA604B55B26012977AAB839F /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CARGO_XCODE_CARGO_DEP_FILE_NAME = liblibrustdesk.d; - CARGO_XCODE_CARGO_FILE_NAME = liblibrustdesk.a; - INSTALL_GROUP = ""; - INSTALL_MODE_FLAG = ""; - INSTALL_OWNER = ""; - PRODUCT_NAME = librustdesk_static; - SKIP_INSTALL = YES; - SUPPORTED_PLATFORMS = "macosx iphonesimulator iphoneos appletvsimulator appletvos"; - }; - name = Debug; - }; - CA604B55B260C858B7409EE3 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CARGO_XCODE_CARGO_DEP_FILE_NAME = naming.d; - CARGO_XCODE_CARGO_FILE_NAME = naming; - PRODUCT_NAME = naming; - SUPPORTED_PLATFORMS = macosx; - }; - name = Debug; - }; - CA604B55B260C9FA710A2219 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CARGO_XCODE_CARGO_DEP_FILE_NAME = rustdesk.d; - CARGO_XCODE_CARGO_FILE_NAME = rustdesk; - PRODUCT_NAME = rustdesk; - SUPPORTED_PLATFORMS = macosx; - }; - name = Debug; - }; - CA604B55B260D6691E4C3FF1 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CARGO_XCODE_CARGO_DEP_FILE_NAME = liblibrustdesk.d; - CARGO_XCODE_CARGO_FILE_NAME = liblibrustdesk.dylib; - PRODUCT_NAME = librustdesk; - SUPPORTED_PLATFORMS = macosx; - }; - name = Debug; - }; - CA60583BB9CE12977AAB839F /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CARGO_XCODE_CARGO_DEP_FILE_NAME = liblibrustdesk.d; - CARGO_XCODE_CARGO_FILE_NAME = liblibrustdesk.a; - INSTALL_GROUP = ""; - INSTALL_MODE_FLAG = ""; - INSTALL_OWNER = ""; - PRODUCT_NAME = librustdesk_static; - SKIP_INSTALL = YES; - SUPPORTED_PLATFORMS = "macosx iphonesimulator iphoneos appletvsimulator appletvos"; - }; - name = Release; - }; - CA60583BB9CEC858B7409EE3 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CARGO_XCODE_CARGO_DEP_FILE_NAME = naming.d; - CARGO_XCODE_CARGO_FILE_NAME = naming; - PRODUCT_NAME = naming; - SUPPORTED_PLATFORMS = macosx; - }; - name = Release; - }; - CA60583BB9CEC9FA710A2219 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CARGO_XCODE_CARGO_DEP_FILE_NAME = rustdesk.d; - CARGO_XCODE_CARGO_FILE_NAME = rustdesk; - PRODUCT_NAME = rustdesk; - SUPPORTED_PLATFORMS = macosx; - }; - name = Release; - }; - CA60583BB9CED6691E4C3FF1 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CARGO_XCODE_CARGO_DEP_FILE_NAME = liblibrustdesk.d; - CARGO_XCODE_CARGO_FILE_NAME = liblibrustdesk.dylib; - PRODUCT_NAME = librustdesk; - SUPPORTED_PLATFORMS = macosx; - }; - name = Release; - }; CA608F3F78EE228BE02872F8 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -387,42 +114,6 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ - CA6028B9540B12977AAB839F /* Build configuration list for PBXNativeTarget "librustdesk-staticlib" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - CA60583BB9CE12977AAB839F /* Release */, - CA604B55B26012977AAB839F /* Debug */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - CA6028B9540BC858B7409EE3 /* Build configuration list for PBXNativeTarget "naming-bin" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - CA60583BB9CEC858B7409EE3 /* Release */, - CA604B55B260C858B7409EE3 /* Debug */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - CA6028B9540BC9FA710A2219 /* Build configuration list for PBXNativeTarget "rustdesk-bin" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - CA60583BB9CEC9FA710A2219 /* Release */, - CA604B55B260C9FA710A2219 /* Debug */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - CA6028B9540BD6691E4C3FF1 /* Build configuration list for PBXNativeTarget "librustdesk-cdylib" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - CA60583BB9CED6691E4C3FF1 /* Release */, - CA604B55B260D6691E4C3FF1 /* Debug */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; CA603C4309E180E02D6C7F57 /* Build configuration list for PBXProject "rustdesk" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 432b15b15..738567b22 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -5,253 +5,260 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "49.0.0" + version: "50.0.0" after_layout: dependency: transitive description: name: after_layout - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.0" analyzer: dependency: transitive description: name: analyzer - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "5.1.0" + version: "5.2.0" animations: dependency: transitive description: name: animations - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.7" archive: dependency: transitive description: name: archive - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "3.3.1" + version: "3.3.4" args: dependency: transitive description: name: args - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.3.1" async: dependency: transitive description: name: async - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.9.0" auto_size_text: dependency: "direct main" description: name: auto_size_text - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.0" back_button_interceptor: dependency: "direct main" description: name: back_button_interceptor - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "6.0.1" + version: "6.0.2" + bot_toast: + dependency: "direct main" + description: + name: bot_toast + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.0.3" build: dependency: transitive description: name: build - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.3.1" build_config: dependency: transitive description: name: build_config - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "1.1.0" + version: "1.1.1" build_daemon: dependency: transitive description: name: build_daemon - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.0" build_resolvers: dependency: transitive description: name: build_resolvers - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.10" build_runner: dependency: "direct dev" description: name: build_runner - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "2.2.1" + version: "2.3.2" build_runner_core: dependency: transitive description: name: build_runner_core - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "7.2.4" + version: "7.2.7" built_collection: dependency: transitive description: name: built_collection - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "5.1.1" built_value: dependency: transitive description: name: built_value - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "8.4.1" + version: "8.4.2" cached_network_image: dependency: transitive description: name: cached_network_image - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.2.1" cached_network_image_platform_interface: dependency: transitive description: name: cached_network_image_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.0" cached_network_image_web: dependency: transitive description: name: cached_network_image_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.1" characters: dependency: transitive description: name: characters - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.0" charcode: dependency: transitive description: name: charcode - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.1" checked_yaml: dependency: transitive description: name: checked_yaml - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.1" clock: dependency: transitive description: name: clock - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.0" code_builder: dependency: transitive description: name: code_builder - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "4.3.0" collection: dependency: transitive description: name: collection - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.16.0" contextmenu: dependency: "direct main" description: name: contextmenu - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.0" convert: dependency: transitive description: name: convert - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "3.0.2" + version: "3.1.0" cross_file: dependency: transitive description: name: cross_file - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.3.3+2" crypto: dependency: transitive description: name: crypto - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.2" csslib: dependency: transitive description: name: csslib - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.17.2" cupertino_icons: dependency: "direct main" description: name: cupertino_icons - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.5" dart_style: dependency: transitive description: name: dart_style - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.4" dash_chat_2: dependency: "direct main" description: name: dash_chat_2 - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.0.15" desktop_drop: dependency: "direct main" description: name: desktop_drop - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.3.3" desktop_multi_window: dependency: "direct main" description: path: "." - ref: bf278fc8a8ff787e46fa3ab97674373bfaa20f23 - resolved-ref: bf278fc8a8ff787e46fa3ab97674373bfaa20f23 + ref: "8ee8eb59cabf6ac83a13fe002de7d4a231263a58" + resolved-ref: "8ee8eb59cabf6ac83a13fe002de7d4a231263a58" url: "https://github.com/Kingtous/rustdesk_desktop_multi_window" source: git version: "0.1.0" @@ -259,91 +266,91 @@ packages: dependency: "direct main" description: name: device_info_plus - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "4.1.3" device_info_plus_linux: dependency: transitive description: name: device_info_plus_linux - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.0" device_info_plus_macos: dependency: transitive description: name: device_info_plus_macos - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.0" device_info_plus_platform_interface: dependency: transitive description: name: device_info_plus_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.0" device_info_plus_web: dependency: transitive description: name: device_info_plus_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.0" device_info_plus_windows: dependency: transitive description: name: device_info_plus_windows - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "4.1.0" draggable_float_widget: dependency: "direct main" description: name: draggable_float_widget - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.0.2" event_bus: dependency: transitive description: name: event_bus - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.0" external_path: dependency: "direct main" description: name: external_path - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.3" ffi: dependency: "direct main" description: name: ffi - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.1" file: dependency: transitive description: name: file - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "6.1.4" file_picker: dependency: "direct main" description: name: file_picker - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "5.2.1" + version: "5.2.2" fixnum: dependency: transitive description: name: fixnum - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.1" flutter: @@ -355,29 +362,29 @@ packages: dependency: transitive description: name: flutter_blurhash - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.7.0" flutter_breadcrumb: dependency: "direct main" description: name: flutter_breadcrumb - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.1" flutter_cache_manager: dependency: transitive description: name: flutter_cache_manager - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.3.0" flutter_custom_cursor: dependency: "direct main" description: path: "." - ref: dec2166e881c47d922e1edc484d10d2cd5c2103b - resolved-ref: dec2166e881c47d922e1edc484d10d2cd5c2103b + ref: bfb19c84a8244771488bc05cc5f9c9b5e0324cfd + resolved-ref: bfb19c84a8244771488bc05cc5f9c9b5e0324cfd url: "https://github.com/Kingtous/rustdesk_flutter_custom_cursor" source: git version: "0.0.1" @@ -385,14 +392,14 @@ packages: dependency: "direct main" description: name: flutter_improved_scrolling - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.0.3" flutter_lints: dependency: "direct dev" description: name: flutter_lints - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.1" flutter_localizations: @@ -404,14 +411,14 @@ packages: dependency: transitive description: name: flutter_parsed_text - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.1" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.7" flutter_rust_bridge: @@ -427,9 +434,9 @@ packages: dependency: "direct main" description: name: flutter_svg - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "1.1.5" + version: "1.1.6" flutter_web_plugins: dependency: transitive description: flutter @@ -439,408 +446,415 @@ packages: dependency: "direct dev" description: name: freezed - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "2.1.1" + version: "2.2.1" freezed_annotation: dependency: "direct main" description: name: freezed_annotation - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "2.1.0" + version: "2.2.0" frontend_server_client: dependency: transitive description: name: frontend_server_client - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "2.1.3" + version: "3.1.0" get: dependency: "direct main" description: name: get - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "4.6.5" glob: dependency: transitive description: name: glob - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "2.1.0" + version: "2.1.1" graphs: dependency: transitive description: name: graphs - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "2.1.0" + version: "2.2.0" html: dependency: transitive description: name: html - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "0.15.0" + version: "0.15.1" http: dependency: "direct main" description: name: http - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.13.5" http_multi_server: dependency: transitive description: name: http_multi_server - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.2.1" http_parser: dependency: transitive description: name: http_parser - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "4.0.1" + version: "4.0.2" icons_launcher: dependency: "direct dev" description: name: icons_launcher - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.4" image: dependency: "direct main" description: name: image - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "3.2.0" + version: "3.2.2" image_picker: dependency: "direct main" description: name: image_picker - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.8.6" image_picker_android: dependency: transitive description: name: image_picker_android - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.8.5+3" image_picker_for_web: dependency: transitive description: name: image_picker_for_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.10" image_picker_ios: dependency: transitive description: name: image_picker_ios - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.8.6+1" image_picker_platform_interface: dependency: transitive description: name: image_picker_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.6.2" intl: dependency: transitive description: name: intl - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.17.0" io: dependency: transitive description: name: io - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.3" js: dependency: transitive description: name: js - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.6.4" json_annotation: dependency: transitive description: name: json_annotation - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "4.7.0" lints: dependency: transitive description: name: lints - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "2.0.0" + version: "2.0.1" logging: dependency: transitive description: name: logging - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.0" matcher: dependency: transitive description: name: matcher - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.12.12" material_color_utilities: dependency: transitive description: name: material_color_utilities - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.1.4" menu_base: dependency: transitive description: name: menu_base - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.1.1" meta: dependency: transitive description: name: meta - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.7.0" mime: dependency: transitive description: name: mime - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.2" nested: dependency: transitive description: name: nested - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.0" octo_image: dependency: transitive description: name: octo_image - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.2" package_config: dependency: transitive description: name: package_config - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0" package_info_plus: dependency: "direct main" description: name: package_info_plus - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.4.3+1" package_info_plus_linux: dependency: transitive description: name: package_info_plus_linux - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.5" package_info_plus_macos: dependency: transitive description: name: package_info_plus_macos - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.0" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.2" package_info_plus_web: dependency: transitive description: name: package_info_plus_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.6" package_info_plus_windows: dependency: transitive description: name: package_info_plus_windows - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0" path: dependency: "direct main" description: name: path - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.8.1" path_drawing: dependency: transitive description: name: path_drawing - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.1" path_parsing: dependency: transitive description: name: path_parsing - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.1" path_provider: dependency: "direct main" description: name: path_provider - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.11" path_provider_android: dependency: transitive description: name: path_provider_android - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "2.0.20" + version: "2.0.21" path_provider_ios: dependency: transitive description: name: path_provider_ios - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.11" path_provider_linux: dependency: transitive description: name: path_provider_linux - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.7" path_provider_macos: dependency: transitive description: name: path_provider_macos - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.6" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.5" path_provider_windows: dependency: transitive description: name: path_provider_windows - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.3" pedantic: dependency: transitive description: name: pedantic - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.11.1" petitparser: dependency: transitive description: name: petitparser - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "5.0.0" platform: dependency: transitive description: name: platform - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.0" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.3" + pointycastle: + dependency: transitive + description: + name: pointycastle + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.6.2" pool: dependency: transitive description: name: pool - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.5.1" process: dependency: transitive description: name: process - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "4.2.4" provider: dependency: "direct main" description: name: provider - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "6.0.3" + version: "6.0.4" pub_semver: dependency: transitive description: name: pub_semver - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "2.1.1" + version: "2.1.3" pubspec_parse: dependency: transitive description: name: pubspec_parse - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.1" qr_code_scanner: dependency: "direct main" description: name: qr_code_scanner - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.1" rxdart: dependency: "direct main" description: name: rxdart - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "0.27.5" + version: "0.27.7" screen_retriever: dependency: transitive description: @@ -854,91 +868,35 @@ packages: dependency: "direct main" description: name: scroll_pos - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.3.0" settings_ui: dependency: "direct main" description: name: settings_ui - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.2" - shared_preferences: - dependency: "direct main" - description: - name: shared_preferences - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.15" - shared_preferences_android: - dependency: transitive - description: - name: shared_preferences_android - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.13" - shared_preferences_ios: - dependency: transitive - description: - name: shared_preferences_ios - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.1" - shared_preferences_linux: - dependency: transitive - description: - name: shared_preferences_linux - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.1" - shared_preferences_macos: - dependency: transitive - description: - name: shared_preferences_macos - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.4" - shared_preferences_platform_interface: - dependency: transitive - description: - name: shared_preferences_platform_interface - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0" - shared_preferences_web: - dependency: transitive - description: - name: shared_preferences_web - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.4" - shared_preferences_windows: - dependency: transitive - description: - name: shared_preferences_windows - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.1" shelf: dependency: transitive description: name: shelf - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.4.0" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "1.0.2" + version: "1.0.3" shortid: dependency: transitive description: name: shortid - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.1.2" sky_engine: @@ -950,84 +908,84 @@ packages: dependency: transitive description: name: source_gen - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "1.2.5" + version: "1.2.6" source_span: dependency: transitive description: name: source_span - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.9.1" sqflite: dependency: transitive description: name: sqflite - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.3+1" sqflite_common: dependency: transitive description: name: sqflite_common - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "2.3.0" + version: "2.4.0" stack_trace: dependency: transitive description: name: stack_trace - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.10.0" stream_channel: dependency: transitive description: name: stream_channel - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.1" stream_transform: dependency: transitive description: name: stream_transform - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "2.0.1" + version: "2.1.0" string_scanner: dependency: transitive description: name: string_scanner - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.1" synchronized: dependency: transitive description: name: synchronized - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.0+3" term_glyph: dependency: transitive description: name: term_glyph - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.1" timing: dependency: transitive description: name: timing - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.0" toggle_switch: dependency: "direct main" description: name: toggle_switch - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.4.0" tray_manager: @@ -1043,232 +1001,234 @@ packages: dependency: "direct main" description: name: tuple - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.1" typed_data: dependency: transitive description: name: typed_data - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.1" uni_links: dependency: "direct main" description: name: uni_links - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.5.1" uni_links_desktop: dependency: "direct main" description: - name: uni_links_desktop - url: "https://pub.dartlang.org" - source: hosted + path: "." + ref: "5be5113d59c753989dbf1106241379e3fd4c9b18" + resolved-ref: "5be5113d59c753989dbf1106241379e3fd4c9b18" + url: "https://github.com/fufesou/uni_links_desktop.git" + source: git version: "0.1.3" uni_links_platform_interface: dependency: transitive description: name: uni_links_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.0" uni_links_web: dependency: transitive description: name: uni_links_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.1.0" universal_io: dependency: transitive description: name: universal_io - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.4" url_launcher: dependency: "direct main" description: name: url_launcher - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "6.1.6" url_launcher_android: dependency: transitive description: name: url_launcher_android - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "6.0.19" + version: "6.0.21" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "6.0.17" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.1" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.1" url_launcher_web: dependency: transitive description: name: url_launcher_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.13" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.1" uuid: dependency: transitive description: name: uuid - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "3.0.6" + version: "3.0.7" vector_math: dependency: transitive description: name: vector_math - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.2" video_player: dependency: transitive description: name: video_player - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.4.7" video_player_android: dependency: transitive description: name: video_player_android - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.3.9" video_player_avfoundation: dependency: transitive description: name: video_player_avfoundation - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.3.7" video_player_platform_interface: dependency: transitive description: name: video_player_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "5.1.4" video_player_web: dependency: transitive description: name: video_player_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.12" visibility_detector: dependency: "direct main" description: name: visibility_detector - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.3.3" wakelock: dependency: "direct main" description: name: wakelock - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.6.2" wakelock_macos: dependency: transitive description: name: wakelock_macos - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.4.0" wakelock_platform_interface: dependency: transitive description: name: wakelock_platform_interface - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.3.0" wakelock_web: dependency: transitive description: name: wakelock_web - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.4.0" wakelock_windows: dependency: transitive description: name: wakelock_windows - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.2.1" watcher: dependency: transitive description: name: watcher - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "1.0.1" + version: "1.0.2" web_socket_channel: dependency: transitive description: name: web_socket_channel - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.0" win32: dependency: transitive description: name: win32 - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "3.0.0" + version: "3.1.1" win32_registry: dependency: transitive description: name: win32_registry - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.2" window_manager: dependency: "direct main" description: path: "." - ref: "88487257cbafc501599ab4f82ec343b46acec020" - resolved-ref: "88487257cbafc501599ab4f82ec343b46acec020" + ref: "32b24c66151b72bba033ef8b954486aa9351d97b" + resolved-ref: "32b24c66151b72bba033ef8b954486aa9351d97b" url: "https://github.com/Kingtous/rustdesk_window_manager" source: git version: "0.2.7" @@ -1285,28 +1245,28 @@ packages: dependency: transitive description: name: xdg_directories - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.2.0+2" xml: dependency: transitive description: name: xml - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "6.1.0" yaml: dependency: transitive description: name: yaml - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.1" zxing2: dependency: "direct main" description: name: zxing2 - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.1.0" sdks: From 1491dea9d81473cb66070e280226e1470da6228c Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 21 Nov 2022 17:10:38 +0800 Subject: [PATCH 1006/2015] fix: nightly build --- .github/workflows/flutter-nightly.yml | 372 ++++++++++++++++++++------ build.py | 35 ++- 2 files changed, 309 insertions(+), 98 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 0b49b9893..76b82044d 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -10,7 +10,9 @@ env: LLVM_VERSION: "10.0" FLUTTER_VERSION: "3.0.5" TAG_NAME: "nightly" - VCPKG_COMMIT_ID: '6ca56aeb457f033d344a7106cb3f9f1abf8f4e98' + # vcpkg version: 2022.05.10 + # for multiarch gcc compatibility + VCPKG_COMMIT_ID: "14e7bb4ae24616ec54ff6b2f6ef4e8659434ea44" VERSION: "1.2.0" jobs: @@ -23,7 +25,7 @@ jobs: job: # - { target: i686-pc-windows-msvc , os: windows-2019 } # - { target: x86_64-pc-windows-gnu , os: windows-2019 } - - { target: x86_64-pc-windows-msvc , os: windows-2019 } + - { target: x86_64-pc-windows-msvc, os: windows-2019 } steps: - name: Checkout source code uses: actions/checkout@v3 @@ -36,9 +38,9 @@ jobs: - name: Install flutter uses: subosito/flutter-action@v2 with: - channel: 'stable' + channel: "stable" flutter-version: ${{ env.FLUTTER_VERSION }} - + - name: Replace engine with rustdesk custom flutter engine run: | flutter doctor -v @@ -100,18 +102,22 @@ jobs: files: | rustdesk-*.exe - builf-for-macOS: + build-for-macOS: name: ${{ matrix.job.target }} (${{ matrix.job.os }},${{ matrix.job.extra-build-args }}) runs-on: ${{ matrix.job.os }} strategy: fail-fast: false matrix: job: - - { target: x86_64-apple-darwin , os: macos-10.15, extra-build-args: ""} + - { + target: x86_64-apple-darwin, + os: macos-10.15, + extra-build-args: "", + } steps: - name: Checkout source code uses: actions/checkout@v3 - + - name: Install build runtime run: | brew install llvm create-dmg nasm yasm cmake gcc wget ninja @@ -119,7 +125,7 @@ jobs: - name: Install flutter uses: subosito/flutter-action@v2 with: - channel: 'stable' + channel: "stable" flutter-version: ${{ env.FLUTTER_VERSION }} - name: Install Rust toolchain @@ -143,7 +149,7 @@ jobs: pushd /tmp/flutter_rust_bridge/frb_codegen && cargo install --path . && popd pushd flutter && flutter pub get && popd ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart - + - name: Restore from cache and install vcpkg uses: lukka/run-vcpkg@v7 with: @@ -187,47 +193,85 @@ jobs: files: | rustdesk*-${{ matrix.job.target }}.dmg - build-for-linux: - name: ${{ matrix.job.target }} (${{ matrix.job.os }},${{ matrix.job.extra-build-args }}) + build-vcpkg-deps-linux: runs-on: ${{ matrix.job.os }} strategy: fail-fast: false matrix: job: - # - { target: aarch64-unknown-linux-gnu , os: ubuntu-18.04, use-cross: true } - # - { target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true } - # - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true } - # - { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } - # - { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } - - { target: x86_64-unknown-linux-gnu , os: ubuntu-18.04, extra-build-args: ""} - - { target: x86_64-unknown-linux-gnu , os: ubuntu-18.04, extra-build-args: "--flatpak"} - # - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } + # - { arch: armv7 , os: ubuntu-18.04} + - { arch: x86_64, os: ubuntu-18.04 } + # - { arch: aarch64 , os: ubuntu-18.04} + steps: + - name: Create vcpkg artifacts folder + run: mkdir -p /opt/artifacts + + - name: Cache Vcpkg + id: cache-vcpkg + uses: actions/cache@v3 + with: + path: /opt/artifacts + key: vcpkg-${{ matrix.job.arch }} + + - uses: Kingtous/run-on-arch-action@amd64-support + name: Run vcpkg install on ${{ matrix.job.arch }} + id: vcpkg + with: + arch: ${{ matrix.job.arch }} + distro: ubuntu18.04 + githubToken: ${{ github.token }} + setup: | + ls -l "/opt/artifacts" + dockerRunArgs: | + --volume "/opt/artifacts:/artifacts" + shell: /bin/bash + install: | + apt update -y + # CMake 3.15+ + apt install -y gpg wget ca-certificates + echo 'deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ bionic main' | tee /etc/apt/sources.list.d/kitware.list >/dev/null + wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/null | gpg --dearmor - | tee /usr/share/keyrings/kitware-archive-keyring.gpg >/dev/null + apt update -y + apt install -y curl zip unzip tar git cmake g++ gcc build-essential pkg-config wget nasm yasm ninja-build + cmake --version + gcc -v + run: | + export VCPKG_FORCE_SYSTEM_BINARIES=1 + pushd /artifacts + git clone https://github.com/microsoft/vcpkg.git || true + git config --global --add safe.directory /artifacts/vcpkg || true + pushd vcpkg + git reset --hard ${{ env.VCPKG_COMMIT_ID }} + ./bootstrap-vcpkg.sh + ./vcpkg install libvpx libyuv opus + + - name: Upload artifacts + uses: actions/upload-artifact@master + with: + name: vcpkg-artifact-${{ matrix.job.arch }} + path: | + /opt/artifacts/vcpkg/installed + + generate-bridge-linux: + name: generate bridge + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: false + matrix: + job: + - { + target: x86_64-unknown-linux-gnu, + os: ubuntu-18.04, + extra-build-args: "", + } steps: - name: Checkout source code uses: actions/checkout@v3 - - name: Get build target triple - uses: jungwinter/split@v2 - id: build-target-triple - with: - separator: '-' - msg: ${{ matrix.job.target }} - - name: Install prerequisites run: | - case ${{ matrix.job.target }} in - x86_64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt install -y g++ gcc;; - arm-unknown-linux-*) sudo apt-get -y update ; sudo apt-get -y install gcc-arm-linux-gnueabihf ;; - aarch64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt-get -y install gcc-aarch64-linux-gnu ;; - esac - # common package - sudo apt install -y git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libvdpau-dev libva-dev libclang-dev llvm-dev libclang-10-dev llvm-10-dev - - - name: Install flutter - uses: subosito/flutter-action@v2 - with: - channel: 'stable' - flutter-version: ${{ env.FLUTTER_VERSION }} + sudo apt update -y + sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libvdpau-dev libva-dev libclang-dev llvm-dev libclang-10-dev llvm-10-dev pkg-config - name: Install Rust toolchain uses: actions-rs/toolchain@v1 @@ -239,49 +283,210 @@ jobs: - uses: Swatinem/rust-cache@v2 with: - prefix-key: ${{ matrix.job.os }} + prefix-key: bridge-${{ matrix.job.os }} + + - name: Cache Bridge + id: cache-bridge + uses: actions/cache@v3 + with: + path: /tmp/flutter_rust_bridge + key: vcpkg-${{ matrix.job.arch }} + + - name: Install flutter + uses: subosito/flutter-action@v2 + with: + channel: "stable" + flutter-version: ${{ env.FLUTTER_VERSION }} + + - name: Install ffigen + run: | + dart pub global activate ffigen --version 5.0.1 - name: Install flutter rust bridge deps shell: bash run: | - dart pub global activate ffigen --version 5.0.1 - # flutter_rust_bridge - pushd /tmp && git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1 && popd + pushd /tmp && git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1 || true && popd pushd /tmp/flutter_rust_bridge/frb_codegen && cargo install --path . && popd pushd flutter && flutter pub get && popd + + - name: Run flutter rust bridge + run: | ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart - - name: Restore from cache and install vcpkg - uses: lukka/run-vcpkg@v7 + - name: Upload Artifcat + uses: actions/upload-artifact@master with: - setupOnly: true - vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} + name: bridge-artifact + path: | + ./src/bridge_generated.rs + ./flutter/lib/generated_bridge.dart + ./flutter/lib/generated_bridge.freezed.dart - - name: Install vcpkg dependencies + build-rustdesk-lib-linux: + needs: [generate-bridge-linux, build-vcpkg-deps-linux] + name: build-rust-lib ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: false + matrix: + job: + # - { arch: aarch64, target: aarch64-unknown-linux-gnu , os: ubuntu-18.04, use-cross: true, extra-build-features: "" } + # - { arch: aarch64, target: aarch64-unknown-linux-gnu , os: ubuntu-18.04, use-cross: true, extra-build-features: "flatpak" } + # - { arch: armv7, target: arm-unknown-linux-gnueabihf , os: ubuntu-18.04, use-cross: true, extra-build-features: "" } + # - { arch: armv7, target: arm-unknown-linux-gnueabihf , os: ubuntu-18.04, use-cross: true, extra-build-features: "flatpak" } + # - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true } + # - { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } + # - { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } + - { + arch: x86_64, + target: x86_64-unknown-linux-gnu, + os: ubuntu-18.04, + extra-build-features: "", + } + - { + arch: x86_64, + target: x86_64-unknown-linux-gnu, + os: ubuntu-18.04, + extra-build-features: "flatpak", + } + # - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } + steps: + - name: Checkout source code + uses: actions/checkout@v3 + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: ${{ matrix.job.target }} + override: true + profile: minimal # minimal component installation (ie, no documentation) + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: bridge-${{ matrix.job.os }} + + - name: Disable rust bridge build run: | + sed -i "s/gen_flutter_rust_bridge();/\/\//g" build.rs + + - name: Restore bridge files + uses: actions/download-artifact@master + with: + name: bridge-artifact + path: ./ + + - name: Restore vcpkg files + uses: actions/download-artifact@master + with: + name: vcpkg-artifact-${{ matrix.job.arch }} + path: /opt/artifacts/vcpkg/installed + + - name: Output devs + run: | + ls -l ./ + tree -L 3 /opt/artifacts/vcpkg/installed + + - name: Install prerequisites + run: | + sudo apt update -y case ${{ matrix.job.target }} in - x86_64-unknown-linux-gnu) $VCPKG_ROOT/vcpkg install libvpx libyuv opus;; - arm-unknown-linux-*) $VCPKG_ROOT/vcpkg install libvpx:arm-linux libyuv:arm-linux opus:arm-linux;; - aarch64-unknown-linux-gnu) $VCPKG_ROOT/vcpkg install libvpx:arm64-linux libyuv:arm64-linux opus:arm64-linux;; + x86_64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt install -y g++ gcc;; + arm-unknown-linux-*) sudo apt-get -y update ; sudo apt-get -y install gcc-arm-linux-gnueabihf ;; + aarch64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt-get -y install gcc-aarch64-linux-gnu ;; esac - shell: bash + # common package + sudo apt install -y git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libvdpau-dev libva-dev libclang-dev llvm-dev libclang-10-dev llvm-10-dev pkg-config tree - - name: Install cargo bundle tools + - name: Build rustdesk lib run: | - cargo install cargo-bundle + export VCPKG_ROOT=/opt/artifacts/vcpkg + cargo build --lib --features hwcodec,flutter,${{ matrix.job.extra-build-features }} --release - - name: Show version information (Rust, cargo, GCC) - shell: bash + - name: Upload Artifacts + uses: actions/upload-artifact@master + with: + name: librustdesk-${{ matrix.job.arch }}-${{ matrix.job.extra-build-features }}.so + path: target/release/liblibrustdesk.so + + build-rustdesk-linux: + needs: [build-rustdesk-lib-linux] + name: build-rustdesk ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-args }}] + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: false + matrix: + job: + # - { arch: aarch64, target: aarch64-unknown-linux-gnu , os: ubuntu-18.04, use-cross: true, extra-build-features: "" } + # - { arch: aarch64, target: aarch64-unknown-linux-gnu , os: ubuntu-18.04, use-cross: true, extra-build-features: "flatpak" } + # - { arch: armv7, target: arm-unknown-linux-gnueabihf , os: ubuntu-18.04, use-cross: true, extra-build-features: "" } + # - { arch: armv7, target: arm-unknown-linux-gnueabihf , os: ubuntu-18.04, use-cross: true, extra-build-features: "flatpak" } + # - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true } + # - { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } + # - { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } + - { + arch: x86_64, + target: x86_64-unknown-linux-gnu, + os: ubuntu-18.04, + extra-build-features: "", + } + - { + arch: x86_64, + target: x86_64-unknown-linux-gnu, + os: ubuntu-18.04, + extra-build-features: "flatpak", + } + # - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } + steps: + - name: Checkout source code + uses: actions/checkout@v3 + + - name: Restore bridge files + uses: actions/download-artifact@master + with: + name: bridge-artifact + path: ./ + + - name: Prepare env run: | - gcc --version || true - rustup -V - rustup toolchain list - rustup default - cargo -V - rustc -V + sudo apt update -y + sudo apt install -y git curl wget nasm yasm libgtk-3-dev + mkdir -p ./target/release/ - - name: Build rustdesk - run: ./build.py --flutter --hwcodec ${{ matrix.job.extra-build-args }} + - name: Restore rust lib files + uses: actions/download-artifact@master + with: + name: librustdesk-${{ matrix.job.arch }}-${{ matrix.job.extra-build-features }}.so + path: ./target/release/liblibrustdesk.so + + - uses: Kingtous/run-on-arch-action@amd64-support + name: Run vcpkg install on ${{ matrix.job.arch }} + id: vcpkg + with: + arch: ${{ matrix.job.arch }} + distro: ubuntu18.04 + githubToken: ${{ github.token }} + setup: | + ls -l "${PWD}" + dockerRunArgs: | + --volume "${PWD}:/workspace" + --volume "/opt/artifacts:/opt/artifacts" + shell: /bin/bash + install: | + apt update -y + apt install -y git cmake g++ gcc build-essential nasm yasm curl unzip xz-utils python3 wget pkg-config ninja-build pkg-config libgtk-3-dev liblzma-dev clang libappindicator3-dev + run: | + # disable git safe.directory + git config --global --add safe.directory "*" + # Setup Flutter + pushd /opt + wget https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_${{ env.FLUTTER_VERSION }}-stable.tar.xz + tar xf flutter_linux_${{ env.FLUTTER_VERSION }}-stable.tar.xz + ls -l . + export PATH=/opt/flutter/bin:$PATH + flutter doctor -v + pushd /workspace + python3 ./build.py --flutter --hwcodec --skip-cargo - name: Rename rustdesk shell: bash @@ -297,21 +502,21 @@ jobs: tag_name: ${{ env.TAG_NAME }} files: | rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb - + - name: Upload Artifcat uses: actions/upload-artifact@master - if: ${{ contains(matrix.job.extra-build-args, 'flatpak') }} + if: ${{ contains(matrix.job.extra-build-features, 'flatpak') }} with: name: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb path: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb - name: Patch archlinux PKGBUILD - if: ${{ matrix.job.extra-build-args == '' }} + if: ${{ matrix.job.extra-build-features == '' }} run: | - sed -i "s/arch=('x86_64')/arch=('${{ steps.build-target-triple.outputs._0 }}')/g" res/PKGBUILD + sed -i "s/arch=('x86_64')/arch=('${{ matrix.job.arch }}')/g" res/PKGBUILD - name: Build archlinux package - if: ${{ matrix.job.extra-build-args == '' }} + if: ${{ matrix.job.extra-build-features == '' }} uses: vufa/arch-makepkg-action@master with: packages: > @@ -346,7 +551,7 @@ jobs: cd res && HBB=`pwd`/.. FLUTTER=1 makepkg -f - name: Publish archlinux package - if: ${{ matrix.job.extra-build-args == '' }} + if: ${{ matrix.job.extra-build-features == '' }} uses: softprops/action-gh-release@v1 with: prerelease: true @@ -356,40 +561,34 @@ jobs: - name: Make RPM package shell: bash - if: ${{ matrix.job.extra-build-args == '' }} + if: ${{ matrix.job.extra-build-features == '' }} run: | sudo apt install -y rpm HBB=`pwd` rpmbuild ./res/rpm-flutter.spec -bb - pushd ~/rpmbuild/RPMS/${{ steps.build-target-triple.outputs._0 }} + pushd ~/rpmbuild/RPMS/${{ matrix.job.arch }} for name in rustdesk*??.rpm; do mv "$name" "${name%%.rpm}-fedora28-centos8.rpm" done - name: Publish fedora28/centos8 package - if: ${{ matrix.job.extra-build-args == '' }} + if: ${{ matrix.job.extra-build-features == '' }} uses: softprops/action-gh-release@v1 with: prerelease: true tag_name: ${{ env.TAG_NAME }} files: | - /home/runner/rpmbuild/RPMS/${{ steps.build-target-triple.outputs._0 }}/*.rpm + /home/runner/rpmbuild/RPMS/${{ matrix.job.arch }}/*.rpm build-flatpak: name: Build Flatpak - needs: [build-for-linux] + needs: [build-rustdesk-linux] runs-on: ${{ matrix.job.os }} strategy: fail-fast: false matrix: job: - - { target: aarch64-unknown-linux-gnu , os: ubuntu-18.04, use-cross: true } - # - { target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true } - # - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true } - # - { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } - # - { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } - # - { target: x86_64-apple-darwin , os: macos-10.15 } - - { target: x86_64-unknown-linux-gnu , os: ubuntu-18.04, arch: x86_64} - # - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } + # - { target: aarch64-unknown-linux-gnu , os: ubuntu-18.04, use-cross: true, arch: arm64 } + - { target: x86_64-unknown-linux-gnu, os: ubuntu-18.04, arch: x86_64 } steps: - name: Checkout source code uses: actions/checkout@v3 @@ -404,11 +603,11 @@ jobs: with: name: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb path: . - - - name: Rename Binary + + - name: Rename Binary run: | mv rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb rustdesk-${{ env.VERSION }}.deb - + - name: Install Flatpak deps run: | flatpak --user remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo @@ -429,4 +628,3 @@ jobs: tag_name: ${{ env.TAG_NAME }} files: | flatpak/rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}.flatpak - diff --git a/build.py b/build.py index 445eb15bf..aa98ec17e 100755 --- a/build.py +++ b/build.py @@ -15,6 +15,7 @@ osx = platform.platform().startswith( hbb_name = 'rustdesk' + ('.exe' if windows else '') exe_path = 'target/release/' + hbb_name flutter_win_target_dir = 'flutter/build/windows/runner/Release/' +skip_cargo = False def get_version(): @@ -87,6 +88,11 @@ def make_parser(): action='store_true', help='Build rustdesk libs with the flatpak feature enabled' ) + parser.add_argument( + '--skip-cargo', + action='store_true', + help='Skip cargo build process, only flutter version + Linux supported currently' + ) return parser @@ -227,8 +233,9 @@ def ffi_bindgen_function_refactor(): def build_flutter_deb(version, features): - os.system(f'cargo build --features {features} --lib --release') - ffi_bindgen_function_refactor() + if not skip_cargo: + os.system(f'cargo build --features {features} --lib --release') + ffi_bindgen_function_refactor() os.chdir('flutter') os.system('flutter build linux --release') os.system('mkdir -p tmpdeb/usr/bin/') @@ -236,7 +243,6 @@ def build_flutter_deb(version, features): os.system('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/') os.system('mkdir -p tmpdeb/usr/share/applications/') os.system('mkdir -p tmpdeb/usr/share/polkit-1/actions') - os.system('rm tmpdeb/usr/bin/rustdesk') os.system( 'cp -r build/linux/x64/release/bundle/* tmpdeb/usr/lib/rustdesk/') @@ -266,7 +272,8 @@ def build_flutter_deb(version, features): def build_flutter_dmg(version, features): - os.system(f'cargo build --features {features} --lib --release') + if not skip_cargo: + os.system(f'cargo build --features {features} --lib --release') # copy dylib os.system( "cp target/release/liblibrustdesk.dylib target/release/librustdesk.dylib") @@ -282,7 +289,8 @@ def build_flutter_dmg(version, features): def build_flutter_arch_manjaro(version, features): - os.system(f'cargo build --features {features} --lib --release') + if not skip_cargo: + os.system(f'cargo build --features {features} --lib --release') ffi_bindgen_function_refactor() os.chdir('flutter') os.system('flutter build linux --release') @@ -292,10 +300,11 @@ def build_flutter_arch_manjaro(version, features): def build_flutter_windows(version, features): - os.system(f'cargo build --features {features} --lib --release') - if not os.path.exists("target/release/librustdesk.dll"): - print("cargo build failed, please check rust source code.") - exit(-1) + if not skip_cargo: + os.system(f'cargo build --features {features} --lib --release') + if not os.path.exists("target/release/librustdesk.dll"): + print("cargo build failed, please check rust source code.") + exit(-1) os.chdir('flutter') os.system('flutter build windows --release') os.chdir('..') @@ -320,6 +329,7 @@ def build_flutter_windows(version, features): def main(): + global skip_cargo parser = make_parser() args = parser.parse_args() @@ -339,6 +349,9 @@ def main(): flutter = args.flutter if not flutter: os.system('python3 res/inline-sciter.py') + print(args.skip_cargo) + if args.skip_cargo: + skip_cargo = True portable = args.portable if windows: # build virtual display dynamic library @@ -401,8 +414,8 @@ def main(): build_flutter_dmg(version, features) pass else: - os.system( - 'mv target/release/bundle/deb/rustdesk*.deb ./flutter/rustdesk.deb') + # os.system( + # 'mv target/release/bundle/deb/rustdesk*.deb ./flutter/rustdesk.deb') build_flutter_deb(version, features) else: os.system('cargo bundle --release --features ' + features) From 649543dfea24b859af56c70528737d1db1cbe642 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 22 Nov 2022 19:48:26 +0800 Subject: [PATCH 1007/2015] opt: ci step name --- .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 76b82044d..57d6ae504 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -103,7 +103,7 @@ jobs: rustdesk-*.exe build-for-macOS: - name: ${{ matrix.job.target }} (${{ matrix.job.os }},${{ matrix.job.extra-build-args }}) + name: ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-args }}] runs-on: ${{ matrix.job.os }} strategy: fail-fast: false @@ -460,7 +460,7 @@ jobs: path: ./target/release/liblibrustdesk.so - uses: Kingtous/run-on-arch-action@amd64-support - name: Run vcpkg install on ${{ matrix.job.arch }} + name: Build rustdesk binary for ${{ matrix.job.arch }} id: vcpkg with: arch: ${{ matrix.job.arch }} From 0862057b855f7fba69d0492a70c94731ddd1a441 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 22 Nov 2022 20:27:20 +0800 Subject: [PATCH 1008/2015] fix: so rename --- .github/workflows/flutter-nightly.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 57d6ae504..251d5b52c 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -453,11 +453,17 @@ jobs: sudo apt install -y git curl wget nasm yasm libgtk-3-dev mkdir -p ./target/release/ - - name: Restore rust lib files + - name: Restore the rustdesk lib file uses: actions/download-artifact@master with: name: librustdesk-${{ matrix.job.arch }}-${{ matrix.job.extra-build-features }}.so - path: ./target/release/liblibrustdesk.so + path: ./target/release/ + + - name: Rename the rustdesk lib file + shell: bash + run: | + pushd ./target/release + mv librustdesk-${{ matrix.job.arch }}-${{ matrix.job.extra-build-features }}.so librustdesk.so - uses: Kingtous/run-on-arch-action@amd64-support name: Build rustdesk binary for ${{ matrix.job.arch }} From 4a3dd3a5afbc9ca600f6579fb2563b3ce8ece3e1 Mon Sep 17 00:00:00 2001 From: kingtous Date: Tue, 22 Nov 2022 20:30:22 +0800 Subject: [PATCH 1009/2015] opt: relative macos dylib --- Cargo.toml | 1 + flutter/lib/models/native_model.dart | 4 +++- flutter/macos/Runner.xcodeproj/project.pbxproj | 10 ++++++---- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6375bce26..c7b0c51b8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -161,3 +161,4 @@ codegen-units = 1 panic = 'abort' strip = true #opt-level = 'z' # only have smaller size after strip +rpath = true \ No newline at end of file diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index e8aad8638..d29e0fd2c 100644 --- a/flutter/lib/models/native_model.dart +++ b/flutter/lib/models/native_model.dart @@ -96,7 +96,9 @@ class PlatformFFI { ? DynamicLibrary.open('librustdesk.so') : Platform.isWindows ? DynamicLibrary.open('librustdesk.dll') - : DynamicLibrary.process(); + : Platform.isMacOS + ? DynamicLibrary.open("liblibrustdesk.dylib") + : DynamicLibrary.process(); debugPrint('initializing FFI $_appType'); try { _translate = dylib.lookupFunction('translate'); diff --git a/flutter/macos/Runner.xcodeproj/project.pbxproj b/flutter/macos/Runner.xcodeproj/project.pbxproj index 8a44007a9..5f369ea9e 100644 --- a/flutter/macos/Runner.xcodeproj/project.pbxproj +++ b/flutter/macos/Runner.xcodeproj/project.pbxproj @@ -26,7 +26,8 @@ 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 */; }; - 84010B42292B58A400152837 /* liblibrustdesk.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 84010B41292B585400152837 /* liblibrustdesk.a */; }; + 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 */; }; /* End PBXBuildFile section */ @@ -47,6 +48,7 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( + 84010BA9292CF68300152837 /* liblibrustdesk.dylib in Embed Libraries */, ); name = "Embed Libraries"; runOnlyForDeploymentPostprocessing = 0; @@ -72,7 +74,7 @@ 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 = ""; }; - 84010B41292B585400152837 /* liblibrustdesk.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = liblibrustdesk.a; path = ../../target/release/liblibrustdesk.a; 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 = ""; }; CCB6FE9A2848A6B800E58D48 /* bridge_generated.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = bridge_generated.h; path = Runner/bridge_generated.h; sourceTree = ""; }; @@ -84,7 +86,7 @@ buildActionMask = 2147483647; files = ( C5E54335B73C89F72DB1B606 /* Pods_Runner.framework in Frameworks */, - 84010B42292B58A400152837 /* liblibrustdesk.a in Frameworks */, + 84010BA8292CF66600152837 /* liblibrustdesk.dylib in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -170,7 +172,7 @@ D73912EC22F37F3D000D13A0 /* Frameworks */ = { isa = PBXGroup; children = ( - 84010B41292B585400152837 /* liblibrustdesk.a */, + 84010BA7292CF66600152837 /* liblibrustdesk.dylib */, 26C84465887F29AE938039CB /* Pods_Runner.framework */, ); name = Frameworks; From 9bbe2919a1bf423feef9770681197defcc4e53f6 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 22 Nov 2022 20:52:50 +0800 Subject: [PATCH 1010/2015] fix: linux ci --- .github/workflows/flutter-nightly.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 251d5b52c..499a5f996 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -459,12 +459,6 @@ jobs: name: librustdesk-${{ matrix.job.arch }}-${{ matrix.job.extra-build-features }}.so path: ./target/release/ - - name: Rename the rustdesk lib file - shell: bash - run: | - pushd ./target/release - mv librustdesk-${{ matrix.job.arch }}-${{ matrix.job.extra-build-features }}.so librustdesk.so - - uses: Kingtous/run-on-arch-action@amd64-support name: Build rustdesk binary for ${{ matrix.job.arch }} id: vcpkg From 064322715435d73c332aac9597167a3e60e98f84 Mon Sep 17 00:00:00 2001 From: kingtous Date: Tue, 22 Nov 2022 23:01:42 +0800 Subject: [PATCH 1011/2015] fix: macos titlebar --- flutter/lib/main.dart | 11 ++++--- flutter/macos/Runner/MainFlutterWindow.swift | 31 ++++++++++++++++++++ 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 989ba12f5..e83c4b323 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -42,7 +42,9 @@ Future main(List args) async { if (args.isNotEmpty && args.first == 'multi_window') { windowId = int.parse(args[1]); stateGlobal.setWindowId(windowId!); - WindowController.fromWindowId(windowId!).showTitleBar(false); + if (!Platform.isMacOS) { + WindowController.fromWindowId(windowId!).showTitleBar(false); + } final argument = args[2].isEmpty ? {} : jsonDecode(args[2]) as Map; @@ -168,13 +170,14 @@ void runMultiWindow( ); switch (appType) { case kAppTypeDesktopRemote: - await restoreWindowPosition(WindowType.RemoteDesktop, windowId: windowId!); + await restoreWindowPosition(WindowType.RemoteDesktop, + windowId: windowId!); break; case kAppTypeDesktopFileTransfer: - await restoreWindowPosition(WindowType.FileTransfer, windowId: windowId!); + await restoreWindowPosition(WindowType.FileTransfer, windowId: windowId!); break; case kAppTypeDesktopPortForward: - await restoreWindowPosition(WindowType.PortForward, windowId: windowId!); + await restoreWindowPosition(WindowType.PortForward, windowId: windowId!); break; default: // no such appType diff --git a/flutter/macos/Runner/MainFlutterWindow.swift b/flutter/macos/Runner/MainFlutterWindow.swift index 688292371..2ebdf7fc0 100644 --- a/flutter/macos/Runner/MainFlutterWindow.swift +++ b/flutter/macos/Runner/MainFlutterWindow.swift @@ -1,7 +1,22 @@ import Cocoa import FlutterMacOS +import desktop_multi_window // import bitsdojo_window_macos +import desktop_drop +import device_info_plus_macos +import flutter_custom_cursor +import package_info_plus_macos +import path_provider_macos +import screen_retriever +import sqflite +import tray_manager +import uni_links_desktop +import url_launcher_macos +import wakelock_macos +import window_manager +import window_size + class MainFlutterWindow: NSWindow { override func awakeFromNib() { if (!rustdesk_core_main()){ @@ -14,6 +29,22 @@ class MainFlutterWindow: NSWindow { self.setFrame(windowFrame, display: true) RegisterGeneratedPlugins(registry: flutterViewController) + + FlutterMultiWindowPlugin.setOnWindowCreatedCallback { controller in + // Register the plugin which you want access from other isolate. + // DesktopLifecyclePlugin.register(with: controller.registrar(forPlugin: "DesktopLifecyclePlugin")) + DesktopDropPlugin.register(with: controller.registrar(forPlugin: "DesktopDropPlugin")) + DeviceInfoPlusMacosPlugin.register(with: controller.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) + FlutterCustomCursorPlugin.register(with: controller.registrar(forPlugin: "FlutterCustomCursorPlugin")) + FLTPackageInfoPlusPlugin.register(with: controller.registrar(forPlugin: "FLTPackageInfoPlusPlugin")) + PathProviderPlugin.register(with: controller.registrar(forPlugin: "PathProviderPlugin")) + SqflitePlugin.register(with: controller.registrar(forPlugin: "SqflitePlugin")) + TrayManagerPlugin.register(with: controller.registrar(forPlugin: "TrayManagerPlugin")) + UniLinksDesktopPlugin.register(with: controller.registrar(forPlugin: "UniLinksDesktopPlugin")) + UrlLauncherPlugin.register(with: controller.registrar(forPlugin: "UrlLauncherPlugin")) + WakelockMacosPlugin.register(with: controller.registrar(forPlugin: "WakelockMacosPlugin")) + WindowSizePlugin.register(with: controller.registrar(forPlugin: "WindowSizePlugin")) + } super.awakeFromNib() } From ce9427c65c21e4bf1d71e515b5b0193dc6e2af31 Mon Sep 17 00:00:00 2001 From: "Miguel F. G" <116861809+flusheDData@users.noreply.github.com> Date: Tue, 22 Nov 2022 17:46:28 +0100 Subject: [PATCH 1012/2015] Update es.rs A small change for consistency (Enable/Habilitar instead of Activate/Activar --- src/lang/es.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/es.rs b/src/lang/es.rs index a59437581..ef7d8e0fc 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -306,7 +306,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Use permanent password", "Usar contraseña permamente"), ("Use both passwords", "Usar ambas contraseñas"), ("Set permanent password", "Establecer contraseña permamente"), - ("Enable Remote Restart", "Activar reinicio remoto"), + ("Enable Remote Restart", "Habilitar reinicio remoto"), ("Allow remote restart", "Permitir reinicio remoto"), ("Restart Remote Device", "Reiniciar dispositivo"), ("Are you sure you want to restart", "Esta Seguro que desea reiniciar?"), From 2211a36e0f38c53f10a8f7737d59ce92dca770a3 Mon Sep 17 00:00:00 2001 From: Agent-JY Date: Tue, 22 Nov 2022 18:50:32 +0100 Subject: [PATCH 1013/2015] Update de.rs Added missing translations --- src/lang/de.rs | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/src/lang/de.rs b/src/lang/de.rs index 5d82f84d1..3ba0ae0cb 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -368,32 +368,32 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Deny LAN Discovery", "LAN-Erkennung verbieten"), ("Write a message", "Nachricht schreiben"), ("Prompt", ""), - ("Please wait for confirmation of UAC...", ""), + ("Please wait for confirmation of UAC...", "Bitte auf die Bestätigung des Nutzers warten..."), ("elevated_foreground_window_tip", ""), - ("Disconnected", ""), + ("Disconnected", "Verbindung abgebrochen"), ("Other", ""), - ("Confirm before closing multiple tabs", ""), - ("Keyboard Settings", ""), - ("Custom", ""), - ("Full Access", ""), - ("Screen Share", ""), + ("Confirm before closing multiple tabs", "Bitte vor dem Schließen mehrerer Tabs bestägigen"), + ("Keyboard Settings", "Tastatureinstellungen"), + ("Custom", "Individuell"), + ("Full Access", "Vollzugriff"), + ("Screen Share", "Bildschirmfreigabe"), ("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 Bildschirm aus, der freigegeben werden soll (auf der Peer-Seite arbeiten)."), - ("Show RustDesk", ""), - ("This PC", ""), - ("or", ""), - ("Continue with", ""), - ("Elevate", ""), - ("Zoom cursor", ""), - ("Accept sessions via password", ""), - ("Accept sessions via click", ""), - ("Accept sessions via both", ""), - ("Please wait for the remote side to accept your session request...", ""), - ("One-time Password", ""), - ("Use one-time password", ""), - ("One-time password length", ""), - ("Request access to your device", ""), + ("Show RustDesk", "RustDesk anzeigen"), + ("This PC", "Dieser PC"), + ("or", "oder"), + ("Continue with", "Fortfahren mit"), + ("Elevate", "Erheben"), + ("Zoom cursor", "Cursor zoomen"), + ("Accept sessions via password", "Sitzung mit Passwort bestätigen"), + ("Accept sessions via click", "Sitzung mit einem Klick bestätigen"), + ("Accept sessions via both", "Sitzung durch beides bestätigen"), + ("Please wait for the remote side to accept your session request...", "Bitte warten Sie auf die Gegenstelle, dass diese Ihre Sitzungsanfrage bestätigt..."), + ("One-time Password", "Einmalpasswort"), + ("Use one-time password", "Einmalpasswort verwenden"), + ("One-time password length", "Länge des Einmalpassworts"), + ("Request access to your device", "Zugriff zu Ihrem Gerät erbitten"), ].iter().cloned().collect(); } From cf721e9bb3009b9ceea1a656173bdbe26b40e228 Mon Sep 17 00:00:00 2001 From: Xerxes-2 Date: Wed, 23 Nov 2022 02:42:36 +1100 Subject: [PATCH 1014/2015] support CIDR for whitelist Signed-off-by: Xerxes-2 --- Cargo.lock | 31 ++++++++++++++++++++++++++ Cargo.toml | 1 + flutter/lib/common/widgets/dialog.dart | 2 +- src/server/connection.rs | 6 ++++- src/ui/index.tis | 2 +- 5 files changed, 39 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9eb270683..09485efa6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -633,6 +633,19 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "cidr-utils" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "355d5b5df67e58b523953d0c1a8d3d2c05f5af51f1332b0199b9c92263614ed0" +dependencies = [ + "debug-helper", + "num-bigint", + "num-traits 0.2.15", + "once_cell", + "regex", +] + [[package]] name = "clang-sys" version = "1.3.3" @@ -1244,6 +1257,12 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7046468a81e6a002061c01e6a7c83139daf91b11c30e66795b13217c2d885c8b" +[[package]] +name = "debug-helper" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f578e8e2c440e7297e008bb5486a3a8a194775224bbc23729b0dbdfaeebf162e" + [[package]] name = "default-net" version = "0.11.0" @@ -3291,6 +3310,17 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "num-bigint" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +dependencies = [ + "autocfg 1.1.0", + "num-integer", + "num-traits 0.2.15", +] + [[package]] name = "num-complex" version = "0.4.2" @@ -4375,6 +4405,7 @@ dependencies = [ "cc", "cfg-if 1.0.0", "chrono", + "cidr-utils", "clap 3.2.17", "clipboard", "cocoa", diff --git a/Cargo.toml b/Cargo.toml index 6375bce26..cfe18f019 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -68,6 +68,7 @@ url = { version = "2.1", features = ["serde"] } reqwest = { version = "0.11", features = ["blocking", "json", "rustls-tls"], default-features=false } chrono = "0.4.23" +cidr-utils = "0.5.9" [target.'cfg(not(any(target_os = "android", target_os = "linux")))'.dependencies] cpal = "0.13.5" diff --git a/flutter/lib/common/widgets/dialog.dart b/flutter/lib/common/widgets/dialog.dart index 35a303c0b..8fab02a77 100644 --- a/flutter/lib/common/widgets/dialog.dart +++ b/flutter/lib/common/widgets/dialog.dart @@ -133,7 +133,7 @@ void changeWhiteList({Function()? callback}) async { final ips = newWhiteListField.trim().split(RegExp(r"[\s,;\n]+")); // test ip - final ipMatch = RegExp(r"^\d+\.\d+\.\d+\.\d+$"); + final ipMatch = RegExp(r"^\d+\.\d+\.\d+\.\d+(\/\d+)?$"); for (final ip in ips) { if (!ipMatch.hasMatch(ip)) { msg = "${translate("Invalid IP")} $ip"; diff --git a/src/server/connection.rs b/src/server/connection.rs index 11960be8a..50c91d057 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -7,6 +7,7 @@ use crate::video_service; #[cfg(any(target_os = "android", target_os = "ios"))] use crate::{common::DEVICE_NAME, flutter::connection_manager::start_channel}; use crate::{ipc, VERSION}; +use cidr_utils::cidr::IpCidr; use hbb_common::{ config::Config, fs, @@ -631,7 +632,10 @@ impl Connection { .is_none() && whitelist .iter() - .filter(|x| x.parse() == Ok(addr.ip())) + .filter(|x| match IpCidr::from_str(x) { + Ok(cidr) => cidr.contains(addr.ip()), + Err(_) => false, + }) .next() .is_none() { diff --git a/src/ui/index.tis b/src/ui/index.tis index 8e2238b2d..6b1d1b7c7 100644 --- a/src/ui/index.tis +++ b/src/ui/index.tis @@ -395,7 +395,7 @@ class MyIdMenu: Reactor.Component { if (value) { var values = value.split(/[\s,;\n]+/g); for (var ip in values) { - if (!ip.match(/^\d+\.\d+\.\d+\.\d+$/)) { + if (!ip.match(/^\d+\.\d+\.\d+\.\d+(\/\d+)?$/)) { return translate("Invalid IP") + ": " + ip; } } From de951ad70ac7ac287864f06cb47e37e2ca85680c Mon Sep 17 00:00:00 2001 From: Xerxes-2 Date: Wed, 23 Nov 2022 04:09:49 +1100 Subject: [PATCH 1015/2015] update IPv4 check and add IPv6 check in whitelist Signed-off-by: Xerxes-2 --- flutter/lib/common/widgets/dialog.dart | 7 +++++-- src/server/connection.rs | 5 +---- src/ui/index.tis | 3 ++- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/flutter/lib/common/widgets/dialog.dart b/flutter/lib/common/widgets/dialog.dart index 8fab02a77..a6de0384f 100644 --- a/flutter/lib/common/widgets/dialog.dart +++ b/flutter/lib/common/widgets/dialog.dart @@ -133,9 +133,12 @@ void changeWhiteList({Function()? callback}) async { final ips = newWhiteListField.trim().split(RegExp(r"[\s,;\n]+")); // test ip - final ipMatch = RegExp(r"^\d+\.\d+\.\d+\.\d+(\/\d+)?$"); + final ipMatch = RegExp( + r"^(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)(\/([1-9]|[1-2][0-9]|3[0-2])){0,1}$"); + final ipv6Match = RegExp( + r"^(((?:[0-9A-Fa-f]{1,4}))*((?::[0-9A-Fa-f]{1,4}))*::((?:[0-9A-Fa-f]{1,4}))*((?::[0-9A-Fa-f]{1,4}))*|((?:[0-9A-Fa-f]{1,4}))((?::[0-9A-Fa-f]{1,4})){7})(\/([1-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])){0,1}$"); for (final ip in ips) { - if (!ipMatch.hasMatch(ip)) { + if (!ipMatch.hasMatch(ip) && !ipv6Match.hasMatch(ip)) { msg = "${translate("Invalid IP")} $ip"; setState(() { isInProgress = false; diff --git a/src/server/connection.rs b/src/server/connection.rs index 50c91d057..a337d6022 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -632,10 +632,7 @@ impl Connection { .is_none() && whitelist .iter() - .filter(|x| match IpCidr::from_str(x) { - Ok(cidr) => cidr.contains(addr.ip()), - Err(_) => false, - }) + .filter(|x| IpCidr::from_str(x).map_or(false, |y| y.contains(addr.ip()))) .next() .is_none() { diff --git a/src/ui/index.tis b/src/ui/index.tis index 6b1d1b7c7..9dcd4f4c4 100644 --- a/src/ui/index.tis +++ b/src/ui/index.tis @@ -395,7 +395,8 @@ class MyIdMenu: Reactor.Component { if (value) { var values = value.split(/[\s,;\n]+/g); for (var ip in values) { - if (!ip.match(/^\d+\.\d+\.\d+\.\d+(\/\d+)?$/)) { + if (!ip.match(/^(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)(\/([1-9]|[1-2][0-9]|3[0-2])){0,1}$/) + && !ip.match(/^(((?:[0-9A-Fa-f]{1,4}))*((?::[0-9A-Fa-f]{1,4}))*::((?:[0-9A-Fa-f]{1,4}))*((?::[0-9A-Fa-f]{1,4}))*|((?:[0-9A-Fa-f]{1,4}))((?::[0-9A-Fa-f]{1,4})){7})(\/([1-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])){0,1}$/)) { return translate("Invalid IP") + ": " + ip; } } From ae7d94632a02ee66dcd36f688c628386c709a195 Mon Sep 17 00:00:00 2001 From: emporiobviver <106774307+emporiobviver@users.noreply.github.com> Date: Tue, 22 Nov 2022 16:52:05 -0300 Subject: [PATCH 1016/2015] Update ptbr.rs --- src/lang/ptbr.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 7d9c0b270..42d28df71 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -385,15 +385,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("This PC", "Este PC"), ("or", "ou"), ("Continue with", "Continuar com"), - ("Elevate", ""), - ("Zoom cursor", ""), - ("Accept sessions via password", ""), - ("Accept sessions via click", ""), - ("Accept sessions via both", ""), - ("Please wait for the remote side to accept your session request...", ""), - ("One-time Password", ""), - ("Use one-time password", ""), - ("One-time password length", ""), - ("Request access to your device", ""), + ("Elevate", "Elevar"), + ("Zoom cursor", "Aumentar cursor"), + ("Accept sessions via password", "Aceitar sessões via senha"), + ("Accept sessions via click", "Aceitar sessões via clique"), + ("Accept sessions via both", "Aceitar sessões de ambos os modos"), + ("Please wait for the remote side to accept your session request...", "Por favor aguarde enquanto o cliente remoto aceita seu pedido de sessão..."), + ("One-time Password", "Senha de uso único"), + ("Use one-time password", "Usar senha de uso único"), + ("One-time password length", "Comprimento da senha de uso único"), + ("Request access to your device", "Solicitar acesso ao seu dispositivo"), ].iter().cloned().collect(); } From 8b4d50f3fb392ceeb549166f57935c8cded33b7c Mon Sep 17 00:00:00 2001 From: 21pages Date: Wed, 23 Nov 2022 09:41:05 +0800 Subject: [PATCH 1017/2015] flutter version allow hide cm Signed-off-by: 21pages --- .../lib/desktop/pages/connection_page.dart | 16 +++-- .../desktop/pages/desktop_setting_page.dart | 69 +++++++++++++------ flutter/lib/main.dart | 26 +++++-- flutter/lib/models/server_model.dart | 39 ++++++++++- libs/hbb_common/src/password_security.rs | 6 ++ src/flutter_ffi.rs | 4 ++ 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/en.rs | 3 +- src/lang/eo.rs | 2 + src/lang/es.rs | 2 + src/lang/fa.rs | 2 + src/lang/fr.rs | 2 + src/lang/hu.rs | 2 + src/lang/id.rs | 2 + src/lang/it.rs | 2 + src/lang/ja.rs | 2 + src/lang/ko.rs | 2 + src/lang/kz.rs | 2 + src/lang/pl.rs | 2 + src/lang/pt_PT.rs | 2 + src/lang/ptbr.rs | 2 + src/lang/ru.rs | 2 + src/lang/sk.rs | 2 + src/lang/template.rs | 2 + src/lang/tr.rs | 2 + src/lang/tw.rs | 2 + src/lang/ua.rs | 2 + src/lang/vn.rs | 2 + src/server/connection.rs | 11 ++- src/server/video_service.rs | 4 +- src/ui_cm_interface.rs | 4 +- src/ui_interface.rs | 9 ++- src/ui_session_interface.rs | 1 + 37 files changed, 198 insertions(+), 44 deletions(-) diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 671335bfd..83e57dba8 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -4,6 +4,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common/widgets/address_book.dart'; import 'package:flutter_hbb/consts.dart'; @@ -177,12 +178,15 @@ class _ConnectionPageState extends State children: [ Row( children: [ - Text( - translate('Control Remote Desktop'), - style: Theme.of(context) - .textTheme - .titleLarge - ?.merge(TextStyle(height: 1)), + Expanded( + child: AutoSizeText( + translate('Control Remote Desktop'), + maxLines: 1, + style: Theme.of(context) + .textTheme + .titleLarge + ?.merge(TextStyle(height: 1)), + ), ), ], ).marginOnly(bottom: 15), diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index b5aeaa7c3..056b1028b 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -658,13 +658,9 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { initialKey: modeInitialKey, onChanged: (key) => model.setApproveMode(key), ).marginOnly(left: _kContentHMargin), - Offstage( - offstage: !usePassword, - child: radios[0], - ), - Offstage( - offstage: !usePassword, - child: _SubLabeledWidget( + if (usePassword) radios[0], + if (usePassword) + _SubLabeledWidget( 'One-time password length', Row( children: [ @@ -672,20 +668,13 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { ], ), enabled: tmpEnabled && !locked), - ), - Offstage( - offstage: !usePassword, - child: radios[1], - ), - Offstage( - offstage: !usePassword, - child: _SubButton('Set permanent password', setPasswordDialog, + if (usePassword) radios[1], + if (usePassword) + _SubButton('Set permanent password', setPasswordDialog, permEnabled && !locked), - ), - Offstage( - offstage: !usePassword, - child: radios[2], - ), + if (usePassword) + hide_cm(!locked).marginOnly(left: _kContentHSubMargin - 6), + if (usePassword) radios[2], ]); }))); } @@ -814,6 +803,46 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { ).marginOnly(left: _kCheckBoxLeftMargin); }); } + + Widget hide_cm(bool enabled) { + return ChangeNotifierProvider.value( + value: gFFI.serverModel, + child: Consumer(builder: (context, model, child) { + final enableHideCm = model.approveMode == 'password' && + model.verificationMethod == kUsePermanentPassword; + onHideCmChanged(bool? b) { + if (b != null) { + bind.mainSetOption( + key: 'allow-hide-cm', value: bool2option('allow-hide-cm', b)); + } + } + + return Tooltip( + message: enableHideCm ? "" : translate('hide_cm_tip'), + child: GestureDetector( + onTap: + enableHideCm ? () => onHideCmChanged(!model.hideCm) : null, + child: Row( + children: [ + Checkbox( + value: model.hideCm, + onChanged: enabled && enableHideCm + ? onHideCmChanged + : null) + .marginOnly(right: 5), + Expanded( + child: Text( + translate('Hide connection management window'), + style: TextStyle( + color: _disabledTextColor( + context, enabled && enableHideCm)), + ), + ), + ], + ), + )); + })); + } } class _Network extends StatefulWidget { diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index e83c4b323..44db1436e 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -85,7 +85,7 @@ Future main(List args) async { debugPrint("--cm started"); desktopType = DesktopType.cm; await windowManager.ensureInitialized(); - runConnectionManagerScreen(); + runConnectionManagerScreen(args.contains('--hide')); } else if (args.contains('--install')) { runInstallPage(); } else { @@ -185,16 +185,23 @@ void runMultiWindow( } } -void runConnectionManagerScreen() async { +void runConnectionManagerScreen(bool hide) async { await initEnv(kAppTypeMain); - // initialize window - WindowOptions windowOptions = - getHiddenTitleBarWindowOptions(size: kConnectionManagerWindowSize); _runApp( '', const DesktopServerPage(), MyTheme.currentThemeMode(), ); + if (hide) { + hideCmWindow(); + } else { + showCmWindow(); + } +} + +void showCmWindow() { + WindowOptions windowOptions = + getHiddenTitleBarWindowOptions(size: kConnectionManagerWindowSize); windowManager.waitUntilReadyToShow(windowOptions, () async { await windowManager.show(); await Future.wait([windowManager.focus(), windowManager.setOpacity(1)]); @@ -204,6 +211,15 @@ void runConnectionManagerScreen() async { }); } +void hideCmWindow() { + WindowOptions windowOptions = + getHiddenTitleBarWindowOptions(size: kConnectionManagerWindowSize); + windowManager.setOpacity(0); + windowManager.waitUntilReadyToShow(windowOptions, () async { + await windowManager.hide(); + }); +} + void _runApp( String title, Widget home, diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index be3f02b5d..456c3cdd2 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:flutter_hbb/main.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:get/get.dart'; import 'package:wakelock/wakelock.dart'; @@ -28,6 +29,7 @@ class ServerModel with ChangeNotifier { bool _audioOk = false; bool _fileOk = false; bool _showElevation = true; + bool _hideCm = false; int _connectStatus = 0; // Rendezvous Server status String _verificationMethod = ""; String _temporaryPasswordLength = ""; @@ -56,6 +58,8 @@ class ServerModel with ChangeNotifier { bool get showElevation => _showElevation; + bool get hideCm => _hideCm; + int get connectStatus => _connectStatus; String get verificationMethod { @@ -74,6 +78,10 @@ class ServerModel with ChangeNotifier { setVerificationMethod(String method) async { await bind.mainSetOption(key: "verification-method", value: method); + if (method != kUsePermanentPassword) { + await bind.mainSetOption( + key: 'allow-hide-cm', value: bool2option('allow-hide-cm', false)); + } } String get temporaryPasswordLength { @@ -90,6 +98,10 @@ class ServerModel with ChangeNotifier { setApproveMode(String mode) async { await bind.mainSetOption(key: 'approve-mode', value: mode); + if (mode != 'password') { + await bind.mainSetOption( + key: 'allow-hide-cm', value: bool2option('allow-hide-cm', false)); + } } TextEditingController get serverId => _serverId; @@ -125,7 +137,11 @@ class ServerModel with ChangeNotifier { } if (!isTest) { - Future.delayed(Duration.zero, timerCallback); + Future.delayed(Duration.zero, () async { + if (await bind.optionSynced()) { + await timerCallback(); + } + }); Timer.periodic(Duration(milliseconds: 500), (timer) async { await timerCallback(); }); @@ -166,6 +182,12 @@ class ServerModel with ChangeNotifier { final temporaryPasswordLength = await bind.mainGetOption(key: "temporary-password-length"); final approveMode = await bind.mainGetOption(key: 'approve-mode'); + var hideCm = option2bool( + 'allow-hide-cm', await bind.mainGetOption(key: 'allow-hide-cm')); + if (!(approveMode == 'password' && + verificationMethod == kUsePermanentPassword)) { + hideCm = false; + } if (_approveMode != approveMode) { _approveMode = approveMode; update = true; @@ -190,6 +212,17 @@ class ServerModel with ChangeNotifier { _temporaryPasswordLength = temporaryPasswordLength; update = true; } + if (_hideCm != hideCm) { + _hideCm = hideCm; + if (desktopType == DesktopType.cm) { + if (hideCm) { + hideCmWindow(); + } else { + showCmWindow(); + } + } + update = true; + } if (update) { notifyListeners(); } @@ -436,11 +469,11 @@ class ServerModel with ChangeNotifier { }, page: desktop.buildConnectionCard(client))); Future.delayed(Duration.zero, () async { - window_on_top(null); + if (!hideCm) window_on_top(null); }); if (client.authorized) { cmHiddenTimer = Timer(const Duration(seconds: 3), () { - windowManager.minimize(); + if (!hideCm) windowManager.minimize(); cmHiddenTimer = null; }); } diff --git a/libs/hbb_common/src/password_security.rs b/libs/hbb_common/src/password_security.rs index adaafebb3..602906990 100644 --- a/libs/hbb_common/src/password_security.rs +++ b/libs/hbb_common/src/password_security.rs @@ -76,6 +76,12 @@ pub fn approve_mode() -> ApproveMode { } } +pub fn hide_cm() -> bool { + approve_mode() == ApproveMode::Password + && verification_method() == VerificationMethod::OnlyUsePermanentPassword + && !Config::get_option("allow-hide-cm").is_empty() +} + const VERSION_LEN: usize = 2; pub fn encrypt_str_or_original(s: &str, version: &str) -> String { diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index ea33290fe..00f9b51e6 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1098,6 +1098,10 @@ pub fn version_to_number(v: String) -> i64 { hbb_common::get_version_number(&v) } +pub fn option_synced() -> bool { + crate::ui_interface::option_synced() +} + pub fn main_is_installed() -> SyncReturn { SyncReturn(is_installed()) } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index f5684b4c4..0dd21e168 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -394,5 +394,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Use one-time password", ""), ("One-time password length", ""), ("Request access to your device", ""), + ("Hide connection management window", ""), + ("hide_cm_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 7256c5d1f..a3b3b47c8 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -395,5 +395,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Use one-time password", "使用一次性密码"), ("One-time password length", "一次性密码长度"), ("Request access to your device", "请求访问你的设备"), + ("Hide connection management window", "隐藏连接管理窗口"), + ("hide_cm_tip", "在只允许密码连接并且只用固定密码的情况下才允许隐藏"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 63fac7288..450f3971a 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -395,5 +395,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Use one-time password", ""), ("One-time password length", ""), ("Request access to your device", ""), + ("Hide connection management window", ""), + ("hide_cm_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 4278cdc20..ea7263ac8 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -395,5 +395,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Use one-time password", ""), ("One-time password length", ""), ("Request access to your device", ""), + ("Hide connection management window", ""), + ("hide_cm_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 3ba0ae0cb..66514fa0f 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -395,5 +395,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Use one-time password", "Einmalpasswort verwenden"), ("One-time password length", "Länge des Einmalpassworts"), ("Request access to your device", "Zugriff zu Ihrem Gerät erbitten"), + ("Hide connection management window", ""), + ("hide_cm_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/en.rs b/src/lang/en.rs index ee68d4431..2550135a1 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -33,7 +33,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("elevated_foreground_window_tip", "The current window of the remote desktop requires higher privilege to operate, so it's unable to use the mouse and keyboard temporarily. You can request the remote user to minimize the current window, or click elevation button on the connection management window. To avoid this problem, it is recommended to install the software on the remote device."), ("JumpLink", "View"), ("Stop service", "Stop Service"), - ("or", ""), - ("Continue with", ""), + ("hide_cm_tip", "Allow hiding only if accepting sessions via password and using pernament password"), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 3c7ac806a..797eb2bb6 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -395,5 +395,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Use one-time password", ""), ("One-time password length", ""), ("Request access to your device", ""), + ("Hide connection management window", ""), + ("hide_cm_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index ef7d8e0fc..ca67a68be 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -395,5 +395,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Use one-time password", "Usar contraseña de un solo uso"), ("One-time password length", "Longitud de la contraseña de un solo uso"), ("Request access to your device", "Solicitud de acceso a su dispositivo"), + ("Hide connection management window", ""), + ("hide_cm_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 0ea1f6f55..4dfb22621 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -394,5 +394,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Use one-time password", "استفاده از رمز عبور یکبار مصرف"), ("One-time password length", "طول رمز عبور یکبار مصرف"), ("Request access to your device", ""), + ("Hide connection management window", ""), + ("hide_cm_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 4b8d0d83e..6c9cb6a3f 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -395,5 +395,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Use one-time password", ""), ("One-time password length", ""), ("Request access to your device", ""), + ("Hide connection management window", ""), + ("hide_cm_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 9802ddb6f..417c83f45 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -395,5 +395,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Use one-time password", ""), ("One-time password length", ""), ("Request access to your device", ""), + ("Hide connection management window", ""), + ("hide_cm_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 002789066..b76bb687d 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -395,5 +395,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Use one-time password", ""), ("One-time password length", ""), ("Request access to your device", ""), + ("Hide connection management window", ""), + ("hide_cm_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 76e034450..83741d47d 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -395,5 +395,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Use one-time password", "Usa password monouso"), ("One-time password length", "Lunghezza password monouso"), ("Request access to your device", "Richiedi l'accesso al tuo dispositivo"), + ("Hide connection management window", ""), + ("hide_cm_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index b032fbe7e..8d806416d 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -395,5 +395,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Use one-time password", ""), ("One-time password length", ""), ("Request access to your device", ""), + ("Hide connection management window", ""), + ("hide_cm_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 338fc7ffe..9f8027be7 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -395,5 +395,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Use one-time password", ""), ("One-time password length", ""), ("Request access to your device", ""), + ("Hide connection management window", ""), + ("hide_cm_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 3a343da21..3a8c27cf3 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -395,5 +395,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Use one-time password", ""), ("One-time password length", ""), ("Request access to your device", ""), + ("Hide connection management window", ""), + ("hide_cm_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index ecc09ce2a..dae77ed88 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -395,5 +395,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Use one-time password", "Użyj hasła jednorazowego"), ("One-time password length", "Długość hasła jednorazowego"), ("Request access to your device", "Żądanie dostępu do Twojego urządzenia"), + ("Hide connection management window", ""), + ("hide_cm_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index dbb5fbbe9..bc5fbbdfd 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -395,5 +395,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Use one-time password", ""), ("One-time password length", ""), ("Request access to your device", ""), + ("Hide connection management window", ""), + ("hide_cm_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 42d28df71..0d77eb905 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -395,5 +395,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Use one-time password", "Usar senha de uso único"), ("One-time password length", "Comprimento da senha de uso único"), ("Request access to your device", "Solicitar acesso ao seu dispositivo"), + ("Hide connection management window", ""), + ("hide_cm_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index ded00af23..e318b7cda 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -395,5 +395,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Use one-time password", ""), ("One-time password length", ""), ("Request access to your device", ""), + ("Hide connection management window", ""), + ("hide_cm_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 8da18e035..33f2be7ab 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -395,5 +395,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Use one-time password", ""), ("One-time password length", ""), ("Request access to your device", ""), + ("Hide connection management window", ""), + ("hide_cm_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index bc9bc95e3..8f855d96a 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -395,5 +395,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Use one-time password", ""), ("One-time password length", ""), ("Request access to your device", ""), + ("Hide connection management window", ""), + ("hide_cm_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 86283557f..a97f832ba 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -395,5 +395,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Use one-time password", ""), ("One-time password length", ""), ("Request access to your device", ""), + ("Hide connection management window", ""), + ("hide_cm_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index cc8f65e1c..4945fd511 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -395,5 +395,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Use one-time password", "使用一次性密碼"), ("One-time password length", "一次性密碼長度"), ("Request access to your device", "請求訪問你的設備"), + ("Hide connection management window", "隱藏連接管理窗口"), + ("hide_cm_tip", "在只允許密碼連接並且只用固定密碼的情況下才允許隱藏"), ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 7d14ee7e0..3861f0598 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -395,5 +395,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Use one-time password", ""), ("One-time password length", ""), ("Request access to your device", ""), + ("Hide connection management window", ""), + ("hide_cm_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 3ddacdf8d..8ddeadfca 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -395,5 +395,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Use one-time password", ""), ("One-time password length", ""), ("Request access to your device", ""), + ("Hide connection management window", ""), + ("hide_cm_tip", ""), ].iter().cloned().collect(); } diff --git a/src/server/connection.rs b/src/server/connection.rs index a337d6022..249dadc5e 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1571,17 +1571,18 @@ async fn start_ipc( if let Ok(s) = crate::ipc::connect(1000, "_cm").await { stream = Some(s); } else { + let extra_args = if password::hide_cm() { "--hide" } else { "" }; let run_done; if crate::platform::is_root() { let mut res = Ok(None); for _ in 0..10 { #[cfg(not(target_os = "linux"))] { - res = crate::platform::run_as_user("--cm"); + res = crate::platform::run_as_user(&format!("--cm {}", extra_args)); } #[cfg(target_os = "linux")] { - res = crate::platform::run_as_user("--cm", None); + res = crate::platform::run_as_user(&format!("--cm {}", extra_args), None); } if res.is_ok() { break; @@ -1596,10 +1597,14 @@ async fn start_ipc( run_done = false; } if !run_done { + let mut args = vec!["--cm"]; + if !extra_args.is_empty() { + args.push(&extra_args); + } super::CHILD_PROCESS .lock() .unwrap() - .push(crate::run_me(vec!["--cm"])?); + .push(crate::run_me(args)?); } for _ in 0..10 { sleep(0.3).await; diff --git a/src/server/video_service.rs b/src/server/video_service.rs index 3ccc3af39..db419fc65 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -192,7 +192,7 @@ fn create_capturer( privacy_mode_id: i32, display: Display, use_yuv: bool, - current: usize, + _current: usize, _portable_service_running: bool, ) -> ResultType> { #[cfg(not(windows))] @@ -256,7 +256,7 @@ fn create_capturer( log::debug!("Create capturer dxgi|gdi"); #[cfg(windows)] return crate::portable_service::client::create_capturer( - current, + _current, display, use_yuv, _portable_service_running, diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index 5edf53507..26e5e4077 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -784,11 +784,11 @@ pub fn can_elevate() -> bool { return false; } -pub fn elevate_portable(id: i32) { +pub fn elevate_portable(_id: i32) { #[cfg(windows)] { let lock = CLIENTS.read().unwrap(); - if let Some(s) = lock.get(&id) { + if let Some(s) = lock.get(&_id) { allow_err!(s.tx.send(ipc::Data::DataPortableService( ipc::DataPortableService::RequestStart ))); diff --git a/src/ui_interface.rs b/src/ui_interface.rs index e1dc3005e..28ce897bc 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -39,6 +39,7 @@ lazy_static::lazy_static! { static ref OPTIONS : Arc>> = Arc::new(Mutex::new(Config::get_options())); static ref ASYNC_JOB_STATUS : Arc> = Default::default(); static ref TEMPORARY_PASSWD : Arc> = Arc::new(Mutex::new("".to_owned())); + pub static ref OPTION_SYNCED : Arc> = Default::default(); } #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -924,7 +925,8 @@ async fn check_connect_status_(reconnect: bool, rx: mpsc::UnboundedReceiver { - *OPTIONS.lock().unwrap() = v + *OPTIONS.lock().unwrap() = v; + *OPTION_SYNCED.lock().unwrap() = true; } Ok(Some(ipc::Data::Config((name, Some(value))))) => { if name == "id" { @@ -967,6 +969,11 @@ async fn check_connect_status_(reconnect: bool, rx: mpsc::UnboundedReceiver bool { + OPTION_SYNCED.lock().unwrap().clone() +} + #[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] #[tokio::main(flavor = "current_thread")] pub(crate) async fn send_to_cm(data: &ipc::Data) { diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 9b00730e5..efc82cbc1 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -200,6 +200,7 @@ impl Session { h265 = h265 && encoding_265; return (h264, h265); } + #[allow(dead_code)] (false, false) } From 7c04855e15fcd5f1ff146ca753f8a18d831367fd Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 23 Nov 2022 14:23:57 +0800 Subject: [PATCH 1018/2015] fix cm hidden timer null Signed-off-by: fufesou --- flutter/lib/desktop/pages/server_page.dart | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index aae6da8fc..6c586994b 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -107,13 +107,14 @@ class ConnectionManagerState extends State { @override Widget build(BuildContext context) { final serverModel = Provider.of(context); - final pointerHandler = serverModel.cmHiddenTimer != null - ? (PointerEvent e) { - serverModel.cmHiddenTimer!.cancel(); - serverModel.cmHiddenTimer = null; - debugPrint("CM hidden timer has been canceled"); - } - : null; + pointerHandler(PointerEvent e) { + if (serverModel.cmHiddenTimer != null) { + serverModel.cmHiddenTimer!.cancel(); + serverModel.cmHiddenTimer = null; + debugPrint("CM hidden timer has been canceled"); + } + } + return serverModel.clients.isEmpty ? Column( children: [ From af65267555eb8006ca987dedc3c3ebcd23d429a0 Mon Sep 17 00:00:00 2001 From: Agent-JY Date: Wed, 23 Nov 2022 10:54:52 +0100 Subject: [PATCH 1019/2015] Update de.rs --- src/lang/de.rs | 46 +++++++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/src/lang/de.rs b/src/lang/de.rs index 66514fa0f..318f2b645 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -3,7 +3,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = [ ("Status", "Status"), ("Your Desktop", "Ihr Desktop"), - ("desk_tip", "Mit dieser ID und diesem Passwort können Sie auf Ihren Desktop zugreifen."), + ("desk_tip", "Mit dieser ID und diesem Passwort kann auf Ihren Desktop zugegriffen werden."), ("Password", "Passwort"), ("Ready", "Bereit"), ("Established", "Verbunden"), @@ -12,7 +12,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Start Service", "Starte Vermittlungsdienst"), ("Service is running", "Vermittlungsdienst aktiv"), ("Service is not running", "Vermittlungsdienst deaktiviert"), - ("not_ready_status", "Nicht bereit. Bitte überprüfen Sie Ihre Verbindung"), + ("not_ready_status", "Nicht bereit. Bitte überprüfen Sie Ihre Netzwerkverbindung"), ("Control Remote Desktop", "Entfernten PC steuern"), ("Transfer File", "Datei übertragen"), ("Connect", "Verbinden"), @@ -30,9 +30,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("IP Whitelisting", "IP-Whitelist"), ("ID/Relay Server", "ID/Vermittlungsserver"), ("Import Server Config", "Serverkonfiguration importieren"), - ("Export Server Config", ""), + ("Export Server Config", "Serverkonfiguration exportieren"), ("Import server configuration successfully", "Serverkonfiguration erfolgreich importiert"), - ("Export server configuration successfully", ""), + ("Export server configuration successfully", "Serverkonfiguration erfolgreich exportiert"), ("Invalid server configuration", "Ungültige Serverkonfiguration"), ("Clipboard is empty", "Zwischenablage ist leer"), ("Stop service", "Vermittlungsdienst deaktivieren"), @@ -67,12 +67,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Connection Error", "Verbindungsfehler"), ("Error", "Fehler"), ("Reset by the peer", "Verbindung wurde von der Gegenstelle zurückgesetzt"), - ("Connecting...", "Verbinden..."), + ("Connecting...", "Verbindung wird hergestellt..."), ("Connection in progress. Please wait.", "Die Verbindung wird hergestellt. Bitte warten..."), ("Please try 1 minute later", "Bitte versuchen Sie es später erneut"), ("Login Error", "Anmeldefehler"), ("Successful", "Erfolgreich"), - ("Connected, waiting for image...", "Verbunden, warte auf Bild..."), + ("Connected, waiting for image...", "Verbindung hergestellt. Warten auf Bild..."), ("Name", "Name"), ("Type", "Typ"), ("Modified", "Geändert"), @@ -89,15 +89,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Delete", "Löschen"), ("Properties", "Eigenschaften"), ("Multi Select", "Mehrfachauswahl"), - ("Select All", ""), - ("Unselect All", ""), + ("Select All", "Alles auswählen"), + ("Unselect All", "Alles abwählen"), ("Empty Directory", "Leerer Ordner"), ("Not an empty directory", "Ordner ist nicht leer"), ("Are you sure you want to delete this file?", "Sind Sie sicher, dass Sie diese Datei löschen wollen?"), ("Are you sure you want to delete this empty directory?", "Sind Sie sicher, dass Sie diesen leeren Ordner löschen möchten?"), ("Are you sure you want to delete the file of this directory?", "Sind Sie sicher, dass Sie die Datei dieses Ordners löschen möchten?"), ("Do this for all conflicts", "Für alle Konflikte merken"), - ("This is irreversible!", "Dies ist irreversibel!"), + ("This is irreversible!", "Dies kann nicht rückgängig gemacht werden!"), ("Deleting", "Löschen"), ("files", "Dateien"), ("Waiting", "Warten"), @@ -113,14 +113,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stretch", "Strecken"), ("Scrollbar", "Scrollleiste"), ("ScrollAuto", "Automatisch scrollen"), - ("Good image quality", "Qualität"), + ("Good image quality", "Hohe Bildqualität"), ("Balanced", "Ausgeglichen"), ("Optimize reaction time", "Geschwindigkeit"), - ("Custom", ""), + ("Custom", "Individuell"), ("Show remote cursor", "Entfernten Cursor anzeigen"), ("Show quality monitor", "Qualitätsüberwachung anzeigen"), ("Disable clipboard", "Zwischenablage deaktivieren"), - ("Lock after session end", "Sperren nach Sitzungsende"), + ("Lock after session end", "Nach Sitzungsende sperren"), ("Insert", "Einfügen"), ("Insert Lock", "Win+L (Sperren) senden"), ("Refresh", "Aktualisieren"), @@ -136,7 +136,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to make direct connection to remote desktop", "Direkte Verbindung zum entfernten PC fehlgeschlagen"), ("Set Password", "Passwort festlegen"), ("OS Password", "Betriebssystem-Passwort"), - ("install_tip", "Aufgrund der UAC kann RustDesk in manchen Fällen nicht ordnungsgemäß funktionieren. Um UAC zu vermeiden, klicken Sie bitte auf die Schaltfläche unten, um RustDesk auf dem System zu installieren"), + ("install_tip", "Aufgrund der Benutzerkontensteuerung (UAC) kann RustDesk in manchen Fällen nicht ordnungsgemäß funktionieren. Um die Benutzerkontensteuerung zu umgehen, klicken Sie bitte auf die Schaltfläche unten, um RustDesk auf dem System zu installieren"), ("Click to upgrade", "Zum Aktualisieren anklicken"), ("Click to download", "Zum Herunterladen klicken"), ("Click to update", "Zum Aktualisieren klicken"), @@ -161,11 +161,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Action", "Aktion"), ("Add", "Hinzufügen"), ("Local Port", "Lokaler Port"), - ("Local Address", "Lokale ddresse"), + ("Local Address", "Lokale Addresse"), ("Change Local Port", "Lokalen Port ändern"), ("setup_server_tip", "Für eine schnellere Verbindung richten Sie bitte Ihren eigenen Verbindungsserver ein"), ("Too short, at least 6 characters.", "Zu kurz, mindestens 6 Zeichen."), - ("The confirmation is not identical.", "Die Passwörter sind nicht identisch."), + ("The confirmation is not identical.", "Die Passwörter stimmen nicht überein."), ("Permissions", "Berechtigungen"), ("Accept", "Akzeptieren"), ("Dismiss", "Ablehnen"), @@ -181,7 +181,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Relayed and unencrypted connection", "Vermittelte und unverschlüsselte Verbindung"), ("Enter Remote ID", "Remote-ID eingeben"), ("Enter your password", "Geben Sie Ihr Passwort ein"), - ("Logging in...", "Anmeldung..."), + ("Logging in...", "Anmelden..."), ("Enable RDP session sharing", "RDP-Sitzungsfreigabe aktivieren"), ("Auto Login", "Automatisch anmelden (nur gültig, wenn Sie \"Sperren nach Sitzungsende\" aktiviert haben)"), ("Enable Direct IP Access", "Direkten IP-Zugang aktivieren"), @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("whitelist_tip", "Nur IPs auf der Whitelist können zugreifen"), ("Login", "Anmelden"), ("Logout", "Abmelden"), - ("Tags", "Stichworte"), + ("Tags", "Schlagworte"), ("Search ID", "Suche ID"), ("Current Wayland display server is not supported", "Der aktuelle Wayland-Anzeigeserver wird nicht unterstützt"), ("whitelist_sep", "Getrennt durch Komma, Semikolon, Leerzeichen oder Zeilenumbruch"), @@ -220,15 +220,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Username missed", "Benutzername vergessen"), ("Password missed", "Passwort vergessen"), ("Wrong credentials", "Falsche Anmeldedaten"), - ("Edit Tag", "Stichwort bearbeiten"), + ("Edit Tag", "Schlagwort bearbeiten"), ("Unremember Password", "Passwort vergessen"), ("Favorites", "Favoriten"), ("Add to Favorites", "Zu Favoriten hinzufügen"), ("Remove from Favorites", "Aus Favoriten entfernen"), - ("Empty", "Leer"), + ("Empty", "Keine Einträge"), ("Invalid folder name", "Ungültiger Ordnername"), ("Socks5 Proxy", "Socks5 Proxy"), - ("Hostname", "Rechnername"), + ("Hostname", "Hostname"), ("Discovered", "Gefunden"), ("install_daemon_tip", "Um mit System zu starten, muss der Systemdienst installiert sein"), ("Remote ID", "Entfernte ID"), @@ -278,7 +278,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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."), - ("Account", "Account"), + ("Account", "Konto"), ("Overwrite", "Überschreiben"), ("This file exists, skip or overwrite this file?", "Diese Datei existiert; überspringen oder überschreiben?"), ("Quit", "Beenden"), @@ -298,7 +298,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Language", "Sprache"), ("Keep RustDesk background service", "RustDesk im Hintergrund ausführen"), ("Ignore Battery Optimizations", "Batterieoptimierung ignorieren"), - ("android_open_battery_optimizations_tip", "Möchten Sie die Batterieopimierungs-Einstellungen öffnen?"), + ("android_open_battery_optimizations_tip", "Möchten Sie die Einstellungen zur Batterieopimierung öffnen?"), ("Connection not allowed", "Verbindung abgelehnt"), ("Legacy mode", "Kompatibilitätsmodus"), ("Map mode", ""), @@ -332,7 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Adaptiv skalieren"), ("General", "Allgemein"), ("Security", "Sicherheit"), - ("Account", "Account"), + ("Account", "KOnto"), ("Theme", "Farbgebung"), ("Dark Theme", "dunkle Farbgebung"), ("Dark", "Dunkel"), From 29978f1a3eab967d3be9a491b54965167402c104 Mon Sep 17 00:00:00 2001 From: Robin Fackler Date: Wed, 23 Nov 2022 15:07:56 +0100 Subject: [PATCH 1020/2015] If an ID contains invalid filename characters, encode it using base64 --- libs/hbb_common/src/config.rs | 38 +++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 492680061..525234e41 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -11,7 +11,9 @@ use std::{ use anyhow::Result; use directories_next::ProjectDirs; use rand::Rng; +use regex::Regex; use serde_derive::{Deserialize, Serialize}; +use sodiumoxide::base64; use sodiumoxide::crypto::sign; use crate::{ @@ -849,7 +851,17 @@ impl PeerConfig { } fn path(id: &str) -> PathBuf { - let path: PathBuf = [PEERS, id.replace(":", "_").as_str()].iter().collect(); + let mut id_encoded: String; + + //If the id contains invalid chars, encode it + let forbidden_paths = Regex::new(r".*[<>:/\\|\?\*].*").unwrap(); + if forbidden_paths.is_match(id) { + id_encoded = + ("base64_".to_string() + base64::encode(id, base64::Variant::Original).as_str()) + } else { + id_encoded = id.to_string(); + } + let path: PathBuf = [PEERS, id_encoded.as_str()].iter().collect(); Config::with_extension(Config::path(path)) } @@ -871,27 +883,23 @@ impl PeerConfig { .file_stem() .map(|p| p.to_str().unwrap_or("")) .unwrap_or("") - .replace("_", ":") .to_owned(); - //rename PeerConfig files if they contain ":" - //to stay backward compatible with *nix - let current_filename = p - .file_name() - .unwrap_or(OsStr::new("")) - .to_str() - .unwrap_or(""); - if current_filename.contains(":") { - if let Some(path) = p.parent() { - fs::rename(p, path.join(current_filename.replace(":", "_"))).ok(); - } + let id_decoded_string: String; + if id.starts_with("base64_") { + let id_decoded = base64::decode(&id[7..], base64::Variant::Original) + .unwrap_or(Vec::new()); + id_decoded_string = + String::from_utf8_lossy(&id_decoded).as_ref().to_owned(); + } else { + id_decoded_string = id; } - let c = PeerConfig::load(&id); + let c = PeerConfig::load(&id_decoded_string); if c.info.platform.is_empty() { fs::remove_file(&p).ok(); } - (id, t, c) + (id_decoded_string, t, c) }) .filter(|p| !p.2.info.platform.is_empty()) .collect(); From f20679f24aa1c04215aaea4e93e77819190bc9ab Mon Sep 17 00:00:00 2001 From: Robin Fackler Date: Wed, 23 Nov 2022 15:29:28 +0100 Subject: [PATCH 1021/2015] Fix crash if id == "base64_" --- libs/hbb_common/src/config.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 8adeae6fe..2f989fd87 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -931,7 +931,7 @@ impl PeerConfig { .to_owned(); let id_decoded_string: String; - if id.starts_with("base64_") { + if id.starts_with("base64_") && id != "base64_" { let id_decoded = base64::decode(&id[7..], base64::Variant::Original) .unwrap_or(Vec::new()); id_decoded_string = From c1af464203a472b439b648199e75a375ff52b194 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 23 Nov 2022 22:34:17 +0800 Subject: [PATCH 1022/2015] minor improve --- libs/hbb_common/src/config.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 2f989fd87..40ae61f90 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -931,7 +931,7 @@ impl PeerConfig { .to_owned(); let id_decoded_string: String; - if id.starts_with("base64_") && id != "base64_" { + if id.starts_with("base64_") && id.len() != 7 { let id_decoded = base64::decode(&id[7..], base64::Variant::Original) .unwrap_or(Vec::new()); id_decoded_string = From 464db9589e5caf2a7412b2a64a848989051b6508 Mon Sep 17 00:00:00 2001 From: jimmyGALLAND <64364019+jimmyGALLAND@users.noreply.github.com> Date: Wed, 23 Nov 2022 23:23:07 +0100 Subject: [PATCH 1023/2015] Update fr.rs --- src/lang/fr.rs | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 6c9cb6a3f..53c3b3bfa 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -304,14 +304,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Map mode", ""), ("Translate mode", ""), ("Use permanent password", "Utiliser un mot de passe permanent"), - ("Use both passwords", "Utiliser les mots de passe temporaire et permanent"), + ("Use both passwords", "Utiliser les mots de passe unique et permanent"), ("Set permanent password", "Définir le mot de passe permanent"), ("Enable Remote Restart", "Activer le redémarrage à distance"), ("Allow remote restart", "Autoriser le redémarrage à distance"), ("Restart Remote Device", "Redémarrer l'appareil à distance"), ("Are you sure you want to restart", "Êtes-vous sûrs de vouloir redémarrer l'appareil ?"), ("Restarting Remote Device", "Redémarrage de l'appareil distant"), - ("remote_restarting_tip", ""), + ("remote_restarting_tip", "L'appareil distant redémarre, veuillez fermer cette boîte de message et vous reconnecter avec un mot de passe permanent après un certain temps"), ("Copied", "Copié"), ("Exit Fullscreen", "Quitter le mode plein écran"), ("Fullscreen", "Plein écran"), @@ -368,8 +368,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Deny LAN Discovery", "Interdir la découverte réseau local"), ("Write a message", "Ecrire un message"), ("Prompt", ""), - ("Please wait for confirmation of UAC...", ""), - ("elevated_foreground_window_tip", ""), + ("Please wait for confirmation of UAC...", "Veuillez attendre la confirmation de l'UAC..."), + ("elevated_foreground_window_tip", "La fenêtre actuelle que la machine distante nécessite des privilèges plus élevés pour fonctionner, elle ne peut donc pas être atteinte par la souris et le clavier. Vous pouvez demander à l'utilisateur distant de réduire la fenêtre actuelle ou de cliquer sur le bouton d'élévation dans la fenêtre de gestion des connexions. Pour éviter ce problème, il est recommandé d'installer le logiciel sur l'appareil distant."), ("Disconnected", "Déconnecté"), ("Other", "Divers"), ("Confirm before closing multiple tabs", "Confirmer avant de fermer plusieurs onglets"), @@ -387,15 +387,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Continue with", "Continuer avec"), ("Elevate", ""), ("Zoom cursor", ""), - ("Accept sessions via password", ""), - ("Accept sessions via click", ""), - ("Accept sessions via both", ""), - ("Please wait for the remote side to accept your session request...", ""), - ("One-time Password", ""), - ("Use one-time password", ""), - ("One-time password length", ""), - ("Request access to your device", ""), - ("Hide connection management window", ""), - ("hide_cm_tip", ""), + ("Accept sessions via password", "Accepter les sessions via mot de passe"), + ("Accept sessions via click", "Accepter les sessions via clique de confirmation"), + ("Accept sessions via both", "Accepter les sessions via mot de passe ou clique de confirmation"), + ("Please wait for the remote side to accept your session request...", "Veuillez attendre que votre demande de session distante soit accepter ..."), + ("One-time Password", "Mot de passe unique"), + ("Use one-time password", "Utiliser un mot de passe unique"), + ("One-time password length", "Longueur du mot de passe unique"), + ("Request access to your device", "Demande d'accès à votre appareil"), + ("Hide connection management window", "Masquer la fenêtre de gestion des connexions"), + ("hide_cm_tip", "Autoriser le masquage uniquement si vous acceptez des sessions via un mot de passe et utilisez un mot de passe permanent"), ].iter().cloned().collect(); } From 02c1bc6080fd5e8de83f6a74c9d4a76d6aff5281 Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 24 Nov 2022 11:19:16 +0800 Subject: [PATCH 1024/2015] hide zoom-cursor if view-style is original Signed-off-by: fufesou --- flutter/lib/consts.dart | 24 ++++++ .../lib/desktop/pages/remote_tab_page.dart | 7 +- .../lib/desktop/widgets/remote_menubar.dart | 81 +++++++++++-------- flutter/lib/mobile/pages/remote_page.dart | 25 +++--- flutter/lib/models/model.dart | 2 +- libs/hbb_common/src/config.rs | 5 +- 6 files changed, 92 insertions(+), 52 deletions(-) diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index acba32780..4d3cd8cd9 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -70,6 +70,30 @@ const kMouseControlDistance = 12; /// [kMouseControlTimeoutMSec] indicates the timeout (in milliseconds) that self-side can get control of mouse. const kMouseControlTimeoutMSec = 1000; +/// [kRemoteViewStyleOriginal] Show remote image without scaling. +const kRemoteViewStyleOriginal = 'original'; + +/// [kRemoteViewStyleAdaptive] Show remote image scaling by ratio factor. +const kRemoteViewStyleAdaptive = 'adaptive'; + +/// [kRemoteScrollStyleAuto] Scroll image auto by position. +const kRemoteScrollStyleAuto = 'scrollauto'; + +/// [kRemoteScrollStyleBar] Scroll image with scroll bar. +const kRemoteScrollStyleBar = 'scrollbar'; + +/// [kRemoteImageQualityBest] Best image quality. +const kRemoteImageQualityBest = 'best'; + +/// [kRemoteImageQualityBalanced] Balanced image quality, mid performance. +const kRemoteImageQualityBalanced = 'balanced'; + +/// [kRemoteImageQualityLow] Low image quality, better performance. +const kRemoteImageQualityLow = 'low'; + +/// [kRemoteImageQualityCustom] Custom image quality. +const kRemoteImageQualityCustom = 'custom'; + /// flutter/packages/flutter/lib/src/services/keyboard_key.dart -> _keyLabels /// see [LogicalKeyboardKey.keyLabel] const Map logicalKeyMap = { diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index 1e942272c..85df25477 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -236,12 +236,12 @@ class _ConnectionTabPageState extends State { optionsGetter: () => [ MenuEntryRadioOption( text: translate('Scale original'), - value: 'original', + value: kRemoteViewStyleOriginal, dismissOnClicked: true, ), MenuEntryRadioOption( text: translate('Scale adaptive'), - value: 'adaptive', + value: kRemoteViewStyleAdaptive, dismissOnClicked: true, ), ], @@ -249,8 +249,7 @@ class _ConnectionTabPageState extends State { // null means peer id is not found, which there's no need to care about await bind.sessionGetViewStyle(id: key) ?? '', optionSetter: (String oldValue, String newValue) async { - await bind.sessionSetViewStyle( - id: key, value: newValue); + await bind.sessionSetViewStyle(id: key, value: newValue); ffi.canvasModel.updateViewStyle(); cancelFunc(); }, diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index ed69f3e65..656dc8546 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -7,6 +7,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hbb/models/chat_model.dart'; import 'package:flutter_hbb/models/state_model.dart'; +import 'package:flutter_hbb/consts.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:rxdart/rxdart.dart' as rxdart; @@ -25,6 +26,7 @@ class MenubarState { final kStoreKey = 'remoteMenubarState'; late RxBool show; late RxBool _pin; + RxString viewStyle = RxString(kRemoteViewStyleOriginal); MenubarState() { final s = bind.getLocalFlutterConfig(k: kStoreKey); @@ -67,21 +69,25 @@ class MenubarState { switchPin() async { _pin.value = !_pin.value; // Save everytime changed, as this func will not be called frequently - await save(); + await _savePin(); } setPin(bool v) async { if (_pin.value != v) { _pin.value = v; // Save everytime changed, as this func will not be called frequently - await save(); + await _savePin(); } } - save() async { + _savePin() async { bind.setLocalFlutterConfig( k: kStoreKey, v: jsonEncode({'pin': _pin.value})); } + + save() async { + await _savePin(); + } } class _MenubarTheme { @@ -404,6 +410,8 @@ class _RemoteMenubarState extends State { Widget _buildDisplay(BuildContext context) { return FutureBuilder(future: () async { + widget.state.viewStyle.value = + await bind.sessionGetViewStyle(id: widget.id) ?? ''; final supportedHwcodec = await bind.sessionSupportedHwcodec(id: widget.id); return {'supportedHwcodec': supportedHwcodec}; @@ -719,20 +727,24 @@ class _RemoteMenubarState extends State { optionsGetter: () => [ MenuEntryRadioOption( text: translate('Scale original'), - value: 'original', + value: kRemoteViewStyleOriginal, dismissOnClicked: true, ), MenuEntryRadioOption( text: translate('Scale adaptive'), - value: 'adaptive', + value: kRemoteViewStyleAdaptive, dismissOnClicked: true, ), ], - curOptionGetter: () async => - // null means peer id is not found, which there's no need to care about - await bind.sessionGetViewStyle(id: widget.id) ?? '', + curOptionGetter: () async { + // null means peer id is not found, which there's no need to care about + final viewStyle = await bind.sessionGetViewStyle(id: widget.id) ?? ''; + widget.state.viewStyle.value = viewStyle; + return viewStyle; + }, optionSetter: (String oldValue, String newValue) async { await bind.sessionSetViewStyle(id: widget.id, value: newValue); + widget.state.viewStyle.value = newValue; widget.ffi.canvasModel.updateViewStyle(); }, padding: padding, @@ -744,12 +756,12 @@ class _RemoteMenubarState extends State { optionsGetter: () => [ MenuEntryRadioOption( text: translate('ScrollAuto'), - value: 'scrollauto', + value: kRemoteScrollStyleAuto, dismissOnClicked: true, ), MenuEntryRadioOption( text: translate('Scrollbar'), - value: 'scrollbar', + value: kRemoteScrollStyleBar, dismissOnClicked: true, ), ], @@ -769,22 +781,22 @@ class _RemoteMenubarState extends State { optionsGetter: () => [ MenuEntryRadioOption( text: translate('Good image quality'), - value: 'best', + value: kRemoteImageQualityBest, dismissOnClicked: true, ), MenuEntryRadioOption( text: translate('Balanced'), - value: 'balanced', + value: kRemoteImageQualityBalanced, dismissOnClicked: true, ), MenuEntryRadioOption( text: translate('Optimize reaction time'), - value: 'low', + value: kRemoteImageQualityLow, dismissOnClicked: true, ), MenuEntryRadioOption( text: translate('Custom'), - value: 'custom', + value: kRemoteImageQualityCustom, dismissOnClicked: true), ], curOptionGetter: () async => @@ -821,7 +833,7 @@ class _RemoteMenubarState extends State { } } - if (newValue == 'custom') { + if (newValue == kRemoteImageQualityCustom) { final btnClose = msgBoxButton(translate('Close'), () async { await setCustomValues(); widget.ffi.dialogManager.dismissAll(); @@ -1089,24 +1101,26 @@ class _RemoteMenubarState extends State { ); }()); - /// Show remote cursor - displayMenu.add(() { - final opt = 'zoom-cursor'; - final state = PeerBoolOption.find(widget.id, opt); - return MenuEntrySwitch2( - switchType: SwitchType.scheckbox, - text: translate('Zoom cursor'), - getter: () { - return state; - }, - setter: (bool v) async { - state.value = v; - await bind.sessionToggleOption(id: widget.id, value: opt); - }, - padding: padding, - dismissOnClicked: true, - ); - }()); + /// Show remote cursor scaling with image + if (widget.state.viewStyle.value != kRemoteViewStyleOriginal) { + displayMenu.add(() { + final opt = 'zoom-cursor'; + final state = PeerBoolOption.find(widget.id, opt); + return MenuEntrySwitch2( + switchType: SwitchType.scheckbox, + text: translate('Zoom cursor'), + getter: () { + return state; + }, + setter: (bool v) async { + state.value = v; + await bind.sessionToggleOption(id: widget.id, value: opt); + }, + padding: padding, + dismissOnClicked: true, + ); + }()); + } /// Show quality monitor displayMenu.add(MenuEntrySwitch( @@ -1179,7 +1193,6 @@ class _RemoteMenubarState extends State { optionSetter: (String oldValue, String newValue) async { await bind.sessionSetKeyboardMode( id: widget.id, keyboardMode: newValue); - widget.ffi.canvasModel.updateViewStyle(); }, ) ]; diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index b17f0ef54..e6ebfcb73 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -4,6 +4,7 @@ import 'dart:ui' as ui; import 'package:flutter/material.dart'; 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:get/get_state_manager/src/rx_flutter/rx_obx_widget.dart'; @@ -642,7 +643,7 @@ class _RemotePageState extends State { // FIXME: // null means no session of id // empty string means no password - var password = await bind.sessionGetOption(id: id, arg: "os-password"); + var password = await bind.sessionGetOption(id: id, arg: 'os-password'); if (password != null) { bind.sessionInputOsPassword(id: widget.id, value: password); } else { @@ -908,13 +909,13 @@ class ImagePainter extends CustomPainter { void showOptions( BuildContext context, String id, OverlayDialogManager dialogManager) async { - String quality = await bind.sessionGetImageQuality(id: id) ?? 'balanced'; - if (quality == '') quality = 'balanced'; + String quality = + await bind.sessionGetImageQuality(id: id) ?? kRemoteImageQualityBalanced; + if (quality == '') quality = kRemoteImageQualityBalanced; String codec = await bind.sessionGetOption(id: id, arg: 'codec-preference') ?? 'auto'; if (codec == '') codec = 'auto'; - String viewStyle = - await bind.sessionGetOption(id: id, arg: 'view-style') ?? ''; + String viewStyle = await bind.sessionGetViewStyle(id: id) ?? ''; var displays = []; final pi = gFFI.ffiModel.pi; @@ -1017,12 +1018,16 @@ void showOptions( } final radios = [ - getRadio('Scale original', 'original', viewStyle, setViewStyle), - getRadio('Scale adaptive', 'adaptive', viewStyle, setViewStyle), + getRadio( + 'Scale original', kRemoteViewStyleOriginal, viewStyle, setViewStyle), + getRadio( + 'Scale adaptive', kRemoteViewStyleAdaptive, viewStyle, setViewStyle), const Divider(color: MyTheme.border), - getRadio('Good image quality', 'best', quality, setQuality), - getRadio('Balanced', 'balanced', quality, setQuality), - getRadio('Optimize reaction time', 'low', quality, setQuality), + getRadio( + 'Good image quality', kRemoteImageQualityBest, quality, setQuality), + getRadio('Balanced', kRemoteImageQualityBalanced, quality, setQuality), + getRadio('Optimize reaction time', kRemoteImageQualityLow, quality, + setQuality), const Divider(color: MyTheme.border) ]; diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index b7d72f878..a7a51b9a0 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -565,7 +565,7 @@ class CanvasModel with ChangeNotifier { updateScrollStyle() async { final style = await bind.sessionGetScrollStyle(id: id); - if (style == 'scrollbar') { + if (style == kRemoteScrollStyleBar) { _scrollStyle = ScrollStyle.scrollbar; _scrollX = 0.0; _scrollY = 0.0; diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 40ae61f90..f476816ef 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -1,6 +1,5 @@ use std::{ collections::HashMap, - ffi::{OsStr, OsString}, fs, net::{IpAddr, Ipv4Addr, SocketAddr}, path::{Path, PathBuf}, @@ -896,13 +895,13 @@ impl PeerConfig { } fn path(id: &str) -> PathBuf { - let mut id_encoded: String; + let id_encoded: String; //If the id contains invalid chars, encode it let forbidden_paths = Regex::new(r".*[<>:/\\|\?\*].*").unwrap(); if forbidden_paths.is_match(id) { id_encoded = - ("base64_".to_string() + base64::encode(id, base64::Variant::Original).as_str()) + "base64_".to_string() + base64::encode(id, base64::Variant::Original).as_str(); } else { id_encoded = id.to_string(); } From 9b1c5cce9ece4890cc451f53b9f09056b0004b36 Mon Sep 17 00:00:00 2001 From: Agent-JY Date: Thu, 24 Nov 2022 14:34:35 +0100 Subject: [PATCH 1025/2015] Update de.rs --- src/lang/de.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/lang/de.rs b/src/lang/de.rs index 318f2b645..61a25c25e 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -301,7 +301,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_open_battery_optimizations_tip", "Möchten Sie die Einstellungen zur Batterieopimierung öffnen?"), ("Connection not allowed", "Verbindung abgelehnt"), ("Legacy mode", "Kompatibilitätsmodus"), - ("Map mode", ""), + ("Map mode", ""), //Muss noch angepasst werden ("Translate mode", "Übersetzungsmodus"), ("Use permanent password", "Dauerhaftes Passwort verwenden"), ("Use both passwords", "Beide Passwörter verwenden"), @@ -367,11 +367,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable LAN Discovery", "LAN-Erkennung aktivieren"), ("Deny LAN Discovery", "LAN-Erkennung verbieten"), ("Write a message", "Nachricht schreiben"), - ("Prompt", ""), + ("Prompt", ""), //Aufforderung? ("Please wait for confirmation of UAC...", "Bitte auf die Bestätigung des Nutzers warten..."), ("elevated_foreground_window_tip", ""), ("Disconnected", "Verbindung abgebrochen"), - ("Other", ""), + ("Other", ""), //Muss noch angepasst werden ("Confirm before closing multiple tabs", "Bitte vor dem Schließen mehrerer Tabs bestägigen"), ("Keyboard Settings", "Tastatureinstellungen"), ("Custom", "Individuell"), @@ -395,7 +395,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Use one-time password", "Einmalpasswort verwenden"), ("One-time password length", "Länge des Einmalpassworts"), ("Request access to your device", "Zugriff zu Ihrem Gerät erbitten"), - ("Hide connection management window", ""), - ("hide_cm_tip", ""), + ("Hide connection management window", "Fenster zur Verwaltung der Verbindung verstecken"), + ("hide_cm_tip", "Verstecken nur erlauben, wenn die Sitzung über ein festes Passwort erstellt wurde"), // Sehr unklar. Muss noch angepasst werden. Original: Allow hiding only if accepting sessions via password and using pernament password ].iter().cloned().collect(); } From f17b439a3430c2cb265e85dfcf2ac7c943e03453 Mon Sep 17 00:00:00 2001 From: neoGalaxy88 Date: Thu, 24 Nov 2022 16:54:49 +0100 Subject: [PATCH 1026/2015] 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 83741d47d..ef77e18e2 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -395,7 +395,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Use one-time password", "Usa password monouso"), ("One-time password length", "Lunghezza password monouso"), ("Request access to your device", "Richiedi l'accesso al tuo dispositivo"), - ("Hide connection management window", ""), + ("Hide connection management window", "Nascondi la finestra di gestione delle connessioni"), ("hide_cm_tip", ""), ].iter().cloned().collect(); } From 8f9b837f787f633aa6aa5e1eb8a32717a96da5d9 Mon Sep 17 00:00:00 2001 From: Agent-JY Date: Thu, 24 Nov 2022 19:25:42 +0100 Subject: [PATCH 1027/2015] Update de.rs --- src/lang/de.rs | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/lang/de.rs b/src/lang/de.rs index 61a25c25e..5b0a86b37 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -21,7 +21,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Confirmation", "Bestätigung"), ("TCP Tunneling", "TCP Tunneln"), ("Remove", "Entfernen"), - ("Refresh random password", "Neues zufälliges Passwort"), + ("Refresh random password", "Zufälliges Passwort erzeugen"), ("Set your own password", "Eigenes Passwort setzen"), ("Enable Keyboard/Mouse", "Tastatur/Maus aktivieren"), ("Enable Clipboard", "Zwischenablage aktivieren"), @@ -43,7 +43,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Audio Input", "Audioeingang"), ("Enhancements", "Verbesserungen"), ("Hardware Codec", "Hardware-Codec"), - ("Adaptive Bitrate", "Adaptive Bitrate"), + ("Adaptive Bitrate", "Bitrate automatisch anpassen"), ("ID Server", "ID-Server"), ("Relay Server", "Vermittlungsserver"), ("API Server", "API-Server"), @@ -60,7 +60,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Retry", "Erneut versuchen"), ("OK", "OK"), ("Password Required", "Passwort erforderlich"), - ("Please enter your password", "Bitte geben Sie das Passwort ein"), + ("Please enter your password", "Bitte geben Sie das Passwort der Gegenstelle ein"), ("Remember password", "Passwort merken"), ("Wrong Password", "Falsches Passwort"), ("Do you want to enter again?", "Erneut verbinden?"), @@ -152,8 +152,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("agreement_tip", "Durch die Installation akzeptieren Sie die Lizenzvereinbarung"), ("Accept and Install", "Akzeptieren und Installieren"), ("End-user license agreement", "Lizenzvereinbarung für Endbenutzer"), - ("Generating ...", "Generiere..."), - ("Your installation is lower version.", "Ihre Installation ist älter."), + ("Generating ...", "Wird generiert..."), + ("Your installation is lower version.", "Ihre Version ist veraltet."), ("not_close_tcp_tip", "Schließen Sie dieses Fenster nicht, solange Sie den Tunnel benutzen."), ("Listening ...", "Lausche..."), ("Remote Host", "Entfernter PC"), @@ -163,7 +163,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Local Port", "Lokaler Port"), ("Local Address", "Lokale Addresse"), ("Change Local Port", "Lokalen Port ändern"), - ("setup_server_tip", "Für eine schnellere Verbindung richten Sie bitte Ihren eigenen Verbindungsserver ein"), + ("setup_server_tip", "für eine schnellere Verbindung richten Sie bitte Ihren eigenen Verbindungsserver ein."), ("Too short, at least 6 characters.", "Zu kurz, mindestens 6 Zeichen."), ("The confirmation is not identical.", "Die Passwörter stimmen nicht überein."), ("Permissions", "Berechtigungen"), @@ -229,7 +229,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Invalid folder name", "Ungültiger Ordnername"), ("Socks5 Proxy", "Socks5 Proxy"), ("Hostname", "Hostname"), - ("Discovered", "Gefunden"), + ("Discovered", "Automatisch gefunden"), ("install_daemon_tip", "Um mit System zu starten, muss der Systemdienst installiert sein"), ("Remote ID", "Entfernte ID"), ("Paste", "Einfügen"), @@ -253,7 +253,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Pinch to Zoom", "2-Finger-Zoom"), ("Canvas Zoom", "Sichtfeld-Zoom"), ("Reset canvas", "Sichtfeld zurücksetzen"), - ("No permission of file transfer", "Keine Dateizugriff-Berechtigung"), + ("No permission of file transfer", "Keine Berechtigung für den Dateizugriff"), ("Note", "Anmerkung"), ("Connection", "Verbindung"), ("Share Screen", "Bildschirm freigeben"), @@ -303,15 +303,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Legacy mode", "Kompatibilitätsmodus"), ("Map mode", ""), //Muss noch angepasst werden ("Translate mode", "Übersetzungsmodus"), - ("Use permanent password", "Dauerhaftes Passwort verwenden"), + ("Use permanent password", "Permanentes Passwort verwenden"), ("Use both passwords", "Beide Passwörter verwenden"), - ("Set permanent password", "Dauerhaftes Passwort setzen"), + ("Set permanent password", "Permanentes Passwort setzen"), ("Enable Remote Restart", "Entfernten Neustart aktivieren"), ("Allow remote restart", "Entfernten Neustart erlauben"), ("Restart Remote Device", "Entferntes Gerät neu starten"), ("Are you sure you want to restart", "Möchten Sie das entfernte Gerät wirklich neu starten?"), ("Restarting Remote Device", "Entferntes Gerät wird neu gestartet"), - ("remote_restarting_tip", "Entferntes Gerät startet neu, bitte schließen Sie diese Meldung und verbinden Sie sich mit dem dauerhaften Passwort erneut."), + ("remote_restarting_tip", "Entferntes Gerät startet neu, bitte schließen Sie diese Meldung und verbinden Sie sich mit dem permanenten Passwort erneut."), ("Copied", "Kopiert"), ("Exit Fullscreen", "Vollbild beenden"), ("Fullscreen", "Ganzer Bildschirm"), @@ -337,7 +337,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Dark Theme", "dunkle Farbgebung"), ("Dark", "Dunkel"), ("Light", "Hell"), - ("Follow System", ""), + ("Follow System", "System-Standard"), ("Enable hardware codec", "Hardware-Codec aktivieren"), ("Unlock Security Settings", "Sicherheitseinstellungen entsperren"), ("Enable Audio", "Audio aktivieren"), @@ -346,9 +346,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Direct IP Access", "Direkter IP-Zugriff"), ("Proxy", "Proxy"), ("Port", "Port"), - ("Apply", "Übernehmen"), + ("Apply", "Anwenden"), ("Disconnect all devices?", "Alle Geräte trennen?"), - ("Clear", "Leeren"), + ("Clear", "Zurücksetzen"), ("Audio Input Device", "Audioeingabegerät"), ("Deny remote access", "Fernzugriff verbieten"), ("Use IP Whitelisting", "IP-Whitelist benutzen"), @@ -371,8 +371,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please wait for confirmation of UAC...", "Bitte auf die Bestätigung des Nutzers warten..."), ("elevated_foreground_window_tip", ""), ("Disconnected", "Verbindung abgebrochen"), - ("Other", ""), //Muss noch angepasst werden - ("Confirm before closing multiple tabs", "Bitte vor dem Schließen mehrerer Tabs bestägigen"), + ("Other", "Weitere Einstellungen"), + ("Confirm before closing multiple tabs", "Nachfragen, wenn mehrere Tabs geschlossen werden"), ("Keyboard Settings", "Tastatureinstellungen"), ("Custom", "Individuell"), ("Full Access", "Vollzugriff"), @@ -389,7 +389,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Zoom cursor", "Cursor zoomen"), ("Accept sessions via password", "Sitzung mit Passwort bestätigen"), ("Accept sessions via click", "Sitzung mit einem Klick bestätigen"), - ("Accept sessions via both", "Sitzung durch beides bestätigen"), + ("Accept sessions via both", "Sitzung durch Klick und Passwort bestätigen"), ("Please wait for the remote side to accept your session request...", "Bitte warten Sie auf die Gegenstelle, dass diese Ihre Sitzungsanfrage bestätigt..."), ("One-time Password", "Einmalpasswort"), ("Use one-time password", "Einmalpasswort verwenden"), From 04b3c09d2ca5727ee0a6c3e2bd8650568e2b1125 Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Thu, 24 Nov 2022 21:07:42 +0100 Subject: [PATCH 1028/2015] Updated it.rs strings en.rs typo --- src/lang/en.rs | 2 +- src/lang/it.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/en.rs b/src/lang/en.rs index 2550135a1..a661c17bc 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -33,6 +33,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("elevated_foreground_window_tip", "The current window of the remote desktop requires higher privilege to operate, so it's unable to use the mouse and keyboard temporarily. You can request the remote user to minimize the current window, or click elevation button on the connection management window. To avoid this problem, it is recommended to install the software on the remote device."), ("JumpLink", "View"), ("Stop service", "Stop Service"), - ("hide_cm_tip", "Allow hiding only if accepting sessions via password and using pernament password"), + ("hide_cm_tip", "Allow hiding only if accepting sessions via password and using permanent password"), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index ef77e18e2..523386eb5 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -396,6 +396,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("One-time password length", "Lunghezza password monouso"), ("Request access to your device", "Richiedi l'accesso al tuo dispositivo"), ("Hide connection management window", "Nascondi la finestra di gestione delle connessioni"), - ("hide_cm_tip", ""), + ("hide_cm_tip", "Permetti di nascondere solo se si accettano sessioni con password permanente"), ].iter().cloned().collect(); } From 1f19f76842de4064b9a9f238764d9659c5436cc6 Mon Sep 17 00:00:00 2001 From: "Miguel F. G" <116861809+flusheDData@users.noreply.github.com> Date: Fri, 25 Nov 2022 03:25:44 +0100 Subject: [PATCH 1029/2015] Update es.rs Newest items translated --- src/lang/es.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/es.rs b/src/lang/es.rs index ca67a68be..514d21480 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -395,7 +395,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Use one-time password", "Usar contraseña de un solo uso"), ("One-time password length", "Longitud de la contraseña de un solo uso"), ("Request access to your device", "Solicitud de acceso a su dispositivo"), - ("Hide connection management window", ""), - ("hide_cm_tip", ""), + ("Hide connection management window", "Ocultar ventana de gestión de conexión"), + ("hide_cm_tip", "Permitir ocultar solo si se aceptan sesiones a través de contraseña y usando contraseña permanente"), ].iter().cloned().collect(); } From 5c1281029437dd16717112fc29952dfa2fe22838 Mon Sep 17 00:00:00 2001 From: Manos Date: Fri, 25 Nov 2022 11:20:06 +0200 Subject: [PATCH 1030/2015] Add Greek translation --- src/lang.rs | 3 + src/lang/gr.rs | 401 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 404 insertions(+) create mode 100644 src/lang/gr.rs diff --git a/src/lang.rs b/src/lang.rs index 30ce2d372..790e35287 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -25,6 +25,7 @@ mod kz; mod ua; mod fa; mod ca; +mod gr; lazy_static::lazy_static! { pub static ref LANGS: Value = @@ -53,6 +54,7 @@ lazy_static::lazy_static! { ("ua", "Українська"), ("fa", "فارسی"), ("ca", "Català"), + ("gr", "Ελληνικά"), ]); } @@ -105,6 +107,7 @@ pub fn translate_locale(name: String, locale: &str) -> String { "ua" => ua::T.deref(), "fa" => fa::T.deref(), "ca" => ca::T.deref(), + "gr" => gr::T.deref(), _ => en::T.deref(), }; if let Some(v) = m.get(&name as &str) { diff --git a/src/lang/gr.rs b/src/lang/gr.rs new file mode 100644 index 000000000..e17311e17 --- /dev/null +++ b/src/lang/gr.rs @@ -0,0 +1,401 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "Κατάσταση"), + ("Your Desktop", "Ο σταθμός εργασίας σας"), + ("desk_tip", "Η πρόσβαση στον σταθμό εργασίας σας είναι δυνατή με αυτό το αναγνωριστικό και τον κωδικό πρόσβασης."), + ("Password", "Κωδικός πρόσβασης"), + ("Ready", "Έτοιμο"), + ("Established", "Συνδέθηκε"), + ("connecting_status", "Σύνδεση στο δίκτυο RustDesk..."), + ("Enable Service", "Ενεργοποίηση υπηρεσίας"), + ("Start Service", "Έναρξη υπηρεσίας"), + ("Service is running", "Η υπηρεσία εκτελείται"), + ("Service is not running", "Η υπηρεσία δεν εκτελείται"), + ("not_ready_status", "Δεν είναι έτοιμο. Ελέγξτε τη σύνδεσή σας"), + ("Control Remote Desktop", "Έλεγχος απομακρυσμένου σταθμού εργασίας"), + ("Transfer File", "Μεταφορά αρχείου"), + ("Connect", "Σύνδεση"), + ("Recent Sessions", "Πρόσφατες συνεδρίες"), + ("Address Book", "Βιβλίο διευθύνσεων"), + ("Confirmation", "Επιβεβαίωση"), + ("TCP Tunneling", "TCP Tunneling"), + ("Remove", "Κατάργηση"), + ("Refresh random password", "Νέος τυχαίος κωδικός πρόσβασης"), + ("Set your own password", "Ορίστε τον δικό σας κωδικό πρόσβασης"), + ("Enable Keyboard/Mouse", "Ενεργοποίηση πληκτρολογίου/ποντικιού"), + ("Enable Clipboard", "Ενεργοποίηση Προχείρου"), + ("Enable File Transfer", "Ενεργοποίηση μεταφοράς αρχείων"), + ("Enable TCP Tunneling", "Ενεργοποίηση TCP Tunneling"), + ("IP Whitelisting", "Λίστα επιτρεπόμενων IP"), + ("ID/Relay Server", "Διακομιστής ID/Αναμετάδοσης"), + ("Import Server Config", "Εισαγωγή διαμόρφωσης διακομιστή"), + ("Export Server Config", "Εξαγωγή διαμόρφωσης διακομιστή"), + ("Import server configuration successfully", "Επιτυχής εισαγωγή διαμόρφωσης διακομιστή"), + ("Export server configuration successfully", "Επιτυχής εξαγωγή διαμόρφωσης διακομιστή"), + ("Invalid server configuration", "Μη έγκυρη διαμόρφωση διακομιστή"), + ("Clipboard is empty", "Το πρόχειρο είναι κενό"), + ("Stop service", "Διακοπή υπηρεσίας"), + ("Change ID", "Αλλαγή αναγνωριστικού ID"), + ("Website", "Ιστότοπος"), + ("About", "Πληροφορίες για το"), + ("Mute", "Σίγαση"), + ("Audio Input", "Είσοδος ήχου"), + ("Enhancements", "Βελτιώσεις"), + ("Hardware Codec", "Κωδικοποιητής υλικού"), + ("Adaptive Bitrate", "Adaptive Bitrate"), + ("ID Server", "Διακομιστής ID"), + ("Relay Server", "Διακομιστής αναμετάδοσης"), + ("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", "Μη διαθέσιμος"), + ("Too frequent", "Πολύ συχνά"), + ("Cancel", "Ακύρωση"), + ("Skip", "Παράλειψη"), + ("Close", "Κλείσιμο"), + ("Retry", "Δοκίμασε ξανά"), + ("OK", "Εντάξει"), + ("Password Required", "Απαιτείται κωδικός πρόσβασης"), + ("Please enter your password", "Παρακαλώ εισάγετε τον κωδικό πρόσβασης"), + ("Remember password", "Απομνημόνευση κωδικού πρόσβασης"), + ("Wrong Password", "Λάθος κωδικός πρόσβασης"), + ("Do you want to enter again?", "Επανασύνδεση;"), + ("Connection Error", "Σφάλμα σύνδεσης"), + ("Error", "Σφάλμα"), + ("Reset by the peer", "Η σύνδεση επαναφέρθηκε από τον απομακρυσμένο σταθμό"), + ("Connecting...", "Σύνδεση..."), + ("Connection in progress. Please wait.", "Σύνδεση σε εξέλιξη. Παρακαλώ περιμένετε."), + ("Please try 1 minute later", "Παρακαλώ ξαναδοκιμάστε σε 1 λεπτό"), + ("Login Error", "Σφάλμα εισόδου"), + ("Successful", "Επιτυχής"), + ("Connected, waiting for image...", "Συνδέθηκε, αναμονή για εικόνα..."), + ("Name", "Όνομα"), + ("Type", "Τύπος"), + ("Modified", "Τροποποιήθηκε"), + ("Size", "Μέγεθος"), + ("Show Hidden Files", "Εμφάνιση κρυφών αρχείων"), + ("Receive", "Λήψη"), + ("Send", "Αποστολή"), + ("Refresh File", "Ανανέωση αρχείου"), + ("Local", "Τοπικό"), + ("Remote", "Απομακρυσμένο"), + ("Remote Computer", "Απομακρυσμένος υπολογιστής"), + ("Local Computer", "Τοπικός υπολογιστής"), + ("Confirm Delete", "Επιβεβαίωση διαγραφής"), + ("Delete", "Διαγραφή"), + ("Properties", "Ιδιότητες"), + ("Multi Select", "Πολλαπλή επιλογή"), + ("Select All", "Επιλογή όλων"), + ("Unselect All", "Κατάργηση επιλογής όλων"), + ("Empty Directory", "Κενός φάκελος"), + ("Not an empty directory", "Ο φάκελος δεν είναι κενός"), + ("Are you sure you want to delete this file?", "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτό το αρχείο;"), + ("Are you sure you want to delete this empty directory?", "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτόν τον κενό φάκελο;"), + ("Are you sure you want to delete the file of this directory?", "Είστε βέβαιοι ότι θέλετε να διαγράψετε το αρχείο αυτού του φακέλου;"), + ("Do this for all conflicts", "Κάνε αυτό για όλες τις διενέξεις"), + ("This is irreversible!", "Αυτό είναι μη αναστρέψιμο!"), + ("Deleting", "Διαγραφή"), + ("files", "αρχεία"), + ("Waiting", "Αναμονή"), + ("Finished", "Ολοκληρώθηκε"), + ("Speed", "Ταχύτητα"), + ("Custom Image Quality", "Προσαρμοσμένη ποιότητα εικόνας"), + ("Privacy mode", "Λειτουργία απορρήτου"), + ("Block user input", "Αποκλεισμός εισόδου χρήστη"), + ("Unblock user input", "Κατάργηση αποκλεισμού εισόδου χρήστη"), + ("Adjust Window", "Προσαρμογή παραθύρου"), + ("Original", "Πρωτότυπο"), + ("Shrink", "Συρρίκνωση"), + ("Stretch", "Προσαρμογή"), + ("Scrollbar", "Γραμμή κύλισης"), + ("ScrollAuto", "Αυτόματη κύλιση"), + ("Good image quality", "Καλή ποιότητα εικόνας"), + ("Balanced", "Ισορροπημένο"), + ("Optimize reaction time", "Βελτιστοποίηση χρόνου αντίδρασης"), + ("Custom", "Προσαρμοσμένο"), + ("Show remote cursor", "Εμφάνιση απομακρυσμένου κέρσορα"), + ("Show quality monitor", "Εμφάνιση παρακολούθησης ποιότητας"), + ("Disable clipboard", "Απενεργοποίηση προχείρου"), + ("Lock after session end", "Κλείδωμα μετά το τέλος της συνεδρίας"), + ("Insert", "Εισάγετε"), + ("Insert Lock", "Εισαγωγή κλειδαριάς"), + ("Refresh", "Ανανέωση"), + ("ID does not exist", "Το αναγνωριστικό ID δεν υπάρχει"), + ("Failed to connect to rendezvous server", "Αποτυχία σύνδεσης με διακομιστή"), + ("Please try later", "Παρακαλώ δοκιμάστε αργότερα"), + ("Remote desktop is offline", "Ο απομακρυσμένος σταθμός εργασίας είναι εκτός σύνδεσης"), + ("Key mismatch", "Μη έγκυρο κλειδί"), + ("Timeout", "Τέλος χρόνου"), + ("Failed to connect to relay server", "Αποτυχία σύνδεσης με διακομιστή αναμετάδοσης"), + ("Failed to connect via rendezvous server", "Απέτυχε η σύνδεση μέσω διακομιστή"), + ("Failed to connect via relay server", "Απέτυχε η σύνδεση μέσω διακομιστή αναμετάδοσης"), + ("Failed to make direct connection to remote desktop", "Απέτυχε η απευθείας σύνδεση με απομακρυσμένο σταθμό εργασίας"), + ("Set Password", "Ορίστε κωδικό"), + ("OS Password", "Κωδικός πρόσβασης λειτουργικού συστήματος"), + ("install_tip", "Λόγω UAC, το RustDesk ενδέχεται να μην λειτουργεί σωστά σε ορισμένες περιπτώσεις. Για να αποφύγετε το UAC, κάντε κλικ στο κουμπί παρακάτω για να εγκαταστήσετε το RustDesk στο σύστημα"), + ("Click to upgrade", "Κάντε κλικ για αναβάθμιση"), + ("Click to download", "Κάντε κλικ για λήψη"), + ("Click to update", "Κάντε κλικ για ενημέρωση"), + ("Configure", "Διαμόρφωση"), + ("config_acc", "Για τον απομακρυσμένο έλεγχο του υπολογιστή σας, πρέπει να εκχωρήσετε δικαιώματα πρόσβασης στο RustDesk."), + ("config_screen", "Για να αποκτήσετε απομακρυσμένη πρόσβαση στον υπολογιστή σας, πρέπει να εκχωρήσετε το δικαίωμα RustDesk \"Screen Capture\"."), + ("Installing ...", "Εγκατάσταση ..."), + ("Install", "Εγκατάσταση"), + ("Installation", "Εγκατάσταση"), + ("Installation Path", "Διαδρομή εγκατάστασης"), + ("Create start menu shortcuts", "Δημιουργία συντομεύσεων μενού έναρξης"), + ("Create desktop icon", "Δημιουργία εικονιδίου επιφάνειας εργασίας"), + ("agreement_tip", "Με την εγκατάσταση αποδέχεστε την άδεια χρήσης"), + ("Accept and Install", "Αποδοχή και εγκατάσταση"), + ("End-user license agreement", "Σύμβαση άδειας χρήσης τελικού χρήστη"), + ("Generating ...", "Δημιουργία ..."), + ("Your installation is lower version.", "Η έκδοση της εγκατάστασής σας είναι παλαιότερη."), + ("not_close_tcp_tip", "Μην κλείσετε αυτό το παράθυρο ενώ χρησιμοποιείτε το τούνελ."), + ("Listening ...", "Αναμονή ..."), + ("Remote Host", "Απομακρυσμένος υπολογιστής"), + ("Remote Port", "Απομακρυσμένη θύρα"), + ("Action", "Δράση"), + ("Add", "Προσθήκη"), + ("Local Port", "Τοπική θύρα"), + ("Local Address", "Τοπική διεύθυνση"), + ("Change Local Port", "Αλλαγή τοπικής θύρας"), + ("setup_server_tip", "Για πιο γρήγορη σύνδεση, ρυθμίστε τον δικό σας διακομιστή σύνδεσης"), + ("Too short, at least 6 characters.", "Πολύ μικρό, τουλάχιστον 6 χαρακτήρες."), + ("The confirmation is not identical.", "Η επιβεβαίωση δεν είναι πανομοιότυπη."), + ("Permissions", "Άδειες"), + ("Accept", "Αποδοχή"), + ("Dismiss", "Απόρριψη"), + ("Disconnect", "Αποσύνδεση"), + ("Allow using keyboard and mouse", "Να επιτρέπεται η χρήση πληκτρολογίου και ποντικιού"), + ("Allow using clipboard", "Να επιτρέπεται η χρήση του προχείρου"), + ("Allow hearing sound", "Να επιτρέπεται η ακρόαση"), + ("Allow file copy and paste", "Να επιτρέπεται η αντιγραφή και επικόλληση αρχείου"), + ("Connected", "Συνδεδεμένο"), + ("Direct and encrypted connection", "Άμεση και κρυπτογραφημένη σύνδεση"), + ("Relayed and encrypted connection", "Κρυπτογραφημένη σύνδεση με αναμετάδοση"), + ("Direct and unencrypted connection", "Άμεση και μη κρυπτογραφημένη σύνδεση"), + ("Relayed and unencrypted connection", "Μη κρυπτογραφημένη σύνδεση με αναμετάδοση"), + ("Enter Remote ID", "Εισαγωγή απομακρυσμένου αναγνωριστικού ID"), + ("Enter your password", "Εισάγετε τον κωδικό σας"), + ("Logging in...", "Σύνδεση..."), + ("Enable RDP session sharing", "Ενεργοποίηση κοινής χρήσης RDP"), + ("Auto Login", "Αυτόματη είσοδος"), + ("Enable Direct IP Access", "Ενεργοποίηση άμεσης πρόσβασης IP"), + ("Rename", "Μετονομασία"), + ("Space", "Χώρος"), + ("Create Desktop Shortcut", "Δημιουργία συντόμευσης στην επιφάνεια εργασίας"), + ("Change Path", "Αλλαγή διαδρομής"), + ("Create Folder", "Δημιουργία φακέλου"), + ("Please enter the folder name", "Παρακαλώ εισάγετε το όνομα του φακέλου"), + ("Fix it", "Επιδιόρθωσε το"), + ("Warning", "Προειδοποίηση"), + ("Login screen using Wayland is not supported", "Η οθόνη εισόδου με χρήση του Wayland δεν υποστηρίζεται"), + ("Reboot required", "Απαιτείται επανεκκίνηση"), + ("Unsupported display server ", "Μη υποστηριζόμενος διακομιστής εμφάνισης "), + ("x11 expected", "X11 αναμενόμενο"), + ("Port", "Θύρα"), + ("Settings", "Ρυθμίσεις"), + ("Username", "Όνομα χρήστη"), + ("Invalid port", "Μη έγκυρη θύρα"), + ("Closed manually by the peer", "Έκλεισε από τον απομακρυσμένο σταθμό"), + ("Enable remote configuration modification", "Ενεργοποίηση απομακρυσμένης τροποποίησης ρυθμίσεων"), + ("Run without install", "Εκτέλεση χωρίς εγκατάσταση"), + ("Always connected via relay", "Πάντα συνδεδεμένο μέσω αναμετάδοσης"), + ("Always connect via relay", "Σύνδεση πάντα μέσω αναμετάδοσης"), + ("whitelist_tip", "Μόνο οι IP της λίστας επιτρεπόμενων έχουν πρόσβαση"), + ("Login", "Σύνδεση"), + ("Logout", "Αποσύνδεση"), + ("Tags", "Ετικέτες"), + ("Search ID", "Αναζήτηση ID"), + ("Current Wayland display server is not supported", "Ο τρέχων διακομιστής εμφάνισης Wayland δεν υποστηρίζεται"), + ("whitelist_sep", "Διαχωρίζονται με κόμμα, ερωτηματικό, διάστημα ή νέα γραμμή"), + ("Add ID", "Προσθήκη αναγνωριστικού ID"), + ("Add Tag", "Προσθήκη ετικέτας"), + ("Unselect all tags", "Κατάργηση επιλογής όλων των ετικετών"), + ("Network error", "Σφάλμα δικτύου"), + ("Username missed", "Δεν συμπληρώσατε το όνομα χρήστη"), + ("Password missed", "Δεν συμπληρώσατε τον κωδικό πρόσβασης"), + ("Wrong credentials", "Λάθος διαπιστευτήρια"), + ("Edit Tag", "Επεξεργασία ετικέτας"), + ("Unremember Password", "Διαγραφή απομνημονευμένου κωδικού"), + ("Favorites", "Αγαπημένα"), + ("Add to Favorites", "Προσθήκη στα αγαπημένα"), + ("Remove from Favorites", "Κατάργηση από τα Αγαπημένα"), + ("Empty", "Άδειο"), + ("Invalid folder name", "Μη έγκυρο όνομα φακέλου"), + ("Socks5 Proxy", "Διαμεσολαβητής Socks5"), + ("Hostname", "Όνομα υπολογιστή"), + ("Discovered", "Ανακαλύφθηκε"), + ("install_daemon_tip", "Για να ξεκινά με την εκκίνηση του υπολογιστή, πρέπει να εγκαταστήσετε την υπηρεσία συστήματος"), + ("Remote ID", "Απομακρυσμένο ID"), + ("Paste", "Επικόλληση"), + ("Paste here?", "Επικόλληση εδώ;"), + ("Are you sure to close the connection?", "Είστε βέβαιοι ότι θέλετε να κλείσετε αυτήν τη σύνδεση;"), + ("Download new version", "Λήψη νέας έκδοσης"), + ("Touch mode", "Λειτουργία αφής"), + ("Mouse mode", "Λειτουργία ποντικιού"), + ("One-Finger Tap", "Πάτημα με ένα δάχτυλο"), + ("Left Mouse", "Αριστερό κλικ"), + ("One-Long Tap", "Παρατεταμένο πάτημα με ένα δάχτυλο"), + ("Two-Finger Tap", "Πάτημα με δύο δάχτυλα"), + ("Right Mouse", "Δεξί κλικ"), + ("One-Finger Move", "Κίνηση με ένα δάχτυλο"), + ("Double Tap & Move", "Διπλό πάτημα και μετακίνηση"), + ("Mouse Drag", "Σύρετε το ποντίκι"), + ("Three-Finger vertically", "Τρία δάχτυλα, κάθετα"), + ("Mouse Wheel", "Τροχός ποντικιού"), + ("Two-Finger Move", "Κίνηση με δύο δάχτυλα"), + ("Canvas Move", "Κίνηση καμβά"), + ("Pinch to Zoom", "Τσίμπημα για ζουμ"), + ("Canvas Zoom", "Ζουμ σε καμβά"), + ("Reset canvas", "Επαναφορά καμβά"), + ("No permission of file transfer", "Δεν υπάρχει άδεια για μεταφορά αρχείων"), + ("Note", "Σημείωση"), + ("Connection", "Σύνδεση"), + ("Share Screen", "Κοινή χρήση οθόνης"), + ("CLOSE", "Απενεργοποίηση"), + ("OPEN", "Ενεργοποίηση"), + ("Chat", "Κουβέντα"), + ("Total", "Σύνολο"), + ("items", "στοιχεία"), + ("Selected", "Επιλεγμένο"), + ("Screen Capture", "Αποτύπωση οθόνης"), + ("Input Control", "Έλεγχος εισόδου"), + ("Audio Capture", "Λήψη ήχου"), + ("File Connection", "Σύνδεση αρχείου"), + ("Screen Connection", "Σύνδεση οθόνης"), + ("Do you accept?", "Δέχεσαι?"), + ("Open System Setting", "Άνοιγμα ρυθμίσεων συστήματος"), + ("How to get Android input permission?", "Πώς να αποκτήσω άδεια εισαγωγής Android;"), + ("android_input_permission_tip1", "Για να μπορεί μία απομακρυσμένη συσκευή να ελέγχει τη συσκευή σας Android, πρέπει να επιτρέψετε στο RustDesk να χρησιμοποιεί την υπηρεσία \"Προσβασιμότητα\"."), + ("android_input_permission_tip2", "Παρακαλώ μεταβείτε στην επόμενη σελίδα ρυθμίσεων συστήματος, βρείτε και πληκτρολογήστε [Εγκατεστημένες υπηρεσίες], ενεργοποιήστε την υπηρεσία [Είσοδος RustDesk]."), + ("android_new_connection_tip", "θέλω να ελέγξω τη συσκευή σου."), + ("android_service_will_start_tip", "Η ενεργοποίηση της κοινής χρήσης οθόνης θα ξεκινήσει αυτόματα την υπηρεσία, ώστε άλλες συσκευές να μπορούν να ελέγχουν αυτήν τη συσκευή Android."), + ("android_stop_service_tip", "Η απενεργοποίηση της υπηρεσίας θα αποσυνδέσει αυτόματα όλες τις εγκατεστημένες συνδέσεις."), + ("android_version_audio_tip", "Η έκδοση Android που διαθέτετε δεν υποστηρίζει εγγραφή ήχου, ενημερώστε το σε Android 10 ή νεότερη έκδοση, εάν είναι δυνατόν."), + ("android_start_service_tip", "Πατήστε [Ενεργοποίηση υπηρεσίας] ή ενεργοποιήστε την άδεια [Πρόσβαση στην οθόνη] για να ξεκινήσετε την υπηρεσία κοινής χρήσης οθόνης."), + ("Account", "Λογαριασμός"), + ("Overwrite", "Αντικατάσταση"), + ("This file exists, skip or overwrite this file?", "Αυτό το αρχείο υπάρχει, παράβλεψη ή αντικατάσταση αυτού του αρχείου;"), + ("Quit", "Έξοδος"), + ("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"), + ("Help", "Βοήθεια"), + ("Failed", "Απέτυχε"), + ("Succeeded", "Επιτυχής"), + ("Someone turns on privacy mode, exit", "Κάποιος ενεργοποιεί τη λειτουργία απορρήτου, έξοδος"), + ("Unsupported", "Δεν υποστηρίζεται"), + ("Peer denied", "Ο απομακρυσμένος σταθμός απέρριψε τη σύνδεση"), + ("Please install plugins", "Παρακαλώ εγκαταστήστε πρόσθετα"), + ("Peer exit", "Ο απομακρυσμένος σταθμός έχει αποσυνδεθεί"), + ("Failed to turn off", "Αποτυχία απενεργοποίησης"), + ("Turned off", "Απενεργοποιημένο"), + ("In privacy mode", "Σε λειτουργία απορρήτου"), + ("Out privacy mode", "Εκτός λειτουργίας απορρήτου"), + ("Language", "Γλώσσα"), + ("Keep RustDesk background service", "Εκτέλεση του RustDesk στο παρασκήνιο"), + ("Ignore Battery Optimizations", "Παράβλεψη βελτιστοποιήσεων μπαταρίας"), + ("android_open_battery_optimizations_tip", "Θέλετε να ανοίξετε τις ρυθμίσεις βελτιστοποίησης μπαταρίας;"), + ("Connection not allowed", "Η σύνδεση απορρίφθηκε"), + ("Legacy mode", "Λειτουργία συμβατότητας"), + ("Map mode", "Map mode"), + ("Translate mode", "Λειτουργία μετάφρασης"), + ("Use permanent password", "Χρήση μόνιμου κωδικού πρόσβασης"), + ("Use both passwords", "Χρήση και των δύο κωδικών πρόσβασης"), + ("Set permanent password", "Ορισμός μόνιμου κωδικού πρόσβασης"), + ("Enable Remote Restart", "Ενεργοποίηση απομακρυσμένης επανεκκίνησης"), + ("Allow remote restart", "Να επιτρέπεται η απομακρυσμένη επανεκκίνηση"), + ("Restart Remote Device", "Επανεκκίνηση απομακρυσμένης συσκευής"), + ("Are you sure you want to restart", "Είστε βέβαιοι ότι θέλετε να κάνετε επανεκκίνηση"), + ("Restarting Remote Device", "Επανεκκίνηση απομακρυσμένης συσκευής"), + ("remote_restarting_tip", "Η απομακρυσμένη συσκευή επανεκκινείται, κλείστε αυτό το μήνυμα και επανασυνδεθείτε χρησιμοποιώντας τον μόνιμο κωδικό πρόσβασης."), + ("Copied", "Αντιγράφηκε"), + ("Exit Fullscreen", "Έξοδος από πλήρη οθόνη"), + ("Fullscreen", "Πλήρης οθόνη"), + ("Mobile Actions", "Mobile Actions"), + ("Select Monitor", "Επιλογή οθόνης"), + ("Control Actions", "Ενέργειες ελέγχου"), + ("Display Settings", "Ρυθμίσεις οθόνης"), + ("Ratio", "Αναλογία"), + ("Image Quality", "Ποιότητα εικόνας"), + ("Scroll Style", "Στυλ κύλισης"), + ("Show Menubar", "Εμφάνιση γραμμής μενού"), + ("Hide Menubar", "Απόκρυψη γραμμής μενού"), + ("Direct Connection", "Απευθείας σύνδεση"), + ("Relay Connection", "Αναμεταδιδόμενη σύνδεση"), + ("Secure Connection", "Ασφαλής σύνδεση"), + ("Insecure Connection", "Μη ασφαλής σύνδεση"), + ("Scale original", "Κλιμάκωση πρωτότυπου"), + ("Scale adaptive", "Προσαρμοστική κλίμακα"), + ("General", "Γενικά"), + ("Security", "Ασφάλεια"), + ("Account", "Λογαριασμός"), + ("Theme", "Θέμα"), + ("Dark Theme", "Σκούρο θέμα"), + ("Dark", "Σκούρο"), + ("Light", "Φωτεινό"), + ("Follow System", "Ακολουθήστε το σύστημα"), + ("Enable hardware codec", "Ενεργοποίηση κωδικοποιητή υλικού"), + ("Unlock Security Settings", "Ξεκλείδωμα ρυθμίσεων ασφαλείας"), + ("Enable Audio", "Ενεργοποίηση ήχου"), + ("Unlock Network Settings", "Ξεκλείδωμα ρυθμίσεων δικτύου"), + ("Server", "Διακομιστής"), + ("Direct IP Access", "Άμεση πρόσβαση IP"), + ("Proxy", "Διαμεσολαβητής"), + ("Port", "Θύρα"), + ("Apply", "Εφαρμογή"), + ("Disconnect all devices?", "Αποσύνδεση όλων των συσκευών;"), + ("Clear", "Καθαρισμός"), + ("Audio Input Device", "Συσκευή εισόδου ήχου"), + ("Deny remote access", "Απόρριψη απομακρυσμένης πρόσβασης"), + ("Use IP Whitelisting", "Χρήση λευκής λίστας IP"), + ("Network", "Δίκτυο"), + ("Enable RDP", "Ενεργοποίηση RDP"), + ("Pin menubar", "Καρφίτσωμα γραμμής μενού"), + ("Unpin menubar", "Ξεκαρφίτσωμα γραμμής μενού"), + ("Recording", "Ηχογράφηση"), + ("Directory", "Ευρετήριο"), + ("Automatically record incoming sessions", "Αυτόματη εγγραφή εισερχόμενων συνεδριών"), + ("Change", "Αλλαγή"), + ("Start session recording", "Έναρξη εγγραφής συνεδρίας"), + ("Stop session recording", "Διακοπή εγγραφής συνεδρίας"), + ("Enable Recording Session", "Ενεργοποίηση συνεδρίας εγγραφής"), + ("Allow recording session", "Να επιτρέπεται η εγγραφή"), + ("Enable LAN Discovery", "Ενεργοποίηση εντοπισμού LAN"), + ("Deny LAN Discovery", "Απαγόρευση εντοπισμού LAN"), + ("Write a message", "Γράψτε ένα μήνυμα"), + ("Prompt", "Προτροπή"), + ("Please wait for confirmation of UAC...", "Παρακαλώ περιμένετε για επιβεβαίωση του UAC..."), + ("elevated_foreground_window_tip", "Το τρέχον παράθυρο της απομακρυσμένης επιφάνειας εργασίας απαιτεί υψηλότερα δικαιώματα για να λειτουργήσει, επομένως δεν μπορεί να χρησιμοποιήσει προσωρινά το ποντίκι και το πληκτρολόγιο. Μπορείτε να ζητήσετε από τον απομακρυσμένο χρήστη να ελαχιστοποιήσει το τρέχον παράθυρο ή να κάνετε κλικ στο κουμπί ανύψωσης στο παράθυρο διαχείρισης σύνδεσης. Για να αποφύγετε αυτό το πρόβλημα, συνιστάται η εγκατάσταση του λογισμικού στην απομακρυσμένη συσκευή."), + ("Disconnected", "Αποσυνδέθηκε"), + ("Other", "Άλλα"), + ("Confirm before closing multiple tabs", "Επιβεβαίωση πριν κλείσετε πολλές καρτέλες"), + ("Keyboard Settings", "Ρυθμίσεις πληκτρολογίου"), + ("Custom", "Προσαρμογή ποιότητας εικόνας"), + ("Full Access", "Πλήρης πρόσβαση"), + ("Screen Share", "Κοινή χρήση οθόνης"), + ("Wayland requires Ubuntu 21.04 or higher version.", "Το Wayland απαιτεί Ubuntu 21.04 ή νεότερη έκδοση."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Το Wayland απαιτεί υψηλότερη έκδοση του linux distro. Δοκιμάστε την επιφάνεια εργασίας X11 ή αλλάξτε το λειτουργικό σας σύστημα."), + ("JumpLink", "Προβολή"), + ("Please Select the screen to be shared(Operate on the peer side).", "Επιλέξτε την οθόνη που θέλετε να μοιραστείτε (Λειτουργία στην πλευρά του απομακρυσμένου σταθμού)."), + ("Show RustDesk", "Εμφάνιση RustDesk"), + ("This PC", "Αυτός ο υπολογιστής"), + ("or", "ή"), + ("Continue with", "Συνέχεια με"), + ("Elevate", "Ανύψωση"), + ("Zoom cursor", "Μεγέθυνση στον κέρσορα"), + ("Accept sessions via password", "Αποδοχή συνεδριών μέσω κωδικού πρόσβασης"), + ("Accept sessions via click", "Αποδοχή συνεδριών μέσω κλικ"), + ("Accept sessions via both", "Αποδοχή συνεδριών και από τα δύο"), + ("Please wait for the remote side to accept your session request...", "Παρακαλώ περιμένετε μέχρι η απομακρυσμένη πλευρά να αποδεχτεί το αίτημα συνεδρίας σας..."), + ("One-time Password", "Κωδικός μίας χρήσης"), + ("Use one-time password", "Χρήση κωδικού πρόσβασης μίας χρήσης"), + ("One-time password length", "Μήκος κωδικού πρόσβασης μίας χρήσης"), + ("Request access to your device", "Αίτημα πρόσβασης στη συσκευή σας"), + ("Hide connection management window", "Απόκρυψη παραθύρου διαχείρισης σύνδεσης"), + ("hide_cm_tip", "Να επιτρέπεται η απόκρυψη, μόνο εάν αποδέχεστε συνδέσεις μέσω κωδικού πρόσβασης και χρησιμοποιείτε μόνιμο κωδικό πρόσβασης"), + ].iter().cloned().collect(); +} From fc89257566754f1205378229a3044f06abb09df9 Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 23 Nov 2022 14:13:11 +0800 Subject: [PATCH 1031/2015] test Signed-off-by: fufesou --- src/server/video_service.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/server/video_service.rs b/src/server/video_service.rs index db419fc65..d59c1cd5c 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -604,11 +604,13 @@ fn run(sp: GenericService) -> ResultType<()> { { would_block_count += 1; if !scrap::is_x11() { - if would_block_count >= 100 { + if would_block_count >= 1000 { // For now, the user should choose and agree screen sharing agiain. // to-do: Remember choice, attendless... - super::wayland::release_resouce(); - bail!("Wayland capturer none 100 times, try restart captuere"); + // super::wayland::release_resouce(); + // bail!("Wayland capturer none 100 times, try restart captuere"); + log::error!("Wayland capturer none 1000 times, try restart captuere"); + would_block_count = 0; } } } From e31130d4f8b740963d8d25d94d26824194d46376 Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 23 Nov 2022 23:57:01 +0800 Subject: [PATCH 1032/2015] wayland, fix check Lock && Mod Resolution Signed-off-by: fufesou --- libs/scrap/src/wayland/pipewire.rs | 42 ++++++++++++++++++++++++++++-- src/server/video_service.rs | 11 +++----- src/server/wayland.rs | 1 + 3 files changed, 44 insertions(+), 10 deletions(-) diff --git a/libs/scrap/src/wayland/pipewire.rs b/libs/scrap/src/wayland/pipewire.rs index 844e8eead..6277ed97c 100644 --- a/libs/scrap/src/wayland/pipewire.rs +++ b/libs/scrap/src/wayland/pipewire.rs @@ -16,9 +16,10 @@ use gstreamer as gst; use gstreamer::prelude::*; use gstreamer_app::AppSink; +use hbb_common::config; + use super::capturable::PixelProvider; use super::capturable::{Capturable, Recorder}; - use super::pipewire_dbus::{OrgFreedesktopPortalRequestResponse, OrgFreedesktopPortalScreenCast}; #[derive(Debug, Clone, Copy)] @@ -130,6 +131,7 @@ impl PipeWireRecorder { let src = gst::ElementFactory::make("pipewiresrc", None)?; src.set_property("fd", &capturable.fd.as_raw_fd())?; src.set_property("path", &format!("{}", capturable.path))?; + src.set_property("keepalive_time", &1_000.as_raw_fd())?; // For some reason pipewire blocks on destruction of AppSink if this is not set to true, // see: https://gitlab.freedesktop.org/pipewire/pipewire/-/issues/982 @@ -384,6 +386,8 @@ fn streams_from_response(response: OrgFreedesktopPortalRequestResponse) -> Vec

    = 4 { + let restore_token = config::LocalConfig::get_option(RESTORE_TOKEN_CONF_KEY); + if !restore_token.is_empty() { + args.insert( + RESTORE_TOKEN.to_string(), + Variant(Box::new(restore_token)), + ); + } + // persist_mode may be configured by the user. + args.insert( + "persist_mode".to_string(), + Variant(Box::new(2u32)), + ); + } + } args.insert( "handle_token".to_string(), Variant(Box::new("u2".to_string())), @@ -476,12 +502,24 @@ fn request_screen_cast( c, path, move |r: OrgFreedesktopPortalRequestResponse, c, _| { + let portal = get_portal(c); + if let Ok(version) = portal.version() { + if version >= 4 { + if let Some(restore_token) = r.results.get(RESTORE_TOKEN) { + if let Some(restore_token) = restore_token.as_str() { + config::LocalConfig::set_option( + RESTORE_TOKEN_CONF_KEY.to_owned(), + restore_token.to_owned(), + ); + } + } + } + } streams .clone() .lock() .unwrap() .append(&mut streams_from_response(r)); - let portal = get_portal(c); fd.clone().lock().unwrap().replace( portal.open_pipe_wire_remote(session.clone(), HashMap::new())?, ); diff --git a/src/server/video_service.rs b/src/server/video_service.rs index d59c1cd5c..2fc2d2f43 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -599,18 +599,13 @@ fn run(sp: GenericService) -> ResultType<()> { } try_gdi += 1; } - #[cfg(target_os = "linux")] { would_block_count += 1; if !scrap::is_x11() { - if would_block_count >= 1000 { - // For now, the user should choose and agree screen sharing agiain. - // to-do: Remember choice, attendless... - // super::wayland::release_resouce(); - // bail!("Wayland capturer none 100 times, try restart captuere"); - log::error!("Wayland capturer none 1000 times, try restart captuere"); - would_block_count = 0; + if would_block_count >= 100 { + super::wayland::release_resouce(); + bail!("Wayland capturer none 100 times, try restart captuere"); } } } diff --git a/src/server/wayland.rs b/src/server/wayland.rs index 7fd6f106b..22134cdff 100644 --- a/src/server/wayland.rs +++ b/src/server/wayland.rs @@ -241,6 +241,7 @@ pub(super) fn get_display_num() -> ResultType { } } +#[allow(dead_code)] pub(super) fn release_resouce() { if scrap::is_x11() { return; From 9dfa02a702277238dcd66afe696d5cf0e9ca3b8f Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 25 Nov 2022 15:22:44 +0800 Subject: [PATCH 1033/2015] Fix wayland input after Lock and Wake Signed-off-by: fufesou --- libs/enigo/src/lib.rs | 14 +++++++ libs/enigo/src/linux/nix_impl.rs | 72 +++++++++++++++++++++----------- libs/enigo/src/linux/xdo.rs | 16 +++++++ src/ipc.rs | 1 + src/server.rs | 5 ++- src/server/input_service.rs | 40 ++++++++++++++---- src/server/uinput.rs | 35 +++++++++++++++- src/server/video_service.rs | 10 +++++ src/server/wayland.rs | 2 +- 9 files changed, 159 insertions(+), 36 deletions(-) diff --git a/libs/enigo/src/lib.rs b/libs/enigo/src/lib.rs index 01e9b67d2..083345e63 100644 --- a/libs/enigo/src/lib.rs +++ b/libs/enigo/src/lib.rs @@ -118,6 +118,13 @@ pub enum MouseButton { /// Representing an interface and a set of mouse functions every /// operating system implementation _should_ implement. pub trait MouseControllable { + // https://stackoverflow.com/a/33687996 + /// Offer the ability to confer concrete type. + fn as_any(&self) -> &dyn std::any::Any; + + /// Offer the ability to confer concrete type. + fn as_mut_any(&mut self) -> &mut dyn std::any::Any; + /// Lets the mouse cursor move to the specified x and y coordinates. /// /// The topleft corner of your monitor screen is x=0 y=0. Move @@ -425,6 +432,13 @@ pub enum Key { /// Representing an interface and a set of keyboard functions every /// operating system implementation _should_ implement. pub trait KeyboardControllable { + // https://stackoverflow.com/a/33687996 + /// Offer the ability to confer concrete type. + fn as_any(&self) -> &dyn std::any::Any; + + /// Offer the ability to confer concrete type. + fn as_mut_any(&mut self) -> &mut dyn std::any::Any; + /// Types the string parsed with DSL. /// /// Typing {+SHIFT}hello{-SHIFT} becomes HELLO. diff --git a/libs/enigo/src/linux/nix_impl.rs b/libs/enigo/src/linux/nix_impl.rs index e09f826dc..4eb890c29 100644 --- a/libs/enigo/src/linux/nix_impl.rs +++ b/libs/enigo/src/linux/nix_impl.rs @@ -3,14 +3,17 @@ use crate::{Key, KeyboardControllable, MouseButton, MouseControllable}; use std::io::Read; use tfc::{traits::*, Context as TFC_Context, Key as TFC_Key}; +pub type CustomKeyboard = Box; +pub type CustomMouce = Box; + /// The main struct for handling the event emitting // #[derive(Default)] pub struct Enigo { xdo: EnigoXdo, is_x11: bool, tfc: Option, - uinput_keyboard: Option>, - uinput_mouse: Option>, + custom_keyboard: Option, + cutsom_mouse: Option, } impl Enigo { @@ -22,16 +25,21 @@ impl Enigo { pub fn set_delay(&mut self, delay: u64) { self.xdo.set_delay(delay) } - /// Set uinput keyboard. - pub fn set_uinput_keyboard( - &mut self, - uinput_keyboard: Option>, - ) { - self.uinput_keyboard = uinput_keyboard + /// Set custom keyboard. + pub fn set_custom_keyboard(&mut self, custom_keyboard: CustomKeyboard) { + self.custom_keyboard = Some(custom_keyboard) } - /// Set uinput mouse. - pub fn set_uinput_mouse(&mut self, uinput_mouse: Option>) { - self.uinput_mouse = uinput_mouse + /// Set custom mouse. + pub fn set_custom_mouse(&mut self, custom_mouse: CustomMouce) { + self.cutsom_mouse = Some(custom_mouse) + } + /// Get custom keyboard. + pub fn get_custom_keyboard(&mut self) -> &mut Option { + &mut self.custom_keyboard + } + /// Get custom mouse. + pub fn get_custom_mouse(&mut self) -> &mut Option { + &mut self.cutsom_mouse } fn tfc_key_down_or_up(&mut self, key: Key, down: bool, up: bool) -> bool { @@ -84,19 +92,27 @@ impl Default for Enigo { } else { None }, - uinput_keyboard: None, - uinput_mouse: None, + custom_keyboard: None, + cutsom_mouse: None, xdo: EnigoXdo::default(), } } } impl MouseControllable for Enigo { + fn as_any(&self) -> &dyn std::any::Any { + self + } + + fn as_mut_any(&mut self) -> &mut dyn std::any::Any { + self + } + fn mouse_move_to(&mut self, x: i32, y: i32) { if self.is_x11 { self.xdo.mouse_move_to(x, y); } else { - if let Some(mouse) = &mut self.uinput_mouse { + if let Some(mouse) = &mut self.cutsom_mouse { mouse.mouse_move_to(x, y) } } @@ -105,7 +121,7 @@ impl MouseControllable for Enigo { if self.is_x11 { self.xdo.mouse_move_relative(x, y); } else { - if let Some(mouse) = &mut self.uinput_mouse { + if let Some(mouse) = &mut self.cutsom_mouse { mouse.mouse_move_relative(x, y) } } @@ -114,7 +130,7 @@ impl MouseControllable for Enigo { if self.is_x11 { self.xdo.mouse_down(button) } else { - if let Some(mouse) = &mut self.uinput_mouse { + if let Some(mouse) = &mut self.cutsom_mouse { mouse.mouse_down(button) } else { Ok(()) @@ -125,7 +141,7 @@ impl MouseControllable for Enigo { if self.is_x11 { self.xdo.mouse_up(button) } else { - if let Some(mouse) = &mut self.uinput_mouse { + if let Some(mouse) = &mut self.cutsom_mouse { mouse.mouse_up(button) } } @@ -134,7 +150,7 @@ impl MouseControllable for Enigo { if self.is_x11 { self.xdo.mouse_click(button) } else { - if let Some(mouse) = &mut self.uinput_mouse { + if let Some(mouse) = &mut self.cutsom_mouse { mouse.mouse_click(button) } } @@ -143,7 +159,7 @@ impl MouseControllable for Enigo { if self.is_x11 { self.xdo.mouse_scroll_x(length) } else { - if let Some(mouse) = &mut self.uinput_mouse { + if let Some(mouse) = &mut self.cutsom_mouse { mouse.mouse_scroll_x(length) } } @@ -152,7 +168,7 @@ impl MouseControllable for Enigo { if self.is_x11 { self.xdo.mouse_scroll_y(length) } else { - if let Some(mouse) = &mut self.uinput_mouse { + if let Some(mouse) = &mut self.cutsom_mouse { mouse.mouse_scroll_y(length) } } @@ -180,11 +196,19 @@ fn get_led_state(key: Key) -> bool { } impl KeyboardControllable for Enigo { + fn as_any(&self) -> &dyn std::any::Any { + self + } + + fn as_mut_any(&mut self) -> &mut dyn std::any::Any { + self + } + fn get_key_state(&mut self, key: Key) -> bool { if self.is_x11 { self.xdo.get_key_state(key) } else { - if let Some(keyboard) = &mut self.uinput_keyboard { + if let Some(keyboard) = &mut self.custom_keyboard { keyboard.get_key_state(key) } else { get_led_state(key) @@ -196,7 +220,7 @@ impl KeyboardControllable for Enigo { if self.is_x11 { self.xdo.key_sequence(sequence) } else { - if let Some(keyboard) = &mut self.uinput_keyboard { + if let Some(keyboard) = &mut self.custom_keyboard { keyboard.key_sequence(sequence) } } @@ -211,7 +235,7 @@ impl KeyboardControllable for Enigo { Ok(()) } } else { - if let Some(keyboard) = &mut self.uinput_keyboard { + if let Some(keyboard) = &mut self.custom_keyboard { keyboard.key_down(key) } else { Ok(()) @@ -225,7 +249,7 @@ impl KeyboardControllable for Enigo { self.xdo.key_up(key) } } else { - if let Some(keyboard) = &mut self.uinput_keyboard { + if let Some(keyboard) = &mut self.custom_keyboard { keyboard.key_up(key) } } diff --git a/libs/enigo/src/linux/xdo.rs b/libs/enigo/src/linux/xdo.rs index ff687eee2..ed2d28dc1 100644 --- a/libs/enigo/src/linux/xdo.rs +++ b/libs/enigo/src/linux/xdo.rs @@ -102,6 +102,14 @@ impl Drop for EnigoXdo { } } impl MouseControllable for EnigoXdo { + fn as_any(&self) -> &dyn std::any::Any { + self + } + + fn as_mut_any(&mut self) -> &mut dyn std::any::Any { + self + } + fn mouse_move_to(&mut self, x: i32, y: i32) { if self.xdo.is_null() { return; @@ -277,6 +285,14 @@ fn keysequence<'a>(key: Key) -> Cow<'a, str> { }) } impl KeyboardControllable for EnigoXdo { + fn as_any(&self) -> &dyn std::any::Any { + self + } + + fn as_mut_any(&mut self) -> &mut dyn std::any::Any { + self + } + fn get_key_state(&mut self, key: Key) -> bool { if self.xdo.is_null() { return false; diff --git a/src/ipc.rs b/src/ipc.rs index d2d57f8c9..eb2d364ae 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -117,6 +117,7 @@ pub enum DataMouse { Click(enigo::MouseButton), ScrollX(i32), ScrollY(i32), + Refresh, } #[derive(Debug, Serialize, Deserialize, Clone)] diff --git a/src/server.rs b/src/server.rs index 7e00532fe..f5326288a 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,6 +1,8 @@ use crate::ipc::Data; use bytes::Bytes; pub use connection::*; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use hbb_common::config::Config2; use hbb_common::{ allow_err, anyhow::{anyhow, Context}, @@ -15,8 +17,6 @@ use hbb_common::{ timeout, tokio, ResultType, Stream, }; #[cfg(not(any(target_os = "android", target_os = "ios")))] -use hbb_common::config::Config2; -#[cfg(not(any(target_os = "android", target_os = "ios")))] use service::ServiceTmpl; use service::{GenericService, Service, Subscriber}; use std::{ @@ -377,6 +377,7 @@ pub async fn start_server(is_server: bool) { #[cfg(windows)] crate::platform::windows::bootstrap(); input_service::fix_key_down_timeout_loop(); + allow_err!(video_service::check_init().await); #[cfg(target_os = "macos")] tokio::spawn(async { sync_and_watch_config_dir().await }); crate::RendezvousMediator::start_all().await; diff --git a/src/server/input_service.rs b/src/server/input_service.rs index ca63fed94..4789e459b 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -219,19 +219,43 @@ lazy_static::lazy_static! { static ref IS_SERVER: bool = std::env::args().nth(1) == Some("--server".to_owned()); } +// First call set_uinput() will create keyboard and mouse clients. +// The clients are ipc connections that must live shorter than tokio runtime. +// Thus this funtion must not be called in a temporary runtime. #[cfg(target_os = "linux")] pub async fn set_uinput() -> ResultType<()> { // Keyboard and mouse both open /dev/uinput // TODO: Make sure there's no race - let keyboard = super::uinput::client::UInputKeyboard::new().await?; - log::info!("UInput keyboard created"); - let mouse = super::uinput::client::UInputMouse::new().await?; - log::info!("UInput mouse created"); - let xxx = ENIGO.lock(); - let mut en = xxx.unwrap(); - en.set_uinput_keyboard(Some(Box::new(keyboard))); - en.set_uinput_mouse(Some(Box::new(mouse))); + if ENIGO.lock().unwrap().get_custom_keyboard().is_none() { + let keyboard = super::uinput::client::UInputKeyboard::new().await?; + log::info!("UInput keyboard created"); + ENIGO + .lock() + .unwrap() + .set_custom_keyboard(Box::new(keyboard)); + } + + let mouse_created = ENIGO.lock().unwrap().get_custom_mouse().is_some(); + if mouse_created { + std::thread::spawn(|| { + if let Some(mouse) = ENIGO.lock().unwrap().get_custom_mouse() { + if let Some(mouse) = mouse + .as_mut_any() + .downcast_mut::() + { + allow_err!(mouse.send_refresh()); + } else { + log::error!("failed downcast uinput mouse"); + } + } + }); + } else { + let mouse = super::uinput::client::UInputMouse::new().await?; + log::info!("UInput mouse created"); + ENIGO.lock().unwrap().set_custom_mouse(Box::new(mouse)); + } + Ok(()) } diff --git a/src/server/uinput.rs b/src/server/uinput.rs index 5051d548d..78b22c562 100644 --- a/src/server/uinput.rs +++ b/src/server/uinput.rs @@ -63,6 +63,14 @@ pub mod client { } impl KeyboardControllable for UInputKeyboard { + fn as_any(&self) -> &dyn std::any::Any { + self + } + + fn as_mut_any(&mut self) -> &mut dyn std::any::Any { + self + } + fn get_key_state(&mut self, key: Key) -> bool { match self.send_get_key_state(Data::Keyboard(DataKeyboard::GetKeyState(key))) { Ok(state) => state, @@ -105,9 +113,21 @@ pub mod client { async fn send(&mut self, data: Data) -> ResultType<()> { self.conn.send(&data).await } + + pub fn send_refresh(&mut self) -> ResultType<()> { + self.send(Data::Mouse(DataMouse::Refresh)) + } } impl MouseControllable for UInputMouse { + fn as_any(&self) -> &dyn std::any::Any { + self + } + + fn as_mut_any(&mut self) -> &mut dyn std::any::Any { + self + } + fn mouse_move_to(&mut self, x: i32, y: i32) { allow_err!(self.send(Data::Mouse(DataMouse::MoveTo(x, y)))); } @@ -492,6 +512,9 @@ pub mod service { allow_err!(mouse.scroll_wheel(&scroll)) } } + DataMouse::Refresh => { + // unreachable!() + } } } @@ -562,7 +585,17 @@ pub mod service { Ok(Some(data)) => { match data { Data::Mouse(data) => { - handle_mouse(&mut mouse, &data); + if let DataMouse::Refresh = data { + mouse = match mouce::Mouse::new_uinput(rng_x, rng_y) { + Ok(mouse) => mouse, + Err(e) => { + log::error!("Failed to create mouse, {}", e); + return; + } + } + } else { + handle_mouse(&mut mouse, &data); + } } _ => { } diff --git a/src/server/video_service.rs b/src/server/video_service.rs index 2fc2d2f43..04df87bef 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -759,6 +759,16 @@ fn get_display_num() -> usize { } } +pub async fn check_init() -> ResultType<()> { + #[cfg(target_os = "linux")] + { + if !scrap::is_x11() { + return super::wayland::check_init().await; + } + } + Ok(()) +} + pub(super) fn get_displays_2(all: &Vec) -> (usize, Vec) { let mut displays = Vec::new(); let mut primary = 0; diff --git a/src/server/wayland.rs b/src/server/wayland.rs index 22134cdff..4ffb9e225 100644 --- a/src/server/wayland.rs +++ b/src/server/wayland.rs @@ -115,7 +115,7 @@ pub(super) fn is_inited() -> Option { } } -async fn check_init() -> ResultType<()> { +pub(super) async fn check_init() -> ResultType<()> { if !scrap::is_x11() { let mut minx = 0; let mut maxx = 0; From 87306a3d01d2fb08d2f1e37320306bd1fcb9dc0b Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 25 Nov 2022 18:27:17 +0800 Subject: [PATCH 1034/2015] wayland filter last same frame Signed-off-by: fufesou --- libs/scrap/src/wayland/pipewire.rs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/libs/scrap/src/wayland/pipewire.rs b/libs/scrap/src/wayland/pipewire.rs index 6277ed97c..a7b4c1357 100644 --- a/libs/scrap/src/wayland/pipewire.rs +++ b/libs/scrap/src/wayland/pipewire.rs @@ -122,6 +122,7 @@ pub struct PipeWireRecorder { appsink: AppSink, width: usize, height: usize, + saved_raw_data: Vec, // for faster compare and copy } impl PipeWireRecorder { @@ -160,6 +161,7 @@ impl PipeWireRecorder { height: 0, buffer_cropped: vec![], is_cropped: false, + saved_raw_data: Vec::new(), }) } } @@ -192,6 +194,9 @@ impl Recorder for PipeWireRecorder { let buf = buf .into_mapped_buffer_readable() .map_err(|_| GStreamerError("Failed to map buffer.".into()))?; + if let Err(..) = crate::would_block_if_equal(&mut self.saved_raw_data, buf.as_slice()) { + return Ok(PixelProvider::NONE); + } let buf_size = buf.get_size(); // BGRx is 4 bytes per pixel if buf_size != (w * h * 4) { @@ -433,16 +438,10 @@ fn request_screen_cast( if version >= 4 { let restore_token = config::LocalConfig::get_option(RESTORE_TOKEN_CONF_KEY); if !restore_token.is_empty() { - args.insert( - RESTORE_TOKEN.to_string(), - Variant(Box::new(restore_token)), - ); + args.insert(RESTORE_TOKEN.to_string(), Variant(Box::new(restore_token))); } // persist_mode may be configured by the user. - args.insert( - "persist_mode".to_string(), - Variant(Box::new(2u32)), - ); + args.insert("persist_mode".to_string(), Variant(Box::new(2u32))); } } args.insert( From fff6aad1c5d4035f48a56493c5068540ba77ba53 Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 25 Nov 2022 18:30:30 +0800 Subject: [PATCH 1035/2015] compile win & macos Signed-off-by: fufesou --- libs/enigo/src/macos/macos_impl.rs | 16 ++++++++++++++++ libs/enigo/src/win/win_impl.rs | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/libs/enigo/src/macos/macos_impl.rs b/libs/enigo/src/macos/macos_impl.rs index fb9c2d680..937320f7d 100644 --- a/libs/enigo/src/macos/macos_impl.rs +++ b/libs/enigo/src/macos/macos_impl.rs @@ -163,6 +163,14 @@ impl Default for Enigo { } impl MouseControllable for Enigo { + fn as_any(&self) -> &dyn std::any::Any { + self + } + + fn as_mut_any(&mut self) -> &mut dyn std::any::Any { + self + } + fn mouse_move_to(&mut self, x: i32, y: i32) { let pressed = Self::pressed_buttons(); @@ -319,6 +327,14 @@ impl MouseControllable for Enigo { // com/questions/1918841/how-to-convert-ascii-character-to-cgkeycode impl KeyboardControllable for Enigo { + fn as_any(&self) -> &dyn std::any::Any { + self + } + + fn as_mut_any(&mut self) -> &mut dyn std::any::Any { + self + } + fn key_sequence(&mut self, sequence: &str) { // NOTE(dustin): This is a fix for issue https://github.com/enigo-rs/enigo/issues/68 // TODO(dustin): This could be improved by aggregating 20 bytes worth of graphemes at a time diff --git a/libs/enigo/src/win/win_impl.rs b/libs/enigo/src/win/win_impl.rs index e7512399e..4a4fd7fc4 100644 --- a/libs/enigo/src/win/win_impl.rs +++ b/libs/enigo/src/win/win_impl.rs @@ -105,6 +105,14 @@ fn get_error() -> String { } impl MouseControllable for Enigo { + fn as_any(&self) -> &dyn std::any::Any { + self + } + + fn as_mut_any(&mut self) -> &mut dyn std::any::Any { + self + } + fn mouse_move_to(&mut self, x: i32, y: i32) { mouse_event( MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_VIRTUALDESK, @@ -170,6 +178,14 @@ impl MouseControllable for Enigo { } impl KeyboardControllable for Enigo { + fn as_any(&self) -> &dyn std::any::Any { + self + } + + fn as_mut_any(&mut self) -> &mut dyn std::any::Any { + self + } + fn key_sequence(&mut self, sequence: &str) { let mut buffer = [0; 2]; From 98705bb759cc1637bec58db4dd85ea80639fc929 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Wed, 23 Nov 2022 21:31:37 +0800 Subject: [PATCH 1036/2015] feat: add armv7 arm64 libyuv compile --- .github/workflows/flutter-nightly.yml | 54 ++++++++++++++++++--------- 1 file changed, 37 insertions(+), 17 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 499a5f996..da9c921bf 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -5,6 +5,8 @@ on: # schedule build every night - cron: "0 0 * * *" workflow_dispatch: + # REMOVE ME ON PR + push: env: LLVM_VERSION: "10.0" @@ -199,9 +201,9 @@ jobs: fail-fast: false matrix: job: - # - { arch: armv7 , os: ubuntu-18.04} + - { arch: armv7 , os: ubuntu-18.04} - { arch: x86_64, os: ubuntu-18.04 } - # - { arch: aarch64 , os: ubuntu-18.04} + - { arch: aarch64 , os: ubuntu-18.04} steps: - name: Create vcpkg artifacts folder run: mkdir -p /opt/artifacts @@ -227,24 +229,42 @@ jobs: shell: /bin/bash install: | apt update -y - # CMake 3.15+ - apt install -y gpg wget ca-certificates - echo 'deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ bionic main' | tee /etc/apt/sources.list.d/kitware.list >/dev/null - wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/null | gpg --dearmor - | tee /usr/share/keyrings/kitware-archive-keyring.gpg >/dev/null - apt update -y - apt install -y curl zip unzip tar git cmake g++ gcc build-essential pkg-config wget nasm yasm ninja-build + case "${arch}" in + x86_64) + # CMake 3.15+ + apt install -y gpg wget ca-certificates + echo 'deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ bionic main' | tee /etc/apt/sources.list.d/kitware.list >/dev/null + wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/null | gpg --dearmor - | tee /usr/share/keyrings/kitware-archive-keyring.gpg >/dev/null + apt update -y + apt install -y curl zip unzip tar git cmake g++ gcc build-essential pkg-config wget nasm yasm ninja-build + ;; + aarch64|armv7) + apt install -y curl zip unzip tar git cmake g++ gcc build-essential pkg-config wget nasm yasm ninja-build + esac cmake --version gcc -v run: | - export VCPKG_FORCE_SYSTEM_BINARIES=1 - pushd /artifacts - git clone https://github.com/microsoft/vcpkg.git || true - git config --global --add safe.directory /artifacts/vcpkg || true - pushd vcpkg - git reset --hard ${{ env.VCPKG_COMMIT_ID }} - ./bootstrap-vcpkg.sh - ./vcpkg install libvpx libyuv opus - + case "${arch}" in + x86_64) + export VCPKG_FORCE_SYSTEM_BINARIES=1 + pushd /artifacts + git clone https://github.com/microsoft/vcpkg.git || true + git config --global --add safe.directory /artifacts/vcpkg || true + pushd vcpkg + git reset --hard ${{ env.VCPKG_COMMIT_ID }} + ./bootstrap-vcpkg.sh + ./vcpkg install libvpx libyuv opus + ;; + aarch64|armv7) + git clone https://chromium.googlesource.com/libyuv/libyuv + pushd libyuv + mkdir build + pushd build + mkdir -p /opt/artifacts/vcpkg/installed + cmake .. -DCMAKE_INSTALL_PREFIX=/opt/artifacts/vcpkg/installed + make -j4 && make install + ;; + esac - name: Upload artifacts uses: actions/upload-artifact@master with: From 1ab65563a43be9194b222487140877d87eef2b91 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Wed, 23 Nov 2022 23:18:03 +0800 Subject: [PATCH 1037/2015] fix: ci --- .github/workflows/flutter-nightly.yml | 159 +++++++++++++++++--------- 1 file changed, 105 insertions(+), 54 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index da9c921bf..3721c859d 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -229,22 +229,22 @@ jobs: shell: /bin/bash install: | apt update -y - case "${arch}" in + case "${{ matrix.job.arch }}" in x86_64) # CMake 3.15+ apt install -y gpg wget ca-certificates echo 'deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ bionic main' | tee /etc/apt/sources.list.d/kitware.list >/dev/null wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/null | gpg --dearmor - | tee /usr/share/keyrings/kitware-archive-keyring.gpg >/dev/null apt update -y - apt install -y curl zip unzip tar git cmake g++ gcc build-essential pkg-config wget nasm yasm ninja-build + apt install -y curl zip unzip tar git cmake g++ gcc build-essential pkg-config wget nasm yasm ninja-build libjpeg8-dev ;; aarch64|armv7) - apt install -y curl zip unzip tar git cmake g++ gcc build-essential pkg-config wget nasm yasm ninja-build + apt install -y curl zip unzip tar git cmake g++ gcc build-essential pkg-config wget nasm yasm ninja-build libjpeg8-dev esac cmake --version gcc -v run: | - case "${arch}" in + case "${{ matrix.job.arch }}" in x86_64) export VCPKG_FORCE_SYSTEM_BINARIES=1 pushd /artifacts @@ -256,12 +256,13 @@ jobs: ./vcpkg install libvpx libyuv opus ;; aarch64|armv7) - git clone https://chromium.googlesource.com/libyuv/libyuv + git clone https://chromium.googlesource.com/libyuv/libyuv || true pushd libyuv + git pull mkdir build pushd build - mkdir -p /opt/artifacts/vcpkg/installed - cmake .. -DCMAKE_INSTALL_PREFIX=/opt/artifacts/vcpkg/installed + mkdir -p /artifacts/vcpkg/installed + cmake .. -DCMAKE_INSTALL_PREFIX=/artifacts/vcpkg/installed make -j4 && make install ;; esac @@ -350,8 +351,8 @@ jobs: fail-fast: false matrix: job: - # - { arch: aarch64, target: aarch64-unknown-linux-gnu , os: ubuntu-18.04, use-cross: true, extra-build-features: "" } - # - { arch: aarch64, target: aarch64-unknown-linux-gnu , os: ubuntu-18.04, use-cross: true, extra-build-features: "flatpak" } + - { arch: aarch64, target: aarch64-unknown-linux-gnu , os: ubuntu-18.04, use-cross: true, extra-build-features: "" } + - { arch: aarch64, target: aarch64-unknown-linux-gnu , os: ubuntu-18.04, use-cross: true, extra-build-features: "flatpak" } # - { arch: armv7, target: arm-unknown-linux-gnueabihf , os: ubuntu-18.04, use-cross: true, extra-build-features: "" } # - { arch: armv7, target: arm-unknown-linux-gnueabihf , os: ubuntu-18.04, use-cross: true, extra-build-features: "flatpak" } # - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true } @@ -374,17 +375,17 @@ jobs: - name: Checkout source code uses: actions/checkout@v3 - - name: Install Rust toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - target: ${{ matrix.job.target }} - override: true - profile: minimal # minimal component installation (ie, no documentation) + # - name: Install Rust toolchain + # uses: actions-rs/toolchain@v1 + # with: + # toolchain: stable + # target: ${{ matrix.job.target }} + # override: true + # profile: minimal # minimal component installation (ie, no documentation) - - uses: Swatinem/rust-cache@v2 - with: - prefix-key: bridge-${{ matrix.job.os }} + # - uses: Swatinem/rust-cache@v2 + # with: + # prefix-key: bridge-${{ matrix.job.os }} - name: Disable rust bridge build run: | @@ -402,26 +403,56 @@ jobs: name: vcpkg-artifact-${{ matrix.job.arch }} path: /opt/artifacts/vcpkg/installed - - name: Output devs - run: | - ls -l ./ - tree -L 3 /opt/artifacts/vcpkg/installed - - - name: Install prerequisites - run: | - sudo apt update -y - case ${{ matrix.job.target }} in - x86_64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt install -y g++ gcc;; - arm-unknown-linux-*) sudo apt-get -y update ; sudo apt-get -y install gcc-arm-linux-gnueabihf ;; - aarch64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt-get -y install gcc-aarch64-linux-gnu ;; - esac - # common package - sudo apt install -y git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libvdpau-dev libva-dev libclang-dev llvm-dev libclang-10-dev llvm-10-dev pkg-config tree - - - name: Build rustdesk lib - run: | - export VCPKG_ROOT=/opt/artifacts/vcpkg - cargo build --lib --features hwcodec,flutter,${{ matrix.job.extra-build-features }} --release + - uses: Kingtous/run-on-arch-action@amd64-support + name: Build rustdesk library for ${{ matrix.job.arch }} + id: vcpkg + with: + arch: ${{ matrix.job.arch }} + distro: ubuntu18.04 + githubToken: ${{ github.token }} + setup: | + ls -l "${PWD}" + dockerRunArgs: | + --volume "${PWD}:/workspace" + --volume "/opt/artifacts:/opt/artifacts" + shell: /bin/bash + install: | + apt update -y + apt install -y -qq git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libvdpau-dev libva-dev libclang-dev llvm-dev libclang-10-dev llvm-10-dev pkg-config tree g++ gcc libvpx-dev libopus-dev tree + # output devs + ls -l ./ + tree -L 3 /opt/artifacts/vcpkg/installed + run: | + # disable git safe.directory + git config --global --add safe.directory "*" + # rust + pushd /opt + wget -O rust.tar.gz https://static.rust-lang.org/dist/rust-1.65.0-${{ matrix.job.target }}.tar.gz + tar -zxvf rust.tar.gz > /dev/null + cd rust-1.65.0-${{ matrix.job.target }} && ./install.sh + pushd /workspace + # mock + case "${{ matrix.job.arch }}" in + x86_64) + # no need mock on x86_64 + export VCPKG_ROOT=/opt/artifacts/vcpkg + ;; + aarch64) + cp -r /opt/artifacts/vcpkg/installed/* /usr + mkdir -p /vcpkg/installed/arm64-linux + ln -s /usr/lib /vcpkg/installed/arm64-linux/lib + ln -s /usr/include /vcpkg/installed/arm64-linux/include + export VCPKG_ROOT=/vcpkg + ;; + armv7) + cp -r /opt/artifacts/vcpkg/installed/* /usr + mkdir -p /vcpkg/installed/arm-linux + ln -s /usr/lib /vcpkg/installed/arm-linux/lib + ln -s /usr/include /vcpkg/installed/arm-linux/include + export VCPKG_ROOT=/vcpkg + ;; + esac + cargo build --lib --features hwcodec,flutter,${{ matrix.job.extra-build-features }} --release - name: Upload Artifacts uses: actions/upload-artifact@master @@ -437,8 +468,8 @@ jobs: fail-fast: false matrix: job: - # - { arch: aarch64, target: aarch64-unknown-linux-gnu , os: ubuntu-18.04, use-cross: true, extra-build-features: "" } - # - { arch: aarch64, target: aarch64-unknown-linux-gnu , os: ubuntu-18.04, use-cross: true, extra-build-features: "flatpak" } + - { arch: aarch64, target: aarch64-unknown-linux-gnu , os: ubuntu-18.04, use-cross: true, extra-build-features: "" } + - { arch: aarch64, target: aarch64-unknown-linux-gnu , os: ubuntu-18.04, use-cross: true, extra-build-features: "flatpak" } # - { arch: armv7, target: arm-unknown-linux-gnueabihf , os: ubuntu-18.04, use-cross: true, extra-build-features: "" } # - { arch: armv7, target: arm-unknown-linux-gnueabihf , os: ubuntu-18.04, use-cross: true, extra-build-features: "flatpak" } # - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true } @@ -470,7 +501,7 @@ jobs: - name: Prepare env run: | sudo apt update -y - sudo apt install -y git curl wget nasm yasm libgtk-3-dev + sudo apt install -y -qq git curl wget nasm yasm libgtk-3-dev mkdir -p ./target/release/ - name: Restore the rustdesk lib file @@ -494,7 +525,7 @@ jobs: shell: /bin/bash install: | apt update -y - apt install -y git cmake g++ gcc build-essential nasm yasm curl unzip xz-utils python3 wget pkg-config ninja-build pkg-config libgtk-3-dev liblzma-dev clang libappindicator3-dev + apt install -y -qq git cmake g++ gcc build-essential nasm yasm curl unzip xz-utils python3 wget pkg-config ninja-build pkg-config libgtk-3-dev liblzma-dev clang libappindicator3-dev run: | # disable git safe.directory git config --global --add safe.directory "*" @@ -579,16 +610,36 @@ jobs: files: | res/rustdesk*.zst - - name: Make RPM package - shell: bash - if: ${{ matrix.job.extra-build-features == '' }} - run: | - sudo apt install -y rpm - HBB=`pwd` rpmbuild ./res/rpm-flutter.spec -bb - pushd ~/rpmbuild/RPMS/${{ matrix.job.arch }} - for name in rustdesk*??.rpm; do - mv "$name" "${name%%.rpm}-fedora28-centos8.rpm" - done + - uses: Kingtous/run-on-arch-action@amd64-support + name: Build rustdesk rpm package for ${{ matrix.job.arch }} + id: rpm + with: + arch: ${{ matrix.job.arch }} + distro: ubuntu18.04 + githubToken: ${{ github.token }} + setup: | + ls -l "${PWD}" + dockerRunArgs: | + --volume "${PWD}:/workspace" + --volume "/opt/artifacts:/opt/artifacts" + shell: /bin/bash + install: | + apt update -y + apt install -y rpm + run: | + pushd /workspace + case ${{ matrix.job.arch }} + armv7) + sed -i "s/64bit/32bit/g" ./res/rpm-flutter.spec + ;; + esac + HBB=`pwd` rpmbuild ./res/rpm-flutter.spec -bb + pushd ~/rpmbuild/RPMS/${{ matrix.job.arch }} + mkdir -p /opt/artifacts/rpm + for name in rustdesk*??.rpm; do + mv "$name" "/opt/artifacts/rpm/${name%%.rpm}-fedora28-centos8.rpm" + done + - name: Publish fedora28/centos8 package if: ${{ matrix.job.extra-build-features == '' }} @@ -597,7 +648,7 @@ jobs: prerelease: true tag_name: ${{ env.TAG_NAME }} files: | - /home/runner/rpmbuild/RPMS/${{ matrix.job.arch }}/*.rpm + /opt/artifacts/rpm/*.rpm build-flatpak: name: Build Flatpak From 55004f915972cfbb8eee06e5966eb724e1ec12ae Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 25 Nov 2022 10:27:34 +0800 Subject: [PATCH 1038/2015] opt: arm specific meta opt: combine rpm package fix: rpm & PKGBUILD --- .github/workflows/flutter-nightly.yml | 703 ++++++++++++++--- Cargo.lock | 1006 ++++++++++++++----------- build.py | 2 +- 3 files changed, 1163 insertions(+), 548 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 3721c859d..79362647a 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -10,6 +10,7 @@ on: env: LLVM_VERSION: "10.0" + # Note: currently 3.0.5 does not support arm64 officially, we use latest stable version first. FLUTTER_VERSION: "3.0.5" TAG_NAME: "nightly" # vcpkg version: 2022.05.10 @@ -201,9 +202,9 @@ jobs: fail-fast: false matrix: job: - - { arch: armv7 , os: ubuntu-18.04} - - { arch: x86_64, os: ubuntu-18.04 } - - { arch: aarch64 , os: ubuntu-18.04} + # - { arch: armv7, os: ubuntu-20.04 } + - { arch: x86_64, os: ubuntu-20.04 } + - { arch: aarch64, os: ubuntu-20.04 } steps: - name: Create vcpkg artifacts folder run: mkdir -p /opt/artifacts @@ -239,7 +240,7 @@ jobs: apt install -y curl zip unzip tar git cmake g++ gcc build-essential pkg-config wget nasm yasm ninja-build libjpeg8-dev ;; aarch64|armv7) - apt install -y curl zip unzip tar git cmake g++ gcc build-essential pkg-config wget nasm yasm ninja-build libjpeg8-dev + apt install -y curl zip unzip tar git cmake g++ gcc build-essential pkg-config wget nasm yasm ninja-build libjpeg8-dev automake libtool esac cmake --version gcc -v @@ -256,14 +257,24 @@ jobs: ./vcpkg install libvpx libyuv opus ;; aarch64|armv7) + pushd /artifacts + # libyuv git clone https://chromium.googlesource.com/libyuv/libyuv || true pushd libyuv git pull - mkdir build + mkdir -p build pushd build mkdir -p /artifacts/vcpkg/installed cmake .. -DCMAKE_INSTALL_PREFIX=/artifacts/vcpkg/installed make -j4 && make install + popd + popd + # libopus, ubuntu 18.04 prebuilt is not be compiled with -fPIC + wget -O opus.tar.gz http://archive.ubuntu.com/ubuntu/pool/main/o/opus/opus_1.1.2.orig.tar.gz + tar -zxvf opus.tar.gz; ls -l + pushd opus-1.1.2 + ./autogen.sh; ./configure --prefix=/artifacts/vcpkg/installed + make -j4; make install ;; esac - name: Upload artifacts @@ -343,53 +354,93 @@ jobs: ./flutter/lib/generated_bridge.dart ./flutter/lib/generated_bridge.freezed.dart - build-rustdesk-lib-linux: + build-rustdesk-lib-linux-amd64: needs: [generate-bridge-linux, build-vcpkg-deps-linux] name: build-rust-lib ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] runs-on: ${{ matrix.job.os }} strategy: fail-fast: false matrix: + # use a high level qemu-user-static job: - - { arch: aarch64, target: aarch64-unknown-linux-gnu , os: ubuntu-18.04, use-cross: true, extra-build-features: "" } - - { arch: aarch64, target: aarch64-unknown-linux-gnu , os: ubuntu-18.04, use-cross: true, extra-build-features: "flatpak" } - # - { arch: armv7, target: arm-unknown-linux-gnueabihf , os: ubuntu-18.04, use-cross: true, extra-build-features: "" } - # - { arch: armv7, target: arm-unknown-linux-gnueabihf , os: ubuntu-18.04, use-cross: true, extra-build-features: "flatpak" } - # - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true } # - { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } # - { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } - { arch: x86_64, target: x86_64-unknown-linux-gnu, - os: ubuntu-18.04, + os: ubuntu-20.04, extra-build-features: "", } - { arch: x86_64, target: x86_64-unknown-linux-gnu, - os: ubuntu-18.04, + os: ubuntu-20.04, extra-build-features: "flatpak", } # - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } steps: + - name: Maximize build space + run: | + sudo rm -rf /opt/ghc + sudo rm -rf /usr/local/lib/android + sudo rm -rf /usr/share/dotnet + sudo apt update -y + sudo apt install qemu-user-static + - name: Checkout source code uses: actions/checkout@v3 - # - name: Install Rust toolchain - # uses: actions-rs/toolchain@v1 - # with: - # toolchain: stable - # target: ${{ matrix.job.target }} - # override: true - # profile: minimal # minimal component installation (ie, no documentation) + - name: Set Swap Space + uses: pierotofy/set-swap-space@master + with: + swap-size-gb: 12 - # - uses: Swatinem/rust-cache@v2 - # with: - # prefix-key: bridge-${{ matrix.job.os }} + - name: Free Space + run: | + df + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: ${{ matrix.job.target }} + override: true + profile: minimal # minimal component installation (ie, no documentation) + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: rustdesk-lib-cache + key: ${{ matrix.job.target }}-${{ matrix.job.extra-build-features }} + cache-directories: "/opt/rust-registry" + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: ${{ matrix.job.target }} + override: true + profile: minimal # minimal component installation (ie, no documentation) + + - name: Install local registry + run: | + mkdir -p /opt/rust-registry + cargo install cargo-local-registry + + - name: Build local registry + uses: nick-fields/retry@v2 + id: build-local-registry + continue-on-error: true + with: + max_attempts: 3 + timeout_minutes: 15 + retry_on: error + command: cargo local-registry --sync ./Cargo.lock /opt/rust-registry - name: Disable rust bridge build run: | sed -i "s/gen_flutter_rust_bridge();/\/\//g" build.rs + # only build cdylib + sed -i "s/\[\"cdylib\", \"staticlib\", \"rlib\"\]/\[\"cdylib\"\]/g" Cargo.toml - name: Restore bridge files uses: actions/download-artifact@master @@ -409,16 +460,23 @@ jobs: with: arch: ${{ matrix.job.arch }} distro: ubuntu18.04 + # not ready yet + # distro: ubuntu18.04-rustdesk githubToken: ${{ github.token }} setup: | ls -l "${PWD}" + ls -l /opt/artifacts/vcpkg/installed dockerRunArgs: | --volume "${PWD}:/workspace" --volume "/opt/artifacts:/opt/artifacts" + --volume "/opt/rust-registry:/opt/rust-registry" shell: /bin/bash install: | apt update -y - apt install -y -qq git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libvdpau-dev libva-dev libclang-dev llvm-dev libclang-10-dev llvm-10-dev pkg-config tree g++ gcc libvpx-dev libopus-dev tree + echo -e "installing deps" + apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libvdpau-dev libva-dev libclang-dev llvm-dev libclang-10-dev llvm-10-dev pkg-config tree g++ gcc libvpx-dev tree > /dev/null + # we have libopus compiled by us. + apt remove -y libopus-dev || true # output devs ls -l ./ tree -L 3 /opt/artifacts/vcpkg/installed @@ -427,32 +485,31 @@ jobs: git config --global --add safe.directory "*" # rust pushd /opt - wget -O rust.tar.gz https://static.rust-lang.org/dist/rust-1.65.0-${{ matrix.job.target }}.tar.gz - tar -zxvf rust.tar.gz > /dev/null - cd rust-1.65.0-${{ matrix.job.target }} && ./install.sh + wget -O rust.tar.gz https://static.rust-lang.org/dist/rust-1.64.0-${{ matrix.job.target }}.tar.gz + tar -zxvf rust.tar.gz > /dev/null && rm rust.tar.gz + cd rust-1.64.0-${{ matrix.job.target }} && ./install.sh + rm -rf rust-1.64.0-${{ matrix.job.target }} + # edit config + mkdir -p ~/.cargo/ + echo """ + [source.crates-io] + registry = 'https://github.com/rust-lang/crates.io-index' + replace-with = 'local-registry' + + [source.local-registry] + local-registry = '/opt/rust-registry/' + """ > ~/.cargo/config + cat ~/.cargo/config + # start build pushd /workspace # mock case "${{ matrix.job.arch }}" in x86_64) # no need mock on x86_64 export VCPKG_ROOT=/opt/artifacts/vcpkg - ;; - aarch64) - cp -r /opt/artifacts/vcpkg/installed/* /usr - mkdir -p /vcpkg/installed/arm64-linux - ln -s /usr/lib /vcpkg/installed/arm64-linux/lib - ln -s /usr/include /vcpkg/installed/arm64-linux/include - export VCPKG_ROOT=/vcpkg - ;; - armv7) - cp -r /opt/artifacts/vcpkg/installed/* /usr - mkdir -p /vcpkg/installed/arm-linux - ln -s /usr/lib /vcpkg/installed/arm-linux/lib - ln -s /usr/include /vcpkg/installed/arm-linux/include - export VCPKG_ROOT=/vcpkg + cargo build --lib --features hwcodec,flutter,${{ matrix.job.extra-build-features }} --release ;; esac - cargo build --lib --features hwcodec,flutter,${{ matrix.job.extra-build-features }} --release - name: Upload Artifacts uses: actions/upload-artifact@master @@ -460,31 +517,410 @@ jobs: name: librustdesk-${{ matrix.job.arch }}-${{ matrix.job.extra-build-features }}.so path: target/release/liblibrustdesk.so - build-rustdesk-linux: - needs: [build-rustdesk-lib-linux] - name: build-rustdesk ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-args }}] + build-rustdesk-lib-linux-arm: + needs: [generate-bridge-linux, build-vcpkg-deps-linux] + name: build-rust-lib ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: false + matrix: + # use a high level qemu-user-static + job: + - { + arch: aarch64, + target: aarch64-unknown-linux-gnu, + os: ubuntu-20.04, + use-cross: true, + extra-build-features: "", + } + - { arch: aarch64, target: aarch64-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true, extra-build-features: "flatpak" } + # - { + # arch: armv7, + # target: arm-unknown-linux-gnueabihf, + # os: ubuntu-20.04, + # use-cross: true, + # extra-build-features: "", + # } + # - { arch: armv7, target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true, extra-build-features: "flatpak" } + # - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true } + steps: + - name: Maximize build space + run: | + sudo rm -rf /opt/ghc + sudo rm -rf /usr/local/lib/android + sudo rm -rf /usr/share/dotnet + sudo apt update -y + sudo apt install qemu-user-static + + - name: Checkout source code + uses: actions/checkout@v3 + + - name: Set Swap Space + uses: pierotofy/set-swap-space@master + with: + swap-size-gb: 12 + + - name: Free Space + run: | + df + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: ${{ matrix.job.target }} + override: true + profile: minimal # minimal component installation (ie, no documentation) + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: ${{ matrix.job.target }} + override: true + profile: minimal # minimal component installation (ie, no documentation) + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: rustdesk-lib-cache + key: ${{ matrix.job.target }}-${{ matrix.job.extra-build-features }} + cache-directories: "/opt/rust-registry" + + + - name: Install local registry + run: | + mkdir -p /opt/rust-registry + cargo install cargo-local-registry + + - name: Build local registry + uses: nick-fields/retry@v2 + id: build-local-registry + continue-on-error: true + with: + max_attempts: 3 + timeout_minutes: 15 + retry_on: error + command: cargo local-registry --sync ./Cargo.lock /opt/rust-registry + + - name: Disable rust bridge build + run: | + sed -i "s/gen_flutter_rust_bridge();/\/\//g" build.rs + # only build cdylib + sed -i "s/\[\"cdylib\", \"staticlib\", \"rlib\"\]/\[\"cdylib\"\]/g" Cargo.toml + + - name: Restore bridge files + uses: actions/download-artifact@master + with: + name: bridge-artifact + path: ./ + + - name: Restore vcpkg files + uses: actions/download-artifact@master + with: + name: vcpkg-artifact-${{ matrix.job.arch }} + path: /opt/artifacts/vcpkg/installed + + - uses: Kingtous/run-on-arch-action@amd64-support + name: Build rustdesk library for ${{ matrix.job.arch }} + id: vcpkg + with: + arch: ${{ matrix.job.arch }} + distro: ubuntu18.04-rustdesk + githubToken: ${{ github.token }} + setup: | + ls -l "${PWD}" + ls -l /opt/artifacts/vcpkg/installed + dockerRunArgs: | + --volume "${PWD}:/workspace" + --volume "/opt/artifacts:/opt/artifacts" + --volume "/opt/rust-registry:/opt/rust-registry" + shell: /bin/bash + install: | + apt update -y + echo -e "installing deps" + apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libvdpau-dev libva-dev libclang-dev llvm-dev libclang-10-dev llvm-10-dev pkg-config tree g++ gcc libvpx-dev tree > /dev/null + # we have libopus compiled by us. + apt remove -y libopus-dev || true + # output devs + ls -l ./ + tree -L 3 /opt/artifacts/vcpkg/installed + run: | + # disable git safe.directory + git config --global --add safe.directory "*" + # rust + pushd /opt + wget -O rust.tar.gz https://static.rust-lang.org/dist/rust-1.64.0-${{ matrix.job.target }}.tar.gz + tar -zxvf rust.tar.gz > /dev/null && rm rust.tar.gz + cd rust-1.64.0-${{ matrix.job.target }} && ./install.sh + rm -rf rust-1.64.0-${{ matrix.job.target }} + # edit config + mkdir -p ~/.cargo/ + echo """ + [source.crates-io] + registry = 'https://github.com/rust-lang/crates.io-index' + replace-with = 'local-registry' + + [source.local-registry] + local-registry = '/opt/rust-registry/' + """ > ~/.cargo/config + cat ~/.cargo/config + # start build + pushd /workspace + # mock + case "${{ matrix.job.arch }}" in + aarch64) + cp -r /opt/artifacts/vcpkg/installed/lib/* /usr/lib/aarch64-linux-gnu/ + cp -r /opt/artifacts/vcpkg/installed/include/* /usr/include/ + ls -l /opt/artifacts/vcpkg/installed/lib/ + mkdir -p /vcpkg/installed/arm64-linux + ln -s /usr/lib/aarch64-linux-gnu /vcpkg/installed/arm64-linux/lib + ln -s /usr/include /vcpkg/installed/arm64-linux/include + export VCPKG_ROOT=/vcpkg + # disable hwcodec for compilation + cargo build --lib --features flutter,${{ matrix.job.extra-build-features }} --release + ;; + armv7) + cp -r /opt/artifacts/vcpkg/installed/lib/* /usr/lib/arm-linux-gnueabihf/ + cp -r /opt/artifacts/vcpkg/installed/include/* /usr/include/ + mkdir -p /vcpkg/installed/arm-linux + ln -s /usr/lib/arm-linux-gnueabihf /vcpkg/installed/arm-linux/lib + ln -s /usr/include /vcpkg/installed/arm-linux/include + export VCPKG_ROOT=/vcpkg + # disable hwcodec for compilation + cargo build --lib --features flutter,${{ matrix.job.extra-build-features }} --release + ;; + esac + + - name: Upload Artifacts + uses: actions/upload-artifact@master + with: + name: librustdesk-${{ matrix.job.arch }}-${{ matrix.job.extra-build-features }}.so + path: target/release/liblibrustdesk.so + + build-rustdesk-linux-arm: + needs: [build-rustdesk-lib-linux-arm] + name: build-rustdesk ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] runs-on: ${{ matrix.job.os }} strategy: fail-fast: false matrix: job: - - { arch: aarch64, target: aarch64-unknown-linux-gnu , os: ubuntu-18.04, use-cross: true, extra-build-features: "" } - - { arch: aarch64, target: aarch64-unknown-linux-gnu , os: ubuntu-18.04, use-cross: true, extra-build-features: "flatpak" } - # - { arch: armv7, target: arm-unknown-linux-gnueabihf , os: ubuntu-18.04, use-cross: true, extra-build-features: "" } - # - { arch: armv7, target: arm-unknown-linux-gnueabihf , os: ubuntu-18.04, use-cross: true, extra-build-features: "flatpak" } + - { + arch: aarch64, + target: aarch64-unknown-linux-gnu, + os: ubuntu-20.04, + use-cross: true, + extra-build-features: "", + } + - { + arch: aarch64, + target: aarch64-unknown-linux-gnu, + os: ubuntu-20.04, + use-cross: true, + extra-build-features: "flatpak", + } + # - { arch: armv7, target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true, extra-build-features: "" } + # - { arch: armv7, target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true, extra-build-features: "flatpak" } # - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true } + steps: + - name: Checkout source code + uses: actions/checkout@v3 + + - name: Restore bridge files + uses: actions/download-artifact@master + with: + name: bridge-artifact + path: ./ + + - name: Prepare env + run: | + sudo apt update -y + sudo apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev + mkdir -p ./target/release/ + + - name: Restore the rustdesk lib file + uses: actions/download-artifact@master + with: + name: librustdesk-${{ matrix.job.arch }}-${{ matrix.job.extra-build-features }}.so + path: ./target/release/ + + - name: Download Flutter + shell: bash + run: | + pushd /opt + # Currently 3.0.5 does not support arm build + git clone https://github.com/flutter/flutter.git -b stable || true + pushd flutter + git fetch origin && git reset --hard origin/stable + # TODO: `flutter_improved_scrolling` needs to be revised to support arm64 trackpad. + # sed xxx + + - uses: Kingtous/run-on-arch-action@amd64-support + name: Build rustdesk binary for ${{ matrix.job.arch }} + id: vcpkg + with: + arch: ${{ matrix.job.arch }} + distro: ubuntu18.04-rustdesk + githubToken: ${{ github.token }} + setup: | + ls -l "${PWD}" + dockerRunArgs: | + --volume "${PWD}:/workspace" + --volume "/opt/artifacts:/opt/artifacts" + --volume "/opt/flutter:/opt/flutter" + shell: /bin/bash + install: | + apt update -y + apt-get -qq install -y git cmake g++ gcc build-essential nasm yasm curl unzip xz-utils python3 wget pkg-config ninja-build pkg-config libgtk-3-dev liblzma-dev clang libappindicator3-dev rpm + run: | + # disable git safe.directory + git config --global --add safe.directory "*" + # Setup Flutter + export PATH=/opt/flutter/bin:$PATH + flutter doctor -v + flutter precache + pushd /workspace + # edit to arm64 + case ${{ matrix.job.arch }} in + aarch64) + sed -i "s/Architecture: amd64/Architecture: arm64/g" ./build.py + sed -i "s/x64\/release/arm64\/release/g" ./build.py + ;; + armv7) + sed -i "s/Architecture: amd64/Architecture: arm/g" ./build.py + sed -i "s/x64\/release/arm\/release/g" ./build.py + ;; + esac + python3 ./build.py --flutter --hwcodec --skip-cargo + # rpm package + echo -e "start packaging" + pushd /workspace + case ${{ matrix.job.arch }} in + armv7) + sed -i "s/64bit/32bit/g" ./res/rpm-flutter.spec + sed -i "s/linux\/x64/linux\/arm/g" ./res/rpm-flutter.spec + ;; + aarch64) + sed -i "s/linux\/x64/linux\/arm64/g" ./res/rpm-flutter.spec + ;; + esac + HBB=`pwd` rpmbuild ./res/rpm-flutter.spec -bb + pushd ~/rpmbuild/RPMS/${{ matrix.job.arch }} + mkdir -p /opt/artifacts/rpm + for name in rustdesk*??.rpm; do + mv "$name" "/opt/artifacts/rpm/${name%%.rpm}-fedora28-centos8.rpm" + done + + + - name: Rename rustdesk + shell: bash + run: | + for name in rustdesk*??.deb; do + mv "$name" "${name%%.deb}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb" + done + + - name: Publish debian package + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb + + - name: Upload Artifcat + uses: actions/upload-artifact@master + if: ${{ contains(matrix.job.extra-build-features, 'flatpak') }} + with: + name: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb + path: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb + + - name: Patch archlinux PKGBUILD + if: ${{ matrix.job.extra-build-features == '' }} + run: | + sed -i "s/arch=('x86_64')/arch=('${{ matrix.job.arch }}')/g" res/PKGBUILD + case ${{ matrix.job.arch }} in + armv7) + sed -i "s/linux\/x64/linux\/arm/g" ./res/PKGBUILD + ;; + aarch64) + sed -i "s/linux\/x64/linux\/arm64/g" ./res/PKGBUILD + ;; + esac + + # Temporary disable for there is no many archlinux arm hosts + # - name: Build archlinux package + # if: ${{ matrix.job.extra-build-features == '' }} + # uses: vufa/arch-makepkg-action@master + # with: + # packages: > + # llvm + # clang + # libva + # libvdpau + # rust + # gstreamer + # unzip + # git + # cmake + # gcc + # curl + # wget + # yasm + # nasm + # zip + # make + # pkg-config + # clang + # gtk3 + # xdotool + # libxcb + # libxfixes + # alsa-lib + # pipewire + # python + # ttf-arphic-uming + # libappindicator-gtk3 + # scripts: | + # cd res && HBB=`pwd`/.. FLUTTER=1 makepkg -f + + # - name: Publish archlinux package + # if: ${{ matrix.job.extra-build-features == '' }} + # uses: softprops/action-gh-release@v1 + # with: + # prerelease: true + # tag_name: ${{ env.TAG_NAME }} + # files: | + # res/rustdesk*.zst + + - name: Publish fedora28/centos8 package + if: ${{ matrix.job.extra-build-features == '' }} + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + /opt/artifacts/rpm/*.rpm + + build-rustdesk-linux-amd64: + needs: [build-rustdesk-lib-linux-amd64] + name: build-rustdesk ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: false + matrix: + job: # - { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } # - { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } - { arch: x86_64, target: x86_64-unknown-linux-gnu, - os: ubuntu-18.04, + os: ubuntu-20.04, extra-build-features: "", } - { arch: x86_64, target: x86_64-unknown-linux-gnu, - os: ubuntu-18.04, + os: ubuntu-20.04, extra-build-features: "flatpak", } # - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } @@ -501,7 +937,7 @@ jobs: - name: Prepare env run: | sudo apt update -y - sudo apt install -y -qq git curl wget nasm yasm libgtk-3-dev + sudo apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev mkdir -p ./target/release/ - name: Restore the rustdesk lib file @@ -525,7 +961,7 @@ jobs: shell: /bin/bash install: | apt update -y - apt install -y -qq git cmake g++ gcc build-essential nasm yasm curl unzip xz-utils python3 wget pkg-config ninja-build pkg-config libgtk-3-dev liblzma-dev clang libappindicator3-dev + apt-get -qq install -y git cmake g++ gcc build-essential nasm yasm curl unzip xz-utils python3 wget pkg-config ninja-build pkg-config libgtk-3-dev liblzma-dev clang libappindicator3-dev rpm run: | # disable git safe.directory git config --global --add safe.directory "*" @@ -538,6 +974,19 @@ jobs: flutter doctor -v pushd /workspace python3 ./build.py --flutter --hwcodec --skip-cargo + # rpm package + pushd /workspace + case ${{ matrix.job.arch }} in + armv7) + sed -i "s/64bit/32bit/g" ./res/rpm-flutter.spec + ;; + esac + HBB=`pwd` rpmbuild ./res/rpm-flutter.spec -bb + pushd ~/rpmbuild/RPMS/${{ matrix.job.arch }} + mkdir -p /opt/artifacts/rpm + for name in rustdesk*??.rpm; do + mv "$name" "/opt/artifacts/rpm/${name%%.rpm}-fedora28-centos8.rpm" + done - name: Rename rustdesk shell: bash @@ -610,37 +1059,6 @@ jobs: files: | res/rustdesk*.zst - - uses: Kingtous/run-on-arch-action@amd64-support - name: Build rustdesk rpm package for ${{ matrix.job.arch }} - id: rpm - with: - arch: ${{ matrix.job.arch }} - distro: ubuntu18.04 - githubToken: ${{ github.token }} - setup: | - ls -l "${PWD}" - dockerRunArgs: | - --volume "${PWD}:/workspace" - --volume "/opt/artifacts:/opt/artifacts" - shell: /bin/bash - install: | - apt update -y - apt install -y rpm - run: | - pushd /workspace - case ${{ matrix.job.arch }} - armv7) - sed -i "s/64bit/32bit/g" ./res/rpm-flutter.spec - ;; - esac - HBB=`pwd` rpmbuild ./res/rpm-flutter.spec -bb - pushd ~/rpmbuild/RPMS/${{ matrix.job.arch }} - mkdir -p /opt/artifacts/rpm - for name in rustdesk*??.rpm; do - mv "$name" "/opt/artifacts/rpm/${name%%.rpm}-fedora28-centos8.rpm" - done - - - name: Publish fedora28/centos8 package if: ${{ matrix.job.extra-build-features == '' }} uses: softprops/action-gh-release@v1 @@ -650,25 +1068,83 @@ jobs: files: | /opt/artifacts/rpm/*.rpm - build-flatpak: + # Temporary disable flatpak arm build + # + # build-flatpak-arm: + # name: Build Flatpak + # needs: [build-rustdesk-linux-arm] + # runs-on: ${{ matrix.job.os }} + # strategy: + # fail-fast: false + # matrix: + # job: + # # - { target: aarch64-unknown-linux-gnu , os: ubuntu-18.04, arch: arm64 } + # - { target: aarch64-unknown-linux-gnu, os: ubuntu-20.04, arch: arm64 } + # steps: + # - name: Checkout source code + # uses: actions/checkout@v3 + + # - name: Download Binary + # uses: actions/download-artifact@master + # with: + # name: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb + # path: . + + # - name: Rename Binary + # run: | + # mv rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb rustdesk-${{ env.VERSION }}.deb + + # - uses: Kingtous/run-on-arch-action@amd64-support + # name: Build rustdesk flatpak package for ${{ matrix.job.arch }} + # id: rpm + # with: + # arch: ${{ matrix.job.arch }} + # distro: ubuntu18.04 + # githubToken: ${{ github.token }} + # setup: | + # ls -l "${PWD}" + # dockerRunArgs: | + # --volume "${PWD}:/workspace" + # shell: /bin/bash + # install: | + # apt update -y + # apt install -y rpm + # run: | + # pushd /workspace + # # install + # apt update -y + # apt install -y flatpak flatpak-builder cmake g++ gcc git curl wget nasm yasm libgtk-3-dev git + # # flatpak deps + # flatpak --user remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo + # flatpak --user install -y flathub org.freedesktop.Platform/${{ matrix.job.arch }}/21.08 + # flatpak --user install -y flathub org.freedesktop.Sdk/${{ matrix.job.arch }}/21.08 + # # package + # pushd flatpak + # git clone https://github.com/flathub/shared-modules.git --depth=1 + # flatpak-builder --user --force-clean --repo=repo ./build ./rustdesk.json + # flatpak build-bundle ./repo rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}.flatpak org.rustdesk.rustdesk + + # - name: Publish flatpak package + # uses: softprops/action-gh-release@v1 + # with: + # prerelease: true + # tag_name: ${{ env.TAG_NAME }} + # files: | + # flatpak/rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}.flatpak + + build-flatpak-amd64: name: Build Flatpak - needs: [build-rustdesk-linux] + needs: [build-rustdesk-linux-amd64] runs-on: ${{ matrix.job.os }} strategy: fail-fast: false matrix: job: - # - { target: aarch64-unknown-linux-gnu , os: ubuntu-18.04, use-cross: true, arch: arm64 } - - { target: x86_64-unknown-linux-gnu, os: ubuntu-18.04, arch: x86_64 } + - { target: x86_64-unknown-linux-gnu, os: ubuntu-20.04, arch: x86_64 } steps: - name: Checkout source code uses: actions/checkout@v3 - - name: Install dependencies - run: | - sudo apt update - sudo apt install -y flatpak flatpak-builder cmake g++ gcc git curl wget nasm yasm libgtk-3-dev - - name: Download Binary uses: actions/download-artifact@master with: @@ -679,18 +1155,35 @@ jobs: run: | mv rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb rustdesk-${{ env.VERSION }}.deb - - name: Install Flatpak deps - run: | - flatpak --user remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo - flatpak --user install -y flathub org.freedesktop.Platform/${{ matrix.job.arch }}/21.08 - flatpak --user install -y flathub org.freedesktop.Sdk/${{ matrix.job.arch }}/21.08 - - - name: Make Flatpak package - run: | - pushd flatpak - git clone https://github.com/flathub/shared-modules.git --depth=1 - flatpak-builder --user --force-clean --repo=repo ./build ./rustdesk.json - flatpak build-bundle ./repo rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}.flatpak org.rustdesk.rustdesk + - uses: Kingtous/run-on-arch-action@amd64-support + name: Build rustdesk flatpak package for ${{ matrix.job.arch }} + id: rpm + with: + arch: ${{ matrix.job.arch }} + distro: ubuntu18.04 + githubToken: ${{ github.token }} + setup: | + ls -l "${PWD}" + dockerRunArgs: | + --volume "${PWD}:/workspace" + shell: /bin/bash + install: | + apt update -y + apt install -y rpm + run: | + pushd /workspace + # install + apt update -y + apt install -y flatpak flatpak-builder cmake g++ gcc git curl wget nasm yasm libgtk-3-dev git + # flatpak deps + flatpak --user remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo + flatpak --user install -y flathub org.freedesktop.Platform/${{ matrix.job.arch }}/21.08 + flatpak --user install -y flathub org.freedesktop.Sdk/${{ matrix.job.arch }}/21.08 + # package + pushd flatpak + git clone https://github.com/flathub/shared-modules.git --depth=1 + flatpak-builder --user --force-clean --repo=repo ./build ./rustdesk.json + flatpak build-bundle ./repo rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}.flatpak org.rustdesk.rustdesk - name: Publish flatpak package uses: softprops/action-gh-release@v1 diff --git a/Cargo.lock b/Cargo.lock index 09485efa6..529be08d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,12 +23,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" -[[package]] -name = "ahash" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "739f4a8db6605981345c5654f3a85b056ce52f37a39d34da03f25bf2151ea16e" - [[package]] name = "ahash" version = "0.7.6" @@ -42,9 +36,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "0.7.18" +version = "0.7.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" dependencies = [ "memchr", ] @@ -120,16 +114,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e9dd62f37dea550caf48c77591dc50bd1a378ce08855be1a0c42a97b7550fb" dependencies = [ "android_log-sys", - "env_logger 0.9.0", + "env_logger 0.9.3", "log", "once_cell", ] [[package]] name = "android_system_properties" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7ed72e1635e121ca3e79420540282af22da58be50de153d36f81ddc6b83aa9e" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" dependencies = [ "libc", ] @@ -145,9 +139,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.62" +version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1485d4d2cc45e7b201ee3767015c96faa5904387c9d87c6efdd0fb511f12d305" +checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6" [[package]] name = "arboard" @@ -185,52 +179,53 @@ version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e14485364214912d3b19cc3435dde4df66065127f05fa0d75c712f36f12c2f28" dependencies = [ - "concurrent-queue", + "concurrent-queue 1.2.4", "event-listener", "futures-core", ] [[package]] name = "async-executor" -version = "1.4.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "871f9bb5e0a22eeb7e8cf16641feb87c9dc67032ccf8ff49e772eb9941d3a965" +checksum = "17adb73da160dfb475c183343c8cccd80721ea5a605d3eb57125f0a7b7a92d0b" dependencies = [ + "async-lock", "async-task", - "concurrent-queue", + "concurrent-queue 2.0.0", "fastrand", "futures-lite", - "once_cell", "slab", ] [[package]] name = "async-io" -version = "1.8.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ab006897723d9352f63e2b13047177c3982d8d79709d713ce7747a8f19fd1b0" +checksum = "e8121296a9f05be7f34aa4196b1747243b3b62e048bb7906f644f3fbfc490cf7" dependencies = [ + "async-lock", "autocfg 1.1.0", - "concurrent-queue", + "concurrent-queue 1.2.4", "futures-lite", "libc", "log", - "once_cell", "parking", "polling", "slab", - "socket2 0.4.6", + "socket2 0.4.7", "waker-fn", "winapi 0.3.9", ] [[package]] name = "async-lock" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e97a171d191782fba31bb902b14ad94e24a68145032b7eedf871ab0bc0d077b6" +checksum = "c8101efe8695a6c17e02911402145357e718ac92d3ff88ae8419e84b1707b685" dependencies = [ "event-listener", + "futures-lite", ] [[package]] @@ -253,9 +248,9 @@ dependencies = [ [[package]] name = "async-recursion" -version = "0.3.2" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7d78656ba01f1b93024b7c3a0467f1608e4be67d725749fdcd7d2c7678fd7a2" +checksum = "2cda8f4bcc10624c4e85bc66b3f452cca98cfa5ca002dc83a16aad2367641bea" dependencies = [ "proc-macro2", "quote", @@ -270,9 +265,9 @@ checksum = "7a40729d2133846d9ed0ea60a8b9541bccddab49cd30f0715a1da672fe9a2524" [[package]] name = "async-trait" -version = "0.1.57" +version = "0.1.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76464446b8bc32758d7e88ee1a804d9914cd9b1cb264c029899680b0be29826f" +checksum = "1e805d94e6b5001b651426cf4cd446b1ab5f319d27bab5c644f61de0a804360c" dependencies = [ "proc-macro2", "quote", @@ -300,7 +295,7 @@ dependencies = [ "glib-sys 0.15.10", "gobject-sys 0.15.10", "libc", - "system-deps 6.0.2", + "system-deps 6.0.3", ] [[package]] @@ -354,16 +349,16 @@ dependencies = [ "cc", "cfg-if 1.0.0", "libc", - "miniz_oxide 0.5.3", + "miniz_oxide 0.5.4", "object", "rustc-demangle", ] [[package]] name = "base64" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "bindgen" @@ -375,7 +370,7 @@ dependencies = [ "cexpr", "clang-sys", "clap 2.34.0", - "env_logger 0.9.0", + "env_logger 0.9.3", "lazy_static", "lazycell", "log", @@ -385,7 +380,27 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "which 4.2.5", + "which 4.3.0", +] + +[[package]] +name = "bindgen" +version = "0.61.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a022e58a142a46fea340d68012b9201c094e93ec3d033a944a24f8fd4a4f09a" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "peeking_take_while", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", ] [[package]] @@ -414,9 +429,9 @@ checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" [[package]] name = "block-buffer" -version = "0.10.2" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" +checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" dependencies = [ "generic-array", ] @@ -458,15 +473,15 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.11.0" +version = "3.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d" +checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" [[package]] name = "bytemuck" -version = "1.12.1" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f5715e491b5a1598fc2bef5a606847b5dc1d48ea625bd3c02c00de8285591da" +checksum = "aaa3a8d9a1ca92e282c96a32d6511b695d7d994d1d102ba85d279f9b2756947f" [[package]] name = "byteorder" @@ -476,11 +491,11 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" -version = "1.2.1" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db" +checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c" dependencies = [ - "serde 1.0.144", + "serde 1.0.147", ] [[package]] @@ -510,7 +525,7 @@ checksum = "3c55d429bef56ac9172d25fecb85dc8068307d17acd74b377866b7a1ef25d3c8" dependencies = [ "glib-sys 0.15.10", "libc", - "system-deps 6.0.2", + "system-deps 6.0.3", ] [[package]] @@ -529,7 +544,7 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88ad0e1e3e88dd237a156ab9f571021b8a158caa0ae44b1968a241efb5144c1e" dependencies = [ - "serde 1.0.144", + "serde 1.0.147", ] [[package]] @@ -538,7 +553,7 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbdb825da8a5df079a43676dbe042702f1707b1109f713a01420fbb4cc71fa27" dependencies = [ - "serde 1.0.144", + "serde 1.0.147", ] [[package]] @@ -549,9 +564,9 @@ checksum = "4acbb09d9ee8e23699b9634375c72795d095bf268439da88562cf9b501f181fa" dependencies = [ "camino", "cargo-platform", - "semver 1.0.13", - "serde 1.0.144", - "serde_json 1.0.85", + "semver 1.0.14", + "serde 1.0.147", + "serde_json 1.0.89", ] [[package]] @@ -560,14 +575,14 @@ version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b6d248e3ca02f3fbfabcb9284464c596baec223a26d91bbf44a5a62ddb0d900" dependencies = [ - "clap 3.2.17", + "clap 3.2.23", "heck 0.4.0", "indexmap", "log", "proc-macro2", "quote", - "serde 1.0.144", - "serde_json 1.0.85", + "serde 1.0.147", + "serde_json 1.0.89", "syn", "tempfile", "toml", @@ -575,9 +590,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.73" +version = "1.0.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" +checksum = "e9f73505338f7d905b19d18738976aae232eb46b8efc15554ffc56deb5d9ebe4" dependencies = [ "jobserver", ] @@ -599,9 +614,9 @@ dependencies = [ [[package]] name = "cfg-expr" -version = "0.10.3" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0aacacf4d96c24b2ad6eb8ee6df040e4f27b0d0b39a5710c30091baa830485db" +checksum = "b0357a6402b295ca3a86bc148e84df46c02e41f41fef186bda662557ef6328aa" dependencies = [ "smallvec", ] @@ -648,9 +663,9 @@ dependencies = [ [[package]] name = "clang-sys" -version = "1.3.3" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a050e2153c5be08febd6734e29298e844fdb0fa21aeddd63b4eb7baa106c69b" +checksum = "fa2e27ae6ab525c3d369ded447057bca5438d86dc3a68f6faafb8269ba82ebf3" dependencies = [ "glob", "libc", @@ -674,9 +689,9 @@ dependencies = [ [[package]] name = "clap" -version = "3.2.17" +version = "3.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29e724a68d9319343bb3328c9cc2dfde263f4b3142ee1059a9980580171c954b" +checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5" dependencies = [ "atty", "bitflags", @@ -686,14 +701,14 @@ dependencies = [ "once_cell", "strsim 0.10.0", "termcolor", - "textwrap 0.15.0", + "textwrap 0.16.0", ] [[package]] name = "clap_derive" -version = "3.2.17" +version = "3.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13547f7012c01ab4a0e8f8967730ada8f9fdf419e8b6c792788f39cf4e46eefa" +checksum = "ea0c8bce528c4be4da13ea6fead8965e95b6073585a2f05204bd8f4119f82a65" dependencies = [ "heck 0.4.0", "proc-macro-error", @@ -718,7 +733,7 @@ dependencies = [ "cc", "hbb_common", "lazy_static", - "serde 1.0.144", + "serde 1.0.147", "serde_derive", "thiserror", ] @@ -745,18 +760,18 @@ dependencies = [ [[package]] name = "cmake" -version = "0.1.48" +version = "0.1.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8ad8cef104ac57b68b89df3208164d228503abbdce70f6880ffa3d970e7443a" +checksum = "db34956e100b30725f2eb215f90d4871051239535632f84fea3bc92722c66b7c" dependencies = [ "cc", ] [[package]] name = "cocoa" -version = "0.24.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f63902e9223530efb4e26ccd0cf55ec30d592d3b42e21a28defc42a9586e832" +checksum = "f425db7937052c684daec3bd6375c8abe2d146dca4b8b143d6db777c39138f3a" dependencies = [ "bitflags", "block", @@ -783,6 +798,16 @@ dependencies = [ "objc", ] +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + [[package]] name = "color_quant" version = "1.1.0" @@ -808,13 +833,22 @@ dependencies = [ "cache-padded", ] +[[package]] +name = "concurrent-queue" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd7bef69dc86e3c610e4e7aed41035e2a7ed12e72dd7530f61327a6579a4390b" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "confy" version = "0.4.0" source = "git+https://github.com/open-trade/confy#630cc28a396cb7d01eefdd9f3824486fe4d8554b" dependencies = [ "directories-next", - "serde 1.0.144", + "serde 1.0.147", "thiserror", "toml", ] @@ -919,11 +953,11 @@ dependencies = [ [[package]] name = "coreaudio-sys" -version = "0.2.10" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dff444d80630d7073077d38d40b4501fd518bd2b922c2a55edcc8b0f7be57e6" +checksum = "1a9444b94b8024feecc29e01a9706c69c1e26bfee480221c90764200cfd778fb" dependencies = [ - "bindgen", + "bindgen 0.61.0", ] [[package]] @@ -953,9 +987,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc948ebb96241bb40ab73effeb80d9f93afaad49359d159a5e61be51619fe813" +checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" dependencies = [ "libc", ] @@ -992,23 +1026,22 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.10" +version = "0.9.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "045ebe27666471bb549370b4b0b3e51b07f56325befa4284db65fc89c02511b1" +checksum = "01a9af1f4c2ef74bb8aa1f7e19706bc72d03598c8a570bb5de72243c7a9d9d5a" dependencies = [ "autocfg 1.1.0", "cfg-if 1.0.0", "crossbeam-utils", - "memoffset", - "once_cell", + "memoffset 0.7.1", "scopeguard", ] [[package]] name = "crossbeam-queue" -version = "0.3.6" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd42583b04998a5363558e5f9291ee5a5ff6b49944332103f251e7479a82aa7" +checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" dependencies = [ "cfg-if 1.0.0", "crossbeam-utils", @@ -1016,12 +1049,11 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.11" +version = "0.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51887d4adc7b564537b15adcfb307936f8075dfcd5f00dde9a9f1d29383682bc" +checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f" dependencies = [ "cfg-if 1.0.0", - "once_cell", ] [[package]] @@ -1034,16 +1066,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "cstr_core" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd98742e4fdca832d40cab219dc2e3048de17d873248f83f17df47c1bea70956" -dependencies = [ - "cty", - "memchr", -] - [[package]] name = "ctrlc" version = "3.2.3" @@ -1061,10 +1083,54 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35" [[package]] -name = "dark-light" -version = "0.2.2" +name = "cxx" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b83576e2eee2d9cdaa8d08812ae59cbfe1b5ac7ac5ac4b8400303c6148a88c1" +checksum = "d4a41a86530d0fe7f5d9ea779916b7cadd2d4f9add748b99c2c029cbbdfaf453" +dependencies = [ + "cc", + "cxxbridge-flags", + "cxxbridge-macro", + "link-cplusplus", +] + +[[package]] +name = "cxx-build" +version = "1.0.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06416d667ff3e3ad2df1cd8cd8afae5da26cf9cec4d0825040f88b5ca659a2f0" +dependencies = [ + "cc", + "codespan-reporting", + "once_cell", + "proc-macro2", + "quote", + "scratch", + "syn", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "820a9a2af1669deeef27cb271f476ffd196a2c4b6731336011e0ba63e2c7cf71" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a08a6e2fcc370a089ad3b4aaf54db3b1b4cee38ddabce5896b33eb693275f470" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dark-light" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413487ef345ab5cdfbf23e66070741217a701bce70f2f397a54221b4f2b6056a" dependencies = [ "dconf_rs", "detect-desktop-environment", @@ -1072,7 +1138,7 @@ dependencies = [ "objc", "rust-ini", "web-sys", - "winreg 0.8.0", + "winreg 0.10.1", "zbus", "zvariant", ] @@ -1304,9 +1370,9 @@ checksum = "21d8ad60dd5b13a4ee6bd8fa2d5d88965c597c67bce32b5fc49c94f55cb50810" [[package]] name = "digest" -version = "0.10.5" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adfbc57365a37acbd2ebf2b64d7e69bb766e2fea813521ed536f5d0520dcf86c" +checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" dependencies = [ "block-buffer", "crypto-common", @@ -1380,12 +1446,9 @@ dependencies = [ [[package]] name = "dlv-list" -version = "0.2.3" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68df3f2b690c1b86e65ef7830956aededf3cb0a16f898f79b9a6f421a7b6211b" -dependencies = [ - "rand 0.8.5", -] +checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" [[package]] name = "docopt" @@ -1395,7 +1458,7 @@ checksum = "7f3f119846c823f9eafcf953a8f6ffb6ed69bf6240883261a7f13b634579a51f" dependencies = [ "lazy_static", "regex", - "serde 1.0.144", + "serde 1.0.147", "strsim 0.10.0", ] @@ -1418,7 +1481,7 @@ dependencies = [ "cc", "hbb_common", "lazy_static", - "serde 1.0.144", + "serde 1.0.147", "serde_derive", "thiserror", ] @@ -1471,7 +1534,7 @@ dependencies = [ "objc", "pkg-config", "rdev", - "serde 1.0.144", + "serde 1.0.147", "serde_derive", "tfc", "unicode-segmentation", @@ -1517,7 +1580,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e75d4cd21b95383444831539909fbb14b9dc3fdceb2a6f5d36577329a1f55ccb" dependencies = [ "enumflags2_derive", - "serde 1.0.144", + "serde 1.0.147", ] [[package]] @@ -1543,9 +1606,9 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.9.0" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3" +checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7" dependencies = [ "atty", "humantime", @@ -1649,20 +1712,20 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e1c54951450cbd39f3dbcf1005ac413b49487dabf18a720ad2383eccfeffb92" dependencies = [ - "memoffset", + "memoffset 0.6.5", "rustc_version 0.3.3", ] [[package]] name = "filetime" -version = "0.2.17" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94a7bbaa59354bc20dd75b67f23e2797b4490e9d6928203fb105c79e448c86c" +checksum = "4b9663d381d07ae25dc88dbdf27df458faa83a9b25336bcac83d5e452b5fc9d3" dependencies = [ "cfg-if 1.0.0", "libc", "redox_syscall", - "windows-sys 0.36.1", + "windows-sys 0.42.0", ] [[package]] @@ -1672,7 +1735,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6" dependencies = [ "crc32fast", - "miniz_oxide 0.5.3", + "miniz_oxide 0.5.4", ] [[package]] @@ -1718,13 +1781,13 @@ dependencies = [ "cbindgen", "convert_case", "enum_dispatch", - "env_logger 0.9.0", + "env_logger 0.9.3", "lazy_static", "log", "pathdiff", "quote", "regex", - "serde 1.0.144", + "serde 1.0.147", "serde_yaml", "structopt", "syn", @@ -1761,11 +1824,10 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" dependencies = [ - "matches", "percent-encoding", ] @@ -1799,9 +1861,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "futures" -version = "0.3.23" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab30e97ab6aacfe635fad58f22c2bb06c8b685f7421eb1e064a729e2a5f481fa" +checksum = "38390104763dc37a5145a53c29c63c1290b5d316d6086ec32c293f6736051bb0" dependencies = [ "futures-channel", "futures-core", @@ -1814,9 +1876,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.23" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bfc52cbddcfd745bf1740338492bb0bd83d76c67b445f91c5fb29fae29ecaa1" +checksum = "52ba265a92256105f45b719605a571ffe2d1f0fea3807304b522c1d778f79eed" dependencies = [ "futures-core", "futures-sink", @@ -1824,15 +1886,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.23" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2acedae88d38235936c3922476b10fced7b2b68136f5e3c03c2d5be348a1115" +checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac" [[package]] name = "futures-executor" -version = "0.3.23" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d11aa21b5b587a64682c0094c2bdd4df0076c5324961a40cc3abd7f37930528" +checksum = "7acc85df6714c176ab5edf386123fafe217be88c0840ec11f199441134a074e2" dependencies = [ "futures-core", "futures-task", @@ -1841,9 +1903,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.23" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93a66fc6d035a26a3ae255a6d2bca35eda63ae4c5512bef54449113f7a1228e5" +checksum = "00f5fb52a06bdcadeb54e8d3671f8888a39697dcb0b81b23b55174030427f4eb" [[package]] name = "futures-lite" @@ -1862,9 +1924,9 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.23" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0db9cce532b0eae2ccf2766ab246f114b56b9cf6d445e00c2549fbc100ca045d" +checksum = "bdfb8ce053d86b91919aad980c220b1fb8401a9394410e1c289ed7e66b61835d" dependencies = [ "proc-macro2", "quote", @@ -1873,21 +1935,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.23" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca0bae1fe9752cf7fd9b0064c674ae63f97b37bc714d745cbde0afb7ec4e6765" +checksum = "39c15cf1a4aa79df40f1bb462fb39676d0ad9e366c2a33b590d7c66f4f81fcf9" [[package]] name = "futures-task" -version = "0.3.23" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "842fc63b931f4056a24d59de13fb1272134ce261816e063e634ad0c15cdc5306" +checksum = "2ffb393ac5d9a6eaa9d3fdf37ae2776656b706e200c8e16b1bdb227f5198e6ea" [[package]] name = "futures-util" -version = "0.3.23" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0828a5471e340229c11c77ca80017937ce3c58cb788a17e5f1c2d5c485a9577" +checksum = "197676987abd2f9cadff84926f410af1c183608d36641465df73ae8211dc65d6" dependencies = [ "futures-channel", "futures-core", @@ -1949,7 +2011,7 @@ dependencies = [ "glib-sys 0.15.10", "gobject-sys 0.15.10", "libc", - "system-deps 6.0.2", + "system-deps 6.0.3", ] [[package]] @@ -1966,7 +2028,7 @@ dependencies = [ "libc", "pango-sys", "pkg-config", - "system-deps 6.0.2", + "system-deps 6.0.3", ] [[package]] @@ -1991,9 +2053,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" +checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" dependencies = [ "cfg-if 1.0.0", "libc", @@ -2032,7 +2094,7 @@ dependencies = [ "glib-sys 0.15.10", "gobject-sys 0.15.10", "libc", - "system-deps 6.0.2", + "system-deps 6.0.3", "winapi 0.3.9", ] @@ -2123,7 +2185,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef4b192f8e65e9cf76cbf4ea71fa8e3be4a0e18ffe3d68b8da6836974cc5bad4" dependencies = [ "libc", - "system-deps 6.0.2", + "system-deps 6.0.3", ] [[package]] @@ -2151,7 +2213,7 @@ checksum = "0d57ce44246becd17153bd035ab4d32cfee096a657fc01f2231c9278378d1e0a" dependencies = [ "glib-sys 0.15.10", "libc", - "system-deps 6.0.2", + "system-deps 6.0.3", ] [[package]] @@ -2325,7 +2387,7 @@ dependencies = [ "gobject-sys 0.15.10", "libc", "pango-sys", - "system-deps 6.0.2", + "system-deps 6.0.3", ] [[package]] @@ -2344,9 +2406,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.14" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca32592cf21ac7ccab1825cd87f6c9b3d9022c44d086172ed0966bec8af30be" +checksum = "5f9f29bc9dda355256b2916cf526ab02ce0aeaaaf2bad60d65ef3f12f11dd0f4" dependencies = [ "bytes", "fnv", @@ -2361,22 +2423,13 @@ dependencies = [ "tracing", ] -[[package]] -name = "hashbrown" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" -dependencies = [ - "ahash 0.4.7", -] - [[package]] name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" dependencies = [ - "ahash 0.7.6", + "ahash", ] [[package]] @@ -2389,7 +2442,7 @@ dependencies = [ "confy", "directories-next", "dirs-next", - "env_logger 0.9.0", + "env_logger 0.9.3", "filetime", "futures", "futures-util", @@ -2402,9 +2455,9 @@ dependencies = [ "quinn", "rand 0.8.5", "regex", - "serde 1.0.144", + "serde 1.0.147", "serde_derive", - "serde_json 1.0.85", + "serde_json 1.0.89", "socket2 0.3.19", "sodiumoxide", "tokio", @@ -2459,7 +2512,7 @@ checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" dependencies = [ "bytes", "fnv", - "itoa 1.0.3", + "itoa 1.0.4", ] [[package]] @@ -2475,9 +2528,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.7.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "496ce29bb5a52785b44e0f7ca2847ae0bb839c9bd28f69acac9b99d461c0c04c" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" [[package]] name = "httpdate" @@ -2496,19 +2549,19 @@ name = "hwcodec" version = "0.1.0" source = "git+https://github.com/21pages/hwcodec#f54d69b35251ade110373403ddefcb8b49c87305" dependencies = [ - "bindgen", + "bindgen 0.59.2", "cc", "log", - "serde 1.0.144", + "serde 1.0.147", "serde_derive", - "serde_json 1.0.85", + "serde_json 1.0.89", ] [[package]] name = "hyper" -version = "0.14.20" +version = "0.14.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02c929dc5c39e335a03c405292728118860721b10190d98c2a0f0efd5baafbac" +checksum = "034711faac9d2166cb1baf1a2fb0b60b1f277f8492fd72176c17f3515e1abd3c" dependencies = [ "bytes", "futures-channel", @@ -2519,9 +2572,9 @@ dependencies = [ "http-body", "httparse", "httpdate", - "itoa 1.0.3", + "itoa 1.0.4", "pin-project-lite", - "socket2 0.4.6", + "socket2 0.4.7", "tokio", "tower-service", "tracing", @@ -2530,9 +2583,9 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.23.0" +version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d87c48c02e0dc5e3b849a2041db3029fd066650f8f717c07bf8ed78ccb895cac" +checksum = "59df7c4e19c950e6e0e868dcc0a300b09a9b88e9ec55bd879ca819087a77355d" dependencies = [ "http", "hyper", @@ -2543,17 +2596,28 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.46" +version = "0.1.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad2bfd338099682614d3ee3fe0cd72e0b6a41ca6a87f6a74a3bd593c91650501" +checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765" dependencies = [ "android_system_properties", "core-foundation-sys 0.8.3", + "iana-time-zone-haiku", "js-sys", "wasm-bindgen", "winapi 0.3.9", ] +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" +dependencies = [ + "cxx", + "cxx-build", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -2562,11 +2626,10 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "0.2.3" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" dependencies = [ - "matches", "unicode-bidi", "unicode-normalization", ] @@ -2597,18 +2660,18 @@ dependencies = [ [[package]] name = "include_dir" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "482a2e29200b7eed25d7fdbd14423326760b7f6658d21a4cf12d55a50713c69f" +checksum = "18762faeff7122e89e0857b02f7ce6fcc0d101d5e9ad2ad7846cc01d61b7f19e" dependencies = [ "include_dir_macros", ] [[package]] name = "include_dir_macros" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e074c19deab2501407c91ba1860fa3d6820bfde307db6d8cb851b55a10be89b" +checksum = "b139284b5cf57ecfa712bcc66950bb635b31aff41c188e8a4cfc758eca374a3f" dependencies = [ "proc-macro2", "quote", @@ -2616,12 +2679,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.9.1" +version = "1.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" +checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" dependencies = [ "autocfg 1.1.0", - "hashbrown 0.12.3", + "hashbrown", ] [[package]] @@ -2667,9 +2730,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.5.0" +version = "2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b" +checksum = "f88c5561171189e69df9d98bcf18fd5f9558300f7ea7b801eb8a0fd748bd8745" [[package]] name = "itertools" @@ -2688,9 +2751,9 @@ checksum = "8324a32baf01e2ae060e9de58ed0bc2320c9a2833491ee36cd3b4c414de4db8c" [[package]] name = "itoa" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754" +checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc" [[package]] name = "jni" @@ -2714,9 +2777,9 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "jobserver" -version = "0.1.24" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af25a77299a7f711a01975c35a6a424eb6862092cc2d6c72c4ed6cbc56dfc1fa" +checksum = "068b1ee6743e4d11fb9c6a1e6064b3693a1b600e7f5f5988047d98b3dc9fb90b" dependencies = [ "libc", ] @@ -2729,9 +2792,9 @@ checksum = "229d53d58899083193af11e15917b5640cd40b29ff475a1fe4ef725deb02d0f2" [[package]] name = "js-sys" -version = "0.3.59" +version = "0.3.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "258451ab10b34f8af53416d1fdab72c22e805f0c92a1136d59470ec0b11138b2" +checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" dependencies = [ "wasm-bindgen", ] @@ -2784,9 +2847,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.132" +version = "0.2.137" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" +checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89" [[package]] name = "libdbus-sys" @@ -2799,9 +2862,9 @@ dependencies = [ [[package]] name = "libloading" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efbc0f03f9a775e9f6aed295c6a1ba2253c5757a9e03d55c6caa46a681abcddd" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" dependencies = [ "cfg-if 1.0.0", "winapi 0.3.9", @@ -2876,6 +2939,15 @@ dependencies = [ "walkdir", ] +[[package]] +name = "link-cplusplus" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9272ab7b96c9046fbc5bc56c06c117cb639fe2d509df0c421cad82d2915cf369" +dependencies = [ + "cc", +] + [[package]] name = "linked-hash-map" version = "0.5.6" @@ -2884,9 +2956,9 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "lock_api" -version = "0.4.7" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" +checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" dependencies = [ "autocfg 1.1.0", "scopeguard", @@ -2903,9 +2975,9 @@ dependencies = [ [[package]] name = "mac_address" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d1bc1084549d60725ccc53a2bfa07f67fe4689fda07b05a36531f2988104a" +checksum = "b238e3235c8382b7653c6408ed1b08dd379bdb9fdf990fb0bbae3db2cc0ae963" dependencies = [ "nix 0.23.1", "winapi 0.3.9", @@ -2934,7 +3006,7 @@ name = "magnum-opus" version = "0.4.0" source = "git+https://github.com/SoLongAndThanksForAllThePizza/magnum-opus#6247071a64af7b18e2d553e235729e6865f63ece" dependencies = [ - "bindgen", + "bindgen 0.59.2", "target_build_utils", ] @@ -2947,12 +3019,6 @@ dependencies = [ "libc", ] -[[package]] -name = "matches" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" - [[package]] name = "md5" version = "0.7.0" @@ -2989,6 +3055,15 @@ dependencies = [ "autocfg 1.1.0", ] +[[package]] +name = "memoffset" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +dependencies = [ + "autocfg 1.1.0", +] + [[package]] name = "mime" version = "0.3.16" @@ -3022,9 +3097,9 @@ dependencies = [ [[package]] name = "miniz_oxide" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f5c75688da582b8ffc1f1799e9db273f32133c49e048f614d22ec3256773ccc" +checksum = "96590ba8f175222643a85693f33d26e9c8a015f599c216509b1a6894af675d34" dependencies = [ "adler", ] @@ -3050,14 +3125,14 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf" +checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de" dependencies = [ "libc", "log", "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.36.1", + "windows-sys 0.42.0", ] [[package]] @@ -3105,10 +3180,9 @@ dependencies = [ [[package]] name = "mouce" version = "0.2.1" -source = "git+https://github.com/fufesou/mouce.git#26da8d4b0009b7f96996799c2a5c0990a8dbf08b" +source = "git+https://github.com/fufesou/mouce.git#aa18ba25bb47484282e972a4b95a8e1d753230b5" dependencies = [ "glob", - "libc", ] [[package]] @@ -3151,7 +3225,7 @@ checksum = "451422b7e4718271c8b5b3aadf5adedba43dc76312454b387e98fae0fc951aa0" dependencies = [ "bitflags", "jni-sys", - "ndk-sys 0.4.0", + "ndk-sys 0.4.1+23.1.7779620", "num_enum", "raw-window-handle 0.5.0", "thiserror", @@ -3223,18 +3297,18 @@ dependencies = [ [[package]] name = "ndk-sys" -version = "0.4.0" +version = "0.4.1+23.1.7779620" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21d83ec9c63ec5bf950200a8e508bdad6659972187b625469f58ef8c08e29046" +checksum = "3cf2aae958bd232cac5069850591667ad422d263686d75b52a065f9badeee5a3" dependencies = [ "jni-sys", ] [[package]] name = "net2" -version = "0.2.37" +version = "0.2.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "391630d12b68002ae1e25e8f974306474966550ad82dac6886fb8910c19568ae" +checksum = "74d0df99cfcd2530b2e694f6e17e7f37b8e26bb23983ac530c0c97408837c631" dependencies = [ "cfg-if 0.1.10", "libc", @@ -3251,7 +3325,7 @@ dependencies = [ "cc", "cfg-if 1.0.0", "libc", - "memoffset", + "memoffset 0.6.5", ] [[package]] @@ -3264,7 +3338,7 @@ dependencies = [ "cc", "cfg-if 1.0.0", "libc", - "memoffset", + "memoffset 0.6.5", ] [[package]] @@ -3276,7 +3350,7 @@ dependencies = [ "bitflags", "cfg-if 1.0.0", "libc", - "memoffset", + "memoffset 0.6.5", ] [[package]] @@ -3289,6 +3363,8 @@ dependencies = [ "bitflags", "cfg-if 1.0.0", "libc", + "memoffset 0.6.5", + "pin-utils", ] [[package]] @@ -3393,9 +3469,9 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.13.1" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" +checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5" dependencies = [ "hermit-abi", "libc", @@ -3494,9 +3570,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.13.1" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "074864da206b4973b84eb91683020dbefd6a8c3f0f38e054d93954e891935e4e" +checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" [[package]] name = "openssl-probe" @@ -3506,19 +3582,19 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "ordered-multimap" -version = "0.3.1" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c672c7ad9ec066e428c00eb917124a06f08db19e2584de982cc34b1f4c12485" +checksum = "ccd746e37177e1711c20dd619a1620f34f5c8b569c53590a72dedd5344d8924a" dependencies = [ "dlv-list", - "hashbrown 0.9.1", + "hashbrown", ] [[package]] name = "ordered-stream" -version = "0.0.1" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44630c059eacfd6e08bdaa51b1db2ce33119caa4ddc1235e923109aa5f25ccb1" +checksum = "034ce384018b245e8d8424bbe90577fbd91a533be74107e465e3474eb2285eef" dependencies = [ "futures-core", "pin-project-lite", @@ -3526,9 +3602,9 @@ dependencies = [ [[package]] name = "os_str_bytes" -version = "6.3.0" +version = "6.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff" +checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" [[package]] name = "padlock" @@ -3558,7 +3634,7 @@ dependencies = [ "glib-sys 0.15.10", "gobject-sys 0.15.10", "libc", - "system-deps 6.0.2", + "system-deps 6.0.3", ] [[package]] @@ -3600,7 +3676,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ "lock_api", - "parking_lot_core 0.9.3", + "parking_lot_core 0.9.4", ] [[package]] @@ -3619,22 +3695,22 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929" +checksum = "4dc9e0dc2adc1c69d09143aff38d3d30c5c3f0df0dad82e6d25547af174ebec0" dependencies = [ "cfg-if 1.0.0", "libc", "redox_syscall", "smallvec", - "windows-sys 0.36.1", + "windows-sys 0.42.0", ] [[package]] name = "paste" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9423e2b32f7a043629287a536f21951e8c6a82482d0acb1eeebfc90bc2225b22" +checksum = "b1de2e551fb905ac83f73f7aedf2f0cb4a0da7e35efa24a202a936269f1f18e1" [[package]] name = "pathdiff" @@ -3650,15 +3726,15 @@ checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" [[package]] name = "percent-encoding" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" +checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" [[package]] name = "pest" -version = "2.3.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b0560d531d1febc25a3c9398a62a71256c0178f2e3443baedd9ad4bb8c9deb4" +checksum = "a528564cc62c19a7acac4d81e01f39e53e25e17b934878f4c6d25cc2836e62f8" dependencies = [ "thiserror", "ucd-trie", @@ -3736,9 +3812,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.25" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" +checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" [[package]] name = "png" @@ -3754,9 +3830,9 @@ dependencies = [ [[package]] name = "polling" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899b00b9c8ab553c743b3e11e87c5c7d423b2a2de229ba95b24a756344748011" +checksum = "ab4609a838d88b73d8238967b60dd115cc08d38e2bbaf51ee1e4b695f89122e2" dependencies = [ "autocfg 1.1.0", "cfg-if 1.0.0", @@ -3768,9 +3844,9 @@ dependencies = [ [[package]] name = "ppv-lite86" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "pretty-hex" @@ -3780,9 +3856,9 @@ checksum = "bc5c99d529f0d30937f6f4b8a86d988047327bb88d04d2c4afc356de74722131" [[package]] name = "primal-check" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b264861209b0641a9b7571695029f516698bd3f2bf46eb61fca408675630b8c" +checksum = "9df7f93fd637f083201473dab4fee2db4c429d32e55e3299980ab3957ab916a0" dependencies = [ "num-integer", ] @@ -3833,18 +3909,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.43" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" +checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725" dependencies = [ "unicode-ident", ] [[package]] name = "protobuf" -version = "3.1.0" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ee4a7d8b91800c8f167a6268d1a1026607368e1adc84e98fe044aeb905302f7" +checksum = "b55bad9126f378a853655831eb7363b7b01b81d19f8cb1218861086ca4a1a61e" dependencies = [ "bytes", "once_cell", @@ -3854,9 +3930,9 @@ dependencies = [ [[package]] name = "protobuf-codegen" -version = "3.1.0" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07b893e5e7d3395545d5244f8c0d33674025bd566b26c03bfda49b82c6dec45e" +checksum = "0dd418ac3c91caa4032d37cb80ff0d44e2ebe637b2fb243b6234bf89cdac4901" dependencies = [ "anyhow", "once_cell", @@ -3869,9 +3945,9 @@ dependencies = [ [[package]] name = "protobuf-parse" -version = "3.1.0" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b1447dd751c434cc1b415579837ebd0411ed7d67d465f38010da5d7cd33af4d" +checksum = "9d39b14605eaa1f6a340aec7f320b34064feb26c93aec35d6a9a2272a8ddfa49" dependencies = [ "anyhow", "indexmap", @@ -3880,14 +3956,14 @@ dependencies = [ "protobuf-support", "tempfile", "thiserror", - "which 4.2.5", + "which 4.3.0", ] [[package]] name = "protobuf-support" -version = "3.1.0" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ca157fe12fc7ee2e315f2f735e27df41b3d97cdd70ea112824dac1ffb08ee1c" +checksum = "a5d4d7b8601c814cfb36bcebb79f0e61e45e1e93640cf778837833bbed05c372" dependencies = [ "thiserror", ] @@ -3946,14 +4022,14 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f832d8958db3e84d2ec93b5eb2272b45aa23cf7f8fe6e79f578896f4e6c231b" +checksum = "b07946277141531aea269befd949ed16b2c85a780ba1043244eda0969e538e54" dependencies = [ "futures-util", "libc", "quinn-proto", - "socket2 0.4.6", + "socket2 0.4.7", "tokio", "tracing", ] @@ -4000,7 +4076,7 @@ checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha 0.3.1", - "rand_core 0.6.3", + "rand_core 0.6.4", ] [[package]] @@ -4020,7 +4096,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core 0.6.3", + "rand_core 0.6.4", ] [[package]] @@ -4040,9 +4116,9 @@ checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" [[package]] name = "rand_core" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom", ] @@ -4129,11 +4205,10 @@ dependencies = [ [[package]] name = "rayon" -version = "1.5.3" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd99e5772ead8baa5215278c9b15bf92087709e9c1b2d1f97cdb5a183c933a7d" +checksum = "1e060280438193c554f654141c9ea9417886713b7acd75974c85b18a69a88e0b" dependencies = [ - "autocfg 1.1.0", "crossbeam-deque", "either", "rayon-core", @@ -4141,9 +4216,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.9.3" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "258bcdb5ac6dad48491bb2992db6b7cf74878b0384908af124823d118c99683f" +checksum = "cac410af5d00ab6884528b4ab69d1e8e146e8d471201800fa1b4524126de6ad3" dependencies = [ "crossbeam-channel", "crossbeam-deque", @@ -4165,7 +4240,7 @@ dependencies = [ "inotify", "lazy_static", "libc", - "mio 0.8.4", + "mio 0.8.5", "strum 0.24.1", "strum_macros 0.24.3", "widestring 1.0.2", @@ -4184,9 +4259,9 @@ dependencies = [ [[package]] name = "realfft" -version = "3.0.1" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8028eb3fabd68ddf331f744ba9c25a939804e276d820f9b218ab25a4bd7b91b8" +checksum = "3052e66d6ebeff8049607775c41d39a58d1dfa91a2733e89f2b7816bce2ea4cc" dependencies = [ "rustfft", ] @@ -4213,9 +4288,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" +checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a" dependencies = [ "aho-corasick", "memchr", @@ -4224,9 +4299,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.27" +version = "0.6.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" +checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" [[package]] name = "remove_dir_all" @@ -4249,9 +4324,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.11" +version = "0.11.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b75aa69a3f06bbcc66ede33af2af253c6f7a86b1ca0033f60c580a27074fbf92" +checksum = "68cc60575865c7831548863cc02356512e3f1dc2f3f82cb837d7fc4cc8f3c97c" dependencies = [ "base64", "bytes", @@ -4265,15 +4340,15 @@ dependencies = [ "hyper-rustls", "ipnet", "js-sys", - "lazy_static", "log", "mime", + "once_cell", "percent-encoding", "pin-project-lite", "rustls", "rustls-pemfile 1.0.1", - "serde 1.0.144", - "serde_json 1.0.85", + "serde 1.0.147", + "serde_json 1.0.89", "serde_urlencoded", "tokio", "tokio-rustls", @@ -4314,9 +4389,9 @@ dependencies = [ [[package]] name = "rpassword" -version = "7.0.0" +version = "7.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b763cb66df1c928432cc35053f8bd4cec3335d8559fc16010017d16b3c1680" +checksum = "20c9f5d2a0c3e2ea729ab3706d22217177770654c3ef5056b68b69d07332d3f5" dependencies = [ "libc", "winapi 0.3.9", @@ -4346,9 +4421,9 @@ dependencies = [ [[package]] name = "rust-ini" -version = "0.17.0" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63471c4aa97a1cf8332a5f97709a79a4234698de6a1f5087faf66f2dae810e22" +checksum = "f6d5f2436026b4f6e79dc829837d467cc7e9a55ee40e750d716713540715a2df" dependencies = [ "cfg-if 1.0.0", "ordered-multimap", @@ -4389,7 +4464,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" dependencies = [ - "semver 1.0.13", + "semver 1.0.14", ] [[package]] @@ -4406,7 +4481,7 @@ dependencies = [ "cfg-if 1.0.0", "chrono", "cidr-utils", - "clap 3.2.17", + "clap 3.2.23", "clipboard", "cocoa", "core-foundation 0.9.3", @@ -4446,16 +4521,16 @@ dependencies = [ "rdev", "repng", "reqwest", - "rpassword 7.0.0", + "rpassword 7.1.0", "rubato", "runas", "rust-pulsectl", "samplerate", "sciter-rs", "scrap", - "serde 1.0.144", + "serde 1.0.147", "serde_derive", - "serde_json 1.0.85", + "serde_json 1.0.89", "sha2", "shared_memory", "shutdown_hooks", @@ -4489,9 +4564,9 @@ dependencies = [ [[package]] name = "rustfft" -version = "6.0.1" +version = "6.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1d089e5c57521629a59f5f39bca7434849ff89bd6873b521afe389c1c602543" +checksum = "e17d4f6cbdb180c9f4b2a26bbf01c4e647f1e1dea22fe8eb9db54198b32f9434" dependencies = [ "num-complex", "num-integer", @@ -4499,13 +4574,14 @@ dependencies = [ "primal-check", "strength_reduce", "transpose", + "version_check", ] [[package]] name = "rustls" -version = "0.20.6" +version = "0.20.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aab8ee6c7097ed6057f43c187a62418d0c05a4bd5f18b3571db50ee0f9ce033" +checksum = "539a2bfe908f471bfa933876bd1eb6a19cf2176d375f82ef7f99530a40e48c2c" dependencies = [ "log", "ring", @@ -4596,9 +4672,9 @@ dependencies = [ [[package]] name = "scoped-tls" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" [[package]] name = "scopeguard" @@ -4611,7 +4687,7 @@ name = "scrap" version = "0.5.0" dependencies = [ "android_logger 0.10.1", - "bindgen", + "bindgen 0.59.2", "block", "cfg-if 1.0.0", "dbus", @@ -4629,14 +4705,20 @@ dependencies = [ "num_cpus", "quest", "repng", - "serde 1.0.144", - "serde_json 1.0.85", + "serde 1.0.147", + "serde_json 1.0.89", "target_build_utils", "tracing", "webm", "winapi 0.3.9", ] +[[package]] +name = "scratch" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8132065adcfd6e02db789d9285a0deb2f3fcb04002865ab67d5fb103533898" + [[package]] name = "sct" version = "0.7.0" @@ -4681,11 +4763,11 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93f6841e709003d68bb2deee8c343572bf446003ec20a583e76f7b15cebf3711" +checksum = "e25dfac463d778e353db5be2449d1cce89bd6fd23c9f1ea21310ce6e5a1b29c4" dependencies = [ - "serde 1.0.144", + "serde 1.0.147", ] [[package]] @@ -4705,18 +4787,18 @@ checksum = "34b623917345a631dc9608d5194cc206b3fe6c3554cd1c75b937e55e285254af" [[package]] name = "serde" -version = "1.0.144" +version = "1.0.147" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860" +checksum = "d193d69bae983fc11a79df82342761dfbf28a99fc8d203dca4c3c1b590948965" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.144" +version = "1.0.147" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00" +checksum = "4f1d362ca8fc9c3e3a7484440752472d68a6caa98f1ab81d99b5dfe517cec852" dependencies = [ "proc-macro2", "quote", @@ -4737,13 +4819,13 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.85" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44" +checksum = "020ff22c755c2ed3f8cf162dbb41a7268d934702f3ed3631656ea597e08fc3db" dependencies = [ - "itoa 1.0.3", + "itoa 1.0.4", "ryu", - "serde 1.0.144", + "serde 1.0.147", ] [[package]] @@ -4764,9 +4846,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", - "itoa 1.0.3", + "itoa 1.0.4", "ryu", - "serde 1.0.144", + "serde 1.0.147", ] [[package]] @@ -4777,25 +4859,21 @@ checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b" dependencies = [ "indexmap", "ryu", - "serde 1.0.144", + "serde 1.0.147", "yaml-rust", ] [[package]] name = "sha1" -version = "0.6.1" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1da05c97445caa12d05e848c4a4fcbbea29e748ac28f7e80e9b010392063770" +checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" dependencies = [ - "sha1_smol", + "cfg-if 1.0.0", + "cpufeatures", + "digest", ] -[[package]] -name = "sha1_smol" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" - [[package]] name = "sha2" version = "0.10.6" @@ -4853,9 +4931,9 @@ dependencies = [ [[package]] name = "signature" -version = "1.6.0" +version = "1.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0ea32af43239f0d353a7dd75a22d94c329c8cdaafdcb4c1c1335aa10c298a4a" +checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" [[package]] name = "simple_rc" @@ -4863,7 +4941,7 @@ version = "0.1.0" dependencies = [ "confy", "hbb_common", - "serde 1.0.144", + "serde 1.0.147", "serde_derive", "walkdir", ] @@ -4885,9 +4963,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" [[package]] name = "smithay-client-toolkit" @@ -4921,9 +4999,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10c98bba371b9b22a71a9414e420f92ddeb2369239af08200816169d5e2dd7aa" +checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" dependencies = [ "libc", "winapi 0.3.9", @@ -4938,7 +5016,7 @@ dependencies = [ "ed25519", "libc", "libsodium-sys", - "serde 1.0.144", + "serde 1.0.147", ] [[package]] @@ -4967,9 +5045,9 @@ checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0" [[package]] name = "strength_reduce" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3ff2f71c82567c565ba4b3009a9350a96a7269eaa4001ebedae926230bc2254" +checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" [[package]] name = "strsim" @@ -5046,9 +5124,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.99" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13" +checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d" dependencies = [ "proc-macro2", "quote", @@ -5069,12 +5147,10 @@ dependencies = [ [[package]] name = "sys-locale" -version = "0.2.1" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "658ee915b6c7b73ec4c1ffcd838506b5c5a4087eadc1ec8f862f1066cf2c8132" +checksum = "3358acbb4acd4146138b9bda219e904a6bb5aaaa237f8eed06f4d6bc1580ecee" dependencies = [ - "cc", - "cstr_core", "js-sys", "libc", "wasm-bindgen", @@ -5135,15 +5211,15 @@ dependencies = [ [[package]] name = "system-deps" -version = "6.0.2" +version = "6.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1a45a1c4c9015217e12347f2a411b57ce2c4fc543913b14b6fe40483328e709" +checksum = "2955b1fe31e1fa2fbd1976b71cc69a606d7d4da16f6de3333d0c92d51419aeff" dependencies = [ "cfg-expr", "heck 0.4.0", "pkg-config", "toml", - "version-compare 0.1.0", + "version-compare 0.1.1", ] [[package]] @@ -5215,9 +5291,9 @@ dependencies = [ [[package]] name = "textwrap" -version = "0.15.0" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" +checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" [[package]] name = "tfc" @@ -5232,18 +5308,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.32" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5f6586b7f764adc0231f4c79be7b920e766bb2f3e51b3661cdb263828f19994" +checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.32" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12bafc5b54507e0149cdf1b145a5d80ab80a90bcd9275df43d4fff68460f6c21" +checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" dependencies = [ "proc-macro2", "quote", @@ -5287,7 +5363,7 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2702e08a7a860f005826c6815dcac101b19b5eb330c27fe4a5928fec1d20ddd" dependencies = [ - "itoa 1.0.3", + "itoa 1.0.4", "libc", "num_threads", "time-macros", @@ -5316,21 +5392,20 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.21.1" +version = "1.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0020c875007ad96677dcc890298f4b942882c5d4eb7cc8f439fc3bf813dc9c95" +checksum = "d76ce4a75fb488c605c54bf610f221cea8b0dafb53333c1a67e8ee199dcd2ae3" dependencies = [ "autocfg 1.1.0", "bytes", "libc", "memchr", - "mio 0.8.4", + "mio 0.8.5", "num_cpus", - "once_cell", "parking_lot 0.12.1", "pin-project-lite", "signal-hook-registry", - "socket2 0.4.6", + "socket2 0.4.7", "tokio-macros", "winapi 0.3.9", ] @@ -5384,7 +5459,7 @@ dependencies = [ "futures-io", "futures-sink", "futures-util", - "hashbrown 0.12.3", + "hashbrown", "pin-project-lite", "slab", "tokio", @@ -5397,7 +5472,7 @@ version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" dependencies = [ - "serde 1.0.144", + "serde 1.0.147", ] [[package]] @@ -5408,9 +5483,9 @@ checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" -version = "0.1.36" +version = "0.1.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fce9567bd60a67d08a16488756721ba392f24f29006402881e43b19aac64307" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" dependencies = [ "cfg-if 1.0.0", "pin-project-lite", @@ -5420,9 +5495,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.22" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11c75893af559bc8e10716548bdef5cb2b983f8e637db9d0e15126b61b484ee2" +checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" dependencies = [ "proc-macro2", "quote", @@ -5431,18 +5506,18 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.29" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aeea4303076558a00714b823f9ad67d58a3bbda1df83d8827d21193156e22f7" +checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" dependencies = [ "once_cell", ] [[package]] name = "transpose" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95f9c900aa98b6ea43aee227fd680550cdec726526aab8ac801549eadb25e39f" +checksum = "e6522d49d03727ffb138ae4cbc1283d3774f0d10aa7f9bf52e6784c45daf9b23" dependencies = [ "num-integer", "strength_reduce", @@ -5450,9 +5525,9 @@ dependencies = [ [[package]] name = "tray-item" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76863575f7842ed64fda361f417a787efa82811b4617267709066969cd4ccf3b" +checksum = "0914b62e00e8f51241806cb9f9c4ea6b10c75d94cae02c89278de6f4b98c7d0f" dependencies = [ "cocoa", "core-graphics 0.22.3", @@ -5489,9 +5564,9 @@ checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" [[package]] name = "ucd-trie" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89570599c4fe5585de2b388aab47e99f7fa4e9238a1399f707a02e356058141c" +checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81" [[package]] name = "uds_windows" @@ -5511,36 +5586,36 @@ checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" [[package]] name = "unicode-ident" -version = "1.0.3" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf" +checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" [[package]] name = "unicode-normalization" -version = "0.1.21" +version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854cbdc4f7bc6ae19c820d44abdc3277ac3e1b2b93db20a636825d9322fb60e6" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" dependencies = [ "tinyvec", ] [[package]] name = "unicode-segmentation" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" +checksum = "0fdbf052a0783de01e944a6ce7a8cb939e295b1e7be835a1112c3b9a7f047a5a" [[package]] name = "unicode-width" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" [[package]] name = "unicode-xid" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" [[package]] name = "untrusted" @@ -5550,22 +5625,21 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] name = "url" -version = "2.2.2" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" dependencies = [ "form_urlencoded", "idna", - "matches", "percent-encoding", - "serde 1.0.144", + "serde 1.0.147", ] [[package]] name = "uuid" -version = "1.1.2" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd6469f4314d5f1ffec476e05f17cc9a78bc7a27a6a857842170bdf8d6f98d2f" +checksum = "422ee0de9031b5b948b97a8fc04e3aa35230001a722ddd27943e0be31564ce4c" dependencies = [ "getrandom", ] @@ -5584,9 +5658,9 @@ checksum = "d63556a25bae6ea31b52e640d7c41d1ab27faba4ccb600013837a3d0b3994ca1" [[package]] name = "version-compare" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe88247b92c1df6b6de80ddc290f3976dbdf2f5f5d3fd049a9fb598c6dd5ca73" +checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29" [[package]] name = "version_check" @@ -5664,9 +5738,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.82" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7652e3f6c4706c8d9cd54832c4a4ccb9b5336e2c3bd154d5cccfbf1c1f5f7d" +checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" dependencies = [ "cfg-if 1.0.0", "wasm-bindgen-macro", @@ -5674,9 +5748,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.82" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "662cd44805586bd52971b9586b1df85cdbbd9112e4ef4d8f41559c334dc6ac3f" +checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" dependencies = [ "bumpalo", "log", @@ -5689,9 +5763,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.32" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa76fb221a1f8acddf5b54ace85912606980ad661ac7a503b4570ffd3a624dad" +checksum = "23639446165ca5a5de86ae1d8896b737ae80319560fbaa4c2887b7da6e7ebd7d" dependencies = [ "cfg-if 1.0.0", "js-sys", @@ -5701,9 +5775,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.82" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b260f13d3012071dfb1512849c033b1925038373aea48ced3012c09df952c602" +checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5711,9 +5785,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.82" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5be8e654bdd9b79216c2929ab90721aa82faf65c48cdf08bdc4e7f51357b80da" +checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" dependencies = [ "proc-macro2", "quote", @@ -5724,9 +5798,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.82" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6598dd0bd3c7d51095ff6531a5b23e02acdc81804e30d8f07afb77b7215a140a" +checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" [[package]] name = "wayland-client" @@ -5803,9 +5877,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.59" +version = "0.3.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed055ab27f941423197eb86b2035720b1a3ce40504df082cac2ecc6ed73335a1" +checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f" dependencies = [ "js-sys", "wasm-bindgen", @@ -5841,9 +5915,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.22.4" +version = "0.22.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1c760f0d366a6c24a02ed7816e23e691f5d92291f94d15e836006fd11b04daf" +checksum = "368bfe657969fb01238bb756d351dcade285e0f6fcbd36dcb23359a5169975be" dependencies = [ "webpki", ] @@ -5875,21 +5949,22 @@ dependencies = [ [[package]] name = "which" -version = "4.2.5" +version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c4fb54e6113b6a8772ee41c3404fb0301ac79604489467e0a9ce1f3e97c24ae" +checksum = "1c831fbbee9e129a8cf93e7747a82da9d95ba8e16621cae60ec2cdc849bacb7b" dependencies = [ "either", - "lazy_static", "libc", + "once_cell", ] [[package]] name = "whoami" -version = "1.2.1" +version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524b58fa5a20a2fb3014dd6358b70e6579692a56ef6fce928834e488f42f65e8" +checksum = "d6631b6a2fd59b1841b622e8f1a7ad241ef0a46f2d580464ce8140ac94cbd571" dependencies = [ + "bumpalo", "wasm-bindgen", "web-sys", ] @@ -6031,6 +6106,27 @@ dependencies = [ "windows_x86_64_msvc 0.36.1", ] +[[package]] +name = "windows-sys" +version = "0.42.0" +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_x86_64_gnullvm", + "windows_x86_64_msvc 0.42.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" + [[package]] name = "windows_aarch64_msvc" version = "0.28.0" @@ -6055,6 +6151,12 @@ version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" + [[package]] name = "windows_i686_gnu" version = "0.28.0" @@ -6079,6 +6181,12 @@ version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" +[[package]] +name = "windows_i686_gnu" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" + [[package]] name = "windows_i686_msvc" version = "0.28.0" @@ -6103,6 +6211,12 @@ version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" +[[package]] +name = "windows_i686_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" + [[package]] name = "windows_x86_64_gnu" version = "0.28.0" @@ -6127,6 +6241,18 @@ version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" + [[package]] name = "windows_x86_64_msvc" version = "0.28.0" @@ -6151,6 +6277,12 @@ version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" + [[package]] name = "winit" version = "0.26.1" @@ -6167,7 +6299,7 @@ dependencies = [ "lazy_static", "libc", "log", - "mio 0.8.4", + "mio 0.8.5", "ndk 0.5.0", "ndk-glue 0.5.2", "ndk-sys 0.2.2", @@ -6193,15 +6325,6 @@ dependencies = [ "winapi 0.3.9", ] -[[package]] -name = "winreg" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d107f8c6e916235c4c01cabb3e8acf7bea8ef6a63ca2e7fa0527c049badfc48c" -dependencies = [ - "winapi 0.3.9", -] - [[package]] name = "winreg" version = "0.10.1" @@ -6226,7 +6349,7 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7f97e69b28b256ccfb02472c25057132e234aa8368fea3bb0268def564ce1f2" dependencies = [ - "clap 3.2.17", + "clap 3.2.23", ] [[package]] @@ -6241,9 +6364,9 @@ dependencies = [ [[package]] name = "wyz" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30b31594f29d27036c383b53b59ed3476874d518f0efb151b27a4c275141390e" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" dependencies = [ "tap", ] @@ -6316,9 +6439,9 @@ dependencies = [ [[package]] name = "zbus" -version = "2.3.2" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d8f1a037b2c4a67d9654dc7bdfa8ff2e80555bbefdd3c1833c1d1b27c963a6b" +checksum = "a25ae891bd547674b368906552115143031c16c23a0f2f4b2f5f5436ab2e6a9f" dependencies = [ "async-broadcast", "async-channel", @@ -6337,12 +6460,11 @@ dependencies = [ "futures-sink", "futures-util", "hex", - "lazy_static", - "nix 0.23.1", + "nix 0.25.0", "once_cell", "ordered-stream", "rand 0.8.5", - "serde 1.0.144", + "serde 1.0.147", "serde_repr", "sha1", "static_assertions", @@ -6356,9 +6478,9 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "2.3.2" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f8fb5186d1c87ae88cf234974c240671238b4a679158ad3b94ec465237349a6" +checksum = "8aa37701ce7b3a43632d2b0ad9d4aef602b46be6bdd7fba3b7c5007f9f6eb2c2" dependencies = [ "proc-macro-crate 1.2.1", "proc-macro2", @@ -6369,11 +6491,11 @@ dependencies = [ [[package]] name = "zbus_names" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41a408fd8a352695690f53906dc7fd036be924ec51ea5e05666ff42685ed0af5" +checksum = "d69bb79b44e1901ed8b217e485d0f01991aec574479b68cb03415f142bc7ae67" dependencies = [ - "serde 1.0.144", + "serde 1.0.147", "static_assertions", "zvariant", ] @@ -6409,23 +6531,23 @@ dependencies = [ [[package]] name = "zvariant" -version = "3.6.0" +version = "3.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bd68e4e6432ef19df47d7e90e2e72b5e7e3d778e0ae3baddf12b951265cc758" +checksum = "5c817f416f05fcbc833902f1e6064b72b1778573978cfeac54731451ccc9e207" dependencies = [ "byteorder", "enumflags2", "libc", - "serde 1.0.144", + "serde 1.0.147", "static_assertions", "zvariant_derive", ] [[package]] name = "zvariant_derive" -version = "3.6.0" +version = "3.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08e977eaa3af652f63d479ce50d924254ad76722a6289ec1a1eac3231ca30430" +checksum = "fdd24fffd02794a76eb10109de463444064c88f5adb9e9d1a78488adc332bfef" dependencies = [ "proc-macro-crate 1.2.1", "proc-macro2", diff --git a/build.py b/build.py index aa98ec17e..42438909f 100755 --- a/build.py +++ b/build.py @@ -217,7 +217,7 @@ Version: %s Architecture: amd64 Maintainer: open-trade Homepage: https://rustdesk.com -Depends: libgtk-3-0, libxcb-randr0, libxdo3, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2, libsystemd0, pipewire, curl, libappindicator3-1, libva-drm2, libva-x11-2, libvdpau1 +Depends: libgtk-3-0, libxcb-randr0, libxdo3, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2, libsystemd0, curl, libappindicator3-1, libva-drm2, libva-x11-2, libvdpau1, libgstreamer-plugins-base1.0-0 Description: A remote control software. """ % version From cc89a571dabc847d1f5a9049bb73881aa4b2ae7f Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 25 Nov 2022 20:41:44 +0800 Subject: [PATCH 1039/2015] opt: remove on-push trigger --- .github/workflows/flutter-nightly.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 79362647a..88e074f7e 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -5,8 +5,6 @@ on: # schedule build every night - cron: "0 0 * * *" workflow_dispatch: - # REMOVE ME ON PR - push: env: LLVM_VERSION: "10.0" From e10e3f46ceabf4b55bc5623f5eb4f6ab40c2a188 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 25 Nov 2022 20:55:15 +0800 Subject: [PATCH 1040/2015] opt: naming --- .github/workflows/flutter-nightly.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 88e074f7e..7bbc159b2 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -698,7 +698,7 @@ jobs: build-rustdesk-linux-arm: needs: [build-rustdesk-lib-linux-arm] name: build-rustdesk ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] - runs-on: ${{ matrix.job.os }} + runs-on: ubuntu-20.04 # 20.04 has more performance on arm build strategy: fail-fast: false matrix: @@ -706,14 +706,14 @@ jobs: - { arch: aarch64, target: aarch64-unknown-linux-gnu, - os: ubuntu-20.04, + os: ubuntu-18.04, # just for naming package, not running host use-cross: true, extra-build-features: "", } - { arch: aarch64, target: aarch64-unknown-linux-gnu, - os: ubuntu-20.04, + os: ubuntu-18.04, # just for naming package, not running host use-cross: true, extra-build-features: "flatpak", } @@ -902,7 +902,7 @@ jobs: build-rustdesk-linux-amd64: needs: [build-rustdesk-lib-linux-amd64] name: build-rustdesk ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] - runs-on: ${{ matrix.job.os }} + runs-on: ubuntu-20.04 strategy: fail-fast: false matrix: @@ -912,13 +912,13 @@ jobs: - { arch: x86_64, target: x86_64-unknown-linux-gnu, - os: ubuntu-20.04, + os: ubuntu-18.04, extra-build-features: "", } - { arch: x86_64, target: x86_64-unknown-linux-gnu, - os: ubuntu-20.04, + os: ubuntu-18.04, extra-build-features: "flatpak", } # - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } From 21bb7c9cec79882cec6aa394f5f8fcc71e512351 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 25 Nov 2022 21:16:03 +0800 Subject: [PATCH 1041/2015] opt: disable flatpak for arm --- .github/workflows/flutter-nightly.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 7bbc159b2..3d4c8ddca 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -531,7 +531,7 @@ jobs: use-cross: true, extra-build-features: "", } - - { arch: aarch64, target: aarch64-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true, extra-build-features: "flatpak" } + # - { arch: aarch64, target: aarch64-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true, extra-build-features: "flatpak" } # - { # arch: armv7, # target: arm-unknown-linux-gnueabihf, @@ -710,13 +710,13 @@ jobs: use-cross: true, extra-build-features: "", } - - { - arch: aarch64, - target: aarch64-unknown-linux-gnu, - os: ubuntu-18.04, # just for naming package, not running host - use-cross: true, - extra-build-features: "flatpak", - } + # - { + # arch: aarch64, + # target: aarch64-unknown-linux-gnu, + # os: ubuntu-18.04, # just for naming package, not running host + # use-cross: true, + # extra-build-features: "flatpak", + # } # - { arch: armv7, target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true, extra-build-features: "" } # - { arch: armv7, target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true, extra-build-features: "flatpak" } # - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true } From 45561fca7c0ab1fb11fd3644d86196eb5790d3f0 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 25 Nov 2022 22:10:32 +0800 Subject: [PATCH 1042/2015] fix: flatpak x86_64 artifact name --- .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 3d4c8ddca..b6223754d 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -1133,12 +1133,12 @@ jobs: build-flatpak-amd64: name: Build Flatpak needs: [build-rustdesk-linux-amd64] - runs-on: ${{ matrix.job.os }} + runs-on: ubuntu-20.04 strategy: fail-fast: false matrix: job: - - { target: x86_64-unknown-linux-gnu, os: ubuntu-20.04, arch: x86_64 } + - { target: x86_64-unknown-linux-gnu, os: ubuntu-18.04, arch: x86_64 } steps: - name: Checkout source code uses: actions/checkout@v3 From 1f418e910f901ddcc81d619c247630e4df702879 Mon Sep 17 00:00:00 2001 From: Agent-JY Date: Fri, 25 Nov 2022 17:20:33 +0100 Subject: [PATCH 1043/2015] Update de.rs Typos and other improvements --- src/lang/de.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/lang/de.rs b/src/lang/de.rs index 5b0a86b37..0197b099a 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -137,9 +137,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Set Password", "Passwort festlegen"), ("OS Password", "Betriebssystem-Passwort"), ("install_tip", "Aufgrund der Benutzerkontensteuerung (UAC) kann RustDesk in manchen Fällen nicht ordnungsgemäß funktionieren. Um die Benutzerkontensteuerung zu umgehen, klicken Sie bitte auf die Schaltfläche unten, um RustDesk auf dem System zu installieren"), - ("Click to upgrade", "Zum Aktualisieren anklicken"), + ("Click to upgrade", "Upgrade"), ("Click to download", "Zum Herunterladen klicken"), - ("Click to update", "Zum Aktualisieren klicken"), + ("Click to update", "Update"), ("Configure", "Konfigurieren"), ("config_acc", "Um Ihren PC aus der Ferne zu steuern, müssen Sie RustDesk Zugriffsrechte erteilen."), ("config_screen", "Um aus der Ferne auf Ihren PC zugreifen zu können, müssen Sie RustDesk \"Bildschirm-Aufnahme\"-Berechtigung erteilen."), @@ -221,7 +221,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Password missed", "Passwort vergessen"), ("Wrong credentials", "Falsche Anmeldedaten"), ("Edit Tag", "Schlagwort bearbeiten"), - ("Unremember Password", "Passwort vergessen"), + ("Unremember Password", "Gespeichertes Passwort löschen"), ("Favorites", "Favoriten"), ("Add to Favorites", "Zu Favoriten hinzufügen"), ("Remove from Favorites", "Aus Favoriten entfernen"), @@ -229,7 +229,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Invalid folder name", "Ungültiger Ordnername"), ("Socks5 Proxy", "Socks5 Proxy"), ("Hostname", "Hostname"), - ("Discovered", "Automatisch gefunden"), + ("Discovered", "Im LAN erkannt"), ("install_daemon_tip", "Um mit System zu starten, muss der Systemdienst installiert sein"), ("Remote ID", "Entfernte ID"), ("Paste", "Einfügen"), @@ -332,7 +332,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Adaptiv skalieren"), ("General", "Allgemein"), ("Security", "Sicherheit"), - ("Account", "KOnto"), + ("Account", "Konto"), ("Theme", "Farbgebung"), ("Dark Theme", "dunkle Farbgebung"), ("Dark", "Dunkel"), @@ -396,6 +396,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("One-time password length", "Länge des Einmalpassworts"), ("Request access to your device", "Zugriff zu Ihrem Gerät erbitten"), ("Hide connection management window", "Fenster zur Verwaltung der Verbindung verstecken"), - ("hide_cm_tip", "Verstecken nur erlauben, wenn die Sitzung über ein festes Passwort erstellt wurde"), // Sehr unklar. Muss noch angepasst werden. Original: Allow hiding only if accepting sessions via password and using pernament password + ("hide_cm_tip", "Dies ist nur möglich, wenn der Zugriff nur über ein permanentes Passwort erfolgt."), // Sehr unklar. Muss noch angepasst werden. Original: Allow hiding only if accepting sessions via password and using pernament password ].iter().cloned().collect(); } From 70863f09dea29028872a4ad1a56e351eedef5820 Mon Sep 17 00:00:00 2001 From: Manos G <87467035+7th-fret@users.noreply.github.com> Date: Fri, 25 Nov 2022 20:25:45 +0000 Subject: [PATCH 1044/2015] Update gr.rs --- src/lang/gr.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lang/gr.rs b/src/lang/gr.rs index e17311e17..5e9dd2c95 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -351,13 +351,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clear", "Καθαρισμός"), ("Audio Input Device", "Συσκευή εισόδου ήχου"), ("Deny remote access", "Απόρριψη απομακρυσμένης πρόσβασης"), - ("Use IP Whitelisting", "Χρήση λευκής λίστας IP"), + ("Use IP Whitelisting", "Χρήση λίστας επιτρεπόμενων IP"), ("Network", "Δίκτυο"), ("Enable RDP", "Ενεργοποίηση RDP"), ("Pin menubar", "Καρφίτσωμα γραμμής μενού"), ("Unpin menubar", "Ξεκαρφίτσωμα γραμμής μενού"), - ("Recording", "Ηχογράφηση"), - ("Directory", "Ευρετήριο"), + ("Recording", "Εγγραφή"), + ("Directory", "Φάκελος εγγραφών"), ("Automatically record incoming sessions", "Αυτόματη εγγραφή εισερχόμενων συνεδριών"), ("Change", "Αλλαγή"), ("Start session recording", "Έναρξη εγγραφής συνεδρίας"), From 6cf1fcfc3a864ea2104c8ed499888c26eff75bb4 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sat, 26 Nov 2022 09:23:47 +0800 Subject: [PATCH 1045/2015] fix: remove git safe directory detection --- .github/workflows/flutter-nightly.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index b6223754d..c3c6aba27 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -243,12 +243,13 @@ jobs: cmake --version gcc -v run: | + # disable git safe.directory + git config --global --add safe.directory "*" case "${{ matrix.job.arch }}" in x86_64) export VCPKG_FORCE_SYSTEM_BINARIES=1 pushd /artifacts git clone https://github.com/microsoft/vcpkg.git || true - git config --global --add safe.directory /artifacts/vcpkg || true pushd vcpkg git reset --hard ${{ env.VCPKG_COMMIT_ID }} ./bootstrap-vcpkg.sh @@ -1169,6 +1170,8 @@ jobs: apt update -y apt install -y rpm run: | + # disable git safe.directory + git config --global --add safe.directory "*" pushd /workspace # install apt update -y From 56ee947e2c0a4f7c76a6bbc911c87882c228de02 Mon Sep 17 00:00:00 2001 From: fufesou Date: Sat, 26 Nov 2022 10:46:57 +0800 Subject: [PATCH 1046/2015] do not show scroll options when scale adaptive Signed-off-by: fufesou --- .../lib/desktop/widgets/remote_menubar.dart | 55 ++++++++++--------- 1 file changed, 30 insertions(+), 25 deletions(-) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 656dc8546..39c37dd8b 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -751,31 +751,6 @@ class _RemoteMenubarState extends State { dismissOnClicked: true, ), MenuEntryDivider(), - MenuEntryRadios( - text: translate('Scroll Style'), - optionsGetter: () => [ - MenuEntryRadioOption( - text: translate('ScrollAuto'), - value: kRemoteScrollStyleAuto, - dismissOnClicked: true, - ), - MenuEntryRadioOption( - text: translate('Scrollbar'), - value: kRemoteScrollStyleBar, - dismissOnClicked: true, - ), - ], - curOptionGetter: () async => - // null means peer id is not found, which there's no need to care about - await bind.sessionGetScrollStyle(id: widget.id) ?? '', - optionSetter: (String oldValue, String newValue) async { - await bind.sessionSetScrollStyle(id: widget.id, value: newValue); - widget.ffi.canvasModel.updateScrollStyle(); - }, - padding: padding, - dismissOnClicked: true, - ), - MenuEntryDivider(), MenuEntryRadios( text: translate('Image Quality'), optionsGetter: () => [ @@ -955,6 +930,36 @@ class _RemoteMenubarState extends State { MenuEntryDivider(), ]; + if (widget.state.viewStyle.value == kRemoteViewStyleOriginal) { + displayMenu.insert( + 2, + MenuEntryRadios( + text: translate('Scroll Style'), + optionsGetter: () => [ + MenuEntryRadioOption( + text: translate('ScrollAuto'), + value: kRemoteScrollStyleAuto, + dismissOnClicked: true, + ), + MenuEntryRadioOption( + text: translate('Scrollbar'), + value: kRemoteScrollStyleBar, + dismissOnClicked: true, + ), + ], + curOptionGetter: () async => + // null means peer id is not found, which there's no need to care about + await bind.sessionGetScrollStyle(id: widget.id) ?? '', + optionSetter: (String oldValue, String newValue) async { + await bind.sessionSetScrollStyle(id: widget.id, value: newValue); + widget.ffi.canvasModel.updateScrollStyle(); + }, + padding: padding, + dismissOnClicked: true, + )); + displayMenu.insert(3, MenuEntryDivider()); + } + if (_isWindowCanBeAdjusted(remoteCount)) { displayMenu.insert( 0, From 03c1395565e427aca72df77b286ac569324fbbb1 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sat, 26 Nov 2022 10:36:08 +0800 Subject: [PATCH 1047/2015] opt: remove listener warning output --- flutter/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index a05898468..cb3ce5fc9 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -63,7 +63,7 @@ dependencies: desktop_multi_window: git: url: https://github.com/Kingtous/rustdesk_desktop_multi_window - ref: 8ee8eb59cabf6ac83a13fe002de7d4a231263a58 + ref: cb086219bd4760a95a483cb14c1791d2a39ca5a0 freezed_annotation: ^2.0.3 tray_manager: git: From f343478016514481df79c6859688c69b15365877 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sat, 26 Nov 2022 11:40:13 +0800 Subject: [PATCH 1048/2015] opt: hide main window when using shortcut --- flutter/lib/common.dart | 34 +++++++++++++------ flutter/lib/consts.dart | 1 + .../lib/desktop/pages/desktop_home_page.dart | 15 +++++--- flutter/lib/main.dart | 11 ++++-- flutter/pubspec.lock | 18 +++++----- 5 files changed, 52 insertions(+), 27 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 483aba384..fed58e999 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -6,6 +6,7 @@ import 'dart:typed_data'; import 'package:back_button_interceptor/back_button_interceptor.dart'; import 'package:desktop_multi_window/desktop_multi_window.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -321,7 +322,7 @@ void window_on_top(int? id) { windowManager.restore(); windowManager.show(); windowManager.focus(); - rustDeskWinManager.registerActiveWindow(0); + rustDeskWinManager.registerActiveWindow(kWindowMainId); } else { WindowController.fromWindowId(id) ..focus() @@ -1227,41 +1228,50 @@ StreamSubscription? listenUniLinks() { return sub; } -void checkArguments() { +/// Returns true if we successfully handle the startup arguments. +bool checkArguments() { // check connect args final connectIndex = bootArgs.indexOf("--connect"); if (connectIndex == -1) { - return; + return false; } String? arg = bootArgs.length < connectIndex + 1 ? null : bootArgs[connectIndex + 1]; if (arg != null) { if (arg.startsWith(kUniLinksPrefix)) { - parseRustdeskUri(arg); + return parseRustdeskUri(arg); } else { + // remove "--connect xxx" in the `bootArgs` array + bootArgs.removeAt(connectIndex); + bootArgs.removeAt(connectIndex); // fallback to peer id - rustDeskWinManager.newRemoteDesktop(arg); - bootArgs.removeAt(connectIndex); - bootArgs.removeAt(connectIndex); + Future.delayed(Duration.zero, () { + rustDeskWinManager.newRemoteDesktop(arg); + }); + return true; } } + return false; } /// Parse `rustdesk://` unilinks /// +/// Returns true if we successfully handle the uri provided. /// [Functions] /// 1. New Connection: rustdesk://connection/new/your_peer_id -void parseRustdeskUri(String uriPath) { +bool parseRustdeskUri(String uriPath) { final uri = Uri.tryParse(uriPath); if (uri == null) { print("uri is not valid: $uriPath"); - return; + return false; } - callUniLinksUriHandler(uri); + return callUniLinksUriHandler(uri); } /// uri handler -void callUniLinksUriHandler(Uri uri) { +/// +/// Returns true if we successfully handle the uri provided. +bool callUniLinksUriHandler(Uri uri) { debugPrint("uni links called: $uri"); // new connection if (uri.authority == "connection" && uri.path.startsWith("/new/")) { @@ -1269,7 +1279,9 @@ void callUniLinksUriHandler(Uri uri) { Future.delayed(Duration.zero, () { rustDeskWinManager.newRemoteDesktop(peerId); }); + return true; } + return false; } /// Connect to a peer with [id]. diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 4d3cd8cd9..ae0d334e2 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -20,6 +20,7 @@ const String kTabLabelHomePage = "Home"; const String kTabLabelSettingPage = "Settings"; const String kWindowPrefix = "wm_"; +const int kWindowMainId = 0; // the executable name of the portable version const String kEnvPortableExecutable = "RUSTDESK_APPNAME"; diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index f7b07cf3a..4be64eee0 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -420,7 +420,17 @@ class _DesktopHomePageState extends State // initTray(); trayManager.addListener(this); rustDeskWinManager.registerActiveWindowListener(onActiveWindowChanged); - rustDeskWinManager.registerActiveWindow(0); + // main window may be hidden because of the initial uni link or arguments. + // note that we must wrap this active window registration in future because + // we must ensure the execution is after `windowManager.hide/show()`. + Future.delayed(Duration.zero, () { + windowManager.isVisible().then((visibility) { + if (visibility) { + rustDeskWinManager.registerActiveWindow(kWindowMainId); + } + }); + }); + rustDeskWinManager.setMethodHandler((call, fromWindowId) async { debugPrint( "[Main] call ${call.method} with args ${call.arguments} from window $fromWindowId"); @@ -455,9 +465,6 @@ class _DesktopHomePageState extends State rustDeskWinManager.unregisterActiveWindow(call.arguments["id"]); } }); - Future.delayed(Duration.zero, () { - checkArguments(); - }); _uniLinksSubscription = listenUniLinks(); } diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 44db1436e..8af2a477f 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -118,13 +118,18 @@ void runMainApp(bool startService) async { gFFI.serverModel.startService(); } runApp(App()); + // check the startup argument, if we successfully handle the argument, we keep the main window hidden. + if (checkArguments()) { + windowManager.hide(); + } else { + windowManager.show(); + windowManager.focus(); + } // set window option WindowOptions windowOptions = getHiddenTitleBarWindowOptions(); windowManager.waitUntilReadyToShow(windowOptions, () async { restoreWindowPosition(WindowType.Main); - await windowManager.show(); - await windowManager.focus(); - await windowManager.setOpacity(1); + windowManager.setOpacity(1); }); } diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 738567b22..29726a365 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -35,7 +35,7 @@ packages: name: archive url: "https://pub.flutter-io.cn" source: hosted - version: "3.3.4" + version: "3.3.5" args: dependency: transitive description: @@ -257,8 +257,8 @@ packages: dependency: "direct main" description: path: "." - ref: "8ee8eb59cabf6ac83a13fe002de7d4a231263a58" - resolved-ref: "8ee8eb59cabf6ac83a13fe002de7d4a231263a58" + ref: cb086219bd4760a95a483cb14c1791d2a39ca5a0 + resolved-ref: cb086219bd4760a95a483cb14c1791d2a39ca5a0 url: "https://github.com/Kingtous/rustdesk_desktop_multi_window" source: git version: "0.1.0" @@ -462,7 +462,7 @@ packages: name: frontend_server_client url: "https://pub.flutter-io.cn" source: hosted - version: "3.1.0" + version: "3.2.0" get: dependency: "direct main" description: @@ -735,7 +735,7 @@ packages: name: path_provider_android url: "https://pub.flutter-io.cn" source: hosted - version: "2.0.21" + version: "2.0.22" path_provider_ios: dependency: transitive description: @@ -1054,14 +1054,14 @@ packages: name: url_launcher url: "https://pub.flutter-io.cn" source: hosted - version: "6.1.6" + version: "6.1.7" url_launcher_android: dependency: transitive description: name: url_launcher_android url: "https://pub.flutter-io.cn" source: hosted - version: "6.0.21" + version: "6.0.22" url_launcher_ios: dependency: transitive description: @@ -1124,7 +1124,7 @@ packages: name: video_player url: "https://pub.flutter-io.cn" source: hosted - version: "2.4.7" + version: "2.4.8" video_player_android: dependency: transitive description: @@ -1215,7 +1215,7 @@ packages: name: win32 url: "https://pub.flutter-io.cn" source: hosted - version: "3.1.1" + version: "3.1.2" win32_registry: dependency: transitive description: From 280ff84b1c9ab71a3ddf8574091a59c737b2fcb4 Mon Sep 17 00:00:00 2001 From: fufesou Date: Sat, 26 Nov 2022 12:38:12 +0800 Subject: [PATCH 1049/2015] fix remote cursor, when scrollbar is checked Signed-off-by: fufesou --- flutter/lib/desktop/pages/remote_page.dart | 44 ++++++++++++++-------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index c0a05c6d5..6d0d9a047 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -506,7 +506,6 @@ class CursorPaint extends StatelessWidget { Widget build(BuildContext context) { final m = Provider.of(context); final c = Provider.of(context); - // final adjust = m.adjustForKeyboard(); double hotx = m.hotx; double hoty = m.hoty; if (m.image == null) { @@ -515,21 +514,34 @@ class CursorPaint extends StatelessWidget { hoty = preDefaultCursor.image!.height / 2; } } - return zoomCursor.isTrue - ? CustomPaint( - painter: ImagePainter( - image: m.image ?? preDefaultCursor.image, - x: m.x - hotx + c.x / c.scale, - y: m.y - hoty + c.y / c.scale, - scale: c.scale), - ) - : CustomPaint( - painter: ImagePainter( - image: m.image ?? preDefaultCursor.image, - x: (m.x - hotx) * c.scale + c.x, - y: (m.y - hoty) * c.scale + c.y, - scale: 1.0), - ); + + double cx = c.x; + double cy = c.y; + if (c.scrollStyle == ScrollStyle.scrollbar) { + final d = c.parent.target!.ffiModel.display; + final imageWidth = d.width * c.scale; + final imageHeight = d.height * c.scale; + cx = -imageWidth * c.scrollX; + cy = -imageHeight * c.scrollY; + } + + double x = (m.x - hotx) * c.scale + cx; + double y = (m.y - hoty) * c.scale + cx; + double scale = 1.0; + if (zoomCursor.isTrue) { + x = m.x - hotx + cx / c.scale; + y = m.y - hoty + cy / c.scale; + scale = c.scale; + } + + return CustomPaint( + painter: ImagePainter( + image: m.image ?? preDefaultCursor.image, + x: x, + y: y, + scale: scale, + ), + ); } } From 4da9ecc4c72127e60beebee89bf8f143546ad02f Mon Sep 17 00:00:00 2001 From: kingtous Date: Sat, 26 Nov 2022 18:26:01 +0800 Subject: [PATCH 1050/2015] opt: add necessary deps for flatpak --- .github/workflows/flutter-nightly.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index c3c6aba27..0f99d5d51 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -1168,7 +1168,7 @@ jobs: shell: /bin/bash install: | apt update -y - apt install -y rpm + apt install -y rpm git wget curl run: | # disable git safe.directory git config --global --add safe.directory "*" From b0f3fff3a0a067ef448cb376af990285bcbc669f Mon Sep 17 00:00:00 2001 From: Agent-JY Date: Sat, 26 Nov 2022 17:39:47 +0100 Subject: [PATCH 1051/2015] Update de.rs Minor changes --- 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 0197b099a..c2290b95c 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -56,7 +56,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Too frequent", "Zu häufig"), ("Cancel", "Abbrechen"), ("Skip", "Überspringen"), - ("Close", "Schließen"), + ("Close", "Sitzung beenden"), ("Retry", "Erneut versuchen"), ("OK", "OK"), ("Password Required", "Passwort erforderlich"), @@ -314,10 +314,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("remote_restarting_tip", "Entferntes Gerät startet neu, bitte schließen Sie diese Meldung und verbinden Sie sich mit dem permanenten Passwort erneut."), ("Copied", "Kopiert"), ("Exit Fullscreen", "Vollbild beenden"), - ("Fullscreen", "Ganzer Bildschirm"), + ("Fullscreen", "Vollbild"), ("Mobile Actions", "Mobile Aktionen"), - ("Select Monitor", "Wählen Sie Überwachen aus"), - ("Control Actions", "Kontrollaktionen"), + ("Select Monitor", "Bildschirm auswählen"), + ("Control Actions", "Aktionen"), ("Display Settings", "Bildschirmeinstellungen"), ("Ratio", "Verhältnis"), ("Image Quality", "Bildqualität"), @@ -328,8 +328,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Relay Connection", "Relaisverbindung"), ("Secure Connection", "Sichere Verbindung"), ("Insecure Connection", "Unsichere Verbindung"), - ("Scale original", "Original skalieren"), - ("Scale adaptive", "Adaptiv skalieren"), + ("Scale original", "Keine Saklierung"), + ("Scale adaptive", "Automatische Saklierung"), ("General", "Allgemein"), ("Security", "Sicherheit"), ("Account", "Konto"), @@ -354,7 +354,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Use IP Whitelisting", "IP-Whitelist benutzen"), ("Network", "Netzwerk"), ("Enable RDP", "RDP aktivieren"), - ("Pin menubar", "Pin-Menüleiste"), + ("Pin menubar", "Menüleiste anpinnen"), ("Unpin menubar", "Menüleiste lösen"), ("Recording", "Aufnahme"), ("Directory", "Verzeichnis"), From c083ecb4f3d8c814ca7ba05b73b29013f9f091d0 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sun, 27 Nov 2022 00:53:43 +0800 Subject: [PATCH 1052/2015] opt: use flutter-elinux 3.0.5 for arm64 --- .github/workflows/flutter-nightly.yml | 28 +++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 0f99d5d51..b5123c8fa 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -746,13 +746,16 @@ jobs: - name: Download Flutter shell: bash run: | + # disable git safe.directory + git config --global --add safe.directory "*" pushd /opt - # Currently 3.0.5 does not support arm build - git clone https://github.com/flutter/flutter.git -b stable || true - pushd flutter - git fetch origin && git reset --hard origin/stable - # TODO: `flutter_improved_scrolling` needs to be revised to support arm64 trackpad. - # sed xxx + # clone repo and reset to flutter 3.0.5 + git clone https://github.com/sony/flutter-elinux.git || true + pushd flutter-elinux + # reset to flutter 3.0.5 + git fetch + git reset --hard b09a90eee643859ce4e676839227edd9fd3feba8 + popd - uses: Kingtous/run-on-arch-action@amd64-support name: Build rustdesk binary for ${{ matrix.job.arch }} @@ -766,7 +769,7 @@ jobs: dockerRunArgs: | --volume "${PWD}:/workspace" --volume "/opt/artifacts:/opt/artifacts" - --volume "/opt/flutter:/opt/flutter" + --volume "/opt/flutter-elinux:/opt/flutter-elinux" shell: /bin/bash install: | apt update -y @@ -774,12 +777,13 @@ jobs: run: | # disable git safe.directory git config --global --add safe.directory "*" - # Setup Flutter - export PATH=/opt/flutter/bin:$PATH - flutter doctor -v - flutter precache pushd /workspace - # edit to arm64 + # we use flutter-elinux to build our rustdesk + sed -i "s/flutter build linux --release/flutter-elinux build linux/g" ./build.py + # Setup flutter-elinux + export PATH=/opt/flutter-elinux/bin:$PATH + flutter-elinux doctor -v + # edit to corresponding arch case ${{ matrix.job.arch }} in aarch64) sed -i "s/Architecture: amd64/Architecture: arm64/g" ./build.py From 48501302ac270b309c717b80cb558446e74a451f Mon Sep 17 00:00:00 2001 From: solokot Date: Sat, 26 Nov 2022 20:17:55 +0300 Subject: [PATCH 1053/2015] Update ru.rs --- src/lang/ru.rs | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/lang/ru.rs b/src/lang/ru.rs index e318b7cda..81a427d51 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -358,12 +358,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Unpin menubar", "Открепить строку меню"), ("Recording", "Запись"), ("Directory", "Папка"), - ("Automatically record incoming sessions", "Автоматически записывать входящие сессии"), + ("Automatically record incoming sessions", "Автоматически записывать входящие сеансы"), ("Change", "Изменить"), - ("Start session recording", "Начать запись сессии"), - ("Stop session recording", "Остановить запись сессии"), - ("Enable Recording Session", "Включить запись сессии"), - ("Allow recording session", "Разрешить запись сессии"), + ("Start session recording", "Начать запись сеанса"), + ("Stop session recording", "Остановить запись сеанса"), + ("Enable Recording Session", "Включить запись сеанса"), + ("Allow recording session", "Разрешить запись сеанса"), ("Enable LAN Discovery", "Включить обнаружение в локальной сети"), ("Deny LAN Discovery", "Запретить обнаружение в локальной сети"), ("Write a message", "Написать сообщение"), @@ -387,15 +387,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Continue with", "Продолжить с"), ("Elevate", "Повысить"), ("Zoom cursor", "Масштабировать курсор"), - ("Accept sessions via password", ""), - ("Accept sessions via click", ""), - ("Accept sessions via both", ""), - ("Please wait for the remote side to accept your session request...", ""), - ("One-time Password", ""), - ("Use one-time password", ""), - ("One-time password length", ""), - ("Request access to your device", ""), - ("Hide connection management window", ""), - ("hide_cm_tip", ""), + ("Accept sessions via password", "Принимать сеансы по паролю"), + ("Accept sessions via click", "Принимать сеансы по нажатию"), + ("Accept sessions via both", "Принимать сеансы по паролю+нажатию"), + ("Please wait for the remote side to accept your session request...", "Подождите, пока удалённая сторона примет ваш запрос на сеанс..."), + ("One-time Password", "Одноразовый пароль"), + ("Use one-time password", "Использовать одноразовый пароль"), + ("One-time password length", "Длина одноразового пароля"), + ("Request access to your device", "Запрос доступа к вашему устройству"), + ("Hide connection management window", "Скрывать окно управления соединениями"), + ("hide_cm_tip", "Разрешать скрытие случае, если принимаются сеансы по паролю или используется постоянный пароль"), ].iter().cloned().collect(); } From 7f897223ff17c70a07a60abd377130f731741159 Mon Sep 17 00:00:00 2001 From: mehdi-song Date: Sun, 27 Nov 2022 08:43:18 +0330 Subject: [PATCH 1054/2015] 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 4dfb22621..ac4a8771b 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -393,8 +393,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("One-time Password", "رمز عبور یکبار مصرف"), ("Use one-time password", "استفاده از رمز عبور یکبار مصرف"), ("One-time password length", "طول رمز عبور یکبار مصرف"), - ("Request access to your device", ""), - ("Hide connection management window", ""), - ("hide_cm_tip", ""), + ("Request access to your device", "دسترسی به دستگاه خود را درخواست کنید"), + ("Hide connection management window", "پنهان کردن پنجره مدیریت اتصال"), + ("hide_cm_tip", "فقط در صورت پذیرفتن جلسات از طریق رمز عبور و استفاده از رمز عبور دائمی، مخفی شدن مجاز است"), ].iter().cloned().collect(); } From 51cfa6f6660be4f261a5481f2d4ae3b9baa96e06 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sun, 27 Nov 2022 12:31:53 +0800 Subject: [PATCH 1055/2015] run_as_user use vec arg Signed-off-by: 21pages --- src/platform/linux.rs | 7 ++++--- src/platform/macos.rs | 9 ++++----- src/platform/windows.rs | 6 +++--- src/server/connection.rs | 13 ++++++------- 4 files changed, 17 insertions(+), 18 deletions(-) diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 8c0df67dd..c8abe432e 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -324,7 +324,7 @@ pub fn start_os_service() { ) { stop_rustdesk_servers(); std::thread::sleep(std::time::Duration::from_millis(super::SERVICE_INTERVAL)); - match run_as_user("--server", Some((cur_uid, cur_user))) { + match run_as_user(vec!["--server"], Some((cur_uid, cur_user))) { Ok(ps) => user_server = ps, Err(err) => { log::error!("Failed to start server: {}", err); @@ -566,7 +566,7 @@ fn is_opensuse() -> bool { } pub fn run_as_user( - arg: &str, + arg: Vec<&str>, user: Option<(String, String)>, ) -> ResultType> { let (uid, username) = match user { @@ -575,7 +575,8 @@ pub fn run_as_user( }; let cmd = std::env::current_exe()?; let xdg = &format!("XDG_RUNTIME_DIR=/run/user/{}", uid) as &str; - let mut args = vec![xdg, "-u", &username, cmd.to_str().unwrap_or(""), arg]; + let mut args = vec![xdg, "-u", &username, cmd.to_str().unwrap_or("")]; + args.append(&mut arg.clone()); // -E required for opensuse if is_opensuse() { args.insert(0, "-E"); diff --git a/src/platform/macos.rs b/src/platform/macos.rs index 9a474321e..edb2aadb1 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -394,12 +394,12 @@ pub fn is_root() -> bool { crate::username() == "root" } -pub fn run_as_user(arg: &str) -> ResultType> { +pub fn run_as_user(arg: Vec<&str>) -> ResultType> { let uid = get_active_userid(); let cmd = std::env::current_exe()?; - let task = std::process::Command::new("launchctl") - .args(vec!["asuser", &uid, cmd.to_str().unwrap_or(""), arg]) - .spawn()?; + let mut args = vec!["asuser", &uid, cmd.to_str().unwrap_or("")]; + args.append(&mut arg.clone()); + let task = std::process::Command::new("launchctl").args(args).spawn()?; Ok(Some(task)) } @@ -538,7 +538,6 @@ pub fn quit_gui() { }; } - pub fn get_double_click_time() -> u32 { // to-do: https://github.com/servo/core-foundation-rs/blob/786895643140fa0ee4f913d7b4aeb0c4626b2085/cocoa/src/appkit.rs#L2823 500 as _ diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 19c092f29..075f7ed08 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -580,11 +580,11 @@ async fn launch_server(session_id: DWORD, close_first: bool) -> ResultType ResultType> { +pub fn run_as_user(arg: Vec<&str>) -> ResultType> { let cmd = format!( "\"{}\" {}", std::env::current_exe()?.to_str().unwrap_or(""), - arg, + arg.join(" "), ); let session_id = unsafe { get_current_session(share_rdp()) }; use std::os::windows::ffi::OsStrExt; @@ -596,7 +596,7 @@ pub fn run_as_user(arg: &str) -> ResultType> { let h = unsafe { LaunchProcessWin(wstr, session_id, TRUE) }; if h.is_null() { bail!( - "Failed to launch {} with session id {}: {}", + "Failed to launch {:?} with session id {}: {}", arg, session_id, get_error() diff --git a/src/server/connection.rs b/src/server/connection.rs index 249dadc5e..fb281adde 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1571,18 +1571,21 @@ async fn start_ipc( if let Ok(s) = crate::ipc::connect(1000, "_cm").await { stream = Some(s); } else { - let extra_args = if password::hide_cm() { "--hide" } else { "" }; + let mut args = vec!["--cm"]; + if password::hide_cm() { + args.push("--hide"); + }; let run_done; if crate::platform::is_root() { let mut res = Ok(None); for _ in 0..10 { #[cfg(not(target_os = "linux"))] { - res = crate::platform::run_as_user(&format!("--cm {}", extra_args)); + res = crate::platform::run_as_user(args.clone()); } #[cfg(target_os = "linux")] { - res = crate::platform::run_as_user(&format!("--cm {}", extra_args), None); + res = crate::platform::run_as_user(args.clone(), None); } if res.is_ok() { break; @@ -1597,10 +1600,6 @@ async fn start_ipc( run_done = false; } if !run_done { - let mut args = vec!["--cm"]; - if !extra_args.is_empty() { - args.push(&extra_args); - } super::CHILD_PROCESS .lock() .unwrap() From 248f18f0d8092eea16a734320a3679ccbe3edade Mon Sep 17 00:00:00 2001 From: 21pages Date: Sun, 27 Nov 2022 16:56:02 +0800 Subject: [PATCH 1056/2015] specify linux cm target size at beginning Signed-off-by: 21pages --- flutter/linux/main.cc | 6 ++++++ flutter/linux/my_application.cc | 9 ++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/flutter/linux/main.cc b/flutter/linux/main.cc index d409bfd2b..aae79349a 100644 --- a/flutter/linux/main.cc +++ b/flutter/linux/main.cc @@ -4,6 +4,7 @@ #define RUSTDESK_LIB_PATH "librustdesk.so" // #define RUSTDESK_LIB_PATH "/usr/lib/rustdesk/librustdesk.so" typedef bool (*RustDeskCoreMain)(); +bool gIsConnectionManager = false; bool flutter_rustdesk_core_main() { void* librustdesk = dlopen(RUSTDESK_LIB_PATH, RTLD_LAZY); @@ -24,6 +25,11 @@ int main(int argc, char** argv) { if (!flutter_rustdesk_core_main()) { return 0; } + for (int i = 0; i < argc; i++) { + if (strcmp(argv[i], "--cm") == 0) { + gIsConnectionManager = true; + } + } g_autoptr(MyApplication) app = my_application_new(); return g_application_run(G_APPLICATION(app), argc, argv); } diff --git a/flutter/linux/my_application.cc b/flutter/linux/my_application.cc index 97af444fa..215c6f0ee 100644 --- a/flutter/linux/my_application.cc +++ b/flutter/linux/my_application.cc @@ -14,6 +14,8 @@ struct _MyApplication { G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) +extern bool gIsConnectionManager; + // Implements GApplication::activate. static void my_application_activate(GApplication* application) { MyApplication* self = MY_APPLICATION(application); @@ -51,7 +53,12 @@ static void my_application_activate(GApplication* application) { // auto bdw = bitsdojo_window_from(window); // <--- add this line // bdw->setCustomFrame(true); // <-- add this line - gtk_window_set_default_size(window, 800, 600); // <-- comment this line + int width = 800, height = 600; + if (gIsConnectionManager) { + width = 300; + height = 400; + } + gtk_window_set_default_size(window, width, height); // <-- comment this line gtk_widget_show(GTK_WIDGET(window)); gtk_widget_set_opacity(GTK_WIDGET(window), 0); From 6de3488f63c9a8ea26551d06c8b53a4b74f024bf Mon Sep 17 00:00:00 2001 From: rustdesk Date: Mon, 28 Nov 2022 16:14:24 +0800 Subject: [PATCH 1057/2015] remove more pipewire --- flutter/pubspec.lock | 336 +++++++++++++++++++++---------------------- res/rpm-flutter.spec | 2 +- 2 files changed, 169 insertions(+), 169 deletions(-) diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 29726a365..7440b5b9e 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -5,252 +5,252 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "50.0.0" after_layout: dependency: transitive description: name: after_layout - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.2.0" analyzer: dependency: transitive description: name: analyzer - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "5.2.0" animations: dependency: transitive description: name: animations - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.7" archive: dependency: transitive description: name: archive - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.3.5" args: dependency: transitive description: name: args - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.3.1" async: dependency: transitive description: name: async - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.9.0" auto_size_text: dependency: "direct main" description: name: auto_size_text - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.0" back_button_interceptor: dependency: "direct main" description: name: back_button_interceptor - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "6.0.2" bot_toast: dependency: "direct main" description: name: bot_toast - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "4.0.3" build: dependency: transitive description: name: build - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.3.1" build_config: dependency: transitive description: name: build_config - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.1.1" build_daemon: dependency: transitive description: name: build_daemon - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.1.0" build_resolvers: dependency: transitive description: name: build_resolvers - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.10" build_runner: dependency: "direct dev" description: name: build_runner - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.3.2" build_runner_core: dependency: transitive description: name: build_runner_core - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "7.2.7" built_collection: dependency: transitive description: name: built_collection - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "5.1.1" built_value: dependency: transitive description: name: built_value - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "8.4.2" cached_network_image: dependency: transitive description: name: cached_network_image - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.2.1" cached_network_image_platform_interface: dependency: transitive description: name: cached_network_image_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.0" cached_network_image_web: dependency: transitive description: name: cached_network_image_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.1" characters: dependency: transitive description: name: characters - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.2.0" charcode: dependency: transitive description: name: charcode - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.3.1" checked_yaml: dependency: transitive description: name: checked_yaml - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.1" clock: dependency: transitive description: name: clock - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.1.0" code_builder: dependency: transitive description: name: code_builder - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "4.3.0" collection: dependency: transitive description: name: collection - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.16.0" contextmenu: dependency: "direct main" description: name: contextmenu - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.0" convert: dependency: transitive description: name: convert - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.1.0" cross_file: dependency: transitive description: name: cross_file - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.3.3+2" crypto: dependency: transitive description: name: crypto - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.2" csslib: dependency: transitive description: name: csslib - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.17.2" cupertino_icons: dependency: "direct main" description: name: cupertino_icons - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.5" dart_style: dependency: transitive description: name: dart_style - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.2.4" dash_chat_2: dependency: "direct main" description: name: dash_chat_2 - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.0.15" desktop_drop: dependency: "direct main" description: name: desktop_drop - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.3.3" desktop_multi_window: @@ -266,91 +266,91 @@ packages: dependency: "direct main" description: name: device_info_plus - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "4.1.3" device_info_plus_linux: dependency: transitive description: name: device_info_plus_linux - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.0" device_info_plus_macos: dependency: transitive description: name: device_info_plus_macos - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.0" device_info_plus_platform_interface: dependency: transitive description: name: device_info_plus_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.0" device_info_plus_web: dependency: transitive description: name: device_info_plus_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.0" device_info_plus_windows: dependency: transitive description: name: device_info_plus_windows - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "4.1.0" draggable_float_widget: dependency: "direct main" description: name: draggable_float_widget - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.0.2" event_bus: dependency: transitive description: name: event_bus - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.0" external_path: dependency: "direct main" description: name: external_path - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.3" ffi: dependency: "direct main" description: name: ffi - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.1" file: dependency: transitive description: name: file - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "6.1.4" file_picker: dependency: "direct main" description: name: file_picker - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "5.2.2" fixnum: dependency: transitive description: name: fixnum - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.1" flutter: @@ -362,21 +362,21 @@ packages: dependency: transitive description: name: flutter_blurhash - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.7.0" flutter_breadcrumb: dependency: "direct main" description: name: flutter_breadcrumb - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.1" flutter_cache_manager: dependency: transitive description: name: flutter_cache_manager - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.3.0" flutter_custom_cursor: @@ -392,14 +392,14 @@ packages: dependency: "direct main" description: name: flutter_improved_scrolling - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.0.3" flutter_lints: dependency: "direct dev" description: name: flutter_lints - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.1" flutter_localizations: @@ -411,14 +411,14 @@ packages: dependency: transitive description: name: flutter_parsed_text - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.2.1" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.7" flutter_rust_bridge: @@ -434,7 +434,7 @@ packages: dependency: "direct main" description: name: flutter_svg - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.1.6" flutter_web_plugins: @@ -446,413 +446,413 @@ packages: dependency: "direct dev" description: name: freezed - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.2.1" freezed_annotation: dependency: "direct main" description: name: freezed_annotation - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.2.0" frontend_server_client: dependency: transitive description: name: frontend_server_client - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.2.0" get: dependency: "direct main" description: name: get - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "4.6.5" glob: dependency: transitive description: name: glob - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.1" graphs: dependency: transitive description: name: graphs - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.2.0" html: dependency: transitive description: name: html - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.15.1" http: dependency: "direct main" description: name: http - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.13.5" http_multi_server: dependency: transitive description: name: http_multi_server - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.2.1" http_parser: dependency: transitive description: name: http_parser - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "4.0.2" icons_launcher: dependency: "direct dev" description: name: icons_launcher - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.4" image: dependency: "direct main" description: name: image - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.2.2" image_picker: dependency: "direct main" description: name: image_picker - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.8.6" image_picker_android: dependency: transitive description: name: image_picker_android - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.8.5+3" image_picker_for_web: dependency: transitive description: name: image_picker_for_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.10" image_picker_ios: dependency: transitive description: name: image_picker_ios - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.8.6+1" image_picker_platform_interface: dependency: transitive description: name: image_picker_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.6.2" intl: dependency: transitive description: name: intl - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.17.0" io: dependency: transitive description: name: io - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.3" js: dependency: transitive description: name: js - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.6.4" json_annotation: dependency: transitive description: name: json_annotation - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "4.7.0" lints: dependency: transitive description: name: lints - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.1" logging: dependency: transitive description: name: logging - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.1.0" matcher: dependency: transitive description: name: matcher - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.12.12" material_color_utilities: dependency: transitive description: name: material_color_utilities - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.1.4" menu_base: dependency: transitive description: name: menu_base - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.1.1" meta: dependency: transitive description: name: meta - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.7.0" mime: dependency: transitive description: name: mime - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.2" nested: dependency: transitive description: name: nested - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.0" octo_image: dependency: transitive description: name: octo_image - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.2" package_config: dependency: transitive description: name: package_config - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.0" package_info_plus: dependency: "direct main" description: name: package_info_plus - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.4.3+1" package_info_plus_linux: dependency: transitive description: name: package_info_plus_linux - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.5" package_info_plus_macos: dependency: transitive description: name: package_info_plus_macos - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.3.0" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.2" package_info_plus_web: dependency: transitive description: name: package_info_plus_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.6" package_info_plus_windows: dependency: transitive description: name: package_info_plus_windows - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.0" path: dependency: "direct main" description: name: path - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.8.1" path_drawing: dependency: transitive description: name: path_drawing - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.1" path_parsing: dependency: transitive description: name: path_parsing - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.1" path_provider: dependency: "direct main" description: name: path_provider - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.11" path_provider_android: dependency: transitive description: name: path_provider_android - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.22" path_provider_ios: dependency: transitive description: name: path_provider_ios - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.11" path_provider_linux: dependency: transitive description: name: path_provider_linux - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.7" path_provider_macos: dependency: transitive description: name: path_provider_macos - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.6" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.5" path_provider_windows: dependency: transitive description: name: path_provider_windows - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.3" pedantic: dependency: transitive description: name: pedantic - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.11.1" petitparser: dependency: transitive description: name: petitparser - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "5.0.0" platform: dependency: transitive description: name: platform - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.1.0" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.3" pointycastle: dependency: transitive description: name: pointycastle - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.6.2" pool: dependency: transitive description: name: pool - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.5.1" process: dependency: transitive description: name: process - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "4.2.4" provider: dependency: "direct main" description: name: provider - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "6.0.4" pub_semver: dependency: transitive description: name: pub_semver - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.3" pubspec_parse: dependency: transitive description: name: pubspec_parse - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.2.1" qr_code_scanner: dependency: "direct main" description: name: qr_code_scanner - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.1" rxdart: dependency: "direct main" description: name: rxdart - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.27.7" screen_retriever: @@ -868,35 +868,35 @@ packages: dependency: "direct main" description: name: scroll_pos - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.3.0" settings_ui: dependency: "direct main" description: name: settings_ui - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.2" shelf: dependency: transitive description: name: shelf - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.4.0" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.3" shortid: dependency: transitive description: name: shortid - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.1.2" sky_engine: @@ -908,84 +908,84 @@ packages: dependency: transitive description: name: source_gen - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.2.6" source_span: dependency: transitive description: name: source_span - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.9.1" sqflite: dependency: transitive description: name: sqflite - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.3+1" sqflite_common: dependency: transitive description: name: sqflite_common - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.4.0" stack_trace: dependency: transitive description: name: stack_trace - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.10.0" stream_channel: dependency: transitive description: name: stream_channel - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.1" stream_transform: dependency: transitive description: name: stream_transform - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.0" string_scanner: dependency: transitive description: name: string_scanner - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.1.1" synchronized: dependency: transitive description: name: synchronized - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.0+3" term_glyph: dependency: transitive description: name: term_glyph - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.2.1" timing: dependency: transitive description: name: timing - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.0" toggle_switch: dependency: "direct main" description: name: toggle_switch - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.4.0" tray_manager: @@ -1001,21 +1001,21 @@ packages: dependency: "direct main" description: name: tuple - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.1" typed_data: dependency: transitive description: name: typed_data - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.3.1" uni_links: dependency: "direct main" description: name: uni_links - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.5.1" uni_links_desktop: @@ -1031,196 +1031,196 @@ packages: dependency: transitive description: name: uni_links_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.0" uni_links_web: dependency: transitive description: name: uni_links_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.1.0" universal_io: dependency: transitive description: name: universal_io - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.4" url_launcher: dependency: "direct main" description: name: url_launcher - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "6.1.7" url_launcher_android: dependency: transitive description: name: url_launcher_android - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "6.0.22" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "6.0.17" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.1" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.1" url_launcher_web: dependency: transitive description: name: url_launcher_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.13" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.1" uuid: dependency: transitive description: name: uuid - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.7" vector_math: dependency: transitive description: name: vector_math - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.2" video_player: dependency: transitive description: name: video_player - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.4.8" video_player_android: dependency: transitive description: name: video_player_android - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.3.9" video_player_avfoundation: dependency: transitive description: name: video_player_avfoundation - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.3.7" video_player_platform_interface: dependency: transitive description: name: video_player_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "5.1.4" video_player_web: dependency: transitive description: name: video_player_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.12" visibility_detector: dependency: "direct main" description: name: visibility_detector - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.3.3" wakelock: dependency: "direct main" description: name: wakelock - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.6.2" wakelock_macos: dependency: transitive description: name: wakelock_macos - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.4.0" wakelock_platform_interface: dependency: transitive description: name: wakelock_platform_interface - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.3.0" wakelock_web: dependency: transitive description: name: wakelock_web - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.4.0" wakelock_windows: dependency: transitive description: name: wakelock_windows - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.2.1" watcher: dependency: transitive description: name: watcher - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.2" web_socket_channel: dependency: transitive description: name: web_socket_channel - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.2.0" win32: dependency: transitive description: name: win32 - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.1.2" win32_registry: dependency: transitive description: name: win32_registry - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.2" window_manager: @@ -1245,28 +1245,28 @@ packages: dependency: transitive description: name: xdg_directories - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.2.0+2" xml: dependency: transitive description: name: xml - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "6.1.0" yaml: dependency: transitive description: name: yaml - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.1.1" zxing2: dependency: "direct main" description: name: zxing2 - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.1.0" sdks: diff --git a/res/rpm-flutter.spec b/res/rpm-flutter.spec index 2ba1cbfa4..73bb993aa 100644 --- a/res/rpm-flutter.spec +++ b/res/rpm-flutter.spec @@ -3,7 +3,7 @@ Version: 1.2.0 Release: 0 Summary: RPM package License: GPL-3.0 -Requires: gtk3 libxcb libxdo libXfixes pipewire alsa-lib curl libappindicator-gtk3 libvdpau libva +Requires: gtk3 libxcb libxdo libXfixes alsa-lib curl libappindicator-gtk3 libvdpau libva Provides: libdesktop_drop_plugin.so()(64bit), libdesktop_multi_window_plugin.so()(64bit), libflutter_custom_cursor_plugin.so()(64bit), libflutter_linux_gtk.so()(64bit), libscreen_retriever_plugin.so()(64bit), libtray_manager_plugin.so()(64bit), liburl_launcher_linux_plugin.so()(64bit), libwindow_manager_plugin.so()(64bit), libwindow_size_plugin.so()(64bit) %description From a1d4847231a9ba99e93f39655c59a8ebafb9d3a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ahmet=20=C3=87ad=C4=B1rc=C4=B1?= Date: Mon, 28 Nov 2022 12:42:13 +0300 Subject: [PATCH 1058/2015] Update tr.rs --- src/lang/tr.rs | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/lang/tr.rs b/src/lang/tr.rs index a97f832ba..0f5dd42ba 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -30,9 +30,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("IP Whitelisting", "İzinli IP listesi"), ("ID/Relay Server", "ID/Relay Sunucusu"), ("Import Server Config", "Sunucu ayarlarını içe aktar"), - ("Export Server Config", ""), + ("Export Server Config", "Sunucu Yapılandırmasını Dışa Aktar"), ("Import server configuration successfully", "Sunucu ayarları başarıyla içe aktarıldı"), - ("Export server configuration successfully", ""), + ("Export server configuration successfully", "Sunucu yapılandırmasını başarıyla dışa aktar"), ("Invalid server configuration", "Geçersiz sunucu ayarı"), ("Clipboard is empty", "Kopyalanan geçici veri boş"), ("Stop service", "Servisi Durdur"), @@ -296,8 +296,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("In privacy mode", "Gizlilik modunda"), ("Out privacy mode", "Gizlilik modu dışında"), ("Language", "Dil"), - ("Keep RustDesk background service", ""), - ("Ignore Battery Optimizations", ""), + ("Keep RustDesk background service", "RustDesk arka plan hizmetini sürdürün"), + ("Ignore Battery Optimizations", "Pil Optimizasyonlarını Yoksay"), ("android_open_battery_optimizations_tip", ""), ("Connection not allowed", "bağlantıya izin verilmedi"), ("Legacy mode", "Eski mod"), @@ -367,7 +367,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable LAN Discovery", "Yerel Ağ Keşfine İzin Ver"), ("Deny LAN Discovery", "Yerl Ağ Keşfine İzin Verme"), ("Write a message", "Bir mesaj yazın"), - ("Prompt", ""), + ("Prompt", "İstem"), ("Please wait for confirmation of UAC...", "UAC onayı için lütfen bekleyiniz..."), ("elevated_foreground_window_tip", "elevated_foreground_window_tip"), ("Disconnected", "Bağlantı Kesildi"), @@ -386,16 +386,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("or", "veya"), ("Continue with", "bununla devam et"), ("Elevate", "Yükseltme"), - ("Zoom cursor", ""), - ("Accept sessions via password", ""), - ("Accept sessions via click", ""), - ("Accept sessions via both", ""), - ("Please wait for the remote side to accept your session request...", ""), - ("One-time Password", ""), - ("Use one-time password", ""), - ("One-time password length", ""), - ("Request access to your device", ""), - ("Hide connection management window", ""), + ("Zoom cursor", "Yakınlaştırma imleci"), + ("Accept sessions via password", "Oturumları parola ile kabul etme"), + ("Accept sessions via click", "Tıklama yoluyla oturumları kabul edin"), + ("Accept sessions via both", "Her ikisi aracılığıyla oturumları kabul edin"), + ("Please wait for the remote side to accept your session request...", "Lütfen uzak tarafın oturum isteğinizi kabul etmesini bekleyin..."), + ("One-time Password", "Tek Kullanımlık Şifre"), + ("Use one-time password", "Tek seferlik parola kullanın"), + ("One-time password length", "Tek seferlik şifre uzunluğu"), + ("Request access to your device", "Cihazınıza erişim talep edin"), + ("Hide connection management window", "Bağlantı yönetimi penceresini gizle"), ("hide_cm_tip", ""), ].iter().cloned().collect(); } From ccd874ff807b71bbeaf0d513277ebdf53ecbba4e Mon Sep 17 00:00:00 2001 From: Kasper Date: Mon, 28 Nov 2022 12:49:55 +0100 Subject: [PATCH 1059/2015] Create sv.rs --- src/lang/sv.rs | 402 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 402 insertions(+) create mode 100644 src/lang/sv.rs diff --git a/src/lang/sv.rs b/src/lang/sv.rs new file mode 100644 index 000000000..52faa8fed --- /dev/null +++ b/src/lang/sv.rs @@ -0,0 +1,402 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "Status"), + ("Your Desktop", "Ditt skrivbord"), + ("desk_tip", "Ditt skrivbord kan delas med hjälp av detta ID och lösenord"), + ("Password", "Lösenord"), + ("Ready", "Redo"), + ("Established", "Uppkopplad"), + ("connecting_status", "Ansluter till RustDesk..."), + ("Enable Service", "Sätt på tjänsten"), + ("Start Service", "Starta tjänsten"), + ("Service is running", "Tjänsten är startad"), + ("Service is not running", "Tjänsten är ej startad"), + ("not_ready_status", "Ej redo. Kontrollera din nätverksanslutning"), + ("Control Remote Desktop", "Kontrollera fjärrskrivbord"), + ("Transfer File", "Överför fil"), + ("Connect", "Anslut"), + ("Recent Sessions", "Dina senaste sessioner"), + ("Address Book", "Addressbok"), + ("Confirmation", "Bekräftelse"), + ("TCP Tunneling", "TCP Tunnel"), + ("Remove", "Ta bort"), + ("Refresh random password", "Skapa nytt slumpmässigt lösenord"), + ("Set your own password", "Skapa ditt eget lösenord"), + ("Enable Keyboard/Mouse", "Tillåt tangentbord/mus"), + ("Enable Clipboard", "Tillåt urklipp"), + ("Enable File Transfer", "Tillåt filöverföring"), + ("Enable TCP Tunneling", "Tillåt TCP tunnel"), + ("IP Whitelisting", "IP Vitlisting"), + ("ID/Relay Server", "ID/Relay Server"), + ("Import Server Config", "Importera Server config"), + ("Export Server Config", "Exportera Server config"), + ("Import server configuration successfully", "Importering lyckades"), + ("Export server configuration successfully", "Exportering lyckades"), + ("Invalid server configuration", "Ogiltig server config"), + ("Clipboard is empty", "Urklippet är tomt"), + ("Stop service", "Avsluta tjänsten"), + ("Change ID", "Byt ID"), + ("Website", "Hemsida"), + ("About", "Om"), + ("Mute", "Tyst"), + ("Audio Input", "Ljud input"), + ("Enhancements", "Förbättringar"), + ("Hardware Codec", "Hårdvarucodec"), + ("Adaptive Bitrate", "Adaptiv Bitrate"), + ("ID Server", "ID server"), + ("Relay Server", "Relay Server"), + ("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"), + ("Too frequent", "För ofta"), + ("Cancel", "Avbryt"), + ("Skip", "Hoppa över"), + ("Close", "Stäng"), + ("Retry", "Försök igen"), + ("OK", "OK"), + ("Password Required", "Lösenord krävs"), + ("Please enter your password", "Skriv in ditt lösenord"), + ("Remember password", "Kom ihåg lösenord"), + ("Wrong Password", "Fel lösenord"), + ("Do you want to enter again?", "Vill du skriva in igen?"), + ("Connection Error", "Anslutningsfel"), + ("Error", "Ett fel uppstod"), + ("Reset by the peer", "Återställt av klienten"), + ("Connecting...", "Ansluter..."), + ("Connection in progress. Please wait.", "Anslutning pågår. Var god vänta."), + ("Please try 1 minute later", "Försök igen om en minut"), + ("Login Error", "Inloggningsfel"), + ("Successful", "Lyckat"), + ("Connected, waiting for image...", "Ansluten, väntar på bild..."), + ("Name", "Namn"), + ("Type", "Typ"), + ("Modified", "Modifierad"), + ("Size", "Storlek"), + ("Show Hidden Files", "Visa gömda filer"), + ("Receive", "Ta emot"), + ("Send", "Skicka"), + ("Refresh File", "Uppdatera fil"), + ("Local", "Lokalt"), + ("Remote", "Fjärr"), + ("Remote Computer", "Fjärrdator"), + ("Local Computer", "Lokal dator"), + ("Confirm Delete", "Bekräfta borttagning"), + ("Delete", "Ta bort"), + ("Properties", "Egenskaper"), + ("Multi Select", "Välj flera"), + ("Select All", "Markera alla "), + ("Unselect All", "Avmärkera alla"), + ("Empty Directory", "Tom mapp"), + ("Not an empty directory", "Inte en tom mapp"), + ("Are you sure you want to delete this file?", "Är du säker att du vill ta bort filen?"), + ("Are you sure you want to delete this empty directory?", "Är du säker att du vill ta bort den tomma mappen?"), + ("Are you sure you want to delete the file of this directory?", "Är du säker att du vill ta bort filen ur mappen?"), + ("Do this for all conflicts", "Gör för alla konflikter"), + ("This is irreversible!", "Detta går ej att ångra!"), + ("Deleting", "Tar bort"), + ("files", "filer"), + ("Waiting", "Väntnar"), + ("Finished", "Klar"), + ("Speed", "Hastighet"), + ("Custom Image Quality", "Anpassad bildkvalitet"), + ("Privacy mode", "Säkerhetsläge"), + ("Block user input", "Blokera användarinput"), + ("Unblock user input", "Tillåt användarinput"), + ("Adjust Window", "Ändra fönster"), + ("Original", "Orginal"), + ("Shrink", "Krymp"), + ("Stretch", "Sträck ut"), + ("Scrollbar", "Scrollbar"), + ("ScrollAuto", "ScrollAuto"), + ("Good image quality", "Bra bildkvalitet"), + ("Balanced", "Balanserad"), + ("Optimize reaction time", "Optimera reaktionstid"), + ("Custom", "Anpassad"), + ("Show remote cursor", "Visa fjärrmus"), + ("Show quality monitor", "Visa bildkvalitet"), + ("Disable clipboard", "Stäng av urklipp"), + ("Lock after session end", "Lås efter sessionens slut"), + ("Insert", "Insert"), + ("Insert Lock", "Insert lås"), + ("Refresh", "Uppdatera"), + ("ID does not exist", "Detta ID existerar inte"), + ("Failed to connect to rendezvous server", "Lyckades inte ansluta till randezvous servern"), + ("Please try later", "Försök igen senare"), + ("Remote desktop is offline", "Fjärrskrivbordet är offline"), + ("Key mismatch", "Nyckeln stämmer inte"), + ("Timeout", "Timeout"), + ("Failed to connect to relay server", "Lyckades inte ansluta till relay servern"), + ("Failed to connect via rendezvous server", "Lyckades inte ansluta via randezvous servern"), + ("Failed to connect via relay server", "Lyckades inte ansluta via relay servern"), + ("Failed to make direct connection to remote desktop", "Lyckades inte ansluta direkt till fjärrskrivbordet"), + ("Set Password", "Välj lösenord"), + ("OS Password", "OS lösenord"), + ("install_tip", "På grund av UAC, kan inte RustDesk fungera ordentligt på klientsidan. För att undvika UAC, tryck på knappen nedan för att installera RustDesk på systemet."), + ("Click to upgrade", "Klicka för att nedgradera"), + ("Click to download", "Klicka för att ladda ner"), + ("Click to update", "Klicka för att uppdatera"), + ("Configure", "Konfigurera"), + ("config_acc", "För att kontrollera din dator på distans måste du ge RustDesk \"Tillgänglighets\" rättigheter."), + ("config_screen", "För att kontrollera din dator på distans måste du ge RustDesk \"Skärminspelnings\" rättigheter."), + ("Installing ...", "Installerar..."), + ("Install", "Installera"), + ("Installation", "Installation"), + ("Installation Path", "Installationsplats"), + ("Create start menu shortcuts", "Skapa startmeny genväg"), + ("Create desktop icon", "Skapa ikon på skrivbordet"), + ("agreement_tip", "Genom att starta installationen accepterar du licensavtalet."), + ("Accept and Install", "Acceptera och installera"), + ("End-user license agreement", "End-user license agreement"), + ("Generating ...", "Genererar..."), + ("Your installation is lower version.", "Ditt skrivbord har en lägre version"), + ("not_close_tcp_tip", "Stäng inde detta fönster när du använder tunneln"), + ("Listening ...", "Lyssnar..."), + ("Remote Host", "Fjärrhost"), + ("Remote Port", "Fjärrport"), + ("Action", "Handling"), + ("Add", "Lägg till"), + ("Local Port", "Lokal port"), + ("Local Address", "Lokal adress"), + ("Change Local Port", "Ändra lokal port"), + ("setup_server_tip", "Sätt upp din egen server för en snabbare anslutning"), + ("Too short, at least 6 characters.", "För kort, minst 6 tecken."), + ("The confirmation is not identical.", "Bekräftelsen stämmer inte."), + ("Permissions", "Rättigheter"), + ("Accept", "Acceptera"), + ("Dismiss", "Tillåt inte"), + ("Disconnect", "Koppla ifrån"), + ("Allow using keyboard and mouse", "Tillåt tangentbord och mus"), + ("Allow using clipboard", "Tillåt urklipp"), + ("Allow hearing sound", "Tillåt att höra ljud"), + ("Allow file copy and paste", "Tillåt kopiering av filer"), + ("Connected", "Ansluten"), + ("Direct and encrypted connection", "Direkt och krypterad anslutning"), + ("Relayed and encrypted connection", "Vidarebefodrad och krypterad anslutning"), + ("Direct and unencrypted connection", "Direkt och okrypterad anslutning"), + ("Relayed and unencrypted connection", "Vidarebefodrad och okrypterad anslutning"), + ("Enter Remote ID", "Skriv in fjärr-ID"), + ("Enter your password", "Skriv in ditt lösenord"), + ("Logging in...", "Loggar in..."), + ("Enable RDP session sharing", "Tillåt RDP sessionsdelning"), + ("Auto Login", "Auto Login (Bara giltigt om du sätter \"Lås efter sessionens slut\")"), + ("Enable Direct IP Access", "Tillåt direkt IP anslutningar"), + ("Rename", "Byt namn"), + ("Space", "Mellanslag"), + ("Create Desktop Shortcut", "Skapa skrivbordsgenväg"), + ("Change Path", "Ändra plats"), + ("Create Folder", "Skapa mapp"), + ("Please enter the folder name", "Skriv in namnet på mappen"), + ("Fix it", "Fixa det"), + ("Warning", "Varning"), + ("Login screen using Wayland is not supported", "Login med Wayland stöds inte"), + ("Reboot required", "Omstart krävs"), + ("Unsupported display server ", "Displayserver stöds inte "), + ("x11 expected", "x11 förväntades"), + ("Port", "Port"), + ("Settings", "Inställningar"), + ("Username", "Användarnamn"), + ("Invalid port", "Ogiltig port"), + ("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"), + ("Always connect via relay", "Anslut alltid via relay"), + ("whitelist_tip", "Bara vitlistade IPs kan koppla upp till mig"), + ("Login", "Logga in"), + ("Logout", "Logga ut"), + ("Tags", "Taggar"), + ("Search ID", "Sök ID"), + ("Current Wayland display server is not supported", "Nuvarande Wayland displayserver stöds inte"), + ("whitelist_sep", "Separerat av ett comma, semikolon, mellanslag eller ny linje"), + ("Add ID", "Lägg till ID"), + ("Add Tag", "Lägg till Tagg"), + ("Unselect all tags", "Avmarkera alla taggar"), + ("Network error", "Nätverksfel"), + ("Username missed", "Användarnamn saknas"), + ("Password missed", "Lösenord saknas"), + ("Wrong credentials", "Fel användarnamn eller lösenord"), + ("Edit Tag", "Ändra Tagg"), + ("Unremember Password", "Glöm lösenord"), + ("Favorites", "Favoriter"), + ("Add to Favorites", "Lägg till favorit"), + ("Remove from Favorites", "Ta bort från favoriter"), + ("Empty", "Tom"), + ("Invalid folder name", "Ogiltigt mappnamn"), + ("Socks5 Proxy", "Socks5 Proxy"), + ("Hostname", "Hostname"), + ("Discovered", "Upptäckt"), + ("install_daemon_tip", "För att starta efter boot måste du installera systemtjänsten."), + ("android_input_permission_tip1", "För att kontrollera din Android-enhet med mus eller touch, måste du tillåta RustDesk att använda \"Tillgänglighets\" tjänsten."), + ("Remote ID", "Fjärr ID"), + ("Paste", "Klistra in"), + ("Paste here?", "Klistra in här?"), + ("Are you sure to close the connection?", "Är du säker att du vill avsluta anslutningen?"), + ("Download new version", "Ladda ner ny version"), + ("Touch mode", "Touchläge"), + ("Mouse mode", "Musläge"), + ("One-Finger Tap", "En fingers tryck"), + ("Left Mouse", "Vänster mus"), + ("One-Long Tap", "Långt tryck"), + ("Two-Finger Tap", "Långt tryck med två fingrar"), + ("Right Mouse", "Höger mus"), + ("One-Finger Move", "En fingers drag"), + ("Double Tap & Move", "Dubbeltryck och flytta"), + ("Mouse Drag", "Dra med musen"), + ("Three-Finger vertically", "Tre fingrar vertikalt"), + ("Mouse Wheel", "Scrollhjul"), + ("Two-Finger Move", "Två fingers flytt"), + ("Canvas Move", "Flytta canvas"), + ("Pinch to Zoom", "Nyp för zoom"), + ("Canvas Zoom", "Canvas zoom"), + ("Reset canvas", "Återställ canvas"), + ("No permission of file transfer", "Rättigheter saknas"), + ("Note", "Notering"), + ("Connection", "Anslutning"), + ("Share Screen", "Dela skärm"), + ("CLOSE", "STÄNG"), + ("OPEN", "ÖPPNA"), + ("Chat", "Chatt"), + ("Total", "Totalt"), + ("items", "föremål"), + ("Selected", "Valda"), + ("Screen Capture", "Skärminspelning"), + ("Input Control", "Inputkontroll"), + ("Audio Capture", "Ljudinspelning"), + ("File Connection", "Fil anslutning"), + ("Screen Connection", "Skärm anslutning"), + ("Do you accept?", "Accepterar du?"), + ("Open System Setting", "Öppna systeminställnig"), + ("How to get Android input permission?", "Hur får man Android rättigheter?"), + ("android_input_permission_tip1", "Android rättigheter saknas"), + ("android_input_permission_tip2", "Gå till systeminställningarna, hitta [Installed Services], sätt på [RustDesk Input] tjänsten."), + ("android_new_connection_tip", "Ny kontrollförfrågan mottagen, denna vill kontrollera din enhet."), + ("android_service_will_start_tip", "Sätter du på \"skärminspelning\" kommer tjänsten automatiskt att starta. Detta tillåter andra enheter att kontrollera din enhet."), + ("android_stop_service_tip", "Genom att stänga av tjänsten kommer alla enheter att kopplas ifrån."), + ("android_version_audio_tip", "Din version av Android stödjer inte ljudinspelning, Android 10 eller nyare krävs"), + ("android_start_service_tip", "Tryck på [Starta tjänsten] eller tillåt [skärminspelning] för att starta skärmdelning."), + ("Account", "Konto"), + ("Overwrite", "Skriv över"), + ("This file exists, skip or overwrite this file?", "Filen finns redan, hoppa över eller skriv över filen?"), + ("Quit", "Avsluta"), + ("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"), + ("Help", "Hjälp"), + ("Failed", "Misslyckades"), + ("Succeeded", "Lyckades"), + ("Someone turns on privacy mode, exit", "Någon sätter på säkerhetesläge, avsluta"), + ("Unsupported", "Stöds inte"), + ("Peer denied", "Klienten nekade"), + ("Please install plugins", "Var god installera plugins"), + ("Peer exit", "Avsluta klient"), + ("Failed to turn off", "Misslyckades med avstängning"), + ("Turned off", "Avstängd"), + ("In privacy mode", "I säkerhetsläge"), + ("Out privacy mode", "Ur säkerhetsläge"), + ("Language", "Språk"), + ("Keep RustDesk background service", "Behåll RustDesk i bakgrunden"), + ("Ignore Battery Optimizations", "Ignorera batterioptimering"), + ("android_open_battery_optimizations_tip", "Om du vill stänga av denna funktion, gå till nästa RustDesk programs inställningar, hitta [Batteri], Checka ur [Obegränsad]"), + ("Connection not allowed", "Anslutning ej tillåten"), + ("Legacy mode", "Legacy mode"), + ("Map mode", "Kartläge"), + ("Translate mode", "Översättningsläge"), + ("Use permanent password", "Använd permanent lösenord"), + ("Use both passwords", "Använd båda lösenorden"), + ("Set permanent password", "Ställ in permanent lösenord"), + ("Enable Remote Restart", "Sätt på fjärromstart"), + ("Allow remote restart", "Tillåt fjärromstart"), + ("Restart Remote Device", "Starta om fjärrenheten"), + ("Are you sure you want to restart", "Är du säker att du vill starta om?"), + ("Restarting Remote Device", "Startar om fjärrenheten"), + ("remote_restarting_tip", "Enheten startar om, stäng detta meddelande och anslut igen om en liten stund"), + ("Copied", "Kopierad"), + ("Exit Fullscreen", "Gå ur fullskärmsläge"), + ("Fullscreen", "Fullskärm"), + ("Mobile Actions", "Mobila återgärder"), + ("Select Monitor", "Välj skärm"), + ("Control Actions", "Kontroller"), + ("Display Settings", "Skärminställningar"), + ("Ratio", "Ratio"), + ("Image Quality", "Bildkvalitet"), + ("Scroll Style", "Scrollstil"), + ("Show Menubar", "Visa meny"), + ("Hide Menubar", "Stäng meny"), + ("Direct Connection", "Direktanslutning"), + ("Relay Connection", "Relayanslutning"), + ("Secure Connection", "Säker anslutning"), + ("Insecure Connection", "Osäker anslutning"), + ("Scale original", "Skala orginal"), + ("Scale adaptive", "Skala adaptivt"), + ("General", "Generellt"), + ("Security", "Säkerhet"), + ("Account", "Konto"), + ("Theme", "Tema"), + ("Dark Theme", "Mörkt tema"), + ("Dark", "Mörk"), + ("Light", "Ljus"), + ("Follow System", "Följ system"), + ("Enable hardware codec", "Aktivera hårdvarucodec"), + ("Unlock Security Settings", "Lås upp säkerhetsinställningar"), + ("Enable Audio", "Sätt på ljud"), + ("Unlock Network Settings", "Lås upp nätverksinställningar"), + ("Server", "Server"), + ("Direct IP Access", "Direkt IP åtkomst"), + ("Proxy", "Proxy"), + ("Port", "Port"), + ("Apply", "Tillämpa"), + ("Disconnect all devices?", "Koppla ifrån alla enheter?"), + ("Clear", "Töm"), + ("Audio Input Device", "Inmatningsenhet för ljud"), + ("Deny remote access", "Stäng av fjärråtkomst"), + ("Use IP Whitelisting", "Använd IP Vitlistning"), + ("Network", "Nätvärk"), + ("Enable RDP", "Aktivera RDP"), + ("Pin menubar", "Fäst meny"), + ("Unpin menubar", "Sluta fäst meny"), + ("Recording", "Spelar in"), + ("Directory", "Katalog"), + ("Automatically record incoming sessions", "Spela in inkommande sessioner automatiskt"), + ("Change", "Byt"), + ("Start session recording", "Starta inspelning"), + ("Stop session recording", "Avsluta inspelning"), + ("Enable Recording Session", "Sätt på sessionsinspelning"), + ("Allow recording session", "Tillåt sessionsinspelning"), + ("Enable LAN Discovery", "Sätt på LAN upptäckt"), + ("Deny LAN Discovery", "Neka LAN upptäckt"), + ("Write a message", "Skriv ett meddelande"), + ("Prompt", "Prompt"), + ("Please wait for confirmation of UAC...", "Var god vänta för UAC bekräftelse..."), + ("elevated_foreground_window_tip", "Detta fönster hos klienten kräver en högre behörighet. Du kan be användaren att minimera fönstret, eller att ge högre behörigheter i fönstret för anslutningsinställningar. För att undvika detta problem i framtiden, installera programmet på klientens sida."), + ("Disconnected", "Frånkopplad"), + ("Other", "Övrigt"), + ("Confirm before closing multiple tabs", "Bekräfta innan du stänger flera flikar"), + ("Keyboard Settings", "Tangentbordsinställningar"), + ("Custom", "Anpassat"), + ("Full Access", "Full tillgång"), + ("Screen Share", "Skärmdelning"), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland kräver Ubuntu 21.04 eller högre."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland kräver en högre version av linux. Försök igen eller byt OS."), + ("JumpLink", "JumpLink"), + ("Please Select the screen to be shared(Operate on the peer side).", "Välj skärm att dela"), + ("Show RustDesk", "Visa RustDesk"), + ("This PC", "Denna dator"), + ("or", "eller"), + ("Continue with", "Fortsätt med"), + ("Elevate", "Höj upp"), + ("Zoom cursor", "Zoom"), + ("Accept sessions via password", "Acceptera sessioner via lösenord"), + ("Accept sessions via click", "Acceptera sessioner via klick"), + ("Accept sessions via both", "Acceptera sessioner via båda"), + ("Please wait for the remote side to accept your session request...", "Var god vänta på att klienten accepterar din förfrågan..."), + ("One-time Password", "En-gångs lösenord"), + ("Use one-time password", "Använd en-gångs lösenord"), + ("One-time password length", "Längd på en-gångs lösenord"), + ("Request access to your device", "Begär åtkomst till din enhet"), + ("Hide connection management window", "Göm hanteringsfönster"), + ("hide_cm_tip", "Tillåt att gömma endast om accepterande sessioner med lösenord och permanenta lösenord"), + ].iter().cloned().collect(); +} From ddd082c4f3edad92ee2e1387304d4ed4659c58b4 Mon Sep 17 00:00:00 2001 From: Manos G <87467035+7th-fret@users.noreply.github.com> Date: Mon, 28 Nov 2022 13:56:24 +0200 Subject: [PATCH 1060/2015] Update gr.rs --- src/lang/gr.rs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/lang/gr.rs b/src/lang/gr.rs index 5e9dd2c95..93b75d54e 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -38,7 +38,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop service", "Διακοπή υπηρεσίας"), ("Change ID", "Αλλαγή αναγνωριστικού ID"), ("Website", "Ιστότοπος"), - ("About", "Πληροφορίες για το"), + ("About", "Πληροφορίες"), ("Mute", "Σίγαση"), ("Audio Input", "Είσοδος ήχου"), ("Enhancements", "Βελτιώσεις"), @@ -52,7 +52,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("id_change_tip", "Επιτρέπονται μόνο οι χαρακτήρες a-z, A-Z, 0-9 και _ (υπογράμμιση). Το πρώτο γράμμα πρέπει να είναι a-z, A-Z και το μήκος πρέπει να είναι μεταξύ 6 και 16 χαρακτήρων."), ("Invalid format", "Μη έγκυρη μορφή"), ("server_not_support", "Αυτή η δυνατότητα δεν υποστηρίζεται ακόμη από τον διακομιστή"), - ("Not available", "Μη διαθέσιμος"), + ("Not available", "Μη διαθέσιμο"), ("Too frequent", "Πολύ συχνά"), ("Cancel", "Ακύρωση"), ("Skip", "Παράλειψη"), @@ -105,8 +105,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Speed", "Ταχύτητα"), ("Custom Image Quality", "Προσαρμοσμένη ποιότητα εικόνας"), ("Privacy mode", "Λειτουργία απορρήτου"), - ("Block user input", "Αποκλεισμός εισόδου χρήστη"), - ("Unblock user input", "Κατάργηση αποκλεισμού εισόδου χρήστη"), + ("Block user input", "Αποκλεισμός χειρισμού από τον χρήστη"), + ("Unblock user input", "Κατάργηση αποκλεισμού χειρισμού από τον χρήστη"), ("Adjust Window", "Προσαρμογή παραθύρου"), ("Original", "Πρωτότυπο"), ("Shrink", "Συρρίκνωση"), @@ -118,7 +118,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Optimize reaction time", "Βελτιστοποίηση χρόνου αντίδρασης"), ("Custom", "Προσαρμοσμένο"), ("Show remote cursor", "Εμφάνιση απομακρυσμένου κέρσορα"), - ("Show quality monitor", "Εμφάνιση παρακολούθησης ποιότητας"), + ("Show quality monitor", "Εμφάνιση παρακολούθησης ποιότητας σύνδεσης"), ("Disable clipboard", "Απενεργοποίηση προχείρου"), ("Lock after session end", "Κλείδωμα μετά το τέλος της συνεδρίας"), ("Insert", "Εισάγετε"), @@ -133,7 +133,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to connect to relay server", "Αποτυχία σύνδεσης με διακομιστή αναμετάδοσης"), ("Failed to connect via rendezvous server", "Απέτυχε η σύνδεση μέσω διακομιστή"), ("Failed to connect via relay server", "Απέτυχε η σύνδεση μέσω διακομιστή αναμετάδοσης"), - ("Failed to make direct connection to remote desktop", "Απέτυχε η απευθείας σύνδεση με απομακρυσμένο σταθμό εργασίας"), + ("Failed to make direct connection to remote desktop", "Απέτυχε η απευθείας σύνδεση με τον απομακρυσμένο σταθμό εργασίας"), ("Set Password", "Ορίστε κωδικό"), ("OS Password", "Κωδικός πρόσβασης λειτουργικού συστήματος"), ("install_tip", "Λόγω UAC, το RustDesk ενδέχεται να μην λειτουργεί σωστά σε ορισμένες περιπτώσεις. Για να αποφύγετε το UAC, κάντε κλικ στο κουμπί παρακάτω για να εγκαταστήσετε το RustDesk στο σύστημα"), @@ -172,7 +172,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Disconnect", "Αποσύνδεση"), ("Allow using keyboard and mouse", "Να επιτρέπεται η χρήση πληκτρολογίου και ποντικιού"), ("Allow using clipboard", "Να επιτρέπεται η χρήση του προχείρου"), - ("Allow hearing sound", "Να επιτρέπεται η ακρόαση"), + ("Allow hearing sound", "Να επιτρέπεται η αναπαραγωγή ήχου"), ("Allow file copy and paste", "Να επιτρέπεται η αντιγραφή και επικόλληση αρχείου"), ("Connected", "Συνδεδεμένο"), ("Direct and encrypted connection", "Άμεση και κρυπτογραφημένη σύνδεση"), @@ -196,7 +196,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Login screen using Wayland is not supported", "Η οθόνη εισόδου με χρήση του Wayland δεν υποστηρίζεται"), ("Reboot required", "Απαιτείται επανεκκίνηση"), ("Unsupported display server ", "Μη υποστηριζόμενος διακομιστής εμφάνισης "), - ("x11 expected", "X11 αναμενόμενο"), + ("x11 expected", "απαιτείται X11"), ("Port", "Θύρα"), ("Settings", "Ρυθμίσεις"), ("Username", "Όνομα χρήστη"), @@ -268,7 +268,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Audio Capture", "Λήψη ήχου"), ("File Connection", "Σύνδεση αρχείου"), ("Screen Connection", "Σύνδεση οθόνης"), - ("Do you accept?", "Δέχεσαι?"), + ("Do you accept?", "Δέχεσαι;"), ("Open System Setting", "Άνοιγμα ρυθμίσεων συστήματος"), ("How to get Android input permission?", "Πώς να αποκτήσω άδεια εισαγωγής Android;"), ("android_input_permission_tip1", "Για να μπορεί μία απομακρυσμένη συσκευή να ελέγχει τη συσκευή σας Android, πρέπει να επιτρέψετε στο RustDesk να χρησιμοποιεί την υπηρεσία \"Προσβασιμότητα\"."), @@ -337,7 +337,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Dark Theme", "Σκούρο θέμα"), ("Dark", "Σκούρο"), ("Light", "Φωτεινό"), - ("Follow System", "Ακολουθήστε το σύστημα"), + ("Follow System", "Από το σύστημα"), ("Enable hardware codec", "Ενεργοποίηση κωδικοποιητή υλικού"), ("Unlock Security Settings", "Ξεκλείδωμα ρυθμίσεων ασφαλείας"), ("Enable Audio", "Ενεργοποίηση ήχου"), @@ -362,7 +362,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change", "Αλλαγή"), ("Start session recording", "Έναρξη εγγραφής συνεδρίας"), ("Stop session recording", "Διακοπή εγγραφής συνεδρίας"), - ("Enable Recording Session", "Ενεργοποίηση συνεδρίας εγγραφής"), + ("Enable Recording Session", "Ενεργοποίηση εγγραφής συνεδρίας"), ("Allow recording session", "Να επιτρέπεται η εγγραφή"), ("Enable LAN Discovery", "Ενεργοποίηση εντοπισμού LAN"), ("Deny LAN Discovery", "Απαγόρευση εντοπισμού LAN"), From 95f70e7bb7d548e5e6c7ec7e6857857a6429c9dd Mon Sep 17 00:00:00 2001 From: Kasper Date: Mon, 28 Nov 2022 12:56:42 +0100 Subject: [PATCH 1061/2015] Update lang.rs --- src/lang.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/lang.rs b/src/lang.rs index 790e35287..de0fed0b8 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -26,6 +26,7 @@ mod ua; mod fa; mod ca; mod gr; +mod sv; lazy_static::lazy_static! { pub static ref LANGS: Value = @@ -55,6 +56,7 @@ lazy_static::lazy_static! { ("fa", "فارسی"), ("ca", "Català"), ("gr", "Ελληνικά"), + ("sv", "Svenska"), ]); } @@ -108,6 +110,7 @@ pub fn translate_locale(name: String, locale: &str) -> String { "fa" => fa::T.deref(), "ca" => ca::T.deref(), "gr" => gr::T.deref(), + "gr" => sv::T.deref(), _ => en::T.deref(), }; if let Some(v) = m.get(&name as &str) { From a3695e15aa62f355e306a2bea69cc1940776a15c Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 28 Nov 2022 18:54:35 +0800 Subject: [PATCH 1062/2015] feat: add android nightly --- .github/workflows/flutter-nightly.yml | 91 +++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index b5123c8fa..6bd4ae26d 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -353,6 +353,97 @@ jobs: ./flutter/lib/generated_bridge.dart ./flutter/lib/generated_bridge.freezed.dart + build-rustdesk-android-arm64: + needs: [generate-bridge-linux] + name: build-rust-lib-android-arm64 ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: false + matrix: + job: + - { + arch: x86_64, + target: aarch64-linux-android, + os: ubuntu-18.04, + extra-build-features: "", + } + steps: + - name: Install dependencies + run: | + sudo apt update + sudo apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libvdpau-dev libva-dev libclang-dev llvm-dev libclang-10-dev llvm-10-dev pkg-config tree g++ + - name: Checkout source code + uses: actions/checkout@v3 + - uses: nttld/setup-ndk@v1 + id: setup-ndk + with: + ndk-version: r22b + add-to-path: true + + - name: Download deps + shell: bash + env: + ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} + run: | + pushd /opt + wget https://github.com/rustdesk/doc.rustdesk.com/releases/download/console/dep.tar.gz + tar xzf dep.tar.gz + + - name: Restore bridge files + uses: actions/download-artifact@master + with: + name: bridge-artifact + path: ./ + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + override: true + profile: minimal # minimal component installation (ie, no documentation) + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: rustdesk-lib-cache + key: ${{ matrix.job.target }}-${{ matrix.job.extra-build-features }} + + - name: Disable rust bridge build + run: | + sed -i "s/gen_flutter_rust_bridge();/\/\//g" build.rs + # only build cdylib + sed -i "s/\[\"cdylib\", \"staticlib\", \"rlib\"\]/\[\"cdylib\"\]/g" Cargo.toml + + - name: Build rustdesk arm64 so + run: | + rustup target add aarch64-linux-android + cargo install cargo-ndk + ./flutter/ndk_arm64.sh + mkdir -p ./flutter/android/app/src/main/jniLibs/arm64-v8a + cp ./target/aarch64-linux-android/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so + + - name: Build rustdesk + shell: bash + run: | + pushd flutter + flutter build apk --release --target-platform android-arm64 --split-per-abi + mv build/app/outputs/flutter-apk/app-arm64-v8a-release.apk ../rustdesk-${{ env.VERSION }}-arm64-v8a-release.apk + + - name: Upload Artifacts + uses: actions/upload-artifact@master + with: + name: rustdesk-${{ env.VERSION }}-arm64-v8a-release.apk + path: rustdesk-${{ env.VERSION }}-arm64-v8a-release.apk + + - name: Publish apk package + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + rustdesk-${{ env.VERSION }}-arm64-v8a-release.apk + + + build-rustdesk-lib-linux-amd64: needs: [generate-bridge-linux, build-vcpkg-deps-linux] name: build-rust-lib ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] From 8b1449cf8b45c09b8fdc2548bb4d7644d33b4526 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 28 Nov 2022 19:56:59 +0800 Subject: [PATCH 1063/2015] fix: android ci --- .github/workflows/flutter-nightly.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 6bd4ae26d..310154fa5 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -371,7 +371,7 @@ jobs: - name: Install dependencies run: | sudo apt update - sudo apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libvdpau-dev libva-dev libclang-dev llvm-dev libclang-10-dev llvm-10-dev pkg-config tree g++ + sudo apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libvdpau-dev libva-dev libclang-dev llvm-dev libclang-10-dev llvm-10-dev pkg-config tree g++ libc6-dev g++-multilib - name: Checkout source code uses: actions/checkout@v3 - uses: nttld/setup-ndk@v1 @@ -382,8 +382,6 @@ jobs: - name: Download deps shell: bash - env: - ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} run: | pushd /opt wget https://github.com/rustdesk/doc.rustdesk.com/releases/download/console/dep.tar.gz @@ -414,6 +412,10 @@ jobs: sed -i "s/\[\"cdylib\", \"staticlib\", \"rlib\"\]/\[\"cdylib\"\]/g" Cargo.toml - name: Build rustdesk arm64 so + env: + ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} + ANDROID_NDK_ROOT: ${{ steps.setup-ndk.outputs.ndk-path }} + VCPKG_ROOT: /opt/vcpkg run: | rustup target add aarch64-linux-android cargo install cargo-ndk @@ -914,6 +916,7 @@ jobs: done - name: Publish debian package + if: ${{ matrix.job.extra-build-features == '' }} uses: softprops/action-gh-release@v1 with: prerelease: true From 99d6ec269775bb2e700b4d80ef6bc9ddef6e99a5 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 28 Nov 2022 20:20:45 +0800 Subject: [PATCH 1064/2015] fix: android ci opt: android ci opt: bridge compile time fix: android sign fix: android so opt: format --- .github/workflows/flutter-nightly.yml | 85 ++++++++++++++++++++------- flutter/ndk_arm.sh | 2 +- flutter/ndk_arm64.sh | 2 +- flutter/ndk_x64.sh | 2 +- src/ui_interface.rs | 5 +- 5 files changed, 70 insertions(+), 26 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 310154fa5..79b7ee2fd 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -302,7 +302,7 @@ jobs: - name: Install prerequisites run: | sudo apt update -y - sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libvdpau-dev libva-dev libclang-dev llvm-dev libclang-10-dev llvm-10-dev pkg-config + sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang cmake libclang-dev ninja-build llvm-dev libclang-10-dev llvm-10-dev pkg-config - name: Install Rust toolchain uses: actions-rs/toolchain@v1 @@ -315,6 +315,7 @@ jobs: - uses: Swatinem/rust-cache@v2 with: prefix-key: bridge-${{ matrix.job.os }} + workspace: "/tmp/flutter_rust_bridge/frb_codegen" - name: Cache Bridge id: cache-bridge @@ -328,6 +329,7 @@ jobs: with: channel: "stable" flutter-version: ${{ env.FLUTTER_VERSION }} + cache: true - name: Install ffigen run: | @@ -355,7 +357,7 @@ jobs: build-rustdesk-android-arm64: needs: [generate-bridge-linux] - name: build-rust-lib-android-arm64 ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] + name: build rustdesk android apk ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] runs-on: ${{ matrix.job.os }} strategy: fail-fast: false @@ -367,13 +369,24 @@ jobs: os: ubuntu-18.04, extra-build-features: "", } + # - { + # arch: x86_64, + # target: armv7-linux-androideabi, + # os: ubuntu-18.04, + # extra-build-features: "", + # } steps: - name: Install dependencies run: | sudo apt update - sudo apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libvdpau-dev libva-dev libclang-dev llvm-dev libclang-10-dev llvm-10-dev pkg-config tree g++ libc6-dev g++-multilib + sudo apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libvdpau-dev libva-dev libclang-dev llvm-dev libclang-10-dev llvm-10-dev pkg-config tree g++ libc6-dev gcc-multilib g++-multilib openjdk-11-jdk-headless - name: Checkout source code uses: actions/checkout@v3 + - name: Install flutter + uses: subosito/flutter-action@v2 + with: + channel: "stable" + flutter-version: ${{ env.FLUTTER_VERSION }} - uses: nttld/setup-ndk@v1 id: setup-ndk with: @@ -408,43 +421,73 @@ jobs: - name: Disable rust bridge build run: | sed -i "s/gen_flutter_rust_bridge();/\/\//g" build.rs - # only build cdylib - sed -i "s/\[\"cdylib\", \"staticlib\", \"rlib\"\]/\[\"cdylib\"\]/g" Cargo.toml - - - name: Build rustdesk arm64 so + + - name: Build rustdesk lib env: ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} ANDROID_NDK_ROOT: ${{ steps.setup-ndk.outputs.ndk-path }} VCPKG_ROOT: /opt/vcpkg run: | - rustup target add aarch64-linux-android + rustup target add ${{ matrix.job.target }} cargo install cargo-ndk - ./flutter/ndk_arm64.sh - mkdir -p ./flutter/android/app/src/main/jniLibs/arm64-v8a - cp ./target/aarch64-linux-android/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so + case ${{ matrix.job.target }} in + aarch64-linux-android) + ./flutter/ndk_arm64.sh + mkdir -p ./flutter/android/app/src/main/jniLibs/arm64-v8a + cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so + ;; + armv7-linux-androideabi) + ./flutter/ndk_arm.sh + mkdir -p ./flutter/android/app/src/main/jniLibs/armeabi-v7a + cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/librustdesk.so + ;; + esac - name: Build rustdesk shell: bash + env: + JAVA_HOME: /usr/lib/jvm/java-11-openjdk-amd64 run: | + export PATH=/usr/lib/jvm/java-11-openjdk-amd64/bin:$PATH + # download so pushd flutter - flutter build apk --release --target-platform android-arm64 --split-per-abi - mv build/app/outputs/flutter-apk/app-arm64-v8a-release.apk ../rustdesk-${{ env.VERSION }}-arm64-v8a-release.apk + wget -O so.tar.gz https://github.com/rustdesk/doc.rustdesk.com/releases/download/console/so.tar.gz + tar xzvf so.tar.gz + popd + # temporary use debug sign config + sed -i "s/signingConfigs.release/signingConfigs.debug/g" ./flutter/android/app/build.gradle + case ${{ matrix.job.target }} in + aarch64-linux-android) + mkdir -p ./flutter/android/app/src/main/jniLibs/arm64-v8a + cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so + # build flutter + pushd flutter + flutter build apk --release --target-platform android-arm64 --split-per-abi + mv build/app/outputs/flutter-apk/app-arm64-v8a-release.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk + ;; + armv7-linux-androideabi) + mkdir -p ./flutter/android/app/src/main/jniLibs/armeabi-v7a + cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/librustdesk.so + # build flutter + pushd flutter + flutter build apk --release --target-platform android-arm --split-per-abi + mv build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk + ;; + esac - name: Upload Artifacts uses: actions/upload-artifact@master with: - name: rustdesk-${{ env.VERSION }}-arm64-v8a-release.apk - path: rustdesk-${{ env.VERSION }}-arm64-v8a-release.apk - + name: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk + path: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk + - name: Publish apk package uses: softprops/action-gh-release@v1 with: prerelease: true tag_name: ${{ env.TAG_NAME }} files: | - rustdesk-${{ env.VERSION }}-arm64-v8a-release.apk - - + rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk build-rustdesk-lib-linux-amd64: needs: [generate-bridge-linux, build-vcpkg-deps-linux] @@ -678,7 +721,6 @@ jobs: key: ${{ matrix.job.target }}-${{ matrix.job.extra-build-features }} cache-directories: "/opt/rust-registry" - - name: Install local registry run: | mkdir -p /opt/rust-registry @@ -907,7 +949,6 @@ jobs: mv "$name" "/opt/artifacts/rpm/${name%%.rpm}-fedora28-centos8.rpm" done - - name: Rename rustdesk shell: bash run: | @@ -1208,7 +1249,7 @@ jobs: # apt install -y rpm # run: | # pushd /workspace - # # install + # # install # apt update -y # apt install -y flatpak flatpak-builder cmake g++ gcc git curl wget nasm yasm libgtk-3-dev git # # flatpak deps diff --git a/flutter/ndk_arm.sh b/flutter/ndk_arm.sh index fe9c81016..7c2415d2d 100755 --- a/flutter/ndk_arm.sh +++ b/flutter/ndk_arm.sh @@ -1,2 +1,2 @@ #!/usr/bin/env bash -cargo ndk --platform 21 --target armv7-linux-androideabi build --release +cargo ndk --platform 21 --target armv7-linux-androideabi build --release --features flutter diff --git a/flutter/ndk_arm64.sh b/flutter/ndk_arm64.sh index d28009f6c..99420ae8c 100755 --- a/flutter/ndk_arm64.sh +++ b/flutter/ndk_arm64.sh @@ -1,2 +1,2 @@ #!/usr/bin/env bash -cargo ndk --platform 21 --target aarch64-linux-android build --release +cargo ndk --platform 21 --target aarch64-linux-android build --release --features flutter diff --git a/flutter/ndk_x64.sh b/flutter/ndk_x64.sh index 6272b0390..30bd4902d 100755 --- a/flutter/ndk_x64.sh +++ b/flutter/ndk_x64.sh @@ -1,2 +1,2 @@ #!/usr/bin/env bash -cargo ndk --platform 21 --target x86_64-linux-android build --release +cargo ndk --platform 21 --target x86_64-linux-android build --release --features flutter diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 28ce897bc..41f22a563 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -876,7 +876,10 @@ pub fn check_zombie(children: Children) { } pub fn start_option_status_sync() { - let _sender = SENDER.lock().unwrap(); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + let _sender = SENDER.lock().unwrap(); + } } // not call directly From 80e6e94841c3658aed218dfdbf2b83489ff72789 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 29 Nov 2022 10:46:02 +0800 Subject: [PATCH 1065/2015] opt: add android sign --- .github/workflows/flutter-nightly.yml | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 79b7ee2fd..34a51cb7b 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -474,12 +474,28 @@ jobs: mv build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk ;; esac + popd + mkdir -p signed-apk; pushd signed-apk + mv ../rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk . + + - uses: r0adkll/sign-android-release@v1 + name: Sign app APK + id: sign-rustdesk + with: + releaseDirectory: ./signed-apk + signingKeyBase64: ${{ secrets.SIGNING_KEY }} + alias: ${{ secrets.ALIAS }} + keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }} + keyPassword: ${{ secrets.KEY_PASSWORD }} + env: + # override default build-tools version (29.0.3) -- optional + BUILD_TOOLS_VERSION: "30.0.2" - name: Upload Artifacts uses: actions/upload-artifact@master with: - name: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk - path: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk + name: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release-signed.apk + path: ${{steps.sign-rustdesk.outputs.signedReleaseFile}} - name: Publish apk package uses: softprops/action-gh-release@v1 @@ -487,7 +503,7 @@ jobs: prerelease: true tag_name: ${{ env.TAG_NAME }} files: | - rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk + ${{steps.sign-rustdesk.outputs.signedReleaseFile}} build-rustdesk-lib-linux-amd64: needs: [generate-bridge-linux, build-vcpkg-deps-linux] From 9c108b2171026b2eb2a5ecbafa4686835fe50618 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 29 Nov 2022 11:24:37 +0800 Subject: [PATCH 1066/2015] opt: add android prefix --- .github/workflows/flutter-nightly.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 34a51cb7b..68bf30ac0 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -483,10 +483,10 @@ jobs: id: sign-rustdesk with: releaseDirectory: ./signed-apk - signingKeyBase64: ${{ secrets.SIGNING_KEY }} - alias: ${{ secrets.ALIAS }} - keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }} - keyPassword: ${{ secrets.KEY_PASSWORD }} + signingKeyBase64: ${{ secrets.ANDROID_SIGNING_KEY }} + alias: ${{ secrets.ANDROID_ALIAS }} + keyStorePassword: ${{ secrets.ANDROID_KEY_STORE_PASSWORD }} + keyPassword: ${{ secrets.ANDROID_KEY_PASSWORD }} env: # override default build-tools version (29.0.3) -- optional BUILD_TOOLS_VERSION: "30.0.2" From 4d044ca57ae99d9878ef44f3c7dd527ecfcc9b90 Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 29 Nov 2022 16:36:35 +0800 Subject: [PATCH 1067/2015] wayland cursor embeded Signed-off-by: fufesou --- flutter/lib/desktop/pages/remote_page.dart | 44 ++++++------ .../lib/desktop/pages/remote_tab_page.dart | 11 +-- .../lib/desktop/widgets/remote_menubar.dart | 36 +++++----- flutter/lib/mobile/pages/remote_page.dart | 68 +++++++++++-------- flutter/lib/models/model.dart | 6 ++ libs/hbb_common/protos/message.proto | 2 + libs/scrap/src/common/mod.rs | 16 +++++ libs/scrap/src/common/wayland.rs | 4 +- libs/scrap/src/common/x11.rs | 2 + src/client/io_loop.rs | 2 +- src/flutter.rs | 4 +- src/lang.rs | 2 +- src/server.rs | 6 +- src/server/video_service.rs | 6 ++ src/server/wayland.rs | 5 +- src/ui/remote.rs | 5 +- src/ui/remote.tis | 11 ++- src/ui_session_interface.rs | 4 +- 18 files changed, 152 insertions(+), 82 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 6d0d9a047..bac80e7ac 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -235,12 +235,14 @@ class _RemotePageState extends State })) ]; - paints.add(Obx(() => Visibility( - visible: _showRemoteCursor.isTrue && _remoteCursorMoved.isTrue, - child: CursorPaint( - id: widget.id, - zoomCursor: _zoomCursor, - )))); + if (!_ffi.canvasModel.cursorEmbeded) { + paints.add(Obx(() => Visibility( + visible: _showRemoteCursor.isTrue && _remoteCursorMoved.isTrue, + child: CursorPaint( + id: widget.id, + zoomCursor: _zoomCursor, + )))); + } paints.add(QualityMonitor(_ffi.qualityMonitorModel)); paints.add(RemoteMenubar( id: widget.id, @@ -300,20 +302,22 @@ class _ImagePaintState extends State { mouseRegion({child}) => Obx(() => MouseRegion( cursor: cursorOverImage.isTrue - ? keyboardEnabled.isTrue - ? (() { - if (remoteCursorMoved.isTrue) { - _lastRemoteCursorMoved = true; - return SystemMouseCursors.none; - } else { - if (_lastRemoteCursorMoved) { - _lastRemoteCursorMoved = false; - _firstEnterImage.value = true; - } - return _buildCustomCursor(context, s); - } - }()) - : _buildDisabledCursor(context, s) + ? c.cursorEmbeded + ? SystemMouseCursors.none + : keyboardEnabled.isTrue + ? (() { + if (remoteCursorMoved.isTrue) { + _lastRemoteCursorMoved = true; + return SystemMouseCursors.none; + } else { + if (_lastRemoteCursorMoved) { + _lastRemoteCursorMoved = false; + _firstEnterImage.value = true; + } + return _buildCustomCursor(context, s); + } + }()) + : _buildDisabledCursor(context, s) : MouseCursor.defer, onHover: (evt) {}, child: child)); diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index 85df25477..713c3d13c 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -255,8 +255,11 @@ class _ConnectionTabPageState extends State { }, padding: padding, ), - MenuEntryDivider(), - () { + ]); + + if (!ffi.canvasModel.cursorEmbeded) { + menu.add(MenuEntryDivider()); + menu.add(() { final state = ShowRemoteCursorState.find(key); return MenuEntrySwitch2( switchType: SwitchType.scheckbox, @@ -272,8 +275,8 @@ class _ConnectionTabPageState extends State { }, padding: padding, ); - }() - ]); + }()); + } if (perms['keyboard'] != false) { if (perms['clipboard'] != false) { diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 39c37dd8b..d40a21e5d 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -1088,23 +1088,25 @@ class _RemoteMenubarState extends State { } /// Show remote cursor - displayMenu.add(() { - final state = ShowRemoteCursorState.find(widget.id); - return MenuEntrySwitch2( - switchType: SwitchType.scheckbox, - text: translate('Show remote cursor'), - getter: () { - return state; - }, - setter: (bool v) async { - state.value = v; - await bind.sessionToggleOption( - id: widget.id, value: 'show-remote-cursor'); - }, - padding: padding, - dismissOnClicked: true, - ); - }()); + if (!widget.ffi.canvasModel.cursorEmbeded) { + displayMenu.add(() { + final state = ShowRemoteCursorState.find(widget.id); + return MenuEntrySwitch2( + switchType: SwitchType.scheckbox, + text: translate('Show remote cursor'), + getter: () { + return state; + }, + setter: (bool v) async { + state.value = v; + await bind.sessionToggleOption( + id: widget.id, value: 'show-remote-cursor'); + }, + padding: padding, + dismissOnClicked: true, + ); + }()); + } /// Show remote cursor scaling with image if (widget.state.viewStyle.value != kRemoteViewStyleOriginal) { diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index e6ebfcb73..fcfb8ad60 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -494,38 +494,45 @@ class _RemotePageState extends State { Widget getBodyForMobile() { return Container( color: MyTheme.canvasColor, - child: Stack(children: [ - ImagePaint(), - CursorPaint(), - QualityMonitor(gFFI.qualityMonitorModel), - getHelpTools(), - SizedBox( - width: 0, - height: 0, - child: !_showEdit - ? Container() - : TextFormField( - textInputAction: TextInputAction.newline, - autocorrect: false, - enableSuggestions: false, - autofocus: true, - focusNode: _mobileFocusNode, - maxLines: null, - initialValue: _value, - // trick way to make backspace work always - keyboardType: TextInputType.multiline, - onChanged: handleSoftKeyboardInput, - ), - ), - ])); + child: Stack(children: () { + final paints = [ + ImagePaint(), + QualityMonitor(gFFI.qualityMonitorModel), + getHelpTools(), + SizedBox( + width: 0, + height: 0, + child: !_showEdit + ? Container() + : TextFormField( + textInputAction: TextInputAction.newline, + autocorrect: false, + enableSuggestions: false, + autofocus: true, + focusNode: _mobileFocusNode, + maxLines: null, + initialValue: _value, + // trick way to make backspace work always + keyboardType: TextInputType.multiline, + onChanged: handleSoftKeyboardInput, + ), + ), + ]; + if (!gFFI.canvasModel.cursorEmbeded) { + paints.add(CursorPaint()); + } + return paints; + }())); } Widget getBodyForDesktopWithListener(bool keyboard) { var paints = [ImagePaint()]; - final cursor = bind.sessionGetToggleOptionSync( - id: widget.id, arg: 'show-remote-cursor'); - if (keyboard || cursor) { - paints.add(CursorPaint()); + if (!gFFI.canvasModel.cursorEmbeded) { + final cursor = bind.sessionGetToggleOptionSync( + id: widget.id, arg: 'show-remote-cursor'); + if (keyboard || cursor) { + paints.add(CursorPaint()); + } } return Container( color: MyTheme.canvasColor, child: Stack(children: paints)); @@ -1046,9 +1053,12 @@ void showOptions( } final toggles = [ - getToggle(id, setState, 'show-remote-cursor', 'Show remote cursor'), getToggle(id, setState, 'show-quality-monitor', 'Show quality monitor'), ]; + if (!gFFI.canvasModel.cursorEmbeded) { + toggles.insert(0, + getToggle(id, setState, 'show-remote-cursor', 'Show remote cursor')); + } return CustomAlertDialog( content: Column( diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index a7a51b9a0..f6bfde941 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -221,6 +221,7 @@ class FfiModel with ChangeNotifier { _display.y = double.parse(evt['y']); _display.width = int.parse(evt['width']); _display.height = int.parse(evt['height']); + _display.cursorEmbeded = int.parse(evt['cursor_embeded']) == 1; if (old != _pi.currentDisplay) { parent.target?.cursorModel.updateDisplayOrigin(_display.x, _display.y); } @@ -330,6 +331,7 @@ class FfiModel with ChangeNotifier { d.y = d0['y'].toDouble(); d.width = d0['width']; d.height = d0['height']; + d.cursorEmbeded = d0['cursor_embeded'] == 1; _pi.displays.add(d); } if (_pi.currentDisplay < _pi.displays.length) { @@ -582,6 +584,9 @@ class CanvasModel with ChangeNotifier { notifyListeners(); } + bool get cursorEmbeded => + parent.target?.ffiModel.display.cursorEmbeded ?? false; + int getDisplayWidth() { final defaultWidth = (isDesktop || isWebDesktop) ? kDesktopDefaultDisplayWidth @@ -1311,6 +1316,7 @@ class Display { double y = 0; int width = 0; int height = 0; + bool cursorEmbeded = false; Display() { width = (isDesktop || isWebDesktop) diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index 34983599a..9217388aa 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -40,6 +40,7 @@ message DisplayInfo { int32 height = 4; string name = 5; bool online = 6; + bool cursor_embeded = 7; } message PortForward { @@ -419,6 +420,7 @@ message SwitchDisplay { sint32 y = 3; int32 width = 4; int32 height = 5; + bool cursor_embeded = 6; } message PermissionInfo { diff --git a/libs/scrap/src/common/mod.rs b/libs/scrap/src/common/mod.rs index 468efb88e..82f65537b 100644 --- a/libs/scrap/src/common/mod.rs +++ b/libs/scrap/src/common/mod.rs @@ -69,3 +69,19 @@ pub trait TraitCapturer { pub fn is_x11() -> bool { "x11" == hbb_common::platform::linux::get_display_server() } + +#[cfg(x11)] +#[inline] +pub fn is_cursor_embeded() -> bool { + if is_x11() { + x11::IS_CURSOR_EMBEDED + } else { + wayland::IS_CURSOR_EMBEDED + } +} + +#[cfg(not(x11))] +#[inline] +pub fn is_cursor_embeded() -> bool { + false +} diff --git a/libs/scrap/src/common/wayland.rs b/libs/scrap/src/common/wayland.rs index 6ad2d84cb..2593e56fe 100644 --- a/libs/scrap/src/common/wayland.rs +++ b/libs/scrap/src/common/wayland.rs @@ -4,6 +4,8 @@ use std::{io, sync::RwLock, time::Duration}; pub struct Capturer(Display, Box, bool, Vec); +pub const IS_CURSOR_EMBEDED: bool = true; + lazy_static::lazy_static! { static ref MAP_ERR: RwLock io::Error>> = Default::default(); } @@ -66,7 +68,7 @@ impl Display { } pub fn all() -> io::Result> { - Ok(pipewire::get_capturables(false) + Ok(pipewire::get_capturables(true) .map_err(map_err)? .drain(..) .map(|x| Display(x)) diff --git a/libs/scrap/src/common/x11.rs b/libs/scrap/src/common/x11.rs index 791514deb..a7122adcb 100644 --- a/libs/scrap/src/common/x11.rs +++ b/libs/scrap/src/common/x11.rs @@ -3,6 +3,8 @@ use std::{io, ops, time::Duration}; pub struct Capturer(x11::Capturer); +pub const IS_CURSOR_EMBEDED: bool = true; + impl Capturer { pub fn new(display: Display, yuv: bool) -> io::Result { x11::Capturer::new(display.0, yuv).map(Capturer) diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 6ad4f96d6..16f91d89d 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -976,7 +976,7 @@ impl Remote { self.handler.ui_handler.switch_display(&s); self.video_sender.send(MediaData::Reset).ok(); if s.width > 0 && s.height > 0 { - self.handler.set_display(s.x, s.y, s.width, s.height); + self.handler.set_display(s.x, s.y, s.width, s.height, s.cursor_embeded); } } Some(misc::Union::CloseReason(c)) => { diff --git a/src/flutter.rs b/src/flutter.rs index 9c4208625..9649c9b46 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -155,7 +155,7 @@ impl InvokeUiSession for FlutterHandler { } /// unused in flutter, use switch_display or set_peer_info - fn set_display(&self, _x: i32, _y: i32, _w: i32, _h: i32) {} + fn set_display(&self, _x: i32, _y: i32, _w: i32, _h: i32, _cursor_embeded: bool) {} fn update_privacy_mode(&self) { self.push_event("update_privacy_mode", [].into()); @@ -295,6 +295,7 @@ impl InvokeUiSession for FlutterHandler { h.insert("y", d.y); h.insert("width", d.width); h.insert("height", d.height); + h.insert("cursor_embeded", if d.cursor_embeded { 1 } else { 0 }); displays.push(h); } let displays = serde_json::ser::to_string(&displays).unwrap_or("".to_owned()); @@ -343,6 +344,7 @@ impl InvokeUiSession for FlutterHandler { ("y", &display.y.to_string()), ("width", &display.width.to_string()), ("height", &display.height.to_string()), + ("cursor_embeded", &{if display.cursor_embeded {1} else {0}}.to_string()), ], ); } diff --git a/src/lang.rs b/src/lang.rs index de0fed0b8..6254e988a 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -110,7 +110,7 @@ pub fn translate_locale(name: String, locale: &str) -> String { "fa" => fa::T.deref(), "ca" => ca::T.deref(), "gr" => gr::T.deref(), - "gr" => sv::T.deref(), + "sv" => sv::T.deref(), _ => en::T.deref(), }; if let Some(v) = m.get(&name as &str) { diff --git a/src/server.rs b/src/server.rs index f5326288a..a12a31dbd 100644 --- a/src/server.rs +++ b/src/server.rs @@ -85,8 +85,10 @@ pub fn new() -> ServerPtr { #[cfg(not(any(target_os = "android", target_os = "ios")))] { server.add_service(Box::new(clipboard_service::new())); - server.add_service(Box::new(input_service::new_cursor())); - server.add_service(Box::new(input_service::new_pos())); + if !video_service::capture_cursor_embeded() { + server.add_service(Box::new(input_service::new_cursor())); + server.add_service(Box::new(input_service::new_pos())); + } } Arc::new(RwLock::new(server)) } diff --git a/src/server/video_service.rs b/src/server/video_service.rs index 04df87bef..a169b3835 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -74,6 +74,10 @@ fn is_capturer_mag_supported() -> bool { false } +pub fn capture_cursor_embeded() -> bool { + scrap::is_cursor_embeded() +} + pub fn notify_video_frame_feched(conn_id: i32, frame_tm: Option) { FRAME_FETCHED_NOTIFIER.0.send((conn_id, frame_tm)).unwrap() } @@ -455,6 +459,7 @@ fn run(sp: GenericService) -> ResultType<()> { y: c.origin.1 as _, width: c.width as _, height: c.height as _, + cursor_embeded: capture_cursor_embeded(), ..Default::default() }); let mut msg_out = Message::new(); @@ -783,6 +788,7 @@ pub(super) fn get_displays_2(all: &Vec) -> (usize, Vec) { height: d.height() as _, name: d.name(), online: d.is_online(), + cursor_embeded: false, ..Default::default() }); } diff --git a/src/server/wayland.rs b/src/server/wayland.rs index 4ffb9e225..071077c84 100644 --- a/src/server/wayland.rs +++ b/src/server/wayland.rs @@ -127,7 +127,10 @@ pub(super) async fn check_init() -> ResultType<()> { if *lock == 0 { let all = Display::all()?; let num = all.len(); - let (primary, displays) = super::video_service::get_displays_2(&all); + let (primary, mut displays) = super::video_service::get_displays_2(&all); + for display in displays.iter_mut() { + display.cursor_embeded = true; + } let mut rects: Vec<((i32, i32), usize, usize)> = Vec::new(); for d in &all { diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 66b46cf85..3d209a71c 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -79,8 +79,8 @@ impl InvokeUiSession for SciterHandler { } } - fn set_display(&self, x: i32, y: i32, w: i32, h: i32) { - self.call("setDisplay", &make_args!(x, y, w, h)); + fn set_display(&self, x: i32, y: i32, w: i32, h: i32, cursor_embeded: bool) { + self.call("setDisplay", &make_args!(x, y, w, h, cursor_embeded)); // https://sciter.com/forums/topic/color_spaceiyuv-crash // Nothing spectacular in decoder – done on CPU side. // So if you can do BGRA translation on your side – the better. @@ -223,6 +223,7 @@ impl InvokeUiSession for SciterHandler { display.set_item("y", d.y); display.set_item("width", d.width); display.set_item("height", d.height); + display.set_item("cursor_embeded", d.cursor_embeded); displays.push(display); } pi_sciter.set_item("displays", displays); diff --git a/src/ui/remote.tis b/src/ui/remote.tis index 02f0de270..012205abc 100644 --- a/src/ui/remote.tis +++ b/src/ui/remote.tis @@ -6,6 +6,7 @@ var display_width = 0; var display_height = 0; var display_origin_x = 0; var display_origin_y = 0; +var display_cursor_embeded = false; var display_scale = 1; var keyboard_enabled = true; // server side var clipboard_enabled = true; // server side @@ -15,11 +16,12 @@ var restart_enabled = true; // server side var recording_enabled = true; // server side var scroll_body = $(body); -handler.setDisplay = function(x, y, w, h) { +handler.setDisplay = function(x, y, w, h, cursor_embeded) { display_width = w; display_height = h; display_origin_x = x; display_origin_y = y; + display_cursor_embeded = cursor_embeded; adaptDisplay(); if (recording) handler.record_screen(true, w, h); } @@ -195,6 +197,9 @@ function handler.onMouse(evt) dragging = false; break; case Event.MOUSE_MOVE: + if (display_cursor_embeded) { + break; + } if (cursor_img.style#display != "none" && keyboard_enabled) { cursor_img.style#display = "none"; } @@ -360,6 +365,10 @@ function updateCursor(system=false) { } function refreshCursor() { + if (display_cursor_embeded) { + cursor_img.style#display = "none"; + return; + } if (cur_id != -1) { handler.setCursorId(cur_id); } diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index efc82cbc1..6f8820e87 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -1098,7 +1098,7 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { fn set_cursor_data(&self, cd: CursorData); fn set_cursor_id(&self, id: String); fn set_cursor_position(&self, cp: CursorPosition); - fn set_display(&self, x: i32, y: i32, w: i32, h: i32); + fn set_display(&self, x: i32, y: i32, w: i32, h: i32, cursor_embeded: bool); fn switch_display(&self, display: &SwitchDisplay); fn set_peer_info(&self, peer_info: &PeerInfo); // flutter fn update_privacy_mode(&self); @@ -1211,7 +1211,7 @@ impl Interface for Session { input_os_password(p, true, self.clone()); } let current = &pi.displays[pi.current_display as usize]; - self.set_display(current.x, current.y, current.width, current.height); + self.set_display(current.x, current.y, current.width, current.height, current.cursor_embeded); } self.update_privacy_mode(); // Save recent peers, then push event to flutter. So flutter can refresh peer page. From 5877bcf2a16180e1916017825ed93102c046c41c Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 29 Nov 2022 17:59:11 +0800 Subject: [PATCH 1068/2015] fix cursor embeded value on x11 Signed-off-by: fufesou --- libs/scrap/src/common/x11.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/scrap/src/common/x11.rs b/libs/scrap/src/common/x11.rs index a7122adcb..dacc265ff 100644 --- a/libs/scrap/src/common/x11.rs +++ b/libs/scrap/src/common/x11.rs @@ -3,7 +3,7 @@ use std::{io, ops, time::Duration}; pub struct Capturer(x11::Capturer); -pub const IS_CURSOR_EMBEDED: bool = true; +pub const IS_CURSOR_EMBEDED: bool = false; impl Capturer { pub fn new(display: Display, yuv: bool) -> io::Result { From d2d7c2e50a7ccabb5accbe7ca9493f336e5eb122 Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 29 Nov 2022 18:52:04 +0800 Subject: [PATCH 1069/2015] sciter show-remote-cursor option Signed-off-by: fufesou --- src/ui/header.tis | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/ui/header.tis b/src/ui/header.tis index 01699a583..086696726 100644 --- a/src/ui/header.tis +++ b/src/ui/header.tis @@ -164,6 +164,13 @@ class Header: Reactor.Component { var codecs = handler.supported_hwcodec(); var show_codec = handler.has_hwcodec() && (codecs[0] || codecs[1]); + var cursor_embeded = false; + if ((pi.displays || []).length > 0) { + if (pi.displays.length > pi.current_display) { + cursor_embeded = pi.displays[pi.current_display].cursor_embeded; + } + } + return

  • {translate('Adjust Window')}
  • @@ -184,7 +191,7 @@ class Header: Reactor.Component { {codecs[1] ?
  • {svg_checkmark}H265
  • : ""}
    : ""}
    -
  • {svg_checkmark}{translate('Show remote cursor')}
  • + {!cursor_embeded &&
  • {svg_checkmark}{translate('Show remote cursor')}
  • }
  • {svg_checkmark}{translate('Show quality monitor')}
  • {audio_enabled ?
  • {svg_checkmark}{translate('Mute')}
  • : ""} {is_win && pi.platform == 'Windows' && file_enabled ?
  • {svg_checkmark}{translate('Allow file copy and paste')}
  • : ""} From 420dd9c9db50bd6088257cfdbe5d9ad8e34b40e4 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 29 Nov 2022 22:00:27 +0800 Subject: [PATCH 1070/2015] mac help cards two mac issues: 1) windows position not saved, position not got, win manager issue? 2) freeCache not found from custom cursor channel --- .../lib/desktop/pages/connection_page.dart | 7 +- .../lib/desktop/pages/desktop_home_page.dart | 165 +++++++++++++----- .../desktop/pages/desktop_setting_page.dart | 2 +- .../macos/Runner.xcodeproj/project.pbxproj | 10 +- flutter/windows/runner/main.cpp | 2 +- src/flutter.rs | 2 +- src/flutter_ffi.rs | 4 + src/lang.rs | 2 +- 8 files changed, 136 insertions(+), 58 deletions(-) diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 83e57dba8..a1e87b418 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -42,7 +42,7 @@ class _ConnectionPageState extends State final RxBool _idInputFocused = false.obs; final FocusNode _idFocusNode = FocusNode(); - var svcStopped = false.obs; + var svcStopped = Get.find(tag: 'stop-service'); var svcStatusCode = 0.obs; var svcIsUsingPublicServer = true.obs; @@ -67,7 +67,6 @@ class _ConnectionPageState extends State _idFocusNode.addListener(() { _idInputFocused.value = _idFocusNode.hasFocus; }); - Get.put(svcStopped, tag: 'service-stop'); windowManager.addListener(this); } @@ -75,7 +74,6 @@ class _ConnectionPageState extends State void dispose() { _idController.dispose(); _updateTimer?.cancel(); - Get.delete(tag: 'service-stop'); windowManager.removeListener(this); super.dispose(); } @@ -296,7 +294,7 @@ class _ConnectionPageState extends State // stop Offstage( offstage: !svcStopped.value, - child: GestureDetector( + child: InkWell( onTap: () async { bool checked = !bind.mainIsInstalled() || await bind.mainCheckSuperUserPermission(); @@ -357,7 +355,6 @@ class _ConnectionPageState extends State } updateStatus() async { - svcStopped.value = await bind.mainGetOption(key: "stop-service") == "Y"; final status = jsonDecode(await bind.mainGetConnectStatus()) as Map; svcStatusCode.value = status["status_num"]; diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 4be64eee0..a6d4691ce 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -40,7 +40,12 @@ class _DesktopHomePageState extends State @override bool get wantKeepAlive => true; var updateUrl = ''; + var systemError = ''; StreamSubscription? _uniLinksSubscription; + var svcStopped = false.obs; + var watchIsCanScreenRecording = false; + var watchIsProcessTrust = false; + Timer? _updateTimer; @override Widget build(BuildContext context) { @@ -317,13 +322,37 @@ class _DesktopHomePageState extends State await launchUrl(url); }); } - if (Platform.isMacOS) {} + if (systemError.isNotEmpty) { + return buildInstallCard("", systemError, "", () {}); + } + if (Platform.isMacOS) { + if (!bind.mainIsCanScreenRecording(prompt: false)) { + return buildInstallCard("Permissions", "config_screen", "Configure", + () async { + bind.mainIsCanScreenRecording(prompt: true); + watchIsCanScreenRecording = true; + }, help: 'Help', link: translate("doc_mac_permission")); + } else if (!bind.mainIsProcessTrusted(prompt: false)) { + return buildInstallCard("Permissions", "config_acc", "Configure", + () async { + bind.mainIsProcessTrusted(prompt: true); + watchIsProcessTrust = true; + }, help: 'Help', link: translate("doc_mac_permission")); + } else if (!svcStopped.value && + bind.mainIsInstalled() && + !bind.mainIsInstalledDaemon(prompt: false)) { + return buildInstallCard("", "install_daemon_tip", "Install", () async { + bind.mainIsInstalledDaemon(prompt: true); + }); + } + } if (bind.mainIsInstalledLowerVersion()) {} return Container(); } Widget buildInstallCard(String title, String content, String btnText, - GestureTapCallback onPressed) { + GestureTapCallback onPressed, + {String? help, String? link}) { return Container( margin: EdgeInsets.only(top: 20), child: Container( @@ -338,44 +367,64 @@ class _DesktopHomePageState extends State )), padding: EdgeInsets.all(20), child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: (title.isNotEmpty - ? [ - Center( - child: Text( - translate(title), - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 15), - ).marginOnly(bottom: 6)), - ] - : []) + - [ - Text( - translate(content), - style: TextStyle( - height: 1.5, - color: Colors.white, - fontWeight: FontWeight.normal, - fontSize: 13), - ).marginOnly(bottom: 20), - Row(mainAxisAlignment: MainAxisAlignment.center, children: [ - FixedWidthButton( - width: 150, - padding: 8, - isOutline: true, - text: translate(btnText), - textColor: Colors.white, - borderColor: Colors.white, - textSize: 20, - radius: 10, - onTap: onPressed, - ) - ]), - ], - )), + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: (title.isNotEmpty + ? [ + Center( + child: Text( + translate(title), + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 15), + ).marginOnly(bottom: 6)), + ] + : []) + + [ + Text( + translate(content), + style: TextStyle( + height: 1.5, + color: Colors.white, + fontWeight: FontWeight.normal, + fontSize: 13), + ).marginOnly(bottom: 20) + ] + + (btnText.isNotEmpty + ? [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + FixedWidthButton( + width: 150, + padding: 8, + isOutline: true, + text: translate(btnText), + textColor: Colors.white, + borderColor: Colors.white, + textSize: 20, + radius: 10, + onTap: onPressed, + ) + ]) + ] + : []) + + (help != null + ? [ + Center( + child: InkWell( + onTap: () async => + await launchUrl(Uri.parse(link!)), + child: Text( + translate(help), + style: TextStyle( + decoration: TextDecoration.underline, + color: Colors.white, + fontSize: 12), + )).marginOnly(top: 6)), + ] + : []))), ); } @@ -412,16 +461,42 @@ class _DesktopHomePageState extends State void initState() { super.initState(); bind.mainStartGrabKeyboard(); - Timer(const Duration(seconds: 5), () async { - updateUrl = await bind.mainGetSoftwareUpdateUrl(); - if (updateUrl.isNotEmpty) setState(() {}); + _updateTimer = Timer.periodic(const Duration(seconds: 1), (timer) async { + final url = await bind.mainGetSoftwareUpdateUrl(); + if (updateUrl != url) { + updateUrl = url; + setState(() {}); + } + final error = await bind.mainGetError(); + if (systemError != error) { + systemError = error; + setState(() {}); + } + final v = await bind.mainGetOption(key: "stop-service") == "Y"; + if (v != svcStopped.value) { + svcStopped.value = v; + setState(() {}); + } + if (watchIsCanScreenRecording) { + if (bind.mainIsCanScreenRecording(prompt: false)) { + watchIsCanScreenRecording = false; + setState(() {}); + } + } + if (watchIsProcessTrust) { + if (bind.mainIsProcessTrusted(prompt: false)) { + watchIsProcessTrust = false; + setState(() {}); + } + } }); + Get.put(svcStopped, tag: 'stop-service'); // disable this tray because we use tray function provided by rust now // initTray(); trayManager.addListener(this); rustDeskWinManager.registerActiveWindowListener(onActiveWindowChanged); // main window may be hidden because of the initial uni link or arguments. - // note that we must wrap this active window registration in future because + // note that we must wrap this active window registration in future because // we must ensure the execution is after `windowManager.hide/show()`. Future.delayed(Duration.zero, () { windowManager.isVisible().then((visibility) { @@ -475,6 +550,8 @@ class _DesktopHomePageState extends State // rustDeskWinManager.unregisterActiveWindowListener(onActiveWindowChanged); trayManager.removeListener(this); _uniLinksSubscription?.cancel(); + Get.delete(tag: 'stop-service'); + _updateTimer?.cancel(); super.dispose(); } } diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 056b1028b..a51b4d035 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -434,7 +434,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { bool get wantKeepAlive => true; bool locked = bind.mainIsInstalled(); final scrollController = ScrollController(); - final RxBool serviceStop = Get.find(tag: 'service-stop'); + final RxBool serviceStop = Get.find(tag: 'stop-service'); @override Widget build(BuildContext context) { diff --git a/flutter/macos/Runner.xcodeproj/project.pbxproj b/flutter/macos/Runner.xcodeproj/project.pbxproj index 5f369ea9e..ec4baf141 100644 --- a/flutter/macos/Runner.xcodeproj/project.pbxproj +++ b/flutter/macos/Runner.xcodeproj/project.pbxproj @@ -439,7 +439,7 @@ "$(inherited)", ../../target/profile, ); - MACOSX_DEPLOYMENT_TARGET = 10.15; + MACOSX_DEPLOYMENT_TARGET = 10.14; PRODUCT_BUNDLE_IDENTIFIER = com.carriez.rustdesk; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; @@ -499,7 +499,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.15; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -547,7 +547,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.15; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -574,7 +574,7 @@ "$(inherited)", ../../target/debug, ); - MACOSX_DEPLOYMENT_TARGET = 10.15; + MACOSX_DEPLOYMENT_TARGET = 10.14; PRODUCT_BUNDLE_IDENTIFIER = com.carriez.rustdesk; PROVISIONING_PROFILE_SPECIFIER = ""; "SWIFT_OBJC_BRIDGING_HEADER[arch=*]" = Runner/bridge_generated.h; @@ -601,7 +601,7 @@ "$(inherited)", ../../target/release, ); - MACOSX_DEPLOYMENT_TARGET = 10.15; + MACOSX_DEPLOYMENT_TARGET = 10.14; PRODUCT_BUNDLE_IDENTIFIER = com.carriez.rustdesk; PROVISIONING_PROFILE_SPECIFIER = ""; "SWIFT_OBJC_BRIDGING_HEADER[arch=*]" = Runner/bridge_generated.h; diff --git a/flutter/windows/runner/main.cpp b/flutter/windows/runner/main.cpp index 5680ab5d3..9b75aa086 100644 --- a/flutter/windows/runner/main.cpp +++ b/flutter/windows/runner/main.cpp @@ -25,7 +25,7 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, return EXIT_FAILURE; } FUNC_RUSTDESK_CORE_MAIN rustdesk_core_main = - (FUNC_RUSTDESK_CORE_MAIN)GetProcAddress(hInstance, "rustdesk_core_main"); + (FUNC_RUSTDESK_CORE_MAIN)GetProcAddress(hInstance, "rustdesk_core_main_args"); if (!rustdesk_core_main) { std::cout << "Failed to get rustdesk_core_main" << std::endl; diff --git a/src/flutter.rs b/src/flutter.rs index 9c4208625..27d12860b 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -40,7 +40,7 @@ pub extern "C" fn rustdesk_core_main() -> bool { #[cfg(windows)] #[no_mangle] -pub extern "C" fn rustdesk_core_main(args_len: *mut c_int) -> *mut *mut c_char { +pub extern "C" fn rustdesk_core_main_args(args_len: *mut c_int) -> *mut *mut c_char { unsafe { std::ptr::write(args_len, 0) }; #[cfg(not(any(target_os = "android", target_os = "ios")))] { diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 00f9b51e6..0f3fb5ef6 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -483,6 +483,10 @@ pub fn main_get_option(key: String) -> String { get_option(key) } +pub fn main_get_error() -> String { + get_error() +} + pub fn main_set_option(key: String, value: String) { if key.eq("custom-rendezvous-server") { set_option(key, value); diff --git a/src/lang.rs b/src/lang.rs index de0fed0b8..6254e988a 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -110,7 +110,7 @@ pub fn translate_locale(name: String, locale: &str) -> String { "fa" => fa::T.deref(), "ca" => ca::T.deref(), "gr" => gr::T.deref(), - "gr" => sv::T.deref(), + "sv" => sv::T.deref(), _ => en::T.deref(), }; if let Some(v) = m.get(&name as &str) { From 557e99d09ee30fa751f7bcacd7b4c5e461856787 Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 29 Nov 2022 22:08:09 +0800 Subject: [PATCH 1071/2015] wayalnd, do not share screen when no connections Signed-off-by: fufesou --- src/server.rs | 1 - src/server/video_service.rs | 15 +++++---------- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/src/server.rs b/src/server.rs index a12a31dbd..91a0da071 100644 --- a/src/server.rs +++ b/src/server.rs @@ -379,7 +379,6 @@ pub async fn start_server(is_server: bool) { #[cfg(windows)] crate::platform::windows::bootstrap(); input_service::fix_key_down_timeout_loop(); - allow_err!(video_service::check_init().await); #[cfg(target_os = "macos")] tokio::spawn(async { sync_and_watch_config_dir().await }); crate::RendezvousMediator::start_all().await; diff --git a/src/server/video_service.rs b/src/server/video_service.rs index a169b3835..173373dbc 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -660,6 +660,11 @@ fn run(sp: GenericService) -> ResultType<()> { std::thread::sleep(spf - elapsed); } } + + if !scrap::is_x11() { + super::wayland::release_resouce(); + } + Ok(()) } @@ -764,16 +769,6 @@ fn get_display_num() -> usize { } } -pub async fn check_init() -> ResultType<()> { - #[cfg(target_os = "linux")] - { - if !scrap::is_x11() { - return super::wayland::check_init().await; - } - } - Ok(()) -} - pub(super) fn get_displays_2(all: &Vec) -> (usize, Vec) { let mut displays = Vec::new(); let mut primary = 0; From 6e1f8f0294b04d80c7bea987ff7a308c644d1c10 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 29 Nov 2022 22:36:35 +0800 Subject: [PATCH 1072/2015] new fetchID and periodic_immediate --- flutter/lib/common.dart | 7 ++++++ .../lib/desktop/pages/connection_page.dart | 2 +- .../lib/desktop/pages/desktop_home_page.dart | 3 ++- flutter/lib/models/server_model.dart | 25 ++++--------------- 4 files changed, 15 insertions(+), 22 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index fed58e999..60936cfdf 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1403,3 +1403,10 @@ void onActiveWindowChanged() async { } } } + +Timer periodic_immediate(Duration duration, Future Function() callback) { + Future.delayed(Duration.zero, callback); + return Timer.periodic(duration, (timer) async { + await callback(); + }); +} diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index a1e87b418..a830d6399 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -61,7 +61,7 @@ class _ConnectionPageState extends State } }(); } - _updateTimer = Timer.periodic(Duration(seconds: 1), (timer) { + _updateTimer = periodic_immediate(Duration(seconds: 1), () async { updateStatus(); }); _idFocusNode.addListener(() { diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index a6d4691ce..712563a56 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -461,7 +461,8 @@ class _DesktopHomePageState extends State void initState() { super.initState(); bind.mainStartGrabKeyboard(); - _updateTimer = Timer.periodic(const Duration(seconds: 1), (timer) async { + _updateTimer = periodic_immediate(const Duration(seconds: 1), () async { + await gFFI.serverModel.fetchID(); final url = await bind.mainGetSoftwareUpdateUrl(); if (updateUrl != url) { updateUrl = url; diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index 456c3cdd2..344733324 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -324,7 +324,6 @@ class ServerModel with ChangeNotifier { parent.target?.ffiModel.updateEventListener(""); await parent.target?.invokeMethod("init_service"); await bind.mainStartService(); - _fetchID(); updateClientState(); if (!Platform.isLinux) { // current linux is not supported @@ -360,26 +359,12 @@ class ServerModel with ChangeNotifier { } } - _fetchID() async { - final old = _serverId.id; - var count = 0; - const maxCount = 10; - while (count < maxCount) { - await Future.delayed(Duration(seconds: 1)); - final id = await bind.mainGetMyId(); - if (id.isEmpty) { - continue; - } else { - _serverId.id = id; - } - - debugPrint("fetch id again at $count:id:${_serverId.id}"); - count++; - if (_serverId.id != old) { - break; - } + fetchID() async { + final id = await bind.mainGetMyId(); + if (id != _serverId.id) { + _serverId.id = id; + notifyListeners(); } - notifyListeners(); } changeStatue(String name, bool value) { From 8a4f8e202db9a3a6a28abe87285dad66ae54ce03 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 29 Nov 2022 23:03:16 +0800 Subject: [PATCH 1073/2015] opt: ui & cursor - opt: win7 frameless - opt: disable cursor output & macos free cache - opt: main window, set location before show/hide --- flutter/lib/common.dart | 26 ++++++++++++++++++- flutter/lib/consts.dart | 19 ++++++++++++++ .../lib/desktop/pages/desktop_home_page.dart | 10 ------- flutter/lib/main.dart | 18 +++++++++++-- flutter/lib/models/native_model.dart | 1 + flutter/lib/utils/multi_window_manager.dart | 5 ++-- flutter/pubspec.yaml | 4 +-- 7 files changed, 65 insertions(+), 18 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 60936cfdf..4e6c8ab14 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -6,7 +6,7 @@ import 'dart:typed_data'; import 'package:back_button_interceptor/back_button_interceptor.dart'; import 'package:desktop_multi_window/desktop_multi_window.dart'; -import 'package:flutter/foundation.dart'; +import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -42,6 +42,8 @@ var isWeb = false; var isWebDesktop = false; var version = ""; int androidVersion = 0; +/// only avaliable for Windows target +int windowsBuildNumber = 0; DesktopType? desktopType; /// * debug or test only, DO NOT enable in release build @@ -1410,3 +1412,25 @@ Timer periodic_immediate(Duration duration, Future Function() callback) { await callback(); }); } +/// return a human readable windows version +WindowsTarget getWindowsTarget(int buildNumber) { + if (!Platform.isWindows) { + return WindowsTarget.naw; + } + if (buildNumber >= 22000) { + return WindowsTarget.w11; + } else if (buildNumber >= 10240) { + return WindowsTarget.w10; + } else if (buildNumber >= 9600) { + return WindowsTarget.w8_1; + } else if (buildNumber >= 9200) { + return WindowsTarget.w8; + } else if (buildNumber >= 7601) { + return WindowsTarget.w7; + } else if (buildNumber >= 6002) { + return WindowsTarget.vista; + } else { + // minimum support + return WindowsTarget.xp; + } +} diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index ae0d334e2..b0099ca7c 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'dart:io'; +import 'package:flutter_hbb/common.dart'; + const double kDesktopRemoteTabBarHeight = 28.0; /// [kAppTypeMain] used by 'Desktop Main Page' , 'Mobile (Client and Server)' , 'Desktop CM Page', "Install Page" @@ -324,3 +326,20 @@ const Map physicalKeyMap = { 0x000c019e: 'LOCK_SCREEN', 0x000c0208: 'VK_PRINT', }; + +/// The windows targets in the publish time order. +enum WindowsTarget { + naw, // not a windows target + xp, + vista, + w7, + w8, + w8_1, + w10, + w11 +} + +/// A convenient method to transform a build number to the corresponding windows version. +extension WindowsTargetExt on int { + WindowsTarget get windowsVersion => getWindowsTarget(this); +} diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 712563a56..53f4d4d90 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -496,16 +496,6 @@ class _DesktopHomePageState extends State // initTray(); trayManager.addListener(this); rustDeskWinManager.registerActiveWindowListener(onActiveWindowChanged); - // main window may be hidden because of the initial uni link or arguments. - // note that we must wrap this active window registration in future because - // we must ensure the execution is after `windowManager.hide/show()`. - Future.delayed(Duration.zero, () { - windowManager.isVisible().then((visibility) { - if (visibility) { - rustDeskWinManager.registerActiveWindow(kWindowMainId); - } - }); - }); rustDeskWinManager.setMethodHandler((call, fromWindowId) async { debugPrint( diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 8af2a477f..08095e54c 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -118,17 +118,20 @@ void runMainApp(bool startService) async { gFFI.serverModel.startService(); } runApp(App()); + // restore the location of the main window before window hide or show + await restoreWindowPosition(WindowType.Main); // check the startup argument, if we successfully handle the argument, we keep the main window hidden. if (checkArguments()) { windowManager.hide(); } else { windowManager.show(); windowManager.focus(); + // move registration of active main window here to prevent async visible check. + rustDeskWinManager.registerActiveWindow(kWindowMainId); } // set window option WindowOptions windowOptions = getHiddenTitleBarWindowOptions(); windowManager.waitUntilReadyToShow(windowOptions, () async { - restoreWindowPosition(WindowType.Main); windowManager.setOpacity(1); }); } @@ -173,6 +176,11 @@ void runMultiWindow( widget, MyTheme.currentThemeMode(), ); + // we do not hide titlebar on win7 because of the frame overflow. + if (Platform.isWindows && + const [WindowsTarget.w7].contains(windowsBuildNumber.windowsVersion)) { + WindowController.fromWindowId(windowId!).showTitleBar(true); + } switch (appType) { case kAppTypeDesktopRemote: await restoreWindowPosition(WindowType.RemoteDesktop, @@ -273,12 +281,18 @@ void runInstallPage() async { } WindowOptions getHiddenTitleBarWindowOptions({Size? size}) { + var defaultTitleBarStyle = TitleBarStyle.hidden; + // we do not hide titlebar on win7 because of the frame overflow. + if (Platform.isWindows && + const [WindowsTarget.w7].contains(windowsBuildNumber.windowsVersion)) { + defaultTitleBarStyle = TitleBarStyle.normal; + } return WindowOptions( size: size, center: false, backgroundColor: Colors.transparent, skipTaskbar: false, - titleBarStyle: TitleBarStyle.hidden, + titleBarStyle: defaultTitleBarStyle, ); } diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index d29e0fd2c..0fa023e53 100644 --- a/flutter/lib/models/native_model.dart +++ b/flutter/lib/models/native_model.dart @@ -145,6 +145,7 @@ class PlatformFFI { WindowsDeviceInfo winInfo = await deviceInfo.windowsInfo; name = winInfo.computerName; id = winInfo.computerName; + windowsBuildNumber = winInfo.buildNumber; } catch (e) { debugPrint("$e"); name = "unknown"; diff --git a/flutter/lib/utils/multi_window_manager.dart b/flutter/lib/utils/multi_window_manager.dart index 34367fece..91cb9a08a 100644 --- a/flutter/lib/utils/multi_window_manager.dart +++ b/flutter/lib/utils/multi_window_manager.dart @@ -202,9 +202,8 @@ class RustDeskMultiWindowManager { // ignore } else { _activeWindows.add(windowId); - _notifyActiveWindow(); } - + _notifyActiveWindow(); } /// Remove active window which has [`windowId`] @@ -218,8 +217,8 @@ class RustDeskMultiWindowManager { // ignore } else { _activeWindows.remove(windowId); - _notifyActiveWindow(); } + _notifyActiveWindow(); } void registerActiveWindowListener(VoidCallback callback) { diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index cb3ce5fc9..f84cbedcd 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -36,7 +36,7 @@ dependencies: provider: ^6.0.3 tuple: ^2.0.0 wakelock: ^0.6.2 - device_info_plus: ^4.1.2 + device_info_plus: ^8.0.0 #firebase_analytics: ^9.1.5 package_info_plus: ^1.4.2 url_launcher: ^6.0.9 @@ -72,7 +72,7 @@ dependencies: flutter_custom_cursor: git: url: https://github.com/Kingtous/rustdesk_flutter_custom_cursor - ref: bfb19c84a8244771488bc05cc5f9c9b5e0324cfd + ref: 74b1b314142b6775c1243067a3503ac568ebc74b window_size: git: url: https://github.com/google/flutter-desktop-embedding.git From ab9de4c7e83db03a63a1624e5bd207a09fcd7c9f Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 29 Nov 2022 23:17:27 +0800 Subject: [PATCH 1074/2015] fix build Signed-off-by: fufesou --- src/server/video_service.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/server/video_service.rs b/src/server/video_service.rs index 173373dbc..c8f59d60d 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -661,6 +661,7 @@ fn run(sp: GenericService) -> ResultType<()> { } } + #[cfg(target_os = "linux")] if !scrap::is_x11() { super::wayland::release_resouce(); } From 60073e037e2ca1d02645e413ac15735ccb897d9e Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 30 Nov 2022 10:28:03 +0800 Subject: [PATCH 1075/2015] wayland better uinput control Signed-off-by: fufesou --- src/server.rs | 4 +++ src/server/input_service.rs | 56 ++++++++++++++++++------------------- src/server/wayland.rs | 5 ++-- 3 files changed, 34 insertions(+), 31 deletions(-) diff --git a/src/server.rs b/src/server.rs index 91a0da071..d08dd2672 100644 --- a/src/server.rs +++ b/src/server.rs @@ -379,6 +379,10 @@ pub async fn start_server(is_server: bool) { #[cfg(windows)] crate::platform::windows::bootstrap(); input_service::fix_key_down_timeout_loop(); + #[cfg(target_os = "linux")] + if crate::platform::current_is_wayland() { + allow_err!(input_service::setup_uinput(0, 1920, 0, 1080).await); + } #[cfg(target_os = "macos")] tokio::spawn(async { sync_and_watch_config_dir().await }); crate::RendezvousMediator::start_all().await; diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 4789e459b..b465658bb 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -223,44 +223,44 @@ lazy_static::lazy_static! { // The clients are ipc connections that must live shorter than tokio runtime. // Thus this funtion must not be called in a temporary runtime. #[cfg(target_os = "linux")] -pub async fn set_uinput() -> ResultType<()> { +pub async fn setup_uinput(minx: i32, maxx: i32, miny: i32, maxy: i32) -> ResultType<()> { // Keyboard and mouse both open /dev/uinput // TODO: Make sure there's no race + set_uinput_resolution(minx, maxx, miny, maxy).await?; - if ENIGO.lock().unwrap().get_custom_keyboard().is_none() { - let keyboard = super::uinput::client::UInputKeyboard::new().await?; - log::info!("UInput keyboard created"); - ENIGO - .lock() - .unwrap() - .set_custom_keyboard(Box::new(keyboard)); - } + let keyboard = super::uinput::client::UInputKeyboard::new().await?; + log::info!("UInput keyboard created"); + let mouse = super::uinput::client::UInputMouse::new().await?; + log::info!("UInput mouse created"); - let mouse_created = ENIGO.lock().unwrap().get_custom_mouse().is_some(); - if mouse_created { - std::thread::spawn(|| { - if let Some(mouse) = ENIGO.lock().unwrap().get_custom_mouse() { - if let Some(mouse) = mouse - .as_mut_any() - .downcast_mut::() - { - allow_err!(mouse.send_refresh()); - } else { - log::error!("failed downcast uinput mouse"); - } - } - }); - } else { - let mouse = super::uinput::client::UInputMouse::new().await?; - log::info!("UInput mouse created"); - ENIGO.lock().unwrap().set_custom_mouse(Box::new(mouse)); + ENIGO + .lock() + .unwrap() + .set_custom_keyboard(Box::new(keyboard)); + ENIGO.lock().unwrap().set_custom_mouse(Box::new(mouse)); + Ok(()) +} + +#[cfg(target_os = "linux")] +pub async fn update_mouse_resolution(minx: i32, maxx: i32, miny: i32, maxy: i32) -> ResultType<()> { + set_uinput_resolution(minx, maxx, miny, maxy).await?; + + if let Some(mouse) = ENIGO.lock().unwrap().get_custom_mouse() { + if let Some(mouse) = mouse + .as_mut_any() + .downcast_mut::() + { + allow_err!(mouse.send_refresh()); + } else { + log::error!("failed downcast uinput mouse"); + } } Ok(()) } #[cfg(target_os = "linux")] -pub async fn set_uinput_resolution(minx: i32, maxx: i32, miny: i32, maxy: i32) -> ResultType<()> { +async fn set_uinput_resolution(minx: i32, maxx: i32, miny: i32, maxy: i32) -> ResultType<()> { super::uinput::client::set_resolution(minx, maxx, miny, maxy).await } diff --git a/src/server/wayland.rs b/src/server/wayland.rs index 071077c84..fdf9bccec 100644 --- a/src/server/wayland.rs +++ b/src/server/wayland.rs @@ -174,14 +174,13 @@ pub(super) async fn check_init() -> ResultType<()> { if minx != maxx && miny != maxy { log::info!( - "send uinput resolution: ({}, {}), ({}, {})", + "update mouse resolution: ({}, {}), ({}, {})", minx, maxx, miny, maxy ); - allow_err!(input_service::set_uinput_resolution(minx, maxx, miny, maxy).await); - allow_err!(input_service::set_uinput().await); + allow_err!(input_service::update_mouse_resolution(minx, maxx, miny, maxy).await); } } Ok(()) From 767950d429618594945887c951af17738e6bc50b Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 30 Nov 2022 10:43:15 +0800 Subject: [PATCH 1076/2015] remove unneccessary ?? Signed-off-by: fufesou --- flutter/lib/models/native_model.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index 0fa023e53..be80bb65b 100644 --- a/flutter/lib/models/native_model.dart +++ b/flutter/lib/models/native_model.dart @@ -131,7 +131,7 @@ class PlatformFFI { AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; name = '${androidInfo.brand}-${androidInfo.model}'; id = androidInfo.id.hashCode.toString(); - androidVersion = androidInfo.version.sdkInt ?? 0; + androidVersion = androidInfo.version.sdkInt; } else if (Platform.isIOS) { IosDeviceInfo iosInfo = await deviceInfo.iosInfo; name = iosInfo.utsname.machine ?? ''; From c6e658e256c315c88d446e02f75dc1f2ae3dd9bc Mon Sep 17 00:00:00 2001 From: Kingtous Date: Wed, 30 Nov 2022 13:56:02 +0800 Subject: [PATCH 1077/2015] opt: fix win7 crash on latest device_info_plus --- flutter/lib/common.dart | 53 +++++++++++++- .../lib/desktop/widgets/tabbar_widget.dart | 73 ++++++++++++------- flutter/lib/main.dart | 6 +- flutter/lib/models/native_model.dart | 9 ++- flutter/pubspec.yaml | 4 +- 5 files changed, 107 insertions(+), 38 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 4e6c8ab14..63ab39df2 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1,12 +1,15 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:ffi' hide Size; import 'dart:io'; import 'dart:math'; import 'dart:typed_data'; import 'package:back_button_interceptor/back_button_interceptor.dart'; import 'package:desktop_multi_window/desktop_multi_window.dart'; -import 'package:device_info_plus/device_info_plus.dart'; +import 'package:ffi/ffi.dart'; +import 'package:flutter/foundation.dart'; +import 'package:win32/win32.dart' as win32; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -42,6 +45,7 @@ var isWeb = false; var isWebDesktop = false; var version = ""; int androidVersion = 0; + /// only avaliable for Windows target int windowsBuildNumber = 0; DesktopType? desktopType; @@ -1412,11 +1416,12 @@ Timer periodic_immediate(Duration duration, Future Function() callback) { await callback(); }); } + /// return a human readable windows version WindowsTarget getWindowsTarget(int buildNumber) { if (!Platform.isWindows) { return WindowsTarget.naw; - } + } if (buildNumber >= 22000) { return WindowsTarget.w11; } else if (buildNumber >= 10240) { @@ -1434,3 +1439,47 @@ WindowsTarget getWindowsTarget(int buildNumber) { return WindowsTarget.xp; } } + +/// Get windows target build number. +/// +/// [Note] +/// Please use this function wrapped with `Platform.isWindows`. +int getWindowsTargetBuildNumber() { + final rtlGetVersion = DynamicLibrary.open('ntdll.dll').lookupFunction< + Void Function(Pointer), + void Function(Pointer)>('RtlGetVersion'); + final osVersionInfo = getOSVERSIONINFOEXPointer(); + rtlGetVersion(osVersionInfo); + int buildNumber = osVersionInfo.ref.dwBuildNumber; + calloc.free(osVersionInfo); + return buildNumber; +} + +/// Get Windows OS version pointer +/// +/// [Note] +/// Please use this function wrapped with `Platform.isWindows`. +Pointer getOSVERSIONINFOEXPointer() { + final pointer = calloc(); + pointer.ref + ..dwOSVersionInfoSize = sizeOf() + ..dwBuildNumber = 0 + ..dwMajorVersion = 0 + ..dwMinorVersion = 0 + ..dwPlatformId = 0 + ..szCSDVersion = '' + ..wServicePackMajor = 0 + ..wServicePackMinor = 0 + ..wSuiteMask = 0 + ..wProductType = 0 + ..wReserved = 0; + return pointer; +} + +/// Indicating we need to use compatible ui mode. +/// +/// [Conditions] +/// - Windows 7, window will overflow when we use frameless ui. +bool get kUseCompatibleUiMode => + Platform.isWindows && + const [WindowsTarget.w7].contains(windowsBuildNumber.windowsVersion); diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 506399e6f..e868b37e2 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -266,7 +266,8 @@ class DesktopTab extends StatelessWidget { Widget build(BuildContext context) { return Column(children: [ Obx(() => Offstage( - offstage: !stateGlobal.showTabBar.isTrue, + offstage: !stateGlobal.showTabBar.isTrue || + (kUseCompatibleUiMode && isHideSingleItem()), child: SizedBox( height: _kTabBarHeight, child: Column( @@ -335,6 +336,15 @@ class DesktopTab extends StatelessWidget { .toList(growable: false)))); } + /// Check whether to show ListView + /// + /// Conditions: + /// - hide single item when only has one item (home) on [DesktopTabPage]. + bool isHideSingleItem() { + return state.value.tabs.length == 1 && + controller.tabType == DesktopTabType.main; + } + Widget _buildBar() { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -362,23 +372,26 @@ class DesktopTab extends StatelessWidget { child: const SizedBox( width: 78, )), - Row(children: [ - Offstage( - offstage: !showLogo, - child: SvgPicture.asset( - 'assets/logo.svg', - width: 16, - height: 16, - )), - Offstage( - offstage: !showTitle, - child: const Text( - "RustDesk", - style: TextStyle(fontSize: 13), - ).marginOnly(left: 2)) - ]).marginOnly( - left: 5, - right: 10, + Offstage( + offstage: kUseCompatibleUiMode, + child: Row(children: [ + Offstage( + offstage: !showLogo, + child: SvgPicture.asset( + 'assets/logo.svg', + width: 16, + height: 16, + )), + Offstage( + offstage: !showTitle, + child: const Text( + "RustDesk", + style: TextStyle(fontSize: 13), + ).marginOnly(left: 2)) + ]).marginOnly( + left: 5, + right: 10, + ), ), Expanded( child: Listener( @@ -407,16 +420,20 @@ class DesktopTab extends StatelessWidget { unSelectedTabBackgroundColor))), ], ))), - WindowActionPanel( - isMainWindow: isMainWindow, - tabType: tabType, - state: state, - tail: tail, - isMaximized: isMaximized, - showMinimize: showMinimize, - showMaximize: showMaximize, - showClose: showClose, - onClose: onWindowCloseButton, + // hide simulated action buttons when we in compatible ui mode, because of reusing system title bar. + Offstage( + offstage: kUseCompatibleUiMode, + child: WindowActionPanel( + isMainWindow: isMainWindow, + tabType: tabType, + state: state, + tail: tail, + isMaximized: isMaximized, + showMinimize: showMinimize, + showMaximize: showMaximize, + showClose: showClose, + onClose: onWindowCloseButton, + ), ) ], ); diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 08095e54c..2015c02b2 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -177,8 +177,7 @@ void runMultiWindow( MyTheme.currentThemeMode(), ); // we do not hide titlebar on win7 because of the frame overflow. - if (Platform.isWindows && - const [WindowsTarget.w7].contains(windowsBuildNumber.windowsVersion)) { + if (kUseCompatibleUiMode) { WindowController.fromWindowId(windowId!).showTitleBar(true); } switch (appType) { @@ -283,8 +282,7 @@ void runInstallPage() async { WindowOptions getHiddenTitleBarWindowOptions({Size? size}) { var defaultTitleBarStyle = TitleBarStyle.hidden; // we do not hide titlebar on win7 because of the frame overflow. - if (Platform.isWindows && - const [WindowsTarget.w7].contains(windowsBuildNumber.windowsVersion)) { + if (kUseCompatibleUiMode) { defaultTitleBarStyle = TitleBarStyle.normal; } return WindowOptions( diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index be80bb65b..dab658c25 100644 --- a/flutter/lib/models/native_model.dart +++ b/flutter/lib/models/native_model.dart @@ -10,6 +10,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:path_provider/path_provider.dart'; +import 'package:win32/win32.dart' as win32; import '../common.dart'; import '../generated_bridge.dart'; @@ -142,12 +143,14 @@ class PlatformFFI { id = linuxInfo.machineId ?? linuxInfo.id; } else if (Platform.isWindows) { try { + // request windows build number to fix overflow on win7 + windowsBuildNumber = getWindowsTargetBuildNumber(); WindowsDeviceInfo winInfo = await deviceInfo.windowsInfo; name = winInfo.computerName; id = winInfo.computerName; - windowsBuildNumber = winInfo.buildNumber; - } catch (e) { - debugPrint("$e"); + } catch (e, stacktrace) { + debugPrint("get windows device info failed: $e"); + debugPrintStack(stackTrace: stacktrace); name = "unknown"; id = "unknown"; } diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index f84cbedcd..8de0be4d6 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -36,7 +36,7 @@ dependencies: provider: ^6.0.3 tuple: ^2.0.0 wakelock: ^0.6.2 - device_info_plus: ^8.0.0 + device_info_plus: ^4.1.2 #firebase_analytics: ^9.1.5 package_info_plus: ^1.4.2 url_launcher: ^6.0.9 @@ -102,6 +102,8 @@ dependencies: path: ^1.8.1 auto_size_text: ^3.0.0 bot_toast: ^4.0.3 + win32: any + dev_dependencies: icons_launcher: ^2.0.4 From 85529105be17b966a90da47e3699f547a962f23d Mon Sep 17 00:00:00 2001 From: Kingtous Date: Wed, 30 Nov 2022 13:57:46 +0800 Subject: [PATCH 1078/2015] fix: sdkInt fallback to 0 --- flutter/lib/models/native_model.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index dab658c25..68b85968a 100644 --- a/flutter/lib/models/native_model.dart +++ b/flutter/lib/models/native_model.dart @@ -132,7 +132,7 @@ class PlatformFFI { AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; name = '${androidInfo.brand}-${androidInfo.model}'; id = androidInfo.id.hashCode.toString(); - androidVersion = androidInfo.version.sdkInt; + androidVersion = androidInfo.version.sdkInt ?? 0; } else if (Platform.isIOS) { IosDeviceInfo iosInfo = await deviceInfo.iosInfo; name = iosInfo.utsname.machine ?? ''; From 10e13362218ba1a0cdc66aa44615ad9c2ed90d94 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Wed, 30 Nov 2022 14:05:49 +0800 Subject: [PATCH 1079/2015] opt: preserve addon buttons --- .../lib/desktop/widgets/tabbar_widget.dart | 120 +++++++++--------- 1 file changed, 63 insertions(+), 57 deletions(-) diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index e868b37e2..daf9272f7 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -421,19 +421,16 @@ class DesktopTab extends StatelessWidget { ], ))), // hide simulated action buttons when we in compatible ui mode, because of reusing system title bar. - Offstage( - offstage: kUseCompatibleUiMode, - child: WindowActionPanel( - isMainWindow: isMainWindow, - tabType: tabType, - state: state, - tail: tail, - isMaximized: isMaximized, - showMinimize: showMinimize, - showMaximize: showMaximize, - showClose: showClose, - onClose: onWindowCloseButton, - ), + WindowActionPanel( + isMainWindow: isMainWindow, + tabType: tabType, + state: state, + tail: tail, + isMaximized: isMaximized, + showMinimize: showMinimize, + showMaximize: showMaximize, + showClose: showClose, + onClose: onWindowCloseButton, ) ], ); @@ -547,50 +544,59 @@ class WindowActionPanelState extends State children: [ Offstage(offstage: widget.tail == null, child: widget.tail), Offstage( - offstage: !widget.showMinimize, - child: ActionIcon( - message: 'Minimize', - icon: IconFont.min, - onTap: () { - if (widget.isMainWindow) { - windowManager.minimize(); - } else { - WindowController.fromWindowId(windowId!).minimize(); - } - }, - isClose: false, - )), - Offstage( - offstage: !widget.showMaximize, - child: Obx(() => ActionIcon( - message: widget.isMaximized.value ? "Restore" : "Maximize", - icon: widget.isMaximized.value - ? IconFont.restore - : IconFont.max, - onTap: _toggleMaximize, - isClose: false, - ))), - Offstage( - offstage: !widget.showClose, - child: ActionIcon( - message: 'Close', - icon: IconFont.close, - onTap: () async { - final res = await widget.onClose?.call() ?? true; - if (res) { - // hide for all window - // note: the main window can be restored by tray icon - Future.delayed(Duration.zero, () async { - if (widget.isMainWindow) { - await windowManager.close(); - } else { - await WindowController.fromWindowId(windowId!).close(); - } - }); - } - }, - isClose: true, - )), + offstage: kUseCompatibleUiMode, + child: Row( + children: [ + Offstage( + offstage: !widget.showMinimize, + child: ActionIcon( + message: 'Minimize', + icon: IconFont.min, + onTap: () { + if (widget.isMainWindow) { + windowManager.minimize(); + } else { + WindowController.fromWindowId(windowId!).minimize(); + } + }, + isClose: false, + )), + Offstage( + offstage: !widget.showMaximize, + child: Obx(() => ActionIcon( + message: + widget.isMaximized.value ? "Restore" : "Maximize", + icon: widget.isMaximized.value + ? IconFont.restore + : IconFont.max, + onTap: _toggleMaximize, + isClose: false, + ))), + Offstage( + offstage: !widget.showClose, + child: ActionIcon( + message: 'Close', + icon: IconFont.close, + onTap: () async { + final res = await widget.onClose?.call() ?? true; + if (res) { + // hide for all window + // note: the main window can be restored by tray icon + Future.delayed(Duration.zero, () async { + if (widget.isMainWindow) { + await windowManager.close(); + } else { + await WindowController.fromWindowId(windowId!) + .close(); + } + }); + } + }, + isClose: true, + )) + ], + ), + ), ], ); } From f1c41c5814351e0aed3307b7b04f0f38577b74af Mon Sep 17 00:00:00 2001 From: Nico Mak Date: Wed, 30 Nov 2022 10:10:01 +0100 Subject: [PATCH 1080/2015] Added new public server information New public servers added. Vultr removed (https://twitter.com/rustdesk/status/1595597511980470272) https://github.com/rustdesk/rustdesk/discussions/1657 Corrected VCPU to vCPU. Sorry for the pull request spam. --- README.md | 3 ++- docs/README-AR.md | 8 +++++--- docs/README-CS.md | 9 +++++---- docs/README-DA.md | 9 +++++---- docs/README-DE.md | 10 +++++----- docs/README-EO.md | 8 +++++--- docs/README-ES.md | 9 +++++---- docs/README-FA.md | 10 +++++----- docs/README-FI.md | 10 +++++----- docs/README-FR.md | 12 +++++++----- docs/README-HU.md | 10 +++++----- docs/README-ID.md | 10 +++++----- docs/README-IT.md | 10 +++++----- docs/README-JP.md | 10 +++++----- docs/README-KR.md | 10 +++++----- docs/README-ML.md | 10 +++++----- docs/README-NL.md | 10 +++++----- docs/README-PL.md | 10 +++++----- docs/README-PTBR.md | 10 +++++----- docs/README-RU.md | 12 +++++++----- docs/README-UA.md | 12 +++++++----- docs/README-VN.md | 10 +++++----- docs/README-ZH.md | 12 +++++++----- 23 files changed, 120 insertions(+), 104 deletions(-) diff --git a/README.md b/README.md index dfaa389a6..2fd744429 100644 --- a/README.md +++ b/README.md @@ -35,9 +35,10 @@ Below are the servers you are using for free, it may change along the time. If y | Location | Vendor | Specification | | --------- | ------------- | ------------------ | | Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM | -| Singapore | Vultr | 1 vCPU / 1GB RAM | | Germany | Hetzner | 2 vCPU / 4GB RAM | | Germany | Codext | 4 vCPU / 8GB RAM | +| Finland (Helsinki) | 0x101 Cyber Security | 4 vCPU / 8GB RAM | +| USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8GB RAM | ## Dependencies diff --git a/docs/README-AR.md b/docs/README-AR.md index de6e7663c..ad7303806 100644 --- a/docs/README-AR.md +++ b/docs/README-AR.md @@ -32,9 +32,11 @@ فيما يلي الخوادم التي تستخدمها مجانًا، وقد تتغير طوال الوقت. إذا لم تكن قريبًا من أحد هؤلاء، فقد تكون شبكتك بطيئة. | الموقع | المورد | المواصفات | | --------- | ------------- | ------------------ | -| Seoul | AWS lightsail | 1 VCPU / 0.5GB RAM | -| Singapore | Vultr | 1 VCPU / 1GB RAM | -| Dallas | Vultr | 1 VCPU / 1GB RAM | | +| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM | +| Germany | Hetzner | 2 vCPU / 4GB RAM | +| Germany | Codext | 4 vCPU / 8GB RAM | +| Finland (Helsinki) | 0x101 Cyber Security | 4 vCPU / 8GB RAM | +| USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8GB RAM | ## التبعيات diff --git a/docs/README-CS.md b/docs/README-CS.md index b1e77165f..d56464eff 100644 --- a/docs/README-CS.md +++ b/docs/README-CS.md @@ -27,10 +27,11 @@ Projekt RustDesk vítá přiložení ruky k dílu od každého. Jak začít se d Níže jsou uvedeny servery zdarma k vašemu použití (údaje se mohou v čase měnit). Pokud se nenacházíte v oblastech světa poblíž nich, spojení může být pomalé. | umístění | dodavatel | parametry | | --------- | ------------- | ------------------ | -| Soul | AWS lightsail | 1 VCPU / 0,5GB RAM | -| Singapur | Vultr | 1 VCPU / 1GB RAM | -| Germany | Hetzner | 2 VCPU / 4GB RAM | -| Germany | Codext | 4 VCPU / 8GB RAM | +| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM | +| Germany | Hetzner | 2 vCPU / 4GB RAM | +| Germany | Codext | 4 vCPU / 8GB RAM | +| Finland (Helsinki) | 0x101 Cyber Security | 4 vCPU / 8GB RAM | +| USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8GB RAM | ## Softwarové součásti, na kterých závisí diff --git a/docs/README-DA.md b/docs/README-DA.md index 6887aefa0..dde5c7a0d 100644 --- a/docs/README-DA.md +++ b/docs/README-DA.md @@ -25,10 +25,11 @@ Nedenfor er de servere, du bruger gratis, det kan ændre sig med tiden. Hvis du | Beliggenhed | Udbyder | Specifikation | | ---------- | ------------- | ------------------ | -| Seoul | AWS lightsail | 1 vCPU / 0,5 GB RAM | -| Singapore | Vultr | 1 vCPU / 1 GB RAM | -| Tyskland | Hetzner | 2 vCPU / 4 GB RAM | -| Tyskland | Codext | 4 vCPU / 8 GB RAM | +| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM | +| Germany | Hetzner | 2 vCPU / 4GB RAM | +| Germany | Codext | 4 vCPU / 8GB RAM | +| Finland (Helsinki) | 0x101 Cyber Security | 4 vCPU / 8GB RAM | +| USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8GB RAM | ## Afhængigheder diff --git a/docs/README-DE.md b/docs/README-DE.md index 074b2af2c..0b51d8fdd 100644 --- a/docs/README-DE.md +++ b/docs/README-DE.md @@ -25,11 +25,11 @@ Hier sind die Server, die du kostenlos nutzen kannst, es kann sein das sich dies | Standort | Serverart | Spezifikationen | Kommentare | | --------- | ------------- | ------------------ | ---------- | -| Seoul | AWS lightsail | 1 VCPU / 0.5GB RAM | | -| Singapore | Vultr | 1 VCPU / 1GB RAM | | -| Dallas | Vultr | 1 VCPU / 1GB RAM | | -| Germany | Codext | 2 VCPU / 4GB RAM | -| Germany | Hetzner | 4 VCPU / 8GB RAM | +| 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 | ## Abhängigkeiten diff --git a/docs/README-EO.md b/docs/README-EO.md index e4ed91e7c..7471636eb 100644 --- a/docs/README-EO.md +++ b/docs/README-EO.md @@ -24,9 +24,11 @@ RustDesk bonvenigas kontribuon de ĉiuj. Vidu [`docs/CONTRIBUTING.md`](CONTRIBUT Malsupre estas la serviloj, kiuj vi uzas senpage, ĝi povas ŝanĝi laŭlonge de la tempo. Se vi ne estas proksima de unu de tiuj, via reto povas esti malrapida. | Situo | Vendanto | Detaloj | | --------- | ------------- | ------------------ | -| Seulo | AWS lightsail | 1 VCPU / 0.5GB RAM | -| Singapuro | Vultr | 1 VCPU / 1GB RAM | -| Dalaso | Vultr | 1 VCPU / 1GB RAM | | +| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM | +| Germany | Hetzner | 2 vCPU / 4GB RAM | +| Germany | Codext | 4 vCPU / 8GB RAM | +| Finland (Helsinki) | 0x101 Cyber Security | 4 vCPU / 8GB RAM | +| USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8GB RAM | ## Dependantaĵoj diff --git a/docs/README-ES.md b/docs/README-ES.md index f647ecbfd..16f65adcc 100644 --- a/docs/README-ES.md +++ b/docs/README-ES.md @@ -31,10 +31,11 @@ A continuación se muestran los servidores gratuitos, pueden cambiar a medida qu | Ubicación | Compañía | Especificación | | --------- | ------------- | ------------------ | -| Seúl | AWS lightsail | 1 vCPU / 0.5GB RAM | -| Singapur | Vultr | 1 vCPU / 1GB RAM | -| Alemania | Hetzner | 2 vCPU / 4GB RAM | -| Alemania | Codext | 4 vCPU / 8GB RAM | +| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM | +| Germany | Hetzner | 2 vCPU / 4GB RAM | +| Germany | Codext | 4 vCPU / 8GB RAM | +| Finland (Helsinki) | 0x101 Cyber Security | 4 vCPU / 8GB RAM | +| USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8GB RAM | ## Dependencias diff --git a/docs/README-FA.md b/docs/README-FA.md index 64a540700..d86c82836 100644 --- a/docs/README-FA.md +++ b/docs/README-FA.md @@ -29,11 +29,11 @@ سرورهایی زیر را به صورت رایگان میتوانید استفاده می کنید. این لیست ممکن است در طول زمان تغییر کند. اگر به این سرورها نزدیک نیستید، ممکن است سرویس شما کند شود. | موقعیت | سرویس دهنده | مشخصات | | --------- | ------------- | ------------------ | -| Seoul | AWS lightsail | 1 VCPU / 0.5GB RAM | -| Singapore | Vultr | 1 VCPU / 1GB RAM | -| Dallas | Vultr | 1 VCPU / 1GB RAM | | -| Germany | Hetzner | 2 VCPU / 4GB RAM | -| Germany | Codext | 4 VCPU / 8GB RAM | +| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM | +| Germany | Hetzner | 2 vCPU / 4GB RAM | +| Germany | Codext | 4 vCPU / 8GB RAM | +| Finland (Helsinki) | 0x101 Cyber Security | 4 vCPU / 8GB RAM | +| USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8GB RAM | ## وابستگی ها diff --git a/docs/README-FI.md b/docs/README-FI.md index 4ee6c73e1..f7a087087 100644 --- a/docs/README-FI.md +++ b/docs/README-FI.md @@ -24,11 +24,11 @@ RustDesk toivottaa avustukset tervetulleiksi kaikilta. Katso lisätietoja [`docs Alla on palvelimia, joita voit käyttää ilmaiseksi, ne saattavat muuttua ajan mittaan. Jos et ole lähellä yhtä näistä, verkkosi voi olla hidas. | Sijainti | Myyjä | Määrittely | | --------- | ------------- | ------------------ | -| Seoul | AWS lightsail | 1 VCPU / 0.5GB RAM | -| Singapore | Vultr | 1 VCPU / 1GB RAM | -| Dallas | Vultr | 1 VCPU / 1GB RAM | | -| Germany | Hetzner | 2 VCPU / 4GB RAM | -| Germany | Codext | 4 VCPU / 8GB RAM | +| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM | +| Germany | Hetzner | 2 vCPU / 4GB RAM | +| Germany | Codext | 4 vCPU / 8GB RAM | +| Finland (Helsinki) | 0x101 Cyber Security | 4 vCPU / 8GB RAM | +| USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8GB RAM | ## Riippuvuudet diff --git a/docs/README-FR.md b/docs/README-FR.md index 1d88ce0f2..fdb253bd0 100644 --- a/docs/README-FR.md +++ b/docs/README-FR.md @@ -23,11 +23,13 @@ RustDesk accueille les contributions de tout le monde. Voir [`docs/CONTRIBUTING. Ci-dessous se trouvent les serveurs que vous utilisez gratuitement, cela peut changer au fil du temps. Si vous n'êtes pas proche de l'un d'entre eux, votre réseau peut être lent. -- Séoul, AWS lightsail, 1 VCPU/0.5G RAM -- Singapour, Vultr, 1 VCPU/1G RAM -- Dallas, Vultr, 1 VCPU/1G RAM -- Germany, Codext, 2 VCPU / 4GB RAM -- Germany, Hetzner, 4 VCPU / 8GB RAM +| Location | Vendor | Specification | +| --------- | ------------- | ------------------ | +| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM | +| Germany | Hetzner | 2 vCPU / 4GB RAM | +| Germany | Codext | 4 vCPU / 8GB RAM | +| Finland (Helsinki) | 0x101 Cyber Security | 4 vCPU / 8GB RAM | +| USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8GB RAM | ## Dépendances diff --git a/docs/README-HU.md b/docs/README-HU.md index 13d3ee4e0..6c22a3b7c 100644 --- a/docs/README-HU.md +++ b/docs/README-HU.md @@ -32,11 +32,11 @@ A RustDesk szívesen fogad minden contributiont, támogatást mindenkitől. Lás Ezalatt az üzenet alatt találhatóak azok a publikus szerverek, amelyeket ingyen használhatsz. Ezek a szerverek változhatnak a jövőben, illetve a hálózatuk lehet hogy lassú lehet. | Hely | Host | Specifikáció | | --------- | ------------- | ------------------ | -| Seoul | AWS lightsail | 1 VCPU / 0.5GB RAM | -| Singapore | Vultr | 1 VCPU / 1GB RAM | -| Dallas | Vultr | 1 VCPU / 1GB RAM | | -| Germany | Hetzner | 2 VCPU / 4GB RAM | -| Germany | Codext | 4 VCPU / 8GB RAM | +| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM | +| Germany | Hetzner | 2 vCPU / 4GB RAM | +| Germany | Codext | 4 vCPU / 8GB RAM | +| Finland (Helsinki) | 0x101 Cyber Security | 4 vCPU / 8GB RAM | +| USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8GB RAM | ## Dependencies diff --git a/docs/README-ID.md b/docs/README-ID.md index d9986090d..9616cd31d 100644 --- a/docs/README-ID.md +++ b/docs/README-ID.md @@ -24,11 +24,11 @@ RustDesk menyambut baik kontribusi dari semua orang. Lihat [`docs/CONTRIBUTING.m Di bawah ini adalah server yang bisa Anda gunakan secara gratis, dapat berubah seiring waktu. Jika Anda tidak dekat dengan salah satu dari ini, jaringan Anda mungkin lambat. | Lokasi | Vendor | Spesifikasi | | --------- | ------------- | ------------------ | -| Seoul | AWS lightsail | 1 VCPU / 0.5GB RAM | -| Singapore | Vultr | 1 VCPU / 1GB RAM | -| Dallas | Vultr | 1 VCPU / 1GB RAM | -| Germany | Hetzner | 2 VCPU / 4GB RAM | -| Germany | Codext | 4 VCPU / 8GB RAM | +| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM | +| Germany | Hetzner | 2 vCPU / 4GB RAM | +| Germany | Codext | 4 vCPU / 8GB RAM | +| Finland (Helsinki) | 0x101 Cyber Security | 4 vCPU / 8GB RAM | +| USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8GB RAM | ## Dependencies diff --git a/docs/README-IT.md b/docs/README-IT.md index 4285ca587..f074510c9 100644 --- a/docs/README-IT.md +++ b/docs/README-IT.md @@ -24,11 +24,11 @@ RustDesk accoglie il contributo di tutti. Per ulteriori informazioni su come ini Qui sotto trovate i server che possono essere usati gratuitamente, la lista potrebbe cambiare nel tempo. Se non si è vicini a uno di questi server, la vostra connessione potrebbe essere lenta. | Posizione | Vendor | Specifiche | | --------- | ------------- | ------------------ | -| Seoul | AWS lightsail | 1 VCPU / 0.5GB RAM | -| Singapore | Vultr | 1 VCPU / 1GB RAM | -| Dallas | Vultr | 1 VCPU / 1GB RAM | -| Germany | Hetzner | 2 VCPU / 4GB RAM | -| Germany | Codext | 4 VCPU / 8GB RAM | +| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM | +| Germany | Hetzner | 2 vCPU / 4GB RAM | +| Germany | Codext | 4 vCPU / 8GB RAM | +| Finland (Helsinki) | 0x101 Cyber Security | 4 vCPU / 8GB RAM | +| USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8GB RAM | ## Dipendenze diff --git a/docs/README-JP.md b/docs/README-JP.md index a36261e0b..6d3b6d380 100644 --- a/docs/README-JP.md +++ b/docs/README-JP.md @@ -29,11 +29,11 @@ RustDeskは誰からの貢献も歓迎します。 貢献するには [`docs/CON 下記のサーバーは、無料で使用できますが、後々変更されることがあります。これらのサーバーから遠い場合、接続が遅い可能性があります。 | Location | Vendor | Specification | | --------- | ------------- | ------------------ | -| Seoul | AWS lightsail | 1 VCPU / 0.5GB RAM | -| Singapore | Vultr | 1 VCPU / 1GB RAM | -| Dallas | Vultr | 1 VCPU / 1GB RAM | -| Germany | Hetzner | 2 VCPU / 4GB RAM | -| Germany | Codext | 4 VCPU / 8GB RAM | +| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM | +| Germany | Hetzner | 2 vCPU / 4GB RAM | +| Germany | Codext | 4 vCPU / 8GB RAM | +| Finland (Helsinki) | 0x101 Cyber Security | 4 vCPU / 8GB RAM | +| USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8GB RAM | ## 依存関係 diff --git a/docs/README-KR.md b/docs/README-KR.md index f99a6dbda..8cefbbcee 100644 --- a/docs/README-KR.md +++ b/docs/README-KR.md @@ -29,11 +29,11 @@ RustDesk는 모든 기여를 환영합니다. 기여하고자 한다면 [`docs/C 표에 있는 서버는 무료로 사용할 수 있지만 추후 변경될 수도 있습니다. 이 서버에서 멀다면, 네트워크가 느려질 가능성도 있습니다. | Location | Vendor | Specification | | --------- | ------------- | ------------------ | -| Seoul | AWS lightsail | 1 VCPU / 0.5GB RAM | -| Singapore | Vultr | 1 VCPU / 1GB RAM | -| Dallas | Vultr | 1 VCPU / 1GB RAM | -| Germany | Hetzner | 2 VCPU / 4GB RAM | -| Germany | Codext | 4 VCPU / 8GB RAM | +| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM | +| Germany | Hetzner | 2 vCPU / 4GB RAM | +| Germany | Codext | 4 vCPU / 8GB RAM | +| Finland (Helsinki) | 0x101 Cyber Security | 4 vCPU / 8GB RAM | +| USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8GB RAM | ## 의존관계 diff --git a/docs/README-ML.md b/docs/README-ML.md index 2c8854858..288a78db8 100644 --- a/docs/README-ML.md +++ b/docs/README-ML.md @@ -24,11 +24,11 @@ നിങ്ങൾ സൗജന്യമായി ഉപയോഗിക്കുന്ന സെർവറുകൾ ചുവടെയുണ്ട്, അത് സമയത്തിനനുസരിച്ച് മാറിയേക്കാം. നിങ്ങൾ ഇവയിലൊന്നിനോട് അടുത്തല്ലെങ്കിൽ, നിങ്ങളുടെ നെറ്റ്‌വർക്ക് സ്ലോ ആയേക്കാം. | സ്ഥാനം | കച്ചവടക്കാരൻ | വിവരണം | | --------- | ------------- | ------------------ | -| Seoul | AWS lightsail | 1 VCPU / 0.5GB RAM | -| Singapore | Vultr | 1 VCPU / 1GB RAM | -| Dallas | Vultr | 1 VCPU / 1GB RAM | -| Germany | Hetzner | 2 VCPU / 4GB RAM | -| Germany | Codext | 4 VCPU / 8GB RAM | +| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM | +| Germany | Hetzner | 2 vCPU / 4GB RAM | +| Germany | Codext | 4 vCPU / 8GB RAM | +| Finland (Helsinki) | 0x101 Cyber Security | 4 vCPU / 8GB RAM | +| USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8GB RAM | ## ഡിപെൻഡൻസികൾ diff --git a/docs/README-NL.md b/docs/README-NL.md index 90973514a..1aca2b893 100644 --- a/docs/README-NL.md +++ b/docs/README-NL.md @@ -24,11 +24,11 @@ RustDesk verwelkomt bijdragen van iedereen. Zie [`docs/CONTRIBUTING.md`](CONTRIB Onderstaande servers zijn de servers die je gratis kunt gebruiken, ze kunnen op termijn veranderen. Als je niet fysiek dichtbij een van deze servers bent, kan je verbinding traag werken. | Locatie | Aanbieder | Specificaties | | --------- | ------------- | ------------------ | -| Seoul | AWS lightsail | 1 VCPU / 0.5GB RAM | -| Singapore | Vultr | 1 VCPU / 1GB RAM | -| Dallas | Vultr | 1 VCPU / 1GB RAM | -| Germany | Hetzner | 2 VCPU / 4GB RAM | -| Germany | Codext | 4 VCPU / 8GB RAM | +| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM | +| Germany | Hetzner | 2 vCPU / 4GB RAM | +| Germany | Codext | 4 vCPU / 8GB RAM | +| Finland (Helsinki) | 0x101 Cyber Security | 4 vCPU / 8GB RAM | +| USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8GB RAM | ## Afhankelijkheden diff --git a/docs/README-PL.md b/docs/README-PL.md index 6888f01d3..85c5f4a61 100644 --- a/docs/README-PL.md +++ b/docs/README-PL.md @@ -24,11 +24,11 @@ RustDesk zaprasza do współpracy każdego. Zobacz [`docs/CONTRIBUTING.md`](CONT Poniżej znajdują się serwery, z których można korzystać za darmo, może się to zmienić z upływem czasu. Jeśli nie znajdujesz się w pobliżu jednego z nich, Twoja prędkość połączenia może być niska. | Lokalizacja | Dostawca | Specyfikacja | | --------- | ------------- | ------------------ | -| Seul | AWS lightsail | 1 VCPU / 0.5GB RAM | -| Singapur | Vultr | 1 VCPU / 1GB RAM | -| Dallas | Vultr | 1 VCPU / 1GB RAM | -| Germany | Hetzner | 2 VCPU / 4GB RAM | -| Germany | Codext | 4 VCPU / 8GB RAM | +| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM | +| Germany | Hetzner | 2 vCPU / 4GB RAM | +| Germany | Codext | 4 vCPU / 8GB RAM | +| Finland (Helsinki) | 0x101 Cyber Security | 4 vCPU / 8GB RAM | +| USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8GB RAM | ## Zależności diff --git a/docs/README-PTBR.md b/docs/README-PTBR.md index 33e529e99..f9d5e0fc3 100644 --- a/docs/README-PTBR.md +++ b/docs/README-PTBR.md @@ -25,11 +25,11 @@ Abaixo estão os servidores que você está utilizando de graça, ele pode mudar | Localização | Fornecedor | Especificações | | ----------- | ------------- | ------------------ | -| Seul | AWS lightsail | 1 VCPU / 0.5GB RAM | -| Singapura | Vultr | 1 VCPU / 1GB RAM | -| Dallas | Vultr | 1 VCPU / 1GB RAM | -| Germany | Hetzner | 2 VCPU / 4GB RAM | -| Germany | Codext | 4 VCPU / 8GB RAM | +| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM | +| Germany | Hetzner | 2 vCPU / 4GB RAM | +| Germany | Codext | 4 vCPU / 8GB RAM | +| Finland (Helsinki) | 0x101 Cyber Security | 4 vCPU / 8GB RAM | +| USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8GB RAM | ## Dependências diff --git a/docs/README-RU.md b/docs/README-RU.md index 26215cab7..242341a6b 100644 --- a/docs/README-RU.md +++ b/docs/README-RU.md @@ -30,11 +30,13 @@ RustDesk приветствует вклад каждого. Ознакомьт Ниже приведены бесплатные публичные сервера, используемые по умолчанию. Имейте ввиду, они могут меняться со временем. Также стоит отметить, что скорость работы сети зависит от вашего местоположения и расстояния до серверов. Подключение происходит к ближайшему доступному. | Расположение | Поставщик | Технические характеристики | | --------- | ------------- | ------------------ | -| Сеул | AWS lightsail | 1 VCPU / 0.5GB RAM | -| Сингапур | Vultr | 1 VCPU / 1GB RAM | -| Даллас | Vultr | 1 VCPU / 1GB RAM | -| Германия | Hetzner | 2 VCPU / 4GB RAM | -| Германия | Codext | 4 VCPU / 8GB RAM | +| Сеул | AWS lightsail | 1 vCPU / 0.5GB RAM | +| Сингапур | Vultr | 1 vCPU / 1GB RAM | +| Даллас | Vultr | 1 vCPU / 1GB RAM | +| Германия | Hetzner | 2 vCPU / 4GB RAM | +| Германия | Codext | 4 vCPU / 8GB RAM | +| Финляндия (Хельсинки) | 0x101 Cyber Security | 4 vCPU / 8GB RAM | +| США (Эшберн) | 0x101 Cyber Security | 4 vCPU / 8GB RAM | ## Зависимости diff --git a/docs/README-UA.md b/docs/README-UA.md index 3f20773f3..3615b9064 100644 --- a/docs/README-UA.md +++ b/docs/README-UA.md @@ -30,11 +30,13 @@ RustDesk вітає внесок кожного. Дивіться [`docs/CONTRIB Нижче наведені сервери, для безкоштовного використання, вони можуть змінюватися з часом. Якщо ви не перебуваєте поруч з одним із них, ваша мережа може працювати повільно. | Місцезнаходження | Постачальник | Технічні характеристики | | --------- | ------------- | ------------------ | -| Сеул | AWS lightsail | 1 VCPU / 0.5GB RAM | -| Сінгапур | Vultr | 1 VCPU / 1GB RAM | -| Даллас | Vultr | 1 VCPU / 1GB RAM -Німеччина | Hetzner | 2 VCPU / 4GB RAM | 2 VCPU / 4GB RAM | Німеччина | Hetzner | 2 VCPU / 4GB RAM | -| Germany | Codext | 4 VCPU / 8GB RAM | +| Сеул | AWS lightsail | 1 vCPU / 0.5GB RAM | +| Сінгапур | Vultr | 1 vCPU / 1GB RAM | +| Даллас | Vultr | 1 vCPU / 1GB RAM +Німеччина | Hetzner | 2 vCPU / 4GB RAM | 2 VCPU / 4GB RAM | Німеччина | Hetzner | 2 VCPU / 4GB RAM | +| Germany | Codext | 4 vCPU / 8GB RAM | +| Finland (Helsinki) | 0x101 Cyber Security | 4 vCPU / 8GB RAM | +| USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8GB RAM | ## Залежності diff --git a/docs/README-VN.md b/docs/README-VN.md index 25a166f24..295f54c6b 100644 --- a/docs/README-VN.md +++ b/docs/README-VN.md @@ -33,11 +33,11 @@ Dưới đây là những máy chủ mà bạn có thể sử dụng mà không | Địa điểm | Nhà cung cấp | Cấu hình | | --------- | ------------- | ------------------ | -| Seoul | AWS lightsail | 1 VCPU / 0.5GB RAM | -| Singapore | Vultr | 1 VCPU / 1GB RAM | -| Dallas | Vultr | 1 VCPU / 1GB RAM | -| Germany | Hetzner | 2 VCPU / 4GB RAM | -| Germany | Codext | 4 VCPU / 8GB RAM | +| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM | +| Germany | Hetzner | 2 vCPU / 4GB RAM | +| Germany | Codext | 4 vCPU / 8GB RAM | +| Finland (Helsinki) | 0x101 Cyber Security | 4 vCPU / 8GB RAM | +| USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8GB RAM | ## Dependencies diff --git a/docs/README-ZH.md b/docs/README-ZH.md index 5c0bd1d46..7ec87ec50 100644 --- a/docs/README-ZH.md +++ b/docs/README-ZH.md @@ -24,11 +24,13 @@ Chat with us: [知乎](https://www.zhihu.com/people/rustdesk) | [Discord](https: 以下是您可以使用的、免费的、会随时更新的公共服务器列表,在国内也许网速会很慢或者无法访问。 -- 首尔, AWS lightsail, 1 VCPU/0.5G RAM -- 新加坡, Vultr, 1 VCPU/1G RAM -- 达拉斯, Vultr, 1 VCPU/1G RAM -- 德国, Codext, 2 VCPU / 4GB RAM -- 德国, Hetzner, 4 VCPU / 8GB RAM +| Location | Vendor | Specification | +| --------- | ------------- | ------------------ | +| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM | +| Germany | Hetzner | 2 vCPU / 4GB RAM | +| Germany | Codext | 4 vCPU / 8GB RAM | +| Finland (Helsinki) | 0x101 Cyber Security | 4 vCPU / 8GB RAM | +| USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8GB RAM | ## 依赖 From c0cf6c31738e15f3f85c71f341f7033d2646a9b0 Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 30 Nov 2022 22:56:22 +0800 Subject: [PATCH 1081/2015] fix image blurry Signed-off-by: fufesou --- flutter/lib/desktop/pages/remote_page.dart | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index bac80e7ac..7b3e0fe82 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -413,8 +413,12 @@ class _ImagePaintState extends State { ); } else { widget = Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [widget], + children: [ + Container( + width: ((layoutSize.width - size.width) ~/ 2).toDouble(), + ), + widget, + ], ); } if (layoutSize.height < size.height) { @@ -430,8 +434,12 @@ class _ImagePaintState extends State { ); } else { widget = Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [widget], + children: [ + Container( + height: ((layoutSize.height - size.height) ~/ 2).toDouble(), + ), + widget, + ], ); } if (layoutSize.width < size.width) { @@ -576,7 +584,8 @@ class ImagePainter extends CustomPainter { paint.filterQuality = FilterQuality.high; } } - canvas.drawImage(image!, Offset(x, y), paint); + canvas.drawImage( + image!, Offset(x.toInt().toDouble(), y.toInt().toDouble()), paint); } @override From c6ad1b94b174910f1901a878826f1b26e095b374 Mon Sep 17 00:00:00 2001 From: Don Date: Wed, 30 Nov 2022 17:37:52 +0100 Subject: [PATCH 1082/2015] Added Albanian (Shqip) Language, soon all the phrases to be translated will be added) --- src/lang.rs | 3 +++ src/lang/sq.rs | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 src/lang/sq.rs diff --git a/src/lang.rs b/src/lang.rs index 6254e988a..cec9801c2 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -27,6 +27,7 @@ mod fa; mod ca; mod gr; mod sv; +mod sq; lazy_static::lazy_static! { pub static ref LANGS: Value = @@ -57,6 +58,7 @@ lazy_static::lazy_static! { ("ca", "Català"), ("gr", "Ελληνικά"), ("sv", "Svenska"), + ("sq", "Shqip"), ]); } @@ -111,6 +113,7 @@ pub fn translate_locale(name: String, locale: &str) -> String { "ca" => ca::T.deref(), "gr" => gr::T.deref(), "sv" => sv::T.deref(), + "sq" => sq::T.deref(), _ => en::T.deref(), }; if let Some(v) = m.get(&name as &str) { diff --git a/src/lang/sq.rs b/src/lang/sq.rs new file mode 100644 index 000000000..e9bac2449 --- /dev/null +++ b/src/lang/sq.rs @@ -0,0 +1,39 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("desk_tip", "Desktopi juaj mund të aksesohet me këtë ID dhe fjalëkalim."), + ("connecting_status", "Duke u lidhur me rrjetin RustDesk.") + ("not_ready_status", "Jo gati.Ju lutem kontolloni lidhjen tuaj."), + ("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."), + ("install_tip", "Për shkak të UAC, Rustdesk nuk mund të punoj sic duhet si nje remote në distancë në disa raste. Për të shamngur UAC, ju lutem klikoni butonin më poshtë për të instaluar RustDesk në sistem."), + ("config_acc", "Për të kontrolluar Desktopin tuaj nga distanca, duhet të jepni leje RustDesk \"Aksesueshmëri\"."), + ("config_screen", "Për të aksesuar Desktopin tuaj nga distanca, duhet ti jepni lejet RustDesk \"Regjistrimin e ekranit\"."), + ("agreement_tip", "Duke filluar instalimin, ju pranoni marrëveshjen e licencës"), + ("not_close_tcp_tip", "Mos e mbyll këtë dritare ndërsa jeni duke përdorur tunelin"), + ("setup_server_tip", "Për lidhje më të shpejtë, ju lutemi konfiguroni serverin tuaj"), + ("Auto Login", "Hyrja automatike (e vlefshme vetëm nëse vendosni \"Kyçja pas përfundimit të sesionit\")."), + ("whitelist_tip", "Vetëm IP e listës së bardhë mund të më aksesoj."), + ("whitelist_sep", "Të ndara me presje, pikëpresje, hapësira ose rresht të ri"), + ("wrong credentials", "Emri i përdoruesit ose fjalëkalimi i gabuar"), + ("invalid_http", "Duhet të fillojë me http:// ose https://"), + ("install_daemon_tip", "Për të nisur në boot, duhet të instaloni shërbimin e sistemit"), + ("android_input_permission_tip1", "Në mënyrë që një pajisje në distancë të kontrollojë pajisjen tuaj Android nëpërmjet mausit ose prekjes, duhet të lejoni RustDesk të përdorë shërbimin."), + ("android_input_permission_tip2", "Ju lutemi shkoni në faqen tjetër të cilësimeve të sistemit, gjeni dhe shtypni [Shërbimet e Instaluara], aktivizoni shërbimin [RustDesk Input]"), + ("android_new_connection_tip", "Është marrë një kërkesë e re kontrolli, e cila dëshiron të kontrollojë pajisjen tuaj aktuale."), + ("android_service_will_start_tip", "Aktivizimi i \"Regjistrimi i ekranit\" do të nisë automatikisht shërbimin, duke lejuar pajisjet e tjera të kërkojnë një lidhje me pajisjen tuaj."), + ("android_stop_service_tip", "Mbyllja e shërbimit do të mbyllë automatikisht të gjitha lidhjet e vendosura."), + ("android_version_audio_tip", "Versioni aktual i Android nuk mbështet regjistrimin e audios, ju lutemi përmirësoni në Android 10 ose më të lartë."), + ("android_start_service_tip", "Shtyp [Fillo Shërbimin] ose HAP lejen e [Kapjen e Ekranit] për të nisur shërbimin e ndarjes së ekranit."), + ("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"), + ("doc_fix_wayland", "https://rustdesk.com/docs/en/manual/linux/#x11-required"), + ("server_not_support", "Nuk suportohet akoma nga severi"), + ("android_open_battery_optimizations_tip", "Nëse dëshironi ta çaktivizoni këtë veçori, ju lutemi shkoni te faqja tjetër e cilësimeve të aplikacionit RustDesk, gjeni dhe shtypni [Batteri], hiqni zgjedhjen [Te pakufizuara]"), + ("remote_restarting_tip", "Pajisja në distancë po riniset, ju lutemi mbyllni këtë kuti mesazhi dhe lidheni përsëri me fjalëkalim të përhershëm pas një kohe"), + ("Are you sure to close the connection?", "A jeni i sigurt për të mbyllur lidhjen?"), + ("elevation_prompt", "Drejtimi i softuerit pa ngritjen e privilegjeve mund të shkaktojë probleme kur përdoruesit në distancë përdorin dritare të caktuara."), + ("uac_warning", "Qasja e refuzuar përkohësisht për shkak të kërkesës për lartësi, ju lutemi prisni që përdoruesi në distancë të pranojë dialogun UAC. Për të shmangur këtë problem, rekomandohet instalimi i softuerit në pajisjen në distancë ose ekzekutimi i tij me privilegje administratori."), + ("elevated_foreground_window_warning", "Përkohësisht është e pamundur për të përdorur mausin dhe tastierën, për shkak se dritarja aktuale e desktopit në distancë kërkon privilegj më të lartë për të vepruar,ju mund t'i kërkoni përdoruesit në distancë të minimizojë dritaren aktuale. Për të shmangur këtë problem, rekomandohet të instaloni softuerin në pajisjen në distancë ose ekzekutoni atë me privilegje administratori."), + ("JumpLink", "Shiko"), + ("Stop service", "Ndalo shërbimin"), + ].iter().cloned().collect(); +} From ac33924a9c271a625e9ff2b1b982b7cb79727f87 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 1 Dec 2022 11:19:51 +0800 Subject: [PATCH 1083/2015] remove flutter tray --- .../lib/desktop/pages/desktop_home_page.dart | 41 +------------------ flutter/pubspec.lock | 29 ++----------- flutter/pubspec.yaml | 4 -- 3 files changed, 4 insertions(+), 70 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 53f4d4d90..058710aaf 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -14,11 +14,8 @@ import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/server_model.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; -import 'package:flutter_hbb/utils/tray_manager.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; -import 'package:tray_manager/tray_manager.dart'; -import 'package:window_manager/window_manager.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:window_size/window_size.dart' as window_size; @@ -34,7 +31,7 @@ class DesktopHomePage extends StatefulWidget { const borderColor = Color(0xFF2F65BA); class _DesktopHomePageState extends State - with TrayListener, AutomaticKeepAliveClientMixin { + with AutomaticKeepAliveClientMixin { final _leftPaneScrollController = ScrollController(); @override @@ -428,35 +425,6 @@ class _DesktopHomePageState extends State ); } - @override - void onTrayIconMouseDown() { - windowManager.show(); - } - - @override - void onTrayIconRightMouseDown() { - // linux does not support popup menu manually. - // linux will handle popup action ifself. - if (Platform.isMacOS || Platform.isWindows) { - trayManager.popUpContextMenu(); - } - } - - @override - void onTrayMenuItemClick(MenuItem menuItem) { - switch (menuItem.key) { - case kTrayItemQuitKey: - windowManager.close(); - break; - case kTrayItemShowKey: - windowManager.show(); - windowManager.focus(); - break; - default: - break; - } - } - @override void initState() { super.initState(); @@ -492,9 +460,6 @@ class _DesktopHomePageState extends State } }); Get.put(svcStopped, tag: 'stop-service'); - // disable this tray because we use tray function provided by rust now - // initTray(); - trayManager.addListener(this); rustDeskWinManager.registerActiveWindowListener(onActiveWindowChanged); rustDeskWinManager.setMethodHandler((call, fromWindowId) async { @@ -536,10 +501,6 @@ class _DesktopHomePageState extends State @override void dispose() { - // destoryTray(); - // fix: disable unregister to prevent from receiving events from other windows - // rustDeskWinManager.unregisterActiveWindowListener(onActiveWindowChanged); - trayManager.removeListener(this); _uniLinksSubscription?.cancel(); Get.delete(tag: 'stop-service'); _updateTimer?.cancel(); diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 7440b5b9e..9803f065c 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -383,8 +383,8 @@ packages: dependency: "direct main" description: path: "." - ref: bfb19c84a8244771488bc05cc5f9c9b5e0324cfd - resolved-ref: bfb19c84a8244771488bc05cc5f9c9b5e0324cfd + ref: "74b1b314142b6775c1243067a3503ac568ebc74b" + resolved-ref: "74b1b314142b6775c1243067a3503ac568ebc74b" url: "https://github.com/Kingtous/rustdesk_flutter_custom_cursor" source: git version: "0.0.1" @@ -617,13 +617,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.1.4" - menu_base: - dependency: transitive - description: - name: menu_base - url: "https://pub.dartlang.org" - source: hosted - version: "0.1.1" meta: dependency: transitive description: @@ -892,13 +885,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.3" - shortid: - dependency: transitive - description: - name: shortid - url: "https://pub.dartlang.org" - source: hosted - version: "0.1.2" sky_engine: dependency: transitive description: flutter @@ -988,15 +974,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.4.0" - tray_manager: - dependency: "direct main" - description: - path: "." - ref: "3aa37c86e47ea748e7b5507cbe59f2c54ebdb23a" - resolved-ref: "3aa37c86e47ea748e7b5507cbe59f2c54ebdb23a" - url: "https://github.com/Kingtous/rustdesk_tray_manager" - source: git - version: "0.1.8" tuple: dependency: "direct main" description: @@ -1210,7 +1187,7 @@ packages: source: hosted version: "2.2.0" win32: - dependency: transitive + dependency: "direct main" description: name: win32 url: "https://pub.dartlang.org" diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 8de0be4d6..3d34c30bc 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -65,10 +65,6 @@ dependencies: url: https://github.com/Kingtous/rustdesk_desktop_multi_window ref: cb086219bd4760a95a483cb14c1791d2a39ca5a0 freezed_annotation: ^2.0.3 - tray_manager: - git: - url: https://github.com/Kingtous/rustdesk_tray_manager - ref: 3aa37c86e47ea748e7b5507cbe59f2c54ebdb23a flutter_custom_cursor: git: url: https://github.com/Kingtous/rustdesk_flutter_custom_cursor From 7c09e6690206753632317ac518caecf7c826c4ac Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 1 Dec 2022 13:52:12 +0800 Subject: [PATCH 1084/2015] fix connect on subwindow, notify main window Signed-off-by: fufesou --- flutter/lib/common.dart | 33 +++++++++++++++---- flutter/lib/consts.dart | 3 ++ .../lib/desktop/pages/desktop_home_page.dart | 11 +++++-- .../lib/desktop/widgets/remote_menubar.dart | 4 ++- .../lib/desktop/widgets/tabbar_widget.dart | 4 +-- 5 files changed, 44 insertions(+), 11 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 63ab39df2..eac7fbf9b 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1290,11 +1290,24 @@ bool callUniLinksUriHandler(Uri uri) { return false; } +connectMainDesktop(String id, + {required bool isFileTransfer, + required bool isTcpTunneling, + required bool isRDP}) async { + if (isFileTransfer) { + await rustDeskWinManager.newFileTransfer(id); + } else if (isTcpTunneling || isRDP) { + await rustDeskWinManager.newPortForward(id, isRDP); + } else { + await rustDeskWinManager.newRemoteDesktop(id); + } +} + /// Connect to a peer with [id]. /// If [isFileTransfer], starts a session only for file transfer. /// If [isTcpTunneling], starts a session only for tcp tunneling. /// If [isRDP], starts a session only for rdp. -void connect(BuildContext context, String id, +connect(BuildContext context, String id, {bool isFileTransfer = false, bool isTcpTunneling = false, bool isRDP = false}) async { @@ -1304,12 +1317,20 @@ void connect(BuildContext context, String id, "more than one connect type"); if (isDesktop) { - if (isFileTransfer) { - await rustDeskWinManager.newFileTransfer(id); - } else if (isTcpTunneling || isRDP) { - await rustDeskWinManager.newPortForward(id, isRDP); + if (desktopType == DesktopType.main) { + await connectMainDesktop( + id, + isFileTransfer: isFileTransfer, + isTcpTunneling: isTcpTunneling, + isRDP: isRDP, + ); } else { - await rustDeskWinManager.newRemoteDesktop(id); + await rustDeskWinManager.call(WindowType.Main, kWindowConnect, { + 'id': id, + 'isFileTransfer': isFileTransfer, + 'isTcpTunneling': isTcpTunneling, + 'isRDP': isRDP, + }); } } else { if (isFileTransfer) { diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index b0099ca7c..50e7f594b 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -11,9 +11,12 @@ const String kAppTypeDesktopRemote = "remote"; const String kAppTypeDesktopFileTransfer = "file transfer"; const String kAppTypeDesktopPortForward = "port forward"; +const String kWindowMainWindowOnTop = "main_window_on_top"; +const String kWindowGetWindowInfo = "get_window_info"; const String kWindowActionRebuild = "rebuild"; const String kWindowEventHide = "hide"; const String kWindowEventShow = "show"; +const String kWindowConnect = "connect"; const String kUniLinksPrefix = "rustdesk://"; const String kActionNewConnection = "connection/new/"; diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 53f4d4d90..f387c450f 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -500,9 +500,9 @@ class _DesktopHomePageState extends State rustDeskWinManager.setMethodHandler((call, fromWindowId) async { debugPrint( "[Main] call ${call.method} with args ${call.arguments} from window $fromWindowId"); - if (call.method == "main_window_on_top") { + if (call.method == kWindowMainWindowOnTop) { window_on_top(null); - } else if (call.method == "get_window_info") { + } else if (call.method == kWindowGetWindowInfo) { final screen = (await window_size.getWindowInfo()).screen; if (screen == null) { return ""; @@ -529,6 +529,13 @@ class _DesktopHomePageState extends State rustDeskWinManager.registerActiveWindow(call.arguments["id"]); } else if (call.method == kWindowEventHide) { rustDeskWinManager.unregisterActiveWindow(call.arguments["id"]); + } else if (call.method == kWindowConnect) { + await connectMainDesktop( + call.arguments['id'], + isFileTransfer: call.arguments['isFileTransfer'], + isTcpTunneling: call.arguments['isTcpTunneling'], + isRDP: call.arguments['isRDP'], + ); } }); _uniLinksSubscription = listenUniLinks(); diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index d40a21e5d..250910396 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -8,6 +8,7 @@ import 'package:flutter/services.dart'; 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:get/get.dart'; import 'package:provider/provider.dart'; import 'package:rxdart/rxdart.dart' as rxdart; @@ -201,7 +202,8 @@ class _RemoteMenubarState extends State { } _updateScreen() async { - final v = await DesktopMultiWindow.invokeMethod(0, 'get_window_info', ''); + final v = await rustDeskWinManager.call( + WindowType.Main, kWindowGetWindowInfo, ''); final String valueStr = v; if (valueStr.isEmpty) { _screen = null; diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index daf9272f7..436011cb5 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -1033,8 +1033,8 @@ class AddButton extends StatelessWidget { return ActionIcon( message: 'New Connection', icon: IconFont.add, - onTap: () => - rustDeskWinManager.call(WindowType.Main, "main_window_on_top", ""), + onTap: () => rustDeskWinManager.call( + WindowType.Main, kWindowMainWindowOnTop, ""), isClose: false); } } From 60d0b9209b3daf31c14cf6ffb6a9437699f6b04c Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 1 Dec 2022 14:11:09 +0800 Subject: [PATCH 1085/2015] fix sq.rs --- src/lang/sq.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/sq.rs b/src/lang/sq.rs index e9bac2449..626606dd2 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -2,7 +2,7 @@ lazy_static::lazy_static! { pub static ref T: std::collections::HashMap<&'static str, &'static str> = [ ("desk_tip", "Desktopi juaj mund të aksesohet me këtë ID dhe fjalëkalim."), - ("connecting_status", "Duke u lidhur me rrjetin RustDesk.") + ("connecting_status", "Duke u lidhur me rrjetin RustDesk."), ("not_ready_status", "Jo gati.Ju lutem kontolloni lidhjen tuaj."), ("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."), ("install_tip", "Për shkak të UAC, Rustdesk nuk mund të punoj sic duhet si nje remote në distancë në disa raste. Për të shamngur UAC, ju lutem klikoni butonin më poshtë për të instaluar RustDesk në sistem."), From 387a7f2df4471c7c8dd77c1bf2d4883ef4be5623 Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 1 Dec 2022 21:48:19 +0800 Subject: [PATCH 1086/2015] ios get data dir Signed-off-by: fufesou --- Cargo.toml | 2 +- README.md | 2 +- build.py | 2 +- flutter/lib/models/native_model.dart | 14 +++++++++--- flutter/lib/utils/tray_manager.dart | 32 ---------------------------- libs/hbb_common/src/config.rs | 4 ++++ src/flutter_ffi.rs | 18 ++++++++++++++-- 7 files changed, 34 insertions(+), 40 deletions(-) delete mode 100644 flutter/lib/utils/tray_manager.dart diff --git a/Cargo.toml b/Cargo.toml index fd84b73aa..2861b3f63 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -151,7 +151,7 @@ hound = "3.5" name = "RustDesk" identifier = "com.carriez.rustdesk" icon = ["res/32x32.png", "res/128x128.png", "res/128x128@2x.png"] -deb_depends = ["libgtk-3-0", "libxcb-randr0", "libxdo3", "libxfixes3", "libxcb-shape0", "libxcb-xfixes0", "libasound2", "libsystemd0", "curl", "libappindicator3-1", "libvdpau1", "libva2"] +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"] diff --git a/README.md b/README.md index 2fd744429..ad19edaa1 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ Please download sciter dynamic library yourself. ```sh sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \ libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \ - libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev + libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev ``` ### openSUSE Tumbleweed diff --git a/build.py b/build.py index 42438909f..fc62f8ecb 100755 --- a/build.py +++ b/build.py @@ -217,7 +217,7 @@ Version: %s Architecture: amd64 Maintainer: open-trade Homepage: https://rustdesk.com -Depends: libgtk-3-0, libxcb-randr0, libxdo3, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2, libsystemd0, curl, libappindicator3-1, libva-drm2, libva-x11-2, libvdpau1, libgstreamer-plugins-base1.0-0 +Depends: libgtk-3-0, libxcb-randr0, libxdo3, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2, libsystemd0, curl, libva-drm2, libva-x11-2, libvdpau1, libgstreamer-plugins-base1.0-0 Description: A remote control software. """ % version diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index 68b85968a..0a833583e 100644 --- a/flutter/lib/models/native_model.dart +++ b/flutter/lib/models/native_model.dart @@ -29,6 +29,7 @@ typedef HandleEvent = Future Function(Map evt); /// Hides the platform differences. class PlatformFFI { String _dir = ''; + // _homeDir is only needed for Android and IOS. String _homeDir = ''; F2? _translate; final _eventHandlers = >{}; @@ -119,8 +120,10 @@ class PlatformFFI { if (isAndroid) { // only support for android _homeDir = (await ExternalPath.getExternalStorageDirectories())[0]; + } else if (isIOS) { + _homeDir = _ffiBind.mainGetDataDirIos(); } else { - _homeDir = (await getDownloadsDirectory())?.path ?? ''; + // no need to set home dir } } catch (e) { debugPrint('initialize failed: $e'); @@ -159,8 +162,13 @@ class PlatformFFI { name = macOsInfo.computerName; id = macOsInfo.systemGUID ?? ''; } - debugPrint( - '_appType:$_appType,info1-id:$id,info2-name:$name,dir:$_dir,homeDir:$_homeDir'); + if (isAndroid || isIOS) { + debugPrint( + '_appType:$_appType,info1-id:$id,info2-name:$name,dir:$_dir,homeDir:$_homeDir'); + } else { + debugPrint( + '_appType:$_appType,info1-id:$id,info2-name:$name,dir:$_dir'); + } await _ffiBind.mainDeviceId(id: id); await _ffiBind.mainDeviceName(name: name); await _ffiBind.mainSetHomeDir(home: _homeDir); diff --git a/flutter/lib/utils/tray_manager.dart b/flutter/lib/utils/tray_manager.dart deleted file mode 100644 index 91550e1d8..000000000 --- a/flutter/lib/utils/tray_manager.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'dart:io'; - -import 'package:tray_manager/tray_manager.dart'; - -import '../common.dart'; - -const kTrayItemShowKey = "show"; -const kTrayItemQuitKey = "quit"; - -Future initTray({List? extra_item}) async { - List items = [ - MenuItem(key: kTrayItemShowKey, label: translate("Show RustDesk")), - MenuItem.separator(), - MenuItem(key: kTrayItemQuitKey, label: translate("Quit")), - ]; - if (extra_item != null) { - items.insertAll(0, extra_item); - } - if (Platform.isMacOS || Platform.isWindows) { - await trayManager.setToolTip("rustdesk"); - } - if (Platform.isMacOS || Platform.isLinux) { - await trayManager.setTitle("rustdesk"); - } - await trayManager - .setIcon(Platform.isWindows ? "assets/logo.ico" : "assets/logo.png"); - await trayManager.setContextMenu(Menu(items: items)); -} - -Future destoryTray() async { - return trayManager.destroy(); -} diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index f476816ef..328a1ea59 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -57,6 +57,10 @@ lazy_static::lazy_static! { lazy_static::lazy_static! { pub static ref APP_DIR: Arc> = Default::default(); +} + +#[cfg(any(target_os = "android", target_os = "ios"))] +lazy_static::lazy_static! { pub static ref APP_HOME_DIR: Arc> = Default::default(); } diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 0f3fb5ef6..094a2faa7 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -967,8 +967,22 @@ pub fn session_change_prefer_codec(id: String) { } } -pub fn main_set_home_dir(home: String) { - *config::APP_HOME_DIR.write().unwrap() = home; +pub fn main_set_home_dir(_home: String) { + #[cfg(any(target_os = "android", target_os = "ios"))] + { + *config::APP_HOME_DIR.write().unwrap() = _home; + } +} + +// This is a temporary method to get data dir for ios +pub fn main_get_data_dir_ios() -> SyncReturn { + let data_dir = config::Config::path("data"); + if !data_dir.exists() { + if let Err(e) = std::fs::create_dir_all(&data_dir) { + log::warn!("Failed to create data dir {}", e); + } + } + SyncReturn(data_dir.to_string_lossy().to_string()) } pub fn main_stop_service() { From e166d5ef6c4a5901f75d9e8d8682e116b879c804 Mon Sep 17 00:00:00 2001 From: Don Date: Thu, 1 Dec 2022 15:28:41 +0100 Subject: [PATCH 1087/2015] Translated all of the keywords/phrases in lang/template.rs to Albanian language --- src/lang/sq.rs | 388 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 375 insertions(+), 13 deletions(-) diff --git a/src/lang/sq.rs b/src/lang/sq.rs index e9bac2449..65ba2f518 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -1,22 +1,276 @@ lazy_static::lazy_static! { pub static ref T: std::collections::HashMap<&'static str, &'static str> = [ + ("Status", "Statusi"), + ("Your Desktop", "Desktopi juaj"), ("desk_tip", "Desktopi juaj mund të aksesohet me këtë ID dhe fjalëkalim."), - ("connecting_status", "Duke u lidhur me rrjetin RustDesk.") + ("Password", "fjalëkalimi"), + ("Ready", "Gati"), + ("Established", "I themeluar"), + ("connecting_status", "statusi_i_lidhjes"), + ("Enable Service", "Aktivizo Shërbimin"), + ("Start Service", "Nis Shërbimin"), + ("Service is running", "Shërbimi është duke funksionuar"), + ("Service is not running", "Shërbimi nuk është duke funksionuar"), ("not_ready_status", "Jo gati.Ju lutem kontolloni lidhjen tuaj."), + ("Control Remote Desktop", "Kontrolli i desktopit në distancë"), + ("Transfer File", "Transfero dosje"), + ("Connect", "Lidh"), + ("Recent Sessions", "Sessioni i fundit"), + ("Address Book", "Libër adresash"), + ("Confirmation", "Konfirmimi"), + ("TCP Tunneling", "TCP tunel"), + ("Remove", "Hiqni"), + ("Refresh random password", "Rifreskoni fjalëkalimin e rastësishëm"), + ("Set your own password", "Vendosni fjalëkalimin tuaj"), + ("Enable Keyboard/Mouse","Aktivizoni Tastierën/Mousin"), + ("Enable Clipboard", "Aktivizo"), + ("Enable File Transfer", "Aktivizoni transferimin e skedarëve"), + ("Enable TCP Tunneling", "Aktivizoni TCP Tunneling"), + ("IP whitelisting", "Lista e bardhë IP"), + ("ID/Relay Server", "ID/server rele"), + ("Import Server Config", "Konfigurimi i severit të importit"), + ("Export Server Config", "Konfigurimi i severit të eksportit"), + ("Import server configuration successfully", "Konfigurimi i severit të importit i suksesshëm"), + ("Export server configuration successfully", "Konfigurimi i severit të eksprotit i suksesshëm"), + ("Invalid server configuration", "Konfigurim i pavlefshëm i serverit"), + ("Clipboard is empty","Clipboard është bosh"), + ("Stop service", "Ndaloni shërbimin"), + ("Change ID", "Ndryshoni ID"), + ("Website", "Faqe ëebi"), + ("About", "Rreth"), + ("Mute", "Pa zë"), + ("Audio Input", "Inputi zërit"), + ("Enhancements", "Përmirësimet"), + ("Hardware Codec", "Kodeku Harduerik"), + ("Adaptive Bitrate", "Shpejtësia adaptive e biteve"), + ("ID Server", "ID e serverit"), + ("Relay Server", "Serveri rele"), + ("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"), + ("Too frequent", "Shumë i përdorur"), + ("Cancel", "Anullo"), + ("Skip", "Kalo"), + ("Close", "Mbyll"), + ("Retry", "Riprovo"), + ("OK", "OK"), + ("Password Required", "Fjalëkalimi i detyrueshëm"), + ("Please enter your password", "Ju lutem vendosni fjalëkalimin tuaj"), + ("Remember password", "Mbani mend fjalëkalimin"), + ("Wrong Password", "Fjalëkalim i gabuar"), + ("Do you want to enter again?", "Dëshironi të vendosni përsëri"), + ("Connection Error", "Gabim në lidhje"), + ("Error", "Gabim"), + ("Reset by the peer", "Riseto nga peer"), + ("Connecting...", "Duke u lidhur"), + ("Connection in progress. Please wait.", "Lidhja në progres. Ju lutem prisni"), + ("Please try 1 minute later", "Ju lutemi provoni 1 minut më vonë"), + ("Login Error", "Gabim në login"), + ("Successful", "E suksesshme"), + ("Connected, waiting for image...", "E lidhur , prisni për imazhin..."), + ("Name", "Emri"), + ("Type", "Shkruaj"), + ("Modified", "E modifikuar"), + ("Size", "Madhesia" + ("Show Hidden Files", "Shfaq skedarët e fshehur"), + ("Receive", "Merr"), + ("Send", "Dërgo"), + ("Refresh File", "Rifreskoni skedarët"), + ("Local", "Lokal"), + ("Remote", "Në distancë"), + ("Remote Computer", "Kompjuter në distancë"), + ("Local Computer", "Kompjuter Lokal"), + ("Confirm Delete", "Konfirmoni fshirjen"), + ("Delete", "Fshij"), + ("Properties", "Karakteristikat"), + ("Multi Select", "Shumë përzgjedhje"), + ("Select All", "Selektoni të gjitha"), + ("Unselect All", "Ç'selektoni të gjitha"), + ("Empty Directory", "Direktori boshe"), + ("Not an empty directory", "Jo një direktori boshe"), + ("Are you sure you want to delete this file?", "Jeni të sigurtë që doni të fshini këtë skedarë"), + ("Are you sure you want to delete this empty directory?", "Jeni të sigurtë që dëshironi të fshini këtë direktori boshe"), + ("Are you sure you want to delete the file of this directory?", "Jeni të sigurtë që dëshironi te fshini skedarin e kësaj direktorie"), + ("Do this for all conflicts", "Bëjeni këtë për të gjitha konfliktet"), + ("This is irreversible!", "Kjo është e pakthyeshme"), + ("Deleting", "Duke i fshirë"), + ("files", "Skedarë"), + ("Waiting", "Në pritje"), + ("Finished", "Përfunduar"), + ("Speed", "Shpejtësia"), + ("Custom Image Quality", "Cilësi e personalizuar imazhi"), + ("Privacy mode", "Modaliteti i Privatësisë"), + ("Block user input", "Blloko inputin e përdorusesit"), + ("Unblock user input", "Zhblloko inputin e përdorusesit"), + ("Adjust Window", "Rregulloni dritaren"), + ("Original", "Origjinal"), + ("Shrink", "Shkurtim"), + ("Stretch", "Shtrirje"), + ("Scrollbar", "Shiriti i lëvizjes"), + ("ScrollAuto", "Levizje automatikisht"), + ("Good image quality", "Cilësi e mirë imazhi"), + ("Balanced", "E balancuar"), + ("Optimize reaction time", "Optimizo kohën e reagimit"), + ("Custom", "E personalizuar"), + ("Show remote cursor", "Shfaq kursorin në distancë"), + ("Show quality monitor", "Shaq cilësinë e monitorit"), + ("Disable clipboard", "Ç'aktivizo clipboard"), + ("Lock after session end", "Kyç pasi sesioni të përfundoj"), + ("Insert", "Fut"), + ("Insert Lock", "Fut bllokimin"), + ("Refresh", "Rifresko"), + ("ID does not exist", "ID nuk ekziston"), + ("Failed to connect to rendezvous server", "Dështoj të lidhet me serverin e takimit"), + ("Please try later", "Ju lutemi provoni më vonë"), + ("Remote desktop is offline", "Desktopi në distancë nuk është në linjë"), + ("Key mismatch", "Mospërputhje kryesore"), + ("Timeout", "Koha mbaroi"), + ("Failed to connect to relay server", "Lidhja me serverin transmetues dështoi"), + ("Failed to connect via rendezvous server", "Lidhja nëpërmjet serverit të takimit dështoi"), + ("Failed to connect via relay server","Lidhja nëpërmjet serverit të transmetimit dështoi"), + ("Failed to make direct connection to remote desktop", "Lidhja direkte me desktopin në distancë dështoi"), + ("Set Password", "Vendosni fjalëkalimin"), + ("OS Password", "OS fjalëkalim"), ("install_tip", "Për shkak të UAC, Rustdesk nuk mund të punoj sic duhet si nje remote në distancë në disa raste. Për të shamngur UAC, ju lutem klikoni butonin më poshtë për të instaluar RustDesk në sistem."), + ("Click to upgrade", "Klikoni për përmirësim"), + ("Click to download", "Klikoni për tu shkarkuar"), + ("Click to update", "Klikoni për përditësim"), + ("Configure", "Koniguro"), ("config_acc", "Për të kontrolluar Desktopin tuaj nga distanca, duhet të jepni leje RustDesk \"Aksesueshmëri\"."), ("config_screen", "Për të aksesuar Desktopin tuaj nga distanca, duhet ti jepni lejet RustDesk \"Regjistrimin e ekranit\"."), + ("Installing ...", "Duke u instaluar"), + ("Install", "Instalo"), + ("Installation", "Instalimi"), + ("Installation Path", "Rruga instalimit"), + ("Create start menu shortcuts", "Krijoni shortcuts për menunë e fillimit"), + ("Create desktop icon", "Krijoni ikonën e desktopit"), ("agreement_tip", "Duke filluar instalimin, ju pranoni marrëveshjen e licencës"), + ("Accept and Install", "Pranoni dhe instaloni"), + ("End-user license agreement", "Marrëeveshja e licencës së perdoruesit fundor"), + ("Generating ...", "Duke gjeneruar"), + ("Your installation is lower version.", "Instalimi juaj është version i ulët"), ("not_close_tcp_tip", "Mos e mbyll këtë dritare ndërsa jeni duke përdorur tunelin"), + ("Listening ...", "Duke dëgjuar"), + ("Remote Host", "Host në distancë"), + ("Remote Port", "Port në distancë"), + ("Action", "Veprim"), + ("Add", "Shto"), + ("Local Port", "Portë Lokale"), + ("Local Address", "Adresë Lokale"), + ("Change Local Port", "Ndryshoni portën lokale"), ("setup_server_tip", "Për lidhje më të shpejtë, ju lutemi konfiguroni serverin tuaj"), - ("Auto Login", "Hyrja automatike (e vlefshme vetëm nëse vendosni \"Kyçja pas përfundimit të sesionit\")."), + ("Too short, at least 6 characters.", "Shumë e shkurtër , nevojiten të paktën 6 karaktere"), + ("The confirmation is not identical.", "Konfirmimi nuk është identik"), + ("Permissions", "Leje"), + ("Accept", "Prano"), + ("Dismiss", "Hiq"), + ("Disconnect", "Shkëput"), + ("Allow using keyboard and mouse", "Lejoni përdorimin e Tastierës dhe Mousit"), + ("Allow using clipboard", "Lejoni përdorimin e clipboard"), + ("Allow hearing sound", "Lejoni dëgjimin e zërit"), + ("Allow file copy and paste", "Lejoni kopjimin dhe pastimin e skedarëve"), + ("Connected", "I lidhur"), + ("Direct and encrypted connection", "Lidhje direkte dhe enkriptuar"), + ("Relayed and encrypted connection", "Lidhje transmetuese dhe e enkriptuar"), + ("Direct and unencrypted connection", "Lidhje direkte dhe jo e enkriptuar"), + ("Relayed and unencrypted connection", "Lidhje transmetuese dhe jo e enkriptuar"), + ("Enter Remote ID", "Vendosni ID në distancë"), + ("Enter your password", "Vendosni fjalëkalimin tuaj"), + ("Logging in...", "Duke u loguar"), + ("Enable RDP session sharing", "Aktivizoni shpërndarjen e sesionit RDP"), + ("Auto Login", "Hyrje automatike"), + ("Enable Direct IP Access", "Aktivizoni aksesimin e IP direkte"), + ("Rename", "Riemërto"), + ("Space", "Hapërsirë"), + ("Create Desktop Shortcut", "Krijoni shortcut desktop"), + ("Change Path", "Ndrysho rrugëzimin"), + ("Create Folder", "Krijoni një folder"), + ("Please enter the folder name", "Ju lutem vendosni emrin e folderit"), + ("Fix it", "Rregulloni ate"), + ("Warning", "Dicka po shkon keq"), + ("Login screen using Wayland is not supported", "Hyrja në ekran duke përdorur Wayland muk suportohet"), + ("Reboot required", "Kërkohet rinisja"), + ("Unsupported display server ", "Nuk supurtohet severi ekranit"), + ("x11 expected", "Pritet x11"), + ("Port", "Port"), + ("Settings", "Cilësimet"), + ("Username", "Emri i përdoruesit"), + ("Invalid port", "Port e pavlefshme"), + ("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"), + ("Always connect via relay", "Gjithmonë lidheni me transmetues"), ("whitelist_tip", "Vetëm IP e listës së bardhë mund të më aksesoj."), + ("Login", "Hyrje"), + ("Logout", "Dalje"), + ("Tags", "Tage"), + ("Search ID", "Kerko ID"), + ("Current Wayland display server is not supported", "Serveri aktual i ekranit Wayland nuk mbështetet"), ("whitelist_sep", "Të ndara me presje, pikëpresje, hapësira ose rresht të ri"), - ("wrong credentials", "Emri i përdoruesit ose fjalëkalimi i gabuar"), - ("invalid_http", "Duhet të fillojë me http:// ose https://"), + ("Add ID", "Shto ID"), + ("Add Tag", "Shto Tag"), + ("Unselect all tags", "Hiq selektimin e te gjithë tageve"), + ("Network error", "Gabim në rrjet"), + ("Username missed", "Mungon përdorusesi"), + ("Password missed", "Mungon fjalëkalimi"), + ("Wrong credentials", "Kredinciale të gabuara"), + ("Edit Tag", "Edito tagun"), + ("Unremember Password", "Fjalëkalim jo i kujtueshëm"), + ("Favorites", "Te preferuarat"), + ("Add to Favorites", "Shto te të preferuarat"), + ("Remove from Favorites", "Hiq nga të preferuarat"), + ("Empty", "Bosh"), + ("Invalid folder name", "Emri i dosjes i pavlefshëm"), + ("Socks5 Proxy", "Socks5 Proxy"), + ("Hostname", "Emri Hostit"), + ("Discovered", "I pambuluar"), ("install_daemon_tip", "Për të nisur në boot, duhet të instaloni shërbimin e sistemit"), + ("Remote ID", "ID në distancë"), + ("Paste", "Ngjit"), + ("Paste here?", "Ngjit këtu"), + ("Are you sure to close the connection?", "Jeni të sigurtë të mbyllni lidhjen"), + ("Download new version", "Shkarko versionin e ri"), + ("Touch mode", "Metoda me prekje"), + ("Mouse mode", "Modaliteti mausit"), + ("One-Finger Tap", "Prekja Një gisht"), + ("Left Mouse", "Mausi majt"), + ("One-Long Tap", "Prekja nje-gjate"), + ("Two-Finger Tap", "Prekja dy-gishta"), + ("Right Mouse", "Mausi i djathtë"), + ("One-Finger Move", "Lëvizja një-gisht"), + ("Double Tap & Move", "Prekja dhe lëvizja e dyfishtë"), + ("Mouse Drag", "Zhvendosja e mausit"), + ("Three-Finger vertically", "Tre-Gishta vertikalisht"), + ("Mouse Wheel", "Rrota mausit"), + ("Two-Finger Move", "Lëvizja Dy-Gishta"), + ("Canvas Move", "Lëvizja Canvas"), + ("Pinch to Zoom", "Prekni për të zmadhuar"), + ("Canvas Zoom", "Zmadhimi Canavas"), + ("Reset canvas", "Riseto canvas"), + ("No permission of file transfer", "Nuk ka leje për transferimin e dosjesve"), + ("Note", "Shënime"), + ("Connection", "Lidhja"), + ("Share Screen", "Ndaj ekranin"), + ("CLOSE", "Mbyll"), + ("OPEN", "Hap"), + ("Chat", "Biseda"), + ("Total", "Total"), + ("items", "artikuj"), + ("Selected", "E zgjedhur"), + ("Screen Capture", "Kapja e ekranit"), + ("Input Control", "Kontrollo inputin"), + ("Audio Capture", "Kapja e zërit"), + ("File Connection", "Lidhja e skedarëve"), + ("Screen Connection", "Lidhja e ekranit"), + ("Do you accept?", "E pranoni"), + ("Open System Setting", "Hapni cilësimet e sistemit"), + ("How to get Android input permission?", "Si të merrni leje e inputit të Android"), ("android_input_permission_tip1", "Në mënyrë që një pajisje në distancë të kontrollojë pajisjen tuaj Android nëpërmjet mausit ose prekjes, duhet të lejoni RustDesk të përdorë shërbimin."), ("android_input_permission_tip2", "Ju lutemi shkoni në faqen tjetër të cilësimeve të sistemit, gjeni dhe shtypni [Shërbimet e Instaluara], aktivizoni shërbimin [RustDesk Input]"), ("android_new_connection_tip", "Është marrë një kërkesë e re kontrolli, e cila dëshiron të kontrollojë pajisjen tuaj aktuale."), @@ -24,16 +278,124 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_stop_service_tip", "Mbyllja e shërbimit do të mbyllë automatikisht të gjitha lidhjet e vendosura."), ("android_version_audio_tip", "Versioni aktual i Android nuk mbështet regjistrimin e audios, ju lutemi përmirësoni në Android 10 ose më të lartë."), ("android_start_service_tip", "Shtyp [Fillo Shërbimin] ose HAP lejen e [Kapjen e Ekranit] për të nisur shërbimin e ndarjes së ekranit."), + ("Account", "Llogari"), + ("Overwrite", "Përshkruaj"), + ("This file exists, skip or overwrite this file?", "Ky skedar ekziston , tejkalo ose përshkruaj këtë skedarë"), + ("Quit", "Hiq"), ("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"), - ("doc_fix_wayland", "https://rustdesk.com/docs/en/manual/linux/#x11-required"), - ("server_not_support", "Nuk suportohet akoma nga severi"), + ("Help", "Ndihmë"), + ("Failed", "Deshtoi"), + ("Succeeded", "Sukses"), + ("Someone turns on privacy mode, exit", "Dikush ka ndezur menyrën e privatësisë , largohu"), + ("Unsupported", "Nuk mbështetet"), + ("Peer denied", "Peer mohohet"), + ("Please install plugins", "Ju lutemi instaloni shtojcat"), + ("Peer exit", "Dalje peer"), + ("Failed to turn off", "Dështoi të fiket"), + ("Turned off", "I fikur"), + ("In privacy mode", "Në modalitetin e privatësisë"), + ("Out privacy mode", "Jashtë modaliteti i privatësisë"), + ("Language", "Gjuha"), + ("Keep RustDesk background service", "Mbaje shërbimin e sfondit të RustDesk"), + ("Ignore Battery Optimizations", "Injoro optimizimet e baterisë"), ("android_open_battery_optimizations_tip", "Nëse dëshironi ta çaktivizoni këtë veçori, ju lutemi shkoni te faqja tjetër e cilësimeve të aplikacionit RustDesk, gjeni dhe shtypni [Batteri], hiqni zgjedhjen [Te pakufizuara]"), + ("Connection not allowed", "Lidhja nuk lejohet"), + ("Legacy mode", "Modaliteti i trashëgimisë"), + ("Map mode", "Modaliteti i hartës"), + ("Translate mode", "Modaliteti i përkthimit"), + ("Use permanent password", "Përdor fjalëkalim të përhershëm"), + ("Use both passwords", "Përdor të dy fjalëkalimet"), + ("Set permanent password", "Vendos fjalëkalimin e përhershëm"), + ("Enable Remote Restart", "Aktivizo rinisjen në distancë"), + ("Allow remote restart", "Lejo rinisjen në distancë"), + ("Restart Remote Device", "Rinisni pajisjen në distancë"), + ("Are you sure you want to restart", "A jeni i sigurt që dëshironi të rinisni"), + ("Restarting Remote Device", "Rinisja e pajisjes në distancë"), ("remote_restarting_tip", "Pajisja në distancë po riniset, ju lutemi mbyllni këtë kuti mesazhi dhe lidheni përsëri me fjalëkalim të përhershëm pas një kohe"), - ("Are you sure to close the connection?", "A jeni i sigurt për të mbyllur lidhjen?"), - ("elevation_prompt", "Drejtimi i softuerit pa ngritjen e privilegjeve mund të shkaktojë probleme kur përdoruesit në distancë përdorin dritare të caktuara."), - ("uac_warning", "Qasja e refuzuar përkohësisht për shkak të kërkesës për lartësi, ju lutemi prisni që përdoruesi në distancë të pranojë dialogun UAC. Për të shmangur këtë problem, rekomandohet instalimi i softuerit në pajisjen në distancë ose ekzekutimi i tij me privilegje administratori."), - ("elevated_foreground_window_warning", "Përkohësisht është e pamundur për të përdorur mausin dhe tastierën, për shkak se dritarja aktuale e desktopit në distancë kërkon privilegj më të lartë për të vepruar,ju mund t'i kërkoni përdoruesit në distancë të minimizojë dritaren aktuale. Për të shmangur këtë problem, rekomandohet të instaloni softuerin në pajisjen në distancë ose ekzekutoni atë me privilegje administratori."), - ("JumpLink", "Shiko"), - ("Stop service", "Ndalo shërbimin"), - ].iter().cloned().collect(); + ("Copied", "Kopjuar"), + ("Exit Fullscreen", "Dil nga ekrani i plotë"), + ("Fullscreen", "Ekran i plotë"), + ("Mobile Actions", "Veprimet celulare"), + ("Select Monitor", "Zgjidh Monitor"), + ("Control Actions", "Veprimet e kontrollit"), + ("Display Settings", "Cilësimet e ekranit"), + ("Ratio", "Raport"), + ("Image Quality", "Cilësia e imazhit"), + ("Scroll Style", "Stili i lëvizjes"), + ("Show Menubar", "Shfaq shiritin e menusë"), + ("Hide Menubar", "Fshih menunë"), + ("Direct Connection", "Lidhja e drejtpërdrejtë"), + ("Relay Connection", "Lidhja rele"), + ("Secure Connection", "Lidhje e sigurt"), + ("Insecure Connection", "Lidhje e pasigurt"), + ("Scale original", "Shkalla origjinale"), + ("Scale adaptive", " E përsjhtatshme në shkallë"), + ("General", "Gjeneral"), + ("Security", "Siguria"), + ("Account", "Llogaria"), + ("Theme", "Theme"), + ("Dark Theme", "Theme e errët"), + ("Dark", "E errët"), + ("Light", "Drita"), + ("Follow System", "Ndiq sistemin"), + ("Enable hardware codec", "Aktivizo kodekun e harduerit"), + ("Unlock Security Settings", "Zhbllokoni cilësimet e sigurisë"), + ("Enable Audio", "Aktivizo audio"), + ("Unlock Network Settings", "Zhbllokoni cilësimet e rrjetit"), + ("Server", "Server"), + ("Direct IP Access", "Qasje e drejtpërdrejtë IP"), + ("Proxy", "Proxy"), + ("Port", "Port"), + ("Apply", "Apliko"), + ("Disconnect all devices?", "Shkyç të gjitha pajisjet?"), + ("Clear", "Pastro"), + ("Audio Input Device", "Pajisja e hyrjes audio"), + ("Deny remote access", "Mohoni qasjen në distancë"), + ("Use IP Whitelisting", "Përdor listën e bardhë IP"), + ("Network", "Rrjeti"), + ("Enable RDP", "Aktivizo RDP"), + ("Pin menubar", "Pin menubar"), + ("Unpin menubar", "Zgjidh shiritin e menusë"), + ("Recording", "Regjistrimi"), + ("Directory", "Direktoria"), + ("Automatically record incoming sessions", "Regjistro automatikisht seancat hyrëse"), + ("Change", "Ndrysho"), + ("Start session recording", "Fillo regjistrimin e sesionit"), + ("Stop session recording", "Ndalo regjistrimin e sesionit"), + ("Enable Recording Session", "Aktivizo seancën e regjistrimit"), + ("Allow recording session", "Lejo regjistrimin e sesionit"), + ("Enable LAN Discovery", "Aktivizo zbulimin e LAN"), + ("Deny LAN Discovery", "Mohoni zbulimin e LAN"), + ("Write a message", "Shkruani një mesazh"), + ("Prompt", "Prompt"), + ("Please wait for confirmation of UAC...", "Ju lutemi prisni për konfirmimin e UAC..."), + ("elevated_foreground_window_tip", "Përkohësisht është e pamundur për të përdorur mausin dhe tastierën, për shkak se dritarja aktuale e desktopit në distancë kërkon privilegj më të lartë për të vepruar,ju mund t'i kërkoni përdoruesit në distancë të minimizojë dritaren aktuale. Për të shmangur këtë problem, rekomandohet të instaloni softuerin në pajisjen në distancë ose ekzekutoni atë me privilegje administratori."), + ("Disconnected", "Shkyçur"), + ("Other", "Tjetër"), + ("Confirm before closing multiple tabs", "Konfirmo përpara se të mbyllësh shumë skeda"), + ("Keyboard Settings", "Cilësimet e tastierës"), + ("Custom", "personalizuar" + ("Full Access", "Qasje e plotë"), + ("Screen Share", "Ndarja e ekranit"), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland kërkon Ubuntu 21.04 ose version më të lartë"), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland kërkon një version më të lartë të shpërndarjes linux. Ju lutemi provoni desktopin X11 ose ndryshoni OS."), + ("JumpLink", "JumpLink"), + ("Please Select the screen to be shared(Operate on the peer side).", "Ju lutemi zgjidhni ekranin që do të ndahet (Vepro në anën e kolegëve"), + ("Show RustDesk", "Shfaq RustDesk"), + ("This PC", "Ky PC"), + ("or", "ose"), + ("Continue with", "Vazhdo me"), + ("Elevate", "Ngritja"), + ("Zoom cursor", "Zmadho kursorin"), + ("Accept sessions via password", "Prano sesionin nëpërmjet fjalëkalimit"), + ("Accept sessions via click", "Prano sesionet nëpërmjet klikimit"), + ("Accept sessions via both", "Prano sesionet nëpërmjet të dyjave"), + ("Please wait for the remote side to accept your session request...", "Ju lutem prisni që ana në distancë të pranoj kërkësen tuaj"), + ("One-time Password", "Fjalëkalim Një-herë"), + ("Use one-time password", "Përdorni fjalëkalim Një-herë"), + ("One-time password length", "Gjatësia e fjalëkalimit një herë"), + ("Request access to your device", "Kërko akses në pajisjejn tuaj"), + ("Hide connection management window", "Fshih dritaren e menaxhimit të lidhjes"), + ("hide_cm_tip", "Kjo është e mundur vetëm nëse aksesi bëhet nëpërmjet një fjalëkalimi të përhershëm"), + ].iter().cloned().collect(); } From b9155a46c144a522b851c2312c8f8e7912b259b3 Mon Sep 17 00:00:00 2001 From: Don Date: Thu, 1 Dec 2022 15:45:10 +0100 Subject: [PATCH 1088/2015] Small syntax fixes --- src/lang/sq.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 65ba2f518..8076dc21f 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -76,7 +76,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Name", "Emri"), ("Type", "Shkruaj"), ("Modified", "E modifikuar"), - ("Size", "Madhesia" + ("Size", "Madhesia"), ("Show Hidden Files", "Shfaq skedarët e fshehur"), ("Receive", "Merr"), ("Send", "Dërgo"), @@ -374,7 +374,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", "Tjetër"), ("Confirm before closing multiple tabs", "Konfirmo përpara se të mbyllësh shumë skeda"), ("Keyboard Settings", "Cilësimet e tastierës"), - ("Custom", "personalizuar" + ("Custom", "Personalizuar"), ("Full Access", "Qasje e plotë"), ("Screen Share", "Ndarja e ekranit"), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland kërkon Ubuntu 21.04 ose version më të lartë"), From 5ea4a130eaaa7cff8a145dc8d8afd5ddab726618 Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 1 Dec 2022 22:53:49 +0800 Subject: [PATCH 1089/2015] update uni_links_desktop Signed-off-by: fufesou --- flutter/pubspec.lock | 10 ++++------ flutter/pubspec.yaml | 5 +---- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 9803f065c..9a4d3d77b 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -998,12 +998,10 @@ packages: uni_links_desktop: dependency: "direct main" description: - path: "." - ref: "5be5113d59c753989dbf1106241379e3fd4c9b18" - resolved-ref: "5be5113d59c753989dbf1106241379e3fd4c9b18" - url: "https://github.com/fufesou/uni_links_desktop.git" - source: git - version: "0.1.3" + name: uni_links_desktop + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.4" uni_links_platform_interface: dependency: transitive description: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 3d34c30bc..698809be3 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -91,10 +91,7 @@ dependencies: # url: https://github.com/Kingtous/flutter_improved_scrolling # ref: 62f09545149f320616467c306c8c5f71714a18e6 uni_links: ^0.5.1 - uni_links_desktop: - git: - url: https://github.com/fufesou/uni_links_desktop.git - ref: 5be5113d59c753989dbf1106241379e3fd4c9b18 + uni_links_desktop: ^0.1.4 path: ^1.8.1 auto_size_text: ^3.0.0 bot_toast: ^4.0.3 From 9c55c897be2f634d87585510f3d134d255d136b8 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 2 Dec 2022 10:26:52 +0800 Subject: [PATCH 1090/2015] fix: remove tray manager in multi window --- flutter/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 698809be3..fee322b58 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -63,7 +63,7 @@ dependencies: desktop_multi_window: git: url: https://github.com/Kingtous/rustdesk_desktop_multi_window - ref: cb086219bd4760a95a483cb14c1791d2a39ca5a0 + ref: 65a6acfdee49d6fc56c4c89ebb214d308543eb2b freezed_annotation: ^2.0.3 flutter_custom_cursor: git: From 8f2d21f794321def1fa8aa3aa6812c89dc145579 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 2 Dec 2022 11:41:22 +0800 Subject: [PATCH 1091/2015] opt: add build wrap in build.py --- build.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/build.py b/build.py index fc62f8ecb..56361ba0b 100755 --- a/build.py +++ b/build.py @@ -8,6 +8,7 @@ import urllib.request import shutil import hashlib import argparse +import sys windows = platform.platform().startswith('Windows') osx = platform.platform().startswith( @@ -17,6 +18,14 @@ exe_path = 'target/release/' + hbb_name flutter_win_target_dir = 'flutter/build/windows/runner/Release/' skip_cargo = False +def custom_os_system(cmd): + err = os._system(cmd) + if err != 0: + print(f"Error occured when executing: {cmd}. Exiting.") + sys.exit(-1) +# replace prebuilt os.system +os._system = os.system +os.system = custom_os_system def get_version(): with open("Cargo.toml", encoding="utf-8") as fh: From c09a7d445aab1fcb9d5d142478ffd760ed15a838 Mon Sep 17 00:00:00 2001 From: kingtous Date: Fri, 2 Dec 2022 14:59:54 +0800 Subject: [PATCH 1092/2015] fix: build in os.system wrapper --- build.py | 2 +- flutter/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.py b/build.py index 56361ba0b..23a189869 100755 --- a/build.py +++ b/build.py @@ -252,7 +252,7 @@ def build_flutter_deb(version, features): os.system('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/') os.system('mkdir -p tmpdeb/usr/share/applications/') os.system('mkdir -p tmpdeb/usr/share/polkit-1/actions') - os.system('rm tmpdeb/usr/bin/rustdesk') + os.system('rm tmpdeb/usr/bin/rustdesk || true') os.system( 'cp -r build/linux/x64/release/bundle/* tmpdeb/usr/lib/rustdesk/') os.system( diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index fee322b58..ddf5e8a53 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -63,7 +63,7 @@ dependencies: desktop_multi_window: git: url: https://github.com/Kingtous/rustdesk_desktop_multi_window - ref: 65a6acfdee49d6fc56c4c89ebb214d308543eb2b + ref: 82f9eab81cb2c7bfb938def7a1b399a6279bbc75 freezed_annotation: ^2.0.3 flutter_custom_cursor: git: From 5465b849714c0d75c996c3145f287ee1a01046d8 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 2 Dec 2022 16:20:46 +0800 Subject: [PATCH 1093/2015] refactor: remove tray manager in macos --- flutter/macos/Runner/MainFlutterWindow.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flutter/macos/Runner/MainFlutterWindow.swift b/flutter/macos/Runner/MainFlutterWindow.swift index 2ebdf7fc0..1d16763ee 100644 --- a/flutter/macos/Runner/MainFlutterWindow.swift +++ b/flutter/macos/Runner/MainFlutterWindow.swift @@ -10,7 +10,7 @@ import package_info_plus_macos import path_provider_macos import screen_retriever import sqflite -import tray_manager +// import tray_manager import uni_links_desktop import url_launcher_macos import wakelock_macos @@ -39,7 +39,7 @@ class MainFlutterWindow: NSWindow { FLTPackageInfoPlusPlugin.register(with: controller.registrar(forPlugin: "FLTPackageInfoPlusPlugin")) PathProviderPlugin.register(with: controller.registrar(forPlugin: "PathProviderPlugin")) SqflitePlugin.register(with: controller.registrar(forPlugin: "SqflitePlugin")) - TrayManagerPlugin.register(with: controller.registrar(forPlugin: "TrayManagerPlugin")) + // TrayManagerPlugin.register(with: controller.registrar(forPlugin: "TrayManagerPlugin")) UniLinksDesktopPlugin.register(with: controller.registrar(forPlugin: "UniLinksDesktopPlugin")) UrlLauncherPlugin.register(with: controller.registrar(forPlugin: "UrlLauncherPlugin")) WakelockMacosPlugin.register(with: controller.registrar(forPlugin: "WakelockMacosPlugin")) From f2a6a8a88bdf6e54a0efb7a0547fb005f28dfcd1 Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 2 Dec 2022 21:34:20 +0800 Subject: [PATCH 1094/2015] do not show privacy action if peer does not support Signed-off-by: fufesou --- flutter/lib/desktop/widgets/remote_menubar.dart | 2 +- flutter/lib/models/model.dart | 7 +++++++ src/flutter.rs | 13 ++++++++++++- src/server/video_service.rs | 12 ++++++++---- 4 files changed, 28 insertions(+), 6 deletions(-) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 250910396..8385bf63c 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -1170,7 +1170,7 @@ class _RemoteMenubarState extends State { } displayMenu.add(_createSwitchMenuEntry( 'Lock after session end', 'lock-after-session-end', padding, true)); - if (pi.platform == 'Windows') { + if (pi.features.privacyMode) { displayMenu.add(MenuEntrySwitch2( switchType: SwitchType.scheckbox, text: translate('Privacy mode'), diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index f6bfde941..805bcde33 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -344,6 +344,8 @@ class FfiModel with ChangeNotifier { _waitForImage[peerId] = true; _reconnects = 1; } + Map features = json.decode(evt['features']); + _pi.features.privacyMode = features['privacy_mode'] == 1; } notifyListeners(); } @@ -1328,6 +1330,10 @@ class Display { } } +class Features { + bool privacyMode = false; +} + class PeerInfo { String version = ''; String username = ''; @@ -1336,6 +1342,7 @@ class PeerInfo { bool sasEnabled = false; int currentDisplay = 0; List displays = []; + Features features = Features(); } const canvasKey = 'canvas'; diff --git a/src/flutter.rs b/src/flutter.rs index b4d724286..a2c307f5a 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -8,7 +8,8 @@ use std::{ use flutter_rust_bridge::{StreamSink, ZeroCopyBuffer}; use hbb_common::{ - bail, config::LocalConfig, message_proto::*, rendezvous_proto::ConnType, ResultType, + bail, config::LocalConfig, get_version_number, message_proto::*, rendezvous_proto::ConnType, + ResultType, }; use serde_json::json; @@ -299,6 +300,15 @@ impl InvokeUiSession for FlutterHandler { displays.push(h); } let displays = serde_json::ser::to_string(&displays).unwrap_or("".to_owned()); + 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 }); + } + // compatible with 1.1.9 + if get_version_number(&pi.version) < get_version_number("1.2.0") { + features.insert("privacy_mode", 0); + } + let features = serde_json::ser::to_string(&features).unwrap_or("".to_owned()); self.push_event( "peer_info", vec![ @@ -308,6 +318,7 @@ impl InvokeUiSession for FlutterHandler { ("sas_enabled", &pi.sas_enabled.to_string()), ("displays", &displays), ("version", &pi.version), + ("features", &features), ("current_display", &pi.current_display.to_string()), ], ); diff --git a/src/server/video_service.rs b/src/server/video_service.rs index c8f59d60d..28b73cf7c 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -21,9 +21,12 @@ use super::{video_qos::VideoQoS, *}; #[cfg(windows)] use crate::portable_service::client::PORTABLE_SERVICE_RUNNING; -use hbb_common::tokio::sync::{ - mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}, - Mutex as TokioMutex, +use hbb_common::{ + get_version_number, + tokio::sync::{ + mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}, + Mutex as TokioMutex, + }, }; #[cfg(not(windows))] use scrap::Capturer; @@ -92,7 +95,8 @@ pub fn get_privacy_mode_conn_id() -> i32 { pub fn is_privacy_mode_supported() -> bool { #[cfg(windows)] - return *IS_CAPTURER_MAGNIFIER_SUPPORTED; + return *IS_CAPTURER_MAGNIFIER_SUPPORTED + && get_version_number(&crate::VERSION) > get_version_number("1.1.9"); #[cfg(not(windows))] return false; } From e6264038da1efe92ea4e13554542ffbcc9805b83 Mon Sep 17 00:00:00 2001 From: fufesou Date: Sat, 3 Dec 2022 21:23:19 +0800 Subject: [PATCH 1095/2015] win set extra info while simulate inputs Signed-off-by: fufesou --- Cargo.lock | 2 +- src/server/connection.rs | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 529be08d6..bf3ce1f04 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4229,7 +4229,7 @@ dependencies = [ [[package]] name = "rdev" version = "0.5.0-2" -source = "git+https://github.com/asur4s/rdev#fdcee04f10ea0ef00d36aa612eabb9605ae9f2fc" +source = "git+https://github.com/asur4s/rdev#4051761e7ccf434a443b8e9592c23160c9cace56" dependencies = [ "cocoa", "core-foundation 0.9.3", diff --git a/src/server/connection.rs b/src/server/connection.rs index fb281adde..c45a00af6 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -506,7 +506,11 @@ impl Connection { #[cfg(not(any(target_os = "android", target_os = "ios")))] fn handle_input(receiver: std_mpsc::Receiver, tx: Sender) { let mut block_input_mode = false; - + #[cfg(target_os = "windows")] + { + rdev::set_dw_mouse_extra_info(enigo::ENIGO_INPUT_EXTRA_VALUE); + rdev::set_dw_keyboard_extra_info(enigo::ENIGO_INPUT_EXTRA_VALUE); + } loop { match receiver.recv_timeout(std::time::Duration::from_millis(500)) { Ok(v) => match v { From c37f4dabe18bf85677a6fcd1011cdc2baf4f7201 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sun, 4 Dec 2022 17:00:30 +0800 Subject: [PATCH 1096/2015] add LSUIElement=1 to flutter/macos/Runner/Info.plist --- flutter/macos/Runner/Info.plist | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/flutter/macos/Runner/Info.plist b/flutter/macos/Runner/Info.plist index 7b985c870..d1077e0e4 100644 --- a/flutter/macos/Runner/Info.plist +++ b/flutter/macos/Runner/Info.plist @@ -40,6 +40,8 @@ NSMainNibFile MainMenu NSPrincipalClass - NSApplication + NSApplication + LSUIElement + 1 From 37851a380d6ba27c8a34063a62efa28ded6ddecb Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sun, 4 Dec 2022 17:10:26 +0800 Subject: [PATCH 1097/2015] fix my stupid --- build.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.py b/build.py index 23a189869..96d406e09 100755 --- a/build.py +++ b/build.py @@ -461,9 +461,9 @@ def main(): #rcodesign sign --p12-file ~/.p12/rustdesk-developer-id.p12 --p12-password-file ~/.p12/.cert-pass --code-signature-flags runtime ./rustdesk-{1}.dmg codesign -s "Developer ID Application: {0}" --force --options runtime ./rustdesk-{1}.dmg # https://pyoxidizer.readthedocs.io/en/latest/apple_codesign_rcodesign.html - rcodesign notarize --api-issuer 69a6de7d-2907-47e3-e053-5b8c7c11a4d1 --api-key 9JBRHG3JHT --staple ./rustdesk-{1}.dmg + rcodesign notarize --api-issuer {2} --api-key {3} --staple ./rustdesk-{1}.dmg # verify: spctl -a -t exec -v /Applications/RustDesk.app - '''.format(pa, version)) + '''.format(pa, version, os.environ.get('api-issuer'), os.environ.get('api-key'))) else: print('Not signed') else: From b7fc3b7f64f8dc59a8154d4f5455096c9727ed54 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sun, 4 Dec 2022 15:38:16 +0800 Subject: [PATCH 1098/2015] feat: add windows sign config --- .github/workflows/flutter-nightly.yml | 39 +++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 68bf30ac0..87fafe1de 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -88,12 +88,41 @@ jobs: - name: Build rustdesk run: python3 .\build.py --portable --hwcodec --flutter - - name: Rename rustdesk + - name: Sign rustdesk files + uses: DanaBear/code-sign-action@v4 + with: + certificate: '${{ secrets.WINDOWS_PFX_BASE64 }}' + password: '${{ secrets.WINDOWS_PFX_PASSWORD }}' + certificatesha1: '${{ secrets.WINDOWS_PFX_SHA1_THUMBPRINT }}' + # certificatename: '${{ secrets.CERTNAME }}' + folder: './flutter/build/windows/runner/Release/' + recursive: true + + - name: Build self-extracted executable shell: bash run: | - for name in rustdesk*??-install.exe; do - mv "$name" "${name%%-install.exe}-${{ matrix.job.target }}.exe" - done + pushd ./libs/portable + python3 ./generate.py -f ../../flutter/build/windows/runner/Release/ -o . -e ../../flutter/build/windows/runner/Release/ + popd + mkdir -p ./SignOutput + mv ./target/release/rustdesk-portable-packer.exe ./SignOutput/rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}.exe + + # - name: Rename rustdesk + # shell: bash + # run: | + # for name in rustdesk*??-install.exe; do + # mv "$name" ./SignOutput/"${name%%-install.exe}-${{ matrix.job.target }}.exe" + # done + + - name: Sign rustdesk self-extracted file + uses: DanaBear/code-sign-action@v4 + with: + certificate: '${{ secrets.WINDOWS_PFX_BASE64 }}' + password: '${{ secrets.WINDOWS_PFX_PASSWORD }}' + certificatesha1: '${{ secrets.WINDOWS_PFX_SHA1_THUMBPRINT }}' + # certificatename: '${{ secrets.WINDOWS_PFX_NAME }}' + folder: './SignOutput' + recursive: false - name: Publish Release uses: softprops/action-gh-release@v1 @@ -101,7 +130,7 @@ jobs: prerelease: true tag_name: ${{ env.TAG_NAME }} files: | - rustdesk-*.exe + ./SignOutput/rustdesk-*.exe build-for-macOS: name: ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-args }}] From 837ff42ab06bd4330ccc980ceec17b60a4d267c6 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sun, 4 Dec 2022 15:47:06 +0800 Subject: [PATCH 1099/2015] opt: add flutter cache for windows macos --- .github/workflows/flutter-nightly.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 87fafe1de..927b3df07 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -41,6 +41,7 @@ jobs: with: channel: "stable" flutter-version: ${{ env.FLUTTER_VERSION }} + cache: true - name: Replace engine with rustdesk custom flutter engine run: | @@ -157,6 +158,7 @@ jobs: with: channel: "stable" flutter-version: ${{ env.FLUTTER_VERSION }} + cache: true - name: Install Rust toolchain uses: actions-rs/toolchain@v1 From 3a1d8e377769014d742b4bf8a7dd5dcbdc36f5db Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sun, 4 Dec 2022 17:28:26 +0800 Subject: [PATCH 1100/2015] fix: replace with a valid timestamp server --- .github/workflows/flutter-nightly.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 927b3df07..4ccd42081 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -90,7 +90,7 @@ jobs: run: python3 .\build.py --portable --hwcodec --flutter - name: Sign rustdesk files - uses: DanaBear/code-sign-action@v4 + uses: GermanBluefox/code-sign-action@v7 with: certificate: '${{ secrets.WINDOWS_PFX_BASE64 }}' password: '${{ secrets.WINDOWS_PFX_PASSWORD }}' @@ -103,7 +103,7 @@ jobs: shell: bash run: | pushd ./libs/portable - python3 ./generate.py -f ../../flutter/build/windows/runner/Release/ -o . -e ../../flutter/build/windows/runner/Release/ + python3 ./generate.py -f ../../flutter/build/windows/runner/Release/ -o . -e ../../flutter/build/windows/runner/Release/rustdesk.exe popd mkdir -p ./SignOutput mv ./target/release/rustdesk-portable-packer.exe ./SignOutput/rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}.exe @@ -116,7 +116,7 @@ jobs: # done - name: Sign rustdesk self-extracted file - uses: DanaBear/code-sign-action@v4 + uses: GermanBluefox/code-sign-action@v7 with: certificate: '${{ secrets.WINDOWS_PFX_BASE64 }}' password: '${{ secrets.WINDOWS_PFX_PASSWORD }}' From 3c584a7c014f7a7906c724f5b17e7e9100f61fd8 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sun, 4 Dec 2022 18:38:50 +0800 Subject: [PATCH 1101/2015] add lang --- src/lang/ca.rs | 802 ++++++++++++++++++++++--------------------- src/lang/cn.rs | 1 + src/lang/cs.rs | 1 + src/lang/da.rs | 1 + src/lang/de.rs | 7 +- src/lang/en.rs | 1 + src/lang/eo.rs | 1 + src/lang/es.rs | 1 + src/lang/fa.rs | 4 +- src/lang/fr.rs | 1 + src/lang/gr.rs | 793 +++++++++++++++++++++--------------------- src/lang/hu.rs | 1 + src/lang/id.rs | 1 + src/lang/it.rs | 5 +- 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 | 5 +- src/lang/ptbr.rs | 1 + src/lang/ru.rs | 1 + src/lang/sk.rs | 1 + src/lang/sq.rs | 13 +- src/lang/sv.rs | 6 +- src/lang/template.rs | 1 + src/lang/tr.rs | 1 + src/lang/tw.rs | 1 + src/lang/ua.rs | 1 + src/lang/vn.rs | 1 + 29 files changed, 843 insertions(+), 813 deletions(-) diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 0dd21e168..e7794b989 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -1,400 +1,402 @@ -lazy_static::lazy_static! { -pub static ref T: std::collections::HashMap<&'static str, &'static str> = - [ - ("Status", "Estat"), - ("Your Desktop", "EL teu escriptori"), - ("desk_tip", "Pots accedir al teu escriptori amb aquest ID i contrasenya."), - ("Password", "Contrasenya"), - ("Ready", "Llest"), - ("Established", "Establert"), - ("connecting_status", "Connexió a la xarxa RustDesk en progrés..."), - ("Enable Service", "Habilitar Servei"), - ("Start Service", "Iniciar Servei"), - ("Service is running", "El servei s'està executant"), - ("Service is not running", "El servei no s'està executant"), - ("not_ready_status", "No està llest. Comprova la teva connexió"), - ("Control Remote Desktop", "Controlar escriptori remot"), - ("Transfer File", "Transferir arxiu"), - ("Connect", "Connectar"), - ("Recent Sessions", "Sessions recents"), - ("Address Book", "Directori"), - ("Confirmation", "Confirmació"), - ("TCP Tunneling", "Túnel TCP"), - ("Remove", "Eliminar"), - ("Refresh random password", "Actualitzar contrasenya aleatòria"), - ("Set your own password", "Estableix la teva pròpia contrasenya"), - ("Enable Keyboard/Mouse", "Habilitar teclat/ratolí"), - ("Enable Clipboard", "Habilitar portapapers"), - ("Enable File Transfer", "Habilitar transferència d'arxius"), - ("Enable TCP Tunneling", "Habilitar túnel TCP"), - ("IP Whitelisting", "Direccions IP admeses"), - ("ID/Relay Server", "Servidor ID/Relay"), - ("Import Server Config", "Importar configuració de servidor"), - ("Export Server Config", "Exportar configuració del servidor"), - ("Import server configuration successfully", "Configuració de servidor importada amb èxit"), - ("Export server configuration successfully", "Configuració de servidor exportada con èxit"), - ("Invalid server configuration", "Configuració de servidor incorrecta"), - ("Clipboard is empty", "El portapapers està buit"), - ("Stop service", "Aturar servei"), - ("Change ID", "Canviar ID"), - ("Website", "Lloc web"), - ("About", "Sobre"), - ("Mute", "Silenciar"), - ("Audio Input", "Entrada d'àudio"), - ("Enhancements", "Millores"), - ("Hardware Codec", "Còdec de hardware"), - ("Adaptive Bitrate", "Tasa de bits adaptativa"), - ("ID Server", "Servidor de IDs"), - ("Relay Server", "Servidor Relay"), - ("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"), - ("Too frequent", "Massa comú"), - ("Cancel", "Cancel·lar"), - ("Skip", "Saltar"), - ("Close", "Tancar"), - ("Retry", "Reintentar"), - ("OK", ""), - ("Password Required", "Es necessita la contrasenya"), - ("Please enter your password", "Si us plau, introdueixi la seva contrasenya"), - ("Remember password", "Recordar contrasenya"), - ("Wrong Password", "Contrasenya incorrecta"), - ("Do you want to enter again?", "Vol tornar a entrar?"), - ("Connection Error", "Error de connexió"), - ("Error", ""), - ("Reset by the peer", "Reestablert pel peer"), - ("Connecting...", "Connectant..."), - ("Connection in progress. Please wait.", "Connexió en procés. Esperi."), - ("Please try 1 minute later", "Torni a provar-ho d'aquí un minut"), - ("Login Error", "Error d'inicio de sessió"), - ("Successful", "Exitós"), - ("Connected, waiting for image...", "Connectant, esperant imatge..."), - ("Name", "Nom"), - ("Type", "Tipus"), - ("Modified", "Modificat"), - ("Size", "Grandària"), - ("Show Hidden Files", "Mostrar arxius ocults"), - ("Receive", "Rebre"), - ("Send", "Enviar"), - ("Refresh File", "Actualitzar arxiu"), - ("Local", ""), - ("Remote", "Remot"), - ("Remote Computer", "Ordinador remot"), - ("Local Computer", "Ordinador local"), - ("Confirm Delete", "Confirma eliminació"), - ("Delete", "Eliminar"), - ("Properties", "Propietats"), - ("Multi Select", "Selecció múltiple"), - ("Select All", "Selecciona-ho Tot"), - ("Unselect All", "Deselecciona-ho Tot"), - ("Empty Directory", "Directori buit"), - ("Not an empty directory", "No és un directori buit"), - ("Are you sure you want to delete this file?", "Estàs segur que vols eliminar aquest arxiu?"), - ("Are you sure you want to delete this empty directory?", "Estàs segur que vols eliminar aquest directori buit?"), - ("Are you sure you want to delete the file of this directory?", "Estàs segur que vols eliminar aquest arxiu d'aquest directori?"), - ("Do this for all conflicts", "Fes això per a tots els conflictes"), - ("This is irreversible!", "Això és irreversible!"), - ("Deleting", "Eliminant"), - ("files", "arxius"), - ("Waiting", "Esperant"), - ("Finished", "Acabat"), - ("Speed", "Velocitat"), - ("Custom Image Quality", "Qualitat d'imatge personalitzada"), - ("Privacy mode", "Mode privat"), - ("Block user input", "Bloquejar entrada d'usuari"), - ("Unblock user input", "Desbloquejar entrada d'usuari"), - ("Adjust Window", "Ajustar finestra"), - ("Original", "Original"), - ("Shrink", "Reduir"), - ("Stretch", "Estirar"), - ("Scrollbar", "Barra de desplaçament"), - ("ScrollAuto", "Desplaçament automàtico"), - ("Good image quality", "Bona qualitat d'imatge"), - ("Balanced", "Equilibrat"), - ("Optimize reaction time", "Optimitzar el temps de reacció"), - ("Custom", "Personalitzat"), - ("Show remote cursor", "Mostrar cursor remot"), - ("Show quality monitor", "Mostrar qualitat del monitor"), - ("Disable clipboard", "Deshabilitar portapapers"), - ("Lock after session end", "Bloquejar després del final de la sessió"), - ("Insert", "Inserir"), - ("Insert Lock", "Inserir bloqueig"), - ("Refresh", "Actualitzar"), - ("ID does not exist", "L'ID no existeix"), - ("Failed to connect to rendezvous server", "No es pot connectar al servidor rendezvous"), - ("Please try later", "Siusplau provi-ho més tard"), - ("Remote desktop is offline", "L'escriptori remot està desconecctat"), - ("Key mismatch", "La clau no coincideix"), - ("Timeout", "Temps esgotat"), - ("Failed to connect to relay server", "No es pot connectar al servidor de relay"), - ("Failed to connect via rendezvous server", "No es pot connectar a través del servidor de rendezvous"), - ("Failed to connect via relay server", "No es pot connectar a través del servidor de relay"), - ("Failed to make direct connection to remote desktop", "No s'ha pogut establir una connexió directa amb l'escriptori remot"), - ("Set Password", "Configurar la contrasenya"), - ("OS Password", "contrasenya del sistema operatiu"), - ("install_tip", ""), - ("Click to upgrade", "Clicar per actualitzar"), - ("Click to download", "Clicar per descarregar"), - ("Click to update", "Clicar per refrescar"), - ("Configure", "Configurar"), - ("config_acc", ""), - ("config_screen", ""), - ("Installing ...", "Instal·lant ..."), - ("Install", "Instal·lar"), - ("Installation", "Instal·lació"), - ("Installation Path", "Ruta d'instal·lació"), - ("Create start menu shortcuts", "Crear accessos directes al menú d'inici"), - ("Create desktop icon", "Crear icona d'escriptori"), - ("agreement_tip", ""), - ("Accept and Install", "Acceptar i instal·lar"), - ("End-user license agreement", "Acord de llicència d'usuario final"), - ("Generating ...", "Generant ..."), - ("Your installation is lower version.", "La seva instal·lació és una versión inferior."), - ("not_close_tcp_tip", ""), - ("Listening ...", "Escoltant..."), - ("Remote Host", "Hoste remot"), - ("Remote Port", "Port remot"), - ("Action", "Acció"), - ("Add", "Afegirr"), - ("Local Port", "Port local"), - ("Local Address", "Adreça Local"), - ("Change Local Port", "Canviar Port Local"), - ("setup_server_tip", ""), - ("Too short, at least 6 characters.", "Massa curt, almenys 6 caràcters."), - ("The confirmation is not identical.", "La confirmación no coincideix."), - ("Permissions", "Permisos"), - ("Accept", "Acceptar"), - ("Dismiss", "Cancel·lar"), - ("Disconnect", "Desconnectar"), - ("Allow using keyboard and mouse", "Permetre l'ús del teclat i ratolí"), - ("Allow using clipboard", "Permetre usar portapapers"), - ("Allow hearing sound", "Permetre escoltar so"), - ("Allow file copy and paste", "Permetre copiar i enganxar arxius"), - ("Connected", "Connectat"), - ("Direct and encrypted connection", "Connexió directa i xifrada"), - ("Relayed and encrypted connection", "connexió retransmesa i xifrada"), - ("Direct and unencrypted connection", "connexió directa i sense xifrar"), - ("Relayed and unencrypted connection", "connexió retransmesa i sense xifrar"), - ("Enter Remote ID", "Introduixi l'ID remot"), - ("Enter your password", "Introdueixi la seva contrasenya"), - ("Logging in...", "Iniciant sessió..."), - ("Enable RDP session sharing", "Habilitar l'ús compartit de sessions RDP"), - ("Auto Login", "Inici de sessió automàtic"), - ("Enable Direct IP Access", "Habilitar accés IP directe"), - ("Rename", "Renombrar"), - ("Space", "Espai"), - ("Create Desktop Shortcut", "Crear accés directe a l'escriptori"), - ("Change Path", "Cnviar ruta"), - ("Create Folder", "Crear carpeta"), - ("Please enter the folder name", "Indiqui el nom de la carpeta"), - ("Fix it", "Soluciona-ho"), - ("Warning", "Avís"), - ("Login screen using Wayland is not supported", "La pantalla d'inici de sessió amb Wayland no és compatible"), - ("Reboot required", "Cal reiniciar"), - ("Unsupported display server ", "Servidor de visualització no compatible"), - ("x11 expected", "x11 necessari"), - ("Port", ""), - ("Settings", "Ajustaments"), - ("Username", " Nom d'usuari"), - ("Invalid port", "Port incorrecte"), - ("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"), - ("Always connect via relay", "Connecta sempre a través de relay"), - ("whitelist_tip", ""), - ("Login", "Inicia sessió"), - ("Logout", "Sortir"), - ("Tags", ""), - ("Search ID", "Cerca ID"), - ("Current Wayland display server is not supported", "El servidor de visualització actual de Wayland no és compatible"), - ("whitelist_sep", ""), - ("Add ID", "Afegir ID"), - ("Add Tag", "Afegir tag"), - ("Unselect all tags", "Deseleccionar tots els tags"), - ("Network error", "Error de xarxa"), - ("Username missed", "Nom d'usuari oblidat"), - ("Password missed", "Contrasenya oblidada"), - ("Wrong credentials", "Credencials incorrectes"), - ("Edit Tag", "Editar tag"), - ("Unremember Password", "Contrasenya oblidada"), - ("Favorites", "Preferits"), - ("Add to Favorites", "Afegir a preferits"), - ("Remove from Favorites", "Treure de preferits"), - ("Empty", "Buit"), - ("Invalid folder name", "Nom de carpeta incorrecte"), - ("Socks5 Proxy", "Proxy Socks5"), - ("Hostname", ""), - ("Discovered", "Descobert"), - ("install_daemon_tip", ""), - ("Remote ID", "ID remot"), - ("Paste", "Enganxar"), - ("Paste here?", "Enganxar aquí?"), - ("Are you sure to close the connection?", "Estàs segur que vols tancar la connexió?"), - ("Download new version", "Descarregar nova versió"), - ("Touch mode", "Mode tàctil"), - ("Mouse mode", "Mode ratolí"), - ("One-Finger Tap", "Toqui amb un dit"), - ("Left Mouse", "Ratolí esquerra"), - ("One-Long Tap", "Toc llarg"), - ("Two-Finger Tap", "Toqui amb dos dits"), - ("Right Mouse", "Botó dret"), - ("One-Finger Move", "Moviment amb un dir"), - ("Double Tap & Move", "Toqui dos cops i mogui"), - ("Mouse Drag", "Arrastri amb el ratolí"), - ("Three-Finger vertically", "Tres dits verticalment"), - ("Mouse Wheel", "Roda del ratolí"), - ("Two-Finger Move", "Moviment amb dos dits"), - ("Canvas Move", "Moviment del llenç"), - ("Pinch to Zoom", "Pessiga per fer zoom"), - ("Canvas Zoom", "Ampliar llenç"), - ("Reset canvas", "Reestablir llenç"), - ("No permission of file transfer", "No tens permís de transferència de fitxers"), - ("Note", "Nota"), - ("Connection", "connexió"), - ("Share Screen", "Compartir pantalla"), - ("CLOSE", "TANCAR"), - ("OPEN", "OBRIR"), - ("Chat", "Xat"), - ("Total", "Total"), - ("items", "ítems"), - ("Selected", "Seleccionat"), - ("Screen Capture", "Captura de pantalla"), - ("Input Control", "Control d'entrada"), - ("Audio Capture", "Captura d'àudio"), - ("File Connection", "connexió d'arxius"), - ("Screen Connection", "connexió de pantalla"), - ("Do you accept?", "Acceptes?"), - ("Open System Setting", "Configuració del sistema obert"), - ("How to get Android input permission?", "Com obtenir el permís d'entrada d'Android?"), - ("android_input_permission_tip1", "Per a que un dispositiu remot controli el seu dispositiu Android amb el ratolí o tocs, cal permetre que RustDesk utilitzi el servei d' \"Accesibilitat\"."), - ("android_input_permission_tip2", "Vagi a la pàgina de [Serveis instal·lats], activi el servici [RustDesk Input]."), - ("android_new_connection_tip", "S'ha rebut una nova sol·licitud de control per al dispositiu actual."), - ("android_service_will_start_tip", "Habilitar la captura de pantalla iniciarà el servei automàticament, i permetrà que altres dispositius sol·licitin una connexió des d'aquest dispositiu."), - ("android_stop_service_tip", "Tancar el servei tancarà totes les connexions establertes."), - ("android_version_audio_tip", "La versión actual de Android no admet la captura d'àudio, actualizi a Android 10 o superior."), - ("android_start_service_tip", "Toqui el permís [Iniciar servei] o OBRIR [Captura de pantalla] per iniciar el servei d'ús compartit de pantalla."), - ("Account", "Compte"), - ("Overwrite", "Sobreescriure"), - ("This file exists, skip or overwrite this file?", "Aquest arxiu ja existeix, ometre o sobreescriure l'arxiu?"), - ("Quit", "Sortir"), - ("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"), - ("Help", "Ajuda"), - ("Failed", "Ha fallat"), - ("Succeeded", "Aconseguit"), - ("Someone turns on privacy mode, exit", "Algú ha activat el mode de privacitat, surti"), - ("Unsupported", "No suportat"), - ("Peer denied", "Peer denegat"), - ("Please install plugins", "Instal·li complements"), - ("Peer exit", "El peer ha sortit"), - ("Failed to turn off", "Error en apagar"), - ("Turned off", "Apagat"), - ("In privacy mode", "En mode de privacitat"), - ("Out privacy mode", "Fora del mode de privacitat"), - ("Language", "Idioma"), - ("Keep RustDesk background service", "Mantenir RustDesk com a servei en segon pla"), - ("Ignore Battery Optimizations", "Ignorar optimizacions de la bateria"), - ("android_open_battery_optimizations_tip", ""), - ("Connection not allowed", "Connexió no disponible"), - ("Legacy mode", "Mode heretat"), - ("Map mode", "Mode mapa"), - ("Translate mode", "Mode traduit"), - ("Use permanent password", "Utilitzar contrasenya permament"), - ("Use both passwords", "Utilitzar ambdues contrasenyas"), - ("Set permanent password", "Establir contrasenya permament"), - ("Enable Remote Restart", "Activar reinici remot"), - ("Allow remote restart", "Permetre reinici remot"), - ("Restart Remote Device", "Reiniciar dispositiu"), - ("Are you sure you want to restart", "Està segur que vol reiniciar?"), - ("Restarting Remote Device", "Reiniciant dispositiu remot"), - ("remote_restarting_tip", "Dispositiu remot reiniciant, tanqui aquest missatge i tornis a connectar amb la contrasenya."), - ("Copied", "Copiat"), - ("Exit Fullscreen", "Sortir de la pantalla completa"), - ("Fullscreen", "Pantalla completa"), - ("Mobile Actions", "Accions mòbils"), - ("Select Monitor", "Seleccionar monitor"), - ("Control Actions", "Accions de control"), - ("Display Settings", "Configuració de pantalla"), - ("Ratio", "Relació"), - ("Image Quality", "Qualitat d'imatge"), - ("Scroll Style", "Estil de desplaçament"), - ("Show Menubar", "Mostra barra de menú"), - ("Hide Menubar", "Amaga barra de menú"), - ("Direct Connection", "Connexió directa"), - ("Relay Connection", "Connexió Relay"), - ("Secure Connection", "Connexió segura"), - ("Insecure Connection", "Connexió insegura"), - ("Scale original", "Escala original"), - ("Scale adaptive", "Escala adaptativa"), - ("General", ""), - ("Security", "Seguritat"), - ("Account", "Compte"), - ("Theme", "Tema"), - ("Dark Theme", "Tema Fosc"), - ("Dark", "Fosc"), - ("Light", "Clar"), - ("Follow System", "Tema del sistema"), - ("Enable hardware codec", "Habilitar còdec per hardware"), - ("Unlock Security Settings", "Desbloquejar ajustaments de seguritat"), - ("Enable Audio", "Habilitar àudio"), - ("Unlock Network Settings", "Desbloquejar Ajustaments de Xarxa"), - ("Server", "Servidor"), - ("Direct IP Access", "Accés IP Directe"), - ("Proxy", ""), - ("Port", ""), - ("Apply", "Aplicar"), - ("Disconnect all devices?", "Desconnectar tots els dispositius?"), - ("Clear", "Netejar"), - ("Audio Input Device", "Dispositiu d'entrada d'àudio"), - ("Deny remote access", "Denegar accés remot"), - ("Use IP Whitelisting", "Utilitza llista de IPs admeses"), - ("Network", "Xarxa"), - ("Enable RDP", "Habilitar RDP"), - ("Pin menubar", "Bloqueja barra de menú"), - ("Unpin menubar", "Desbloquejar barra de menú"), - ("Recording", "Gravant"), - ("Directory", "Directori"), - ("Automatically record incoming sessions", "Gravació automàtica de sessions entrants"), - ("Change", "Canviar"), - ("Start session recording", "Començar gravació de sessió"), - ("Stop session recording", "Aturar gravació de sessió"), - ("Enable Recording Session", "Habilitar gravació de sessió"), - ("Allow recording session", "Permetre gravació de sessió"), - ("Enable LAN Discovery", "Habilitar descobriment de LAN"), - ("Deny LAN Discovery", "Denegar descobriment de LAN"), - ("Write a message", "Escriure un missatge"), - ("Prompt", ""), - ("elevation_prompt", ""), - ("uac_warning", ""), - ("elevated_foreground_window_warning", ""), - ("Disconnected", "Desconnectat"), - ("Other", "Altre"), - ("Confirm before closing multiple tabs", "Confirmar abans de tancar múltiples pestanyes"), - ("Keyboard Settings", "Ajustaments de teclat"), - ("Custom", "Personalitzat"), - ("Full Access", "Acces complet"), - ("Screen Share", "Compartir pantalla"), - ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland requereix Ubuntu 21.04 o una versió superior."), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland requereix una versió superior de la distribución de Linux. Provi l'escriptori X11 o canvïi el seu sistema operatiu."), - ("JumpLink", "Veure"), - ("Please Select the screen to be shared(Operate on the peer side).", "Seleccioni la pantalla que es compartirà (Operar al costat del peer)."), - ("Show RustDesk", "Mostrar RustDesk"), - ("This PC", "Aquest PC"), - ("or", "o"), - ("Continue with", "Continuar amb"), - ("Accept sessions via password", ""), - ("Accept sessions via click", ""), - ("Accept sessions via both", ""), - ("Please wait for the remote side to accept your session request...", ""), - ("One-time Password", ""), - ("Use one-time password", ""), - ("One-time password length", ""), - ("Request access to your device", ""), - ("Hide connection management window", ""), - ("hide_cm_tip", ""), - ].iter().cloned().collect(); -} +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "Estat"), + ("Your Desktop", "EL teu escriptori"), + ("desk_tip", "Pots accedir al teu escriptori amb aquest ID i contrasenya."), + ("Password", "Contrasenya"), + ("Ready", "Llest"), + ("Established", "Establert"), + ("connecting_status", "Connexió a la xarxa RustDesk en progrés..."), + ("Enable Service", "Habilitar Servei"), + ("Start Service", "Iniciar Servei"), + ("Service is running", "El servei s'està executant"), + ("Service is not running", "El servei no s'està executant"), + ("not_ready_status", "No està llest. Comprova la teva connexió"), + ("Control Remote Desktop", "Controlar escriptori remot"), + ("Transfer File", "Transferir arxiu"), + ("Connect", "Connectar"), + ("Recent Sessions", "Sessions recents"), + ("Address Book", "Directori"), + ("Confirmation", "Confirmació"), + ("TCP Tunneling", "Túnel TCP"), + ("Remove", "Eliminar"), + ("Refresh random password", "Actualitzar contrasenya aleatòria"), + ("Set your own password", "Estableix la teva pròpia contrasenya"), + ("Enable Keyboard/Mouse", "Habilitar teclat/ratolí"), + ("Enable Clipboard", "Habilitar portapapers"), + ("Enable File Transfer", "Habilitar transferència d'arxius"), + ("Enable TCP Tunneling", "Habilitar túnel TCP"), + ("IP Whitelisting", "Direccions IP admeses"), + ("ID/Relay Server", "Servidor ID/Relay"), + ("Import Server Config", "Importar configuració de servidor"), + ("Export Server Config", "Exportar configuració del servidor"), + ("Import server configuration successfully", "Configuració de servidor importada amb èxit"), + ("Export server configuration successfully", "Configuració de servidor exportada con èxit"), + ("Invalid server configuration", "Configuració de servidor incorrecta"), + ("Clipboard is empty", "El portapapers està buit"), + ("Stop service", "Aturar servei"), + ("Change ID", "Canviar ID"), + ("Website", "Lloc web"), + ("About", "Sobre"), + ("Mute", "Silenciar"), + ("Audio Input", "Entrada d'àudio"), + ("Enhancements", "Millores"), + ("Hardware Codec", "Còdec de hardware"), + ("Adaptive Bitrate", "Tasa de bits adaptativa"), + ("ID Server", "Servidor de IDs"), + ("Relay Server", "Servidor Relay"), + ("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"), + ("Too frequent", "Massa comú"), + ("Cancel", "Cancel·lar"), + ("Skip", "Saltar"), + ("Close", "Tancar"), + ("Retry", "Reintentar"), + ("OK", ""), + ("Password Required", "Es necessita la contrasenya"), + ("Please enter your password", "Si us plau, introdueixi la seva contrasenya"), + ("Remember password", "Recordar contrasenya"), + ("Wrong Password", "Contrasenya incorrecta"), + ("Do you want to enter again?", "Vol tornar a entrar?"), + ("Connection Error", "Error de connexió"), + ("Error", ""), + ("Reset by the peer", "Reestablert pel peer"), + ("Connecting...", "Connectant..."), + ("Connection in progress. Please wait.", "Connexió en procés. Esperi."), + ("Please try 1 minute later", "Torni a provar-ho d'aquí un minut"), + ("Login Error", "Error d'inicio de sessió"), + ("Successful", "Exitós"), + ("Connected, waiting for image...", "Connectant, esperant imatge..."), + ("Name", "Nom"), + ("Type", "Tipus"), + ("Modified", "Modificat"), + ("Size", "Grandària"), + ("Show Hidden Files", "Mostrar arxius ocults"), + ("Receive", "Rebre"), + ("Send", "Enviar"), + ("Refresh File", "Actualitzar arxiu"), + ("Local", ""), + ("Remote", "Remot"), + ("Remote Computer", "Ordinador remot"), + ("Local Computer", "Ordinador local"), + ("Confirm Delete", "Confirma eliminació"), + ("Delete", "Eliminar"), + ("Properties", "Propietats"), + ("Multi Select", "Selecció múltiple"), + ("Select All", "Selecciona-ho Tot"), + ("Unselect All", "Deselecciona-ho Tot"), + ("Empty Directory", "Directori buit"), + ("Not an empty directory", "No és un directori buit"), + ("Are you sure you want to delete this file?", "Estàs segur que vols eliminar aquest arxiu?"), + ("Are you sure you want to delete this empty directory?", "Estàs segur que vols eliminar aquest directori buit?"), + ("Are you sure you want to delete the file of this directory?", "Estàs segur que vols eliminar aquest arxiu d'aquest directori?"), + ("Do this for all conflicts", "Fes això per a tots els conflictes"), + ("This is irreversible!", "Això és irreversible!"), + ("Deleting", "Eliminant"), + ("files", "arxius"), + ("Waiting", "Esperant"), + ("Finished", "Acabat"), + ("Speed", "Velocitat"), + ("Custom Image Quality", "Qualitat d'imatge personalitzada"), + ("Privacy mode", "Mode privat"), + ("Block user input", "Bloquejar entrada d'usuari"), + ("Unblock user input", "Desbloquejar entrada d'usuari"), + ("Adjust Window", "Ajustar finestra"), + ("Original", "Original"), + ("Shrink", "Reduir"), + ("Stretch", "Estirar"), + ("Scrollbar", "Barra de desplaçament"), + ("ScrollAuto", "Desplaçament automàtico"), + ("Good image quality", "Bona qualitat d'imatge"), + ("Balanced", "Equilibrat"), + ("Optimize reaction time", "Optimitzar el temps de reacció"), + ("Custom", "Personalitzat"), + ("Show remote cursor", "Mostrar cursor remot"), + ("Show quality monitor", "Mostrar qualitat del monitor"), + ("Disable clipboard", "Deshabilitar portapapers"), + ("Lock after session end", "Bloquejar després del final de la sessió"), + ("Insert", "Inserir"), + ("Insert Lock", "Inserir bloqueig"), + ("Refresh", "Actualitzar"), + ("ID does not exist", "L'ID no existeix"), + ("Failed to connect to rendezvous server", "No es pot connectar al servidor rendezvous"), + ("Please try later", "Siusplau provi-ho més tard"), + ("Remote desktop is offline", "L'escriptori remot està desconecctat"), + ("Key mismatch", "La clau no coincideix"), + ("Timeout", "Temps esgotat"), + ("Failed to connect to relay server", "No es pot connectar al servidor de relay"), + ("Failed to connect via rendezvous server", "No es pot connectar a través del servidor de rendezvous"), + ("Failed to connect via relay server", "No es pot connectar a través del servidor de relay"), + ("Failed to make direct connection to remote desktop", "No s'ha pogut establir una connexió directa amb l'escriptori remot"), + ("Set Password", "Configurar la contrasenya"), + ("OS Password", "contrasenya del sistema operatiu"), + ("install_tip", ""), + ("Click to upgrade", "Clicar per actualitzar"), + ("Click to download", "Clicar per descarregar"), + ("Click to update", "Clicar per refrescar"), + ("Configure", "Configurar"), + ("config_acc", ""), + ("config_screen", ""), + ("Installing ...", "Instal·lant ..."), + ("Install", "Instal·lar"), + ("Installation", "Instal·lació"), + ("Installation Path", "Ruta d'instal·lació"), + ("Create start menu shortcuts", "Crear accessos directes al menú d'inici"), + ("Create desktop icon", "Crear icona d'escriptori"), + ("agreement_tip", ""), + ("Accept and Install", "Acceptar i instal·lar"), + ("End-user license agreement", "Acord de llicència d'usuario final"), + ("Generating ...", "Generant ..."), + ("Your installation is lower version.", "La seva instal·lació és una versión inferior."), + ("not_close_tcp_tip", ""), + ("Listening ...", "Escoltant..."), + ("Remote Host", "Hoste remot"), + ("Remote Port", "Port remot"), + ("Action", "Acció"), + ("Add", "Afegirr"), + ("Local Port", "Port local"), + ("Local Address", "Adreça Local"), + ("Change Local Port", "Canviar Port Local"), + ("setup_server_tip", ""), + ("Too short, at least 6 characters.", "Massa curt, almenys 6 caràcters."), + ("The confirmation is not identical.", "La confirmación no coincideix."), + ("Permissions", "Permisos"), + ("Accept", "Acceptar"), + ("Dismiss", "Cancel·lar"), + ("Disconnect", "Desconnectar"), + ("Allow using keyboard and mouse", "Permetre l'ús del teclat i ratolí"), + ("Allow using clipboard", "Permetre usar portapapers"), + ("Allow hearing sound", "Permetre escoltar so"), + ("Allow file copy and paste", "Permetre copiar i enganxar arxius"), + ("Connected", "Connectat"), + ("Direct and encrypted connection", "Connexió directa i xifrada"), + ("Relayed and encrypted connection", "connexió retransmesa i xifrada"), + ("Direct and unencrypted connection", "connexió directa i sense xifrar"), + ("Relayed and unencrypted connection", "connexió retransmesa i sense xifrar"), + ("Enter Remote ID", "Introduixi l'ID remot"), + ("Enter your password", "Introdueixi la seva contrasenya"), + ("Logging in...", "Iniciant sessió..."), + ("Enable RDP session sharing", "Habilitar l'ús compartit de sessions RDP"), + ("Auto Login", "Inici de sessió automàtic"), + ("Enable Direct IP Access", "Habilitar accés IP directe"), + ("Rename", "Renombrar"), + ("Space", "Espai"), + ("Create Desktop Shortcut", "Crear accés directe a l'escriptori"), + ("Change Path", "Cnviar ruta"), + ("Create Folder", "Crear carpeta"), + ("Please enter the folder name", "Indiqui el nom de la carpeta"), + ("Fix it", "Soluciona-ho"), + ("Warning", "Avís"), + ("Login screen using Wayland is not supported", "La pantalla d'inici de sessió amb Wayland no és compatible"), + ("Reboot required", "Cal reiniciar"), + ("Unsupported display server ", "Servidor de visualització no compatible"), + ("x11 expected", "x11 necessari"), + ("Port", ""), + ("Settings", "Ajustaments"), + ("Username", " Nom d'usuari"), + ("Invalid port", "Port incorrecte"), + ("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"), + ("Always connect via relay", "Connecta sempre a través de relay"), + ("whitelist_tip", ""), + ("Login", "Inicia sessió"), + ("Logout", "Sortir"), + ("Tags", ""), + ("Search ID", "Cerca ID"), + ("Current Wayland display server is not supported", "El servidor de visualització actual de Wayland no és compatible"), + ("whitelist_sep", ""), + ("Add ID", "Afegir ID"), + ("Add Tag", "Afegir tag"), + ("Unselect all tags", "Deseleccionar tots els tags"), + ("Network error", "Error de xarxa"), + ("Username missed", "Nom d'usuari oblidat"), + ("Password missed", "Contrasenya oblidada"), + ("Wrong credentials", "Credencials incorrectes"), + ("Edit Tag", "Editar tag"), + ("Unremember Password", "Contrasenya oblidada"), + ("Favorites", "Preferits"), + ("Add to Favorites", "Afegir a preferits"), + ("Remove from Favorites", "Treure de preferits"), + ("Empty", "Buit"), + ("Invalid folder name", "Nom de carpeta incorrecte"), + ("Socks5 Proxy", "Proxy Socks5"), + ("Hostname", ""), + ("Discovered", "Descobert"), + ("install_daemon_tip", ""), + ("Remote ID", "ID remot"), + ("Paste", "Enganxar"), + ("Paste here?", "Enganxar aquí?"), + ("Are you sure to close the connection?", "Estàs segur que vols tancar la connexió?"), + ("Download new version", "Descarregar nova versió"), + ("Touch mode", "Mode tàctil"), + ("Mouse mode", "Mode ratolí"), + ("One-Finger Tap", "Toqui amb un dit"), + ("Left Mouse", "Ratolí esquerra"), + ("One-Long Tap", "Toc llarg"), + ("Two-Finger Tap", "Toqui amb dos dits"), + ("Right Mouse", "Botó dret"), + ("One-Finger Move", "Moviment amb un dir"), + ("Double Tap & Move", "Toqui dos cops i mogui"), + ("Mouse Drag", "Arrastri amb el ratolí"), + ("Three-Finger vertically", "Tres dits verticalment"), + ("Mouse Wheel", "Roda del ratolí"), + ("Two-Finger Move", "Moviment amb dos dits"), + ("Canvas Move", "Moviment del llenç"), + ("Pinch to Zoom", "Pessiga per fer zoom"), + ("Canvas Zoom", "Ampliar llenç"), + ("Reset canvas", "Reestablir llenç"), + ("No permission of file transfer", "No tens permís de transferència de fitxers"), + ("Note", "Nota"), + ("Connection", "connexió"), + ("Share Screen", "Compartir pantalla"), + ("CLOSE", "TANCAR"), + ("OPEN", "OBRIR"), + ("Chat", "Xat"), + ("Total", "Total"), + ("items", "ítems"), + ("Selected", "Seleccionat"), + ("Screen Capture", "Captura de pantalla"), + ("Input Control", "Control d'entrada"), + ("Audio Capture", "Captura d'àudio"), + ("File Connection", "connexió d'arxius"), + ("Screen Connection", "connexió de pantalla"), + ("Do you accept?", "Acceptes?"), + ("Open System Setting", "Configuració del sistema obert"), + ("How to get Android input permission?", "Com obtenir el permís d'entrada d'Android?"), + ("android_input_permission_tip1", "Per a que un dispositiu remot controli el seu dispositiu Android amb el ratolí o tocs, cal permetre que RustDesk utilitzi el servei d' \"Accesibilitat\"."), + ("android_input_permission_tip2", "Vagi a la pàgina de [Serveis instal·lats], activi el servici [RustDesk Input]."), + ("android_new_connection_tip", "S'ha rebut una nova sol·licitud de control per al dispositiu actual."), + ("android_service_will_start_tip", "Habilitar la captura de pantalla iniciarà el servei automàticament, i permetrà que altres dispositius sol·licitin una connexió des d'aquest dispositiu."), + ("android_stop_service_tip", "Tancar el servei tancarà totes les connexions establertes."), + ("android_version_audio_tip", "La versión actual de Android no admet la captura d'àudio, actualizi a Android 10 o superior."), + ("android_start_service_tip", "Toqui el permís [Iniciar servei] o OBRIR [Captura de pantalla] per iniciar el servei d'ús compartit de pantalla."), + ("Account", "Compte"), + ("Overwrite", "Sobreescriure"), + ("This file exists, skip or overwrite this file?", "Aquest arxiu ja existeix, ometre o sobreescriure l'arxiu?"), + ("Quit", "Sortir"), + ("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"), + ("Help", "Ajuda"), + ("Failed", "Ha fallat"), + ("Succeeded", "Aconseguit"), + ("Someone turns on privacy mode, exit", "Algú ha activat el mode de privacitat, surti"), + ("Unsupported", "No suportat"), + ("Peer denied", "Peer denegat"), + ("Please install plugins", "Instal·li complements"), + ("Peer exit", "El peer ha sortit"), + ("Failed to turn off", "Error en apagar"), + ("Turned off", "Apagat"), + ("In privacy mode", "En mode de privacitat"), + ("Out privacy mode", "Fora del mode de privacitat"), + ("Language", "Idioma"), + ("Keep RustDesk background service", "Mantenir RustDesk com a servei en segon pla"), + ("Ignore Battery Optimizations", "Ignorar optimizacions de la bateria"), + ("android_open_battery_optimizations_tip", ""), + ("Connection not allowed", "Connexió no disponible"), + ("Legacy mode", "Mode heretat"), + ("Map mode", "Mode mapa"), + ("Translate mode", "Mode traduit"), + ("Use permanent password", "Utilitzar contrasenya permament"), + ("Use both passwords", "Utilitzar ambdues contrasenyas"), + ("Set permanent password", "Establir contrasenya permament"), + ("Enable Remote Restart", "Activar reinici remot"), + ("Allow remote restart", "Permetre reinici remot"), + ("Restart Remote Device", "Reiniciar dispositiu"), + ("Are you sure you want to restart", "Està segur que vol reiniciar?"), + ("Restarting Remote Device", "Reiniciant dispositiu remot"), + ("remote_restarting_tip", "Dispositiu remot reiniciant, tanqui aquest missatge i tornis a connectar amb la contrasenya."), + ("Copied", "Copiat"), + ("Exit Fullscreen", "Sortir de la pantalla completa"), + ("Fullscreen", "Pantalla completa"), + ("Mobile Actions", "Accions mòbils"), + ("Select Monitor", "Seleccionar monitor"), + ("Control Actions", "Accions de control"), + ("Display Settings", "Configuració de pantalla"), + ("Ratio", "Relació"), + ("Image Quality", "Qualitat d'imatge"), + ("Scroll Style", "Estil de desplaçament"), + ("Show Menubar", "Mostra barra de menú"), + ("Hide Menubar", "Amaga barra de menú"), + ("Direct Connection", "Connexió directa"), + ("Relay Connection", "Connexió Relay"), + ("Secure Connection", "Connexió segura"), + ("Insecure Connection", "Connexió insegura"), + ("Scale original", "Escala original"), + ("Scale adaptive", "Escala adaptativa"), + ("General", ""), + ("Security", "Seguritat"), + ("Account", "Compte"), + ("Theme", "Tema"), + ("Dark Theme", "Tema Fosc"), + ("Dark", "Fosc"), + ("Light", "Clar"), + ("Follow System", "Tema del sistema"), + ("Enable hardware codec", "Habilitar còdec per hardware"), + ("Unlock Security Settings", "Desbloquejar ajustaments de seguritat"), + ("Enable Audio", "Habilitar àudio"), + ("Unlock Network Settings", "Desbloquejar Ajustaments de Xarxa"), + ("Server", "Servidor"), + ("Direct IP Access", "Accés IP Directe"), + ("Proxy", ""), + ("Port", ""), + ("Apply", "Aplicar"), + ("Disconnect all devices?", "Desconnectar tots els dispositius?"), + ("Clear", "Netejar"), + ("Audio Input Device", "Dispositiu d'entrada d'àudio"), + ("Deny remote access", "Denegar accés remot"), + ("Use IP Whitelisting", "Utilitza llista de IPs admeses"), + ("Network", "Xarxa"), + ("Enable RDP", "Habilitar RDP"), + ("Pin menubar", "Bloqueja barra de menú"), + ("Unpin menubar", "Desbloquejar barra de menú"), + ("Recording", "Gravant"), + ("Directory", "Directori"), + ("Automatically record incoming sessions", "Gravació automàtica de sessions entrants"), + ("Change", "Canviar"), + ("Start session recording", "Començar gravació de sessió"), + ("Stop session recording", "Aturar gravació de sessió"), + ("Enable Recording Session", "Habilitar gravació de sessió"), + ("Allow recording session", "Permetre gravació de sessió"), + ("Enable LAN Discovery", "Habilitar descobriment de LAN"), + ("Deny LAN Discovery", "Denegar descobriment de LAN"), + ("Write a message", "Escriure un missatge"), + ("Prompt", ""), + ("Please wait for confirmation of UAC...", ""), + ("elevated_foreground_window_tip", ""), + ("Disconnected", "Desconnectat"), + ("Other", "Altre"), + ("Confirm before closing multiple tabs", "Confirmar abans de tancar múltiples pestanyes"), + ("Keyboard Settings", "Ajustaments de teclat"), + ("Custom", "Personalitzat"), + ("Full Access", "Acces complet"), + ("Screen Share", "Compartir pantalla"), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland requereix Ubuntu 21.04 o una versió superior."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland requereix una versió superior de la distribución de Linux. Provi l'escriptori X11 o canvïi el seu sistema operatiu."), + ("JumpLink", "Veure"), + ("Please Select the screen to be shared(Operate on the peer side).", "Seleccioni la pantalla que es compartirà (Operar al costat del peer)."), + ("Show RustDesk", "Mostrar RustDesk"), + ("This PC", "Aquest PC"), + ("or", "o"), + ("Continue with", "Continuar amb"), + ("Elevate", ""), + ("Zoom cursor", ""), + ("Accept sessions via password", ""), + ("Accept sessions via click", ""), + ("Accept sessions via both", ""), + ("Please wait for the remote side to accept your session request...", ""), + ("One-time Password", ""), + ("Use one-time password", ""), + ("One-time password length", ""), + ("Request access to your device", ""), + ("Hide connection management window", ""), + ("hide_cm_tip", ""), + ("wayland_experiment_tip", ""), + ].iter().cloned().collect(); +} diff --git a/src/lang/cn.rs b/src/lang/cn.rs index a3b3b47c8..8af5229f2 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -397,5 +397,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request access to your device", "请求访问你的设备"), ("Hide connection management window", "隐藏连接管理窗口"), ("hide_cm_tip", "在只允许密码连接并且只用固定密码的情况下才允许隐藏"), + ("wayland_experiment_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 450f3971a..547b233f4 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -397,5 +397,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request access to your device", ""), ("Hide connection management window", ""), ("hide_cm_tip", ""), + ("wayland_experiment_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index ea7263ac8..5d815a90b 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -397,5 +397,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request access to your device", ""), ("Hide connection management window", ""), ("hide_cm_tip", ""), + ("wayland_experiment_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index c2290b95c..e4306301b 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -301,7 +301,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_open_battery_optimizations_tip", "Möchten Sie die Einstellungen zur Batterieopimierung öffnen?"), ("Connection not allowed", "Verbindung abgelehnt"), ("Legacy mode", "Kompatibilitätsmodus"), - ("Map mode", ""), //Muss noch angepasst werden + ("Map mode", ""), //Muss noch angepasst wer"), ("Translate mode", "Übersetzungsmodus"), ("Use permanent password", "Permanentes Passwort verwenden"), ("Use both passwords", "Beide Passwörter verwenden"), @@ -367,7 +367,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable LAN Discovery", "LAN-Erkennung aktivieren"), ("Deny LAN Discovery", "LAN-Erkennung verbieten"), ("Write a message", "Nachricht schreiben"), - ("Prompt", ""), //Aufforderung? + ("Prompt", ""), //Aufforderu"), ("Please wait for confirmation of UAC...", "Bitte auf die Bestätigung des Nutzers warten..."), ("elevated_foreground_window_tip", ""), ("Disconnected", "Verbindung abgebrochen"), @@ -396,6 +396,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("One-time password length", "Länge des Einmalpassworts"), ("Request access to your device", "Zugriff zu Ihrem Gerät erbitten"), ("Hide connection management window", "Fenster zur Verwaltung der Verbindung verstecken"), - ("hide_cm_tip", "Dies ist nur möglich, wenn der Zugriff nur über ein permanentes Passwort erfolgt."), // Sehr unklar. Muss noch angepasst werden. Original: Allow hiding only if accepting sessions via password and using pernament password + ("hide_cm_tip", "Dies ist nur möglich, wenn der Zugriff nur über ein permanentes Passwort erfolgt."), // Sehr unklar. Muss noch angepasst werden. Original: Allow hiding only if accepting sessions via password and using pernament passw"), + ("wayland_experiment_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/en.rs b/src/lang/en.rs index a661c17bc..b6992230d 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -34,5 +34,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("JumpLink", "View"), ("Stop service", "Stop Service"), ("hide_cm_tip", "Allow hiding only if accepting sessions via password and using permanent password"), + ("wayland_experiment_tip", "Wayland support is in experimental stage, please use X11 if you require unattended access."), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 797eb2bb6..f5a2f7e55 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -397,5 +397,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request access to your device", ""), ("Hide connection management window", ""), ("hide_cm_tip", ""), + ("wayland_experiment_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 514d21480..d8362ff05 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -397,5 +397,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request access to your device", "Solicitud de acceso a su dispositivo"), ("Hide connection management window", "Ocultar ventana de gestión de conexión"), ("hide_cm_tip", "Permitir ocultar solo si se aceptan sesiones a través de contraseña y usando contraseña permanente"), + ("wayland_experiment_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index ac4a8771b..ee3706e34 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -278,7 +278,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_stop_service_tip", "با بستن سرویس، تمام اتصالات برقرار شده به طور خودکار بسته می شود"), ("android_version_audio_tip", "نسخه فعلی اندروید از ضبط صدا پشتیبانی نمی‌کند، لطفاً به اندروید 10 یا بالاتر به‌روزرسانی کنید"), ("android_start_service_tip", "برای شروع سرویس اشتراک‌گذاری صفحه، روی مجوز \"شروع مرحله‌بندی سرور\" یا OPEN \"Screen Capture\" کلیک کنید."), - ("Account", "حساب"), + ("Account", "حساب کاربری"), ("Overwrite", "بازنویسی"), ("This file exists, skip or overwrite this file?", "این فایل وجود دارد، از فایل رد شود یا بازنویسی شود؟"), ("Quit", "خروج"), @@ -385,6 +385,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("This PC", "This PC"), ("or", "یا"), ("Continue with", "ادامه با"), + ("Elevate", ""), ("Zoom cursor", "نشانگر بزرگنمایی"), ("Accept sessions via password", "قبول درخواست با رمز عبور"), ("Accept sessions via click", "قبول درخواست با کلیک موس"), @@ -396,5 +397,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request access to your device", "دسترسی به دستگاه خود را درخواست کنید"), ("Hide connection management window", "پنهان کردن پنجره مدیریت اتصال"), ("hide_cm_tip", "فقط در صورت پذیرفتن جلسات از طریق رمز عبور و استفاده از رمز عبور دائمی، مخفی شدن مجاز است"), + ("wayland_experiment_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 53c3b3bfa..860f59518 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -397,5 +397,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request access to your device", "Demande d'accès à votre appareil"), ("Hide connection management window", "Masquer la fenêtre de gestion des connexions"), ("hide_cm_tip", "Autoriser le masquage uniquement si vous acceptez des sessions via un mot de passe et utilisez un mot de passe permanent"), + ("wayland_experiment_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/gr.rs b/src/lang/gr.rs index 93b75d54e..20bb98200 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -1,401 +1,402 @@ lazy_static::lazy_static! { pub static ref T: std::collections::HashMap<&'static str, &'static str> = [ - ("Status", "Κατάσταση"), - ("Your Desktop", "Ο σταθμός εργασίας σας"), - ("desk_tip", "Η πρόσβαση στον σταθμό εργασίας σας είναι δυνατή με αυτό το αναγνωριστικό και τον κωδικό πρόσβασης."), - ("Password", "Κωδικός πρόσβασης"), - ("Ready", "Έτοιμο"), - ("Established", "Συνδέθηκε"), - ("connecting_status", "Σύνδεση στο δίκτυο RustDesk..."), - ("Enable Service", "Ενεργοποίηση υπηρεσίας"), - ("Start Service", "Έναρξη υπηρεσίας"), - ("Service is running", "Η υπηρεσία εκτελείται"), - ("Service is not running", "Η υπηρεσία δεν εκτελείται"), - ("not_ready_status", "Δεν είναι έτοιμο. Ελέγξτε τη σύνδεσή σας"), - ("Control Remote Desktop", "Έλεγχος απομακρυσμένου σταθμού εργασίας"), - ("Transfer File", "Μεταφορά αρχείου"), - ("Connect", "Σύνδεση"), - ("Recent Sessions", "Πρόσφατες συνεδρίες"), - ("Address Book", "Βιβλίο διευθύνσεων"), - ("Confirmation", "Επιβεβαίωση"), - ("TCP Tunneling", "TCP Tunneling"), - ("Remove", "Κατάργηση"), - ("Refresh random password", "Νέος τυχαίος κωδικός πρόσβασης"), - ("Set your own password", "Ορίστε τον δικό σας κωδικό πρόσβασης"), - ("Enable Keyboard/Mouse", "Ενεργοποίηση πληκτρολογίου/ποντικιού"), - ("Enable Clipboard", "Ενεργοποίηση Προχείρου"), - ("Enable File Transfer", "Ενεργοποίηση μεταφοράς αρχείων"), - ("Enable TCP Tunneling", "Ενεργοποίηση TCP Tunneling"), - ("IP Whitelisting", "Λίστα επιτρεπόμενων IP"), - ("ID/Relay Server", "Διακομιστής ID/Αναμετάδοσης"), - ("Import Server Config", "Εισαγωγή διαμόρφωσης διακομιστή"), - ("Export Server Config", "Εξαγωγή διαμόρφωσης διακομιστή"), - ("Import server configuration successfully", "Επιτυχής εισαγωγή διαμόρφωσης διακομιστή"), - ("Export server configuration successfully", "Επιτυχής εξαγωγή διαμόρφωσης διακομιστή"), - ("Invalid server configuration", "Μη έγκυρη διαμόρφωση διακομιστή"), - ("Clipboard is empty", "Το πρόχειρο είναι κενό"), - ("Stop service", "Διακοπή υπηρεσίας"), - ("Change ID", "Αλλαγή αναγνωριστικού ID"), - ("Website", "Ιστότοπος"), - ("About", "Πληροφορίες"), - ("Mute", "Σίγαση"), - ("Audio Input", "Είσοδος ήχου"), - ("Enhancements", "Βελτιώσεις"), - ("Hardware Codec", "Κωδικοποιητής υλικού"), - ("Adaptive Bitrate", "Adaptive Bitrate"), - ("ID Server", "Διακομιστής ID"), - ("Relay Server", "Διακομιστής αναμετάδοσης"), - ("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", "Μη διαθέσιμο"), - ("Too frequent", "Πολύ συχνά"), - ("Cancel", "Ακύρωση"), - ("Skip", "Παράλειψη"), - ("Close", "Κλείσιμο"), - ("Retry", "Δοκίμασε ξανά"), - ("OK", "Εντάξει"), - ("Password Required", "Απαιτείται κωδικός πρόσβασης"), - ("Please enter your password", "Παρακαλώ εισάγετε τον κωδικό πρόσβασης"), - ("Remember password", "Απομνημόνευση κωδικού πρόσβασης"), - ("Wrong Password", "Λάθος κωδικός πρόσβασης"), - ("Do you want to enter again?", "Επανασύνδεση;"), - ("Connection Error", "Σφάλμα σύνδεσης"), - ("Error", "Σφάλμα"), - ("Reset by the peer", "Η σύνδεση επαναφέρθηκε από τον απομακρυσμένο σταθμό"), - ("Connecting...", "Σύνδεση..."), - ("Connection in progress. Please wait.", "Σύνδεση σε εξέλιξη. Παρακαλώ περιμένετε."), - ("Please try 1 minute later", "Παρακαλώ ξαναδοκιμάστε σε 1 λεπτό"), - ("Login Error", "Σφάλμα εισόδου"), - ("Successful", "Επιτυχής"), - ("Connected, waiting for image...", "Συνδέθηκε, αναμονή για εικόνα..."), - ("Name", "Όνομα"), - ("Type", "Τύπος"), - ("Modified", "Τροποποιήθηκε"), - ("Size", "Μέγεθος"), - ("Show Hidden Files", "Εμφάνιση κρυφών αρχείων"), - ("Receive", "Λήψη"), - ("Send", "Αποστολή"), - ("Refresh File", "Ανανέωση αρχείου"), - ("Local", "Τοπικό"), - ("Remote", "Απομακρυσμένο"), - ("Remote Computer", "Απομακρυσμένος υπολογιστής"), - ("Local Computer", "Τοπικός υπολογιστής"), - ("Confirm Delete", "Επιβεβαίωση διαγραφής"), - ("Delete", "Διαγραφή"), - ("Properties", "Ιδιότητες"), - ("Multi Select", "Πολλαπλή επιλογή"), - ("Select All", "Επιλογή όλων"), - ("Unselect All", "Κατάργηση επιλογής όλων"), - ("Empty Directory", "Κενός φάκελος"), - ("Not an empty directory", "Ο φάκελος δεν είναι κενός"), - ("Are you sure you want to delete this file?", "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτό το αρχείο;"), - ("Are you sure you want to delete this empty directory?", "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτόν τον κενό φάκελο;"), - ("Are you sure you want to delete the file of this directory?", "Είστε βέβαιοι ότι θέλετε να διαγράψετε το αρχείο αυτού του φακέλου;"), - ("Do this for all conflicts", "Κάνε αυτό για όλες τις διενέξεις"), - ("This is irreversible!", "Αυτό είναι μη αναστρέψιμο!"), - ("Deleting", "Διαγραφή"), - ("files", "αρχεία"), - ("Waiting", "Αναμονή"), - ("Finished", "Ολοκληρώθηκε"), - ("Speed", "Ταχύτητα"), - ("Custom Image Quality", "Προσαρμοσμένη ποιότητα εικόνας"), - ("Privacy mode", "Λειτουργία απορρήτου"), - ("Block user input", "Αποκλεισμός χειρισμού από τον χρήστη"), - ("Unblock user input", "Κατάργηση αποκλεισμού χειρισμού από τον χρήστη"), - ("Adjust Window", "Προσαρμογή παραθύρου"), - ("Original", "Πρωτότυπο"), - ("Shrink", "Συρρίκνωση"), - ("Stretch", "Προσαρμογή"), - ("Scrollbar", "Γραμμή κύλισης"), - ("ScrollAuto", "Αυτόματη κύλιση"), - ("Good image quality", "Καλή ποιότητα εικόνας"), - ("Balanced", "Ισορροπημένο"), - ("Optimize reaction time", "Βελτιστοποίηση χρόνου αντίδρασης"), - ("Custom", "Προσαρμοσμένο"), - ("Show remote cursor", "Εμφάνιση απομακρυσμένου κέρσορα"), - ("Show quality monitor", "Εμφάνιση παρακολούθησης ποιότητας σύνδεσης"), - ("Disable clipboard", "Απενεργοποίηση προχείρου"), - ("Lock after session end", "Κλείδωμα μετά το τέλος της συνεδρίας"), - ("Insert", "Εισάγετε"), - ("Insert Lock", "Εισαγωγή κλειδαριάς"), - ("Refresh", "Ανανέωση"), - ("ID does not exist", "Το αναγνωριστικό ID δεν υπάρχει"), - ("Failed to connect to rendezvous server", "Αποτυχία σύνδεσης με διακομιστή"), - ("Please try later", "Παρακαλώ δοκιμάστε αργότερα"), - ("Remote desktop is offline", "Ο απομακρυσμένος σταθμός εργασίας είναι εκτός σύνδεσης"), - ("Key mismatch", "Μη έγκυρο κλειδί"), - ("Timeout", "Τέλος χρόνου"), - ("Failed to connect to relay server", "Αποτυχία σύνδεσης με διακομιστή αναμετάδοσης"), - ("Failed to connect via rendezvous server", "Απέτυχε η σύνδεση μέσω διακομιστή"), - ("Failed to connect via relay server", "Απέτυχε η σύνδεση μέσω διακομιστή αναμετάδοσης"), - ("Failed to make direct connection to remote desktop", "Απέτυχε η απευθείας σύνδεση με τον απομακρυσμένο σταθμό εργασίας"), - ("Set Password", "Ορίστε κωδικό"), - ("OS Password", "Κωδικός πρόσβασης λειτουργικού συστήματος"), - ("install_tip", "Λόγω UAC, το RustDesk ενδέχεται να μην λειτουργεί σωστά σε ορισμένες περιπτώσεις. Για να αποφύγετε το UAC, κάντε κλικ στο κουμπί παρακάτω για να εγκαταστήσετε το RustDesk στο σύστημα"), - ("Click to upgrade", "Κάντε κλικ για αναβάθμιση"), - ("Click to download", "Κάντε κλικ για λήψη"), - ("Click to update", "Κάντε κλικ για ενημέρωση"), - ("Configure", "Διαμόρφωση"), - ("config_acc", "Για τον απομακρυσμένο έλεγχο του υπολογιστή σας, πρέπει να εκχωρήσετε δικαιώματα πρόσβασης στο RustDesk."), - ("config_screen", "Για να αποκτήσετε απομακρυσμένη πρόσβαση στον υπολογιστή σας, πρέπει να εκχωρήσετε το δικαίωμα RustDesk \"Screen Capture\"."), - ("Installing ...", "Εγκατάσταση ..."), - ("Install", "Εγκατάσταση"), - ("Installation", "Εγκατάσταση"), - ("Installation Path", "Διαδρομή εγκατάστασης"), - ("Create start menu shortcuts", "Δημιουργία συντομεύσεων μενού έναρξης"), - ("Create desktop icon", "Δημιουργία εικονιδίου επιφάνειας εργασίας"), - ("agreement_tip", "Με την εγκατάσταση αποδέχεστε την άδεια χρήσης"), - ("Accept and Install", "Αποδοχή και εγκατάσταση"), - ("End-user license agreement", "Σύμβαση άδειας χρήσης τελικού χρήστη"), - ("Generating ...", "Δημιουργία ..."), - ("Your installation is lower version.", "Η έκδοση της εγκατάστασής σας είναι παλαιότερη."), - ("not_close_tcp_tip", "Μην κλείσετε αυτό το παράθυρο ενώ χρησιμοποιείτε το τούνελ."), - ("Listening ...", "Αναμονή ..."), - ("Remote Host", "Απομακρυσμένος υπολογιστής"), - ("Remote Port", "Απομακρυσμένη θύρα"), - ("Action", "Δράση"), - ("Add", "Προσθήκη"), - ("Local Port", "Τοπική θύρα"), - ("Local Address", "Τοπική διεύθυνση"), - ("Change Local Port", "Αλλαγή τοπικής θύρας"), - ("setup_server_tip", "Για πιο γρήγορη σύνδεση, ρυθμίστε τον δικό σας διακομιστή σύνδεσης"), - ("Too short, at least 6 characters.", "Πολύ μικρό, τουλάχιστον 6 χαρακτήρες."), - ("The confirmation is not identical.", "Η επιβεβαίωση δεν είναι πανομοιότυπη."), - ("Permissions", "Άδειες"), - ("Accept", "Αποδοχή"), - ("Dismiss", "Απόρριψη"), - ("Disconnect", "Αποσύνδεση"), - ("Allow using keyboard and mouse", "Να επιτρέπεται η χρήση πληκτρολογίου και ποντικιού"), - ("Allow using clipboard", "Να επιτρέπεται η χρήση του προχείρου"), - ("Allow hearing sound", "Να επιτρέπεται η αναπαραγωγή ήχου"), - ("Allow file copy and paste", "Να επιτρέπεται η αντιγραφή και επικόλληση αρχείου"), - ("Connected", "Συνδεδεμένο"), - ("Direct and encrypted connection", "Άμεση και κρυπτογραφημένη σύνδεση"), - ("Relayed and encrypted connection", "Κρυπτογραφημένη σύνδεση με αναμετάδοση"), - ("Direct and unencrypted connection", "Άμεση και μη κρυπτογραφημένη σύνδεση"), - ("Relayed and unencrypted connection", "Μη κρυπτογραφημένη σύνδεση με αναμετάδοση"), - ("Enter Remote ID", "Εισαγωγή απομακρυσμένου αναγνωριστικού ID"), - ("Enter your password", "Εισάγετε τον κωδικό σας"), - ("Logging in...", "Σύνδεση..."), - ("Enable RDP session sharing", "Ενεργοποίηση κοινής χρήσης RDP"), - ("Auto Login", "Αυτόματη είσοδος"), - ("Enable Direct IP Access", "Ενεργοποίηση άμεσης πρόσβασης IP"), - ("Rename", "Μετονομασία"), - ("Space", "Χώρος"), - ("Create Desktop Shortcut", "Δημιουργία συντόμευσης στην επιφάνεια εργασίας"), - ("Change Path", "Αλλαγή διαδρομής"), - ("Create Folder", "Δημιουργία φακέλου"), - ("Please enter the folder name", "Παρακαλώ εισάγετε το όνομα του φακέλου"), - ("Fix it", "Επιδιόρθωσε το"), - ("Warning", "Προειδοποίηση"), - ("Login screen using Wayland is not supported", "Η οθόνη εισόδου με χρήση του Wayland δεν υποστηρίζεται"), - ("Reboot required", "Απαιτείται επανεκκίνηση"), - ("Unsupported display server ", "Μη υποστηριζόμενος διακομιστής εμφάνισης "), - ("x11 expected", "απαιτείται X11"), - ("Port", "Θύρα"), - ("Settings", "Ρυθμίσεις"), - ("Username", "Όνομα χρήστη"), - ("Invalid port", "Μη έγκυρη θύρα"), - ("Closed manually by the peer", "Έκλεισε από τον απομακρυσμένο σταθμό"), - ("Enable remote configuration modification", "Ενεργοποίηση απομακρυσμένης τροποποίησης ρυθμίσεων"), - ("Run without install", "Εκτέλεση χωρίς εγκατάσταση"), - ("Always connected via relay", "Πάντα συνδεδεμένο μέσω αναμετάδοσης"), - ("Always connect via relay", "Σύνδεση πάντα μέσω αναμετάδοσης"), - ("whitelist_tip", "Μόνο οι IP της λίστας επιτρεπόμενων έχουν πρόσβαση"), - ("Login", "Σύνδεση"), - ("Logout", "Αποσύνδεση"), - ("Tags", "Ετικέτες"), - ("Search ID", "Αναζήτηση ID"), - ("Current Wayland display server is not supported", "Ο τρέχων διακομιστής εμφάνισης Wayland δεν υποστηρίζεται"), - ("whitelist_sep", "Διαχωρίζονται με κόμμα, ερωτηματικό, διάστημα ή νέα γραμμή"), - ("Add ID", "Προσθήκη αναγνωριστικού ID"), - ("Add Tag", "Προσθήκη ετικέτας"), - ("Unselect all tags", "Κατάργηση επιλογής όλων των ετικετών"), - ("Network error", "Σφάλμα δικτύου"), - ("Username missed", "Δεν συμπληρώσατε το όνομα χρήστη"), - ("Password missed", "Δεν συμπληρώσατε τον κωδικό πρόσβασης"), - ("Wrong credentials", "Λάθος διαπιστευτήρια"), - ("Edit Tag", "Επεξεργασία ετικέτας"), - ("Unremember Password", "Διαγραφή απομνημονευμένου κωδικού"), - ("Favorites", "Αγαπημένα"), - ("Add to Favorites", "Προσθήκη στα αγαπημένα"), - ("Remove from Favorites", "Κατάργηση από τα Αγαπημένα"), - ("Empty", "Άδειο"), - ("Invalid folder name", "Μη έγκυρο όνομα φακέλου"), - ("Socks5 Proxy", "Διαμεσολαβητής Socks5"), - ("Hostname", "Όνομα υπολογιστή"), - ("Discovered", "Ανακαλύφθηκε"), - ("install_daemon_tip", "Για να ξεκινά με την εκκίνηση του υπολογιστή, πρέπει να εγκαταστήσετε την υπηρεσία συστήματος"), - ("Remote ID", "Απομακρυσμένο ID"), - ("Paste", "Επικόλληση"), - ("Paste here?", "Επικόλληση εδώ;"), - ("Are you sure to close the connection?", "Είστε βέβαιοι ότι θέλετε να κλείσετε αυτήν τη σύνδεση;"), - ("Download new version", "Λήψη νέας έκδοσης"), - ("Touch mode", "Λειτουργία αφής"), - ("Mouse mode", "Λειτουργία ποντικιού"), - ("One-Finger Tap", "Πάτημα με ένα δάχτυλο"), - ("Left Mouse", "Αριστερό κλικ"), - ("One-Long Tap", "Παρατεταμένο πάτημα με ένα δάχτυλο"), - ("Two-Finger Tap", "Πάτημα με δύο δάχτυλα"), - ("Right Mouse", "Δεξί κλικ"), - ("One-Finger Move", "Κίνηση με ένα δάχτυλο"), - ("Double Tap & Move", "Διπλό πάτημα και μετακίνηση"), - ("Mouse Drag", "Σύρετε το ποντίκι"), - ("Three-Finger vertically", "Τρία δάχτυλα, κάθετα"), - ("Mouse Wheel", "Τροχός ποντικιού"), - ("Two-Finger Move", "Κίνηση με δύο δάχτυλα"), - ("Canvas Move", "Κίνηση καμβά"), - ("Pinch to Zoom", "Τσίμπημα για ζουμ"), - ("Canvas Zoom", "Ζουμ σε καμβά"), - ("Reset canvas", "Επαναφορά καμβά"), - ("No permission of file transfer", "Δεν υπάρχει άδεια για μεταφορά αρχείων"), - ("Note", "Σημείωση"), - ("Connection", "Σύνδεση"), - ("Share Screen", "Κοινή χρήση οθόνης"), - ("CLOSE", "Απενεργοποίηση"), - ("OPEN", "Ενεργοποίηση"), - ("Chat", "Κουβέντα"), - ("Total", "Σύνολο"), - ("items", "στοιχεία"), - ("Selected", "Επιλεγμένο"), - ("Screen Capture", "Αποτύπωση οθόνης"), - ("Input Control", "Έλεγχος εισόδου"), - ("Audio Capture", "Λήψη ήχου"), - ("File Connection", "Σύνδεση αρχείου"), - ("Screen Connection", "Σύνδεση οθόνης"), - ("Do you accept?", "Δέχεσαι;"), - ("Open System Setting", "Άνοιγμα ρυθμίσεων συστήματος"), - ("How to get Android input permission?", "Πώς να αποκτήσω άδεια εισαγωγής Android;"), - ("android_input_permission_tip1", "Για να μπορεί μία απομακρυσμένη συσκευή να ελέγχει τη συσκευή σας Android, πρέπει να επιτρέψετε στο RustDesk να χρησιμοποιεί την υπηρεσία \"Προσβασιμότητα\"."), - ("android_input_permission_tip2", "Παρακαλώ μεταβείτε στην επόμενη σελίδα ρυθμίσεων συστήματος, βρείτε και πληκτρολογήστε [Εγκατεστημένες υπηρεσίες], ενεργοποιήστε την υπηρεσία [Είσοδος RustDesk]."), - ("android_new_connection_tip", "θέλω να ελέγξω τη συσκευή σου."), - ("android_service_will_start_tip", "Η ενεργοποίηση της κοινής χρήσης οθόνης θα ξεκινήσει αυτόματα την υπηρεσία, ώστε άλλες συσκευές να μπορούν να ελέγχουν αυτήν τη συσκευή Android."), - ("android_stop_service_tip", "Η απενεργοποίηση της υπηρεσίας θα αποσυνδέσει αυτόματα όλες τις εγκατεστημένες συνδέσεις."), - ("android_version_audio_tip", "Η έκδοση Android που διαθέτετε δεν υποστηρίζει εγγραφή ήχου, ενημερώστε το σε Android 10 ή νεότερη έκδοση, εάν είναι δυνατόν."), - ("android_start_service_tip", "Πατήστε [Ενεργοποίηση υπηρεσίας] ή ενεργοποιήστε την άδεια [Πρόσβαση στην οθόνη] για να ξεκινήσετε την υπηρεσία κοινής χρήσης οθόνης."), - ("Account", "Λογαριασμός"), - ("Overwrite", "Αντικατάσταση"), - ("This file exists, skip or overwrite this file?", "Αυτό το αρχείο υπάρχει, παράβλεψη ή αντικατάσταση αυτού του αρχείου;"), - ("Quit", "Έξοδος"), - ("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"), - ("Help", "Βοήθεια"), - ("Failed", "Απέτυχε"), - ("Succeeded", "Επιτυχής"), - ("Someone turns on privacy mode, exit", "Κάποιος ενεργοποιεί τη λειτουργία απορρήτου, έξοδος"), - ("Unsupported", "Δεν υποστηρίζεται"), - ("Peer denied", "Ο απομακρυσμένος σταθμός απέρριψε τη σύνδεση"), - ("Please install plugins", "Παρακαλώ εγκαταστήστε πρόσθετα"), - ("Peer exit", "Ο απομακρυσμένος σταθμός έχει αποσυνδεθεί"), - ("Failed to turn off", "Αποτυχία απενεργοποίησης"), - ("Turned off", "Απενεργοποιημένο"), - ("In privacy mode", "Σε λειτουργία απορρήτου"), - ("Out privacy mode", "Εκτός λειτουργίας απορρήτου"), - ("Language", "Γλώσσα"), - ("Keep RustDesk background service", "Εκτέλεση του RustDesk στο παρασκήνιο"), - ("Ignore Battery Optimizations", "Παράβλεψη βελτιστοποιήσεων μπαταρίας"), - ("android_open_battery_optimizations_tip", "Θέλετε να ανοίξετε τις ρυθμίσεις βελτιστοποίησης μπαταρίας;"), - ("Connection not allowed", "Η σύνδεση απορρίφθηκε"), - ("Legacy mode", "Λειτουργία συμβατότητας"), - ("Map mode", "Map mode"), - ("Translate mode", "Λειτουργία μετάφρασης"), - ("Use permanent password", "Χρήση μόνιμου κωδικού πρόσβασης"), - ("Use both passwords", "Χρήση και των δύο κωδικών πρόσβασης"), - ("Set permanent password", "Ορισμός μόνιμου κωδικού πρόσβασης"), - ("Enable Remote Restart", "Ενεργοποίηση απομακρυσμένης επανεκκίνησης"), - ("Allow remote restart", "Να επιτρέπεται η απομακρυσμένη επανεκκίνηση"), - ("Restart Remote Device", "Επανεκκίνηση απομακρυσμένης συσκευής"), - ("Are you sure you want to restart", "Είστε βέβαιοι ότι θέλετε να κάνετε επανεκκίνηση"), - ("Restarting Remote Device", "Επανεκκίνηση απομακρυσμένης συσκευής"), - ("remote_restarting_tip", "Η απομακρυσμένη συσκευή επανεκκινείται, κλείστε αυτό το μήνυμα και επανασυνδεθείτε χρησιμοποιώντας τον μόνιμο κωδικό πρόσβασης."), - ("Copied", "Αντιγράφηκε"), - ("Exit Fullscreen", "Έξοδος από πλήρη οθόνη"), - ("Fullscreen", "Πλήρης οθόνη"), - ("Mobile Actions", "Mobile Actions"), - ("Select Monitor", "Επιλογή οθόνης"), - ("Control Actions", "Ενέργειες ελέγχου"), - ("Display Settings", "Ρυθμίσεις οθόνης"), - ("Ratio", "Αναλογία"), - ("Image Quality", "Ποιότητα εικόνας"), - ("Scroll Style", "Στυλ κύλισης"), - ("Show Menubar", "Εμφάνιση γραμμής μενού"), - ("Hide Menubar", "Απόκρυψη γραμμής μενού"), - ("Direct Connection", "Απευθείας σύνδεση"), - ("Relay Connection", "Αναμεταδιδόμενη σύνδεση"), - ("Secure Connection", "Ασφαλής σύνδεση"), - ("Insecure Connection", "Μη ασφαλής σύνδεση"), - ("Scale original", "Κλιμάκωση πρωτότυπου"), - ("Scale adaptive", "Προσαρμοστική κλίμακα"), - ("General", "Γενικά"), - ("Security", "Ασφάλεια"), - ("Account", "Λογαριασμός"), - ("Theme", "Θέμα"), - ("Dark Theme", "Σκούρο θέμα"), - ("Dark", "Σκούρο"), - ("Light", "Φωτεινό"), - ("Follow System", "Από το σύστημα"), - ("Enable hardware codec", "Ενεργοποίηση κωδικοποιητή υλικού"), - ("Unlock Security Settings", "Ξεκλείδωμα ρυθμίσεων ασφαλείας"), - ("Enable Audio", "Ενεργοποίηση ήχου"), - ("Unlock Network Settings", "Ξεκλείδωμα ρυθμίσεων δικτύου"), - ("Server", "Διακομιστής"), - ("Direct IP Access", "Άμεση πρόσβαση IP"), - ("Proxy", "Διαμεσολαβητής"), - ("Port", "Θύρα"), - ("Apply", "Εφαρμογή"), - ("Disconnect all devices?", "Αποσύνδεση όλων των συσκευών;"), - ("Clear", "Καθαρισμός"), - ("Audio Input Device", "Συσκευή εισόδου ήχου"), - ("Deny remote access", "Απόρριψη απομακρυσμένης πρόσβασης"), - ("Use IP Whitelisting", "Χρήση λίστας επιτρεπόμενων IP"), - ("Network", "Δίκτυο"), - ("Enable RDP", "Ενεργοποίηση RDP"), - ("Pin menubar", "Καρφίτσωμα γραμμής μενού"), - ("Unpin menubar", "Ξεκαρφίτσωμα γραμμής μενού"), - ("Recording", "Εγγραφή"), - ("Directory", "Φάκελος εγγραφών"), - ("Automatically record incoming sessions", "Αυτόματη εγγραφή εισερχόμενων συνεδριών"), - ("Change", "Αλλαγή"), - ("Start session recording", "Έναρξη εγγραφής συνεδρίας"), - ("Stop session recording", "Διακοπή εγγραφής συνεδρίας"), - ("Enable Recording Session", "Ενεργοποίηση εγγραφής συνεδρίας"), - ("Allow recording session", "Να επιτρέπεται η εγγραφή"), - ("Enable LAN Discovery", "Ενεργοποίηση εντοπισμού LAN"), - ("Deny LAN Discovery", "Απαγόρευση εντοπισμού LAN"), - ("Write a message", "Γράψτε ένα μήνυμα"), - ("Prompt", "Προτροπή"), - ("Please wait for confirmation of UAC...", "Παρακαλώ περιμένετε για επιβεβαίωση του UAC..."), - ("elevated_foreground_window_tip", "Το τρέχον παράθυρο της απομακρυσμένης επιφάνειας εργασίας απαιτεί υψηλότερα δικαιώματα για να λειτουργήσει, επομένως δεν μπορεί να χρησιμοποιήσει προσωρινά το ποντίκι και το πληκτρολόγιο. Μπορείτε να ζητήσετε από τον απομακρυσμένο χρήστη να ελαχιστοποιήσει το τρέχον παράθυρο ή να κάνετε κλικ στο κουμπί ανύψωσης στο παράθυρο διαχείρισης σύνδεσης. Για να αποφύγετε αυτό το πρόβλημα, συνιστάται η εγκατάσταση του λογισμικού στην απομακρυσμένη συσκευή."), - ("Disconnected", "Αποσυνδέθηκε"), - ("Other", "Άλλα"), - ("Confirm before closing multiple tabs", "Επιβεβαίωση πριν κλείσετε πολλές καρτέλες"), - ("Keyboard Settings", "Ρυθμίσεις πληκτρολογίου"), - ("Custom", "Προσαρμογή ποιότητας εικόνας"), - ("Full Access", "Πλήρης πρόσβαση"), - ("Screen Share", "Κοινή χρήση οθόνης"), - ("Wayland requires Ubuntu 21.04 or higher version.", "Το Wayland απαιτεί Ubuntu 21.04 ή νεότερη έκδοση."), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Το Wayland απαιτεί υψηλότερη έκδοση του linux distro. Δοκιμάστε την επιφάνεια εργασίας X11 ή αλλάξτε το λειτουργικό σας σύστημα."), - ("JumpLink", "Προβολή"), - ("Please Select the screen to be shared(Operate on the peer side).", "Επιλέξτε την οθόνη που θέλετε να μοιραστείτε (Λειτουργία στην πλευρά του απομακρυσμένου σταθμού)."), - ("Show RustDesk", "Εμφάνιση RustDesk"), - ("This PC", "Αυτός ο υπολογιστής"), - ("or", "ή"), - ("Continue with", "Συνέχεια με"), - ("Elevate", "Ανύψωση"), - ("Zoom cursor", "Μεγέθυνση στον κέρσορα"), - ("Accept sessions via password", "Αποδοχή συνεδριών μέσω κωδικού πρόσβασης"), - ("Accept sessions via click", "Αποδοχή συνεδριών μέσω κλικ"), - ("Accept sessions via both", "Αποδοχή συνεδριών και από τα δύο"), - ("Please wait for the remote side to accept your session request...", "Παρακαλώ περιμένετε μέχρι η απομακρυσμένη πλευρά να αποδεχτεί το αίτημα συνεδρίας σας..."), - ("One-time Password", "Κωδικός μίας χρήσης"), - ("Use one-time password", "Χρήση κωδικού πρόσβασης μίας χρήσης"), - ("One-time password length", "Μήκος κωδικού πρόσβασης μίας χρήσης"), - ("Request access to your device", "Αίτημα πρόσβασης στη συσκευή σας"), - ("Hide connection management window", "Απόκρυψη παραθύρου διαχείρισης σύνδεσης"), - ("hide_cm_tip", "Να επιτρέπεται η απόκρυψη, μόνο εάν αποδέχεστε συνδέσεις μέσω κωδικού πρόσβασης και χρησιμοποιείτε μόνιμο κωδικό πρόσβασης"), + ("Status", "Κατάσταση"), + ("Your Desktop", "Ο σταθμός εργασίας σας"), + ("desk_tip", "Η πρόσβαση στον σταθμό εργασίας σας είναι δυνατή με αυτό το αναγνωριστικό και τον κωδικό πρόσβασης."), + ("Password", "Κωδικός πρόσβασης"), + ("Ready", "Έτοιμο"), + ("Established", "Συνδέθηκε"), + ("connecting_status", "Σύνδεση στο δίκτυο RustDesk..."), + ("Enable Service", "Ενεργοποίηση υπηρεσίας"), + ("Start Service", "Έναρξη υπηρεσίας"), + ("Service is running", "Η υπηρεσία εκτελείται"), + ("Service is not running", "Η υπηρεσία δεν εκτελείται"), + ("not_ready_status", "Δεν είναι έτοιμο. Ελέγξτε τη σύνδεσή σας"), + ("Control Remote Desktop", "Έλεγχος απομακρυσμένου σταθμού εργασίας"), + ("Transfer File", "Μεταφορά αρχείου"), + ("Connect", "Σύνδεση"), + ("Recent Sessions", "Πρόσφατες συνεδρίες"), + ("Address Book", "Βιβλίο διευθύνσεων"), + ("Confirmation", "Επιβεβαίωση"), + ("TCP Tunneling", "TCP Tunneling"), + ("Remove", "Κατάργηση"), + ("Refresh random password", "Νέος τυχαίος κωδικός πρόσβασης"), + ("Set your own password", "Ορίστε τον δικό σας κωδικό πρόσβασης"), + ("Enable Keyboard/Mouse", "Ενεργοποίηση πληκτρολογίου/ποντικιού"), + ("Enable Clipboard", "Ενεργοποίηση Προχείρου"), + ("Enable File Transfer", "Ενεργοποίηση μεταφοράς αρχείων"), + ("Enable TCP Tunneling", "Ενεργοποίηση TCP Tunneling"), + ("IP Whitelisting", "Λίστα επιτρεπόμενων IP"), + ("ID/Relay Server", "Διακομιστής ID/Αναμετάδοσης"), + ("Import Server Config", "Εισαγωγή διαμόρφωσης διακομιστή"), + ("Export Server Config", "Εξαγωγή διαμόρφωσης διακομιστή"), + ("Import server configuration successfully", "Επιτυχής εισαγωγή διαμόρφωσης διακομιστή"), + ("Export server configuration successfully", "Επιτυχής εξαγωγή διαμόρφωσης διακομιστή"), + ("Invalid server configuration", "Μη έγκυρη διαμόρφωση διακομιστή"), + ("Clipboard is empty", "Το πρόχειρο είναι κενό"), + ("Stop service", "Διακοπή υπηρεσίας"), + ("Change ID", "Αλλαγή αναγνωριστικού ID"), + ("Website", "Ιστότοπος"), + ("About", "Πληροφορίες"), + ("Mute", "Σίγαση"), + ("Audio Input", "Είσοδος ήχου"), + ("Enhancements", "Βελτιώσεις"), + ("Hardware Codec", "Κωδικοποιητής υλικού"), + ("Adaptive Bitrate", "Adaptive Bitrate"), + ("ID Server", "Διακομιστής ID"), + ("Relay Server", "Διακομιστής αναμετάδοσης"), + ("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", "Μη διαθέσιμο"), + ("Too frequent", "Πολύ συχνά"), + ("Cancel", "Ακύρωση"), + ("Skip", "Παράλειψη"), + ("Close", "Κλείσιμο"), + ("Retry", "Δοκίμασε ξανά"), + ("OK", "Εντάξει"), + ("Password Required", "Απαιτείται κωδικός πρόσβασης"), + ("Please enter your password", "Παρακαλώ εισάγετε τον κωδικό πρόσβασης"), + ("Remember password", "Απομνημόνευση κωδικού πρόσβασης"), + ("Wrong Password", "Λάθος κωδικός πρόσβασης"), + ("Do you want to enter again?", "Επανασύνδεση;"), + ("Connection Error", "Σφάλμα σύνδεσης"), + ("Error", "Σφάλμα"), + ("Reset by the peer", "Η σύνδεση επαναφέρθηκε από τον απομακρυσμένο σταθμό"), + ("Connecting...", "Σύνδεση..."), + ("Connection in progress. Please wait.", "Σύνδεση σε εξέλιξη. Παρακαλώ περιμένετε."), + ("Please try 1 minute later", "Παρακαλώ ξαναδοκιμάστε σε 1 λεπτό"), + ("Login Error", "Σφάλμα εισόδου"), + ("Successful", "Επιτυχής"), + ("Connected, waiting for image...", "Συνδέθηκε, αναμονή για εικόνα..."), + ("Name", "Όνομα"), + ("Type", "Τύπος"), + ("Modified", "Τροποποιήθηκε"), + ("Size", "Μέγεθος"), + ("Show Hidden Files", "Εμφάνιση κρυφών αρχείων"), + ("Receive", "Λήψη"), + ("Send", "Αποστολή"), + ("Refresh File", "Ανανέωση αρχείου"), + ("Local", "Τοπικό"), + ("Remote", "Απομακρυσμένο"), + ("Remote Computer", "Απομακρυσμένος υπολογιστής"), + ("Local Computer", "Τοπικός υπολογιστής"), + ("Confirm Delete", "Επιβεβαίωση διαγραφής"), + ("Delete", "Διαγραφή"), + ("Properties", "Ιδιότητες"), + ("Multi Select", "Πολλαπλή επιλογή"), + ("Select All", "Επιλογή όλων"), + ("Unselect All", "Κατάργηση επιλογής όλων"), + ("Empty Directory", "Κενός φάκελος"), + ("Not an empty directory", "Ο φάκελος δεν είναι κενός"), + ("Are you sure you want to delete this file?", "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτό το αρχείο;"), + ("Are you sure you want to delete this empty directory?", "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτόν τον κενό φάκελο;"), + ("Are you sure you want to delete the file of this directory?", "Είστε βέβαιοι ότι θέλετε να διαγράψετε το αρχείο αυτού του φακέλου;"), + ("Do this for all conflicts", "Κάνε αυτό για όλες τις διενέξεις"), + ("This is irreversible!", "Αυτό είναι μη αναστρέψιμο!"), + ("Deleting", "Διαγραφή"), + ("files", "αρχεία"), + ("Waiting", "Αναμονή"), + ("Finished", "Ολοκληρώθηκε"), + ("Speed", "Ταχύτητα"), + ("Custom Image Quality", "Προσαρμοσμένη ποιότητα εικόνας"), + ("Privacy mode", "Λειτουργία απορρήτου"), + ("Block user input", "Αποκλεισμός χειρισμού από τον χρήστη"), + ("Unblock user input", "Κατάργηση αποκλεισμού χειρισμού από τον χρήστη"), + ("Adjust Window", "Προσαρμογή παραθύρου"), + ("Original", "Πρωτότυπο"), + ("Shrink", "Συρρίκνωση"), + ("Stretch", "Προσαρμογή"), + ("Scrollbar", "Γραμμή κύλισης"), + ("ScrollAuto", "Αυτόματη κύλιση"), + ("Good image quality", "Καλή ποιότητα εικόνας"), + ("Balanced", "Ισορροπημένο"), + ("Optimize reaction time", "Βελτιστοποίηση χρόνου αντίδρασης"), + ("Custom", "Προσαρμογή ποιότητας εικόνας"), + ("Show remote cursor", "Εμφάνιση απομακρυσμένου κέρσορα"), + ("Show quality monitor", "Εμφάνιση παρακολούθησης ποιότητας σύνδεσης"), + ("Disable clipboard", "Απενεργοποίηση προχείρου"), + ("Lock after session end", "Κλείδωμα μετά το τέλος της συνεδρίας"), + ("Insert", "Εισάγετε"), + ("Insert Lock", "Εισαγωγή κλειδαριάς"), + ("Refresh", "Ανανέωση"), + ("ID does not exist", "Το αναγνωριστικό ID δεν υπάρχει"), + ("Failed to connect to rendezvous server", "Αποτυχία σύνδεσης με διακομιστή"), + ("Please try later", "Παρακαλώ δοκιμάστε αργότερα"), + ("Remote desktop is offline", "Ο απομακρυσμένος σταθμός εργασίας είναι εκτός σύνδεσης"), + ("Key mismatch", "Μη έγκυρο κλειδί"), + ("Timeout", "Τέλος χρόνου"), + ("Failed to connect to relay server", "Αποτυχία σύνδεσης με διακομιστή αναμετάδοσης"), + ("Failed to connect via rendezvous server", "Απέτυχε η σύνδεση μέσω διακομιστή"), + ("Failed to connect via relay server", "Απέτυχε η σύνδεση μέσω διακομιστή αναμετάδοσης"), + ("Failed to make direct connection to remote desktop", "Απέτυχε η απευθείας σύνδεση με τον απομακρυσμένο σταθμό εργασίας"), + ("Set Password", "Ορίστε κωδικό"), + ("OS Password", "Κωδικός πρόσβασης λειτουργικού συστήματος"), + ("install_tip", "Λόγω UAC, το RustDesk ενδέχεται να μην λειτουργεί σωστά σε ορισμένες περιπτώσεις. Για να αποφύγετε το UAC, κάντε κλικ στο κουμπί παρακάτω για να εγκαταστήσετε το RustDesk στο σύστημα"), + ("Click to upgrade", "Κάντε κλικ για αναβάθμιση"), + ("Click to download", "Κάντε κλικ για λήψη"), + ("Click to update", "Κάντε κλικ για ενημέρωση"), + ("Configure", "Διαμόρφωση"), + ("config_acc", "Για τον απομακρυσμένο έλεγχο του υπολογιστή σας, πρέπει να εκχωρήσετε δικαιώματα πρόσβασης στο RustDesk."), + ("config_screen", "Για να αποκτήσετε απομακρυσμένη πρόσβαση στον υπολογιστή σας, πρέπει να εκχωρήσετε το δικαίωμα RustDesk \"Screen Capture\"."), + ("Installing ...", "Εγκατάσταση ..."), + ("Install", "Εγκατάσταση"), + ("Installation", "Εγκατάσταση"), + ("Installation Path", "Διαδρομή εγκατάστασης"), + ("Create start menu shortcuts", "Δημιουργία συντομεύσεων μενού έναρξης"), + ("Create desktop icon", "Δημιουργία εικονιδίου επιφάνειας εργασίας"), + ("agreement_tip", "Με την εγκατάσταση αποδέχεστε την άδεια χρήσης"), + ("Accept and Install", "Αποδοχή και εγκατάσταση"), + ("End-user license agreement", "Σύμβαση άδειας χρήσης τελικού χρήστη"), + ("Generating ...", "Δημιουργία ..."), + ("Your installation is lower version.", "Η έκδοση της εγκατάστασής σας είναι παλαιότερη."), + ("not_close_tcp_tip", "Μην κλείσετε αυτό το παράθυρο ενώ χρησιμοποιείτε το τούνελ."), + ("Listening ...", "Αναμονή ..."), + ("Remote Host", "Απομακρυσμένος υπολογιστής"), + ("Remote Port", "Απομακρυσμένη θύρα"), + ("Action", "Δράση"), + ("Add", "Προσθήκη"), + ("Local Port", "Τοπική θύρα"), + ("Local Address", "Τοπική διεύθυνση"), + ("Change Local Port", "Αλλαγή τοπικής θύρας"), + ("setup_server_tip", "Για πιο γρήγορη σύνδεση, ρυθμίστε τον δικό σας διακομιστή σύνδεσης"), + ("Too short, at least 6 characters.", "Πολύ μικρό, τουλάχιστον 6 χαρακτήρες."), + ("The confirmation is not identical.", "Η επιβεβαίωση δεν είναι πανομοιότυπη."), + ("Permissions", "Άδειες"), + ("Accept", "Αποδοχή"), + ("Dismiss", "Απόρριψη"), + ("Disconnect", "Αποσύνδεση"), + ("Allow using keyboard and mouse", "Να επιτρέπεται η χρήση πληκτρολογίου και ποντικιού"), + ("Allow using clipboard", "Να επιτρέπεται η χρήση του προχείρου"), + ("Allow hearing sound", "Να επιτρέπεται η αναπαραγωγή ήχου"), + ("Allow file copy and paste", "Να επιτρέπεται η αντιγραφή και επικόλληση αρχείου"), + ("Connected", "Συνδεδεμένο"), + ("Direct and encrypted connection", "Άμεση και κρυπτογραφημένη σύνδεση"), + ("Relayed and encrypted connection", "Κρυπτογραφημένη σύνδεση με αναμετάδοση"), + ("Direct and unencrypted connection", "Άμεση και μη κρυπτογραφημένη σύνδεση"), + ("Relayed and unencrypted connection", "Μη κρυπτογραφημένη σύνδεση με αναμετάδοση"), + ("Enter Remote ID", "Εισαγωγή απομακρυσμένου αναγνωριστικού ID"), + ("Enter your password", "Εισάγετε τον κωδικό σας"), + ("Logging in...", "Σύνδεση..."), + ("Enable RDP session sharing", "Ενεργοποίηση κοινής χρήσης RDP"), + ("Auto Login", "Αυτόματη είσοδος"), + ("Enable Direct IP Access", "Ενεργοποίηση άμεσης πρόσβασης IP"), + ("Rename", "Μετονομασία"), + ("Space", "Χώρος"), + ("Create Desktop Shortcut", "Δημιουργία συντόμευσης στην επιφάνεια εργασίας"), + ("Change Path", "Αλλαγή διαδρομής"), + ("Create Folder", "Δημιουργία φακέλου"), + ("Please enter the folder name", "Παρακαλώ εισάγετε το όνομα του φακέλου"), + ("Fix it", "Επιδιόρθωσε το"), + ("Warning", "Προειδοποίηση"), + ("Login screen using Wayland is not supported", "Η οθόνη εισόδου με χρήση του Wayland δεν υποστηρίζεται"), + ("Reboot required", "Απαιτείται επανεκκίνηση"), + ("Unsupported display server ", "Μη υποστηριζόμενος διακομιστής εμφάνισης "), + ("x11 expected", "απαιτείται X11"), + ("Port", "Θύρα"), + ("Settings", "Ρυθμίσεις"), + ("Username", "Όνομα χρήστη"), + ("Invalid port", "Μη έγκυρη θύρα"), + ("Closed manually by the peer", "Έκλεισε από τον απομακρυσμένο σταθμό"), + ("Enable remote configuration modification", "Ενεργοποίηση απομακρυσμένης τροποποίησης ρυθμίσεων"), + ("Run without install", "Εκτέλεση χωρίς εγκατάσταση"), + ("Always connected via relay", "Πάντα συνδεδεμένο μέσω αναμετάδοσης"), + ("Always connect via relay", "Σύνδεση πάντα μέσω αναμετάδοσης"), + ("whitelist_tip", "Μόνο οι IP της λίστας επιτρεπόμενων έχουν πρόσβαση"), + ("Login", "Σύνδεση"), + ("Logout", "Αποσύνδεση"), + ("Tags", "Ετικέτες"), + ("Search ID", "Αναζήτηση ID"), + ("Current Wayland display server is not supported", "Ο τρέχων διακομιστής εμφάνισης Wayland δεν υποστηρίζεται"), + ("whitelist_sep", "Διαχωρίζονται με κόμμα, ερωτηματικό, διάστημα ή νέα γραμμή"), + ("Add ID", "Προσθήκη αναγνωριστικού ID"), + ("Add Tag", "Προσθήκη ετικέτας"), + ("Unselect all tags", "Κατάργηση επιλογής όλων των ετικετών"), + ("Network error", "Σφάλμα δικτύου"), + ("Username missed", "Δεν συμπληρώσατε το όνομα χρήστη"), + ("Password missed", "Δεν συμπληρώσατε τον κωδικό πρόσβασης"), + ("Wrong credentials", "Λάθος διαπιστευτήρια"), + ("Edit Tag", "Επεξεργασία ετικέτας"), + ("Unremember Password", "Διαγραφή απομνημονευμένου κωδικού"), + ("Favorites", "Αγαπημένα"), + ("Add to Favorites", "Προσθήκη στα αγαπημένα"), + ("Remove from Favorites", "Κατάργηση από τα Αγαπημένα"), + ("Empty", "Άδειο"), + ("Invalid folder name", "Μη έγκυρο όνομα φακέλου"), + ("Socks5 Proxy", "Διαμεσολαβητής Socks5"), + ("Hostname", "Όνομα υπολογιστή"), + ("Discovered", "Ανακαλύφθηκε"), + ("install_daemon_tip", "Για να ξεκινά με την εκκίνηση του υπολογιστή, πρέπει να εγκαταστήσετε την υπηρεσία συστήματος"), + ("Remote ID", "Απομακρυσμένο ID"), + ("Paste", "Επικόλληση"), + ("Paste here?", "Επικόλληση εδώ;"), + ("Are you sure to close the connection?", "Είστε βέβαιοι ότι θέλετε να κλείσετε αυτήν τη σύνδεση;"), + ("Download new version", "Λήψη νέας έκδοσης"), + ("Touch mode", "Λειτουργία αφής"), + ("Mouse mode", "Λειτουργία ποντικιού"), + ("One-Finger Tap", "Πάτημα με ένα δάχτυλο"), + ("Left Mouse", "Αριστερό κλικ"), + ("One-Long Tap", "Παρατεταμένο πάτημα με ένα δάχτυλο"), + ("Two-Finger Tap", "Πάτημα με δύο δάχτυλα"), + ("Right Mouse", "Δεξί κλικ"), + ("One-Finger Move", "Κίνηση με ένα δάχτυλο"), + ("Double Tap & Move", "Διπλό πάτημα και μετακίνηση"), + ("Mouse Drag", "Σύρετε το ποντίκι"), + ("Three-Finger vertically", "Τρία δάχτυλα, κάθετα"), + ("Mouse Wheel", "Τροχός ποντικιού"), + ("Two-Finger Move", "Κίνηση με δύο δάχτυλα"), + ("Canvas Move", "Κίνηση καμβά"), + ("Pinch to Zoom", "Τσίμπημα για ζουμ"), + ("Canvas Zoom", "Ζουμ σε καμβά"), + ("Reset canvas", "Επαναφορά καμβά"), + ("No permission of file transfer", "Δεν υπάρχει άδεια για μεταφορά αρχείων"), + ("Note", "Σημείωση"), + ("Connection", "Σύνδεση"), + ("Share Screen", "Κοινή χρήση οθόνης"), + ("CLOSE", "Απενεργοποίηση"), + ("OPEN", "Ενεργοποίηση"), + ("Chat", "Κουβέντα"), + ("Total", "Σύνολο"), + ("items", "στοιχεία"), + ("Selected", "Επιλεγμένο"), + ("Screen Capture", "Αποτύπωση οθόνης"), + ("Input Control", "Έλεγχος εισόδου"), + ("Audio Capture", "Λήψη ήχου"), + ("File Connection", "Σύνδεση αρχείου"), + ("Screen Connection", "Σύνδεση οθόνης"), + ("Do you accept?", "Δέχεσαι;"), + ("Open System Setting", "Άνοιγμα ρυθμίσεων συστήματος"), + ("How to get Android input permission?", "Πώς να αποκτήσω άδεια εισαγωγής Android;"), + ("android_input_permission_tip1", "Για να μπορεί μία απομακρυσμένη συσκευή να ελέγχει τη συσκευή σας Android, πρέπει να επιτρέψετε στο RustDesk να χρησιμοποιεί την υπηρεσία \"Προσβασιμότητα\"."), + ("android_input_permission_tip2", "Παρακαλώ μεταβείτε στην επόμενη σελίδα ρυθμίσεων συστήματος, βρείτε και πληκτρολογήστε [Εγκατεστημένες υπηρεσίες], ενεργοποιήστε την υπηρεσία [Είσοδος RustDesk]."), + ("android_new_connection_tip", "θέλω να ελέγξω τη συσκευή σου."), + ("android_service_will_start_tip", "Η ενεργοποίηση της κοινής χρήσης οθόνης θα ξεκινήσει αυτόματα την υπηρεσία, ώστε άλλες συσκευές να μπορούν να ελέγχουν αυτήν τη συσκευή Android."), + ("android_stop_service_tip", "Η απενεργοποίηση της υπηρεσίας θα αποσυνδέσει αυτόματα όλες τις εγκατεστημένες συνδέσεις."), + ("android_version_audio_tip", "Η έκδοση Android που διαθέτετε δεν υποστηρίζει εγγραφή ήχου, ενημερώστε το σε Android 10 ή νεότερη έκδοση, εάν είναι δυνατόν."), + ("android_start_service_tip", "Πατήστε [Ενεργοποίηση υπηρεσίας] ή ενεργοποιήστε την άδεια [Πρόσβαση στην οθόνη] για να ξεκινήσετε την υπηρεσία κοινής χρήσης οθόνης."), + ("Account", "Λογαριασμός"), + ("Overwrite", "Αντικατάσταση"), + ("This file exists, skip or overwrite this file?", "Αυτό το αρχείο υπάρχει, παράβλεψη ή αντικατάσταση αυτού του αρχείου;"), + ("Quit", "Έξοδος"), + ("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"), + ("Help", "Βοήθεια"), + ("Failed", "Απέτυχε"), + ("Succeeded", "Επιτυχής"), + ("Someone turns on privacy mode, exit", "Κάποιος ενεργοποιεί τη λειτουργία απορρήτου, έξοδος"), + ("Unsupported", "Δεν υποστηρίζεται"), + ("Peer denied", "Ο απομακρυσμένος σταθμός απέρριψε τη σύνδεση"), + ("Please install plugins", "Παρακαλώ εγκαταστήστε πρόσθετα"), + ("Peer exit", "Ο απομακρυσμένος σταθμός έχει αποσυνδεθεί"), + ("Failed to turn off", "Αποτυχία απενεργοποίησης"), + ("Turned off", "Απενεργοποιημένο"), + ("In privacy mode", "Σε λειτουργία απορρήτου"), + ("Out privacy mode", "Εκτός λειτουργίας απορρήτου"), + ("Language", "Γλώσσα"), + ("Keep RustDesk background service", "Εκτέλεση του RustDesk στο παρασκήνιο"), + ("Ignore Battery Optimizations", "Παράβλεψη βελτιστοποιήσεων μπαταρίας"), + ("android_open_battery_optimizations_tip", "Θέλετε να ανοίξετε τις ρυθμίσεις βελτιστοποίησης μπαταρίας;"), + ("Connection not allowed", "Η σύνδεση απορρίφθηκε"), + ("Legacy mode", "Λειτουργία συμβατότητας"), + ("Map mode", "Map mode"), + ("Translate mode", "Λειτουργία μετάφρασης"), + ("Use permanent password", "Χρήση μόνιμου κωδικού πρόσβασης"), + ("Use both passwords", "Χρήση και των δύο κωδικών πρόσβασης"), + ("Set permanent password", "Ορισμός μόνιμου κωδικού πρόσβασης"), + ("Enable Remote Restart", "Ενεργοποίηση απομακρυσμένης επανεκκίνησης"), + ("Allow remote restart", "Να επιτρέπεται η απομακρυσμένη επανεκκίνηση"), + ("Restart Remote Device", "Επανεκκίνηση απομακρυσμένης συσκευής"), + ("Are you sure you want to restart", "Είστε βέβαιοι ότι θέλετε να κάνετε επανεκκίνηση"), + ("Restarting Remote Device", "Επανεκκίνηση απομακρυσμένης συσκευής"), + ("remote_restarting_tip", "Η απομακρυσμένη συσκευή επανεκκινείται, κλείστε αυτό το μήνυμα και επανασυνδεθείτε χρησιμοποιώντας τον μόνιμο κωδικό πρόσβασης."), + ("Copied", "Αντιγράφηκε"), + ("Exit Fullscreen", "Έξοδος από πλήρη οθόνη"), + ("Fullscreen", "Πλήρης οθόνη"), + ("Mobile Actions", "Mobile Actions"), + ("Select Monitor", "Επιλογή οθόνης"), + ("Control Actions", "Ενέργειες ελέγχου"), + ("Display Settings", "Ρυθμίσεις οθόνης"), + ("Ratio", "Αναλογία"), + ("Image Quality", "Ποιότητα εικόνας"), + ("Scroll Style", "Στυλ κύλισης"), + ("Show Menubar", "Εμφάνιση γραμμής μενού"), + ("Hide Menubar", "Απόκρυψη γραμμής μενού"), + ("Direct Connection", "Απευθείας σύνδεση"), + ("Relay Connection", "Αναμεταδιδόμενη σύνδεση"), + ("Secure Connection", "Ασφαλής σύνδεση"), + ("Insecure Connection", "Μη ασφαλής σύνδεση"), + ("Scale original", "Κλιμάκωση πρωτότυπου"), + ("Scale adaptive", "Προσαρμοστική κλίμακα"), + ("General", "Γενικά"), + ("Security", "Ασφάλεια"), + ("Account", "Λογαριασμός"), + ("Theme", "Θέμα"), + ("Dark Theme", "Σκούρο θέμα"), + ("Dark", "Σκούρο"), + ("Light", "Φωτεινό"), + ("Follow System", "Από το σύστημα"), + ("Enable hardware codec", "Ενεργοποίηση κωδικοποιητή υλικού"), + ("Unlock Security Settings", "Ξεκλείδωμα ρυθμίσεων ασφαλείας"), + ("Enable Audio", "Ενεργοποίηση ήχου"), + ("Unlock Network Settings", "Ξεκλείδωμα ρυθμίσεων δικτύου"), + ("Server", "Διακομιστής"), + ("Direct IP Access", "Άμεση πρόσβαση IP"), + ("Proxy", "Διαμεσολαβητής"), + ("Port", "Θύρα"), + ("Apply", "Εφαρμογή"), + ("Disconnect all devices?", "Αποσύνδεση όλων των συσκευών;"), + ("Clear", "Καθαρισμός"), + ("Audio Input Device", "Συσκευή εισόδου ήχου"), + ("Deny remote access", "Απόρριψη απομακρυσμένης πρόσβασης"), + ("Use IP Whitelisting", "Χρήση λίστας επιτρεπόμενων IP"), + ("Network", "Δίκτυο"), + ("Enable RDP", "Ενεργοποίηση RDP"), + ("Pin menubar", "Καρφίτσωμα γραμμής μενού"), + ("Unpin menubar", "Ξεκαρφίτσωμα γραμμής μενού"), + ("Recording", "Εγγραφή"), + ("Directory", "Φάκελος εγγραφών"), + ("Automatically record incoming sessions", "Αυτόματη εγγραφή εισερχόμενων συνεδριών"), + ("Change", "Αλλαγή"), + ("Start session recording", "Έναρξη εγγραφής συνεδρίας"), + ("Stop session recording", "Διακοπή εγγραφής συνεδρίας"), + ("Enable Recording Session", "Ενεργοποίηση εγγραφής συνεδρίας"), + ("Allow recording session", "Να επιτρέπεται η εγγραφή"), + ("Enable LAN Discovery", "Ενεργοποίηση εντοπισμού LAN"), + ("Deny LAN Discovery", "Απαγόρευση εντοπισμού LAN"), + ("Write a message", "Γράψτε ένα μήνυμα"), + ("Prompt", "Προτροπή"), + ("Please wait for confirmation of UAC...", "Παρακαλώ περιμένετε για επιβεβαίωση του UAC..."), + ("elevated_foreground_window_tip", "Το τρέχον παράθυρο της απομακρυσμένης επιφάνειας εργασίας απαιτεί υψηλότερα δικαιώματα για να λειτουργήσει, επομένως δεν μπορεί να χρησιμοποιήσει προσωρινά το ποντίκι και το πληκτρολόγιο. Μπορείτε να ζητήσετε από τον απομακρυσμένο χρήστη να ελαχιστοποιήσει το τρέχον παράθυρο ή να κάνετε κλικ στο κουμπί ανύψωσης στο παράθυρο διαχείρισης σύνδεσης. Για να αποφύγετε αυτό το πρόβλημα, συνιστάται η εγκατάσταση του λογισμικού στην απομακρυσμένη συσκευή."), + ("Disconnected", "Αποσυνδέθηκε"), + ("Other", "Άλλα"), + ("Confirm before closing multiple tabs", "Επιβεβαίωση πριν κλείσετε πολλές καρτέλες"), + ("Keyboard Settings", "Ρυθμίσεις πληκτρολογίου"), + ("Custom", "Προσαρμογή ποιότητας εικόνας"), + ("Full Access", "Πλήρης πρόσβαση"), + ("Screen Share", "Κοινή χρήση οθόνης"), + ("Wayland requires Ubuntu 21.04 or higher version.", "Το Wayland απαιτεί Ubuntu 21.04 ή νεότερη έκδοση."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Το Wayland απαιτεί υψηλότερη έκδοση του linux distro. Δοκιμάστε την επιφάνεια εργασίας X11 ή αλλάξτε το λειτουργικό σας σύστημα."), + ("JumpLink", "Προβολή"), + ("Please Select the screen to be shared(Operate on the peer side).", "Επιλέξτε την οθόνη που θέλετε να μοιραστείτε (Λειτουργία στην πλευρά του απομακρυσμένου σταθμού)."), + ("Show RustDesk", "Εμφάνιση RustDesk"), + ("This PC", "Αυτός ο υπολογιστής"), + ("or", "ή"), + ("Continue with", "Συνέχεια με"), + ("Elevate", "Ανύψωση"), + ("Zoom cursor", "Μεγέθυνση στον κέρσορα"), + ("Accept sessions via password", "Αποδοχή συνεδριών μέσω κωδικού πρόσβασης"), + ("Accept sessions via click", "Αποδοχή συνεδριών μέσω κλικ"), + ("Accept sessions via both", "Αποδοχή συνεδριών και από τα δύο"), + ("Please wait for the remote side to accept your session request...", "Παρακαλώ περιμένετε μέχρι η απομακρυσμένη πλευρά να αποδεχτεί το αίτημα συνεδρίας σας..."), + ("One-time Password", "Κωδικός μίας χρήσης"), + ("Use one-time password", "Χρήση κωδικού πρόσβασης μίας χρήσης"), + ("One-time password length", "Μήκος κωδικού πρόσβασης μίας χρήσης"), + ("Request access to your device", "Αίτημα πρόσβασης στη συσκευή σας"), + ("Hide connection management window", "Απόκρυψη παραθύρου διαχείρισης σύνδεσης"), + ("hide_cm_tip", "Να επιτρέπεται η απόκρυψη, μόνο εάν αποδέχεστε συνδέσεις μέσω κωδικού πρόσβασης και χρησιμοποιείτε μόνιμο κωδικό πρόσβασης"), + ("wayland_experiment_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 417c83f45..51f072e16 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -397,5 +397,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request access to your device", ""), ("Hide connection management window", ""), ("hide_cm_tip", ""), + ("wayland_experiment_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index b76bb687d..b2492553d 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -397,5 +397,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request access to your device", ""), ("Hide connection management window", ""), ("hide_cm_tip", ""), + ("wayland_experiment_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 523386eb5..ec993ad04 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -116,7 +116,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Good image quality", "Qualità immagine migliore"), ("Balanced", "Bilanciato"), ("Optimize reaction time", "Ottimizza il tempo di reazione"), - ("Custom", "Personalizza"), + ("Custom", "Personalizzato"), ("Show remote cursor", "Mostra il cursore remoto"), ("Show quality monitor", "Visualizza qualità video"), ("Disable clipboard", "Disabilita appunti"), @@ -278,7 +278,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_stop_service_tip", "La chiusura del servizio chiuderà automaticamente tutte le connessioni stabilite."), ("android_version_audio_tip", "L'attuale versione di Android non supporta l'acquisizione audio, esegui l'upgrade ad Android 10 o versioni successive."), ("android_start_service_tip", "Toccare [Avvia servizio] o APRI l'autorizzazione [Cattura schermo] per avviare il servizio di condivisione dello schermo."), - ("Account", ""), + ("Account", "Account"), ("Overwrite", "Sovrascrivi"), ("This file exists, skip or overwrite this file?", "Questo file esiste, saltare o sovrascrivere questo file?"), ("Quit", "Esci"), @@ -397,5 +397,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request access to your device", "Richiedi l'accesso al tuo dispositivo"), ("Hide connection management window", "Nascondi la finestra di gestione delle connessioni"), ("hide_cm_tip", "Permetti di nascondere solo se si accettano sessioni con password permanente"), + ("wayland_experiment_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 8d806416d..5cd6d8d6d 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -397,5 +397,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request access to your device", ""), ("Hide connection management window", ""), ("hide_cm_tip", ""), + ("wayland_experiment_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 9f8027be7..31d69841c 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -397,5 +397,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request access to your device", ""), ("Hide connection management window", ""), ("hide_cm_tip", ""), + ("wayland_experiment_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 3a8c27cf3..c32fa778d 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -397,5 +397,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request access to your device", ""), ("Hide connection management window", ""), ("hide_cm_tip", ""), + ("wayland_experiment_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index dae77ed88..edd9a4e45 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -397,5 +397,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request access to your device", "Żądanie dostępu do Twojego urządzenia"), ("Hide connection management window", ""), ("hide_cm_tip", ""), + ("wayland_experiment_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index bc5fbbdfd..1c174af8d 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -116,7 +116,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Good image quality", "Qualidade visual boa"), ("Balanced", "Equilibrada"), ("Optimize reaction time", "Optimizar tempo de reacção"), - ("Custom", "Personalizado"), + ("Custom", ""), ("Show remote cursor", "Mostrar cursor remoto"), ("Show quality monitor", ""), ("Disable clipboard", "Desabilitar área de transferência"), @@ -197,7 +197,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Reboot required", "Reinicialização necessária"), ("Unsupported display server ", "Servidor de display não suportado"), ("x11 expected", "x11 em falha"), - ("Port", "Porta"), + ("Port", ""), ("Settings", "Configurações"), ("Username", "Nome de utilizador"), ("Invalid port", "Porta inválida"), @@ -397,5 +397,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request access to your device", ""), ("Hide connection management window", ""), ("hide_cm_tip", ""), + ("wayland_experiment_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 0d77eb905..86bd1b77f 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -397,5 +397,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request access to your device", "Solicitar acesso ao seu dispositivo"), ("Hide connection management window", ""), ("hide_cm_tip", ""), + ("wayland_experiment_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 81a427d51..f52a6de0b 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -397,5 +397,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request access to your device", "Запрос доступа к вашему устройству"), ("Hide connection management window", "Скрывать окно управления соединениями"), ("hide_cm_tip", "Разрешать скрытие случае, если принимаются сеансы по паролю или используется постоянный пароль"), + ("wayland_experiment_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 33f2be7ab..194b3dc83 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -397,5 +397,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request access to your device", ""), ("Hide connection management window", ""), ("hide_cm_tip", ""), + ("wayland_experiment_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 8076dc21f..a7cb45443 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -23,18 +23,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Remove", "Hiqni"), ("Refresh random password", "Rifreskoni fjalëkalimin e rastësishëm"), ("Set your own password", "Vendosni fjalëkalimin tuaj"), - ("Enable Keyboard/Mouse","Aktivizoni Tastierën/Mousin"), + ("Enable Keyboard/Mouse", "Aktivizoni Tastierën/Mousin"), ("Enable Clipboard", "Aktivizo"), ("Enable File Transfer", "Aktivizoni transferimin e skedarëve"), ("Enable TCP Tunneling", "Aktivizoni TCP Tunneling"), - ("IP whitelisting", "Lista e bardhë IP"), + ("IP Whitelisting", ""), ("ID/Relay Server", "ID/server rele"), ("Import Server Config", "Konfigurimi i severit të importit"), ("Export Server Config", "Konfigurimi i severit të eksportit"), ("Import server configuration successfully", "Konfigurimi i severit të importit i suksesshëm"), ("Export server configuration successfully", "Konfigurimi i severit të eksprotit i suksesshëm"), ("Invalid server configuration", "Konfigurim i pavlefshëm i serverit"), - ("Clipboard is empty","Clipboard është bosh"), + ("Clipboard is empty", "Clipboard është bosh"), ("Stop service", "Ndaloni shërbimin"), ("Change ID", "Ndryshoni ID"), ("Website", "Faqe ëebi"), @@ -116,7 +116,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Good image quality", "Cilësi e mirë imazhi"), ("Balanced", "E balancuar"), ("Optimize reaction time", "Optimizo kohën e reagimit"), - ("Custom", "E personalizuar"), + ("Custom", "Personalizuar"), ("Show remote cursor", "Shfaq kursorin në distancë"), ("Show quality monitor", "Shaq cilësinë e monitorit"), ("Disable clipboard", "Ç'aktivizo clipboard"), @@ -132,7 +132,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Timeout", "Koha mbaroi"), ("Failed to connect to relay server", "Lidhja me serverin transmetues dështoi"), ("Failed to connect via rendezvous server", "Lidhja nëpërmjet serverit të takimit dështoi"), - ("Failed to connect via relay server","Lidhja nëpërmjet serverit të transmetimit dështoi"), + ("Failed to connect via relay server", "Lidhja nëpërmjet serverit të transmetimit dështoi"), ("Failed to make direct connection to remote desktop", "Lidhja direkte me desktopin në distancë dështoi"), ("Set Password", "Vendosni fjalëkalimin"), ("OS Password", "OS fjalëkalim"), @@ -278,7 +278,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_stop_service_tip", "Mbyllja e shërbimit do të mbyllë automatikisht të gjitha lidhjet e vendosura."), ("android_version_audio_tip", "Versioni aktual i Android nuk mbështet regjistrimin e audios, ju lutemi përmirësoni në Android 10 ose më të lartë."), ("android_start_service_tip", "Shtyp [Fillo Shërbimin] ose HAP lejen e [Kapjen e Ekranit] për të nisur shërbimin e ndarjes së ekranit."), - ("Account", "Llogari"), + ("Account", "Llogaria"), ("Overwrite", "Përshkruaj"), ("This file exists, skip or overwrite this file?", "Ky skedar ekziston , tejkalo ose përshkruaj këtë skedarë"), ("Quit", "Hiq"), @@ -397,5 +397,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request access to your device", "Kërko akses në pajisjejn tuaj"), ("Hide connection management window", "Fshih dritaren e menaxhimit të lidhjes"), ("hide_cm_tip", "Kjo është e mundur vetëm nëse aksesi bëhet nëpërmjet një fjalëkalimi të përhershëm"), + ("wayland_experiment_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 52faa8fed..79c9bbcc1 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -116,7 +116,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Good image quality", "Bra bildkvalitet"), ("Balanced", "Balanserad"), ("Optimize reaction time", "Optimera reaktionstid"), - ("Custom", "Anpassad"), + ("Custom", "Anpassat"), ("Show remote cursor", "Visa fjärrmus"), ("Show quality monitor", "Visa bildkvalitet"), ("Disable clipboard", "Stäng av urklipp"), @@ -231,7 +231,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Hostname", "Hostname"), ("Discovered", "Upptäckt"), ("install_daemon_tip", "För att starta efter boot måste du installera systemtjänsten."), - ("android_input_permission_tip1", "För att kontrollera din Android-enhet med mus eller touch, måste du tillåta RustDesk att använda \"Tillgänglighets\" tjänsten."), ("Remote ID", "Fjärr ID"), ("Paste", "Klistra in"), ("Paste here?", "Klistra in här?"), @@ -398,5 +397,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request access to your device", "Begär åtkomst till din enhet"), ("Hide connection management window", "Göm hanteringsfönster"), ("hide_cm_tip", "Tillåt att gömma endast om accepterande sessioner med lösenord och permanenta lösenord"), - ].iter().cloned().collect(); + ("wayland_experiment_tip", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 8f855d96a..acc92be20 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -397,5 +397,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request access to your device", ""), ("Hide connection management window", ""), ("hide_cm_tip", ""), + ("wayland_experiment_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 0f5dd42ba..bd2e9cd62 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -397,5 +397,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request access to your device", "Cihazınıza erişim talep edin"), ("Hide connection management window", "Bağlantı yönetimi penceresini gizle"), ("hide_cm_tip", ""), + ("wayland_experiment_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 4945fd511..35b2cb2cf 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -397,5 +397,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request access to your device", "請求訪問你的設備"), ("Hide connection management window", "隱藏連接管理窗口"), ("hide_cm_tip", "在只允許密碼連接並且只用固定密碼的情況下才允許隱藏"), + ("wayland_experiment_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 3861f0598..b1fdca17a 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -397,5 +397,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request access to your device", ""), ("Hide connection management window", ""), ("hide_cm_tip", ""), + ("wayland_experiment_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 8ddeadfca..acac782d1 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -397,5 +397,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request access to your device", ""), ("Hide connection management window", ""), ("hide_cm_tip", ""), + ("wayland_experiment_tip", ""), ].iter().cloned().collect(); } From 3f009a3bc73a0137a13c24cc8c3a2e90a1dc65e3 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sun, 4 Dec 2022 18:39:59 +0800 Subject: [PATCH 1102/2015] wayland tip --- .../lib/desktop/pages/desktop_home_page.dart | 32 ++++++++++++------- res/lang.py | 7 ++-- src/flutter_ffi.rs | 9 ++++++ 3 files changed, 35 insertions(+), 13 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 9eae57b9e..e0cb5a676 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -301,15 +301,6 @@ class _DesktopHomePageState extends State } Widget buildHelpCards() { - if (Platform.isWindows) { - if (!bind.mainIsInstalled()) { - return buildInstallCard( - "", "install_tip", "Install", bind.mainGotoInstall); - } else if (bind.mainIsInstalledLowerVersion()) { - return buildInstallCard("Status", "Your installation is lower version.", - "Click to upgrade", bind.mainUpdateMe); - } - } if (updateUrl.isNotEmpty) { return buildInstallCard( "Status", @@ -322,7 +313,15 @@ class _DesktopHomePageState extends State if (systemError.isNotEmpty) { return buildInstallCard("", systemError, "", () {}); } - if (Platform.isMacOS) { + if (Platform.isWindows) { + if (!bind.mainIsInstalled()) { + return buildInstallCard( + "", "install_tip", "Install", bind.mainGotoInstall); + } else if (bind.mainIsInstalledLowerVersion()) { + return buildInstallCard("Status", "Your installation is lower version.", + "Click to upgrade", bind.mainUpdateMe); + } + } else if (Platform.isMacOS) { if (!bind.mainIsCanScreenRecording(prompt: false)) { return buildInstallCard("Permissions", "config_screen", "Configure", () async { @@ -342,8 +341,19 @@ class _DesktopHomePageState extends State bind.mainIsInstalledDaemon(prompt: true); }); } + } else if (Platform.isLinux) { + if (bind.mainCurrentIsWayland()) { + return buildInstallCard( + "Warning", translate("wayland_experiment_tip"), "", () async {}, + help: 'Help', + link: 'https://rustdesk.com/docs/en/manual/linux/#x11-required'); + } else if (bind.mainIsLoginWayland()) { + return buildInstallCard("Warning", + "Login screen using Wayland is not supported", "", () async {}, + help: 'Help', + link: 'https://rustdesk.com/docs/en/manual/linux/#login-screen'); + } } - if (bind.mainIsInstalledLowerVersion()) {} return Container(); } diff --git a/res/lang.py b/res/lang.py index d1d4254ed..5aa6f4d15 100644 --- a/res/lang.py +++ b/res/lang.py @@ -16,7 +16,9 @@ def get_lang(lang): def line_split(line): toks = line.split('", "') - assert(len(toks) == 2) + if len(toks) != 2: + print(line) + assert(0) k = toks[0][2:] v = toks[1][:-3] return k, v @@ -34,7 +36,8 @@ def main(): def expand(): for fn in glob.glob('./src/lang/*'): lang = os.path.basename(fn)[:-3] - if lang in ['en','cn']: continue + if lang in ['en','cn']: continue + print(lang) dict = get_lang(lang) fw = open("./src/lang/%s.rs"%lang, "wt") for line in open('./src/lang/cn.rs'): diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 094a2faa7..23ba4ef4b 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -529,6 +529,7 @@ pub fn main_get_app_name() -> String { pub fn main_get_app_name_sync() -> SyncReturn { SyncReturn(get_app_name()) } + pub fn main_get_license() -> String { get_license() } @@ -1207,6 +1208,14 @@ pub fn main_on_main_window_close() { crate::portable_service::client::drop_portable_service_shared_memory(); } +pub fn main_current_is_wayland() -> SyncReturn { + SyncReturn(current_is_wayland()) +} + +pub fn main_is_login_wayland() -> SyncReturn { + SyncReturn(is_login_wayland()) +} + #[cfg(target_os = "android")] pub mod server_side { use jni::{ From 280147f26a4235ce7aff6ea7d7c2844545f55470 Mon Sep 17 00:00:00 2001 From: solokot Date: Sun, 4 Dec 2022 14:18:24 +0300 Subject: [PATCH 1103/2015] Update ru.rs --- src/lang/ru.rs | 78 +++++++++++++++++++++++++------------------------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/src/lang/ru.rs b/src/lang/ru.rs index f52a6de0b..bcb539498 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -3,7 +3,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = [ ("Status", "Статус"), ("Your Desktop", "Ваш рабочий стол"), - ("desk_tip", "Ваш рабочий стол доступен с этим идентификатором и паролем"), + ("desk_tip", "Ваш рабочий стол доступен с этим ID и паролем"), ("Password", "Пароль"), ("Ready", "Готово"), ("Established", "Установлено"), @@ -12,7 +12,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Start Service", "Запустить службу"), ("Service is running", "Служба запущена"), ("Service is not running", "Служба не запущена"), - ("not_ready_status", "Не готово. Пожалуйста, проверьте подключение."), + ("not_ready_status", "Не выполнено. Проверьте подключение."), ("Control Remote Desktop", "Управление удалённым рабочим столом"), ("Transfer File", "Передать файл"), ("Connect", "Подключиться"), @@ -33,7 +33,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Export Server Config", "Экспортировать конфигурацию сервера"), ("Import server configuration successfully", "Конфигурация сервера успешно импортирована"), ("Export server configuration successfully", "Конфигурация сервера успешно экспортирована"), - ("Invalid server configuration", "Недопустимая конфигурация сервера"), + ("Invalid server configuration", "Неправильная конфигурация сервера"), ("Clipboard is empty", "Буфер обмена пуст"), ("Stop service", "Остановить службу"), ("Change ID", "Изменить ID"), @@ -48,30 +48,30 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Relay Server", "Сервер ретрансляции"), ("API Server", "API-сервер"), ("invalid_http", "Должен начинаться с http:// или https://"), - ("Invalid IP", "Неверный IP-адрес"), + ("Invalid IP", "Неправильный IP-адрес"), ("id_change_tip", "Допускаются только символы a-z, A-Z, 0-9 и _ (подчёркивание). Первая буква должна быть a-z, A-Z. Длина от 6 до 16"), - ("Invalid format", "Неверный формат"), + ("Invalid format", "Неправильный формат"), ("server_not_support", "Пока не поддерживается сервером"), ("Not available", "Недоступно"), ("Too frequent", "Слишком часто"), ("Cancel", "Отменить"), ("Skip", "Пропустить"), ("Close", "Закрыть"), - ("Retry", "Попробовать снова"), + ("Retry", "Повторить"), ("OK", "ОК"), ("Password Required", "Требуется пароль"), - ("Please enter your password", "Пожалуйста, введите пароль"), + ("Please enter your password", "Введите пароль"), ("Remember password", "Запомнить пароль"), - ("Wrong Password", "Неверный пароль"), - ("Do you want to enter again?", "Хотите снова войти?"), + ("Wrong Password", "Неправильный пароль"), + ("Do you want to enter again?", "Повторить вход?"), ("Connection Error", "Ошибка подключения"), ("Error", "Ошибка"), ("Reset by the peer", "Сброшено удалённым узлом"), ("Connecting...", "Подключение..."), - ("Connection in progress. Please wait.", "Выполняется подключение. Пожалуйста, подождите."), - ("Please try 1 minute later", "Попробуйте через 1 минуту"), + ("Connection in progress. Please wait.", "Выполняется подключение. Подождите."), + ("Please try 1 minute later", "Попробуйте через минуту"), ("Login Error", "Ошибка входа"), - ("Successful", "Операция успешна"), + ("Successful", "Успешно"), ("Connected, waiting for image...", "Подключено, ожидание изображения..."), ("Name", "Имя"), ("Type", "Тип"), @@ -93,10 +93,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Unselect All", "Снять все"), ("Empty Directory", "Пустая папка"), ("Not an empty directory", "Папка не пуста"), - ("Are you sure you want to delete this file?", "Вы уверены, что хотите удалить этот файл?"), - ("Are you sure you want to delete this empty directory?", "Вы уверены, что хотите удалить пустую папку?"), - ("Are you sure you want to delete the file of this directory?", "Вы уверены, что хотите удалить файл из этой папки?"), - ("Do this for all conflicts", "Это относится ко всем конфликтам"), + ("Are you sure you want to delete this file?", "Удалить этот файл?"), + ("Are you sure you want to delete this empty directory?", "Удалить пустую папку?"), + ("Are you sure you want to delete the file of this directory?", "Удалить файл из этой папки?"), + ("Do this for all conflicts", "Применить ко всем конфликтам"), ("This is irreversible!", "Это необратимо!"), ("Deleting", "Удаление"), ("files", "файлы"), @@ -125,21 +125,21 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Insert Lock", "Установить замок"), ("Refresh", "Обновить"), ("ID does not exist", "ID не существует"), - ("Failed to connect to rendezvous server", "Не удалось подключиться к промежуточному серверу"), + ("Failed to connect to rendezvous server", "Невозможно подключиться к промежуточному серверу"), ("Please try later", "Пожалуйста, попробуйте позже"), ("Remote desktop is offline", "Удалённый рабочий стол не в сети"), ("Key mismatch", "Несоответствие ключей"), - ("Timeout", "Тайм-аут"), - ("Failed to connect to relay server", "Не удалось подключиться к серверу ретрансляции"), - ("Failed to connect via rendezvous server", "Не удалось подключиться через промежуточный сервер"), - ("Failed to connect via relay server", "Не удалось подключиться через сервер ретрансляции"), - ("Failed to make direct connection to remote desktop", "Не удалось установить прямое подключение к удалённому рабочему столу"), + ("Timeout", "Истекло время ожидания"), + ("Failed to connect to relay server", "Невозможно подключиться к серверу ретрансляции"), + ("Failed to connect via rendezvous server", "Невозможно подключиться через промежуточный сервер"), + ("Failed to connect via relay server", "Невозможно подключиться через сервер ретрансляции"), + ("Failed to make direct connection to remote desktop", "Невозможно установить прямое подключение к удалённому рабочему столу"), ("Set Password", "Установить пароль"), ("OS Password", "Пароль ОС"), ("install_tip", "В некоторых случаях из-за UAC RustDesk может работать неправильно на удалённом узле. Чтобы избежать UAC, нажмите кнопку ниже, чтобы установить RustDesk в системе."), - ("Click to upgrade", "Нажмите, чтобы проверить наличие обновлений"), - ("Click to download", "Нажмите, чтобы скачать"), - ("Click to update", "Нажмите, чтобы обновить"), + ("Click to upgrade", "Нажмите для проверки обновлений"), + ("Click to download", "Нажмите для скачивания"), + ("Click to update", "Нажмите для обновления"), ("Configure", "Настроить"), ("config_acc", "Чтобы удалённо управлять своим рабочим столом, вы должны предоставить RustDesk права \"доступа\""), ("config_screen", "Для удалённого доступа к рабочему столу вы должны предоставить RustDesk права \"снимок экрана\""), @@ -156,7 +156,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Your installation is lower version.", "Установлена более ранняя версия"), ("not_close_tcp_tip", "Не закрывать это окно при использовании туннеля"), ("Listening ...", "Ожидание..."), - ("Remote Host", "Удалённая машина"), + ("Remote Host", "Удалённый узел"), ("Remote Port", "Удалённый порт"), ("Action", "Действие"), ("Add", "Добавить"), @@ -190,17 +190,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Create Desktop Shortcut", "Создать ярлык на рабочем столе"), ("Change Path", "Изменить путь"), ("Create Folder", "Создать папку"), - ("Please enter the folder name", "Пожалуйста, введите имя папки"), + ("Please enter the folder name", "Введите имя папки"), ("Fix it", "Исправить"), ("Warning", "Предупреждение"), ("Login screen using Wayland is not supported", "Вход в систему с использованием Wayland не поддерживается"), ("Reboot required", "Требуется перезагрузка"), - ("Unsupported display server ", "Неподдерживаемый сервер дисплея"), + ("Unsupported display server ", "Неподдерживаемый сервер отображения"), ("x11 expected", "Ожидается X11"), ("Port", "Порт"), ("Settings", "Настройки"), - ("Username", "Имя пользователя"), - ("Invalid port", "Неверный порт"), + ("Username", "Пользователь"), + ("Invalid port", "Неправильный порт"), ("Closed manually by the peer", "Закрыто удалённым узлом вручную"), ("Enable remote configuration modification", "Разрешить удалённое изменение конфигурации"), ("Run without install", "Запустить без установки"), @@ -215,11 +215,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("whitelist_sep", "Раздельно запятой, точкой с запятой, пробелом или новой строкой"), ("Add ID", "Добавить ID"), ("Add Tag", "Добавить ключевое слово"), - ("Unselect all tags", "Отменить выбор всех тегов"), + ("Unselect all tags", "Отменить выбор всех меток"), ("Network error", "Ошибка сети"), ("Username missed", "Имя пользователя отсутствует"), ("Password missed", "Забыли пароль"), - ("Wrong credentials", "Неверные учётные данные"), + ("Wrong credentials", "Неправильные учётные данные"), ("Edit Tag", "Изменить метку"), ("Unremember Password", "Не сохранять пароль"), ("Favorites", "Избранное"), @@ -227,14 +227,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Remove from Favorites", "Удалить из избранного"), ("Empty", "Пусто"), ("Invalid folder name", "Недопустимое имя папки"), - ("Socks5 Proxy", "Socks5-прокси"), - ("Hostname", "Имя"), + ("Socks5 Proxy", "SOCKS5-прокси"), + ("Hostname", "Узел"), ("Discovered", "Найдено"), ("install_daemon_tip", "Для запуска при загрузке необходимо установить системную службу"), - ("Remote ID", "Удалённый идентификатор"), + ("Remote ID", "Удалённый ID"), ("Paste", "Вставить"), ("Paste here?", "Вставить сюда?"), - ("Are you sure to close the connection?", "Вы уверены, что хотите завершить подключение?"), + ("Are you sure to close the connection?", "Завершить подключение?"), ("Download new version", "Скачать новую версию"), ("Touch mode", "Сенсорный режим"), ("Mouse mode", "Режим мыши"), @@ -289,9 +289,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Someone turns on privacy mode, exit", "Кто-то включает режим конфиденциальности, выход"), ("Unsupported", "Не поддерживается"), ("Peer denied", "Отклонено удалённым узлом"), - ("Please install plugins", "Пожалуйста, установите плагины"), + ("Please install plugins", "Установите плагины"), ("Peer exit", "Удалённый узел отключён"), - ("Failed to turn off", "Не удалось отключить"), + ("Failed to turn off", "Невозможно отключить"), ("Turned off", "Отключён"), ("In privacy mode", "В режиме конфиденциальности"), ("Out privacy mode", "Выход из режима конфиденциальности"), @@ -397,6 +397,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request access to your device", "Запрос доступа к вашему устройству"), ("Hide connection management window", "Скрывать окно управления соединениями"), ("hide_cm_tip", "Разрешать скрытие случае, если принимаются сеансы по паролю или используется постоянный пароль"), - ("wayland_experiment_tip", ""), + ("wayland_experiment_tip", "Поддержка Wayland находится на экспериментальной стадии, используйте X11, если вам требуется автоматический доступ."), ].iter().cloned().collect(); } From fb0c75f1887102a6e4e45ad994e872dc5003bf40 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sat, 3 Dec 2022 16:32:22 +0800 Subject: [PATCH 1104/2015] fix theme sync Signed-off-by: 21pages --- flutter/lib/common.dart | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index eac7fbf9b..4c298d917 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -215,18 +215,15 @@ class MyTheme { } static void changeDarkMode(ThemeMode mode) { - final preference = getThemeModePreference(); - if (preference != mode) { + Get.changeThemeMode(mode); + if (desktopType == DesktopType.main) { if (mode == ThemeMode.system) { bind.mainSetLocalOption(key: kCommConfKeyTheme, value: ''); } else { bind.mainSetLocalOption( key: kCommConfKeyTheme, value: mode.toShortString()); } - Get.changeThemeMode(mode); - if (desktopType == DesktopType.main) { - bind.mainChangeTheme(dark: currentThemeMode().toShortString()); - } + bind.mainChangeTheme(dark: currentThemeMode().toShortString()); } } From be74f9033497ab9ef52a7d7b0e68a843b64e0218 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sun, 4 Dec 2022 15:15:48 +0800 Subject: [PATCH 1105/2015] right menu to show/hide peer card Signed-off-by: 21pages --- flutter/lib/common/widgets/peer_tab_page.dart | 232 +++++++++++++----- .../lib/desktop/pages/connection_page.dart | 23 +- flutter/lib/mobile/pages/connection_page.dart | 15 +- 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 | 2 +- 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/ru.rs | 1 + src/lang/sk.rs | 1 + src/lang/sq.rs | 1 + src/lang/sv.rs | 1 + src/lang/template.rs | 1 + src/lang/tr.rs | 1 + src/lang/tw.rs | 1 + src/lang/ua.rs | 1 + src/lang/vn.rs | 1 + 31 files changed, 198 insertions(+), 101 deletions(-) diff --git a/flutter/lib/common/widgets/peer_tab_page.dart b/flutter/lib/common/widgets/peer_tab_page.dart index 523230810..3e8f04b93 100644 --- a/flutter/lib/common/widgets/peer_tab_page.dart +++ b/flutter/lib/common/widgets/peer_tab_page.dart @@ -1,36 +1,73 @@ +import 'dart:ui' as ui; + +import 'package:bot_toast/bot_toast.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common/widgets/address_book.dart'; import 'package:flutter_hbb/common/widgets/peers_view.dart'; import 'package:flutter_hbb/common/widgets/peer_card.dart'; import 'package:flutter_hbb/consts.dart'; +import 'package:flutter_hbb/desktop/widgets/popup_menu.dart'; +import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; +import 'package:flutter_hbb/desktop/widgets/material_mod_popup_menu.dart' + as mod_menu; import 'package:get/get.dart'; import '../../common.dart'; import '../../models/platform_model.dart'; class PeerTabPage extends StatefulWidget { - final List tabs; - final List children; - const PeerTabPage({required this.tabs, required this.children, Key? key}) - : super(key: key); + const PeerTabPage({Key? key}) : super(key: key); @override State createState() => _PeerTabPageState(); } +class _TabEntry { + final String name; + final Widget widget; + final Function() load; + _TabEntry(this.name, this.widget, this.load); +} + class _PeerTabPageState extends State with SingleTickerProviderStateMixin { - final RxInt _tabIndex = 0.obs; + late final RxInt _tabHiddenFlag; + late final RxString _currentTab; + final List<_TabEntry> entries = [ + _TabEntry( + 'Recent Sessions', + RecentPeersView( + menuPadding: kDesktopMenuPadding, + ), + bind.mainLoadRecentPeers), + _TabEntry( + 'Favorites', + FavoritePeersView( + menuPadding: kDesktopMenuPadding, + ), + bind.mainLoadFavPeers), + _TabEntry( + 'Discovered', + DiscoveredPeersView( + menuPadding: kDesktopMenuPadding, + ), + bind.mainDiscover), + _TabEntry( + 'Address Book', + const AddressBook( + menuPadding: kDesktopMenuPadding, + ), + () => {}), + ]; @override void initState() { - setPeer(); - super.initState(); - } - - setPeer() { - final index = bind.getLocalFlutterConfig(k: 'peer-tab-index'); - if (index != '') { - _tabIndex.value = int.parse(index); - } + _tabHiddenFlag = (int.tryParse( + bind.getLocalFlutterConfig(k: 'hidden-peer-card'), + radix: 2) ?? + 0) + .obs; + _currentTab = bind.getLocalFlutterConfig(k: 'current-peer-tab').obs; + adjustTab(); final uiType = bind.getLocalFlutterConfig(k: 'peer-card-ui-type'); if (uiType != '') { @@ -38,27 +75,14 @@ class _PeerTabPageState extends State ? PeerUiType.list : PeerUiType.grid; } + super.initState(); } // hard code for now - Future _handleTabSelection(int index) async { - _tabIndex.value = index; - await bind.setLocalFlutterConfig(k: 'peer-tab-index', v: index.toString()); - switch (index) { - case 0: - bind.mainLoadRecentPeers(); - break; - case 1: - bind.mainLoadFavPeers(); - break; - case 2: - bind.mainDiscover(); - break; - case 3: - - /// AddressBook initState will refresh ab state - break; - } + Future handleTabSelection(String tabName) async { + _currentTab.value = tabName; + await bind.setLocalFlutterConfig(k: 'current-peer-tab', v: tabName); + entries.firstWhereOrNull((e) => e.name == tabName)?.load(); } @override @@ -80,8 +104,9 @@ class _PeerTabPageState extends State child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - Expanded(child: _createSwitchBar(context)), - const SizedBox(width: 10), + Expanded( + child: visibleContextMenuListener( + _createSwitchBar(context))), const PeerSearchBar(), Offstage( offstage: !isDesktop, @@ -97,44 +122,48 @@ class _PeerTabPageState extends State Widget _createSwitchBar(BuildContext context) { final textColor = Theme.of(context).textTheme.titleLarge?.color; - return ListView( + return Obx(() => ListView( scrollDirection: Axis.horizontal, shrinkWrap: true, controller: ScrollController(), - children: super.widget.tabs.asMap().entries.map((t) { - return Obx(() => InkWell( - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 8), - decoration: BoxDecoration( - color: _tabIndex.value == t.key - ? Theme.of(context).backgroundColor - : null, - borderRadius: BorderRadius.circular(isDesktop ? 2 : 6), - ), - child: Align( - alignment: Alignment.center, - child: Text( - t.value, - textAlign: TextAlign.center, - style: TextStyle( - height: 1, - fontSize: 14, - color: - _tabIndex.value == t.key ? textColor : textColor - ?..withOpacity(0.5)), - ), - )), - onTap: () async => await _handleTabSelection(t.key), - )); - }).toList()); + children: entries.where((e) => !isTabHidden(e.name)).map((t) { + return InkWell( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8), + decoration: BoxDecoration( + color: _currentTab.value == t.name + ? Theme.of(context).backgroundColor + : null, + borderRadius: BorderRadius.circular(isDesktop ? 2 : 6), + ), + child: Align( + alignment: Alignment.center, + child: Text( + translate(t.name), + textAlign: TextAlign.center, + style: TextStyle( + height: 1, + fontSize: 14, + color: + _currentTab.value == t.name ? textColor : textColor + ?..withOpacity(0.5)), + ), + )), + onTap: () async => await handleTabSelection(t.name), + ); + }).toList())); } Widget _createPeersView() { final verticalMargin = isDesktop ? 12.0 : 6.0; return Expanded( - child: Obx(() => widget - .children[_tabIndex.value]) //: (to) => _tabIndex.value = to) - .marginSymmetric(vertical: verticalMargin), + child: Obx(() => + entries + .firstWhereOrNull((e) => e.name == _currentTab.value) + ?.widget ?? + visibleContextMenuListener(Center( + child: Text(translate('Right click to select tabs')), + ))).marginSymmetric(vertical: verticalMargin), ); } @@ -167,6 +196,81 @@ class _PeerTabPageState extends State .toList(), ); } + + bool isTabHidden(String name) { + int index = entries.indexWhere((e) => e.name == name); + if (index >= 0) { + return _tabHiddenFlag & (1 << index) != 0; + } + assert(false); + return false; + } + + adjustTab() { + List visibleTabs = + entries.where((e) => !isTabHidden(e.name)).map((e) => e.name).toList(); + if (visibleTabs.isNotEmpty) { + if (!visibleTabs.contains(_currentTab.value)) { + handleTabSelection(visibleTabs[0]); + } + } else { + _currentTab.value = ''; + } + } + + Widget visibleContextMenuListener(Widget child) { + return Listener( + onPointerDown: (e) { + if (e.kind != ui.PointerDeviceKind.mouse) { + return; + } + if (e.buttons == 2) { + showRightMenu( + (CancelFunc cancelFunc) { + return visibleContextMenu(cancelFunc); + }, + target: e.position, + ); + } + }, + child: child); + } + + Widget visibleContextMenu(CancelFunc cancelFunc) { + final List menu = entries.asMap().entries.map((e) { + int bitMask = 1 << e.key; + return MenuEntrySwitch( + switchType: SwitchType.scheckbox, + text: translate(e.value.name), + getter: () async { + return _tabHiddenFlag.value & bitMask == 0; + }, + setter: (show) async { + if (show) { + _tabHiddenFlag.value &= ~bitMask; + } else { + _tabHiddenFlag.value |= bitMask; + } + await bind.setLocalFlutterConfig( + k: 'hidden-peer-card', + v: _tabHiddenFlag.value.toRadixString(2)); + cancelFunc(); + adjustTab(); + }); + }).toList(); + return mod_menu.PopupMenu( + items: menu + .map((entry) => entry.build( + context, + const MenuConfig( + commonColor: MyTheme.accent, + height: 20.0, + dividerHeight: 12.0, + ))) + .expand((i) => i) + .toList(), + ); + } } class PeerSearchBar extends StatefulWidget { diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index a830d6399..81f526c12 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -121,28 +121,7 @@ class _ConnectionPageState extends State ])), SliverFillRemaining( hasScrollBody: false, - child: PeerTabPage( - tabs: [ - translate('Recent Sessions'), - translate('Favorites'), - translate('Discovered'), - translate('Address Book') - ], - children: [ - RecentPeersView( - menuPadding: kDesktopMenuPadding, - ), - FavoritePeersView( - menuPadding: kDesktopMenuPadding, - ), - DiscoveredPeersView( - menuPadding: kDesktopMenuPadding, - ), - const AddressBook( - menuPadding: kDesktopMenuPadding, - ), - ], - ).paddingOnly(right: 12.0), + child: PeerTabPage().paddingOnly(right: 12.0), ) ], ).paddingOnly(left: 12.0), diff --git a/flutter/lib/mobile/pages/connection_page.dart b/flutter/lib/mobile/pages/connection_page.dart index e99226c4d..957910324 100644 --- a/flutter/lib/mobile/pages/connection_page.dart +++ b/flutter/lib/mobile/pages/connection_page.dart @@ -75,20 +75,7 @@ class _ConnectionPageState extends State { ])), SliverFillRemaining( hasScrollBody: false, - child: PeerTabPage( - tabs: [ - translate('Recent Sessions'), - translate('Favorites'), - translate('Discovered'), - translate('Address Book') - ], - children: [ - RecentPeersView(), - FavoritePeersView(), - DiscoveredPeersView(), - const AddressBook(), - ], - ), + child: PeerTabPage(), ) ], ).marginOnly(top: 2, left: 10, right: 10); diff --git a/src/lang/ca.rs b/src/lang/ca.rs index e7794b989..6fc919b8d 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -398,5 +398,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Hide connection management window", ""), ("hide_cm_tip", ""), ("wayland_experiment_tip", ""), + ("Right click to select tabs", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 8af5229f2..1b49f6c4a 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -398,5 +398,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Hide connection management window", "隐藏连接管理窗口"), ("hide_cm_tip", "在只允许密码连接并且只用固定密码的情况下才允许隐藏"), ("wayland_experiment_tip", ""), + ("Right click to select tabs", "右键选择选项卡"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 547b233f4..6455023de 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -398,5 +398,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Hide connection management window", ""), ("hide_cm_tip", ""), ("wayland_experiment_tip", ""), + ("Right click to select tabs", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 5d815a90b..afd6476ee 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -398,5 +398,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Hide connection management window", ""), ("hide_cm_tip", ""), ("wayland_experiment_tip", ""), + ("Right click to select tabs", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index e4306301b..273a607ed 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -398,5 +398,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Hide connection management window", "Fenster zur Verwaltung der Verbindung verstecken"), ("hide_cm_tip", "Dies ist nur möglich, wenn der Zugriff nur über ein permanentes Passwort erfolgt."), // Sehr unklar. Muss noch angepasst werden. Original: Allow hiding only if accepting sessions via password and using pernament passw"), ("wayland_experiment_tip", ""), + ("Right click to select tabs", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index f5a2f7e55..3a38b6601 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -398,5 +398,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Hide connection management window", ""), ("hide_cm_tip", ""), ("wayland_experiment_tip", ""), + ("Right click to select tabs", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index d8362ff05..39d031c9d 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -398,5 +398,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Hide connection management window", "Ocultar ventana de gestión de conexión"), ("hide_cm_tip", "Permitir ocultar solo si se aceptan sesiones a través de contraseña y usando contraseña permanente"), ("wayland_experiment_tip", ""), + ("Right click to select tabs", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index ee3706e34..7513f84e8 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -398,5 +398,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Hide connection management window", "پنهان کردن پنجره مدیریت اتصال"), ("hide_cm_tip", "فقط در صورت پذیرفتن جلسات از طریق رمز عبور و استفاده از رمز عبور دائمی، مخفی شدن مجاز است"), ("wayland_experiment_tip", ""), + ("Right click to select tabs", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 860f59518..a4eedfa2a 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -398,5 +398,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Hide connection management window", "Masquer la fenêtre de gestion des connexions"), ("hide_cm_tip", "Autoriser le masquage uniquement si vous acceptez des sessions via un mot de passe et utilisez un mot de passe permanent"), ("wayland_experiment_tip", ""), + ("Right click to select tabs", ""), ].iter().cloned().collect(); } diff --git a/src/lang/gr.rs b/src/lang/gr.rs index 20bb98200..986919a8b 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -397,6 +397,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request access to your device", "Αίτημα πρόσβασης στη συσκευή σας"), ("Hide connection management window", "Απόκρυψη παραθύρου διαχείρισης σύνδεσης"), ("hide_cm_tip", "Να επιτρέπεται η απόκρυψη, μόνο εάν αποδέχεστε συνδέσεις μέσω κωδικού πρόσβασης και χρησιμοποιείτε μόνιμο κωδικό πρόσβασης"), - ("wayland_experiment_tip", ""), + ("Right click to select tabs", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 51f072e16..941caeac1 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -398,5 +398,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Hide connection management window", ""), ("hide_cm_tip", ""), ("wayland_experiment_tip", ""), + ("Right click to select tabs", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index b2492553d..7f4a50523 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -398,5 +398,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Hide connection management window", ""), ("hide_cm_tip", ""), ("wayland_experiment_tip", ""), + ("Right click to select tabs", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index ec993ad04..98ea9366e 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -398,5 +398,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Hide connection management window", "Nascondi la finestra di gestione delle connessioni"), ("hide_cm_tip", "Permetti di nascondere solo se si accettano sessioni con password permanente"), ("wayland_experiment_tip", ""), + ("Right click to select tabs", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 5cd6d8d6d..4121dd55c 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -398,5 +398,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Hide connection management window", ""), ("hide_cm_tip", ""), ("wayland_experiment_tip", ""), + ("Right click to select tabs", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 31d69841c..a1cd730e9 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -398,5 +398,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Hide connection management window", ""), ("hide_cm_tip", ""), ("wayland_experiment_tip", ""), + ("Right click to select tabs", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index c32fa778d..1e623c0b6 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -398,5 +398,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Hide connection management window", ""), ("hide_cm_tip", ""), ("wayland_experiment_tip", ""), + ("Right click to select tabs", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index edd9a4e45..ed14e3ca2 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -398,5 +398,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Hide connection management window", ""), ("hide_cm_tip", ""), ("wayland_experiment_tip", ""), + ("Right click to select tabs", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 1c174af8d..65620e3e8 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -398,5 +398,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Hide connection management window", ""), ("hide_cm_tip", ""), ("wayland_experiment_tip", ""), + ("Right click to select tabs", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 86bd1b77f..cf388a88c 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -398,5 +398,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Hide connection management window", ""), ("hide_cm_tip", ""), ("wayland_experiment_tip", ""), + ("Right click to select tabs", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index bcb539498..76a8cca87 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -398,5 +398,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Hide connection management window", "Скрывать окно управления соединениями"), ("hide_cm_tip", "Разрешать скрытие случае, если принимаются сеансы по паролю или используется постоянный пароль"), ("wayland_experiment_tip", "Поддержка Wayland находится на экспериментальной стадии, используйте X11, если вам требуется автоматический доступ."), + ("Right click to select tabs", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 194b3dc83..e7a9b12b5 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -398,5 +398,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Hide connection management window", ""), ("hide_cm_tip", ""), ("wayland_experiment_tip", ""), + ("Right click to select tabs", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index a7cb45443..1926c849b 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -398,5 +398,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Hide connection management window", "Fshih dritaren e menaxhimit të lidhjes"), ("hide_cm_tip", "Kjo është e mundur vetëm nëse aksesi bëhet nëpërmjet një fjalëkalimi të përhershëm"), ("wayland_experiment_tip", ""), + ("Right click to select tabs", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 79c9bbcc1..cfef903a7 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -398,5 +398,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Hide connection management window", "Göm hanteringsfönster"), ("hide_cm_tip", "Tillåt att gömma endast om accepterande sessioner med lösenord och permanenta lösenord"), ("wayland_experiment_tip", ""), + ("Right click to select tabs", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index acc92be20..ed7189ce6 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -398,5 +398,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Hide connection management window", ""), ("hide_cm_tip", ""), ("wayland_experiment_tip", ""), + ("Right click to select tabs", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index bd2e9cd62..6c518da81 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -398,5 +398,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Hide connection management window", "Bağlantı yönetimi penceresini gizle"), ("hide_cm_tip", ""), ("wayland_experiment_tip", ""), + ("Right click to select tabs", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 35b2cb2cf..e7c024420 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -398,5 +398,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Hide connection management window", "隱藏連接管理窗口"), ("hide_cm_tip", "在只允許密碼連接並且只用固定密碼的情況下才允許隱藏"), ("wayland_experiment_tip", ""), + ("Right click to select tabs", "右鍵選擇選項卡"), ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index b1fdca17a..713d15c69 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -398,5 +398,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Hide connection management window", ""), ("hide_cm_tip", ""), ("wayland_experiment_tip", ""), + ("Right click to select tabs", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index acac782d1..c59de33fc 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -398,5 +398,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Hide connection management window", ""), ("hide_cm_tip", ""), ("wayland_experiment_tip", ""), + ("Right click to select tabs", ""), ].iter().cloned().collect(); } From 9bbe236651e33ca99d05498e3e9a0f351d234e4c Mon Sep 17 00:00:00 2001 From: 21pages Date: Sun, 4 Dec 2022 17:44:33 +0800 Subject: [PATCH 1106/2015] peer tab recorder Signed-off-by: 21pages --- flutter/lib/common/widgets/peer_tab_page.dart | 142 ++++++++++++------ 1 file changed, 92 insertions(+), 50 deletions(-) diff --git a/flutter/lib/common/widgets/peer_tab_page.dart b/flutter/lib/common/widgets/peer_tab_page.dart index 3e8f04b93..01e3939ee 100644 --- a/flutter/lib/common/widgets/peer_tab_page.dart +++ b/flutter/lib/common/widgets/peer_tab_page.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'dart:ui' as ui; import 'package:bot_toast/bot_toast.dart'; @@ -30,8 +31,9 @@ class _TabEntry { class _PeerTabPageState extends State with SingleTickerProviderStateMixin { - late final RxInt _tabHiddenFlag; - late final RxString _currentTab; + late final RxInt tabHiddenFlag; + late final RxString currentTab; + late final RxList visibleOrderedTabs; final List<_TabEntry> entries = [ _TabEntry( 'Recent Sessions', @@ -61,12 +63,30 @@ class _PeerTabPageState extends State @override void initState() { - _tabHiddenFlag = (int.tryParse( + tabHiddenFlag = (int.tryParse( bind.getLocalFlutterConfig(k: 'hidden-peer-card'), radix: 2) ?? 0) .obs; - _currentTab = bind.getLocalFlutterConfig(k: 'current-peer-tab').obs; + currentTab = bind.getLocalFlutterConfig(k: 'current-peer-tab').obs; + visibleOrderedTabs = entries + .where((e) => !isTabHidden(e.name)) + .map((e) => e.name) + .toList() + .obs; + try { + final json = jsonDecode(bind.getLocalFlutterConfig(k: 'peer-tab-order')); + if (json is List) { + final List list = json.map((e) => e.toString()).toList(); + if (list.length == visibleOrderedTabs.length && + visibleOrderedTabs.every((e) => list.contains(e))) { + visibleOrderedTabs.value = list; + } + } + } catch (e) { + debugPrint('$e'); + } + adjustTab(); final uiType = bind.getLocalFlutterConfig(k: 'peer-card-ui-type'); @@ -78,9 +98,8 @@ class _PeerTabPageState extends State super.initState(); } - // hard code for now Future handleTabSelection(String tabName) async { - _currentTab.value = tabName; + currentTab.value = tabName; await bind.setLocalFlutterConfig(k: 'current-peer-tab', v: tabName); entries.firstWhereOrNull((e) => e.name == tabName)?.load(); } @@ -122,45 +141,62 @@ class _PeerTabPageState extends State Widget _createSwitchBar(BuildContext context) { final textColor = Theme.of(context).textTheme.titleLarge?.color; - return Obx(() => ListView( - scrollDirection: Axis.horizontal, - shrinkWrap: true, - controller: ScrollController(), - children: entries.where((e) => !isTabHidden(e.name)).map((t) { - return InkWell( - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 8), - decoration: BoxDecoration( - color: _currentTab.value == t.name - ? Theme.of(context).backgroundColor - : null, - borderRadius: BorderRadius.circular(isDesktop ? 2 : 6), - ), - child: Align( - alignment: Alignment.center, - child: Text( - translate(t.name), - textAlign: TextAlign.center, - style: TextStyle( - height: 1, - fontSize: 14, - color: - _currentTab.value == t.name ? textColor : textColor + return Obx(() { + int indexCounter = -1; + return ReorderableListView( + buildDefaultDragHandles: false, + onReorder: (oldIndex, newIndex) { + var list = visibleOrderedTabs.toList(); + if (oldIndex < newIndex) { + newIndex -= 1; + } + final String item = list.removeAt(oldIndex); + list.insert(newIndex, item); + bind.setLocalFlutterConfig( + k: 'peer-tab-order', v: jsonEncode(list)); + visibleOrderedTabs.value = list; + }, + scrollDirection: Axis.horizontal, + shrinkWrap: true, + scrollController: ScrollController(), + children: visibleOrderedTabs.map((t) { + indexCounter++; + return ReorderableDragStartListener( + key: ValueKey(t), + index: indexCounter, + child: InkWell( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8), + decoration: BoxDecoration( + color: currentTab.value == t + ? Theme.of(context).backgroundColor + : null, + borderRadius: BorderRadius.circular(isDesktop ? 2 : 6), + ), + child: Align( + alignment: Alignment.center, + child: Text( + translate(t), + textAlign: TextAlign.center, + style: TextStyle( + height: 1, + fontSize: 14, + color: currentTab.value == t ? textColor : textColor ?..withOpacity(0.5)), - ), - )), - onTap: () async => await handleTabSelection(t.name), - ); - }).toList())); + ), + )), + onTap: () async => await handleTabSelection(t), + ), + ); + }).toList()); + }); } Widget _createPeersView() { final verticalMargin = isDesktop ? 12.0 : 6.0; return Expanded( child: Obx(() => - entries - .firstWhereOrNull((e) => e.name == _currentTab.value) - ?.widget ?? + entries.firstWhereOrNull((e) => e.name == currentTab.value)?.widget ?? visibleContextMenuListener(Center( child: Text(translate('Right click to select tabs')), ))).marginSymmetric(vertical: verticalMargin), @@ -200,21 +236,19 @@ class _PeerTabPageState extends State bool isTabHidden(String name) { int index = entries.indexWhere((e) => e.name == name); if (index >= 0) { - return _tabHiddenFlag & (1 << index) != 0; + return tabHiddenFlag & (1 << index) != 0; } assert(false); return false; } adjustTab() { - List visibleTabs = - entries.where((e) => !isTabHidden(e.name)).map((e) => e.name).toList(); - if (visibleTabs.isNotEmpty) { - if (!visibleTabs.contains(_currentTab.value)) { - handleTabSelection(visibleTabs[0]); + if (visibleOrderedTabs.isNotEmpty) { + if (!visibleOrderedTabs.contains(currentTab.value)) { + handleTabSelection(visibleOrderedTabs[0]); } } else { - _currentTab.value = ''; + currentTab.value = ''; } } @@ -243,17 +277,25 @@ class _PeerTabPageState extends State switchType: SwitchType.scheckbox, text: translate(e.value.name), getter: () async { - return _tabHiddenFlag.value & bitMask == 0; + return tabHiddenFlag.value & bitMask == 0; }, setter: (show) async { if (show) { - _tabHiddenFlag.value &= ~bitMask; + tabHiddenFlag.value &= ~bitMask; } else { - _tabHiddenFlag.value |= bitMask; + tabHiddenFlag.value |= bitMask; } await bind.setLocalFlutterConfig( - k: 'hidden-peer-card', - v: _tabHiddenFlag.value.toRadixString(2)); + k: 'hidden-peer-card', v: tabHiddenFlag.value.toRadixString(2)); + visibleOrderedTabs.removeWhere((e) => isTabHidden(e)); + visibleOrderedTabs.addAll(entries + .where((e) => + !visibleOrderedTabs.contains(e.name) && + !isTabHidden(e.name)) + .map((e) => e.name) + .toList()); + await bind.setLocalFlutterConfig( + k: 'peer-tab-order', v: jsonEncode(visibleOrderedTabs)); cancelFunc(); adjustTab(); }); From 5a7f610b593a3f0cddd0b4263dd304edc20ada76 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sun, 4 Dec 2022 18:47:02 +0800 Subject: [PATCH 1107/2015] fix cm elevate button visibility of different conn type Signed-off-by: 21pages --- flutter/lib/desktop/pages/server_page.dart | 12 ++++++++---- flutter/lib/models/server_model.dart | 19 +++++++++++++++++++ src/ui/cm.tis | 2 +- src/ui_session_interface.rs | 10 ++++++++-- 4 files changed, 36 insertions(+), 7 deletions(-) diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index 6c586994b..fa367f488 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -238,7 +238,7 @@ Widget buildConnectionCard(Client client) { key: ValueKey(client.id), children: [ _CmHeader(client: client), - client.isFileTransfer || client.disconnected + client.type_() != ClientType.remote || client.disconnected ? Offstage() : _PrivilegeBoard(client: client), Expanded( @@ -376,7 +376,7 @@ class _CmHeaderState extends State<_CmHeader> ), ), Offstage( - offstage: !client.authorized || client.isFileTransfer, + offstage: !client.authorized || client.type_() != ClientType.remote, child: IconButton( onPressed: () => checkClickTime( client.id, () => gFFI.chatModel.toggleCMChatPage(client.id)), @@ -510,7 +510,9 @@ class _CmControlPanel extends StatelessWidget { buildAuthorized(BuildContext context) { final bool canElevate = bind.cmCanElevate(); final model = Provider.of(context); - final showElevation = canElevate && model.showElevation; + final showElevation = canElevate && + model.showElevation && + client.type_() == ClientType.remote; return Column( mainAxisAlignment: MainAxisAlignment.end, children: [ @@ -560,7 +562,9 @@ class _CmControlPanel extends StatelessWidget { buildUnAuthorized(BuildContext context) { final bool canElevate = bind.cmCanElevate(); final model = Provider.of(context); - final showElevation = canElevate && model.showElevation; + final showElevation = canElevate && + model.showElevation && + client.type_() == ClientType.remote; final showAccept = model.approveMode != 'password'; return Column( mainAxisAlignment: MainAxisAlignment.end, diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index 344733324..338da4ee3 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -581,10 +581,17 @@ class ServerModel with ChangeNotifier { } } +enum ClientType { + remote, + file, + portForward, +} + class Client { int id = 0; // client connections inner count id bool authorized = false; bool isFileTransfer = false; + String portForward = ""; String name = ""; String peerId = ""; // peer user's id,show at app bool keyboard = false; @@ -604,6 +611,7 @@ class Client { id = json['id']; authorized = json['authorized']; isFileTransfer = json['is_file_transfer']; + portForward = json['port_forward']; name = json['name']; peerId = json['peer_id']; keyboard = json['keyboard']; @@ -620,6 +628,7 @@ class Client { data['id'] = id; data['is_start'] = authorized; data['is_file_transfer'] = isFileTransfer; + data['port_forward'] = portForward; data['name'] = name; data['peer_id'] = peerId; data['keyboard'] = keyboard; @@ -631,6 +640,16 @@ class Client { data['disconnected'] = disconnected; return data; } + + ClientType type_() { + if (isFileTransfer) { + return ClientType.file; + } else if (portForward.isNotEmpty) { + return ClientType.portForward; + } else { + return ClientType.remote; + } + } } String getLoginDialogTag(int id) { diff --git a/src/ui/cm.tis b/src/ui/cm.tis index 2cfc14bf1..74eb6c6d2 100644 --- a/src/ui/cm.tis +++ b/src/ui/cm.tis @@ -29,7 +29,7 @@ class Body: Reactor.Component }; var right_style = show_chat ? "" : "display: none"; var disconnected = c.disconnected; - var show_elevation_btn = handler.can_elevate() && show_elevation; + var show_elevation_btn = handler.can_elevate() && show_elevation && !c.is_file_transfer && c.port_forward.length == 0; var show_accept_btn = handler.get_option('approve-mode') != 'password'; // below size:* is work around for Linux, it alreayd set in css, but not work, shit sciter return
    diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 6f8820e87..6b635436d 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -200,7 +200,7 @@ impl Session { h265 = h265 && encoding_265; return (h264, h265); } - #[allow(dead_code)] + #[allow(unreachable_code)] (false, false) } @@ -1211,7 +1211,13 @@ impl Interface for Session { input_os_password(p, true, self.clone()); } let current = &pi.displays[pi.current_display as usize]; - self.set_display(current.x, current.y, current.width, current.height, current.cursor_embeded); + self.set_display( + current.x, + current.y, + current.width, + current.height, + current.cursor_embeded, + ); } self.update_privacy_mode(); // Save recent peers, then push event to flutter. So flutter can refresh peer page. From 9788f3684f4de401001141df5fe0a9aa031d6048 Mon Sep 17 00:00:00 2001 From: 21pages Date: Wed, 30 Nov 2022 11:13:02 +0800 Subject: [PATCH 1108/2015] judge attribute when parse ab json Signed-off-by: 21pages --- flutter/lib/common/widgets/address_book.dart | 2 +- flutter/lib/desktop/pages/connection_page.dart | 7 +++---- flutter/lib/models/ab_model.dart | 17 ++++++++++++----- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/flutter/lib/common/widgets/address_book.dart b/flutter/lib/common/widgets/address_book.dart index c96dc115a..5b3527fa0 100644 --- a/flutter/lib/common/widgets/address_book.dart +++ b/flutter/lib/common/widgets/address_book.dart @@ -90,7 +90,7 @@ class _AddressBookState extends State { Text(translate(error)), TextButton( onPressed: () { - setState(() {}); + gFFI.abModel.pullAb(); }, child: Text(translate("Retry"))) ], diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 81f526c12..7500fe99e 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -113,7 +113,7 @@ class _ConnectionPageState extends State delegate: SliverChildListDelegate([ Row( children: [ - _buildRemoteIDTextField(context), + Flexible(child: _buildRemoteIDTextField(context)), ], ).marginOnly(top: 22), SizedBox(height: 12), @@ -237,9 +237,8 @@ class _ConnectionPageState extends State ), ), ); - return Center( - child: Container( - constraints: const BoxConstraints(maxWidth: 600), child: w)); + return Container( + constraints: const BoxConstraints(maxWidth: 600), child: w); } Widget buildStatus() { diff --git a/flutter/lib/models/ab_model.dart b/flutter/lib/models/ab_model.dart index 5a055fd14..afee97e75 100644 --- a/flutter/lib/models/ab_model.dart +++ b/flutter/lib/models/ab_model.dart @@ -34,13 +34,20 @@ class AbModel { if (resp.body.isNotEmpty && resp.body.toLowerCase() != "null") { Map json = jsonDecode(resp.body); if (json.containsKey('error')) { - abError = json['error']; + abError.value = json['error']; } else if (json.containsKey('data')) { final data = jsonDecode(json['data']); - tags.value = data['tags']; - peers.clear(); - for (final peer in data['peers']) { - peers.add(Peer.fromJson(peer)); + if (data != null) { + tags.clear(); + peers.clear(); + if (data['tags'] is List) { + tags.value = data['tags']; + } + if (data['peers'] is List) { + for (final peer in data['peers']) { + peers.add(Peer.fromJson(peer)); + } + } } } return resp.body; From 5b9a76f8a580ac19d0e6e74d841283498fcd1917 Mon Sep 17 00:00:00 2001 From: csf Date: Sun, 4 Dec 2022 22:41:44 +0900 Subject: [PATCH 1109/2015] fix file transfer load/save config, opt breadCrumbScroll --- .../lib/desktop/pages/file_manager_page.dart | 29 +++++++++---------- .../lib/mobile/pages/file_manager_page.dart | 11 +++---- flutter/lib/models/file_model.dart | 10 +++++-- 3 files changed, 28 insertions(+), 22 deletions(-) diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 021f03713..e5476fc2f 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -93,6 +93,7 @@ class _FileManagerPageState extends State Wakelock.enable(); } debugPrint("File manager page init success with id ${widget.id}"); + model.onDirChanged = breadCrumbScrollToEnd; // register location listener _locationNodeLocal.addListener(onLocalLocationFocusChanged); _locationNodeRemote.addListener(onRemoteLocationFocusChanged); @@ -100,17 +101,18 @@ class _FileManagerPageState extends State @override void dispose() { - model.onClose(); - _ffi.close(); - _ffi.dialogManager.dismissAll(); - if (!Platform.isLinux) { - Wakelock.disable(); - } - Get.delete(tag: 'ft_${widget.id}'); - _locationNodeLocal.removeListener(onLocalLocationFocusChanged); - _locationNodeRemote.removeListener(onRemoteLocationFocusChanged); - _locationNodeLocal.dispose(); - _locationNodeRemote.dispose(); + model.onClose().whenComplete(() { + _ffi.close(); + _ffi.dialogManager.dismissAll(); + if (!Platform.isLinux) { + Wakelock.disable(); + } + Get.delete(tag: 'ft_${widget.id}'); + _locationNodeLocal.removeListener(onLocalLocationFocusChanged); + _locationNodeRemote.removeListener(onRemoteLocationFocusChanged); + _locationNodeLocal.dispose(); + _locationNodeRemote.dispose(); + }); super.dispose(); } @@ -636,7 +638,6 @@ class _FileManagerPageState extends State }), IconButton( onPressed: () { - breadCrumbScrollToEnd(isLocal); model.refresh(isLocal: isLocal); }, splashRadius: kDesktopIconButtonSplashRadius, @@ -999,9 +1000,7 @@ class _FileManagerPageState extends State } openDirectory(String path, {bool isLocal = false}) { - model.openDirectory(path, isLocal: isLocal).then((_) { - breadCrumbScrollToEnd(isLocal); - }); + model.openDirectory(path, isLocal: isLocal); } void handleDragDone(DropDoneDetails details, bool isLocal) { diff --git a/flutter/lib/mobile/pages/file_manager_page.dart b/flutter/lib/mobile/pages/file_manager_page.dart index 6e5c91484..5a96cda62 100644 --- a/flutter/lib/mobile/pages/file_manager_page.dart +++ b/flutter/lib/mobile/pages/file_manager_page.dart @@ -32,15 +32,17 @@ class _FileManagerPageState extends State { .showLoading(translate('Connecting...'), onCancel: closeConnection); }); gFFI.ffiModel.updateEventListener(widget.id); + model.onDirChanged = (_) => breadCrumbScrollToEnd(); Wakelock.enable(); } @override void dispose() { - model.onClose(); - gFFI.close(); - gFFI.dialogManager.dismissAll(); - Wakelock.disable(); + model.onClose().whenComplete(() { + gFFI.close(); + gFFI.dialogManager.dismissAll(); + Wakelock.disable(); + }); super.dispose(); } @@ -309,7 +311,6 @@ class _FileManagerPageState extends State { } if (entries[index].isDirectory || entries[index].isDrive) { model.openDirectory(entries[index].path); - breadCrumbScrollToEnd(); } else { // Perform file-related tasks. } diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index 4beac01be..ee5c081a6 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -42,6 +42,9 @@ class FileModel extends ChangeNotifier { /// JobTable final _jobTable = List.empty(growable: true).obs; + /// `isLocal` bool + Function(bool)? onDirChanged; + RxList get jobTable => _jobTable; bool get isLocal => _isSelectedLocal; @@ -354,10 +357,12 @@ class FileModel extends ChangeNotifier { await bind.sessionLoadLastTransferJobs(id: '${parent.target?.id}'); } - onClose() { + Future onClose() async { parent.target?.dialogManager.dismissAll(); jobReset(); + onDirChanged = null; + // save config Map msgMap = {}; @@ -367,7 +372,7 @@ class FileModel extends ChangeNotifier { msgMap["remote_show_hidden"] = _remoteOption.showHidden ? "Y" : ""; final id = parent.target?.id ?? ""; for (final msg in msgMap.entries) { - bind.sessionPeerOption(id: id, name: msg.key, value: msg.value); + await bind.sessionPeerOption(id: id, name: msg.key, value: msg.value); } _currentLocalDir.clear(); _currentRemoteDir.clear(); @@ -421,6 +426,7 @@ class FileModel extends ChangeNotifier { _currentRemoteDir = fd; } notifyListeners(); + onDirChanged?.call(isLocal); } catch (e) { debugPrint("Failed to openDirectory $path: $e"); } From 952c8a1bf74c8c461392626021dd9f01d1864d48 Mon Sep 17 00:00:00 2001 From: fufesou Date: Sun, 4 Dec 2022 21:59:26 +0800 Subject: [PATCH 1110/2015] remove unused resource files when packing Signed-off-by: fufesou --- build.py | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/build.py b/build.py index 96d406e09..127469784 100755 --- a/build.py +++ b/build.py @@ -38,13 +38,15 @@ def get_version(): def parse_rc_features(feature): available_features = { 'IddDriver': { - 'zip_url': 'https://github.com/fufesou/RustDeskIddDriver/releases/download/v0.1/RustDeskIddDriver_x64_pic_en.zip', - 'checksum_url': 'https://github.com/fufesou/RustDeskTempTopMostWindow/releases/download/v0.1/checksum_md5', + 'zip_url': 'https://github.com/fufesou/RustDeskIddDriver/releases/download/v0.1/RustDeskIddDriver_x64.zip', + 'checksum_url': 'https://github.com/fufesou/RustDeskIddDriver/releases/download/v0.1/checksum_md5', + 'exclude': ['README.md'], }, 'PrivacyMode': { 'zip_url': 'https://github.com/fufesou/RustDeskTempTopMostWindow/releases/download/v0.1' '/TempTopMostWindow_x64_pic_en.zip', 'checksum_url': 'https://github.com/fufesou/RustDeskTempTopMostWindow/releases/download/v0.1/checksum_md5', + 'include': ['WindowInjection.dll'], } } apply_features = {} @@ -142,8 +144,9 @@ def generate_build_script_for_docker(): def download_extract_features(features, res_dir): - proxy = '' + import re + proxy = '' def req(url): if not proxy: return url @@ -154,6 +157,11 @@ def download_extract_features(features, res_dir): return r for (feat, feat_info) in features.items(): + includes = feat_info['include'] if 'include' in feat_info and feat_info['include'] else [] + includes = [ re.compile(p) for p in includes ] + excludes = feat_info['exclude'] if 'exclude' in feat_info and feat_info['exclude'] else [] + excludes = [ re.compile(p) for p in excludes ] + print(f'{feat} download begin') download_filename = feat_info['zip_url'].split('/')[-1] checksum_md5_response = urllib.request.urlopen( @@ -170,7 +178,22 @@ def download_extract_features(features, res_dir): zip_file = zipfile.ZipFile(filename) zip_list = zip_file.namelist() for f in zip_list: - zip_file.extract(f, res_dir) + file_exclude = False + for p in excludes: + if p.match(f) is not None: + file_exclude = True + break + if file_exclude: + continue + + file_include = False if includes else True + for p in includes: + if p.match(f) is not None: + file_include = True + break + if file_include: + print(f'extract file {f}') + zip_file.extract(f, res_dir) zip_file.close() os.remove(download_filename) print(f'{feat} extract end') From c79b6eb0bb03576fecc0d9ae49fddd91e1c151a4 Mon Sep 17 00:00:00 2001 From: csf Date: Sun, 4 Dec 2022 23:44:03 +0900 Subject: [PATCH 1111/2015] fix file transfer local Windows path can't split --- .../lib/desktop/pages/file_manager_page.dart | 15 +++------ .../lib/mobile/pages/file_manager_page.dart | 11 ++++--- flutter/lib/models/file_model.dart | 33 ++++++++----------- 3 files changed, 23 insertions(+), 36 deletions(-) diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index e5476fc2f..3bccb4b92 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -825,7 +825,7 @@ class _FileManagerPageState extends State final x = offset.dx; final y = offset.dy + size.height + 1; - final isPeerWindows = isWindows(isLocal); + final isPeerWindows = model.getCurrentIsWindows(isLocal); final List menuItems = [ MenuEntryButton( childBuilder: (TextStyle? style) => isPeerWindows @@ -913,7 +913,8 @@ class _FileManagerPageState extends State bool isLocal, void Function(List) onPressed) { final path = model.getCurrentDir(isLocal).path; final breadCrumbList = List.empty(growable: true); - if (isWindows(isLocal) && path == '/') { + final isWindows = model.getCurrentIsWindows(isLocal); + if (isWindows && path == '/') { breadCrumbList.add(BreadCrumbItem( content: TextButton( child: buildWindowsThisPC(), @@ -922,7 +923,7 @@ class _FileManagerPageState extends State onPressed: () => onPressed(['/'])) .marginSymmetric(horizontal: 4))); } else { - final list = PathUtil.split(path, model.getCurrentIsWindows(isLocal)); + final list = PathUtil.split(path, isWindows); breadCrumbList.addAll(list.asMap().entries.map((e) => BreadCrumbItem( content: TextButton( child: Text(e.value), @@ -934,14 +935,6 @@ class _FileManagerPageState extends State return breadCrumbList; } - bool isWindows(bool isLocal) { - if (isLocal) { - return Platform.isWindows; - } else { - return _ffi.ffiModel.pi.platform.toLowerCase() == "windows"; - } - } - breadCrumbScrollToEnd(bool isLocal) { Future.delayed(Duration(milliseconds: 200), () { final breadCrumbScroller = getBreadCrumbScrollController(isLocal); diff --git a/flutter/lib/mobile/pages/file_manager_page.dart b/flutter/lib/mobile/pages/file_manager_page.dart index 5a96cda62..73df2cb01 100644 --- a/flutter/lib/mobile/pages/file_manager_page.dart +++ b/flutter/lib/mobile/pages/file_manager_page.dart @@ -138,7 +138,7 @@ class _FileManagerPageState extends State { child: Row( children: [ Icon( - model.currentShowHidden + model.getCurrentShowHidden() ? Icons.check_box_outlined : Icons.check_box_outline_blank, color: Theme.of(context).iconTheme.color), @@ -185,7 +185,8 @@ class _FileManagerPageState extends State { model.createDir(PathUtil.join( model.currentDir.path, name.value.text, - model.currentIsWindows)); + model + .getCurrentIsWindows())); close(); } }, @@ -351,12 +352,12 @@ class _FileManagerPageState extends State { if (model.currentHome.startsWith(list[0])) { // absolute path for (var item in list) { - path = PathUtil.join(path, item, model.currentIsWindows); + path = PathUtil.join(path, item, model.getCurrentIsWindows()); } } else { path += model.currentHome; for (var item in list) { - path = PathUtil.join(path, item, model.currentIsWindows); + path = PathUtil.join(path, item, model.getCurrentIsWindows()); } } model.openDirectory(path); @@ -500,7 +501,7 @@ class _FileManagerPageState extends State { List getPathBreadCrumbItems( void Function() onHome, void Function(List) onPressed) { final path = model.currentShortPath; - final list = PathUtil.split(path, model.currentIsWindows); + final list = PathUtil.split(path, model.getCurrentIsWindows()); final breadCrumbList = [ BreadCrumbItem( content: IconButton( diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index ee5c081a6..182f51ef8 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; @@ -144,18 +145,14 @@ class FileModel extends ChangeNotifier { } } - bool get currentShowHidden => - _isSelectedLocal ? _localOption.showHidden : _remoteOption.showHidden; - - bool getCurrentShowHidden(bool isLocal) { - return isLocal ? _localOption.showHidden : _remoteOption.showHidden; + bool getCurrentShowHidden([bool? isLocal]) { + final isLocal_ = isLocal ?? _isSelectedLocal; + return isLocal_ ? _localOption.showHidden : _remoteOption.showHidden; } - bool get currentIsWindows => - _isSelectedLocal ? _localOption.isWindows : _remoteOption.isWindows; - - bool getCurrentIsWindows(bool isLocal) { - return isLocal ? _localOption.isWindows : _remoteOption.isWindows; + bool getCurrentIsWindows([bool? isLocal]) { + final isLocal_ = isLocal ?? _isSelectedLocal; + return isLocal_ ? _localOption.isWindows : _remoteOption.isWindows; } final _fileFetcher = FileFetcher(); @@ -330,13 +327,13 @@ class FileModel extends ChangeNotifier { _localOption.showHidden = (await bind.sessionGetPeerOption( id: parent.target?.id ?? "", name: "local_show_hidden")) .isNotEmpty; + _localOption.isWindows = Platform.isWindows; _remoteOption.showHidden = (await bind.sessionGetPeerOption( id: parent.target?.id ?? "", name: "remote_show_hidden")) .isNotEmpty; - _remoteOption.isWindows = parent.target?.ffiModel.pi.platform == "Windows"; - - debugPrint("remote platform: ${parent.target?.ffiModel.pi.platform}"); + _remoteOption.isWindows = + parent.target?.ffiModel.pi.platform.toLowerCase() == "windows"; await Future.delayed(Duration(milliseconds: 100)); @@ -404,14 +401,10 @@ class FileModel extends ChangeNotifier { if (!isBack) { pushHistory(isLocal); } - final showHidden = - isLocal ? _localOption.showHidden : _remoteOption.showHidden; - final isWindows = - isLocal ? _localOption.isWindows : _remoteOption.isWindows; + final showHidden = getCurrentShowHidden(isLocal); + final isWindows = getCurrentIsWindows(isLocal); // process /C:\ -> C:\ on Windows - if (isLocal - ? _localOption.isWindows - : _remoteOption.isWindows && path.length > 1 && path[0] == '/') { + if (isWindows && path.length > 1 && path[0] == '/') { path = path.substring(1); if (path[path.length - 1] != '\\') { path = "$path\\"; From 763c314253600e94f170a2edc46fdeab4d98e607 Mon Sep 17 00:00:00 2001 From: csf Date: Sun, 4 Dec 2022 23:51:48 +0900 Subject: [PATCH 1112/2015] opt file transfer Windows BreadCrumbScroll --- flutter/lib/desktop/pages/file_manager_page.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 3bccb4b92..88f470717 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -801,7 +801,8 @@ class _FileManagerPageState extends State onPointerSignal: (e) { if (e is PointerScrollEvent) { final sc = getBreadCrumbScrollController(isLocal); - sc.jumpTo(sc.offset + e.scrollDelta.dy / 4); + final scale = Platform.isWindows ? 2 : 4; + sc.jumpTo(sc.offset + e.scrollDelta.dy / scale); } }, child: BreadCrumb( From 8d1254cf14b69f545c9cefa026c5eeb0e7dd3e7c Mon Sep 17 00:00:00 2001 From: rustdesk Date: Mon, 5 Dec 2022 00:26:13 +0800 Subject: [PATCH 1113/2015] fix is_login_wayland --- flutter/pubspec.lock | 4 ++-- src/platform/linux.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 9a4d3d77b..a24f73954 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -257,8 +257,8 @@ packages: dependency: "direct main" description: path: "." - ref: cb086219bd4760a95a483cb14c1791d2a39ca5a0 - resolved-ref: cb086219bd4760a95a483cb14c1791d2a39ca5a0 + ref: "82f9eab81cb2c7bfb938def7a1b399a6279bbc75" + resolved-ref: "82f9eab81cb2c7bfb938def7a1b399a6279bbc75" url: "https://github.com/Kingtous/rustdesk_desktop_multi_window" source: git version: "0.1.0" diff --git a/src/platform/linux.rs b/src/platform/linux.rs index c8abe432e..82d6592db 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -416,9 +416,9 @@ fn get_display() -> String { pub fn is_login_wayland() -> bool { if let Ok(contents) = std::fs::read_to_string("/etc/gdm3/custom.conf") { - contents.contains("#WaylandEnable=false") + contents.contains("#WaylandEnable=false") || contents.contains("WaylandEnable=true") } else if let Ok(contents) = std::fs::read_to_string("/etc/gdm/custom.conf") { - contents.contains("#WaylandEnable=false") + contents.contains("#WaylandEnable=false") || contents.contains("WaylandEnable=true") } else { false } From 251ce41d36558ddbc828e60e688e9b1bbf4d2c92 Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 5 Dec 2022 09:33:01 +0800 Subject: [PATCH 1114/2015] fix menu padding mistake Signed-off-by: 21pages --- flutter/lib/common/widgets/peer_tab_page.dart | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/flutter/lib/common/widgets/peer_tab_page.dart b/flutter/lib/common/widgets/peer_tab_page.dart index 01e3939ee..5a498a1c4 100644 --- a/flutter/lib/common/widgets/peer_tab_page.dart +++ b/flutter/lib/common/widgets/peer_tab_page.dart @@ -29,6 +29,10 @@ class _TabEntry { _TabEntry(this.name, this.widget, this.load); } +EdgeInsets? _menuPadding() { + return isDesktop ? kDesktopMenuPadding : null; +} + class _PeerTabPageState extends State with SingleTickerProviderStateMixin { late final RxInt tabHiddenFlag; @@ -38,25 +42,25 @@ class _PeerTabPageState extends State _TabEntry( 'Recent Sessions', RecentPeersView( - menuPadding: kDesktopMenuPadding, + menuPadding: _menuPadding(), ), bind.mainLoadRecentPeers), _TabEntry( 'Favorites', FavoritePeersView( - menuPadding: kDesktopMenuPadding, + menuPadding: _menuPadding(), ), bind.mainLoadFavPeers), _TabEntry( 'Discovered', DiscoveredPeersView( - menuPadding: kDesktopMenuPadding, + menuPadding: _menuPadding(), ), bind.mainDiscover), _TabEntry( 'Address Book', - const AddressBook( - menuPadding: kDesktopMenuPadding, + AddressBook( + menuPadding: _menuPadding(), ), () => {}), ]; From c3ea787aa8847c3b9045770b20f6c29cb5da5b7d Mon Sep 17 00:00:00 2001 From: rustdesk Date: Mon, 5 Dec 2022 17:01:12 +0800 Subject: [PATCH 1115/2015] remove tray manager in pod lock --- flutter/macos/Podfile.lock | 6 ------ 1 file changed, 6 deletions(-) diff --git a/flutter/macos/Podfile.lock b/flutter/macos/Podfile.lock index 952996e5f..8d41945c8 100644 --- a/flutter/macos/Podfile.lock +++ b/flutter/macos/Podfile.lock @@ -20,8 +20,6 @@ PODS: - sqflite (0.0.2): - FlutterMacOS - FMDB (>= 2.7.5) - - tray_manager (0.0.1): - - FlutterMacOS - uni_links_desktop (0.0.1): - FlutterMacOS - url_launcher_macos (0.0.1): @@ -43,7 +41,6 @@ DEPENDENCIES: - path_provider_macos (from `Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos`) - screen_retriever (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos`) - sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/macos`) - - tray_manager (from `Flutter/ephemeral/.symlinks/plugins/tray_manager/macos`) - uni_links_desktop (from `Flutter/ephemeral/.symlinks/plugins/uni_links_desktop/macos`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) - wakelock_macos (from `Flutter/ephemeral/.symlinks/plugins/wakelock_macos/macos`) @@ -73,8 +70,6 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos sqflite: :path: Flutter/ephemeral/.symlinks/plugins/sqflite/macos - tray_manager: - :path: Flutter/ephemeral/.symlinks/plugins/tray_manager/macos uni_links_desktop: :path: Flutter/ephemeral/.symlinks/plugins/uni_links_desktop/macos url_launcher_macos: @@ -97,7 +92,6 @@ SPEC CHECKSUMS: path_provider_macos: 3c0c3b4b0d4a76d2bf989a913c2de869c5641a19 screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38 sqflite: a5789cceda41d54d23f31d6de539d65bb14100ea - tray_manager: 9064e219c56d75c476e46b9a21182087930baf90 uni_links_desktop: 45900fb319df48fcdea2df0756e9c2626696b026 url_launcher_macos: 597e05b8e514239626bcf4a850fcf9ef5c856ec3 wakelock_macos: bc3f2a9bd8d2e6c89fee1e1822e7ddac3bd004a9 From cbc372991bc52a77becea81e4b87ad4332ba5bc4 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 5 Dec 2022 19:40:49 +0800 Subject: [PATCH 1116/2015] feat: add skip feature --- libs/hbb_common/src/fs.rs | 36 ++++++++++++++++++++++--- src/client/io_loop.rs | 57 ++++++++++++++++++++++++--------------- src/flutter.rs | 4 +-- src/lang/cn.rs | 1 + src/ui/file_transfer.tis | 11 ++++++-- 5 files changed, 80 insertions(+), 29 deletions(-) diff --git a/libs/hbb_common/src/fs.rs b/libs/hbb_common/src/fs.rs index dd8a7530e..8477c82ff 100644 --- a/libs/hbb_common/src/fs.rs +++ b/libs/hbb_common/src/fs.rs @@ -215,6 +215,8 @@ pub struct TransferJob { transferred: u64, enable_overwrite_detection: bool, file_confirmed: bool, + // indicating the last file is skipped + file_skipped: bool, file_is_waiting: bool, default_overwrite_strategy: Option, } @@ -541,25 +543,50 @@ impl TransferJob { pub fn set_file_confirmed(&mut self, file_confirmed: bool) { log::info!("id: {}, file_confirmed: {}", self.id, file_confirmed); self.file_confirmed = file_confirmed; + self.file_skipped = false; } pub fn set_file_is_waiting(&mut self, file_is_waiting: bool) { self.file_is_waiting = file_is_waiting; } + #[inline] pub fn file_is_waiting(&self) -> bool { self.file_is_waiting } + #[inline] pub fn file_confirmed(&self) -> bool { self.file_confirmed } - pub fn skip_current_file(&mut self) -> bool { + /// Indicating whether the last file is skipped + #[inline] + pub fn file_skipped(&self) -> bool { + self.file_skipped + } + + /// Indicating whether the whole task is skipped + #[inline] + pub fn job_skipped(&self) -> bool { + self.file_skipped() && self.files.len() == 1 + } + + /// Get job error message, useful for getting status when job had finished + pub fn job_error(&self) -> Option { + if self.job_skipped() { + return Some("skipped".to_string()); + } + None + } + + pub fn set_file_skipped(&mut self) -> bool { + log::debug!("skip file {} in job {}", self.file_num, self.id); self.file.take(); self.set_file_confirmed(false); self.set_file_is_waiting(false); self.file_num += 1; + self.file_skipped = true; true } @@ -571,7 +598,7 @@ impl TransferJob { Some(file_transfer_send_confirm_request::Union::Skip(s)) => { if s { log::debug!("skip file id:{}, file_num:{}", r.id, r.file_num); - self.skip_current_file(); + self.set_file_skipped(); } else { self.set_file_confirmed(true); } @@ -719,7 +746,10 @@ pub async fn handle_read_jobs( Ok(None) => { if !job.enable_overwrite_detection || (!job.file_confirmed && !job.file_is_waiting) { - finished.push(job.id()); + // for getting error detail, we do not remove this job, we will handle it in io loop + if job.job_error().is_none() { + finished.push(job.id()); + } stream.send(&new_done(job.id(), job.file_num())).await?; } else { // waiting confirmation. diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 16f91d89d..d7468ea9a 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -499,7 +499,7 @@ impl Remote { } let mut msg = Message::new(); let mut file_action = FileAction::new(); - file_action.set_send_confirm(FileTransferSendConfirmRequest { + let req = FileTransferSendConfirmRequest { id, file_num, union: if need_override { @@ -508,7 +508,9 @@ impl Remote { Some(file_transfer_send_confirm_request::Union::Skip(true)) }, ..Default::default() - }); + }; + job.confirm(&req); + file_action.set_send_confirm(req); msg.set_file_action(file_action); allow_err!(peer.send(&msg).await); } @@ -862,28 +864,30 @@ impl Remote { match fs::is_write_need_confirmation(&write_path, &digest) { Ok(res) => match res { DigestCheckResult::IsSame => { - let msg= new_send_confirm(FileTransferSendConfirmRequest { + let req = FileTransferSendConfirmRequest { id: digest.id, file_num: digest.file_num, union: Some(file_transfer_send_confirm_request::Union::Skip(true)), ..Default::default() - }); + }; + job.confirm(&req); + let msg = new_send_confirm(req); allow_err!(peer.send(&msg).await); } DigestCheckResult::NeedConfirm(digest) => { if let Some(overwrite) = overwrite_strategy { - let msg = new_send_confirm( - FileTransferSendConfirmRequest { - id: digest.id, - file_num: digest.file_num, - union: Some(if overwrite { - file_transfer_send_confirm_request::Union::OffsetBlk(0) - } else { - file_transfer_send_confirm_request::Union::Skip(true) - }), - ..Default::default() - }, - ); + let req = FileTransferSendConfirmRequest { + id: digest.id, + file_num: digest.file_num, + union: Some(if overwrite { + file_transfer_send_confirm_request::Union::OffsetBlk(0) + } else { + file_transfer_send_confirm_request::Union::Skip(true) + }), + ..Default::default() + }; + job.confirm(&req); + let msg = new_send_confirm(req); allow_err!(peer.send(&msg).await); } else { self.handler.override_file_confirm( @@ -895,14 +899,14 @@ impl Remote { } } DigestCheckResult::NoSuchFile => { - let msg = new_send_confirm( - FileTransferSendConfirmRequest { + let req = FileTransferSendConfirmRequest { id: digest.id, file_num: digest.file_num, union: Some(file_transfer_send_confirm_request::Union::OffsetBlk(0)), ..Default::default() - }, - ); + }; + job.confirm(&req); + let msg = new_send_confirm(req); allow_err!(peer.send(&msg).await); } }, @@ -915,6 +919,7 @@ impl Remote { } } Some(file_response::Union::Block(block)) => { + log::debug!("recv block: {}", block.blk_id); if let Some(job) = fs::get_job(block.id, &mut self.write_jobs) { if let Err(_err) = job.write(block).await { // to-do: add "skip" for writing job @@ -923,11 +928,18 @@ impl Remote { } } Some(file_response::Union::Done(d)) => { + let mut err: Option = None; if let Some(job) = fs::get_job(d.id, &mut self.write_jobs) { job.modify_time(); + err = job.job_error(); fs::remove_job(d.id, &mut self.write_jobs); } - self.handle_job_status(d.id, d.file_num, None); + if let Some(job) = fs::get_job(d.id, &mut self.read_jobs) { + job.modify_time(); + err = job.job_error(); + fs::remove_job(d.id, &mut self.read_jobs); + } + self.handle_job_status(d.id, d.file_num, err); } Some(file_response::Union::Error(e)) => { self.handle_job_status(e.id, e.file_num, Some(e.error)); @@ -976,7 +988,8 @@ impl Remote { self.handler.ui_handler.switch_display(&s); self.video_sender.send(MediaData::Reset).ok(); if s.width > 0 && s.height > 0 { - self.handler.set_display(s.x, s.y, s.width, s.height, s.cursor_embeded); + self.handler + .set_display(s.x, s.y, s.width, s.height, s.cursor_embeded); } } Some(misc::Union::CloseReason(c)) => { diff --git a/src/flutter.rs b/src/flutter.rs index a2c307f5a..6924bada3 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -210,10 +210,10 @@ impl InvokeUiSession for FlutterHandler { ); } - fn job_done(&self, id: i32, file_num: i32) { + fn job_done(&self, id: i32, file_num: i32, skipped: bool) { self.push_event( "job_done", - vec![("id", &id.to_string()), ("file_num", &file_num.to_string())], + vec![("id", &id.to_string()), ("file_num", &file_num.to_string()), ("skipped", skipped)], ); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 1b49f6c4a..c429d9539 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -399,5 +399,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", "在只允许密码连接并且只用固定密码的情况下才允许隐藏"), ("wayland_experiment_tip", ""), ("Right click to select tabs", "右键选择选项卡"), + ("Skipped", "已跳过"), ].iter().cloned().collect(); } diff --git a/src/ui/file_transfer.tis b/src/ui/file_transfer.tis index 451117403..f69f6d323 100644 --- a/src/ui/file_transfer.tis +++ b/src/ui/file_transfer.tis @@ -245,7 +245,13 @@ class JobTable: Reactor.Component { var percent = job.total_size == 0 ? 100 : (100. * job.finished_size / job.total_size).toInteger(); // (100. * i / (n || 1)).toInteger(); if (job.finished) percent = '100'; if (percent) res += ", " + percent + "%"; - if (job.finished) res = translate("Finished") + " " + res; + if (job.finished) { + if (job.err == "skipped") { + res = translate("Skipped") + " " + res; + } else { + res = translate("Finished") + " " + res; + } + } if (job.speed) res += ", " + getSize(0, job.speed) + "/s"; return res; } @@ -268,9 +274,10 @@ class JobTable: Reactor.Component { if (file_num < job.file_num) return; job.file_num = file_num; var n = job.num_entries || job.entries.length; - job.finished = job.file_num >= n - 1 || err == "cancel"; + job.finished = job.file_num >= n - 1 || err == "cancel" || err == "skipped"; job.finished_size = finished_size; job.speed = speed || 0; + job.err = err; this.updateJob(job); if (job.type == "del-dir") { if (job.finished) { From 97066080bfd28e9256a88a5cbe8c24a771ce5396 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 5 Dec 2022 20:09:48 +0800 Subject: [PATCH 1117/2015] feat: skip status for flutter --- .../lib/desktop/pages/file_manager_page.dart | 2 +- flutter/lib/models/file_model.dart | 22 +++++++++++++++++++ src/client/io_loop.rs | 1 - src/flutter.rs | 4 ++-- 4 files changed, 25 insertions(+), 4 deletions(-) diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 88f470717..b2e5232ab 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -458,7 +458,7 @@ class _FileManagerPageState extends State Wrap( children: [ Text( - '${item.state.display()} ${max(0, item.fileNum)}/${item.fileCount} '), + '${item.display()} ${max(0, item.fileNum)}/${item.fileCount} '), Text( '${translate("files")} ${readableFileSize(item.totalSize.toDouble())} '), Offstage( diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index 182f51ef8..744a3a502 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -268,6 +268,7 @@ class FileModel extends ChangeNotifier { } jobError(Map evt) { + final err = evt['err'].toString(); if (!isDesktop) { if (_jobResultListener.isListening) { _jobResultListener.complete(evt); @@ -275,12 +276,24 @@ class FileModel extends ChangeNotifier { } _selectMode = false; _jobProgress.clear(); + _jobProgress.err = err; _jobProgress.state = JobState.error; + _jobProgress.fileNum = int.parse(evt['file_num']); + if (err == "skipped") { + _jobProgress.state = JobState.done; + _jobProgress.finishedSize = _jobProgress.totalSize; + } } else { int jobIndex = getJob(int.parse(evt['id'])); if (jobIndex != -1) { final job = jobTable[jobIndex]; job.state = JobState.error; + job.err = err; + job.fileNum = int.parse(evt['file_num']); + if (err == "skipped") { + job.state = JobState.done; + job.finishedSize = job.totalSize; + } } } debugPrint("jobError $evt"); @@ -1089,6 +1102,7 @@ class JobProgress { var remote = ""; var to = ""; var showHidden = false; + var err = ""; clear() { state = JobState.none; @@ -1100,6 +1114,14 @@ class JobProgress { fileCount = 0; remote = ""; to = ""; + err = ""; + } + + String display() { + if (state == JobState.done && err == "skipped") { + return translate("Skipped"); + } + return state.display(); } } diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index d7468ea9a..5adca6d81 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -919,7 +919,6 @@ impl Remote { } } Some(file_response::Union::Block(block)) => { - log::debug!("recv block: {}", block.blk_id); if let Some(job) = fs::get_job(block.id, &mut self.write_jobs) { if let Err(_err) = job.write(block).await { // to-do: add "skip" for writing job diff --git a/src/flutter.rs b/src/flutter.rs index 6924bada3..a2c307f5a 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -210,10 +210,10 @@ impl InvokeUiSession for FlutterHandler { ); } - fn job_done(&self, id: i32, file_num: i32, skipped: bool) { + fn job_done(&self, id: i32, file_num: i32) { self.push_event( "job_done", - vec![("id", &id.to_string()), ("file_num", &file_num.to_string()), ("skipped", skipped)], + vec![("id", &id.to_string()), ("file_num", &file_num.to_string())], ); } From bbddbde6a03818a6d6c78057768fbc8f84eb30bb Mon Sep 17 00:00:00 2001 From: csf Date: Mon, 5 Dec 2022 21:57:08 +0900 Subject: [PATCH 1118/2015] mobile skipping info & fix mobile breadCrumbScroller has no client error --- flutter/lib/desktop/pages/file_manager_page.dart | 4 ++-- flutter/lib/mobile/pages/file_manager_page.dart | 12 +++++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index b2e5232ab..9d8ef6f7a 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -489,8 +489,8 @@ class _FileManagerPageState extends State icon: const Icon(Icons.restart_alt_rounded)), ), IconButton( - icon: const Icon(Icons.delete_forever_outlined), - splashRadius: kDesktopIconButtonSplashRadius, + icon: const Icon(Icons.close), + splashRadius: 1, onPressed: () { model.jobTable.removeAt(index); model.cancelJob(item.id); diff --git a/flutter/lib/mobile/pages/file_manager_page.dart b/flutter/lib/mobile/pages/file_manager_page.dart index 73df2cb01..549a44b78 100644 --- a/flutter/lib/mobile/pages/file_manager_page.dart +++ b/flutter/lib/mobile/pages/file_manager_page.dart @@ -335,10 +335,12 @@ class _FileManagerPageState extends State { breadCrumbScrollToEnd() { Future.delayed(Duration(milliseconds: 200), () { - _breadCrumbScroller.animateTo( - _breadCrumbScroller.position.maxScrollExtent, - duration: Duration(milliseconds: 200), - curve: Curves.fastLinearToSlowEaseIn); + if (_breadCrumbScroller.hasClients) { + _breadCrumbScroller.animateTo( + _breadCrumbScroller.position.maxScrollExtent, + duration: Duration(milliseconds: 200), + curve: Curves.fastLinearToSlowEaseIn); + } }); } @@ -479,7 +481,7 @@ class _FileManagerPageState extends State { return BottomSheetBody( leading: Icon(Icons.check), title: "${translate("Successful")}!", - text: "", + text: model.jobProgress.display(), onCanceled: () => model.jobReset(), ); case JobState.error: From aa746e665886d8f0fa6cdd0272f4452f1307db87 Mon Sep 17 00:00:00 2001 From: "Miguel F. G" <116861809+flusheDData@users.noreply.github.com> Date: Mon, 5 Dec 2022 13:58:46 +0100 Subject: [PATCH 1119/2015] 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 39d031c9d..d8b00b9bf 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -397,7 +397,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request access to your device", "Solicitud de acceso a su dispositivo"), ("Hide connection management window", "Ocultar ventana de gestión de conexión"), ("hide_cm_tip", "Permitir ocultar solo si se aceptan sesiones a través de contraseña y usando contraseña permanente"), - ("wayland_experiment_tip", ""), - ("Right click to select tabs", ""), + ("wayland_experiment_tip", "El soporte para Wayland está en fase experimental, por favor, use X11 si necesita acceso desatendido."), + ("Right click to select tabs", "Clic derecho para seleccionar pestañas"), ].iter().cloned().collect(); } From 80cf4c5b157bdafd2a9c00da3db783192a8c217a Mon Sep 17 00:00:00 2001 From: solokot Date: Mon, 5 Dec 2022 19:13:36 +0300 Subject: [PATCH 1120/2015] Update ru.rs --- src/lang/ru.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 76a8cca87..e7cc686c2 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -398,6 +398,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Hide connection management window", "Скрывать окно управления соединениями"), ("hide_cm_tip", "Разрешать скрытие случае, если принимаются сеансы по паролю или используется постоянный пароль"), ("wayland_experiment_tip", "Поддержка Wayland находится на экспериментальной стадии, используйте X11, если вам требуется автоматический доступ."), - ("Right click to select tabs", ""), + ("Right click to select tabs", "Выбор вкладок щелчком правой кнопки мыши"), ].iter().cloned().collect(); } From fa9d12a7ac7eeae76330867bbad3ffbd2c84bbfd Mon Sep 17 00:00:00 2001 From: Manos G <87467035+7th-fret@users.noreply.github.com> Date: Mon, 5 Dec 2022 19:42:27 +0200 Subject: [PATCH 1121/2015] Update gr.rs --- src/lang/gr.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lang/gr.rs b/src/lang/gr.rs index 986919a8b..e4055e16f 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -397,6 +397,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request access to your device", "Αίτημα πρόσβασης στη συσκευή σας"), ("Hide connection management window", "Απόκρυψη παραθύρου διαχείρισης σύνδεσης"), ("hide_cm_tip", "Να επιτρέπεται η απόκρυψη, μόνο εάν αποδέχεστε συνδέσεις μέσω κωδικού πρόσβασης και χρησιμοποιείτε μόνιμο κωδικό πρόσβασης"), - ("Right click to select tabs", ""), + ("wayland_experiment_tip", "Η υποστήριξη Wayland βρίσκεται σε πειραματικό στάδιο, χρησιμοποιήστε το X11 εάν χρειάζεστε πρόσβαση χωρίς επίβλεψη."), + ("Right click to select tabs", "Κάντε δεξί κλικ για να επιλέξετε καρτέλες"), ].iter().cloned().collect(); } From 5616b20879fc6ec5e97e06296606b605b43ff71f Mon Sep 17 00:00:00 2001 From: 21pages Date: Sun, 27 Nov 2022 12:16:45 +0800 Subject: [PATCH 1122/2015] opt add ab id Signed-off-by: 21pages --- flutter/lib/common/widgets/address_book.dart | 105 ++++++++++++++----- flutter/lib/common/widgets/peer_card.dart | 29 +++++ flutter/lib/models/ab_model.dart | 14 ++- 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/pl.rs | 1 + src/lang/pt_PT.rs | 1 + src/lang/ptbr.rs | 1 + src/lang/ru.rs | 1 + src/lang/sk.rs | 1 + src/lang/sq.rs | 1 + src/lang/sv.rs | 1 + src/lang/template.rs | 1 + src/lang/tr.rs | 1 + src/lang/tw.rs | 1 + src/lang/ua.rs | 1 + src/lang/vn.rs | 1 + 31 files changed, 148 insertions(+), 28 deletions(-) diff --git a/flutter/lib/common/widgets/address_book.dart b/flutter/lib/common/widgets/address_book.dart index 5b3527fa0..799b0be67 100644 --- a/flutter/lib/common/widgets/address_book.dart +++ b/flutter/lib/common/widgets/address_book.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common/formatter/id_formatter.dart'; import 'package:flutter_hbb/common/widgets/peer_card.dart'; import 'package:flutter_hbb/common/widgets/peers_view.dart'; import 'package:flutter_hbb/desktop/widgets/popup_menu.dart'; @@ -237,29 +238,32 @@ class _AddressBookState extends State { } void abAddId() async { - var field = ""; - var msg = ""; var isInProgress = false; - TextEditingController controller = TextEditingController(text: field); + IDTextEditingController idController = IDTextEditingController(text: ''); + TextEditingController aliasController = TextEditingController(text: ''); + final tags = List.of(gFFI.abModel.tags); + var selectedTag = List.empty(growable: true).obs; + final style = TextStyle(fontSize: 14.0); + String? errorMsg; gFFI.dialogManager.show((setState, close) { submit() async { setState(() { - msg = ""; isInProgress = true; + errorMsg = null; }); - field = controller.text.trim(); - if (field.isEmpty) { + String id = idController.id; + if (id.isEmpty) { // pass } else { - final ids = field.trim().split(RegExp(r"[\s,;\n]+")); - field = ids.join(','); - for (final newId in ids) { - if (gFFI.abModel.idContainBy(newId)) { - continue; - } - gFFI.abModel.addId(newId); + if (gFFI.abModel.idContainBy(id)) { + setState(() { + isInProgress = false; + errorMsg = translate('ID already exists'); + }); + return; } + gFFI.abModel.addId(id, aliasController.text.trim(), selectedTag); await gFFI.abModel.pushAb(); this.setState(() {}); // final currentPeers @@ -272,21 +276,70 @@ class _AddressBookState extends State { content: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(translate("whitelist_sep")), - const SizedBox( - height: 8.0, - ), - Row( + Column( children: [ - Expanded( - child: TextField( - maxLines: null, - decoration: InputDecoration( - border: const OutlineInputBorder(), - errorText: msg.isEmpty ? null : translate(msg), + Align( + alignment: Alignment.centerLeft, + child: Row( + children: [ + Text( + '*', + style: TextStyle(color: Colors.red, fontSize: 14), ), - controller: controller, - focusNode: FocusNode()..requestFocus()), + Text( + 'ID', + style: style, + ), + ], + ), + ), + TextField( + controller: idController, + inputFormatters: [IDTextInputFormatter()], + decoration: InputDecoration( + isDense: true, + border: OutlineInputBorder(), + errorText: errorMsg), + style: style, + ), + Align( + alignment: Alignment.centerLeft, + child: Text( + translate('Alias'), + style: style, + ), + ).marginOnly(top: 8, bottom: 2), + TextField( + controller: aliasController, + decoration: InputDecoration( + border: OutlineInputBorder(), + isDense: true, + ), + style: style, + ), + Align( + alignment: Alignment.centerLeft, + child: Text( + translate('Tags'), + style: style, + ), + ).marginOnly(top: 8), + Container( + child: Wrap( + children: tags + .map((e) => AddressBookTag( + name: e, + tags: selectedTag, + onTap: () { + if (selectedTag.contains(e)) { + selectedTag.remove(e); + } else { + selectedTag.add(e); + } + }, + showActionMenu: false)) + .toList(growable: false), + ), ), ], ), diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index 8df84af6c..4486a6e27 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -586,6 +586,26 @@ abstract class BasePeerCard extends StatelessWidget { ); } + @protected + MenuEntryBase _addToAb(Peer peer) { + return MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Add to Address Book'), + style: style, + ), + proc: () { + () async { + if (!gFFI.abModel.idContainBy(peer.id)) { + gFFI.abModel.addPeer(peer); + await gFFI.abModel.pushAb(); + } + }(); + }, + padding: menuPadding, + dismissOnClicked: true, + ); + } + void _rename(String id, bool isAddressBook) async { RxBool isInProgress = false.obs; var name = peer.alias; @@ -679,6 +699,9 @@ class RecentPeerCard extends BasePeerCard { menuItems.add(_unrememberPasswordAction(peer.id)); } menuItems.add(_addFavAction(peer.id)); + if (!gFFI.abModel.idContainBy(peer.id)) { + menuItems.add(_addToAb(peer)); + } return menuItems; } } @@ -716,6 +739,9 @@ class FavoritePeerCard extends BasePeerCard { menuItems.add(_rmFavAction(peer.id, () async { await bind.mainLoadFavPeers(); })); + if (!gFFI.abModel.idContainBy(peer.id)) { + menuItems.add(_addToAb(peer)); + } return menuItems; } } @@ -744,6 +770,9 @@ class DiscoveredPeerCard extends BasePeerCard { } menuItems.add(MenuEntryDivider()); menuItems.add(_removeAction(peer.id, () async {})); + if (!gFFI.abModel.idContainBy(peer.id)) { + menuItems.add(_addToAb(peer)); + } return menuItems; } } diff --git a/flutter/lib/models/ab_model.dart b/flutter/lib/models/ab_model.dart index afee97e75..a24c01366 100644 --- a/flutter/lib/models/ab_model.dart +++ b/flutter/lib/models/ab_model.dart @@ -68,11 +68,21 @@ class AbModel { peers.clear(); } - void addId(String id) async { + void addId(String id, String alias, List tags) { if (idContainBy(id)) { return; } - peers.add(Peer.fromJson({"id": id})); + final peer = Peer.fromJson({ + 'id': id, + 'alias': alias, + 'tags': tags, + }); + peers.add(peer); + } + + void addPeer(Peer peer) { + peers.removeWhere((e) => e.id == peer.id); + peers.add(peer); } void addTag(String tag) async { diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 6fc919b8d..70190729c 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -399,5 +399,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", ""), ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), + ("Add to Address Book", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index c429d9539..daa2af065 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -400,5 +400,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("wayland_experiment_tip", ""), ("Right click to select tabs", "右键选择选项卡"), ("Skipped", "已跳过"), + ("Add to Address Book", "添加到地址簿"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 6455023de..33c6492f7 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -399,5 +399,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", ""), ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), + ("Add to Address Book", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index afd6476ee..1aa53ca57 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -399,5 +399,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", ""), ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), + ("Add to Address Book", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 273a607ed..223237def 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -399,5 +399,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", "Dies ist nur möglich, wenn der Zugriff nur über ein permanentes Passwort erfolgt."), // Sehr unklar. Muss noch angepasst werden. Original: Allow hiding only if accepting sessions via password and using pernament passw"), ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), + ("Add to Address Book", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 3a38b6601..c2748a9bc 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -399,5 +399,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", ""), ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), + ("Add to Address Book", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index d8b00b9bf..1069b4905 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -399,5 +399,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", "Permitir ocultar solo si se aceptan sesiones a través de contraseña y usando contraseña permanente"), ("wayland_experiment_tip", "El soporte para Wayland está en fase experimental, por favor, use X11 si necesita acceso desatendido."), ("Right click to select tabs", "Clic derecho para seleccionar pestañas"), + ("Add to Address Book", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 7513f84e8..11c17887a 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -399,5 +399,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", "فقط در صورت پذیرفتن جلسات از طریق رمز عبور و استفاده از رمز عبور دائمی، مخفی شدن مجاز است"), ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), + ("Add to Address Book", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index a4eedfa2a..c3d241bf8 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -399,5 +399,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", "Autoriser le masquage uniquement si vous acceptez des sessions via un mot de passe et utilisez un mot de passe permanent"), ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), + ("Add to Address Book", ""), ].iter().cloned().collect(); } diff --git a/src/lang/gr.rs b/src/lang/gr.rs index e4055e16f..ecabd8f31 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -399,5 +399,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", "Να επιτρέπεται η απόκρυψη, μόνο εάν αποδέχεστε συνδέσεις μέσω κωδικού πρόσβασης και χρησιμοποιείτε μόνιμο κωδικό πρόσβασης"), ("wayland_experiment_tip", "Η υποστήριξη Wayland βρίσκεται σε πειραματικό στάδιο, χρησιμοποιήστε το X11 εάν χρειάζεστε πρόσβαση χωρίς επίβλεψη."), ("Right click to select tabs", "Κάντε δεξί κλικ για να επιλέξετε καρτέλες"), + ("Add to Address Book", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 941caeac1..d0f2f4412 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -399,5 +399,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", ""), ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), + ("Add to Address Book", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 7f4a50523..b8f9e392d 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -399,5 +399,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", ""), ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), + ("Add to Address Book", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 98ea9366e..f11a06c80 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -399,5 +399,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", "Permetti di nascondere solo se si accettano sessioni con password permanente"), ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), + ("Add to Address Book", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 4121dd55c..4ca33e76a 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -399,5 +399,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", ""), ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), + ("Add to Address Book", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index a1cd730e9..93338165b 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -399,5 +399,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", ""), ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), + ("Add to Address Book", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 1e623c0b6..a7d6f299d 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -399,5 +399,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", ""), ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), + ("Add to Address Book", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index ed14e3ca2..d3f991d44 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -399,5 +399,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", ""), ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), + ("Add to Address Book", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 65620e3e8..4a457218c 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -399,5 +399,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", ""), ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), + ("Add to Address Book", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index cf388a88c..af59e4f2e 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -399,5 +399,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", ""), ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), + ("Add to Address Book", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index e7cc686c2..ff8dfc316 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -399,5 +399,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", "Разрешать скрытие случае, если принимаются сеансы по паролю или используется постоянный пароль"), ("wayland_experiment_tip", "Поддержка Wayland находится на экспериментальной стадии, используйте X11, если вам требуется автоматический доступ."), ("Right click to select tabs", "Выбор вкладок щелчком правой кнопки мыши"), + ("Add to Address Book", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index e7a9b12b5..13672d086 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -399,5 +399,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", ""), ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), + ("Add to Address Book", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 1926c849b..5ec59c4be 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -399,5 +399,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", "Kjo është e mundur vetëm nëse aksesi bëhet nëpërmjet një fjalëkalimi të përhershëm"), ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), + ("Add to Address Book", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index cfef903a7..1feb5d55e 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -399,5 +399,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", "Tillåt att gömma endast om accepterande sessioner med lösenord och permanenta lösenord"), ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), + ("Add to Address Book", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index ed7189ce6..6993cb43c 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -399,5 +399,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", ""), ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), + ("Add to Address Book", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 6c518da81..7b66af60e 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -399,5 +399,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", ""), ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), + ("Add to Address Book", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index e7c024420..6f0e8806b 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -399,5 +399,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", "在只允許密碼連接並且只用固定密碼的情況下才允許隱藏"), ("wayland_experiment_tip", ""), ("Right click to select tabs", "右鍵選擇選項卡"), + ("Add to Address Book", "添加到地址簿"), ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 713d15c69..92fd2db8a 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -399,5 +399,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", ""), ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), + ("Add to Address Book", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index c59de33fc..1d32aad5e 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -399,5 +399,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", ""), ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), + ("Add to Address Book", ""), ].iter().cloned().collect(); } From 6f7eb17c48478f1fb058b4401dbd27da424b8ae6 Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 28 Nov 2022 18:16:29 +0800 Subject: [PATCH 1123/2015] ab: read respectively and sync when set Signed-off-by: 21pages --- flutter/lib/common.dart | 6 +- flutter/lib/common/widgets/peer_card.dart | 152 ++++++++++++++-------- flutter/lib/models/ab_model.dart | 29 ++++- flutter/lib/models/peer_model.dart | 30 ++++- src/flutter_ffi.rs | 47 +------ src/ui_interface.rs | 13 ++ 6 files changed, 165 insertions(+), 112 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 4c298d917..b9077b0cb 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -923,7 +923,8 @@ bool option2bool(String option, String value) { } else if (option.startsWith("allow-") || option == "stop-service" || option == "direct-server" || - option == "stop-rendezvous-service") { + option == "stop-rendezvous-service" || + option == "force-always-relay") { res = value == "Y"; } else { assert(false); @@ -939,7 +940,8 @@ String bool2option(String option, bool b) { } else if (option.startsWith('allow-') || option == "stop-service" || option == "direct-server" || - option == "stop-rendezvous-service") { + option == "stop-rendezvous-service" || + option == "force-always-relay") { res = b ? 'Y' : ''; } else { assert(false); diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index 4486a6e27..449b67092 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -56,6 +56,9 @@ class _PeerCardState extends State<_PeerCard> Widget _buildMobile() { final peer = super.widget.peer; + final name = + '${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}'; + return Card( margin: EdgeInsets.symmetric(horizontal: 2), child: GestureDetector( @@ -90,7 +93,7 @@ class _PeerCardState extends State<_PeerCard> ? formatID(peer.id) : peer.alias) ]), - Text('${peer.username}@${peer.hostname}') + Text(name) ], ).paddingOnly(left: 8.0), ), @@ -145,6 +148,8 @@ class _PeerCardState extends State<_PeerCard> Widget _buildPeerTile( BuildContext context, Peer peer, Rx deco) { + final name = + '${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}'; final greyStyle = TextStyle( fontSize: 11, color: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6)); @@ -184,7 +189,7 @@ class _PeerCardState extends State<_PeerCard> Align( alignment: Alignment.centerLeft, child: Text( - '${peer.username}@${peer.hostname}', + name, style: greyStyle, textAlign: TextAlign.start, overflow: TextOverflow.ellipsis, @@ -206,7 +211,8 @@ class _PeerCardState extends State<_PeerCard> Widget _buildPeerCard( BuildContext context, Peer peer, Rx deco) { - final name = '${peer.username}@${peer.hostname}'; + final name = + '${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}'; return Card( color: Colors.transparent, elevation: 0, @@ -310,11 +316,20 @@ class _PeerCardState extends State<_PeerCard> bool get wantKeepAlive => true; } +enum CardType { + recent, + fav, + lan, + ab, +} + abstract class BasePeerCard extends StatelessWidget { final Peer peer; final EdgeInsets? menuPadding; + final CardType cardType; - BasePeerCard({required this.peer, this.menuPadding, Key? key}) + BasePeerCard( + {required this.peer, required this.cardType, this.menuPadding, Key? key}) : super(key: key); @override @@ -419,7 +434,7 @@ abstract class BasePeerCard extends StatelessWidget { if (Navigator.canPop(context)) { Navigator.pop(context); } - _rdpDialog(id); + _rdpDialog(id, cardType); }, )), )) @@ -471,17 +486,16 @@ abstract class BasePeerCard extends StatelessWidget { switchType: SwitchType.scheckbox, text: translate('Always connect via relay'), getter: () async { - return (await bind.mainGetPeerOption(id: id, key: option)).isNotEmpty; + if (cardType == CardType.ab) { + return gFFI.abModel.find(id)?.forceAlwaysRelay ?? false; + } else { + return (await bind.mainGetPeerOption(id: id, key: option)).isNotEmpty; + } }, setter: (bool v) async { - String value; - String oldValue = await bind.mainGetPeerOption(id: id, key: option); - if (oldValue.isEmpty) { - value = 'Y'; - } else { - value = ''; - } - await bind.mainSetPeerOption(id: id, key: option, value: value); + gFFI.abModel.setPeerForceAlwaysRelay(id, v); + await bind.mainSetPeerOption( + id: id, key: option, value: bool2option('force-always-relay', v)); }, padding: menuPadding, dismissOnClicked: true, @@ -489,14 +503,14 @@ abstract class BasePeerCard extends StatelessWidget { } @protected - MenuEntryBase _renameAction(String id, bool isAddressBook) { + MenuEntryBase _renameAction(String id) { return MenuEntryButton( childBuilder: (TextStyle? style) => Text( translate('Rename'), style: style, ), proc: () { - _rename(id, isAddressBook); + _rename(id); }, padding: menuPadding, dismissOnClicked: true, @@ -606,33 +620,22 @@ abstract class BasePeerCard extends StatelessWidget { ); } - void _rename(String id, bool isAddressBook) async { + void _rename(String id) async { RxBool isInProgress = false.obs; - var name = peer.alias; - var controller = TextEditingController(text: name); - if (isAddressBook) { - final peer = gFFI.abModel.peers.firstWhereOrNull((p) => id == p.id); - if (peer == null) { - // this should not happen - } else { - name = peer.alias; - } + String name; + if (cardType == CardType.ab) { + name = gFFI.abModel.find(id)?.alias ?? ""; + } else { + name = await bind.mainGetPeerOption(id: id, key: 'alias'); } + var controller = TextEditingController(text: name); gFFI.dialogManager.show((setState, close) { submit() async { isInProgress.value = true; - name = controller.text; + String name = controller.text.trim(); await bind.mainSetPeerAlias(id: id, alias: name); - if (isAddressBook) { - gFFI.abModel.setPeerAlias(id, name); - await gFFI.abModel.pushAb(); - } - if (isAddressBook) { - gFFI.abModel.pullAb(); - } else { - bind.mainLoadRecentPeers(); - bind.mainLoadFavPeers(); - } + gFFI.abModel.setPeerAlias(id, name); + update(); close(); isInProgress.value = false; } @@ -666,11 +669,32 @@ abstract class BasePeerCard extends StatelessWidget { ); }); } + + void update() { + switch (cardType) { + case CardType.recent: + bind.mainLoadRecentPeers(); + break; + case CardType.fav: + bind.mainLoadFavPeers(); + break; + case CardType.lan: + bind.mainLoadLanPeers(); + break; + case CardType.ab: + gFFI.abModel.pullAb(); + break; + } + } } class RecentPeerCard extends BasePeerCard { RecentPeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key}) - : super(peer: peer, menuPadding: menuPadding, key: key); + : super( + peer: peer, + cardType: CardType.recent, + menuPadding: menuPadding, + key: key); @override Future>> _buildMenuItems( @@ -691,7 +715,7 @@ class RecentPeerCard extends BasePeerCard { menuItems.add(_createShortCutAction(peer.id)); } menuItems.add(MenuEntryDivider()); - menuItems.add(_renameAction(peer.id, false)); + menuItems.add(_renameAction(peer.id)); menuItems.add(_removeAction(peer.id, () async { await bind.mainLoadRecentPeers(); })); @@ -708,7 +732,11 @@ class RecentPeerCard extends BasePeerCard { class FavoritePeerCard extends BasePeerCard { FavoritePeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key}) - : super(peer: peer, menuPadding: menuPadding, key: key); + : super( + peer: peer, + cardType: CardType.fav, + menuPadding: menuPadding, + key: key); @override Future>> _buildMenuItems( @@ -729,7 +757,7 @@ class FavoritePeerCard extends BasePeerCard { menuItems.add(_createShortCutAction(peer.id)); } menuItems.add(MenuEntryDivider()); - menuItems.add(_renameAction(peer.id, false)); + menuItems.add(_renameAction(peer.id)); menuItems.add(_removeAction(peer.id, () async { await bind.mainLoadFavPeers(); })); @@ -748,7 +776,11 @@ class FavoritePeerCard extends BasePeerCard { class DiscoveredPeerCard extends BasePeerCard { DiscoveredPeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key}) - : super(peer: peer, menuPadding: menuPadding, key: key); + : super( + peer: peer, + cardType: CardType.lan, + menuPadding: menuPadding, + key: key); @override Future>> _buildMenuItems( @@ -779,7 +811,11 @@ class DiscoveredPeerCard extends BasePeerCard { class AddressBookPeerCard extends BasePeerCard { AddressBookPeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key}) - : super(peer: peer, menuPadding: menuPadding, key: key); + : super( + peer: peer, + cardType: CardType.ab, + menuPadding: menuPadding, + key: key); @override Future>> _buildMenuItems( @@ -800,7 +836,7 @@ class AddressBookPeerCard extends BasePeerCard { menuItems.add(_createShortCutAction(peer.id)); } menuItems.add(MenuEntryDivider()); - menuItems.add(_renameAction(peer.id, false)); + menuItems.add(_renameAction(peer.id)); menuItems.add(_removeAction(peer.id, () async {})); if (await bind.mainPeerHasPassword(id: peer.id)) { menuItems.add(_unrememberPasswordAction(peer.id)); @@ -901,23 +937,33 @@ class AddressBookPeerCard extends BasePeerCard { } } -void _rdpDialog(String id) async { - final portController = TextEditingController( - text: await bind.mainGetPeerOption(id: id, key: 'rdp_port')); - final userController = TextEditingController( - text: await bind.mainGetPeerOption(id: id, key: 'rdp_username')); +void _rdpDialog(String id, CardType card) async { + String port, username; + if (card == CardType.ab) { + port = gFFI.abModel.find(id)?.rdpPort ?? ''; + username = gFFI.abModel.find(id)?.rdpUsername ?? ''; + } else { + port = await bind.mainGetPeerOption(id: id, key: 'rdp_port'); + username = await bind.mainGetPeerOption(id: id, key: 'rdp_username'); + } + + final portController = TextEditingController(text: port); + final userController = TextEditingController(text: username); final passwordController = TextEditingController( text: await bind.mainGetPeerOption(id: id, key: 'rdp_password')); RxBool secure = true.obs; gFFI.dialogManager.show((setState, close) { submit() async { + String port = portController.text.trim(); + String username = userController.text; + String password = passwordController.text; + await bind.mainSetPeerOption(id: id, key: 'rdp_port', value: port); await bind.mainSetPeerOption( - id: id, key: 'rdp_port', value: portController.text.trim()); + id: id, key: 'rdp_username', value: username); await bind.mainSetPeerOption( - id: id, key: 'rdp_username', value: userController.text); - await bind.mainSetPeerOption( - id: id, key: 'rdp_password', value: passwordController.text); + id: id, key: 'rdp_password', value: password); + gFFI.abModel.setRdp(id, port, username); close(); } diff --git a/flutter/lib/models/ab_model.dart b/flutter/lib/models/ab_model.dart index a24c01366..ab5a7cb80 100644 --- a/flutter/lib/models/ab_model.dart +++ b/flutter/lib/models/ab_model.dart @@ -122,6 +122,10 @@ class AbModel { } } + Peer? find(String id) { + return peers.firstWhereOrNull((e) => e.id == id); + } + bool idContainBy(String id) { return peers.where((element) => element.id == id).isNotEmpty; } @@ -160,13 +164,28 @@ class AbModel { } } - void setPeerAlias(String id, String value) { + Future setPeerAlias(String id, String value) async { final it = peers.where((p0) => p0.id == id); - if (it.isEmpty) { - debugPrint("$id is not exists"); - return; - } else { + if (it.isNotEmpty) { it.first.alias = value; + await pushAb(); + } + } + + Future setPeerForceAlwaysRelay(String id, bool value) async { + final it = peers.where((p0) => p0.id == id); + if (it.isNotEmpty) { + it.first.forceAlwaysRelay = value; + await pushAb(); + } + } + + Future setRdp(String id, String port, String username) async { + final it = peers.where((p0) => p0.id == id); + if (it.isNotEmpty) { + it.first.rdpPort = port; + it.first.rdpUsername = username; + await pushAb(); } } diff --git a/flutter/lib/models/peer_model.dart b/flutter/lib/models/peer_model.dart index 6dd94bcf4..ad5183ae3 100644 --- a/flutter/lib/models/peer_model.dart +++ b/flutter/lib/models/peer_model.dart @@ -9,6 +9,9 @@ class Peer { final String platform; String alias; List tags; + bool forceAlwaysRelay = false; + String rdpPort; + String rdpUsername; bool online = false; Peer.fromJson(Map json) @@ -17,7 +20,10 @@ class Peer { hostname = json['hostname'] ?? '', platform = json['platform'] ?? '', alias = json['alias'] ?? '', - tags = json['tags'] ?? []; + tags = json['tags'] ?? [], + forceAlwaysRelay = json['forceAlwaysRelay'] == 'true', + rdpPort = json['rdpPort'] ?? '', + rdpUsername = json['rdpUsername'] ?? ''; Map toJson() { return { @@ -27,6 +33,9 @@ class Peer { "platform": platform, "alias": alias, "tags": tags, + "forceAlwaysRelay": forceAlwaysRelay.toString(), + "rdpPort": rdpPort, + "rdpUsername": rdpUsername, }; } @@ -37,16 +46,23 @@ class Peer { required this.platform, required this.alias, required this.tags, + required this.forceAlwaysRelay, + required this.rdpPort, + required this.rdpUsername, }); Peer.loading() : this( - id: '...', - username: '...', - hostname: '...', - platform: '...', - alias: '', - tags: []); + id: '...', + username: '...', + hostname: '...', + platform: '...', + alias: '', + tags: [], + forceAlwaysRelay: false, + rdpPort: '', + rdpUsername: '', + ); } class Peers extends ChangeNotifier { diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 23ba4ef4b..1250f7e19 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -641,45 +641,11 @@ pub fn main_peer_has_password(id: String) -> bool { peer_has_password(id) } -pub fn main_get_recent_peers() -> String { - if !config::APP_DIR.read().unwrap().is_empty() { - let peers: Vec> = PeerConfig::peers() - .drain(..) - .map(|(id, _, p)| { - HashMap::<&str, String>::from_iter([ - ("id", id), - ("username", p.info.username.clone()), - ("hostname", p.info.hostname.clone()), - ("platform", p.info.platform.clone()), - ( - "alias", - p.options.get("alias").unwrap_or(&"".to_owned()).to_owned(), - ), - ]) - }) - .collect(); - serde_json::ser::to_string(&peers).unwrap_or("".to_owned()) - } else { - String::new() - } -} - pub fn main_load_recent_peers() { if !config::APP_DIR.read().unwrap().is_empty() { let peers: Vec> = PeerConfig::peers() .drain(..) - .map(|(id, _, p)| { - HashMap::<&str, String>::from_iter([ - ("id", id), - ("username", p.info.username.clone()), - ("hostname", p.info.hostname.clone()), - ("platform", p.info.platform.clone()), - ( - "alias", - p.options.get("alias").unwrap_or(&"".to_owned()).to_owned(), - ), - ]) - }) + .map(|(id, _, p)| peer_to_map(id, p)) .collect(); if let Some(s) = flutter::GLOBAL_EVENT_STREAM .read() @@ -705,16 +671,7 @@ pub fn main_load_fav_peers() { .into_iter() .filter_map(|(id, _, p)| { if favs.contains(&id) { - Some(HashMap::<&str, String>::from_iter([ - ("id", id), - ("username", p.info.username.clone()), - ("hostname", p.info.hostname.clone()), - ("platform", p.info.platform.clone()), - ( - "alias", - p.options.get("alias").unwrap_or(&"".to_owned()).to_owned(), - ), - ])) + Some(peer_to_map(id, p)) } else { None } diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 41f22a563..59082d00d 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -685,6 +685,19 @@ pub fn discover() { }); } +pub fn peer_to_map(id: String, p: PeerConfig) -> HashMap<&'static str, String> { + HashMap::<&str, String>::from_iter([ + ("id", id), + ("username", p.info.username.clone()), + ("hostname", p.info.hostname.clone()), + ("platform", p.info.platform.clone()), + ( + "alias", + p.options.get("alias").unwrap_or(&"".to_owned()).to_owned(), + ), + ]) +} + #[inline] pub fn get_lan_peers() -> Vec> { config::LanPeers::load() From c0443e95d6b41b7b01ee7e29f84595ede570683b Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 6 Dec 2022 11:49:07 +0800 Subject: [PATCH 1124/2015] fix command line password permission Signed-off-by: 21pages --- src/core_main.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/core_main.rs b/src/core_main.rs index f890a9525..d0ce9e0d1 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -214,7 +214,11 @@ pub fn core_main() -> Option> { return None; } else if args[0] == "--password" { if args.len() == 2 { - crate::ipc::set_permanent_password(args[1].to_owned()).unwrap(); + if crate::platform::is_root() { + crate::ipc::set_permanent_password(args[1].to_owned()).unwrap(); + } else { + log::info!("Permission denied!"); + } } return None; } else if args[0] == "--check-hwcodec-config" { From c77fe6c01ce8cee1ce9f26e8b14ea867b61d9eea Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 6 Dec 2022 12:11:26 +0800 Subject: [PATCH 1125/2015] fix: infinite execution loop when transfer data --- libs/hbb_common/src/fs.rs | 34 +++++++++++++++++++++++++++------- src/ipc.rs | 5 +++++ src/server/connection.rs | 7 +++++++ src/ui_cm_interface.rs | 6 ++++++ 4 files changed, 45 insertions(+), 7 deletions(-) diff --git a/libs/hbb_common/src/fs.rs b/libs/hbb_common/src/fs.rs index 8477c82ff..6de638ce0 100644 --- a/libs/hbb_common/src/fs.rs +++ b/libs/hbb_common/src/fs.rs @@ -572,6 +572,22 @@ impl TransferJob { self.file_skipped() && self.files.len() == 1 } + /// Check whether the job is completed after `read` returns `None` + /// This is a helper function which gives additional lifecycle when the job reads `None`. + /// If returns `true`, it means we can delete the job automatically. `False` otherwise. + /// + /// [`Note`] + /// Conditions: + /// 1. Files are not waiting for comfirmation by peers. + #[inline] + pub fn job_completed(&self) -> bool { + // has no error, Condition 2 + if !self.enable_overwrite_detection || (!self.file_confirmed && !self.file_is_waiting) { + return true; + } + return false; + } + /// Get job error message, useful for getting status when job had finished pub fn job_error(&self) -> Option { if self.job_skipped() { @@ -597,7 +613,6 @@ impl TransferJob { match r.union { Some(file_transfer_send_confirm_request::Union::Skip(s)) => { if s { - log::debug!("skip file id:{}, file_num:{}", r.id, r.file_num); self.set_file_skipped(); } else { self.set_file_confirmed(true); @@ -744,13 +759,16 @@ pub async fn handle_read_jobs( stream.send(&new_block(block)).await?; } Ok(None) => { - if !job.enable_overwrite_detection || (!job.file_confirmed && !job.file_is_waiting) - { - // for getting error detail, we do not remove this job, we will handle it in io loop - if job.job_error().is_none() { - finished.push(job.id()); + if job.job_completed() { + finished.push(job.id()); + let err = job.job_error(); + if err.is_some() { + stream + .send(&new_error(job.id(), err.unwrap(), job.file_num())) + .await?; + } else { + stream.send(&new_done(job.id(), job.file_num())).await?; } - stream.send(&new_done(job.id(), job.file_num())).await?; } else { // waiting confirmation. } @@ -758,8 +776,10 @@ pub async fn handle_read_jobs( } } for id in finished { + log::info!("remove read job {}", id); remove_job(id, jobs); } + // log::info!("read jobs: {:?}", jobs.iter().map(|item| {item.id}).collect::>()); Ok(()) } diff --git a/src/ipc.rs b/src/ipc.rs index eb2d364ae..478094cf2 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -75,6 +75,11 @@ pub enum FS { id: i32, file_num: i32, }, + WriteError { + id: i32, + file_num: i32, + err: String + }, WriteOffset { id: i32, file_num: i32, diff --git a/src/server/connection.rs b/src/server/connection.rs index c45a00af6..fdd0ea77a 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1304,6 +1304,13 @@ impl Connection { last_modified: d.last_modified, is_upload: true, }), + Some(file_response::Union::Error(e)) => { + self.send_fs(ipc::FS::WriteError { + id: e.id, + file_num: e.file_num, + err: e.error, + }); + } _ => {} }, Some(message::Union::Misc(misc)) => match misc.union { diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index 26e5e4077..97ae82b8b 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -596,6 +596,12 @@ async fn handle_fs(fs: ipc::FS, write_jobs: &mut Vec, tx: &Unbo fs::remove_job(id, write_jobs); } } + ipc::FS::WriteError { id, file_num, err } => { + if let Some(job) = fs::get_job(id, write_jobs) { + send_raw(fs::new_error(id, err, file_num), tx); + fs::remove_job(id, write_jobs); + } + } ipc::FS::WriteBlock { id, file_num, From e3c239f5ae731db1c1a6af9e46def7f77d5fd95e Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 6 Dec 2022 15:09:57 +0800 Subject: [PATCH 1126/2015] fix: write job resets --- src/client/io_loop.rs | 8 +++----- src/ui_cm_interface.rs | 4 ++-- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 5adca6d81..efeacb61c 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -933,14 +933,12 @@ impl Remote { err = job.job_error(); fs::remove_job(d.id, &mut self.write_jobs); } - if let Some(job) = fs::get_job(d.id, &mut self.read_jobs) { - job.modify_time(); - err = job.job_error(); - fs::remove_job(d.id, &mut self.read_jobs); - } self.handle_job_status(d.id, d.file_num, err); } Some(file_response::Union::Error(e)) => { + if let Some(job) = fs::get_job(e.id, &mut self.write_jobs) { + fs::remove_job(e.id, &mut self.write_jobs); + } self.handle_job_status(e.id, e.file_num, Some(e.error)); } _ => {} diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index 97ae82b8b..695d60417 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -598,8 +598,8 @@ async fn handle_fs(fs: ipc::FS, write_jobs: &mut Vec, tx: &Unbo } ipc::FS::WriteError { id, file_num, err } => { if let Some(job) = fs::get_job(id, write_jobs) { - send_raw(fs::new_error(id, err, file_num), tx); - fs::remove_job(id, write_jobs); + send_raw(fs::new_error(job.id(), err, file_num), tx); + fs::remove_job(job.id(), write_jobs); } } ipc::FS::WriteBlock { From bb42e88bb27755749e5cdb952c3f2185f733e00a Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 6 Dec 2022 15:17:51 +0800 Subject: [PATCH 1127/2015] opt: remove outputs --- libs/hbb_common/src/fs.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/libs/hbb_common/src/fs.rs b/libs/hbb_common/src/fs.rs index 6de638ce0..e08414324 100644 --- a/libs/hbb_common/src/fs.rs +++ b/libs/hbb_common/src/fs.rs @@ -776,10 +776,8 @@ pub async fn handle_read_jobs( } } for id in finished { - log::info!("remove read job {}", id); remove_job(id, jobs); } - // log::info!("read jobs: {:?}", jobs.iter().map(|item| {item.id}).collect::>()); Ok(()) } From 0e5cc75f18cdf55d6070a0c9d92b4e9a858dc8f2 Mon Sep 17 00:00:00 2001 From: NicKoehler <53040044+NicKoehler@users.noreply.github.com> Date: Tue, 6 Dec 2022 12:23:40 +0100 Subject: [PATCH 1128/2015] 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 f11a06c80..b7d449a62 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -397,8 +397,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request access to your device", "Richiedi l'accesso al tuo dispositivo"), ("Hide connection management window", "Nascondi la finestra di gestione delle connessioni"), ("hide_cm_tip", "Permetti di nascondere solo se si accettano sessioni con password permanente"), - ("wayland_experiment_tip", ""), - ("Right click to select tabs", ""), - ("Add to Address Book", ""), + ("wayland_experiment_tip", "Il supporto Wayland è in fase sperimentale, utilizza X11 se necessiti di un accesso stabile."), + ("Right click to select tabs", "Clic con il tasto destro per selezionare le schede"), + ("Add to Address Book", "Aggiungi alla rubrica"), ].iter().cloned().collect(); } From d1068775d9d0f4bdc3e9009ad82a8b3bda38cec1 Mon Sep 17 00:00:00 2001 From: solokot Date: Tue, 6 Dec 2022 21:41:32 +0300 Subject: [PATCH 1129/2015] Update ru.rs --- src/lang/ru.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lang/ru.rs b/src/lang/ru.rs index ff8dfc316..8d990fc66 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -372,7 +372,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("elevated_foreground_window_tip", "Текущее окно удалённого рабочего стола требует более высоких привилегий для работы, поэтому временно невозможно использовать мышь и клавиатуру. Можно попросить удалённого пользователя свернуть текущее окно или нажать кнопку повышения прав в окне управления подключением. Чтобы избежать этой проблемы в дальнейшем, рекомендуется выполнить установку программного обеспечения на удалённом устройстве."), ("Disconnected", "Отключено"), ("Other", "Другое"), - ("Confirm before closing multiple tabs", "Подтверждение закрытия несколько вкладок"), + ("Confirm before closing multiple tabs", "Подтверждать закрытие несколько вкладок"), ("Keyboard Settings", "Настройки клавиатуры"), ("Custom", "Своё"), ("Full Access", "Полный доступ"), @@ -380,7 +380,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland требует Ubuntu 21.04 или более позднюю версию."), ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Для Wayland требуется более поздняя версия дистрибутива Linux. Пожалуйста, попробуйте рабочий стол X11 или смените ОС."), ("JumpLink", "Просмотр"), - ("Please Select the screen to be shared(Operate on the peer side).", "Пожалуйста, выберите экран для совместного использования (работайте на одноранговой стороне)."), + ("Please Select the screen to be shared(Operate on the peer side).", "Выберите экран для совместного использования (работайте на одноранговой стороне)."), ("Show RustDesk", "Показать RustDesk"), ("This PC", "Этот компьютер"), ("or", "или"), @@ -399,6 +399,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", "Разрешать скрытие случае, если принимаются сеансы по паролю или используется постоянный пароль"), ("wayland_experiment_tip", "Поддержка Wayland находится на экспериментальной стадии, используйте X11, если вам требуется автоматический доступ."), ("Right click to select tabs", "Выбор вкладок щелчком правой кнопки мыши"), - ("Add to Address Book", ""), + ("Add to Address Book", "Добавить в адресную книгу"), ].iter().cloned().collect(); } From a4e4768adf6926d6f642445def65e90926e00098 Mon Sep 17 00:00:00 2001 From: mehdi-song Date: Wed, 7 Dec 2022 10:33:58 +0330 Subject: [PATCH 1130/2015] Update fa.rs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New Translation 👍 ("wayland_experiment_tip", "پشتیبانی Wayland در مرحله آزمایشی است، لطفاً در صورت نیاز به دسترسی بدون مراقبت از X11 استفاده کنید.") ("Right click to select tabs", "برای انتخاب تب ها راست کلیک کنید") ("Add to Address Book", "افزودن به دفترچه آدرس") --- 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 11c17887a..1e1689cbb 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -397,8 +397,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request access to your device", "دسترسی به دستگاه خود را درخواست کنید"), ("Hide connection management window", "پنهان کردن پنجره مدیریت اتصال"), ("hide_cm_tip", "فقط در صورت پذیرفتن جلسات از طریق رمز عبور و استفاده از رمز عبور دائمی، مخفی شدن مجاز است"), - ("wayland_experiment_tip", ""), - ("Right click to select tabs", ""), - ("Add to Address Book", ""), + ("wayland_experiment_tip", "پشتیبانی Wayland در مرحله آزمایشی است، لطفاً در صورت نیاز به دسترسی بدون مراقبت از X11 استفاده کنید."), + ("Right click to select tabs", "برای انتخاب تب ها راست کلیک کنید"), + ("Add to Address Book", "افزودن به دفترچه آدرس"), ].iter().cloned().collect(); } From da08400fe56846083c7a77e90f08f617dd897a3a Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 7 Dec 2022 15:13:24 +0800 Subject: [PATCH 1131/2015] remote menu, draggable hide widget Signed-off-by: fufesou --- .../lib/desktop/widgets/remote_menubar.dart | 202 ++++++++++++------ flutter/pubspec.lock | 16 +- flutter/pubspec.yaml | 2 +- 3 files changed, 157 insertions(+), 63 deletions(-) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 8385bf63c..2ae5b96c4 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -11,7 +11,7 @@ import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; -import 'package:rxdart/rxdart.dart' as rxdart; +import 'package:debounce_throttle/debounce_throttle.dart'; import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:window_size/window_size.dart' as window_size; @@ -119,10 +119,11 @@ class RemoteMenubar extends StatefulWidget { } class _RemoteMenubarState extends State { - final Rx _hideColor = Colors.white12.obs; - final _rxHideReplay = rxdart.ReplaySubject(); + late Debouncer _debouncerHide; bool _isCursorOverImage = false; window_size.Screen? _screen; + final _fractionX = 0.5.obs; + final _dragging = false.obs; int get windowId => stateGlobal.windowId; @@ -139,23 +140,26 @@ class _RemoteMenubarState extends State { initState() { super.initState(); + _debouncerHide = Debouncer( + Duration(milliseconds: 5000), + onChanged: _debouncerHideProc, + initialValue: 0, + ); + widget.onEnterOrLeaveImageSetter((enter) { if (enter) { - _rxHideReplay.add(0); + _debouncerHide.value = 0; _isCursorOverImage = true; } else { _isCursorOverImage = false; } }); + } - _rxHideReplay - .throttleTime(const Duration(milliseconds: 5000), - trailing: true, leading: false) - .listen((int v) { - if (!pin && show.isTrue && _isCursorOverImage) { - show.value = false; - } - }); + _debouncerHideProc(int v) { + if (!pin && show.isTrue && _isCursorOverImage && _dragging.isFalse) { + show.value = false; + } } @override @@ -169,36 +173,29 @@ class _RemoteMenubarState extends State { Widget build(BuildContext context) { return Align( alignment: Alignment.topCenter, - child: Obx( - () => show.value ? _buildMenubar(context) : _buildShowHide(context)), + child: Obx(() => show.value + ? _buildMenubar(context) + : _buildDraggableShowHide(context)), ); } - Widget _buildShowHide(BuildContext context) { - return Obx(() => Tooltip( - message: translate(show.value ? 'Hide Menubar' : 'Show Menubar'), - child: SizedBox( - width: 100, - height: 13, - child: TextButton( - onHover: (bool v) { - _hideColor.value = v ? Colors.white60 : Colors.white24; - }, - onPressed: () { - show.value = !show.value; - _hideColor.value = Colors.white24; - if (show.isTrue) { - _updateScreen(); - } - }, - child: Obx(() => Container( - decoration: BoxDecoration( - color: _hideColor.value, - border: Border.all(color: MyTheme.border), - borderRadius: BorderRadius.all(Radius.circular(5.0)), - ), - ).marginOnly(bottom: 8.0)), - )))); + Widget _buildDraggableShowHide(BuildContext context) { + return Obx(() { + if (show.isTrue && _dragging.isFalse) { + _debouncerHide.value = 1; + } + return Align( + alignment: FractionalOffset(_fractionX.value, 0), + child: Offstage( + offstage: _dragging.isTrue, + child: _DraggableShowHide( + dragging: _dragging, + fractionX: _fractionX, + show: show, + ), + ), + ); + }); } _updateScreen() async { @@ -255,13 +252,12 @@ class _RemoteMenubarState extends State { decoration: BoxDecoration( color: Colors.white, border: Border.all(color: MyTheme.border), - borderRadius: BorderRadius.all(Radius.circular(10.0)), ), child: Row( mainAxisSize: MainAxisSize.min, children: menubarItems, )), - _buildShowHide(context), + _buildDraggableShowHide(context), ])); } @@ -831,15 +827,13 @@ class _RemoteMenubarState extends State { qualityInitValue = qualityMaxValue; } final RxDouble qualitySliderValue = RxDouble(qualityInitValue); - final qualityRxReplay = rxdart.ReplaySubject(); - qualityRxReplay - .throttleTime(const Duration(milliseconds: 1000), - trailing: true, leading: false) - .listen((double v) { - () async { - await setCustomValues(quality: v); - }(); - }); + final debouncerQuanlity = Debouncer( + Duration(milliseconds: 1000), + onChanged: (double v) { + setCustomValues(quality: v); + }, + initialValue: qualityInitValue, + ); final qualitySlider = Obx(() => Row( children: [ Slider( @@ -849,7 +843,7 @@ class _RemoteMenubarState extends State { divisions: 90, onChanged: (double value) { qualitySliderValue.value = value; - qualityRxReplay.add(value); + debouncerQuanlity.value = value; }, ), SizedBox( @@ -869,15 +863,13 @@ class _RemoteMenubarState extends State { fpsInitValue = 30; } final RxDouble fpsSliderValue = RxDouble(fpsInitValue); - final fpsRxReplay = rxdart.ReplaySubject(); - fpsRxReplay - .throttleTime(const Duration(milliseconds: 1000), - trailing: true, leading: false) - .listen((double v) { - () async { - await setCustomValues(fps: v); - }(); - }); + final debouncerFps = Debouncer( + Duration(milliseconds: 1000), + onChanged: (double v) { + setCustomValues(fps: v); + }, + initialValue: qualityInitValue, + ); bool? direct; try { direct = ConnectionTypeState.find(widget.id).direct.value == @@ -898,7 +890,7 @@ class _RemoteMenubarState extends State { divisions: 22, onChanged: (double value) { fpsSliderValue.value = value; - fpsRxReplay.add(value); + debouncerFps.value = value; }, ))), SizedBox( @@ -1352,3 +1344,91 @@ void showAuditDialog(String id, dialogManager) async { ); }); } + +class _DraggableShowHide extends StatefulWidget { + final RxDouble fractionX; + final RxBool dragging; + final RxBool show; + const _DraggableShowHide({ + Key? key, + required this.fractionX, + required this.dragging, + required this.show, + }) : super(key: key); + + @override + State<_DraggableShowHide> createState() => __DraggableShowHideState(); +} + +class __DraggableShowHideState extends State<_DraggableShowHide> { + Offset position = Offset.zero; + Size size = Size.zero; + + Widget _buildDraggable(BuildContext context) { + return Draggable( + axis: Axis.horizontal, + child: Icon( + Icons.drag_indicator, + size: 15, + ), + feedback: widget, + onDragStarted: (() { + final RenderObject? renderObj = context.findRenderObject(); + if (renderObj != null) { + final RenderBox renderBox = renderObj as RenderBox; + size = renderBox.size; + position = renderBox.localToGlobal(Offset.zero); + } + widget.dragging.value = true; + }), + onDragEnd: (details) { + final mediaSize = MediaQueryData.fromWindow(ui.window).size; + widget.fractionX.value += + (details.offset.dx - position.dx) / (mediaSize.width - size.width); + if (widget.fractionX.value < 0.35) { + widget.fractionX.value = 0.35; + } + if (widget.fractionX.value > 0.65) { + widget.fractionX.value = 0.65; + } + widget.dragging.value = false; + }, + ); + } + + @override + Widget build(BuildContext context) { + final ButtonStyle buttonStyle = ButtonStyle( + minimumSize: MaterialStateProperty.all(const Size(0, 0)), + padding: MaterialStateProperty.all(EdgeInsets.zero), + ); + final child = Row( + mainAxisSize: MainAxisSize.min, + children: [ + _buildDraggable(context), + TextButton( + onPressed: () => setState(() { + widget.show.value = !widget.show.value; + }), + child: Obx((() => Icon( + widget.show.isTrue ? Icons.expand_less : Icons.expand_more, + size: 15, + ))), + ), + ], + ); + return TextButtonTheme( + data: TextButtonThemeData(style: buttonStyle), + child: Container( + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: MyTheme.border), + ), + child: SizedBox( + height: 15, + child: child, + ), + ), + ); + } +} diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index a24f73954..625862dbf 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -246,6 +246,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.0.15" + debounce_throttle: + dependency: "direct main" + description: + name: debounce_throttle + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" desktop_drop: dependency: "direct main" description: @@ -842,7 +849,7 @@ packages: source: hosted version: "1.0.1" rxdart: - dependency: "direct main" + dependency: transitive description: name: rxdart url: "https://pub.dartlang.org" @@ -885,6 +892,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.3" + simple_observable: + dependency: transitive + description: + name: simple_observable + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" sky_engine: dependency: transitive description: flutter diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index ddf5e8a53..a8a3d7050 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -79,7 +79,7 @@ dependencies: contextmenu: ^3.0.0 desktop_drop: ^0.3.3 scroll_pos: ^0.3.0 - rxdart: ^0.27.5 + debounce_throttle: ^2.0.0 file_picker: ^5.1.0 flutter_svg: ^1.1.5 flutter_improved_scrolling: ^0.0.3 From b6c1b9e45ebfd51f87a17a6cfd26a8d4f3dd5fe7 Mon Sep 17 00:00:00 2001 From: "Miguel F. G" <116861809+flusheDData@users.noreply.github.com> Date: Wed, 7 Dec 2022 16:53:47 +0100 Subject: [PATCH 1132/2015] Update es.rs New term added --- src/lang/es.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/es.rs b/src/lang/es.rs index 1069b4905..17c3ddf07 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -399,6 +399,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", "Permitir ocultar solo si se aceptan sesiones a través de contraseña y usando contraseña permanente"), ("wayland_experiment_tip", "El soporte para Wayland está en fase experimental, por favor, use X11 si necesita acceso desatendido."), ("Right click to select tabs", "Clic derecho para seleccionar pestañas"), - ("Add to Address Book", ""), + ("Add to Address Book", "Añadir a la libreta de direcciones"), ].iter().cloned().collect(); } From 3b60304d148d24a43c0f903d3435bab7c42d114a Mon Sep 17 00:00:00 2001 From: Asura Date: Tue, 15 Nov 2022 23:09:29 -0800 Subject: [PATCH 1133/2015] refactor: keyboard of client --- Cargo.lock | 26 +- Cargo.toml | 2 +- .../lib/desktop/widgets/remote_menubar.dart | 9 +- flutter/lib/mobile/pages/remote_page.dart | 5 +- flutter/lib/models/input_model.dart | 2 +- flutter/pubspec.lock | 19 +- libs/hbb_common/src/config.rs | 2 + src/client.rs | 32 +- src/common.rs | 10 +- src/flutter.rs | 2 - src/flutter_ffi.rs | 37 +- src/keyboard.rs | 678 ++++++++++++++ src/lib.rs | 1 + src/platform/linux.rs | 20 + src/platform/macos.rs | 4 + src/platform/mod.rs | 1 + src/platform/windows.rs | 14 + src/ui_session_interface.rs | 826 +----------------- 18 files changed, 833 insertions(+), 857 deletions(-) create mode 100644 src/keyboard.rs diff --git a/Cargo.lock b/Cargo.lock index bf3ce1f04..5708905db 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1533,7 +1533,7 @@ dependencies = [ "log", "objc", "pkg-config", - "rdev", + "rdev 0.5.0-2 (git+https://github.com/asur4s/rdev)", "serde 1.0.147", "serde_derive", "tfc", @@ -4229,7 +4229,27 @@ dependencies = [ [[package]] name = "rdev" version = "0.5.0-2" -source = "git+https://github.com/asur4s/rdev#4051761e7ccf434a443b8e9592c23160c9cace56" +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", + "strum 0.24.1", + "strum_macros 0.24.3", + "widestring 1.0.2", + "winapi 0.3.9", + "x11 2.20.0", +] + +[[package]] +name = "rdev" +version = "0.5.0-2" +source = "git+https://github.com/asur4s/rdev#fdcee04f10ea0ef00d36aa612eabb9605ae9f2fc" dependencies = [ "cocoa", "core-foundation 0.9.3", @@ -4518,7 +4538,7 @@ dependencies = [ "num_cpus", "objc", "parity-tokio-ipc", - "rdev", + "rdev 0.5.0-2", "repng", "reqwest", "rpassword 7.1.0", diff --git a/Cargo.toml b/Cargo.toml index 2861b3f63..afd617f78 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,7 +63,7 @@ default-net = "0.11.0" wol-rs = "0.9.1" flutter_rust_bridge = { git = "https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge", optional = true } errno = "0.2.8" -rdev = { git = "https://github.com/asur4s/rdev" } +rdev = { path = "../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 2ae5b96c4..c1a7bdce2 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -1189,11 +1189,12 @@ class _RemoteMenubarState extends State { MenuEntryRadioOption(text: translate('Legacy mode'), value: 'legacy'), MenuEntryRadioOption(text: translate('Map mode'), value: 'map'), ], - curOptionGetter: () async => - await bind.sessionGetKeyboardName(id: widget.id), + curOptionGetter: () async { + return await bind.sessionGetKeyboardMode(id: widget.id) ?? "legacy"; + }, optionSetter: (String oldValue, String newValue) async { - await bind.sessionSetKeyboardMode( - id: widget.id, keyboardMode: newValue); + await bind.sessionSetKeyboardMode(id: widget.id, value: newValue); + widget.ffi.canvasModel.updateViewStyle(); }, ) ]; diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index fcfb8ad60..d0388b8fe 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -692,10 +692,11 @@ class _RemotePageState extends State { } void changePhysicalKeyboardInputMode() async { - var current = await bind.sessionGetKeyboardName(id: widget.id); + 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, keyboardMode: v ?? ''); + await bind.sessionPeerOption( + id: widget.id, name: "keyboard-mode", value: v ?? ""); setState(() => current = v ?? ''); Future.delayed(Duration(milliseconds: 300), close); } diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index bd1131c7a..b488f30f3 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -54,7 +54,7 @@ class InputModel { InputModel(this.parent); KeyEventResult handleRawKeyEvent(FocusNode data, RawKeyEvent e) { - bind.sessionGetKeyboardName(id: id).then((result) { + bind.sessionGetKeyboardMode(id: id).then((result) { keyboardMode = result.toString(); }); diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 625862dbf..964ae51aa 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -63,14 +63,7 @@ packages: name: back_button_interceptor url: "https://pub.dartlang.org" source: hosted - version: "6.0.2" - bot_toast: - dependency: "direct main" - description: - name: bot_toast - url: "https://pub.dartlang.org" - source: hosted - version: "4.0.3" + version: "6.0.1" build: dependency: transitive description: @@ -264,8 +257,8 @@ packages: dependency: "direct main" description: path: "." - ref: "82f9eab81cb2c7bfb938def7a1b399a6279bbc75" - resolved-ref: "82f9eab81cb2c7bfb938def7a1b399a6279bbc75" + ref: bf278fc8a8ff787e46fa3ab97674373bfaa20f23 + resolved-ref: bf278fc8a8ff787e46fa3ab97674373bfaa20f23 url: "https://github.com/Kingtous/rustdesk_desktop_multi_window" source: git version: "0.1.0" @@ -390,8 +383,8 @@ packages: dependency: "direct main" description: path: "." - ref: "74b1b314142b6775c1243067a3503ac568ebc74b" - resolved-ref: "74b1b314142b6775c1243067a3503ac568ebc74b" + ref: dec2166e881c47d922e1edc484d10d2cd5c2103b + resolved-ref: dec2166e881c47d922e1edc484d10d2cd5c2103b url: "https://github.com/Kingtous/rustdesk_flutter_custom_cursor" source: git version: "0.0.1" @@ -1015,7 +1008,7 @@ packages: name: uni_links_desktop url: "https://pub.dartlang.org" source: hosted - version: "0.1.4" + version: "0.1.3" uni_links_platform_interface: dependency: transitive description: diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 328a1ea59..d4701aada 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -203,6 +203,8 @@ pub struct PeerConfig { pub enable_file_transfer: bool, #[serde(default)] pub show_quality_monitor: bool, + #[serde(default)] + pub keyboard_mode: String, // The other scalar value must before this #[serde(default, deserialize_with = "PeerConfig::deserialize_options")] diff --git a/src/client.rs b/src/client.rs index 1dd3021b2..3b932a39a 100644 --- a/src/client.rs +++ b/src/client.rs @@ -49,10 +49,7 @@ pub use super::lang::*; pub mod file_trait; pub mod helper; pub mod io_loop; -use crate::{ - server::video_service::{SCRAP_X11_REF_URL, SCRAP_X11_REQUIRED}, - ui_session_interface::global_save_keyboard_mode, -}; +use crate::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); @@ -989,6 +986,17 @@ impl LoginConfigHandler { self.save_config(config); } + /// Save keyboard mode to the current config. + /// + /// # Arguments + /// + /// * `value` - The view style to be saved. + pub fn save_keyboard_mode(&mut self, value: String) { + let mut config = self.load_config(); + config.keyboard_mode = value; + self.save_config(config); + } + /// Save scroll style to the current config. /// /// # Arguments @@ -1382,9 +1390,6 @@ impl LoginConfigHandler { if !pi.version.is_empty() { self.version = hbb_common::get_version_number(&pi.version); } - if hbb_common::get_version_number(&pi.version) < hbb_common::get_version_number("1.2.0") { - global_save_keyboard_mode("legacy".to_owned()); - } self.features = pi.features.clone().into_option(); let serde = PeerInfoSerde { username: pi.username.clone(), @@ -1407,6 +1412,14 @@ impl LoginConfigHandler { log::debug!("remove password of {}", self.id); } } + if config.keyboard_mode == "" { + if hbb_common::get_version_number(&pi.version) < hbb_common::get_version_number("1.2.0") + { + config.keyboard_mode = "legacy".to_string(); + } else { + config.keyboard_mode = "map".to_string(); + } + } self.conn_id = pi.conn_id; // no matter if change, for update file time self.save_config(config); @@ -2024,8 +2037,3 @@ fn decode_id_pk(signed: &[u8], key: &sign::PublicKey) -> ResultType<(String, [u8 bail!("Wrong public length"); } } - -#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] -pub fn disable_keyboard_listening() { - crate::ui_session_interface::KEYBOARD_HOOKED.store(true, Ordering::SeqCst); -} diff --git a/src/common.rs b/src/common.rs index ea02cf810..9023780f4 100644 --- a/src/common.rs +++ b/src/common.rs @@ -3,6 +3,14 @@ use std::{ sync::{Arc, Mutex}, }; +#[derive(Debug, Eq, PartialEq)] +pub enum GrabState { + Ready, + Run, + Wait, + Exit, +} + #[cfg(not(any(target_os = "android", target_os = "ios")))] pub use arboard::Clipboard as ClipboardContext; @@ -11,7 +19,7 @@ use hbb_common::compress::decompress; use hbb_common::{ allow_err, anyhow::bail, - compress::{compress as compress_func}, + compress::compress as compress_func, config::{self, Config, COMPRESS_LEVEL, RENDEZVOUS_TIMEOUT}, get_version_number, log, message_proto::*, diff --git a/src/flutter.rs b/src/flutter.rs index a2c307f5a..2bb7a9faf 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -422,8 +422,6 @@ pub fn session_start_(id: &str, event_stream: StreamSink) -> ResultTy *session.event_stream.write().unwrap() = Some(event_stream); let session = session.clone(); std::thread::spawn(move || { - // if flutter : disable keyboard listen - crate::client::disable_keyboard_listening(); io_loop(session); }); Ok(()) diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 1250f7e19..09f943d80 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -18,7 +18,7 @@ use hbb_common::{ use crate::flutter::{self, SESSIONS}; use crate::ui_interface::{self, *}; #[cfg(not(any(target_os = "android", target_os = "ios")))] -use crate::ui_session_interface::CUR_SESSION; +use crate::keyboard::CUR_SESSION; use crate::{ client::file_trait::FileManager, flutter::{make_fd_to_json, session_add, session_start_}, @@ -225,6 +225,20 @@ pub fn session_set_image_quality(id: String, value: String) { } } +pub fn session_get_keyboard_mode(id: String) -> Option { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + Some(session.get_keyboard_mode()) + } else { + None + } +} + +pub fn session_set_keyboard_mode(id: String, value: String) { + if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) { + session.save_keyboard_mode(value); + } +} + pub fn session_get_custom_image_quality(id: String) -> Option> { if let Some(session) = SESSIONS.read().unwrap().get(&id) { Some(session.get_custom_image_quality()) @@ -271,7 +285,7 @@ pub fn session_handle_flutter_key_event( down_or_up: bool, ) { if let Some(session) = SESSIONS.read().unwrap().get(&id) { - session.handle_flutter_key_event(&name, keycode, scancode, down_or_up); + // session.handle_flutter_key_event(&name, keycode, scancode, down_or_up); } } @@ -282,7 +296,6 @@ pub fn session_enter_or_leave(id: String, enter: bool) { *CUR_SESSION.lock().unwrap() = Some(session.clone()); session.enter(); } else { - *CUR_SESSION.lock().unwrap() = None; session.leave(); } } @@ -299,12 +312,14 @@ pub fn session_input_key( command: bool, ) { if let Some(session) = SESSIONS.read().unwrap().get(&id) { + // #[cfg(any(target_os = "android", target_os = "ios"))] session.input_key(&name, down, press, alt, ctrl, shift, command); } } pub fn session_input_string(id: String, value: String) { if let Some(session) = SESSIONS.read().unwrap().get(&id) { + // #[cfg(any(target_os = "android", target_os = "ios"))] session.input_string(&value); } } @@ -329,19 +344,6 @@ pub fn session_get_peer_option(id: String, name: String) -> String { "".to_string() } -pub fn session_get_keyboard_name(id: String) -> String { - if let Some(session) = SESSIONS.read().unwrap().get(&id) { - return session.get_keyboard_mode(); - } - "legacy".to_string() -} - -pub fn session_set_keyboard_mode(id: String, keyboard_mode: String) { - if let Some(session) = SESSIONS.read().unwrap().get(&id) { - session.save_keyboard_mode(keyboard_mode); - } -} - pub fn session_input_os_password(id: String, value: String) { if let Some(session) = SESSIONS.read().unwrap().get(&id) { session.input_os_password(value, true); @@ -1083,8 +1085,7 @@ pub fn main_is_installed() -> SyncReturn { } pub fn main_start_grab_keyboard() { - #[cfg(not(any(target_os = "android", target_os = "ios")))] - crate::ui_session_interface::global_grab_keyboard(); + crate::keyboard::client::start_grab_loop(); } pub fn main_is_installed_lower_version() -> SyncReturn { diff --git a/src/keyboard.rs b/src/keyboard.rs new file mode 100644 index 000000000..f278237ca --- /dev/null +++ b/src/keyboard.rs @@ -0,0 +1,678 @@ +use crate::client::get_key_state; +use crate::common::GrabState; +#[cfg(feature = "flutter")] +use crate::flutter::FlutterHandler; +use crate::platform; +use crate::ui_session_interface::Session; +use hbb_common::{allow_err, log, message_proto::*}; +use rdev::{Event, EventType, Key}; +use std::collections::{HashMap, HashSet}; +use std::sync::{mpsc, Arc, Mutex, RwLock}; +use std::time::SystemTime; + +#[cfg(feature = "flutter")] +lazy_static::lazy_static! { + pub static ref CUR_SESSION: Arc>>> = Default::default(); + pub static ref GRAB_SENDER: Arc>>> = Default::default(); +} + +lazy_static::lazy_static! { + static ref TO_RELEASE: Arc>> = Arc::new(Mutex::new(HashSet::::new())); + static ref MODIFIERS_STATE: Mutex> = { + let mut m = HashMap::new(); + m.insert(Key::ShiftLeft, false); + m.insert(Key::ShiftRight, false); + m.insert(Key::ControlLeft, false); + m.insert(Key::ControlRight, false); + m.insert(Key::Alt, false); + m.insert(Key::AltGr, false); + m.insert(Key::MetaLeft, false); + m.insert(Key::MetaRight, false); + Mutex::new(m) + }; + +} + +pub mod client { + use super::{client_keyboard_mode, components, *}; + + pub fn get_keyboard_mode() -> String { + return components::get_keyboard_mode(); + } + + pub fn save_keyboard_mode(value: String) { + components::save_keyboard_mode(value); + } + + pub fn start_grab_loop() { + let (sender, receiver) = mpsc::channel::(); + unsafe { + components::grab_loop(receiver); + *GRAB_SENDER.lock().unwrap() = Some(sender); + } + change_grab_status(GrabState::Ready); + } + + pub fn change_grab_status(state: GrabState) { + if GrabState::Wait == state { + components::release_remote_keys(); + } + unsafe { + if let Some(sender) = &*GRAB_SENDER.lock().unwrap() { + log::info!("grab state: {:?}", state); + sender.send(state); + } + } + } + + pub fn process_event(event: Event) { + if components::is_long_press(&event) { + return; + } + let key_event = components::event_to_key_event(&event); + log::info!("key event: {:?}", key_event); + components::send_key_event(&key_event); + } + + pub fn get_modifiers_state( + alt: bool, + ctrl: bool, + shift: bool, + command: bool, + ) -> (bool, bool, bool, bool) { + components::get_modifiers_state(alt, ctrl, shift, command) + } + + pub fn legacy_modifiers( + key_event: &mut KeyEvent, + alt: bool, + ctrl: bool, + shift: bool, + command: bool, + ) { + components::legacy_modifiers(key_event, alt, ctrl, shift, command); + } + + pub fn lock_screen() { + components::lock_screen(); + } + + pub fn ctrl_alt_del() { + components::ctrl_alt_del(); + } +} + +pub mod server { + pub fn simulate() {} +} + +mod components { + use super::*; + use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; + use std::thread; + + pub static KEYBOARD_HOOKED: AtomicBool = AtomicBool::new(false); + + pub fn grab_loop(recv: mpsc::Receiver) { + thread::spawn(move || loop { + if let Some(state) = recv.recv().ok() { + match state { + GrabState::Ready => { + #[cfg(any(target_os = "windows", target_os = "macos"))] + std::thread::spawn(move || { + let func = move |event: Event| match event.event_type { + EventType::KeyPress(key) | EventType::KeyRelease(key) => { + // todo!: CAPSLOCK don't work + if KEYBOARD_HOOKED.load(Ordering::SeqCst) { + keyboard::client::process_event(event); + return None; + } else { + return Some(event); + } + } + _ => Some(event), + }; + if let Err(error) = rdev::grab(func) { + log::error!("rdev Error: {:?}", error) + } + }); + + #[cfg(target_os = "linux")] + rdev::start_grab_listen(move |event: Event| match event.event_type { + EventType::KeyPress(key) | EventType::KeyRelease(key) => { + if let Key::Unknown(keycode) = key { + log::error!("rdev get unknown key, keycode is : {:?}", keycode); + } else { + crate::keyboard::client::process_event(event); + } + None + } + _ => Some(event), + }); + } + GrabState::Run => { + #[cfg(any(target_os = "windows", target_os = "macos"))] + KEYBOARD_HOOKED.swap(true, Ordering::SeqCst); + + #[cfg(target_os = "linux")] + rdev::enable_grab().ok(); + } + GrabState::Wait => { + #[cfg(any(target_os = "windows", target_os = "macos"))] + KEYBOARD_HOOKED.swap(false, Ordering::SeqCst); + + #[cfg(target_os = "linux")] + rdev::disable_grab().ok(); + } + GrabState::Exit => { + #[cfg(target_os = "linux")] + rdev::exit_grab_listen().ok(); + } + } + } + }); + } + + pub fn is_long_press(event: &Event) -> bool { + let mut keys = MODIFIERS_STATE.lock().unwrap(); + match event.event_type { + EventType::KeyPress(k) => { + if let Some(&state) = keys.get(&k) { + if state == true { + return true; + } + } + } + _ => {} + }; + return false; + } + + pub fn release_remote_keys() { + // todo!: client quit suddenly, how to release keys? + let to_release = TO_RELEASE.lock().unwrap(); + let keys = to_release.iter().map(|&key| key).collect::>(); + drop(to_release); + for key in keys { + let event_type = EventType::KeyRelease(key); + let event = event_type_to_event(event_type); + log::info!("release key: {:?}", key); + client::process_event(event); + } + } + + pub fn get_keyboard_mode() -> String { + if let Some(handler) = CUR_SESSION.lock().unwrap().as_ref() { + handler.get_keyboard_mode() + } else { + "legacy".to_string() + } + } + + pub fn save_keyboard_mode(value: String) { + release_remote_keys(); + if let Some(handler) = CUR_SESSION.lock().unwrap().as_mut() { + handler.save_keyboard_mode(value); + } + } + + pub fn get_keyboard_mode_enum() -> KeyboardMode { + match get_keyboard_mode().as_str() { + "map" => KeyboardMode::Map, + "translate" => KeyboardMode::Translate, + _ => KeyboardMode::Legacy, + } + } + + pub fn add_numlock_capslock_state(key_event: &mut KeyEvent) { + if get_key_state(enigo::Key::CapsLock) { + key_event.modifiers.push(ControlKey::CapsLock.into()); + } + if get_key_state(enigo::Key::NumLock) { + key_event.modifiers.push(ControlKey::NumLock.into()); + } + } + + pub fn convert_numpad_keys(key: Key) -> Key { + if get_key_state(enigo::Key::NumLock) { + return key; + } + match key { + Key::Kp0 => Key::Insert, + Key::KpDecimal => Key::Delete, + Key::Kp1 => Key::End, + Key::Kp2 => Key::DownArrow, + Key::Kp3 => Key::PageDown, + Key::Kp4 => Key::LeftArrow, + Key::Kp5 => Key::Clear, + Key::Kp6 => Key::RightArrow, + Key::Kp7 => Key::Home, + Key::Kp8 => Key::UpArrow, + Key::Kp9 => Key::PageUp, + _ => key, + } + } + + pub fn get_modifiers_state( + alt: bool, + ctrl: bool, + shift: bool, + command: bool, + ) -> (bool, bool, bool, bool) { + let modifiers_lock = MODIFIERS_STATE.lock().unwrap(); + let ctrl = *modifiers_lock.get(&Key::ControlLeft).unwrap() + || *modifiers_lock.get(&Key::ControlRight).unwrap() + || ctrl; + let shift = *modifiers_lock.get(&Key::ShiftLeft).unwrap() + || *modifiers_lock.get(&Key::ShiftRight).unwrap() + || shift; + let command = *modifiers_lock.get(&Key::MetaLeft).unwrap() + || *modifiers_lock.get(&Key::MetaRight).unwrap() + || command; + let alt = *modifiers_lock.get(&Key::Alt).unwrap() + || *modifiers_lock.get(&Key::AltGr).unwrap() + || alt; + + (alt, ctrl, shift, command) + } + + fn update_modifiers_state(event: &Event) { + // for mouse + let mut keys = MODIFIERS_STATE.lock().unwrap(); + match event.event_type { + EventType::KeyPress(k) => { + if keys.contains_key(&k) { + keys.insert(k, true); + } + } + EventType::KeyRelease(k) => { + if keys.contains_key(&k) { + keys.insert(k, false); + } + } + _ => {} + }; + } + + pub fn legacy_modifiers( + key_event: &mut KeyEvent, + alt: bool, + ctrl: bool, + shift: bool, + command: bool, + ) { + if alt + && !crate::is_control_key(&key_event, &ControlKey::Alt) + && !crate::is_control_key(&key_event, &ControlKey::RAlt) + { + key_event.modifiers.push(ControlKey::Alt.into()); + } + if shift + && !crate::is_control_key(&key_event, &ControlKey::Shift) + && !crate::is_control_key(&key_event, &ControlKey::RShift) + { + key_event.modifiers.push(ControlKey::Shift.into()); + } + if ctrl + && !crate::is_control_key(&key_event, &ControlKey::Control) + && !crate::is_control_key(&key_event, &ControlKey::RControl) + { + key_event.modifiers.push(ControlKey::Control.into()); + } + if command + && !crate::is_control_key(&key_event, &ControlKey::Meta) + && !crate::is_control_key(&key_event, &ControlKey::RWin) + { + key_event.modifiers.push(ControlKey::Meta.into()); + } + } + + pub fn event_to_key_event(event: &Event) -> KeyEvent { + let mut key_event = KeyEvent::new(); + update_modifiers_state(event); + + let mut to_release = TO_RELEASE.lock().unwrap(); + match event.event_type { + EventType::KeyPress(key) => { + to_release.insert(key); + } + EventType::KeyRelease(key) => { + to_release.remove(&key); + } + _ => {} + } + drop(to_release); + + let keyboard_mode = get_keyboard_mode_enum(); + key_event.mode = keyboard_mode.into(); + match keyboard_mode { + KeyboardMode::Map => { + client_keyboard_mode::map_keyboard_mode(event, &mut key_event); + } + KeyboardMode::Translate => { + client_keyboard_mode::translate_keyboard_mode(event, &mut key_event); + } + _ => { + client_keyboard_mode::legacy_keyboard_mode(event, &mut key_event); + } + }; + #[cfg(not(any(target_os = "android", target_os = "ios")))] + add_numlock_capslock_state(&mut key_event); + + return key_event; + } + + pub fn event_type_to_event(event_type: EventType) -> Event { + Event { + event_type, + time: SystemTime::now(), + name: None, + code: 0, + scan_code: 0, + } + } + + pub fn ctrl_alt_del() { + let mut key_event = KeyEvent::new(); + if get_peer_platform() == "Windows" { + key_event.set_control_key(ControlKey::CtrlAltDel); + key_event.down = true; + } else { + key_event.set_control_key(ControlKey::Delete); + legacy_modifiers(&mut key_event, true, true, false, false); + key_event.press = true; + } + key_event.mode = KeyboardMode::Legacy.into(); + send_key_event(&key_event); + } + + pub fn lock_screen() { + let mut key_event = KeyEvent::new(); + key_event.set_control_key(ControlKey::LockScreen); + key_event.down = true; + key_event.mode = KeyboardMode::Legacy.into(); + send_key_event(&key_event); + } + + #[cfg(feature = "flutter")] + pub fn send_key_event(key_event: &KeyEvent) { + if let Some(handler) = CUR_SESSION.lock().unwrap().as_ref() { + log::info!("Sending key even {:?}", key_event); + handler.send_key_event(key_event); + } + } + + pub fn get_peer_platform() -> String { + if let Some(handler) = CUR_SESSION.lock().unwrap().as_ref() { + handler.peer_platform() + } else { + log::error!("get peer platform error"); + "Windows".to_string() + } + } +} + +mod client_keyboard_mode { + use super::*; + use components; + use rdev::EventType; + + static mut IS_ALT_GR: bool = false; + + pub fn legacy_keyboard_mode(event: &Event, key_event: &mut KeyEvent) { + // 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; + } + }; + + let peer = components::get_peer_platform(); + let is_win = peer == "Windows"; + if is_win { + key = components::convert_numpad_keys(key); + } + + let alt = get_key_state(enigo::Key::Alt); + #[cfg(windows)] + let ctrl = { + let mut tmp = + get_key_state(enigo::Key::Control) || get_key_state(enigo::Key::RightControl); + unsafe { + if IS_ALT_GR { + if alt || key == Key::AltGr { + if tmp { + tmp = false; + } + } else { + IS_ALT_GR = false; + } + } + } + tmp + }; + #[cfg(not(windows))] + let ctrl = get_key_state(enigo::Key::Control) || get_key_state(enigo::Key::RightControl); + let shift = get_key_state(enigo::Key::Shift) || get_key_state(enigo::Key::RightShift); + #[cfg(windows)] + let command = crate::platform::windows::get_win_key_state(); + #[cfg(not(windows))] + let command = get_key_state(enigo::Key::Meta); + let control_key = match key { + Key::Alt => Some(ControlKey::Alt), + Key::AltGr => Some(ControlKey::RAlt), + Key::Backspace => Some(ControlKey::Backspace), + Key::ControlLeft => { + // when pressing AltGr, an extra VK_LCONTROL with a special + // scancode with bit 9 set is sent, let's ignore this. + #[cfg(windows)] + if event.scan_code & 0x200 != 0 { + unsafe { + IS_ALT_GR = true; + } + return; + } + Some(ControlKey::Control) + } + Key::ControlRight => Some(ControlKey::RControl), + Key::DownArrow => Some(ControlKey::DownArrow), + Key::Escape => Some(ControlKey::Escape), + Key::F1 => Some(ControlKey::F1), + Key::F10 => Some(ControlKey::F10), + Key::F11 => Some(ControlKey::F11), + Key::F12 => Some(ControlKey::F12), + Key::F2 => Some(ControlKey::F2), + Key::F3 => Some(ControlKey::F3), + Key::F4 => Some(ControlKey::F4), + Key::F5 => Some(ControlKey::F5), + Key::F6 => Some(ControlKey::F6), + Key::F7 => Some(ControlKey::F7), + Key::F8 => Some(ControlKey::F8), + Key::F9 => Some(ControlKey::F9), + Key::LeftArrow => Some(ControlKey::LeftArrow), + Key::MetaLeft => Some(ControlKey::Meta), + Key::MetaRight => Some(ControlKey::RWin), + Key::Return => Some(ControlKey::Return), + Key::RightArrow => Some(ControlKey::RightArrow), + Key::ShiftLeft => Some(ControlKey::Shift), + Key::ShiftRight => Some(ControlKey::RShift), + Key::Space => Some(ControlKey::Space), + Key::Tab => Some(ControlKey::Tab), + Key::UpArrow => Some(ControlKey::UpArrow), + Key::Delete => { + if is_win && ctrl && alt { + components::ctrl_alt_del(); + return; + } + Some(ControlKey::Delete) + } + Key::Apps => Some(ControlKey::Apps), + Key::Cancel => Some(ControlKey::Cancel), + Key::Clear => Some(ControlKey::Clear), + Key::Kana => Some(ControlKey::Kana), + Key::Hangul => Some(ControlKey::Hangul), + Key::Junja => Some(ControlKey::Junja), + Key::Final => Some(ControlKey::Final), + Key::Hanja => Some(ControlKey::Hanja), + Key::Hanji => Some(ControlKey::Hanja), + Key::Convert => Some(ControlKey::Convert), + Key::Print => Some(ControlKey::Print), + Key::Select => Some(ControlKey::Select), + Key::Execute => Some(ControlKey::Execute), + Key::PrintScreen => Some(ControlKey::Snapshot), + Key::Help => Some(ControlKey::Help), + Key::Sleep => Some(ControlKey::Sleep), + Key::Separator => Some(ControlKey::Separator), + Key::KpReturn => Some(ControlKey::NumpadEnter), + Key::Kp0 => Some(ControlKey::Numpad0), + Key::Kp1 => Some(ControlKey::Numpad1), + Key::Kp2 => Some(ControlKey::Numpad2), + Key::Kp3 => Some(ControlKey::Numpad3), + Key::Kp4 => Some(ControlKey::Numpad4), + Key::Kp5 => Some(ControlKey::Numpad5), + Key::Kp6 => Some(ControlKey::Numpad6), + Key::Kp7 => Some(ControlKey::Numpad7), + Key::Kp8 => Some(ControlKey::Numpad8), + Key::Kp9 => Some(ControlKey::Numpad9), + Key::KpDivide => Some(ControlKey::Divide), + Key::KpMultiply => Some(ControlKey::Multiply), + Key::KpDecimal => Some(ControlKey::Decimal), + Key::KpMinus => Some(ControlKey::Subtract), + Key::KpPlus => Some(ControlKey::Add), + Key::CapsLock | Key::NumLock | Key::ScrollLock => { + return; + } + Key::Home => Some(ControlKey::Home), + Key::End => Some(ControlKey::End), + Key::Insert => Some(ControlKey::Insert), + Key::PageUp => Some(ControlKey::PageUp), + Key::PageDown => Some(ControlKey::PageDown), + Key::Pause => Some(ControlKey::Pause), + _ => None, + }; + if let Some(k) = control_key { + key_event.set_control_key(k); + } else { + let mut chr = match event.name { + Some(ref s) => { + if s.len() <= 2 { + // exclude chinese characters + s.chars().next().unwrap_or('\0') + } else { + '\0' + } + } + _ => '\0', + }; + if chr == '·' { + // special for Chinese + chr = '`'; + } + if chr == '\0' { + chr = match key { + Key::Num1 => '1', + Key::Num2 => '2', + Key::Num3 => '3', + Key::Num4 => '4', + Key::Num5 => '5', + Key::Num6 => '6', + Key::Num7 => '7', + Key::Num8 => '8', + Key::Num9 => '9', + Key::Num0 => '0', + Key::KeyA => 'a', + Key::KeyB => 'b', + Key::KeyC => 'c', + Key::KeyD => 'd', + Key::KeyE => 'e', + Key::KeyF => 'f', + Key::KeyG => 'g', + Key::KeyH => 'h', + Key::KeyI => 'i', + Key::KeyJ => 'j', + Key::KeyK => 'k', + Key::KeyL => 'l', + Key::KeyM => 'm', + Key::KeyN => 'n', + Key::KeyO => 'o', + Key::KeyP => 'p', + Key::KeyQ => 'q', + Key::KeyR => 'r', + Key::KeyS => 's', + Key::KeyT => 't', + Key::KeyU => 'u', + Key::KeyV => 'v', + Key::KeyW => 'w', + Key::KeyX => 'x', + Key::KeyY => 'y', + Key::KeyZ => 'z', + Key::Comma => ',', + Key::Dot => '.', + Key::SemiColon => ';', + Key::Quote => '\'', + Key::LeftBracket => '[', + Key::RightBracket => ']', + Key::Slash => '/', + Key::BackSlash => '\\', + Key::Minus => '-', + Key::Equal => '=', + Key::BackQuote => '`', + _ => '\0', + } + } + if chr != '\0' { + if chr == 'l' && is_win && command { + components::lock_screen(); + return; + } + key_event.set_chr(chr as _); + } else { + log::error!("Unknown key {:?}", &event); + return; + } + } + let (alt, ctrl, shift, command) = + components::get_modifiers_state(alt, ctrl, shift, command); + components::legacy_modifiers(key_event, alt, ctrl, shift, command); + + if down_or_up == true { + key_event.down = true; + } + } + + pub fn map_keyboard_mode(event: &Event, key_event: &mut KeyEvent) { + let peer = components::get_peer_platform(); + + let key = match event.event_type { + EventType::KeyPress(key) => { + key_event.down = true; + key + } + EventType::KeyRelease(key) => { + key_event.down = false; + key + } + _ => return, + }; + let keycode: u32 = match peer.as_str() { + "Windows" => rdev::win_keycode_from_key(key).unwrap_or_default().into(), + "MacOS" => rdev::macos_keycode_from_key(key).unwrap_or_default().into(), + _ => rdev::linux_keycode_from_key(key).unwrap_or_default().into(), + }; + key_event.set_chr(keycode); + } + + pub fn translate_keyboard_mode(event: &Event, key_event: &mut KeyEvent) {} +} + +pub mod server_keyboard_mode { + use super::*; + + pub fn legacy_keyboard_mode(key_event: &KeyEvent) {} + + pub fn map_keyboard_mode(key_event: &KeyEvent) {} + + pub fn translate_keyboard_mode(key_event: &KeyEvent) {} +} diff --git a/src/lib.rs b/src/lib.rs index eb8a876ec..7b94c8a2c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,7 @@ #[cfg(not(any(target_os = "ios")))] /// cbindgen:ignore pub mod platform; +mod keyboard; #[cfg(not(any(target_os = "android", target_os = "ios")))] pub use platform::{get_cursor, get_cursor_data, get_cursor_pos, start_os_service}; #[cfg(not(any(target_os = "ios")))] diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 82d6592db..8dc9b51d0 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -715,3 +715,23 @@ pub fn get_double_click_time() -> u32 { double_click_time } } + +pub mod keyboard { + use crate::common::GrabState; + use hbb_common::{allow_err, log, message_proto::*}; + use rdev::{Event, EventType, Key}; + use std::sync::mpsc; + use std::thread; + + + + pub fn _legacy_keyboard_mode(event: &Event, key_event: &KeyEvent) { + log::info!("{:?}", event); + } + + pub fn _client_map_keyboard_mode(event: &Event, key_event: &KeyEvent) { + + } + + pub fn _translate_keyboard_mode(event: &Event, key_event: &KeyEvent) {} +} diff --git a/src/platform/macos.rs b/src/platform/macos.rs index edb2aadb1..c8daeeffa 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -542,3 +542,7 @@ pub fn get_double_click_time() -> u32 { // to-do: https://github.com/servo/core-foundation-rs/blob/786895643140fa0ee4f913d7b4aeb0c4626b2085/cocoa/src/appkit.rs#L2823 500 as _ } + +pub mod keyboard{ + +} \ No newline at end of file diff --git a/src/platform/mod.rs b/src/platform/mod.rs index f6b79da59..ed5fcfaa1 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -75,3 +75,4 @@ mod tests { } } } + diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 075f7ed08..7d3cbec2d 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -1712,3 +1712,17 @@ pub fn send_message_to_hnwd( } return true; } + +pub mod keyboard { + use crate::common::GrabState; + use hbb_common::{allow_err, log, message_proto::*}; + use rdev::{Event, EventType, Key}; + use std::sync::mpsc; + + + use crate::keyboard; + + + + +} diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 6b635436d..49955b476 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -4,8 +4,6 @@ use crate::client::{ load_config, send_mouse, start_video_audio_threads, FileManager, Key, LoginConfigHandler, QualityStatus, KEY_MAP, }; -#[cfg(not(any(target_os = "android", target_os = "ios")))] -use crate::client::{get_key_state, SERVER_KEYBOARD_ENABLED}; #[cfg(target_os = "linux")] use crate::common::IS_X11; use crate::{client::Data, client::Interface}; @@ -15,53 +13,12 @@ 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, EventType::*, Key as RdevKey}; -#[cfg(not(any(target_os = "android", target_os = "ios")))] -use rdev::{Keyboard as RdevKeyboard, KeyboardState}; -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; use std::ops::{Deref, DerefMut}; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::{Arc, Mutex, RwLock}; -/// IS_IN KEYBOARD_HOOKED sciter only pub static IS_IN: AtomicBool = AtomicBool::new(false); -pub static KEYBOARD_HOOKED: AtomicBool = AtomicBool::new(false); -pub static HOTKEY_HOOKED: AtomicBool = AtomicBool::new(false); -#[cfg(windows)] -static mut IS_ALT_GR: bool = false; -#[cfg(feature = "flutter")] -#[cfg(not(any(target_os = "android", target_os = "ios")))] -use crate::flutter::FlutterHandler; - -lazy_static::lazy_static! { - static ref TO_RELEASE: Arc>> = Arc::new(Mutex::new(HashSet::::new())); -} - -#[cfg(not(any(target_os = "android", target_os = "ios")))] -lazy_static::lazy_static! { - static ref KEYBOARD: Arc> = Arc::new(Mutex::new(RdevKeyboard::new().unwrap())); -} - -#[cfg(feature = "flutter")] -#[cfg(not(any(target_os = "android", target_os = "ios")))] -lazy_static::lazy_static! { - pub static ref CUR_SESSION: Arc>>> = Default::default(); -} - -lazy_static::lazy_static! { - static ref MUTEX_SPECIAL_KEYS: Mutex> = { - let mut m = HashMap::new(); - m.insert(RdevKey::ShiftLeft, false); - m.insert(RdevKey::ShiftRight, false); - m.insert(RdevKey::ControlLeft, false); - m.insert(RdevKey::ControlRight, false); - m.insert(RdevKey::Alt, false); - m.insert(RdevKey::AltGr, false); - m.insert(RdevKey::MetaLeft, false); - m.insert(RdevKey::MetaRight, false); - Mutex::new(m) - }; -} #[derive(Clone, Default)] pub struct Session { @@ -92,11 +49,11 @@ impl Session { } pub fn get_keyboard_mode(&self) -> String { - global_get_keyboard_mode() + self.lc.read().unwrap().keyboard_mode.clone() } - pub fn save_keyboard_mode(&self, value: String) { - global_save_keyboard_mode(value); + pub fn save_keyboard_mode(&mut self, value: String) { + self.lc.write().unwrap().save_keyboard_mode(value); } pub fn save_view_style(&mut self, value: String) { @@ -307,439 +264,6 @@ impl Session { self.lc.read().unwrap().info.platform.clone() } - pub fn ctrl_alt_del(&self) { - if self.peer_platform() == "Windows" { - let mut key_event = KeyEvent::new(); - key_event.set_control_key(ControlKey::CtrlAltDel); - // todo - key_event.down = true; - self.send_key_event(key_event, KeyboardMode::Legacy); - } else { - let mut key_event = KeyEvent::new(); - key_event.set_control_key(ControlKey::Delete); - self.legacy_modifiers(&mut key_event, true, true, false, false); - // todo - key_event.press = true; - self.send_key_event(key_event, KeyboardMode::Legacy); - } - } - - fn send_key_event(&self, mut evt: KeyEvent, keyboard_mode: KeyboardMode) { - // mode: legacy(0), map(1), translate(2), auto(3) - evt.mode = keyboard_mode.into(); - let mut msg_out = Message::new(); - msg_out.set_key_event(evt); - self.send(Data::Message(msg_out)); - } - - #[allow(dead_code)] - fn convert_numpad_keys(&self, key: RdevKey) -> RdevKey { - #[cfg(not(any(target_os = "android", target_os = "ios")))] - if get_key_state(enigo::Key::NumLock) { - return key; - } - match key { - RdevKey::Kp0 => RdevKey::Insert, - RdevKey::KpDecimal => RdevKey::Delete, - RdevKey::Kp1 => RdevKey::End, - RdevKey::Kp2 => RdevKey::DownArrow, - RdevKey::Kp3 => RdevKey::PageDown, - RdevKey::Kp4 => RdevKey::LeftArrow, - RdevKey::Kp5 => RdevKey::Clear, - RdevKey::Kp6 => RdevKey::RightArrow, - RdevKey::Kp7 => RdevKey::Home, - RdevKey::Kp8 => RdevKey::UpArrow, - RdevKey::Kp9 => RdevKey::PageUp, - _ => key, - } - } - - fn map_keyboard_mode(&self, down_or_up: bool, key: RdevKey, _evt: Option) { - // map mode(1): Send keycode according to the peer platform. - #[cfg(target_os = "windows")] - let key = if let Some(e) = _evt { - rdev::get_win_key(e.code.into(), e.scan_code) - } else { - key - }; - - let peer = self.peer_platform(); - let mut key_event = KeyEvent::new(); - // According to peer platform. - let keycode: u32 = if peer == "Linux" { - rdev::linux_keycode_from_key(key).unwrap_or_default().into() - } else if peer == "Windows" { - rdev::win_keycode_from_key(key).unwrap_or_default().into() - } else { - // Without Clear Key on Mac OS - if key == rdev::Key::Clear { - return; - } - rdev::macos_keycode_from_key(key).unwrap_or_default().into() - }; - - key_event.set_chr(keycode); - key_event.down = down_or_up; - #[cfg(not(any(target_os = "android", target_os = "ios")))] - if get_key_state(enigo::Key::CapsLock) { - key_event.modifiers.push(ControlKey::CapsLock.into()); - } - #[cfg(not(any(target_os = "android", target_os = "ios")))] - if get_key_state(enigo::Key::NumLock) { - key_event.modifiers.push(ControlKey::NumLock.into()); - } - self.send_key_event(key_event, KeyboardMode::Map); - } - - #[cfg(not(any(target_os = "android", target_os = "ios")))] - fn translate_keyboard_mode(&self, down_or_up: bool, key: RdevKey, evt: Event) { - // translate mode(2): locally generated characters are send to the peer. - - // get char - let string = match KEYBOARD.lock() { - Ok(mut keyboard) => { - let string = keyboard.add(&evt.event_type).unwrap_or_default(); - if keyboard.is_dead() && string == "" && down_or_up == true { - return; - } - string - } - Err(_) => "".to_owned(), - }; - - // maybe two string - let chars = if string == "" { - None - } else { - let chars: Vec = string.chars().collect(); - Some(chars) - }; - - if let Some(chars) = chars { - for chr in chars { - let mut key_event = KeyEvent::new(); - key_event.set_chr(chr as _); - key_event.down = true; - key_event.press = false; - - self.send_key_event(key_event, KeyboardMode::Translate); - } - } else { - let success = if down_or_up == true { - TO_RELEASE.lock().unwrap().insert(key) - } else { - TO_RELEASE.lock().unwrap().remove(&key) - }; - - // AltGr && LeftControl(SpecialKey) without action - if key == RdevKey::AltGr || evt.scan_code == 541 { - return; - } - if success { - self.map_keyboard_mode(down_or_up, key, None); - } - } - } - - fn legacy_modifiers( - &self, - key_event: &mut KeyEvent, - alt: bool, - ctrl: bool, - shift: bool, - command: bool, - ) { - if alt - && !crate::is_control_key(&key_event, &ControlKey::Alt) - && !crate::is_control_key(&key_event, &ControlKey::RAlt) - { - key_event.modifiers.push(ControlKey::Alt.into()); - } - if shift - && !crate::is_control_key(&key_event, &ControlKey::Shift) - && !crate::is_control_key(&key_event, &ControlKey::RShift) - { - key_event.modifiers.push(ControlKey::Shift.into()); - } - if ctrl - && !crate::is_control_key(&key_event, &ControlKey::Control) - && !crate::is_control_key(&key_event, &ControlKey::RControl) - { - key_event.modifiers.push(ControlKey::Control.into()); - } - if command - && !crate::is_control_key(&key_event, &ControlKey::Meta) - && !crate::is_control_key(&key_event, &ControlKey::RWin) - { - key_event.modifiers.push(ControlKey::Meta.into()); - } - #[cfg(not(any(target_os = "android", target_os = "ios")))] - if get_key_state(enigo::Key::CapsLock) { - key_event.modifiers.push(ControlKey::CapsLock.into()); - } - #[cfg(not(any(target_os = "android", target_os = "ios")))] - if self.peer_platform() != "Mac OS" { - if get_key_state(enigo::Key::NumLock) { - key_event.modifiers.push(ControlKey::NumLock.into()); - } - } - } - - #[cfg(not(any(target_os = "android", target_os = "ios")))] - fn legacy_keyboard_mode(&self, down_or_up: bool, key: RdevKey, evt: Event) { - // legacy mode(0): Generate characters locally, look for keycode on other side. - let peer = self.peer_platform(); - let is_win = peer == "Windows"; - - let alt = get_key_state(enigo::Key::Alt); - #[cfg(windows)] - let ctrl = { - let mut tmp = - get_key_state(enigo::Key::Control) || get_key_state(enigo::Key::RightControl); - unsafe { - if IS_ALT_GR { - if alt || key == RdevKey::AltGr { - if tmp { - tmp = false; - } - } else { - IS_ALT_GR = false; - } - } - } - tmp - }; - #[cfg(not(windows))] - let ctrl = get_key_state(enigo::Key::Control) || get_key_state(enigo::Key::RightControl); - let shift = get_key_state(enigo::Key::Shift) || get_key_state(enigo::Key::RightShift); - #[cfg(windows)] - let command = crate::platform::windows::get_win_key_state(); - #[cfg(not(windows))] - let command = get_key_state(enigo::Key::Meta); - let control_key = match key { - RdevKey::Alt => Some(ControlKey::Alt), - RdevKey::AltGr => Some(ControlKey::RAlt), - RdevKey::Backspace => Some(ControlKey::Backspace), - RdevKey::ControlLeft => { - // when pressing AltGr, an extra VK_LCONTROL with a special - // scancode with bit 9 set is sent, let's ignore this. - #[cfg(windows)] - if evt.scan_code & 0x200 != 0 { - unsafe { - IS_ALT_GR = true; - } - return; - } - Some(ControlKey::Control) - } - RdevKey::ControlRight => Some(ControlKey::RControl), - RdevKey::DownArrow => Some(ControlKey::DownArrow), - RdevKey::Escape => Some(ControlKey::Escape), - RdevKey::F1 => Some(ControlKey::F1), - RdevKey::F10 => Some(ControlKey::F10), - RdevKey::F11 => Some(ControlKey::F11), - RdevKey::F12 => Some(ControlKey::F12), - RdevKey::F2 => Some(ControlKey::F2), - RdevKey::F3 => Some(ControlKey::F3), - RdevKey::F4 => Some(ControlKey::F4), - RdevKey::F5 => Some(ControlKey::F5), - RdevKey::F6 => Some(ControlKey::F6), - RdevKey::F7 => Some(ControlKey::F7), - RdevKey::F8 => Some(ControlKey::F8), - RdevKey::F9 => Some(ControlKey::F9), - RdevKey::LeftArrow => Some(ControlKey::LeftArrow), - RdevKey::MetaLeft => Some(ControlKey::Meta), - RdevKey::MetaRight => Some(ControlKey::RWin), - RdevKey::Return => Some(ControlKey::Return), - RdevKey::RightArrow => Some(ControlKey::RightArrow), - RdevKey::ShiftLeft => Some(ControlKey::Shift), - RdevKey::ShiftRight => Some(ControlKey::RShift), - RdevKey::Space => Some(ControlKey::Space), - RdevKey::Tab => Some(ControlKey::Tab), - RdevKey::UpArrow => Some(ControlKey::UpArrow), - RdevKey::Delete => { - if is_win && ctrl && alt { - self.ctrl_alt_del(); - return; - } - Some(ControlKey::Delete) - } - RdevKey::Apps => Some(ControlKey::Apps), - RdevKey::Cancel => Some(ControlKey::Cancel), - RdevKey::Clear => Some(ControlKey::Clear), - RdevKey::Kana => Some(ControlKey::Kana), - RdevKey::Hangul => Some(ControlKey::Hangul), - RdevKey::Junja => Some(ControlKey::Junja), - RdevKey::Final => Some(ControlKey::Final), - RdevKey::Hanja => Some(ControlKey::Hanja), - RdevKey::Hanji => Some(ControlKey::Hanja), - RdevKey::Convert => Some(ControlKey::Convert), - RdevKey::Print => Some(ControlKey::Print), - RdevKey::Select => Some(ControlKey::Select), - RdevKey::Execute => Some(ControlKey::Execute), - RdevKey::PrintScreen => Some(ControlKey::Snapshot), - RdevKey::Help => Some(ControlKey::Help), - RdevKey::Sleep => Some(ControlKey::Sleep), - RdevKey::Separator => Some(ControlKey::Separator), - RdevKey::KpReturn => Some(ControlKey::NumpadEnter), - RdevKey::Kp0 => Some(ControlKey::Numpad0), - RdevKey::Kp1 => Some(ControlKey::Numpad1), - RdevKey::Kp2 => Some(ControlKey::Numpad2), - RdevKey::Kp3 => Some(ControlKey::Numpad3), - RdevKey::Kp4 => Some(ControlKey::Numpad4), - RdevKey::Kp5 => Some(ControlKey::Numpad5), - RdevKey::Kp6 => Some(ControlKey::Numpad6), - RdevKey::Kp7 => Some(ControlKey::Numpad7), - RdevKey::Kp8 => Some(ControlKey::Numpad8), - RdevKey::Kp9 => Some(ControlKey::Numpad9), - RdevKey::KpDivide => Some(ControlKey::Divide), - RdevKey::KpMultiply => Some(ControlKey::Multiply), - RdevKey::KpDecimal => Some(ControlKey::Decimal), - RdevKey::KpMinus => Some(ControlKey::Subtract), - RdevKey::KpPlus => Some(ControlKey::Add), - RdevKey::CapsLock | RdevKey::NumLock | RdevKey::ScrollLock => { - return; - } - RdevKey::Home => Some(ControlKey::Home), - RdevKey::End => Some(ControlKey::End), - RdevKey::Insert => Some(ControlKey::Insert), - RdevKey::PageUp => Some(ControlKey::PageUp), - RdevKey::PageDown => Some(ControlKey::PageDown), - RdevKey::Pause => Some(ControlKey::Pause), - _ => None, - }; - let mut key_event = KeyEvent::new(); - if let Some(k) = control_key { - key_event.set_control_key(k); - } else { - let mut chr = match evt.name { - Some(ref s) => { - if s.len() <= 2 { - // exclude chinese characters - s.chars().next().unwrap_or('\0') - } else { - '\0' - } - } - _ => '\0', - }; - if chr == '·' { - // special for Chinese - chr = '`'; - } - if chr == '\0' { - chr = match key { - RdevKey::Num1 => '1', - RdevKey::Num2 => '2', - RdevKey::Num3 => '3', - RdevKey::Num4 => '4', - RdevKey::Num5 => '5', - RdevKey::Num6 => '6', - RdevKey::Num7 => '7', - RdevKey::Num8 => '8', - RdevKey::Num9 => '9', - RdevKey::Num0 => '0', - RdevKey::KeyA => 'a', - RdevKey::KeyB => 'b', - RdevKey::KeyC => 'c', - RdevKey::KeyD => 'd', - RdevKey::KeyE => 'e', - RdevKey::KeyF => 'f', - RdevKey::KeyG => 'g', - RdevKey::KeyH => 'h', - RdevKey::KeyI => 'i', - RdevKey::KeyJ => 'j', - RdevKey::KeyK => 'k', - RdevKey::KeyL => 'l', - RdevKey::KeyM => 'm', - RdevKey::KeyN => 'n', - RdevKey::KeyO => 'o', - RdevKey::KeyP => 'p', - RdevKey::KeyQ => 'q', - RdevKey::KeyR => 'r', - RdevKey::KeyS => 's', - RdevKey::KeyT => 't', - RdevKey::KeyU => 'u', - RdevKey::KeyV => 'v', - RdevKey::KeyW => 'w', - RdevKey::KeyX => 'x', - RdevKey::KeyY => 'y', - RdevKey::KeyZ => 'z', - RdevKey::Comma => ',', - RdevKey::Dot => '.', - RdevKey::SemiColon => ';', - RdevKey::Quote => '\'', - RdevKey::LeftBracket => '[', - RdevKey::RightBracket => ']', - RdevKey::Slash => '/', - RdevKey::BackSlash => '\\', - RdevKey::Minus => '-', - RdevKey::Equal => '=', - RdevKey::BackQuote => '`', - _ => '\0', - } - } - if chr != '\0' { - if chr == 'l' && is_win && command { - self.lock_screen(); - return; - } - key_event.set_chr(chr as _); - } else { - log::error!("Unknown key {:?}", evt); - return; - } - } - - #[cfg(not(any(target_os = "android", target_os = "ios")))] - let (alt, ctrl, shift, command) = get_all_hotkey_state(alt, ctrl, shift, command); - self.legacy_modifiers(&mut key_event, alt, ctrl, shift, command); - - if down_or_up == true { - key_event.down = true; - } - self.send_key_event(key_event, KeyboardMode::Legacy) - } - - fn key_down_or_up(&self, down_or_up: bool, key: RdevKey, evt: Event) { - // Call different functions according to keyboard mode. - let mode = match self.get_keyboard_mode().as_str() { - "map" => KeyboardMode::Map, - "legacy" => KeyboardMode::Legacy, - "translate" => KeyboardMode::Translate, - _ => KeyboardMode::Legacy, - }; - - #[cfg(not(windows))] - let key = self.convert_numpad_keys(key); - - let mut to_release = TO_RELEASE.lock().unwrap(); - match mode { - KeyboardMode::Map => { - if down_or_up == true { - to_release.insert(key); - } else { - to_release.remove(&key); - } - self.map_keyboard_mode(down_or_up, key, Some(evt)); - } - KeyboardMode::Legacy => - { - #[cfg(not(any(target_os = "android", target_os = "ios")))] - self.legacy_keyboard_mode(down_or_up, key, evt) - } - KeyboardMode::Translate => { - #[cfg(not(any(target_os = "android", target_os = "ios")))] - self.translate_keyboard_mode(down_or_up, key, evt); - } - _ => - { - #[cfg(not(any(target_os = "android", target_os = "ios")))] - self.legacy_keyboard_mode(down_or_up, key, evt) - } - } - } - pub fn get_platform(&self, is_remote: bool) -> String { if is_remote { self.peer_platform() @@ -768,6 +292,13 @@ impl Session { return "".to_owned(); } + pub fn send_key_event(&self, mut evt: &KeyEvent) { + // mode: legacy(0), map(1), translate(2), auto(3) + let mut msg_out = Message::new(); + msg_out.set_key_event(evt.clone()); + self.send(Data::Message(msg_out)); + } + pub fn send_chat(&self, text: String) { let mut misc = Misc::new(); misc.set_chat_message(ChatMessage { @@ -790,77 +321,14 @@ impl Session { self.send(Data::Message(msg_out)); } - pub fn lock_screen(&self) { - let mut key_event = KeyEvent::new(); - key_event.set_control_key(ControlKey::LockScreen); - // todo - key_event.down = true; - self.send_key_event(key_event, KeyboardMode::Legacy); - } - pub fn enter(&self) { IS_IN.store(true, Ordering::SeqCst); - #[cfg(target_os = "linux")] - self.grab_hotkeys(true); - - #[cfg(windows)] - crate::platform::windows::stop_system_key_propagate(true); + keyboard::client::change_grab_status(GrabState::Run); } pub fn leave(&self) { IS_IN.store(false, Ordering::SeqCst); - #[cfg(target_os = "linux")] - self.grab_hotkeys(false); - - for key in TO_RELEASE.lock().unwrap().iter() { - self.map_keyboard_mode(false, *key, None) - } - #[cfg(windows)] - crate::platform::windows::stop_system_key_propagate(false); - } - - #[cfg(target_os = "linux")] - pub fn grab_hotkeys(&self, _grab: bool) { - if _grab { - rdev::enable_grab().ok(); - } else { - rdev::disable_grab().ok(); - } - } - - pub fn handle_flutter_key_event( - &self, - name: &str, - keycode: i32, - scancode: i32, - down_or_up: bool, - ) { - if scancode < 0 || keycode < 0 { - return; - } - let keycode: u32 = keycode as u32; - let scancode: u32 = scancode as u32; - - #[cfg(not(target_os = "windows"))] - let key = rdev::key_from_scancode(scancode) as RdevKey; - // Windows requires special handling - #[cfg(target_os = "windows")] - let key = rdev::get_win_key(keycode, scancode); - - let event_type = if down_or_up { - KeyPress(key) - } else { - KeyRelease(key) - }; - let evt = Event { - time: std::time::SystemTime::now(), - name: Option::Some(name.to_owned()), - code: keycode as _, - scan_code: scancode as _, - event_type: event_type, - }; - - self.key_down_or_up(down_or_up, key, evt) + keyboard::client::change_grab_status(GrabState::Wait); } // flutter only TODO new input @@ -874,9 +342,6 @@ impl Session { shift: bool, command: bool, ) { - if HOTKEY_HOOKED.load(Ordering::SeqCst) { - return; - } let chars: Vec = name.chars().collect(); if chars.len() == 1 { let key = Key::_Raw(chars[0] as _); @@ -921,25 +386,6 @@ impl Session { key_event.set_chr(chr); } Key::ControlKey(key) => { - #[cfg(not(any(target_os = "android", target_os = "ios")))] - let key = if !get_key_state(enigo::Key::NumLock) { - match key { - ControlKey::Numpad0 => ControlKey::Insert, - ControlKey::Decimal => ControlKey::Delete, - ControlKey::Numpad1 => ControlKey::End, - ControlKey::Numpad2 => ControlKey::DownArrow, - ControlKey::Numpad3 => ControlKey::PageDown, - ControlKey::Numpad4 => ControlKey::LeftArrow, - ControlKey::Numpad5 => ControlKey::Clear, - ControlKey::Numpad6 => ControlKey::RightArrow, - ControlKey::Numpad7 => ControlKey::Home, - ControlKey::Numpad8 => ControlKey::UpArrow, - ControlKey::Numpad9 => ControlKey::PageUp, - _ => key, - } - } else { - key - }; key_event.set_control_key(key.clone()); } Key::_Raw(raw) => { @@ -947,17 +393,15 @@ impl Session { } } - #[cfg(not(any(target_os = "android", target_os = "ios")))] - let (alt, ctrl, shift, command) = get_all_hotkey_state(alt, ctrl, shift, command); - - self.legacy_modifiers(&mut key_event, alt, ctrl, shift, command); if v == 1 { key_event.down = true; } else if v == 3 { key_event.press = true; } + keyboard::client::legacy_modifiers(&mut key_event, alt, ctrl, shift, command); + key_event.mode = KeyboardMode::Legacy.into(); - self.send_key_event(key_event, KeyboardMode::Legacy); + self.send_key_event(&key_event); } pub fn send_mouse( @@ -979,8 +423,10 @@ impl Session { } } - #[cfg(not(any(target_os = "android", target_os = "ios")))] - let (alt, ctrl, shift, command) = get_all_hotkey_state(alt, ctrl, shift, command); + // todo! chieh + // #[cfg(not(any(target_os = "android", target_os = "ios")))] + let (alt, ctrl, shift, command) = + keyboard::client::get_modifiers_state(alt, ctrl, shift, command); send_mouse(mask, x, y, alt, ctrl, shift, command, self); // on macos, ctrl + left button down = right button down, up won't emit, so we need to @@ -1243,23 +689,6 @@ impl Interface for Session { crate::platform::windows::add_recent_document(&path); } } - // only run in sciter - #[cfg(not(feature = "flutter"))] - { - // rdev::grab and rdev::listen use the same api in macOS & Windows - /* todo! Unused */ - #[cfg(not(any( - target_os = "android", - target_os = "ios", - target_os = "macos", - target_os = "windows", - target_os = "linux", - )))] - self.start_keyboard_hook(); - /* todo! (sciter) Only one device can be connected at the same time in linux */ - #[cfg(not(any(target_os = "android", target_os = "ios")))] - self.start_grab_hotkey(); - } } async fn handle_hash(&mut self, pass: &str, hash: Hash, peer: &mut Stream) { @@ -1300,113 +729,14 @@ impl Interface for Session { } } -#[cfg(not(any(target_os = "android", target_os = "ios")))] impl Session { - fn handle_hotkey_event(&self, event: Event) { - // if is long press, don't do anything. - if is_long_press(&event) { - return; - } - - let (key, down) = match event.event_type { - EventType::KeyPress(key) => (key, true), - EventType::KeyRelease(key) => (key, false), - _ => return, - }; - - self.key_down_or_up(down, key, event); + pub fn lock_screen(&self) { + log::info!("Sending key even"); + crate::keyboard::client::lock_screen(); } - - #[allow(dead_code)] - fn start_grab_hotkey(&self) { - if self.is_port_forward() || self.is_file_transfer() { - return; - } - #[cfg(target_os = "linux")] - if !*IS_X11.lock().unwrap() { - return; - } - if HOTKEY_HOOKED.swap(true, Ordering::SeqCst) { - return; - } - - log::info!("starting grab hotkeys"); - let me = self.clone(); - - #[cfg(target_os = "linux")] - { - let func = move |event: Event| match event.event_type { - EventType::KeyPress(_key) | EventType::KeyRelease(_key) => { - me.handle_hotkey_event(event); - None - } - _ => Some(event), - }; - rdev::start_grab_listen(func) - } - #[cfg(any(target_os = "windows", target_os = "macos"))] - std::thread::spawn(move || { - let func = move |event: Event| match event.event_type { - EventType::KeyPress(..) | EventType::KeyRelease(..) => { - // grab all keys - if !IS_IN.load(Ordering::SeqCst) - || !SERVER_KEYBOARD_ENABLED.load(Ordering::SeqCst) - { - return Some(event); - } else { - me.handle_hotkey_event(event); - return None; - } - } - _ => Some(event), - }; - if let Err(error) = rdev::grab(func) { - log::error!("Error: {:?}", error) - } - }); - } - - #[allow(dead_code)] - fn start_keyboard_hook(&self) { - // only run in sciter - if self.is_port_forward() || self.is_file_transfer() { - return; - } - if KEYBOARD_HOOKED.swap(true, Ordering::SeqCst) { - return; - } - log::info!("keyboard hooked"); - - let me = self.clone(); - #[cfg(windows)] - crate::platform::windows::enable_lowlevel_keyboard(std::ptr::null_mut() as _); - std::thread::spawn(move || { - // This will block. - std::env::set_var("KEYBOARD_ONLY", "y"); - - let func = move |evt: Event| { - /* todo! IS_IN can't determine if the user is focused on remote page */ - if !IS_IN.load(Ordering::SeqCst) || !SERVER_KEYBOARD_ENABLED.load(Ordering::SeqCst) - { - return; - } - if is_long_press(&evt) { - return; - } - let (key, down) = match evt.event_type { - EventType::KeyPress(key) => (key, true), - EventType::KeyRelease(key) => (key, false), - _ => return, - }; - me.key_down_or_up(down, key, evt); - }; - /* todo!: Shift + a -> AA in sciter - * rdev::listen and rdev::grab both send a - */ - if let Err(error) = rdev::listen(func) { - log::error!("rdev: {:?}", error); - } - }); + pub fn ctrl_alt_del(&self) { + log::info!("Sending key even"); + crate::keyboard::client::lock_screen(); } } @@ -1560,107 +890,3 @@ async fn send_note(url: String, id: String, conn_id: i32, note: String) { let body = serde_json::json!({ "id": id, "Id": conn_id, "note": note }); allow_err!(crate::post_request(url, body.to_string(), "").await); } - -fn get_hotkey_state(key: RdevKey) -> bool { - if let Some(&state) = MUTEX_SPECIAL_KEYS.lock().unwrap().get(&key) { - return state; - } else { - return false; - } -} - -fn get_all_hotkey_state( - alt: bool, - ctrl: bool, - shift: bool, - command: bool, -) -> (bool, bool, bool, bool) { - let ctrl = - get_hotkey_state(RdevKey::ControlLeft) || get_hotkey_state(RdevKey::ControlRight) || ctrl; - let shift = - get_hotkey_state(RdevKey::ShiftLeft) || get_hotkey_state(RdevKey::ShiftRight) || shift; - let command = - get_hotkey_state(RdevKey::MetaLeft) || get_hotkey_state(RdevKey::MetaRight) || command; - let alt = get_hotkey_state(RdevKey::Alt) || get_hotkey_state(RdevKey::AltGr) || alt; - - (alt, ctrl, shift, command) -} - -#[cfg(feature = "flutter")] -#[cfg(not(any(target_os = "android", target_os = "ios")))] -pub fn send_key_event_to_session(event: rdev::Event) { - if let Some(handler) = CUR_SESSION.lock().unwrap().as_ref() { - handler.handle_hotkey_event(event); - } -} - -#[cfg(feature = "flutter")] -pub fn global_grab_keyboard() { - if HOTKEY_HOOKED.swap(true, Ordering::SeqCst) { - return; - } - log::info!("starting global grab keyboard"); - - #[cfg(target_os = "linux")] - { - let func = move |event: Event| match event.event_type { - EventType::KeyPress(_key) | EventType::KeyRelease(_key) => { - send_key_event_to_session(event); - None - } - _ => Some(event), - }; - rdev::start_grab_listen(func) - } - - #[cfg(any(target_os = "windows", target_os = "macos"))] - std::thread::spawn(move || { - let func = move |event: Event| match event.event_type { - EventType::KeyPress(..) | EventType::KeyRelease(..) => { - // grab all keys - if !IS_IN.load(Ordering::SeqCst) { - return Some(event); - } else { - send_key_event_to_session(event); - return None; - } - } - _ => Some(event), - }; - if let Err(error) = rdev::grab(func) { - log::error!("Error: {:?}", error) - } - }); -} - -pub fn global_get_keyboard_mode() -> String { - return std::env::var("KEYBOARD_MODE") - .unwrap_or(String::from("map")) - .to_lowercase(); -} - -pub fn global_save_keyboard_mode(value: String) { - std::env::set_var("KEYBOARD_MODE", value); -} - -fn is_long_press(event: &Event) -> bool { - let mut keys = MUTEX_SPECIAL_KEYS.lock().unwrap(); - match event.event_type { - EventType::KeyPress(k) => { - if let Some(&state) = keys.get(&k) { - if state == true { - return true; - } else { - keys.insert(k, true); - } - } - } - EventType::KeyRelease(k) => { - if keys.contains_key(&k) { - keys.insert(k, false); - } - } - _ => {} - }; - return false; -} From dff5d55f50e6b3747c663325bf068d7532b8fad3 Mon Sep 17 00:00:00 2001 From: Asura Date: Sat, 3 Dec 2022 00:26:45 -0800 Subject: [PATCH 1134/2015] =?UTF-8?q?fix=20#2211=EF=BC=9ACAPS=20Lock=20don?= =?UTF-8?q?'t=20work?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/keyboard.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/keyboard.rs b/src/keyboard.rs index f278237ca..c28c81909 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -122,7 +122,10 @@ mod components { std::thread::spawn(move || { let func = move |event: Event| match event.event_type { EventType::KeyPress(key) | EventType::KeyRelease(key) => { - // todo!: CAPSLOCK don't work + // fix #2211:CAPS LOCK don't work + if key == Key::CapsLock || key == Key::NumLock { + return Some(event); + } if KEYBOARD_HOOKED.load(Ordering::SeqCst) { keyboard::client::process_event(event); return None; From 176ed4380756768b5c76f560bedc160df344987f Mon Sep 17 00:00:00 2001 From: Asura Date: Wed, 7 Dec 2022 19:31:32 -0800 Subject: [PATCH 1135/2015] refacotor: simplify mod of keyboard --- src/client.rs | 2 - src/keyboard.rs | 1043 +++++++++++++++++------------------ src/platform/linux.rs | 19 - src/platform/macos.rs | 4 - src/platform/windows.rs | 14 - src/server/input_service.rs | 3 - 6 files changed, 495 insertions(+), 590 deletions(-) diff --git a/src/client.rs b/src/client.rs index 3b932a39a..03bbf5918 100644 --- a/src/client.rs +++ b/src/client.rs @@ -6,8 +6,6 @@ use cpal::{ }; use magnum_opus::{Channels::*, Decoder as AudioDecoder}; use sha2::{Digest, Sha256}; -#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] -use std::sync::atomic::Ordering; use std::{ collections::HashMap, net::SocketAddr, diff --git a/src/keyboard.rs b/src/keyboard.rs index c28c81909..07bcecf76 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -2,14 +2,20 @@ use crate::client::get_key_state; use crate::common::GrabState; #[cfg(feature = "flutter")] use crate::flutter::FlutterHandler; -use crate::platform; use crate::ui_session_interface::Session; -use hbb_common::{allow_err, log, message_proto::*}; +use hbb_common::{log, message_proto::*}; use rdev::{Event, EventType, Key}; use std::collections::{HashMap, HashSet}; -use std::sync::{mpsc, Arc, Mutex, RwLock}; +use std::sync::atomic::AtomicBool; +#[cfg(any(target_os = "windows", target_os = "macos"))] +use std::sync::atomic::Ordering; +use std::sync::{mpsc, Arc, Mutex}; +use std::thread; use std::time::SystemTime; +static mut IS_ALT_GR: bool = false; +pub static KEYBOARD_HOOKED: AtomicBool = AtomicBool::new(false); + #[cfg(feature = "flutter")] lazy_static::lazy_static! { pub static ref CUR_SESSION: Arc>>> = Default::default(); @@ -34,175 +40,7 @@ lazy_static::lazy_static! { } pub mod client { - use super::{client_keyboard_mode, components, *}; - - pub fn get_keyboard_mode() -> String { - return components::get_keyboard_mode(); - } - - pub fn save_keyboard_mode(value: String) { - components::save_keyboard_mode(value); - } - - pub fn start_grab_loop() { - let (sender, receiver) = mpsc::channel::(); - unsafe { - components::grab_loop(receiver); - *GRAB_SENDER.lock().unwrap() = Some(sender); - } - change_grab_status(GrabState::Ready); - } - - pub fn change_grab_status(state: GrabState) { - if GrabState::Wait == state { - components::release_remote_keys(); - } - unsafe { - if let Some(sender) = &*GRAB_SENDER.lock().unwrap() { - log::info!("grab state: {:?}", state); - sender.send(state); - } - } - } - - pub fn process_event(event: Event) { - if components::is_long_press(&event) { - return; - } - let key_event = components::event_to_key_event(&event); - log::info!("key event: {:?}", key_event); - components::send_key_event(&key_event); - } - - pub fn get_modifiers_state( - alt: bool, - ctrl: bool, - shift: bool, - command: bool, - ) -> (bool, bool, bool, bool) { - components::get_modifiers_state(alt, ctrl, shift, command) - } - - pub fn legacy_modifiers( - key_event: &mut KeyEvent, - alt: bool, - ctrl: bool, - shift: bool, - command: bool, - ) { - components::legacy_modifiers(key_event, alt, ctrl, shift, command); - } - - pub fn lock_screen() { - components::lock_screen(); - } - - pub fn ctrl_alt_del() { - components::ctrl_alt_del(); - } -} - -pub mod server { - pub fn simulate() {} -} - -mod components { use super::*; - use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; - use std::thread; - - pub static KEYBOARD_HOOKED: AtomicBool = AtomicBool::new(false); - - pub fn grab_loop(recv: mpsc::Receiver) { - thread::spawn(move || loop { - if let Some(state) = recv.recv().ok() { - match state { - GrabState::Ready => { - #[cfg(any(target_os = "windows", target_os = "macos"))] - std::thread::spawn(move || { - let func = move |event: Event| match event.event_type { - EventType::KeyPress(key) | EventType::KeyRelease(key) => { - // fix #2211:CAPS LOCK don't work - if key == Key::CapsLock || key == Key::NumLock { - return Some(event); - } - if KEYBOARD_HOOKED.load(Ordering::SeqCst) { - keyboard::client::process_event(event); - return None; - } else { - return Some(event); - } - } - _ => Some(event), - }; - if let Err(error) = rdev::grab(func) { - log::error!("rdev Error: {:?}", error) - } - }); - - #[cfg(target_os = "linux")] - rdev::start_grab_listen(move |event: Event| match event.event_type { - EventType::KeyPress(key) | EventType::KeyRelease(key) => { - if let Key::Unknown(keycode) = key { - log::error!("rdev get unknown key, keycode is : {:?}", keycode); - } else { - crate::keyboard::client::process_event(event); - } - None - } - _ => Some(event), - }); - } - GrabState::Run => { - #[cfg(any(target_os = "windows", target_os = "macos"))] - KEYBOARD_HOOKED.swap(true, Ordering::SeqCst); - - #[cfg(target_os = "linux")] - rdev::enable_grab().ok(); - } - GrabState::Wait => { - #[cfg(any(target_os = "windows", target_os = "macos"))] - KEYBOARD_HOOKED.swap(false, Ordering::SeqCst); - - #[cfg(target_os = "linux")] - rdev::disable_grab().ok(); - } - GrabState::Exit => { - #[cfg(target_os = "linux")] - rdev::exit_grab_listen().ok(); - } - } - } - }); - } - - pub fn is_long_press(event: &Event) -> bool { - let mut keys = MODIFIERS_STATE.lock().unwrap(); - match event.event_type { - EventType::KeyPress(k) => { - if let Some(&state) = keys.get(&k) { - if state == true { - return true; - } - } - } - _ => {} - }; - return false; - } - - pub fn release_remote_keys() { - // todo!: client quit suddenly, how to release keys? - let to_release = TO_RELEASE.lock().unwrap(); - let keys = to_release.iter().map(|&key| key).collect::>(); - drop(to_release); - for key in keys { - let event_type = EventType::KeyRelease(key); - let event = event_type_to_event(event_type); - log::info!("release key: {:?}", key); - client::process_event(event); - } - } pub fn get_keyboard_mode() -> String { if let Some(handler) = CUR_SESSION.lock().unwrap().as_ref() { @@ -219,41 +57,34 @@ mod components { } } - pub fn get_keyboard_mode_enum() -> KeyboardMode { - match get_keyboard_mode().as_str() { - "map" => KeyboardMode::Map, - "translate" => KeyboardMode::Translate, - _ => KeyboardMode::Legacy, + pub fn start_grab_loop() { + let (sender, receiver) = mpsc::channel::(); + unsafe { + grab_loop(receiver); + *GRAB_SENDER.lock().unwrap() = Some(sender); + } + change_grab_status(GrabState::Ready); + } + + pub fn change_grab_status(state: GrabState) { + if GrabState::Wait == state { + release_remote_keys(); + } + unsafe { + if let Some(sender) = &*GRAB_SENDER.lock().unwrap() { + log::info!("grab state: {:?}", state); + sender.send(state); + } } } - pub fn add_numlock_capslock_state(key_event: &mut KeyEvent) { - if get_key_state(enigo::Key::CapsLock) { - key_event.modifiers.push(ControlKey::CapsLock.into()); - } - if get_key_state(enigo::Key::NumLock) { - key_event.modifiers.push(ControlKey::NumLock.into()); - } - } - - pub fn convert_numpad_keys(key: Key) -> Key { - if get_key_state(enigo::Key::NumLock) { - return key; - } - match key { - Key::Kp0 => Key::Insert, - Key::KpDecimal => Key::Delete, - Key::Kp1 => Key::End, - Key::Kp2 => Key::DownArrow, - Key::Kp3 => Key::PageDown, - Key::Kp4 => Key::LeftArrow, - Key::Kp5 => Key::Clear, - Key::Kp6 => Key::RightArrow, - Key::Kp7 => Key::Home, - Key::Kp8 => Key::UpArrow, - Key::Kp9 => Key::PageUp, - _ => key, + pub fn process_event(event: Event) { + if is_long_press(&event) { + return; } + let key_event = event_to_key_event(&event); + log::info!("key event: {:?}", key_event); + send_key_event(&key_event); } pub fn get_modifiers_state( @@ -262,39 +93,7 @@ mod components { shift: bool, command: bool, ) -> (bool, bool, bool, bool) { - let modifiers_lock = MODIFIERS_STATE.lock().unwrap(); - let ctrl = *modifiers_lock.get(&Key::ControlLeft).unwrap() - || *modifiers_lock.get(&Key::ControlRight).unwrap() - || ctrl; - let shift = *modifiers_lock.get(&Key::ShiftLeft).unwrap() - || *modifiers_lock.get(&Key::ShiftRight).unwrap() - || shift; - let command = *modifiers_lock.get(&Key::MetaLeft).unwrap() - || *modifiers_lock.get(&Key::MetaRight).unwrap() - || command; - let alt = *modifiers_lock.get(&Key::Alt).unwrap() - || *modifiers_lock.get(&Key::AltGr).unwrap() - || alt; - - (alt, ctrl, shift, command) - } - - fn update_modifiers_state(event: &Event) { - // for mouse - let mut keys = MODIFIERS_STATE.lock().unwrap(); - match event.event_type { - EventType::KeyPress(k) => { - if keys.contains_key(&k) { - keys.insert(k, true); - } - } - EventType::KeyRelease(k) => { - if keys.contains_key(&k) { - keys.insert(k, false); - } - } - _ => {} - }; + get_modifiers_state(alt, ctrl, shift, command) } pub fn legacy_modifiers( @@ -330,49 +129,12 @@ mod components { } } - pub fn event_to_key_event(event: &Event) -> KeyEvent { + pub fn lock_screen() { let mut key_event = KeyEvent::new(); - update_modifiers_state(event); - - let mut to_release = TO_RELEASE.lock().unwrap(); - match event.event_type { - EventType::KeyPress(key) => { - to_release.insert(key); - } - EventType::KeyRelease(key) => { - to_release.remove(&key); - } - _ => {} - } - drop(to_release); - - let keyboard_mode = get_keyboard_mode_enum(); - key_event.mode = keyboard_mode.into(); - match keyboard_mode { - KeyboardMode::Map => { - client_keyboard_mode::map_keyboard_mode(event, &mut key_event); - } - KeyboardMode::Translate => { - client_keyboard_mode::translate_keyboard_mode(event, &mut key_event); - } - _ => { - client_keyboard_mode::legacy_keyboard_mode(event, &mut key_event); - } - }; - #[cfg(not(any(target_os = "android", target_os = "ios")))] - add_numlock_capslock_state(&mut key_event); - - return key_event; - } - - pub fn event_type_to_event(event_type: EventType) -> Event { - Event { - event_type, - time: SystemTime::now(), - name: None, - code: 0, - scan_code: 0, - } + key_event.set_control_key(ControlKey::LockScreen); + key_event.down = true; + key_event.mode = KeyboardMode::Legacy.into(); + send_key_event(&key_event); } pub fn ctrl_alt_del() { @@ -388,294 +150,479 @@ mod components { key_event.mode = KeyboardMode::Legacy.into(); send_key_event(&key_event); } - - pub fn lock_screen() { - let mut key_event = KeyEvent::new(); - key_event.set_control_key(ControlKey::LockScreen); - key_event.down = true; - key_event.mode = KeyboardMode::Legacy.into(); - send_key_event(&key_event); - } - - #[cfg(feature = "flutter")] - pub fn send_key_event(key_event: &KeyEvent) { - if let Some(handler) = CUR_SESSION.lock().unwrap().as_ref() { - log::info!("Sending key even {:?}", key_event); - handler.send_key_event(key_event); - } - } - - pub fn get_peer_platform() -> String { - if let Some(handler) = CUR_SESSION.lock().unwrap().as_ref() { - handler.peer_platform() - } else { - log::error!("get peer platform error"); - "Windows".to_string() - } - } } -mod client_keyboard_mode { - use super::*; - use components; - use rdev::EventType; - - static mut IS_ALT_GR: bool = false; - - pub fn legacy_keyboard_mode(event: &Event, key_event: &mut KeyEvent) { - // 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; - } - }; - - let peer = components::get_peer_platform(); - let is_win = peer == "Windows"; - if is_win { - key = components::convert_numpad_keys(key); - } - - let alt = get_key_state(enigo::Key::Alt); - #[cfg(windows)] - let ctrl = { - let mut tmp = - get_key_state(enigo::Key::Control) || get_key_state(enigo::Key::RightControl); - unsafe { - if IS_ALT_GR { - if alt || key == Key::AltGr { - if tmp { - tmp = false; +pub fn grab_loop(recv: mpsc::Receiver) { + thread::spawn(move || loop { + if let Some(state) = recv.recv().ok() { + match state { + GrabState::Ready => { + #[cfg(any(target_os = "windows", target_os = "macos"))] + std::thread::spawn(move || { + let func = move |event: Event| match event.event_type { + EventType::KeyPress(key) | EventType::KeyRelease(key) => { + // fix #2211:CAPS LOCK don't work + if key == Key::CapsLock || key == Key::NumLock { + return Some(event); + } + if KEYBOARD_HOOKED.load(Ordering::SeqCst) { + keyboard::client::process_event(event); + return None; + } else { + return Some(event); + } + } + _ => Some(event), + }; + if let Err(error) = rdev::grab(func) { + log::error!("rdev Error: {:?}", error) } - } else { - IS_ALT_GR = false; + }); + + #[cfg(target_os = "linux")] + rdev::start_grab_listen(move |event: Event| match event.event_type { + EventType::KeyPress(key) | EventType::KeyRelease(key) => { + if let Key::Unknown(keycode) = key { + log::error!("rdev get unknown key, keycode is : {:?}", keycode); + } else { + crate::keyboard::client::process_event(event); + } + None + } + _ => Some(event), + }); + } + GrabState::Run => { + #[cfg(any(target_os = "windows", target_os = "macos"))] + KEYBOARD_HOOKED.swap(true, Ordering::SeqCst); + + #[cfg(target_os = "linux")] + rdev::enable_grab().ok(); + } + GrabState::Wait => { + #[cfg(any(target_os = "windows", target_os = "macos"))] + KEYBOARD_HOOKED.swap(false, Ordering::SeqCst); + + #[cfg(target_os = "linux")] + rdev::disable_grab().ok(); + } + GrabState::Exit => { + #[cfg(target_os = "linux")] + rdev::exit_grab_listen().ok(); + } + } + } + }); +} + +pub fn is_long_press(event: &Event) -> bool { + let mut keys = MODIFIERS_STATE.lock().unwrap(); + match event.event_type { + EventType::KeyPress(k) => { + if let Some(&state) = keys.get(&k) { + if state == true { + return true; + } + } + } + _ => {} + }; + return false; +} + +pub fn release_remote_keys() { + // todo!: client quit suddenly, how to release keys? + let to_release = TO_RELEASE.lock().unwrap(); + let keys = to_release.iter().map(|&key| key).collect::>(); + drop(to_release); + for key in keys { + let event_type = EventType::KeyRelease(key); + let event = event_type_to_event(event_type); + log::info!("release key: {:?}", key); + client::process_event(event); + } +} + +pub fn get_keyboard_mode_enum() -> KeyboardMode { + match client::get_keyboard_mode().as_str() { + "map" => KeyboardMode::Map, + "translate" => KeyboardMode::Translate, + _ => KeyboardMode::Legacy, + } +} + +pub fn add_numlock_capslock_state(key_event: &mut KeyEvent) { + if get_key_state(enigo::Key::CapsLock) { + key_event.modifiers.push(ControlKey::CapsLock.into()); + } + if get_key_state(enigo::Key::NumLock) { + key_event.modifiers.push(ControlKey::NumLock.into()); + } +} + +pub fn convert_numpad_keys(key: Key) -> Key { + if get_key_state(enigo::Key::NumLock) { + return key; + } + match key { + Key::Kp0 => Key::Insert, + Key::KpDecimal => Key::Delete, + Key::Kp1 => Key::End, + Key::Kp2 => Key::DownArrow, + Key::Kp3 => Key::PageDown, + Key::Kp4 => Key::LeftArrow, + Key::Kp5 => Key::Clear, + Key::Kp6 => Key::RightArrow, + Key::Kp7 => Key::Home, + Key::Kp8 => Key::UpArrow, + Key::Kp9 => Key::PageUp, + _ => key, + } +} + +pub fn get_modifiers_state( + alt: bool, + ctrl: bool, + shift: bool, + command: bool, +) -> (bool, bool, bool, bool) { + let modifiers_lock = MODIFIERS_STATE.lock().unwrap(); + let ctrl = *modifiers_lock.get(&Key::ControlLeft).unwrap() + || *modifiers_lock.get(&Key::ControlRight).unwrap() + || ctrl; + let shift = *modifiers_lock.get(&Key::ShiftLeft).unwrap() + || *modifiers_lock.get(&Key::ShiftRight).unwrap() + || shift; + let command = *modifiers_lock.get(&Key::MetaLeft).unwrap() + || *modifiers_lock.get(&Key::MetaRight).unwrap() + || command; + let alt = + *modifiers_lock.get(&Key::Alt).unwrap() || *modifiers_lock.get(&Key::AltGr).unwrap() || alt; + + (alt, ctrl, shift, command) +} + +fn update_modifiers_state(event: &Event) { + // for mouse + let mut keys = MODIFIERS_STATE.lock().unwrap(); + match event.event_type { + EventType::KeyPress(k) => { + if keys.contains_key(&k) { + keys.insert(k, true); + } + } + EventType::KeyRelease(k) => { + if keys.contains_key(&k) { + keys.insert(k, false); + } + } + _ => {} + }; +} + +pub fn event_to_key_event(event: &Event) -> KeyEvent { + let mut key_event = KeyEvent::new(); + update_modifiers_state(event); + + let mut to_release = TO_RELEASE.lock().unwrap(); + match event.event_type { + EventType::KeyPress(key) => { + to_release.insert(key); + } + EventType::KeyRelease(key) => { + to_release.remove(&key); + } + _ => {} + } + drop(to_release); + + let keyboard_mode = get_keyboard_mode_enum(); + key_event.mode = keyboard_mode.into(); + match keyboard_mode { + KeyboardMode::Map => { + map_keyboard_mode(event, &mut key_event); + } + KeyboardMode::Translate => { + translate_keyboard_mode(event, &mut key_event); + } + _ => { + legacy_keyboard_mode(event, &mut key_event); + } + }; + #[cfg(not(any(target_os = "android", target_os = "ios")))] + add_numlock_capslock_state(&mut key_event); + + return key_event; +} + +pub fn event_type_to_event(event_type: EventType) -> Event { + Event { + event_type, + time: SystemTime::now(), + name: None, + code: 0, + scan_code: 0, + } +} + +#[cfg(feature = "flutter")] +pub fn send_key_event(key_event: &KeyEvent) { + if let Some(handler) = CUR_SESSION.lock().unwrap().as_ref() { + log::info!("Sending key even {:?}", key_event); + handler.send_key_event(key_event); + } +} + +pub fn get_peer_platform() -> String { + if let Some(handler) = CUR_SESSION.lock().unwrap().as_ref() { + handler.peer_platform() + } else { + log::error!("get peer platform error"); + "Windows".to_string() + } +} + +pub fn legacy_keyboard_mode(event: &Event, key_event: &mut KeyEvent) { + // 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; + } + }; + + let peer = get_peer_platform(); + let is_win = peer == "Windows"; + if is_win { + key = convert_numpad_keys(key); + } + + let alt = get_key_state(enigo::Key::Alt); + #[cfg(windows)] + let ctrl = { + let mut tmp = get_key_state(enigo::Key::Control) || get_key_state(enigo::Key::RightControl); + unsafe { + if IS_ALT_GR { + if alt || key == Key::AltGr { + if tmp { + tmp = false; } + } else { + IS_ALT_GR = false; } } - tmp - }; - #[cfg(not(windows))] - let ctrl = get_key_state(enigo::Key::Control) || get_key_state(enigo::Key::RightControl); - let shift = get_key_state(enigo::Key::Shift) || get_key_state(enigo::Key::RightShift); - #[cfg(windows)] - let command = crate::platform::windows::get_win_key_state(); - #[cfg(not(windows))] - let command = get_key_state(enigo::Key::Meta); - let control_key = match key { - Key::Alt => Some(ControlKey::Alt), - Key::AltGr => Some(ControlKey::RAlt), - Key::Backspace => Some(ControlKey::Backspace), - Key::ControlLeft => { - // when pressing AltGr, an extra VK_LCONTROL with a special - // scancode with bit 9 set is sent, let's ignore this. - #[cfg(windows)] - if event.scan_code & 0x200 != 0 { - unsafe { - IS_ALT_GR = true; - } - return; + } + tmp + }; + #[cfg(not(windows))] + let ctrl = get_key_state(enigo::Key::Control) || get_key_state(enigo::Key::RightControl); + let shift = get_key_state(enigo::Key::Shift) || get_key_state(enigo::Key::RightShift); + #[cfg(windows)] + let command = crate::platform::windows::get_win_key_state(); + #[cfg(not(windows))] + let command = get_key_state(enigo::Key::Meta); + let control_key = match key { + Key::Alt => Some(ControlKey::Alt), + Key::AltGr => Some(ControlKey::RAlt), + Key::Backspace => Some(ControlKey::Backspace), + Key::ControlLeft => { + // when pressing AltGr, an extra VK_LCONTROL with a special + // scancode with bit 9 set is sent, let's ignore this. + #[cfg(windows)] + if event.scan_code & 0x200 != 0 { + unsafe { + IS_ALT_GR = true; } - Some(ControlKey::Control) - } - Key::ControlRight => Some(ControlKey::RControl), - Key::DownArrow => Some(ControlKey::DownArrow), - Key::Escape => Some(ControlKey::Escape), - Key::F1 => Some(ControlKey::F1), - Key::F10 => Some(ControlKey::F10), - Key::F11 => Some(ControlKey::F11), - Key::F12 => Some(ControlKey::F12), - Key::F2 => Some(ControlKey::F2), - Key::F3 => Some(ControlKey::F3), - Key::F4 => Some(ControlKey::F4), - Key::F5 => Some(ControlKey::F5), - Key::F6 => Some(ControlKey::F6), - Key::F7 => Some(ControlKey::F7), - Key::F8 => Some(ControlKey::F8), - Key::F9 => Some(ControlKey::F9), - Key::LeftArrow => Some(ControlKey::LeftArrow), - Key::MetaLeft => Some(ControlKey::Meta), - Key::MetaRight => Some(ControlKey::RWin), - Key::Return => Some(ControlKey::Return), - Key::RightArrow => Some(ControlKey::RightArrow), - Key::ShiftLeft => Some(ControlKey::Shift), - Key::ShiftRight => Some(ControlKey::RShift), - Key::Space => Some(ControlKey::Space), - Key::Tab => Some(ControlKey::Tab), - Key::UpArrow => Some(ControlKey::UpArrow), - Key::Delete => { - if is_win && ctrl && alt { - components::ctrl_alt_del(); - return; - } - Some(ControlKey::Delete) - } - Key::Apps => Some(ControlKey::Apps), - Key::Cancel => Some(ControlKey::Cancel), - Key::Clear => Some(ControlKey::Clear), - Key::Kana => Some(ControlKey::Kana), - Key::Hangul => Some(ControlKey::Hangul), - Key::Junja => Some(ControlKey::Junja), - Key::Final => Some(ControlKey::Final), - Key::Hanja => Some(ControlKey::Hanja), - Key::Hanji => Some(ControlKey::Hanja), - Key::Convert => Some(ControlKey::Convert), - Key::Print => Some(ControlKey::Print), - Key::Select => Some(ControlKey::Select), - Key::Execute => Some(ControlKey::Execute), - Key::PrintScreen => Some(ControlKey::Snapshot), - Key::Help => Some(ControlKey::Help), - Key::Sleep => Some(ControlKey::Sleep), - Key::Separator => Some(ControlKey::Separator), - Key::KpReturn => Some(ControlKey::NumpadEnter), - Key::Kp0 => Some(ControlKey::Numpad0), - Key::Kp1 => Some(ControlKey::Numpad1), - Key::Kp2 => Some(ControlKey::Numpad2), - Key::Kp3 => Some(ControlKey::Numpad3), - Key::Kp4 => Some(ControlKey::Numpad4), - Key::Kp5 => Some(ControlKey::Numpad5), - Key::Kp6 => Some(ControlKey::Numpad6), - Key::Kp7 => Some(ControlKey::Numpad7), - Key::Kp8 => Some(ControlKey::Numpad8), - Key::Kp9 => Some(ControlKey::Numpad9), - Key::KpDivide => Some(ControlKey::Divide), - Key::KpMultiply => Some(ControlKey::Multiply), - Key::KpDecimal => Some(ControlKey::Decimal), - Key::KpMinus => Some(ControlKey::Subtract), - Key::KpPlus => Some(ControlKey::Add), - Key::CapsLock | Key::NumLock | Key::ScrollLock => { return; } - Key::Home => Some(ControlKey::Home), - Key::End => Some(ControlKey::End), - Key::Insert => Some(ControlKey::Insert), - Key::PageUp => Some(ControlKey::PageUp), - Key::PageDown => Some(ControlKey::PageDown), - Key::Pause => Some(ControlKey::Pause), - _ => None, - }; - if let Some(k) = control_key { - key_event.set_control_key(k); - } else { - let mut chr = match event.name { - Some(ref s) => { - if s.len() <= 2 { - // exclude chinese characters - s.chars().next().unwrap_or('\0') - } else { - '\0' - } + Some(ControlKey::Control) + } + Key::ControlRight => Some(ControlKey::RControl), + Key::DownArrow => Some(ControlKey::DownArrow), + Key::Escape => Some(ControlKey::Escape), + Key::F1 => Some(ControlKey::F1), + Key::F10 => Some(ControlKey::F10), + Key::F11 => Some(ControlKey::F11), + Key::F12 => Some(ControlKey::F12), + Key::F2 => Some(ControlKey::F2), + Key::F3 => Some(ControlKey::F3), + Key::F4 => Some(ControlKey::F4), + Key::F5 => Some(ControlKey::F5), + Key::F6 => Some(ControlKey::F6), + Key::F7 => Some(ControlKey::F7), + Key::F8 => Some(ControlKey::F8), + Key::F9 => Some(ControlKey::F9), + Key::LeftArrow => Some(ControlKey::LeftArrow), + Key::MetaLeft => Some(ControlKey::Meta), + Key::MetaRight => Some(ControlKey::RWin), + Key::Return => Some(ControlKey::Return), + Key::RightArrow => Some(ControlKey::RightArrow), + Key::ShiftLeft => Some(ControlKey::Shift), + Key::ShiftRight => Some(ControlKey::RShift), + Key::Space => Some(ControlKey::Space), + Key::Tab => Some(ControlKey::Tab), + Key::UpArrow => Some(ControlKey::UpArrow), + Key::Delete => { + if is_win && ctrl && alt { + client::ctrl_alt_del(); + return; + } + Some(ControlKey::Delete) + } + Key::Apps => Some(ControlKey::Apps), + Key::Cancel => Some(ControlKey::Cancel), + Key::Clear => Some(ControlKey::Clear), + Key::Kana => Some(ControlKey::Kana), + Key::Hangul => Some(ControlKey::Hangul), + Key::Junja => Some(ControlKey::Junja), + Key::Final => Some(ControlKey::Final), + Key::Hanja => Some(ControlKey::Hanja), + Key::Hanji => Some(ControlKey::Hanja), + Key::Convert => Some(ControlKey::Convert), + Key::Print => Some(ControlKey::Print), + Key::Select => Some(ControlKey::Select), + Key::Execute => Some(ControlKey::Execute), + Key::PrintScreen => Some(ControlKey::Snapshot), + Key::Help => Some(ControlKey::Help), + Key::Sleep => Some(ControlKey::Sleep), + Key::Separator => Some(ControlKey::Separator), + Key::KpReturn => Some(ControlKey::NumpadEnter), + Key::Kp0 => Some(ControlKey::Numpad0), + Key::Kp1 => Some(ControlKey::Numpad1), + Key::Kp2 => Some(ControlKey::Numpad2), + Key::Kp3 => Some(ControlKey::Numpad3), + Key::Kp4 => Some(ControlKey::Numpad4), + Key::Kp5 => Some(ControlKey::Numpad5), + Key::Kp6 => Some(ControlKey::Numpad6), + Key::Kp7 => Some(ControlKey::Numpad7), + Key::Kp8 => Some(ControlKey::Numpad8), + Key::Kp9 => Some(ControlKey::Numpad9), + Key::KpDivide => Some(ControlKey::Divide), + Key::KpMultiply => Some(ControlKey::Multiply), + Key::KpDecimal => Some(ControlKey::Decimal), + Key::KpMinus => Some(ControlKey::Subtract), + Key::KpPlus => Some(ControlKey::Add), + Key::CapsLock | Key::NumLock | Key::ScrollLock => { + return; + } + Key::Home => Some(ControlKey::Home), + Key::End => Some(ControlKey::End), + Key::Insert => Some(ControlKey::Insert), + Key::PageUp => Some(ControlKey::PageUp), + Key::PageDown => Some(ControlKey::PageDown), + Key::Pause => Some(ControlKey::Pause), + _ => None, + }; + if let Some(k) = control_key { + key_event.set_control_key(k); + } else { + let mut chr = match event.name { + Some(ref s) => { + if s.len() <= 2 { + // exclude chinese characters + s.chars().next().unwrap_or('\0') + } else { + '\0' } + } + _ => '\0', + }; + if chr == '·' { + // special for Chinese + chr = '`'; + } + if chr == '\0' { + chr = match key { + Key::Num1 => '1', + Key::Num2 => '2', + Key::Num3 => '3', + Key::Num4 => '4', + Key::Num5 => '5', + Key::Num6 => '6', + Key::Num7 => '7', + Key::Num8 => '8', + Key::Num9 => '9', + Key::Num0 => '0', + Key::KeyA => 'a', + Key::KeyB => 'b', + Key::KeyC => 'c', + Key::KeyD => 'd', + Key::KeyE => 'e', + Key::KeyF => 'f', + Key::KeyG => 'g', + Key::KeyH => 'h', + Key::KeyI => 'i', + Key::KeyJ => 'j', + Key::KeyK => 'k', + Key::KeyL => 'l', + Key::KeyM => 'm', + Key::KeyN => 'n', + Key::KeyO => 'o', + Key::KeyP => 'p', + Key::KeyQ => 'q', + Key::KeyR => 'r', + Key::KeyS => 's', + Key::KeyT => 't', + Key::KeyU => 'u', + Key::KeyV => 'v', + Key::KeyW => 'w', + Key::KeyX => 'x', + Key::KeyY => 'y', + Key::KeyZ => 'z', + Key::Comma => ',', + Key::Dot => '.', + Key::SemiColon => ';', + Key::Quote => '\'', + Key::LeftBracket => '[', + Key::RightBracket => ']', + Key::Slash => '/', + Key::BackSlash => '\\', + Key::Minus => '-', + Key::Equal => '=', + Key::BackQuote => '`', _ => '\0', - }; - if chr == '·' { - // special for Chinese - chr = '`'; } - if chr == '\0' { - chr = match key { - Key::Num1 => '1', - Key::Num2 => '2', - Key::Num3 => '3', - Key::Num4 => '4', - Key::Num5 => '5', - Key::Num6 => '6', - Key::Num7 => '7', - Key::Num8 => '8', - Key::Num9 => '9', - Key::Num0 => '0', - Key::KeyA => 'a', - Key::KeyB => 'b', - Key::KeyC => 'c', - Key::KeyD => 'd', - Key::KeyE => 'e', - Key::KeyF => 'f', - Key::KeyG => 'g', - Key::KeyH => 'h', - Key::KeyI => 'i', - Key::KeyJ => 'j', - Key::KeyK => 'k', - Key::KeyL => 'l', - Key::KeyM => 'm', - Key::KeyN => 'n', - Key::KeyO => 'o', - Key::KeyP => 'p', - Key::KeyQ => 'q', - Key::KeyR => 'r', - Key::KeyS => 's', - Key::KeyT => 't', - Key::KeyU => 'u', - Key::KeyV => 'v', - Key::KeyW => 'w', - Key::KeyX => 'x', - Key::KeyY => 'y', - Key::KeyZ => 'z', - Key::Comma => ',', - Key::Dot => '.', - Key::SemiColon => ';', - Key::Quote => '\'', - Key::LeftBracket => '[', - Key::RightBracket => ']', - Key::Slash => '/', - Key::BackSlash => '\\', - Key::Minus => '-', - Key::Equal => '=', - Key::BackQuote => '`', - _ => '\0', - } - } - if chr != '\0' { - if chr == 'l' && is_win && command { - components::lock_screen(); - return; - } - key_event.set_chr(chr as _); - } else { - log::error!("Unknown key {:?}", &event); + } + if chr != '\0' { + if chr == 'l' && is_win && command { + client::lock_screen(); return; } + key_event.set_chr(chr as _); + } else { + log::error!("Unknown key {:?}", &event); + return; } - let (alt, ctrl, shift, command) = - components::get_modifiers_state(alt, ctrl, shift, command); - components::legacy_modifiers(key_event, alt, ctrl, shift, command); + } + let (alt, ctrl, shift, command) = get_modifiers_state(alt, ctrl, shift, command); + client::legacy_modifiers(key_event, alt, ctrl, shift, command); - if down_or_up == true { + if down_or_up == true { + key_event.down = true; + } +} + +pub fn map_keyboard_mode(event: &Event, key_event: &mut KeyEvent) { + let peer = get_peer_platform(); + + let key = match event.event_type { + EventType::KeyPress(key) => { key_event.down = true; + key } - } - - pub fn map_keyboard_mode(event: &Event, key_event: &mut KeyEvent) { - let peer = components::get_peer_platform(); - - let key = match event.event_type { - EventType::KeyPress(key) => { - key_event.down = true; - key - } - EventType::KeyRelease(key) => { - key_event.down = false; - key - } - _ => return, - }; - let keycode: u32 = match peer.as_str() { - "Windows" => rdev::win_keycode_from_key(key).unwrap_or_default().into(), - "MacOS" => rdev::macos_keycode_from_key(key).unwrap_or_default().into(), - _ => rdev::linux_keycode_from_key(key).unwrap_or_default().into(), - }; - key_event.set_chr(keycode); - } - - pub fn translate_keyboard_mode(event: &Event, key_event: &mut KeyEvent) {} + EventType::KeyRelease(key) => { + key_event.down = false; + key + } + _ => return, + }; + let keycode: u32 = match peer.as_str() { + "Windows" => rdev::win_keycode_from_key(key).unwrap_or_default().into(), + "MacOS" => rdev::macos_keycode_from_key(key).unwrap_or_default().into(), + _ => rdev::linux_keycode_from_key(key).unwrap_or_default().into(), + }; + key_event.set_chr(keycode); } -pub mod server_keyboard_mode { - use super::*; - - pub fn legacy_keyboard_mode(key_event: &KeyEvent) {} - - pub fn map_keyboard_mode(key_event: &KeyEvent) {} - - pub fn translate_keyboard_mode(key_event: &KeyEvent) {} -} +pub fn translate_keyboard_mode(event: &Event, key_event: &mut KeyEvent) {} diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 8dc9b51d0..664fb9c7e 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -716,22 +716,3 @@ pub fn get_double_click_time() -> u32 { } } -pub mod keyboard { - use crate::common::GrabState; - use hbb_common::{allow_err, log, message_proto::*}; - use rdev::{Event, EventType, Key}; - use std::sync::mpsc; - use std::thread; - - - - pub fn _legacy_keyboard_mode(event: &Event, key_event: &KeyEvent) { - log::info!("{:?}", event); - } - - pub fn _client_map_keyboard_mode(event: &Event, key_event: &KeyEvent) { - - } - - pub fn _translate_keyboard_mode(event: &Event, key_event: &KeyEvent) {} -} diff --git a/src/platform/macos.rs b/src/platform/macos.rs index c8daeeffa..edb2aadb1 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -542,7 +542,3 @@ pub fn get_double_click_time() -> u32 { // to-do: https://github.com/servo/core-foundation-rs/blob/786895643140fa0ee4f913d7b4aeb0c4626b2085/cocoa/src/appkit.rs#L2823 500 as _ } - -pub mod keyboard{ - -} \ No newline at end of file diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 7d3cbec2d..075f7ed08 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -1712,17 +1712,3 @@ pub fn send_message_to_hnwd( } return true; } - -pub mod keyboard { - use crate::common::GrabState; - use hbb_common::{allow_err, log, message_proto::*}; - use rdev::{Event, EventType, Key}; - use std::sync::mpsc; - - - use crate::keyboard; - - - - -} diff --git a/src/server/input_service.rs b/src/server/input_service.rs index b465658bb..187afc5a5 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -1002,9 +1002,6 @@ pub fn handle_key_(evt: &KeyEvent) { } match evt.mode.unwrap() { - KeyboardMode::Legacy => { - legacy_keyboard_mode(evt); - } KeyboardMode::Map => { map_keyboard_mode(evt); } From a98174448f5a1382ed2dd7b1f11fb302efe59bdf Mon Sep 17 00:00:00 2001 From: Asura Date: Wed, 7 Dec 2022 19:47:59 -0800 Subject: [PATCH 1136/2015] refactor: sync status of caps and numlock --- src/server/input_service.rs | 59 ++++++++++++------------------------- 1 file changed, 19 insertions(+), 40 deletions(-) diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 187afc5a5..d9b37feab 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -6,6 +6,7 @@ use dispatch::Queue; use enigo::{Enigo, Key, KeyboardControllable, MouseButton, MouseControllable}; use hbb_common::{config::COMPRESS_LEVEL, get_time, protobuf::EnumOrUnknown}; use rdev::{simulate, EventType, Key as RdevKey}; +use std::time::Duration; use std::{ convert::TryFrom, ops::Sub, @@ -753,15 +754,14 @@ fn rdev_key_down_or_up(key: RdevKey, down_or_up: bool) { true => EventType::KeyPress(key), false => EventType::KeyRelease(key), }; - let delay = std::time::Duration::from_millis(20); match simulate(&event_type) { Ok(()) => (), Err(_simulate_error) => { log::error!("Could not send {:?}", &event_type); } } - // Let ths OS catchup (at least MacOS) - std::thread::sleep(delay); + #[cfg(target_os = "macos")] + std::thread::sleep(Duration::from_millis(20)); } fn rdev_key_click(key: RdevKey) { @@ -769,8 +769,7 @@ fn rdev_key_click(key: RdevKey) { rdev_key_down_or_up(key, false); } -fn sync_status(evt: &KeyEvent) -> (bool, bool) { - /* todo! Shift+delete */ +fn sync_status(evt: &KeyEvent) { let mut en = ENIGO.lock().unwrap(); // remote caps status @@ -808,7 +807,18 @@ fn sync_status(evt: &KeyEvent) -> (bool, bool) { _ => click_numlock, } }; - return (click_capslock, click_numlock); + + if click_capslock { + #[cfg(not(target_os = "macos"))] + en.key_click(enigo::Key::CapsLock); + #[cfg(target_os = "macos")] + en.key_down(enigo::Key::CapsLock); + } + + if click_numlock { + #[cfg(not(target_os = "macos"))] + en.key_click(enigo::Key::NumLock); + } } fn map_keyboard_mode(evt: &KeyEvent) { @@ -816,25 +826,12 @@ fn map_keyboard_mode(evt: &KeyEvent) { #[cfg(windows)] crate::platform::windows::try_change_desktop(); - let (click_capslock, click_numlock) = sync_status(evt); - // Wayland #[cfg(target_os = "linux")] if !*IS_X11.lock().unwrap() { let mut en = ENIGO.lock().unwrap(); let code = evt.chr() as u16; - #[cfg(not(target_os = "macos"))] - if click_capslock { - en.key_click(enigo::Key::CapsLock); - } - #[cfg(not(target_os = "macos"))] - if click_numlock { - en.key_click(enigo::Key::NumLock); - } - #[cfg(target_os = "macos")] - en.key_down(enigo::Key::CapsLock); - if evt.down { en.key_down(enigo::Key::Raw(code)).ok(); } else { @@ -843,35 +840,14 @@ fn map_keyboard_mode(evt: &KeyEvent) { return; } - #[cfg(not(target_os = "macos"))] - if click_capslock { - rdev_key_click(RdevKey::CapsLock); - } - #[cfg(not(target_os = "macos"))] - if click_numlock { - rdev_key_click(RdevKey::NumLock); - } - #[cfg(target_os = "macos")] - if evt.down && click_capslock { - rdev_key_down_or_up(RdevKey::CapsLock, evt.down); - } - rdev_key_down_or_up(RdevKey::Unknown(evt.chr()), evt.down); return; } fn legacy_keyboard_mode(evt: &KeyEvent) { - let (click_capslock, click_numlock) = sync_status(evt); - #[cfg(windows)] crate::platform::windows::try_change_desktop(); let mut en = ENIGO.lock().unwrap(); - if click_capslock { - en.key_click(Key::CapsLock); - } - if click_numlock { - en.key_click(Key::NumLock); - } // disable numlock if press home etc when numlock is on, // because we will get numpad value (7,8,9 etc) if not #[cfg(windows)] @@ -1001,6 +977,9 @@ pub fn handle_key_(evt: &KeyEvent) { return; } + if evt.down { + sync_status(evt) + } match evt.mode.unwrap() { KeyboardMode::Map => { map_keyboard_mode(evt); From 95775678ca2d7a5254a0e14b67a0e35353b4a5da Mon Sep 17 00:00:00 2001 From: Asura Date: Wed, 7 Dec 2022 20:32:34 -0800 Subject: [PATCH 1137/2015] refactor: get modifier state --- Cargo.lock | 24 ++---------------------- Cargo.toml | 2 +- src/keyboard.rs | 45 ++++++++++++++++++--------------------------- 3 files changed, 21 insertions(+), 50 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5708905db..529be08d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1533,7 +1533,7 @@ dependencies = [ "log", "objc", "pkg-config", - "rdev 0.5.0-2 (git+https://github.com/asur4s/rdev)", + "rdev", "serde 1.0.147", "serde_derive", "tfc", @@ -4226,26 +4226,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", - "strum 0.24.1", - "strum_macros 0.24.3", - "widestring 1.0.2", - "winapi 0.3.9", - "x11 2.20.0", -] - [[package]] name = "rdev" version = "0.5.0-2" @@ -4538,7 +4518,7 @@ dependencies = [ "num_cpus", "objc", "parity-tokio-ipc", - "rdev 0.5.0-2", + "rdev", "repng", "reqwest", "rpassword 7.1.0", diff --git a/Cargo.toml b/Cargo.toml index afd617f78..2861b3f63 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,7 +63,7 @@ default-net = "0.11.0" wol-rs = "0.9.1" flutter_rust_bridge = { git = "https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge", optional = true } errno = "0.2.8" -rdev = { path = "../rdev" } +rdev = { git = "https://github.com/asur4s/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 07bcecf76..08cc5760a 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -93,7 +93,20 @@ pub mod client { shift: bool, command: bool, ) -> (bool, bool, bool, bool) { - get_modifiers_state(alt, ctrl, shift, command) + let modifiers_lock = MODIFIERS_STATE.lock().unwrap(); + let ctrl = *modifiers_lock.get(&Key::ControlLeft).unwrap() + || *modifiers_lock.get(&Key::ControlRight).unwrap() + || ctrl; + let shift = *modifiers_lock.get(&Key::ShiftLeft).unwrap() + || *modifiers_lock.get(&Key::ShiftRight).unwrap() + || shift; + let command = *modifiers_lock.get(&Key::MetaLeft).unwrap() + || *modifiers_lock.get(&Key::MetaRight).unwrap() + || command; + let alt = + *modifiers_lock.get(&Key::Alt).unwrap() || *modifiers_lock.get(&Key::AltGr).unwrap() || alt; + + (alt, ctrl, shift, command) } pub fn legacy_modifiers( @@ -166,7 +179,7 @@ pub fn grab_loop(recv: mpsc::Receiver) { return Some(event); } if KEYBOARD_HOOKED.load(Ordering::SeqCst) { - keyboard::client::process_event(event); + client::process_event(event); return None; } else { return Some(event); @@ -185,7 +198,7 @@ pub fn grab_loop(recv: mpsc::Receiver) { if let Key::Unknown(keycode) = key { log::error!("rdev get unknown key, keycode is : {:?}", keycode); } else { - crate::keyboard::client::process_event(event); + client::process_event(event); } None } @@ -243,7 +256,7 @@ pub fn release_remote_keys() { } } -pub fn get_keyboard_mode_enum() -> KeyboardMode { +pub fn get_keyboard_mode_enum() -> KeyboardMode { match client::get_keyboard_mode().as_str() { "map" => KeyboardMode::Map, "translate" => KeyboardMode::Translate, @@ -280,28 +293,6 @@ pub fn convert_numpad_keys(key: Key) -> Key { } } -pub fn get_modifiers_state( - alt: bool, - ctrl: bool, - shift: bool, - command: bool, -) -> (bool, bool, bool, bool) { - let modifiers_lock = MODIFIERS_STATE.lock().unwrap(); - let ctrl = *modifiers_lock.get(&Key::ControlLeft).unwrap() - || *modifiers_lock.get(&Key::ControlRight).unwrap() - || ctrl; - let shift = *modifiers_lock.get(&Key::ShiftLeft).unwrap() - || *modifiers_lock.get(&Key::ShiftRight).unwrap() - || shift; - let command = *modifiers_lock.get(&Key::MetaLeft).unwrap() - || *modifiers_lock.get(&Key::MetaRight).unwrap() - || command; - let alt = - *modifiers_lock.get(&Key::Alt).unwrap() || *modifiers_lock.get(&Key::AltGr).unwrap() || alt; - - (alt, ctrl, shift, command) -} - fn update_modifiers_state(event: &Event) { // for mouse let mut keys = MODIFIERS_STATE.lock().unwrap(); @@ -595,7 +586,7 @@ pub fn legacy_keyboard_mode(event: &Event, key_event: &mut KeyEvent) { return; } } - let (alt, ctrl, shift, command) = get_modifiers_state(alt, ctrl, shift, command); + let (alt, ctrl, shift, command) = client::get_modifiers_state(alt, ctrl, shift, command); client::legacy_modifiers(key_event, alt, ctrl, shift, command); if down_or_up == true { From 2136931f8097f906832061c37308243a48915bb3 Mon Sep 17 00:00:00 2001 From: 21pages Date: Wed, 7 Dec 2022 16:30:44 +0800 Subject: [PATCH 1138/2015] upload record Signed-off-by: 21pages --- libs/scrap/src/common/record.rs | 33 +++++- src/client.rs | 2 + src/core_main.rs | 2 - src/hbbs_http.rs | 1 + src/hbbs_http/record_upload.rs | 204 ++++++++++++++++++++++++++++++++ src/server/portable_service.rs | 6 +- src/server/video_service.rs | 64 +++++++--- src/ui_interface.rs | 1 + 8 files changed, 287 insertions(+), 26 deletions(-) create mode 100644 src/hbbs_http/record_upload.rs diff --git a/libs/scrap/src/common/record.rs b/libs/scrap/src/common/record.rs index 83bd9eee7..9f38f2d6a 100644 --- a/libs/scrap/src/common/record.rs +++ b/libs/scrap/src/common/record.rs @@ -12,11 +12,10 @@ use hwcodec::mux::{MuxContext, Muxer}; use std::{ fs::{File, OpenOptions}, io, - time::Instant, -}; -use std::{ ops::{Deref, DerefMut}, path::PathBuf, + sync::mpsc::Sender, + time::Instant, }; use webm::mux::{self, Segment, Track, VideoTrack, Writer}; @@ -31,12 +30,14 @@ pub enum RecordCodecID { #[derive(Debug, Clone)] pub struct RecorderContext { + pub server: bool, pub id: String, pub default_dir: String, pub filename: String, pub width: usize, pub height: usize, pub codec_id: RecordCodecID, + pub tx: Option>, } impl RecorderContext { @@ -52,7 +53,8 @@ impl RecorderContext { std::fs::create_dir_all(&dir)?; } } - let file = self.id.clone() + let file = if self.server { "s" } else { "c" }.to_string() + + &self.id.clone() + &chrono::Local::now().format("_%Y%m%d%H%M%S").to_string() + if self.codec_id == RecordCodecID::VP9 { ".webm" @@ -60,7 +62,7 @@ impl RecorderContext { ".mp4" }; self.filename = PathBuf::from(&dir).join(file).to_string_lossy().to_string(); - log::info!("video save to:{}", self.filename); + log::info!("video will save to:{}", self.filename); Ok(()) } } @@ -75,6 +77,14 @@ pub trait RecorderApi { fn write_video(&mut self, frame: &EncodedVideoFrame) -> bool; } +#[derive(Debug)] +pub enum RecordState { + NewFile(String), + NewFrame, + WriteTail, + RemoveFile, +} + pub struct Recorder { pub inner: Box, ctx: RecorderContext, @@ -110,6 +120,7 @@ impl Recorder { #[cfg(not(feature = "hwcodec"))] _ => bail!("unsupported codec type"), }; + recorder.send_state(RecordState::NewFile(recorder.ctx.filename.clone())); Ok(recorder) } @@ -123,6 +134,7 @@ impl Recorder { _ => bail!("unsupported codec type"), }; self.ctx = ctx; + self.send_state(RecordState::NewFile(self.ctx.filename.clone())); Ok(()) } @@ -171,8 +183,13 @@ impl Recorder { } _ => bail!("unsupported frame type"), } + self.send_state(RecordState::NewFrame); Ok(()) } + + fn send_state(&self, state: RecordState) { + self.ctx.tx.as_ref().map(|tx| tx.send(state)); + } } struct WebmRecorder { @@ -237,9 +254,12 @@ impl RecorderApi for WebmRecorder { impl Drop for WebmRecorder { fn drop(&mut self) { std::mem::replace(&mut self.webm, None).map_or(false, |webm| webm.finalize(None)); + let mut state = RecordState::WriteTail; if !self.written || self.start.elapsed().as_secs() < MIN_SECS { std::fs::remove_file(&self.ctx.filename).ok(); + state = RecordState::RemoveFile; } + self.ctx.tx.as_ref().map(|tx| tx.send(state)); } } @@ -292,8 +312,11 @@ impl RecorderApi for HwRecorder { impl Drop for HwRecorder { fn drop(&mut self) { self.muxer.write_tail().ok(); + let mut state = RecordState::WriteTail; if !self.written || self.start.elapsed().as_secs() < MIN_SECS { std::fs::remove_file(&self.ctx.filename).ok(); + state = RecordState::RemoveFile; } + self.ctx.tx.as_ref().map(|tx| tx.send(state)); } } diff --git a/src/client.rs b/src/client.rs index c646b2b7f..1dd3021b2 100644 --- a/src/client.rs +++ b/src/client.rs @@ -863,12 +863,14 @@ impl VideoHandler { self.record = false; if start { self.recorder = Recorder::new(RecorderContext { + server: false, id, default_dir: crate::ui_interface::default_video_save_directory(), filename: "".to_owned(), width: w as _, height: h as _, codec_id: scrap::record::RecordCodecID::VP9, + tx: None, }) .map_or(Default::default(), |r| Arc::new(Mutex::new(Some(r)))); } else { diff --git a/src/core_main.rs b/src/core_main.rs index d0ce9e0d1..c92de843d 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -216,8 +216,6 @@ pub fn core_main() -> Option> { if args.len() == 2 { if crate::platform::is_root() { crate::ipc::set_permanent_password(args[1].to_owned()).unwrap(); - } else { - log::info!("Permission denied!"); } } return None; diff --git a/src/hbbs_http.rs b/src/hbbs_http.rs index ceb3a6081..08ad36eb9 100644 --- a/src/hbbs_http.rs +++ b/src/hbbs_http.rs @@ -4,6 +4,7 @@ use serde_json::{Map, Value}; #[cfg(feature = "flutter")] pub mod account; +pub mod record_upload; #[derive(Debug)] pub enum HbbHttpResponse { diff --git a/src/hbbs_http/record_upload.rs b/src/hbbs_http/record_upload.rs new file mode 100644 index 000000000..93bc745c2 --- /dev/null +++ b/src/hbbs_http/record_upload.rs @@ -0,0 +1,204 @@ +use bytes::Bytes; +use hbb_common::{bail, config::Config, lazy_static, log, ResultType}; +use reqwest::blocking::{Body, Client}; +use scrap::record::RecordState; +use serde::Serialize; +use serde_json::Map; +use std::{ + fs::File, + io::{prelude::*, SeekFrom}, + sync::{mpsc::Receiver, Arc, Mutex}, + time::{Duration, Instant}, +}; + +const MAX_HEADER_LEN: usize = 1024; +const SHOULD_SEND_TIME: Duration = Duration::from_secs(1); +const SHOULD_SEND_SIZE: u64 = 1024 * 1024; + +lazy_static::lazy_static! { + static ref ENABLE: Arc> = Default::default(); +} + +pub fn is_enable() -> bool { + ENABLE.lock().unwrap().clone() +} + +pub fn run(rx: Receiver) { + let mut uploader = RecordUploader { + client: Client::new(), + api_server: crate::get_api_server( + Config::get_option("api-server"), + Config::get_option("custom-rendezvous-server"), + ), + filepath: Default::default(), + filename: Default::default(), + upload_size: Default::default(), + running: Default::default(), + last_send: Instant::now(), + }; + std::thread::spawn(move || loop { + if let Err(e) = match rx.recv() { + Ok(state) => match state { + RecordState::NewFile(filepath) => uploader.handle_new_file(filepath), + RecordState::NewFrame => { + if uploader.running { + uploader.handle_frame(false) + } else { + Ok(()) + } + } + RecordState::WriteTail => { + if uploader.running { + uploader.handle_tail() + } else { + Ok(()) + } + } + RecordState::RemoveFile => { + if uploader.running { + uploader.handle_remove() + } else { + Ok(()) + } + } + }, + Err(e) => { + log::trace!("upload thread stop:{}", e); + break; + } + } { + uploader.running = false; + log::error!("upload stop:{}", e); + } + }); +} + +struct RecordUploader { + client: Client, + api_server: String, + filepath: String, + filename: String, + upload_size: u64, + running: bool, + last_send: Instant, +} +impl RecordUploader { + fn send(&self, query: &Q, body: B) -> ResultType<()> + where + Q: Serialize + ?Sized, + B: Into, + { + match self + .client + .post(format!("{}/api/record", self.api_server)) + .query(query) + .body(body) + .send() + { + Ok(resp) => { + if let Ok(m) = resp.json::>() { + if let Some(e) = m.get("error") { + bail!(e.to_string()); + } + } + Ok(()) + } + Err(e) => bail!(e.to_string()), + } + } + + fn handle_new_file(&mut self, filepath: String) -> ResultType<()> { + match std::path::PathBuf::from(&filepath).file_name() { + Some(filename) => match filename.to_owned().into_string() { + Ok(filename) => { + self.filename = filename.clone(); + self.filepath = filepath.clone(); + self.upload_size = 0; + self.running = true; + self.last_send = Instant::now(); + self.send(&[("type", "new"), ("file", &filename)], Bytes::new())?; + Ok(()) + } + Err(_) => bail!("can't parse filename:{:?}", filename), + }, + None => bail!("can't parse filepath:{}", filepath), + } + } + + fn handle_frame(&mut self, flush: bool) -> ResultType<()> { + if !flush && self.last_send.elapsed() < SHOULD_SEND_TIME { + return Ok(()); + } + match File::open(&self.filepath) { + Ok(mut file) => match file.metadata() { + Ok(m) => { + let len = m.len(); + if len <= self.upload_size { + return Ok(()); + } + if !flush && len - self.upload_size < SHOULD_SEND_SIZE { + return Ok(()); + } + let mut buf = Vec::new(); + match file.seek(SeekFrom::Start(self.upload_size)) { + Ok(_) => match file.read_to_end(&mut buf) { + Ok(length) => { + self.send( + &[ + ("type", "part"), + ("file", &self.filename), + ("offset", &self.upload_size.to_string()), + ("length", &length.to_string()), + ], + buf, + )?; + self.upload_size = len; + self.last_send = Instant::now(); + Ok(()) + } + Err(e) => bail!(e.to_string()), + }, + Err(e) => bail!(e.to_string()), + } + } + Err(e) => bail!(e.to_string()), + }, + Err(e) => bail!(e.to_string()), + } + } + + fn handle_tail(&mut self) -> ResultType<()> { + self.handle_frame(true)?; + match File::open(&self.filepath) { + Ok(mut file) => { + let mut buf = vec![0u8; MAX_HEADER_LEN]; + match file.read(&mut buf) { + Ok(length) => { + buf.truncate(length); + self.send( + &[ + ("type", "tail"), + ("file", &self.filename), + ("offset", "0"), + ("length", &length.to_string()), + ], + buf, + )?; + log::info!("upload success, file:{}", self.filename); + Ok(()) + } + Err(e) => bail!(e.to_string()), + } + } + Err(e) => bail!(e.to_string()), + } + } + + fn handle_remove(&mut self) -> ResultType<()> { + self.send( + &[("type", "remove"), ("file", &self.filename)], + Bytes::new(), + )?; + Ok(()) + } +} diff --git a/src/server/portable_service.rs b/src/server/portable_service.rs index ace70e1bd..6d2e92ae3 100644 --- a/src/server/portable_service.rs +++ b/src/server/portable_service.rs @@ -44,7 +44,7 @@ const ADDR_CAPTURE_FRAME_COUNTER: usize = ADDR_CAPTURE_WOULDBLOCK + size_of:: { @@ -622,7 +622,7 @@ pub mod client { async fn start_ipc_server_async(rx: mpsc::UnboundedReceiver) { use DataPortableService::*; let rx = Arc::new(tokio::sync::Mutex::new(rx)); - let postfix = IPC_PROFIX; + let postfix = IPC_SUFFIX; #[cfg(feature = "flutter")] let quick_support = { let args: Vec<_> = std::env::args().collect(); diff --git a/src/server/video_service.rs b/src/server/video_service.rs index 28b73cf7c..686e28f35 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -481,22 +481,7 @@ fn run(sp: GenericService) -> ResultType<()> { #[cfg(windows)] log::info!("gdi: {}", c.is_gdi()); let codec_name = Encoder::current_hw_encoder_name(); - #[cfg(not(target_os = "ios"))] - let recorder = if !Config::get_option("allow-auto-record-incoming").is_empty() { - Recorder::new(RecorderContext { - id: "local".to_owned(), - default_dir: crate::ui_interface::default_video_save_directory(), - filename: "".to_owned(), - width: c.width, - height: c.height, - codec_id: scrap::record::RecordCodecID::VP9, - }) - .map_or(Default::default(), |r| Arc::new(Mutex::new(Some(r)))) - } else { - Default::default() - }; - #[cfg(target_os = "ios")] - let recorder: Arc>> = Default::default(); + let recorder = get_recorder(c.width, c.height, &codec_name); #[cfg(windows)] start_uac_elevation_check(); @@ -673,6 +658,53 @@ fn run(sp: GenericService) -> ResultType<()> { Ok(()) } +fn get_recorder( + width: usize, + height: usize, + codec_name: &Option, +) -> Arc>> { + #[cfg(not(target_os = "ios"))] + let recorder = if !Config::get_option("allow-auto-record-incoming").is_empty() { + use crate::hbbs_http::record_upload; + use scrap::record::RecordCodecID::*; + + let tx = if record_upload::is_enable() { + let (tx, rx) = std::sync::mpsc::channel(); + record_upload::run(rx); + Some(tx) + } else { + None + }; + let codec_id = match codec_name { + Some(name) => { + if name.contains("264") { + H264 + } else { + H265 + } + } + None => VP9, + }; + Recorder::new(RecorderContext { + server: true, + id: Config::get_id(), + default_dir: crate::ui_interface::default_video_save_directory(), + filename: "".to_owned(), + width, + height, + codec_id, + tx, + }) + .map_or(Default::default(), |r| Arc::new(Mutex::new(Some(r)))) + } else { + Default::default() + }; + #[cfg(target_os = "ios")] + let recorder: Arc>> = Default::default(); + + recorder +} + fn check_privacy_mode_changed(sp: &GenericService, privacy_mode_id: i32) -> ResultType<()> { let privacy_mode_id_2 = *PRIVACY_MODE_CONN_ID.lock().unwrap(); if privacy_mode_id != privacy_mode_id_2 { diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 59082d00d..604d2e222 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -685,6 +685,7 @@ pub fn discover() { }); } +#[cfg(feature = "flutter")] pub fn peer_to_map(id: String, p: PeerConfig) -> HashMap<&'static str, String> { HashMap::<&str, String>::from_iter([ ("id", id), From 9e75019d13498ddad80decb476c870e9190a4061 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 8 Dec 2022 13:42:57 +0800 Subject: [PATCH 1139/2015] fix one log --- src/core_main.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/core_main.rs b/src/core_main.rs index c92de843d..342f438ee 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -216,6 +216,8 @@ pub fn core_main() -> Option> { if args.len() == 2 { if crate::platform::is_root() { crate::ipc::set_permanent_password(args[1].to_owned()).unwrap(); + } else { + println!("Administrative privileges required!"); } } return None; From 162f29c80d10e82ae4623cfe39ef625c078b5a20 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Thu, 8 Dec 2022 17:08:31 +0800 Subject: [PATCH 1140/2015] fix: get display server in flatpak --- libs/hbb_common/src/platform/linux.rs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/libs/hbb_common/src/platform/linux.rs b/libs/hbb_common/src/platform/linux.rs index 1a20ed0e1..8c3f2c85c 100644 --- a/libs/hbb_common/src/platform/linux.rs +++ b/libs/hbb_common/src/platform/linux.rs @@ -43,7 +43,8 @@ pub fn get_display_server() -> String { } fn get_display_server_of_session(session: &str) -> String { - if let Ok(output) = run_loginctl(Some(vec!["show-session", "-p", "Type", session])) + let mut display_server = if let Ok(output) = + run_loginctl(Some(vec!["show-session", "-p", "Type", session])) // Check session type of the session { let display_server = String::from_utf8_lossy(&output.stdout) @@ -76,16 +77,17 @@ fn get_display_server_of_session(session: &str) -> String { display_server } } else { - // loginctl has not given the expected output. try something else. - if let Ok(sestype) = std::env::var("XDG_SESSION_TYPE") { - return sestype.to_owned(); - } - // If the session is not a tty, then just return the type as usual - display_server + "".to_owned() } } else { "".to_owned() + }; + // loginctl has not given the expected output. try something else. + if let Ok(sestype) = std::env::var("XDG_SESSION_TYPE") { + display_server = sestype; } + // If the session is not a tty, then just return the type as usual + display_server } pub fn get_values_of_seat0(indices: Vec) -> Vec { @@ -126,8 +128,7 @@ pub fn get_values_of_seat0(indices: Vec) -> Vec { } fn is_active(sid: &str) -> bool { - if let Ok(output) = run_loginctl(Some(vec!["show-session", "-p", "State", sid])) - { + if let Ok(output) = run_loginctl(Some(vec!["show-session", "-p", "State", sid])) { String::from_utf8_lossy(&output.stdout).contains("active") } else { false From 7dadf3ba2f934435b7de4f4f08803bf542db0951 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Thu, 8 Dec 2022 17:41:14 +0800 Subject: [PATCH 1141/2015] opt: remove redundant restore job --- flutter/lib/models/file_model.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index 744a3a502..a7968f701 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -363,8 +363,6 @@ class FileModel extends ChangeNotifier { if (_currentRemoteDir.path.isEmpty) { openDirectory(_remoteOption.home, isLocal: false); } - // load last transfer jobs - await bind.sessionLoadLastTransferJobs(id: '${parent.target?.id}'); } Future onClose() async { From 3cfcaf65ad976d896698e7e1973e1f9e7c36016d Mon Sep 17 00:00:00 2001 From: Kingtous Date: Thu, 8 Dec 2022 17:43:46 +0800 Subject: [PATCH 1142/2015] opt: add display server check --- libs/hbb_common/src/platform/linux.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/libs/hbb_common/src/platform/linux.rs b/libs/hbb_common/src/platform/linux.rs index 8c3f2c85c..e50823438 100644 --- a/libs/hbb_common/src/platform/linux.rs +++ b/libs/hbb_common/src/platform/linux.rs @@ -82,9 +82,11 @@ fn get_display_server_of_session(session: &str) -> String { } else { "".to_owned() }; - // loginctl has not given the expected output. try something else. - if let Ok(sestype) = std::env::var("XDG_SESSION_TYPE") { - display_server = sestype; + if display_server.is_empty() { + // loginctl has not given the expected output. try something else. + if let Ok(sestype) = std::env::var("XDG_SESSION_TYPE") { + display_server = sestype; + } } // If the session is not a tty, then just return the type as usual display_server From fa3618f4063b1c8fa80680ee202d24e1a2548f4f Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Thu, 8 Dec 2022 19:59:37 +0800 Subject: [PATCH 1143/2015] Update Cargo.toml --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2861b3f63..a783b1abe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,7 +63,7 @@ default-net = "0.11.0" wol-rs = "0.9.1" flutter_rust_bridge = { git = "https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge", optional = true } errno = "0.2.8" -rdev = { git = "https://github.com/asur4s/rdev" } +rdev = { git = "https://github.com/rustdesk/rdev" } url = { version = "2.1", features = ["serde"] } reqwest = { version = "0.11", features = ["blocking", "json", "rustls-tls"], default-features=false } @@ -162,4 +162,4 @@ codegen-units = 1 panic = 'abort' strip = true #opt-level = 'z' # only have smaller size after strip -rpath = true \ No newline at end of file +rpath = true From 22d071d2ef94fda47ea90527054397ef239aa050 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 8 Dec 2022 20:08:33 +0800 Subject: [PATCH 1144/2015] change to rustdesk/rdev and remove warn --- Cargo.lock | 26 ++++++++++++++++++++++++-- src/client/io_loop.rs | 2 +- src/server/video_service.rs | 3 ++- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bf3ce1f04..237369d2c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1533,7 +1533,7 @@ dependencies = [ "log", "objc", "pkg-config", - "rdev", + "rdev 0.5.0-2 (git+https://github.com/asur4s/rdev)", "serde 1.0.147", "serde_derive", "tfc", @@ -4248,6 +4248,28 @@ dependencies = [ "x11 2.20.0", ] +[[package]] +name = "rdev" +version = "0.5.0-2" +source = "git+https://github.com/rustdesk/rdev#25c29f61bfdf5d8ec50f0a8a7743bc1d85eb2c04" +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", + "mio 0.8.5", + "strum 0.24.1", + "strum_macros 0.24.3", + "widestring 1.0.2", + "winapi 0.3.9", + "x11 2.20.0", +] + [[package]] name = "rdrand" version = "0.4.0" @@ -4518,7 +4540,7 @@ dependencies = [ "num_cpus", "objc", "parity-tokio-ipc", - "rdev", + "rdev 0.5.0-2 (git+https://github.com/rustdesk/rdev)", "repng", "reqwest", "rpassword 7.1.0", diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index efeacb61c..326857d3f 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -936,7 +936,7 @@ impl Remote { self.handle_job_status(d.id, d.file_num, err); } Some(file_response::Union::Error(e)) => { - if let Some(job) = fs::get_job(e.id, &mut self.write_jobs) { + if let Some(_job) = fs::get_job(e.id, &mut self.write_jobs) { fs::remove_job(e.id, &mut self.write_jobs); } self.handle_job_status(e.id, e.file_num, Some(e.error)); diff --git a/src/server/video_service.rs b/src/server/video_service.rs index 686e28f35..6d1235ed8 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -21,8 +21,9 @@ use super::{video_qos::VideoQoS, *}; #[cfg(windows)] use crate::portable_service::client::PORTABLE_SERVICE_RUNNING; +#[cfg(windows)] +use hbb_common::get_version_number; use hbb_common::{ - get_version_number, tokio::sync::{ mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}, Mutex as TokioMutex, From 8704e1573864565503413fea54b3545c5451b683 Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Thu, 8 Dec 2022 23:05:51 +0800 Subject: [PATCH 1145/2015] fix last pr --- libs/hbb_common/src/platform/linux.rs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/libs/hbb_common/src/platform/linux.rs b/libs/hbb_common/src/platform/linux.rs index e50823438..9e581d01d 100644 --- a/libs/hbb_common/src/platform/linux.rs +++ b/libs/hbb_common/src/platform/linux.rs @@ -65,20 +65,12 @@ fn get_display_server_of_session(session: &str) -> String { { if xorg_results.trim_end().to_string() != "" { // If it is, manually return "x11", otherwise return tty - "x11".to_owned() - } else { - display_server + return "x11".to_owned() } - } else { - // If any of these commands fail just fall back to the display server - display_server } - } else { - display_server } - } else { - "".to_owned() } + display_server } else { "".to_owned() }; From d5a93adbf028e0f568f3bf429afae7a969a5b6e7 Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Thu, 8 Dec 2022 23:07:22 +0800 Subject: [PATCH 1146/2015] typo --- libs/hbb_common/src/platform/linux.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/hbb_common/src/platform/linux.rs b/libs/hbb_common/src/platform/linux.rs index 9e581d01d..4c6375dd7 100644 --- a/libs/hbb_common/src/platform/linux.rs +++ b/libs/hbb_common/src/platform/linux.rs @@ -65,7 +65,7 @@ fn get_display_server_of_session(session: &str) -> String { { if xorg_results.trim_end().to_string() != "" { // If it is, manually return "x11", otherwise return tty - return "x11".to_owned() + return "x11".to_owned(); } } } From d3d4c7dac4a09eb11e7e7b2abdcb4b1ac287c2b8 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 9 Dec 2022 10:49:47 +0800 Subject: [PATCH 1147/2015] opt: enable debug stacktrace output & add json serde check --- flutter/lib/common.dart | 8 ++++---- flutter/lib/common/widgets/peer_tab_page.dart | 17 ++++++++++------- .../lib/desktop/pages/desktop_setting_page.dart | 2 +- flutter/lib/desktop/pages/desktop_tab_page.dart | 2 +- flutter/lib/desktop/widgets/login.dart | 2 +- flutter/lib/mobile/pages/server_page.dart | 2 +- flutter/lib/models/native_model.dart | 9 ++++----- 7 files changed, 22 insertions(+), 20 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index b9077b0cb..0f5502f54 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1013,7 +1013,7 @@ class LastWindowPosition { return LastWindowPosition(m["width"], m["height"], m["offsetWidth"], m["offsetHeight"], m["isMaximized"]); } catch (e) { - debugPrint(e.toString()); + debugPrintStack(label: e.toString()); return null; } } @@ -1147,7 +1147,7 @@ Future restoreWindowPosition(WindowType type, {int? windowId}) async { final pos = bind.getLocalFlutterConfig(k: kWindowPrefix + type.name); var lpos = LastWindowPosition.loadFromString(pos); if (lpos == null) { - debugPrint("window position saved, but cannot be parsed"); + debugPrint("no window position saved, ignoring position restoration"); return false; } @@ -1212,7 +1212,7 @@ Future initUniLinks() async { } parseRustdeskUri(initialLink); } catch (err) { - debugPrint("$err"); + debugPrintStack(label: "$err"); } } @@ -1422,7 +1422,7 @@ void onActiveWindowChanged() async { rustDeskWinManager.closeAllSubWindows() ]); } catch (err) { - debugPrint("$err"); + debugPrintStack(label: "$err"); } finally { await windowManager.setPreventClose(false); await windowManager.close(); diff --git a/flutter/lib/common/widgets/peer_tab_page.dart b/flutter/lib/common/widgets/peer_tab_page.dart index 5a498a1c4..1711e7b72 100644 --- a/flutter/lib/common/widgets/peer_tab_page.dart +++ b/flutter/lib/common/widgets/peer_tab_page.dart @@ -79,16 +79,19 @@ class _PeerTabPageState extends State .toList() .obs; try { - final json = jsonDecode(bind.getLocalFlutterConfig(k: 'peer-tab-order')); - if (json is List) { - final List list = json.map((e) => e.toString()).toList(); - if (list.length == visibleOrderedTabs.length && - visibleOrderedTabs.every((e) => list.contains(e))) { - visibleOrderedTabs.value = list; + final conf = bind.getLocalFlutterConfig(k: 'peer-tab-order'); + if (conf.isNotEmpty) { + final json = jsonDecode(conf); + if (json is List) { + final List list = json.map((e) => e.toString()).toList(); + if (list.length == visibleOrderedTabs.length && + visibleOrderedTabs.every((e) => list.contains(e))) { + visibleOrderedTabs.value = list; + } } } } catch (e) { - debugPrint('$e'); + debugPrintStack(label: '$e'); } adjustTab(); diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index a51b4d035..06cabebe7 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -63,7 +63,7 @@ class DesktopSettingPage extends StatefulWidget { DesktopTabPage.onAddSetting(initialPage: page); } } catch (e) { - debugPrint('$e'); + debugPrintStack(label: '$e'); } } } diff --git a/flutter/lib/desktop/pages/desktop_tab_page.dart b/flutter/lib/desktop/pages/desktop_tab_page.dart index 794dd1c08..57c7fe4b8 100644 --- a/flutter/lib/desktop/pages/desktop_tab_page.dart +++ b/flutter/lib/desktop/pages/desktop_tab_page.dart @@ -31,7 +31,7 @@ class DesktopTabPage extends StatefulWidget { initialPage: initialPage, ))); } catch (e) { - debugPrint('$e'); + debugPrintStack(label: '$e'); } } } diff --git a/flutter/lib/desktop/widgets/login.dart b/flutter/lib/desktop/widgets/login.dart index 3e58a6de2..053653ab3 100644 --- a/flutter/lib/desktop/widgets/login.dart +++ b/flutter/lib/desktop/widgets/login.dart @@ -460,7 +460,7 @@ Future loginDialog() async { debugPrint('$resp'); completer.complete(true); } catch (err) { - debugPrint(err.toString()); + debugPrintStack(label: err.toString()); cancel(); return; } diff --git a/flutter/lib/mobile/pages/server_page.dart b/flutter/lib/mobile/pages/server_page.dart index 0b2a51d40..38ad18f14 100644 --- a/flutter/lib/mobile/pages/server_page.dart +++ b/flutter/lib/mobile/pages/server_page.dart @@ -564,7 +564,7 @@ void androidChannelInit() { } } } catch (e) { - debugPrint("MethodCallHandler err:$e"); + debugPrintStack(label: "MethodCallHandler err:$e"); } return ""; }); diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index 0a833583e..cf2de4219 100644 --- a/flutter/lib/models/native_model.dart +++ b/flutter/lib/models/native_model.dart @@ -126,7 +126,7 @@ class PlatformFFI { // no need to set home dir } } catch (e) { - debugPrint('initialize failed: $e'); + debugPrintStack(label: 'initialize failed: $e'); } String id = 'NA'; String name = 'Flutter'; @@ -151,9 +151,8 @@ class PlatformFFI { WindowsDeviceInfo winInfo = await deviceInfo.windowsInfo; name = winInfo.computerName; id = winInfo.computerName; - } catch (e, stacktrace) { - debugPrint("get windows device info failed: $e"); - debugPrintStack(stackTrace: stacktrace); + } catch (e) { + debugPrintStack(label: "get windows device info failed: $e"); name = "unknown"; id = "unknown"; } @@ -174,7 +173,7 @@ class PlatformFFI { await _ffiBind.mainSetHomeDir(home: _homeDir); await _ffiBind.mainInit(appDir: _dir); } catch (e) { - debugPrint('initialize failed: $e'); + debugPrintStack(label: 'initialize failed: $e'); } version = await getVersion(); } From 49bcf8f794822a9df36b50d4b057e4ed00611c19 Mon Sep 17 00:00:00 2001 From: Chieh Wang Date: Thu, 8 Dec 2022 18:51:20 +0800 Subject: [PATCH 1148/2015] fix conflict --- Cargo.lock | 2 +- flutter/pubspec.lock | 90 ++++++++++++++++++------------------- src/flutter_ffi.rs | 2 +- src/keyboard.rs | 56 ++++++++++++----------- src/server/input_service.rs | 5 --- src/ui_session_interface.rs | 43 +++++++++++++++--- 6 files changed, 114 insertions(+), 84 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 529be08d6..bf3ce1f04 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4229,7 +4229,7 @@ dependencies = [ [[package]] name = "rdev" version = "0.5.0-2" -source = "git+https://github.com/asur4s/rdev#fdcee04f10ea0ef00d36aa612eabb9605ae9f2fc" +source = "git+https://github.com/asur4s/rdev#4051761e7ccf434a443b8e9592c23160c9cace56" dependencies = [ "cocoa", "core-foundation 0.9.3", diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 964ae51aa..d79ff0595 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -7,7 +7,7 @@ packages: name: _fe_analyzer_shared url: "https://pub.dartlang.org" source: hosted - version: "50.0.0" + version: "49.0.0" after_layout: dependency: transitive description: @@ -21,7 +21,7 @@ packages: name: analyzer url: "https://pub.dartlang.org" source: hosted - version: "5.2.0" + version: "5.1.0" animations: dependency: transitive description: @@ -35,7 +35,7 @@ packages: name: archive url: "https://pub.dartlang.org" source: hosted - version: "3.3.5" + version: "3.3.1" args: dependency: transitive description: @@ -64,6 +64,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "6.0.1" + bot_toast: + dependency: "direct main" + description: + name: bot_toast + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.3" build: dependency: transitive description: @@ -77,7 +84,7 @@ packages: name: build_config url: "https://pub.dartlang.org" source: hosted - version: "1.1.1" + version: "1.1.0" build_daemon: dependency: transitive description: @@ -98,14 +105,14 @@ packages: name: build_runner url: "https://pub.dartlang.org" source: hosted - version: "2.3.2" + version: "2.2.1" build_runner_core: dependency: transitive description: name: build_runner_core url: "https://pub.dartlang.org" source: hosted - version: "7.2.7" + version: "7.2.4" built_collection: dependency: transitive description: @@ -119,7 +126,7 @@ packages: name: built_value url: "https://pub.dartlang.org" source: hosted - version: "8.4.2" + version: "8.4.1" cached_network_image: dependency: transitive description: @@ -196,7 +203,7 @@ packages: name: convert url: "https://pub.dartlang.org" source: hosted - version: "3.1.0" + version: "3.0.2" cross_file: dependency: transitive description: @@ -257,8 +264,8 @@ packages: dependency: "direct main" description: path: "." - ref: bf278fc8a8ff787e46fa3ab97674373bfaa20f23 - resolved-ref: bf278fc8a8ff787e46fa3ab97674373bfaa20f23 + ref: "82f9eab81cb2c7bfb938def7a1b399a6279bbc75" + resolved-ref: "82f9eab81cb2c7bfb938def7a1b399a6279bbc75" url: "https://github.com/Kingtous/rustdesk_desktop_multi_window" source: git version: "0.1.0" @@ -345,7 +352,7 @@ packages: name: file_picker url: "https://pub.dartlang.org" source: hosted - version: "5.2.2" + version: "5.2.1" fixnum: dependency: transitive description: @@ -383,8 +390,8 @@ packages: dependency: "direct main" description: path: "." - ref: dec2166e881c47d922e1edc484d10d2cd5c2103b - resolved-ref: dec2166e881c47d922e1edc484d10d2cd5c2103b + ref: "74b1b314142b6775c1243067a3503ac568ebc74b" + resolved-ref: "74b1b314142b6775c1243067a3503ac568ebc74b" url: "https://github.com/Kingtous/rustdesk_flutter_custom_cursor" source: git version: "0.0.1" @@ -436,7 +443,7 @@ packages: name: flutter_svg url: "https://pub.dartlang.org" source: hosted - version: "1.1.6" + version: "1.1.5" flutter_web_plugins: dependency: transitive description: flutter @@ -448,21 +455,21 @@ packages: name: freezed url: "https://pub.dartlang.org" source: hosted - version: "2.2.1" + version: "2.1.1" freezed_annotation: dependency: "direct main" description: name: freezed_annotation url: "https://pub.dartlang.org" source: hosted - version: "2.2.0" + version: "2.1.0" frontend_server_client: dependency: transitive description: name: frontend_server_client url: "https://pub.dartlang.org" source: hosted - version: "3.2.0" + version: "2.1.3" get: dependency: "direct main" description: @@ -476,21 +483,21 @@ packages: name: glob url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" + version: "2.1.0" graphs: dependency: transitive description: name: graphs url: "https://pub.dartlang.org" source: hosted - version: "2.2.0" + version: "2.1.0" html: dependency: transitive description: name: html url: "https://pub.dartlang.org" source: hosted - version: "0.15.1" + version: "0.15.0" http: dependency: "direct main" description: @@ -511,7 +518,7 @@ packages: name: http_parser url: "https://pub.dartlang.org" source: hosted - version: "4.0.2" + version: "4.0.1" icons_launcher: dependency: "direct dev" description: @@ -525,7 +532,7 @@ packages: name: image url: "https://pub.dartlang.org" source: hosted - version: "3.2.2" + version: "3.2.0" image_picker: dependency: "direct main" description: @@ -595,7 +602,7 @@ packages: name: lints url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "2.0.0" logging: dependency: transitive description: @@ -728,7 +735,7 @@ packages: name: path_provider_android url: "https://pub.dartlang.org" source: hosted - version: "2.0.22" + version: "2.0.20" path_provider_ios: dependency: transitive description: @@ -792,13 +799,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.3" - pointycastle: - dependency: transitive - description: - name: pointycastle - url: "https://pub.dartlang.org" - source: hosted - version: "3.6.2" pool: dependency: transitive description: @@ -819,14 +819,14 @@ packages: name: provider url: "https://pub.dartlang.org" source: hosted - version: "6.0.4" + version: "6.0.3" pub_semver: dependency: transitive description: name: pub_semver url: "https://pub.dartlang.org" source: hosted - version: "2.1.3" + version: "2.1.1" pubspec_parse: dependency: transitive description: @@ -847,7 +847,7 @@ packages: name: rxdart url: "https://pub.dartlang.org" source: hosted - version: "0.27.7" + version: "0.27.5" screen_retriever: dependency: transitive description: @@ -884,7 +884,7 @@ packages: name: shelf_web_socket url: "https://pub.dartlang.org" source: hosted - version: "1.0.3" + version: "1.0.2" simple_observable: dependency: transitive description: @@ -903,7 +903,7 @@ packages: name: source_gen url: "https://pub.dartlang.org" source: hosted - version: "1.2.6" + version: "1.2.5" source_span: dependency: transitive description: @@ -924,7 +924,7 @@ packages: name: sqflite_common url: "https://pub.dartlang.org" source: hosted - version: "2.4.0" + version: "2.3.0" stack_trace: dependency: transitive description: @@ -945,7 +945,7 @@ packages: name: stream_transform url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.0.1" string_scanner: dependency: transitive description: @@ -1008,7 +1008,7 @@ packages: name: uni_links_desktop url: "https://pub.dartlang.org" source: hosted - version: "0.1.3" + version: "0.1.4" uni_links_platform_interface: dependency: transitive description: @@ -1036,14 +1036,14 @@ packages: name: url_launcher url: "https://pub.dartlang.org" source: hosted - version: "6.1.7" + version: "6.1.6" url_launcher_android: dependency: transitive description: name: url_launcher_android url: "https://pub.dartlang.org" source: hosted - version: "6.0.22" + version: "6.0.19" url_launcher_ios: dependency: transitive description: @@ -1092,7 +1092,7 @@ packages: name: uuid url: "https://pub.dartlang.org" source: hosted - version: "3.0.7" + version: "3.0.6" vector_math: dependency: transitive description: @@ -1106,7 +1106,7 @@ packages: name: video_player url: "https://pub.dartlang.org" source: hosted - version: "2.4.8" + version: "2.4.7" video_player_android: dependency: transitive description: @@ -1183,7 +1183,7 @@ packages: name: watcher url: "https://pub.dartlang.org" source: hosted - version: "1.0.2" + version: "1.0.1" web_socket_channel: dependency: transitive description: @@ -1197,7 +1197,7 @@ packages: name: win32 url: "https://pub.dartlang.org" source: hosted - version: "3.1.2" + version: "3.0.0" win32_registry: dependency: transitive description: diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 09f943d80..0c90c3131 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -285,7 +285,7 @@ pub fn session_handle_flutter_key_event( down_or_up: bool, ) { if let Some(session) = SESSIONS.read().unwrap().get(&id) { - // session.handle_flutter_key_event(&name, keycode, scancode, down_or_up); + session.handle_flutter_key_event(&name, keycode, scancode, down_or_up); } } diff --git a/src/keyboard.rs b/src/keyboard.rs index 08cc5760a..f0c14a1e9 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -1,7 +1,10 @@ +#[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::client::get_key_state; use crate::common::GrabState; #[cfg(feature = "flutter")] use crate::flutter::FlutterHandler; +#[cfg(not(feature = "flutter"))] +use crate::ui::remote::SciterHandler; use crate::ui_session_interface::Session; use hbb_common::{log, message_proto::*}; use rdev::{Event, EventType, Key}; @@ -16,10 +19,18 @@ use std::time::SystemTime; static mut IS_ALT_GR: bool = false; pub static KEYBOARD_HOOKED: AtomicBool = AtomicBool::new(false); +lazy_static::lazy_static! { + pub static ref GRAB_SENDER: Arc>>> = Default::default(); +} + #[cfg(feature = "flutter")] lazy_static::lazy_static! { pub static ref CUR_SESSION: Arc>>> = Default::default(); - pub static ref GRAB_SENDER: Arc>>> = Default::default(); +} + +#[cfg(not(feature = "flutter"))] +lazy_static::lazy_static! { + pub static ref CUR_SESSION: Arc>>> = Default::default(); } lazy_static::lazy_static! { @@ -50,19 +61,12 @@ pub mod client { } } - pub fn save_keyboard_mode(value: String) { - release_remote_keys(); - if let Some(handler) = CUR_SESSION.lock().unwrap().as_mut() { - handler.save_keyboard_mode(value); - } - } - pub fn start_grab_loop() { let (sender, receiver) = mpsc::channel::(); - unsafe { - grab_loop(receiver); - *GRAB_SENDER.lock().unwrap() = Some(sender); - } + + grab_loop(receiver); + *GRAB_SENDER.lock().unwrap() = Some(sender); + change_grab_status(GrabState::Ready); } @@ -70,11 +74,8 @@ pub mod client { if GrabState::Wait == state { release_remote_keys(); } - unsafe { - if let Some(sender) = &*GRAB_SENDER.lock().unwrap() { - log::info!("grab state: {:?}", state); - sender.send(state); - } + if let Some(sender) = &*GRAB_SENDER.lock().unwrap() { + sender.send(state).ok(); } } @@ -83,7 +84,6 @@ pub mod client { return; } let key_event = event_to_key_event(&event); - log::info!("key event: {:?}", key_event); send_key_event(&key_event); } @@ -103,9 +103,10 @@ pub mod client { let command = *modifiers_lock.get(&Key::MetaLeft).unwrap() || *modifiers_lock.get(&Key::MetaRight).unwrap() || command; - let alt = - *modifiers_lock.get(&Key::Alt).unwrap() || *modifiers_lock.get(&Key::AltGr).unwrap() || alt; - + let alt = *modifiers_lock.get(&Key::Alt).unwrap() + || *modifiers_lock.get(&Key::AltGr).unwrap() + || alt; + (alt, ctrl, shift, command) } @@ -229,7 +230,7 @@ pub fn grab_loop(recv: mpsc::Receiver) { } pub fn is_long_press(event: &Event) -> bool { - let mut keys = MODIFIERS_STATE.lock().unwrap(); + let keys = MODIFIERS_STATE.lock().unwrap(); match event.event_type { EventType::KeyPress(k) => { if let Some(&state) = keys.get(&k) { @@ -251,12 +252,11 @@ pub fn release_remote_keys() { for key in keys { let event_type = EventType::KeyRelease(key); let event = event_type_to_event(event_type); - log::info!("release key: {:?}", key); client::process_event(event); } } -pub fn get_keyboard_mode_enum() -> KeyboardMode { +pub fn get_keyboard_mode_enum() -> KeyboardMode { match client::get_keyboard_mode().as_str() { "map" => KeyboardMode::Map, "translate" => KeyboardMode::Translate, @@ -264,6 +264,7 @@ pub fn get_keyboard_mode_enum() -> KeyboardMode { } } +#[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn add_numlock_capslock_state(key_event: &mut KeyEvent) { if get_key_state(enigo::Key::CapsLock) { key_event.modifiers.push(ControlKey::CapsLock.into()); @@ -273,6 +274,7 @@ pub fn add_numlock_capslock_state(key_event: &mut KeyEvent) { } } +#[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn convert_numpad_keys(key: Key) -> Key { if get_key_state(enigo::Key::NumLock) { return key; @@ -337,6 +339,7 @@ pub fn event_to_key_event(event: &Event) -> KeyEvent { translate_keyboard_mode(event, &mut key_event); } _ => { + #[cfg(not(any(target_os = "android", target_os = "ios")))] legacy_keyboard_mode(event, &mut key_event); } }; @@ -356,10 +359,8 @@ pub fn event_type_to_event(event_type: EventType) -> Event { } } -#[cfg(feature = "flutter")] pub fn send_key_event(key_event: &KeyEvent) { if let Some(handler) = CUR_SESSION.lock().unwrap().as_ref() { - log::info!("Sending key even {:?}", key_event); handler.send_key_event(key_event); } } @@ -373,6 +374,7 @@ pub fn get_peer_platform() -> String { } } +#[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn legacy_keyboard_mode(event: &Event, key_event: &mut KeyEvent) { // legacy mode(0): Generate characters locally, look for keycode on other side. let (mut key, down_or_up) = match event.event_type { @@ -616,4 +618,4 @@ pub fn map_keyboard_mode(event: &Event, key_event: &mut KeyEvent) { key_event.set_chr(keycode); } -pub fn translate_keyboard_mode(event: &Event, key_event: &mut KeyEvent) {} +pub fn translate_keyboard_mode(_event: &Event, _key_event: &mut KeyEvent) {} diff --git a/src/server/input_service.rs b/src/server/input_service.rs index d9b37feab..4d4389870 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -764,11 +764,6 @@ fn rdev_key_down_or_up(key: RdevKey, down_or_up: bool) { std::thread::sleep(Duration::from_millis(20)); } -fn rdev_key_click(key: RdevKey) { - rdev_key_down_or_up(key, true); - rdev_key_down_or_up(key, false); -} - fn sync_status(evt: &KeyEvent) { let mut en = ENIGO.lock().unwrap(); diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 49955b476..6bdf88b11 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -4,8 +4,7 @@ use crate::client::{ load_config, send_mouse, start_video_audio_threads, FileManager, Key, LoginConfigHandler, QualityStatus, KEY_MAP, }; -#[cfg(target_os = "linux")] -use crate::common::IS_X11; +use crate::common::GrabState; use crate::{client::Data, client::Interface}; use async_trait::async_trait; use hbb_common::config::{Config, LocalConfig, PeerConfig}; @@ -17,7 +16,8 @@ use std::collections::HashMap; use std::ops::{Deref, DerefMut}; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::{Arc, Mutex, RwLock}; - +use crate::keyboard; +use rdev::{Event, EventType::*}; pub static IS_IN: AtomicBool = AtomicBool::new(false); #[derive(Clone, Default)] @@ -292,7 +292,7 @@ impl Session { return "".to_owned(); } - pub fn send_key_event(&self, mut evt: &KeyEvent) { + pub fn send_key_event(&self, evt: &KeyEvent) { // mode: legacy(0), map(1), translate(2), auto(3) let mut msg_out = Message::new(); msg_out.set_key_event(evt.clone()); @@ -362,6 +362,40 @@ impl Session { self.send(Data::Message(msg_out)); } + pub fn handle_flutter_key_event( + &self, + name: &str, + keycode: i32, + scancode: i32, + down_or_up: bool, + ) { + if scancode < 0 || keycode < 0 { + return; + } + let keycode: u32 = keycode as u32; + let scancode: u32 = scancode as u32; + + #[cfg(not(target_os = "windows"))] + let key = rdev::key_from_scancode(scancode) as rdev::Key; + // Windows requires special handling + #[cfg(target_os = "windows")] + let key = rdev::get_win_key(keycode, scancode); + + let event_type = if down_or_up { + KeyPress(key) + } else { + KeyRelease(key) + }; + let event = Event { + time: std::time::SystemTime::now(), + name: Option::Some(name.to_owned()), + code: keycode as _, + scan_code: scancode as _, + event_type: event_type, + }; + keyboard::client::process_event(event); + } + // flutter only TODO new input fn _input_key( &self, @@ -423,7 +457,6 @@ impl Session { } } - // todo! chieh // #[cfg(not(any(target_os = "android", target_os = "ios")))] let (alt, ctrl, shift, command) = keyboard::client::get_modifiers_state(alt, ctrl, shift, command); From 03fc91e557a25ed64e7be7516590d705ad0e12e3 Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 9 Dec 2022 15:53:51 +0800 Subject: [PATCH 1149/2015] fix block_on runtime Signed-off-by: fufesou --- src/server/input_service.rs | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/server/input_service.rs b/src/server/input_service.rs index b465658bb..b6a7f9718 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -245,16 +245,18 @@ pub async fn setup_uinput(minx: i32, maxx: i32, miny: i32, maxy: i32) -> ResultT pub async fn update_mouse_resolution(minx: i32, maxx: i32, miny: i32, maxy: i32) -> ResultType<()> { set_uinput_resolution(minx, maxx, miny, maxy).await?; - if let Some(mouse) = ENIGO.lock().unwrap().get_custom_mouse() { - if let Some(mouse) = mouse - .as_mut_any() - .downcast_mut::() - { - allow_err!(mouse.send_refresh()); - } else { - log::error!("failed downcast uinput mouse"); + std::thread::spawn(|| { + if let Some(mouse) = ENIGO.lock().unwrap().get_custom_mouse() { + if let Some(mouse) = mouse + .as_mut_any() + .downcast_mut::() + { + allow_err!(mouse.send_refresh()); + } else { + log::error!("failed downcast uinput mouse"); + } } - } + }); Ok(()) } From fc85dca911420c22bcf886b9af874ac483f50bf4 Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 9 Dec 2022 17:45:07 +0800 Subject: [PATCH 1150/2015] fix refresh uinput mouse resulition Signed-off-by: fufesou --- src/server/uinput.rs | 37 ++++++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/src/server/uinput.rs b/src/server/uinput.rs index 78b22c562..a2e91e57b 100644 --- a/src/server/uinput.rs +++ b/src/server/uinput.rs @@ -4,7 +4,7 @@ use evdev::{ uinput::{VirtualDevice, VirtualDeviceBuilder}, AttributeSet, EventType, InputEvent, }; -use hbb_common::{allow_err, bail, log, tokio, ResultType}; +use hbb_common::{allow_err, bail, log, tokio::{self, runtime::Runtime}, ResultType}; static IPC_CONN_TIMEOUT: u64 = 1000; static IPC_REQUEST_TIMEOUT: u64 = 1000; @@ -17,24 +17,24 @@ pub mod client { pub struct UInputKeyboard { conn: Connection, + rt: Runtime, } impl UInputKeyboard { pub async fn new() -> ResultType { let conn = ipc::connect(IPC_CONN_TIMEOUT, IPC_POSTFIX_KEYBOARD).await?; - Ok(Self { conn }) + let rt = Runtime::new()?; + Ok(Self { conn, rt }) } - #[tokio::main(flavor = "current_thread")] - async fn send(&mut self, data: Data) -> ResultType<()> { - self.conn.send(&data).await + fn send(&mut self, data: Data) -> ResultType<()> { + self.rt.block_on(self.conn.send(&data)) } - #[tokio::main(flavor = "current_thread")] - async fn send_get_key_state(&mut self, data: Data) -> ResultType { - self.conn.send(&data).await?; + fn send_get_key_state(&mut self, data: Data) -> ResultType { + self.rt.block_on(self.conn.send(&data))?; - match self.conn.next_timeout(IPC_REQUEST_TIMEOUT).await { + match self.rt.block_on(self.conn.next_timeout(IPC_REQUEST_TIMEOUT)) { Ok(Some(Data::KeyboardResponse(ipc::DataKeyboardResponse::GetKeyState(state)))) => { Ok(state) } @@ -101,17 +101,18 @@ pub mod client { pub struct UInputMouse { conn: Connection, + rt: Runtime, } impl UInputMouse { pub async fn new() -> ResultType { let conn = ipc::connect(IPC_CONN_TIMEOUT, IPC_POSTFIX_MOUSE).await?; - Ok(Self { conn }) + let rt = Runtime::new()?; + Ok(Self { conn, rt }) } - #[tokio::main(flavor = "current_thread")] - async fn send(&mut self, data: Data) -> ResultType<()> { - self.conn.send(&data).await + fn send(&mut self, data: Data) -> ResultType<()> { + self.rt.block_on(self.conn.send(&data)) } pub fn send_refresh(&mut self) -> ResultType<()> { @@ -586,6 +587,16 @@ pub mod service { match data { Data::Mouse(data) => { if let DataMouse::Refresh = data { + let resolution = RESOLUTION.lock().unwrap(); + let rng_x = resolution.0.clone(); + let rng_y = resolution.1.clone(); + log::info!( + "Refresh uinput mouce with rng_x: ({}, {}), rng_y: ({}, {})", + rng_x.0, + rng_x.1, + rng_y.0, + rng_y.1 + ); mouse = match mouce::Mouse::new_uinput(rng_x, rng_y) { Ok(mouse) => mouse, Err(e) => { From 0bc0d489d6db9d30d11da9772199956f0e651101 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 9 Dec 2022 19:12:49 +0800 Subject: [PATCH 1151/2015] remove useless debug info --- src/ipc.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ipc.rs b/src/ipc.rs index 478094cf2..18a81398c 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -544,7 +544,7 @@ async fn check_pid(postfix: &str) { } } } - hbb_common::allow_err!(std::fs::remove_file(&Config::ipc_path(postfix))); + std::fs::remove_file(&Config::ipc_path(postfix)); } #[inline] From e1bc6e34b3a5cedee51082edf2798c2f658520c3 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 9 Dec 2022 19:43:26 +0800 Subject: [PATCH 1152/2015] remove a compile warn --- src/ipc.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ipc.rs b/src/ipc.rs index 18a81398c..34711a900 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -78,7 +78,7 @@ pub enum FS { WriteError { id: i32, file_num: i32, - err: String + err: String, }, WriteOffset { id: i32, @@ -544,7 +544,7 @@ async fn check_pid(postfix: &str) { } } } - std::fs::remove_file(&Config::ipc_path(postfix)); + std::fs::remove_file(&Config::ipc_path(postfix)).ok(); } #[inline] From cb640e48c5cdbb6f04c415dcd666cb627ef2de44 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 9 Dec 2022 20:59:25 +0800 Subject: [PATCH 1153/2015] refactor punch hole --- src/rendezvous_mediator.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/rendezvous_mediator.rs b/src/rendezvous_mediator.rs index 9350085c4..89c1ca227 100644 --- a/src/rendezvous_mediator.rs +++ b/src/rendezvous_mediator.rs @@ -379,7 +379,10 @@ impl RendezvousMediator { ) .await?; let local_addr = socket.local_addr(); - allow_err!(socket_client::connect_tcp(peer_addr, local_addr, 300).await); + // key important here for punch hole to tell gateway incoming peer is safe. + // before we use 300ms, 1000ms seems more safe. + // it can not be async here, because local_addr can not be reused, we must close the connection before use it again. + allow_err!(socket_client::connect_tcp(peer_addr, local_addr, 1000).await); socket }; let mut msg_out = Message::new(); From 12390a66e82e5e58dd23987e4c9da8b39c978b96 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 9 Dec 2022 21:04:49 +0800 Subject: [PATCH 1154/2015] shorter punch --- src/rendezvous_mediator.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/rendezvous_mediator.rs b/src/rendezvous_mediator.rs index 89c1ca227..2b2dd05bc 100644 --- a/src/rendezvous_mediator.rs +++ b/src/rendezvous_mediator.rs @@ -379,10 +379,9 @@ impl RendezvousMediator { ) .await?; let local_addr = socket.local_addr(); - // key important here for punch hole to tell gateway incoming peer is safe. - // before we use 300ms, 1000ms seems more safe. + // key important here for punch hole to tell my gateway incoming peer is safe. // it can not be async here, because local_addr can not be reused, we must close the connection before use it again. - allow_err!(socket_client::connect_tcp(peer_addr, local_addr, 1000).await); + allow_err!(socket_client::connect_tcp(peer_addr, local_addr, 30).await); socket }; let mut msg_out = Message::new(); From d916c540295081fc688fcd11bae1c06ed8a6817e Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 9 Dec 2022 21:16:09 +0800 Subject: [PATCH 1155/2015] fix sciter keyboard Signed-off-by: fufesou --- src/flutter.rs | 2 ++ src/flutter_ffi.rs | 4 +--- src/keyboard.rs | 27 ++++++++++++++++----------- src/ui.rs | 7 +++++-- src/ui/remote.rs | 15 +++++++++++++++ src/ui_session_interface.rs | 6 ++++-- 6 files changed, 43 insertions(+), 18 deletions(-) diff --git a/src/flutter.rs b/src/flutter.rs index 2bb7a9faf..788a9f540 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -324,6 +324,8 @@ impl InvokeUiSession for FlutterHandler { ); } + fn on_connected(&self, _conn_type: ConnType) {} + fn msgbox(&self, msgtype: &str, title: &str, text: &str, link: &str, retry: bool) { let has_retry = if retry { "true" } else { "" }; self.push_event( diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 0c90c3131..ddfaad06d 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -17,8 +17,6 @@ use hbb_common::{ use crate::flutter::{self, SESSIONS}; use crate::ui_interface::{self, *}; -#[cfg(not(any(target_os = "android", target_os = "ios")))] -use crate::keyboard::CUR_SESSION; use crate::{ client::file_trait::FileManager, flutter::{make_fd_to_json, session_add, session_start_}, @@ -293,7 +291,7 @@ pub fn session_enter_or_leave(id: String, enter: bool) { #[cfg(not(any(target_os = "android", target_os = "ios")))] if let Some(session) = SESSIONS.read().unwrap().get(&id) { if enter { - *CUR_SESSION.lock().unwrap() = Some(session.clone()); + crate::keyboard::set_cur_session(session.clone()); session.enter(); } else { session.leave(); diff --git a/src/keyboard.rs b/src/keyboard.rs index f0c14a1e9..eb39301f3 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -8,29 +8,31 @@ use crate::ui::remote::SciterHandler; use crate::ui_session_interface::Session; use hbb_common::{log, message_proto::*}; use rdev::{Event, EventType, Key}; -use std::collections::{HashMap, HashSet}; -use std::sync::atomic::AtomicBool; -#[cfg(any(target_os = "windows", target_os = "macos"))] -use std::sync::atomic::Ordering; -use std::sync::{mpsc, Arc, Mutex}; -use std::thread; -use std::time::SystemTime; +use std::{ + collections::{HashMap, HashSet}, + sync::{ + atomic::{AtomicBool, Ordering}, + mpsc, Arc, Mutex, + }, + thread, + time::SystemTime, +}; static mut IS_ALT_GR: bool = false; -pub static KEYBOARD_HOOKED: AtomicBool = AtomicBool::new(false); +static KEYBOARD_HOOKED: AtomicBool = AtomicBool::new(false); lazy_static::lazy_static! { - pub static ref GRAB_SENDER: Arc>>> = Default::default(); + static ref GRAB_SENDER: Arc>>> = Default::default(); } #[cfg(feature = "flutter")] lazy_static::lazy_static! { - pub static ref CUR_SESSION: Arc>>> = Default::default(); + static ref CUR_SESSION: Arc>>> = Default::default(); } #[cfg(not(feature = "flutter"))] lazy_static::lazy_static! { - pub static ref CUR_SESSION: Arc>>> = Default::default(); + static ref CUR_SESSION: Arc>>> = Default::default(); } lazy_static::lazy_static! { @@ -47,7 +49,10 @@ lazy_static::lazy_static! { m.insert(Key::MetaRight, false); Mutex::new(m) }; +} +pub fn set_cur_session(session: Session) { + *CUR_SESSION.lock().unwrap() = Some(session); } pub mod client { diff --git a/src/ui.rs b/src/ui.rs index e282d19c4..921c137ec 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -124,12 +124,15 @@ pub fn start(args: &mut [String]) { let args: Vec = iter.map(|x| x.clone()).collect(); frame.set_title(&id); frame.register_behavior("native-remote", move || { - Box::new(remote::SciterSession::new( + let handler = remote::SciterSession::new( cmd.clone(), id.clone(), pass.clone(), args.clone(), - )) + ); + let inner = handler.inner(); + crate::keyboard::set_cur_session(inner); + Box::new(handler) }); page = "remote.html"; } else { diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 3d209a71c..29b1a9eee 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -231,6 +231,17 @@ impl InvokeUiSession for SciterHandler { self.call("updatePi", &make_args!(pi_sciter)); } + fn on_connected(&self, conn_type: ConnType) { + match conn_type { + ConnType::RDP => {}, + ConnType::PORT_FORWARD => {}, + ConnType::FILE_TRANSFER => {}, + ConnType::DEFAULT_CONN => { + crate::keyboard::client::start_grab_loop(); + }, + } + } + fn msgbox(&self, msgtype: &str, title: &str, text: &str, link: &str, retry: bool) { self.call2( "msgbox_retry", @@ -434,6 +445,10 @@ impl SciterSession { Self(session) } + pub fn inner(&self) -> Session { + self.0.clone() + } + fn get_custom_image_quality(&mut self) -> Value { let mut v = Value::array(0); for x in self.lc.read().unwrap().custom_image_quality.iter() { diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 6bdf88b11..e594ac94b 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -5,6 +5,7 @@ use crate::client::{ QualityStatus, KEY_MAP, }; use crate::common::GrabState; +use crate::keyboard; use crate::{client::Data, client::Interface}; use async_trait::async_trait; use hbb_common::config::{Config, LocalConfig, PeerConfig}; @@ -12,12 +13,11 @@ 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::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::{Arc, Mutex, RwLock}; -use crate::keyboard; -use rdev::{Event, EventType::*}; pub static IS_IN: AtomicBool = AtomicBool::new(false); #[derive(Clone, Default)] @@ -580,6 +580,7 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { fn set_display(&self, x: i32, y: i32, w: i32, h: i32, cursor_embeded: bool); fn switch_display(&self, display: &SwitchDisplay); fn set_peer_info(&self, peer_info: &PeerInfo); // flutter + fn on_connected(&self, conn_type: ConnType); fn update_privacy_mode(&self); fn set_permission(&self, name: &str, value: bool); fn close_success(&self); @@ -712,6 +713,7 @@ impl Interface for Session { "", ); } + self.on_connected(self.lc.read().unwrap().conn_type); #[cfg(windows)] { let mut path = std::env::temp_dir(); From aa5debe986c874abf64fb8d997f9616db1b0ebc0 Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 9 Dec 2022 21:42:26 +0800 Subject: [PATCH 1156/2015] remove unused logic Signed-off-by: fufesou --- src/keyboard.rs | 126 +++++++++++++++++++++--------------------------- 1 file changed, 54 insertions(+), 72 deletions(-) diff --git a/src/keyboard.rs b/src/keyboard.rs index eb39301f3..a6e1a8ee9 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -12,7 +12,7 @@ use std::{ collections::{HashMap, HashSet}, sync::{ atomic::{AtomicBool, Ordering}, - mpsc, Arc, Mutex, + Arc, Mutex, }, thread, time::SystemTime, @@ -21,10 +21,6 @@ use std::{ static mut IS_ALT_GR: bool = false; static KEYBOARD_HOOKED: AtomicBool = AtomicBool::new(false); -lazy_static::lazy_static! { - static ref GRAB_SENDER: Arc>>> = Default::default(); -} - #[cfg(feature = "flutter")] lazy_static::lazy_static! { static ref CUR_SESSION: Arc>>> = Default::default(); @@ -67,20 +63,32 @@ pub mod client { } pub fn start_grab_loop() { - let (sender, receiver) = mpsc::channel::(); - - grab_loop(receiver); - *GRAB_SENDER.lock().unwrap() = Some(sender); - - change_grab_status(GrabState::Ready); + thread::spawn(grab_loop); } pub fn change_grab_status(state: GrabState) { - if GrabState::Wait == state { - release_remote_keys(); - } - if let Some(sender) = &*GRAB_SENDER.lock().unwrap() { - sender.send(state).ok(); + match state { + GrabState::Ready => {} + GrabState::Run => { + #[cfg(any(target_os = "windows", target_os = "macos"))] + KEYBOARD_HOOKED.swap(true, Ordering::SeqCst); + + #[cfg(target_os = "linux")] + rdev::enable_grab().ok(); + } + GrabState::Wait => { + release_remote_keys(); + + #[cfg(any(target_os = "windows", target_os = "macos"))] + KEYBOARD_HOOKED.swap(false, Ordering::SeqCst); + + #[cfg(target_os = "linux")] + rdev::disable_grab().ok(); + } + GrabState::Exit => { + #[cfg(target_os = "linux")] + rdev::exit_grab_listen().ok(); + } } } @@ -171,67 +179,41 @@ pub mod client { } } -pub fn grab_loop(recv: mpsc::Receiver) { - thread::spawn(move || loop { - if let Some(state) = recv.recv().ok() { - match state { - GrabState::Ready => { - #[cfg(any(target_os = "windows", target_os = "macos"))] - std::thread::spawn(move || { - let func = move |event: Event| match event.event_type { - EventType::KeyPress(key) | EventType::KeyRelease(key) => { - // fix #2211:CAPS LOCK don't work - if key == Key::CapsLock || key == Key::NumLock { - return Some(event); - } - if KEYBOARD_HOOKED.load(Ordering::SeqCst) { - client::process_event(event); - return None; - } else { - return Some(event); - } - } - _ => Some(event), - }; - if let Err(error) = rdev::grab(func) { - log::error!("rdev Error: {:?}", error) - } - }); - - #[cfg(target_os = "linux")] - rdev::start_grab_listen(move |event: Event| match event.event_type { - EventType::KeyPress(key) | EventType::KeyRelease(key) => { - if let Key::Unknown(keycode) = key { - log::error!("rdev get unknown key, keycode is : {:?}", keycode); - } else { - client::process_event(event); - } - None - } - _ => Some(event), - }); +pub fn grab_loop() { + #[cfg(any(target_os = "windows", target_os = "macos"))] + std::thread::spawn(move || { + let func = move |event: Event| match event.event_type { + EventType::KeyPress(key) | EventType::KeyRelease(key) => { + // fix #2211:CAPS LOCK don't work + if key == Key::CapsLock || key == Key::NumLock { + return Some(event); } - GrabState::Run => { - #[cfg(any(target_os = "windows", target_os = "macos"))] - KEYBOARD_HOOKED.swap(true, Ordering::SeqCst); - - #[cfg(target_os = "linux")] - rdev::enable_grab().ok(); - } - GrabState::Wait => { - #[cfg(any(target_os = "windows", target_os = "macos"))] - KEYBOARD_HOOKED.swap(false, Ordering::SeqCst); - - #[cfg(target_os = "linux")] - rdev::disable_grab().ok(); - } - GrabState::Exit => { - #[cfg(target_os = "linux")] - rdev::exit_grab_listen().ok(); + if KEYBOARD_HOOKED.load(Ordering::SeqCst) { + client::process_event(event); + return None; + } else { + return Some(event); } } + _ => Some(event), + }; + if let Err(error) = rdev::grab(func) { + log::error!("rdev Error: {:?}", error) } }); + + #[cfg(target_os = "linux")] + rdev::start_grab_listen(move |event: Event| match event.event_type { + EventType::KeyPress(key) | EventType::KeyRelease(key) => { + if let Key::Unknown(keycode) = key { + log::error!("rdev get unknown key, keycode is : {:?}", keycode); + } else { + client::process_event(event); + } + None + } + _ => Some(event), + }); } pub fn is_long_press(event: &Event) -> bool { From 1e8e1700df4c6a78f616a29e78b6e660c4c446e4 Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 9 Dec 2022 22:33:50 +0800 Subject: [PATCH 1157/2015] simplify logic Signed-off-by: fufesou --- src/keyboard.rs | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/keyboard.rs b/src/keyboard.rs index a6e1a8ee9..740afaf8f 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -14,7 +14,6 @@ use std::{ atomic::{AtomicBool, Ordering}, Arc, Mutex, }, - thread, time::SystemTime, }; @@ -63,7 +62,7 @@ pub mod client { } pub fn start_grab_loop() { - thread::spawn(grab_loop); + super::start_grab_loop(); } pub fn change_grab_status(state: GrabState) { @@ -179,7 +178,7 @@ pub mod client { } } -pub fn grab_loop() { +pub fn start_grab_loop() { #[cfg(any(target_os = "windows", target_os = "macos"))] std::thread::spawn(move || { let func = move |event: Event| match event.event_type { @@ -233,10 +232,9 @@ pub fn is_long_press(event: &Event) -> bool { pub fn release_remote_keys() { // todo!: client quit suddenly, how to release keys? - let to_release = TO_RELEASE.lock().unwrap(); - let keys = to_release.iter().map(|&key| key).collect::>(); - drop(to_release); - for key in keys { + let to_release = TO_RELEASE.lock().unwrap().clone(); + TO_RELEASE.lock().unwrap().clear(); + for key in to_release { let event_type = EventType::KeyRelease(key); let event = event_type_to_event(event_type); client::process_event(event); @@ -304,17 +302,15 @@ pub fn event_to_key_event(event: &Event) -> KeyEvent { let mut key_event = KeyEvent::new(); update_modifiers_state(event); - let mut to_release = TO_RELEASE.lock().unwrap(); match event.event_type { EventType::KeyPress(key) => { - to_release.insert(key); + TO_RELEASE.lock().unwrap().insert(key); } EventType::KeyRelease(key) => { - to_release.remove(&key); + TO_RELEASE.lock().unwrap().remove(&key); } _ => {} } - drop(to_release); let keyboard_mode = get_keyboard_mode_enum(); key_event.mode = keyboard_mode.into(); From d846a7efd51d0723a545c558d8485fcb06c5f75e Mon Sep 17 00:00:00 2001 From: Agent-JY Date: Fri, 9 Dec 2022 18:28:38 +0100 Subject: [PATCH 1158/2015] Update de.rs --- src/lang/de.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lang/de.rs b/src/lang/de.rs index 223237def..6bd2a9bbe 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -56,7 +56,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Too frequent", "Zu häufig"), ("Cancel", "Abbrechen"), ("Skip", "Überspringen"), - ("Close", "Sitzung beenden"), + ("Close", "Schließen"), ("Retry", "Erneut versuchen"), ("OK", "OK"), ("Password Required", "Passwort erforderlich"), @@ -358,7 +358,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Unpin menubar", "Menüleiste lösen"), ("Recording", "Aufnahme"), ("Directory", "Verzeichnis"), - ("Automatically record incoming sessions", "Automatische Aufzeichnung eingehender Sitzungen"), + ("Automatically record incoming sessions", "Eingehende Sitzungen automatisch aufzeichnen"), ("Change", "Ändern"), ("Start session recording", "Sitzungsaufzeichnung starten"), ("Stop session recording", "Sitzungsaufzeichnung beenden"), @@ -398,7 +398,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Hide connection management window", "Fenster zur Verwaltung der Verbindung verstecken"), ("hide_cm_tip", "Dies ist nur möglich, wenn der Zugriff nur über ein permanentes Passwort erfolgt."), // Sehr unklar. Muss noch angepasst werden. Original: Allow hiding only if accepting sessions via password and using pernament passw"), ("wayland_experiment_tip", ""), - ("Right click to select tabs", ""), - ("Add to Address Book", ""), + ("Right click to select tabs", "Register mit rechtem Mausklick auswählen"), + ("Add to Address Book", "Zum Adressbuch hinzufügen"), ].iter().cloned().collect(); } From 7f4a453cc87b586430768914a75e0c5592d578de Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sat, 10 Dec 2022 10:57:21 +0800 Subject: [PATCH 1159/2015] opt: listen status for system tray on Linux --- Cargo.lock | 80 ++++++++++++++++++++++++++++++++++++++++++++++++++--- Cargo.toml | 1 + src/tray.rs | 42 ++++++++++++++++++++-------- 3 files changed, 108 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 237369d2c..469828592 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2007,7 +2007,7 @@ version = "0.15.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "140b2f5378256527150350a8346dbdb08fadc13453a7a2d73aecd5fab3c402a7" dependencies = [ - "gio-sys", + "gio-sys 0.15.10", "glib-sys 0.15.10", "gobject-sys 0.15.10", "libc", @@ -2022,7 +2022,7 @@ checksum = "32e7a08c1e8f06f4177fb7e51a777b8c1689f743a7bc11ea91d44d2226073a88" dependencies = [ "cairo-sys-rs", "gdk-pixbuf-sys", - "gio-sys", + "gio-sys 0.15.10", "glib-sys 0.15.10", "gobject-sys 0.15.10", "libc", @@ -2078,7 +2078,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-io", - "gio-sys", + "gio-sys 0.15.10", "glib 0.15.12", "libc", "once_cell", @@ -2098,6 +2098,19 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "gio-sys" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9b693b8e39d042a95547fc258a7b07349b1f0b48f4b2fa3108ba3c51c0b5229" +dependencies = [ + "glib-sys 0.16.3", + "gobject-sys 0.16.3", + "libc", + "system-deps 6.0.3", + "winapi 0.3.9", +] + [[package]] name = "glib" version = "0.10.3" @@ -2137,6 +2150,28 @@ dependencies = [ "thiserror", ] +[[package]] +name = "glib" +version = "0.16.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cd04d150a2c63e6779f43aec7e04f5374252479b7bed5f45146d9c0e821f161" +dependencies = [ + "bitflags", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys 0.16.3", + "glib-macros 0.16.3", + "glib-sys 0.16.3", + "gobject-sys 0.16.3", + "libc", + "once_cell", + "smallvec", + "thiserror", +] + [[package]] name = "glib-macros" version = "0.10.1" @@ -2168,6 +2203,21 @@ dependencies = [ "syn", ] +[[package]] +name = "glib-macros" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e084807350b01348b6d9dbabb724d1a0bb987f47a2c85de200e98e12e30733bf" +dependencies = [ + "anyhow", + "heck 0.4.0", + "proc-macro-crate 1.2.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "glib-sys" version = "0.10.1" @@ -2188,6 +2238,16 @@ dependencies = [ "system-deps 6.0.3", ] +[[package]] +name = "glib-sys" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61a4f46316d06bfa33a7ac22df6f0524c8be58e3db2d9ca99ccb1f357b62a65" +dependencies = [ + "libc", + "system-deps 6.0.3", +] + [[package]] name = "glob" version = "0.3.0" @@ -2216,6 +2276,17 @@ dependencies = [ "system-deps 6.0.3", ] +[[package]] +name = "gobject-sys" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3520bb9c07ae2a12c7f2fbb24d4efc11231c8146a86956413fb1a79bb760a0f1" +dependencies = [ + "glib-sys 0.16.3", + "libc", + "system-deps 6.0.3", +] + [[package]] name = "gstreamer" version = "0.16.7" @@ -2382,7 +2453,7 @@ dependencies = [ "cairo-sys-rs", "gdk-pixbuf-sys", "gdk-sys", - "gio-sys", + "gio-sys 0.15.10", "glib-sys 0.15.10", "gobject-sys 0.15.10", "libc", @@ -4522,6 +4593,7 @@ dependencies = [ "flexi_logger", "flutter_rust_bridge", "flutter_rust_bridge_codegen", + "glib 0.16.5", "gtk", "hbb_common", "hound", diff --git a/Cargo.toml b/Cargo.toml index a783b1abe..2be48eca9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,6 +69,7 @@ url = { version = "2.1", features = ["serde"] } reqwest = { version = "0.11", features = ["blocking", "json", "rustls-tls"], default-features=false } chrono = "0.4.23" cidr-utils = "0.5.9" +glib = "0.16.5" [target.'cfg(not(any(target_os = "android", target_os = "linux")))'.dependencies] cpal = "0.13.5" diff --git a/src/tray.rs b/src/tray.rs index 3658739a4..b73e46301 100644 --- a/src/tray.rs +++ b/src/tray.rs @@ -88,6 +88,9 @@ pub fn start_tray() { /// 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"); @@ -106,9 +109,9 @@ pub fn start_tray() { crate::client::translate("Stop service".to_owned()) }; let menu_item_service = gtk::MenuItem::with_label(label.as_str()); - menu_item_service.connect_activate(move |item| { + menu_item_service.connect_activate(move |_| { let _lock = crate::ui_interface::SENDER.lock().unwrap(); - update_tray_service_item(item); + change_service_state(); }); menu.append(&menu_item_service); // show tray item @@ -116,6 +119,16 @@ pub fn start_tray() { 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"); @@ -123,17 +136,25 @@ pub fn start_tray() { } #[cfg(target_os = "linux")] +fn change_service_state() { + if is_service_stoped() { + 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_stoped() { - debug!("Now try to start service"); - item.set_label(&crate::client::translate("Stop service".to_owned())); - crate::ipc::set_option("stop-service", ""); - } else { - debug!("Now try to stop service"); item.set_label(&crate::client::translate("Start Service".to_owned())); - crate::ipc::set_option("stop-service", "Y"); + } else { + item.set_label(&crate::client::translate("Stop service".to_owned())); } } @@ -189,10 +210,10 @@ pub fn make_tray() { match mode { dark_light::Mode::Dark => { icon_path = "mac-tray-light.png"; - }, + } dark_light::Mode::Light => { icon_path = "mac-tray-dark.png"; - }, + } } if let Ok(mut tray) = TrayItem::new(&crate::get_app_name(), icon_path) { tray.add_label(&format!( @@ -211,4 +232,3 @@ pub fn make_tray() { } } } - From 4aa5c934245228336b60c459b396ba67b0e99e13 Mon Sep 17 00:00:00 2001 From: fufesou Date: Sat, 10 Dec 2022 12:37:18 +0800 Subject: [PATCH 1160/2015] move glib dep to linux part Signed-off-by: fufesou --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 2be48eca9..7715ebded 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,7 +69,6 @@ url = { version = "2.1", features = ["serde"] } reqwest = { version = "0.11", features = ["blocking", "json", "rustls-tls"], default-features=false } chrono = "0.4.23" cidr-utils = "0.5.9" -glib = "0.16.5" [target.'cfg(not(any(target_os = "android", target_os = "linux")))'.dependencies] cpal = "0.13.5" @@ -119,6 +118,7 @@ dbus = "0.9" dbus-crossroads = "0.5" gtk = "0.15" libappindicator = "0.7" +glib = "0.16.5" [target.'cfg(target_os = "android")'.dependencies] android_logger = "0.11" From 22155616e420e7c5222abc964bbf540e3deadd87 Mon Sep 17 00:00:00 2001 From: fufesou Date: Sat, 10 Dec 2022 12:40:45 +0800 Subject: [PATCH 1161/2015] fix key stick Signed-off-by: fufesou --- src/keyboard.rs | 28 +++++++++++++++++----------- src/ui_session_interface.rs | 2 +- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/keyboard.rs b/src/keyboard.rs index 740afaf8f..8d9aedeb6 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -91,7 +91,7 @@ pub mod client { } } - pub fn process_event(event: Event) { + pub fn process_event(event: &Event) { if is_long_press(&event) { return; } @@ -181,19 +181,25 @@ pub mod client { pub fn start_grab_loop() { #[cfg(any(target_os = "windows", target_os = "macos"))] std::thread::spawn(move || { - let func = move |event: Event| match event.event_type { - EventType::KeyPress(key) | EventType::KeyRelease(key) => { - // fix #2211:CAPS LOCK don't work - if key == Key::CapsLock || key == Key::NumLock { - return Some(event); - } - if KEYBOARD_HOOKED.load(Ordering::SeqCst) { - client::process_event(event); + let try_handle_keyboard = move |event: Event, key: Key, is_press: bool| -> Option { + // fix #2211:CAPS LOCK don't work + if key == Key::CapsLock || key == Key::NumLock { + return Some(event); + } + if KEYBOARD_HOOKED.load(Ordering::SeqCst) { + client::process_event(&event); + if is_press { return None; } else { return Some(event); } + } else { + return Some(event); } + }; + let func = move |event: Event| match event.event_type { + EventType::KeyPress(key) => try_handle_keyboard(event, key, true), + EventType::KeyRelease(key) => try_handle_keyboard(event, key, false), _ => Some(event), }; if let Err(error) = rdev::grab(func) { @@ -207,7 +213,7 @@ pub fn start_grab_loop() { if let Key::Unknown(keycode) = key { log::error!("rdev get unknown key, keycode is : {:?}", keycode); } else { - client::process_event(event); + client::process_event(&event); } None } @@ -237,7 +243,7 @@ pub fn release_remote_keys() { for key in to_release { let event_type = EventType::KeyRelease(key); let event = event_type_to_event(event_type); - client::process_event(event); + client::process_event(&event); } } diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index e594ac94b..0cf2f2e2d 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -393,7 +393,7 @@ impl Session { scan_code: scancode as _, event_type: event_type, }; - keyboard::client::process_event(event); + keyboard::client::process_event(&event); } // flutter only TODO new input From c2648b48711f7e1342102ae8f3660fa6074a5fae Mon Sep 17 00:00:00 2001 From: fufesou Date: Sat, 10 Dec 2022 12:55:19 +0800 Subject: [PATCH 1162/2015] fix build flutter Signed-off-by: fufesou --- src/keyboard.rs | 6 ++++++ src/ui.rs | 5 +++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/keyboard.rs b/src/keyboard.rs index 8d9aedeb6..d4a51c0f3 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -46,6 +46,12 @@ lazy_static::lazy_static! { }; } +#[cfg(feature = "flutter")] +pub fn set_cur_session(session: Session) { + *CUR_SESSION.lock().unwrap() = Some(session); +} + +#[cfg(not(feature = "flutter"))] pub fn set_cur_session(session: Session) { *CUR_SESSION.lock().unwrap() = Some(session); } diff --git a/src/ui.rs b/src/ui.rs index 921c137ec..e13f11d87 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -130,8 +130,9 @@ pub fn start(args: &mut [String]) { pass.clone(), args.clone(), ); - let inner = handler.inner(); - crate::keyboard::set_cur_session(inner); + #[cfg(not(feature = "flutter"))] + crate::keyboard::set_cur_session(handler.inner()); + Box::new(handler) }); page = "remote.html"; From a780519fe05ac8b615d7f5e48b209e8cd8723f66 Mon Sep 17 00:00:00 2001 From: fufesou Date: Sat, 10 Dec 2022 19:46:41 +0800 Subject: [PATCH 1163/2015] allow_err with msg Signed-off-by: fufesou --- libs/hbb_common/src/lib.rs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/libs/hbb_common/src/lib.rs b/libs/hbb_common/src/lib.rs index ae564685f..0f9f7824c 100644 --- a/libs/hbb_common/src/lib.rs +++ b/libs/hbb_common/src/lib.rs @@ -66,6 +66,21 @@ macro_rules! allow_err { } else { } }; + + ($e:expr, $($arg:tt)*) => { + if let Err(err) = $e { + log::debug!( + "{:?}, {}, {}:{}:{}:{}", + err, + format_args!($($arg)*), + module_path!(), + file!(), + line!(), + column!() + ); + } else { + } + }; } #[inline] @@ -250,4 +265,10 @@ mod tests { let addr = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(192, 168, 16, 32), 21116)); assert_eq!(addr, AddrMangle::decode(&AddrMangle::encode(addr))); } + + #[test] + fn test_allow_err() { + allow_err!(Err("test err") as Result<(), &str>); + allow_err!(Err("test err with msg") as Result<(), &str>, "prompt {}", "failed"); + } } From a6135068a9905979b8b7c90b6c3e235c9dab2fe0 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sun, 11 Dec 2022 00:16:02 +0800 Subject: [PATCH 1164/2015] https://github.com/rustdesk/rustdesk/issues/2468 --- src/platform/linux.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 82d6592db..8a15c83dd 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -179,7 +179,8 @@ fn set_x11_env(uid: &str) { log::info!("uid of seat0: {}", uid); let gdm = format!("/run/user/{}/gdm/Xauthority", uid); let mut auth = get_env_tries("XAUTHORITY", uid, 10); - if auth.is_empty() { + // auth is another user's when uid = 0, https://github.com/rustdesk/rustdesk/issues/2468 + if auth.is_empty() || uid == "0" { auth = if std::path::Path::new(&gdm).exists() { gdm } else { From 46a6df86ea412d520355499d48edd677bf05ea0c Mon Sep 17 00:00:00 2001 From: kingtous Date: Sun, 11 Dec 2022 14:17:29 +0800 Subject: [PATCH 1165/2015] refactor: use latest custom cursor api --- flutter/lib/desktop/pages/remote_page.dart | 50 +++++++++++++--------- flutter/lib/models/model.dart | 5 ++- flutter/pubspec.yaml | 2 +- 3 files changed, 34 insertions(+), 23 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 7b3e0fe82..130a5e6ad 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -5,6 +5,8 @@ import 'dart:ui' as ui; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_custom_cursor/cursor_manager.dart' + as custom_cursor_manager; import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:wakelock/wakelock.dart'; @@ -109,17 +111,17 @@ class _RemotePageState extends State id: widget.id, arg: 'show-remote-cursor'); _zoomCursor.value = bind.sessionGetToggleOptionSync(id: widget.id, arg: 'zoom-cursor'); - if (!_isCustomCursorInited) { - customCursorController.registerNeedUpdateCursorCallback( - (String? lastKey, String? currentKey) async { - if (_firstEnterImage.value) { - _firstEnterImage.value = false; - return true; - } - return lastKey == null || lastKey != currentKey; - }); - _isCustomCursorInited = true; - } + // if (!_isCustomCursorInited) { + // customCursorController.registerNeedUpdateCursorCallback( + // (String? lastKey, String? currentKey) async { + // if (_firstEnterImage.value) { + // _firstEnterImage.value = false; + // return true; + // } + // return lastKey == null || lastKey != currentKey; + // }); + // _isCustomCursorInited = true; + // } } @override @@ -366,15 +368,23 @@ class _ImagePaintState extends State { return MouseCursor.defer; } else { final key = cache.updateGetKey(scale, zoomCursor.value); - cursor.addKey(key); - return FlutterCustomMemoryImageCursor( - pixbuf: cache.data, - key: key, - hotx: cache.hotx, - hoty: cache.hoty, - imageWidth: (cache.width * cache.scale).toInt(), - imageHeight: (cache.height * cache.scale).toInt(), - ); + if (!cursor.cachedKeys.contains(key)) { + debugPrint("Register custom cursor with key $key"); + // [Safety] + // It's ok to call async registerCursor in current synchronous context, + // because activating the cursor is also an async call and will always + // be executed after this. + custom_cursor_manager.CursorManager.instance + .registerCursor(custom_cursor_manager.CursorData() + ..buffer = cache.data! + ..height = (cache.height * cache.scale).toInt() + ..width = (cache.width * cache.scale).toInt() + ..hotX = cache.hotx + ..hotY = cache.hoty + ..name = key); + cursor.addKey(key); + } + return FlutterCustomMemoryImageCursor(key: key); } } diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 805bcde33..37c246fe1 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -19,7 +19,7 @@ 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/flutter_custom_cursor.dart'; +import 'package:flutter_custom_cursor/cursor_manager.dart'; import 'package:flutter_svg/flutter_svg.dart'; import '../common.dart'; @@ -1113,7 +1113,8 @@ class CursorModel with ChangeNotifier { _clearCache() { final keys = {...cachedKeys}; for (var k in keys) { - customCursorController.freeCache(k); + debugPrint("deleting cursor with key $k"); + CursorManager.instance.deleteCursor(k); } } } diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index a8a3d7050..9084446be 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -68,7 +68,7 @@ dependencies: flutter_custom_cursor: git: url: https://github.com/Kingtous/rustdesk_flutter_custom_cursor - ref: 74b1b314142b6775c1243067a3503ac568ebc74b + ref: da241145957988efd91cc12a364848eabe505e83 window_size: git: url: https://github.com/google/flutter-desktop-embedding.git From 941b7a365bd45a3362a1b4f35197afd4e4227b9e Mon Sep 17 00:00:00 2001 From: Asura Date: Sun, 11 Dec 2022 06:22:24 -0800 Subject: [PATCH 1166/2015] opt: catch error of grab --- Cargo.lock | 28 +++------------------------- Cargo.toml | 2 +- src/keyboard.rs | 14 ++++++++------ 3 files changed, 12 insertions(+), 32 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 237369d2c..07d95ac50 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1533,7 +1533,7 @@ dependencies = [ "log", "objc", "pkg-config", - "rdev 0.5.0-2 (git+https://github.com/asur4s/rdev)", + "rdev", "serde 1.0.147", "serde_derive", "tfc", @@ -4229,29 +4229,7 @@ dependencies = [ [[package]] name = "rdev" version = "0.5.0-2" -source = "git+https://github.com/asur4s/rdev#4051761e7ccf434a443b8e9592c23160c9cace56" -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", - "mio 0.8.5", - "strum 0.24.1", - "strum_macros 0.24.3", - "widestring 1.0.2", - "winapi 0.3.9", - "x11 2.20.0", -] - -[[package]] -name = "rdev" -version = "0.5.0-2" -source = "git+https://github.com/rustdesk/rdev#25c29f61bfdf5d8ec50f0a8a7743bc1d85eb2c04" +source = "git+https://github.com/asur4s/rdev#3d6d413a9b2ab703edc22071acea31826b0efce3" dependencies = [ "cocoa", "core-foundation 0.9.3", @@ -4540,7 +4518,7 @@ dependencies = [ "num_cpus", "objc", "parity-tokio-ipc", - "rdev 0.5.0-2 (git+https://github.com/rustdesk/rdev)", + "rdev", "repng", "reqwest", "rpassword 7.1.0", diff --git a/Cargo.toml b/Cargo.toml index a783b1abe..cbf384a38 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,7 +63,7 @@ default-net = "0.11.0" wol-rs = "0.9.1" flutter_rust_bridge = { git = "https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge", optional = true } errno = "0.2.8" -rdev = { git = "https://github.com/rustdesk/rdev" } +rdev = { git = "https://github.com/asur4s/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 d4a51c0f3..b2e19ac73 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -7,7 +7,7 @@ use crate::flutter::FlutterHandler; use crate::ui::remote::SciterHandler; use crate::ui_session_interface::Session; use hbb_common::{log, message_proto::*}; -use rdev::{Event, EventType, Key}; +use rdev::{Event, EventType, Key, GrabError}; use std::{ collections::{HashMap, HashSet}, sync::{ @@ -79,7 +79,7 @@ pub mod client { KEYBOARD_HOOKED.swap(true, Ordering::SeqCst); #[cfg(target_os = "linux")] - rdev::enable_grab().ok(); + rdev::enable_grab(); } GrabState::Wait => { release_remote_keys(); @@ -88,11 +88,11 @@ pub mod client { KEYBOARD_HOOKED.swap(false, Ordering::SeqCst); #[cfg(target_os = "linux")] - rdev::disable_grab().ok(); + rdev::disable_grab(); } GrabState::Exit => { #[cfg(target_os = "linux")] - rdev::exit_grab_listen().ok(); + rdev::exit_grab_listen(); } } } @@ -214,7 +214,7 @@ pub fn start_grab_loop() { }); #[cfg(target_os = "linux")] - rdev::start_grab_listen(move |event: Event| match event.event_type { + if let Err(err) = rdev::start_grab_listen(move |event: Event| match event.event_type { EventType::KeyPress(key) | EventType::KeyRelease(key) => { if let Key::Unknown(keycode) = key { log::error!("rdev get unknown key, keycode is : {:?}", keycode); @@ -224,7 +224,9 @@ pub fn start_grab_loop() { None } _ => Some(event), - }); + }) { + log::error!("Failed to init rdev grab thread: {:?}", err); + }; } pub fn is_long_press(event: &Event) -> bool { From bc5a959c902bde1a6e2fa6bafc02dbf30ac5f36e Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sun, 11 Dec 2022 23:23:26 +0800 Subject: [PATCH 1167/2015] opt: backport cursor api --- .github/workflows/flutter-nightly.yml | 4 ++-- flutter/pubspec.lock | 16 +++++++--------- flutter/pubspec.yaml | 5 +---- 3 files changed, 10 insertions(+), 15 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 4ccd42081..5474183db 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -47,8 +47,8 @@ jobs: run: | flutter doctor -v flutter precache --windows - Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.0.5-rustdesk/windows-x64-release-flutter.zip -OutFile windows-x64-release-flutter.zip - Expand-Archive windows-x64-release-flutter.zip -DestinationPath engine + Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.0.5-rustdesk.2/windows-x64-flutter-release.zip -OutFile windows-x64-flutter-release.zip + Expand-Archive windows-x64-flutter-release.zip -DestinationPath engine mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-3.0.5-x64/bin/cache/artifacts/engine/windows-x64-release/ - name: Install Rust toolchain diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 625862dbf..aec2abcde 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -352,7 +352,7 @@ packages: name: file_picker url: "https://pub.dartlang.org" source: hosted - version: "5.2.2" + version: "5.2.3" fixnum: dependency: transitive description: @@ -389,11 +389,9 @@ packages: flutter_custom_cursor: dependency: "direct main" description: - path: "." - ref: "74b1b314142b6775c1243067a3503ac568ebc74b" - resolved-ref: "74b1b314142b6775c1243067a3503ac568ebc74b" - url: "https://github.com/Kingtous/rustdesk_flutter_custom_cursor" - source: git + name: flutter_custom_cursor + url: "https://pub.dartlang.org" + source: hosted version: "0.0.1" flutter_improved_scrolling: dependency: "direct main" @@ -455,7 +453,7 @@ packages: name: freezed url: "https://pub.dartlang.org" source: hosted - version: "2.2.1" + version: "2.3.2" freezed_annotation: dependency: "direct main" description: @@ -637,7 +635,7 @@ packages: name: mime url: "https://pub.dartlang.org" source: hosted - version: "1.0.2" + version: "1.0.3" nested: dependency: transitive description: @@ -1113,7 +1111,7 @@ packages: name: video_player url: "https://pub.dartlang.org" source: hosted - version: "2.4.8" + version: "2.4.9" video_player_android: dependency: transitive description: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 9084446be..31caa91fd 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -65,10 +65,7 @@ dependencies: url: https://github.com/Kingtous/rustdesk_desktop_multi_window ref: 82f9eab81cb2c7bfb938def7a1b399a6279bbc75 freezed_annotation: ^2.0.3 - flutter_custom_cursor: - git: - url: https://github.com/Kingtous/rustdesk_flutter_custom_cursor - ref: da241145957988efd91cc12a364848eabe505e83 + flutter_custom_cursor: ^0.0.1 window_size: git: url: https://github.com/google/flutter-desktop-embedding.git From 5aaaf2a5e1198c3b84ec87081c2928b55bc91bb1 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sun, 11 Dec 2022 23:37:07 +0800 Subject: [PATCH 1168/2015] update: deps --- Cargo.lock | 332 ++++++++++++++++++++++++++++------------------------- 1 file changed, 174 insertions(+), 158 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 469828592..3db348fd0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -76,7 +76,7 @@ dependencies = [ "alsa-sys", "bitflags", "libc", - "nix 0.23.1", + "nix 0.23.2", ] [[package]] @@ -175,11 +175,11 @@ dependencies = [ [[package]] name = "async-channel" -version = "1.7.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14485364214912d3b19cc3435dde4df66065127f05fa0d75c712f36f12c2f28" +checksum = "cf46fee83e5ccffc220104713af3292ff9bc7c64c7de289f66dae8e38d826833" dependencies = [ - "concurrent-queue 1.2.4", + "concurrent-queue", "event-listener", "futures-core", ] @@ -192,7 +192,7 @@ checksum = "17adb73da160dfb475c183343c8cccd80721ea5a605d3eb57125f0a7b7a92d0b" dependencies = [ "async-lock", "async-task", - "concurrent-queue 2.0.0", + "concurrent-queue", "fastrand", "futures-lite", "slab", @@ -200,13 +200,13 @@ dependencies = [ [[package]] name = "async-io" -version = "1.10.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8121296a9f05be7f34aa4196b1747243b3b62e048bb7906f644f3fbfc490cf7" +checksum = "8c374dda1ed3e7d8f0d9ba58715f924862c63eae6849c92d3a18e7fbde9e2794" dependencies = [ "async-lock", "autocfg 1.1.0", - "concurrent-queue 1.2.4", + "concurrent-queue", "futures-lite", "libc", "log", @@ -215,7 +215,7 @@ dependencies = [ "slab", "socket2 0.4.7", "waker-fn", - "winapi 0.3.9", + "windows-sys 0.42.0", ] [[package]] @@ -230,20 +230,20 @@ dependencies = [ [[package]] name = "async-process" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02111fd8655a613c25069ea89fc8d9bb89331fa77486eb3bc059ee757cfa481c" +checksum = "6381ead98388605d0d9ff86371043b5aa922a3905824244de40dc263a14fcba4" dependencies = [ "async-io", + "async-lock", "autocfg 1.1.0", "blocking", "cfg-if 1.0.0", "event-listener", "futures-lite", "libc", - "once_cell", "signal-hook", - "winapi 0.3.9", + "windows-sys 0.42.0", ] [[package]] @@ -265,9 +265,9 @@ checksum = "7a40729d2133846d9ed0ea60a8b9541bccddab49cd30f0715a1da672fe9a2524" [[package]] name = "async-trait" -version = "0.1.58" +version = "0.1.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e805d94e6b5001b651426cf4cd446b1ab5f319d27bab5c644f61de0a804360c" +checksum = "31e6e93155431f3931513b243d371981bb2770112b370c82745a1d19d2f99364" dependencies = [ "proc-macro2", "quote", @@ -438,16 +438,16 @@ dependencies = [ [[package]] name = "blocking" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6ccb65d468978a086b69884437ded69a90faab3bbe6e67f242173ea728acccc" +checksum = "3c67b173a56acffd6d2326fb7ab938ba0b00a71480e14902b2591c87bc5741e8" dependencies = [ "async-channel", + "async-lock", "async-task", "atomic-waker", "fastrand", "futures-lite", - "once_cell", ] [[package]] @@ -495,15 +495,9 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c" dependencies = [ - "serde 1.0.147", + "serde 1.0.149", ] -[[package]] -name = "cache-padded" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c" - [[package]] name = "cairo-rs" version = "0.15.12" @@ -544,7 +538,7 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88ad0e1e3e88dd237a156ab9f571021b8a158caa0ae44b1968a241efb5144c1e" dependencies = [ - "serde 1.0.147", + "serde 1.0.149", ] [[package]] @@ -553,7 +547,7 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbdb825da8a5df079a43676dbe042702f1707b1109f713a01420fbb4cc71fa27" dependencies = [ - "serde 1.0.147", + "serde 1.0.149", ] [[package]] @@ -565,7 +559,7 @@ dependencies = [ "camino", "cargo-platform", "semver 1.0.14", - "serde 1.0.147", + "serde 1.0.149", "serde_json 1.0.89", ] @@ -581,7 +575,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "serde 1.0.147", + "serde 1.0.149", "serde_json 1.0.89", "syn", "tempfile", @@ -643,7 +637,7 @@ dependencies = [ "js-sys", "num-integer", "num-traits 0.2.15", - "time 0.1.44", + "time 0.1.45", "wasm-bindgen", "winapi 0.3.9", ] @@ -733,7 +727,7 @@ dependencies = [ "cc", "hbb_common", "lazy_static", - "serde 1.0.147", + "serde 1.0.149", "serde_derive", "thiserror", ] @@ -824,15 +818,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "concurrent-queue" -version = "1.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af4780a44ab5696ea9e28294517f1fffb421a83a25af521333c838635509db9c" -dependencies = [ - "cache-padded", -] - [[package]] name = "concurrent-queue" version = "2.0.0" @@ -848,7 +833,7 @@ version = "0.4.0" source = "git+https://github.com/open-trade/confy#630cc28a396cb7d01eefdd9f3824486fe4d8554b" dependencies = [ "directories-next", - "serde 1.0.147", + "serde 1.0.149", "thiserror", "toml", ] @@ -976,7 +961,7 @@ dependencies = [ "mach", "ndk 0.6.0", "ndk-glue 0.6.2", - "nix 0.23.1", + "nix 0.23.2", "oboe", "parking_lot 0.11.2", "stdweb", @@ -1068,12 +1053,12 @@ dependencies = [ [[package]] name = "ctrlc" -version = "3.2.3" +version = "3.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d91974fbbe88ec1df0c24a4f00f99583667a7e2e6272b2b92d294d81e462173" +checksum = "1631ca6e3c59112501a9d87fd86f21591ff77acd31331e8a73f8d80a65bbdd71" dependencies = [ - "nix 0.25.0", - "winapi 0.3.9", + "nix 0.26.1", + "windows-sys 0.42.0", ] [[package]] @@ -1084,9 +1069,9 @@ checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35" [[package]] name = "cxx" -version = "1.0.82" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4a41a86530d0fe7f5d9ea779916b7cadd2d4f9add748b99c2c029cbbdfaf453" +checksum = "bdf07d07d6531bfcdbe9b8b739b104610c6508dcc4d63b410585faf338241daf" dependencies = [ "cc", "cxxbridge-flags", @@ -1096,9 +1081,9 @@ dependencies = [ [[package]] name = "cxx-build" -version = "1.0.82" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06416d667ff3e3ad2df1cd8cd8afae5da26cf9cec4d0825040f88b5ca659a2f0" +checksum = "d2eb5b96ecdc99f72657332953d4d9c50135af1bac34277801cc3937906ebd39" dependencies = [ "cc", "codespan-reporting", @@ -1111,15 +1096,15 @@ dependencies = [ [[package]] name = "cxxbridge-flags" -version = "1.0.82" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "820a9a2af1669deeef27cb271f476ffd196a2c4b6731336011e0ba63e2c7cf71" +checksum = "ac040a39517fd1674e0f32177648334b0f4074625b5588a64519804ba0553b12" [[package]] name = "cxxbridge-macro" -version = "1.0.82" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a08a6e2fcc370a089ad3b4aaf54db3b1b4cee38ddabce5896b33eb693275f470" +checksum = "1362b0ddcfc4eb0a1f57b68bd77dd99f0e826958a96abd0ae9bd092e114ffed6" dependencies = [ "proc-macro2", "quote", @@ -1458,7 +1443,7 @@ checksum = "7f3f119846c823f9eafcf953a8f6ffb6ed69bf6240883261a7f13b634579a51f" dependencies = [ "lazy_static", "regex", - "serde 1.0.147", + "serde 1.0.149", "strsim 0.10.0", ] @@ -1481,7 +1466,7 @@ dependencies = [ "cc", "hbb_common", "lazy_static", - "serde 1.0.147", + "serde 1.0.149", "serde_derive", "thiserror", ] @@ -1503,9 +1488,9 @@ checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" [[package]] name = "embed-resource" -version = "1.7.2" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc24ff8d764818e9ab17963b0593c535f077a513f565e75e4352d758bc4d8c0" +checksum = "e62abb876c07e4754fae5c14cafa77937841f01740637e17d78dc04352f32a5e" dependencies = [ "cc", "rustc_version 0.4.0", @@ -1534,7 +1519,7 @@ dependencies = [ "objc", "pkg-config", "rdev 0.5.0-2 (git+https://github.com/asur4s/rdev)", - "serde 1.0.147", + "serde 1.0.149", "serde_derive", "tfc", "unicode-segmentation", @@ -1580,7 +1565,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e75d4cd21b95383444831539909fbb14b9dc3fdceb2a6f5d36577329a1f55ccb" dependencies = [ "enumflags2_derive", - "serde 1.0.147", + "serde 1.0.149", ] [[package]] @@ -1679,7 +1664,7 @@ source = "git+https://github.com/fufesou/evdev#cec616e37790293d2cd2aa54a96601ed6 dependencies = [ "bitvec", "libc", - "nix 0.23.1", + "nix 0.23.2", ] [[package]] @@ -1718,9 +1703,9 @@ dependencies = [ [[package]] name = "filetime" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b9663d381d07ae25dc88dbdf27df458faa83a9b25336bcac83d5e452b5fc9d3" +checksum = "4e884668cd0c7480504233e951174ddc3b382f7c2666e3b7310b5c4e7b0c37f9" dependencies = [ "cfg-if 1.0.0", "libc", @@ -1730,12 +1715,12 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6" +checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" dependencies = [ "crc32fast", - "miniz_oxide 0.5.4", + "miniz_oxide 0.6.2", ] [[package]] @@ -1787,7 +1772,7 @@ dependencies = [ "pathdiff", "quote", "regex", - "serde 1.0.147", + "serde 1.0.149", "serde_yaml", "structopt", "syn", @@ -2526,7 +2511,7 @@ dependencies = [ "quinn", "rand 0.8.5", "regex", - "serde 1.0.147", + "serde 1.0.149", "serde_derive", "serde_json 1.0.89", "socket2 0.3.19", @@ -2618,12 +2603,12 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hwcodec" version = "0.1.0" -source = "git+https://github.com/21pages/hwcodec#f54d69b35251ade110373403ddefcb8b49c87305" +source = "git+https://github.com/21pages/hwcodec#e819484c4c010199f2a0977bdf306b4edbeafbae" dependencies = [ "bindgen 0.59.2", "cc", "log", - "serde 1.0.147", + "serde 1.0.149", "serde_derive", "serde_json 1.0.89", ] @@ -2654,9 +2639,9 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.23.1" +version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59df7c4e19c950e6e0e868dcc0a300b09a9b88e9ec55bd879ca819087a77355d" +checksum = "1788965e61b367cd03a62950836d5cd41560c3577d90e40e0819373194d1661c" dependencies = [ "http", "hyper", @@ -2801,9 +2786,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.5.1" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f88c5561171189e69df9d98bcf18fd5f9558300f7ea7b801eb8a0fd748bd8745" +checksum = "ec947b7a4ce12e3b87e353abae7ce124d025b6c7d6c5aea5cc0bcf92e9510ded" [[package]] name = "itertools" @@ -2918,9 +2903,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.137" +version = "0.2.138" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89" +checksum = "db6d7e329c562c5dfab7a46a2afabc8b987ab9a4834c9d1ca04dc54c1546cef8" [[package]] name = "libdbus-sys" @@ -3050,7 +3035,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b238e3235c8382b7653c6408ed1b08dd379bdb9fdf990fb0bbae3db2cc0ae963" dependencies = [ - "nix 0.23.1", + "nix 0.23.2", "winapi 0.3.9", ] @@ -3175,6 +3160,15 @@ dependencies = [ "adler", ] +[[package]] +name = "miniz_oxide" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" +dependencies = [ + "adler", +] + [[package]] name = "mio" version = "0.6.23" @@ -3251,7 +3245,7 @@ dependencies = [ [[package]] name = "mouce" version = "0.2.1" -source = "git+https://github.com/fufesou/mouce.git#aa18ba25bb47484282e972a4b95a8e1d753230b5" +source = "git+https://github.com/fufesou/mouce.git#ed83800d532b95d70e39915314f6052aa433e9b9" dependencies = [ "glob", ] @@ -3401,9 +3395,9 @@ dependencies = [ [[package]] name = "nix" -version = "0.23.1" +version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f866317acbd3a240710c63f065ffb1e4fd466259045ccb504130b7f668f35c6" +checksum = "8f3790c00a0150112de0f4cd161e3d7fc4b2d8a5542ffc35f099a2562aecb35c" dependencies = [ "bitflags", "cc", @@ -3414,9 +3408,9 @@ dependencies = [ [[package]] name = "nix" -version = "0.24.2" +version = "0.24.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "195cdbc1741b8134346d515b3a56a1c94b0912758009cfd53f99ea0f57b065fc" +checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069" dependencies = [ "bitflags", "cfg-if 1.0.0", @@ -3426,9 +3420,9 @@ dependencies = [ [[package]] name = "nix" -version = "0.25.0" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e322c04a9e3440c327fca7b6c8a63e6890a32fa2ad689db972425f07e0d22abb" +checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" dependencies = [ "autocfg 1.1.0", "bitflags", @@ -3438,6 +3432,18 @@ dependencies = [ "pin-utils", ] +[[package]] +name = "nix" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a58d1d356c6597d08cde02c2f09d785b09e28711837b1ed667dc652c08a694" +dependencies = [ + "bitflags", + "cfg-if 1.0.0", + "libc", + "static_assertions", +] + [[package]] name = "nom" version = "7.1.1" @@ -3663,9 +3669,9 @@ dependencies = [ [[package]] name = "ordered-stream" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "034ce384018b245e8d8424bbe90577fbd91a533be74107e465e3474eb2285eef" +checksum = "01ca8c99d73c6e92ac1358f9f692c22c0bfd9c4701fa086f5d365c0d4ea818ea" dependencies = [ "futures-core", "pin-project-lite", @@ -3747,7 +3753,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ "lock_api", - "parking_lot_core 0.9.4", + "parking_lot_core 0.9.5", ] [[package]] @@ -3766,9 +3772,9 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dc9e0dc2adc1c69d09143aff38d3d30c5c3f0df0dad82e6d25547af174ebec0" +checksum = "7ff9f3fef3968a3ec5945535ed654cb38ff72d7495a25619e2247fb15a2ed9ba" dependencies = [ "cfg-if 1.0.0", "libc", @@ -3803,9 +3809,9 @@ checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" [[package]] name = "pest" -version = "2.4.1" +version = "2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a528564cc62c19a7acac4d81e01f39e53e25e17b934878f4c6d25cc2836e62f8" +checksum = "cc8bed3549e0f9b0a2a78bf7c0018237a2cdf085eecbbc048e52612438e4e9d0" dependencies = [ "thiserror", "ucd-trie", @@ -3901,16 +3907,16 @@ dependencies = [ [[package]] name = "polling" -version = "2.4.0" +version = "2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab4609a838d88b73d8238967b60dd115cc08d38e2bbaf51ee1e4b695f89122e2" +checksum = "166ca89eb77fd403230b9c156612965a81e094ec6ec3aa13663d4c8b113fa748" dependencies = [ "autocfg 1.1.0", "cfg-if 1.0.0", "libc", "log", "wepoll-ffi", - "winapi 0.3.9", + "windows-sys 0.42.0", ] [[package]] @@ -4276,11 +4282,10 @@ dependencies = [ [[package]] name = "rayon" -version = "1.6.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e060280438193c554f654141c9ea9417886713b7acd75974c85b18a69a88e0b" +checksum = "6db3a213adf02b3bcfd2d3846bb41cb22857d131789e01df434fb7e7bc0759b7" dependencies = [ - "crossbeam-deque", "either", "rayon-core", ] @@ -4300,7 +4305,7 @@ dependencies = [ [[package]] name = "rdev" version = "0.5.0-2" -source = "git+https://github.com/asur4s/rdev#4051761e7ccf434a443b8e9592c23160c9cace56" +source = "git+https://github.com/asur4s/rdev#3d6d413a9b2ab703edc22071acea31826b0efce3" dependencies = [ "cocoa", "core-foundation 0.9.3", @@ -4316,13 +4321,13 @@ dependencies = [ "strum_macros 0.24.3", "widestring 1.0.2", "winapi 0.3.9", - "x11 2.20.0", + "x11 2.20.1", ] [[package]] name = "rdev" version = "0.5.0-2" -source = "git+https://github.com/rustdesk/rdev#25c29f61bfdf5d8ec50f0a8a7743bc1d85eb2c04" +source = "git+https://github.com/rustdesk/rdev#3d6d413a9b2ab703edc22071acea31826b0efce3" dependencies = [ "cocoa", "core-foundation 0.9.3", @@ -4338,7 +4343,7 @@ dependencies = [ "strum_macros 0.24.3", "widestring 1.0.2", "winapi 0.3.9", - "x11 2.20.0", + "x11 2.20.1", ] [[package]] @@ -4352,9 +4357,9 @@ dependencies = [ [[package]] name = "realfft" -version = "3.1.0" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3052e66d6ebeff8049607775c41d39a58d1dfa91a2733e89f2b7816bce2ea4cc" +checksum = "93d6b8e8f0c6d2234aa58048d7290c60bf92cd36fd2888cd8331c66ad4f2e1d2" dependencies = [ "rustfft", ] @@ -4440,7 +4445,7 @@ dependencies = [ "pin-project-lite", "rustls", "rustls-pemfile 1.0.1", - "serde 1.0.147", + "serde 1.0.149", "serde_json 1.0.89", "serde_urlencoded", "tokio", @@ -4482,9 +4487,20 @@ dependencies = [ [[package]] name = "rpassword" -version = "7.1.0" +version = "7.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20c9f5d2a0c3e2ea729ab3706d22217177770654c3ef5056b68b69d07332d3f5" +checksum = "6678cf63ab3491898c0d021b493c94c9b221d91295294a2a5746eacbe5928322" +dependencies = [ + "libc", + "rtoolbox", + "winapi 0.3.9", +] + +[[package]] +name = "rtoolbox" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "034e22c514f5c0cb8a10ff341b9b048b5ceb21591f31c8f44c43b960f9b3524a" dependencies = [ "libc", "winapi 0.3.9", @@ -4615,14 +4631,14 @@ dependencies = [ "rdev 0.5.0-2 (git+https://github.com/rustdesk/rdev)", "repng", "reqwest", - "rpassword 7.1.0", + "rpassword 7.2.0", "rubato", "runas", "rust-pulsectl", "samplerate", "sciter-rs", "scrap", - "serde 1.0.147", + "serde 1.0.149", "serde_derive", "serde_json 1.0.89", "sha2", @@ -4799,7 +4815,7 @@ dependencies = [ "num_cpus", "quest", "repng", - "serde 1.0.147", + "serde 1.0.149", "serde_json 1.0.89", "target_build_utils", "tracing", @@ -4861,7 +4877,7 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e25dfac463d778e353db5be2449d1cce89bd6fd23c9f1ea21310ce6e5a1b29c4" dependencies = [ - "serde 1.0.147", + "serde 1.0.149", ] [[package]] @@ -4881,18 +4897,18 @@ checksum = "34b623917345a631dc9608d5194cc206b3fe6c3554cd1c75b937e55e285254af" [[package]] name = "serde" -version = "1.0.147" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d193d69bae983fc11a79df82342761dfbf28a99fc8d203dca4c3c1b590948965" +checksum = "256b9932320c590e707b94576e3cc1f7c9024d0ee6612dfbcf1cb106cbe8e055" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.147" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f1d362ca8fc9c3e3a7484440752472d68a6caa98f1ab81d99b5dfe517cec852" +checksum = "b4eae9b04cbffdfd550eb462ed33bc6a1b68c935127d008b27444d08380f94e4" dependencies = [ "proc-macro2", "quote", @@ -4919,7 +4935,7 @@ checksum = "020ff22c755c2ed3f8cf162dbb41a7268d934702f3ed3631656ea597e08fc3db" dependencies = [ "itoa 1.0.4", "ryu", - "serde 1.0.147", + "serde 1.0.149", ] [[package]] @@ -4942,7 +4958,7 @@ dependencies = [ "form_urlencoded", "itoa 1.0.4", "ryu", - "serde 1.0.147", + "serde 1.0.149", ] [[package]] @@ -4953,7 +4969,7 @@ checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b" dependencies = [ "indexmap", "ryu", - "serde 1.0.147", + "serde 1.0.149", "yaml-rust", ] @@ -4987,7 +5003,7 @@ checksum = "ba8593196da75d9dc4f69349682bd4c2099f8cde114257d1ef7ef1b33d1aba54" dependencies = [ "cfg-if 1.0.0", "libc", - "nix 0.23.1", + "nix 0.23.2", "rand 0.8.5", "win-sys", ] @@ -5035,7 +5051,7 @@ version = "0.1.0" dependencies = [ "confy", "hbb_common", - "serde 1.0.147", + "serde 1.0.149", "serde_derive", "walkdir", ] @@ -5110,7 +5126,7 @@ dependencies = [ "ed25519", "libc", "libsodium-sys", - "serde 1.0.147", + "serde 1.0.149", ] [[package]] @@ -5218,9 +5234,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.103" +version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d" +checksum = "60b9b43d45702de4c839cb9b51d9f529c5dd26a4aff255b42b1ebc03e88ee908" dependencies = [ "proc-macro2", "quote", @@ -5442,9 +5458,9 @@ dependencies = [ [[package]] name = "time" -version = "0.1.44" +version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" dependencies = [ "libc", "wasi 0.10.0+wasi-snapshot-preview1", @@ -5486,9 +5502,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.22.0" +version = "1.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76ce4a75fb488c605c54bf610f221cea8b0dafb53333c1a67e8ee199dcd2ae3" +checksum = "eab6d665857cc6ca78d6e80303a02cea7a7851e85dfbd77cbdc09bd129f1ef46" dependencies = [ "autocfg 1.1.0", "bytes", @@ -5501,14 +5517,14 @@ dependencies = [ "signal-hook-registry", "socket2 0.4.7", "tokio-macros", - "winapi 0.3.9", + "windows-sys 0.42.0", ] [[package]] name = "tokio-macros" -version = "1.8.0" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484" +checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" dependencies = [ "proc-macro2", "quote", @@ -5566,7 +5582,7 @@ version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" dependencies = [ - "serde 1.0.147", + "serde 1.0.149", ] [[package]] @@ -5652,9 +5668,9 @@ checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" [[package]] name = "typenum" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" [[package]] name = "ucd-trie" @@ -5726,7 +5742,7 @@ dependencies = [ "form_urlencoded", "idna", "percent-encoding", - "serde 1.0.147", + "serde 1.0.149", ] [[package]] @@ -5783,9 +5799,9 @@ dependencies = [ [[package]] name = "vswhom-sys" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22025f6d8eb903ebf920ea6933b70b1e495be37e2cb4099e62c80454aaf57c39" +checksum = "d3b17ae1f6c8a2b28506cd96d412eebf83b4a0ff2cbefeeb952f2f9dfa44ba18" dependencies = [ "cc", "libc", @@ -5905,7 +5921,7 @@ dependencies = [ "bitflags", "downcast-rs", "libc", - "nix 0.24.2", + "nix 0.24.3", "scoped-tls", "wayland-commons", "wayland-scanner", @@ -5918,7 +5934,7 @@ version = "0.29.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8691f134d584a33a6606d9d717b95c4fa20065605f798a3f350d78dced02a902" dependencies = [ - "nix 0.24.2", + "nix 0.24.3", "once_cell", "smallvec", "wayland-sys", @@ -5930,7 +5946,7 @@ version = "0.29.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6865c6b66f13d6257bef1cd40cbfe8ef2f150fb8ebbdb1e8e873455931377661" dependencies = [ - "nix 0.24.2", + "nix 0.24.3", "wayland-client", "xcursor", ] @@ -6009,9 +6025,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.22.5" +version = "0.22.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368bfe657969fb01238bb756d351dcade285e0f6fcbd36dcb23359a5169975be" +checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" dependencies = [ "webpki", ] @@ -6476,9 +6492,9 @@ dependencies = [ [[package]] name = "x11" -version = "2.20.0" +version = "2.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7ae97874a928d821b061fce3d1fc52f08071dd53c89a6102bc06efcac3b2908" +checksum = "c2638d5b9c17ac40575fb54bb461a4b1d2a8d1b4ffcc4ff237d254ec59ddeb82" dependencies = [ "libc", "pkg-config", @@ -6486,9 +6502,9 @@ dependencies = [ [[package]] name = "x11-dl" -version = "2.20.0" +version = "2.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c83627bc137605acc00bb399c7b908ef460b621fc37c953db2b09f88c449ea6" +checksum = "b1536d6965a5d4e573c7ef73a2c15ebcd0b2de3347bdf526c34c297c00ac40f0" dependencies = [ "lazy_static", "libc", @@ -6533,9 +6549,9 @@ dependencies = [ [[package]] name = "zbus" -version = "3.5.0" +version = "3.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a25ae891bd547674b368906552115143031c16c23a0f2f4b2f5f5436ab2e6a9f" +checksum = "938ea6da98c75c2c37a86007bd17fd8e208cbec24e086108c87ece98e9edec0d" dependencies = [ "async-broadcast", "async-channel", @@ -6554,11 +6570,11 @@ dependencies = [ "futures-sink", "futures-util", "hex", - "nix 0.25.0", + "nix 0.25.1", "once_cell", "ordered-stream", "rand 0.8.5", - "serde 1.0.147", + "serde 1.0.149", "serde_repr", "sha1", "static_assertions", @@ -6572,9 +6588,9 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "3.5.0" +version = "3.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8aa37701ce7b3a43632d2b0ad9d4aef602b46be6bdd7fba3b7c5007f9f6eb2c2" +checksum = "45066039ebf3330820e495e854f8b312abb68f0a39e97972d092bd72e8bb3e8e" dependencies = [ "proc-macro-crate 1.2.1", "proc-macro2", @@ -6585,11 +6601,11 @@ dependencies = [ [[package]] name = "zbus_names" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d69bb79b44e1901ed8b217e485d0f01991aec574479b68cb03415f142bc7ae67" +checksum = "6c737644108627748a660d038974160e0cbb62605536091bdfa28fd7f64d43c8" dependencies = [ - "serde 1.0.147", + "serde 1.0.149", "static_assertions", "zvariant", ] @@ -6625,23 +6641,23 @@ dependencies = [ [[package]] name = "zvariant" -version = "3.8.0" +version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c817f416f05fcbc833902f1e6064b72b1778573978cfeac54731451ccc9e207" +checksum = "56f8c89c183461e11867ded456db252eae90874bc6769b7adbea464caa777e51" dependencies = [ "byteorder", "enumflags2", "libc", - "serde 1.0.147", + "serde 1.0.149", "static_assertions", "zvariant_derive", ] [[package]] name = "zvariant_derive" -version = "3.8.0" +version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdd24fffd02794a76eb10109de463444064c88f5adb9e9d1a78488adc332bfef" +checksum = "155247a5d1ab55e335421c104ccd95d64f17cebbd02f50cdbc1c33385f9c4d81" dependencies = [ "proc-macro-crate 1.2.1", "proc-macro2", From cc0c335e1e8b9dbc77a5c8d1275dca62d5307dc7 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sun, 11 Dec 2022 23:43:47 +0800 Subject: [PATCH 1169/2015] fix: rdev compilation --- src/ui_session_interface.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 6b635436d..7d88ce22a 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -822,9 +822,9 @@ impl Session { #[cfg(target_os = "linux")] pub fn grab_hotkeys(&self, _grab: bool) { if _grab { - rdev::enable_grab().ok(); + rdev::enable_grab(); } else { - rdev::disable_grab().ok(); + rdev::disable_grab(); } } @@ -1342,7 +1342,7 @@ impl Session { } _ => Some(event), }; - rdev::start_grab_listen(func) + rdev::start_grab_listen(func); } #[cfg(any(target_os = "windows", target_os = "macos"))] std::thread::spawn(move || { @@ -1610,7 +1610,7 @@ pub fn global_grab_keyboard() { } _ => Some(event), }; - rdev::start_grab_listen(func) + rdev::start_grab_listen(func); } #[cfg(any(target_os = "windows", target_os = "macos"))] From 5a6e879c0a3121a390800327711e518e9be2db29 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 12 Dec 2022 00:14:38 +0800 Subject: [PATCH 1170/2015] update: cursor to 0.0.2 --- flutter/pubspec.lock | 2 +- flutter/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index aec2abcde..5f7c67618 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -392,7 +392,7 @@ packages: name: flutter_custom_cursor url: "https://pub.dartlang.org" source: hosted - version: "0.0.1" + version: "0.0.2" flutter_improved_scrolling: dependency: "direct main" description: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 31caa91fd..a87727f7b 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -65,7 +65,7 @@ dependencies: url: https://github.com/Kingtous/rustdesk_desktop_multi_window ref: 82f9eab81cb2c7bfb938def7a1b399a6279bbc75 freezed_annotation: ^2.0.3 - flutter_custom_cursor: ^0.0.1 + flutter_custom_cursor: ^0.0.2 window_size: git: url: https://github.com/google/flutter-desktop-embedding.git From c5560b66b1ece513e7ac39957731665e67f4bb23 Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Mon, 12 Dec 2022 09:41:46 +0800 Subject: [PATCH 1171/2015] Update Cargo.toml --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 7715ebded..006663a66 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,7 +47,7 @@ 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" -magnum-opus = { git = "https://github.com/SoLongAndThanksForAllThePizza/magnum-opus" } +magnum-opus = { git = "https://github.com/rustdesk/magnum-opus" } dasp = { version = "0.11", features = ["signal", "interpolate-linear", "interpolate"], optional = true } rubato = { version = "0.12", optional = true } samplerate = { version = "0.2", optional = true } From 194e1206b4e212a1a5ae736d47d26919f8e2fc55 Mon Sep 17 00:00:00 2001 From: fufesou Date: Mon, 12 Dec 2022 21:45:46 +0800 Subject: [PATCH 1172/2015] fix macos mmatch Signed-off-by: fufesou --- src/keyboard.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/keyboard.rs b/src/keyboard.rs index b2e19ac73..5159eae80 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -7,7 +7,9 @@ use crate::flutter::FlutterHandler; use crate::ui::remote::SciterHandler; use crate::ui_session_interface::Session; use hbb_common::{log, message_proto::*}; -use rdev::{Event, EventType, Key, GrabError}; +#[cfg(target_os = "linux")] +use rdev::GrabError; +use rdev::{Event, EventType, Key}; use std::{ collections::{HashMap, HashSet}, sync::{ @@ -594,7 +596,8 @@ pub fn legacy_keyboard_mode(event: &Event, key_event: &mut KeyEvent) { } pub fn map_keyboard_mode(event: &Event, key_event: &mut KeyEvent) { - let peer = get_peer_platform(); + let mut peer = get_peer_platform().to_lowercase(); + peer.retain(|c| !c.is_whitespace()); let key = match event.event_type { EventType::KeyPress(key) => { @@ -608,8 +611,8 @@ pub fn map_keyboard_mode(event: &Event, key_event: &mut KeyEvent) { _ => return, }; let keycode: u32 = match peer.as_str() { - "Windows" => rdev::win_keycode_from_key(key).unwrap_or_default().into(), - "MacOS" => rdev::macos_keycode_from_key(key).unwrap_or_default().into(), + "windows" => rdev::win_keycode_from_key(key).unwrap_or_default().into(), + "macos" => rdev::macos_keycode_from_key(key).unwrap_or_default().into(), _ => rdev::linux_keycode_from_key(key).unwrap_or_default().into(), }; key_event.set_chr(keycode); From 98e085419c5d9d693958029454890a33b10102e7 Mon Sep 17 00:00:00 2001 From: fufesou Date: Mon, 12 Dec 2022 21:46:58 +0800 Subject: [PATCH 1173/2015] move rdev grab from main page to remote page Signed-off-by: fufesou --- flutter/lib/desktop/pages/desktop_home_page.dart | 1 - flutter/lib/desktop/screen/desktop_remote_screen.dart | 5 ++++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index e0cb5a676..1e8512b2e 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -438,7 +438,6 @@ class _DesktopHomePageState extends State @override void initState() { super.initState(); - bind.mainStartGrabKeyboard(); _updateTimer = periodic_immediate(const Duration(seconds: 1), () async { await gFFI.serverModel.fetchID(); final url = await bind.mainGetSoftwareUpdateUrl(); diff --git a/flutter/lib/desktop/screen/desktop_remote_screen.dart b/flutter/lib/desktop/screen/desktop_remote_screen.dart index 57886b2f2..e8361a652 100644 --- a/flutter/lib/desktop/screen/desktop_remote_screen.dart +++ b/flutter/lib/desktop/screen/desktop_remote_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/desktop/pages/remote_tab_page.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/desktop/widgets/refresh_wrapper.dart'; import 'package:provider/provider.dart'; @@ -8,7 +9,9 @@ import 'package:provider/provider.dart'; class DesktopRemoteScreen extends StatelessWidget { final Map params; - const DesktopRemoteScreen({Key? key, required this.params}) : super(key: key); + DesktopRemoteScreen({Key? key, required this.params}) : super(key: key) { + bind.mainStartGrabKeyboard(); + } @override Widget build(BuildContext context) { From 3dd43b79ec0409fc38103bed0c7eb0bc3cd993d5 Mon Sep 17 00:00:00 2001 From: "Miguel F. G" <116861809+flusheDData@users.noreply.github.com> Date: Mon, 12 Dec 2022 16:49:26 +0100 Subject: [PATCH 1174/2015] Update es.rs 'Addresbook' update --- src/lang/es.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/es.rs b/src/lang/es.rs index 17c3ddf07..8690f8b3f 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -17,7 +17,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Transfer File", "Transferir archivo"), ("Connect", "Conectar"), ("Recent Sessions", "Sesiones recientes"), - ("Address Book", "Directorio"), + ("Address Book", "Libreta de direcciones"), ("Confirmation", "Confirmación"), ("TCP Tunneling", "Túnel TCP"), ("Remove", "Quitar"), From cd511010a887a74819e1e3194d276630fa6e0477 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 13 Dec 2022 00:10:53 +0800 Subject: [PATCH 1175/2015] update lock --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 3db348fd0..ae5f7b0c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3060,7 +3060,7 @@ dependencies = [ [[package]] name = "magnum-opus" version = "0.4.0" -source = "git+https://github.com/SoLongAndThanksForAllThePizza/magnum-opus#6247071a64af7b18e2d553e235729e6865f63ece" +source = "git+https://github.com/rustdesk/magnum-opus#79be072c939168e907fe851690759dcfd6a326af" dependencies = [ "bindgen 0.59.2", "target_build_utils", From e58f2186ec86d454b64da324b46bad38d1b7ece5 Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 13 Dec 2022 09:47:23 +0800 Subject: [PATCH 1176/2015] don't new hwcodec decoders if option disabled Signed-off-by: 21pages --- libs/scrap/src/common/codec.rs | 22 +++++++++++++++------- libs/scrap/src/common/hwcodec.rs | 3 ++- libs/scrap/src/common/mediacodec.rs | 1 + 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/libs/scrap/src/common/codec.rs b/libs/scrap/src/common/codec.rs index d729342d6..9535e9f3a 100644 --- a/libs/scrap/src/common/codec.rs +++ b/libs/scrap/src/common/codec.rs @@ -218,7 +218,7 @@ impl Encoder { #[inline] pub fn current_hw_encoder_name() -> Option { #[cfg(feature = "hwcodec")] - if check_hwcodec_config() { + if enable_hwcodec_option() { return HwEncoder::current_name().lock().unwrap().clone(); } else { return None; @@ -229,7 +229,7 @@ impl Encoder { pub fn supported_encoding() -> (bool, bool) { #[cfg(feature = "hwcodec")] - if check_hwcodec_config() { + if enable_hwcodec_option() { let best = HwEncoder::best(); ( best.h264.as_ref().map_or(false, |c| c.score > 0), @@ -246,7 +246,7 @@ impl Encoder { impl Decoder { pub fn video_codec_state(_id: &str) -> VideoCodecState { #[cfg(feature = "hwcodec")] - if check_hwcodec_config() { + if enable_hwcodec_option() { let best = HwDecoder::best(); return VideoCodecState { score_vpx: SCORE_VPX, @@ -257,7 +257,7 @@ impl Decoder { }; } #[cfg(feature = "mediacodec")] - if check_hwcodec_config() { + if enable_hwcodec_option() { let score_h264 = if H264_DECODER_SUPPORT.load(std::sync::atomic::Ordering::SeqCst) { 92 } else { @@ -287,11 +287,19 @@ impl Decoder { Decoder { vpx, #[cfg(feature = "hwcodec")] - hw: HwDecoder::new_decoders(), + hw: if enable_hwcodec_option() { + HwDecoder::new_decoders() + } else { + HwDecoders::default() + }, #[cfg(feature = "hwcodec")] i420: vec![], #[cfg(feature = "mediacodec")] - media_codec: MediaCodecDecoder::new_decoders(), + media_codec: if enable_hwcodec_option() { + MediaCodecDecoder::new_decoders() + } else { + MediaCodecDecoders::default() + }, } } @@ -415,7 +423,7 @@ impl Decoder { } #[cfg(any(feature = "hwcodec", feature = "mediacodec"))] -fn check_hwcodec_config() -> bool { +fn enable_hwcodec_option() -> bool { if let Some(v) = Config2::get().options.get("enable-hwcodec") { return v != "N"; } diff --git a/libs/scrap/src/common/hwcodec.rs b/libs/scrap/src/common/hwcodec.rs index 166f7516c..c77da3f8f 100644 --- a/libs/scrap/src/common/hwcodec.rs +++ b/libs/scrap/src/common/hwcodec.rs @@ -94,7 +94,7 @@ impl EncoderApi for HwEncoder { frames.push(EncodedVideoFrame { data: Bytes::from(frame.data), pts: frame.pts as _, - key:frame.key == 1, + key: frame.key == 1, ..Default::default() }); } @@ -175,6 +175,7 @@ pub struct HwDecoder { pub info: CodecInfo, } +#[derive(Default)] pub struct HwDecoders { pub h264: Option, pub h265: Option, diff --git a/libs/scrap/src/common/mediacodec.rs b/libs/scrap/src/common/mediacodec.rs index fa821246c..406baecb5 100644 --- a/libs/scrap/src/common/mediacodec.rs +++ b/libs/scrap/src/common/mediacodec.rs @@ -37,6 +37,7 @@ impl Deref for MediaCodecDecoder { } } +#[derive(Default)] pub struct MediaCodecDecoders { pub h264: Option, pub h265: Option, From d08eb0441c2536014751351147237dd0e1ea1d44 Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 13 Dec 2022 16:25:05 +0800 Subject: [PATCH 1177/2015] log Signed-off-by: 21pages --- libs/scrap/src/common/hwcodec.rs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/libs/scrap/src/common/hwcodec.rs b/libs/scrap/src/common/hwcodec.rs index c77da3f8f..55c2309b5 100644 --- a/libs/scrap/src/common/hwcodec.rs +++ b/libs/scrap/src/common/hwcodec.rs @@ -190,22 +190,28 @@ impl HwDecoder { } pub fn new_decoders() -> HwDecoders { + flog("enter new_decoders"); let best = HwDecoder::best(); + flog(&format!("best:${:?}", best)); let mut h264: Option = None; let mut h265: Option = None; let mut fail = false; if let Some(info) = best.h264 { + flog(&format!("before new h264 codec")); h264 = HwDecoder::new(info).ok(); if h264.is_none() { fail = true; } + flog(&format!("new h264 codec result:{:}", h264.is_some())); } if let Some(info) = best.h265 { + flog(&format!("before new h265 codec")); h265 = HwDecoder::new(info).ok(); if h265.is_none() { fail = true; } + flog(&format!("new h265 codec result:{:}", h265.is_some())); } if fail { check_config_process(true); @@ -322,11 +328,21 @@ pub fn check_config_process(force_reset: bool) { } if let Ok(exe) = std::env::current_exe() { std::thread::spawn(move || { - std::process::Command::new(exe) + let result = std::process::Command::new(exe) .arg("--check-hwcodec-config") .status() .ok(); + flog(&format!("check codec process run result:{:?}", result)); HwCodecConfig::refresh(); }); }; } + +pub fn flog(s: &str) { + use hbb_common::chrono::prelude::*; + use std::io::Write; + let mut option = std::fs::OpenOptions::new(); + if let Ok(mut f) = option.append(true).create(true).open("/tmp/log.txt") { + write!(&mut f, "{:?} {}\n", Local::now(), s).ok(); + } +} From 72594c7e0e3baa8238d6568c455f3437eebe997c Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 13 Dec 2022 21:30:08 +0800 Subject: [PATCH 1178/2015] remove hwcodec log Signed-off-by: 21pages --- libs/scrap/src/common/hwcodec.rs | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/libs/scrap/src/common/hwcodec.rs b/libs/scrap/src/common/hwcodec.rs index 55c2309b5..c77da3f8f 100644 --- a/libs/scrap/src/common/hwcodec.rs +++ b/libs/scrap/src/common/hwcodec.rs @@ -190,28 +190,22 @@ impl HwDecoder { } pub fn new_decoders() -> HwDecoders { - flog("enter new_decoders"); let best = HwDecoder::best(); - flog(&format!("best:${:?}", best)); let mut h264: Option = None; let mut h265: Option = None; let mut fail = false; if let Some(info) = best.h264 { - flog(&format!("before new h264 codec")); h264 = HwDecoder::new(info).ok(); if h264.is_none() { fail = true; } - flog(&format!("new h264 codec result:{:}", h264.is_some())); } if let Some(info) = best.h265 { - flog(&format!("before new h265 codec")); h265 = HwDecoder::new(info).ok(); if h265.is_none() { fail = true; } - flog(&format!("new h265 codec result:{:}", h265.is_some())); } if fail { check_config_process(true); @@ -328,21 +322,11 @@ pub fn check_config_process(force_reset: bool) { } if let Ok(exe) = std::env::current_exe() { std::thread::spawn(move || { - let result = std::process::Command::new(exe) + std::process::Command::new(exe) .arg("--check-hwcodec-config") .status() .ok(); - flog(&format!("check codec process run result:{:?}", result)); HwCodecConfig::refresh(); }); }; } - -pub fn flog(s: &str) { - use hbb_common::chrono::prelude::*; - use std::io::Write; - let mut option = std::fs::OpenOptions::new(); - if let Ok(mut f) = option.append(true).create(true).open("/tmp/log.txt") { - write!(&mut f, "{:?} {}\n", Local::now(), s).ok(); - } -} From 856f84d37adb5edf594718363ddc6b39bda7bffb Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 14 Dec 2022 00:32:03 +0800 Subject: [PATCH 1179/2015] add a debug info --- src/core_main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core_main.rs b/src/core_main.rs index 342f438ee..eb8721563 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -173,7 +173,7 @@ pub fn core_main() -> Option> { crate::start_os_service(); return None; } else if args[0] == "--server" { - log::info!("start --server"); + log::info!("start --server with user {}", crate::username()); #[cfg(target_os = "windows")] { crate::start_server(true); From 5ee3e3f347edd32f32610151554f6a7d2e993ce2 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 14 Dec 2022 00:51:43 +0800 Subject: [PATCH 1180/2015] fix Issue #1244 --- src/platform/windows.rs | 10 ++++++---- src/windows.cc | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 075f7ed08..a2a99800f 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -439,6 +439,7 @@ extern "C" { fn win32_disable_lowlevel_keyboard(hwnd: HWND); fn win_stop_system_key_propagate(v: BOOL); fn is_win_down() -> BOOL; + fn is_local_system() -> BOOL; } extern "system" { @@ -718,10 +719,10 @@ pub fn set_share_rdp(enable: bool) { } pub fn get_active_username() -> String { - let name = crate::username(); - if name != "SYSTEM" { - return name; + if !is_root() { + return crate::username(); } + extern "C" { fn get_active_user(path: *mut u16, n: u32, rdp: BOOL) -> u32; } @@ -757,7 +758,8 @@ pub fn is_prelogin() -> bool { } pub fn is_root() -> bool { - crate::username() == "SYSTEM" + // https://stackoverflow.com/questions/4023586/correct-way-to-find-out-if-a-service-is-running-as-the-system-user + unsafe { is_local_system() == TRUE } } pub fn lock_screen() { diff --git a/src/windows.cc b/src/windows.cc index dd3fa2e9e..137ae399e 100644 --- a/src/windows.cc +++ b/src/windows.cc @@ -588,4 +588,44 @@ extern "C" stop_system_key_propagate = v; } + // https://stackoverflow.com/questions/4023586/correct-way-to-find-out-if-a-service-is-running-as-the-system-user + BOOL is_local_system() + { + HANDLE hToken; + UCHAR bTokenUser[sizeof(TOKEN_USER) + 8 + 4 * SID_MAX_SUB_AUTHORITIES]; + PTOKEN_USER pTokenUser = (PTOKEN_USER)bTokenUser; + ULONG cbTokenUser; + SID_IDENTIFIER_AUTHORITY siaNT = SECURITY_NT_AUTHORITY; + PSID pSystemSid; + BOOL bSystem; + + // open process token + if (!OpenProcessToken(GetCurrentProcess(), + TOKEN_QUERY, + &hToken)) + return FALSE; + + // retrieve user SID + if (!GetTokenInformation(hToken, TokenUser, pTokenUser, + sizeof(bTokenUser), &cbTokenUser)) + { + CloseHandle(hToken); + return FALSE; + } + + CloseHandle(hToken); + + // allocate LocalSystem well-known SID + if (!AllocateAndInitializeSid(&siaNT, 1, SECURITY_LOCAL_SYSTEM_RID, + 0, 0, 0, 0, 0, 0, 0, &pSystemSid)) + return FALSE; + + // compare the user SID from the token with the LocalSystem SID + bSystem = EqualSid(pTokenUser->User.Sid, pSystemSid); + + FreeSid(pSystemSid); + + return bSystem; + } + } // end of extern "C" \ No newline at end of file From 422f7b53596d9f023717fdbc0b7a29cfeb543937 Mon Sep 17 00:00:00 2001 From: asur4s Date: Tue, 13 Dec 2022 14:40:22 -0800 Subject: [PATCH 1181/2015] refacotr: sync status --- src/server/input_service.rs | 185 ++++++++++++++++-------------------- 1 file changed, 81 insertions(+), 104 deletions(-) diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 695c6f3d5..e1355785d 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -766,55 +766,92 @@ fn rdev_key_down_or_up(key: RdevKey, down_or_up: bool) { std::thread::sleep(Duration::from_millis(20)); } -fn sync_status(evt: &KeyEvent) { +fn is_modifier_in_key_event(modifier: ControlKey, key_event: &KeyEvent) -> bool { + key_event + .modifiers + .iter() + .position(|&m| m == modifier.into()) + .is_some() +} + +fn is_not_same_status(client_locking: bool, remote_locking: bool) -> bool { + client_locking != remote_locking +} + +#[cfg(target_os = "windows")] +fn has_numpad_key(key_event: &KeyEvent) -> bool { + key_event + .modifiers + .iter() + .filter(|&&ck| NUMPAD_KEY_MAP.get(&ck.value()).is_some()) + .count() + != 0 +} + +#[cfg(target_os = "windows")] +fn is_rdev_numpad_key(key_event: &KeyEvent) -> bool { + let code = key_event.chr(); + let key = rdev::get_win_key(code, 0); + match key { + RdevKey::Home + | RdevKey::UpArrow + | RdevKey::PageUp + | RdevKey::LeftArrow + | RdevKey::RightArrow + | RdevKey::End + | RdevKey::DownArrow + | RdevKey::PageDown + | RdevKey::Insert + | RdevKey::Delete => true, + _ => false, + } +} + +#[cfg(target_os = "windows")] +fn is_numlock_disabled(key_event: &KeyEvent) -> bool { + // disable numlock if press home etc when numlock is on, + // because we will get numpad value (7,8,9 etc) if not + match key_event.mode.unwrap() { + KeyboardMode::Map => is_rdev_numpad_key(key_event), + _ => has_numpad_key(key_event), + } +} + +fn click_capslock(en: &mut Enigo) { + #[cfg(not(targe_os = "macos"))] + en.key_click(enigo::Key::CapsLock); + #[cfg(target_os = "macos")] + en.key_down(enigo::Key::CapsLock); +} + +fn click_numlock(en: &mut Enigo) { + // without numlock in macos + #[cfg(not(target_os = "macos"))] + en.key_click(enigo::Key::NumLock); +} + +fn sync_status(key_event: &KeyEvent) { let mut en = ENIGO.lock().unwrap(); - // remote caps status - let caps_locking = evt - .modifiers - .iter() - .position(|&r| r == ControlKey::CapsLock.into()) - .is_some(); - // remote numpad status - let num_locking = evt - .modifiers - .iter() - .position(|&r| r == ControlKey::NumLock.into()) - .is_some(); + let client_caps_locking = is_modifier_in_key_event(ControlKey::CapsLock, key_event); + let client_num_locking = is_modifier_in_key_event(ControlKey::NumLock, key_event); + let remote_caps_locking = en.get_key_state(enigo::Key::CapsLock); + let remote_num_locking = en.get_key_state(enigo::Key::NumLock); - let click_capslock = (caps_locking && !en.get_key_state(enigo::Key::CapsLock)) - || (!caps_locking && en.get_key_state(enigo::Key::CapsLock)); - let click_numlock = (num_locking && !en.get_key_state(enigo::Key::NumLock)) - || (!num_locking && en.get_key_state(enigo::Key::NumLock)); - #[cfg(windows)] - let click_numlock = { - let code = evt.chr(); - let key = rdev::get_win_key(code, 0); - match key { - RdevKey::Home - | RdevKey::UpArrow - | RdevKey::PageUp - | RdevKey::LeftArrow - | RdevKey::RightArrow - | RdevKey::End - | RdevKey::DownArrow - | RdevKey::PageDown - | RdevKey::Insert - | RdevKey::Delete => en.get_key_state(enigo::Key::NumLock), - _ => click_numlock, - } - }; + let need_click_capslock = is_not_same_status(client_caps_locking, remote_caps_locking); + let need_click_numlock = is_not_same_status(client_num_locking, remote_num_locking); - if click_capslock { - #[cfg(not(target_os = "macos"))] - en.key_click(enigo::Key::CapsLock); - #[cfg(target_os = "macos")] - en.key_down(enigo::Key::CapsLock); + #[cfg(not(target_os = "windows"))] + let disable_numlock = false; + #[cfg(target_os = "windows")] + let disable_numlock = is_numlock_disabled(key_event); + + if need_click_capslock { + click_capslock(&mut en); } - if click_numlock { - #[cfg(not(target_os = "macos"))] - en.key_click(enigo::Key::NumLock); + if need_click_numlock && !disable_numlock { + click_capslock(&mut en); } } @@ -845,10 +882,7 @@ fn legacy_keyboard_mode(evt: &KeyEvent) { #[cfg(windows)] crate::platform::windows::try_change_desktop(); let mut en = ENIGO.lock().unwrap(); - // disable numlock if press home etc when numlock is on, - // because we will get numpad value (7,8,9 etc) if not - #[cfg(windows)] - let mut _disable_numlock = false; + #[cfg(target_os = "macos")] en.reset_flag(); // When long-pressed the command key, then press and release @@ -896,14 +930,6 @@ fn legacy_keyboard_mode(evt: &KeyEvent) { match evt.union { Some(key_event::Union::ControlKey(ck)) => { if let Some(key) = KEY_MAP.get(&ck.value()) { - #[cfg(windows)] - if let Some(_) = NUMPAD_KEY_MAP.get(&ck.value()) { - _disable_numlock = en.get_key_state(Key::NumLock); - if _disable_numlock { - en.key_down(Key::NumLock).ok(); - en.key_up(Key::NumLock); - } - } if evt.down { en.key_down(key.clone()).ok(); KEYS_DOWN @@ -1001,52 +1027,3 @@ async fn send_sas() -> ResultType<()> { timeout(1000, stream.send(&crate::ipc::Data::SAS)).await??; Ok(()) } - -#[cfg(test)] -mod test { - use super::*; - use rdev::{listen, Event, EventType, Key}; - use std::sync::mpsc; - - #[test] - fn test_handle_key() { - // listen - let (tx, rx) = mpsc::channel(); - std::thread::spawn(move || { - std::env::set_var("KEYBOARD_ONLY", "y"); - let func = move |event: Event| { - tx.send(event).ok(); - }; - if let Err(error) = listen(func) { - println!("Error: {:?}", error); - } - }); - // set key/char base on char - let mut evt = KeyEvent::new(); - evt.set_chr(66); - evt.mode = KeyboardMode::Legacy.into(); - - evt.modifiers.push(ControlKey::CapsLock.into()); - - // press - evt.down = true; - handle_key(&evt); - if let Ok(listen_evt) = rx.recv() { - assert_eq!(listen_evt.event_type, EventType::KeyPress(Key::Num1)) - } - // release - evt.down = false; - handle_key(&evt); - if let Ok(listen_evt) = rx.recv() { - assert_eq!(listen_evt.event_type, EventType::KeyRelease(Key::Num1)) - } - } - #[test] - fn test_get_key_state() { - let mut en = ENIGO.lock().unwrap(); - println!( - "[*] test_get_key_state: {:?}", - en.get_key_state(enigo::Key::NumLock) - ); - } -} From 14e21863bab561031bf0b60dcded0c0cd2633ff4 Mon Sep 17 00:00:00 2001 From: asur4s Date: Tue, 13 Dec 2022 15:33:41 -0800 Subject: [PATCH 1182/2015] update deps --- flutter/pubspec.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index d79ff0595..c7008dbfe 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -154,7 +154,7 @@ packages: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.2.1" charcode: dependency: transitive description: @@ -175,7 +175,7 @@ packages: name: clock url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.1.1" code_builder: dependency: transitive description: @@ -623,14 +623,14 @@ packages: name: material_color_utilities url: "https://pub.dartlang.org" source: hosted - version: "0.1.4" + version: "0.1.5" meta: dependency: transitive description: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.7.0" + version: "1.8.0" mime: dependency: transitive description: @@ -707,7 +707,7 @@ packages: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.1" + version: "1.8.2" path_drawing: dependency: transitive description: From 880a0d4209eb1d74d47d4e61fd7275a38b4a19ba Mon Sep 17 00:00:00 2001 From: 21pages Date: Sun, 11 Dec 2022 21:40:35 +0800 Subject: [PATCH 1183/2015] add group peer card Signed-off-by: 21pages --- flutter/lib/common.dart | 9 +- flutter/lib/common/hbbs/hbbs.dart | 39 ++ flutter/lib/common/widgets/address_book.dart | 7 +- flutter/lib/common/widgets/my_group.dart | 183 +++++++++ flutter/lib/common/widgets/peer_card.dart | 39 ++ flutter/lib/common/widgets/peer_tab_page.dart | 353 ++++++++++++------ flutter/lib/common/widgets/peers_view.dart | 18 + .../desktop/pages/desktop_setting_page.dart | 22 +- flutter/lib/main.dart | 1 + flutter/lib/mobile/pages/settings_page.dart | 1 - flutter/lib/models/ab_model.dart | 12 +- flutter/lib/models/group_model.dart | 139 +++++++ flutter/lib/models/model.dart | 5 +- flutter/lib/models/user_model.dart | 88 ++--- 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/ru.rs | 2 + src/lang/sk.rs | 2 + src/lang/sq.rs | 2 + src/lang/sv.rs | 2 + src/lang/template.rs | 2 + src/lang/tr.rs | 2 + src/lang/tw.rs | 2 + src/lang/ua.rs | 2 + src/lang/vn.rs | 2 + 42 files changed, 777 insertions(+), 195 deletions(-) create mode 100644 flutter/lib/common/hbbs/hbbs.dart create mode 100644 flutter/lib/common/widgets/my_group.dart create mode 100644 flutter/lib/models/group_model.dart diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 0f5502f54..d8a6cd30f 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -99,22 +99,28 @@ class IconFont { class ColorThemeExtension extends ThemeExtension { const ColorThemeExtension({ required this.border, + required this.highlight, }); final Color? border; + final Color? highlight; static const light = ColorThemeExtension( border: Color(0xFFCCCCCC), + highlight: Color(0xFFE5E5E5), ); static const dark = ColorThemeExtension( border: Color(0xFF555555), + highlight: Color(0xFF3F3F3F), ); @override - ThemeExtension copyWith({Color? border}) { + ThemeExtension copyWith( + {Color? border, Color? highlight}) { return ColorThemeExtension( border: border ?? this.border, + highlight: highlight ?? this.highlight, ); } @@ -126,6 +132,7 @@ class ColorThemeExtension extends ThemeExtension { } return ColorThemeExtension( border: Color.lerp(border, other.border, t), + highlight: Color.lerp(highlight, other.highlight, t), ); } } diff --git a/flutter/lib/common/hbbs/hbbs.dart b/flutter/lib/common/hbbs/hbbs.dart new file mode 100644 index 000000000..856f88d20 --- /dev/null +++ b/flutter/lib/common/hbbs/hbbs.dart @@ -0,0 +1,39 @@ +import 'package:flutter_hbb/models/peer_model.dart'; + +class UserPayload { + String name = ''; + String email = ''; + String note = ''; + int? status; + String grp = ''; + bool is_admin = false; + + UserPayload.fromJson(Map json) + : name = json['name'] ?? '', + email = json['email'] ?? '', + note = json['note'] ?? '', + status = json['status'], + grp = json['grp'] ?? '', + is_admin = json['is_admin'] == true; +} + +class PeerPayload { + String id = ''; + String info = ''; + int? status; + String user = ''; + String user_name = ''; + String note = ''; + + PeerPayload.fromJson(Map json) + : id = json['id'] ?? '', + info = json['info'] ?? '', + status = json['status'], + user = json['user'] ?? '', + user_name = json['user_name'] ?? '', + note = json['note'] ?? ''; + + static Peer toPeer(PeerPayload p) { + return Peer.fromJson({"id": p.id}); + } +} diff --git a/flutter/lib/common/widgets/address_book.dart b/flutter/lib/common/widgets/address_book.dart index 799b0be67..fbeca25b2 100644 --- a/flutter/lib/common/widgets/address_book.dart +++ b/flutter/lib/common/widgets/address_book.dart @@ -28,7 +28,6 @@ class _AddressBookState extends State { @override void initState() { super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) => gFFI.abModel.pullAb()); } @override @@ -45,11 +44,7 @@ class _AddressBookState extends State { handleLogin() { // TODO refactor login dialog for desktop and mobile if (isDesktop) { - loginDialog().then((success) { - if (success) { - gFFI.abModel.pullAb(); - } - }); + loginDialog(); } else { showLogin(gFFI.dialogManager); } diff --git a/flutter/lib/common/widgets/my_group.dart b/flutter/lib/common/widgets/my_group.dart new file mode 100644 index 000000000..77ddf779b --- /dev/null +++ b/flutter/lib/common/widgets/my_group.dart @@ -0,0 +1,183 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common/widgets/peers_view.dart'; +import 'package:get/get.dart'; + +import '../../common.dart'; + +class MyGroup extends StatefulWidget { + final EdgeInsets? menuPadding; + const MyGroup({Key? key, this.menuPadding}) : super(key: key); + + @override + State createState() { + return _MyGroupState(); + } +} + +class _MyGroupState extends State { + static final RxString selectedUser = ''.obs; + static final RxString searchUserText = ''.obs; + static TextEditingController searchUserController = TextEditingController(); + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) => FutureBuilder( + future: buildBody(context), + builder: (context, snapshot) { + if (snapshot.hasData) { + return snapshot.data!; + } else { + return const Offstage(); + } + }); + + Future buildBody(BuildContext context) async { + return Obx(() { + if (gFFI.groupModel.userLoading.value) { + return const Center( + child: CircularProgressIndicator(), + ); + } + if (gFFI.groupModel.userLoadError.isNotEmpty) { + return _buildShowError(gFFI.groupModel.userLoadError.value); + } + return Row( + children: [ + _buildLeftDesktop(), + Expanded( + child: Align( + alignment: Alignment.topLeft, + child: MyGroupPeerView( + menuPadding: widget.menuPadding, + initPeers: gFFI.groupModel.peersShow.value)), + ) + ], + ); + }); + } + + Widget _buildShowError(String error) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(translate(error)), + TextButton( + onPressed: () { + gFFI.groupModel.pull(); + }, + child: Text(translate("Retry"))) + ], + )); + } + + Widget _buildLeftDesktop() { + return Row( + children: [ + Card( + margin: EdgeInsets.symmetric(horizontal: 4.0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: + BorderSide(color: Theme.of(context).scaffoldBackgroundColor)), + child: Container( + width: 200, + height: double.infinity, + padding: + const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), + child: Column( + children: [ + _buildLeftHeader(), + Expanded( + child: Container( + width: double.infinity, + height: double.infinity, + decoration: + BoxDecoration(borderRadius: BorderRadius.circular(2)), + child: _buildUserContacts(), + ).marginSymmetric(vertical: 8.0), + ) + ], + ), + ), + ).marginOnly(right: 8.0), + ], + ); + } + + Widget _buildLeftHeader() { + return Row( + children: [ + Expanded( + child: TextField( + controller: searchUserController, + onChanged: (value) { + searchUserText.value = value; + }, + decoration: InputDecoration( + prefixIcon: Icon( + Icons.search_rounded, + color: Theme.of(context).hintColor, + ), + contentPadding: const EdgeInsets.symmetric(vertical: 10), + hintText: translate("Search"), + hintStyle: + TextStyle(fontSize: 14, color: Theme.of(context).hintColor), + border: InputBorder.none, + isDense: true, + ), + )), + ], + ); + } + + Widget _buildUserContacts() { + return Obx(() { + return Column( + children: gFFI.groupModel.users + .where((p0) { + if (searchUserText.isNotEmpty) { + return p0.name.contains(searchUserText.value); + } + return true; + }) + .map((e) => _buildUserItem(e.name)) + .toList()); + }); + } + + Widget _buildUserItem(String username) { + return InkWell(onTap: () { + if (selectedUser.value != username) { + selectedUser.value = username; + gFFI.groupModel.pullUserPeers(username); + } + }, child: Obx( + () { + bool selected = selectedUser.value == username; + return Container( + decoration: BoxDecoration( + color: selected ? MyTheme.color(context).highlight : null, + border: Border( + bottom: BorderSide( + width: 0.7, + color: Theme.of(context).dividerColor.withOpacity(0.1))), + ), + child: Container( + child: Row( + children: [ + Icon(Icons.person_outline_rounded, color: Colors.grey, size: 16) + .marginOnly(right: 4), + Expanded(child: Text(username)), + ], + ).paddingSymmetric(vertical: 4), + ), + ); + }, + )).marginSymmetric(horizontal: 12); + } +} diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index 449b67092..44f82575e 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -321,6 +321,7 @@ enum CardType { fav, lan, ab, + grp, } abstract class BasePeerCard extends StatelessWidget { @@ -684,6 +685,9 @@ abstract class BasePeerCard extends StatelessWidget { case CardType.ab: gFFI.abModel.pullAb(); break; + case CardType.grp: + gFFI.groupModel.pull(); + break; } } } @@ -937,6 +941,41 @@ class AddressBookPeerCard extends BasePeerCard { } } +class MyGroupPeerCard extends BasePeerCard { + MyGroupPeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key}) + : super( + peer: peer, + cardType: CardType.grp, + menuPadding: menuPadding, + key: key); + + @override + Future>> _buildMenuItems( + BuildContext context) async { + final List> menuItems = [ + _connectAction(context, peer), + _transferFileAction(context, peer.id), + ]; + if (isDesktop && peer.platform != 'Android') { + menuItems.add(_tcpTunnelingAction(context, peer.id)); + } + menuItems.add(await _forceAlwaysRelayAction(peer.id)); + if (peer.platform == 'Windows') { + menuItems.add(_rdpAction(context, peer.id)); + } + menuItems.add(_wolAction(peer.id)); + if (Platform.isWindows) { + menuItems.add(_createShortCutAction(peer.id)); + } + menuItems.add(MenuEntryDivider()); + menuItems.add(_renameAction(peer.id)); + if (await bind.mainPeerHasPassword(id: peer.id)) { + menuItems.add(_unrememberPasswordAction(peer.id)); + } + return menuItems; + } +} + void _rdpDialog(String id, CardType card) async { String port, username; if (card == CardType.ab) { diff --git a/flutter/lib/common/widgets/peer_tab_page.dart b/flutter/lib/common/widgets/peer_tab_page.dart index 1711e7b72..f6f5c0403 100644 --- a/flutter/lib/common/widgets/peer_tab_page.dart +++ b/flutter/lib/common/widgets/peer_tab_page.dart @@ -4,6 +4,7 @@ import 'dart:ui' as ui; import 'package:bot_toast/bot_toast.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common/widgets/address_book.dart'; +import 'package:flutter_hbb/common/widgets/my_group.dart'; import 'package:flutter_hbb/common/widgets/peers_view.dart'; import 'package:flutter_hbb/common/widgets/peer_card.dart'; import 'package:flutter_hbb/consts.dart'; @@ -16,6 +17,151 @@ import 'package:get/get.dart'; import '../../common.dart'; import '../../models/platform_model.dart'; +const int groupTabIndex = 4; + +class StatePeerTab { + final RxInt currentTab = 0.obs; + static const List tabIndexs = [0, 1, 2, 3, 4]; + List tabOrder = List.empty(growable: true); + final RxList visibleTabOrder = RxList.empty(growable: true); + int tabHiddenFlag = 0; + final RxList tabNames = [ + translate('Recent Sessions'), + translate('Favorites'), + translate('Discovered'), + translate('Address Book'), + translate('Group'), + ].obs; + + StatePeerTab._() { + tabHiddenFlag = (int.tryParse( + bind.getLocalFlutterConfig(k: 'hidden-peer-card'), + radix: 2) ?? + 0); + currentTab.value = + int.tryParse(bind.getLocalFlutterConfig(k: 'peer-tab-index')) ?? 0; + if (!tabIndexs.contains(currentTab.value)) { + currentTab.value = tabIndexs[0]; + } + tabOrder = tabIndexs.toList(); + try { + final conf = bind.getLocalFlutterConfig(k: 'peer-tab-order'); + if (conf.isNotEmpty) { + final json = jsonDecode(conf); + if (json is List) { + final List list = + json.map((e) => int.tryParse(e.toString()) ?? -1).toList(); + if (list.length == tabOrder.length && + tabOrder.every((e) => list.contains(e))) { + tabOrder = list; + } + } + } + } catch (e) { + debugPrintStack(label: '$e'); + } + visibleTabOrder.value = tabOrder.where((e) => !isTabHidden(e)).toList(); + visibleTabOrder.remove(groupTabIndex); + } + static final StatePeerTab instance = StatePeerTab._(); + + check() { + List oldOrder = visibleTabOrder; + if (filterGroupCard()) { + visibleTabOrder.remove(groupTabIndex); + if (currentTab.value == groupTabIndex) { + currentTab.value = + visibleTabOrder.firstWhereOrNull((e) => e != groupTabIndex) ?? 0; + bind.setLocalFlutterConfig( + k: 'peer-tab-index', v: currentTab.value.toString()); + } + } else { + if (gFFI.userModel.isAdmin.isFalse && + gFFI.userModel.groupName.isNotEmpty) { + tabNames[groupTabIndex] = gFFI.userModel.groupName.value; + } else { + tabNames[groupTabIndex] = translate('Group'); + } + if (isTabHidden(groupTabIndex)) { + visibleTabOrder.remove(groupTabIndex); + } else { + if (!visibleTabOrder.contains(groupTabIndex)) { + addTabInOrder(visibleTabOrder, groupTabIndex); + } + } + if (visibleTabOrder.contains(groupTabIndex) && + int.tryParse(bind.getLocalFlutterConfig(k: 'peer-tab-index')) == + groupTabIndex) { + currentTab.value = groupTabIndex; + } + } + if (oldOrder != visibleTabOrder) { + saveTabOrder(); + } + } + + bool isTabHidden(int tabindex) { + return tabHiddenFlag & (1 << tabindex) != 0; + } + + bool filterGroupCard() { + if (gFFI.groupModel.users.isEmpty || + (gFFI.userModel.isAdmin.isFalse && gFFI.userModel.groupName.isEmpty)) { + return true; + } else { + return false; + } + } + + addTabInOrder(List list, int tabIndex) { + if (!tabOrder.contains(tabIndex) || list.contains(tabIndex)) { + return; + } + bool sameOrder = true; + int lastIndex = -1; + for (int i = 0; i < list.length; i++) { + var index = tabOrder.lastIndexOf(list[i]); + if (index > lastIndex) { + lastIndex = index; + continue; + } else { + sameOrder = false; + break; + } + } + if (sameOrder) { + var indexInTabOrder = tabOrder.indexOf(tabIndex); + var left = List.empty(growable: true); + for (int i = 0; i < indexInTabOrder; i++) { + left.add(tabOrder[i]); + } + int insertIndex = list.lastIndexWhere((e) => left.contains(e)); + if (insertIndex < 0) { + insertIndex = 0; + } else { + insertIndex += 1; + } + list.insert(insertIndex, tabIndex); + } else { + list.add(tabIndex); + } + } + + saveTabOrder() { + var list = statePeerTab.visibleTabOrder.toList(); + var left = tabOrder + .where((e) => !statePeerTab.visibleTabOrder.contains(e)) + .toList(); + for (var t in left) { + addTabInOrder(list, t); + } + statePeerTab.tabOrder = list; + bind.setLocalFlutterConfig(k: 'peer-tab-order', v: jsonEncode(list)); + } +} + +final statePeerTab = StatePeerTab.instance; + class PeerTabPage extends StatefulWidget { const PeerTabPage({Key? key}) : super(key: key); @override @@ -23,10 +169,9 @@ class PeerTabPage extends StatefulWidget { } class _TabEntry { - final String name; final Widget widget; final Function() load; - _TabEntry(this.name, this.widget, this.load); + _TabEntry(this.widget, this.load); } EdgeInsets? _menuPadding() { @@ -35,65 +180,36 @@ EdgeInsets? _menuPadding() { class _PeerTabPageState extends State with SingleTickerProviderStateMixin { - late final RxInt tabHiddenFlag; - late final RxString currentTab; - late final RxList visibleOrderedTabs; final List<_TabEntry> entries = [ _TabEntry( - 'Recent Sessions', RecentPeersView( menuPadding: _menuPadding(), ), bind.mainLoadRecentPeers), _TabEntry( - 'Favorites', FavoritePeersView( menuPadding: _menuPadding(), ), bind.mainLoadFavPeers), _TabEntry( - 'Discovered', DiscoveredPeersView( menuPadding: _menuPadding(), ), bind.mainDiscover), _TabEntry( - 'Address Book', AddressBook( menuPadding: _menuPadding(), ), () => {}), + _TabEntry( + MyGroup( + menuPadding: _menuPadding(), + ), + () => {}), ]; @override void initState() { - tabHiddenFlag = (int.tryParse( - bind.getLocalFlutterConfig(k: 'hidden-peer-card'), - radix: 2) ?? - 0) - .obs; - currentTab = bind.getLocalFlutterConfig(k: 'current-peer-tab').obs; - visibleOrderedTabs = entries - .where((e) => !isTabHidden(e.name)) - .map((e) => e.name) - .toList() - .obs; - try { - final conf = bind.getLocalFlutterConfig(k: 'peer-tab-order'); - if (conf.isNotEmpty) { - final json = jsonDecode(conf); - if (json is List) { - final List list = json.map((e) => e.toString()).toList(); - if (list.length == visibleOrderedTabs.length && - visibleOrderedTabs.every((e) => list.contains(e))) { - visibleOrderedTabs.value = list; - } - } - } - } catch (e) { - debugPrintStack(label: '$e'); - } - adjustTab(); final uiType = bind.getLocalFlutterConfig(k: 'peer-card-ui-type'); @@ -105,10 +221,11 @@ class _PeerTabPageState extends State super.initState(); } - Future handleTabSelection(String tabName) async { - currentTab.value = tabName; - await bind.setLocalFlutterConfig(k: 'current-peer-tab', v: tabName); - entries.firstWhereOrNull((e) => e.name == tabName)?.load(); + Future handleTabSelection(int tabIndex) async { + if (tabIndex < entries.length) { + statePeerTab.currentTab.value = tabIndex; + entries[tabIndex].load(); + } } @override @@ -148,25 +265,26 @@ class _PeerTabPageState extends State Widget _createSwitchBar(BuildContext context) { final textColor = Theme.of(context).textTheme.titleLarge?.color; + statePeerTab.visibleTabOrder + .removeWhere((e) => !StatePeerTab.tabIndexs.contains(e)); return Obx(() { int indexCounter = -1; return ReorderableListView( buildDefaultDragHandles: false, onReorder: (oldIndex, newIndex) { - var list = visibleOrderedTabs.toList(); + var list = statePeerTab.visibleTabOrder.toList(); if (oldIndex < newIndex) { newIndex -= 1; } - final String item = list.removeAt(oldIndex); + final int item = list.removeAt(oldIndex); list.insert(newIndex, item); - bind.setLocalFlutterConfig( - k: 'peer-tab-order', v: jsonEncode(list)); - visibleOrderedTabs.value = list; + statePeerTab.visibleTabOrder.value = list; + statePeerTab.saveTabOrder(); }, scrollDirection: Axis.horizontal, shrinkWrap: true, scrollController: ScrollController(), - children: visibleOrderedTabs.map((t) { + children: statePeerTab.visibleTabOrder.map((t) { indexCounter++; return ReorderableDragStartListener( key: ValueKey(t), @@ -175,7 +293,7 @@ class _PeerTabPageState extends State child: Container( padding: const EdgeInsets.symmetric(horizontal: 8), decoration: BoxDecoration( - color: currentTab.value == t + color: statePeerTab.currentTab.value == t ? Theme.of(context).backgroundColor : null, borderRadius: BorderRadius.circular(isDesktop ? 2 : 6), @@ -183,16 +301,22 @@ class _PeerTabPageState extends State child: Align( alignment: Alignment.center, child: Text( - translate(t), + statePeerTab.tabNames[t], // TODO textAlign: TextAlign.center, style: TextStyle( height: 1, fontSize: 14, - color: currentTab.value == t ? textColor : textColor + color: statePeerTab.currentTab.value == t + ? textColor + : textColor ?..withOpacity(0.5)), ), )), - onTap: () async => await handleTabSelection(t), + onTap: () async { + await handleTabSelection(t); + await bind.setLocalFlutterConfig( + k: 'peer-tab-index', v: t.toString()); + }, ), ); }).toList()); @@ -201,13 +325,24 @@ class _PeerTabPageState extends State Widget _createPeersView() { final verticalMargin = isDesktop ? 12.0 : 6.0; + statePeerTab.visibleTabOrder + .removeWhere((e) => !StatePeerTab.tabIndexs.contains(e)); return Expanded( - child: Obx(() => - entries.firstWhereOrNull((e) => e.name == currentTab.value)?.widget ?? - visibleContextMenuListener(Center( - child: Text(translate('Right click to select tabs')), - ))).marginSymmetric(vertical: verticalMargin), - ); + child: Obx(() { + if (statePeerTab.visibleTabOrder.isEmpty) { + return visibleContextMenuListener(Center( + child: Text(translate('Right click to select tabs')), + )); + } else { + if (statePeerTab.visibleTabOrder + .contains(statePeerTab.currentTab.value)) { + return entries[statePeerTab.currentTab.value].widget; + } else { + statePeerTab.currentTab.value = statePeerTab.visibleTabOrder[0]; + return entries[statePeerTab.currentTab.value].widget; + } + } + }).marginSymmetric(vertical: verticalMargin)); } Widget _createPeerViewTypeSwitch(BuildContext context) { @@ -240,22 +375,14 @@ class _PeerTabPageState extends State ); } - bool isTabHidden(String name) { - int index = entries.indexWhere((e) => e.name == name); - if (index >= 0) { - return tabHiddenFlag & (1 << index) != 0; - } - assert(false); - return false; - } - adjustTab() { - if (visibleOrderedTabs.isNotEmpty) { - if (!visibleOrderedTabs.contains(currentTab.value)) { - handleTabSelection(visibleOrderedTabs[0]); + if (statePeerTab.visibleTabOrder.isNotEmpty) { + if (!statePeerTab.visibleTabOrder + .contains(statePeerTab.currentTab.value)) { + handleTabSelection(statePeerTab.visibleTabOrder[0]); } } else { - currentTab.value = ''; + statePeerTab.currentTab.value = 0; } } @@ -278,47 +405,53 @@ class _PeerTabPageState extends State } Widget visibleContextMenu(CancelFunc cancelFunc) { - final List menu = entries.asMap().entries.map((e) { - int bitMask = 1 << e.key; - return MenuEntrySwitch( - switchType: SwitchType.scheckbox, - text: translate(e.value.name), - getter: () async { - return tabHiddenFlag.value & bitMask == 0; - }, - setter: (show) async { - if (show) { - tabHiddenFlag.value &= ~bitMask; - } else { - tabHiddenFlag.value |= bitMask; - } - await bind.setLocalFlutterConfig( - k: 'hidden-peer-card', v: tabHiddenFlag.value.toRadixString(2)); - visibleOrderedTabs.removeWhere((e) => isTabHidden(e)); - visibleOrderedTabs.addAll(entries - .where((e) => - !visibleOrderedTabs.contains(e.name) && - !isTabHidden(e.name)) - .map((e) => e.name) - .toList()); - await bind.setLocalFlutterConfig( - k: 'peer-tab-order', v: jsonEncode(visibleOrderedTabs)); - cancelFunc(); - adjustTab(); - }); - }).toList(); - return mod_menu.PopupMenu( - items: menu - .map((entry) => entry.build( - context, - const MenuConfig( - commonColor: MyTheme.accent, - height: 20.0, - dividerHeight: 12.0, - ))) - .expand((i) => i) - .toList(), - ); + return Obx(() { + final List menu = List.empty(growable: true); + for (int i = 0; i < statePeerTab.tabNames.length; i++) { + if (i == groupTabIndex && statePeerTab.filterGroupCard()) { + continue; + } + int bitMask = 1 << i; + menu.add(MenuEntrySwitch( + switchType: SwitchType.scheckbox, + text: statePeerTab.tabNames[i], + getter: () async { + return statePeerTab.tabHiddenFlag & bitMask == 0; + }, + setter: (show) async { + if (show) { + statePeerTab.tabHiddenFlag &= ~bitMask; + } else { + statePeerTab.tabHiddenFlag |= bitMask; + } + await bind.setLocalFlutterConfig( + k: 'hidden-peer-card', + v: statePeerTab.tabHiddenFlag.toRadixString(2)); + statePeerTab.visibleTabOrder + .removeWhere((e) => statePeerTab.isTabHidden(e)); + for (int j = 0; j < statePeerTab.tabNames.length; j++) { + if (!statePeerTab.visibleTabOrder.contains(j) && + !statePeerTab.isTabHidden(j)) { + statePeerTab.visibleTabOrder.add(j); + } + } + statePeerTab.saveTabOrder(); + cancelFunc(); + adjustTab(); + })); + } + return mod_menu.PopupMenu( + items: menu + .map((entry) => entry.build( + context, + const MenuConfig( + commonColor: MyTheme.accent, + height: 20.0, + dividerHeight: 12.0, + ))) + .expand((i) => i) + .toList()); + }); } } diff --git a/flutter/lib/common/widgets/peers_view.dart b/flutter/lib/common/widgets/peers_view.dart index 6e52bfeb8..9c98f24b8 100644 --- a/flutter/lib/common/widgets/peers_view.dart +++ b/flutter/lib/common/widgets/peers_view.dart @@ -326,3 +326,21 @@ class AddressBookPeersView extends BasePeersView { return true; } } + +class MyGroupPeerView extends BasePeersView { + MyGroupPeerView( + {Key? key, + EdgeInsets? menuPadding, + ScrollController? scrollController, + required List initPeers}) + : super( + key: key, + name: 'my group peer', + loadEvent: 'load_my_group_peers', + peerCardBuilder: (Peer peer) => MyGroupPeerCard( + peer: peer, + menuPadding: menuPadding, + ), + initPeers: initPeers, + ); +} diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 06cabebe7..422b4d3e1 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -1059,21 +1059,13 @@ class _AccountState extends State<_Account> { } Widget accountAction() { - return _futureBuilder(future: () async { - return await gFFI.userModel.getUserName(); - }(), hasData: (_) { - return Obx(() => _Button( - gFFI.userModel.userName.value.isEmpty ? 'Login' : 'Logout', - () => { - gFFI.userModel.userName.value.isEmpty - ? loginDialog().then((success) { - if (success) { - gFFI.abModel.pullAb(); - } - }) - : gFFI.userModel.logOut() - })); - }); + return Obx(() => _Button( + gFFI.userModel.userName.value.isEmpty ? 'Login' : 'Logout', + () => { + gFFI.userModel.userName.value.isEmpty + ? loginDialog() + : gFFI.userModel.logOut() + })); } } diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 2015c02b2..6d09ef139 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -117,6 +117,7 @@ void runMainApp(bool startService) async { // await windowManager.ensureInitialized(); gFFI.serverModel.startService(); } + gFFI.userModel.refreshCurrentUser(); runApp(App()); // restore the location of the main window before window hide or show await restoreWindowPosition(WindowType.Main); diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index 269439b1d..8c7cdb5c7 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -547,7 +547,6 @@ void showLogin(OverlayDialogManager dialogManager) { error = resp['error']; return; } - gFFI.abModel.pullAb(); } close(); }, diff --git a/flutter/lib/models/ab_model.dart b/flutter/lib/models/ab_model.dart index ab5a7cb80..d8a0e8f99 100644 --- a/flutter/lib/models/ab_model.dart +++ b/flutter/lib/models/ab_model.dart @@ -21,10 +21,8 @@ class AbModel { AbModel(this.parent); - FFI? get _ffi => parent.target; - Future pullAb() async { - if (_ffi!.userModel.userName.isEmpty) return; + if (gFFI.userModel.userName.isEmpty) return; abLoading.value = true; abError.value = ""; final api = "${await bind.mainGetApiServer()}/api/ab/get"; @@ -63,7 +61,8 @@ class AbModel { return null; } - void reset() { + Future reset() async { + await bind.mainSetLocalOption(key: "selected-tags", value: ''); tags.clear(); peers.clear(); } @@ -188,9 +187,4 @@ class AbModel { await pushAb(); } } - - void clear() { - peers.clear(); - tags.clear(); - } } diff --git a/flutter/lib/models/group_model.dart b/flutter/lib/models/group_model.dart new file mode 100644 index 000000000..4dfcf189b --- /dev/null +++ b/flutter/lib/models/group_model.dart @@ -0,0 +1,139 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/common/widgets/peer_tab_page.dart'; +import 'package:flutter_hbb/common/hbbs/hbbs.dart'; +import 'package:flutter_hbb/models/model.dart'; +import 'package:flutter_hbb/models/peer_model.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; +import 'package:get/get.dart'; +import 'dart:convert'; +import 'package:http/http.dart' as http; + +class GroupModel { + final RxBool userLoading = false.obs; + final RxString userLoadError = "".obs; + final RxBool peerLoading = false.obs; //to-do: not used + final RxString peerLoadError = "".obs; + final RxList users = RxList.empty(growable: true); + final RxList peerPayloads = RxList.empty(growable: true); + final RxList peersShow = RxList.empty(growable: true); + WeakReference parent; + + GroupModel(this.parent); + + Future reset() async { + userLoading.value = false; + userLoadError.value = ""; + peerLoading.value = false; + peerLoadError.value = ""; + users.clear(); + peerPayloads.clear(); + peersShow.clear(); + } + + Future pull() async { + await reset(); + if (gFFI.userModel.userName.isEmpty || + (gFFI.userModel.isAdmin.isFalse && gFFI.userModel.groupName.isEmpty)) { + statePeerTab.check(); + return; + } + userLoading.value = true; + userLoadError.value = ""; + final api = "${await bind.mainGetApiServer()}/api/users"; + try { + var uri0 = Uri.parse(api); + final pageSize = 20; + var total = 0; + int current = 1; + do { + var uri = Uri( + scheme: uri0.scheme, + host: uri0.host, + path: uri0.path, + port: uri0.port, + queryParameters: { + 'current': current.toString(), + 'pageSize': pageSize.toString(), + if (gFFI.userModel.isAdmin.isFalse) + 'grp': gFFI.userModel.groupName.value, + }); + current += pageSize; + final resp = await http.get(uri, headers: await getHttpHeaders()); + if (resp.body.isNotEmpty && resp.body.toLowerCase() != "null") { + Map json = jsonDecode(resp.body); + if (json.containsKey('error')) { + throw json['error']; + } else { + total = json['total']; + if (json.containsKey('data')) { + final data = json['data']; + if (data is List) { + for (final user in data) { + users.add(UserPayload.fromJson(user)); + } + } + } + } + } + } while (current < total); + } catch (err) { + debugPrint('$err'); + userLoadError.value = err.toString(); + } finally { + userLoading.value = false; + statePeerTab.check(); + } + } + + Future pullUserPeers(String username) async { + peerPayloads.clear(); + peersShow.clear(); + peerLoading.value = true; + peerLoadError.value = ""; + final api = "${await bind.mainGetApiServer()}/api/peers"; + try { + var uri0 = Uri.parse(api); + final pageSize = 20; + var total = 0; + int current = 1; + do { + var uri = Uri( + scheme: uri0.scheme, + host: uri0.host, + path: uri0.path, + port: uri0.port, + queryParameters: { + 'current': current.toString(), + 'pageSize': pageSize.toString(), + 'user_name': username + }); + current += pageSize; + final resp = await http.get(uri, headers: await getHttpHeaders()); + if (resp.body.isNotEmpty && resp.body.toLowerCase() != "null") { + Map json = jsonDecode(resp.body); + if (json.containsKey('error')) { + throw json['error']; + } else { + total = json['total']; + if (json.containsKey('data')) { + final data = json['data']; + if (data is List) { + for (final p in data) { + final peer = PeerPayload.fromJson(p); + peerPayloads.add(peer); + peersShow.add(PeerPayload.toPeer(peer)); + } + } + } + } + } + } while (current < total); + } catch (err) { + debugPrint('$err'); + peerLoadError.value = err.toString(); + } finally { + peerLoading.value = false; + } + } +} diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 37c246fe1..3659e8d58 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -12,6 +12,7 @@ import 'package:flutter_hbb/generated_bridge.dart'; import 'package:flutter_hbb/models/ab_model.dart'; import 'package:flutter_hbb/models/chat_model.dart'; import 'package:flutter_hbb/models/file_model.dart'; +import 'package:flutter_hbb/models/group_model.dart'; import 'package:flutter_hbb/models/server_model.dart'; import 'package:flutter_hbb/models/user_model.dart'; import 'package:flutter_hbb/models/state_model.dart'; @@ -1221,6 +1222,7 @@ class FFI { late final ChatModel chatModel; // session late final FileModel fileModel; // session late final AbModel abModel; // global + late final GroupModel groupModel; // global late final UserModel userModel; // global late final QualityMonitorModel qualityMonitorModel; // session late final RecordingModel recordingModel; // recording @@ -1234,8 +1236,9 @@ class FFI { serverModel = ServerModel(WeakReference(this)); chatModel = ChatModel(WeakReference(this)); fileModel = FileModel(WeakReference(this)); - abModel = AbModel(WeakReference(this)); userModel = UserModel(WeakReference(this)); + abModel = AbModel(WeakReference(this)); + groupModel = GroupModel(WeakReference(this)); qualityMonitorModel = QualityMonitorModel(WeakReference(this)); recordingModel = RecordingModel(WeakReference(this)); inputModel = InputModel(WeakReference(this)); diff --git a/flutter/lib/models/user_model.dart b/flutter/lib/models/user_model.dart index e6065743c..751b01637 100644 --- a/flutter/lib/models/user_model.dart +++ b/flutter/lib/models/user_model.dart @@ -1,7 +1,8 @@ import 'dart:async'; import 'dart:convert'; -import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common/hbbs/hbbs.dart'; +import 'package:flutter_hbb/common/widgets/peer_tab_page.dart'; import 'package:get/get.dart'; import 'package:http/http.dart' as http; @@ -10,17 +11,19 @@ import 'model.dart'; import 'platform_model.dart'; class UserModel { - var userName = ''.obs; + final RxString userName = ''.obs; + final RxString groupName = ''.obs; + final RxBool isAdmin = false.obs; WeakReference parent; - UserModel(this.parent) { - refreshCurrentUser(); - } + UserModel(this.parent); void refreshCurrentUser() async { - await getUserName(); final token = bind.mainGetLocalOption(key: 'access_token'); - if (token == '') return; + if (token == '') { + await _updateOtherModels(); + return; + } final url = await bind.mainGetApiServer(); final body = { 'id': await bind.mainGetMyId(), @@ -35,55 +38,42 @@ class UserModel { body: json.encode(body)); final status = response.statusCode; if (status == 401 || status == 400) { - resetToken(); + reset(); return; } - await _parseResp(response.body); + final data = json.decode(response.body); + final error = data['error']; + if (error != null) { + throw error; + } + await _parseUserInfo(data); } catch (e) { print('Failed to refreshCurrentUser: $e'); + } finally { + await _updateOtherModels(); } } - void resetToken() async { + Future reset() async { await bind.mainSetLocalOption(key: 'access_token', value: ''); await bind.mainSetLocalOption(key: 'user_info', value: ''); + await gFFI.abModel.reset(); + await gFFI.groupModel.reset(); userName.value = ''; + groupName.value = ''; + statePeerTab.check(); } - Future _parseResp(String body) async { - final data = json.decode(body); - final error = data['error']; - if (error != null) { - return error!; - } - final token = data['access_token']; - if (token != null) { - await bind.mainSetLocalOption(key: 'access_token', value: token); - } - final info = data['user']; - if (info != null) { - final value = json.encode(info); - await bind.mainSetOption(key: 'user_info', value: value); - userName.value = info['name']; - } - return ''; + Future _parseUserInfo(dynamic userinfo) async { + bind.mainSetLocalOption(key: 'user_info', value: jsonEncode(userinfo)); + userName.value = userinfo['name'] ?? ''; + groupName.value = userinfo['grp'] ?? ''; + isAdmin.value = userinfo['is_admin'] == true; } - Future getUserName() async { - if (userName.isNotEmpty) { - return userName.value; - } - final userInfo = bind.mainGetLocalOption(key: 'user_info'); - if (userInfo.trim().isEmpty) { - return ''; - } - final m = jsonDecode(userInfo); - if (m == null) { - userName.value = ''; - } else { - userName.value = m['name'] ?? ''; - } - return userName.value; + Future _updateOtherModels() async { + await gFFI.abModel.pullAb(); + await gFFI.groupModel.pull(); } Future logOut() async { @@ -95,13 +85,7 @@ class UserModel { 'uuid': await bind.mainGetUuid(), }, headers: await getHttpHeaders()); - await Future.wait([ - bind.mainSetLocalOption(key: 'access_token', value: ''), - bind.mainSetLocalOption(key: 'user_info', value: ''), - bind.mainSetLocalOption(key: 'selected-tags', value: ''), - ]); - parent.target?.abModel.clear(); - userName.value = ''; + await reset(); gFFI.dialogManager.dismissByTag(tag); } @@ -119,12 +103,12 @@ class UserModel { final body = jsonDecode(resp.body); bind.mainSetLocalOption( key: 'access_token', value: body['access_token'] ?? ''); - bind.mainSetLocalOption( - key: 'user_info', value: jsonEncode(body['user'])); - this.userName.value = body['user']?['name'] ?? ''; + await _parseUserInfo(body['user']); return body; } catch (err) { return {'error': '$err'}; + } finally { + await _updateOtherModels(); } } } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 70190729c..720c448e9 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), ("Add to Address Book", ""), + ("Group", ""), + ("Search", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index daa2af065..bc5708987 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -401,5 +401,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Right click to select tabs", "右键选择选项卡"), ("Skipped", "已跳过"), ("Add to Address Book", "添加到地址簿"), + ("Group", "小组"), + ("Search", "搜索"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 33c6492f7..fe0087d40 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), ("Add to Address Book", ""), + ("Group", ""), + ("Search", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 1aa53ca57..a17f26918 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), ("Add to Address Book", ""), + ("Group", ""), + ("Search", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 6bd2a9bbe..877b5c9ac 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("wayland_experiment_tip", ""), ("Right click to select tabs", "Register mit rechtem Mausklick auswählen"), ("Add to Address Book", "Zum Adressbuch hinzufügen"), + ("Group", ""), + ("Search", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index c2748a9bc..d0705af1b 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), ("Add to Address Book", ""), + ("Group", ""), + ("Search", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 8690f8b3f..3e7def305 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("wayland_experiment_tip", "El soporte para Wayland está en fase experimental, por favor, use X11 si necesita acceso desatendido."), ("Right click to select tabs", "Clic derecho para seleccionar pestañas"), ("Add to Address Book", "Añadir a la libreta de direcciones"), + ("Group", ""), + ("Search", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 1e1689cbb..88f2e0841 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("wayland_experiment_tip", "پشتیبانی Wayland در مرحله آزمایشی است، لطفاً در صورت نیاز به دسترسی بدون مراقبت از X11 استفاده کنید."), ("Right click to select tabs", "برای انتخاب تب ها راست کلیک کنید"), ("Add to Address Book", "افزودن به دفترچه آدرس"), + ("Group", ""), + ("Search", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index c3d241bf8..6339919aa 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), ("Add to Address Book", ""), + ("Group", ""), + ("Search", ""), ].iter().cloned().collect(); } diff --git a/src/lang/gr.rs b/src/lang/gr.rs index ecabd8f31..98dc87470 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("wayland_experiment_tip", "Η υποστήριξη Wayland βρίσκεται σε πειραματικό στάδιο, χρησιμοποιήστε το X11 εάν χρειάζεστε πρόσβαση χωρίς επίβλεψη."), ("Right click to select tabs", "Κάντε δεξί κλικ για να επιλέξετε καρτέλες"), ("Add to Address Book", ""), + ("Group", ""), + ("Search", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index d0f2f4412..ee77b53e6 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), ("Add to Address Book", ""), + ("Group", ""), + ("Search", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index b8f9e392d..173a21e31 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), ("Add to Address Book", ""), + ("Group", ""), + ("Search", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index b7d449a62..84a41a96a 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("wayland_experiment_tip", "Il supporto Wayland è in fase sperimentale, utilizza X11 se necessiti di un accesso stabile."), ("Right click to select tabs", "Clic con il tasto destro per selezionare le schede"), ("Add to Address Book", "Aggiungi alla rubrica"), + ("Group", ""), + ("Search", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 4ca33e76a..e9914c0fe 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), ("Add to Address Book", ""), + ("Group", ""), + ("Search", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 93338165b..6f514f706 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), ("Add to Address Book", ""), + ("Group", ""), + ("Search", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index a7d6f299d..69c4115ca 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), ("Add to Address Book", ""), + ("Group", ""), + ("Search", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index d3f991d44..1a6fceb12 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), ("Add to Address Book", ""), + ("Group", ""), + ("Search", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 4a457218c..f279d6e7a 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), ("Add to Address Book", ""), + ("Group", ""), + ("Search", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index af59e4f2e..18b803ec3 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), ("Add to Address Book", ""), + ("Group", ""), + ("Search", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 8d990fc66..629308f89 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("wayland_experiment_tip", "Поддержка Wayland находится на экспериментальной стадии, используйте X11, если вам требуется автоматический доступ."), ("Right click to select tabs", "Выбор вкладок щелчком правой кнопки мыши"), ("Add to Address Book", "Добавить в адресную книгу"), + ("Group", ""), + ("Search", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 13672d086..7f7c865cb 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), ("Add to Address Book", ""), + ("Group", ""), + ("Search", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 5ec59c4be..132b8fcdc 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), ("Add to Address Book", ""), + ("Group", ""), + ("Search", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 1feb5d55e..b68537a65 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), ("Add to Address Book", ""), + ("Group", ""), + ("Search", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 6993cb43c..99033faea 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), ("Add to Address Book", ""), + ("Group", ""), + ("Search", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 7b66af60e..32cd4a374 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), ("Add to Address Book", ""), + ("Group", ""), + ("Search", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 6f0e8806b..2ff28f970 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("wayland_experiment_tip", ""), ("Right click to select tabs", "右鍵選擇選項卡"), ("Add to Address Book", "添加到地址簿"), + ("Group", "小組"), + ("Search", "搜索"), ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 92fd2db8a..854514cfc 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), ("Add to Address Book", ""), + ("Group", ""), + ("Search", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 1d32aad5e..0667e2629 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), ("Add to Address Book", ""), + ("Group", ""), + ("Search", ""), ].iter().cloned().collect(); } From 7a938ace02e2f8e37e514b90d422a342dfe7be9f Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 13 Dec 2022 12:55:41 +0800 Subject: [PATCH 1184/2015] feat: add list file search listener --- .../lib/desktop/pages/file_manager_page.dart | 339 +++++++++++------- .../widgets/list_search_action_listener.dart | 75 ++++ flutter/lib/models/file_model.dart | 2 +- 3 files changed, 295 insertions(+), 121 deletions(-) create mode 100644 flutter/lib/desktop/widgets/list_search_action_listener.dart diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 9d8ef6f7a..98f1481ff 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -7,6 +7,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_breadcrumb/flutter_breadcrumb.dart'; +import 'package:flutter_hbb/desktop/widgets/list_search_action_listener.dart'; import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; import 'package:flutter_hbb/models/file_model.dart'; import 'package:get/get.dart'; @@ -32,6 +33,18 @@ enum LocationStatus { fileSearchBar } +/// The status of currently focused scope of the mouse +enum MouseFocusScope { + /// Mouse is in local field. + local, + + /// Mouse is in remote field. + remote, + + /// Mouse is not in local field, remote neither. + none +} + class FileManagerPage extends StatefulWidget { const FileManagerPage({Key? key, required this.id}) : super(key: key); final String id; @@ -55,6 +68,11 @@ class _FileManagerPageState extends State final _searchTextRemote = "".obs; final _breadCrumbScrollerLocal = ScrollController(); final _breadCrumbScrollerRemote = ScrollController(); + final _mouseFocusScope = Rx(MouseFocusScope.none); + final _keyboardNodeLocal = FocusNode(debugLabel: "keyboardNodeLocal"); + final _keyboardNodeRemote = FocusNode(debugLabel: "keyboardNodeRemote"); + final _listSearchBufferLocal = TimeoutStringBuffer(); + final _listSearchBufferRemote = TimeoutStringBuffer(); /// [_lastClickTime], [_lastClickEntry] help to handle double click int _lastClickTime = @@ -197,6 +215,7 @@ class _FileManagerPageState extends State } Widget body({bool isLocal = false}) { + final scrollController = ScrollController(); return Container( decoration: BoxDecoration(border: Border.all(color: Colors.black26)), margin: const EdgeInsets.all(16.0), @@ -209,7 +228,8 @@ class _FileManagerPageState extends State onDragExited: (exit) { _dropMaskVisible.value = false; }, - child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + child: + Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ headTools(isLocal), Expanded( child: Row( @@ -217,8 +237,8 @@ class _FileManagerPageState extends State children: [ Expanded( child: SingleChildScrollView( - controller: ScrollController(), - child: _buildDataTable(context, isLocal), + controller: scrollController, + child: _buildDataTable(context, isLocal, scrollController), ), ) ], @@ -228,7 +248,9 @@ class _FileManagerPageState extends State ); } - Widget _buildDataTable(BuildContext context, bool isLocal) { + Widget _buildDataTable( + BuildContext context, bool isLocal, ScrollController scrollController) { + const rowHeight = 25.0; final fd = model.getCurrentDir(isLocal); final entries = fd.entries; final sortIndex = (SortBy style) { @@ -246,127 +268,194 @@ class _FileManagerPageState extends State final sortAscending = isLocal ? model.localSortAscending : model.remoteSortAscending; - return ObxValue( - (searchText) { - final filteredEntries = searchText.isNotEmpty - ? entries.where((element) { - return element.name.contains(searchText.value); - }).toList(growable: false) - : entries; - return DataTable( - key: ValueKey(isLocal ? 0 : 1), - showCheckboxColumn: false, - dataRowHeight: 25, - 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 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, + return MouseRegion( + onEnter: (evt) { + print("enter $evt"); + _mouseFocusScope.value = + isLocal ? MouseFocusScope.local : MouseFocusScope.remote; + if (isLocal) { + _keyboardNodeLocal.requestFocus(); + } else { + _keyboardNodeRemote.requestFocus(); + } + }, + onExit: (evt) { + print("exit $evt"); + _mouseFocusScope.value = MouseFocusScope.none; + // FocusManager.instance.primaryFocus?.unfocus(); + }, + child: ListSearchActionListener( + node: isLocal ? _keyboardNodeLocal : _keyboardNodeRemote, + buffer: isLocal ? _listSearchBufferLocal : _listSearchBufferRemote, + 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) { + final index = entries.indexOf(selectedEntries.items.first); + if (index < 0) { + return; + } + skipCount = index + 1; + } + var searchResult = entries + .skip(skipCount) + .where((element) => element.name.startsWith(buffer)); + if (searchResult.isEmpty) { + // loop + searchResult = + entries.where((element) => element.name.startsWith(buffer)); + } + if (searchResult.isEmpty) { + return; + } + final offset = entries.indexOf(searchResult.first) * rowHeight; + setState(() { + selectedEntries.clear(); + selectedEntries.add(isLocal, searchResult.first); + debugPrint("focused on ${searchResult.first.name}"); + }); + }, + onSearch: (buffer) { + debugPrint("searching for $buffer"); + final selectedEntries = getSelectedItems(isLocal); + final searchResult = + entries.where((element) => element.name.startsWith(buffer)); + selectedEntries.clear(); + if (searchResult.isEmpty) { + return; + } + setState(() { + selectedEntries.add(isLocal, searchResult.first); + debugPrint("focused on ${searchResult.first.name}"); + }); + }, + child: ObxValue( + (searchText) { + final filteredEntries = searchText.isNotEmpty + ? entries.where((element) { + 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 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)) - .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)) - ]), - )), - onTap: () { - final items = getSelectedItems(isLocal); - - // handle double click - if (_checkDoubleClick(entry)) { - openDirectory(entry.path, isLocal: isLocal); - items.clear(); - return; - } - _onSelectedChanged( - items, filteredEntries, entry, isLocal); - }, - ), - DataCell(FittedBox( - child: Tooltip( + ?.withOpacity(0.7), + ).marginSymmetric(horizontal: 2), + Expanded( + child: Text(entry.name, + 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); + }, + ), + 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: lastModifiedStr, + message: sizeStr, 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), - ); - }, - isLocal ? _searchTextLocal : _searchTextRemote, + sizeStr, + overflow: TextOverflow.ellipsis, + style: + TextStyle(fontSize: 10, color: MyTheme.darkGray), + ))), + ]); + }).toList(growable: false), + ); + }, + isLocal ? _searchTextLocal : _searchTextRemote, + ), + ), ); } @@ -1015,4 +1104,14 @@ class _FileManagerPageState extends State } model.sendFiles(items, isRemote: false); } + + void refocusKeyboardListener(bool isLocal) { + Future.delayed(Duration.zero, () { + if (isLocal) { + _keyboardNodeLocal.requestFocus(); + } else { + _keyboardNodeRemote.requestFocus(); + } + }); + } } diff --git a/flutter/lib/desktop/widgets/list_search_action_listener.dart b/flutter/lib/desktop/widgets/list_search_action_listener.dart new file mode 100644 index 000000000..9598c3400 --- /dev/null +++ b/flutter/lib/desktop/widgets/list_search_action_listener.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; + +class ListSearchActionListener extends StatelessWidget { + final FocusNode node; + final TimeoutStringBuffer buffer; + final Widget child; + final Function(String) onNext; + final Function(String) onSearch; + + const ListSearchActionListener( + {super.key, + required this.node, + required this.buffer, + required this.child, + required this.onNext, + required this.onSearch}); + + @mustCallSuper + @override + Widget build(BuildContext context) { + return KeyboardListener( + autofocus: true, + onKeyEvent: (kv) { + final ch = kv.character; + if (ch == null) { + return; + } + final action = buffer.input(ch); + switch (action) { + case ListSearchAction.search: + onSearch(buffer.buffer); + break; + case ListSearchAction.next: + onNext(buffer.buffer); + break; + } + }, + focusNode: node, + child: child); + } +} + +enum ListSearchAction { search, next } + +class TimeoutStringBuffer { + var _buffer = ""; + late DateTime _duration; + + static int timeoutMilliSec = 1500; + + String get buffer => _buffer; + + TimeoutStringBuffer() { + _duration = DateTime.now(); + } + + ListSearchAction input(String ch) { + final curr = DateTime.now(); + try { + if (curr.difference(_duration).inMilliseconds > timeoutMilliSec) { + _buffer = ch; + return ListSearchAction.search; + } else { + if (ch == _buffer) { + return ListSearchAction.next; + } else { + _buffer += ch; + return ListSearchAction.search; + } + } + } finally { + _duration = curr; + } + } +} diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index a7968f701..142479c2a 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -213,7 +213,7 @@ class FileModel extends ChangeNotifier { } receiveFileDir(Map evt) { - debugPrint("recv file dir:$evt"); + // debugPrint("recv file dir:$evt"); if (evt['is_local'] == "false") { // init remote home, the connection will automatic read remote home when established, try { From 03af5042ecbba43ba9d2520357816f39135a91b9 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Wed, 14 Dec 2022 11:56:36 +0800 Subject: [PATCH 1185/2015] opt: jump to selected item --- .../lib/desktop/pages/file_manager_page.dart | 59 +++++++++++++------ 1 file changed, 40 insertions(+), 19 deletions(-) diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 98f1481ff..b9fa1a143 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -228,8 +228,7 @@ class _FileManagerPageState extends State onDragExited: (exit) { _dropMaskVisible.value = false; }, - child: - Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ headTools(isLocal), Expanded( child: Row( @@ -270,7 +269,6 @@ class _FileManagerPageState extends State return MouseRegion( onEnter: (evt) { - print("enter $evt"); _mouseFocusScope.value = isLocal ? MouseFocusScope.local : MouseFocusScope.remote; if (isLocal) { @@ -280,9 +278,7 @@ class _FileManagerPageState extends State } }, onExit: (evt) { - print("exit $evt"); _mouseFocusScope.value = MouseFocusScope.none; - // FocusManager.instance.primaryFocus?.unfocus(); }, child: ListSearchActionListener( node: isLocal ? _keyboardNodeLocal : _keyboardNodeRemote, @@ -304,19 +300,18 @@ class _FileManagerPageState extends State .skip(skipCount) .where((element) => element.name.startsWith(buffer)); if (searchResult.isEmpty) { - // loop + // cannot find next, lets restart search from head searchResult = entries.where((element) => element.name.startsWith(buffer)); } if (searchResult.isEmpty) { + setState(() { + getSelectedItems(isLocal).clear(); + }); return; } - final offset = entries.indexOf(searchResult.first) * rowHeight; - setState(() { - selectedEntries.clear(); - selectedEntries.add(isLocal, searchResult.first); - debugPrint("focused on ${searchResult.first.name}"); - }); + _jumpToEntry( + isLocal, searchResult.first, scrollController, rowHeight, buffer); }, onSearch: (buffer) { debugPrint("searching for $buffer"); @@ -325,12 +320,13 @@ class _FileManagerPageState extends State entries.where((element) => element.name.startsWith(buffer)); selectedEntries.clear(); if (searchResult.isEmpty) { + setState(() { + getSelectedItems(isLocal).clear(); + }); return; } - setState(() { - selectedEntries.add(isLocal, searchResult.first); - debugPrint("focused on ${searchResult.first.name}"); - }); + _jumpToEntry( + isLocal, searchResult.first, scrollController, rowHeight, buffer); }, child: ObxValue( (searchText) { @@ -420,7 +416,7 @@ class _FileManagerPageState extends State )), onTap: () { final items = getSelectedItems(isLocal); - + // handle double click if (_checkDoubleClick(entry)) { openDirectory(entry.path, isLocal: isLocal); @@ -446,8 +442,8 @@ class _FileManagerPageState extends State child: Text( sizeStr, overflow: TextOverflow.ellipsis, - style: - TextStyle(fontSize: 10, color: MyTheme.darkGray), + style: TextStyle( + fontSize: 10, color: MyTheme.darkGray), ))), ]); }).toList(growable: false), @@ -459,6 +455,31 @@ class _FileManagerPageState extends State ); } + void _jumpToEntry(bool isLocal, Entry entry, + ScrollController scrollController, double rowHeight, String buffer) { + final entries = model.getCurrentDir(isLocal).entries; + final index = entries.indexOf(entry); + if (index == -1) { + debugPrint("entry is not valid: ${entry.path}"); + } + final selectedEntries = getSelectedItems(isLocal); + final searchResult = + entries.where((element) => element.name.startsWith(buffer)); + selectedEntries.clear(); + if (searchResult.isEmpty) { + return; + } + final offset = min( + max(scrollController.position.minScrollExtent, + entries.indexOf(searchResult.first) * rowHeight), + scrollController.position.maxScrollExtent); + scrollController.jumpTo(offset); + setState(() { + selectedEntries.add(isLocal, searchResult.first); + debugPrint("focused on ${searchResult.first.name}"); + }); + } + void _onSelectedChanged(SelectedItems selectedItems, List entries, Entry entry, bool isLocal) { final isCtrlDown = RawKeyboard.instance.keysPressed From e1bfd73ca1d0437fd8a0133637132451f132034d Mon Sep 17 00:00:00 2001 From: 21pages Date: Wed, 14 Dec 2022 12:27:57 +0800 Subject: [PATCH 1186/2015] fix group model pull error Signed-off-by: 21pages --- flutter/lib/models/group_model.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/flutter/lib/models/group_model.dart b/flutter/lib/models/group_model.dart index 4dfcf189b..98dc57086 100644 --- a/flutter/lib/models/group_model.dart +++ b/flutter/lib/models/group_model.dart @@ -65,7 +65,7 @@ class GroupModel { if (json.containsKey('error')) { throw json['error']; } else { - total = json['total']; + if (total == 0) total = json['total']; if (json.containsKey('data')) { final data = json['data']; if (data is List) { @@ -76,7 +76,7 @@ class GroupModel { } } } - } while (current < total); + } while (current < total + 1); } catch (err) { debugPrint('$err'); userLoadError.value = err.toString(); @@ -115,7 +115,7 @@ class GroupModel { if (json.containsKey('error')) { throw json['error']; } else { - total = json['total']; + if (total == 0) total = json['total']; if (json.containsKey('data')) { final data = json['data']; if (data is List) { @@ -128,7 +128,7 @@ class GroupModel { } } } - } while (current < total); + } while (current < total + 1); } catch (err) { debugPrint('$err'); peerLoadError.value = err.toString(); From 099029f5e2799fc7879466a5d6c3345bc8049d46 Mon Sep 17 00:00:00 2001 From: 21pages Date: Wed, 14 Dec 2022 12:47:08 +0800 Subject: [PATCH 1187/2015] add group peer card mobile compatibility like address book Signed-off-by: 21pages --- flutter/lib/common/widgets/my_group.dart | 118 +++++++++++++++-------- 1 file changed, 80 insertions(+), 38 deletions(-) diff --git a/flutter/lib/common/widgets/my_group.dart b/flutter/lib/common/widgets/my_group.dart index 77ddf779b..65eaba40f 100644 --- a/flutter/lib/common/widgets/my_group.dart +++ b/flutter/lib/common/widgets/my_group.dart @@ -45,18 +45,11 @@ class _MyGroupState extends State { if (gFFI.groupModel.userLoadError.isNotEmpty) { return _buildShowError(gFFI.groupModel.userLoadError.value); } - return Row( - children: [ - _buildLeftDesktop(), - Expanded( - child: Align( - alignment: Alignment.topLeft, - child: MyGroupPeerView( - menuPadding: widget.menuPadding, - initPeers: gFFI.groupModel.peersShow.value)), - ) - ], - ); + if (isDesktop) { + return _buildDesktop(); + } else { + return _buildMobile(); + } }); } @@ -75,37 +68,86 @@ class _MyGroupState extends State { )); } - Widget _buildLeftDesktop() { - return Row( - children: [ - Card( - margin: EdgeInsets.symmetric(horizontal: 4.0), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - side: - BorderSide(color: Theme.of(context).scaffoldBackgroundColor)), - child: Container( - width: 200, - height: double.infinity, - padding: - const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), - child: Column( - children: [ - _buildLeftHeader(), - Expanded( - child: Container( + Widget _buildDesktop() { + return Obx( + () => Row( + children: [ + Card( + margin: EdgeInsets.symmetric(horizontal: 4.0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide( + color: Theme.of(context).scaffoldBackgroundColor)), + child: Container( + width: 200, + height: double.infinity, + padding: + const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), + child: Column( + children: [ + _buildLeftHeader(), + Expanded( + child: Container( + width: double.infinity, + height: double.infinity, + decoration: + BoxDecoration(borderRadius: BorderRadius.circular(2)), + child: _buildUserContacts(), + ).marginSymmetric(vertical: 8.0), + ) + ], + ), + ), + ).marginOnly(right: 8.0), + Expanded( + child: Align( + alignment: Alignment.topLeft, + child: MyGroupPeerView( + menuPadding: widget.menuPadding, + initPeers: gFFI.groupModel.peersShow.value)), + ) + ], + ), + ); + } + + Widget _buildMobile() { + return Obx( + () => Column( + children: [ + Card( + margin: EdgeInsets.symmetric(horizontal: 4.0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide( + color: Theme.of(context).scaffoldBackgroundColor)), + child: Container( + padding: + const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _buildLeftHeader(), + Container( width: double.infinity, - height: double.infinity, decoration: - BoxDecoration(borderRadius: BorderRadius.circular(2)), + BoxDecoration(borderRadius: BorderRadius.circular(4)), child: _buildUserContacts(), - ).marginSymmetric(vertical: 8.0), - ) - ], + ).marginSymmetric(vertical: 8.0) + ], + ), ), ), - ).marginOnly(right: 8.0), - ], + Divider(), + Expanded( + child: Align( + alignment: Alignment.topLeft, + child: MyGroupPeerView( + menuPadding: widget.menuPadding, + initPeers: gFFI.groupModel.peersShow.value)), + ) + ], + ), ); } From d675bfa7e38f38ecbee738fc3da1cbc03b0ceb4e Mon Sep 17 00:00:00 2001 From: asur4s Date: Tue, 13 Dec 2022 15:40:44 -0800 Subject: [PATCH 1188/2015] rename: sync status && add numlock capslock --- src/keyboard.rs | 4 ++-- src/server/input_service.rs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/keyboard.rs b/src/keyboard.rs index b2e19ac73..97ce37b12 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -264,7 +264,7 @@ pub fn get_keyboard_mode_enum() -> KeyboardMode { } #[cfg(not(any(target_os = "android", target_os = "ios")))] -pub fn add_numlock_capslock_state(key_event: &mut KeyEvent) { +pub fn add_numlock_capslock_status(key_event: &mut KeyEvent) { if get_key_state(enigo::Key::CapsLock) { key_event.modifiers.push(ControlKey::CapsLock.into()); } @@ -341,7 +341,7 @@ pub fn event_to_key_event(event: &Event) -> KeyEvent { } }; #[cfg(not(any(target_os = "android", target_os = "ios")))] - add_numlock_capslock_state(&mut key_event); + add_numlock_capslock_status(&mut key_event); return key_event; } diff --git a/src/server/input_service.rs b/src/server/input_service.rs index e1355785d..edda4416c 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -830,7 +830,7 @@ fn click_numlock(en: &mut Enigo) { en.key_click(enigo::Key::NumLock); } -fn sync_status(key_event: &KeyEvent) { +fn sync_numlock_capslock_status(key_event: &KeyEvent) { let mut en = ENIGO.lock().unwrap(); let client_caps_locking = is_modifier_in_key_event(ControlKey::CapsLock, key_event); @@ -851,7 +851,7 @@ fn sync_status(key_event: &KeyEvent) { } if need_click_numlock && !disable_numlock { - click_capslock(&mut en); + click_numlock(&mut en); } } @@ -1001,7 +1001,7 @@ pub fn handle_key_(evt: &KeyEvent) { } if evt.down { - sync_status(evt) + sync_numlock_capslock_status(evt) } match evt.mode.unwrap() { KeyboardMode::Map => { From 711e6bd631e7198a3972cf47a76b14fa3e27b2cf Mon Sep 17 00:00:00 2001 From: Manos G <87467035+7th-fret@users.noreply.github.com> Date: Wed, 14 Dec 2022 14:44:46 +0200 Subject: [PATCH 1189/2015] Update gr.rs --- src/lang/gr.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/lang/gr.rs b/src/lang/gr.rs index 98dc87470..b1a8174ee 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -121,8 +121,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Εμφάνιση παρακολούθησης ποιότητας σύνδεσης"), ("Disable clipboard", "Απενεργοποίηση προχείρου"), ("Lock after session end", "Κλείδωμα μετά το τέλος της συνεδρίας"), - ("Insert", "Εισάγετε"), - ("Insert Lock", "Εισαγωγή κλειδαριάς"), + ("Insert", ""), + ("Insert Lock", "Κλείδωμα απομακρυσμένου σταθμού"), ("Refresh", "Ανανέωση"), ("ID does not exist", "Το αναγνωριστικό ID δεν υπάρχει"), ("Failed to connect to rendezvous server", "Αποτυχία σύνδεσης με διακομιστή"), @@ -179,7 +179,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Relayed and encrypted connection", "Κρυπτογραφημένη σύνδεση με αναμετάδοση"), ("Direct and unencrypted connection", "Άμεση και μη κρυπτογραφημένη σύνδεση"), ("Relayed and unencrypted connection", "Μη κρυπτογραφημένη σύνδεση με αναμετάδοση"), - ("Enter Remote ID", "Εισαγωγή απομακρυσμένου αναγνωριστικού ID"), + ("Enter Remote ID", "Εισαγωγή απομακρυσμένου ID"), ("Enter your password", "Εισάγετε τον κωδικό σας"), ("Logging in...", "Σύνδεση..."), ("Enable RDP session sharing", "Ενεργοποίηση κοινής χρήσης RDP"), @@ -399,8 +399,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", "Να επιτρέπεται η απόκρυψη, μόνο εάν αποδέχεστε συνδέσεις μέσω κωδικού πρόσβασης και χρησιμοποιείτε μόνιμο κωδικό πρόσβασης"), ("wayland_experiment_tip", "Η υποστήριξη Wayland βρίσκεται σε πειραματικό στάδιο, χρησιμοποιήστε το X11 εάν χρειάζεστε πρόσβαση χωρίς επίβλεψη."), ("Right click to select tabs", "Κάντε δεξί κλικ για να επιλέξετε καρτέλες"), - ("Add to Address Book", ""), - ("Group", ""), - ("Search", ""), + ("Add to Address Book", "Προσθήκη στο Βιβλίο Διευθύνσεων"), + ("Group", "Ομάδα"), + ("Search", "Αναζήτηση"), ].iter().cloned().collect(); } From eb71d064c8e8134e006fe81a271dd60f21cb9199 Mon Sep 17 00:00:00 2001 From: solokot Date: Wed, 14 Dec 2022 16:41:24 +0300 Subject: [PATCH 1190/2015] Update ru.rs --- src/lang/ru.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 629308f89..74c4aefb7 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -400,7 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("wayland_experiment_tip", "Поддержка Wayland находится на экспериментальной стадии, используйте X11, если вам требуется автоматический доступ."), ("Right click to select tabs", "Выбор вкладок щелчком правой кнопки мыши"), ("Add to Address Book", "Добавить в адресную книгу"), - ("Group", ""), - ("Search", ""), + ("Group", "Группа"), + ("Search", "Поиск"), ].iter().cloned().collect(); } From 35f69bcf5194fd3779a6fc83730a8bdac3e5dcee Mon Sep 17 00:00:00 2001 From: "Miguel F. G" <116861809+flusheDData@users.noreply.github.com> Date: Wed, 14 Dec 2022 15:31:32 +0100 Subject: [PATCH 1191/2015] 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 3e7def305..6c5516539 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -400,7 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("wayland_experiment_tip", "El soporte para Wayland está en fase experimental, por favor, use X11 si necesita acceso desatendido."), ("Right click to select tabs", "Clic derecho para seleccionar pestañas"), ("Add to Address Book", "Añadir a la libreta de direcciones"), - ("Group", ""), - ("Search", ""), + ("Group", "Grupo"), + ("Search", "Búsqueda"), ].iter().cloned().collect(); } From e8cf817a1501d591bd22892ff95b8c7e98183723 Mon Sep 17 00:00:00 2001 From: jimmyGALLAND <64364019+jimmyGALLAND@users.noreply.github.com> Date: Wed, 14 Dec 2022 22:34:16 +0100 Subject: [PATCH 1192/2015] Update fr.rs --- src/lang/fr.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 6339919aa..9168426e9 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -399,8 +399,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", "Autoriser le masquage uniquement si vous acceptez des sessions via un mot de passe et utilisez un mot de passe permanent"), ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), - ("Add to Address Book", ""), - ("Group", ""), - ("Search", ""), + ("Add to Address Book", "Ajouter au carnet d'adresses"), + ("Group", "Groupe"), + ("Search", "Rechercher"), ].iter().cloned().collect(); } From 206e20cfe3210cd4be2744ecffca5ec51ba76570 Mon Sep 17 00:00:00 2001 From: Boban Jovanovic Date: Thu, 15 Dec 2022 12:19:48 +0100 Subject: [PATCH 1193/2015] Serbian language added --- src/lang/sr.rs | 412 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 412 insertions(+) create mode 100644 src/lang/sr.rs diff --git a/src/lang/sr.rs b/src/lang/sr.rs new file mode 100644 index 000000000..515ae6884 --- /dev/null +++ b/src/lang/sr.rs @@ -0,0 +1,412 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "Status"), + ("Your Desktop", "Vaša radna površina"), + ("desk_tip", "Tvojoj radnoj površini se može pristupiti ovim ID i lozinkom."), + ("Password", "Lozinka"), + ("Ready", "Spremno"), + ("Established", "Uspostavljeno"), + ("connecting_status", "Spajanje na RustDesk mrežu..."), + ("Enable Service", "Dozvoli servis"), + ("Start Service", "Pokreni servis"), + ("Service is running", "Servis je pokrenut"), + ("Service is not running", "Servis nije pokrenut"), + ("not_ready_status", "Nije spremno. Proverite konekciju."), + ("Control Remote Desktop", "Upravljanje udaljenom radnom površinom"), + ("Transfer File", "Prenos fajla"), + ("Connect", "Spajanje"), + ("Recent Sessions", "Poslednje sesije"), + ("Address Book", "Adresar"), + ("Confirmation", "Potvrda"), + ("TCP Tunneling", "TCP tunel"), + ("Remove", "Ukloni"), + ("Refresh random password", "Osveži slučajnu lozinku"), + ("Set your own password", "Postavi lozinku"), + ("Enable Keyboard/Mouse", "Dozvoli tastaturu/miša"), + ("Enable Clipboard", "Dozvoli clipboard"), + ("Enable File Transfer", "Dozvoli prenos fajla"), + ("Enable TCP Tunneling", "Dozvoli TCP tunel"), + ("IP Whitelisting", "IP pouzdana lista"), + ("ID/Relay Server", "ID/Posredni server"), + ("Import Server Config", "Import server konfiguracije"), + ("Export Server Config", "Eksport server konfiguracije"), + ("Import server configuration successfully", "Import server konfiguracije uspešan"), + ("Export server configuration successfully", "Eksport server konfiguracije uspešan"), + ("Invalid server configuration", "Pogrešna konfiguracija servera"), + ("Clipboard is empty", "Clipboard je prazan"), + ("Stop service", "Zaustavi servis"), + ("Change ID", "Promeni ID"), + ("Website", "Web sajt"), + ("About", "O programu"), + ("Mute", "Utišaj"), + ("Audio Input", "Audio ulaz"), + ("Enhancements", "Proširenja"), + ("Hardware Codec", "Hardverski kodek"), + ("Adaptive Bitrate", "Prilagodljiva brzina uzorka"), + ("ID Server", "ID server"), + ("Relay Server", "Posredni server"), + ("API Server", "API server"), + ("invalid_http", "Nevažeći http"), + ("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 nije podržan"), + ("Not available", "Nije dostupno"), + ("Too frequent", "Previše često"), + ("Cancel", "Otkaži"), + ("Skip", "Preskoči"), + ("Close", "Zatvori"), + ("Retry", "Ponovi"), + ("OK", "Ok"), + ("Password Required", "Potrebna lozinka"), + ("Please enter your password", "Molimo unesite svoju lozinku"), + ("Remember password", "Zapamti lozinku"), + ("Wrong Password", "Pogrešna lozinka"), + ("Do you want to enter again?", "Želite li da unesete ponovo?"), + ("Connection Error", "Greška u konekciji"), + ("Error", "Greška"), + ("Reset by the peer", "Prekinuto sa druge strane"), + ("Connecting...", "Povezivanje..."), + ("Connection in progress. Please wait.", "Povezivanje u toku. Molimo sačekajte."), + ("Please try 1 minute later", "Pokušajte minut kasnije"), + ("Login Error", "Greška u prijavljivanju"), + ("Successful", "Uspešno"), + ("Connected, waiting for image...", "Spojeno, sačekajte sliku..."), + ("Name", "Ime"), + ("Type", "Tip"), + ("Modified", "Izmenjeno"), + ("Size", "Veličina"), + ("Show Hidden Files", "Prikaži skrivene datoteke"), + ("Receive", "Prijem"), + ("Send", "Slanje"), + ("Refresh File", "Osveži datoteku"), + ("Local", "Lokalno"), + ("Remote", "Udaljeno"), + ("Remote Computer", "Udaljeno računar"), + ("Local Computer", "Lokalni računar"), + ("Confirm Delete", "Potvrdite brisanje"), + ("Delete", "Brisanje"), + ("Properties", "Osobine"), + ("Multi Select", "Višestruko selektovanje"), + ("Select All", "Selektuj sve"), + ("Unselect All", "Deselektuj sve"), + ("Empty Directory", "Prazan direktorijum"), + ("Not an empty directory", "Nije prazan direktorijum"), + ("Are you sure you want to delete this file?", "Da li ste sigurni da želite da obrišete ovu datoteku?"), + ("Are you sure you want to delete this empty directory?", "Da li ste sigurni da želite da obrišete ovaj prazan direktorijum?"), + ("Are you sure you want to delete the file of this directory?", "Da li ste sigurni da želite da obrišete datoteku ovog direktorijuma?"), + ("Do this for all conflicts", "Uradi ovo za sve konflikte"), + ("This is irreversible!", "Ovo je nepovratno"), + ("Deleting", "Brisanje"), + ("files", "datoteke"), + ("Waiting", "Čekanje"), + ("Finished", "Završeno"), + ("Speed", "Brzina"), + ("Custom Image Quality", "Korisnički kvalitet slike"), + ("Privacy mode", "Mod privatnosti"), + ("Block user input", "Blokiraj korisnikov unos"), + ("Unblock user input", "Odblokiraj korisnikov unos"), + ("Adjust Window", "Podesi prozor"), + ("Original", "Original"), + ("Shrink", "Skupi"), + ("Stretch", "Raširi"), + ("Scrollbar", "Skrol linija"), + ("ScrollAuto", "Auto skrol"), + ("Good image quality", "Dobar kvalitet slike"), + ("Balanced", "Balansirano"), + ("Optimize reaction time", "Optimizuj vreme reakcije"), + ("Custom", "Korisnički"), + ("Show remote cursor", "Prikaži udaljeni kursor"), + ("Show quality monitor", "Prikaži monitor kvaliteta"), + ("Disable clipboard", "Zabrani clipboard"), + ("Lock after session end", "Zaključaj po završetku sesije"), + ("Insert", "Umetni"), + ("Insert Lock", "Zaključaj umetanje"), + ("Refresh", "Osveži"), + ("ID does not exist", "ID ne postoji"), + ("Failed to connect to rendezvous server", "Greška u spajanju na server za povezivanje"), + ("Please try later", "Molimo pokušajte kasnije"), + ("Remote desktop is offline", "Udaljeni ekran je isključen"), + ("Key mismatch", "Pogrešan ključ"), + ("Timeout", "Isteklo vreme"), + ("Failed to connect to relay server", "Gršeka u spajanju na posredni server"), + ("Failed to connect via rendezvous server", "Greška u spajanju preko servera za povezivanje"), + ("Failed to connect via relay server", "Greška u spajanju preko posrednog servera"), + ("Failed to make direct connection to remote desktop", "Greška u direktnom spajanju na udaljenu radnu površinu"), + ("Set Password", "Postavi lozinku"), + ("OS Password", "OS lozinka"), + ("install_tip", "Zbog UAC (User Account Control - kontrola naloga korisnika), RustDesk ne može raditi pravilno u nekim slučajevima. Da biste prevazišli UAC, kliknite taster ispod da instalirate RustDesk na sistem."), + ("Click to upgrade", "Klik za nadogradnju"), + ("Click to download", "Klik za skidanje"), + ("Click to update", "Klik za ažuriranje"), + ("Configure", "Konfigurisanje"), + ("config_acc", "Da biste daljinski kontrolisali radnu površinu, RustDesk-u treba da dodelite \"Accessibility\" prava."), + ("config_screen", "Da biste daljinski pristupili radnoj površini, RustDesk-u treba da dodelite \"Screen Recording\" prava."), + ("Installing ...", "Instaliranje..."), + ("Install", "Instaliraj"), + ("Installation", "Instalacija"), + ("Installation Path", "Putanja za instalaciju"), + ("Create start menu shortcuts", "Kreiraj prečice u meniju"), + ("Create desktop icon", "Kreiraj ikonicu na radnoj površini"), + ("agreement_tip", "Pokretanjem instalacije prihvatate ugovor o licenciranju."), + ("Accept and Install", "Prihvat i instaliraj"), + ("End-user license agreement", "Ugovor sa krajnjim korisnikom"), + ("Generating ...", "Generisanje..."), + ("Your installation is lower version.", "Vaša instalacija je niže verzije"), + ("not_close_tcp_tip", "Ne zatvarajte ovaj prozor dok koristite tunel"), + ("Listening ...", "Na slušanju..."), + ("Remote Host", "Adresa udaljenog uređaja"), + ("Remote Port", "Udaljeni port"), + ("Action", "Akcija"), + ("Add", "Dodaj"), + ("Local Port", "Lokalni port"), + ("Local Address", "Lokalna adresa"), + ("Change Local Port", "Promeni lokalni port"), + ("setup_server_tip", "Za brže spajanje, molimo da koristite svoj server"), + ("Too short, at least 6 characters.", "Prekratko, najmanje 6 znakova."), + ("The confirmation is not identical.", "Potvrda nije identična"), + ("Permissions", "Dozvole"), + ("Accept", "Prihvati"), + ("Dismiss", "Odbaci"), + ("Disconnect", "Raskini konekciju"), + ("Allow using keyboard and mouse", "Dozvoli korišćenje tastature i miša"), + ("Allow using clipboard", "Dozvoli korišćenje clipboard-a"), + ("Allow hearing sound", "Dozvoli da se čuje zvuk"), + ("Allow file copy and paste", "Dozvoli kopiranje i lepljenje fajlova"), + ("Connected", "Spojeno"), + ("Direct and encrypted connection", "Direktna i kriptovana konekcija"), + ("Relayed and encrypted connection", "Posredna i kriptovana konekcija"), + ("Direct and unencrypted connection", "Direktna i nekriptovana konekcija"), + ("Relayed and unencrypted connection", "Posredna i nekriptovana konekcija"), + ("Enter Remote ID", "Unesi ID udaljenog uređaja"), + ("Enter your password", "Unesi svoju lozinku"), + ("Logging in...", "Prijava..."), + ("Enable RDP session sharing", "Dozvoli deljenje RDP sesije"), + ("Auto Login", "Auto prijavljivanje (Važeće samo ako ste postavili \"Lock after session end\")"), + ("Enable Direct IP Access", "Dozvoli direktan pristup preko IP"), + ("Rename", "Preimenuj"), + ("Space", "Prazno"), + ("Create Desktop Shortcut", "Kreiraj prečicu na radnoj površini"), + ("Change Path", "Promeni putanju"), + ("Create Folder", "Kreiraj direktorijum"), + ("Please enter the folder name", "Unesite ime direktorijuma"), + ("Fix it", "Popravi ga"), + ("Warning", "Upozorenje"), + ("Login screen using Wayland is not supported", "Ekran za prijavu koji koristi Wayland nije podržan"), + ("Reboot required", "Potreban je restart"), + ("Unsupported display server ", "Nepodržan server za prikaz"), + ("x11 expected", "x11 očekivan"), + ("Port", "Port"), + ("Settings", "Postavke"), + ("Username", "Korisničko ime"), + ("Invalid port", "Pogrešan port"), + ("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"), + ("Always connect via relay", "Uvek se spoj preko posrednika"), + ("whitelist_tip", "Samo dozvoljene IP mi mogu pristupiti"), + ("Login", "Prijava"), + ("Logout", "Odjava"), + ("Tags", "Oznake"), + ("Search ID", "Traži ID"), + ("Current Wayland display server is not supported", "Tekući Wazland server za prikaz nije podržan"), + ("whitelist_sep", "Odvojeno zarezima, tačka zarezima, praznim mestima ili novim redovima"), + ("Add ID", "Dodaj ID"), + ("Add Tag", "Dodaj oznaku"), + ("Unselect all tags", "Odselektuj sve oznake"), + ("Network error", "Greška na mreži"), + ("Username missed", "Korisničko ime promašeno"), + ("Password missed", "Lozinka promašena"), + ("Wrong credentials", "Pogrešno korisničko ime ili lozinka"), + ("invalid_http", "mora početi sa http:// ili https://"), + ("Edit Tag", "Izmeni oznaku"), + ("Unremember Password", "Zaboravi lozinku"), + ("Favorites", "Favoriti"), + ("Add to Favorites", "Dodaj u favorite"), + ("Remove from Favorites", "Izbaci iz favorita"), + ("Empty", "Prazno"), + ("Invalid folder name", "Pogrešno ime direktorijuma"), + ("Socks5 Proxy", "Socks5 proksi"), + ("Hostname", "Ime uređaja"), + ("Discovered", "Otkriveno"), + ("install_daemon_tip", "Za pokretanje pri startu sistema, treba da instalirate sistemski servis."), + ("Remote ID", "Udaljeni ID"), + ("Paste", "Nalepi"), + ("Paste here?", "Nalepi ovde?"), + ("Are you sure to close the connection?", "Da li ste sigurni da zatvarate konekciju?"), + ("Download new version", "Preuzmi novu verziju"), + ("Touch mode", "Mod na dodir"), + ("Mouse mode", "Miš mod"), + ("One-Finger Tap", "Pritisak jednim prstom"), + ("Left Mouse", "Levi miš"), + ("One-Long Tap", "Dugi pritisak"), + ("Two-Finger Tap", "Pritisak sa dva prsta"), + ("Right Mouse", "Desni miš"), + ("One-Finger Move", "Pomeranje jednim prstom"), + ("Double Tap & Move", "Dupli pritisak i pomeranje"), + ("Mouse Drag", "Prevlačenje mišem"), + ("Three-Finger vertically", "Sa tri prsta vertikalno"), + ("Mouse Wheel", "Točkić miša"), + ("Two-Finger Move", "Pomeranje sa dva prsta"), + ("Canvas Move", "Pomeranje pozadine"), + ("Pinch to Zoom", "Stisnite za zumiranje"), + ("Canvas Zoom", "Zumiranje pozadine"), + ("Reset canvas", "Resetuj pozadinu"), + ("No permission of file transfer", "Nemate pravo prenosa datoteka"), + ("Note", "Primedba"), + ("Connection", "Konekcija"), + ("Share Screen", "Podeli ekran"), + ("CLOSE", "ZATVORI"), + ("OPEN", "OTVORI"), + ("Chat", "Dopisivanje"), + ("Total", "Ukupno"), + ("items", "stavki"), + ("Selected", "Izabrano"), + ("Screen Capture", "Snimanje ekrana"), + ("Input Control", "Kontrola unosa"), + ("Audio Capture", "Snimanje zvuka"), + ("File Connection", "Spajanje preko datoteke"), + ("Screen Connection", "Podeli konekciju"), + ("Do you accept?", "Prihvatate?"), + ("Open System Setting", "Postavke otvorenog sistema"), + ("How to get Android input permission?", "Kako dobiti pristup za Android unos?"), + ("android_input_permission_tip1", "Da bi daljinski uređaj kontrolisao vaš Android uređaj preko miša ili na dodir, treba da dozvolite RustDesk-u da koristi \"Accessibility\" servis."), + ("android_input_permission_tip2", "Molimo pređite na sledeću stranicu sistemskih podešavanja, pronađite i unesite [Installed Services], uključite [RustDesk Input] servis."), + ("android_new_connection_tip", "Primljen je novi zahtev za upravljanje, koji želi da upravlja ovim vašim uređajem."), + ("android_service_will_start_tip", "Uključenje \"Screen Capture\" automatski će pokrenuti servis, dozvoljavajući drugim uređajima da zahtevaju spajanje na vaš uređaj."), + ("android_stop_service_tip", "Zatvaranje servisa automatski će zatvoriti sve uspostavljene konekcije."), + ("android_version_audio_tip", "Tekuća Android verzija ne podržava audio snimanje, molimo nadogradite na Android 10 ili veći."), + ("android_start_service_tip", "Kliknite [Start Service] ili OPEN [Screen Capture] dozvolu da pokrenete servis deljenja ekrana."), + ("Account", "Nalog"), + ("Overwrite", "Prepiši preko"), + ("This file exists, skip or overwrite this file?", "Ova datoteka postoji, preskoči ili prepiši preko?"), + ("Quit", "Izlaz"), + ("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"), + ("doc_fix_wayland", "https://rustdesk.com/docs/en/manual/linux/#x11-required"), + ("server_not_support", "Server još uvek ne podržava"), + ("Help", "Pomoć"), + ("Failed", "Greška"), + ("Succeeded", "Uspešno"), + ("Someone turns on privacy mode, exit", "Neko je uključio mod privatnosti, izlaz."), + ("Unsupported", "Nepodržano"), + ("Peer denied", "Klijent zabranjen"), + ("Please install plugins", "Molimo instalirajte dodatke"), + ("Peer exit", "Klijent izašao"), + ("Failed to turn off", "Greška kod isključenja"), + ("Turned off", "Isključeno"), + ("In privacy mode", "U modu privatnosti"), + ("Out privacy mode", "Van moda privatnosti"), + ("Language", "Jezik"), + ("Keep RustDesk background service", "Zadrži RustDesk kao pozadinski servis"), + ("Ignore Battery Optimizations", "Zanemari optimizacije baterije"), + ("android_open_battery_optimizations_tip", "Ako želite da onemogućite ovu funkciju, molimo idite na sledeću stranicu za podešavanje RustDesk aplikacije, pronađite i uđite u [Battery], isključite [Unrestricted]"), + ("Connection not allowed", "Konekcija nije dozvoljena"), + ("Legacy mode", "Zastareli mod"), + ("Map mode", "Mod mapiranja"), + ("Translate mode", "Mod prevođenja"), + ("Use permanent password", "Koristi trajnu lozinku"), + ("Use both passwords", "Koristi obe lozinke"), + ("Set permanent password", "Postavi trajnu lozinku"), + ("Enable Remote Restart", "Omogući daljinsko restartovanje"), + ("Allow remote restart", "Dozvoli daljinsko restartovanje"), + ("Restart Remote Device", "Restartuj daljinski uređaj"), + ("Are you sure you want to restart", "Da li ste sigurni da želite restart"), + ("Restarting Remote Device", "Restartovanje daljinskog uređaja"), + ("remote_restarting_tip", "Udaljeni uređaj se restartuje, molimo zatvorite ovu poruku i ponovo se kasnije povežite trajnom šifrom"), + ("Are you sure to close the connection?", "Da li ste sigurni da želite da zatvorite konekciju?"), + + ("Copied", "Kopirano"), + ("Exit Fullscreen", "Napusti mod celog ekrana"), + ("Fullscreen", "Mod celog ekrana"), + ("Mobile Actions", "Mobilne akcije"), + ("Select Monitor", "Izbor monitora"), + ("Control Actions", "Upravljačke akcije"), + ("Display Settings", "Postavke prikaza"), + ("Ratio", "Odnos"), + ("Image Quality", "Kvalitet slike"), + ("Scroll Style", "Stil skrolovanja"), + ("Show Menubar", "Prikaži meni"), + ("Hide Menubar", "Sakrij meni"), + ("Direct Connection", "Direktna konekcija"), + ("Relay Connection", "Posredna konekcija"), + ("Secure Connection", "Bezbedna konekcija"), + ("Insecure Connection", "Nebezbedna konekcija"), + ("Scale original", "Skaliraj original"), + ("Scale adaptive", "Adaptivno skaliranje"), + ("General", "Uopšteno"), + ("Security", "Bezbednost"), + ("Account", "Nalog"), + ("Theme", "Tema"), + ("Dark Theme", "Tamna tema"), + ("Dark", "Tamno"), + ("Light", "Svetlo"), + ("Follow System", "Prati sistem"), + ("Enable hardware codec", "Omogući hardverski kodek"), + ("Unlock Security Settings", "Otključaj postavke bezbednosti"), + ("Enable Audio", "Dozvoli zvuk"), + ("Unlock Network Settings", "Otključaj postavke mreže"), + ("Server", "Server"), + ("Direct IP Access", "Direktan IP pristup"), + ("Proxy", "Proksi"), + ("Port", "Port"), + ("Apply", "Primeni"), + ("Disconnect all devices?", "Otkači sve uređaju?"), + ("Clear", "Obriši"), + ("Audio Input Device", "Uređaj za ulaz zvuka"), + ("Deny remote access", "Zabrani daljinski pristup"), + ("Use IP Whitelisting", "Koristi listu pouzdanih IP"), + ("Network", "Mreža"), + ("Enable RDP", "Dozvoli RDP"), + ("Pin menubar", "Zakači meni"), + ("Unpin menubar", "Otkači meni"), + ("Recording", "Snimanje"), + ("Directory", "Direktorijum"), + ("Automatically record incoming sessions", "Automatski snimaj dolazne sesije"), + ("Change", "Promeni"), + ("Start session recording", "Započni snimanje sesije"), + ("Stop session recording", "Zaustavi snimanje sesije"), + ("Enable Recording Session", "Omogući snimanje sesije"), + ("Allow recording session", "Dozvoli snimanje sesije"), + ("Enable LAN Discovery", "Omogući LAN otkrivanje"), + ("Deny LAN Discovery", "Zabrani LAN otkrivanje"), + ("Write a message", "Napiši poruku"), + ("Prompt", "Prompt"), + ("Please wait for confirmation of UAC...", "Molimo sačekajte UAC potvrdu..."), + ("elevated_foreground_window_tip", "Tekući prozor udaljene radne površine zahteva veću privilegiju za rad, tako da trenutno nije moguće koristiti miša i tastaturu. Možete zahtevati od udaljenog korisnika da minimizira aktivni prozor, ili kliknuti na taster za podizanje privilegija u prozoru za rad sa konekcijom. Da biste prevazišli ovaj problem, preporučljivo je da instalirate softver na udaljeni uređaj."), + ("Disconnected", "Odspojeno"), + ("Other", "Ostalo"), + ("Confirm before closing multiple tabs", "Potvrdite pre zatvaranja više kartica"), + ("Keyboard Settings", "Postavke tastature"), + ("Custom", "Korisnički"), + ("Full Access", "Pun pristup"), + ("Screen Share", "Deljenje ekrana"), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland zahteva Ubuntu 21.04 ili veću verziju"), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland zahteva veću verziju Linux distribucije. Molimo pokušajte X11 ili promenite OS."), + ("JumpLink", "Vidi"), + ("Stop service", "Stopiraj servis"), + ("Please Select the screen to be shared(Operate on the peer side).", "Molimo izaberite eekran koji će biti podeljen (Za rad na klijent strani)"), + ("Show RustDesk", "Prikazi RustDesk"), + ("This PC", "Ovaj PC"), + ("or", "ili"), + ("Continue with", "Nastavi sa"), + ("Elevate", "Izdigni"), + ("Zoom cursor", "Zumiraj kursor"), + ("Accept sessions via password", "Prihvati sesije preko lozinke"), + ("Accept sessions via click", "Prihvati sesije preko klika"), + ("Accept sessions via both", "Prihvati sesije preko oboje"), + ("Please wait for the remote side to accept your session request...", "Molimo sačekajte da udaljena strana prihvati vaš zahtev za sesijom..."), + ("One-time Password", "Jednokratna lozinka"), + ("Use one-time password", "Koristi jednokratnu lozinku"), + ("One-time password length", "Dužina jednokratne lozinke"), + ("Request access to your device", "Zahtev za pristup vašem uređaju"), + ("Hide connection management window", "Sakrij prozor za uređivanje konekcije"), + ("hide_cm_tip", "Skrivanje dozvoljeno samo prihvatanjem sesije preko lozinke i korišćenjem trajne lozinke"), + ("wayland_experiment_tip", "Wayland eksperiment savet"), + ("Right click to select tabs", "Desni klik za izbor kartica"), + ("Add to Address Book", "Dodaj u adresar"), + ("Group", "Grupa"), + ("Search", "Pretraga"), + ].iter().cloned().collect(); +} From 11c96922784ae896b81f4c85abcb481014d06a7f Mon Sep 17 00:00:00 2001 From: asur4s Date: Wed, 14 Dec 2022 00:57:28 -0800 Subject: [PATCH 1194/2015] refacotor: legacy of server --- src/server/input_service.rs | 446 ++++++++++++++++++++---------------- 1 file changed, 251 insertions(+), 195 deletions(-) diff --git a/src/server/input_service.rs b/src/server/input_service.rs index edda4416c..021d85732 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -280,13 +280,17 @@ pub fn mouse_move_relative(x: i32, y: i32) { en.mouse_move_relative(x, y); } -#[cfg(not(target_os = "macos"))] +#[cfg(windows)] fn modifier_sleep() { // sleep for a while, this is only for keying in rdp in peer so far - #[cfg(windows)] std::thread::sleep(std::time::Duration::from_nanos(1)); } +#[inline] +fn is_pressed(key: &Key, en: &mut Enigo) -> bool { + get_modifier_state(key.clone(), en) +} + #[inline] fn get_modifier_state(key: Key, en: &mut Enigo) -> bool { // https://github.com/rustdesk/rustdesk/issues/332 @@ -505,6 +509,7 @@ pub fn handle_mouse_(evt: &MouseEvent) { if key != &Key::CapsLock && key != &Key::NumLock { if !get_modifier_state(key.clone(), &mut en) { en.key_down(key.clone()).ok(); + #[cfg(windows)] modifier_sleep(); to_release.push(key); } @@ -643,100 +648,6 @@ pub async fn lock_screen() { super::video_service::switch_to_primary().await; } -lazy_static::lazy_static! { - static ref KEY_MAP: HashMap = - [ - (ControlKey::Alt, Key::Alt), - (ControlKey::Backspace, Key::Backspace), - (ControlKey::CapsLock, Key::CapsLock), - (ControlKey::Control, Key::Control), - (ControlKey::Delete, Key::Delete), - (ControlKey::DownArrow, Key::DownArrow), - (ControlKey::End, Key::End), - (ControlKey::Escape, Key::Escape), - (ControlKey::F1, Key::F1), - (ControlKey::F10, Key::F10), - (ControlKey::F11, Key::F11), - (ControlKey::F12, Key::F12), - (ControlKey::F2, Key::F2), - (ControlKey::F3, Key::F3), - (ControlKey::F4, Key::F4), - (ControlKey::F5, Key::F5), - (ControlKey::F6, Key::F6), - (ControlKey::F7, Key::F7), - (ControlKey::F8, Key::F8), - (ControlKey::F9, Key::F9), - (ControlKey::Home, Key::Home), - (ControlKey::LeftArrow, Key::LeftArrow), - (ControlKey::Meta, Key::Meta), - (ControlKey::Option, Key::Option), - (ControlKey::PageDown, Key::PageDown), - (ControlKey::PageUp, Key::PageUp), - (ControlKey::Return, Key::Return), - (ControlKey::RightArrow, Key::RightArrow), - (ControlKey::Shift, Key::Shift), - (ControlKey::Space, Key::Space), - (ControlKey::Tab, Key::Tab), - (ControlKey::UpArrow, Key::UpArrow), - (ControlKey::Numpad0, Key::Numpad0), - (ControlKey::Numpad1, Key::Numpad1), - (ControlKey::Numpad2, Key::Numpad2), - (ControlKey::Numpad3, Key::Numpad3), - (ControlKey::Numpad4, Key::Numpad4), - (ControlKey::Numpad5, Key::Numpad5), - (ControlKey::Numpad6, Key::Numpad6), - (ControlKey::Numpad7, Key::Numpad7), - (ControlKey::Numpad8, Key::Numpad8), - (ControlKey::Numpad9, Key::Numpad9), - (ControlKey::Cancel, Key::Cancel), - (ControlKey::Clear, Key::Clear), - (ControlKey::Menu, Key::Alt), - (ControlKey::Pause, Key::Pause), - (ControlKey::Kana, Key::Kana), - (ControlKey::Hangul, Key::Hangul), - (ControlKey::Junja, Key::Junja), - (ControlKey::Final, Key::Final), - (ControlKey::Hanja, Key::Hanja), - (ControlKey::Kanji, Key::Kanji), - (ControlKey::Convert, Key::Convert), - (ControlKey::Select, Key::Select), - (ControlKey::Print, Key::Print), - (ControlKey::Execute, Key::Execute), - (ControlKey::Snapshot, Key::Snapshot), - (ControlKey::Insert, Key::Insert), - (ControlKey::Help, Key::Help), - (ControlKey::Sleep, Key::Sleep), - (ControlKey::Separator, Key::Separator), - (ControlKey::Scroll, Key::Scroll), - (ControlKey::NumLock, Key::NumLock), - (ControlKey::RWin, Key::RWin), - (ControlKey::Apps, Key::Apps), - (ControlKey::Multiply, Key::Multiply), - (ControlKey::Add, Key::Add), - (ControlKey::Subtract, Key::Subtract), - (ControlKey::Decimal, Key::Decimal), - (ControlKey::Divide, Key::Divide), - (ControlKey::Equals, Key::Equals), - (ControlKey::NumpadEnter, Key::NumpadEnter), - (ControlKey::RAlt, Key::RightAlt), - (ControlKey::RControl, Key::RightControl), - (ControlKey::RShift, Key::RightShift), - ].iter().map(|(a, b)| (a.value(), b.clone())).collect(); - static ref NUMPAD_KEY_MAP: HashMap = - [ - (ControlKey::Home, true), - (ControlKey::UpArrow, true), - (ControlKey::PageUp, true), - (ControlKey::LeftArrow, true), - (ControlKey::RightArrow, true), - (ControlKey::End, true), - (ControlKey::DownArrow, true), - (ControlKey::PageDown, true), - (ControlKey::Insert, true), - (ControlKey::Delete, true), - ].iter().map(|(a, b)| (a.value(), b.clone())).collect(); -} - pub fn handle_key(evt: &KeyEvent) { #[cfg(target_os = "macos")] if !*IS_SERVER { @@ -766,14 +677,18 @@ fn rdev_key_down_or_up(key: RdevKey, down_or_up: bool) { std::thread::sleep(Duration::from_millis(20)); } -fn is_modifier_in_key_event(modifier: ControlKey, key_event: &KeyEvent) -> bool { +fn is_modifier_in_key_event(control_key: ControlKey, key_event: &KeyEvent) -> bool { key_event .modifiers .iter() - .position(|&m| m == modifier.into()) + .position(|&m| m == control_key.into()) .is_some() } +fn control_key_to_key(control_key: &EnumOrUnknown) -> Option<&Key> { + KEY_MAP.get(&control_key.value()) +} + fn is_not_same_status(client_locking: bool, remote_locking: bool) -> bool { client_locking != remote_locking } @@ -878,123 +793,160 @@ fn map_keyboard_mode(evt: &KeyEvent) { return; } -fn legacy_keyboard_mode(evt: &KeyEvent) { - #[cfg(windows)] - crate::platform::windows::try_change_desktop(); - let mut en = ENIGO.lock().unwrap(); - - #[cfg(target_os = "macos")] - en.reset_flag(); +#[cfg(target_os = "macos")] +fn add_flags_to_enigo(en: &mut Enigo, key_event: &KeyEvent) { // When long-pressed the command key, then press and release // the Tab key, there should be CGEventFlagCommand in the flag. - #[cfg(target_os = "macos")] + en.reset_flag(); for ck in evt.modifiers.iter() { if let Some(key) = KEY_MAP.get(&ck.value()) { en.add_flag(key); } } - #[cfg(not(target_os = "macos"))] - let mut to_release = Vec::new(); +} - if evt.down { - let ck = if let Some(key_event::Union::ControlKey(ck)) = evt.union { - ck.value() - } else { - -1 - }; - fix_modifiers(&evt.modifiers[..], &mut en, ck); - for ref ck in evt.modifiers.iter() { - if let Some(key) = KEY_MAP.get(&ck.value()) { +fn get_control_key_value(key_event: &KeyEvent) -> i32 { + if let Some(key_event::Union::ControlKey(ck)) = key_event.union { + ck.value() + } else { + -1 + } +} + +fn release_unpressed_modifiers(en: &mut Enigo, key_event: &KeyEvent) { + let ck_value = get_control_key_value(key_event); + fix_modifiers(&key_event.modifiers[..], en, ck_value); +} + +fn is_altgr_pressed(en: &mut Enigo) -> bool { + KEYS_DOWN + .lock() + .unwrap() + .get(&(ControlKey::RAlt.value() as _)) + .is_some() +} + +fn press_modifiers(en: &mut Enigo, key_event: &KeyEvent, to_release: &mut Vec) { + for ref ck in key_event.modifiers.iter() { + if let Some(key) = control_key_to_key(ck) { + if !is_pressed(key, en) { #[cfg(target_os = "linux")] - if key == &Key::Alt && !get_modifier_state(key.clone(), &mut en) { - // for AltGr on Linux - if KEYS_DOWN - .lock() - .unwrap() - .get(&(ControlKey::RAlt.value() as _)) - .is_some() - { - continue; - } - } - #[cfg(not(target_os = "macos"))] - if !get_modifier_state(key.clone(), &mut en) { - en.key_down(key.clone()).ok(); - modifier_sleep(); - to_release.push(key); + if key == &Key::Alt && is_altgr_pressed(en) { + continue; } + en.key_down(key.clone()).ok(); + to_release.push(key.clone()); + #[cfg(windows)] + modifier_sleep(); } } } +} - match evt.union { - Some(key_event::Union::ControlKey(ck)) => { - if let Some(key) = KEY_MAP.get(&ck.value()) { - if evt.down { - en.key_down(key.clone()).ok(); - KEYS_DOWN - .lock() - .unwrap() - .insert(ck.value() as _, Instant::now()); - } else { - en.key_up(key.clone()); - KEYS_DOWN.lock().unwrap().remove(&(ck.value() as _)); - } - } else if ck.value() == ControlKey::CtrlAltDel.value() { - // have to spawn new thread because send_sas is tokio_main, the caller can not be tokio_main. - std::thread::spawn(|| { - allow_err!(send_sas()); - }); - } else if ck.value() == ControlKey::LockScreen.value() { - lock_screen_2(); - } - } - Some(key_event::Union::Chr(chr)) => { - if evt.down { - if en.key_down(get_layout(chr)).is_ok() { - KEYS_DOWN - .lock() - .unwrap() - .insert(chr as u64 + KEY_CHAR_START, Instant::now()); - } else { - if let Ok(chr) = char::try_from(chr) { - let mut x = chr.to_string(); - if get_modifier_state(Key::Shift, &mut en) - || get_modifier_state(Key::CapsLock, &mut en) - { - x = x.to_uppercase(); - } - en.key_sequence(&x); - } - } - KEYS_DOWN - .lock() - .unwrap() - .insert(chr as u64 + KEY_CHAR_START, Instant::now()); - } else { - en.key_up(get_layout(chr)); - KEYS_DOWN - .lock() - .unwrap() - .remove(&(chr as u64 + KEY_CHAR_START)); - } - } - Some(key_event::Union::Unicode(chr)) => { - if let Ok(chr) = char::try_from(chr) { - en.key_sequence(&chr.to_string()); - } - } - Some(key_event::Union::Seq(ref seq)) => { - en.key_sequence(&seq); - } - _ => {} +fn sync_modifiers(en: &mut Enigo, key_event: &KeyEvent, to_release: &mut Vec) { + #[cfg(target_os = "macos")] + add_flag_to_enigo(&mut en, key_event); + + if key_event.down { + release_unpressed_modifiers(en, key_event); + #[cfg(not(target_os = "macos"))] + press_modifiers(en, key_event, to_release); } - #[cfg(not(target_os = "macos"))] +} + +fn process_control_key(en: &mut Enigo, ck: &EnumOrUnknown, down: bool) { + let mut key_down = KEYS_DOWN.lock().unwrap(); + + if ck.value() == ControlKey::CtrlAltDel.value() { + // have to spawn new thread because send_sas is tokio_main, the caller can not be tokio_main. + std::thread::spawn(|| { + allow_err!(send_sas()); + }); + } else if ck.value() == ControlKey::LockScreen.value() { + lock_screen_2(); + } else if let Some(key) = control_key_to_key(ck) { + if down { + en.key_down(key.clone()).ok(); + key_down.insert(ck.value() as _, Instant::now()); + } else { + en.key_up(key.clone()); + key_down.remove(&(ck.value() as _)); + } + } +} + +#[inline] +fn chr_to_record_chr(chr: u32) -> u64 { + chr as u64 + KEY_CHAR_START +} + +#[inline] +fn need_to_uppercase(en: &mut Enigo) -> bool { + get_modifier_state(Key::Shift, en) || get_modifier_state(Key::CapsLock, en) +} + +fn process_chr(en: &mut Enigo, chr: u32, down: bool) { + let mut key_down = KEYS_DOWN.lock().unwrap(); + let key = get_layout(chr); + let record_chr = chr_to_record_chr(chr); + + if down { + if en.key_down(key).is_ok() { + key_down.insert(record_chr, Instant::now()); + } else { + if let Ok(chr) = char::try_from(chr) { + let mut s = chr.to_string(); + if need_to_uppercase(en) { + s = s.to_uppercase(); + } + en.key_sequence(&s); + }; + } + key_down.insert(record_chr, Instant::now()); + } else { + en.key_up(key); + key_down.remove(&record_chr); + } +} + +fn process_unicode(en: &mut Enigo, chr: u32) { + if let Ok(chr) = char::try_from(chr) { + en.key_sequence(&chr.to_string()); + } +} + +fn process_seq(en: &mut Enigo, sequence: &str) { + en.key_sequence(&sequence); +} + +fn release_keys(en: &mut Enigo, to_release: &Vec) { for key in to_release { en.key_up(key.clone()); } } +fn legacy_keyboard_mode(evt: &KeyEvent) { + #[cfg(windows)] + crate::platform::windows::try_change_desktop(); + #[cfg(not(target_os = "macos"))] + let mut to_release: Vec = Vec::new(); + + let mut en = ENIGO.lock().unwrap(); + sync_modifiers(&mut en, &evt, &mut to_release); + + let down = evt.down; + match evt.union { + Some(key_event::Union::ControlKey(ck)) => process_control_key(&mut en, &ck, down), + Some(key_event::Union::Chr(chr)) => process_chr(&mut en, chr, down), + Some(key_event::Union::Unicode(chr)) => process_unicode(&mut en, chr), + Some(key_event::Union::Seq(ref seq)) => process_seq(&mut en, seq), + _ => {} + } + + #[cfg(not(target_os = "macos"))] + release_keys(&mut en, &to_release); +} + pub fn handle_key_(evt: &KeyEvent) { if EXITING.load(Ordering::SeqCst) { return; @@ -1027,3 +979,107 @@ async fn send_sas() -> ResultType<()> { timeout(1000, stream.send(&crate::ipc::Data::SAS)).await??; Ok(()) } + +lazy_static::lazy_static! { + static ref MODIFIER_MAP: HashMap = [ + (ControlKey::Alt, Key::Alt), + (ControlKey::RAlt, Key::RightAlt), + (ControlKey::Control, Key::Control), + (ControlKey::RControl, Key::RightControl), + (ControlKey::Shift, Key::Shift), + (ControlKey::RShift, Key::RightShift), + (ControlKey::Meta, Key::Meta), + (ControlKey::RWin, Key::RWin), + ].iter().map(|(a, b)| (a.value(), b.clone())).collect(); + static ref KEY_MAP: HashMap = + [ + (ControlKey::Alt, Key::Alt), + (ControlKey::Backspace, Key::Backspace), + (ControlKey::CapsLock, Key::CapsLock), + (ControlKey::Control, Key::Control), + (ControlKey::Delete, Key::Delete), + (ControlKey::DownArrow, Key::DownArrow), + (ControlKey::End, Key::End), + (ControlKey::Escape, Key::Escape), + (ControlKey::F1, Key::F1), + (ControlKey::F10, Key::F10), + (ControlKey::F11, Key::F11), + (ControlKey::F12, Key::F12), + (ControlKey::F2, Key::F2), + (ControlKey::F3, Key::F3), + (ControlKey::F4, Key::F4), + (ControlKey::F5, Key::F5), + (ControlKey::F6, Key::F6), + (ControlKey::F7, Key::F7), + (ControlKey::F8, Key::F8), + (ControlKey::F9, Key::F9), + (ControlKey::Home, Key::Home), + (ControlKey::LeftArrow, Key::LeftArrow), + (ControlKey::Meta, Key::Meta), + (ControlKey::Option, Key::Option), + (ControlKey::PageDown, Key::PageDown), + (ControlKey::PageUp, Key::PageUp), + (ControlKey::Return, Key::Return), + (ControlKey::RightArrow, Key::RightArrow), + (ControlKey::Shift, Key::Shift), + (ControlKey::Space, Key::Space), + (ControlKey::Tab, Key::Tab), + (ControlKey::UpArrow, Key::UpArrow), + (ControlKey::Numpad0, Key::Numpad0), + (ControlKey::Numpad1, Key::Numpad1), + (ControlKey::Numpad2, Key::Numpad2), + (ControlKey::Numpad3, Key::Numpad3), + (ControlKey::Numpad4, Key::Numpad4), + (ControlKey::Numpad5, Key::Numpad5), + (ControlKey::Numpad6, Key::Numpad6), + (ControlKey::Numpad7, Key::Numpad7), + (ControlKey::Numpad8, Key::Numpad8), + (ControlKey::Numpad9, Key::Numpad9), + (ControlKey::Cancel, Key::Cancel), + (ControlKey::Clear, Key::Clear), + (ControlKey::Menu, Key::Alt), + (ControlKey::Pause, Key::Pause), + (ControlKey::Kana, Key::Kana), + (ControlKey::Hangul, Key::Hangul), + (ControlKey::Junja, Key::Junja), + (ControlKey::Final, Key::Final), + (ControlKey::Hanja, Key::Hanja), + (ControlKey::Kanji, Key::Kanji), + (ControlKey::Convert, Key::Convert), + (ControlKey::Select, Key::Select), + (ControlKey::Print, Key::Print), + (ControlKey::Execute, Key::Execute), + (ControlKey::Snapshot, Key::Snapshot), + (ControlKey::Insert, Key::Insert), + (ControlKey::Help, Key::Help), + (ControlKey::Sleep, Key::Sleep), + (ControlKey::Separator, Key::Separator), + (ControlKey::Scroll, Key::Scroll), + (ControlKey::NumLock, Key::NumLock), + (ControlKey::RWin, Key::RWin), + (ControlKey::Apps, Key::Apps), + (ControlKey::Multiply, Key::Multiply), + (ControlKey::Add, Key::Add), + (ControlKey::Subtract, Key::Subtract), + (ControlKey::Decimal, Key::Decimal), + (ControlKey::Divide, Key::Divide), + (ControlKey::Equals, Key::Equals), + (ControlKey::NumpadEnter, Key::NumpadEnter), + (ControlKey::RAlt, Key::RightAlt), + (ControlKey::RControl, Key::RightControl), + (ControlKey::RShift, Key::RightShift), + ].iter().map(|(a, b)| (a.value(), b.clone())).collect(); + static ref NUMPAD_KEY_MAP: HashMap = + [ + (ControlKey::Home, true), + (ControlKey::UpArrow, true), + (ControlKey::PageUp, true), + (ControlKey::LeftArrow, true), + (ControlKey::RightArrow, true), + (ControlKey::End, true), + (ControlKey::DownArrow, true), + (ControlKey::PageDown, true), + (ControlKey::Insert, true), + (ControlKey::Delete, true), + ].iter().map(|(a, b)| (a.value(), b.clone())).collect(); +} From 8a4f4449c848de1ee2e4b26427775c472ae928b3 Mon Sep 17 00:00:00 2001 From: Boban Jovanovic Date: Thu, 15 Dec 2022 13:16:20 +0100 Subject: [PATCH 1195/2015] Serbian language added --- src/lang.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/lang.rs b/src/lang.rs index cec9801c2..bed0d7aa1 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -28,6 +28,7 @@ mod ca; mod gr; mod sv; mod sq; +mod sr; lazy_static::lazy_static! { pub static ref LANGS: Value = @@ -59,6 +60,7 @@ lazy_static::lazy_static! { ("gr", "Ελληνικά"), ("sv", "Svenska"), ("sq", "Shqip"), + ("sr", "Srpski"), ]); } @@ -114,6 +116,7 @@ pub fn translate_locale(name: String, locale: &str) -> String { "gr" => gr::T.deref(), "sv" => sv::T.deref(), "sq" => sq::T.deref(), + "sr" => sr::T.deref(), _ => en::T.deref(), }; if let Some(v) = m.get(&name as &str) { From bf1b11c3817688ba75b9c71e78266cc23677906b Mon Sep 17 00:00:00 2001 From: Boban Jovanovic Date: Thu, 15 Dec 2022 13:26:34 +0100 Subject: [PATCH 1196/2015] Serbian name --- src/lang.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang.rs b/src/lang.rs index bed0d7aa1..c9c49abc1 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -28,7 +28,7 @@ mod ca; mod gr; mod sv; mod sq; -mod sr; +mod sr; // Serbian lazy_static::lazy_static! { pub static ref LANGS: Value = From dffae1f5f54ea016cae9a550b54d8df19a41ac58 Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Thu, 15 Dec 2022 20:51:09 +0800 Subject: [PATCH 1197/2015] Update sr.rs --- src/lang/sr.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 515ae6884..42d593096 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -316,7 +316,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Restarting Remote Device", "Restartovanje daljinskog uređaja"), ("remote_restarting_tip", "Udaljeni uređaj se restartuje, molimo zatvorite ovu poruku i ponovo se kasnije povežite trajnom šifrom"), ("Are you sure to close the connection?", "Da li ste sigurni da želite da zatvorite konekciju?"), - ("Copied", "Kopirano"), ("Exit Fullscreen", "Napusti mod celog ekrana"), ("Fullscreen", "Mod celog ekrana"), From ffba1d4f7a8a626c7bdcb875531e1d587fd7266e Mon Sep 17 00:00:00 2001 From: asur4s Date: Thu, 15 Dec 2022 03:51:50 -0800 Subject: [PATCH 1198/2015] refactor: release key of server --- src/server/input_service.rs | 168 +++++++++++++++++++++++------------- 1 file changed, 107 insertions(+), 61 deletions(-) diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 021d85732..7a1ae6592 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -69,6 +69,7 @@ struct Input { y: i32, } +const KEY_RDEV_START: u64 = 999; const KEY_CHAR_START: u64 = 9999; #[derive(Clone, Default)] @@ -339,7 +340,7 @@ pub fn handle_mouse(evt: &MouseEvent, conn: i32) { pub fn fix_key_down_timeout_loop() { std::thread::spawn(move || loop { - std::thread::sleep(std::time::Duration::from_millis(1_000)); + std::thread::sleep(std::time::Duration::from_millis(10_000)); fix_key_down_timeout(false); }); if let Err(err) = ctrlc::set_handler(move || { @@ -360,38 +361,61 @@ pub fn fix_key_down_timeout_at_exit() { } #[inline] -fn get_layout(key: u32) -> Key { - Key::Layout(std::char::from_u32(key).unwrap_or('\0')) +fn record_key_is_control_key(record_key: u64) -> bool { + record_key < KEY_CHAR_START +} + +#[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 +} + +#[inline] +fn record_key_to_key(record_key: u64) -> Option { + if record_key_is_control_key(record_key) { + control_key_value_to_key(record_key as _) + } else if record_key_is_chr(record_key) { + let chr: u32 = (record_key - KEY_CHAR_START) as _; + Some(char_value_to_key(chr)) + } else { + None + } +} + +#[inline] +fn release_record_key(record_key: u64) { + let func = move || { + if record_key_is_rdev_layout(record_key) { + rdev_key_down_or_up(RdevKey::Unknown((record_key - KEY_RDEV_START) as _), false); + } else if let Some(key) = record_key_to_key(record_key) { + ENIGO.lock().unwrap().key_up(key); + log::debug!("Fixed {:?} timeout", key); + } + }; + + #[cfg(target_os = "macos")] + QUEUE.exec_async(func); + #[cfg(not(target_os = "macos"))] + func(); } fn fix_key_down_timeout(force: bool) { - if KEYS_DOWN.lock().unwrap().is_empty() { + let key_down = KEYS_DOWN.lock().unwrap(); + if key_down.is_empty() { return; } - let cloned = (*KEYS_DOWN.lock().unwrap()).clone(); - for (key, value) in cloned.into_iter() { - if force || value.elapsed().as_millis() >= 360_000 { - KEYS_DOWN.lock().unwrap().remove(&key); - let key = if key < KEY_CHAR_START { - if let Some(key) = KEY_MAP.get(&(key as _)) { - Some(*key) - } else { - None - } - } else { - Some(get_layout((key - KEY_CHAR_START) as _)) - }; - if let Some(key) = key { - let func = move || { - let mut en = ENIGO.lock().unwrap(); - en.key_up(key); - log::debug!("Fixed {:?} timeout", key); - }; - #[cfg(target_os = "macos")] - QUEUE.exec_async(func); - #[cfg(not(target_os = "macos"))] - func(); - } + let cloned = (*key_down).clone(); + drop(key_down); + + for (record_key, time) in cloned.into_iter() { + if force || time.elapsed().as_millis() >= 360_000 { + record_pressed_key(record_key, false); + release_record_key(record_key); } } } @@ -685,8 +709,14 @@ fn is_modifier_in_key_event(control_key: ControlKey, key_event: &KeyEvent) -> bo .is_some() } -fn control_key_to_key(control_key: &EnumOrUnknown) -> Option<&Key> { - KEY_MAP.get(&control_key.value()) +#[inline] +fn control_key_value_to_key(value: i32) -> Option { + KEY_MAP.get(&value).and_then(|k| Some(*k)) +} + +#[inline] +fn char_value_to_key(value: u32) -> Key { + Key::Layout(std::char::from_u32(value).unwrap_or('\0')) } fn is_not_same_status(client_locking: bool, remote_locking: bool) -> bool { @@ -772,6 +802,8 @@ 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(); @@ -818,7 +850,7 @@ fn release_unpressed_modifiers(en: &mut Enigo, key_event: &KeyEvent) { fix_modifiers(&key_event.modifiers[..], en, ck_value); } -fn is_altgr_pressed(en: &mut Enigo) -> bool { +fn is_altgr_pressed() -> bool { KEYS_DOWN .lock() .unwrap() @@ -828,10 +860,10 @@ fn is_altgr_pressed(en: &mut Enigo) -> bool { fn press_modifiers(en: &mut Enigo, key_event: &KeyEvent, to_release: &mut Vec) { for ref ck in key_event.modifiers.iter() { - if let Some(key) = control_key_to_key(ck) { - if !is_pressed(key, en) { + if let Some(key) = control_key_value_to_key(ck.value()) { + if !is_pressed(&key, en) { #[cfg(target_os = "linux")] - if key == &Key::Alt && is_altgr_pressed(en) { + if key == Key::Alt && is_altgr_pressed() { continue; } en.key_down(key.clone()).ok(); @@ -855,44 +887,25 @@ fn sync_modifiers(en: &mut Enigo, key_event: &KeyEvent, to_release: &mut Vec, down: bool) { - let mut key_down = KEYS_DOWN.lock().unwrap(); - - if ck.value() == ControlKey::CtrlAltDel.value() { - // have to spawn new thread because send_sas is tokio_main, the caller can not be tokio_main. - std::thread::spawn(|| { - allow_err!(send_sas()); - }); - } else if ck.value() == ControlKey::LockScreen.value() { - lock_screen_2(); - } else if let Some(key) = control_key_to_key(ck) { + if let Some(key) = control_key_value_to_key(ck.value()) { if down { - en.key_down(key.clone()).ok(); - key_down.insert(ck.value() as _, Instant::now()); + en.key_down(key).ok(); } else { - en.key_up(key.clone()); - key_down.remove(&(ck.value() as _)); + en.key_up(key); } } } -#[inline] -fn chr_to_record_chr(chr: u32) -> u64 { - chr as u64 + KEY_CHAR_START -} - #[inline] fn need_to_uppercase(en: &mut Enigo) -> bool { get_modifier_state(Key::Shift, en) || get_modifier_state(Key::CapsLock, en) } fn process_chr(en: &mut Enigo, chr: u32, down: bool) { - let mut key_down = KEYS_DOWN.lock().unwrap(); - let key = get_layout(chr); - let record_chr = chr_to_record_chr(chr); + let key = char_value_to_key(chr); if down { if en.key_down(key).is_ok() { - key_down.insert(record_chr, Instant::now()); } else { if let Ok(chr) = char::try_from(chr) { let mut s = chr.to_string(); @@ -902,10 +915,8 @@ fn process_chr(en: &mut Enigo, chr: u32, down: bool) { en.key_sequence(&s); }; } - key_down.insert(record_chr, Instant::now()); } else { en.key_up(key); - key_down.remove(&record_chr); } } @@ -925,6 +936,30 @@ fn release_keys(en: &mut Enigo, to_release: &Vec) { } } +fn record_pressed_key(record_key: u64, down: bool) { + let mut key_down = KEYS_DOWN.lock().unwrap(); + if down { + key_down.insert(record_key, Instant::now()); + } else { + key_down.remove(&record_key); + } +} + +fn is_function_key(ck: &EnumOrUnknown) -> bool { + let mut res = false; + if ck.value() == ControlKey::CtrlAltDel.value() { + // have to spawn new thread because send_sas is tokio_main, the caller can not be tokio_main. + std::thread::spawn(|| { + allow_err!(send_sas()); + }); + res = true; + } else if ck.value() == ControlKey::LockScreen.value() { + lock_screen_2(); + res = true; + } + return res; +} + fn legacy_keyboard_mode(evt: &KeyEvent) { #[cfg(windows)] crate::platform::windows::try_change_desktop(); @@ -936,8 +971,19 @@ fn legacy_keyboard_mode(evt: &KeyEvent) { let down = evt.down; match evt.union { - Some(key_event::Union::ControlKey(ck)) => process_control_key(&mut en, &ck, down), - Some(key_event::Union::Chr(chr)) => process_chr(&mut en, chr, down), + Some(key_event::Union::ControlKey(ck)) => { + if is_function_key(&ck) { + return; + } + let record_key = ck.value() as u64; + record_pressed_key(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); + process_chr(&mut en, chr, down) + } Some(key_event::Union::Unicode(chr)) => process_unicode(&mut en, chr), Some(key_event::Union::Seq(ref seq)) => process_seq(&mut en, seq), _ => {} From 870822e99d97985b098530763ca20ed63d0cd96f Mon Sep 17 00:00:00 2001 From: Boban Jovanovic Date: Thu, 15 Dec 2022 18:54:13 +0100 Subject: [PATCH 1199/2015] Update Serbian language --- src/lang.rs | 2 +- src/lang/es.rs | 4 ++-- src/lang/sr.rs | 18 +++++++++--------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/lang.rs b/src/lang.rs index c9c49abc1..bed0d7aa1 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -28,7 +28,7 @@ mod ca; mod gr; mod sv; mod sq; -mod sr; // Serbian +mod sr; lazy_static::lazy_static! { pub static ref LANGS: Value = diff --git a/src/lang/es.rs b/src/lang/es.rs index 3e7def305..6c5516539 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -400,7 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("wayland_experiment_tip", "El soporte para Wayland está en fase experimental, por favor, use X11 si necesita acceso desatendido."), ("Right click to select tabs", "Clic derecho para seleccionar pestañas"), ("Add to Address Book", "Añadir a la libreta de direcciones"), - ("Group", ""), - ("Search", ""), + ("Group", "Grupo"), + ("Search", "Búsqueda"), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 42d593096..1465d1fac 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -43,7 +43,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Audio Input", "Audio ulaz"), ("Enhancements", "Proširenja"), ("Hardware Codec", "Hardverski kodek"), - ("Adaptive Bitrate", "Prilagodljiva brzina uzorka"), + ("Adaptive Bitrate", "Prilagodljiva gustina podataka"), ("ID Server", "ID server"), ("Relay Server", "Posredni server"), ("API Server", "API server"), @@ -83,7 +83,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Refresh File", "Osveži datoteku"), ("Local", "Lokalno"), ("Remote", "Udaljeno"), - ("Remote Computer", "Udaljeno računar"), + ("Remote Computer", "Udaljeni računar"), ("Local Computer", "Lokalni računar"), ("Confirm Delete", "Potvrdite brisanje"), ("Delete", "Brisanje"), @@ -130,15 +130,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Remote desktop is offline", "Udaljeni ekran je isključen"), ("Key mismatch", "Pogrešan ključ"), ("Timeout", "Isteklo vreme"), - ("Failed to connect to relay server", "Gršeka u spajanju na posredni server"), + ("Failed to connect to relay server", "Greška u spajanju na posredni server"), ("Failed to connect via rendezvous server", "Greška u spajanju preko servera za povezivanje"), ("Failed to connect via relay server", "Greška u spajanju preko posrednog servera"), ("Failed to make direct connection to remote desktop", "Greška u direktnom spajanju na udaljenu radnu površinu"), ("Set Password", "Postavi lozinku"), ("OS Password", "OS lozinka"), - ("install_tip", "Zbog UAC (User Account Control - kontrola naloga korisnika), RustDesk ne može raditi pravilno u nekim slučajevima. Da biste prevazišli UAC, kliknite taster ispod da instalirate RustDesk na sistem."), + ("install_tip", "Zbog UAC RustDesk ne može raditi pravilno u nekim slučajevima. Da biste prevazišli UAC, kliknite taster ispod da instalirate RustDesk na sistem."), ("Click to upgrade", "Klik za nadogradnju"), - ("Click to download", "Klik za skidanje"), + ("Click to download", "Klik za preuzimanje"), ("Click to update", "Klik za ažuriranje"), ("Configure", "Konfigurisanje"), ("config_acc", "Da biste daljinski kontrolisali radnu površinu, RustDesk-u treba da dodelite \"Accessibility\" prava."), @@ -150,7 +150,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Create start menu shortcuts", "Kreiraj prečice u meniju"), ("Create desktop icon", "Kreiraj ikonicu na radnoj površini"), ("agreement_tip", "Pokretanjem instalacije prihvatate ugovor o licenciranju."), - ("Accept and Install", "Prihvat i instaliraj"), + ("Accept and Install", "Prihvati i instaliraj"), ("End-user license agreement", "Ugovor sa krajnjim korisnikom"), ("Generating ...", "Generisanje..."), ("Your installation is lower version.", "Vaša instalacija je niže verzije"), @@ -179,8 +179,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Relayed and encrypted connection", "Posredna i kriptovana konekcija"), ("Direct and unencrypted connection", "Direktna i nekriptovana konekcija"), ("Relayed and unencrypted connection", "Posredna i nekriptovana konekcija"), - ("Enter Remote ID", "Unesi ID udaljenog uređaja"), - ("Enter your password", "Unesi svoju lozinku"), + ("Enter Remote ID", "Unesite ID udaljenog uređaja"), + ("Enter your password", "Unesite svoju lozinku"), ("Logging in...", "Prijava..."), ("Enable RDP session sharing", "Dozvoli deljenje RDP sesije"), ("Auto Login", "Auto prijavljivanje (Važeće samo ako ste postavili \"Lock after session end\")"), @@ -385,7 +385,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland zahteva veću verziju Linux distribucije. Molimo pokušajte X11 ili promenite OS."), ("JumpLink", "Vidi"), ("Stop service", "Stopiraj servis"), - ("Please Select the screen to be shared(Operate on the peer side).", "Molimo izaberite eekran koji će biti podeljen (Za rad na klijent strani)"), + ("Please Select the screen to be shared(Operate on the peer side).", "Molimo izaberite ekran koji će biti podeljen (Za rad na klijent strani)"), ("Show RustDesk", "Prikazi RustDesk"), ("This PC", "Ovaj PC"), ("or", "ili"), From 4a4336979e791ff078d4e4eb015aa65ab053ff23 Mon Sep 17 00:00:00 2001 From: Agent-JY Date: Fri, 16 Dec 2022 12:37:47 +0100 Subject: [PATCH 1200/2015] Update de.rs --- src/lang/de.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/lang/de.rs b/src/lang/de.rs index 877b5c9ac..1261b405e 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -367,7 +367,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable LAN Discovery", "LAN-Erkennung aktivieren"), ("Deny LAN Discovery", "LAN-Erkennung verbieten"), ("Write a message", "Nachricht schreiben"), - ("Prompt", ""), //Aufforderu"), + ("Prompt", ""), //Aufforderung??? ("Please wait for confirmation of UAC...", "Bitte auf die Bestätigung des Nutzers warten..."), ("elevated_foreground_window_tip", ""), ("Disconnected", "Verbindung abgebrochen"), @@ -396,11 +396,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("One-time password length", "Länge des Einmalpassworts"), ("Request access to your device", "Zugriff zu Ihrem Gerät erbitten"), ("Hide connection management window", "Fenster zur Verwaltung der Verbindung verstecken"), - ("hide_cm_tip", "Dies ist nur möglich, wenn der Zugriff nur über ein permanentes Passwort erfolgt."), // Sehr unklar. Muss noch angepasst werden. Original: Allow hiding only if accepting sessions via password and using pernament passw"), - ("wayland_experiment_tip", ""), + ("hide_cm_tip", "Dies ist nur möglich, wenn der Zugriff nur über ein permanentes Passwort erfolgt."), + ("wayland_experiment_tip", "Die Unterstützung von Wayland ist nur experimentell. Bitte nutzen Sie X11, wenn Sie einen unbeaufsichtigten Zugriff benötigen."), ("Right click to select tabs", "Register mit rechtem Mausklick auswählen"), ("Add to Address Book", "Zum Adressbuch hinzufügen"), - ("Group", ""), - ("Search", ""), + ("Group", "Gruppe"), + ("Search", "Suchen"), ].iter().cloned().collect(); } From 9d2364b307f548ebc958c94e410f6ae4aa39cb6a Mon Sep 17 00:00:00 2001 From: csf Date: Fri, 16 Dec 2022 23:15:26 +0900 Subject: [PATCH 1201/2015] fix Android cannot get id --- flutter/lib/mobile/pages/server_page.dart | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/flutter/lib/mobile/pages/server_page.dart b/flutter/lib/mobile/pages/server_page.dart index 38ad18f14..abccdf683 100644 --- a/flutter/lib/mobile/pages/server_page.dart +++ b/flutter/lib/mobile/pages/server_page.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_hbb/mobile/widgets/dialog.dart'; import 'package:provider/provider.dart'; @@ -107,12 +109,23 @@ class ServerPage extends StatefulWidget implements PageShape { } class _ServerPageState extends State { + Timer? _updateTimer; + @override void initState() { super.initState(); + _updateTimer = periodic_immediate(const Duration(seconds: 3), () async { + await gFFI.serverModel.fetchID(); + }); gFFI.serverModel.checkAndroidPermission(); } + @override + void dispose() { + _updateTimer?.cancel(); + super.dispose(); + } + @override Widget build(BuildContext context) { checkService(); From 3e8c1c46b6ac5047425badbe0dea635b3cb95ce2 Mon Sep 17 00:00:00 2001 From: csf Date: Fri, 16 Dec 2022 23:18:30 +0900 Subject: [PATCH 1202/2015] fix logOut failing, add invoking logOut before id server change --- .../desktop/pages/desktop_setting_page.dart | 4 ++++ flutter/lib/mobile/pages/scan_page.dart | 3 +++ flutter/lib/models/user_model.dart | 22 ++++++++++--------- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 422b4d3e1..0a20c93e1 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -932,6 +932,10 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin { return false; } } + final old = await bind.mainGetOption(key: 'custom-rendezvous-server'); + if (old.isNotEmpty && old != idServer) { + await gFFI.userModel.logOut(); + } // should set one by one await bind.mainSetOption( key: 'custom-rendezvous-server', value: idServer); diff --git a/flutter/lib/mobile/pages/scan_page.dart b/flutter/lib/mobile/pages/scan_page.dart index 3bd381d92..810bcbca3 100644 --- a/flutter/lib/mobile/pages/scan_page.dart +++ b/flutter/lib/mobile/pages/scan_page.dart @@ -261,6 +261,9 @@ void showServerSettingsWithValue(String id, String relay, String key, }); if (await validate()) { if (id != id0) { + if (id0.isNotEmpty) { + await gFFI.userModel.logOut(); + } bind.mainSetOption(key: "custom-rendezvous-server", value: id); } if (relay != relay0) { diff --git a/flutter/lib/models/user_model.dart b/flutter/lib/models/user_model.dart index 751b01637..44fef5443 100644 --- a/flutter/lib/models/user_model.dart +++ b/flutter/lib/models/user_model.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'dart:convert'; -import 'package:flutter_hbb/common/hbbs/hbbs.dart'; import 'package:flutter_hbb/common/widgets/peer_tab_page.dart'; import 'package:get/get.dart'; import 'package:http/http.dart' as http; @@ -78,15 +77,18 @@ class UserModel { Future logOut() async { final tag = gFFI.dialogManager.showLoading(translate('Waiting')); - final url = await bind.mainGetApiServer(); - final _ = await http.post(Uri.parse('$url/api/logout'), - body: { - 'id': await bind.mainGetMyId(), - 'uuid': await bind.mainGetUuid(), - }, - headers: await getHttpHeaders()); - await reset(); - gFFI.dialogManager.dismissByTag(tag); + try { + final url = await bind.mainGetApiServer(); + final _ = await http.post(Uri.parse('$url/api/logout'), + body: { + 'id': await bind.mainGetMyId(), + 'uuid': await bind.mainGetUuid(), + }, + headers: await getHttpHeaders()); + } finally { + await reset(); + gFFI.dialogManager.dismissByTag(tag); + } } Future> login(String userName, String pass) async { From e097dfabf697e3ce2ec1383aedf05709244e2bf0 Mon Sep 17 00:00:00 2001 From: fufesou Date: Sat, 17 Dec 2022 11:56:58 +0800 Subject: [PATCH 1203/2015] fix send ctrl+alt+del Signed-off-by: fufesou --- src/ui_session_interface.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 0cf2f2e2d..f38cb6ad3 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -766,12 +766,10 @@ impl Interface for Session { impl Session { pub fn lock_screen(&self) { - log::info!("Sending key even"); crate::keyboard::client::lock_screen(); } pub fn ctrl_alt_del(&self) { - log::info!("Sending key even"); - crate::keyboard::client::lock_screen(); + crate::keyboard::client::ctrl_alt_del(); } } From 895d339d86cd154679a870ee317a60c1f382ad5f Mon Sep 17 00:00:00 2001 From: 21pages Date: Sat, 17 Dec 2022 12:27:31 +0800 Subject: [PATCH 1204/2015] fix theme sync Signed-off-by: 21pages --- 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 d8a6cd30f..15d058b87 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -230,7 +230,7 @@ class MyTheme { bind.mainSetLocalOption( key: kCommConfKeyTheme, value: mode.toShortString()); } - bind.mainChangeTheme(dark: currentThemeMode().toShortString()); + bind.mainChangeTheme(dark: mode.toShortString()); } } From 747318827b63ec3d4f81c8a74f05de88127a4ade Mon Sep 17 00:00:00 2001 From: 21pages Date: Sat, 17 Dec 2022 12:28:11 +0800 Subject: [PATCH 1205/2015] fix peer tab translation and visibility Signed-off-by: 21pages --- flutter/lib/common/widgets/peer_tab_page.dart | 40 ++++++++++++++----- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/flutter/lib/common/widgets/peer_tab_page.dart b/flutter/lib/common/widgets/peer_tab_page.dart index f6f5c0403..b501bb44b 100644 --- a/flutter/lib/common/widgets/peer_tab_page.dart +++ b/flutter/lib/common/widgets/peer_tab_page.dart @@ -18,6 +18,7 @@ import '../../common.dart'; import '../../models/platform_model.dart'; const int groupTabIndex = 4; +const String defaultGroupTabname = 'Group'; class StatePeerTab { final RxInt currentTab = 0.obs; @@ -26,11 +27,11 @@ class StatePeerTab { final RxList visibleTabOrder = RxList.empty(growable: true); int tabHiddenFlag = 0; final RxList tabNames = [ - translate('Recent Sessions'), - translate('Favorites'), - translate('Discovered'), - translate('Address Book'), - translate('Group'), + 'Recent Sessions', + 'Favorites', + 'Discovered', + 'Address Book', + defaultGroupTabname, ].obs; StatePeerTab._() { @@ -80,7 +81,7 @@ class StatePeerTab { gFFI.userModel.groupName.isNotEmpty) { tabNames[groupTabIndex] = gFFI.userModel.groupName.value; } else { - tabNames[groupTabIndex] = translate('Group'); + tabNames[groupTabIndex] = defaultGroupTabname; } if (isTabHidden(groupTabIndex)) { visibleTabOrder.remove(groupTabIndex); @@ -301,7 +302,7 @@ class _PeerTabPageState extends State child: Align( alignment: Alignment.center, child: Text( - statePeerTab.tabNames[t], // TODO + translatedTabname(t), textAlign: TextAlign.center, style: TextStyle( height: 1, @@ -323,6 +324,23 @@ class _PeerTabPageState extends State }); } + translatedTabname(int index) { + if (index < statePeerTab.tabNames.length) { + final name = statePeerTab.tabNames[index]; + if (index == groupTabIndex) { + if (name == defaultGroupTabname) { + return translate(name); + } else { + return name; + } + } else { + return translate(name); + } + } + assert(false); + return index.toString(); + } + Widget _createPeersView() { final verticalMargin = isDesktop ? 12.0 : 6.0; statePeerTab.visibleTabOrder @@ -414,7 +432,7 @@ class _PeerTabPageState extends State int bitMask = 1 << i; menu.add(MenuEntrySwitch( switchType: SwitchType.scheckbox, - text: statePeerTab.tabNames[i], + text: translatedTabname(i), getter: () async { return statePeerTab.tabHiddenFlag & bitMask == 0; }, @@ -430,9 +448,9 @@ class _PeerTabPageState extends State statePeerTab.visibleTabOrder .removeWhere((e) => statePeerTab.isTabHidden(e)); for (int j = 0; j < statePeerTab.tabNames.length; j++) { - if (!statePeerTab.visibleTabOrder.contains(j) && - !statePeerTab.isTabHidden(j)) { - statePeerTab.visibleTabOrder.add(j); + if (!statePeerTab.isTabHidden(j) && + !(j == groupTabIndex && statePeerTab.filterGroupCard())) { + statePeerTab.addTabInOrder(statePeerTab.visibleTabOrder, j); } } statePeerTab.saveTabOrder(); From 66b2d4bb04c6da4f3ec2a760e3bf34063d594286 Mon Sep 17 00:00:00 2001 From: Boban Jovanovic Date: Thu, 15 Dec 2022 18:54:13 +0100 Subject: [PATCH 1206/2015] Updated Serbian langugage --- src/lang.rs | 2 +- src/lang/es.rs | 4 ++-- src/lang/sr.rs | 26 +++++++++++++------------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/lang.rs b/src/lang.rs index c9c49abc1..bed0d7aa1 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -28,7 +28,7 @@ mod ca; mod gr; mod sv; mod sq; -mod sr; // Serbian +mod sr; lazy_static::lazy_static! { pub static ref LANGS: Value = diff --git a/src/lang/es.rs b/src/lang/es.rs index 3e7def305..6c5516539 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -400,7 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("wayland_experiment_tip", "El soporte para Wayland está en fase experimental, por favor, use X11 si necesita acceso desatendido."), ("Right click to select tabs", "Clic derecho para seleccionar pestañas"), ("Add to Address Book", "Añadir a la libreta de direcciones"), - ("Group", ""), - ("Search", ""), + ("Group", "Grupo"), + ("Search", "Búsqueda"), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 42d593096..c915ad262 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -3,7 +3,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = [ ("Status", "Status"), ("Your Desktop", "Vaša radna površina"), - ("desk_tip", "Tvojoj radnoj površini se može pristupiti ovim ID i lozinkom."), + ("desk_tip", "Vašoj radnoj površini se može pristupiti ovim ID i lozinkom."), ("Password", "Lozinka"), ("Ready", "Spremno"), ("Established", "Uspostavljeno"), @@ -25,7 +25,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Set your own password", "Postavi lozinku"), ("Enable Keyboard/Mouse", "Dozvoli tastaturu/miša"), ("Enable Clipboard", "Dozvoli clipboard"), - ("Enable File Transfer", "Dozvoli prenos fajla"), + ("Enable File Transfer", "Dozvoli prenos fajlova"), ("Enable TCP Tunneling", "Dozvoli TCP tunel"), ("IP Whitelisting", "IP pouzdana lista"), ("ID/Relay Server", "ID/Posredni server"), @@ -43,7 +43,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Audio Input", "Audio ulaz"), ("Enhancements", "Proširenja"), ("Hardware Codec", "Hardverski kodek"), - ("Adaptive Bitrate", "Prilagodljiva brzina uzorka"), + ("Adaptive Bitrate", "Prilagodljiva gustina podataka"), ("ID Server", "ID server"), ("Relay Server", "Posredni server"), ("API Server", "API server"), @@ -83,7 +83,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Refresh File", "Osveži datoteku"), ("Local", "Lokalno"), ("Remote", "Udaljeno"), - ("Remote Computer", "Udaljeno računar"), + ("Remote Computer", "Udaljeni računar"), ("Local Computer", "Lokalni računar"), ("Confirm Delete", "Potvrdite brisanje"), ("Delete", "Brisanje"), @@ -130,15 +130,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Remote desktop is offline", "Udaljeni ekran je isključen"), ("Key mismatch", "Pogrešan ključ"), ("Timeout", "Isteklo vreme"), - ("Failed to connect to relay server", "Gršeka u spajanju na posredni server"), + ("Failed to connect to relay server", "Greška u spajanju na posredni server"), ("Failed to connect via rendezvous server", "Greška u spajanju preko servera za povezivanje"), ("Failed to connect via relay server", "Greška u spajanju preko posrednog servera"), ("Failed to make direct connection to remote desktop", "Greška u direktnom spajanju na udaljenu radnu površinu"), ("Set Password", "Postavi lozinku"), ("OS Password", "OS lozinka"), - ("install_tip", "Zbog UAC (User Account Control - kontrola naloga korisnika), RustDesk ne može raditi pravilno u nekim slučajevima. Da biste prevazišli UAC, kliknite taster ispod da instalirate RustDesk na sistem."), + ("install_tip", "Zbog UAC RustDesk ne može raditi pravilno u nekim slučajevima. Da biste prevazišli UAC, kliknite taster ispod da instalirate RustDesk na sistem."), ("Click to upgrade", "Klik za nadogradnju"), - ("Click to download", "Klik za skidanje"), + ("Click to download", "Klik za preuzimanje"), ("Click to update", "Klik za ažuriranje"), ("Configure", "Konfigurisanje"), ("config_acc", "Da biste daljinski kontrolisali radnu površinu, RustDesk-u treba da dodelite \"Accessibility\" prava."), @@ -150,7 +150,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Create start menu shortcuts", "Kreiraj prečice u meniju"), ("Create desktop icon", "Kreiraj ikonicu na radnoj površini"), ("agreement_tip", "Pokretanjem instalacije prihvatate ugovor o licenciranju."), - ("Accept and Install", "Prihvat i instaliraj"), + ("Accept and Install", "Prihvati i instaliraj"), ("End-user license agreement", "Ugovor sa krajnjim korisnikom"), ("Generating ...", "Generisanje..."), ("Your installation is lower version.", "Vaša instalacija je niže verzije"), @@ -179,8 +179,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Relayed and encrypted connection", "Posredna i kriptovana konekcija"), ("Direct and unencrypted connection", "Direktna i nekriptovana konekcija"), ("Relayed and unencrypted connection", "Posredna i nekriptovana konekcija"), - ("Enter Remote ID", "Unesi ID udaljenog uređaja"), - ("Enter your password", "Unesi svoju lozinku"), + ("Enter Remote ID", "Unesite ID udaljenog uređaja"), + ("Enter your password", "Unesite svoju lozinku"), ("Logging in...", "Prijava..."), ("Enable RDP session sharing", "Dozvoli deljenje RDP sesije"), ("Auto Login", "Auto prijavljivanje (Važeće samo ako ste postavili \"Lock after session end\")"), @@ -341,7 +341,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Dark Theme", "Tamna tema"), ("Dark", "Tamno"), ("Light", "Svetlo"), - ("Follow System", "Prati sistem"), + ("Follow System", "Prema sistemu"), ("Enable hardware codec", "Omogući hardverski kodek"), ("Unlock Security Settings", "Otključaj postavke bezbednosti"), ("Enable Audio", "Dozvoli zvuk"), @@ -376,7 +376,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("elevated_foreground_window_tip", "Tekući prozor udaljene radne površine zahteva veću privilegiju za rad, tako da trenutno nije moguće koristiti miša i tastaturu. Možete zahtevati od udaljenog korisnika da minimizira aktivni prozor, ili kliknuti na taster za podizanje privilegija u prozoru za rad sa konekcijom. Da biste prevazišli ovaj problem, preporučljivo je da instalirate softver na udaljeni uređaj."), ("Disconnected", "Odspojeno"), ("Other", "Ostalo"), - ("Confirm before closing multiple tabs", "Potvrdite pre zatvaranja više kartica"), + ("Confirm before closing multiple tabs", "Potvrda pre zatvaranja više kartica"), ("Keyboard Settings", "Postavke tastature"), ("Custom", "Korisnički"), ("Full Access", "Pun pristup"), @@ -385,7 +385,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland zahteva veću verziju Linux distribucije. Molimo pokušajte X11 ili promenite OS."), ("JumpLink", "Vidi"), ("Stop service", "Stopiraj servis"), - ("Please Select the screen to be shared(Operate on the peer side).", "Molimo izaberite eekran koji će biti podeljen (Za rad na klijent strani)"), + ("Please Select the screen to be shared(Operate on the peer side).", "Molimo izaberite ekran koji će biti podeljen (Za rad na klijent strani)"), ("Show RustDesk", "Prikazi RustDesk"), ("This PC", "Ovaj PC"), ("or", "ili"), From 39f8e2d7123b8565e738e9fa24f2270e80ba065c Mon Sep 17 00:00:00 2001 From: asur4s Date: Thu, 15 Dec 2022 03:51:50 -0800 Subject: [PATCH 1207/2015] refactor: release key of server --- src/server/input_service.rs | 173 +++++++++++++++++++++++------------- 1 file changed, 109 insertions(+), 64 deletions(-) diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 021d85732..66d2b0414 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -69,6 +69,7 @@ struct Input { y: i32, } +const KEY_RDEV_START: u64 = 999; const KEY_CHAR_START: u64 = 9999; #[derive(Clone, Default)] @@ -339,7 +340,7 @@ pub fn handle_mouse(evt: &MouseEvent, conn: i32) { pub fn fix_key_down_timeout_loop() { std::thread::spawn(move || loop { - std::thread::sleep(std::time::Duration::from_millis(1_000)); + std::thread::sleep(std::time::Duration::from_millis(10_000)); fix_key_down_timeout(false); }); if let Err(err) = ctrlc::set_handler(move || { @@ -360,38 +361,61 @@ pub fn fix_key_down_timeout_at_exit() { } #[inline] -fn get_layout(key: u32) -> Key { - Key::Layout(std::char::from_u32(key).unwrap_or('\0')) +fn record_key_is_control_key(record_key: u64) -> bool { + record_key < KEY_CHAR_START +} + +#[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 +} + +#[inline] +fn record_key_to_key(record_key: u64) -> Option { + if record_key_is_control_key(record_key) { + control_key_value_to_key(record_key as _) + } else if record_key_is_chr(record_key) { + let chr: u32 = (record_key - KEY_CHAR_START) as _; + Some(char_value_to_key(chr)) + } else { + None + } +} + +#[inline] +fn release_record_key(record_key: u64) { + let func = move || { + if record_key_is_rdev_layout(record_key) { + rdev_key_down_or_up(RdevKey::Unknown((record_key - KEY_RDEV_START) as _), false); + } else if let Some(key) = record_key_to_key(record_key) { + ENIGO.lock().unwrap().key_up(key); + log::debug!("Fixed {:?} timeout", key); + } + }; + + #[cfg(target_os = "macos")] + QUEUE.exec_async(func); + #[cfg(not(target_os = "macos"))] + func(); } fn fix_key_down_timeout(force: bool) { - if KEYS_DOWN.lock().unwrap().is_empty() { + let key_down = KEYS_DOWN.lock().unwrap(); + if key_down.is_empty() { return; } - let cloned = (*KEYS_DOWN.lock().unwrap()).clone(); - for (key, value) in cloned.into_iter() { - if force || value.elapsed().as_millis() >= 360_000 { - KEYS_DOWN.lock().unwrap().remove(&key); - let key = if key < KEY_CHAR_START { - if let Some(key) = KEY_MAP.get(&(key as _)) { - Some(*key) - } else { - None - } - } else { - Some(get_layout((key - KEY_CHAR_START) as _)) - }; - if let Some(key) = key { - let func = move || { - let mut en = ENIGO.lock().unwrap(); - en.key_up(key); - log::debug!("Fixed {:?} timeout", key); - }; - #[cfg(target_os = "macos")] - QUEUE.exec_async(func); - #[cfg(not(target_os = "macos"))] - func(); - } + let cloned = (*key_down).clone(); + drop(key_down); + + for (record_key, time) in cloned.into_iter() { + if force || time.elapsed().as_millis() >= 360_000 { + record_pressed_key(record_key, false); + release_record_key(record_key); } } } @@ -685,8 +709,14 @@ fn is_modifier_in_key_event(control_key: ControlKey, key_event: &KeyEvent) -> bo .is_some() } -fn control_key_to_key(control_key: &EnumOrUnknown) -> Option<&Key> { - KEY_MAP.get(&control_key.value()) +#[inline] +fn control_key_value_to_key(value: i32) -> Option { + KEY_MAP.get(&value).and_then(|k| Some(*k)) +} + +#[inline] +fn char_value_to_key(value: u32) -> Key { + Key::Layout(std::char::from_u32(value).unwrap_or('\0')) } fn is_not_same_status(client_locking: bool, remote_locking: bool) -> bool { @@ -772,6 +802,8 @@ 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(); @@ -798,7 +830,7 @@ fn add_flags_to_enigo(en: &mut Enigo, key_event: &KeyEvent) { // When long-pressed the command key, then press and release // the Tab key, there should be CGEventFlagCommand in the flag. en.reset_flag(); - for ck in evt.modifiers.iter() { + for ck in key_event.modifiers.iter() { if let Some(key) = KEY_MAP.get(&ck.value()) { en.add_flag(key); } @@ -818,7 +850,7 @@ fn release_unpressed_modifiers(en: &mut Enigo, key_event: &KeyEvent) { fix_modifiers(&key_event.modifiers[..], en, ck_value); } -fn is_altgr_pressed(en: &mut Enigo) -> bool { +fn is_altgr_pressed() -> bool { KEYS_DOWN .lock() .unwrap() @@ -828,10 +860,10 @@ fn is_altgr_pressed(en: &mut Enigo) -> bool { fn press_modifiers(en: &mut Enigo, key_event: &KeyEvent, to_release: &mut Vec) { for ref ck in key_event.modifiers.iter() { - if let Some(key) = control_key_to_key(ck) { - if !is_pressed(key, en) { + if let Some(key) = control_key_value_to_key(ck.value()) { + if !is_pressed(&key, en) { #[cfg(target_os = "linux")] - if key == &Key::Alt && is_altgr_pressed(en) { + if key == Key::Alt && is_altgr_pressed() { continue; } en.key_down(key.clone()).ok(); @@ -845,7 +877,7 @@ fn press_modifiers(en: &mut Enigo, key_event: &KeyEvent, to_release: &mut Vec) { #[cfg(target_os = "macos")] - add_flag_to_enigo(&mut en, key_event); + add_flags_to_enigo(en, key_event); if key_event.down { release_unpressed_modifiers(en, key_event); @@ -855,44 +887,25 @@ fn sync_modifiers(en: &mut Enigo, key_event: &KeyEvent, to_release: &mut Vec, down: bool) { - let mut key_down = KEYS_DOWN.lock().unwrap(); - - if ck.value() == ControlKey::CtrlAltDel.value() { - // have to spawn new thread because send_sas is tokio_main, the caller can not be tokio_main. - std::thread::spawn(|| { - allow_err!(send_sas()); - }); - } else if ck.value() == ControlKey::LockScreen.value() { - lock_screen_2(); - } else if let Some(key) = control_key_to_key(ck) { + if let Some(key) = control_key_value_to_key(ck.value()) { if down { - en.key_down(key.clone()).ok(); - key_down.insert(ck.value() as _, Instant::now()); + en.key_down(key).ok(); } else { - en.key_up(key.clone()); - key_down.remove(&(ck.value() as _)); + en.key_up(key); } } } -#[inline] -fn chr_to_record_chr(chr: u32) -> u64 { - chr as u64 + KEY_CHAR_START -} - #[inline] fn need_to_uppercase(en: &mut Enigo) -> bool { get_modifier_state(Key::Shift, en) || get_modifier_state(Key::CapsLock, en) } fn process_chr(en: &mut Enigo, chr: u32, down: bool) { - let mut key_down = KEYS_DOWN.lock().unwrap(); - let key = get_layout(chr); - let record_chr = chr_to_record_chr(chr); + let key = char_value_to_key(chr); if down { if en.key_down(key).is_ok() { - key_down.insert(record_chr, Instant::now()); } else { if let Ok(chr) = char::try_from(chr) { let mut s = chr.to_string(); @@ -902,10 +915,8 @@ fn process_chr(en: &mut Enigo, chr: u32, down: bool) { en.key_sequence(&s); }; } - key_down.insert(record_chr, Instant::now()); } else { en.key_up(key); - key_down.remove(&record_chr); } } @@ -925,10 +936,33 @@ fn release_keys(en: &mut Enigo, to_release: &Vec) { } } +fn record_pressed_key(record_key: u64, down: bool) { + let mut key_down = KEYS_DOWN.lock().unwrap(); + if down { + key_down.insert(record_key, Instant::now()); + } else { + key_down.remove(&record_key); + } +} + +fn is_function_key(ck: &EnumOrUnknown) -> bool { + let mut res = false; + if ck.value() == ControlKey::CtrlAltDel.value() { + // have to spawn new thread because send_sas is tokio_main, the caller can not be tokio_main. + std::thread::spawn(|| { + allow_err!(send_sas()); + }); + res = true; + } else if ck.value() == ControlKey::LockScreen.value() { + lock_screen_2(); + res = true; + } + return res; +} + fn legacy_keyboard_mode(evt: &KeyEvent) { #[cfg(windows)] crate::platform::windows::try_change_desktop(); - #[cfg(not(target_os = "macos"))] let mut to_release: Vec = Vec::new(); let mut en = ENIGO.lock().unwrap(); @@ -936,8 +970,19 @@ fn legacy_keyboard_mode(evt: &KeyEvent) { let down = evt.down; match evt.union { - Some(key_event::Union::ControlKey(ck)) => process_control_key(&mut en, &ck, down), - Some(key_event::Union::Chr(chr)) => process_chr(&mut en, chr, down), + Some(key_event::Union::ControlKey(ck)) => { + if is_function_key(&ck) { + return; + } + let record_key = ck.value() as u64; + record_pressed_key(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); + process_chr(&mut en, chr, down) + } Some(key_event::Union::Unicode(chr)) => process_unicode(&mut en, chr), Some(key_event::Union::Seq(ref seq)) => process_seq(&mut en, seq), _ => {} From 15bebcf45b0fb94a265214062c0f9f727d2e6034 Mon Sep 17 00:00:00 2001 From: Jimmy GALLAND Date: Sat, 17 Dec 2022 14:40:57 +0100 Subject: [PATCH 1208/2015] add tab about translate --- .../lib/desktop/pages/desktop_setting_page.dart | 16 ++++++++-------- 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 | 1 + 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/ru.rs | 3 +++ src/lang/sk.rs | 3 +++ src/lang/sq.rs | 3 +++ src/lang/sr.rs | 3 +++ src/lang/sv.rs | 3 +++ src/lang/template.rs | 3 +++ src/lang/tr.rs | 3 +++ src/lang/tw.rs | 3 +++ src/lang/ua.rs | 3 +++ src/lang/vn.rs | 3 +++ 31 files changed, 96 insertions(+), 8 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 0a20c93e1..613f19810 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -1106,22 +1106,22 @@ class _AboutState extends State<_About> { const SizedBox( height: 8.0, ), - Text('Version: $version').marginSymmetric(vertical: 4.0), - Text('Build Date: $buildDate').marginSymmetric(vertical: 4.0), + Text(translate('Version') + ': $version').marginSymmetric(vertical: 4.0), + Text(translate('Build Date') + ': $buildDate').marginSymmetric(vertical: 4.0), InkWell( onTap: () { launchUrlString('https://rustdesk.com/privacy'); }, - child: const Text( - 'Privacy Statement', + child: Text( + translate('Privacy Statement'), style: linkStyle, ).marginSymmetric(vertical: 4.0)), InkWell( onTap: () { launchUrlString('https://rustdesk.com'); }, - child: const Text( - 'Website', + child: Text( + translate('Website'), style: linkStyle, ).marginSymmetric(vertical: 4.0)), Container( @@ -1138,8 +1138,8 @@ class _AboutState extends State<_About> { 'Copyright © 2022 Purslane Ltd.\n$license', style: const TextStyle(color: Colors.white), ), - const Text( - 'Made with heart in this chaotic world!', + Text( + translate('Slogan_tip'), style: TextStyle( fontWeight: FontWeight.w800, color: Colors.white), diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 720c448e9..fd9e7c2c9 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -39,6 +39,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Canviar ID"), ("Website", "Lloc web"), ("About", "Sobre"), + ("About RustDesk", ""), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", "Silenciar"), ("Audio Input", "Entrada d'àudio"), ("Enhancements", "Millores"), diff --git a/src/lang/cn.rs b/src/lang/cn.rs index bc5708987..5dce8cd38 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -39,6 +39,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "改变ID"), ("Website", "网站"), ("About", "关于"), + ("About RustDesk", ""), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", "静音"), ("Audio Input", "音频输入"), ("Enhancements", "增强功能"), diff --git a/src/lang/cs.rs b/src/lang/cs.rs index fe0087d40..52571ef07 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -39,6 +39,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Změnit identifikátor"), ("Website", "Webové stránky"), ("About", "O aplikaci"), + ("About RustDesk", ""), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", "Ztlumit"), ("Audio Input", "Vstup zvuku"), ("Enhancements", ""), diff --git a/src/lang/da.rs b/src/lang/da.rs index a17f26918..138777e32 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -39,6 +39,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Ændre ID"), ("Website", "Hjemmeside"), ("About", "Omkring"), + ("About RustDesk", ""), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", "Sluk for mikrofonen"), ("Audio Input", "Lydindgang"), ("Enhancements", ""), diff --git a/src/lang/de.rs b/src/lang/de.rs index 1261b405e..3c04f2ec4 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -39,6 +39,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "ID ändern"), ("Website", "Webseite"), ("About", "Über"), + ("About RustDesk", ""), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", "Stummschalten"), ("Audio Input", "Audioeingang"), ("Enhancements", "Verbesserungen"), diff --git a/src/lang/en.rs b/src/lang/en.rs index b6992230d..f351b575d 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -35,5 +35,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop service", "Stop Service"), ("hide_cm_tip", "Allow hiding only if accepting sessions via password and using permanent password"), ("wayland_experiment_tip", "Wayland support is in experimental stage, please use X11 if you require unattended access."), + ("Slogan_tip", "Made with heart in this chaotic world!"), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index d0705af1b..d22cb2311 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -39,6 +39,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Ŝanĝi identigilon"), ("Website", "Retejo"), ("About", "Pri"), + ("About RustDesk", ""), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", "Muta"), ("Audio Input", "Aŭdia enigo"), ("Enhancements", ""), diff --git a/src/lang/es.rs b/src/lang/es.rs index 6c5516539..ce0254a98 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -39,6 +39,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Cambiar ID"), ("Website", "Sitio web"), ("About", "Acerca de"), + ("About RustDesk", ""), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", "Silenciar"), ("Audio Input", "Entrada de audio"), ("Enhancements", "Mejoras"), diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 88f2e0841..8797f209e 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -39,6 +39,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "تعویض شناسه"), ("Website", "وب سایت"), ("About", "درباره"), + ("About RustDesk", ""), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", "بستن صدا"), ("Audio Input", "ورودی صدا"), ("Enhancements", "بهبودها"), diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 9168426e9..124bfc00c 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -39,6 +39,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Changer d'ID"), ("Website", "Site Web"), ("About", "À propos de"), + ("About RustDesk", "À propos de RustDesk"), + ("Slogan_tip", "Fait avec cœur dans ce monde chaotique!"), + ("Privacy Statement", "Déclaration de confidentialité"), ("Mute", "Muet"), ("Audio Input", "Entrée audio"), ("Enhancements", "Améliorations"), diff --git a/src/lang/gr.rs b/src/lang/gr.rs index b1a8174ee..933a84143 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -39,6 +39,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Αλλαγή αναγνωριστικού ID"), ("Website", "Ιστότοπος"), ("About", "Πληροφορίες"), + ("About RustDesk", ""), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", "Σίγαση"), ("Audio Input", "Είσοδος ήχου"), ("Enhancements", "Βελτιώσεις"), diff --git a/src/lang/hu.rs b/src/lang/hu.rs index ee77b53e6..f3f1e8fd9 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -39,6 +39,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Azonosító megváltoztatása"), ("Website", "Weboldal"), ("About", "Rólunk"), + ("About RustDesk", ""), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", "Némítás"), ("Audio Input", "Hangátvitel"), ("Enhancements", "Fejlesztések"), diff --git a/src/lang/id.rs b/src/lang/id.rs index 173a21e31..89728b3e6 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -39,6 +39,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Ubah ID"), ("Website", "Website"), ("About", "Tentang"), + ("About RustDesk", ""), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", "Bisukan"), ("Audio Input", "Masukkan Audio"), ("Enhancements", "Peningkatan"), diff --git a/src/lang/it.rs b/src/lang/it.rs index 84a41a96a..2237c81db 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -39,6 +39,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Cambia ID"), ("Website", "Sito web"), ("About", "Informazioni"), + ("About RustDesk", ""), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", "Silenzia"), ("Audio Input", "Input audio"), ("Enhancements", "Miglioramenti"), diff --git a/src/lang/ja.rs b/src/lang/ja.rs index e9914c0fe..e40c81ae8 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -39,6 +39,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "IDを変更"), ("Website", "公式サイト"), ("About", "情報"), + ("About RustDesk", ""), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", "ミュート"), ("Audio Input", "音声入力デバイス"), ("Enhancements", "追加機能"), diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 6f514f706..426a027db 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -39,6 +39,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "ID 변경"), ("Website", "웹사이트"), ("About", "정보"), + ("About RustDesk", ""), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", "음소거"), ("Audio Input", "오디오 입력"), ("Enhancements", ""), diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 69c4115ca..6acd892f8 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -39,6 +39,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "ID ауыстыру"), ("Website", "Web-сайт"), ("About", "Туралы"), + ("About RustDesk", ""), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", "Дыбыссыздандыру"), ("Audio Input", "Аудио Еңгізу"), ("Enhancements", "Жақсартулар"), diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 1a6fceb12..54cc10164 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -39,6 +39,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Zmień ID"), ("Website", "Strona internetowa"), ("About", "O"), + ("About RustDesk", ""), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", "Wycisz"), ("Audio Input", "Wejście audio"), ("Enhancements", "Ulepszenia"), diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index f279d6e7a..4d3d057ee 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -39,6 +39,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Alterar ID"), ("Website", "Website"), ("About", "Sobre"), + ("About RustDesk", ""), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", "Silenciar"), ("Audio Input", "Entrada de Áudio"), ("Enhancements", "Melhorias"), diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 18b803ec3..bc878b680 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -39,6 +39,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Alterar ID"), ("Website", "Website"), ("About", "Sobre"), + ("About RustDesk", ""), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", "Desativar som"), ("Audio Input", "Entrada de Áudio"), ("Enhancements", "Melhorias"), diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 74c4aefb7..66c8f7626 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -39,6 +39,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Изменить ID"), ("Website", "Сайт"), ("About", "О программе"), + ("About RustDesk", ""), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", "Отключить звук"), ("Audio Input", "Аудиовход"), ("Enhancements", "Улучшения"), diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 7f7c865cb..e1b82d4f4 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -39,6 +39,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Zmeniť ID"), ("Website", "Webová stránka"), ("About", "O RustDesk"), + ("About RustDesk", ""), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", "Stíšiť"), ("Audio Input", "Zvukový vstup"), ("Enhancements", ""), diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 132b8fcdc..cbc71d4aa 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -39,6 +39,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Ndryshoni ID"), ("Website", "Faqe ëebi"), ("About", "Rreth"), + ("About RustDesk", ""), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", "Pa zë"), ("Audio Input", "Inputi zërit"), ("Enhancements", "Përmirësimet"), diff --git a/src/lang/sr.rs b/src/lang/sr.rs index c915ad262..43490c0b2 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -39,6 +39,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Promeni ID"), ("Website", "Web sajt"), ("About", "O programu"), + ("About RustDesk", ""), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", "Utišaj"), ("Audio Input", "Audio ulaz"), ("Enhancements", "Proširenja"), diff --git a/src/lang/sv.rs b/src/lang/sv.rs index b68537a65..4dcababc0 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -39,6 +39,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Byt ID"), ("Website", "Hemsida"), ("About", "Om"), + ("About RustDesk", ""), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", "Tyst"), ("Audio Input", "Ljud input"), ("Enhancements", "Förbättringar"), diff --git a/src/lang/template.rs b/src/lang/template.rs index 99033faea..34fe5077f 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -39,6 +39,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", ""), ("Website", ""), ("About", ""), + ("About RustDesk", ""), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", ""), ("Audio Input", ""), ("Enhancements", ""), diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 32cd4a374..b0c0686c1 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -39,6 +39,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "ID Değiştir"), ("Website", "Website"), ("About", "Hakkında"), + ("About RustDesk", ""), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", "Sustur"), ("Audio Input", "Ses Girişi"), ("Enhancements", "Geliştirmeler"), diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 2ff28f970..d8d6c5ba0 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -39,6 +39,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "更改 ID"), ("Website", "網站"), ("About", "關於"), + ("About RustDesk", ""), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", "靜音"), ("Audio Input", "音訊輸入"), ("Enhancements", "增強功能"), diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 854514cfc..42884fd52 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -39,6 +39,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Змінити ID"), ("Website", "Веб-сайт"), ("About", "Про RustDesk"), + ("About RustDesk", ""), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", "Вимкнути звук"), ("Audio Input", "Аудіовхід"), ("Enhancements", "Покращення"), diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 0667e2629..412f04999 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -39,6 +39,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Thay đổi ID"), ("Website", "Trang web"), ("About", "About"), + ("About RustDesk", ""), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", "Tắt tiếng"), ("Audio Input", "Đầu vào âm thanh"), ("Enhancements", "Các tiện itchs"), From 4b571aaa337c05f81ae29974de5a1dcd514215bd Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 15 Dec 2022 17:16:05 +0800 Subject: [PATCH 1209/2015] refact keyboard, mid commit Signed-off-by: fufesou --- Cargo.lock | 3 ++- src/keyboard.rs | 19 +++++++++++++++++-- src/server/input_service.rs | 19 ++++++++++++++++--- 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f40582eb7..2b220c15d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4305,7 +4305,7 @@ dependencies = [ [[package]] name = "rdev" version = "0.5.0-2" -source = "git+https://github.com/asur4s/rdev#3d6d413a9b2ab703edc22071acea31826b0efce3" +source = "git+https://github.com/asur4s/rdev#18bb9dd64563fc9761005bb39ff830e6402e326e" dependencies = [ "cocoa", "core-foundation 0.9.3", @@ -4316,6 +4316,7 @@ dependencies = [ "inotify", "lazy_static", "libc", + "log", "mio 0.8.5", "strum 0.24.1", "strum_macros 0.24.3", diff --git a/src/keyboard.rs b/src/keyboard.rs index 99e1c1455..eb7e91a37 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -610,11 +610,26 @@ pub fn map_keyboard_mode(event: &Event, key_event: &mut KeyEvent) { } _ => return, }; - let keycode: u32 = match peer.as_str() { - "windows" => rdev::win_keycode_from_key(key).unwrap_or_default().into(), + + #[cfg(target_os = "windows")] + let keycode = match peer.as_str() { + "windows" => event.scan_code, "macos" => rdev::macos_keycode_from_key(key).unwrap_or_default().into(), _ => rdev::linux_keycode_from_key(key).unwrap_or_default().into(), }; + #[cfg(target_os = "macos")] + let keycode = match peer.as_str() { + "windows" => rdev::win_scancode_from_key(key).unwrap_or_default().into(), + "macos" => rdev::macos_keycode_from_key(key).unwrap_or_default().into(), + _ => event.code, + }; + #[cfg(target_os = "linux")] + let keycode = match peer.as_str() { + "windows" => rdev::win_scancode_from_key(key).unwrap_or_default().into(), + "macos" => event.code, + _ => rdev::linux_keycode_from_key(key).unwrap_or_default().into(), + }; + key_event.set_chr(keycode); } diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 66d2b0414..d47cacf4a 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -5,7 +5,7 @@ use crate::common::IS_X11; use dispatch::Queue; use enigo::{Enigo, Key, KeyboardControllable, MouseButton, MouseControllable}; use hbb_common::{config::COMPRESS_LEVEL, get_time, protobuf::EnumOrUnknown}; -use rdev::{simulate, EventType, Key as RdevKey}; +use rdev::{self, simulate, EventType, Key as RdevKey, RawKey}; use std::time::Duration; use std::{ convert::TryFrom, @@ -686,6 +686,20 @@ pub fn handle_key(evt: &KeyEvent) { handle_key_(evt); } +fn sim_rdev_rawkey(code: u32, down_or_up: bool) { + #[cfg(target_os = "windows")] + let rawkey = RawKey::ScanCode(code); + #[cfg(target_os = "linux")] + let rawkey = RawKey::LinuxXorgKeycode(code); + // to-do: test android + #[cfg(target_os = "android")] + let rawkey = RawKey::LinuxConsoleKeycode(code); + #[cfg(target_os = "macos")] + let rawkey = RawKey::MacVirtualKeycode(code); + + rdev_key_down_or_up(RdevKey::RawKey(rawkey), down_or_up); +} + fn rdev_key_down_or_up(key: RdevKey, down_or_up: bool) { let event_type = match down_or_up { true => EventType::KeyPress(key), @@ -821,8 +835,7 @@ fn map_keyboard_mode(evt: &KeyEvent) { return; } - rdev_key_down_or_up(RdevKey::Unknown(evt.chr()), evt.down); - return; + sim_rdev_rawkey(evt.chr(), evt.down); } #[cfg(target_os = "macos")] From 0fb1d4049f15f9a62d9eeb3d2159a937e8629af0 Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 15 Dec 2022 21:30:49 +0800 Subject: [PATCH 1210/2015] keyboard win, mid commit Signed-off-by: fufesou --- src/keyboard.rs | 61 ++++++++++++++++++++++++++----------------------- 1 file changed, 33 insertions(+), 28 deletions(-) diff --git a/src/keyboard.rs b/src/keyboard.rs index eb7e91a37..6b4b6fea5 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -103,8 +103,9 @@ pub mod client { if is_long_press(&event) { return; } - let key_event = event_to_key_event(&event); - send_key_event(&key_event); + if let Some(key_event) = event_to_key_event(&event) { + send_key_event(&key_event); + } } pub fn get_modifiers_state( @@ -314,7 +315,7 @@ fn update_modifiers_state(event: &Event) { }; } -pub fn event_to_key_event(event: &Event) -> KeyEvent { +pub fn event_to_key_event(event: &Event) -> Option { let mut key_event = KeyEvent::new(); update_modifiers_state(event); @@ -330,22 +331,22 @@ pub fn event_to_key_event(event: &Event) -> KeyEvent { let keyboard_mode = get_keyboard_mode_enum(); key_event.mode = keyboard_mode.into(); - match keyboard_mode { + let mut key_event = match keyboard_mode { KeyboardMode::Map => { - map_keyboard_mode(event, &mut key_event); + map_keyboard_mode(event, key_event)? } KeyboardMode::Translate => { - translate_keyboard_mode(event, &mut key_event); + translate_keyboard_mode(event, key_event)? } _ => { #[cfg(not(any(target_os = "android", target_os = "ios")))] - legacy_keyboard_mode(event, &mut key_event); + legacy_keyboard_mode(event, key_event)? } }; #[cfg(not(any(target_os = "android", target_os = "ios")))] add_numlock_capslock_status(&mut key_event); - return key_event; + return Some(key_event); } pub fn event_type_to_event(event_type: EventType) -> Event { @@ -374,15 +375,15 @@ pub fn get_peer_platform() -> String { } #[cfg(not(any(target_os = "android", target_os = "ios")))] -pub fn legacy_keyboard_mode(event: &Event, key_event: &mut KeyEvent) { +pub fn legacy_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option { // 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; + return None; } - }; + }; let peer = get_peer_platform(); let is_win = peer == "Windows"; @@ -422,11 +423,11 @@ pub fn legacy_keyboard_mode(event: &Event, key_event: &mut KeyEvent) { // when pressing AltGr, an extra VK_LCONTROL with a special // scancode with bit 9 set is sent, let's ignore this. #[cfg(windows)] - if event.scan_code & 0x200 != 0 { + if (event.scan_code >> 8) == 0xE0 { unsafe { IS_ALT_GR = true; } - return; + return None; } Some(ControlKey::Control) } @@ -458,7 +459,7 @@ pub fn legacy_keyboard_mode(event: &Event, key_event: &mut KeyEvent) { Key::Delete => { if is_win && ctrl && alt { client::ctrl_alt_del(); - return; + return None; } Some(ControlKey::Delete) } @@ -496,7 +497,7 @@ pub fn legacy_keyboard_mode(event: &Event, key_event: &mut KeyEvent) { Key::KpMinus => Some(ControlKey::Subtract), Key::KpPlus => Some(ControlKey::Add), Key::CapsLock | Key::NumLock | Key::ScrollLock => { - return; + return None; } Key::Home => Some(ControlKey::Home), Key::End => Some(ControlKey::End), @@ -579,27 +580,28 @@ pub fn legacy_keyboard_mode(event: &Event, key_event: &mut KeyEvent) { if chr != '\0' { if chr == 'l' && is_win && command { client::lock_screen(); - return; + return None; } key_event.set_chr(chr as _); } else { log::error!("Unknown key {:?}", &event); - return; + return None; } } let (alt, ctrl, shift, command) = client::get_modifiers_state(alt, ctrl, shift, command); - client::legacy_modifiers(key_event, alt, ctrl, shift, command); + client::legacy_modifiers(&mut key_event, alt, ctrl, shift, command); if down_or_up == true { key_event.down = true; } + Some(key_event) } -pub fn map_keyboard_mode(event: &Event, key_event: &mut KeyEvent) { +pub fn map_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option { let mut peer = get_peer_platform().to_lowercase(); peer.retain(|c| !c.is_whitespace()); - let key = match event.event_type { + let mut key = match event.event_type { EventType::KeyPress(key) => { key_event.down = true; key @@ -608,29 +610,32 @@ pub fn map_keyboard_mode(event: &Event, key_event: &mut KeyEvent) { key_event.down = false; key } - _ => return, + _ => return None, }; #[cfg(target_os = "windows")] let keycode = match peer.as_str() { "windows" => event.scan_code, - "macos" => rdev::macos_keycode_from_key(key).unwrap_or_default().into(), - _ => rdev::linux_keycode_from_key(key).unwrap_or_default().into(), + "macos" => rdev::win_scancode_to_macos_code(event.scan_code)?, + _ => rdev::win_scancode_to_linux_code(event.scan_code)?, }; #[cfg(target_os = "macos")] let keycode = match peer.as_str() { - "windows" => rdev::win_scancode_from_key(key).unwrap_or_default().into(), - "macos" => rdev::macos_keycode_from_key(key).unwrap_or_default().into(), + "windows" => rdev::macos_code_to_win_scancode(event.code as _)?, + "macos" => rdev::macos_code_to_linux_code(event.code as _)?, _ => event.code, }; #[cfg(target_os = "linux")] let keycode = match peer.as_str() { - "windows" => rdev::win_scancode_from_key(key).unwrap_or_default().into(), + "windows" => rdev::linux_code_to_win_scancode(event.code as _)?, "macos" => event.code, - _ => rdev::linux_keycode_from_key(key).unwrap_or_default().into(), + _ => rdev::linux_code_to_macos_code(event.code as _)?, }; key_event.set_chr(keycode); + Some(key_event) } -pub fn translate_keyboard_mode(_event: &Event, _key_event: &mut KeyEvent) {} +pub fn translate_keyboard_mode(_event: &Event, mut _key_event: KeyEvent) -> Option { + None +} From 7c8d40dc72b70a996011a6b1668a6e4af9178bf8 Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 15 Dec 2022 22:16:39 +0800 Subject: [PATCH 1211/2015] fix keycodes Signed-off-by: fufesou --- src/keyboard.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/keyboard.rs b/src/keyboard.rs index 6b4b6fea5..edd03d6e6 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -622,14 +622,14 @@ pub fn map_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option rdev::macos_code_to_win_scancode(event.code as _)?, - "macos" => rdev::macos_code_to_linux_code(event.code as _)?, - _ => event.code, + "macos" => event.code, + _ => 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" => event.code, - _ => rdev::linux_code_to_macos_code(event.code as _)?, + "macos" => rdev::linux_code_to_macos_code(event.code as _)?, + _ => event.code, }; key_event.set_chr(keycode); From 38efbd5a17dd4e19df98d20072ccd3622f0e8a7d Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 15 Dec 2022 23:10:57 +0800 Subject: [PATCH 1212/2015] linux remove some warns Signed-off-by: fufesou --- src/keyboard.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/keyboard.rs b/src/keyboard.rs index edd03d6e6..cfd1453f6 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -7,19 +7,19 @@ use crate::flutter::FlutterHandler; use crate::ui::remote::SciterHandler; use crate::ui_session_interface::Session; use hbb_common::{log, message_proto::*}; -#[cfg(target_os = "linux")] -use rdev::GrabError; use rdev::{Event, EventType, Key}; +#[cfg(any(target_os = "windows", target_os = "macos"))] +use std::sync::atomic::{AtomicBool, Ordering}; use std::{ collections::{HashMap, HashSet}, - sync::{ - atomic::{AtomicBool, Ordering}, - Arc, Mutex, - }, + sync::{Arc, Mutex}, time::SystemTime, }; +#[cfg(windows)] static mut IS_ALT_GR: bool = false; + +#[cfg(any(target_os = "windows", target_os = "macos"))] static KEYBOARD_HOOKED: AtomicBool = AtomicBool::new(false); #[cfg(feature = "flutter")] From cfca4047c51279112526ad6056e3ebd373168a19 Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 15 Dec 2022 23:14:33 +0800 Subject: [PATCH 1213/2015] fix build Signed-off-by: fufesou --- src/keyboard.rs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/keyboard.rs b/src/keyboard.rs index cfd1453f6..df052ebbc 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -598,21 +598,19 @@ pub fn legacy_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option Option { - let mut peer = get_peer_platform().to_lowercase(); - peer.retain(|c| !c.is_whitespace()); - - let mut key = match event.event_type { - EventType::KeyPress(key) => { + match event.event_type { + EventType::KeyPress(..) => { key_event.down = true; - key } - EventType::KeyRelease(key) => { + EventType::KeyRelease(..) => { key_event.down = false; - key } _ => 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.scan_code, @@ -622,14 +620,14 @@ pub fn map_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option rdev::macos_code_to_win_scancode(event.code as _)?, - "macos" => event.code, + "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" => rdev::linux_code_to_macos_code(event.code as _)?, - _ => event.code, + _ => event.code as _, }; key_event.set_chr(keycode); From 0c057139e43fa6e4d4d6a62969d31da3431c4f9b Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 16 Dec 2022 23:45:31 +0800 Subject: [PATCH 1214/2015] macos mid commit Signed-off-by: fufesou --- src/ui/macos.rs | 1 - src/ui_session_interface.rs | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/ui/macos.rs b/src/ui/macos.rs index 488d1afc8..ab3fb9079 100644 --- a/src/ui/macos.rs +++ b/src/ui/macos.rs @@ -13,7 +13,6 @@ use objc::{ }; use sciter::{make_args, Host}; use std::{ffi::c_void, rc::Rc}; -use dark_light; static APP_HANDLER_IVAR: &str = "GoDeskAppHandler"; diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index f38cb6ad3..f6d5a0e94 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -376,7 +376,7 @@ impl Session { let scancode: u32 = scancode as u32; #[cfg(not(target_os = "windows"))] - let key = rdev::key_from_scancode(scancode) as rdev::Key; + let key = rdev::key_from_code(keycode) as rdev::Key; // Windows requires special handling #[cfg(target_os = "windows")] let key = rdev::get_win_key(keycode, scancode); From 98d033560705e8e8edb94960f70b545122ce9bef Mon Sep 17 00:00:00 2001 From: fufesou Date: Sun, 18 Dec 2022 15:07:53 +0800 Subject: [PATCH 1215/2015] remove warn Signed-off-by: fufesou --- src/server/input_service.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/server/input_service.rs b/src/server/input_service.rs index d47cacf4a..7e56da064 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -863,6 +863,7 @@ fn release_unpressed_modifiers(en: &mut Enigo, key_event: &KeyEvent) { fix_modifiers(&key_event.modifiers[..], en, ck_value); } +#[cfg(target_os = "linux")] fn is_altgr_pressed() -> bool { KEYS_DOWN .lock() From f3d71024edd71d963201f560158107d65644d7cc Mon Sep 17 00:00:00 2001 From: fufesou Date: Sun, 18 Dec 2022 15:12:34 +0800 Subject: [PATCH 1216/2015] comment untested android key code Signed-off-by: fufesou --- src/server/input_service.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 7e56da064..6b678dbdc 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -691,9 +691,9 @@ fn sim_rdev_rawkey(code: u32, down_or_up: bool) { let rawkey = RawKey::ScanCode(code); #[cfg(target_os = "linux")] let rawkey = RawKey::LinuxXorgKeycode(code); - // to-do: test android - #[cfg(target_os = "android")] - let rawkey = RawKey::LinuxConsoleKeycode(code); + // // to-do: test android + // #[cfg(target_os = "android")] + // let rawkey = RawKey::LinuxConsoleKeycode(code); #[cfg(target_os = "macos")] let rawkey = RawKey::MacVirtualKeycode(code); From 2490d027a5e527aa5a283ea586d71c4454d33193 Mon Sep 17 00:00:00 2001 From: Alt37 Date: Sun, 18 Dec 2022 14:14:25 +0200 Subject: [PATCH 1217/2015] Update Ukrainian UI translation (ua.rs) --- src/lang/ua.rs | 116 ++++++++++++++++++++++++------------------------- 1 file changed, 58 insertions(+), 58 deletions(-) diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 42884fd52..e6bf21930 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -2,8 +2,8 @@ lazy_static::lazy_static! { pub static ref T: std::collections::HashMap<&'static str, &'static str> = [ ("Status", "Статус"), - ("Your Desktop", "Ваш робочий стіл"), - ("desk_tip", "Ваш робочий стіл доступний з цим ідентифікатором і паролем"), + ("Your Desktop", "Ваша стільниця"), + ("desk_tip", "Ваша стільниця доступна з цим ідентифікатором і паролем"), ("Password", "Пароль"), ("Ready", "Готово"), ("Established", "Встановлено"), @@ -13,7 +13,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Service is running", "Служба працює"), ("Service is not running", "Служба не запущена"), ("not_ready_status", "Не готово. Будь ласка, перевірте підключення"), - ("Control Remote Desktop", "Управління віддаленим робочим столом"), + ("Control Remote Desktop", "Управління віддаленою стільницею"), ("Transfer File", "Передати файл"), ("Connect", "Підключитися"), ("Recent Sessions", "Останні сеанси"), @@ -30,7 +30,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("IP Whitelisting", "Список дозволених IP-адрес"), ("ID/Relay Server", "ID/Сервер ретрансляції"), ("Import Server Config", "Імпортувати конфігурацію сервера"), - ("Export Server Config", ""), + ("Export Server Config", "Експортувати конфігурацію сервера"), ("Import server configuration successfully", "Конфігурацію сервера успішно імпортовано"), ("Export server configuration successfully", ""), ("Invalid server configuration", "Недійсна конфігурація сервера"), @@ -39,9 +39,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Змінити ID"), ("Website", "Веб-сайт"), ("About", "Про RustDesk"), - ("About RustDesk", ""), - ("Slogan_tip", ""), - ("Privacy Statement", ""), + ("About RustDesk", "Про RustDesk"), + ("Slogan_tip", "Створено з душею в цьому хаотичному світі!"), + ("Privacy Statement", "Декларація про конфіденційність"), ("Mute", "Вимкнути звук"), ("Audio Input", "Аудіовхід"), ("Enhancements", "Покращення"), @@ -92,8 +92,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Delete", "Видалити"), ("Properties", "Властивості"), ("Multi Select", "Багатоелементний вибір"), - ("Select All", ""), - ("Unselect All", ""), + ("Select All", "Вибрати все"), + ("Unselect All", "Скасувати вибір"), ("Empty Directory", "Порожня папка"), ("Not an empty directory", "Папка не порожня"), ("Are you sure you want to delete this file?", "Ви впевнені, що хочете видалити цей файл?"), @@ -119,8 +119,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Good image quality", "Хороша якість зображення"), ("Balanced", "Збалансований"), ("Optimize reaction time", "Оптимізувати час реакції"), - ("Custom", ""), - ("Show remote cursor", "Показати віддалений курсор"), + ("Custom", "Користувацькі"), + ("Show remote cursor", "Показати віддалений вказівник"), ("Show quality monitor", "Показати якість"), ("Disable clipboard", "Відключити буфер обміну"), ("Lock after session end", "Вихід з облікового запису після завершення сеансу"), @@ -130,13 +130,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ID does not exist", "ID не існує"), ("Failed to connect to rendezvous server", "Не вдалося підключитися до проміжного сервера"), ("Please try later", "Будь ласка, спробуйте пізніше"), - ("Remote desktop is offline", "Віддалений робочий стіл не в мережі"), + ("Remote desktop is offline", "Віддалена стільниця не в мережі"), ("Key mismatch", "Невідповідність ключів"), ("Timeout", "Тайм-аут"), ("Failed to connect to relay server", "Не вдалося підключитися до сервера ретрансляції"), ("Failed to connect via rendezvous server", "Не вдалося підключитися через проміжний сервер"), ("Failed to connect via relay server", "Не вдалося підключитися через сервер ретрансляції"), - ("Failed to make direct connection to remote desktop", "Не вдалося встановити пряме підключення до віддаленого робочого столу"), + ("Failed to make direct connection to remote desktop", "Не вдалося встановити пряме підключення до віддаленої стільниці"), ("Set Password", "Встановити пароль"), ("OS Password", "Пароль ОС"), ("install_tip", "У деяких випадках через UAC RustDesk може працювати некоректно на віддаленому вузлі. Щоб уникнути UAC, натисніть кнопку нижче, щоб встановити RustDesk у системі"), @@ -144,14 +144,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Click to download", "Натисніть, щоб завантажити"), ("Click to update", "Натисніть, щоб оновити"), ("Configure", "Налаштувати"), - ("config_acc", "Щоб віддалено керувати своїм робочим столом, ви повинні надати RustDesk права \"доступу\""), - ("config_screen", "Для віддаленого доступу до робочого столу ви повинні надати RustDesk права \"знімок екрану\""), + ("config_acc", "Щоб віддалено керувати своєю стільницею, ви повинні надати RustDesk права \"доступності\""), + ("config_screen", "Для віддаленого доступу до стільниці ви повинні надати RustDesk права для \"запису екрану\""), ("Installing ...", "Встановлюється..."), ("Install", "Встановити"), ("Installation", "Установка"), ("Installation Path", "Шлях встановлення"), ("Create start menu shortcuts", "Створити ярлики меню \"Пуск\""), - ("Create desktop icon", "Створити значок на робочому столі"), + ("Create desktop icon", "Створити значок на стільниці"), ("agreement_tip", "Починаючи установку, ви приймаєте умови ліцензійної угоди"), ("Accept and Install", "Прийняти і встановити"), ("End-user license agreement", "Ліцензійна угода з кінцевим користувачем"), @@ -164,8 +164,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Action", "Дія"), ("Add", "Додати"), ("Local Port", "Локальний порт"), - ("Local Address", ""), - ("Change Local Port", ""), + ("Local Address", "Локальна адреса"), + ("Change Local Port", "Змінити локальний порт"), ("setup_server_tip", "Для більш швидкого підключення налаштуйте свій власний сервер підключення"), ("Too short, at least 6 characters.", "Занадто коротко, мінімум 6 символів"), ("The confirmation is not identical.", "Підтвердження не збігається"), @@ -190,7 +190,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable Direct IP Access", "Увімкнути прямий IP-доступ"), ("Rename", "Перейменувати"), ("Space", "Місце"), - ("Create Desktop Shortcut", "Створити ярлик на робочому столі"), + ("Create Desktop Shortcut", "Створити ярлик на стільниці"), ("Change Path", "Змінити шлях"), ("Create Folder", "Створити папку"), ("Please enter the folder name", "Будь ласка, введіть ім'я папки"), @@ -198,7 +198,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Warning", "Попередження"), ("Login screen using Wayland is not supported", "Вхід у систему з використанням Wayland не підтримується"), ("Reboot required", "Потрібне перезавантаження"), - ("Unsupported display server ", ""), + ("Unsupported display server ", "Графічний сервер не підтримується"), ("x11 expected", "Очікується X11"), ("Port", "Порт"), ("Settings", "Налаштування"), @@ -214,8 +214,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logout", "Вийти"), ("Tags", "Ключові слова"), ("Search ID", "Пошук за ID"), - ("Current Wayland display server is not supported", "Поточний сервер відображення Wayland не підтримується"), - ("whitelist_sep", "Окремо комою, крапкою з комою, пропуском або новим рядком"), + ("Current Wayland display server is not supported", "Поточний графічний сервер Wayland не підтримується"), + ("whitelist_sep", "Розділені комою, крапкою з комою, пробілом або новим рядком"), ("Add ID", "Додати ID"), ("Add Tag", "Додати ключове слово"), ("Unselect all tags", "Скасувати вибір усіх тегів"), @@ -303,9 +303,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Ignore Battery Optimizations", "Ігнорувати оптимізацію батареї"), ("android_open_battery_optimizations_tip", "Перейдіть на наступну сторінку налаштувань"), ("Connection not allowed", "Підключення не дозволено"), - ("Legacy mode", ""), - ("Map mode", ""), - ("Translate mode", ""), + ("Legacy mode", "Застарілий режим"), + ("Map mode", "Режим карти"), + ("Translate mode", "Режим перекладу"), ("Use permanent password", "Використовувати постійний пароль"), ("Use both passwords", "Використовувати обидва паролі"), ("Set permanent password", "Встановити постійний пароль"), @@ -343,7 +343,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Follow System", "Використовувати системну"), ("Enable hardware codec", "Увімкнути апаратний кодек"), ("Unlock Security Settings", "Розблокувати налаштування безпеки"), - ("Enable Audio", "Вімкнути аудіо"), + ("Enable Audio", "Увімкнути аудіо"), ("Unlock Network Settings", "Розблокувати мережеві налаштування"), ("Server", "Сервер"), ("Direct IP Access", "Прямий IP доступ"), @@ -370,40 +370,40 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable LAN Discovery", "Увімкнути пошук локальної мережі"), ("Deny LAN Discovery", "Заборонити виявлення локальної мережі"), ("Write a message", "Написати повідомлення"), - ("Prompt", ""), - ("Please wait for confirmation of UAC...", ""), - ("elevated_foreground_window_tip", ""), - ("Disconnected", ""), - ("Other", ""), + ("Prompt", "Підказка"), + ("Please wait for confirmation of UAC...", "Будь ласка, зачекай підтвердження UAC..."), + ("elevated_foreground_window_tip", "Поточне вікно віддаленої стільниці потребує розширених прав для роботи, тому наразі неможливо використати мишку та клавіатуру. Ви можете запропонувати віддаленому користувачу згорнути поточне вікно чи натиснути кнопку розширення прав у вікні керування з'єднаннями. Для уникнення цієї проблеми, рекомендується встановити програму на віддаленому пристрої"), + ("Disconnected", "Віключено"), + ("Other", "Інше"), ("Confirm before closing multiple tabs", ""), - ("Keyboard Settings", ""), - ("Custom", ""), - ("Full Access", ""), - ("Screen Share", ""), + ("Keyboard Settings", "Налаштування клавіатури"), + ("Custom", "Користувацькі"), + ("Full Access", "Повний доступ"), + ("Screen Share", "Демонстрація екрану"), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland потребує Ubuntu 21.04 або новішої версії."), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Для Wayland потрібна новіша версія дистрибутива Linux. Будь ласка, спробуйте робочий стіл X11 або змініть свою ОС."), - ("JumpLink", "View"), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Для Wayland потрібна новіша версія дистрибутива Linux. Будь ласка, спробуйте стільницю на X11 або змініть свою ОС."), + ("JumpLink", "Перегляд"), ("Please Select the screen to be shared(Operate on the peer side).", "Будь ласка, виберіть екран, до якого потрібно надати доступ (працюйте на стороні однорангового пристрою)."), - ("Show RustDesk", ""), - ("This PC", ""), - ("or", ""), - ("Continue with", ""), - ("Elevate", ""), - ("Zoom cursor", ""), - ("Accept sessions via password", ""), - ("Accept sessions via click", ""), - ("Accept sessions via both", ""), - ("Please wait for the remote side to accept your session request...", ""), - ("One-time Password", ""), - ("Use one-time password", ""), - ("One-time password length", ""), - ("Request access to your device", ""), - ("Hide connection management window", ""), - ("hide_cm_tip", ""), - ("wayland_experiment_tip", ""), - ("Right click to select tabs", ""), - ("Add to Address Book", ""), - ("Group", ""), - ("Search", ""), + ("Show RustDesk", "Показати RustDesk"), + ("This PC", "Цей ПК"), + ("or", "чи"), + ("Continue with", "Продовжити з"), + ("Elevate", "Розширення прав"), + ("Zoom cursor", "Збільшити вказівник"), + ("Accept sessions via password", "Підтверджувати сеанси паролем"), + ("Accept sessions via click", "Підтверджувати сеанси натисканням"), + ("Accept sessions via both", "Підтверджувати сеанси обома способами"), + ("Please wait for the remote side to accept your session request...", "Буль ласка, зачекайте, поки віддалена сторона підтвердить запит на сеанс..."), + ("One-time Password", "Одноразовий пароль"), + ("Use one-time password", "Використати одноразовий пароль"), + ("One-time password length", "Довжина одноразового пароля"), + ("Request access to your device", "Дати запит щодо доступ до свого пристрою"), + ("Hide connection management window", "Приховати вікно керування з'єднаннями"), + ("hide_cm_tip", "Дозволено приховати лише якщо сеанс підтверджується постійним паролем"), + ("wayland_experiment_tip", "Підтримка Wayland на експериментальній стадії, будь ласка, використовуйте X11, якщо необхідний автоматичний доступ."), + ("Right click to select tabs", "Правий клік для вибору вкладки"), + ("Add to Address Book", "Додати IP до Адресної книги"), + ("Group", "Група"), + ("Search", "Пошук"), ].iter().cloned().collect(); } From 70bdfad34583f8ff17892cdab7cf43627d490b7b Mon Sep 17 00:00:00 2001 From: User Date: Sun, 18 Dec 2022 15:26:32 +0200 Subject: [PATCH 1218/2015] Update Ukrainian UI translation (ua.rs) --- src/lang/ua.rs | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/lang/ua.rs b/src/lang/ua.rs index e6bf21930..83b5e6984 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -13,7 +13,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Service is running", "Служба працює"), ("Service is not running", "Служба не запущена"), ("not_ready_status", "Не готово. Будь ласка, перевірте підключення"), - ("Control Remote Desktop", "Управління віддаленою стільницею"), + ("Control Remote Desktop", "Керування віддаленою стільницею"), ("Transfer File", "Передати файл"), ("Connect", "Підключитися"), ("Recent Sessions", "Останні сеанси"), @@ -153,7 +153,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Create start menu shortcuts", "Створити ярлики меню \"Пуск\""), ("Create desktop icon", "Створити значок на стільниці"), ("agreement_tip", "Починаючи установку, ви приймаєте умови ліцензійної угоди"), - ("Accept and Install", "Прийняти і встановити"), + ("Accept and Install", "Прийняти та встановити"), ("End-user license agreement", "Ліцензійна угода з кінцевим користувачем"), ("Generating ...", "Генерація..."), ("Your installation is lower version.", "Ваша установка більш ранньої версії"), @@ -173,15 +173,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Accept", "Прийняти"), ("Dismiss", "Відхилити"), ("Disconnect", "Відключити"), - ("Allow using keyboard and mouse", "Дозволити використання клавіатури і миші"), + ("Allow using keyboard and mouse", "Дозволити використання клавіатури та миші"), ("Allow using clipboard", "Дозволити використання буфера обміну"), ("Allow hearing sound", "Дозволити передачу звуку"), - ("Allow file copy and paste", "Дозволити копіювання і вставку файлів"), + ("Allow file copy and paste", "Дозволити копіювання та вставку файлів"), ("Connected", "Підключено"), - ("Direct and encrypted connection", "Пряме і зашифроване з'єднання"), - ("Relayed and encrypted connection", "Ретрансльоване і зашифроване з'єднання"), - ("Direct and unencrypted connection", "Пряме і незашифроване з'єднання"), - ("Relayed and unencrypted connection", "Ретрансльоване і незашифроване з'єднання"), + ("Direct and encrypted connection", "Пряме та зашифроване з'єднання"), + ("Relayed and encrypted connection", "Ретрансльоване та зашифроване з'єднання"), + ("Direct and unencrypted connection", "Пряме та незашифроване з'єднання"), + ("Relayed and unencrypted connection", "Ретрансльоване та незашифроване з'єднання"), ("Enter Remote ID", "Введіть віддалений ID"), ("Enter your password", "Введіть пароль"), ("Logging in...", "Вхід..."), @@ -247,7 +247,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Two-Finger Tap", "Дотик двома пальцями"), ("Right Mouse", "Права миша"), ("One-Finger Move", "Рух одним пальцем"), - ("Double Tap & Move", "Подвійне натискання і переміщення"), + ("Double Tap & Move", "Подвійне натискання та переміщення"), ("Mouse Drag", "Перетягування мишею"), ("Three-Finger vertically", "Трьома пальцями по вертикалі"), ("Mouse Wheel", "Коліщатко миші"), @@ -275,8 +275,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Open System Setting", "Відкрити налаштування системи"), ("How to get Android input permission?", "Як отримати дозвіл на введення Android?"), ("android_input_permission_tip1", "Щоб віддалений пристрій міг керувати вашим Android-пристроєм за допомогою миші або торкання, вам необхідно дозволити RustDesk використовувати службу \"Спеціальні можливості\"."), - ("android_input_permission_tip2", "Перейдіть на наступну сторінку системних налаштувань, знайдіть і увійдіть у [Встановлені служби], увімкніть службу [RustDesk Input]."), - ("android_new_connection_tip", "Отримано новий запит на управління вашим поточним пристроєм."), + ("android_input_permission_tip2", "Перейдіть на наступну сторінку системних налаштувань, знайдіть та увійдіть у [Встановлені служби], увімкніть службу [RustDesk Input]."), + ("android_new_connection_tip", "Отримано новий запит на керування вашим поточним пристроєм."), ("android_service_will_start_tip", "Увімкнення захоплення екрана автоматично запускає службу, дозволяючи іншим пристроям запитувати з'єднання з цього пристрою."), ("android_stop_service_tip", "Закриття служби автоматично закриє всі встановлені з'єднання."), ("android_version_audio_tip", "Поточна версія Android не підтримує захоплення звуку, оновіть її до Android 10 або вище."), @@ -314,13 +314,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Restart Remote Device", "Перезапустити віддалений пристрій"), ("Are you sure you want to restart", "Ви впевнені, що хочете виконати перезапуск?"), ("Restarting Remote Device", "Перезавантаження віддаленого пристрою"), - ("remote_restarting_tip", "Віддалений пристрій перезапускається. Будь ласка, закрийте це повідомлення і через деякий час перепідключіться, використовуючи постійний пароль."), + ("remote_restarting_tip", "Віддалений пристрій перезапускається. Будь ласка, закрийте це повідомлення та через деякий час перепідключіться, використовуючи постійний пароль."), ("Copied", ""), ("Exit Fullscreen", "Вийти з повноекранного режиму"), ("Fullscreen", "Повноекранний"), ("Mobile Actions", "Мобільні дії"), ("Select Monitor", "Виберіть монітор"), - ("Control Actions", "Дії з управління"), + ("Control Actions", "Дії для керування"), ("Display Settings", "Налаштування відображення"), ("Ratio", "Співвідношення"), ("Image Quality", "Якість зображення"), @@ -340,7 +340,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Dark Theme", "Темна тема"), ("Dark", "Темна"), ("Light", "Світла"), - ("Follow System", "Використовувати системну"), + ("Follow System", "Як у системі"), ("Enable hardware codec", "Увімкнути апаратний кодек"), ("Unlock Security Settings", "Розблокувати налаштування безпеки"), ("Enable Audio", "Увімкнути аудіо"), @@ -371,9 +371,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Deny LAN Discovery", "Заборонити виявлення локальної мережі"), ("Write a message", "Написати повідомлення"), ("Prompt", "Підказка"), - ("Please wait for confirmation of UAC...", "Будь ласка, зачекай підтвердження UAC..."), - ("elevated_foreground_window_tip", "Поточне вікно віддаленої стільниці потребує розширених прав для роботи, тому наразі неможливо використати мишку та клавіатуру. Ви можете запропонувати віддаленому користувачу згорнути поточне вікно чи натиснути кнопку розширення прав у вікні керування з'єднаннями. Для уникнення цієї проблеми, рекомендується встановити програму на віддаленому пристрої"), - ("Disconnected", "Віключено"), + ("Please wait for confirmation of UAC...", "Будь ласка, зачекайте підтвердження UAC..."), + ("elevated_foreground_window_tip", "Поточне вікно віддаленої стільниці потребує розширених прав для роботи, тому наразі неможливо використати мишу та клавіатуру. Ви можете запропонувати віддаленому користувачу згорнути поточне вікно чи натиснути кнопку розширення прав у вікні керування з'єднаннями. Для уникнення цієї проблеми, рекомендується встановити програму на віддаленому пристрої"), + ("Disconnected", "Відключено"), ("Other", "Інше"), ("Confirm before closing multiple tabs", ""), ("Keyboard Settings", "Налаштування клавіатури"), From 277a4b1b2975f98ce5cdbf222e0410b85540c660 Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 19 Dec 2022 15:09:08 +0800 Subject: [PATCH 1219/2015] fix reconnect when reset by the peer && improper 10054 reconnect Signed-off-by: 21pages --- src/client.rs | 13 +++++++++---- src/client/io_loop.rs | 6 +++++- src/ui_session_interface.rs | 11 ++++++++++- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/client.rs b/src/client.rs index 03bbf5918..2922a488b 100644 --- a/src/client.rs +++ b/src/client.rs @@ -892,6 +892,8 @@ pub struct LoginConfigHandler { pub supported_encoding: Option<(bool, bool)>, pub restarting_remote_device: bool, pub force_relay: bool, + pub direct: Option, + pub received: bool, } impl Deref for LoginConfigHandler { @@ -929,6 +931,8 @@ impl LoginConfigHandler { self.supported_encoding = None; self.restarting_remote_device = false; self.force_relay = !self.get_option("force-always-relay").is_empty(); + self.direct = None; + self.received = false; } /// Check if the client should auto login. @@ -1815,6 +1819,7 @@ pub trait Interface: Send + Clone + 'static + Sized { fn handle_login_error(&mut self, err: &str) -> bool; fn handle_peer_info(&mut self, pi: PeerInfo); fn set_force_relay(&mut self, direct: bool, received: bool); + fn set_connection_info(&mut self, direct: bool, received: bool); fn is_file_transfer(&self) -> bool; fn is_port_forward(&self) -> bool; fn is_rdp(&self) -> bool; @@ -1990,11 +1995,10 @@ lazy_static::lazy_static! { /// * `title` - The title of the message. /// * `text` - The text of the message. #[inline] -pub fn check_if_retry(msgtype: &str, title: &str, text: &str) -> bool { +pub fn check_if_retry(msgtype: &str, title: &str, text: &str, retry_for_relay: bool) -> bool { msgtype == "error" && title == "Connection Error" - && (text.contains("10054") - || text.contains("104") + && ((text.contains("10054") || text.contains("104")) && retry_for_relay || (!text.to_lowercase().contains("offline") && !text.to_lowercase().contains("exist") && !text.to_lowercase().contains("handshake") @@ -2002,7 +2006,8 @@ pub fn check_if_retry(msgtype: &str, title: &str, text: &str) -> bool { && !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("not allowed") + && !text.to_lowercase().contains("reset by the peer"))) } #[inline] diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 326857d3f..ceddbc004 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -107,6 +107,7 @@ impl Remote { 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); // just build for now #[cfg(not(windows))] @@ -144,7 +145,10 @@ impl Remote { } Ok(ref bytes) => { last_recv_time = Instant::now(); - received = true; + if !received { + received = true; + self.handler.set_connection_info(direct, true); + } self.data_count.fetch_add(bytes.len(), Ordering::Relaxed); if !self.handle_msg_from_peer(bytes, &mut peer).await { break diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index f6d5a0e94..9868b5bb1 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -657,7 +657,10 @@ impl Interface for Session { } fn msgbox(&self, msgtype: &str, title: &str, text: &str, link: &str) { - let retry = check_if_retry(msgtype, title, text); + let direct = self.lc.read().unwrap().direct.unwrap_or_default(); + let received = self.lc.read().unwrap().received; + let retry_for_relay = direct && !received; + let retry = check_if_retry(msgtype, title, text, retry_for_relay); self.ui_handler.msgbox(msgtype, title, text, link, retry); } @@ -745,6 +748,12 @@ impl Interface for Session { } } + fn set_connection_info(&mut self, direct: bool, received: bool) { + let mut lc = self.lc.write().unwrap(); + lc.direct = Some(direct); + lc.received = received; + } + fn set_force_relay(&mut self, direct: bool, received: bool) { let mut lc = self.lc.write().unwrap(); lc.force_relay = false; From c5ad9893ff04f2efbe300a9e278986eac6c732ce Mon Sep 17 00:00:00 2001 From: csf Date: Mon, 19 Dec 2022 22:38:33 +0900 Subject: [PATCH 1220/2015] request other user's peers in sam e group --- flutter/lib/models/group_model.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/flutter/lib/models/group_model.dart b/flutter/lib/models/group_model.dart index 98dc57086..adc92f182 100644 --- a/flutter/lib/models/group_model.dart +++ b/flutter/lib/models/group_model.dart @@ -106,7 +106,8 @@ class GroupModel { queryParameters: { 'current': current.toString(), 'pageSize': pageSize.toString(), - 'user_name': username + 'grp': gFFI.userModel.groupName.value, + 'target_user': username }); current += pageSize; final resp = await http.get(uri, headers: await getHttpHeaders()); From 4837d84209ee0de8bfd2ffa8938ec3ae029e3ab9 Mon Sep 17 00:00:00 2001 From: asur4s Date: Tue, 20 Dec 2022 01:09:35 -0800 Subject: [PATCH 1221/2015] opt: enum KeyboardMode --- libs/hbb_common/src/keyboard.rs | 39 +++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 libs/hbb_common/src/keyboard.rs diff --git a/libs/hbb_common/src/keyboard.rs b/libs/hbb_common/src/keyboard.rs new file mode 100644 index 000000000..10979f520 --- /dev/null +++ b/libs/hbb_common/src/keyboard.rs @@ -0,0 +1,39 @@ +use std::{fmt, slice::Iter, str::FromStr}; + +use crate::protos::message::KeyboardMode; + +impl fmt::Display for KeyboardMode { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + KeyboardMode::Legacy => write!(f, "legacy"), + KeyboardMode::Map => write!(f, "map"), + KeyboardMode::Translate => write!(f, "translate"), + KeyboardMode::Auto => write!(f, "auto"), + } + } +} + +impl FromStr for KeyboardMode { + type Err = (); + fn from_str(s: &str) -> Result { + match s { + "legacy" => Ok(KeyboardMode::Legacy), + "map" => Ok(KeyboardMode::Map), + "translate" => Ok(KeyboardMode::Translate), + "auto" => Ok(KeyboardMode::Auto), + _ => Err(()), + } + } +} + +impl KeyboardMode { + pub fn iter() -> Iter<'static, KeyboardMode> { + static KEYBOARD_MODES: [KeyboardMode; 4] = [ + KeyboardMode::Legacy, + KeyboardMode::Map, + KeyboardMode::Translate, + KeyboardMode::Auto, + ]; + KEYBOARD_MODES.iter() + } +} From c267fc9d9bfa4f6ff7a155fd9c3abd4ae3b67c0e Mon Sep 17 00:00:00 2001 From: asur4s Date: Tue, 20 Dec 2022 01:11:52 -0800 Subject: [PATCH 1222/2015] refactor: set default keyboard mode --- src/client.rs | 16 +++++++++------- src/common.rs | 9 +++++++++ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/client.rs b/src/client.rs index 03bbf5918..1bb2ff861 100644 --- a/src/client.rs +++ b/src/client.rs @@ -23,7 +23,7 @@ use hbb_common::{ Config, PeerConfig, PeerInfoSerde, CONNECT_TIMEOUT, READ_TIMEOUT, RELAY_PORT, RENDEZVOUS_TIMEOUT, }, - log, + get_version_number, log, message_proto::{option_message::BoolOption, *}, protobuf::Message as _, rand, @@ -47,7 +47,10 @@ pub use super::lang::*; pub mod file_trait; pub mod helper; pub mod io_loop; -use crate::server::video_service::{SCRAP_X11_REF_URL, SCRAP_X11_REQUIRED}; +use crate::{ + common::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); @@ -1410,12 +1413,11 @@ impl LoginConfigHandler { log::debug!("remove password of {}", self.id); } } - if config.keyboard_mode == "" { - if hbb_common::get_version_number(&pi.version) < hbb_common::get_version_number("1.2.0") - { - config.keyboard_mode = "legacy".to_string(); + if config.keyboard_mode.is_empty() { + if is_keyboard_mode_supported(&KeyboardMode::Map, get_version_number(&pi.version)) { + config.keyboard_mode = KeyboardMode::Map.to_string(); } else { - config.keyboard_mode = "map".to_string(); + config.keyboard_mode = KeyboardMode::Legacy.to_string(); } } self.conn_id = pi.conn_id; diff --git a/src/common.rs b/src/common.rs index 9023780f4..0f3794261 100644 --- a/src/common.rs +++ b/src/common.rs @@ -689,6 +689,15 @@ pub fn make_privacy_mode_msg(state: back_notification::PrivacyModeState) -> Mess msg_out } +pub fn is_keyboard_mode_supported(keyboard_mode: &KeyboardMode, version_number: i64) -> bool { + match keyboard_mode { + KeyboardMode::Legacy => true, + KeyboardMode::Map => version_number >= hbb_common::get_version_number("1.2.0"), + KeyboardMode::Translate => true, + KeyboardMode::Auto => true, + } +} + #[cfg(not(target_os = "linux"))] lazy_static::lazy_static! { pub static ref IS_X11: Mutex = Mutex::new(false); From c67b9528698fbfa902c9b10674234b89d6fcd519 Mon Sep 17 00:00:00 2001 From: csf Date: Tue, 20 Dec 2022 23:36:04 +0900 Subject: [PATCH 1223/2015] fix logOut fail, add logOut request timeout and catch err --- flutter/lib/models/user_model.dart | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/flutter/lib/models/user_model.dart b/flutter/lib/models/user_model.dart index 44fef5443..e5d2c9e15 100644 --- a/flutter/lib/models/user_model.dart +++ b/flutter/lib/models/user_model.dart @@ -79,12 +79,16 @@ class UserModel { final tag = gFFI.dialogManager.showLoading(translate('Waiting')); try { final url = await bind.mainGetApiServer(); - final _ = await http.post(Uri.parse('$url/api/logout'), - body: { - 'id': await bind.mainGetMyId(), - 'uuid': await bind.mainGetUuid(), - }, - headers: await getHttpHeaders()); + await http + .post(Uri.parse('$url/api/logout'), + body: { + 'id': await bind.mainGetMyId(), + 'uuid': await bind.mainGetUuid(), + }, + headers: await getHttpHeaders()) + .timeout(Duration(seconds: 2)); + } catch (e) { + print("request /api/logout failed: err=$e"); } finally { await reset(); gFFI.dialogManager.dismissByTag(tag); From 58c1be39c840dab33aab5e3d760f2c8688240442 Mon Sep 17 00:00:00 2001 From: csf Date: Tue, 20 Dec 2022 23:55:54 +0900 Subject: [PATCH 1224/2015] add catch err --- flutter/lib/desktop/pages/file_manager_page.dart | 2 ++ flutter/lib/desktop/widgets/remote_menubar.dart | 4 +++- flutter/lib/mobile/pages/remote_page.dart | 6 ++++-- flutter/lib/models/file_model.dart | 5 +++-- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index b9fa1a143..60b22a516 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -982,6 +982,8 @@ class _FileManagerPageState extends State }, dismissOnClicked: true)); } + } catch (e) { + debugPrint("buildBread fetchDirectory err=$e"); } finally { if (!isLocal) { _ffi.dialogManager.dismissByTag(loadingTag); diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index c1a7bdce2..a7d7b6dc6 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -1032,7 +1032,9 @@ class _RemoteMenubarState extends State { final h265 = codecsJson['h265'] ?? false; codecs.add(h264); codecs.add(h265); - } finally {} + } catch (e) { + debugPrint("Show Codec Preference err=$e"); + } if (codecs.length == 2 && (codecs[0] || codecs[1])) { displayMenu.add(MenuEntryRadios( text: translate('Codec Preference'), diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index d0388b8fe..c1db230bb 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -696,7 +696,7 @@ class _RemotePageState extends State { gFFI.dialogManager.show((setState, close) { void setMode(String? v) async { await bind.sessionPeerOption( - id: widget.id, name: "keyboard-mode", value: v ?? ""); + id: widget.id, name: "keyboard-mode", value: v ?? ""); setState(() => current = v ?? ''); Future.delayed(Duration(milliseconds: 300), close); } @@ -978,7 +978,9 @@ void showOptions( final h265 = codecsJson['h265'] ?? false; codecs.add(h264); codecs.add(h265); - } finally {} + } catch (e) { + debugPrint("Show Codec Preference err=$e"); + } } dialogManager.show((setState, close) { diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index 142479c2a..c08d2e623 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -213,7 +213,6 @@ class FileModel extends ChangeNotifier { } receiveFileDir(Map evt) { - // debugPrint("recv file dir:$evt"); if (evt['is_local'] == "false") { // init remote home, the connection will automatic read remote home when established, try { @@ -237,7 +236,9 @@ class FileModel extends ChangeNotifier { debugPrint("init remote home:${fd.path}"); _currentRemoteDir = fd; } - } finally {} + } catch (e) { + debugPrint("receiveFileDir err=$e"); + } } _fileFetcher.tryCompleteTask(evt['value'], evt['is_local']); notifyListeners(); From d910e7ad96c8389e1bc3f2a2f0f3c13d2b43485f Mon Sep 17 00:00:00 2001 From: Sangha Lee Date: Wed, 21 Dec 2022 03:32:42 +0900 Subject: [PATCH 1225/2015] update gstreamer --- Cargo.lock | 275 ++++++++++------------------- libs/scrap/Cargo.toml | 6 +- libs/scrap/src/wayland/pipewire.rs | 32 ++-- 3 files changed, 109 insertions(+), 204 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2b220c15d..336fc4b50 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -295,7 +295,7 @@ dependencies = [ "glib-sys 0.15.10", "gobject-sys 0.15.10", "libc", - "system-deps 6.0.3", + "system-deps", ] [[package]] @@ -313,6 +313,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "065374052e7df7ee4047b1160cca5e1467a12351a40b3da123c870ba0b8eda2a" +[[package]] +name = "atomic_refcell" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73b5e5f48b927f04e952dedc932f31995a65a0bf65ec971c74436e51bf6e970d" + [[package]] name = "atty" version = "0.2.14" @@ -519,7 +525,7 @@ checksum = "3c55d429bef56ac9172d25fecb85dc8068307d17acd74b377866b7a1ef25d3c8" dependencies = [ "glib-sys 0.15.10", "libc", - "system-deps 6.0.3", + "system-deps", ] [[package]] @@ -1996,7 +2002,7 @@ dependencies = [ "glib-sys 0.15.10", "gobject-sys 0.15.10", "libc", - "system-deps 6.0.3", + "system-deps", ] [[package]] @@ -2013,7 +2019,7 @@ dependencies = [ "libc", "pango-sys", "pkg-config", - "system-deps 6.0.3", + "system-deps", ] [[package]] @@ -2079,7 +2085,7 @@ dependencies = [ "glib-sys 0.15.10", "gobject-sys 0.15.10", "libc", - "system-deps 6.0.3", + "system-deps", "winapi 0.3.9", ] @@ -2092,29 +2098,10 @@ dependencies = [ "glib-sys 0.16.3", "gobject-sys 0.16.3", "libc", - "system-deps 6.0.3", + "system-deps", "winapi 0.3.9", ] -[[package]] -name = "glib" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c685013b7515e668f1b57a165b009d4d28cb139a8a989bbd699c10dad29d0c5" -dependencies = [ - "bitflags", - "futures-channel", - "futures-core", - "futures-executor", - "futures-task", - "futures-util", - "glib-macros 0.10.1", - "glib-sys 0.10.1", - "gobject-sys 0.10.0", - "libc", - "once_cell", -] - [[package]] name = "glib" version = "0.15.12" @@ -2157,22 +2144,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "glib-macros" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41486a26d1366a8032b160b59065a59fb528530a46a49f627e7048fb8c064039" -dependencies = [ - "anyhow", - "heck 0.3.3", - "itertools", - "proc-macro-crate 0.1.5", - "proc-macro-error", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "glib-macros" version = "0.15.11" @@ -2181,7 +2152,7 @@ checksum = "25a68131a662b04931e71891fb14aaf65ee4b44d08e8abc10f49e77418c86c64" dependencies = [ "anyhow", "heck 0.4.0", - "proc-macro-crate 1.2.1", + "proc-macro-crate", "proc-macro-error", "proc-macro2", "quote", @@ -2196,23 +2167,13 @@ checksum = "e084807350b01348b6d9dbabb724d1a0bb987f47a2c85de200e98e12e30733bf" dependencies = [ "anyhow", "heck 0.4.0", - "proc-macro-crate 1.2.1", + "proc-macro-crate", "proc-macro-error", "proc-macro2", "quote", "syn", ] -[[package]] -name = "glib-sys" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7e9b997a66e9a23d073f2b1abb4dbfc3925e0b8952f67efd8d9b6e168e4cdc1" -dependencies = [ - "libc", - "system-deps 1.3.2", -] - [[package]] name = "glib-sys" version = "0.15.10" @@ -2220,7 +2181,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef4b192f8e65e9cf76cbf4ea71fa8e3be4a0e18ffe3d68b8da6836974cc5bad4" dependencies = [ "libc", - "system-deps 6.0.3", + "system-deps", ] [[package]] @@ -2230,7 +2191,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61a4f46316d06bfa33a7ac22df6f0524c8be58e3db2d9ca99ccb1f357b62a65" dependencies = [ "libc", - "system-deps 6.0.3", + "system-deps", ] [[package]] @@ -2239,17 +2200,6 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" -[[package]] -name = "gobject-sys" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "952133b60c318a62bf82ee75b93acc7e84028a093e06b9e27981c2b6fe68218c" -dependencies = [ - "glib-sys 0.10.1", - "libc", - "system-deps 1.3.2", -] - [[package]] name = "gobject-sys" version = "0.15.10" @@ -2258,7 +2208,7 @@ checksum = "0d57ce44246becd17153bd035ab4d32cfee096a657fc01f2231c9278378d1e0a" dependencies = [ "glib-sys 0.15.10", "libc", - "system-deps 6.0.3", + "system-deps", ] [[package]] @@ -2269,28 +2219,28 @@ checksum = "3520bb9c07ae2a12c7f2fbb24d4efc11231c8146a86956413fb1a79bb760a0f1" dependencies = [ "glib-sys 0.16.3", "libc", - "system-deps 6.0.3", + "system-deps", ] [[package]] name = "gstreamer" -version = "0.16.7" +version = "0.19.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ff5d0f7ff308ae37e6eb47b6ded17785bdea06e438a708cd09e0288c1862f33" +checksum = "87a7570ad1d3c1cbf64561ada514fe0c03cf834f2076b85ffc616756c840b665" dependencies = [ "bitflags", "cfg-if 1.0.0", "futures-channel", "futures-core", "futures-util", - "glib 0.10.3", - "glib-sys 0.10.1", - "gobject-sys 0.10.0", + "glib 0.16.5", "gstreamer-sys", "libc", "muldiv", - "num-rational", + "num-integer", + "num-rational 0.4.1", "once_cell", + "option-operations", "paste", "pretty-hex", "thiserror", @@ -2298,94 +2248,86 @@ dependencies = [ [[package]] name = "gstreamer-app" -version = "0.16.5" +version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc80888271338c3ede875d8cafc452eb207476ff5539dcbe0018a8f5b827af0e" +checksum = "c45202b4d565034d4fe5577c990d3a99eaf0c2bfd2cb3f73f70db14d58e0208c" dependencies = [ "bitflags", "futures-core", "futures-sink", - "glib 0.10.3", - "glib-sys 0.10.1", - "gobject-sys 0.10.0", + "glib 0.16.5", "gstreamer", "gstreamer-app-sys", "gstreamer-base", - "gstreamer-sys", "libc", "once_cell", ] [[package]] name = "gstreamer-app-sys" -version = "0.9.1" +version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "813f64275c9e7b33b828b9efcf9dfa64b95996766d4de996e84363ac65b87e3d" +checksum = "29b0159da8dd0672c1a5507445c70c8dc483abfb63a0295cabaedd396f1d67d1" dependencies = [ - "glib-sys 0.10.1", + "glib-sys 0.16.3", "gstreamer-base-sys", "gstreamer-sys", "libc", - "system-deps 1.3.2", + "system-deps", ] [[package]] name = "gstreamer-base" -version = "0.16.5" +version = "0.19.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bafd01c56f59cb10f4b5a10f97bb4bdf8c2b2784ae5b04da7e2d400cf6e6afcf" +checksum = "a61a299f9ea2ca892b43e2e428b86c679875e95ba23f8ae06fd730308df630f0" dependencies = [ + "atomic_refcell", "bitflags", - "glib 0.10.3", - "glib-sys 0.10.1", - "gobject-sys 0.10.0", + "cfg-if 1.0.0", + "glib 0.16.5", "gstreamer", "gstreamer-base-sys", - "gstreamer-sys", "libc", ] [[package]] name = "gstreamer-base-sys" -version = "0.9.1" +version = "0.19.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4b7b6dc2d6e160a1ae28612f602bd500b3fa474ce90bf6bb2f08072682beef5" +checksum = "dbc3c4476e1503ae245c89fbe20060c30ec6ade5f44620bcc402cbc70a3911a1" dependencies = [ - "glib-sys 0.10.1", - "gobject-sys 0.10.0", + "glib-sys 0.16.3", + "gobject-sys 0.16.3", "gstreamer-sys", "libc", - "system-deps 1.3.2", + "system-deps", ] [[package]] name = "gstreamer-sys" -version = "0.9.1" +version = "0.19.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc1f154082d01af5718c5f8a8eb4f565a4ea5586ad8833a8fc2c2aa6844b601d" +checksum = "545f52ad8a480732cc4290fd65dfe42952c8ae374fe581831ba15981fedf18a4" dependencies = [ - "glib-sys 0.10.1", - "gobject-sys 0.10.0", + "glib-sys 0.16.3", + "gobject-sys 0.16.3", "libc", - "system-deps 1.3.2", + "system-deps", ] [[package]] name = "gstreamer-video" -version = "0.16.7" +version = "0.19.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7bbb1485d87469849ec45c08e03c2f280d3ea20ff3c439d03185be54e3ce98e" +checksum = "5e99623fb99436c4b2da66ae94b25881c94db5144afc1bd7c84cee5cabb72f18" dependencies = [ "bitflags", + "cfg-if 1.0.0", "futures-channel", - "futures-util", - "glib 0.10.3", - "glib-sys 0.10.1", - "gobject-sys 0.10.0", + "glib 0.16.5", "gstreamer", "gstreamer-base", - "gstreamer-base-sys", - "gstreamer-sys", "gstreamer-video-sys", "libc", "once_cell", @@ -2393,16 +2335,16 @@ dependencies = [ [[package]] name = "gstreamer-video-sys" -version = "0.9.1" +version = "0.19.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92347e46438007d6a2386302125f62cb9df6769cdacb931af5c0f12c1ee21de4" +checksum = "9206e9df0ed84824bfe4cc13e3359154ad7624221c7d3d6242585db3f19a15d9" dependencies = [ - "glib-sys 0.10.1", - "gobject-sys 0.10.0", + "glib-sys 0.16.3", + "gobject-sys 0.16.3", "gstreamer-base-sys", "gstreamer-sys", "libc", - "system-deps 1.3.2", + "system-deps", ] [[package]] @@ -2443,7 +2385,7 @@ dependencies = [ "gobject-sys 0.15.10", "libc", "pango-sys", - "system-deps 6.0.3", + "system-deps", ] [[package]] @@ -2453,7 +2395,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24f518afe90c23fba585b2d7697856f9e6a7bbc62f65588035e66f6afb01a2e9" dependencies = [ "anyhow", - "proc-macro-crate 1.2.1", + "proc-macro-crate", "proc-macro-error", "proc-macro2", "quote", @@ -2700,7 +2642,7 @@ dependencies = [ "byteorder", "color_quant", "num-iter", - "num-rational", + "num-rational 0.3.2", "num-traits 0.2.15", "png", "tiff", @@ -2790,15 +2732,6 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec947b7a4ce12e3b87e353abae7ce124d025b6c7d6c5aea5cc0bcf92e9510ded" -[[package]] -name = "itertools" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b" -dependencies = [ - "either", -] - [[package]] name = "itoa" version = "0.3.4" @@ -3252,9 +3185,9 @@ dependencies = [ [[package]] name = "muldiv" -version = "0.2.1" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0419348c027fa7be448d2ae7ea0e4e04c2334c31dc4e74ab29f00a2a7ca69204" +checksum = "956787520e75e9bd233246045d19f42fb73242759cc57fba9611d940ae96d4b0" [[package]] name = "ndk" @@ -3339,7 +3272,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0df7ac00c4672f9d5aece54ee3347520b7e20f158656c7db2e6de01902eb7a6c" dependencies = [ "darling", - "proc-macro-crate 1.2.1", + "proc-macro-crate", "proc-macro2", "quote", "syn", @@ -3526,6 +3459,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" @@ -3569,7 +3513,7 @@ version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b0498641e53dd6ac1a4f22547548caa6864cc4933784319cd1775271c5a46ce" dependencies = [ - "proc-macro-crate 1.2.1", + "proc-macro-crate", "proc-macro2", "quote", "syn", @@ -3657,6 +3601,15 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +[[package]] +name = "option-operations" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c26d27bb1aeab65138e4bf7666045169d1717febcc9ff870166be8348b223d0" +dependencies = [ + "paste", +] + [[package]] name = "ordered-multimap" version = "0.4.3" @@ -3711,7 +3664,7 @@ dependencies = [ "glib-sys 0.15.10", "gobject-sys 0.15.10", "libc", - "system-deps 6.0.3", + "system-deps", ] [[package]] @@ -3927,9 +3880,9 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "pretty-hex" -version = "0.2.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc5c99d529f0d30937f6f4b8a86d988047327bb88d04d2c4afc356de74722131" +checksum = "c6fa0831dd7cc608c38a5e323422a0077678fa5744aa2be4ad91c4ece8eec8d5" [[package]] name = "primal-check" @@ -3940,15 +3893,6 @@ dependencies = [ "num-integer", ] -[[package]] -name = "proc-macro-crate" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" -dependencies = [ - "toml", -] - [[package]] name = "proc-macro-crate" version = "1.2.1" @@ -4318,8 +4262,8 @@ dependencies = [ "libc", "log", "mio 0.8.5", - "strum 0.24.1", - "strum_macros 0.24.3", + "strum", + "strum_macros", "widestring 1.0.2", "winapi 0.3.9", "x11 2.20.1", @@ -5174,30 +5118,12 @@ dependencies = [ "syn", ] -[[package]] -name = "strum" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57bd81eb48f4c437cadc685403cad539345bf703d78e63707418431cecd4522b" - [[package]] name = "strum" version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" -[[package]] -name = "strum_macros" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87c85aa3f8ea653bfd3ddf25f7ee357ee4d204731f6aa9ad04002306f6e2774c" -dependencies = [ - "heck 0.3.3", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "strum_macros" version = "0.24.3" @@ -5283,21 +5209,6 @@ dependencies = [ "libc", ] -[[package]] -name = "system-deps" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f3ecc17269a19353b3558b313bba738b25d82993e30d62a18406a24aba4649b" -dependencies = [ - "heck 0.3.3", - "pkg-config", - "strum 0.18.0", - "strum_macros 0.18.0", - "thiserror", - "toml", - "version-compare 0.0.10", -] - [[package]] name = "system-deps" version = "6.0.3" @@ -5308,7 +5219,7 @@ dependencies = [ "heck 0.4.0", "pkg-config", "toml", - "version-compare 0.1.1", + "version-compare", ] [[package]] @@ -5739,12 +5650,6 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" -[[package]] -name = "version-compare" -version = "0.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d63556a25bae6ea31b52e640d7c41d1ab27faba4ccb600013837a3d0b3994ca1" - [[package]] name = "version-compare" version = "0.1.1" @@ -6571,7 +6476,7 @@ version = "3.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45066039ebf3330820e495e854f8b312abb68f0a39e97972d092bd72e8bb3e8e" dependencies = [ - "proc-macro-crate 1.2.1", + "proc-macro-crate", "proc-macro2", "quote", "regex", @@ -6638,7 +6543,7 @@ version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "155247a5d1ab55e335421c104ccd95d64f17cebbd02f50cdbc1c33385f9c4d81" dependencies = [ - "proc-macro-crate 1.2.1", + "proc-macro-crate", "proc-macro2", "quote", "syn", diff --git a/libs/scrap/Cargo.toml b/libs/scrap/Cargo.toml index e2eb43177..de6a6f2bf 100644 --- a/libs/scrap/Cargo.toml +++ b/libs/scrap/Cargo.toml @@ -48,9 +48,9 @@ bindgen = "0.59" [target.'cfg(target_os = "linux")'.dependencies] dbus = { version = "0.9", optional = true } tracing = { version = "0.1", optional = true } -gstreamer = { version = "0.16", optional = true } -gstreamer-app = { version = "0.16", features = ["v1_10"], optional = true } -gstreamer-video = { version = "0.16", optional = true } +gstreamer = { version = "0.19", optional = true } +gstreamer-app = { version = "0.19", features = ["v1_16"], optional = true } +gstreamer-video = { version = "0.19", optional = true } [target.'cfg(any(target_os = "windows", target_os = "linux"))'.dependencies] hwcodec = { git = "https://github.com/21pages/hwcodec", optional = true } diff --git a/libs/scrap/src/wayland/pipewire.rs b/libs/scrap/src/wayland/pipewire.rs index a7b4c1357..d43a1b278 100644 --- a/libs/scrap/src/wayland/pipewire.rs +++ b/libs/scrap/src/wayland/pipewire.rs @@ -129,18 +129,18 @@ impl PipeWireRecorder { pub fn new(capturable: PipeWireCapturable) -> Result> { let pipeline = gst::Pipeline::new(None); - let src = gst::ElementFactory::make("pipewiresrc", None)?; - src.set_property("fd", &capturable.fd.as_raw_fd())?; - src.set_property("path", &format!("{}", capturable.path))?; - src.set_property("keepalive_time", &1_000.as_raw_fd())?; + let src = gst::ElementFactory::make_with_name("pipewiresrc", None).unwrap(); + src.set_property("fd", &capturable.fd.as_raw_fd()); + src.set_property("path", &format!("{}", capturable.path)); + src.set_property("keepalive_time", &1_000.as_raw_fd()); // For some reason pipewire blocks on destruction of AppSink if this is not set to true, // see: https://gitlab.freedesktop.org/pipewire/pipewire/-/issues/982 - src.set_property("always-copy", &true)?; + src.set_property("always-copy", &true); - let sink = gst::ElementFactory::make("appsink", None)?; - sink.set_property("drop", &true)?; - sink.set_property("max-buffers", &1u32)?; + let sink = gst::ElementFactory::make_with_name("appsink", None).unwrap(); + sink.set_property("drop", &true); + sink.set_property("max-buffers", &1u32); pipeline.add_many(&[&src, &sink])?; src.link(&sink)?; @@ -173,20 +173,20 @@ impl Recorder for PipeWireRecorder { .try_pull_sample(gst::ClockTime::from_mseconds(timeout_ms)) { let cap = sample - .get_caps() + .caps() .ok_or("Failed get caps")? - .get_structure(0) + .structure(0) .ok_or("Failed to get structure")?; - let w: i32 = cap.get_value("width")?.get_some()?; - let h: i32 = cap.get_value("height")?.get_some()?; + let w: i32 = cap.value("width")?.get()?; + let h: i32 = cap.value("height")?.get()?; let w = w as usize; let h = h as usize; let buf = sample - .get_buffer_owned() + .buffer_owned() .ok_or_else(|| GStreamerError("Failed to get owned buffer.".into()))?; let mut crop = buf - .get_meta::() - .map(|m| m.get_rect()); + .meta::() + .map(|m| m.rect()); // only crop if necessary if Some((0, 0, w as u32, h as u32)) == crop { crop = None; @@ -197,7 +197,7 @@ impl Recorder for PipeWireRecorder { if let Err(..) = crate::would_block_if_equal(&mut self.saved_raw_data, buf.as_slice()) { return Ok(PixelProvider::NONE); } - let buf_size = buf.get_size(); + let buf_size = buf.size(); // BGRx is 4 bytes per pixel if buf_size != (w * h * 4) { // for some reason the width and height of the caps do not guarantee correct buffer From 38f66df091e1d2b4ce8f7be84d40af73a98a0c78 Mon Sep 17 00:00:00 2001 From: Sangha Lee Date: Wed, 21 Dec 2022 04:12:34 +0900 Subject: [PATCH 1226/2015] implement RGB0 #2608 --- libs/scrap/src/common/wayland.rs | 6 +++++ libs/scrap/src/wayland/capturable.rs | 2 ++ libs/scrap/src/wayland/pipewire.rs | 33 ++++++++++++++++++---------- 3 files changed, 30 insertions(+), 11 deletions(-) diff --git a/libs/scrap/src/common/wayland.rs b/libs/scrap/src/common/wayland.rs index 2593e56fe..6e89568e1 100644 --- a/libs/scrap/src/common/wayland.rs +++ b/libs/scrap/src/common/wayland.rs @@ -50,6 +50,12 @@ impl TraitCapturer for Capturer { } else { x })), + PixelProvider::RGB0(w, h, x) => Ok(Frame(if self.2 { + crate::common::rgba_to_i420(w as _, h as _, &x, &mut self.3); + &self.3[..] + } else { + x + })), PixelProvider::NONE => Err(std::io::ErrorKind::WouldBlock.into()), _ => Err(map_err("Invalid data")), } diff --git a/libs/scrap/src/wayland/capturable.rs b/libs/scrap/src/wayland/capturable.rs index 05a5ec71d..61f80ecbf 100644 --- a/libs/scrap/src/wayland/capturable.rs +++ b/libs/scrap/src/wayland/capturable.rs @@ -4,6 +4,7 @@ use std::error::Error; pub enum PixelProvider<'a> { // 8 bits per color RGB(usize, usize, &'a [u8]), + RGB0(usize, usize, &'a [u8]), BGR0(usize, usize, &'a [u8]), // width, height, stride BGR0S(usize, usize, usize, &'a [u8]), @@ -14,6 +15,7 @@ impl<'a> PixelProvider<'a> { pub fn size(&self) -> (usize, usize) { match self { PixelProvider::RGB(w, h, _) => (*w, *h), + PixelProvider::RGB0(w, h, _) => (*w, *h), PixelProvider::BGR0(w, h, _) => (*w, *h), PixelProvider::BGR0S(w, h, _, _) => (*w, *h), PixelProvider::NONE => (0, 0), diff --git a/libs/scrap/src/wayland/pipewire.rs b/libs/scrap/src/wayland/pipewire.rs index d43a1b278..8e3848862 100644 --- a/libs/scrap/src/wayland/pipewire.rs +++ b/libs/scrap/src/wayland/pipewire.rs @@ -117,6 +117,7 @@ impl Capturable for PipeWireCapturable { pub struct PipeWireRecorder { buffer: Option>, buffer_cropped: Vec, + pix_fmt: String, is_cropped: bool, pipeline: gst::Pipeline, appsink: AppSink, @@ -144,19 +145,27 @@ impl PipeWireRecorder { pipeline.add_many(&[&src, &sink])?; src.link(&sink)?; + let appsink = sink .dynamic_cast::() .map_err(|_| GStreamerError("Sink element is expected to be an appsink!".into()))?; - appsink.set_caps(Some(&gst::Caps::new_simple( + let mut caps = gst::Caps::new_empty(); + caps.merge_structure(gst::structure::Structure::new( "video/x-raw", &[("format", &"BGRx")], - ))); + )); + caps.merge_structure(gst::structure::Structure::new( + "video/x-raw", + &[("format", &"RGBx")], + )); + appsink.set_caps(Some(&caps)); pipeline.set_state(gst::State::Playing)?; Ok(Self { pipeline, appsink, buffer: None, + pix_fmt: "".into(), width: 0, height: 0, buffer_cropped: vec![], @@ -179,6 +188,7 @@ impl Recorder for PipeWireRecorder { .ok_or("Failed to get structure")?; let w: i32 = cap.value("width")?.get()?; let h: i32 = cap.value("height")?.get()?; + self.pix_fmt = cap.value("format")?.get()?; let w = w as usize; let h = h as usize; let buf = sample @@ -241,15 +251,16 @@ impl Recorder for PipeWireRecorder { if self.buffer.is_none() { return Err(Box::new(GStreamerError("No buffer available!".into()))); } - Ok(PixelProvider::BGR0( - self.width, - self.height, - if self.is_cropped { - self.buffer_cropped.as_slice() - } else { - self.buffer.as_ref().unwrap().as_slice() - }, - )) + let buf = if self.is_cropped { + self.buffer_cropped.as_slice() + } else { + self.buffer.as_ref().unwrap().as_slice() + }; + match self.pix_fmt.as_str() { + "BGRx" => Ok(PixelProvider::BGR0(self.width, self.height, buf)), + "RGBx" => Ok(PixelProvider::RGB0(self.width, self.height, buf)), + _ => unreachable!(), + } } } From 8e0b7e1fc36b97676470b340d1fba3582f5afb6e Mon Sep 17 00:00:00 2001 From: solokot Date: Tue, 20 Dec 2022 23:38:42 +0300 Subject: [PATCH 1227/2015] Update ru.rs --- src/lang/ru.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 66c8f7626..f42d3146e 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -39,9 +39,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Изменить ID"), ("Website", "Сайт"), ("About", "О программе"), - ("About RustDesk", ""), - ("Slogan_tip", ""), - ("Privacy Statement", ""), + ("About RustDesk", "О RustDesk"), + ("Slogan_tip", "Сделано с душой в этом безумном мире!"), + ("Privacy Statement", "Заявление о конфиденциальности"), ("Mute", "Отключить звук"), ("Audio Input", "Аудиовход"), ("Enhancements", "Улучшения"), From fea017148665e45f62993570791af9ab9514ec39 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 21 Dec 2022 10:57:41 +0800 Subject: [PATCH 1228/2015] fix android compile issue --- src/keyboard.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/keyboard.rs b/src/keyboard.rs index df052ebbc..72efac381 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -339,8 +339,12 @@ pub fn event_to_key_event(event: &Event) -> Option { translate_keyboard_mode(event, key_event)? } _ => { + let res; #[cfg(not(any(target_os = "android", target_os = "ios")))] - legacy_keyboard_mode(event, key_event)? + res = legacy_keyboard_mode(event, key_event)?; + #[cfg(any(target_os = "android", target_os = "ios"))] + res = None; + res } }; #[cfg(not(any(target_os = "android", target_os = "ios")))] From e47d1c7dbe843f952210de755a508fed89b21c07 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 21 Dec 2022 11:13:08 +0800 Subject: [PATCH 1229/2015] fix compile again --- src/keyboard.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/keyboard.rs b/src/keyboard.rs index 72efac381..2c09258d8 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -343,7 +343,7 @@ pub fn event_to_key_event(event: &Event) -> Option { #[cfg(not(any(target_os = "android", target_os = "ios")))] res = legacy_keyboard_mode(event, key_event)?; #[cfg(any(target_os = "android", target_os = "ios"))] - res = None; + res = None?; res } }; From f0653bb10a55243aa6864167261b17852674cb00 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 21 Dec 2022 11:38:54 +0800 Subject: [PATCH 1230/2015] fix compile again --- src/keyboard.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/keyboard.rs b/src/keyboard.rs index 2c09258d8..641925338 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -339,12 +339,14 @@ pub fn event_to_key_event(event: &Event) -> Option { translate_keyboard_mode(event, key_event)? } _ => { - let res; #[cfg(not(any(target_os = "android", target_os = "ios")))] - res = legacy_keyboard_mode(event, key_event)?; + { + legacy_keyboard_mode(event, key_event)? + } #[cfg(any(target_os = "android", target_os = "ios"))] - res = None?; - res + { + None? + } } }; #[cfg(not(any(target_os = "android", target_os = "ios")))] From 8bb62abd3e487b66f4566e8f360bd602ab5ea297 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 20 Dec 2022 22:07:48 +0800 Subject: [PATCH 1231/2015] fix: cannot input alt+tab when cursor is outside --- flutter/lib/desktop/pages/remote_page.dart | 7 +++++-- flutter/lib/desktop/pages/remote_tab_page.dart | 15 ++++++++++++++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 130a5e6ad..e66d84a35 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -157,6 +157,11 @@ class _RemotePageState extends State focusNode: _rawKeyFocusNode, onFocusChange: (bool v) { _imageFocused = v; + if (_imageFocused) { + _ffi.inputModel.enterOrLeave(true); + } else { + _ffi.inputModel.enterOrLeave(false); + } }, inputModel: _ffi.inputModel, child: getBodyForDesktop(context))); @@ -195,7 +200,6 @@ class _RemotePageState extends State // } } - _ffi.inputModel.enterOrLeave(true); } void leaveView(PointerExitEvent evt) { @@ -208,7 +212,6 @@ class _RemotePageState extends State // } } - _ffi.inputModel.enterOrLeave(false); } Widget getBodyForDesktop(BuildContext context) { diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index 713c3d13c..f7237af96 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -37,7 +37,8 @@ class ConnectionTabPage extends StatefulWidget { State createState() => _ConnectionTabPageState(params); } -class _ConnectionTabPageState extends State { +class _ConnectionTabPageState extends State + with MultiWindowListener { final tabController = Get.put(DesktopTabController( tabType: DesktopTabType.remoteScreen, onSelected: (_, id) => bind.setCurSessionId(id: id))); @@ -105,14 +106,26 @@ class _ConnectionTabPageState extends State { } _update_remote_count(); }); + DesktopMultiWindow.addListener(this); } @override void dispose() { super.dispose(); + DesktopMultiWindow.removeListener(this); _menubarState.save(); } + @override + void onWindowBlur() { + super.onWindowBlur(); + final state = tabController.state.value; + if (state.tabs.isNotEmpty) { + final sessionId = state.tabs[state.selected].key; + bind.sessionEnterOrLeave(id: sessionId, enter: false); + } + } + @override Widget build(BuildContext context) { final tabWidget = Obx( From 52f60154db4e292c612fa15e3f01ac558c666708 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Wed, 21 Dec 2022 10:40:43 +0800 Subject: [PATCH 1232/2015] opt: remove not working on multiple times --- flutter/lib/desktop/pages/remote_page.dart | 15 +++++++++++++-- flutter/lib/desktop/pages/remote_tab_page.dart | 10 ---------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index e66d84a35..4061fb74d 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:io'; import 'dart:ui' as ui; +import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -48,7 +49,7 @@ class RemotePage extends StatefulWidget { } class _RemotePageState extends State - with AutomaticKeepAliveClientMixin { + with AutomaticKeepAliveClientMixin, MultiWindowListener { Timer? _timer; String keyboardMode = "legacy"; final _cursorOverImage = false.obs; @@ -57,7 +58,7 @@ class _RemotePageState extends State late RxBool _remoteCursorMoved; late RxBool _keyboardEnabled; - final FocusNode _rawKeyFocusNode = FocusNode(); + final FocusNode _rawKeyFocusNode = FocusNode(debugLabel: "rawkeyFocusNode"); var _imageFocused = false; Function(bool)? _onEnterOrLeaveImage4Menubar; @@ -111,6 +112,7 @@ class _RemotePageState extends State id: widget.id, arg: 'show-remote-cursor'); _zoomCursor.value = bind.sessionGetToggleOptionSync(id: widget.id, arg: 'zoom-cursor'); + DesktopMultiWindow.addListener(this); // if (!_isCustomCursorInited) { // customCursorController.registerNeedUpdateCursorCallback( // (String? lastKey, String? currentKey) async { @@ -124,9 +126,18 @@ class _RemotePageState extends State // } } + @override + void onWindowBlur() { + super.onWindowBlur(); + // unfocus the key focus when the whole window is lost focus, + // and let OS to handle events instead. + _rawKeyFocusNode.unfocus(); + } + @override void dispose() { debugPrint("REMOTE PAGE dispose ${widget.id}"); + DesktopMultiWindow.removeListener(this); _ffi.dialogManager.hideMobileActionsOverlay(); _ffi.recordingModel.onClose(); _rawKeyFocusNode.dispose(); diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index f7237af96..10c2d4487 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -116,16 +116,6 @@ class _ConnectionTabPageState extends State _menubarState.save(); } - @override - void onWindowBlur() { - super.onWindowBlur(); - final state = tabController.state.value; - if (state.tabs.isNotEmpty) { - final sessionId = state.tabs[state.selected].key; - bind.sessionEnterOrLeave(id: sessionId, enter: false); - } - } - @override Widget build(BuildContext context) { final tabWidget = Obx( From 01f497c234c3fa3f58981819b9652ab780035a21 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Wed, 21 Dec 2022 11:50:30 +0800 Subject: [PATCH 1233/2015] opt: remove unnecessary window listener --- flutter/lib/desktop/pages/remote_tab_page.dart | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index 10c2d4487..713c3d13c 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -37,8 +37,7 @@ class ConnectionTabPage extends StatefulWidget { State createState() => _ConnectionTabPageState(params); } -class _ConnectionTabPageState extends State - with MultiWindowListener { +class _ConnectionTabPageState extends State { final tabController = Get.put(DesktopTabController( tabType: DesktopTabType.remoteScreen, onSelected: (_, id) => bind.setCurSessionId(id: id))); @@ -106,13 +105,11 @@ class _ConnectionTabPageState extends State } _update_remote_count(); }); - DesktopMultiWindow.addListener(this); } @override void dispose() { super.dispose(); - DesktopMultiWindow.removeListener(this); _menubarState.save(); } From 4f74acba76ad8a9da3bc41719b71ede3ec4d3cc4 Mon Sep 17 00:00:00 2001 From: csf Date: Wed, 21 Dec 2022 15:14:43 +0900 Subject: [PATCH 1234/2015] add ServerConfig, update server config import and export --- flutter/lib/common.dart | 39 +++++++++++++++ .../desktop/pages/desktop_setting_page.dart | 47 +++++++++---------- 2 files changed, 62 insertions(+), 24 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 15d058b87..8e8e50ae9 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1510,3 +1510,42 @@ Pointer getOSVERSIONINFOEXPointer() { bool get kUseCompatibleUiMode => Platform.isWindows && const [WindowsTarget.w7].contains(windowsBuildNumber.windowsVersion); + +class ServerConfig { + late String idServer; + late String relayServer; + late String apiServer; + late String key; + + ServerConfig( + {String? idServer, String? relayServer, String? apiServer, String? key}) { + this.idServer = idServer?.trim() ?? ''; + this.relayServer = relayServer?.trim() ?? ''; + this.apiServer = apiServer?.trim() ?? ''; + this.key = key?.trim() ?? ''; + } + + /// throw decoding failure + ServerConfig.decode(String msg) { + final input = msg.split('').reversed.join(''); + final bytes = base64Decode(base64.normalize(input)); + final json = jsonDecode(utf8.decode(bytes)); + + idServer = json['host'] ?? ''; + relayServer = json['relay'] ?? ''; + apiServer = json['api'] ?? ''; + key = json['key'] ?? ''; + } + + String encode() { + Map config = {}; + config['host'] = idServer.trim(); + config['relay'] = relayServer.trim(); + config['api'] = apiServer.trim(); + config['key'] = key.trim(); + return base64Encode(Uint8List.fromList(jsonEncode(config).codeUnits)) + .split('') + .reversed + .join(); + } +} diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 613f19810..a45de24b0 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -958,23 +958,17 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin { import() { Clipboard.getData(Clipboard.kTextPlain).then((value) { - TextEditingController mytext = TextEditingController(); - String? aNullableString = ''; - aNullableString = value?.text; - mytext.text = aNullableString.toString(); - if (mytext.text.isNotEmpty) { + final text = value?.text; + if (text != null && text.isNotEmpty) { try { - Map config = jsonDecode(mytext.text); - if (config.containsKey('IdServer')) { - String id = config['IdServer'] ?? ''; - String relay = config['RelayServer'] ?? ''; - String api = config['ApiServer'] ?? ''; - String key = config['Key'] ?? ''; - idController.text = id; - relayController.text = relay; - apiController.text = api; - keyController.text = key; - Future success = set(id, relay, api, key); + final sc = ServerConfig.decode(text); + if (sc.idServer.isNotEmpty) { + idController.text = sc.idServer; + relayController.text = sc.relayServer; + apiController.text = sc.apiServer; + keyController.text = sc.key; + Future success = + set(sc.idServer, sc.relayServer, sc.apiServer, sc.key); success.then((value) { if (value) { showToast( @@ -996,12 +990,15 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin { } export() { - Map config = {}; - config['IdServer'] = idController.text.trim(); - config['RelayServer'] = relayController.text.trim(); - config['ApiServer'] = apiController.text.trim(); - config['Key'] = keyController.text.trim(); - Clipboard.setData(ClipboardData(text: jsonEncode(config))); + final text = ServerConfig( + idServer: idController.text, + relayServer: relayController.text, + apiServer: apiController.text, + key: keyController.text) + .encode(); + debugPrint("ServerConfig export: $text"); + + Clipboard.setData(ClipboardData(text: text)); showToast(translate('Export server configuration successfully')); } @@ -1106,8 +1103,10 @@ class _AboutState extends State<_About> { const SizedBox( height: 8.0, ), - Text(translate('Version') + ': $version').marginSymmetric(vertical: 4.0), - Text(translate('Build Date') + ': $buildDate').marginSymmetric(vertical: 4.0), + Text(translate('Version') + ': $version') + .marginSymmetric(vertical: 4.0), + Text(translate('Build Date') + ': $buildDate') + .marginSymmetric(vertical: 4.0), InkWell( onTap: () { launchUrlString('https://rustdesk.com/privacy'); From ecac34af803dd50bb60729aa860fe8e56c32bcd9 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 21 Dec 2022 14:30:24 +0800 Subject: [PATCH 1235/2015] fix android CI --- src/keyboard.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/keyboard.rs b/src/keyboard.rs index 641925338..4b42bdf5d 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -635,6 +635,8 @@ pub fn map_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option rdev::linux_code_to_macos_code(event.code as _)?, _ => event.code as _, }; + #[cfg(any(target_os = "android", target_os = "ios"))] + let keycode = 0; key_event.set_chr(keycode); Some(key_event) From f5cc55ab3db9e3db05381dcacf2fe181c22d2f31 Mon Sep 17 00:00:00 2001 From: csf Date: Wed, 21 Dec 2022 15:41:07 +0900 Subject: [PATCH 1236/2015] refactor mobile import ServerConfig --- flutter/lib/mobile/pages/scan_page.dart | 160 ++---------------------- flutter/lib/mobile/widgets/dialog.dart | 144 +++++++++++++++++++++ 2 files changed, 151 insertions(+), 153 deletions(-) diff --git a/flutter/lib/mobile/pages/scan_page.dart b/flutter/lib/mobile/pages/scan_page.dart index 810bcbca3..32208f6a4 100644 --- a/flutter/lib/mobile/pages/scan_page.dart +++ b/flutter/lib/mobile/pages/scan_page.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:convert'; import 'dart:io'; import 'package:flutter/material.dart'; @@ -9,11 +8,11 @@ import 'package:qr_code_scanner/qr_code_scanner.dart'; import 'package:zxing2/qrcode.dart'; import '../../common.dart'; -import '../../models/platform_model.dart'; +import '../widgets/dialog.dart'; class ScanPage extends StatefulWidget { @override - _ScanPageState createState() => _ScanPageState(); + State createState() => _ScanPageState(); } class _ScanPageState extends State { @@ -42,9 +41,9 @@ class _ScanPageState extends State { icon: Icon(Icons.image_search), iconSize: 32.0, onPressed: () async { - final ImagePicker _picker = ImagePicker(); + final ImagePicker picker = ImagePicker(); final XFile? file = - await _picker.pickImage(source: ImageSource.gallery); + await picker.pickImage(source: ImageSource.gallery); if (file != null) { var image = img.decodeNamedImage( File(file.path).readAsBytesSync(), file.path)!; @@ -139,158 +138,13 @@ class _ScanPageState extends State { return; } try { - Map values = json.decode(data.substring(7)); - var host = values['host'] != null ? values['host'] as String : ''; - var key = values['key'] != null ? values['key'] as String : ''; - var api = values['api'] != null ? values['api'] as String : ''; + final sc = ServerConfig.decode(data.substring(7)); Timer(Duration(milliseconds: 60), () { - showServerSettingsWithValue(host, '', key, api, gFFI.dialogManager); + showServerSettingsWithValue(sc.idServer, sc.relayServer, sc.key, + sc.apiServer, gFFI.dialogManager); }); } catch (e) { showToast('Invalid QR code'); } } } - -void showServerSettingsWithValue(String id, String relay, String key, - String api, OverlayDialogManager dialogManager) async { - Map oldOptions = jsonDecode(await bind.mainGetOptions()); - String id0 = oldOptions['custom-rendezvous-server'] ?? ""; - String relay0 = oldOptions['relay-server'] ?? ""; - String api0 = oldOptions['api-server'] ?? ""; - String key0 = oldOptions['key'] ?? ""; - var isInProgress = false; - final idController = TextEditingController(text: id); - final relayController = TextEditingController(text: relay); - final apiController = TextEditingController(text: api); - - String? idServerMsg; - String? relayServerMsg; - String? apiServerMsg; - - dialogManager.show((setState, close) { - Future validate() async { - if (idController.text != id) { - final res = await validateAsync(idController.text); - setState(() => idServerMsg = res); - if (idServerMsg != null) return false; - id = idController.text; - } - if (relayController.text != relay) { - relayServerMsg = await validateAsync(relayController.text); - if (relayServerMsg != null) return false; - relay = relayController.text; - } - if (apiController.text != relay) { - apiServerMsg = await validateAsync(apiController.text); - if (apiServerMsg != null) return false; - api = apiController.text; - } - return true; - } - - return CustomAlertDialog( - title: Text(translate('ID/Relay Server')), - content: Form( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TextFormField( - controller: idController, - decoration: InputDecoration( - labelText: translate('ID Server'), - errorText: idServerMsg), - ) - ] + - (isAndroid - ? [ - TextFormField( - controller: relayController, - decoration: InputDecoration( - labelText: translate('Relay Server'), - errorText: relayServerMsg), - ) - ] - : []) + - [ - TextFormField( - controller: apiController, - decoration: InputDecoration( - labelText: translate('API Server'), - ), - autovalidateMode: AutovalidateMode.onUserInteraction, - validator: (v) { - if (v != null && v.length > 0) { - if (!(v.startsWith('http://') || - v.startsWith("https://"))) { - return translate("invalid_http"); - } - } - return apiServerMsg; - }, - ), - TextFormField( - initialValue: key, - decoration: InputDecoration( - labelText: 'Key', - ), - onChanged: (String? value) { - if (value != null) key = value.trim(); - }, - ), - Offstage( - offstage: !isInProgress, - child: LinearProgressIndicator()) - ])), - actions: [ - TextButton( - style: flatButtonStyle, - onPressed: () { - close(); - }, - child: Text(translate('Cancel')), - ), - TextButton( - style: flatButtonStyle, - onPressed: () async { - setState(() { - idServerMsg = null; - relayServerMsg = null; - apiServerMsg = null; - isInProgress = true; - }); - if (await validate()) { - if (id != id0) { - if (id0.isNotEmpty) { - await gFFI.userModel.logOut(); - } - bind.mainSetOption(key: "custom-rendezvous-server", value: id); - } - if (relay != relay0) { - bind.mainSetOption(key: "relay-server", value: relay); - } - if (key != key0) bind.mainSetOption(key: "key", value: key); - if (api != api0) { - bind.mainSetOption(key: "api-server", value: api); - } - close(); - } - setState(() { - isInProgress = false; - }); - }, - child: Text(translate('OK')), - ), - ], - ); - }); -} - -Future validateAsync(String value) async { - value = value.trim(); - if (value.isEmpty) { - return null; - } - final res = await bind.mainTestIfValidServer(server: value); - return res.isEmpty ? null : res; -} diff --git a/flutter/lib/mobile/widgets/dialog.dart b/flutter/lib/mobile/widgets/dialog.dart index 96f96658a..8cfaf6a02 100644 --- a/flutter/lib/mobile/widgets/dialog.dart +++ b/flutter/lib/mobile/widgets/dialog.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:convert'; import 'package:flutter/material.dart'; import '../../common.dart'; @@ -236,6 +237,149 @@ void wrongPasswordDialog(String id, OverlayDialogManager dialogManager) { ])); } +void showServerSettingsWithValue(String id, String relay, String key, + String api, OverlayDialogManager dialogManager) async { + Map oldOptions = jsonDecode(await bind.mainGetOptions()); + String id0 = oldOptions['custom-rendezvous-server'] ?? ""; + String relay0 = oldOptions['relay-server'] ?? ""; + String api0 = oldOptions['api-server'] ?? ""; + String key0 = oldOptions['key'] ?? ""; + var isInProgress = false; + final idController = TextEditingController(text: id); + final relayController = TextEditingController(text: relay); + final apiController = TextEditingController(text: api); + + String? idServerMsg; + String? relayServerMsg; + String? apiServerMsg; + + dialogManager.show((setState, close) { + Future validate() async { + if (idController.text != id) { + final res = await validateAsync(idController.text); + setState(() => idServerMsg = res); + if (idServerMsg != null) return false; + id = idController.text; + } + if (relayController.text != relay) { + relayServerMsg = await validateAsync(relayController.text); + if (relayServerMsg != null) return false; + relay = relayController.text; + } + if (apiController.text != relay) { + apiServerMsg = await validateAsync(apiController.text); + if (apiServerMsg != null) return false; + api = apiController.text; + } + return true; + } + + return CustomAlertDialog( + title: Text(translate('ID/Relay Server')), + content: Form( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextFormField( + controller: idController, + decoration: InputDecoration( + labelText: translate('ID Server'), + errorText: idServerMsg), + ) + ] + + (isAndroid + ? [ + TextFormField( + controller: relayController, + decoration: InputDecoration( + labelText: translate('Relay Server'), + errorText: relayServerMsg), + ) + ] + : []) + + [ + TextFormField( + controller: apiController, + decoration: InputDecoration( + labelText: translate('API Server'), + ), + autovalidateMode: AutovalidateMode.onUserInteraction, + validator: (v) { + if (v != null && v.isNotEmpty) { + if (!(v.startsWith('http://') || + v.startsWith("https://"))) { + return translate("invalid_http"); + } + } + return apiServerMsg; + }, + ), + TextFormField( + initialValue: key, + decoration: InputDecoration( + labelText: 'Key', + ), + onChanged: (String? value) { + if (value != null) key = value.trim(); + }, + ), + Offstage( + offstage: !isInProgress, + child: LinearProgressIndicator()) + ])), + actions: [ + TextButton( + style: flatButtonStyle, + onPressed: () { + close(); + }, + child: Text(translate('Cancel')), + ), + TextButton( + style: flatButtonStyle, + onPressed: () async { + setState(() { + idServerMsg = null; + relayServerMsg = null; + apiServerMsg = null; + isInProgress = true; + }); + if (await validate()) { + if (id != id0) { + if (id0.isNotEmpty) { + await gFFI.userModel.logOut(); + } + bind.mainSetOption(key: "custom-rendezvous-server", value: id); + } + if (relay != relay0) { + bind.mainSetOption(key: "relay-server", value: relay); + } + if (key != key0) bind.mainSetOption(key: "key", value: key); + if (api != api0) { + bind.mainSetOption(key: "api-server", value: api); + } + close(); + } + setState(() { + isInProgress = false; + }); + }, + child: Text(translate('OK')), + ), + ], + ); + }); +} + +Future validateAsync(String value) async { + value = value.trim(); + if (value.isEmpty) { + return null; + } + final res = await bind.mainTestIfValidServer(server: value); + return res.isEmpty ? null : res; +} + class PasswordWidget extends StatefulWidget { PasswordWidget({Key? key, required this.controller, this.autoFocus = true}) : super(key: key); From cba6a3e0ee0bc2db41de8b2d4d94f934f09d6e71 Mon Sep 17 00:00:00 2001 From: csf Date: Wed, 21 Dec 2022 16:24:01 +0900 Subject: [PATCH 1237/2015] refactor to use ServerConfig --- flutter/lib/common.dart | 13 ++++- flutter/lib/mobile/pages/scan_page.dart | 3 +- flutter/lib/mobile/pages/settings_page.dart | 6 +- flutter/lib/mobile/widgets/dialog.dart | 64 ++++++++++----------- 4 files changed, 44 insertions(+), 42 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 8e8e50ae9..46ba90d66 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1525,7 +1525,9 @@ class ServerConfig { this.key = key?.trim() ?? ''; } - /// throw decoding failure + /// decode from shared string (from user shared or rustdesk-server generated) + /// also see [encode] + /// throw when decoding failure ServerConfig.decode(String msg) { final input = msg.split('').reversed.join(''); final bytes = base64Decode(base64.normalize(input)); @@ -1537,6 +1539,8 @@ class ServerConfig { key = json['key'] ?? ''; } + /// encode to shared string + /// also see [ServerConfig.decode] String encode() { Map config = {}; config['host'] = idServer.trim(); @@ -1548,4 +1552,11 @@ class ServerConfig { .reversed .join(); } + + /// from local options + ServerConfig.fromOptions(Map options) + : idServer = options['custom-rendezvous-server'] ?? "", + relayServer = options['relay-server'] ?? "", + apiServer = options['api-server'] ?? "", + key = options['key'] ?? ""; } diff --git a/flutter/lib/mobile/pages/scan_page.dart b/flutter/lib/mobile/pages/scan_page.dart index 32208f6a4..8778d78f7 100644 --- a/flutter/lib/mobile/pages/scan_page.dart +++ b/flutter/lib/mobile/pages/scan_page.dart @@ -140,8 +140,7 @@ class _ScanPageState extends State { try { final sc = ServerConfig.decode(data.substring(7)); Timer(Duration(milliseconds: 60), () { - showServerSettingsWithValue(sc.idServer, sc.relayServer, sc.key, - sc.apiServer, gFFI.dialogManager); + showServerSettingsWithValue(sc, gFFI.dialogManager); }); } catch (e) { showToast('Invalid QR code'); diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index 8c7cdb5c7..9637ecb40 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -391,11 +391,7 @@ class _SettingsState extends State with WidgetsBindingObserver { void showServerSettings(OverlayDialogManager dialogManager) async { Map options = jsonDecode(await bind.mainGetOptions()); - String id = options['custom-rendezvous-server'] ?? ""; - String relay = options['relay-server'] ?? ""; - String api = options['api-server'] ?? ""; - String key = options['key'] ?? ""; - showServerSettingsWithValue(id, relay, key, api, dialogManager); + showServerSettingsWithValue(ServerConfig.fromOptions(options), dialogManager); } void showLanguageSettings(OverlayDialogManager dialogManager) async { diff --git a/flutter/lib/mobile/widgets/dialog.dart b/flutter/lib/mobile/widgets/dialog.dart index 8cfaf6a02..d70902513 100644 --- a/flutter/lib/mobile/widgets/dialog.dart +++ b/flutter/lib/mobile/widgets/dialog.dart @@ -237,17 +237,16 @@ void wrongPasswordDialog(String id, OverlayDialogManager dialogManager) { ])); } -void showServerSettingsWithValue(String id, String relay, String key, - String api, OverlayDialogManager dialogManager) async { +void showServerSettingsWithValue( + ServerConfig serverConfig, OverlayDialogManager dialogManager) async { Map oldOptions = jsonDecode(await bind.mainGetOptions()); - String id0 = oldOptions['custom-rendezvous-server'] ?? ""; - String relay0 = oldOptions['relay-server'] ?? ""; - String api0 = oldOptions['api-server'] ?? ""; - String key0 = oldOptions['key'] ?? ""; + final oldCfg = ServerConfig.fromOptions(oldOptions); + var isInProgress = false; - final idController = TextEditingController(text: id); - final relayController = TextEditingController(text: relay); - final apiController = TextEditingController(text: api); + final idCtrl = TextEditingController(text: serverConfig.idServer); + final relayCtrl = TextEditingController(text: serverConfig.relayServer); + final apiCtrl = TextEditingController(text: serverConfig.apiServer); + final keyCtrl = TextEditingController(text: serverConfig.key); String? idServerMsg; String? relayServerMsg; @@ -255,21 +254,18 @@ void showServerSettingsWithValue(String id, String relay, String key, dialogManager.show((setState, close) { Future validate() async { - if (idController.text != id) { - final res = await validateAsync(idController.text); + if (idCtrl.text != oldCfg.idServer) { + final res = await validateAsync(idCtrl.text); setState(() => idServerMsg = res); if (idServerMsg != null) return false; - id = idController.text; } - if (relayController.text != relay) { - relayServerMsg = await validateAsync(relayController.text); + if (relayCtrl.text != oldCfg.relayServer) { + relayServerMsg = await validateAsync(relayCtrl.text); if (relayServerMsg != null) return false; - relay = relayController.text; } - if (apiController.text != relay) { - apiServerMsg = await validateAsync(apiController.text); + if (apiCtrl.text != oldCfg.apiServer) { + apiServerMsg = await validateAsync(apiCtrl.text); if (apiServerMsg != null) return false; - api = apiController.text; } return true; } @@ -281,7 +277,7 @@ void showServerSettingsWithValue(String id, String relay, String key, mainAxisSize: MainAxisSize.min, children: [ TextFormField( - controller: idController, + controller: idCtrl, decoration: InputDecoration( labelText: translate('ID Server'), errorText: idServerMsg), @@ -290,7 +286,7 @@ void showServerSettingsWithValue(String id, String relay, String key, (isAndroid ? [ TextFormField( - controller: relayController, + controller: relayCtrl, decoration: InputDecoration( labelText: translate('Relay Server'), errorText: relayServerMsg), @@ -299,7 +295,7 @@ void showServerSettingsWithValue(String id, String relay, String key, : []) + [ TextFormField( - controller: apiController, + controller: apiCtrl, decoration: InputDecoration( labelText: translate('API Server'), ), @@ -315,13 +311,10 @@ void showServerSettingsWithValue(String id, String relay, String key, }, ), TextFormField( - initialValue: key, + controller: keyCtrl, decoration: InputDecoration( labelText: 'Key', ), - onChanged: (String? value) { - if (value != null) key = value.trim(); - }, ), Offstage( offstage: !isInProgress, @@ -345,18 +338,21 @@ void showServerSettingsWithValue(String id, String relay, String key, isInProgress = true; }); if (await validate()) { - if (id != id0) { - if (id0.isNotEmpty) { + if (idCtrl.text != oldCfg.idServer) { + if (oldCfg.idServer.isNotEmpty) { await gFFI.userModel.logOut(); } - bind.mainSetOption(key: "custom-rendezvous-server", value: id); + bind.mainSetOption( + key: "custom-rendezvous-server", value: idCtrl.text); } - if (relay != relay0) { - bind.mainSetOption(key: "relay-server", value: relay); + if (relayCtrl.text != oldCfg.relayServer) { + bind.mainSetOption(key: "relay-server", value: relayCtrl.text); } - if (key != key0) bind.mainSetOption(key: "key", value: key); - if (api != api0) { - bind.mainSetOption(key: "api-server", value: api); + if (keyCtrl.text != oldCfg.key) { + bind.mainSetOption(key: "key", value: keyCtrl.text); + } + if (apiCtrl.text != oldCfg.apiServer) { + bind.mainSetOption(key: "api-server", value: apiCtrl.text); } close(); } @@ -429,7 +425,7 @@ class _PasswordWidgetState extends State { color: Theme.of(context).primaryColorDark, ), onPressed: () { - // Update the state i.e. toogle the state of passwordVisible variable + // Update the state i.e. toggle the state of passwordVisible variable setState(() { _passwordVisible = !_passwordVisible; }); From bbbaa88ebf9cd9393df017f2833664a14fb3db6a Mon Sep 17 00:00:00 2001 From: Agent-JY Date: Wed, 21 Dec 2022 10:14:11 +0100 Subject: [PATCH 1238/2015] Update de.rs --- src/lang/de.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lang/de.rs b/src/lang/de.rs index 3c04f2ec4..d5d75d90b 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -39,9 +39,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "ID ändern"), ("Website", "Webseite"), ("About", "Über"), - ("About RustDesk", ""), - ("Slogan_tip", ""), - ("Privacy Statement", ""), + ("About RustDesk", "Über RustDesk"), + ("Slogan_tip", "Mit Herzblut programmiert - in einer Welt, die im Chaos versinkt"), + ("Privacy Statement", "Datenschutz"), ("Mute", "Stummschalten"), ("Audio Input", "Audioeingang"), ("Enhancements", "Verbesserungen"), @@ -372,7 +372,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Write a message", "Nachricht schreiben"), ("Prompt", ""), //Aufforderung??? ("Please wait for confirmation of UAC...", "Bitte auf die Bestätigung des Nutzers warten..."), - ("elevated_foreground_window_tip", ""), + ("elevated_foreground_window_tip", "Das aktuell geöffnete Fenster des ferngesteuerten Computers benötigt höhere Rechte. Deshalb ist es derzeit nicht möglich, die Maus und die Tastatur zu verwenden. Bitten Sie den Nutzer, dessen Computer Sie fernsteuern, das Fenster zu minimieren oder die Rechte zu erhöhen. Um dieses Problem zunünftig zu vermeiden, wird empfohlen, die Software auf dem ferngesteuerten Computer zu installieren."), ("Disconnected", "Verbindung abgebrochen"), ("Other", "Weitere Einstellungen"), ("Confirm before closing multiple tabs", "Nachfragen, wenn mehrere Tabs geschlossen werden"), From 364387028734c1dc603d6ff2a7924c9cd54247e3 Mon Sep 17 00:00:00 2001 From: Sangha Lee Date: Wed, 21 Dec 2022 18:42:22 +0900 Subject: [PATCH 1239/2015] fix unsafe code --- libs/scrap/src/wayland/pipewire.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/libs/scrap/src/wayland/pipewire.rs b/libs/scrap/src/wayland/pipewire.rs index 8e3848862..d6ac70777 100644 --- a/libs/scrap/src/wayland/pipewire.rs +++ b/libs/scrap/src/wayland/pipewire.rs @@ -130,7 +130,7 @@ impl PipeWireRecorder { pub fn new(capturable: PipeWireCapturable) -> Result> { let pipeline = gst::Pipeline::new(None); - let src = gst::ElementFactory::make_with_name("pipewiresrc", None).unwrap(); + let src = gst::ElementFactory::make_with_name("pipewiresrc", None)?; src.set_property("fd", &capturable.fd.as_raw_fd()); src.set_property("path", &format!("{}", capturable.path)); src.set_property("keepalive_time", &1_000.as_raw_fd()); @@ -139,7 +139,7 @@ impl PipeWireRecorder { // see: https://gitlab.freedesktop.org/pipewire/pipewire/-/issues/982 src.set_property("always-copy", &true); - let sink = gst::ElementFactory::make_with_name("appsink", None).unwrap(); + let sink = gst::ElementFactory::make_with_name("appsink", None)?; sink.set_property("drop", &true); sink.set_property("max-buffers", &1u32); @@ -254,12 +254,18 @@ impl Recorder for PipeWireRecorder { let buf = if self.is_cropped { self.buffer_cropped.as_slice() } else { - self.buffer.as_ref().unwrap().as_slice() + self.buffer + .as_ref() + .ok_or("Failed to get buffer as ref")? + .as_slice() }; match self.pix_fmt.as_str() { "BGRx" => Ok(PixelProvider::BGR0(self.width, self.height, buf)), "RGBx" => Ok(PixelProvider::RGB0(self.width, self.height, buf)), - _ => unreachable!(), + _ => Err(Box::new(GStreamerError(format!( + "Unreachable! Unknown pix_fmt, {}", + &self.pix_fmt + )))), } } } From 9a203c4804133e1c777a9fbdcf58e24dcbfd8f2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Be=C3=A0?= Date: Wed, 21 Dec 2022 12:43:21 +0100 Subject: [PATCH 1240/2015] update ca.rs changed typo seguritat->seguretat --- src/lang/ca.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/ca.rs b/src/lang/ca.rs index fd9e7c2c9..a8f39a23f 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -334,7 +334,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale original", "Escala original"), ("Scale adaptive", "Escala adaptativa"), ("General", ""), - ("Security", "Seguritat"), + ("Security", "Seguretat"), ("Account", "Compte"), ("Theme", "Tema"), ("Dark Theme", "Tema Fosc"), @@ -342,7 +342,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Light", "Clar"), ("Follow System", "Tema del sistema"), ("Enable hardware codec", "Habilitar còdec per hardware"), - ("Unlock Security Settings", "Desbloquejar ajustaments de seguritat"), + ("Unlock Security Settings", "Desbloquejar ajustaments de seguretat"), ("Enable Audio", "Habilitar àudio"), ("Unlock Network Settings", "Desbloquejar Ajustaments de Xarxa"), ("Server", "Servidor"), From 92f31d30201309ba75ca68b516c34ab49c6a0343 Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 21 Dec 2022 16:38:46 +0800 Subject: [PATCH 1241/2015] fix win-linux IntlBackslash Signed-off-by: fufesou --- Cargo.lock | 2 +- flutter/lib/desktop/widgets/remote_menubar.dart | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 336fc4b50..21defd4d0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4249,7 +4249,7 @@ dependencies = [ [[package]] name = "rdev" version = "0.5.0-2" -source = "git+https://github.com/asur4s/rdev#18bb9dd64563fc9761005bb39ff830e6402e326e" +source = "git+https://github.com/asur4s/rdev#81aa6559e931fed914e0d38edfd98cbe4bc908c1" dependencies = [ "cocoa", "core-foundation 0.9.3", diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index a7d7b6dc6..0c2a6a971 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -827,7 +827,7 @@ class _RemoteMenubarState extends State { qualityInitValue = qualityMaxValue; } final RxDouble qualitySliderValue = RxDouble(qualityInitValue); - final debouncerQuanlity = Debouncer( + final debouncerQuality = Debouncer( Duration(milliseconds: 1000), onChanged: (double v) { setCustomValues(quality: v); @@ -843,7 +843,7 @@ class _RemoteMenubarState extends State { divisions: 90, onChanged: (double value) { qualitySliderValue.value = value; - debouncerQuanlity.value = value; + debouncerQuality.value = value; }, ), SizedBox( From 2ae38c93f08d09e0ad02597c20090ae7b7446bfa Mon Sep 17 00:00:00 2001 From: Kingtous Date: Wed, 21 Dec 2022 22:39:30 +0800 Subject: [PATCH 1242/2015] opt: use whole focus instead to trigger session enter or leave --- flutter/lib/common/widgets/remote_input.dart | 25 +++++++++++----- flutter/lib/desktop/pages/remote_page.dart | 31 +++++++++++++++----- 2 files changed, 41 insertions(+), 15 deletions(-) diff --git a/flutter/lib/common/widgets/remote_input.dart b/flutter/lib/common/widgets/remote_input.dart index 3a79d24fb..017850cf5 100644 --- a/flutter/lib/common/widgets/remote_input.dart +++ b/flutter/lib/common/widgets/remote_input.dart @@ -9,11 +9,12 @@ class RawKeyFocusScope extends StatelessWidget { final InputModel inputModel; final Widget child; - RawKeyFocusScope( - {this.focusNode, - this.onFocusChange, - required this.inputModel, - required this.child}); + RawKeyFocusScope({ + this.focusNode, + this.onFocusChange, + required this.inputModel, + required this.child, + }); @override Widget build(BuildContext context) { @@ -35,11 +36,15 @@ class RawPointerMouseRegion extends StatelessWidget { final MouseCursor? cursor; final PointerEnterEventListener? onEnter; final PointerExitEventListener? onExit; + final PointerDownEventListener? onPointerDown; + final PointerUpEventListener? onPointerUp; RawPointerMouseRegion( {this.onEnter, this.onExit, this.cursor, + this.onPointerDown, + this.onPointerUp, required this.inputModel, required this.child}); @@ -47,8 +52,14 @@ class RawPointerMouseRegion extends StatelessWidget { Widget build(BuildContext context) { return Listener( onPointerHover: inputModel.onPointHoverImage, - onPointerDown: inputModel.onPointDownImage, - onPointerUp: inputModel.onPointUpImage, + onPointerDown: (evt) { + onPointerDown?.call(evt); + inputModel.onPointDownImage(evt); + }, + onPointerUp: (evt) { + onPointerUp?.call(evt); + inputModel.onPointUpImage(evt); + }, onPointerMove: inputModel.onPointMoveImage, onPointerSignal: inputModel.onPointerSignalImage, /* diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 4061fb74d..8fd4ee07c 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -52,6 +52,7 @@ class _RemotePageState extends State with AutomaticKeepAliveClientMixin, MultiWindowListener { Timer? _timer; String keyboardMode = "legacy"; + bool _isWindowBlur = false; final _cursorOverImage = false.obs; late RxBool _showRemoteCursor; late RxBool _zoomCursor; @@ -59,7 +60,6 @@ class _RemotePageState extends State late RxBool _keyboardEnabled; final FocusNode _rawKeyFocusNode = FocusNode(debugLabel: "rawkeyFocusNode"); - var _imageFocused = false; Function(bool)? _onEnterOrLeaveImage4Menubar; @@ -104,7 +104,6 @@ class _RemotePageState extends State if (!Platform.isLinux) { Wakelock.enable(); } - _rawKeyFocusNode.requestFocus(); _ffi.ffiModel.updateEventListener(widget.id); _ffi.qualityMonitorModel.checkShowQualityMonitor(widget.id); // Session option should be set after models.dart/FFI.start @@ -129,11 +128,18 @@ class _RemotePageState extends State @override void onWindowBlur() { super.onWindowBlur(); + _isWindowBlur = true; // unfocus the key focus when the whole window is lost focus, // and let OS to handle events instead. _rawKeyFocusNode.unfocus(); } + @override + void onWindowFocus() { + super.onWindowFocus(); + _isWindowBlur = false; + } + @override void dispose() { debugPrint("REMOTE PAGE dispose ${widget.id}"); @@ -166,9 +172,16 @@ class _RemotePageState extends State color: Colors.black, child: RawKeyFocusScope( focusNode: _rawKeyFocusNode, - onFocusChange: (bool v) { - _imageFocused = v; - if (_imageFocused) { + onFocusChange: (bool imageFocused) { + debugPrint( + "onFocusChange(window active:${!_isWindowBlur}) $imageFocused"); + if (_isWindowBlur) { + imageFocused = false; + Future.delayed(Duration.zero, () { + _rawKeyFocusNode.unfocus(); + }); + } + if (imageFocused) { _ffi.inputModel.enterOrLeave(true); } else { _ffi.inputModel.enterOrLeave(false); @@ -199,9 +212,6 @@ class _RemotePageState extends State } void enterView(PointerEnterEvent evt) { - if (!_imageFocused) { - _rawKeyFocusNode.requestFocus(); - } _cursorOverImage.value = true; _firstEnterImage.value = true; if (_onEnterOrLeaveImage4Menubar != null) { @@ -244,6 +254,11 @@ class _RemotePageState extends State listenerBuilder: (child) => RawPointerMouseRegion( onEnter: enterView, onExit: leaveView, + onPointerDown: (event) { + if (!_rawKeyFocusNode.hasFocus) { + _rawKeyFocusNode.requestFocus(); + } + }, inputModel: _ffi.inputModel, child: child, ), From 9c24117b13abdffdc104465723cff95d0df3a8dc Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Thu, 22 Dec 2022 12:22:26 +0800 Subject: [PATCH 1243/2015] Revert "Implement RGB0 pixel format " --- Cargo.lock | 275 ++++++++++++++++++--------- libs/scrap/Cargo.toml | 6 +- libs/scrap/src/common/wayland.rs | 6 - libs/scrap/src/wayland/capturable.rs | 2 - libs/scrap/src/wayland/pipewire.rs | 71 +++---- 5 files changed, 215 insertions(+), 145 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 21defd4d0..5a7d410fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -295,7 +295,7 @@ dependencies = [ "glib-sys 0.15.10", "gobject-sys 0.15.10", "libc", - "system-deps", + "system-deps 6.0.3", ] [[package]] @@ -313,12 +313,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "065374052e7df7ee4047b1160cca5e1467a12351a40b3da123c870ba0b8eda2a" -[[package]] -name = "atomic_refcell" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73b5e5f48b927f04e952dedc932f31995a65a0bf65ec971c74436e51bf6e970d" - [[package]] name = "atty" version = "0.2.14" @@ -525,7 +519,7 @@ checksum = "3c55d429bef56ac9172d25fecb85dc8068307d17acd74b377866b7a1ef25d3c8" dependencies = [ "glib-sys 0.15.10", "libc", - "system-deps", + "system-deps 6.0.3", ] [[package]] @@ -2002,7 +1996,7 @@ dependencies = [ "glib-sys 0.15.10", "gobject-sys 0.15.10", "libc", - "system-deps", + "system-deps 6.0.3", ] [[package]] @@ -2019,7 +2013,7 @@ dependencies = [ "libc", "pango-sys", "pkg-config", - "system-deps", + "system-deps 6.0.3", ] [[package]] @@ -2085,7 +2079,7 @@ dependencies = [ "glib-sys 0.15.10", "gobject-sys 0.15.10", "libc", - "system-deps", + "system-deps 6.0.3", "winapi 0.3.9", ] @@ -2098,10 +2092,29 @@ dependencies = [ "glib-sys 0.16.3", "gobject-sys 0.16.3", "libc", - "system-deps", + "system-deps 6.0.3", "winapi 0.3.9", ] +[[package]] +name = "glib" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c685013b7515e668f1b57a165b009d4d28cb139a8a989bbd699c10dad29d0c5" +dependencies = [ + "bitflags", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "glib-macros 0.10.1", + "glib-sys 0.10.1", + "gobject-sys 0.10.0", + "libc", + "once_cell", +] + [[package]] name = "glib" version = "0.15.12" @@ -2144,6 +2157,22 @@ dependencies = [ "thiserror", ] +[[package]] +name = "glib-macros" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41486a26d1366a8032b160b59065a59fb528530a46a49f627e7048fb8c064039" +dependencies = [ + "anyhow", + "heck 0.3.3", + "itertools", + "proc-macro-crate 0.1.5", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "glib-macros" version = "0.15.11" @@ -2152,7 +2181,7 @@ checksum = "25a68131a662b04931e71891fb14aaf65ee4b44d08e8abc10f49e77418c86c64" dependencies = [ "anyhow", "heck 0.4.0", - "proc-macro-crate", + "proc-macro-crate 1.2.1", "proc-macro-error", "proc-macro2", "quote", @@ -2167,13 +2196,23 @@ checksum = "e084807350b01348b6d9dbabb724d1a0bb987f47a2c85de200e98e12e30733bf" dependencies = [ "anyhow", "heck 0.4.0", - "proc-macro-crate", + "proc-macro-crate 1.2.1", "proc-macro-error", "proc-macro2", "quote", "syn", ] +[[package]] +name = "glib-sys" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e9b997a66e9a23d073f2b1abb4dbfc3925e0b8952f67efd8d9b6e168e4cdc1" +dependencies = [ + "libc", + "system-deps 1.3.2", +] + [[package]] name = "glib-sys" version = "0.15.10" @@ -2181,7 +2220,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef4b192f8e65e9cf76cbf4ea71fa8e3be4a0e18ffe3d68b8da6836974cc5bad4" dependencies = [ "libc", - "system-deps", + "system-deps 6.0.3", ] [[package]] @@ -2191,7 +2230,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61a4f46316d06bfa33a7ac22df6f0524c8be58e3db2d9ca99ccb1f357b62a65" dependencies = [ "libc", - "system-deps", + "system-deps 6.0.3", ] [[package]] @@ -2200,6 +2239,17 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" +[[package]] +name = "gobject-sys" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "952133b60c318a62bf82ee75b93acc7e84028a093e06b9e27981c2b6fe68218c" +dependencies = [ + "glib-sys 0.10.1", + "libc", + "system-deps 1.3.2", +] + [[package]] name = "gobject-sys" version = "0.15.10" @@ -2208,7 +2258,7 @@ checksum = "0d57ce44246becd17153bd035ab4d32cfee096a657fc01f2231c9278378d1e0a" dependencies = [ "glib-sys 0.15.10", "libc", - "system-deps", + "system-deps 6.0.3", ] [[package]] @@ -2219,28 +2269,28 @@ checksum = "3520bb9c07ae2a12c7f2fbb24d4efc11231c8146a86956413fb1a79bb760a0f1" dependencies = [ "glib-sys 0.16.3", "libc", - "system-deps", + "system-deps 6.0.3", ] [[package]] name = "gstreamer" -version = "0.19.4" +version = "0.16.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87a7570ad1d3c1cbf64561ada514fe0c03cf834f2076b85ffc616756c840b665" +checksum = "9ff5d0f7ff308ae37e6eb47b6ded17785bdea06e438a708cd09e0288c1862f33" dependencies = [ "bitflags", "cfg-if 1.0.0", "futures-channel", "futures-core", "futures-util", - "glib 0.16.5", + "glib 0.10.3", + "glib-sys 0.10.1", + "gobject-sys 0.10.0", "gstreamer-sys", "libc", "muldiv", - "num-integer", - "num-rational 0.4.1", + "num-rational", "once_cell", - "option-operations", "paste", "pretty-hex", "thiserror", @@ -2248,86 +2298,94 @@ dependencies = [ [[package]] name = "gstreamer-app" -version = "0.19.2" +version = "0.16.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c45202b4d565034d4fe5577c990d3a99eaf0c2bfd2cb3f73f70db14d58e0208c" +checksum = "cc80888271338c3ede875d8cafc452eb207476ff5539dcbe0018a8f5b827af0e" dependencies = [ "bitflags", "futures-core", "futures-sink", - "glib 0.16.5", + "glib 0.10.3", + "glib-sys 0.10.1", + "gobject-sys 0.10.0", "gstreamer", "gstreamer-app-sys", "gstreamer-base", + "gstreamer-sys", "libc", "once_cell", ] [[package]] name = "gstreamer-app-sys" -version = "0.19.2" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29b0159da8dd0672c1a5507445c70c8dc483abfb63a0295cabaedd396f1d67d1" +checksum = "813f64275c9e7b33b828b9efcf9dfa64b95996766d4de996e84363ac65b87e3d" dependencies = [ - "glib-sys 0.16.3", + "glib-sys 0.10.1", "gstreamer-base-sys", "gstreamer-sys", "libc", - "system-deps", + "system-deps 1.3.2", ] [[package]] name = "gstreamer-base" -version = "0.19.3" +version = "0.16.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a61a299f9ea2ca892b43e2e428b86c679875e95ba23f8ae06fd730308df630f0" +checksum = "bafd01c56f59cb10f4b5a10f97bb4bdf8c2b2784ae5b04da7e2d400cf6e6afcf" dependencies = [ - "atomic_refcell", "bitflags", - "cfg-if 1.0.0", - "glib 0.16.5", + "glib 0.10.3", + "glib-sys 0.10.1", + "gobject-sys 0.10.0", "gstreamer", "gstreamer-base-sys", + "gstreamer-sys", "libc", ] [[package]] name = "gstreamer-base-sys" -version = "0.19.3" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc3c4476e1503ae245c89fbe20060c30ec6ade5f44620bcc402cbc70a3911a1" +checksum = "a4b7b6dc2d6e160a1ae28612f602bd500b3fa474ce90bf6bb2f08072682beef5" dependencies = [ - "glib-sys 0.16.3", - "gobject-sys 0.16.3", + "glib-sys 0.10.1", + "gobject-sys 0.10.0", "gstreamer-sys", "libc", - "system-deps", + "system-deps 1.3.2", ] [[package]] name = "gstreamer-sys" -version = "0.19.4" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "545f52ad8a480732cc4290fd65dfe42952c8ae374fe581831ba15981fedf18a4" +checksum = "fc1f154082d01af5718c5f8a8eb4f565a4ea5586ad8833a8fc2c2aa6844b601d" dependencies = [ - "glib-sys 0.16.3", - "gobject-sys 0.16.3", + "glib-sys 0.10.1", + "gobject-sys 0.10.0", "libc", - "system-deps", + "system-deps 1.3.2", ] [[package]] name = "gstreamer-video" -version = "0.19.4" +version = "0.16.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e99623fb99436c4b2da66ae94b25881c94db5144afc1bd7c84cee5cabb72f18" +checksum = "f7bbb1485d87469849ec45c08e03c2f280d3ea20ff3c439d03185be54e3ce98e" dependencies = [ "bitflags", - "cfg-if 1.0.0", "futures-channel", - "glib 0.16.5", + "futures-util", + "glib 0.10.3", + "glib-sys 0.10.1", + "gobject-sys 0.10.0", "gstreamer", "gstreamer-base", + "gstreamer-base-sys", + "gstreamer-sys", "gstreamer-video-sys", "libc", "once_cell", @@ -2335,16 +2393,16 @@ dependencies = [ [[package]] name = "gstreamer-video-sys" -version = "0.19.4" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9206e9df0ed84824bfe4cc13e3359154ad7624221c7d3d6242585db3f19a15d9" +checksum = "92347e46438007d6a2386302125f62cb9df6769cdacb931af5c0f12c1ee21de4" dependencies = [ - "glib-sys 0.16.3", - "gobject-sys 0.16.3", + "glib-sys 0.10.1", + "gobject-sys 0.10.0", "gstreamer-base-sys", "gstreamer-sys", "libc", - "system-deps", + "system-deps 1.3.2", ] [[package]] @@ -2385,7 +2443,7 @@ dependencies = [ "gobject-sys 0.15.10", "libc", "pango-sys", - "system-deps", + "system-deps 6.0.3", ] [[package]] @@ -2395,7 +2453,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24f518afe90c23fba585b2d7697856f9e6a7bbc62f65588035e66f6afb01a2e9" dependencies = [ "anyhow", - "proc-macro-crate", + "proc-macro-crate 1.2.1", "proc-macro-error", "proc-macro2", "quote", @@ -2642,7 +2700,7 @@ dependencies = [ "byteorder", "color_quant", "num-iter", - "num-rational 0.3.2", + "num-rational", "num-traits 0.2.15", "png", "tiff", @@ -2732,6 +2790,15 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec947b7a4ce12e3b87e353abae7ce124d025b6c7d6c5aea5cc0bcf92e9510ded" +[[package]] +name = "itertools" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "0.3.4" @@ -3185,9 +3252,9 @@ dependencies = [ [[package]] name = "muldiv" -version = "1.0.1" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "956787520e75e9bd233246045d19f42fb73242759cc57fba9611d940ae96d4b0" +checksum = "0419348c027fa7be448d2ae7ea0e4e04c2334c31dc4e74ab29f00a2a7ca69204" [[package]] name = "ndk" @@ -3272,7 +3339,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0df7ac00c4672f9d5aece54ee3347520b7e20f158656c7db2e6de01902eb7a6c" dependencies = [ "darling", - "proc-macro-crate", + "proc-macro-crate 1.2.1", "proc-macro2", "quote", "syn", @@ -3459,17 +3526,6 @@ 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" @@ -3513,7 +3569,7 @@ version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b0498641e53dd6ac1a4f22547548caa6864cc4933784319cd1775271c5a46ce" dependencies = [ - "proc-macro-crate", + "proc-macro-crate 1.2.1", "proc-macro2", "quote", "syn", @@ -3601,15 +3657,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" -[[package]] -name = "option-operations" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c26d27bb1aeab65138e4bf7666045169d1717febcc9ff870166be8348b223d0" -dependencies = [ - "paste", -] - [[package]] name = "ordered-multimap" version = "0.4.3" @@ -3664,7 +3711,7 @@ dependencies = [ "glib-sys 0.15.10", "gobject-sys 0.15.10", "libc", - "system-deps", + "system-deps 6.0.3", ] [[package]] @@ -3880,9 +3927,9 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "pretty-hex" -version = "0.3.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6fa0831dd7cc608c38a5e323422a0077678fa5744aa2be4ad91c4ece8eec8d5" +checksum = "bc5c99d529f0d30937f6f4b8a86d988047327bb88d04d2c4afc356de74722131" [[package]] name = "primal-check" @@ -3893,6 +3940,15 @@ dependencies = [ "num-integer", ] +[[package]] +name = "proc-macro-crate" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" +dependencies = [ + "toml", +] + [[package]] name = "proc-macro-crate" version = "1.2.1" @@ -4262,8 +4318,8 @@ dependencies = [ "libc", "log", "mio 0.8.5", - "strum", - "strum_macros", + "strum 0.24.1", + "strum_macros 0.24.3", "widestring 1.0.2", "winapi 0.3.9", "x11 2.20.1", @@ -5118,12 +5174,30 @@ dependencies = [ "syn", ] +[[package]] +name = "strum" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57bd81eb48f4c437cadc685403cad539345bf703d78e63707418431cecd4522b" + [[package]] name = "strum" version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" +[[package]] +name = "strum_macros" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87c85aa3f8ea653bfd3ddf25f7ee357ee4d204731f6aa9ad04002306f6e2774c" +dependencies = [ + "heck 0.3.3", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "strum_macros" version = "0.24.3" @@ -5209,6 +5283,21 @@ dependencies = [ "libc", ] +[[package]] +name = "system-deps" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f3ecc17269a19353b3558b313bba738b25d82993e30d62a18406a24aba4649b" +dependencies = [ + "heck 0.3.3", + "pkg-config", + "strum 0.18.0", + "strum_macros 0.18.0", + "thiserror", + "toml", + "version-compare 0.0.10", +] + [[package]] name = "system-deps" version = "6.0.3" @@ -5219,7 +5308,7 @@ dependencies = [ "heck 0.4.0", "pkg-config", "toml", - "version-compare", + "version-compare 0.1.1", ] [[package]] @@ -5650,6 +5739,12 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" +[[package]] +name = "version-compare" +version = "0.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d63556a25bae6ea31b52e640d7c41d1ab27faba4ccb600013837a3d0b3994ca1" + [[package]] name = "version-compare" version = "0.1.1" @@ -6476,7 +6571,7 @@ version = "3.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45066039ebf3330820e495e854f8b312abb68f0a39e97972d092bd72e8bb3e8e" dependencies = [ - "proc-macro-crate", + "proc-macro-crate 1.2.1", "proc-macro2", "quote", "regex", @@ -6543,7 +6638,7 @@ version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "155247a5d1ab55e335421c104ccd95d64f17cebbd02f50cdbc1c33385f9c4d81" dependencies = [ - "proc-macro-crate", + "proc-macro-crate 1.2.1", "proc-macro2", "quote", "syn", diff --git a/libs/scrap/Cargo.toml b/libs/scrap/Cargo.toml index de6a6f2bf..e2eb43177 100644 --- a/libs/scrap/Cargo.toml +++ b/libs/scrap/Cargo.toml @@ -48,9 +48,9 @@ bindgen = "0.59" [target.'cfg(target_os = "linux")'.dependencies] dbus = { version = "0.9", optional = true } tracing = { version = "0.1", optional = true } -gstreamer = { version = "0.19", optional = true } -gstreamer-app = { version = "0.19", features = ["v1_16"], optional = true } -gstreamer-video = { version = "0.19", optional = true } +gstreamer = { version = "0.16", optional = true } +gstreamer-app = { version = "0.16", features = ["v1_10"], optional = true } +gstreamer-video = { version = "0.16", optional = true } [target.'cfg(any(target_os = "windows", target_os = "linux"))'.dependencies] hwcodec = { git = "https://github.com/21pages/hwcodec", optional = true } diff --git a/libs/scrap/src/common/wayland.rs b/libs/scrap/src/common/wayland.rs index 6e89568e1..2593e56fe 100644 --- a/libs/scrap/src/common/wayland.rs +++ b/libs/scrap/src/common/wayland.rs @@ -50,12 +50,6 @@ impl TraitCapturer for Capturer { } else { x })), - PixelProvider::RGB0(w, h, x) => Ok(Frame(if self.2 { - crate::common::rgba_to_i420(w as _, h as _, &x, &mut self.3); - &self.3[..] - } else { - x - })), PixelProvider::NONE => Err(std::io::ErrorKind::WouldBlock.into()), _ => Err(map_err("Invalid data")), } diff --git a/libs/scrap/src/wayland/capturable.rs b/libs/scrap/src/wayland/capturable.rs index 61f80ecbf..05a5ec71d 100644 --- a/libs/scrap/src/wayland/capturable.rs +++ b/libs/scrap/src/wayland/capturable.rs @@ -4,7 +4,6 @@ use std::error::Error; pub enum PixelProvider<'a> { // 8 bits per color RGB(usize, usize, &'a [u8]), - RGB0(usize, usize, &'a [u8]), BGR0(usize, usize, &'a [u8]), // width, height, stride BGR0S(usize, usize, usize, &'a [u8]), @@ -15,7 +14,6 @@ impl<'a> PixelProvider<'a> { pub fn size(&self) -> (usize, usize) { match self { PixelProvider::RGB(w, h, _) => (*w, *h), - PixelProvider::RGB0(w, h, _) => (*w, *h), PixelProvider::BGR0(w, h, _) => (*w, *h), PixelProvider::BGR0S(w, h, _, _) => (*w, *h), PixelProvider::NONE => (0, 0), diff --git a/libs/scrap/src/wayland/pipewire.rs b/libs/scrap/src/wayland/pipewire.rs index d6ac70777..a7b4c1357 100644 --- a/libs/scrap/src/wayland/pipewire.rs +++ b/libs/scrap/src/wayland/pipewire.rs @@ -117,7 +117,6 @@ impl Capturable for PipeWireCapturable { pub struct PipeWireRecorder { buffer: Option>, buffer_cropped: Vec, - pix_fmt: String, is_cropped: bool, pipeline: gst::Pipeline, appsink: AppSink, @@ -130,42 +129,34 @@ impl PipeWireRecorder { pub fn new(capturable: PipeWireCapturable) -> Result> { let pipeline = gst::Pipeline::new(None); - let src = gst::ElementFactory::make_with_name("pipewiresrc", None)?; - src.set_property("fd", &capturable.fd.as_raw_fd()); - src.set_property("path", &format!("{}", capturable.path)); - src.set_property("keepalive_time", &1_000.as_raw_fd()); + let src = gst::ElementFactory::make("pipewiresrc", None)?; + src.set_property("fd", &capturable.fd.as_raw_fd())?; + src.set_property("path", &format!("{}", capturable.path))?; + src.set_property("keepalive_time", &1_000.as_raw_fd())?; // For some reason pipewire blocks on destruction of AppSink if this is not set to true, // see: https://gitlab.freedesktop.org/pipewire/pipewire/-/issues/982 - src.set_property("always-copy", &true); + src.set_property("always-copy", &true)?; - let sink = gst::ElementFactory::make_with_name("appsink", None)?; - sink.set_property("drop", &true); - sink.set_property("max-buffers", &1u32); + let sink = gst::ElementFactory::make("appsink", None)?; + sink.set_property("drop", &true)?; + sink.set_property("max-buffers", &1u32)?; pipeline.add_many(&[&src, &sink])?; src.link(&sink)?; - let appsink = sink .dynamic_cast::() .map_err(|_| GStreamerError("Sink element is expected to be an appsink!".into()))?; - let mut caps = gst::Caps::new_empty(); - caps.merge_structure(gst::structure::Structure::new( + appsink.set_caps(Some(&gst::Caps::new_simple( "video/x-raw", &[("format", &"BGRx")], - )); - caps.merge_structure(gst::structure::Structure::new( - "video/x-raw", - &[("format", &"RGBx")], - )); - appsink.set_caps(Some(&caps)); + ))); pipeline.set_state(gst::State::Playing)?; Ok(Self { pipeline, appsink, buffer: None, - pix_fmt: "".into(), width: 0, height: 0, buffer_cropped: vec![], @@ -182,21 +173,20 @@ impl Recorder for PipeWireRecorder { .try_pull_sample(gst::ClockTime::from_mseconds(timeout_ms)) { let cap = sample - .caps() + .get_caps() .ok_or("Failed get caps")? - .structure(0) + .get_structure(0) .ok_or("Failed to get structure")?; - let w: i32 = cap.value("width")?.get()?; - let h: i32 = cap.value("height")?.get()?; - self.pix_fmt = cap.value("format")?.get()?; + let w: i32 = cap.get_value("width")?.get_some()?; + let h: i32 = cap.get_value("height")?.get_some()?; let w = w as usize; let h = h as usize; let buf = sample - .buffer_owned() + .get_buffer_owned() .ok_or_else(|| GStreamerError("Failed to get owned buffer.".into()))?; let mut crop = buf - .meta::() - .map(|m| m.rect()); + .get_meta::() + .map(|m| m.get_rect()); // only crop if necessary if Some((0, 0, w as u32, h as u32)) == crop { crop = None; @@ -207,7 +197,7 @@ impl Recorder for PipeWireRecorder { if let Err(..) = crate::would_block_if_equal(&mut self.saved_raw_data, buf.as_slice()) { return Ok(PixelProvider::NONE); } - let buf_size = buf.size(); + let buf_size = buf.get_size(); // BGRx is 4 bytes per pixel if buf_size != (w * h * 4) { // for some reason the width and height of the caps do not guarantee correct buffer @@ -251,22 +241,15 @@ impl Recorder for PipeWireRecorder { if self.buffer.is_none() { return Err(Box::new(GStreamerError("No buffer available!".into()))); } - let buf = if self.is_cropped { - self.buffer_cropped.as_slice() - } else { - self.buffer - .as_ref() - .ok_or("Failed to get buffer as ref")? - .as_slice() - }; - match self.pix_fmt.as_str() { - "BGRx" => Ok(PixelProvider::BGR0(self.width, self.height, buf)), - "RGBx" => Ok(PixelProvider::RGB0(self.width, self.height, buf)), - _ => Err(Box::new(GStreamerError(format!( - "Unreachable! Unknown pix_fmt, {}", - &self.pix_fmt - )))), - } + Ok(PixelProvider::BGR0( + self.width, + self.height, + if self.is_cropped { + self.buffer_cropped.as_slice() + } else { + self.buffer.as_ref().unwrap().as_slice() + }, + )) } } From 0819a3d8eafb5f4da7963ecc279b191ad2a34507 Mon Sep 17 00:00:00 2001 From: Sangha Lee Date: Thu, 22 Dec 2022 18:54:27 +0900 Subject: [PATCH 1244/2015] Revert "Merge pull request #2628 from rustdesk/revert-2612-master" This reverts commit e50882a660d425f53169f1138fd54c9b3abf3176, reversing changes made to 7f006102b5a018575cc3fc1a9c0e458dbb8a8993. --- Cargo.lock | 275 +++++++++------------------ libs/scrap/Cargo.toml | 6 +- libs/scrap/src/common/wayland.rs | 6 + libs/scrap/src/wayland/capturable.rs | 2 + libs/scrap/src/wayland/pipewire.rs | 71 ++++--- 5 files changed, 145 insertions(+), 215 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5a7d410fc..21defd4d0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -295,7 +295,7 @@ dependencies = [ "glib-sys 0.15.10", "gobject-sys 0.15.10", "libc", - "system-deps 6.0.3", + "system-deps", ] [[package]] @@ -313,6 +313,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "065374052e7df7ee4047b1160cca5e1467a12351a40b3da123c870ba0b8eda2a" +[[package]] +name = "atomic_refcell" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73b5e5f48b927f04e952dedc932f31995a65a0bf65ec971c74436e51bf6e970d" + [[package]] name = "atty" version = "0.2.14" @@ -519,7 +525,7 @@ checksum = "3c55d429bef56ac9172d25fecb85dc8068307d17acd74b377866b7a1ef25d3c8" dependencies = [ "glib-sys 0.15.10", "libc", - "system-deps 6.0.3", + "system-deps", ] [[package]] @@ -1996,7 +2002,7 @@ dependencies = [ "glib-sys 0.15.10", "gobject-sys 0.15.10", "libc", - "system-deps 6.0.3", + "system-deps", ] [[package]] @@ -2013,7 +2019,7 @@ dependencies = [ "libc", "pango-sys", "pkg-config", - "system-deps 6.0.3", + "system-deps", ] [[package]] @@ -2079,7 +2085,7 @@ dependencies = [ "glib-sys 0.15.10", "gobject-sys 0.15.10", "libc", - "system-deps 6.0.3", + "system-deps", "winapi 0.3.9", ] @@ -2092,29 +2098,10 @@ dependencies = [ "glib-sys 0.16.3", "gobject-sys 0.16.3", "libc", - "system-deps 6.0.3", + "system-deps", "winapi 0.3.9", ] -[[package]] -name = "glib" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c685013b7515e668f1b57a165b009d4d28cb139a8a989bbd699c10dad29d0c5" -dependencies = [ - "bitflags", - "futures-channel", - "futures-core", - "futures-executor", - "futures-task", - "futures-util", - "glib-macros 0.10.1", - "glib-sys 0.10.1", - "gobject-sys 0.10.0", - "libc", - "once_cell", -] - [[package]] name = "glib" version = "0.15.12" @@ -2157,22 +2144,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "glib-macros" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41486a26d1366a8032b160b59065a59fb528530a46a49f627e7048fb8c064039" -dependencies = [ - "anyhow", - "heck 0.3.3", - "itertools", - "proc-macro-crate 0.1.5", - "proc-macro-error", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "glib-macros" version = "0.15.11" @@ -2181,7 +2152,7 @@ checksum = "25a68131a662b04931e71891fb14aaf65ee4b44d08e8abc10f49e77418c86c64" dependencies = [ "anyhow", "heck 0.4.0", - "proc-macro-crate 1.2.1", + "proc-macro-crate", "proc-macro-error", "proc-macro2", "quote", @@ -2196,23 +2167,13 @@ checksum = "e084807350b01348b6d9dbabb724d1a0bb987f47a2c85de200e98e12e30733bf" dependencies = [ "anyhow", "heck 0.4.0", - "proc-macro-crate 1.2.1", + "proc-macro-crate", "proc-macro-error", "proc-macro2", "quote", "syn", ] -[[package]] -name = "glib-sys" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7e9b997a66e9a23d073f2b1abb4dbfc3925e0b8952f67efd8d9b6e168e4cdc1" -dependencies = [ - "libc", - "system-deps 1.3.2", -] - [[package]] name = "glib-sys" version = "0.15.10" @@ -2220,7 +2181,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef4b192f8e65e9cf76cbf4ea71fa8e3be4a0e18ffe3d68b8da6836974cc5bad4" dependencies = [ "libc", - "system-deps 6.0.3", + "system-deps", ] [[package]] @@ -2230,7 +2191,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61a4f46316d06bfa33a7ac22df6f0524c8be58e3db2d9ca99ccb1f357b62a65" dependencies = [ "libc", - "system-deps 6.0.3", + "system-deps", ] [[package]] @@ -2239,17 +2200,6 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" -[[package]] -name = "gobject-sys" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "952133b60c318a62bf82ee75b93acc7e84028a093e06b9e27981c2b6fe68218c" -dependencies = [ - "glib-sys 0.10.1", - "libc", - "system-deps 1.3.2", -] - [[package]] name = "gobject-sys" version = "0.15.10" @@ -2258,7 +2208,7 @@ checksum = "0d57ce44246becd17153bd035ab4d32cfee096a657fc01f2231c9278378d1e0a" dependencies = [ "glib-sys 0.15.10", "libc", - "system-deps 6.0.3", + "system-deps", ] [[package]] @@ -2269,28 +2219,28 @@ checksum = "3520bb9c07ae2a12c7f2fbb24d4efc11231c8146a86956413fb1a79bb760a0f1" dependencies = [ "glib-sys 0.16.3", "libc", - "system-deps 6.0.3", + "system-deps", ] [[package]] name = "gstreamer" -version = "0.16.7" +version = "0.19.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ff5d0f7ff308ae37e6eb47b6ded17785bdea06e438a708cd09e0288c1862f33" +checksum = "87a7570ad1d3c1cbf64561ada514fe0c03cf834f2076b85ffc616756c840b665" dependencies = [ "bitflags", "cfg-if 1.0.0", "futures-channel", "futures-core", "futures-util", - "glib 0.10.3", - "glib-sys 0.10.1", - "gobject-sys 0.10.0", + "glib 0.16.5", "gstreamer-sys", "libc", "muldiv", - "num-rational", + "num-integer", + "num-rational 0.4.1", "once_cell", + "option-operations", "paste", "pretty-hex", "thiserror", @@ -2298,94 +2248,86 @@ dependencies = [ [[package]] name = "gstreamer-app" -version = "0.16.5" +version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc80888271338c3ede875d8cafc452eb207476ff5539dcbe0018a8f5b827af0e" +checksum = "c45202b4d565034d4fe5577c990d3a99eaf0c2bfd2cb3f73f70db14d58e0208c" dependencies = [ "bitflags", "futures-core", "futures-sink", - "glib 0.10.3", - "glib-sys 0.10.1", - "gobject-sys 0.10.0", + "glib 0.16.5", "gstreamer", "gstreamer-app-sys", "gstreamer-base", - "gstreamer-sys", "libc", "once_cell", ] [[package]] name = "gstreamer-app-sys" -version = "0.9.1" +version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "813f64275c9e7b33b828b9efcf9dfa64b95996766d4de996e84363ac65b87e3d" +checksum = "29b0159da8dd0672c1a5507445c70c8dc483abfb63a0295cabaedd396f1d67d1" dependencies = [ - "glib-sys 0.10.1", + "glib-sys 0.16.3", "gstreamer-base-sys", "gstreamer-sys", "libc", - "system-deps 1.3.2", + "system-deps", ] [[package]] name = "gstreamer-base" -version = "0.16.5" +version = "0.19.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bafd01c56f59cb10f4b5a10f97bb4bdf8c2b2784ae5b04da7e2d400cf6e6afcf" +checksum = "a61a299f9ea2ca892b43e2e428b86c679875e95ba23f8ae06fd730308df630f0" dependencies = [ + "atomic_refcell", "bitflags", - "glib 0.10.3", - "glib-sys 0.10.1", - "gobject-sys 0.10.0", + "cfg-if 1.0.0", + "glib 0.16.5", "gstreamer", "gstreamer-base-sys", - "gstreamer-sys", "libc", ] [[package]] name = "gstreamer-base-sys" -version = "0.9.1" +version = "0.19.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4b7b6dc2d6e160a1ae28612f602bd500b3fa474ce90bf6bb2f08072682beef5" +checksum = "dbc3c4476e1503ae245c89fbe20060c30ec6ade5f44620bcc402cbc70a3911a1" dependencies = [ - "glib-sys 0.10.1", - "gobject-sys 0.10.0", + "glib-sys 0.16.3", + "gobject-sys 0.16.3", "gstreamer-sys", "libc", - "system-deps 1.3.2", + "system-deps", ] [[package]] name = "gstreamer-sys" -version = "0.9.1" +version = "0.19.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc1f154082d01af5718c5f8a8eb4f565a4ea5586ad8833a8fc2c2aa6844b601d" +checksum = "545f52ad8a480732cc4290fd65dfe42952c8ae374fe581831ba15981fedf18a4" dependencies = [ - "glib-sys 0.10.1", - "gobject-sys 0.10.0", + "glib-sys 0.16.3", + "gobject-sys 0.16.3", "libc", - "system-deps 1.3.2", + "system-deps", ] [[package]] name = "gstreamer-video" -version = "0.16.7" +version = "0.19.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7bbb1485d87469849ec45c08e03c2f280d3ea20ff3c439d03185be54e3ce98e" +checksum = "5e99623fb99436c4b2da66ae94b25881c94db5144afc1bd7c84cee5cabb72f18" dependencies = [ "bitflags", + "cfg-if 1.0.0", "futures-channel", - "futures-util", - "glib 0.10.3", - "glib-sys 0.10.1", - "gobject-sys 0.10.0", + "glib 0.16.5", "gstreamer", "gstreamer-base", - "gstreamer-base-sys", - "gstreamer-sys", "gstreamer-video-sys", "libc", "once_cell", @@ -2393,16 +2335,16 @@ dependencies = [ [[package]] name = "gstreamer-video-sys" -version = "0.9.1" +version = "0.19.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92347e46438007d6a2386302125f62cb9df6769cdacb931af5c0f12c1ee21de4" +checksum = "9206e9df0ed84824bfe4cc13e3359154ad7624221c7d3d6242585db3f19a15d9" dependencies = [ - "glib-sys 0.10.1", - "gobject-sys 0.10.0", + "glib-sys 0.16.3", + "gobject-sys 0.16.3", "gstreamer-base-sys", "gstreamer-sys", "libc", - "system-deps 1.3.2", + "system-deps", ] [[package]] @@ -2443,7 +2385,7 @@ dependencies = [ "gobject-sys 0.15.10", "libc", "pango-sys", - "system-deps 6.0.3", + "system-deps", ] [[package]] @@ -2453,7 +2395,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24f518afe90c23fba585b2d7697856f9e6a7bbc62f65588035e66f6afb01a2e9" dependencies = [ "anyhow", - "proc-macro-crate 1.2.1", + "proc-macro-crate", "proc-macro-error", "proc-macro2", "quote", @@ -2700,7 +2642,7 @@ dependencies = [ "byteorder", "color_quant", "num-iter", - "num-rational", + "num-rational 0.3.2", "num-traits 0.2.15", "png", "tiff", @@ -2790,15 +2732,6 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec947b7a4ce12e3b87e353abae7ce124d025b6c7d6c5aea5cc0bcf92e9510ded" -[[package]] -name = "itertools" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b" -dependencies = [ - "either", -] - [[package]] name = "itoa" version = "0.3.4" @@ -3252,9 +3185,9 @@ dependencies = [ [[package]] name = "muldiv" -version = "0.2.1" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0419348c027fa7be448d2ae7ea0e4e04c2334c31dc4e74ab29f00a2a7ca69204" +checksum = "956787520e75e9bd233246045d19f42fb73242759cc57fba9611d940ae96d4b0" [[package]] name = "ndk" @@ -3339,7 +3272,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0df7ac00c4672f9d5aece54ee3347520b7e20f158656c7db2e6de01902eb7a6c" dependencies = [ "darling", - "proc-macro-crate 1.2.1", + "proc-macro-crate", "proc-macro2", "quote", "syn", @@ -3526,6 +3459,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" @@ -3569,7 +3513,7 @@ version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b0498641e53dd6ac1a4f22547548caa6864cc4933784319cd1775271c5a46ce" dependencies = [ - "proc-macro-crate 1.2.1", + "proc-macro-crate", "proc-macro2", "quote", "syn", @@ -3657,6 +3601,15 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +[[package]] +name = "option-operations" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c26d27bb1aeab65138e4bf7666045169d1717febcc9ff870166be8348b223d0" +dependencies = [ + "paste", +] + [[package]] name = "ordered-multimap" version = "0.4.3" @@ -3711,7 +3664,7 @@ dependencies = [ "glib-sys 0.15.10", "gobject-sys 0.15.10", "libc", - "system-deps 6.0.3", + "system-deps", ] [[package]] @@ -3927,9 +3880,9 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "pretty-hex" -version = "0.2.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc5c99d529f0d30937f6f4b8a86d988047327bb88d04d2c4afc356de74722131" +checksum = "c6fa0831dd7cc608c38a5e323422a0077678fa5744aa2be4ad91c4ece8eec8d5" [[package]] name = "primal-check" @@ -3940,15 +3893,6 @@ dependencies = [ "num-integer", ] -[[package]] -name = "proc-macro-crate" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" -dependencies = [ - "toml", -] - [[package]] name = "proc-macro-crate" version = "1.2.1" @@ -4318,8 +4262,8 @@ dependencies = [ "libc", "log", "mio 0.8.5", - "strum 0.24.1", - "strum_macros 0.24.3", + "strum", + "strum_macros", "widestring 1.0.2", "winapi 0.3.9", "x11 2.20.1", @@ -5174,30 +5118,12 @@ dependencies = [ "syn", ] -[[package]] -name = "strum" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57bd81eb48f4c437cadc685403cad539345bf703d78e63707418431cecd4522b" - [[package]] name = "strum" version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" -[[package]] -name = "strum_macros" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87c85aa3f8ea653bfd3ddf25f7ee357ee4d204731f6aa9ad04002306f6e2774c" -dependencies = [ - "heck 0.3.3", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "strum_macros" version = "0.24.3" @@ -5283,21 +5209,6 @@ dependencies = [ "libc", ] -[[package]] -name = "system-deps" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f3ecc17269a19353b3558b313bba738b25d82993e30d62a18406a24aba4649b" -dependencies = [ - "heck 0.3.3", - "pkg-config", - "strum 0.18.0", - "strum_macros 0.18.0", - "thiserror", - "toml", - "version-compare 0.0.10", -] - [[package]] name = "system-deps" version = "6.0.3" @@ -5308,7 +5219,7 @@ dependencies = [ "heck 0.4.0", "pkg-config", "toml", - "version-compare 0.1.1", + "version-compare", ] [[package]] @@ -5739,12 +5650,6 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" -[[package]] -name = "version-compare" -version = "0.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d63556a25bae6ea31b52e640d7c41d1ab27faba4ccb600013837a3d0b3994ca1" - [[package]] name = "version-compare" version = "0.1.1" @@ -6571,7 +6476,7 @@ version = "3.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45066039ebf3330820e495e854f8b312abb68f0a39e97972d092bd72e8bb3e8e" dependencies = [ - "proc-macro-crate 1.2.1", + "proc-macro-crate", "proc-macro2", "quote", "regex", @@ -6638,7 +6543,7 @@ version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "155247a5d1ab55e335421c104ccd95d64f17cebbd02f50cdbc1c33385f9c4d81" dependencies = [ - "proc-macro-crate 1.2.1", + "proc-macro-crate", "proc-macro2", "quote", "syn", diff --git a/libs/scrap/Cargo.toml b/libs/scrap/Cargo.toml index e2eb43177..de6a6f2bf 100644 --- a/libs/scrap/Cargo.toml +++ b/libs/scrap/Cargo.toml @@ -48,9 +48,9 @@ bindgen = "0.59" [target.'cfg(target_os = "linux")'.dependencies] dbus = { version = "0.9", optional = true } tracing = { version = "0.1", optional = true } -gstreamer = { version = "0.16", optional = true } -gstreamer-app = { version = "0.16", features = ["v1_10"], optional = true } -gstreamer-video = { version = "0.16", optional = true } +gstreamer = { version = "0.19", optional = true } +gstreamer-app = { version = "0.19", features = ["v1_16"], optional = true } +gstreamer-video = { version = "0.19", optional = true } [target.'cfg(any(target_os = "windows", target_os = "linux"))'.dependencies] hwcodec = { git = "https://github.com/21pages/hwcodec", optional = true } diff --git a/libs/scrap/src/common/wayland.rs b/libs/scrap/src/common/wayland.rs index 2593e56fe..6e89568e1 100644 --- a/libs/scrap/src/common/wayland.rs +++ b/libs/scrap/src/common/wayland.rs @@ -50,6 +50,12 @@ impl TraitCapturer for Capturer { } else { x })), + PixelProvider::RGB0(w, h, x) => Ok(Frame(if self.2 { + crate::common::rgba_to_i420(w as _, h as _, &x, &mut self.3); + &self.3[..] + } else { + x + })), PixelProvider::NONE => Err(std::io::ErrorKind::WouldBlock.into()), _ => Err(map_err("Invalid data")), } diff --git a/libs/scrap/src/wayland/capturable.rs b/libs/scrap/src/wayland/capturable.rs index 05a5ec71d..61f80ecbf 100644 --- a/libs/scrap/src/wayland/capturable.rs +++ b/libs/scrap/src/wayland/capturable.rs @@ -4,6 +4,7 @@ use std::error::Error; pub enum PixelProvider<'a> { // 8 bits per color RGB(usize, usize, &'a [u8]), + RGB0(usize, usize, &'a [u8]), BGR0(usize, usize, &'a [u8]), // width, height, stride BGR0S(usize, usize, usize, &'a [u8]), @@ -14,6 +15,7 @@ impl<'a> PixelProvider<'a> { pub fn size(&self) -> (usize, usize) { match self { PixelProvider::RGB(w, h, _) => (*w, *h), + PixelProvider::RGB0(w, h, _) => (*w, *h), PixelProvider::BGR0(w, h, _) => (*w, *h), PixelProvider::BGR0S(w, h, _, _) => (*w, *h), PixelProvider::NONE => (0, 0), diff --git a/libs/scrap/src/wayland/pipewire.rs b/libs/scrap/src/wayland/pipewire.rs index a7b4c1357..d6ac70777 100644 --- a/libs/scrap/src/wayland/pipewire.rs +++ b/libs/scrap/src/wayland/pipewire.rs @@ -117,6 +117,7 @@ impl Capturable for PipeWireCapturable { pub struct PipeWireRecorder { buffer: Option>, buffer_cropped: Vec, + pix_fmt: String, is_cropped: bool, pipeline: gst::Pipeline, appsink: AppSink, @@ -129,34 +130,42 @@ impl PipeWireRecorder { pub fn new(capturable: PipeWireCapturable) -> Result> { let pipeline = gst::Pipeline::new(None); - let src = gst::ElementFactory::make("pipewiresrc", None)?; - src.set_property("fd", &capturable.fd.as_raw_fd())?; - src.set_property("path", &format!("{}", capturable.path))?; - src.set_property("keepalive_time", &1_000.as_raw_fd())?; + let src = gst::ElementFactory::make_with_name("pipewiresrc", None)?; + src.set_property("fd", &capturable.fd.as_raw_fd()); + src.set_property("path", &format!("{}", capturable.path)); + src.set_property("keepalive_time", &1_000.as_raw_fd()); // For some reason pipewire blocks on destruction of AppSink if this is not set to true, // see: https://gitlab.freedesktop.org/pipewire/pipewire/-/issues/982 - src.set_property("always-copy", &true)?; + src.set_property("always-copy", &true); - let sink = gst::ElementFactory::make("appsink", None)?; - sink.set_property("drop", &true)?; - sink.set_property("max-buffers", &1u32)?; + let sink = gst::ElementFactory::make_with_name("appsink", None)?; + sink.set_property("drop", &true); + sink.set_property("max-buffers", &1u32); pipeline.add_many(&[&src, &sink])?; src.link(&sink)?; + let appsink = sink .dynamic_cast::() .map_err(|_| GStreamerError("Sink element is expected to be an appsink!".into()))?; - appsink.set_caps(Some(&gst::Caps::new_simple( + let mut caps = gst::Caps::new_empty(); + caps.merge_structure(gst::structure::Structure::new( "video/x-raw", &[("format", &"BGRx")], - ))); + )); + caps.merge_structure(gst::structure::Structure::new( + "video/x-raw", + &[("format", &"RGBx")], + )); + appsink.set_caps(Some(&caps)); pipeline.set_state(gst::State::Playing)?; Ok(Self { pipeline, appsink, buffer: None, + pix_fmt: "".into(), width: 0, height: 0, buffer_cropped: vec![], @@ -173,20 +182,21 @@ impl Recorder for PipeWireRecorder { .try_pull_sample(gst::ClockTime::from_mseconds(timeout_ms)) { let cap = sample - .get_caps() + .caps() .ok_or("Failed get caps")? - .get_structure(0) + .structure(0) .ok_or("Failed to get structure")?; - let w: i32 = cap.get_value("width")?.get_some()?; - let h: i32 = cap.get_value("height")?.get_some()?; + let w: i32 = cap.value("width")?.get()?; + let h: i32 = cap.value("height")?.get()?; + self.pix_fmt = cap.value("format")?.get()?; let w = w as usize; let h = h as usize; let buf = sample - .get_buffer_owned() + .buffer_owned() .ok_or_else(|| GStreamerError("Failed to get owned buffer.".into()))?; let mut crop = buf - .get_meta::() - .map(|m| m.get_rect()); + .meta::() + .map(|m| m.rect()); // only crop if necessary if Some((0, 0, w as u32, h as u32)) == crop { crop = None; @@ -197,7 +207,7 @@ impl Recorder for PipeWireRecorder { if let Err(..) = crate::would_block_if_equal(&mut self.saved_raw_data, buf.as_slice()) { return Ok(PixelProvider::NONE); } - let buf_size = buf.get_size(); + let buf_size = buf.size(); // BGRx is 4 bytes per pixel if buf_size != (w * h * 4) { // for some reason the width and height of the caps do not guarantee correct buffer @@ -241,15 +251,22 @@ impl Recorder for PipeWireRecorder { if self.buffer.is_none() { return Err(Box::new(GStreamerError("No buffer available!".into()))); } - Ok(PixelProvider::BGR0( - self.width, - self.height, - if self.is_cropped { - self.buffer_cropped.as_slice() - } else { - self.buffer.as_ref().unwrap().as_slice() - }, - )) + let buf = if self.is_cropped { + self.buffer_cropped.as_slice() + } else { + self.buffer + .as_ref() + .ok_or("Failed to get buffer as ref")? + .as_slice() + }; + match self.pix_fmt.as_str() { + "BGRx" => Ok(PixelProvider::BGR0(self.width, self.height, buf)), + "RGBx" => Ok(PixelProvider::RGB0(self.width, self.height, buf)), + _ => Err(Box::new(GStreamerError(format!( + "Unreachable! Unknown pix_fmt, {}", + &self.pix_fmt + )))), + } } } From 59a82a9fbd0ad7b8cca3483f3e4e6b2f34fb2a54 Mon Sep 17 00:00:00 2001 From: Sangha Lee Date: Thu, 22 Dec 2022 20:42:34 +0900 Subject: [PATCH 1245/2015] downgrade gstreamer to 0.16 --- Cargo.lock | 275 +++++++++++++++++++---------- libs/scrap/Cargo.toml | 6 +- libs/scrap/src/wayland/pipewire.rs | 38 ++-- 3 files changed, 209 insertions(+), 110 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 21defd4d0..5a7d410fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -295,7 +295,7 @@ dependencies = [ "glib-sys 0.15.10", "gobject-sys 0.15.10", "libc", - "system-deps", + "system-deps 6.0.3", ] [[package]] @@ -313,12 +313,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "065374052e7df7ee4047b1160cca5e1467a12351a40b3da123c870ba0b8eda2a" -[[package]] -name = "atomic_refcell" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73b5e5f48b927f04e952dedc932f31995a65a0bf65ec971c74436e51bf6e970d" - [[package]] name = "atty" version = "0.2.14" @@ -525,7 +519,7 @@ checksum = "3c55d429bef56ac9172d25fecb85dc8068307d17acd74b377866b7a1ef25d3c8" dependencies = [ "glib-sys 0.15.10", "libc", - "system-deps", + "system-deps 6.0.3", ] [[package]] @@ -2002,7 +1996,7 @@ dependencies = [ "glib-sys 0.15.10", "gobject-sys 0.15.10", "libc", - "system-deps", + "system-deps 6.0.3", ] [[package]] @@ -2019,7 +2013,7 @@ dependencies = [ "libc", "pango-sys", "pkg-config", - "system-deps", + "system-deps 6.0.3", ] [[package]] @@ -2085,7 +2079,7 @@ dependencies = [ "glib-sys 0.15.10", "gobject-sys 0.15.10", "libc", - "system-deps", + "system-deps 6.0.3", "winapi 0.3.9", ] @@ -2098,10 +2092,29 @@ dependencies = [ "glib-sys 0.16.3", "gobject-sys 0.16.3", "libc", - "system-deps", + "system-deps 6.0.3", "winapi 0.3.9", ] +[[package]] +name = "glib" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c685013b7515e668f1b57a165b009d4d28cb139a8a989bbd699c10dad29d0c5" +dependencies = [ + "bitflags", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "glib-macros 0.10.1", + "glib-sys 0.10.1", + "gobject-sys 0.10.0", + "libc", + "once_cell", +] + [[package]] name = "glib" version = "0.15.12" @@ -2144,6 +2157,22 @@ dependencies = [ "thiserror", ] +[[package]] +name = "glib-macros" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41486a26d1366a8032b160b59065a59fb528530a46a49f627e7048fb8c064039" +dependencies = [ + "anyhow", + "heck 0.3.3", + "itertools", + "proc-macro-crate 0.1.5", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "glib-macros" version = "0.15.11" @@ -2152,7 +2181,7 @@ checksum = "25a68131a662b04931e71891fb14aaf65ee4b44d08e8abc10f49e77418c86c64" dependencies = [ "anyhow", "heck 0.4.0", - "proc-macro-crate", + "proc-macro-crate 1.2.1", "proc-macro-error", "proc-macro2", "quote", @@ -2167,13 +2196,23 @@ checksum = "e084807350b01348b6d9dbabb724d1a0bb987f47a2c85de200e98e12e30733bf" dependencies = [ "anyhow", "heck 0.4.0", - "proc-macro-crate", + "proc-macro-crate 1.2.1", "proc-macro-error", "proc-macro2", "quote", "syn", ] +[[package]] +name = "glib-sys" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e9b997a66e9a23d073f2b1abb4dbfc3925e0b8952f67efd8d9b6e168e4cdc1" +dependencies = [ + "libc", + "system-deps 1.3.2", +] + [[package]] name = "glib-sys" version = "0.15.10" @@ -2181,7 +2220,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef4b192f8e65e9cf76cbf4ea71fa8e3be4a0e18ffe3d68b8da6836974cc5bad4" dependencies = [ "libc", - "system-deps", + "system-deps 6.0.3", ] [[package]] @@ -2191,7 +2230,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61a4f46316d06bfa33a7ac22df6f0524c8be58e3db2d9ca99ccb1f357b62a65" dependencies = [ "libc", - "system-deps", + "system-deps 6.0.3", ] [[package]] @@ -2200,6 +2239,17 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" +[[package]] +name = "gobject-sys" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "952133b60c318a62bf82ee75b93acc7e84028a093e06b9e27981c2b6fe68218c" +dependencies = [ + "glib-sys 0.10.1", + "libc", + "system-deps 1.3.2", +] + [[package]] name = "gobject-sys" version = "0.15.10" @@ -2208,7 +2258,7 @@ checksum = "0d57ce44246becd17153bd035ab4d32cfee096a657fc01f2231c9278378d1e0a" dependencies = [ "glib-sys 0.15.10", "libc", - "system-deps", + "system-deps 6.0.3", ] [[package]] @@ -2219,28 +2269,28 @@ checksum = "3520bb9c07ae2a12c7f2fbb24d4efc11231c8146a86956413fb1a79bb760a0f1" dependencies = [ "glib-sys 0.16.3", "libc", - "system-deps", + "system-deps 6.0.3", ] [[package]] name = "gstreamer" -version = "0.19.4" +version = "0.16.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87a7570ad1d3c1cbf64561ada514fe0c03cf834f2076b85ffc616756c840b665" +checksum = "9ff5d0f7ff308ae37e6eb47b6ded17785bdea06e438a708cd09e0288c1862f33" dependencies = [ "bitflags", "cfg-if 1.0.0", "futures-channel", "futures-core", "futures-util", - "glib 0.16.5", + "glib 0.10.3", + "glib-sys 0.10.1", + "gobject-sys 0.10.0", "gstreamer-sys", "libc", "muldiv", - "num-integer", - "num-rational 0.4.1", + "num-rational", "once_cell", - "option-operations", "paste", "pretty-hex", "thiserror", @@ -2248,86 +2298,94 @@ dependencies = [ [[package]] name = "gstreamer-app" -version = "0.19.2" +version = "0.16.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c45202b4d565034d4fe5577c990d3a99eaf0c2bfd2cb3f73f70db14d58e0208c" +checksum = "cc80888271338c3ede875d8cafc452eb207476ff5539dcbe0018a8f5b827af0e" dependencies = [ "bitflags", "futures-core", "futures-sink", - "glib 0.16.5", + "glib 0.10.3", + "glib-sys 0.10.1", + "gobject-sys 0.10.0", "gstreamer", "gstreamer-app-sys", "gstreamer-base", + "gstreamer-sys", "libc", "once_cell", ] [[package]] name = "gstreamer-app-sys" -version = "0.19.2" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29b0159da8dd0672c1a5507445c70c8dc483abfb63a0295cabaedd396f1d67d1" +checksum = "813f64275c9e7b33b828b9efcf9dfa64b95996766d4de996e84363ac65b87e3d" dependencies = [ - "glib-sys 0.16.3", + "glib-sys 0.10.1", "gstreamer-base-sys", "gstreamer-sys", "libc", - "system-deps", + "system-deps 1.3.2", ] [[package]] name = "gstreamer-base" -version = "0.19.3" +version = "0.16.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a61a299f9ea2ca892b43e2e428b86c679875e95ba23f8ae06fd730308df630f0" +checksum = "bafd01c56f59cb10f4b5a10f97bb4bdf8c2b2784ae5b04da7e2d400cf6e6afcf" dependencies = [ - "atomic_refcell", "bitflags", - "cfg-if 1.0.0", - "glib 0.16.5", + "glib 0.10.3", + "glib-sys 0.10.1", + "gobject-sys 0.10.0", "gstreamer", "gstreamer-base-sys", + "gstreamer-sys", "libc", ] [[package]] name = "gstreamer-base-sys" -version = "0.19.3" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc3c4476e1503ae245c89fbe20060c30ec6ade5f44620bcc402cbc70a3911a1" +checksum = "a4b7b6dc2d6e160a1ae28612f602bd500b3fa474ce90bf6bb2f08072682beef5" dependencies = [ - "glib-sys 0.16.3", - "gobject-sys 0.16.3", + "glib-sys 0.10.1", + "gobject-sys 0.10.0", "gstreamer-sys", "libc", - "system-deps", + "system-deps 1.3.2", ] [[package]] name = "gstreamer-sys" -version = "0.19.4" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "545f52ad8a480732cc4290fd65dfe42952c8ae374fe581831ba15981fedf18a4" +checksum = "fc1f154082d01af5718c5f8a8eb4f565a4ea5586ad8833a8fc2c2aa6844b601d" dependencies = [ - "glib-sys 0.16.3", - "gobject-sys 0.16.3", + "glib-sys 0.10.1", + "gobject-sys 0.10.0", "libc", - "system-deps", + "system-deps 1.3.2", ] [[package]] name = "gstreamer-video" -version = "0.19.4" +version = "0.16.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e99623fb99436c4b2da66ae94b25881c94db5144afc1bd7c84cee5cabb72f18" +checksum = "f7bbb1485d87469849ec45c08e03c2f280d3ea20ff3c439d03185be54e3ce98e" dependencies = [ "bitflags", - "cfg-if 1.0.0", "futures-channel", - "glib 0.16.5", + "futures-util", + "glib 0.10.3", + "glib-sys 0.10.1", + "gobject-sys 0.10.0", "gstreamer", "gstreamer-base", + "gstreamer-base-sys", + "gstreamer-sys", "gstreamer-video-sys", "libc", "once_cell", @@ -2335,16 +2393,16 @@ dependencies = [ [[package]] name = "gstreamer-video-sys" -version = "0.19.4" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9206e9df0ed84824bfe4cc13e3359154ad7624221c7d3d6242585db3f19a15d9" +checksum = "92347e46438007d6a2386302125f62cb9df6769cdacb931af5c0f12c1ee21de4" dependencies = [ - "glib-sys 0.16.3", - "gobject-sys 0.16.3", + "glib-sys 0.10.1", + "gobject-sys 0.10.0", "gstreamer-base-sys", "gstreamer-sys", "libc", - "system-deps", + "system-deps 1.3.2", ] [[package]] @@ -2385,7 +2443,7 @@ dependencies = [ "gobject-sys 0.15.10", "libc", "pango-sys", - "system-deps", + "system-deps 6.0.3", ] [[package]] @@ -2395,7 +2453,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24f518afe90c23fba585b2d7697856f9e6a7bbc62f65588035e66f6afb01a2e9" dependencies = [ "anyhow", - "proc-macro-crate", + "proc-macro-crate 1.2.1", "proc-macro-error", "proc-macro2", "quote", @@ -2642,7 +2700,7 @@ dependencies = [ "byteorder", "color_quant", "num-iter", - "num-rational 0.3.2", + "num-rational", "num-traits 0.2.15", "png", "tiff", @@ -2732,6 +2790,15 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec947b7a4ce12e3b87e353abae7ce124d025b6c7d6c5aea5cc0bcf92e9510ded" +[[package]] +name = "itertools" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "0.3.4" @@ -3185,9 +3252,9 @@ dependencies = [ [[package]] name = "muldiv" -version = "1.0.1" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "956787520e75e9bd233246045d19f42fb73242759cc57fba9611d940ae96d4b0" +checksum = "0419348c027fa7be448d2ae7ea0e4e04c2334c31dc4e74ab29f00a2a7ca69204" [[package]] name = "ndk" @@ -3272,7 +3339,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0df7ac00c4672f9d5aece54ee3347520b7e20f158656c7db2e6de01902eb7a6c" dependencies = [ "darling", - "proc-macro-crate", + "proc-macro-crate 1.2.1", "proc-macro2", "quote", "syn", @@ -3459,17 +3526,6 @@ 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" @@ -3513,7 +3569,7 @@ version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b0498641e53dd6ac1a4f22547548caa6864cc4933784319cd1775271c5a46ce" dependencies = [ - "proc-macro-crate", + "proc-macro-crate 1.2.1", "proc-macro2", "quote", "syn", @@ -3601,15 +3657,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" -[[package]] -name = "option-operations" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c26d27bb1aeab65138e4bf7666045169d1717febcc9ff870166be8348b223d0" -dependencies = [ - "paste", -] - [[package]] name = "ordered-multimap" version = "0.4.3" @@ -3664,7 +3711,7 @@ dependencies = [ "glib-sys 0.15.10", "gobject-sys 0.15.10", "libc", - "system-deps", + "system-deps 6.0.3", ] [[package]] @@ -3880,9 +3927,9 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "pretty-hex" -version = "0.3.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6fa0831dd7cc608c38a5e323422a0077678fa5744aa2be4ad91c4ece8eec8d5" +checksum = "bc5c99d529f0d30937f6f4b8a86d988047327bb88d04d2c4afc356de74722131" [[package]] name = "primal-check" @@ -3893,6 +3940,15 @@ dependencies = [ "num-integer", ] +[[package]] +name = "proc-macro-crate" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" +dependencies = [ + "toml", +] + [[package]] name = "proc-macro-crate" version = "1.2.1" @@ -4262,8 +4318,8 @@ dependencies = [ "libc", "log", "mio 0.8.5", - "strum", - "strum_macros", + "strum 0.24.1", + "strum_macros 0.24.3", "widestring 1.0.2", "winapi 0.3.9", "x11 2.20.1", @@ -5118,12 +5174,30 @@ dependencies = [ "syn", ] +[[package]] +name = "strum" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57bd81eb48f4c437cadc685403cad539345bf703d78e63707418431cecd4522b" + [[package]] name = "strum" version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" +[[package]] +name = "strum_macros" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87c85aa3f8ea653bfd3ddf25f7ee357ee4d204731f6aa9ad04002306f6e2774c" +dependencies = [ + "heck 0.3.3", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "strum_macros" version = "0.24.3" @@ -5209,6 +5283,21 @@ dependencies = [ "libc", ] +[[package]] +name = "system-deps" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f3ecc17269a19353b3558b313bba738b25d82993e30d62a18406a24aba4649b" +dependencies = [ + "heck 0.3.3", + "pkg-config", + "strum 0.18.0", + "strum_macros 0.18.0", + "thiserror", + "toml", + "version-compare 0.0.10", +] + [[package]] name = "system-deps" version = "6.0.3" @@ -5219,7 +5308,7 @@ dependencies = [ "heck 0.4.0", "pkg-config", "toml", - "version-compare", + "version-compare 0.1.1", ] [[package]] @@ -5650,6 +5739,12 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" +[[package]] +name = "version-compare" +version = "0.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d63556a25bae6ea31b52e640d7c41d1ab27faba4ccb600013837a3d0b3994ca1" + [[package]] name = "version-compare" version = "0.1.1" @@ -6476,7 +6571,7 @@ version = "3.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45066039ebf3330820e495e854f8b312abb68f0a39e97972d092bd72e8bb3e8e" dependencies = [ - "proc-macro-crate", + "proc-macro-crate 1.2.1", "proc-macro2", "quote", "regex", @@ -6543,7 +6638,7 @@ version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "155247a5d1ab55e335421c104ccd95d64f17cebbd02f50cdbc1c33385f9c4d81" dependencies = [ - "proc-macro-crate", + "proc-macro-crate 1.2.1", "proc-macro2", "quote", "syn", diff --git a/libs/scrap/Cargo.toml b/libs/scrap/Cargo.toml index de6a6f2bf..e2eb43177 100644 --- a/libs/scrap/Cargo.toml +++ b/libs/scrap/Cargo.toml @@ -48,9 +48,9 @@ bindgen = "0.59" [target.'cfg(target_os = "linux")'.dependencies] dbus = { version = "0.9", optional = true } tracing = { version = "0.1", optional = true } -gstreamer = { version = "0.19", optional = true } -gstreamer-app = { version = "0.19", features = ["v1_16"], optional = true } -gstreamer-video = { version = "0.19", optional = true } +gstreamer = { version = "0.16", optional = true } +gstreamer-app = { version = "0.16", features = ["v1_10"], optional = true } +gstreamer-video = { version = "0.16", optional = true } [target.'cfg(any(target_os = "windows", target_os = "linux"))'.dependencies] hwcodec = { git = "https://github.com/21pages/hwcodec", optional = true } diff --git a/libs/scrap/src/wayland/pipewire.rs b/libs/scrap/src/wayland/pipewire.rs index d6ac70777..abbdf3f25 100644 --- a/libs/scrap/src/wayland/pipewire.rs +++ b/libs/scrap/src/wayland/pipewire.rs @@ -130,18 +130,18 @@ impl PipeWireRecorder { pub fn new(capturable: PipeWireCapturable) -> Result> { let pipeline = gst::Pipeline::new(None); - let src = gst::ElementFactory::make_with_name("pipewiresrc", None)?; - src.set_property("fd", &capturable.fd.as_raw_fd()); - src.set_property("path", &format!("{}", capturable.path)); - src.set_property("keepalive_time", &1_000.as_raw_fd()); + let src = gst::ElementFactory::make("pipewiresrc", None)?; + src.set_property("fd", &capturable.fd.as_raw_fd())?; + src.set_property("path", &format!("{}", capturable.path))?; + src.set_property("keepalive_time", &1_000.as_raw_fd())?; // For some reason pipewire blocks on destruction of AppSink if this is not set to true, // see: https://gitlab.freedesktop.org/pipewire/pipewire/-/issues/982 - src.set_property("always-copy", &true); + src.set_property("always-copy", &true)?; - let sink = gst::ElementFactory::make_with_name("appsink", None)?; - sink.set_property("drop", &true); - sink.set_property("max-buffers", &1u32); + let sink = gst::ElementFactory::make("appsink", None)?; + sink.set_property("drop", &true)?; + sink.set_property("max-buffers", &1u32)?; pipeline.add_many(&[&src, &sink])?; src.link(&sink)?; @@ -182,21 +182,25 @@ impl Recorder for PipeWireRecorder { .try_pull_sample(gst::ClockTime::from_mseconds(timeout_ms)) { let cap = sample - .caps() + .get_caps() .ok_or("Failed get caps")? - .structure(0) + .get_structure(0) .ok_or("Failed to get structure")?; - let w: i32 = cap.value("width")?.get()?; - let h: i32 = cap.value("height")?.get()?; - self.pix_fmt = cap.value("format")?.get()?; + let w: i32 = cap.get_value("width")?.get_some()?; + let h: i32 = cap.get_value("height")?.get_some()?; let w = w as usize; let h = h as usize; + self.pix_fmt = cap + .get::<&str>("format")? + .ok_or("Failed to get pixel format")? + .to_string(); + let buf = sample - .buffer_owned() + .get_buffer_owned() .ok_or_else(|| GStreamerError("Failed to get owned buffer.".into()))?; let mut crop = buf - .meta::() - .map(|m| m.rect()); + .get_meta::() + .map(|m| m.get_rect()); // only crop if necessary if Some((0, 0, w as u32, h as u32)) == crop { crop = None; @@ -207,7 +211,7 @@ impl Recorder for PipeWireRecorder { if let Err(..) = crate::would_block_if_equal(&mut self.saved_raw_data, buf.as_slice()) { return Ok(PixelProvider::NONE); } - let buf_size = buf.size(); + let buf_size = buf.get_size(); // BGRx is 4 bytes per pixel if buf_size != (w * h * 4) { // for some reason the width and height of the caps do not guarantee correct buffer From 602932ba970f304dbd0c75407bc7d889ad1f1683 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Thu, 22 Dec 2022 19:54:04 +0800 Subject: [PATCH 1246/2015] fix: keeps mouse region grab key on linux --- flutter/lib/desktop/pages/remote_page.dart | 54 ++++++++++++++++------ 1 file changed, 39 insertions(+), 15 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 8fd4ee07c..dd569a110 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -128,21 +128,31 @@ class _RemotePageState extends State @override void onWindowBlur() { super.onWindowBlur(); - _isWindowBlur = true; - // unfocus the key focus when the whole window is lost focus, - // and let OS to handle events instead. - _rawKeyFocusNode.unfocus(); + // On windows, we use `focus` way to handle keyboard better. + // Now on Linux, there's some rdev issues which will break the input. + // We disable the `focus` way for non-Windows temporarily. + if (Platform.isWindows) { + _isWindowBlur = true; + // unfocus the primary-focus when the whole window is lost focus, + // and let OS to handle events instead. + _rawKeyFocusNode.unfocus(); + } } @override void onWindowFocus() { super.onWindowFocus(); - _isWindowBlur = false; + // See [onWindowBlur]. + if (Platform.isWindows) { + _isWindowBlur = false; + } } @override void dispose() { debugPrint("REMOTE PAGE dispose ${widget.id}"); + // ensure we leave this session, this is a double check + bind.sessionEnterOrLeave(id: widget.id, enter: false); DesktopMultiWindow.removeListener(this); _ffi.dialogManager.hideMobileActionsOverlay(); _ffi.recordingModel.onClose(); @@ -175,16 +185,19 @@ class _RemotePageState extends State onFocusChange: (bool imageFocused) { debugPrint( "onFocusChange(window active:${!_isWindowBlur}) $imageFocused"); - if (_isWindowBlur) { - imageFocused = false; - Future.delayed(Duration.zero, () { - _rawKeyFocusNode.unfocus(); - }); - } - if (imageFocused) { - _ffi.inputModel.enterOrLeave(true); - } else { - _ffi.inputModel.enterOrLeave(false); + // 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, @@ -221,6 +234,13 @@ class _RemotePageState extends State // } } + // See [onWindowBlur]. + if (!Platform.isWindows) { + if (!_rawKeyFocusNode.hasFocus) { + _rawKeyFocusNode.requestFocus(); + } + bind.sessionEnterOrLeave(id: widget.id, enter: true); + } } void leaveView(PointerExitEvent evt) { @@ -233,6 +253,10 @@ class _RemotePageState extends State // } } + // See [onWindowBlur]. + if (!Platform.isWindows) { + bind.sessionEnterOrLeave(id: widget.id, enter: false); + } } Widget getBodyForDesktop(BuildContext context) { From 4815099482d2b07a20196e028e4855e9cb4d7161 Mon Sep 17 00:00:00 2001 From: Huabing Zhou Date: Fri, 23 Dec 2022 22:00:05 +0800 Subject: [PATCH 1247/2015] make m1 run (used brew install llvm) --- flutter/macos/Runner.xcodeproj/project.pbxproj | 6 +++--- libs/scrap/src/quartz/display.rs | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/flutter/macos/Runner.xcodeproj/project.pbxproj b/flutter/macos/Runner.xcodeproj/project.pbxproj index ec4baf141..a5a476285 100644 --- a/flutter/macos/Runner.xcodeproj/project.pbxproj +++ b/flutter/macos/Runner.xcodeproj/project.pbxproj @@ -378,7 +378,7 @@ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - ARCHS = x86_64; + ARCHS = "$(ARCHS_STANDARD)"; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -459,7 +459,7 @@ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - ARCHS = x86_64; + ARCHS = "$(ARCHS_STANDARD)"; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -513,7 +513,7 @@ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - ARCHS = x86_64; + ARCHS = "$(ARCHS_STANDARD)"; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; diff --git a/libs/scrap/src/quartz/display.rs b/libs/scrap/src/quartz/display.rs index ff96b2c1c..47ace49db 100644 --- a/libs/scrap/src/quartz/display.rs +++ b/libs/scrap/src/quartz/display.rs @@ -13,6 +13,7 @@ impl Display { pub fn online() -> Result, CGError> { unsafe { + #[allow(invalid_value)] let mut arr: [u32; 16] = mem::MaybeUninit::uninit().assume_init(); let mut len: u32 = 0; From eac8327f57f2a2d07ad8f70ea5bf84f0621330c8 Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 23 Dec 2022 23:10:34 +0800 Subject: [PATCH 1248/2015] fix adjust window Signed-off-by: fufesou --- flutter/lib/desktop/widgets/remote_menubar.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 0c2a6a971..c776f0e89 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -171,6 +171,8 @@ class _RemoteMenubarState extends State { @override Widget build(BuildContext context) { + // No need to use future builder here. + _updateScreen(); return Align( alignment: Alignment.topCenter, child: Obx(() => show.value From e95b7197fc5fedfdb1da6afabba374422b958e50 Mon Sep 17 00:00:00 2001 From: BigRetroMike Date: Fri, 23 Dec 2022 17:27:03 +0100 Subject: [PATCH 1249/2015] Polish language update; The more I use it the more I notice the context. --- src/lang/pl.rs | 52 +++++++++++++++++++++++++------------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 54cc10164..eb3a45d53 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -3,7 +3,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = [ ("Status", "Status"), ("Your Desktop", "Twój pulpit"), - ("desk_tip", "Aby połaczyć się z tym urządzeniem należy użyć tego ID i hasła."), + ("desk_tip", "W celu zestawienia połączenia z tym urządzeniem należy poniższego ID i hasła."), ("Password", "Hasło"), ("Ready", "Gotowe"), ("Established", "Nawiązano"), @@ -13,7 +13,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Service is running", "Usługa uruchomiona"), ("Service is not running", "Usługa nie jest uruchomiona"), ("not_ready_status", "Brak gotowości"), - ("Control Remote Desktop", "Kontroluj zdalny pulpit"), + ("Control Remote Desktop", "Połącz się z"), ("Transfer File", "Transfer plików"), ("Connect", "Połącz"), ("Recent Sessions", "Ostatnie sesje"), @@ -121,7 +121,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Optimize reaction time", "Zoptymalizuj czas reakcji"), ("Custom", "Własne"), ("Show remote cursor", "Pokazuj zdalny kursor"), - ("Show quality monitor", "Pokazuj jakość monitora"), + ("Show quality monitor", "Parametry połączenia"), ("Disable clipboard", "Wyłącz schowek"), ("Lock after session end", "Zablokuj po zakończeniu sesji"), ("Insert", "Wyślij"), @@ -140,9 +140,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Set Password", "Ustaw hasło"), ("OS Password", "Hasło systemu operacyjnego"), ("install_tip", "RustDesk może nie działać poprawnie na maszynie zdalnej z przyczyn związanych z UAC. W celu uniknięcią problemów z UAC, kliknij poniższy przycisk by zainstalować RustDesk w swoim systemie."), - ("Click to upgrade", "Kliknij, aby zaktualizować (upgrade)"), - ("Click to download", "Kliknij, aby pobrać"), - ("Click to update", "Kliknij, aby zaktualizować (update)"), + ("Click to upgrade", "Zaktualizuj"), + ("Click to download", "Pobierz"), + ("Click to update", "Uaktualinij"), ("Configure", "Konfiguruj"), ("config_acc", "Konfiguracja konta"), ("config_screen", "Konfiguracja ekranu"), @@ -317,20 +317,20 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("remote_restarting_tip", "Trwa ponownie uruchomienie zdalnego urządzenia, zamknij ten komunikat i ponownie nawiąż za chwilę połączenie używając hasła permanentnego"), ("Copied", "Skopiowano"), ("Exit Fullscreen", "Wyłączyć tryb pełnoekranowy"), - ("Fullscreen", "Pełny ekran"), - ("Mobile Actions", "Działania mobilne"), - ("Select Monitor", "Wybierz Monitor"), - ("Control Actions", "Działania kontrolne"), + ("Fullscreen", "Tryb pełnoekranowy"), + ("Mobile Actions", "Dostępne mobilne polecenia"), + ("Select Monitor", "Wybierz ekran"), + ("Control Actions", "Dostępne polecenia"), ("Display Settings", "Ustawienia wyświetlania"), ("Ratio", "Proporcje"), ("Image Quality", "Jakość obrazu"), ("Scroll Style", "Styl przewijania"), ("Show Menubar", "Pokaż pasek menu"), ("Hide Menubar", "Ukryj pasek menu"), - ("Direct Connection", "Połącznie Bezpośrednie"), - ("Relay Connection", "Połączenie Pośrednie"), - ("Secure Connection", "Połączenie Bezpieczne"), - ("Insecure Connection", "Połączenie Niebezpieczne"), + ("Direct Connection", "Połącznie bezpośrednie"), + ("Relay Connection", "Połączenie przez bramkę"), + ("Secure Connection", "Połączenie szyfrowane"), + ("Insecure Connection", "Połączenie nieszyfrowane"), ("Scale original", "Skaluj oryginalnie"), ("Scale adaptive", "Skaluj adaptacyjnie"), ("General", "Ogólne"), @@ -367,11 +367,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop session recording", "Zatrzymaj nagrywanie sesji"), ("Enable Recording Session", "Włącz Nagrywanie Sesji"), ("Allow recording session", "Zezwól na nagrywanie sesji"), - ("Enable LAN Discovery", "Włącz Wykrywanie LAN"), - ("Deny LAN Discovery", "Zablokuj Wykrywanie LAN"), + ("Enable LAN Discovery", "Włącz wykrywanie urządzenia w sieci LAN"), + ("Deny LAN Discovery", "Zablokuj wykrywanie urządzenia w sieci LAN"), ("Write a message", "Napisz wiadomość"), ("Prompt", "Monit"), - ("Please wait for confirmation of UAC...", ""), + ("Please wait for confirmation of UAC...", "Oczekuje potwierdzenia ustawień UAC"), ("elevated_foreground_window_tip", ""), ("Disconnected", "Rozłączone"), ("Other", "Inne"), @@ -388,22 +388,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("This PC", "Ten komputer"), ("or", "albo"), ("Continue with", "Kontynuuj z"), - ("Elevate", "Podwyższ"), + ("Elevate", "Uzyskaj uprawnienia"), ("Zoom cursor", "Zoom kursora"), - ("Accept sessions via password", "Akceptuj sesje używając hasła"), - ("Accept sessions via click", "Akceptuj sesję klikając"), - ("Accept sessions via both", "Akceptuj sesjęna dwa sposoby"), - ("Please wait for the remote side to accept your session request...", "Proszę czekać aż zdalny host zaakceptuje Twoją prośbę..."), + ("Accept sessions via password", "Uwierzytelnij sesję używając hasła"), + ("Accept sessions via click", "Uwierzytelnij sesję poprzez kliknięcie"), + ("Accept sessions via both", "Uwierzytelnij sesję za pomocą obu sposobów"), + ("Please wait for the remote side to accept your session request...", "Oczekiwanie, na zatwierdzenie sesji przez host zdalny..."), ("One-time Password", "Hasło jednorazowe"), ("Use one-time password", "Użyj hasła jednorazowego"), ("One-time password length", "Długość hasła jednorazowego"), ("Request access to your device", "Żądanie dostępu do Twojego urządzenia"), - ("Hide connection management window", ""), + ("Hide connection management window", "Ukryj okno zarządzania połączeniem"), ("hide_cm_tip", ""), ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), - ("Add to Address Book", ""), - ("Group", ""), - ("Search", ""), + ("Add to Address Book", "Dodaj do Książki Adresowej"), + ("Group", "Grypy"), + ("Search", "Szukaj"), ].iter().cloned().collect(); } From f18aebce22049ff0cfb5bca3271294462ac4cf06 Mon Sep 17 00:00:00 2001 From: behrooz3500 Date: Sat, 24 Dec 2022 22:29:59 +0330 Subject: [PATCH 1250/2015] Edit Persian translations --- src/lang/fa.rs | 180 ++++++++++++++++++++++++------------------------- 1 file changed, 90 insertions(+), 90 deletions(-) diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 8797f209e..b3850e1f2 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -9,22 +9,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Established", "اتصال برقرار شد"), ("connecting_status", "...در حال برقراری ارتباط با سرور"), ("Enable Service", "فعالسازی سرویس"), - ("Start Service", "اجرا سرویس"), + ("Start Service", "اجرای سرویس"), ("Service is running", "سرویس در حال اجرا است"), ("Service is not running", "سرویس اجرا نشده"), ("not_ready_status", "ارتباط برقرار نشد. لطفا شبکه خود را بررسی کنید"), ("Control Remote Desktop", "کنترل دسکتاپ میزبان"), - ("Transfer File", "جابه جایی فایل"), + ("Transfer File", "انتقال فایل"), ("Connect", "اتصال"), ("Recent Sessions", "جلسات اخیر"), ("Address Book", "دفترچه آدرس"), ("Confirmation", "تایید"), ("TCP Tunneling", "TCP تانل"), ("Remove", "حذف"), - ("Refresh random password", "رمز عبور تصادفی را بروز کنید"), + ("Refresh random password", "بروزرسانی رمز عبور تصادفی"), ("Set your own password", "!رمز عبور دلخواه بگذارید"), - ("Enable Keyboard/Mouse", "Keyboard/Mouse فعالسازی"), - ("Enable Clipboard", "Clipboard فعالسازی"), + ("Enable Keyboard/Mouse", " فعالسازی ماوس/صفحه کلید"), + ("Enable Clipboard", "فعال سازی کلیپبورد"), ("Enable File Transfer", "انتقال فایل را فعال کنید"), ("Enable TCP Tunneling", "را فعال کنید TCP تانل"), ("IP Whitelisting", "های مجاز IP لیست"), @@ -34,7 +34,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Import server configuration successfully", "تنظیمات سرور با فایل کانفیگ با موفقیت انجام شد"), ("Export server configuration successfully", "ایجاد فایل کانفیگ از تنظیمات فعلی با موفقیت انجام شد"), ("Invalid server configuration", "تنظیمات سرور نامعتبر است"), - ("Clipboard is empty", "خالی است Clipboard"), + ("Clipboard is empty", "کلیپبورد خالی است"), ("Stop service", "توقف سرویس"), ("Change ID", "تعویض شناسه"), ("Website", "وب سایت"), @@ -53,10 +53,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("invalid_http", "شروع شود http:// یا https:// باید با"), ("Invalid IP", "نامعتبر است IP آدرس"), ("id_change_tip", "شناسه باید طبق این شرایط باشد : حروف کوچک و بزرگ انگلیسی و اعداد از 0 تا 9، _ و همچنین حرف اول آن فقط حروف بزرگ یا کوچک انگلیسی و طول آن بین 6 الی 16 کاراکتر باشد"), - ("Invalid format", "فرمت نادرس است"), + ("Invalid format", "فرمت نادرست است"), ("server_not_support", "هنوز توسط سرور مورد نظر پشتیبانی نمی شود"), ("Not available", "در دسترسی نیست"), - ("Too frequent", "تعداد زیاد"), + ("Too frequent", "خیلی رایج"), ("Cancel", "لغو"), ("Skip", "رد کردن"), ("Close", "بستن"), @@ -75,7 +75,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please try 1 minute later", "لطفا بعد از 1 دقیقه مجددا تلاش کنید"), ("Login Error", "ورود ناموفق بود"), ("Successful", "ورود با موفقیت انجام شد"), - ("Connected, waiting for image...", "ارتباط وصل شد. برای دریافت تصویر دسکتاپ میزبان منتظر بمانید..."), + ("Connected, waiting for image...", "...ارتباط برقرار شد. انتظار برای دریافت تصاویر"), ("Name", "نام"), ("Type", "نوع فایل"), ("Modified", "تاریخ تغییر"), @@ -87,20 +87,20 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Local", "محلی"), ("Remote", "از راه دور"), ("Remote Computer", "سیستم میزبان"), - ("Local Computer", "سیستم از راه دور"), - ("Confirm Delete", "حذف را تایید کنید"), + ("Local Computer", "سیستم راه دور"), + ("Confirm Delete", "تایید حذف"), ("Delete", "حذف"), - ("Properties", "Properties"), - ("Multi Select", "انتخاب همزمان"), + ("Properties", "مشخصات"), + ("Multi Select", "انتخاب دسته ای"), ("Select All", "انتخاب همه"), - ("Unselect All", "عدم انتخاب همه"), + ("Unselect All", "لغو انتخاب همه"), ("Empty Directory", "پوشه خالی"), ("Not an empty directory", "پوشه خالی نیست"), ("Are you sure you want to delete this file?", "از حذف این فایل مطمئن هستید؟"), ("Are you sure you want to delete this empty directory?", "از حذف این پوشه خالی مطمئن هستید؟"), ("Are you sure you want to delete the file of this directory?", "از حذف فایل موجود در این پوشه مطمئن هستید؟"), - ("Do this for all conflicts", "این عمل را برای همه ی تضادها انجام شود"), - ("This is irreversible!", "این برگشت ناپذیر است!"), + ("Do this for all conflicts", "این عمل برای همه ی تضادها انجام شود"), + ("This is irreversible!", "این اقدام برگشت ناپذیر است!"), ("Deleting", "در حال حذف"), ("files", "فایل ها"), ("Waiting", "انتظار"), @@ -108,21 +108,21 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Speed", "سرعت"), ("Custom Image Quality", "سفارشی سازی کیفیت تصاویر"), ("Privacy mode", "حالت حریم خصوصی"), - ("Block user input", "ورودی کاربر را مسدود کنید"), - ("Unblock user input", "قفل ورودی کاربر را باز کنید"), - ("Adjust Window", "پنجره را تنظیم کنید"), + ("Block user input", "بلاک کردن ورودی کاربر"), + ("Unblock user input", "آنبلاک کردن ورودی کاربر"), + ("Adjust Window", "تنظیم پنجره"), ("Original", "اصل"), - ("Shrink", ""), - ("Stretch", ""), - ("Scrollbar", ""), - ("ScrollAuto", ""), + ("Shrink", "کوچک کردن"), + ("Stretch", "کشیدن تصویر"), + ("Scrollbar", "اسکرول بار"), + ("ScrollAuto", "پیمایش/اسکرول خودکار"), ("Good image quality", "کیفیت خوب تصویر"), ("Balanced", "متعادل"), - ("Optimize reaction time", "زمان واکنش را بهینه کنید"), + ("Optimize reaction time", "بهینه سازی زمان واکنش"), ("Custom", "سفارشی"), ("Show remote cursor", "نمایش مکان نما موس میزبان"), ("Show quality monitor", "نمایش کیفیت مانیتور"), - ("Disable clipboard", "Clipboard غیرفعالسازی"), + ("Disable clipboard", " غیرفعالسازی کلیپبورد"), ("Lock after session end", "قفل کردن حساب کاربری سیستم عامل پس از پایان جلسه"), ("Insert", "افزودن"), ("Insert Lock", "افزودن قفل"), @@ -130,23 +130,23 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ID does not exist", "شناسه وجود ندارد"), ("Failed to connect to rendezvous server", "اتصال به سرور تولید شناسه انجام نشد"), ("Please try later", "لطفا بعدا تلاش کنید"), - ("Remote desktop is offline", "دسکتاپ از راه دور خاموش است"), + ("Remote desktop is offline", "دسکتاپ راه دور آفلاین است"), ("Key mismatch", "عدم تطابق کلید"), ("Timeout", "زمان انتظار به پایان رسید"), ("Failed to connect to relay server", "سرور وصل نشد Relay به"), ("Failed to connect via rendezvous server", "اتصال از طریق سرور تولید شناسه انجام نشد"), ("Failed to connect via relay server", "انجام نشد Relay اتصال از طریق سرور"), - ("Failed to make direct connection to remote desktop", "اتصال مستقیم به دسکتاپ از راه دور با موفقیت انجام نشد"), - ("Set Password", "اختصاص رمزعبور"), + ("Failed to make direct connection to remote desktop", "اتصال مستقیم به دسکتاپ راه دور انجام نشد"), + ("Set Password", "تنظیم رمزعبور"), ("OS Password", "رمز عیور سیستم عامل"), ("install_tip", "لطفا برنامه را نصب کنید UAC و جلوگیری از خطای RustDesk برای راحتی در استفاده از نرم افزار"), ("Click to upgrade", "برای ارتقا کلیک کنید"), ("Click to download", "برای دانلود کلیک کنید"), ("Click to update", "برای به روز رسانی کلیک کنید"), ("Configure", "تنظیم"), - ("config_acc", "برای کنترل از راه دور دسکتاپ، باید به RustDesk مجوز \"access\" بدهید"), - ("config_screen", "برای دسترسی از راه دور به دسکتاپ خود، باید به RustDesk مجوزهای \"screenshot\" بدهید."), - ("Installing ...", "در حال نصب..."), + ("config_acc", "بدهید \"access\" مجوز RustDesk برای کنترل از راه دور دسکتاپ باید به"), + ("config_screen", "بدهید \"screenshot\" مجوز RustDesk برای کنترل از راه دور دسکتاپ باید به"), + ("Installing ...", "...در حال نصب"), ("Install", "نصب"), ("Installation", "نصب و راه اندازی"), ("Installation Path", "محل نصب"), @@ -155,28 +155,28 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("agreement_tip", "با شروع نصب، شرایط توافق نامه مجوز را می پذیرید"), ("Accept and Install", "قبول و شروع نصب"), ("End-user license agreement", "قرارداد مجوز کاربر نهایی"), - ("Generating ...", "پدید آوردن..."), - ("Your installation is lower version.", "نسخه قبلی نصب شده است"), + ("Generating ...", "...در حال تولید"), + ("Your installation is lower version.", "نسخه قدیمی تری نصب شده است"), ("not_close_tcp_tip", "هنگام استفاده از تونل این پنجره را نبندید"), - ("Listening ...", "انتظار..."), - ("Remote Host", "دستگاه از راه دور"), + ("Listening ...", "...انتظار"), + ("Remote Host", "هاست راه دور"), ("Remote Port", "پورت راه دور"), ("Action", "عملیات"), ("Add", "افزودن"), ("Local Port", "پورت محلی"), ("Local Address", "آدرس محلی"), ("Change Local Port", "تغییر پورت محلی"), - ("setup_server_tip", "برای اتصال سریعتر، سرور اتصال خود را راه اندازی کنید"), + ("setup_server_tip", "برای اتصال سریعتر، سرور اتصال ضخصی خود را راه اندازی کنید"), ("Too short, at least 6 characters.", "بسیار کوتاه حداقل 6 کاراکتر مورد نیاز است"), ("The confirmation is not identical.", "تأیید ناموفق بود."), ("Permissions", "دسترسی ها"), ("Accept", "پذیرفتن"), ("Dismiss", "رد کردن"), ("Disconnect", "قطع اتصال"), - ("Allow using keyboard and mouse", "اجازه استفاده از صفحه کلید و ماوس را بدهید"), - ("Allow using clipboard", "را بدهید Clipboard اجازه استفاده از"), - ("Allow hearing sound", "اجازه شنیدن صدا را بدهید"), - ("Allow file copy and paste", "اجازه کپی و چسباندن فایل را بدهید"), + ("Allow using keyboard and mouse", "مجاز بودن استفاده از صفحه کلید و ماوس"), + ("Allow using clipboard", "مجاز بودن استفاده از کلیپبورد"), + ("Allow hearing sound", "مجاز بودن شنیدن صدا"), + ("Allow file copy and paste", "مجاز بودن کپی و چسباندن فایل"), ("Connected", "متصل شده"), ("Direct and encrypted connection", "اتصال مستقیم و رمزگذاری شده"), ("Relayed and encrypted connection", "و رمزگذاری شده Relay اتصال از طریق"), @@ -184,7 +184,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Relayed and unencrypted connection", "و رمزگذاری نشده Relay اتصال از طریق"), ("Enter Remote ID", "شناسه از راه دور را وارد کنید"), ("Enter your password", "زمر عبور خود را وارد کنید"), - ("Logging in...", "در حال ورود..."), + ("Logging in...", "...در حال ورود"), ("Enable RDP session sharing", "اشتراک گذاری جلسه RDP را فعال کنید"), ("Auto Login", "ورود خودکار"), ("Enable Direct IP Access", "دسترسی مستقیم IP را فعال کنید"), @@ -196,7 +196,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please enter the folder name", "نام پوشه را وارد کنید"), ("Fix it", "بازسازی"), ("Warning", "هشدار"), - ("Login screen using Wayland is not supported", "ورود به سیستم با استفاده از Wayland پشتیبانی نمی شود"), + ("Login screen using Wayland is not supported", "پشتیبانی نمی شود Wayland ورود به سیستم با استفاده از "), ("Reboot required", "راه اندازی مجدد مورد نیاز است"), ("Unsupported display server ", "سرور تصویر پشتیبانی نشده است"), ("x11 expected", ""), @@ -205,16 +205,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Username", "نام کاربری"), ("Invalid port", "پورت نامعتبر است"), ("Closed manually by the peer", "به صورت دستی توسط میزبان بسته شد"), - ("Enable remote configuration modification", "تغییرات پیکربندی از راه دور را مجاز کنید"), + ("Enable remote configuration modification", "فعال بودن اعمال تغییرات پیکربندی از راه دور"), ("Run without install", "بدون نصب اجرا شود"), ("Always connected via relay", "متصل است Relay همیشه با"), - ("Always connect via relay", "برای اتصال استفاده کنید Relay از"), - ("whitelist_tip", "فقط آدرس های IP مجاز می توانند به این دسکتاپ متصل شوند"), + ("Always connect via relay", "برای اتصال استفاده شود Relay از"), + ("whitelist_tip", "های مجاز می توانند به این دسکتاپ متصل شوند IP فقط"), ("Login", "ورود"), ("Logout", "خروج"), ("Tags", "برچسب ها"), ("Search ID", "جستجوی شناسه"), - ("Current Wayland display server is not supported", "سرور نمای فعلی Wayland پشتیبانی نمی شود"), + ("Current Wayland display server is not supported", "پشتیبانی نمی شود Wayland سرور نمایش فعلی"), ("whitelist_sep", "با کاما، نقطه ویرگول، فاصله یا خط جدید از هم جدا می شوند"), ("Add ID", "افزودن شناسه"), ("Add Tag", "افزودن برچسب"), @@ -223,19 +223,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Username missed", "نام کاربری وجود ندارد"), ("Password missed", "رمزعبور وجود ندارد"), ("Wrong credentials", "اعتبارنامه نادرست است"), - ("Edit Tag", "برچسب را تغییر دهید"), - ("Unremember Password", "رمز عبور را ذخیره نکنید"), - ("Favorites", "موارد دلخواه"), + ("Edit Tag", "ویرایش برچسب"), + ("Unremember Password", "رمز عبور ذخیره نشود"), + ("Favorites", "اتصالات دلخواه"), ("Add to Favorites", "افزودن به علاقه مندی ها"), ("Remove from Favorites", "از علاقه مندی ها حذف شود"), ("Empty", "موردی وجود ندارد"), ("Invalid folder name", "نام پوشه نامعتبر است"), ("Socks5 Proxy", "Socks5 Proxy"), - ("Hostname", "Hostname"), + ("Hostname", "نام هاست"), ("Discovered", "پیدا شده"), ("install_daemon_tip", "برای شروع در هنگام راه اندازی، باید سرویس سیستم را نصب کنید"), - ("Remote ID", "شناسه از راه دور"), - ("Paste", "درج کنید"), + ("Remote ID", "شناسه راه دور"), + ("Paste", "درج"), ("Paste here?", "اینجا درج شود؟"), ("Are you sure to close the connection?", "آیا مطمئن هستید که می خواهید اتصال را پایان دهید؟"), ("Download new version", "دانلود نسخه جدید"), @@ -244,7 +244,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("One-Finger Tap", "با یک انگشت لمس کنید"), ("Left Mouse", "دکمه سمت چپ ماوس"), ("One-Long Tap", "لمس طولانی با یک انگشت"), - ("Two-Finger Tap", "با دو انگشت لمس کنید"), + ("Two-Finger Tap", "لمس دو انگشتی"), ("Right Mouse", "دکمه سمت راست ماوس"), ("One-Finger Move", "با یک انگشت حرکت کنید"), ("Double Tap & Move", "دو ضربه سریع بزنید و حرکت دهید"), @@ -253,7 +253,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Mouse Wheel", "چرخ ماوس"), ("Two-Finger Move", "با دو انگشت حرکت کنید"), ("Canvas Move", ""), - ("Pinch to Zoom", "زوم را کوچک کنید"), + ("Pinch to Zoom", "با دو انگشت بکشید تا زوم شود"), ("Canvas Zoom", ""), ("Reset canvas", ""), ("No permission of file transfer", "مجوز انتقال فایل داده نشده"), @@ -264,14 +264,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OPEN", "باز کردن"), ("Chat", "چت"), ("Total", "مجموع"), - ("items", "موارد"), + ("items", "آیتم ها"), ("Selected", "انتخاب شده"), ("Screen Capture", "ضبط صفحه"), ("Input Control", "کنترل ورودی"), ("Audio Capture", "ضبط صدا"), ("File Connection", "ارتباط فایل"), ("Screen Connection", "ارتباط صفحه"), - ("Do you accept?", "شما می پذیرید؟"), + ("Do you accept?", "آیا می پذیرید؟"), ("Open System Setting", "باز کردن تنظیمات سیستم"), ("How to get Android input permission?", "چگونه مجوز ورود به سیستم اندروید را دریافت کنیم؟"), ("android_input_permission_tip1", "برای اینکه یک دستگاه راه دور بتواند دستگاه Android شما را از طریق ماوس یا لمسی کنترل کند، باید به RustDesk اجازه دهید از ویژگی \"Accessibility\" استفاده کند."), @@ -283,7 +283,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_start_service_tip", "برای شروع سرویس اشتراک‌گذاری صفحه، روی مجوز \"شروع مرحله‌بندی سرور\" یا OPEN \"Screen Capture\" کلیک کنید."), ("Account", "حساب کاربری"), ("Overwrite", "بازنویسی"), - ("This file exists, skip or overwrite this file?", "این فایل وجود دارد، از فایل رد شود یا بازنویسی شود؟"), + ("This file exists, skip or overwrite this file?", "این فایل وجود دارد، از فایل رد شود یا آن را بازنویسی کند؟"), ("Quit", "خروج"), ("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"), ("Help", "راهنما"), @@ -294,27 +294,27 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Peer denied", "توسط میزبان راه دور رد شد"), ("Please install plugins", "لطفا افزونه ها را نصب کنید"), ("Peer exit", "میزبان خارج شد"), - ("Failed to turn off", "خاموش کردن با موفقیت انجام نشد"), + ("Failed to turn off", "خاموش کردن انجام نشد"), ("Turned off", "خاموش شد"), ("In privacy mode", "در حالت حریم خصوصی"), ("Out privacy mode", "خارج از حالت حریم خصوصی"), ("Language", "زبان"), - ("Keep RustDesk background service", "سرویس RustDesk را در پس زمینه نگه دارید"), - ("Ignore Battery Optimizations", "بهینه سازی باتری را نادیده بگیرید"), + ("Keep RustDesk background service", "را در پس زمینه نگه دارید RustDesk سرویس"), + ("Ignore Battery Optimizations", "بهینه سازی باتری نادیده گرفته شود"), ("android_open_battery_optimizations_tip", "به صفحه تنظیمات بعدی بروید"), ("Connection not allowed", "اتصال مجاز نیست"), - ("Legacy mode", "پشتیبانی موارد قدیمی"), - ("Map mode", "حالت نقشه"), + ("Legacy mode", "legacy حالت"), + ("Map mode", "map حالت"), ("Translate mode", "حالت ترجمه"), - ("Use permanent password", "از رمز عبور دائمی استفاده کنید"), - ("Use both passwords", "از هر دو رمز عبور استفاده کنید"), - ("Set permanent password", "یک رمز عبور دائمی تنظیم کنید"), - ("Enable Remote Restart", "فعال کردن راه‌اندازی مجدد از راه دور"), - ("Allow remote restart", "اجازه راه اندازی مجدد از راه دور"), - ("Restart Remote Device", "راه‌اندازی مجدد دستگاه از راه دور"), + ("Use permanent password", "از رمز عبور دائمی استفاده شود"), + ("Use both passwords", "از هر دو رمز عبور استفاده شود"), + ("Set permanent password", "یک رمز عبور دائمی تنظیم شود"), + ("Enable Remote Restart", "فعال کردن قابلیت ریستارت از راه دور"), + ("Allow remote restart", "مجاز بودن ریستارت از راه دور"), + ("Restart Remote Device", "ریستارت کردن از راه دور"), ("Are you sure you want to restart", "ایا مطمئن هستید میخواهید راه اندازی مجدد انجام بدید؟"), - ("Restarting Remote Device", "راه اندازی مجدد یک دستگاه راه دور"), - ("remote_restarting_tip", "دستگاه راه دور دوباره راه اندازی می شود. این پیام را ببندید و پس از مدتی با استفاده از یک رمز عبور دائمی دوباره وصل شوید."), + ("Restarting Remote Device", "در حال راه اندازی مجدد دستگاه راه دور"), + ("remote_restarting_tip", "دستگاه راه دور در حال راه اندازی مجدد است. این پیام را ببندید و پس از مدتی با استفاده از یک رمز عبور دائمی دوباره وصل شوید."), ("Copied", "کپی شده است"), ("Exit Fullscreen", "از حالت تمام صفحه خارج شوید"), ("Fullscreen", "تمام صفحه"), @@ -340,25 +340,25 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Dark Theme", "نمایه تیره"), ("Dark", "تیره"), ("Light", "روشن"), - ("Follow System", "سیستم را دنبال کنید"), - ("Enable hardware codec", "از کدک سخت افزاری استفاده کنید"), - ("Unlock Security Settings", "تنظیمات امنیتی را باز کنید"), - ("Enable Audio", "صدا را روشن کنید"), - ("Unlock Network Settings", "باز کردن قفل تنظیمات شبکه"), + ("Follow System", "پیروی از سیستم"), + ("Enable hardware codec", "فعال سازی کدک سخت افزاری"), + ("Unlock Security Settings", "آنلاک شدن تنظیمات امنیتی"), + ("Enable Audio", "فعال شدن صدا"), + ("Unlock Network Settings", "آنلاک شدن تنظیمات شبکه"), ("Server", "سرور"), - ("Direct IP Access", "دسترسی مستقیم به IP"), + ("Direct IP Access", "IP دسترسی مستقیم "), ("Proxy", "پروکسی"), ("Port", "پورت"), ("Apply", "ثبت"), - ("Disconnect all devices?", "همه دستگاه ها را غیرفعال کنید؟"), + ("Disconnect all devices?", "همه دستگاه ها قطع شوند؟"), ("Clear", "پاک کردن"), ("Audio Input Device", "منبع صدا"), ("Deny remote access", "دسترسی از راه دور را رد کنید"), - ("Use IP Whitelisting", "از لیست سفید IP استفاده کنید"), + ("Use IP Whitelisting", "های مجاز IP استفاده از"), ("Network", "شبکه"), - ("Enable RDP", "RDP را فعال کنید"), - ("Pin menubar", "نوار منو ثابت کنید"), - ("Unpin menubar", "پین نوار منو را بردارید"), + ("Enable RDP", "RDP فعال شدن"), + ("Pin menubar", "پین کردن نوار منو"), + ("Unpin menubar", "آنپین کردن نوار منو"), ("Recording", "در حال ضبط"), ("Directory", "مسیر"), ("Automatically record incoming sessions", "ضبط خودکار جلسات ورودی"), @@ -366,7 +366,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Start session recording", "شروع ضبط جلسه"), ("Stop session recording", "توقف ضبط جلسه"), ("Enable Recording Session", "فعالسازی ضبط جلسه"), - ("Allow recording session", "مجوز ضبط جلسه"), + ("Allow recording session", "مجومجاز بودن ضبط جلسه"), ("Enable LAN Discovery", "فعالسازی جستجو در شبکه"), ("Deny LAN Discovery", "غیر فعالسازی جستجو در شبکه"), ("Write a message", "یک پیام بنویسید"), @@ -374,26 +374,26 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please wait for confirmation of UAC...", ""), ("elevated_foreground_window_tip", ""), ("Disconnected", "قطع ارتباط"), - ("Other", "دیگر"), - ("Confirm before closing multiple tabs", "بستن چندین برگه را تأیید کنید"), + ("Other", "سایر"), + ("Confirm before closing multiple tabs", "تایید بستن دسته ای برگه ها"), ("Keyboard Settings", "تنظیمات صفحه کلید"), ("Custom", "سفارشی"), ("Full Access", "دسترسی کامل"), ("Screen Share", "اشتراک گذاری صفحه"), - ("Wayland requires Ubuntu 21.04 or higher version.", ""), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", ""), + ("Wayland requires Ubuntu 21.04 or higher version.", "نیازمند اوبونتو نسخه 21.04 یا بالاتر است Wayland"), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "استفاده کنید و یا سیستم عامل خود را تغییر دهید X11 نیازمند نسخه بالاتری از توزیع لینوکس است. لطفا از دسکتاپ با سیستم"), ("JumpLink", ""), ("Please Select the screen to be shared(Operate on the peer side).", "لطفاً صفحه‌ای را برای اشتراک‌گذاری انتخاب کنید (در سمت همتا به همتا کار کنید)."), - ("Show RustDesk", "RustDesk را نشان دهید"), + ("Show RustDesk", "RustDesk نمایش"), ("This PC", "This PC"), ("or", "یا"), ("Continue with", "ادامه با"), - ("Elevate", ""), - ("Zoom cursor", "نشانگر بزرگنمایی"), + ("Elevate", "افزایش سطح"), + ("Zoom cursor", " بزرگنمایی نشانگر ماوس"), ("Accept sessions via password", "قبول درخواست با رمز عبور"), ("Accept sessions via click", "قبول درخواست با کلیک موس"), ("Accept sessions via both", "قبول درخواست با هر دو"), - ("Please wait for the remote side to accept your session request...", "لطفا صبر کنید تا میزبان درخواست شما را قبول کند..."), + ("Please wait for the remote side to accept your session request...", "...لطفا صبر کنید تا میزبان درخواست شما را قبول کند"), ("One-time Password", "رمز عبور یکبار مصرف"), ("Use one-time password", "استفاده از رمز عبور یکبار مصرف"), ("One-time password length", "طول رمز عبور یکبار مصرف"), From 2d6075189d9f6ca26ae8ccaafedb882206a09429 Mon Sep 17 00:00:00 2001 From: KG7x Date: Sun, 25 Dec 2022 19:23:03 +0300 Subject: [PATCH 1251/2015] Cleanup --- flutter/build_android_deps.sh | 16 +++--- flutter/web/js/gen_js_from_hbb.py | 4 +- flutter/web/js/src/codec.js | 4 +- flutter/windows/runner/win32_window.cpp | 2 +- libs/enigo/.vscode/launch.json | 2 +- .../dylib/src/win10/IddController.c | 2 +- .../dylib/src/win10/IddController.h | 20 +++---- res/gen_icon.sh | 2 +- res/lang.py | 22 ++++---- src/ui/cm.css | 6 +-- src/ui/cm.html | 2 +- src/ui/common.css | 6 +-- src/ui/file_transfer.css | 52 +++++++++---------- src/ui/header.css | 2 +- 14 files changed, 71 insertions(+), 71 deletions(-) diff --git a/flutter/build_android_deps.sh b/flutter/build_android_deps.sh index f120346cf..a30abd154 100755 --- a/flutter/build_android_deps.sh +++ b/flutter/build_android_deps.sh @@ -1,7 +1,7 @@ #!/bin/bash -# Build libyuv / opus / libvpx / oboe for Android -# Required: +# Build libyuv / opus / libvpx / oboe for Android +# Required: # 1. set VCPKG_ROOT / ANDROID_NDK path environment variables # 2. vcpkg initialized # 3. ndk, version: 22 (if ndk < 22 you need to change LD as `export LD=$TOOLCHAIN/bin/$NDK_LLVM_TARGET-ld`) @@ -23,7 +23,7 @@ HOST_TAG="linux-x86_64" # current platform, set as `ls $ANDROID_NDK/toolchains/l TOOLCHAIN=$ANDROID_NDK/toolchains/llvm/prebuilt/$HOST_TAG function build { - ANDROID_ABI=$1 + ANDROID_ABI=$1 VCPKG_TARGET=$2 NDK_LLVM_TARGET=$3 LIBVPX_TARGET=$4 @@ -111,15 +111,15 @@ patch -N -d build/oboe -p1 < ../src/oboe.patch # x86_64-linux-android # i686-linux-android -# LIBVPX_TARGET : -# arm64-android-gcc -# armv7-android-gcc +# LIBVPX_TARGET : +# arm64-android-gcc +# armv7-android-gcc # x86_64-android-gcc -# x86-android-gcc +# x86-android-gcc # args: ANDROID_ABI VCPKG_TARGET NDK_LLVM_TARGET LIBVPX_TARGET build arm64-v8a arm64-android aarch64-linux-android arm64-android-gcc -build armeabi-v7a arm-android arm-linux-androideabi armv7-android-gcc +build armeabi-v7a arm-android arm-linux-androideabi armv7-android-gcc # rm -rf build/libvpx # rm -rf build/oboe \ No newline at end of file diff --git a/flutter/web/js/gen_js_from_hbb.py b/flutter/web/js/gen_js_from_hbb.py index 0bdde54e4..8ee553b35 100755 --- a/flutter/web/js/gen_js_from_hbb.py +++ b/flutter/web/js/gen_js_from_hbb.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -import re +import re import os import glob from tabnanny import check @@ -69,7 +69,7 @@ def main(): for ln in open('../../../Cargo.toml', encoding='utf-8'): if ln.startswith('version ='): print('export const ' + ln) - + def removeComment(ln): return re.sub('\s+\/\/.*$', '', ln) diff --git a/flutter/web/js/src/codec.js b/flutter/web/js/src/codec.js index dc579b5f3..27c9565ec 100644 --- a/flutter/web/js/src/codec.js +++ b/flutter/web/js/src/codec.js @@ -22,8 +22,8 @@ import { simd } from "wasm-feature-detect"; export async function loadVp9(callback) { - // Multithreading is used only if `options.threading` is true. - // This requires browser support for the new `SharedArrayBuffer` and `Atomics` APIs, + // Multithreading is used only if `options.threading` is true. + // This requires browser support for the new `SharedArrayBuffer` and `Atomics` APIs, // currently available in Firefox and Chrome with experimental flags enabled. // 所有主流浏览器均默认于2018年1月5日禁用SharedArrayBuffer const isSIMD = await simd(); diff --git a/flutter/windows/runner/win32_window.cpp b/flutter/windows/runner/win32_window.cpp index 9ada9ab2e..2ff6d686c 100644 --- a/flutter/windows/runner/win32_window.cpp +++ b/flutter/windows/runner/win32_window.cpp @@ -116,7 +116,7 @@ bool Win32Window::CreateAndShow(const std::wstring& title, HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); double scale_factor = dpi / 96.0; - + HWND window = CreateWindow( window_class, title.c_str(), WS_OVERLAPPEDWINDOW, Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), diff --git a/libs/enigo/.vscode/launch.json b/libs/enigo/.vscode/launch.json index a7a40dcfe..123e0bc42 100644 --- a/libs/enigo/.vscode/launch.json +++ b/libs/enigo/.vscode/launch.json @@ -1,7 +1,7 @@ { "version": "0.2.0", "configurations": [ - + { "name": "Debug", "type": "gdb", diff --git a/libs/virtual_display/dylib/src/win10/IddController.c b/libs/virtual_display/dylib/src/win10/IddController.c index a30fa9d0a..27dd22792 100644 --- a/libs/virtual_display/dylib/src/win10/IddController.c +++ b/libs/virtual_display/dylib/src/win10/IddController.c @@ -221,7 +221,7 @@ BOOL DeviceCreate(PHSWDEVICE hSwDevice) SW_DEVICE_CREATE_INFO createInfo = { 0 }; PCWSTR description = L"RustDesk Idd Driver"; - // These match the Pnp id's in the inf file so OS will load the driver when the device is created + // These match the Pnp id's in the inf file so OS will load the driver when the device is created PCWSTR instanceId = L"RustDeskIddDriver"; PCWSTR hardwareIds = L"RustDeskIddDriver\0\0"; PCWSTR compatibleIds = L"RustDeskIddDriver\0\0"; diff --git a/libs/virtual_display/dylib/src/win10/IddController.h b/libs/virtual_display/dylib/src/win10/IddController.h index f92f72647..f7f3df3f5 100644 --- a/libs/virtual_display/dylib/src/win10/IddController.h +++ b/libs/virtual_display/dylib/src/win10/IddController.h @@ -14,7 +14,7 @@ extern "C" { * @param rebootRequired [out] Indicates whether a restart is required. * * @return TRUE/FALSE. If FALSE returned, error message can be retrieved by GetLastMsg() - * + * * @see GetLastMsg#GetLastMsg */ BOOL InstallUpdate(LPCTSTR fullInfPath, PBOOL rebootRequired); @@ -34,11 +34,11 @@ BOOL Uninstall(LPCTSTR fullInfPath, PBOOL rebootRequired); /** * @brief Check if RustDeskIddDriver device is created before. * The driver device(adapter) should be single instance. - * + * * @param created [out] Indicates whether the device is created before. * * @return TRUE/FALSE. If FALSE returned, error message can be retrieved by GetLastMsg() - * + * * @see GetLastMsg#GetLastMsg * */ @@ -48,11 +48,11 @@ BOOL IsDeviceCreated(PBOOL created); * @brief Create device. * Only one device should be created. * If device is installed ealier, this function returns FALSE. - * + * * @param hSwDevice [out] Handler of software device, used by DeviceCreate(). Should be **NULL**. * * @return TRUE/FALSE. If FALSE returned, error message can be retrieved by GetLastMsg() - * + * * @see GetLastMsg#GetLastMsg * */ @@ -79,9 +79,9 @@ VOID DeviceClose(HSWDEVICE hSwDevice); * 1 means doing once and retry one time... * * @return TRUE/FALSE. If FALSE returned, error message can be retrieved by GetLastMsg() - * + * * @see GetLastMsg#GetLastMsg - * + * * @remark Plug in monitor may fail if device is created in a very short time. * System need some time to prepare the device. * @@ -94,7 +94,7 @@ BOOL MonitorPlugIn(UINT index, UINT edid, INT retries); * @param index [in] Monitor index, should be 0, 1, 2. * * @return TRUE/FALSE. If FALSE returned, error message can be retrieved by GetLastMsg() - * + * * @see GetLastMsg#GetLastMsg * */ @@ -133,9 +133,9 @@ const char* GetLastMsg(); * @brief Set if print error message when debug. * * @param b [in] TRUE to enable printing message. - * + * * @remark For now, no need to read evironment variable to check if should print. - * + * */ VOID SetPrintErrMsg(BOOL b); diff --git a/res/gen_icon.sh b/res/gen_icon.sh index 40b67aa53..83252a6ae 100644 --- a/res/gen_icon.sh +++ b/res/gen_icon.sh @@ -3,5 +3,5 @@ for size in 16 32 64 128 256 512 1024; do #inkscape -z -o $size.png -w $size -h $size icon.svg >/dev/null 2>/dev/null convert icon.png -resize ${size}x${size} app_icon_$size.png done -# from ImageMagick +# from ImageMagick #/bin/rm 16.png 32.png 48.png 128.png 256.png diff --git a/res/lang.py b/res/lang.py index 5aa6f4d15..974449f2a 100644 --- a/res/lang.py +++ b/res/lang.py @@ -4,22 +4,22 @@ import os import glob import sys import csv - -def get_lang(lang): - out = {} + +def get_lang(lang): + out = {} for ln in open('./src/lang/%s.rs'%lang): ln = ln.strip() if ln.startswith('("'): k, v = line_split(ln) out[k] = v - return out + return out def line_split(line): - toks = line.split('", "') + toks = line.split('", "') if len(toks) != 2: print(line) assert(0) - k = toks[0][2:] + k = toks[0][2:] v = toks[1][:-3] return k, v @@ -34,8 +34,8 @@ def main(): def expand(): - for fn in glob.glob('./src/lang/*'): - lang = os.path.basename(fn)[:-3] + for fn in glob.glob('./src/lang/*'): + lang = os.path.basename(fn)[:-3] if lang in ['en','cn']: continue print(lang) dict = get_lang(lang) @@ -52,11 +52,11 @@ def expand(): else: fw.write(line) fw.close() - + def to_csv(): - for fn in glob.glob('./src/lang/*.rs'): - lang = os.path.basename(fn)[:-3] + for fn in glob.glob('./src/lang/*.rs'): + lang = os.path.basename(fn)[:-3] csvfile = open('./src/lang/%s.csv'%lang, "wt") csvwriter = csv.writer(csvfile) for line in open(fn): diff --git a/src/ui/cm.css b/src/ui/cm.css index ff4d422e4..960c8b567 100644 --- a/src/ui/cm.css +++ b/src/ui/cm.css @@ -45,7 +45,7 @@ div.right-panel { div.icon-and-id { flow: horizontal; - border-spacing: 1em; + border-spacing: 1em; } div.icon { @@ -64,7 +64,7 @@ div.id { div.permissions { flow: horizontal; - border-spacing: 0.5em; + border-spacing: 0.5em; } div.permissions > div { @@ -141,7 +141,7 @@ button.elevate>span { } button.elevate>span>span { - margin-left:*; + margin-left:*; margin-right:*; } diff --git a/src/ui/cm.html b/src/ui/cm.html index 4edb4a762..aabaa0294 100644 --- a/src/ui/cm.html +++ b/src/ui/cm.html @@ -4,7 +4,7 @@ @import url(common.css); @import url(cm.css); - diff --git a/src/ui/common.css b/src/ui/common.css index 1814ad32d..0fb9afcb1 100644 --- a/src/ui/common.css +++ b/src/ui/common.css @@ -56,7 +56,7 @@ button[type=checkbox], button[type=checkbox]:active { button.outline { border: color(border) solid 1px; - background: transparent; + background: transparent; color: color(text); } @@ -115,7 +115,7 @@ textarea:empty { .base:disabled { background: transparent; } .slider:hover { background: grey; } .slider:active { background: grey; } - .base { size: 16px; } + .base { size: 16px; } .corner { background: white; } } @@ -185,7 +185,7 @@ header div.window-icon icon { header caption { size: *; -} +} @media platform != "OSX" { button.window { diff --git a/src/ui/file_transfer.css b/src/ui/file_transfer.css index 9b45ea2b7..7fd4ac7a8 100644 --- a/src/ui/file_transfer.css +++ b/src/ui/file_transfer.css @@ -12,22 +12,22 @@ div#file-transfer { } table -{ +{ font: system; border: 1px solid color(border); flow: table-fixed; prototype: Grid; size: *; padding:0; - border-spacing: 0; + border-spacing: 0; overflow-x: auto; overflow-y: hidden; } - -table > thead { + +table > thead { behavior: column-resizer; border-bottom: color(border) solid 1px; -} +} table > tbody { behavior: select-multiple; @@ -41,20 +41,20 @@ table th { } table th -{ +{ padding: 4px; foreground-repeat: no-repeat; foreground-position: 50% 3px auto auto; border-left: color(border) solid 1px; -} +} -table th.sortable[sort=asc] -{ +table th.sortable[sort=asc] +{ foreground-image: url(stock:arrow-down); -} +} table th.sortable[sort=desc] -{ +{ foreground-image: url(stock:arrow-up); } @@ -81,10 +81,10 @@ table.has_current thead th:current { table tr:nth-child(odd) { background-color: white; } /* each odd row */ table tr:nth-child(even) { background-color: #F4F5F6; } /* each even row */ -table.has_current tr:current /* current row */ -{ - background-color: color(accent); -} +table.has_current tr:current /* current row */ +{ + background-color: color(accent); +} table.has_current tbody tr:checked { @@ -95,9 +95,9 @@ table.has_current tbody tr:checked td { color: highlighttext; } -table td -{ - padding: 4px; +table td +{ + padding: 4px; text-align: left; font-size: 1em; height: 1.4em; @@ -124,11 +124,11 @@ table td:nth-child(4) { section { size: *; margin: 1em; - border-spacing: 0.5em; + border-spacing: 0.5em; } table td:nth-child(1) { - foreground-repeat: no-repeat; + foreground-repeat: no-repeat; foreground-position: 50% 50% } @@ -160,11 +160,11 @@ div.toolbar > div.button:hover { div.toolbar > div.send { flow: horizontal; - border-spacing: 0.5em; + border-spacing: 0.5em; } div.remote > div.send svg { - transform: scale(-1, 1); + transform: scale(-1, 1); } div.navbar { @@ -207,7 +207,7 @@ table.job-table tr td { padding: 0.5em 1em; border-bottom: color(border) 1px solid; flow: horizontal; - border-spacing: 1em; + border-spacing: 1em; height: 3em; overflow-x: hidden; } @@ -217,11 +217,11 @@ table.job-table tr svg { } table.job-table tr.is_remote svg { - transform: scale(-1, 1); + transform: scale(-1, 1); } table.job-table tr.is_remote div.svg_continue svg { - transform: scale(1, 1); + transform: scale(1, 1); } table.job-table tr td div.text { @@ -246,7 +246,7 @@ table#port-forward thead tr th { table#port-forward tr td { height: 3em; - text-align: left; + text-align: left; } table#port-forward input[type=text], table#port-forward input[type=number] { diff --git a/src/ui/header.css b/src/ui/header.css index e248b46d5..8fe408612 100644 --- a/src/ui/header.css +++ b/src/ui/header.css @@ -8,7 +8,7 @@ header #screens { height: 22px; border-radius: 4px; flow: horizontal; - border-spacing: 0.5em; + border-spacing: 0.5em; padding-right: 1em; position: relative; } From 4e7568dec1d86cf5f506596fc2ca5533ad6a04da Mon Sep 17 00:00:00 2001 From: KG7x Date: Sun, 25 Dec 2022 19:36:51 +0300 Subject: [PATCH 1252/2015] Optimize images (loseless) --- .../metadata/android/en-US/images/icon.png | Bin 6694 -> 6312 bytes .../en-US/images/phoneScreenshots/1.png | Bin 27937 -> 27006 bytes .../en-US/images/phoneScreenshots/2.png | Bin 330410 -> 326177 bytes .../en-US/images/phoneScreenshots/3.png | Bin 435468 -> 431697 bytes .../en-US/images/phoneScreenshots/4.png | Bin 133999 -> 126986 bytes .../en-US/images/sevenInchScreenshots/5.png | Bin 21781 -> 19742 bytes .../en-US/images/sevenInchScreenshots/6.png | Bin 638301 -> 462394 bytes .../en-US/images/sevenInchScreenshots/7.png | Bin 393680 -> 388078 bytes .../en-US/images/sevenInchScreenshots/8.png | Bin 282455 -> 273393 bytes .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 2521 -> 1605 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 1681 -> 1087 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 3274 -> 2097 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 4804 -> 3013 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 6268 -> 3799 bytes flutter/assets/logo.png | Bin 12213 -> 8643 bytes .../Icon-App-1024x1024@1x.png | Bin 12076 -> 10508 bytes .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin 364 -> 360 bytes .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin 574 -> 564 bytes .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin 811 -> 779 bytes .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin 467 -> 455 bytes .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin 806 -> 781 bytes .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin 1101 -> 1072 bytes .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin 574 -> 564 bytes .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin 997 -> 978 bytes .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin 1411 -> 1368 bytes .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin 1411 -> 1368 bytes .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin 2037 -> 1962 bytes .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin 939 -> 926 bytes .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin 1735 -> 1691 bytes .../Icon-App-83.5x83.5@2x.png | Bin 1901 -> 1839 bytes .../AppIcon.appiconset/app_icon_1024.png | Bin 15849 -> 13838 bytes .../AppIcon.appiconset/app_icon_128.png | Bin 1570 -> 1517 bytes .../AppIcon.appiconset/app_icon_16.png | Bin 354 -> 349 bytes .../AppIcon.appiconset/app_icon_256.png | Bin 3330 -> 3103 bytes .../AppIcon.appiconset/app_icon_32.png | Bin 569 -> 562 bytes .../AppIcon.appiconset/app_icon_512.png | Bin 6852 -> 6186 bytes .../AppIcon.appiconset/app_icon_64.png | Bin 909 -> 901 bytes flutter/web/icons/Icon-192.png | Bin 5781 -> 4103 bytes flutter/web/icons/Icon-512.png | Bin 17101 -> 12570 bytes flutter/web/icons/Icon-maskable-192.png | Bin 12422 -> 4106 bytes flutter/web/icons/Icon-maskable-512.png | Bin 23540 -> 12626 bytes libs/clipboard/docs/assets/scene3.png | Bin 7346 -> 5480 bytes libs/clipboard/docs/assets/win_A_B.png | Bin 53354 -> 43658 bytes libs/clipboard/docs/assets/win_B_A.png | Bin 54430 -> 43515 bytes res/128x128.png | Bin 1629 -> 1575 bytes res/128x128@2x.png | Bin 3042 -> 2760 bytes res/32x32.png | Bin 504 -> 493 bytes res/icon-margin.png | Bin 15172 -> 12179 bytes res/icon.png | Bin 15849 -> 12963 bytes res/mac-tray-dark.png | Bin 391 -> 275 bytes res/mac-tray-light.png | Bin 475 -> 270 bytes 51 files changed, 0 insertions(+), 0 deletions(-) diff --git a/fastlane/metadata/android/en-US/images/icon.png b/fastlane/metadata/android/en-US/images/icon.png index 543fe8346a6f14587816460dabb2e39d71c05a61..3668c7106125ac99104bc125a94186fc1487e58c 100644 GIT binary patch literal 6312 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 Hw+#6|BEv~o literal 6694 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 F{{yd3%jy6C diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png index 32e7b35541083c763666350f46cc5145790b53f1..e84ed4d21476ec59d1aee00da62b37f43ca26f1a 100644 GIT binary patch literal 27006 zcmc$_XINCtvM4+Q5(E(>3zD;dfWV++B}Wk?4oQ$Oz*3MXS&^XR90m}PAUO?#1PLM_ z8D@xU01*Tv=bY~N?!C{s_x!le_uN0<{OIXbRb5?OU0vN>wbna5T{Q|aCNc;FLZP9q ztPg=)0U;2wU{XRnL^r4e1%VKp=xIMu;YitK4WD9k`$B7#L2H&ktshFI<$Eh+^rmME zgL66kvs@;hZxr&+uHA!Es<_e^MX@Jtu*EHtaNZ*%zKUnX8aYqIbo=UUWoEywE41tc z6tuSj`^ZFfg^+t6FekokN2saOx$X1gv&+4ci_6Q)*10q1`orHl=jPQ1Asxrh(1%I= z#|GsGkw1=4FD|!^E_xQv)(Utek(DK0QCb?EiCmczOx@xR*C}=>GNS{Ni$D|6DPDA6z@zKe_n1c{aR!GBh+a zFfh>9*VoS%y}hli zt>dHP`i7>)#>VF6=JWIOmX_8(%gX};LnkLE1A{{`1AA$h{gIWE;)#Rok%O4Q{jt^4 zqw~wjz4M6aqvvCXdj0!yKlTLMcPSgUA?Pi}uRF>;duBriWq(dEyJyM2jwL&H*;{w6 z)^BU{?mfdE%p9Drom_ZL9CmD;1>=rBE}Zy+hke^;wJWDz*H0IYE)oX!`)zn z?ch-q3ia{h$Fj0AG#Z_kmseO=SYKc7=jZ3<=2lr*X=i80$;oMKZ0zajsiLBin3$NH zoUE&>tEQ$VDJeNPI4CD47aktY$Hx~E5>i}Tj6@=XgM-u3(jp@xg@uJJEiL2X;#^!@ ze0+SiUytsDjqm6F3JncCUp(B62D7uX_fn=0D;JLUj!s9mkG2nwJJv1^&tnSrV?XZ~ zH63B6PgBYc>bsAp7tT5`Cqp#@^TTr+bL+dSJ3o7;hfo6(En~RvlbG6}Kj6|x17^Al zM@2=&%E~Hv@tYe0u~*Pge(=NxTx)%eH)lw2&Uq0-(cjwt>(733BZKDq$9qwi95>hr zxPNcaA76ZMXAmlT{9O}pU>Mvij=&-ZWOX13FRni1pd5biUbK~Fj@$6z55B7(G2F!P zmr0LP80?DM7*DSz(kJJOpl|DkY<@O4CcND_kXCxL_6wgQy~h!?vRr`cECofTf*2Xx7LdSOsJN)L#hon#BFpPB+K&2eB;r2M#@MDrbw_90lrvYlrGc)3A z?YiO~Kk&;JyxV|r679@3WJnpC$n6YJ6R>ltOxMUy5G znZNVf8ziy#m_#7MAtan%cAX|fWWfx4m@#o%(@-q@E|IMZmfzNb&el{YpTPRNBJ;Ix z5Iw(}=uyI9eK3#brGUH&R$b@-ky$HI{Z8s_Nh>1`)??y8M9-to3N;u~4NYG}T|H#=&(4&zAhKHRKTwT52Ze4qx9?8Nx>Rs zWw?Kp4eY)qPxq)GS`20$3dlVtq*oW%ZbxMWb+g84y~d$Byrd;ABN81NN6CGB{#jwJ@7C>x zCoIDpI7tb2GVip&_Iy~UiSSA+8)x0th2wg_Y8{r|^F=&* z>x>HZi9ED3H&gX1v8@X^0`V?+>+B`?9iFG7%0ku!Jf zX=ucHw{9onb6QOoBW`wND4>6N6GQ)11?0?la#dA2*Dq05B@r4`B}{B4-UI_=oq8RU zEDG;*Bq#AEL-D<+SesLeY;p=33%LFrSOs7;<7J{~!9Si1C z?^?9r)#IqLlER4lcbIKks#iQkTs5@aWM(%pMk|Nf%w|job5eyK`d~4%#e!qJhRQX!b^3_zpP9Z>t@CH{oL^ zf#tC+9K4%BnmPLGvn4W)RXDnsDe5Z)%`LvyZR&&JC)<-^aFO z6X?6MRYhYV-BwpE^(L>sXLp9wPZyc1vGhoRyiY&qG$c_bbs)GZbrzML-MmMfUCUCY zH>9fKIy3Avh*0|V;%W-!ndnUakfom;7e%LK2sUhDxUXZYbW@4xM<(oto#>`g!xHKj z_mf}|I-L{kOlirX^j7W-L_D!A+zlKum9AlFR%^03^eOVrK-=-DN!`t4`Llqqjlza~ z_eG{E&}#`8g!F6Q&X=7l!-RduBtN*c0U75}9Qlt=rgF6Y+l7z{*hNTbG3(M15GPUey99n)#db0(RIeuL&Iwc{D8!BVJhDit(~+d@2^Kr} zK+9J|{ZL+mG?`b7(e}kAo7b%3KW_KOW$g|8M)CjK@AlgCW3LQ7s^DLfq`}YhZa}H@ z=nUhFJ3TH}5ns(I2#3PHCuYz2zW}IwPAFT=?V`jwCoVM}2aAnq(@9ug-cZ@RsrqQC zL2T>~QH8ekW$Xnc^Z^^=Y1~IM`{-F@QBqm-eV|nRW{!kja8;~pWzW7pNpt*bJ5|xE zlDf=^9!=QSRbGfsO@Dso>%89=D~z_--zq1Gs?}kZWck)UnLR5}5dG2tk_jtd^l{*C zHNl}C@5;+&y#JP)fadMxD2#RkCzP;ZZaah6ukaoFk6hbPKWxK(M3|(g!2NpgwDXJGEKJ^v! zq62+oB4o;fGJhZUgXA-(fI6nylD6Z}_?(rZi2SP;lz=eL#Rr4)1@aAHgFY zj_?97&6&H}<05I1$s&2e+}?Q}3aYMc%rqNeetj0a%?cFHnGXdKpTZfVUVC0h63nB@ z9=n2h06RS8g=frSM*wBkPxMaEtp~?`pwML2p=)r2l9kEOC;sKlg?j z#FP*r15dkCGK?nl&qu^=H=hf&v;c0YiwBv1W^3{+BBmmxNWU%M+v3t1b~T#~XKnX@ zHbpYMh+pD}-iJc(g8YSnbbrhvbZPF=_NS&vf22eKg`qVI>!h9R*#l#1RbeczX@yDk zty*39{+AbV?p5hf?|_zDb*)a?xC=;WBY7EPVf6Jwza=yx@#G}ItAja^18;Y{W%}Ag z+oM;=I*0e(sK^uw0E{1Adj`Ccok#JA@VnshqI)veS8r|3$bWz>gt8uvEN-Wv!7Dz8 zcZCW8v}^aZTehX`2tt<7^GaTTo;F_sNPW9bVOdkH(UdEjwUgNiw3#X63A?r`_L)^MA#D6SPnw>@;z}s){6g zh1;;LguR!|K;M4VC3Wlq7A#o#TK*b!BL%MQ?oN#g#j^EAk=R;CdCCGkR|U&%j+%F~ z#Bc-sgmn+@11nt^beU`^^TzK~G!W#?U!fofYZ4V!v(6ygS22YXF0n!BT5k{YUfhOB1Ua9fG8H3CvAjU-`_3Bu(p%q-6+Bf0&X^hh1DR@{Q!Ed>&ME zV;1g{0`)52u+HOYTLG#EG72@cVn9=YGM&Vr3l~u2K%j?6iv-FGx zY2t&H`CpU=kpYe91ornI`9EI?Pp{{=yyC^UR@kaV# zZ1=~*@4U_W-mPfW`EAl1$HOR!aItzc0J1@T564dWzbs)^WSutYv)?`bogo#>&6{X) zhrVP|_yxqY&*rP?9R{hA5+cRnY@W#V1A3`oZJRor8s9xYN8G$BWs z^K3Asu+4^7L!40g@}NhbI}i z;v^Vcxm!C;MCHX+vHlqHG$$%~wFw4O}z@bc-FRZv>!IWu}T?edU7c(Z()ra3d zxVOYFfGI>Y=IU98ut;Fgqpb1N5BY&yv(rEqr}7#X9Su_pGRlb$TGf*ol?j5^`1kFLzyLCxM7s^z^3NPof@k6n{}xB z;RR1WUFb$}HJ-Z&U{aypH#*>QrW~GB@%aIYgG>mTC4#qc3ZGDR(FXnd1G8%>m`nR5l|f zltRF967n}@jfv4`&`1MUj7(k~r-e}>YPKy4GSNZ|g%3%GMVM{mzSHSn_F7jfDlZ1@ z9JT2209RkS$MKEE3|4@@l}__tf}Fo)+28(2jV>WcLn|57$R-qti^-v{VlD!iu3wSawOwFIr-Od&lGcFu+U);sM4FwTapI5ebQTO7cs5x=E6udZGB zmB0Qi4!8d(ad8@2x4Q3Jn1J@$7f&IHjjg#9cv-VSsNj*Gf!<=XRK;H@bv~1%!btN- zBt%}(e4>$n^?T(dp&9R^Nj6N_Q-ejH8`GK@{cH*0+QD<_#e561gK#KUxK#VPB6=BL zCMOAw$6{;w(AV-&wG_55dKW&&UD5>n-f^q^A`GKd$VHJczU)%S>-YRZ0AC}0OT~h_ z)$%z!1r6gqsKE`sXZf7J-l{ienPV96YPXBlPAjV=+PV-wT{ORp_lGb>4VY7~M{uUn zTTh!g^S3c0OU`^ZB1G@~;;j*E=h-zb=A^dfpFbAr*w`v*ae#6JA68`<+AKpI4XzvuwLI8JB6iP3-X)v(}zum1sg)D=;9);g^Vltw0nwPBy#t4` z^6diM0=4cqsoAymXf<7A2&X^k4b7VxKkgTL1R?!qi5?GSWIMo{TCkSuR|+mE3{^(8 zX6?4F_z=x5wGiLhLTuf3ZFQdY+%nzL*`oHD52Fgu!`Q!I#xBZ!Lx}+~Q)VrE-%-<@ zvFYeHZLSj8U$Togbwh?rgLJ_(v?$r!cLPZcS*vTQm~XeuNDRgeB%5U{nu0hY5c3u| zYm@4D-`=tkm6>~An=SD4ZPLS>-VB;t11>J8*1k{45meaDDzu5#m_Fb8+V@wD*0G(a zbw0BWxd_8ATeWW{Evl|)GdrkvR4X3f5$xL&T;4jPvs|L&BW5f;;_Gs9W`eI-?@w}z zYt2faWif#9S}%^SdyxSv0P?W81D_D2b74gyy5Khy$vFF^}<1px${gWYAH;XkkRylnF1H(3b_A-~L z#S6=f$Zkw%8hx_csUL@Eqc49;IsWdCUw*ZP7nu7kCrT1X1RrGnddG*c3i%oQ^Nrw^ z%$CKL+Yn(3w1tGS)w`9Ul?@Wef<5YD?(n(UzfG(^BDmMRsx|W^(8S{f*)Z+xr;~SN zfG+diGm-<;&43@?SMD`0UfD<3f~qMus-EoH7qZzEq9tFXQ&~^mkp*mzW~%al{QF$J zJ4avS0d2Sq5|!?(>w0o=2VJ01b{XQ8*E)`C@vKrZ=J)elsD4Lr zJ3OT@CfojW=QOa$g&`AWj#*?8Mzn7-aM>~Um*_BKVY0T{lOrczmXA_+TE16XJO{l* zBZ(0umx`QrU18pUHuEBaz3fW{S296wG)t8W0}}oE^=mO_F!xxAO91B_15bq&l)bR# z%%PwR>P#vd-T6t?>s5q6u29f`82fNr0Na2!jv8@RM!yX4BGa+E&?z>0NS{!GhMS6>jI+l+Qr-lg_G|Vj~}w^&FfD}$Dyy@LC7co zw*AZ_c(cc)s(!oPY;?7p>1%H!vDxfZyxQ+#Q|9)jjuFh?h$B4lI=_B&@u#vs-tPei z+?e9v6$3{~86l!^XSWe0mw;lC7`)|-U)1@o;fd5sEhegqFS1&N7qxx7) z3-5z+D(?ct`{rFQuT7HHy;LLNNLio5!b0x9t-+$Tw@!cHR}D~ppS;$TR2{`X$-2Oz zYc|>a7rDH*H2_^54ZF+*0{;7@vN`PQFo4dAIT7{XY4=!iXU!_fekZXj7}oj zt94E>+;~$c_eeB^)vocTACr-BQ9!L?9dJ1=shMb6`YV~KFd^JjyeghunAxn1e#+!^ z8aZ7QNZl=J$Y>Evr8|Dpdv=C&eukFz*3BOX+x+`Oyun0I;*h z+fJt!9p|JC-#1D+xY~yrw%X*4I7db{3}hB}O~&gTf(K0=e1LfWb`R>I%j9aiS85l^ zbWl}~?nycJiPODap)4Tf_~CPzi@O9kJn3DazoWkgoeaZ^*-3$cIcCCo&x-4*SJGab znXIM3{3baO$@sbdeBUyp5gm$}ugz9rw*jkdCbe%%UO+FdUJzYSTo7JB14+tX3j^b` zEwe$hbgm^RfXU?za~n`KYN~dRMhq}PhoH3a+b3gx_nLTK5$Z0YvFpwaY|734-Ma|9 z)1bi7Obq_UO`QZt>%9FDgOH)^UEItMz*{CQ?4Z)3Y_v$ni$<-dQ#gD%A`L$QiqGR= z1^wHAmcv}4w`GjO@!H2}`6Oj+<`iMm9sutmx(8nRv^Chf>Ezt!rLEj^sq<9Uh69sR z$rGZ0l#}k%Zffm#PpMNgqf%qJxk;Lm$1jA8YVBBtcPMUTP0l-b2E2GPd_ZS~1*LO$UYhTw=O}pVcE?D>aluBMS z5;=OCD!1zc4BuB`UrkDXyb~qwSoQsf9!r1Oz)p*-e;~Ux&g;jR$cCr!D^#fk%0@_V zYb$6=$NKavyp?7aN1tX1{dtA0nZq!~Rt|WXB#qEvI1izkZbt>oH{7G1P|%3M(fgA;h(IVCRD>8;9x9LHNT`u=~sWb@F;sPP0BpNiND+w z1(Xk`#l5r9h3WW#{c-a+lc|lnP z5xe#JYm|zbe(-P&!jCq1T8;qGYpDU22p;S z%czBUVp6y~5HvYf1UaL`Hth4gS{-z^P=g*l>hxV7+#UO{>I?5mBWqQVh_0F`Kd+yW zZgq}_DT0tZivJV{!Z;Y@zlzzG#Q%EwZ<;)c|D6U;{XbF85I^$5vQgg*sF$au6Cj*| zHX2+CWG1p!n;%;`9&Av-jIAK>!w+7+XXQkmS2kKt9~_7Mg&LX0j;&1LwY#%^R)IS> z*z85q2Im~(ULj5k$}F!Fu_I1i-Wx|dfZ$6X+ThOz+pa}21!beyIs&SLU#m?h7A*Q! zDgl+(k?XzDbBvM5&vGL6(w0lFa-ap3)pDi}`4)I_vD>tUmuD%~07{AsJll<{go7!^ zO3;YY<@R4FTQKGW*?%Hd(0_-luKafp>wki%{vGMU$F%-K^?w1i#{AV(AjR!g@&wo{ zkp}ZtMjpkFL>OL!Iv`$;mPx4dftdiue2T8ew}R4t*0>hi*AbHrzd$#eF-Ub;c8LADdXi195ghU6@7?2^Y~ORdkkRm(VD9_0Ts02m=vF*5 zp!^YKUjKQoHuG>x8mt`M>|g}H{8YoVpilLs;PAz#XhBT99kL-zzV!FD)>Dq6RVQSc$i2(%$tZq(1C_Jt z26~3vy1zRD3tidVFkG=;wIsWwZ2J7elsR>5ysQ(N{u&2{6-%Ef;=>Q@ZeKdQ*9mC4MwfuOv`{)IJ; zIKPSkP>n2zBMlFr3RMh0qrVT0!@AT`OjNBHZ?fJ>e!nejJn8a@4{{Ky{z4nBNDpy| z%s?{+M_*lFcY07W>UG`P$UMq_U3KYNu)dj`nLZN(XPFq##wbe+OY}snzEsWlM3a(a zR07bp>Y7h+!iw$@RNmq2q0VB+trPm}24Zp(yvcZ+p~ZEHQB z94y+V2#Tb2F$HsRkln<333E#QDgcNT@8ss`EMzL8*Mzy4p8!12qts{s241rd9>&axyzMcLKJzIHUH z@-J&p+wECD6&Ch|kQO7Nj*KU)yqQ}3mbC;RYn@}#Tgjw???JLx%VTB{I1^U7$cMDo z3)XdSrE+9fI|~syCN5!9-_ZZp~n6p{|-V&5TY+>G^vNy2y z@8?Ty+JGsLwH|T|tYk`sZq&9`xD74M&bfgGc=C&h(3oF^i|F)Q!K2?x#mL4ZrKfftTcWy4GKsyyAHGuniSp&_k~ajzQMC zcM+#oddv9O@uc<>`kt|G1KC?BvS#ZRp2iD8xsm&L0s={`-7ktq*Ud5 z$B=C;MQgQTP_Ct=Qxw{L0`IM(SME1H#>-)heBv*qq=j@h^^cTAqVMd}lHuOb<1H1o z{inv6;YQt_y8CWp#6%}FNuK<%V;XFpbBaWAC4|RYuLjEgp@zF7zvYTGjw;-1JdX8! zmuN7H66^5VlsS(RX+gh7bNSKsC-d`-{%MQJi(^sDLE4uQWvMkrfBFtSVIyQ_gYLd7 z3$5@ETYF~jPW*w;e#Sr1syW$N0WlMhTD(T7gE=UNuuE|if5y+))m9DkhU0Tx)_CyB zkjP)hm)x;_(ury}YG#N%jZ`gMH-YvJR4rs3_T=p*jX1l4mS4cT@RfP`n|u)alFeHV z4h}feedXpi9Lf}}sL>$D>B}#vzJqt9t{RnZh(0{I*80+nRrK=@{u4TcimF&1qrvAF z%;PoJvh(xpiE5Q{Y*$QQbUzHyo#3$npa1S?Mphuv?=q>cD4fGvnJQqWZ+TK*elYhxJB@}t3*Y>H z{Oiy25Z2P@2!wb?>Whjr^baN%m5Tb*0-baq2npLKEq-!S#YESBPL&(mM2?GkxxP9O z2=8NDO&4sdX1kG_BL);4y?oJZ1BPwKN%k8dpQSu@GbhOD>@^7)_f?J{=t2e1C)d~(`{alDTYVc~Nr z6ytD$6s(*1`&^V-yrKZhU~vECtrhEl3Sl0~AKEjl3uB8Q&BGB=ZKOb8ns0$l>ZocS z{&*xLZ?3HnS`w|!ppE<5GpHvIlu)Q$MgG_(*_m6jHN#N^^Eh?Y`+7lCNJ`ioTH#&Q&*@#qqxsw``Fcyg)5 zjNjU9oIb!fZ9o^(?O$CA@qer39kG#@cL3$@B$5fGMSfre{@ngf5mcIBVW z`bv-73Quq7<2ySWgw=2$wD{%uX9R8DqimkU>udAdy#LHu=Cg50Q$(FRBnlM!Sv^uM zRs*Rw9zpYTj>!WG`%izqQ{i?FBI5w!xlc7HZNWPiCnEDsTHnp7AC#ew&(C>Y>HD|b zli_`u2L7~)SKMFQ+$?2bU}0)Y=30h8S4>UU;5-s^imWrz_Q8Zqk|26iirQ(>`|}(Aayy?)7f~n6l^rB z;lK0dQGuZ_L6DGW-i+duk*i;Va5*2J2KEVz(LM7LVrC(01r5w}90{4=DAfw?4#0?BT5a(5DY|<8^uc_>kcB zNHK9MAvCJ)g$S>;%@kW>qGC2rk2fN46B5>`y z2zio<=iMvT`@`=rMoxWOW_ejd1#6hP!gJy=4woK94rO?`RJ${*DNfPom)p>(*DG=` z^Xm=d7|BzviVRAZ3|eId^Zg2}{aw`gE9#y#&lCe}<17PD{qr4nH%+y{^P8f*9avteA-b%uZy%);^w$%n?Khr_wZ;cgPhLto*0oIh zvE{*feF3#6S<9iFg}F`yQK!?$8;W&wEi1Yhyp+p*_Nw|FY`LOoh1QmoM zFIo%Bd`Y|)TA?96;FOH|J1aJ(TtuVg0~}fAiGA(o)%U(=7pS|;Ygk=H8LhJw&7#n+ z2U(QHewDX*%A6ceFA?G>izYpJjA@=to=e`mdSQGFryW;Ti(Yda<&z&JxO(BxD}IUE z@Yl1)dE#TJuLL`}POUxf@^$n@y%(yqW`z{t3tL-yS9uND%dAx8Xbg$@d563ny871< z*0sFVJZ3hipsr+SDgLNq=O~V%dvkelAN;nvAPG=x*GP-jQ6roQ?6$R>!IPoXIHvGz z)crarA##XD2cB1-G3QH1YiJg&%9W~!3-sx^wPpF$IOtjAPs>m7zFvk($kdPhLq-$B z7I0M$omtLzFy|}eT4!YP0NqzMeafMjLY)TDCo*|rO+ne{7~i0`P?=Tsu=pdVgcH!)9TZ|HTXJ+S5f9a7 z;(x9KcNR|`Fl#lVsOP)7Z0H>iOZ=$g;05UZ4#8eea4dnI-jH~HcKPiINmTa7elsOd zfiRHkF}N0~JzpSY1tCi(o^??}X6sA6uJR4oGqE8rZ}@~tzXnLxIKwsBN!0m^ESU#p0HOB?+|tTIC6guQ?hXf;QhzM>5YD$ zy%c$eCTHh*eZm2OK5Ff0iQ#rhP&j{t&A@a%585%G=`cQ=HmWvjS&NaWn zm3#>H+VI!1bF!aepfl)VO%3&NA1rEieZ*kqS%~=STI|Q^_dY|DzL$KuN^ZdiGj<3A z%b3~fJJBc&wv4#}4jvA$_?Mb#G2u%^Cui^(Q+9PH*T{fobV;PCr}4izjL|E2lQ~a% z(D=>-&S4M6!Zpu}OaHWiUu%<5#Pjo{Fnb-jiW?bQ}8zML5$JvagSl zOHXR(4Y#!@iHp(kp>`-#@|?y+}6(3WCnB-4W`akjkmB<){(>7Mhc0$#^NUY&VvjWSD$$X zO^L{U>}Mh_KsB0`k9pK2um){(mj26*+WN|)9~ba&;_uh9`|T@)qNT4CScLIgd-npF zqo2{Fn!$;|*LO0+M$42l9LKeb9NB0lOBTV0NQu|YY9_V!o}q?Fnh5EOioG8>l@3Z- z5pv0BBA?JJ;{0xv43Ug^xe}gK9o&<@^}X!hcDvinrWA}@eik#{od45(q4v*DsDMiTl@b5M5;rGJn@c7+QVB%bTqQ7jNW@C6$-Oo;9{_NO3sfkjbD-J_n zet;<Th8xdh*sQ%R@gQ0wpG(WQf^A^)QZ$D%C*{5(4B*+ zn!azxee|d`3i`8F`SwCk@3JszB<>PE+x&VqDjKiOu|-(f#hMdnvMIhIeQMQ@;N;8gO4aN&8viY019| zZ4Y7nkn%0W=!4bT$z3acL)&oKJC+yLg)smG@|r!YzyCl;(KC2YdXhY^SK8>R366h^ zc+4Ka7e+t8R*=k;*#yqvOz15546p`5ODRgr;_$&zs0nlM-(oBKzp8OHFo-i@B`g^F zxBREdZU;cdh^+w#;XmcyLa_&glv1>U5Y>OmzeRB7fAMe~{5Q6g;;8fARam3vQ=$9U zt|&^}?3nvzhf3jwW1?bi+MH=vj4q10kBCH+!ye(NAgz#D zWiy-+(mMRKE&`GAJQbF1U_MO<`Es+RO!shV1q3wb-<}o)ELJ=e z%Es3fZ=|a|;j#>4m3uRiYPJtTPjwcyjSd<*@AfhmJ--{Ab z;Dy}Zk+n2HV4|^OhEBSNKKI!mtz*X$mTcImJY->63F>}f*Etyky%8&sd>drJ97Mr| zZ$e6w>)$zgtK$;is}n-toVj450hmn->D%XPM?r&IT3`SKADhI7?05bHndtCsI0r1N#0Uf~66 zE6GHDRsx^AzXEa9H=vVzgw))CLZ*J$(o5q{iTCj;hsnSHYyqO|u0m#A-WR>M0Q2N6@!+w+SWf&z9G{3of@@+$#;5u) z1qt(i=hQXIpjHdsYYTyRuZkIpY2$bx!HxZC_U$O0R0w1vu*!gh8$zH++tD-8b76f= z$^YJLRcIO*91lW9h;#nF0w(jXx4So=5bGsV4sXg*VX`S(BmbunE4 zPy3fJIP3plvM#d+dEi9&W`y`k5e2C}GgTgpT^&%21B2OXq5mMfHqIyOo%g@I=6^rG znXq|)Flf+^(7r34cYiFu&}JRe^m85K05iLS~YwkdttlR z>#(S#tk)N#gHz^Zc9uPlP~NZw%b(jYi=E~u-8`$^yYo426c+nt#UNJww|v3+ikbIg z-%Yh0e)NLjcPDt5E>fQJv5GZVE@VQ9M-*PS{%wW6cCzX4wAA<;K2m9f5cE^#JQM?r zN39$u9)8|9mthRIrCyZ_{$kiy;N!=MUA=~*8LQk2&^9kA}5GM z=P#U`-;=}Yi!cQ->U*1o_*CzksvQiC=jKo7L{^-FnHS`!yaHYIrByN)QxN+v5;&yt zCl6=j*jF|y=$0lm4szYx3SfhlzSqRdI(&&wQvtS`BVQScprSIjud;Q1!IhK&Bqq1U z{t<)-OP~IzODYK+N-VbbRpCk{kBa3cfzQUC6ZrwTmO-HoTRcBrrykF41=VwLW_#mv zhO7xM59KLybl>+weU|o|{K(noi%`j|@%)n7a9WmtbKkRt@(5#|mT7HJR=|5R{HcdV z6(!`r;^GI}xnQei#pi#BiL{XjaJz2vkoz6jpQlq}6)-_&%@b#`O}XxKy%3;K)b#*0 zW7Rz()6o3SK}xK|qa+E;!7W8gAth1X?~afC^OctMGm@4xKUJ3=Y3{s_J#URh;qT#( zr%-?ReYp({v-!fv5hWy|Tq#!>Ls~FnE^>j1`$>6ag}s3Ty6}?yOhJGKt2n{AT!0UG zL~`zSjKBEFHhrUFW%cnB9x-Fg0ex$RiDmmA53gDlf_q6L7`!E^gdsBccK95!-4Sq- zN{dAXi4+UK37uaBf)A~IIsXxW7ty7_icDYR;cyoRK0wyq0XGz}Wu@pb@(MINd5$VZ zo%sfq-ncJO-;(Dne1sh8ajyZCCp1a)_}hcPM!`BoFkw~Hw?CflwK{hQ1ixxCXT{(R zycoI`X~)?47FhyZZ*33-Jf!h)3Fj-7e>AGRAitdXjNyX8(*gz21AoKu=b|Th6Izg0 zZf>*qW`F7Lo0 zfUA%q4o#@3hzCb$f-7+|E~%finc0C?iU!8{-9hoNIG9LYFiHat!Vs$?QBVx;-}jlJ z8q8F2|6l(^VTj)QM)=H*V1A%~mW03(%pcsDKOgN_SjWnqdj+7|bqFpFSuoX)6~^!9 z!<@jyn$(54uAMy&;J($`R(3EcUy9bGKP(g}Ay8*quwK5}UGp?_R=8zhA%M z&o{aYRDE{+*%g|&ExjM4jboZMT8Igu*GrhxZk1FPsKp?(KKyD9I6jden+n2z(&KZ^ z{>13|H^TIejc+PCN&12x&}&>%6R8!CcnRHN=K@78)2x`FYsc9eaDCrOM-igoI z*X2cPc>YcyXHm1S^sT%@X_0`|6R15w_&I7IN_@F9Qog%zv$r$b2x10xBZ? zb?Ma%-IW;=CElsu!+mSi%Q!Y`+90DOKV3Cf!yEZY1?z5gH!|smqMU4#zV_Kvk^JV( zDv^A>pN=C{TY4+`&Lzve#4zmf+2+y9N47WJ*)$QL1d!S)sKo08kY4B3hYrl0)IJpz z2l9_NW!^^aXB98gbRxtbHoP3CAEM14;VRU@+J_E#Ju9YGwB+2Z4ndHFK5WZKM*I60 z(vZq)Z+L5G!Vqmm&kPlWZ8wwzbdUvQ*U}R1g^+W}Tcyd2kA=j#v47}Q7S_frK2*7T zD@fqqGEk5TYtX(9xGB@{I?&D%Jej(;J%jMyUgZXk{W#$8fvIr)444j_(qChpF#m|~ zTPJ$A3q`TqsnQfvA;gW439-;v&s4%u=Z}2Ci#SGdB9P$udHu%K>E5qD3;GE}0)fX` z1%2GWR~d55r{;kG{@P3w0*neK8GH6^Lv%r|&Y`gX-Un9NyAE{;D2vvQ+%c*54?iXq z=wM;mDkRQ#4Xsw)R~sb!9)Jr|>DOQDCBjAKP^Lqv%5I2%M&R*48e$b#Vd-)W@@B3N zRoj|vPQ1ozoX6D|y{F1?%_%p@Ods@M=8*XPD2kGW-NQb=KUD6W&#X-M^y&7UHv$H_ zl+R}(^_>I|SX36oGku=ast5u6`5Lkg$@!__r!JPIIG> zxDl(Ga}ZV4Gq{jE%hFkJEzF~5Kf0vP7Ch+Btf#~q1@J(o)%PIWXu9G1`|{N>-j$L-J|%q5a9@&7($7E>-85GGv~e#c%2YVV@TGF5@5QY* zEbo$nOLVk4i&$F6Dd>NX-!PK_Pa!tuTwb&tKc59q&$ZcftYKS?P79^Zey z{>)P4mE2A_bl|7m0mGk$=*2(8e}acuZ% zUo^1)tDWl(YpQAX0RfRFC?XJ0x}ah~3{8p@A<|T&Mnq{6;LxOqND+`Gy(xrl2)$_t zMJWQ(Bp?Yjp(7wAARry&20vfqz0bYh-}ipc``4M-GG}*oclPX@-;R(fKRX+_py*Bf zs&Smr(`%z2pQe-(v#PXV9&_?hVwbrA*P)ZfgdR%GI)EPZ`ubIKouFXzCs4)|a_W zCwD8;YIJ7FAXb7REp3Cev8W5HMt`a{GMS@PxJ-QYDo^PO>KPm{_Ju3{)lVYz%>=B; zeXNAb$;T+sPlZE87m5-=w-h|LHm$1*6EACW;31iPy;~EXR=TC-lBj)bEsr*lNS|jv zmDoDACSpXwJgZ9?+s>e^w) z$I1XYAxgt~fjcxjMLUJLhKiX6rr$)SN=g0+G;%s`t0E z-$uJuE~6CT>Sk*KAXbN(X}V;q6Yp6;bR0y!smMkzI?$(vr_J6^pUs(qgtO%_qBaCv zAC*tEWuRq{AwJ%8SrWv0m6UjSLIcJ%Lmm#)yjklj3@d^iL}#-`VqeY_W$KP0r{gWU z$wl0rkR^M#x>_~Ho&!W6h`k|pEwR6X0&III(#k)($Tr|_f@`}g=fzM3gEZum@UPkO z*F+)ne~}00%%MNi=Pyz$dLQ8PSB?K(8FQ4HdV~rJcJM_qN?ym&B=Nw{p>`BE6_;OR znPgXg@#OVP|5>*!tZcjjED)dXq0?O!8;P_RsjOcoaOF%{uqy zTS_y@dJ`HXXXlZ;w|9k?KXO=d4XeO(pTavR=Y|lK<`rZI{uRwM!sZew-(w*Ir5dka z*1I)U9(}bx6+4j{hT?7TPJlp(t@Ns_0VRx6-H(i~mIC!}Hfz|xa(6SOO{F1#aR z#IGG4I6b4YI`nmibmL7KZ^Ld3xp{M5or{xiQZP>vOgz^rXHggz@rDiOWtSe`*~QNp zW*k6-q^rrMQL@Kp`8FzlS_HDd)#>RH=^_&NB3nQ-dRmYvI)fUUHJ{MOS+0|EuohYX zhd65z|JJp!%-7}4N6>RipZwU3P#aA|ALfVA=>A86R%*$_Q52TDUGDrjD>(RR)5Qg( z!xnmQu@Xa-8g4lnywoQZgmO&5HIBI7cWzS(`mRQp)44?L>dazI7%H#l@G1L=CM}B0 zcq*uq&-h)ZI3EOY@&$O+f8d=_tcRQ~^X*_dnrbx`GspET|CJrx3)(Owsg5!&+cHUi`}1GOO!hM^LT(d zsFmpoU_U^W0N+X~bC`mIBdjDJrBsv8K%}QJA6-tBA5UWTjzW^CY7MsxxfL3jE&_4X z_o6V}w~ObK2?0+Y9xg21c-0~IA`)#3`fkD@d&q%&gNXG9+P%nirAyB#+~x$My3P^y zClA$5-?R3TKEzum1bjyj-)9swnJiXU#QfP;&OO^s@T}Z4{aO3=Ok84T5 z#M_Djw@snWt@fBZ+k%Gy`+{48_DFjwMSm5XD^n6derJ{Rb*}N_U`1aT{*4-OG zSt-YJSwHPDKbfybSeT$NZZ!mdqOWvyum%v!2lg9g9-e5XoIUqeavM^$bc99BaQoru zWl#C@X+Oz7Bztray5@9V^xHVfz9*fFOoZ1q}P zgB`{2U8I7Z=zMzp%=gvE_^{%o8D@G#{~IXG=Hi+-L2TYP^1d1|dUd@DU2=rvNWLkt zK_@J({W2WBpqF^UYIBgCj^$Va;ZAJ=BP#qt$3<4}Qp9_3a~Gta09pwCt{z{uIj+v> zwQ*^f>pCrzDDpiS?}`iR=XGfG9?6)+V~`O*x)$){Bbezy${Q~grD(VNjP*@p!|fA1 z%4!0@83pWQth+Y2Dmn-I9q;Kpd}B1&0o&7gcd}h@M1++)hM7+w8pCOFMbdwB=vl5q zfBET&bPYgy-EbZte9#M9TJ*MRvW>QXqr}GXo47bUI~BW^JcRc0h=LDXFxML9qtHiq1pv%gf$m2!g#uNa5LslmPnYB2 zYKojwPz19gj%LK8$$ql;IQb1~g!U>^YEYNEHd2$87cN6-%{>hM`u60nTE@;lt1k>c z$KO}mtEG2{i8S603Y3l9dpA~Y>?JKXrC8M-rvVIYSY4`}dH(CwF73KmcmE|XFG1qR zx1Hm6$Q4NbC!MK^Nt^xshC5^IYAXr^f97b+n-PRp7v3uR98&QI1C(f?&_5DW=;pbX z2a&0-%B9X{dLGwe057k0bNiL}J|Lh=<&0ZzL8`D94*RPIbNuLG%?$!3+2++P(UFGv zv0tqSUML-0e&n;FnuU4I`%sN@k|7u*;%8wX)<#1sT@7dSr%gtZXlpYE z8Q8>nYm}xF7DPZ>9VpQp*<&>0o}=$C0F#S56J(L_Et)jTvcStpTqYvaT?=9WG=bp1 z{+O(4nRgp9ZeD}LMiZ-(CvbzZr7c}3Q#!@%GI$t*0`B8_e0593JRlJx6^o3>O(dX(I;sV2yBigyJ|;9lwO}=WMUGcFc1F5F<(c~ zc}Se$!f{3j!-vyh*PZF1h=Mbplbd3>V=+%_D6;XJ zD$xVg4x&Pm28?%C%d*Ilae^6VoewFCGeUZ6d~Ci3w|bukJ{PW#G))buI~fIlVUSCH ziO+&WmZrP9a$O3!i&tbIhviG5cXh4k_yI$7+ll7O6rG~xi>j@mgQJ=f>_y{jYc?@C zqzi4wJ3-s150AUP;AvKpg6NnB6>yXBt(BKd-O{HzsHE|FmQ_w0;<#S|MmbiWQg?Bo&qx~-ipSj+amoKSpM|eQfXPRmf$!$07jB0Ms zVb6D;nrM0L+vHSj%n7_~|2QsPuPja-QK}j%QKL!NKu|vzUHqpUNtK}hKt7Z-DI{7SwdNQSQ zNQJ<)JKx3jnFTnpWGC!8)ydjFm6g}87{>+Ewu`|u5m;mU`HUKM;VxR{v!xWxN545*_|V$h#WGO%(e(iiC|G70 zRqBkDoAm0Q-CordT@ja9h&CzrLu(#3xODBZ1{lTQ8NgBR&wK0v;gbF9u2>NnfZ}z| zCWiL>+_BCtI+By`l+heDDun!y-TLj!7=08F!E{Osm3DqdtS$_W80N_htvoDQenoO_L}y)KHq?+hW8M#nv4$FTT%#-LQtdqKF_R4F;8rai^tOcWYx6)~($ zEMHxh0_Zo*mVy^9L*3~MpS-Y=r-;A5EU3b`7~TOxhN(M8E+(hgp^7xXBy4TBJ3&%b z{AEDHsoN`!Xv3C3!Hi_d5ziH;%I<-kRi9O~BHEhbHIZi~a9uMR!~Bs)2X^()u+$KH zAL|ApuAK!qKg37ACS~6|_Fnl=xis*2eFCk*A0@n$ z@YCsed!(l*&0Y7|Y`7##C;qXXL{B#<&Ze9U& zw@nEeZ?>!aRwL!!qEpZKVyN6}sGn*55D9pg;m59gF$&FLZVguXZ0fOFD|B1sI=#j! z8+=^KL1O4a4`-nk@rx)P<^4VlecQrcdYuppcN_ZT zf}O$U*%LR{**xzCEzxCUsSy~&N=1F)#%hUgx*(lBSU6EI74XhldaxGcda?*g>=VVu z2Qjhs`1@K&~kJlbeLg3&t zd}YV-1`^~GAbB+!DQV~uk*OEl-AP&-#aI`Sp%6%j!Vh*R>{0^3a_3RR90!Y`1gcjbtS ztEVwdnM`;+9zJz&w%AVYd45b9;G~URoqS~8` zWMyhPn$WP`>v`-o1|(+~E?m1L2^a{TT|O46g@+)Mm*tiRyUkY9k}+Px16Qk5xyi8~ zW2@ef#udv#Hd2Bx_y;X?oi}G2`KYN`grJCl4St0$r*I?ain7|YxqO8GRlZ!K>X*TB zx9QcI-rgOW;Q{I!mT1)ws~Z!+>+kj`rrRo!AEPyoxs6&P!R^wT*vgJTiK0kK4RCZo?8A?dVr`tJ0q*q+ z+{1Bjr5`?Aais%nQjn?6*t0QaHI+|mCB%u-JU}p`Q>beaM)}1(>hiY@klyuf7JFlu zycDK~ZhPtJ84oo>3pULbZYQp)=Fu>?stYPIbXx`(7g8DyibIJv$)0HSPBxjOJFqQI>Kv2P}wW?-= z9hnBFe{T$y4=AYk8%z}cR8gwpMN_M4e`?e%7@18 zIh*n_#axwVF>W5YV5ohZPW0^R*G^k5`P(huG$_kn8LfXT|&^o9*g8%+LlhJB|= z`0e=gN!s_pMgH-0I{oo~a_o}|TKm4z=0AQ_sXrd9GyjS)M23RQ@$Qpc{J+WM!OG&O zQvsAb1EDr04!m4}8~yU**B$k*CrTGsoqNKD)o-xs!do(elr2l~lSAa^s-`SZ!vIni zavMZrvF0G2x2U^XFntbC(D{lAc&|F5O{oM73bJm$h;@2WfTlK7bu&5yV(CAh6KHq^ z*ut44QTPOfyh4EWk66Q(#Ue;m5+G*H6^Wep7QIm7W8w_*Ee$B6_*G5Ibfe}6Q^E&Z z9UXxdTM&=$^n{5M5V#SU_8P;}bPb}CkTt(fZdrE8aQMswzjnK;f$O3*+yhe}y)gaR z1axa2e@Ct_e&n+_TnV{M!k^XF$cK=&Hq2%ncxFW-PhRPwfT4Mpf(kNgWSH3 zevRHi?tnJQc?YC<+M@i~2M%mb|9&6!Y1l`dX8*GY0nqdT&HaaM>pt864E6pt z^Jl62-NOEzJ1`mk{WALV-F@I|`af%1SC?^nNK{~^x`hr(=&2LHV45ynxqa14kIR&{BNO*jaGB-SdxTJOr7ih+7nft5f17a0=8BCHVLi;; z)&o!jFT`zoYpG>2t`P>vseP3>=C(yFk&y7hDqfOCLl+qL0vT3yQQq5`e`aZ8%;tT` o+27~V{~ihDKs1&AEz3uK`t(Ma4PmNoFX}H;MN7F*@qzDu0b;#D_y7O^ literal 27937 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 H+K2xKz5qQb diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png index 0f93685456211578c6b180799544066a126f6d24..5a83dc1f0d19b06b0311d74eec6efeba6f61cb17 100644 GIT binary patch literal 326177 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

    LM&< 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 xW|@(0G)D6}18 zs`vyaG|3pJ$cjrqV*0E3WUWxw#hs6TRRd>()qDgs{;kA|I_#smX`gEJ{6<$W0-J

    3?)?2jJspH+2s%-H!|z4W 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ⅇ7W?qd!6Gk-*bhmNO*{2Vc97!R{4@_-tyS;eEy~;;8QLkIdFV$FJCsq zWM=VlNK4A@wNKHI)54VZ_v`Q)m9ubj3O+R5du(6-$F{p;RK@o>kMz?en7^1t$n0HQ z`u6iup#c80sZj4IoHeH*hR5biYpV@>GRT6k;^wfeB8erR15{4Opyq$vz4J6 za;vAJDoV2GdC~LIbp{Y45MB~xwWkgs;s8QN@maz!DwC|K5zQgv8*G5m)!neqUUAoB zt+*}#Q$Nq4cDsYMIs0(KVh-fxnS9vqX0tI71m99YaES1ocUbwU>WvKFKV&I#wqq;5i`vDHPHfJ<)O*wC4^=Yk8 zs?LDHO>f3Viw|Koh%i^h$v1u*e9XeLFMrg{2-4*UrpSw<5|iv;JEl2wpnEnMMa-A1 zt)0_8m&+mjiVb`}+wY+kI)X@1R}nA>urJ^7ZC#*F1Mm_5mRp%f`nYQ3{v8kyP0czV zqHs51%|B~lJhuYK-pIqkDif8mKUyF%wcf{S(xO!POP2YosodjNHo=yc;lilIC_CRL zssV%eM|VN^eDVO({%T(?E0>c6fX#ND<u&G}{RzYA-Q$=L_$l6;c5@2Myl-*gyKX!BHCwly zNwK7-$8*7A(JOmHtbo;Jo5^Yg@>Cb}%B^;csv+vDBYjs_$LE+5K6R>|*J1t*PMp5? zbBa&nq}I~9$OPOyAhAxd9O7Ea|Ju@L;FJbZTSVH2SX&G_+qt$LAC z8^#_3I5=mQqaMYE&hr?;xA%FyX!@){2J}=>sp7J+!EbrKUaq!+>2UawkwnW3Ed}r2 z&1?oZJOB40G46f~>-J72IiWs;OXT|Qa-? zJ)QrBiPEfmc0IvSn8t^7GDJ91($l)F0idcc7)cLp3Ly1~ zCWfBMRRe!U$2q>NZ~xxg{1A?{Feg}a=(11TW8NZAP^OGB2b*mBzB~y!7NkG-I zzK6Q6VpiEv)$;3sCId^p`Q1fxssRVk@xGZn_Da6qs+r8ux)Lg4@bHw*}6tIK^NXR8GEJdh*Sf1eB&!tP$L?W_?3wn`LioS|lonO`y zs9llvE6Su@Mx|z#cp*IDjq^x8Zic$|TX9as8ldW=R;o+cJ=cZbA~1_iI~=*pPkJNa zHiYSVoHfH{oz}CbP+I?O5nn%L&$AGmzs~tTtCxU%K`;4~9w*kRcRB%dyXBxa2f)uh zB)=TG(*;tif5`hgzS%EjeCqeG42QrE|7g{PO>Z$5Q#TTn6~=tJ{_^9|p~U6Jw?&o- z?I-@@47jkiY{Y0c7?RpaA!x2IC{*ou*j{#ILI=y;r4d+u;t+2hYrgM z%#R_fzx{T2HFpKcfg(J+2FhC&23Q45*?}nUj&oH-H6kaRRHU_DY<>oR{LAQF#Un;9 z!vs&4hbn%rm2AZ$MNMhjStv`}Fihoc@;2PD6u7|<&jav^8_014@X5XqTTlDL}#vDV;9a+{#VV~ zGyKg$F_c-0$zo^aFy&G$V3{Tg;r-?felb4UtlP5!mYgul`e0RsXF7BLVN`J(N3DxN zfko-8_XC$qCB(p@nWn~dkSZiZEQ1QpHpQou>c2kUeLdCHZh0DIybuj}sa{Uf^r0(? zAtJg0JH~6{*M0E9hPMp8&e!?(TPOR|g>93q+K%H=QwD0mGK*$5qDN{~_ax0%NFmmC zJ2geMb=f|Xlw+YMvlD(fpe6P<&%z}6BD{^ImTuE{l*z?Bp6RL)ai%x2Zb%geFqf$T z#QZbfTfelhp=T<-e{X$1qTlEP421R`aJ$L<#@q|nhUO$Ye2TY(i2{wXE~@Dg#2|N+ zva9g5VQx@(IOhOag*Z#1Ckcy8q4TD|G|3CxK_{W�ND%!0hPhG1RhZN~}w>TVa&C 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

    W|f`?K8Uhf@h0Q@Fl486$qHwI_*t34AKVzR4N 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%0Da(WCwO`B5<$PWD&_EkxYpYL;oZaUW@Y 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

    &?A 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 F{{T5Ujj;d# literal 330410 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�qBR|z0Z{de_Aj$w%+pG(vb#tCmgESaXz(i!M}N6 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̀$A}|eUrIy5vbHmCZ(p;gWIEz5Mg-y=ZMILq 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

    a23EOEnO%_qHPAFFS{==zD3+|H_6xQkR> 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�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

    BpbI%o^=327RZNZV0oxU{l7H8hEMdO2? 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%0DUYN+b36Fn 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ⅇ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%0AiKvLz?+_tcP^sIGn2 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%0D4VB4W{TuFdH*`IPTZDZ%u zFP8`Yr1&S~8V(fW|5A6ZS0i?$l))NN1;*|rlhXE3VS)OLByP4 zVO>cUEZX*AH@ZT~yIYs`PoxfQyM>j(0GrYsUsD-!WLEC@y2FQkgQ-36rxVB)oLT5{ z6T%%Pjlh&RdZRr4xQ;j0Mvp&1WU=|2)UJ*q*FYVd!l)M&wFGo4o`Dg~?jp+Nt&*~~ z&laF9KoL?|;-!CZJlmx8P-0h`tHEb_a1-a zBHf;qcqr{oI#SHmp`;gx_71L43LUfsGpx#?52MHboWrUD31ce_1jtOBwaFaa9N{iu zY%mJLGYSl#D?r^^Ya2LYxhY#yK!WBZ57mcinD|l4Kyo{C=+&>5`qGNn_7r&3 zWzuM<5EvLqOO|7pYWc!q3Me`B(BpiJ0*S}DfbzY?w}IHG8X3VAgiS^o3j~zGy0z9; za7NZ_gE^BRImcsfYFy8lOTyY}hhcdZtAZ^u@03zxLpu~EE`K@GC`ymO)?|?{Sy9_i zjoM)JQ}B`zxZ%qw$)lGa;Ov$VOMaE_QsG=1vo;0E1a+;o6`Yeb63~Q1nUOYsC@DiQ z@|qEua$j3EtEBZXfiwd&`e;;>m3do>UeX&UYuUld6g%!6F2UM@(VB+dWGdU6(>74{ zisi*<#BEn+qg9CXsjjyGis;ASXq{n zs%x#aopwNwueaIQjJ6r|qcPTYnz6mYy^X`~D%m{-z?h(hj`6*_N)T#iMGq$Qi=)l- z)Pxl0w2xs_jUg!n(gW<}VAXLM*43|y+AVDfYg7A|_!fACu{3g9Pc~3nYi$)55lB&j zBfcb8XKffm&NH@rNZYDlMOa#FOA{*6`iwNYVYhvdzJ--6(2)Sp$!C%UlPaOET7Gqu z&g2f2igY0t&hXBMQffXE9bj5=c$`HyTXxP_o6UH-pG#}4t>E&@%l58Da4M{|u?y@9 z)|U2fcF0{ZoyadJ_-|Q4=L!m80p@zRH(|JOR=0Tmf^QmEy3sb*91k5MlDTSEWPc<8 zbYfEeu)+u;!TncnL5H&;WOeZwOun{8q&!yCuC=y;vn9*?Ha445mPlL9+9=j7?9#3M Y1}Xp={MavXU;qFB07*qoM6N<$g6^LaBLDyZ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png index a4048ae696d0a860e3a50144b32d61b3ea24c744..39a15ba7712e988b39e22a74e29b71ad13408d34 100644 GIT binary patch literal 126986 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 Q11yl5lD1- literal 133999 zcmc$FWmHsu&_CVXoq`}Cxs)i~Eh*CB5?2~T>8=F{0RidmSe6C>0qLb-S3tU38r;9X z=l%cPbIx<-%{?>c-0$bicV_O)+?iNC9aUli1_CrRG-7o%B?B}xECd=FK?ok^KaNgt z6%-8(<55q`P#J|nU0htCd7%FB4PF~U*HLl`%B>qH=qidm{m<&^>MJ=V8e00>`$v?p z4w@B;o`LE2+WO7S&Beue$0~}!ad2;MzkUOCd2@epaU~$5ffCX}3F$MjuwGr9(^~g- zZJ>UxUXfE$*Da$8i;CIUInnG;#-?VdD->)O)v<%p(bM1B-nm4f+6=6ES5TZ>+{dRE z8k*WiD3r3Q8VrF-N&O-rDg9hn)ZD`APDtzE_ymhZf?weI?A!tt)AJWE<&H1U6Cg?H znOX1MJWkF|zAs-dB7Sv$?-di582mX5pPDW$ui$;g9~B+ji9|`K{pB)BCSc$W4Uc%r z^UUF$6A?3iU;h9e0kO1r`Ude}C{EXG;Qi&uoEu-soLGuJ@mAJov=gHKL>)A%N-Wg9M!X4TB!q~>K_b*HMS z3tT&dRY(!KR@LDpijG_4U0jWn*+*iB2}W`C{2i44r@-Li(fHb_(MMEn*HXvyKI`)r zbC0O6-*=Y(qKwpOnEW?SrHn=|QCFw?KypiJc?TJftd84fe#@-dz6I6Xzry++a`;>LfD$o<()CZW^u48i17><$%;X2YX7$a zyD}2{e-4X=b8Ei3!50(4-i)s3j2ShHLsUUbXTT^_46;@OzW;;%e=H!E@nQQf;t2g* zy<@SG$NJv{+2A%fQqw0#6(~C9+Qj>Snf8Y&H*|a4Ra0Iy6opb{S&!t|jzUY0aR|_L zbp6UR%2A_>1u|r0-;B@$B0f_pG?Odn;LWMvNwNR`nGa45qSC4y1(m0sh~8O^bYUs2 z*%sTaq%|RO6j?}Ro~smtQ6y~t&+pLW%Lm(n^8QyR3{NGY^{yGCTHztRR)^RE!2yfI zT8d}QT@^c`<>TeBBWKadBbG{lv-FCxd1GHM4qovPtFgN?WHNV1uJPNi8N z7%Cs1?bgqh%|q0tFunc9FhnjH1G%M6cisG>NAlKj?%}U;A{N}@Xv-X(;~jKjnDz6? zZWmN6nUR5U-zcl`tsbJc-9DKvB4*r3l18X;XOj;Kw5 z57gBMI1O93hm6BA8y}8`7Lv~`@b|x8qjsY8nx(oc)(32momQQaF=niySCZE^_svQL zt82givXp@4GMyD;*~&jy4Dx3ol#C5LGH+#H0~J-{vlz3EhS|~1n5T|dnr47+HNu@A zCH?X#OXHuq%Jy0}iQ9bs(NTv*ZYPn_pm)tL+_F^c)!GgUgGgW>a%X)kW)kc&cG~h* zKAR67OBf2C_p+vj2_9~EzvHQ^z4%5swRp@w7#GqkNyeZXjFt91(cLbRYCuT4vW+j; zNMLkOR@H8sR#GuiXSiH@Q=>VMd8HgCT5%*j{w#_(?%LV8scz2EIko~)1v7cuY=L^! zorc&PF)}Z#0{LVvZKXJvt@dh2J zNgOmyLXVDoM;mP<4WmQ%KE=B0D~orO@jP@`GR>4s6wOeQr&E}b8~(xD*oTVmzmNK2`Zs@keyA-_ z!6zZ{f>P)$2ghelp*Q4F@UmPEnY*pGVP>j6v|R}3TZ&jN5re|i^xf5)Y9{RpENw6Q zwbEa9=AIKl4`6VUi)waBYW}z*^cz0nuyM-K1OjqZuRltjb559gep~@C`it0?Q9SP6|FmA zLwyJ_fIs=NqohvKdiDWxl45jpBOaJ*&W_?|=bODcEe3(a)1M?f#t7yxA#`>^{nvqe z5TQt?;7KATh4vy5?Yu5dp-wo$b7m(N^a%&-Ql^}Z4;&@H>mq9Dx?hDaX`FL>AJ2-j^A?LBBjy67g;_J$!#Qi{AT;&*NK@J}?oIG`?7p zC#U$UOF|-1h4WgWk&rt}|Ltt>?R4G8lSF)6&v0S6Qbyciz!&}_7hOQr(hmjBX^gQdT#|u&VRp2He9-eU?jyR((`|3ga0*!{gjP0QJ{t9WN z#S7(K7(~}C{jKwUBKJ1ER75O880|CP6Kr)+d}>_|jqy@?gbI>QdL)MXT3PHz!rIN5 z2!{ec4+B3XYFdXnU~IqY42aS=c63Wst08qvmY z$6uKi6kV6gYSc-Rj}x)Rv#W4u2$}p7>~>kt)H_jqv>jzcOH<&X{1C)n?qrfS{K^7K z?w8QB^1_z$PFks89aa+8Cy{7N&iZ@JI#c=5CLdBuC@NS^fkFHT|2|dfq>3L;!7R)8 zoCa&)qlGI{>`(VD_9)j`L|n2d#^f&14E?hxO^LS^umEn)a&pbhhX8mwY0>y!PQ;-Y zSo5i(`6pv=lb2$)b_l3S#XJiK809XBS3)NF<(;e&rJmexjl18TU#=~fjUiITn_!Z6 z1+q_}{~rA3K`g*=7jC_;29({Zr%Ud%p^^2?>r{htJFWockbHqF;#hZF&cI1KeCXFD z&plz2b!qegm4?44q1aY_cO~Yy3v*9~UGDsG_vvstIIl z1;y;?y;o0n%lut6BxUDZmZ6gDEc(P$8s~{d3(Z=30qeuIf>UL!no&JneD%T<*`j=9 zx~1?hZjY`PYkTIF3Kxa~-QTg&l3fhG77@P|@r~%GAzaUKFHNhREEUrz6g0vT?)Y?C z@~rN8zWSB3U4EhC97}~`$+3qvXMNAP8$)bsbnv=D0mPC_zi;j(8@V-j!1$f0LI|eK zkWVgBGuQTZ&4u4Db7Aho-hOMCjr& zga!y*ILEIN8gUlF10S<@)$vsI8M@st1Ht#t5tX@p?3KdUSL_M^<`17TD@)N`m?M|aZnEjoD|3Cx!RK2w0PXJ5th;BuS+W@ zs|-R?HmDIkN<^#tggaBt3=Y$~Y^W|-Wa>dczt=s96vJVp<)ZKDIxVF31Gzkwcee!A+ua*~E(G4B;?ub?_%T z`NB_dnC{^BnvOhr-9KRQ!s~n{z}=~A$3I|ijl(%YMe=P9ZUXPR)I62@v?HaGP^=qi(=fmk4!NHMW~-QEaeiYdRS|$RGX_(jQDUFZDkge zu!OTBZ+_j-E613mzNalX-#Kj<<#?nT5zmZgvz?QltFiR8nOv=b5h2k3Dk66T`tChV zLDi)vDPSJ2yJX>DX@y@=PI~S~gymbg%l(K&sSI^ZcwR4jow^YLy5oO&nc5m9_$Em2 zcVGuSpJ{?Kw$$+s;q<-4rKg`aRJOBTj09*|2mxgi0365!7<$Sf5&WMcBF`J)S4~M@ zQzsCX{xBf7`G}TA?4JcO!9kOVTvCK-()?&LocmV$4i8-&FAA??U~8uP$$u=IXvc9bqWt1teEp#KVdD@RbAO;JhN6AfcQJp%)JE5G~NgOx#zrfNl^- z=sm7l#NHUL>$HJh)))se%53_6*a)OKMRGsHQgCB9QhhLfhy@eo8gNQo|7;F?tOMVd zze7rcHSOT(j?lRRZP$)B(B;I;mws?Z%&GLmIMArCI=E>Ve&xV~I3_7Z9uFDbV5(lD z4hcPuDF6QT0tgVWEwaA%>z2?h{?y-;f%l7eyL5rNv{!j9cCTLhu^;o5T)(q=YvbiJ zo`8E90g|078)P<3ywX)yR^l$G@lpKLIVh5yKc_j@TKm)pJVR2EnZK|*W-H39Ev4&* z%iF~6+xo*r(I@CwMq!oysnxDoy(RrlB!d#x#9o2+je~#1iTh?bO-0O@!B53NBu*u& z=xCc%&G`itqSJMBFd+xmPt8Bjk_J;cU@QBhzo?Fq{`%`xdRoHi4q>MDS1~ZpR!bJ& zsr17m^(n_9xMP_-d1{iJ-O0d9Z6ueOB{gFpM5a+<~e zSxYTGkp3?sIZPt{*82@s1DN_~-P1+kk+^)4(APZB4`*})1HTIZ_lCF(JuHvtMaj=p zM3J=!!*=hnMb8l z8v6Vugq?He%Dr_>Ah*9?@k4zU%q&BvOxt}P0?ld)QiWok|DaeA@KaD-=A^HJjahof zHdW#{>ISq%g-8fQk;=LLL)2jmwP;XtAy#;x`6*pu2MwK#7&-u>Fa&Fbk z(q!kc%Gxy_#UYNlo>oETVANYx#7`VQ7*+-i;JZgK4B<+(T~P^<{uW4~#qs}gmZo75 zxRQbx%Xh9C{cI1(`1{`ar;wrAoJij%w5rPLm^rizdHrU65hyKh-J^*)m8!bO&aCf^ zeRS`cO+y316u@y7ebcPm>KyN<3djgZST_I11kFXoON0-ww3WS6!YaSoQfX_6f% zm(H!*)=eq%tHjc*_6*brfa=g4XKurWx5Pq)Lc)~iBo9jNbd}nwv5r92&0i+oG|ipx ze3>)$D@XjA;X1Z&{@PmwGG?qgDsaXmrk?a!eib>GHrQ1?B#m1(q)vd;&tB`+4rtP; z5-@IgpHNkyHQs>8{MLIRXKM&lX5B&Nj7gO`r|fYjT45VvlU4QPd;x4nQvwbjWY5L? zKH$?_)%!)IRrYox=I6u}bg(NS3ML-aFzNE~BJ;$j2BttY45&y)e3&z)Y_552d^tAg zY}X7V-RP5d%R16UHDFgkY%&=*PVYH8O3pP!i24^5ssw7o5FaNbuxXesGp9_4wMR;Q z{(XkN%;8OdkzXZi-oF7nsEq-~P|mJjNM*VxuZobc%YZ`mAf~&TSS{q*3!<{=sUPo% zS&7HY4DG{mNO653Ngj89NK*Is={izyRNi$Th!IU@^GGDe2ppGQJnjT{TRn0E4lJK_ z`sV*y_lO5^UWk$_Bs5rmA$>hQjC_6`^?FX1ZDx_O@VSLB>Fmv=>S&UwQIPe}%+?L| z*L>T8J*AG)nd^^FD2h2(x>$YB9Y0yca4yM-%w4Jfn*Mu1c`#~(TqtO@;r249MqFV{ zTkVg0{RmItZ8b!yTY&>&h8X}c{6Kd}mWKCH&E9=j4 z=`}mOd>lyUn4KT(L{C-4*t1&CH8ritAO9}he96Bo2mCbk+l{}%ZOf$!KfHfwc;$*i zOMqp?7}b699E3B^cZ~BLnrk~G`COA42`wiBOV^k%>Z~?62YO5AX%3iBBeZCK+OvHw zGMUDcaPI$FLBbIyIyWDb#TEh*{H0;m+`HSwB1eLA60Rfj^(b;ozV6h>{L|-CExZ?r zX8g_3XFJ;@#9@#)PIjDW5z)+H9iam`_ZmX{1VexJlc=arYQjH098`;Q`<*#e(*`sc z+GVq8rdrRrgp6tmYF|X{2@naa6is~)Q|r6XEyjmfHV>FN-xEcynoOo916DTX>bS+H zxkD?2JQKKlylPhB-pZt`HlgLjl|X}ERq?1C&x8XGFAlF9;B~r$HDV)DridScFu?Ts zu7-wMlnI}Oh4K)M3i0*wR~wK&CyaUz1<{4m_(C8Ko%rl*)Rgpg(_y`z8SRq^?J0kf z2PFAc`Gh@(N7Bm5zYCh&9oDB@swplue8o-=sR4^zY|mt0t4vf#^Wy?eUL)PewQV8! ziVx7QDep3Pd;$G!RqS6Qib&-75*Fe!!?+3~^wBk@D37Oy8?j$VZ)bEI*Es%HN%<2H zQA~czL0>le`paz*vCfDbt2h_(4Nv>?{F;flg}Kj1=rK~wxF$$JqK4PykW*X0j8U7ya{em9* zqxBaP%0IbG@N&=D9+c^feiNb5Fmwy1)6 z%9Bf3sK2naaWxr~op{(JVK!_0O^+fH{)`%MSBr9Wo;t6kg8l~sc-n#TfrA1Xe_339 z?BWwwpcMxcEeK!nwviinb}Jxlz~(U%?Iz$2oFp1QzjbTQ#Fy1ANSD0|U~N#nG0^f? zoy0FmDJFoG^u}NY&eSB^t572kH%HvPJSa#$8Pk_s;c9M`%K0YKC}ej?4-FjKa3KXU zYsCjCz}afAtJ_8iXG9o;5%4$9O+UWXhFOx${=gy#%g->MdHMMAR01DEbz+k3^W%Qe zPW4C0cIyaOl5$WeoykwG{5pyaPZ}&!P<#-0XE||a%qfheUUftBW@_WdsZY4vJoav- z2b*qPeE4hQh_`QFaEU8~Z&9@LWKGfE?tsAY=@^~IB2-4kk+5($Y~E#JQ?La%tPUI+ zB?z@*J>!r#zd}sx;|zKU9qMmsQ4;D<^w`QV0<3id6IrSS>zOLPSg6S8<(qvnb2`ekyKsiy>O-4y7mN z)mgKUmH2Iw^+!h`Te)W7s09s*J^3OhT_L-wQd7a42W3A;L`c7ZF_jMJP%7Xappn#t z!$`)Y1@g4!qn3AZNQS=`SVqG7yI?MnFw*;zUn{@r>XPNTc1RGL383AvYgawR`&Tcz zdd3xPDf*Um%X%F%TO*4gKh8#--eV0Pd=*CQKBwdDX+(pHlU^XesUA|`qPLN{62*P+ ze@CV&Nl2X*SWUWQH$emT$wnyuP$Bx1+?xIWGXdXkG0g%Lrshi0oCMICWI%63N>L*V z)a7?Km1FcQq8Joh?)jAb+h44*iWkt!r{#rh!dg0pBcCbc36#vQ>rENO>#3jxF2A^y zLI+de>7cT})bbt<1WtWK*@#?r_KS+w1p=7BX6SM;+}Fz%oV)-lt#)PyhnG@d1dzPe zI{M58)juR$uV3x6156ZBRhaM#+C1qo*GA`$}kzQq@ zFGvrp1+lEnum6(Y6Q6?p&~Sfh4ovV80ZZF|eBmJ4ZJXt3kDpT4W;h6yn~K2}4Nm() z?N>hTir}JaS3Em%GTNTMic}ZMNG_0ti@$S%_KR)eX4!=<`+KlVwAnQ8OFn!6TnnKX zCw`hs@_Oi}ocTl){{7A-jC>C#&4-s6;D&K1P+5HpgK4IxHjeL^Lffi^zJA*5Y&`D!T}-0>tH5Ey-lCYL5~EJHtv^9(Nl|dwp;Dc`EX zJnu{Q;J4}i=bXr#M0q4Z?`Yrb20ra!d|S9N4#JcZ)K+U$aD)v7BD+^t0|z*ZNS&bg zAI}(-LHWc!26=6goY&*5rDB6zaT{4I2mV%RI2QVN-t986pjM2lga+Rx6(v4wK>k6W zQB1;Dsmctaaa$AIuYizo@ZYDJ4~i{R)m7_j@cDg{o(QuMsSC5{cy&M6R z*}(_-hm)oFG}xom-t>P(=>%Gu;~?BgdRlG0dS1&yPB9Q_6kaDYVDoU>Y`3R_!?7yJ zNoM4eKc1@(Qwn2C`8EUsYo z>*U`v#IP|h=DxlnT0Z5NGb>94eX43dqEUcBrI_pd{RZL;U8E?+FOWlG6|`e<`*kf> z!DQ1I2!bgeDrM3?6&q9N;QKu9_ef{`1giA$XGWE2MRAHqzqXgkBWo(PMhn!4ls?Qm zQ2O}z^89CwFVD7k@@wdn@$^;6ZGEz@|4FCK#F6sQmcPCWAzz-!7A3g_7`>rGu+XYh zkK0AUp5Co9G~jp{S4{U5Nob4xYuNEDyLNkiHCv8Z?xaQpHTYe%>-6g}sd9|S$^42G zjx!t{aZl$_5YsH-gK5~0ehEh+bb1$`2z!A=4)z?1kfw* z3@{qZWy&;S4IoC6PUc2@&-%P1hA>bRReV1?ZRg;64CtSJxy^geLSXlL zNU^?~e(C0q&=S+^5BoTs{0~^pgMKG8R<{SSNGnPXlwkjS&KqhJ> zi%ygRCTdz7kCx{0Z`_x#+@{nnr>3y8QGsZ?w@J0D^J3o1c#Tf)CqsT6UEkbf?sP04 zhtIz+OUv^5JTd2@$@N*FIa%Q7uuRsHQMv$)vF@S}V&5G9VGr!|+h3`*LbRI!>Tegy zp4Btc#DndycL zR9}g+HwwaC@q}v1v(^A~=>K)k-@a|Ltw`YR#Ev4@1rK4lPh5}>&`8Q8HCyOnE&iU* z6<|wKnz?=TrcC=cD0?A&nEjLJxbd3~2tC7>sj;KM?t&v7W>-@eCHb-A<}DQ{r0(Of zWwsj5GSN<1DT_cK3pCULZb*s0bo;Nco!Eg`D#x_MKDE?UM8Dv)Zd)GW z|MFt>xxU6jN+bV)G(%zK$B!Qs#+u_Zo@9?Q0U!;J{hrAPuWwKAr>4REjHhDAGs$wH z_bizkBEw!26$VB|;!6%438NqMXMMgsX{{O1+K_dnn<~FO%24(h$NKc)w?^l(r)yc{ zs}YK^jXt4yjS|F_--~A@6g8F3(>F<_OiSJ0T%`+$*Dj|^Gi8d`*QNY{1dvUZR$8J8 z(#cC2=|cc^nNp#FQm^DgFc8Keuw50XLrZ~n&JAszg>*4zCyXi^4&Q7JL?8b9SsVBD z2VjXF-1ID8-~)gTOpdzT+dpMY^HoJ2S~Au(+N_F6;zIF}_6#=Ag;IxJhS3C&;@mFk zw@`MW0e2RAef@MM{U0+&BYe88&LF7AM%P1yxFhqvJmZ3>Ru+E7MSvJ0j$?r};GE6e zOvLT#UTM^(GwHOHXQa2U#6evtUq7`&DCednY9qcvu~H;(f#rjRmj4~1@VQvQkFI5TkpNk>m@0h z{W5GWyV%dTVEmdvTb1in&@|07yg@|XQfcsrNxo}+_D((8F~k^Wv+(7U_FZgs@WJks zrcJM&(icI;P9v|70V6bJ_1SLjkJjZh%m99ku0 zGv*dAqA>(!b@=U7;*1>CAYrvw>2D0a;xBNj8MaXhF}{+L#Y9^vY}GX^DAgaeoKQx0 zM#rF!{kR@4=!bj{?H}1eI<~rl7u$~yj=88`9EiRa!UBO^e&}s7_}+pF#%nusKSsk` z{;hr6a3N7AfSC7}LKnL^IzrBPHw_e0&%%=B(W6^4F;hFs-t!ujk10_p&9lIe(=hga1Uyd$;!`}IF2 zk;!5bY+n>Zbft#zpv}TQXz=fGhC-@|+5dhY=zBc2J6dKP*$JG`?F2o1u;yD-i02BQ zI`&~6DL9Odws3Xlms_%lZ@*eu5I>ODhaAcS(L=ML8j)@SnyCq<3q+kxWgR#8_nu?z zX?@4js#)c`53E_GEi>cqd%R@>kXLl`wds?}fVT;O(3+QReNo$A zwmR$C<(OWDh~N=v_QUN;8Ikc!lwBH`AZay9S13z3+0$dc3G|gY~fq2ArEW&b(s@Z5xLbx=< zQ0+t{8$9q@f*CbYjUZ|%!~zU(T}%FDgy&RmXyZkLFV8q}N=f1Vp>kwxOq`T`I=!?A znj`6bZ{d#@^65k)>$Z!?PjX+R!?>`&s)ZT9j4QDZkbaOiRVpwx(STVjFzGMCMIlcoC$O@5&p9U z$5}Q)i)x~Y+-ExGH_^Fx3jI>Ao%XD4wAQqg+RKj&wRXg`d<$0*ETC+f(=4jFs8itf z6NWZ3B0$jbC>X6OeEWGAXwjIVt>I@!pSDsv%i|rnGa6hA-zDgO{$J+Jl z%sRq}3ONqjiSywG;QnZ0ib^G3X-&G`ax&K2*^aXXCv=Ac*1_jg+k z5CU@<28O&?d_59%FzHga#K0%-6)9gzd1Z^Ym z+?pDEc9Bo~D5#zmXSSt#FiNTt98~eS5M(PZAP@nPcsFaD2y=`_z{-!di(vUn@Tl&9 z`v3yBm;sSc=-=es$<@9e&WA7FT}&K16GTrH+IxRkh+p`62}H@M3>ic;I@O6h5I#s$n` zUjT-Ca^!~eEC0%2eQ`)qVTTtbR~*78UM?4=05a?*i{m%+FBf*AmvPrl^P+grGA zivqMw9_+G^1bRjLui}Dk=`H~isz~6#C23F<@E(M5nWs*;a9$5v!`*x5I=vI8^ce?i zb*mR+5&Xb?w^QG<@1w2A!S|o{wSj$UNXM1F0%K&DV1WP#y`unqFQ2f6E;UJ>QTCb+ zVW}KDl@bEn7si)Px4qMDTD?H0X6vWv{+MvL@Yp)SW5F`XuGP<;uJCCHhZe+_s>5~o zdkZj}**(9BSb)_WWC{8a0BcCh1A=L$t?622huv3Ij{(Y*^qq@U$43?3X8~@rDqj*F z{}?7J-K_U3QVv{a;^X42F26)P_!QOryL4(=`D`Q{)ED%5JRD>XzEFi zbyHR^SThFdwcc*%O62)J9GYHHldZM*`E{CNgAE(&JmqB1b5_G&;ETfmv}9lqU7@!8 z@At0*0i}assq_Al7-#giZXU&GfT>9fnlW#?8Sc`t*2SdjD~8j(*!17V9lv?Uf9;bC zNt$pw&&cYiC0x*FHu-8jogQ5C!i7V%4bY~sFCKsdsRg~ z&cyeE`Bvkp3&*uzW%*nsIaGM^(Tl(G`=*ketC^3*t@*A(v~%nx{n~_O?E1qD*O4k8 z;`J>_At<-KEK8?y4b1o!a!zAOBEGN^TK7g-|6g+PAT{_NddLfkhqc`~t`^MgExet~ z^I(y&vC1O=f;>;Fw>C+KP>D{@xq1o!De8&ln&-}5oF6-W4_c+)yvCD|_*wTW=H>_k zoE<4k%lui8vE|k$B04*js*X+En zHAct}HS|F2QL0g?DUXPdussfCmuxg)53Wdm++K{{P$5G5*kdiXeJ9t0Y3?U9XN{j) zseSkj_9>EVJ?gB?qF{EX{oV9WI`I3gGg;&qYjOf z1%^{N3EM*UB`<29FbbN5zyI;@ayl{ot>?Vr7h>4Kp8EnU>74eIvLRXms6P5Nvz)-> z^=ZM;4oZa8#P>Nz*|EHcYg<@~-DtW=DJQ0*jlusXAf>!L<8VgJ_thcsh4M89swU+S zuKCgUBNtqR_xAyHC8mN@-Mj>%RyWatvBIC%J#2E8a@&r}Y1B>P&zhqf(WT~*yo2K( zn>2Zf7ww-Fv^vj#(lX)z&Rom;7i@M?n(kQxLEm>THA2_$JB?)*6f3m>DN}Fxp(i@%Vy#Gokok zzL}Doyt~j_*n6BXERX+F`LOEq=Lml;B{XG~zquI&AH$}hG})mh&ypjf3gG>f;Z|wK z3*5K^wZyHDqVAviYR8T5QuVvu2&lPb5c#kZKGr6_t&tzBf1+Va5s)7sz`^0rHl~B` z5_O2q>*n6;Y{r!kWnLg(qaJxY?W?Lld%Q1NqkL0=+t^do>4YqAuAP@bWcq1)3WZHy zolv*P8!|>&K2LZTZoW1(X*fm4FIJZnH8)cLdV^;W!`zrS;@n>P(H2=v2}@Ar|DhS3 zYy5>7@5J~;{GsepPZy^bxlgl!*K*8~BJXqG19<~+#ZMi7@yAC;DRm0U%&uJi*iHwB1l>3c`Z zf<5Bsi=-thy`>i}^Gg^+#cE{wMv=TmGjSc6Y}&MjnpP!j=|;cm?Zkcp<-E@NtzN@Z zs+Kb+lJ{-PEMdNcf_T+BbzW5CIm1Wtq7nqFXe-;EpONe}jOz@Cs)L6J$8OEhIR>7= zS2^U^@wu&kB;k#Tgc=@pOii&RZ?2yRJC7>Fzwg2hU5kl&@@CgELrT3M-rwj-e^9z~ zs>-L+F*arcd%W|5xvl22S|7^dydQNGF%RKH=GBFCOjw9%Quj}d=bD@?+|U=qlR$(q z*Q|u~Qw!G9oMG^a^ydZJ7DW zTO9cP;Dn8F_JFEqx!F4%{Og+^Q~Zi573B=5b@9>-Z{Nf~&6nrdpO2%&3KR`b5*?s8f0JV4v( zy?dKU@ie-K+krKARtMxA)~^tXjHQz{tQ=;+zVODrY{W@^*M&TJ?J>LEyFcyWbpq?@ z4ep;|+&n#ov%VQG1ct~eKX{}hB-VAm;{p?glU=@LG`iEWme!?sOxEU!BAOmMyUjQI z8iMOaO7I#*KA*+|#v^ze>*xEg+EH!zGxl_+tO_YZ(`u_w`>rqIH%vu;V|(GGj`#He zyCjv`Hl=#N-oW>^{Cx$KRN!eZF6)C$S)vmfS}0nKaO&?>L3#B1K)yn$|Fu0Z;Q=$E zLRiluzo5*!#Z(a4;F%zMw_dhn23=q;JzZ7!vRQw`?UU+J2o<(W?(1A~6aE=>H~#UB3G6(0Y&OZ|)z!JT?=;KC=- zyJ(-1R8ho9cTrMXmz0<&mYv%tju3Sh75S+C@e7#o-Yy)(SyVI6oe!i)Af6zJ5D5t| zGaZ~-?SNM%&}5a=vgxDK`qh(g=N69Sm99^!AO{>qA`4B(&}5o>6CguWHse%B8?{DR zYg1y-)>1d;nPYT!y@~bi@> zaWYnae)IufoU1Z!C0!^WH;syp7V`z(LA8kx^Ut69;IYrMmgwPFQm`)(cl!6+FzgkT zrC5J6q*7j5uPtj!$&UXLyi|rJ(ApvOU_E@(3Cx!foI3YY1gseg3ZRGnM?gkS z^Q7|0x9=B?V=AjRFQfmA>J~!+zXSg|Y&l~T(9!ZS`uDB>rbHB~^)?QPY=Ye3?^~F0 z%^^6#z8)SnW}iNNDxnB5u44b)Pi7w8JCwv4uZ)rA5?bzvB%o!UO`Zt4zXF8ZEm}+; z9&Rr%Eq;N;5Wv`*>nnHVxyBV&P}Lj>35n2GgN$zD{>{5}jC&PMZ}lesk01BjKN91T zu_+YBymk}gC7K%BGh`lW<{~rod>QR>A(eRXC&Qp?#a%LupJzz;Z_fCsfB6)M$@ZJS z*c*PDO{~$m10LET(=bDDc-l!9OCsON{-w14rX=_)nGhpZt8}Hsso@%7S>CvHgJqpY zFD6XWAD*nTN6k?EqPPFcbm8tLI5Es?TIS(5v2jhGQrnn7z_S7w2oZus**y3#X)9#n zpMM-lAi{E{xQP%T!|Vvq3B3w{^N;-CG@1Iy`JZO8~-Q(@Zlz&y~#cbT1J&<_XHPIK=oBl9v89jZtyR0f!nSGy^mq!|{ zrWnbd2t&p+-7=Zms8w4Z+(@Du*OYn!WEj;3=n#z()>lEMeo8X62a*%FQr7;ygGB7^ zi>_f*R^7kHBDe||uzo95L1$kjg^Tc;qx;2D+(ZStXbhwnBnos^Jv*J+6bm3sl zA;cqP`XwQ>Lz+=TOojZAD4Olmn?P_0REZy26@zHJv={ysLPO}lSl{2RQKI*IUFgN< zg*K6|`Mg~$d(rD19lP~Yc#Z-^3o$VDAn>Q`{Ba2|@&k0~KSlbFnvuH(l&+$(4Ma!> zGPHIVUd~Kvm<^98e`L8ABB8Y0#%>9lWrpHE+^SumaYUqM^`T2&N;f34Z9Xvd)+GO^ z-|&A++cRW$I8%;JLX9wF))KtFR%feuLtJn8Jd)iokadkW7Gd-)7Lfy}b82H_%1ywe zZu|6`c{C)ss=hgk;GN@n_{|PVOOixYydwm%eT0yPFZuN3zbBFLC`4e%uZNQDvLjX6 zcr>nzs_+r--@Atp>n9E3qgRsw9}E7C*4K3vh(l+eEV$mKnu4MRylD%PF8$5okSoRI79nq8#3J99-_Cs`>DHmCPDRq?r2f)88HI;3*4q z?N&_ZHu!ph?lFPx$)*CYOrDvR4zjVCC5mF9fxzM?kj9!y{Tbeecf*<$Y--0s`Pu|9 zbkm~d0?40XW&p~MIgRc`0`ogF6t{s$*4U3Y10NkmVE$Gs({oQaHeyHvcqCPIyS&3 ze}&3rpoQB1Pq_fT*K*WjAjG6If_2Dm3a9)x`oF29NlrzwQ-asQ`A73Mv)e(r-jZ0( z5!UJ!(Q@-(K21w*aJ=19@3fzc?S!SNOs)b`plp2h*@Vvz2i_D=aR$-?;(?`|oUW9H z&Dz}0l>(1gR%BRz#ECp!ij{5fIOPh`1n52Ym+fBqL3*KdHyoW*iARN5adHps1-2_jw@ec!jSZwXmu zxGh<-Q?gZ-vYTXI##rMXTPQ-&5N60dND0}p@6vzt`Tib!U$5VT|AXHX^O|wp=RW6L z=bUSKpX*$1Agts11*721zqwM^W2(@U@CI$-F;(nzM+IBq5;8z6^tZMLB}IT3E-94F zvwP_0rBPQ#;9a5_Vtzqi!Fa84f70W=ggA3g31tkd$Z-8J!Z8l^!?+5xMrjTdTvOp4 z%UrmfsX>mj=y9~aKKSDfMK`d+u3eoJVrA{Y(H$ zl>^RIF}muueHAlR6)#TWBa`{!W}&GwQCdB}^Rk*AWM=x@Um73>Te|5jb9G|Q!|n{w z6o<7{7+0(00zRpbdBcgr*l!-5auBxow`?A+r|4wD5*DK?|F!w9oz3NvzZXV7(UuB@ zIEsfPOWKHul&oXKB7zkfVgF+tp^NT!$|WNO-0HlekfpcOvU9A zTh2+$6F5c--&q+1(x=z*q~ydr@iDwrp__dnWc;GXdXX6T;Z5|x4T=nq@^v^M9=&M7J3oLF2PAILve;Cu7NgLgfQXpsNiB;ei}9rfp}&1OMXo zpb>bA{X9Gmk)UIJz0RP!(VGrvhn@T%V4OM)`c{$Q)T87EeetX%N!VnX{6z4l5e`^x z`66ENQv{)-5U|W99pEkdI6QBwo{X){hxDS5#JHQD0ESy`SKPmZ10cdjJtns`u=i%QNQT0cg zh%N^52|$YyT1fWN%kx$OkK}1$+RQWmxGgXt$*3sKpKaw;HU1NJ^?MSTw=}=AqhV9~ z8AoDZFT>KwB1cY5pf0L0A`O--K&n&GNUl0UL#4~ja_#^6g*hxJwTqX5lua*w`;*8C z)18%*J#(R}v7Dyn7xNdc-JkjJY2j*|3W>x^7%z#~qY}omBucypN(vH99Vq($`Y%Ny z>r#BLcTWq0cU2$)gB6kjQbPWy;=;tcgdPto}ap|PCx{@Zk`c9`-Dy)D~yBxUGM}ChRL757h$^4Il0~(Tjw< zJE`QP$`$8-`b4DE@;d+jE2rT+9%;)?)pa{r`TfXyfpw_x*?FE77Cx=`YuoptDh94S zRx=6s7{j{_5AQzuT=3h;iMfql|M!}|WL%D)x1%!)f-;)vaJ11PBmE`&x1sbRp(V#Z zU)T1i$i5^smLf9w0J8N;MB1B55%`aMq4vBrJ((jxl;Hp=O6kI;N+ zZ~X2^`doj5IC5>98kC7;Ijpz7rmLoBL)Y_0Oi?P|O!O=T-{68Yc$v8Yw3ldk3w}@C zOYS>7tTjyKWAFY_zMfp~oEy%bzX@P*H*hy8$VwPVybYG&-Y5(;tiBNo9H9w~~ z3@zDBMCQ@VGAgoDZvXxHf^g8|j_-d{F&{D+zhI^vKil)-=-SiZ#PmDiR}iz`7d37P z2Mz{6xpoQ`cap4@B(+Tai}!j2dr>%nA-V9r*?W>YFT20b+zuZQyiRyi^=++oyv4TX zMvntG*~sqT^h5)eDaQ0`%;HQd@6Gdff?1maJkFR=7{=Wf5*C-i!4*o#(`cAwV+8+3 zHEW+icT@{cs7i-DPJd~Ui(i+qM!xb(q8h@7PG+#c6W`DG*ZLMv}uB{zR zT9R3Er{oy@nY()b+Fit_or~1ekjx7fHDns}gauCgYh@<_(r!q_z=@-5@SMm(4lb)l z7^@%6VgoS*ti6OR3JW}=h+Mw_$3O43#dALUXvThi_i53Tw>vN@a~BBy=>r-Hq$w!N z$;l~dQU)e5prv`jV%X$3xq6{Xbr!jspMHt6kFBl(?0Nb5+%cxIg4<_Y)@7f5sbbqm zT7zGJ^OeBRhG#db@w@0RYR~7YTsI8lgBK0T6C$ic*#okf$(_aq29A!7WMa+bK`Fgm zNe>2cgu@oN8YG`TTC|3@v`7^RoA5eB#%S|MTEFf_9(TX~Q|@jvuWXHE`yVk;c!3Yh zGYy1HUWa{)&u-E#^IIV=dD#%3uQw?idGveRWFn9XV^SA)q)Tk#xEpM~9u$jBFw#&A zyzDB5UjDpNw({VQr3nh$H}FxYNa}vl`+S``nLziA7o_C_bpf@6_m#j8__Hb}j_}-@ zJ31g=#R|@|8|>@zeVjRJ-%8veaP|c!M@7Wphq5V;{>X#%S?UBgafQXh#H5LRp`i9L zHPQd{@Q>UvS%*0=C0BCe2FmIecZn>Drez>TmSt^z_M+@Mr`CH33B5k&`mbw8iC@~} z6%4l4dv|VKPIB=gWG<|`QmMts=p?IcA-9f5uW79rAi1%TrqN3yj}_huT(%M5Vz1TI zSzRhGwUoKgpGxGaiSJ>##8X@jMRCJS*3wh|Wx`giJgD@BCR12wiN z*NGrh==1lSo&b^Oh+>og8avhOhB?kg0{DPmvFw^Ac9-@D^V2O`Sr<~@2*V_U?2ebd za{VvS;?nsz)XtK+T+05`*2!Lw?2uA})Kv^Dr*;tkzXRgM|Cd#U>{h1Q^|_rcomp3y zU`U_+eRfl9A;?7Tj33vXfVHvfCzLi6@438F@tPlP*)>XvZ&RHS%(|EP;RnlO7e0N& zvpZBPj$giQ4dl^A)ay#P|9%~=Z2ViH1SkA!e65`_I*tV(E8x@MCi6HuOha}tH>fe> zbrw07#W#YD#bhd+|b4x(wA(xMi|)_mR*qiKPrU? zZQ|i!>%!avxglwelT;KP+YB{sk$Ea^v(?8hV5-At+^lm`;mFYFf*6=S{^-Tnp3TI> zgpK{A$=jOoTluVxjl+BHS0P?-em%ZM-^FFNyEk4Ec{ha%5OC!d98@j_3J8Dn8x^Hb z5@2bTKXd-ZjnS;jIyHv4eGJQOBqTwQLg=3{=)wE_Vlc26#Qu&#+THnm>|}!lIxVQ) z>AwN`>hWjY737i&xQ2dR`D$yR#{SIbX{_Gd@$vDdE<$7Nn|tpVl5|LBbaW&ECM&Cd z{qQBU+TGo)r9{aOcHK~?i%Ml4kY~UPO z!+l~j5v4@ z5KuByEhGnZb(Vzw$)VCo?Gj1KAyu9jKo;LA9~QWzd*c=x**j++Kq4s%2@GkWYF!&Q zcB5G`Q)Lj?hm^c>bkyF!Y7p_fT_pPMkbxTjtnTmc?qV(Ji^UmauG`<17tI>5v$Iv? z=1YLRk7aC|B6C)IZ#m-;CYi18Vy?fT$vu1rx+E-S4vW66D=g>s5~PByD41f?zg{sEdKc6 z3>YqW6hE6iV4=YIj!2N{n|MCshbCJRU<9ayB{L}r9!+Lz*IJB&;|-O~JsSXfB=wi9 zg0bGollmt;JqA=YH-iVJcfgKe0Yt@^RtZqtNM3gh#LoCGnHMKH>;31v>l`w*`?=(0TzY zGR}<${^tNDO%y;3Z>4N$;1a;J z2Kf74B9Fd~1vQ^yV^`j4pssh135eO9Rk%zZT?m#ckRsM36s*o(%{Zf5WM`+8UrWF? zH3{BUyVF>^Ix{mvz_`?t5ZHv_-sa4++Ux+mq?Yi~ir8_>;G66+pZ4HmbD_IGPxWCXq*WyTJO;Mb3XqgCD% z<8E0~4#VJ{Y{8MoH+Fx20}~~aXPbu}#f-jJJbNH9wC}~lS3Ju2{uWCAgo0j>N^CNp z#aD=l=T1WN*B;d~Tpady@b6Vw3gM|QRfQ9ND9`L2%-3**t(K03jf8bA1lH>!1H!l3 z1@|ZmzN|KKtRzl&VVfQTElbzJ{Cs^S&x^^Y0ylG>YlXb-Gm4fkIes3tL%y$;-gq1Q z69uhMkHV}C>@GV)KPzC9Dwpqhua?7(*39fK&*Qa3Ooh0kxmw3Pr2#A*>MaC8hsOs! z&$w`f(ByM2@y=Cx)`-7@T&DebSD$?^#j%V*WO@$yP?fpB59OEG#krB)xyOix?`28s zd|qUcAMu%G2%>z6AevPokTqX6D*SCjgIOE;+dawKcPcl{BYO^yLstF0!j6bdKE*G> zYmVy_5(`FpjgQZSm5@a~4-*D+VBfC;O=IG~J|}s>xh}k=F{%f@YX!|abca}yO@G8= z+i~=Je)`Dv`a${ltUe~D-Mzh9@#e@st@i^r@(s&J`%*e1|JX(>t*?*QI^J-ce)}nY z!IGAp@A9~SYqu?TwCqg~d<~QLN#9AseuNZ`8(V6esu|yL>0Jkm9hH)-KHM>7PtvM& z>`cBIX~w%CTsjwHduF%JHDd{KjVwC%%YkRV?`HEqjwO{2v-MQho{1W@4X5+be(J+k6 z@vxhz6bEw@OZ@xlEjkL8(0Jj2sVHJ{nC>0DZ8qff_8gWE@R}-zWeA;-FD8rBg-BRN zvCv(#v~fmdR*lj?tRnd0(yr=U(yX67ceG%{RAHm|mBEW~1H2<;?7AcAdM~Mpxuco8 zzkkUV8)$53Ypbtkb*x@`Hqtw~I@>_~0;aBY!Adrd#oL2=CiZo?IITR8Da)z>>1?=F zWJ7h@bTjvm^pQw22~UGIG@~o!x=Ps@?Pc@jlq=<_MZNpJP<4L(q zCwg9>WHJuT_%2@rXvgTH=Bjt~u=+fINqjJr!J;o30I|NKT2I+YRvVty@d=M@*qX$bh%#LXo1$d++cxGbwzUFR^|Z*!fWWE_ZwL&%fMS(8tstXUbq?J}%1a=PHy=_>?ROToeHypX=X!Y$HTM^LV&l6jve>URPHK}TxLrdw2)GVAeEB}voEme&w8xnC_I%~2-YNrB0`6|BoJNQK0@SP>~{HZ z&v#)k&p3bo@~V`7#BxACg#pU>$!QB+qDeU;f(`5l40ueE{QhJd2qWv1;hsKPKrt z@2R#%F2VNpajv4HS}(EF&Euk@oO8pAje*Sod5Pga<}Gf4i*ac2p%+-#i!MKa?Cymh zhN;97pPaNiHGxQf|EQ?HeJRXWl)U7)2jvBazHN-joj$5gBCb$m;Hq&fnVy%AZ37z< zL!eg6X0j12I!_Xn7dbwcZf9^fCcEYENgL$YuX?QeuHTdNOHOfY58c@nzp@UyxSHH5 z65aTz@i|G?S8n!9mN8f$mfh(QVf)duu#g8LK=D+~n0R#Ki>tBA;QK4Pzt6WM@+Nfo z?vU%0rG@DB8OaLd-t(?^&wg&Ve=f`nYsZhk>2;+`1zWBWIb5$%rDKk_u01Lk!rzO| z>u4#d21a*hcXq}JyPCx?14!ff+8h1tMPDafD+|movJTgekXmV+F+hb3G(!o$Ob{{ePCxG<+G=2|-;w-9F(r&NHfXvhn4|=zmslmd zdAlbA>3M!&oWSXzucjdr3Eie+i?m5pS=RPU5fg2z9+5 z`#*$Od4zxkLNAwg;x83MTNR8t-<_XSF<9KvE=wzlO}MC2>&nHy;_sB56B_w8$c2Y% zv$5K7Iz(4+<3=P4AVdrrXE}Xr5LP6exmw6~%6MZRR`I3=MB;dN-=4^?UXp}K`UT_I z*&4h)D9A`;K|CYuHb(8T46BZ2zH4M1{{BDGa$9i^iwEZmMm!FUZuA3LS4o>!3@>0L z9VwQrzW5)~jsJ)AKR*6F;_F!L)ANZ-Qsv((JCM*=3}P0^AKGNAdF}e5d1;%&8|HFg z&ms#BO&56Yg^A6@Aj}BL9ok7lxLr2sf1G-kGn&1UJ-dsDiI4<>jiD0ug9XGW-{`40 zBgziIVwR|q?S)d_9$aqgC3QKtt{b!l7p+0i4}^M+!VY*I4D^6u2v0X;bD1HpZDW3?E8QM+?6ItA z898qIbmCv?l!Ml5h3ip z-%>8BEGsLk10u6;d)ku4U~0!IP4=S@wUE0_J~-7?{x*w^Ka z77ryTB@b#*kE^rDPrGcWW}(l&QP)lc9sG3{VK;~vtu%V+VQeS8rg}+}_iG8T(O{{M zHE%0hCF-jRe*{pls`+??{-IIyKJpnB%*q@@V2E`#HldK^$sc({!~J3GzfKB1U|vDkuuf$tQJ$i%Hgt&Zr@|p@syq zvW3A4RGWv1@YTTFOahwjTW1#BP1aDZ`V5O5!%|Hk*~>Fj?}9#6m)}r*wyyc!?aYEK zRrCB?i|jmhR$*BqMK*baFo$qyNxDon;*wkL2cKtea}y71?D6j>sP82anP}7GQT)p=sBQ_Nj=aFw?pgnH367)3dX2<|gr;Gs z-{C1HFZC(5o>Ttt*tq(~%MeJcfBslq*CfZ!Hu-91mBiAyF_(!_Mwe5EdHp)>hZ`63 z(U0%h_CF5-KDPuDHQwqBF1sm(Y|8ZDm#~=X0ybUYLh|a@qt4)Ar2l+TOvAI3Do-o2 zHz@bBPpi&6Eg%z09gQC$R8AGaEi08+12=*l28%SGQrdp-B_$%X+J1OE&ZOg%dZvV= zMCA#2)Hp0zl8IlPS&7QbFz@%>vDmJ2@hU8Zg6d98#LQijZ=n;Hp8C*LsmRdXIj?F! zC!$XqTlQW`6Y1evK?yJ?zq_{4BVeQ^)IvHYuIBrkN4;SC%CDo2i>p;ZR*pgRQ~MEG zLF{WQ@l2unxWU2aDkX_Mw`P&jg^lfHlLAv%sNyW92Cr3b=v0As1I_FNs$d#t`<+t{ zbd&>xZl6QrZW6OpfzCRj!CE>TrH$lA1v0$`29j8Kz(=1dzUwXxlD6*3#pQSGqC#8M zo~HCC+5&;9$QU-4^jUnbB;sBIe*P*=yOuHV>vc24Qa3S!*Sb~)8#TBj#i|Qr#_&*= zrP~7cetKUkAXt{`If1nA(Me7tG>W7o72D9Y_5&sR)_}w9Mdy0Bu!Trv!^__*-JX6M zij#X|)pq9oVZ^6V4DP#WM74!>|8D`2+<8WoEg%yZ5DLa0E9s zP-cimi<-U)7HWPfYBjG-Uehbd$;EZFvb#(4;d(aCGN&b(7n7hoE+bt~4sWm9nFnSoJPZMAqREojg4L?ln~nFYmV zMT#ubuwf0Dtk1U&#l&=0q6E?Paq`2X-FY`AsEsDiCn~CQ;?>32Y8v}59B)5PUtye> znu^$2`uQoX#h#R)LSruCyOEYgPfuSNN{8=7C&}$vZ&x4zHp1@AdvkIsv-4WOL^eeC z2-52iE&PDTrdGhzF+e**k5&;fjiwp5{`pU5oH>z#-Fdm8_ZPDJMVZRcCcFY>1Te7a z0n8@G)NtZ2boG}E$Uuk|4HS~?9G1Y~b{F&%z?Q#yNEDTp9zatLi__)r>WUm(OiEI7 zx=Q9es&Tau@Ts-Lm}V7f%rAUjHj@APaf~x~AW%MNJ#`V`8}ATTZNAvc*u1AXRRTit zYj2B<%aV(x*&6%oeA>Zgoo>FFxXD<0w&FUFAe1q`adq{4egDiTfrOC%=y|4A0EKtU z4vx?Kkk2~PWAq5R4N5(y^d{BdM5=3<+7%XK>zg5iQka+9t|_-vI#kJ~{y40Uz*$}n z*=C`_~S>4@`(r0uQW)1;rCSA!cYvfd5}lQLvqM67#0tcGR%)+jXRSS!q4VMOI*ge z;gLAEG-R3fsX>+50pH0=pOsxQMX{G)(ca*}Io6^!RlVmm*aulqkd_tEvH&J;>x{(I zWa$E{c(($g2@so(PBP>6Ur8**s;0*q05@}?iicz(Nfp@5zq}v^NoE76j-_?HtaF~L zlB<%?($azfDig5eswq=&q`GGXJ}wpdWjcw445IO6sMgLCTeSJFoNw|EXMSfY`H+W1 z8}Ka^Y!CI*;fyHY;_MP+R^Q7Zt#4MvF^l?TDI2D<{BUlE=%P>#x}B^VbtBAUziunj zYEn>qQbr+_>deaTUKbLfNR1@9{G~9Bmyimr6IdNjaHDkLufQ!A!nS;oL{lV&$&#Cn z0l#Z{1H>6REi_xxINa z0tBbMr$20%+X6aK;Nl-$Kqj8}_54>IAo{ySSeP%L=6x5+%?m)Ce7S=E{PXmgia&oq zCd|6DJh2ozH^+0I(Gl|zmVErQg7M_-Un*;d(i9r^F|K%|osG?VQwObU;INIGyIH|M zV~C&6O+o_)EXaoXCxo%P#QvC5jSGPpg6y_{{L%eehq{);i>(0Q4shTJ>cIA*8d5Z# zfStBPt>tYhelsXOwZqSUZ-A;7Zi2!f>kE9`wmx7SrvtnmIt_16V>R%*D-+UJI>!nG zKWOak?$*(jN(Y0sU%c28EV)B|ZMvFpN1$~8w}-NiNkspi9Ie@A#CDN#{1{(tyyQ$N zGNRdj(V@AwHKc4MvOOtriBp8nw^B=8pG)F?q*DCR$T`@$w_9AMe8P6>;OEcH66P;H z=13oepUkEddZzcv3FSntlV3s!m_1c^(*|Zx{EJ-!%e64UD%c;wUq*b8cnM6?9=KZs zWi~>o3D8ZOYFJ=@Hsqm6tz3ld8YXB7fu9;HNoLu-Z2**=h+9cXj@;RHJ(e38^M_yiU!w=5!U-es?R)pJ0EbumZ&$Re2V6k11 zV)m`~uU@@<7Qaj?y#A9F&i(z+Tj?*!$&nThpPX#cmeZe#-yiTb2pjeSeB z1H45Dq#)^37avf8hqXeF5zms-A@)1qr$=a6ft*w=x zR5Moj?6ss*f09(>E-8zpmU?9GcOP7oNT32O4l8gkj5L5G6@aM{M~@HktAkidVQp1- z7$s7@-gU;rd_vE}POAOibe_hVJu`*~-RU?uB4 z`L1vLK27QD)l#OAocx?Z%WGcnffsOO14jw=Af(y56c#MVGZ3RT1R|)_hR~jRAE0pr-PzxO8z)@$ATSA~FVJy`4ZBRO%)57jTLS@pJ zqPTd5gqpC$&*v)@xOIccs$meV$YhZ47uE`7~+9Qb^D1)Dd0s%QtaXAl4{d*0} zNHgPZu72VjV}w&DiD3sv`p4@5HZSx}%{IFv3|Q+p@~*d5f9JR!^RBgE?j~^EV=*%s zqWgr#u|~jAv+q8+yYIZZ3MojgBSJ2Cw;LLBAuQZ;8^=qp&l8vL@KY>SBuVHeY56*Q z+Yx`lF2t(%XrJoA=4ah;#aPkt+eH#}4TLR^dod^BX=!zJ$lD+~A_Sm{(>)VIn$2Kv zS0;G_Zz|%kGLIpV3K)MStUL<}7ox-mZ8B8i)x1n_6X&eClEw=lyiP(hNaRGi>_8MO)kS;*po!#26l7-fuKA%GDg-3OginB zaWYXhBVdD&%n|f%s?F!^MqIoHxEOIUskHPuJ!_^J*ZC_q^#GnTilI~%22^W4w<;5q zRPNpR!!&Okwn6cv^#0OSd^I{u2ZVSNTpuTDKcT!&YHzKN7DvT0e)*#L&Zmee9-1Cw z9BGlu9uh}UYgdE;XgD`%4pM(J8#)ODa|&P|6iT4hYqA);+kV$oFsBBwV~YBMWTV8- z>+sbJs*?Kda}Wk+6#viBOU>*YSE5JD3zuceVBCc?!om6pMo6{`DzZ8*$QZZP_v9sS zYAg}#)_rqFYfc(%Yw`)t5>%0+>f34fOx1NhxieA?I*>W1575b* z0xo=D&LgPfS0OCmb0NVp8G~CV&CqzkL|*X4*ZYp3+yg3b;?(AEZR-KMFU+`=n8K{V zjFeR9i4l-ILy7`@=h{P$HOO{iGbtP8Jsh4WpG3EGl*%}y#W^FuMOVkV>mnYjT<37x zG68`|pT)}~Y=R24ikAcYw}y+gt1TNQUTK_z%4_eoR}yBz-MAmw8~ozq(4na}sXI4} zX>BE@{$YW9DD)&z-~<^?BM!6Zb(A_G?iF0}wAZ3BE= z$mR!W^)ZZJkTDnafL>o@SS6uMn}6Ga`>M3bOMK=qQXQEAE&mG(E;c#V$e4w(-ahxj z=R?W3WIiX*AumSN>Re(eY@gr}?IU&#g`0bTmuZhu&V)F{>2n`7wmy60U0jA>&B>3K zvYegO(>T05Zmj&r95b99G5V_nI~WeIf{Q8_jeyq6FrD|fI#u2>JwW-m0K?8%@IpdE z1MoE+%KvXBPcv0!W@in!nD_Qs&U7~FN?NPrVlNcBtP zu@_4!jhMkjyjEZUX{n@>2VF(lkqq&4qE(CM^i9XlZ(E{_05@mkG6tr2@p~QG&@BKt z@cnH3xZc0mB@8ZttRCz_+lG-RAs2Ga`m!zwHAi)N<_&VoezdDA)&t(jev1xhgg!UJ5j109sWqR=%YDf)=lVvHNQS znZ6Zi*5aPU<6691V@O|0lc_9kXih2onD4lO%vy11d$3Ig-DMBpll6 z)H_t03Zcf=jSxf3<&(!{&Pv8x;3Dk@f_<|I!ll@=fY9{qF(gTA(|8L-&ctK_Jd*XG zYF|?h-(UB8heJtb)VKy(B46lK2b~-r+%q_0Pqt>p&pRXi^|+X8VQRei;Ia&-fQ)iu z&#%V>B!BQuaGWmv>LW{5FyiKOXfuK|CNJp$69z%|L04U!oYzS87E@etDR$iIRB`iX z(YVztNX4v~0|%>u^!Q6)qpaFSz`Q5s-^@(gnk3D;t8ll1nR81A3ITR;`Jq$O0vx8y zMZZs|Y{i@XGgTFsv8^9BAB;z|%6_A&H5b{tJZ-i>DIoaWZ9I`J&gn~u*MY)L?&Irz z#g$SmoyL1V3^cAv-do;uz0+Wcy1>hF@d1IfJI!)_r8HSh=HlVjB0_)U%?dq9bC1eg zltI^@rug|C3u1c*a*H2{(lg<%3JMGcKY4>mefaruOOhDbE-5y(@pw0)E%3MQ;jS2* z((k-rB)Qav%;@u}t3kq5a6?vQ&5NvJ>~DlRd<(o>3&n!16B1$l|(dyV^Ovq7deXSd8n z*)gy#>Mcn{apkZw>Kd{kQ#`i>#E63J!5csR92vg9)m(07U^F-}ZHwGeEV(?y`^ySd1l>&A!H8Q| zW8XLeOVfH_k6F5IIn-4KN&KmYRP2Bw6q2n;90SNOfCu>bjbDl-Fj}D75YR_*ib zmB0{%?$YN)Wl*Nln`ao}S?kOOMEq!Z$f9SCG^0b_pxu2~4DG|9d+8GfRRVz@CmMbs z49^+4rk5?2%^;P6ql;hV$6VI$(6mxcOj&cDXI7=HE0(Hej(Qil!e`+h44B1P0vxyF z&)mZOru62KyNSZBJU0*rlkUI3lULx%VJU(lM|jSG9tStmiDE3G3EBk-u*a`YI-uuJ z8uW^|dy9arH3PKc%3%4B!ew!l>pW<2PI(V(KN4)&(AvC7 zsKy@gx$z);aA!Gbkn-x%;R#P3Sfd$sqF~hXyFL-+s5m`)fy}fP`f>OpPP51Y8P;Bc zHRDbtihw+f!ug67;(w*VVdJ5EpWO;ZA|tq{Ba4USu1Qk;)A`S?dY8V|_&$F>g_@+t zG|ml8GqZh1BMSEmx~H;k{L=T0o3W%9`MEKRtfGj2Z7e{V|NVkOuE_|BhHeRPna3z3 zqhaM@Z-s+)Dp_TO8s8yO3$XNDp(#I@u;kZ$+Qa&8}0DFTZ9;<`Er3q^jyLb4q2jws<2MSp{!LGNdkK`Xo4MG+Zb|eY&W~jb2 zdii#=p=5z2w@oQ*Gs}!fV@Ax0JuTr~f=!jpg6<4@ngXXb^Ote>!Xw3!-=PnYqXq!Q zw`Bk6!{Z!enpL*)(c)gJ+WaaT>LB1ww z1wz!|RLd+A%^8OduQwu~#T43{7_5Lytab?ufA#&h_v_gkYJc9GaZNyOp>eZvE};ZY zD-uR&Li8{NXog68JQAyhK!DxV?Fz%mvITE-LAf@*7fv3Y)~jS>d7T-MDd zE$x(!G@3)U_BUqz7~cCnX8s+r3KeUoz^E3(-=5gt4%=NBo7mW29A@^3=C$j@$9n*zqC>*&J{M@&*6id0b znSqpVf&<#QNaAB65_ITK{IvCzN(hnbDzFAeP4;pNXP`^#NS1OuBAB(AJ1_-DK4)n{tj@hdaw|%)(8->Gr54@K;j=^a^|`U`m~B&%SRoT+ovA zlN{ZJ#kH^fF6zC0!S^-2`})acCOx?ex}=jC%=#nB;R$(7O>zu$c2|@yD}@JBIQw}J zdKvttet6uj7^^tn_xo)~vIR3Dhn%S1FW5W)Je%sGCAMw~%#>=(kYN;r%QX4=(v)#j zM}#mYl~daqsjbYMlT*`H`*rWT(c~FedZ}E38@$os)Gl6R#9+91*q=e*!(tK#D#s zrlzvS7>Z*IbKVJ6ip%3o?3@UhGWg6-hViZfNPRsaLlWBuwX#&yXNF29{Cq zlm$lMM47mo>H~aj*J%n*1D_}EF%&RS3hS?at>IwL zoEfoD52$Ds0acZAbWNX7RAfT^`6Vzg8zMQEX76dQXRj-YGIqgdO5br_^h4ur>8Q8d zZ;lsmeRB0&{nHY{FV1^Z-lD9IDl7xkXV1=JwK@^ngffbC_ zV14Fm46YPQl~Gb=fyxUpd(zvQ0qIn9tDcS>L#@((*O+g4$Xd)1Cz-10G}y_(nQ6-k z3qs0$rKEE+`Wt+jvH62eylFl`BT0wlz}129w2aJ076_xH5i(^U?IW(^Cf(5cW>Wnm zN|RGJ3M^miyh2mNaka4Rs?aTOaSN(H>yP>AZrosAJ+jU_b9VRnO*)Ts0#$)o(tmyz z*caK@8T+~~um4`&PQr6HcxM&tt}kOttPv1(1yLZf)M7n{Rne2Bv^*t$Wo1e}c~+9; ziPJirbCHqAw7IrP!ScG%+eH|$h)js4l!{W}2+aK73vyg-ltSM<(M4UG3(LFG_u_8sDVF!5KI^f{NNk?+ zmDG0#{SH!9R_Ny@`x71F7eXBIdQr;uTD)@Kg*QtfZ_IuMI%q;Xiu6qs1#nzw`uv~2 z?;>lJ+2(X}SrFWd%KFn)uDyF(zd|Vsi zocTk*<|FI^s}+-d9Dswe9ycp5?t}XvPhZEHKBdK%uYxhV?}Y8Y8VpvD!Jo0;tG4t} z5FPt4TgOJ0(mA|*9>LuPJ?L?1{VwYh_VtM?f4rCN`!QExdXvz}8bn$%f+(^i4eaYe z_w^S=t%$Cggm3L2y|NbXLnwLD($13vJpU0y91Tn#yd~ZC^yIw{D;A?knvIH zo&nz16kV5SVlRKj4Fs>Z#?y-3AGq9))qD0r^Rb7B)JLNpk@Koy70M&Z$4X6c_#vSsWHHv&y5@)m4zK47~g<+$D!?yj~{ zRwmukl3DZTPJ<3R<2n^y8^q$*AL28A-ehdL{>ji3cwBs$4`j@R60%G|##SKkT9)CQy8|JIf zEoMpkB;*nO%|h(=GVC>T5MhI0Hgd)`uZ>9iYxN;Vm4-R^u{mr|M*5XIf4@Id`mw+#wg$^bXi8pU-%Sb-_jLa8 zcDBapvY`S3x5v+y9L08)@bDW^he z)p+65@yZ9DZ`JkVyj57~5iHBEi)LY2|mKdZmrzSf93$?PS?Y=S@*_ zOHP$Ir35I>Tb9Rp$sF|*p-ZhI-jO)RE`!ZepM* zGQuB?^SzgT28`|M@9*E=C!tt@N3hyk)OTk>($dlz8VnvBG4`+02QetyOnnhz&Vj7^ zRh`D4{vu&Y=dUP-Y?f3o3d)!uXCI0^LLRL&4Bafl7M5c#ColO4+TwFo;H))Uwo_!B z24m8*@!X`Y=%3}`ZNqb#0MD39VNuH9#@&WTX59L6lD2ym+;yK+kVnFvNbcW4ZGY+y zalN!k;5*3wNM@pS>f?H6H|5L2?mA11Lpuo&qKge}SGp0t=b))VUpTw|>~#Nny1hGI zzx3rf`nR$8l_W*TZ!-}h3|ir z^0Jh$!dUDM)(W2Sx1JI74nE%B$jGQ&js05T^BH`)~gWoy%EoRMeae#-M_0%-$Xcmj+<0iN*BPkG(=twYjl=E8q|#qwvX;#4N~AKV5^iT34?c{x<~owYK_P4pqA zda@K7S`N(@d?0N)DzT|xn1&O+)%h6JGB z1u1F<_@XMRMPL)UKvQTrOv$M&XSr*`()ro zl9o$Yn5XCRWDDuQ%JubeONrC$t4TQ-J8v%oLqotjGuBw?poGtWHb4BaPr9@4S6jXB zds#OeBR4-emfV*iCa3&8-Wnp^LPWN!K%k&EdSz{G{dKbHto;QV+Q)Nqb2uD2n?M-9 z8&+*^aBHSj0x#emCVhADeaso00KV*2P~+QIN=*;z9sddzrUP|Jwcx7a)4rI^;R8aTPnoGhgh1j2J;!gwwHATC|I6r(0>)*Gm zeytP{YQQr4JZg(^DapQNSVg6X)rOpbOh1Q}_Y1)yT@e-skdO<9U<_ve@Q0?QjOkqZ zD)(Nj7Ugo%a}~gSP)tI?G8Z%Z@QOk-q2KkAXCWDJx-*5MtjyPUdHGQRHIsSbd|IWn>_G>~do1mq&?4zu31eP{`9YX!d&LMhNsxnx z^bD#L?`(pwcL=bSuU<9NJm0Kr=+1gfg~zRIIR%njV=yw6?kYI-RcW7)a?gT8V#Mfr=hjYR%!KQ&u7QSIdPXqhz2 zCky%7?l*+zW>b_i=Rc41(-DFeM@U=mpQk$*K|>?w@^~h{7{UDTH+TCU^J{z5)pnkQ zI+2T)-~^txG*aDgXi;0JALdw|YKdvMc;P)C^&@ei!n^T@A{oy%7CZLt+5uKI_7}JZ zq9Kwf3ctTT=D;vk@PJ7xg)pku+~#(7z)}KDC}y?T0ZD02oQx`it!k${{jc%9nzP&$ z6#-Arx5vuz@&moY&a4lk=Ssgz{RVo-KU=Ter$>}Z-nrRz+0vA%lY=gm?u<6-)f3ji z*P6g+pg8$A;YaN^5gK;c`J@1~M;|-<$#mE)(I+j9lrkr0bFS!%wI!# zmJnd)FycY!?%Ey60p#0esB>V81JWOmH6{Kc$H(1l#S6S24e z23G>>o=wgxfepQB_xOSj!sL-Ip8k!`v?Ly1M&k&-Rn6=S@dB|WW+)dpN*9Rh%iEV* zT&3coQmu~b(36%nA@=L>78IK*rEvS{U-#bVqF=wl6EkxyL#_9Q*pfN-(;}#2pCa^) zSf5`r*L9uv8jRogb)KbzgNNTDvQX7@Ni@^e$9v!U49k5w4g?4=srPI3M~SpzFC(lf zuxp(Q?LC)SKx*g~{N5Am6TF}>!8KH=ekE|gglm*SCO zN2^UPU?DAx4JnPrtv9<+ri?g6H#|gFH+Mo>w#Wg-3Rt-v&}s*mVazBitL;lIA7Tu5 zNHJOuU?lC+P2K6|gp-OlCRYHr@=3v$6ogoq>dUCgnq2ylH~JiF!){)_PE(0n7;e$K zJg2?$ovFTtZt<6nMBmeS2+GB;_ul?;fv`K_J~}$e#wG^4pip6}$(Z)00Ggj)x4&R1 zKGYPQ0DFS5D(o2^)}!v8gafMY=t>0f_ZM`-7Cxo1jFsx^nF3^L%jVpDp6ZPLfgJHN zcS0lMm%B(_#{g<(r$;wrjP@+8ELv1|ReAB{p<*C6x0?m9G>9QZQ4ga?pt(b^WI(>B z&qIdAj6)SL%$OZ8r${Js206}wfC-SjYeB5dLwdpiBpE24{Q56-+U)>s;PMsG_D;yM zq3}OS{UgX5AoX3pb-ZQRKHd%CS%8P}U>H|2AUrP`c<>OpMPmqHa!mf&1kY3kL{YjQ z77dk=;kEMb!Sa1{P)@vckJ5ynA!h|eo@Gt%0h;)m%DnvFYX?FpSNJv z_)!W^jB!@hEGL}Zt~yjr)z?9`b!ykApb5hSe&m#x*B!0=0GZvD+&*+(S}G zIWMD77>#bqD?f${kspP|BpcZf#y2# z<*>GD>}JJc*XS4!^Ltm_GMk;6Y7K(fVDgkM2E6rd#~XaHy|p zsc9~b){`8k$g36Us_iTqlasR%TDL`UHrxL4UC|8=)KH0894-zoql{ZA1(yPY8oTa! z^<2eg4$U^}UB7f_<@ml0-tcNRIH26fg34Is`|YR!Q0zka?>FbyyWWys8+o@X)e@t8 zysj>A$M)1tt@ZjMSaoaxuwbkdd$1I)4B|k1+`(m}zb(?=8FVnjTW-H4rTEMrVQ|SZ zJ)ig@LI!^8@Ks}5B*1p~av`=ycg3{{BIW5kzKd)l2P0mSFhdgEb!YB6J8%C{boFkh zE-*~OBTZIn)s27`tGkv&Mv~dZX_I49YhVSiK0l!}BR(PswyqnR)G zy?wBHu_o)!fnbN*=;EmHQAa);C7v*e?^4_R;ymQ?%+sdVG$sIxm8>8gF(c(-h-BOk zZ|vz&BhOm5MdNOq6?&Y)>1Czk==+5-W3~>UAFpu|w~3~ZTt6F@IMvZE_MFz^qGCku zY-{~Divac6)_KrQA=qKdl6s)E0#cI7@~LK8!O8_S7HHwmC1C z^u#Ao9v~1&@xTPUF(Za-V7hXA8L?-yGUwRNCt=FsZtJ=4mMytBr8=*VS}XXuv74No zL>?-G?%E|ioCu^!Id<$BMmpxb2d4|$ytZ%|N>p1cuY%rbexT&z)pOB)pR?&#{I9oR zyI2Wifi{t^AAhvc2*A1b;pOql3$IOIZ;!b9Dk7f-g9!fi(twWYzusA^08Dp_QO0V3 z?+aiWg-*2t7siHmBseRGa=fe@Q31n508$-UmX7X5p!4?Yp>dVvXp~UJf1d8FL{;RX z`z}<0@2izWTELJQ7${*T$7bU$NZ(W@Tw9cdg8Ik8A&I)-B@4 z_4UbyJ<%4n&;p!bZpPa*Gk@pj`WK3Q%=!$s`$X~UL-)Q8VP*@tOPFtkpsno)nzdgU zM&;jqhq#trejpue(lBBt9=y0RbF@S4Xj=S$Q9=5!ExdeB@T76HCl`Y-u-HwvTsZ;mvNT zw*}%;d|iX1($#XkW^U@~b1g<1A9-G^(wGx|GKfB`My(#df3wBs{mp}8!SWf&kQ1bJ z!uWv*YxF1VJa>OE>HwgiBpCzMd49T^i1Y-OulVA zIMMmfgp4CER#Y1C%B1!+EQXUOZn{+mGF_HKTFI+tsfi4)oGM$Nvb|6SN?c%%_*X|0 z3IwV~Kuf*V$v)RM4J%S`DuRB9YXEolwgT44n|6$f#un=F1e^q_;X{hDA2z%Zy4gS0 z)py8|dzs3A^wJBI3lxA+$@oaDAf5zin@8LU?-P{Hu*;y7Jl=vGqvHyjl8nr*aKsiE1wsF8PlwQ^v}$QC6@av2y$UzT6l4uCkuSuzuEUx=Ub1WbPQ+*en#V z+xjK>*n+oXAeJS`ng>RkV3-NqeN5}uQrp)QSYDVW92FrC5OwWCzy5ne#dw%hS z%H6s>!Fazb$*qY-yHigUyj?}uXELz+2ma%l(tftQ0pn9kwJ1( z=z)&U(3K%8>`_OBx)GtD8>&$k=FV|lN%ZT%V>wSyG8R_6m^xLI@<3T?Qi6}d0n;I_ zA_{F1)*-Bz(lKr*XF?U{ohX~-mQ585=YRY0noJSLQ6+Sdn`Cw7LM(ES~f=z;$ zo>V?pGdAgK>f!xwCz|tvr9hig>7RqL#w}GyHPPiVEPG<+$82@&u_M~ae}29ZomYD3 zu42+se#lM!?jQhpmW|ls25Mtr3;WXu(McG^Yv45eW5yvnYyGPQJ*cM1#h#0>38*;x z#h~)Db4}<6@!GFB8MPNC{NHnwrqS7`b$_Ih8`*vLI6L-0zIP9A97_Xt49w`F^M%fS zp*0+IPcD2~rVwe3#sE#+wGr)l#n+kRK_;r-sJ%V}dbk(zP$qE<+1q;u8BF8?YM>T& zDklD?5s@?5!ZbDnYUno0GuP=tqC>4+<3-$axH(uc~E&Dj5)x6+nfZy1s+a#O;b!GDB?d~8W+CY4nx~Q% zMku{OW$Rmx+>Pat&elI_%88)z?N8tK>4*Bk<#G} z^4{NYNgvxF+3Tqk+pH9RMX?oOut`^GkxzA3xm8$X|8=9~%aU8^{ah0hVZ~b2~i|SE}9bX*Zu7eStYHk>i_k0siXUhcM-Uo6A#8S zu7!*$Ss?F~TNsB@|F7qyY}8g>E?>^$F)@Ls*sIohjVk}1d{0ppH;uCz_OBRmORy#w z8kzt5@=wG`0Duvn!3@?HgX1M=`d`)@HX5K9{c8Wm!wYp#(KMdFqa5!$FT5NUYM@lz zIIDyXr_xMqwEv5sGmfjJPLWIOWiq z-gtfQMp#-sPh%gE1}ut;2ktWidykb(ZYhmb+jevO3o&X?pV9?@z?9rucgpy`HKR#? zNZH=SBM$S5zre7n50}83HxxE|Z7M1&MKCcs=UYQ5!z`Hzi@mYm&3iK(2R%w5eYsbs z#d&&~M{G~>Gs%IbHWlTiY?3f1Feni2fUfA{EHZ95QqU(IpC<`B{q&Zjqd;F}|X4C2#7?tB-$AUq0QQnf5yT(6_Rq zSIw@8dG(7eVO3rkf^kU-^4myXTBEgFr|0~W#1++-*lJn5%V;WUqx>?UhNt2Vh^`(cs z1mK(Iu^mo1T}ssz!Q9n-#!)opyBw&xZW0tDA-bP;X#c_nU2=GM7(wzvCT{x7Ly`fX z3O~H`aSQoCLNiXNZBv%UY1+J#LG|=(y~8)R%bvF_jXHFMI4oVB;NhcUNL@y_R>kZO z2Xt3g@Q+``jTVWoXp6_6l<=mlr#qnS!*b(aLOKOyMm3k<+3R+%9UYIC$+Rme332-K zkTy*@MITo`OC>6Asy-=c>(txNurcxx($kOUNU}#7TXvzY9^ZaBq`%9!$Qej1;8bmS ziJ8EXYySFm#^l&-fX|@Mlm7nVch&kW_wE`7LUBb3weTE59nSsGc~>j|9Z1#`TA4m# z+`YEO61Olnm#9DK>8w}WmBFChU>Msgu2P#0ETrMDKmu=7-yMF886v0(!D97*Tv(3(=%T$uMxz_=CHsfer22PmilcD(r6}vL_v@5w61Rg)ztIC zl=J7232&endCLS*^C7lLptF z4vtHvXuZ!oKtr}hb_i(o0cZioo|?m_emm#nij@5)r=+r*-Pc-k@(KpZy<;}Jtg>MS zN-CmLYT9IxWXe-B@m*HdyozGnY)?>+yDcc9Ik79h@sT)tQE((y7?P23g?zgU>FDe^ zXl@n$ka97<@?s9Plsms7(Yn*LBTXor*B10wnpE8X(p3?Aa3hLh!2FYDz81=P@#mEm zeB(s?jA8o8V5M$3=Dh_~t4a zpYR3#`j(m$8qJM2ToKMNIWRJkE7W^@al8=mNs}f9J%%B>%sx2C&CTt+0_K*K>`7IO zcrZeR9)vT>q{HNebfyi!F=kG@_hCnV^tJx9riWvx`_I)X{py=L@O((3vbK<=w!Rig zb?SAz@@bQ=Mqz#U+o<8N0HLr+QZPIzBZK39^7x{ut}X@fBO04UN~0D!qtrn6Jgse2 zq*=48?3{$MC7_{#Od4R?cAQYyvci7w8lL%rA=cVi}C}-aP zp)Dll+~&TcA!Tl6rVf-Xl%bi4n|vbsO3Mn*II!sm*Yk=jmS@*`YHMrV)+ae~wzkyT zE~lSF_Lh8pm~eL$dQLsO^`#@=&s)6C&Weg}l`|-Yf_4)nuU4M^Cn}XX03DIeJVNcz zF;vyT=_18Pns5x>bB?q3Jy+uE!F-dL3#yWL+tg84pDs%vs-NGyZEiAjSWYw{UKv_B zLTmGb{z~8tj*bn~l+00Am!6H9&y-V)cg(XA1uUQ>tfwbR)o$-Aw#;u^fHgfMEJIx? z=wu7gBfFf4VShvu(Z6u@vbW6vPv@!hFRx?FfNpJ|>9)xTAnZY+$SRNy#b%CJ#7Jnk zBS3iiy5LDoF;=+xQ80Ti8LJ2eGjSzKEAsOOPq*d4SnZEpYTrw(bj~&n;vzVn&+p0d zw_zS=5zBiV4{^*X3BAT@%#*%@tjwpUg;bqE`%{&uaDy2T~D{w7v!pwlgT?}WH?riBbHFkBLX-RsfExMNnjv(e_y}`{xPxVtE(Qf zD^`QGj_kL<9_tC1<4{y0HVPi2L}tp2F&{-p%HD`~$AJWvKBtM1+GX{@7x4^p1VPs59^xLXMJ#CWcZ7o%@SSq6!aLDoG;UFa^K= z0MqJG%om|x47dWXsI=FIS(LBn=Z|mkD0jLXfmG~B+1X<4irNly5KR{PB0Jl1{&_)N z-S?T_={>EPCa(<6^*-mz^^nN~Dc!RHG5@~?p*ld?VH)OKk!da#hLh8uTq~sD{4id% zI#?eHUEoV8`d36P>N*hl;002YNE=Awz|}I5u8y-*G6Ph7Yk4ILr?wks;72I88l>5d zF_>TzvC_Gge&kHa>RvKe>Hchx=$+=}s$L@txS<=jmc>5RGN1 zpLQ@Dt?)4{W-)7K7W$4aoh>VHQG=LjZ}@}gk8fnD3q?iu&K-pl!r!#Erl@5cE!P%Z z=;JxW`dw_|aS`SkJF2mfzYUwtp^Z`N($k-xMtK32mTPrprKP1Bx?Y_DF~-@JMdjLF zv>!tAUeyoA6tl)$Uz5&X95>t1W|9Gn?;GYv^BbsZg9`QS$`5!cDIaQRy-uL`*0Abx zdNe6zVJ}YWoa%?Pd;-de7jG$#S>(3|HF6&d26+Eab92shUzjfmt&pquFAkl{4%n1zTGxMwkA6 z7e_=CNF-<0@p7U6&X$dcvSvP*|*{$lKVlX};FCp%REEj*e@kT6iwZG;#eK zZ|9}^%zx|;;x);l>!FC85kK#P@ke2^;ty8Yp|VO4Z>Hf$15`Ds{5rN~k9#h-$A#eVMDS&7JNODAsaxqFt}l-Kkc>BqhcY%g6($J_`B3AS@^}0Fx@`^@qBip3X-&|!m(6O4De!v?UFML zU7Ws>v&jBFhl=14)VN&#Qhel!sB6Xh)TH~!O2~FY&4quR2E&4WLV&r$knlDH&LZF8 zH6lPLiy#YW_IPlPN5*f??A+t(+3T{=Q9i?XpU{R*6x!CK@%a9_ z5Q$>{5|x~nqn8&PkLMN0$x153%O?F!ffcM`Yt}Gq65*t6@)$I6)8ZeQ0n_Mc+cX}t zN=>$M!~?N!WZj(OK|)k`1gETdWI1!9RpD|axq7p16H^=>MSle>$sRjtHphKgGt9O6 zqVD_l9&6Jxb~a*;pJd%cZDY}vO6EYc77jRLcEEITUuA1@@=cemM(U-K>1J!H=tGw6 z4LQND_@RxfYhrB=JB@RzRqt9Eg8}b(9t!$-875VX{3C&SnOIG zXGyEs4dxN|g?J0ek?=9!Q}!fb@Js zvp;d1t;>}Ox;R=YEoDt9D=VwwKH+D4hL02aB0)0n{B$QHecg73$MVhJKn@^1bxbgf zdCKE!q_(c^

    F;LOkQ`*^i8($lxz(*VmLqsn1;@P&`OBl!Zo-y`^Q~_eHtlC(<|f zm-k1qy`}i*yKwm*nWBcTD7f9YgKP~l%U`KeVvAYvba_?>-Do-GFK>^WMu7o7i69Q# zn=(w{uFq*a;TA}BRrb$lO8t{q@DImS!z+`sZHxQna0hJ2y%@UE33ig^DoV1?g_l9H0@ z0p!W6Yciq`E@n%o2@C;3`MZTVPNlHAIsvknUNqX*&kt97p-3@Cq!!!&v@@{qzK&ld zX7o@Uif4FA|CXAcpYu68-oMnx5mp7M0Peo~C0_(2pFR!861JnJrsgw`6%Yg7-i1Zy zbnr_^Y+u;)z==MZh~ ze)=yN9l#@pLzGW>@BQ@2lPMVu@c1=dY!A zS^O;WRX>!t6F0yN;QQDJwt>zqk5032uAHn7uU82jE+N_ ze3k86B?3<|#-T`%xcl?`l!v{2wfQ10g6LQwdwm$qts!9x1SpQ6i_*j#v)ycg?=>S? z$bhQ#LReXjt({p3^T3a@EB~8v1&bBk6TU^DA57QX!S)E zyU8P?pf#Iw>&#+kaG-iTOH$@!b6~ok+{C{nZvp_dhEic}_Ax~F-$S!e^fG~qPBAM9 zsBevW&esRRh$>f$H*GI9?TP?rXASn2C1s9s9rOBk4waM$+0ufzr~Oqje-ZKv;6Cbb z77g09*M4dgXqzk1^j@s@5F?{Qjq3%q6X3P)x%Q{j!BBgdF=&>S(7eH(eQ{OgukM8x z1+~&~P{k@dgc&C>@D@}ud~O^pN6kZ&Qdd{^d=*TTu!W1ziqyYMZv3)(^85VUmbf3x zl&!5%F5`^C>2Da~4!n~n%FPjfPFn8pC3vd{Of6&A~zbQS&hXbct(c zpc)&yzY&GQ;cPk}{p3)_^Oj)QlXR>|* z?vh(1{AInRepb5D&M2{d46b1yr*30&cVl=4`H(4a(^*G6LKN;QXau2fZfRSboh`5O zHnrLA)k)Z*8d7#GppKdx#B>xec(luC9E_j79LR+kL}# ztOV63L%QPPh~VL6mJJH508p4IlJ&J$g!OMB!a9}@UZ}6Hyd5sbiF>r4to%H3<+ZME z&t1h28a`8t<8hG!b&H3joJ0Uf`{l-IhR$Wjp68p z(pa=KRNF2htHQ_gzsP0d<>&X4Y8GO|U1?k18O;|)OXURz-#g(pr+wJXJ8@RuajiOa zxIR?=Q_k9Ys%adKL=DT{BZRcR|XiN zb$o=fW%r9B0`k zz?iTe6*2$N-0jB2hFMsPtK_9*xSvJlDAQ`}|5;TnY|NY_jz-8s-h~y_BoD`Q@e5XgFhGzz^lm$pt#D_QNy?j) z*d0!Zv{Z;Ef$Lx68M)v7p*>GH zjD{LHySR|tx0wGWWGo=)24-i{lypcVmKg#3)Mt+J$#XmHezT@$z1Y%B%{ID?j*P>> z5DNS(M*zhUv?-sheDi3f13A4!g#K*0TL@||rnl60TXNkx5Tgf8dkc7t_Us+2IT1wY zB48l|I!_IRVgf-7^yq@*aU7eX5ff^Ai2Im9S7U*?+R%%TH&GG0M#9XZ{fm?3oI}1B z#59qZv;7GnGR;!zK~sapDG)PuOWuI13N5e1Vq($!VT7bZ3K!nM;Xsb#w2 z%%YuwI$U{37Q!EU4F_s+g92Go%7j`4>e??6Ya@A54-)n89Z$v%3X8_l=@R#^;|ce= zyD{yQBj96??D6Vnnzs3Qaw#pSCKlZ4qsl-au6{DYOq&jKtS1teNKIBzRGA;i(Y z^+qJ=3xMOkc(=|BD+hb%CvT=v@@_%xa2hVKtceeV8h{o?jZjgAqG2Ssz^#{kj2<}} z!8(RA*07&sK2STrHR->-xKRmfJ5&PLd*bj+K!Ap zu2r*E9;a5y7MN9jgr{JQU)mffb6sB-FBV7FgnEu)FC5fw>yyRuUat%TYMfR~+QNw_ zl=hzKIUQ(`keSDnmsGJ#elCUP;KU>#LfDj%DJ0YKu1#!}K5i@cF>Zg9A~RN{^=&v2 zOmS=ciS_lW4N3ttCw)syrc_L~IXR~V$W^2~z_p5-0pL+Yiy=JeAEK=taM9`X2&!19 zrh|R91Suky?;em0dKauVGh7XqnWBFP9i9#NZq9EF#X{L`ofkj;Im}4Lny3uECmlUO z4!7t}ymXBZTopd}s~1DL%^4&0D&Pe3%)ZafD*EjPyb4{1OcEdL{g#{ElXmd8i}{%4 z4I;}EXf=3G6$WiaU<1uSAA3QsBFaEShyZY^rAvJK@;eOy-WRI=eyZM@15E7@+4zrc znwa-Wc&V^UNFhl-7fV5+>o?I>sH)WWlo1!vhbPTIPHd^Vmic$}$>S|=6slRRpijwP z>hI^6S;skbv!b_;Y+5@1E)Qii8~TgVE#)b%;ym-`>?om(G;D8+_1Ld9>4V7%-s2ie z@2pdIN*JZq$FK+SD&CBm=JlZ{p;-+CZyFIA=RTUU%6aYuqI3`oh*85X;?;rLJBp68 z2Z*2z2;{R5mKlJ7Hv2!|dh_?i(=k-)o0%EoLZ8Ab*-Z22 zm5tz{AK+?ZI&Kvz?x$e>WQxkBFXZ8+pMic74I|yv&$ZD0o;|%GW0S@zRr2dYu|IJJ z<(B>q_CF33SX`Lj<8LjS(}IR~pigEGnWI4L*~Fn{Fvn;5Q(oc%e;eQ3)kWRcrq3T_ zwJKDEJph@D*}2F`HxG#GTAnlKl&K`KyOsz{&$HbItA7i2xUa2T&84#|`#~*fo2O2k zB^j3e{9C^3ijS1&=g=SCzUA2$4L@=Jys-`PpL5$D5w18au18B>R*jG$v9X;KKYTF~ zsV1U%)3P{m@!0mMBNUfqwwW~{q2ZzL!z@FmVJC|XtXhG7x*hgow}6p9m9Dlf9+VS@ zDi<=My5d9r_4T#$3av4Lt*zAtT)s?#28ZVbveBOVo^6w>T*fE^`C4&*+|Uovir;r| zk+PHL!3L%oR!Nuk*%xh(LUyyLn$*RQ*}pG+n#%6E362dtInq+!D66>r#+bEQQOvy%|9XOhH^OV8Tc z4~dGJ+rGNI7O5!t;nmt+xw4z1qjNhsi8C8o-Pc3+E>k0-k)&_q#OqJPUmDdHL!Q^x z0a_4U9^Z1G)%|tMH%G^c0Ks>NzR92xKq9QOKmtLw43(i)ykON1$bi3o$Y)Z~==<6W ztChT4`X)X{n!pxwb?tUJRCK5uRsU;>{Ux=%jlse|l+g1mfwAye*jhgQzAYs7D!`|3 zjAwBuCxQ}7ju(E=2vV$A>M^PMs!+hVh}09b`E<7_DOK7rNeD!)g)me5#$ylU$QFGf ziGJ`tArWbwW($ss^Ou%^j^TTwc9&X;xzctEFx`*z1Pn7L5`T7^ni+Q+VwuO_7W+=w z%2?CENeV9E=v4N ztZ59%HG#E5T5c*4f#d;K@iwH5L>*B~&;%#McPY~S(L==~)F&YMtZ+}s^Fg>A%vp=E zI~(#g}Tc{p)#_1Z4Rkikwf z-t?JiRN5vMw8c<-HLIfHd7^Rsw2Z1) zIIXaYRoWDcoSr)k$dZK1Nvld{tW=Kv5fG|)yreTBvwh1DGE^trbUgegC3k*=d+SzDTKa~}ryHwMMW_xd}cm8>po*(>f zj}WI+XCUzvCu5=i#gXBoogKXFDs`Wp3N*1cvJ#z2@qF4VvPylr%$i-L2Dx^)lpW{p zEFh4#N0~iTdId4;y3x+D5K1nye_fq+`z*J5nmb=CYc=M@t7@wTdvk&|^A$80cJKU2 zZrG;#d%MpXXGbiHR+BGcH&)4($#4YylK(t^bZ3U#L#0@|e%8eq{A94S^FJ`*D(IHX z_FbT+d6N-+Kg(+qUj7cJo4+vmwE1cHH=9=`?Tm`rA3nqq;$S(JPgeR+?dF-%cb1?A zzDC1eDtUYJ(F{lJ{`QsmygRy?y!Do@pHq;$hO~dIy-NLG#q@wGlvwP)iotX>NWarA zEzzy*??=DkD)~f(kA1o`u=572ns`IXvy31Wpq85TY1U0L`snz0#Lh#=)*X1Q2i5Z^nb4gOIjBB-onY9G1TXOdp#}jH6`zrvuI{kCx7GWK6fS2q07n4WG5^F zYnL7qd1%F}Vc#JB^y$VPG+VrR{5jd#b%VwBDIZMU<7nUWx1w~y&l#9uLuvtCp-$x# z?9%H~OxVReg?W{UA7|-m91Jf6Nh!x+ERwmz>zf(8^G|BBk`(_G-?THypgVOxpgc^r6VjEI8g)J~?a&BU&+xIoft&WjBqff_L#= zgjc^{NC8Y2>^Z1@cGaPJ@qd~(m4YqFV{NQk96^1d`qm+S{tYiYld!wU{o61M$uf`E z=~l}vK(CZctMRd$k3p+QfKgZ-y6i(jB2_oJr_k+9f3)*k(Sm9JY3Aj_xElJ8pZwKQ zc&Qf~ura8A3ERyzH~$os{%LyoJ_#vKR8_VF{kpNBTG`iqBwOO;H*4`^%x891grS`C zib8;K2ZU8rIo?(8-` zO`@3iZoEs}L2kLUy!!t+} za*%BQi6&6MtoL4NcZ~Ty$9I!NX}Wn@gVFo0wH0X-rQ0-bt;@$>f8UyqMCKY>cCJ}& ze1y1NF%2U#o$s^J|Fq~4fK<94%Ta@7PrN5^QTOEtWfs8^PQSEFkk6QRO7;Y}&D9UnKd*zOwm2g#oen3taU>vo4)OVfwEGU*(B(K}|a^RxdQ z)-*iEac9ipZzQ!|i#gNMEWD5_9UG{(*aa}AzSG`W0s2p5B`q+(dY;D zC#TGmc+%sK?$)SZ)=5OvDC_dL5#i*#mC%Arym#lJ=hi4t%z-56lGvFuGHMl3)z#|; zr*PBG7i+T#^zIhDK?(}jGDQ2&{6Nxha^UtXlF)#x6ey~}EihvTXQhHz|81D&{3>-| zXjYVM8P_j2-HaIT$olG3|DXTftC5E@0LVDI0=VDjU24h zj>?Q!N=5lxgB2|CQ^#~%w)(I^ul~iLOg`~dk^w}At-E2AE4kM%D|bH2MdFnXIUr}E zf_RK32l92Go0r!WYz0lSf-atc?j z8b>%xl4AtG0LY*=i*e>af!+wiKmv zKg}I+rNH)Fq%v_r^aXB}JW?)81wohoZLJ`?QATo1iHH7}HC3uF4RE-kkB$BgDqTX_ zvPR-}tPo@>)RMe$C%f;z+q#nh9uKIXc^_QO@$EOBVXyCn;e7s~WIs9NN&D3TFGJr; zw=Nd7ps)}Whf20ryAnuV0jqaa%e#d&xbes~O?PqAxjZ4PYaK7Hw2I0#i2V&o4TEHeLU#1t4Lzw3^wLO$4a zlIT(H|8V^b?X=v(kB_#;p!rp$;?Z}8Rz|GP6mw}Olrk}#(6*XqGg}nT9rAt^*o%eU zQosdPvRr;&8L8-dE31J#u3a;5HxV3C#8qiQJV|I@nL!pI7Pv#r4t@THXw0|PeY6v4 z`8viP#^=StLD{(ucWT@c~*S{(@D0@}JyLDPntA@A*5DXLm z7n4%Dn!ol+(A6_-b1j`WM$+fc&}1Ay(EX0|?|Zf4T$)qH3g&EMQ= z-{Sl!=M)JZ59idAGHS}APaz>e&h%Iwu*7-(Z~?S)LpL8cgsW2JRRL=O5FBIMXH{3m zDw0JVk^CXeu{d_(wv;rFaC`WjB)SM2$|)bypko9%ynG}ICEqdJ$z<7 zi|GaGBNy%Ca34a|^&jaHIud5;UzJ|nMZR~sstD#MPXNh+o+SZFy%ItPa}RIRD@cN@ z7~i9kB2ovL3gUKGCzK2emX7H{R1lU*!wS`@jzvU`{|0gtD)oZpgSR$f1Wh|Ibm!n& zRQ4^?c1feS_Q!Woo$ql8aDt&ZO zv)Vu!Yf$a_(J)cn^XS$6jfYcLIZB=+oF(T7$+|cUdDu4jbJ{C2k8f^(z5l*2)!x?u zyhBIA@Z+k-0)$cm?eA{bc*Vcod#Z_%B3lsm%R`a~aq@NJm(K7XMwzB3m=FNzYk}x> zL_d5r~AlL8I2#a9IH1WF!l@dbR@VItI?o9V^S>w2GH4Vek4`X6st zsP!jsK*Z<0AaLVAkPKZ7DIq#KmUX$LBC9rO6bsA!C(z)6JM7!<9(Bb>OZtr1!N zr6Y^OM+Z(8GD}%2!%mcPut#{Tpg17eeX{UGG+s!goHB4(GGP(}pmf=02sMpdgZ;MAX!PU^JtjYY|LGb6o}lJNZ3fPQd2Q4g~(! zcmIIDQzUj&`cG^)Q8svBAJy0h%5t6L&x1>|o~3X^I90_V$Z_2Hw}X}D!ijeH>#D`P zPMx_~=!*26x4-|*0EO<|`uh5A#pO!Yw7ezp=6 ztrjFk0U1Z|$#c>Ka*6Jyej{`HGl31wSV zRWg$L{b!^jdiHhDNVk&lL$)=K@1vuwOCZ3&1qTdKp86_U`uVjwQ1b8)HT%!kpq>YX zmzEMoY_Rtq!YdBW>+1MWs!B@6#>VIc^(zR}$=;EoGB1xYL#+giJZlNcCZfSiCnzgB z_&Hl!bnrq<_iU&GebREnn-?%{lgD&)H;9a{HBjr1uJ@V>AlE-F?d;0qHqNB|cQ?!4 z6ngW*nhZ0e?gCqv>a0rFcVK?;?~YZGS^EbWFkKH%-efeM4y2N|aT>+)9{srtw2n%U zkIZ+v3~Yi1^GG}-osx~sR-=K-N?yt%o=1J?{r&xkNuo#3p0$5WR5?FhX?<5;c8Eq+ zw=FWMsjCFDy1Ro+i392HCrhB8oaS|mqCWQ~TwX3NF0QY4cY}l;mzS4qQn42f#u^$L zw(KB);kl)@_IO7C-hwgD*7`$yuBBK)b}qg?1~#_KH+6e^TwGjdXMvjgpC#TpHg)lY zhN{`^`${oh%1fVtFq-MPIdPfeH4DAE2JOuj)4D9WO>z3;&cAnKi8bH1PuE1AmUlAs zQO>XUhi>5D2!wfVej|~&VP?njlL8^7F)A<>V4y zBrvvn5|lEI{`?6h$d%64KNSbbN3gIiDPS8dO#bgLgjxeYl_v^{{aT_=f`V&jzOAhg zaS(YSEF8-W_87{u8a1H>`@w-E)2=}s95|_m5@Tk_0sg3LZS4jZCg1FqmI=o{qC|>x zbYq!i3{u{Sg2U=DF-?X%bacvs;U6AGXxXQcvSEocPCL(s2vn4Hx%i!f5E@#XDmmTD zN6tvO?6kCx#9zxU+71ALoUJZut5Y zwt|_kJsVvtDJjIbgcn2nEK{~eiPv=HS`e`I zV4j$oS(#=s1ZG^te<}S2D(vv#63?WR9pFm^dAY##YmC@0^IQ((NiZG_BDL-yK z_g4iJ6>(f1>U@A#e@kkQjg8&3E`ht2FeIDLt-M;I7os#k^x|!|$src!s zz~I)<^{-oXW*>EeB2+x_-1v{&>}+@Oo3c~y#N;|gdU_8@LF<{hNW=t5qOW3SE|+DS$5D_Hqky5~T}L7z6LF2tNwjeUIcY9VECX-^yU7;c2KzG6lwF z(&b2ab`JbB9Z8cFSKRo!o)mQ^+N)K6?{hP>1U;AF1 z5zj+h7(x3Ow;)-Md3bna{=Ot;nYy}5`&oKf7i^+tz0Lw@K~PI7H8N;5Bnyt?)TycG z_Hw}dJga?7-_|Cx9>ljWjZ1FdRm$g=btM`Ed7rP!3gd3L1l>jDWQ(1=u?|y7<=A9< z-N|YJN?^??Gv=hGevLaVt{Vzy*WUE{0SyR8i||IGzq@`VUFr_87b|L6Y&#dyAh7el z0nt9f!miVW@~~ppFx^5F29OGqj{_(6{i*bqFoIC@^{aZ+mTWGvx^~TG`W#vr7io*U zM-{8*zEoB9kw^8V>YrDD;a_VcKJsuYSL>y7its=xnwggReZU_rlGlGz`KaA&Y>TjX z41$M`sO%b-I?pXS)3)xSRtFGsI^QD^lWs)s`CtgWr> zzjeu`Hr11`XJ!*Dv4ksr9X_w5=}P63RbfQIA+Ibtp``RKr7#RTX0u`Tl(V z8#?|0jx(%HQK|Eo}q-$kaiy>f-RsH$kzl+xD{G`W=y;P61o^T*$+5eie_GsvZ3 zo`sfF2EKALBYS_Dm^{=}Tr`0kD8U}x#Te=?RO%*vnMiw?`O=XVWW6(%p23+_L3UZd z@Bvl)zA?E3r2eZ7%*sk|9cFbZbb!5nJ4|cv29Wap%<%784WF_w5O&pbV_5$x?yIHU$-<{YDV`{I zCuq(ynV_iA_+Bx+57`Z9?zoN9fvN)N{U=+75d6WxkJa$mYRn8(piqo-=SL&GP@Fg5 zSRIr>(#imDbi1?(up5pKX4ag5Og#jR4mL?J5-4 zXts{YTDHN0EjW%vVSX289$cb_)%6|Re0_}-@0Q7)46C;uIIlH67fWpz9)1~v{cI>X zd1?y84sVS=t1WoWrQ%6i2vrYlvv#Er8XkQ$x&O5dTHsJYaqqT*H$k_;(dn-M8mJ(XY#S2Xu&(fZ_*vVesPhr>1UrNQNTw_-9%@j&P4TOX-d zJQXQFoWir;kwj<(87-$Fc%IE`rgK*cSxd{h8KI!$FP%QmW*MxA@}MH0f*{WCLjJC3)DRnGokkNHVf}m2m~1p#SGScLWuR_uVM6B z3fLJq6RHw=X{?H{tJ6HPbbgfPO4kI={bVR}BH~_f2@xI`L1K2m!u)R&Hc7!f;`9HH zo*E;YJjZFpY;n;2?7DT=nNZCNl$6KK>NB$dlL95MD0V>ul=%`(Rtze znXf_%)exFu?&_EiC^IzOpV)tMOiNUioVAErP{^_#5`$%?79G>zyt&|b{G$f5><=$x zMw`F403gOKK4myAp;6kC>#dO5D5RfDyHsiI_ZMR$=3&9*DR0oIN=ns%TAGLgBDT#Q z_Ks3W-Lau(#tXOR4xJ6sIgRuw0LawQ?XY9;W4X`#vNQ^7YX`_O=e#)zszFdDTpq$p z3E(p|m|?mapvkTwAye`ur@~HEm?W|sKd@cW(Zt+1-Hf}`N4pHz*3=}=WF4~3GT-r> zap~A72pcOfho&YF8#7cn$Ga;z+oi}PKKeO0P)U=j6{gX7LIK5g& zv;0AcK)7d9)`qHfVsENi@l9wzL3J zozJ4F+@FPLC}$i%K&Fjoqp*(dIc6;8xHoD5WOTSCftffm;6XQCE1LyN?Q{HTtk_v&gc6*OAoPDDPM(*vA znOdONd{X6s*2cW6DR%_6nhxVq(}dHTqps*y5co}J0$Z|1()bpWbcvomULhyjnd%Na zrN8Tl7I|HUy`G>0%FkebZM^aq>JVMJ9=-k(!vXKK#q3J3>+xchc^FpH%#g}vrqO*j~rI0cBx+hHE z-E@zhTgMnyg`hoUuc1H-{F?Cmv(m5~56qcFl3onMdADozRbvI|;KHiP4zJfn zJ$7-uq-|Viga+`h6&$WY{8a<=y47as9iEmzm`t%&vlTyJnKh?w@l|QDAf+ZyJ;vl& zpq?|AQdMsRO#p>X`o{*Yt0=rfw16`s4ZsXH8XJ0fSs9pG{61q0&l{h(+8vw8sL>dh zD!>x1*$|hoO6@}8E~fdIyzvX+H6g6aWu)M$u4k9;P1;D%V-aPTX~5)EHWZ!_ z_20!qE}+64H4@^0DFJ(`K+tZN1Wu4w3XHr;(PlLNS_$zKuYn-`S?J^v6GG7k<#QEk z=0KLP2@8(6ZJPX17e+L*eGe`d*V59meGkzhWze?KGB=FL*DF@t8G@vIJEWEMFfeo) z_BpBd%stmH_)+Z4qn()}HN1^zex7LHS;6|6j$g(KZ=vsP4Q09N)4s|Wm-Qb>y&Kzu zTSWXaNilQo+i6klfhzQI$*02e?=2pjH;RT=e#J?(TTw~d0aZLzY|}dC*kJe!PWuY^ zdgWbHqjBCqOb}F{7ZVMpcW}-QBr5n?j%OPHHOO_iDcAkoSw3q3-N-(!ODlC9QS~SVINzH#g*&K-QnBBZEQN1Taj)YXU(tmse z=egI-h7QKal|qa`i@ZilU^)pjGD!i6tpcl!pvv~Zn8hsNg)y`Wp*eeMQHmJ`s_Q#p z{uLmu6@>X&w^ZNt^>vh~qDUXOet9{ppOsYbGK^w#*OY_!=k^dJJUBQcqzU^)-7wp59L{hSDqGDBJI@!D)@(dFG^|Vy!M_u0Eyfk zQh_=gB`Y7y7V9%u4e%s9nB|hBv(8nXPCXj$>(Q|&_YQBd!|twC&TMThEG*2FLYDH| zaBd%{62&bD;-8YwUx8l%9~W}_17=fb(6bVVebp?P2_+8b7ghx+c{MmoXkeMwR9FgO zW$bf>*#otva~iq-*|iBex5VTxOMrq7pcMxqLvuU4AoNms{deolrr7WUb1blGqGOUV znQ=rf1ZU^TYFNp4&t31MUOk$No14o^Xyv%1fFU)^BvErdg%V!$r4U!rII4+nc) zti3&?j0_mvDk>_{ItsJ+Q0DTjXu*Sa@NY}oE%epd2T_J3uf@Zior#7ASg-AuFTW)S zOiE)sScUI;_=!3e_m2Lk>r4;6i^W#XKm^X$o!ZovR;_(~HjUp5Ln%xsJp0cDmOob7aVD_}_x##kQQx$}*rry{+uy$9x7V&3rlpY?+SOXN! zVSd#jAUL=kZfCWw0VLi!2t8qgL+Rk_s&Hs(wLbOu`0h{phzaRas%UYF=a&@~%d-$MyVg9rUMQSN5_|S6HG`Z)uymD)4C4z?y{!5eR7( z4j7FGM+-*E>CUe_-T%PGL)V`9&%M9e5Ps!KY3|ZLaQ;)2;K-$PWaC<)smHu$#YoJb z^K;4^-xjgRV15P(3q17Y@A<$C(R8QsXTZw(M*r7!Ie7!VNrhjqqQwO9Nd#CY@MBS0 zT1tR_-__bDN%nJ-Ro_Qpf&I(NjXQ(Bdb6=PY3WnTyJdDBwKk0)@zIZn8i+|FL(zN4 z!wQ^F_dt=L1&^*+|NASB)4(5Y@C@f?yT)(=pS%$x?9mjwx8c`>B~%UgU&mz$t)|oQ zXAE4Cf=30n(+x$E6T+Q{+$TR*fwxQzq6RoqPMh9FVN&**EuTr`S$mTQtHAV~)XzVB z3sID!RWjffU%WVXH&6=y$*}Dj=-PD|NR5 zp2O_A9{6Mboe@|+PICQawKgN;JATd-UgH{G^HU(Y3KwEVnB=`!3@igH_W5h|%{1mT z!qAPPJn+k#?r2ca7I;8_)^C}E2+vi7p}XFJ-52l(s3iH1ENp7m+g4~5Ur z&Jn65l;MVZVwCqI33y@#1!>VkKE`=B>&94BL%{yB6tkd-ElUFFL+3PPkr@9v@F%`8 zb{kGxV+`Ghz{#sS)1j&qHElgS-en@MliqxJ6a!840LIy#pFf+42dnITb$;npVt6ES zr7gzU(Mxn6m4VJZZGAH@9pc~DU3vLZOR&ittRYydziJc5JE)i&GLx3;C|2F zj7K9vpa$=qFCTD@4-y47t2QI9o))v2uYW_un^x_bwasxnHGgjdkmzv9K!Iv=mWord zeKV{x6sY5YH}_@CVcr&1V$P@MPS4)$NEZddY1IH`c=P8uT;9y}Q@;@~;RG*jp4D)I zub0kNKyk!Q&>$_(3>QZ}1#aKgZQ78QMVH)-4R`VF%a;7RSsAQ=OkErQhhlo`)a-~aj7W&DbnkeHZoj|$-6&oGjI^z!(E8Sd0O zqXHvW_~848C6SGPaJePF4iced((_bS?Q=j)m{a+EIc z@$Vd;O^s-wQO^^Tygkdj4j>hzM=Y9DQP?x#WO$5NQfqcS;=raqsYckMFti|BDMYpm zQd(1w?_~<8@ZSM)Qo*;G1G9YmRe+B)JXBEzp5ZV%P^k{QG??Yv!_3T{TFqH3O#n5b ze>FFbt0<n;>h8dAD%P(%qS-$rOadiES4cG2<7$yBF|DC|w7|f+U zh-Hz47(s1}?irgLr2EjbxOJsCeES>JalCn0xGusa{2Q~_!wNU8hu2C=)j~c&k&k8} zg0qC8Y_$-F1&tC&=XEVSqZ=ewFv0m$6X2N-2A7fbL_bc#1;G+O_~z)Ppk=f@j1K$! z+WY(8t*{qYeUb^{c2^7^;#PA+4MMC8 zvJ=l%4pFoKyftfg>rJt4B@o>RISb&W0~+s1g=-@e7$K|D?*&i}=SFiD+pXs4p)Y0? z|8R=4f&R0Q9VBLg%^2!;UZqc2rO%`)lCs1C;*XcUZQ4@xu;K4oj#KKz*-nGSqd-!$jDSC+*)**bLJ;+_t$5F#ZX{8Pip!t& z%?WtpqV%qaTn@l3q*&9uWDQT8u{K%$=Q7<(>*~R`yg)5QU8)fa*}gf}yBvJi4f5UH zQ30O%Yo(fFUrznBLpMINKbxNKrkJfrwFW-7+XLHHXu+v$umU$^2i6qo`}6i9QE{)muJ4FR{dO3743P6bD1$8or!+> zEp-E{J8DMKq!hzMEu1Qc_SFM?dFj{0zW<<1#brO@{ma$Z`o4MXar!1p@l$?n zNk%VQ3zg6+m;bU26gCI=YY^IDblE0=p#9BE7*1*u9{QvXBVm^*%>;$(mOupOvM=BS zq>Ll2pqW;tmW{`fhzHx|ZBIRqVkoOtpoCgCRqfs9Ke5 z_1TIyWa~#7z-;(?w&`J@M*jvU4Uc*gY;DI_SgM zyM7IYcV(661fg8)WoVHV7huDO@h1<7=Az=wJpRb#@BSom!BT$a&Vl|hP2T2Q0+EA{ z4VKiMl7M%yyo%24`vtV2ODgXg8(En&((wiT(H-|c=|8;e+d&{7s(9)%6iB-C>fIsfI+ zyt?1_^sFAko%+#JJ8#V0ROmq&Mc~1WQccdZ#eB~hoS%m$`_EnCO;qk%3t~RT8AyJ6 z`kANu`1HK%=$;Dr`4ojvEwJ&3Ln)KND?}A^K!XMyX!Py|s>Be;DVYoaUj}@o!ARhQ zAuGoEIA4Ur9~EZ-3h2i>DCHk??lZgYv{y+e!Fw|3a)L^P=iRNHnbUK>&ac&%m_-|z z^^@bi?`D;l+~0V6Nu^2NRBz;Dnwxk?@M(-@HbG9oL`R>AZ#aFaj1deDJ%%uaa*kDG zvU37~LKg0R*FjF8!GJpWW5Av44dnIhcmZr0WJ4Oct>;w)=?VY^A|rAkb3J-cSqreD z+UD<^U77){F+W2U0VfELd6F1W{^*eeGU}#hiA38x{T0M)K$7q17JX>cTzsnGjQ?+v z%^5fUDz$IWw6NnMnX4AYs|CiX%1p{!2oI!8RAk!q!`5`H48Y!^(97qW4S}I$>5p*i zt>lrD2LwW|DG{?!u2hw6-kEI5LI|UM0i@dY5jm<-|6(>aY=-ZL5OBP^gc95lY{{AC zvogfR3Ww6}w1gw5a-NF%&X)3f#cwLw1%ae5coxMOJx`?)mT%mPJ^qru=g?Sv^BwuZ zGom>88%Nfi(daJEx|R-2mw?0C+m)FqIS_I16s2q?SzJy*fQR&)p27TE>~#Ujbv7Qk zn{EgJrOga1RESZ2?3t2jUX{@!VEe7R@W|Q#9nEupUpDF7IL>i;K$203CdJIeJdmi^ z(wgX2pJ?pJ1w&4u$6rT6lHJ;y>-)@7krnKkL=r>AE4@+u*73>6UK@#$D-1qF7x!py4x zWETOr`C_yyMA;q!y9mQu?fRC{_~7&=y;Is83sn*R@r<36z-J_!T24uBxbxMU+c9Q8 z2g~THn=R0T+StipE|QlgQrKJ*SyvEg{MCp?i9pcNb)*3uPq@Mf)JE1n(Lf`=%LBK@ z_dO)eT1rkL`(R&3T z*zls>WE#krlC(D|5d*F6H07S1WKfiwT8GHYsJ znAsd{tOU~BaW>bX2NfWz3%f8Ye1~iL_bnBC z#fipKh7-J=CA?itfvdKmDudt-0+rJKuQ!}3DV=d+-y%GRuC5Emzv4pU{eRWEl`KAv za+Y#nByW(P$CEDwBtMbRuoomUKTNd-2TEY2-9c>rxr3 z$q{yZ_g54qRq^5{$Lvn&ZeRzZlvQmj!e;%<-v5a1vEuLA3-YhR+X|C`EZ$G<0dkvm z0sj)edSF*)|JwC7VA}1<%e^ao_UC`^1t))oY!5)l5^ec=m+0K&qQ4vFzk2|glR8O0 zMbwwf2Ps48(97SFZsI|`fG`rTbpz}Qbqsz>vQLi2w{1z29Q*5g9W&v^P^ylv5e~vqp6${ zVzea0)sUQ0lBUr{$;57H8)RCOs9XKHG4U^}lC@}fnQBI!YTpw&9K=Z2UR-|=q70?m zI%JXAG8vWQRdLM^-qiT@%}6IeaN6X*CY*OeJF)4m^b1@0Q)M$l67>Px^4jWUP7!T3 z@!#&cYOutW>?(cY97<6=IG9g%0ePWeN}sHN)Hm4GjH(Q^>sJJ!e{R)WZ&n@#P&= zy`v$-*F(W;4^OC7MM1S*KCpdf-bYFmYkQ3^%7P8;Fn^ll;Pp05@1q*a$)pAgOjq0- zR>}^-B&fAr1tm41PmvVI3}pSx2M_lzz1C^qC>M2>2UCF(LDII{$}a8DrpynnF1g`U z?z;O5M8G5;h_p_poTdR-hxd_)Y89wVG^9ui=CMWbYfHu>*J3{kgWXN0&QOOURhRAM z_{es34$LBi+rQ!&Da1s`AX_U}iqu~^$7WLaN2(F^t-Xqetrg@muwzN{61BqO{qd_H zrI>pOR}M^W+7RAS17ZC}9~aBwm6f>|J(16{Dp3y@VxzsZnl&7)S=y#*6*+)qL$*%l0M*pWe-s zx!F*^vc8dAI1P4VP^kG?M=f9G_9_dob+nrT5&JY5>mES`I5asl5dxHwabKxXC9%t< zA(|xb$rn(lis(DCt#SKD_dzYs6iDfLGQTPjP*scozY%Tyyg#Gf!xx%_SuC!9``8CA z!nonyOx{U7?^TT0g7q4dT^mw==1`ffP+8%-2!CIgis><;GLiw2RVlcBtAdxgB?u2x zxn7PFOt`Ta?*)eg+cY!i$xJ~9(iAuni-N_XD3{@V1Ox$|nU`YL69-5D!1j!#(BQ4uAwX# zW1}W2@p*j8U#b1x?N^f`6hto?-Gj$J_FX@eRxHVXl$3@UPq1d}0~$_xu0;yRlk6KN zY5ns#cl&DmT$-7OEvS~w&zh88JCdP4KPb;MKt?&&ua5UrPzd-L9jTYDe}8ORdVSsF zr!&MYkNSiPo)L!ZpYQhu@Gr=wM9$H$U6;bvNddhp&B@c0O3hfIZ!ehlOvJZM&aol*=c+rH{sEf z$=LV`vw#>)7wOF5?C;%Kd);jQ+4MkV{15=sJypH6tHKg6NpE%MuK#4VU<5Vb!U29m z+y`5gq0NfFrHB>@fQ_-KK*b_DfL@HBn%Y|DsX_nmk5jVYjTP_PHPuE;39U|X zbZP>!_Mu5#a-WjnEdN?#hiOa6>~LgK=zOgjfTdj~smPUw_uAI>tE1N?+%irI zC4BhAHG}_kJ?iP$osKeE8`Eaz-R%KPwi z!3Gkdynod_W)~PFA$R1unbFdmcWJD~(!EOuh%;%eL_>%c2N{X(wM>~niRcn5C_i9gAWCTOs2%gE~@PdU07KG^0&~cF*iTVPcG*c5W$QtuNzYbWG~>D?mxe^!t|Kz z{%B;owh7!r-~R`j&Hj9gnEF+$mO-|e)qqEr*dTqK{%mY!$mXNXUA@I`Eu0@eOWfaY zNdM7w!O}Eo zIqCw`GCpUb~f&u*$_qb%N;mGcV;iD8ARHJN`PV@Z57-DDMHWSGc5upF5+ z5-aD?KbG!&KRxK+lG*8GZBakeX;{`TIP~oK{z}&;m05W9+#y7eb2s;A1mbg|`fCrN zbXIkiZ)U#nDJ~)r-8Fu})c&U54_*W@Q2aVsMUMIF4-4Z#u3U5|_ zyK@BspaFW`i63(ZeJMsedz6kvPo8$tfjacOWDtKT3nBgQfC9?e0Q96oq0R4U;D+XlQ5E?{8$Kx`)+P)mmM-TI;N<}A*yu5Goeou5Dz?d7yp&Om*+3x8>Ng(WmjNE{EW(?a`aB^M17aVa39QqpP%W>R2mkov_CGIxO=zy zYG(W3Kz#Tvitd@cIqh`K#s$ILFVU>B6q&q}kglx>_X+2wF@;g7Me$mh40>g!>?!kM z0($(p>1|tbZC1mSp{D)SkAJOr=3g3&E$y24z4zhr%l+)e2dhpiZ&mP<9 zGrx$4f4xN8N~#{!+9U%L6x+Y+(I?Ly&YkC`)yf9`# z3BfaZJee@oo(%Kl;t4q?N!<_z9L*JOoR|Onk#Sx^`kZkdi5hUK*EGaU+4;dZ&zgK> zH5DA)*96iB?E0$4> z_r4%TZdECwFlN8YP2;DDw6e=v%o?>j)(S+9p19}YI6J}VTc7Wb-1(z&tGZ@3vqI|f zdZ8gwM(^M?DGL5eEs%?utjjK&;<-(P*wF@Az-q;T@i}}*>z(b}m^W*5)$Ri+uD%27 zP#o7V-->>0UH+wX-q^RGL&}aglewCt3mJay25c)_X;u(t!9Q5nK&;mrHc&yly033EG@P9tPfyYZU{O$>_3KwUA-NCC9^aX zZ4lzCl6RjdOTDd!=Sn>>GrLZC-i{e%tpV+%0SF*GU3*tkv2nGxT~;JLO;9mnlC+Os zE~dpy#Y)S&CqUB8Q6*L(C*^MT96$jL*mMgtF^##$V%ea8BlU6*&C9|vJX!N3(=}OM z7S}tBz}D{)gAx+X-)I=a>Uy4PlDVOzd#Gf+Q6N?jz-rNH~uK~7Ey=3s|bD%BcOkISIcxn=#K5L zIdfQtPgLNup1amx9{I+%$WwAQQ!z8xQX5%IOeptA#^#aVi)*m2E+Y@4;1Sr!G3C!Fuc1 zGVS7aQ8sg&fD|rT0R(P+$Z+{-Hal?#Fgvp@`J->l0t&q!`cO7!^kSQd$s6`@DpQ)n zww0ZYLhbK9ty5^%C(Z>^I1%jXHIKuqN!ECD2vZHmEjz(Kts!(?rDObt-AykfL^0 z6o9cqVPF4!)ct%s3o?sA%pK4%FBqDD;aZ{rToWK-x7ZclmyU@MCeX`V3{LU|MSmIB zfLdte)fOY}Kl5`8K&iW+?mtG!G|_-G$}GMLPf(5RgfP|opHxB`4k#DVDf+Fbaa@b) zE#-P71;0lO#%i*{RHFAt9s7?Com)`_&?sT;lR@q%LQSqjm?|S@`pxq-B*y-Cfxd^6 zIpg4+>Y>jFDynuJQ88IgYRPyiVu7z&RIRX}1*9x$`%Wl4m%E$WHhXTcj zKMmVxQQKFTj4n(H%U`hR4Ha;4(XW#uzkI*f)>haI7U%oet$eLwWoL#QZ-o$}X-`-ApU%cu1My{ZM z*B?hYTz$ZWBVmt?op;~p83^&@d3d0oB4URhJuJSroXhLzhL-hmh=_=A)lkQfq>kpo_hB*&*Uwqmg&T=^)Of$N5%lsFmSCtV+SipItI*l4 zSl0@6*H(F(H{%GMAT#V=&v3+G+kIYkPQiZfP5*O%>n!~}6>5aj$>Q`(4Gg+(E_RB- z+i^W4mihuV6 zpMDEqP=Ax#Pi{+gZ?OPWkd>8%47@g95|wo>`g?jRE4T45{IF~WG#k3;Hhbo&*Y=`t zSilxSF=I)(f9ZUGsELZn#jvu?@|-Js7uY7;ee}?%-onjoO`EtuGqr4as>*l!_r2gk zg$#|+v1)lPg|Dj0u+ZZ#7PHLc3WhD^GeUoc6LlUvrsIV+)H0n(%GG8wGk+cw3`-bP zC17GspU2UeIN7=&ysOv|bN3*j`{G1Kz}Nab>_~sE&d}xR6dU>qQ`8aOjR)#1O^=Qyr=R20JC7}=t*oj@ z0mE{Z2jsxazW|hrod|dj=Qgh+VlcmRVM%tRKM;e7&_wP69y5>kpq1I573 zo#L^MRhUz0me5VusC^w=h!#e;H5GLGR5-YEZ&Fk1_0NGzSs!TbUbgs5ay1x1~m zeLEIz;6%m>Szk9{G(9nhW=+!OVPI(d!}ds4{ry5CPj1$}uKz9s=J&RC!Nnj^QeIDW zxZ?NxwT7yDrO7XG4eqCPnN5GL+S=(hB>2dP$~UbxzvjQ|w6Tz{E1WJfNn}OnP@Dmw z9d?%CU0qoEdtqJ%JE@uN+pOO#hkgZQ+7D&x0L`}_Ry1TfJWoT>Ew!Ei5=>{Wm9bay zyp{pkIp%CzTw~5=C6Rd5&(JkH+3PxzHGR|W38l;PF`ty(pK^2Eb(a+-NbV26*yjJ> zE%qVdI&68~YlIwW+_lM*^LS=C$zr-WU!{(<|mowpTw}3fUDlrrl?Wa6QQB zO{QWk{+=GXqG~&(Fay8Lur}7w?#Ua6281;&VXx%YXmX!~JZtAUcMk2yV)@&V@;ZI9 z$|3FWymvM4QUsy`^ZJ2IZt`UQ7|SpB5{TofGk?j;R1+1`Q&|61M|`1g*afC5B0f>s ze&mw$eE&3S(xyQ)Hxolau7&;Xqo5uVdLLQ9mgVsL4|}nG7p9K{wbAwB6=wHnlD>@x zyf}I{nY70HRzE5*sYj|8IXZNZvl>2YSfr58}MkW81bAD z(9KcC{;-C5$e|GbVldUW0Qg01DX=|1N#8{X{#e#6RKsTE!2JAxg?)4kEEFeW_z#H< z-M{uM2pU_vImJ?h0_*<{?XX`*?DFto9qV?`i4i+2Qx|3t_Yq8BV(|*XTyvj3=DVek z2?0sJyw>-Hj{WdIc%My|PyhD1wD3<>{Bd#H`dVfpl4T_kU#gAc^Jc=1g4GSnsic5r z?*YN=H7=JyONoR>WKo(338u#p&4TqX^lj96aM-V(lYC68Ipxn{x*L2u{5D^g4-_NF zB*P+}bcu`!mpmK&m?iS;HY6*WNXJ>n>TT}SNfg1D>^%G>lDS>3fRA*?obIcH-Kd)J zTX`oB!;&5YMlUBfx?EEUe}5li_lUm*h=$R^@*mFn zQYhISScL}Fs*tFsYw9CgZR(u~0)U6l|gR`+r_YO_*LjcLSuxW zndLLik{NIpLn>dUr(S7dlBI_9>bj>L81NV%FBhYM@*)!qMtllGr{ac&ZUGii(uIE- z8zb>Z-X2bdPb*#O_xT(C3QdHxec6)%yFapm5U*HF`9D4|(NY;ZourJ-Fr|dzBdrQMW7_c)39V9yryf5iy=dpZ8>qKKITvU^u9cdsty}Ij zdD_qLNrHR#L1VoNc;X1LbSj_=S6$74_pQI*NTg>&Ra1Y77Zsl<5@xIO4|%6T0<5q5 z{(Q*#%#vQpgkk(mQoj-l!z?7`b(6PbtL4R;k@NWg2Jlvy%=_8QZOM>NH1~50eE7WA z4H0O58L7XfG!%JD%mBDL&vZE@e0t=X-0^0x^nFMn zdk7KHmxmb+w@E=pmQo}QXZ@RrT8lc5sF7lXCM_IB`h$b@_O2T{Hgg9p`c$r@8WGE` zwWeBda|^b!txS;1WAu$F%5qWJh^xr08ZSeD3}bieZcE{d(9lmAiN{JLWsVh}-jyZG zKIJyai^NmKmJr-Ii$dPn5yJ}x zU39L&Bjy!|162|bym3+j)?srvGC-(a@3q%rHFxEJajNBu=%cD$6 z@Tu-3aJd?`HY7bQEmmwlJa}i19o2JSx#{2uHMZJlg*WqlZK(5=nWzZM)U{-oQ7H6@ zTmLvaFF?&dr*F8PVVYQdPq_&G_&3K$pKZC0;)_=oZ>GLGK3w10D=``t$~7zTl?giu zi~oFFjLZ8!Q@P9j>`}UD*L#6kZ$Q)~xXR{L*GKy|dHU9OwR0B`GtC>iI7tnw%L-vXK7nf(Me%ckFtsA6?-aC zit+OoXP%*(uJ>s-AEAA}z81nXmsb8gYducnPt)HDXuVyP#ZH%(*A93;kS_Y_Y8mE{ zHu2BssWpM$oc%ci!$`b*p6ROxh`cr|oz|NZ-Sce3d z?L`LkX)tCyeMa>j?^EBIp!M>PcmqLynzxwbPgKv?Qm ze*GgMeR+8olaofA_kKY`-&{NPA35)%+?YFA*=(lrGRuk(^@|hB+y>RWbkQ8Vs@U6S zab$6?{io2|1Z2zf++vc_nA`-7a12fWu|{e;{#TLH9saRY?N47Hb%3STTp6i z0S@8pF8rqNly|?}G@I$TcM!dLSJIMW`_EyO8g)Q!p3)I2nGgb$s<1fe{pfr3O1afc znm(iJgJg3D^bQ_ybd>(&^h2k}l9C#qtPovp+%7rmi^CtE2*{(4z)TXd8v*s7ad&`z zZe8`@o7cQqX;a&W5_hYA#m#Yq8(p_XHFO)|X3(p;hme6ssAgKA`9}SB30pj8?#m;J zm4(n)Onr%NG4PKVY#N-}{HgJbuoT+Z@Cx(28W#HU5lYY|8FS+qkC6&B(98?M14-}i z%FDmTNTni0Oa8gd-vpx+&R75iN$fRJpZUH9Aa>`h*c+3|jn66zi=2slfhI9VROrmLkrqOV+#F3fp&#Z8bE3fP1grepZe%lf_Ug>(I6Un8)$w|v9LLbIdlvG zVuX)z6J%yGFsKVseaFAUwE@`wZkHDX?ZRjsf?b$jk8wpCKf->ar?`*7QK!rv{=r#& zv4y$023kfyX6M1jDz6uIU(*efRVCi#TFa3EpKPGKVrs>Xg)Odv>!ND0tOlH>e!77R zyYbW&>K~{izMsW!O}J>+sRC$$CYAbuGWa)*=kq!}k^Ph+?lh*ROLcrkN>a2$p`p|7 z;^?Q{^&_oRN8AZ<+l;H8!!t z6<$oS&$CQu&OqI|WNqQ%_lLN-_Cp+rDVA5VpP(-ruRk#8zV&=Y;3R(Y|VE0V5Cu(8<4KwrV;|O0|WvF_=Kz6w|KmWmGGIk{rQnCIzCi`Atfo4DV z@Lt=8w(3(|zWYw{n_dBa%g$^HtnH;-pvd6kAVsb&2;FUU+;L)ZW~!{|8T>JjgvaY! z>XsPw)#~^4#m;{Br57(UvE`{s`8vx)owJEhO5~5DAKaxwu5;mQQ_uG}R$m|b?bFZq z#h|=tlXvicJ&&{<@A4@-jB&8Vy?td z0sZ6Z(4crshCeL{C8Z8T*%uES#{DLRCFQ4JrdMG}UunR52F0SmJShDYMUZ`k3A76h zR0JKR)S<@)Q`v1IATj?GJ%WV1l{&o<7gua>?4!9@pY~<;mWKRlz>?U*+n(xQCID8^ z8=JoHXXv2Dc%^OhSbi#GnIQ|YDBc(a6JvSvD?m`5Wn-C-LsHwNYOZV(6Hbb{6Bizt zv1C0|RPeR8*O#*mSGP8d_L^M7(x{5j+F3o+TsXIjsLnKetCB!!Jt_r$BrqXSRV zu=f^Fi3j&#rU9aBr^hJX{Y!E!Hc)=WYP=b5>E$6wiV=mWykXD>1-BQBK{ZB+ag#tN z2TQFr``O4tf){ycnH(q`JNk)Mb_y(Rttay z{1QNVdh6bn|K(}G4Uj!y;&kGy(XHn7U}mGJ9)QRe?g~`84@fhA&#hFSf)$CtwNI7quKA!I+e?DT z2e*NPTIARWyE;@v3A+g(fg=qVi!2#XkfVkT+Cq*M_=MKNMr;UywvcNHSQ05v3JpSF zMjcQ~rDqSp?9}4=LmD711ZX@80orh{h-;bnDb*V@%YTX|pyY>Eb(Fey<3je?!e21< z{fx&>w@L#=?usBNwukPY*hd3h-|V1=op!j!3{cMnJ7{a= zMnP%m#mRw_b_U6zZgKr=!O)l1xz4??1byAy@_IPF%pK9z9DM;8{ihZ7)JMdt)G(g( zWVI!zk^6y2B<2nEEm|an)HV(_q$piQHXeV1U3@$IQ2Gm(V~w^*g-%K`%(dUk+&1}^ zlh=S=aBEOZx?uVwf|mA^1N(+VWqj>(fxcftJ51Wpo9@Cwo-PRXPjeNo$ZL_cbHA}| z!B$kgmRUT?{GV&aYoVw)^A!;ZSM{wL{J0{0y>Gvw;^AC_cwVMfPfe}`7~Edv#%H$3 zY43gMz{>djm?|}@Vr-zz_Tm<&6J&R5hfgxc)>KPwH0dEU%F?q(h&P&a1o%>nRO&VK zY&lp=#ZuWDZ!C>r>`%wcX{Zw<%4tk>F4Q)Z@UJ-aIz=v zuAsHi6ZjqP?>_f0XAD0H+r1dO9nvHCx6_KKGs_!3@IBV>7+=HKzfVk96qvp0+_SSa zmyL@0^KAtwdRnA}ot>M^zG^A1KSoZ{Cu!`lfZRfUhyBI%&zV8T^zkRDn8O03=sAZn zG~yNHm_K`x!$QCD1>$`}hno75OrajqWN_R=F3u}4)=MZY^DI`@J?zaL&rCA%Teh9w z?lWcL6BW^7O#DJp=Qyw;ti_0H1us2L`(i(W+MKQ?m0?mG6g1HMqfV*qbKbi;gngD4 z{!)H##!ZeN=f&LqlX*pz;xRSz#ykG4+PzWn!43{aPxc%~kH77UF2H#_cugw5OEO~dZBm57 z&ghDSjDs^<+o`nlrq~2upOUSL>VoH;X}^7S-?ckvUi?ziE~2P*98JNM(^4u>Em6X%m zAKS%18r){o2M;tueu;qzn_Uq3fJ+SWg6W!58gf;UP9%veAL&r{acM%Uc+ApBDdO)_7F-Fu zDD$gb5mdd+U7-B(HRcHpwP3sCEWATh?v(njid@aoosX%I!OF2|gcbVsq<0rL3mo4SR76u5@KD zWDnde;=p}f{f;I92n+oXn8rV#LDmAjE9~;D))wf|VnCJOY1qAP4hBAE+hMm@U`g(o z3#T{h;tiryfE)HHDoj4Lk_UGOYKKj*!xACK@Hq~^>8~FDL(x@6Mb&j-LPAnn7#gHQ ziIMI`KtgKh9OBX-{n8x@(jXmz^vp=AfRsoK%^g5W8kCR_&~LuKcdc1>tuyEBd-i_n zMyVM{I<(=`_lJec(x5FR=_)crDNymyjRevwf^(Ff7^Gbp!7U~y&M^puPf?K>&^mv zP4QK3eIuMiUqi?^<=J(=72F@v?-6=N93t7r1}9Qvr+$_o_4!y?=i}-ea4T3v3*i0MQ4PUn=_IvfuSZ9m7QFl!$Hjl74TP z#>v04wJ+ND`kD61>7P<#ij%l*4<1jefKDJ|R>8EyL2L9Viy&M4@Kg`@SiE7H60DF3BmnMc zAAa4TMYx#CPv3HaZ}pF#MzQ(d%#6%$J;gg4TUlp(8Wqvpu3>M;)Qdi| zP8eIrBwSeqWQyRDh$-*gi+viAbS@HDCm$o$wIo(th#FJ;x2OsWxQv4?!>6NVhx!f!kkXrOVhF=(cOQYR?Q~?hkba+v#_|KDNnk_QQtIWXkvr-DX&6nDG zQaPowEKA0F>2(|NE*kDrpb53X9D++AAI zmin6lYq9d_0`di!YJ-4h`kHZ`<&OH1tQst7aZ(bjU?45I_ z)1@LVR~T%=?Ne!vUMq%eei*MDp1}h*-sp(#d{BlMiJ&hgP)hi$SWjG6_*le;SX8SD z#E2dg*n>{xZfsRZz+)Yi5WO%Zh>1Y+?@@Rq3HG@tT(^UT6nH}jSdWM!0=bw+AFu$& zT9?8g9y+(CpBiY@fVC~EfSMSosw>NAv1mG_gxT>%WSn(QQ2m58fegdGkQdsV}BsD1y!XBek^ziY!KO*QLEOlA~ z!}jUU3trTNn)&Y-8Q8pzjIRv^K7Tt11NP{8QHX8i;E6yp;L|MJL*Cp-`a+LQ9fD9) zEgEfGaKRG$?+lyMh5pX=LhB4r>>*npQ)!PiQbVv>x1}NyPlB&YJRc;l`r{sF*iC+O z7)CMN%F01dhO_8A0A~8Bx{Y@QCraw33hM zwdQHkwC$Q?M6jG(v*}`%nQM->UW&GIKB=j%Hrcc_rPO`++}nim#SK2;QAwASlM9`% zq3Ds$xRa~*_fLs8`^$_;qqU1`AK6j6{vv&#{%usuu)zNXA<`q>5r>kO3eUAkm(ggb zEUYoj`Ze{a%||h=dh1bDXrB)s*IbFZdbVM~)MXu0{4W{Zfg6h8xIygiZvHpST_QU&+kL#=xFrEa((cYo(op4qGgmXE714c=1 z91);%RSCgABtQrea2gY%H#GsOVR3PoXEZ`!Qg))qGkqI%XORMj#fiaQVYNG+$0MCePy$Ya56k(`c{-zk| z7i$B#n8QQJMX~`qGw<+nnCLNeR;u1^sVI6`q=_xFt}dhbSNQ>+nkJP=Z&U@gO+F zjlI4I&*P$@;1K>}*>XL9%7LmrYQT760iA0$1wF1D?}h$KfFJlDwm?qp5l=p?P$9@S zd6Q9)9;m_s;Wf=^l5{1;%r+u^SRqoLymnYHs2BC?j}6S`7w9)q`f7omb84RLHz-wf z2JJyT+Y+cneGdiJ33^Z`!odJx?}v5dAhvq(-9vF^jqj(*4ArKW1C`_roBCL^?YCW6 z=!QwIUSOLTSWbz8AKwiSL&5=z0D_m3{LkODcnQ zwtB5<7*9*;6nmdTWfT1ZYS0j-h0Xuvy)xy|8Tmas{9fJF8Wd>+54( z&aOO5cgZ379~gC)8cSn}z-QahZ~Qgz`PpXU`4)2ceh}`ob7>Jof|sDW{HBA;UUsj124hOMlN;ut=Mwcw|qZytPmp>v2Xqxu$sVy4F3)lk-V`&I&3 z7A$;f)g)!IPG?=VsIHR*M`_jBTR3(u-N3oRXr65nEH(HCAEC@lZRj=2N)^Va`&Pf+q5$SovC*U*iG9} zd5pb*kL50q078?E@Xa-06dq8L`_N_eSE893EOGoVc6pZ_{rle8C!lB-rgCM4mPShG zqguY)z{3{Ri>lvxU<)U?*NqIwYKqSNr6^q1C@P`8X{eFjaRho+b8 zMZ`iyHq2Ea6>d|67nA4&A33x%cv_o+qW;sA+CM?koPJT6$3zAAfb_jkN6a$^11wCr zd(?qoi>Hr&^WxuEpE)x|U`F`f+<8{+=?~H_CeCKP(WfZE1LHM%U;<`dCC2GZstLO< zzv;!cpLT>}Aj_UEc>dzOQyRtwqR5_t(dI{-#Och*fFU5d02gl_UvpMemyzGXjoutuM~z9Sa8IUW%+SHd zR$0%+%EwXah<6;%l0_@VgHY&sGx!)O-0^-Oq7xqhuhgud4`l(_LnwhI6oWEEtSbif ziVfqKP|+R!2<$35S!e$C^CogcF5BK=4DMf0Io`HY^v$Hqq)=$*7dHZpc(x%4KPb<6 zbuq>ByN0&{5JJ=cWCC&)a&>ab>O5>PK7~^k-IYf>1PF_KUEN;ZW^%(VvtOW3KsrDU z26uQ?mgXrmxA`rwDpS+3cYS=|O$} z@B1l87z)sKvD9}{1MPt0_$c+O9aBg^BHKIoVj0+(hIy{SBct83N!e=q#l0>k-h+x17f7~r5^yQN6==HG zZ}uthq&jTQU96m(Q@$w_uuSJh%Imr8`JJw+*}CNyF4Kd4^kCtjq;@r!{hY+xN?_A- z==Sdvy^i()Hu%(uYD{bL!P!K(Z;YBvQHKjeA#W--9!jv98#ONOPPJy93|;-l_`n86 z4PI3%L#kC^;r9{tvO;BroM2wHlfxP+4Hb+7U`R;F!8hd!35osLn1$cpJi<7^3i@|H zlcp{L+TOh~s}#ole~AMPS~4Bs2XWAGpN`kd^5ECzan2(Lv1~tR*hp3Lb*7`$=oEw+0EHyx%(puTnw4lpyCvTh9%aih;GUkk6>yNHWXD~Wf_ zejxbIdLukLnb|OwXu+Fk?Xf%Qrz;;I?dTwZZ+QWM^;jRf_o!U2xunx{FHQT#K;pq3 zVRa0gzVJZ&Yirh0s)1R>$47!m5(aHlr^lvTRbNGS@<;&2-a&;+0#N@XAgK8WafC#| z4+IduX}~`NSbis(XH*5oF*bH#Bb3ARJiq? z>IO_$yY1!FKua77&5)-uGDrf=f6#~z`%|vn8Kt9hqr}KfI zI4%gAhhKJ4Dx6;seF-}3ggTDvh5;)I;2+RKcjl9?3Wlg`+-bnWQ61aK59XbN4>|U7 zSBn>h{%&S1(EmUV4tcb<$qjm>UT!1rGEqSxJvF}*`XsfDx5I5#;0C@eM{z6b5~@YN zU-14C=6W}98q7y)11Z0Uu>Ni%@3e%oK=?gO^Z}X8C(Jji5hqM)Gecvf``f^mT%j17 zqHdnmk*}y)+(8)FQb~R(DqoxymyyF7^TBg7-%-Dw@WU%}_`R9`iV^d43y&H!T7B1j zk;FN-(b~20y%wx;2$1FLziPvQ-i%n&b`ntkz8QEicKjaAk<^ievE%C z>Fp4Ua)Njh?b%*T>{RCnrfdoxD0MRvJJB6fTtWG(mk5mb?sf4+M^gm6w$|ozLR;e- zC0B@YZsjPckRQ*oC)?4IZ>YX;#pC4BN=~vi(P3D*RNTD`4;wCW$|}5Sa1T3un^913 zv$^cA;D4N1;9CxSt{absN+t$+pflf3|77iC1Rk8RqBYz!i*}sL({szh$Y3xm@3RY= z)`}?XmUR0?8Zsv1lw2Cf7GbigZ_3jwT%SpA^WZ{Z66w#h$OIYXGZj(52FAkVSZ{n$k5X9ltda&sjy zT?~(fsguiUOkyHA+ZxzE2+_WnmxTVdLf7PaWiAr}3E>^1-`{F~^!-;Gc+KwQUq0S} z4=(G{@v*=MBs)bxIcs>T03x|vFF`Y`4A>2H>_uPn#zLz!5aY6Ne=}^dkYeOH^m5fc z!NjBa-%BQ-TeUKM^xgFSX*Xd`+0m00J%y*0B zadF#U9p*Yaq2;5*Wmh2ev*DCEG*ng zSFkauI5{yPBFu5qR|#Da^7j|wOC$}|_fLUH8IbeS)99#_uYHf{ISY|@D(4XEi7zv4 zZ)eZ1fUY)gpL`aizW?y3UqvPIG7}8wa6|x#ezO;wnTj`CG-|yR{IrzN>ZK z*`AtsZ@FN!5j~Yq<>ETm;4i`A&4vuLF2Tn&;e4%h|0%SXN@AVN-~SloOaAp+!*4NR zvi7@bsP0~9jJ3^Hm-`TD3h0#yo#Q8%9=gWe2!3>&0ftrr2qKW@kIg`$CZze-l_D+3 zo*54-wzaO79 zTIIQxdKXb5^FfRJ&QbOv@(-KGvF|R&+H9Ik z5;0I=sw^6@Zcg=BkyTMqv36Vck%YM%#bEe9`C9D~@c^T)$gXd+WAzIMe|qtap2_`? zX5|HCwsmIQ-UMOoOLKLqM}7>b9v``nemq=&37?Qwq}SP(oO3jR-JcHXQ;%uI-@$2H z47giPOL)F98X<%ASa1ic)axqj9@M341X>4YOsJS?xz?x`=@ES*uN11tJjhvXSB5w} zK!p6-%Y+X2_GGY6*ykUfXf~yneut=G^dVrb3>vzw0#nQWyqgKdlBhmLd&NA7A7*dN z@CV>$?uflvq~v41eWAKveFQ+3fc1e7p#gubz6(=^z)T{~i#7W{_DyXE-HY7O zD6+KxW6Sk0*p}uF?XuZ(O{?1`0h-5(Al4gP6QFcR#+ON8TF_@((a*IcbR)YXum4K} zb6Ex4XEw0uMJuYaF8MFhK`phn$}4C8P1cQMhOjDm_p_ecI&vm(X-^4r(i*xN`+U3@J6C6H=K59F-lXCX_g_6ySb?;ckEBj6T&KS_nD_s{DYlDe0s-+6qtM*lE+%J zHP)6EL0@LeO`n2w>J@qh(~a-n-;1?re#%;!?W^aazWY}k#N%Z4NFBx=^$;J46zy{- z>d>!0rWUKd;}}Ayv=D)-`{!5{;B(?w54oIFo}}1?X8TV$BzgqbkEqKurIydqaToSy z2%DNO!{t32R@(@GdbxYA8X9N0HoQ{6+pFE-P#;0&ppL-++0tvxLp!r~TV=wpKmX`B zvYbB>{g&%`nGPcAr8BkIq-YG?BaRI*B~vvTEr0M6YF_Z4?Hn}x0ii0zl_BMPoM{$E0ZKEXH9O?Bk1loIpX(H-cE;!2e5e|Lc{ zgcH_$pTA^jTfLzJg~mwkm32KMgvG}!+}!;9WbmxGxY$I1kt#rtiF}?xBPcTCIBJ0u zmCy4puH;d+6#8JWo&SaRy~H(TFuv@CT&`pq_pVtL8QSf! zr@Y+fg(WOZc)8vzto?l#7ahTr+5mt)GGQc*SE zTqsvZ3rasQ7*)WN%_k@+D{kB{*VI%$C*X=-P<<*_=HyPo?haMFA)B#LL-*qqQp~*x z-5CB#Vn{l&b5<9P9oayi`X()qvYED&XIw+Rwh1jOGmwIEl_K8YhZ{EXG@b?VD>@QQXjync2j^ z&9qy_8=P-i zTX9KYR!-Jak*==k8mfu+YwSLmk|vjF4RQt*my~DKKk}F6Zv8mX3aw(H*XDk1sqiu3 zy6zbFHj~hZPYK=na86QO+_;hci)!6c67jxS!+PSWwh7=#RvE6TnX3g&?dc?j>o^6{ z(PIOwY=tU<$G6XvFxs-9{$7_n8u~8_`I^5!HXn$PF<^=%L5x%Otog zH%K-a{1lA*^K{B(O`I5%Q!N)3hNn_Qq2?7Uj#@;)Leq9~K-O4xk`71%+a~$l&;GI@ z$+4=Gl?qg{K0=ZW>Mkf3Jd64_TM7M{odj9}hOlxLfB{SX{&aT%6T5!^QjI!2AG?cy z*Vzo`+JDw*c%j?#$!14P%RJV(JU55r4e#*rOn^{oy1L$(0egd0Li-ZbrEj)T7LI5a z=d(E!l#-19nVUDMDDMp|f6*G`9|rL}zd4Z@Q;QiB8m;uCFWC4?t&M|kDl4na|2Vw& zF8$T;@NiCw06fAgLnJP?w&uB@*WKS6MRkBM?uRm?woP-M^Hw8i3;$@4E7k!|*<+YP zNUs_%J7d^_+r$jDnL|P|^wLK3h#c#5%PGHVlQU-T;gQ;q0~95ymY#3FrV4i9yXWs% zqVV@BmQq1VfhV?nFcu$?>I69rWpt^G_mi$5=WmlBB7#3Y5{bqsjzZjR7iB6Ycr7eM zw>rGW5zxK)qW+ZGsB&7|@3D52aPtCh1sk2dv)!;JTVIHWq!4quS`t)~v3^igMC2LG z=;-rTXLV^+N%nAr;fQ;2wpJY1!@n|8QtUwWzjh|Fm=`t44+UveNUdxt3cd)@DeE>GKJxrV(b#=V^v_ z_p`UG4F%DU>Ma)+VFu9{jkt`3yyHYT424%wISuSu|?-Gu&Sju$5;){CJB4ridUyj-#Oda#NH5ZumVw z5f4}&NmYaRI4(a0b@sThu=e^?ZU_}zwOdr^vx-f!57fkzut2S-kYjNEV8JtF|D|`P zN8E#47`^-6rRqOa`tkjrab`A|NLVz^WRmW^$?njQpR!?!Q{LsD={Z7*U6Z}Mxvoug z3Mm~@rv8=B`Q)cllsX8$EvvIs5cILYYm-sf<`gsc;zK;bSF=JA?PDb~$ieH?nySR*;PUubB|0>UC{cy)lNwK}Cw+N@ zL`vIqpT2=@uLC2R-;f1rlJry*?M8QHS3Su2hr+{SvAgsBQ(Nmnx1L_B`MgF}mU2x* z$4GasUG9Ez8*T8$ADPq%zaM^66RWI0v)k0G-^&dVt8lEbGd+4^N3^;5E7NL%ekh-_ zYbGu&vEIZN&k)(O_1W<7}_%OPyG24daz^keV0wY+Xh*|jd$ z^N4ZLO2Y42e=PK>lHiA!E%~SyU$g9EoC>`_E%ahf&<92E!U#=~7(0I@MO`lmwK*J#idqOZ>E7EL8VcrTA8l22Y1;fPBNqQ7b*j1F zUSeZkfO(WQx^+bA9Cvufl7e@Do_o8G7)uYa}U&it2b+Uqv#IdagTk9DhEy#nUUAF zma~K%^6=S13)!j8_8^nAk5&LK0JhfARjjro8$T;LRJ!*|cnSl>pC zGwS-K!ecuPAqNM~EgIAvW3za9eYa1Z+$v@(gCu|_B!B1v+QtUi%&v2?gRe-~e?^34 z@V;nhISFUg_OT+5&zdQIQCwTA2HBaj8(0!bk>C_hZ+*zI_Wt^1x!w}xhFL})0JKJ) zn^9P|2J`7DOooNnl_{_h(Hd^m5x?%~7y)Ol_B=f{8p8MQ@zOr|d2mSssyoQ|Lq{3Bk_4Rk}rrry1$!DpW6!U9f{P%9J%|^5@|Df z23+`mAj2P}P=hSD`b_tYPe_0X2V;^B_PQ^UV{L2WR^+ns0IB0unpf!?Q9iD)9#r1~ zIq_kiauJ?vwMbsLiAp(lw(^`_c0X?;?#rE>&*n;TVfP&{+rC;w9b>7*Z~FzL4;tS# zi12ZIn0WrcrECafNAGU)ASx@dA*1Ew<4tJ78z&R{Lgt6Vf$`#1Bc|sD{=;r_0^>Qq zH$9;SnrEe@m1N8ebOj!Z0E;L~$FG6pH z5m-u8DsqD()Dx9qeb`0?ql`)FA~=4FCN%#_ay8qCz8Fo1i~pnq>IzQy;DRf0(CKDM z5bCV~^Ta{)E|3Dzu8Gk19t{iVha6Oxgt-C#IWjl)wevG4tPhvDfUgZLHZOazO^E%= z&t{X4U!97PifS+()ozE{*j_e;4|y}1atrjnGF7?pR=ZMaYEo$~1x4AbGd4mAQ5`0D zCJ^#123mXNPD%OnnEJfl*0QUNj%*FejUcAI@F@`;Ki;6yx^x#_fV9-1(S(uXFg#SQ zWk)62cCmt8s{R{^nYrt#1+ulTz*=hmO6qpLEed*(H0_qJA*2ymX?i%@K;b5oV4E9H z8@pg6RH5?4T!_$_E>(y+|M|PmiFc7(%d{5Of#;t|ab&9wd4K&r|1)oC(Tyw_cW9oy zuQwJSILH|;z#QZi#0zp#ao?ad!;iZ7H|W+`)IE)lafpD|r{81F7hNCi-D^eAQ& zy~WrJ#K>a%m8r~KMhjN-}W)9!e0Ea(ma(yDkRH8$o%~-I#wam8v z7%71jiFCECh^)qdg?BM~MpUC#+=zURh|ITF!V~o;YpJBO2A9Fon$LA)zmNJ2>zJs~ zhqbAGU8nptDLS12-C!s*i^6|@ukmLz8XxES|XW9RGDHB{m0BLqv)!V2Fh{9 zptxkHn|Y~UIn53T{^f$-)Ng1MwUAHVc4%OB56aO&A(L>@gSw+*c*9f?$KN1C#J@LF zP&@?kmibMdj%2pIDzV zazs3l8rHfPc=r*0^PU&XaR29q_?--X!)OAEa$2+We_vLjI`e3dC5kz9N1nTN!aiel zw59|p<=hpbV31}#%aapJK;9vKtgRm;Rq~g(TVDF%+x-LJHuHIq>>~&md#jn`=#1oAb4(O=so=l^cRsW{;cG zG0{&~DxV0Uj{ka{%*d^zjT4xAhrce1ZFw~39pnYhYODw#e19dUkB|t9o>6g8fLDJE zY+g#CVGZRqVi}CzS6=9m-E8=o&k6{&we@#%zgq1eCRO1Qrk~R_f1Xzw>9@_F)19p% zp=zT#>h8MwcSRP(_Hk3^XvNy9+tu~S>&6y?G+#ZL^n$&0*)gNcZVs@F=o8yclG{(T zxD@0oo&*!$>NMc}K3$6_(<4zqplKbRRFLny3b5)GZKzEDCk1MZufiSA z%pTHQs1~_OAwtMz;!&QwV4E7G5)}!D{l^c#N$ov*2?(TYT+wDJu0^k%vrEb4J@cla zO?;0_^x=J=uNkpcj(MtGjB!Z=yTB}b_c?!xy3cbWL%b%6fhc2#;ePOWyc?ISCGp_rB*a{ z8mVCh-oXVgK_!@2GBgw*Qih4)17@l4F@dt9<5qRZi;0sH97KnT4uNk=*yG&*bDilA z&9nCl&0vIHB2RT22`gl_h_Q^6#B-LQX|6d$jffU~1L(*E?gL?z^8gO{Nq2C7&+ zeKhT#$ELv;8~qT899&bcj4@61hsq1GABU{h!a{>@P!3)nT95m^pyrJ&Zri1bGd@XA z4n4vy-Y?Dm`ZC|cS|494Lsmt$#44q1Ss9Koei?lWcsEG;U)8U;AfG?+*BH%M{sb;> z9g5={-3m~A2XYVT+!e`#M^ z+U!9?TIqZZNW|d1`h*p#&5{wN-PRCP$29O^(Ll$-sr42HfA*-wgjhXO(x2bW4Cmb~ zb*uD~&-aW|)UVt5RuPu{Ly43-lcgUt8a^+M?CKSA`{O`u$R2LXe)`WffRxDWu#O>d z3WWOmE+{)n##?V2BbY}voWi^BZm-W$ z8GhrV^>Z{O**p~C&iTe}$TfP&(PKSfSX*-I%oAtn1N>=uOk2Z)$>P34-9Xzq>cWDU zGW(PsD4w%e3`S8BH*hCIl<)X*++uD&SFrl_~u9VXt#iRF(K?3-Z%FtGfr0HLX% zBg+57m$i|kz|WnEBeVn}ClvT2fe%bVgCg;PYfgBBNE&ME)fCE43*OzXhB*4BikUD$ zJi_Xf{Ou7(^Yj{+!4SB|)9h^n^aY$1{DPtevjhqw*b$BQ0kd=JDNazI9-RwKg~OzI z;IiLrU?_wQCLAFz>(`6=iq^z@$9U-0{06;P)3L)LMoX3EM!z*1OwOm)g`s;X=b`?_ zi8UQX=^A)o6p6TWP<&o}CndcGzXrW9TR{z3_e0Dm1EYRoe4xO}S^&T*U<8Bmdz|y|NF> zuw9GvVo#V^3K^6q6bQv7y@{jnksYl`uFxFqXx%z-Bp{`^eJeIKlvS7F?0Gg!ha7J0 zMmD_X7T7Fa6{yg2+uDw|F=#zH+FLmm5jH-`2}7DIBM25qEyaGPez3Fo)I9s#e864_ z62NEiCgSc-T1oe7$D*2wtGV!q_%|D7EgzlD4zbB+B`iO@CA#eRiQ#8`k_-*c8{dS{ z41SP)7Wu7%NjUEwB0gE7#;;(@p@|$g_)WVvmKW2JbL40~L{edj5`GE0h~>=iSx?3N zIf(fn@JIxR#gvgd9t2HpCn6=gpSp~Uvu8l_?H|1+;46`)A*>3a7^NHgeE@If#QDhg z`2tn3e`y$qIvlzI^F8nZV%_|7!b|tMjcTw4<4{nk_s)I>H*XJN8D&th?RbU72f z&u2NxYFC=3h90-u(**-KS(=0CjMb}x3zfDM4PAbsnz&O_VJ4bpE@twSmTX8+=Bx8qoi4~#FQWN!o~Lx zfY&~*TO3#nsOrwo*PR5{Bs0CVKL`hRgSdhF8aU|G;r>(X4C~W=-N6 z;HZ{pWA=d9_Ubpf1tj)2HjWOju8tR(2Qj?qY3xRy+1X{K5CBd^2M)>g9=~bGHk+FT zwf{`0d|`@^GWpS^!5q#_f~Z1IWxl!Y`u1nvhwuA@K%3wLb#XxmUImqL(<kG6>>(UD!umyG-9dU$W|}Pvxy&}r z!q=Oy^etKglrYizX z!y{#2eT)CEb(+g9A4lOP!sLoS&bB zSUl}3+Hsk7;udVW;pwp;&S5in^qTr11FYr0H$AVWUKxL0GnV@-(zKi<8TaOtg&>`j zWP4lB`Qi5$#@?)3o&Cv#=cBk8%Hh3Dv%!ps#nZDjkQek%c9n}>@lqR@+*f%HOf|jg zoV>?t70Gh>DT_fTIa8bU*2o)&dse*f=Da{y8hZcUi62lzD^zaez!ysws8g{Rdzg)q z%h|jtak(tpi#+yG6MA-YP2y(4e%oTvVPCIqvL&!hiPT8^ERi8k++>&aMfpz$!Q$Fj z37@l+a_Zl9v;4Zk1Rlr*uRy`PK-%_2mv<<|!2R7UbpBbs~JUbmHu%h5 z`XG8K2=A@@wwFLn)DO_`Bky^Ke3DSu(dfwe!Nf5QNH<`TC(=} zy)RY*(gs%LCdIYKateZEbgKE{aX3210`V>*mH7wxpivio7FxMYcfcu7x}KJrH0gOw ze&OYV`_hOnn;FpFnlfRCCMNj_!kI|D$n6|=DwpbOGIp7&a!q!br&)`#Dw}( z?%oS&Yk91Fbf|C=)QJb2eK)YG-1J}Q(`e|VFC&m64X=D$TP|Fn1ZlREZ$4v9{?qmh zaYC2rgP=vYAq+Jj1lCHBKSnArpy$xXhUns=Pz_Q|6HRLyE>zLDt)BvK$eT*=e)&cu z0eWk)T`3%w#H@ufGsM?# z(xoTr{bAezc>%GH{Dow%=dB^oH;q1;EpPuDf4`a7gjD^B1fI6jQ+7(vLZrRFX#|pI zICi9;1fAyMyZapTusQM`_rsKj`%zX`P2U+!D&#JIj5)U(6b-D#vNO`Iw|Y6rmZjrJ zt-lm|r1+AdW_6gxq~P;0YeV3pn&wi%Q=z-zU+d=gf3NTFNg*U_` z>?BXq5A66{mVnpvbeQXc@ov)uIq5L}$_%GD%U42&6L`W~8&O5iglCL*&%-EiZX{KH;iAmV}olsbryMg=W9m^2V&ph$VV z%8xcL(Ya62^VG7$dFMEwj!2oFemb1Xs7B>G5nu#7vw3f^sC@2^oK_-tVvKYYskfa- z(6}@95=5q|4Y(_D_gKn*yf9E8Aali60*6nwAARbbV)h6QEZF&zm9+jFYK- znO<=G+N#R?Agg7l+_9Nj1IH(i8KyTvmCz*|)^b6D#yhoXos59bXUJ=QLq4>7qbtJy z?aRA^+%89kfdiVF!Jff~F76zEB@Wt+5(I3xZ;p_t2PRlk_ad~ZciFhN{Cv0*(hYTk z-QfWcAW#j~4TYiT0FvYkl&PKW=p(EjyStcfjSU$bX0J}({az;nIyY%v_`!2^`IZm- zN{+|@5GBVIjX8}YvNqFEs53nT%j+jAS^l~M!s+mAR|6fy=ia}Y@NRC+q6i|u)KY5K zBLme%1ndZcmY@oZ9NrLy<|#r4>b0DM&y^HA73 z9p!0IhjPcBi5LF#VXxhF4eFta>s#r$GsB8{NS2^?s2Oh~z*!f$q(A4<;NI{b*0lwj z5BT@qT+p9p5QTN>UfRkv@~Vd-?_LFBGcRF}NB?yZ7hN1`R^z9QlBOaVAT;(bK^r!&;5M1&V_;KUWf2PgJ zp=s$PQB4I@(tH(0 zys+*p8#hJp_jpy1A2TZ(sufjpBlcPZs=Lxra+T@ay5)T&>x@7rM~nRZV9(*v*oO!@ zEEZ)xKjsL}7P<%I0{B4VlJ&>MBvcO#kax}s@4ltre_EIEBy*E$E;K>s)0X__$p;jt z{2J$tl*d<-$B$c|$mwnkkE2<1X-sb=y6det9hK^A*T0|)wb#B-5!5+3h$qcH&Y!;~ z?@a3yeY!GN$DsBl+J^N}c8=8K`-z>gyGr7n#k5s?DX!f?pOgMRC7{GiID8!FVqQC4 zd3`5jPocSQ3bN|Yc+{DL4Y#mlK0 ziv*Hiy45S^*29Gk92I(Rzv2SVgJui8Q=ttQxU6Tz__WR3JlDp#_=_U>X3+0HCo?n9 z+YBJVQfFEe^^ql!T1d&JYK^B2KP$lL_WH(ezB}LC52{=3LlpEMEm5+e}=ilLV1H{?Z z^}@gSo>A@H{#Cv@*T>?ZhjHVE-(smqTde-W7gP%TBl7KOXik>*!#p*HFGL3G=lSCB z?BCa~3TTULvfIK&q#4piSv&hCG=~doMPUni@R9#jH{)&R3H~#(K~jxXZ@FL+WW9%{KdiJ2k~Yy z+PFW9`Pl_qYvnBMhErHrrU)dbaINLSC%=3*9x@f;O1!uFkh+LnH&>&yDe^E1WtkXI zckK@!aEGUo+c4kQq(N2Z$ZkUCezjd>r_)_awS-Xso8m8<2mu}hu2+#N20KUdgipA& zV$iwua9JT`7?Ue+ub@CrvM0;b_TyhSgXJptAT1tZaE>)V12PrFhvxZLrH1{)VI2vU zB8g2^VL{I*T`#|LUm8GF(*KZyBX|zme%@@r=!#c;Q^uI}?(rVW&xT6j34!NGx8J%y zHxzlb?5mz=c~%H78a|CDrJtTcOL374z;Q}VbkewAkL=|bnjjg!65hU@{kQl7&yj9U zgYlJ^%Q)jzviN_oeaT(syxt=Ud^C9U9qjZ`EZb}MO_hYg736&s~ao#u{=|4OAtAd5&d z9%1W2#gmEF;b*GjE99)6q==)O^8-~(YevCpcg2wp0bJJ~ZagkC&4bT$jQ6fwPdQKG z&Gl|V6SzDeW|Q2#UGfuIA^e>Q3};sJfn+p2L*gcUtS5&xgLN(3GZSsX<>28b_Zzx; zy(kqmWCIEt{XW_k&K*OX3}v7)FLEFI5DnFVJxy>A4K@L;B;j1VDN8jx4!_{!ggGfi zFP%;h^e!yCy&R@re`}~a#~oe0X^wh&Py6v2bL{(XvSNybDHAg!L!pdjlwA|GGIc0X z%f2Epd4S@+HA;M@hJnR?Yy&ye@m0xmSA5!TwD9X~jf<9Yk#>g;HvAb6?2*1A0jHS| z9Am(JkT$nBFd@H;S3VXxiRjOa?Sp;`KZu3a6p1E4b80CL4)eQCkEVhad^(+AUHKMN z5oX~a^AE9%JyFN96$3R$=yG>elid?b6|eePRy(&B!19CsLMi%;ckP1h=E2|Xd?n*3 zPRK0gGu+0qa~8U0y;4-We?b~VLHqBW{^+!zX`*Mq0bP2Tk zVy3WMo6iT_!f>s43%fM><==F!IM4f+>FZ7#-hgcFOfHA)UfNIkTOS+4&@ zy`A7)Q<3`seFM}M`6A-yd+-}BqS0M;bkTbVVz<5z|cSW0!Yg(kn3kx z(?eQVR)fFRvt18T0tl4oAD-D`AOTW(ta#r%Xr}GK#MFuYeho%K>Q4HMc@2$YB1b%* z{`1FAM{5*I6$g~1=Beu6ms*IItfgQG5g+q`#Y|3v0olu_!aMCrIvx%nXp2=(rJcgW%Hna85hdr_ldgw=9QrMZPO zy4gb`(89TpDKb1fJT&Im{=M*5aw`x09cQb|GP>rSo8z~orM5QA7LpGlUG@(ST&FGk z=ik47q6Ui~*|>dY&i=hT-v{@0ixbb5^tSq31Dmf^J&LV#u{CP%s|2rNqB=269#)Y8b zSZqz;S^nGhQbbWt&G4Zum9MS%sOMBiJ;d}-Yf;iyeISOVfv~%U^rb2{U{q!KjENtO z^TfU!x^CE4^-5#V%-)b(N2Q^RTO^5Rzd!1~=0} z-)`p+kGZgtpOp3d3$7g3LsEG@R{JK0ALIF-I?`?bgH;KOz?{Zsi-fjRcX8w3|C&%# zLpKipeqWHqTkCcLhG_tgGNiK~vNO!n`NG`E-NJ@hsB_S;=T~fPCN=nz#Fbjwx zN=Qhz_kQo^dH#WCe%ja0%+8tFIp@q=*XMeF{O8R`-PihS0o8i21+vi^cpV}Y)Kfjt z!N2Mn+@iwIMEdKp9#BPkSqG0*>ta5+q(8Q?6zvd=(Cg(mT!?UaYx;ZL1hc+w!-Z#b zIGELGCh$~63orN2iVD)yI$i7EiV5iB5PqGvw9w?ek0v7gOZGXLyNlni_Gi`YOd+?X zmdtAsn#gJY4CitRpMM!H8>|5#dYtUZ8YOb#6Nn%a%wqk=r?0|}e?g1uxaJ+Z=ElN) zomT+%t32s$k>D_v`2?_&+7I6jE1zcqq$+vlXf8q7Z*D#vi;f+JL!Yb3%u@=e0D}h( z46)#Vd937>5tK<##83+0!)l`jQy#LA&pUFL1asBD%r@gnbm6aP>hV&=#E9j$hu|rq z)zjat63f=<5b~ebbdP;H^a)!rJyk9<&IvIy4}0H#R41i9w@pYjE{W1gM>SLF$w>U< zh0M>5{(JuK>m?TQuNlpfzVX5bx?F24!@Hrvw=}HYmtuq-|Q=_S_C zkaL+@u(1am>SF4#z9)1*HeqpCfqZb2}YRV&si^_LJ*Jv8$H(g_#TS z9aEp4bqAg9VOr8C`i1*qgR4{yn~7h*$h_V%JC0~Ke|oBGWH~58^|QNf^yl{wQpNPQ zj|a~WzO|r|aR8zGw(u_hpo^SkuCbQWbM?G94`LH+?h7&UR+_vP>N@Z%0R`S)DvQrY z|55^*kRg!m@V)4^WuwL#8DPBdPYn21nol_u@B;F^X!AttZLY%;fp`=+V^b&q5onT% zPivHp2V^(g2VU@7PQ3ZlGsxQetVU%Cq7ITZ06Xg{^N~LlJn_+hB-L9b{ZYFq~1iEY^DyCa)_7sW}~e8p^)s>*G3(liEw zWJ^hsn#kUfr~-@i%$q_muMJ_IO^)+}AK%fR>{OFtSj=H{^QNF>49>h68(*b02oChc z=^~|9HLEbiR4X2Ha6mlJST{Y&MlG)rMY)t*qIgijrp6 z(hE0=u&GaQG45W%A2GP%@zYPKrPk`#+s4|5@K7hHU&eyWg?`J-_|Fd-&=T+M?VTt` zRPNjg8^Vt6iHRw+whRpuMGz=9?N4oMVd7lUm5l3MHbm$jKMpK&v*$RN068F>{nAvs zzL*V3#&L8A48xix3XwvH%*p$KzQaT?(bJ#Y(XDVjzxjn~u!CDY?R-{C(Zq27|K0+Z zG2q8#zu+y`8Xz5YUR}NYYo$E=HU_{VLKY}Z_bZ-2Xd#@z2unVHbDiQ%f~lIZ?3McEFueFt|~h>a-u^UTc2Hsx2FpK zW;C|8X6y7dPwM^rvz=2=@Wp@P_i>=yn@~Z@*GXxb9LyEn!yhIh&Hla-pq=$o&5CpO zrzNtptIT8Lr=_rF4+wm`YRbewPd{wG?k)VlTp{dh$vmbdFghqG$l`du=J_V!qMiLo zs<2Fg-RNgE_QTjq0BF!BD6gp}w?4k1PV5~?BK^unKW;DD_L47Vh7n|E$1~AiiYpm# zV6Ck7`KNVpxQ%>Byg;#mk5Bg{ek(4_)pgsqy``nBjI#inPLvJKnj+!i=6=ctVhFmq z5c#awk6pfeiBPnGK6q-Ki7p>MIIpk&7;7#N8)R~CqdyL7gMgH{nZ_hGrKiWomya;K z(Jz#9cD_8?Oe{~!hdzC@1OTEWh`92MvRUll^!wucnwyJDXYDJ>wnA^3S4vbQA}HHR zoin%r6-#-~fr@G?^R=6YM+>4zVd48ZIVovOzv{@ZU*+xN>i*qL-(R0xUHZInr1#D6 zXx9t~Z0bMcrIrd35hEsGmbmfSASAQmh?JI;;G}odpAIf$21`<~&L;XYBry17-7emP-D?q^Jjo>4EVMa50JYbfx7sl z1gXXf1*@U1E+oKJ9`=n#u(+;;66O(sdQFFTtOuRNhheT&AWI2gdpbmwdW-Eo)b)x9 zp(_JVvKRZ560|x^=%Wr9Pe!G8OlRG3PoKR)3XXHWZG_$2wBA*iKyIe0P=E1EV6()q ztC>RVYvx1Pi|5EMruHBOx9YuG+J}-akhcw0sCReA_Vcbx zxsrIo`uk+tvn?6i#)fc_{4okx8)K}4aQr%6)o%G9qW(B0Df0~vml93|A(^O2%#0GSO*%#XkKj?;L^FxThV%K5uY zOxem+)bTv7Hjd4e7@h2#oWMhzOG+ZoNcjo-aJ)}Xk7zK7KR|FsHK#yb@3F3)p~DKqXaDC*YA~oMvT_b#cl# zB5&QQOE$1JL(Js7$0ysfy-GJVb$hEF0q^i@)Edgm-(>Z$q&;{rKS!+*oykcV|6?K# z%9&I859@5y8qd>Mo3L1-kpf`Se8LJt600{zm}wH3XGI@pFw>(m{_+Ri-rj0!Z@F{f zu|z7JXicP|Oo-*tr}1#1A2}10Fso6@QM!|DEHCasne>X4(a245dk-t=HYuvhxnGF0x*eAqJ7*-N~_oUci9mf3NMmJ@yUl>W_Pb{6Bw zG%sjBm{5KY5w%N=$Xd|!!o8>Rqc74F!dE$91VPp{X1M6qA^S`SUer_pIObCoQ!Dc`wPd;3;=^Pi}85kIDHYvl4J= ziLp!|fx5Ff-r{(UMnK@*L#@6UgERNl8PCtYn-Pz;kN7B3NONsXi-jd5)EG6Aw>mZ* zHlKa+aCPnQsjx7?z2!Rml_dE!&n@e@YwCNH#cm&6b-umP)K8YT8IB+ZtcAkP&RoRP zSLGAcyenVEa@{NIRDGn+ydJ>z_S#-fZo=CvW5$1zGs{)7OP;?yN%PI0(vE2HIJk${ z`__GprcJs4!Ru@L{SX5S@V()>CwcIR zMQtBgCi)i^0Ch`)9+bwp8{JRjjz-;miA7Oy>k0on3S~6k`6cO5-5E#5Bp;N4`lyv; zc`WOt4Bp=yW1k&KCux{dguIyZVH)1iRfHTuFu$j?-_6bf-iOOhP2uE>XwtNJI1{~r z$?wVSop)62JAsidTw=n)2Ih2E{j58~4-^Xc%|F|VR@bU3cRuIpczLc}Hn%rfU>9A! zAH1I*q%ZI*?@>&jb*H8pU#6<^xd=*|Bbt=3c_FPsMwVdM-W$sE`Y7h!n3&i*YtL7VbPaxw~BRoQF|1u@M(aq>2LzWODR zMRXJtkeEvt?C+Td7rSN?Z@l|@ZH8~Pw`aR75_c?b>)7@iDsLHMfjbF>k0M8u*wCnDE#j5^0)ZIt4|7rQpFEl zwm(-@%*qk8$!V53-60AI<(EWaY;;Ik40?g%|THF{eYkHYxKgNo{eJLro99qVym9wy3| zs?|u8>^xjIj0C-4CWcTOQo`&B7Qwc=!%yj|_qGs=m&j z>^8>6j*RF9p}wd6eQ42CbqC>LUW8kAl$IOt0uh_r^131+3EqOcWAva}kjFEQ(^P%1^y z$GribsYXQ4eV=8Ha%9>%R5#7cz;}O7)zlP9Q;pv-%KWSa>qbjh2p`pbekM~buP@~G zwWqG!=SGTUGjM;OR&jiLc@C&P`QcTbgY&9jDR~`>%%=K9KZC zh?bYPkIrmZsm*8v7(fKvEvJ8EH|mL1FEhl(a;tys)+>Q>&*l8E1Dn!$6uG!hmfo2D zGMcq)a6<*$R&^;kg@?B{%~vJJ^%=LHV-#O>4E(;W zCUO`oM)h^YO{VS-62T}rQ5=@<+kQ|GFsCIWQ_6nMf8v7s9i2e~pPgvm zIva9b0K+05WFjzw+e?Onl{NY2QlYq2Awok}sfUucnMt;d6N=UNJO+(C2pVEFpGabU zDYKkd**6B}PhT1^T;7bo7h;lT&%N%_y4q_Q&FRJZfF6ua{Hb1k)u}0Ff;>M(X73n- z>vVj|sA4NV^LGuXiF22U);YJOW<4aJv4A#9tF4|;!(p{9h}X!#Jh)&N1;CM4ROc#& zaG=p9KzwPvk3O@?gzhd&U<-ef!Lj9eaM06;<&}UgbdQ5xoJ{#;?Gz9iAR!st&|yZ@KKT0%B`?4A5G%D+G~*kne95~1ha))p!Pk|2 z=lzWl%^{-miOaBtF{q2ys@{|Cm-}%n;);|`ckQJ6fbRG@F9InaGK_O%xm#o(;s3}2 zCNLguLk8Qaj4fE{!+p1nOCNlQL-WY&+vc%W%XX6S9tI5*a@IBEEv!1cts=x&veb#HsG=2~#>UrJr2sN{kiCgA_{egFwKcEqfpm1Mws;;8r;GR0OvvgZp}nJEo`|0<05&~4^ScRBS%8Mv>cdb_Wn(Qa?DA&Pk- z>q~YBhp4-qTCjvm9C)tk*|@&0{w`BR$jKQo$Z4{2XtlNRTV?55&}p@>gq!zib#CAr z&$t7zMRom>{X@rVkq&yubxsd_GRw~jaAHe*fBXyCO>FvxC!in+&$&Klv#mfbc|4fz+k zDksP2s0-cb1lQm>q$C+ySpj^53XA1jXB)!-nE5im(>Re*M$+x|*d#hN{Osh zer&z9TFM;DZgaoJz07#>lk0=i9^18sX5-~C`x6qu`TUH5NVmRl_~9YPm!Tw^A!}+< zzTU8lkis^wlKaZf?<#lvv??`8hZ#7O;b4Wy>~q7=rQ)P*zNT?v>tFMI{fp9`Dm8W< ztCTj3=r`OpG>KAN1Txq3Z#0RZOO|BQ3kzll5`qLTGGb0so^UYdhJul1@k9@MOHDrN zYhP`i>GN=KB^Fu~n3y4LFcn=xc{y75Va0U9pxHW~tk&fbuI?~tFI+4)(*6g9HQ%dW z4D)n+JTCH5*RRw-o){va{f-7N8+*>9*E0J^;{yD=gqTY+*Bk8V> zp3u&DUxVlI#=_FkD$l_aSFX$`$f@oabZnU%b|YeCj>q!->l@q;T{lNqsy~HqB{G>T zrqz3(SuHBzX&QP)ah5U7IeZw!D<(YqOL8*g)^CQ4wg?aR9w5(0*^PC56i~PtDy>5q z^DlS5{p8)DpAxz4T=v_4{*sF^+&vf}6bCw&*IyPJuzIhZai48wu*C~?JnO_mIO_Q( z6J;kNpDOfH*!MX?q=2)jw;g=7+80S^5L;lw1YAhX>*iCd=!|dTb9My0b|*t%-Nz8m zdu+bEXw-2dWDz2u6w6m%@X>RQd)h6ft9#BR#=kNVIi6(jBhaNzJtaZ zML>Zcm1#v=*{pztod5gHN`W?8MTY$p4*Ic2@r9hhK{x_JO@40!Ek7QnMuOWHE-r!( zzO-<;Leri~szQw8QQt)oq0NLa-Cek@5gvl~ z9aE8DHxWQsC;)Km!jnGLXqu|$sY3IzyF?r05rXD1;P;=0pm`X2*rn~A2r&7%NOz** z4BFplN(2jtM*W~gqyC_2m%+e;UAB8bSS2NFFPI!gQ;X^k9&;HRgzK-lq{}(i!0bOl zU^LT?Zv8ITa7w`67Aa`%gnZ|Aw~qke5)D9>t20uqvtq_Y*2XMH{nx9|veVo@L5*e2 z%tJ5UCsn=8mo5n)gSmfBDmRY>r*h~yioT#;S{&M@xp0^ZkzW12Wf-`m_sP+rf|8zb zd`XDy^zG#li_;SVk3`u3I~6Y<&ixP)427_sj4Yj}K> zrw~udge2AuhfnS@s$YLlN0On6 zJ6#zB%3=Ury7xTi_yF!LUa~okYJFjWUz2ftMMy|3Y2TEIDBS5`PrW`~e1(T7kgJk? zcCC{pO|wnmdDCh#LMSy6bn+9g?d&Uvj<`G$P?H0&;rA2qe8_`(lIG?Kab&kl0d zr!w;ubk9skvWzy^2DY6pzIb0Wym*(F`6DHHdUzAT6R=j={pvO;v!lg*d-VsrD=*LZ zt#7(==f4x<@2NmQocqQ?MwE~9b2KmYSF8`KM`v*+$MLbUrqGa(*3qavsG7bd^kse( z?Y_Yy6Zh5JPv(k~aT_KQhxX3}k*?CBsasiBWBf{>33lbJT*y= zWSovlSTsq!yUq(C#}9)o;>W{HI`6@vqN>?ozr~33Ln@pL!B)+>A7V&*?E|+*4)8aa zDz>z=FXi@a%Oyry+TuRlhlLmbSYk6cODyIUAfdm7O;u&;=UeSuMQ{05Q%@96yS*)v5QORFC z`~B%{5Hp->3D^O@cpj$5o+Qm+ljpm3pJW)N!AiivP9!)GQ+FjL%Kj$vmf>ATQGjjvrv3;& zg20@~$6XN5NRM62u&TYol_RE{gous zG3dkR949!6%}<_OE%7PI7CyQBS=#sLYDq+5aNoU3keM$2rO~J$-}*3Q&dFsD$#X?N zWVBLT%=#p&Sz#{X{v5a_f;&NoGvqACR+Zb&Z?2wFx1vnZWoAC&YNO-e1B9M`^$lfP z?o=b@8+Fmy2ff9-maHF#&>#2K>%!C47#+z^SrlsFh6UIrhFie45m;!%qO=a@js5M+ zhoF}bNN^r)asttX{BdB?wxbh9H<|+^;QfF#z~#>vT#mKpiDJCSw5LZ{;j`X=EFLgnJaPjW;nk?C#HU z^U5<>S%>LkgGZ|vAX?V!-&%Klr*-#dxg}$BwGN-S@9m_TA5TI1*oXkI&bB(gnST`V zBd8AVK9hr+PEJJxw*6#(TqGZSXDmO?V*8%E(;jfJu0$5Cl5upp%ae`_`xJ6CQh1o^ zzyFOHpp3%fhaS5|p_a;_$JHtjds0AP>lbvEdOA}c>vqJQj2h0GUZ5&jmzPk)hy_{p z;J}m(9s|S<(8O9bh_3`Nsi2NAB0y|PE8+cJEYdY+1hN*?Fdt*VU$CE|!J+5$uq=v! zJ#%GDC%SwrZ7Xw=|GrY*=>9Wb=$I^`fD}zz6Aq@Gun@h=LOU|9Vi~h3w9O*cT9(kU zQL;4%6eDj2siif8*vFY+GAE|N2yOI2$HQqe7i7V=#{esa4{;l11~D6{cS2r58US0G zI@BMA8d%tX3&eWLCDCobMGRG^1koq01%ElC1jH*g7>jd9il-Wy!}n)i!wWlJw65~s zs@Z)Lq!C9*NHnI3IrL4OFy9l;oQW|Zrev+%HGnfpF3f$kXHs_C=e8Wi{Tp7P=y@L9 zR=k1s?PIXJ9Co`O!Y9_+r4Qf=4@s>q@NF+PvOM1ch*I!GbWtntvzn3U) zIQrQRiz2==GO=*`m-n&Kvnu<}w^>&^uZ92f@hdnGJd3b&=E%_t5=eItyT0F77JJ-O>etr zPNdV%p}V>ac3MmnR67&Cr#R$!AD^hqoeA+A6^-&G0)qI9b!Xd(>RL(y2w)=b+U=)M z)3l2<#E3vsc-@mt7!fRu7+UjidLgaac>KCrIj@BbkTLG3nWn851cKsb_3>fbNoeo} zI?GB4({}}p3OgCkF+zAz2(MUTjo>K|%auMp2pawp*bOt$gI?F^U%Rtc&@n=2dIr|E zJgY>vi5cL4s6y~t)7;|0TJ{M7eG=X~8CAADt@06I3GX1atxV-pcmCK7-B#Do`ANIQ zy9e=y1)#v^hsNx_&bbPP&oAP~($UGz4;HPc)pf=QR(tk?W1p$&xIdu46(j-eqkiOh z>I+LGRH+KsFR?0Gw#KaRIbq#BsR%@3Emc}`=HAei>P)YYzWxsNe{E5bZ#uw997$NT zY_8G2F{IqyA))shm$I5JLNJ7+3J*Afeb7XOk$DY8|?~$}0GBx0uwc zr7i{h{4qedH#iSH_Op`)=A2)uvtd0p_NajNg`$a9_hAb1X?784%G|$Z98GKh{7z?> zK`SMGD|4FmruH7Jo)OkLiY_Ot@=!rIXya5aRcWSaqYSp=YrP?E6M zzz$R&En!n`P~>xP3vfePi;}5Dh3;CyuB%bN$qQFxpeCH5MsVztAeL)HTZB*W7u*1_ zhop>4d1gRhQ-4-m_)iVxRo-W!W4jvmLRb*S{<98yXxhL2P^8*P^`y%&OU*a)Z24%g z?xPsMPcY)cY;OLqz?c_hSz3hm%Ii$`GE=ZKVDWkMo``13zrFxP?xI{GDFGxf%$)X! zOD93GMwBwyMY$`6Erg2RBmpecnt-!LljLNyc+vr+Py2@*Wj^VqgN=6 z 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 V5!46EIxwD(~x=VMwGdQS6UzscR4 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 L(N?Zev<~?nCTddr diff --git a/fastlane/metadata/android/en-US/images/sevenInchScreenshots/5.png b/fastlane/metadata/android/en-US/images/sevenInchScreenshots/5.png index 5d726ab36a709506c9cfdd0c455b60b618db1d8d..5574ee7dced450f08bcfe25f2fb7e850a5b98838 100644 GIT binary patch literal 19742 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 diff --git a/fastlane/metadata/android/en-US/images/sevenInchScreenshots/6.png b/fastlane/metadata/android/en-US/images/sevenInchScreenshots/6.png index 2c3fad113d4e0468a997843085dba51f45b44f35..8e0a83a6a619ab6b53052499d61347eb3cc9aac6 100644 GIT binary patch literal 462394 zcmZ5{18`;A*7b?)q&qe{wr$()bZpzU?T&4;la4#KZQD72?z`{4_r8DCuCrIwu2XBy zxyHnpYwvIcIdKG7Y*+vQfFLO$q67dye@9w=9Fd;Cg|6T3>JvvY>ZJ z-jn%0Kean z_d7U5ixAQ~+KPJm&-VoaPyul#(Jxs6-PsMxftv(d^8{ISZA6nM;C`gc*vK=A#s|ap z!)rS*lU8PY@ENU7~roj`Ba{gLQ zdpezONxtY9waxAMmerlo*dwgbI+>2O%hC5Sv5u?j!VcAzD4Z%BN`})s`d|Is(Lu;b zM?6h_fcH{jdkX5}3fv|Lc~_ZZro9{TYu#0V3Hi zk{iQSkMJU#NrBPonI%TUP!RS2@C6In9S)`rekV%imL?3t(E|x13meh-lZN;6%qwI-NbQgF-J?p9I-nNL>7C>ZH{uoc`SaWj^Xqsoa^wE^ z$6)*b4}cIuy4_7SR9k@>z4h<4pG{-9#zT5HE_zHufj-HJU-A&kd%<(?BgzCFjuJpU zcuOZFVQ0GKOb~`#TS>GV+RZa%cQimd5dJX!LQ2dPNZk961jpzd_xEFm>@|8mZ&!T( z0Hl3#l7OGj`)z!~{fXT|*j}d(%+x)Ww56s42<(|@vZ?y*`6IykXtFcJVTqFT3rDx- zW_K)q8Z;5icC)Qsmh9l}v>i(jd|$xr3%KjA?1>5T)YPxH&oYHER(N<6c;`;;_T3X} zOv`H*7aI;bN_iEy0(}inDJ3!6^3S*|+-`3_HcDM)*F&hr(lSBXg-g#GyAWLo%r;nDU0iw^(FGRn zS);x^esO!LaZ_Q3Oze6OOnTgKvXC!P#wE>gu?|_)P_qytS{qKE+|?fW9#QSvKW@@c zW<7K~ZlC3D^|ScwXQJmer1(yI;`P0J;0aU$4%dN zhyXCJ0m(NQ1d$t#7)Nh}nia>@W+TMcot?Ln6WH?BRh3l7d@%i2+l&Jco>b|5hqn6S zr0)`SDjWQi873etA){X>hV-?-0rhtRzSL^|Fh-_78aUrbhkeKv{FV@nZ{ z&`^aUUFIQA)~R3o zj__cQH;X}fDOz2@x40m^3`pL#lJ4{cU0zFweoSrg5>)qX?!|1Kr8pg0(paZ8#TB-0 z<_na?a?RDFr5;O6Mb{F%NUYDf^li%-y-F5oMsI_w`l@=u^kux{RL}fQoOb@(&;SGp zmuJ&dAXGR`z8%4xr^I*WOj>lcr*CQ?7-$cqPEk{8>Xy!4U9T}NE9Vc(KLY3#GT60g zLHbr!E({n1q`8bT$TQBxTW$39^qN;o7S+{1fRV}6$zlaCMVe-#3=~e>cuN?0G1Olo ztNKamU>gPNvz5x`tnjKfa0-9jCX5)VXhb`2fvau={`cmmoPgzb8=6p@m4` zmtN+3=~ER-u);y$ijUOC@+VbQ?@oQwSh%nH?)g*a<+yg!Uu@}GpoRJb0seI_h>q-w z?2dtjcA((b+-n4)enF(z(Y@9%o*bhs9o z#MqmU1S`I4D7v(LWInHCQXfy}>d9{(xo=p)dhhS1HLFC-QRZLFyj!7brJ`eaE`PB3 zS@)&}q;iMz%pOP4cX-Bm2TwlLZVo&ohoB_mLq#}T!t8G~Z<^r=MifOLqh2n!@xXT} z4htepz9=B|`^5g?ZjJm-+T4Ny$ITtRKvbwyk+=<4{_^Zq%}qbciI~?jSlGFWr&=Dl zYFKQF)~>6hqnp$Jhe5sax$kVcgb*n$_9R*u`fj(-Lc9_(CsiW2%zF1%{FIS1?jpai z4}+c&Pe% zch}H?^nz(uFkjA|A)mw7AN112SB*er#P9tQ7TCWl9wC^}9q*OV#pr5KCn5VQ49Aen zu;@z1|F#iPKOLmkc{kN97>-RjQjCkK7`n#SP@RugDB zaS#SGQwR$wzDJP~2{p0OT~NC$G!F;QSJbgpn6zwPF}%pyx2*e}Nxe@)AN?k?*9Do5 zeC0d=q0y}n+<}+p#lLSCjed|TrSSK?Aj-dd)UL)nwVO=tnQcIvl;w}$E+_T)L%5V+ z-;e66Uo>tlzf2OXU-dLr+{ zx193z>~uNfBZ;D5dUdfTAm^#@I+WkOVcP3$?>I7bQ=ka(7fAOG>4(SzK;I~4Op)Gz z3BQ73{_-)t6iOTWWZ{rrzdGy2iZ3)HC&7RKpFol%FfsC1BVw0g$GXkpd*vj4G%lBS zsiQq4i6uUMg8rKuPrdbOE)(bO1-gqJ&DZsx(XLFjQ0?arbItMV&~)1;UE6Dg0wp-% zbLliTCp-3vFiKutqL2U;Prt~n_AT0TIoos*@wI|4 zw$5Yja40A&uDlL`7QHIgcpUM3Gco4TH1t1$R$p9(zl*s*>&8tJCyqm2ip~}?H>_F& zZpAyVQlL0s8;wgVTo_3fLei8Hgecr_S(4W{g>(f0=MD=(BOVFF){W?p)4wU z7co>^oPkt;07q3VbubW~WN%8Qmk7rMAhkmexhexLSacl!#&8rv+@$U9|h#nW9om zYDFPOH}hD-ut`I*Rw8 zWiN~C6$WY}$dsj|>m2C(^nk3ei}@(_0lNg~5?Ec@32O;EXX<#>8l-=1_HTcbfKkY& z6ilij-;kV}!EFve$Z{*vQZ~=?&sB(ZY4y!-s(riDv8VQ9Lgr*XxBvz2>COFZoAlLe zkEODw>Ot2=`QhVlVfd4qte~Jf8xromv|k6(jC7Xu=y&$F$*|As(mB$eUV;7);8uDp zB1l$|BHK1a41X2_x%ODReV?w7(=c^(^rn>JBPx}ud|uZdqs5e~sEP*79?o6)RX{CN zogmKJc1F^RpLh1h9E!gEymnkBpuuSVjWy#jd3Y<}xvHFU5t^0oI6}rXMYIg#b!cUv z-Rzr?R8&HdYiH-=WQFM{-4L@Y+M)I1BX5E?z71ltrvxs071uE}82+f8C=lFSER?tQ zWWt`JnS=FE_@;g`-cPB>f1$mYh0hpF6?7$xhE7D1Bri$C*iSWu1d8Y7ujn*C;)s&j z#{6iF`9GB(13Vf5AD48LOtWgr*RXaU8 zIzllgoSrWjk{4mQ+L_EORuD4hnZk)C4Xa^lvZjs$8MsbZ75#@NssQtkzmw zRL5*{XC^JA$dD^ZmP%&ox1~~E?e6RhGI@ey2e=J5SBI(2_Kb(rwQ$CyQSaMs{Z7iC)Yu*#;D`>R5a#TSG0QsNOrvjiTAIk{>3DFPCmQeenfe&^o4}xi2h>U{FsHiBD+Qd00(D!KK}=XUmaVT|^6z zD~$TC1m=L+%DmT}c3xI;1zgON?@8Pt2Cy-^1}}w;JpAHP&x(Axwk(<7$Nc+{^<;lB{7FG(K9z_Vo(z_# zy-{1s=v@iphU3Dc&w92Ynu1A&j?w4RkS5PUzOJ6vPhbyb%6Y^#Ju@64yhyqXZjD@P zpWKOz9n5Ia%0iB#kO$MRsf)Wx$qG9v=SZq2Zmg zX-!r#V&GRGJFBZpUR>esM;`EDJoEMzPTupNQpFLZbj*-gz^L8A)fFj2Ll*H&*{k}` z!**_C7u&D9_hlIq_ZY0klDEB>z?!5x;M25`*H5Qf>(%*hcR+-h_>a`L!>6>yGDFwvV2z(y=-`13FItGi<5sjOdc} zYJh#fcU7t6jH#gb5fQ4U_e7Oq4LG+=$alJWOvCZ~KDrcYmAx+&LFh6JjdAV*{()Z_ zG?T*adsn_o2uVaiRji+83B^@GL*@r6rMmr_$ya0Lp8?7kj*j=ogJ5lb0l zhJQ^E(hR4gJVkFus4D!w1?Q3i$cA3YE=Nqw6Dv<46dqJWI+sKjA69ITnnPP<>Yx!_ zieCi7-evCC0lNFdVFPD6LO^*#S67$mq<-EgV>up+HZ9jOA8&91@_n=+OukZYE}wUn zT*<)qk@`w`0)E0fK`X4AF*y*CAlTc5$RepS6__%q$SGKLOX~eRjh}30b@d=zDj}@D z=QBx|e%;F`O??=-Vc!yp7B>AFY3pHXkCNWq2IQv0r?i6v?IA{>*0B|9_7?muVS&eHPJ!<4Pk-iFmU&?jKiO=isZ>qU$;ll$MLy;1 zrAXHnLh`m)9qthkLtMf~M{IT78H0r|z!W=g&G`8!lhBHv>4>E~03xtMqir<@0`j5= z1-@oG9$q@HtXCw~Maa0mvAEp6tzA>M+z7AjeU42(upa1nhmMZux(8W9`m;D|;j%Xa zh1Let{+mdppI@jxs5-l=?jMfSk~4qUyO4z<3ZiBTLO*+UV(o^yM2fcUisjKK*cfC* zCTzq&V;{I+p@Mmj>zm9=pC^>RSn)_6qV?1zZI9AvrRfHXq;dNiH3U=~34ivlU(pfH z?Mjy?u9(Ce!3_-~KbY8L8>Y=>(&b8`<>#2MU~zf+Y{W+iuX`1M9ZZ>H)z1a_Zsj3v zrDtasO2kiu9Et33)WO)Zbc!q*q03HO*l#88H7ch|?$nmgInm;j@Sd@SZ|s@9n#Xa7 zcKW&lWVl25qKE?;dkz}qpj{4TXvyW!5@bS9Zb?N_P18J;^O?~LicK56yF;)Px(I#t zZ!+)xn^W1@dB1zO@d>_25{sx$KF_C7R#)WZ zE1VIL0ZeEwG%exh(bA*^kBb8s8kU_{&f2PlSAeBfJco(X^B4(7QS-(o7@Bo7V-e-W z2u|aohKjv8x0#|4yMCWsTk4r|+8E(>=okm%EqFZBJs#eXACxdI?>J8b5!UJPI+2@b z5Hxpxl-P?X>9#zmA^Hm}Q{wMRq(A?MdVJPQS6-nFz>i=4VE|u`m$!l468UM3qY%`# zz}q4otgjpCQK2>PV~4j%jTCcFH#Z3^{ua@j$HyaHd^PhY5~e$u2zL4D?19OGhxp%d zdILT;?T2F<3p^kyslpO3ah2N(h(}TQu72;JPVa`ff0Pi}5RPo4@M^o7ve-Lkrq-@l ztRG1>EFw1klNeC1S-(g&Fz_5yztda!6}C1{=H zQntLnuQv0pwy7{lv3q4C^jaK(K|G4$`=a@px_v73LC9h%%Pt?5KVezCgs6SOoc;P8 z=|n{rnU%a4;l#GE)qG|$vH5|~96ioZ7o11KleHASP?O=0k;#1rr>HXyTfMHA+s949 zPegF(GmNiiud`BgIEWVzGE%{Kt|Kmxl)m?LcJ}_d*vNqv(5D^4jurn!&}360JLKbS z30~V?W$Bg!`U0G%(^7sOQ5( zMe~f;)X+_m8x7fit0P!s^R+T4*H zjNmtSeeRNpJ$H(L0+@LH!60CCBUC&JuD7f=6OxI7C2NxHg0qN^hqud|*2ECgi|}1(ki8|4{-v z=W2YJmo~O<57>8Tabyx;UAM{9*+PmSi+)8t)hwGU?XMVv<5<}hyEE5bOv>!msIHW0 z5Va+bX6B@{M(kP*^!aLZ>b$Cf%mjHhD3ey`73FD3wR+j3?y+8CCV!#}WmGKK`_Q33BS zyttV>dQ*o$lWRjWnYxS&GH1ysO_lr-_SuQNov-lKd%s08W-)I@%r}9z0m`s>x_QY+ z^(@v*eNlfCWcAD+vTf2*M0&jYE&ZWfL$3|}ZT=e{zxNvlHiEo7-*#&|$8N}P22ws= z3fmTqZ{c(P?tze7NMa&(CAo;HVWl8Z{J*EEn5!i~Mxph~er*7Tyv~A`ebbNfVfiV0 zf955N(iLcd^>DGY`@A& zcBALyb1XLKiv!?ibg+(r>D>_Eg(DiQdOG0og#Zd|4cfsFFQrl zs937!FZ6V`6VJbmTHS{QwYGOBu*cJ=eA`-JWI>9>a7IxeT(mz>87IjU^lfufW}YL5 zHRTh|{SK2RY)4{_slq9O5FxayX28~Tyj-Qjn!<5oq|42M zyy{d#oE6{@cGKHui>Kk|*3tB}eZh2VGfgS7<1=p3y^@ru?oQXc_uh7`H4blPak}!i zcd30Q5s^S`;SwZg0t_rYn2YaVV|`tD@wRTzxe9q?S3|zqZ6zGO$?V}#G^@pL)Wo!V z+t&rGp2(I{m(In_(NT2B`FjhkoyT+dckLWz`1q3MN6{=R$QQzJOSni-Zt)h;@vbg`!HfQ>b3pF_r0K!*?%RHhAv6x@Z*S$O|#tOXdB&*O9{YqESM<*mc`f1bZ2L4aRF8t+rb5~}dR<~QWZhguR zp1%sn8(jSGB|`yNA*!C!HD~$eA84~b=cL_zjWX1+ma~K5zAfGtr`dHqY7dVUw>XLf z1D(lAs=zFCv0dKdZQ2LJZBC;42sv`qN{yIh(hEyCEe~iYn(H8BdLC-2+X}ohvP{S| zdXdmIlH1%?x{O^=1E*`%{{>vL+Y0!Br|zLWyWg7j)Y_@gA~S$emp*xzl3G6OjLSZy z9#BD2L}W~!=>I^F>*{k}5BfHl3vLvllS8uTGV0tlO>-_46$Fud!8{Lh*&?suv7{-`HN$ z5CLcp7YYFT`x(HG{`5e+6d;jbgYCzg)(WlwoaHY1;62F2s8o|I+a^^I!-{ZI`bKM( zkQlUUSm8t-?FF}kBsb=Z5S`{A%_b=;CP`*+@l%yK&IIuz?C?cN8tUf(5~TyLvCbGb zB2DWCE!Hbr+jD|#>g&b7m;Ah)0sNebb;TW70203u{oG8SOT1GM7t80F=p?V}l5dK0 zJ;sTty!UNC6pLs*RQd^DCsm!@j@sD(ul0^;swhRN>y|ZR+wMB8qTDbO=XG8@kk>}6x z_&xWWNJp$S@HbUM?^UjVCyY+F5%vYaoShx%#`+)>_Gh_WRHv?50?Eky{#ny6$8)=g ziGQ_-L^f`YrSo()wey@f6>GVY4NLfSBsGasUQTKQKHrE!i?k_M1e*`1h?8x$lw6sL zR%>2|kH<>2r~nd87M9<;3evz`?P>MUYI*@qzUs=uolxRvhGw}=9b|S>aXI?)FKf)`n1^)Da8C+G(*@%XR zAmyT}X+lYy>wmdC=qUO!S#e+1d{6Yez}I+E=|2gK^DD_r7bASxrYc4uF* z+D=DnZh4D!P<#$4mRQblpz|n?)NN?r)V4YK0NdKx&Hcv!0Kmn!e}wTv6?moD{;3K9 zxCu+DyY){9p{QPu${@#NGoLg@Y6@e{tfqb6jp$>0Ci8pgLAOwE6BWhVQ9n0WT@!!GMDm{|r^%p97`RKlIW+XryG&rf$!>n z+qd^)?fHr%R)uaubwEfP(0lHEb+F0rb^q9X`oUD9yqC-K47T|`r3-v@6+GEt7q_8H zHkVYsSdJ^k1(igDZ)|K)4l4?G-IKs#Chg`okTm@W{1zL_Cy%FR%zCLnCRgWPekPy& zx$lxo!}^Q}h#)z>F79{9IgX6)@QNL7Zi^QT>^JL*0Dz?LD{%U@hvt^s*nYI=Z}S?M zUQh!3qDJ{6d){;3O#vlQf4y+?IZQr41kR6hv@Cl!%%qhSOVsRiN=}}C1T}OP4J_%i z{$U_XIis$k&{usJ;afFvFN^FdX>R0e?2eXzhC2m5(nC$_zh2bpnIGc+xlz;CMygpa zdD-!N(b2v%ZSn*Fc6#`;`E%~6Yk&xAX$Patt$frhsgrCi0y5FeR2wrBn-*QPbN8)p z+13Y$0D!?W@MdoFIh%>=hWu~wKHXPRU2ANA0T3f7W!Z5(g0@KwGYpqsH1sO-o%IJB zc+l#%TxBl1LR#?yX*|=Bs$C7V3--m|s|=}8#&2!e-hPKr!1j+tBOSgWLr2aVRC-(I z?$BW;Ef9SU`+YNhV(5y$p0LWQ8<8~ z4m#aT{u(lBRfyA4sGDKT%F8nVuG~>HenFOl`l) z>KG^N_fIHF1O1=8%`Jsr_H!F@#;yp;+V)#S3n-wq?o5>1u}^)qo6AmFQa)d9c`7B9 z^~BnJ5;EoX(hF&oJV}}}Q7dXuE4WGAl>P#%(`6*4*?5NzgVt%f{?zR4zUnv>8L-t9 zLehS2%M2AqtvzpxMECB?D(Us^I(ORh%k;m70l@{$l8yh;#u6a9*?aeyP|YUC(4;D4 z+;|E)LMUb3M6RyoF&BCT)fD?2D|{m78S$$Ic2k6_Ws#J>dSR;ATOYtxJ&CSNe(eAf zLQ(cu=JCq6<1oi|=$?50u1sNU)0d*t{H#E@*s>wxwLAT~&wA7@?Hj!JTZ(gT;QzBd zpV{f3`Dbj-6GNVmfjsa_nxO;%Z)Ep{$)m}rRfsJvXCgJH2P133)Z34#i`c>NKWkX+ zci!fB$a1=lak4Oq5GDJ97yP zk(}p4(oIE>D%F1N1U43FR5k+cd6@ej2>=FAy#*MiD-&%l8n8{sy5_IHzS|Lgg2it^ zC27gk&jqz`lv2rIP$@lBOQeFFKHftXi|T6hbkw5LdRx+87Reh*gv#`)TG@@9)U_&q zQ_=i_?0m+{P}*3`MQhY09VQg;SdtypbUU%)++v#vcHFoRY;gUmVC|_3=(WYX?OVfS za%BfQxcF-XzOUnDf1U%zBDm&ubR=BTUG}Q4G)YUYRGz<|=VXbiOX{JLx?gU<+lF*z zk7aIy1I~flOmFs`AD3`hG@;-%*U1Q8_Z|;Ne6AB`Wy<7%^GC}KuWM}$C341@R=nEl zMjLC(v)zsJ&Zi5@jrQ3!kG^?Df`Km$rsHq-skxAMmr!aodh6|(>{XBV_n<=1L_x@Z z|AeX5gQPp1P5JD;I3G^r#K0r*`d<7gj>bvxpc~2M&xOBStLuNVHxiG=MNIiAf2o(r zR)vHJd!*^&T^URw?~z)^%)G&bmeRDmW^8zeV+wqsEm za(jQB{CqKC|KdXJ&j6Wx4iDYLgiSR<2qIR!<13q$Uj~!06v0{#5}5d1CQag`deAHS zdb_K=t0S#WtKDLC_OiS+Bszh`N%J(ngo(R}2h&j%|vp>2#eoAvg9jED2pW{Y`LV1@2#bDUm93hmW3HfyC0T!dmwj$p-2Qlrz@(0vXNiLHT$Qh>wb)&<_#_{!=$QL z>p=WMjV&@7lslR(nC$k3>b2N%&ClC1IB)d@^8MRs{&Ju1$WkD49j-MgwA$Zy?@t$% zi{)%*8lQhlCW^;kuQzH&s#Yv+L1<tc0tHlG#VXNM}7qpko5AoXFyYL$y2hcMuX1vfb>Cj@}`Q2+WAU_*N@+@Tb}BLGWAFP8`3lkClj+*n3Zb z%h~T`z)rEZ8Dv_CdX0XI&8!|bA^|6?TG%c+oREyDqejrN1fC%&-;Z^%cr?(x*c#I_ z+(}~II7S+2NXUY4a-TF)$VE-zl~tTpP>JI0QjcQ}w)h82SwY1~oO7Yp6^Za%4*Oi4`8I))AJZjUcyRp&1aOT@OsBJ?!@IW6;7N6!{_eiQnMQ z!k>JkOQkN()_MQUXo!QlM();7?*+jRf4#%FxoIGIXzYE}^$ft;aQFuyHmq0d{$yT* z(T}&o_2c1`p3rQqbUWNg-1Tw|F~Ar20oU_;6og~g`#eMS>tt>uG@M~gJwxkuYm~gi zlJ6EF+c(wnr-AXi+iNcOH?s?!wjfE2f!gTlR(lc#D66EGb8~Y)=)-;eGZZDf zWH#H50c|pw;jjPV_6~PILdWMX`yIgQ2H3jU?hkFP)GXjCv{)}E>oBr^x8|7 zD>K4Wtt|j&E|xcrEmbMM?AG?U8~&AcbTyVbQKQ*_dds9KX}C9A5@QsF#fwU@X@L5T zMhfK%YNPL^jZ7Z+Ke7B1mKtBQ7~-B))#hl z@j}DGHdH;@NTo z5)RjI-(?2?CZ83wUWf1P!T3hkD-`0Hez^)2a%~&>*PG zK77qq1N`o+b%tI}uC!RV$#IYS_1>BNQDc3zBY%te@@>Wt?@Sj3_xH!iscn*20)CMH zwHPc8Rh!Y36tCXuz~H*)qthPaCY#d}G(6k4=T`y-_E&mB4u?C;4dz}b0fdEtfzvcM zd$03$=ZeKj@7n`fF9?o)e?*cHgs}`(>I0x=odZTpn7ot>s9(s1P2Qrf-bQ4qTYZwC zz5VslAT%NjNI`rK2)NR~;6Q!FpRsrEcdssP*Ar+DL!ZK8S?tS=t|79M3s~;n1t}KCIEX=S z{SHa8PV!C#q;7AK1=UeN24-D?lgs`fd|$)>Ln@rKi5lFdwQUDzCPbKZmkeS@KCMEr zD~U@iS7dn{y+AW+s8!hpN?Eo|i_dx0jxt&sr)Lj)y+r;~a; zCfsIb4@Ukt*Ar8cn_wAL(OZ06Wu|vbv%=Xw!&k_It=-@yJ8?=}NS)!zHqF0gWwR0t zh`^w(9sE>X0KD@=w?Ql#Yqq2=e{@CN$lA&(g;t->_ZM-k!e<~ce)hfM;$x$Og8Yo) zkcRkPDMW`2&DNX#V~j>(b&O|~n|}tD?9p1R*ME;*HQO$J*&QP2mY0&^@;>-O;QR5u zGXQta|9!D)GwivbcAV)g3X=~W=l*nlpaV2-e>lDXQ#SZ?i;95X!yVY;h}xCd?d)Q5 zUV9a$+u;(Z`D>!h;~pEI$eiSTEQvOH?CJ{o&)Fy9Wlc>FUCvkVd0lXCX|~6i0ni8t zU?_H3oAdd`MOw0RaRA+?WJMz6;=)c%`xzR2?P~9G;O_I8#sSD^Xpv(P$=|l> zwHN7{o12%MKVv*Niroi7p;nj2d18tDG7W-&i5?@S=~8PO+Z6P9<#)sF?L2ZSDy&qO zhX(=Z=TRtSEvbDrDta}m_2i0kvvz-XavgcBA&J|1Q0w}~e4@fL)M$h$Rolt989zu_ zVB8`2eBnC^@d>qsZSd375{tcP`0TKL&*ELOIT_+t79J6v{m5Jq8nDM?NJc#=m^jub zcXYcIA_s~32l|1Tewp-vPR_m(3Kxoijjj9OO8OUmoVcyg`HITzqa$bs12PaA)k{5I zX5-P{Gf#KN>u_Myl(iPs&L?w_W5FcLrV|;vCmXG{Js^DDKBf~{#@gA`>U5P074fTw z17oRH9^h#B_#Zd()K)+0n;=dd-<~8>+PWPN<=lF=x`_WQISNn`c%RIf=Qj(&=%|%x z1e?Fw#hRY;6(Mnwbats(za<#~8zADUH21 z*KRh8Y4iTQ#s+oTV7OaMh96nGDKe%sCvsuh@?!jVQ}jg7$a^3hh1;kA;in`;^sz=@8r#D z+VL9i>>0TlRlC)4eUVgZ-{3RcS0W4(Rn!QH4(>0s5z*4AbYMh5X(7RKlVnMTg6Yf> z--}T-CC9Oow(rCb1XThE?YWh6SPiLEJ1M#FUe+vbJ$Y_q=YCEF=$kN+7^Yj zFdv5*O-gx3+-ola#?hC>~VL{x{!Fq>lbOuX>PLr5c zQiTF18yg~^e6f^{X@D~gi9&qHw&(gc780|GOz@owrz#DRl7j;irv8W5b1zS@xX4z$ z)A39&97Z4N6d#{Y+&2zV@Kl?N>g9Ule!=&&af1J{K4>D+0%ib8V{Wv3%t=Z@m$?sI4{oy1@AQga7}0LF>2H8R0|)7CvRuUH z@yct0i<=PtQRqfx!1S|7Izj%{t;WO~`^!9V>$0DEU2FQ=)?tW17;0gxPF)y^kCJ|+ z3g!K;10_3+QAVrANmZY$B=UG|qL`19&~`qJKrio-l7(vf<7{NY`!MEKp_;eQ!h@F| zgoUv#Z2N~9dl;BXuWB9|{b8=yGjix}x^q7Y`kmkBb{U;)bN1wu@ppQ9PTx?dh`PJ^ z2kttDE5YqcDSQzuFIHj#K$QTp-}y@h?@Y<#(3xw*X^?(snv8bqc-DiuT#80EHVLEoBTi*Ts zV)wW-^o0Ok$}6Mq7Ad!9)n}XSe}YovASVY`pv=ia+3vwfM3K!Uf2;4#?~%WF7rhLK z)9FD~#bUKTG|z`JJe1TF>p-zqQ~BjacVt8iBy%4aKIZvq3$1qX-Cuk^m&1G~qUn>^ zYBhZ$A~KxR=V2esVG>7O(Ars+%4u^CTsuh`xqUewx~iD$eln;yftVcN@%VTF!}Nbo zS3Ij^NDa&|v9m0!Vs^2W=4Z1bquoN@DeThV%-Z+;Rx)R^zjfLxso{<_`F$eoy+3?a z?KpQ5ghE_D@`x^QGa}V?m2;P1IAKHOC1M+;Vrx`-lYS?6Ky!!P?FCQyng0>GW{K(- zwNKWqKA!^znY`3Soa@f9QbN#sKlKA%+Tq?yWOC{P7-}r|@w*A!O&cb1FWpO|)5jYG zVF(-y3w~qvzOMpwF;SnBfI!$jY?vp_%`ae`>!L>j%`%?f?&j>6Rk)@tkvjY( z(eqA;rgC$d@hIx&iD|>c!_9d+Gizcza=hy1BU}SF7!y;Wagb=jXn~xa4b4v*- z0J6RBpd-**)Ch*(rQeAudC2f$+~?&dz(vowf zrrmbZ{OFCajKVr}BKtHiqH6tK=v(&hesaXW4}o1%KqYvWSg1%$)WoeJsr5%yB|%6b z;{MVXA*JDg_OI!CLKN4Sx#qOsOKPEQoO}Nsr}NJWRxH&rE?p>Go|T4mY2tl^Zn^YQ z=qu>s6Mfh`X(fQ_c(M!iY6+m7C*a>FR$Bv5d{6n!GAS+h&0KritOXmTjCxudE7aZRoe8_Bbzd)Y zR?I_H!vD6nUk4TF->qZ`kVcJ##{1CDoO|O<%JmP30CQ&>wL(C^JuyUFqD|;|h6iO} z1VH~17)IQyxj9eGulXahX@ka5kxhGGPqP9$qqw-|)G5~GZEGkMH8yp#f^;%}BA$*p zv*^04+NNR!w#hprUB+FM{+w~OYW7H^*7+jS`rl#znjh^lLZcUl?;kQiYxmGrtWIXF zp|~%=K|la1=kr^8Cz_rEWDTa1xpRy26u{%;p>7qtDqAwT3v^4HZ;PqaZ3}ELQ1TvH z;$HlXgSr9>e|^D~L*!N&I6l?hE<2oBGjJ6w;rb7}BPJ2aU^DHZ+HcT1jb=qnK3 zm@J2xg>cY5xCnNoed|rRie&Q#cRck#*Q=aLe|3-fg$mU~t2*qfO(PgkrQ%x%@0 zHu{8E+Q97N!R7OLET8ctzwHLFS=59~DU*ZJtH;IiYdX?3!UMa&>|g z^V6}rmorAzi}~e)@ORDg=0_ zwkH;n2~92!RG0auF`dt_rDvq+-cEcre5^IPVxNA)7fq9tL?2zP>c+u&Wx!ft8nZ4j#MD{CaFeTY(`!VbrMH7h z@+&D`xILmB<8--tm0I&gIH#SfX)N;+J+(z6dEyUnE(=M)^W*Ez$)e)Zdc@QElsla& z7tG3khjC<(IIsG36Pez9WwDlBClC;GaZPmOUMh8}R0IxI6o5{B=}6U}K%sHT)#$>r z{3#fyTKZGjEDenRu%9kl@euACgC|?|kF~b+|3}zc0LAeweV~H`*C4^&A-Fri5+Jy{ zdvMnUf&~eLAb~(2I0Ok!aCdiicX#&9y*K~wyWgu<^`^FJr*>;+&-Cfj-KYE4zu9tL z)}6{ImT(;u7*Q`0!tAn}8q9UxA-`rkcr$ZqS7>NxAXGi&RCFPu_Jm6N*H3!c|62wS zs#cr1#-TLq-Nz3d(cN>UJKJ}Q!GvHv!W7oCtC*e)AhI_1Bku{}Ete`Ij|KRZPJ1fg9WpX;qSV0fi^mYadKSZ68Y01}X#j;!GiSe0PH2vJ>%d>9|ENe-!Xt?&)bEPCO~CeT(a z`$aP8kCCPYe6ZmDelfbIPc?_ju$#PwdlaFi?SY11Kz}72i(EBKnB|8OTIRS6k1So0 zgX%Ep|BM8n0=jXq%10bky{GTZFGYn`DK8HOy55Jr0KcKYhAD4L%oy~#?nZ>4yl~!z z^tiM6kkrn5R|fV)mS&p|4w!i$BS93Vs&7h_-DS60il-#vnN@ynuaHEbO^$!X zfK%dUQ1VUCR#m{y)nbV#f1?E<`l8@Niup#2{(=$i9Mm#VW~nW+a^d#@2ENeDye`jF zcWJ7hfH(4@Zr8rf zZ~k-%V~3N0bM@*5Yo-7R=tZUmz9J1b5;LFy z0`A>3l+>fw=f5g#HtQ1*v|Pk3jCpcPg-WHIDl$ov(;Xf&T7n+bim032h1p(gVsIMM z5SB-CS;7-#@G~J^@`D@~=I4j{b9<`&0v9Q{&T><><6u%K*;cQ=aN^dUt>oS+b>0tC zvv}@+?6V5ja#?I+(s|!>tt96^(J>Yb1Olnz>h-W>o|r!#IopA4z>tPe9yvvxx3eXZ zeojoE78U4emPdF2pMzg2kMR1f%u!?nu*y$3cQt1|(h=WaFi3=z%-(%z@$CT8BIx{A zyEBp4|2^$-Z1-Q-Nbzn|r@9Nro)+Z+#i?&TMP~yVh|!0u2C^E}#L6B^mYx|ze~9s5 znv^7cprLnoxeX>V^c} zdYbwNr8@UuC6W!C;>k~jcHt@y=lNkJHt#ue_eJHFH2zs`>yqTpZ|!tOYZB1xO14sJ z`Z!p_T8sqVyOhLm2Qb9V@F@>{XfMFU1~wmsDErqC3) za=WZJ2O4t{o4d2%#{OaN^j2+07{;0(9%YT~lFNAxvT>m*Ei#*>euHMI_V)gI@Ac+c z_Cl@0*u~;gd6p!-QYyc(xR`0-FWuk5ti1cjN5l@Sij!RVN~{JI1)3$PQdk%mRQIAA z-Jc?PSD&)n+aA3;sj!--OLXgnMOp8c)6Z6~96lmIG^0V}G~d-I!uVC_8Ljl1_xCQ+t1skD6e^ru0S6%A?p_1^50>6Baesr}O=)QcJlR8fLt;f)$}uk2|EE z^MFJG(V@3=={%#3L1FD*dYxpG6L-Vk(yR+~xNPlfm^P>vYnC`?hkZ;g=1m7!p607< zb8~aCKYRt#7S7o+?D1c|$i+oFLD=nP?sxLswmy+cuTd1KHSn&1wVEG&3G`sS&T?YA zq(*RKt3N3;lI}fGF|%H^Jxu-y4o-s4^|8DgP2lt6Wy!Iamw44!j&n9xN(+2=j?ot2 zAt9-fo^e^^cZQXkDaIU&7p`}ms9lHE+Q{LY8`N=iSgd>Z-}-TAf|NP(LyY;WTSwHF*zDKUITFItYxV`%l+IY~3o~xu*L>fTWq)d;*{V*4 z-Np`&%hR?+O}@318Fct68wlr zRZ6te5xPXJu}KZ#vX#{+@iJEset=eEP_4RXRUmgq+Z&hlUjzsXIcWY1JSJ%-R#~aI z-EN%S92*`ReFnRE@p{v;q#M)H-KKc4k;f;R`S~m7Q&x678?PVf^soBwlqzqsET{B{9K#(?{$F;ehu1*7}^VoXzU#SVj@+x|~h z%~JJuEM!ZyN+;k1Umng$ma3Boc?8c=InDo(9a-Kt{WhKpN>{DKcIy6W|LS0&XRa%e z0vcQDi>c`bB2#8-Yr)*o)dA{f3|6r4AZTDf!k-C^-cyTu#b#Kb+%{FLS!J^jL;-5| zKQG`GYjr$Vt_|p4%W9LW=jITd9+>bS+*SG55wX^hsb?-l;D(BdEb6jrJVeA3kyz=! zFddgEUeB#T_YV2=W~t7E^bRqGh}X?SFyYJBl2bDIA^K3`V7k`x?w5B8wDp zIi39AO4}LCc*lKq?i3Y`wA5%Lxig%;sGq(yj{A+~?y?XkS3WsdQ`@!uP4F<|*1O0v zR>fEhD(tbG_i58YHICW-!EkUC(CwjA>@?$%xCIcG}j1`cSpUwY0csBB)pMOEMgdE)|WOcXKyh68Vgy zeAM4xc&5})qd=L`^l7oq+jZ<`?n+A_)Xpz$I9>2~r4~^Wg1Unnzm~Pmk!`|VGrNoA zG~Rh8h2<0z)C`9vI9V-k6=>v0YoFVPOdm^1$nONXlJ?x#ZkLv~c6&cJ=6>vE3^nvx zZ11}0FNi4rz0M()9Tf>r|NgtUm%zp@x6OG~zS58Ossf)Yla8>_`?*T1?rxc&a+6ZM z20<#}%l<9YaUKgK|D2qOs;Ah_h+-t4Kh)`&d2qEWVROi z=Jq{dk6MF<8*$p~J}~o;r}zV=1mARQeO@GR4c2d==QDXFvyl6q$IdWmn50}$j%jYu zzJpt7NN_OayRa8%z7=LYr9Zpm_l-GlbaJ#K)gwQ$`loFFG1;IKe3e#fuc7sc#I`+w zk^ln+Cg^ac+3O6J2`AwTxrk3^d7azcISApQ!1vfnZ}3d3wKk};cHS??rnZT;2T>~s zo*I(HhWH#m3k&y5#!a)Ry6W&*^c9#ABx(^c$!O^F*;7A;h2pX3s~;6j+3nWkZG%Zt z(dK5iKkpkbP92Ew*DJ-de$sK#@+8V+RbS0l+l`{Y!B;yhNEp;RPv8qy+T)_oK7`gf zEQR$~?hNBGsCWJxG-6-+`t=9bvs~^^=^7_TA?~*4Bz#14vinb!W&bMGr!Ut+fg@h8 zT_;_*{1Tp(a`vlJ3iLtVwRO4$5g~-+Mm%XDzd9!zgJ}U*jug6s<#(gdi zLmAPYo*r(fgPF+V$8^3;sZ8x2nHBK(tK&OH&={l3?!jS%sROrOx>UX;u4;M4$NAmwlmEgtr8(B zTO`=pmLPU&0%$b~d~!eE>%IL~$#RtH>Dw z{=BoZTcc3-wZq`7`44ZO{mH_SmdANw06@JxC~FD+(Y6)-qHlrbX?y5P?!f31n_lS~ z7de!&AGrfbYt3m~o@APM0uG2kor@O=sgS!#j!h-L*B6Q}aZd2H+9mVuRwLi!$;xf7 zh%f=jjp%JkK9}P&tU}A<{X%D_)G~$L^sl4k>{qPrm&Ay;zcB#Zye_&Mol46Mj%xe6 zPh!uvaBX%orCV&;y#~PQ(v8@l_Mf|yfU_yST2_ORlv!oNfee3y3LT(m zHtmvIOyRN|ZWlw`FXx2;`#oR*DOdm=B$C7eC;)K)0{r{4e?C~nD+VAQ=DPD`sjK9L zRp8Q~MEA$lfNzFY?M>v5q^M-6TF6AeD@&3u=gJWa*m9%e+KakV0|Ww02-8qdqU!M7 z%tLaswnP9VFuWO-35gkp6>purh#-pu7NCFyqA$haWKs|S+o{3_0!Hg-5Q2KzA-u~YU(;N6fbTO*qP!_sGN5v@|)A*6mkGnAH>77c_!y0tbviHBpA zwJ5d~#AsN+spfc3mwYlV99TAl92GbE-Y%g9N3mBptx=gKQc8vwRw1AhU(9o%0$;y_ zNfn0++*2pxkILvV!7UHZ^oa-$iCWtSj?Eh$K)KrOEy{cp1#0gd4B*jo_(la3C+V%ne106+m`tf9=HmW(U^{g1YY^G*ScH%E-Ma1h1x zhI8ERSmKdh(1CLpG@2K3l!%7~MKPTo!vexQNH?f{`yW5TYrG4yL2}6#PzaP^NSX0O z0qkeX2Yi4;#3LGSpBos> zv2o3A34FRohu_$^`9tuJlGFURo=5zn>f&Jk)33=wJteiP)M|Cz2aI%eX`7TKhVx2~ zqd#=-(+WTO-J!Nauj*f`X(%Vi!-zNCqrZA>=yJMBsXcYdJtb%ji2Kq*<{Q0H*JsL% zJUM~4@7~!={|Z%|Kx=EB1b|%(rEs;}dvqdBoQ0_vz__D3rkgwE)*j86`*_V62O!Af z9-4?Yby{hD!T?Q1A_ypho8m68t=>lg3dn#||B)GL%i-@`d)`Jz@k0s4-vqje?JYh2Eiff z#0<>=6sO})B-mY`?B-|WlNs&sfr55ZBEajW2{Yzd)X;G~p@;WBMgdf||6>)vGt@udJngwFr zeJ~+|Fn}EiBhczTTVYY0#A~unLkjE?P()W?0F(IyJhs!_Rd2fbzXwym`Vxd12|j-= zQZ9u9PJb~rK2`dGrca{ap=_MjVV=2A`*LMjd5Xv(vb&XtN&HsCr*yp!HJrboOlHwO*}C z8yKOzfCq#;jwfTicSlV+m41jGp#zYk9hQOEKhS99r+Z9eI>Q&`NWf{H^tRB`2{zz# z>jaxeub448K3%GJVhbpP5n-o4-@G*5X13(f<;>ozU4!{lGk*iQCe>wk_+jg-^p&sQ13DB;0!Aqmu z=+qeY6npeLThCR1fqcP%0g9ehJE4y2f}Rq0bx3HJAEcc$*TMt12v{%wdS*%(2L#}{1McJ% z66cmv>qIThL_l=4oCI$&CTCeIL2Ga?=JEUbsnleX1+QT|6u4=$>@mloSj>=iy^fuC zH%v5KB9HiDEbeJnRRSVcmu8b-bL^?aD%X9~Q$XM|?aO7>@lh0Q|KD~uWS7eK*9RlL zUkfL!Gc)Dj^qzlmqWucIuP=W#Km<_mS^M`l^=ci=HuH_Bej~NX0Vc*99#=%wScL}p zWFw@-+)krdj$oH@^L^NGfSj%E1^#R7wqn59tlKovdL6VxuXStajOH3VSJ<$}(Ap;@ z;}VC`SKu;LbD5O8^sZc`au|WpdU-9GmpBQ=!PKfX`Y${XJkWu+iZb5h1uj$jl3^I& z6o{lf-7}5RH$QI+F$MO2wTT`{6$wAP5qpF;k3Z&Rdf^Smf#6nljgg){{}62LmeeJ< zWcYA`2{;8(wtLd)*4PeF7z6#!?6DSOe~_G3=Z5*v#D~4i7o|#cYn^!j$Yb&`m>(o?h5NTd}H9 zMYG+D+YLb`P7$YnD*>yuJ&oP@Cov4r+0;i+Xk!x@8Ob0Qn^ga6~%pM^MXt4!G>IibVk0 zdO*jm1^}r-j?MQza&r4(%|X0qU^8|b;&CBt=qGC<7_!y;7U;8(LseB3x8j*h#El$K zs0b`3er~&NoLw%30c@8aU-%`t_^baw6Afx(e1ZYU4{z}^w4%#B|p5;1pS75FzFmOe-Or;YTUV{N+E`ks*MA(|<^F8DDrl_xrZBFK! z^m4HQV_!x;!zBJ5$V!v#l`U0x12sBet#$xFY?o?CnXdBl^3Eo}Sbm+qVB+UX7FYnO zAwvRpvI36lm-g>3Z19UkFKEDW_B6h& zx!QitMITM{y5nHKzwQi2e+*9i-IOQ_2ejcNu#?V>`?jko%qELU+WS~C9NjAT_XnUz z%-IC#<3GuU-af|-Y6r(0fFlS^}P-q4kKm_28dV=N9fL1|H9{$$JUN*Wo7#$U9 zmhFXs?qc-{@zpPHoOY_2Um87P$Krso0mVB~L3dX59PPM|J0h78z1Cm<1plnGI{$Vyb5biXW(Qi4GNeHur3SL4d7zbXAQ+WQHA*uxFbSq{EH}x5 zE3i-1;%mO6uF|*U2bjO1$p3rW60i5qH;>+sC7<^R>o6us9q}PJFp#BjZ^#Y+5yLE( zj?IH5Ljc5{N!m)8tUnuODw{|xLr=~4AG{XeBo3$fa9?rKk^&6g2mr_)%cd>J3o~q# ziA81oBAV{#5WLcpLL~yA{|>eh9dy?bWE}bQF%=U^uh_A8f1Y|#ct;@$XCt>QB&G|u z1}GXE=C!siAp*KYO}-EAX@jd7$$chUTIQb@UIVR~B39CFYaL~o8PAo3D3y_p^MSQX zZGqjv$EiE4u`T~BqkrwofT0Yo@Wk}fz{yKl3uv?@CgU!Y1)#tgN+osM6Lde`=lK7Q z(*8Tl6?Xc8M#yR2>oiqhn5|j1?63Gk6b^bnCQI&ryB0piBO)qF<_`gr8!ijsP2Vyy zwuyIttWD(>Yc|p8(tQ{8pRaQRQCH;J4hNb95a*`~h}XEUj#@v}xUZh!mk(RrR&Ix8 zXMGjo5a6Bv%AJ}OH;qJGs`q-A{|v^OR?FQ|Lm0{n0NiZpv*H1GcWb4tu_%yhZgrq) z2gLb?s6wuP265m4M*^l@;=a{wH;PH}Co7&y5J&&QJ?!T;JYYi}hP5eh#T~BsY3g1T zG9WMd5Z#glFahd70T7qGrlSdPZ@$e1F1Si4R?~jS8@v$rPfdkqIe^>es?limYtsgyWkB#DlcHbH}LpA`pW2Of5C+cTgzJ)ySCC*W&I2ngWix!4Id#g zO<>wrVry8o482Dq?330DujqqZ=|N#Ol#lZ-P4V(|Y_z(PiW95+y8=ahz15*v6F0Q|`j&y2LT8(B;52VVFL2Gy+T?s709 z;EG7a3|Y>d61Sr@G{MArh6O{EKFJPCbi~12Fupp~x9a5Rp*Z*HaBgjj9;ev%r1jvX z`Yll{?ch|-Q31PpsLmxTy5v2`?(Qpm`=g@>vEu3TM(=y*oM{qi#0X9H*Cul#G_&GR|Ku$xPMuWxc&sIH_>;#v zTE1bh>g_z7z|ALdXD+xJ5&NJTN;_?x9V8Z+uN!G!YohqKecp0-UT)VS#fFgzZhjKy zaH*^4G1fbd4jXeFoPW%#j=y~rR{1M;YgiJCFLiSyS4bm+yuvqvy9^j}-*0J5{TW+t z^HJBETx2$WJcUax1|xdZ98N`H=2{boehGj`OWr*!6Zbg_T5yyehG#NlnK2EA8-Swy zrTA;pTRDD}?N4wuM#C6oM#p}?2+LBYsJvF4WIg6Qc8UFH1Rk$L0!r$ad?e;B$G6dY z2xm_E{hA-%6#e~II&PSN2baOTLTXgl?wi$^u*2UenE{TjTCame0NF!52_<8Q_vfJ^ zfFFAIiMrY-7Tu7XwnjAYceYMOE|md-?ZwPT#Vk*0X(mJ(GpE{-yEHo2(~GHi+Wt@G zBo3Jo6-vJ~n6~IW6#nX-^`-UM|lsq)B=+EqL2kS^|@MWTVqwn|lY-g#1Yp z4QRDX67qaKj7pdGS_-+uqWtGx#Bo*& zZWmAdF_?a8T^nJ=ZOJJT?Z;n{JdO1J#4vu|wfX%bgso(wX%8k9u=+-u?frEff?1iE z5^!wh7PUINX~83l{d-PjQ_;tu=NA{A#?q@jADrPNus8UOvX)@8@uto_Nq}>LyMZ0! zcf!VJw%nY*e?0n^3}zJGi(DO;MjyR=@p?OLW$jx5^8K4G+MPP1>J`y-rqm#|)Lb}r z6^*D>i&Wmaq6?QFl7qi`(|^pl>;kQK1%GE`y+`cMrVwZAma}p#t`aTYvo=KsgG%ej zWB3DNa+7D~9hzl}#C~A(w+DMt$!lRFF1o?zA@V5_r@PN2NsBkiomoUtW9+nwulq%% zj(xwbE9`KKsq|Kttqf7!E0yk!6s$TA@|5^Jbr0;4xyA%Dw*y2Q|w^%*k?z(n(<)pE{ z_slkVrr(mY4EA8NnNxGS>HC`jz)Cc3i9!9x?L8mCUrW)N23zYMz}=7+Ya$~dZvH{^ z@PnR3tKU-;LXws1YQC-Ll__oQqUht?OSY;H66bhrPlk|OJIlg%B8Ap=N zpz}@jn@Fq@zps;II1&eXq@V+RmguHSOc{5Td}wgQ;@6`JP>V z-<}}sRTIuWIf)h_A4P52AGlw>`tgmtS=2@oW&>AjK3-HB)kw=zY|8IL=BIH5n1%0+ zeKior7|300!X8`X&v}%lCYnndEb5c%(#9B2Z3a1dFviass|%)^>cmv@lpcG zXV(>H%P)sg^0qcc)b(>s#67C0u*A3P4kZnRc2tAB6H}#g_XY1TNb+G8zIiZ21(YSh z!ioPiHJPxPo(++tS3<8tDt^f75PLYo>W(!DA|3^o5{wmGt`^Xf03@V30 zo5s4MK1T-ENBf=)j}HdoCjyICRo;A)5t5eT)Mu>8gQA$;erdaRwV^1?Gq6NXhe~z$ zSb%pTcGK6HX1q;}3iX^7EcIVV-1n2_wb3Uv{ts{W6;~dqiN*A9=oOaVEj;$x*yyAk zK1MBR7{1QVvu*e_M0a95gCeZ*S3;}*3@NIXjm;t{BK2$WSCTq&1;6j_@i(o-B91J! zR}+k=@-eSwB?H`wc=eT<@|1<+@HW^j+dsbwE$Qs}&8pcEC6nqM%K{H?B1Uq?&fzOg z)erY`?O?+QE9xZHJA0N@%$`aHD)P$N6W0~074TJq({6a=;!{{vFXwv&@*sWx;JiK- zp@fIVj~NpG*uZXIHRCc-9klVxiYy1=q%))@H z_4TXQ2$p3$SOaKYbQWEqyB;v*CiHXF==di`?|LMQ!KWWt4R<^1M@vpII8c(8ze62^ zananThjNY5|9pmLYM-;YXj(}|0coFM4+h`he4>j0_qc!>qy`_64fT8#Jzm;1qN zs)F5b_h;)0J>DM#=6e&LNqIv-YJ(~89_9E?!pQ~%zT@83a8FS zkyp_QBB+BM>!vC6V|&49Kwk3--oak8Zg%feO$++Uf^|qn>%6sV|8f4vZLHuy??_-_ z*57|R{i~+N$P|$4Pzm)Lm($@{dPC;I?N;BdOcnQ*e@~I#w+0pI z>ro$D%%2n;1i0z1!=}|y*H1`&i?2U9QObYH>Dc}&P%yBj^2w7>H!rlvmkmHGp=a-4 zwJ-*mk+#ytj@cgTq$Ge*8l#SSC9Q3Z)CCpMNu0Xl0y=zn7TLX@gGndug5!f95NeIg zX0Fl=Vg6a${{&6t@K|WtNW0fXvLzLCV{s0o&US&@x9;Tu&lf+QHckQ`I!*#t?98cn zM}i*Lcf_DOV$aV#Vvo-PX$tdz)B%Py~jI!LFd+lia zDM6|lyu}MT$Js7jOi?23s`aU_3`qJ(s7}_AFXj8i%JRS4xA>u)b4rJ?8fD zu>K!C@*~*Sq=_G^7Ec=3R_x}h`^3r^8yY-k3e9?+9ZAQ3`UlZ?rzwS6UI{ zvAmt^{-Z`eVBn?oyUfO z%f~eBmXS4R@eyF9n_rkPdv2^J!k+H#_UUw*kqAL}sZpp^dkiwKC}!|`?6u!r>cA=$ROpzV+B+6=Ajc)yc3g8VgalFN(u^>XICOVhuUx6zzS=Cy?Cd;Jq|~M-m(vV zu(r`599(Q9SbNdiPR8NqRjOYf0DXR1UF*Q1SMu@p2A?o+4R+9aUu>s~e%S9s#)*K; zHQIz!yH_+KM@BA-Hp|A6HBiQTZiHQxUkAVK{gASts_@%%u|MtWSGTKu9)gH$Z1hO9 zL?+FTM_?|!vB^JzVy?m_AqKocIdY`ovZ!nE3xM9DtIn7 zOU1f;#iMyw%Mm8!On^ksT6=c-!-D70GAR!6!@*1$RVXT(PGiLX;DW#trzLm-39Qa6 zUq0sIY4%&N3-vY<=jjRaB{B#Q^mK9i*lBBJl@F#UUpf@1WdA9Q>BBjxcF6NncPwFokf(#MP(zn|5|X-M__3y@ruwq;2L1ir0}gT& z{;((5`t5D-Jssp2c~h5V8AC)zM+b7UL`Q$3Vnjwl`=wWl!9UvIe(+^~4hHT%E6CaTyN#!+Y3ukMoz0M)45bv>X4*vm?cc@E!pA=|o+oo1 z*UxYX`3g-gTPSEnTE&V=cfFK>H(LXdDS7WxKf)uvc^yE7@F}fBas=+d#JM$NNyyCX zC##XLqq$V5D!g0SQiV4TZfEj?$nHWdTgeVc9L;5SO>)f$F~ZYiYR$ABqiXWGrU}lm zOCHwgIzOIo>cOls9SVAvkguP2u34rya@M;hIXVl8u$iuUXYDk)7SeZKs#i(J%|AQQ z0f&!xxKt^`O1Isg8?_$T#ZbD#X*sxCqQ^UuCZy$oQli)LJe0ms=QUKD$3~$(N~p&w*@KVMZAT|+1yxV6AIoi{CfweZ_3|G zePzFDE!{|MMs!xadB@)p!nsvd_hy?;R;noFG%%vRNKW>njU0p(B*lOMmZ1hh9w5um z^x&`2S?3MpU~78j#ib+r54tthYnz9U`&V{9pFol)vKp{-7piVS)iHgvuuy9!n?4!G zjzN2SvDqitnexdS`rO2=K|T*hKt#Y|&`4kkNJ&nX8lcuFgAGWRgi>K(alY+rcW;l7 zfB;KERf6i|P{4Jwx4~U6{Q4+i3mu6ey{2&+e|wayb#&Bt?h7r^*PHgPsMo1{;oX=Z zuS!f*6!lAxum*mi!Sm$J9Z?X99;J@9vo-?{*c^Q{nk5ng^Bo0=LF13QVPtuz!uvMu zi)q+@jfRNTs0J4W9vl5kmqaJ17#SVecnig3ZF4c?|5$}+GUzfCGzJ`KaJk(=3k!B8 z?6L^CU-qhTx0K}WX*^*{sxTtH3@Z~+OU&h%=@HxAYOVS$);h9n8gcb!ze!>D*Ls(T zM2mY7Zc9^s&=r;JA~ruW(*lJ^7{dffoI0I>&>I?NfFhpI6rnuiX)hE7uSom%o5#Hg z-Eq&kUt#*xT*yO`yyt*3jgjlNAA_3X+SuM;z7Y#gnbDV#B zdw&J?NwH8+V3D!DJdp1M$$3UHxL$c+q3A7p#d-eFUUrlc!5|k!)6<4spIr)?YN~l< z5RdhWyv!1kMr}+eRZ%`-)Z3&bksT`P{}2*%L!M#4pvkK<+e#5Z$fDm!lUP1_0StSN zEH{7M;W1x|7D-V&A@WjeC87}s(eDj7%tFqC4;Ro z0sC4eQV}j+*lru`l%pvO%~>k^^!&QisD(_boFFV{@Yvt?hF_q;`bwizy^x9&B*6NZ z6O!KGA<-A0JAkU_H?ciLL%VvmSmWPwNyrr$23c+?_rKdu1p6S~F?xF*F210B==m(4 zlS@`Qc%FK^ShEaRU4EoJdS^(T}S`r6Rs;`gP*}jb^Nq zVv}ry1_V5hTBz?dK^r`eAr)lhp&VnLDMAKN8PR>cGk&(#n+bmD)=c|zY&p^hORUI>Gvt^+%c9-{sF3dN_ zE6M%E?PLAN@ZNaMhD>EqlM1=#6&}2H(_i17o}Y(VwqRL1gML22)!BuY7oqx{mlQlU z)6L1d(ARRC*^T8;I9M7H-^;kvLYs>6!*tM7(6S^o)Ysa~WF>0o5quZ+-fs&*Eaw7o zZ=jQcXqfJ$1i(tCTZX7k-cnKPPx-I+t}0iL8)v)9U`Yw>o9_msBgQUkdk=g~rpiS~ zO{@Q&sDW)r?jki1RaG!mrN34S`2m$GfYN@^fE4EqAyL6!dcFO)QXlfCJ$D(Z5dWa0 zL>Lzdvx_v!o`4O<0jZjXX$?4i<}&S1d@cVGr4%kAGUo2?g1#h3@pc_VaFyv`As?oL z0S1lfZfoybp{1zMkECVI+b+x3st{^Kd7pXhrLgWP>xJO zvO~DgeA^xXm{`13Q22-eFXDSsXi)pSJ)~znn`zR4@ZS?JMGOb*K833_yY0QScjrVH zt@J!0=5uJJC?Nn2gPy7uUN=@)l`XRmw&KFSqa#;YfX9K03)C)h%TbiOmwE+*%ga@4 zP>dgMGTDnAl&=V>{%u?0ldbqA*!^Het5DOxmNF}=9LMLe!ZVhXFC*gJTZNo0HT`gO zTF%KbBeB)Z$V`Y@!fiuGIgTz=Pn$0Cej4=^Z?Rf_%`eVu*%&koG{A15`_nO{@jlGC zu4V~cwYfKrBX}`FzSwd|`(^oSor;`X0sy)@~sWeGJQPrlihsW?GsVhq8@GLGaso&`C*mqBz>( zqK`9vt;0)V#Xw+y%jbvtySaJKu%i}&*3xuA95@JzR&qv@xhZho1Z5TRL1~m_ErsKE zy=JjTM?=FO{2^<;yWB*qu=;p+@k1DoB432<24wpqL78kd?p;Gvt5)V z1(`~`f(9C#p8cpoz(Zdsq2ZH8OV_%C~4t~G8 zJu~~=i1+B{k9c{a)HyphFY_@}#haRnTIW1NK51sWLgMNE<11GEMlX<4ilEA238lty zxiQGzlgd7CsnY0yw+6$#Y#$)WXW`61FRG1?>Q$Eas za>T)}3X2R4?F#aLST{9A0x1YYMN|$mSPdI*Bi2ge*G#n7j087NW-=w?IdnOKN}QpE5jBap0)cco?BkDsGB~^+KY&@(fLbm zGI4lJB-GHWgwPT-tKnZd$TMY23U$9dhKQK#-P{Jj7*7r9L7Tl?Q)B#YWUidSi9{T7Dz3UlR=yAV&`}QbYko}Y$G8#Si@kV!b51kB1Y7Ben?diTZeKb|9#kghQd@x-?-giz^@cQIK z*SRVP@Kg);Wowo4~<_ z*dGZ>R<46S*$Q14L@_CLs~Sws&_iJ|lqS{wcLpG3BW_JL7IA)784wwph84 zcc>z-x%tXCPis|j!j$*@;Ilgs3!~m*Rd7%ZE9%tYAdEeshI8Ss9ggsZVNXk-LhA^t zV^gu)U&V|SY%3rvi4bI5tWU&5^p&%?9zoiE+@gt+U}GceU}95Wimiu&^a2ZzdqXMz zH){c;Knx5!8XQlVNq#I3eBScoN=pFa=`-A=P7{mH?g0#`9-_Dzkn&J=1d%6-&LJ+q zkOC`?6ZxR)7*ROy#-btZjHKM76HL`I5fN@`{qHR~(&t?qNG$=N-p7_d!2xk=jm6qE zM>D<#$+4_Pru|Q#oD#lJ?Ekhh@Di6^2L%uJF?HnPZ(PIxJ!0b1A1_Nnv+~S^cs}f< z=D0LaIt4ex62Rzn`R{n`A`jZMWRx2G8I|ejTMm5nDrNb*LM8X;?xuwiq3FF+G4%`j z%3xnLSq(x`ZsTu=GVdaYkR;8Q(%_@0H8x3j9`T z&pr|rdg@!`iQ#}>NWtvihRTu4A2V8!k*VT$!Jap;vjtwFszRr<7Mdl=9#eQDR=M*G zKD_Y1wG;-hK{MXSdvDhBd53Z!J~cLU7#H=Y!8d`_0Xt0!J)bI#RF!PmGQAej>_%ub z!7*!uu@zy$OAtF7n&yad^KEmH98v=n<*LYw^jvpiLh0fZ3`b~NWY_qNIoAEv3>*nc z_lyLp_ZDm^J~kt}BBpFsn~ZB7>J8!=#kdU4Vj(3`JL5f}D&sSzuV?Bu@5xmpA>vl< zW`D*x=HV+Oi4wBTCr@*4hF@m?tYS+NI!&yYxI zTqsCrc_)Hu-&0_5yjgp5k@jU6ao|~t@<_UtgT1@V_^UnpxQ4J@hbf^;?OzT{pTq$z zCPRsV+G>d6&*6mU-c}vI(V8B~TF!k*460Ncq2o}-eS!9Z!n67B@XUV)WAyj^PV>8o z!nlvAj8Bsx5(9=F7R}Fe57I1{9y>pch-d2S`O9Ue)NX|*t0^XpY7furPvTF69y#5Z z3i)1bYXcI}J@vxorJ@Aogd2`*&I73(Nk6FvaeU=l@w?pBvocGj#njZ89lgsBbmy8P zMx+&a6ky8#)&Y7WX9jnXg=;=)59+9@1ye}Ap= zqu&`7_9wOO$yln^O!EqcchmuS7XMqS@#sVPqYtf?Qbk-Yx$d+a)C=FGqP1S-OjmJV zET7GJEIcDv$-YRqL@te)^u-8R{7G0>`A=QqZ>cIy zwiGHF^PJJjY&m^x&%KGwRiRt*L(A8IRRCTIV*^j`^h4F>vpASUzA7ZLr5v|Tlb7xg10t$8iXNbcT~qh{N4i{^*Q-`AF2Op<{;WNmAgi^1r8)Uz8T|?-Ph{`PGSqFRYlk?~#N9F6^f! zMDuI?f*U_-!EvXvj8={w=5XUN1(gI7CB(h<{u7uQb>~*6nD3`gwWbloSG$mKUGe;t zsHcJE$$RFL#TJjD*>-p`dnfG5QY$w%u0a z`_=-)K_hyz^jBsBQj;+*U;3)IaXP9+%|!yA2UE#b%sn)hM+K=9w2r0+-R~bJ3CliK zvmGB08%hfdgfo+Ds{J#UxAFf`Z(2xIlaQs^M4binh_Gd314Dliy#V5_au#>|hTn-J zVYy~E71e2*H^*KZbvGjv&ns`sOO&=wC2P~(z{`#qcm*cJ{(W=?UKv4W3K@x@cN*1a zKM)ISNJBj99exi~=Os_6tF`E2`uFb32PzABlw5xos?I~7+6t^L-|3DN@={nrtX=L$Af9UyvCMe1khS96|Ydm6w^9X_fww1tVwArl#3~7>$3No!m z>}h8%Tyh_(rexib#=n~V&r?`><5ii{JT{|BE;ju=?3IaY59e@h-~B&ieRW*ZYxF;D zAfTWijiiWxl;i*dkQR_GL11)u4fRTgwA4Up=@?x~=YRo|(VYV}y0+iG_rm@DUibU_ zv;X#a;ymZP&-;Fcq&4T1vHDl5slE-eKU&^vU9McxRh+{wI1IU zG7Lx%tBBUJ+7`BGzeia7&tBr8zXAgnnJDz5JQiJcGqpWT7W;DyP4sm~079isU-IAj zF<+K6E(?a>ogJ1D>K$1Cirwt+>asA2xBX0{1vEr8C}Q8_M}KTsNj~!w&o5Y0_f7pj z!tbAy_hCz?EezkL#VtHx@y`P&sF*5B=3gyD|| z4Yz~w*GhM8ZKkYO-GbB*w2A0m&DW>>2FfN%|Dw+`TO#X4$lA=JKs zCPduKxu!lK&FX{2)HWlH=8Xaau9FFfaLJRnk#)(8~NaMnRdK|1>*xg zp2u9qTJ`i4-o(3j^Y@>=qb^??OO^9?A2MWa6f0H06YXWWY2Pu^I`g$xZfW1Sjic3uH!b~a+4?tgApxhg zAAMbK{fWNH-;oWB;aV{_Ioj2sFRZ!+bkDuiT8+8fj`ce;?^UOoaoH1f|m1C9a+`A$b- zEeR|8d_5gi601c6|)xCZ#*y6J`7lm^r7(UA^-yO57Grf!L zZoRCviGD2NP&@#N@N}sNeN**11+&*_>v-|kkvbpB9;l4ae{vt&yA>Mio{x9?w|V%@^FrJiR$@PlG8IUjNcaSuYK`8l&vW^) zEqTk1`$pP@Px2|X$6m)QmhCNX?9tr-E4VJbXh=)8+wg8= zg4HsKqCJihLCAi%HwThEzd3z!|znHQMz9VU2V#smXclKJRhBsDYTG^Z za#*G*DvWD8T?TuoZLjqOVT6z_NWicRLB+y^Q!;C+=NaeHggUHcWmMd-*^v zCALwk3>&2o9}sEe}yuEM^B%hqn| zajzt0XRw_TqpY#6>R$buYQ+I|-@x~weIxFX0}FCCmdgF|g#oX-BE)Aa|Gc41;61eO zJs-pWrv2s#zMXfwIU8h5o|lDX^A)-`UWqZjw{FmT{Ocb14<>-jKh5#p6Sv`^tYr;_ zx|jzsA+lWEUao(VJq(yySl*}77BT3I!k9dF4E>jz#Au`omqt#y)%=nOibtD$mNzDb z%8BuX<-xxQcq7iGKjUAXrNyfG) z02h()AniNpPv(`N_Yg152e3f<7$B8z{EDK71t6_Qy6POcC=)5!jmo_JrmA!e5C~yc z8-i@z?NC{QX3fx@Gi#G48a~VDJN-?+_4B5?I)PZ|WBP8@CgsAze)emlP>d_FPc6Pp zK#ahtt4!&Lo-%e`!TFKx-lP_E!Fgqdfy;}wll>9e>T_shW41?3q#WYXW@)5wm-s82 zN!UkM$*G;6OyYBKL0%r|#JBN`mJ~tPe|?R0mGE3DQn_NcGAB!+X48Y~13n`hSX4p& zP0iPVfc_^0yppUa8$FLy`H~#sjSap!cs1>0qOi2X2*B0#COMzgrqii(mLVE%3gREr zdT>A$!6eQGhcfIFXpFeTyQR#8l-g8dMN62Im0 z^PVbj8zEoh5rdm-PI$~K6T#oEi68##S zbN}ZUn0uxbV9007lc8IM5!h{&e%)~#Uoufb5m0!TRU3R{TIzFpGPYiqgi#?g%vgdM z?=YXjMYC4dQ|&c;9gQ2OlkNH<@&jIjMOYvS?nJqH59Bt)G}{x#f$$S%!S^U{UON-y z4QxSyq}uPMjZ%~;-8o!vwK?y{fDiH=)Xpu#jXr$zG8%TwxXWgXXz!_YP{$?41b}Nw zy!rT_%o-*+xM8E7ZnkYyS7eWy{;a^X=0qj4l{zmf(0YvX3Uy6fLDcst>Cr%0=AIKJ z^;b>2zOG#}JD;Tj{T6u$eeW~Z_;^Sut&PEcd5_QvkHgL`9*4}j+t#+t7!HhyvF=X# zN`S)ioXUid2Q_WR-}u<_1D2=X;BeJ^=>L_jMEN1@qetLx+SKm{!(}xb!;G*h;b@2F zB{viHyURLeHu~Nk#I!HhspJK`j`BW%J;m1RO$&?}A9tA4DoO6zobf+O4#XY%~_Lr z4OK+rXZv32s4TyvpLeV$Ri#qTe`2VAg*8w4I*m|$Lp&;ZB=OWPp?PVs{-iIc@lpGj z=6U?;x+F6`-t{Ke&`lRtm@Pzc^yqPX^U}Ik4e+w3hNu&H=_b_ zAKMezlo>aVD9gF7frV-in=2bX=JG*ib;iZIm)_lHj04%>jSqmvxz$F%CuEFU4jLOeTNwWpVMFXh>al0+Vt0VU=G7DsjfAAxsV zmli?goPMan0EQ`JS;;Ns$g+hA(|b6L=r!{F@s#DEfh@CEteb+li^Q>u|10mVT7PB` z|9b1M?_2z1)|Lt!Os-@~QusPo=bnnkoMns*Ix^1yr0Re|z5Tr=jx>Xu=3IU~E~&)j zM}AfC+q5ek`&A6=?D}^Qf$nu^e0+m-OHzj$po67Ud)%ibLQ;g9a{FbJ4TlUW;kJH2M8 zE!G=h1?yGJZB%a3E>XM%Efi`$mu8A$_?Y5Qu>^IGaxRVX_p)6!Jo?ElbcEMg_LP2k zhNnp`ns{T^V)l8~;`H=t>7_A9E0d8vj9+c@E};lQCuX2$&;*rL?9Bkn2$yx5icahq z$wYCzpJue!1aBc;hB*nU_}sbc0a?DJbbI-)++3N1AWGS%MJFh8xvZM5+v{@rj~!Nb z9gNCx?S*p}Q8tMPI+XRqHwK3&2R{?l=YfvCeRTfy;q|kO4!oz80-4&@mi}^C4Yr_==rD@={Z*KroucAWII}DGBNUr_*Yh08? zbeHgKgf``#a%VU0^#t9p!}fjjjKHyF1kNDQ5qHn%Cxsu!%qseY@X?<}_tC@bM`*o| z^VVTFP!w+4x)LBL3%cZ6Z=E?v?x#1Eht{{9r0lO`EYh{o;2nk$?Im29t@p%uQg(h1 zH9)!9zvhE(IQcc|cjdS964#|kkq_^nMNeT~-swW1aI!nro+~eE;EnjAqnDx9GR8NQ zL9F?vHeNEbW-r?>G;yv^IHnh&0m>Y;upZ7;_i&$XM}9j2-aG+5eS z98WL+ag=;7eT2^iyh4EUr2kx=w*;X*BRx;PI2LnaT%AR?%YD^948%-R{dsMBOa#_Z z*}Ut8fZ3?+JjP)FkLiSky0a|;aZ*Gsqw%{i?UIvrek01e!8&R9*E`8_!^@^FHpA(> zGgU%Ittt0NA_#@lo1(JE4nZ@!z=)McTN#~B;|4WrkSLwDxvoUu#78@d-5oo@*@b%> zDd(EO98AbReI9Wgx%f#6M&3whEoCK^| zNd=1xvzC$r``|m#eFvU-czlpu!KwA>Kcq;|lGasM)}FAB%7XN&j`U~A6VGp(ZgVhs zRNtI+(6(+ef`S*~$n8NPuQkm+n@Jt(QqTzWU+KZOl3C#Bb z2)Q&7Bnfj*Z`dh6nz0mPTnef2@*mK^q)<11;Le~b1TEFd6hpA-NL}Sgr|xr*mC1tA zA1@tp>F>5-_->FTi1Fv?0G6}FXncK*4RP*?nUe`_>XnCXS>#gdD=cX*~qfLhC#m~0is#OB1ZR4ePPo1 zmHm;Ip$+g(UR#7h0`Cw%_U%T}%r_^(iMAEnD|spByjCQfZ5=Zw&p#t;LY6v7_@?bh z0n>}HAYD~N5!l^+$?tv=`js)Ppngz6(yc?#x-_( zfal!RJ^k!fw+y+=`xik=o!&RVxhs#H*udW)J*%sDG<-o){0Olu0ZX zfR67j@J%9!1hOK9doIg}`wV(DuqsNHWnwDE3#FH*IlN~p`jf4fuI}rnw{95LOPT{# zPEcWPO55yA8AWR)F+nnR^_G7=1xdhWXM*ZmP*u?ewvth7kivI5{k~-Jf9uDf{wu-7 z;|Mb6+MP}lWyNL;fj>A+J@h$#-h&j{5{s~ss0^y20^!v^>pbl0nt3$j53HlN3UHPh0!q%aO+^M8^zF{8d;IVX+Q&tg|v%~6;^LMJ^VCXaV|EMY1TP)2zk%rSE zeM|1oEdpI=JsQ_vK!ehKOK~uZN*tT=otS}%%zF#vk#wdYiE+&h{?VZ;!86aHl25iQ zzR?N)-JJVinhDyENyzkC~c7VaH8}ydyD_tsO#X}_3{(pm@1*NGCl6`wh-)? zDJF4`e^uF;t=^tQaK}!!u|GV@dran47ynH$1Yb96lv~KJS{b(8!p)h(xz%!(_wMTR zn0z<~T>i+nT87irfn6>oxh%oJQBl=nbM2J`yTq910TH-pHN@Y*l3UhXw*fN>y~6En zf0t6nIXRTF6`29&#nBsYp3fGC%hJ6r8-494i*p1loyIgsty4>Q@T$umEzS8(uuweQ z9BG`M(~#=!4`b-30+j8PcS@GIO1Rr=FaO~+E|cDr=6|-eKPBgM@Oa@HjYS0M(#fc2 zhp+2$vupTVV~m?bmfT(toghr~CEWBnQgOv52N5UW{ite_Gv`9aarZ~VsXNVdxDW2f z)D%Do>*fxNFBL{2NkN-a&GsLn^}cF9aV^*u9-!A&3KQgE68@=QG?9_Nqo zC^C+%eV^qJqq_pLLFs?8$CoR0506GzBA((qUB@6pmwiVW%@SPghMnJOGX*!tWbTKz-HpW#wTQ4o zlK^(p?6X;No)Yr!R)+?=R@#w`re6teT1& zC`@|`J16Jk>N@Ov>0UU@2P|v9T(S%fQ)o9SN}G$GS(+*lg}c0D=adBbxalh`kUo8ce;5uzC_yHuI$e^B;r?*Hpj6h3rd2>GaR`P zC!j8N(sQLO$k^m1mDrLQ9I{Hfl)1}t6{5ynsSmZ_cEA1&SiGNEAZhlT%myKT*?VBB;=y29<;gyT|c^q6Y5>vz8`<#>(uRH(9cOHZN} z%!^i7R^J%CS<@gnejB8@C9jz7`k!rP!OVb8)NP{Ex9Ce1^@s7;J&F#2o5NKANRqG? zFU0LDjgw~y1hi+z>*n=y3?aUn!CwY>?r8>N)3ckoe77kxK^R;F+&#NdB|mEi?>h>~ zn~<0v*c=x4Y5WR+abItBRF~@r`VjCVJN?H2^rOgXuRAkachJ_tJhRCb6a#a<02E=P zx-y9cFarqC!~IY%d-eSOCxwOi$xg5D)HQjvIF&Y%Ik_{YrYH4j9G?@7qi7qKUM6g5 z@ZHpatpvQ@q`9|0S4-X~Ay=Bs$F}lJbZffhX>lXRoR44f&za!~ z=Mxqtfn0>A_*E&rAw4_bPc_6O?*9 zBKYr-<1YLA!E6JL47V&$FauE0&wrCnrPO0(VV`}_}gE2mTIM-&&qIZ zyHml!S5Z26-c{F%`vD#k3&4Z0J$elvFtp8WT&K zd8oWG?zAd9)c?z~Sh&*iA1=e`8{H;@buDK$v&yM-p!)Ai&0KnuZHuZKS49!o_7unSWEwGki;6aVwIET76!!Q_9)Obu3 z-H3d`Gt^+oU!-+UmCeawWI_L4z2_*(2y23;!6aDRyjNI^`kq>KM3mR@N-VEa&f{&u zhQdG2C;9K&cmZpRj%}|{8<*)W-GG<$UR)V#$SCGkmiOH{ZMAnEHj6N`I!f%cm+KIe zJa$T~8S0mlEW7yH+gBGG(76P2q1LInv60!^2!Y}t#E9>xtf9Q`QA;!GWFiZ2qcmG~xIzD< zftcTwWJC3=m}`%BZpiJ1@nDzP%0@!B;+l@ZsZPR*Vp=Y5U#fUPrPv4%_o_amjIfZ) zIq>EkU73x9Mit!p{vPE7LHSW=5~(~ z?v%_c1P#=TN|MQ5EK?k#8T6U?ttcBau0U*(2VOjcmC#Xfx`UN#1~QUKw6{x`ZnS&> zq)g9w!FP>_5^%*w{$~{9y~hR68DJV?B>znSDD`LKFF)-?&o7UTRQVwhK_%jhSAba`7&J-8a#MU{}fKy6Yx!5&+cEbpz==kXgF4Vi1W_JXKdPltCxKVw$ty;MeCP(*h=^Ldffw4M`T z^$;7jKFPjWk4|?Nd|JOaLss{T5#*0>Tk4sdDKw<^6U9 zNiw92A)i{MaRfRL{4YiN59WG+w_R;tKPS@goiYZU-y;3hN2kWF5?*Cc12pfZDiRhn z&K+wT=}YU#*D{sq_L5sYfdnzkX;sI}R!bR6ZJwl@Z#^YCu$)~(yi8YW)a>h-ZQ|~W zj693{=vwTieuMfQW^z$tV#p=Xr3m;H{20RbX7S~wT4}=-)$+gI=hwKKs15;|Z=%$3 zZsXbo8T&u6@4_1`ffM?{6`*Hc%$Y-Ti+T^HjxUcK*-c2pZm8`^iIQ z123gj#{ngC5#55P0xhh=kd@u4rSHG;Z@!yHnVC%H@&t6%b>)9Sab_4hxrq493QHYx zRhS6v*XRRZ0J~2*1jaO-6rCros9!u&Jm*64<{%Rb1&<_78}KaSo;~73azkJ$pb_0+ z+%4I%1pNK&LrFyMJj2ls-@awU8^7glQ>(J0Ecb+QbU_#Tpg;qo(!1~4^}I)16$lGS{!`!#;ZIGipL6OL2aqMUQ2Lk>dpSv9E`P%DO<5p*%kyhLCgUP z$eLtPW@1|reBZJ*aX zjmecTpT>&d(A_UNA#EOiI*l5MvX%b<$d;_ViZ%KL)?4F)vGb&S9VUFI~m^x{a z3uOO$C*~!t)sh5K&FG^s7uQc7JQKn!-o& zl5d#7PlcRE1QHQhaw6Zp`apt2bjJ%X`PeiZmi)cqO4uJh;rb&R?XUdl<5?!eUWyIo zZL+Pzwq)j`*0A|w$--Sxssf{oLXN{t#E9M9@9&OQNoUO*it;_ekFgbwXVPrxebxxM zS*jlCTeczja!&I)%>^fe{|s@5vN2!fPuUX^p=B%bs~9s$#sFf;pA1F?9Z}SE!={_I zhZA4s_>P>6ZcU;Pzo`dh78q}r3^ScRpoo95v+bV2#y|9eoSC0#yMwhI(Vnk-o=@iB zkUP`mNoNv&bhI60_|IL!vnGhli=9!%mpW}U_EL?>G(x|4PvHowV#%6y0F<$!i;Xt! z-tpsIDr>G%yB*>I+v>{2QK_RJ(4IEfh49%ub3=0<--MQ&$NlY(IY>AGIbFB8_p3@r zV-E)&<__6^pB)U?!zO`&T~z<|t`qP^$sY3Tl-Xm!ZA*=n9md#`$g-RFz1X_*TUm!W z)J5SRQ80T)p2NT=1ghy3J&JUW7mDA9VMd#KIf)y#jaVOrHJcHRHGXtP*Bx5TkFXT- z;cF)dmtUj5g@-%sHQ$JbJH=2BKEW(eZ|DVzCGCunj!KUSzW1@AS2~wj^@0;6GDhSm>!4J*VTqFd@E2Udxwv=wpF0`vu0+yCR<^nslG@p1&5YtZ)WVC)xX&xAiuj zCHgcB`t0YBMK*PDe*3<*=;*X_DGMUD^BmJ$KK$jgS zNc&cgbG*T`0j+%fxd=cXNV!`_df%_ARSLKjpJ285fxM_AYmW|ar7iwHtuf2-P%hb6 zg7khmjt7BCGc`@ZpZJnInTszi_%Pj-*lV6BKkVXlD!IV{)eptzn;{Zg>+Xnj?}bBm zf^~^D={_=?9zK<9rL>2(HoT4Db?N)B7^mauK^6xSlPu7Stbq}plg+}z=L{3x=7|Qf zQNI|t-?_dG=Yq?)r5md$XfTbUj>Q|J!)3Qr>^CEy4F`TUHJ6`#5moXs83p`R+Ksxx z>BV?ra-#1;C8s7~T{@!kGc-gO7;d#BCRD1vOwQODP%~r(*wf=V940_YP&z996-;>F zP*Od67)8^JVNmcDw!@zm^-D2KIY)SQr7(5-pQ^-6IafJ4BZV9jdCC(roRL~KRk^u1 zR{rk|yZ-%3#UMK^Z1-AN$)RmUZdsnPT~~fC7{i#9+(iXA6z~-HzdV$fHS2T{b`W^Q z_ZEj0?}Wh_nt}+ceuh3Qb-GO?=kvxeLFc`d2QF!^DUSas-~IYTFd{7;tyFa9%`{n& zQ;l)=ye0kKHoq-gN=52PY?6HcG%Qj!Y#jZ=S69=CI@)2dEI-d`@ew!gz!e){g!72A zzEiE=^cBkH+CwqXRX!%lxR8WzMam>D@@z#@jC`+%w8>!@i8o72X6iI!e|pnW594ga9r-FmWLBa`GU7jW+N*y%12^G0nX)=9Q1y3af#S zd8?G_4iZ+UEQwrI*$z!_DDgID3Ty7vmDZRFHZLYxW0$8}vYL4A14vnM6Nc+Ry zeBRAeNb7ZM_KYd1*vlrmOF1YQ)f{kUXxy36u2B3K+EW{hL=@|;M5mYPI{QlusYi?R z`7`^tbmb}(-GP4+Dp-<#UVVC$xT>9Pwi;>l(vPV69iZpHJjRm+2wM+^hyxFT$|?~5ib_fllg zW%gb_n)QxyHSUFaX?@6yeCHHarll5p@O3ErUrY4>Z%X*CRUb@S?@t8)`o2=Bwi&6% zjORwrOK$yJ<|lF!vzUXn#bqM^A2`66;%EkFxlT876yh^d`EJ&Iy@++1+@eG4kP@`X z1ST0^dlGXG$Ke;Max z2lgi_*FCG3#^ZVrLx*J?PodVUdagVK;&dpe{wk4WY<=IU^vw~h(z zZ-1&Vl^%HD3%R)^reAhy5|M?kux!{R#@v{)noi|>YrkAqzZY#BegtK%mx^xw;@_kR zT%vN9T|~Eq9xjcGBtX}%fK>6&HSx;X_WpZPwvXXMsrQh1@BSnX`DH2#<@RujYM691 zaw1ET`nF%@A`t*04OR_vS0ju@Cx4K{_!~)->@PlTyzKO)^qZ#rk&8rQhP=mfd7C$OEK4A9@DlbUHXDO1q;giTpL)J4^zG zut8Tr^(&MCI;|H6KZaC3iP%yl9(>K@S@~Dz7UW_wEa$wfSq6-JjW;d+%zbr3Tcnv} zSuH&cldER#b_KX;3ONTKLXB%>+|0lFkP=8ZG4OakVZwz20Z*59SzN5bJ|7X09Q<>z zc>h{?&3>K~Z5nIXPwB&WrhCw7)6rZYn*udKL+)BxXErK>l4^@FqSp==8C5FpDefwt zJ5oa0n59~U39`-g2vuf!kGUC#GrXb{pBOm#&SYxtSjIOfc?46F+^C;_UAkWhJ}OT0 z3C!zWM#eo)V6I#YSWcJLyht(9VvBEW%1%h;TKwRG$akztM!W9}0?6!g>J{(R9{8^t zOt5;7@cvK}^eH>3E@5DVXS2Q(f%Nb>>zLe>1n*9Smi2pb?uy{h`cNB zXE{u22GwjRlPI+np2^~@11Z7ToLAEybrT+*%W?tZ;&avK{=dX21JK{>&vfY5#XI>X z#N697Fs@(sXM)sXHee@*)M0KC{e44X!HmaVe{!87h|3I8)w^mg5ny#df0Df;O2250 zO~LP6!DP#b!q1kl7_EUZ0(_iXpwcL@OwA?MEj@vzT}#_FA=6`{9Q9IGST9HD!y)Sg zar^Ur4dX56M-m47)9EtO`xM=liGtRET!YBd^icsnrS9@2%#QYV9!C=4zviG%0(6#O z5nN+A6vz@9)IfAgKDOhJsWW=^cHQi#2gd-iBXe>lh~#(th002jCv+MAP%dq9@90+! z0veYp<8{aLrVXSDlnooE?MHB|?3H%tKeyvMIh-1mhN+Q@z5ovWd4MBwcv~H(U*TWV zJlA1EO4+dmN~3~DOx=ZpzstGlR7w3H_^POm@CR%2IdXxE4_yOQ?)2kv6`f|E_{|GWikZoZtd(f$; z5x$`a{ z8FMRRy>)x~wog|@hBn(-Bm?G35)LZa9c@wkP*&~Txr_(f1T)JH%)WJS*1ts za0GCy`D0<*WvLm$MB z08Y8VpPNu}T%F_*n6V*)avS^{r(W5-zf( z2RCp`nW|h(B65D#5B}=wt+D&L*9gmv31oV8Zuu*=GBQq^=``k}3L;toGdXucjGupT zhk6D2x)jTfanpQK#HG+|`N#JQD!Rt5?gm76*?p*%@^j@6Itp6ri<|#->U(azZ*x6q zF?ie-)W>cpOxNk^@qzyHikn3cKI6|AeVdRTpE$E9^*NM(A7dCu5J%d1M*)yexx{F5 z;-`6_mpdKT*3H~+sY1YB7vfgTn;c!wWwE$*& ze&TO;)FqmFQ!<{HpK(a+(0QdOX0O&Oo)@CzG@}h|bKq27hN6}wHYNy3?Hc9Oc<&x= zhk6IHB{$w#Ob$ zjaC}W^?5G%>j7iUzD48rf^dh=q%ED+`A+MATGruDO|G5N?&81Jy8Rl#ZAAlRV`J&< zyWXUs+8GtQsG?*3>&Tugc&eagBC{23dwx50(`*1yPULq?gTah}F68zVPpvMpi=*}(OByLU95;DuO)FQy=zFKz8+E>J z1a34$2Wb%3qUKoafn!vqM^b>vX2m%=k206v#-_!%>@738BT*MnqvCh7R`M3iH3Qt% z-)`{glI`LGX8B8ns$OCS>;{>PBBnvNT48uAsGS*S9(J z+_*LItcpqiRL@!_`w1v9H&dJ7pg za#>usj7yrzPBS_6_o0QjoKP5pL+pu3Nt?WmqL^A?0p@9A`Vqs*!yWy}T;l9Yt4LatxdQ%tKr0jF{E81`HkARr#TL1A$p>9nwZG# z`V6dsXU3YuH~=})oZ~Kqg^9ww&#xzQ4gQqyDs&^}8#3$V0_irmnG#n|E|VI)E{T8G zG>*zjZqlY8C!`gYJw3~s?~1RkvZB|U#J*4u#CyB9k+_qR*!Dj3V$$t3@=xU zs?O>DFjP0s@Am^XCxdT$Z;b2^J0n!X=+DLeB&UsQ6NRm``u>|use|`cG(P(0jOxu` z2!Y|z)BPQ2@P~u0hwuYGz_`-GD}_-IKwjW;uJ^03#|sV3`z!+`L} zzf1uQyTNWi^a*rdJwnn0<_rcfL}rf9EIYcZ+6Z>~PyQ*w}A zL+DG8xzaG=a-N}-FNjX6&I%mT-jz&MZoi@e7&5Q9!*sDyG*D?fDPclcxcy7hI06`| z&<^Us1Sd}asgRBgmqm(fo2rE!(Bx`lnGPptf|K`oDs zdO_;qErXXpHJQ~CF>$--GQr8oFEY!@Hutd3F1_mdrfM}3JR6C_xdE_30U1I^vk!TH zT@gUCm>T8HAhn-|#PN=8RdIUlIT}s~Xb#Ys!22^XzC+X+f=6P4V6Z&8F96m_)x`^LgEOo6zae9_i5*B z2?TS&l{xAxlC%bV0#Ni@q1?=FpSbAt$ZYx?8WSR=RK5PX7AY6cP$4CPjElL?nBsU; zf5d7_mnJWlrRzt)pKcmatf$SA)GMgyU5HQ@H-$G2%U4>zBPUi`q%F9wZNHIiq3PsN;~@HO2!2T-u;efA z%of|b;w>uXP9q%rLnnhKM}#+u|A_VI*LR-9dBDlmqey-+NH@0p2ndnc_B>+f?!ENF zMKcrerxzhEweywFmZ{$OJv;|43E!eSGD;@AW>~NDh|xgfSTqmx=Wz~lTvJqa6NcHi z^?VpaXFd}!*qG~3H7a|aU)B8Q6N+|NK2DoNOO9pT7N!8Cf93g8msZ?V1i8@1e411> z&4@#zPL0B=^xX|*uX)z)&SimZV)R`D!G(eBHMHoz$jJ0hSj$79Ao*KhXOr$WTop zlS{YION_?%?82#5u&pzE<8Vz98T@fnwfW2l!#lcC|_?aO)M2d+odA*Seu#bVWt~| zQ66f@k~)NGxde;QNA{F)1>COFpZ#|=# zK2p6%@1RR+tZad^OvZe^;NBeGiS1{$vd}3-Od(-lANN~${*1D7Pue4~Bb3G0@8Rp4EQ4*2$`!2$>^Rlo7&=f-(;C-vjoebcb<-F^q(0#dl&B= z_FvXqsgim|oe6JLowGkKd>LE|*mhyKj3@8Dc zZ5r)&mk3E`R8zl&3LEN~ZikJqm0Ne~*7p?MydUFh%g$GEx^tC^JElM&07=LkuSadol!yYG0GCkHRIAe8whQmHbM2EdFKHapxTceS zrSJTEiDZHx8HEDww)M&{SYNz$Q>c z{dO7GuaebJE-N_d?9SRv+VpxDw}4t;k0ZKxd)%g}qa~&0vC+}1Ssh^yo!^S4g+=Vt z7DhY>&8=kGVatQDFG24SR3|&b4oAn(;A;#b-_z>1&nb~$a325cOR5$@+gXsZbSi3F zT|P*DI|LxM(V^Ptkwn$7q+c(-Wcma?=i+s8X04nAQ>4BE@+tNY0Aag}L5927qwH zmqx8Wt>Q}!CWD`7APa7~L)ed=7vH7@taNq>3HTeV7axl?PlGJGV+5+Ko(`9aWw6av zE{4@$H?E~s_F@-%j+HnqT|JV)p19VjNU%5UOxDWw`wppymgVM9Nt`5Gy`uZG3gpH0 z?rgdkhd#AwvE0&CVuZvjQP>SK!Xm#L4R&p{pc96^i_JU(d&wDKpG|0br9>?y_IwVp z!%i}-q{H_|1%ArF9MQJPlaHO8DR?)0f3WK`)+JH}Og!1&Vao+hBg!_hDUgW=3Ry{` z{$(r2pA5FF8nx)kW>E1KCAq*ld6TyDH8-qjbOQof3^}+^mg{y;t)CR3pkFSmgz~Z_ zr_ZX4r629ZE)HBHiz{*`=)$Wi)Yomys_9VcRNt{)vwQyRaYg3Twt5-uL@V7QyBcBz z8oB9k#S}XXlGZX}W zE`9s=%8Vht+pTN6QIFw}J55Xt#KfzEyc)b8k*!Q^*RcehC02i=S(AgVR|}W8Gz>SsibYM|6Dv4-sX0_D3>F0%;}~a%Q{8<$*JVSA zBt$v!^}?=Wf2ULA)ut2Hw6HH8iqeUV4Z-e@;Io!Nzxp4VsnjkAJvV36Wmyk1`|L^G zuS95^%^`hD&rld}ejal9wvV2g;AiG)^D^IeHJZljY7jl#iHe+L{$3|P%0M%+^}yKZ zUAb-)prl)6l+M#Xx_D&o)D`#mH-7nDP1@S)OfPTEfoVhBbJMB{K^%u}QCoemimX=)H}r{QEcGb2O*5Ug zG%394p1Sqm`WVEUt!BCxw*2?cEF24mVkh2ptz4@3j&KGYM(XnoXQH+U>t;M^zKItN zgk?yq$VWG@)Iy&{g*AJ`9ET!eRmbIL_g>9UI3V#COY>g@Oh*~4JR3wY9rDJQDV^RX zT$m9Dm7~CIy4q=Kx}!iHO|A3s4G|=-HA5`h6CQSa zn#y9nyF5CG6rJ6V%IgK+UG7OdB*a5)h~<9l3i#G*tZ3wYjj>Gqmro}_tu|8VCNUoA z+s)40Z2)dS&P3*=&!}(hp}b+FY{1w~sg~$hI(d;F+n_GcNCMb4lA=wK7_^~x*EyQ_ zHmakogg5?mPE@6B?Lws)dErt5ErKzii$PChgOauT1r;-hbNs@WHJ}=u+Po9D>?gfy zG0N!`hflHJMS}i-$oSZ5{ir8j`C+}}LGeL|r(9MYy=ieC)?{v0 z;r&AX;(34c1eKV?)ANVE3rfLMKJVy63voSvaU-a|H#g})=3{=D{>=I=*hIWlDJj!z z${l!*S>aZSu;MJc$Cql)rkQeI8%aC(z2SoAa+6$bUeLX0zr|n;M9hU3b0PZMrPonY zy+!{be$lVGwogI*jP6Pbb!R83rC8H**d(<81M!f5b zlZb#da?CS&>I%)DW{dR_Sw{qJndMxjQt+#P%GSBa9(%TDSbcYb@K<+H>1Kq8|Qct=|nC`#RpL z)4_226Vy+^^rJ!ohhi-ruYlGf*#tMZ^s1rEbt@=q0xA98(!0ZnXLnj{DncD>G215h z7a5Dx@%p5Vl>O>=h{G$`)x3$+lZlcaYbU&Kg%Zx2KG3V+8#XJt15pxh@l1=>UVdRi zCif4e!^f51=qtK$%V@OqLMgn`YIJ*hGBh~P_gToLW7Bh9@8#AB*Kl_CW8V!vgzmM1Ip-m+5jggd=_whGs@wrhA!_VI>Z5>;SIp>GU+C^=g zD&qc6Wfi0B93xP!{HdVn^B1ML!e|FT{F#luKvHJzg;F6TB@>jFxBD`LMrSk%YG0f3 z75B3F|FQMfQBk&C`|w~PC?JBQfYRNaigXG{Gk|o1bccw5ba%(l-Hk{yG(!(c4BZSc z4DrqVJbL^5-u0bp!Cdgitm}-u&tuoI&s4ucCPQoFNxV4tLsg9E?D;(z5|*DM^9%j7 zIgmgi$@ngDU&uUrlpJ8Aqpv(AjkvzN(=?Mw zh?Jd`l-mpx7sL`v9t*@_hq94JXFltXOF+M#c-z_MO)IfHSwTEcTz2tsw0KV#9f{fO zuB_hG#3RE%b9d0y)mLzgd4AmRENP_vXtn&P%d!kW(RW2y31<^2CgA8?-H7G(8t2>p zfjn<8vd=kRW*hi$qe^SW>BzO!nr-M8*wFmb*Rn%{)2mFh4_N_E7=_u8z1|-`?(Zu? zw;G&o6~BhfAGobwMYC*Na)sUem$s#AWBp1(rzhXfuBFsmV+dKmI_f*r#w`eCzE;6n z6h6VxYh9xO04AI#O|#$J!~^~hibYus0037n_DOM^s0i|OFiY@?tdF~u)sHq;be-RE znRz0nMMX8<^2{BbKhW~ivNW70j<-|!q>kF2ifN{fA!1sH#;u`t_0?VG%VC&@kr&5U zfB5<(Rg7_-FzC16G+M~35mnN2Fzm2h!Ak?+2ceC{IU1lJ)ei>V#Tb8ju57_R3QsKQ zs%?FnYJ9Tuqo8>@+_NkFW=49u;%@$~@ycav^K>rsw{xm%^`BQp4b-bAewukF_p}a~ ze80;-Q;m@2P8C&aR*)dN@B2Ms#17%*yURMRgmI!FkUaYB4cYv@phLl_59Iwco$A!^ zeRcBZ;Jxw*SYr zq^}A%SF~UL9Oi{s77mtv-9IsD6_WbdBzyG8IrM?Z^G2ci?UxjvOLz620oF*yrphh6YwOi4kAEb*%-z@?bZ;_c^tOFl+-#o&@JulE9$CW{MtEjo5&}K( zHlwAFt)T$Ys||V7ybR+5H!9YyY-d(QVl4(3(c5PZAzFo`6zJpbTJ94VV7FZ23EF&y3RgH7a#n|4qB@QHk}Qj`Z<* z&YII=_6zhc$Gq+7FqEreBUpl2IpWL_YV!wI>@Ht!j!U1rXmimyH`6>ZMP(UrKjkk$ zk)pAhALK=L)j&HNi^xAs9f$iJ!mze3yhdJ->UsW(C;!j!LUsN9{dwKIv$2^aTgNG#39ClT)xYoh0@ni>KQw~1=oD1sTAwRUub?jPU-q6FW5t}JCjN&9eW)+FsVR3bLC<(2;P-6-L0Ci6tE6A03gV-Z3LD**5&vWW)GOYDWx zSTrk=P7dft)vu$if{Ghwu6#e)35`SkDcSwIiecCJ2;QxN(yW7uh_GRptc`;PBmZpIqR*8vdzD6tLeVq;9S8J$OjkH3YK5-bLU z@Y@JHe5AMgbS-Cx9~W1%gzyW(rVkq#k=@B2!=%6fUuG0Z477D(F%2qS{G`zAEK&x*Z)Ol7}DtrY58t8tD_@U3jzANu@<0cZoyLZu5GTPrKy!* z+z8`^_Na;D_hOru{p~Mf_*wtY?EnyqoRTVoRC>utNNnyf?lV~z-+v-B{lNO{5rwO3 zdW9+hV4)INNXq^U7pXC4_+}#ud0c*@=$~LxTpwO`wP>&iGVX{F+N$Ti`e4uBP;kIs zEuCeAn^SYK5J(K{(W2scc|HCoTYFt_D!I~$!jWCFBKb%6w6bM3o4M3I|GaMlH?rsn zrr_y}SSTBc{)l@LzJL{e*~pv?3$~%1>}N6PP+7My=WC{7`lDi){;V8|d!mz7nq@b* zu`(u}y#3wlY~w`D8bego#VQZRTt^Y}%hTB1kh;>Fd^*~Tanl5kN#gZ^#{ACp|5h*R zDJcQ&r%3?MjOEEaoOdl1wPhWYsT$3}2QveF(!L_w z2Zv%;zlC&|_jg`mJfNh9tf>0vHO=Te?ZxI8C-e5Tf;#}v(?in%>frs zY}xL+hsdJlN&VyZaV+NH@tJp~#@h5iG&Fk+CE={oH5h2dolv{ZWd>AD}DxgB{RaR(tDDr^QEW{H#nJOm5cwBO?+8Z-vY}6TAWf05yb1qp-Li?bMod38;^0s@O%71?HpzJAqYGumHKw&yWp$clL7w;L@79-d zxmq$NuV=tU^eyAMpwhT?)!#-nz<)dMccv>!(TuAHUyKF(3(N&6g;NW6q?*WX#sPt->U z#+6myH<$DCvZN*Ad2;_+@=92AE{L^ZU^KjJ&|IX&q7Su5`&wfUIHi`c4wKsUfUUm3 zli|wFmZhs}r$KO9Bei2=Fomy-dZDLzYTjp%i|cg{W&g-*&bJj;9yg`#H|L!LoqH`aN2hYb(@PFBTsqxi#njdMx&?th|Fi=J_|H$2^yMR= zkuBmvAIb3uKXk1~UQ9T+kBS6X(?5mOw6d=~07MV(e9BcF6ugp9T>ZfOR1+r#))L%3 z1x;Ymr0KjqsSP8AvU%*!XadPSx84qR|H_f)5!P4IEV-@YtZElT#HO(}gWfFn*X~X# z`m0wRkKT>p967(AbyJcsZ>aggII}~55)&w$M=z53au7)$?N;dNW*Vl+5}!v%%I9C6 zqccH_=C7?#SS>VUQlKUOBxSK7np>dkL#bwl9o)SyB$NRn>DNK^=Ndt6)t|yP6kdl8 z5{o!hicX8RiqjOx-=Fv4^wrL}rXSCFOtjR7N}<~mEWTJRIgwdM8qD0Q3HY$u%yc=> z>T#O54xe3dXl8pq_Yg<;D0_VYZp~`&@UR_OF}xw4L>(r{pv|JmI)QGP8T=Fg5D5t6 zO-K~7VGtGsZmn$}hxWy}AIJW$Bw-RoDawsKHGH?BIwxfIQ>!&rVLk1;rkU9?3@uyB zyxs3sAXe*euemlhDNq?Af>_66-5kwGkja9U#Ml$P^A7xi6;Oc>^ayL7$B zHCPH=PC7`8M=uD&%BORfco-C#()H4`mVmlmXe^vf_GKBDmU(;6bQ%UN-2UT z!{!f2$_rDxjnblukDlMZqj3^M*f=(3nyJ*u@*L)TKrP;XhT+wpzQw3Nog0@;Et|e| z(?YbZ&7SFeDx6X3BOP)T!m^oVAn6K^5(^2KKZBoU=9Jx_ba+JCRWjojOED-L~fKcGa#u-NVl|lbPV( z?~VH4Mr+tNVQq6zk&_Q+K~aW!X3x1=ehj(I{biP<4RWm8?$5#GLOvDSB88-NO9V*Z z_dyJ!mX*U5^Jk&kU~Kha-eT=Nieb^Dg#ftBvWrSv@KW|VxQyP^Ng?pRZ&U8UznCj*bRAU zZWvaFP`Ysq7p$j%GF_WbCZuCaH7lBgFZOCp&AQWb$zQhqU6Z=DB--T2uR9~L3^T~qQrdW2H0bPS(Jd{XRX8K$u z&G_4*W#!ytu19X;tJ~t42&d~RExD3I_u@b6+U5NR-k|;?2k3U~ej3k#b*}&Z?L!&T z+cfO)y)gZoHx(9auD+*LIxY(t0?# zT_4x8Vz_yZS^ncNi3xKg ziL@|JnuV{dmQ^%p*Go^YYa8p45M;_7Vze?}zRW*dD)j#t+@F+aZp2&Mqt>k<-9{Dz zms7`9Drb^R-Mcx1J#8fMWC{BQ6`cs12ND078o>YW;~QP;a}HTd0s6<|J9!p%!Htgw zYL1clf~IagjJO2~QN*LynoD*b5bC{VJ6Nn{t~Z)4K*vBB z%sRBQ#z2T3+tZ`=yxCG=LUh7OTkR>HsY_{i?+ydVQ#VO|P4xy*+{49c-+^dYUn9+G zX**v?i$(RyX|e#ynP3FzEJcEzP0w`Cxnm}&d*oSuB8rRo#Y9+N%xa`j3Qw+2uacwD zPnPX}NkbV{)hCf6^Y#OedQC*bJXbD$M?FwP-xx^RkcGT-~93kIdw!K574oiKp;~fVxAU zjFsEzdX`0d{U0FpEQSJS#Gr@&MO zs%u{aC{W(iPbp|T_*Pp=qlZ~bz_U1gZlFlw6Z{UB8gRBlAy%j|&HW+b)f&ql%TUld zdo;rUV>Cr4zFxay??x}7etf!!Q(r;R$9Br`Tzcvn0_i(lITM!`@AHn%S}b^dX5LjE z>0{XGb^bb8eZnTQKK$|i#p6M%X2HyeAR+v|xVzTCI;@Mu)e*7fyr!$r)TZw;xz;pm zaUHQ%9@|!@WxS@^aq6eZX4ONWm_lILehXANuEkKHks*>!2Us6ntx2FLBIIqHBFXPBC?Pwvhst&vVPU@Oo>2#+1QBTwdvOAin{KexY zOpA%lXI!F@*(B31vG$YDF7B>x8h z?~f1spT`I9H+yZP7i~R!UGDE`2r^@FXAo2ZyfmR3HQvUq5>Zx{9 zQGSVI)Q{czAQXVtF0@3!e6pMl@LlmpIh9m6ar!qTp9?uj>@N ztkH=@HLVSk^z0J@*2rbvJ>p9-qo`F}M*8vJo_y4n#Q%=J0RSTp6#e^+M&4M0I*U29 z6*UXb32+3^rz)(cUROocs}30VVE_b;4`q^&p@hCaa+zuRN*UoY#Boo$6G}DpO3XSd zW{M>a%I|5+7QLGz+hMykj?~!TN+cs&(0p_~86m z4b_{&!JwpnC(C{xP$y+s+qF-E&e2jkV;nJ0^l(qqSVP3jv@nbNMe>Z5D94~e!m_Cq zQ=Xr|hiG^teHj*9`YltV9c+{S0Kqd=ruPGjsT&$@_d86pL){ zd1JOTy`N;*-eT8M+jekZ@hDcdAYff*X2;}-Nx9ZVWp!5RWtodY7F z+^qCa|H?J}j7zBj`RSLXkC0oga^WD*iLmTRh76tYwF{QO4{7bK-ad&T#WQgVWg`*! zf7L``6m1u3GygN*n7g%dI)#YG+7yZwJ}Cehxy;omf>j{804-}XiXduId>tGzg&F_- z+HaroKJzZ3EeO3x7xNq4NYKZ%}Qu9Aj9A4YyqXT44#85VU+ou*xHB2Ef@~_lnSm3?^`W3^=&*moq8$r|LAVX0LVyyg*);YO?qNJ=T-FS1HfdX#45%v`Y6$HIHZX4Ef=C-X-&m2Cw72ky4d^V^lrYd3c57N@mK5$My z=H7eCJYQTLaZCzgCk|BHKm%ky{^uT9dSev2#G^p(9 zjc-W4;#-nu8qo@6b5T=}8BM`inYOa4&_ zZ|7umSqsCR3ZUivgTC7CoQpovN_pscxVCIQWi-kA7X4sFIRh09WFnpq19)%*xytPm zvUs2gpiPjHR}1mQJkT#i&SO&bH3Px7Wyo@1KK{~Kh56Fd%@09 zIMuM*Ck(LwJPa2(GC<2#)SC?M#)}d=P$lch2WmH&0)lsoWp- z(+A?Ol_!t7e~7g3)thZsS2TjJn^7zZyRCXhWyzN2jrrN{8hd)MoDe9esxV6}T86*& zc{#tY_}U;sbc14`ZpgULXLqra3PND30pKdq=G&R*F2Wbt;A>Ak1|~i!m^M;jJnYZ% z{eaOhgXoHGeG=(rloZ)Xub`P#A$EKDj?C$7Ol&B|-t#t9%Xl@bP*dTY9f3|(s?kqj zXL~K-AryC=MPi*NM5jMGI8*9@NhNPrBidJ1A`kTVX7SK*CFIpzJG<+eE>#`aQ)>R6 zS<$jtn}Rio&GjBzknJb@(W}DUeMs}oLMo-FR7UFvM~UV(f!o&E1zqoXCpwce?lNfb zbmvi2?2E7i+B+p-#(EQN!#4Vy9`NOv&~bPWWLvY^GGzwS#3tZq$~_G<+t89Sm+azf zWEs<^`)2D$FbkrY@h>d?g!eN6b^cyNW3HQNd;poGU}fpKfX=lcik%`d$nbFtS9&hH zPxt4J>BqT62OGw3FPk=1kiq-4;|m4Mgt5a(ho2DFr@GG{8eaGy!F;h{(CzGxYV#4Z z)2lChG1&xJD z=zfAVhoZenkapM(3l*b+fh^Mq|69XAC2TYw^Twe!ilq%N9y*X%@XQmp77Oz`pnN}F zLT=0(=$z;Wd!H4bQKL z<`>M&ibSJNCO6Mi=PEYw{=pWHUriR}o%`&dfGfqO<@S_Sf^;Q~d8Wtust-Ks4DMuU07FDiX@T0g8&af8=8f@J>5n-i3Clnza$&MxUuhEkk%@EmjJcXIZ zwthZ?#{&s(KB{lg#_Z=OnOl9ZrU}(xyFFDGEUizbFMeU6Js}C<^IX;*=1!@mO*xyK zIvfCu=v+ZSe^AC55^26adf}wOasYJe2aMNzVqJIZ^`=dWc!mp zl|rYNm_E%uxM-4?7Sy)Tl>7I>Hj^H+$zMMjW$oGhfle4#~AI>do4SdvIPk3Nr zVM}MtqZ1vF$NtDvH`%OT{N=+F4Q7IZ_1mBSc;L{3gJU!hF$m8`zf>o}#zMOw=Su;S~OV4G$WXS&q)4#Bqw@Rs;rG->kx*f`^^ zc#z~!W*hc3vxe;&`5w3Q+P9vnjJClO29mGl6#8n3T1Ui7A-Y6SK)(J*!$Qpv^nm!= zW+FWde$4k)kD46e-{d_UUg5d`ZF@tcq0Yt0XRjG5;kI&D9JXVVe8P8$KyT2j*=&AI zMlqT5Y8sw#U_?+jEwYpa;jeI>%io@6HG$WZ#FuNuw0PTmev}RhnRdQ;(G2w4=-!?# zS9Vt0gQNVJzejdI#RA3mH~Dofu&#)!4Kr1>kE+Dc@EYG#AuE?~DeuBcd_1rn{DGZV zoh!vxlLj(;E_7NpR5;+!Az#LH6)eW)e%NtQVWN`wT(lH`a*XUc?m`p?iIvfAlPQSSK{3hY>{o59XDZ_^zxv4TfTlw3j zcR~36$E1#<#r_y?&?mk!kMIkS=^YsZaM$j zQD@?(vtKizaIL&ZX>FG3!a2$z6v(BKwsy?*n<(+9(b=DW#X)lq;A$75Se@{QE4n6+ zws=Eu_*wXN_~-4P4DXZM_G$tHCpSL4;uE|4=`*-JWv=((***nee6DH->p-j*BDJ)d4$%NXR6Xiz77c8;4k7kEcwb~O8lyH?t zT`Vqq;tdY*bxrCyb*V*$L35D@{E930oP@~E9}p;}C6YP`YiE`EvOa>t=1iu)K6N;L z(Y~#Mgr1pvX0&1ww9Fy2mUX-kB?tWvqM}FpZy*zOh`1Ghc^0(PYTn#`0a(vov9wxs z4TEglm|TmNPHlVBFqw>4Rh=!zXyl;tYE@{k-o3n`is8+?Sh@2zQDN1a)AP>=LTf=2 zyBvRqGBYZhui={p&jk$e-?rM?Jr6zjpwjwE@M!t?MVKgwiy2wAZ3J2IeanR(u-&}5 zkbST-$+}X=Hop~cKMOtM;sG>xAz!7tfO5doCgBoWKh3JT{zV}rk6)W4&(ESPhXlR9 zNWC@>ort6RC~nA*O3yKPyPhP&QmJr%E}+d-O1zwUa+S@P-e|0}ak)xwG3_f5uUx0) zOInJ`AYkjQ-&zB|AeX+V{1u92DvQ9hIeNf>f^W~jv9pn$@OhBgxkJ+hYX|)td3rkf zE03e@U_DU`f)?lM!e|?=zImcS*HS{DnQO-~iQB}|ON8>3hq6+)H76bCu5-bx6Nc^V zy>Mwd&I}&eJ#dZJy|hKk)7VmxsFM@^Rl4=>#zNg=LT%E;lDmp8sW@%*R{vzxO~1>a~rEY z%)Anl_p_JKz|rF}Mw$`Rb=tzuc!Y&aA#3UppE ze)tqlmSSVB`O4=kWkPHE7zb@z@uQpea_PXrWr}@-{_sm(sL0jUT5OWOXK#VtOBn*m z?c6fEgpA34)`#2_U-d1LN(}bXpleZLueDi{`tjUAXCs27L~(M@aCjr52%)YNym4~* z+dns@$ULo`Jz9*)@~cWboPn$F7|wKT%!zYZ%CU$o?Zt-2-o$C@JYck##+$>Br9Di= zT1^8>2(7dYCz9bttgfu8c%A%(!~V2gzqRc@erV^t^eN_&GtUJD(nj4zuBg?_E`jye z??%7edsK?JKWA9g6U}3WoWsuTi>od+2Od)`FK}2}x@1)V5e_9Ts{ZdDg4!GJ*XlHX zxapl!M4MBpW6~ifYo7c0Gus())PqA!*e^lMoLiCW(gvwUo1yvtv_lWNSBei5^7 z{4Q5FRB}s#U^oBGitAEfRR3SU?6VQT0FtSAo6D!569X;d3@81P`?LGI7g9zuL#C~L zCA>t|b;fNA$bmhnE45^at&6bYh`Rbd=S&|vyA*Odi?R$w!3DijI#Qc@9l0`4AQQCr z=UK`Wt8{>H;Rfs;pq%iZpn_8?>ENQi%d@285J?A6z(B9gy*2f6wmeAf$=1Q*HO%z} zSi4c+bMoL-W@hHCsrbj)&pzx)ytgfqsx7w7OY6l`aT=s|9rE%f{Y3jxHsk#dKeU-B zzou{dm87ls@(;BYYLB++|Kd*6rB0&pfTUwrrG}f7Lv8(;r zgcQy|$>aRvIiF*j>g2KS$XN(JZ7~?3~A7`*h(IM*#A!9yN;WL za!9EdtUE1O`h_24k{h!93}iaHa`|x*)+l4qwtj{u@LQ>QZ5f;&+*?Z6Z9(2G#0YA%>zh0_4IgUceKOB-H+Nqz2|Z-AYH1T#aN zcUtWAY?}ZV>&dMQDe+IxFX>a4jAt@t6*!Tu&dmEMT*hFUBt3E`R7QcdculJigxNSm zPs_f>zKVovCX2Rn%50V%|s+mAj z1$zQpO)-rjSu(i!9Fl6+^D;CXtmzS=HFy(Sz0C8$c|e;qC8k%cl^2{bCe=c&=WW`l zW0*P}>q_>7ySyJ&aQZVJk~@a^4Q`Ti#~pi3ed>s`9zrx@Z?C2Y`7vyEgl_jT-m;f-_S@|vb4Uqj5Kq_HI= z*3^T7PNU6ul+0kvR*w6_i#J0V8Oc|+yU_LVUp=X7bV@(m)xxDXe!aBqPzJYsn!Lo| zoZ8W>$qK>I=YrF?F=|j@!SqWRbr)dB@8~PEr+Z>xf|}B^#YI9#-`0l)4|evK4&T<; zLW^tSn872HwpaFMN98H?>*X#M>x<6D5kp>gyu+!-yu>>>a;P+s{|<06+A~@N_1W6; zch0Pb*juDFHtxxF9uf2b5u93HS_|w(?t4Jwyno!&1_O2wUGus=l`JnzhU63ABr}jC zt8;h@qYN+Mho1W{Nsh|Si-=Up;=Z-^WAy~d=Z8#+4Rwb7`a3@AQH4J;`Ywkb(WgGA z4V2QpZ?)Y^lhL4a#F^5j;%ZF7>lwy<$ZOHh6Duot{U;DnMbqL?LdvAzzo35hq4!*P<;ocLgy%Z1^NnbeYVJeeOoI+|28^mwO zIL)YQ_Uk$A*QaJkgeRgs2N3Dq=+noYfp@=>+K=mHh6rSYe3BaDPTzLEXV|U=lB&P_ zk+`l&YS^(9AUoOlP4~${XzFD-Z?qE3Od3MfVl`*<|U6UIn zR+~||Tp`^GGfd4A2YVu;X$CT-vKm(wi$`8t4>I+E$BSbQr%Qz{h)1*X{AO3mCO+9; zB^UhJ3V)3M$#1zv^KW^1Q+Um%XhD(<6y*tdRNZ#(VXdq9j1TPa`ntu$(o6&w;BQ1? zE`Jw33WNSgD+Zx|_NfVWRz9t}fZ4O!sRm~m`8mW%qW=hYb2D}wOP{ztUPm>NPYn!B z9uK;hni}YMe`tIock{rx2*LH^JeB~`FkpJh#%Cgrk0)C*OB6mrg>O!kni!PnEvM7~ zym?ErC}TkSR~)(&B#GXPX?)p@t+$;PxErs6J;aVMahS)Zv8E+L1K0%Ua~e+k) zZ!sMZCew(zzGNspPKfeI5OBVHAFkloNB7z1)h?MPK2^jhes!3K-g8~`QzI8jj}y#x@McbnAWlJsH@5GT4L8#`v!7k_K44m%tSOj8L%Km)icF}LPZFl+b8)C!YmInL|=SFD+udo@7?B^0aY*grAQZF@L zNGO&4U8PwkSMJ?ev)gHenSN)ks@PRqMs1Ed`ew6zdH+sqH+Z3%2E*xuwa;rJ-CT3x z3s$2l|=(0+04?ZLWgZkFn@vgD&O~RTQ@^Voi9&IJFl!$ zDLngedasX!R*DhL>wMrtny9qROdnQS*EGmL{uC_5JTn53>T&r?+KBvmE->|?^YDh_ zZlm*B6MT1hct-^z^gB-CMx`fcDcoqx+3N%vhIQP1HF0w*^LWSEFnrIZ=2vIUYvvVy zH8jUq6OjO}%_V+|D;}*;!XM;}of=#bL>*`>mAhH;Y5=H)5i5^@$>e-C?K!Dlez3TqVApH7!iK~oV-P$z=Q;aP+w4zGD9dSKgH zuIp9vkqR1r;C*K8KiV%JK|gc_ZamUKUn@+rgXpD(_a~U$iV$0S<%cV{)&|P@Tm;8b zASNhg+SW1-Ll7*d36QQ%teML~CU72$d>v$04gG-WrG~%=ZD6W8;uXxtCDOFhLhHa~r9rO-;)fK z1QjRz8$U@#W3OMccCv_{&b~1C1xv@w#s%!!c2l){reItY`(P)6h#2WJgsd6j;_(J# ztt*HeAU>~03h9>*N=aoY^+{C))BtBw+BWsoE$ikF?zkyrIt-nyLbyb=f04$dQ4iut z5sEfoK(6f^`?lte1kqiwEgtBuJ5x&0-CMH8rVz}ZXnwEG1K9%f?nTsXVkl8nTEe@y z{PyT-+tm0dyx$R=;B#6oE4iQ;pkHJz;&k$WGB3;s6@4F%9SgO$RXecglGBsQW=)mu z#-(2wDAL}#Q%c#V_oj&Rh{c=sb={R`{!t6444Z!+bwSCTwPg(DLrB9>f^HGoLm4&^ zYczqC*IM-iQ%hz$+7IuAj*Kro_RMr$0r>OKN5{6pA&W+lK!*S+rn~7{?_EFc&)`Pu z(PTCs#4v{#d`hg`$s#WTT#d0QL`$0C4U61)HQ~ixz^izVZ`bbw3!3vRGsI1-MqO>; zL9~ZUPj)9-E}4~9Hc9j+UYF|MXY#fmgdW3+nGW=FEY+3@%`Q^ecN;_ctVt>}OR<}W z@y(~&j#v&8?zCRuS$eE#*9-}#Cf1~iG3wjN1gic~87MU5^_W!Y5ntCSHsmGIo8-Fh z*y*|}VUS>-OrJqy&i95Lv(r=@>uZx12U#9+>zx?>O3xW4A{!JjFGW$1B0((K zDHBZE_cGX++~@2xl@hmBh`j%S8ZG=;HG8q!UCY-2Zm7vv+WE>v1-6a6Y$SGg`Di%1BS>0UjRtx)eU0%&xR4PJCa=E=EKlAo&NkxijN0_c3dO(m~7$iO8dKk552F~NcNX#X%z$=4CxA)rAf+T)d)YRz?I!%lMLK*V1?^;r*yIQ zH;MKW-eYcljj+~OWaoAocct=yFW8#95$Y&EB1zb;mse?P4iZkw=Z;MmbWwG>9Ec&} zX!+b%sBi2SeLbywlMl0&{isLPJrtDtx}|42SX$=CO996K7%a^ISB;PJ=}=g2FLQxX z=|~k7gI3AlI=t9&=0^w1 zCgS7t+md6ie>6#W7O!Zf%?7?2yt*DLsXrvr`!_E`OL+v4?SW183gXH>AlxIC<|RaB z^LQS!JNO*eFziS>DwXM*6ICHmgpnOD{OA>FZqWYm02Pk4?=7M4>%x*Ym|?Dyn9)u} zXSKS6f{4kU!s^HiO+DgM!n;+2NpEDMpY(`nl78U?GCaINsr;g1kIOUNWZBue)odr& zWf`VPQTE|n?-MZ(YW{vBSqLt%qUN2QQe(_Ks+(SuIP$)k`KI-F#G8|GI?OV&q7fH7?a2Rb`fcAXCZ7iovy&sRcTYiO7o4IYpt|lhSeV(QCf5MS~ve?!r3_ zF#1X~d>%vH^^Q|M_K;&Dr`{ti=%e&H~WJ6LiQrQI6&tjNZ(0j z_nzNNu-iVL4qIE27@fkMMOQo(v=CT{ z>aTs|G*Meh0KdD)@e@w~g*6MRjD3vMY?JaPB-OEJbnmQkUOSh{-@$_4A{?n%v^o7f7xWZa9$pk3rLdc&#^dRfH> zhZm6ceyN`IKIh5`Mf^mHWgoY$5p4z*GD~sHq*R+xD>>~QO(b89w{Rm^PdDRAD_i99 z5>!$5L3L`BR2b&qQy0tZvV8{ulHO^;U1@0A-rJhp-JsbleJ^l53GnieGZ=>=6nBkT z(w#vlF9z0C6ka?VRncg06=X2(eS*S*KajWcm>G*zSfelOeH0C= z5IP9QNSHMV&gozgq<|^6Ynrf?`1XC}9~LM%Kjws{y_%5_Pu!+HR^3-TS2Z1Ve*P_> zhT|IwY=23GvzntDR`ZPtfx~xC*U{yZOj_+|eieKXtWa#j-zT0X9jx)X3*ucGCB7ctM^#%eG9D8--eu#vUo|a88?XuKN0P9s!OXJ@4(T#@3dXBH2j8d35 z^sw!Vlf11QocHOR6&-7i;MTDU-0+$6bBV;nZ0;@;8uMT_xBp~IAgVB+6jXSftw{D5 z7%s@k;8nCoY`VW4RzsLHTrq05RF*8$BXfU23$ReMQsiP;$#o7}w{B+_W*$8}=wu zKW;X1KMu40Jz62D=jJ>2_#0PTy+#&S&RxL|LpAEEO=grJ!1lV9Be=lWuXS`V&~2q) zUa_y$7<`bjoP1x(+G;Go2JEBjmQhu9Ny=xXFiFcEf1JqXtdd+B+rpZtFd30H zBQrnczq}uzFR*E(d}9eB9lLrID~576$vg4HKywePZZL^WH7mWVk>#i3-!c76prT)f4n=2L%2fp)Reg0Hjr*HUQ#q!euramVgI5p&2Dnb@X(XG=0wND z^N8=w*V_uWP9|8GRHF}XgWIf(YAWB&F`s1z1A`P*M2U3qX5c%%x9xTF^6!rs!8aGn zl_>j&b3=)^*1CpJR4&uwiWrO@`-ulKlG>Ovh%RFUOc(_7EtfIGdQOfMo4me%)brH# z>xLO27YoJEQ#b!H&s@_nL#g4Z>BL=Og;aB9*Wvk-Q{4dxgru;uZjT$gov6-k(BUGM z`w+4*)0cN0q1$F=9FdH4^bkNi1%ph_ehTLy1%t(4wlRe=RRQn`04uR_Ch*}C`{!e zhR)<>*C-nJ;h-sOk9?gef~fo|*3>pF{yUxDCAwb`LV?okL!{A;IE{`gt$yC|Uz&o8`ap+pd4I;C>HOlc&mO z(W##8hONA%R+-D*b_Gw^Zy;`Cj0DgNoJVzv2~7{UzU==bB*=K>Gqe=D7cWFos<6t# zIjH!&tke+{j)+*f#)=?FWKfjpo6%B>9BTKFJb^W~X{#A@_L8scDUu2J|DPcm^I|0EL#?Vg0lkc6YA@7d^h6?E|NKq#yMZ6F}8X=A7a*i#YL4| z{u1LJa}~|uQC@Q}1r^&Irl6jOIcRz*eH!RjpJ17>muFzFW^UWx`l~T+6Ktn?fl%1@ z>W9}wEu;KXRH?{=PPRfjTxMD?p7_a^t>Rx3Y+sC5YW1$sL7?ez_ocM~&Mn9n&Uap;cw!s9LPehOsqEIPnjHRE(MA z0E7A8Uy#rYnK`WZt=yz0(Twhqh`g|#4{pmL8R;9mnhq6+Ub3$twe~0|n>vX^IH=m= z%6=B`y0OTgkCcQOEk4jtAr=)w)6jkX`t@<9kwn{iX_T{`e9CyokZ6Vr6gQvy>&c%@O z(dWtw12(skyfjtiWv}=kZj>5jbA(Ar^EB@DCzmhx@-Wbdh+$<+So;kNXFXVpy^>g> zF*;~Dtw`Pt=WPQ@<+XtqXgS|Cj8?Y8bif&I%fxDKH|p@7W)mBI8Q&W|Utr$u60xke zy5)miFtV268z*cxyzW`slm+rV9aV&W*+tgEOY;T~=afJxP!pUNpSjIN z#v*XdEZi!sDAk$@6^4>Pq8;3EB!Z3?kV}tQ$ra^NSdUXqu);($f<;z!R z_v=wU>kbZ*dK)Uerrduu(jQ)9)HX$m)9n6PXQ2Re=%b@RZuN7=n%MgG zJgrn*#o|WzU`3pyUYy&zFZ2D_i*)1BkrOAGSsbQ=LxkfV^G^MLRDE?+RBiY6h$sju z0!oRXq<}O-NU6Zkpd#JfAnnkisKn61&`3&mH`2_|HKf#lq|8u5euwAxKKgv;kAYz= z);j0h``&Tw>)LlA`ej+Drw@`s3njc)hJNrNPUpj2qtDcvOTwb^Df=2fC`7zGh17VN z`5kpOVH*tfng?%8W^V5LDE=|O#4pq!!facPpMpJdWK*P-h~2N^sl3{_OoG9;>NK}R zxRMXP`A-d;h!m;brWYv~@@b)N{X_)-2mI&-S-LP0-^CgTP3>~Z#B4?_&+gX=InkTn z)Pc-HFUq^ox}48Y>^vP!5B?xWQNOMSn@a6A;z2cJ7GnFGgT0lnP$=0!NWJAMlSBB*D#u-E@0DI4K` zYp1k3R4lut*Db)eVp_qPv08G`kDmq>H4JT>j(j&C`N)j*EqNPSVAD7BggW1@&&Q1c zve85+s0=PLaYrzcNRpMSZCfSO3+yl*`Ytun6dI|?4mwF$b;@sEEkMa93@mJHW!+1H ztjJ}MJf1|1&#XkYcF`xM2PW`#Us91 zg07#R8-#Qz`HXZsOluWcqE#mfi;L}=8xTXAK*9I#WdW*b+k%0%@cA#+j*MalzcW>r zHG7FllHM#Dx#Mex;$bjKnH9EUK?&{Mtx|!zw#>X6f${d+?d>UuSxO`dICeR|v2jpV zVAPtSeD4gn%BW2$>%H0mUoJGDd>O4VQ`1opv@}oU!-`k#y~eS#z!RhFZk7p!*y1G( z?DwSb3g|$}$5W)rc^h6TnZ333TYR6+7V^XQnk6vdVs)ihYZRXmo+sBF?Xrb0!fR9&g~$oIBz7OV6%<{}|GR(Y5Gy2LfS*GIJGUHI$t6-Yi0&$?uE;n< z(upzhn+;T2{zY?*uDA4_oZ%5dO71Da@Z20k!p=r~%1i zC@k>}3BM+ni1%6vx;MWlw5N&a8Y5iFx=88vChefv)#_P+aEXKOEKJk9x&p3`ELK!! zt$l)euFI_(ziaGCk@I7JpnM^U zaCy6|N*X_1--Mbp44E*-8Mx=7&4b_tkC9?K*54&zTyiG4%rK^}@jB-#H`JH{kZ)>C z9E$|iL?wwqc5yt`-5M{}HhB4u}di5K<0#2-E< zvX=XSt@2yHlh@ee__I!$W+VvPII>fm-MZ$vb~1Wa2~?m|@y5iT#&5TCa~H8iQ}D@~ zufLFcoWTD(CUP+3FnRHS<)Z^B+p%SW(YU*npH@;Xhf5lG{;be3Lw0eae5d>kvC={0 zRIcK3si~~#;zKheS#`{tOpg~`?^t;U8hiM)~oZh>lFm2 zpM@%7Liq14WYvf;vVF5kWssB7d3xq zi0}NjZI=s(b1ls-wB-Un8qB&jT9inzmm8SMUAI!0DSl;X#iVuLc-Xt%$B)=sNhCFQ z#YQmk@M_M6g-&$;x-qF4nU8B}M6Z~rsCg(ngsVvyNMlLuA}v#(8qtl4o7-WgNkdJ8 z6EuxZkh`lE56rnyng)zL=8&kiHT&RK-y)Ygmbh*5q%~Nd+$Q){N*rd*rPBUJzF8+t zTR#vgq2al6wXqp@Al6h;_rvd8-=uGj-l}o>x4HwdqMV!GtGDvwWbCPD}CecD~;&&{GzZ0HRnD6 ziR8S>g%%@cOTu?8nqmZe#JxH z`Xq(mp*6cGBc|LO-3>bD?i%UX@aWQvt}vT~L>qgMHszdTi z)jOG8QYs4jYOQ{&@wc#myt7OB-E+vR3OBt9xS`zx+dQ`_Yph8Te+z^No~tdKVMdbH z4w_^%gfgbHhbTO0{44x*_Cl|fu7Pwv873w@wtrZ}}l zaf{lSZp-4<;aqku$b_Qi5$3#6cVOxwxCbqL{m0)M8z=bN^#zE~->1v{>}Hfw2Vb}p zhu?dH-{!CXs(#*dFmPKl{bMgXF>9i=y)FjtspY{7m&dswFv2Q19`zN|nasNOP{rF+ z617hwMebG(DLQ4>zG$nV>H0XPJysQ4=-!&S|DRQ4(Vbe{t-)Bjy1&i_s^0{##Ka)$UnItmgXvMA4nUf-#HWITv~ z?m%x|R0Q~Bd}ALSpOy?vfs6U>c2HWscpXXvGvbKJcoj;OdKWbPDR;S_&z9dH7%RK6!AfV(tU@x-1$5r{(atD!U_jB z>G(hC83^5S6BGxedM}=eSJ@TZjq>$jW2Sz)XWLY`ZM!O{dO%LPH*6i&?MpY> z+{Sfy@`NM#RxEA(Pdu1lfEy6p0DcG+FBjXBC|Vu~=p5lEXH2`iGWO<JS zD0Sij!n-WUaxr~kTAq3v9%GQN!svA{Mjd;1GfF*!L8JKrq)w|_qE3R~2okeMdb67u z2<9s^t%6V8H?0uwJTynV6Ev9yB(Md*JIhJ&RUnT--`KfK0&b4uQ$s`6g5E~s3%s+o zd~p5E!8UN(A2i?+pPbBAf(r0QT~jIQLqNAxfnRWb?>P?9-FDp$v6WfI;!> zeTWzlv{HWK7 zy)M5G0Wc-VZ0%3vtQ{RL$cMZ)l}5Tudk*DU=;Z)j3# zw}v;Y_aMY>pxbX94D%W;xGyAOHBFxvKjEc(Mh|na(mS&(55M&eXP*BsJ$|=p-t|^P*vfZxc7BOQ6Z+%pu1jDE0&|;DI03Q{+zlY z-O z0qg_H4&fgs!y3ceO@LneL$*?TjoJ8M?Eur~+w{XV@<}N7MV1GAsq$rsV;oEyR;y8c zN|A(Bid940o;Vp^JkZs}q&uiD7l_!izkXxzf1~k$yWzQ-_O{3;sr!_=*5nmifEC)0 z!Ak%wZx0{r1KoS?Z@=gRm(wb~scNxy_%N-AChUPI4dbK@;Az7ii09QZP~QuL(D<;3 z_?~|3R9+O%As719zDu4kz%=|_g0w~u8jq^Z*Qtu+M|BXBUAehpb;`smJ}0Cz2Y)?a zdnh3ywc>NLSwBgZho8&y)?Yqc3(;K%`)>aVpTS`|5fVT7>9SYs#tB(rWH#_SJWBW{ z*MA@fH^b3}9|L6siKM{Q)BW$S^4_}YR|4)-&#L9JhDM!AV`_j&*Q5snFJ&cvIbo$= ztDdDCpa0@0hRTqIs^U*~$A1U{wJ_k1oOEu4J2LdazouV|TO>Eve*9Z98nOK?WCX5s z8U+tN>i?|LL;u`6l=v&!d!Oj^^v1V@sL(gJNLO=Y=xbfjc^ag_@SpnOa*wCXj(xJ= z9Je&I9lANi&J42FFzI6_QkW~BQ|I~&rElhPwle~IR2~(sN9LG@3 zU{9upNa~U+8llS~{@Jm3%42q&DAY!W_`m_EwkX|Tk>6PrVg%8*~>x>;|Gsp z*>+!B_ivN=7C1Epv$fkMu_P%QYrBiMTE^;KB>jDJ0IykF(_r(n24{;HmekD6^U3ya zq~E8cWB)7H_ltWqws?N1<}>*685d>JwTRx5@+!IZ; zXi@(irH_wG9k0}#D|A-qgc?dNvV);*``DBKQyDs7M7MYQ+Xa&Lv6V} z4Om4vrnSwO$9FeUq~Lc4qPgPCpg$p8RttOP?cd*sG;UYW49L2X*ru8}g9?ft+f*!p ziLU-yj@5M-({mg9%9}N`-INu{w9S-o$)rNRC-Bv0Tw8o^==CfkpSS2Gj7by{^#MJ# z4D=F4Ycglpz?M0ro7*<{x|tHQ)J->d_Do-*m&iP0i)z!~sxh_%yYKvDWg!|ZOU;WV zZeZq||4+6;^v406UI8J-t(z1zx}-}4j8*0eO~=D_=;^QbCtW&fjraJ|2xgPpO#KZ9 zm51%2uvov|?L>7rbj`z878|50c6Uih9R@CNDMTzGq*C8KMtg)ZSt3TaDs2s-N8Q+7 zy!NjkhzDwH2l##IbGe_eV40ikBoaLuy2%hD~OPBWp~ETV64($g%hn@j=f6?o%z z3aUn+Q$D78xEq-R-BR)KSrtFr8}fquueE~C|20(*D7dXq5r2BR>Br{?8J_CrG<{L$ zg#n+de`i$sd0!AxJaTOvz72J?GP`YiKmm`wm2*uOL(a6xIwgIe?C70=PR|6WAl3^X zE|H!=D{do{A3OF1<~bwhBs({E-BCJR2%yDmUM=^1#nU43%y7Jw9j{h?kKAHDkpqiS z>?KQ`Kz8HN6;A7WE0ny;J%CWK)&#u^;d;sZe2G)7S$=Ck?d-+BH`{XjpV4`b@p)Qr zzLcYE5KB9_!67d5)zD$>~<-K7!E>X$%BO2fnEz6qH zL63})znI_*P7s2o32sMmFa7IFKKGUoxyx$kF5kT_0afPyyBQ3SP}N0nLh|x0^s(8S zP=Vt}ly~o=Nuqt~*Hzf*^AF%kWh_XKwIdY4Gk~es44wVm&?528Qf&hp)cK!V|NmM$ zJed*QWKS|=-YN^eZ1Kc?u=F|LxN_Z!xVdMiPWKeeO+1!++J~ufs}c*a7vPH$vt)uU z-JeUV{ndHjv;n0YJS4{mn>%Sbw3gT%l;Yi2Y4AcTDZSLtuo8HBKdyWIondlf+giwP z^ta^?kkIi~LKe}OV6z6%@tLR@-|4M{@XspyUy6Y9toaFU6Cd_v=gtJfFuTN?gxgEVI9G7QOZ|v(CFa+< zm{U>%#w#A6KleIB&xigGtmf$;IXk?d`5^|}Kd_oSkJ9+-!ULKrL7%E4VcyB}iFfiA z7Jn#|K^mF$NuxQbs$Tc~z_#?ihlg-CNn;PH8^zHU(crUad8Ft$J=}wJN2ob9iFfMp zIds2`dAL2u>nH0rNTy( zQMhVhRK5gmhUY}peG1XwCf$@|RAZV;QZXTZja*=qHE9}|fllc2emzWh=cxQuF5@cN z4D%UH8Rd8Acu@!}5gJ$`&oy$IEk#3Cs+4hrfPE>A@0zRZfW zeRG$*XQ%$js>G`y7VMKbP^JH5)@p5O;u{3dL?qr#eP6MQefZ~$`8l)ov{Aj5wZZ;I4tR=Bw`k{We5e(DqS{7lgrFR5hc@SEqCdR3x~Mh8`rcD=&$NxR=T#%VB2pf4o{z+&w4BTpd}=R~$D9#ao#Rv5 zWp8?TKDS?{toB~u*FF8;Ar$b-pFr{XRLPy}vR09j4yI!ZP|98-)vSfLQ6@9k&=1i4 zepH}tM*X6+pgTrbD1qke*Xw|@u6pXo^BWEtr*mHl9y_>cS1x=`FYtZidw6&tBKi~e z62LyBzMJTg3MhD@!i3%?*jvcH59=-Vn)6goMqbkFpwrWHG+0*Uoacd0W11>f2SfHr zbC-UXf3w+#dGx-{pj@&CFUfnF`$W{6neFi=-H6T(t7_WVt_S;V51fl2k@Ag6Pm{mV zex+%wd2rP+q$I&kXd2ak-iwdsMI8>juwWXEbm1qPU{ry4Mf=>m7igQR@>JP3;S8ar{N0o*nA2U-Hitc?FKZq%)b+d{5a-+vF(eS9la8NAr z5{e>^Y75PGQaXV5cLUnW&wgx>)hph|E(0AvW(Y5++zbga8k@OU@n&n{9`t;y#_25f zeeS$)8GpL3q0*+x!OQs0r!T~t+54^9rWY;jDY-@UUYs>|@{kxkQdlD6k!JX%0w?987pSdxuoFokPTUr2egmV&SN5U~!=tH0C4m5FUWo>3 zs(bIs=9dxdX)$LNrMZqxVn}tk4p+E=Lv6*X_*c&V6SN^#gE_cDpc4a){D2(*dBZap zHRzevM_&@=mISbQlyVFHHKTXLB3egplU7icDM2Wax6jC@GIC=fu3d#~H9~0gw`=iNglvV&t9R=S&a8}QeVb=lS zMeXHw^=L+exjWVd49$ybPBHKwpKn|5NlQ_ z5aJ6+=|?<$ROEr7S1J@MXG-?B38@+SXzcYG@ zlb1ZYDtQO1k4}u1Jj!CvXAY$Txu)az=}&-E%x(kjX9P<+1|8RkKDJ$lJ~Is9+K1e1eX@3MHJX<`XQL(4Ct}jho>f?o4d*L zXN|lU)4(B^<%GwLXjT=QFxIm0dx6vUE1kxSk`hYadCELMK~RdF6pky}`1&8E zl{BVVg>ccQ&uIeXk*Y&4mE@Gg8S@;+v++{kn<8bh-Ld~+p9856=mA_)8U%`ds!XDT z^U5VLCU;?hj@Ovc`wBXxjY(jJ!aLiYE)u%v{KT^7OkK?k)uAwfDc&er31=-A7fe-k zlR9=HY6myBGd7br^Y&~%ffz_*pDD~%>F92cd%4cKYZ1Jjpkyv(ske083FVq#Mu!0u zT^t2=HLBTo?x<6!L(im4HrBnuyac|)8rF3>%zYjOs8`ffC9~L~?uDIQNS>!&aHDrF z8vV!^d+L1;l6fWf=Fe?Ijhu)wPDVx`b#IH{33jgD6x6}CIkO-AON1O^O*}0gd&+vo z@4q|F9^S4!VQ-U;YP)C8XKQmBgC~eY{Z& z8?vQ4J<0%3T2P4?0E!UyWD8*KfhG;k6nt-?O1=C=@0;m%1((FGo-2ehT`V=9<-mwMAM3aRL4GYXP{pS4; z3COXj&Y=-NOSM)8({%|m!rCmN&hlts0$k9&p#fW8G3a^^E7&vfIPIV*Sj3b`#EJQvmqQKcq>i zTQKtxo|h=^HLj&Nx8k5&m@!iwbw3nDb#%}ZH-22SYlwzFub%ojMSHD$4&VH>oZyPr z2{D^bt(^=SA@ufQX;{PY}qSWo0=I4f3*aUuY6RIItLs1e6UrrSdk-h9xB`nf`DI^pb6)YVs{E>r)=Pv=j@t8JR*z4aMR2AC-g z)1lZ@Ul#*P)LGybp%9k*pGkmO?R5LMsWb0)RgV;_gX2(LfOQ5^bI2D$Sidk zS4X+)x?c)dqMr-tnST5qMf$%f+F(Z@WfEzm-!AtB39rm$Fjz^)zXSH;IsHWFlJ;x6 zfY85n|D9o5@BEl$BaFy%D4o%b+^CT%)F{Np&u1@#e#HA>Zg>hE@@%{D@IH9{@L(sT zb}UIS>MrEqmLn6i7B{yndg-Uh)W#v)S-6^fVtgO5Aa#F%`lZ>+#%io`1)0{=^i2daWXUs?#Ma7!cy%l6tsRbXp8{f}sqPlh!eALO&_Wt{ka->apa9 zf9Ua>5`#A4I>D(X3G~uIzubAfV(e~|^*6%uIs7uINs^1j)dsFU{-edWBl@ewzhqH% zD3m!Ud%Q1sW%l({r;GEE7Kx($K}bZBu&*9AP`cz*0}0J4B*A9W)b~=)gDoYX4+Ye% zKwZ80Y=mQDCcfcp%W*`{WgpBdDw+kMxyE<`SqN@qgBk3#+H%RA$KVbePY&R_+gcg6 z)lJ{;1JoF{D1P)v&(%)Nui0=W#U%|h#rm>vfpFFwWjXx#_*F;#;qGlw*DBMMV&4p1 zz2UONy%*KjejaA8R3GIsJ7f+O-2a?3QNfnS82waz=LUqvP505c-IObj!@J-=Kmst0 z2e|G;nTKvWh8f|=pSX9KBA4Wi@e|V|bRaU{aBY;$cd&`s%6;v1vc}qBCUbk8LN;?p zpM=^cNkV?e-tG#8WI+b|;+N9^PPR|>44bL@?Mp# z!Y?9r0De|eyY;(3gaD<=&zB4Bf1hVrm{KqHhsNVy_&0HS83TGTTVZoFmuOY{{^`f@ zdU)KtM{AQK7iBPolh5KwmX_!BwT*gZ{Z{knd*~1#l)*3nqAP>l_Go5Bhgz3UPnchy z!^EDL7_n1uE_T_;c-B3i>hQ~rknku|; zfoi;12+CT$TwCg-JOpX)Tb{TQE+QpS|FB*lMAbUQC8GMlK?0Y{Z3~0OSAVtd|CufI ztQ7KqHEHJoeP>^&q!6FA$UVhi^^U+XrtV$_4mDS~hlOMfiIIa~yn4 z6MP;!5^qw(@=$BvZ!Kg|97B_5o-d0-F|G*8=A(N^J^Eevn~x?}cDb`$vpS)a*bw^D zYe?b&i5#{NhR*OVOGb4>f4^MDJWHO9CYE#lSedu@?W-kbHvz$UIs3u+Sd^=6sMm?{a`XE5~%69R((Gv zKlNn*iW6u!#E*S$2R18wqO?h&O@BC6n{yw+HE2RmW)h zQ@)zxTw;&&f&r-9xTI$I-V>CllCO^o#>>rCBgvx+JpBD_$6Un}zd2tv;9E}1GAkr> zvApHYn!?D@F1sFfhrnFYRWGynd%kU>X1eT4G(7???kxuDg_tVE$K*h(`Ab7gEOSso zUvi~)M8UI{F5pLv%FT5F#-Cuxe|l>mD?FtOgAy4HVVVaZ&?UK#PAH)AP5kFEMoX7o zIkM2=Q^nn{m)%uIK-lVOtE3$!;1I>MOGhK0M2Z;798E0nO3o9-H+0?Y;60o@q<}fJ zJGlvFydvc<>ZF+tT@i+2f>^=yxHVamvW4p1q69E1;oatLmjEh~E~@aCnFPS!O*(TH z1s|z*gmw$sti{hH+Kvi8dN{CGXkpe^Yk;sQ=#GqbdPOP|Gd7V}kjF3bqvLeV{!a5vx)BQtrPlKwPD zBOReGO5&Y{^<8wrB^i;KbivG8uR?y-s0-A~8#^?v#KJXc?62uLB8~PI0pBXXh2zXP z3tYMo2-yFP7#Xl~-E>j@%;C6Jn{R9B^x=z(1L>5jv71c(TB){Wif`?$okxu;YF7{v zo&3=4Eg_QcEK z5Quu>_W!3Xmw^$uev*KmQ4Nx%4hflaXcimY7rNr#8dm%$++&RZLye8kJWq0$2fDxk z-`-)BFiuNRZDwv>>(nQ8XO~r?b;Zk zXsGV0J75!C-$40Q|28>_veJKim5REEqs$f|Az10DGF??iG1g4%HYKuq>CyX2;@99hKlTl*6stFO)i^%IF0gk?xW?5|HM;uE zm-JMpy^yN~qTM~sCXt-PD@t&?{idNHeky-B^H|MrXw4v^)r4G~)!aM=!RRLtPopo%>Hf8L`k}7I zd2(O!z1ZZ~Tb~I|(33d0KFK$VuC=Cx+|-i=mwT%6O40?CBo2DR>#ay zH1Y3Hs6UBY%Q#yb2Dd^yNmTaenA05#y8^Pe0Dq#+M#R}g3JQ_7Y_b@hF2)JyJVmFG zJ_+OJL$bmY1A6kD+L0M-(V7>fnzj`fRUVZc4PjW+)}n@V?B$dOA?utcrrXe2@q~BJ zT`Ufjlr%`cpIghFw{&Ml2qOlIH9jx}4&B=A;P!qsqNjPwVK|{73sHxJG^=u<3oGGt zg)$mlU>k#cloUbvwN#dks%pm#O&%mjHF8%T&IBu?5gmSYaOR*ho3fd>Xm{`u*@vpR zjD&@!)5ewA#HgMbquN6E0X-C4wP4MXBU>!5~Y&7~C>+L0!`zt9EJ3!|FD3xy~nz zrr;oQF9AbEKp`{IAoh0TZZ`A-1<=b!tEKu%XV9ZK6EmvpYHPJ~2*y1ptAmO@pZ<7! zVZAo8UiCOTdjwJFc_ntveX{V$F`ng?N{F^^JO#gPhKJW9{n1;_Cz)OY)92jwPGuI| zNb5;~k;Max6zUrbv}&u6zPxKYtb89TBASQUyp`tVNzOhWgv5I~bylVQuOae(zUJRo zkaou|kjHw&D_p>Gr4K+Qm|6)B-baio`=@$hdZ-KmIE$S{gwe{$>;X@zyxHq<)V^M^ zeDAAVQaikf?|BWK%2Dl!#X_DDRo+)Gl+x?fW;trM**L8hW_mTFMrEqY=7Lm}XwqbI zHZrXgu(0}ycSxjXn;?hZBkl$@>olwb>BNhJ@5HYp4O}Q{rb!0vs-RwDq9NPf{RNh) zKizC)8%Dw9W7j%;5C!QHt55{B2bQfeH|t&dsJ#~$$t|noqC30E`xUOi&FL4@snOy) zKKu@rf_UT0KpuB+DopP8@(6j*kB*d;pjJyIE!cXRY z0lH*dIeqPt6V)_yt3==H35sSF2(Z@_q7d`k{Ek+a=NVojiSFG@LBXi+F~WW@#b*Pw zCr_ol)@A=l`BWu9KCR5lo-M;N>>%89vm-?^9G)vlS^gz!->W#jlhtegH){j9uF9mpI0oYO1TK>o7DXJBo_OiOwN}N=`E#E z%M?N$nyPd#qKyR!u*#)GfSafMIGm=zHiIi+Wea$ z{iXH&L+dVj%Ms+d5DL=z7_qq-lQ7_S?!p_u{5JX@p|hH*1n_Kt*}8NkdsqS35&pEp zo6xegl$SgAhu3h+#ORm9zZmb8d$NQpYoz@g>Y{_?3cZZ1fduwcDUP#DaHejXmj~Yg z$6fC&>Mtl!=3tFw&%ZXhbJF%ga31gECS}KWb;#;K(!A|>xx=G?Uy~A=xZGL)tPpMS z_ssm2RpGl7W2ZEUUKqi{9s4z^0;YJk@}$%+RoKGf5{(w}o^8)<}0eZ+( zKtSJCRxY8Kj*7WANqfcrR%;h;_HA`yfCJEHhzn+^FYVa@n}r)zrbH*{ut%$&ZnFyH zkXf%DOgl`h9U&*X)PIBq!%8A1bhl#?{uu@Gni1yudqY;@m{eJA5lr)(OsR2L;E@xH zZK^>$AFe^f4f7^c=t9!#uyVdGvdQbb5N~B=PGprfY?T7-Js^XSNJ{#luH0YiGVQRM zR35k0jO`_R1-5En*Lrs?<|Te>=KLYn+Y>B926|4GY4%1u+s_R?ag&7b_XGn$w%IfP z6-9>0PtfER2VJQv2tM)|l zEPM<%4ecrTH0o^oyW+0F$uZc8e%wBP-mah#_rE*-=W@Xx@PX&r9dzhY*z`kTo*dEZ zJ0Cc&g_Mm`Zm@VX*W^R@j$V;JPu<|}s@t&tHmt+kwC_u%tDas?Vdp-hA>kBs z$iL{^gAMAVNg=grQugDVop{y9{6SK(M8(K9F1ZC~sTnJD#Lk|4>+J`5!ng{gIw8f$ z#>S+pxan4%kP>YsCb>c0{Z^}9ZwxEtU1AbP<-s-0q&;=nxUcoo3jaQ4OV2Kz)o#a? z<-u+NH^LFd*wmCe`Ygn8yLitYhv`0?WNS`sXXgKao0;hEBau9MhE?7;?)i!)*W!7v z*i>^O6HRbPZxn@;?%`+gkpCY5A-a=(VD5xyGyHhBOluAmeMZG zlhM9EqAFpJapg0QJ3sULeF)~NwO4H~v|*&(1Q$vb{xXm&-JiIaGMrF?;^fn@FO!+k zY0qPr)cZt>d1c!|u*ArdLe`-Ep;2+Ce48<|dU_(9&g@kjf{%$UkB}=qt`jq7eK6O$ zp`(k+=S6Anlqyn4E;1K6nrS|%5ibKDj5YNI8(GhOBKsMHeH~VbW=DNG-5cfoC~5u? zl>$4TvSwWu05^ZaIUNW-YECxoXfxI-l)vDoJ@atgkkY&H?{f-^NVLC%yFJ%&qMQk! zW{`jKNG3fsifX2lnsa_SZY`&)mOYwT6vV0d!r?0ALE__mqk}5>`%dLD&GG@!rol5! zHTtY!%W}v(wz;os6kqHjd{8Tll|A>x-WL^3B*Yo|=Jw!lf!X|7v;DO;ONau!{?LzF zZ+<_DUgN@|W>pjQKDsEs{RX>>uI)H zQ6&lOp-lA~Vrow#eO{9=8O7vRv5|P5GdU-AQvPLk}sf`}CxKJ!|ZN z@t%|=rD2gBrPJtI#~Rnj7GvY2Ox`5ZaT;pT!^`EFfyuL$+vj&mi?8I$SlnH`jcI&h z_>lI51tvncz{e@-`IF_iI_(ZO`3Z0M3rDft=gRlF?qE3E(NRiw*3+nIRbDB|1=09? zm>|>mGu|oo#eYT&<5LOr0S3A0q!N{fQD%Z3{eBfq8C5QQutk*<0*&%M+44|ce`;mI zA%3lD_K>~;U#{-8_OFwii{(_opde@tJaT`&B84fSU~>JyTd7}%n_1`DCYk-&mXe6O z`kd|c6K6$|(58V(ieyBdRcGy~lkVy_HqJ~pDNJ&U$~}h$9~+CeBX)w+AqaHCgxn&9 zg@HwKqsLut&D&49wQ8b-xOJXqL?15i+x{Ri#3)?Rxfet`e z_(d9`tNzZ2RUy$-=7^b%HNX9-0|n;Gmmq3a_WU+y2|bO`eG-l zx7gZsN$(4(=;}FNMVFkfjz3)`4x&U4QMU=ZW3F1(v>)D6$4?|sf0>~34&%GMhjn&` zom912)Q~b47IxS^@#I1i7g4G?8P*ZZ94s5IW*59uL5Lpv2O3~vi&?^aTy}KNBd{Jq ztIdh2j)%{Gu|LI`tK{O6-!0v-ff z+|fxI-I;|jHDm~Y-E~9h7jP#OOjM#rGV?T)&3IWwa+LYE!Xd;XOharEX6=SYSih#A zhgb!PlIWs|7X8)mbfzkk1nyj-+%%f|iAAW3}FH2je&p~n8s*F;h|+(zUR z7tK_P^-lH#xSa%2Gbt*}+)f}_8GNRva3*P5*@etX`u_Q3Oc_3jvf+u?- zLw8NRg@%n%J-b@aR)O^`?Kl3-b|%CA`>#I*rtwyDZQ+kUmUn%T@>I{FmqI?uFe7UD zm{0SM{T_sof}V+4;3Y9pI_Q_2TC=pAtKztu9rcUB&8l=W&GR1@Q4EsIXj&zgylC6A ztYz-%+XrUQ*4FPD>e0SEKpXtE?8k@vHhr`l(gJRyUhS*c=09bosm1ZZi zM`K3#;j;`WlB5+dPOU^gHSFrzc3yY|2S@DvkF7T~GP!03?<)lGFnX0^oj~KE#Rexv z^L^hQ5@15ACYKF+cvFN(tDBU9f|6V6jCu7~=5L6-v++TFjHqAzVyl&U>Sf#e?iY zcw#Cs;UQV_ofcYw2A71%HHJ67(Y?EK-^~jOBuB zuU3xFa%r`}+3Zs~I9BjF{X5p*lh}8o{CLVw0wCwgHWFI(LH?r^LH+_&-#iu@56&rT zH#fywUGWC|IxO-&{9Qh(E1;3phs?LPT#t11-`w}%2Jw8EGnE&)Yitm)>eY(x_CR3R z^Qnj^HUXHS|E;I-MABx@tcl7N>3NnZuleAKYzHTLAw!ntmFa3^rS}Thlk*1~%K?cR z-s$SaOzK3tgmt`{Ha$x~Z0^O^Wt9zMO#k$bRi>)y&zMY%I{AdqZX$eZ%hgy|3d361 zCxE+h#bvHT#&U}5G>}zbSHcGEGeHy&Umb$h(ww-~x5l_tCwQTg)4q7&$saop`1<9$vq&9^}!1$Plvkd(C*^&A}Q`~ zJ@>Ntv@~;vv$l1xpVwOMKKtL}f@lf;abN%U)c^zvabDQFnijfap5x8s6&3&J8@xJR zlFOhBu_DNJB}_)~9q_Zev3_?qRjOuVtNv(zfp|G+I^iOv`2U7eA~x zGYGqD16bHED^$oz02;IHLmswX8YEXkvzpFwkpd1W8yR|MJ#0Uxk@J$6W+%P+zN2Yu zlDjk7;F@0iWP5nK`90@jGFgi5U+V#Z9$&d#-@kB&w!B6X57hG9y=$xV zj$<#I>71q}qmM+c##e~rN~^X+@iKcBqC7~f+!*C z=)KpIf+#bBAiAhw^xjDkLG&{EM2|9h@1hQa=)E(_s59EA-<mLx;6u|NCXPhTw;ad{hD_JU0#KV(Nkz+*i&@L0Q z%#H%T9bS1Zih84VYem%c`B0|$=X!hC^U0g@#feK_*v*psC7B51<}w6=ExjqaX@KJd zQa6d+wGPW^@-AV0`ZNz@$Ir#%ZiIOHU6@xvo7@l20&J|JQI}I0%giS~<1k;sT6`u>q52Tbr^S2@pvp`JD>;bBEl)tzMW=oLCIkN&_!xqi$wuE_y)a(vRGr^yslW;o z6irOre~Ol~)rL~>^nDA7Vi5D@4Ta@XKuZtnO{v;u&;3jY;)T$u3S3@9 z)_vh*o&CK4Tr<6Gm82UBTA~v-W&7uXSbF>h|6k3H79JC6<~uJ!(x0!&DPJO9;qkOt z5;>(=Ha#f0C;is#+4k366EXL3@Brh@nPFV?rv3ep66m!T#=&zd;Ahih62H06E*p1)aaAF9hlPx&y;ouEwdr>p6rJ!GIED;M`h2|Gw>!Vfx<8af1W7+1S0u zrBhE`d@C+sT=eIz=Z0C}8gUAXj87GIePk9Z?8=LL@_GpKg{z?bfx;kB1NwpPprG<_ zpNf`xvBkUen%%t*UG1$hr++Q@0m|}gu00Q{6!}G0so~2rp_`_zl)n3=pc;lDZ2drml3<1g{zmk{CC>M z_a|jEKIP&Wu~j9JlbvcR7pB#VUUp#r^_1_tekwNgs^~^3#tm~mzTBI4h@occh<>ZV zco~-`UyUi3eVW2)FA80SWDT(4bC*PmJB^wSy-oa*_v+)n!I!!oW+__r4zXC2G>a9f z!$K8nC?o{sIZy;*fzW+jiV#j|;mTV$fA;?$956B;Xdu!4g2>ccxaT)cFWZ~;peAnb zIb^3r(_LE|$ROvFgEi`BL#DzqM!4E|mnuB!6@4o|v#O#K9LZy?_2tGw5(mMlP(wWwLM$)Bo1jKEXOzfyRHzst8PR| zdB5JfyPns3(jNo!AWC0i><(UR?@5c1{v}+8IH)|g4U_sfFlt{TCSUVcEK0WH4`dM% z^#`(8GXG|Vh2_3JO10Jt;3s0f2wzk}j5hRw`9ZF9=T9IU#l}8{0KBv0&@N}r+KS^z zNBWII5+N@W%(3awo)CF0ZJ5!%Wm7+^lnolA5QEw(aZhMX*-Xz%oMQEXtfYQ%%EWFL z@u$e#T)L(_oB*m(=AV{ByrE@Qr#=NKua6AWi1)hVo7-x(pP)RJsho;DUblFQ*1UKs zf|@|VJr@Hf*9vk-sex+zMX!G+g@I;gX3WVNN`~AT8XjdQAoCc+bn(F9ei#fjuvl|( z$tfXPc@7c2Mg!%kew@0p8v8%OJYdE2pn#%kMq|laO-kt4-Z$?iqV)qU!pkP3Q_Fs0k39lF-tN)u!=b7OcFSFnj?ZS7vy zPchdV8!(u74UPA}2k*Qi49`|6C|*%$r_vwO^7o7?hT0Q<8P#86}dD#dQxs z(tor*_61rdZ}E*ol@%odB_y9wgIysj}id76o=a!jc0xW27e_)UGs4&?Ve4 zNJ%{j0yD3O=DW#w`d8#+704sL`T3e_OKhmt^wpKo>Q*RE95*PRW=Vs!$5k*l)eN$J zIrXDHT%j;`BQU{}Wh;p=mZ2;P6-#8cM4FxFN{h5QRMh+OEE74sas7Mk^0GXy&@fKg zb5KM<-EcU3pvOrRnFKX?jr}5%2|-H+8fuCVYFK|vw)mBXLmIV(p8rNTK2i{`QQsSk zXcqRAahz%ceSOgUxEV~U`JE4v{DPlQs0*y7BKT078 zIB4T}`N!e_J+5Cb_=2D00rvPm>8Lrp92UfT{4~-G9O`ToOW&)ciRhE9Lig6-hFRY> z2Vup@JVN6ljXn;?MfVKO8EF@>FX+-vHoPM5I{W;LsxNpb2W6U}8)k)OLL1zD&511{F&Q6hRpp#lnt z4_2P|V0~3LeCVq`EZC3#>^jSC6Yq@GjQ_2wUjd(JNNaLr86X0j1~Chf#Z zGx*lVM!1{`yX9jw$fH$7htb+pn4U zQI@`6C4i_lQB6OK6?z%aP{=!xsm|lNC*&tA9?RlZ6@_n{t=&DZ08~rd#7!&b8zU;7 zwgenF9R+Tek^ghaJGnHOwo94N-NK8P7`bIUicsAYvS6Ji?Jaa{k!Ow@Pwb73N7nl(jP@%<~Y)& zw{6`A_oTOdwjKD_>hS)SK>zP^bIY{ANDZmTr3G)NI#Hd1z$D=kP2W*t7sf+HJ%)@_7F)6Ws49=YNe)9M%FM$K58jiU22H z8(V;rzJdMdj2^)0o7mHTi5AoQig=ygH7&NcKt(1Bc&37>{$EpWCj?Qg?55{@)z-Ed z%H`V5=-;L?MCLCD3^uw@*M?PF;DB?ukz} z2mTYm{)haT`^p&urB5hmUKCGzE5rgBcHSu8>-!@W?>x;4YHFgv{xS(N~E}(UEPc{W^uix zr)5o}Vvx$Lo86sg!-R6onMfZ2a)P+G^?9W3%k%F|N zRZ(MBiYxu!$(71PF4|`T}sEUb>)Ih zN1Xo_a#mFAZQ0GFm>S&H5)bXhp~92<)qia049AQ7b40x)Wg4NDxUcj`fSt%Mrx$b2 z;$A0C3rH&PP4}o#w*aFU)sSH>ScOz>?)}mnL+vZh09Ibe858x!M#{}r;59VWkX7@S zjG{6<)vD){aY7t3l)wrI@H@WEE;m3#1+=|_e_?9F<9>f( z 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%0A<6a3TYXJD(^teV2XjP7KwhS>Cu8G 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�`t!sibX1^mIh{CuG8FUD^zBg4Xu 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

    >c(4+3W($aFP~T=;lRA;n;Hm1W5@ zmLs7ft(Qq68*o!}(V5}SfjOocGq8pV+*V_=_BfgPp=lj-oocU+hT_H^ZW=pphjfA; zYDX%Y9uAzZ6Vdl{`*kax{$rpGx|C^<4e%}oIcl+I_rVXPvO(#pow|d6LRu|T!f|tG z;Yz<4pGJUrTDAM#UM>Aj7^pb-k!Do{L=QCG`gV+i&tt7Sb`K86GCjwdVYxmCFNwe% zers72oM^GpRR?&r{)Mz6*n_GJPd`VIP@m2a8OR}XmcPcz#p!;s+>G8~eZl`dnvy&C zoXEOuzdvhC@^QItTy0b$4ClL;JGU|24h5NtjM6hIcEW-Ut@uUuqp5-5P z5|!zldAdsc-N;~e2h~qpil92Bn#oAIq(>1EhlJ33L zR%r=zxK^W=74iGO|Eg-+tp1z&V2zG#>RR_J={Kl8S-38cOrKM(P@yY}GR{Z}?svd@mExf^y z&HG$Gb`5n@QRL=`vLdeeGOp_R2NpDEitRX>n?u2zgUU=}r}`wfxl_+@nJ7fzm*K(e zx1HY?d$@qLh|!=5RRYCVFi3f(lgv-7K#c!v5209ZQTpxvRNb~4UW}T~ILZKdrdXah zGCAxs(nfDG8swj&mRBV2TIaIpn0(KW0jKTU@mswQC0o^Rd$?Ywe=l^zC1slWsb#Jg zf~FoK53NzY_d97CR#H*IVZC9)IZ(r+P||kN#0l_OKHl|_5?&q^6&0F(jc#t*ej1id z4b7AqpxX%)pV96;PZUEb+d2>|tK*DfI%H!8(-MRCc~>%UCuy8gdKYoPHuCAGb%VFP z>U<=)96YL`&XB!9j%PvmGLfcrMOpfOS%tfl@|jyA9H+qds*~fUXqek$j%NcS`&;6| zTKmR>Gg?To0BCx3EOF?@uzb#4UsLO)T^cSkPnrZky?Tn#uY_~nEo8&Y5!||FCF!jGY34$vgAO@iQ z;ex0;b;W`0C6jyU?)Y97cV@eUNgWG)vuDXt*|!5TG~X zZDJi4d%6meW9eHZ-*58`KNL%SB$X&^#1t2QDe65Vy_LwKtZ!G;h8)e3uxfUXE=aQF zNmj2i`wd@dZA8T}4&WOz^ICI1gQhsV+am1MFqO5}09+GDA3TU$AhS=NR+%^M_C(&^ zn}Mw8+H7|&=3T9%T^L)StQ+TQBGi9dE|(Sh6c zuJ|J(VuBo(gmP0IXh9^!6I!|n;fFMYOGfT8%SCHmL2^~|nz=|p+xQw8Iq2DsBgc`H z(OoW}K06cdOUxq!4v~X284!*QcKa0#%lcE%7EaNA_fT(a4p`_Rzm~um#d-*>S0and z2opHUCQKve%a?J;U00Bk{7a1cNs1BpO{$t^Hef=k@`*O?H1kM&ucT>zNtd04nLv*r zxxwVWNTO##Yd-5|m(*1nnj<_p8KRG6W#zm|87Gh98Lf9Gh{(ARv=1&Tnk1sz8{+oe zsoL$`_t2JX{e?8i%d>NjgN4|%Z@|C@L8&~Q>ecK5hfh2tL6YQ+ zEIl!s{4CS}TEGO?4C{)nRfqddqTN8QEEp-7%J-aKvv|77x|3@^g4{y`D2 zW&EPx9Djgk?j1qw?(RDk`$JTk#)XRy~oZ66xRDl07|F?%OUDVD+fnx~XmuFHd5Ln^8E8l*yipmioDvBb?}~;{^~lsr*|;fG~==`=lkFm+o44 z+3<(?dfUo5Ay~RJj&qC?F9vt8s%NQm@3++54_$hGY+}IcIdYS%{PJIQQE;oT!K5

    9Llncpjw%ry=SMvq^+TtFwVk1b3KKKIjdn94X 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�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�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%0AJfA;(sssn*c(0g0oL+_s-Kfy> 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
    2{s-WzJ<*!w-FcW{)KgJKGk=YkLQ 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

    viwgxmdxV1CJI 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 WlpkdlMT!_Rd6JVO#-ZAtaN{3aPCrfn literal 638301 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 z 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

    %=K|TrO^loDv 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

    ~Bc>pV7+j4`VdZ;5Wym{gG)9CbEx1O$9_yJ+%`= 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%0Ahp3yHHGe>hh$DlE8NHQ@lOg{z3e62s#=qkWcP=x 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 U&@LOr%4WMsW7tD>Oa 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⋘COm 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


    ij{&YLntqS|7|J(S+Rpc-i2? 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

    cda??Lf3Jn$CFe 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 diff --git a/fastlane/metadata/android/en-US/images/sevenInchScreenshots/7.png b/fastlane/metadata/android/en-US/images/sevenInchScreenshots/7.png index 5873757f9fc67b8f6a589d69a07a986dcc3bf7ce..0618ae0b633e54e8e31b77f60ee9fb851aae9728 100644 GIT binary patch literal 388078 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 z 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

    QkKfPd!aQ@~y8Up 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

    by3{c?C` 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౎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 z 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
    EI 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

    eskfo{+pV^73S;sQ{|D(0e+QDR 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�j98Y|u+j`>Y9;B0;uRePQfma=dD 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

    9kyddVGYn;J~;_}7c#io;M7H{ey!JR`_LWKi+q& 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 Na9Oi7^B2L1e*vP}os9qh literal 282455 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 literal 12213 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 VncU=5FjR1?SE{-y<^NfS{vTs6dAR@p diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png index d4ae9af185b3becf4062488b688aa74cdd78a6d0..c35862a8cad79e203ff3a6a98b58612aa478d041 100644 GIT binary patch literal 10508 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 P00000NkvXXu0mjfGG`qP delta 609 zcmV-n0-pVg2CD{;)qg=rL_t(Y$DNn!Vxlk*MrViuf`BMi6t&*!y{*0MzW<|bKoaAc zLCE=2bb10wzDy}#j-Le<)W)*oWufIVJ3?b--c?=e7!oqFuV;s)jW^c(3x+q^vr2c+LWL>+#z%$Lpu!#A z-KT5i!);ZYg|s|E$!jb&C*d0>N$H@Ypp&VwaCbL*L%1%Jx3&LYM|pEE(#t>64J%es z9~O&=FgcoPUw@9QmHXAx`XkEP?fNK-TOzh1K0V!2i&^#cvK;(f7cQ|&79szNm}!3D zP}as|{IJA+tnK8K2;|Y8KbFR2DI?_tUiqbSy&>hLIBc{Wld>;l`mraZ{6_C(SN0wP zOQb~8je2EiLCFc0I2F;Rn&e)DM3-G5PoAvTsA@eteSbc`DhIz*RxQ~G16?_g4K~qK z*Qg#UdZ*EQjQ#`#$0vJ%>~vp=Ztc?tf=`s}hUzZ9p=3bG(HSM}bQ)ckqNk{M0g^-< z+1u3js4}vEW-jt8`+|oGTNkQ03R}DxsO}w`>5=QL*~l_bOneITS1`saWr6svSBs3NyFxrW)r%3{avZ3L z@$9!c&UoXYJU2~&I8Y;40Y_Ex2q@IJ0GU(m46N1I0i_y21b-OSVjxg!1pKJtfK(lO z;HruPLUl}lt7uRs%?R(8jB?`R4oBMoN7lPQ)3tc6IDtJT+}+@Z3ptq!2)Zi zmfP_3X*98T;apvdjHjC{UOLmO;9!AL7WjJTpL`#Ob7j%)rj}95NlqO`iTV|aa`l;~ gB1(+jF-6KR%@ZjJ$S5guDF6Tf07*qoM6N<$g1m`-;Q#;t delta 294 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 V<`8^9L}vg1002ovPDHLkV1l&RdO-jH delta 810 zcmV+_1J(S}2jvHl$bSO|Nklp z|M8%zwiP9g3(5uiX(;elVf~O%4tcs!3s0JlGfr4J!GYKA|Af^DPWbygVI_hi{^c`a zaDp?vm_Q>qv?c^CI20w=!>JTgI5k5Er$PugB`iXG4t`$i<9{s)MtP=MmMvGCXR?ZV z)uT^G+`Q1;dG(4vsxRov?mj^J;_d&GveBh)7u;O=?_l(B`}-r0SOqsjl5}QxNZ2J= z|2rWY3fSAUiAWvch>8aG(|*aQXo;H@DY~{98!;)~xXQADZ?}GJQq}M4KtJq+O?@k* z=uB`|@002ZBY(A+S-Ro>HLqtX@LtRXDSjYrDdVpd7kJ^DCcG$2c7q>!Rg$eyLM`QG zj$eqeM=WlkOe9yhs+-P)Bx960+FiF}V(rpZlrD^<2Q9HAHZAKBViS~TO3S!~mfZh+ zfiee<3`$yhVVDRz5u53|*tpb8TE!J~rW};Q{(`NVmVc0EY*)Vybfl&-KG#IR4KIu} z;TWp|YOmOsYXWAjN)sBMW?4+@(|`ttoF7WKfd;R00;7w?2osKpVDc_s66TmD3`EAk zb7%}W<^!;9<`_EZ1FbiaH!3>E2g91Nl~7JUKMbWSk{rxz_5w*ZXbOV(pP} zfGp~eY|wyW(>_~s^QH({x#jhe+ zyFN@iz^Ng!IUMrapcNcC5-4+dsGAUox;`8cgZyB8?T=Bf4XzyJUM07*qoM6N<$f^`y%VgLXD diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png index 3cf8a0dc28bbb331115a6660482c4c357e6f875c..a13129e156dcc8e44937d4492ef206d7725d8548 100644 GIT binary patch delta 1174 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 delta 1846 zcmV-62g&%V5A_d_)PDyHNkl2lgI6b0aOyZ|A??4Ri|On)M81*Agrfx?|)d$Zs78_At)yypw%T)>M{X4-7Cf=)re#EOx;D=&EgThG zo_nvSVB+{O@%ULAM|EDXTQfXVHDH8i9JLt1EjRHnhk%hn9F-ZZJi|lG<|9U%o(`b` zqtLe&<|fC)||W|^pf#jEhNkDGhTqBCn^~DNRsnT1$!L5Q^m$Xa-0{c zC<`j0*h_M$eR*bi?;#iXNXUR{zTqgOqAij!r=reD>7=STk`Pi+3IF9>s%pW!Dpd_} z;+4wUFn{f^qZ+$`Q&ww4MRpEm6`rVM)pIUB+Iym!l>?(DRIqF15ve&JKVUPq8L!{) zA>`NlE@QcCm{YJBV>Im`Mt(YCoF@myY#AdsKtXPM#|2WQ+_DZu)fHo`VTNC%)Wm_h z=aEWf($}SoUBy*8B}!Fbf>WbJF^4Nz1IiRFXn&9vVx9lRSrf`ULtoxCKPW9IQFEYe zO0kWF+boiO)CG@1%UL2P|0>C&qt;1ia$7^wk|J(9sm@>uJwpnGiIv%!4=rVitSso6 zBvoL1Pcl?#dh;j}K*u+VOrWhr5qAa+B?{OMv^loq=Hzwf@(+2I~w#LP9nsMi$O!;rvyQS4Em0Xh9&csG8vyR%?OhXNzZbhA{3 zF}CTQ_@=Q-v>T8rZquN^0mf7)&@(&})nHDZ0MMvOMVZHmGM&!CmrGtY80 zUXEyY3QOE1r!nH|-%xnCNs|I~7}TPGZ-2J)sKTHg1!iVnSqx#&PqW3Ks&kj3KZSNQ z3S!csD|ZS#O!1TcRo$H}Ck_Kq0KHeg-m%q<=ws zTT}HNxHf*qDFI{eF69;cROJBQ`WYt4=8MAYejh<572L zbx%E@%wH=f!GCc%5nDNT?^9}EWPb^_k%qcW$~X;+2eyvD1tQYgQR2wf?_N-98Q>z5 zC8Yvf)k@v zutSAD40)wOd?lbExbvuR15<_`)nT`w;73&2H(<<;YR>hGfRyM)9@Va3(40DYcU^&R zxPx`Cg!0dAk9wTVS=h(xmF^EWU{s&F!W3U@bV9cD726qgd0qL|$o_1n-Y}@QmW@@Z zHMXKdT?5I&R6-5*F{Y+j{(n+)&IPP!gv?!`-!8pEfBQf!YrOMpmjtT?PR`-kqLy$A zi~hvaFvp3j_eZv1mHSGyBwx@GY;)&S>f{+7-c-OkF0G|YV zzH6_$EmVlxeSYK{@7~83Y0Njh`Q|iTRO(yb?r_i_H*jV#+jM=HdG&S;+daxf?=nTvM)(`dsnHk9n3;f9`pw% knvE;azsX-5?%VvvKhuP9!+?#Tn*aa+07*qoM6N<$g58jY#sB~S diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png index 01047f4beb6b14665a36334c21ae1ba860309217..229bdf563dba4a57d01b59bcc19522eb674596e3 100644 GIT binary patch delta 737 zcmV<70v`RV2c8Fz$$!8}L_t(|+P&Dw$Zgry)c+)s9Y`n$x+%a#!UrvONd^cpUj*SnA$~~Bi0-P)oWCw+)kf39u5MoR) zLP54f1BGefjKa8BpfDb66voE?BevaIA6MH=r@zAcyiQ1xJ%0f3b|l$!1H2__@ump? zUEY-1{u^1fcsB%qHt(!XfS1@`J0@ZP*iZ`Rc)@B+g#`dds%AD!Z9}SdGP6W0fSEyw zDSH6uFzrx>iABt`@oidVG}qAcLuT{<;KyRdL_kM0nRYng>SvM-SB6fRs<^yFqV76O z8EB`aYEwswxPOFHjBGqLrnHZch5D3=3ZCgx8u@snL#b3pKH)Gi^zm4mQWyCmCUy(t z5LIPI6c6j8pu)M2;{3U(Kh+~eLNAw2EX$t*9bf_W5P#{(6mz`Cgyl-W!xx% zzEA@u0zBu)j>BU;WgKeArkZFg?m$CUpoz!6Qcd8oGo?22i4mol_QTRz0%obP79b1jHg)?+RVs#a zWDFcC_J4IewJcH9=qc;%ev2s=Xk3_PRlomkbBvi*gVu~0*6A86W=3NTEm37gQ<+iF{Hk3+dfmhUzOoW-M?9|7r z7B!1EE%oo-n*QJ&tL}lny7KPnRQ&hf4_5O^IaKGXcL* z5YGY|0>!8jWCewANsv6D0FiYI#)`nFT#J8$vAM-ZyefZzH!atD`#HLIe+&2nFkB_0 TNV|W_00000NkvXXu0mjfpq*m- delta 750 zcmV3|gz zftv5ivQnL;lO`2VW_aFNsKn|zF=et~c7R&Gha7BTp(dkq3+A8!C-WL?f<|=y2eU0S z!iWhJpdqh>AsREr8I5tULu0zI&=?DU$9SFg=fAl+PUeR?Ykx5DDK)$PexuaL@lZ)H zrpySbDP>xbTVYgVx>+C<(TzFrp3KYF8|6Zzj>P;eo^XgsU?TNNqK&10$b1sLa=12B+9V3w zd*lX=O$9H?YJVv@4obtm_#{{=Hb^XHF~FU{ngks@G8g-GQC8j~tw;OA5Ef-2JJ1oa zDdYlS+wY`3_JkfG*VpRPZRsS`<5FSEc4X-^L^{hq#{*udG0#tK^Z2N2DCa`LFP#*f zL~UgyQTw2H>O{S86IJQ6`NTWwhtu*oRTd;1 z4%P#El&Ljj4kot>Am#bo>;$Qv@S#gIYAA&Xq_)KRvSNEC7D3Kf65A*jmdrh%H#F*d zxRXSr?thP0O|aPB?N*xv?~2>wzD1(pOlWV-$5e2E!i9>=-uG`epCJ|Xp|qiLxYIRO zNF~DrOmQz^Bd(l+qeo!}KG&=x6ZV)9$#{-wzJkPCNJ=Y4kOBp$OG& z@{z}7AP}z gx|N@!XScV2e=uAnqefs%Y5)KL07*qoM6N<$g2YyH=Kufz diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png index ceb72179336b060f32dc050b7b257e4a0d5fa925..caffb26a36f60caa3ab4f40d826505730fca1ef8 100644 GIT binary patch delta 1499 zcmV<11tj{%4Vw*+(tor`L_t(|+U(BNVZ%TW1;DqGQ49t#%M^zGTML@$^KJuE!GMl+ zZvtC|NOn~Y=opSxwFN2kcyJ{)t{_pkx)5s~A=x}S7h5eLT`xb4O;m6YAH|daOnA;g zEC3sxvKO0xkz^NJso2=q!M=;}k$HrhM|B6*$$A2OMb-_?Zpc)JX1hT+T zPeB(fJ>v!}-68->CTy^zLj_0TTmS4}YT!^9%neADnm1h{>O5eN?$U>DJM|&Qu+kJ- zyWM-e(prD@*n@EP%`5HnJ#1vOr&ELwNex^&bc3~_LWjC>PgrZ(rDGY^bUNXbK16u0 z<}R=tWR@rZ-+zlk+}(BD_rJ8KV<%G$fi7n9e+$TImnu4kd;S#RU7G@L;-YNT9VI^A zy-?IUpj7U|D@9|xd!j+oL3t`I8f@`yNR#Fh$}(!w#KXG*&2}+LF>z?*oKrAH9q7AC zn8FjfQZQ~qTeZbBTfAE{l{bX0j?YwMVY!e!KvU3{C=Zo~}X0D7uDW|-q$gIU5S zXmPKZWsi3OGdUyZa2L$f#)o@mYSy7*#Y}yCI(W>~twO;Qvur2$=B~v|&n~QWnoQ?! zeq2VDUiO%@HfNgGRULw0@EkHtID(~Hrsxe~m~{3{rsxK+au?Fbe?qEYv!&S$th8tn z?px=WsoHG1x~FJ~LVA>pXL{?jcZCA(D5_~5orWmjMzi&= ze7!|>Oo4MmF$YS#7`e4=RxrV{*1%ZlF4h=njem@L3Z^J7qM+#_ry~X448=_;sUxQc z3Kp=?2`P9)P6-7y{X=IT`M6vDw2k$z4=v==Fb61f=VeplOg({>tB`^!a@kPOhMh>^ zM;8>#VCRj3C;Q4ZVQHeUX20^M9xScxqZqL~DJ`|_7qS@zOIWJd|7wSVH(09LZ*xbv z(SI$?><38YA62kjDP5V^kFlhb!N0JtTML%%?E4|6U<^xZg+Pc$K@2-ng?m?*f(GpL z6|!*=1w+_rQBYGj_N-vV^%Sm2s}iQ=9VLOn?ajH;ANymK$VE**`SXl?f>3=^y7E-U zZI6PctG&GP2|3-U_vM0;qfVgh8HIHm3V-gAm)oT7)pDhkP)(@1lvO=S?vUFfRY%#9 zk}mR_Ih6RZ;*{h{F&Vx=0l}D}JH;fbS4Vz3itbT_JEq8=X=SB2)%}1V%3HF>kfLT_ zKf`UlGa0CVNU*2D!p&2zkTU0pg08wWxUS+%SACjnP}qPb;~LIV@6p7MQCyEkD}NW+ zrVfpUuyCMRZ|x%8OGL94th_OW|1g7*TWc|e)3ATEn!-tja@p#uF;h)osqQeHlf2+W z=eEZTEf=9XW^BXyr?Ue7C7IEA|E zHB)TQ>dNK|N)W%%%sXeJ@Ea({V4K}yk2HDyY2Q7!pR&w10|UcHH#% zDRQM-g1@ihPjm+T$Rm0G5TC9oPH*?nfSm?xxW{@2CmM%(3|kefagl0R;Ue84!bM8J z3NFx<2TpK-Y6K%(pd~?$aB+?fL0;qHypbST!-Z)$BuI{{xF~K!5+u36McI=i>;rt- z*8D0M54ac==X=4L?TaZ_msI})%T$=QwZoHwaTnmvxqIo4YP%!+u|`CxVC15XKXiO^ zXebyt9|rgnSGA?X@4yqz^KGr+;x|=lw~5CS{|11PT%1l$Qo;ZL002ovPDHLkV1gO@ B;TZq` delta 1544 zcmV+j2KV`!4aW_T(tqGdL_t(|+T7UdcA_v81>n88iaLTYR*?+b_avp?}M$(sDH0dkDtNj2K3`0&n<%8 ze108^{|4*9IAapz6vpY0pbGY&uhu987Qb?H4_9LMT^<{u(Bg?`lHeK^8=h*Q5L3X(ZCqOWks>Y% zzbMpO;G*oe9fbxcY$?%9a5=3mCFUrkl!>;ulq{l*gF>Hjj{z|l?ZGTY6Si$c@pLUNGMks`=;%}jg zy<(Lo6g*b4GF`dGthDd84J$<*Xfa`>JN&b7Sm|dQ8f;l*p5wr4mz6$jC`%%ib7v<% zBM(k>O)49)%x=6rq~5Cv^xv4bqiH4+HTEfhjrYBV4-vXFPh)M!JdZ(%pi z*C+HP6m&xik3BPpIIe{>X;#9p+*8N1kbk|Yjs3o#SgBlu_BVe{D8{Q>Uz9ekm44e3 zR2uZ=)cGYX+sWHLmC~Y+Ja~rq2Q!>*7XW65=jVk6CJxq^rkFGY4iP z#Y}GySTS2~ZVK(}Gg}uT7v`uP#>nJ^UFJ}2@kgt9RP#+ATQp0U>m5RoA`P*fk@C9h z(V*)C+Q%YR*ZD3{l- zQ=^w(ygdad-@aMTnKmQ-s~h4mg1*a?WoD5`O2r)w??ck%5nqYtm z+PL65Dkvma$+nylMbn;yQ diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png index e416e55177f85ace6cab28f916166ba677c71a21..751104548a96b25993043b95b8ab079875a21077 100644 GIT binary patch delta 1648 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 FV1lEzTQUFu 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 9a3310e01c49f8b36849b6a24d12dc4325827e2a..f51492c53e43124bfb81705e26f7e59004cf9c88 100644 GIT binary patch literal 13838 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 BYk&X% literal 15849 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

    YV5r>{ zuNJ*Q0X$FkB(j+b5>X{{W+mOY8A|G0rsOx%nRm`L^S)Rgb1Ie01JAIz;tug|~3=*AyK<@>$t)EZ=8>k0fV!0$(3DB*(0^8!Dy?5sfb}EyB4wq2h7BTWf z5;FgI!X91(%%-6fxSr}&>@<=aOvM_9_0i=0ZM+}PHODny0@eZUtm^AuTJZw(cMZuj z*bohAQYJU{4DTgYNYY0Qh9IiiHtLt$#gP04oa}*nh5t+P3%?Wk5p^qSilku%FWImi zxb~l597Z@ErovWtQ-ev6FaeWv@+85u3#@QSZlan4EH}ZFTLpr4uH#>$3HTBWjlmS8uAA|mXKR(xed^ya+-z`)!rsTzlcspq0^wQEWoqIquGcN z8<7P3$>KE@mRfdsoGvZa{=QG1I&9@D={!E(*0u;ikLPj5LbZwL-9~+i<-%_005wzn z2@1WnKSgzi<=fOTq;V3?rEqwXlk>5Tt$B$m1md|wq6Bldl1>&>HP2w~Ov(B@)L0AS zOx|d;!?YEg$j7?XDbd|={1&u#l5>4j)vlDchUxsh=@q&=>J8a~0%qE~@X=`NPlo#I z!w-Lv)RcUVgxFLX?Mki6pou@M`*4dlq9;(69XJ`9{D?V$63W(@ z5Ed`BHzy#D2|8T7-+3zS?bo{$%&&Yoh;IVXH;eI~z(n8btulW)=YLgs22!?q&MFrg z;luf?k+2XW?&sGw?2|-Vos#a8OMcGu-F@*Zvr(3u_eE-Q;w40PuCB0cD`xMr)tRIPd@f diff --git a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png index 1e8713fdad6fd243908a4bc401bf385e7a97111d..97555e4a23b225b6366cfc624c50b4ba2da6117b 100644 GIT binary patch delta 1431 zcmV;I1!(%B4DAb$Xnz=g|3Q`ifVlsp&Hr+-{}X)wJd^*I#s6)t|5Tv=oXG#w=l}Nl z|NsC0tI_{lr2pyj|MK|%0&@Q_jQ@ze|N8v@&E)@%!2dUp{{?jaD2V^<_5Zlo{|I&e z@Am&=sQ=mO|H9t?40!)Yng8SP|9Q3l4|@L~ga4t+|4p3#{eS-d-|hd%;r~*f|5l;@ zXsiFe+yArG|7C2I>;M1&6?9TgQvePlH9r?tnaSnx_E&Y$`~OP_?}%0-9yb601hPp) zK~#7F?9GP_gfI{Uzy;^L#@~OfUja$ttmJ86aB5kWWm&de>5r&=!%N0GA7c7Q4Blmf z=S-=MA8WN3Jb%IGox%C-582?12-69cNVJ1X1CPPMjd=Kdol5O0TS7=ij20%rAD(}{iDWWf*uqZMF~0H*b*Y60rK z!4rY~qkjPShg_`yo%-;|x%})J2ObI6rAh%@KQjv=?q>}$=qph8z+*yi9KZ(4Sed{n z1s+aEKB#uUB^;{~2)4i@)$aq%+J*ctoYV-I8L#zcJ)+;=csE+$U?)lhbWYojdmQa( zec^S3kt+}wrNH307H51_-WgaNufIT*0Yh8lgnx^sZ-BwBdkYjHV6MLe`qLH|^u7Yx z3NWrlejzxs-IeqdxHL@t=8KGh71sL+m`sayHF<&60>ajM33MUu(1^UGaBw_)?;{W! zxUdU(O@0J~L3;=!I0?9wM1TPQsI7Yl6gbP+6Cswk+d6pxYTypYi4XyPKd_e~5m00GZ9iMTUH23)cfjC24~XR<1xz#T zy-C$Mz+ihh;S${%VAz?x1@a@XIJZ;9IZazHfW_Y{*r-fwc)-~mA~YJc?zn(Q)>MH2 z>3ra@xfb8u5KXoraJXtjg#gtX;D6D$_wwHtz@?28ZP%Aa;B#Q|A57XQJFw7hz{RB~ zd&l?}g-14Z2G7rox`tc-lqwV;HSPnH>v%kY^g`918)^opnsfC`pzV~OoR+%(WLx zkF}xikmS~K<)fX$_;|0qJ$7c#ODtj+nat6_wHN7XT;9iqyYbs>-PKg$B9r7*uGmb1 z|E(>9sLuOe<@(pY9TNZmfB*=)w0~q{%mZ=^kJ&uu^Ptg_PK;VT>-DhN({7L3J?|HT l16Vx3nhL{}X)wAcOx8djGiC z|2L5TFpU4r{(|LODptI_|$-v7Sa|FhNqfVlr$r2kEv|2&ib z1$6(&;s2z~|Ch!8W2pb*@c)j$|9Q3lL6!gS_W#-H|DntONPn6CaVg8%>k6m(KfQvmiq`^j||@$co)nM*Yz4hU9=S1;lhcmMzd=t)FD zRCwC$n|XiIFc^SUU~`+BPT$bGg?5FO`>x!_#=ieYQYRj78Xz_)=+8cXvO%6CZ_+nM zUu9%uWMpLQ<$wL%?Hftl-o5|R(l73B@>iFyC2@I`zxj0WC&d@8b0C@Ab%9&qT^>>) z|BkEp4~4go5^oD1ILiN+gVe}$Idwl7VfTOlkgAHGp)5GKWyECFv$SXA1(Kmk%i~OXAyz^5dYXC97kDDbAc)#5aJ}DWda8t z{(n-{5fF%Qmazmbwz%2+U&k|-l2~ABQ3HyTNXJ2t zSYSovKRm>9B(0ZNV1rVu4)9{)>8q1ez-*#5mj~S7sbG>sARdEN1Gs79zDp_))~Hno zHxV@#CKPZrYBX`<>uCPyClu&u)GCCVWq%Q?Clr`ki3Kbv0zE5f7j)w-ARYsQ5^(dZ zqqa*L10G7q=LT@I#%WyAQQ)8ld&L88wu18j`(fAxUer3yOA}AU4eEDzv2kh~B^6MI zB-uE0(F)0$CKZ4Q-hK;#lNJ76!h9gU|Fs4_lrpuOm`ET#3`o-iE_yTJoRD1=tA7hT zs3V*eKP4212LUOYTn$i>w=VE1xH1tOr__?TGE%4?n|pASiB9)`fBy)P z&b6g3&&lRU7%b^6<(1?O3Vl>MSbw_wz!4O%8-$`B!amGNg;2h0;Y}*vA{0SK0VZxs zCln1^SU_128mrYUF;3Iac@kRC3x5ef7?8L^W8$J{H2g)4#2Kp+1i;q_gIO~w!47vc zFA@e#17rxOZJn^te{2~(qsr4;TH*p=E(n_m%717ozO(}iqeYn1Rg}GBv`2|YHcpxB zKQr=mX8CJXq5wFk4REg0VF^?oRk`Pe#*k6WptMh*>PtU4HQU;rgUhG5K!4K(=>niW z1)N8>+SmX=0XEJ^<2X6h8(kIB5rF=(Y~ihs_%ICCHl%oi!t`6S11G1&!O8%TDG*2L zZ67qWbJ$<(O0O<_8-PbZ@FJ5voz#O`Dm7i5B=}i|37!#|Ex9t6PN$@ob|t6zx%&l_kXGS|2+kQIY7JFN`m}?88R}HwXOzr z%FF|bO!jnf43W5;+y9d9hyq7Tcy42s|JLXCzW?6;aM7PPem*?%6D0ZSLnPL2+M-z$ ztE9Byhqka`%(2AJ@+m1287&1XZ@s)>;c(WWadXQ3zaG4cPAyn&$<(s(p2LByQWf5^ zQLk7Z#QbEjief2veQw>wPYW8phSIgreI+QkJYD@<);T3K0RVUVth4|C 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 7d06c1d80cf005f640b9e248e81601bd69d54d85..c695fd3a37f48b7ec7dc4f7a5111800044b4e3b3 100644 GIT binary patch delta 3017 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 LNkvXXu0mjf$F{84 delta 3246 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 UOVY_Hz5j7-4ji(4Y~hyhKatt@b^rhX literal 6852 zcmcI}S5TBqu=c#Wz!D{c5*NuqG73mo5F`hQi%Ld7K{6tF$w_d@h(rZca!wLff*^t- zQIW8KAV~oMMbdlx>c9H0&eeHps=A-9o}Q`hnwskB1Y<)j8cH@w0051SwuT7+h<_>q zkR$(z-ZH*H03bGFeKXB}6nMk`rwfK*;02TU!AB=B(ic7#2va#J{HPmtuZG1E;PpLtWfyMy0>^d2w@TpIEqHVVHpqjAm*K%h zIHC#nVe4`MScm#X3giriS4%*Me&=jEF(CsgC z9gE&Ix6b~qnGbU|2>PK&odTB6Z~nWp=D;yD0ZP|(p(GkZ9*^;OSv+?XzQaaXP` zpKicg+`vS>mGT)m1=d*CKB@?jz#K^l!Ektyg}VAn-Py4`ndSwT)O^dmzqbV(q5mrK zzhTsd>p|KB#YZkW^*P6%?Yp$otTm6s2ZCG8ST!XZSYb%%W$P zPnfNf;bFf6*8#PMr@XD~Wu#gzEzQ|iNu#H&towR}0dUIxNxj6#SmAZD0HNH=!iZDm zm2Tfp<-+vjxau%Nj!~WM-4?Sf@!1_`!K~tr zH#P`YY6+@znf0j=%+K|ihSrX6%IHs6lOqjbi#gLQ9I@cd_m#?_FateJ z@=P-D;;eku(_-YPG}2q(4$>H{-=B(O$5Do%Ju-&FD@H~86I2$$h3F6ZV=^NcL|;Xb z7B%IGL8eH#vU+wZOp*c?p^MbieHFol3AWlME!hWV>!l8v+3YXVf*nlXaO6n$s=MDj zFVsMn)X^3p?Q67^a2N5CvA>=<0GHJeM4NN|r3}3r;yT34hH}t*Bb1|V&(rKgMtP^6 zrk7B4h3?iGvLYjiKQ4r}d~D6QxtM0+!PHGP8F5G?My8He%2+tnjUHEx2AnOIzzO3h z#8}tk$YmzHknTo@`|!`K1r$?&9|v@3X>Pu{=4xtwCGgv??`d~A5IflwCS2Wr1Ai~h zO>e)@G9Pidye**;el<9F_h^N8yp@g)rz-B&68LN5dL*eAhTw23tDY{2dWu9bO7drof5O?vWqeon^^IJcSYGNL zwIPjc(#xNj{N*GpfWxXU)rv%J3r}bb>6K-EmR;MZ@Z;8n`m%@xCKMQE*eGT zT<{tG)(_dxC`N5JP&9j1JXmN4%$rlfW7)%`*KJeKsTJVuKK-0)t}j z>BHZBc#d6reIjSylr7lHpluFt;lFGIqEDc=jvK;z)plIqw?PmE`d?R*a_fA|N@Xp-P%5l2=RS&roFKEkFvBgNYetY~C?>V#F|`p2D#H!*hJ9 z!0GL^ti|8U$iZr>l&kG+XTIol2-nV=T^e1p3w1C?dz z7F4vU^AKR%nCcsMp8$(W%zF8i2qzSdcz=+xqYs7mtYx6JMp)}SLt;*u>L@!8fdGxg zb15p!bSe>Geh6APRw#g!6#r3kzAJ#FK|!>BloRZg85D_XyMsYxE%zHhsr!Q?`V~}U z?%XT_QVJT1lJxF@$EYc8OxGJ-<=F`FOQ{ec@bz~+5=p0~DTDS#J~9xlNYo=Swyks6 zv!wOAK|u|^I1Wlp7$JR87FCA~IC0@Xs7S?OGDtCuI+4{Nbd;1|9CTz`wbf-6MlHt- zBpH&Wv4D1?%K5FWSUOG*-=ZWPEO527>hz*bT=v8!#VM|tq%ngT)D+>AEO9x*>hUiU zKb{0iFv7e^*YuG@(!t;!t0-CrDpGK|lxS_t;W+@;w&MCoaK94|T=hxjsng6pM2Yh# zK*Uu7qGX=t_D;B!0)xIbSgW?mM#VYU53?ovpik2nzAC)1X{TfnEBpfr3UDAj zF#4Jrmifet$IET}Wjo?UFwmgB%m1hX|^5*J;n%fV5HNq}^7ka{@Wx zOMB%fYQNY3hfdv4yiEeRovmGRxx(m$hhnuh73_Em3UV-_;KtM$@qtpL@{0iN*t;{} z)#99Q2(5|f7hoH7$x2cWAoNuSiu!c##(R~PtzO!^n@BIk${XHCpI~mE{EKep4@uV@ zL%}iRww%SyIf^36iUu*Bf1oylaSr#7a`;DCL0F5WXgya9zcyn9l2A_=(%iiJRS?%jjtg&? zW5UL5aB9;&MG~wt&0p8-H;~}zz!T^1boD{Z+$B*X5TT~_roI0sACLx&Lt$g5$=NU4 z;LK&KUiYVS7B60w;K{vhRcB9C644J8P1?oG&e(jR|A`yVJ!e&Qi@PLu1bP_?qMwG6 zq5U!G{i7*yxGJbavr^x@7sY`I7*`N&G*M-&l?St&*vw;1F7yGhDTUPH`+0m&GZ`I+ zRqC3>Lc*LE*L$7{leMUf23ZgyTFX{(g}a2dewnAD{#~S5gA8zTtJG)=MrjL<9*a9@ z29@OHL!)}GA1`$7ELc>&-Qg_B$%KfZcgBV1R6kT^Kk(6(5=D9$Rub%TNv+AGzh0~` zz85u7F9jO)#A@mSFxiNja7lmV(RM<i@~GvNgWGHwksR00})oqzDwfSNCA8@9-fj zNehM0rp{K2QmA&>!}0jiwwhP8#K}849s?+Au1K4R(#eT=L?spG4V!TzIc6GoS~yvK zl3szftg~d+6pBQ5fAeRV|5n@h%mSq4l4`os5M&D`oVYD=!kl7}Taajd$yeM&ofudb z#$BbsSgs_aoG+nfm@%pj!XGVRyQ|+m$^`z_zVIG_wwLvfvK7NoU)R4 z2MPIT-l#0x8O>i2DpCJ&W}K=04NasLF1$CVKiQNS{JO^C?>A^XSY`FO>KY}M5qBuP z)5)WX7ElB5TH8~jpwh@upzJ9n)PfB5X~Jp{(NH&W!SA#@h|Q<;j=cAOEnBk?9mtJf1qQ*nIc z`$U_3u&Nu+VnCfDI#+Ck0~(!{;>O|^dKLrAGd|REpMrm=A;I8+{F6iR{eKqi*7kd zu@~}vp~G~g63x+V0XE-Y4R(Yp%OiP1176Po2g)mV$?b+6g0f}-pGA9*>gU&tTaEVU zN6VH^Jl%&q8cbb#rs&WZG!K=*`f1IlgQl9AP4;(9*H;6gS8aLUrFFbh17lJ|>;T^m z|TO&~W=*1lrkMroI!y$}${jq#>Tt@3R_ zggSb&8P6PYf$Z&&Ia>ESmZQVRpqz1mHPWEK7aPot^J6(okrn2UQRG+(1FxktrV_)T z_=g3bQotYN!{}jsOYjljMll$F3@Pau8E#M#>oP{N=>8yE9Yk|wZ3bEeu++f|LQj# zeNT~HsGBx;A^lpLiY;d^Z z;i)u=3Q~&a*;@()b6id-iK`JOUEu8cpYq>G+o*a!zq`ipl+5dY)vbupi z5M_%S<4qT4h6Z2gxL&VjC7jlUQ6zk##{?jZ;v#PU#E4v2;Rhl%YFpJyK2S(GJ2m9P2y8K8enGe5 z(f3~mb7hi9#xJhu#>6TkeLUMMZjT(YhBE+rGgv@W*@Y%yKqAtNsviHD9_lo&7D)G!E;S@d60RW5oUT@aHWR#m%aCK^VJw^^aAx zMXK|yF7;{#VD1uiC1(gPp(Y5gcl|FVj#BG9wmra+iIrAzG{J`j*iPBoZOoeCxi`J+ z&J_vbHZD;|%lJ@rFZ5uR{pS(r+}^&vDx*QKi5|goc@IY!z+;A-Rv$MhqC#}UzqD}% zA&S<>1bV1^P=B%|>wc_Cf-vDXePj7Gl!bJf?!$9yE(aDt+#m$Q3hT!AwNI3e6(* z3a2tnCrwvwN1*4U%c(va*tr{CzDrd!8(sJHk;miZ9%oWV<=3>}4n*g+5qr8Uwq{5tYMrG5ej{PK7h(w3& zk%*@G#|VdVxX;w`sz_OtNrELSiU+c&SE?jAtOMt>I1-(n4uK&9Z2fJ^C&HQ>)}Hoy z5;1c6$q?Ni_S!q$2{~guYT;~4GFM*UG(?a^cI0jw<6(phvNFp7lHYz~=j+#~aj#oy z1r;78>NiVWS{21+Y~@s%vVmP^>Lj+Rus(ejsY_FgSUKuJkvWLzaji&!M|qzU%6BK| zbCh*U^E5C=VQ#ziR`2{iGf#c;NJ{HIF=ci zRlz-E5fS>G8D#l5=^_=cow%6gZv8@jS zGZbdl_9FlLr{Ax{gnJUd+IFY-W&EVdpqzy0l;gAwwp)+C_d}~M;T@(WquI`{7*D{_ zgnxFpvF}OYwQ^tPdSb!6OsS?X=xb6QllABnDC;H?Cn!?OzNHDvl_L+8sI|?vk34Ci zYHG(5{r3Rphr;+-YnAuJslq2d2bAHfzaFz8ECjvlt{^*LZ!*Hx(va51=h&Hntr_dM^jNr3{1*U72n{dw} zEMey)nF3=Ev7as5Eu^Ym3_L+mQv!EZ4GNc-9q+lwxAej3#R?G4#?s8|=nTp$(TMq_ zP@H@N!eyoi5i1#0n5@aSf}&NNNr5=0&W?%T5ki~u4tX*)70{pNq?mox4C)VLi_`By zxOdkLBb%R#;#R!VMu%Df=Ge~&F*UZ&UpwPV(K$>-RPW|{!?vkF!y#Lop9Om`S=Tj^9V2cX@fa3{waFpSA3wf62Lt&pXJv3)iv!|=om(_AzTDk^A)vnOVD&+0{!#^FrX7Y=Gs>h~wh z3}A3yC$O2MUFUGAYuLeefvW#a_(c^6d>dea+5WK&h2EE(k_i)SG6+owtd@7rL^HX6lNiQzo z=97D&!b-NMVM(@tb**_|C2w{LZE)y#&EuW${TlvW`7Z?{UKFz)m0?^C)DgNYS6xp_{gE=QaJo z!seDhXTXq&RMCzUBVlW)t3sdj?TE&zq-xrD5jt%y44ii2<#9 z?PWUdS93n0a}5`cc9GkclnZzpwFN{^7pMN@om42^o9Xl0PlTJ&=foz0zf)+~9cCBY z>RMtV@TWZ}Ym*V-_{Q1xZOdRPr8hYTyDk1c^LA5+lFe>fd{3Eb9XU~gFLkgoKmT%U z@6=KDE$*%-u9p*IJiL$n`t@4rb5EnIfr^Sc(~d;IU*73vYsq`P-8*Z~O9#>iu;j%9 zMq!px$+7FGIQPb~W5_~R_`?PuO>bTW?%tlE`Al43>! zk4JMw&IKSHesq?8OnQMOfAT`2a-AhRa`^XQLg~jSBkbY3E>7_mlt}yY^MHb`@mXZz z%!Qw@$Vb02)enB|#Z`QIM=C9~_V2>PhVqh0{4Nv^YpN@{YrU*j*4B=d#XgxDWLm{V zXWH}(nR~>ZSzC!m`13NY^ED<4Vd!t^c%72V1wDyi!HeC}A!(u4opoZliFxMEf5XoS z63NIPzRrR#63f@#q##^aQJUt zTLVvh?de>+bIM&io&M{)%kT#m_aupu;f_ko2l-m+uj}k*-0U7Fvip9&E#3v0DQgj5 zxp$ZS>b}7C6YorrY=MwaNUAb`|FwugcA%Z#Mil$ZYkBSN@wc t{XZN|b8(RWr;`6_DX@$BZz%QugGeH;>}A{zeaZdLuA^zFQLSzl`9ETrXJ7yT 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 c9bf7b6bb844a1cce7ca10f364d964aa68aec5e7..64558031098587ed42fa4c3edad58b95d7fe67bd 100644 GIT binary patch delta 658 zcmV;D0&V?`2ZaZa|9^2wL_t(|+U=JGn(H7GM#mNYxYym?Z2#+;X(8pc$w$a@J0sP5 zaJe`C`OSEIdAL>J;r%f+{OSIgDDr&&l#GA9BRRNxO_a~$l?~T*xFdASfgkR$;31JVQ!7lPZAa4jerAn43XTAJckDu5Mu6@SO<6BzDNuXurtJOYrQ zr_)a=2Esc1L~jfTlm*aQD|(`!VW`s+x~D-=fSOA=J`*t3RVWD%c3A+cL3Jfwm7pNN zLd4UCg^n*?R;tJp!06GKa`PBabn7%`qajl;UB>M{)U4yzVGqX<<+QSDfUry%y%0E$ zYH33m^%62_;D1rZ$iT(G#tqxbYk(TBK!eA}pu@X13LzCtdH3<|7GS{V!%Ql$d0Sf` z*I3PXWF-RBB2mvPP&C=#bRZG%FS!;;fCZaTdPyeWUC;+fVz22DvnUgIqN9@VPT0u= zBlewks$eTVlndlFkPF6=KCsv>vD_-+I;k=F9fC sU-y#<$6JnsyN3^6IvV+o;%d#5ACkVsc!qBy#DcCtfBkI(*NUnZo;h% zZLy}>H~#C^1$3`H*%NE4cLfkAGXMZDKm!O@fEH!B?D#5(Fn`|%G7ZsU0zh}{k*O_3 zP_3gfvK<5J06=sWigeLP1vg55D3g^4+yIc{cPZ6ChIK+p5Vj1Q00=D!)lUJak1=oo zzhkrPA;R2AT2A}ve3ZTLQ8)t4*xC(5(u`jj-5TXGSTu%sdbgkzC6Yz@e zBkm@I6}}H|On`xVjmycSPxHt!01$e_&Rc=6Wx<&P1Avn(ol+eR{VXQ}764taq*V~( zBOTLx7QiNsq6nREVgWYUoEa7Hg@a50+k$Gqmf-_EtA7&5Glvrwv?$1fbj)0_0DBVa zG9Xa!9bo~+Q5LE*1rSTAUQ`AkkF{hOPxub{sI8HLpv3D!lAxWq@=0PHIEvwh~ zpxN}O+xW2E^tey^;CQO>!O`fGr`Ngr*Wc0)I1}!Uq^ux*-2eap07*qoM6N<$f+c!3 AGynhq diff --git a/flutter/web/icons/Icon-192.png b/flutter/web/icons/Icon-192.png index db3e7671384b14458b2610be18b9a0ee667d5b8b..5d4566850a8c402c3ca0565f20295e9466361d9e 100644 GIT binary patch literal 4103 zcmV+i5cuzjP)qGlpW<*8!oyKYs5CTEA&iJJH_EywktiVKZ$xBl>>eW{c8{47O2&|kB`Z`6 zr>Zn$s1Sxm$@A&O5VAAMkSOU&#+Y$R#{(*i9x8ye=Eus!(IR(n^hX#P)XB=I8=qu(D0W)Nc|{> zC|{M0V|K5?L5LM=gLEA4=X`&PWiNn`DT!~HX5lCm!bD4703lPN9Gl3Tf0*SjfRG_^ ztqD?S!Ar=mUJtllNu?J+NbFmrU*XIY+~+4`lM3SuRiwh`kfJYukTyle_N4d=Aml++ zm>k&kA!I~hga1J%UH~C!xL5s01@ZhK!~wef=O&j!mstqOSD85Z@6(@5$6f#-p}!ko zsAHWcWYFKUH|%t*^MpiH1`boZQo&C~*TXTR)y@62+fh`eWXFkw>HgMb5 z{ygLOQb4_dz^qlkhV8(lWk9=$K(SolmK@-K42^xJ0mzZXffpMAO~wFgwxCe`{yR{v z2e3~#{%76$J+NXE_NuKBpw~>`%l5!I6_e0s8h{*I68Nkg(0e8j$%iLZ=o#NL@BR+R z-v!T_vK%PW9XP*o0)3_di2QTQKXCK0!1g@EE%_4Mn&bWXvsWWw4PO9!+y*!zJI3eI z07M2$W@ljT8u+G0qrJ7^;;Qgf8@B`B4FN8#R_HScr#S_4_O)B#yT&f|)|OYBASsfs zQRgYLc>d2biOU1OMn7Ql4kS%mBc9qMU!bI`-|Rh8GvcHH_D=^sZwL9u*P=s;#oo5M z1EEbFCIOdlYUGvyWN|xjDYWY3GO@R>!6;}|-Yy_#kZkeN04a?eDzzGqKy-1{Slby~ z8<%bbUTQ=Gq$E%L3M}7*z;t8X*xTA=DgsseNx+GvX@HbDx&3Vf0@v*xLL>UlLf~4t z8F;!r4PZ4_)BxtJff2g%+t}OPXBLc+tPKv#paHBy;&rw}V2tkhF6OpNY>!bIu>d%; z0u5kI2t!sbU$edsYA#z_MI?)ZiVFdErw&z%Y*b*~2lbe#qxB{5MiH>LrM zZ**JQ4Br@qN07OPaE zD-EC%Ihc{HLTgliq__S(^b;(Tn3e|6w$I;kJgm}Vzj*88DP@6Ozr!j?HjMj!paGJN zd~U7Mka_WChG;w*)@l8=WZo^H0c2_*3rcG=Wx4E9#n;!vvLM$Nt<$s>vOc2$d`EKL ztlbJLBr!-;`}i{u9{m}ZuoPCQ)p#1fR}Rhu#xF5{4lF@zAGJ=-&8i#f7Mu^P+6*i7 zSvwlQH){2Vv6{M~Ao%Wt()!Xd%3@R22-|i-WNh5fWz}hbgt;}xym`Ou(6pJbC%z{* zjw@A=5^>&I7%4fY**~2Ih?_&RfSIez8u(4en3r}?66C(!T;7$nhh#K>{`4T;^XObt z%lvHBuQvsjZGE=!K3Kr?X%hLsW8sPbc2vm|NDzmeOe|;@MgJy(|%vfo7F%N?R-6S=5VA{09 zL+p-4eApUtRuQN^ZA$|bNwHi6rUmO#J5T2QKLnd8W5uSu=18Rh4$TVgq;vjS4Z9V= zBMG|XD;JnPX+r}Ph8*eyB1xWgYFTN68%P1ajoX8J*ZZZ@0RMZKvv6GyDF9uprzJJW zf?E^3(2xfB4{tRO=8)$4I`Vuckws}B8aRgr_|GjbiJjLgIW=Wrh#!XqlUwM*Dm1_z zB;}rVeKS~|O=RjhYGI&xg=m2KBXx%`|9v}-Tv=29dSKZ`!y-k4EK9n1hu%}(Yis|z z9i&3fK=P&10PnSAHZv2D#9h-?XrG4C07K^K{p=QgU6DNbE3|6K2H5~{)&O$&q&JWM zhpjoti`tDFy-0V~Y-bH115#(GLky*Uhy(&^RmrXn8=%t^Xwy3_Vrvu8yDi#pwxb4+ zZOE+=ec=uV$DEy+yx9!ev?EW(f_ny#giqR3raK3<*FUFq+jQ1XbI<_w2I|Y?aE9mf zgyfgu(5CDz&KN-dJX31!%JYc1Dcajkbj$!UACKfii{5V)Z#x+yv?Zczht4qr+?u0r z`g5{}tPUi&iuK2pa>xMTj=CzGC(=$*t_QT~)*Ocn(0YOmA-Cj#AD~TdHFw4U`YJm< z{tE|jf;ojGdRB`n^>)Jmav@p{KeQolx2V161u}Q*ageqCK?fzzhcrAd6ES+8Xhry`*|yM}99|5BjVf zM3!8LAzh|A627kMLC-dTHa%TG(e{!B>!3ybXFFnmt7<}HbT+;$zebo9ZssZj`kbnZtNdMY+%+Xq zPFCbj0t1~7lOyl#dC;cxP9EFHYLRj)I^&mE%VSeA{E><-pdExWL}6>Qt{s+b*UuJmky@`Z7X`I!rR8??D{1Z_m@a7{0U`2e^jfg?5&( z3gV&S{sNhjf}8h$cjQ5vBqZjvat<5d)h5s?sTp=%VJD76?rUq+*u@6*Kd5NKZ9Ab= z!{*C2U(hd{J3pdgxy~6N_j}Iy~YmfRC z6fma&q-gfa&CssiGZ`u^yxbT8NW!0K0Il_C^0?qqu&zXX9v{^(SQ2KZk(O+jGVxLx+bQ@_~7+%*VHk$kBWO#>9l#Z@7n z$iTECPj*F8H_P*iO$b!q4Ux9c0DI+|!3b2cFi!83I;X|b52=?H$w#2t5{W5O9}RF! z3CNs0aLEbvai#1Y{KuDqq&74{CA&(SXaEnF=7ZPrO>$sk<-fkPTMihFkgPin&ZGf6 zrR12g62_?ebPpvw1Lo#>!01IVN>BW1!MihW(dOZ1lBTsm%kk!hQ{S2+yJ&H5CG!r3 zfsK+-1tTSy?LKZ3{2F1u^n!oaL33fGR&JJc2n`T72V?+a7QRYWC(2X`$d&F6Oj`jf^y>)AHV>Gk z*gs8J3OxF=KYhQUF3@uZtkT4#lIxQO@Rj-_VVxR{POR`-MrS`3wfet=7ge?J+{A`# z#Y5-AI`x-6(*X*&&|1=;)FnGb6;XN{-+NpU}`)NZ1DwP*mXJl6nnQfz^w zo!+)a+fIZv>OOtXLZ1c*B&ABeB_=qsc%#32My9!wSML* z7_Y9=j2QehfbdQW)2Bn;_Qn^lhtZOfMhBQP?V$mL2Y-az>or!_*GUZn)E(&IT!RMC z%at|FJtw#&C-#0UTxVwB-$$*D{{3kHLFU@ymcTgO>cIe#v&ST@-U8g0;u$kks%1NF zFbW3AOY$wVZgdTrt0jwb@T6+u*A3fYgzoqz_ICH5jld<_%awXteZWNn2$G0t=zP1r z0J&2UsHU&PZ!D~%{Nz0Hl4VdGtO=%sFG|gBk2U;2;IB5X;(g{BPixhA3Ke(>m zKyPh1x01hifu%okDd&-&_m=dR28cufDX1Wsl?zN+4qx=Z58m6*dIA!bM3u_!*I7j^ zz{w6Vz}hGv|Gk1)v!A5;i0@x{$h`OtlTIra-@V`oOXUDd(y4fO9$760Sil#tcTOxV zr^|BxwdhJKQh+YAb7Fhy7gsHMZ8B?>q>Pb}@ISX&iFf9T0fzHMc+XNHVc1ggzgBr!yNhI?ljr-?7cx|-G&FLw^Muq_DvXkjcAk)I znF2lRXy*xeP^nLGxC&vXV;v_XqB3xpQlb!sIM#VW2B|-lh76}VPe>SP>Q5EJsVang zM>K_WByCa<^q^tUmLb%_F&Jz;GJ?eiI!q61&JR!qXq529BrFiEFxnJ#7 zA+$@;juX;K6{#@JP$6tiv5ph6DavUoCWY{FN_C!)FkV-l6hi$J={zAnE6)nykQ4}! zAY_n|u^gAOaFhyRf~6fNWQxkhF)ASygh@=Xs^f&DVX~63>x;xiX&7ux=LrwHKErX! zSIImojNgpzI3Z#DqRQc5l{CqAQo)`o2r+D*|5c?$1?z&whIE{eT=f;sRN587g8`;h zA!JyT`&Hly<31HaHw~9+2+5DqwIHO~G^ODb6-HPNAAHO?N=Rh)mMZs5BQ2}{X7JCA&xcj>9>aHsy z5K&}Tv7 z>O}O@?sw~v&kZ>A2RZ1VBK5(M5x@xd5?n^$axst)k6EMEX>zCRCW9s$QJ7Ae+}M6dF; zOv@I)NVgjKd)9S&2RB9&4G8xbSnqDAh-b3 zg7^1PaLQtlq%-hzupgd$3Y6gvz+p7Ez%POaIf2t0PC>({Fd0_AP&tL z=j<%~%@e0U35>_y{Fc}w9hY&(fAiQ8Aj9pA!(1_!EEah#CCiy#^5WPLz~Qgr?pQ3c zJW~3_Zvx14dtrBdEU`~VGTa^|FMbn%!*DI`SS->w-1pm;pfb4%*nv%EvBU>OB+`H< zoFCN)P{57Yo8J;YBv8lz_-zW*Popyyiw^n&IKNhJ_!&sj?2yGGNj?eG2>e>Q#NPat zc%u?UbPnKfzHUz}7EQDb;L=357Z!^aT$%)cb9DP*v1q|j0;r?g4~s<;bpk-TZZ9kr zO{52aD!RR}STvCyz*cFt#4pLVdUFLx2tdb%(C1v}bq<_c2P!2%_EvcCef8gPIW-L) zyaf6*gIejZV;^kX1Iu>6qAl>nPADh}-%h;akm4CZVEMnH2t69Zl$)S-8LP9y`H;O0=B$O;Yhi29!XK8humUK98mTa#E%a*v7oM%2 z@}csN`S8#O-febk1Pg|#QN{~ve}oxd!jx68soZ*KmJ+N0j>B=Fdn35DBlJHHQYtt} zbY1B3GT!MyN3E7q|B_?7fz7}iPsJF8}7KJ(Qt zV6cbcGEHm2w+}?dHo$?S@Y+gvW-+YW6}iuiCE|V&Tq70k=?ZsuRbNMv79Nin+ct=- z{i-Q&M`yUb6Uv@#e3i8 zuy7MRIM@5sELQQj)bc-x0l2G+nrr{5d-QI8fYeG**+Zaty(kHwG~F9P_C4^{5NK9A z`u0pGt_0AdF{~U8PhTDR=}s`JrG$QM^`IqOBTv7Uu;x!NzQ39(#Vc_kfK$_8`XKn| zb~vw|tZgqhb*gY$Qer@@f4+SHu4^r8+Z1A60DYUo*Z0F8H2s0SJYCZ?1*O!h3eyI` ztRYY<&LYENRsdB~;Dzhp-CLlJ#&}mRhi6{a3(=r|OZBkm*HW(^lEkC{TGoYCBjA5K z>h=Tz9)80n9LUtFq87;G`+F0wUx_&Z{Gl~0xfjlk&|y}%BzR~?e&wF7@Y!9^ z@D$yiM3kbW3E)AY%Jm1SfB>gRor|NkMTCrcnL>7 z=s_&H7}-PZ4h?@tw4!%O6eM1;{z6wP+%?Gjicg<2&DExxa0vL0p zxySG8cEPJF;Kk*zCpY@`5&|lpcThXHrj`EOfnXdhgm&Z9G|&$iD}YXCsBm19rar|k z%vu9aEQIAd^!ZUU4Q^};85gN3qG_zy0iB+LLc^k4Plk~KNUa2`N5DC!>GniX?84Ml z@b`IUl^skorar?K@$wc`3+Pyl5T!J3`3?u&wrbJ^Mi^xdTk{%8wC@y z%{WY}oCFiD*6l_47$<9 zlmM=2r8l;}uo&*24ZSD9u7mpg2$yBsq2rS<{Y%}Rz^KdBTE$e15kRE`cuZr6?ZG_g z_oCY2@zYa37`b_H{j2cDH}rOrX;KSryFj1)@-RjK85f0L7r&xx->06K@2%5gKbkzZ z1o});C(OEdsF%6t7DfmlwG!OdO^@wuKSPg+@T~!J=4i2C16=m3zGfM<)8Xd!dTf`6 z5dyfiqn=~_&3mEqQ|52gMS{g!q4#r<57h{j5#7x?)GR@}OGycO!X0+!s;6Sk0eRcH z$leN9y{KnRaCS|&+Q1zl?E<)=t$g*dBLynj(xg*eDL&Z*8E@;ceODJfcFRJm031g( zsgP#q4Ao~hUYVE;&n}U-+x0B-LN>Gtpv#%i++!cDaG9`Jotnlwch6EmYmzvQ83l_= zqg4PyI>_2vw@V##S=2h<)*15afkEw5V-8c$=1O`pT-!>{USTl|d|l1|Es8ZcDuYdu zx>Z%vaZ}JHfIqaB%^`hy5v=;JoIStr=mPmOkp2ek25A$3SUY9eE&*oBwu+}{&N z3RHnFDNLyHEiM2l)i2xxJAG7d-xOwgtY{q$?F`rn5*0v7BD4}Jb2x3ajGb8H8Cz%6P*v`9A}W9jPM5K>Vh8-_3k)8iXf^9+_E9+O2u@ek20Z1wR`~7AV zhxI#B0;ruXV<#A50+6cu=BH-UCbNB?jGa31B7n06M*vIsNR0qhk|9kfHtb}G37~3< zj2$}yLE3txPoW9PE@?rvzYR_XzqiDZVBUR3p&i-X$0+6cAR?i9` zB1nz^zi2$Wi8%q-5g;;1jsW|u03wGS0U|>Kp)lpc`Fbn&$q7Jc4Uk&NiXak5b?(`f zD`O`ZVgmRUN>1(_n}RV6$LHMpH^}`<3)$XjomwmQ>zBFysWV zSC!$Gpo0M=_xwzYx^m^~<3<3vd2;i;_6==JP&i0dY?GWBoD9*jC{m57tEE7*+A_9c zkxQG(+4|;3Ia?<~Q~;7{@6$}qRxFX62;GFbI2IMlH|RSaQ2|J*{R+V@G_k~mXUTQ% z+`LzR+gecpNS55|e5O4Wt_ZyaM@|z8n28Etq!|55n* z2RVDekQadPuy;>aS$lq^;VE!!YdL!hH=0qxQd|I^ZI-Rva8(O9*PaQK%kYaJQSfef z?^`)rWg#wrgL&|Q;QxF=0RDEVoIO8sc1;-8NzR_@!u# zZsBye>XqTe>usns!1ZnP92CB?Lf&RsXcNHELil&Kyxoh>vAaU8>d8-i0QThS8TrdX zn*ad*{kfiM&JSK9zwk4K(@%l9L-jPJcy@_-)w{I{VAnyIvQpk|z=5d)q05=_wslf7 z4L-zGxbx4Q|TU3mXRA-)GJhlLG1gipi z#TWtXI|7e=q}vO%J^Ic4FsPkwPrTze%I~!!;c^3t0&Lj}V?WXDMcEi50Dz|#!TR00 zy-+(QuiOCd4~6sU>GmcxE;t>QXDGjGRnhH@BI9lI_Wh5?AydaDWNye1(1{gOYSu{iKUt_NtBcCy@(W+(z+c{jxe>^BZQl_7*&BK_3IDUCSd#-6 zjMrc8&MypuSLh(nv9BDU4aD+lz|h z3ju(tC3F2`eNm$oq0wpHuJ7I`I(|fpS!>{qnYz6Q58ns?R6U^qlcBI!wafUO9SxM2flJ!sV0vwbx%3z7znc`ZBjpH-89D_fUNy)$mjAX{({% zi?MU_gO3FO@X~ViG&*opwI})Z(W@(EB;P`P$JYCIkQ~@ASe6 z@Um&OCp{=2H!pgAcxBNRXfqZ*+Nj%;=n^vm0CfWmnX2}u_ZzY<@DkHwf&hw&;n4-q zW1?AAoTEid37~{&tD*H6GlJ;6#M7ohn*Z1{q z+a2)mRM#~E6c)qikD<*NSh7uz54wn10f73${_{gP=P^~AQU|{r3H{nb8b*MHo8bI$ zDx%Z$3J%dArUg*Ke}92NQ{a;Sz`9+sw)cAwz(K(gVDnzM=4Cb4Uy~ze*EHfn03|Hg zpwBp2tEETcn7q5uh}M#j27+t1-~`dH8r^RH%0|Rqub-L3L95<@YM+Z`Z-_ZCLo+A;WzIhe5hMu~;u(H%mcMLwpe128Du#NN`K?T zgcyemAXmyGi$#co!0_Y0d7=QuIXi*d1RSzhH#UjKc z#yPK@><4A#1m6i{N5>zFMT|wjy}^E1cB0=%w;K6;3WS-ZYO!cxEy;9cloK>xuI#E1 zk30KGq7Se((w-wE6q1BU$h9JoTT1R# zjwEd4KK4I;e~*XnGxN^-zVpsI@65dK=e~)N4kLyO0{~#u)73Hs00}>lfbJ0d_b*^@ z8vvOpJuMBhfRR7rG$sFT9R2vW@?E!=$LQ3^JC%^ScOIkgm*YoOK}Kb->%zpPQL0|( z%~;pM0F`&`Pya0GnXeK)H{AJd)A1r^;qgv$O(R$1VYPo7K{DX!^H65^%HZVO{Z-%hn1weV=E}CoJd3~cx zzfoi`0s%zu7iz4}^h(J_D-Qe=-|9meFVO(9<&3JXQjl_DzwVOuw&&P&05H<4N+B^7 z9ds3y!WJtTf`6810a?vimA{nr{^8UHm8PUgrbFOq>%{Wt7sZ}1<`az@>(CrxzwDiJOjGo87g>|l;7UIIpi0kBbflwx3EQ~?fMYFrM9e?Qt)**) z767W~#>7napR`~j$mnRE+)ksMLE zG&KlLi*&T^MQ`0(;`qKing-xc)-@=!Aaw4z&orj;FamsG&3h~>O_uT~4**W8&qp2h zMUMh-+HzxwH6f5ukji-&fXvd;u7x``onhgrp34jXgfg!B8+3+!-j6v3Y2S{0I!C(f z7u*&$Iy%DTqaodIGW;S(2WyFB-R_vWl%LvGD+W+&yW- z{C9#|T~O)1s~N_ZZA@29?W!iAD(kdXg(rk}8;-+q@7qR4hJRL7EKvIn2%UX+`ij%2 z5_WY5I&Ye1xSCdeS<~4?0(_^Fe`B<+P-QW&oagaeY=cHoijT zJTTse_v+Ul(Rn)-Zw z{*=qX?zwQF(v$b=YyVo@_B>gk-uhyz94$MbMiz>C{H*|fZVLSONz0`i(uNX&VWMED z&pziiU@p<%k*90nB z6uVO}{eQCQP;32tzW>NvO;_X(q{ucVC52(2h?Kt(k9T$cqo4d5X6y(}`+3ut4FT|& zXV7cRdcX5Uz7=47BrJLM)!X4`13jI(N)m#v&&Hf?aS{tD+{$h6#V1`KZSt~%acyDv zorl~zp;Y@nL2{=&;h#4iU53huE=k_Y)N`-)?*)#_jjPP;H&!(|7u+&--gtSr$@kHo zC-qlL>RQkF)U|fWRR6UZ4uGPc8$t1@4qYl%Grv+}BUWZ+D|US#`^3P;M2b`?`;)Mp z?nedDU-G+S3akvh6}_%xzA?Y0=CHjaGhaIEbSTjh5PAQN92$_1*4<0h-x5xdbz|0D z{&#reOili~fAM+)!$%4}_BHEtoV8V@&NJ708g8qadn#1%JmdfA`(xOKU0zRidM2Zt$h6264QL! zg{fMpCLZx8;wcO1ZnrJ08kJ4n7oQB&@_PetKc0L1xx4jnL!UimC00@=pO&Wa@NEmR zZRr&0^bddcUFPMle^q6nYU_m0gjw`J}fKla353j==4_~&~vrU-df++B)^%6&xFwkD_T zw7?P-m*lgP6Xu%k=R6+Up~Qy%wvUs>!r< zZp-}i$`-0y-8!D{{??CYfBoj;T=F{qlQ)rS`T{+WaZNkv!;!ETsLUxPXZ8oDT^!nr zss@gapHB>g>dPi=G?@2Odpjt8LDbS!gC{AMx+Xcg;8!uHeY5tY|g?=5Q7W<60i|m){YS%^d&KZHVY~Xu&}PIxGlX zoGgwl{E4KNhQ)L15yn{Wu+(Mq;g<{$>MpBU##;jI_KTAtGFd;#R2heiU9TGP%UqCc zdy)O>tIB#)WR~^lZXFN99r_>TTjh#E@7snrq4(TiYI>=5W@0Y5{8nz*hV$xG4fd{a z-i86e*KQA?(n1>8VS`Yu6w1a#T8`D31uebbD|xsR9Gp7c0#f*QiUE zylZiKpT+XmS6Oo=@v!gor?>0Y*LcP;9o{dQeHY4##C0QflU<@g*c|NLt-kKu%~Fky zBCyrw?9-Bys`@CglWy)6ZTxS2Uo|xU1qr=kq@VkCS@@bC6m4B>U+F-Warif- zMKa!CsZmfO8P?Zl8Xr1^fr9@~q{Y~4NJF$ip=#ZyTFK4BNS-}QB6SJcvuioGUXRQ- z`Sjjm~>0m__s{WlPablAy0Mf zg=}}Pc&O^YPSCD12G4vwSJ4?uV-FGpsn(;YlPg)Lawc6X|6+qjaqZ%W!&d*f)LZIf z()E3Xp0pt)GQWXONIJOxHy_~kZ%a(ByP6_4-5tVvxy6$j`zBvB1;UO@#o6W8N1Ut> zn`S&3FhmbWR7sW2cppkY;_H_kYHY}IVU#?OJ|BVQ7IJs=!rn6j*xUt?nH($%KN-Ka zzaLhThI1@Y!i|4w1o`0Us1m7{jR9!^6c_+gPNMz%j}Izv@vc1))!1p|36=krCasEl z%9g6@aALZz;R3Ynu+yMN9uB|j9=Q=1@VpL3GBeYsX8$1_boxXt7gZ7PD-CxWdRr?` zpm?7yADK@YxjA+ns%}4u?+9&f%bl5^twixQbGP|BkFzaF^Kl}U-8rIHccv;s8hYC# zFQRy_?HgU5O|GeioBMPw>*6^fI_3TBV`18M8tv`F{NsJ)4|bsQl42@0+tdu}zYoV} z%WNygmP~ja3iw!?3r7W)NYZXcJ_@?BnViBphvIdzLD14qp-lk=2IBB91e2-BLuK{J zgoJArn+EemLm-y#KlStd85^*~iw2%!(Eqr0pkiZi_^Bne8suMpmkm} zonR8MUd1LvrN3_znFZ2H$JC6bTz7_b( z;6!Ri;-x}ap%7QAqW^>}GgVU4dCzex^V?k+rMfp%>tnC>r0IFyVV?1276qI4+4m?2aXLd z33L?!Wq$s9 zPdLSAvt%$d^et_v@^H_-{7FeRt;i&w`KsHE)AW$Tev3k&Jyx@ed#}-;tF621T?n2dVz}^npe@}<6+8{p~Ly$M7+zFtee%Gh@L#vdi``Xwi$`ycBQ)?^!*mtDAFzSl0X!~bg?3i$ zIS=E;_T$sL?hZHC?`Qx;JBjhh2eIeG(4fpyWKm6!4lvx&%UzheD@2Jhnw>USZ$T~p zay(mqK(Qmive`94v-^ohi|2+QqkWHtmd1~?=j{djP|4RPLvvPWcZ~F!j*HuUFb#Ve zBGb7q)bPxdEcfYQ{%B@W2bbQ#zNlE3?2Y-ZCOrCOq5WEokwA8ZHCZkqE&1a?_gP=` z$;+2S{r61X=Ux52+TzmuS_h@t)3W)isU5n>mtDb@L&9oz)|*Do=qvsxa<6>g``(I7 zlg0cZ_#3oT-=AmQ!~7JYZ-HsU%isY{^W_^tjCS*%T;KLNa%~yt!JQ;vLuTPwO3k^E zMqjJB%C0k^7yfDPs&bcK*K|H0`!nXBcST8Ew)!xqpRv91^)CG@guSdkL#=^62Hd%V zqmZ-qKaINh^iYx;3dUHG@tcMYvX%ufmKCcUr;bfzjPb`9?@*{h?r~SdDfl(of`_ce zbZ`UojPrW^t=lhS;=8_?yYHQCW-H~Dk?UiHMFZB8>oihV8;feH1&_59_i#A=N*$ff zeO&6awm4Y;o?3H_dYO=s5vc}gtCF%pwHlPYZY1YR`IPLvEV!;8b;B8!cWk=&Y|?)p zW=cL@-FO6ld9J`FGeHg>-`z>+)?75SqGBnHry||2ZiiCsc8=fi%DK&;-4@FRN|dz8 zxE*6X={rJ}R=$(Jq}bUo+=Xv757RyaZr8qrA zl5U2Pv9C5(Z?lZ|KmPnE2E}A*!K;#fgGOGz60 zRU+qCwGbqWN;FfsV;bB@E0IfXKJUD$NzD<2CHGWs#b%Q(uSC~GE`99S9unWgEvU=7Ix?NXXv%C$h;R1vYC#38%NVbXOvpt3OI;F#gJ5?RPf;UU0{8 z{Uhh$d9Tc5)$5r=JSUXRF60!kgWz{Goe}fp0+#RVnYNm}9vz+t(o7I)a;%cR1-G3v zd%kzc@(EsU)(4xnEYG3~J1gpmGvP=QIrqa64NQ8>-0|@ars$!`c=r5`%ZvO;G|mb$Nk*p1mt8;T32KVAjdr?2;7j7tn>*F(6;Nr=tf;~>o6q3Xvp%;L`5k+Gv?~- z49(w9!vq0EyRL9YUNP`pPA37CyOf-?6<=K`?CPQa zF}K5zl<&$ENUwn)-Mxn?&XW~V8A>d{^^A(@66%wk1W{<96oxB`PIPOhzjkXUik!hT zEl5U#4Q@^|4ef$|PD(z68>f3FA$gb~pEv@#gsH2>nZs9%jIdPWporsUy|J&I1W9OM z)6HR~aWm?l35TLaeV=n6;noY}%CHxn0o*<(P()BQXs5fT+;qka`>huNk93pNr~(qFYcHApsO_K7Wc)op>0FvzU>* znE%5^gZ;6&?$DsHI6@2r-1G>tKhx^k9V$ymj~#x6zB)pyjv&Ra9WiS*hRL>Pa=k~| zGtV0#%|Q8@tp<`QXG%c2)ojd1%LL7{B*YI{)ZEI&Av}hw9{wV`sEbIPa=4g~jAsL9 zm(1Ew-dF!fUn`y%GJWt7Wlk{*@;QQs=P-|iE@?%1CH&W}llta7o$QfcNYZ>5<&*a` z&8ZU$hAd_p8&OUI01Ygs8IE-AH5JfrsT+Qxtqfo^VSl)h#FEPZGQ-)N*$E1R%TxzbXfaTUNH=Ber;cN?_I!=B_ltzD+)` zypxg!_q9{=OS987{q$k-776%grj`1d7lK8=X0p2mx5acaqt*);KAvKhZfYDfe}^}4 zri(5Kj9S9oD!jTR;d{xF^BkCr#`rHSXqMV)B+B|UHQ z>8j5$w;wb&$r)l}JQ)R#6K~(E*Tu&C`Rdg29Erc^iupm&6oyO+;blCuhbIq^gI=M$ zYf2Hr>iIJMR6W+c=88<^_7os}_*?U!%Q$t?&-3`;7+KV2)7qIaj895C<>H z*1YD^P=SvaCugXmG_wGY6erkSyP?4ydM48ycW==f4 z*jn@Y{VJv&bJ}3jY|5%|OgaxG00rOT3Rmutr8SxD@A@N6F(5Yds zKr|?=9Yao906;c-gZ8e;bgjH@0+N`HnYBs0Dj!4wW)X=uq^~@#UN6yl(TDZLAXDNq zS%x28Yt9@2c0dG}nBGRvAGrbQ@k7o*!m@+Gv3i-K>%7Efqy6p!s}o@Hch6X zZ4n7}9X4`pjjjOY^Be+4br0Ed0(%6>`LrjXWawLy{Si+A$=N*?11C}b;phV!5yv6ncsSdP~x ze<;K74LbCI9cfSA8jwdZAp05EtVF7}lbf?70fj76p=6C9c8b~~YU}{HRS`M>?qGD+ zXbs_oZ$fDHCAcD#K1mL+`EHl_g($eajKH%>$O&>A`HWU^fSPs{d8>q*2ll}LSU(`Y z8I_=$5dUx0DDYxOGCx`O71?9~xp>vL6Hv;5b!gC70!UMp913k}g1g4cx1@AQk&G~+ zT)caNwRLJbuwtsnHgeP6UZ*$~rjQnnZmIiqcK+5q2zVuGy8rD@N-U1)O}bymc^E~b zF0ldP~maA}x z9U~j^hJZQ-D2G*}ffNOU#_?_mOHv8}DO?Vce%<+xR0Plu_hQs#W@cYT@EHCF^KmPJ*AUXc<@eqy9`DWn|U*07!$E(pqF6v0?;seoMTY?-w;op_TCo%AlC~V;f zs6*muQt54Z=pu=;p7cjnf4a;5_=^GV;^pvETPs2XCW_vEYO#}?IA|V0=_2rD%GVM2 zS7;!1tv+sTiXBW$>YJcbPR;kYOrzcuzKG{Y!5k@{p*!|>N(NV7lxk+hS441#;}a&H z`Fp(qWiE5?#Ie6unSri^iuotYoOj^qq2QFJJjX`r96MNd5Gz7fYWir~`4G}emEtkQ z%K}BwF*eDpxo|rbhj5s++_f(UIL5~P612J3yVi;o zqbuZIaZsOsR>|+xPro;Q<|-A0^kO$=x?;yACfOgW;=ndZyeNA4FuySI<)*s0CR9 zBOJZ`!UYTLtCh@cm_OGzv+IYlrZ-18;;{AA4@z(r1&V4k#dF~nPJ%f2zBnyblrw-a zqSGq*JOl48--8I*s1o2fp{H@=ZMwDv_KVsUb9AYII6@pKI7Q*;F_;2zMI7B+c) zGLl=Ru~ste`{OAGu&s1R5SW|^2E1G_o#B}f_4}q`Rz2!1a9p(}2n+|On**)uz1$i} z^c) zC*}tOH87{sQ>~9_?5>+=U<^l&c5s^jg%eneL4J^pi;-#qD|P+Jk$e)oWnF|<&(NEA zoc4v%j#zixopfp4@E<<&+tJfP0?@oY>-?^0(UtoX{XOU(KRpVPeEz#L-`2DMe?I(Kk)(MH@y^b{%W>{CHkIGfg=lM50D-g)@AA7XxV`39?aY6Cfh%3$42`KY{FDZVf=mpaQrz>;G)PG z9d3=yq(SB`9#51o0hD)zhScKu7vW`oygEmj!c?x9(A+ucyz0=30dcncMTkJ*0-LM^ z{=@s;ZhSV;B@&6BJSOCYA6ZToJRCRsb#S%?NvpaSRO7GZtBD{zbknnm zgQOBdd)EX4HK}Lj2Fpf-z3_={d^w!(&2{T72?3My(o8txU%8fFlU2tgy@*3~+RAcl zpuRrtAdQT8pkE;WK3VkNgxha})jJsOX4}rg9ZYCIJ*7v+!c57WQljWTqe_F2zxLq~ z^0yR0Vn7L;SWHcLnv&_5l18Ncfb%w2ReevWd@XwOs2xB@7>tSF|-S9n9* zM20n8Kkej*awr%&8{R9ue#jL*@)Psw;u&nMEZk2$49=6wT}(BnvvnLCIo!($Lr%7& z*L_5%VF%y!bR)&PJGu^q`zrlLPLGfzKaxK!O6tvxGR3%z?5isiUo@{IiF+mHiEDp@ zDTmz^_3(#(6vR{}QnRnFg}#qBGZFE+o696er5eNPj_IIBPpYn3hmh;hNdvdhoeN@O zXsIuWtSZuvktFLM?q^pB8y0#)XK|jJVXBXvo5YYCq9U4XWiH@&}57@2>Yr!d>2FJabOX zy4*EigHz4S#3+esar_TOVeShS$hL&hsr8B&As6Kx2r`skS(2@<1`Vr4fC_@7!7r+dduzKxOFMdcnR5zS6+%4}td-c)sR6hc zbo0yA^|=V?Q|5iE{SAwAqjjCU0omd+6iFRuOyU^=sFZcsZc&)cuB(01%QmHVJYrvw zXZQM%Kz}m~m>9Df3Dd_C9vQQqO}9inMhFL&ydJ!BvaI=oX3xpgeLGmf;aUFZU2yeT zzHMfs`S#Zv^Ql79^46Z7x5^5q|4fOemrL%cjkHzS!ZQXwYBY5N3r>5!P+^;aC;6r( z%*yYDmegK&JXXFkbiZ^CB?^KU>S4BcWf=KzeT2Cy>$vN!)Qc>#QAFy$=cMV2!6z3o z*g%}Dik$?p{Y|S&#N3N*r#6~@(b4PDd)X?NhP!BkjCJMDtVo=PS=;3%apKYA%zA2E zC%fgoO#YE#bnx&@iu`4Ag|}gL&wB)tGW??7*a3 zS&8`T327q5J$&f=_QK~shjQO*W3Nt3{t434#(>NHah&vg`q~A9K6h&D8Fqcx4EFjR z3NY>24Eez8mgC^vOD$t3W_LCn=lXWJ{$%jC;~%|}zDsYo1oPOB3K{r%U;r+iZSHoQ zV@4e#@wEM|hmn&U8gg+&gs3oz$yY)u=(Yzpfc_V~NhDR8L?r2N(I^9o#dV^`vD>Dp zt~NxLPYc#5^9>^EIALvhQ%h;SRZ*k$rTiI%jD>15toKjnbUMy=O~J zr->$v0`*@tBhX`1LV4y{c@$n*G^a^;~**sGacbbE4GQ4q^uUvG)Mr24J1 z$$c**3HR)DJfUa)#{iCheKGIqFz=U{1rvSnoG@$j3^Yc<7t z6;E)Q5}nDV9@wq_x*jd9d&C2F<=onla&!N$9;@BcAm*jx0@#Txk-ZzgAP zz=E2~0?U2sZWN4zwrcai8MsJp=@nr)J2ke*$6t2}My=L$p8tPyw2@>?uS^-+AClyZ zOo*y0SWx`N31c9h=Q%1esdFJ9H|5O&n)mv)MdRwHTlv5QX~fB(*(!OlzPID8BmsNs z-V;A-e{B}XtoKgBdhU_PNZKox0PfUsX_A!lJ2fG3G~oGs?OTw7OjP$yu;H)-9b$3L zx0t7pmlM(LS6)y{z^}U8#eiLghR+3i;AsP z%0GU*)~^@pxA_qfEOhj=>cV-1qqd0EVXHRR&|ey`?ppd>CE_HZd2S~tH(b>Kg?ReR zbbC~bc^MXRvE7sm<~#IA5+}^FoZzGjo^7`dvj{VR73t0eih&GW@J;;2M&qzyczoAb zTe#{`n0wlMo%e@qZ$D=!)PekEV{cYu_TjOm2n~n}h_6mL%gwe)vZM_RYYJ1d zAfIwk-q5*sh_cKBdo91E+?%FTo^?}Y!}GW@DDSr}$GITF4#DvBcaFcnw*XcG+OWOL zZ$wX!VWMT$pJ8?Plosrq`exd5GMqN}OAL}SuZm1NRW@k9bmT)**g=(;U-!6ib=XJs zw;0mGI7;f$72Y!$&5k^=(CZJ;zE;8!`*2TP&%r|%9#RE8EZaM<=KF))f3dPdTnx>$ zdWy#=H0Wsx31Xub=T=|w@Y@3HD6iWxjK4yG|{X|(q; zSd`4xdxzp0rXwL#gm4M?rY3BVHrP|{3ZPwix{?Y#*#{G1Dt2qbvU6QXejF z!-cvbG(3~bmfvw^ZLii7W=l<-oE!gH#8Vrh|HF51>3YoReVie%iAeli&hZe(3Uq{j zftAl=zar=h0B&N`^mo&sy-?Ke;gGSWpejCxgjV@I6(7Uq8PDTTH$CP|>$jFBj66&0 z%O|@hMT>r@gstC_W0CtmMgm@?Z_ZuPKyE|k6NaOF#8n>ZFaLqd!QW!eq(R+!RH;ic zH-o~-$bIGJrXAVP2-Mj(>8Je}F7Nj7ivnF1JOm*uht^l?BX^1>EEV3;LYWhX^I(C@ zp7H2y&)<_FznUI9?CySwSB3Vk&wtmz2dfXqI^OVwm4&t3{Hy>kZjA;3pW?$0d{&<` zNbh}pk~FAa8D{!?w5#tqM`gZECQ6nHZK36>ZwDbVavvWkH*jo)HOTW=K;ebR3}Zm~2dyVE_^#FG!qym)0WnFYU>7z%!B z_m4#0aRp4tweP~sOpZQU$f(rW?mjvn9er$XgJ5+PLkg5aUFZ=1tv?xLnOSwQxX7%? zI+ZhfVSDI4MzStWuiN$18|J^%z#caI7hAg6;@Q<&wb*2iKN#0zn~UjW+k*^)+q(Ga zY-Q;+8?lBj$t{7{p8*4G_~*8CU8rSum!?kp8YS)+EOae9d*OgG>z*`p5GNJyJTP=f z&+?n#D$zWkl?`tzg>uQfd!~Al#`rt7OPW58L%-Mm@NCu2ex0cVNA}%q7xQN`?v)ww z4%NgJkRgD1VBMdmJF z;XJ0ZxWL+$u9dnvk-ARzBfK-;13huuZ0Doy{121e2WN)bvxX1_?CXjBuU!msm-y|> z<74^Gk^L%>G6aTjtF;RsJed;TOFp)kRZ@$RagE>m`k|nzWov%sLGYYISo+q^qZbCb zzxnO#au4&pCkLgjG5b$x;)Ke!(A!t!gJMFLKd!UYRtoChZxGJEv-+ccjyZ3(VW&D~ z#NkTowU<;^HP@KE?U|UC0-ezWX)WWD0;5@W(qzq1;u)$jCGp$3e)FWT-U-g{muK5b zt53Wgn@O#J%~MTGM&*v70wbLoRY%q?WBOAuN(44!2OM z-xcV(jS^4Cexv(TeEB*Hz{mY-MxHQn-FJh}A*pC@6AE*ys~82vs^jDfYNs@#y$+0KJ?s z{C3MgRqCLz3(J%D)vVherW;AFSKKmO&35zwz?V_wzkYr(CdJ3cSFHOg)T3;@AF?BV zHE$BO^0$0IBoaU8`d59cWtq_EXgUCn4s3fgyKUb~2@Zq`iM%$bnwU{l?^tt$h|F{T zjB6SrPm_2KfV>61)n{v_f3hjX& z&ZD34IZEO#&utt4T-Tmr>{iC0+I@tp zYLcq5?^#bK-9{m(M2YrN7Dw=+!+oK^FI7^!kil%g zW~7}dRR6ibQ5%sBT|p`r97VtPtgGd$Q!x+Y+LC^b<+E-G^&f0uWfR+(d2Iy%UX_aa^ G68;CvQ0u<{ literal 17101 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 KC>Go`3;I6*@Aeu1 diff --git a/flutter/web/icons/Icon-maskable-192.png b/flutter/web/icons/Icon-maskable-192.png index 36597c1bcf7758fed5bb1dc94813565345f3c765..30147e96ef31e6bf72170d69837eec2f42d4b8d4 100644 GIT binary patch literal 4106 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 IM6N<$f~u99dH?_b literal 12422 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 Ef_OS_6951J diff --git a/flutter/web/icons/Icon-maskable-512.png b/flutter/web/icons/Icon-maskable-512.png index f2f79e64b6ac3306440140d8d24037236ff6957e..e84ca5bc7a212951765d3993264a50517b8fe82f 100644 GIT binary patch literal 12626 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 W@8tc(%LU$`Ley0eDxZ~}2mKdgBeJOg literal 7346 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 E1Ku{24FCWD literal 53354 zcmd?R1z4187dDE50wP_~IWV+@BHcBBICKbzG)N0bD+ z9S*r>XlMgBO0rU#Z;UommSU;L&*u)fZ={T}_F0-U)#D&qFE5OYcOMz?$KEgNW$vm< zNc{E31ohJ$G^KX#rEghfS*1g%G+ZwxGUw`JQ-^<)sOMoJe(~|((67#AC4xX2iy;vB zu@;Dw{rwe#_%rw)ui7v}iGF>{5O^vHWkg*Eyrly}{&@8hT`uI08+d;L6aIcH7(NN; zkDKnj`@i9)%(82%t5NOk@>9Zs(Md_{1L7Bjo)rbGKp#K8MYZn(%gqI!P0d}iXyiPt z{5-z6xY$`x+{E4!&r|)`?|DmWYuvQx(TK)=8?hSr`h2{(d!pRZZfY(vGn09gF4FDj z`rG6rdutGGY)+c*B!3!fnz#D-$xiY5<~cBUY&+C+3a)@AiAyBN}ZIPoRycSR90RNVq}!A%*)S@i8uDuv!AIO z&1&X}={=kz}46TC&9wYq9WDktoHT4L9Dci$hTp#~fJBo(mOiZ1~V3c5Sx z2`p0^9%_jX3F+k>8jlb$pVo$RI0#$Bnd_^5!-i6dd)M55@KADcYRYco%WR|9QrgXB zPN?wJ)sA1hWvYif*J!J6-pQ<&+VSQD6TfA@CX`2UEcwqnSQ-bm#a6xeNGGXzF$qqdU9i6pwdVzvl4W_vmWD-Ca}*nFwJi!=g%0r-iq5X7?$<%-`w56=l-~To~DJf~yR+LzgakDR<@8z2*PmU8}kSwgwZc7$> z+{W4(`f8?5i$x{jhfsC*&qRL9k&U+?@49ec8}9u&su)593^1sLMRp7>?vSx*^FS^% zX$tsKef{h*t8O~PS*r&|O z$vF^5*@su@sFffl$Di7y7uQG->y=(-DM`WpS;=qL@n<9y$bJn zy_@Pxc?eqzu&uvK0me5<9Ia`0A-gEe@4D9ebUt`}IlpFpBk7TC!{od#WH?7nN6_eN zpCLLIq(*IMu`718GRW^TUAfmGKk$`}rt7~NvUHcG`iv(2qf!UzyeB>64c;4W@VILv69krhbuo8? z01k*(H*n~SjM7?Kq>HP7e#1zA+2vdG9iWM=9aExRfL26BLfp@RFU* zq>_2x?_1E828KtTd7s*K;Gvc2RO)gLO_Ir4m z-&CSJy7#ZsN%xyOU>Vvl5_h9mrY6ZwIUr5SOQ}A`6ZRQ`QHg5eXKlqk5;Gw&J1)RF z1+KiWdU1Vu27&M9_INYN&Ckz6C|#VLM~t%LjXagOy2`8mdgrHb$8f&RfJ!b$M&#?m zRVYZKtE;Q(NSjsxUMOk>Gz)occHT;xf3Py(1*)M(EmgqpOCPPKibG4V)e5gX%L34ibNo3ehj#7&)ts|5>>?c; z9c2!j7e1?R^g^!d@ElGyHl0X(+<4iW7Kc+$OPlXU0!;l@L32kYyJQkd||J0 z+$c6u;yM8-s~9B>Wn@sYwJkuMZ9YD}#bk%3z5Lc-8s-qxPFR$ZgjZnA<2y8^(VT_@ zt4#Fq8C+4(d1H(#QNTJzn3m5Brf*~UIR}M2_b_2Z+Kgy4s%5}Unctwghe7vB*CN>T z^3J!D)(@x~w$T+5g1ix{5-rxip~c`K0TDUVd$g7hh;yik?ga+x82e~ZQc;QhLUhuX zU)Uk1)L&?IICsX509fEp9!i8QL@(Jy$n5>iL@2cq7qyC=0lOc-^V9ljf3|wtZ)sz^ zIL&e*Tv$Mfd4F^7`!np?9k8 zPPHtS6CDX?$Wk(N4Cq%uau*}1t%pbLN_*Ma*;ziWdbLrbrU0f3eWrAvu$6$<_!={mo%iOf8np1S@AzQ0E>wk9)02cZH2Ub zxcW2T8BazW=9&!S=H;u<@7{mr1C2P_D8**z2V9=dUGHx{d`X`a<>p2K5KYmi<_C|v zL&IK>XCQd(aV)DE0$<|tnG(Ku?k+>n$GQ66?;>Z^#z(WD;$t9d?8@Hw=ttB=ge(SRep08@|jHm&-1rFg+II5C~kJUs6|T`^4QPRNpvs} zOWm>5aYqIUX~40ZXhVtC_;n>}cIW+1pg zq<8N&MVPx-T|7hvngZkMq(Ca4AFLj4P37Ywe8)#eJwD_})PEzlV{koHL4pZ~M1aGc zW^%eaZ?vYh_wFFR`BLs3J_}qmk?*TZc-3mgl;6pWdp>?dsW#1((VO`@1HqY<&m-Sm zKdFuY+(6sRk1N`pnJ9vfGNIyEhZ24DP7=^h;6CI49U_Ku04G3C+VsGXHe)b^8=xCJDS_^7 z16_cXKZC=XpTVC4K=%+8R!#%UN?-}x!!I3Niv}D$zzLZ^RVW1JzOp=F<$wt>d~(q zA)B51l$k*{-jt_tc5okGdrEP8s;TH?r4v6%);F`6I#>3jzzzTl|3Rk!h&*ANhyt*xyp*L1wUjX?cHBB8j!J{8X5iG*ML@!SWVmwwaH(?OzII^#yHUPC9YYX=XJ z0vs=kDyAPby`frod0Fo_9j8%i`D&_;G*o>R1xFQ4OwubVDmp_+ITnGp@X-RD9**J$ zMnU`OXLfFz-x<7)H>0nwE|h_w%FMz7qM;E>LQYI)aM;+rols_E5TuA zBu_)jj6+aRr|J5912cjY^7lo&gMhDVve|HMt4DO=W2`(|CIpF;GcB_bkm{2*k7>id zbx?k2;WgtZ6qqhBEE#mL_HPgr)R;|)W!rhsAU1Vl*dRq4`Q%&I9nBEI`Pj_2W2Xyd|2|ONMN94{Ej~) zNpb63HxMdG0$m06-#$q}vY*kCldrG^gMk<<*JXr8#7%`JO~m~vg2%WhqP)C3?i;-{ z#;sQMrf4RG?qM|vt{Z?)(rt%2sVyfk%aFoRQ8WM8-_>u z?2Yp-E7B#!_267|y#suU?8C&B>vpKYOFk(%#OokPqVqldQm%%AEJ61vkP&+@F_kAp z09}{nd)_r{KavZLNlMC(;CWs{U!eV2y#vO`ApQFM(E1GE0O4Sh@Gk+F1)dyZod{a= zg&TnIu&JG+a%f-xAoluYUQTL^@6~zUE1#x?L#@9Xq1%{937Tg2&N|r(kBd;E-<;gN z;NDCSg|Y}GL$53JSecrX;E4>3!804V)cX_K%o>rJon^|dWnyJY5jn1*qZATt_o$!R z$EsE*n+?`dCOUpnx$D3lGk{p0CNA)Ta&TgPwCjxPdrnIl8lz-wo!6TzIN%b|sFdb& zoYU5L;6DXh=C29tZ^Ls&QHn z^?3wSm+~hN8Zv#M!Z|!V$c%Q@awNT#s`iiFo0XfV3vx-I5LZZHBFf|+mV%df<8=U$ zld`E!uG`81Preg0Xk*p(4$e~+7ssZ7wl(t5@c1mErj`Y&{zwuqCLLhi3vvUIiVMW-Ij_-^}F_n>%A31 z_bhVB`THC^Hg;hYKboVlSIx3{7@O)L8S**I%!F-jcfPgpJm~h{S0ghA`}pt&9ak@c zZk)7pRV<5dMKi9jewO~?r$HQ;!N#?Hz(+fNe;cAZXc)p#&qOimo>g;QG$Jq;N2W;n zm7V@YVoJY%PFHug`iXN3Z`sED<>o{=`}ymOV>^n6FW!dgXJuyQZoWC`2^}Z@r|(qF zol(du$^T05NhAnsCc1ZBgvi+9-ca0Sz4JsA2+g_<>s25mpf346K|y_twui#KjcrIJ zgLa>td*&#kHl*m)wK3}nrnIJ^;g(jFc@J5lkW03Xo^kNsoFL$M(^(++^$OTXjd5oM z2-UWtTOW;5VGRf2SNElS25tSKOF|CER)T`nnEDuf51oCR9Ywhe1`HAoXH=F0g~a}X z3+{17eGD2CQMXWhX1`^qR8+$h9Yk%&vrEPS(s|5hzLYd5J``1ECR6BayA4TXuzg?< z5;x#1SzjCV<3IXEk~ND7%?iyHiP3RUkV{4?_pJ`MhLTP)O~&c^>Xa4iK&)F0NpZ`G zIpjb4qyKH^{X#ST_?rRwHgz0#T-}*JuSc{YOqxe%KoFh#>1EuQ6~yeglkX{BzW32j z4ALdlBhqyg`XurR>~V0tNiw8Rdf{_|@i!ckw}ZZxXJ2OuBOub14Bvi3q^p~uea&1_ z9dc21Ed3p$C8Jb%Yb;;zXT8Vsye<8F~Gg^xzmZ^+{!xZvy&b4iK(XKA@YFl6=_ z{G0^{>F@s=a}5nkI#c4Fu_@dR)0YWl-+lus*`2gBXVTCnSd!NM!YtEYR={BEj%&S9 zH^8n-tosb!kPbdl1w)u0d;&0ruyvIAj{Eb+@Y5v7h~@!e@DN+peP$F$ zCr{^I9=EDp-Yze`{*n+yJogy3S%C;+F5agl9}nW3p?+y3;Zr_X)UuhAg*}ubx7)&+ zLYRj`F(ez0*UBZv_U4b z9S+-ujIuKj)k{(>X9e>!jf+BB>B5Mts-CJ{7Cq-u?QClH__->wp0}hP`-3g7&K!La z)w+FEH#hXUPAc%Cn^*E4^lY$^7uJ**ca-;bVPM?uL9&iB)`p9;9m`N0GxQaFBG5=R zJh8)Eu(#v%{W5jY)wf5;#VHmy#MzsJ@&3n@id1HIuLhUfU+_<2>`6&*K?uYvMh6N$ z%QL$+I*E6LYp(5YUNMkE*K39uszV)mdUu5&M0ggo+Dk%}wR{pb(d;5ozKW9~lS`WY z)`{em&+yEt5gyU)rptth#6rf{ch9N&Mf1B|J~Vl%yfqyuR{l=kW2)1AyqHqPW*ugU zhqjQ`C6>b|RU-JmZ9dmr4uF z>{>isaUY$82Mi4k&bxlLV-qoV>g8MT$(OP*urW8Ewi$J^-FG`ugd)MUfQkDRa(=mu z*syEV=2|$3>$>EBR5ouBK&@yMw_v^H%-1OM)Y7dpu~5XB8wEB?mSD41Wv+znGb)RsN>`VK_+j_*!M%X=vyLu2^dZs9RPTMff59(XrF!uu%;N6;f zgtqs(T5`XZ0l~$%D7{|pR6@-}RECCg`6*(PcR6zzQK#dUy{&y@MQ)t>B7(S4Ho}5d z=fJ?E6R%y0;+(qk33JbVHktbHb(LbMhC*N%A=3!SZ$@F|GjVw}fW1#iQ zix-t=HkXmxA)^-uDy5#RZ(n8=H}7c&92z5RIqV|18;AGbnqn!qHZU-8S=8EVB){wq zE>;vE^-yr%CWp3se~@z;!=@6&3zzYH4ew-!%v4&lkt$7%2ysi2tlF0|2+Sp25i$(9 zeyClp(T4i}IAgK;?H~wCzeidH9!hneEi@rx@uy}#MO-IZ8HUN*UdaeDSuS@4Hrq+V>z9?by4~&z?j%>4sn}q9X&rS}q3VdV9`MY2f?(ZEj1fYRSTZ#83*>PXQX zPf5C#3^aGW6hyt=1dZ+EdwZn{=4f8>p_QCwflNFSa{jyt~b^peKl8fhlJ( zh>znqzevk3A8DzhKi%qaSsLe( z<2!p+%PDTmWY9vnSJzbBC&}#B z6G}2xQKz@AqT6l}B2_~S8W7`pQa^(X^Sm^@4(ko6$FlC5BDwg+<~@1Moz|y?xoMpu zOgD?A>leGtT#LRWD4xF+S{hl-T?quegMCWB8f~#P+L9ss>uw0__JNlacR!6Nd=X-N zWuCTpr_GN5yLBMNVy80}$8lju1eH)lk$S(o3E5oO5WIEC@GE$wXp*&(zfRA0LOpX* zsO~W05P(mALeyVQ`~{%1XB)b?SlSWygWBD^5Qno`d^wQ+E|)vo zfBUV86LBNBoq=W6Kp0Vs9R2V|!xWnDI!npjRP?4PO=`neU{S91CgsjUOPx8HKe6V& z?CLW@kl)$?GVpMk=IGcJKR^PBA!MUqA+q4vIb=DW9SqQ(FW&y0k#_LZf;;XoPH&P5 zhJO8EReZx7Q`)BjuwS>sG=J9cav()d^Hd*cz!rSvYE}qku}o8Lz8ALQpU+a-%w}NO z?FJ}(oU3yW(7XJh-y|4ZnSCyQz9X@nWXHjz3T^RzsN-YE5tV@;;zy_59#}M(3;1WB zu|s178@VAh*0%2%7@nw2F%KnJ(z1Sj7++#|F-Q{I4UXoROY#robeYe@f~8dnLGp)w4klvFRmTzRsMR>h-MP zXr2)&l8=nc^7)s}w|_kFcU*$yzRC>6(l<~Dpy`Cq3BXiJtF$cp*j`s-Q!M-2Dp zk;IVeq6$@?15}Rxarv3=yxo5U#i;Ox8Ca0NG5-Ib{}7)GLPJBNuB|=xet+M&*sZUx zuWHN2#-?F%bbNfO%J$W(LmM3}E%T|O%E|+rXC?i8>=1{3MZ`F(-0S%dd8I}-vCTqU zvsE+6!$ggfjl7`Asu=@*b*`-;yQv0Vp`oE57xa(Gf?xOqVd}=d_@xnFYwz4lFYlCJ z-w+Akq}}fS;8Ol`WV!;pclCLvA9q2&hb}Id45V@x!kW6;!apFoFuDySAFH-}FSZ1W z-Ehb_OYEv$H?)i0P}HSeeOsHXT5?^T@YG}bv}7&#wb+|#yL!Kdv&J0U({WZ}yD71o zqnE{QYR#wb?CNzCQjaP;Ol=zMl38!g@rz5J2=@2IzeuE#&Eshf*6sK??eAyrjlVV- z;c^hS}0uQe1H03QRO8x9jv$d8TbJ84eCoc@cN0M@wBL5Q}AoMj#`h_T91Pgmke;? z;Zm*1bKA2FX=vFu1KfYcz-uS_rW%jG_e&nix7Op*S~Iq{k4t)@rKXy*$e>8JMg+o7P&Oag3g6dWg@NSleWf3(j?|s-An_ z#@v%R8DJz&1RZGI?DgTN*JJK0K~A5SBDdMnM<&} zxY)-2cjE5P2KR%IBZ5G1{U5&X?`&rq4Vk!l}{IVLl4#2 zY}gTP`OKuG(r|0NO~@$TiW@ReylD`uNVuq&MlL|sy%qDY_^~~}mHvl~#+HZXTjfO& z*QPLLU~}NcivXF%s(QN^?O1M9R^%Xs%zD-88E`}R^El+BUe8ZQgmz^XK!AG#2%* z7#_0Y%l+L-{_FY|?F^`MZliD0_zzwX*qy&R7qKmymtF~lP(pAanRT`Dag|lo5Np#! z_JUd+ZClu#ZpR3P3NAa&IDqHXD(*1|Kj`OG&MN&39n}%gQuHvb)qJF-WUi6LUamf_ zzLKcmW827Hpk)raHc&-HDTuM`#6Ky>t8Dj&(4ZOp52ioG~W*0F|O)#oQ zTd)2hi%E5hW~)rV?LC8D*!^_tXFZG0npu2P6yW_!hEWkBG>~O_#ns>rxx283hU_)S z$@In}Pks2k4V*Yl6P0jc1G1b+4LBH5^oIxpi&0do1p>eb(~&tfxO%?&sOVn9vs8oloPIV42}lQ!#Qg0j0Rps=W~_<1N#RLN z{{W95$>%(L@godKjA&^e4TJK}V2~DbL6x$pc5W`g^y!Sykf?1J z-kTWxDoZXIvwIh5w?Pn?K25ji`Xh1L|5;;bd~FPMNdCSv#73bjK29LE`ui(+x1%Qd zvOqA&`AjkAw;vwDLcR}Cs-=Hgg*sx;-w6Z^IW_^nwEkacq=HwPKk}%FACZ40R-=4? zJZy_q^XN7@cM7nR*njh1jGb`9cxjO+Da5AU*reNu5Iy+23y>z~OfCRwAEMI%<-bLWeR?_kgO%n)O(rnE=X;h&VXEP(5+{ zWRp`&tO)4Ix-|T}#z?YQ(_+S1r>pE_ZUYs1e`K&xldo=t7V?Fb(Z@(j3k1lJKkGjR zG?jB~1Qhy!HvV~|=jiYXP!puByiEYWj8dxnT-(Uc|xxkprR#vaRVrW zi#a`gbPHV$95$45@YB3)2#2BTQ`g3lk_^0wT6DBZoaDhniSFY$xljLA z+o6cLb)P8ed0*vGKr_isk9Y+;hWbUqd`P@j}G>TIok- zxSaKQHxrN{$J0A+ysj9b@f@z=Ciz}t(zw+eCJBJJzu5X$k~d5<3u zD{s<^5j?DzC^HYEw4815aGr0uGm1C9Uoh129qj~PI-pF3&LA>#Bi z@YQUC9*yY)HBwsm<@-Hec_5-B%1IV|Yr`%jcY(hr)8hpa;$M(};lwC*>anFYlBE}i810KavA7h%FO#$02 z|Cok>$c%>EE2#Yg)rwFWP}CH9Urt^=f0eXsJe|6WtmGA*%i-1nP>geIj8vKfknQx{ ze_`bq7fFA57Fh2ocVxZKMHQ+Zh8@ZOvJI=U7;6c3ED3W=W89>=hl8WxQ!4P(GFrw= zsfQ3LDeRdkB~npsMBKAgVulX|+Zuc@q!R=}@*AQp^7y;3=X6%I#uw1V`p3LdWSY(FrXme=U01{yi|LiROsZLPdzQ* zuK&_c^&gUlto-~MT!{g@Dk;KR*T>b(d7Y6CX4o$?W7z~AJz`V(!i1RgIa`V^^E|SN z^RXXD70bsxW;79^jwm(8p4b42*))gle(D%~!8uoU@ebsXz(l*XK66{H>sqW;{LOW8x~5G;tLOS^-->)-#17k<@(1$yTkRi@Je2iTT)r@+8l39xh&NSe!`gaF zioNawcEIl6$!ammC*V-yd9ts># zN0Zx<+#$?8=afO%!=@j+np?h9V6@X>obVFvxPvRiNE)4j476PZPFF$xoEjp%FbtRaV>jk;M zfM$sh31#nvKQ8E76&MrCe))eNglk!fO4}a}i2&s6 zAeC&LV_$M}Qgo`s_5+`T55gIKYmfMy54`FXRgoUJqDI${O!hH&>7-`##{k0>bb8?^ zVKF0v%CnD-;aHl6bqs<*qUWJRR=mqWR%FWs8LA0XZH1z}n~~0)F@{tdy7)j!vIh50 zx)#u8>Eh+YQdkk}y}K_J?wA@E2b!v~i$#@6&J>b{5Jwr}DUp;6tN=dhh|^BS3@e&` z^Ga&@iELC}sSeZVlcsM>?LVdv$y+k2Cj#h)4Tx(C?dWgSeI~`iVpNJIR@r+u@`)PCE?Y`3r4RqP zoOIjGD-av0YaQe$u$)+CK|{W^Q+@b!0J|fK<|<$%s&tgvz!(#WF(^|X;km=>E!S0- zc8`!(HZ1>V?>LG1s>7D)_%h*N?Ja0T7W=l#`Uo3{9foG&K~>h{v-g#Jf3JhC07hIk zD0XvsuukYh;3Iwe4mDFtpt-Yt{_;RtF{9MVK-vkj)^E7_%|LPPBk@H{M!;RxMj5cI z^jseXls)te5vcOWp;60Kgj<{NgYpF?3#NZJz0Gf;z$PBg=Y=%P=m8CiBuT}ku3T4< z>IeycU1ofiwYqjP<4-^yH^?NLgv@d8#{5m3KD~3ABvdJtTwnyPSiXH~Xx3}t zcVB>QYrenO#g04xYAy1xh`Ir_uz6<`L#op^OzHQgnd9v~Q6nBBSo;Fz-6WAJ z4Audz66(3mEGLDplqc9eZ!I_1E>~Hw<@2Z`1%L?gW3Tv3RwN)Uy5Je~9RzA#dYWgG z1Fl4^GVU;0Hdlj5Q`bbRYy%WFO^3MpU#G0`B8AhwP|I zk6@bup9{$pIr{Fol#6u__T$OMKJ*<~?B`2~x;@C2Vs?8;@NS|h2b3@HZU?wJl;~J& zzSdf(l~1OVbU9!Zt_3GZsWVPic#Aai&*XR8$?2R9S#5F{D z2_u@hI#*>tDZZT7iphoe&LAd;_(n=bmhZgDAT1OF{z0v`Oz^0McUvMb%i~E=VIh2a z8k2_(M{C`Hin&Qts++1uOVirA*BKS+0ui((VV0_rf!&2(I(Wh_L=}%5Ei)Cp6Zh<*VH5_BOFS7HmxU|b2#_WKvDxSP z&&!Vp&RPzJBxg@k+jL8Kn#Q`wrJCn zR)z0M6=f)%zHKWtA_~rOQ*3&>0D!~-FPixWc24}-%r?IXG0H}aDZlWHEX<2X%|Jn~>%yxBa+DhFHU{>=7rNTb z^{%TwGr%aTk1xMTA&meWL;x+_Z#uZHX4sgEwdXg8^i@`HjLI~(JFyLA!&t+=7Ir67_g zogT8&rxG_{+17J1%w+bxqM+>6kgw1q`}eZFsTLP^gmb@?iO9zI?yQDe5gJT$L(r6? zOAjA(=yn$}Nk0h+7rodlU#E_Ia$*E5^= zo;v?rf=tCU8DB(uQPiWojkwjfncZd~9FaQC6+BmOCj%-3Eac*`2)mkjq?ab-tH@!9NL^ zKwgVal&BwRAuSH2pxu)k>}$6XgFp=={ug@47%ZBMZgVAYk-k3HLAI+1cYW|__23Wv zC<+)6;5_Ihzf*-eRe;w>(P}=RpU=DD8dTeBDd!pcUAZbLDn45}7fU0|QV9No1X2bA z$DV#7E|{+-7^-JlOEON*QTv4!2`CNV4Q~PE0sJ9N=abBSLaFYeKB&@z<19LO!=z{; zt+?{j7b6gj%d8>Ql6lG+e~FkU27`aJ@gmkfnfSJ8oX1xD%6KLv-P2 zEz5oJ9Yz3{;b%a2+LQJn3(gLi3VDSnz%>=&o=v`tfQIgPS(xL1< z_+Dyih}eX}Y4QJMr~u%W+?@@H)4kQg2P6?l1mT_i@EOSKsIrTf*XCO80vmzPc^c9e zSpfCS7MJvhmDV(+^(#BLhx{DCca0?I+>e&7KU}|GY@5pmp|t+BeGvAZ$}W1fg9kWO|tkxUa2M~v7wHbx@| zLf5Z?xW)f+DQC@u&tmUu1$ab5 z&aroqZmG19#}X-sNiiW{A9yNE9PdaG?lJki#q{@rK&NQU6=2Q~GxhET8`AJu&ke51 zVv`dJ-z^fi>4;U^xVo@SHh0ynOEt<*=VeBziU!m{e|=HDFe z`CS51&XiWCS9%RZbXd5E0TYTDVElN69aO9y8nTvl{@^XPit`NwpQA zysWIoQeGplmfpWp(W+iA7evj#_qsKTH5|9JEP_<=BT3eYWj5r7!fC(t(2;`oD75fn zg>iql0Ru$_%bTuegRtlrUT6Qd52-BG?Y(i03xb`ST^&pHYlKw`ckhjd5N$WV_6qE* zr0Z3f_FCPKXEezW9=H`jm?w4i;fsYa46Fa>3W2s>=mU*!`r6POYhTdODpZtpVN-Nt z=5N}|^D1-Vv1}|oEK`%dni<2FlC4jNv zb6Hl2grP_d(8w1Lce~$WK+2+~GS08hfQPn_)X^SD0z_)z)n0DTL-8WJEzFRX+MUa; zfImHmH1r-VJ^cqeRg^>%QS_cPxWL>cSH{y$_O-L^!AMg&f9;B9%$-fDI=$BhTe$w< zI6>Dw_G*AI?)~tQC+mYbB)mjz&2td9a@t<%@a3KJ_V|thq0NCO%++D7b>YLrqD5ea zkQPh+pSXD;aE9TcE~HZM((36Wc0GTspeR`e+r>9Z54&6C2cnr4#yXK*!J|eQ?tnC= z-HmcQWT?+rZsr^5ZFRd&xoOb@T3N;ZZ|dCY6fQfnN@CL|_S@Kmmx4sIQ2rb&>bUM#rgj!x(h9StK?qxYarGg{O%f`F3KwYASY zgC$X#SYqy#2B~dTtJ9%CIUdE%?gODg&fwl@Yk@%XD#hOW2Y1WBk;%55S|5bO-*RR- zJl+t{hkql#dPvso6qyY?w5$(e zNgkB2b{0+3MfK7*bI%cENV}$LBo`q1rF;nph11->#6I-`G|(VRU{jPARD%4LB+-QM4n5&jOCYQlZ{{1cqg{Q_6o0wxyw--ue0YBgXu6a5w6s8Z}rYXDOqSoM+VO3&WPX zm1$VY^S~q5)F#D2GJ8uQkfaDRnu9~GT#jc@7A#_<;#$zd(W;R=F`3nJCfE2-cA5e4 zyhWVx-I=sSH{qnD%$YYn1~~UJg?OPw#lrOQTQT5pYA)fBmN;G<^Nm0|c67rVG-jwH zP7!%l5_5DJRf<=v{iUu-)KNkf5JaA)y*1`Z#kf~&(amOK z2lnb7;g=!1!RlUceHt*ndjoE%3aXFOrjFjtZg~=h5;;-*34_6qNPLXd0DSRhEtvy* z-@kpM2Pn~0x0=X~_0a;c6|^ogpzt?-Gl?1`50)XiBkM;m6_tt`k_;+fF;K!krqsKj z>k`eLWJG43aA*mRcLLdw^;ucPm91qg!c{Bl=;L)8YYz4pM1?>eP}iCN_*K-9Qfz}+ zuGt8t%pb&qPaF~o6LyWa1A(DjyaQLf$lw}Ao*I*fF8hqROuLk$Ab z4I&}kAdL#r&Cn$SLn)w0ry|lJ-6h@K;d9R3`@Z+yzyJNb=fhmrS=Tz(I@b4?@WCAk zgJNJR3?C?;+Wv-N+kR5N*GGHtTv>HEqleou;>qua6H?6!%3mm;T< zn9;Qhg%;n!_J6(lSdR8hy{6|_dTzNS=oxsW!v)%^?f(7{vpl5!u@ONgxKO*@5cm6N z<|-AP9|Na4MwH0cY}``etcY@|gr>P^>1YavqMe#hc8+k}k9HlQsdcmrf;_!C1NQ4z zL)}BHYdHh1wS-cX6V}$Kq8ykm4^Pfq2SLGfg<{9cuAPLe7&?^8FBnTEzXB^ES3-(G zZO+`8t7Ug@qR8fEeFEhqZj};3F6y5Tr@$PdYG9CF<48kE!!4{~Q@eYM{1Cuoj;E{x zz~%ainut?B8l{lvIYe`7697&M5{8X&CYyHvp$SSz$ar~vtTl3Sx|})0|MAkL4AU!s zP=+gEXm@=ODx5w|#vY=>sL`zf4@^^PywoP{{XFGcvRvuk>&4KiaXbBBGuLZ^?9EeC<@%Nm_oGy@WjuPgj2hVK zaA>}Aw> z_|Hin5XwAvMGD$&*Ho`SG*^Ifge3nyiTWM=`>^pUkzCAEy}!R70iy{G$o~my8O6`_Ck9RRPyRL~kaS#s@8hsm~_;_(V*$f!^a0=T*srQOAwEm1>;tZ6x{$9ii%)wCPBXl=A==yC3&9v$+@(Nvb9{USbG}jdaiJxYO z>R>&V7q6_uzCv;6w{I+TK9lmGEor-t9>e!ol7KvJ}SU|=YNp)Iyp z7T*daMh$&D$u9yn->QqO(t~@Y`&+f!tU^tvXe1dRnOKET z&?+*AMMWh7dX^-GiN_ScGUX$cqdx#7QNhA}tz!Mv*tR)e>XdgYgCJKmWag*(RUG=R zMwmBS->rg=_^rQ<?Rw~zmwe9cvXIJSY2te{`TMVKo1Yb-<+=3qaeb^&p()Hu=RW!xnyoxQW&dg za<5xnUiH;k89Xz>%u$nMk6tB7P`O=oGfS$Um;j~fJP|>52aI2;$J{x;)#;RVHrMzt zCeTLzV-3n9JfRz>63}7yDTy1SsZH~G4{;t)uv{#g(O4eN@?=y(R)krIXOp7bdul_dVoF6K&~ zT0Z@H)}m3aMfJBqj{yp-D!sA(qFVIw7iNX)|NS2~0pIHA%##<)ngFh`v1+YfBOLJ> zF>c6{6UWT1ORqsD2*0d$?oUg|O&A}GRHOuq5J!<-zc1<>jb1(%t`mQVyZ)FhI|wRj ziC+G<2Y0{$7FDle+2yMhe0+wu$QGIM<2-&Y<&+mMa*o>Y3bY&a85DP7vp$=2)87F{ z=g@e$>Z>&b9}|6kTkKR7{U>y2ww3r6ocqS{+?-`%Y{1^2`hHcf*z~TuY1!ap2`a1xZg#L~N-B zxHv6rI9gG$42_5v$SwgBdVuc7n2V2jiD0fj8Ibw!KfCiVVzFB_&VcKca9!s_%6JZK zwM+_Y4QCwCc0QximM+LfNsoUh%qf2_o)Q-~Boq{h&iRxLHanMs(ne#Aa~fY8A^JR@kaOcFwKDIb~ECEmt5d zHqydF-D6atjsnFXj0eBh1SK-O*DO7pJ!5U_$^Q5?VHGRhoL_7C79l#x=6W*|WrO8# z9~F~CCs0&NKcR!$_rfbV{P1f)7B9b8N!nevv@~aTNwEOHEIESKV`0Wk81a_HFWR96 z(={4vTn4cTT~7%N_DJKRuK{7+w`R8Qk%fz|xNyhq^tNTtOXLP4ya?CU$<`*kxu`RG z{lPE#WAD#=2m$$%4HyHK@fRrSGg61+Nv)`;uIlk(M?7Ai(`uz0OIT5C+pYFOgFV1= zUU}GJZ4-x>5^^$r7C>vJ99dRbrf|@&F7)J~J||9ObZXqZhR(M`RsMV9X5^iTkw^DR zs2)usW4kftg} zRRNPg^*;_G0lCX$UMoujS$`3_ksN!prnE)P&(HAcbn7|bC!^OV^pQ%TeY}Dc?cy&k zJl1hFa+RzAnl1O5Qx#*cbPCv45Mwc6iu@8H}@rVJ;;O^s1Wh$)qEBf8UsP<~6!o`jWq zA>U@4FW$4(YE!oWpleco{mLmSEbROp7jS?^2iS=2&&hf0t-f67CSd{R5nAKKFpI=* ze5{r}vM89}cK>KYT%4K=5*(WkXTXuJ?9dQt!^{CCL99Uyv%!v`K5l3^{SV&9u=OR= zd4-7H{@R7~F?d<*D^oVSw{526J;w*u_O^-ECgGn6Uq!yNP<}OUtaK_t2Pxb=cUK5e%H&K!> zb9p{e|M!?6s`wSsN2VVzuq(4g6ssbbG(TAM_`?c>vz0jJ{Y|sTJ16XvLbN(ZYl35G zH6%}-Au+gBk!2J3sKNu`p$y~#q$r+NzH@W^ zM)&D`Y2h_#+Lnot^F3-6xc-vTw_2*jN@=uE;q*FebxOyQ$F1X*JSD$QC`W1bn;mkh zN;p*3<0V3>nPnilaI|k=@hs`o>G{R&3IB9Nr)2rr2YW6d-?JT~hi$o!jkrhRE z`3?^c_s~oy>-9^cPRtNhS=k^QQa(A>oNO%W8Pw z;9iaMk2NVmRcDW?>KsLmdNO<(N9%zhLuA>dhZPqhgGg>HKD#AiCYr^AD0Ha2mJEze zfY`Ijm-P8qT0P0Mhq~{5GVxLCPr_hI_}ElaI%qI6aJVjkCJreZYZ3w(=_9|7H=T*x5dmNvkL)_$VWIOiA^96yyQZQ@it%jR4uEqPg9gZ^ zW#hKk+1n?wX%*+MqZL{yDJkkAxW(_Rkv>4LB#ZcYe?;0tCFXQG$c=cJT=j#N)cGqIvsk&h{)(D zB+HqV$+ZZ~*A?t{da7lS;B`#fA4$yA7SXn|Nxv{pYVH0(%aB;xbKT5ulE2vA+e2B0 zE!L~eCW<>Htjo9Ro++w+IDGm&#Ztenam9JPR1RZ%lI$CKy@;{A<|HqJ8adlSutX|( z3Z0XUy1I(@-!#uwBPlN&^VARyiu(6QzE18;?gXD|!jgP^CM^2C#C{sBlF8mp+n2th zOjfe2IbS`Le7RHei##@9=B6Q$C}vlq@Hc!${rgeGcaeck@L?p<#uie}7T8>l(dfdn`lYlj6&Erc8I3CD3qL z;T(7XVt@Q6K#o?9@V91UU;xNIlbdCy2LsAx090SAMAll^VhwBuw=~xc>&5gn$F*>!#s>P&;VYcvF>le z%dLP;=;_}|fbzRPU63$SFM0s{0aAfT!4Er$IYyQ#Uz)`&VfWTlbm$Iux>+=38+?JD zckQj2RWSXOEq69|4Ml^+Jh@89y?F({0~fW2v0BMKySEwy#t_)vmW^jpFO7^GaLWcp zBrt9*!ga$xo4iadRZZ<5!<3Az?-lJ4vgpBV6}(MmQkL8ww69>4v@bEp^us+JaX^B6 zzwW~#YJ&AcZ18^KNz@15D1TrtB_pZt7l;)o0SigP|&%mRos)Mawwe7a@cwW>$_LU;t9bYHw|=W^@M7)(CcTQTJ!@Qq(_{WX*x- zn9g~rOQkh9;k}Y$7{tP5Qv>AQwYbHm)YspOOm#qI3J5D&)b^0nB5M%P-E)hp(k#TJf*@ ziTF>hdjI!)rpZkKTwBqE-V!B|yx*WjpHvJMkzY@sk<9R6%6&Z7;0AGXmzlQ%ODDtw zo{C0g%*HtSm^|6*)VPqlZJ`?4)Ntn3&l;L#SklV8OOTmUeQKhs;Mfw_%DUlIM;cR0 zKOUu#%35&*rGmeQ$<-qnG0>0TI|aG5=hL7Y48-i=1xTgimTSS3S-34y&8*w`9#_Oj zjE&Oih$K&Gw}=^btrm7fK%W|e{epI~$fD{Q-F~uw(J=NrBy^LR9lHLV~Eg?qxfn*yBC;96c>EWAs|c6!J89(IalJEJ_ZB{HBAqCNeK;1rv(({C07lgWtdCUl{@50N=^a+YM>dlTP9=$LaOWP7A9n5aU@I{x2FIf4!N-2 z2)91U&V4wb{+$B!=Fp>vh=?^jFF#1SB)$wVX80}fsK(laVutl$^dnda^g*|t!2A10 zWX5*d=2*LLt7iGX?N5I)_Ggt6p^%rtkWVz>lnZ`?r3SFWBT!rJ#|i5ML_`xk632G| zgsh(oLlR+l=J7?>_qZci4Fs5fs_qYn@dt@my*tTt$>_6%)mT$%wq#4`@};qzKopGh z{WP<@WN&_erTQlG$#1@F$th{$PgJ?(b^GMVYeHY|Q*vK@4@T2bxGqk9F zLYG3BaeakJyN)b^1V+gMsC?{h(Y~!X`R4o@>sO7t!9kq^2gT4E2jdJ#&B?nXTkjHM z0*2Z1G;S^|{FblxePuaX)^WZ`c+n?)y+1zu(lx;(B>l?g7ys8ZHrj6I3(iBz&y(Mo zSG>C^p`ohJUz@J1U^T4J(fV_-^okv)g?@T}VJwZU(D|tmdw!O@bMB!4{VkqACu`Vk zanY;2#75@8`&7keS>#@xKPmffWB2WU;=){L3&AnmQlB3F_j5*`kl+_c`>6^&^MiN=FS4A2shZeRdv zi?mAfwFjJ_Ek0(N8iK^Q0&Y2bBQIylik$EMk*hp+S zYQgk~MWcevHL5F2pSiVEscj?JBUU%Q@C+2QbJq6P_7-f4g_gp1##&B@23?04KPIBM zJIzdfnU5jLeppjRw47k_-a~_o)>5?Y7R&os`)?>bp+>`@9Kt>5HR%i1e|ft#>n9UD z)_T#|<;BPWFu81?!7D zrVivz&1DeY{B$x6NqB6RFPqz6@K@Z7j zjjQ$7#O6bg2?YBEjxfx`XHgbCMTj2%Apd(XJBr&Im}oE=Dg~60@N9-&_kh_A#ThP3 zja8n`=$QF2rbUZkM+PV$XR#CjmJCI^aIUGT3g{M2{3KyVIHhW0$Wd1u0Ax{uK<4tn zBpABTC48R&7D#VAxh^djpKq%?R?zY!-dVhL_mqa~3k-%;~NQ9N7ANfOmkfFmKP zbCM<2_$?{7;i(5yT?3vRW#9Bc^H;bpo0(Fxw@KQEQU=RZD?rN6Ve?CK9}yrp$$s|G zsmwHroT-403Eq@n02TAhouuT|Dvr>hCTihK7%)lf{OtGvWxB3ZFx6;e6-6FX^MEO5 zYeUDf%0-^E*dVTDq7uc;ql&)C3km7ys;Z$F#0+$Kcu@>&3cAkDD3CJM1y$Sfe7W(^ zFjP^Q`glht3RfP(VPG(Y)UxD5Tx8(wRST=w$OOVw3;u5^O~KdNd$12XGDfjn$);T7LT#`3R!6v>POM{+pv#rmzR0HG_ta>R>v#k-!(^I zQh&a&R6Y#hGN>4=>OrocC{;q4L(~_EiT3z^?kO+gIBE?guMOlnXjVUuhCr-KTqFvT(HAkw{>X)I;6URiG zL?R-oxKQ&g@~fak#?x_Ss1()R7NaWc_OXVt z*g(KKLA@)JD_$W%ZZVWVwcM>p-FBRwf1*;MduvM7YxXsZ3zM>EK=CdpwBp>2%gtxD z42=?e$H`Howo>|~_KtykNLhTm$6W(kTdl0`KK{%uVt0I*_(k00)SiaN+RS+Vak8<% zxxBwT1^IN_N&bnmX>=x;;k_3EbjO0mT9xuNTlp@h>LfpT?Ax?L#(^iV7l6Q+d2wjb z>&%d|s?gL#Yt4>9fWMocmB;^cG12tS0arfT4wRG)jzAK26jt$Dg-~{UYQCq6TEyz%dZG(Lho2s-ZAj0 zFFU_uARDx~%|0rg(|=Z@Wj~SIu_LNNRff#5JDIweWk=P&2|Kc=i5Cy;Uw>f7s^WR| zLAv6w9&pf46vPv!)4POpGgD@XH$VAFnveloXRZZ}F7nt0V$Cg( zG*Yaot*P9fBsWaiTmtDe*bO&0T%4Ci?-+owlZkTETxp5mEfMmmNr{qO39p{_5Pw!5 zurb+EK@FqS%FdZ+>`x614{QTjlWZ23XqzVi#X|h$>-#*0T%vhr`^#qEulNqP z1y*3#f_GatsW;Zru2mZTtbFGV0nEh7-0R2LUq0N$yTJmVT}^x(+jx1d2J+{Btj*_0 zbU!Pt_-L=cnbVIJp^gEZZ@Y-RErAXN#I;SnNE+E;?oWPLKF(IoVrGJO>A9=&zcnrjU=UCB=UrZR6vCEmAA!8Bg16V=7|q2f{q@IXircl z-%KEWVL7*W!B*(wq7$xl4m{Fgp7|s3X(t~D6HxFh+(7o}YUk6#unn9_$H{W(T(nU)ISCK>`;&S6;87U(mj3_3DV&Nt`BS{ z7tW6!?oW;zGPQo|uIbatoL?G@4k7ceZyk6qW>;+B&><>Avh^W|+kxXimISGkueD$G zYDR35n#^RadfnROv8VT-&a(ei)v^Ato%7SFo;!98MI}b=6(E;z(^13j^?X=n>il!j zvLDseA*vW_D@_ev_>!*o!=f=`3 z<{LuP^6f4S71Rh*;KcATND%+xx^CbpONk3W4*Vdz(I~4_kahmzGB}hhsilgfmFEx= znwBIYDTwu>#tw$o@V-=ciXQ(Sgfjo_%izKfslg>1oc_`=^uab!Uve6xzk>Qdf6wt8 z?(MI*RGau6B|4iscoJ(AXF82Rs*(`wXs)9XE~l2YOZ>9pM?~R7Vb@+)x=iYD57$D~ zH#dM+8ep^|jTCrA+U=}Ae=MLD*kPY4SXR+Pv;5veO#f~x@%gcvb9jrWv#8uPS`iA@ zB~flp%0ur1=dZZ+D!zGR8`<_)kpjfLj>&Sn())p9%$2pff(tiBU_Q2m9{cvdN32h% zMbf}4Lhgikzh#2Bgr@^RJR_ic@NtFAN02CaKg8&H56R_0L<@I?F9oSn_)Moe5IE_M zIG@i)o}Ryrp5qr68aH{|{$b0YmAQ32_^68IQOAz($_%$5? zHWkmgiFk090BfZG5SQ~oQ)4-q9tl6D_fxQJ& z=9j)eN3NA}>mHv>NVqUX?@5DEaHg=Of4{fA+jb1>DVeQeluy_|Eqn%q+BX8B8 zKmPnhc%5463Qr}P%l`G}vGPz;H0LiF6*!KXEIhu__ShZ!ME?)dw~=cg)} zTCk)&?6J}wyhf(FqlS@!-iJoAuy54Lm9j2JpHuzq4@$^@FX3!SZzG+b*QNqE8~~u| zkBcT?>|8D}wrSm^#a_jGoJnMTzWtL0tU1N!$ytfyipfaiv+j_P!mF5AA@NvG{zbCJ8KDP$=4fBZj>N310T-zc^a_kA~wcf zhz_1^5%l|jSlHONMkx$pitF^Qni{?UFkun=DaaD9cF03HvVgn!7LifRt|Ky0sLw~& zAGPfFmFS&vsGTA6Ql0#(>8ut+1ljMxFtGN&i@JJjknbPv)ZaT>b6X9>nLD10ww9Rz zxr;3?sx%0wHfI(i!6jsM?Dzr`%>eoJCod)2no4eeQrMvfouEC-bKzb=R8tQ50|%Kt7qc-!Y7x%4rN4Yg;sp; z1EWk&*98&=IM-Fxw~B^JbSCX@B`>Yr4V}G>4t{{vGN$XkWIy@F!<8mwq1(~Tb2@P1 zopaG4g@otxO-oh!hU z9bZd>Fzh7y)ieHf-}eHm6JT%t*%6QNpl;5F3KN-a)+_yp0sfCtF4ES={+|jIROKJ0 zpnI8D1AehUH1vnZ2PnKa&3?U}44Q^*&x2@~m}Jr7mmG9o;Ou{lkMx8isLd zT+4p<64RXPkEKJAgRR)yDxPMe+~pTrs5cWf>t2A4{ zqq+V)MTb|v+*H;qwxb8@tD%iOb;{`zZK9ggDhg_-Zuj5i32KOoYsbqHq_%9s;a*~j z2W7?k-=KW86;W{Nk~iVra!t>*M#8{5&KCI9QUCUS8EtFu?-$II29N?p&ul+4;4(j3+zP14R(RQ7jBz(--0un($H zkL(Mq<3n>uHH>2P=@vtV`t9npBGL9o!wb&fOOA8Nlt3^fOP0I`g>(9G} z&jdgvnqQ;b{Y=lY@L_8ce+p~&=Z%)*6r)&`m*Eq`rf_8qne;)^5d{NZf6}~$s`ZTb z*>k)8*c2$= z_JAjug1mglGAZQ!?|r|kDDSJ&<-)DTtC8QV=uS~!9;ndhd#Rop8F}w?*8e`eSUqpy zfbI-yVj!#|KBmvf*dbscgDU~l-<){CCV>A@!v2u`V&UvW+tu3{o~BPl z^kMwHpS4sfLPn$Godlkf?bC353X1HOfZ!kO%JFw^(k*~;xRbT+Tj=|BlZelZ&Fw|2 z*nu!04Fqd9QB7MgWZrV~?!5>vC2L7~q%?{s_@kE1c|VM&n1C(OH%y&R z%PUN$tAlU8TKYq<>RFcP5S6pk6LK|#T>e}RbN$7{^+UtsZKI>{;p2a4@H|acDUjlj zq=W=Q201q`V8WOxrv)(GmAQ;^UJA9b1i{K3#pDF(Av4ig0oVHoux`TcY$BqG zZ@djw9}c;pp%rKFGW!-mO>du@JZRwrbD>w#*?PK#ySOtLW*&(Vzc+s*fAV!cnNb^rhE7R*@zzG-TE56WV$`Zky_?}>jRcn))YR)-skemL7tG40G zcp8WRlf1M)_@0c^K7^Ic zZx-5h30rOaef+x^Df4Ki$}t;VMOY*DikgO9SdTksLs!&Z+t;J+#B6u{ltqs671iq2jBa zaswbWUw;P7R&QwE2r_eoU_|?rI!E>UgXTa|)cJH0In&`WRIItA_nBFK(1Cb>i%8eE zS5dK{&vaTOn+s}pZ!*9kQtWq|eOb$DEyJT^(SddvAxbchp(D=kH1%RVo4evr?C%&6 z%Q0aZf||zYe~0oKtu)oL$3)6UlcWo68Cw=uYTzYvE|wSdkB@qkAcq4rT`4xGN-Ms+ z5Y&7%k9kMbo~hN3xQtnPn~Y_swUIhU_QY&=h;uvr%@URUCB5J2a^qrA@+$bbTmKD7 z!OuN|GMJ900<)2hz?WLelTm~qbRp%stdj=AXVxqahVP%>NFWY}#tM1^$V!HdJN*f` zcr|p8sl8Zi&%0vn^%ee6(UAAgXaQ=aur22R(yTscm1V@m;h3P!y62sLE+{ZzPeE4I zXz9oH2+%N8$SGpBglU+PB9HVcV{@YI=2nUUUgSYTcXR^|IuNiWnv(J^GOGw8CB%Lh1Ol$ z_4I#k+P_@vpm#(s`&;)a{rq0&E&uC81N#S^Nq#n4eSKeDef2QJxZ+UhZ-v|+R&8h_ zH|$%6WM0xC{dZ@IKNY?D*Y^5rH1c;wmSxI8o^vM5@hXkUc%-)BIGm4=It-wsg z@>wrpq(#5NmJA?s{+~C>`kFcrH&`kX47C&gK#2>mJu$ZD;{W5hRy*ss8D!AApG&B8 z?_xv&IBoy0r!!jKF7Y#Alb?OJtvPmtP5)0->tAEga1RMrzMPlVQ}JBQi>UCq@y{g= zeCW`yKYDz3*9A+Dy@EU+*s8pTCpCR5x)iTY*{xSeVOZo2wdItyCs(|Hws2zNmSi+@wBW9ZzotbAXk`{P%xu``$l2 z%?VJs6|PEBDtB-sIN+!eAWRC@KT*%&R?eZ(%h?E&Qz&3jREd)wU*9+f>jInj?-rf~ zW0EJnb%^-*v0)(qiF$Z|trAMi)!)bLv>MhUOukHxUp?_cCb`&`3o4?paO5!W9m%4V z*vJ#PoP1cr1pwww6h%!Y&5w9hQ4@yeeK!71KHbzntM$*f^ne6Y0@Z;$AYP%Z>Hxoo zSuIZ~9thyaD!?vMeZKMl>2fF{g;>Mo7JhyQvSx=vSfEIFIv+X4XZ>YM1sjW3dt4VDQp{?CAd_Ws@7 zT~$zP>;a4`v*utt9@91m=-H2=2gYFt7B$oD>3V~+BMjiC^%MBWw?eW>rNXZ+4Cu`w zbOIM&LCDGJkU7~3kGQJYRTIr+-d&@=qj0FMCsp@sScM7bLh58p8gz&rk4Ie-TZTv)a<$DT?15gNILx&a-b7* zW!@)1)n%I_;z-^AL)AU;zt`*8J#9vS0N%h$+Ssr;UHedC3cTxh z+;^U%E#X;9BS|)d*fD0JH|x zhK|(-B=gusqiiq$b`XJ&5&9$ZO&(kifMTWXaD7ArVR3%6iB{sV(MSehy}c;`fBIB? z^Ybzm6r)|I-m_f4+DSTI>h;h2F-YL$+XnhcL`TGLV1`o=5TJ+!?b4c?oAYacB7k+S z$(M1efeAg2?+F?9wEw*-o$kv;zed@ek(c^AZ%~w0_}7Wsp}pf!n47yG^Oa^2}7T7>M{@5i5K_5*|Rp;!^I zAaFrGSUE_|V4P8wkC}TA%M7P^N)5w~SpZ0Zp^AIdDF)iw7J9O0_x>J&t!sF+?6&?n zfCsIHeZNOGlAJ{$fn5b@VjI=v3}i@}t!*&Ou{XrVW0PP8(B6s59h4|W#p>I3_wZgbLw`@OM z*tSMeSQ+7A+&=fE7Lu#G=12nIb7ke_W09Y|j_rDV!XemapcQAZ>QT^!xJz`>T%7V_ zl}!kRWUerW)(O(TP1V-6=w%lguH4P6KCwasoJuL_e#Y6pM0IJxwKD3ew4RJ-wa_aNuh-af7o=Dw`Z3HSB&2$s4#ZSiH z)y1YOKLrqPJIgWpmx2Z`!%e^D(NQgrg*H5mKnuZXwm!_wkDUSX|9ZzA>2IENnpk&x zUUntXRDvXw*{D>2MQ^q9vbq<0i2vm5-5&wi57f!L9h1boD(d!oL|8o+YQq^sMfI1u zQM(Bvql4fu7L3ltR;5qP0JCNua&K`15q24~gfFLl z)%!@i_LgjZWWaGbI3+Z}-Fisq>bd_hJ^421!@9)7#B-T+yJi-gZ*uxDk_N7QJKRJR zDA@9>3LNe1it7%C^;EUA^48@3@$LsFD`t*z;h$VqRm(r(#?W}5%zvjCo2Pzy@-yh` z>|G3cxnt=CEEO8>)2~Dr>6S|V>Qm9w);fTLmsoKU3&q3BDz>)$@Ogcn&z9l{@3Sle zK5W&WWOc8cMyOHoY3x|`FSGTZ7DBwAoL9*sk1wGNWI-)$kW z*ac0F_9#s39{(LJ1W!fig`+IJb)}CF7ZjK@yiG*IgxX`KAFQ&$h36Tk>H0Kf)2^-U z1ktp`MH0^W+F3*iAGC5`{lsAT&W&8Q@b{QaJNU;Ai@KcO^4`P!)%Kvw`yNgKzNJ%4djeXhejD5#ditBY_?1S>RW_)_K!Zfh&37 z`NdX1z6iD1Xr`$ak;R(cx)~tPj>Xh;R$Qs9Dj*LkzU2)JEabJb@R;i(Lop~$eu%(b zY@X$8oN(xB-iwM4F?le^BXaPCY}?o&ompBDuUf1ryh&V!TENjcV*lMzeZ)^jn=>3$F){Vq1E;_dY0@gn8vK5Zb!qV86 zMI+_?>ZZBe_8$Z?!wgaln+niAGZ$#x)N$?>40}69TQX?z)o?>_0GD3HQwlmf5bQkq zPw>+}B8}b2w=-v>HA0~)xr0^xwca1N=vv1=ylz>g5@_R$wr1xENUi)uOm#KtbpTY2 z`8H0Eu0YCVkmy=)^mJ{2Tkyt;9DeE4>Y}H6^B$Dl>`l0_2PIGcYISu4po!=JYVV8A zUp0e(yTj6nY;|H+| z52TPAEq5@AAD{wlfG-x%5XV?Z!AYH^tzx^KC$AAgZ|z!?JSYek*ve$0D&l#+)f%k2CeY$Y{Iv8j!5 z$5$UiSc79ZrEV*Ssyh7V;6!nSKrt}hzQJvT07KiOT0`gTOAV~5v}^3sRWr5t8KXmc}uj-`DZ3_!hsq){>EwYBZamySkL(yM)v z35kAVm#9){Q10o*==|%QA+89rLlA>%ct!8D76K)==b^07 z*+-o(Eoy-#2NJ`I29bb9WTKCG=@5SWtn8bsi(mi#e-MJJY=ElNgGR$$N<)v&g?!G+ zSY^vvYHV;_+Y(&nTX2VD6K|TPO7XRkzG;F&Z8(RQA$jla5*0R@zOctL#aR@2wRK=> zESU*}x&^_I$8EcbLqOc-#WSAO?awoR>j9e13=qhX^BwbM<|pK6Z)<>KieEMULSM_6 zr;sW8LlJqf)rCXyT>g7~4 z(iC3o7F*Sf4|y#BAt>upSa! zDCWvY6WfyNlVp_{cEcyZ!8FzW2Iq!AZ3V;n?{3wCVJpl+P8juK90Ybb zLTCWW1A25W5fhKdfx}{6v<0!a&NWU!jWQ!<} z+HH^tdnpIMX2PwcQDDVAUN9}6?6G4PoV8MsJh)wIJe7*kwq;2+ zSeqC3*f^DMaso$1)v~H}>IrV8o?|)lo40C!uW2k}3Q9c;_E*9E@%GYd{oTGN1;HES zUmka|ONnf#N%~zp$EKpytfBa{r6Nf&K8x9T?GSif7an-?B?TF}<-N)0UvN)61YVYZ z-#-ButBI$wFI(2@2-}#KI|oZ)5_0;@N!W%~2B>VABngG(qBTOPg&`bRL*TNZ;dRhD zO}oLL!uwJw@toxF7BDzm)aq{@QN&>GKDaLE@p%eg{8Z293a{PgN}|D({6P3>fq~$adTY zYXg=5j%xvZCU{OO$nF3@@SmY1ii^Q{wBD<-BO}0sUiLNn9I;m-^&@S{pmV&{dU+NW z7K0-eiG-u=nG$;J)X*Z=Z*T>#u(DT4Bu9Hz`(|qWgvk`#%9_nZn&&~$(bst}^*sdPp<1@>u2O8iV5;t1bK}04=1n%uXb@%6e zpmS38o%EarpvtkK(N4fWLr8|^@Hjnf<`(zdvaVfzWW|eIfqBk zi0e5?_#`yY!`qxH z9p_sf6EH|R-7<}{=6RIrr3Bm(69C^mrx`*+ahgn2{p=`rGchGWSGln*mRTdnN2=zj z0#F>!k&mr7WT?k%$hSQk*3PAcJ$jtvsC{QW%v2W0xlC6E2G1M>w{bBxivmvw1>qjv zj?E&~neq2XF&+M~LUw?J-#nIR zUI9w75-}Lst6UBOTNZE0;++4H@S~sx$Rh_m)b!$FKb*#d4LXUi$E8x0KTBvWR@K>P z3jJ;-^~;0d8m#EFQLPrt&th+l_@64JbWDPh!91fC?+Mz!I}kUA5J=L?OUH!oJ9`iz9(70Q<{%|3f?znHn^aX% zAtLlb{QalKw&U^lh1I%|FEiEq^4Z>vs2_>cx+x8e|70w$K2(Kju&iS4)4z}?)xSXT z!UN`0)qXLUdlcqSbtIOKvRW2>`Ta`%&uqd!Q;#*v06yWg*g-HMZ1nNi^UXrhZf$ecz$9oTkya znY|f0s!`7vIc#1h}!t>fhLB25di(h+Qdof$`u zO~$K1U%;4WC_FA=RErim{7It!J0nxGJp0YR)P46C=c0~|j;fwDqml(^SL15DP%ZrM zLG1AKcuT>Bem@D`{JIois~Ci}9|xV^m57XH|CZug`^B%Ju$4IYb{0lS_t`3 z&Su<7Ke6)p^!sji`(ic=^}ft5lUtx2RhkobwM#h2M_Ld^UeaT2TAw8{KRUOtHBsm< zf0zGaBHkef#X>?t)-n`~jg9wK^D;X?Q)rf*WG(PlClPV`XmwNK3>0MRY2mm<5o_kf z+AlDLBnL5*mmKc3OQ_xozTfY4o;x%Ae3jye(oFrOXZ-Q)?ja7@p}KFjXj=O(_J3_fB=ftVAO$O{m%1C6sJpVd zyTG?f$Y~*XaLAQWFdwi{{jT;iORa`j350}SZKZhwzF+_7C^LZ8H{|5x$T>Nc))>XP ziYhAE7LR*j;7PJ>nSEv(qC^u@)the9tBId&rY9Us)Q7;o8w@g=?P|pO6h3>Qm0N&v z%3$vM>QMcdQEEd&+AWqg~euO zZm*sL0DB?Xgp7<#2Vl;>KjJnH$=J%4V5|iu#e38_F3L6De}F z-HpvB_uU|ZZ?dv;sQ(c-;y|q#p*Qe=-drO^S63@Wox@hK2Nptk`~P|RBwVeHiH--tiW?0|Gsc8`OFUh zMv1@-pcjo^$xE04o=e+kf*^rL{~_t*ITl{Fo^++yIl}X%GU-;cRl8WpfJ3Wi09HQV zwnGjeDCwVzc~1xd*;5BM2v_ z@`g-l?adg$b0CChnGb~Nb4!ZV37s(F_DPvC1(g=ob@4&MrBXwLXoZ#wtBhwM>Xe&Z zJ0xX{N<)N;Ij}n+?2{3TV1OauDd?&E)1HVkyq^iusUDy64xj9NM=kf@Mdw#klw#S6f#X;!a}LQ@qvQ7d1M$!S(P751ho^`u3B(<;8IW9}H;Mh3N%r z2jSA2z_$&pXPMrjxrgn(j)22SVHIF@`WWP)mbIYA5}mZ}PNr8%!xaMdg8z1HydsA% z))fGcTRjGrc&V+oh%(E|d7a)by17@Q-(g{k9Lf3tm>rr^5083?cdz>bi?unJfX1>` zoHYtu{gb<@lzPy_XHJxx{Vaok@gN_AiqDc?ZFtEVnvKlrp3z;!wCn!r!u^}jQ5;S} z^2G3p_yh5VFa2-V{$xRi-32+P_w_JvnP2qm7C&48T2}F35K@z4y2U}LeDLhkc_z@B zLZMJ^)@aE<5Lo^E`kUZWEQ|db4@CFtZ=011oZFa|E~az&Z{ifmIGLG(!oXe z?Nf_;zr=`PcQf$KdL5-ayr*wFFV{k2mi7BXwJqf)b!<>=N${{;E!qDF?+F>XEy-r4 z{%B7u0pB(UA(OT}>)By&0TP{5UGrY5IxmCQ99I4z)EE5nu$V>~csQP*5zu&eYO;{IrPbU?dc(4S$rQGCG zY@z9Wj{K{+_ZLg+j}qe4!5eu;&mlBQ|M%Jr;)*LyY^7o_c7ImSi^d;~@hH^dh^(~% zsib77&WqqhC$_sT!56NOs{ecjetbfeo9*#LKe4?x^oV+vf8cP|^oxBN3&UL(CFOcj zrn^VJXBofsyLi!QK(y#TLknL+N*=kH`MVHa7!CL+Y@>$}a)%)67ci5Xk;(X7XiT|D z1gcJqi(N?L-{3<4o0kSMN>n-X_lI2M`}7-n!*+yW&tn33(H3Wp8oZ$vRiU8q{r6w& zXR`qQSyx*)`DnUceeC}JTNw|J+LsNU+DV_zHskR?J)v>x$C3m7_M565nMGCE!Wi#o zUbJ){FM4M!G3e7V7(My>c&m$8I1CzIrn=7Ix8)TzumN4sijDKkk`lztI}g6l7Ay)o zuW!G^O%`&Jx0!7yFbDkr+lCWawnvZZPtlQ70ydqPC9=KQSYJQYx)#^h9(13q*JxW? z_A^1(9hv~knb)V>9GYN@MG$xd0qKGrylCZ?h(Cz#O={K}y+RV6i3pj_i+?UVA!#Ot z(Keru@BlNHO!uhN5m?YtYxnRHK9cu1+Keq~IF;vl@?_KvfrxJH=y0)ECakgn>e<#m z{aq6pKn0syrGXT&sIrZFv;wju7IJors^~Yr5BG6GjVW#BuKg>*+iVda2U5F!9l^%R z3g&GmFpfQJ4t~37(!seru+ODsWg6n;7YiR%)K362%7xcJNIOWDLOh1-sJ09gTa>8(i4c+JNU$z(0pZtp zP>}ZkUAK``28?_^Mt4{fbxm7dS^0%2hTI4GRk;=7=iC0&KzSrqni8{1>LM_XDK!M& zwSX0({o{xGptq)G3}(-gfnGY4^%UKmD(Zp!13HN;KaSQX%jBIgoSWQbe+O7y;mc~& z>8~0gHeTcQJJs$7xk6&D+h5%E&NspZWn^S>=7Gi1fjMY>OOXM`#+1avMM7*?7bqF) zq=q)Zm-`k~kzm8on=X^1$NQHh)%l_?Y9FL9Y%7C7*qrXK+=5xH!@4K|o;jq|(GAo@z-H_gQ6Om$f)r~6W4P|# zLTnj5;|03Qp*9wiO?+=*aQ~h2mGJrE;>T{F!#R4ejehF^xBvlcNB1&|zGieUF%0sU ztA<&7xzt&2Pww-AR**LHdj;fTi}7N&>lRD2e2XZT97&}~j`~k{%g=sZbSw0qWu`HH zrT}zXLnVOJ^ZwSX0f%E*MVo2`M&Q^O;`I)GToDMtsrewQ(QlcNTW+Mwa@9QklpFN#R;%4sq4lFLa{lc zMr>tk*dI`AMJII`->WGftP^;m*N~dTCjF4PD}NBb)ZV;2nR=o+{!f0>r2!-Kja*p9 z#=yszPI-%5LlF5k|JpXfXHhu#O_z-DPQM$sO}2kESte2+-bw2lE6^1+)tbABkZJ^T z{O;X8fCgz3KXbw)9~~W?D-OXm^d1wtHn~(xO`vf7;QTTF!d(OdI~@+4=eDo>zMc4_ zB=YunXJ4Pjypb(QI~w3+mh-j-@Ql>kCMuQVpp7drM-q+WKOx8{a5MbnMZZX;mBLGZ z4de`8hS_-)Oi(Oa?d|s%V*A|7k1H#?!waMo!eWTH_wPT7Hp+GhD%5_IZA@vD>FWMn zB46mu3$n5w<6Zl|0&sxP*FS)FgL&Yvht|1DD{ax?pi9_U6~bb)@rsPRkj!pEr@X~y!B~x0tF** zI^RfH=+=znG$$7q2(ZoR4D>!cu$i|{T@wXU@QLUKgSxg3CJDgyOn}Z)Pf=4+vwjL{ z@AO01^SqLZ@#N+x=xMYDTQP=&_4l3@e+K5yD1a)f9LDK~a;tQPB(tO1rqwlCScy@y zv9id83d=Qu%SMaEDp-3{X5?;Oa9ejd_YRMpLzw5|miGW7N#m3U79W4rejJgqJe23r zxX~BD-D$GoVLeDd|Jl9XeIbLw#KO43PphXbIbEdHafoTp&j%Su#Anav2(hw1Jn7y{ zIOv0>%p-r2vI=oIT$rWTO{`D9ut?40n!n5FWWmT`Ix~(tivQZ<{Fj!(#{&fsM!FiB zB?_^uwWrtiiUxS5)3{EJC_bm&OI6urE3;m4iq6$?Og-)rHwx?j?31wU+Um9d*-JXk z`5<6GS6-kX(9Xfr6V4TeJ|Fe8-X~o8?D#|Ea?d3?o0Wg>z)g=^c4pAS>yoq!ZBx;* z*Jc&+`Bc$Z%=)!%9dp9%sbAE4t?5740)d9Wag)`@;_NlYZ~YrsXwl0O6D}|SdUK|N zjLD!ODarC|Yuo^6RuZ+*f_AgIx_Y)FJ9ZGuZ&|c-(4Cc`)P0~3`tL;5?*($(l-6K` z>p-BcLujSY4sX=3>Eh#;>W^RYp`tQ&=YJ_3^{|?4JjXw=tC*rLIZucH|so z7!5=^xXs+x<`#Kzcu5KU$}ac7b5aPRlNG5P1=t27GwB0anO(yCSE;p3>N5B-0eB>b zCbS@RHf3y^=>;6=Z;%HbaNyoI1Ga^KG8~B?VM9=92e$NHu89dG)eD6Y%t`i?36N!%{DL%VKruSI?2`?B{m67ZET(H<};g&tc$KGat_8y=zcKY0LPbfrKwO>C_EjNcHcfShGcbfRP z@JQ7xV$Unc&`5hGe{hFqD2fH*@y8ZEQY&_(e@g=aW??R!t2nR&z*YD|Vp9>(Dj>u_ zz^DipDg(+JK>2BthhPOTQ2t%w;at!{VIuTIA#uOBy~9?ae~bWwQ9 z$lXpWVzFRL7iQ9X;c1`=e-y{3GfmH!c>`~mh!)mX&O(;5rsLf>ep_nt1vI7{O^k3S z`i!m>A8IWLEj2eu&ZnbHFP%-&amdl1By?&bQzb{_b9cqVJ5+`RJoDl8paSaQL~e=Cn*B3%8eJX? zl@H^1gs;Q!5f3~Wo&{V-1@d@ybv%7gwEdoy^zRTc-QqLFTAVzS$(^e^v$FH*#oZ`2 zyS3NGBPIGUBwAx60uZ{;1%C?xS>C&bO^p?nnOej7D$U<)nFeoF%@ z2QTL0)xYPuN|-F_ih6-GmSbv{(6*9U9uhwH-e2?Muj23C0Fl$NEi6f)uNhK!P`x_) zTY9$`^;TRg)hLUQPJk1!L`4z^A+GffUyZS$eo00C2+V#!ooov+0N!qZWzHk>sIhkb z>8oe{6zC8tKR;uWd1~%g0YX8)e$CieSXfwE&}OW3Ml8_D8=p)0he_%~#_Y3KQ$FYM~md_IfZ+0RhsG z8SCBAb>UFe`?}C?Zg+=N>5_YnfGC$Ku&i2r%FHbLGxu6Y_jgoLIAjq}eBw(>c~mF@ZkV3lze_}een_ZrMz0G@)6^HMu4p#JgAW0zaJP-z zARGFQD8qWXB8SQjlsn3O_%X3aWX7gUaKOD&v9}L|0TOi3Mk3k!Y#_Y|@OS@#VCnzF z&qV*i@-rdJ)X(|~Tk-ZlPDNZ@Svss{{&`VtLfsu-Cji+ zF%avXIJ9FMz)f;a!H~Wts(2YsVczcj+h0~=`G^kVjt?4V@#VdS+O*wH&LR=_p=%Cx ztE-kLfDly|H?{)xsM4GPlsa{axcLj9AtjHb7d@3-(5eYu^sW()k*A*u^sXuqjJwUq z6G-J^OhLCfa0KAiM`gM~<07@cu!Z(yvgIPypBKQFIzhJ%S7(qK*a(%^Nui3C>S+cAq^ z4yM|$8lSU@7oys{43TSIQtT4bgFvInR8zDc_GwHM8H;<{eu=M^v@C~9WYN>YPYvrs zD{dlCz{5@fa3{kgB=zZqLiWGV&iU7?5PiOJ`+oK(+jMGTdPwAVK4L_Yh+8QerWDW# z3bv98!uXfU0eWB06_LFZ6?_cfVd-%vMukM&NNM0*acjKU`{68ABokYZeF^C{sC+E9 z$WVGw^-gPJtUJ;MXx&@mElz`k^M?qQMZGl;dVf{ala#?;DDTjJKPS^2PHXQdU-$BqV@Lr-lvpFkr$0CL z8V(>M)*5ZjSXREF=TjLbz0RKG{&1p&n;jG5Z+GTo2Dlkv@H@UCq>N%jOn7(;H?512 z1V=8-+;J1yP%9u{w)#LqHH!4Q_Hps7Ts$@;@rS|{y$fhxl#&RA+>d*}I9F~p&Iwhj z;WEK<^e6gFEif;Gb8EmDsFhpl$8#CHxPDVo+5=Mej`WrI*fTmW2oR@M^I~vFnC?#z zhKh>n#bUv7x9bj0{*6CKK9oP6i>iY59IFnVU@b5^aFHrBXDM3P?p#_i$%IA~?rS65 zuPMd$sYYuR%y7lHyjJMl<;BP{auP=D#%22wI~c!8Zp~NxxbN@AMiHp&I5R`yNn&~f zn8Eb}{+DRaaukKi7C;^4pM8l+Eb_U!l$W)8Z|WJsY;>gtrqIk3AKT7|d=nw+(4xdk z9CV6$+Ib6h+u;!CxMlyjzi_i>ueevG@(h3#qCMw!9~|}rB_xZZ^u3w`@K@a=R)5eQ zBa! zoxtWaqdtlfTesSy%Gj1oTrl9!V0G)6&x3BMhK`uH6bd|}LSj*;=kw;OdL>qgK9SVJ zHRji(GNzngFyKTO(7*M01m=Zgy>ck?W}eC>{sdyPWG`z48r}Db(xK6@t6Z(eR$Qls zSH83c{#@Md`q%bkrh#N9SYbTzG!Qj(eS93NUZh)QIUWvBXN@HH##$c6DFxKEq2y;= zwed@6SD%(Mb3MuQ@`EPc<*57YN7ATZfOq!*KB;KWem;G@pvkQcopuwZ=Ai?FG+p-~;q|~Z2Kk`(ema-?muxVk z9f5sIDyL``&A%2ts3B@ZXEvAri2*Gwl-ws2iQa3AsbkWOZ37cVkyaYd*PPC8c`qEDL%xIulqKuiig}IkvE(rIb=8LqC-iBJO&=> zk}X<7x^!_1EBmV~&*DVi2bLg$N!PUQhrozCd5yE~Yj*Hx%yb%HfzNI^9f zIH06@-H+=A5y?yP-M}>oyWF8Qq=YHe{2w5ILdnj?n1*8~Mf$eS_QPh~^BEMJmAhy5l25u+S`{tStV3= zRjIYCtJHh&hXZlkqSSi6(9gS!I2uYGKfD<4BJg6;hsD_WJWRnSl(38!F^(=y3?$ldtVi;$qS)Vy1k(N+Prk~)S$I1>Nk50<$KS-tSlmR1wGV7~vStD}P z7mpu56CRmwp~eXKGEgBkg05X!hW&{wV)^YJa#N;PVfW?NUrE*Z*IlQ$1j-(}#ZJC2 zl93xAbSw*tNqw|BSSSq6NEB9LXK)`mPR^I>R-=S`jWLIDQG#Y@YSq22+-U5PxOWSr zq1(6&DoK0BuaovVZHAMdIb{ioYp}EtgLBiY%!ZIT&*KV@QSF?D^VJI-%FzhDg>N9F z=5S#d^vY3e>^&_!Vz6&S1w&_X7-gcr0RekO-ae>qx-%UIQerd%--T*?@`tWH~m z-?m|O^5whs8AkSH=~~D%)Gn zIXh<1Tre5PAWA5+GWeZ@``oCDzpvWcwy~qFiP*A;_3~*DbTCk;&rSPdIj&LC9!68) z?_w9IH`QSK6u=yAhWP7U_)AFrOBvaKzt0kXT<=2O`1U;pm8 znMpu$7)xX`1)mLtn8z-ONJ2+T;Y_}9Yr)N)zonMN+5H&bA zjE&3Gh>KHT-7NCR5#i+O&b{8fd(CK;%p_WdR1$Ur_3jn~=Q=~;`dPyk{^U`1MuVq9$iiGE?P8u~TpB+#4ak7?^&XQ|Iuk@5b(>RfZ6iNq<6jJvbiNaOPX=h?A zCMaJx{>Hn!w!yoswYG}P4p1rlDp+)!u1{(6k$2gfEHkgMjyPuSbChmdGRIEp73V{2 znNUU2uptP=1InnBNdwjiXWB;=kD|%C@)q7`ynKFQ4p23GF4D|t?0hpRUJFRy$)%M= zzjzO|T(a?Z$8}jH$IZdY&!8hg0xTI|*D>#O>8SNe!6(|FTY0r&?*Vu?d?XPy7m{=I z`Bu#>j?&;_rIML`6Nug_FnQgob|+6w1g1~*0u2;{>bXQD=a{G{(XV2x4#oL~B` zw9uDR&RX%~qZis1x;b&Nm74JHY%Shj7?i>aYJ7v#E!N7^tm7+USG=G3bihA%-DMrC zxS*e0U!v|Z??(EKNp)G53_A(utmc>Y$D@|>H*!Zy!#>FW#GL@by0+n|Us35&s+8B| zJq&_rrLUe6_8S>QiaI~cBB;O(#)Xy%Dx9h0*TX=q^6WWs^9{Xt(vrwtu2>f17X45% zxp~rFl{RN+62*^Rukqnz0SnczRNaK7WX!V)AW8fzMI=3q6*kpEc<+Wo)piIqmng$e z?4M(o@ujR@;hvwaQt}nGnG%(>NpuhMS|LSoe7*;z+}35?H(KYUtWk%7D{npxRput= zwPwQUh*5)ry z$6N-!QNDT^nNADAU6yH_=5hIR=zH!zMcCgswMsYN@zlPsrttKOV;Vub-v(W{&)DG~ z+zSK}Z-qxAr%9^c zLdBZP5dyv{^tp|_q`H|^J3m=M|D`fV%V`=1g#k;wfBjgsd!gJ&KQThcz)|w+fR}dx z$JFnm7kPJ2>&)?~R}=q4H-)Gw*yepO^O?cgz(sI^rRb7D2bKdGbFcf8S%Km-vb7b~ z)l$sp)xFItq+u3eVOAlUDz9KzyoBdP%l?$t6!rF7kbCvN08_(dOCQVx9*M|i z#Wkf$T5xUMl6Rzi@Tl)Z_Ty5Wa>I-+=Wsk(cle`lkt@+>fbtUx=4k(V!lDwe9Ig@F zI8EQpSJpjC`}2&9UX1{;LjqNdZYdWWsa229Fx5OO%zc~a%^(avew@UD1#_g=&FVWz z!pT|5$7l0bK?K53Bp!T zAg}s3cG)z!vfI{?Sn6r?p;tHg;THR&sQ6c5qE`hfaYvVGj|&qJAoBiN3{6oukNfU+ z-6uNOVcViAwOtpu6_7J&8JUe7w-*Ru=R2w}uPy1$qI|h*atfk{L?0!Ie4M!npcv%X z*o&Xuo4}UxU3X8BU%b?>v>(g;Su80E@CRTUJ% zK|9)m_R}4$vM>s)Dsc=kXlR=1T>Dc%Fe@39M-ZgQ0rBQUQ1m_+pP+}|0j}n?x;#8Q z>Os$O1)k*_Hy?s*++?QCjqQ1reFM-jp>)z|@T`_iwfiV*kmWOiv9>m8^1HDMpq_58 zqn{6f>Dvz&F@U(}a^h1{IRXC4bT;LD>q$-Y4uy5ZkXoMnf|J6l*Bo8(nV*+B<5Wsb zw$OJ3E#*eG;&=t2t1b$;AAkU8${vsDHz;C{e6j0Fo8%yO)H~@}qZzd|g9+7=S=UuN z&TX}kFx5-tPsx3Z@n+$zjm)aW)0Z0>pO0hUp+imnxTZiPh3RZ`bac0qDe_Oe&P0dD z+O%zBfq_(_(6zx!fLN{Y&qPLl>2UL8ZveK|DMp4*!C*y3Ru-F9W!PTgo=QMf)l;|K zu7}^a&N&}F+RGQ9{e!F4QvV?Wca!7Ww?ifm6nV!Rjwb-F z;pJ5?n`BqkaYAo?O#kZ*)kG9mXgJRSR$;Y4d{@*#ss`Ek7c1N1o45n4&DaLf3e623+ipefoGRPp7HV+zu3$Gv zpm~e7>$W5}rt$F36z$_;lgZgZ!42x+CxnbLXELhywMuyv7K*UWDm6dZVR8Exr+9Ro z9lI`U5y`nupH0w6WWeyR<8U{5Kn@wii3lLmO=2@H2Eb?6>*{TVqo4LQ(8D_rS-LT^ z#_&wP!$uwXxaXR|hr}aqJhaI;z{G*jPijXy_ITuY(^|skG?tTVU?2DmrG()(`+Xv+ z*eReI1FJ+Zop%(Cu=Ah8&7ZXx+tZqQfGJOYvx>J7_T5Lu1&Ps9G2X=I5pc}+Y8D8G z3ts~KVNLKJ3CY5+iuhd%4>yZAu@EwgqLM;pYpb-ET2Tx#;x8~3?zi%Oy*YVmYiXfIVP^ICDU(C-&}VlROAb1>~017qs63PIT4;p{i-gXyE~j)EmCVNzOV!_VPX};ubr} zNtxzCv+ztu$93x;_q|+c8nHu=EWbtXG2$>Zi=lR1RrnFiu+z#nlPF?O*p$iY z)YP;KUG3=9og8{6jrxMYVxazLCeO4Zl8#V_Zkq&(#HwWZP3!CZgYGru(`myO*H_(T ze18xYw!FVP&Ek)jc6U>NtE!wg@36#T_} z0#Mu@(9AaZS>xoeI>MR~6ijsw(m6@@4)3SW)dkbu)Q&X^4*oReQMee>wGD)QjB>rE z8W3Fkscemc%1$^CCC-^C#xEj=aUWuM|_b zr&6MDb#yS+#`13In+TX>%^Z7^&=sNv`8jvA6PYusGeHYcuN1Bn5OIm$g2tQwAREpmyy}D$LDQhd7o`np>aqJF=kL^IB z(>|;r;&TmPI%+bUUgo1#<HVB=Iak?f~#e^|AC`>h|Hl2WjkkZE^$?q==3mlQGt*rxk6C0=u__$iCO zyzkoiG{?JRKBqWhl4QbJMSCBZw*Fl+!9KAlk)m5wRaN(w{GF7lwC6sF6v6J^xN&2* z$mc@A_C5UkorP;v*W80FBl@XwldhO3XKX!xHh$Mj!oyFF)(qWf1p5hYk4Az6A##OO z1X+3Cxzlhf3gVf}DR{Qt`C?PRl+sNKva-ATruzIddtU#Sx1&e7&&ggZEh;rjVf;f;saTZDv{V4$6wM9ilM{grhw_e+Cg1%%YrI@a$TjCagw&V@JPOQ2Ba`*<~y6unnv73@#*NA!SJVvq>=Lnd?a)2CpGuIP4yZND3cm- zr_5~eHgM;ICrCHSrX>^Tv*qI0FHH45vv$nRsaPdP2`{@n_6}j!*R7-N+S?dk@9vl} z1`?J^@8Dr10oSAgxDx1$GdV;f% zOL|otNy|qo$T2@>uWie=L?J-|CK(|scFF3gqU$)mlAn5MWP~yc8@(hlZ_fodz}o{) zR@~8V=G~;9-eheeZBSNlost*+W(&);>%At~@+5RfoJ&30?+qW0PI=17_sEgygzj;T zX=8ltKwh)T6>J0IyV9+<*3#Syc2+CWZ3U|06coy&0y-51^Oeo<*cvHpfd3l~;ndT>j&i3bshB0kMciN0I`r|G{_-Y!#aS>ry`O0}1A+M(DfDI0*;j7+ z{R(A-lv|rh+>dh(RY&)ICx!m;Q3|~$;(x|}5_S_Ic)Sw0<7q0+o-7`pNwL|Z-Du(Q z76xav2ktsvnzUtsmgU?DVjT=IsDP0E-oz)S2dkr}$2XCZ^xJQQOz7GgbV+3IWqt-u zxiDH;Y$@L89G3?zT597&CC%*EE;rTRnMa0_V(t51qEQRf;lxl+ir|%y*QcMGE+~K1 z)Wf>onMi4g-`IH@clJ-7#3FCJ2fCa+U>8|(yAw*Kvp1btR^gFxXZUwvem8=__u4_l zzFb#{Wo-J)n0KB{q;g)Kh;GpVHy;-(^D`!i1k3n3W|eg11l3@7)5fvRcTn9Z7FfI@ z)qXH;`J8|W&N83zcem%ME=o1m0+i@xY8VXKzHWxmEaujuArUJDH7VsHx7^&K>o6`#c$or5I1I?;B6@+P(RQGk`il(hptZ zXA16XurR`aF?;VHH;;?&N`5cr_KZ?VHd4UPsui4e>k9j{gL#arn_P*AxzDONVd|>! zFS(=x@Q3@+rT%M<;i_Y&zgUBU2B&Idy58lug%k?*-02_x#@838;zHxUg1g-8)>{?D zN;IBYJ@w&TqM|RQQK`N(?0xcb)3Kdimcb8_VhYqLz3VC^mJ6Ib9}%=d`) zumcogX|mH36IvKbq^vPLQNjycBj58csYWQ2x~AsCNYTTDLQNS1=0~~rI`OiG68EeD zIrOZ*t838S(2`o>I)i64e;f-#SkR%Cak+ucFb_hcvf`g?Y$@ET;)`>4+wO=2IM%IN3yy|nm$p@m?Nz1g|*Pb zM#(9^i7^_6=u2i3b@4bw1)yrvG{4?pc84IVO4&3!!Zud-?#5-@c$-2-qVdH5l4Upc z*JG%Jq#}WO{%Q}Nn=9HNoI2L4dbY4-q~PsczQSI*RwEb;huxh;m#dlCQ%lbv8&6oEe(QR zcNFN+Jz}OxxtH9Nl1N~BZDWueCBxF;rfBe*-6nb2@dN%c|6;cBD#}N#L-4-Z?7Fei zy+b9hBdd3(u9tKbeGiIlPn*`vR!_|>NoA~~aanb2R%h8O16i;-Z-|*l zvnf(A-eYNb%btHAsohn1NJ*!L`$cJR<`>U;I>%|7SVqQq_QkaJ0R9w&3vb4`a*y&O zc^2)I6!r5U5$S4+;I)<{YBR6(bg?O;*%7+D0zvV@rhWhUtU{`zwJ_^)tSt0T zLKwciP830snErVc`07so!6tZ=66MeIM#zCWY9=uz*24vkEmOv6)P^ z=&%&=!9wUWZTd9@rCMP-AQ{Q%z`%vKa?*FMC zS>#>ri}uymaN-Z2E4Jnys(uV3Fu66z2EP|0l<>yu^(r;b27_QgnK+!tB>PzFAcW=H zg07ctM-)E|$&EyZifX{Y{CEvPuDI;*ijiC3){u$t8TU%z#Vyr%CaM(2UijjMhKmR& z^l>ErInmJf?a!}@l{;G0#S}~O&eN5@t_rRtERbrQjSP=G#HY)glw;ywTJv|kM{Z`h z-!r-N`rpv=KZm{W+7~Zz`6&aVh673`Fqf@Ra$LN6ZT8N;AZCewe;n-I|BW94pnZP! zO`Ldxt_&RJ>Y^62+}P6vp!$1(s0C~cvH^yeLz&Ony2!p~yc@Y0fMF2Wv4s;FSuHZ%JzXK)ouM+|hgF*HGxge0Fzu*8C zc@q>Uq9m?<6(&ciU0kfp+r)6JH&s79p+6)7y^jqnC2XXOr~>W zrQiw2d_R^>d?JYiz^~we|C7SA2V4)#SMUw_|0_Sd+FPFe*Fj4a0)5B-e7HSkYPMj2KpW$5?+0qoJvrT_o{ diff --git a/libs/clipboard/docs/assets/win_B_A.png b/libs/clipboard/docs/assets/win_B_A.png index 377fa801f973be79c975c4337fe0817fca3d2da3..4484b21ac6c4c8daec7ed6ecfa8ce676a459586d 100644 GIT binary patch literal 43515 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 z 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 R<}L6~T0&mDL{!iBe*rIU#v}j$ diff --git a/res/128x128.png b/res/128x128.png index cd35a0bc80ecea70e8ecc5cf41089cb5afdd6111..26cbf702ca0fd75b8f56ae3792ac812472fd77d1 100644 GIT binary patch delta 1210 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 Hu0mjfUQ6x9 delta 2988 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 delta 452 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 z≧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 literal 15849 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 IM6N<$f@tP^f&c&j delta 375 zcmV--0f_#S0*3>T8Gi-<001BJ|6u?C010qNS#tmY0H^=}0H^_Z^s~kQ000?uMObuG zZ)S9NVRB^vcXxL#X>MzCV_|S*E^l&Yo9;Xs0003LNklks|&B|A7C%%4SxgC}f~0CKlOD#v2fe&41k)?oQ9WIQP_BPv2kl zyzle!vnFtW7vwR7R~%vb7yWm&VHf3aW-ymvC+JAct+-BvC5&P`!OM6+dkol01cw+1 z{2=xc&3=u{;WH2$4aQtgLdIY-02jzK2!OMo--i1D%>QGC1@(8~BLKZk1oQ{}dluP1 zmYWDDg+7x-A#ecsCIWJyFJy5WfYl}fmP3C7Q|g~)rGa@Z(N5Ng^8glA7ON*1`>>-j ze@%BBlRNR0q!%%wvMi;9d#rlusvCESa287mc7?9LW^Ad>d5Jd+tNe*)oMKV3sb9v4 VXG`~lp*jEn002ovPDHLkV1i3-p3VRO diff --git a/res/mac-tray-light.png b/res/mac-tray-light.png index c3e107410ce32019410c055f63885874e97747cd..ad8bfa3960d06e2b108f7fe4cab95a89a481a38a 100644 GIT binary patch delta 253 zcmV+_A(1OFPEseh85xH83Ft7_ zLY;umhDDCiUIDf`0C;A2l<4NTr~a8^x?gVFS2&vj^r{wZl8^AF00000NkvXXu0mjf DL1u8y delta 460 zcmV;-0W z#_`V+MdS}K5fevJBoiHjLyj0A_bAsXWkN;<%FKikQBstUTXIPsD5XrrV=U_#3x+Wn&&Cxxh80P^c7W5^-!t98u_UkC28(e7TYH`x zNw%bvG7oQBsWpBy$?@^MX`q8R*^3QVzzG~`;uDM}spbE7l~NvJbqk)UfLi9lCTvb} zbE4!{MLou~=6@NhfFGFFgrOww2LKkJHnFa674Q@F7V4Utg@-C#rWT&RFG=#S^FB$p?+#yz;$i;Z@JRHtxd3;f1e zoa*{fN*Tgo?CYi%_mZsb05kCtvwQRkTG~eWMK7^h>P1bBB>CKbOtV^Zmh>RCAJcKA zr#;1vB%iAG5736BzPz2-jYU|6U+tm2!X;eqPwF2PMw2s<+<@W$0000 Date: Sun, 25 Dec 2022 20:21:13 +0300 Subject: [PATCH 1253/2015] Fix typo --- .github/workflows/flutter-nightly.yml | 6 +++--- build.py | 2 +- .../android/en-US/full_description.txt | 2 +- flutter/lib/common.dart | 6 +++--- flutter/lib/common/widgets/peer_card.dart | 2 +- flutter/lib/utils/multi_window_manager.dart | 2 +- libs/enigo/src/lib.rs | 2 +- libs/enigo/src/macos/macos_impl.rs | 2 +- libs/hbb_common/src/fs.rs | 2 +- libs/scrap/src/dxgi/mag.rs | 2 +- .../dylib/src/win10/IddController.c | 18 +++++++++--------- .../dylib/src/win10/IddController.h | 2 +- src/core_main.rs | 2 +- src/platform/macos.rs | 2 +- src/server/connection.rs | 2 +- src/server/input_service.rs | 2 +- src/server/video_qos.rs | 2 +- src/ui/cm.tis | 2 +- src/ui/remote.tis | 2 +- src/windows.cc | 2 +- 20 files changed, 32 insertions(+), 32 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 5474183db..17e338edc 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -377,7 +377,7 @@ jobs: run: | ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart - - name: Upload Artifcat + - name: Upload Artifact uses: actions/upload-artifact@master with: name: bridge-artifact @@ -1012,7 +1012,7 @@ jobs: files: | rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb - - name: Upload Artifcat + - name: Upload Artifact uses: actions/upload-artifact@master if: ${{ contains(matrix.job.extra-build-features, 'flatpak') }} with: @@ -1188,7 +1188,7 @@ jobs: files: | rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb - - name: Upload Artifcat + - name: Upload Artifact uses: actions/upload-artifact@master if: ${{ contains(matrix.job.extra-build-features, 'flatpak') }} with: diff --git a/build.py b/build.py index 127469784..ca91b3581 100755 --- a/build.py +++ b/build.py @@ -21,7 +21,7 @@ skip_cargo = False def custom_os_system(cmd): err = os._system(cmd) if err != 0: - print(f"Error occured when executing: {cmd}. Exiting.") + print(f"Error occurred when executing: {cmd}. Exiting.") sys.exit(-1) # replace prebuilt os.system os._system = os.system diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt index 966ad3df8..1f35ef92d 100644 --- a/fastlane/metadata/android/en-US/full_description.txt +++ b/fastlane/metadata/android/en-US/full_description.txt @@ -4,7 +4,7 @@ Doc: https://rustdesk.com/docs/en/manual/mobile/ In order for a remote device to control your Android device via mouse or touch, you need to allow RustDesk to use the "Accessibility" service, RustDesk uses AccessibilityService API to implement Addroid remote control. -In addtion to remote control, you can also transfer files between Android devices and PCs easily with RustDesk. +In addition to remote control, you can also transfer files between Android devices and PCs easily with RustDesk. You have full control of your data, with no concerns about security. You can use our rendezvous/relay server, or self-hosting, or write your own rendezvous/relay server. Self-hosting server is free and open source: https://github.com/rustdesk/rustdesk-server diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 46ba90d66..8d047dd0d 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -46,7 +46,7 @@ var isWebDesktop = false; var version = ""; int androidVersion = 0; -/// only avaliable for Windows target +/// only available for Windows target int windowsBuildNumber = 0; DesktopType? desktopType; @@ -1373,7 +1373,7 @@ Future> getHttpHeaders() async { }; } -// Simple wrapper of built-in types for refrence use. +// Simple wrapper of built-in types for reference use. class SimpleWrapper { T value; SimpleWrapper(this.value); @@ -1409,7 +1409,7 @@ Future reloadAllWindows() async { /// Indicate the flutter app is running in portable mode. /// /// [Note] -/// Portable build is only avaliable on Windows. +/// Portable build is only available on Windows. bool isRunningInPortableMode() { if (!Platform.isWindows) { return false; diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index 44f82575e..a98739606 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -464,7 +464,7 @@ abstract class BasePeerCard extends StatelessWidget { ); } - /// Only avaliable on Windows. + /// Only available on Windows. @protected MenuEntryBase _createShortCutAction(String id) { return MenuEntryButton( diff --git a/flutter/lib/utils/multi_window_manager.dart b/flutter/lib/utils/multi_window_manager.dart index 91cb9a08a..de6750a3f 100644 --- a/flutter/lib/utils/multi_window_manager.dart +++ b/flutter/lib/utils/multi_window_manager.dart @@ -208,7 +208,7 @@ class RustDeskMultiWindowManager { /// Remove active window which has [`windowId`] /// - /// [Avaliability] + /// [Availability] /// This function should only be called from main window. /// For other windows, please post a unregister(hide) event to main window handler: /// `rustDeskWinManager.call(WindowType.Main, kWindowEventHide, {"id": windowId!});` diff --git a/libs/enigo/src/lib.rs b/libs/enigo/src/lib.rs index 083345e63..a47f9558d 100644 --- a/libs/enigo/src/lib.rs +++ b/libs/enigo/src/lib.rs @@ -21,7 +21,7 @@ //! Possible use cases could be for testing user interfaces on different //! plattforms, //! building remote control applications or just automating tasks for user -//! interfaces unaccessible by a public API or scripting laguage. +//! interfaces unaccessible by a public API or scripting language. //! //! For the keyboard there are currently two modes you can use. The first mode //! is represented by the [key_sequence]() function diff --git a/libs/enigo/src/macos/macos_impl.rs b/libs/enigo/src/macos/macos_impl.rs index 937320f7d..1ae41f0c3 100644 --- a/libs/enigo/src/macos/macos_impl.rs +++ b/libs/enigo/src/macos/macos_impl.rs @@ -68,7 +68,7 @@ extern "C" { ) -> Boolean; fn CGEventPost(tapLocation: CGEventTapLocation, event: *mut MyCGEvent); - // Actually return CFDataRef which is const here, but for coding convienence, return *mut c_void + // Actually return CFDataRef which is const here, but for coding convenience, return *mut c_void fn TISGetInputSourceProperty(source: TISInputSourceRef, property: *const c_void) -> *mut c_void; // not present in servo/core-graphics diff --git a/libs/hbb_common/src/fs.rs b/libs/hbb_common/src/fs.rs index e08414324..fec8b8670 100644 --- a/libs/hbb_common/src/fs.rs +++ b/libs/hbb_common/src/fs.rs @@ -578,7 +578,7 @@ impl TransferJob { /// /// [`Note`] /// Conditions: - /// 1. Files are not waiting for comfirmation by peers. + /// 1. Files are not waiting for confirmation by peers. #[inline] pub fn job_completed(&self) -> bool { // has no error, Condition 2 diff --git a/libs/scrap/src/dxgi/mag.rs b/libs/scrap/src/dxgi/mag.rs index 78f14194c..0de86055e 100644 --- a/libs/scrap/src/dxgi/mag.rs +++ b/libs/scrap/src/dxgi/mag.rs @@ -339,7 +339,7 @@ impl CapturerMag { } // Register the host window class. See the MSDN documentation of the - // Magnification API for more infomation. + // Magnification API for more information. let wcex = WNDCLASSEXA { cbSize: size_of::() as _, style: 0, diff --git a/libs/virtual_display/dylib/src/win10/IddController.c b/libs/virtual_display/dylib/src/win10/IddController.c index 27dd22792..c1faccfc2 100644 --- a/libs/virtual_display/dylib/src/win10/IddController.c +++ b/libs/virtual_display/dylib/src/win10/IddController.c @@ -66,7 +66,7 @@ const char* GetLastMsg() BOOL InstallUpdate(LPCWSTR fullInfPath, PBOOL rebootRequired) { - SetLastMsg("Sucess"); + SetLastMsg("Success"); // UpdateDriverForPlugAndPlayDevicesW may return FALSE while driver was successfully installed... if (FALSE == UpdateDriverForPlugAndPlayDevicesW( @@ -96,7 +96,7 @@ BOOL InstallUpdate(LPCWSTR fullInfPath, PBOOL rebootRequired) BOOL Uninstall(LPCWSTR fullInfPath, PBOOL rebootRequired) { - SetLastMsg("Sucess"); + SetLastMsg("Success"); if (FALSE == DiUninstallDriverW( NULL, @@ -122,7 +122,7 @@ BOOL Uninstall(LPCWSTR fullInfPath, PBOOL rebootRequired) BOOL IsDeviceCreated(PBOOL created) { - SetLastMsg("Sucess"); + SetLastMsg("Success"); HDEVINFO hardwareDeviceInfo = SetupDiGetClassDevs( &GUID_DEVINTERFACE_IDD_DRIVER_DEVICE, @@ -181,7 +181,7 @@ BOOL IsDeviceCreated(PBOOL created) BOOL DeviceCreate(PHSWDEVICE hSwDevice) { - SetLastMsg("Sucess"); + SetLastMsg("Success"); if (*hSwDevice != NULL) { @@ -274,7 +274,7 @@ BOOL DeviceCreate(PHSWDEVICE hSwDevice) VOID DeviceClose(HSWDEVICE hSwDevice) { - SetLastMsg("Sucess"); + SetLastMsg("Success"); if (hSwDevice != INVALID_HANDLE_VALUE && hSwDevice != NULL) { @@ -284,7 +284,7 @@ VOID DeviceClose(HSWDEVICE hSwDevice) BOOL MonitorPlugIn(UINT index, UINT edid, INT retries) { - SetLastMsg("Sucess"); + SetLastMsg("Success"); if (retries < 0) { @@ -359,7 +359,7 @@ BOOL MonitorPlugIn(UINT index, UINT edid, INT retries) BOOL MonitorPlugOut(UINT index) { - SetLastMsg("Sucess"); + SetLastMsg("Success"); HANDLE hDevice = DeviceOpenHandle(); if (hDevice == INVALID_HANDLE_VALUE || hDevice == NULL) @@ -400,7 +400,7 @@ BOOL MonitorPlugOut(UINT index) BOOL MonitorModesUpdate(UINT index, UINT modeCount, PMonitorMode modes) { - SetLastMsg("Sucess"); + SetLastMsg("Success"); HANDLE hDevice = DeviceOpenHandle(); if (hDevice == INVALID_HANDLE_VALUE || hDevice == NULL) @@ -721,7 +721,7 @@ Clean0: // https://stackoverflow.com/questions/67164846/createfile-fails-unless-i-disable-enable-my-device HANDLE DeviceOpenHandle() { - SetLastMsg("Sucess"); + SetLastMsg("Success"); // const int maxDevPathLen = 256; TCHAR devicePath[256] = { 0 }; diff --git a/libs/virtual_display/dylib/src/win10/IddController.h b/libs/virtual_display/dylib/src/win10/IddController.h index f7f3df3f5..767d64798 100644 --- a/libs/virtual_display/dylib/src/win10/IddController.h +++ b/libs/virtual_display/dylib/src/win10/IddController.h @@ -47,7 +47,7 @@ BOOL IsDeviceCreated(PBOOL created); /** * @brief Create device. * Only one device should be created. - * If device is installed ealier, this function returns FALSE. + * If device is installed earlier, this function returns FALSE. * * @param hSwDevice [out] Handler of software device, used by DeviceCreate(). Should be **NULL**. * diff --git a/src/core_main.rs b/src/core_main.rs index eb8721563..1f42f8aad 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -305,7 +305,7 @@ fn core_main_invoke_new_connection(mut args: std::env::Args) -> Option { log::error!("Failed to start server: {}", err); } - _ => { /*no hapen*/ } + _ => { /*no happen*/ } } } std::thread::sleep(std::time::Duration::from_millis(super::SERVICE_INTERVAL)); diff --git a/src/server/connection.rs b/src/server/connection.rs index fdd0ea77a..301766704 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -244,7 +244,7 @@ impl Connection { loop { tokio::select! { - // biased; // video has higher priority // causing test_delay_timer failed while transfering big file + // biased; // video has higher priority // causing test_delay_timer failed while transferring big file Some(data) = rx_from_cm.recv() => { match data { diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 6b678dbdc..a94a7a2ef 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -223,7 +223,7 @@ lazy_static::lazy_static! { // First call set_uinput() will create keyboard and mouse clients. // The clients are ipc connections that must live shorter than tokio runtime. -// Thus this funtion must not be called in a temporary runtime. +// Thus this function must not be called in a temporary runtime. #[cfg(target_os = "linux")] pub async fn setup_uinput(minx: i32, maxx: i32, miny: i32, maxy: i32) -> ResultType<()> { // Keyboard and mouse both open /dev/uinput diff --git a/src/server/video_qos.rs b/src/server/video_qos.rs index d75596157..47bf49707 100644 --- a/src/server/video_qos.rs +++ b/src/server/video_qos.rs @@ -198,7 +198,7 @@ impl VideoQoS { #[cfg(target_os = "android")] { - // fix when andorid screen shrinks + // fix when android screen shrinks let fix = scrap::Display::fix_quality() as u32; log::debug!("Android screen, fix quality:{}", fix); let base_bitrate = base_bitrate * fix; diff --git a/src/ui/cm.tis b/src/ui/cm.tis index 74eb6c6d2..716f2c6dd 100644 --- a/src/ui/cm.tis +++ b/src/ui/cm.tis @@ -31,7 +31,7 @@ class Body: Reactor.Component var disconnected = c.disconnected; var show_elevation_btn = handler.can_elevate() && show_elevation && !c.is_file_transfer && c.port_forward.length == 0; var show_accept_btn = handler.get_option('approve-mode') != 'password'; - // below size:* is work around for Linux, it alreayd set in css, but not work, shit sciter + // below size:* is work around for Linux, it already set in css, but not work, shit sciter return

    diff --git a/src/ui/remote.tis b/src/ui/remote.tis index 012205abc..36f997540 100644 --- a/src/ui/remote.tis +++ b/src/ui/remote.tis @@ -26,7 +26,7 @@ handler.setDisplay = function(x, y, w, h, cursor_embeded) { if (recording) handler.record_screen(true, w, h); } -// in case toolbar not shown correclty +// in case toolbar not shown correctly view.windowMinSize = (scaleIt(500), scaleIt(300)); function adaptDisplay() { diff --git a/src/windows.cc b/src/windows.cc index 137ae399e..c4286ebdd 100644 --- a/src/windows.cc +++ b/src/windows.cc @@ -95,7 +95,7 @@ extern "C" CreateEnvironmentBlock(&lpEnvironment, // Environment block hToken, // New token - TRUE); // Inheritence + TRUE); // Inheritance } if (lpEnvironment) { From 9c07a0f2d8a6d5265540c621736b34fc689cfb1a Mon Sep 17 00:00:00 2001 From: csf Date: Mon, 26 Dec 2022 10:12:01 +0900 Subject: [PATCH 1254/2015] fix mobile api server validation bug --- flutter/lib/mobile/widgets/dialog.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/lib/mobile/widgets/dialog.dart b/flutter/lib/mobile/widgets/dialog.dart index d70902513..2df80d9fd 100644 --- a/flutter/lib/mobile/widgets/dialog.dart +++ b/flutter/lib/mobile/widgets/dialog.dart @@ -264,7 +264,6 @@ void showServerSettingsWithValue( if (relayServerMsg != null) return false; } if (apiCtrl.text != oldCfg.apiServer) { - apiServerMsg = await validateAsync(apiCtrl.text); if (apiServerMsg != null) return false; } return true; @@ -355,6 +354,7 @@ void showServerSettingsWithValue( bind.mainSetOption(key: "api-server", value: apiCtrl.text); } close(); + showToast(translate('Successful')); } setState(() { isInProgress = false; From b3114d4147b41b78dfa0b754e763647e2eee1c11 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sun, 18 Dec 2022 16:17:10 +0800 Subject: [PATCH 1255/2015] file audit Signed-off-by: 21pages --- .../lib/desktop/widgets/remote_menubar.dart | 3 +- src/common.rs | 4 +- src/flutter_ffi.rs | 4 +- src/server/connection.rs | 87 ++++++++++++++++--- src/ui/header.tis | 2 +- src/ui/remote.rs | 10 +-- src/ui_session_interface.rs | 5 +- 7 files changed, 90 insertions(+), 25 deletions(-) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index c776f0e89..1a7e9aeb7 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -571,7 +571,8 @@ class _RemoteMenubarState extends State { ), ]); // {handler.get_audit_server() &&
  • {translate('Note')}
  • } - final auditServer = bind.sessionGetAuditServerSync(id: widget.id); + final auditServer = + bind.sessionGetAuditServerSync(id: widget.id, typ: "conn"); if (auditServer.isNotEmpty) { displayMenu.add( MenuEntryButton( diff --git a/src/common.rs b/src/common.rs index 9023780f4..fe76b3168 100644 --- a/src/common.rs +++ b/src/common.rs @@ -620,12 +620,12 @@ pub fn get_api_server(api: String, custom: String) -> String { "https://admin.rustdesk.com".to_owned() } -pub fn get_audit_server(api: String, custom: String) -> String { +pub fn get_audit_server(api: String, custom: String, typ: String) -> String { let url = get_api_server(api, custom); if url.is_empty() || url.contains("rustdesk.com") { return "".to_owned(); } - format!("{}/api/audit", url) + format!("{}/api/audit/{}", url, typ) } pub async fn post_request(url: String, body: String, header: &str) -> ResultType { diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index ddfaad06d..dc9d7a04a 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -894,9 +894,9 @@ pub fn session_restart_remote_device(id: String) { } } -pub fn session_get_audit_server_sync(id: String) -> SyncReturn { +pub fn session_get_audit_server_sync(id: String, typ: String) -> SyncReturn { let res = if let Some(session) = SESSIONS.read().unwrap().get(&id) { - session.get_audit_server() + session.get_audit_server(typ) } else { "".to_owned() }; diff --git a/src/server/connection.rs b/src/server/connection.rs index fdd0ea77a..32ce97ea8 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -93,7 +93,8 @@ pub struct Connection { tx_input: std_mpsc::Sender, // handle input messages video_ack_required: bool, peer_info: (String, String), - api_server: String, + server_audit_conn: String, + server_audit_file: String, lr: LoginRequest, last_recv_time: Arc>, chat_unanswered: bool, @@ -184,7 +185,8 @@ impl Connection { tx_input, video_ack_required: false, peer_info: Default::default(), - api_server: "".to_owned(), + server_audit_conn: "".to_owned(), + server_audit_file: "".to_owned(), lr: Default::default(), last_recv_time: Arc::new(Mutex::new(Instant::now())), chat_unanswered: false, @@ -384,7 +386,7 @@ impl Connection { } else { conn.timer = time::interval_at(Instant::now() + SEC30, SEC30); } - conn.post_audit(json!({})); // heartbeat + conn.post_conn_audit(json!({})); // heartbeat }, Some((instant, value)) = rx_video.recv() => { if !conn.video_ack_required { @@ -497,7 +499,7 @@ impl Connection { conn.on_close(&err.to_string(), false).await; } - conn.post_audit(json!({ + conn.post_conn_audit(json!({ "action": "close", })); log::info!("#{} connection loop exited", id); @@ -601,7 +603,7 @@ impl Connection { if last_recv_time.elapsed() >= H1 { bail!("Timeout"); } - self.post_audit(json!({})); // heartbeat + self.post_conn_audit(json!({})); // heartbeat } } } @@ -650,7 +652,7 @@ impl Connection { msg_out.set_hash(self.hash.clone()); self.send(msg_out).await; self.get_api_server(); - self.post_audit(json!({ + self.post_conn_audit(json!({ "ip": addr.ip(), "action": "new", })); @@ -658,17 +660,23 @@ impl Connection { } fn get_api_server(&mut self) { - self.api_server = crate::get_audit_server( + self.server_audit_conn = crate::get_audit_server( Config::get_option("api-server"), Config::get_option("custom-rendezvous-server"), + "conn".to_owned(), + ); + self.server_audit_file = crate::get_audit_server( + Config::get_option("api-server"), + Config::get_option("custom-rendezvous-server"), + "file".to_owned(), ); } - fn post_audit(&self, v: Value) { - if self.api_server.is_empty() { + fn post_conn_audit(&self, v: Value) { + if self.server_audit_conn.is_empty() { return; } - let url = self.api_server.clone(); + let url = self.server_audit_conn.clone(); let mut v = v; v["id"] = json!(Config::get_id()); v["uuid"] = json!(base64::encode(hbb_common::get_uuid())); @@ -678,6 +686,41 @@ impl Connection { }); } + fn post_file_audit(&self, action: &str, path: &str, files: Vec<(String, i64)>, info: Value) { + if self.server_audit_file.is_empty() { + return; + } + let url = self.server_audit_file.clone(); + let file_num = files.len(); + let mut files = files; + files.sort_by(|a, b| b.1.cmp(&a.1)); + files.truncate(10); + let is_file = match action { + "send" | "receive" => files.len() == 1 && files[0].0.is_empty(), + "remove_dir" | "create_dir" => false, + "remove_file" => true, + _ => true, + }; + let mut info = info; + info["ip"] = json!(self.ip.clone()); + info["name"] = json!(self.lr.my_name.clone()); + info["num"] = json!(file_num); + info["files"] = json!(files); + let v = json!({ + "id":json!(Config::get_id()), + "uuid":json!(base64::encode(hbb_common::get_uuid())), + "Id":json!(self.inner.id), + "peer_id":json!(self.lr.my_id), + "action": action, + "path":path, + "is_file":is_file, + "info":json!(info).to_string(), + }); + tokio::spawn(async move { + allow_err!(Self::post_audit_async(url, v).await); + }); + } + #[inline] async fn post_audit_async(url: String, v: Value) -> ResultType<()> { crate::post_request(url, v.to_string(), "").await?; @@ -695,7 +738,7 @@ impl Connection { } else { 0 }; - self.post_audit(json!({"peer": self.peer_info, "Type": conn_type})); + 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 res = LoginResponse::new(); @@ -1225,8 +1268,18 @@ impl Connection { Ok(job) => { self.send(fs::new_dir(id, path, job.files().to_vec())) .await; + let mut files = job.files().to_owned(); self.read_jobs.push(job); self.timer = time::interval(MILLI1); + self.post_file_audit( + "send", + &s.path, + files + .drain(..) + .map(|f| (f.name, f.size as _)) + .collect(), + json!({}), + ); } } } @@ -1237,7 +1290,7 @@ impl Connection { &self.lr.version, )); self.send_fs(ipc::FS::NewWrite { - path: r.path, + path: r.path.clone(), id: r.id, file_num: r.file_num, files: r @@ -1248,6 +1301,16 @@ impl Connection { .collect(), overwrite_detection: od, }); + self.post_file_audit( + "receive", + &r.path, + r.files + .to_vec() + .drain(..) + .map(|f| (f.name, f.size as _)) + .collect(), + json!({}), + ); } Some(file_action::Union::RemoveDir(d)) => { self.send_fs(ipc::FS::RemoveDir { diff --git a/src/ui/header.tis b/src/ui/header.tis index 086696726..d1bb91cb9 100644 --- a/src/ui/header.tis +++ b/src/ui/header.tis @@ -208,7 +208,7 @@ class Header: Reactor.Component { {keyboard_enabled ?
  • {translate('OS Password')}
  • : ""}
  • {translate('Transfer File')}
  • {translate('TCP Tunneling')}
  • - {handler.get_audit_server() &&
  • {translate('Note')}
  • } + {handler.get_audit_server("conn") &&
  • {translate('Note')}
  • }
    {keyboard_enabled && (pi.platform == "Linux" || pi.sas_enabled) ?
  • {translate('Insert')} Ctrl + Alt + Del
  • : ""} {restart_enabled && (pi.platform == "Linux" || pi.platform == "Windows" || pi.platform == "Mac OS") ?
  • {translate('Restart Remote Device')}
  • : ""} diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 29b1a9eee..df5d98038 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -233,12 +233,12 @@ impl InvokeUiSession for SciterHandler { fn on_connected(&self, conn_type: ConnType) { match conn_type { - ConnType::RDP => {}, - ConnType::PORT_FORWARD => {}, - ConnType::FILE_TRANSFER => {}, + ConnType::RDP => {} + ConnType::PORT_FORWARD => {} + ConnType::FILE_TRANSFER => {} ConnType::DEFAULT_CONN => { crate::keyboard::client::start_grab_loop(); - }, + } } } @@ -348,7 +348,7 @@ impl sciter::EventHandler for SciterSession { } sciter::dispatch_script_call! { - fn get_audit_server(); + fn get_audit_server(String); fn send_note(String); fn is_xfce(); fn get_id(); diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 9868b5bb1..55984e343 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -173,7 +173,7 @@ impl Session { self.send(Data::Message(msg)); } - pub fn get_audit_server(&self) -> String { + pub fn get_audit_server(&self, typ: String) -> String { if self.lc.read().unwrap().conn_id <= 0 || LocalConfig::get_option("access_token").is_empty() { @@ -182,11 +182,12 @@ impl Session { crate::get_audit_server( Config::get_option("api-server"), Config::get_option("custom-rendezvous-server"), + typ, ) } pub fn send_note(&self, note: String) { - let url = self.get_audit_server(); + let url = self.get_audit_server("conn".to_string()); let id = self.id.clone(); let conn_id = self.lc.read().unwrap().conn_id; std::thread::spawn(move || { From 38b6ba66915c1ce327b3a36faba189b1f25de7d8 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sun, 18 Dec 2022 17:00:10 +0800 Subject: [PATCH 1256/2015] split connection timer Signed-off-by: 21pages --- src/server/connection.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/server/connection.rs b/src/server/connection.rs index 32ce97ea8..659a97136 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -72,6 +72,8 @@ pub struct Connection { hash: Hash, read_jobs: Vec, timer: Interval, + file_timer: Interval, + http_timer: Interval, file_transfer: Option<(String, bool)>, port_forward_socket: Option>, port_forward_address: String, @@ -164,6 +166,8 @@ impl Connection { hash, read_jobs: Vec::new(), timer: time::interval(SEC30), + file_timer: time::interval(SEC30), + http_timer: time::interval(Duration::from_secs(3)), file_transfer: None, port_forward_socket: None, port_forward_address: "".to_owned(), @@ -377,15 +381,17 @@ impl Connection { break; } }, - _ = conn.timer.tick() => { + _ = conn.file_timer.tick() => { if !conn.read_jobs.is_empty() { if let Err(err) = fs::handle_read_jobs(&mut conn.read_jobs, &mut conn.stream).await { conn.on_close(&err.to_string(), false).await; break; } } else { - conn.timer = time::interval_at(Instant::now() + SEC30, SEC30); + conn.file_timer = time::interval_at(Instant::now() + SEC30, SEC30); } + } + _ = conn.http_timer.tick() => { conn.post_conn_audit(json!({})); // heartbeat }, Some((instant, value)) = rx_video.recv() => { @@ -1270,7 +1276,7 @@ impl Connection { .await; let mut files = job.files().to_owned(); self.read_jobs.push(job); - self.timer = time::interval(MILLI1); + self.file_timer = time::interval(MILLI1); self.post_file_audit( "send", &s.path, From 866ab240875905c01ae72ff06a7267cbd3a018f7 Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 20 Dec 2022 10:22:28 +0800 Subject: [PATCH 1257/2015] disconnect conn from web console Signed-off-by: 21pages --- 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/pl.rs | 1 + src/lang/pt_PT.rs | 1 + src/lang/ptbr.rs | 1 + src/lang/ru.rs | 1 + src/lang/sk.rs | 1 + src/lang/sq.rs | 1 + src/lang/sr.rs | 1 + src/lang/sv.rs | 1 + src/lang/template.rs | 1 + src/lang/tr.rs | 1 + src/lang/tw.rs | 1 + src/lang/ua.rs | 1 + src/lang/vn.rs | 1 + src/server/connection.rs | 47 ++++++++++++++++++++++++++++++++-------- 30 files changed, 67 insertions(+), 9 deletions(-) diff --git a/src/lang/ca.rs b/src/lang/ca.rs index a8f39a23f..2f9bf7b25 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -405,5 +405,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", ""), ("Group", ""), ("Search", ""), + ("Closed manually by the web console", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 5dce8cd38..be0d7803e 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -406,5 +406,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", "添加到地址簿"), ("Group", "小组"), ("Search", "搜索"), + ("Closed manually by the web console", "被web控制台手动关闭"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 52571ef07..eb57edc0a 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -405,5 +405,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", ""), ("Group", ""), ("Search", ""), + ("Closed manually by the web console", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 138777e32..c34a31aef 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -405,5 +405,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", ""), ("Group", ""), ("Search", ""), + ("Closed manually by the web console", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index d5d75d90b..b551774bf 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -405,5 +405,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", "Zum Adressbuch hinzufügen"), ("Group", "Gruppe"), ("Search", "Suchen"), + ("Closed manually by the web console", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index d22cb2311..3f22f489e 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -405,5 +405,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", ""), ("Group", ""), ("Search", ""), + ("Closed manually by the web console", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index ce0254a98..6923029ff 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -405,5 +405,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", "Añadir a la libreta de direcciones"), ("Group", "Grupo"), ("Search", "Búsqueda"), + ("Closed manually by the web console", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index b3850e1f2..20a1663f2 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -405,5 +405,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", "افزودن به دفترچه آدرس"), ("Group", ""), ("Search", ""), + ("Closed manually by the web console", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 124bfc00c..3b81fa1e4 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -405,5 +405,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", "Ajouter au carnet d'adresses"), ("Group", "Groupe"), ("Search", "Rechercher"), + ("Closed manually by the web console", ""), ].iter().cloned().collect(); } diff --git a/src/lang/gr.rs b/src/lang/gr.rs index 933a84143..c708ce478 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -405,5 +405,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", "Προσθήκη στο Βιβλίο Διευθύνσεων"), ("Group", "Ομάδα"), ("Search", "Αναζήτηση"), + ("Closed manually by the web console", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index f3f1e8fd9..849bfa0af 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -405,5 +405,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", ""), ("Group", ""), ("Search", ""), + ("Closed manually by the web console", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 89728b3e6..da69f2c76 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -405,5 +405,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", ""), ("Group", ""), ("Search", ""), + ("Closed manually by the web console", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 2237c81db..3b112618d 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -405,5 +405,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", "Aggiungi alla rubrica"), ("Group", ""), ("Search", ""), + ("Closed manually by the web console", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index e40c81ae8..3d76f446b 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -405,5 +405,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", ""), ("Group", ""), ("Search", ""), + ("Closed manually by the web console", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 426a027db..92bb85bce 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -405,5 +405,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", ""), ("Group", ""), ("Search", ""), + ("Closed manually by the web console", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 6acd892f8..90a2730f6 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -405,5 +405,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", ""), ("Group", ""), ("Search", ""), + ("Closed manually by the web console", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index eb3a45d53..e5604c442 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -405,5 +405,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", "Dodaj do Książki Adresowej"), ("Group", "Grypy"), ("Search", "Szukaj"), + ("Closed manually by the web console", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 4d3d057ee..2adb4eb9e 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -405,5 +405,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", ""), ("Group", ""), ("Search", ""), + ("Closed manually by the web console", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index bc878b680..6256d2e7a 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -405,5 +405,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", ""), ("Group", ""), ("Search", ""), + ("Closed manually by the web console", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index f42d3146e..6e08322f3 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -405,5 +405,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", "Добавить в адресную книгу"), ("Group", "Группа"), ("Search", "Поиск"), + ("Closed manually by the web console", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index e1b82d4f4..a65e0519b 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -405,5 +405,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", ""), ("Group", ""), ("Search", ""), + ("Closed manually by the web console", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index cbc71d4aa..27f37260d 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -405,5 +405,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", ""), ("Group", ""), ("Search", ""), + ("Closed manually by the web console", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 43490c0b2..5d04f0150 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -410,5 +410,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", "Dodaj u adresar"), ("Group", "Grupa"), ("Search", "Pretraga"), + ("Closed manually by the web console", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 4dcababc0..72cc83fce 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -405,5 +405,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", ""), ("Group", ""), ("Search", ""), + ("Closed manually by the web console", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 34fe5077f..a92df2b52 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -405,5 +405,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", ""), ("Group", ""), ("Search", ""), + ("Closed manually by the web console", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index b0c0686c1..cb7203c2d 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -405,5 +405,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", ""), ("Group", ""), ("Search", ""), + ("Closed manually by the web console", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index d8d6c5ba0..6eef3656c 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -405,5 +405,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", "添加到地址簿"), ("Group", "小組"), ("Search", "搜索"), + ("Closed manually by the web console", "被web控制台手動關閉"), ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 83b5e6984..373c3267e 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -405,5 +405,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", "Додати IP до Адресної книги"), ("Group", "Група"), ("Search", "Пошук"), + ("Closed manually by the web console", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 412f04999..7b7808bea 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -405,5 +405,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", ""), ("Group", ""), ("Search", ""), + ("Closed manually by the web console", ""), ].iter().cloned().collect(); } diff --git a/src/server/connection.rs b/src/server/connection.rs index 659a97136..bfc8cd78b 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -26,6 +26,7 @@ use hbb_common::{ }; #[cfg(any(target_os = "android", target_os = "ios"))] use scrap::android::call_main_service_mouse_input; +use serde::Deserialize; use serde_json::{json, value::Value}; use sha2::{Digest, Sha256}; #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -261,7 +262,7 @@ impl Connection { } } ipc::Data::Close => { - conn.on_close_manually("connection manager").await; + conn.on_close_manually("connection manager", "peer").await; break; } ipc::Data::ChatMessage{text} => { @@ -392,7 +393,10 @@ impl Connection { } } _ = conn.http_timer.tick() => { - conn.post_conn_audit(json!({})); // heartbeat + if let Err(_) = Connection::post_heartbeat(conn.server_audit_conn.clone(), conn.inner.id).await { + conn.on_close_manually("web console", "web console").await; + break; + } }, Some((instant, value)) = rx_video.recv() => { if !conn.video_ack_required { @@ -420,7 +424,7 @@ impl Connection { Some(message::Union::Misc(m)) => { match &m.union { Some(misc::Union::StopService(_)) => { - conn.on_close_manually("stop service").await; + conn.on_close_manually("stop service", "peer").await; break; } _ => {}, @@ -609,7 +613,7 @@ impl Connection { if last_recv_time.elapsed() >= H1 { bail!("Timeout"); } - self.post_conn_audit(json!({})); // heartbeat + Connection::post_heartbeat(self.server_audit_conn.clone(), self.inner.id).await?; } } } @@ -692,6 +696,25 @@ impl Connection { }); } + async fn post_heartbeat(server_audit_conn: String, conn_id: i32) -> ResultType<()> { + if server_audit_conn.is_empty() { + return Ok(()); + } + let url = server_audit_conn.clone(); + let mut v = Value::default(); + v["id"] = json!(Config::get_id()); + v["uuid"] = json!(base64::encode(hbb_common::get_uuid())); + v["Id"] = json!(conn_id); + if let Ok(rsp) = Self::post_audit_async(url, v).await { + if let Ok(rsp) = serde_json::from_str::(&rsp) { + if rsp.action == "disconnect" { + bail!("disconnect by server"); + } + } + } + return Ok(()); + } + fn post_file_audit(&self, action: &str, path: &str, files: Vec<(String, i64)>, info: Value) { if self.server_audit_file.is_empty() { return; @@ -728,9 +751,8 @@ impl Connection { } #[inline] - async fn post_audit_async(url: String, v: Value) -> ResultType<()> { - crate::post_request(url, v.to_string(), "").await?; - Ok(()) + async fn post_audit_async(url: String, v: Value) -> ResultType { + crate::post_request(url, v.to_string(), "").await } async fn send_logon_response(&mut self) { @@ -1610,10 +1632,10 @@ impl Connection { self.port_forward_socket.take(); } - async fn on_close_manually(&mut self, close_from: &str) { + async fn on_close_manually(&mut self, close_from: &str, close_by: &str) { self.close_manually = true; let mut misc = Misc::new(); - misc.set_close_reason("Closed manually by the peer".into()); + misc.set_close_reason(format!("Closed manually by the {}", close_by)); let mut msg_out = Message::new(); msg_out.set_misc(misc); self.send(msg_out).await; @@ -1790,3 +1812,10 @@ mod privacy_mode { } } } + +#[derive(Debug, Deserialize)] +struct ConnAuditResponse { + #[allow(dead_code)] + ret: bool, + action: String, +} From 56f154f69a39389f99b02a54132a850975ba0a7f Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 20 Dec 2022 16:20:42 +0800 Subject: [PATCH 1258/2015] alarm audit Signed-off-by: 21pages --- src/server/connection.rs | 47 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/src/server/connection.rs b/src/server/connection.rs index bfc8cd78b..136a4b692 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -654,6 +654,13 @@ impl Connection { { self.send_login_error("Your ip is blocked by the peer") .await; + Self::post_alarm_audit( + AlarmAuditType::IpWhiltelist, //"ip whiltelist", + true, + json!({ + "ip":addr.ip(), + }), + ); sleep(1.).await; return false; } @@ -750,6 +757,26 @@ impl Connection { }); } + pub fn post_alarm_audit(typ: AlarmAuditType, from_remote: bool, info: Value) { + let url = crate::get_audit_server( + Config::get_option("api-server"), + Config::get_option("custom-rendezvous-server"), + "alarm".to_owned(), + ); + if url.is_empty() { + return; + } + let mut v = Value::default(); + v["id"] = json!(Config::get_id()); + v["uuid"] = json!(base64::encode(hbb_common::get_uuid())); + v["typ"] = json!(typ as i8); + v["from_remote"] = json!(from_remote); + v["info"] = serde_json::Value::String(info.to_string()); + tokio::spawn(async move { + allow_err!(Self::post_audit_async(url, v).await); + }); + } + #[inline] async fn post_audit_async(url: String, v: Value) -> ResultType { crate::post_request(url, v.to_string(), "").await @@ -1157,8 +1184,22 @@ impl Connection { if failure.2 > 30 { self.send_login_error("Too many wrong password attempts") .await; + Self::post_alarm_audit( + AlarmAuditType::ManyWrongPassword, + true, + json!({ + "ip":self.ip, + }), + ); } else if time == failure.0 && failure.1 > 6 { self.send_login_error("Please try 1 minute later").await; + Self::post_alarm_audit( + AlarmAuditType::FrequentAttempt, + true, + json!({ + "ip":self.ip, + }), + ); } else if !self.validate_password() { if failure.0 == time { failure.1 += 1; @@ -1819,3 +1860,9 @@ struct ConnAuditResponse { ret: bool, action: String, } + +pub enum AlarmAuditType { + IpWhiltelist = 0, + ManyWrongPassword = 1, + FrequentAttempt = 2, +} From 54ce0a977587161fa5259aafcdf727430c7190bb Mon Sep 17 00:00:00 2001 From: 21pages Date: Fri, 23 Dec 2022 10:36:18 +0800 Subject: [PATCH 1259/2015] refactor audit field Signed-off-by: 21pages --- flutter/lib/models/group_model.dart | 12 +++++------ src/server/connection.rs | 33 +++++++++++++++++------------ 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/flutter/lib/models/group_model.dart b/flutter/lib/models/group_model.dart index adc92f182..f220d62f1 100644 --- a/flutter/lib/models/group_model.dart +++ b/flutter/lib/models/group_model.dart @@ -45,8 +45,9 @@ class GroupModel { var uri0 = Uri.parse(api); final pageSize = 20; var total = 0; - int current = 1; + int current = 0; do { + current += 1; var uri = Uri( scheme: uri0.scheme, host: uri0.host, @@ -58,7 +59,6 @@ class GroupModel { if (gFFI.userModel.isAdmin.isFalse) 'grp': gFFI.userModel.groupName.value, }); - current += pageSize; final resp = await http.get(uri, headers: await getHttpHeaders()); if (resp.body.isNotEmpty && resp.body.toLowerCase() != "null") { Map json = jsonDecode(resp.body); @@ -76,7 +76,7 @@ class GroupModel { } } } - } while (current < total + 1); + } while (current * pageSize < total); } catch (err) { debugPrint('$err'); userLoadError.value = err.toString(); @@ -96,8 +96,9 @@ class GroupModel { var uri0 = Uri.parse(api); final pageSize = 20; var total = 0; - int current = 1; + int current = 0; do { + current += 1; var uri = Uri( scheme: uri0.scheme, host: uri0.host, @@ -109,7 +110,6 @@ class GroupModel { 'grp': gFFI.userModel.groupName.value, 'target_user': username }); - current += pageSize; final resp = await http.get(uri, headers: await getHttpHeaders()); if (resp.body.isNotEmpty && resp.body.toLowerCase() != "null") { Map json = jsonDecode(resp.body); @@ -129,7 +129,7 @@ class GroupModel { } } } - } while (current < total + 1); + } while (current * pageSize < total); } catch (err) { debugPrint('$err'); peerLoadError.value = err.toString(); diff --git a/src/server/connection.rs b/src/server/connection.rs index 136a4b692..394d330a9 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -697,7 +697,7 @@ impl Connection { let mut v = v; v["id"] = json!(Config::get_id()); v["uuid"] = json!(base64::encode(hbb_common::get_uuid())); - v["Id"] = json!(self.inner.id); + v["conn_id"] = json!(self.inner.id); tokio::spawn(async move { allow_err!(Self::post_audit_async(url, v).await); }); @@ -711,7 +711,7 @@ impl Connection { let mut v = Value::default(); v["id"] = json!(Config::get_id()); v["uuid"] = json!(base64::encode(hbb_common::get_uuid())); - v["Id"] = json!(conn_id); + v["conn_id"] = json!(conn_id); if let Ok(rsp) = Self::post_audit_async(url, v).await { if let Ok(rsp) = serde_json::from_str::(&rsp) { if rsp.action == "disconnect" { @@ -722,7 +722,13 @@ impl Connection { return Ok(()); } - fn post_file_audit(&self, action: &str, path: &str, files: Vec<(String, i64)>, info: Value) { + fn post_file_audit( + &self, + r#type: FileAuditType, + path: &str, + files: Vec<(String, i64)>, + info: Value, + ) { if self.server_audit_file.is_empty() { return; } @@ -731,12 +737,7 @@ impl Connection { let mut files = files; files.sort_by(|a, b| b.1.cmp(&a.1)); files.truncate(10); - let is_file = match action { - "send" | "receive" => files.len() == 1 && files[0].0.is_empty(), - "remove_dir" | "create_dir" => false, - "remove_file" => true, - _ => true, - }; + let is_file = files.len() == 1 && files[0].0.is_empty(); let mut info = info; info["ip"] = json!(self.ip.clone()); info["name"] = json!(self.lr.my_name.clone()); @@ -745,9 +746,8 @@ impl Connection { let v = json!({ "id":json!(Config::get_id()), "uuid":json!(base64::encode(hbb_common::get_uuid())), - "Id":json!(self.inner.id), "peer_id":json!(self.lr.my_id), - "action": action, + "type": r#type as i8, "path":path, "is_file":is_file, "info":json!(info).to_string(), @@ -793,7 +793,7 @@ impl Connection { } else { 0 }; - self.post_conn_audit(json!({"peer": self.peer_info, "Type": conn_type})); + 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 res = LoginResponse::new(); @@ -1341,7 +1341,7 @@ impl Connection { self.read_jobs.push(job); self.file_timer = time::interval(MILLI1); self.post_file_audit( - "send", + FileAuditType::RemoteSend, &s.path, files .drain(..) @@ -1371,7 +1371,7 @@ impl Connection { overwrite_detection: od, }); self.post_file_audit( - "receive", + FileAuditType::RemoteReceive, &r.path, r.files .to_vec() @@ -1866,3 +1866,8 @@ pub enum AlarmAuditType { ManyWrongPassword = 1, FrequentAttempt = 2, } + +pub enum FileAuditType { + RemoteSend = 0, + RemoteReceive = 1, +} From a5643a6b59b59b26a2c2960ab870fb7045d7a01c Mon Sep 17 00:00:00 2001 From: 21pages Date: Sat, 24 Dec 2022 14:29:32 +0800 Subject: [PATCH 1260/2015] fix two finger scroll Signed-off-by: 21pages --- flutter/lib/desktop/pages/desktop_setting_page.dart | 1 + flutter/lib/desktop/widgets/tabbar_widget.dart | 1 + 2 files changed, 2 insertions(+) diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index a45de24b0..40cd794ab 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -125,6 +125,7 @@ class _DesktopSettingPageState extends State scrollController: controller, child: PageView( controller: controller, + physics: NeverScrollableScrollPhysics(), children: const [ _General(), _Safety(), diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 436011cb5..f6a5da819 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -331,6 +331,7 @@ class DesktopTab extends StatelessWidget { return _buildBlock( child: Obx(() => PageView( controller: state.value.pageController, + physics: NeverScrollableScrollPhysics(), children: state.value.tabs .map((tab) => tab.page) .toList(growable: false)))); From 8b7e6935f4e19c92ba10cc2fc815500bee39404b Mon Sep 17 00:00:00 2001 From: 21pages Date: Sat, 24 Dec 2022 19:47:12 +0800 Subject: [PATCH 1261/2015] remove ReorderedListView Signed-off-by: 21pages --- flutter/lib/common/widgets/peer_tab_page.dart | 233 ++++++------------ 1 file changed, 74 insertions(+), 159 deletions(-) diff --git a/flutter/lib/common/widgets/peer_tab_page.dart b/flutter/lib/common/widgets/peer_tab_page.dart index b501bb44b..0c24fe7ea 100644 --- a/flutter/lib/common/widgets/peer_tab_page.dart +++ b/flutter/lib/common/widgets/peer_tab_page.dart @@ -1,4 +1,3 @@ -import 'dart:convert'; import 'dart:ui' as ui; import 'package:bot_toast/bot_toast.dart'; @@ -22,10 +21,7 @@ const String defaultGroupTabname = 'Group'; class StatePeerTab { final RxInt currentTab = 0.obs; - static const List tabIndexs = [0, 1, 2, 3, 4]; - List tabOrder = List.empty(growable: true); - final RxList visibleTabOrder = RxList.empty(growable: true); - int tabHiddenFlag = 0; + final RxInt tabHiddenFlag = 0.obs; final RxList tabNames = [ 'Recent Sessions', 'Favorites', @@ -35,44 +31,25 @@ class StatePeerTab { ].obs; StatePeerTab._() { - tabHiddenFlag = (int.tryParse( + tabHiddenFlag.value = (int.tryParse( bind.getLocalFlutterConfig(k: 'hidden-peer-card'), radix: 2) ?? 0); + var tabs = _notHiddenTabs(); currentTab.value = int.tryParse(bind.getLocalFlutterConfig(k: 'peer-tab-index')) ?? 0; - if (!tabIndexs.contains(currentTab.value)) { - currentTab.value = tabIndexs[0]; + if (!tabs.contains(currentTab.value)) { + currentTab.value = 0; } - tabOrder = tabIndexs.toList(); - try { - final conf = bind.getLocalFlutterConfig(k: 'peer-tab-order'); - if (conf.isNotEmpty) { - final json = jsonDecode(conf); - if (json is List) { - final List list = - json.map((e) => int.tryParse(e.toString()) ?? -1).toList(); - if (list.length == tabOrder.length && - tabOrder.every((e) => list.contains(e))) { - tabOrder = list; - } - } - } - } catch (e) { - debugPrintStack(label: '$e'); - } - visibleTabOrder.value = tabOrder.where((e) => !isTabHidden(e)).toList(); - visibleTabOrder.remove(groupTabIndex); } static final StatePeerTab instance = StatePeerTab._(); check() { - List oldOrder = visibleTabOrder; + var tabs = _notHiddenTabs(); if (filterGroupCard()) { - visibleTabOrder.remove(groupTabIndex); if (currentTab.value == groupTabIndex) { currentTab.value = - visibleTabOrder.firstWhereOrNull((e) => e != groupTabIndex) ?? 0; + tabs.firstWhereOrNull((e) => e != groupTabIndex) ?? 0; bind.setLocalFlutterConfig( k: 'peer-tab-index', v: currentTab.value.toString()); } @@ -83,26 +60,22 @@ class StatePeerTab { } else { tabNames[groupTabIndex] = defaultGroupTabname; } - if (isTabHidden(groupTabIndex)) { - visibleTabOrder.remove(groupTabIndex); - } else { - if (!visibleTabOrder.contains(groupTabIndex)) { - addTabInOrder(visibleTabOrder, groupTabIndex); - } - } - if (visibleTabOrder.contains(groupTabIndex) && + if (tabs.contains(groupTabIndex) && int.tryParse(bind.getLocalFlutterConfig(k: 'peer-tab-index')) == groupTabIndex) { currentTab.value = groupTabIndex; } } - if (oldOrder != visibleTabOrder) { - saveTabOrder(); - } } - bool isTabHidden(int tabindex) { - return tabHiddenFlag & (1 << tabindex) != 0; + List currentTabs() { + var v = List.empty(growable: true); + for (int i = 0; i < tabNames.length; i++) { + if (!_isTabHidden(i) && !_isTabFilter(i)) { + v.add(i); + } + } + return v; } bool filterGroupCard() { @@ -114,50 +87,25 @@ class StatePeerTab { } } - addTabInOrder(List list, int tabIndex) { - if (!tabOrder.contains(tabIndex) || list.contains(tabIndex)) { - return; - } - bool sameOrder = true; - int lastIndex = -1; - for (int i = 0; i < list.length; i++) { - var index = tabOrder.lastIndexOf(list[i]); - if (index > lastIndex) { - lastIndex = index; - continue; - } else { - sameOrder = false; - break; - } - } - if (sameOrder) { - var indexInTabOrder = tabOrder.indexOf(tabIndex); - var left = List.empty(growable: true); - for (int i = 0; i < indexInTabOrder; i++) { - left.add(tabOrder[i]); - } - int insertIndex = list.lastIndexWhere((e) => left.contains(e)); - if (insertIndex < 0) { - insertIndex = 0; - } else { - insertIndex += 1; - } - list.insert(insertIndex, tabIndex); - } else { - list.add(tabIndex); - } + bool _isTabHidden(int tabindex) { + return tabHiddenFlag & (1 << tabindex) != 0; } - saveTabOrder() { - var list = statePeerTab.visibleTabOrder.toList(); - var left = tabOrder - .where((e) => !statePeerTab.visibleTabOrder.contains(e)) - .toList(); - for (var t in left) { - addTabInOrder(list, t); + bool _isTabFilter(int tabIndex) { + if (tabIndex == groupTabIndex) { + return filterGroupCard(); } - statePeerTab.tabOrder = list; - bind.setLocalFlutterConfig(k: 'peer-tab-order', v: jsonEncode(list)); + return false; + } + + List _notHiddenTabs() { + var v = List.empty(growable: true); + for (int i = 0; i < tabNames.length; i++) { + if (!_isTabHidden(i)) { + v.add(i); + } + } + return v; } } @@ -266,59 +214,41 @@ class _PeerTabPageState extends State Widget _createSwitchBar(BuildContext context) { final textColor = Theme.of(context).textTheme.titleLarge?.color; - statePeerTab.visibleTabOrder - .removeWhere((e) => !StatePeerTab.tabIndexs.contains(e)); return Obx(() { - int indexCounter = -1; - return ReorderableListView( - buildDefaultDragHandles: false, - onReorder: (oldIndex, newIndex) { - var list = statePeerTab.visibleTabOrder.toList(); - if (oldIndex < newIndex) { - newIndex -= 1; - } - final int item = list.removeAt(oldIndex); - list.insert(newIndex, item); - statePeerTab.visibleTabOrder.value = list; - statePeerTab.saveTabOrder(); - }, + var tabs = statePeerTab.currentTabs(); + return ListView( scrollDirection: Axis.horizontal, - shrinkWrap: true, - scrollController: ScrollController(), - children: statePeerTab.visibleTabOrder.map((t) { - indexCounter++; - return ReorderableDragStartListener( - key: ValueKey(t), - index: indexCounter, - child: InkWell( - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 8), - decoration: BoxDecoration( - color: statePeerTab.currentTab.value == t - ? Theme.of(context).backgroundColor - : null, - borderRadius: BorderRadius.circular(isDesktop ? 2 : 6), + physics: NeverScrollableScrollPhysics(), + controller: ScrollController(), + children: tabs.map((t) { + return InkWell( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8), + decoration: BoxDecoration( + color: statePeerTab.currentTab.value == t + ? Theme.of(context).backgroundColor + : null, + borderRadius: BorderRadius.circular(isDesktop ? 2 : 6), + ), + child: Align( + alignment: Alignment.center, + child: Text( + translatedTabname(t), + textAlign: TextAlign.center, + style: TextStyle( + height: 1, + fontSize: 14, + color: statePeerTab.currentTab.value == t + ? textColor + : textColor + ?..withOpacity(0.5)), ), - child: Align( - alignment: Alignment.center, - child: Text( - translatedTabname(t), - textAlign: TextAlign.center, - style: TextStyle( - height: 1, - fontSize: 14, - color: statePeerTab.currentTab.value == t - ? textColor - : textColor - ?..withOpacity(0.5)), - ), - )), - onTap: () async { - await handleTabSelection(t); - await bind.setLocalFlutterConfig( - k: 'peer-tab-index', v: t.toString()); - }, - ), + )), + onTap: () async { + await handleTabSelection(t); + await bind.setLocalFlutterConfig( + k: 'peer-tab-index', v: t.toString()); + }, ); }).toList()); }); @@ -343,20 +273,18 @@ class _PeerTabPageState extends State Widget _createPeersView() { final verticalMargin = isDesktop ? 12.0 : 6.0; - statePeerTab.visibleTabOrder - .removeWhere((e) => !StatePeerTab.tabIndexs.contains(e)); return Expanded( child: Obx(() { - if (statePeerTab.visibleTabOrder.isEmpty) { + var tabs = statePeerTab.currentTabs(); + if (tabs.isEmpty) { return visibleContextMenuListener(Center( child: Text(translate('Right click to select tabs')), )); } else { - if (statePeerTab.visibleTabOrder - .contains(statePeerTab.currentTab.value)) { + if (tabs.contains(statePeerTab.currentTab.value)) { return entries[statePeerTab.currentTab.value].widget; } else { - statePeerTab.currentTab.value = statePeerTab.visibleTabOrder[0]; + statePeerTab.currentTab.value = tabs[0]; return entries[statePeerTab.currentTab.value].widget; } } @@ -394,13 +322,9 @@ class _PeerTabPageState extends State } adjustTab() { - if (statePeerTab.visibleTabOrder.isNotEmpty) { - if (!statePeerTab.visibleTabOrder - .contains(statePeerTab.currentTab.value)) { - handleTabSelection(statePeerTab.visibleTabOrder[0]); - } - } else { - statePeerTab.currentTab.value = 0; + var tabs = statePeerTab.currentTabs(); + if (tabs.isNotEmpty && !tabs.contains(statePeerTab.currentTab.value)) { + statePeerTab.currentTab.value = tabs[0]; } } @@ -438,22 +362,13 @@ class _PeerTabPageState extends State }, setter: (show) async { if (show) { - statePeerTab.tabHiddenFlag &= ~bitMask; + statePeerTab.tabHiddenFlag.value &= ~bitMask; } else { - statePeerTab.tabHiddenFlag |= bitMask; + statePeerTab.tabHiddenFlag.value |= bitMask; } await bind.setLocalFlutterConfig( k: 'hidden-peer-card', - v: statePeerTab.tabHiddenFlag.toRadixString(2)); - statePeerTab.visibleTabOrder - .removeWhere((e) => statePeerTab.isTabHidden(e)); - for (int j = 0; j < statePeerTab.tabNames.length; j++) { - if (!statePeerTab.isTabHidden(j) && - !(j == groupTabIndex && statePeerTab.filterGroupCard())) { - statePeerTab.addTabInOrder(statePeerTab.visibleTabOrder, j); - } - } - statePeerTab.saveTabOrder(); + v: statePeerTab.tabHiddenFlag.value.toRadixString(2)); cancelFunc(); adjustTab(); })); From 6d95a66de38a7c4a940681dd793e92af8abe811a Mon Sep 17 00:00:00 2001 From: rustdesk Date: Mon, 26 Dec 2022 11:25:55 +0800 Subject: [PATCH 1262/2015] remove some errors on mac --- libs/enigo/examples/mouse.rs | 19 +++++++++++-------- libs/portable/src/bin_reader.rs | 5 ++++- libs/portable/src/main.rs | 2 +- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/libs/enigo/examples/mouse.rs b/libs/enigo/examples/mouse.rs index 50a3506cf..f963e041e 100644 --- a/libs/enigo/examples/mouse.rs +++ b/libs/enigo/examples/mouse.rs @@ -23,15 +23,18 @@ fn main() { enigo.mouse_click(MouseButton::Left); thread::sleep(wait_time); - enigo.mouse_scroll_x(2); - thread::sleep(wait_time); + #[cfg(not(target_os = "macos"))] + { + enigo.mouse_scroll_x(2); + thread::sleep(wait_time); - enigo.mouse_scroll_x(-2); - thread::sleep(wait_time); + enigo.mouse_scroll_x(-2); + thread::sleep(wait_time); - enigo.mouse_scroll_y(2); - thread::sleep(wait_time); + enigo.mouse_scroll_y(2); + thread::sleep(wait_time); - enigo.mouse_scroll_y(-2); - thread::sleep(wait_time); + enigo.mouse_scroll_y(-2); + thread::sleep(wait_time); + } } diff --git a/libs/portable/src/bin_reader.rs b/libs/portable/src/bin_reader.rs index 499c18e2c..2d0b1bf7e 100644 --- a/libs/portable/src/bin_reader.rs +++ b/libs/portable/src/bin_reader.rs @@ -4,7 +4,10 @@ use std::{ path::PathBuf, }; +#[cfg(windows)] const BIN_DATA: &[u8] = include_bytes!("../data.bin"); +#[cfg(not(windows))] +const BIN_DATA: &[u8] = &[]; // 4bytes const LENGTH: usize = 4; const IDENTIFIER_LENGTH: usize = 8; @@ -118,7 +121,7 @@ impl BinaryReader { (parsed, executable) } - #[cfg(unix)] + #[cfg(linux)] pub fn configure_permission(&self, prefix: &PathBuf) { use std::os::unix::prelude::PermissionsExt; diff --git a/libs/portable/src/main.rs b/libs/portable/src/main.rs index edcbdd1fd..13dd0c3dc 100644 --- a/libs/portable/src/main.rs +++ b/libs/portable/src/main.rs @@ -30,7 +30,7 @@ fn setup(reader: BinaryReader, dir: Option, clear: bool) -> Option Date: Mon, 26 Dec 2022 11:24:51 +0700 Subject: [PATCH 1263/2015] Update id.rs fixed some errors in the Indonesian translation --- src/lang/id.rs | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/src/lang/id.rs b/src/lang/id.rs index da69f2c76..6862832b3 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -15,7 +15,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("not_ready_status", "Belum siap. Silakan periksa koneksi Anda"), ("Control Remote Desktop", "Kontrol Remote Desktop"), ("Transfer File", "File Transfer"), - ("Connect", "Menghubung"), + ("Connect", "Terhubung"), ("Recent Sessions", "Sesi Terkini"), ("Address Book", "Buku Alamat"), ("Confirmation", "Konfirmasi"), @@ -30,18 +30,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("IP Whitelisting", "Daftar Putih IP"), ("ID/Relay Server", "ID/Relay Server"), ("Import Server Config", "Impor Konfigurasi Server"), - ("Export Server Config", ""), + ("Export Server Config", "Ekspor Konfigutasi Server"), ("Import server configuration successfully", "Impor konfigurasi server berhasil"), - ("Export server configuration successfully", ""), + ("Export server configuration successfully", "Ekspor konfigurasi server berhasil"), ("Invalid server configuration", "Konfigurasi server tidak valid"), ("Clipboard is empty", "Papan klip kosong"), ("Stop service", "Hentikan Layanan"), ("Change ID", "Ubah ID"), ("Website", "Website"), ("About", "Tentang"), - ("About RustDesk", ""), + ("About RustDesk", "Tentang RustDesk"), ("Slogan_tip", ""), - ("Privacy Statement", ""), + ("Privacy Statement", "Pernyataan Privasi"), ("Mute", "Bisukan"), ("Audio Input", "Masukkan Audio"), ("Enhancements", "Peningkatan"), @@ -61,16 +61,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Skip", "Lanjutkan"), ("Close", "Tutup"), ("Retry", "Ulangi"), - ("OK", "OK"), - ("Password Required", "Password dibutukan"), - ("Please enter your password", "Silahkan masukkan password anda"), + ("OK", "Oke"), + ("Password Required", "Kata sandi dibutuhkan"), + ("Please enter your password", "Silahkan masukkan kata sandi anda"), ("Remember password", "Ingat Password"), - ("Wrong Password", "Password Salah"), + ("Wrong Password", "Kata sandi Salah"), ("Do you want to enter again?", "Apakah anda ingin masuk lagi?"), ("Connection Error", "Kesalahan koneksi"), ("Error", "Kesalahan"), ("Reset by the peer", "Setel ulang oleh rekan"), - ("Connecting...", "Hubungkan..."), + ("Connecting...", "Menghubungkan..."), ("Connection in progress. Please wait.", "Koneksi sedang berlangsung. Mohon tunggu."), ("Please try 1 minute later", "Silahkan coba 1 menit lagi"), ("Login Error", "Kesalahan Login"), @@ -111,7 +111,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Block user input", "Blokir masukan pengguna"), ("Unblock user input", "Jangan blokir masukan pengguna"), ("Adjust Window", "Sesuaikan Jendela"), - ("Original", "Original"), + ("Original", "Asli"), ("Shrink", "Susutkan"), ("Stretch", "Regangkan"), ("Scrollbar", "Scroll bar"), @@ -384,18 +384,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland membutuhkan versi distro linux yang lebih tinggi. Silakan coba desktop X11 atau ubah OS Anda."), ("JumpLink", "View"), ("Please Select the screen to be shared(Operate on the peer side).", "Silakan Pilih layar yang akan dibagikan (Operasi di sisi rekan)."), - ("Show RustDesk", ""), - ("This PC", ""), - ("or", ""), - ("Continue with", ""), + ("Show RustDesk", "Tampilkan RustDesk"), + ("This PC", "PC ini"), + ("or", "atau"), + ("Continue with", "Lanjutkan dengan"), ("Elevate", ""), ("Zoom cursor", ""), - ("Accept sessions via password", ""), - ("Accept sessions via click", ""), - ("Accept sessions via both", ""), - ("Please wait for the remote side to accept your session request...", ""), - ("One-time Password", ""), - ("Use one-time password", ""), + ("Accept sessions via password", "Izinkan sesi dengan kata sandi"), + ("Accept sessions via click", "Izinkan sesi dengan klik"), + ("Accept sessions via both", "Izinkan sesi dengan keduanya"), + ("Please wait for the remote side to accept your session request...", "Harap tunggu sisi jarak jauh untuk menerima permintaan sesi Anda..."), + ("One-time Password", "Kata sandi satu kali"), + ("Use one-time password", "Gunakan kata sandi satu kali"), ("One-time password length", ""), ("Request access to your device", ""), ("Hide connection management window", ""), @@ -404,7 +404,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Right click to select tabs", ""), ("Add to Address Book", ""), ("Group", ""), - ("Search", ""), + ("Search", "Pencarian"), ("Closed manually by the web console", ""), ].iter().cloned().collect(); } From 91c77a184cfb02f0237767a0b7e83b2887fb5033 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Mon, 26 Dec 2022 13:10:43 +0800 Subject: [PATCH 1264/2015] fr-FR --- fastlane/metadata/android/fr-FR/full_description.txt | 11 +++++++++++ fastlane/metadata/android/fr-FR/short_description.txt | 1 + 2 files changed, 12 insertions(+) create mode 100644 fastlane/metadata/android/fr-FR/full_description.txt create mode 100644 fastlane/metadata/android/fr-FR/short_description.txt diff --git a/fastlane/metadata/android/fr-FR/full_description.txt b/fastlane/metadata/android/fr-FR/full_description.txt new file mode 100644 index 000000000..effb820d6 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/full_description.txt @@ -0,0 +1,11 @@ +Une application de bureau à distance open source, l'alternative open source à TeamViewer. +Code source : https://github.com/rustdesk/rustdesk +Doc : https://rustdesk.com/docs/en/manual/mobile/ + +Pour qu'un appareil distant puisse contrôler votre appareil Android via la souris ou le toucher, vous devez autoriser RustDesk à utiliser le service "Accessibilité", RustDesk utilise l'API AccessibilityService pour implémenter la télécommande Addroid. + +En plus du contrôle à distance, vous pouvez également transférer facilement des fichiers entre des appareils Android et des PC avec RustDesk. + +Vous avez le contrôle total de vos données, sans aucun souci de sécurité. Vous pouvez utiliser notre serveur de rendez-vous/relais, ou l'auto-hébergement, ou écrire votre propre serveur de rendez-vous/relais. Le serveur auto-hébergé est gratuit et open source : https://github.com/rustdesk/rustdesk-server + +Veuillez télécharger et installer la version de bureau à partir de : https://rustdesk.com, vous pourrez alors accéder et contrôler votre bureau à partir de votre mobile, ou contrôler votre mobile à partir du bureau. diff --git a/fastlane/metadata/android/fr-FR/short_description.txt b/fastlane/metadata/android/fr-FR/short_description.txt new file mode 100644 index 000000000..e1f4b4b0f --- /dev/null +++ b/fastlane/metadata/android/fr-FR/short_description.txt @@ -0,0 +1 @@ +Une application de bureau à distance open source, l'alternative open source à TeamViewer. From c348282dbfed354b6294720cd70970257e9ba135 Mon Sep 17 00:00:00 2001 From: fufesou Date: Mon, 26 Dec 2022 14:34:35 +0800 Subject: [PATCH 1265/2015] fix adjust window, check visiable frame size Signed-off-by: fufesou --- flutter/lib/desktop/widgets/remote_menubar.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 1a7e9aeb7..b60fa7128 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -701,11 +701,11 @@ class _RemoteMenubarState extends State { return false; } double scale = _screen!.scaleFactor; - double selfWidth = _screen!.frame.width; - double selfHeight = _screen!.frame.height; + double selfWidth = _screen!.visibleFrame.width; + double selfHeight = _screen!.visibleFrame.height; if (isFullscreen) { - selfWidth = _screen!.visibleFrame.width; - selfHeight = _screen!.visibleFrame.height; + selfWidth = _screen!.frame.width; + selfHeight = _screen!.frame.height; } final canvasModel = widget.ffi.canvasModel; From 633253647fd9e021e28609703d52b0711fa1a09f Mon Sep 17 00:00:00 2001 From: rustdesk Date: Mon, 26 Dec 2022 16:41:33 +0800 Subject: [PATCH 1266/2015] ipv6 mangle --- libs/hbb_common/src/config.rs | 9 +++++++-- libs/hbb_common/src/lib.rs | 38 ++++++++++++++++++++++++++++++----- 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index d4701aada..e2592bbea 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -1,7 +1,7 @@ use std::{ collections::HashMap, fs, - net::{IpAddr, Ipv4Addr, SocketAddr}, + net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, path::{Path, PathBuf}, sync::{Arc, Mutex, RwLock}, time::SystemTime, @@ -512,7 +512,12 @@ impl Config { #[inline] pub fn get_any_listen_addr() -> SocketAddr { - SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 0) + SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0) + } + + #[inline] + pub fn get_any_listen_addr_v6() -> SocketAddr { + SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0) } pub fn get_rendezvous_server() -> String { diff --git a/libs/hbb_common/src/lib.rs b/libs/hbb_common/src/lib.rs index 0f9f7824c..5a6d24b91 100644 --- a/libs/hbb_common/src/lib.rs +++ b/libs/hbb_common/src/lib.rs @@ -10,7 +10,7 @@ pub use protos::rendezvous as rendezvous_proto; use std::{ fs::File, io::{self, BufRead}, - net::{Ipv4Addr, SocketAddr, SocketAddrV4}, + net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4}, path::Path, time::{self, SystemTime, UNIX_EPOCH}, }; @@ -66,7 +66,7 @@ macro_rules! allow_err { } else { } }; - + ($e:expr, $($arg:tt)*) => { if let Err(err) = $e { log::debug!( @@ -117,13 +117,31 @@ impl AddrMangle { } bytes[..(16 - n_padding)].to_vec() } - _ => { - panic!("Only support ipv4"); + SocketAddr::V6(addr_v6) => { + let mut x = addr_v6.ip().octets().to_vec(); + let port: [u8; 2] = addr_v6.port().to_le_bytes(); + x.push(port[0]); + x.push(port[1]); + x } } } pub fn decode(bytes: &[u8]) -> SocketAddr { + if bytes.len() > 16 { + if bytes.len() != 18 { + return Config::get_any_listen_addr_v6(); + } + #[allow(invalid_value)] + let mut tmp: [u8; 2] = unsafe { std::mem::MaybeUninit::uninit().assume_init() }; + tmp.copy_from_slice(&bytes[16..]); + let port = u16::from_le_bytes(tmp); + #[allow(invalid_value)] + let mut tmp: [u8; 16] = unsafe { std::mem::MaybeUninit::uninit().assume_init() }; + tmp.copy_from_slice(&bytes[..16]); + let ip = std::net::Ipv6Addr::from(tmp); + return SocketAddr::new(IpAddr::V6(ip), port); + } let mut padded = [0u8; 16]; padded[..bytes.len()].copy_from_slice(&bytes); let number = u128::from_le_bytes(padded); @@ -264,11 +282,21 @@ mod tests { fn test_mangle() { let addr = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(192, 168, 16, 32), 21116)); assert_eq!(addr, AddrMangle::decode(&AddrMangle::encode(addr))); + + let addr = "[2001:db8::1]:8080".parse::().unwrap(); + assert_eq!(addr, AddrMangle::decode(&AddrMangle::encode(addr))); + + let addr = "[2001:db8:ff::1111]:80".parse::().unwrap(); + assert_eq!(addr, AddrMangle::decode(&AddrMangle::encode(addr))); } #[test] fn test_allow_err() { allow_err!(Err("test err") as Result<(), &str>); - allow_err!(Err("test err with msg") as Result<(), &str>, "prompt {}", "failed"); + allow_err!( + Err("test err with msg") as Result<(), &str>, + "prompt {}", + "failed" + ); } } From 0d0957cea5b451e9b89a0d719f3d247017d581b3 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Mon, 26 Dec 2022 17:44:29 +0800 Subject: [PATCH 1267/2015] remove u128 compare --- libs/scrap/src/common/android.rs | 2 +- libs/scrap/src/common/mod.rs | 4 ++-- libs/scrap/src/common/quartz.rs | 2 +- libs/scrap/src/dxgi/mod.rs | 2 +- libs/scrap/src/wayland/pipewire.rs | 2 +- libs/scrap/src/x11/capturer.rs | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/libs/scrap/src/common/android.rs b/libs/scrap/src/common/android.rs index 949188712..8daf8e4bb 100644 --- a/libs/scrap/src/common/android.rs +++ b/libs/scrap/src/common/android.rs @@ -13,7 +13,7 @@ lazy_static! { pub struct Capturer { display: Display, bgra: Vec, - saved_raw_data: Vec, // for faster compare and copy + saved_raw_data: Vec, // for faster compare and copy } impl Capturer { diff --git a/libs/scrap/src/common/mod.rs b/libs/scrap/src/common/mod.rs index 82f65537b..0e2f998df 100644 --- a/libs/scrap/src/common/mod.rs +++ b/libs/scrap/src/common/mod.rs @@ -44,8 +44,8 @@ pub mod record; mod vpx; #[inline] -pub fn would_block_if_equal(old: &mut Vec, b: &[u8]) -> std::io::Result<()> { - let b = unsafe { std::slice::from_raw_parts::(b.as_ptr() as _, b.len() / 16) }; +pub fn would_block_if_equal(old: &mut Vec, b: &[u8]) -> std::io::Result<()> { + // does this really help? if b == &old[..] { return Err(std::io::ErrorKind::WouldBlock.into()); } diff --git a/libs/scrap/src/common/quartz.rs b/libs/scrap/src/common/quartz.rs index 6e29c2441..a02d55ebb 100644 --- a/libs/scrap/src/common/quartz.rs +++ b/libs/scrap/src/common/quartz.rs @@ -8,7 +8,7 @@ pub struct Capturer { frame: Arc>>, use_yuv: bool, i420: Vec, - saved_raw_data: Vec, // for faster compare and copy + saved_raw_data: Vec, // for faster compare and copy } impl Capturer { diff --git a/libs/scrap/src/dxgi/mod.rs b/libs/scrap/src/dxgi/mod.rs index 6b60b256d..5829686b5 100644 --- a/libs/scrap/src/dxgi/mod.rs +++ b/libs/scrap/src/dxgi/mod.rs @@ -50,7 +50,7 @@ pub struct Capturer { rotated: Vec, gdi_capturer: Option, gdi_buffer: Vec, - saved_raw_data: Vec, // for faster compare and copy + saved_raw_data: Vec, // for faster compare and copy } impl Capturer { diff --git a/libs/scrap/src/wayland/pipewire.rs b/libs/scrap/src/wayland/pipewire.rs index abbdf3f25..c1c84f98e 100644 --- a/libs/scrap/src/wayland/pipewire.rs +++ b/libs/scrap/src/wayland/pipewire.rs @@ -123,7 +123,7 @@ pub struct PipeWireRecorder { appsink: AppSink, width: usize, height: usize, - saved_raw_data: Vec, // for faster compare and copy + saved_raw_data: Vec, // for faster compare and copy } impl PipeWireRecorder { diff --git a/libs/scrap/src/x11/capturer.rs b/libs/scrap/src/x11/capturer.rs index ed424c35a..0dcfcfdab 100644 --- a/libs/scrap/src/x11/capturer.rs +++ b/libs/scrap/src/x11/capturer.rs @@ -14,7 +14,7 @@ pub struct Capturer { size: usize, use_yuv: bool, yuv: Vec, - saved_raw_data: Vec, // for faster compare and copy + saved_raw_data: Vec, // for faster compare and copy } impl Capturer { From 85620b73a74780b510f2fbbca624baa9726303e3 Mon Sep 17 00:00:00 2001 From: asur4s Date: Wed, 21 Dec 2022 00:03:15 -0800 Subject: [PATCH 1268/2015] opt: get supported keyboard modes --- libs/hbb_common/src/lib.rs | 1 + src/common.rs | 4 ++-- src/ui_session_interface.rs | 14 +++++++++++++- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/libs/hbb_common/src/lib.rs b/libs/hbb_common/src/lib.rs index ae564685f..4245b1d73 100644 --- a/libs/hbb_common/src/lib.rs +++ b/libs/hbb_common/src/lib.rs @@ -40,6 +40,7 @@ pub use tokio_socks::TargetAddr; pub mod password_security; pub use chrono; pub use directories_next; +pub mod keyboard; #[cfg(feature = "quic")] pub type Stream = quic::Connection; diff --git a/src/common.rs b/src/common.rs index 0f3794261..02b4a0c10 100644 --- a/src/common.rs +++ b/src/common.rs @@ -693,8 +693,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 => true, - KeyboardMode::Auto => true, + KeyboardMode::Translate => false, + KeyboardMode::Auto => false, } } diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 0cf2f2e2d..e7ac620ee 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -4,7 +4,7 @@ use crate::client::{ load_config, send_mouse, start_video_audio_threads, FileManager, Key, LoginConfigHandler, QualityStatus, KEY_MAP, }; -use crate::common::GrabState; +use crate::common::{is_keyboard_mode_supported, GrabState}; use crate::keyboard; use crate::{client::Data, client::Interface}; use async_trait::async_trait; @@ -48,6 +48,10 @@ impl Session { self.lc.read().unwrap().custom_image_quality.clone() } + pub fn get_peer_version(&self) -> i64 { + self.lc.read().unwrap().version.clone() + } + pub fn get_keyboard_mode(&self) -> String { self.lc.read().unwrap().keyboard_mode.clone() } @@ -198,6 +202,14 @@ impl Session { crate::platform::is_xfce() } + pub fn get_supported_keyboard_modes(&self) -> Vec { + let version = self.get_peer_version(); + KeyboardMode::iter() + .filter(|&mode| is_keyboard_mode_supported(mode, version)) + .map(|&mode| mode) + .collect::>() + } + pub fn remove_port_forward(&self, port: i32) { let mut config = self.load_config(); config.port_forwards = config From a3769ca8e937116faaaaf1a4a64235e26fd3b332 Mon Sep 17 00:00:00 2001 From: asur4s Date: Mon, 26 Dec 2022 02:30:25 -0800 Subject: [PATCH 1269/2015] opt: map mode hide when unsupported --- .../lib/desktop/widgets/remote_menubar.dart | 23 +++++++++++++++---- src/flutter_ffi.rs | 18 +++++++++++++++ 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index c1a7bdce2..fc35fb47e 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -1185,10 +1185,25 @@ class _RemoteMenubarState extends State { final keyboardMenu = [ MenuEntryRadios( text: translate('Ratio'), - optionsGetter: () => [ - MenuEntryRadioOption(text: translate('Legacy mode'), value: 'legacy'), - MenuEntryRadioOption(text: translate('Map mode'), value: 'map'), - ], + optionsGetter: () { + List list = []; + List modes = ["legacy"]; + + if (bind.sessionIsKeyboardModeSupported(id: widget.id, mode: "map")) { + modes.add("map"); + } + + for (String mode in modes) { + if (mode == "legacy") { + list.add(MenuEntryRadioOption( + text: translate('Legacy mode'), value: 'legacy')); + } else if (mode == "map") { + list.add(MenuEntryRadioOption( + text: translate('Map mode'), value: 'map')); + } + } + return list; + }, curOptionGetter: () async { return await bind.sessionGetKeyboardMode(id: widget.id) ?? "legacy"; }, diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index ddfaad06d..61b01f50c 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -7,11 +7,14 @@ use std::{ use flutter_rust_bridge::{StreamSink, SyncReturn, ZeroCopyBuffer}; use serde_json::json; +use crate::common::is_keyboard_mode_supported; +use hbb_common::message_proto::KeyboardMode; use hbb_common::ResultType; use hbb_common::{ config::{self, LocalConfig, PeerConfig, ONLINE}, fs, log, }; +use std::str::FromStr; // use crate::hbbs_http::account::AuthResult; @@ -245,6 +248,21 @@ pub fn session_get_custom_image_quality(id: String) -> Option> { } } +pub fn session_is_keyboard_mode_supported(id: String, mode: String) -> SyncReturn { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + if let Ok(mode) = KeyboardMode::from_str(&mode[..]) { + SyncReturn(is_keyboard_mode_supported( + &mode, + session.get_peer_version(), + )) + } else { + SyncReturn(false) + } + } else { + SyncReturn(false) + } +} + pub fn session_set_custom_image_quality(id: String, value: i32) { if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) { session.save_custom_image_quality(value); From ecce656b8d455d21a339e00c7866af32353482b2 Mon Sep 17 00:00:00 2001 From: "Miguel F. G" <116861809+flusheDData@users.noreply.github.com> Date: Mon, 26 Dec 2022 15:06:26 +0100 Subject: [PATCH 1270/2015] Update es.rs --- src/lang/es.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/es.rs b/src/lang/es.rs index 6923029ff..0a2ccfd35 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -405,6 +405,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", "Añadir a la libreta de direcciones"), ("Group", "Grupo"), ("Search", "Búsqueda"), - ("Closed manually by the web console", ""), + ("Closed manually by the web console", "Cerrado manualmente por la consola web"), ].iter().cloned().collect(); } From 0e4935592d50372cf1d2a2e8b86c797ebc5dfba3 Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 26 Dec 2022 22:18:34 +0800 Subject: [PATCH 1271/2015] part revert #2602 Signed-off-by: 21pages --- src/client.rs | 10 +++------- src/client/io_loop.rs | 6 +----- src/ui_session_interface.rs | 11 +---------- 3 files changed, 5 insertions(+), 22 deletions(-) diff --git a/src/client.rs b/src/client.rs index 2922a488b..f08f50ffd 100644 --- a/src/client.rs +++ b/src/client.rs @@ -892,8 +892,6 @@ pub struct LoginConfigHandler { pub supported_encoding: Option<(bool, bool)>, pub restarting_remote_device: bool, pub force_relay: bool, - pub direct: Option, - pub received: bool, } impl Deref for LoginConfigHandler { @@ -931,8 +929,6 @@ impl LoginConfigHandler { self.supported_encoding = None; self.restarting_remote_device = false; self.force_relay = !self.get_option("force-always-relay").is_empty(); - self.direct = None; - self.received = false; } /// Check if the client should auto login. @@ -1819,7 +1815,6 @@ pub trait Interface: Send + Clone + 'static + Sized { fn handle_login_error(&mut self, err: &str) -> bool; fn handle_peer_info(&mut self, pi: PeerInfo); fn set_force_relay(&mut self, direct: bool, received: bool); - fn set_connection_info(&mut self, direct: bool, received: bool); fn is_file_transfer(&self) -> bool; fn is_port_forward(&self) -> bool; fn is_rdp(&self) -> bool; @@ -1995,10 +1990,11 @@ lazy_static::lazy_static! { /// * `title` - The title of the message. /// * `text` - The text of the message. #[inline] -pub fn check_if_retry(msgtype: &str, title: &str, text: &str, retry_for_relay: bool) -> bool { +pub fn check_if_retry(msgtype: &str, title: &str, text: &str) -> bool { msgtype == "error" && title == "Connection Error" - && ((text.contains("10054") || text.contains("104")) && retry_for_relay + && (text.contains("10054") + || text.contains("104") || (!text.to_lowercase().contains("offline") && !text.to_lowercase().contains("exist") && !text.to_lowercase().contains("handshake") diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index ceddbc004..326857d3f 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -107,7 +107,6 @@ impl Remote { 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); // just build for now #[cfg(not(windows))] @@ -145,10 +144,7 @@ impl Remote { } Ok(ref bytes) => { last_recv_time = Instant::now(); - if !received { - received = true; - self.handler.set_connection_info(direct, true); - } + received = true; self.data_count.fetch_add(bytes.len(), Ordering::Relaxed); if !self.handle_msg_from_peer(bytes, &mut peer).await { break diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 55984e343..434086445 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -658,10 +658,7 @@ impl Interface for Session { } fn msgbox(&self, msgtype: &str, title: &str, text: &str, link: &str) { - let direct = self.lc.read().unwrap().direct.unwrap_or_default(); - let received = self.lc.read().unwrap().received; - let retry_for_relay = direct && !received; - let retry = check_if_retry(msgtype, title, text, retry_for_relay); + let retry = check_if_retry(msgtype, title, text); self.ui_handler.msgbox(msgtype, title, text, link, retry); } @@ -749,12 +746,6 @@ impl Interface for Session { } } - fn set_connection_info(&mut self, direct: bool, received: bool) { - let mut lc = self.lc.write().unwrap(); - lc.direct = Some(direct); - lc.received = received; - } - fn set_force_relay(&mut self, direct: bool, received: bool) { let mut lc = self.lc.write().unwrap(); lc.force_relay = false; From 7d5876f7b899042eb4f1e4c2b3bfd230a91f41bb Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 27 Dec 2022 11:42:48 +0800 Subject: [PATCH 1272/2015] refactor handle_login_error to avoid dead lock, and recover pr #2602 --- src/cli.rs | 2 +- src/client.rs | 66 +++++++++++++++++++++---------------- src/client/io_loop.rs | 6 +++- src/ui_session_interface.rs | 19 ++++++++--- 4 files changed, 57 insertions(+), 36 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 59c356a5a..dd39569ca 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -57,7 +57,7 @@ impl Interface for Session { } fn handle_login_error(&mut self, err: &str) -> bool { - self.lc.write().unwrap().handle_login_error(err, self) + handle_login_error(self.lc.clone(), err, self) } fn handle_peer_info(&mut self, pi: PeerInfo) { diff --git a/src/client.rs b/src/client.rs index f08f50ffd..bf11c4b6d 100644 --- a/src/client.rs +++ b/src/client.rs @@ -892,6 +892,8 @@ pub struct LoginConfigHandler { pub supported_encoding: Option<(bool, bool)>, pub restarting_remote_device: bool, pub force_relay: bool, + pub direct: Option, + pub received: bool, } impl Deref for LoginConfigHandler { @@ -929,6 +931,8 @@ impl LoginConfigHandler { self.supported_encoding = None; self.restarting_remote_device = false; self.force_relay = !self.get_option("force-always-relay").is_empty(); + self.direct = None; + self.received = false; } /// Check if the client should auto login. @@ -1338,32 +1342,6 @@ impl LoginConfigHandler { } } - /// Handle login error. - /// Return true if the password is wrong, return false if there's an actual error. - pub fn handle_login_error(&mut self, err: &str, interface: &impl Interface) -> bool { - if err == "Wrong Password" { - self.password = Default::default(); - interface.msgbox("re-input-password", err, "Do you want to enter again?", ""); - true - } else if err == "No Password Access" { - self.password = Default::default(); - interface.msgbox( - "wait-remote-accept-nook", - "Prompt", - "Please wait for the remote side to accept your session request...", - "", - ); - true - } else { - if err.contains(SCRAP_X11_REQUIRED) { - interface.msgbox("error", "Login Error", err, SCRAP_X11_REF_URL); - } else { - interface.msgbox("error", "Login Error", err, ""); - } - false - } - } - /// Get user name. /// Return the name of the given peer. If the peer has no name, return the name in the config. /// @@ -1726,6 +1704,36 @@ fn _input_os_password(p: String, activate: bool, interface: impl Interface) { interface.send(Data::Message(msg_out)); } +/// Handle login error. +/// Return true if the password is wrong, return false if there's an actual error. +pub fn handle_login_error( + lc: Arc>, + err: &str, + interface: &impl Interface, +) -> bool { + if err == "Wrong Password" { + lc.write().unwrap().password = Default::default(); + interface.msgbox("re-input-password", err, "Do you want to enter again?", ""); + true + } else if err == "No Password Access" { + lc.write().unwrap().password = Default::default(); + interface.msgbox( + "wait-remote-accept-nook", + "Prompt", + "Please wait for the remote side to accept your session request...", + "", + ); + true + } else { + if err.contains(SCRAP_X11_REQUIRED) { + interface.msgbox("error", "Login Error", err, SCRAP_X11_REF_URL); + } else { + interface.msgbox("error", "Login Error", err, ""); + } + false + } +} + /// Handle hash message sent by peer. /// Hash will be used for login. /// @@ -1815,6 +1823,7 @@ pub trait Interface: Send + Clone + 'static + Sized { fn handle_login_error(&mut self, err: &str) -> bool; fn handle_peer_info(&mut self, pi: PeerInfo); fn set_force_relay(&mut self, direct: bool, received: bool); + fn set_connection_info(&mut self, direct: bool, received: bool); fn is_file_transfer(&self) -> bool; fn is_port_forward(&self) -> bool; fn is_rdp(&self) -> bool; @@ -1990,11 +1999,10 @@ lazy_static::lazy_static! { /// * `title` - The title of the message. /// * `text` - The text of the message. #[inline] -pub fn check_if_retry(msgtype: &str, title: &str, text: &str) -> bool { +pub fn check_if_retry(msgtype: &str, title: &str, text: &str, retry_for_relay: bool) -> bool { msgtype == "error" && title == "Connection Error" - && (text.contains("10054") - || text.contains("104") + && ((text.contains("10054") || text.contains("104")) && retry_for_relay || (!text.to_lowercase().contains("offline") && !text.to_lowercase().contains("exist") && !text.to_lowercase().contains("handshake") diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 326857d3f..ceddbc004 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -107,6 +107,7 @@ impl Remote { 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); // just build for now #[cfg(not(windows))] @@ -144,7 +145,10 @@ impl Remote { } Ok(ref bytes) => { last_recv_time = Instant::now(); - received = true; + if !received { + received = true; + self.handler.set_connection_info(direct, true); + } self.data_count.fetch_add(bytes.len(), Ordering::Relaxed); if !self.handle_msg_from_peer(bytes, &mut peer).await { break diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 434086445..6f115571c 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -1,8 +1,8 @@ use crate::client::io_loop::Remote; use crate::client::{ - check_if_retry, handle_hash, handle_login_from_ui, handle_test_delay, input_os_password, - load_config, send_mouse, start_video_audio_threads, FileManager, Key, LoginConfigHandler, - QualityStatus, KEY_MAP, + 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::GrabState; use crate::keyboard; @@ -658,12 +658,15 @@ impl Interface for Session { } fn msgbox(&self, msgtype: &str, title: &str, text: &str, link: &str) { - let retry = check_if_retry(msgtype, title, text); + let direct = self.lc.read().unwrap().direct.unwrap_or_default(); + let received = self.lc.read().unwrap().received; + let retry_for_relay = direct && !received; + let retry = check_if_retry(msgtype, title, text, retry_for_relay); self.ui_handler.msgbox(msgtype, title, text, link, retry); } fn handle_login_error(&mut self, err: &str) -> bool { - self.lc.write().unwrap().handle_login_error(err, self) + handle_login_error(self.lc.clone(), err, self) } fn handle_peer_info(&mut self, mut pi: PeerInfo) { @@ -746,6 +749,12 @@ impl Interface for Session { } } + fn set_connection_info(&mut self, direct: bool, received: bool) { + let mut lc = self.lc.write().unwrap(); + lc.direct = Some(direct); + lc.received = received; + } + fn set_force_relay(&mut self, direct: bool, received: bool) { let mut lc = self.lc.write().unwrap(); lc.force_relay = false; From 71bd35f8b29684e79e0480c9344923c3fcc3dadf Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 27 Dec 2022 12:30:23 +0800 Subject: [PATCH 1273/2015] refactor socket_client to prepare for nat64 --- libs/hbb_common/src/socket_client.rs | 88 +++++++++++++++++++++------- libs/hbb_common/src/udp.rs | 4 +- 2 files changed, 69 insertions(+), 23 deletions(-) diff --git a/libs/hbb_common/src/socket_client.rs b/libs/hbb_common/src/socket_client.rs index 72ab73f16..e09d38c3a 100644 --- a/libs/hbb_common/src/socket_client.rs +++ b/libs/hbb_common/src/socket_client.rs @@ -6,15 +6,25 @@ use crate::{ }; use anyhow::Context; use std::net::SocketAddr; -use tokio::net::ToSocketAddrs; +use std::net::ToSocketAddrs; use tokio_socks::{IntoTargetAddr, TargetAddr}; -fn to_socket_addr(host: &str) -> ResultType { - use std::net::ToSocketAddrs; - host.to_socket_addrs()? - .filter(|x| x.is_ipv4()) - .next() - .context("Failed to solve") +fn to_socket_addr(host: T) -> ResultType { + let mut addr_ipv4 = None; + let mut addr_ipv6 = None; + for addr in host.to_socket_addrs()? { + if addr.is_ipv4() && addr_ipv4.is_none() { + addr_ipv4 = Some(addr); + } + if addr.is_ipv6() && addr_ipv6.is_none() { + addr_ipv6 = Some(addr); + } + } + if let Some(addr) = addr_ipv4 { + Ok(addr) + } else { + addr_ipv6.context("Failed to solve") + } } pub fn get_target_addr(host: &str) -> ResultType> { @@ -44,15 +54,43 @@ pub fn test_if_valid_server(host: &str) -> String { } } -pub async fn connect_tcp<'t, T: IntoTargetAddr<'t>>( +pub trait IntoTargetAddr2<'a> { + /// Converts the value of self to a `TargetAddr`. + fn into_target_addr2(&self) -> ResultType>; +} + +impl<'a> IntoTargetAddr2<'a> for SocketAddr { + fn into_target_addr2(&self) -> ResultType> { + Ok(TargetAddr::Ip(*self)) + } +} + +impl<'a> IntoTargetAddr2<'a> for TargetAddr<'a> { + fn into_target_addr2(&self) -> ResultType> { + Ok(self.clone()) + } +} + +impl<'a> IntoTargetAddr2<'a> for String { + fn into_target_addr2(&self) -> ResultType> { + Ok(to_socket_addr(self)?.into_target_addr()?) + } +} + +impl<'a> IntoTargetAddr2<'a> for &str { + fn into_target_addr2(&self) -> ResultType> { + Ok(to_socket_addr(self)?.into_target_addr()?) + } +} + +pub async fn connect_tcp<'t, T: IntoTargetAddr2<'t> + std::fmt::Debug>( target: T, local: SocketAddr, ms_timeout: u64, ) -> ResultType { - let target_addr = target.into_target_addr()?; - + let target_addr = target.into_target_addr2()?; if let Some(conf) = Config::get_socks() { - FramedStream::connect( + return FramedStream::connect( conf.proxy.as_str(), target_addr, local, @@ -60,23 +98,21 @@ pub async fn connect_tcp<'t, T: IntoTargetAddr<'t>>( conf.password.as_str(), ms_timeout, ) - .await - } else { - let addr = std::net::ToSocketAddrs::to_socket_addrs(&target_addr)? - .filter(|x| x.is_ipv4()) - .next() - .context("Invalid target addr, no valid ipv4 address can be resolved.")?; - Ok(FramedStream::new(addr, local, ms_timeout).await?) + .await; } + let addr = ToSocketAddrs::to_socket_addrs(&target_addr)? + .next() + .context(format!("Invalid target addr: {:?}", target))?; + Ok(FramedStream::new(addr, local, ms_timeout).await?) } pub async fn new_udp(local: T, ms_timeout: u64) -> ResultType { match Config::get_socks() { - None => Ok(FramedSocket::new(local).await?), + None => Ok(FramedSocket::new(to_socket_addr(&local)?).await?), Some(conf) => { let socket = FramedSocket::new_proxy( conf.proxy.as_str(), - local, + to_socket_addr(local)?, conf.username.as_str(), conf.password.as_str(), ms_timeout, @@ -89,7 +125,17 @@ pub async fn new_udp(local: T, ms_timeout: u64) -> ResultType< pub async fn rebind_udp(local: T) -> ResultType> { match Config::get_network_type() { - NetworkType::Direct => Ok(Some(FramedSocket::new(local).await?)), + NetworkType::Direct => Ok(Some(FramedSocket::new(to_socket_addr(local)?).await?)), _ => Ok(None), } } +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_to_socket_addr() { + assert_eq!(to_socket_addr("127.0.0.1:8080").unwrap(), "127.0.0.1:8080".parse().unwrap()); + assert!(to_socket_addr("[ff::]:0").unwrap().is_ipv6()); + assert!(to_socket_addr("xx").is_err()); + } +} \ No newline at end of file diff --git a/libs/hbb_common/src/udp.rs b/libs/hbb_common/src/udp.rs index 3532dd1e0..1f5bf2637 100644 --- a/libs/hbb_common/src/udp.rs +++ b/libs/hbb_common/src/udp.rs @@ -49,7 +49,7 @@ impl FramedSocket { #[allow(clippy::never_loop)] pub async fn new_reuse(addr: T) -> ResultType { - for addr in addr.to_socket_addrs()?.filter(|x| x.is_ipv4()) { + for addr in addr.to_socket_addrs()? { let socket = new_socket(addr, true, 0)?.into_udp_socket(); return Ok(Self::Direct(UdpFramed::new( UdpSocket::from_std(socket)?, @@ -63,7 +63,7 @@ impl FramedSocket { addr: T, buf_size: usize, ) -> ResultType { - for addr in addr.to_socket_addrs()?.filter(|x| x.is_ipv4()) { + for addr in addr.to_socket_addrs()? { return Ok(Self::Direct(UdpFramed::new( UdpSocket::from_std(new_socket(addr, false, buf_size)?.into_udp_socket())?, BytesCodec::new(), From b241925fe093dc4da804a5aac419375f4ca7653f Mon Sep 17 00:00:00 2001 From: fufesou Date: Mon, 26 Dec 2022 16:25:09 +0800 Subject: [PATCH 1274/2015] keyboard support more keys Signed-off-by: fufesou --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 5a7d410fc..392c2c1f7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4305,7 +4305,7 @@ dependencies = [ [[package]] name = "rdev" version = "0.5.0-2" -source = "git+https://github.com/asur4s/rdev#81aa6559e931fed914e0d38edfd98cbe4bc908c1" +source = "git+https://github.com/asur4s/rdev#e2d6171a6d844aaac19cdbda04239e6b8cc483e5" dependencies = [ "cocoa", "core-foundation 0.9.3", From 22e1e7c8d84d145b33475b27b7da6e1729ac84d2 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 27 Dec 2022 16:18:28 +0800 Subject: [PATCH 1275/2015] fix: regrab key from minimize restore on windows --- flutter/lib/desktop/pages/remote_page.dart | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index dd569a110..e16a61890 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -148,6 +148,16 @@ class _RemotePageState extends State } } + @override + void onWindowRestore() { + super.onWindowRestore(); + // On windows, we use `onWindowRestore` way to handle window restore from + // a minimized state. + if (Platform.isWindows) { + _isWindowBlur = false; + } + } + @override void dispose() { debugPrint("REMOTE PAGE dispose ${widget.id}"); From d244551ad89ff2d686f780b21057f5b020ab334b Mon Sep 17 00:00:00 2001 From: Nikos Fazakis Date: Tue, 27 Dec 2022 11:36:53 +0200 Subject: [PATCH 1276/2015] Mac Wakeup using caffeinate --- src/server.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/server.rs b/src/server.rs index d08dd2672..707557840 100644 --- a/src/server.rs +++ b/src/server.rs @@ -104,6 +104,11 @@ async fn accept_connection_(server: ServerPtr, socket: Stream, secure: bool) -> if let Ok((stream, addr)) = timeout(CONNECT_TIMEOUT, listener.accept()).await? { stream.set_nodelay(true).ok(); let stream_addr = stream.local_addr()?; + if cfg!(target_os = "macos") { + use std::process::Command; + Command::new("/usr/bin/caffeinate").arg("-u").arg("-t 2").output().expect("failed to execute caffeinate"); + println!("wake up macos..."); + } create_tcp_connection(server, Stream::from(stream, stream_addr), addr, secure).await?; } Ok(()) From 48e684335ee7da31b8fdd775b63dea6cb5686e81 Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 27 Dec 2022 16:45:13 +0800 Subject: [PATCH 1277/2015] choose keyboard layout type, mid commit Signed-off-by: fufesou --- flutter/lib/desktop/pages/remote_page.dart | 5 + .../widgets/kb_layout_type_chooser.dart | 227 ++++++++++++++++++ flutter/lib/desktop/widgets/login.dart | 10 +- .../lib/desktop/widgets/remote_menubar.dart | 53 +++- flutter/lib/models/model.dart | 10 + libs/hbb_common/src/config.rs | 10 + src/flutter_ffi.rs | 8 + src/keyboard.rs | 18 +- src/lang/cn.rs | 2 + src/lang/tw.rs | 2 + src/ui_interface.rs | 10 + 11 files changed, 345 insertions(+), 10 deletions(-) create mode 100644 flutter/lib/desktop/widgets/kb_layout_type_chooser.dart diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index dd569a110..5da5c066c 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -23,6 +23,7 @@ import '../../models/model.dart'; import '../../models/platform_model.dart'; import '../../common/shared_state.dart'; import '../widgets/remote_menubar.dart'; +import '../widgets/kb_layout_type_chooser.dart'; bool _isCustomCursorInited = false; final SimpleWrapper _firstEnterImage = SimpleWrapper(false); @@ -95,6 +96,10 @@ class _RemotePageState extends State _initStates(widget.id); _ffi = FFI(); Get.put(_ffi, tag: widget.id); + _ffi.imageModel.addCallbackOnFirstImage((String peerId) { + showKBLayoutTypeChooserIfNeeded( + _ffi.ffiModel.pi.platform, _ffi.dialogManager); + }); _ffi.start(widget.id); WidgetsBinding.instance.addPostFrameCallback((_) { SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); diff --git a/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart b/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart new file mode 100644 index 000000000..35ab4c81e --- /dev/null +++ b/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart @@ -0,0 +1,227 @@ +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; + +import '../../common.dart'; + +typedef KBChoosedCallback = bool Function(String); + +const double _kImageMarginVertical = 6.0; +const double _kImageMarginHorizental = 10.0; +const double _kImageBoarderWidth = 4.0; +const double _kImagePaddingWidth = 4.0; +const Color _kImageBorderColor = Color.fromARGB(125, 202, 247, 2); +const double _kBorderRadius = 6.0; +const String _kKBLayoutTypeISO = 'ISO'; +const String _kKBLayoutTypeNotISO = 'Not ISO'; + +const _kKBLayoutImageMap = { + _kKBLayoutTypeISO: 'KB_LAYOUT_ISO', + _kKBLayoutTypeNotISO: 'KB_LAYOUT_NOT_ISO', +}; + +class _KBImage extends StatelessWidget { + final String kbLayoutType; + final double imageWidth; + final RxString choosedType; + const _KBImage({ + Key? key, + required this.kbLayoutType, + required this.imageWidth, + required this.choosedType, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Obx(() { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(_kBorderRadius), + border: Border.all( + color: choosedType.value == kbLayoutType + ? _kImageBorderColor + : Colors.transparent, + width: _kImageBoarderWidth, + ), + ), + margin: EdgeInsets.symmetric( + horizontal: _kImageMarginHorizental, + vertical: _kImageMarginVertical, + ), + padding: EdgeInsets.all(_kImagePaddingWidth), + child: SvgPicture.asset( + 'assets/${_kKBLayoutImageMap[kbLayoutType] ?? ""}.svg', + width: imageWidth - + _kImageMarginHorizental * 2 - + _kImagePaddingWidth * 2 - + _kImageBoarderWidth * 2, + ), + ); + }); + } +} + +class _KBChooser extends StatelessWidget { + final String kbLayoutType; + final double imageWidth; + final RxString choosedType; + final KBChoosedCallback cb; + const _KBChooser({ + Key? key, + required this.kbLayoutType, + required this.imageWidth, + required this.choosedType, + required this.cb, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + TextButton( + onPressed: () { + choosedType.value = kbLayoutType; + }, + child: _KBImage( + kbLayoutType: kbLayoutType, + imageWidth: imageWidth, + choosedType: choosedType, + ), + style: TextButton.styleFrom(padding: EdgeInsets.zero), + ), + TextButton( + child: Row( + children: [ + Obx(() => Radio( + splashRadius: 0, + value: kbLayoutType, + groupValue: choosedType.value, + onChanged: (String? newValue) { + if (newValue != null) { + if (cb(newValue)) { + choosedType.value = newValue; + } + } + }, + )), + Text(kbLayoutType), + ], + ), + onPressed: () { + if (cb(kbLayoutType)) { + choosedType.value = kbLayoutType; + } + }, + ), + ], + ); + } +} + +class KBLayoutTypeChooser extends StatelessWidget { + final RxString choosedType; + final double width; + final double height; + final double dividerWidth; + final KBChoosedCallback cb; + KBLayoutTypeChooser({ + Key? key, + required this.choosedType, + required this.width, + required this.height, + required this.dividerWidth, + required this.cb, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final imageWidth = width / 2 - dividerWidth; + return Container( + color: Colors.white, + child: SizedBox( + width: width, + height: height, + child: Center( + child: Row( + children: [ + _KBChooser( + kbLayoutType: _kKBLayoutTypeISO, + imageWidth: imageWidth, + choosedType: choosedType, + cb: cb, + ), + VerticalDivider( + width: dividerWidth * 2, + ), + _KBChooser( + kbLayoutType: _kKBLayoutTypeNotISO, + imageWidth: imageWidth, + choosedType: choosedType, + cb: cb, + ), + ], + ), + ), + ), + ); + } +} + +RxString KBLayoutType = ''.obs; + +String getLocalPlatformForKBLayoutType(String peerPlatform) { + String localPlatform = ''; + if (peerPlatform != 'Mac OS') { + return localPlatform; + } + + if (Platform.isWindows) { + localPlatform = 'Windows'; + } else if (Platform.isLinux) { + localPlatform = 'Linux'; + } + // to-do: web desktop support ? + return localPlatform; +} + +showKBLayoutTypeChooserIfNeeded( + String peerPlatform, + OverlayDialogManager dialogManager, +) async { + final localPlatform = getLocalPlatformForKBLayoutType(peerPlatform); + if (localPlatform == '') { + return; + } + KBLayoutType.value = bind.getLocalKbLayoutType(); + if (KBLayoutType.value == _kKBLayoutTypeISO || + KBLayoutType.value == _kKBLayoutTypeNotISO) { + return; + } + showKBLayoutTypeChooser(localPlatform, dialogManager); +} + +showKBLayoutTypeChooser( + String localPlatform, + OverlayDialogManager dialogManager, +) { + dialogManager.show((setState, close) { + return CustomAlertDialog( + title: + Text('${translate('Select local keyboard type')} ($localPlatform)'), + content: KBLayoutTypeChooser( + choosedType: KBLayoutType, + width: 360, + height: 200, + dividerWidth: 4.0, + cb: (String v) { + bind.setLocalKbLayoutType(kbLayoutType: v); + KBLayoutType.value = bind.getLocalKbLayoutType(); + return v == KBLayoutType.value; + }), + actions: [msgBoxButton(translate('Close'), close)], + onCancel: close, + ); + }); +} diff --git a/flutter/lib/desktop/widgets/login.dart b/flutter/lib/desktop/widgets/login.dart index 053653ab3..0736f0864 100644 --- a/flutter/lib/desktop/widgets/login.dart +++ b/flutter/lib/desktop/widgets/login.dart @@ -9,7 +9,7 @@ import 'package:url_launcher/url_launcher.dart'; import '../../common.dart'; -final kMidButtonPadding = const EdgeInsets.fromLTRB(15, 0, 15, 0); +final _kMidButtonPadding = const EdgeInsets.fromLTRB(15, 0, 15, 0); class _IconOP extends StatelessWidget { final String icon; @@ -53,7 +53,7 @@ class ButtonOP extends StatelessWidget { Expanded( child: Container( height: height, - padding: kMidButtonPadding, + padding: _kMidButtonPadding, child: Obx(() => ElevatedButton( style: ElevatedButton.styleFrom( primary: curOP.value.isEmpty || curOP.value == op @@ -315,7 +315,7 @@ class LoginWidgetUserPass extends StatelessWidget { height: 8.0, ), Container( - padding: kMidButtonPadding, + padding: _kMidButtonPadding, child: Row( children: [ ConstrainedBox( @@ -343,7 +343,7 @@ class LoginWidgetUserPass extends StatelessWidget { height: 8.0, ), Container( - padding: kMidButtonPadding, + padding: _kMidButtonPadding, child: Row( children: [ ConstrainedBox( @@ -377,7 +377,7 @@ class LoginWidgetUserPass extends StatelessWidget { Expanded( child: Container( height: 38, - padding: kMidButtonPadding, + padding: _kMidButtonPadding, child: Obx(() => ElevatedButton( style: curOP.value.isEmpty || curOP.value == 'rustdesk' ? null diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index b60fa7128..0cd4ee00f 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -22,6 +22,7 @@ import '../../models/platform_model.dart'; import '../../common/shared_state.dart'; import './popup_menu.dart'; import './material_mod_popup_menu.dart' as mod_menu; +import './kb_layout_type_chooser.dart'; class MenubarState { final kStoreKey = 'remoteMenubarState'; @@ -1187,7 +1188,7 @@ class _RemoteMenubarState extends State { } List> _getKeyboardMenu() { - final keyboardMenu = [ + final List> keyboardMenu = [ MenuEntryRadios( text: translate('Ratio'), optionsGetter: () => [ @@ -1203,7 +1204,55 @@ class _RemoteMenubarState extends State { }, ) ]; - + final localPlatform = + getLocalPlatformForKBLayoutType(widget.ffi.ffiModel.pi.platform); + if (localPlatform != '') { + keyboardMenu.add(MenuEntryDivider()); + keyboardMenu.add( + MenuEntryButton( + childBuilder: (TextStyle? style) => Container( + alignment: AlignmentDirectional.center, + height: _MenubarTheme.height, + child: Row( + children: [ + Obx(() => RichText( + text: TextSpan( + text: '${translate('Local keyboard type')}: ', + style: DefaultTextStyle.of(context).style, + children: [ + TextSpan( + text: KBLayoutType.value, + style: TextStyle(fontWeight: FontWeight.bold), + ), + ], + ), + )), + Expanded( + child: Align( + alignment: Alignment.centerRight, + child: Transform.scale( + scale: 0.8, + child: IconButton( + padding: EdgeInsets.zero, + icon: const Icon(Icons.settings), + onPressed: () { + if (Navigator.canPop(context)) { + Navigator.pop(context); + } + showKBLayoutTypeChooser( + localPlatform, widget.ffi.dialogManager); + }, + ), + ), + )) + ], + )), + proc: () {}, + padding: EdgeInsets.zero, + dismissOnClicked: false, + ), + ); + } return keyboardMenu; } diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 3659e8d58..63062a928 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -381,12 +381,22 @@ class ImageModel with ChangeNotifier { WeakReference parent; + final List _callbacksOnFirstImage = []; + ImageModel(this.parent); + addCallbackOnFirstImage(Function(String) cb) => + _callbacksOnFirstImage.add(cb); + onRgba(Uint8List rgba) { if (_waitForImage[id]!) { _waitForImage[id] = false; parent.target?.dialogManager.dismissAll(); + if (isDesktop) { + for (final cb in _callbacksOnFirstImage) { + cb(id); + } + } } final pid = parent.target?.id; ui.decodeImageFromPixels( diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index e2592bbea..d86ac3463 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -998,6 +998,8 @@ pub struct LocalConfig { #[serde(default)] remote_id: String, // latest used one #[serde(default)] + kb_layout_type: String, + #[serde(default)] size: Size, #[serde(default)] pub fav: Vec, @@ -1017,6 +1019,14 @@ impl LocalConfig { Config::store_(self, "_local"); } + pub fn get_kb_layout_type() -> String { + LOCAL_CONFIG.read().unwrap().kb_layout_type.clone() + } + + pub fn set_kb_layout_type(kb_layout_type: String) { + LOCAL_CONFIG.write().unwrap().kb_layout_type = kb_layout_type + } + pub fn get_size() -> Size { LOCAL_CONFIG.read().unwrap().size } diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index dc9d7a04a..3be0c9fed 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -181,6 +181,14 @@ pub fn set_local_flutter_config(k: String, v: String) { ui_interface::set_local_flutter_config(k, v); } +pub fn get_local_kb_layout_type() -> SyncReturn { + SyncReturn(ui_interface::get_kb_layout_type()) +} + +pub fn set_local_kb_layout_type(kb_layout_type: String) { + ui_interface::set_kb_layout_type(kb_layout_type) +} + pub fn session_get_view_style(id: String) -> Option { if let Some(session) = SESSIONS.read().unwrap().get(&id) { Some(session.get_view_style()) diff --git a/src/keyboard.rs b/src/keyboard.rs index 4b42bdf5d..d22573fbc 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -6,7 +6,7 @@ use crate::flutter::FlutterHandler; #[cfg(not(feature = "flutter"))] use crate::ui::remote::SciterHandler; use crate::ui_session_interface::Session; -use hbb_common::{log, message_proto::*}; +use hbb_common::{log, message_proto::*, config::LocalConfig}; use rdev::{Event, EventType, Key}; #[cfg(any(target_os = "windows", target_os = "macos"))] use std::sync::atomic::{AtomicBool, Ordering}; @@ -620,7 +620,13 @@ pub fn map_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option event.scan_code, - "macos" => rdev::win_scancode_to_macos_code(event.scan_code)?, + "macos" => { + if 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)?, }; #[cfg(target_os = "macos")] @@ -632,7 +638,13 @@ pub fn map_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option rdev::linux_code_to_win_scancode(event.code as _)?, - "macos" => rdev::linux_code_to_macos_code(event.code as _)?, + "macos" => { + if LocalConfig::get_kb_layout_type() == "ISO" { + rdev::linux_code_to_macos_iso_code(event.scan_code)? + } else { + rdev::linux_code_to_macos_code(event.code as _)? + } + }, _ => event.code as _, }; #[cfg(any(target_os = "android", target_os = "ios"))] diff --git a/src/lang/cn.rs b/src/lang/cn.rs index be0d7803e..5d04268b0 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -407,5 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Group", "小组"), ("Search", "搜索"), ("Closed manually by the web console", "被web控制台手动关闭"), + ("Local keyboard type", "本地键盘类型"), + ("Select local keyboard type", "请选择本地键盘类型"), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 6eef3656c..301384ea3 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -406,5 +406,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Group", "小組"), ("Search", "搜索"), ("Closed manually by the web console", "被web控制台手動關閉"), + ("Local keyboard type", "本地鍵盤類型"), + ("Select local keyboard type", "請選擇本地鍵盤類型"), ].iter().cloned().collect(); } diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 604d2e222..3e4cd681f 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -202,6 +202,16 @@ pub fn set_local_flutter_config(key: String, value: String) { LocalConfig::set_flutter_config(key, value); } +#[inline] +pub fn get_kb_layout_type() -> String { + LocalConfig::get_kb_layout_type() +} + +#[inline] +pub fn set_kb_layout_type(kb_layout_type: String) { + LocalConfig::set_kb_layout_type(kb_layout_type); +} + #[inline] pub fn peer_has_password(id: String) -> bool { !PeerConfig::load(&id).password.is_empty() From ebdead8766b5ace96922c3a39bb57034f0d07c41 Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 27 Dec 2022 16:46:56 +0800 Subject: [PATCH 1278/2015] add svg Signed-off-by: fufesou --- flutter/assets/kb_layout_iso.svg | 1 + flutter/assets/kb_layout_not_iso.svg | 1 + flutter/lib/desktop/widgets/kb_layout_type_chooser.dart | 4 ++-- 3 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 flutter/assets/kb_layout_iso.svg create mode 100644 flutter/assets/kb_layout_not_iso.svg diff --git a/flutter/assets/kb_layout_iso.svg b/flutter/assets/kb_layout_iso.svg new file mode 100644 index 000000000..69f0c96cb --- /dev/null +++ b/flutter/assets/kb_layout_iso.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/flutter/assets/kb_layout_not_iso.svg b/flutter/assets/kb_layout_not_iso.svg new file mode 100644 index 000000000..09a055be3 --- /dev/null +++ b/flutter/assets/kb_layout_not_iso.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart b/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart index 35ab4c81e..9269d1a60 100644 --- a/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart +++ b/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart @@ -18,8 +18,8 @@ const String _kKBLayoutTypeISO = 'ISO'; const String _kKBLayoutTypeNotISO = 'Not ISO'; const _kKBLayoutImageMap = { - _kKBLayoutTypeISO: 'KB_LAYOUT_ISO', - _kKBLayoutTypeNotISO: 'KB_LAYOUT_NOT_ISO', + _kKBLayoutTypeISO: 'kb_layout_iso', + _kKBLayoutTypeNotISO: 'kb_layout_not_iso', }; class _KBImage extends StatelessWidget { From 50c33450b94de574d9259f44fe69c7eded6a6ef3 Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 27 Dec 2022 17:49:32 +0800 Subject: [PATCH 1279/2015] fix keyboard type store Signed-off-by: fufesou --- .../widgets/kb_layout_type_chooser.dart | 75 +++++++++---------- libs/hbb_common/src/config.rs | 4 +- 2 files changed, 39 insertions(+), 40 deletions(-) diff --git a/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart b/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart index 9269d1a60..6601160a7 100644 --- a/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart +++ b/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart @@ -6,7 +6,7 @@ import 'package:flutter_hbb/models/platform_model.dart'; import '../../common.dart'; -typedef KBChoosedCallback = bool Function(String); +typedef KBChoosedCallback = Future Function(String); const double _kImageMarginVertical = 6.0; const double _kImageMarginHorizental = 10.0; @@ -78,11 +78,19 @@ class _KBChooser extends StatelessWidget { @override Widget build(BuildContext context) { + onChanged(String? v) async { + if (v != null) { + if (await cb(v)) { + choosedType.value = v; + } + } + } + return Column( children: [ TextButton( onPressed: () { - choosedType.value = kbLayoutType; + onChanged(kbLayoutType); }, child: _KBImage( kbLayoutType: kbLayoutType, @@ -98,21 +106,13 @@ class _KBChooser extends StatelessWidget { splashRadius: 0, value: kbLayoutType, groupValue: choosedType.value, - onChanged: (String? newValue) { - if (newValue != null) { - if (cb(newValue)) { - choosedType.value = newValue; - } - } - }, + onChanged: onChanged, )), Text(kbLayoutType), ], ), onPressed: () { - if (cb(kbLayoutType)) { - choosedType.value = kbLayoutType; - } + onChanged(kbLayoutType); }, ), ], @@ -138,31 +138,28 @@ class KBLayoutTypeChooser extends StatelessWidget { @override Widget build(BuildContext context) { final imageWidth = width / 2 - dividerWidth; - return Container( - color: Colors.white, - child: SizedBox( - width: width, - height: height, - child: Center( - child: Row( - children: [ - _KBChooser( - kbLayoutType: _kKBLayoutTypeISO, - imageWidth: imageWidth, - choosedType: choosedType, - cb: cb, - ), - VerticalDivider( - width: dividerWidth * 2, - ), - _KBChooser( - kbLayoutType: _kKBLayoutTypeNotISO, - imageWidth: imageWidth, - choosedType: choosedType, - cb: cb, - ), - ], - ), + return SizedBox( + width: width, + height: height, + child: Center( + child: Row( + children: [ + _KBChooser( + kbLayoutType: _kKBLayoutTypeISO, + imageWidth: imageWidth, + choosedType: choosedType, + cb: cb, + ), + VerticalDivider( + width: dividerWidth * 2, + ), + _KBChooser( + kbLayoutType: _kKBLayoutTypeNotISO, + imageWidth: imageWidth, + choosedType: choosedType, + cb: cb, + ), + ], ), ), ); @@ -215,8 +212,8 @@ showKBLayoutTypeChooser( width: 360, height: 200, dividerWidth: 4.0, - cb: (String v) { - bind.setLocalKbLayoutType(kbLayoutType: v); + cb: (String v) async { + await bind.setLocalKbLayoutType(kbLayoutType: v); KBLayoutType.value = bind.getLocalKbLayoutType(); return v == KBLayoutType.value; }), diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index d86ac3463..4bc33cda5 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -1024,7 +1024,9 @@ impl LocalConfig { } pub fn set_kb_layout_type(kb_layout_type: String) { - LOCAL_CONFIG.write().unwrap().kb_layout_type = kb_layout_type + let mut config = LOCAL_CONFIG.write().unwrap(); + config.kb_layout_type = kb_layout_type; + config.store(); } pub fn get_size() -> Size { From 971475c7784a82327d5204a162ea5672500b9d68 Mon Sep 17 00:00:00 2001 From: neoGalaxy88 Date: Tue, 27 Dec 2022 12:14:32 +0100 Subject: [PATCH 1280/2015] 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 3b112618d..9ec40c0aa 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -403,8 +403,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("wayland_experiment_tip", "Il supporto Wayland è in fase sperimentale, utilizza X11 se necessiti di un accesso stabile."), ("Right click to select tabs", "Clic con il tasto destro per selezionare le schede"), ("Add to Address Book", "Aggiungi alla rubrica"), - ("Group", ""), - ("Search", ""), - ("Closed manually by the web console", ""), + ("Group", "Gruppo"), + ("Search", "Cerca"), + ("Closed manually by the web console", "Chiudi manualmente dalla console Web"), ].iter().cloned().collect(); } From ff5228f7d4b21f4db1d0f87dd4d6fd0508f3e190 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 27 Dec 2022 22:55:54 +0800 Subject: [PATCH 1281/2015] fix encoding --- res/lang.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/res/lang.py b/res/lang.py index 974449f2a..e894cbb87 100644 --- a/res/lang.py +++ b/res/lang.py @@ -7,7 +7,7 @@ import csv def get_lang(lang): out = {} - for ln in open('./src/lang/%s.rs'%lang): + for ln in open('./src/lang/%s.rs'%lang, encoding='utf8'): ln = ln.strip() if ln.startswith('("'): k, v = line_split(ln) @@ -39,8 +39,8 @@ def expand(): if lang in ['en','cn']: continue print(lang) dict = get_lang(lang) - fw = open("./src/lang/%s.rs"%lang, "wt") - for line in open('./src/lang/cn.rs'): + fw = open("./src/lang/%s.rs"%lang, "wt", encoding='utf8') + for line in open('./src/lang/cn.rs', encoding='utf8'): line_strip = line.strip() if line_strip.startswith('("'): k, v = line_split(line_strip) @@ -57,9 +57,9 @@ def expand(): def to_csv(): for fn in glob.glob('./src/lang/*.rs'): lang = os.path.basename(fn)[:-3] - csvfile = open('./src/lang/%s.csv'%lang, "wt") + csvfile = open('./src/lang/%s.csv'%lang, "wt", encoding='utf8') csvwriter = csv.writer(csvfile) - for line in open(fn): + for line in open(fn, encoding='utf8'): line_strip = line.strip() if line_strip.startswith('("'): k, v = line_split(line_strip) @@ -68,8 +68,8 @@ def to_csv(): def to_rs(lang): - csvfile = open('%s.csv'%lang, "rt") - fw = open("./src/lang/%s.rs"%lang, "wt") + csvfile = open('%s.csv'%lang, "rt", encoding='utf8') + fw = open("./src/lang/%s.rs"%lang, "wt", encoding='utf8') fw.write('''lazy_static::lazy_static! { pub static ref T: std::collections::HashMap<&'static str, &'static str> = [ From 75f57cf0fc4fd9d2b770ab088aad78015da491d9 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 27 Dec 2022 23:26:11 +0800 Subject: [PATCH 1282/2015] fix trans --- .../desktop/pages/desktop_setting_page.dart | 6 +++--- res/lang.py | 3 ++- src/lang/ca.rs | 4 +++- src/lang/cn.rs | 1 - src/lang/cs.rs | 4 +++- src/lang/da.rs | 4 +++- src/lang/de.rs | 6 ++++-- src/lang/eo.rs | 4 +++- src/lang/es.rs | 4 +++- src/lang/fa.rs | 4 +++- src/lang/fr.rs | 4 +++- src/lang/gr.rs | 4 +++- src/lang/hu.rs | 4 +++- src/lang/id.rs | 4 +++- src/lang/it.rs | 4 +++- src/lang/ja.rs | 4 +++- src/lang/ko.rs | 4 +++- src/lang/kz.rs | 4 +++- src/lang/pl.rs | 4 +++- src/lang/pt_PT.rs | 4 +++- src/lang/ptbr.rs | 4 +++- src/lang/ru.rs | 4 +++- src/lang/sk.rs | 4 +++- src/lang/sq.rs | 4 +++- src/lang/sr.rs | 19 ++++++++----------- src/lang/sv.rs | 4 +++- src/lang/template.rs | 4 +++- src/lang/tr.rs | 4 +++- src/lang/tw.rs | 2 +- src/lang/ua.rs | 4 +++- src/lang/vn.rs | 4 +++- src/ui/index.tis | 2 +- 32 files changed, 94 insertions(+), 45 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 40cd794ab..15f78daeb 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -1097,16 +1097,16 @@ class _AboutState extends State<_About> { child: SingleChildScrollView( controller: scrollController, physics: NeverScrollableScrollPhysics(), - child: _Card(title: 'About RustDesk', children: [ + child: _Card(title: '${translate('About')} RustDesk', children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox( height: 8.0, ), - Text(translate('Version') + ': $version') + Text('${translate('Version')}: $version') .marginSymmetric(vertical: 4.0), - Text(translate('Build Date') + ': $buildDate') + Text('${translate('Build Date')}: $buildDate') .marginSymmetric(vertical: 4.0), InkWell( onTap: () { diff --git a/res/lang.py b/res/lang.py index e894cbb87..37bbfb3b1 100644 --- a/res/lang.py +++ b/res/lang.py @@ -45,7 +45,8 @@ def expand(): if line_strip.startswith('("'): k, v = line_split(line_strip) if k in dict: - line = line.replace(v, dict[k]) + # embrased with " to avoid empty v + line = line.replace('"%s"'%v, '"%s"'%dict[k]) else: line = line.replace(v, "") fw.write(line) diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 2f9bf7b25..e5c43ccbc 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -39,7 +39,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Canviar ID"), ("Website", "Lloc web"), ("About", "Sobre"), - ("About RustDesk", ""), ("Slogan_tip", ""), ("Privacy Statement", ""), ("Mute", "Silenciar"), @@ -402,9 +401,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", ""), ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), + ("Skipped", ""), ("Add to Address Book", ""), ("Group", ""), ("Search", ""), ("Closed manually by the web console", ""), + ("Local keyboard type", ""), + ("Select local keyboard type", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 5d04268b0..7d8f51ddc 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -39,7 +39,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "改变ID"), ("Website", "网站"), ("About", "关于"), - ("About RustDesk", ""), ("Slogan_tip", ""), ("Privacy Statement", ""), ("Mute", "静音"), diff --git a/src/lang/cs.rs b/src/lang/cs.rs index eb57edc0a..609ebb81e 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -39,7 +39,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Změnit identifikátor"), ("Website", "Webové stránky"), ("About", "O aplikaci"), - ("About RustDesk", ""), ("Slogan_tip", ""), ("Privacy Statement", ""), ("Mute", "Ztlumit"), @@ -402,9 +401,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", ""), ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), + ("Skipped", ""), ("Add to Address Book", ""), ("Group", ""), ("Search", ""), ("Closed manually by the web console", ""), + ("Local keyboard type", ""), + ("Select local keyboard type", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index c34a31aef..82a0fec94 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -39,7 +39,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Ændre ID"), ("Website", "Hjemmeside"), ("About", "Omkring"), - ("About RustDesk", ""), ("Slogan_tip", ""), ("Privacy Statement", ""), ("Mute", "Sluk for mikrofonen"), @@ -402,9 +401,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", ""), ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), + ("Skipped", ""), ("Add to Address Book", ""), ("Group", ""), ("Search", ""), ("Closed manually by the web console", ""), + ("Local keyboard type", ""), + ("Select local keyboard type", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index b551774bf..3150bcaa3 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -39,7 +39,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "ID ändern"), ("Website", "Webseite"), ("About", "Über"), - ("About RustDesk", "Über RustDesk"), ("Slogan_tip", "Mit Herzblut programmiert - in einer Welt, die im Chaos versinkt"), ("Privacy Statement", "Datenschutz"), ("Mute", "Stummschalten"), @@ -370,7 +369,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable LAN Discovery", "LAN-Erkennung aktivieren"), ("Deny LAN Discovery", "LAN-Erkennung verbieten"), ("Write a message", "Nachricht schreiben"), - ("Prompt", ""), //Aufforderung??? + ("Prompt", ""), //Aufforderung"), ("Please wait for confirmation of UAC...", "Bitte auf die Bestätigung des Nutzers warten..."), ("elevated_foreground_window_tip", "Das aktuell geöffnete Fenster des ferngesteuerten Computers benötigt höhere Rechte. Deshalb ist es derzeit nicht möglich, die Maus und die Tastatur zu verwenden. Bitten Sie den Nutzer, dessen Computer Sie fernsteuern, das Fenster zu minimieren oder die Rechte zu erhöhen. Um dieses Problem zunünftig zu vermeiden, wird empfohlen, die Software auf dem ferngesteuerten Computer zu installieren."), ("Disconnected", "Verbindung abgebrochen"), @@ -402,9 +401,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", "Dies ist nur möglich, wenn der Zugriff nur über ein permanentes Passwort erfolgt."), ("wayland_experiment_tip", "Die Unterstützung von Wayland ist nur experimentell. Bitte nutzen Sie X11, wenn Sie einen unbeaufsichtigten Zugriff benötigen."), ("Right click to select tabs", "Register mit rechtem Mausklick auswählen"), + ("Skipped", ""), ("Add to Address Book", "Zum Adressbuch hinzufügen"), ("Group", "Gruppe"), ("Search", "Suchen"), ("Closed manually by the web console", ""), + ("Local keyboard type", ""), + ("Select local keyboard type", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 3f22f489e..0de56d0ad 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -39,7 +39,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Ŝanĝi identigilon"), ("Website", "Retejo"), ("About", "Pri"), - ("About RustDesk", ""), ("Slogan_tip", ""), ("Privacy Statement", ""), ("Mute", "Muta"), @@ -402,9 +401,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", ""), ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), + ("Skipped", ""), ("Add to Address Book", ""), ("Group", ""), ("Search", ""), ("Closed manually by the web console", ""), + ("Local keyboard type", ""), + ("Select local keyboard type", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 0a2ccfd35..fdac9dd48 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -39,7 +39,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Cambiar ID"), ("Website", "Sitio web"), ("About", "Acerca de"), - ("About RustDesk", ""), ("Slogan_tip", ""), ("Privacy Statement", ""), ("Mute", "Silenciar"), @@ -402,9 +401,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", "Permitir ocultar solo si se aceptan sesiones a través de contraseña y usando contraseña permanente"), ("wayland_experiment_tip", "El soporte para Wayland está en fase experimental, por favor, use X11 si necesita acceso desatendido."), ("Right click to select tabs", "Clic derecho para seleccionar pestañas"), + ("Skipped", ""), ("Add to Address Book", "Añadir a la libreta de direcciones"), ("Group", "Grupo"), ("Search", "Búsqueda"), ("Closed manually by the web console", "Cerrado manualmente por la consola web"), + ("Local keyboard type", ""), + ("Select local keyboard type", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 20a1663f2..d964db945 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -39,7 +39,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "تعویض شناسه"), ("Website", "وب سایت"), ("About", "درباره"), - ("About RustDesk", ""), ("Slogan_tip", ""), ("Privacy Statement", ""), ("Mute", "بستن صدا"), @@ -402,9 +401,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", "فقط در صورت پذیرفتن جلسات از طریق رمز عبور و استفاده از رمز عبور دائمی، مخفی شدن مجاز است"), ("wayland_experiment_tip", "پشتیبانی Wayland در مرحله آزمایشی است، لطفاً در صورت نیاز به دسترسی بدون مراقبت از X11 استفاده کنید."), ("Right click to select tabs", "برای انتخاب تب ها راست کلیک کنید"), + ("Skipped", ""), ("Add to Address Book", "افزودن به دفترچه آدرس"), ("Group", ""), ("Search", ""), ("Closed manually by the web console", ""), + ("Local keyboard type", ""), + ("Select local keyboard type", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 3b81fa1e4..2f13a9807 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -39,7 +39,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Changer d'ID"), ("Website", "Site Web"), ("About", "À propos de"), - ("About RustDesk", "À propos de RustDesk"), ("Slogan_tip", "Fait avec cœur dans ce monde chaotique!"), ("Privacy Statement", "Déclaration de confidentialité"), ("Mute", "Muet"), @@ -402,9 +401,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", "Autoriser le masquage uniquement si vous acceptez des sessions via un mot de passe et utilisez un mot de passe permanent"), ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), + ("Skipped", ""), ("Add to Address Book", "Ajouter au carnet d'adresses"), ("Group", "Groupe"), ("Search", "Rechercher"), ("Closed manually by the web console", ""), + ("Local keyboard type", ""), + ("Select local keyboard type", ""), ].iter().cloned().collect(); } diff --git a/src/lang/gr.rs b/src/lang/gr.rs index c708ce478..811dca2c6 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -39,7 +39,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Αλλαγή αναγνωριστικού ID"), ("Website", "Ιστότοπος"), ("About", "Πληροφορίες"), - ("About RustDesk", ""), ("Slogan_tip", ""), ("Privacy Statement", ""), ("Mute", "Σίγαση"), @@ -402,9 +401,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", "Να επιτρέπεται η απόκρυψη, μόνο εάν αποδέχεστε συνδέσεις μέσω κωδικού πρόσβασης και χρησιμοποιείτε μόνιμο κωδικό πρόσβασης"), ("wayland_experiment_tip", "Η υποστήριξη Wayland βρίσκεται σε πειραματικό στάδιο, χρησιμοποιήστε το X11 εάν χρειάζεστε πρόσβαση χωρίς επίβλεψη."), ("Right click to select tabs", "Κάντε δεξί κλικ για να επιλέξετε καρτέλες"), + ("Skipped", ""), ("Add to Address Book", "Προσθήκη στο Βιβλίο Διευθύνσεων"), ("Group", "Ομάδα"), ("Search", "Αναζήτηση"), ("Closed manually by the web console", ""), + ("Local keyboard type", ""), + ("Select local keyboard type", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 849bfa0af..3aaf0c87d 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -39,7 +39,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Azonosító megváltoztatása"), ("Website", "Weboldal"), ("About", "Rólunk"), - ("About RustDesk", ""), ("Slogan_tip", ""), ("Privacy Statement", ""), ("Mute", "Némítás"), @@ -402,9 +401,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", ""), ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), + ("Skipped", ""), ("Add to Address Book", ""), ("Group", ""), ("Search", ""), ("Closed manually by the web console", ""), + ("Local keyboard type", ""), + ("Select local keyboard type", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 6862832b3..ccb4bbd8a 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -39,7 +39,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Ubah ID"), ("Website", "Website"), ("About", "Tentang"), - ("About RustDesk", "Tentang RustDesk"), ("Slogan_tip", ""), ("Privacy Statement", "Pernyataan Privasi"), ("Mute", "Bisukan"), @@ -402,9 +401,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", ""), ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), + ("Skipped", ""), ("Add to Address Book", ""), ("Group", ""), ("Search", "Pencarian"), ("Closed manually by the web console", ""), + ("Local keyboard type", ""), + ("Select local keyboard type", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 9ec40c0aa..38015f56c 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -39,7 +39,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Cambia ID"), ("Website", "Sito web"), ("About", "Informazioni"), - ("About RustDesk", ""), ("Slogan_tip", ""), ("Privacy Statement", ""), ("Mute", "Silenzia"), @@ -402,9 +401,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", "Permetti di nascondere solo se si accettano sessioni con password permanente"), ("wayland_experiment_tip", "Il supporto Wayland è in fase sperimentale, utilizza X11 se necessiti di un accesso stabile."), ("Right click to select tabs", "Clic con il tasto destro per selezionare le schede"), + ("Skipped", ""), ("Add to Address Book", "Aggiungi alla rubrica"), ("Group", "Gruppo"), ("Search", "Cerca"), ("Closed manually by the web console", "Chiudi manualmente dalla console Web"), + ("Local keyboard type", ""), + ("Select local keyboard type", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 3d76f446b..5f6dcc79e 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -39,7 +39,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "IDを変更"), ("Website", "公式サイト"), ("About", "情報"), - ("About RustDesk", ""), ("Slogan_tip", ""), ("Privacy Statement", ""), ("Mute", "ミュート"), @@ -402,9 +401,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", ""), ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), + ("Skipped", ""), ("Add to Address Book", ""), ("Group", ""), ("Search", ""), ("Closed manually by the web console", ""), + ("Local keyboard type", ""), + ("Select local keyboard type", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 92bb85bce..0f7df38b9 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -39,7 +39,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "ID 변경"), ("Website", "웹사이트"), ("About", "정보"), - ("About RustDesk", ""), ("Slogan_tip", ""), ("Privacy Statement", ""), ("Mute", "음소거"), @@ -402,9 +401,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", ""), ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), + ("Skipped", ""), ("Add to Address Book", ""), ("Group", ""), ("Search", ""), ("Closed manually by the web console", ""), + ("Local keyboard type", ""), + ("Select local keyboard type", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 90a2730f6..f352be343 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -39,7 +39,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "ID ауыстыру"), ("Website", "Web-сайт"), ("About", "Туралы"), - ("About RustDesk", ""), ("Slogan_tip", ""), ("Privacy Statement", ""), ("Mute", "Дыбыссыздандыру"), @@ -402,9 +401,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", ""), ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), + ("Skipped", ""), ("Add to Address Book", ""), ("Group", ""), ("Search", ""), ("Closed manually by the web console", ""), + ("Local keyboard type", ""), + ("Select local keyboard type", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index e5604c442..9593ffe54 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -39,7 +39,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Zmień ID"), ("Website", "Strona internetowa"), ("About", "O"), - ("About RustDesk", ""), ("Slogan_tip", ""), ("Privacy Statement", ""), ("Mute", "Wycisz"), @@ -402,9 +401,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", ""), ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), + ("Skipped", ""), ("Add to Address Book", "Dodaj do Książki Adresowej"), ("Group", "Grypy"), ("Search", "Szukaj"), ("Closed manually by the web console", ""), + ("Local keyboard type", ""), + ("Select local keyboard type", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 2adb4eb9e..dcadfb0e7 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -39,7 +39,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Alterar ID"), ("Website", "Website"), ("About", "Sobre"), - ("About RustDesk", ""), ("Slogan_tip", ""), ("Privacy Statement", ""), ("Mute", "Silenciar"), @@ -402,9 +401,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", ""), ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), + ("Skipped", ""), ("Add to Address Book", ""), ("Group", ""), ("Search", ""), ("Closed manually by the web console", ""), + ("Local keyboard type", ""), + ("Select local keyboard type", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 6256d2e7a..48e964f7d 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -39,7 +39,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Alterar ID"), ("Website", "Website"), ("About", "Sobre"), - ("About RustDesk", ""), ("Slogan_tip", ""), ("Privacy Statement", ""), ("Mute", "Desativar som"), @@ -402,9 +401,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", ""), ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), + ("Skipped", ""), ("Add to Address Book", ""), ("Group", ""), ("Search", ""), ("Closed manually by the web console", ""), + ("Local keyboard type", ""), + ("Select local keyboard type", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 6e08322f3..78cb14980 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -39,7 +39,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Изменить ID"), ("Website", "Сайт"), ("About", "О программе"), - ("About RustDesk", "О RustDesk"), ("Slogan_tip", "Сделано с душой в этом безумном мире!"), ("Privacy Statement", "Заявление о конфиденциальности"), ("Mute", "Отключить звук"), @@ -402,9 +401,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", "Разрешать скрытие случае, если принимаются сеансы по паролю или используется постоянный пароль"), ("wayland_experiment_tip", "Поддержка Wayland находится на экспериментальной стадии, используйте X11, если вам требуется автоматический доступ."), ("Right click to select tabs", "Выбор вкладок щелчком правой кнопки мыши"), + ("Skipped", ""), ("Add to Address Book", "Добавить в адресную книгу"), ("Group", "Группа"), ("Search", "Поиск"), ("Closed manually by the web console", ""), + ("Local keyboard type", ""), + ("Select local keyboard type", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index a65e0519b..72995dd94 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -39,7 +39,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Zmeniť ID"), ("Website", "Webová stránka"), ("About", "O RustDesk"), - ("About RustDesk", ""), ("Slogan_tip", ""), ("Privacy Statement", ""), ("Mute", "Stíšiť"), @@ -402,9 +401,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", ""), ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), + ("Skipped", ""), ("Add to Address Book", ""), ("Group", ""), ("Search", ""), ("Closed manually by the web console", ""), + ("Local keyboard type", ""), + ("Select local keyboard type", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 27f37260d..7a2472aa8 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -39,7 +39,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Ndryshoni ID"), ("Website", "Faqe ëebi"), ("About", "Rreth"), - ("About RustDesk", ""), ("Slogan_tip", ""), ("Privacy Statement", ""), ("Mute", "Pa zë"), @@ -402,9 +401,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", "Kjo është e mundur vetëm nëse aksesi bëhet nëpërmjet një fjalëkalimi të përhershëm"), ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), + ("Skipped", ""), ("Add to Address Book", ""), ("Group", ""), ("Search", ""), ("Closed manually by the web console", ""), + ("Local keyboard type", ""), + ("Select local keyboard type", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 5d04f0150..0d15b199c 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -35,11 +35,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Export server configuration successfully", "Eksport server konfiguracije uspešan"), ("Invalid server configuration", "Pogrešna konfiguracija servera"), ("Clipboard is empty", "Clipboard je prazan"), - ("Stop service", "Zaustavi servis"), + ("Stop service", "Stopiraj servis"), ("Change ID", "Promeni ID"), ("Website", "Web sajt"), ("About", "O programu"), - ("About RustDesk", ""), ("Slogan_tip", ""), ("Privacy Statement", ""), ("Mute", "Utišaj"), @@ -50,11 +49,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ID Server", "ID server"), ("Relay Server", "Posredni server"), ("API Server", "API server"), - ("invalid_http", "Nevažeći http"), + ("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 nije podržan"), + ("server_not_support", "Server još uvek ne podržava"), ("Not available", "Nije dostupno"), ("Too frequent", "Previše često"), ("Cancel", "Otkaži"), @@ -223,7 +222,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Username missed", "Korisničko ime promašeno"), ("Password missed", "Lozinka promašena"), ("Wrong credentials", "Pogrešno korisničko ime ili lozinka"), - ("invalid_http", "mora početi sa http:// ili https://"), ("Edit Tag", "Izmeni oznaku"), ("Unremember Password", "Zaboravi lozinku"), ("Favorites", "Favoriti"), @@ -238,7 +236,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Remote ID", "Udaljeni ID"), ("Paste", "Nalepi"), ("Paste here?", "Nalepi ovde?"), - ("Are you sure to close the connection?", "Da li ste sigurni da zatvarate konekciju?"), + ("Are you sure to close the connection?", "Da li ste sigurni da želite da zatvorite konekciju?"), ("Download new version", "Preuzmi novu verziju"), ("Touch mode", "Mod na dodir"), ("Mouse mode", "Miš mod"), @@ -287,8 +285,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("This file exists, skip or overwrite this file?", "Ova datoteka postoji, preskoči ili prepiši preko?"), ("Quit", "Izlaz"), ("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"), - ("doc_fix_wayland", "https://rustdesk.com/docs/en/manual/linux/#x11-required"), - ("server_not_support", "Server još uvek ne podržava"), ("Help", "Pomoć"), ("Failed", "Greška"), ("Succeeded", "Uspešno"), @@ -318,7 +314,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Are you sure you want to restart", "Da li ste sigurni da želite restart"), ("Restarting Remote Device", "Restartovanje daljinskog uređaja"), ("remote_restarting_tip", "Udaljeni uređaj se restartuje, molimo zatvorite ovu poruku i ponovo se kasnije povežite trajnom šifrom"), - ("Are you sure to close the connection?", "Da li ste sigurni da želite da zatvorite konekciju?"), ("Copied", "Kopirano"), ("Exit Fullscreen", "Napusti mod celog ekrana"), ("Fullscreen", "Mod celog ekrana"), @@ -387,7 +382,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland zahteva Ubuntu 21.04 ili veću verziju"), ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland zahteva veću verziju Linux distribucije. Molimo pokušajte X11 ili promenite OS."), ("JumpLink", "Vidi"), - ("Stop service", "Stopiraj servis"), ("Please Select the screen to be shared(Operate on the peer side).", "Molimo izaberite ekran koji će biti podeljen (Za rad na klijent strani)"), ("Show RustDesk", "Prikazi RustDesk"), ("This PC", "Ovaj PC"), @@ -407,9 +401,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", "Skrivanje dozvoljeno samo prihvatanjem sesije preko lozinke i korišćenjem trajne lozinke"), ("wayland_experiment_tip", "Wayland eksperiment savet"), ("Right click to select tabs", "Desni klik za izbor kartica"), + ("Skipped", ""), ("Add to Address Book", "Dodaj u adresar"), ("Group", "Grupa"), ("Search", "Pretraga"), ("Closed manually by the web console", ""), - ].iter().cloned().collect(); + ("Local keyboard type", ""), + ("Select local keyboard type", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 72cc83fce..9fa3c75fb 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -39,7 +39,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Byt ID"), ("Website", "Hemsida"), ("About", "Om"), - ("About RustDesk", ""), ("Slogan_tip", ""), ("Privacy Statement", ""), ("Mute", "Tyst"), @@ -402,9 +401,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", "Tillåt att gömma endast om accepterande sessioner med lösenord och permanenta lösenord"), ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), + ("Skipped", ""), ("Add to Address Book", ""), ("Group", ""), ("Search", ""), ("Closed manually by the web console", ""), + ("Local keyboard type", ""), + ("Select local keyboard type", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index a92df2b52..1bd9f5e98 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -39,7 +39,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", ""), ("Website", ""), ("About", ""), - ("About RustDesk", ""), ("Slogan_tip", ""), ("Privacy Statement", ""), ("Mute", ""), @@ -402,9 +401,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", ""), ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), + ("Skipped", ""), ("Add to Address Book", ""), ("Group", ""), ("Search", ""), ("Closed manually by the web console", ""), + ("Local keyboard type", ""), + ("Select local keyboard type", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index cb7203c2d..f74d0b435 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -39,7 +39,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "ID Değiştir"), ("Website", "Website"), ("About", "Hakkında"), - ("About RustDesk", ""), ("Slogan_tip", ""), ("Privacy Statement", ""), ("Mute", "Sustur"), @@ -402,9 +401,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", ""), ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), + ("Skipped", ""), ("Add to Address Book", ""), ("Group", ""), ("Search", ""), ("Closed manually by the web console", ""), + ("Local keyboard type", ""), + ("Select local keyboard type", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 301384ea3..a3eb9691d 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -39,7 +39,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "更改 ID"), ("Website", "網站"), ("About", "關於"), - ("About RustDesk", ""), ("Slogan_tip", ""), ("Privacy Statement", ""), ("Mute", "靜音"), @@ -402,6 +401,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", "在只允許密碼連接並且只用固定密碼的情況下才允許隱藏"), ("wayland_experiment_tip", ""), ("Right click to select tabs", "右鍵選擇選項卡"), + ("Skipped", ""), ("Add to Address Book", "添加到地址簿"), ("Group", "小組"), ("Search", "搜索"), diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 373c3267e..09ed272d5 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -39,7 +39,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Змінити ID"), ("Website", "Веб-сайт"), ("About", "Про RustDesk"), - ("About RustDesk", "Про RustDesk"), ("Slogan_tip", "Створено з душею в цьому хаотичному світі!"), ("Privacy Statement", "Декларація про конфіденційність"), ("Mute", "Вимкнути звук"), @@ -402,9 +401,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", "Дозволено приховати лише якщо сеанс підтверджується постійним паролем"), ("wayland_experiment_tip", "Підтримка Wayland на експериментальній стадії, будь ласка, використовуйте X11, якщо необхідний автоматичний доступ."), ("Right click to select tabs", "Правий клік для вибору вкладки"), + ("Skipped", ""), ("Add to Address Book", "Додати IP до Адресної книги"), ("Group", "Група"), ("Search", "Пошук"), ("Closed manually by the web console", ""), + ("Local keyboard type", ""), + ("Select local keyboard type", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 7b7808bea..c95d4fe63 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -39,7 +39,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Thay đổi ID"), ("Website", "Trang web"), ("About", "About"), - ("About RustDesk", ""), ("Slogan_tip", ""), ("Privacy Statement", ""), ("Mute", "Tắt tiếng"), @@ -402,9 +401,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", ""), ("wayland_experiment_tip", ""), ("Right click to select tabs", ""), + ("Skipped", ""), ("Add to Address Book", ""), ("Group", ""), ("Search", ""), ("Closed manually by the web console", ""), + ("Local keyboard type", ""), + ("Select local keyboard type", ""), ].iter().cloned().collect(); } diff --git a/src/ui/index.tis b/src/ui/index.tis index 9dcd4f4c4..c141d0efe 100644 --- a/src/ui/index.tis +++ b/src/ui/index.tis @@ -361,7 +361,7 @@ class MyIdMenu: Reactor.Component { function showAbout() { var name = handler.get_app_name(); - msgbox("custom-nocancel-nook-hasclose", "About " + name, "
    \ + msgbox("custom-nocancel-nook-hasclose", translate("About") + " " + name, "
    \
    Version: " + handler.get_version() + " \
    Privacy Statement
    \
    Website
    \ From 43b3a04f33704cf931c5aca5f074cf4dd2caf37e Mon Sep 17 00:00:00 2001 From: Nikos Fazakis Date: Tue, 27 Dec 2022 22:26:37 +0200 Subject: [PATCH 1283/2015] spawn update --- src/server.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server.rs b/src/server.rs index 707557840..7f9b055dc 100644 --- a/src/server.rs +++ b/src/server.rs @@ -106,7 +106,7 @@ async fn accept_connection_(server: ServerPtr, socket: Stream, secure: bool) -> let stream_addr = stream.local_addr()?; if cfg!(target_os = "macos") { use std::process::Command; - Command::new("/usr/bin/caffeinate").arg("-u").arg("-t 2").output().expect("failed to execute caffeinate"); + Command::new("/usr/bin/caffeinate").arg("-u").arg("-t 5").spawn().expect("failed to execute caffeinate"); println!("wake up macos..."); } create_tcp_connection(server, Stream::from(stream, stream_addr), addr, secure).await?; From 08b8f403975a16f6eeeff2afeb6f11f32e29384d Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 28 Dec 2022 13:52:13 +0800 Subject: [PATCH 1284/2015] nat64 --- libs/hbb_common/src/config.rs | 13 ++- libs/hbb_common/src/lib.rs | 9 ++- libs/hbb_common/src/socket_client.rs | 87 ++++++++++++++++++-- src/client.rs | 41 +++++----- src/common.rs | 20 ++--- src/rendezvous_mediator.rs | 66 ++++++++------- src/server.rs | 7 +- src/server/connection.rs | 2 +- src/ui.rs | 115 ++------------------------- src/ui_interface.rs | 15 +--- 10 files changed, 167 insertions(+), 208 deletions(-) diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 4bc33cda5..1d427a2e9 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -511,13 +511,12 @@ impl Config { } #[inline] - pub fn get_any_listen_addr() -> SocketAddr { - SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0) - } - - #[inline] - pub fn get_any_listen_addr_v6() -> SocketAddr { - SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0) + pub fn get_any_listen_addr(is_ipv4: bool) -> SocketAddr { + if is_ipv4 { + SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0) + } else { + SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0) + } } pub fn get_rendezvous_server() -> String { diff --git a/libs/hbb_common/src/lib.rs b/libs/hbb_common/src/lib.rs index 5a6d24b91..d1893564c 100644 --- a/libs/hbb_common/src/lib.rs +++ b/libs/hbb_common/src/lib.rs @@ -130,7 +130,7 @@ impl AddrMangle { pub fn decode(bytes: &[u8]) -> SocketAddr { if bytes.len() > 16 { if bytes.len() != 18 { - return Config::get_any_listen_addr_v6(); + return Config::get_any_listen_addr(false); } #[allow(invalid_value)] let mut tmp: [u8; 2] = unsafe { std::mem::MaybeUninit::uninit().assume_init() }; @@ -300,3 +300,10 @@ mod tests { ); } } + +#[inline] +pub fn is_ipv4_str(id: &str) -> bool { + regex::Regex::new(r"^\d+\.\d+\.\d+\.\d+(:\d+)?$") + .unwrap() + .is_match(id) +} \ No newline at end of file diff --git a/libs/hbb_common/src/socket_client.rs b/libs/hbb_common/src/socket_client.rs index e09d38c3a..615579d65 100644 --- a/libs/hbb_common/src/socket_client.rs +++ b/libs/hbb_common/src/socket_client.rs @@ -85,10 +85,21 @@ impl<'a> IntoTargetAddr2<'a> for &str { pub async fn connect_tcp<'t, T: IntoTargetAddr2<'t> + std::fmt::Debug>( target: T, - local: SocketAddr, ms_timeout: u64, ) -> ResultType { let target_addr = target.into_target_addr2()?; + let local = Config::get_any_listen_addr(is_ipv4(&target_addr)); + connect_tcp_local(target_addr, local, ms_timeout) + .await + .context(format!("Invalid target addr: {:?}", target)) +} + +pub async fn connect_tcp_local<'t, T: IntoTargetAddr<'t> + std::fmt::Debug>( + target: T, + local: SocketAddr, + ms_timeout: u64, +) -> ResultType { + let target_addr = target.into_target_addr()?; if let Some(conf) = Config::get_socks() { return FramedStream::connect( conf.proxy.as_str(), @@ -100,13 +111,43 @@ pub async fn connect_tcp<'t, T: IntoTargetAddr2<'t> + std::fmt::Debug>( ) .await; } - let addr = ToSocketAddrs::to_socket_addrs(&target_addr)? + let mut addr = ToSocketAddrs::to_socket_addrs(&target_addr)? .next() - .context(format!("Invalid target addr: {:?}", target))?; + .context(format!("Invalid target addr: {:?}", target_addr))?; + if local.is_ipv6() && addr.is_ipv4() { + addr = query_nip_io(&addr)?; + } Ok(FramedStream::new(addr, local, ms_timeout).await?) } -pub async fn new_udp(local: T, ms_timeout: u64) -> ResultType { +#[inline] +pub fn is_ipv4(target: &TargetAddr<'_>) -> bool { + match target { + TargetAddr::Ip(addr) => addr.is_ipv4(), + _ => true, + } +} + +#[inline] +pub fn query_nip_io(addr: &SocketAddr) -> ResultType { + to_socket_addr(format!("{}.nip.io:{}", addr.ip(), addr.port())) +} + +#[inline] +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)); + } + } + addr +} + +pub async fn new_udp_for(target: &TargetAddr<'_>, ms_timeout: u64) -> ResultType { + new_udp(Config::get_any_listen_addr(is_ipv4(target)), ms_timeout).await +} + +async fn new_udp(local: T, ms_timeout: u64) -> ResultType { match Config::get_socks() { None => Ok(FramedSocket::new(to_socket_addr(&local)?).await?), Some(conf) => { @@ -123,19 +164,49 @@ pub async fn new_udp(local: T, ms_timeout: u64) -> ResultType< } } -pub async fn rebind_udp(local: T) -> ResultType> { +pub async fn rebind_udp_for(target: &TargetAddr<'_>) -> ResultType> { match Config::get_network_type() { - NetworkType::Direct => Ok(Some(FramedSocket::new(to_socket_addr(local)?).await?)), + NetworkType::Direct => Ok(Some( + FramedSocket::new(Config::get_any_listen_addr(is_ipv4(target))).await?, + )), _ => Ok(None), } } + #[cfg(test)] mod tests { use super::*; #[test] fn test_to_socket_addr() { - assert_eq!(to_socket_addr("127.0.0.1:8080").unwrap(), "127.0.0.1:8080".parse().unwrap()); + assert_eq!( + to_socket_addr("127.0.0.1:8080").unwrap(), + "127.0.0.1:8080".parse().unwrap() + ); assert!(to_socket_addr("[ff::]:0").unwrap().is_ipv6()); assert!(to_socket_addr("xx").is_err()); } -} \ No newline at end of file + + #[test] + fn test_nat64() { + assert_eq!(ipv4_to_ipv6("1.1.1.1".to_owned(), true), "1.1.1.1"); + assert_eq!(ipv4_to_ipv6("1.1.1.1".to_owned(), false), "1.1.1.1.nip.io"); + assert_eq!( + ipv4_to_ipv6("1.1.1.1:8080".to_owned(), false), + "1.1.1.1.nip.io:8080" + ); + assert_eq!( + ipv4_to_ipv6("rustdesk.com".to_owned(), false), + "rustdesk.com" + ); + if to_socket_addr("rustdesk.com:80").unwrap().is_ipv6() { + assert!(query_nip_io(&"1.1.1.1:80".parse().unwrap()) + .unwrap() + .is_ipv6()); + return; + } + assert_eq!( + query_nip_io(&"1.1.1.1:80".parse().unwrap()).unwrap(), + "1.1.1.1:80".parse().unwrap() + ); + } +} diff --git a/src/client.rs b/src/client.rs index bf11c4b6d..c52b1adf3 100644 --- a/src/client.rs +++ b/src/client.rs @@ -167,12 +167,10 @@ impl Client { interface: impl Interface, ) -> ResultType<(Stream, bool)> { // to-do: remember the port for each peer, so that we can retry easier - let any_addr = Config::get_any_listen_addr(); - if crate::is_ip(peer) { + if hbb_common::is_ipv4_str(peer) { return Ok(( socket_client::connect_tcp( crate::check_port(peer, RELAY_PORT + 1), - any_addr, RENDEZVOUS_TIMEOUT, ) .await?, @@ -180,13 +178,12 @@ impl Client { )); } let (mut rendezvous_server, servers, contained) = crate::get_rendezvous_server(1_000).await; - let mut socket = - socket_client::connect_tcp(&*rendezvous_server, any_addr, RENDEZVOUS_TIMEOUT).await; + let mut socket = socket_client::connect_tcp(&*rendezvous_server, RENDEZVOUS_TIMEOUT).await; debug_assert!(!servers.contains(&rendezvous_server)); if socket.is_err() && !servers.is_empty() { log::info!("try the other servers: {:?}", servers); for server in servers { - socket = socket_client::connect_tcp(&*server, any_addr, RENDEZVOUS_TIMEOUT).await; + socket = socket_client::connect_tcp(&*server, RENDEZVOUS_TIMEOUT).await; if socket.is_ok() { rendezvous_server = server; break; @@ -203,7 +200,7 @@ impl Client { let mut relay_server = "".to_owned(); let start = std::time::Instant::now(); - let mut peer_addr = any_addr; + let mut peer_addr = Config::get_any_listen_addr(true); let mut peer_nat_type = NatType::UNKNOWN_NAT; let my_nat_type = crate::get_nat_type(100).await; let mut is_local = false; @@ -264,9 +261,15 @@ impl Client { rr.relay_server ); signed_id_pk = rr.pk().into(); - let mut conn = - Self::create_relay(peer, rr.uuid, rr.relay_server, key, conn_type) - .await?; + let mut conn = Self::create_relay( + peer, + rr.uuid, + rr.relay_server, + key, + conn_type, + my_addr.is_ipv4(), + ) + .await?; Self::secure_connection( peer, signed_id_pk, @@ -373,7 +376,7 @@ impl Client { log::info!("peer address: {}, timeout: {}", peer, connect_timeout); let start = std::time::Instant::now(); // NOTICE: Socks5 is be used event in intranet. Which may be not a good way. - let mut conn = socket_client::connect_tcp(peer, local_addr, connect_timeout).await; + let mut conn = socket_client::connect_tcp_local(peer, local_addr, connect_timeout).await; let mut direct = !conn.is_err(); if interface.is_force_relay() || conn.is_err() { if !relay_server.is_empty() { @@ -506,16 +509,16 @@ impl Client { token: &str, conn_type: ConnType, ) -> ResultType { - let any_addr = Config::get_any_listen_addr(); let mut succeed = false; let mut uuid = "".to_owned(); + let mut ipv4 = true; for i in 1..=3 { // use different socket due to current hbbs implement requiring different nat address for each attempt - let mut socket = - socket_client::connect_tcp(rendezvous_server, any_addr, RENDEZVOUS_TIMEOUT) - .await - .with_context(|| "Failed to connect to rendezvous server")?; + let mut socket = socket_client::connect_tcp(rendezvous_server, RENDEZVOUS_TIMEOUT) + .await + .with_context(|| "Failed to connect to rendezvous server")?; + ipv4 = socket.local_addr().is_ipv4(); let mut msg_out = RendezvousMessage::new(); uuid = Uuid::new_v4().to_string(); log::info!( @@ -550,7 +553,7 @@ impl Client { if !succeed { bail!("Timeout"); } - Self::create_relay(peer, uuid, relay_server, key, conn_type).await + Self::create_relay(peer, uuid, relay_server, key, conn_type, ipv4).await } /// Create a relay connection to the server. @@ -560,10 +563,10 @@ impl Client { relay_server: String, key: &str, conn_type: ConnType, + ipv4: bool, ) -> ResultType { let mut conn = socket_client::connect_tcp( - crate::check_port(relay_server, RELAY_PORT), - Config::get_any_listen_addr(), + socket_client::ipv4_to_ipv6(crate::check_port(relay_server, RELAY_PORT), ipv4), CONNECT_TIMEOUT, ) .await diff --git a/src/common.rs b/src/common.rs index fe76b3168..2885f844c 100644 --- a/src/common.rs +++ b/src/common.rs @@ -312,7 +312,6 @@ async fn test_nat_type_() -> ResultType { let mut port2 = 0; let server1 = socket_client::get_target_addr(&server1)?; let server2 = socket_client::get_target_addr(&server2)?; - let mut addr = Config::get_any_listen_addr(); for i in 0..2 { let mut socket = socket_client::connect_tcp( if i == 0 { @@ -320,11 +319,15 @@ async fn test_nat_type_() -> ResultType { } else { server2.clone() }, - addr, RENDEZVOUS_TIMEOUT, ) .await?; - addr = socket.local_addr(); + if i == 0 { + Config::set_option( + "local-ip-addr".to_owned(), + socket.local_addr().ip().to_string(), + ); + } socket.send(&msg_out).await?; if let Some(Ok(bytes)) = socket.next_timeout(RENDEZVOUS_TIMEOUT).await { if let Ok(msg_in) = RendezvousMessage::parse_from_bytes(&bytes) { @@ -347,7 +350,6 @@ async fn test_nat_type_() -> ResultType { break; } } - Config::set_option("local-ip-addr".to_owned(), addr.ip().to_string()); let ok = port1 > 0 && port2 > 0; if ok { let t = if port1 == port2 { @@ -424,7 +426,6 @@ async fn test_rendezvous_server_() { let tm = std::time::Instant::now(); if socket_client::connect_tcp( crate::check_port(&host, RENDEZVOUS_PORT), - Config::get_any_listen_addr(), RENDEZVOUS_TIMEOUT, ) .await @@ -526,8 +527,7 @@ async fn check_software_update_() -> hbb_common::ResultType<()> { let rendezvous_server = socket_client::get_target_addr(&format!("rs-sg.rustdesk.com:{}", config::RENDEZVOUS_PORT))?; - let mut socket = - socket_client::new_udp(Config::get_any_listen_addr(), RENDEZVOUS_TIMEOUT).await?; + let mut socket = socket_client::new_udp_for(&rendezvous_server, RENDEZVOUS_TIMEOUT).await?; let mut msg_out = RendezvousMessage::new(); msg_out.set_software_update(SoftwareUpdate { @@ -567,12 +567,6 @@ pub fn get_full_name() -> String { ) } -pub fn is_ip(id: &str) -> bool { - hbb_common::regex::Regex::new(r"^\d+\.\d+\.\d+\.\d+(:\d+)?$") - .unwrap() - .is_match(id) -} - pub fn is_setup(name: &str) -> bool { name.to_lowercase().ends_with("install.exe") } diff --git a/src/rendezvous_mediator.rs b/src/rendezvous_mediator.rs index 2b2dd05bc..8e2e1251a 100644 --- a/src/rendezvous_mediator.rs +++ b/src/rendezvous_mediator.rs @@ -111,15 +111,13 @@ impl RendezvousMediator { }) .unwrap_or(host.to_owned()); let mut rz = Self { - addr: Config::get_any_listen_addr().into_target_addr()?, + addr: socket_client::get_target_addr(&crate::check_port(&host, RENDEZVOUS_PORT))?, host: host.clone(), host_prefix, last_id_pk_registry: "".to_owned(), }; - rz.addr = socket_client::get_target_addr(&crate::check_port(&host, RENDEZVOUS_PORT))?; - let any_addr = Config::get_any_listen_addr(); - let mut socket = socket_client::new_udp(any_addr, RENDEZVOUS_TIMEOUT).await?; + let mut socket = socket_client::new_udp_for(&rz.addr, RENDEZVOUS_TIMEOUT).await?; const TIMER_OUT: Duration = Duration::from_secs(1); let mut timer = interval(TIMER_OUT); @@ -253,7 +251,7 @@ impl RendezvousMediator { rz.addr = socket_client::get_target_addr(&crate::check_port(&host, RENDEZVOUS_PORT))?; // in some case of network reconnect (dial IP network), // old UDP socket not work any more after network recover - if let Some(s) = socket_client::rebind_udp(any_addr).await? { + if let Some(s) = socket_client::rebind_udp_for(&rz.addr).await? { socket = s; } last_dns_check = Instant::now(); @@ -293,19 +291,15 @@ impl RendezvousMediator { ) -> ResultType<()> { let peer_addr = AddrMangle::decode(&socket_addr); log::info!( - "create_relay requested from from {:?}, relay_server: {}, uuid: {}, secure: {}", + "create_relay requested from {:?}, relay_server: {}, uuid: {}, secure: {}", peer_addr, relay_server, uuid, secure, ); - let mut socket = socket_client::connect_tcp( - self.addr.to_owned(), - Config::get_any_listen_addr(), - RENDEZVOUS_TIMEOUT, - ) - .await?; + let mut socket = + socket_client::connect_tcp(self.addr.to_owned(), RENDEZVOUS_TIMEOUT).await?; let mut msg_out = Message::new(); let mut rr = RelayResponse { @@ -320,24 +314,35 @@ impl RendezvousMediator { } msg_out.set_relay_response(rr); socket.send(&msg_out).await?; - crate::create_relay_connection(server, relay_server, uuid, peer_addr, secure).await; + let v4 = socket_client::is_ipv4(&self.addr); + crate::create_relay_connection(server, relay_server, uuid, peer_addr, secure, v4).await; Ok(()) } async fn handle_intranet(&self, fla: FetchLocalAddr, server: ServerPtr) -> ResultType<()> { + let relay_server = self.get_relay_server(fla.relay_server); + if !socket_client::is_ipv4(&self.addr) { + // nat64, go relay directly, because current hbbs will crash if demangle ipv6 address + let uuid = Uuid::new_v4().to_string(); + return self + .create_relay( + fla.socket_addr.into(), + relay_server, + uuid, + server, + true, + true, + ) + .await; + } let peer_addr = AddrMangle::decode(&fla.socket_addr); log::debug!("Handle intranet from {:?}", peer_addr); - let mut socket = socket_client::connect_tcp( - self.addr.to_owned(), - Config::get_any_listen_addr(), - RENDEZVOUS_TIMEOUT, - ) - .await?; + let mut socket = + socket_client::connect_tcp(self.addr.to_owned(), RENDEZVOUS_TIMEOUT).await?; let local_addr = socket.local_addr(); let local_addr: SocketAddr = format!("{}:{}", local_addr.ip(), local_addr.port()).parse()?; let mut msg_out = Message::new(); - let relay_server = self.get_relay_server(fla.relay_server); msg_out.set_local_addr(LocalAddr { id: Config::get_id(), socket_addr: AddrMangle::encode(peer_addr).into(), @@ -372,16 +377,12 @@ impl RendezvousMediator { let peer_addr = AddrMangle::decode(&ph.socket_addr); log::debug!("Punch hole to {:?}", peer_addr); let mut socket = { - let socket = socket_client::connect_tcp( - self.addr.to_owned(), - Config::get_any_listen_addr(), - RENDEZVOUS_TIMEOUT, - ) - .await?; + let socket = + socket_client::connect_tcp(self.addr.to_owned(), RENDEZVOUS_TIMEOUT).await?; let local_addr = socket.local_addr(); // key important here for punch hole to tell my gateway incoming peer is safe. // it can not be async here, because local_addr can not be reused, we must close the connection before use it again. - allow_err!(socket_client::connect_tcp(peer_addr, local_addr, 30).await); + allow_err!(socket_client::connect_tcp_local(peer_addr, local_addr, 30).await); socket }; let mut msg_out = Message::new(); @@ -534,7 +535,9 @@ async fn direct_server(server: ServerPtr) { if let Ok(Ok((stream, addr))) = hbb_common::timeout(1000, l.accept()).await { stream.set_nodelay(true).ok(); log::info!("direct access from {}", addr); - let local_addr = stream.local_addr().unwrap_or(Config::get_any_listen_addr()); + let local_addr = stream + .local_addr() + .unwrap_or(Config::get_any_listen_addr(true)); let server = server.clone(); tokio::spawn(async move { allow_err!( @@ -653,12 +656,7 @@ async fn create_online_stream() -> ResultType { } let online_server = format!("{}:{}", tmp[0], port - 1); let server_addr = socket_client::get_target_addr(&online_server)?; - socket_client::connect_tcp( - server_addr, - Config::get_any_listen_addr(), - RENDEZVOUS_TIMEOUT, - ) - .await + socket_client::connect_tcp(server_addr, RENDEZVOUS_TIMEOUT).await } async fn query_online_states_( diff --git a/src/server.rs b/src/server.rs index d08dd2672..bbe2d5f2a 100644 --- a/src/server.rs +++ b/src/server.rs @@ -215,9 +215,10 @@ pub async fn create_relay_connection( uuid: String, peer_addr: SocketAddr, secure: bool, + ipv4: bool, ) { if let Err(err) = - create_relay_connection_(server, relay_server, uuid.clone(), peer_addr, secure).await + create_relay_connection_(server, relay_server, uuid.clone(), peer_addr, secure, ipv4).await { log::error!( "Failed to create relay connection for {} with uuid {}: {}", @@ -234,10 +235,10 @@ async fn create_relay_connection_( uuid: String, peer_addr: SocketAddr, secure: bool, + ipv4: bool, ) -> ResultType<()> { let mut stream = socket_client::connect_tcp( - crate::check_port(relay_server, RELAY_PORT), - Config::get_any_listen_addr(), + socket_client::ipv4_to_ipv6(crate::check_port(relay_server, RELAY_PORT), ipv4), CONNECT_TIMEOUT, ) .await?; diff --git a/src/server/connection.rs b/src/server/connection.rs index 5852b2b06..e1a360e7a 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1148,7 +1148,7 @@ impl Connection { } _ => {} } - if !crate::is_ip(&lr.username) && lr.username != Config::get_id() { + if !hbb_common::is_ipv4_str(&lr.username) && lr.username != Config::get_id() { self.send_login_error("Offline").await; } else if password::approve_mode() == ApproveMode::Click || password::approve_mode() == ApproveMode::Both && !password::has_valid_password() diff --git a/src/ui.rs b/src/ui.rs index e13f11d87..4adb018c8 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -9,13 +9,9 @@ use sciter::Value; use hbb_common::{ allow_err, - config::{self, Config, PeerConfig, RENDEZVOUS_PORT, RENDEZVOUS_TIMEOUT}, - futures::future::join_all, + config::{self, PeerConfig}, log, - protobuf::Message as _, rendezvous_proto::*, - tcp::FramedStream, - tokio, }; use crate::common::get_app_name; @@ -31,8 +27,6 @@ pub mod remote; #[cfg(target_os = "windows")] pub mod win_privacy; -type Message = RendezvousMessage; - pub type Children = Arc)>>; #[allow(dead_code)] type Status = (i32, bool, i64, String); @@ -124,15 +118,11 @@ pub fn start(args: &mut [String]) { let args: Vec = iter.map(|x| x.clone()).collect(); frame.set_title(&id); frame.register_behavior("native-remote", move || { - let handler = remote::SciterSession::new( - cmd.clone(), - id.clone(), - pass.clone(), - args.clone(), - ); + let handler = + remote::SciterSession::new(cmd.clone(), id.clone(), pass.clone(), args.clone()); #[cfg(not(feature = "flutter"))] crate::keyboard::set_cur_session(handler.inner()); - + Box::new(handler) }); page = "remote.html"; @@ -514,7 +504,7 @@ impl UI { fn change_id(&self, id: String) { let old_id = self.get_id(); - change_id(id, old_id); + change_id_shared(id, old_id); } fn post_request(&self, url: String, body: String, header: String) { @@ -694,101 +684,6 @@ fn get_sound_inputs() -> Vec { .collect() } -const INVALID_FORMAT: &'static str = "Invalid format"; -const UNKNOWN_ERROR: &'static str = "Unknown error"; - -#[tokio::main(flavor = "current_thread")] -async fn change_id(id: String, old_id: String) -> &'static str { - if !hbb_common::is_valid_custom_id(&id) { - return INVALID_FORMAT; - } - let uuid = machine_uid::get().unwrap_or("".to_owned()); - if uuid.is_empty() { - return UNKNOWN_ERROR; - } - let rendezvous_servers = crate::ipc::get_rendezvous_servers(1_000).await; - let mut futs = Vec::new(); - let err: Arc> = Default::default(); - for rendezvous_server in rendezvous_servers { - let err = err.clone(); - let id = id.to_owned(); - let uuid = uuid.clone(); - let old_id = old_id.clone(); - futs.push(tokio::spawn(async move { - let tmp = check_id(rendezvous_server, old_id, id, uuid).await; - if !tmp.is_empty() { - *err.lock().unwrap() = tmp; - } - })); - } - join_all(futs).await; - let err = *err.lock().unwrap(); - if err.is_empty() { - crate::ipc::set_config_async("id", id.to_owned()).await.ok(); - } - err -} - -async fn check_id( - rendezvous_server: String, - old_id: String, - id: String, - uuid: String, -) -> &'static str { - let any_addr = Config::get_any_listen_addr(); - if let Ok(mut socket) = FramedStream::new( - crate::check_port(rendezvous_server, RENDEZVOUS_PORT), - any_addr, - RENDEZVOUS_TIMEOUT, - ) - .await - { - let mut msg_out = Message::new(); - msg_out.set_register_pk(RegisterPk { - old_id, - id, - uuid: uuid.into(), - ..Default::default() - }); - let mut ok = false; - if socket.send(&msg_out).await.is_ok() { - if let Some(Ok(bytes)) = socket.next_timeout(3_000).await { - if let Ok(msg_in) = RendezvousMessage::parse_from_bytes(&bytes) { - match msg_in.union { - Some(rendezvous_message::Union::RegisterPkResponse(rpr)) => { - match rpr.result.enum_value_or_default() { - register_pk_response::Result::OK => { - ok = true; - } - register_pk_response::Result::ID_EXISTS => { - return "Not available"; - } - register_pk_response::Result::TOO_FREQUENT => { - return "Too frequent"; - } - register_pk_response::Result::NOT_SUPPORT => { - return "server_not_support"; - } - register_pk_response::Result::INVALID_ID_FORMAT => { - return INVALID_FORMAT; - } - _ => {} - } - } - _ => {} - } - } - } - } - if !ok { - return UNKNOWN_ERROR; - } - } else { - return "Failed to connect to rendezvous server"; - } - "" -} - // sacrifice some memory pub fn value_crash_workaround(values: &[Value]) -> Arc> { let persist = Arc::new(values.to_vec()); diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 3e4cd681f..ffcf110b3 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -14,20 +14,17 @@ use hbb_common::{ tokio::{self, sync::mpsc, time}, }; -#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] use hbb_common::{ config::{RENDEZVOUS_PORT, RENDEZVOUS_TIMEOUT}, futures::future::join_all, protobuf::Message as _, rendezvous_proto::*, - tcp::FramedStream, }; #[cfg(feature = "flutter")] use crate::hbbs_http::account; use crate::{common::SOFTWARE_UPDATE_URL, ipc}; -#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] type Message = RendezvousMessage; pub type Children = Arc)>>; @@ -752,7 +749,7 @@ pub fn change_id(id: String) { *ASYNC_JOB_STATUS.lock().unwrap() = " ".to_owned(); let old_id = get_id(); std::thread::spawn(move || { - *ASYNC_JOB_STATUS.lock().unwrap() = change_id_(id, old_id).to_owned(); + *ASYNC_JOB_STATUS.lock().unwrap() = change_id_shared(id, old_id).to_owned(); }); } @@ -1009,14 +1006,11 @@ pub(crate) async fn send_to_cm(data: &ipc::Data) { } } -#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] const INVALID_FORMAT: &'static str = "Invalid format"; -#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] const UNKNOWN_ERROR: &'static str = "Unknown error"; -#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] #[tokio::main(flavor = "current_thread")] -async fn change_id_(id: String, old_id: String) -> &'static str { +pub async fn change_id_shared(id: String, old_id: String) -> &'static str { if !hbb_common::is_valid_custom_id(&id) { return INVALID_FORMAT; } @@ -1064,17 +1058,14 @@ async fn change_id_(id: String, old_id: String) -> &'static str { err } -#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] async fn check_id( rendezvous_server: String, old_id: String, id: String, uuid: String, ) -> &'static str { - let any_addr = Config::get_any_listen_addr(); - if let Ok(mut socket) = FramedStream::new( + if let Ok(mut socket) = hbb_common::socket_client::connect_tcp( crate::check_port(rendezvous_server, RENDEZVOUS_PORT), - any_addr, RENDEZVOUS_TIMEOUT, ) .await From be20e03ee17619d6a402631b318124fc3076b416 Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 28 Dec 2022 15:20:42 +0800 Subject: [PATCH 1285/2015] fix MacOS kVK_ISO_Section Signed-off-by: fufesou --- Cargo.lock | 27 +++++++++++++++++++++++++-- Cargo.toml | 2 +- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 392c2c1f7..7fd3ddd7c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1518,7 +1518,7 @@ dependencies = [ "log", "objc", "pkg-config", - "rdev", + "rdev 0.5.0-2 (git+https://github.com/asur4s/rdev)", "serde 1.0.149", "serde_derive", "tfc", @@ -4325,6 +4325,29 @@ dependencies = [ "x11 2.20.1", ] +[[package]] +name = "rdev" +version = "0.5.0-2" +source = "git+https://github.com/fufesou/rdev#bc2db2f13dfdc95df8a02eb03f71325c173283dc" +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 = "rdrand" version = "0.4.0" @@ -4607,7 +4630,7 @@ dependencies = [ "num_cpus", "objc", "parity-tokio-ipc", - "rdev", + "rdev 0.5.0-2 (git+https://github.com/fufesou/rdev)", "repng", "reqwest", "rpassword 7.2.0", diff --git a/Cargo.toml b/Cargo.toml index feca1593e..4e2437185 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,7 +63,7 @@ default-net = "0.11.0" wol-rs = "0.9.1" flutter_rust_bridge = { git = "https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge", optional = true } errno = "0.2.8" -rdev = { git = "https://github.com/asur4s/rdev" } +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 b9ee0590bb2dda0a63aea14146fc054c9a719f69 Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 28 Dec 2022 15:26:44 +0800 Subject: [PATCH 1286/2015] update rdev dep Signed-off-by: fufesou --- Cargo.lock | 27 ++------------------------- libs/enigo/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7fd3ddd7c..ebf82edac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1518,7 +1518,7 @@ dependencies = [ "log", "objc", "pkg-config", - "rdev 0.5.0-2 (git+https://github.com/asur4s/rdev)", + "rdev", "serde 1.0.149", "serde_derive", "tfc", @@ -4302,29 +4302,6 @@ dependencies = [ "num_cpus", ] -[[package]] -name = "rdev" -version = "0.5.0-2" -source = "git+https://github.com/asur4s/rdev#e2d6171a6d844aaac19cdbda04239e6b8cc483e5" -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" @@ -4630,7 +4607,7 @@ dependencies = [ "num_cpus", "objc", "parity-tokio-ipc", - "rdev 0.5.0-2 (git+https://github.com/fufesou/rdev)", + "rdev", "repng", "reqwest", "rpassword 7.2.0", diff --git a/libs/enigo/Cargo.toml b/libs/enigo/Cargo.toml index 83c79e064..2c4070ed1 100644 --- a/libs/enigo/Cargo.toml +++ b/libs/enigo/Cargo.toml @@ -22,7 +22,7 @@ appveyor = { repository = "pythoneer/enigo-85xiy" } serde = { version = "1.0", optional = true } serde_derive = { version = "1.0", optional = true } log = "0.4" -rdev = { git = "https://github.com/asur4s/rdev" } +rdev = { git = "https://github.com/fufesou/rdev" } tfc = { git = "https://github.com/asur4s/The-Fat-Controller" } hbb_common = { path = "../hbb_common" } From bf65a88647c4f449bd9ed525a73f35c1599d5798 Mon Sep 17 00:00:00 2001 From: Agent-JY Date: Wed, 28 Dec 2022 13:04:28 +0100 Subject: [PATCH 1287/2015] Update de.rs --- src/lang/de.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/lang/de.rs b/src/lang/de.rs index 3150bcaa3..607fd13d8 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -369,7 +369,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable LAN Discovery", "LAN-Erkennung aktivieren"), ("Deny LAN Discovery", "LAN-Erkennung verbieten"), ("Write a message", "Nachricht schreiben"), - ("Prompt", ""), //Aufforderung"), + ("Prompt", "Meldung"), ("Please wait for confirmation of UAC...", "Bitte auf die Bestätigung des Nutzers warten..."), ("elevated_foreground_window_tip", "Das aktuell geöffnete Fenster des ferngesteuerten Computers benötigt höhere Rechte. Deshalb ist es derzeit nicht möglich, die Maus und die Tastatur zu verwenden. Bitten Sie den Nutzer, dessen Computer Sie fernsteuern, das Fenster zu minimieren oder die Rechte zu erhöhen. Um dieses Problem zunünftig zu vermeiden, wird empfohlen, die Software auf dem ferngesteuerten Computer zu installieren."), ("Disconnected", "Verbindung abgebrochen"), @@ -401,12 +401,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", "Dies ist nur möglich, wenn der Zugriff nur über ein permanentes Passwort erfolgt."), ("wayland_experiment_tip", "Die Unterstützung von Wayland ist nur experimentell. Bitte nutzen Sie X11, wenn Sie einen unbeaufsichtigten Zugriff benötigen."), ("Right click to select tabs", "Register mit rechtem Mausklick auswählen"), - ("Skipped", ""), + ("Skipped", "Übersprungen"), ("Add to Address Book", "Zum Adressbuch hinzufügen"), ("Group", "Gruppe"), ("Search", "Suchen"), - ("Closed manually by the web console", ""), - ("Local keyboard type", ""), - ("Select local keyboard type", ""), + ("Closed manually by the web console", "Manuell über die Webkonsole beendet"), + ("Local keyboard type", "Lokaler Tastaturtyp"), + ("Select local keyboard type", "Lokalen Tastaturtyp Auswählen"), ].iter().cloned().collect(); } From 4d2e62981b9766e5cc256b7865eac505ec53709e Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 29 Dec 2022 00:02:31 +0800 Subject: [PATCH 1288/2015] make cli compilable --- src/cli.rs | 32 +++++++++++----- src/client.rs | 27 +++++++++++--- src/client/file_trait.rs | 2 +- src/common.rs | 19 ++++++++++ src/flutter.rs | 20 +--------- src/keyboard.rs | 44 ++++++++++------------ src/ui_session_interface.rs | 74 +++++++++++++++---------------------- 7 files changed, 115 insertions(+), 103 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index dd39569ca..e16c20f74 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -6,6 +6,7 @@ use hbb_common::{ protobuf::Message as _, tokio::{self, sync::mpsc}, Stream, + rendezvous_proto::ConnType, }; use std::sync::{Arc, RwLock}; @@ -33,14 +34,18 @@ impl Session { .lc .write() .unwrap() - .initialize(id.to_owned(), false, true); + .initialize(id.to_owned(), ConnType::PORT_FORWARD); session } } #[async_trait] impl Interface for Session { - fn msgbox(&self, msgtype: &str, title: &str, text: &str) { + fn get_login_config_handler(&self) -> Arc> { + return self.lc.clone(); + } + + fn msgbox(&self, msgtype: &str, title: &str, text: &str, link: &str) { if msgtype == "input-password" { self.sender .send(Data::Login((self.password.clone(), true))) @@ -61,12 +66,11 @@ impl Interface for Session { } fn handle_peer_info(&mut self, pi: PeerInfo) { - let username = self.lc.read().unwrap().get_username(&pi); - self.lc.write().unwrap().handle_peer_info(username, pi); + self.lc.write().unwrap().handle_peer_info(&pi); } - async fn handle_hash(&mut self, hash: Hash, peer: &mut Stream) { - handle_hash(self.lc.clone(), hash, self, peer).await; + async fn handle_hash(&mut self, pass: &str, hash: Hash, peer: &mut Stream) { + handle_hash(self.lc.clone(), &pass, hash, self, peer).await; } async fn handle_login_from_ui(&mut self, password: String, remember: bool, peer: &mut Stream) { @@ -95,9 +99,19 @@ pub async fn start_one_port_forward( crate::common::test_nat_type(); let (sender, mut receiver) = mpsc::unbounded_channel::(); let handler = Session::new(&id, sender); - handler.lc.write().unwrap().port_forward = (remote_host, remote_port); - if let Err(err) = - crate::port_forward::listen(handler.id.clone(), port, handler.clone(), receiver, &key, &token).await + if let Err(err) = crate::port_forward::listen( + handler.id.clone(), + handler.password.clone(), + port, + handler.clone(), + receiver, + &key, + &token, + handler.lc.clone(), + remote_host, + remote_port, + ) + .await { log::error!("Failed to listen on {}: {}", port, err); } diff --git a/src/client.rs b/src/client.rs index c52b1adf3..de08ee0e6 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1481,6 +1481,19 @@ impl LoginConfigHandler { msg_out.set_misc(misc); msg_out } + + pub fn set_force_relay(&mut self, direct: bool, received: bool) { + self.force_relay = false; + if direct && !received { + let errno = errno::errno().0; + log::info!("errno is {}", errno); + // TODO: check mac and ios + if cfg!(windows) && errno == 10054 || !cfg!(windows) && errno == 104 { + self.force_relay = true; + self.set_option("force-always-relay".to_owned(), "Y".to_owned()); + } + } + } } /// Media data. @@ -1825,18 +1838,20 @@ pub trait Interface: Send + Clone + 'static + Sized { fn msgbox(&self, msgtype: &str, title: &str, text: &str, link: &str); fn handle_login_error(&mut self, err: &str) -> bool; fn handle_peer_info(&mut self, pi: PeerInfo); - fn set_force_relay(&mut self, direct: bool, received: bool); - fn set_connection_info(&mut self, direct: bool, received: bool); - fn is_file_transfer(&self) -> bool; - fn is_port_forward(&self) -> bool; - fn is_rdp(&self) -> bool; fn on_error(&self, err: &str) { self.msgbox("error", "Error", err, ""); } - fn is_force_relay(&self) -> bool; async fn handle_hash(&mut self, pass: &str, hash: Hash, peer: &mut Stream); async fn handle_login_from_ui(&mut self, password: String, remember: bool, peer: &mut Stream); async fn handle_test_delay(&mut self, t: TestDelay, peer: &mut Stream); + + fn get_login_config_handler(&self) -> Arc>; + fn set_force_relay(&self, direct: bool, received: bool) { + self.get_login_config_handler().write().unwrap().set_force_relay(direct, received); + } + fn is_force_relay(&self) -> bool { + self.get_login_config_handler().read().unwrap().force_relay + } } /// Data used by the client interface. diff --git a/src/client/file_trait.rs b/src/client/file_trait.rs index b94177c51..2ecfca837 100644 --- a/src/client/file_trait.rs +++ b/src/client/file_trait.rs @@ -22,7 +22,7 @@ pub trait FileManager: Interface { #[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))] fn read_dir(&self, path: &str, include_hidden: bool) -> String { - use crate::flutter::make_fd_to_json; + use crate::common::make_fd_to_json; match fs::read_dir(&fs::get_path(path), include_hidden) { Ok(fd) => make_fd_to_json(fd.id, fd.path, &fd.entries), Err(_) => "".into(), diff --git a/src/common.rs b/src/common.rs index 2885f844c..f83fbc69d 100644 --- a/src/common.rs +++ b/src/common.rs @@ -693,3 +693,22 @@ lazy_static::lazy_static! { lazy_static::lazy_static! { pub static ref IS_X11: Mutex = Mutex::new("x11" == hbb_common::platform::linux::get_display_server()); } + +pub fn make_fd_to_json(id: i32, path: String, entries: &Vec) -> String { + use serde_json::json; + let mut fd_json = serde_json::Map::new(); + fd_json.insert("id".into(), json!(id)); + fd_json.insert("path".into(), json!(path)); + + let mut entries_out = vec![]; + for entry in entries { + let mut entry_map = serde_json::Map::new(); + entry_map.insert("entry_type".into(), json!(entry.entry_type.value())); + entry_map.insert("name".into(), json!(entry.name)); + entry_map.insert("size".into(), json!(entry.size)); + entry_map.insert("modified_time".into(), json!(entry.modified_time)); + entries_out.push(entry_map); + } + fd_json.insert("entries".into(), json!(entries_out)); + serde_json::to_string(&fd_json).unwrap_or("".into()) +} diff --git a/src/flutter.rs b/src/flutter.rs index 788a9f540..4798820e7 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -242,7 +242,7 @@ impl InvokeUiSession for FlutterHandler { self.push_event( "file_dir", vec![ - ("value", &make_fd_to_json(id, path, entries)), + ("value", &crate::common::make_fd_to_json(id, path, entries)), ("is_local", "false"), ], ); @@ -545,24 +545,6 @@ pub fn get_session_id(id: String) -> String { }; } -pub fn make_fd_to_json(id: i32, path: String, entries: &Vec) -> String { - let mut fd_json = serde_json::Map::new(); - fd_json.insert("id".into(), json!(id)); - fd_json.insert("path".into(), json!(path)); - - let mut entries_out = vec![]; - for entry in entries { - let mut entry_map = serde_json::Map::new(); - entry_map.insert("entry_type".into(), json!(entry.entry_type.value())); - entry_map.insert("name".into(), json!(entry.name)); - entry_map.insert("size".into(), json!(entry.size)); - entry_map.insert("modified_time".into(), json!(entry.modified_time)); - entries_out.push(entry_map); - } - fd_json.insert("entries".into(), json!(entries_out)); - serde_json::to_string(&fd_json).unwrap_or("".into()) -} - pub fn make_fd_flutter(id: i32, entries: &Vec, only_count: bool) -> String { let mut m = serde_json::Map::new(); m.insert("id".into(), json!(id)); diff --git a/src/keyboard.rs b/src/keyboard.rs index d22573fbc..a3088c63f 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -3,10 +3,10 @@ use crate::client::get_key_state; use crate::common::GrabState; #[cfg(feature = "flutter")] use crate::flutter::FlutterHandler; -#[cfg(not(feature = "flutter"))] +#[cfg(not(any(feature = "flutter", feature = "cli")))] use crate::ui::remote::SciterHandler; use crate::ui_session_interface::Session; -use hbb_common::{log, message_proto::*, config::LocalConfig}; +use hbb_common::{log, message_proto::*}; use rdev::{Event, EventType, Key}; #[cfg(any(target_os = "windows", target_os = "macos"))] use std::sync::atomic::{AtomicBool, Ordering}; @@ -27,7 +27,7 @@ lazy_static::lazy_static! { static ref CUR_SESSION: Arc>>> = Default::default(); } -#[cfg(not(feature = "flutter"))] +#[cfg(not(any(feature = "flutter", feature = "cli")))] lazy_static::lazy_static! { static ref CUR_SESSION: Arc>>> = Default::default(); } @@ -53,7 +53,7 @@ pub fn set_cur_session(session: Session) { *CUR_SESSION.lock().unwrap() = Some(session); } -#[cfg(not(feature = "flutter"))] +#[cfg(not(any(feature = "flutter", feature = "cli")))] pub fn set_cur_session(session: Session) { *CUR_SESSION.lock().unwrap() = Some(session); } @@ -62,11 +62,11 @@ pub mod client { use super::*; pub fn get_keyboard_mode() -> String { + #[cfg(not(feature = "cli"))] if let Some(handler) = CUR_SESSION.lock().unwrap().as_ref() { - handler.get_keyboard_mode() - } else { - "legacy".to_string() - } + return handler.get_keyboard_mode(); + } + "legacy".to_string() } pub fn start_grab_loop() { @@ -332,12 +332,8 @@ pub fn event_to_key_event(event: &Event) -> Option { let keyboard_mode = get_keyboard_mode_enum(); key_event.mode = keyboard_mode.into(); let mut key_event = match keyboard_mode { - KeyboardMode::Map => { - map_keyboard_mode(event, key_event)? - } - KeyboardMode::Translate => { - translate_keyboard_mode(event, key_event)? - } + KeyboardMode::Map => map_keyboard_mode(event, key_event)?, + KeyboardMode::Translate => translate_keyboard_mode(event, key_event)?, _ => { #[cfg(not(any(target_os = "android", target_os = "ios")))] { @@ -366,18 +362,18 @@ pub fn event_type_to_event(event_type: EventType) -> Event { } pub fn send_key_event(key_event: &KeyEvent) { + #[cfg(not(feature = "cli"))] if let Some(handler) = CUR_SESSION.lock().unwrap().as_ref() { handler.send_key_event(key_event); } } pub fn get_peer_platform() -> String { + #[cfg(not(feature = "cli"))] if let Some(handler) = CUR_SESSION.lock().unwrap().as_ref() { - handler.peer_platform() - } else { - log::error!("get peer platform error"); - "Windows".to_string() - } + return handler.peer_platform(); + } + "Windows".to_string() } #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -389,7 +385,7 @@ pub fn legacy_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option { return None; } - }; + }; let peer = get_peer_platform(); let is_win = peer == "Windows"; @@ -621,12 +617,12 @@ pub fn map_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option event.scan_code, "macos" => { - if LocalConfig::get_kb_layout_type() == "ISO" { + 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)?, }; #[cfg(target_os = "macos")] @@ -639,12 +635,12 @@ pub fn map_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option rdev::linux_code_to_win_scancode(event.code as _)?, "macos" => { - if LocalConfig::get_kb_layout_type() == "ISO" { + if hbb_common::config::LocalConfig::get_kb_layout_type() == "ISO" { rdev::linux_code_to_macos_iso_code(event.scan_code)? } else { rdev::linux_code_to_macos_code(event.code as _)? } - }, + } _ => event.code as _, }; #[cfg(any(target_os = "android", target_os = "ios"))] diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 6f115571c..63a8d8711 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -32,6 +32,32 @@ pub struct Session { } impl Session { + pub fn is_file_transfer(&self) -> bool { + self.lc + .read() + .unwrap() + .conn_type + .eq(&ConnType::FILE_TRANSFER) + } + + pub fn is_port_forward(&self) -> bool { + self.lc + .read() + .unwrap() + .conn_type + .eq(&ConnType::PORT_FORWARD) + } + + pub fn is_rdp(&self) -> bool { + self.lc.read().unwrap().conn_type.eq(&ConnType::RDP) + } + + pub fn set_connection_info(&mut self, direct: bool, received: bool) { + let mut lc = self.lc.write().unwrap(); + lc.direct = Some(direct); + lc.received = received; + } + pub fn get_view_style(&self) -> String { self.lc.read().unwrap().view_style.clone() } @@ -631,32 +657,16 @@ impl FileManager for Session {} #[async_trait] impl Interface for Session { + fn get_login_config_handler(&self) -> Arc> { + return self.lc.clone(); + } + fn send(&self, data: Data) { if let Some(sender) = self.sender.read().unwrap().as_ref() { sender.send(data).ok(); } } - fn is_file_transfer(&self) -> bool { - self.lc - .read() - .unwrap() - .conn_type - .eq(&ConnType::FILE_TRANSFER) - } - - fn is_port_forward(&self) -> bool { - self.lc - .read() - .unwrap() - .conn_type - .eq(&ConnType::PORT_FORWARD) - } - - fn is_rdp(&self) -> bool { - self.lc.read().unwrap().conn_type.eq(&ConnType::RDP) - } - fn msgbox(&self, msgtype: &str, title: &str, text: &str, link: &str) { let direct = self.lc.read().unwrap().direct.unwrap_or_default(); let received = self.lc.read().unwrap().received; @@ -748,30 +758,6 @@ impl Interface for Session { handle_test_delay(t, peer).await; } } - - fn set_connection_info(&mut self, direct: bool, received: bool) { - let mut lc = self.lc.write().unwrap(); - lc.direct = Some(direct); - lc.received = received; - } - - fn set_force_relay(&mut self, direct: bool, received: bool) { - let mut lc = self.lc.write().unwrap(); - lc.force_relay = false; - if direct && !received { - let errno = errno::errno().0; - log::info!("errno is {}", errno); - // TODO: check mac and ios - if cfg!(windows) && errno == 10054 || !cfg!(windows) && errno == 104 { - lc.force_relay = true; - lc.set_option("force-always-relay".to_owned(), "Y".to_owned()); - } - } - } - - fn is_force_relay(&self) -> bool { - self.lc.read().unwrap().force_relay - } } impl Session { From cfaeb6ac0a51e7b015d80eb75f936e67fb2e4c04 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 29 Dec 2022 00:16:41 +0800 Subject: [PATCH 1289/2015] fix ci --- src/client.rs | 2 +- src/flutter_ffi.rs | 3 ++- src/rendezvous_mediator.rs | 2 +- src/tray.rs | 2 +- src/ui.rs | 1 - 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/client.rs b/src/client.rs index de08ee0e6..1c6f63f36 100644 --- a/src/client.rs +++ b/src/client.rs @@ -420,7 +420,7 @@ impl Client { key: &str, conn: &mut Stream, direct: bool, - mut interface: impl Interface, + interface: impl Interface, ) -> ResultType<()> { let rs_pk = get_rs_pk(if key.is_empty() { hbb_common::config::RS_PUB_KEY diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 3be0c9fed..bf5ebaf4e 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -19,7 +19,8 @@ use crate::flutter::{self, SESSIONS}; use crate::ui_interface::{self, *}; use crate::{ client::file_trait::FileManager, - flutter::{make_fd_to_json, session_add, session_start_}, + common::make_fd_to_json, + flutter::{session_add, session_start_}, }; fn initialize(app_dir: &str) { *config::APP_DIR.write().unwrap() = app_dir.to_owned(); diff --git a/src/rendezvous_mediator.rs b/src/rendezvous_mediator.rs index 8e2e1251a..f962810a2 100644 --- a/src/rendezvous_mediator.rs +++ b/src/rendezvous_mediator.rs @@ -24,7 +24,7 @@ use hbb_common::{ time::{interval, Duration}, }, udp::FramedSocket, - AddrMangle, IntoTargetAddr, ResultType, TargetAddr, + AddrMangle, ResultType, TargetAddr, }; use crate::server::{check_zombie, new as new_server, ServerPtr}; diff --git a/src/tray.rs b/src/tray.rs index b73e46301..1afd988ae 100644 --- a/src/tray.rs +++ b/src/tray.rs @@ -206,7 +206,7 @@ fn is_service_stoped() -> bool { pub fn make_tray() { use tray_item::TrayItem; let mode = dark_light::detect(); - let mut icon_path = ""; + let icon_path; match mode { dark_light::Mode::Dark => { icon_path = "mac-tray-light.png"; diff --git a/src/ui.rs b/src/ui.rs index 4adb018c8..d45a64298 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -11,7 +11,6 @@ use hbb_common::{ allow_err, config::{self, PeerConfig}, log, - rendezvous_proto::*, }; use crate::common::get_app_name; From becf2a9cd79abaa1bc315a70c054cb71ba94c924 Mon Sep 17 00:00:00 2001 From: Phongsathorn Date: Thu, 29 Dec 2022 02:04:41 +0700 Subject: [PATCH 1290/2015] lang: create th.rs (Thai translation) --- src/lang/th.rs | 413 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 413 insertions(+) create mode 100644 src/lang/th.rs diff --git a/src/lang/th.rs b/src/lang/th.rs new file mode 100644 index 000000000..186e88453 --- /dev/null +++ b/src/lang/th.rs @@ -0,0 +1,413 @@ +lazy_static::lazy_static! { + pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "สถานะ"), + ("Your Desktop", "หน้าจอของคุณ"), + ("desk_tip", "คุณสามารถเข้าถึงเดสก์ท็อปของคุณได้ด้วย ID และรหัสผ่านต่อไปนี้"), + ("Password", "รหัสผ่าน"), + ("Ready", "พร้อม"), + ("Established", "เชื่อมต่อแล้ว"), + ("connecting_status", "กำลังเชื่อมต่อไปยังเครือข่าย RustDesk..."), + ("Enable Service", "เปิดใช้การงานเซอร์วิส"), + ("Start Service", "เริ่มต้นใช้งานเซอร์วิส"), + ("Service is running", "เซอร์วิสกำลังทำงาน"), + ("Service is not running", "เซอร์วิสไม่ทำงาน"), + ("not_ready_status", "ไม่พร้อมใช้งาน กรุณาตรวจสอบการเชื่อมต่ออินเทอร์เน็ตของคุณ"), + ("Control Remote Desktop", "การควบคุมเดสก์ท็อปปลายทาง"), + ("Transfer File", "การถ่ายโอนไฟล์"), + ("Connect", "เชื่อมต่อ"), + ("Recent Sessions", "เซสชันล่าสุด"), + ("Address Book", "สมุดรายชื่อ"), + ("Confirmation", "การยืนยัน"), + ("TCP Tunneling", "อุโมงค์การเชื่อมต่อ TCP"), + ("Remove", "ลบ"), + ("Refresh random password", "รีเฟรชรหัสผ่านใหม่แบบสุ่ม"), + ("Set your own password", "ตั้งรหัสผ่านของคุณเอง"), + ("Enable Keyboard/Mouse", "เปิดการใช้งาน คีย์บอร์ด/เมาส์"), + ("Enable Clipboard", "เปิดการใช้งาน คลิปบอร์ด"), + ("Enable File Transfer", "เปิดการใช้งาน การถ่ายโอนไฟล์"), + ("Enable TCP Tunneling", "เปิดการใช้งาน อุโมงค์การเชื่อมต่อ TCP"), + ("IP Whitelisting", "IP ไวท์ลิสต์"), + ("ID/Relay Server", "เซิร์ฟเวอร์ ID/Relay"), + ("Import Server Config", "นำเข้าการตั้งค่าเซิร์ฟเวอร์"), + ("Export Server Config", "ส่งออกการตั้งค่าเซิร์ฟเวอร์"), + ("Import server configuration successfully", "นำเข้าการตั้งค่าเซิร์ฟเวอร์เสร็จสมบูรณ์"), + ("Export server configuration successfully", "ส่งออกการตั้งค่าเซิร์ฟเวอร์เสร็จสมบูรณ์"), + ("Invalid server configuration", "การตั้งค่าของเซิร์ฟเวอร์ไม่ถูกต้อง"), + ("Clipboard is empty", "คลิปบอร์ดว่างเปล่า"), + ("Stop service", "หยุดการใช้งานเซอร์วิส"), + ("Change ID", "เปลี่ยน ID"), + ("Website", "เว็บไซต์"), + ("About", "เกี่ยวกับ"), + ("Slogan_tip", "ทำด้วยใจ ในโลกใบนี้ที่ยุ่งเหยิง!"), + ("Privacy Statement", "คำแถลงเกี่ยวกับความเป็นส่วนตัว"), + ("Mute", "ปิดเสียง"), + ("Audio Input", "ออดิโออินพุท"), + ("Enhancements", "การปรับปรุง"), + ("Hardware Codec", "ฮาร์ดแวร์ codec"), + ("Adaptive Bitrate", "บิทเรทผันแปร"), + ("ID Server", "เซิร์ฟเวอร์ ID"), + ("Relay Server", "เซิร์ฟเวอร์ Relay"), + ("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", "ไม่พร้อมใช้งาน"), + ("Too frequent", "ดำเนินการถี่เกินไป"), + ("Cancel", "ยกเลิก"), + ("Skip", "ข้าม"), + ("Close", "ปิด"), + ("Retry", "ลองใหม่อีกครั้ง"), + ("OK", "ตกลง"), + ("Password Required", "ต้องใช้รหัสผ่าน"), + ("Please enter your password", "กรุณาใส่รหัสผ่านของคุณ"), + ("Remember password", "จดจำรหัสผ่าน"), + ("Wrong Password", "รหัสผ่านไม่ถูกต้อง"), + ("Do you want to enter again?", "ต้องการใส่ข้อมูลอีกครั้งหรือไม่?"), + ("Connection Error", "การเชื่อมต่อผิดพลาด"), + ("Error", "ข้อผิดพลาด"), + ("Reset by the peer", "รีเซ็ตโดยอีกฝั่ง"), + ("Connecting...", "กำลังเชื่อมต่อ..."), + ("Connection in progress. Please wait.", "กำลังดำเนินการเชื่อมต่อ กรุณารอซักครู่"), + ("Please try 1 minute later", "กรุณาลองใหม่อีกครั้งใน 1 นาที"), + ("Login Error", "การเข้าสู่ระบบผิดพลาด"), + ("Successful", "สำเร็จ"), + ("Connected, waiting for image...", "เชื่อมต่อสำเร็จ กำลังรับข้อมูลภาพ..."), + ("Name", "ชื่อ"), + ("Type", "ประเภท"), + ("Modified", "แก้ไขล่าสุด"), + ("Size", "ขนาด"), + ("Show Hidden Files", "แสดงไฟล์ที่ถูกซ่อน"), + ("Receive", "รับ"), + ("Send", "ส่ง"), + ("Refresh File", "รีเฟรชไฟล์"), + ("Local", "ต้นทาง"), + ("Remote", "ปลายทาง"), + ("Remote Computer", "คอมพิวเตอร์ปลายทาง"), + ("Local Computer", "คอมพิวเตอร์ต้นทาง"), + ("Confirm Delete", "ยืนยันการลบ"), + ("Delete", "ลบ"), + ("Properties", "ข้อมูล"), + ("Multi Select", "เลือกหลายรายการ"), + ("Select All", "เลือกทั้งหมด"), + ("Unselect All", "ยกเลิกการเลือกทั้งหมด"), + ("Empty Directory", "ไดเรกทอรีว่างเปล่า"), + ("Not an empty directory", "ไม่ใช่ไดเรกทอรีว่างเปล่า"), + ("Are you sure you want to delete this file?", "คุณแน่ใจหรือไม่ที่จะลบไฟล์นี้?"), + ("Are you sure you want to delete this empty directory?", "คุณแน่ใจหรือไม่ที่จะลบไดเรอทอรีว่างเปล่านี้?"), + ("Are you sure you want to delete the file of this directory?", "คุณแน่ใจหรือไม่ที่จะลบไฟล์ของไดเรกทอรีนี้?"), + ("Do this for all conflicts", "ดำเนินการแบบเดียวกันสำหรับรายการทั้งหมด"), + ("This is irreversible!", "การดำเนินการนี้ไม่สามารถย้อนกลับได้!"), + ("Deleting", "กำลังลบ"), + ("files", "ไฟล์"), + ("Waiting", "กำลังรอ"), + ("Finished", "เสร็จแล้ว"), + ("Speed", "ความเร็ว"), + ("Custom Image Quality", "คุณภาพของภาพแบบกำหนดเอง"), + ("Privacy mode", "โหมดความเป็นส่วนตัว"), + ("Block user input", "บล็อคอินพุทจากผู้ใช้งาน"), + ("Unblock user input", "ยกเลิกการบล็อคอินพุทจากผู้ใช้งาน"), + ("Adjust Window", "ปรับขนาดหน้าต่าง"), + ("Original", "ต้นฉบับ"), + ("Shrink", "ย่อ"), + ("Stretch", "ยืด"), + ("Scrollbar", "แถบเลื่อน"), + ("ScrollAuto", "เลื่อนอัตโนมัติ"), + ("Good image quality", "ภาพคุณภาพดี"), + ("Balanced", "สมดุล"), + ("Optimize reaction time", "เน้นการตอบสนอง"), + ("Custom", "กำหนดเอง"), + ("Show remote cursor", "แสดงเคอร์เซอร์ปลายทาง"), + ("Show quality monitor", "แสดงคุณภาพหน้าจอ"), + ("Disable clipboard", "ปิดการใช้งานคลิปบอร์ด"), + ("Lock after session end", "ล็อคหลังจากจบเซสชัน"), + ("Insert", "แทรก"), + ("Insert Lock", "แทรกล็อค"), + ("Refresh", "รีเฟรช"), + ("ID does not exist", "ไม่พอข้อมูล ID"), + ("Failed to connect to rendezvous server", "การเชื่อมต่อไปยังเซิร์ฟเวอร์นัดพบล้มเหลว"), + ("Please try later", "กรุณาลองใหม่ในภายหลัง"), + ("Remote desktop is offline", "เดสก์ท็อปปลายทางออฟไลน์"), + ("Key mismatch", "คีย์ไม่ถูกต้อง"), + ("Timeout", "หมดเวลา"), + ("Failed to connect to relay server", "การเชื่อมต่อไปยังเซิร์ฟเวอร์รีเลย์ล้มเหลว"), + ("Failed to connect via rendezvous server", "การเชื่อมต่อผ่านเซิร์ฟเวอร์นัดพบล้มเหลว"), + ("Failed to connect via relay server", "การเชื่อมต่อผ่านเซิร์ฟเวอร์รีเลย์ล้มเหลว"), + ("Failed to make direct connection to remote desktop", "การเชื่อมต่อตรงไปยังเดสก์ท็อปปลายทางล้มเหลว"), + ("Set Password", "ตั้งรหัสผ่าน"), + ("OS Password", "รหัสผ่านระบบปฏิบัติการ"), + ("install_tip", "เนื่องด้วยข้อจำกัดของการใช้งาน UAC ทำให้ RustDesk ไม่สามารถทำงานได้ปกติในฝั่งปลายทางในบางครั้ง เพื่อหลีกเลี่ยงข้อจำกัดของ UAC กรุณากดปุ่มด้านล่างเพื่อติดตั้ง RustDesk ไปยังระบบของคุณ"), + ("Click to upgrade", "คลิกเพื่ออัปเกรด"), + ("Click to download", "คลิกเพื่อดาวน์โหลด"), + ("Click to update", "คลิกเพื่ออัปเดต"), + ("Configure", "ปรับแต่งค่า"), + ("config_acc", "เพื่อที่จะควบคุมเดสก์ท็อปปลายทางของคุณ คุณจำเป็นจะต้องอนุญาตสิทธิ์ \"การเข้าถึง\" ให้แก่ RustDesk"), + ("config_screen", "เพื่อที่จะควบคุมเดสก์ท็อปปลายทางของคุณ คุณจำเป็นจะต้องอนุญาตสิทธิ์ \"การบันทึกภาพหน้าจอ\" ให้แก่ RustDesk"), + ("Installing ...", "กำลังติดตั้ง ..."), + ("Install", "ติดตั้ง"), + ("Installation", "การติดตั้ง"), + ("Installation Path", "ตำแหน่งที่ติดตั้ง"), + ("Create start menu shortcuts", "สร้างทางลัดไปยัง Start Menu"), + ("Create desktop icon", "สร้างไอคอนบนเดสก์ท็อป"), + ("agreement_tip", "ในการเริ่มต้นการติดตั้ง ถือว่าคุณได้ยอมรับข้อตกลงใบอนุญาตแล้ว"), + ("Accept and Install", "ยอมรับและติดตั้ง"), + ("End-user license agreement", "ข้อตกลงใบอนุญาตผู้ใช้งาน"), + ("Generating ...", "กำลังสร้าง ..."), + ("Your installation is lower version.", "การติดตั้งของคุณเป็นเวอร์ชั่นที่ต่ำกว่า"), + ("not_close_tcp_tip", "อย่าปิดหน้าต่างนี้ในขณะที่คุณกำลังใช้งานอุโมงค์การเชื่อมต่อ"), + ("Listening ...", "กำลังรอรับข้อมูล ..."), + ("Remote Host", "โฮสต์ปลายทาง"), + ("Remote Port", "พอร์ทปลายทาง"), + ("Action", "การดำเนินการ"), + ("Add", "เพิ่ม"), + ("Local Port", "พอร์ทต้นทาง"), + ("Local Address", "ที่อยู่ต้นทาง"), + ("Change Local Port", "เปลี่ยนพอร์ทต้นทาง"), + ("setup_server_tip", "เพื่อการเชื่อมต่อที่เร็วขึ้น กรุณาเซ็ตอัปเซิร์ฟเวอร์ของคุณเอง"), + ("Too short, at least 6 characters.", "สั้นเกินไป ต้องไม่ต่ำกว่า 6 ตัวอักษร"), + ("The confirmation is not identical.", "การยืนยันข้อมูลไม่ถูกต้อง"), + ("Permissions", "สิทธิ์การใช้งาน"), + ("Accept", "ยอมรับ"), + ("Dismiss", "ปิด"), + ("Disconnect", "ยกเลิกการเชื่อมต่อ"), + ("Allow using keyboard and mouse", "อนุญาตให้ใช้งานคีย์บอร์ดและเมาส์"), + ("Allow using clipboard", "อนุญาตให้ใช้คลิปบอร์ด"), + ("Allow hearing sound", "อนุญาตให้ได้ยินเสียง"), + ("Allow file copy and paste", "อนุญาตให้มีการคัดลอกและวางไฟล์"), + ("Connected", "เชื่อมต่อแล้ว"), + ("Direct and encrypted connection", "การเชื่อมต่อตรงที่มีการเข้ารหัส"), + ("Relayed and encrypted connection", "การเชื่อมต่อแบบรีเลย์ที่มีการเข้ารหัส"), + ("Direct and unencrypted connection", "การเชื่อมต่อตรงที่ไม่มีการเข้ารหัส"), + ("Relayed and unencrypted connection", "การเชื่อมต่อแบบรีเลย์ที่ไม่มีการเข้ารหัส"), + ("Enter Remote ID", "กรอก ID ปลายทาง"), + ("Enter your password", "กรอกรหัสผ่าน"), + ("Logging in...", "กำลังเข้าสู่ระบบ..."), + ("Enable RDP session sharing", "เปิดการใช้งานการแชร์เซสชัน RDP"), + ("Auto Login", "เข้าสู่ระบอัตโนมัติ"), + ("Enable Direct IP Access", "เปิดการใช้งาน IP ตรง"), + ("Rename", "ปลายทาง"), + ("Space", "พื้นที่ว่าง"), + ("Create Desktop Shortcut", "สร้างทางลัดบนเดสก์ท็อป"), + ("Change Path", "เปลี่ยนตำแหน่ง"), + ("Create Folder", "สร้างโฟลเดอร์"), + ("Please enter the folder name", "กรุณาใส่ชื่อโฟลเดอร์"), + ("Fix it", "แก้ไข"), + ("Warning", "คำเตือน"), + ("Login screen using Wayland is not supported", "หน้าจอการเข้าสู่ระบบโดยใช้ Wayland ยังไม่ถูกรองรับ"), + ("Reboot required", "จำเป็นต้องเริ่มต้นระบบใหม่"), + ("Unsupported display server ", "เซิร์ฟเวอร์การแสดงผลที่ไม่รองรับ"), + ("x11 expected", "ต้องใช้งาน x11"), + ("Port", "พอร์ท"), + ("Settings", "ตั้งค่า"), + ("Username", "ชื่อผู้ใช้งาน"), + ("Invalid port", "พอร์ทไม่ถูกต้อง"), + ("Closed manually by the peer", "ถูกปิดโดยอีกฝั่งการการเชื่อมต่อ"), + ("Enable remote configuration modification", "เปิดการใช้งานการแก้ไขการตั้งค่าปลายทาง"), + ("Run without install", "ใช้งานโดยไม่ต้องติดตั้ง"), + ("Always connected via relay", "เชื่อมต่อผ่านรีเลย์เสมอ"), + ("Always connect via relay", "เชื่อมต่อผ่านรีเลย์เสมอ"), + ("whitelist_tip", "อนุญาตเฉพาะการเชื่อมต่อจาก IP ที่ไวท์ลิสต์"), + ("Login", "เข้าสู่ระบบ"), + ("Logout", "ออกจากระบบ"), + ("Tags", "แท็ก"), + ("Search ID", "ค้นหา ID"), + ("Current Wayland display server is not supported", "เซิร์ฟเวอร์การแสดงผล Wayland ปัจจุบันไม่รองรับ"), + ("whitelist_sep", "คั่นโดยเครื่องหมาย comma semicolon เว้นวรรค หรือ ขึ้นบรรทัดใหม่"), + ("Add ID", "เพิ่ม ID"), + ("Add Tag", "เพิ่มแท็ก"), + ("Unselect all tags", "ยกเลิกการเลือกแท็กทั้งหมด"), + ("Network error", "ข้อผิดพลาดของเครือข่าย"), + ("Username missed", "ไม่พบข้อมูลผู้ใช้งาน"), + ("Password missed", "ไม่พบรหัสผ่าน"), + ("Wrong credentials", "ข้อมูลสำหรับเข้าสู่ระบบไม่ถูกต้อง"), + ("Edit Tag", "แก้ไขแท็ก"), + ("Unremember Password", "ยกเลิกการจดจำรหัสผ่าน"), + ("Favorites", "รายการโปรด"), + ("Add to Favorites", "เพิ่มไปยังรายการโปรด"), + ("Remove from Favorites", "ลบออกจากรายการโปรด"), + ("Empty", "ว่างเปล่า"), + ("Invalid folder name", "ชื่อโฟลเดอร์ไม่ถูกต้อง"), + ("Socks5 Proxy", "พรอกซี Socks5"), + ("Hostname", "ชื่อโฮสต์"), + ("Discovered", "ค้นพบ"), + ("install_daemon_tip", "หากต้องการใช้งานขณะระบบเริ่มต้น คุณจำเป็นจะต้องติดตั้งเซอร์วิส"), + ("Remote ID", "ID ปลายทาง"), + ("Paste", "วาง"), + ("Paste here?", "วางที่นี่หรือไม่?"), + ("Are you sure to close the connection?", "คุณแน่ใจหรือไม่ที่จะปิดการเชื่อมต่อ?"), + ("Download new version", "ดาวน์โหลดเวอร์ชั่นใหม่"), + ("Touch mode", "โหมดการสัมผัส"), + ("Mouse mode", "โหมดการใช้เมาส์"), + ("One-Finger Tap", "แตะนิ้วเดียว"), + ("Left Mouse", "เมาส์ซ้าย"), + ("One-Long Tap", "แตะยาวหนึ่งครั้ง"), + ("Two-Finger Tap", "แตะสองนิ้ว"), + ("Right Mouse", "เมาส์ขวา"), + ("One-Finger Move", "ลากนิ้วเดียว"), + ("Double Tap & Move", "แตะเบิ้ลและลาก"), + ("Mouse Drag", "ลากเมาส์"), + ("Three-Finger vertically", "สามนิ้วแนวตั้ง"), + ("Mouse Wheel", "ลูกลิ้งเมาส์"), + ("Two-Finger Move", "ลากสองนิ้ว"), + ("Canvas Move", "ลากแคนวาส"), + ("Pinch to Zoom", "ถ่างเพื่อขยาย"), + ("Canvas Zoom", "ขยายแคนวาส"), + ("Reset canvas", "รีเซ็ตแคนวาส"), + ("No permission of file transfer", "ไม่มีสิทธิ์ในการถ่ายโอนไฟล์"), + ("Note", "บันทึกข้อความ"), + ("Connection", "การเชื่อมต่อ"), + ("Share Screen", "แชร์หน้าจอ"), + ("CLOSE", "ปิด"), + ("OPEN", "เปิด"), + ("Chat", "แชท"), + ("Total", "รวม"), + ("items", "รายการ"), + ("Selected", "ถูกเลือก"), + ("Screen Capture", "แคปเจอร์หน้าจอ"), + ("Input Control", "ควบคุมอินพุท"), + ("Audio Capture", "แคปเจอร์เสียง"), + ("File Connection", "การเชื่อมต่อไฟล์"), + ("Screen Connection", "การเชื่อมต่อหน้าจอ"), + ("Do you accept?", "ยอมรับหรือไม่?"), + ("Open System Setting", "เปิดการตั้งค่าระบบ"), + ("How to get Android input permission?", "เปิดสิทธิ์การใช้งานอินพุทของแอนดรอยด์ได้อย่างไร?"), + ("android_input_permission_tip1", "ในการที่จะอนุญาตให้เครื่องปลายทางควบคุมอุปกรณ์แอนดรอยด์ของคุณโดยใช้เมาส์หรือการสัมผัส คุณจำเป็นจะต้องอนุญาตสิทธิ์ \"การเข้าถึง\" ให้แก่เซอร์วิสของ RustDesk"), + ("android_input_permission_tip2", "กรุณาไปยังหน้าตั้งค่าถัดไป ค้นหาและเข้าไปยัง [เซอร์วิสที่ถูกติดตั้ง] และเปิดการใช้งานเซอร์วิส [อินพุท RustDesk]"), + ("android_new_connection_tip", "ได้รับคำขอควบคุมใหม่ที่ต้องการควบคุมอุปกรณ์ของคุณ"), + ("android_service_will_start_tip", "การเปิดการใช้งาน \"การบันทึกหน้าจอ\" จะเป็นการเริ่มต้นการทำงานของเซอร์วิสโดยอัตโนมัติ ที่จะอนุญาตให้อุปกรณ์อื่นๆ ส่งคำขอเข้าถึงมายังอุปกรณ์ของคุณได้"), + ("android_stop_service_tip", "การปิดการใช้งานเซอร์วิสจะปิดการเชื่อมต่อทั้งหมดโดยอัตโนมัติ"), + ("android_version_audio_tip", "เวอร์ชั่นแอนดรอยด์ปัจจุบันของคุณไม่รองรับการบันทึกข้อมูลเสียง กรุณาอัปเกรดเป็นแอนดรอยด์เวอร์ชั่น 10 หรือสูงกว่า"), + ("android_start_service_tip", "แตะ [เริ่มต้นใช้งานเซอร์วิส] หรือเปิดสิทธิ์ [การบันทึกหน้าจอ] เพื่อเริ่มเซอร์วิสการแชร์หน้าจอ"), + ("Account", "บัญชี"), + ("Overwrite", "เขียนทับ"), + ("This file exists, skip or overwrite this file?", "พบไฟล์ที่มีอยู่แล้ว ต้องการเขียนทับหรือไม่?"), + ("Quit", "ออก"), + ("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"), + ("Help", "ช่วยเหลือ"), + ("Failed", "ล้มเหลว"), + ("Succeeded", "สำเร็จ"), + ("Someone turns on privacy mode, exit", "มีใครบางคนเปิดใช้งานโหมดความเป็นส่วนตัว กำลังออก"), + ("Unsupported", "ไม่รองรับ"), + ("Peer denied", "ถูกปฏิเสธโดยอีกฝั่ง"), + ("Please install plugins", "กรุณาติดตั้งปลั๊กอิน"), + ("Peer exit", "อีกฝั่งออก"), + ("Failed to turn off", "การปิดล้มเหลว"), + ("Turned off", "ปิด"), + ("In privacy mode", "อยู่ในโหมดความเป็นส่วนตัว"), + ("Out privacy mode", "อยู่นอกโหมดความเป็นส่วนตัว"), + ("Language", "ภาษา"), + ("Keep RustDesk background service", "คงสถานะการทำงานเบื้องหลังของเซอร์วิส RustDesk"), + ("Ignore Battery Optimizations", "เพิกเฉยการตั้งค่าการใช้งาน Battery Optimization"), + ("android_open_battery_optimizations_tip", "หากคุณต้องการปิดการใช้งานฟีเจอร์นี้ กรุณาไปยังหน้าตั้งค่าในแอปพลิเคชัน RustDesk ค้นหาหัวข้อ [Battery] และยกเลิกการเลือกรายการ [Unrestricted]"), + ("Connection not allowed", "การเชื่อมต่อไม่อนุญาต"), + ("Legacy mode", ""), + ("Map mode", ""), + ("Translate mode", ""), + ("Use permanent password", "ใช้รหัสผ่านถาวร"), + ("Use both passwords", "ใช้รหัสผ่านทั้งสองแบบ"), + ("Set permanent password", "ตั้งค่ารหัสผ่านถาวร"), + ("Enable Remote Restart", "เปิดการใช้งานการรีสตาร์ทระบบทางไกล"), + ("Allow remote restart", "อนุญาตการรีสตาร์ทระบบทางไกล"), + ("Restart Remote Device", "รีสตาร์ทอุปกรณ์ปลายทาง"), + ("Are you sure you want to restart", "คุณแน่ใจหรือไม่ที่จะรีสตาร์ท"), + ("Restarting Remote Device", "กำลังรีสตาร์ทระบบปลายทาง"), + ("remote_restarting_tip", "ระบบปลายทางกำลังรีสตาร์ท กรุณาปิดกล่องข้อความนี้และดำเนินการเขื่อมต่อใหม่อีกครั้งด้วยรหัสผ่านถาวรหลังจากผ่านไปซักครู่"), + ("Copied", "คัดลอกแล้ว"), + ("Exit Fullscreen", "ออกจากเต็มหน้าจอ"), + ("Fullscreen", "เต็มหน้าจอ"), + ("Mobile Actions", "การดำเนินการบนมือถือ"), + ("Select Monitor", "เลือกหน้าจอ"), + ("Control Actions", "การดำเนินการควบคุม"), + ("Display Settings", "การตั้งค่าแสดงผล"), + ("Ratio", "อัตราส่วน"), + ("Image Quality", "คุณภาพภาพ"), + ("Scroll Style", "ลักษณะการเลื่อน"), + ("Show Menubar", "แสดงแถบเมนู"), + ("Hide Menubar", "ซ่อนแถบเมนู"), + ("Direct Connection", "การเชื่อมต่อตรง"), + ("Relay Connection", "การเชื่อมต่อแบบรีเลย์"), + ("Secure Connection", "การเชื่อมต่อที่ปลอดภัย"), + ("Insecure Connection", "การเชื่อมต่อที่ไม่ปลอดภัย"), + ("Scale original", "ขนาดเดิม"), + ("Scale adaptive", "ขนาดยืดหยุ่น"), + ("General", "ทั่วไป"), + ("Security", "ความปลอดภัย"), + ("Account", "บัญชี"), + ("Theme", "ธีม"), + ("Dark Theme", "ธีมมืด"), + ("Dark", "มืด"), + ("Light", "สว่าง"), + ("Follow System", "ตามระบบ"), + ("Enable hardware codec", "เปิดการใช้งานฮาร์ดแวร์ codec"), + ("Unlock Security Settings", "ปลดล็อคการตั้งค่าความปลอดภัย"), + ("Enable Audio", "เปิดการใช้งานเสียง"), + ("Unlock Network Settings", "ปลดล็อคการตั้งค่าเครือข่าย"), + ("Server", "เซิร์ฟเวอร์"), + ("Direct IP Access", "การเข้าถึง IP ตรง"), + ("Proxy", "พรอกซี"), + ("Port", "พอร์ท"), + ("Apply", "นำไปใช้"), + ("Disconnect all devices?", "ยกเลิกการเชื่อมต่ออุปกรณ์ทั้งหมด?"), + ("Clear", "ล้างข้อมูล"), + ("Audio Input Device", "อุปกรณ์รับอินพุทข้อมูลเสียง"), + ("Deny remote access", "ปฏิเสธการเชื่อมต่อ"), + ("Use IP Whitelisting", "ใช้งาน IP ไวท์ลิสต์"), + ("Network", "เครือข่าย"), + ("Enable RDP", "เปิดการใช้งาน RDP"), + ("Pin menubar", "ปักหมุดแถบเมนู"), + ("Unpin menubar", "ยกเลิกการปักหมุดแถบเมนู"), + ("Recording", "การบันทึก"), + ("Directory", "ไดเรกทอรี่"), + ("Automatically record incoming sessions", "บันทึกเซสชันขาเข้าโดยอัตโนมัติ"), + ("Change", "เปลี่ยน"), + ("Start session recording", "เริ่มต้นการบันทึกเซสชัน"), + ("Stop session recording", "หยุดการบันทึกเซสซัน"), + ("Enable Recording Session", "เปิดใช้งานการบันทึกเซสชัน"), + ("Allow recording session", "อนุญาตการบันทึกเซสชัน"), + ("Enable LAN Discovery", "เปิดการใช้งานการค้นหาในวง LAN"), + ("Deny LAN Discovery", "ปฏิเสธการใช้งานการค้นหาในวง LAN"), + ("Write a message", "เขียนข้อความ"), + ("Prompt", ""), + ("Please wait for confirmation of UAC...", "กรุณารอการยืนยันจาก UAC..."), + ("elevated_foreground_window_tip", "หน้าต่างปัจจุบันของเครื่องปลายทางต้องการสิทธิ์การใช้งานที่สูงขึ้นสำหรับการทำงาน ดังนั้นเมาส์และคีย์บอร์ดจะไม่สามารถใช้งานได้ชั่วคราว คุณสามารถขอผู้ใช้งานปลายทางให้ย่อหน้าต่าง หรือคลิกปุ่มให้สิทธิ์การใช้งานในหน้าต่างการจัดการการเชื่อมต่อ เพื่อหลีกเลี่ยงปัญหานี้เราแนะนำให้ดำเนินการติดตั้งซอฟท์แวร์ในเครื่องปลายทาง"), + ("Disconnected", "ยกเลิกการเชื่อมต่อ"), + ("Other", "อื่นๆ"), + ("Confirm before closing multiple tabs", "ยืนยันการปิดหลายแท็บ"), + ("Keyboard Settings", "การตั้งค่าคีย์บอร์ด"), + ("Custom", "กำหนดเอง"), + ("Full Access", "การเข้าถึงทั้งหมด"), + ("Screen Share", "การแชร์จอ"), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland ต้องการ Ubuntu เวอร์ชั่น 21.04 หรือสูงกว่า"), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland ต้องการลินุกซ์เวอร์ชันที่สูงกว่านี้ กรุณาเปลี่ยนไปใช้เดสก์ท็อป X11 หรือเปลี่ยนระบบปฏิบัติการของคุณ"), + ("JumpLink", "View"), + ("Please Select the screen to be shared(Operate on the peer side).", "กรุณาเลือกหน้าจอที่ต้องการแชร์ (ใช้งานในอีกฝั่งของการเชื่อมต่อ)"), + ("Show RustDesk", "แสดง RustDesk"), + ("This PC", ""), + ("or", "หรือ"), + ("Continue with", "ทำต่อด้วย"), + ("Elevate", "ยกระดับ"), + ("Zoom cursor", "ขยายเคอร์เซอร์"), + ("Accept sessions via password", "ยอมรับการเชื่อมต่อด้วยรหัสผ่าน"), + ("Accept sessions via click", "ยอมรับการเชื่อมต่อด้วยการคลิก"), + ("Accept sessions via both", "ยอมรับการเชื่อมต่อด้วยทั้งสองวิธิ"), + ("Please wait for the remote side to accept your session request...", "กรุณารอให้อีกฝั่งยอมรับการเชื่อมต่อของคุณ..."), + ("One-time Password", "รหัสผ่านครั้งเดียว"), + ("Use one-time password", "ใช้รหัสผ่านครั้งเดียว"), + ("One-time password length", "ความยาวรหัสผ่านครั้งเดียว"), + ("Request access to your device", "คำขอการเข้าถึงอุปกรณ์ของคุณ"), + ("Hide connection management window", "ซ่อนหน้าต่างการจัดการการเชื่อมต่อ"), + ("hide_cm_tip", "อนุญาตการซ่อนก็ต่อเมื่อยอมรับการเชื่อมต่อด้วยรหัสผ่าน และต้องเป็นรหัสผ่านถาวรเท่านั้น"), + ("wayland_experiment_tip", "การสนับสนุน Wayland ยังอยู่ในขั้นตอนการทดลอง กรุณาใช้ X11 หากคุณต้องการใช้งานการเข้าถึงแบบไม่มีผู้ดูแล"), + ("Right click to select tabs", "คลิกขวาเพื่อเลือกแท็บ"), + ("Skipped", "ข้าม"), + ("Add to Address Book", "เพิ่มไปยังสมุดรายชื่อ"), + ("Group", "กลุ่ม"), + ("Search", "ค้นหา"), + ("Closed manually by the web console", "ถูกปิดโดยเว็บคอนโซล"), + ("Local keyboard type", "ประเภทคีย์บอร์ด"), + ("Select local keyboard type", "เลือกประเภทคีย์บอร์ด"), + ].iter().cloned().collect(); + } + \ No newline at end of file From c5205af73fa2f718d9ffabc5e317b698d8c4fdc7 Mon Sep 17 00:00:00 2001 From: Phongsathorn Date: Thu, 29 Dec 2022 02:05:10 +0700 Subject: [PATCH 1291/2015] lang: modify lang.rs to add Thai language --- src/lang.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/lang.rs b/src/lang.rs index bed0d7aa1..5ea408416 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -29,6 +29,7 @@ mod gr; mod sv; mod sq; mod sr; +mod th; lazy_static::lazy_static! { pub static ref LANGS: Value = @@ -61,6 +62,7 @@ lazy_static::lazy_static! { ("sv", "Svenska"), ("sq", "Shqip"), ("sr", "Srpski"), + ("th", "ภาษาไทย"), ]); } @@ -117,6 +119,7 @@ pub fn translate_locale(name: String, locale: &str) -> String { "sv" => sv::T.deref(), "sq" => sq::T.deref(), "sr" => sr::T.deref(), + "th" => th::T.deref(), _ => en::T.deref(), }; if let Some(v) = m.get(&name as &str) { From 7dc0c0757891b63464c1be21e59e155bda6e8afb Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 29 Dec 2022 10:59:17 +0800 Subject: [PATCH 1292/2015] make start_server available in cli --- src/client.rs | 4 +++- src/main.rs | 4 ++++ src/server.rs | 7 +++++++ src/server/video_service.rs | 2 -- 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/client.rs b/src/client.rs index 1c6f63f36..87660bfbc 100644 --- a/src/client.rs +++ b/src/client.rs @@ -47,13 +47,15 @@ pub use super::lang::*; pub mod file_trait; pub mod helper; pub mod io_loop; -use crate::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); pub const MILLI1: Duration = Duration::from_millis(1); pub const SEC30: Duration = Duration::from_secs(30); +const SCRAP_X11_REQUIRED: &str = "x11 expected"; +const SCRAP_X11_REF_URL: &str = "https://rustdesk.com/docs/en/manual/linux/#x11-required"; + /// Client of the remote desktop. pub struct Client; diff --git a/src/main.rs b/src/main.rs index 9c7170309..dfb64a7ca 100644 --- a/src/main.rs +++ b/src/main.rs @@ -71,6 +71,8 @@ fn main() { if options.len() > 3 { remote_host = options[3].clone(); } + common::test_rendezvous_server(); + common::test_nat_type(); let key = matches.value_of("key").unwrap_or("").to_owned(); let token = LocalConfig::get_option("access_token"); cli::start_one_port_forward( @@ -81,6 +83,8 @@ fn main() { key, token, ); + } else if let Some(p) = matches.value_of("server") { + crate::start_server(true); } common::global_clean(); } diff --git a/src/server.rs b/src/server.rs index bbe2d5f2a..4e57d9e46 100644 --- a/src/server.rs +++ b/src/server.rs @@ -26,6 +26,7 @@ use std::{ time::Duration, }; +#[cfg(not(feature = "cli"))] pub mod audio_service; cfg_if::cfg_if! { if #[cfg(not(any(target_os = "android", target_os = "ios")))] { @@ -53,8 +54,14 @@ mod connection; pub mod portable_service; mod service; mod video_qos; +#[cfg(not(feature = "cli"))] pub mod video_service; +#[cfg(feature = "cli")] +mod stub; +#[cfg(feature = "cli")] +pub use stub::*; + use hbb_common::tcp::new_listener; pub type Childs = Arc>>; diff --git a/src/server/video_service.rs b/src/server/video_service.rs index 6d1235ed8..4750ec05f 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -51,8 +51,6 @@ use virtual_display; pub const SCRAP_UBUNTU_HIGHER_REQUIRED: &str = "Wayland requires Ubuntu 21.04 or higher version."; pub const SCRAP_OTHER_VERSION_OR_X11_REQUIRED: &str = "Wayland requires higher version of linux distro. Please try X11 desktop or change your OS."; -pub const SCRAP_X11_REQUIRED: &str = "x11 expected"; -pub const SCRAP_X11_REF_URL: &str = "https://rustdesk.com/docs/en/manual/linux/#x11-required"; pub const NAME: &'static str = "video"; From 6bf17104775636d1bbd9a03fc5fb10170df586df Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 29 Dec 2022 11:18:03 +0800 Subject: [PATCH 1293/2015] add --connect to cli --- src/cli.rs | 23 +++++++++++++++++++++-- src/main.rs | 7 +++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index e16c20f74..b4f552d5f 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -4,9 +4,9 @@ use hbb_common::{ log, message_proto::*, protobuf::Message as _, + rendezvous_proto::ConnType, tokio::{self, sync::mpsc}, Stream, - rendezvous_proto::ConnType, }; use std::sync::{Arc, RwLock}; @@ -44,7 +44,7 @@ impl Interface for Session { fn get_login_config_handler(&self) -> Arc> { return self.lc.clone(); } - + fn msgbox(&self, msgtype: &str, title: &str, text: &str, link: &str) { if msgtype == "input-password" { self.sender @@ -86,6 +86,25 @@ impl Interface for Session { } } +#[tokio::main(flavor = "current_thread")] +pub async fn connect_test( + id: &str, + key: String, + token: String, +) { + let (sender, mut receiver) = mpsc::unbounded_channel::(); + let handler = Session::new(&id, sender); + if let Err(err) = crate::client::Client::start( + id, + &key, + &token, + ConnType::PORT_FORWARD, + handler, + ).await { + log::error!("Failed to connect {}: {}", &id, err); + } +} + #[tokio::main(flavor = "current_thread")] pub async fn start_one_port_forward( id: String, diff --git a/src/main.rs b/src/main.rs index dfb64a7ca..ca0bc2234 100644 --- a/src/main.rs +++ b/src/main.rs @@ -36,6 +36,7 @@ fn main() { use hbb_common::log; let args = format!( "-p, --port-forward=[PORT-FORWARD-OPTIONS] 'Format: remote-id:local-port:remote-port[:remote-host]' + -c, --connect=[REMOTE_ID] 'test only' -k, --key=[KEY] '' -s, --server... 'Start server'", ); @@ -83,6 +84,12 @@ fn main() { key, token, ); + } else if let Some(p) = matches.value_of("connect") { + common::test_rendezvous_server(); + common::test_nat_type(); + let key = matches.value_of("key").unwrap_or("").to_owned(); + let token = LocalConfig::get_option("access_token"); + cli::connect_test(p, key, token); } else if let Some(p) = matches.value_of("server") { crate::start_server(true); } From 4845e7dbeb412b360412de6ee2e0368d7968b653 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 29 Dec 2022 11:27:53 +0800 Subject: [PATCH 1294/2015] fix cli --- src/client.rs | 4 +--- src/server/video_service.rs | 2 ++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client.rs b/src/client.rs index 87660bfbc..9c27d98ff 100644 --- a/src/client.rs +++ b/src/client.rs @@ -47,15 +47,13 @@ pub use super::lang::*; pub mod file_trait; pub mod helper; pub mod io_loop; +use crate::video_service::{SCRAP_X11_REQUIRED, SCRAP_X11_REF_URL}; 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); -const SCRAP_X11_REQUIRED: &str = "x11 expected"; -const SCRAP_X11_REF_URL: &str = "https://rustdesk.com/docs/en/manual/linux/#x11-required"; - /// Client of the remote desktop. pub struct Client; diff --git a/src/server/video_service.rs b/src/server/video_service.rs index 4750ec05f..6d1235ed8 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -51,6 +51,8 @@ use virtual_display; pub const SCRAP_UBUNTU_HIGHER_REQUIRED: &str = "Wayland requires Ubuntu 21.04 or higher version."; pub const SCRAP_OTHER_VERSION_OR_X11_REQUIRED: &str = "Wayland requires higher version of linux distro. Please try X11 desktop or change your OS."; +pub const SCRAP_X11_REQUIRED: &str = "x11 expected"; +pub const SCRAP_X11_REF_URL: &str = "https://rustdesk.com/docs/en/manual/linux/#x11-required"; pub const NAME: &'static str = "video"; From 250fb314ceca83b35f029019a955bb0afe7b0896 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 29 Dec 2022 11:44:06 +0800 Subject: [PATCH 1295/2015] stub for cli --- src/server/stub.rs | 48 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 src/server/stub.rs diff --git a/src/server/stub.rs b/src/server/stub.rs new file mode 100644 index 000000000..ba2967222 --- /dev/null +++ b/src/server/stub.rs @@ -0,0 +1,48 @@ +use super::*; + +pub mod audio_service { + use super::*; + pub const NAME: &'static str = "audio"; + pub fn new() -> GenericService { + let sp = GenericService::new(NAME, true); + sp + } + pub fn restart() {} +} + +pub mod video_service { + use super::*; + pub const NAME: &'static str = "video"; + pub fn new() -> GenericService { + let sp = GenericService::new(NAME, true); + sp + } + pub fn is_privacy_mode_supported() -> bool { + false + } + pub fn test_create_capturer(privacy_mode_id: i32, timeout_millis: u64) -> bool { + false + } + pub fn refresh() {} + pub async fn switch_display(i: i32) {} + pub async fn get_displays() -> ResultType<(usize, Vec)> { + bail!("No displayes"); + } + pub fn is_inited_msg() -> Option { + None + } + pub fn capture_cursor_embeded() -> bool { + false + } + pub async fn switch_to_primary() {} + pub fn set_privacy_mode_conn_id(_: i32) {} + pub fn get_privacy_mode_conn_id() -> i32 { + 0 + } + pub fn notify_video_frame_feched(_: i32, _: Option) {} + lazy_static::lazy_static! { + pub static ref VIDEO_QOS: Arc> = Default::default(); + } + pub const SCRAP_X11_REQUIRED: &str = ""; + pub const SCRAP_X11_REF_URL: &str = ""; +} From 8c7bc08776330567360e10db40559659c3c43ddf Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 29 Dec 2022 12:47:02 +0800 Subject: [PATCH 1296/2015] remove video/audio decoder in client for cli --- src/client.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/client.rs b/src/client.rs index 9c27d98ff..37c5009a0 100644 --- a/src/client.rs +++ b/src/client.rs @@ -4,6 +4,7 @@ use cpal::{ traits::{DeviceTrait, HostTrait, StreamTrait}, Device, Host, StreamConfig, }; +#[cfg(not(features = "cli"))] use magnum_opus::{Channels::*, Decoder as AudioDecoder}; use sha2::{Digest, Sha256}; use std::{ @@ -36,6 +37,7 @@ use hbb_common::{ }; pub use helper::LatencyController; pub use helper::*; +#[cfg(not(features = "cli"))] use scrap::{ codec::{Decoder, DecoderCfg}, record::{Recorder, RecorderContext}, @@ -585,6 +587,7 @@ impl Client { } /// Audio handler for the [`Client`]. +#[cfg(not(features = "cli"))] #[derive(Default)] pub struct AudioHandler { audio_decoder: Option<(AudioDecoder, Vec)>, @@ -601,6 +604,7 @@ pub struct AudioHandler { latency_controller: Arc>, } +#[cfg(not(features = "cli"))] impl AudioHandler { /// Create a new audio handler. pub fn new(latency_controller: Arc>) -> Self { @@ -796,6 +800,7 @@ impl AudioHandler { } /// Video handler for the [`Client`]. +#[cfg(not(features = "cli"))] pub struct VideoHandler { decoder: Decoder, latency_controller: Arc>, @@ -804,6 +809,7 @@ pub struct VideoHandler { record: bool, } +#[cfg(not(features = "cli"))] impl VideoHandler { /// Create a new video handler. pub fn new(latency_controller: Arc>) -> Self { @@ -1524,6 +1530,7 @@ where let latency_controller = LatencyController::new(); let latency_controller_cl = latency_controller.clone(); + #[cfg(not(features = "cli"))] std::thread::spawn(move || { let mut video_handler = VideoHandler::new(latency_controller); loop { @@ -1548,6 +1555,7 @@ where } log::info!("Video decoder loop exits"); }); + #[cfg(not(features = "cli"))] std::thread::spawn(move || { let mut audio_handler = AudioHandler::new(latency_controller_cl); loop { From 45072a4de1e029b535c36b1e5f1cbce0d1652221 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 29 Dec 2022 13:00:57 +0800 Subject: [PATCH 1297/2015] more for cli --- src/common.rs | 1 + src/server.rs | 2 ++ src/server/connection.rs | 5 +++++ 3 files changed, 8 insertions(+) diff --git a/src/common.rs b/src/common.rs index f83fbc69d..3aa7e3815 100644 --- a/src/common.rs +++ b/src/common.rs @@ -50,6 +50,7 @@ lazy_static::lazy_static! { } pub fn global_init() -> bool { + #[cfg(not(feature = "cli"))] #[cfg(target_os = "linux")] { if !scrap::is_x11() { diff --git a/src/server.rs b/src/server.rs index 4e57d9e46..b1b252446 100644 --- a/src/server.rs +++ b/src/server.rs @@ -31,6 +31,7 @@ pub mod audio_service; cfg_if::cfg_if! { if #[cfg(not(any(target_os = "android", target_os = "ios")))] { mod clipboard_service; +#[cfg(not(feature = "cli"))] #[cfg(target_os = "linux")] pub(crate) mod wayland; #[cfg(target_os = "linux")] @@ -318,6 +319,7 @@ impl Drop for Server { for s in self.services.values() { s.join(); } + #[cfg(not(feature = "cli"))] #[cfg(target_os = "linux")] wayland::clear(); } diff --git a/src/server/connection.rs b/src/server/connection.rs index e1a360e7a..32b1a04e0 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -500,6 +500,7 @@ impl Connection { let _ = privacy_mode::turn_off_privacy(0); } video_service::notify_video_frame_feched(id, None); + #[cfg(not(feature = "cli"))] scrap::codec::Encoder::update_video_encoder(id, scrap::codec::EncoderUpdate::Remove); video_service::VIDEO_QOS.lock().unwrap().reset(); if conn.authorized { @@ -1076,17 +1077,20 @@ impl Connection { if let Some(o) = lr.option.as_ref() { self.update_option(o).await; if let Some(q) = o.video_codec_state.clone().take() { + #[cfg(not(feature = "cli"))] scrap::codec::Encoder::update_video_encoder( self.inner.id(), scrap::codec::EncoderUpdate::State(q), ); } else { + #[cfg(not(feature = "cli"))] scrap::codec::Encoder::update_video_encoder( self.inner.id(), scrap::codec::EncoderUpdate::DisableHwIfNotExist, ); } } else { + #[cfg(not(feature = "cli"))] scrap::codec::Encoder::update_video_encoder( self.inner.id(), scrap::codec::EncoderUpdate::DisableHwIfNotExist, @@ -1645,6 +1649,7 @@ impl Connection { } } if let Some(q) = o.video_codec_state.clone().take() { + #[cfg(not(feature = "cli"))] scrap::codec::Encoder::update_video_encoder( self.inner.id(), scrap::codec::EncoderUpdate::State(q), From 40aaddf1082e2e10a7529b9998df979576aa638f Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 29 Dec 2022 13:29:22 +0800 Subject: [PATCH 1298/2015] remove stub.rs, it is really hard to remove scrap totally, which is used all where --- src/client.rs | 10 +-------- src/common.rs | 1 - src/main.rs | 11 --------- src/server.rs | 9 -------- src/server/connection.rs | 5 ----- src/server/stub.rs | 48 ---------------------------------------- 6 files changed, 1 insertion(+), 83 deletions(-) delete mode 100644 src/server/stub.rs diff --git a/src/client.rs b/src/client.rs index 37c5009a0..1c6f63f36 100644 --- a/src/client.rs +++ b/src/client.rs @@ -4,7 +4,6 @@ use cpal::{ traits::{DeviceTrait, HostTrait, StreamTrait}, Device, Host, StreamConfig, }; -#[cfg(not(features = "cli"))] use magnum_opus::{Channels::*, Decoder as AudioDecoder}; use sha2::{Digest, Sha256}; use std::{ @@ -37,7 +36,6 @@ use hbb_common::{ }; pub use helper::LatencyController; pub use helper::*; -#[cfg(not(features = "cli"))] use scrap::{ codec::{Decoder, DecoderCfg}, record::{Recorder, RecorderContext}, @@ -49,7 +47,7 @@ pub use super::lang::*; pub mod file_trait; pub mod helper; pub mod io_loop; -use crate::video_service::{SCRAP_X11_REQUIRED, SCRAP_X11_REF_URL}; +use crate::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); @@ -587,7 +585,6 @@ impl Client { } /// Audio handler for the [`Client`]. -#[cfg(not(features = "cli"))] #[derive(Default)] pub struct AudioHandler { audio_decoder: Option<(AudioDecoder, Vec)>, @@ -604,7 +601,6 @@ pub struct AudioHandler { latency_controller: Arc>, } -#[cfg(not(features = "cli"))] impl AudioHandler { /// Create a new audio handler. pub fn new(latency_controller: Arc>) -> Self { @@ -800,7 +796,6 @@ impl AudioHandler { } /// Video handler for the [`Client`]. -#[cfg(not(features = "cli"))] pub struct VideoHandler { decoder: Decoder, latency_controller: Arc>, @@ -809,7 +804,6 @@ pub struct VideoHandler { record: bool, } -#[cfg(not(features = "cli"))] impl VideoHandler { /// Create a new video handler. pub fn new(latency_controller: Arc>) -> Self { @@ -1530,7 +1524,6 @@ where let latency_controller = LatencyController::new(); let latency_controller_cl = latency_controller.clone(); - #[cfg(not(features = "cli"))] std::thread::spawn(move || { let mut video_handler = VideoHandler::new(latency_controller); loop { @@ -1555,7 +1548,6 @@ where } log::info!("Video decoder loop exits"); }); - #[cfg(not(features = "cli"))] std::thread::spawn(move || { let mut audio_handler = AudioHandler::new(latency_controller_cl); loop { diff --git a/src/common.rs b/src/common.rs index 3aa7e3815..f83fbc69d 100644 --- a/src/common.rs +++ b/src/common.rs @@ -50,7 +50,6 @@ lazy_static::lazy_static! { } pub fn global_init() -> bool { - #[cfg(not(feature = "cli"))] #[cfg(target_os = "linux")] { if !scrap::is_x11() { diff --git a/src/main.rs b/src/main.rs index ca0bc2234..9c7170309 100644 --- a/src/main.rs +++ b/src/main.rs @@ -36,7 +36,6 @@ fn main() { use hbb_common::log; let args = format!( "-p, --port-forward=[PORT-FORWARD-OPTIONS] 'Format: remote-id:local-port:remote-port[:remote-host]' - -c, --connect=[REMOTE_ID] 'test only' -k, --key=[KEY] '' -s, --server... 'Start server'", ); @@ -72,8 +71,6 @@ fn main() { if options.len() > 3 { remote_host = options[3].clone(); } - common::test_rendezvous_server(); - common::test_nat_type(); let key = matches.value_of("key").unwrap_or("").to_owned(); let token = LocalConfig::get_option("access_token"); cli::start_one_port_forward( @@ -84,14 +81,6 @@ fn main() { key, token, ); - } else if let Some(p) = matches.value_of("connect") { - common::test_rendezvous_server(); - common::test_nat_type(); - let key = matches.value_of("key").unwrap_or("").to_owned(); - let token = LocalConfig::get_option("access_token"); - cli::connect_test(p, key, token); - } else if let Some(p) = matches.value_of("server") { - crate::start_server(true); } common::global_clean(); } diff --git a/src/server.rs b/src/server.rs index b1b252446..bbe2d5f2a 100644 --- a/src/server.rs +++ b/src/server.rs @@ -26,12 +26,10 @@ use std::{ time::Duration, }; -#[cfg(not(feature = "cli"))] pub mod audio_service; cfg_if::cfg_if! { if #[cfg(not(any(target_os = "android", target_os = "ios")))] { mod clipboard_service; -#[cfg(not(feature = "cli"))] #[cfg(target_os = "linux")] pub(crate) mod wayland; #[cfg(target_os = "linux")] @@ -55,14 +53,8 @@ mod connection; pub mod portable_service; mod service; mod video_qos; -#[cfg(not(feature = "cli"))] pub mod video_service; -#[cfg(feature = "cli")] -mod stub; -#[cfg(feature = "cli")] -pub use stub::*; - use hbb_common::tcp::new_listener; pub type Childs = Arc>>; @@ -319,7 +311,6 @@ impl Drop for Server { for s in self.services.values() { s.join(); } - #[cfg(not(feature = "cli"))] #[cfg(target_os = "linux")] wayland::clear(); } diff --git a/src/server/connection.rs b/src/server/connection.rs index 32b1a04e0..e1a360e7a 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -500,7 +500,6 @@ impl Connection { let _ = privacy_mode::turn_off_privacy(0); } video_service::notify_video_frame_feched(id, None); - #[cfg(not(feature = "cli"))] scrap::codec::Encoder::update_video_encoder(id, scrap::codec::EncoderUpdate::Remove); video_service::VIDEO_QOS.lock().unwrap().reset(); if conn.authorized { @@ -1077,20 +1076,17 @@ impl Connection { if let Some(o) = lr.option.as_ref() { self.update_option(o).await; if let Some(q) = o.video_codec_state.clone().take() { - #[cfg(not(feature = "cli"))] scrap::codec::Encoder::update_video_encoder( self.inner.id(), scrap::codec::EncoderUpdate::State(q), ); } else { - #[cfg(not(feature = "cli"))] scrap::codec::Encoder::update_video_encoder( self.inner.id(), scrap::codec::EncoderUpdate::DisableHwIfNotExist, ); } } else { - #[cfg(not(feature = "cli"))] scrap::codec::Encoder::update_video_encoder( self.inner.id(), scrap::codec::EncoderUpdate::DisableHwIfNotExist, @@ -1649,7 +1645,6 @@ impl Connection { } } if let Some(q) = o.video_codec_state.clone().take() { - #[cfg(not(feature = "cli"))] scrap::codec::Encoder::update_video_encoder( self.inner.id(), scrap::codec::EncoderUpdate::State(q), diff --git a/src/server/stub.rs b/src/server/stub.rs deleted file mode 100644 index ba2967222..000000000 --- a/src/server/stub.rs +++ /dev/null @@ -1,48 +0,0 @@ -use super::*; - -pub mod audio_service { - use super::*; - pub const NAME: &'static str = "audio"; - pub fn new() -> GenericService { - let sp = GenericService::new(NAME, true); - sp - } - pub fn restart() {} -} - -pub mod video_service { - use super::*; - pub const NAME: &'static str = "video"; - pub fn new() -> GenericService { - let sp = GenericService::new(NAME, true); - sp - } - pub fn is_privacy_mode_supported() -> bool { - false - } - pub fn test_create_capturer(privacy_mode_id: i32, timeout_millis: u64) -> bool { - false - } - pub fn refresh() {} - pub async fn switch_display(i: i32) {} - pub async fn get_displays() -> ResultType<(usize, Vec)> { - bail!("No displayes"); - } - pub fn is_inited_msg() -> Option { - None - } - pub fn capture_cursor_embeded() -> bool { - false - } - pub async fn switch_to_primary() {} - pub fn set_privacy_mode_conn_id(_: i32) {} - pub fn get_privacy_mode_conn_id() -> i32 { - 0 - } - pub fn notify_video_frame_feched(_: i32, _: Option) {} - lazy_static::lazy_static! { - pub static ref VIDEO_QOS: Arc> = Default::default(); - } - pub const SCRAP_X11_REQUIRED: &str = ""; - pub const SCRAP_X11_REF_URL: &str = ""; -} From 1651cef4f3cb25ecfdd80cb2a7693b4a8868c11a Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 29 Dec 2022 14:17:47 +0800 Subject: [PATCH 1299/2015] recover --server and --connect in cli --- src/ipc.rs | 8 ++++---- src/main.rs | 11 +++++++++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/ipc.rs b/src/ipc.rs index 34711a900..c562225b4 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -111,7 +111,7 @@ pub enum DataKeyboardResponse { GetKeyState(bool), } -#[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] +#[cfg(not(any(target_os = "android", target_os = "ios")))] #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(tag = "t", content = "c")] pub enum DataMouse { @@ -195,11 +195,11 @@ pub enum Data { ClipboardFileEnabled(bool), PrivacyModeState((i32, PrivacyModeState)), TestRendezvousServer, - #[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] + #[cfg(not(any(target_os = "android", target_os = "ios")))] Keyboard(DataKeyboard), - #[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] + #[cfg(not(any(target_os = "android", target_os = "ios")))] KeyboardResponse(DataKeyboardResponse), - #[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] + #[cfg(not(any(target_os = "android", target_os = "ios")))] Mouse(DataMouse), Control(DataControl), Theme(String), diff --git a/src/main.rs b/src/main.rs index 9c7170309..ca0bc2234 100644 --- a/src/main.rs +++ b/src/main.rs @@ -36,6 +36,7 @@ fn main() { use hbb_common::log; let args = format!( "-p, --port-forward=[PORT-FORWARD-OPTIONS] 'Format: remote-id:local-port:remote-port[:remote-host]' + -c, --connect=[REMOTE_ID] 'test only' -k, --key=[KEY] '' -s, --server... 'Start server'", ); @@ -71,6 +72,8 @@ fn main() { if options.len() > 3 { remote_host = options[3].clone(); } + common::test_rendezvous_server(); + common::test_nat_type(); let key = matches.value_of("key").unwrap_or("").to_owned(); let token = LocalConfig::get_option("access_token"); cli::start_one_port_forward( @@ -81,6 +84,14 @@ fn main() { key, token, ); + } else if let Some(p) = matches.value_of("connect") { + common::test_rendezvous_server(); + common::test_nat_type(); + let key = matches.value_of("key").unwrap_or("").to_owned(); + let token = LocalConfig::get_option("access_token"); + cli::connect_test(p, key, token); + } else if let Some(p) = matches.value_of("server") { + crate::start_server(true); } common::global_clean(); } From 01ade733049c456c0603476c0afb8b8db657d713 Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 29 Dec 2022 14:28:15 +0800 Subject: [PATCH 1300/2015] fix macos sticky fn, https://stackoverflow.com/questions/74938870/sticky-fn-after-home-is-simulated-programmatically-macos Signed-off-by: fufesou --- Cargo.lock | 2 +- libs/enigo/src/macos/macos_impl.rs | 1 + src/server/input_service.rs | 13 ++++++++----- src/tray.rs | 12 +++++++----- src/ui_interface.rs | 3 +++ 5 files changed, 20 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ebf82edac..edb74b010 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4305,7 +4305,7 @@ dependencies = [ [[package]] name = "rdev" version = "0.5.0-2" -source = "git+https://github.com/fufesou/rdev#bc2db2f13dfdc95df8a02eb03f71325c173283dc" +source = "git+https://github.com/fufesou/rdev#edddb71a88bd8a4737ef4216861b426490c49f2e" dependencies = [ "cocoa", "core-foundation 0.9.3", diff --git a/libs/enigo/src/macos/macos_impl.rs b/libs/enigo/src/macos/macos_impl.rs index 1ae41f0c3..68457a4a2 100644 --- a/libs/enigo/src/macos/macos_impl.rs +++ b/libs/enigo/src/macos/macos_impl.rs @@ -40,6 +40,7 @@ const BUF_LEN: usize = 4; #[allow(improper_ctypes)] #[allow(non_snake_case)] #[link(name = "ApplicationServices", kind = "framework")] +#[link(name = "Carbon", kind = "framework")] extern "C" { fn CFDataGetBytePtr(theData: CFDataRef) -> *const u8; fn TISCopyCurrentKeyboardInputSource() -> TISInputSourceRef; diff --git a/src/server/input_service.rs b/src/server/input_service.rs index a94a7a2ef..755238c27 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -288,6 +288,7 @@ fn modifier_sleep() { } #[inline] +#[cfg(not(target_os = "macos"))] fn is_pressed(key: &Key, en: &mut Enigo) -> bool { get_modifier_state(key.clone(), en) } @@ -780,13 +781,13 @@ fn click_capslock(en: &mut Enigo) { #[cfg(not(targe_os = "macos"))] en.key_click(enigo::Key::CapsLock); #[cfg(target_os = "macos")] - en.key_down(enigo::Key::CapsLock); + let _ = en.key_down(enigo::Key::CapsLock); } -fn click_numlock(en: &mut Enigo) { +fn click_numlock(_en: &mut Enigo) { // without numlock in macos #[cfg(not(target_os = "macos"))] - en.key_click(enigo::Key::NumLock); + _en.key_click(enigo::Key::NumLock); } fn sync_numlock_capslock_status(key_event: &KeyEvent) { @@ -872,6 +873,7 @@ fn is_altgr_pressed() -> bool { .is_some() } +#[cfg(not(target_os = "macos"))] fn press_modifiers(en: &mut Enigo, key_event: &KeyEvent, to_release: &mut Vec) { for ref ck in key_event.modifiers.iter() { if let Some(key) = control_key_value_to_key(ck.value()) { @@ -889,14 +891,14 @@ fn press_modifiers(en: &mut Enigo, key_event: &KeyEvent, to_release: &mut Vec) { +fn sync_modifiers(en: &mut Enigo, key_event: &KeyEvent, _to_release: &mut Vec) { #[cfg(target_os = "macos")] add_flags_to_enigo(en, key_event); if key_event.down { release_unpressed_modifiers(en, key_event); #[cfg(not(target_os = "macos"))] - press_modifiers(en, key_event, to_release); + press_modifiers(en, key_event, _to_release); } } @@ -944,6 +946,7 @@ fn process_seq(en: &mut Enigo, sequence: &str) { en.key_sequence(&sequence); } +#[cfg(not(target_os = "macos"))] fn release_keys(en: &mut Enigo, to_release: &Vec) { for key in to_release { en.key_up(key.clone()); diff --git a/src/tray.rs b/src/tray.rs index 1afd988ae..98a4127a3 100644 --- a/src/tray.rs +++ b/src/tray.rs @@ -1,3 +1,4 @@ +#[cfg(any(target_os = "linux", target_os = "windows"))] use super::ui_interface::get_option_opt; #[cfg(target_os = "linux")] use hbb_common::log::{debug, error, info}; @@ -44,7 +45,7 @@ pub fn start_tray() { } else { *control_flow = ControlFlow::Wait; } - let stopped = is_service_stoped(); + let stopped = is_service_stopped(); let state = if stopped { 2 } else { 1 }; let old = *old_state.lock().unwrap(); if state != old { @@ -101,7 +102,7 @@ pub fn start_tray() { } if let Some(mut appindicator) = get_default_app_indicator() { let mut menu = gtk::Menu::new(); - let stoped = is_service_stoped(); + let stoped = is_service_stopped(); // start/stop service let label = if stoped { crate::client::translate("Start Service".to_owned()) @@ -137,7 +138,7 @@ pub fn start_tray() { #[cfg(target_os = "linux")] fn change_service_state() { - if is_service_stoped() { + if is_service_stopped() { debug!("Now try to start service"); crate::ipc::set_option("stop-service", ""); } else { @@ -151,7 +152,7 @@ fn change_service_state() { fn update_tray_service_item(item: >k::MenuItem) { use gtk::traits::GtkMenuItemExt; - if is_service_stoped() { + 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())); @@ -194,7 +195,8 @@ fn get_default_app_indicator() -> Option { /// Check if service is stoped. /// Return [`true`] if service is stoped, [`false`] otherwise. #[inline] -fn is_service_stoped() -> bool { +#[cfg(any(target_os = "linux", target_os = "windows"))] +fn is_service_stopped() -> bool { if let Some(v) = get_option_opt("stop-service") { v == "Y" } else { diff --git a/src/ui_interface.rs b/src/ui_interface.rs index ffcf110b3..2e4ca4ea3 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -158,6 +158,7 @@ pub fn get_license() -> String { } #[inline] +#[cfg(any(target_os = "linux", target_os = "windows"))] pub fn get_option_opt(key: &str) -> Option { OPTIONS.lock().unwrap().get(key).map(|x| x.clone()) } @@ -199,11 +200,13 @@ pub fn set_local_flutter_config(key: String, value: String) { LocalConfig::set_flutter_config(key, value); } +#[cfg(feature = "flutter")] #[inline] pub fn get_kb_layout_type() -> String { LocalConfig::get_kb_layout_type() } +#[cfg(feature = "flutter")] #[inline] pub fn set_kb_layout_type(kb_layout_type: String) { LocalConfig::set_kb_layout_type(kb_layout_type); From 94cecb186059e27706501c52b0ee2c24e601f3b3 Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 29 Dec 2022 17:11:10 +0800 Subject: [PATCH 1301/2015] macos, use private CGEventSource Signed-off-by: fufesou --- Cargo.lock | 2 +- src/server/connection.rs | 2 ++ src/server/input_service.rs | 66 +++++++++++++++++++++++++++++-------- 3 files changed, 56 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index edb74b010..b0b29503f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4305,7 +4305,7 @@ dependencies = [ [[package]] name = "rdev" version = "0.5.0-2" -source = "git+https://github.com/fufesou/rdev#edddb71a88bd8a4737ef4216861b426490c49f2e" +source = "git+https://github.com/fufesou/rdev#196b589573f90703a601e6b105dd7c917fc388f9" dependencies = [ "cocoa", "core-foundation 0.9.3", diff --git a/src/server/connection.rs b/src/server/connection.rs index e1a360e7a..b569ef708 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -523,6 +523,8 @@ impl Connection { rdev::set_dw_mouse_extra_info(enigo::ENIGO_INPUT_EXTRA_VALUE); rdev::set_dw_keyboard_extra_info(enigo::ENIGO_INPUT_EXTRA_VALUE); } + #[cfg(target_os = "macos")] + reset_input_ondisconn(); loop { match receiver.recv_timeout(std::time::Duration::from_millis(500)) { Ok(v) => match v { diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 755238c27..709513867 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -5,7 +5,9 @@ use crate::common::IS_X11; use dispatch::Queue; use enigo::{Enigo, Key, KeyboardControllable, MouseButton, MouseControllable}; use hbb_common::{config::COMPRESS_LEVEL, get_time, protobuf::EnumOrUnknown}; -use rdev::{self, simulate, EventType, Key as RdevKey, RawKey}; +use rdev::{self, EventType, Key as RdevKey, RawKey}; +#[cfg(target_os = "macos")] +use rdev::{CGEventSourceStateID, CGEventTapLocation, VirtualInput}; use std::time::Duration; use std::{ convert::TryFrom, @@ -221,6 +223,10 @@ lazy_static::lazy_static! { static ref IS_SERVER: bool = std::env::args().nth(1) == Some("--server".to_owned()); } +// virtual_input must be used in main thread. +// No need to wrap mutex. +static mut VIRTUAL_INPUT: Option = None; + // First call set_uinput() will create keyboard and mouse clients. // The clients are ipc connections that must live shorter than tokio runtime. // Thus this function must not be called in a temporary runtime. @@ -392,13 +398,15 @@ fn record_key_to_key(record_key: u64) -> Option { fn release_record_key(record_key: u64) { let func = move || { if record_key_is_rdev_layout(record_key) { - rdev_key_down_or_up(RdevKey::Unknown((record_key - KEY_RDEV_START) as _), false); + 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); } }; - + #[cfg(target_os = "macos")] QUEUE.exec_async(func); #[cfg(not(target_os = "macos"))] @@ -687,7 +695,27 @@ pub fn handle_key(evt: &KeyEvent) { handle_key_(evt); } -fn sim_rdev_rawkey(code: u32, down_or_up: bool) { +#[inline] +fn reset_input() { + unsafe { + VIRTUAL_INPUT = VirtualInput::new( + CGEventSourceStateID::Private, + CGEventTapLocation::AnnotatedSession, + ) + .ok(); + } +} + +#[cfg(target_os = "macos")] +pub fn reset_input_ondisconn() { + if !*IS_SERVER { + QUEUE.exec_async(reset_input); + } else { + reset_input(); + } +} + +fn sim_rdev_rawkey(code: u32, keydown: bool) { #[cfg(target_os = "windows")] let rawkey = RawKey::ScanCode(code); #[cfg(target_os = "linux")] @@ -698,22 +726,34 @@ fn sim_rdev_rawkey(code: u32, down_or_up: bool) { #[cfg(target_os = "macos")] let rawkey = RawKey::MacVirtualKeycode(code); - rdev_key_down_or_up(RdevKey::RawKey(rawkey), down_or_up); + let event_type = if keydown { + EventType::KeyPress(RdevKey::RawKey(rawkey)) + } else { + EventType::KeyRelease(RdevKey::RawKey(rawkey)) + }; + simulate_(&event_type); } -fn rdev_key_down_or_up(key: RdevKey, down_or_up: bool) { - let event_type = match down_or_up { - true => EventType::KeyPress(key), - false => EventType::KeyRelease(key), - }; - match simulate(&event_type) { +#[cfg(target_os = "macos")] +#[inline] +fn simulate_(event_type: &EventType) { + unsafe { + if let Some(virtual_input) = &VIRTUAL_INPUT { + let _ = virtual_input.simulate(&event_type); + std::thread::sleep(Duration::from_millis(20)); + } + } +} + +#[cfg(not(target_os = "macos"))] +#[inline] +fn simulate_(event_type: &EventType) { + match rdev::simulate(&event_type) { Ok(()) => (), Err(_simulate_error) => { log::error!("Could not send {:?}", &event_type); } } - #[cfg(target_os = "macos")] - std::thread::sleep(Duration::from_millis(20)); } fn is_modifier_in_key_event(control_key: ControlKey, key_event: &KeyEvent) -> bool { From bf63d397e026b5ff5b8c593a60340289a16f9f6b Mon Sep 17 00:00:00 2001 From: Kingtous Date: Thu, 29 Dec 2022 17:51:00 +0800 Subject: [PATCH 1302/2015] opt: add a double check on focus --- flutter/lib/desktop/pages/remote_page.dart | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index b9f25e0dd..2ecc1e6c9 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -294,6 +294,16 @@ class _RemotePageState extends State onEnter: enterView, onExit: leaveView, onPointerDown: (event) { + // A double check for blur status. + // Note: If there's an `onPointerDown` event is triggered, `_isWindowBlur` is expected being false. + // Sometimes the system does not send the necessary focus event to flutter. We should manually + // handle this inconsistent status by setting `_isWindowBlur` to false. So we can + // ensure the grab-key thread is running when our users are clicking the remote canvas. + if (_isWindowBlur) { + debugPrint( + "Unexpected status: onPointerDown is triggered while the remote window is in blur status"); + _isWindowBlur = false; + } if (!_rawKeyFocusNode.hasFocus) { _rawKeyFocusNode.requestFocus(); } From 0b9b71e4fca5f0397f0d5c9e964b55da23ec99c5 Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 29 Dec 2022 18:16:06 +0800 Subject: [PATCH 1303/2015] move sleep from main thread Signed-off-by: fufesou --- src/server/input_service.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 709513867..dc126be5f 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -223,8 +223,7 @@ lazy_static::lazy_static! { static ref IS_SERVER: bool = std::env::args().nth(1) == Some("--server".to_owned()); } -// virtual_input must be used in main thread. -// No need to wrap mutex. +static mut VIRTUAL_INPUT_MTX: Mutex<()> = Mutex::new(()); static mut VIRTUAL_INPUT: Option = None; // First call set_uinput() will create keyboard and mouse clients. @@ -687,17 +686,21 @@ pub fn handle_key(evt: &KeyEvent) { // having GUI, run main GUI thread, otherwise crash let evt = evt.clone(); QUEUE.exec_async(move || handle_key_(&evt)); + std::thread::sleep(Duration::from_millis(20)); return; } #[cfg(windows)] crate::portable_service::client::handle_key(evt); #[cfg(not(windows))] handle_key_(evt); + #[cfg(target_os = "macos")] + std::thread::sleep(Duration::from_millis(20)); } #[inline] fn reset_input() { unsafe { + let _lock = VIRTUAL_INPUT_MTX.lock(); VIRTUAL_INPUT = VirtualInput::new( CGEventSourceStateID::Private, CGEventTapLocation::AnnotatedSession, @@ -738,9 +741,9 @@ fn sim_rdev_rawkey(code: u32, keydown: bool) { #[inline] fn simulate_(event_type: &EventType) { unsafe { + let _lock = VIRTUAL_INPUT_MTX.lock(); if let Some(virtual_input) = &VIRTUAL_INPUT { let _ = virtual_input.simulate(&event_type); - std::thread::sleep(Duration::from_millis(20)); } } } From fb5cfabf5184c818521fc021689c384d6b77dbcf Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 29 Dec 2022 19:10:25 +0800 Subject: [PATCH 1304/2015] fix build Signed-off-by: fufesou --- src/server/input_service.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/server/input_service.rs b/src/server/input_service.rs index dc126be5f..bd2ad9a16 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -223,7 +223,9 @@ lazy_static::lazy_static! { static ref IS_SERVER: bool = std::env::args().nth(1) == Some("--server".to_owned()); } +#[cfg(target_os = "macos")] static mut VIRTUAL_INPUT_MTX: Mutex<()> = Mutex::new(()); +#[cfg(target_os = "macos")] static mut VIRTUAL_INPUT: Option = None; // First call set_uinput() will create keyboard and mouse clients. @@ -697,6 +699,7 @@ pub fn handle_key(evt: &KeyEvent) { std::thread::sleep(Duration::from_millis(20)); } +#[cfg(target_os = "macos")] #[inline] fn reset_input() { unsafe { From 67ad937fdd883b4bc74cf60767dfd88ce0a20a5a Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 29 Dec 2022 20:34:52 +0800 Subject: [PATCH 1305/2015] fix nat64 and refactor ipv6 --- libs/hbb_common/src/socket_client.rs | 159 +++++++++++++-------------- libs/hbb_common/src/tcp.rs | 110 +++++++++--------- libs/hbb_common/src/udp.rs | 9 ++ src/cli.rs | 2 + src/client.rs | 2 +- src/common.rs | 11 +- src/rendezvous_mediator.rs | 20 ++-- 7 files changed, 157 insertions(+), 156 deletions(-) diff --git a/libs/hbb_common/src/socket_client.rs b/libs/hbb_common/src/socket_client.rs index 615579d65..667a5161e 100644 --- a/libs/hbb_common/src/socket_client.rs +++ b/libs/hbb_common/src/socket_client.rs @@ -6,44 +6,18 @@ use crate::{ }; use anyhow::Context; use std::net::SocketAddr; -use std::net::ToSocketAddrs; +use tokio::net::ToSocketAddrs; use tokio_socks::{IntoTargetAddr, TargetAddr}; -fn to_socket_addr(host: T) -> ResultType { - let mut addr_ipv4 = None; - let mut addr_ipv6 = None; - for addr in host.to_socket_addrs()? { - if addr.is_ipv4() && addr_ipv4.is_none() { - addr_ipv4 = Some(addr); - } - if addr.is_ipv6() && addr_ipv6.is_none() { - addr_ipv6 = Some(addr); - } - } - if let Some(addr) = addr_ipv4 { - Ok(addr) - } else { - addr_ipv6.context("Failed to solve") - } -} - -pub fn get_target_addr(host: &str) -> ResultType> { - let addr = match Config::get_network_type() { - NetworkType::Direct => to_socket_addr(&host)?.into_target_addr()?, - NetworkType::ProxySocks => host.into_target_addr()?, - } - .to_owned(); - Ok(addr) -} - pub fn test_if_valid_server(host: &str) -> String { let mut host = host.to_owned(); if !host.contains(":") { host = format!("{}:{}", host, 0); } + use std::net::ToSocketAddrs; match Config::get_network_type() { - NetworkType::Direct => match to_socket_addr(&host) { + NetworkType::Direct => match host.to_socket_addrs() { Err(err) => err.to_string(), Ok(_) => "".to_owned(), }, @@ -54,56 +28,51 @@ pub fn test_if_valid_server(host: &str) -> String { } } -pub trait IntoTargetAddr2<'a> { - /// Converts the value of self to a `TargetAddr`. - fn into_target_addr2(&self) -> ResultType>; +pub trait IsResolvedSocketAddr { + fn resolve(&self) -> Option<&SocketAddr>; } -impl<'a> IntoTargetAddr2<'a> for SocketAddr { - fn into_target_addr2(&self) -> ResultType> { - Ok(TargetAddr::Ip(*self)) +impl IsResolvedSocketAddr for SocketAddr { + fn resolve(&self) -> Option<&SocketAddr> { + Some(&self) } } -impl<'a> IntoTargetAddr2<'a> for TargetAddr<'a> { - fn into_target_addr2(&self) -> ResultType> { - Ok(self.clone()) +impl IsResolvedSocketAddr for String { + fn resolve(&self) -> Option<&SocketAddr> { + None } } -impl<'a> IntoTargetAddr2<'a> for String { - fn into_target_addr2(&self) -> ResultType> { - Ok(to_socket_addr(self)?.into_target_addr()?) +impl IsResolvedSocketAddr for &str { + fn resolve(&self) -> Option<&SocketAddr> { + None } } -impl<'a> IntoTargetAddr2<'a> for &str { - fn into_target_addr2(&self) -> ResultType> { - Ok(to_socket_addr(self)?.into_target_addr()?) - } -} - -pub async fn connect_tcp<'t, T: IntoTargetAddr2<'t> + std::fmt::Debug>( +#[inline] +pub async fn connect_tcp< + 't, + T: IntoTargetAddr<'t> + ToSocketAddrs + IsResolvedSocketAddr + std::fmt::Display, +>( target: T, ms_timeout: u64, ) -> ResultType { - let target_addr = target.into_target_addr2()?; - let local = Config::get_any_listen_addr(is_ipv4(&target_addr)); - connect_tcp_local(target_addr, local, ms_timeout) - .await - .context(format!("Invalid target addr: {:?}", target)) + connect_tcp_local(target, None, ms_timeout).await } -pub async fn connect_tcp_local<'t, T: IntoTargetAddr<'t> + std::fmt::Debug>( +pub async fn connect_tcp_local< + 't, + T: IntoTargetAddr<'t> + ToSocketAddrs + IsResolvedSocketAddr + std::fmt::Display, +>( target: T, - local: SocketAddr, + local: Option, ms_timeout: u64, ) -> ResultType { - let target_addr = target.into_target_addr()?; if let Some(conf) = Config::get_socks() { return FramedStream::connect( conf.proxy.as_str(), - target_addr, + target, local, conf.username.as_str(), conf.password.as_str(), @@ -111,13 +80,15 @@ pub async fn connect_tcp_local<'t, T: IntoTargetAddr<'t> + std::fmt::Debug>( ) .await; } - let mut addr = ToSocketAddrs::to_socket_addrs(&target_addr)? - .next() - .context(format!("Invalid target addr: {:?}", target_addr))?; - if local.is_ipv6() && addr.is_ipv4() { - addr = query_nip_io(&addr)?; + if let Some(target) = target.resolve() { + if let Some(local) = local { + if local.is_ipv6() && target.is_ipv4() { + let target = query_nip_io(&target).await?; + return Ok(FramedStream::new(target, Some(local), ms_timeout).await?); + } + } } - Ok(FramedStream::new(addr, local, ms_timeout).await?) + Ok(FramedStream::new(target, local, ms_timeout).await?) } #[inline] @@ -129,8 +100,12 @@ pub fn is_ipv4(target: &TargetAddr<'_>) -> bool { } #[inline] -pub fn query_nip_io(addr: &SocketAddr) -> ResultType { - to_socket_addr(format!("{}.nip.io:{}", addr.ip(), addr.port())) +pub async fn query_nip_io(addr: &SocketAddr) -> ResultType { + tokio::net::lookup_host(format!("{}.nip.io:{}", addr.ip(), addr.port())) + .await? + .filter(|x| x.is_ipv6()) + .next() + .context("Failed to get ipv6 from nip.io") } #[inline] @@ -143,17 +118,29 @@ pub fn ipv4_to_ipv6(addr: String, ipv4: bool) -> String { addr } -pub async fn new_udp_for(target: &TargetAddr<'_>, ms_timeout: u64) -> ResultType { - new_udp(Config::get_any_listen_addr(is_ipv4(target)), ms_timeout).await +async fn test_is_ipv4(target: &str) -> bool { + if let Ok(Ok(s)) = super::timeout(1000, tokio::net::TcpStream::connect(target)).await { + return s.local_addr().map(|x| x.is_ipv4()).unwrap_or(true); + } + true +} + +#[inline] +pub async fn new_udp_for(target: &str, ms_timeout: u64) -> ResultType { + new_udp( + Config::get_any_listen_addr(test_is_ipv4(target).await), + ms_timeout, + ) + .await } async fn new_udp(local: T, ms_timeout: u64) -> ResultType { match Config::get_socks() { - None => Ok(FramedSocket::new(to_socket_addr(&local)?).await?), + None => Ok(FramedSocket::new(local).await?), Some(conf) => { let socket = FramedSocket::new_proxy( conf.proxy.as_str(), - to_socket_addr(local)?, + local, conf.username.as_str(), conf.password.as_str(), ms_timeout, @@ -164,10 +151,10 @@ async fn new_udp(local: T, ms_timeout: u64) -> ResultType) -> ResultType> { +pub async fn rebind_udp_for(target: &str) -> ResultType> { match Config::get_network_type() { NetworkType::Direct => Ok(Some( - FramedSocket::new(Config::get_any_listen_addr(is_ipv4(target))).await?, + FramedSocket::new(Config::get_any_listen_addr(test_is_ipv4(target).await)).await?, )), _ => Ok(None), } @@ -175,19 +162,17 @@ pub async fn rebind_udp_for(target: &TargetAddr<'_>) -> ResultType Result( - remote_addr: T1, - local_addr: T2, + pub async fn new( + remote_addr: T, + local_addr: Option, ms_timeout: u64, ) -> ResultType { - for local_addr in lookup_host(&local_addr).await? { - for remote_addr in lookup_host(&remote_addr).await? { - let stream = super::timeout( - ms_timeout, - new_socket(local_addr, true)?.connect(remote_addr), - ) - .await??; - stream.set_nodelay(true).ok(); - let addr = stream.local_addr()?; - return Ok(Self( - Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()), - addr, - None, - 0, - )); + for remote_addr in lookup_host(&remote_addr).await? { + let local = if let Some(addr) = local_addr { + addr + } else { + crate::config::Config::get_any_listen_addr(remote_addr.is_ipv4()) + }; + if let Ok(socket) = new_socket(local, true) { + if let Ok(Ok(stream)) = + super::timeout(ms_timeout, socket.connect(remote_addr)).await + { + stream.set_nodelay(true).ok(); + let addr = stream.local_addr()?; + return Ok(Self( + Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()), + addr, + None, + 0, + )); + } } } - bail!("could not resolve to any address"); + bail!(format!("Failed to connect to {}", remote_addr)); } - pub async fn connect<'a, 't, P, T1, T2>( + pub async fn connect<'a, 't, P, T>( proxy: P, - target: T1, - local: T2, + target: T, + local_addr: Option, username: &'a str, password: &'a str, ms_timeout: u64, ) -> ResultType where P: ToProxyAddrs, - T1: IntoTargetAddr<'t>, - T2: ToSocketAddrs, + T: IntoTargetAddr<'t>, { - if let Some(local) = lookup_host(&local).await?.next() { - if let Some(proxy) = proxy.to_proxy_addrs().next().await { - let stream = - super::timeout(ms_timeout, new_socket(local, true)?.connect(proxy?)).await??; - stream.set_nodelay(true).ok(); - let stream = if username.trim().is_empty() { - super::timeout( - ms_timeout, - Socks5Stream::connect_with_socket(stream, target), - ) - .await?? - } else { - super::timeout( - ms_timeout, - Socks5Stream::connect_with_password_and_socket( - stream, target, username, password, - ), - ) - .await?? - }; - let addr = stream.local_addr()?; - return Ok(Self( - Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()), - addr, - None, - 0, - )); + if let Some(Ok(proxy)) = proxy.to_proxy_addrs().next().await { + let local = if let Some(addr) = local_addr { + addr + } else { + crate::config::Config::get_any_listen_addr(proxy.is_ipv4()) }; - }; + let stream = + super::timeout(ms_timeout, new_socket(local, true)?.connect(proxy)).await??; + stream.set_nodelay(true).ok(); + let stream = if username.trim().is_empty() { + super::timeout( + ms_timeout, + Socks5Stream::connect_with_socket(stream, target), + ) + .await?? + } else { + super::timeout( + ms_timeout, + Socks5Stream::connect_with_password_and_socket( + stream, target, username, password, + ), + ) + .await?? + }; + let addr = stream.local_addr()?; + return Ok(Self( + Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()), + addr, + None, + 0, + )); + } bail!("could not resolve to any address"); } diff --git a/libs/hbb_common/src/udp.rs b/libs/hbb_common/src/udp.rs index 1f5bf2637..38121a4e1 100644 --- a/libs/hbb_common/src/udp.rs +++ b/libs/hbb_common/src/udp.rs @@ -164,4 +164,13 @@ impl FramedSocket { None } } + + pub fn is_ipv4(&self) -> bool { + if let FramedSocket::Direct(x) = self { + if let Ok(v) = x.get_ref().local_addr() { + return v.is_ipv4(); + } + } + true + } } diff --git a/src/cli.rs b/src/cli.rs index b4f552d5f..2b2cae320 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -102,6 +102,8 @@ pub async fn connect_test( handler, ).await { log::error!("Failed to connect {}: {}", &id, err); + } else { + // rpassword::prompt_password("Input anything to exit").ok(); } } diff --git a/src/client.rs b/src/client.rs index 1c6f63f36..fe9d9dac0 100644 --- a/src/client.rs +++ b/src/client.rs @@ -376,7 +376,7 @@ impl Client { log::info!("peer address: {}, timeout: {}", peer, connect_timeout); let start = std::time::Instant::now(); // NOTICE: Socks5 is be used event in intranet. Which may be not a good way. - let mut conn = socket_client::connect_tcp_local(peer, local_addr, connect_timeout).await; + let mut conn = socket_client::connect_tcp_local(peer, Some(local_addr), connect_timeout).await; let mut direct = !conn.is_err(); if interface.is_force_relay() || conn.is_err() { if !relay_server.is_empty() { diff --git a/src/common.rs b/src/common.rs index f83fbc69d..1ae9b7dbe 100644 --- a/src/common.rs +++ b/src/common.rs @@ -310,15 +310,9 @@ async fn test_nat_type_() -> ResultType { }); let mut port1 = 0; let mut port2 = 0; - let server1 = socket_client::get_target_addr(&server1)?; - let server2 = socket_client::get_target_addr(&server2)?; for i in 0..2 { let mut socket = socket_client::connect_tcp( - if i == 0 { - server1.clone() - } else { - server2.clone() - }, + if i == 0 { &*server1 } else { &*server2 }, RENDEZVOUS_TIMEOUT, ) .await?; @@ -525,8 +519,7 @@ pub fn check_software_update() { async fn check_software_update_() -> hbb_common::ResultType<()> { sleep(3.).await; - let rendezvous_server = - socket_client::get_target_addr(&format!("rs-sg.rustdesk.com:{}", config::RENDEZVOUS_PORT))?; + let rendezvous_server = format!("rs-sg.rustdesk.com:{}", config::RENDEZVOUS_PORT); let mut socket = socket_client::new_udp_for(&rendezvous_server, RENDEZVOUS_TIMEOUT).await?; let mut msg_out = RendezvousMessage::new(); diff --git a/src/rendezvous_mediator.rs b/src/rendezvous_mediator.rs index f962810a2..ca08172d3 100644 --- a/src/rendezvous_mediator.rs +++ b/src/rendezvous_mediator.rs @@ -38,10 +38,11 @@ static SHOULD_EXIT: AtomicBool = AtomicBool::new(false); #[derive(Clone)] pub struct RendezvousMediator { - addr: TargetAddr<'static>, + addr: String, host: String, host_prefix: String, last_id_pk_registry: String, + is_ipv4: bool, } impl RendezvousMediator { @@ -111,13 +112,15 @@ impl RendezvousMediator { }) .unwrap_or(host.to_owned()); let mut rz = Self { - addr: socket_client::get_target_addr(&crate::check_port(&host, RENDEZVOUS_PORT))?, + addr: crate::check_port(&host, RENDEZVOUS_PORT), + is_ipv4: false, host: host.clone(), host_prefix, last_id_pk_registry: "".to_owned(), }; let mut socket = socket_client::new_udp_for(&rz.addr, RENDEZVOUS_TIMEOUT).await?; + rz.is_ipv4 = socket.is_ipv4(); const TIMER_OUT: Duration = Duration::from_secs(1); let mut timer = interval(TIMER_OUT); @@ -248,11 +251,11 @@ impl RendezvousMediator { Config::update_latency(&host, -1); old_latency = 0; if last_dns_check.elapsed().as_millis() as i64 > DNS_INTERVAL { - rz.addr = socket_client::get_target_addr(&crate::check_port(&host, RENDEZVOUS_PORT))?; // in some case of network reconnect (dial IP network), // old UDP socket not work any more after network recover if let Some(s) = socket_client::rebind_udp_for(&rz.addr).await? { socket = s; + rz.is_ipv4 = socket.is_ipv4(); } last_dns_check = Instant::now(); } @@ -314,14 +317,14 @@ impl RendezvousMediator { } msg_out.set_relay_response(rr); socket.send(&msg_out).await?; - let v4 = socket_client::is_ipv4(&self.addr); - crate::create_relay_connection(server, relay_server, uuid, peer_addr, secure, v4).await; + crate::create_relay_connection(server, relay_server, uuid, peer_addr, secure, self.is_ipv4) + .await; Ok(()) } async fn handle_intranet(&self, fla: FetchLocalAddr, server: ServerPtr) -> ResultType<()> { let relay_server = self.get_relay_server(fla.relay_server); - if !socket_client::is_ipv4(&self.addr) { + if !self.is_ipv4 { // nat64, go relay directly, because current hbbs will crash if demangle ipv6 address let uuid = Uuid::new_v4().to_string(); return self @@ -382,7 +385,7 @@ impl RendezvousMediator { let local_addr = socket.local_addr(); // key important here for punch hole to tell my gateway incoming peer is safe. // it can not be async here, because local_addr can not be reused, we must close the connection before use it again. - allow_err!(socket_client::connect_tcp_local(peer_addr, local_addr, 30).await); + allow_err!(socket_client::connect_tcp_local(peer_addr, Some(local_addr), 30).await); socket }; let mut msg_out = Message::new(); @@ -655,8 +658,7 @@ async fn create_online_stream() -> ResultType { bail!("Invalid server address: {}", rendezvous_server); } let online_server = format!("{}:{}", tmp[0], port - 1); - let server_addr = socket_client::get_target_addr(&online_server)?; - socket_client::connect_tcp(server_addr, RENDEZVOUS_TIMEOUT).await + socket_client::connect_tcp(online_server, RENDEZVOUS_TIMEOUT).await } async fn query_online_states_( From f14faa85d239f4760b34da574678171218995467 Mon Sep 17 00:00:00 2001 From: Nikos Fazakis Date: Thu, 29 Dec 2022 14:42:16 +0200 Subject: [PATCH 1306/2015] fix relay mac connections and cleanup --- src/server.rs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/server.rs b/src/server.rs index 7f9b055dc..9b5ffd57a 100644 --- a/src/server.rs +++ b/src/server.rs @@ -93,6 +93,14 @@ pub fn new() -> ServerPtr { Arc::new(RwLock::new(server)) } +fn mac_wakeup(){ + #[cfg(target_os = "macos")]{ + use std::process::Command; + Command::new("/usr/bin/caffeinate").arg("-u").arg("-t 5").spawn().expect("failed to execute caffeinate"); + println!("wake up macos"); + } +} + async fn accept_connection_(server: ServerPtr, socket: Stream, secure: bool) -> ResultType<()> { let local_addr = socket.local_addr(); drop(socket); @@ -104,11 +112,7 @@ async fn accept_connection_(server: ServerPtr, socket: Stream, secure: bool) -> if let Ok((stream, addr)) = timeout(CONNECT_TIMEOUT, listener.accept()).await? { stream.set_nodelay(true).ok(); let stream_addr = stream.local_addr()?; - if cfg!(target_os = "macos") { - use std::process::Command; - Command::new("/usr/bin/caffeinate").arg("-u").arg("-t 5").spawn().expect("failed to execute caffeinate"); - println!("wake up macos..."); - } + mac_wakeup(); create_tcp_connection(server, Stream::from(stream, stream_addr), addr, secure).await?; } Ok(()) @@ -257,6 +261,7 @@ async fn create_relay_connection_( ..Default::default() }); stream.send(&msg_out).await?; + mac_wakeup(); create_tcp_connection(server, stream, peer_addr, secure).await?; Ok(()) } From 9859b4f27d952e224ff5ac83f283deb88a2973f6 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 29 Dec 2022 22:31:01 +0800 Subject: [PATCH 1307/2015] fix ipv6 refactory --- libs/hbb_common/src/socket_client.rs | 48 +++++++++++++++++++--------- src/common.rs | 3 +- src/main.rs | 2 +- src/rendezvous_mediator.rs | 42 ++++++++++++------------ 4 files changed, 58 insertions(+), 37 deletions(-) diff --git a/libs/hbb_common/src/socket_client.rs b/libs/hbb_common/src/socket_client.rs index 667a5161e..b7cb13754 100644 --- a/libs/hbb_common/src/socket_client.rs +++ b/libs/hbb_common/src/socket_client.rs @@ -118,20 +118,33 @@ pub fn ipv4_to_ipv6(addr: String, ipv4: bool) -> String { addr } -async fn test_is_ipv4(target: &str) -> bool { +async fn test_target(target: &str) -> ResultType { if let Ok(Ok(s)) = super::timeout(1000, tokio::net::TcpStream::connect(target)).await { - return s.local_addr().map(|x| x.is_ipv4()).unwrap_or(true); + if let Ok(addr) = s.peer_addr() { + return Ok(addr); + } } - true + tokio::net::lookup_host(target) + .await? + .next() + .context(format!("Failed to look up host for {}", target)) } #[inline] -pub async fn new_udp_for(target: &str, ms_timeout: u64) -> ResultType { - new_udp( - Config::get_any_listen_addr(test_is_ipv4(target).await), - ms_timeout, - ) - .await +pub async fn new_udp_for( + target: &str, + ms_timeout: u64, +) -> ResultType<(FramedSocket, TargetAddr<'static>)> { + let (ipv4, target) = if NetworkType::Direct == Config::get_network_type() { + let addr = test_target(target).await?; + (addr.is_ipv4(), addr.into_target_addr()?) + } else { + (true, target.into_target_addr()?) + }; + Ok(( + new_udp(Config::get_any_listen_addr(ipv4), ms_timeout).await?, + target.to_owned(), + )) } async fn new_udp(local: T, ms_timeout: u64) -> ResultType { @@ -151,13 +164,18 @@ async fn new_udp(local: T, ms_timeout: u64) -> ResultType ResultType> { - match Config::get_network_type() { - NetworkType::Direct => Ok(Some( - FramedSocket::new(Config::get_any_listen_addr(test_is_ipv4(target).await)).await?, - )), - _ => Ok(None), +pub async fn rebind_udp_for( + target: &str, +) -> ResultType)>> { + if Config::get_network_type() != NetworkType::Direct { + return Ok(None); } + let addr = test_target(target).await?; + let v4 = addr.is_ipv4(); + Ok(Some(( + FramedSocket::new(Config::get_any_listen_addr(v4)).await?, + addr.into_target_addr()?.to_owned(), + ))) } #[cfg(test)] diff --git a/src/common.rs b/src/common.rs index 1ae9b7dbe..c28bbc3fc 100644 --- a/src/common.rs +++ b/src/common.rs @@ -520,7 +520,8 @@ async fn check_software_update_() -> hbb_common::ResultType<()> { sleep(3.).await; let rendezvous_server = format!("rs-sg.rustdesk.com:{}", config::RENDEZVOUS_PORT); - let mut socket = socket_client::new_udp_for(&rendezvous_server, RENDEZVOUS_TIMEOUT).await?; + let (mut socket, rendezvous_server) = + socket_client::new_udp_for(&rendezvous_server, RENDEZVOUS_TIMEOUT).await?; let mut msg_out = RendezvousMessage::new(); msg_out.set_software_update(SoftwareUpdate { diff --git a/src/main.rs b/src/main.rs index ca0bc2234..67ddb875f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -38,7 +38,7 @@ fn main() { "-p, --port-forward=[PORT-FORWARD-OPTIONS] 'Format: remote-id:local-port:remote-port[:remote-host]' -c, --connect=[REMOTE_ID] 'test only' -k, --key=[KEY] '' - -s, --server... 'Start server'", + -s, --server=[] 'Start server'", ); let matches = App::new("rustdesk") .version(crate::VERSION) diff --git a/src/rendezvous_mediator.rs b/src/rendezvous_mediator.rs index ca08172d3..ec70bdf84 100644 --- a/src/rendezvous_mediator.rs +++ b/src/rendezvous_mediator.rs @@ -18,13 +18,14 @@ use hbb_common::{ log, protobuf::Message as _, rendezvous_proto::*, - sleep, socket_client, + sleep, + socket_client::{self, is_ipv4}, tokio::{ self, select, time::{interval, Duration}, }, udp::FramedSocket, - AddrMangle, ResultType, TargetAddr, + AddrMangle, ResultType, }; use crate::server::{check_zombie, new as new_server, ServerPtr}; @@ -38,11 +39,10 @@ static SHOULD_EXIT: AtomicBool = AtomicBool::new(false); #[derive(Clone)] pub struct RendezvousMediator { - addr: String, + addr: hbb_common::tokio_socks::TargetAddr<'static>, host: String, host_prefix: String, last_id_pk_registry: String, - is_ipv4: bool, } impl RendezvousMediator { @@ -111,17 +111,15 @@ impl RendezvousMediator { } }) .unwrap_or(host.to_owned()); + let host = crate::check_port(&host, RENDEZVOUS_PORT); + let (mut socket, addr) = socket_client::new_udp_for(&host, RENDEZVOUS_TIMEOUT).await?; let mut rz = Self { - addr: crate::check_port(&host, RENDEZVOUS_PORT), - is_ipv4: false, + addr: addr, host: host.clone(), host_prefix, last_id_pk_registry: "".to_owned(), }; - let mut socket = socket_client::new_udp_for(&rz.addr, RENDEZVOUS_TIMEOUT).await?; - rz.is_ipv4 = socket.is_ipv4(); - const TIMER_OUT: Duration = Duration::from_secs(1); let mut timer = interval(TIMER_OUT); let mut last_timer: Option = None; @@ -253,9 +251,9 @@ impl RendezvousMediator { if last_dns_check.elapsed().as_millis() as i64 > DNS_INTERVAL { // in some case of network reconnect (dial IP network), // old UDP socket not work any more after network recover - if let Some(s) = socket_client::rebind_udp_for(&rz.addr).await? { + if let Some((s, addr)) = socket_client::rebind_udp_for(&rz.host).await? { socket = s; - rz.is_ipv4 = socket.is_ipv4(); + rz.addr = addr; } last_dns_check = Instant::now(); } @@ -301,8 +299,7 @@ impl RendezvousMediator { secure, ); - let mut socket = - socket_client::connect_tcp(self.addr.to_owned(), RENDEZVOUS_TIMEOUT).await?; + let mut socket = socket_client::connect_tcp(&*self.host, RENDEZVOUS_TIMEOUT).await?; let mut msg_out = Message::new(); let mut rr = RelayResponse { @@ -317,14 +314,21 @@ impl RendezvousMediator { } msg_out.set_relay_response(rr); socket.send(&msg_out).await?; - crate::create_relay_connection(server, relay_server, uuid, peer_addr, secure, self.is_ipv4) - .await; + crate::create_relay_connection( + server, + relay_server, + uuid, + peer_addr, + secure, + is_ipv4(&self.addr), + ) + .await; Ok(()) } async fn handle_intranet(&self, fla: FetchLocalAddr, server: ServerPtr) -> ResultType<()> { let relay_server = self.get_relay_server(fla.relay_server); - if !self.is_ipv4 { + if !is_ipv4(&self.addr) { // nat64, go relay directly, because current hbbs will crash if demangle ipv6 address let uuid = Uuid::new_v4().to_string(); return self @@ -340,8 +344,7 @@ impl RendezvousMediator { } let peer_addr = AddrMangle::decode(&fla.socket_addr); log::debug!("Handle intranet from {:?}", peer_addr); - let mut socket = - socket_client::connect_tcp(self.addr.to_owned(), RENDEZVOUS_TIMEOUT).await?; + let mut socket = socket_client::connect_tcp(&*self.host, RENDEZVOUS_TIMEOUT).await?; let local_addr = socket.local_addr(); let local_addr: SocketAddr = format!("{}:{}", local_addr.ip(), local_addr.port()).parse()?; @@ -380,8 +383,7 @@ impl RendezvousMediator { let peer_addr = AddrMangle::decode(&ph.socket_addr); log::debug!("Punch hole to {:?}", peer_addr); let mut socket = { - let socket = - socket_client::connect_tcp(self.addr.to_owned(), RENDEZVOUS_TIMEOUT).await?; + let socket = socket_client::connect_tcp(&*self.host, RENDEZVOUS_TIMEOUT).await?; let local_addr = socket.local_addr(); // key important here for punch hole to tell my gateway incoming peer is safe. // it can not be async here, because local_addr can not be reused, we must close the connection before use it again. From 08278059ac691d0704115c57add3ccf248d034c2 Mon Sep 17 00:00:00 2001 From: Nikos Fazakis Date: Thu, 29 Dec 2022 16:52:45 +0200 Subject: [PATCH 1308/2015] mac_wakeup resolve panic possibility --- src/server.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/server.rs b/src/server.rs index 9b5ffd57a..89a5b9314 100644 --- a/src/server.rs +++ b/src/server.rs @@ -96,8 +96,8 @@ pub fn new() -> ServerPtr { fn mac_wakeup(){ #[cfg(target_os = "macos")]{ use std::process::Command; - Command::new("/usr/bin/caffeinate").arg("-u").arg("-t 5").spawn().expect("failed to execute caffeinate"); - println!("wake up macos"); + Command::new("/usr/bin/caffeinate").arg("-u").arg("-t 5").spawn().ok(); + log::info!("wake up macos"); } } From 07e399cf810faa9a25791dcaf4b906ab1b5e4e70 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 29 Dec 2022 23:27:17 +0800 Subject: [PATCH 1309/2015] more logic in cli connect --- src/cli.rs | 47 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 2b2cae320..57d63d397 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,6 +1,8 @@ use crate::client::*; use hbb_common::{ config::PeerConfig, + config::READ_TIMEOUT, + futures::{SinkExt, StreamExt}, log, message_proto::*, protobuf::Message as _, @@ -87,23 +89,38 @@ impl Interface for Session { } #[tokio::main(flavor = "current_thread")] -pub async fn connect_test( - id: &str, - key: String, - token: String, -) { +pub async fn connect_test(id: &str, key: String, token: String) { let (sender, mut receiver) = mpsc::unbounded_channel::(); let handler = Session::new(&id, sender); - if let Err(err) = crate::client::Client::start( - id, - &key, - &token, - ConnType::PORT_FORWARD, - handler, - ).await { - log::error!("Failed to connect {}: {}", &id, err); - } else { - // rpassword::prompt_password("Input anything to exit").ok(); + match crate::client::Client::start(id, &key, &token, ConnType::PORT_FORWARD, handler).await { + Err(err) => { + log::error!("Failed to connect {}: {}", &id, err); + } + Ok((mut stream, direct)) => { + log::info!("direct: {}", direct); + // rpassword::prompt_password("Input anything to exit").ok(); + loop { + tokio::select! { + res = hbb_common::timeout(READ_TIMEOUT, stream.next()) => match res { + Err(_) => { + log::error!("Timeout"); + break; + } + Ok(Some(Ok(bytes))) => { + let msg_in = Message::parse_from_bytes(&bytes).unwrap(); + match msg_in.union { + Some(message::Union::Hash(hash)) => { + log::info!("Got hash"); + break; + } + _ => {} + } + } + _ => {} + } + } + } + } } } From 944ca510ccde61ecb30d40b923837d6e0d9f9b4f Mon Sep 17 00:00:00 2001 From: Nikos Fazakis Date: Thu, 29 Dec 2022 22:22:16 +0200 Subject: [PATCH 1310/2015] mac wakeup compact code --- src/server.rs | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/server.rs b/src/server.rs index 89a5b9314..a81684d19 100644 --- a/src/server.rs +++ b/src/server.rs @@ -93,14 +93,6 @@ pub fn new() -> ServerPtr { Arc::new(RwLock::new(server)) } -fn mac_wakeup(){ - #[cfg(target_os = "macos")]{ - use std::process::Command; - Command::new("/usr/bin/caffeinate").arg("-u").arg("-t 5").spawn().ok(); - log::info!("wake up macos"); - } -} - async fn accept_connection_(server: ServerPtr, socket: Stream, secure: bool) -> ResultType<()> { let local_addr = socket.local_addr(); drop(socket); @@ -112,7 +104,6 @@ async fn accept_connection_(server: ServerPtr, socket: Stream, secure: bool) -> if let Ok((stream, addr)) = timeout(CONNECT_TIMEOUT, listener.accept()).await? { stream.set_nodelay(true).ok(); let stream_addr = stream.local_addr()?; - mac_wakeup(); create_tcp_connection(server, Stream::from(stream, stream_addr), addr, secure).await?; } Ok(()) @@ -203,6 +194,11 @@ pub async fn create_tcp_connection( } } + #[cfg(target_os = "macos")]{ + use std::process::Command; + Command::new("/usr/bin/caffeinate").arg("-u").arg("-t 5").spawn().ok(); + log::info!("wake up macos"); + } Connection::start(addr, stream, id, Arc::downgrade(&server)).await; Ok(()) } @@ -261,7 +257,6 @@ async fn create_relay_connection_( ..Default::default() }); stream.send(&msg_out).await?; - mac_wakeup(); create_tcp_connection(server, stream, peer_addr, secure).await?; Ok(()) } From 56041a5aac487c038b32735dc62d17cb048bff98 Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 30 Dec 2022 16:14:30 +0800 Subject: [PATCH 1311/2015] fix remote cursor pos Signed-off-by: fufesou --- flutter/lib/desktop/pages/remote_page.dart | 9 ++++---- .../lib/desktop/widgets/remote_menubar.dart | 2 -- flutter/lib/models/model.dart | 21 ++++++++++++++----- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 2ecc1e6c9..6ca711fda 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -316,8 +316,8 @@ class _RemotePageState extends State ]; if (!_ffi.canvasModel.cursorEmbeded) { - paints.add(Obx(() => Visibility( - visible: _showRemoteCursor.isTrue && _remoteCursorMoved.isTrue, + paints.add(Obx(() => Offstage( + offstage: _showRemoteCursor.isFalse || _remoteCursorMoved.isFalse, child: CursorPaint( id: widget.id, zoomCursor: _zoomCursor, @@ -617,7 +617,8 @@ class CursorPaint extends StatelessWidget { double cx = c.x; double cy = c.y; - if (c.scrollStyle == ScrollStyle.scrollbar) { + if (c.viewStyle.style == kRemoteViewStyleOriginal && + c.scrollStyle == ScrollStyle.scrollbar) { final d = c.parent.target!.ffiModel.display; final imageWidth = d.width * c.scale; final imageHeight = d.height * c.scale; @@ -626,7 +627,7 @@ class CursorPaint extends StatelessWidget { } double x = (m.x - hotx) * c.scale + cx; - double y = (m.y - hoty) * c.scale + cx; + double y = (m.y - hoty) * c.scale + cy; double scale = 1.0; if (zoomCursor.isTrue) { x = m.x - hotx + cx / c.scale; diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 0cd4ee00f..78deb7a38 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -365,8 +365,6 @@ class _RemoteMenubarState extends State { RxInt display = CurrentDisplayState.find(widget.id); if (display.value != i) { bind.sessionSwitchDisplay(id: widget.id, value: i); - pi.currentDisplay = i; - display.value = i; } }, ) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 63062a928..e0e88f08e 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -141,7 +141,7 @@ class FfiModel with ChangeNotifier { setConnectionType( peerId, evt['secure'] == 'true', evt['direct'] == 'true'); } else if (name == 'switch_display') { - handleSwitchDisplay(evt); + handleSwitchDisplay(evt, peerId); } else if (name == 'cursor_data') { await parent.target?.cursorModel.updateCursorData(evt); } else if (name == 'cursor_id') { @@ -214,7 +214,7 @@ class FfiModel with ChangeNotifier { } } - handleSwitchDisplay(Map evt) { + handleSwitchDisplay(Map evt, String peerId) { final oldOrientation = _display.width > _display.height; var old = _pi.currentDisplay; _pi.currentDisplay = int.parse(evt['display']); @@ -227,6 +227,12 @@ class FfiModel with ChangeNotifier { parent.target?.cursorModel.updateDisplayOrigin(_display.x, _display.y); } + try { + CurrentDisplayState.find(peerId).value = _pi.currentDisplay; + } catch (e) { + // + } + // remote is mobile, and orientation changed if ((_display.width > _display.height) != oldOrientation) { gFFI.canvasModel.updateViewStyle(); @@ -506,7 +512,7 @@ class ViewStyle { double get scale { double s = 1.0; - if (style == 'adaptive') { + if (style == kRemoteViewStyleAdaptive) { final s1 = width / displayWidth; final s2 = height / displayHeight; s = s1 < s2 ? s1 : s2; @@ -543,6 +549,9 @@ class CanvasModel with ChangeNotifier { double get y => _y; double get scale => _scale; ScrollStyle get scrollStyle => _scrollStyle; + ViewStyle get viewStyle => _lastViewStyle; + + _resetScroll() => setScrollPercent(0.0, 0.0); setScrollPercent(double x, double y) { _scrollX = x; @@ -571,6 +580,9 @@ class CanvasModel with ChangeNotifier { if (_lastViewStyle == viewStyle) { return; } + if (_lastViewStyle.style != viewStyle.style) { + _resetScroll(); + } _lastViewStyle = viewStyle; _scale = viewStyle.scale; _x = (sizeWidth - displayWidth * _scale) / 2; @@ -582,8 +594,7 @@ class CanvasModel with ChangeNotifier { final style = await bind.sessionGetScrollStyle(id: id); if (style == kRemoteScrollStyleBar) { _scrollStyle = ScrollStyle.scrollbar; - _scrollX = 0.0; - _scrollY = 0.0; + _resetScroll(); } else { _scrollStyle = ScrollStyle.scrollauto; } From a231647bd67772477e75083e6e7008c96bd66c8c Mon Sep 17 00:00:00 2001 From: ilGigioVr88 Date: Fri, 30 Dec 2022 09:42:07 +0100 Subject: [PATCH 1312/2015] 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 38015f56c..6f4555867 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -401,12 +401,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", "Permetti di nascondere solo se si accettano sessioni con password permanente"), ("wayland_experiment_tip", "Il supporto Wayland è in fase sperimentale, utilizza X11 se necessiti di un accesso stabile."), ("Right click to select tabs", "Clic con il tasto destro per selezionare le schede"), - ("Skipped", ""), + ("Skipped", "Saltato"), ("Add to Address Book", "Aggiungi alla rubrica"), ("Group", "Gruppo"), ("Search", "Cerca"), ("Closed manually by the web console", "Chiudi manualmente dalla console Web"), - ("Local keyboard type", ""), - ("Select local keyboard type", ""), + ("Local keyboard type", "Tipo tastiera locale"), + ("Select local keyboard type", "Seleziona la tastiera locale"), ].iter().cloned().collect(); } From aec50be4107a79734cc5b4b60facd76465302504 Mon Sep 17 00:00:00 2001 From: NicKoehler <53040044+NicKoehler@users.noreply.github.com> Date: Fri, 30 Dec 2022 09:44:46 +0100 Subject: [PATCH 1313/2015] Update it.rs --- src/lang/it.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/it.rs b/src/lang/it.rs index 38015f56c..a0bd211da 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -406,7 +406,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Group", "Gruppo"), ("Search", "Cerca"), ("Closed manually by the web console", "Chiudi manualmente dalla console Web"), - ("Local keyboard type", ""), - ("Select local keyboard type", ""), + ("Local keyboard type", "Tipo di tastiera locale"), + ("Select local keyboard type", "Seleziona il tipo di tastiera locale"), ].iter().cloned().collect(); } From b8936ddbbe9c504af9f44776831109621a3b913b Mon Sep 17 00:00:00 2001 From: Mr-Update <37781396+Mr-Update@users.noreply.github.com> Date: Fri, 30 Dec 2022 12:13:14 +0100 Subject: [PATCH 1314/2015] Update de.rs --- src/lang/de.rs | 52 +++++++++++++++++++++++++------------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/src/lang/de.rs b/src/lang/de.rs index 607fd13d8..5d9496550 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -19,11 +19,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recent Sessions", "Letzte Sitzungen"), ("Address Book", "Adressbuch"), ("Confirmation", "Bestätigung"), - ("TCP Tunneling", "TCP Tunneln"), + ("TCP Tunneling", "TCP-Tunnelung"), ("Remove", "Entfernen"), ("Refresh random password", "Zufälliges Passwort erzeugen"), ("Set your own password", "Eigenes Passwort setzen"), - ("Enable Keyboard/Mouse", "Tastatur/Maus aktivieren"), + ("Enable Keyboard/Mouse", "Tastatur und Maus aktivieren"), ("Enable Clipboard", "Zwischenablage aktivieren"), ("Enable File Transfer", "Dateiübertragung aktivieren"), ("Enable TCP Tunneling", "TCP-Tunnel aktivieren"), @@ -113,7 +113,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Original", "Original"), ("Shrink", "Verkleinern"), ("Stretch", "Strecken"), - ("Scrollbar", "Scrollleiste"), + ("Scrollbar", "Scroll-Leiste"), ("ScrollAuto", "Automatisch scrollen"), ("Good image quality", "Hohe Bildqualität"), ("Balanced", "Ausgeglichen"), @@ -128,7 +128,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Refresh", "Aktualisieren"), ("ID does not exist", "Diese ID existiert nicht"), ("Failed to connect to rendezvous server", "Verbindung zum Vermittlungsserver fehlgeschlagen"), - ("Please try later", "Bitte versuchen Sie es später erneut"), + ("Please try later", "Bitte versuchen Sie es später erneut."), ("Remote desktop is offline", "Entfernter PC ist offline"), ("Key mismatch", "Schlüssel stimmt nicht überein"), ("Timeout", "Zeitüberschreitung"), @@ -138,20 +138,20 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to make direct connection to remote desktop", "Direkte Verbindung zum entfernten PC fehlgeschlagen"), ("Set Password", "Passwort festlegen"), ("OS Password", "Betriebssystem-Passwort"), - ("install_tip", "Aufgrund der Benutzerkontensteuerung (UAC) kann RustDesk in manchen Fällen nicht ordnungsgemäß funktionieren. Um die Benutzerkontensteuerung zu umgehen, klicken Sie bitte auf die Schaltfläche unten, um RustDesk auf dem System zu installieren"), + ("install_tip", "Aufgrund der Benutzerkontensteuerung (UAC) kann RustDesk in manchen Fällen nicht ordnungsgemäß funktionieren. Um die Benutzerkontensteuerung zu umgehen, klicken Sie bitte auf die Schaltfläche unten, um RustDesk auf dem System zu installieren."), ("Click to upgrade", "Upgrade"), ("Click to download", "Zum Herunterladen klicken"), ("Click to update", "Update"), ("Configure", "Konfigurieren"), ("config_acc", "Um Ihren PC aus der Ferne zu steuern, müssen Sie RustDesk Zugriffsrechte erteilen."), - ("config_screen", "Um aus der Ferne auf Ihren PC zugreifen zu können, müssen Sie RustDesk \"Bildschirm-Aufnahme\"-Berechtigung erteilen."), + ("config_screen", "Um aus der Ferne auf Ihren PC zugreifen zu können, müssen Sie RustDesk die Berechtigung \"Bildschirmaufnahme\" erteilen."), ("Installing ...", "Installiere..."), ("Install", "Installieren"), ("Installation", "Installation"), ("Installation Path", "Installationspfad"), ("Create start menu shortcuts", "Verknüpfung im Startmenü erstellen"), ("Create desktop icon", "Desktop-Verknüpfung erstellen"), - ("agreement_tip", "Durch die Installation akzeptieren Sie die Lizenzvereinbarung"), + ("agreement_tip", "Durch die Installation akzeptieren Sie die Lizenzvereinbarung."), ("Accept and Install", "Akzeptieren und Installieren"), ("End-user license agreement", "Lizenzvereinbarung für Endbenutzer"), ("Generating ...", "Wird generiert..."), @@ -163,7 +163,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Action", "Aktion"), ("Add", "Hinzufügen"), ("Local Port", "Lokaler Port"), - ("Local Address", "Lokale Addresse"), + ("Local Address", "Lokale Adresse"), ("Change Local Port", "Lokalen Port ändern"), ("setup_server_tip", "für eine schnellere Verbindung richten Sie bitte Ihren eigenen Verbindungsserver ein."), ("Too short, at least 6 characters.", "Zu kurz, mindestens 6 Zeichen."), @@ -185,7 +185,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enter your password", "Geben Sie Ihr Passwort ein"), ("Logging in...", "Anmelden..."), ("Enable RDP session sharing", "RDP-Sitzungsfreigabe aktivieren"), - ("Auto Login", "Automatisch anmelden (nur gültig, wenn Sie \"Sperren nach Sitzungsende\" aktiviert haben)"), + ("Auto Login", "Automatisch anmelden (nur gültig, wenn Sie \"Nach Sitzungsende sperren\" aktiviert haben)"), ("Enable Direct IP Access", "Direkten IP-Zugang aktivieren"), ("Rename", "Umbenennen"), ("Space", "Speicherplatz"), @@ -195,7 +195,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please enter the folder name", "Bitte geben Sie den Ordnernamen ein"), ("Fix it", "Reparieren"), ("Warning", "Warnung"), - ("Login screen using Wayland is not supported", "Anmeldebildschirm wird mit Wayland nicht unterstützt"), + ("Login screen using Wayland is not supported", "Anmeldebildschirm mit Wayland wird nicht unterstützt"), ("Reboot required", "Neustart erforderlich"), ("Unsupported display server ", "Nicht unterstützter Display-Server"), ("x11 expected", "X11 erwartet"), @@ -208,7 +208,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Run without install", "Ohne Installation ausführen"), ("Always connected via relay", "Immer über Relay-Server verbunden"), ("Always connect via relay", "Immer über Relay-Server verbinden"), - ("whitelist_tip", "Nur IPs auf der Whitelist können zugreifen"), + ("whitelist_tip", "Nur IPs auf der Whitelist können zugreifen."), ("Login", "Anmelden"), ("Logout", "Abmelden"), ("Tags", "Schlagworte"), @@ -229,10 +229,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Remove from Favorites", "Aus Favoriten entfernen"), ("Empty", "Keine Einträge"), ("Invalid folder name", "Ungültiger Ordnername"), - ("Socks5 Proxy", "Socks5 Proxy"), + ("Socks5 Proxy", "SOCKS5-Proxy"), ("Hostname", "Hostname"), ("Discovered", "Im LAN erkannt"), - ("install_daemon_tip", "Um mit System zu starten, muss der Systemdienst installiert sein"), + ("install_daemon_tip", "Um mit System zu starten, muss der Systemdienst installiert sein."), ("Remote ID", "Entfernte ID"), ("Paste", "Einfügen"), ("Paste here?", "Hier einfügen?"), @@ -274,7 +274,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Open System Setting", "Systemeinstellung öffnen"), ("How to get Android input permission?", "Wie erhalte ich eine Android-Eingabeberechtigung?"), ("android_input_permission_tip1", "Damit ein Remote-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 und geben Sie [Installierte Dienste] ein, 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."), @@ -299,11 +299,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Out privacy mode", "Datenschutzmodus deaktivieren"), ("Language", "Sprache"), ("Keep RustDesk background service", "RustDesk im Hintergrund ausführen"), - ("Ignore Battery Optimizations", "Batterieoptimierung ignorieren"), - ("android_open_battery_optimizations_tip", "Möchten Sie die Einstellungen zur Batterieopimierung öffnen?"), + ("Ignore Battery Optimizations", "Akkuoptimierung ignorieren"), + ("android_open_battery_optimizations_tip", "Möchten Sie die Einstellungen zur Akkuoptimierung öffnen?"), ("Connection not allowed", "Verbindung abgelehnt"), ("Legacy mode", "Kompatibilitätsmodus"), - ("Map mode", ""), //Muss noch angepasst wer"), + ("Map mode", "Kartenmodus"), ("Translate mode", "Übersetzungsmodus"), ("Use permanent password", "Permanentes Passwort verwenden"), ("Use both passwords", "Beide Passwörter verwenden"), @@ -330,16 +330,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Relay Connection", "Relaisverbindung"), ("Secure Connection", "Sichere Verbindung"), ("Insecure Connection", "Unsichere Verbindung"), - ("Scale original", "Keine Saklierung"), - ("Scale adaptive", "Automatische Saklierung"), + ("Scale original", "Keine Skalierung"), + ("Scale adaptive", "Automatische Skalierung"), ("General", "Allgemein"), ("Security", "Sicherheit"), ("Account", "Konto"), ("Theme", "Farbgebung"), - ("Dark Theme", "dunkle Farbgebung"), + ("Dark Theme", "Dunkle Farbgebung"), ("Dark", "Dunkel"), ("Light", "Hell"), - ("Follow System", "System-Standard"), + ("Follow System", "Systemstandard"), ("Enable hardware codec", "Hardware-Codec aktivieren"), ("Unlock Security Settings", "Sicherheitseinstellungen entsperren"), ("Enable Audio", "Audio aktivieren"), @@ -371,7 +371,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Write a message", "Nachricht schreiben"), ("Prompt", "Meldung"), ("Please wait for confirmation of UAC...", "Bitte auf die Bestätigung des Nutzers warten..."), - ("elevated_foreground_window_tip", "Das aktuell geöffnete Fenster des ferngesteuerten Computers benötigt höhere Rechte. Deshalb ist es derzeit nicht möglich, die Maus und die Tastatur zu verwenden. Bitten Sie den Nutzer, dessen Computer Sie fernsteuern, das Fenster zu minimieren oder die Rechte zu erhöhen. Um dieses Problem zunünftig zu vermeiden, wird empfohlen, die Software auf dem ferngesteuerten Computer zu installieren."), + ("elevated_foreground_window_tip", "Das aktuell geöffnete Fenster des ferngesteuerten Computers benötigt höhere Rechte. Deshalb ist es derzeit nicht möglich, die Maus und die Tastatur zu verwenden. Bitten Sie den Nutzer, dessen Computer Sie fernsteuern, das Fenster zu minimieren oder die Rechte zu erhöhen. Um dieses Problem zukünftig zu vermeiden, wird empfohlen, die Software auf dem ferngesteuerten Computer zu installieren."), ("Disconnected", "Verbindung abgebrochen"), ("Other", "Weitere Einstellungen"), ("Confirm before closing multiple tabs", "Nachfragen, wenn mehrere Tabs geschlossen werden"), @@ -391,14 +391,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Zoom cursor", "Cursor zoomen"), ("Accept sessions via password", "Sitzung mit Passwort bestätigen"), ("Accept sessions via click", "Sitzung mit einem Klick bestätigen"), - ("Accept sessions via both", "Sitzung durch Klick und Passwort bestätigen"), - ("Please wait for the remote side to accept your session request...", "Bitte warten Sie auf die Gegenstelle, dass diese Ihre Sitzungsanfrage bestätigt..."), + ("Accept sessions via both", "Sitzung mit Klick und Passwort bestätigen"), + ("Please wait for the remote side to accept your session request...", "Bitte warten Sie, bis die Gegenseite Ihre Sitzungsanfrage akzeptiert hat..."), ("One-time Password", "Einmalpasswort"), ("Use one-time password", "Einmalpasswort verwenden"), ("One-time password length", "Länge des Einmalpassworts"), ("Request access to your device", "Zugriff zu Ihrem Gerät erbitten"), ("Hide connection management window", "Fenster zur Verwaltung der Verbindung verstecken"), - ("hide_cm_tip", "Dies ist nur möglich, wenn der Zugriff nur über ein permanentes Passwort erfolgt."), + ("hide_cm_tip", "Dies ist nur möglich, wenn der Zugriff über ein permanentes Passwort erfolgt."), ("wayland_experiment_tip", "Die Unterstützung von Wayland ist nur experimentell. Bitte nutzen Sie X11, wenn Sie einen unbeaufsichtigten Zugriff benötigen."), ("Right click to select tabs", "Register mit rechtem Mausklick auswählen"), ("Skipped", "Übersprungen"), @@ -407,6 +407,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Search", "Suchen"), ("Closed manually by the web console", "Manuell über die Webkonsole beendet"), ("Local keyboard type", "Lokaler Tastaturtyp"), - ("Select local keyboard type", "Lokalen Tastaturtyp Auswählen"), + ("Select local keyboard type", "Lokalen Tastaturtyp auswählen"), ].iter().cloned().collect(); } From c144dfb81be51f2da8006be4585faf78a9c8c919 Mon Sep 17 00:00:00 2001 From: solokot Date: Sat, 31 Dec 2022 09:26:54 +0300 Subject: [PATCH 1315/2015] 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 78cb14980..82715758b 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -401,12 +401,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", "Разрешать скрытие случае, если принимаются сеансы по паролю или используется постоянный пароль"), ("wayland_experiment_tip", "Поддержка Wayland находится на экспериментальной стадии, используйте X11, если вам требуется автоматический доступ."), ("Right click to select tabs", "Выбор вкладок щелчком правой кнопки мыши"), - ("Skipped", ""), + ("Skipped", "Пропущено"), ("Add to Address Book", "Добавить в адресную книгу"), ("Group", "Группа"), ("Search", "Поиск"), - ("Closed manually by the web console", ""), - ("Local keyboard type", ""), - ("Select local keyboard type", ""), + ("Closed manually by the web console", "Закрыто вручную через веб-консоль"), + ("Local keyboard type", "Тип локальной клавиатуры"), + ("Select local keyboard type", "Выберите тип локальной клавиатуры"), ].iter().cloned().collect(); } From 68e0f336cb0f5fb075603e2bce1124a103a6e942 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 30 Dec 2022 22:57:22 -0800 Subject: [PATCH 1316/2015] fix: macos crash on get interfaces --- Cargo.lock | 3 +-- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b0b29503f..45b7c1eb5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1317,8 +1317,7 @@ checksum = "f578e8e2c440e7297e008bb5486a3a8a194775224bbc23729b0dbdfaeebf162e" [[package]] name = "default-net" version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05e70d471b0ba4e722c85651b3bb04b6880dfdb1224a43ade80c1295314db646" +source = "git+https://github.com/Kingtous/default-net#bdaad8dd5b08efcba303e71729d3d0b1d5ccdb25" dependencies = [ "libc", "memalloc", diff --git a/Cargo.toml b/Cargo.toml index 4e2437185..82c35de79 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 = "0.11.0" +default-net = { git = "https://github.com/Kingtous/default-net" } wol-rs = "0.9.1" flutter_rust_bridge = { git = "https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge", optional = true } errno = "0.2.8" From b114ebf350c7cd3643782792e5d06bc1b9d8891f Mon Sep 17 00:00:00 2001 From: fufesou Date: Sat, 31 Dec 2022 21:41:16 +0800 Subject: [PATCH 1317/2015] fix some misspellings Signed-off-by: fufesou --- flutter/lib/desktop/pages/remote_page.dart | 4 +-- .../lib/desktop/pages/remote_tab_page.dart | 2 +- .../widgets/kb_layout_type_chooser.dart | 6 ++-- .../lib/desktop/widgets/remote_menubar.dart | 2 +- flutter/lib/mobile/pages/remote_page.dart | 6 ++-- flutter/lib/models/model.dart | 10 +++--- libs/hbb_common/protos/message.proto | 4 +-- libs/scrap/src/common/mod.rs | 8 ++--- libs/scrap/src/common/wayland.rs | 2 +- libs/scrap/src/common/x11.rs | 2 +- src/client/io_loop.rs | 2 +- src/flutter.rs | 6 ++-- src/server.rs | 2 +- src/server/connection.rs | 6 ++-- src/server/video_service.rs | 34 ++++++++++--------- src/server/wayland.rs | 2 +- src/ui/header.tis | 6 ++-- src/ui/remote.rs | 6 ++-- src/ui/remote.tis | 10 +++--- src/ui_session_interface.rs | 4 +-- 20 files changed, 63 insertions(+), 61 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 6ca711fda..102dc784a 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -315,7 +315,7 @@ class _RemotePageState extends State })) ]; - if (!_ffi.canvasModel.cursorEmbeded) { + if (!_ffi.canvasModel.cursorEmbedded) { paints.add(Obx(() => Offstage( offstage: _showRemoteCursor.isFalse || _remoteCursorMoved.isFalse, child: CursorPaint( @@ -382,7 +382,7 @@ class _ImagePaintState extends State { mouseRegion({child}) => Obx(() => MouseRegion( cursor: cursorOverImage.isTrue - ? c.cursorEmbeded + ? c.cursorEmbedded ? SystemMouseCursors.none : keyboardEnabled.isTrue ? (() { diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index 713c3d13c..604787290 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -257,7 +257,7 @@ class _ConnectionTabPageState extends State { ), ]); - if (!ffi.canvasModel.cursorEmbeded) { + if (!ffi.canvasModel.cursorEmbedded) { menu.add(MenuEntryDivider()); menu.add(() { final state = ShowRemoteCursorState.find(key); diff --git a/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart b/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart index 6601160a7..cfbdb0c4e 100644 --- a/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart +++ b/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart @@ -9,7 +9,7 @@ import '../../common.dart'; typedef KBChoosedCallback = Future Function(String); const double _kImageMarginVertical = 6.0; -const double _kImageMarginHorizental = 10.0; +const double _kImageMarginHorizontal = 10.0; const double _kImageBoarderWidth = 4.0; const double _kImagePaddingWidth = 4.0; const Color _kImageBorderColor = Color.fromARGB(125, 202, 247, 2); @@ -47,14 +47,14 @@ class _KBImage extends StatelessWidget { ), ), margin: EdgeInsets.symmetric( - horizontal: _kImageMarginHorizental, + horizontal: _kImageMarginHorizontal, vertical: _kImageMarginVertical, ), padding: EdgeInsets.all(_kImagePaddingWidth), child: SvgPicture.asset( 'assets/${_kKBLayoutImageMap[kbLayoutType] ?? ""}.svg', width: imageWidth - - _kImageMarginHorizental * 2 - + _kImageMarginHorizontal * 2 - _kImagePaddingWidth * 2 - _kImageBoarderWidth * 2, ), diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 78deb7a38..244822e19 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -1086,7 +1086,7 @@ class _RemoteMenubarState extends State { } /// Show remote cursor - if (!widget.ffi.canvasModel.cursorEmbeded) { + if (!widget.ffi.canvasModel.cursorEmbedded) { displayMenu.add(() { final state = ShowRemoteCursorState.find(widget.id); return MenuEntrySwitch2( diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index c1db230bb..97ce6268d 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -518,7 +518,7 @@ class _RemotePageState extends State { ), ), ]; - if (!gFFI.canvasModel.cursorEmbeded) { + if (!gFFI.canvasModel.cursorEmbedded) { paints.add(CursorPaint()); } return paints; @@ -527,7 +527,7 @@ class _RemotePageState extends State { Widget getBodyForDesktopWithListener(bool keyboard) { var paints = [ImagePaint()]; - if (!gFFI.canvasModel.cursorEmbeded) { + if (!gFFI.canvasModel.cursorEmbedded) { final cursor = bind.sessionGetToggleOptionSync( id: widget.id, arg: 'show-remote-cursor'); if (keyboard || cursor) { @@ -1058,7 +1058,7 @@ void showOptions( final toggles = [ getToggle(id, setState, 'show-quality-monitor', 'Show quality monitor'), ]; - if (!gFFI.canvasModel.cursorEmbeded) { + if (!gFFI.canvasModel.cursorEmbedded) { toggles.insert(0, getToggle(id, setState, 'show-remote-cursor', 'Show remote cursor')); } diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index e0e88f08e..0f7099bc8 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -222,7 +222,7 @@ class FfiModel with ChangeNotifier { _display.y = double.parse(evt['y']); _display.width = int.parse(evt['width']); _display.height = int.parse(evt['height']); - _display.cursorEmbeded = int.parse(evt['cursor_embeded']) == 1; + _display.cursorEmbedded = int.parse(evt['cursor_embedded']) == 1; if (old != _pi.currentDisplay) { parent.target?.cursorModel.updateDisplayOrigin(_display.x, _display.y); } @@ -338,7 +338,7 @@ class FfiModel with ChangeNotifier { d.y = d0['y'].toDouble(); d.width = d0['width']; d.height = d0['height']; - d.cursorEmbeded = d0['cursor_embeded'] == 1; + d.cursorEmbedded = d0['cursor_embedded'] == 1; _pi.displays.add(d); } if (_pi.currentDisplay < _pi.displays.length) { @@ -608,8 +608,8 @@ class CanvasModel with ChangeNotifier { notifyListeners(); } - bool get cursorEmbeded => - parent.target?.ffiModel.display.cursorEmbeded ?? false; + bool get cursorEmbedded => + parent.target?.ffiModel.display.cursorEmbedded ?? false; int getDisplayWidth() { final defaultWidth = (isDesktop || isWebDesktop) @@ -1343,7 +1343,7 @@ class Display { double y = 0; int width = 0; int height = 0; - bool cursorEmbeded = false; + bool cursorEmbedded = false; Display() { width = (isDesktop || isWebDesktop) diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index 9217388aa..650e42104 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -40,7 +40,7 @@ message DisplayInfo { int32 height = 4; string name = 5; bool online = 6; - bool cursor_embeded = 7; + bool cursor_embedded = 7; } message PortForward { @@ -420,7 +420,7 @@ message SwitchDisplay { sint32 y = 3; int32 width = 4; int32 height = 5; - bool cursor_embeded = 6; + bool cursor_embedded = 6; } message PermissionInfo { diff --git a/libs/scrap/src/common/mod.rs b/libs/scrap/src/common/mod.rs index 0e2f998df..1e78656cf 100644 --- a/libs/scrap/src/common/mod.rs +++ b/libs/scrap/src/common/mod.rs @@ -72,16 +72,16 @@ pub fn is_x11() -> bool { #[cfg(x11)] #[inline] -pub fn is_cursor_embeded() -> bool { +pub fn is_cursor_embedded() -> bool { if is_x11() { - x11::IS_CURSOR_EMBEDED + x11::is_cursor_embedded } else { - wayland::IS_CURSOR_EMBEDED + wayland::is_cursor_embedded } } #[cfg(not(x11))] #[inline] -pub fn is_cursor_embeded() -> bool { +pub fn is_cursor_embedded() -> bool { false } diff --git a/libs/scrap/src/common/wayland.rs b/libs/scrap/src/common/wayland.rs index 6e89568e1..99752fa7f 100644 --- a/libs/scrap/src/common/wayland.rs +++ b/libs/scrap/src/common/wayland.rs @@ -4,7 +4,7 @@ use std::{io, sync::RwLock, time::Duration}; pub struct Capturer(Display, Box, bool, Vec); -pub const IS_CURSOR_EMBEDED: bool = true; +pub const is_cursor_embedded: bool = true; lazy_static::lazy_static! { static ref MAP_ERR: RwLock io::Error>> = Default::default(); diff --git a/libs/scrap/src/common/x11.rs b/libs/scrap/src/common/x11.rs index dacc265ff..138ab5000 100644 --- a/libs/scrap/src/common/x11.rs +++ b/libs/scrap/src/common/x11.rs @@ -3,7 +3,7 @@ use std::{io, ops, time::Duration}; pub struct Capturer(x11::Capturer); -pub const IS_CURSOR_EMBEDED: bool = false; +pub const is_cursor_embedded: bool = false; impl Capturer { pub fn new(display: Display, yuv: bool) -> io::Result { diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index ceddbc004..1f81dfa55 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -990,7 +990,7 @@ impl Remote { self.video_sender.send(MediaData::Reset).ok(); if s.width > 0 && s.height > 0 { self.handler - .set_display(s.x, s.y, s.width, s.height, s.cursor_embeded); + .set_display(s.x, s.y, s.width, s.height, s.cursor_embedded); } } Some(misc::Union::CloseReason(c)) => { diff --git a/src/flutter.rs b/src/flutter.rs index 4798820e7..3036ca9b3 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -156,7 +156,7 @@ impl InvokeUiSession for FlutterHandler { } /// unused in flutter, use switch_display or set_peer_info - fn set_display(&self, _x: i32, _y: i32, _w: i32, _h: i32, _cursor_embeded: bool) {} + fn set_display(&self, _x: i32, _y: i32, _w: i32, _h: i32, _cursor_embedded: bool) {} fn update_privacy_mode(&self) { self.push_event("update_privacy_mode", [].into()); @@ -296,7 +296,7 @@ impl InvokeUiSession for FlutterHandler { h.insert("y", d.y); h.insert("width", d.width); h.insert("height", d.height); - h.insert("cursor_embeded", if d.cursor_embeded { 1 } else { 0 }); + 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()); @@ -357,7 +357,7 @@ impl InvokeUiSession for FlutterHandler { ("y", &display.y.to_string()), ("width", &display.width.to_string()), ("height", &display.height.to_string()), - ("cursor_embeded", &{if display.cursor_embeded {1} else {0}}.to_string()), + ("cursor_embedded", &{if display.cursor_embedded {1} else {0}}.to_string()), ], ); } diff --git a/src/server.rs b/src/server.rs index 8f4dcccdc..5c020261f 100644 --- a/src/server.rs +++ b/src/server.rs @@ -85,7 +85,7 @@ pub fn new() -> ServerPtr { #[cfg(not(any(target_os = "android", target_os = "ios")))] { server.add_service(Box::new(clipboard_service::new())); - if !video_service::capture_cursor_embeded() { + if !video_service::capture_cursor_embedded() { server.add_service(Box::new(input_service::new_cursor())); server.add_service(Box::new(input_service::new_pos())); } diff --git a/src/server/connection.rs b/src/server/connection.rs index b569ef708..919aeae99 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -400,7 +400,7 @@ impl Connection { }, Some((instant, value)) = rx_video.recv() => { if !conn.video_ack_required { - video_service::notify_video_frame_feched(id, Some(instant.into())); + video_service::notify_video_frame_fetched(id, Some(instant.into())); } if let Err(err) = conn.stream.send(&value as &Message).await { conn.on_close(&err.to_string(), false).await; @@ -499,7 +499,7 @@ impl Connection { } else if video_privacy_conn_id == 0 { let _ = privacy_mode::turn_off_privacy(0); } - video_service::notify_video_frame_feched(id, None); + video_service::notify_video_frame_fetched(id, None); scrap::codec::Encoder::update_video_encoder(id, scrap::codec::EncoderUpdate::Remove); video_service::VIDEO_QOS.lock().unwrap().reset(); if conn.authorized { @@ -1464,7 +1464,7 @@ impl Connection { } } Some(misc::Union::VideoReceived(_)) => { - video_service::notify_video_frame_feched( + video_service::notify_video_frame_fetched( self.inner.id, Some(Instant::now().into()), ); diff --git a/src/server/video_service.rs b/src/server/video_service.rs index 6d1235ed8..b986c785c 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -78,11 +78,11 @@ fn is_capturer_mag_supported() -> bool { false } -pub fn capture_cursor_embeded() -> bool { - scrap::is_cursor_embeded() +pub fn capture_cursor_embedded() -> bool { + scrap::is_cursor_embedded() } -pub fn notify_video_frame_feched(conn_id: i32, frame_tm: Option) { +pub fn notify_video_frame_fetched(conn_id: i32, frame_tm: Option) { FRAME_FETCHED_NOTIFIER.0.send((conn_id, frame_tm)).unwrap() } @@ -146,7 +146,7 @@ impl VideoFrameController { fetched_conn_ids.insert(id); } Ok(None) => { - // this branch would nerver be reached + // this branch would never be reached } } } @@ -162,7 +162,7 @@ fn check_display_changed( last_n: usize, last_current: usize, last_width: usize, - last_hegiht: usize, + last_height: usize, ) -> bool { #[cfg(target_os = "linux")] { @@ -187,7 +187,7 @@ fn check_display_changed( if i != last_current { return true; }; - if d.width() != last_width || d.height() != last_hegiht { + if d.width() != last_width || d.height() != last_height { return true; }; } @@ -249,7 +249,7 @@ fn create_capturer( PRIVACY_WINDOW_NAME ); } - log::debug!("Create maginifier capture for {}", privacy_mode_id); + log::debug!("Create magnifier capture for {}", privacy_mode_id); c = Some(Box::new(c1)); } Err(e) => { @@ -385,10 +385,12 @@ fn get_capturer(use_yuv: bool, portable_service_running: bool) -> ResultType ResultType<()> { #[cfg(windows)] ensure_close_virtual_device()?; - // ensure_inited() is needed because release_resouce() may be called. + // ensure_inited() is needed because release_resource() may be called. #[cfg(target_os = "linux")] super::wayland::ensure_inited()?; #[cfg(windows)] @@ -464,7 +466,7 @@ fn run(sp: GenericService) -> ResultType<()> { y: c.origin.1 as _, width: c.width as _, height: c.height as _, - cursor_embeded: capture_cursor_embeded(), + cursor_embedded: capture_cursor_embedded(), ..Default::default() }); let mut msg_out = Message::new(); @@ -599,7 +601,7 @@ fn run(sp: GenericService) -> ResultType<()> { would_block_count += 1; if !scrap::is_x11() { if would_block_count >= 100 { - super::wayland::release_resouce(); + super::wayland::release_resource(); bail!("Wayland capturer none 100 times, try restart captuere"); } } @@ -653,7 +655,7 @@ fn run(sp: GenericService) -> ResultType<()> { #[cfg(target_os = "linux")] if !scrap::is_x11() { - super::wayland::release_resouce(); + super::wayland::release_resource(); } Ok(()) @@ -821,7 +823,7 @@ pub(super) fn get_displays_2(all: &Vec) -> (usize, Vec) { height: d.height() as _, name: d.name(), online: d.is_online(), - cursor_embeded: false, + cursor_embedded: false, ..Default::default() }); } diff --git a/src/server/wayland.rs b/src/server/wayland.rs index fdf9bccec..ce79c55d2 100644 --- a/src/server/wayland.rs +++ b/src/server/wayland.rs @@ -129,7 +129,7 @@ pub(super) async fn check_init() -> ResultType<()> { let num = all.len(); let (primary, mut displays) = super::video_service::get_displays_2(&all); for display in displays.iter_mut() { - display.cursor_embeded = true; + display.cursor_embedded = true; } let mut rects: Vec<((i32, i32), usize, usize)> = Vec::new(); diff --git a/src/ui/header.tis b/src/ui/header.tis index d1bb91cb9..dd0b35541 100644 --- a/src/ui/header.tis +++ b/src/ui/header.tis @@ -164,10 +164,10 @@ class Header: Reactor.Component { var codecs = handler.supported_hwcodec(); var show_codec = handler.has_hwcodec() && (codecs[0] || codecs[1]); - var cursor_embeded = false; + var cursor_embedded = false; if ((pi.displays || []).length > 0) { if (pi.displays.length > pi.current_display) { - cursor_embeded = pi.displays[pi.current_display].cursor_embeded; + cursor_embedded = pi.displays[pi.current_display].cursor_embedded; } } @@ -191,7 +191,7 @@ class Header: Reactor.Component { {codecs[1] ?
  • {svg_checkmark}H265
  • : ""}
    : ""}
    - {!cursor_embeded &&
  • {svg_checkmark}{translate('Show remote cursor')}
  • } + {!cursor_embedded &&
  • {svg_checkmark}{translate('Show remote cursor')}
  • }
  • {svg_checkmark}{translate('Show quality monitor')}
  • {audio_enabled ?
  • {svg_checkmark}{translate('Mute')}
  • : ""} {is_win && pi.platform == 'Windows' && file_enabled ?
  • {svg_checkmark}{translate('Allow file copy and paste')}
  • : ""} diff --git a/src/ui/remote.rs b/src/ui/remote.rs index df5d98038..1f3d5f7ec 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -79,8 +79,8 @@ impl InvokeUiSession for SciterHandler { } } - fn set_display(&self, x: i32, y: i32, w: i32, h: i32, cursor_embeded: bool) { - self.call("setDisplay", &make_args!(x, y, w, h, cursor_embeded)); + fn set_display(&self, x: i32, y: i32, w: i32, h: i32, cursor_embedded: bool) { + self.call("setDisplay", &make_args!(x, y, w, h, cursor_embedded)); // https://sciter.com/forums/topic/color_spaceiyuv-crash // Nothing spectacular in decoder – done on CPU side. // So if you can do BGRA translation on your side – the better. @@ -223,7 +223,7 @@ impl InvokeUiSession for SciterHandler { display.set_item("y", d.y); display.set_item("width", d.width); display.set_item("height", d.height); - display.set_item("cursor_embeded", d.cursor_embeded); + display.set_item("cursor_embedded", d.cursor_embedded); displays.push(display); } pi_sciter.set_item("displays", displays); diff --git a/src/ui/remote.tis b/src/ui/remote.tis index 36f997540..63df0cb09 100644 --- a/src/ui/remote.tis +++ b/src/ui/remote.tis @@ -6,7 +6,7 @@ var display_width = 0; var display_height = 0; var display_origin_x = 0; var display_origin_y = 0; -var display_cursor_embeded = false; +var display_cursor_embedded = false; var display_scale = 1; var keyboard_enabled = true; // server side var clipboard_enabled = true; // server side @@ -16,12 +16,12 @@ var restart_enabled = true; // server side var recording_enabled = true; // server side var scroll_body = $(body); -handler.setDisplay = function(x, y, w, h, cursor_embeded) { +handler.setDisplay = function(x, y, w, h, cursor_embedded) { display_width = w; display_height = h; display_origin_x = x; display_origin_y = y; - display_cursor_embeded = cursor_embeded; + display_cursor_embedded = cursor_embedded; adaptDisplay(); if (recording) handler.record_screen(true, w, h); } @@ -197,7 +197,7 @@ function handler.onMouse(evt) dragging = false; break; case Event.MOUSE_MOVE: - if (display_cursor_embeded) { + if (display_cursor_embedded) { break; } if (cursor_img.style#display != "none" && keyboard_enabled) { @@ -365,7 +365,7 @@ function updateCursor(system=false) { } function refreshCursor() { - if (display_cursor_embeded) { + if (display_cursor_embedded) { cursor_img.style#display = "none"; return; } diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 63a8d8711..c66e1fa3b 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -604,7 +604,7 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { fn set_cursor_data(&self, cd: CursorData); fn set_cursor_id(&self, id: String); fn set_cursor_position(&self, cp: CursorPosition); - fn set_display(&self, x: i32, y: i32, w: i32, h: i32, cursor_embeded: bool); + 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 on_connected(&self, conn_type: ConnType); @@ -710,7 +710,7 @@ impl Interface for Session { current.y, current.width, current.height, - current.cursor_embeded, + current.cursor_embedded, ); } self.update_privacy_mode(); From 635105069a3374aeafd71273af98487b90db7fcf Mon Sep 17 00:00:00 2001 From: fufesou Date: Sat, 31 Dec 2022 22:03:15 +0800 Subject: [PATCH 1318/2015] fix build wayland Signed-off-by: fufesou --- src/server/wayland.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/wayland.rs b/src/server/wayland.rs index ce79c55d2..24b3be110 100644 --- a/src/server/wayland.rs +++ b/src/server/wayland.rs @@ -244,7 +244,7 @@ pub(super) fn get_display_num() -> ResultType { } #[allow(dead_code)] -pub(super) fn release_resouce() { +pub(super) fn release_resource() { if scrap::is_x11() { return; } From b429bfdef52117ec87f3a79dbe6e7092c8a1f7ac Mon Sep 17 00:00:00 2001 From: Mr-Update <37781396+Mr-Update@users.noreply.github.com> Date: Sun, 1 Jan 2023 23:33:52 +0100 Subject: [PATCH 1319/2015] Update de.rs --- src/lang/de.rs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/lang/de.rs b/src/lang/de.rs index 5d9496550..ddb2a45d2 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -12,7 +12,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Start Service", "Starte Vermittlungsdienst"), ("Service is running", "Vermittlungsdienst aktiv"), ("Service is not running", "Vermittlungsdienst deaktiviert"), - ("not_ready_status", "Nicht bereit. Bitte überprüfen Sie Ihre Netzwerkverbindung"), + ("not_ready_status", "Nicht bereit. Bitte überprüfen Sie Ihre Netzwerkverbindung."), ("Control Remote Desktop", "Entfernten PC steuern"), ("Transfer File", "Datei übertragen"), ("Connect", "Verbinden"), @@ -53,7 +53,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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"), + ("server_not_support", "Diese Funktion wird noch nicht vom Server unterstützt."), ("Not available", "Nicht verfügbar"), ("Too frequent", "Zu häufig"), ("Cancel", "Abbrechen"), @@ -62,13 +62,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Retry", "Erneut versuchen"), ("OK", "OK"), ("Password Required", "Passwort erforderlich"), - ("Please enter your password", "Bitte geben Sie das Passwort der Gegenstelle ein"), + ("Please enter your password", "Bitte geben Sie Ihr Passwort ein"), ("Remember password", "Passwort merken"), ("Wrong Password", "Falsches Passwort"), ("Do you want to enter again?", "Erneut verbinden?"), ("Connection Error", "Verbindungsfehler"), ("Error", "Fehler"), - ("Reset by the peer", "Verbindung wurde von der Gegenstelle zurückgesetzt"), + ("Reset by the peer", "Verbindung wurde von der Gegenstelle zurückgesetzt."), ("Connecting...", "Verbindung wird hergestellt..."), ("Connection in progress. Please wait.", "Die Verbindung wird hergestellt. Bitte warten..."), ("Please try 1 minute later", "Bitte versuchen Sie es später erneut"), @@ -94,7 +94,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Select All", "Alles auswählen"), ("Unselect All", "Alles abwählen"), ("Empty Directory", "Leerer Ordner"), - ("Not an empty directory", "Ordner ist nicht leer"), + ("Not an empty directory", "Ordner ist nicht leer."), ("Are you sure you want to delete this file?", "Sind Sie sicher, dass Sie diese Datei löschen wollen?"), ("Are you sure you want to delete this empty directory?", "Sind Sie sicher, dass Sie diesen leeren Ordner löschen möchten?"), ("Are you sure you want to delete the file of this directory?", "Sind Sie sicher, dass Sie die Datei dieses Ordners löschen möchten?"), @@ -126,11 +126,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Insert", "Einfügen"), ("Insert Lock", "Win+L (Sperren) senden"), ("Refresh", "Aktualisieren"), - ("ID does not exist", "Diese ID existiert nicht"), + ("ID does not exist", "Diese ID existiert nicht."), ("Failed to connect to rendezvous server", "Verbindung zum Vermittlungsserver fehlgeschlagen"), ("Please try later", "Bitte versuchen Sie es später erneut."), - ("Remote desktop is offline", "Entfernter PC ist offline"), - ("Key mismatch", "Schlüssel stimmt nicht überein"), + ("Remote desktop is offline", "Entfernter PC ist offline."), + ("Key mismatch", "Schlüssel stimmen nicht überein."), ("Timeout", "Zeitüberschreitung"), ("Failed to connect to relay server", "Verbindung zum Vermittlungsserver fehlgeschlagen"), ("Failed to connect via rendezvous server", "Verbindung über Vermittlungsserver ist fehlgeschlagen"), @@ -138,7 +138,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to make direct connection to remote desktop", "Direkte Verbindung zum entfernten PC fehlgeschlagen"), ("Set Password", "Passwort festlegen"), ("OS Password", "Betriebssystem-Passwort"), - ("install_tip", "Aufgrund der Benutzerkontensteuerung (UAC) kann RustDesk in manchen Fällen nicht ordnungsgemäß funktionieren. Um die Benutzerkontensteuerung zu umgehen, klicken Sie bitte auf die Schaltfläche unten, um RustDesk auf dem System zu installieren."), + ("install_tip", "Aufgrund der Benutzerkontensteuerung (UAC) kann RustDesk in manchen Fällen nicht ordnungsgemäß funktionieren. Um die Benutzerkontensteuerung zu umgehen, klicken Sie bitte auf die Schaltfläche unten und installieren RustDesk auf dem System."), ("Click to upgrade", "Upgrade"), ("Click to download", "Zum Herunterladen klicken"), ("Click to update", "Update"), @@ -157,7 +157,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Generating ...", "Wird generiert..."), ("Your installation is lower version.", "Ihre Version ist veraltet."), ("not_close_tcp_tip", "Schließen Sie dieses Fenster nicht, solange Sie den Tunnel benutzen."), - ("Listening ...", "Lausche..."), + ("Listening ...", "Lauschen..."), ("Remote Host", "Entfernter PC"), ("Remote Port", "Entfernter Port"), ("Action", "Aktion"), @@ -195,7 +195,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please enter the folder name", "Bitte geben Sie den Ordnernamen ein"), ("Fix it", "Reparieren"), ("Warning", "Warnung"), - ("Login screen using Wayland is not supported", "Anmeldebildschirm mit Wayland wird nicht unterstützt"), + ("Login screen using Wayland is not supported", "Anmeldebildschirm mit Wayland wird nicht unterstützt."), ("Reboot required", "Neustart erforderlich"), ("Unsupported display server ", "Nicht unterstützter Display-Server"), ("x11 expected", "X11 erwartet"), @@ -213,7 +213,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logout", "Abmelden"), ("Tags", "Schlagworte"), ("Search ID", "Suche ID"), - ("Current Wayland display server is not supported", "Der aktuelle Wayland-Anzeigeserver wird nicht unterstützt"), + ("Current Wayland display server is not supported", "Der aktuelle Wayland-Anzeigeserver wird nicht unterstützt."), ("whitelist_sep", "Getrennt durch Komma, Semikolon, Leerzeichen oder Zeilenumbruch"), ("Add ID", "ID hinzufügen"), ("Add Tag", "Stichwort hinzufügen"), From 97718b33a6e31cb062b03e6cd4fac2673939ee28 Mon Sep 17 00:00:00 2001 From: fufesou Date: Mon, 2 Jan 2023 13:11:21 +0800 Subject: [PATCH 1320/2015] remove expect Signed-off-by: fufesou --- Cargo.lock | 2 +- libs/enigo/Cargo.toml | 2 +- libs/enigo/src/lib.rs | 5 +++-- libs/enigo/src/linux/nix_impl.rs | 8 +++++++- libs/enigo/src/linux/xdo.rs | 5 +++-- 5 files changed, 15 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 45b7c1eb5..cdccce4cb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5386,7 +5386,7 @@ checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" [[package]] name = "tfc" version = "0.6.1" -source = "git+https://github.com/asur4s/The-Fat-Controller#48303c5dacded6ea1873bc5d69bdde3175cf336a" +source = "git+https://github.com/fufesou/The-Fat-Controller#48303c5dacded6ea1873bc5d69bdde3175cf336a" dependencies = [ "core-graphics 0.22.3", "unicode-segmentation", diff --git a/libs/enigo/Cargo.toml b/libs/enigo/Cargo.toml index 2c4070ed1..cc4173a97 100644 --- a/libs/enigo/Cargo.toml +++ b/libs/enigo/Cargo.toml @@ -23,7 +23,7 @@ serde = { version = "1.0", optional = true } serde_derive = { version = "1.0", optional = true } log = "0.4" rdev = { git = "https://github.com/fufesou/rdev" } -tfc = { git = "https://github.com/asur4s/The-Fat-Controller" } +tfc = { git = "https://github.com/fufesou/The-Fat-Controller" } hbb_common = { path = "../hbb_common" } [features] diff --git a/libs/enigo/src/lib.rs b/libs/enigo/src/lib.rs index a47f9558d..caa08bd55 100644 --- a/libs/enigo/src/lib.rs +++ b/libs/enigo/src/lib.rs @@ -447,8 +447,9 @@ pub trait KeyboardControllable { where Self: Sized, { - self.key_sequence_parse_try(sequence) - .expect("Could not parse sequence"); + if let Err(..) = self.key_sequence_parse_try(sequence) { + println!("Could not parse sequence"); + } } /// Same as key_sequence_parse except returns any errors fn key_sequence_parse_try(&mut self, sequence: &str) -> Result<(), dsl::ParseError> diff --git a/libs/enigo/src/linux/nix_impl.rs b/libs/enigo/src/linux/nix_impl.rs index 4eb890c29..47e6d53c0 100644 --- a/libs/enigo/src/linux/nix_impl.rs +++ b/libs/enigo/src/linux/nix_impl.rs @@ -88,7 +88,13 @@ impl Default for Enigo { Self { is_x11, tfc: if is_x11 { - Some(TFC_Context::new().expect("kbd context error")) + match TFC_Context::new() { + Ok(ctx) => Some(ctx), + Err(..) => { + println!("kbd context error"); + None + } + } } else { None }, diff --git a/libs/enigo/src/linux/xdo.rs b/libs/enigo/src/linux/xdo.rs index ed2d28dc1..204420adc 100644 --- a/libs/enigo/src/linux/xdo.rs +++ b/libs/enigo/src/linux/xdo.rs @@ -391,8 +391,9 @@ impl KeyboardControllable for EnigoXdo { where Self: Sized, { - self.key_sequence_parse_try(sequence) - .expect("Could not parse sequence"); + if let Err(..) = self.key_sequence_parse_try(sequence) { + println!("Could not parse sequence"); + } } fn key_sequence_parse_try(&mut self, sequence: &str) -> Result<(), crate::dsl::ParseError> From f816e53c2b75e9276bb9767c6e5c0b4f77b302ed Mon Sep 17 00:00:00 2001 From: Amy Parker Date: Mon, 2 Jan 2023 11:20:25 -0800 Subject: [PATCH 1321/2015] ignore style warnings in libs/scrap Constant `is_cursor_embedded` does not follow the Rust standard stylistic convention of upper-case global variables and constants. This causes two warnings to be thrown when compiling (tested on Arch Linux, commit = 68fda34, Rust = 1.66.0), one each for the Wayland and X11 common modules. Since these variables are not new, their names should not be modified; to remove the warnings, this patch allows non-style-conforming names on these two constant declarations specifically, suppressing the warnings. It does not affect stylistic warnings on any other code within the project. Signed-off-by: Amy Parker Cc: fufesou --- libs/scrap/src/common/wayland.rs | 1 + libs/scrap/src/common/x11.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/libs/scrap/src/common/wayland.rs b/libs/scrap/src/common/wayland.rs index 99752fa7f..e9a846602 100644 --- a/libs/scrap/src/common/wayland.rs +++ b/libs/scrap/src/common/wayland.rs @@ -4,6 +4,7 @@ use std::{io, sync::RwLock, time::Duration}; pub struct Capturer(Display, Box, bool, Vec); +#[allow(non_upper_case_globals)] pub const is_cursor_embedded: bool = true; lazy_static::lazy_static! { diff --git a/libs/scrap/src/common/x11.rs b/libs/scrap/src/common/x11.rs index 138ab5000..a8359498b 100644 --- a/libs/scrap/src/common/x11.rs +++ b/libs/scrap/src/common/x11.rs @@ -3,6 +3,7 @@ use std::{io, ops, time::Duration}; pub struct Capturer(x11::Capturer); +#[allow(non_upper_case_globals)] pub const is_cursor_embedded: bool = false; impl Capturer { From 6087a29ccc43233134691a9cd5baddced5a310ba Mon Sep 17 00:00:00 2001 From: "Miguel F. G" <116861809+flusheDData@users.noreply.github.com> Date: Mon, 2 Jan 2023 21:44:17 +0100 Subject: [PATCH 1322/2015] 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 fdac9dd48..a99ae8551 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -406,7 +406,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Group", "Grupo"), ("Search", "Búsqueda"), ("Closed manually by the web console", "Cerrado manualmente por la consola web"), - ("Local keyboard type", ""), - ("Select local keyboard type", ""), + ("Local keyboard type", "Tipo de teclado local"), + ("Select local keyboard type", "Seleccionar tipo de teclado local"), ].iter().cloned().collect(); } From 0c14b96f0bcf5a688b8dfb721968b9bab54352b0 Mon Sep 17 00:00:00 2001 From: if0else9 <52785502+if0else9@users.noreply.github.com> Date: Tue, 3 Jan 2023 10:35:04 +0200 Subject: [PATCH 1323/2015] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index ad19edaa1..79255e455 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ Below are the servers you are using for free, it may change along the time. If y | Germany | Codext | 4 vCPU / 8GB RAM | | Finland (Helsinki) | 0x101 Cyber Security | 4 vCPU / 8GB RAM | | USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8GB RAM | +| Ukraine (Kyiv) | dc.volia (2VM) | 2 vCPU / 4GB RAM | ## Dependencies From ac433dc11a61aff7576c431bd6f6c6b8be925279 Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 3 Jan 2023 19:16:23 +0800 Subject: [PATCH 1324/2015] fix post heartbeat block Signed-off-by: 21pages --- src/server/connection.rs | 37 ++++++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/src/server/connection.rs b/src/server/connection.rs index 919aeae99..f91281a52 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -154,6 +154,7 @@ impl Connection { let (tx, mut rx) = mpsc::unbounded_channel::<(Instant, Arc)>(); let (tx_video, mut rx_video) = mpsc::unbounded_channel::<(Instant, Arc)>(); let (tx_input, rx_input) = std_mpsc::channel(); + let (tx_stop, mut rx_stop) = mpsc::unbounded_channel::(); let tx_cloned = tx.clone(); let mut conn = Self { @@ -393,11 +394,12 @@ impl Connection { } } _ = conn.http_timer.tick() => { - if let Err(_) = Connection::post_heartbeat(conn.server_audit_conn.clone(), conn.inner.id).await { - conn.on_close_manually("web console", "web console").await; - break; - } + Connection::post_heartbeat(conn.server_audit_conn.clone(), conn.inner.id, tx_stop.clone()); }, + Some(reason) = rx_stop.recv() => { + conn.on_close_manually(&reason, &reason).await; + + } Some((instant, value)) = rx_video.recv() => { if !conn.video_ack_required { video_service::notify_video_frame_fetched(id, Some(instant.into())); @@ -582,6 +584,7 @@ impl Connection { rx_from_cm: &mut mpsc::UnboundedReceiver, ) -> ResultType<()> { let mut last_recv_time = Instant::now(); + let (tx_stop, mut rx_stop) = mpsc::unbounded_channel::(); if let Some(mut forward) = self.port_forward_socket.take() { log::info!("Running port forwarding loop"); self.stream.set_raw(); @@ -615,7 +618,10 @@ impl Connection { if last_recv_time.elapsed() >= H1 { bail!("Timeout"); } - Connection::post_heartbeat(self.server_audit_conn.clone(), self.inner.id).await?; + Connection::post_heartbeat(self.server_audit_conn.clone(), self.inner.id, tx_stop.clone()); + } + Some(reason) = rx_stop.recv() => { + bail!(reason); } } } @@ -705,23 +711,28 @@ impl Connection { }); } - async fn post_heartbeat(server_audit_conn: String, conn_id: i32) -> ResultType<()> { + fn post_heartbeat( + server_audit_conn: String, + conn_id: i32, + tx_stop: mpsc::UnboundedSender, + ) { if server_audit_conn.is_empty() { - return Ok(()); + return; } let url = server_audit_conn.clone(); let mut v = Value::default(); v["id"] = json!(Config::get_id()); v["uuid"] = json!(base64::encode(hbb_common::get_uuid())); v["conn_id"] = json!(conn_id); - if let Ok(rsp) = Self::post_audit_async(url, v).await { - if let Ok(rsp) = serde_json::from_str::(&rsp) { - if rsp.action == "disconnect" { - bail!("disconnect by server"); + tokio::spawn(async move { + if let Ok(rsp) = Self::post_audit_async(url, v).await { + if let Ok(rsp) = serde_json::from_str::(&rsp) { + if rsp.action == "disconnect" { + tx_stop.send("web console".to_string()).ok(); + } } } - } - return Ok(()); + }); } fn post_file_audit( From 1998f8a6bc236784e149834077b74e98aa010148 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 3 Jan 2023 23:42:40 +0800 Subject: [PATCH 1325/2015] fix drag icon hidden because color is not set --- flutter/lib/desktop/widgets/remote_menubar.dart | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 244822e19..ec17b1fbb 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -1422,7 +1422,8 @@ class __DraggableShowHideState extends State<_DraggableShowHide> { axis: Axis.horizontal, child: Icon( Icons.drag_indicator, - size: 15, + size: 20, + color: Colors.grey, ), feedback: widget, onDragStarted: (() { @@ -1465,7 +1466,7 @@ class __DraggableShowHideState extends State<_DraggableShowHide> { }), child: Obx((() => Icon( widget.show.isTrue ? Icons.expand_less : Icons.expand_more, - size: 15, + size: 20, ))), ), ], @@ -1478,7 +1479,7 @@ class __DraggableShowHideState extends State<_DraggableShowHide> { border: Border.all(color: MyTheme.border), ), child: SizedBox( - height: 15, + height: 20, child: child, ), ), From fd974caa8d63661f8d86a74e02f43ae644949b98 Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 4 Jan 2023 16:41:05 +0800 Subject: [PATCH 1326/2015] try fix https://github.com/rustdesk/rustdesk/issues/2670 Signed-off-by: fufesou --- Cargo.lock | 2 +- flutter/lib/desktop/widgets/login.dart | 14 +++++++------- flutter/lib/desktop/widgets/remote_menubar.dart | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cdccce4cb..e734249de 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4304,7 +4304,7 @@ dependencies = [ [[package]] name = "rdev" version = "0.5.0-2" -source = "git+https://github.com/fufesou/rdev#196b589573f90703a601e6b105dd7c917fc388f9" +source = "git+https://github.com/fufesou/rdev#916e8db3e68dbc0fe95b158c566374b159b9cba8" dependencies = [ "cocoa", "core-foundation 0.9.3", diff --git a/flutter/lib/desktop/widgets/login.dart b/flutter/lib/desktop/widgets/login.dart index 0736f0864..62e6ebc53 100644 --- a/flutter/lib/desktop/widgets/login.dart +++ b/flutter/lib/desktop/widgets/login.dart @@ -107,7 +107,7 @@ class WidgetOP extends StatefulWidget { class _WidgetOPState extends State { Timer? _updateTimer; String _stateMsg = ''; - String _FailedMsg = ''; + String _failedMsg = ''; String _url = ''; @override @@ -140,7 +140,7 @@ class _WidgetOPState extends State { String failedMsg = resultMap['failed_msg']; final String? url = resultMap['url']; final authBody = resultMap['auth_body']; - if (_stateMsg != stateMsg || _FailedMsg != failedMsg) { + if (_stateMsg != stateMsg || _failedMsg != failedMsg) { if (_url.isEmpty && url != null && url.isNotEmpty) { launchUrl(Uri.parse(url)); _url = url; @@ -154,7 +154,7 @@ class _WidgetOPState extends State { setState(() { _stateMsg = stateMsg; - _FailedMsg = failedMsg; + _failedMsg = failedMsg; if (failedMsg.isNotEmpty) { widget.curOP.value = ''; _updateTimer?.cancel(); @@ -166,7 +166,7 @@ class _WidgetOPState extends State { _resetState() { _stateMsg = ''; - _FailedMsg = ''; + _failedMsg = ''; _url = ''; } @@ -190,11 +190,11 @@ class _WidgetOPState extends State { Obx(() { if (widget.curOP.isNotEmpty && widget.curOP.value != widget.config.op) { - _FailedMsg = ''; + _failedMsg = ''; } return Offstage( offstage: - _FailedMsg.isEmpty && widget.curOP.value != widget.config.op, + _failedMsg.isEmpty && widget.curOP.value != widget.config.op, child: Row( children: [ Text( @@ -203,7 +203,7 @@ class _WidgetOPState extends State { ), SizedBox(width: 8), Text( - _FailedMsg, + _failedMsg, style: TextStyle( fontSize: 14, color: Colors.red, diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index ec17b1fbb..1aa2647ee 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -1410,10 +1410,10 @@ class _DraggableShowHide extends StatefulWidget { }) : super(key: key); @override - State<_DraggableShowHide> createState() => __DraggableShowHideState(); + State<_DraggableShowHide> createState() => _DraggableShowHideState(); } -class __DraggableShowHideState extends State<_DraggableShowHide> { +class _DraggableShowHideState extends State<_DraggableShowHide> { Offset position = Offset.zero; Size size = Size.zero; From 55962f2fc98a59c11a70ffcc6d1eb6710af9ba94 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 4 Jan 2023 18:35:31 +0800 Subject: [PATCH 1327/2015] ipv6 support for direct connection, todo: UI input check, relay port change based on ipv6 --- libs/hbb_common/src/lib.rs | 37 +++++++++++++++++++++++++++++++++++-- libs/hbb_common/src/tcp.rs | 34 +++++++++++++++++++++++++++++++++- src/client.rs | 10 +++++++--- src/common.rs | 22 +++++++++++++++++++++- src/rendezvous_mediator.rs | 7 +++---- 5 files changed, 99 insertions(+), 11 deletions(-) diff --git a/libs/hbb_common/src/lib.rs b/libs/hbb_common/src/lib.rs index d1893564c..85e0100d9 100644 --- a/libs/hbb_common/src/lib.rs +++ b/libs/hbb_common/src/lib.rs @@ -285,7 +285,7 @@ mod tests { let addr = "[2001:db8::1]:8080".parse::().unwrap(); assert_eq!(addr, AddrMangle::decode(&AddrMangle::encode(addr))); - + let addr = "[2001:db8:ff::1111]:80".parse::().unwrap(); assert_eq!(addr, AddrMangle::decode(&AddrMangle::encode(addr))); } @@ -306,4 +306,37 @@ pub fn is_ipv4_str(id: &str) -> bool { regex::Regex::new(r"^\d+\.\d+\.\d+\.\d+(:\d+)?$") .unwrap() .is_match(id) -} \ No newline at end of file +} + +#[inline] +pub fn is_ipv6_str(id: &str) -> bool { + regex::Regex::new(r"^((([a-fA-F0-9]{1,4}:{1,2})+[a-fA-F0-9]{1,4})|(\[([a-fA-F0-9]{1,4}:{1,2})+[a-fA-F0-9]{1,4}\]:\d+))$") + .unwrap() + .is_match(id) +} + +#[inline] +pub fn is_ip_str(id: &str) -> bool { + is_ipv4_str(id) || is_ipv6_str(id) +} + +#[cfg(test)] +mod test_lib { + use super::*; + + #[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); + } +} diff --git a/libs/hbb_common/src/tcp.rs b/libs/hbb_common/src/tcp.rs index f46d836da..a1322fc15 100644 --- a/libs/hbb_common/src/tcp.rs +++ b/libs/hbb_common/src/tcp.rs @@ -5,7 +5,7 @@ use protobuf::Message; use sodiumoxide::crypto::secretbox::{self, Key, Nonce}; use std::{ io::{self, Error, ErrorKind}, - net::SocketAddr, + net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, ops::{Deref, DerefMut}, pin::Pin, task::{Context, Poll}, @@ -258,6 +258,38 @@ pub async fn new_listener(addr: T, reuse: bool) -> ResultType< } } +pub async fn listen_any(port: u16) -> ResultType { + if let Ok(mut socket) = TcpSocket::new_v6() { + #[cfg(unix)] + { + use std::os::unix::io::{FromRawFd, IntoRawFd}; + let raw_fd = socket.into_raw_fd(); + let sock2 = unsafe { socket2::Socket::from_raw_fd(raw_fd) }; + sock2.set_only_v6(false).ok(); + socket = unsafe { TcpSocket::from_raw_fd(sock2.into_raw_fd()) }; + } + #[cfg(windows)] + { + use std::os::windows::prelude::{FromRawSocket, IntoRawSocket}; + let raw_socket = socket.into_raw_socket(); + let sock2 = unsafe { socket2::Socket::from_raw_socket(raw_socket) }; + sock2.set_only_v6(false).ok(); + socket = unsafe { TcpSocket::from_raw_socket(sock2.into_raw_socket()) }; + } + if socket + .bind(SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), port)) + .is_ok() + { + if let Ok(l) = socket.listen(DEFAULT_BACKLOG) { + return Ok(l); + } + } + } + let s = TcpSocket::new_v4()?; + s.bind(SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), port))?; + Ok(s.listen(DEFAULT_BACKLOG)?) +} + impl Unpin for DynTcpStream {} impl AsyncRead for DynTcpStream { diff --git a/src/client.rs b/src/client.rs index fe9d9dac0..635c8b661 100644 --- a/src/client.rs +++ b/src/client.rs @@ -167,7 +167,7 @@ impl Client { interface: impl Interface, ) -> ResultType<(Stream, bool)> { // to-do: remember the port for each peer, so that we can retry easier - if hbb_common::is_ipv4_str(peer) { + if hbb_common::is_ip_str(peer) { return Ok(( socket_client::connect_tcp( crate::check_port(peer, RELAY_PORT + 1), @@ -376,7 +376,8 @@ impl Client { log::info!("peer address: {}, timeout: {}", peer, connect_timeout); let start = std::time::Instant::now(); // NOTICE: Socks5 is be used event in intranet. Which may be not a good way. - let mut conn = socket_client::connect_tcp_local(peer, Some(local_addr), connect_timeout).await; + let mut conn = + socket_client::connect_tcp_local(peer, Some(local_addr), connect_timeout).await; let mut direct = !conn.is_err(); if interface.is_force_relay() || conn.is_err() { if !relay_server.is_empty() { @@ -1847,7 +1848,10 @@ pub trait Interface: Send + Clone + 'static + Sized { fn get_login_config_handler(&self) -> Arc>; fn set_force_relay(&self, direct: bool, received: bool) { - self.get_login_config_handler().write().unwrap().set_force_relay(direct, received); + self.get_login_config_handler() + .write() + .unwrap() + .set_force_relay(direct, received); } fn is_force_relay(&self) -> bool { self.get_login_config_handler().read().unwrap().force_relay diff --git a/src/common.rs b/src/common.rs index c28bbc3fc..07da3ea17 100644 --- a/src/common.rs +++ b/src/common.rs @@ -21,7 +21,7 @@ use hbb_common::{ anyhow::bail, compress::compress as compress_func, config::{self, Config, COMPRESS_LEVEL, RENDEZVOUS_TIMEOUT}, - get_version_number, log, + get_version_number, is_ipv6_str, log, message_proto::*, protobuf::Enum, protobuf::Message as _, @@ -477,6 +477,12 @@ pub fn username() -> String { #[inline] pub fn check_port(host: T, port: i32) -> String { let host = host.to_string(); + if is_ipv6_str(&host) { + if host.contains("[") { + return host; + } + return format!("[{}]:{}", host, port); + } if !host.contains(":") { return format!("{}:{}", host, port); } @@ -706,3 +712,17 @@ 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 { + use super::*; + + #[test] + fn test_check_port() { + assert_eq!(check_port("[1:2]:12", 32), "[1:2]:12"); + assert_eq!(check_port("1:2", 32), "[1:2]:32"); + assert_eq!(check_port("1.1.1.1", 32), "1.1.1.1:32"); + assert_eq!(check_port("1.1.1.1:32", 32), "1.1.1.1:32"); + assert_eq!(check_port("test.com:32", 0), "test.com:32"); + } +} diff --git a/src/rendezvous_mediator.rs b/src/rendezvous_mediator.rs index ec70bdf84..2dccb3f1a 100644 --- a/src/rendezvous_mediator.rs +++ b/src/rendezvous_mediator.rs @@ -506,8 +506,7 @@ async fn direct_server(server: ServerPtr) { let disabled = Config::get_option("direct-server").is_empty(); if !disabled && listener.is_none() { port = get_direct_port(); - let addr = format!("0.0.0.0:{}", port); - match hbb_common::tcp::new_listener(&addr, false).await { + match hbb_common::tcp::listen_any(port as _).await { Ok(l) => { listener = Some(l); log::info!( @@ -518,8 +517,8 @@ async fn direct_server(server: ServerPtr) { Err(err) => { // to-do: pass to ui log::error!( - "Failed to start direct server on : {}, error: {}", - addr, + "Failed to start direct server on port: {}, error: {}", + port, err ); loop { From c95019453b2d3fe49e75bfafaa87b0f81b4953cb Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 4 Jan 2023 20:06:48 +0800 Subject: [PATCH 1328/2015] fix https://github.com/rustdesk/rustdesk/issues/2713 Signed-off-by: fufesou --- flutter/lib/models/input_model.dart | 52 +++++++++++++++++++++-------- 1 file changed, 39 insertions(+), 13 deletions(-) diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index b488f30f3..90589ed33 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -17,6 +17,10 @@ import './state_model.dart'; /// Mouse button enum. enum MouseButtons { left, right, wheel } +const _kMouseEventDown = 'mousedown'; +const _kMouseEventUp = 'mouseup'; +const _kMouseEventMove = 'mousemove'; + extension ToString on MouseButtons { String get value { switch (this) { @@ -183,20 +187,42 @@ class InputModel { Map getEvent(PointerEvent evt, String type) { final Map out = {}; - out['type'] = type; out['x'] = evt.position.dx; out['y'] = evt.position.dy; if (alt) out['alt'] = 'true'; if (shift) out['shift'] = 'true'; if (ctrl) out['ctrl'] = 'true'; if (command) out['command'] = 'true'; - out['buttons'] = evt - .buttons; // left button: 1, right button: 2, middle button: 4, 1 | 2 = 3 (left + right) - if (evt.buttons != 0) { - _lastMouseDownButtons = evt.buttons; + + // Check update event type and set buttons to be sent. + int buttons = _lastMouseDownButtons; + if (type == _kMouseEventMove) { + // flutter may emit move event if one button is pressed and anoter button + // is pressing or releasing. + if (evt.buttons != _lastMouseDownButtons) { + // For simplicity + // Just consider 3 - 1 ((Left + Right buttons) - Left button) + // Do not consider 2 - 1 (Right button - Left button) + // or 6 - 5 ((Right + Mid buttons) - (Left + Mid buttons)) + // and so on + buttons = evt.buttons - _lastMouseDownButtons; + if (buttons > 0) { + type = _kMouseEventDown; + } else { + type = _kMouseEventUp; + buttons = -buttons; + } + } } else { - out['buttons'] = _lastMouseDownButtons; + if (evt.buttons != 0) { + buttons = evt.buttons; + } } + _lastMouseDownButtons = evt.buttons; + + out['buttons'] = buttons; + out['type'] = type; + return out; } @@ -260,7 +286,7 @@ class InputModel { isPhysicalMouse.value = true; } if (isPhysicalMouse.value) { - handleMouse(getEvent(e, 'mousemove')); + handleMouse(getEvent(e, _kMouseEventMove)); } } @@ -325,21 +351,21 @@ class InputModel { } } if (isPhysicalMouse.value) { - handleMouse(getEvent(e, 'mousedown')); + handleMouse(getEvent(e, _kMouseEventDown)); } } void onPointUpImage(PointerUpEvent e) { if (e.kind != ui.PointerDeviceKind.mouse) return; if (isPhysicalMouse.value) { - handleMouse(getEvent(e, 'mouseup')); + handleMouse(getEvent(e, _kMouseEventUp)); } } void onPointMoveImage(PointerMoveEvent e) { if (e.kind != ui.PointerDeviceKind.mouse) return; if (isPhysicalMouse.value) { - handleMouse(getEvent(e, 'mousemove')); + handleMouse(getEvent(e, _kMouseEventMove)); } } @@ -388,13 +414,13 @@ class InputModel { var type = ''; var isMove = false; switch (evt['type']) { - case 'mousedown': + case _kMouseEventDown: type = 'down'; break; - case 'mouseup': + case _kMouseEventUp: type = 'up'; break; - case 'mousemove': + case _kMouseEventMove: isMove = true; break; default: From 72162c9a3155d52f9229774feded62f4578e3c62 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 4 Jan 2023 20:27:18 +0800 Subject: [PATCH 1329/2015] fix get relay server for ipv6 --- src/common.rs | 36 +++++++++++++++++++++++++++++++++++- src/rendezvous_mediator.rs | 10 +--------- 2 files changed, 36 insertions(+), 10 deletions(-) diff --git a/src/common.rs b/src/common.rs index 07da3ea17..254c910e6 100644 --- a/src/common.rs +++ b/src/common.rs @@ -478,7 +478,7 @@ pub fn username() -> String { pub fn check_port(host: T, port: i32) -> String { let host = host.to_string(); if is_ipv6_str(&host) { - if host.contains("[") { + if host.starts_with("[") { return host; } return format!("[{}]:{}", host, port); @@ -489,6 +489,31 @@ pub fn check_port(host: T, port: i32) -> String { return host; } +#[inline] +pub fn increase_port(host: T, offset: i32) -> String { + let host = host.to_string(); + if is_ipv6_str(&host) { + if host.starts_with("[") { + let tmp: Vec<&str> = host.split("]:").collect(); + if tmp.len() == 2 { + let port: i32 = tmp[1].parse().unwrap_or(0); + if port > 0 { + return format!("{}]:{}", tmp[0], port + offset); + } + } + } + } 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 { + return format!("{}:{}", tmp[0], port + offset); + } + } + } + return host; +} + pub const POSTFIX_SERVICE: &'static str = "_service"; #[inline] @@ -721,8 +746,17 @@ mod test_common { fn test_check_port() { assert_eq!(check_port("[1:2]:12", 32), "[1:2]:12"); assert_eq!(check_port("1:2", 32), "[1:2]:32"); + assert_eq!(check_port("z1:2", 32), "z1:2"); assert_eq!(check_port("1.1.1.1", 32), "1.1.1.1:32"); assert_eq!(check_port("1.1.1.1:32", 32), "1.1.1.1:32"); assert_eq!(check_port("test.com:32", 0), "test.com:32"); + assert_eq!(increase_port("[1:2]:12", 1), "[1:2]:13"); + assert_eq!(increase_port("1.2.2.4:12", 1), "1.2.2.4:13"); + assert_eq!(increase_port("1.2.2.4", 1), "1.2.2.4"); + assert_eq!(increase_port("test.com", 1), "test.com"); + assert_eq!(increase_port("test.com:13", 4), "test.com:17"); + assert_eq!(increase_port("1:13", 4), "1:13"); + assert_eq!(increase_port("22:1:13", 4), "22:1:13"); + assert_eq!(increase_port("z1:2", 1), "z1:3"); } } diff --git a/src/rendezvous_mediator.rs b/src/rendezvous_mediator.rs index 2dccb3f1a..73c017e2e 100644 --- a/src/rendezvous_mediator.rs +++ b/src/rendezvous_mediator.rs @@ -475,15 +475,7 @@ impl RendezvousMediator { relay_server = provided_by_rendzvous_server; } if relay_server.is_empty() { - if self.host.contains(":") { - let tmp: Vec<&str> = self.host.split(":").collect(); - if tmp.len() == 2 { - let port: u16 = tmp[1].parse().unwrap_or(0); - relay_server = format!("{}:{}", tmp[0], port + 1); - } - } else { - relay_server = self.host.clone(); - } + relay_server = crate::increase_port(&self.host, 1); } relay_server } From 6d6014db270df105323d42114ed1af40be5197f8 Mon Sep 17 00:00:00 2001 From: NoLooseEnds Date: Wed, 4 Jan 2023 13:41:23 +0100 Subject: [PATCH 1330/2015] Update to highres tray icons Update to highres tray icons --- res/mac-tray-dark.png | Bin 275 -> 481 bytes res/mac-tray-light.png | Bin 270 -> 477 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/res/mac-tray-dark.png b/res/mac-tray-dark.png index ba8ed8c12cf19a82e8109f4d7b46e10b9afcff99..73fae6dcae72292e86519be8619968f8a5641e7d 100644 GIT binary patch literal 481 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-s#sNMduI>ds^78VEii%1~Af&7; zucV})qy!{^T%Zg{3P?hvfNUVpJX3lHs9mrm$S;^dV8Vm{0Ur`B6c{W}IN-3MVPCV) zHUe^EKYn@K3FI>}~^O!eVeaUXqKhAe2gc_9lEu5{nSS+gLoB!sCbCdV2 zxUlR%)QPtJY?b0W7Ej9Bu`OrClz9hQxVKNx(blsG4MupZgA*^dft%jUo7kWuK1_iI~kU$0^%|1RXrn^LjVwr$(YZkfw&xwp8a*|yW9xU@X!&-i>hkKYa7Jw~wHWq(XD+h5mh z)W85|71G;cd)RG*1iihAVBP{t!VA6_2xKB~&2%u+g@{~&@v`Z2QavZ4WFe=LJk~`c z_fq^cpOWm-%-*;|XOgj+nVjM+U3CQo&E!FsuD*jQni&ticl8Dw)68M;nhgr+9Z}Em zh^~f5NpmvBfN{>qr`RoEm~RSEG}ppN7JM~KFxuM)Zgn8+x#1w_=D276n`64)?%1ab ZPy``YXG<|C3jzQD002ovPDHLkV1m$=e8m6& diff --git a/res/mac-tray-light.png b/res/mac-tray-light.png index ad8bfa3960d06e2b108f7fe4cab95a89a481a38a..c2a8aea67a9e59a0cea53225eabea585046d1023 100644 GIT binary patch literal 477 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-s#sNMduI>ds|49VrFLosYwF{I4 z`2{mfxR4O=z~O_yfdvW$4I2!4HPr4hFfe+0x;TbZ#I3z{ds4H40Lz7ERxv#f|L-q5 z%QSm!nU7QGFTvuLGaVcZCtJQbHhKQ&o&GKH2y;U=_eY0Gq6(bLbdM^mX>8awL!V*y z-9Wwv0p`Euo^UXHWT;R{<1I*6d*Kqt5Pgurpig9IWRxQmU*VNOTEb%*ZT>Q z9`V6kEIJuk7v{dZ#F+cp>47C{)<5<$GFP^}=o8#tuArKvlD)w4%#xyp7{g>KMxj4( zMZFB1OjmphE=n%>`}AjmGNXDg!QVmsiLL6M`kmw-+SEK%^$z{rpQTs1w?%yb@bF(QoD4o*fHCjbk>Dey>_sNgO* z9o~aoQN>>HF*Ja&QN?8NKQw_cQN=`16Mlf5QN>R1DVzh>qlzma88!fpL>b2ct0Tfg z&>@j4F-}q^xEUFS`3dMS*Fv3u&xS>g(OvjN#{d8T From 18bcb8ca75fe4331460f7f746e705baee725da92 Mon Sep 17 00:00:00 2001 From: Zexi Date: Thu, 5 Jan 2023 01:37:14 +0800 Subject: [PATCH 1331/2015] fix: https://github.com/rustdesk/rustdesk/issues/2718 --- src/lang/ca.rs | 3 --- src/lang/cn.rs | 7 ++----- 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/pl.rs | 3 --- src/lang/pt_PT.rs | 3 --- src/lang/ptbr.rs | 3 --- src/lang/ru.rs | 3 --- src/lang/sk.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 | 4 ---- src/lang/tr.rs | 3 --- src/lang/tw.rs | 3 --- src/lang/ua.rs | 3 --- src/lang/vn.rs | 3 --- 30 files changed, 2 insertions(+), 93 deletions(-) diff --git a/src/lang/ca.rs b/src/lang/ca.rs index e5c43ccbc..093f2572c 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -334,7 +334,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Escala adaptativa"), ("General", ""), ("Security", "Seguretat"), - ("Account", "Compte"), ("Theme", "Tema"), ("Dark Theme", "Tema Fosc"), ("Dark", "Fosc"), @@ -347,7 +346,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", "Servidor"), ("Direct IP Access", "Accés IP Directe"), ("Proxy", ""), - ("Port", ""), ("Apply", "Aplicar"), ("Disconnect all devices?", "Desconnectar tots els dispositius?"), ("Clear", "Netejar"), @@ -376,7 +374,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", "Altre"), ("Confirm before closing multiple tabs", "Confirmar abans de tancar múltiples pestanyes"), ("Keyboard Settings", "Ajustaments de teclat"), - ("Custom", "Personalitzat"), ("Full Access", "Acces complet"), ("Screen Share", "Compartir pantalla"), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland requereix Ubuntu 21.04 o una versió superior."), diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 7d8f51ddc..307cbfd9b 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -118,7 +118,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Good image quality", "好画质"), ("Balanced", "一般画质"), ("Optimize reaction time", "优化反应时间"), - ("Custom", "自定义画质"), + ("Custom", "自定义"), ("Show remote cursor", "显示远程光标"), ("Show quality monitor", "显示质量监测"), ("Disable clipboard", "禁止剪贴板"), @@ -280,7 +280,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_stop_service_tip", "关闭服务将自动关闭所有已建立的连接。"), ("android_version_audio_tip", "当前安卓版本不支持音频录制,请升级至安卓10或更高。"), ("android_start_service_tip", "点击 [启动服务] 或打开 [屏幕录制] 权限开启手机屏幕共享服务。"), - ("Account", "账号"), + ("Account", "账户"), ("Overwrite", "覆盖"), ("This file exists, skip or overwrite this file?", "这个文件/文件夹已存在,跳过/覆盖?"), ("Quit", "退出"), @@ -334,7 +334,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "适应窗口"), ("General", "常规"), ("Security", "安全"), - ("Account", "账户"), ("Theme", "主题"), ("Dark Theme", "暗黑主题"), ("Dark", "黑暗"), @@ -347,7 +346,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", "服务器"), ("Direct IP Access", "IP直接访问"), ("Proxy", "代理"), - ("Port", "端口"), ("Apply", "应用"), ("Disconnect all devices?", "断开所有远程连接?"), ("Clear", "清空"), @@ -376,7 +374,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", "其他"), ("Confirm before closing multiple tabs", "关闭多个标签页时向您确认"), ("Keyboard Settings", "键盘设置"), - ("Custom", "自定义"), ("Full Access", "完全访问"), ("Screen Share", "仅共享屏幕"), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland 需要 Ubuntu 21.04 或更高版本。"), diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 609ebb81e..027de13ba 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -334,7 +334,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Měřítko adaptivní"), ("General", ""), ("Security", ""), - ("Account", ""), ("Theme", ""), ("Dark Theme", ""), ("Dark", ""), @@ -347,7 +346,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", ""), ("Direct IP Access", ""), ("Proxy", ""), - ("Port", ""), ("Apply", ""), ("Disconnect all devices?", ""), ("Clear", ""), @@ -376,7 +374,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", ""), ("Confirm before closing multiple tabs", ""), ("Keyboard Settings", ""), - ("Custom", ""), ("Full Access", ""), ("Screen Share", ""), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland vyžaduje Ubuntu 21.04 nebo vyšší verzi."), diff --git a/src/lang/da.rs b/src/lang/da.rs index 82a0fec94..3361804e8 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -334,7 +334,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Skaler adaptiv"), ("General", "Generelt"), ("Security", "Sikkerhed"), - ("Account", "Konto"), ("Theme", "Thema"), ("Dark Theme", "Mørk Tema"), ("Dark", "Mørk"), @@ -347,7 +346,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", "Server"), ("Direct IP Access", "Direkte IP Adgang"), ("Proxy", "Proxy"), - ("Port", "Port"), ("Apply", "Anvend"), ("Disconnect all devices?", "Afbryd alle enheder?"), ("Clear", "Nulstil"), @@ -376,7 +374,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", ""), ("Confirm before closing multiple tabs", ""), ("Keyboard Settings", ""), - ("Custom", ""), ("Full Access", ""), ("Screen Share", ""), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland kræver Ubuntu 21.04 eller nyere version."), diff --git a/src/lang/de.rs b/src/lang/de.rs index ddb2a45d2..7226550f5 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -334,7 +334,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Automatische Skalierung"), ("General", "Allgemein"), ("Security", "Sicherheit"), - ("Account", "Konto"), ("Theme", "Farbgebung"), ("Dark Theme", "Dunkle Farbgebung"), ("Dark", "Dunkel"), @@ -347,7 +346,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", "Server"), ("Direct IP Access", "Direkter IP-Zugriff"), ("Proxy", "Proxy"), - ("Port", "Port"), ("Apply", "Anwenden"), ("Disconnect all devices?", "Alle Geräte trennen?"), ("Clear", "Zurücksetzen"), @@ -376,7 +374,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", "Weitere Einstellungen"), ("Confirm before closing multiple tabs", "Nachfragen, wenn mehrere Tabs geschlossen werden"), ("Keyboard Settings", "Tastatureinstellungen"), - ("Custom", "Individuell"), ("Full Access", "Vollzugriff"), ("Screen Share", "Bildschirmfreigabe"), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland erfordert Ubuntu 21.04 oder eine höhere Version."), diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 0de56d0ad..a21a2e91e 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -334,7 +334,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Skalo adapta"), ("General", ""), ("Security", ""), - ("Account", ""), ("Theme", ""), ("Dark Theme", ""), ("Dark", ""), @@ -347,7 +346,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", ""), ("Direct IP Access", ""), ("Proxy", ""), - ("Port", ""), ("Apply", ""), ("Disconnect all devices?", ""), ("Clear", ""), @@ -376,7 +374,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", ""), ("Confirm before closing multiple tabs", ""), ("Keyboard Settings", ""), - ("Custom", ""), ("Full Access", ""), ("Screen Share", ""), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland postulas Ubuntu 21.04 aŭ pli altan version."), diff --git a/src/lang/es.rs b/src/lang/es.rs index a99ae8551..b3276949a 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -334,7 +334,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Escala adaptativa"), ("General", ""), ("Security", "Seguridad"), - ("Account", "Cuenta"), ("Theme", "Tema"), ("Dark Theme", "Tema Oscuro"), ("Dark", "Oscuro"), @@ -347,7 +346,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", "Servidor"), ("Direct IP Access", "Acceso IP Directo"), ("Proxy", ""), - ("Port", "Puerto"), ("Apply", "Aplicar"), ("Disconnect all devices?", "¿Desconectar todos los dispositivos?"), ("Clear", "Borrar"), @@ -376,7 +374,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", "Otro"), ("Confirm before closing multiple tabs", "Confirmar antes de cerrar múltiples pestañas"), ("Keyboard Settings", "Ajustes de teclado"), - ("Custom", "Personalizado"), ("Full Access", "Acceso completo"), ("Screen Share", "Compartir pantalla"), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland requiere Ubuntu 21.04 o una versión superior."), diff --git a/src/lang/fa.rs b/src/lang/fa.rs index d964db945..3d4579b27 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -334,7 +334,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "مقیاس تطبیقی"), ("General", "عمومی"), ("Security", "امنیت"), - ("Account", "حساب کاربری"), ("Theme", "نمایه"), ("Dark Theme", "نمایه تیره"), ("Dark", "تیره"), @@ -347,7 +346,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", "سرور"), ("Direct IP Access", "IP دسترسی مستقیم "), ("Proxy", "پروکسی"), - ("Port", "پورت"), ("Apply", "ثبت"), ("Disconnect all devices?", "همه دستگاه ها قطع شوند؟"), ("Clear", "پاک کردن"), @@ -376,7 +374,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", "سایر"), ("Confirm before closing multiple tabs", "تایید بستن دسته ای برگه ها"), ("Keyboard Settings", "تنظیمات صفحه کلید"), - ("Custom", "سفارشی"), ("Full Access", "دسترسی کامل"), ("Screen Share", "اشتراک گذاری صفحه"), ("Wayland requires Ubuntu 21.04 or higher version.", "نیازمند اوبونتو نسخه 21.04 یا بالاتر است Wayland"), diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 2f13a9807..2b0b19be6 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -334,7 +334,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Échelle adaptative"), ("General", "Général"), ("Security", "Sécurité"), - ("Account", "Compte"), ("Theme", "Thème"), ("Dark Theme", "Thème somble"), ("Dark", "Sombre"), @@ -347,7 +346,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", "Serveur"), ("Direct IP Access", "Accès IP direct"), ("Proxy", "Proxy"), - ("Port", "Port"), ("Apply", "Appliquer"), ("Disconnect all devices?", "Déconnecter tous les appareils"), ("Clear", "Effacer"), @@ -376,7 +374,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", "Divers"), ("Confirm before closing multiple tabs", "Confirmer avant de fermer plusieurs onglets"), ("Keyboard Settings", "Configuration clavier"), - ("Custom", "Personnalisé"), ("Full Access", "Accès total"), ("Screen Share", "Partage d'écran"), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland nécessite Ubuntu 21.04 ou une version supérieure."), diff --git a/src/lang/gr.rs b/src/lang/gr.rs index 811dca2c6..4b2777729 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -334,7 +334,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Προσαρμοστική κλίμακα"), ("General", "Γενικά"), ("Security", "Ασφάλεια"), - ("Account", "Λογαριασμός"), ("Theme", "Θέμα"), ("Dark Theme", "Σκούρο θέμα"), ("Dark", "Σκούρο"), @@ -347,7 +346,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", "Διακομιστής"), ("Direct IP Access", "Άμεση πρόσβαση IP"), ("Proxy", "Διαμεσολαβητής"), - ("Port", "Θύρα"), ("Apply", "Εφαρμογή"), ("Disconnect all devices?", "Αποσύνδεση όλων των συσκευών;"), ("Clear", "Καθαρισμός"), @@ -376,7 +374,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", "Άλλα"), ("Confirm before closing multiple tabs", "Επιβεβαίωση πριν κλείσετε πολλές καρτέλες"), ("Keyboard Settings", "Ρυθμίσεις πληκτρολογίου"), - ("Custom", "Προσαρμογή ποιότητας εικόνας"), ("Full Access", "Πλήρης πρόσβαση"), ("Screen Share", "Κοινή χρήση οθόνης"), ("Wayland requires Ubuntu 21.04 or higher version.", "Το Wayland απαιτεί Ubuntu 21.04 ή νεότερη έκδοση."), diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 3aaf0c87d..2f22ab511 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -334,7 +334,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Adaptív méretarány"), ("General", "Általános"), ("Security", "Biztonság"), - ("Account", "Fiók"), ("Theme", "Téma"), ("Dark Theme", "Sötét téma"), ("Dark", "Sötét"), @@ -347,7 +346,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", "Szerver"), ("Direct IP Access", "Közvetlen IP hozzáférés"), ("Proxy", "Proxy"), - ("Port", "Port"), ("Apply", "Alkalmaz"), ("Disconnect all devices?", "Leválasztja az összes eszközt?"), ("Clear", "Tisztítás"), @@ -376,7 +374,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", "Egyéb"), ("Confirm before closing multiple tabs", "Biztos, hogy bezárja az összes lapot?"), ("Keyboard Settings", "Billentyűzet beállítások"), - ("Custom", "Egyedi"), ("Full Access", "Teljes hozzáférés"), ("Screen Share", "Képernyőmegosztás"), ("Wayland requires Ubuntu 21.04 or higher version.", "A Waylandhoz Ubuntu 21.04 vagy újabb verzió szükséges."), diff --git a/src/lang/id.rs b/src/lang/id.rs index ccb4bbd8a..362a7fb85 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -334,7 +334,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Skala adaptif"), ("General", "Umum"), ("Security", "Keamanan"), - ("Account", "Akun"), ("Theme", "Tema"), ("Dark Theme", "Tema gelap"), ("Dark", "Gelap"), @@ -347,7 +346,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", "Server"), ("Direct IP Access", "Direct IP Access"), ("Proxy", "Proxy"), - ("Port", "Port"), ("Apply", "Terapkan"), ("Disconnect all devices?", "Putuskan sambungan semua perangkat?"), ("Clear", ""), @@ -376,7 +374,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", "Lainnya"), ("Confirm before closing multiple tabs", "Konfirmasi sebelum menutup banyak tab"), ("Keyboard Settings", "Pengaturan Papan Ketik"), - ("Custom", "Kustom"), ("Full Access", "Akses penuh"), ("Screen Share", "Berbagi Layar"), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland membutuhkan Ubuntu 21.04 atau versi yang lebih tinggi."), diff --git a/src/lang/it.rs b/src/lang/it.rs index 0280d1700..390670df4 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -334,7 +334,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Scala adattiva"), ("General", "Generale"), ("Security", "Sicurezza"), - ("Account", "Account"), ("Theme", "Tema"), ("Dark Theme", "Tema Scuro"), ("Dark", "Scuro"), @@ -347,7 +346,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", ""), ("Direct IP Access", "Accesso IP diretto"), ("Proxy", ""), - ("Port", "Porta"), ("Apply", "Applica"), ("Disconnect all devices?", "Disconnettere tutti i dispositivi?"), ("Clear", "Ripulisci"), @@ -376,7 +374,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", "Altro"), ("Confirm before closing multiple tabs", "Conferma prima di chiudere più schede"), ("Keyboard Settings", "Impostazioni tastiera"), - ("Custom", "Personalizzato"), ("Full Access", "Accesso completo"), ("Screen Share", "Condivisione dello schermo"), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland richiede Ubuntu 21.04 o successiva."), diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 5f6dcc79e..c247a7582 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -334,7 +334,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "フィットウィンドウ"), ("General", ""), ("Security", ""), - ("Account", ""), ("Theme", ""), ("Dark Theme", ""), ("Dark", ""), @@ -347,7 +346,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", ""), ("Direct IP Access", ""), ("Proxy", ""), - ("Port", ""), ("Apply", ""), ("Disconnect all devices?", ""), ("Clear", ""), @@ -376,7 +374,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", "他の"), ("Confirm before closing multiple tabs", "同時に複数のタブを閉じる前に確認する"), ("Keyboard Settings", ""), - ("Custom", ""), ("Full Access", ""), ("Screen Share", ""), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland には、Ubuntu 21.04 以降のバージョンが必要です。"), diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 0f7df38b9..a4f2fde77 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -334,7 +334,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "맞는 창"), ("General", ""), ("Security", ""), - ("Account", ""), ("Theme", ""), ("Dark Theme", ""), ("Dark", ""), @@ -347,7 +346,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", ""), ("Direct IP Access", ""), ("Proxy", ""), - ("Port", ""), ("Apply", ""), ("Disconnect all devices?", ""), ("Clear", ""), @@ -376,7 +374,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", ""), ("Confirm before closing multiple tabs", ""), ("Keyboard Settings", ""), - ("Custom", ""), ("Full Access", ""), ("Screen Share", ""), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland는 Ubuntu 21.04 이상 버전이 필요합니다."), diff --git a/src/lang/kz.rs b/src/lang/kz.rs index f352be343..d8037ff62 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -334,7 +334,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Scale adaptive"), ("General", ""), ("Security", ""), - ("Account", "Есепкі"), ("Theme", ""), ("Dark Theme", ""), ("Dark", ""), @@ -347,7 +346,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", ""), ("Direct IP Access", ""), ("Proxy", ""), - ("Port", "Порт"), ("Apply", ""), ("Disconnect all devices?", ""), ("Clear", ""), @@ -376,7 +374,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", ""), ("Confirm before closing multiple tabs", ""), ("Keyboard Settings", ""), - ("Custom", ""), ("Full Access", ""), ("Screen Share", ""), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland Ubuntu 21.04 немесе одан жоғары нұсқасын қажет етеді."), diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 9593ffe54..2cf91af4a 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -334,7 +334,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Skaluj adaptacyjnie"), ("General", "Ogólne"), ("Security", "Zabezpieczenia"), - ("Account", "Konto"), ("Theme", "Motyw"), ("Dark Theme", "Ciemny motyw"), ("Dark", "Ciemny"), @@ -347,7 +346,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", "Serwer"), ("Direct IP Access", "Bezpośredni Adres IP"), ("Proxy", "Proxy"), - ("Port", "Port"), ("Apply", "Zastosuj"), ("Disconnect all devices?", "Czy rozłączyć wszystkie urządzenia?"), ("Clear", "Wyczyść"), @@ -376,7 +374,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", "Inne"), ("Confirm before closing multiple tabs", "Potwierdź przed zamknięciem wielu kart"), ("Keyboard Settings", "Ustawienia klawiatury"), - ("Custom", "Własne"), ("Full Access", "Pełny dostęp"), ("Screen Share", "Udostępnianie ekranu"), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland wymaga Ubuntu 21.04 lub nowszego."), diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index dcadfb0e7..fc95cb548 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -334,7 +334,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Escala adaptável"), ("General", ""), ("Security", ""), - ("Account", ""), ("Theme", ""), ("Dark Theme", ""), ("Dark", ""), @@ -347,7 +346,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", ""), ("Direct IP Access", ""), ("Proxy", ""), - ("Port", ""), ("Apply", ""), ("Disconnect all devices?", ""), ("Clear", ""), @@ -376,7 +374,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", "Outro"), ("Confirm before closing multiple tabs", "Confirme antes de fechar vários separadores"), ("Keyboard Settings", "Configurações do teclado"), - ("Custom", ""), ("Full Access", "Controlo total"), ("Screen Share", ""), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland requer Ubuntu 21.04 ou versão superior."), diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 48e964f7d..5a89efd4b 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -334,7 +334,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Escala adaptada"), ("General", "Geral"), ("Security", "Segurança"), - ("Account", "Conta"), ("Theme", "Tema"), ("Dark Theme", "Tema escuro"), ("Dark", "Escuro"), @@ -347,7 +346,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", "Servidor"), ("Direct IP Access", "Acesso direto por IP"), ("Proxy", "Proxy"), - ("Port", "Porta"), ("Apply", "Aplicar"), ("Disconnect all devices?", "Desconectar todos os dispositivos?"), ("Clear", "Limpar"), @@ -376,7 +374,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", "Outro"), ("Confirm before closing multiple tabs", "Confirmar antes de fechar múltiplas abas"), ("Keyboard Settings", "Configurações de teclado"), - ("Custom", "Personalizado"), ("Full Access", "Acesso completo"), ("Screen Share", "Compartilhamento de tela"), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland requer Ubuntu 21.04 ou versão superior."), diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 82715758b..82d8d357e 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -334,7 +334,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Адаптивный масштаб"), ("General", "Общие"), ("Security", "Безопасность"), - ("Account", "Аккаунт"), ("Theme", "Тема"), ("Dark Theme", "Тёмная тема"), ("Dark", "Тёмная"), @@ -347,7 +346,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", "Сервер"), ("Direct IP Access", "Прямой IP-доступ"), ("Proxy", "Прокси"), - ("Port", "Порт"), ("Apply", "Применить"), ("Disconnect all devices?", "Отключить все устройства?"), ("Clear", "Очистить"), @@ -376,7 +374,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", "Другое"), ("Confirm before closing multiple tabs", "Подтверждать закрытие несколько вкладок"), ("Keyboard Settings", "Настройки клавиатуры"), - ("Custom", "Своё"), ("Full Access", "Полный доступ"), ("Screen Share", "Поделиться экраном"), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland требует Ubuntu 21.04 или более позднюю версию."), diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 72995dd94..e61ac9c58 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -334,7 +334,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Prispôsobivá mierka"), ("General", ""), ("Security", ""), - ("Account", ""), ("Theme", ""), ("Dark Theme", ""), ("Dark", ""), @@ -347,7 +346,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", ""), ("Direct IP Access", ""), ("Proxy", ""), - ("Port", ""), ("Apply", ""), ("Disconnect all devices?", ""), ("Clear", ""), @@ -376,7 +374,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", ""), ("Confirm before closing multiple tabs", ""), ("Keyboard Settings", ""), - ("Custom", ""), ("Full Access", ""), ("Screen Share", ""), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland vyžaduje Ubuntu 21.04 alebo vyššiu verziu."), diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 7a2472aa8..c55562942 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -334,7 +334,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", " E përsjhtatshme në shkallë"), ("General", "Gjeneral"), ("Security", "Siguria"), - ("Account", "Llogaria"), ("Theme", "Theme"), ("Dark Theme", "Theme e errët"), ("Dark", "E errët"), @@ -347,7 +346,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", "Server"), ("Direct IP Access", "Qasje e drejtpërdrejtë IP"), ("Proxy", "Proxy"), - ("Port", "Port"), ("Apply", "Apliko"), ("Disconnect all devices?", "Shkyç të gjitha pajisjet?"), ("Clear", "Pastro"), @@ -376,7 +374,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", "Tjetër"), ("Confirm before closing multiple tabs", "Konfirmo përpara se të mbyllësh shumë skeda"), ("Keyboard Settings", "Cilësimet e tastierës"), - ("Custom", "Personalizuar"), ("Full Access", "Qasje e plotë"), ("Screen Share", "Ndarja e ekranit"), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland kërkon Ubuntu 21.04 ose version më të lartë"), diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 0d15b199c..d74baf8b2 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -334,7 +334,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Adaptivno skaliranje"), ("General", "Uopšteno"), ("Security", "Bezbednost"), - ("Account", "Nalog"), ("Theme", "Tema"), ("Dark Theme", "Tamna tema"), ("Dark", "Tamno"), @@ -347,7 +346,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", "Server"), ("Direct IP Access", "Direktan IP pristup"), ("Proxy", "Proksi"), - ("Port", "Port"), ("Apply", "Primeni"), ("Disconnect all devices?", "Otkači sve uređaju?"), ("Clear", "Obriši"), @@ -376,7 +374,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", "Ostalo"), ("Confirm before closing multiple tabs", "Potvrda pre zatvaranja više kartica"), ("Keyboard Settings", "Postavke tastature"), - ("Custom", "Korisnički"), ("Full Access", "Pun pristup"), ("Screen Share", "Deljenje ekrana"), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland zahteva Ubuntu 21.04 ili veću verziju"), diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 9fa3c75fb..6a770c24a 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -334,7 +334,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Skala adaptivt"), ("General", "Generellt"), ("Security", "Säkerhet"), - ("Account", "Konto"), ("Theme", "Tema"), ("Dark Theme", "Mörkt tema"), ("Dark", "Mörk"), @@ -347,7 +346,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", "Server"), ("Direct IP Access", "Direkt IP åtkomst"), ("Proxy", "Proxy"), - ("Port", "Port"), ("Apply", "Tillämpa"), ("Disconnect all devices?", "Koppla ifrån alla enheter?"), ("Clear", "Töm"), @@ -376,7 +374,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", "Övrigt"), ("Confirm before closing multiple tabs", "Bekräfta innan du stänger flera flikar"), ("Keyboard Settings", "Tangentbordsinställningar"), - ("Custom", "Anpassat"), ("Full Access", "Full tillgång"), ("Screen Share", "Skärmdelning"), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland kräver Ubuntu 21.04 eller högre."), diff --git a/src/lang/template.rs b/src/lang/template.rs index 1bd9f5e98..b4113b91a 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -334,7 +334,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", ""), ("General", ""), ("Security", ""), - ("Account", ""), ("Theme", ""), ("Dark Theme", ""), ("Dark", ""), @@ -347,7 +346,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", ""), ("Direct IP Access", ""), ("Proxy", ""), - ("Port", ""), ("Apply", ""), ("Disconnect all devices?", ""), ("Clear", ""), @@ -376,7 +374,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", ""), ("Confirm before closing multiple tabs", ""), ("Keyboard Settings", ""), - ("Custom", ""), ("Full Access", ""), ("Screen Share", ""), ("Wayland requires Ubuntu 21.04 or higher version.", ""), diff --git a/src/lang/th.rs b/src/lang/th.rs index 186e88453..792f4f97a 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -334,7 +334,6 @@ lazy_static::lazy_static! { ("Scale adaptive", "ขนาดยืดหยุ่น"), ("General", "ทั่วไป"), ("Security", "ความปลอดภัย"), - ("Account", "บัญชี"), ("Theme", "ธีม"), ("Dark Theme", "ธีมมืด"), ("Dark", "มืด"), @@ -347,7 +346,6 @@ lazy_static::lazy_static! { ("Server", "เซิร์ฟเวอร์"), ("Direct IP Access", "การเข้าถึง IP ตรง"), ("Proxy", "พรอกซี"), - ("Port", "พอร์ท"), ("Apply", "นำไปใช้"), ("Disconnect all devices?", "ยกเลิกการเชื่อมต่ออุปกรณ์ทั้งหมด?"), ("Clear", "ล้างข้อมูล"), @@ -376,7 +374,6 @@ lazy_static::lazy_static! { ("Other", "อื่นๆ"), ("Confirm before closing multiple tabs", "ยืนยันการปิดหลายแท็บ"), ("Keyboard Settings", "การตั้งค่าคีย์บอร์ด"), - ("Custom", "กำหนดเอง"), ("Full Access", "การเข้าถึงทั้งหมด"), ("Screen Share", "การแชร์จอ"), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland ต้องการ Ubuntu เวอร์ชั่น 21.04 หรือสูงกว่า"), @@ -410,4 +407,3 @@ lazy_static::lazy_static! { ("Select local keyboard type", "เลือกประเภทคีย์บอร์ด"), ].iter().cloned().collect(); } - \ No newline at end of file diff --git a/src/lang/tr.rs b/src/lang/tr.rs index f74d0b435..60bc9dda1 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -334,7 +334,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Ölçek uyarlanabilir"), ("General", "Genel"), ("Security", "Güvenlik"), - ("Account", "Hesap"), ("Theme", "Tema"), ("Dark Theme", "Koyu Tema"), ("Dark", "Koyu"), @@ -347,7 +346,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", "Sunucu"), ("Direct IP Access", "Direk IP Erişimi"), ("Proxy", "Vekil"), - ("Port", "Port"), ("Apply", "Uygula"), ("Disconnect all devices?", "Tüm cihazların bağlantısını kes?"), ("Clear", "Temizle"), @@ -376,7 +374,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", "Diğer"), ("Confirm before closing multiple tabs", "Çoklu sekmeleri kapatmadan önce onayla"), ("Keyboard Settings", "Klavye Ayarları"), - ("Custom", "Özel"), ("Full Access", "Tam Erişim"), ("Screen Share", "Ekran Paylaşımı"), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland, Ubuntu 21.04 veya daha yüksek bir sürüm gerektirir."), diff --git a/src/lang/tw.rs b/src/lang/tw.rs index a3eb9691d..0e08fa508 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -334,7 +334,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "適應窗口"), ("General", "常規"), ("Security", "安全"), - ("Account", "賬戶"), ("Theme", "主題"), ("Dark Theme", "暗黑主題"), ("Dark", "黑暗"), @@ -347,7 +346,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", "服務器"), ("Direct IP Access", "IP直接訪問"), ("Proxy", "代理"), - ("Port", "端口"), ("Apply", "應用"), ("Disconnect all devices?", "斷開所有遠程連接?"), ("Clear", "清空"), @@ -376,7 +374,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", "其他"), ("Confirm before closing multiple tabs", "關閉多個分頁前跟我確認"), ("Keyboard Settings", "鍵盤設置"), - ("Custom", "自定義"), ("Full Access", "完全訪問"), ("Screen Share", "僅共享屏幕"), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland 需要 Ubuntu 21.04 或更高版本。"), diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 09ed272d5..343b62b4f 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -334,7 +334,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Масштаб адаптивний"), ("General", "Загальні"), ("Security", "Безпека"), - ("Account", "Акаунт"), ("Theme", "Тема"), ("Dark Theme", "Темна тема"), ("Dark", "Темна"), @@ -347,7 +346,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", "Сервер"), ("Direct IP Access", "Прямий IP доступ"), ("Proxy", "Проксі"), - ("Port", "Порт"), ("Apply", "Застосувати"), ("Disconnect all devices?", "Відключити всі прилади?"), ("Clear", "Очистити"), @@ -376,7 +374,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", "Інше"), ("Confirm before closing multiple tabs", ""), ("Keyboard Settings", "Налаштування клавіатури"), - ("Custom", "Користувацькі"), ("Full Access", "Повний доступ"), ("Screen Share", "Демонстрація екрану"), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland потребує Ubuntu 21.04 або новішої версії."), diff --git a/src/lang/vn.rs b/src/lang/vn.rs index c95d4fe63..a2fc416f2 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -334,7 +334,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Quy mô thích ứng"), ("General", ""), ("Security", ""), - ("Account", ""), ("Theme", ""), ("Dark Theme", ""), ("Dark", ""), @@ -347,7 +346,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", ""), ("Direct IP Access", ""), ("Proxy", ""), - ("Port", ""), ("Apply", ""), ("Disconnect all devices?", ""), ("Clear", ""), @@ -376,7 +374,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", ""), ("Confirm before closing multiple tabs", ""), ("Keyboard Settings", ""), - ("Custom", ""), ("Full Access", ""), ("Screen Share", ""), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland yêu cầu phiên bản Ubuntu 21.04 trở lên."), From 7fc15539cc96a487a50b7ba17dce54a9ebae2160 Mon Sep 17 00:00:00 2001 From: Jimmy GALLAND Date: Wed, 4 Jan 2023 22:19:15 +0100 Subject: [PATCH 1332/2015] update fr.rs --- src/lang/fr.rs | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 2b0b19be6..1e3beb2e4 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -174,7 +174,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Disconnect", "Déconnecter"), ("Allow using keyboard and mouse", "Autoriser l'utilisation du clavier et de la souris"), ("Allow using clipboard", "Autoriser l'utilisation du presse-papier"), - ("Allow hearing sound", "Autoriser l'audition du son"), + ("Allow hearing sound", "Autoriser l'envoi du son"), ("Allow file copy and paste", "Autoriser le copier-coller de fichiers"), ("Connected", "Connecté"), ("Direct and encrypted connection", "Connexion directe chiffrée"), @@ -183,7 +183,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Relayed and unencrypted connection", "Connexion relais non chiffrée"), ("Enter Remote ID", "Entrer l'ID de l'appareil à distance"), ("Enter your password", "Entrer votre mot de passe"), - ("Logging in...", "Se connecter..."), + ("Logging in...", "En cours de connexion ..."), ("Enable RDP session sharing", "Activer le partage de session RDP"), ("Auto Login", "Connexion automatique (le verrouillage ne sera effectif qu'après la désactivation du premier paramètre)"), ("Enable Direct IP Access", "Autoriser l'accès direct par IP"), @@ -196,7 +196,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Fix it", "Réparer"), ("Warning", "Avertissement"), ("Login screen using Wayland is not supported", "L'écran de connexion utilisant Wayland n'est pas pris en charge"), - ("Reboot required", "Redémarrage pour prendre effet"), + ("Reboot required", "Redémarrage requis"), ("Unsupported display server ", "Le serveur d'affichage actuel n'est pas pris en charge"), ("x11 expected", "x11 requis"), ("Port", "Port"), @@ -219,11 +219,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add Tag", "Ajouter une balise"), ("Unselect all tags", "Désélectionner toutes les balises"), ("Network error", "Erreur réseau"), - ("Username missed", "Nom d'utilisateur manqué"), - ("Password missed", "Mot de passe manqué"), + ("Username missed", "Nom d'utilisateur manquant"), + ("Password missed", "Mot de passe manquant"), ("Wrong credentials", "Identifiant ou mot de passe erroné"), ("Edit Tag", "Modifier la balise"), - ("Unremember Password", "Mot de passe oublié"), + ("Unremember Password", "Oublier le Mot de passe"), ("Favorites", "Favoris"), ("Add to Favorites", "Ajouter aux Favoris"), ("Remove from Favorites", "Retirer des favoris"), @@ -318,7 +318,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Exit Fullscreen", "Quitter le mode plein écran"), ("Fullscreen", "Plein écran"), ("Mobile Actions", "Actions mobiles"), - ("Select Monitor", "Sélectionnez Moniteur"), + ("Select Monitor", "Sélection du Moniteur"), ("Control Actions", "Actions de contrôle"), ("Display Settings", "Paramètres d'affichage"), ("Ratio", "Rapport"), @@ -330,8 +330,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Relay Connection", "Connexion relais"), ("Secure Connection", "Connexion sécurisée"), ("Insecure Connection", "Connexion non sécurisée"), - ("Scale original", "Échelle d'origine"), - ("Scale adaptive", "Échelle adaptative"), + ("Scale original", "Échelle 100%"), + ("Scale adaptive", "Mise à l'échelle Auto"), ("General", "Général"), ("Security", "Sécurité"), ("Theme", "Thème"), @@ -360,8 +360,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Directory", "Répertoire"), ("Automatically record incoming sessions", "Enregistrement automatique des session entrantes"), ("Change", "Modifier"), - ("Start session recording", "Commerce l'enregistrement"), - ("Stop session recording", "Stoper l'enregistrement"), + ("Start session recording", "Commencer l'enregistrement"), + ("Stop session recording", "Stopper l'enregistrement"), ("Enable Recording Session", "Activer l'enregistrement de session"), ("Allow recording session", "Autoriser l'enregistrement de session"), ("Enable LAN Discovery", "Activer la découverte réseau local"), @@ -396,14 +396,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request access to your device", "Demande d'accès à votre appareil"), ("Hide connection management window", "Masquer la fenêtre de gestion des connexions"), ("hide_cm_tip", "Autoriser le masquage uniquement si vous acceptez des sessions via un mot de passe et utilisez un mot de passe permanent"), - ("wayland_experiment_tip", ""), - ("Right click to select tabs", ""), - ("Skipped", ""), + ("wayland_experiment_tip", "Le support Wayland est en phase expérimentale, veuillez utiliser X11 si vous avez besoin d'un accès sans surveillance."), + ("Right click to select tabs", "Clique droit pour selectionner les onglets"), + ("Skipped", "Ignoré"), ("Add to Address Book", "Ajouter au carnet d'adresses"), ("Group", "Groupe"), ("Search", "Rechercher"), - ("Closed manually by the web console", ""), - ("Local keyboard type", ""), - ("Select local keyboard type", ""), + ("Closed manually by the web console", "Fermé manuellement par la console Web"), + ("Local keyboard type", "Disposition du clavier local"), + ("Select local keyboard type", "Selectionner la disposition du clavier local"), ].iter().cloned().collect(); } From 66d50ef55cf61af8f48fa870ab1fc63be071ee1d Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 5 Jan 2023 14:57:48 +0800 Subject: [PATCH 1333/2015] better 48x48 mac tray icon --- res/mac-tray-dark.png | Bin 481 -> 809 bytes res/mac-tray-light.png | Bin 477 -> 810 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/res/mac-tray-dark.png b/res/mac-tray-dark.png index 73fae6dcae72292e86519be8619968f8a5641e7d..860f9fcf5f763cec08b68b4d7fc1eb4cf00e2c40 100644 GIT binary patch literal 809 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sivoN?T!Hi;2MYvMt$SQP;xz=dFx){*RWnv{mVKkiQ?V zdVcyc-^*+j$Ib_rS82OFyzp9feZq3v{D;kfhVv2+>fJLr{ag7;#nI{AvrgVvIbFS2 zC$N>J^Ii4ZFM>w=?Cg?T*RVxunlQXQ@TF!#&B9JCAKAoPszr4oCEU?1O)uML`iiUw zPONeEJoW1!3-6u3k+bz@cB>^9Nhm%(>h{7{M)5M2W3iK3^{Hde7EF-HoPBuniapDs zdU9&*Q$5yZ`%ke=vu9-5xmqh*#cARtIgQceD8YNW`njxgN@xNA D=o26P delta 455 zcmZ3<_KDJUrcNgx*}1Cj!g5Gf!V2sF=> z-eF*15G)Du3uX|Q@Zf*IhlC3S1`8ApIBaOx*X*;6fq~J-)5S5Qf^qG%i|d*d1YFo9 z#6m9q`+xnO+r)OkYVS**Q#LHGf1s0cQ2IIZeuawm=8Qi=-4&B999ZTQG8BK1Iiy_B z!NB*4S<&BEY!c_|DgVV!wV6wM%J4tkoWPmkRJ(=WDXviX!KcL&WVJqPhD>8uw21Lh zF@GVWb>{kSsdMvYCE7l0z5R^$qtt7Th1(PKEHuj%PrlcVTb9vuvfJ(Nw*`lk9@qCa zF|T)OnPbPPzpCI`!tx83qFq-=N>1ITCYZ8C`AOiyKW-b+4}Mu}s=Bt+{#vKg>I>KO z=RD@kR$sE)^pEr1384n%ehX)7E*6Vw`R2cQ;@sqYD=sWM5Otz$KU<~vj>VI*c5KU8 zF=gI?7Vhm6bhPzsLIaYOJKYv>By}iUZE0KA!@4IWhw0zph>XA(WlV vy(_Fowr%#Kg4?qBZ#rZY`r`fC*4x*sSjoQ&IrAn8814+7u6{1-oD!M_BKmxgLwR4EAE&XI+8U?w1?eLpb|3XN<| zib8fvo)!QA0s=`yK~zY`jh5S%sxS`u>me--q-?%@P>n)3$U8R+#4HQP~MFK-R7z|&tY7EFdtwR}9s5~U?b zblxks50SY$ILN5wbC8un11KDrZUmAqG*Q~r7N+f3?vH|wwuLB|ewf)rfjVmA8%ko< z_aW_CD(d=K%#?D-T!P?;DWnRn%rBRKJrN87-fP>s9}d@ zW>RkHpr*JLNt&{!!(NsCbPFoJe!I$ddoA|h}Ox}8?=hw fA9uEYcR=z32XgFn`a{ieUf%01$LiPE!Do&|pyDK=2U2pdfI7 zurQ4%Cfxu40ZBGaDQhe z=tN!sTfqQG_7d0x1pws&goK&x01-R~gZsQbinF^vAsV>h^za5h#}p8>1-CLpJ_AR; zA2D+Z1~M;jVi$oynd6+?fnnf5yN`8t?LfnL^)z_UY9Z(KbCnL9*(G=I!R-(VR?UsDPMENE)bo$k^BZSz9lIR$F|2h1GQ zw&;!#w|5{VVkB#zIn1PTfLt+S8UYggUUH292?EqjaM2m0{pI;!BLOFk0mZrm_`be+ z3m=_8AwQ&KX-bw4#pPpn{a}Y*0e=1CWcl;w2QM6LavOeT_W%F@07*qoL Date: Thu, 5 Jan 2023 14:58:38 +0800 Subject: [PATCH 1334/2015] fix linux to mac, keyboard input Signed-off-by: fufesou --- flutter/lib/models/input_model.dart | 10 +++++----- libs/scrap/src/common/mod.rs | 4 ++-- libs/scrap/src/common/wayland.rs | 2 +- libs/scrap/src/common/x11.rs | 2 +- src/keyboard.rs | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index 90589ed33..52675de41 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -50,7 +50,7 @@ class InputModel { // mouse final isPhysicalMouse = false.obs; - int _lastMouseDownButtons = 0; + int _lastButtons = 0; Offset lastMousePos = Offset.zero; get id => parent.target?.id ?? ""; @@ -195,17 +195,17 @@ class InputModel { if (command) out['command'] = 'true'; // Check update event type and set buttons to be sent. - int buttons = _lastMouseDownButtons; + int buttons = _lastButtons; if (type == _kMouseEventMove) { // flutter may emit move event if one button is pressed and anoter button // is pressing or releasing. - if (evt.buttons != _lastMouseDownButtons) { + if (evt.buttons != _lastButtons) { // For simplicity // Just consider 3 - 1 ((Left + Right buttons) - Left button) // Do not consider 2 - 1 (Right button - Left button) // or 6 - 5 ((Right + Mid buttons) - (Left + Mid buttons)) // and so on - buttons = evt.buttons - _lastMouseDownButtons; + buttons = evt.buttons - _lastButtons; if (buttons > 0) { type = _kMouseEventDown; } else { @@ -218,7 +218,7 @@ class InputModel { buttons = evt.buttons; } } - _lastMouseDownButtons = evt.buttons; + _lastButtons = evt.buttons; out['buttons'] = buttons; out['type'] = type; diff --git a/libs/scrap/src/common/mod.rs b/libs/scrap/src/common/mod.rs index 1e78656cf..1de2f89d6 100644 --- a/libs/scrap/src/common/mod.rs +++ b/libs/scrap/src/common/mod.rs @@ -74,9 +74,9 @@ pub fn is_x11() -> bool { #[inline] pub fn is_cursor_embedded() -> bool { if is_x11() { - x11::is_cursor_embedded + x11::IS_CURSOR_EMBEDDED } else { - wayland::is_cursor_embedded + wayland::IS_CURSOR_EMBEDDED } } diff --git a/libs/scrap/src/common/wayland.rs b/libs/scrap/src/common/wayland.rs index e9a846602..3efaed36e 100644 --- a/libs/scrap/src/common/wayland.rs +++ b/libs/scrap/src/common/wayland.rs @@ -5,7 +5,7 @@ use std::{io, sync::RwLock, time::Duration}; pub struct Capturer(Display, Box, bool, Vec); #[allow(non_upper_case_globals)] -pub const is_cursor_embedded: bool = true; +pub const IS_CURSOR_EMBEDDED: bool = true; lazy_static::lazy_static! { static ref MAP_ERR: RwLock io::Error>> = Default::default(); diff --git a/libs/scrap/src/common/x11.rs b/libs/scrap/src/common/x11.rs index a8359498b..ffeb1b55f 100644 --- a/libs/scrap/src/common/x11.rs +++ b/libs/scrap/src/common/x11.rs @@ -4,7 +4,7 @@ use std::{io, ops, time::Duration}; pub struct Capturer(x11::Capturer); #[allow(non_upper_case_globals)] -pub const is_cursor_embedded: bool = false; +pub const IS_CURSOR_EMBEDDED: bool = false; impl Capturer { pub fn new(display: Display, yuv: bool) -> io::Result { diff --git a/src/keyboard.rs b/src/keyboard.rs index a3088c63f..9fa53757f 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -636,7 +636,7 @@ pub fn map_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option rdev::linux_code_to_win_scancode(event.code as _)?, "macos" => { if hbb_common::config::LocalConfig::get_kb_layout_type() == "ISO" { - rdev::linux_code_to_macos_iso_code(event.scan_code)? + rdev::linux_code_to_macos_iso_code(event.code as _)? } else { rdev::linux_code_to_macos_code(event.code as _)? } From a109788f6e74d033627033d51835428f669194be Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 5 Jan 2023 15:17:57 +0800 Subject: [PATCH 1335/2015] add mac tray icon to mac resources --- flutter/macos/Runner.xcodeproj/project.pbxproj | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/flutter/macos/Runner.xcodeproj/project.pbxproj b/flutter/macos/Runner.xcodeproj/project.pbxproj index a5a476285..6f49b7e19 100644 --- a/flutter/macos/Runner.xcodeproj/project.pbxproj +++ b/flutter/macos/Runner.xcodeproj/project.pbxproj @@ -26,6 +26,8 @@ 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 */; }; 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 */; }; @@ -74,6 +76,8 @@ 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 = ""; }; 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 = ""; }; @@ -127,6 +131,8 @@ 33CC11242044D66E0003C045 /* Resources */ = { isa = PBXGroup; children = ( + 7E4BCD752966B0EC006D24E2 /* mac-tray-dark.png */, + 7E4BCD742966B0EC006D24E2 /* mac-tray-light.png */, 33CC10F22044A3C60003C045 /* Assets.xcassets */, 33CC10F42044A3C60003C045 /* MainMenu.xib */, 33CC10F72044A3C60003C045 /* Info.plist */, @@ -253,6 +259,8 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 7E4BCD762966B0EC006D24E2 /* mac-tray-light.png in Resources */, + 7E4BCD772966B0EC006D24E2 /* mac-tray-dark.png in Resources */, 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, ); From a211949ba42d8fa783950bd44e2d95b7df44dd0d Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 5 Jan 2023 15:57:59 +0800 Subject: [PATCH 1336/2015] add __CGPreLoginApp flag to xcode project following https://stackoverflow.com/questions/41429524/how-to-simulate-keyboard-and-mouse-events-using-cgeventpost-in-login-window-mac https://opensource.apple.com/source/IOHIDFamily/IOHIDFamily-700/IOHIDFamily.xcodeproj/project.pbxproj --- flutter/macos/Runner.xcodeproj/project.pbxproj | 12 ++++++++++++ flutter/macos/rustdesk.xcodeproj/project.pbxproj | 6 ++++++ 2 files changed, 18 insertions(+) diff --git a/flutter/macos/Runner.xcodeproj/project.pbxproj b/flutter/macos/Runner.xcodeproj/project.pbxproj index 6f49b7e19..e375623f0 100644 --- a/flutter/macos/Runner.xcodeproj/project.pbxproj +++ b/flutter/macos/Runner.xcodeproj/project.pbxproj @@ -561,6 +561,12 @@ SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; + OTHER_LDFLAGS = ( + "-sectcreate", + __CGPreLoginApp, + __cgpreloginapp, + /dev/null, + ); }; name = Release; }; @@ -614,6 +620,12 @@ PROVISIONING_PROFILE_SPECIFIER = ""; "SWIFT_OBJC_BRIDGING_HEADER[arch=*]" = Runner/bridge_generated.h; SWIFT_VERSION = 5.0; + OTHER_LDFLAGS = ( + "-sectcreate", + __CGPreLoginApp, + __cgpreloginapp, + /dev/null, + ); }; name = Release; }; diff --git a/flutter/macos/rustdesk.xcodeproj/project.pbxproj b/flutter/macos/rustdesk.xcodeproj/project.pbxproj index e334f0ac5..664f88618 100644 --- a/flutter/macos/rustdesk.xcodeproj/project.pbxproj +++ b/flutter/macos/rustdesk.xcodeproj/project.pbxproj @@ -108,6 +108,12 @@ PRODUCT_NAME = rustdesk; SDKROOT = macosx; SUPPORTS_MACCATALYST = YES; + OTHER_LDFLAGS = ( + "-sectcreate", + __CGPreLoginApp, + __cgpreloginapp, + /dev/null, + ); }; name = Release; }; From 3aa4aaea77309364948fd670351f301c7f36ea49 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 5 Jan 2023 15:59:42 +0800 Subject: [PATCH 1337/2015] more ignore --- flutter/.gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/flutter/.gitignore b/flutter/.gitignore index 3cbfc0f54..9c7e52c12 100644 --- a/flutter/.gitignore +++ b/flutter/.gitignore @@ -54,3 +54,4 @@ lib/generated_bridge.freezed.dart flutter_export_environment.sh Flutter-Generated.xcconfig key.jks +macos/rustdesk.xcodeproj/project.xcworkspace/ From 5618557bfd0ab7316c46c287b4cc050de4f8ce6f Mon Sep 17 00:00:00 2001 From: Kingtous Date: Thu, 5 Jan 2023 16:26:37 +0800 Subject: [PATCH 1338/2015] fix: upload race of deb and flatpak specific build --- .github/workflows/flutter-nightly.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 17e338edc..e4b049a02 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -1181,6 +1181,7 @@ jobs: done - name: Publish debian package + if: ${{ matrix.job.extra-build-features == '' }} uses: softprops/action-gh-release@v1 with: prerelease: true From 5f6a2642781b85b753d029db7cab547a3454bc9d Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 5 Jan 2023 14:27:28 +0800 Subject: [PATCH 1339/2015] optional software render to fix flutter render problem on some nvidia card Signed-off-by: 21pages --- Cargo.lock | 1 + Cargo.toml | 1 + .../desktop/pages/desktop_setting_page.dart | 11 ++- src/core_main.rs | 11 +++ 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/en.rs | 1 + 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/ru.rs | 2 + src/lang/sk.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 | 5 +- src/lang/tr.rs | 2 + src/lang/tw.rs | 2 + src/lang/ua.rs | 2 + src/lang/vn.rs | 2 + src/platform/linux.rs | 89 +++++++++++++++++++ 36 files changed, 175 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e734249de..1ec3929b4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4562,6 +4562,7 @@ dependencies = [ "arboard", "async-process", "async-trait", + "backtrace", "base64", "bytes", "cc", diff --git a/Cargo.toml b/Cargo.toml index 82c35de79..2713df11d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -119,6 +119,7 @@ dbus-crossroads = "0.5" gtk = "0.15" libappindicator = "0.7" glib = "0.16.5" +backtrace = "0.3" [target.'cfg(target_os = "android")'.dependencies] android_logger = "0.11" diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 15f78daeb..45588171b 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -274,6 +274,15 @@ class _GeneralState extends State<_General> { _OptionCheckBox(context, 'Confirm before closing multiple tabs', 'enable-confirm-closing-tabs'), _OptionCheckBox(context, 'Adaptive Bitrate', 'enable-abr'), + if (Platform.isLinux) + Tooltip( + message: translate('software_render_tip'), + child: _OptionCheckBox( + context, + "Always use software rendering", + 'allow-always-software-render', + ), + ) ]); } @@ -1223,7 +1232,7 @@ Widget _OptionCheckBox(BuildContext context, String label, String key, ref.value = option; if (reverse) option = !option; String value = bool2option(key, option); - bind.mainSetOption(key: key, value: value); + await bind.mainSetOption(key: key, value: value); update?.call(); } } diff --git a/src/core_main.rs b/src/core_main.rs index 1f42f8aad..bf6866df5 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -38,6 +38,17 @@ pub fn core_main() -> Option> { } i += 1; } + #[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); + } else { + std::env::remove_var(k); + } + } #[cfg(feature = "flutter")] if _is_flutter_connect { return core_main_invoke_new_connection(std::env::args()); diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 093f2572c..eb38cd436 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -405,5 +405,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 307cbfd9b..c5e4407a6 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -405,5 +405,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the web console", "被web控制台手动关闭"), ("Local keyboard type", "本地键盘类型"), ("Select local keyboard type", "请选择本地键盘类型"), + ("software_render_tip", "如果你使用英伟达显卡, 并且远程窗口在会话建立后会立刻关闭, 那么安装nouveau驱动并且选择使用软件渲染可能会有帮助。重启软件后生效。"), + ("Always use software rendering", "使用软件渲染"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 027de13ba..18b2673c9 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -405,5 +405,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 3361804e8..a1e74259c 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -405,5 +405,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 7226550f5..44404d52f 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -405,5 +405,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the web console", "Manuell über die Webkonsole beendet"), ("Local keyboard type", "Lokaler Tastaturtyp"), ("Select local keyboard type", "Lokalen Tastaturtyp auswählen"), + ("software_render_tip", ""), + ("Always use software rendering", ""), ].iter().cloned().collect(); } diff --git a/src/lang/en.rs b/src/lang/en.rs index f351b575d..b8c8f074d 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -36,5 +36,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", "Allow hiding only if accepting sessions via password and using permanent password"), ("wayland_experiment_tip", "Wayland support is in experimental stage, please use X11 if you require unattended access."), ("Slogan_tip", "Made with heart in this chaotic world!"), + ("software_render_tip", "If you have an Nvidia graphics card and the remote window closes immediately after connecting, installing the nouveau driver and choosing to use software rendering may help. A software restart is required.") ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index a21a2e91e..7b32c1708 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -405,5 +405,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index b3276949a..4a9f9251c 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -405,5 +405,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the web console", "Cerrado manualmente por la consola web"), ("Local keyboard type", "Tipo de teclado local"), ("Select local keyboard type", "Seleccionar tipo de teclado local"), + ("software_render_tip", ""), + ("Always use software rendering", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 3d4579b27..8c0c426ed 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -405,5 +405,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 1e3beb2e4..22b522a9d 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -405,5 +405,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the web console", "Fermé manuellement par la console Web"), ("Local keyboard type", "Disposition du clavier local"), ("Select local keyboard type", "Selectionner la disposition du clavier local"), + ("software_render_tip", ""), + ("Always use software rendering", ""), ].iter().cloned().collect(); } diff --git a/src/lang/gr.rs b/src/lang/gr.rs index 4b2777729..9ca035e65 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -405,5 +405,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 2f22ab511..2b81b90eb 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -405,5 +405,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 362a7fb85..ecc21b3f0 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -405,5 +405,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 390670df4..31cfd345e 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -405,5 +405,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the web console", "Chiudi manualmente dalla console Web"), ("Local keyboard type", "Tipo di tastiera locale"), ("Select local keyboard type", "Seleziona il tipo di tastiera locale"), + ("software_render_tip", ""), + ("Always use software rendering", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index c247a7582..4673d2e41 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -405,5 +405,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index a4f2fde77..5d0b8c8a7 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -405,5 +405,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index d8037ff62..0b55a79ff 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -405,5 +405,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 2cf91af4a..5ca41e3ac 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -405,5 +405,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index fc95cb548..3e203a250 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -405,5 +405,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 5a89efd4b..c17620bc1 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -405,5 +405,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 82d8d357e..1fa6d7528 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -405,5 +405,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the web console", "Закрыто вручную через веб-консоль"), ("Local keyboard type", "Тип локальной клавиатуры"), ("Select local keyboard type", "Выберите тип локальной клавиатуры"), + ("software_render_tip", ""), + ("Always use software rendering", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index e61ac9c58..13bbcf4f7 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -405,5 +405,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index c55562942..d08036bf3 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -405,5 +405,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index d74baf8b2..f9386004d 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -405,5 +405,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 6a770c24a..9f2d1c9f4 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -405,5 +405,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index b4113b91a..145cf45bb 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -405,5 +405,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index 792f4f97a..d6bbe806d 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -280,7 +280,6 @@ lazy_static::lazy_static! { ("android_stop_service_tip", "การปิดการใช้งานเซอร์วิสจะปิดการเชื่อมต่อทั้งหมดโดยอัตโนมัติ"), ("android_version_audio_tip", "เวอร์ชั่นแอนดรอยด์ปัจจุบันของคุณไม่รองรับการบันทึกข้อมูลเสียง กรุณาอัปเกรดเป็นแอนดรอยด์เวอร์ชั่น 10 หรือสูงกว่า"), ("android_start_service_tip", "แตะ [เริ่มต้นใช้งานเซอร์วิส] หรือเปิดสิทธิ์ [การบันทึกหน้าจอ] เพื่อเริ่มเซอร์วิสการแชร์หน้าจอ"), - ("Account", "บัญชี"), ("Overwrite", "เขียนทับ"), ("This file exists, skip or overwrite this file?", "พบไฟล์ที่มีอยู่แล้ว ต้องการเขียนทับหรือไม่?"), ("Quit", "ออก"), @@ -334,6 +333,7 @@ lazy_static::lazy_static! { ("Scale adaptive", "ขนาดยืดหยุ่น"), ("General", "ทั่วไป"), ("Security", "ความปลอดภัย"), + ("Account", "บัญชี"), ("Theme", "ธีม"), ("Dark Theme", "ธีมมืด"), ("Dark", "มืด"), @@ -405,5 +405,8 @@ lazy_static::lazy_static! { ("Closed manually by the web console", "ถูกปิดโดยเว็บคอนโซล"), ("Local keyboard type", "ประเภทคีย์บอร์ด"), ("Select local keyboard type", "เลือกประเภทคีย์บอร์ด"), + ("software_render_tip", ""), + ("Always use software rendering", ""), ].iter().cloned().collect(); } + \ No newline at end of file diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 60bc9dda1..00c620c34 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -405,5 +405,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 0e08fa508..cb83d28ea 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -405,5 +405,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the web console", "被web控制台手動關閉"), ("Local keyboard type", "本地鍵盤類型"), ("Select local keyboard type", "請選擇本地鍵盤類型"), + ("software_render_tip", "如果你使用英偉達顯卡, 並且遠程窗口在會話建立後會立刻關閉, 那麼安裝nouveau驅動並且選擇使用軟件渲染可能會有幫助。重啟軟件後生效。"), + ("Always use software rendering", "使用軟件渲染"), ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 343b62b4f..1c6ac5828 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -405,5 +405,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index a2fc416f2..d2e067b3b 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -405,5 +405,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), ].iter().cloned().collect(); } diff --git a/src/platform/linux.rs b/src/platform/linux.rs index ab436ed30..b2c2e81cb 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -717,3 +717,92 @@ pub fn get_double_click_time() -> u32 { } } +/// forever: may not work +pub fn system_message(title: &str, msg: &str, forever: bool) -> ResultType<()> { + if std::process::Command::new("notify-send") + .arg(title) + .arg(msg) + .spawn() + .is_ok() + { + return Ok(()); + } + if std::process::Command::new("zenity") + .arg("--info") + .arg("--timeout") + .arg(if forever { "0" } else { "3" }) + .arg("--title") + .arg(title) + .arg("--text") + .arg(msg) + .spawn() + .is_ok() + { + return Ok(()); + } + if std::process::Command::new("kdialog") + .arg("--title") + .arg(title) + .arg("--msgbox") + .arg(msg) + .spawn() + .is_ok() + { + return Ok(()); + } + if std::process::Command::new("xmessage") + .arg("-center") + .arg("-timeout") + .arg(if forever { "0" } else { "3" }) + .arg(title) + .arg(msg) + .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() + ); + 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 _); + } +} From adb3450d02d602b1a369c2fd4b4150ff46f1ea59 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 5 Jan 2023 17:45:41 +0800 Subject: [PATCH 1340/2015] more comment on code sign of mac --- build.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/build.py b/build.py index ca91b3581..19e28c416 100755 --- a/build.py +++ b/build.py @@ -481,9 +481,15 @@ def main(): version, 'rustdesk-%s.dmg' % version) if pa: os.system(''' + # https://pyoxidizer.readthedocs.io/en/apple-codesign-0.14.0/apple_codesign.html + # https://pyoxidizer.readthedocs.io/en/stable/tugger_code_signing.html + # https://developer.apple.com/developer-id/ + # goto xcode and login with apple id, manager certificates (Developer ID Application and/or Developer ID Installer) online there (only download and double click (install) cer file can not export p12 because no private key) #rcodesign sign --p12-file ~/.p12/rustdesk-developer-id.p12 --p12-password-file ~/.p12/.cert-pass --code-signature-flags runtime ./rustdesk-{1}.dmg codesign -s "Developer ID Application: {0}" --force --options runtime ./rustdesk-{1}.dmg - # https://pyoxidizer.readthedocs.io/en/latest/apple_codesign_rcodesign.html + # https://appstoreconnect.apple.com/access/api + # https://gregoryszorc.com/docs/apple-codesign/0.16.0/apple_codesign_rcodesign.html#notarizing-and-stapling + # https://documentation.onesignal.com/docs/establishing-an-apns-authentication-key#step-2-generate-a-new-p8-key rcodesign notarize --api-issuer {2} --api-key {3} --staple ./rustdesk-{1}.dmg # verify: spctl -a -t exec -v /Applications/RustDesk.app '''.format(pa, version, os.environ.get('api-issuer'), os.environ.get('api-key'))) From 97cf85d1b7d618900067a1d47d21d7011487fce9 Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 5 Jan 2023 17:14:44 +0800 Subject: [PATCH 1341/2015] mouse forward back support on windows Signed-off-by: fufesou --- flutter/lib/models/input_model.dart | 12 +++++++++--- libs/enigo/src/lib.rs | 4 ++++ libs/enigo/src/win/win_impl.rs | 16 ++++++++++++++-- src/flutter_ffi.rs | 8 +++++--- src/server/input_service.rs | 24 ++++++++++++++++++------ 5 files changed, 50 insertions(+), 14 deletions(-) diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index 52675de41..0137b784e 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -466,15 +466,21 @@ class InputModel { evt['y'] = '${y.round()}'; var buttons = ''; switch (evt['buttons']) { - case 1: + case kPrimaryMouseButton: buttons = 'left'; break; - case 2: + case kSecondaryMouseButton: buttons = 'right'; break; - case 4: + case kMiddleMouseButton: buttons = 'wheel'; break; + case kBackMouseButton: + buttons = 'back'; + break; + case kForwardMouseButton: + buttons = 'forward'; + break; } evt['buttons'] = buttons; bind.sessionSendMouse(id: id, msg: json.encode(evt)); diff --git a/libs/enigo/src/lib.rs b/libs/enigo/src/lib.rs index caa08bd55..fcc2981fd 100644 --- a/libs/enigo/src/lib.rs +++ b/libs/enigo/src/lib.rs @@ -104,6 +104,10 @@ pub enum MouseButton { Middle, /// Right mouse button Right, + /// Back mouse button + Back, + /// Forward mouse button + Forward, /// Scroll up button ScrollUp, diff --git a/libs/enigo/src/win/win_impl.rs b/libs/enigo/src/win/win_impl.rs index 4a4fd7fc4..1b2a3f78e 100644 --- a/libs/enigo/src/win/win_impl.rs +++ b/libs/enigo/src/win/win_impl.rs @@ -134,9 +134,15 @@ impl MouseControllable for Enigo { MouseButton::Left => MOUSEEVENTF_LEFTDOWN, MouseButton::Middle => MOUSEEVENTF_MIDDLEDOWN, MouseButton::Right => MOUSEEVENTF_RIGHTDOWN, + MouseButton::Back => MOUSEEVENTF_XDOWN, + MouseButton::Forward => MOUSEEVENTF_XDOWN, _ => unimplemented!(), }, - 0, + match button { + MouseButton::Back => XBUTTON1 as _, + MouseButton::Forward => XBUTTON2 as _, + _ => 0, + }, 0, 0, ); @@ -155,9 +161,15 @@ impl MouseControllable for Enigo { MouseButton::Left => MOUSEEVENTF_LEFTUP, MouseButton::Middle => MOUSEEVENTF_MIDDLEUP, MouseButton::Right => MOUSEEVENTF_RIGHTUP, + MouseButton::Back => MOUSEEVENTF_XUP, + MouseButton::Forward => MOUSEEVENTF_XUP, _ => unimplemented!(), }, - 0, + match button { + MouseButton::Back => XBUTTON1 as _, + MouseButton::Forward => XBUTTON2 as _, + _ => 0, + }, 0, 0, ); diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index bf5ebaf4e..25161e1e3 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -885,9 +885,11 @@ pub fn session_send_mouse(id: String, msg: String) { } if let Some(buttons) = m.get("buttons") { mask |= match buttons.as_str() { - "left" => 1, - "right" => 2, - "wheel" => 4, + "left" => 0x01, + "right" => 0x02, + "wheel" => 0x04, + "back" => 0x08, + "forward" => 0x10, _ => 0, } << 3; } diff --git a/src/server/input_service.rs b/src/server/input_service.rs index bd2ad9a16..41ce8fd9e 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -556,27 +556,39 @@ pub fn handle_mouse_(evt: &MouseEvent) { en.mouse_move_to(evt.x, evt.y); } 1 => match buttons { - 1 => { + 0x01 => { allow_err!(en.mouse_down(MouseButton::Left)); } - 2 => { + 0x02 => { allow_err!(en.mouse_down(MouseButton::Right)); } - 4 => { + 0x04 => { allow_err!(en.mouse_down(MouseButton::Middle)); } + 0x08 => { + allow_err!(en.mouse_down(MouseButton::Back)); + } + 0x10 => { + allow_err!(en.mouse_down(MouseButton::Forward)); + } _ => {} }, 2 => match buttons { - 1 => { + 0x01 => { en.mouse_up(MouseButton::Left); } - 2 => { + 0x02 => { en.mouse_up(MouseButton::Right); } - 4 => { + 0x04 => { en.mouse_up(MouseButton::Middle); } + 0x08 => { + en.mouse_up(MouseButton::Back); + } + 0x10 => { + en.mouse_up(MouseButton::Forward); + } _ => {} }, 3 | 4 => { From b4feae33bb0891fc0dde013a04b6e1db108493d9 Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 5 Jan 2023 17:45:07 +0800 Subject: [PATCH 1342/2015] support linux mouse back/forward Signed-off-by: fufesou --- libs/enigo/src/linux/xdo.rs | 2 ++ libs/enigo/src/macos/macos_impl.rs | 10 ++++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/libs/enigo/src/linux/xdo.rs b/libs/enigo/src/linux/xdo.rs index 204420adc..2115d7283 100644 --- a/libs/enigo/src/linux/xdo.rs +++ b/libs/enigo/src/linux/xdo.rs @@ -57,6 +57,8 @@ fn mousebutton(button: MouseButton) -> c_int { MouseButton::ScrollDown => 5, MouseButton::ScrollLeft => 6, MouseButton::ScrollRight => 7, + MouseButton::Back => 8, + MouseButton::Forward => 9, } } diff --git a/libs/enigo/src/macos/macos_impl.rs b/libs/enigo/src/macos/macos_impl.rs index 68457a4a2..55f350895 100644 --- a/libs/enigo/src/macos/macos_impl.rs +++ b/libs/enigo/src/macos/macos_impl.rs @@ -226,7 +226,10 @@ impl MouseControllable for Enigo { MouseButton::Left => (CGMouseButton::Left, CGEventType::LeftMouseDown), MouseButton::Middle => (CGMouseButton::Center, CGEventType::OtherMouseDown), MouseButton::Right => (CGMouseButton::Right, CGEventType::RightMouseDown), - _ => unimplemented!(), + _ => { + log::info!("Unsupported button {:?}", button); + return Ok(()); + }, }; let dest = CGPoint::new(current_x as f64, current_y as f64); if let Some(src) = self.event_source.as_ref() { @@ -249,7 +252,10 @@ impl MouseControllable for Enigo { MouseButton::Left => (CGMouseButton::Left, CGEventType::LeftMouseUp), MouseButton::Middle => (CGMouseButton::Center, CGEventType::OtherMouseUp), MouseButton::Right => (CGMouseButton::Right, CGEventType::RightMouseUp), - _ => unimplemented!(), + _ => { + log::info!("Unsupported button {:?}", button); + return; + }, }; let dest = CGPoint::new(current_x as f64, current_y as f64); if let Some(src) = self.event_source.as_ref() { From 20ba62870e772328fa63f919c37b1e9182ee6d87 Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 5 Jan 2023 17:52:57 +0800 Subject: [PATCH 1343/2015] remove unimplemented! Signed-off-by: fufesou --- libs/enigo/src/win/win_impl.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/libs/enigo/src/win/win_impl.rs b/libs/enigo/src/win/win_impl.rs index 1b2a3f78e..fb3b9881f 100644 --- a/libs/enigo/src/win/win_impl.rs +++ b/libs/enigo/src/win/win_impl.rs @@ -136,7 +136,10 @@ impl MouseControllable for Enigo { MouseButton::Right => MOUSEEVENTF_RIGHTDOWN, MouseButton::Back => MOUSEEVENTF_XDOWN, MouseButton::Forward => MOUSEEVENTF_XDOWN, - _ => unimplemented!(), + _ => { + log::info!("Unsupported button {:?}", button); + return Ok(()); + } }, match button { MouseButton::Back => XBUTTON1 as _, @@ -163,7 +166,10 @@ impl MouseControllable for Enigo { MouseButton::Right => MOUSEEVENTF_RIGHTUP, MouseButton::Back => MOUSEEVENTF_XUP, MouseButton::Forward => MOUSEEVENTF_XUP, - _ => unimplemented!(), + _ => { + log::info!("Unsupported button {:?}", button); + return; + } }, match button { MouseButton::Back => XBUTTON1 as _, From db5656b569ddfdcf795609c4bb2abbf893470b7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jernej=20Simon=C4=8Di=C4=8D?= Date: Thu, 5 Jan 2023 13:01:11 +0100 Subject: [PATCH 1344/2015] Add Slovenian translation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jernej Simončič --- src/lang.rs | 3 + src/lang/sl.rs | 409 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 412 insertions(+) create mode 100755 src/lang/sl.rs diff --git a/src/lang.rs b/src/lang.rs index 5ea408416..65505cd70 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -30,6 +30,7 @@ mod sv; mod sq; mod sr; mod th; +mod sl; lazy_static::lazy_static! { pub static ref LANGS: Value = @@ -63,6 +64,7 @@ lazy_static::lazy_static! { ("sq", "Shqip"), ("sr", "Srpski"), ("th", "ภาษาไทย"), + ("sl", "Slovenščina"), ]); } @@ -120,6 +122,7 @@ pub fn translate_locale(name: String, locale: &str) -> String { "sq" => sq::T.deref(), "sr" => sr::T.deref(), "th" => th::T.deref(), + "sl" => sl::T.deref(), _ => en::T.deref(), }; if let Some(v) = m.get(&name as &str) { diff --git a/src/lang/sl.rs b/src/lang/sl.rs new file mode 100755 index 000000000..7cd8f0c98 --- /dev/null +++ b/src/lang/sl.rs @@ -0,0 +1,409 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "Stanje"), + ("Your Desktop", "Vaše namizje"), + ("desk_tip", "Do vašega namizja lahko dostopate s spodnjim IDjem in geslom"), + ("Password", "Geslo"), + ("Ready", "Pripravljen"), + ("Established", "Povezava vzpostavljena"), + ("connecting_status", "Vzpostavljanje povezave z omrežjem RustDesk..."), + ("Enable Service", "Omogoči storitev"), + ("Start Service", "Zaženi storitev"), + ("Service is running", "Storitev se izvaja"), + ("Service is not running", "Storitev se ne izvaja"), + ("not_ready_status", "Ni pripravljeno, preverite vašo mrežno povezavo"), + ("Control Remote Desktop", "Nadzoruj oddaljeno namizje"), + ("Transfer File", "Prenos datotek"), + ("Connect", "Poveži"), + ("Recent Sessions", "Nedavne seje"), + ("Address Book", "Adresar"), + ("Confirmation", "Potrditev"), + ("TCP Tunneling", "TCP tuneliranje"), + ("Remove", "Odstrani"), + ("Refresh random password", "Osveži naključno geslo"), + ("Set your own password", "Nastavi lastno geslo"), + ("Enable Keyboard/Mouse", "Omogoči tipkovnico in miško"), + ("Enable Clipboard", "Omogoči odložišče"), + ("Enable File Transfer", "Omogoči prenos datotek"), + ("Enable TCP Tunneling", "Omogoči TCP tuneliranje"), + ("IP Whitelisting", "Omogoči seznam dovoljenih IPjev"), + ("ID/Relay Server", "Strežnik za ID/posredovanje"), + ("Import Server Config", "Uvozi nastavitve strežnika"), + ("Export Server Config", "Izvozi nastavitve strežnika"), + ("Import server configuration successfully", "Nastavitve strežnika uspešno uvožene"), + ("Export server configuration successfully", "Nastavitve strežnika uspešno izvožene"), + ("Invalid server configuration", "Neveljavne nastavitve strežnika"), + ("Clipboard is empty", "Odložišče je prazno"), + ("Stop service", "Ustavi storitev"), + ("Change ID", "Spremeni ID"), + ("Website", "Spletna stran"), + ("About", "O programu"), + ("Slogan_tip", ""), + ("Privacy Statement", ""), + ("Mute", "Izklopi zvok"), + ("Audio Input", "Avdio vhod"), + ("Enhancements", "Izboljšave"), + ("Hardware Codec", "Strojni kodek"), + ("Adaptive Bitrate", "Prilagodljiva bitna hitrost"), + ("ID Server", "ID strežnik"), + ("Relay Server", "Posredniški strežnik"), + ("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"), + ("Too frequent", "Prepogosto"), + ("Cancel", "Prekliči"), + ("Skip", "Izpusti"), + ("Close", "Zapri"), + ("Retry", "Ponovi"), + ("OK", "V redu"), + ("Password Required", "Potrebno je geslo"), + ("Please enter your password", "Vnesite vaše geslo"), + ("Remember password", "Zapomni si geslo"), + ("Wrong Password", "Napačno geslo"), + ("Do you want to enter again?", "Želite znova vnesti?"), + ("Connection Error", "Napaka pri povezavi"), + ("Error", "Napaka"), + ("Reset by the peer", "Povezava prekinjena"), + ("Connecting...", "Povezovanje..."), + ("Connection in progress. Please wait.", "Vzpostavljanje povezave, prosim počakajte."), + ("Please try 1 minute later", "Poizkusite čez 1 minuto"), + ("Login Error", "Napaka pri prijavi"), + ("Successful", "Uspešno"), + ("Connected, waiting for image...", "Povezava vzpostavljena, čakam na sliko..."), + ("Name", "Ime"), + ("Type", "Vrsta"), + ("Modified", "Čas spremembe"), + ("Size", "Velikost"), + ("Show Hidden Files", "Prikaži skrite datoteke"), + ("Receive", "Prejmi"), + ("Send", "Pošlji"), + ("Refresh File", "Osveži datoteko"), + ("Local", "Lokalno"), + ("Remote", "Oddaljeno"), + ("Remote Computer", "Lokalni računalnik"), + ("Local Computer", "Oddaljeni računalnik"), + ("Confirm Delete", "Potrdi izbris"), + ("Delete", "Izbriši"), + ("Properties", "Lastnosti"), + ("Multi Select", "Večkratna izbira"), + ("Select All", "Izberi vse"), + ("Unselect All", "Počisti vse"), + ("Empty Directory", "Prazen imenik"), + ("Not an empty directory", "Imenik ni prazen"), + ("Are you sure you want to delete this file?", "Ali res želite izbrisati to datoteko?"), + ("Are you sure you want to delete this empty directory?", "Ali res želite izbrisati to prazno mapo?"), + ("Are you sure you want to delete the file of this directory?", "Ali res želite datoteko iz mape?"), + ("Do this for all conflicts", "Naredi to za vse"), + ("This is irreversible!", "Tega dejanja ni mogoče razveljaviti!"), + ("Deleting", "Brisanje"), + ("files", "datoteke"), + ("Waiting", "Čakanje"), + ("Finished", "Opravljeno"), + ("Speed", "Hitrost"), + ("Custom Image Quality", "Kakovost slike po meri"), + ("Privacy mode", "Zasebni način"), + ("Block user input", "Onemogoči uporabnikov vnos"), + ("Unblock user input", "Omogoči uporabnikov vnos"), + ("Adjust Window", "Prilagodi okno"), + ("Original", "Originalno"), + ("Shrink", "Skrči"), + ("Stretch", "Raztegni"), + ("Scrollbar", "Drsenje z drsniki"), + ("ScrollAuto", "Samodejno drsenje"), + ("Good image quality", "Visoka kakovost slike"), + ("Balanced", "Uravnoteženo"), + ("Optimize reaction time", "Optimiraj odzivni čas"), + ("Custom", "Po meri"), + ("Show remote cursor", "Prikaži oddaljeni kazalec miške"), + ("Show quality monitor", "Prikaži nadzornik kakovosti"), + ("Disable clipboard", "Onemogoči odložišče"), + ("Lock after session end", "Zakleni ob koncu seje"), + ("Insert", "Vstavi"), + ("Insert Lock", "Zaklep vstavljanja"), + ("Refresh", "Osveži"), + ("ID does not exist", "ID ne obstaja"), + ("Failed to connect to rendezvous server", "Ni se bilo mogoče povezati na povezovalni strežnik"), + ("Please try later", "Poizkusite znova kasneje"), + ("Remote desktop is offline", "Oddaljeno namizje ni dosegljivo"), + ("Key mismatch", "Ključ ni ustrezen"), + ("Timeout", "Časovna omejitev"), + ("Failed to connect to relay server", "Ni se bilo mogoče povezati na posredniški strežnik"), + ("Failed to connect via rendezvous server", "Ni se bilo mogoče povezati preko povezovalnega strežnika"), + ("Failed to connect via relay server", "Ni se bilo mogoče povezati preko posredniškega strežnika"), + ("Failed to make direct connection to remote desktop", "Ni bilo mogoče vzpostaviti neposredne povezave z oddaljenim namizjem"), + ("Set Password", "Nastavi geslo"), + ("OS Password", "Geslo operacijskega sistema"), + ("install_tip", "Zaradi nadzora uporabniškega računa, RustDesk v nekaterih primerih na oddaljeni strani ne deluje pravilno. Temu se lahko izognete z namestitvijo RustDeska."), + ("Click to upgrade", "Klikni za nadgradnjo"), + ("Click to download", "Klikni za prenos"), + ("Click to update", "Klikni za posodobitev"), + ("Configure", "Nastavi"), + ("config_acc", "Za oddaljeni nadzor namizja morate RustDesku dodeliti pravico za dostopnost"), + ("config_screen", "Za oddaljeni dostop do namizja morate RustDesku dodeliti pravico snemanje zaslona"), + ("Installing ...", "Nameščanje..."), + ("Install", "Namesti"), + ("Installation", "Namestitev"), + ("Installation Path", "Pot za namestitev"), + ("Create start menu shortcuts", "Ustvari bližnjice v meniju Začetek"), + ("Create desktop icon", "Ustvari ikono na namizju"), + ("agreement_tip", "Z namestitvijo se strinjate z licenčno pogodbo"), + ("Accept and Install", "Sprejmi in namesti"), + ("End-user license agreement", "Licenčna pogodba za končnega uporabnika"), + ("Generating ...", "Ustvarjanje ..."), + ("Your installation is lower version.", "Vaša namestitev je starejša"), + ("not_close_tcp_tip", "Med uporabo tunela ne zaprite tega okna"), + ("Listening ...", "Poslušam ..."), + ("Remote Host", "Oddaljeni gostitelj"), + ("Remote Port", "Oddaljena vrata"), + ("Action", "Deljanje"), + ("Add", "Dodaj"), + ("Local Port", "Lokalna vrata"), + ("Local Address", "Lokalni naslov"), + ("Change Local Port", "Spremeni lokalna vrata"), + ("setup_server_tip", "Za hitrejšo povezavo uporabite lasten strežnik"), + ("Too short, at least 6 characters.", "Prekratek, mora biti najmanj 6 znakov."), + ("The confirmation is not identical.", "Potrditev ni enaka."), + ("Permissions", "Dovoljenja"), + ("Accept", "Sprejmi"), + ("Dismiss", "Opusti"), + ("Disconnect", "Prekini povezavo"), + ("Allow using keyboard and mouse", "Dovoli uporabo tipkovnice in miške"), + ("Allow using clipboard", "Dovoli uporabo odložišča"), + ("Allow hearing sound", "Dovoli prenos zvoka"), + ("Allow file copy and paste", "Dovoli kopiraj in prilepi"), + ("Connected", "Povezan"), + ("Direct and encrypted connection", "Neposredna šifrirana povezava"), + ("Relayed and encrypted connection", "Posredovana šifrirana povezava"), + ("Direct and unencrypted connection", "Neposredna nešifrirana povezava"), + ("Relayed and unencrypted connection", "Posredovana šifrirana povezava"), + ("Enter Remote ID", "Vnesi oddaljeni ID"), + ("Enter your password", "Vnesi geslo"), + ("Logging in...", "Prijavljanje..."), + ("Enable RDP session sharing", "Omogoči deljenje RDP seje"), + ("Auto Login", "Samodejna prijava"), + ("Enable Direct IP Access", "Omogoči neposredni dostop preko IP"), + ("Rename", "Preimenuj"), + ("Space", "Prazno"), + ("Create Desktop Shortcut", "Ustvari bližnjico na namizju"), + ("Change Path", "Spremeni pot"), + ("Create Folder", "Ustvari mapo"), + ("Please enter the folder name", "Vnesite ime mape"), + ("Fix it", "Popravi"), + ("Warning", "Opozorilo"), + ("Login screen using Wayland is not supported", "Prijava z Waylandom ni podprta"), + ("Reboot required", "Potreben je ponovni zagon"), + ("Unsupported display server ", "Nepodprt zaslonski strežnik"), + ("x11 expected", "Pričakovan X11"), + ("Port", "Vrata"), + ("Settings", "Nastavitve"), + ("Username", "Uporabniško ime"), + ("Invalid port", "Neveljavno geslo"), + ("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"), + ("Always connect via relay", "Vedno poveži preko posrednika"), + ("whitelist_tip", "Dostop je možen samo iz dovoljenih IPjev"), + ("Login", "Prijavi"), + ("Logout", "Odjavi"), + ("Tags", "Oznake"), + ("Search ID", "Išči ID"), + ("Current Wayland display server is not supported", "Trenutni Wayland zaslonski strežnik ni podprt"), + ("whitelist_sep", "Naslovi ločeni z vejico, podpičjem, presledkom ali novo vrstico"), + ("Add ID", "Dodaj ID"), + ("Add Tag", "Dodaj oznako"), + ("Unselect all tags", ""), + ("Network error", "Omrežna napaka"), + ("Username missed", "Up. ime izpuščeno"), + ("Password missed", "Geslo izpuščeno"), + ("Wrong credentials", "Napačne poverilnice"), + ("Edit Tag", "Uredi oznako"), + ("Unremember Password", "Pozabi geslo"), + ("Favorites", "Priljubljene"), + ("Add to Favorites", "Dodaj med priljubljene"), + ("Remove from Favorites", "Odstrani iz priljubljenih"), + ("Empty", "Prazno"), + ("Invalid folder name", "Napačno ime mape"), + ("Socks5 Proxy", "Socks5 posredniški strežnik"), + ("Hostname", "Ime gostitelja"), + ("Discovered", "Odkriti"), + ("install_daemon_tip", "Za samodejni zagon ob vklopu računalnika je potrebno dodati sistemsko storitev"), + ("Remote ID", "Oddaljeni ID"), + ("Paste", "Prilepi"), + ("Paste here?", "Prilepi tu?"), + ("Are you sure to close the connection?", "Ali želite prekiniti povezavo?"), + ("Download new version", "Prenesi novo različico"), + ("Touch mode", "Način dotika"), + ("Mouse mode", "Način mišle"), + ("One-Finger Tap", "Tap z enim prstom"), + ("Left Mouse", "Leva tipka miške"), + ("One-Long Tap", "Dolg tap z enim prstom"), + ("Two-Finger Tap", "Tap z dvema prstoma"), + ("Right Mouse", "Desna tipka miške"), + ("One-Finger Move", "Premik z enim prstom"), + ("Double Tap & Move", "Dvojni tap in premik"), + ("Mouse Drag", "Vlečenje z miško"), + ("Three-Finger vertically", "Triprstno navpično"), + ("Mouse Wheel", "Miškino kolesce"), + ("Two-Finger Move", "Premik z dvema prstoma"), + ("Canvas Move", "Premik platna"), + ("Pinch to Zoom", "Povečava s približevanjem prstov"), + ("Canvas Zoom", "Povečava platna"), + ("Reset canvas", "Ponastavi platno"), + ("No permission of file transfer", "Ni pravic za prenos datotek"), + ("Note", "Opomba"), + ("Connection", "Povezava"), + ("Share Screen", "Deli zaslon"), + ("CLOSE", "ZAPRI"), + ("OPEN", "ODPRI"), + ("Chat", "Pogovor"), + ("Total", "Skupaj"), + ("items", "elementi"), + ("Selected", "Izbrano"), + ("Screen Capture", "Zajem zaslona"), + ("Input Control", "Nadzor vnosa"), + ("Audio Capture", "Zajem zvoka"), + ("File Connection", "Datotečna povezava"), + ("Screen Connection", "Zaslonska povezava"), + ("Do you accept?", "Ali sprejmete?"), + ("Open System Setting", "Odpri sistemske nastavitve"), + ("How to get Android input permission?", "Kako pridobiti dovoljenje za vnos na Androidu?"), + ("android_input_permission_tip1", "Za oddaljeni nadzor vaše naprave Android, je potrebno RustDesku dodeliti pravico za dostopnost."), + ("android_input_permission_tip2", "Pojdite v sistemske nastavitve, poiščite »Nameščene storitve« in vklopite storitev »RustDesk Input«."), + ("android_new_connection_tip", "Prejeta je bila zahteva za oddaljeni nadzor vaše naprave."), + ("android_service_will_start_tip", "Z vklopom zajema zaslona se bo samodejno zagnala storitev, ki omogoča da oddaljene naprave pošljejo zahtevo za povezavo na vašo napravo."), + ("android_stop_service_tip", "Z zaustavitvijo storitve bodo samodejno prekinjene vse oddaljene povezave."), + ("android_version_audio_tip", "Trenutna različica Androida ne omogoča zajema zvoka. Za zajem zvoka nadgradite na Android 10 ali novejši."), + ("android_start_service_tip", "Tapnite »Zaženi storitev« ali »ODPRI« pri dovoljenju za zajem zaslona da zaženete storitev deljenja zaslona."), + ("Account", "Račun"), + ("Overwrite", "Prepiši"), + ("This file exists, skip or overwrite this file?", "Datoteka obstaja, izpusti ali prepiši?"), + ("Quit", "Izhod"), + ("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"), + ("Help", "Pomoč"), + ("Failed", "Ni uspelo"), + ("Succeeded", "Uspelo"), + ("Someone turns on privacy mode, exit", "Vklopljen je zasebni način, izhod"), + ("Unsupported", "Ni podprto"), + ("Peer denied", "Odjemalec zavrnil"), + ("Please install plugins", "Namestite vključke"), + ("Peer exit", "Odjemalec se je zaprl"), + ("Failed to turn off", "Ni bilo mogoče izklopiti"), + ("Turned off", "Izklopljeno"), + ("In privacy mode", "V zasebnem načinu"), + ("Out privacy mode", "Iz zasebnega načina"), + ("Language", "Jezik"), + ("Keep RustDesk background service", "Ohrani RustDeskovo storitev v ozadju"), + ("Ignore Battery Optimizations", "Prezri optimizacije baterije"), + ("android_open_battery_optimizations_tip", "Če želite izklopiti to možnost, pojdite v nastavitve aplikacije RustDesk, poiščite »Baterija« in izklopite »Neomejeno«"), + ("Connection not allowed", "Povezava ni dovoljena"), + ("Legacy mode", "Stari način"), + ("Map mode", "Način preslikave"), + ("Translate mode", "Način prevajanja"), + ("Use permanent password", "Uporabi stalno geslo"), + ("Use both passwords", "Uporabi obe gesli"), + ("Set permanent password", "Nastavi stalno geslo"), + ("Enable Remote Restart", "Omogoči oddaljeni ponovni zagon"), + ("Allow remote restart", "Dovoli oddaljeni ponovni zagon"), + ("Restart Remote Device", "Znova zaženi oddaljeno napravo"), + ("Are you sure you want to restart", "Ali ste prepričani, da želite znova zagnati"), + ("Restarting Remote Device", "Ponovni zagon oddaljene naprave"), + ("remote_restarting_tip", "Oddaljena naprava se znova zaganja, prosim zaprite to sporočilo in se čez nekaj časa povežite s stalnim geslom."), + ("Copied", "Kopirano"), + ("Exit Fullscreen", "Izhod iz celozaslonskega načina"), + ("Fullscreen", "Celozaslonski način"), + ("Mobile Actions", "Dejanja za prenosne naprave"), + ("Select Monitor", "Izberite zaslon"), + ("Control Actions", "Dejanja za nadzor"), + ("Display Settings", "Nastavitve zaslona"), + ("Ratio", "Razmerje"), + ("Image Quality", "Kakovost slike"), + ("Scroll Style", "Način drsenja"), + ("Show Menubar", "Prikaži meni"), + ("Hide Menubar", "Skrij meni"), + ("Direct Connection", "Neposredna povezava"), + ("Relay Connection", "Posredovana povezava"), + ("Secure Connection", "Zavarovana povezava"), + ("Insecure Connection", "Nezavarovana povezava"), + ("Scale original", "Originalna velikost"), + ("Scale adaptive", "Prilagojena velikost"), + ("General", "Splošno"), + ("Security", "Varnost"), + ("Theme", "Tema"), + ("Dark Theme", "Temna tema"), + ("Dark", "Temna"), + ("Light", "Svetla"), + ("Follow System", "Sistemska"), + ("Enable hardware codec", "Omogoči strojno pospeševanje"), + ("Unlock Security Settings", "Odkleni varnostne nastavitve"), + ("Enable Audio", "Omogoči zvok"), + ("Unlock Network Settings", "Odkleni mrežne nastavitve"), + ("Server", "Strežnik"), + ("Direct IP Access", "Neposredni dostop preko IPja"), + ("Proxy", "Posredniški strežnik"), + ("Apply", "Uveljavi"), + ("Disconnect all devices?", "Odklopi vse naprave?"), + ("Clear", "Počisti"), + ("Audio Input Device", "Vhodna naprava za zvok"), + ("Deny remote access", "Onemogoči oddaljeni dostop"), + ("Use IP Whitelisting", "Omogoči seznam dovoljenih IP naslovov"), + ("Network", "Mreža"), + ("Enable RDP", "Omogoči RDP"), + ("Pin menubar", "Pripni menijsko vrstico"), + ("Unpin menubar", "Odpni menijsko vrstico"), + ("Recording", "Snemanje"), + ("Directory", "Imenik"), + ("Automatically record incoming sessions", "Samodejno snemaj vhodne seje"), + ("Change", "Spremeni"), + ("Start session recording", "Začni snemanje seje"), + ("Stop session recording", "Ustavi snemanje seje"), + ("Enable Recording Session", "Omogoči snemanje seje"), + ("Allow recording session", "Dovoli snemanje seje"), + ("Enable LAN Discovery", "Omogoči odkrivanje lokalnega omrežja"), + ("Deny LAN Discovery", "Onemogoči odkrivanje lokalnega omrežja"), + ("Write a message", "Napiši spoorčilo"), + ("Prompt", "Poziv"), + ("Please wait for confirmation of UAC...", "Počakajte za potrditev nadzora uporabniškega računa"), + ("elevated_foreground_window_tip", "Trenutno aktivno okno na oddaljenem računalniku zahteva višje pravice za upravljanje. Oddaljenega uporabnika lahko prosite, da okno minimizira, ali pa kliknite gumb za povzdig pravic v oknu za upravljanje povezave. Če se želite izogniti temu problemu, na oddaljenem računalniku RustDesk namestite."), + ("Disconnected", "Brez povezave"), + ("Other", "Drugo"), + ("Confirm before closing multiple tabs", "Zahtevajte potrditev pred zapiranjem večih zavihkov"), + ("Keyboard Settings", "Nastavitve tipkovnice"), + ("Full Access", "Poln dostop"), + ("Screen Share", "Deljenje zaslona"), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland zahteva Ubuntu 21.04 ali novejši"), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Zahtevana je novejša različica Waylanda. Posodobite vašo distribucijo ali pa uporabite X11."), + ("JumpLink", "Pogled"), + ("Please Select the screen to be shared(Operate on the peer side).", "Izberite zaslon za delitev (na oddaljeni strani)."), + ("Show RustDesk", "Prikaži RustDesk"), + ("This PC", "Ta računalnik"), + ("or", "ali"), + ("Continue with", "Nadaljuj z"), + ("Elevate", "Povzdig pravic"), + ("Zoom cursor", "Povečaj kazalec miške"), + ("Accept sessions via password", "Sprejmi seje z geslom"), + ("Accept sessions via click", "Sprejmi seje s potrditvijo"), + ("Accept sessions via both", "Sprejmi seje z geslom ali potrditvijo"), + ("Please wait for the remote side to accept your session request...", "Počakajte, da oddaljeni računalnik sprejme povezavo..."), + ("One-time Password", "Enkratno geslo"), + ("Use one-time password", "Uporabi enkratno geslo"), + ("One-time password length", "Dolžina enkratnega gesla"), + ("Request access to your device", "Zahtevaj dostop do svoje naprave"), + ("Hide connection management window", "Skrij okno za upravljanje povezave"), + ("hide_cm_tip", "Dovoli skrivanje samo pri sprejemanju sej z geslom"), + ("wayland_experiment_tip", "Podpora za Wayland je v preizkusni fazi. Uporabite X11, če rabite nespremljan dostop."), + ("Right click to select tabs", "Desno-kliknite za izbiro zavihkov"), + ("Skipped", "Izpuščeno"), + ("Add to Address Book", "Dodaj v adresar"), + ("Group", "Skupina"), + ("Search", "Iskanje"), + ("Closed manually by the web console", "Ročno zaprto iz spletne konzole"), + ("Local keyboard type", "Lokalna vrsta tipkovnice"), + ("Select local keyboard type", "Izberite lokalno vrsto tipkovnice"), + ].iter().cloned().collect(); +} From d657ba29c3a02ad3bd254652a896c58315abf5f2 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 5 Jan 2023 21:03:54 +0800 Subject: [PATCH 1345/2015] modify comment --- build.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.py b/build.py index 19e28c416..d92c0801a 100755 --- a/build.py +++ b/build.py @@ -489,7 +489,7 @@ def main(): codesign -s "Developer ID Application: {0}" --force --options runtime ./rustdesk-{1}.dmg # https://appstoreconnect.apple.com/access/api # https://gregoryszorc.com/docs/apple-codesign/0.16.0/apple_codesign_rcodesign.html#notarizing-and-stapling - # https://documentation.onesignal.com/docs/establishing-an-apns-authentication-key#step-2-generate-a-new-p8-key + # p8 file is generated when you generate api key, download and put it under ~/.private_keys/ rcodesign notarize --api-issuer {2} --api-key {3} --staple ./rustdesk-{1}.dmg # verify: spctl -a -t exec -v /Applications/RustDesk.app '''.format(pa, version, os.environ.get('api-issuer'), os.environ.get('api-key'))) From 19c56cf977b5efa56a1dbfd05bed39fee44e344f Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 5 Jan 2023 21:04:40 +0800 Subject: [PATCH 1346/2015] add comment --- build.py | 1 + 1 file changed, 1 insertion(+) diff --git a/build.py b/build.py index d92c0801a..f0131ad27 100755 --- a/build.py +++ b/build.py @@ -469,6 +469,7 @@ def main(): if pa: os.system(''' # buggy: rcodesign sign ... path/*, have to sign one by one + # install rcodesign via cargo install apple-codesign #rcodesign sign --p12-file ~/.p12/rustdesk-developer-id.p12 --p12-password-file ~/.p12/.cert-pass --code-signature-flags runtime ./target/release/bundle/osx/RustDesk.app/Contents/MacOS/rustdesk #rcodesign sign --p12-file ~/.p12/rustdesk-developer-id.p12 --p12-password-file ~/.p12/.cert-pass --code-signature-flags runtime ./target/release/bundle/osx/RustDesk.app/Contents/MacOS/libsciter.dylib #rcodesign sign --p12-file ~/.p12/rustdesk-developer-id.p12 --p12-password-file ~/.p12/.cert-pass --code-signature-flags runtime ./target/release/bundle/osx/RustDesk.app From 4789b54460098e7b820a329be6240a1deccdb4ec Mon Sep 17 00:00:00 2001 From: kingtous Date: Thu, 29 Dec 2022 21:17:30 +0800 Subject: [PATCH 1347/2015] feat: add macos codesign import --- .github/workflows/flutter-nightly.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index e4b049a02..ed1514770 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -149,6 +149,12 @@ jobs: - name: Checkout source code uses: actions/checkout@v3 + - name: Import the codesign cert + uses: apple-actions/import-codesign-certs@v1 + with: + p12-file-base64: ${{ secrets.MACOS_P12_BASE64 }} + p12-password: ${{ secrets.MACOS_P12_PASSWORD }} + - name: Install build runtime run: | brew install llvm create-dmg nasm yasm cmake gcc wget ninja From 6156faef250f29e9189ff4e6dffe39fe221b0462 Mon Sep 17 00:00:00 2001 From: kingtous Date: Thu, 29 Dec 2022 22:18:02 +0800 Subject: [PATCH 1348/2015] feat: codesign recursively --- .github/workflows/flutter-nightly.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index ed1514770..8c31ee097 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -154,6 +154,12 @@ jobs: with: p12-file-base64: ${{ secrets.MACOS_P12_BASE64 }} p12-password: ${{ secrets.MACOS_P12_PASSWORD }} + keychain: rustdesk + + - name: Check sign + run: | + security default-keychain -s rustdesk.keychain + security find-identity -v - name: Install build runtime run: | @@ -217,6 +223,16 @@ jobs: # --hwcodec not supported on macos yet ./build.py --flutter ${{ matrix.job.extra-build-args }} + - name: Codesign app and create signed dmg + run: | + security default-keychain -s rustdesk.keychain + 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 -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep ./flutter/build/macos/Build/Products/Release/rustdesk.app -v + create-dmg rustdesk-${{ env.VERSION }}.dmg ./flutter/build/macos/Build/Products/Release/rustdesk.app + codesign --force -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep rustdesk-${{ env.VERSION }}.dmg -v + - name: Rename rustdesk run: | for name in rustdesk*??.dmg; do From 863ba0f4fb52c0a57b131f0c6dbc338e21ebb1c2 Mon Sep 17 00:00:00 2001 From: kingtous Date: Thu, 5 Jan 2023 22:05:24 +0800 Subject: [PATCH 1349/2015] feat: notarize dmg support --- .github/workflows/flutter-nightly.yml | 20 +++++++++++++++- .../macos/Runner.xcodeproj/project.pbxproj | 23 +++++++++++++------ .../macos/Runner/DebugProfile.entitlements | 2 ++ flutter/macos/Runner/Release.entitlements | 4 ++++ 4 files changed, 41 insertions(+), 8 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 8c31ee097..d2a256d91 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -156,11 +156,27 @@ jobs: p12-password: ${{ secrets.MACOS_P12_PASSWORD }} keychain: rustdesk - - name: Check sign + - name: Check sign and import sign key run: | security default-keychain -s rustdesk.keychain security find-identity -v + - name: Import notarize key + uses: timheuer/base64-to-file@v1.2 + with: + fileName: rustdesk.json + fileDir: ${{ github.workspace }} + encodedString: ${{ secrets.MACOS_NOTARIZE_JSON }} + + - name: Install rcodesign tool + shell: bash + run: | + pushd /tmp + wget https://github.com/indygreg/apple-platform-rs/releases/download/apple-codesign%2F0.22.0/apple-codesign-0.22.0-macos-universal.tar.gz + tar -zxvf apple-codesign-0.22.0-macos-universal.tar.gz + mv apple-codesign-0.22.0-macos-universal/rcodesign /usr/local/bin + popd + - name: Install build runtime run: | brew install llvm create-dmg nasm yasm cmake gcc wget ninja @@ -232,6 +248,8 @@ jobs: codesign --force -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep ./flutter/build/macos/Build/Products/Release/rustdesk.app -v create-dmg rustdesk-${{ env.VERSION }}.dmg ./flutter/build/macos/Build/Products/Release/rustdesk.app codesign --force -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep rustdesk-${{ env.VERSION }}.dmg -v + # notarize the rustdesk-${{ env.VERSION }}.dmg + rcodesign notary-submit --api-key-path ${{ github.workspace }}/rustdesk.json --staple rustdesk-${{ env.VERSION }}.dmg - name: Rename rustdesk run: | diff --git a/flutter/macos/Runner.xcodeproj/project.pbxproj b/flutter/macos/Runner.xcodeproj/project.pbxproj index e375623f0..1274ec932 100644 --- a/flutter/macos/Runner.xcodeproj/project.pbxproj +++ b/flutter/macos/Runner.xcodeproj/project.pbxproj @@ -436,8 +436,11 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = ""; + ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -558,15 +561,15 @@ MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; ONLY_ACTIVE_ARCH = YES; - SDKROOT = macosx; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; OTHER_LDFLAGS = ( "-sectcreate", __CGPreLoginApp, __cgpreloginapp, /dev/null, ); + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; }; name = Release; }; @@ -577,8 +580,11 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = ""; + ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -604,8 +610,11 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = ""; + ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -616,16 +625,16 @@ ../../target/release, ); MACOSX_DEPLOYMENT_TARGET = 10.14; - PRODUCT_BUNDLE_IDENTIFIER = com.carriez.rustdesk; - PROVISIONING_PROFILE_SPECIFIER = ""; - "SWIFT_OBJC_BRIDGING_HEADER[arch=*]" = Runner/bridge_generated.h; - SWIFT_VERSION = 5.0; OTHER_LDFLAGS = ( "-sectcreate", __CGPreLoginApp, __cgpreloginapp, /dev/null, ); + PRODUCT_BUNDLE_IDENTIFIER = com.carriez.rustdesk; + PROVISIONING_PROFILE_SPECIFIER = ""; + "SWIFT_OBJC_BRIDGING_HEADER[arch=*]" = Runner/bridge_generated.h; + SWIFT_VERSION = 5.0; }; name = Release; }; diff --git a/flutter/macos/Runner/DebugProfile.entitlements b/flutter/macos/Runner/DebugProfile.entitlements index 9f56413f3..b52c39df4 100644 --- a/flutter/macos/Runner/DebugProfile.entitlements +++ b/flutter/macos/Runner/DebugProfile.entitlements @@ -6,6 +6,8 @@ com.apple.security.cs.allow-jit + com.apple.security.device.audio-input + com.apple.security.network.server diff --git a/flutter/macos/Runner/Release.entitlements b/flutter/macos/Runner/Release.entitlements index 08ba3a3fa..7f588d928 100644 --- a/flutter/macos/Runner/Release.entitlements +++ b/flutter/macos/Runner/Release.entitlements @@ -4,6 +4,10 @@ com.apple.security.app-sandbox + com.apple.security.cs.allow-jit + + com.apple.security.device.audio-input + com.apple.security.network.client From 632a981a84e0b6ca5be2d1f3ca2f5c6e5fb78209 Mon Sep 17 00:00:00 2001 From: kingtous Date: Thu, 5 Jan 2023 22:47:47 +0800 Subject: [PATCH 1350/2015] fix: enable hardened runtime in whole project --- flutter/macos/Runner.xcodeproj/project.pbxproj | 3 +++ 1 file changed, 3 insertions(+) diff --git a/flutter/macos/Runner.xcodeproj/project.pbxproj b/flutter/macos/Runner.xcodeproj/project.pbxproj index 1274ec932..b935ab4b2 100644 --- a/flutter/macos/Runner.xcodeproj/project.pbxproj +++ b/flutter/macos/Runner.xcodeproj/project.pbxproj @@ -411,6 +411,7 @@ CODE_SIGN_IDENTITY = "-"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_HARDENED_RUNTIME = YES; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -495,6 +496,7 @@ CODE_SIGN_IDENTITY = "-"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_HARDENED_RUNTIME = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -549,6 +551,7 @@ CODE_SIGN_IDENTITY = "-"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_HARDENED_RUNTIME = YES; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu11; From 1083f5cfca34d3ad20f44c39f2443dfa8cd53b88 Mon Sep 17 00:00:00 2001 From: kingtous Date: Fri, 6 Jan 2023 10:23:46 +0800 Subject: [PATCH 1351/2015] opt: use macos latest host runner --- .github/workflows/flutter-nightly.yml | 5 ++--- build.py | 3 ++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index d2a256d91..5fd0755fd 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -142,7 +142,7 @@ jobs: job: - { target: x86_64-apple-darwin, - os: macos-10.15, + os: macos-latest, extra-build-args: "", } steps: @@ -186,7 +186,6 @@ jobs: with: channel: "stable" flutter-version: ${{ env.FLUTTER_VERSION }} - cache: true - name: Install Rust toolchain uses: actions-rs/toolchain@v1 @@ -246,7 +245,7 @@ jobs: # start sign the rustdesk.app and dmg rm rustdesk-${{ env.VERSION }}.dmg || true codesign --force -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep ./flutter/build/macos/Build/Products/Release/rustdesk.app -v - create-dmg rustdesk-${{ env.VERSION }}.dmg ./flutter/build/macos/Build/Products/Release/rustdesk.app + 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 -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep rustdesk-${{ env.VERSION }}.dmg -v # notarize the rustdesk-${{ env.VERSION }}.dmg rcodesign notary-submit --api-key-path ${{ github.workspace }}/rustdesk.json --staple rustdesk-${{ env.VERSION }}.dmg diff --git a/build.py b/build.py index f0131ad27..75d6fcd89 100755 --- a/build.py +++ b/build.py @@ -305,7 +305,8 @@ def build_flutter_deb(version, features): def build_flutter_dmg(version, features): if not skip_cargo: - os.system(f'cargo build --features {features} --lib --release') + # set minimum osx build target, now is 10.14, which is the same as the flutter xcode project + os.system(f'MACOSX_DEPLOYMENT_TARGET=10.14 cargo build --features {features} --lib --release') # copy dylib os.system( "cp target/release/liblibrustdesk.dylib target/release/librustdesk.dylib") From 04c457aa4e35dd1d05b4a820f9de0e75527ac24c Mon Sep 17 00:00:00 2001 From: kingtous Date: Fri, 6 Jan 2023 10:40:04 +0800 Subject: [PATCH 1352/2015] opt: speed up macos bridge ci speed --- .github/workflows/flutter-nightly.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 5fd0755fd..d7fcb19d1 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -204,8 +204,12 @@ jobs: run: | dart pub global activate ffigen --version 5.0.1 # flutter_rust_bridge - pushd /tmp && git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1 && popd - pushd /tmp/flutter_rust_bridge/frb_codegen && cargo install --path . && popd + pushd /tmp + wget https://github.com/Kingtous/flutter_rust_bridge/releases/download/1.32.0-rustdesk/flutter_rust_bridge_codegen-x86_64-darwin.tgz + tar -zxvf flutter_rust_bridge_codegen-x86_64-darwin.tgz + mkdir -p ~/.cargo/bin + mv flutter_rust_bridge_codegen ~/.cargo/bin; chmod +x ~/.cargo/bin/flutter_rust_bridge_codegen + popd pushd flutter && flutter pub get && popd ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart From 98dec7b2efcccb2b1d27973ca4ffa9756af310d3 Mon Sep 17 00:00:00 2001 From: kingtous Date: Fri, 6 Jan 2023 11:14:54 +0800 Subject: [PATCH 1353/2015] opt: codesign with runtime option --- .github/workflows/flutter-nightly.yml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index d7fcb19d1..79e7db255 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -223,10 +223,6 @@ jobs: run: | $VCPKG_ROOT/vcpkg install libvpx libyuv opus - - name: Install cargo bundle tools - run: | - cargo install cargo-bundle - - name: Show version information (Rust, cargo, Clang) shell: bash run: | @@ -248,9 +244,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 -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 ./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 -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep rustdesk-${{ env.VERSION }}.dmg -v + codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep rustdesk-${{ env.VERSION }}.dmg -v # notarize the rustdesk-${{ env.VERSION }}.dmg rcodesign notary-submit --api-key-path ${{ github.workspace }}/rustdesk.json --staple rustdesk-${{ env.VERSION }}.dmg From b3c8579102ccb7ca2aa6be8d50568237d7a361a2 Mon Sep 17 00:00:00 2001 From: kingtous Date: Fri, 6 Jan 2023 11:55:48 +0800 Subject: [PATCH 1354/2015] opt: add notarize doc --- .github/workflows/flutter-nightly.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 79e7db255..7ce940b89 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -164,6 +164,7 @@ jobs: - name: Import notarize key uses: timheuer/base64-to-file@v1.2 with: + # https://gregoryszorc.com/docs/apple-codesign/stable/apple_codesign_rcodesign.html#notarizing-and-stapling fileName: rustdesk.json fileDir: ${{ github.workspace }} encodedString: ${{ secrets.MACOS_NOTARIZE_JSON }} From b048e5b2808ee7850687f18d3c8ec8fb80e76143 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 6 Jan 2023 12:20:26 +0800 Subject: [PATCH 1355/2015] adding input monitoring priviledge detect for mac --- build.rs | 16 +++++++++++++--- src/flutter_ffi.rs | 4 ++++ src/platform/macos.mm | 34 ++++++++++++++++++++++++++++++++++ src/platform/macos.rs | 8 ++++++++ src/{ => platform}/windows.cc | 0 src/ui_interface.rs | 8 ++++++++ 6 files changed, 67 insertions(+), 3 deletions(-) create mode 100644 src/platform/macos.mm rename src/{ => platform}/windows.cc (100%) diff --git a/build.rs b/build.rs index 67e40752c..ade63f0bc 100644 --- a/build.rs +++ b/build.rs @@ -1,9 +1,16 @@ #[cfg(windows)] fn build_windows() { - cc::Build::new().file("src/windows.cc").compile("windows"); + let file = "src/platform/windows.cc"; + cc::Build::new().file(file).compile("windows"); println!("cargo:rustc-link-lib=WtsApi32"); - println!("cargo:rerun-if-changed=build.rs"); - println!("cargo:rerun-if-changed=windows.cc"); + println!("cargo:rerun-if-changed={}", file); +} + +#[cfg(target_os = "macos")] +fn build_mac() { + let file = "src/platform/macos.mm"; + cc::Build::new().file(file).compile("macos"); + println!("cargo:rerun-if-changed={}", file); } #[cfg(all(windows, feature = "inline"))] @@ -117,5 +124,8 @@ fn main() { #[cfg(windows)] build_windows(); #[cfg(target_os = "macos")] + build_mac(); + #[cfg(target_os = "macos")] println!("cargo:rustc-link-lib=framework=ApplicationServices"); + println!("cargo:rerun-if-changed=build.rs"); } diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 25161e1e3..92f1e0606 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1113,6 +1113,10 @@ pub fn main_is_can_screen_recording(prompt: bool) -> SyncReturn { SyncReturn(is_can_screen_recording(prompt)) } +pub fn main_is_can_input_monitoring(prompt: bool) -> SyncReturn { + SyncReturn(is_can_input_monitoring(prompt)) +} + pub fn main_is_share_rdp() -> SyncReturn { SyncReturn(is_share_rdp()) } diff --git a/src/platform/macos.mm b/src/platform/macos.mm new file mode 100644 index 000000000..b82b269e2 --- /dev/null +++ b/src/platform/macos.mm @@ -0,0 +1,34 @@ +#import +#import +#import + +extern "C" bool InputMonitoringAuthStatus(bool prompt) { + if (@available(macos 10.15, *)) { + IOHIDAccessType theType = IOHIDCheckAccess(kIOHIDRequestTypeListenEvent); + NSLog(@"IOHIDCheckAccess = %d", theType); + switch (theType) { + case kIOHIDAccessTypeGranted: + return true; + break; + case kIOHIDAccessTypeDenied: { + if (prompt) { + NSString *urlString = @"x-apple.systempreferences:com.apple.preference.security?Privacy_ListenEvent"; + [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:urlString]]; + } + break; + } + case kIOHIDAccessTypeUnknown: { + if (prompt) { + bool result = IOHIDRequestAccess(kIOHIDRequestTypeListenEvent); + NSLog(@"IOHIDRequestAccess result = %d", result); + } + break; + } + default: + break; + } + } else { + return true; + } + return false; +} diff --git a/src/platform/macos.rs b/src/platform/macos.rs index 0bbec399c..62fa1ee25 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -32,6 +32,7 @@ extern "C" { fn CGEventGetLocation(e: *const c_void) -> CGPoint; static kAXTrustedCheckOptionPrompt: CFStringRef; fn AXIsProcessTrustedWithOptions(options: CFDictionaryRef) -> BOOL; + fn InputMonitoringAuthStatus(_: BOOL) -> BOOL; } pub fn is_process_trusted(prompt: bool) -> bool { @@ -47,6 +48,13 @@ pub fn is_process_trusted(prompt: bool) -> bool { } } +pub fn is_can_input_monitoring(prompt: bool) -> bool { + unsafe { + let value = if prompt { YES } else { NO }; + InputMonitoringAuthStatus(value) == YES + } +} + // macOS >= 10.15 // https://stackoverflow.com/questions/56597221/detecting-screen-recording-settings-on-macos-catalina/ // remove just one app from all the permissions: tccutil reset All com.carriez.rustdesk diff --git a/src/windows.cc b/src/platform/windows.cc similarity index 100% rename from src/windows.cc rename to src/platform/windows.cc diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 2e4ca4ea3..3b7d1c2c0 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -582,6 +582,14 @@ pub fn is_installed_daemon(_prompt: bool) -> bool { return true; } +#[inline] +pub fn is_can_input_monitoring(_prompt: bool) -> bool { + #[cfg(target_os = "macos")] + return crate::platform::macos::is_can_input_monitoring(_prompt); + #[cfg(not(target_os = "macos"))] + return true; +} + #[inline] pub fn get_error() -> String { #[cfg(not(any(feature = "cli")))] From 84e4389943142441724b2df92fca76e5399321ee Mon Sep 17 00:00:00 2001 From: Amy Parker Date: Thu, 5 Jan 2023 20:32:53 -0800 Subject: [PATCH 1356/2015] remove unnecessary allow block Patch #2701 (609117c: "ignore style warnings in libs/scrap") was merged, but the RustDesk team decided to later instead changed is_cursor_embedded to uppercase (see discussion on the PR), thus no longer triggering the warning and no longer needing the allow block. This was changed in (b723f84: "fix linux to mac, keyboard input"). This patch removes the now unnecessary allowances. Signed-off-by: Amy Parker Cc: fufseou --- libs/scrap/src/common/wayland.rs | 1 - libs/scrap/src/common/x11.rs | 1 - 2 files changed, 2 deletions(-) diff --git a/libs/scrap/src/common/wayland.rs b/libs/scrap/src/common/wayland.rs index 3efaed36e..e625fca7e 100644 --- a/libs/scrap/src/common/wayland.rs +++ b/libs/scrap/src/common/wayland.rs @@ -4,7 +4,6 @@ use std::{io, sync::RwLock, time::Duration}; pub struct Capturer(Display, Box, bool, Vec); -#[allow(non_upper_case_globals)] pub const IS_CURSOR_EMBEDDED: bool = true; lazy_static::lazy_static! { diff --git a/libs/scrap/src/common/x11.rs b/libs/scrap/src/common/x11.rs index ffeb1b55f..61112bff7 100644 --- a/libs/scrap/src/common/x11.rs +++ b/libs/scrap/src/common/x11.rs @@ -3,7 +3,6 @@ use std::{io, ops, time::Duration}; pub struct Capturer(x11::Capturer); -#[allow(non_upper_case_globals)] pub const IS_CURSOR_EMBEDDED: bool = false; impl Capturer { From ce5b49b7dd3c2418eaa1ccb1c53ef2fbd2e3bc4a Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 6 Jan 2023 12:42:16 +0800 Subject: [PATCH 1357/2015] add config_input --- flutter/pubspec.lock | 91 +++++++++++++++++++++++-------------------- src/lang/ca.rs | 1 + src/lang/cn.rs | 17 ++++---- 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/ru.rs | 1 + src/lang/sk.rs | 1 + src/lang/sl.rs | 3 ++ 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 + src/platform/macos.mm | 2 + 34 files changed, 94 insertions(+), 51 deletions(-) diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index bcbad530c..807f932bb 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -35,7 +35,7 @@ packages: name: archive url: "https://pub.dartlang.org" source: hosted - version: "3.3.1" + version: "3.3.5" args: dependency: transitive description: @@ -63,7 +63,7 @@ packages: name: back_button_interceptor url: "https://pub.dartlang.org" source: hosted - version: "6.0.1" + version: "6.0.2" bot_toast: dependency: "direct main" description: @@ -84,7 +84,7 @@ packages: name: build_config url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.1.1" build_daemon: dependency: transitive description: @@ -105,14 +105,14 @@ packages: name: build_runner url: "https://pub.dartlang.org" source: hosted - version: "2.2.1" + version: "2.3.3" build_runner_core: dependency: transitive description: name: build_runner_core url: "https://pub.dartlang.org" source: hosted - version: "7.2.4" + version: "7.2.7" built_collection: dependency: transitive description: @@ -126,7 +126,7 @@ packages: name: built_value url: "https://pub.dartlang.org" source: hosted - version: "8.4.1" + version: "8.4.2" cached_network_image: dependency: transitive description: @@ -154,7 +154,7 @@ packages: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.2.1" + version: "1.2.0" charcode: dependency: transitive description: @@ -175,14 +175,14 @@ packages: name: clock url: "https://pub.dartlang.org" source: hosted - version: "1.1.1" + version: "1.1.0" code_builder: dependency: transitive description: name: code_builder url: "https://pub.dartlang.org" source: hosted - version: "4.3.0" + version: "4.4.0" collection: dependency: transitive description: @@ -203,7 +203,7 @@ packages: name: convert url: "https://pub.dartlang.org" source: hosted - version: "3.0.2" + version: "3.1.0" cross_file: dependency: transitive description: @@ -352,7 +352,7 @@ packages: name: file_picker url: "https://pub.dartlang.org" source: hosted - version: "5.2.3" + version: "5.2.4" fixnum: dependency: transitive description: @@ -441,7 +441,7 @@ packages: name: flutter_svg url: "https://pub.dartlang.org" source: hosted - version: "1.1.5" + version: "1.1.6" flutter_web_plugins: dependency: transitive description: flutter @@ -467,7 +467,7 @@ packages: name: frontend_server_client url: "https://pub.dartlang.org" source: hosted - version: "2.1.3" + version: "3.2.0" get: dependency: "direct main" description: @@ -481,21 +481,21 @@ packages: name: glob url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.1.1" graphs: dependency: transitive description: name: graphs url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.2.0" html: dependency: transitive description: name: html url: "https://pub.dartlang.org" source: hosted - version: "0.15.0" + version: "0.15.1" http: dependency: "direct main" description: @@ -516,7 +516,7 @@ packages: name: http_parser url: "https://pub.dartlang.org" source: hosted - version: "4.0.1" + version: "4.0.2" icons_launcher: dependency: "direct dev" description: @@ -530,7 +530,7 @@ packages: name: image url: "https://pub.dartlang.org" source: hosted - version: "3.2.0" + version: "3.2.2" image_picker: dependency: "direct main" description: @@ -544,7 +544,7 @@ packages: name: image_picker_android url: "https://pub.dartlang.org" source: hosted - version: "0.8.5+3" + version: "0.8.5+4" image_picker_for_web: dependency: transitive description: @@ -558,7 +558,7 @@ packages: name: image_picker_ios url: "https://pub.dartlang.org" source: hosted - version: "0.8.6+1" + version: "0.8.6+3" image_picker_platform_interface: dependency: transitive description: @@ -600,7 +600,7 @@ packages: name: lints url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.0.1" logging: dependency: transitive description: @@ -621,14 +621,14 @@ packages: name: material_color_utilities url: "https://pub.dartlang.org" source: hosted - version: "0.1.5" + version: "0.1.4" meta: dependency: transitive description: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.8.0" + version: "1.7.0" mime: dependency: transitive description: @@ -705,7 +705,7 @@ packages: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.2" + version: "1.8.1" path_drawing: dependency: transitive description: @@ -733,7 +733,7 @@ packages: name: path_provider_android url: "https://pub.dartlang.org" source: hosted - version: "2.0.20" + version: "2.0.22" path_provider_ios: dependency: transitive description: @@ -797,6 +797,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.3" + pointycastle: + dependency: transitive + description: + name: pointycastle + url: "https://pub.dartlang.org" + source: hosted + version: "3.6.2" pool: dependency: transitive description: @@ -817,14 +824,14 @@ packages: name: provider url: "https://pub.dartlang.org" source: hosted - version: "6.0.3" + version: "6.0.5" pub_semver: dependency: transitive description: name: pub_semver url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" + version: "2.1.3" pubspec_parse: dependency: transitive description: @@ -845,7 +852,7 @@ packages: name: rxdart url: "https://pub.dartlang.org" source: hosted - version: "0.27.5" + version: "0.27.7" screen_retriever: dependency: transitive description: @@ -882,7 +889,7 @@ packages: name: shelf_web_socket url: "https://pub.dartlang.org" source: hosted - version: "1.0.2" + version: "1.0.3" simple_observable: dependency: transitive description: @@ -901,7 +908,7 @@ packages: name: source_gen url: "https://pub.dartlang.org" source: hosted - version: "1.2.5" + version: "1.2.6" source_span: dependency: transitive description: @@ -922,7 +929,7 @@ packages: name: sqflite_common url: "https://pub.dartlang.org" source: hosted - version: "2.3.0" + version: "2.4.0" stack_trace: dependency: transitive description: @@ -943,7 +950,7 @@ packages: name: stream_transform url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "2.1.0" string_scanner: dependency: transitive description: @@ -1034,14 +1041,14 @@ packages: name: url_launcher url: "https://pub.dartlang.org" source: hosted - version: "6.1.6" + version: "6.1.7" url_launcher_android: dependency: transitive description: name: url_launcher_android url: "https://pub.dartlang.org" source: hosted - version: "6.0.19" + version: "6.0.22" url_launcher_ios: dependency: transitive description: @@ -1090,7 +1097,7 @@ packages: name: uuid url: "https://pub.dartlang.org" source: hosted - version: "3.0.6" + version: "3.0.7" vector_math: dependency: transitive description: @@ -1104,35 +1111,35 @@ packages: name: video_player url: "https://pub.dartlang.org" source: hosted - version: "2.4.9" + version: "2.4.10" video_player_android: dependency: transitive description: name: video_player_android url: "https://pub.dartlang.org" source: hosted - version: "2.3.9" + version: "2.3.10" video_player_avfoundation: dependency: transitive description: name: video_player_avfoundation url: "https://pub.dartlang.org" source: hosted - version: "2.3.7" + version: "2.3.8" video_player_platform_interface: dependency: transitive description: name: video_player_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "5.1.4" + version: "6.0.1" video_player_web: dependency: transitive description: name: video_player_web url: "https://pub.dartlang.org" source: hosted - version: "2.0.12" + version: "2.0.13" visibility_detector: dependency: "direct main" description: @@ -1181,7 +1188,7 @@ packages: name: watcher url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "1.0.2" web_socket_channel: dependency: transitive description: @@ -1195,7 +1202,7 @@ packages: name: win32 url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "3.1.3" win32_registry: dependency: transitive description: diff --git a/src/lang/ca.rs b/src/lang/ca.rs index eb38cd436..4c85bbf0c 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -407,5 +407,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Select local keyboard type", ""), ("software_render_tip", ""), ("Always use software rendering", ""), + ("config_input", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index c5e4407a6..4b7bda3ea 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -7,7 +7,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Password", "密码"), ("Ready", "就绪"), ("Established", "已建立"), - ("connecting_status", "正在接入RustDesk网络..."), + ("connecting_status", "正在接入 RustDesk 网络..."), ("Enable Service", "允许服务"), ("Start Service", "启动服务"), ("Service is running", "服务正在运行"), @@ -138,13 +138,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to make direct connection to remote desktop", "无法建立直接连接"), ("Set Password", "设置密码"), ("OS Password", "操作系统密码"), - ("install_tip", "你正在运行未安装版本,由于UAC限制,作为被控端,会在某些情况下无法控制鼠标键盘,或者录制屏幕,请点击下面的按钮将RustDesk安装到系统,从而规避上述问题。"), + ("install_tip", "你正在运行未安装版本,由于UAC限制,作为被控端,会在某些情况下无法控制鼠标键盘,或者录制屏幕,请点击下面的按钮将 RustDesk 安装到系统,从而规避上述问题。"), ("Click to upgrade", "点击这里升级"), ("Click to download", "点击这里下载"), ("Click to update", "点击这里更新"), ("Configure", "配置"), - ("config_acc", "为了能够远程控制你的桌面, 请给予RustDesk\"辅助功能\" 权限。"), - ("config_screen", "为了能够远程访问你的桌面, 请给予RustDesk\"屏幕录制\" 权限。"), + ("config_acc", "为了能够远程控制你的桌面, 请给予 RustDesk \"辅助功能\" 权限。"), + ("config_screen", "为了能够远程访问你的桌面, 请给予 RustDesk \"屏幕录制\" 权限。"), ("Installing ...", "安装 ..."), ("Install", "安装"), ("Installation", "安装"), @@ -273,7 +273,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Do you accept?", "是否接受?"), ("Open System Setting", "打开系统设置"), ("How to get Android input permission?", "如何获取安卓的输入权限?"), - ("android_input_permission_tip1", "為了讓遠程設備通過鼠標或者觸屏控制您的安卓設備,你需要允許RustDesk使用\"無障礙\"服務。"), + ("android_input_permission_tip1", "為了讓遠程設備通過鼠標或者觸屏控制您的安卓設備,你需要允許 RustDesk 使用\"無障礙\"服務。"), ("android_input_permission_tip2", "请在接下来的系统设置页面里,找到并进入 [已安装的服务] 页面,将 [RustDesk Input] 服务开启。"), ("android_new_connection_tip", "收到新的连接控制请求,对方想要控制你当前的设备。"), ("android_service_will_start_tip", "开启录屏权限将自动开启服务,允许其他设备向此设备请求建立连接。"), @@ -298,9 +298,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("In privacy mode", "进入隐私模式"), ("Out privacy mode", "退出隐私模式"), ("Language", "语言"), - ("Keep RustDesk background service", "保持RustDesk后台服务"), + ("Keep RustDesk background service", "保持 RustDesk 后台服务"), ("Ignore Battery Optimizations", "忽略电池优化"), - ("android_open_battery_optimizations_tip", "如需关闭此功能,请在接下来的RustDesk应用设置页面中,找到并进入 [电源] 页面,取消勾选 [不受限制]"), + ("android_open_battery_optimizations_tip", "如需关闭此功能,请在接下来的 RustDesk 应用设置页面中,找到并进入 [电源] 页面,取消勾选 [不受限制]"), ("Connection not allowed", "对方不允许连接"), ("Legacy mode", "传统模式"), ("Map mode", "1:1传输"), @@ -380,7 +380,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland 需要更高版本的 linux 发行版。 请尝试 X11 桌面或更改您的操作系统。"), ("JumpLink", "查看"), ("Please Select the screen to be shared(Operate on the peer side).", "请选择要分享的画面(对端操作)。"), - ("Show RustDesk", "显示rustdesk"), + ("Show RustDesk", "显示 RustDesk"), ("This PC", "此电脑"), ("or", "或"), ("Continue with", "使用"), @@ -407,5 +407,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Select local keyboard type", "请选择本地键盘类型"), ("software_render_tip", "如果你使用英伟达显卡, 并且远程窗口在会话建立后会立刻关闭, 那么安装nouveau驱动并且选择使用软件渲染可能会有帮助。重启软件后生效。"), ("Always use software rendering", "使用软件渲染"), + ("config_input", "为了能够通过键盘控制远程桌面, 请给予 RustDesk \"输入监控\" 权限。"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 18b2673c9..741dd284f 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -407,5 +407,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Select local keyboard type", ""), ("software_render_tip", ""), ("Always use software rendering", ""), + ("config_input", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index a1e74259c..1783fd16c 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -407,5 +407,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Select local keyboard type", ""), ("software_render_tip", ""), ("Always use software rendering", ""), + ("config_input", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 44404d52f..6ee9fdaf6 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -407,5 +407,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Select local keyboard type", "Lokalen Tastaturtyp auswählen"), ("software_render_tip", ""), ("Always use software rendering", ""), + ("config_input", ""), ].iter().cloned().collect(); } diff --git a/src/lang/en.rs b/src/lang/en.rs index b8c8f074d..6675ee172 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -36,6 +36,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", "Allow hiding only if accepting sessions via password and using permanent password"), ("wayland_experiment_tip", "Wayland support is in experimental stage, please use X11 if you require unattended access."), ("Slogan_tip", "Made with heart in this chaotic world!"), - ("software_render_tip", "If you have an Nvidia graphics card and the remote window closes immediately after connecting, installing the nouveau driver and choosing to use software rendering may help. A software restart is required.") + ("software_render_tip", "If you have an Nvidia graphics card and the remote window closes immediately after connecting, installing the nouveau driver and choosing to use software rendering may help. A software restart is required."), + ("config_input", "In order to control remote desktop with keyboard, you need to grant RustDesk \"Input Monitoring\" permissions."), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 7b32c1708..a4da577c8 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -407,5 +407,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Select local keyboard type", ""), ("software_render_tip", ""), ("Always use software rendering", ""), + ("config_input", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 4a9f9251c..56453bd8c 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -407,5 +407,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Select local keyboard type", "Seleccionar tipo de teclado local"), ("software_render_tip", ""), ("Always use software rendering", ""), + ("config_input", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 8c0c426ed..8d4ba5cb1 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -407,5 +407,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Select local keyboard type", ""), ("software_render_tip", ""), ("Always use software rendering", ""), + ("config_input", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 22b522a9d..d48d5cfae 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -407,5 +407,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Select local keyboard type", "Selectionner la disposition du clavier local"), ("software_render_tip", ""), ("Always use software rendering", ""), + ("config_input", ""), ].iter().cloned().collect(); } diff --git a/src/lang/gr.rs b/src/lang/gr.rs index 9ca035e65..1ac063cd5 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -407,5 +407,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Select local keyboard type", ""), ("software_render_tip", ""), ("Always use software rendering", ""), + ("config_input", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 2b81b90eb..be3028283 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -407,5 +407,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Select local keyboard type", ""), ("software_render_tip", ""), ("Always use software rendering", ""), + ("config_input", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index ecc21b3f0..14eff9458 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -407,5 +407,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Select local keyboard type", ""), ("software_render_tip", ""), ("Always use software rendering", ""), + ("config_input", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 31cfd345e..e82da1d66 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -407,5 +407,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Select local keyboard type", "Seleziona il tipo di tastiera locale"), ("software_render_tip", ""), ("Always use software rendering", ""), + ("config_input", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 4673d2e41..dc3d81448 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -407,5 +407,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Select local keyboard type", ""), ("software_render_tip", ""), ("Always use software rendering", ""), + ("config_input", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 5d0b8c8a7..11536f0a5 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -407,5 +407,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Select local keyboard type", ""), ("software_render_tip", ""), ("Always use software rendering", ""), + ("config_input", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 0b55a79ff..b0dd2ff70 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -407,5 +407,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Select local keyboard type", ""), ("software_render_tip", ""), ("Always use software rendering", ""), + ("config_input", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 5ca41e3ac..2641d8358 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -407,5 +407,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Select local keyboard type", ""), ("software_render_tip", ""), ("Always use software rendering", ""), + ("config_input", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 3e203a250..c96aed8b6 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -407,5 +407,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Select local keyboard type", ""), ("software_render_tip", ""), ("Always use software rendering", ""), + ("config_input", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index c17620bc1..03d451662 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -407,5 +407,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Select local keyboard type", ""), ("software_render_tip", ""), ("Always use software rendering", ""), + ("config_input", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 1fa6d7528..14e04b940 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -407,5 +407,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Select local keyboard type", "Выберите тип локальной клавиатуры"), ("software_render_tip", ""), ("Always use software rendering", ""), + ("config_input", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 13bbcf4f7..1cfff5beb 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -407,5 +407,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Select local keyboard type", ""), ("software_render_tip", ""), ("Always use software rendering", ""), + ("config_input", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 7cd8f0c98..8761db113 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -405,5 +405,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the web console", "Ročno zaprto iz spletne konzole"), ("Local keyboard type", "Lokalna vrsta tipkovnice"), ("Select local keyboard type", "Izberite lokalno vrsto tipkovnice"), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index d08036bf3..b277c6f2d 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -407,5 +407,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Select local keyboard type", ""), ("software_render_tip", ""), ("Always use software rendering", ""), + ("config_input", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index f9386004d..cc418d305 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -407,5 +407,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Select local keyboard type", ""), ("software_render_tip", ""), ("Always use software rendering", ""), + ("config_input", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 9f2d1c9f4..0a41387c7 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -407,5 +407,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Select local keyboard type", ""), ("software_render_tip", ""), ("Always use software rendering", ""), + ("config_input", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 145cf45bb..87a1563d7 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -407,5 +407,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Select local keyboard type", ""), ("software_render_tip", ""), ("Always use software rendering", ""), + ("config_input", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index d6bbe806d..6a98613ed 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -407,6 +407,7 @@ lazy_static::lazy_static! { ("Select local keyboard type", "เลือกประเภทคีย์บอร์ด"), ("software_render_tip", ""), ("Always use software rendering", ""), + ("config_input", ""), ].iter().cloned().collect(); } \ No newline at end of file diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 00c620c34..931bcec6d 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -407,5 +407,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Select local keyboard type", ""), ("software_render_tip", ""), ("Always use software rendering", ""), + ("config_input", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index cb83d28ea..847edb599 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -407,5 +407,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Select local keyboard type", "請選擇本地鍵盤類型"), ("software_render_tip", "如果你使用英偉達顯卡, 並且遠程窗口在會話建立後會立刻關閉, 那麼安裝nouveau驅動並且選擇使用軟件渲染可能會有幫助。重啟軟件後生效。"), ("Always use software rendering", "使用軟件渲染"), + ("config_input", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 1c6ac5828..65ae32d68 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -407,5 +407,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Select local keyboard type", ""), ("software_render_tip", ""), ("Always use software rendering", ""), + ("config_input", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index d2e067b3b..eab0b3497 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -407,5 +407,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Select local keyboard type", ""), ("software_render_tip", ""), ("Always use software rendering", ""), + ("config_input", ""), ].iter().cloned().collect(); } diff --git a/src/platform/macos.mm b/src/platform/macos.mm index b82b269e2..e6e1723a7 100644 --- a/src/platform/macos.mm +++ b/src/platform/macos.mm @@ -2,6 +2,8 @@ #import #import +// https://github.com/codebytere/node-mac-permissions/blob/main/permissions.mm + extern "C" bool InputMonitoringAuthStatus(bool prompt) { if (@available(macos 10.15, *)) { IOHIDAccessType theType = IOHIDCheckAccess(kIOHIDRequestTypeListenEvent); From 584295f3fadcb065ab9e63bb11f6ddfade99bdc5 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 6 Jan 2023 13:19:08 +0800 Subject: [PATCH 1358/2015] config_input not well tested yet --- flutter/lib/desktop/pages/desktop_home_page.dart | 13 +++++++++++++ src/platform/macos.mm | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 1e8512b2e..43ffbd1e3 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -42,6 +42,7 @@ class _DesktopHomePageState extends State var svcStopped = false.obs; var watchIsCanScreenRecording = false; var watchIsProcessTrust = false; + var watchIsInputMonitoring = false; Timer? _updateTimer; @override @@ -334,6 +335,12 @@ class _DesktopHomePageState extends State bind.mainIsProcessTrusted(prompt: true); watchIsProcessTrust = true; }, help: 'Help', link: translate("doc_mac_permission")); + } else if (!bind.mainIsCanInputMonitoring(prompt: false)) { + return buildInstallCard("Permissions", "config_input", "Configure", + () async { + bind.mainIsCanInputMonitoring(prompt: true); + watchIsInputMonitoring = true; + }, help: 'Help', link: translate("doc_mac_permission")); } else if (!svcStopped.value && bind.mainIsInstalled() && !bind.mainIsInstalledDaemon(prompt: false)) { @@ -467,6 +474,12 @@ class _DesktopHomePageState extends State setState(() {}); } } + if (watchIsInputMonitoring) { + if (bind.mainIsCanInputMonitoring(prompt: false)) { + watchIsInputMonitoring = false; + setState(() {}); + } + } }); Get.put(svcStopped, tag: 'stop-service'); rustDeskWinManager.registerActiveWindowListener(onActiveWindowChanged); diff --git a/src/platform/macos.mm b/src/platform/macos.mm index e6e1723a7..05653ec33 100644 --- a/src/platform/macos.mm +++ b/src/platform/macos.mm @@ -7,7 +7,7 @@ extern "C" bool InputMonitoringAuthStatus(bool prompt) { if (@available(macos 10.15, *)) { IOHIDAccessType theType = IOHIDCheckAccess(kIOHIDRequestTypeListenEvent); - NSLog(@"IOHIDCheckAccess = %d", theType); + NSLog(@"IOHIDCheckAccess = %d, kIOHIDAccessTypeGranted = %d", theType, kIOHIDAccessTypeGranted); switch (theType) { case kIOHIDAccessTypeGranted: return true; From 5a91701d3d44f1312b68c8a575d28ae5e4563bf4 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 6 Jan 2023 15:37:15 +0800 Subject: [PATCH 1359/2015] fix ci --- src/platform/macos.mm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform/macos.mm b/src/platform/macos.mm index 05653ec33..c25a854cc 100644 --- a/src/platform/macos.mm +++ b/src/platform/macos.mm @@ -5,7 +5,7 @@ // https://github.com/codebytere/node-mac-permissions/blob/main/permissions.mm extern "C" bool InputMonitoringAuthStatus(bool prompt) { - if (@available(macos 10.15, *)) { + if (floor(NSAppKitVersionNumber) >= NSAppKitVersionNumber10_15) { IOHIDAccessType theType = IOHIDCheckAccess(kIOHIDRequestTypeListenEvent); NSLog(@"IOHIDCheckAccess = %d, kIOHIDAccessTypeGranted = %d", theType, kIOHIDAccessTypeGranted); switch (theType) { From 56e699a5e63f2a0156d12a53ba10fecabb16c162 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 6 Jan 2023 17:10:38 +0800 Subject: [PATCH 1360/2015] adjust input style of "enter id" --- flutter/lib/desktop/pages/connection_page.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 7500fe99e..939acfbc5 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -6,7 +6,6 @@ import 'dart:io'; import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_hbb/common/widgets/address_book.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart'; import 'package:get/get.dart'; @@ -16,7 +15,6 @@ import 'package:window_manager/window_manager.dart'; import '../../common.dart'; import '../../common/formatter/id_formatter.dart'; import '../../common/widgets/peer_tab_page.dart'; -import '../../common/widgets/peers_view.dart'; import '../../models/platform_model.dart'; import '../widgets/button.dart'; @@ -172,6 +170,7 @@ class _ConnectionPageState extends State Expanded( child: Obx( () => TextField( + maxLength: 90, autocorrect: false, enableSuggestions: false, keyboardType: TextInputType.visiblePassword, @@ -179,7 +178,7 @@ class _ConnectionPageState extends State style: const TextStyle( fontFamily: 'WorkSans', fontSize: 22, - height: 1, + height: 1.25, ), maxLines: 1, cursorColor: From 3e357159f3afa10f93669d2dd34f1b5ea4e5e8e1 Mon Sep 17 00:00:00 2001 From: csf Date: Fri, 6 Jan 2023 19:26:19 +0900 Subject: [PATCH 1361/2015] refactor user login: 1. opt request json type. 2. desktop and mobile use same loginDialog. 3. opt loginDialog UI style. 4. opt login request Exception catch. --- flutter/lib/common.dart | 2 +- flutter/lib/common/hbbs/hbbs.dart | 92 +++- flutter/lib/common/widgets/address_book.dart | 13 +- flutter/lib/desktop/widgets/login.dart | 424 +++++++++--------- flutter/lib/mobile/pages/connection_page.dart | 5 +- flutter/lib/mobile/pages/settings_page.dart | 74 +-- flutter/lib/models/ab_model.dart | 5 +- flutter/lib/models/group_model.dart | 4 +- flutter/lib/models/user_model.dart | 65 +-- 9 files changed, 363 insertions(+), 321 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 8d047dd0d..23847ab52 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1367,7 +1367,7 @@ connect(BuildContext context, String id, } } -Future> getHttpHeaders() async { +Map getHttpHeaders() { return { 'Authorization': 'Bearer ${bind.mainGetLocalOption(key: 'access_token')}' }; diff --git a/flutter/lib/common/hbbs/hbbs.dart b/flutter/lib/common/hbbs/hbbs.dart index 856f88d20..6a80f8269 100644 --- a/flutter/lib/common/hbbs/hbbs.dart +++ b/flutter/lib/common/hbbs/hbbs.dart @@ -1,12 +1,22 @@ import 'package:flutter_hbb/models/peer_model.dart'; +class HttpType { + static const kAuthReqTypeAccount = "account"; + static const kAuthReqTypeMobile = "mobile"; + static const kAuthReqTypeSMSCode = "sms_code"; + static const kAuthReqTypeEmailCode = "email_code"; + + static const kAuthResTypeToken = "access_token"; + static const kAuthResTypeEmailCheck = "email_check"; +} + class UserPayload { String name = ''; String email = ''; String note = ''; int? status; String grp = ''; - bool is_admin = false; + bool isAdmin = false; UserPayload.fromJson(Map json) : name = json['name'] ?? '', @@ -14,7 +24,7 @@ class UserPayload { note = json['note'] ?? '', status = json['status'], grp = json['grp'] ?? '', - is_admin = json['is_admin'] == true; + isAdmin = json['is_admin'] == true; } class PeerPayload { @@ -37,3 +47,81 @@ class PeerPayload { return Peer.fromJson({"id": p.id}); } } + +class LoginRequest { + String? username; + String? password; + String? id; + String? uuid; + bool? autoLogin; + String? type; + String? verificationCode; + String? deviceInfo; + + LoginRequest( + {this.username, + this.password, + this.id, + this.uuid, + this.autoLogin, + this.type, + this.verificationCode, + this.deviceInfo}); + + LoginRequest.fromJson(Map json) { + username = json['username']; + password = json['password']; + id = json['id']; + uuid = json['uuid']; + autoLogin = json['autoLogin']; + type = json['type']; + verificationCode = json['verificationCode']; + deviceInfo = json['deviceInfo']; + } + + Map toJson() { + final Map data = {}; + data['username'] = username ?? ''; + data['password'] = password ?? ''; + data['id'] = id ?? ''; + data['uuid'] = uuid ?? ''; + data['autoLogin'] = autoLogin ?? ''; + data['type'] = type ?? ''; + data['verificationCode'] = verificationCode ?? ''; + data['deviceInfo'] = deviceInfo ?? ''; + return data; + } +} + +class LoginResponse { + String? access_token; + String? type; + UserPayload? user; + + LoginResponse({this.access_token, this.type, this.user}); + + LoginResponse.fromJson(Map json) { + access_token = json['access_token']; + type = json['type']; + print("user: ${json['user']}"); + print("user id: ${json['user']['id']}"); + print("user name: ${json['user']['name']}"); + print("user email: ${json['user']['id']}"); + print("user note: ${json['user']['note']}"); + print("user status: ${json['user']['status']}"); + print("user grp: ${json['user']['grp']}"); + print("user is_admin: ${json['user']['is_admin']}"); + user = json['user'] != null ? UserPayload.fromJson(json['user']) : null; + } +} + +class RequestException implements Exception { + int statusCode; + String cause; + RequestException(this.statusCode, this.cause); + + @override + String toString() { + return "RequestException, statusCode: $statusCode, error: $cause"; + } +} diff --git a/flutter/lib/common/widgets/address_book.dart b/flutter/lib/common/widgets/address_book.dart index fbeca25b2..3e7f46814 100644 --- a/flutter/lib/common/widgets/address_book.dart +++ b/flutter/lib/common/widgets/address_book.dart @@ -9,8 +9,6 @@ import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu; import 'package:get/get.dart'; import '../../common.dart'; -import '../../desktop/pages/desktop_home_page.dart'; -import '../../mobile/pages/settings_page.dart'; class AddressBook extends StatefulWidget { final EdgeInsets? menuPadding; @@ -41,21 +39,12 @@ class _AddressBookState extends State { } }); - handleLogin() { - // TODO refactor login dialog for desktop and mobile - if (isDesktop) { - loginDialog(); - } else { - showLogin(gFFI.dialogManager); - } - } - Future buildBody(BuildContext context) async { return Obx(() { if (gFFI.userModel.userName.value.isEmpty) { return Center( child: InkWell( - onTap: handleLogin, + onTap: loginDialog, child: Text( translate("Login"), style: const TextStyle(decoration: TextDecoration.underline), diff --git a/flutter/lib/desktop/widgets/login.dart b/flutter/lib/desktop/widgets/login.dart index 62e6ebc53..8092dfed6 100644 --- a/flutter/lib/desktop/widgets/login.dart +++ b/flutter/lib/desktop/widgets/login.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common/hbbs/hbbs.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:get/get.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -9,18 +10,21 @@ import 'package:url_launcher/url_launcher.dart'; import '../../common.dart'; -final _kMidButtonPadding = const EdgeInsets.fromLTRB(15, 0, 15, 0); - class _IconOP extends StatelessWidget { final String icon; final double iconWidth; - const _IconOP({Key? key, required this.icon, required this.iconWidth}) + final EdgeInsets margin; + const _IconOP( + {Key? key, + required this.icon, + required this.iconWidth, + this.margin = const EdgeInsets.symmetric(horizontal: 4.0)}) : super(key: key); @override Widget build(BuildContext context) { return Container( - margin: const EdgeInsets.symmetric(horizontal: 4.0), + margin: margin, child: SvgPicture.asset( 'assets/$icon.svg', width: iconWidth, @@ -50,33 +54,33 @@ class ButtonOP extends StatelessWidget { @override Widget build(BuildContext context) { return Row(children: [ - Expanded( - child: Container( - height: height, - padding: _kMidButtonPadding, - child: Obx(() => ElevatedButton( - style: ElevatedButton.styleFrom( - primary: curOP.value.isEmpty || curOP.value == op - ? primaryColor - : Colors.grey, - ).copyWith(elevation: ButtonStyleButton.allOrNull(0.0)), - onPressed: - curOP.value.isEmpty || curOP.value == op ? onTap : null, - child: Stack(children: [ - Center(child: Text('${translate("Continue with")} $op')), - Align( - alignment: Alignment.centerLeft, - child: SizedBox( - width: 120, - child: _IconOP( - icon: op, - iconWidth: iconWidth, - )), - ), - ]), - )), - ), - ) + Container( + height: height, + width: 200, + child: Obx(() => ElevatedButton( + style: ElevatedButton.styleFrom( + primary: curOP.value.isEmpty || curOP.value == op + ? primaryColor + : Colors.grey, + ).copyWith(elevation: ButtonStyleButton.allOrNull(0.0)), + onPressed: curOP.value.isEmpty || curOP.value == op ? onTap : null, + child: Row( + children: [ + SizedBox( + width: 30, + child: _IconOP( + icon: op, + iconWidth: iconWidth, + margin: EdgeInsets.only(right: 5), + )), + Expanded( + child: FittedBox( + fit: BoxFit.scaleDown, + child: Center( + child: Text('${translate("Continue with")} $op')))), + ], + ))), + ), ]); } } @@ -277,22 +281,25 @@ class LoginWidgetOP extends StatelessWidget { children.removeLast(); } return SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: children, - )); + child: Container( + width: 200, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: children, + ))); } } class LoginWidgetUserPass extends StatelessWidget { - final String username; - final String pass; - final String usernameMsg; - final String passMsg; + final TextEditingController username; + final TextEditingController pass; + final String? usernameMsg; + final String? passMsg; final bool isInProgress; final RxString curOP; - final Function(String, String) onLogin; + final RxBool autoLogin; + final Function() onLogin; const LoginWidgetUserPass({ Key? key, required this.username, @@ -301,129 +308,135 @@ class LoginWidgetUserPass extends StatelessWidget { required this.passMsg, required this.isInProgress, required this.curOP, + required this.autoLogin, required this.onLogin, }) : super(key: key); @override Widget build(BuildContext context) { - var userController = TextEditingController(text: username); - var pwdController = TextEditingController(text: pass); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, + return Padding( + padding: EdgeInsets.all(0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(height: 8.0), + DialogTextField( + title: '${translate("Username")}:', + controller: username, + autoFocus: true, + errorText: usernameMsg), + DialogTextField( + title: '${translate("Password")}:', + obscureText: true, + controller: pass, + errorText: passMsg), + Obx(() => CheckboxListTile( + contentPadding: const EdgeInsets.all(0), + dense: true, + controlAffinity: ListTileControlAffinity.leading, + title: Text( + translate("Remember me"), + ), + value: autoLogin.value, + onChanged: (v) { + if (v == null) return; + autoLogin.value = v; + }, + )), + Offstage( + offstage: !isInProgress, + child: const LinearProgressIndicator()), + const SizedBox(height: 12.0), + FittedBox( + child: + Row(mainAxisAlignment: MainAxisAlignment.center, children: [ + Container( + height: 38, + width: 200, + child: Obx(() => ElevatedButton( + child: Text( + translate('Login'), + style: TextStyle(fontSize: 16), + ), + onPressed: + curOP.value.isEmpty || curOP.value == 'rustdesk' + ? () { + onLogin(); + } + : null, + )), + ), + ])), + ], + )); + } +} + +class DialogTextField extends StatelessWidget { + final String title; + final bool autoFocus; + final bool obscureText; + final String? errorText; + final TextEditingController controller; + final FocusNode focusNode = FocusNode(); + + DialogTextField( + {Key? key, + this.autoFocus = false, + this.obscureText = false, + this.errorText, + required this.title, + required this.controller}) + : super(key: key) { + // todo mobile requestFocus, on mobile, widget will reload every time the text changes + if (autoFocus && isDesktop) { + Timer(Duration(milliseconds: 200), () => focusNode.requestFocus()); + } + } + + @override + Widget build(BuildContext context) { + return Row( children: [ - const SizedBox( - height: 8.0, - ), - Container( - padding: _kMidButtonPadding, - child: Row( - children: [ - ConstrainedBox( - constraints: const BoxConstraints(minWidth: 100), - child: Text( - '${translate("Username")}:', - textAlign: TextAlign.start, - ).marginOnly(bottom: 16.0)), - const SizedBox( - width: 24.0, - ), - Expanded( - child: TextField( - decoration: InputDecoration( - border: const OutlineInputBorder(), - errorText: usernameMsg.isNotEmpty ? usernameMsg : null), - controller: userController, - focusNode: FocusNode()..requestFocus(), - ), - ), - ], + Expanded( + child: TextField( + decoration: InputDecoration( + labelText: title, + border: const OutlineInputBorder(), + errorText: errorText), + controller: controller, + focusNode: focusNode, + autofocus: true, + obscureText: obscureText, ), ), - const SizedBox( - height: 8.0, - ), - Container( - padding: _kMidButtonPadding, - child: Row( - children: [ - ConstrainedBox( - constraints: const BoxConstraints(minWidth: 100), - child: Text('${translate("Password")}:') - .marginOnly(bottom: 16.0)), - const SizedBox( - width: 24.0, - ), - Expanded( - child: TextField( - obscureText: true, - decoration: InputDecoration( - border: const OutlineInputBorder(), - errorText: passMsg.isNotEmpty ? passMsg : null), - controller: pwdController, - ), - ), - ], - ), - ), - const SizedBox( - height: 4.0, - ), - Offstage( - offstage: !isInProgress, child: const LinearProgressIndicator()), - const SizedBox( - height: 12.0, - ), - Row(children: [ - Expanded( - child: Container( - height: 38, - padding: _kMidButtonPadding, - child: Obx(() => ElevatedButton( - style: curOP.value.isEmpty || curOP.value == 'rustdesk' - ? null - : ElevatedButton.styleFrom( - primary: Colors.grey, - ), - child: Text( - translate('Login'), - style: TextStyle(fontSize: 16), - ), - onPressed: curOP.value.isEmpty || curOP.value == 'rustdesk' - ? () { - onLogin(userController.text, pwdController.text); - } - : null, - )), - ), - ), - ]), ], - ); + ).paddingSymmetric(vertical: 4.0); } } /// common login dialog for desktop /// call this directly -Future loginDialog() async { - String username = ''; - var usernameMsg = ''; - String pass = ''; - var passMsg = ''; +Future loginDialog() async { + var username = TextEditingController(); + var password = TextEditingController(); + + String? usernameMsg; + String? passwordMsg; var isInProgress = false; - var completer = Completer(); + final autoLogin = true.obs; final RxString curOP = ''.obs; - gFFI.dialogManager.show((setState, close) { + return gFFI.dialogManager.show((setState, close) { cancel() { isInProgress = false; - completer.complete(false); - close(); + close(false); } - onLogin(String username0, String pass0) async { + onLogin() async { setState(() { - usernameMsg = ''; - passMsg = ''; + usernameMsg = null; + passwordMsg = null; isInProgress = true; }); cancel() { @@ -436,30 +449,44 @@ Future loginDialog() async { } curOP.value = 'rustdesk'; - username = username0; - pass = pass0; - if (username.isEmpty) { + if (username.text.isEmpty) { usernameMsg = translate('Username missed'); cancel(); return; } - if (pass.isEmpty) { - passMsg = translate('Password missed'); + if (password.text.isEmpty) { + passwordMsg = translate('Password missed'); cancel(); return; } try { - final resp = await gFFI.userModel.login(username, pass); - if (resp.containsKey('error')) { - passMsg = resp['error']; - cancel(); - return; + final resp = await gFFI.userModel.login(LoginRequest( + username: username.text, + password: password.text, + id: await bind.mainGetMyId(), + uuid: await bind.mainGetUuid(), + autoLogin: autoLogin.value, + type: HttpType.kAuthReqTypeAccount)); + + switch (resp.type) { + case HttpType.kAuthResTypeToken: + if (resp.access_token != null) { + bind.mainSetLocalOption( + key: 'access_token', value: resp.access_token!); + close(true); + return; + } + break; + case HttpType.kAuthResTypeEmailCheck: + break; } - // {access_token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJndWlkIjoiMDFkZjQ2ZjgtZjg3OS00MDE0LTk5Y2QtMGMwYzM2MmViZGJlIiwiZXhwIjoxNjYxNDg2NzYwfQ.GZpe1oI8TfM5yTYNrpcwbI599P4Z_-b2GmnwNl2Lr-w, - // token_type: Bearer, user: {id: , name: admin, email: null, note: null, status: null, grp: null, is_admin: true}} - debugPrint('$resp'); - completer.complete(true); + } on RequestException catch (err) { + passwordMsg = translate(err.cause); + debugPrintStack(label: err.toString()); + cancel(); + return; } catch (err) { + passwordMsg = "Unknown Error"; debugPrintStack(label: err.toString()); cancel(); return; @@ -469,53 +496,50 @@ Future loginDialog() async { return CustomAlertDialog( title: Text(translate('Login')), - content: ConstrainedBox( - constraints: const BoxConstraints(minWidth: 500), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox( - height: 8.0, - ), - LoginWidgetUserPass( - username: username, - pass: pass, - usernameMsg: usernameMsg, - passMsg: passMsg, - isInProgress: isInProgress, - curOP: curOP, - onLogin: onLogin, - ), - const SizedBox( - height: 8.0, - ), - Center( - child: Text( - translate('or'), - style: TextStyle(fontSize: 16), - )), - const SizedBox( - height: 8.0, - ), - LoginWidgetOP( - ops: [ - ConfigOP(op: 'Github', iconWidth: 20), - ConfigOP(op: 'Google', iconWidth: 20), - ConfigOP(op: 'Okta', iconWidth: 38), - ], - curOP: curOP, - cbLogin: (String username) { - gFFI.userModel.userName.value = username; - completer.complete(true); - close(); - }, - ), - ], - ), + contentBoxConstraints: BoxConstraints(minWidth: 400), + content: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox( + height: 8.0, + ), + LoginWidgetUserPass( + username: username, + pass: password, + usernameMsg: usernameMsg, + passMsg: passwordMsg, + isInProgress: isInProgress, + curOP: curOP, + autoLogin: autoLogin, + onLogin: onLogin, + ), + const SizedBox( + height: 8.0, + ), + Center( + child: Text( + translate('or'), + style: TextStyle(fontSize: 16), + )), + const SizedBox( + height: 8.0, + ), + LoginWidgetOP( + ops: [ + ConfigOP(op: 'Github', iconWidth: 20), + ConfigOP(op: 'Google', iconWidth: 20), + ConfigOP(op: 'Okta', iconWidth: 38), + ], + curOP: curOP, + cbLogin: (String username) { + gFFI.userModel.userName.value = username; + close(true); + }, + ), + ], ), actions: [msgBoxButton(translate('Close'), cancel)], onCancel: cancel, ); }); - return completer.future; } diff --git a/flutter/lib/mobile/pages/connection_page.dart b/flutter/lib/mobile/pages/connection_page.dart index 957910324..b8104387e 100644 --- a/flutter/lib/mobile/pages/connection_page.dart +++ b/flutter/lib/mobile/pages/connection_page.dart @@ -7,10 +7,9 @@ import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; import '../../common.dart'; -import '../../common/widgets/address_book.dart'; import '../../common/widgets/peer_tab_page.dart'; -import '../../common/widgets/peers_view.dart'; import '../../consts.dart'; +import '../../desktop/widgets/login.dart'; import '../../models/model.dart'; import '../../models/platform_model.dart'; import 'home_page.dart'; @@ -258,7 +257,7 @@ class _WebMenuState extends State { } if (value == 'login') { if (gFFI.userModel.userName.value.isEmpty) { - showLogin(gFFI.dialogManager); + loginDialog(); } else { gFFI.userModel.logOut(); } diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index 9637ecb40..0764f8247 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -9,6 +9,7 @@ import 'package:url_launcher/url_launcher.dart'; import '../../common.dart'; import '../../common/widgets/dialog.dart'; +import '../../desktop/widgets/login.dart'; import '../../models/model.dart'; import '../../models/platform_model.dart'; import '../widgets/dialog.dart'; @@ -300,7 +301,7 @@ class _SettingsState extends State with WidgetsBindingObserver { leading: Icon(Icons.person), onPressed: (context) { if (gFFI.userModel.userName.value.isEmpty) { - showLogin(gFFI.dialogManager); + loginDialog(); } else { gFFI.userModel.logOut(); } @@ -482,77 +483,6 @@ void showAbout(OverlayDialogManager dialogManager) { }, clickMaskDismiss: true, backDismiss: true); } -void showLogin(OverlayDialogManager dialogManager) { - final passwordController = TextEditingController(); - final nameController = TextEditingController(); - var loading = false; - var error = ''; - dialogManager.show((setState, close) { - return CustomAlertDialog( - title: Text(translate('Login')), - content: Column(mainAxisSize: MainAxisSize.min, children: [ - TextField( - autofocus: true, - autocorrect: false, - enableSuggestions: false, - keyboardType: TextInputType.visiblePassword, - decoration: InputDecoration( - labelText: translate('Username'), - ), - controller: nameController, - ), - PasswordWidget(controller: passwordController, autoFocus: false), - ]), - actions: (loading - ? [CircularProgressIndicator()] - : (error != "" - ? [ - Text(translate(error), - style: TextStyle(color: Colors.red)) - ] - : [])) + - [ - TextButton( - style: flatButtonStyle, - onPressed: loading - ? null - : () { - close(); - setState(() { - loading = false; - }); - }, - child: Text(translate('Cancel')), - ), - TextButton( - style: flatButtonStyle, - onPressed: loading - ? null - : () async { - final name = nameController.text.trim(); - final pass = passwordController.text.trim(); - if (name != "" && pass != "") { - setState(() { - loading = true; - }); - final resp = await gFFI.userModel.login(name, pass); - setState(() { - loading = false; - }); - if (resp.containsKey('error')) { - error = resp['error']; - return; - } - } - close(); - }, - child: Text(translate('OK')), - ), - ], - ); - }); -} - class ScanButton extends StatelessWidget { @override Widget build(BuildContext context) { diff --git a/flutter/lib/models/ab_model.dart b/flutter/lib/models/ab_model.dart index d8a0e8f99..175c8424b 100644 --- a/flutter/lib/models/ab_model.dart +++ b/flutter/lib/models/ab_model.dart @@ -27,8 +27,7 @@ class AbModel { abError.value = ""; final api = "${await bind.mainGetApiServer()}/api/ab/get"; try { - final resp = - await http.post(Uri.parse(api), headers: await getHttpHeaders()); + final resp = await http.post(Uri.parse(api), headers: getHttpHeaders()); if (resp.body.isNotEmpty && resp.body.toLowerCase() != "null") { Map json = jsonDecode(resp.body); if (json.containsKey('error')) { @@ -102,7 +101,7 @@ class AbModel { Future pushAb() async { abLoading.value = true; final api = "${await bind.mainGetApiServer()}/api/ab"; - var authHeaders = await getHttpHeaders(); + var authHeaders = getHttpHeaders(); authHeaders['Content-Type'] = "application/json"; final peersJsonData = peers.map((e) => e.toJson()).toList(); final body = jsonEncode({ diff --git a/flutter/lib/models/group_model.dart b/flutter/lib/models/group_model.dart index f220d62f1..4d9fab0e4 100644 --- a/flutter/lib/models/group_model.dart +++ b/flutter/lib/models/group_model.dart @@ -59,7 +59,7 @@ class GroupModel { if (gFFI.userModel.isAdmin.isFalse) 'grp': gFFI.userModel.groupName.value, }); - final resp = await http.get(uri, headers: await getHttpHeaders()); + final resp = await http.get(uri, headers: getHttpHeaders()); if (resp.body.isNotEmpty && resp.body.toLowerCase() != "null") { Map json = jsonDecode(resp.body); if (json.containsKey('error')) { @@ -110,7 +110,7 @@ class GroupModel { 'grp': gFFI.userModel.groupName.value, 'target_user': username }); - final resp = await http.get(uri, headers: await getHttpHeaders()); + final resp = await http.get(uri, headers: getHttpHeaders()); if (resp.body.isNotEmpty && resp.body.toLowerCase() != "null") { Map json = jsonDecode(resp.body); if (json.containsKey('error')) { diff --git a/flutter/lib/models/user_model.dart b/flutter/lib/models/user_model.dart index e5d2c9e15..79a9778b0 100644 --- a/flutter/lib/models/user_model.dart +++ b/flutter/lib/models/user_model.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:convert'; +import 'package:flutter_hbb/common/hbbs/hbbs.dart'; import 'package:flutter_hbb/common/widgets/peer_tab_page.dart'; import 'package:get/get.dart'; import 'package:http/http.dart' as http; @@ -45,7 +46,9 @@ class UserModel { if (error != null) { throw error; } - await _parseUserInfo(data); + + final user = UserPayload.fromJson(data); + await _parseAndUpdateUser(user); } catch (e) { print('Failed to refreshCurrentUser: $e'); } finally { @@ -55,7 +58,6 @@ class UserModel { Future reset() async { await bind.mainSetLocalOption(key: 'access_token', value: ''); - await bind.mainSetLocalOption(key: 'user_info', value: ''); await gFFI.abModel.reset(); await gFFI.groupModel.reset(); userName.value = ''; @@ -63,11 +65,10 @@ class UserModel { statePeerTab.check(); } - Future _parseUserInfo(dynamic userinfo) async { - bind.mainSetLocalOption(key: 'user_info', value: jsonEncode(userinfo)); - userName.value = userinfo['name'] ?? ''; - groupName.value = userinfo['grp'] ?? ''; - isAdmin.value = userinfo['is_admin'] == true; + Future _parseAndUpdateUser(UserPayload user) async { + userName.value = user.name; + groupName.value = user.grp; + isAdmin.value = user.isAdmin; } Future _updateOtherModels() async { @@ -85,7 +86,7 @@ class UserModel { 'id': await bind.mainGetMyId(), 'uuid': await bind.mainGetUuid(), }, - headers: await getHttpHeaders()) + headers: getHttpHeaders()) .timeout(Duration(seconds: 2)); } catch (e) { print("request /api/logout failed: err=$e"); @@ -95,26 +96,38 @@ class UserModel { } } - Future> login(String userName, String pass) async { + /// throw [RequestException] + Future login(LoginRequest loginRequest) async { final url = await bind.mainGetApiServer(); + final resp = await http.post(Uri.parse('$url/api/login'), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode(loginRequest.toJson())); + + final Map body; try { - final resp = await http.post(Uri.parse('$url/api/login'), - headers: {'Content-Type': 'application/json'}, - body: jsonEncode({ - 'username': userName, - 'password': pass, - 'id': await bind.mainGetMyId(), - 'uuid': await bind.mainGetUuid() - })); - final body = jsonDecode(resp.body); - bind.mainSetLocalOption( - key: 'access_token', value: body['access_token'] ?? ''); - await _parseUserInfo(body['user']); - return body; - } catch (err) { - return {'error': '$err'}; - } finally { - await _updateOtherModels(); + body = jsonDecode(resp.body); + } catch (e) { + print("jsonDecode resp body failed: ${e.toString()}"); + rethrow; } + + if (resp.statusCode != 200) { + throw RequestException(resp.statusCode, body['error'] ?? ''); + } + + final LoginResponse loginResponse; + try { + loginResponse = LoginResponse.fromJson(body); + } catch (e) { + print("jsonDecode LoginResponse failed: ${e.toString()}"); + rethrow; + } + + if (loginResponse.user != null) { + await _parseAndUpdateUser(loginResponse.user!); + } + + await _updateOtherModels(); + return loginResponse; } } From c5e39f4bbbc9be670b793fb3dd01e68c87dd44ad Mon Sep 17 00:00:00 2001 From: sj6219 Date: Fri, 6 Jan 2023 20:07:43 +0900 Subject: [PATCH 1362/2015] windows extended key --- libs/enigo/src/win/win_impl.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/libs/enigo/src/win/win_impl.rs b/libs/enigo/src/win/win_impl.rs index fb3b9881f..2e1108b9e 100644 --- a/libs/enigo/src/win/win_impl.rs +++ b/libs/enigo/src/win/win_impl.rs @@ -56,6 +56,20 @@ fn keybd_event(flags: u32, vk: u16, scan: u16) -> DWORD { input.type_ = INPUT_KEYBOARD; 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 { wVk: vk, wScan: scan, From bdfc429247543d0302c0bcb39ab2dad10aba4e59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jernej=20Simon=C4=8Di=C4=8D?= <1800143+jernejs@users.noreply.github.com> Date: Fri, 6 Jan 2023 13:02:15 +0100 Subject: [PATCH 1363/2015] Slovenian translation fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jernej Simončič --- src/lang/sl.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 8761db113..754c25868 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -85,8 +85,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Refresh File", "Osveži datoteko"), ("Local", "Lokalno"), ("Remote", "Oddaljeno"), - ("Remote Computer", "Lokalni računalnik"), - ("Local Computer", "Oddaljeni računalnik"), + ("Remote Computer", "Oddaljeni računalnik"), + ("Local Computer", "Lokalni računalnik"), ("Confirm Delete", "Potrdi izbris"), ("Delete", "Izbriši"), ("Properties", "Lastnosti"), @@ -124,7 +124,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Disable clipboard", "Onemogoči odložišče"), ("Lock after session end", "Zakleni ob koncu seje"), ("Insert", "Vstavi"), - ("Insert Lock", "Zaklep vstavljanja"), + ("Insert Lock", "Zakleni oddaljeni računalnik"), ("Refresh", "Osveži"), ("ID does not exist", "ID ne obstaja"), ("Failed to connect to rendezvous server", "Ni se bilo mogoče povezati na povezovalni strežnik"), @@ -138,7 +138,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to make direct connection to remote desktop", "Ni bilo mogoče vzpostaviti neposredne povezave z oddaljenim namizjem"), ("Set Password", "Nastavi geslo"), ("OS Password", "Geslo operacijskega sistema"), - ("install_tip", "Zaradi nadzora uporabniškega računa, RustDesk v nekaterih primerih na oddaljeni strani ne deluje pravilno. Temu se lahko izognete z namestitvijo RustDeska."), + ("install_tip", "Zaradi nadzora uporabniškega računa, RustDesk v nekaterih primerih na oddaljeni strani ne deluje pravilno. Temu se lahko izognete z namestitvijo."), ("Click to upgrade", "Klikni za nadgradnjo"), ("Click to download", "Klikni za prenos"), ("Click to update", "Klikni za posodobitev"), @@ -160,7 +160,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Listening ...", "Poslušam ..."), ("Remote Host", "Oddaljeni gostitelj"), ("Remote Port", "Oddaljena vrata"), - ("Action", "Deljanje"), + ("Action", "Dejanje"), ("Add", "Dodaj"), ("Local Port", "Lokalna vrata"), ("Local Address", "Lokalni naslov"), @@ -175,7 +175,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Allow using keyboard and mouse", "Dovoli uporabo tipkovnice in miške"), ("Allow using clipboard", "Dovoli uporabo odložišča"), ("Allow hearing sound", "Dovoli prenos zvoka"), - ("Allow file copy and paste", "Dovoli kopiraj in prilepi"), + ("Allow file copy and paste", "Dovoli kopiranje in lepljenje datotek"), ("Connected", "Povezan"), ("Direct and encrypted connection", "Neposredna šifrirana povezava"), ("Relayed and encrypted connection", "Posredovana šifrirana povezava"), @@ -385,7 +385,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("or", "ali"), ("Continue with", "Nadaljuj z"), ("Elevate", "Povzdig pravic"), - ("Zoom cursor", "Povečaj kazalec miške"), + ("Zoom cursor", "Prilagodi velikost miškinega kazalca"), ("Accept sessions via password", "Sprejmi seje z geslom"), ("Accept sessions via click", "Sprejmi seje s potrditvijo"), ("Accept sessions via both", "Sprejmi seje z geslom ali potrditvijo"), From 921b049e1e6468dd469d8f7e1d46ca69fd253ad1 Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 6 Jan 2023 18:14:31 +0800 Subject: [PATCH 1364/2015] ignore dpi while scale original Signed-off-by: fufesou --- flutter/lib/consts.dart | 2 + flutter/lib/desktop/pages/remote_page.dart | 69 ++++++++----------- .../lib/desktop/widgets/remote_menubar.dart | 22 +++--- flutter/lib/models/model.dart | 34 +++++---- 4 files changed, 61 insertions(+), 66 deletions(-) diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 50e7f594b..7aa200ae9 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -100,6 +100,8 @@ const kRemoteImageQualityLow = 'low'; /// [kRemoteImageQualityCustom] Custom image quality. const kRemoteImageQualityCustom = 'custom'; +const kIgnoreDpi = true; + /// flutter/packages/flutter/lib/src/services/keyboard_key.dart -> _keyLabels /// see [LogicalKeyboardKey.keyLabel] const Map logicalKeyMap = { diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 102dc784a..21728ee38 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -402,35 +402,38 @@ class _ImagePaintState extends State { onHover: (evt) {}, child: child)); - if (c.scrollStyle == ScrollStyle.scrollbar) { - final imageWidth = c.getDisplayWidth() * s; - final imageHeight = c.getDisplayHeight() * s; + final imageWidth = c.getDisplayWidth() * s; + final imageHeight = c.getDisplayHeight() * s; + final imageSize = Size(imageWidth, imageHeight); + bool overflow = + c.size.width < imageSize.width || c.size.height < imageSize.height; + if (overflow && c.scrollStyle == ScrollStyle.scrollbar) { final imageWidget = CustomPaint( - size: Size(imageWidth, imageHeight), + size: imageSize, painter: ImagePainter(image: m.image, x: 0, y: 0, scale: s), ); return NotificationListener( - onNotification: (notification) { - final percentX = _horizontal.hasClients - ? _horizontal.position.extentBefore / - (_horizontal.position.extentBefore + - _horizontal.position.extentInside + - _horizontal.position.extentAfter) - : 0.0; - final percentY = _vertical.hasClients - ? _vertical.position.extentBefore / - (_vertical.position.extentBefore + - _vertical.position.extentInside + - _vertical.position.extentAfter) - : 0.0; - c.setScrollPercent(percentX, percentY); - return false; - }, - child: mouseRegion( - child: _buildCrossScrollbar(context, _buildListener(imageWidget), - Size(imageWidth, imageHeight))), - ); + onNotification: (notification) { + final percentX = _horizontal.hasClients + ? _horizontal.position.extentBefore / + (_horizontal.position.extentBefore + + _horizontal.position.extentInside + + _horizontal.position.extentAfter) + : 0.0; + final percentY = _vertical.hasClients + ? _vertical.position.extentBefore / + (_vertical.position.extentBefore + + _vertical.position.extentInside + + _vertical.position.extentAfter) + : 0.0; + c.setScrollPercent(percentX, percentY); + return false; + }, + child: mouseRegion( + child: Obx(() => _buildCrossScrollbarFromLayout( + context, _buildListener(imageWidget), c.size, imageSize)), + )); } else { final imageWidget = CustomPaint( size: Size(c.size.width, c.size.height), @@ -565,24 +568,6 @@ class _ImagePaintState extends State { return widget; } - Widget _buildCrossScrollbar(BuildContext context, Widget child, Size size) { - var layoutSize = MediaQuery.of(context).size; - // If minimized, w or h may be negative here. - final w = layoutSize.width - kWindowBorderWidth * 2; - final h = - layoutSize.height - kWindowBorderWidth * 2 - kDesktopRemoteTabBarHeight; - layoutSize = Size( - w < 0 ? 0 : w, - h < 0 ? 0 : h, - ); - bool overflow = - layoutSize.width < size.width || layoutSize.height < size.height; - return overflow - ? Obx(() => - _buildCrossScrollbarFromLayout(context, child, layoutSize, size)) - : _buildCrossScrollbarFromLayout(context, child, layoutSize, size); - } - Widget _buildListener(Widget child) { if (listenerBuilder != null) { return listenerBuilder!(child); diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 1aa2647ee..2a7a26d08 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -699,7 +699,7 @@ class _RemoteMenubarState extends State { if (_screen == null) { return false; } - double scale = _screen!.scaleFactor; + final scale = kIgnoreDpi ? 1.0 : _screen!.scaleFactor; double selfWidth = _screen!.visibleFrame.width; double selfHeight = _screen!.visibleFrame.height; if (isFullscreen) { @@ -986,15 +986,17 @@ class _RemoteMenubarState extends State { wndRect.bottom - wndRect.top - mediaSize.height * scale; final canvasModel = widget.ffi.canvasModel; - final width = (canvasModel.getDisplayWidth() + - canvasModel.windowBorderWidth * 2) * - scale + - magicWidth; - final height = (canvasModel.getDisplayHeight() + - canvasModel.tabBarHeight + - canvasModel.windowBorderWidth * 2) * - scale + - magicHeight; + final width = + (canvasModel.getDisplayWidth() * canvasModel.scale + + canvasModel.windowBorderWidth * 2) * + scale + + magicWidth; + final height = + (canvasModel.getDisplayHeight() * canvasModel.scale + + canvasModel.tabBarHeight + + canvasModel.windowBorderWidth * 2) * + scale + + magicHeight; double left = wndRect.left + (wndRect.width - width) / 2; double top = wndRect.top + (wndRect.height - height) / 2; diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 0f7099bc8..39e78cd6b 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -528,6 +528,7 @@ class CanvasModel with ChangeNotifier { double _y = 0; // image scale double _scale = 1.0; + Size _size = Size.zero; // the tabbar over the image // double tabBarHeight = 0.0; // the window border's width @@ -548,6 +549,7 @@ class CanvasModel with ChangeNotifier { double get x => _x; double get y => _y; double get scale => _scale; + Size get size => _size; ScrollStyle get scrollStyle => _scrollStyle; ViewStyle get viewStyle => _lastViewStyle; @@ -562,18 +564,26 @@ class CanvasModel with ChangeNotifier { double get scrollY => _scrollY; updateViewStyle() async { + Size getSize() { + final size = MediaQueryData.fromWindow(ui.window).size; + // If minimized, w or h may be negative here. + double w = size.width - windowBorderWidth * 2; + double h = size.height - tabBarHeight - windowBorderWidth * 2; + return Size(w < 0 ? 0 : w, h < 0 ? 0 : h); + } + final style = await bind.sessionGetViewStyle(id: id); if (style == null) { return; } - final sizeWidth = size.width; - final sizeHeight = size.height; + + _size = getSize(); final displayWidth = getDisplayWidth(); final displayHeight = getDisplayHeight(); final viewStyle = ViewStyle( style: style, - width: sizeWidth, - height: sizeHeight, + width: size.width, + height: size.height, displayWidth: displayWidth, displayHeight: displayHeight, ); @@ -585,8 +595,12 @@ class CanvasModel with ChangeNotifier { } _lastViewStyle = viewStyle; _scale = viewStyle.scale; - _x = (sizeWidth - displayWidth * _scale) / 2; - _y = (sizeHeight - displayHeight * _scale) / 2; + + if (kIgnoreDpi && style == kRemoteViewStyleOriginal) { + _scale = 1.0 / ui.window.devicePixelRatio; + } + _x = (size.width - displayWidth * _scale) / 2; + _y = (size.height - displayHeight * _scale) / 2; notifyListeners(); } @@ -628,14 +642,6 @@ class CanvasModel with ChangeNotifier { double get windowBorderWidth => stateGlobal.windowBorderWidth.value; double get tabBarHeight => stateGlobal.tabBarHeight; - Size get size { - final size = MediaQueryData.fromWindow(ui.window).size; - // If minimized, w or h may be negative here. - double w = size.width - windowBorderWidth * 2; - double h = size.height - tabBarHeight - windowBorderWidth * 2; - return Size(w < 0 ? 0 : w, h < 0 ? 0 : h); - } - moveDesktopMouse(double x, double y) { // On mobile platforms, move the canvas with the cursor. final dw = getDisplayWidth() * _scale; From 947b7c9a4d4c2c4ad433b2404ec0b26c61e14120 Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 6 Jan 2023 20:25:18 +0800 Subject: [PATCH 1365/2015] disable scroll options when image is wrapped by window Signed-off-by: fufesou --- flutter/lib/desktop/pages/remote_page.dart | 10 +- flutter/lib/desktop/widgets/popup_menu.dart | 138 ++++++++++-------- .../lib/desktop/widgets/remote_menubar.dart | 3 +- flutter/lib/models/model.dart | 5 + 4 files changed, 90 insertions(+), 66 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 21728ee38..55a5bbaef 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -402,12 +402,10 @@ class _ImagePaintState extends State { onHover: (evt) {}, child: child)); - final imageWidth = c.getDisplayWidth() * s; - final imageHeight = c.getDisplayHeight() * s; - final imageSize = Size(imageWidth, imageHeight); - bool overflow = - c.size.width < imageSize.width || c.size.height < imageSize.height; - if (overflow && c.scrollStyle == ScrollStyle.scrollbar) { + if (c.imageOverflow.isTrue && c.scrollStyle == ScrollStyle.scrollbar) { + final imageWidth = c.getDisplayWidth() * s; + final imageHeight = c.getDisplayHeight() * s; + final imageSize = Size(imageWidth, imageHeight); final imageWidget = CustomPaint( size: imageSize, painter: ImagePainter(image: m.image, x: 0, y: 0, scale: s), diff --git a/flutter/lib/desktop/widgets/popup_menu.dart b/flutter/lib/desktop/widgets/popup_menu.dart index 20ab31ed9..0cbdad929 100644 --- a/flutter/lib/desktop/widgets/popup_menu.dart +++ b/flutter/lib/desktop/widgets/popup_menu.dart @@ -118,6 +118,15 @@ abstract class MenuEntryBase { this.enabled, }); List> build(BuildContext context, MenuConfig conf); + + enabledStyle(BuildContext context) => TextStyle( + color: Theme.of(context).textTheme.titleLarge?.color, + fontSize: MenuConfig.fontSize, + fontWeight: FontWeight.normal); + disabledStyle() => TextStyle( + color: Colors.grey, + fontSize: MenuConfig.fontSize, + fontWeight: FontWeight.normal); } class MenuEntryDivider extends MenuEntryBase { @@ -189,54 +198,76 @@ class MenuEntryRadios extends MenuEntryBase { mod_menu.PopupMenuEntry _buildMenuItem( BuildContext context, MenuConfig conf, MenuEntryRadioOption opt) { + Widget getTextChild() { + final enabledTextChild = Text( + opt.text, + style: enabledStyle(context), + ); + final disabledTextChild = Text( + opt.text, + style: disabledStyle(), + ); + if (opt.enabled == null) { + return enabledTextChild; + } else { + return Obx( + () => opt.enabled!.isTrue ? enabledTextChild : disabledTextChild); + } + } + + final child = Container( + padding: padding, + alignment: AlignmentDirectional.centerStart, + constraints: + BoxConstraints(minHeight: conf.height, maxHeight: conf.height), + child: Row( + children: [ + getTextChild(), + Expanded( + child: Align( + alignment: Alignment.centerRight, + child: Transform.scale( + scale: MenuConfig.iconScale, + child: Obx(() => opt.value == curOption.value + ? IconButton( + padding: + const EdgeInsets.fromLTRB(8.0, 0.0, 8.0, 0.0), + hoverColor: Colors.transparent, + focusColor: Colors.transparent, + onPressed: () {}, + icon: Icon( + Icons.check, + color: (opt.enabled ?? true.obs).isTrue + ? conf.commonColor + : Colors.grey, + )) + : const SizedBox.shrink()), + ))), + ], + ), + ); + onPressed() { + if (opt.dismissOnClicked && Navigator.canPop(context)) { + Navigator.pop(context); + } + setOption(opt.value); + } + return mod_menu.PopupMenuItem( padding: EdgeInsets.zero, height: conf.height, child: Container( - width: conf.boxWidth, - child: TextButton( - child: Container( - padding: padding, - alignment: AlignmentDirectional.centerStart, - constraints: BoxConstraints( - minHeight: conf.height, maxHeight: conf.height), - child: Row( - children: [ - Text( - opt.text, - style: TextStyle( - color: Theme.of(context).textTheme.titleLarge?.color, - fontSize: MenuConfig.fontSize, - fontWeight: FontWeight.normal), - ), - Expanded( - child: Align( - alignment: Alignment.centerRight, - child: Transform.scale( - scale: MenuConfig.iconScale, - child: Obx(() => opt.value == curOption.value - ? IconButton( - padding: const EdgeInsets.fromLTRB( - 8.0, 0.0, 8.0, 0.0), - hoverColor: Colors.transparent, - focusColor: Colors.transparent, - onPressed: () {}, - icon: Icon( - Icons.check, - color: conf.commonColor, - )) - : const SizedBox.shrink()), - ))), - ], - ), - ), - onPressed: () { - if (opt.dismissOnClicked && Navigator.canPop(context)) { - Navigator.pop(context); - } - setOption(opt.value); - }, - )), + width: conf.boxWidth, + child: opt.enabled == null + ? TextButton( + child: child, + onPressed: onPressed, + ) + : Obx(() => TextButton( + child: child, + onPressed: opt.enabled!.isTrue ? onPressed : null, + )), + ), ); } @@ -567,12 +598,9 @@ class MenuEntrySubMenu extends MenuEntryBase { const SizedBox(width: MenuConfig.midPadding), Obx(() => Text( text, - style: TextStyle( - color: super.enabled!.value - ? Theme.of(context).textTheme.titleLarge?.color - : Colors.grey, - fontSize: MenuConfig.fontSize, - fontWeight: FontWeight.normal), + style: super.enabled!.value + ? enabledStyle(context) + : disabledStyle(), )), Expanded( child: Align( @@ -605,14 +633,6 @@ class MenuEntryButton extends MenuEntryBase { ); Widget _buildChild(BuildContext context, MenuConfig conf) { - final enabledStyle = TextStyle( - color: Theme.of(context).textTheme.titleLarge?.color, - fontSize: MenuConfig.fontSize, - fontWeight: FontWeight.normal); - const disabledStyle = TextStyle( - color: Colors.grey, - fontSize: MenuConfig.fontSize, - fontWeight: FontWeight.normal); super.enabled ??= true.obs; return Obx(() => Container( width: conf.boxWidth, @@ -631,7 +651,7 @@ class MenuEntryButton extends MenuEntryBase { constraints: BoxConstraints(minHeight: conf.height, maxHeight: conf.height), child: childBuilder( - super.enabled!.value ? enabledStyle : disabledStyle), + super.enabled!.value ? enabledStyle(context) : disabledStyle()), ), ))); } diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 2a7a26d08..b69c73091 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -936,11 +936,13 @@ class _RemoteMenubarState extends State { text: translate('ScrollAuto'), value: kRemoteScrollStyleAuto, dismissOnClicked: true, + enabled: widget.ffi.canvasModel.imageOverflow, ), MenuEntryRadioOption( text: translate('Scrollbar'), value: kRemoteScrollStyleBar, dismissOnClicked: true, + enabled: widget.ffi.canvasModel.imageOverflow, ), ], curOptionGetter: () async => @@ -1200,7 +1202,6 @@ class _RemoteMenubarState extends State { }, optionSetter: (String oldValue, String newValue) async { await bind.sessionSetKeyboardMode(id: widget.id, value: newValue); - widget.ffi.canvasModel.updateViewStyle(); }, ) ]; diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 39e78cd6b..1cdecbd03 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -22,6 +22,7 @@ import 'package:tuple/tuple.dart'; import 'package:image/image.dart' as img2; import 'package:flutter_custom_cursor/cursor_manager.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; import '../common.dart'; import '../common/shared_state.dart'; @@ -542,6 +543,8 @@ class CanvasModel with ChangeNotifier { ScrollStyle _scrollStyle = ScrollStyle.scrollauto; ViewStyle _lastViewStyle = ViewStyle(); + final _imageOverflow = false.obs; + WeakReference parent; CanvasModel(this.parent); @@ -552,6 +555,7 @@ class CanvasModel with ChangeNotifier { Size get size => _size; ScrollStyle get scrollStyle => _scrollStyle; ViewStyle get viewStyle => _lastViewStyle; + RxBool get imageOverflow => _imageOverflow; _resetScroll() => setScrollPercent(0.0, 0.0); @@ -601,6 +605,7 @@ class CanvasModel with ChangeNotifier { } _x = (size.width - displayWidth * _scale) / 2; _y = (size.height - displayHeight * _scale) / 2; + _imageOverflow.value = _x < 0 || y < 0; notifyListeners(); } From c1f983a952e8b94dd31f5c897f8c86e1390a8261 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 6 Jan 2023 21:33:01 +0800 Subject: [PATCH 1366/2015] remove ENABLE_HARDENED_RUNTIME in debug to make flutter run -d macos works under m1 --- flutter/macos/Runner.xcodeproj/project.pbxproj | 2 -- 1 file changed, 2 deletions(-) diff --git a/flutter/macos/Runner.xcodeproj/project.pbxproj b/flutter/macos/Runner.xcodeproj/project.pbxproj index b935ab4b2..a8b5306be 100644 --- a/flutter/macos/Runner.xcodeproj/project.pbxproj +++ b/flutter/macos/Runner.xcodeproj/project.pbxproj @@ -496,7 +496,6 @@ CODE_SIGN_IDENTITY = "-"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_HARDENED_RUNTIME = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -587,7 +586,6 @@ CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; DEVELOPMENT_TEAM = ""; - ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", From 40e946267948b27fdebfe2e5a08171179e62f325 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 6 Jan 2023 20:40:29 -0800 Subject: [PATCH 1367/2015] fix: save/restore window position on macos also hide on launch --- flutter/lib/common.dart | 2 +- .../lib/desktop/pages/desktop_home_page.dart | 4 ++-- .../lib/desktop/widgets/tabbar_widget.dart | 14 ++++++++++---- flutter/lib/main.dart | 2 ++ flutter/lib/utils/multi_window_manager.dart | 19 ++++++++++--------- flutter/macos/Runner/MainFlutterWindow.swift | 7 ++++--- flutter/pubspec.yaml | 2 +- 7 files changed, 30 insertions(+), 20 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 8d047dd0d..4ea494fc5 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1418,7 +1418,7 @@ bool isRunningInPortableMode() { } /// Window status callback -void onActiveWindowChanged() async { +Future onActiveWindowChanged() async { print( "[MultiWindowHandler] active window changed: ${rustDeskWinManager.getActiveWindows()}"); if (rustDeskWinManager.getActiveWindows().isEmpty) { diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 43ffbd1e3..fd9814cc2 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -513,9 +513,9 @@ class _DesktopHomePageState extends State } else if (call.method == kWindowActionRebuild) { reloadCurrentWindow(); } else if (call.method == kWindowEventShow) { - rustDeskWinManager.registerActiveWindow(call.arguments["id"]); + await rustDeskWinManager.registerActiveWindow(call.arguments["id"]); } else if (call.method == kWindowEventHide) { - rustDeskWinManager.unregisterActiveWindow(call.arguments["id"]); + await rustDeskWinManager.unregisterActiveWindow(call.arguments["id"]); } else if (call.method == kWindowConnect) { await connectMainDesktop( call.arguments['id'], diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index f6a5da819..91ce6cce6 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -527,13 +527,19 @@ class WindowActionPanelState extends State void onWindowClose() async { // hide window on close if (widget.isMainWindow) { + await rustDeskWinManager.unregisterActiveWindow(0); + // `hide` must be placed after unregisterActiveWindow, because once all windows are hidden, + // flutter closes the application on macOS. We should ensure the post-run logic has ran successfully. + // e.g.: saving window position. await windowManager.hide(); - rustDeskWinManager.unregisterActiveWindow(0); } else { - widget.onClose?.call(); + // it's safe to hide the subwindow await WindowController.fromWindowId(windowId!).hide(); - rustDeskWinManager - .call(WindowType.Main, kWindowEventHide, {"id": windowId!}); + await Future.wait([ + rustDeskWinManager + .call(WindowType.Main, kWindowEventHide, {"id": windowId!}), + widget.onClose?.call() ?? Future.microtask(() => null) + ]); } super.onWindowClose(); } diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 6d09ef139..6fd205a22 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -196,6 +196,8 @@ void runMultiWindow( // no such appType exit(0); } + // show window from hidden status + WindowController.fromWindowId(windowId!).show(); } void runConnectionManagerScreen(bool hide) async { diff --git a/flutter/lib/utils/multi_window_manager.dart b/flutter/lib/utils/multi_window_manager.dart index de6750a3f..cf6d78cd2 100644 --- a/flutter/lib/utils/multi_window_manager.dart +++ b/flutter/lib/utils/multi_window_manager.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:desktop_multi_window/desktop_multi_window.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hbb/common.dart'; @@ -34,7 +35,7 @@ class RustDeskMultiWindowManager { static final instance = RustDeskMultiWindowManager._(); final List _activeWindows = List.empty(growable: true); - final List _windowActiveCallbacks = List.empty(growable: true); + final List _windowActiveCallbacks = List.empty(growable: true); int? _remoteDesktopWindowId; int? _fileTransferWindowId; int? _portForwardWindowId; @@ -191,19 +192,19 @@ class RustDeskMultiWindowManager { return _activeWindows; } - void _notifyActiveWindow() { + Future _notifyActiveWindow() async { for (final callback in _windowActiveCallbacks) { - callback.call(); + await callback.call(); } } - void registerActiveWindow(int windowId) { + Future registerActiveWindow(int windowId) async { if (_activeWindows.contains(windowId)) { // ignore } else { _activeWindows.add(windowId); } - _notifyActiveWindow(); + await _notifyActiveWindow(); } /// Remove active window which has [`windowId`] @@ -212,20 +213,20 @@ class RustDeskMultiWindowManager { /// This function should only be called from main window. /// For other windows, please post a unregister(hide) event to main window handler: /// `rustDeskWinManager.call(WindowType.Main, kWindowEventHide, {"id": windowId!});` - void unregisterActiveWindow(int windowId) { + Future unregisterActiveWindow(int windowId) async { if (!_activeWindows.contains(windowId)) { // ignore } else { _activeWindows.remove(windowId); } - _notifyActiveWindow(); + await _notifyActiveWindow(); } - void registerActiveWindowListener(VoidCallback callback) { + void registerActiveWindowListener(AsyncCallback callback) { _windowActiveCallbacks.add(callback); } - void unregisterActiveWindowListener(VoidCallback callback) { + void unregisterActiveWindowListener(AsyncCallback callback) { _windowActiveCallbacks.remove(callback); } } diff --git a/flutter/macos/Runner/MainFlutterWindow.swift b/flutter/macos/Runner/MainFlutterWindow.swift index 1d16763ee..540cd9ab9 100644 --- a/flutter/macos/Runner/MainFlutterWindow.swift +++ b/flutter/macos/Runner/MainFlutterWindow.swift @@ -49,7 +49,8 @@ class MainFlutterWindow: NSWindow { super.awakeFromNib() } -// override func bitsdojo_window_configure() -> UInt { -// return BDW_CUSTOM_FRAME | BDW_HIDE_ON_STARTUP -// } + override public func order(_ place: NSWindow.OrderingMode, relativeTo otherWin: Int) { + super.order(place, relativeTo: otherWin) + hiddenWindowAtLaunch() + } } diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index a87727f7b..4826f6198 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -63,7 +63,7 @@ dependencies: desktop_multi_window: git: url: https://github.com/Kingtous/rustdesk_desktop_multi_window - ref: 82f9eab81cb2c7bfb938def7a1b399a6279bbc75 + ref: e6d30bde98bd0f4ff50a130e5b1068138307bd03 freezed_annotation: ^2.0.3 flutter_custom_cursor: ^0.0.2 window_size: From 6ebe25a6ba5c4041007614f123b40ce3002773b2 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sat, 7 Jan 2023 13:12:51 +0800 Subject: [PATCH 1368/2015] remove counter https://stackoverflow.com/questions/51893926/how-can-i-hide-letter-counter-from-bottom-of-textfield-in-flutter --- flutter/lib/desktop/pages/connection_page.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 939acfbc5..85749a256 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -184,6 +184,7 @@ class _ConnectionPageState extends State cursorColor: Theme.of(context).textTheme.titleLarge?.color, decoration: InputDecoration( + counterText: '', hintText: _idInputFocused.value ? null : translate('Enter Remote ID'), From f2cc993e1b661a2c71e40d45d097e584b407e280 Mon Sep 17 00:00:00 2001 From: solokot Date: Sat, 7 Jan 2023 12:19:27 +0300 Subject: [PATCH 1369/2015] 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 14e04b940..cb53b363e 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -206,7 +206,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", "Всегда подключён через ретрансляционный сервер"), + ("Always connected via relay", "Всегда подключается через ретрансляционный сервер"), ("Always connect via relay", "Всегда подключаться через ретрансляционный сервер"), ("whitelist_tip", "Только IP-адреса из белого списка могут получить доступ ко мне"), ("Login", "Войти"), @@ -405,8 +405,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the web console", "Закрыто вручную через веб-консоль"), ("Local keyboard type", "Тип локальной клавиатуры"), ("Select local keyboard type", "Выберите тип локальной клавиатуры"), - ("software_render_tip", ""), - ("Always use software rendering", ""), - ("config_input", ""), + ("software_render_tip", "Если у вас видеокарта Nvidia и удалённое окно закрывается сразу после подключения, может помочь установка драйвера Nouveau и выбор использования программной визуализации. Потребуется перезапуск."), + ("Always use software rendering", "Использовать программную визуализацию"), + ("config_input", "Чтобы управлять удалённым рабочим столом с помощью клавиатуры, необходимо предоставить RustDesk разрешения \"Мониторинг ввода\"."), ].iter().cloned().collect(); } From 85e4e0fe4e9c652ab0c234f7a1c45b12f6a80dce Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sat, 7 Jan 2023 23:55:41 +0800 Subject: [PATCH 1370/2015] feat: add appimage build for flutter --- appimage/AppImageBuilder.yml | 90 +++++++++++++++++++++++++++++++++ flutter/linux/my_application.cc | 10 +++- 2 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 appimage/AppImageBuilder.yml diff --git a/appimage/AppImageBuilder.yml b/appimage/AppImageBuilder.yml new file mode 100644 index 000000000..e5e424d0b --- /dev/null +++ b/appimage/AppImageBuilder.yml @@ -0,0 +1,90 @@ +# appimage-builder recipe see https://appimage-builder.readthedocs.io for details +version: 1 +script: + - rm -rf ./AppDir || true + - bsdtar -zxvf ../rustdesk-1.2.0.deb + - tar -xvf ./data.tar.xz + - mkdir ./AppDir + - mv ./usr ./AppDir/usr + # 32x32 icon + - for i in {32,64,128}; do mkdir -p ./AppDir/usr/share/icons/hicolor/$i\x$i/apps/; cp ../res/$i\x$i.png ./AppDir/usr/share/icons/hicolor/$i\x$i/apps/rustdesk.png; done + # desktop file + # - sed -i "s/Icon=\/usr\/share\/rustdesk\/files\/rustdesk.png/Icon=rustdesk/g" ./AppDir/usr/share/applications/rustdesk.desktop + - rm -rf ./AppDir/usr/share/applications +AppDir: + path: ./AppDir + app_info: + id: rustdesk + name: rustdesk + icon: rustdesk + version: 1.2.0 + exec: usr/lib/rustdesk/rustdesk + exec_args: $@ + apt: + arch: + - amd64 + allow_unauthenticated: true + sources: + - sourceline: deb http://mirrors.ustc.edu.cn/ubuntu/ bionic main restricted + - sourceline: deb http://mirrors.ustc.edu.cn/ubuntu/ bionic-updates main restricted + - sourceline: deb http://mirrors.ustc.edu.cn/ubuntu/ bionic universe + - sourceline: deb http://mirrors.ustc.edu.cn/ubuntu/ bionic-updates universe + - sourceline: deb http://mirrors.ustc.edu.cn/ubuntu/ bionic multiverse + - sourceline: deb http://mirrors.ustc.edu.cn/ubuntu/ bionic-updates multiverse + - sourceline: deb http://mirrors.ustc.edu.cn/ubuntu/ bionic-backports main restricted + universe multiverse + - sourceline: deb http://mirrors.ustc.edu.cn/ubuntu/ bionic-security main restricted + - sourceline: deb http://mirrors.ustc.edu.cn/ubuntu/ bionic-security universe + - sourceline: deb http://mirrors.ustc.edu.cn/ubuntu/ bionic-security multiverse + - sourceline: deb http://ppa.launchpad.net/pipewire-debian/pipewire-upstream/ubuntu + bionic main + include: + - libc6:amd64 + - libgtk-3-0 + - libxcb-randr0 + - libxdo3 + - libxfixes3 + - libxcb-shape0 + - libxcb-xfixes0 + - libasound2 + - libsystemd0 + - curl + - libva-drm2 + - libva-x11-2 + - libvdpau1 + - libgstreamer-plugins-base1.0-0 + exclude: + - humanity-icon-theme + - hicolor-icon-theme + - adwaita-icon-theme + - ubuntu-mono + files: + include: [] + exclude: + - usr/share/man + - usr/share/doc/*/README.* + - usr/share/doc/*/changelog.* + - usr/share/doc/*/NEWS.* + - usr/share/doc/*/TODO.* + runtime: + env: + GIO_MODULE_DIR: $APPDIR/usr/lib/x86_64-linux-gnu/gio/modules/ + test: + fedora-30: + image: appimagecrafters/tests-env:fedora-30 + command: ./AppRun + debian-stable: + image: appimagecrafters/tests-env:debian-stable + command: ./AppRun + archlinux-latest: + image: appimagecrafters/tests-env:archlinux-latest + command: ./AppRun + centos-7: + image: appimagecrafters/tests-env:centos-7 + command: ./AppRun + ubuntu-xenial: + image: appimagecrafters/tests-env:ubuntu-xenial + command: ./AppRun +AppImage: + arch: x86_64 + update-information: guess diff --git a/flutter/linux/my_application.cc b/flutter/linux/my_application.cc index 215c6f0ee..21e25fa28 100644 --- a/flutter/linux/my_application.cc +++ b/flutter/linux/my_application.cc @@ -23,7 +23,15 @@ static void my_application_activate(GApplication* application) { GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); // we have custom window frame gtk_window_set_decorated(window, FALSE); - + // try setting icon for rustdesk, which uses the system cache + GtkIconTheme* theme = gtk_icon_theme_get_default(); + gint icons[4] = {256, 128, 64, 32}; + for (int i = 0; i < 4; i++) { + GdkPixbuf* icon = gtk_icon_theme_load_icon(theme, "rustdesk", icons[i], GTK_ICON_LOOKUP_NO_SVG, NULL); + if (icon != nullptr) { + gtk_window_set_icon(window, icon); + } + } // Use a header bar when running in GNOME as this is the common style used // by applications and is the setup most users will be using (e.g. Ubuntu // desktop). From b28e4b0e608ce2af7ece861275ea0ca6227aad16 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sun, 8 Jan 2023 00:22:15 +0800 Subject: [PATCH 1371/2015] feat: appimage nightly ci --- .github/workflows/flutter-nightly.yml | 26 ++++++++++++++++++++++++-- appimage/AppImageBuilder.yml | 17 +++++++---------- flutter/pubspec.yaml | 2 +- res/64x64.png | Bin 0 -> 2264 bytes 4 files changed, 32 insertions(+), 13 deletions(-) create mode 100644 res/64x64.png diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 7ce940b89..1be4bb001 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -1162,7 +1162,7 @@ jobs: - name: Prepare env run: | sudo apt update -y - sudo apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev + sudo apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev libarchive-tools mkdir -p ./target/release/ - name: Restore the rustdesk lib file @@ -1217,7 +1217,8 @@ jobs: shell: bash run: | for name in rustdesk*??.deb; do - mv "$name" "${name%%.deb}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb" + # use cp to duplicate deb files to fit other packages. + cp "$name" "${name%%.deb}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb" done - name: Publish debian package @@ -1285,6 +1286,27 @@ jobs: files: | res/rustdesk*.zst + - name: Build appimage package + if: ${{ matrix.job.extra-build-features == '' }} + shell: bash + run: | + # set-up appimage-builder + wget -O appimage-builder-x86_64.AppImage https://github.com/AppImageCrafters/appimage-builder/releases/download/v1.1.0/appimage-builder-1.1.0-x86_64.AppImage + chmod +x appimage-builder-x86_64.AppImage + sudo mv appimage-builder-x86_64.AppImage /usr/local/bin/appimage-builder + # run appimage-builder + pushd appimage + appimage-builder --skip-tests + + - name: Publish appimage package + if: ${{ matrix.job.extra-build-features == '' }} + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + ./appimage/rustdesk-${{ env.VERSION }}-*.AppImage + - name: Publish fedora28/centos8 package if: ${{ matrix.job.extra-build-features == '' }} uses: softprops/action-gh-release@v1 diff --git a/appimage/AppImageBuilder.yml b/appimage/AppImageBuilder.yml index e5e424d0b..ae95fd2ce 100644 --- a/appimage/AppImageBuilder.yml +++ b/appimage/AppImageBuilder.yml @@ -25,17 +25,14 @@ AppDir: - amd64 allow_unauthenticated: true sources: - - sourceline: deb http://mirrors.ustc.edu.cn/ubuntu/ bionic main restricted - - sourceline: deb http://mirrors.ustc.edu.cn/ubuntu/ bionic-updates main restricted - - sourceline: deb http://mirrors.ustc.edu.cn/ubuntu/ bionic universe - - sourceline: deb http://mirrors.ustc.edu.cn/ubuntu/ bionic-updates universe - - sourceline: deb http://mirrors.ustc.edu.cn/ubuntu/ bionic multiverse - - sourceline: deb http://mirrors.ustc.edu.cn/ubuntu/ bionic-updates multiverse - - sourceline: deb http://mirrors.ustc.edu.cn/ubuntu/ bionic-backports main restricted + - sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic main restricted + - sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-updates main restricted + - sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic universe + - sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-updates universe + - sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic multiverse + - sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-updates multiverse + - sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-backports main restricted universe multiverse - - sourceline: deb http://mirrors.ustc.edu.cn/ubuntu/ bionic-security main restricted - - sourceline: deb http://mirrors.ustc.edu.cn/ubuntu/ bionic-security universe - - sourceline: deb http://mirrors.ustc.edu.cn/ubuntu/ bionic-security multiverse - sourceline: deb http://ppa.launchpad.net/pipewire-debian/pipewire-upstream/ubuntu bionic main include: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 4826f6198..705f4650c 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -63,7 +63,7 @@ dependencies: desktop_multi_window: git: url: https://github.com/Kingtous/rustdesk_desktop_multi_window - ref: e6d30bde98bd0f4ff50a130e5b1068138307bd03 + ref: 057e6eb1bc7dcbcf9dafd1384274a611e4fe7124 freezed_annotation: ^2.0.3 flutter_custom_cursor: ^0.0.2 window_size: diff --git a/res/64x64.png b/res/64x64.png new file mode 100644 index 0000000000000000000000000000000000000000..d93638e6eb1d82b24807d7b3d0a6ec5c72e0bd86 GIT binary patch literal 2264 zcmZ{mc`($E8^=G+bwsXBQ6Wdv((Wq6y1vdWSJqYJT4${*i(T}kB}e4Uy;jFYuH0<0 zlv}$JQqGlgNx6?u{Cwv(^PBnoW`6U`^So!?&%EaS=kvr{m>Kc$i17dbz=t%}w>(mv zf0~Q)XqI>fFpfkLZER@@0HJaK5E%^sdq-B}0ssU-0pPnc0H|jJfT%yQ@dn~(!RdP4 zNFO--r=C)a(vKVv7HMh#VzPh)guuRvMPo+}1*xxVg&X}jcGur}2n<@Cq^fdDMYD73 zn~3NNaNrGI-DJd>OmT2EnwO@JIY($DE!sc!cPe*n0I2Br3!&5MF}~l2Y!XG z736y_FuqC0Q*ojZt!S@axxaL^)k)D#=m71Xu{^NEP6v9sXMl!S&BSKF>D@gr?xZc> zmA9wYWPGvR!UH(ndRTb*H=0Db9dTOVkqN8Sj93Atp)ZCY68qhOJzJ3rXQWX`rW~`2 z^d>wN;!7j+l&I?2!-EVG21K%IyJs15sfgw*J7Rz=y~K@f?8sDqx;rzp#<@1oxLi=mQVQX)Zzo#v#Xn%H&MziYA+3TrCT zA1gN5_3>^sH9r26pUNdTs<+9aggn=obN=4UO@~minGaf+>IISI(iiTRhUVzn@qz~L zuhc);P2c@E(zL1)fy-#@#cu5tK&a2bl0CzzF~m%N#Om9OriSyOg3pB|dlGq;8=ugamML7SrtQhA@^S9-mzxvElswF?gdk{3 zV=ur`xzKbsGuh*}J2E!l>9NZss}wzZ(TSVqA|Ra|%J|tkCr)1)$T=6ml4hb215#p+ zqxM5+`)?N(3=_-4hU;1)c7{x*8*HIF!wdS|=8-aU(!WN|DP^ZlG`-0my)wQb3*rDY%YUybWf_isb?Io0G92KO9?qfsgJ3k_hB{;d`aQ?vZeUahnk=>XS?#LzYq9G zj(w+w!e7MDJUD7}Pc}mnBrsFUbBkZCqoj4xMlyb{A`C@hvSo@(nWB9)uDqI>tOfgC z0tx+%1x%IIucnYaRqYm;U!#jDae;qW?=0bc@|p;)AN3^9&g9~W2r1(40o6CY+(CH@ zJ@wTI96v#!cJ(B`KRI(N(Rl?H?}IiJ=*o}gg2U1l!45FhaW@0D1}#0bJ->N;O<|6G zR#os_%80vij+U8Ym82(&QuYUOz+ztBC}jh$vYhrdtP>xovD4h$oU|!7gucooO9?nz z35uF(ROjx?lWv*bM!%-+=q7r8-PnoQ?2T9BJ&$rwtMuIvmPBrO&684^@k~dO-5E&m zx*?=nEUOvJnEWww3I*}J)QyZB>4IyKEkAu~ANA;0v_+R;$ijJT?I0=&@~)Zffqh|{!i>ThsR0)Y8xax1foC;&6Z@PuBQt~fV{q@uYgQTj zWi5%ua8K5*hg_9Qz?Q}w;s>XaPGXR$?sg{s5Sn1^YIQcnnBCy>jgG~!hT4JgKEk!j ztj^ap{8}UiT!$6n=Ok=HwpT}n-f9J^oF}9b3V08pai2)9pdDlJI7kzr-=*f&> zdPQc zPy9J-+ha`JWIYsZ;dbAbZQ{AKL_To8d;IVkRLjrtBzs-IHvw%cN>vz;z_OFl`B&t!en1^xO|ogYc03O!UJt}voPDrHq^d^Tt;B%73*S`wR@UB^ZEUf z7>!RjP+r_jjxq0V1{c57`be{Cm7Iov-}<-z$10qBPa2 zaP;0bSEXu)><_SBm7)KtS_FCf55H3tOFc6-1m(a9d8dL<0^^b`u|;YaUgd5THtnRM z*j$6NIQ0%Gkwp%*dnJw5{RMcV0>dLAYp-^=wR4@8d?tcM3es10#`2P>UN4$<+4M6@s30Q9*_du+R dkN^KNYU*& Date: Sun, 8 Jan 2023 08:23:06 +0800 Subject: [PATCH 1372/2015] fix: use tmp folder to download appimage-builder --- .github/workflows/flutter-nightly.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 1be4bb001..57a9b2eac 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -1291,9 +1291,11 @@ jobs: shell: bash run: | # set-up appimage-builder + pushd /tmp wget -O appimage-builder-x86_64.AppImage https://github.com/AppImageCrafters/appimage-builder/releases/download/v1.1.0/appimage-builder-1.1.0-x86_64.AppImage chmod +x appimage-builder-x86_64.AppImage sudo mv appimage-builder-x86_64.AppImage /usr/local/bin/appimage-builder + popd # run appimage-builder pushd appimage appimage-builder --skip-tests From 3bf2d749fe44d41db48a1122846d909d8d3d66ab Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sun, 8 Jan 2023 08:58:30 +0800 Subject: [PATCH 1373/2015] add: sudo priviledge --- .github/workflows/flutter-nightly.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 57a9b2eac..b6f82cf2b 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -1298,7 +1298,7 @@ jobs: popd # run appimage-builder pushd appimage - appimage-builder --skip-tests + sudo appimage-builder --skip-tests - name: Publish appimage package if: ${{ matrix.job.extra-build-features == '' }} From 9245e13057fca11a4509994b56cc0db00c3383c6 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sun, 8 Jan 2023 10:27:00 +0800 Subject: [PATCH 1374/2015] fix: use appimage feature --- .github/workflows/flutter-nightly.yml | 16 ++++++++++++++-- build.py | 7 +++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index b6f82cf2b..eb13edf15 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -599,6 +599,12 @@ jobs: os: ubuntu-20.04, extra-build-features: "flatpak", } + - { + arch: x86_64, + target: x86_64-unknown-linux-gnu, + os: ubuntu-20.04, + extra-build-features: "appimage", + } # - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } steps: - name: Maximize build space @@ -1148,6 +1154,12 @@ jobs: os: ubuntu-18.04, extra-build-features: "flatpak", } + - { + arch: x86_64, + target: x86_64-unknown-linux-gnu, + os: ubuntu-18.04, + extra-build-features: "appimage", + } # - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } steps: - name: Checkout source code @@ -1287,7 +1299,7 @@ jobs: res/rustdesk*.zst - name: Build appimage package - if: ${{ matrix.job.extra-build-features == '' }} + if: ${{ matrix.job.extra-build-features == 'appimage' }} shell: bash run: | # set-up appimage-builder @@ -1301,7 +1313,7 @@ jobs: sudo appimage-builder --skip-tests - name: Publish appimage package - if: ${{ matrix.job.extra-build-features == '' }} + if: ${{ matrix.job.extra-build-features == 'appimage' }} uses: softprops/action-gh-release@v1 with: prerelease: true diff --git a/build.py b/build.py index 75d6fcd89..6b107ff4b 100755 --- a/build.py +++ b/build.py @@ -99,6 +99,11 @@ def make_parser(): action='store_true', help='Build rustdesk libs with the flatpak feature enabled' ) + parser.add_argument( + '--appimage', + action='store_true', + help='Build rustdesk libs with the appimage feature enabled' + ) parser.add_argument( '--skip-cargo', action='store_true', @@ -236,6 +241,8 @@ def get_features(args): features.append('flutter') if args.flatpak: features.append('flatpak') + if args.appimage: + features.append('appimage') print("features:", features) return features From a01b87510c0a57c00c45e1ee47aa24d54b75980d Mon Sep 17 00:00:00 2001 From: csf Date: Sun, 8 Jan 2023 23:30:34 +0900 Subject: [PATCH 1375/2015] move login.dart --- flutter/lib/common/hbbs/hbbs.dart | 8 -------- flutter/lib/common/widgets/address_book.dart | 2 +- flutter/lib/{desktop => common}/widgets/login.dart | 0 flutter/lib/desktop/pages/desktop_setting_page.dart | 2 +- flutter/lib/mobile/pages/connection_page.dart | 2 +- flutter/lib/mobile/pages/settings_page.dart | 4 ++-- 6 files changed, 5 insertions(+), 13 deletions(-) rename flutter/lib/{desktop => common}/widgets/login.dart (100%) diff --git a/flutter/lib/common/hbbs/hbbs.dart b/flutter/lib/common/hbbs/hbbs.dart index 6a80f8269..27238db67 100644 --- a/flutter/lib/common/hbbs/hbbs.dart +++ b/flutter/lib/common/hbbs/hbbs.dart @@ -103,14 +103,6 @@ class LoginResponse { LoginResponse.fromJson(Map json) { access_token = json['access_token']; type = json['type']; - print("user: ${json['user']}"); - print("user id: ${json['user']['id']}"); - print("user name: ${json['user']['name']}"); - print("user email: ${json['user']['id']}"); - print("user note: ${json['user']['note']}"); - print("user status: ${json['user']['status']}"); - print("user grp: ${json['user']['grp']}"); - print("user is_admin: ${json['user']['is_admin']}"); user = json['user'] != null ? UserPayload.fromJson(json['user']) : null; } } diff --git a/flutter/lib/common/widgets/address_book.dart b/flutter/lib/common/widgets/address_book.dart index 3e7f46814..34d5af485 100644 --- a/flutter/lib/common/widgets/address_book.dart +++ b/flutter/lib/common/widgets/address_book.dart @@ -3,12 +3,12 @@ import 'package:flutter_hbb/common/formatter/id_formatter.dart'; import 'package:flutter_hbb/common/widgets/peer_card.dart'; import 'package:flutter_hbb/common/widgets/peers_view.dart'; import 'package:flutter_hbb/desktop/widgets/popup_menu.dart'; -import 'package:flutter_hbb/desktop/widgets/login.dart'; import '../../consts.dart'; import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu; import 'package:get/get.dart'; import '../../common.dart'; +import 'login.dart'; class AddressBook extends StatefulWidget { final EdgeInsets? menuPadding; diff --git a/flutter/lib/desktop/widgets/login.dart b/flutter/lib/common/widgets/login.dart similarity index 100% rename from flutter/lib/desktop/widgets/login.dart rename to flutter/lib/common/widgets/login.dart diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 15f78daeb..ca8e47e69 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -8,7 +8,6 @@ import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart'; import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart'; -import 'package:flutter_hbb/desktop/widgets/login.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/server_model.dart'; import 'package:get/get.dart'; @@ -18,6 +17,7 @@ import 'package:url_launcher/url_launcher_string.dart'; import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart'; import '../../common/widgets/dialog.dart'; +import '../../common/widgets/login.dart'; const double _kTabWidth = 235; const double _kTabHeight = 42; diff --git a/flutter/lib/mobile/pages/connection_page.dart b/flutter/lib/mobile/pages/connection_page.dart index b8104387e..6fce887bf 100644 --- a/flutter/lib/mobile/pages/connection_page.dart +++ b/flutter/lib/mobile/pages/connection_page.dart @@ -7,9 +7,9 @@ import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; import '../../common.dart'; +import '../../common/widgets/login.dart'; import '../../common/widgets/peer_tab_page.dart'; import '../../consts.dart'; -import '../../desktop/widgets/login.dart'; import '../../models/model.dart'; import '../../models/platform_model.dart'; import 'home_page.dart'; diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index 0764f8247..b14f3ee65 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -9,7 +9,7 @@ import 'package:url_launcher/url_launcher.dart'; import '../../common.dart'; import '../../common/widgets/dialog.dart'; -import '../../desktop/widgets/login.dart'; +import '../../common/widgets/login.dart'; import '../../models/model.dart'; import '../../models/platform_model.dart'; import '../widgets/dialog.dart'; @@ -398,7 +398,7 @@ void showServerSettings(OverlayDialogManager dialogManager) async { void showLanguageSettings(OverlayDialogManager dialogManager) async { try { final langs = json.decode(await bind.mainGetLangs()) as List; - var lang = await bind.mainGetLocalOption(key: "lang"); + var lang = bind.mainGetLocalOption(key: "lang"); dialogManager.show((setState, close) { setLang(v) { if (lang != v) { From bb8c50b2c709af831304990d4dfcea6ff3d6a23b Mon Sep 17 00:00:00 2001 From: csf Date: Sun, 8 Jan 2023 23:38:58 +0900 Subject: [PATCH 1376/2015] update translate --- 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/pl.rs | 1 + src/lang/pt_PT.rs | 1 + src/lang/ptbr.rs | 1 + src/lang/ru.rs | 1 + src/lang/sk.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 | 817 ++++++++++++++++++++++--------------------- src/lang/tr.rs | 1 + src/lang/tw.rs | 1 + src/lang/ua.rs | 1 + src/lang/vn.rs | 1 + 30 files changed, 438 insertions(+), 408 deletions(-) diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 093f2572c..ffc5396fd 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -210,6 +210,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Connecta sempre a través de relay"), ("whitelist_tip", ""), ("Login", "Inicia sessió"), + ("Remember me", ""), ("Logout", "Sortir"), ("Tags", ""), ("Search ID", "Cerca ID"), diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 307cbfd9b..6a4feeeec 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -210,6 +210,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "强制走中继连接"), ("whitelist_tip", "只有白名单里的ip才能访问我"), ("Login", "登录"), + ("Remember me", "记住我"), ("Logout", "登出"), ("Tags", "标签"), ("Search ID", "查找ID"), diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 027de13ba..17e2ea68b 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -210,6 +210,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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"), + ("Remember me", ""), ("Logout", "Odhlásit se"), ("Tags", "Štítky"), ("Search ID", "Hledat identifikátor"), diff --git a/src/lang/da.rs b/src/lang/da.rs index 3361804e8..c0db3eda0 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -210,6 +210,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Forbindelse via relæ-server"), ("whitelist_tip", "Kun IP'er på udgivelseslisten kan få adgang til mig"), ("Login", "Login"), + ("Remember me", ""), ("Logout", "logger af"), ("Tags", "Nøgleord"), ("Search ID", "Søg ID"), diff --git a/src/lang/de.rs b/src/lang/de.rs index 7226550f5..28b0a51d3 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -210,6 +210,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Immer über Relay-Server verbinden"), ("whitelist_tip", "Nur IPs auf der Whitelist können zugreifen."), ("Login", "Anmelden"), + ("Remember me", ""), ("Logout", "Abmelden"), ("Tags", "Schlagworte"), ("Search ID", "Suche ID"), diff --git a/src/lang/eo.rs b/src/lang/eo.rs index a21a2e91e..e92d7ab31 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -210,6 +210,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Ĉiam konekti per relajso"), ("whitelist_tip", "Nur la IP en la blanka listo povas kontroli mian komputilon"), ("Login", "Konekti"), + ("Remember me", ""), ("Logout", "Malkonekti"), ("Tags", "Etikedi"), ("Search ID", "Serĉi ID"), diff --git a/src/lang/es.rs b/src/lang/es.rs index b3276949a..539627cb9 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -210,6 +210,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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"), + ("Remember me", ""), ("Logout", "Salir"), ("Tags", "Tags"), ("Search ID", "Buscar ID"), diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 3d4579b27..9ca854ecb 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -210,6 +210,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "برای اتصال استفاده شود Relay از"), ("whitelist_tip", "های مجاز می توانند به این دسکتاپ متصل شوند IP فقط"), ("Login", "ورود"), + ("Remember me", ""), ("Logout", "خروج"), ("Tags", "برچسب ها"), ("Search ID", "جستجوی شناسه"), diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 1e3beb2e4..84cd9c4fa 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -210,6 +210,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Forcer la connexion relais"), ("whitelist_tip", "Seul l'IP dans la liste blanche peut accéder à mon appareil"), ("Login", "Connexion"), + ("Remember me", ""), ("Logout", "Déconnexion"), ("Tags", "Étiqueter"), ("Search ID", "Rechercher un ID"), diff --git a/src/lang/gr.rs b/src/lang/gr.rs index 4b2777729..f1ee5be08 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -210,6 +210,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Σύνδεση πάντα μέσω αναμετάδοσης"), ("whitelist_tip", "Μόνο οι IP της λίστας επιτρεπόμενων έχουν πρόσβαση"), ("Login", "Σύνδεση"), + ("Remember me", ""), ("Logout", "Αποσύνδεση"), ("Tags", "Ετικέτες"), ("Search ID", "Αναζήτηση ID"), diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 2f22ab511..92b7c1ba1 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -210,6 +210,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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"), + ("Remember me", ""), ("Logout", "Kilépés"), ("Tags", "Tagok"), ("Search ID", "Azonosító keresése..."), diff --git a/src/lang/id.rs b/src/lang/id.rs index 362a7fb85..2e96785c5 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -210,6 +210,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Selalu terhubung melalui relai"), ("whitelist_tip", "Hanya whitelisted IP yang dapat mengakses saya"), ("Login", "Masuk"), + ("Remember me", ""), ("Logout", "Keluar"), ("Tags", "Tag"), ("Search ID", "Cari ID"), diff --git a/src/lang/it.rs b/src/lang/it.rs index 390670df4..d54887aa4 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -210,6 +210,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Connetti sempre tramite relay"), ("whitelist_tip", "Solo gli indirizzi IP autorizzati possono connettersi a questo desktop"), ("Login", "Accedi"), + ("Remember me", ""), ("Logout", "Esci"), ("Tags", "Tag"), ("Search ID", "Cerca ID"), diff --git a/src/lang/ja.rs b/src/lang/ja.rs index c247a7582..2f3d0b54f 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -210,6 +210,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "常に中継サーバー経由で接続"), ("whitelist_tip", "ホワイトリストに登録されたIPからのみ接続を許可します"), ("Login", "ログイン"), + ("Remember me", ""), ("Logout", "ログアウト"), ("Tags", "タグ"), ("Search ID", "IDを検索"), diff --git a/src/lang/ko.rs b/src/lang/ko.rs index a4f2fde77..21e75baef 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -210,6 +210,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "항상 relay를 통해 접속하기"), ("whitelist_tip", "화이트리스트에 있는 IP만 현 데스크탑에 접속 가능합니다"), ("Login", "로그인"), + ("Remember me", ""), ("Logout", "로그아웃"), ("Tags", "태그"), ("Search ID", "ID 검색"), diff --git a/src/lang/kz.rs b/src/lang/kz.rs index d8037ff62..69caf1b6b 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -210,6 +210,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Әрқашан да релай сербері арқылы қосылу"), ("whitelist_tip", "Маған тек ақ-тізімделген IP қол жеткізе алады"), ("Login", "Кіру"), + ("Remember me", ""), ("Logout", "Шығу"), ("Tags", "Тақтар"), ("Search ID", "ID Іздеу"), diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 2cf91af4a..24674e197 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -210,6 +210,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Zawsze łącz pośrednio"), ("whitelist_tip", "Zezwlaj na łączenie z tym komputerem tylko z adresów IP znajdujących się na białej liście"), ("Login", "Zaloguj"), + ("Remember me", ""), ("Logout", "Wyloguj"), ("Tags", "Tagi"), ("Search ID", "Szukaj ID"), diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index fc95cb548..dcf48eca4 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -210,6 +210,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Sempre conectar via relay"), ("whitelist_tip", "Somente IPs na whitelist podem me acessar"), ("Login", "Login"), + ("Remember me", ""), ("Logout", "Sair"), ("Tags", "Tags"), ("Search ID", "Procurar ID"), diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 5a89efd4b..3ffee4f93 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -210,6 +210,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Sempre conectar via relay"), ("whitelist_tip", "Somente IPs confiáveis podem me acessar"), ("Login", "Login"), + ("Remember me", ""), ("Logout", "Sair"), ("Tags", "Tags"), ("Search ID", "Pesquisar ID"), diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 82d8d357e..8dd6bfbca 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -210,6 +210,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Всегда подключаться через ретрансляционный сервер"), ("whitelist_tip", "Только IP-адреса из белого списка могут получить доступ ко мне"), ("Login", "Войти"), + ("Remember me", ""), ("Logout", "Выйти"), ("Tags", "Метки"), ("Search ID", "Поиск по ID"), diff --git a/src/lang/sk.rs b/src/lang/sk.rs index e61ac9c58..f67eb60cc 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -210,6 +210,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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"), + ("Remember me", ""), ("Logout", "Odhlásenie"), ("Tags", "Štítky"), ("Search ID", "Hľadať ID"), diff --git a/src/lang/sq.rs b/src/lang/sq.rs index c55562942..222f16a8e 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -210,6 +210,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Gjithmonë lidheni me transmetues"), ("whitelist_tip", "Vetëm IP e listës së bardhë mund të më aksesoj."), ("Login", "Hyrje"), + ("Remember me", ""), ("Logout", "Dalje"), ("Tags", "Tage"), ("Search ID", "Kerko ID"), diff --git a/src/lang/sr.rs b/src/lang/sr.rs index d74baf8b2..660df7e9e 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -210,6 +210,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Uvek se spoj preko posrednika"), ("whitelist_tip", "Samo dozvoljene IP mi mogu pristupiti"), ("Login", "Prijava"), + ("Remember me", ""), ("Logout", "Odjava"), ("Tags", "Oznake"), ("Search ID", "Traži ID"), diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 6a770c24a..870d92569 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -210,6 +210,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Anslut alltid via relay"), ("whitelist_tip", "Bara vitlistade IPs kan koppla upp till mig"), ("Login", "Logga in"), + ("Remember me", ""), ("Logout", "Logga ut"), ("Tags", "Taggar"), ("Search ID", "Sök ID"), diff --git a/src/lang/template.rs b/src/lang/template.rs index b4113b91a..3824bf60d 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -210,6 +210,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", ""), ("whitelist_tip", ""), ("Login", ""), + ("Remember me", ""), ("Logout", ""), ("Tags", ""), ("Search ID", ""), diff --git a/src/lang/th.rs b/src/lang/th.rs index 792f4f97a..b2ca698e1 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -1,409 +1,410 @@ lazy_static::lazy_static! { - pub static ref T: std::collections::HashMap<&'static str, &'static str> = - [ - ("Status", "สถานะ"), - ("Your Desktop", "หน้าจอของคุณ"), - ("desk_tip", "คุณสามารถเข้าถึงเดสก์ท็อปของคุณได้ด้วย ID และรหัสผ่านต่อไปนี้"), - ("Password", "รหัสผ่าน"), - ("Ready", "พร้อม"), - ("Established", "เชื่อมต่อแล้ว"), - ("connecting_status", "กำลังเชื่อมต่อไปยังเครือข่าย RustDesk..."), - ("Enable Service", "เปิดใช้การงานเซอร์วิส"), - ("Start Service", "เริ่มต้นใช้งานเซอร์วิส"), - ("Service is running", "เซอร์วิสกำลังทำงาน"), - ("Service is not running", "เซอร์วิสไม่ทำงาน"), - ("not_ready_status", "ไม่พร้อมใช้งาน กรุณาตรวจสอบการเชื่อมต่ออินเทอร์เน็ตของคุณ"), - ("Control Remote Desktop", "การควบคุมเดสก์ท็อปปลายทาง"), - ("Transfer File", "การถ่ายโอนไฟล์"), - ("Connect", "เชื่อมต่อ"), - ("Recent Sessions", "เซสชันล่าสุด"), - ("Address Book", "สมุดรายชื่อ"), - ("Confirmation", "การยืนยัน"), - ("TCP Tunneling", "อุโมงค์การเชื่อมต่อ TCP"), - ("Remove", "ลบ"), - ("Refresh random password", "รีเฟรชรหัสผ่านใหม่แบบสุ่ม"), - ("Set your own password", "ตั้งรหัสผ่านของคุณเอง"), - ("Enable Keyboard/Mouse", "เปิดการใช้งาน คีย์บอร์ด/เมาส์"), - ("Enable Clipboard", "เปิดการใช้งาน คลิปบอร์ด"), - ("Enable File Transfer", "เปิดการใช้งาน การถ่ายโอนไฟล์"), - ("Enable TCP Tunneling", "เปิดการใช้งาน อุโมงค์การเชื่อมต่อ TCP"), - ("IP Whitelisting", "IP ไวท์ลิสต์"), - ("ID/Relay Server", "เซิร์ฟเวอร์ ID/Relay"), - ("Import Server Config", "นำเข้าการตั้งค่าเซิร์ฟเวอร์"), - ("Export Server Config", "ส่งออกการตั้งค่าเซิร์ฟเวอร์"), - ("Import server configuration successfully", "นำเข้าการตั้งค่าเซิร์ฟเวอร์เสร็จสมบูรณ์"), - ("Export server configuration successfully", "ส่งออกการตั้งค่าเซิร์ฟเวอร์เสร็จสมบูรณ์"), - ("Invalid server configuration", "การตั้งค่าของเซิร์ฟเวอร์ไม่ถูกต้อง"), - ("Clipboard is empty", "คลิปบอร์ดว่างเปล่า"), - ("Stop service", "หยุดการใช้งานเซอร์วิส"), - ("Change ID", "เปลี่ยน ID"), - ("Website", "เว็บไซต์"), - ("About", "เกี่ยวกับ"), - ("Slogan_tip", "ทำด้วยใจ ในโลกใบนี้ที่ยุ่งเหยิง!"), - ("Privacy Statement", "คำแถลงเกี่ยวกับความเป็นส่วนตัว"), - ("Mute", "ปิดเสียง"), - ("Audio Input", "ออดิโออินพุท"), - ("Enhancements", "การปรับปรุง"), - ("Hardware Codec", "ฮาร์ดแวร์ codec"), - ("Adaptive Bitrate", "บิทเรทผันแปร"), - ("ID Server", "เซิร์ฟเวอร์ ID"), - ("Relay Server", "เซิร์ฟเวอร์ Relay"), - ("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", "ไม่พร้อมใช้งาน"), - ("Too frequent", "ดำเนินการถี่เกินไป"), - ("Cancel", "ยกเลิก"), - ("Skip", "ข้าม"), - ("Close", "ปิด"), - ("Retry", "ลองใหม่อีกครั้ง"), - ("OK", "ตกลง"), - ("Password Required", "ต้องใช้รหัสผ่าน"), - ("Please enter your password", "กรุณาใส่รหัสผ่านของคุณ"), - ("Remember password", "จดจำรหัสผ่าน"), - ("Wrong Password", "รหัสผ่านไม่ถูกต้อง"), - ("Do you want to enter again?", "ต้องการใส่ข้อมูลอีกครั้งหรือไม่?"), - ("Connection Error", "การเชื่อมต่อผิดพลาด"), - ("Error", "ข้อผิดพลาด"), - ("Reset by the peer", "รีเซ็ตโดยอีกฝั่ง"), - ("Connecting...", "กำลังเชื่อมต่อ..."), - ("Connection in progress. Please wait.", "กำลังดำเนินการเชื่อมต่อ กรุณารอซักครู่"), - ("Please try 1 minute later", "กรุณาลองใหม่อีกครั้งใน 1 นาที"), - ("Login Error", "การเข้าสู่ระบบผิดพลาด"), - ("Successful", "สำเร็จ"), - ("Connected, waiting for image...", "เชื่อมต่อสำเร็จ กำลังรับข้อมูลภาพ..."), - ("Name", "ชื่อ"), - ("Type", "ประเภท"), - ("Modified", "แก้ไขล่าสุด"), - ("Size", "ขนาด"), - ("Show Hidden Files", "แสดงไฟล์ที่ถูกซ่อน"), - ("Receive", "รับ"), - ("Send", "ส่ง"), - ("Refresh File", "รีเฟรชไฟล์"), - ("Local", "ต้นทาง"), - ("Remote", "ปลายทาง"), - ("Remote Computer", "คอมพิวเตอร์ปลายทาง"), - ("Local Computer", "คอมพิวเตอร์ต้นทาง"), - ("Confirm Delete", "ยืนยันการลบ"), - ("Delete", "ลบ"), - ("Properties", "ข้อมูล"), - ("Multi Select", "เลือกหลายรายการ"), - ("Select All", "เลือกทั้งหมด"), - ("Unselect All", "ยกเลิกการเลือกทั้งหมด"), - ("Empty Directory", "ไดเรกทอรีว่างเปล่า"), - ("Not an empty directory", "ไม่ใช่ไดเรกทอรีว่างเปล่า"), - ("Are you sure you want to delete this file?", "คุณแน่ใจหรือไม่ที่จะลบไฟล์นี้?"), - ("Are you sure you want to delete this empty directory?", "คุณแน่ใจหรือไม่ที่จะลบไดเรอทอรีว่างเปล่านี้?"), - ("Are you sure you want to delete the file of this directory?", "คุณแน่ใจหรือไม่ที่จะลบไฟล์ของไดเรกทอรีนี้?"), - ("Do this for all conflicts", "ดำเนินการแบบเดียวกันสำหรับรายการทั้งหมด"), - ("This is irreversible!", "การดำเนินการนี้ไม่สามารถย้อนกลับได้!"), - ("Deleting", "กำลังลบ"), - ("files", "ไฟล์"), - ("Waiting", "กำลังรอ"), - ("Finished", "เสร็จแล้ว"), - ("Speed", "ความเร็ว"), - ("Custom Image Quality", "คุณภาพของภาพแบบกำหนดเอง"), - ("Privacy mode", "โหมดความเป็นส่วนตัว"), - ("Block user input", "บล็อคอินพุทจากผู้ใช้งาน"), - ("Unblock user input", "ยกเลิกการบล็อคอินพุทจากผู้ใช้งาน"), - ("Adjust Window", "ปรับขนาดหน้าต่าง"), - ("Original", "ต้นฉบับ"), - ("Shrink", "ย่อ"), - ("Stretch", "ยืด"), - ("Scrollbar", "แถบเลื่อน"), - ("ScrollAuto", "เลื่อนอัตโนมัติ"), - ("Good image quality", "ภาพคุณภาพดี"), - ("Balanced", "สมดุล"), - ("Optimize reaction time", "เน้นการตอบสนอง"), - ("Custom", "กำหนดเอง"), - ("Show remote cursor", "แสดงเคอร์เซอร์ปลายทาง"), - ("Show quality monitor", "แสดงคุณภาพหน้าจอ"), - ("Disable clipboard", "ปิดการใช้งานคลิปบอร์ด"), - ("Lock after session end", "ล็อคหลังจากจบเซสชัน"), - ("Insert", "แทรก"), - ("Insert Lock", "แทรกล็อค"), - ("Refresh", "รีเฟรช"), - ("ID does not exist", "ไม่พอข้อมูล ID"), - ("Failed to connect to rendezvous server", "การเชื่อมต่อไปยังเซิร์ฟเวอร์นัดพบล้มเหลว"), - ("Please try later", "กรุณาลองใหม่ในภายหลัง"), - ("Remote desktop is offline", "เดสก์ท็อปปลายทางออฟไลน์"), - ("Key mismatch", "คีย์ไม่ถูกต้อง"), - ("Timeout", "หมดเวลา"), - ("Failed to connect to relay server", "การเชื่อมต่อไปยังเซิร์ฟเวอร์รีเลย์ล้มเหลว"), - ("Failed to connect via rendezvous server", "การเชื่อมต่อผ่านเซิร์ฟเวอร์นัดพบล้มเหลว"), - ("Failed to connect via relay server", "การเชื่อมต่อผ่านเซิร์ฟเวอร์รีเลย์ล้มเหลว"), - ("Failed to make direct connection to remote desktop", "การเชื่อมต่อตรงไปยังเดสก์ท็อปปลายทางล้มเหลว"), - ("Set Password", "ตั้งรหัสผ่าน"), - ("OS Password", "รหัสผ่านระบบปฏิบัติการ"), - ("install_tip", "เนื่องด้วยข้อจำกัดของการใช้งาน UAC ทำให้ RustDesk ไม่สามารถทำงานได้ปกติในฝั่งปลายทางในบางครั้ง เพื่อหลีกเลี่ยงข้อจำกัดของ UAC กรุณากดปุ่มด้านล่างเพื่อติดตั้ง RustDesk ไปยังระบบของคุณ"), - ("Click to upgrade", "คลิกเพื่ออัปเกรด"), - ("Click to download", "คลิกเพื่อดาวน์โหลด"), - ("Click to update", "คลิกเพื่ออัปเดต"), - ("Configure", "ปรับแต่งค่า"), - ("config_acc", "เพื่อที่จะควบคุมเดสก์ท็อปปลายทางของคุณ คุณจำเป็นจะต้องอนุญาตสิทธิ์ \"การเข้าถึง\" ให้แก่ RustDesk"), - ("config_screen", "เพื่อที่จะควบคุมเดสก์ท็อปปลายทางของคุณ คุณจำเป็นจะต้องอนุญาตสิทธิ์ \"การบันทึกภาพหน้าจอ\" ให้แก่ RustDesk"), - ("Installing ...", "กำลังติดตั้ง ..."), - ("Install", "ติดตั้ง"), - ("Installation", "การติดตั้ง"), - ("Installation Path", "ตำแหน่งที่ติดตั้ง"), - ("Create start menu shortcuts", "สร้างทางลัดไปยัง Start Menu"), - ("Create desktop icon", "สร้างไอคอนบนเดสก์ท็อป"), - ("agreement_tip", "ในการเริ่มต้นการติดตั้ง ถือว่าคุณได้ยอมรับข้อตกลงใบอนุญาตแล้ว"), - ("Accept and Install", "ยอมรับและติดตั้ง"), - ("End-user license agreement", "ข้อตกลงใบอนุญาตผู้ใช้งาน"), - ("Generating ...", "กำลังสร้าง ..."), - ("Your installation is lower version.", "การติดตั้งของคุณเป็นเวอร์ชั่นที่ต่ำกว่า"), - ("not_close_tcp_tip", "อย่าปิดหน้าต่างนี้ในขณะที่คุณกำลังใช้งานอุโมงค์การเชื่อมต่อ"), - ("Listening ...", "กำลังรอรับข้อมูล ..."), - ("Remote Host", "โฮสต์ปลายทาง"), - ("Remote Port", "พอร์ทปลายทาง"), - ("Action", "การดำเนินการ"), - ("Add", "เพิ่ม"), - ("Local Port", "พอร์ทต้นทาง"), - ("Local Address", "ที่อยู่ต้นทาง"), - ("Change Local Port", "เปลี่ยนพอร์ทต้นทาง"), - ("setup_server_tip", "เพื่อการเชื่อมต่อที่เร็วขึ้น กรุณาเซ็ตอัปเซิร์ฟเวอร์ของคุณเอง"), - ("Too short, at least 6 characters.", "สั้นเกินไป ต้องไม่ต่ำกว่า 6 ตัวอักษร"), - ("The confirmation is not identical.", "การยืนยันข้อมูลไม่ถูกต้อง"), - ("Permissions", "สิทธิ์การใช้งาน"), - ("Accept", "ยอมรับ"), - ("Dismiss", "ปิด"), - ("Disconnect", "ยกเลิกการเชื่อมต่อ"), - ("Allow using keyboard and mouse", "อนุญาตให้ใช้งานคีย์บอร์ดและเมาส์"), - ("Allow using clipboard", "อนุญาตให้ใช้คลิปบอร์ด"), - ("Allow hearing sound", "อนุญาตให้ได้ยินเสียง"), - ("Allow file copy and paste", "อนุญาตให้มีการคัดลอกและวางไฟล์"), - ("Connected", "เชื่อมต่อแล้ว"), - ("Direct and encrypted connection", "การเชื่อมต่อตรงที่มีการเข้ารหัส"), - ("Relayed and encrypted connection", "การเชื่อมต่อแบบรีเลย์ที่มีการเข้ารหัส"), - ("Direct and unencrypted connection", "การเชื่อมต่อตรงที่ไม่มีการเข้ารหัส"), - ("Relayed and unencrypted connection", "การเชื่อมต่อแบบรีเลย์ที่ไม่มีการเข้ารหัส"), - ("Enter Remote ID", "กรอก ID ปลายทาง"), - ("Enter your password", "กรอกรหัสผ่าน"), - ("Logging in...", "กำลังเข้าสู่ระบบ..."), - ("Enable RDP session sharing", "เปิดการใช้งานการแชร์เซสชัน RDP"), - ("Auto Login", "เข้าสู่ระบอัตโนมัติ"), - ("Enable Direct IP Access", "เปิดการใช้งาน IP ตรง"), - ("Rename", "ปลายทาง"), - ("Space", "พื้นที่ว่าง"), - ("Create Desktop Shortcut", "สร้างทางลัดบนเดสก์ท็อป"), - ("Change Path", "เปลี่ยนตำแหน่ง"), - ("Create Folder", "สร้างโฟลเดอร์"), - ("Please enter the folder name", "กรุณาใส่ชื่อโฟลเดอร์"), - ("Fix it", "แก้ไข"), - ("Warning", "คำเตือน"), - ("Login screen using Wayland is not supported", "หน้าจอการเข้าสู่ระบบโดยใช้ Wayland ยังไม่ถูกรองรับ"), - ("Reboot required", "จำเป็นต้องเริ่มต้นระบบใหม่"), - ("Unsupported display server ", "เซิร์ฟเวอร์การแสดงผลที่ไม่รองรับ"), - ("x11 expected", "ต้องใช้งาน x11"), - ("Port", "พอร์ท"), - ("Settings", "ตั้งค่า"), - ("Username", "ชื่อผู้ใช้งาน"), - ("Invalid port", "พอร์ทไม่ถูกต้อง"), - ("Closed manually by the peer", "ถูกปิดโดยอีกฝั่งการการเชื่อมต่อ"), - ("Enable remote configuration modification", "เปิดการใช้งานการแก้ไขการตั้งค่าปลายทาง"), - ("Run without install", "ใช้งานโดยไม่ต้องติดตั้ง"), - ("Always connected via relay", "เชื่อมต่อผ่านรีเลย์เสมอ"), - ("Always connect via relay", "เชื่อมต่อผ่านรีเลย์เสมอ"), - ("whitelist_tip", "อนุญาตเฉพาะการเชื่อมต่อจาก IP ที่ไวท์ลิสต์"), - ("Login", "เข้าสู่ระบบ"), - ("Logout", "ออกจากระบบ"), - ("Tags", "แท็ก"), - ("Search ID", "ค้นหา ID"), - ("Current Wayland display server is not supported", "เซิร์ฟเวอร์การแสดงผล Wayland ปัจจุบันไม่รองรับ"), - ("whitelist_sep", "คั่นโดยเครื่องหมาย comma semicolon เว้นวรรค หรือ ขึ้นบรรทัดใหม่"), - ("Add ID", "เพิ่ม ID"), - ("Add Tag", "เพิ่มแท็ก"), - ("Unselect all tags", "ยกเลิกการเลือกแท็กทั้งหมด"), - ("Network error", "ข้อผิดพลาดของเครือข่าย"), - ("Username missed", "ไม่พบข้อมูลผู้ใช้งาน"), - ("Password missed", "ไม่พบรหัสผ่าน"), - ("Wrong credentials", "ข้อมูลสำหรับเข้าสู่ระบบไม่ถูกต้อง"), - ("Edit Tag", "แก้ไขแท็ก"), - ("Unremember Password", "ยกเลิกการจดจำรหัสผ่าน"), - ("Favorites", "รายการโปรด"), - ("Add to Favorites", "เพิ่มไปยังรายการโปรด"), - ("Remove from Favorites", "ลบออกจากรายการโปรด"), - ("Empty", "ว่างเปล่า"), - ("Invalid folder name", "ชื่อโฟลเดอร์ไม่ถูกต้อง"), - ("Socks5 Proxy", "พรอกซี Socks5"), - ("Hostname", "ชื่อโฮสต์"), - ("Discovered", "ค้นพบ"), - ("install_daemon_tip", "หากต้องการใช้งานขณะระบบเริ่มต้น คุณจำเป็นจะต้องติดตั้งเซอร์วิส"), - ("Remote ID", "ID ปลายทาง"), - ("Paste", "วาง"), - ("Paste here?", "วางที่นี่หรือไม่?"), - ("Are you sure to close the connection?", "คุณแน่ใจหรือไม่ที่จะปิดการเชื่อมต่อ?"), - ("Download new version", "ดาวน์โหลดเวอร์ชั่นใหม่"), - ("Touch mode", "โหมดการสัมผัส"), - ("Mouse mode", "โหมดการใช้เมาส์"), - ("One-Finger Tap", "แตะนิ้วเดียว"), - ("Left Mouse", "เมาส์ซ้าย"), - ("One-Long Tap", "แตะยาวหนึ่งครั้ง"), - ("Two-Finger Tap", "แตะสองนิ้ว"), - ("Right Mouse", "เมาส์ขวา"), - ("One-Finger Move", "ลากนิ้วเดียว"), - ("Double Tap & Move", "แตะเบิ้ลและลาก"), - ("Mouse Drag", "ลากเมาส์"), - ("Three-Finger vertically", "สามนิ้วแนวตั้ง"), - ("Mouse Wheel", "ลูกลิ้งเมาส์"), - ("Two-Finger Move", "ลากสองนิ้ว"), - ("Canvas Move", "ลากแคนวาส"), - ("Pinch to Zoom", "ถ่างเพื่อขยาย"), - ("Canvas Zoom", "ขยายแคนวาส"), - ("Reset canvas", "รีเซ็ตแคนวาส"), - ("No permission of file transfer", "ไม่มีสิทธิ์ในการถ่ายโอนไฟล์"), - ("Note", "บันทึกข้อความ"), - ("Connection", "การเชื่อมต่อ"), - ("Share Screen", "แชร์หน้าจอ"), - ("CLOSE", "ปิด"), - ("OPEN", "เปิด"), - ("Chat", "แชท"), - ("Total", "รวม"), - ("items", "รายการ"), - ("Selected", "ถูกเลือก"), - ("Screen Capture", "แคปเจอร์หน้าจอ"), - ("Input Control", "ควบคุมอินพุท"), - ("Audio Capture", "แคปเจอร์เสียง"), - ("File Connection", "การเชื่อมต่อไฟล์"), - ("Screen Connection", "การเชื่อมต่อหน้าจอ"), - ("Do you accept?", "ยอมรับหรือไม่?"), - ("Open System Setting", "เปิดการตั้งค่าระบบ"), - ("How to get Android input permission?", "เปิดสิทธิ์การใช้งานอินพุทของแอนดรอยด์ได้อย่างไร?"), - ("android_input_permission_tip1", "ในการที่จะอนุญาตให้เครื่องปลายทางควบคุมอุปกรณ์แอนดรอยด์ของคุณโดยใช้เมาส์หรือการสัมผัส คุณจำเป็นจะต้องอนุญาตสิทธิ์ \"การเข้าถึง\" ให้แก่เซอร์วิสของ RustDesk"), - ("android_input_permission_tip2", "กรุณาไปยังหน้าตั้งค่าถัดไป ค้นหาและเข้าไปยัง [เซอร์วิสที่ถูกติดตั้ง] และเปิดการใช้งานเซอร์วิส [อินพุท RustDesk]"), - ("android_new_connection_tip", "ได้รับคำขอควบคุมใหม่ที่ต้องการควบคุมอุปกรณ์ของคุณ"), - ("android_service_will_start_tip", "การเปิดการใช้งาน \"การบันทึกหน้าจอ\" จะเป็นการเริ่มต้นการทำงานของเซอร์วิสโดยอัตโนมัติ ที่จะอนุญาตให้อุปกรณ์อื่นๆ ส่งคำขอเข้าถึงมายังอุปกรณ์ของคุณได้"), - ("android_stop_service_tip", "การปิดการใช้งานเซอร์วิสจะปิดการเชื่อมต่อทั้งหมดโดยอัตโนมัติ"), - ("android_version_audio_tip", "เวอร์ชั่นแอนดรอยด์ปัจจุบันของคุณไม่รองรับการบันทึกข้อมูลเสียง กรุณาอัปเกรดเป็นแอนดรอยด์เวอร์ชั่น 10 หรือสูงกว่า"), - ("android_start_service_tip", "แตะ [เริ่มต้นใช้งานเซอร์วิส] หรือเปิดสิทธิ์ [การบันทึกหน้าจอ] เพื่อเริ่มเซอร์วิสการแชร์หน้าจอ"), - ("Account", "บัญชี"), - ("Overwrite", "เขียนทับ"), - ("This file exists, skip or overwrite this file?", "พบไฟล์ที่มีอยู่แล้ว ต้องการเขียนทับหรือไม่?"), - ("Quit", "ออก"), - ("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"), - ("Help", "ช่วยเหลือ"), - ("Failed", "ล้มเหลว"), - ("Succeeded", "สำเร็จ"), - ("Someone turns on privacy mode, exit", "มีใครบางคนเปิดใช้งานโหมดความเป็นส่วนตัว กำลังออก"), - ("Unsupported", "ไม่รองรับ"), - ("Peer denied", "ถูกปฏิเสธโดยอีกฝั่ง"), - ("Please install plugins", "กรุณาติดตั้งปลั๊กอิน"), - ("Peer exit", "อีกฝั่งออก"), - ("Failed to turn off", "การปิดล้มเหลว"), - ("Turned off", "ปิด"), - ("In privacy mode", "อยู่ในโหมดความเป็นส่วนตัว"), - ("Out privacy mode", "อยู่นอกโหมดความเป็นส่วนตัว"), - ("Language", "ภาษา"), - ("Keep RustDesk background service", "คงสถานะการทำงานเบื้องหลังของเซอร์วิส RustDesk"), - ("Ignore Battery Optimizations", "เพิกเฉยการตั้งค่าการใช้งาน Battery Optimization"), - ("android_open_battery_optimizations_tip", "หากคุณต้องการปิดการใช้งานฟีเจอร์นี้ กรุณาไปยังหน้าตั้งค่าในแอปพลิเคชัน RustDesk ค้นหาหัวข้อ [Battery] และยกเลิกการเลือกรายการ [Unrestricted]"), - ("Connection not allowed", "การเชื่อมต่อไม่อนุญาต"), - ("Legacy mode", ""), - ("Map mode", ""), - ("Translate mode", ""), - ("Use permanent password", "ใช้รหัสผ่านถาวร"), - ("Use both passwords", "ใช้รหัสผ่านทั้งสองแบบ"), - ("Set permanent password", "ตั้งค่ารหัสผ่านถาวร"), - ("Enable Remote Restart", "เปิดการใช้งานการรีสตาร์ทระบบทางไกล"), - ("Allow remote restart", "อนุญาตการรีสตาร์ทระบบทางไกล"), - ("Restart Remote Device", "รีสตาร์ทอุปกรณ์ปลายทาง"), - ("Are you sure you want to restart", "คุณแน่ใจหรือไม่ที่จะรีสตาร์ท"), - ("Restarting Remote Device", "กำลังรีสตาร์ทระบบปลายทาง"), - ("remote_restarting_tip", "ระบบปลายทางกำลังรีสตาร์ท กรุณาปิดกล่องข้อความนี้และดำเนินการเขื่อมต่อใหม่อีกครั้งด้วยรหัสผ่านถาวรหลังจากผ่านไปซักครู่"), - ("Copied", "คัดลอกแล้ว"), - ("Exit Fullscreen", "ออกจากเต็มหน้าจอ"), - ("Fullscreen", "เต็มหน้าจอ"), - ("Mobile Actions", "การดำเนินการบนมือถือ"), - ("Select Monitor", "เลือกหน้าจอ"), - ("Control Actions", "การดำเนินการควบคุม"), - ("Display Settings", "การตั้งค่าแสดงผล"), - ("Ratio", "อัตราส่วน"), - ("Image Quality", "คุณภาพภาพ"), - ("Scroll Style", "ลักษณะการเลื่อน"), - ("Show Menubar", "แสดงแถบเมนู"), - ("Hide Menubar", "ซ่อนแถบเมนู"), - ("Direct Connection", "การเชื่อมต่อตรง"), - ("Relay Connection", "การเชื่อมต่อแบบรีเลย์"), - ("Secure Connection", "การเชื่อมต่อที่ปลอดภัย"), - ("Insecure Connection", "การเชื่อมต่อที่ไม่ปลอดภัย"), - ("Scale original", "ขนาดเดิม"), - ("Scale adaptive", "ขนาดยืดหยุ่น"), - ("General", "ทั่วไป"), - ("Security", "ความปลอดภัย"), - ("Theme", "ธีม"), - ("Dark Theme", "ธีมมืด"), - ("Dark", "มืด"), - ("Light", "สว่าง"), - ("Follow System", "ตามระบบ"), - ("Enable hardware codec", "เปิดการใช้งานฮาร์ดแวร์ codec"), - ("Unlock Security Settings", "ปลดล็อคการตั้งค่าความปลอดภัย"), - ("Enable Audio", "เปิดการใช้งานเสียง"), - ("Unlock Network Settings", "ปลดล็อคการตั้งค่าเครือข่าย"), - ("Server", "เซิร์ฟเวอร์"), - ("Direct IP Access", "การเข้าถึง IP ตรง"), - ("Proxy", "พรอกซี"), - ("Apply", "นำไปใช้"), - ("Disconnect all devices?", "ยกเลิกการเชื่อมต่ออุปกรณ์ทั้งหมด?"), - ("Clear", "ล้างข้อมูล"), - ("Audio Input Device", "อุปกรณ์รับอินพุทข้อมูลเสียง"), - ("Deny remote access", "ปฏิเสธการเชื่อมต่อ"), - ("Use IP Whitelisting", "ใช้งาน IP ไวท์ลิสต์"), - ("Network", "เครือข่าย"), - ("Enable RDP", "เปิดการใช้งาน RDP"), - ("Pin menubar", "ปักหมุดแถบเมนู"), - ("Unpin menubar", "ยกเลิกการปักหมุดแถบเมนู"), - ("Recording", "การบันทึก"), - ("Directory", "ไดเรกทอรี่"), - ("Automatically record incoming sessions", "บันทึกเซสชันขาเข้าโดยอัตโนมัติ"), - ("Change", "เปลี่ยน"), - ("Start session recording", "เริ่มต้นการบันทึกเซสชัน"), - ("Stop session recording", "หยุดการบันทึกเซสซัน"), - ("Enable Recording Session", "เปิดใช้งานการบันทึกเซสชัน"), - ("Allow recording session", "อนุญาตการบันทึกเซสชัน"), - ("Enable LAN Discovery", "เปิดการใช้งานการค้นหาในวง LAN"), - ("Deny LAN Discovery", "ปฏิเสธการใช้งานการค้นหาในวง LAN"), - ("Write a message", "เขียนข้อความ"), - ("Prompt", ""), - ("Please wait for confirmation of UAC...", "กรุณารอการยืนยันจาก UAC..."), - ("elevated_foreground_window_tip", "หน้าต่างปัจจุบันของเครื่องปลายทางต้องการสิทธิ์การใช้งานที่สูงขึ้นสำหรับการทำงาน ดังนั้นเมาส์และคีย์บอร์ดจะไม่สามารถใช้งานได้ชั่วคราว คุณสามารถขอผู้ใช้งานปลายทางให้ย่อหน้าต่าง หรือคลิกปุ่มให้สิทธิ์การใช้งานในหน้าต่างการจัดการการเชื่อมต่อ เพื่อหลีกเลี่ยงปัญหานี้เราแนะนำให้ดำเนินการติดตั้งซอฟท์แวร์ในเครื่องปลายทาง"), - ("Disconnected", "ยกเลิกการเชื่อมต่อ"), - ("Other", "อื่นๆ"), - ("Confirm before closing multiple tabs", "ยืนยันการปิดหลายแท็บ"), - ("Keyboard Settings", "การตั้งค่าคีย์บอร์ด"), - ("Full Access", "การเข้าถึงทั้งหมด"), - ("Screen Share", "การแชร์จอ"), - ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland ต้องการ Ubuntu เวอร์ชั่น 21.04 หรือสูงกว่า"), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland ต้องการลินุกซ์เวอร์ชันที่สูงกว่านี้ กรุณาเปลี่ยนไปใช้เดสก์ท็อป X11 หรือเปลี่ยนระบบปฏิบัติการของคุณ"), - ("JumpLink", "View"), - ("Please Select the screen to be shared(Operate on the peer side).", "กรุณาเลือกหน้าจอที่ต้องการแชร์ (ใช้งานในอีกฝั่งของการเชื่อมต่อ)"), - ("Show RustDesk", "แสดง RustDesk"), - ("This PC", ""), - ("or", "หรือ"), - ("Continue with", "ทำต่อด้วย"), - ("Elevate", "ยกระดับ"), - ("Zoom cursor", "ขยายเคอร์เซอร์"), - ("Accept sessions via password", "ยอมรับการเชื่อมต่อด้วยรหัสผ่าน"), - ("Accept sessions via click", "ยอมรับการเชื่อมต่อด้วยการคลิก"), - ("Accept sessions via both", "ยอมรับการเชื่อมต่อด้วยทั้งสองวิธิ"), - ("Please wait for the remote side to accept your session request...", "กรุณารอให้อีกฝั่งยอมรับการเชื่อมต่อของคุณ..."), - ("One-time Password", "รหัสผ่านครั้งเดียว"), - ("Use one-time password", "ใช้รหัสผ่านครั้งเดียว"), - ("One-time password length", "ความยาวรหัสผ่านครั้งเดียว"), - ("Request access to your device", "คำขอการเข้าถึงอุปกรณ์ของคุณ"), - ("Hide connection management window", "ซ่อนหน้าต่างการจัดการการเชื่อมต่อ"), - ("hide_cm_tip", "อนุญาตการซ่อนก็ต่อเมื่อยอมรับการเชื่อมต่อด้วยรหัสผ่าน และต้องเป็นรหัสผ่านถาวรเท่านั้น"), - ("wayland_experiment_tip", "การสนับสนุน Wayland ยังอยู่ในขั้นตอนการทดลอง กรุณาใช้ X11 หากคุณต้องการใช้งานการเข้าถึงแบบไม่มีผู้ดูแล"), - ("Right click to select tabs", "คลิกขวาเพื่อเลือกแท็บ"), - ("Skipped", "ข้าม"), - ("Add to Address Book", "เพิ่มไปยังสมุดรายชื่อ"), - ("Group", "กลุ่ม"), - ("Search", "ค้นหา"), - ("Closed manually by the web console", "ถูกปิดโดยเว็บคอนโซล"), - ("Local keyboard type", "ประเภทคีย์บอร์ด"), - ("Select local keyboard type", "เลือกประเภทคีย์บอร์ด"), - ].iter().cloned().collect(); - } +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "สถานะ"), + ("Your Desktop", "หน้าจอของคุณ"), + ("desk_tip", "คุณสามารถเข้าถึงเดสก์ท็อปของคุณได้ด้วย ID และรหัสผ่านต่อไปนี้"), + ("Password", "รหัสผ่าน"), + ("Ready", "พร้อม"), + ("Established", "เชื่อมต่อแล้ว"), + ("connecting_status", "กำลังเชื่อมต่อไปยังเครือข่าย RustDesk..."), + ("Enable Service", "เปิดใช้การงานเซอร์วิส"), + ("Start Service", "เริ่มต้นใช้งานเซอร์วิส"), + ("Service is running", "เซอร์วิสกำลังทำงาน"), + ("Service is not running", "เซอร์วิสไม่ทำงาน"), + ("not_ready_status", "ไม่พร้อมใช้งาน กรุณาตรวจสอบการเชื่อมต่ออินเทอร์เน็ตของคุณ"), + ("Control Remote Desktop", "การควบคุมเดสก์ท็อปปลายทาง"), + ("Transfer File", "การถ่ายโอนไฟล์"), + ("Connect", "เชื่อมต่อ"), + ("Recent Sessions", "เซสชันล่าสุด"), + ("Address Book", "สมุดรายชื่อ"), + ("Confirmation", "การยืนยัน"), + ("TCP Tunneling", "อุโมงค์การเชื่อมต่อ TCP"), + ("Remove", "ลบ"), + ("Refresh random password", "รีเฟรชรหัสผ่านใหม่แบบสุ่ม"), + ("Set your own password", "ตั้งรหัสผ่านของคุณเอง"), + ("Enable Keyboard/Mouse", "เปิดการใช้งาน คีย์บอร์ด/เมาส์"), + ("Enable Clipboard", "เปิดการใช้งาน คลิปบอร์ด"), + ("Enable File Transfer", "เปิดการใช้งาน การถ่ายโอนไฟล์"), + ("Enable TCP Tunneling", "เปิดการใช้งาน อุโมงค์การเชื่อมต่อ TCP"), + ("IP Whitelisting", "IP ไวท์ลิสต์"), + ("ID/Relay Server", "เซิร์ฟเวอร์ ID/Relay"), + ("Import Server Config", "นำเข้าการตั้งค่าเซิร์ฟเวอร์"), + ("Export Server Config", "ส่งออกการตั้งค่าเซิร์ฟเวอร์"), + ("Import server configuration successfully", "นำเข้าการตั้งค่าเซิร์ฟเวอร์เสร็จสมบูรณ์"), + ("Export server configuration successfully", "ส่งออกการตั้งค่าเซิร์ฟเวอร์เสร็จสมบูรณ์"), + ("Invalid server configuration", "การตั้งค่าของเซิร์ฟเวอร์ไม่ถูกต้อง"), + ("Clipboard is empty", "คลิปบอร์ดว่างเปล่า"), + ("Stop service", "หยุดการใช้งานเซอร์วิส"), + ("Change ID", "เปลี่ยน ID"), + ("Website", "เว็บไซต์"), + ("About", "เกี่ยวกับ"), + ("Slogan_tip", "ทำด้วยใจ ในโลกใบนี้ที่ยุ่งเหยิง!"), + ("Privacy Statement", "คำแถลงเกี่ยวกับความเป็นส่วนตัว"), + ("Mute", "ปิดเสียง"), + ("Audio Input", "ออดิโออินพุท"), + ("Enhancements", "การปรับปรุง"), + ("Hardware Codec", "ฮาร์ดแวร์ codec"), + ("Adaptive Bitrate", "บิทเรทผันแปร"), + ("ID Server", "เซิร์ฟเวอร์ ID"), + ("Relay Server", "เซิร์ฟเวอร์ Relay"), + ("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", "ไม่พร้อมใช้งาน"), + ("Too frequent", "ดำเนินการถี่เกินไป"), + ("Cancel", "ยกเลิก"), + ("Skip", "ข้าม"), + ("Close", "ปิด"), + ("Retry", "ลองใหม่อีกครั้ง"), + ("OK", "ตกลง"), + ("Password Required", "ต้องใช้รหัสผ่าน"), + ("Please enter your password", "กรุณาใส่รหัสผ่านของคุณ"), + ("Remember password", "จดจำรหัสผ่าน"), + ("Wrong Password", "รหัสผ่านไม่ถูกต้อง"), + ("Do you want to enter again?", "ต้องการใส่ข้อมูลอีกครั้งหรือไม่?"), + ("Connection Error", "การเชื่อมต่อผิดพลาด"), + ("Error", "ข้อผิดพลาด"), + ("Reset by the peer", "รีเซ็ตโดยอีกฝั่ง"), + ("Connecting...", "กำลังเชื่อมต่อ..."), + ("Connection in progress. Please wait.", "กำลังดำเนินการเชื่อมต่อ กรุณารอซักครู่"), + ("Please try 1 minute later", "กรุณาลองใหม่อีกครั้งใน 1 นาที"), + ("Login Error", "การเข้าสู่ระบบผิดพลาด"), + ("Successful", "สำเร็จ"), + ("Connected, waiting for image...", "เชื่อมต่อสำเร็จ กำลังรับข้อมูลภาพ..."), + ("Name", "ชื่อ"), + ("Type", "ประเภท"), + ("Modified", "แก้ไขล่าสุด"), + ("Size", "ขนาด"), + ("Show Hidden Files", "แสดงไฟล์ที่ถูกซ่อน"), + ("Receive", "รับ"), + ("Send", "ส่ง"), + ("Refresh File", "รีเฟรชไฟล์"), + ("Local", "ต้นทาง"), + ("Remote", "ปลายทาง"), + ("Remote Computer", "คอมพิวเตอร์ปลายทาง"), + ("Local Computer", "คอมพิวเตอร์ต้นทาง"), + ("Confirm Delete", "ยืนยันการลบ"), + ("Delete", "ลบ"), + ("Properties", "ข้อมูล"), + ("Multi Select", "เลือกหลายรายการ"), + ("Select All", "เลือกทั้งหมด"), + ("Unselect All", "ยกเลิกการเลือกทั้งหมด"), + ("Empty Directory", "ไดเรกทอรีว่างเปล่า"), + ("Not an empty directory", "ไม่ใช่ไดเรกทอรีว่างเปล่า"), + ("Are you sure you want to delete this file?", "คุณแน่ใจหรือไม่ที่จะลบไฟล์นี้?"), + ("Are you sure you want to delete this empty directory?", "คุณแน่ใจหรือไม่ที่จะลบไดเรอทอรีว่างเปล่านี้?"), + ("Are you sure you want to delete the file of this directory?", "คุณแน่ใจหรือไม่ที่จะลบไฟล์ของไดเรกทอรีนี้?"), + ("Do this for all conflicts", "ดำเนินการแบบเดียวกันสำหรับรายการทั้งหมด"), + ("This is irreversible!", "การดำเนินการนี้ไม่สามารถย้อนกลับได้!"), + ("Deleting", "กำลังลบ"), + ("files", "ไฟล์"), + ("Waiting", "กำลังรอ"), + ("Finished", "เสร็จแล้ว"), + ("Speed", "ความเร็ว"), + ("Custom Image Quality", "คุณภาพของภาพแบบกำหนดเอง"), + ("Privacy mode", "โหมดความเป็นส่วนตัว"), + ("Block user input", "บล็อคอินพุทจากผู้ใช้งาน"), + ("Unblock user input", "ยกเลิกการบล็อคอินพุทจากผู้ใช้งาน"), + ("Adjust Window", "ปรับขนาดหน้าต่าง"), + ("Original", "ต้นฉบับ"), + ("Shrink", "ย่อ"), + ("Stretch", "ยืด"), + ("Scrollbar", "แถบเลื่อน"), + ("ScrollAuto", "เลื่อนอัตโนมัติ"), + ("Good image quality", "ภาพคุณภาพดี"), + ("Balanced", "สมดุล"), + ("Optimize reaction time", "เน้นการตอบสนอง"), + ("Custom", "กำหนดเอง"), + ("Show remote cursor", "แสดงเคอร์เซอร์ปลายทาง"), + ("Show quality monitor", "แสดงคุณภาพหน้าจอ"), + ("Disable clipboard", "ปิดการใช้งานคลิปบอร์ด"), + ("Lock after session end", "ล็อคหลังจากจบเซสชัน"), + ("Insert", "แทรก"), + ("Insert Lock", "แทรกล็อค"), + ("Refresh", "รีเฟรช"), + ("ID does not exist", "ไม่พอข้อมูล ID"), + ("Failed to connect to rendezvous server", "การเชื่อมต่อไปยังเซิร์ฟเวอร์นัดพบล้มเหลว"), + ("Please try later", "กรุณาลองใหม่ในภายหลัง"), + ("Remote desktop is offline", "เดสก์ท็อปปลายทางออฟไลน์"), + ("Key mismatch", "คีย์ไม่ถูกต้อง"), + ("Timeout", "หมดเวลา"), + ("Failed to connect to relay server", "การเชื่อมต่อไปยังเซิร์ฟเวอร์รีเลย์ล้มเหลว"), + ("Failed to connect via rendezvous server", "การเชื่อมต่อผ่านเซิร์ฟเวอร์นัดพบล้มเหลว"), + ("Failed to connect via relay server", "การเชื่อมต่อผ่านเซิร์ฟเวอร์รีเลย์ล้มเหลว"), + ("Failed to make direct connection to remote desktop", "การเชื่อมต่อตรงไปยังเดสก์ท็อปปลายทางล้มเหลว"), + ("Set Password", "ตั้งรหัสผ่าน"), + ("OS Password", "รหัสผ่านระบบปฏิบัติการ"), + ("install_tip", "เนื่องด้วยข้อจำกัดของการใช้งาน UAC ทำให้ RustDesk ไม่สามารถทำงานได้ปกติในฝั่งปลายทางในบางครั้ง เพื่อหลีกเลี่ยงข้อจำกัดของ UAC กรุณากดปุ่มด้านล่างเพื่อติดตั้ง RustDesk ไปยังระบบของคุณ"), + ("Click to upgrade", "คลิกเพื่ออัปเกรด"), + ("Click to download", "คลิกเพื่อดาวน์โหลด"), + ("Click to update", "คลิกเพื่ออัปเดต"), + ("Configure", "ปรับแต่งค่า"), + ("config_acc", "เพื่อที่จะควบคุมเดสก์ท็อปปลายทางของคุณ คุณจำเป็นจะต้องอนุญาตสิทธิ์ \"การเข้าถึง\" ให้แก่ RustDesk"), + ("config_screen", "เพื่อที่จะควบคุมเดสก์ท็อปปลายทางของคุณ คุณจำเป็นจะต้องอนุญาตสิทธิ์ \"การบันทึกภาพหน้าจอ\" ให้แก่ RustDesk"), + ("Installing ...", "กำลังติดตั้ง ..."), + ("Install", "ติดตั้ง"), + ("Installation", "การติดตั้ง"), + ("Installation Path", "ตำแหน่งที่ติดตั้ง"), + ("Create start menu shortcuts", "สร้างทางลัดไปยัง Start Menu"), + ("Create desktop icon", "สร้างไอคอนบนเดสก์ท็อป"), + ("agreement_tip", "ในการเริ่มต้นการติดตั้ง ถือว่าคุณได้ยอมรับข้อตกลงใบอนุญาตแล้ว"), + ("Accept and Install", "ยอมรับและติดตั้ง"), + ("End-user license agreement", "ข้อตกลงใบอนุญาตผู้ใช้งาน"), + ("Generating ...", "กำลังสร้าง ..."), + ("Your installation is lower version.", "การติดตั้งของคุณเป็นเวอร์ชั่นที่ต่ำกว่า"), + ("not_close_tcp_tip", "อย่าปิดหน้าต่างนี้ในขณะที่คุณกำลังใช้งานอุโมงค์การเชื่อมต่อ"), + ("Listening ...", "กำลังรอรับข้อมูล ..."), + ("Remote Host", "โฮสต์ปลายทาง"), + ("Remote Port", "พอร์ทปลายทาง"), + ("Action", "การดำเนินการ"), + ("Add", "เพิ่ม"), + ("Local Port", "พอร์ทต้นทาง"), + ("Local Address", "ที่อยู่ต้นทาง"), + ("Change Local Port", "เปลี่ยนพอร์ทต้นทาง"), + ("setup_server_tip", "เพื่อการเชื่อมต่อที่เร็วขึ้น กรุณาเซ็ตอัปเซิร์ฟเวอร์ของคุณเอง"), + ("Too short, at least 6 characters.", "สั้นเกินไป ต้องไม่ต่ำกว่า 6 ตัวอักษร"), + ("The confirmation is not identical.", "การยืนยันข้อมูลไม่ถูกต้อง"), + ("Permissions", "สิทธิ์การใช้งาน"), + ("Accept", "ยอมรับ"), + ("Dismiss", "ปิด"), + ("Disconnect", "ยกเลิกการเชื่อมต่อ"), + ("Allow using keyboard and mouse", "อนุญาตให้ใช้งานคีย์บอร์ดและเมาส์"), + ("Allow using clipboard", "อนุญาตให้ใช้คลิปบอร์ด"), + ("Allow hearing sound", "อนุญาตให้ได้ยินเสียง"), + ("Allow file copy and paste", "อนุญาตให้มีการคัดลอกและวางไฟล์"), + ("Connected", "เชื่อมต่อแล้ว"), + ("Direct and encrypted connection", "การเชื่อมต่อตรงที่มีการเข้ารหัส"), + ("Relayed and encrypted connection", "การเชื่อมต่อแบบรีเลย์ที่มีการเข้ารหัส"), + ("Direct and unencrypted connection", "การเชื่อมต่อตรงที่ไม่มีการเข้ารหัส"), + ("Relayed and unencrypted connection", "การเชื่อมต่อแบบรีเลย์ที่ไม่มีการเข้ารหัส"), + ("Enter Remote ID", "กรอก ID ปลายทาง"), + ("Enter your password", "กรอกรหัสผ่าน"), + ("Logging in...", "กำลังเข้าสู่ระบบ..."), + ("Enable RDP session sharing", "เปิดการใช้งานการแชร์เซสชัน RDP"), + ("Auto Login", "เข้าสู่ระบอัตโนมัติ"), + ("Enable Direct IP Access", "เปิดการใช้งาน IP ตรง"), + ("Rename", "ปลายทาง"), + ("Space", "พื้นที่ว่าง"), + ("Create Desktop Shortcut", "สร้างทางลัดบนเดสก์ท็อป"), + ("Change Path", "เปลี่ยนตำแหน่ง"), + ("Create Folder", "สร้างโฟลเดอร์"), + ("Please enter the folder name", "กรุณาใส่ชื่อโฟลเดอร์"), + ("Fix it", "แก้ไข"), + ("Warning", "คำเตือน"), + ("Login screen using Wayland is not supported", "หน้าจอการเข้าสู่ระบบโดยใช้ Wayland ยังไม่ถูกรองรับ"), + ("Reboot required", "จำเป็นต้องเริ่มต้นระบบใหม่"), + ("Unsupported display server ", "เซิร์ฟเวอร์การแสดงผลที่ไม่รองรับ"), + ("x11 expected", "ต้องใช้งาน x11"), + ("Port", "พอร์ท"), + ("Settings", "ตั้งค่า"), + ("Username", "ชื่อผู้ใช้งาน"), + ("Invalid port", "พอร์ทไม่ถูกต้อง"), + ("Closed manually by the peer", "ถูกปิดโดยอีกฝั่งการการเชื่อมต่อ"), + ("Enable remote configuration modification", "เปิดการใช้งานการแก้ไขการตั้งค่าปลายทาง"), + ("Run without install", "ใช้งานโดยไม่ต้องติดตั้ง"), + ("Always connected via relay", "เชื่อมต่อผ่านรีเลย์เสมอ"), + ("Always connect via relay", "เชื่อมต่อผ่านรีเลย์เสมอ"), + ("whitelist_tip", "อนุญาตเฉพาะการเชื่อมต่อจาก IP ที่ไวท์ลิสต์"), + ("Login", "เข้าสู่ระบบ"), + ("Remember me", ""), + ("Logout", "ออกจากระบบ"), + ("Tags", "แท็ก"), + ("Search ID", "ค้นหา ID"), + ("Current Wayland display server is not supported", "เซิร์ฟเวอร์การแสดงผล Wayland ปัจจุบันไม่รองรับ"), + ("whitelist_sep", "คั่นโดยเครื่องหมาย comma semicolon เว้นวรรค หรือ ขึ้นบรรทัดใหม่"), + ("Add ID", "เพิ่ม ID"), + ("Add Tag", "เพิ่มแท็ก"), + ("Unselect all tags", "ยกเลิกการเลือกแท็กทั้งหมด"), + ("Network error", "ข้อผิดพลาดของเครือข่าย"), + ("Username missed", "ไม่พบข้อมูลผู้ใช้งาน"), + ("Password missed", "ไม่พบรหัสผ่าน"), + ("Wrong credentials", "ข้อมูลสำหรับเข้าสู่ระบบไม่ถูกต้อง"), + ("Edit Tag", "แก้ไขแท็ก"), + ("Unremember Password", "ยกเลิกการจดจำรหัสผ่าน"), + ("Favorites", "รายการโปรด"), + ("Add to Favorites", "เพิ่มไปยังรายการโปรด"), + ("Remove from Favorites", "ลบออกจากรายการโปรด"), + ("Empty", "ว่างเปล่า"), + ("Invalid folder name", "ชื่อโฟลเดอร์ไม่ถูกต้อง"), + ("Socks5 Proxy", "พรอกซี Socks5"), + ("Hostname", "ชื่อโฮสต์"), + ("Discovered", "ค้นพบ"), + ("install_daemon_tip", "หากต้องการใช้งานขณะระบบเริ่มต้น คุณจำเป็นจะต้องติดตั้งเซอร์วิส"), + ("Remote ID", "ID ปลายทาง"), + ("Paste", "วาง"), + ("Paste here?", "วางที่นี่หรือไม่?"), + ("Are you sure to close the connection?", "คุณแน่ใจหรือไม่ที่จะปิดการเชื่อมต่อ?"), + ("Download new version", "ดาวน์โหลดเวอร์ชั่นใหม่"), + ("Touch mode", "โหมดการสัมผัส"), + ("Mouse mode", "โหมดการใช้เมาส์"), + ("One-Finger Tap", "แตะนิ้วเดียว"), + ("Left Mouse", "เมาส์ซ้าย"), + ("One-Long Tap", "แตะยาวหนึ่งครั้ง"), + ("Two-Finger Tap", "แตะสองนิ้ว"), + ("Right Mouse", "เมาส์ขวา"), + ("One-Finger Move", "ลากนิ้วเดียว"), + ("Double Tap & Move", "แตะเบิ้ลและลาก"), + ("Mouse Drag", "ลากเมาส์"), + ("Three-Finger vertically", "สามนิ้วแนวตั้ง"), + ("Mouse Wheel", "ลูกลิ้งเมาส์"), + ("Two-Finger Move", "ลากสองนิ้ว"), + ("Canvas Move", "ลากแคนวาส"), + ("Pinch to Zoom", "ถ่างเพื่อขยาย"), + ("Canvas Zoom", "ขยายแคนวาส"), + ("Reset canvas", "รีเซ็ตแคนวาส"), + ("No permission of file transfer", "ไม่มีสิทธิ์ในการถ่ายโอนไฟล์"), + ("Note", "บันทึกข้อความ"), + ("Connection", "การเชื่อมต่อ"), + ("Share Screen", "แชร์หน้าจอ"), + ("CLOSE", "ปิด"), + ("OPEN", "เปิด"), + ("Chat", "แชท"), + ("Total", "รวม"), + ("items", "รายการ"), + ("Selected", "ถูกเลือก"), + ("Screen Capture", "แคปเจอร์หน้าจอ"), + ("Input Control", "ควบคุมอินพุท"), + ("Audio Capture", "แคปเจอร์เสียง"), + ("File Connection", "การเชื่อมต่อไฟล์"), + ("Screen Connection", "การเชื่อมต่อหน้าจอ"), + ("Do you accept?", "ยอมรับหรือไม่?"), + ("Open System Setting", "เปิดการตั้งค่าระบบ"), + ("How to get Android input permission?", "เปิดสิทธิ์การใช้งานอินพุทของแอนดรอยด์ได้อย่างไร?"), + ("android_input_permission_tip1", "ในการที่จะอนุญาตให้เครื่องปลายทางควบคุมอุปกรณ์แอนดรอยด์ของคุณโดยใช้เมาส์หรือการสัมผัส คุณจำเป็นจะต้องอนุญาตสิทธิ์ \"การเข้าถึง\" ให้แก่เซอร์วิสของ RustDesk"), + ("android_input_permission_tip2", "กรุณาไปยังหน้าตั้งค่าถัดไป ค้นหาและเข้าไปยัง [เซอร์วิสที่ถูกติดตั้ง] และเปิดการใช้งานเซอร์วิส [อินพุท RustDesk]"), + ("android_new_connection_tip", "ได้รับคำขอควบคุมใหม่ที่ต้องการควบคุมอุปกรณ์ของคุณ"), + ("android_service_will_start_tip", "การเปิดการใช้งาน \"การบันทึกหน้าจอ\" จะเป็นการเริ่มต้นการทำงานของเซอร์วิสโดยอัตโนมัติ ที่จะอนุญาตให้อุปกรณ์อื่นๆ ส่งคำขอเข้าถึงมายังอุปกรณ์ของคุณได้"), + ("android_stop_service_tip", "การปิดการใช้งานเซอร์วิสจะปิดการเชื่อมต่อทั้งหมดโดยอัตโนมัติ"), + ("android_version_audio_tip", "เวอร์ชั่นแอนดรอยด์ปัจจุบันของคุณไม่รองรับการบันทึกข้อมูลเสียง กรุณาอัปเกรดเป็นแอนดรอยด์เวอร์ชั่น 10 หรือสูงกว่า"), + ("android_start_service_tip", "แตะ [เริ่มต้นใช้งานเซอร์วิส] หรือเปิดสิทธิ์ [การบันทึกหน้าจอ] เพื่อเริ่มเซอร์วิสการแชร์หน้าจอ"), + ("Account", "บัญชี"), + ("Overwrite", "เขียนทับ"), + ("This file exists, skip or overwrite this file?", "พบไฟล์ที่มีอยู่แล้ว ต้องการเขียนทับหรือไม่?"), + ("Quit", "ออก"), + ("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"), + ("Help", "ช่วยเหลือ"), + ("Failed", "ล้มเหลว"), + ("Succeeded", "สำเร็จ"), + ("Someone turns on privacy mode, exit", "มีใครบางคนเปิดใช้งานโหมดความเป็นส่วนตัว กำลังออก"), + ("Unsupported", "ไม่รองรับ"), + ("Peer denied", "ถูกปฏิเสธโดยอีกฝั่ง"), + ("Please install plugins", "กรุณาติดตั้งปลั๊กอิน"), + ("Peer exit", "อีกฝั่งออก"), + ("Failed to turn off", "การปิดล้มเหลว"), + ("Turned off", "ปิด"), + ("In privacy mode", "อยู่ในโหมดความเป็นส่วนตัว"), + ("Out privacy mode", "อยู่นอกโหมดความเป็นส่วนตัว"), + ("Language", "ภาษา"), + ("Keep RustDesk background service", "คงสถานะการทำงานเบื้องหลังของเซอร์วิส RustDesk"), + ("Ignore Battery Optimizations", "เพิกเฉยการตั้งค่าการใช้งาน Battery Optimization"), + ("android_open_battery_optimizations_tip", "หากคุณต้องการปิดการใช้งานฟีเจอร์นี้ กรุณาไปยังหน้าตั้งค่าในแอปพลิเคชัน RustDesk ค้นหาหัวข้อ [Battery] และยกเลิกการเลือกรายการ [Unrestricted]"), + ("Connection not allowed", "การเชื่อมต่อไม่อนุญาต"), + ("Legacy mode", ""), + ("Map mode", ""), + ("Translate mode", ""), + ("Use permanent password", "ใช้รหัสผ่านถาวร"), + ("Use both passwords", "ใช้รหัสผ่านทั้งสองแบบ"), + ("Set permanent password", "ตั้งค่ารหัสผ่านถาวร"), + ("Enable Remote Restart", "เปิดการใช้งานการรีสตาร์ทระบบทางไกล"), + ("Allow remote restart", "อนุญาตการรีสตาร์ทระบบทางไกล"), + ("Restart Remote Device", "รีสตาร์ทอุปกรณ์ปลายทาง"), + ("Are you sure you want to restart", "คุณแน่ใจหรือไม่ที่จะรีสตาร์ท"), + ("Restarting Remote Device", "กำลังรีสตาร์ทระบบปลายทาง"), + ("remote_restarting_tip", "ระบบปลายทางกำลังรีสตาร์ท กรุณาปิดกล่องข้อความนี้และดำเนินการเขื่อมต่อใหม่อีกครั้งด้วยรหัสผ่านถาวรหลังจากผ่านไปซักครู่"), + ("Copied", "คัดลอกแล้ว"), + ("Exit Fullscreen", "ออกจากเต็มหน้าจอ"), + ("Fullscreen", "เต็มหน้าจอ"), + ("Mobile Actions", "การดำเนินการบนมือถือ"), + ("Select Monitor", "เลือกหน้าจอ"), + ("Control Actions", "การดำเนินการควบคุม"), + ("Display Settings", "การตั้งค่าแสดงผล"), + ("Ratio", "อัตราส่วน"), + ("Image Quality", "คุณภาพภาพ"), + ("Scroll Style", "ลักษณะการเลื่อน"), + ("Show Menubar", "แสดงแถบเมนู"), + ("Hide Menubar", "ซ่อนแถบเมนู"), + ("Direct Connection", "การเชื่อมต่อตรง"), + ("Relay Connection", "การเชื่อมต่อแบบรีเลย์"), + ("Secure Connection", "การเชื่อมต่อที่ปลอดภัย"), + ("Insecure Connection", "การเชื่อมต่อที่ไม่ปลอดภัย"), + ("Scale original", "ขนาดเดิม"), + ("Scale adaptive", "ขนาดยืดหยุ่น"), + ("General", "ทั่วไป"), + ("Security", "ความปลอดภัย"), + ("Theme", "ธีม"), + ("Dark Theme", "ธีมมืด"), + ("Dark", "มืด"), + ("Light", "สว่าง"), + ("Follow System", "ตามระบบ"), + ("Enable hardware codec", "เปิดการใช้งานฮาร์ดแวร์ codec"), + ("Unlock Security Settings", "ปลดล็อคการตั้งค่าความปลอดภัย"), + ("Enable Audio", "เปิดการใช้งานเสียง"), + ("Unlock Network Settings", "ปลดล็อคการตั้งค่าเครือข่าย"), + ("Server", "เซิร์ฟเวอร์"), + ("Direct IP Access", "การเข้าถึง IP ตรง"), + ("Proxy", "พรอกซี"), + ("Apply", "นำไปใช้"), + ("Disconnect all devices?", "ยกเลิกการเชื่อมต่ออุปกรณ์ทั้งหมด?"), + ("Clear", "ล้างข้อมูล"), + ("Audio Input Device", "อุปกรณ์รับอินพุทข้อมูลเสียง"), + ("Deny remote access", "ปฏิเสธการเชื่อมต่อ"), + ("Use IP Whitelisting", "ใช้งาน IP ไวท์ลิสต์"), + ("Network", "เครือข่าย"), + ("Enable RDP", "เปิดการใช้งาน RDP"), + ("Pin menubar", "ปักหมุดแถบเมนู"), + ("Unpin menubar", "ยกเลิกการปักหมุดแถบเมนู"), + ("Recording", "การบันทึก"), + ("Directory", "ไดเรกทอรี่"), + ("Automatically record incoming sessions", "บันทึกเซสชันขาเข้าโดยอัตโนมัติ"), + ("Change", "เปลี่ยน"), + ("Start session recording", "เริ่มต้นการบันทึกเซสชัน"), + ("Stop session recording", "หยุดการบันทึกเซสซัน"), + ("Enable Recording Session", "เปิดใช้งานการบันทึกเซสชัน"), + ("Allow recording session", "อนุญาตการบันทึกเซสชัน"), + ("Enable LAN Discovery", "เปิดการใช้งานการค้นหาในวง LAN"), + ("Deny LAN Discovery", "ปฏิเสธการใช้งานการค้นหาในวง LAN"), + ("Write a message", "เขียนข้อความ"), + ("Prompt", ""), + ("Please wait for confirmation of UAC...", "กรุณารอการยืนยันจาก UAC..."), + ("elevated_foreground_window_tip", "หน้าต่างปัจจุบันของเครื่องปลายทางต้องการสิทธิ์การใช้งานที่สูงขึ้นสำหรับการทำงาน ดังนั้นเมาส์และคีย์บอร์ดจะไม่สามารถใช้งานได้ชั่วคราว คุณสามารถขอผู้ใช้งานปลายทางให้ย่อหน้าต่าง หรือคลิกปุ่มให้สิทธิ์การใช้งานในหน้าต่างการจัดการการเชื่อมต่อ เพื่อหลีกเลี่ยงปัญหานี้เราแนะนำให้ดำเนินการติดตั้งซอฟท์แวร์ในเครื่องปลายทาง"), + ("Disconnected", "ยกเลิกการเชื่อมต่อ"), + ("Other", "อื่นๆ"), + ("Confirm before closing multiple tabs", "ยืนยันการปิดหลายแท็บ"), + ("Keyboard Settings", "การตั้งค่าคีย์บอร์ด"), + ("Full Access", "การเข้าถึงทั้งหมด"), + ("Screen Share", "การแชร์จอ"), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland ต้องการ Ubuntu เวอร์ชั่น 21.04 หรือสูงกว่า"), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland ต้องการลินุกซ์เวอร์ชันที่สูงกว่านี้ กรุณาเปลี่ยนไปใช้เดสก์ท็อป X11 หรือเปลี่ยนระบบปฏิบัติการของคุณ"), + ("JumpLink", "View"), + ("Please Select the screen to be shared(Operate on the peer side).", "กรุณาเลือกหน้าจอที่ต้องการแชร์ (ใช้งานในอีกฝั่งของการเชื่อมต่อ)"), + ("Show RustDesk", "แสดง RustDesk"), + ("This PC", ""), + ("or", "หรือ"), + ("Continue with", "ทำต่อด้วย"), + ("Elevate", "ยกระดับ"), + ("Zoom cursor", "ขยายเคอร์เซอร์"), + ("Accept sessions via password", "ยอมรับการเชื่อมต่อด้วยรหัสผ่าน"), + ("Accept sessions via click", "ยอมรับการเชื่อมต่อด้วยการคลิก"), + ("Accept sessions via both", "ยอมรับการเชื่อมต่อด้วยทั้งสองวิธิ"), + ("Please wait for the remote side to accept your session request...", "กรุณารอให้อีกฝั่งยอมรับการเชื่อมต่อของคุณ..."), + ("One-time Password", "รหัสผ่านครั้งเดียว"), + ("Use one-time password", "ใช้รหัสผ่านครั้งเดียว"), + ("One-time password length", "ความยาวรหัสผ่านครั้งเดียว"), + ("Request access to your device", "คำขอการเข้าถึงอุปกรณ์ของคุณ"), + ("Hide connection management window", "ซ่อนหน้าต่างการจัดการการเชื่อมต่อ"), + ("hide_cm_tip", "อนุญาตการซ่อนก็ต่อเมื่อยอมรับการเชื่อมต่อด้วยรหัสผ่าน และต้องเป็นรหัสผ่านถาวรเท่านั้น"), + ("wayland_experiment_tip", "การสนับสนุน Wayland ยังอยู่ในขั้นตอนการทดลอง กรุณาใช้ X11 หากคุณต้องการใช้งานการเข้าถึงแบบไม่มีผู้ดูแล"), + ("Right click to select tabs", "คลิกขวาเพื่อเลือกแท็บ"), + ("Skipped", "ข้าม"), + ("Add to Address Book", "เพิ่มไปยังสมุดรายชื่อ"), + ("Group", "กลุ่ม"), + ("Search", "ค้นหา"), + ("Closed manually by the web console", "ถูกปิดโดยเว็บคอนโซล"), + ("Local keyboard type", "ประเภทคีย์บอร์ด"), + ("Select local keyboard type", "เลือกประเภทคีย์บอร์ด"), + ].iter().cloned().collect(); +} diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 60bc9dda1..be95d2b7a 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -210,6 +210,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Always connect via relay"), ("whitelist_tip", "Bu masaüstüne yalnızca yetkili IP adresleri bağlanabilir"), ("Login", "Giriş yap"), + ("Remember me", ""), ("Logout", "Çıkış yap"), ("Tags", "Etiketler"), ("Search ID", "ID Arama"), diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 0e08fa508..96e806d06 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -210,6 +210,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "一律透過轉送連線"), ("whitelist_tip", "只有白名單中的 IP 可以存取"), ("Login", "登入"), + ("Remember me", ""), ("Logout", "登出"), ("Tags", "標籤"), ("Search ID", "搜尋 ID"), diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 343b62b4f..53fa879f5 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -210,6 +210,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Завжди підключатися через ретрансляційний сервер"), ("whitelist_tip", "Тільки IP-адреси з білого списку можуть отримати доступ до мене"), ("Login", "Увійти"), + ("Remember me", ""), ("Logout", "Вийти"), ("Tags", "Ключові слова"), ("Search ID", "Пошук за ID"), diff --git a/src/lang/vn.rs b/src/lang/vn.rs index a2fc416f2..bb638c072 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -210,6 +210,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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"), + ("Remember me", ""), ("Logout", "Đăng xuất"), ("Tags", "Tags"), ("Search ID", "Tìm ID"), From 6d1ca1e69ede54de36914bf3fd8923431acdab3c Mon Sep 17 00:00:00 2001 From: "Miguel F. G" <116861809+flusheDData@users.noreply.github.com> Date: Sun, 8 Jan 2023 20:16:03 +0100 Subject: [PATCH 1377/2015] Update es.rs New terms added. --- src/lang/es.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lang/es.rs b/src/lang/es.rs index 56453bd8c..9252f8f14 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -39,7 +39,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Cambiar ID"), ("Website", "Sitio web"), ("About", "Acerca de"), - ("Slogan_tip", ""), + ("Slogan_tip", "Hecho con corazón en este mundo caótico!"), ("Privacy Statement", ""), ("Mute", "Silenciar"), ("Audio Input", "Entrada de audio"), @@ -405,8 +405,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the web console", "Cerrado manualmente por la consola web"), ("Local keyboard type", "Tipo de teclado local"), ("Select local keyboard type", "Seleccionar tipo de teclado local"), - ("software_render_tip", ""), - ("Always use software rendering", ""), - ("config_input", ""), + ("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\"."), ].iter().cloned().collect(); } From 36a2765cfa7fe4ef58e7bed8c14fc91132642c55 Mon Sep 17 00:00:00 2001 From: Mr-Update <37781396+Mr-Update@users.noreply.github.com> Date: Sun, 8 Jan 2023 22:03:47 +0100 Subject: [PATCH 1378/2015] Update de.rs --- src/lang/de.rs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/lang/de.rs b/src/lang/de.rs index 6ee9fdaf6..29081da63 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -9,7 +9,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Established", "Verbunden"), ("connecting_status", "Verbinden mit dem RustDesk-Netzwerk..."), ("Enable Service", "Vermittlungsdienst aktivieren"), - ("Start Service", "Starte Vermittlungsdienst"), + ("Start Service", "Vermittlungsdienst starten"), ("Service is running", "Vermittlungsdienst aktiv"), ("Service is not running", "Vermittlungsdienst deaktiviert"), ("not_ready_status", "Nicht bereit. Bitte überprüfen Sie Ihre Netzwerkverbindung."), @@ -35,7 +35,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Export server configuration successfully", "Serverkonfiguration erfolgreich exportiert"), ("Invalid server configuration", "Ungültige Serverkonfiguration"), ("Clipboard is empty", "Zwischenablage ist leer"), - ("Stop service", "Vermittlungsdienst deaktivieren"), + ("Stop service", "Vermittlungsdienst stoppen"), ("Change ID", "ID ändern"), ("Website", "Webseite"), ("About", "Über"), @@ -165,7 +165,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Local Port", "Lokaler Port"), ("Local Address", "Lokale Adresse"), ("Change Local Port", "Lokalen Port ändern"), - ("setup_server_tip", "für eine schnellere Verbindung richten Sie bitte Ihren eigenen Verbindungsserver ein."), + ("setup_server_tip", "für eine schnellere Verbindung richten Sie bitte Ihren eigenen Server ein."), ("Too short, at least 6 characters.", "Zu kurz, mindestens 6 Zeichen."), ("The confirmation is not identical.", "Die Passwörter stimmen nicht überein."), ("Permissions", "Berechtigungen"), @@ -239,7 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Are you sure to close the connection?", "Möchten Sie diese Verbindung wirklich trennen?"), ("Download new version", "Neue Version herunterladen"), ("Touch mode", "Touch-Modus"), - ("Mouse mode", "Maus-Modus"), + ("Mouse mode", "Mausmodus"), ("One-Finger Tap", "1-Finger-Tipp"), ("Left Mouse", "Linksklick"), ("One-Long Tap", "1-Finger-Halten"), @@ -255,8 +255,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Pinch to Zoom", "2-Finger-Zoom"), ("Canvas Zoom", "Sichtfeld-Zoom"), ("Reset canvas", "Sichtfeld zurücksetzen"), - ("No permission of file transfer", "Keine Berechtigung für den Dateizugriff"), - ("Note", "Anmerkung"), + ("No permission of file transfer", "Keine Berechtigung für die Dateiübertragung"), + ("Note", "Hinweis"), ("Connection", "Verbindung"), ("Share Screen", "Bildschirm freigeben"), ("CLOSE", "DEAKTIV."), @@ -273,7 +273,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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 Remote-Gerät Ihr Android-Gerät steuern kann, müssen Sie RustDesk erlauben, den Dienst \"Barrierefreiheit\" zu verwenden."), + ("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_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."), @@ -290,9 +290,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Succeeded", "Erfolgreich"), ("Someone turns on privacy mode, exit", "Jemand hat den Datenschutzmodus aktiviert, beende..."), ("Unsupported", "Nicht unterstützt"), - ("Peer denied", "Die Gegenstelle hat die Verbindung abgelehnt"), + ("Peer denied", "Die Gegenstelle hat die Verbindung abgelehnt."), ("Please install plugins", "Bitte installieren Sie Plugins"), - ("Peer exit", "Die Gegenstelle hat die Verbindung getrennt"), + ("Peer exit", "Die Gegenstelle hat die Verbindung getrennt."), ("Failed to turn off", "Ausschalten fehlgeschlagen"), ("Turned off", "Ausgeschaltet"), ("In privacy mode", "Datenschutzmodus aktivieren"), @@ -405,8 +405,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the web console", "Manuell über die Webkonsole beendet"), ("Local keyboard type", "Lokaler Tastaturtyp"), ("Select local keyboard type", "Lokalen Tastaturtyp auswählen"), - ("software_render_tip", ""), - ("Always use software rendering", ""), - ("config_input", ""), + ("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."), ].iter().cloned().collect(); } From a8536118c09718f67af9fc1d1ce9383045774dde Mon Sep 17 00:00:00 2001 From: csf Date: Mon, 9 Jan 2023 14:21:16 +0900 Subject: [PATCH 1379/2015] add verificationCodeDialog, opt loginDialog --- flutter/lib/common/widgets/login.dart | 190 ++++++++++++++++++++++---- flutter/lib/models/user_model.dart | 1 - src/lang/ca.rs | 4 + src/lang/cn.rs | 8 +- src/lang/cs.rs | 4 + src/lang/da.rs | 4 + src/lang/de.rs | 4 + src/lang/en.rs | 1 + src/lang/eo.rs | 4 + src/lang/es.rs | 4 + src/lang/fa.rs | 4 + src/lang/fr.rs | 4 + src/lang/gr.rs | 4 + src/lang/hu.rs | 4 + src/lang/id.rs | 4 + src/lang/it.rs | 4 + src/lang/ja.rs | 4 + src/lang/ko.rs | 4 + src/lang/kz.rs | 4 + src/lang/pl.rs | 4 + src/lang/pt_PT.rs | 4 + src/lang/ptbr.rs | 4 + src/lang/ru.rs | 4 + src/lang/sk.rs | 4 + src/lang/sq.rs | 4 + src/lang/sr.rs | 4 + src/lang/sv.rs | 4 + src/lang/template.rs | 4 + src/lang/th.rs | 4 + src/lang/tr.rs | 4 + src/lang/tw.rs | 4 + src/lang/ua.rs | 4 + src/lang/vn.rs | 4 + 33 files changed, 283 insertions(+), 33 deletions(-) diff --git a/flutter/lib/common/widgets/login.dart b/flutter/lib/common/widgets/login.dart index 8092dfed6..f760132af 100644 --- a/flutter/lib/common/widgets/login.dart +++ b/flutter/lib/common/widgets/login.dart @@ -324,11 +324,13 @@ class LoginWidgetUserPass extends StatelessWidget { title: '${translate("Username")}:', controller: username, autoFocus: true, + prefixIcon: Icon(Icons.account_circle_outlined), errorText: usernameMsg), DialogTextField( title: '${translate("Password")}:', obscureText: true, controller: pass, + prefixIcon: Icon(Icons.lock_outline), errorText: passMsg), Obx(() => CheckboxListTile( contentPadding: const EdgeInsets.all(0), @@ -377,6 +379,8 @@ class DialogTextField extends StatelessWidget { final bool autoFocus; final bool obscureText; final String? errorText; + final String? helperText; + final Widget? prefixIcon; final TextEditingController controller; final FocusNode focusNode = FocusNode(); @@ -385,6 +389,8 @@ class DialogTextField extends StatelessWidget { this.autoFocus = false, this.obscureText = false, this.errorText, + this.helperText, + this.prefixIcon, required this.title, required this.controller}) : super(key: key) { @@ -403,6 +409,9 @@ class DialogTextField extends StatelessWidget { decoration: InputDecoration( labelText: title, border: const OutlineInputBorder(), + prefixIcon: prefixIcon, + helperText: helperText, + helperMaxLines: 8, errorText: errorText), controller: controller, focusNode: focusNode, @@ -427,38 +436,36 @@ Future loginDialog() async { final autoLogin = true.obs; final RxString curOP = ''.obs; - return gFFI.dialogManager.show((setState, close) { - cancel() { + final res = await gFFI.dialogManager.show((setState, close) { + username.addListener(() { + if (usernameMsg != null) { + setState(() => usernameMsg = null); + } + }); + + password.addListener(() { + if (passwordMsg != null) { + setState(() => passwordMsg = null); + } + }); + + onDialogCancel() { isInProgress = false; close(false); } onLogin() async { - setState(() { - usernameMsg = null; - passwordMsg = null; - isInProgress = true; - }); - cancel() { - curOP.value = ''; - if (isInProgress) { - setState(() { - isInProgress = false; - }); - } - } - - curOP.value = 'rustdesk'; + // validate if (username.text.isEmpty) { - usernameMsg = translate('Username missed'); - cancel(); + setState(() => usernameMsg = translate('Username missed')); return; } if (password.text.isEmpty) { - passwordMsg = translate('Password missed'); - cancel(); + setState(() => passwordMsg = translate('Password missed')); return; } + curOP.value = 'rustdesk'; + setState(() => isInProgress = true); try { final resp = await gFFI.userModel.login(LoginRequest( username: username.text, @@ -471,27 +478,33 @@ Future loginDialog() async { switch (resp.type) { case HttpType.kAuthResTypeToken: if (resp.access_token != null) { - bind.mainSetLocalOption( + await bind.mainSetLocalOption( key: 'access_token', value: resp.access_token!); close(true); return; } break; case HttpType.kAuthResTypeEmailCheck: + setState(() => isInProgress = false); + final res = await verificationCodeDialog(resp.user); + if (res == true) { + close(true); + return; + } + break; + default: + passwordMsg = "Failed, bad response from server"; break; } } on RequestException catch (err) { passwordMsg = translate(err.cause); debugPrintStack(label: err.toString()); - cancel(); - return; } catch (err) { - passwordMsg = "Unknown Error"; + passwordMsg = "Unknown Error: $err"; debugPrintStack(label: err.toString()); - cancel(); - return; } - close(); + curOP.value = ''; + setState(() => isInProgress = false); } return CustomAlertDialog( @@ -538,8 +551,125 @@ Future loginDialog() async { ), ], ), - actions: [msgBoxButton(translate('Close'), cancel)], - onCancel: cancel, + actions: [msgBoxButton(translate('Close'), onDialogCancel)], + onCancel: onDialogCancel, ); }); + + if (res != null) { + // update ab and group status + await gFFI.abModel.pullAb(); + await gFFI.groupModel.pull(); + } + + return res; +} + +Future verificationCodeDialog(UserPayload? user) async { + var autoLogin = true; + var isInProgress = false; + String? errorText; + + final code = TextEditingController(); + + final res = await gFFI.dialogManager.show((setState, close) { + bool validate() { + return code.text.length >= 6; + } + + code.addListener(() { + if (errorText != null) { + setState(() => errorText = null); + } + }); + + void onVerify() async { + if (!validate()) { + setState( + () => errorText = translate('Too short, at least 6 characters.')); + return; + } + setState(() => isInProgress = true); + + try { + final resp = await gFFI.userModel.login(LoginRequest( + verificationCode: code.text, + username: user?.name, + id: await bind.mainGetMyId(), + uuid: await bind.mainGetUuid(), + autoLogin: autoLogin, + type: HttpType.kAuthReqTypeEmailCode)); + + switch (resp.type) { + case HttpType.kAuthResTypeToken: + if (resp.access_token != null) { + await bind.mainSetLocalOption( + key: 'access_token', value: resp.access_token!); + close(true); + return; + } + break; + default: + errorText = "Failed, bad response from server"; + break; + } + } on RequestException catch (err) { + errorText = translate(err.cause); + debugPrintStack(label: err.toString()); + } catch (err) { + errorText = "Unknown Error: $err"; + debugPrintStack(label: err.toString()); + } + + setState(() => isInProgress = false); + } + + return CustomAlertDialog( + title: Text(translate("Verification code")), + contentBoxConstraints: BoxConstraints(maxWidth: 300), + content: Column( + children: [ + Offstage( + offstage: user?.email == null, + child: TextField( + decoration: InputDecoration( + labelText: "Email", + prefixIcon: Icon(Icons.email), + border: InputBorder.none), + readOnly: true, + controller: TextEditingController(text: user?.email), + )), + const SizedBox(height: 8), + DialogTextField( + title: '${translate("Verification code")}:', + controller: code, + autoFocus: true, + errorText: errorText, + helperText: translate('verification_tip'), + ), + CheckboxListTile( + contentPadding: const EdgeInsets.all(0), + dense: true, + controlAffinity: ListTileControlAffinity.leading, + title: Row(children: [ + Expanded(child: Text(translate("Trust this device"))) + ]), + value: autoLogin, + onChanged: (v) { + if (v == null) return; + setState(() => autoLogin = !autoLogin); + }, + ), + Offstage( + offstage: !isInProgress, + child: const LinearProgressIndicator()), + ], + ), + actions: [ + TextButton(onPressed: close, child: Text(translate("Cancel"))), + TextButton(onPressed: onVerify, child: Text(translate("Verify"))), + ]); + }); + + return res; } diff --git a/flutter/lib/models/user_model.dart b/flutter/lib/models/user_model.dart index 79a9778b0..b0eebee53 100644 --- a/flutter/lib/models/user_model.dart +++ b/flutter/lib/models/user_model.dart @@ -127,7 +127,6 @@ class UserModel { await _parseAndUpdateUser(loginResponse.user!); } - await _updateOtherModels(); return loginResponse; } } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index ffc5396fd..82052517a 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -210,7 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Connecta sempre a través de relay"), ("whitelist_tip", ""), ("Login", "Inicia sessió"), + ("Verify", ""), ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "Sortir"), ("Tags", ""), ("Search ID", "Cerca ID"), diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 6a4feeeec..c8a6e5f57 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -210,7 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "强制走中继连接"), ("whitelist_tip", "只有白名单里的ip才能访问我"), ("Login", "登录"), + ("Verify", "验证"), ("Remember me", "记住我"), + ("Trust this device", "信任此设备"), + ("Verification code", "验证码"), + ("verification_tip", "检测到新设备登录,已向注册邮箱发送了登录验证码,输入验证码继续登录"), ("Logout", "登出"), ("Tags", "标签"), ("Search ID", "查找ID"), @@ -222,7 +226,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Network error", "网络错误"), ("Username missed", "用户名没有填写"), ("Password missed", "密码没有填写"), - ("Wrong credentials", "用户名或者密码错误"), + ("Wrong credentials", "提供的登入信息错误"), ("Edit Tag", "修改标签"), ("Unremember Password", "忘掉密码"), ("Favorites", "收藏"), @@ -274,7 +278,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Do you accept?", "是否接受?"), ("Open System Setting", "打开系统设置"), ("How to get Android input permission?", "如何获取安卓的输入权限?"), - ("android_input_permission_tip1", "為了讓遠程設備通過鼠標或者觸屏控制您的安卓設備,你需要允許RustDesk使用\"無障礙\"服務。"), + ("android_input_permission_tip1", "为了让远程设备通过鼠标或触屏控制您的安卓设备,你需要允許RustDesk使用\"无障碍\"服务。"), ("android_input_permission_tip2", "请在接下来的系统设置页面里,找到并进入 [已安装的服务] 页面,将 [RustDesk Input] 服务开启。"), ("android_new_connection_tip", "收到新的连接控制请求,对方想要控制你当前的设备。"), ("android_service_will_start_tip", "开启录屏权限将自动开启服务,允许其他设备向此设备请求建立连接。"), diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 17e2ea68b..08aa1fd58 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -210,7 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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"), + ("Verify", ""), ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "Odhlásit se"), ("Tags", "Štítky"), ("Search ID", "Hledat identifikátor"), diff --git a/src/lang/da.rs b/src/lang/da.rs index c0db3eda0..8f1f8a1ff 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -210,7 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Forbindelse via relæ-server"), ("whitelist_tip", "Kun IP'er på udgivelseslisten kan få adgang til mig"), ("Login", "Login"), + ("Verify", ""), ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "logger af"), ("Tags", "Nøgleord"), ("Search ID", "Søg ID"), diff --git a/src/lang/de.rs b/src/lang/de.rs index 28b0a51d3..e76f6b315 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -210,7 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Immer über Relay-Server verbinden"), ("whitelist_tip", "Nur IPs auf der Whitelist können zugreifen."), ("Login", "Anmelden"), + ("Verify", ""), ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "Abmelden"), ("Tags", "Schlagworte"), ("Search ID", "Suche ID"), diff --git a/src/lang/en.rs b/src/lang/en.rs index f351b575d..e9754f296 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -36,5 +36,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", "Allow hiding only if accepting sessions via password and using permanent password"), ("wayland_experiment_tip", "Wayland support is in experimental stage, please use X11 if you require unattended access."), ("Slogan_tip", "Made with heart in this chaotic world!"), + ("verification_tip", "A new device has been detected, and a verification code has been sent to the registered email address, enter the verification code to continue logging in."), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index e92d7ab31..38d3c2588 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -210,7 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Ĉiam konekti per relajso"), ("whitelist_tip", "Nur la IP en la blanka listo povas kontroli mian komputilon"), ("Login", "Konekti"), + ("Verify", ""), ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "Malkonekti"), ("Tags", "Etikedi"), ("Search ID", "Serĉi ID"), diff --git a/src/lang/es.rs b/src/lang/es.rs index 539627cb9..4932cf6e2 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -210,7 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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"), + ("Verify", ""), ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "Salir"), ("Tags", "Tags"), ("Search ID", "Buscar ID"), diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 9ca854ecb..f4c025030 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -210,7 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "برای اتصال استفاده شود Relay از"), ("whitelist_tip", "های مجاز می توانند به این دسکتاپ متصل شوند IP فقط"), ("Login", "ورود"), + ("Verify", ""), ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "خروج"), ("Tags", "برچسب ها"), ("Search ID", "جستجوی شناسه"), diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 84cd9c4fa..69f977c94 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -210,7 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Forcer la connexion relais"), ("whitelist_tip", "Seul l'IP dans la liste blanche peut accéder à mon appareil"), ("Login", "Connexion"), + ("Verify", ""), ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "Déconnexion"), ("Tags", "Étiqueter"), ("Search ID", "Rechercher un ID"), diff --git a/src/lang/gr.rs b/src/lang/gr.rs index f1ee5be08..9ea7cbdc0 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -210,7 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Σύνδεση πάντα μέσω αναμετάδοσης"), ("whitelist_tip", "Μόνο οι IP της λίστας επιτρεπόμενων έχουν πρόσβαση"), ("Login", "Σύνδεση"), + ("Verify", ""), ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "Αποσύνδεση"), ("Tags", "Ετικέτες"), ("Search ID", "Αναζήτηση ID"), diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 92b7c1ba1..1db137c43 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -210,7 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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"), + ("Verify", ""), ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "Kilépés"), ("Tags", "Tagok"), ("Search ID", "Azonosító keresése..."), diff --git a/src/lang/id.rs b/src/lang/id.rs index 2e96785c5..baf3ab144 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -210,7 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Selalu terhubung melalui relai"), ("whitelist_tip", "Hanya whitelisted IP yang dapat mengakses saya"), ("Login", "Masuk"), + ("Verify", ""), ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "Keluar"), ("Tags", "Tag"), ("Search ID", "Cari ID"), diff --git a/src/lang/it.rs b/src/lang/it.rs index d54887aa4..1385a286d 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -210,7 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Connetti sempre tramite relay"), ("whitelist_tip", "Solo gli indirizzi IP autorizzati possono connettersi a questo desktop"), ("Login", "Accedi"), + ("Verify", ""), ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "Esci"), ("Tags", "Tag"), ("Search ID", "Cerca ID"), diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 2f3d0b54f..6930aae13 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -210,7 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "常に中継サーバー経由で接続"), ("whitelist_tip", "ホワイトリストに登録されたIPからのみ接続を許可します"), ("Login", "ログイン"), + ("Verify", ""), ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "ログアウト"), ("Tags", "タグ"), ("Search ID", "IDを検索"), diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 21e75baef..fee465786 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -210,7 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "항상 relay를 통해 접속하기"), ("whitelist_tip", "화이트리스트에 있는 IP만 현 데스크탑에 접속 가능합니다"), ("Login", "로그인"), + ("Verify", ""), ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "로그아웃"), ("Tags", "태그"), ("Search ID", "ID 검색"), diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 69caf1b6b..3ce1d6db3 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -210,7 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Әрқашан да релай сербері арқылы қосылу"), ("whitelist_tip", "Маған тек ақ-тізімделген IP қол жеткізе алады"), ("Login", "Кіру"), + ("Verify", ""), ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "Шығу"), ("Tags", "Тақтар"), ("Search ID", "ID Іздеу"), diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 24674e197..ead9e625f 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -210,7 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Zawsze łącz pośrednio"), ("whitelist_tip", "Zezwlaj na łączenie z tym komputerem tylko z adresów IP znajdujących się na białej liście"), ("Login", "Zaloguj"), + ("Verify", ""), ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "Wyloguj"), ("Tags", "Tagi"), ("Search ID", "Szukaj ID"), diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index dcf48eca4..01248e7b9 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -210,7 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Sempre conectar via relay"), ("whitelist_tip", "Somente IPs na whitelist podem me acessar"), ("Login", "Login"), + ("Verify", ""), ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "Sair"), ("Tags", "Tags"), ("Search ID", "Procurar ID"), diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 3ffee4f93..b155a08fd 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -210,7 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Sempre conectar via relay"), ("whitelist_tip", "Somente IPs confiáveis podem me acessar"), ("Login", "Login"), + ("Verify", ""), ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "Sair"), ("Tags", "Tags"), ("Search ID", "Pesquisar ID"), diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 8dd6bfbca..e055acaa2 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -210,7 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Всегда подключаться через ретрансляционный сервер"), ("whitelist_tip", "Только IP-адреса из белого списка могут получить доступ ко мне"), ("Login", "Войти"), + ("Verify", ""), ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "Выйти"), ("Tags", "Метки"), ("Search ID", "Поиск по ID"), diff --git a/src/lang/sk.rs b/src/lang/sk.rs index f67eb60cc..51f58d9b2 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -210,7 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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"), + ("Verify", ""), ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "Odhlásenie"), ("Tags", "Štítky"), ("Search ID", "Hľadať ID"), diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 222f16a8e..e3952b474 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -210,7 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Gjithmonë lidheni me transmetues"), ("whitelist_tip", "Vetëm IP e listës së bardhë mund të më aksesoj."), ("Login", "Hyrje"), + ("Verify", ""), ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "Dalje"), ("Tags", "Tage"), ("Search ID", "Kerko ID"), diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 660df7e9e..5fd822c27 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -210,7 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Uvek se spoj preko posrednika"), ("whitelist_tip", "Samo dozvoljene IP mi mogu pristupiti"), ("Login", "Prijava"), + ("Verify", ""), ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "Odjava"), ("Tags", "Oznake"), ("Search ID", "Traži ID"), diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 870d92569..a9ed367ce 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -210,7 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Anslut alltid via relay"), ("whitelist_tip", "Bara vitlistade IPs kan koppla upp till mig"), ("Login", "Logga in"), + ("Verify", ""), ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "Logga ut"), ("Tags", "Taggar"), ("Search ID", "Sök ID"), diff --git a/src/lang/template.rs b/src/lang/template.rs index 3824bf60d..e8d0c6c02 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -210,7 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", ""), ("whitelist_tip", ""), ("Login", ""), + ("Verify", ""), ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", ""), ("Tags", ""), ("Search ID", ""), diff --git a/src/lang/th.rs b/src/lang/th.rs index b2ca698e1..0c1a93bb5 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -210,7 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "เชื่อมต่อผ่านรีเลย์เสมอ"), ("whitelist_tip", "อนุญาตเฉพาะการเชื่อมต่อจาก IP ที่ไวท์ลิสต์"), ("Login", "เข้าสู่ระบบ"), + ("Verify", ""), ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "ออกจากระบบ"), ("Tags", "แท็ก"), ("Search ID", "ค้นหา ID"), diff --git a/src/lang/tr.rs b/src/lang/tr.rs index be95d2b7a..ec0c65a62 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -210,7 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Always connect via relay"), ("whitelist_tip", "Bu masaüstüne yalnızca yetkili IP adresleri bağlanabilir"), ("Login", "Giriş yap"), + ("Verify", ""), ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "Çıkış yap"), ("Tags", "Etiketler"), ("Search ID", "ID Arama"), diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 96e806d06..d5fa6ebf0 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -210,7 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "一律透過轉送連線"), ("whitelist_tip", "只有白名單中的 IP 可以存取"), ("Login", "登入"), + ("Verify", ""), ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "登出"), ("Tags", "標籤"), ("Search ID", "搜尋 ID"), diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 53fa879f5..1aeac5263 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -210,7 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Завжди підключатися через ретрансляційний сервер"), ("whitelist_tip", "Тільки IP-адреси з білого списку можуть отримати доступ до мене"), ("Login", "Увійти"), + ("Verify", ""), ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "Вийти"), ("Tags", "Ключові слова"), ("Search ID", "Пошук за ID"), diff --git a/src/lang/vn.rs b/src/lang/vn.rs index bb638c072..a5d99a718 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -210,7 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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"), + ("Verify", ""), ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "Đăng xuất"), ("Tags", "Tags"), ("Search ID", "Tìm ID"), From 87f203db4ace3ceacb2cc7128df261ff85278da2 Mon Sep 17 00:00:00 2001 From: csf Date: Mon, 9 Jan 2023 14:43:05 +0900 Subject: [PATCH 1380/2015] fix loginDialog focus conflict --- flutter/lib/common/widgets/login.dart | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/flutter/lib/common/widgets/login.dart b/flutter/lib/common/widgets/login.dart index f760132af..ce27ceb2c 100644 --- a/flutter/lib/common/widgets/login.dart +++ b/flutter/lib/common/widgets/login.dart @@ -300,8 +300,10 @@ class LoginWidgetUserPass extends StatelessWidget { final RxString curOP; final RxBool autoLogin; final Function() onLogin; + final FocusNode? userFocusNode; const LoginWidgetUserPass({ Key? key, + this.userFocusNode, required this.username, required this.pass, required this.usernameMsg, @@ -323,7 +325,7 @@ class LoginWidgetUserPass extends StatelessWidget { DialogTextField( title: '${translate("Username")}:', controller: username, - autoFocus: true, + focusNode: userFocusNode, prefixIcon: Icon(Icons.account_circle_outlined), errorText: usernameMsg), DialogTextField( @@ -376,29 +378,23 @@ class LoginWidgetUserPass extends StatelessWidget { class DialogTextField extends StatelessWidget { final String title; - final bool autoFocus; final bool obscureText; final String? errorText; final String? helperText; final Widget? prefixIcon; final TextEditingController controller; - final FocusNode focusNode = FocusNode(); + final FocusNode? focusNode; DialogTextField( {Key? key, - this.autoFocus = false, + this.focusNode, this.obscureText = false, this.errorText, this.helperText, this.prefixIcon, required this.title, required this.controller}) - : super(key: key) { - // todo mobile requestFocus, on mobile, widget will reload every time the text changes - if (autoFocus && isDesktop) { - Timer(Duration(milliseconds: 200), () => focusNode.requestFocus()); - } - } + : super(key: key); @override Widget build(BuildContext context) { @@ -429,6 +425,8 @@ class DialogTextField extends StatelessWidget { Future loginDialog() async { var username = TextEditingController(); var password = TextEditingController(); + final userFocusNode = FocusNode()..requestFocus(); + Timer(Duration(milliseconds: 100), () => userFocusNode..requestFocus()); String? usernameMsg; String? passwordMsg; @@ -525,6 +523,7 @@ Future loginDialog() async { curOP: curOP, autoLogin: autoLogin, onLogin: onLogin, + userFocusNode: userFocusNode, ), const SizedBox( height: 8.0, @@ -571,6 +570,8 @@ Future verificationCodeDialog(UserPayload? user) async { String? errorText; final code = TextEditingController(); + final focusNode = FocusNode()..requestFocus(); + Timer(Duration(milliseconds: 100), () => focusNode..requestFocus()); final res = await gFFI.dialogManager.show((setState, close) { bool validate() { @@ -643,8 +644,8 @@ Future verificationCodeDialog(UserPayload? user) async { DialogTextField( title: '${translate("Verification code")}:', controller: code, - autoFocus: true, errorText: errorText, + focusNode: focusNode, helperText: translate('verification_tip'), ), CheckboxListTile( From 9648c57f6796b4093c96e1cef219d7c3f6a65259 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Mon, 9 Jan 2023 13:59:33 +0800 Subject: [PATCH 1381/2015] try out hide_docker --- src/core_main.rs | 3 +++ src/platform/macos.rs | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/src/core_main.rs b/src/core_main.rs index bf6866df5..720c01da8 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -193,6 +193,7 @@ pub fn core_main() -> Option> { #[cfg(target_os = "macos")] { std::thread::spawn(move || crate::start_server(true)); + crate::platform::macos::hide_dock(); crate::tray::make_tray(); return None; } @@ -242,6 +243,8 @@ pub fn core_main() -> Option> { #[cfg(feature = "flutter")] crate::flutter::connection_manager::start_listen_ipc_thread(); crate::ui_interface::start_option_status_sync(); + #[cfg(target_os = "macos")] + crate::platform::macos::hide_dock(); } } //_async_logger_holder.map(|x| x.flush()); diff --git a/src/platform/macos.rs b/src/platform/macos.rs index 62fa1ee25..70e38eb57 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -4,6 +4,7 @@ use super::{CursorData, ResultType}; use cocoa::{ + appkit::{NSApp, NSApplication, NSApplicationActivationPolicy::*}, base::{id, nil, BOOL, NO, YES}, foundation::{NSDictionary, NSPoint, NSSize, NSString}, }; @@ -550,3 +551,9 @@ pub fn get_double_click_time() -> u32 { // to-do: https://github.com/servo/core-foundation-rs/blob/786895643140fa0ee4f913d7b4aeb0c4626b2085/cocoa/src/appkit.rs#L2823 500 as _ } + +pub fn hide_dock() { + unsafe { + NSApp().setActivationPolicy_(NSApplicationActivationPolicyAccessory); + } +} From d2717f29bf8a3546e48223ba98b11694943d9b07 Mon Sep 17 00:00:00 2001 From: fufesou Date: Mon, 9 Jan 2023 14:36:42 +0800 Subject: [PATCH 1382/2015] fix arrow keys on MacOS Signed-off-by: fufesou --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 1ec3929b4..659702704 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4304,7 +4304,7 @@ dependencies = [ [[package]] name = "rdev" version = "0.5.0-2" -source = "git+https://github.com/fufesou/rdev#916e8db3e68dbc0fe95b158c566374b159b9cba8" +source = "git+https://github.com/fufesou/rdev#1be26c7e8ed0d43cebdd8331d467bb61130a2e6e" dependencies = [ "cocoa", "core-foundation 0.9.3", From 80c1b89b47df87aa9541b69ea2be4a68d815698a Mon Sep 17 00:00:00 2001 From: rustdesk Date: Mon, 9 Jan 2023 18:28:11 +0800 Subject: [PATCH 1383/2015] add unit test to test_if_valid_server --- libs/hbb_common/src/socket_client.rs | 73 ++++++++++++++++++++++++++-- src/common.rs | 52 +------------------- 2 files changed, 70 insertions(+), 55 deletions(-) diff --git a/libs/hbb_common/src/socket_client.rs b/libs/hbb_common/src/socket_client.rs index b7cb13754..dde237267 100644 --- a/libs/hbb_common/src/socket_client.rs +++ b/libs/hbb_common/src/socket_client.rs @@ -9,17 +9,54 @@ use std::net::SocketAddr; use tokio::net::ToSocketAddrs; use tokio_socks::{IntoTargetAddr, TargetAddr}; -pub fn test_if_valid_server(host: &str) -> String { - let mut host = host.to_owned(); - if !host.contains(":") { - host = format!("{}:{}", host, 0); +#[inline] +pub fn check_port(host: T, port: i32) -> String { + let host = host.to_string(); + if crate::is_ipv6_str(&host) { + if host.starts_with("[") { + return host; + } + return format!("[{}]:{}", host, port); } + if !host.contains(":") { + return format!("{}:{}", host, port); + } + return 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("[") { + let tmp: Vec<&str> = host.split("]:").collect(); + if tmp.len() == 2 { + let port: i32 = tmp[1].parse().unwrap_or(0); + if port > 0 { + return format!("{}]:{}", tmp[0], port + offset); + } + } + } + } 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 { + return format!("{}:{}", tmp[0], port + offset); + } + } + } + return host; +} + +pub fn test_if_valid_server(host: &str) -> String { + let host = check_port(host, 0); use std::net::ToSocketAddrs; match Config::get_network_type() { NetworkType::Direct => match host.to_socket_addrs() { Err(err) => err.to_string(), - Ok(_) => "".to_owned(), + Ok(x) => "".to_owned(), }, NetworkType::ProxySocks => match &host.into_target_addr() { Err(err) => err.to_string(), @@ -216,4 +253,30 @@ mod tests { } assert!(query_nip_io(&"1.1.1.1:80".parse().unwrap()).await.is_err()); } + + #[test] + fn test_test_if_valid_server() { + assert!(!test_if_valid_server("a").is_empty()); + // on Linux, "1" is resolved to "0.0.0.1" + assert!(test_if_valid_server("1.1.1.1").is_empty()); + assert!(test_if_valid_server("1.1.1.1:1").is_empty()); + } + + #[test] + fn test_check_port() { + assert_eq!(check_port("[1:2]:12", 32), "[1:2]:12"); + assert_eq!(check_port("1:2", 32), "[1:2]:32"); + assert_eq!(check_port("z1:2", 32), "z1:2"); + assert_eq!(check_port("1.1.1.1", 32), "1.1.1.1:32"); + assert_eq!(check_port("1.1.1.1:32", 32), "1.1.1.1:32"); + assert_eq!(check_port("test.com:32", 0), "test.com:32"); + assert_eq!(increase_port("[1:2]:12", 1), "[1:2]:13"); + assert_eq!(increase_port("1.2.2.4:12", 1), "1.2.2.4:13"); + assert_eq!(increase_port("1.2.2.4", 1), "1.2.2.4"); + assert_eq!(increase_port("test.com", 1), "test.com"); + assert_eq!(increase_port("test.com:13", 4), "test.com:17"); + assert_eq!(increase_port("1:13", 4), "1:13"); + assert_eq!(increase_port("22:1:13", 4), "22:1:13"); + assert_eq!(increase_port("z1:2", 1), "z1:3"); + } } diff --git a/src/common.rs b/src/common.rs index 254c910e6..96a7763d0 100644 --- a/src/common.rs +++ b/src/common.rs @@ -476,42 +476,12 @@ pub fn username() -> String { #[inline] pub fn check_port(host: T, port: i32) -> String { - let host = host.to_string(); - if is_ipv6_str(&host) { - if host.starts_with("[") { - return host; - } - return format!("[{}]:{}", host, port); - } - if !host.contains(":") { - return format!("{}:{}", host, port); - } - return host; + hbb_common::socket_client::check_port(host, port) } #[inline] pub fn increase_port(host: T, offset: i32) -> String { - let host = host.to_string(); - if is_ipv6_str(&host) { - if host.starts_with("[") { - let tmp: Vec<&str> = host.split("]:").collect(); - if tmp.len() == 2 { - let port: i32 = tmp[1].parse().unwrap_or(0); - if port > 0 { - return format!("{}]:{}", tmp[0], port + offset); - } - } - } - } 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 { - return format!("{}:{}", tmp[0], port + offset); - } - } - } - return host; + hbb_common::socket_client::increase_port(host, offset) } pub const POSTFIX_SERVICE: &'static str = "_service"; @@ -741,22 +711,4 @@ pub fn make_fd_to_json(id: i32, path: String, entries: &Vec) -> Strin #[cfg(test)] mod test_common { use super::*; - - #[test] - fn test_check_port() { - assert_eq!(check_port("[1:2]:12", 32), "[1:2]:12"); - assert_eq!(check_port("1:2", 32), "[1:2]:32"); - assert_eq!(check_port("z1:2", 32), "z1:2"); - assert_eq!(check_port("1.1.1.1", 32), "1.1.1.1:32"); - assert_eq!(check_port("1.1.1.1:32", 32), "1.1.1.1:32"); - assert_eq!(check_port("test.com:32", 0), "test.com:32"); - assert_eq!(increase_port("[1:2]:12", 1), "[1:2]:13"); - assert_eq!(increase_port("1.2.2.4:12", 1), "1.2.2.4:13"); - assert_eq!(increase_port("1.2.2.4", 1), "1.2.2.4"); - assert_eq!(increase_port("test.com", 1), "test.com"); - assert_eq!(increase_port("test.com:13", 4), "test.com:17"); - assert_eq!(increase_port("1:13", 4), "1:13"); - assert_eq!(increase_port("22:1:13", 4), "22:1:13"); - assert_eq!(increase_port("z1:2", 1), "z1:3"); - } } From f2ff1d2da1fba38b0df1603813ead278c4691ef7 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Mon, 9 Jan 2023 18:44:34 +0800 Subject: [PATCH 1384/2015] fix some port change --- src/common.rs | 27 +++++++-------------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/src/common.rs b/src/common.rs index 96a7763d0..fb705b599 100644 --- a/src/common.rs +++ b/src/common.rs @@ -293,15 +293,7 @@ async fn test_nat_type_() -> ResultType { let start = std::time::Instant::now(); let (rendezvous_server, _, _) = get_rendezvous_server(1_000).await; let server1 = rendezvous_server; - let tmp: Vec<&str> = server1.split(":").collect(); - if tmp.len() != 2 { - bail!("Invalid server address: {}", server1); - } - let port: u16 = tmp[1].parse()?; - if port == 0 { - bail!("Invalid server address: {}", server1); - } - let server2 = format!("{}:{}", tmp[0], port - 1); + let server2 = crate::increase_port(server1, -1); let mut msg_out = RendezvousMessage::new(); let serial = Config::get_serial(); msg_out.set_test_nat_request(TestNatRequest { @@ -592,18 +584,13 @@ pub fn get_api_server(api: String, custom: String) -> String { return lic.api.clone(); } } - let s = get_custom_rendezvous_server(custom); - if !s.is_empty() { - if s.contains(':') { - let tmp: Vec<&str> = s.split(":").collect(); - if tmp.len() == 2 { - let port: u16 = tmp[1].parse().unwrap_or(0); - if port > 2 { - return format!("http://{}:{}", tmp[0], port - 2); - } - } + let s0 = get_custom_rendezvous_server(custom); + if !s0.is_empty() { + let s = crate::increase_port(s0, -2); + if s == s0 { + format!("http://{}:{}", s, config::RENDEZVOUS_PORT - 2); } else { - return format!("http://{}:{}", s, config::RENDEZVOUS_PORT - 2); + format!("http://{}", s); } } "https://admin.rustdesk.com".to_owned() From 12b02514a4e76fb7203184ba13f39167336e34c0 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Mon, 9 Jan 2023 18:45:38 +0800 Subject: [PATCH 1385/2015] fix one warning --- libs/hbb_common/src/socket_client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/hbb_common/src/socket_client.rs b/libs/hbb_common/src/socket_client.rs index dde237267..6f62163d1 100644 --- a/libs/hbb_common/src/socket_client.rs +++ b/libs/hbb_common/src/socket_client.rs @@ -56,7 +56,7 @@ pub fn test_if_valid_server(host: &str) -> String { match Config::get_network_type() { NetworkType::Direct => match host.to_socket_addrs() { Err(err) => err.to_string(), - Ok(x) => "".to_owned(), + Ok(_) => "".to_owned(), }, NetworkType::ProxySocks => match &host.into_target_addr() { Err(err) => err.to_string(), From fb658cba2b1db54b62863bb65a1d1e1b73e24cde Mon Sep 17 00:00:00 2001 From: rustdesk Date: Mon, 9 Jan 2023 18:52:44 +0800 Subject: [PATCH 1386/2015] fix compile --- src/common.rs | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/src/common.rs b/src/common.rs index fb705b599..f10e23d2d 100644 --- a/src/common.rs +++ b/src/common.rs @@ -18,10 +18,9 @@ pub use arboard::Clipboard as ClipboardContext; use hbb_common::compress::decompress; use hbb_common::{ allow_err, - anyhow::bail, compress::compress as compress_func, config::{self, Config, COMPRESS_LEVEL, RENDEZVOUS_TIMEOUT}, - get_version_number, is_ipv6_str, log, + get_version_number, log, message_proto::*, protobuf::Enum, protobuf::Message as _, @@ -293,7 +292,7 @@ async fn test_nat_type_() -> ResultType { let start = std::time::Instant::now(); let (rendezvous_server, _, _) = get_rendezvous_server(1_000).await; let server1 = rendezvous_server; - let server2 = crate::increase_port(server1, -1); + let server2 = crate::increase_port(&server1, -1); let mut msg_out = RendezvousMessage::new(); let serial = Config::get_serial(); msg_out.set_test_nat_request(TestNatRequest { @@ -356,13 +355,7 @@ pub async fn get_rendezvous_server(ms_timeout: u64) -> (String, Vec, boo let (mut a, mut b) = get_rendezvous_server_(ms_timeout).await; let mut b: Vec = b .drain(..) - .map(|x| { - if !x.contains(":") { - format!("{}:{}", x, config::RENDEZVOUS_PORT) - } else { - x - } - }) + .map(|x| socket_client::check_port(x, config::RENDEZVOUS_PORT)) .collect(); let c = if b.contains(&a) { b = b.drain(..).filter(|x| x != &a).collect(); @@ -586,7 +579,7 @@ pub fn get_api_server(api: String, custom: String) -> String { } let s0 = get_custom_rendezvous_server(custom); if !s0.is_empty() { - let s = crate::increase_port(s0, -2); + let s = crate::increase_port(&s0, -2); if s == s0 { format!("http://{}:{}", s, config::RENDEZVOUS_PORT - 2); } else { From 61c03a921da39f2edca79019b9b90a1ae74700bd Mon Sep 17 00:00:00 2001 From: rustdesk Date: Mon, 9 Jan 2023 18:55:37 +0800 Subject: [PATCH 1387/2015] fix ci --- src/common.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common.rs b/src/common.rs index f10e23d2d..0be84e79f 100644 --- a/src/common.rs +++ b/src/common.rs @@ -638,7 +638,7 @@ pub async fn post_request(url: String, body: String, header: &str) -> ResultType if !res.is_empty() { return Ok(res); } - bail!(String::from_utf8_lossy(&output.stderr).to_string()); + hbb_common::bail!(String::from_utf8_lossy(&output.stderr).to_string()); } } From 658c6500d9dc4d52040f96f9639abb1aedf10ceb Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 9 Jan 2023 19:28:56 +0800 Subject: [PATCH 1388/2015] adjust server setting ui Signed-off-by: 21pages --- flutter/lib/desktop/pages/desktop_setting_page.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 6d38c40f9..ac92da14c 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -1460,6 +1460,8 @@ _LabeledTextField( enabled: enabled, obscureText: secure, decoration: InputDecoration( + isDense: true, + contentPadding: EdgeInsets.symmetric(vertical: 15), errorText: errorText.isNotEmpty ? errorText : null), style: TextStyle( color: _disabledTextColor(context, enabled), From b35b426f12b22f83409d4c70958d0d740ec12afc Mon Sep 17 00:00:00 2001 From: 21pages Date: Fri, 6 Jan 2023 10:45:55 +0800 Subject: [PATCH 1389/2015] system_message code clean Signed-off-by: 21pages --- src/platform/linux.rs | 74 +++++++++++++++++++------------------------ 1 file changed, 33 insertions(+), 41 deletions(-) diff --git a/src/platform/linux.rs b/src/platform/linux.rs index b2c2e81cb..3eb8f0b87 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -4,6 +4,7 @@ use hbb_common::{allow_err, bail, log}; use libc::{c_char, c_int, c_void}; use std::{ cell::RefCell, + collections::HashMap, path::PathBuf, sync::{ atomic::{AtomicBool, Ordering}, @@ -719,47 +720,38 @@ pub fn get_double_click_time() -> u32 { /// forever: may not work pub fn system_message(title: &str, msg: &str, forever: bool) -> ResultType<()> { - if std::process::Command::new("notify-send") - .arg(title) - .arg(msg) - .spawn() - .is_ok() - { - return Ok(()); - } - if std::process::Command::new("zenity") - .arg("--info") - .arg("--timeout") - .arg(if forever { "0" } else { "3" }) - .arg("--title") - .arg(title) - .arg("--text") - .arg(msg) - .spawn() - .is_ok() - { - return Ok(()); - } - if std::process::Command::new("kdialog") - .arg("--title") - .arg(title) - .arg("--msgbox") - .arg(msg) - .spawn() - .is_ok() - { - return Ok(()); - } - if std::process::Command::new("xmessage") - .arg("-center") - .arg("-timeout") - .arg(if forever { "0" } else { "3" }) - .arg(title) - .arg(msg) - .spawn() - .is_ok() - { - return Ok(()); + 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"); } From 95844e60cfce5c92351b4a2d7ea3ab381e3ce537 Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 10 Jan 2023 00:40:41 +0800 Subject: [PATCH 1390/2015] win filter scancodes that is greater than 255 Signed-off-by: fufesou --- src/keyboard.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/keyboard.rs b/src/keyboard.rs index 9fa53757f..f29eb27bc 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -65,7 +65,7 @@ pub mod client { #[cfg(not(feature = "cli"))] if let Some(handler) = CUR_SESSION.lock().unwrap().as_ref() { return handler.get_keyboard_mode(); - } + } "legacy".to_string() } @@ -372,7 +372,7 @@ pub fn get_peer_platform() -> String { #[cfg(not(feature = "cli"))] if let Some(handler) = CUR_SESSION.lock().unwrap().as_ref() { return handler.peer_platform(); - } + } "Windows".to_string() } @@ -615,7 +615,13 @@ pub fn map_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option event.scan_code, + "windows" => { + // https://github.com/rustdesk/rustdesk/issues/1371 + if event.scan_code > 255 { + return None; + } + event.scan_code + } "macos" => { if hbb_common::config::LocalConfig::get_kb_layout_type() == "ISO" { rdev::win_scancode_to_macos_iso_code(event.scan_code)? From b75453b08f5ff731884f20f07a4b952cce5d6811 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 01:20:52 -0500 Subject: [PATCH 1391/2015] spelling: a workaround Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- src/ui/cm.tis | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/cm.tis b/src/ui/cm.tis index 716f2c6dd..a1d623322 100644 --- a/src/ui/cm.tis +++ b/src/ui/cm.tis @@ -31,7 +31,7 @@ class Body: Reactor.Component var disconnected = c.disconnected; var show_elevation_btn = handler.can_elevate() && show_elevation && !c.is_file_transfer && c.port_forward.length == 0; var show_accept_btn = handler.get_option('approve-mode') != 'password'; - // below size:* is work around for Linux, it already set in css, but not work, shit sciter + // below size:* is a workaround for Linux, it already set in css, but not work, shit sciter return
    From 8351d331b4d75705926da73c95d0abd322c0e133 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:17 -0500 Subject: [PATCH 1392/2015] spelling: acceleration Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- src/ui/remote.tis | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/remote.tis b/src/ui/remote.tis index 63df0cb09..5c828689d 100644 --- a/src/ui/remote.tis +++ b/src/ui/remote.tis @@ -120,7 +120,7 @@ function resetWheel() { var INERTIA_ACCELERATION = 30; -// not good, precision not enough to simulate accelation effect, +// not good, precision not enough to simulate acceleration effect, // seems have to use pixel based rather line based delta function accWheel(v, is_x) { if (wheeling) return; From 2b93de18ce8d559eb6d30afb7d46461e72473ab2 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:19 -0500 Subject: [PATCH 1393/2015] spelling: activate Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- src/client.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client.rs b/src/client.rs index 635c8b661..440c0b0b0 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1662,7 +1662,7 @@ pub fn send_mouse( interface.send(Data::Message(msg_out)); } -/// Avtivate OS by sending mouse movement. +/// Activate OS by sending mouse movement. /// /// # Arguments /// @@ -1690,7 +1690,7 @@ fn activate_os(interface: &impl Interface) { /// # Arguments /// /// * `p` - The password. -/// * `avtivate` - Whether to activate OS. +/// * `activate` - Whether to activate OS. /// * `interface` - The interface for sending data. pub fn input_os_password(p: String, activate: bool, interface: impl Interface) { std::thread::spawn(move || { @@ -1703,7 +1703,7 @@ pub fn input_os_password(p: String, activate: bool, interface: impl Interface) { /// # Arguments /// /// * `p` - The password. -/// * `avtivate` - Whether to activate OS. +/// * `activate` - Whether to activate OS. /// * `interface` - The interface for sending data. fn _input_os_password(p: String, activate: bool, interface: impl Interface) { if activate { From b4bb5bfecfc81c85568f9b438f56908e5cc82a5d Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:31:17 -0500 Subject: [PATCH 1394/2015] spelling: active Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- src/server/input_service.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 41ce8fd9e..814fea110 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -487,7 +487,7 @@ fn active_mouse_(conn: i32) -> bool { return false; } - let in_actived_dist = |a: i32, b: i32| -> bool { (a - b).abs() < MOUSE_ACTIVE_DISTANCE }; + let in_active_dist = |a: i32, b: i32| -> bool { (a - b).abs() < MOUSE_ACTIVE_DISTANCE }; // Check if input is in valid range match crate::get_cursor_pos() { @@ -496,7 +496,7 @@ fn active_mouse_(conn: i32) -> bool { let lock = LATEST_PEER_INPUT_CURSOR.lock().unwrap(); (lock.x, lock.y) }; - let mut can_active = in_actived_dist(last_in_x, x) && in_actived_dist(last_in_y, y); + let mut can_active = in_active_dist(last_in_x, x) && in_active_dist(last_in_y, y); // The cursor may not have been moved to last input position if system is busy now. // While this is not a common case, we check it again after some time later. if !can_active { @@ -505,7 +505,7 @@ fn active_mouse_(conn: i32) -> bool { std::thread::sleep(std::time::Duration::from_micros(10)); // Sleep here can also somehow suppress delay accumulation. if let Some((x2, y2)) = crate::get_cursor_pos() { - can_active = in_actived_dist(last_in_x, x2) && in_actived_dist(last_in_y, y2); + can_active = in_active_dist(last_in_x, x2) && in_active_dist(last_in_y, y2); } } if !can_active { From cbfcc3657fef0e40920331dcf544070d0051a5e9 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:18 -0500 Subject: [PATCH 1395/2015] spelling: agreement Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- src/ui/install.tis | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ui/install.tis b/src/ui/install.tis index 39301fd02..3a7920bcf 100644 --- a/src/ui/install.tis +++ b/src/ui/install.tis @@ -13,7 +13,7 @@ class Install: Reactor.Component {
    {translate('Create start menu shortcuts')}
    {translate('Create desktop icon')}
    -
    {translate('End-user license agreement')}
    +
    {translate('End-user license agreement')}
    {translate('agreement_tip')}
    @@ -46,7 +46,7 @@ class Install: Reactor.Component { } } - event click $(#aggrement) { + event click $(#agreement) { view.open_url("http://rustdesk.com/privacy"); } From 2929d0f6a5ed0f83e6ae0bdc9c2dbe727f106bb7 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:17 -0500 Subject: [PATCH 1396/2015] spelling: android Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- fastlane/metadata/android/en-US/full_description.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt index 1f35ef92d..f78b3a20b 100644 --- a/fastlane/metadata/android/en-US/full_description.txt +++ b/fastlane/metadata/android/en-US/full_description.txt @@ -2,7 +2,7 @@ An open-source remote desktop application, the open source TeamViewer alternativ Source code: https://github.com/rustdesk/rustdesk Doc: https://rustdesk.com/docs/en/manual/mobile/ -In order for a remote device to control your Android device via mouse or touch, you need to allow RustDesk to use the "Accessibility" service, RustDesk uses AccessibilityService API to implement Addroid remote control. +In order for a remote device to control your Android device via mouse or touch, you need to allow RustDesk to use the "Accessibility" service, RustDesk uses AccessibilityService API to implement Android remote control. In addition to remote control, you can also transfer files between Android devices and PCs easily with RustDesk. From 49c1b3a2df877c3798de827b9d40331e5b682926 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:18 -0500 Subject: [PATCH 1397/2015] spelling: another Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- flutter/lib/models/input_model.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index 0137b784e..63f86078c 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -197,7 +197,7 @@ class InputModel { // Check update event type and set buttons to be sent. int buttons = _lastButtons; if (type == _kMouseEventMove) { - // flutter may emit move event if one button is pressed and anoter button + // flutter may emit move event if one button is pressed and another button // is pressing or releasing. if (evt.buttons != _lastButtons) { // For simplicity From f45fdaa46fc7a397ae8e45a211a867cd324de85d Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:46:31 -0500 Subject: [PATCH 1398/2015] spelling: appveyor Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- libs/enigo/appveyor.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/libs/enigo/appveyor.yml b/libs/enigo/appveyor.yml index af3142ad9..5ad7bc249 100644 --- a/libs/enigo/appveyor.yml +++ b/libs/enigo/appveyor.yml @@ -1,9 +1,9 @@ -# Appveyor configuration template for Rust using rustup for Rust installation +# AppVeyor configuration template for Rust using rustup for Rust installation # https://github.com/starkat99/appveyor-rust ## Operating System (VM environment) ## -# Rust needs at least Visual Studio 2013 Appveyor OS for MSVC targets. +# Rust needs at least Visual Studio 2013 AppVeyor OS for MSVC targets. os: Visual Studio 2015 ## Build Matrix ## @@ -83,7 +83,7 @@ environment: ### Allowed failures ### -# See Appveyor documentation for specific details. In short, place any channel or targets you wish +# See AppVeyor documentation for specific details. In short, place any channel or targets you wish # to allow build failures on (usually nightly at least is a wise choice). This will prevent a build # or test failure in the matching channels/targets from failing the entire build. matrix: @@ -95,7 +95,7 @@ matrix: ## Install Script ## -# This is the most important part of the Appveyor configuration. This installs the version of Rust +# This is the most important part of the AppVeyor configuration. This installs the version of Rust # specified by the 'channel' and 'target' environment variables from the build matrix. This uses # rustup to install Rust. # @@ -110,7 +110,7 @@ install: ## Build Script ## -# 'cargo test' takes care of building for us, so disable Appveyor's build stage. This prevents +# 'cargo test' takes care of building for us, so disable AppVeyor's build stage. This prevents # the "directory does not contain a project or solution file" error. build: false From 185ff9e91e6c9aaa94e26c09f2e0e20e2639d580 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:19 -0500 Subject: [PATCH 1399/2015] spelling: available Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- libs/scrap/src/common/hwcodec.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/scrap/src/common/hwcodec.rs b/libs/scrap/src/common/hwcodec.rs index c77da3f8f..d92ed2a7d 100644 --- a/libs/scrap/src/common/hwcodec.rs +++ b/libs/scrap/src/common/hwcodec.rs @@ -293,8 +293,8 @@ pub fn check_config() { quality: DEFAULT_HW_QUALITY, rc: DEFAULT_RC, }; - let encoders = CodecInfo::score(Encoder::avaliable_encoders(ctx)); - let decoders = CodecInfo::score(Decoder::avaliable_decoders()); + let encoders = CodecInfo::score(Encoder::available_encoders(ctx)); + let decoders = CodecInfo::score(Decoder::available_decoders()); if let Ok(old_encoders) = get_config(CFG_KEY_ENCODER) { if let Ok(old_decoders) = get_config(CFG_KEY_DECODER) { From c40ae690e320044de543c049c06735ac7d404bec Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:20 -0500 Subject: [PATCH 1400/2015] spelling: capture Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- src/server/video_service.rs | 32 ++++++++++++++++---------------- src/server/wayland.rs | 2 +- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/server/video_service.rs b/src/server/video_service.rs index b986c785c..618b003e9 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -309,9 +309,9 @@ pub fn test_create_capturer(privacy_mode_id: i32, timeout_millis: u64) -> bool { } #[cfg(windows)] -fn check_uac_switch(privacy_mode_id: i32, captuerer_privacy_mode_id: i32) -> ResultType<()> { - if captuerer_privacy_mode_id != 0 { - if privacy_mode_id != captuerer_privacy_mode_id { +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()? { bail!("consent.exe is running"); } @@ -330,7 +330,7 @@ pub(super) struct CapturerInfo { pub ndisplay: usize, pub current: usize, pub privacy_mode_id: i32, - pub _captuerer_privacy_mode_id: i32, + pub _capturer_privacy_mode_id: i32, pub capturer: Box, } @@ -371,29 +371,29 @@ fn get_capturer(use_yuv: bool, portable_service_running: bool) -> ResultType ResultType ResultType<()> { while sp.ok() { #[cfg(windows)] - check_uac_switch(c.privacy_mode_id, c._captuerer_privacy_mode_id)?; + check_uac_switch(c.privacy_mode_id, c._capturer_privacy_mode_id)?; let mut video_qos = VIDEO_QOS.lock().unwrap(); if video_qos.check_if_updated() { @@ -602,7 +602,7 @@ fn run(sp: GenericService) -> ResultType<()> { if !scrap::is_x11() { if would_block_count >= 100 { super::wayland::release_resource(); - bail!("Wayland capturer none 100 times, try restart captuere"); + bail!("Wayland capturer none 100 times, try restart capture"); } } } @@ -637,7 +637,7 @@ fn run(sp: GenericService) -> ResultType<()> { while wait_begin.elapsed().as_millis() < timeout_millis as _ { check_privacy_mode_changed(&sp, c.privacy_mode_id)?; #[cfg(windows)] - check_uac_switch(c.privacy_mode_id, c._captuerer_privacy_mode_id)?; + check_uac_switch(c.privacy_mode_id, c._capturer_privacy_mode_id)?; frame_controller.try_wait_next(&mut fetched_conn_ids, 300); // break if all connections have received current frame if fetched_conn_ids.len() >= frame_controller.send_conn_ids.len() { diff --git a/src/server/wayland.rs b/src/server/wayland.rs index 24b3be110..68b9c37cf 100644 --- a/src/server/wayland.rs +++ b/src/server/wayland.rs @@ -276,7 +276,7 @@ pub(super) fn get_capturer() -> ResultType { ndisplay: cap_display_info.num, current: cap_display_info.current, privacy_mode_id: 0, - _captuerer_privacy_mode_id: 0, + _capturer_privacy_mode_id: 0, capturer: Box::new(cap_display_info.capturer.clone()), }) } From 380a1670f0d8f1a965cb9e9b956d251fb2ffc733 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:49:36 -0500 Subject: [PATCH 1401/2015] spelling: chosen Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- .../widgets/kb_layout_type_chooser.dart | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart b/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart index cfbdb0c4e..58a8f7109 100644 --- a/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart +++ b/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart @@ -6,7 +6,7 @@ import 'package:flutter_hbb/models/platform_model.dart'; import '../../common.dart'; -typedef KBChoosedCallback = Future Function(String); +typedef KBChosenCallback = Future Function(String); const double _kImageMarginVertical = 6.0; const double _kImageMarginHorizontal = 10.0; @@ -25,12 +25,12 @@ const _kKBLayoutImageMap = { class _KBImage extends StatelessWidget { final String kbLayoutType; final double imageWidth; - final RxString choosedType; + final RxString chosenType; const _KBImage({ Key? key, required this.kbLayoutType, required this.imageWidth, - required this.choosedType, + required this.chosenType, }) : super(key: key); @override @@ -40,7 +40,7 @@ class _KBImage extends StatelessWidget { decoration: BoxDecoration( borderRadius: BorderRadius.circular(_kBorderRadius), border: Border.all( - color: choosedType.value == kbLayoutType + color: chosenType.value == kbLayoutType ? _kImageBorderColor : Colors.transparent, width: _kImageBoarderWidth, @@ -66,13 +66,13 @@ class _KBImage extends StatelessWidget { class _KBChooser extends StatelessWidget { final String kbLayoutType; final double imageWidth; - final RxString choosedType; - final KBChoosedCallback cb; + final RxString chosenType; + final KBChosenCallback cb; const _KBChooser({ Key? key, required this.kbLayoutType, required this.imageWidth, - required this.choosedType, + required this.chosenType, required this.cb, }) : super(key: key); @@ -81,7 +81,7 @@ class _KBChooser extends StatelessWidget { onChanged(String? v) async { if (v != null) { if (await cb(v)) { - choosedType.value = v; + chosenType.value = v; } } } @@ -95,7 +95,7 @@ class _KBChooser extends StatelessWidget { child: _KBImage( kbLayoutType: kbLayoutType, imageWidth: imageWidth, - choosedType: choosedType, + chosenType: chosenType, ), style: TextButton.styleFrom(padding: EdgeInsets.zero), ), @@ -105,7 +105,7 @@ class _KBChooser extends StatelessWidget { Obx(() => Radio( splashRadius: 0, value: kbLayoutType, - groupValue: choosedType.value, + groupValue: chosenType.value, onChanged: onChanged, )), Text(kbLayoutType), @@ -121,14 +121,14 @@ class _KBChooser extends StatelessWidget { } class KBLayoutTypeChooser extends StatelessWidget { - final RxString choosedType; + final RxString chosenType; final double width; final double height; final double dividerWidth; - final KBChoosedCallback cb; + final KBChosenCallback cb; KBLayoutTypeChooser({ Key? key, - required this.choosedType, + required this.chosenType, required this.width, required this.height, required this.dividerWidth, @@ -147,7 +147,7 @@ class KBLayoutTypeChooser extends StatelessWidget { _KBChooser( kbLayoutType: _kKBLayoutTypeISO, imageWidth: imageWidth, - choosedType: choosedType, + chosenType: chosenType, cb: cb, ), VerticalDivider( @@ -156,7 +156,7 @@ class KBLayoutTypeChooser extends StatelessWidget { _KBChooser( kbLayoutType: _kKBLayoutTypeNotISO, imageWidth: imageWidth, - choosedType: choosedType, + chosenType: chosenType, cb: cb, ), ], @@ -208,7 +208,7 @@ showKBLayoutTypeChooser( title: Text('${translate('Select local keyboard type')} ($localPlatform)'), content: KBLayoutTypeChooser( - choosedType: KBLayoutType, + chosenType: KBLayoutType, width: 360, height: 200, dividerWidth: 4.0, From caa557e360784349e9d30e8ed7efe3a79310ac11 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:21 -0500 Subject: [PATCH 1402/2015] spelling: clipboard Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- libs/clipboard/src/lib.rs | 40 +++++++++++++++++++-------------------- src/clipboard_file.rs | 34 ++++++++++++++++----------------- src/ipc.rs | 4 ++-- src/server/connection.rs | 4 ++-- src/ui_cm_interface.rs | 8 ++++---- 5 files changed, 45 insertions(+), 45 deletions(-) diff --git a/libs/clipboard/src/lib.rs b/libs/clipboard/src/lib.rs index b992e39e3..e7a533d69 100644 --- a/libs/clipboard/src/lib.rs +++ b/libs/clipboard/src/lib.rs @@ -21,7 +21,7 @@ pub use context_send::*; #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(tag = "t", content = "c")] -pub enum ClipbaordFile { +pub enum ClipboardFile { MonitorReady, FormatList { format_list: Vec<(i32, String)>, @@ -61,8 +61,8 @@ struct ConnEnabled { struct MsgChannel { peer_id: String, conn_id: i32, - sender: UnboundedSender, - receiver: Arc>>, + sender: UnboundedSender, + receiver: Arc>>, } #[derive(PartialEq)] @@ -89,7 +89,7 @@ pub fn get_client_conn_id(peer_id: &str) -> Option { pub fn get_rx_cliprdr_client( peer_id: &str, -) -> (i32, Arc>>) { +) -> (i32, Arc>>) { let mut lock = VEC_MSG_CHANNEL.write().unwrap(); match lock.iter().find(|x| x.peer_id == peer_id.to_owned()) { Some(msg_channel) => (msg_channel.conn_id, msg_channel.receiver.clone()), @@ -110,7 +110,7 @@ pub fn get_rx_cliprdr_client( } } -pub fn get_rx_cliprdr_server(conn_id: i32) -> Arc>> { +pub fn get_rx_cliprdr_server(conn_id: i32) -> Arc>> { let mut lock = VEC_MSG_CHANNEL.write().unwrap(); match lock.iter().find(|x| x.conn_id == conn_id) { Some(msg_channel) => msg_channel.receiver.clone(), @@ -131,7 +131,7 @@ pub fn get_rx_cliprdr_server(conn_id: i32) -> Arc, conn_id: i32) -> pub fn server_clip_file( context: &mut Box, conn_id: i32, - msg: ClipbaordFile, + msg: ClipboardFile, ) -> u32 { match msg { - ClipbaordFile::MonitorReady => { + ClipboardFile::MonitorReady => { log::debug!("server_monitor_ready called"); let ret = server_monitor_ready(context, conn_id); log::debug!("server_monitor_ready called, return {}", ret); ret } - ClipbaordFile::FormatList { format_list } => { + ClipboardFile::FormatList { format_list } => { log::debug!("server_format_list called"); let ret = server_format_list(context, conn_id, format_list); log::debug!("server_format_list called, return {}", ret); ret } - ClipbaordFile::FormatListResponse { msg_flags } => { + ClipboardFile::FormatListResponse { msg_flags } => { log::debug!("format_list_response called"); let ret = server_format_list_response(context, conn_id, msg_flags); log::debug!("server_format_list_response called, return {}", ret); ret } - ClipbaordFile::FormatDataRequest { + ClipboardFile::FormatDataRequest { requested_format_id, } => { log::debug!("format_data_request called"); @@ -186,7 +186,7 @@ pub fn server_clip_file( log::debug!("server_format_data_request called, return {}", ret); ret } - ClipbaordFile::FormatDataResponse { + ClipboardFile::FormatDataResponse { msg_flags, format_data, } => { @@ -195,7 +195,7 @@ pub fn server_clip_file( log::debug!("server_format_data_response called, return {}", ret); ret } - ClipbaordFile::FileContentsRequest { + ClipboardFile::FileContentsRequest { stream_id, list_index, dw_flags, @@ -221,7 +221,7 @@ pub fn server_clip_file( log::debug!("server_file_contents_request called, return {}", ret); ret } - ClipbaordFile::FileContentsResponse { + ClipboardFile::FileContentsResponse { msg_flags, stream_id, requested_data, @@ -492,7 +492,7 @@ extern "C" fn client_format_list( } conn_id = (*clip_format_list).connID as i32; } - let data = ClipbaordFile::FormatList { format_list }; + let data = ClipboardFile::FormatList { format_list }; // no need to handle result here if conn_id == 0 { VEC_MSG_CHANNEL @@ -519,7 +519,7 @@ extern "C" fn client_format_list_response( conn_id = (*format_list_response).connID as i32; msg_flags = (*format_list_response).msgFlags as i32; } - let data = ClipbaordFile::FormatListResponse { msg_flags }; + let data = ClipboardFile::FormatListResponse { msg_flags }; send_data(conn_id, data); 0 @@ -537,7 +537,7 @@ extern "C" fn client_format_data_request( conn_id = (*format_data_request).connID as i32; requested_format_id = (*format_data_request).requestedFormatId as i32; } - let data = ClipbaordFile::FormatDataRequest { + let data = ClipboardFile::FormatDataRequest { requested_format_id, }; // no need to handle result here @@ -568,7 +568,7 @@ extern "C" fn client_format_data_response( .to_vec(); } } - let data = ClipbaordFile::FormatDataResponse { + let data = ClipboardFile::FormatDataResponse { msg_flags, format_data, }; @@ -614,7 +614,7 @@ extern "C" fn client_file_contents_request( clip_data_id = (*file_contents_request).clipDataId as i32; } - let data = ClipbaordFile::FileContentsRequest { + let data = ClipboardFile::FileContentsRequest { stream_id, list_index, dw_flags, @@ -653,7 +653,7 @@ extern "C" fn client_file_contents_response( .to_vec(); } } - let data = ClipbaordFile::FileContentsResponse { + let data = ClipboardFile::FileContentsResponse { msg_flags, stream_id, requested_data, diff --git a/src/clipboard_file.rs b/src/clipboard_file.rs index e6f40e215..f0fe41b8d 100644 --- a/src/clipboard_file.rs +++ b/src/clipboard_file.rs @@ -1,9 +1,9 @@ -use clipboard::ClipbaordFile; +use clipboard::ClipboardFile; use hbb_common::message_proto::*; -pub fn clip_2_msg(clip: ClipbaordFile) -> Message { +pub fn clip_2_msg(clip: ClipboardFile) -> Message { match clip { - ClipbaordFile::MonitorReady => Message { + ClipboardFile::MonitorReady => Message { union: Some(message::Union::Cliprdr(Cliprdr { union: Some(cliprdr::Union::Ready(CliprdrMonitorReady { ..Default::default() @@ -12,7 +12,7 @@ pub fn clip_2_msg(clip: ClipbaordFile) -> Message { })), ..Default::default() }, - ClipbaordFile::FormatList { format_list } => { + ClipboardFile::FormatList { format_list } => { let mut formats: Vec = Vec::new(); for v in format_list.iter() { formats.push(CliprdrFormat { @@ -32,7 +32,7 @@ pub fn clip_2_msg(clip: ClipbaordFile) -> Message { ..Default::default() } } - ClipbaordFile::FormatListResponse { msg_flags } => Message { + ClipboardFile::FormatListResponse { msg_flags } => Message { union: Some(message::Union::Cliprdr(Cliprdr { union: Some(cliprdr::Union::FormatListResponse( CliprdrServerFormatListResponse { @@ -44,7 +44,7 @@ pub fn clip_2_msg(clip: ClipbaordFile) -> Message { })), ..Default::default() }, - ClipbaordFile::FormatDataRequest { + ClipboardFile::FormatDataRequest { requested_format_id, } => Message { union: Some(message::Union::Cliprdr(Cliprdr { @@ -58,7 +58,7 @@ pub fn clip_2_msg(clip: ClipbaordFile) -> Message { })), ..Default::default() }, - ClipbaordFile::FormatDataResponse { + ClipboardFile::FormatDataResponse { msg_flags, format_data, } => Message { @@ -74,7 +74,7 @@ pub fn clip_2_msg(clip: ClipbaordFile) -> Message { })), ..Default::default() }, - ClipbaordFile::FileContentsRequest { + ClipboardFile::FileContentsRequest { stream_id, list_index, dw_flags, @@ -102,7 +102,7 @@ pub fn clip_2_msg(clip: ClipbaordFile) -> Message { })), ..Default::default() }, - ClipbaordFile::FileContentsResponse { + ClipboardFile::FileContentsResponse { msg_flags, stream_id, requested_data, @@ -123,28 +123,28 @@ pub fn clip_2_msg(clip: ClipbaordFile) -> Message { } } -pub fn msg_2_clip(msg: Cliprdr) -> Option { +pub fn msg_2_clip(msg: Cliprdr) -> Option { match msg.union { - Some(cliprdr::Union::Ready(_)) => Some(ClipbaordFile::MonitorReady), + Some(cliprdr::Union::Ready(_)) => Some(ClipboardFile::MonitorReady), Some(cliprdr::Union::FormatList(data)) => { let mut format_list: Vec<(i32, String)> = Vec::new(); for v in data.formats.iter() { format_list.push((v.id, v.format.clone())); } - Some(ClipbaordFile::FormatList { format_list }) + Some(ClipboardFile::FormatList { format_list }) } - Some(cliprdr::Union::FormatListResponse(data)) => Some(ClipbaordFile::FormatListResponse { + Some(cliprdr::Union::FormatListResponse(data)) => Some(ClipboardFile::FormatListResponse { msg_flags: data.msg_flags, }), - Some(cliprdr::Union::FormatDataRequest(data)) => Some(ClipbaordFile::FormatDataRequest { + Some(cliprdr::Union::FormatDataRequest(data)) => Some(ClipboardFile::FormatDataRequest { requested_format_id: data.requested_format_id, }), - Some(cliprdr::Union::FormatDataResponse(data)) => Some(ClipbaordFile::FormatDataResponse { + Some(cliprdr::Union::FormatDataResponse(data)) => Some(ClipboardFile::FormatDataResponse { msg_flags: data.msg_flags, format_data: data.format_data.into(), }), Some(cliprdr::Union::FileContentsRequest(data)) => { - Some(ClipbaordFile::FileContentsRequest { + Some(ClipboardFile::FileContentsRequest { stream_id: data.stream_id, list_index: data.list_index, dw_flags: data.dw_flags, @@ -156,7 +156,7 @@ pub fn msg_2_clip(msg: Cliprdr) -> Option { }) } Some(cliprdr::Union::FileContentsResponse(data)) => { - Some(ClipbaordFile::FileContentsResponse { + Some(ClipboardFile::FileContentsResponse { msg_flags: data.msg_flags, stream_id: data.stream_id, requested_data: data.requested_data.into(), diff --git a/src/ipc.rs b/src/ipc.rs index c562225b4..9048db766 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -9,7 +9,7 @@ use parity_tokio_ipc::{ use serde_derive::{Deserialize, Serialize}; #[cfg(not(any(target_os = "android", target_os = "ios")))] -pub use clipboard::ClipbaordFile; +pub use clipboard::ClipboardFile; use hbb_common::{ allow_err, bail, bytes, bytes_codec::BytesCodec, @@ -191,7 +191,7 @@ pub enum Data { Test, SyncConfig(Option<(Config, Config2)>), #[cfg(not(any(target_os = "android", target_os = "ios")))] - ClipbaordFile(ClipbaordFile), + ClipboardFile(ClipboardFile), ClipboardFileEnabled(bool), PrivacyModeState((i32, PrivacyModeState)), TestRendezvousServer, diff --git a/src/server/connection.rs b/src/server/connection.rs index f91281a52..087dbde4c 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -319,7 +319,7 @@ impl Connection { allow_err!(conn.stream.send_raw(bytes).await); } #[cfg(windows)] - ipc::Data::ClipbaordFile(_clip) => { + ipc::Data::ClipboardFile(_clip) => { if conn.file_transfer_enabled() { allow_err!(conn.stream.send(&clip_2_msg(_clip)).await); } @@ -1309,7 +1309,7 @@ impl Connection { if self.file_transfer_enabled() { #[cfg(windows)] if let Some(clip) = msg_2_clip(_clip) { - self.send_to_cm(ipc::Data::ClipbaordFile(clip)) + self.send_to_cm(ipc::Data::ClipboardFile(clip)) } } } diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index 695d60417..a32662d07 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -253,7 +253,7 @@ impl IpcTaskRunner { if !pre_enabled && ContextSend::is_enabled() { allow_err!( self.stream - .send(&Data::ClipbaordFile(clipboard::ClipbaordFile::MonitorReady)) + .send(&Data::ClipboardFile(clipboard::ClipboardFile::MonitorReady)) .await ); } @@ -288,7 +288,7 @@ impl IpcTaskRunner { rx_clip = rx_clip1.lock().await; } else { let rx_clip2; - (_tx_clip, rx_clip2) = unbounded_channel::(); + (_tx_clip, rx_clip2) = unbounded_channel::(); rx_clip1 = Arc::new(TokioMutex::new(rx_clip2)); rx_clip = rx_clip1.lock().await; } @@ -354,7 +354,7 @@ impl IpcTaskRunner { } } #[cfg(windows)] - Data::ClipbaordFile(_clip) => { + Data::ClipboardFile(_clip) => { #[cfg(windows)] { let conn_id = self.conn_id; @@ -394,7 +394,7 @@ impl IpcTaskRunner { clip_file = rx_clip.recv() => match clip_file { Some(_clip) => { #[cfg(windows)] - allow_err!(self.tx.send(Data::ClipbaordFile(_clip))); + allow_err!(self.tx.send(Data::ClipboardFile(_clip))); } None => { // From 19046ba8677968a134dd8dd908a1927598a51d61 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:39:49 -0500 Subject: [PATCH 1403/2015] spelling: colorspace Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- src/platform/macos.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform/macos.rs b/src/platform/macos.rs index 70e38eb57..ae996b68a 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -331,7 +331,7 @@ pub fn get_cursor_data(hcursor: u64) -> ResultType { */ let mut colors: Vec = Vec::new(); colors.reserve((size.height * size.width) as usize * 4); - // TIFF is rgb colrspace, no need to convert + // TIFF is rgb colorspace, no need to convert // let cs: id = msg_send![class!(NSColorSpace), sRGBColorSpace]; for y in 0..(size.height as _) { for x in 0..(size.width as _) { From ec8cb0579f58dd9b797db9328cbc446985d4085a Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:22 -0500 Subject: [PATCH 1404/2015] spelling: common Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- libs/hbb_common/protos/message.proto | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index 650e42104..de0d6e7c1 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -503,7 +503,7 @@ message AudioFrame { // Notify peer to show message box. message MessageBox { - // Message type. Refer to flutter/lib/commom.dart/msgBox(). + // Message type. Refer to flutter/lib/common.dart/msgBox(). string msgtype = 1; string title = 2; // English From 5b3835d1fe8f70c53e5a151a8470d25489614b96 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:23 -0500 Subject: [PATCH 1405/2015] spelling: connecting Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- flutter/web/js/src/connection.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/web/js/src/connection.ts b/flutter/web/js/src/connection.ts index 2846d9078..ce6d26684 100644 --- a/flutter/web/js/src/connection.ts +++ b/flutter/web/js/src/connection.ts @@ -82,7 +82,7 @@ export default class Connection { this._ws = ws; this._id = id; console.log( - new Date() + ": Conntecting to rendezvoous server: " + uri + ", for " + id + new Date() + ": Connecting to rendezvoous server: " + uri + ", for " + id ); await ws.open(); console.log(new Date() + ": Connected to rendezvoous server"); From 51f736e84fc49e8f1e5eb8b6dffe9d4b257a42e0 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:22 -0500 Subject: [PATCH 1406/2015] spelling: connection Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- src/server/portable_service.rs | 2 +- src/ui_interface.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/server/portable_service.rs b/src/server/portable_service.rs index 6d2e92ae3..5d96a330e 100644 --- a/src/server/portable_service.rs +++ b/src/server/portable_service.rs @@ -407,7 +407,7 @@ pub mod server { } ConnCount(Some(n)) => { if n == 0 { - log::info!("Connnection count equals 0, exit"); + log::info!("Connection count equals 0, exit"); stream.send(&Data::DataPortableService(WillClose)).await.ok(); break; } diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 3b7d1c2c0..c628f0186 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -936,7 +936,7 @@ pub fn account_auth_result() -> String { serde_json::to_string(&account::OidcSession::get_result()).unwrap_or_default() } -// notice: avoiding create ipc connecton repeatly, +// notice: avoiding create ipc connection repeatly, // because windows named pipe has serious memory leak issue. #[tokio::main(flavor = "current_thread")] async fn check_connect_status_(reconnect: bool, rx: mpsc::UnboundedReceiver) { From 6ca852363ea28e6c32141064020cd9741d0139d1 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:23 -0500 Subject: [PATCH 1407/2015] spelling: control Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- libs/scrap/src/common/hwcodec.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/scrap/src/common/hwcodec.rs b/libs/scrap/src/common/hwcodec.rs index d92ed2a7d..9cd6077a6 100644 --- a/libs/scrap/src/common/hwcodec.rs +++ b/libs/scrap/src/common/hwcodec.rs @@ -16,7 +16,7 @@ use hwcodec::{ ffmpeg::{CodecInfo, CodecInfos, DataFormat}, AVPixelFormat, Quality::{self, *}, - RateContorl::{self, *}, + RateControl::{self, *}, }; use std::sync::{Arc, Mutex}; @@ -31,7 +31,7 @@ const DEFAULT_PIXFMT: AVPixelFormat = AVPixelFormat::AV_PIX_FMT_YUV420P; pub const DEFAULT_TIME_BASE: [i32; 2] = [1, 30]; const DEFAULT_GOP: i32 = 60; const DEFAULT_HW_QUALITY: Quality = Quality_Default; -const DEFAULT_RC: RateContorl = RC_DEFAULT; +const DEFAULT_RC: RateControl = RC_DEFAULT; pub struct HwEncoder { encoder: Encoder, From 8c901c258528fc8386a0059093f5681837b617ac Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:24 -0500 Subject: [PATCH 1408/2015] spelling: custom Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- libs/enigo/src/linux/nix_impl.rs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/libs/enigo/src/linux/nix_impl.rs b/libs/enigo/src/linux/nix_impl.rs index 47e6d53c0..e2e4bd4a9 100644 --- a/libs/enigo/src/linux/nix_impl.rs +++ b/libs/enigo/src/linux/nix_impl.rs @@ -13,7 +13,7 @@ pub struct Enigo { is_x11: bool, tfc: Option, custom_keyboard: Option, - cutsom_mouse: Option, + custom_mouse: Option, } impl Enigo { @@ -31,7 +31,7 @@ impl Enigo { } /// Set custom mouse. pub fn set_custom_mouse(&mut self, custom_mouse: CustomMouce) { - self.cutsom_mouse = Some(custom_mouse) + self.custom_mouse = Some(custom_mouse) } /// Get custom keyboard. pub fn get_custom_keyboard(&mut self) -> &mut Option { @@ -39,7 +39,7 @@ impl Enigo { } /// Get custom mouse. pub fn get_custom_mouse(&mut self) -> &mut Option { - &mut self.cutsom_mouse + &mut self.custom_mouse } fn tfc_key_down_or_up(&mut self, key: Key, down: bool, up: bool) -> bool { @@ -99,7 +99,7 @@ impl Default for Enigo { None }, custom_keyboard: None, - cutsom_mouse: None, + custom_mouse: None, xdo: EnigoXdo::default(), } } @@ -118,7 +118,7 @@ impl MouseControllable for Enigo { if self.is_x11 { self.xdo.mouse_move_to(x, y); } else { - if let Some(mouse) = &mut self.cutsom_mouse { + if let Some(mouse) = &mut self.custom_mouse { mouse.mouse_move_to(x, y) } } @@ -127,7 +127,7 @@ impl MouseControllable for Enigo { if self.is_x11 { self.xdo.mouse_move_relative(x, y); } else { - if let Some(mouse) = &mut self.cutsom_mouse { + if let Some(mouse) = &mut self.custom_mouse { mouse.mouse_move_relative(x, y) } } @@ -136,7 +136,7 @@ impl MouseControllable for Enigo { if self.is_x11 { self.xdo.mouse_down(button) } else { - if let Some(mouse) = &mut self.cutsom_mouse { + if let Some(mouse) = &mut self.custom_mouse { mouse.mouse_down(button) } else { Ok(()) @@ -147,7 +147,7 @@ impl MouseControllable for Enigo { if self.is_x11 { self.xdo.mouse_up(button) } else { - if let Some(mouse) = &mut self.cutsom_mouse { + if let Some(mouse) = &mut self.custom_mouse { mouse.mouse_up(button) } } @@ -156,7 +156,7 @@ impl MouseControllable for Enigo { if self.is_x11 { self.xdo.mouse_click(button) } else { - if let Some(mouse) = &mut self.cutsom_mouse { + if let Some(mouse) = &mut self.custom_mouse { mouse.mouse_click(button) } } @@ -165,7 +165,7 @@ impl MouseControllable for Enigo { if self.is_x11 { self.xdo.mouse_scroll_x(length) } else { - if let Some(mouse) = &mut self.cutsom_mouse { + if let Some(mouse) = &mut self.custom_mouse { mouse.mouse_scroll_x(length) } } @@ -174,7 +174,7 @@ impl MouseControllable for Enigo { if self.is_x11 { self.xdo.mouse_scroll_y(length) } else { - if let Some(mouse) = &mut self.cutsom_mouse { + if let Some(mouse) = &mut self.custom_mouse { mouse.mouse_scroll_y(length) } } From 919e42b1a1657ecbfb955990be24801dfb9bc3a0 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:24 -0500 Subject: [PATCH 1409/2015] spelling: device Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- src/client/helper.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/helper.rs b/src/client/helper.rs index d38fbf223..e4736c0e8 100644 --- a/src/client/helper.rs +++ b/src/client/helper.rs @@ -15,7 +15,7 @@ const MIN_LATENCY: i64 = 100; /// Only sync the audio to video, not the other way around. #[derive(Debug)] pub struct LatencyController { - last_video_remote_ts: i64, // generated on remote deivce + last_video_remote_ts: i64, // generated on remote device update_time: Instant, allow_audio: bool, } From 7ba932825d4d5f1e95f7b2fef476e457b6234093 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 03:10:42 -0500 Subject: [PATCH 1410/2015] spelling: distro Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- libs/hbb_common/src/platform/linux.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libs/hbb_common/src/platform/linux.rs b/libs/hbb_common/src/platform/linux.rs index 4c6375dd7..a6ae2a9e7 100644 --- a/libs/hbb_common/src/platform/linux.rs +++ b/libs/hbb_common/src/platform/linux.rs @@ -1,15 +1,15 @@ use crate::ResultType; lazy_static::lazy_static! { - pub static ref DISTRO: Disto = Disto::new(); + pub static ref DISTRO: Distro = Distro::new(); } -pub struct Disto { +pub struct Distro { pub name: String, pub version_id: String, } -impl Disto { +impl Distro { fn new() -> Self { let name = run_cmds("awk -F'=' '/^NAME=/ {print $2}' /etc/os-release".to_owned()) .unwrap_or_default() From 43b975bd355496207e3f2b7a5bb7df0cc5697f7f Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:25 -0500 Subject: [PATCH 1411/2015] spelling: elapsed Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- src/ui/cm.tis | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ui/cm.tis b/src/ui/cm.tis index a1d623322..4e46e217f 100644 --- a/src/ui/cm.tis +++ b/src/ui/cm.tis @@ -42,7 +42,7 @@ class Body: Reactor.Component
    {c.name}
    ({c.peer_id})
    {auth - ? {disconnected ? translate('Disconnected') : translate('Connected')}{" "}{getElaspsed(c.time, c.now)} + ? {disconnected ? translate('Disconnected') : translate('Connected')}{" "}{getElapsed(c.time, c.now)} : {translate('Request access to your device')}{"..."}}
    @@ -442,7 +442,7 @@ function self.ready() { view.move(sw - w, 0, w, h); } -function getElaspsed(time, now) { +function getElapsed(time, now) { var seconds = Date.diff(time, now, #seconds); var hours = seconds / 3600; var days = hours / 24; @@ -482,7 +482,7 @@ function updateTime() { if (el) { var c = connections[body.cur]; if (c && c.authorized && !c.disconnected) { - el.text = getElaspsed(c.time, c.now); + el.text = getElapsed(c.time, c.now); } } updateTime(); From 238de6231f0d1f40b9b1a1d32d3dd52ccb08543c Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:25 -0500 Subject: [PATCH 1412/2015] spelling: embraced Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- res/lang.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/lang.py b/res/lang.py index 37bbfb3b1..481d65553 100644 --- a/res/lang.py +++ b/res/lang.py @@ -45,7 +45,7 @@ def expand(): if line_strip.startswith('("'): k, v = line_split(line_strip) if k in dict: - # embrased with " to avoid empty v + # embraced with " to avoid empty v line = line.replace('"%s"'%v, '"%s"'%dict[k]) else: line = line.replace(v, "") From db45907e91e1993a3093607dadd799179c024637 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:26 -0500 Subject: [PATCH 1413/2015] spelling: environment Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- libs/virtual_display/dylib/src/win10/IddController.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/virtual_display/dylib/src/win10/IddController.h b/libs/virtual_display/dylib/src/win10/IddController.h index 767d64798..909f17423 100644 --- a/libs/virtual_display/dylib/src/win10/IddController.h +++ b/libs/virtual_display/dylib/src/win10/IddController.h @@ -134,7 +134,7 @@ const char* GetLastMsg(); * * @param b [in] TRUE to enable printing message. * - * @remark For now, no need to read evironment variable to check if should print. + * @remark For now, no need to read environment variable to check if should print. * */ VOID SetPrintErrMsg(BOOL b); From 53556ba06cde2e47d408b13e1bc61a114f24dcbf Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:26 -0500 Subject: [PATCH 1414/2015] spelling: essentially Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- libs/enigo/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/enigo/src/lib.rs b/libs/enigo/src/lib.rs index fcc2981fd..2794552bd 100644 --- a/libs/enigo/src/lib.rs +++ b/libs/enigo/src/lib.rs @@ -206,7 +206,7 @@ pub trait MouseControllable { /// Click a mouse button /// - /// it's esentially just a consecutive invokation of + /// it's essentially just a consecutive invokation of /// [mouse_down](trait.MouseControllable.html#tymethod.mouse_down) followed /// by a [mouse_up](trait.MouseControllable.html#tymethod.mouse_up). Just /// for From 87e7408cc3cf089b42c0e00b8011216c8c807251 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:27 -0500 Subject: [PATCH 1415/2015] spelling: exist Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- 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 087dbde4c..9d7470f47 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1857,8 +1857,8 @@ mod privacy_mode { pub(super) fn turn_on_privacy(_conn_id: i32) -> ResultType { #[cfg(windows)] { - let plugin_exitst = crate::ui::win_privacy::turn_on_privacy(_conn_id)?; - Ok(plugin_exitst) + let plugin_exist = crate::ui::win_privacy::turn_on_privacy(_conn_id)?; + Ok(plugin_exist) } #[cfg(not(windows))] { From a58303c8c2bd3dc2820f40bd9e76010654d77704 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 01:21:09 -0500 Subject: [PATCH 1416/2015] spelling: github Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- docs/CONTRIBUTING.md | 2 +- flutter/lib/common/widgets/login.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index f3165a684..31fd632e6 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -35,7 +35,7 @@ efforts from contributors on the same issue. - Add tests relevant to the fixed bug or new feature. -For specific git instructions, see [GitHub workflow 101](https://github.com/servo/servo/wiki/Github-workflow). +For specific git instructions, see [GitHub workflow 101](https://github.com/servo/servo/wiki/GitHub-workflow). ## Conduct diff --git a/flutter/lib/common/widgets/login.dart b/flutter/lib/common/widgets/login.dart index ce27ceb2c..15105ae61 100644 --- a/flutter/lib/common/widgets/login.dart +++ b/flutter/lib/common/widgets/login.dart @@ -538,7 +538,7 @@ Future loginDialog() async { ), LoginWidgetOP( ops: [ - ConfigOP(op: 'Github', iconWidth: 20), + ConfigOP(op: 'GitHub', iconWidth: 20), ConfigOP(op: 'Google', iconWidth: 20), ConfigOP(op: 'Okta', iconWidth: 38), ], From d8a6beccbb6328992f01e2d9c06de1d2c443f057 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:32:18 -0500 Subject: [PATCH 1417/2015] spelling: holder Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- src/server/connection.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/connection.rs b/src/server/connection.rs index 9d7470f47..0683b4867 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -148,7 +148,7 @@ impl Connection { ..Default::default() }; let (tx_from_cm_holder, mut rx_from_cm) = mpsc::unbounded_channel::(); - // holding tx_from_cm_holde to avoid cpu burning of rx_from_cm.recv when all sender closed + // holding tx_from_cm_holder to avoid cpu burning of rx_from_cm.recv when all sender closed let tx_from_cm = tx_from_cm_holder.clone(); let (tx_to_cm, rx_to_cm) = mpsc::unbounded_channel::(); let (tx, mut rx) = mpsc::unbounded_channel::<(Instant, Arc)>(); From 69595b7b67c3b74cecf9b91c5bec613d25fc7ea1 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:28 -0500 Subject: [PATCH 1418/2015] spelling: implementation Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- libs/enigo/src/linux/nix_impl.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/enigo/src/linux/nix_impl.rs b/libs/enigo/src/linux/nix_impl.rs index e2e4bd4a9..d9be7b43b 100644 --- a/libs/enigo/src/linux/nix_impl.rs +++ b/libs/enigo/src/linux/nix_impl.rs @@ -21,7 +21,7 @@ impl Enigo { pub fn delay(&self) -> u64 { self.xdo.delay() } - /// Set delay of xdo implemetation. + /// Set delay of xdo implementation. pub fn set_delay(&mut self, delay: u64) { self.xdo.set_delay(delay) } From c9e5e2cb53e1d25e025c19e27f5c30d529fea423 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:28 -0500 Subject: [PATCH 1419/2015] spelling: incoming Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- src/client/io_loop.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 1f81dfa55..5ec7890be 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -728,11 +728,11 @@ impl Remote { self.handler.adapt_size(); self.send_opts_after_login(peer).await; } - let incomming_format = CodecFormat::from(&vf); - if self.video_format != incomming_format { - self.video_format = incomming_format.clone(); + let incoming_format = CodecFormat::from(&vf); + if self.video_format != incoming_format { + self.video_format = incoming_format.clone(); self.handler.update_quality_status(QualityStatus { - codec_format: Some(incomming_format), + codec_format: Some(incoming_format), ..Default::default() }) }; From fc4d2e4b3ecb9ca1cfeb07f1c0650a3fa1e26113 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 01:20:20 -0500 Subject: [PATCH 1420/2015] spelling: into Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- flutter/linux/CMakeLists.txt | 2 +- flutter/windows/CMakeLists.txt | 2 +- flutter/windows/runner/win32_window.h | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/flutter/linux/CMakeLists.txt b/flutter/linux/CMakeLists.txt index c03d4c576..a9fd84088 100644 --- a/flutter/linux/CMakeLists.txt +++ b/flutter/linux/CMakeLists.txt @@ -9,7 +9,7 @@ set(BINARY_NAME "rustdesk") # https://wiki.gnome.org/HowDoI/ChooseApplicationID set(APPLICATION_ID "com.carriez.flutter_hbb") -# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# Explicitly opt into modern CMake behaviors to avoid warnings with recent # versions of CMake. cmake_policy(SET CMP0063 NEW) diff --git a/flutter/windows/CMakeLists.txt b/flutter/windows/CMakeLists.txt index 5cf603360..926941b84 100644 --- a/flutter/windows/CMakeLists.txt +++ b/flutter/windows/CMakeLists.txt @@ -6,7 +6,7 @@ project(rustdesk LANGUAGES CXX) # the on-disk name of your application. set(BINARY_NAME "rustdesk") -# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# Explicitly opt into modern CMake behaviors to avoid warnings with recent # versions of CMake. cmake_policy(SET CMP0063 NEW) diff --git a/flutter/windows/runner/win32_window.h b/flutter/windows/runner/win32_window.h index 77e52ff01..176791120 100644 --- a/flutter/windows/runner/win32_window.h +++ b/flutter/windows/runner/win32_window.h @@ -31,7 +31,7 @@ class Win32Window { // Creates and shows a win32 window with |title| and position and size using // |origin| and |size|. New windows are created on the default monitor. Window // sizes are specified to the OS in physical pixels, hence to ensure a - // consistent size to will treat the width height passed in to this function + // consistent size to will treat the width height passed into this function // as logical pixels and scale to appropriate for the default monitor. Returns // true if the window was created successfully. bool CreateAndShow(const std::wstring& title, From f91daf046a4c3415a89b551c46d805f55bd1a524 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:28 -0500 Subject: [PATCH 1421/2015] spelling: invocation Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- libs/enigo/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/enigo/src/lib.rs b/libs/enigo/src/lib.rs index 2794552bd..f55f3dacd 100644 --- a/libs/enigo/src/lib.rs +++ b/libs/enigo/src/lib.rs @@ -206,7 +206,7 @@ pub trait MouseControllable { /// Click a mouse button /// - /// it's essentially just a consecutive invokation of + /// it's essentially just a consecutive invocation of /// [mouse_down](trait.MouseControllable.html#tymethod.mouse_down) followed /// by a [mouse_up](trait.MouseControllable.html#tymethod.mouse_up). Just /// for From f851c5213a53acdd733aa852c4c7822bade616bd Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 01:21:04 -0500 Subject: [PATCH 1422/2015] spelling: javascript Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 79255e455..bc9bacf19 100644 --- a/README.md +++ b/README.md @@ -198,7 +198,7 @@ Please ensure that you are running these commands from the root of the RustDesk - **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Communicate with [rustdesk-server](https://github.com/rustdesk/rustdesk-server), wait for remote direct (TCP hole punching) or relayed connection - **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: platform specific code - **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter code for mobile -- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Javascript for Flutter web client +- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript for Flutter web client ## Snapshot From 0fb825000015e1929b86868d44ee24d369aa6604 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:29 -0500 Subject: [PATCH 1423/2015] spelling: label Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- .../lib/desktop/pages/desktop_setting_page.dart | 4 ++-- flutter/lib/desktop/pages/port_forward_page.dart | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index ac92da14c..9f2dc988e 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -1436,7 +1436,7 @@ Widget _lock( _LabeledTextField( BuildContext context, - String lable, + String label, TextEditingController controller, String errorText, bool enabled, @@ -1447,7 +1447,7 @@ _LabeledTextField( Expanded( flex: 4, child: Text( - '${translate(lable)}:', + '${translate(label)}:', textAlign: TextAlign.right, style: TextStyle(color: _disabledTextColor(context, enabled)), ), diff --git a/flutter/lib/desktop/pages/port_forward_page.dart b/flutter/lib/desktop/pages/port_forward_page.dart index b2458d096..f513a1c6a 100644 --- a/flutter/lib/desktop/pages/port_forward_page.dart +++ b/flutter/lib/desktop/pages/port_forward_page.dart @@ -127,8 +127,8 @@ class _PortForwardPageState extends State } buildTunnel(BuildContext context) { - text(String lable) => Expanded( - child: Text(translate(lable)).marginOnly(left: _kTextLeftMargin)); + text(String label) => Expanded( + child: Text(translate(label)).marginOnly(left: _kTextLeftMargin)); return Theme( data: Theme.of(context) @@ -241,8 +241,8 @@ class _PortForwardPageState extends State } Widget buildTunnelDataRow(BuildContext context, _PortForward pf, int index) { - text(String lable) => Expanded( - child: Text(lable, style: const TextStyle(fontSize: 20)) + text(String label) => Expanded( + child: Text(label, style: const TextStyle(fontSize: 20)) .marginOnly(left: _kTextLeftMargin)); return Container( @@ -285,11 +285,11 @@ class _PortForwardPageState extends State } buildRdp(BuildContext context) { - text1(String lable) => Expanded( - child: Text(translate(lable)).marginOnly(left: _kTextLeftMargin)); - text2(String lable) => Expanded( + text1(String label) => Expanded( + child: Text(translate(label)).marginOnly(left: _kTextLeftMargin)); + text2(String label) => Expanded( child: Text( - lable, + label, style: const TextStyle(fontSize: 20), ).marginOnly(left: _kTextLeftMargin)); return Theme( From 38b5af5362cde55da1c2bb49ba3a73cfa0bb0753 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:29 -0500 Subject: [PATCH 1424/2015] spelling: latency Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- src/client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.rs b/src/client.rs index 440c0b0b0..cd05586fc 100644 --- a/src/client.rs +++ b/src/client.rs @@ -825,7 +825,7 @@ impl VideoHandler { /// Handle a new video frame. pub fn handle_frame(&mut self, vf: VideoFrame) -> ResultType { if vf.timestamp != 0 { - // Update the lantency controller with the latest timestamp. + // Update the latency controller with the latest timestamp. self.latency_controller .lock() .unwrap() From 1a5eed576851c5879175ea002e98fd84bae5a830 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:30 -0500 Subject: [PATCH 1425/2015] spelling: launched Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- src/ui/macos.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ui/macos.rs b/src/ui/macos.rs index ab3fb9079..835fd87b0 100644 --- a/src/ui/macos.rs +++ b/src/ui/macos.rs @@ -42,7 +42,7 @@ impl DelegateState { } } -static mut LAUCHED: bool = false; +static mut LAUNCHED: bool = false; impl AppHandler for Rc { fn command(&mut self, cmd: u32) { @@ -109,7 +109,7 @@ unsafe fn set_delegate(handler: Option>) { extern "C" fn application_did_finish_launching(_this: &mut Object, _: Sel, _notification: id) { unsafe { - LAUCHED = true; + LAUNCHED = true; } unsafe { let () = msg_send![NSApp(), activateIgnoringOtherApps: YES]; @@ -122,7 +122,7 @@ extern "C" fn application_should_handle_open_untitled_file( _sender: id, ) -> BOOL { unsafe { - if !LAUCHED { + if !LAUNCHED { return YES; } hbb_common::log::debug!("icon clicked on finder"); From 751aa26d8c0657d9a6e7a228b6869d856487f5d0 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:30 -0500 Subject: [PATCH 1426/2015] spelling: memory Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- src/server/portable_service.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/portable_service.rs b/src/server/portable_service.rs index 5d96a330e..4e3d3d1de 100644 --- a/src/server/portable_service.rs +++ b/src/server/portable_service.rs @@ -756,7 +756,7 @@ pub mod client { log::info!("portable service status mismatch"); } if portable_service_running { - log::info!("Create shared memeory capturer"); + log::info!("Create shared memory capturer"); return Ok(Box::new(CapturerPortable::new(current_display, use_yuv))); } else { log::debug!("Create capturer dxgi|gdi"); From 44ead53bc37e633d481a4fc0c36e8b9d030764e5 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:31 -0500 Subject: [PATCH 1427/2015] spelling: minimized Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- flutter/lib/desktop/pages/connection_page.dart | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 85749a256..2dae03250 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -44,7 +44,7 @@ class _ConnectionPageState extends State var svcStatusCode = 0.obs; var svcIsUsingPublicServer = true.obs; - bool isWindowMinisized = false; + bool isWindowMinimized = false; @override void initState() { @@ -80,13 +80,13 @@ class _ConnectionPageState extends State void onWindowEvent(String eventName) { super.onWindowEvent(eventName); if (eventName == 'minimize') { - isWindowMinisized = true; + isWindowMinimized = true; } else if (eventName == 'maximize' || eventName == 'restore') { - if (isWindowMinisized && Platform.isWindows) { - // windows can't update when minisized. + if (isWindowMinimized && Platform.isWindows) { + // windows can't update when minimized. Get.forceAppUpdate(); } - isWindowMinisized = false; + isWindowMinimized = false; } } From ad7640bb0e511751ec746d61fb39e242d297e518 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 01:20:59 -0500 Subject: [PATCH 1428/2015] spelling: nonexistent Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- flutter/linux/flutter/CMakeLists.txt | 2 +- flutter/windows/flutter/CMakeLists.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/flutter/linux/flutter/CMakeLists.txt b/flutter/linux/flutter/CMakeLists.txt index d5bd01648..52af0069a 100644 --- a/flutter/linux/flutter/CMakeLists.txt +++ b/flutter/linux/flutter/CMakeLists.txt @@ -70,7 +70,7 @@ target_link_libraries(flutter INTERFACE add_dependencies(flutter flutter_assemble) # === Flutter tool backend === -# _phony_ is a non-existent file to force this command to run every time, +# _phony_ is a nonexistent file to force this command to run every time, # since currently there's no way to get a full input/output list from the # flutter tool. add_custom_command( diff --git a/flutter/windows/flutter/CMakeLists.txt b/flutter/windows/flutter/CMakeLists.txt index 930d2071a..b5655b2fa 100644 --- a/flutter/windows/flutter/CMakeLists.txt +++ b/flutter/windows/flutter/CMakeLists.txt @@ -79,7 +79,7 @@ target_include_directories(flutter_wrapper_app PUBLIC add_dependencies(flutter_wrapper_app flutter_assemble) # === Flutter tool backend === -# _phony_ is a non-existent file to force this command to run every time, +# _phony_ is a nonexistent file to force this command to run every time, # since currently there's no way to get a full input/output list from the # flutter tool. set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") From d6495f3e086c0a63a99a003c66f6f7789b272763 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:31 -0500 Subject: [PATCH 1429/2015] spelling: pformatetc Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- libs/clipboard/src/windows/wf_cliprdr.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/clipboard/src/windows/wf_cliprdr.c b/libs/clipboard/src/windows/wf_cliprdr.c index 00ef7254e..a66150c40 100644 --- a/libs/clipboard/src/windows/wf_cliprdr.c +++ b/libs/clipboard/src/windows/wf_cliprdr.c @@ -795,11 +795,11 @@ static HRESULT STDMETHODCALLTYPE CliprdrDataObject_QueryGetData(IDataObject *Thi } static HRESULT STDMETHODCALLTYPE CliprdrDataObject_GetCanonicalFormatEtc(IDataObject *This, - FORMATETC *pformatectIn, + FORMATETC *pformatetcIn, FORMATETC *pformatetcOut) { (void)This; - (void)pformatectIn; + (void)pformatetcIn; if (!pformatetcOut) return E_INVALIDARG; From 4993635652b300c349414ea27aa97b6ead371956 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:32 -0500 Subject: [PATCH 1430/2015] spelling: platforms Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- libs/enigo/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/enigo/src/lib.rs b/libs/enigo/src/lib.rs index f55f3dacd..cd2208431 100644 --- a/libs/enigo/src/lib.rs +++ b/libs/enigo/src/lib.rs @@ -19,7 +19,7 @@ //! or any other "special" key on the Linux, macOS and Windows operating system. //! //! Possible use cases could be for testing user interfaces on different -//! plattforms, +//! platforms, //! building remote control applications or just automating tasks for user //! interfaces unaccessible by a public API or scripting language. //! From 33c3489a9eeaf1a0bac7f7c525465eeb325cb4d8 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:58:02 -0500 Subject: [PATCH 1431/2015] spelling: prefer Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- libs/hbb_common/protos/message.proto | 4 ++-- libs/scrap/src/common/codec.rs | 36 ++++++++++++++-------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index de0d6e7c1..e39bc7c6a 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -445,7 +445,7 @@ enum ImageQuality { } message VideoCodecState { - enum PerferCodec { + enum PreferCodec { Auto = 0; VPX = 1; H264 = 2; @@ -455,7 +455,7 @@ message VideoCodecState { int32 score_vpx = 1; int32 score_h264 = 2; int32 score_h265 = 3; - PerferCodec perfer = 4; + PreferCodec prefer = 4; } message OptionMessage { diff --git a/libs/scrap/src/common/codec.rs b/libs/scrap/src/common/codec.rs index 9535e9f3a..acfd4c674 100644 --- a/libs/scrap/src/common/codec.rs +++ b/libs/scrap/src/common/codec.rs @@ -23,7 +23,7 @@ use hbb_common::{ use hbb_common::{ config::{Config2, PeerConfig}, lazy_static, - message_proto::video_codec_state::PerferCodec, + message_proto::video_codec_state::PreferCodec, }; #[cfg(feature = "hwcodec")] @@ -149,29 +149,29 @@ impl Encoder { && states.iter().all(|(_, s)| s.score_h265 > 0); // Preference first - let mut preference = PerferCodec::Auto; + let mut preference = PreferCodec::Auto; let preferences: Vec<_> = states .iter() .filter(|(_, s)| { - s.perfer == PerferCodec::VPX.into() - || s.perfer == PerferCodec::H264.into() && enabled_h264 - || s.perfer == PerferCodec::H265.into() && enabled_h265 + s.prefer == PreferCodec::VPX.into() + || s.prefer == PreferCodec::H264.into() && enabled_h264 + || s.prefer == PreferCodec::H265.into() && enabled_h265 }) - .map(|(_, s)| s.perfer) + .map(|(_, s)| s.prefer) .collect(); if preferences.len() > 0 && preferences.iter().all(|&p| p == preferences[0]) { - preference = preferences[0].enum_value_or(PerferCodec::Auto); + preference = preferences[0].enum_value_or(PreferCodec::Auto); } match preference { - PerferCodec::VPX => *name.lock().unwrap() = None, - PerferCodec::H264 => { + PreferCodec::VPX => *name.lock().unwrap() = None, + PreferCodec::H264 => { *name.lock().unwrap() = best.h264.map_or(None, |c| Some(c.name)) } - PerferCodec::H265 => { + PreferCodec::H265 => { *name.lock().unwrap() = best.h265.map_or(None, |c| Some(c.name)) } - PerferCodec::Auto => { + PreferCodec::Auto => { // score encoder let mut score_vpx = SCORE_VPX; let mut score_h264 = best.h264.as_ref().map_or(0, |c| c.score); @@ -252,7 +252,7 @@ impl Decoder { score_vpx: SCORE_VPX, score_h264: best.h264.map_or(0, |c| c.score), score_h265: best.h265.map_or(0, |c| c.score), - perfer: Self::codec_preference(_id).into(), + prefer: Self::codec_preference(_id).into(), ..Default::default() }; } @@ -272,7 +272,7 @@ impl Decoder { score_vpx: SCORE_VPX, score_h264, score_h265, - perfer: Self::codec_preference(_id).into(), + prefer: Self::codec_preference(_id).into(), ..Default::default() }; } @@ -405,19 +405,19 @@ impl Decoder { } #[cfg(any(feature = "hwcodec", feature = "mediacodec"))] - fn codec_preference(id: &str) -> PerferCodec { + fn codec_preference(id: &str) -> PreferCodec { let codec = PeerConfig::load(id) .options .get("codec-preference") .map_or("".to_owned(), |c| c.to_owned()); if codec == "vp9" { - PerferCodec::VPX + PreferCodec::VPX } else if codec == "h264" { - PerferCodec::H264 + PreferCodec::H264 } else if codec == "h265" { - PerferCodec::H265 + PreferCodec::H265 } else { - PerferCodec::Auto + PreferCodec::Auto } } } From 1011568fc12664ba320d0a508434842c5c356617 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:32 -0500 Subject: [PATCH 1432/2015] spelling: privileged Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- src/platform/macos.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform/macos.rs b/src/platform/macos.rs index ae996b68a..204993c13 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -440,7 +440,7 @@ pub fn start_os_service() { .status() .ok(); println!("The others killed"); - // launchctl load/unload/start agent not work in daemon, show not priviledged. + // launchctl load/unload/start agent not work in daemon, show not privileged. // sudo launchctl asuser 501 open -n also not allowed. std::process::Command::new("launchctl") .args(&[ From 958c0ad18b94205e482fd5156a903fb132dba765 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:33 -0500 Subject: [PATCH 1433/2015] spelling: receiving Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- src/client/io_loop.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 5ec7890be..71d353c88 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -915,7 +915,7 @@ impl Remote { } }, Err(err) => { - println!("error recving digest: {}", err); + println!("error receiving digest: {}", err); } } } From c89e104f3ec533e7fd1782059f17bd33275661ea Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:33 -0500 Subject: [PATCH 1434/2015] spelling: regardless Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- libs/enigo/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/enigo/src/lib.rs b/libs/enigo/src/lib.rs index cd2208431..509bbf97c 100644 --- a/libs/enigo/src/lib.rs +++ b/libs/enigo/src/lib.rs @@ -468,7 +468,7 @@ pub trait KeyboardControllable { /// Emits keystrokes such that the given string is inputted. /// /// You can use many unicode here like: ❤️. This works - /// regadless of the current keyboardlayout. + /// regardless of the current keyboardlayout. /// /// # Example /// From f43b8b23a88ed711b02188277495596e70f30324 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:34 -0500 Subject: [PATCH 1435/2015] spelling: registrar Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- flutter/windows/runner/win32_window.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/windows/runner/win32_window.cpp b/flutter/windows/runner/win32_window.cpp index 2ff6d686c..c15819df0 100644 --- a/flutter/windows/runner/win32_window.cpp +++ b/flutter/windows/runner/win32_window.cpp @@ -43,7 +43,7 @@ class WindowClassRegistrar { public: ~WindowClassRegistrar() = default; - // Returns the singleton registar instance. + // Returns the singleton registrar instance. static WindowClassRegistrar* GetInstance() { if (!instance_) { instance_ = new WindowClassRegistrar(); From 996ed5398e8fdfb037d736e34a3af5e990e277d8 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:34 -0500 Subject: [PATCH 1436/2015] spelling: rendezvous Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- flutter/web/js/src/connection.ts | 4 ++-- src/rendezvous_mediator.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/flutter/web/js/src/connection.ts b/flutter/web/js/src/connection.ts index ce6d26684..b0c479c90 100644 --- a/flutter/web/js/src/connection.ts +++ b/flutter/web/js/src/connection.ts @@ -82,10 +82,10 @@ export default class Connection { this._ws = ws; this._id = id; console.log( - new Date() + ": Connecting to rendezvoous server: " + uri + ", for " + id + new Date() + ": Connecting to rendezvous server: " + uri + ", for " + id ); await ws.open(); - console.log(new Date() + ": Connected to rendezvoous server"); + console.log(new Date() + ": Connected to rendezvous server"); const conn_type = rendezvous.ConnType.DEFAULT_CONN; const nat_type = rendezvous.NatType.SYMMETRIC; const punch_hole_request = rendezvous.PunchHoleRequest.fromPartial({ diff --git a/src/rendezvous_mediator.rs b/src/rendezvous_mediator.rs index 73c017e2e..8b7dae1ba 100644 --- a/src/rendezvous_mediator.rs +++ b/src/rendezvous_mediator.rs @@ -469,10 +469,10 @@ impl RendezvousMediator { Ok(()) } - fn get_relay_server(&self, provided_by_rendzvous_server: String) -> String { + fn get_relay_server(&self, provided_by_rendezvous_server: String) -> String { let mut relay_server = Config::get_option("relay-server"); if relay_server.is_empty() { - relay_server = provided_by_rendzvous_server; + relay_server = provided_by_rendezvous_server; } if relay_server.is_empty() { relay_server = crate::increase_port(&self.host, 1); From 37b0ac6e479d6a49acef237f9e38daad5999837c Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:35 -0500 Subject: [PATCH 1437/2015] spelling: repeatedly Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- src/ui_interface.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui_interface.rs b/src/ui_interface.rs index c628f0186..9984198b8 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -936,7 +936,7 @@ pub fn account_auth_result() -> String { serde_json::to_string(&account::OidcSession::get_result()).unwrap_or_default() } -// notice: avoiding create ipc connection repeatly, +// notice: avoiding create ipc connection repeatedly, // because windows named pipe has serious memory leak issue. #[tokio::main(flavor = "current_thread")] async fn check_connect_status_(reconnect: bool, rx: mpsc::UnboundedReceiver) { From a0b73b96da2cd8939337c3ade34dafe52a0a80ec Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:35 -0500 Subject: [PATCH 1438/2015] spelling: responds Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- flutter/windows/runner/win32_window.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/windows/runner/win32_window.h b/flutter/windows/runner/win32_window.h index 176791120..94a7bcd56 100644 --- a/flutter/windows/runner/win32_window.h +++ b/flutter/windows/runner/win32_window.h @@ -77,7 +77,7 @@ class Win32Window { // OS callback called by message pump. Handles the WM_NCCREATE message which // is passed when the non-client area is being created and enables automatic // non-client DPI scaling so that the non-client area automatically - // responsponds to changes in DPI. All other messages are handled by + // responds to changes in DPI. All other messages are handled by // MessageHandler. static LRESULT CALLBACK WndProc(HWND const window, UINT const message, From 3949e3162c4c66e1f34c5ecd304f4119c16d647e Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:36 -0500 Subject: [PATCH 1439/2015] spelling: rotation Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- libs/scrap/src/dxgi/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/scrap/src/dxgi/mod.rs b/libs/scrap/src/dxgi/mod.rs index 5829686b5..152f502a3 100644 --- a/libs/scrap/src/dxgi/mod.rs +++ b/libs/scrap/src/dxgi/mod.rs @@ -262,7 +262,7 @@ impl Capturer { _ => { return Err(io::Error::new( io::ErrorKind::Other, - "Unknown roration".to_string(), + "Unknown rotation".to_string(), )); } }; From d6bc1d4b8ad593fe55c73f7012b72f8b124a8c50 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:36 -0500 Subject: [PATCH 1440/2015] spelling: selection Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- src/server/connection.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/connection.rs b/src/server/connection.rs index 0683b4867..cbd130233 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -593,7 +593,7 @@ impl Connection { Some(data) = rx_from_cm.recv() => { match data { ipc::Data::Close => { - bail!("Close requested from selfection manager"); + bail!("Close requested from selection manager"); } _ => {} } From 7b047a32abd14e4423d5f6ff8e225d0cdfc1d246 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:37 -0500 Subject: [PATCH 1441/2015] spelling: separate Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- src/server/portable_service.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/portable_service.rs b/src/server/portable_service.rs index 4e3d3d1de..6b87da21b 100644 --- a/src/server/portable_service.rs +++ b/src/server/portable_service.rs @@ -203,7 +203,7 @@ mod utils { } } -// functions called in seperate SYSTEM user process. +// functions called in separate SYSTEM user process. pub mod server { use super::*; From e29866edc97128e09bb6ac30c77627ca26149d07 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:37 -0500 Subject: [PATCH 1442/2015] spelling: separated Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- src/lang/en.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/en.rs b/src/lang/en.rs index b718fc0f9..14d221ef3 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -13,7 +13,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("setup_server_tip", "For faster connection, please set up your own server"), ("Auto Login", "Auto Login (Only valid if you set \"Lock after session end\")"), ("whitelist_tip", "Only whitelisted IP can access me"), - ("whitelist_sep", "Seperated by comma, semicolon, spaces or new line"), + ("whitelist_sep", "Separated by comma, semicolon, spaces or new line"), ("Wrong credentials", "Wrong username or password"), ("invalid_http", "must start with http:// or https://"), ("install_daemon_tip", "For starting on boot, you need to install system service."), From a6b672848befd0427a7488309aa970158530c2ba Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:38 -0500 Subject: [PATCH 1443/2015] spelling: settings Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- src/platform/linux.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 3eb8f0b87..0e4f98d52 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -707,9 +707,9 @@ pub fn get_double_click_time() -> u32 { unsafe { let mut double_click_time = 0u32; let property = std::ffi::CString::new("gtk-double-click-time").unwrap(); - let setings = gtk_settings_get_default(); + let settings = gtk_settings_get_default(); g_object_get( - setings, + settings, property.as_ptr(), &mut double_click_time as *mut u32, 0 as *const libc::c_void, From 5e4ca9ef927662e3239b6c25902fe7b51977c41a Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:38 -0500 Subject: [PATCH 1444/2015] spelling: valid Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- libs/portable/src/bin_reader.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/portable/src/bin_reader.rs b/libs/portable/src/bin_reader.rs index 2d0b1bf7e..0a6cd8ef9 100644 --- a/libs/portable/src/bin_reader.rs +++ b/libs/portable/src/bin_reader.rs @@ -74,7 +74,7 @@ impl BinaryReader { assert!(BIN_DATA.len() > IDENTIFIER_LENGTH, "bin data invalid!"); let mut iden = String::from_utf8_lossy(&BIN_DATA[base..base + IDENTIFIER_LENGTH]); if iden != "rustdesk" { - panic!("bin file is not vaild!"); + panic!("bin file is not valid!"); } base += IDENTIFIER_LENGTH; loop { From 128aa17476e40d333a9aa213cc0025f283bb1c24 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:39 -0500 Subject: [PATCH 1445/2015] spelling: visible Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- flutter/lib/desktop/widgets/tabbar_widget.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 91ce6cce6..d428bcb9b 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -906,7 +906,7 @@ class _TabState extends State<_Tab> with RestorationMixin { children: [ _buildTabContent(), Obx((() => _CloseButton( - visiable: hover.value && widget.closable, + visible: hover.value && widget.closable, tabSelected: isSelected, onClose: () => widget.onClose(), ))) @@ -938,13 +938,13 @@ class _TabState extends State<_Tab> with RestorationMixin { } class _CloseButton extends StatelessWidget { - final bool visiable; + final bool visible; final bool tabSelected; final Function onClose; const _CloseButton({ Key? key, - required this.visiable, + required this.visible, required this.tabSelected, required this.onClose, }) : super(key: key); @@ -954,7 +954,7 @@ class _CloseButton extends StatelessWidget { return SizedBox( width: _kIconSize, child: Offstage( - offstage: !visiable, + offstage: !visible, child: InkWell( customBorder: const RoundedRectangleBorder(), onTap: () => onClose(), From cd921987e9558b32d79f80ffb78966345a9c2a49 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 9 Jan 2023 02:30:39 -0500 Subject: [PATCH 1446/2015] spelling: whitelist Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- 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 cbd130233..c29faa724 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -663,7 +663,7 @@ impl Connection { self.send_login_error("Your ip is blocked by the peer") .await; Self::post_alarm_audit( - AlarmAuditType::IpWhiltelist, //"ip whiltelist", + AlarmAuditType::IpWhitelist, //"ip whitelist", true, json!({ "ip":addr.ip(), @@ -1875,7 +1875,7 @@ struct ConnAuditResponse { } pub enum AlarmAuditType { - IpWhiltelist = 0, + IpWhitelist = 0, ManyWrongPassword = 1, FrequentAttempt = 2, } From 21438894040fca679f4724ca9f715ecd7e08e647 Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 10 Jan 2023 11:09:08 +0800 Subject: [PATCH 1447/2015] fix win scancode filter, ignore 0xE0.. Signed-off-by: fufesou --- src/keyboard.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/keyboard.rs b/src/keyboard.rs index f29eb27bc..a11b0e97e 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -617,7 +617,8 @@ pub fn map_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option { // https://github.com/rustdesk/rustdesk/issues/1371 - if event.scan_code > 255 { + // Filter scancodes that are greater than 255 and the hight word is not 0xE0. + if event.scan_code > 255 && (event.scan_code >> 8) != 0xE0 { return None; } event.scan_code From 6f455be347dd2954c5da78bd00323b2985c869dd Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sun, 8 Jan 2023 16:27:03 +0800 Subject: [PATCH 1448/2015] opt: set title for all windows --- flutter/lib/main.dart | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 6fd205a22..6593f1804 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -61,6 +61,8 @@ Future main(List args) async { kAppTypeDesktopRemote, 'RustDesk - Remote Desktop', ); + WindowController.fromWindowId(windowId!) + .setTitle('RustDesk - Remote Desktop'); break; case WindowType.FileTransfer: desktopType = DesktopType.fileTransfer; @@ -69,6 +71,8 @@ Future main(List args) async { kAppTypeDesktopFileTransfer, 'RustDesk - File Transfer', ); + WindowController.fromWindowId(windowId!) + .setTitle('RustDesk - File Transfer'); break; case WindowType.PortForward: desktopType = DesktopType.portForward; @@ -135,6 +139,7 @@ void runMainApp(bool startService) async { windowManager.waitUntilReadyToShow(windowOptions, () async { windowManager.setOpacity(1); }); + windowManager.setTitle("RustDesk"); } void runMobileApp() async { @@ -198,6 +203,7 @@ void runMultiWindow( } // show window from hidden status WindowController.fromWindowId(windowId!).show(); + WindowController.fromWindowId(windowId!).setTitle(title); } void runConnectionManagerScreen(bool hide) async { From 3f3e71c1f9f48a6dbfb731626bac150118b2d827 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sun, 8 Jan 2023 17:42:18 +0800 Subject: [PATCH 1449/2015] feat: add arm64 appimage build --- .github/workflows/flutter-nightly.yml | 41 ++++++++- appimage/AppImageBuilder-aarch64.yml | 85 +++++++++++++++++++ ...Builder.yml => AppImageBuilder-x86_64.yml} | 0 3 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 appimage/AppImageBuilder-aarch64.yml rename appimage/{AppImageBuilder.yml => AppImageBuilder-x86_64.yml} (100%) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index eb13edf15..aaafede07 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -761,6 +761,13 @@ jobs: use-cross: true, extra-build-features: "", } + - { + arch: aarch64, + target: aarch64-unknown-linux-gnu, + os: ubuntu-18.04, # just for naming package, not running host + use-cross: true, + extra-build-features: "appimage", + } # - { arch: aarch64, target: aarch64-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true, extra-build-features: "flatpak" } # - { # arch: armv7, @@ -939,6 +946,13 @@ jobs: use-cross: true, extra-build-features: "", } + - { + arch: aarch64, + target: aarch64-unknown-linux-gnu, + os: ubuntu-18.04, # just for naming package, not running host + use-cross: true, + extra-build-features: "appimage", + } # - { # arch: aarch64, # target: aarch64-unknown-linux-gnu, @@ -1046,7 +1060,7 @@ jobs: shell: bash run: | for name in rustdesk*??.deb; do - mv "$name" "${name%%.deb}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb" + cp "$name" "${name%%.deb}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb" done - name: Publish debian package @@ -1057,6 +1071,29 @@ jobs: tag_name: ${{ env.TAG_NAME }} files: | rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb + + - name: Build appimage package + if: ${{ matrix.job.extra-build-features == 'appimage' }} + shell: bash + run: | + # set-up appimage-builder + pushd /tmp + wget -O appimage-builder-x86_64.AppImage https://github.com/AppImageCrafters/appimage-builder/releases/download/v1.1.0/appimage-builder-1.1.0-x86_64.AppImage + chmod +x appimage-builder-x86_64.AppImage + sudo mv appimage-builder-x86_64.AppImage /usr/local/bin/appimage-builder + popd + # run appimage-builder + pushd appimage + sudo appimage-builder --skip-tests --recipe ./AppImageBuilder-${{ matrix.job.arch }}.yml + + - name: Publish appimage package + if: ${{ matrix.job.extra-build-features == 'appimage' }} + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + ./appimage/rustdesk-${{ env.VERSION }}-*.AppImage - name: Upload Artifact uses: actions/upload-artifact@master @@ -1310,7 +1347,7 @@ jobs: popd # run appimage-builder pushd appimage - sudo appimage-builder --skip-tests + sudo appimage-builder --skip-tests --recipe ./AppImageBuilder-x86_64.yml - name: Publish appimage package if: ${{ matrix.job.extra-build-features == 'appimage' }} diff --git a/appimage/AppImageBuilder-aarch64.yml b/appimage/AppImageBuilder-aarch64.yml new file mode 100644 index 000000000..12d737718 --- /dev/null +++ b/appimage/AppImageBuilder-aarch64.yml @@ -0,0 +1,85 @@ +# appimage-builder recipe see https://appimage-builder.readthedocs.io for details +version: 1 +script: + - rm -rf ./AppDir || true + - bsdtar -zxvf ../rustdesk-1.2.0.deb + - tar -xvf ./data.tar.xz + - mkdir ./AppDir + - mv ./usr ./AppDir/usr + # 32x32 icon + - for i in {32,64,128}; do mkdir -p ./AppDir/usr/share/icons/hicolor/$i\x$i/apps/; cp ../res/$i\x$i.png ./AppDir/usr/share/icons/hicolor/$i\x$i/apps/rustdesk.png; done + # desktop file + # - sed -i "s/Icon=\/usr\/share\/rustdesk\/files\/rustdesk.png/Icon=rustdesk/g" ./AppDir/usr/share/applications/rustdesk.desktop + - rm -rf ./AppDir/usr/share/applications +AppDir: + path: ./AppDir + app_info: + id: rustdesk + name: rustdesk + icon: rustdesk + version: 1.2.0 + exec: usr/lib/rustdesk/rustdesk + exec_args: $@ + apt: + arch: + - aarch64 + allow_unauthenticated: true + sources: + - sourceline: deb https://mirrors.ustc.edu.cn/ubuntu-ports/ bionic main restricted + - sourceline: deb https://mirrors.ustc.edu.cn/ubuntu-ports/ bionic-updates main restricted + - sourceline: deb https://mirrors.ustc.edu.cn/ubuntu-ports/ bionic universe + - sourceline: deb https://mirrors.ustc.edu.cn/ubuntu-ports/ bionic-updates universe + - sourceline: deb https://mirrors.ustc.edu.cn/ubuntu-ports/ bionic multiverse + - sourceline: deb https://mirrors.ustc.edu.cn/ubuntu-ports/ bionic-updates multiverse + - sourceline: deb https://mirrors.ustc.edu.cn/ubuntu-ports/ bionic-backports main restricted + universe multiverse + include: + - libc6:amd64 + - libgtk-3-0 + - libxcb-randr0 + - libxdo3 + - libxfixes3 + - libxcb-shape0 + - libxcb-xfixes0 + - libasound2 + - libsystemd0 + - curl + - libva-drm2 + - libva-x11-2 + - libvdpau1 + - libgstreamer-plugins-base1.0-0 + exclude: + - humanity-icon-theme + - hicolor-icon-theme + - adwaita-icon-theme + - ubuntu-mono + files: + include: [] + exclude: + - usr/share/man + - usr/share/doc/*/README.* + - usr/share/doc/*/changelog.* + - usr/share/doc/*/NEWS.* + - usr/share/doc/*/TODO.* + runtime: + env: + GIO_MODULE_DIR: $APPDIR/usr/lib/x86_64-linux-gnu/gio/modules/ + test: + fedora-30: + image: appimagecrafters/tests-env:fedora-30 + command: ./AppRun + debian-stable: + image: appimagecrafters/tests-env:debian-stable + command: ./AppRun + archlinux-latest: + image: appimagecrafters/tests-env:archlinux-latest + command: ./AppRun + centos-7: + image: appimagecrafters/tests-env:centos-7 + command: ./AppRun + ubuntu-xenial: + image: appimagecrafters/tests-env:ubuntu-xenial + command: ./AppRun +AppImage: + arch: aarch64 + update-information: guess diff --git a/appimage/AppImageBuilder.yml b/appimage/AppImageBuilder-x86_64.yml similarity index 100% rename from appimage/AppImageBuilder.yml rename to appimage/AppImageBuilder-x86_64.yml From 522aacb9b5c62d21861a221d5e110ae04acd90a3 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 9 Jan 2023 10:38:43 +0800 Subject: [PATCH 1450/2015] fix: missing deps fix: trusted sourceline fix: libc6 arm64 def fix: aarch64 appimage build fix: change to ports ubuntu --- .github/workflows/flutter-nightly.yml | 2 +- appimage/AppImageBuilder-aarch64.yml | 17 ++++++++--------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index aaafede07..e1dfdbe74 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -976,7 +976,7 @@ jobs: - name: Prepare env run: | sudo apt update -y - sudo apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev + sudo apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev libarchive-tools mkdir -p ./target/release/ - name: Restore the rustdesk lib file diff --git a/appimage/AppImageBuilder-aarch64.yml b/appimage/AppImageBuilder-aarch64.yml index 12d737718..ee37baf1c 100644 --- a/appimage/AppImageBuilder-aarch64.yml +++ b/appimage/AppImageBuilder-aarch64.yml @@ -22,19 +22,18 @@ AppDir: exec_args: $@ apt: arch: - - aarch64 + - arm64 allow_unauthenticated: true sources: - - sourceline: deb https://mirrors.ustc.edu.cn/ubuntu-ports/ bionic main restricted - - sourceline: deb https://mirrors.ustc.edu.cn/ubuntu-ports/ bionic-updates main restricted - - sourceline: deb https://mirrors.ustc.edu.cn/ubuntu-ports/ bionic universe - - sourceline: deb https://mirrors.ustc.edu.cn/ubuntu-ports/ bionic-updates universe - - sourceline: deb https://mirrors.ustc.edu.cn/ubuntu-ports/ bionic multiverse - - sourceline: deb https://mirrors.ustc.edu.cn/ubuntu-ports/ bionic-updates multiverse - - sourceline: deb https://mirrors.ustc.edu.cn/ubuntu-ports/ bionic-backports main restricted + - sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ bionic main restricted universe multiverse + key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32' + - sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ bionic-updates main restricted universe multiverse + key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32' + - sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ bionic-backports main restricted universe multiverse + key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32' include: - - libc6:amd64 + - libc6 - libgtk-3-0 - libxcb-randr0 - libxdo3 From 6a9dbbd7a0cdf2c24f9960c5f071adf686e103f1 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 10 Jan 2023 11:07:12 +0800 Subject: [PATCH 1451/2015] fix: remove gio module dir fix: set GDK_BACKEND to x11 --- appimage/AppImageBuilder-aarch64.yml | 1 + appimage/AppImageBuilder-x86_64.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/appimage/AppImageBuilder-aarch64.yml b/appimage/AppImageBuilder-aarch64.yml index ee37baf1c..f3cd8f568 100644 --- a/appimage/AppImageBuilder-aarch64.yml +++ b/appimage/AppImageBuilder-aarch64.yml @@ -63,6 +63,7 @@ AppDir: runtime: env: GIO_MODULE_DIR: $APPDIR/usr/lib/x86_64-linux-gnu/gio/modules/ + GDK_BACKEND: x11 test: fedora-30: image: appimagecrafters/tests-env:fedora-30 diff --git a/appimage/AppImageBuilder-x86_64.yml b/appimage/AppImageBuilder-x86_64.yml index ae95fd2ce..59dd5164f 100644 --- a/appimage/AppImageBuilder-x86_64.yml +++ b/appimage/AppImageBuilder-x86_64.yml @@ -66,6 +66,7 @@ AppDir: runtime: env: GIO_MODULE_DIR: $APPDIR/usr/lib/x86_64-linux-gnu/gio/modules/ + GDK_BACKEND: x11 test: fedora-30: image: appimagecrafters/tests-env:fedora-30 From dac476bce08e487ecfc1034747c7f0c160ab586d Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 10 Jan 2023 12:57:56 +0800 Subject: [PATCH 1452/2015] update hwcodec spell Signed-off-by: 21pages --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 659702704..86707fd62 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2602,7 +2602,7 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hwcodec" version = "0.1.0" -source = "git+https://github.com/21pages/hwcodec#e819484c4c010199f2a0977bdf306b4edbeafbae" +source = "git+https://github.com/21pages/hwcodec#64f885b3787694b16dfcff08256750b0376b2eba" dependencies = [ "bindgen 0.59.2", "cc", From b8e68475d8de1e5473eef0ac54665c23abc6eb7e Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 10 Jan 2023 13:01:19 +0800 Subject: [PATCH 1453/2015] full flutter ci --- .github/workflows/flutter-ci.yml | 995 +++++++++++++++++++++++++++++-- 1 file changed, 961 insertions(+), 34 deletions(-) diff --git a/.github/workflows/flutter-ci.yml b/.github/workflows/flutter-ci.yml index 7825286bd..df47d07d1 100644 --- a/.github/workflows/flutter-ci.yml +++ b/.github/workflows/flutter-ci.yml @@ -1,4 +1,4 @@ -name: Flutter CI +name: Full Flutter CI on: workflow_dispatch: @@ -11,42 +11,142 @@ on: paths-ignore: - ".github/**" +env: + LLVM_VERSION: "10.0" + # Note: currently 3.0.5 does not support arm64 officially, we use latest stable version first. + FLUTTER_VERSION: "3.0.5" + # vcpkg version: 2022.05.10 + # for multiarch gcc compatibility + VCPKG_COMMIT_ID: "14e7bb4ae24616ec54ff6b2f6ef4e8659434ea44" + VERSION: "1.2.0" + jobs: - build: + build-for-windows: name: ${{ matrix.job.target }} (${{ matrix.job.os }}) runs-on: ${{ matrix.job.os }} strategy: - fail-fast: false + fail-fast: true matrix: job: - # - { target: aarch64-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } - # - { target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true } - # - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true } # - { target: i686-pc-windows-msvc , os: windows-2019 } - # - { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } - # - { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } - # - { target: x86_64-apple-darwin , os: macos-10.15 } # - { target: x86_64-pc-windows-gnu , os: windows-2019 } - # - { target: x86_64-pc-windows-msvc , os: windows-2019 } - - { target: x86_64-unknown-linux-gnu , os: ubuntu-20.04 } - # - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } + - { target: x86_64-pc-windows-msvc, os: windows-2019 } steps: - name: Checkout source code uses: actions/checkout@v3 - - name: Install prerequisites - shell: bash - run: | - case ${{ matrix.job.target }} in - x86_64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev;; - # arm-unknown-linux-*) sudo apt-get -y update ; sudo apt-get -y install gcc-arm-linux-gnueabihf ;; - # aarch64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt-get -y install gcc-aarch64-linux-gnu ;; - esac + - name: Install LLVM and Clang + uses: KyleMayes/install-llvm-action@v1 + with: + version: ${{ env.LLVM_VERSION }} - name: Install flutter uses: subosito/flutter-action@v2 with: - channel: 'stable' + channel: "stable" + flutter-version: ${{ env.FLUTTER_VERSION }} + cache: true + + - name: Replace engine with rustdesk custom flutter engine + run: | + flutter doctor -v + flutter precache --windows + Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.0.5-rustdesk.2/windows-x64-flutter-release.zip -OutFile windows-x64-flutter-release.zip + Expand-Archive windows-x64-flutter-release.zip -DestinationPath engine + mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-3.0.5-x64/bin/cache/artifacts/engine/windows-x64-release/ + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: "1.62" + target: ${{ matrix.job.target }} + override: true + components: rustfmt + profile: minimal # minimal component installation (ie, no documentation) + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: ${{ matrix.job.os }} + + - name: Install flutter rust bridge deps + run: | + dart pub global activate ffigen --version 5.0.1 + $exists = Test-Path ~/.cargo/bin/flutter_rust_bridge_codegen.exe + Push-Location .. + git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1 + Push-Location flutter_rust_bridge/frb_codegen ; cargo install --path . ; Pop-Location + Pop-Location + Push-Location flutter ; flutter pub get ; Pop-Location + ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart + + - name: Restore from cache and install vcpkg + uses: lukka/run-vcpkg@v7 + with: + setupOnly: true + vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} + + - name: Install vcpkg dependencies + run: | + $VCPKG_ROOT/vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static + shell: bash + + - name: Build rustdesk + run: python3 .\build.py --portable --hwcodec --flutter + + build-for-macOS: + name: ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-args }}] + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: true + matrix: + job: + - { + target: x86_64-apple-darwin, + os: macos-latest, + extra-build-args: "", + } + steps: + - name: Checkout source code + uses: actions/checkout@v3 + + - name: Import the codesign cert + uses: apple-actions/import-codesign-certs@v1 + with: + p12-file-base64: ${{ secrets.MACOS_P12_BASE64 }} + p12-password: ${{ secrets.MACOS_P12_PASSWORD }} + keychain: rustdesk + + - name: Check sign and import sign key + run: | + security default-keychain -s rustdesk.keychain + security find-identity -v + + - name: Import notarize key + uses: timheuer/base64-to-file@v1.2 + with: + # https://gregoryszorc.com/docs/apple-codesign/stable/apple_codesign_rcodesign.html#notarizing-and-stapling + fileName: rustdesk.json + fileDir: ${{ github.workspace }} + encodedString: ${{ secrets.MACOS_NOTARIZE_JSON }} + + - name: Install rcodesign tool + shell: bash + run: | + pushd /tmp + wget https://github.com/indygreg/apple-platform-rs/releases/download/apple-codesign%2F0.22.0/apple-codesign-0.22.0-macos-universal.tar.gz + tar -zxvf apple-codesign-0.22.0-macos-universal.tar.gz + mv apple-codesign-0.22.0-macos-universal/rcodesign /usr/local/bin + popd + + - name: Install build runtime + run: | + brew install llvm create-dmg nasm yasm cmake gcc wget ninja + + - name: Install flutter + uses: subosito/flutter-action@v2 + with: + channel: "stable" + flutter-version: ${{ env.FLUTTER_VERSION }} - name: Install Rust toolchain uses: actions-rs/toolchain@v1 @@ -56,14 +156,21 @@ jobs: override: true profile: minimal # minimal component installation (ie, no documentation) - - uses: Swatinem/rust-cache@v1 + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: ${{ matrix.job.os }} - name: Install flutter rust bridge deps + shell: bash run: | dart pub global activate ffigen --version 5.0.1 # flutter_rust_bridge - pushd /tmp && git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1 && popd - pushd /tmp/flutter_rust_bridge/frb_codegen && cargo install --path . && popd + pushd /tmp + wget https://github.com/Kingtous/flutter_rust_bridge/releases/download/1.32.0-rustdesk/flutter_rust_bridge_codegen-x86_64-darwin.tgz + tar -zxvf flutter_rust_bridge_codegen-x86_64-darwin.tgz + mkdir -p ~/.cargo/bin + mv flutter_rust_bridge_codegen ~/.cargo/bin; chmod +x ~/.cargo/bin/flutter_rust_bridge_codegen + popd pushd flutter && flutter pub get && popd ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart @@ -71,29 +178,849 @@ jobs: uses: lukka/run-vcpkg@v7 with: setupOnly: true - vcpkgGitCommitId: '1d4128f08e30cec31b94500840c7eca8ebc579cb' + vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} - name: Install vcpkg dependencies run: | $VCPKG_ROOT/vcpkg install libvpx libyuv opus - shell: bash - - name: Show version information (Rust, cargo, GCC) + - name: Show version information (Rust, cargo, Clang) shell: bash run: | - gcc --version || true + clang --version || true rustup -V rustup toolchain list rustup default cargo -V rustc -V - - name: Build rustdesk ffi lib - run: cargo build --features flutter --lib - - - name: Build Flutter + - name: Build rustdesk run: | + # --hwcodec not supported on macos yet + ./build.py --flutter ${{ matrix.job.extra-build-args }} + + build-vcpkg-deps-linux: + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: true + matrix: + job: + # - { arch: armv7, os: ubuntu-20.04 } + - { arch: x86_64, os: ubuntu-20.04 } + - { arch: aarch64, os: ubuntu-20.04 } + steps: + - name: Create vcpkg artifacts folder + run: mkdir -p /opt/artifacts + + - name: Cache Vcpkg + id: cache-vcpkg + uses: actions/cache@v3 + with: + path: /opt/artifacts + key: vcpkg-${{ matrix.job.arch }} + + - uses: Kingtous/run-on-arch-action@amd64-support + name: Run vcpkg install on ${{ matrix.job.arch }} + id: vcpkg + with: + arch: ${{ matrix.job.arch }} + distro: ubuntu18.04 + githubToken: ${{ github.token }} + setup: | + ls -l "/opt/artifacts" + dockerRunArgs: | + --volume "/opt/artifacts:/artifacts" + shell: /bin/bash + install: | + apt update -y + case "${{ matrix.job.arch }}" in + x86_64) + # CMake 3.15+ + apt install -y gpg wget ca-certificates + echo 'deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ bionic main' | tee /etc/apt/sources.list.d/kitware.list >/dev/null + wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/null | gpg --dearmor - | tee /usr/share/keyrings/kitware-archive-keyring.gpg >/dev/null + apt update -y + apt install -y curl zip unzip tar git cmake g++ gcc build-essential pkg-config wget nasm yasm ninja-build libjpeg8-dev + ;; + aarch64|armv7) + apt install -y curl zip unzip tar git cmake g++ gcc build-essential pkg-config wget nasm yasm ninja-build libjpeg8-dev automake libtool + esac + cmake --version + gcc -v + run: | + # disable git safe.directory + git config --global --add safe.directory "*" + case "${{ matrix.job.arch }}" in + x86_64) + export VCPKG_FORCE_SYSTEM_BINARIES=1 + pushd /artifacts + git clone https://github.com/microsoft/vcpkg.git || true + pushd vcpkg + git reset --hard ${{ env.VCPKG_COMMIT_ID }} + ./bootstrap-vcpkg.sh + ./vcpkg install libvpx libyuv opus + ;; + aarch64|armv7) + pushd /artifacts + # libyuv + git clone https://chromium.googlesource.com/libyuv/libyuv || true + pushd libyuv + git pull + mkdir -p build + pushd build + mkdir -p /artifacts/vcpkg/installed + cmake .. -DCMAKE_INSTALL_PREFIX=/artifacts/vcpkg/installed + make -j4 && make install + popd + popd + # libopus, ubuntu 18.04 prebuilt is not be compiled with -fPIC + wget -O opus.tar.gz http://archive.ubuntu.com/ubuntu/pool/main/o/opus/opus_1.1.2.orig.tar.gz + tar -zxvf opus.tar.gz; ls -l + pushd opus-1.1.2 + ./autogen.sh; ./configure --prefix=/artifacts/vcpkg/installed + make -j4; make install + ;; + esac + - name: Upload artifacts + uses: actions/upload-artifact@master + with: + name: vcpkg-artifact-${{ matrix.job.arch }} + path: | + /opt/artifacts/vcpkg/installed + + generate-bridge-linux: + name: generate bridge + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: true + matrix: + job: + - { + target: x86_64-unknown-linux-gnu, + os: ubuntu-18.04, + extra-build-args: "", + } + steps: + - name: Checkout source code + uses: actions/checkout@v3 + + - name: Install prerequisites + run: | + sudo apt update -y + sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang cmake libclang-dev ninja-build llvm-dev libclang-10-dev llvm-10-dev pkg-config + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: ${{ matrix.job.target }} + override: true + profile: minimal # minimal component installation (ie, no documentation) + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: bridge-${{ matrix.job.os }} + workspace: "/tmp/flutter_rust_bridge/frb_codegen" + + - name: Cache Bridge + id: cache-bridge + uses: actions/cache@v3 + with: + path: /tmp/flutter_rust_bridge + key: vcpkg-${{ matrix.job.arch }} + + - name: Install flutter + uses: subosito/flutter-action@v2 + with: + channel: "stable" + flutter-version: ${{ env.FLUTTER_VERSION }} + cache: true + + - name: Install ffigen + run: | + dart pub global activate ffigen --version 5.0.1 + + - name: Install flutter rust bridge deps + shell: bash + run: | + pushd /tmp && git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1 || true && popd + pushd /tmp/flutter_rust_bridge/frb_codegen && cargo install --path . && popd + pushd flutter && flutter pub get && popd + + - name: Run flutter rust bridge + run: | + ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart + + - name: Upload Artifact + uses: actions/upload-artifact@master + with: + name: bridge-artifact + path: | + ./src/bridge_generated.rs + ./flutter/lib/generated_bridge.dart + ./flutter/lib/generated_bridge.freezed.dart + + build-rustdesk-android-arm64: + needs: [generate-bridge-linux] + name: build rustdesk android apk ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: true + matrix: + job: + - { + arch: x86_64, + target: aarch64-linux-android, + os: ubuntu-18.04, + extra-build-features: "", + } + # - { + # arch: x86_64, + # target: armv7-linux-androideabi, + # os: ubuntu-18.04, + # extra-build-features: "", + # } + steps: + - name: Install dependencies + run: | + sudo apt update + sudo apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libvdpau-dev libva-dev libclang-dev llvm-dev libclang-10-dev llvm-10-dev pkg-config tree g++ libc6-dev gcc-multilib g++-multilib openjdk-11-jdk-headless + - name: Checkout source code + uses: actions/checkout@v3 + - name: Install flutter + uses: subosito/flutter-action@v2 + with: + channel: "stable" + flutter-version: ${{ env.FLUTTER_VERSION }} + - uses: nttld/setup-ndk@v1 + id: setup-ndk + with: + ndk-version: r22b + add-to-path: true + + - name: Download deps + shell: bash + run: | + pushd /opt + wget https://github.com/rustdesk/doc.rustdesk.com/releases/download/console/dep.tar.gz + tar xzf dep.tar.gz + + - name: Restore bridge files + uses: actions/download-artifact@master + with: + name: bridge-artifact + path: ./ + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + override: true + profile: minimal # minimal component installation (ie, no documentation) + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: rustdesk-lib-cache + key: ${{ matrix.job.target }}-${{ matrix.job.extra-build-features }} + + - name: Disable rust bridge build + run: | + sed -i "s/gen_flutter_rust_bridge();/\/\//g" build.rs + + - name: Build rustdesk lib + env: + ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} + ANDROID_NDK_ROOT: ${{ steps.setup-ndk.outputs.ndk-path }} + VCPKG_ROOT: /opt/vcpkg + run: | + rustup target add ${{ matrix.job.target }} + cargo install cargo-ndk + case ${{ matrix.job.target }} in + aarch64-linux-android) + ./flutter/ndk_arm64.sh + mkdir -p ./flutter/android/app/src/main/jniLibs/arm64-v8a + cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so + ;; + armv7-linux-androideabi) + ./flutter/ndk_arm.sh + mkdir -p ./flutter/android/app/src/main/jniLibs/armeabi-v7a + cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/librustdesk.so + ;; + esac + + - name: Build rustdesk + shell: bash + env: + JAVA_HOME: /usr/lib/jvm/java-11-openjdk-amd64 + run: | + export PATH=/usr/lib/jvm/java-11-openjdk-amd64/bin:$PATH + # download so pushd flutter - flutter pub get - flutter build linux --debug -v + wget -O so.tar.gz https://github.com/rustdesk/doc.rustdesk.com/releases/download/console/so.tar.gz + tar xzvf so.tar.gz popd + # temporary use debug sign config + sed -i "s/signingConfigs.release/signingConfigs.debug/g" ./flutter/android/app/build.gradle + case ${{ matrix.job.target }} in + aarch64-linux-android) + mkdir -p ./flutter/android/app/src/main/jniLibs/arm64-v8a + cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so + # build flutter + pushd flutter + flutter build apk --release --target-platform android-arm64 --split-per-abi + mv build/app/outputs/flutter-apk/app-arm64-v8a-release.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk + ;; + armv7-linux-androideabi) + mkdir -p ./flutter/android/app/src/main/jniLibs/armeabi-v7a + cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/librustdesk.so + # build flutter + pushd flutter + flutter build apk --release --target-platform android-arm --split-per-abi + mv build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk + ;; + esac + popd + mkdir -p signed-apk; pushd signed-apk + mv ../rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk . + + build-rustdesk-lib-linux-amd64: + needs: [generate-bridge-linux, build-vcpkg-deps-linux] + name: build-rust-lib ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: true + matrix: + # use a high level qemu-user-static + job: + # - { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } + # - { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } + - { + arch: x86_64, + target: x86_64-unknown-linux-gnu, + os: ubuntu-20.04, + extra-build-features: "", + } + - { + arch: x86_64, + target: x86_64-unknown-linux-gnu, + os: ubuntu-20.04, + extra-build-features: "flatpak", + } + - { + arch: x86_64, + target: x86_64-unknown-linux-gnu, + os: ubuntu-20.04, + extra-build-features: "appimage", + } + # - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } + steps: + - name: Maximize build space + run: | + sudo rm -rf /opt/ghc + sudo rm -rf /usr/local/lib/android + sudo rm -rf /usr/share/dotnet + sudo apt update -y + sudo apt install qemu-user-static + + - name: Checkout source code + uses: actions/checkout@v3 + + - name: Set Swap Space + uses: pierotofy/set-swap-space@master + with: + swap-size-gb: 12 + + - name: Free Space + run: | + df + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: ${{ matrix.job.target }} + override: true + profile: minimal # minimal component installation (ie, no documentation) + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: rustdesk-lib-cache + key: ${{ matrix.job.target }}-${{ matrix.job.extra-build-features }} + cache-directories: "/opt/rust-registry" + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: ${{ matrix.job.target }} + override: true + profile: minimal # minimal component installation (ie, no documentation) + + - name: Install local registry + run: | + mkdir -p /opt/rust-registry + cargo install cargo-local-registry + + - name: Build local registry + uses: nick-fields/retry@v2 + id: build-local-registry + continue-on-error: true + with: + max_attempts: 3 + timeout_minutes: 15 + retry_on: error + command: cargo local-registry --sync ./Cargo.lock /opt/rust-registry + + - name: Disable rust bridge build + run: | + sed -i "s/gen_flutter_rust_bridge();/\/\//g" build.rs + # only build cdylib + sed -i "s/\[\"cdylib\", \"staticlib\", \"rlib\"\]/\[\"cdylib\"\]/g" Cargo.toml + + - name: Restore bridge files + uses: actions/download-artifact@master + with: + name: bridge-artifact + path: ./ + + - name: Restore vcpkg files + uses: actions/download-artifact@master + with: + name: vcpkg-artifact-${{ matrix.job.arch }} + path: /opt/artifacts/vcpkg/installed + + - uses: Kingtous/run-on-arch-action@amd64-support + name: Build rustdesk library for ${{ matrix.job.arch }} + id: vcpkg + with: + arch: ${{ matrix.job.arch }} + distro: ubuntu18.04 + # not ready yet + # distro: ubuntu18.04-rustdesk + githubToken: ${{ github.token }} + setup: | + ls -l "${PWD}" + ls -l /opt/artifacts/vcpkg/installed + dockerRunArgs: | + --volume "${PWD}:/workspace" + --volume "/opt/artifacts:/opt/artifacts" + --volume "/opt/rust-registry:/opt/rust-registry" + shell: /bin/bash + install: | + apt update -y + echo -e "installing deps" + apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libvdpau-dev libva-dev libclang-dev llvm-dev libclang-10-dev llvm-10-dev pkg-config tree g++ gcc libvpx-dev tree > /dev/null + # we have libopus compiled by us. + apt remove -y libopus-dev || true + # output devs + ls -l ./ + tree -L 3 /opt/artifacts/vcpkg/installed + run: | + # disable git safe.directory + git config --global --add safe.directory "*" + # rust + pushd /opt + wget -O rust.tar.gz https://static.rust-lang.org/dist/rust-1.64.0-${{ matrix.job.target }}.tar.gz + tar -zxvf rust.tar.gz > /dev/null && rm rust.tar.gz + cd rust-1.64.0-${{ matrix.job.target }} && ./install.sh + rm -rf rust-1.64.0-${{ matrix.job.target }} + # edit config + mkdir -p ~/.cargo/ + echo """ + [source.crates-io] + registry = 'https://github.com/rust-lang/crates.io-index' + replace-with = 'local-registry' + + [source.local-registry] + local-registry = '/opt/rust-registry/' + """ > ~/.cargo/config + cat ~/.cargo/config + # start build + pushd /workspace + # mock + case "${{ matrix.job.arch }}" in + x86_64) + # no need mock on x86_64 + export VCPKG_ROOT=/opt/artifacts/vcpkg + cargo build --lib --features hwcodec,flutter,${{ matrix.job.extra-build-features }} --release + ;; + esac + + - name: Upload Artifacts + uses: actions/upload-artifact@master + with: + name: librustdesk-${{ matrix.job.arch }}-${{ matrix.job.extra-build-features }}.so + path: target/release/liblibrustdesk.so + + build-rustdesk-lib-linux-arm: + needs: [generate-bridge-linux, build-vcpkg-deps-linux] + name: build-rust-lib ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: true + matrix: + # use a high level qemu-user-static + job: + - { + arch: aarch64, + target: aarch64-unknown-linux-gnu, + os: ubuntu-20.04, + use-cross: true, + extra-build-features: "", + } + - { + arch: aarch64, + target: aarch64-unknown-linux-gnu, + os: ubuntu-18.04, # just for naming package, not running host + use-cross: true, + extra-build-features: "appimage", + } + # - { arch: aarch64, target: aarch64-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true, extra-build-features: "flatpak" } + # - { + # arch: armv7, + # target: arm-unknown-linux-gnueabihf, + # os: ubuntu-20.04, + # use-cross: true, + # extra-build-features: "", + # } + # - { arch: armv7, target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true, extra-build-features: "flatpak" } + # - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true } + steps: + - name: Maximize build space + run: | + sudo rm -rf /opt/ghc + sudo rm -rf /usr/local/lib/android + sudo rm -rf /usr/share/dotnet + sudo apt update -y + sudo apt install qemu-user-static + + - name: Checkout source code + uses: actions/checkout@v3 + + - name: Set Swap Space + uses: pierotofy/set-swap-space@master + with: + swap-size-gb: 12 + + - name: Free Space + run: | + df + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: ${{ matrix.job.target }} + override: true + profile: minimal # minimal component installation (ie, no documentation) + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: ${{ matrix.job.target }} + override: true + profile: minimal # minimal component installation (ie, no documentation) + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: rustdesk-lib-cache + key: ${{ matrix.job.target }}-${{ matrix.job.extra-build-features }} + cache-directories: "/opt/rust-registry" + + - name: Install local registry + run: | + mkdir -p /opt/rust-registry + cargo install cargo-local-registry + + - name: Build local registry + uses: nick-fields/retry@v2 + id: build-local-registry + continue-on-error: true + with: + max_attempts: 3 + timeout_minutes: 15 + retry_on: error + command: cargo local-registry --sync ./Cargo.lock /opt/rust-registry + + - name: Disable rust bridge build + run: | + sed -i "s/gen_flutter_rust_bridge();/\/\//g" build.rs + # only build cdylib + sed -i "s/\[\"cdylib\", \"staticlib\", \"rlib\"\]/\[\"cdylib\"\]/g" Cargo.toml + + - name: Restore bridge files + uses: actions/download-artifact@master + with: + name: bridge-artifact + path: ./ + + - name: Restore vcpkg files + uses: actions/download-artifact@master + with: + name: vcpkg-artifact-${{ matrix.job.arch }} + path: /opt/artifacts/vcpkg/installed + + - uses: Kingtous/run-on-arch-action@amd64-support + name: Build rustdesk library for ${{ matrix.job.arch }} + id: vcpkg + with: + arch: ${{ matrix.job.arch }} + distro: ubuntu18.04-rustdesk + githubToken: ${{ github.token }} + setup: | + ls -l "${PWD}" + ls -l /opt/artifacts/vcpkg/installed + dockerRunArgs: | + --volume "${PWD}:/workspace" + --volume "/opt/artifacts:/opt/artifacts" + --volume "/opt/rust-registry:/opt/rust-registry" + shell: /bin/bash + install: | + apt update -y + echo -e "installing deps" + apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libvdpau-dev libva-dev libclang-dev llvm-dev libclang-10-dev llvm-10-dev pkg-config tree g++ gcc libvpx-dev tree > /dev/null + # we have libopus compiled by us. + apt remove -y libopus-dev || true + # output devs + ls -l ./ + tree -L 3 /opt/artifacts/vcpkg/installed + run: | + # disable git safe.directory + git config --global --add safe.directory "*" + # rust + pushd /opt + wget -O rust.tar.gz https://static.rust-lang.org/dist/rust-1.64.0-${{ matrix.job.target }}.tar.gz + tar -zxvf rust.tar.gz > /dev/null && rm rust.tar.gz + cd rust-1.64.0-${{ matrix.job.target }} && ./install.sh + rm -rf rust-1.64.0-${{ matrix.job.target }} + # edit config + mkdir -p ~/.cargo/ + echo """ + [source.crates-io] + registry = 'https://github.com/rust-lang/crates.io-index' + replace-with = 'local-registry' + + [source.local-registry] + local-registry = '/opt/rust-registry/' + """ > ~/.cargo/config + cat ~/.cargo/config + # start build + pushd /workspace + # mock + case "${{ matrix.job.arch }}" in + aarch64) + cp -r /opt/artifacts/vcpkg/installed/lib/* /usr/lib/aarch64-linux-gnu/ + cp -r /opt/artifacts/vcpkg/installed/include/* /usr/include/ + ls -l /opt/artifacts/vcpkg/installed/lib/ + mkdir -p /vcpkg/installed/arm64-linux + ln -s /usr/lib/aarch64-linux-gnu /vcpkg/installed/arm64-linux/lib + ln -s /usr/include /vcpkg/installed/arm64-linux/include + export VCPKG_ROOT=/vcpkg + # disable hwcodec for compilation + cargo build --lib --features flutter,${{ matrix.job.extra-build-features }} --release + ;; + armv7) + cp -r /opt/artifacts/vcpkg/installed/lib/* /usr/lib/arm-linux-gnueabihf/ + cp -r /opt/artifacts/vcpkg/installed/include/* /usr/include/ + mkdir -p /vcpkg/installed/arm-linux + ln -s /usr/lib/arm-linux-gnueabihf /vcpkg/installed/arm-linux/lib + ln -s /usr/include /vcpkg/installed/arm-linux/include + export VCPKG_ROOT=/vcpkg + # disable hwcodec for compilation + cargo build --lib --features flutter,${{ matrix.job.extra-build-features }} --release + ;; + esac + + - name: Upload Artifacts + uses: actions/upload-artifact@master + with: + name: librustdesk-${{ matrix.job.arch }}-${{ matrix.job.extra-build-features }}.so + path: target/release/liblibrustdesk.so + + build-rustdesk-linux-arm: + needs: [build-rustdesk-lib-linux-arm] + name: build-rustdesk ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] + runs-on: ubuntu-20.04 # 20.04 has more performance on arm build + strategy: + fail-fast: true + matrix: + job: + - { + arch: aarch64, + target: aarch64-unknown-linux-gnu, + os: ubuntu-18.04, # just for naming package, not running host + use-cross: true, + extra-build-features: "", + } + - { + arch: aarch64, + target: aarch64-unknown-linux-gnu, + os: ubuntu-18.04, # just for naming package, not running host + use-cross: true, + extra-build-features: "appimage", + } + # - { + # arch: aarch64, + # target: aarch64-unknown-linux-gnu, + # os: ubuntu-18.04, # just for naming package, not running host + # use-cross: true, + # extra-build-features: "flatpak", + # } + # - { arch: armv7, target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true, extra-build-features: "" } + # - { arch: armv7, target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true, extra-build-features: "flatpak" } + # - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true } + steps: + - name: Checkout source code + uses: actions/checkout@v3 + + - name: Restore bridge files + uses: actions/download-artifact@master + with: + name: bridge-artifact + path: ./ + + - name: Prepare env + run: | + sudo apt update -y + sudo apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev libarchive-tools + mkdir -p ./target/release/ + + - name: Restore the rustdesk lib file + uses: actions/download-artifact@master + with: + name: librustdesk-${{ matrix.job.arch }}-${{ matrix.job.extra-build-features }}.so + path: ./target/release/ + + - name: Download Flutter + shell: bash + run: | + # disable git safe.directory + git config --global --add safe.directory "*" + pushd /opt + # clone repo and reset to flutter 3.0.5 + git clone https://github.com/sony/flutter-elinux.git || true + pushd flutter-elinux + # reset to flutter 3.0.5 + git fetch + git reset --hard b09a90eee643859ce4e676839227edd9fd3feba8 + popd + + - uses: Kingtous/run-on-arch-action@amd64-support + name: Build rustdesk binary for ${{ matrix.job.arch }} + id: vcpkg + with: + arch: ${{ matrix.job.arch }} + distro: ubuntu18.04-rustdesk + githubToken: ${{ github.token }} + setup: | + ls -l "${PWD}" + dockerRunArgs: | + --volume "${PWD}:/workspace" + --volume "/opt/artifacts:/opt/artifacts" + --volume "/opt/flutter-elinux:/opt/flutter-elinux" + shell: /bin/bash + install: | + apt update -y + apt-get -qq install -y git cmake g++ gcc build-essential nasm yasm curl unzip xz-utils python3 wget pkg-config ninja-build pkg-config libgtk-3-dev liblzma-dev clang libappindicator3-dev rpm + run: | + # disable git safe.directory + git config --global --add safe.directory "*" + pushd /workspace + # we use flutter-elinux to build our rustdesk + sed -i "s/flutter build linux --release/flutter-elinux build linux/g" ./build.py + # Setup flutter-elinux + export PATH=/opt/flutter-elinux/bin:$PATH + flutter-elinux doctor -v + # edit to corresponding arch + case ${{ matrix.job.arch }} in + aarch64) + sed -i "s/Architecture: amd64/Architecture: arm64/g" ./build.py + sed -i "s/x64\/release/arm64\/release/g" ./build.py + ;; + armv7) + sed -i "s/Architecture: amd64/Architecture: arm/g" ./build.py + sed -i "s/x64\/release/arm\/release/g" ./build.py + ;; + esac + python3 ./build.py --flutter --hwcodec --skip-cargo + + build-rustdesk-linux-amd64: + needs: [build-rustdesk-lib-linux-amd64] + name: build-rustdesk ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] + runs-on: ubuntu-20.04 + strategy: + fail-fast: true + matrix: + job: + # - { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } + # - { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } + - { + arch: x86_64, + target: x86_64-unknown-linux-gnu, + os: ubuntu-18.04, + extra-build-features: "", + } + - { + arch: x86_64, + target: x86_64-unknown-linux-gnu, + os: ubuntu-18.04, + extra-build-features: "flatpak", + } + - { + arch: x86_64, + target: x86_64-unknown-linux-gnu, + os: ubuntu-18.04, + extra-build-features: "appimage", + } + # - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } + steps: + - name: Checkout source code + uses: actions/checkout@v3 + + - name: Restore bridge files + uses: actions/download-artifact@master + with: + name: bridge-artifact + path: ./ + + - name: Prepare env + run: | + sudo apt update -y + sudo apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev libarchive-tools + mkdir -p ./target/release/ + + - name: Restore the rustdesk lib file + uses: actions/download-artifact@master + with: + name: librustdesk-${{ matrix.job.arch }}-${{ matrix.job.extra-build-features }}.so + path: ./target/release/ + + - uses: Kingtous/run-on-arch-action@amd64-support + name: Build rustdesk binary for ${{ matrix.job.arch }} + id: vcpkg + with: + arch: ${{ matrix.job.arch }} + distro: ubuntu18.04 + githubToken: ${{ github.token }} + setup: | + ls -l "${PWD}" + dockerRunArgs: | + --volume "${PWD}:/workspace" + --volume "/opt/artifacts:/opt/artifacts" + shell: /bin/bash + install: | + apt update -y + apt-get -qq install -y git cmake g++ gcc build-essential nasm yasm curl unzip xz-utils python3 wget pkg-config ninja-build pkg-config libgtk-3-dev liblzma-dev clang libappindicator3-dev rpm + run: | + # disable git safe.directory + git config --global --add safe.directory "*" + # Setup Flutter + pushd /opt + wget https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_${{ env.FLUTTER_VERSION }}-stable.tar.xz + tar xf flutter_linux_${{ env.FLUTTER_VERSION }}-stable.tar.xz + ls -l . + export PATH=/opt/flutter/bin:$PATH + flutter doctor -v + pushd /workspace + python3 ./build.py --flutter --hwcodec --skip-cargo From fae8a9489159fcf9bf2afcc3f418c272d4054f1b Mon Sep 17 00:00:00 2001 From: 21pages Date: Wed, 28 Dec 2022 11:01:09 +0800 Subject: [PATCH 1454/2015] sync strategy Signed-off-by: 21pages --- src/hbbs_http.rs | 1 + src/hbbs_http/sync.rs | 132 +++++++++++++++++++++++++++++++++++++++ src/server.rs | 10 ++- src/server/connection.rs | 64 ++++++------------- 4 files changed, 160 insertions(+), 47 deletions(-) create mode 100644 src/hbbs_http/sync.rs diff --git a/src/hbbs_http.rs b/src/hbbs_http.rs index 08ad36eb9..76ced87a0 100644 --- a/src/hbbs_http.rs +++ b/src/hbbs_http.rs @@ -5,6 +5,7 @@ use serde_json::{Map, Value}; #[cfg(feature = "flutter")] pub mod account; pub mod record_upload; +pub mod sync; #[derive(Debug)] pub enum HbbHttpResponse { diff --git a/src/hbbs_http/sync.rs b/src/hbbs_http/sync.rs new file mode 100644 index 000000000..9497cc449 --- /dev/null +++ b/src/hbbs_http/sync.rs @@ -0,0 +1,132 @@ +use std::{collections::HashMap, sync::Mutex, time::Duration}; + +use hbb_common::{ + config::{Config, LocalConfig}, + tokio::{self, sync::broadcast, time::Instant}, +}; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; + +use crate::Connection; + +const TIME_HEARTBEAT: Duration = Duration::from_secs(30); +const TIME_CONN: Duration = Duration::from_secs(3); + +lazy_static::lazy_static! { + static ref SENDER : Mutex>> = Mutex::new(start_hbbs_sync()); +} + +pub fn start() { + let _sender = SENDER.lock().unwrap(); +} + +pub fn signal_receiver() -> broadcast::Receiver> { + SENDER.lock().unwrap().subscribe() +} + +fn start_hbbs_sync() -> broadcast::Sender> { + let (tx, _rx) = broadcast::channel::>(16); + std::thread::spawn(move || start_hbbs_sync_async()); + return tx; +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct StrategyOptions { + pub config_options: HashMap, + pub extra: HashMap, +} + +#[tokio::main(flavor = "current_thread")] +async fn start_hbbs_sync_async() { + tokio::spawn(async move { + let mut interval = tokio::time::interval_at(Instant::now() + TIME_CONN, TIME_CONN); + let mut last_send = Instant::now(); + loop { + tokio::select! { + _ = interval.tick() => { + let url = heartbeat_url(); + let modified_at = LocalConfig::get_option("strategy_timestamp").parse::().unwrap_or(0); + if !url.is_empty() { + let conns = Connection::alive_conns(); + if conns.is_empty() && last_send.elapsed() < TIME_HEARTBEAT { + continue; + } + last_send = Instant::now(); + let mut v = Value::default(); + v["id"] = json!(Config::get_id()); + if !conns.is_empty() { + v["conns"] = json!(conns); + } + v["modified_at"] = json!(modified_at); + if let Ok(s) = crate::post_request(url.clone(), v.to_string(), "").await { + if let Ok(mut rsp) = serde_json::from_str::>(&s) { + if let Some(conns) = rsp.remove("disconnect") { + if let Ok(conns) = serde_json::from_value::>(conns) { + SENDER.lock().unwrap().send(conns).ok(); + } + } + if let Some(rsp_modified_at) = rsp.remove("modified_at") { + if let Ok(rsp_modified_at) = serde_json::from_value::(rsp_modified_at) { + if rsp_modified_at != modified_at { + LocalConfig::set_option("strategy_timestamp".to_string(), rsp_modified_at.to_string()); + } + } + } + if let Some(strategy) = rsp.remove("strategy") { + if let Ok(strategy) = serde_json::from_value::(strategy) { + handle_config_options(strategy.config_options); + } + } + } + } + } + } + } + } + }) + .await + .ok(); +} + +fn heartbeat_url() -> String { + let url = crate::common::get_api_server( + Config::get_option("api-server"), + Config::get_option("custom-rendezvous-server"), + ); + if url.is_empty() || url.contains("rustdesk.com") { + return "".to_owned(); + } + format!("{}/api/heartbeat", url) +} + +fn handle_config_options(config_options: HashMap) { + let map = HashMap::from([ + ("enable-keyboard", ""), + ("enable-clipboard", ""), + ("enable-file-transfer", ""), + ("enable-audio", ""), + ("enable-tunnel", ""), + ("enable-remote-restart", ""), + ("enable-record-session", ""), + ("allow-remote-config-modification", ""), + ("approve-mode", ""), + ("verification-method", "use-both-passwords"), + ("enable-rdp", ""), + ("enable-lan-discovery", ""), + ("direct-server", ""), + ("direct-access-port", ""), + ]); + let mut options = Config::get_options(); + for (k, v) in map { + if let Some(v2) = config_options.get(k) { + if v == v2 { + options.remove(k); + } else { + options.insert(k.to_string(), v2.to_string()); + } + } else { + options.remove(k); + } + } + Config::set_options(options); +} diff --git a/src/server.rs b/src/server.rs index 5c020261f..381e3df90 100644 --- a/src/server.rs +++ b/src/server.rs @@ -194,9 +194,14 @@ pub async fn create_tcp_connection( } } - #[cfg(target_os = "macos")]{ + #[cfg(target_os = "macos")] + { use std::process::Command; - Command::new("/usr/bin/caffeinate").arg("-u").arg("-t 5").spawn().ok(); + Command::new("/usr/bin/caffeinate") + .arg("-u") + .arg("-t 5") + .spawn() + .ok(); log::info!("wake up macos"); } Connection::start(addr, stream, id, Arc::downgrade(&server)).await; @@ -385,6 +390,7 @@ pub async fn start_server(is_server: bool) { #[cfg(windows)] crate::platform::windows::bootstrap(); input_service::fix_key_down_timeout_loop(); + crate::hbbs_http::sync::start(); #[cfg(target_os = "linux")] if crate::platform::current_is_wayland() { allow_err!(input_service::setup_uinput(0, 1920, 0, 1080).await); diff --git a/src/server/connection.rs b/src/server/connection.rs index c29faa724..9764820f3 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -26,7 +26,6 @@ use hbb_common::{ }; #[cfg(any(target_os = "android", target_os = "ios"))] use scrap::android::call_main_service_mouse_input; -use serde::Deserialize; use serde_json::{json, value::Value}; use sha2::{Digest, Sha256}; #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -40,6 +39,7 @@ pub type Sender = mpsc::UnboundedSender<(Instant, Arc)>; lazy_static::lazy_static! { static ref LOGIN_FAILURES: Arc::>> = Default::default(); static ref SESSIONS: Arc::>> = Default::default(); + static ref ALIVE_CONNS: Arc::>> = Default::default(); } pub static CLICK_TIME: AtomicI64 = AtomicI64::new(0); pub static MOUSE_MOVE_TIME: AtomicI64 = AtomicI64::new(0); @@ -74,7 +74,6 @@ pub struct Connection { read_jobs: Vec, timer: Interval, file_timer: Interval, - http_timer: Interval, file_transfer: Option<(String, bool)>, port_forward_socket: Option>, port_forward_address: String, @@ -147,6 +146,7 @@ impl Connection { challenge: Config::get_auto_password(6), ..Default::default() }; + ALIVE_CONNS.lock().unwrap().push(id); let (tx_from_cm_holder, mut rx_from_cm) = mpsc::unbounded_channel::(); // holding tx_from_cm_holder to avoid cpu burning of rx_from_cm.recv when all sender closed let tx_from_cm = tx_from_cm_holder.clone(); @@ -154,7 +154,7 @@ impl Connection { let (tx, mut rx) = mpsc::unbounded_channel::<(Instant, Arc)>(); let (tx_video, mut rx_video) = mpsc::unbounded_channel::<(Instant, Arc)>(); let (tx_input, rx_input) = std_mpsc::channel(); - let (tx_stop, mut rx_stop) = mpsc::unbounded_channel::(); + let mut hbbs_rx = crate::hbbs_http::sync::signal_receiver(); let tx_cloned = tx.clone(); let mut conn = Self { @@ -169,7 +169,6 @@ impl Connection { read_jobs: Vec::new(), timer: time::interval(SEC30), file_timer: time::interval(SEC30), - http_timer: time::interval(Duration::from_secs(3)), file_transfer: None, port_forward_socket: None, port_forward_address: "".to_owned(), @@ -393,12 +392,11 @@ impl Connection { conn.file_timer = time::interval_at(Instant::now() + SEC30, SEC30); } } - _ = conn.http_timer.tick() => { - Connection::post_heartbeat(conn.server_audit_conn.clone(), conn.inner.id, tx_stop.clone()); - }, - Some(reason) = rx_stop.recv() => { - conn.on_close_manually(&reason, &reason).await; - + Ok(conns) = hbbs_rx.recv() => { + if conns.contains(&id) { + conn.on_close_manually("web console", "web console").await; + break; + } } Some((instant, value)) = rx_video.recv() => { if !conn.video_ack_required { @@ -514,6 +512,7 @@ impl Connection { conn.post_conn_audit(json!({ "action": "close", })); + ALIVE_CONNS.lock().unwrap().retain(|&c| c != id); log::info!("#{} connection loop exited", id); } @@ -584,10 +583,10 @@ impl Connection { rx_from_cm: &mut mpsc::UnboundedReceiver, ) -> ResultType<()> { let mut last_recv_time = Instant::now(); - let (tx_stop, mut rx_stop) = mpsc::unbounded_channel::(); if let Some(mut forward) = self.port_forward_socket.take() { log::info!("Running port forwarding loop"); self.stream.set_raw(); + let mut hbbs_rx = crate::hbbs_http::sync::signal_receiver(); loop { tokio::select! { Some(data) = rx_from_cm.recv() => { @@ -618,10 +617,12 @@ impl Connection { if last_recv_time.elapsed() >= H1 { bail!("Timeout"); } - Connection::post_heartbeat(self.server_audit_conn.clone(), self.inner.id, tx_stop.clone()); } - Some(reason) = rx_stop.recv() => { - bail!(reason); + Ok(conns) = hbbs_rx.recv() => { + if conns.contains(&self.inner.id) { + // todo: check reconnect + bail!("Closed manually by the web console"); + } } } } @@ -711,30 +712,6 @@ impl Connection { }); } - fn post_heartbeat( - server_audit_conn: String, - conn_id: i32, - tx_stop: mpsc::UnboundedSender, - ) { - if server_audit_conn.is_empty() { - return; - } - let url = server_audit_conn.clone(); - let mut v = Value::default(); - v["id"] = json!(Config::get_id()); - v["uuid"] = json!(base64::encode(hbb_common::get_uuid())); - v["conn_id"] = json!(conn_id); - tokio::spawn(async move { - if let Ok(rsp) = Self::post_audit_async(url, v).await { - if let Ok(rsp) = serde_json::from_str::(&rsp) { - if rsp.action == "disconnect" { - tx_stop.send("web console".to_string()).ok(); - } - } - } - }); - } - fn post_file_audit( &self, r#type: FileAuditType, @@ -1710,6 +1687,10 @@ impl Connection { async fn send(&mut self, msg: Message) { allow_err!(self.stream.send(&msg).await); } + + pub fn alive_conns() -> Vec { + ALIVE_CONNS.lock().unwrap().clone() + } } #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -1867,13 +1848,6 @@ mod privacy_mode { } } -#[derive(Debug, Deserialize)] -struct ConnAuditResponse { - #[allow(dead_code)] - ret: bool, - action: String, -} - pub enum AlarmAuditType { IpWhitelist = 0, ManyWrongPassword = 1, From ad8e3c7555d7236501d78b855b68ed04aec9c0d6 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 10 Jan 2023 13:21:58 +0800 Subject: [PATCH 1455/2015] remove mac sign --- .github/workflows/flutter-ci.yml | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/.github/workflows/flutter-ci.yml b/.github/workflows/flutter-ci.yml index df47d07d1..738b57c4d 100644 --- a/.github/workflows/flutter-ci.yml +++ b/.github/workflows/flutter-ci.yml @@ -109,35 +109,6 @@ jobs: - name: Checkout source code uses: actions/checkout@v3 - - name: Import the codesign cert - uses: apple-actions/import-codesign-certs@v1 - with: - p12-file-base64: ${{ secrets.MACOS_P12_BASE64 }} - p12-password: ${{ secrets.MACOS_P12_PASSWORD }} - keychain: rustdesk - - - name: Check sign and import sign key - run: | - security default-keychain -s rustdesk.keychain - security find-identity -v - - - name: Import notarize key - uses: timheuer/base64-to-file@v1.2 - with: - # https://gregoryszorc.com/docs/apple-codesign/stable/apple_codesign_rcodesign.html#notarizing-and-stapling - fileName: rustdesk.json - fileDir: ${{ github.workspace }} - encodedString: ${{ secrets.MACOS_NOTARIZE_JSON }} - - - name: Install rcodesign tool - shell: bash - run: | - pushd /tmp - wget https://github.com/indygreg/apple-platform-rs/releases/download/apple-codesign%2F0.22.0/apple-codesign-0.22.0-macos-universal.tar.gz - tar -zxvf apple-codesign-0.22.0-macos-universal.tar.gz - mv apple-codesign-0.22.0-macos-universal/rcodesign /usr/local/bin - popd - - name: Install build runtime run: | brew install llvm create-dmg nasm yasm cmake gcc wget ninja From 00867276eda18923805ef78e15c341420a3dd04d Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 10 Jan 2023 14:11:49 +0800 Subject: [PATCH 1456/2015] fix wayland input Signed-off-by: fufesou --- flutter/lib/common/widgets/remote_input.dart | 4 +- .../desktop/screen/desktop_remote_screen.dart | 5 ++- flutter/lib/models/input_model.dart | 26 +++++++++++-- flutter/lib/models/state_model.dart | 1 + libs/enigo/src/linux/nix_impl.rs | 1 + src/common.rs | 6 +-- src/flutter_ffi.rs | 10 ++++- src/keyboard.rs | 38 ++++++++++++++----- src/server/input_service.rs | 2 +- src/ui_session_interface.rs | 3 +- 10 files changed, 75 insertions(+), 21 deletions(-) diff --git a/flutter/lib/common/widgets/remote_input.dart b/flutter/lib/common/widgets/remote_input.dart index 017850cf5..2d0dcacdf 100644 --- a/flutter/lib/common/widgets/remote_input.dart +++ b/flutter/lib/common/widgets/remote_input.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_hbb/models/state_model.dart'; import '../../models/input_model.dart'; @@ -25,7 +26,8 @@ class RawKeyFocusScope extends StatelessWidget { canRequestFocus: true, focusNode: focusNode, onFocusChange: onFocusChange, - onKey: inputModel.handleRawKeyEvent, + onKey: + stateGlobal.grabKeyboard ? inputModel.handleRawKeyEvent : null, child: child)); } } diff --git a/flutter/lib/desktop/screen/desktop_remote_screen.dart b/flutter/lib/desktop/screen/desktop_remote_screen.dart index e8361a652..bb6bc431b 100644 --- a/flutter/lib/desktop/screen/desktop_remote_screen.dart +++ b/flutter/lib/desktop/screen/desktop_remote_screen.dart @@ -3,6 +3,7 @@ import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/desktop/pages/remote_tab_page.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/desktop/widgets/refresh_wrapper.dart'; +import 'package:flutter_hbb/models/state_model.dart'; import 'package:provider/provider.dart'; /// multi-tab desktop remote screen @@ -10,7 +11,9 @@ class DesktopRemoteScreen extends StatelessWidget { final Map params; DesktopRemoteScreen({Key? key, required this.params}) : super(key: key) { - bind.mainStartGrabKeyboard(); + if (!bind.mainStartGrabKeyboard()) { + stateGlobal.grabKeyboard = true; + } } @override diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index 63f86078c..7356c6ec8 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -119,8 +119,11 @@ class InputModel { keyCode = newData.keyCode; } else if (e.data is RawKeyEventDataLinux) { RawKeyEventDataLinux newData = e.data as RawKeyEventDataLinux; - scanCode = newData.scanCode; - keyCode = newData.keyCode; + // scanCode and keyCode of RawKeyEventDataLinux are incorrect. + // 1. scanCode means keycode + // 2. keyCode means keysym + scanCode = 0; + keyCode = newData.scanCode; } else if (e.data is RawKeyEventDataAndroid) { RawKeyEventDataAndroid newData = e.data as RawKeyEventDataAndroid; scanCode = newData.scanCode + 8; @@ -135,16 +138,33 @@ class InputModel { } else { down = false; } - inputRawKey(e.character ?? "", keyCode, scanCode, down); + inputRawKey(e.character ?? '', keyCode, scanCode, down); } /// Send raw Key Event void inputRawKey(String name, int keyCode, int scanCode, bool down) { + const capslock = 1; + const numlock = 2; + const scrolllock = 3; + int lockModes = 0; + if (HardwareKeyboard.instance.lockModesEnabled + .contains(KeyboardLockMode.capsLock)) { + lockModes |= (1 << capslock); + } + if (HardwareKeyboard.instance.lockModesEnabled + .contains(KeyboardLockMode.numLock)) { + lockModes |= (1 << numlock); + } + if (HardwareKeyboard.instance.lockModesEnabled + .contains(KeyboardLockMode.scrollLock)) { + lockModes |= (1 << scrolllock); + } bind.sessionHandleFlutterKeyEvent( id: id, name: name, keycode: keyCode, scancode: scanCode, + lockModes: lockModes, downOrUp: down); } diff --git a/flutter/lib/models/state_model.dart b/flutter/lib/models/state_model.dart index 53f1a19b1..e4c9fa03f 100644 --- a/flutter/lib/models/state_model.dart +++ b/flutter/lib/models/state_model.dart @@ -9,6 +9,7 @@ import '../consts.dart'; class StateGlobal { int _windowId = -1; bool _fullscreen = false; + bool grabKeyboard = false; final RxBool _showTabBar = true.obs; final RxDouble _resizeEdgeSize = RxDouble(kWindowEdgeSize); final RxDouble _windowBorderWidth = RxDouble(kWindowBorderWidth); diff --git a/libs/enigo/src/linux/nix_impl.rs b/libs/enigo/src/linux/nix_impl.rs index d9be7b43b..f6e172677 100644 --- a/libs/enigo/src/linux/nix_impl.rs +++ b/libs/enigo/src/linux/nix_impl.rs @@ -183,6 +183,7 @@ impl MouseControllable for Enigo { fn get_led_state(key: Key) -> bool { let led_file = match key { + // FIXME: the file may be /sys/class/leds/input2 or input5 ... Key::CapsLock => "/sys/class/leds/input1::capslock/brightness", Key::NumLock => "/sys/class/leds/input1::numlock/brightness", _ => { diff --git a/src/common.rs b/src/common.rs index 0be84e79f..163937479 100644 --- a/src/common.rs +++ b/src/common.rs @@ -51,7 +51,7 @@ lazy_static::lazy_static! { pub fn global_init() -> bool { #[cfg(target_os = "linux")] { - if !scrap::is_x11() { + if !*IS_X11 { crate::server::wayland::set_wayland_scrap_map_err(); } } @@ -660,13 +660,13 @@ pub fn make_privacy_mode_msg(state: back_notification::PrivacyModeState) -> Mess #[cfg(not(target_os = "linux"))] lazy_static::lazy_static! { - pub static ref IS_X11: Mutex = Mutex::new(false); + pub static ref IS_X11: bool = false; } #[cfg(target_os = "linux")] lazy_static::lazy_static! { - pub static ref IS_X11: Mutex = Mutex::new("x11" == hbb_common::platform::linux::get_display_server()); + pub static ref IS_X11: bool = "x11" == hbb_common::platform::linux::get_display_server(); } pub fn make_fd_to_json(id: i32, path: String, entries: &Vec) -> String { diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 92f1e0606..b30e1e108 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -289,10 +289,11 @@ pub fn session_handle_flutter_key_event( name: String, keycode: i32, scancode: i32, + lock_modes: i32, down_or_up: bool, ) { if let Some(session) = SESSIONS.read().unwrap().get(&id) { - session.handle_flutter_key_event(&name, keycode, scancode, down_or_up); + session.handle_flutter_key_event(&name, keycode, scancode, lock_modes, down_or_up); } } @@ -1093,8 +1094,13 @@ pub fn main_is_installed() -> SyncReturn { SyncReturn(is_installed()) } -pub fn main_start_grab_keyboard() { +pub fn main_start_grab_keyboard() -> SyncReturn { + #[cfg(target_os = "linux")] + if !*crate::common::IS_X11 { + return SyncReturn(false); + } crate::keyboard::client::start_grab_loop(); + SyncReturn(true) } pub fn main_is_installed_lower_version() -> SyncReturn { diff --git a/src/keyboard.rs b/src/keyboard.rs index a11b0e97e..2d2aada23 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -99,11 +99,11 @@ pub mod client { } } - pub fn process_event(event: &Event) { + pub fn process_event(event: &Event, lock_modes: Option) { if is_long_press(&event) { return; } - if let Some(key_event) = event_to_key_event(&event) { + if let Some(key_event) = event_to_key_event(&event, lock_modes) { send_key_event(&key_event); } } @@ -196,7 +196,7 @@ pub fn start_grab_loop() { return Some(event); } if KEYBOARD_HOOKED.load(Ordering::SeqCst) { - client::process_event(&event); + client::process_event(&event, None); if is_press { return None; } else { @@ -222,7 +222,7 @@ pub fn start_grab_loop() { if let Key::Unknown(keycode) = key { log::error!("rdev get unknown key, keycode is : {:?}", keycode); } else { - client::process_event(&event); + client::process_event(&event, None); } None } @@ -254,7 +254,7 @@ pub fn release_remote_keys() { for key in to_release { let event_type = EventType::KeyRelease(key); let event = event_type_to_event(event_type); - client::process_event(&event); + client::process_event(&event, None); } } @@ -267,7 +267,23 @@ pub fn get_keyboard_mode_enum() -> KeyboardMode { } #[cfg(not(any(target_os = "android", target_os = "ios")))] -pub fn add_numlock_capslock_status(key_event: &mut KeyEvent) { +fn add_numlock_capslock_with_lock_modes(key_event: &mut KeyEvent, lock_modes: i32) { + const CAPS_LOCK: i32 = 1; + const NUM_LOCK: i32 = 2; + // const SCROLL_LOCK: i32 = 3; + if lock_modes & (1 << CAPS_LOCK) != 0 { + key_event.modifiers.push(ControlKey::CapsLock.into()); + } + if lock_modes & (1 << NUM_LOCK) != 0 { + key_event.modifiers.push(ControlKey::NumLock.into()); + } + // if lock_modes & (1 << SCROLL_LOCK) != 0 { + // key_event.modifiers.push(ControlKey::ScrollLock.into()); + // } +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +fn add_numlock_capslock_status(key_event: &mut KeyEvent) { if get_key_state(enigo::Key::CapsLock) { key_event.modifiers.push(ControlKey::CapsLock.into()); } @@ -315,7 +331,7 @@ fn update_modifiers_state(event: &Event) { }; } -pub fn event_to_key_event(event: &Event) -> Option { +pub fn event_to_key_event(event: &Event, lock_modes: Option) -> Option { let mut key_event = KeyEvent::new(); update_modifiers_state(event); @@ -345,8 +361,12 @@ pub fn event_to_key_event(event: &Event) -> Option { } } }; - #[cfg(not(any(target_os = "android", target_os = "ios")))] - add_numlock_capslock_status(&mut key_event); + if let Some(lock_modes) = lock_modes { + add_numlock_capslock_with_lock_modes(&mut key_event, lock_modes); + } else { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + add_numlock_capslock_status(&mut key_event); + } return Some(key_event); } diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 814fea110..2715a2643 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -882,7 +882,7 @@ fn map_keyboard_mode(evt: &KeyEvent) { // Wayland #[cfg(target_os = "linux")] - if !*IS_X11.lock().unwrap() { + if !*IS_X11 { let mut en = ENIGO.lock().unwrap(); let code = evt.chr() as u16; diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index c66e1fa3b..7bacb9b21 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -394,6 +394,7 @@ impl Session { name: &str, keycode: i32, scancode: i32, + lock_modes: i32, down_or_up: bool, ) { if scancode < 0 || keycode < 0 { @@ -420,7 +421,7 @@ impl Session { scan_code: scancode as _, event_type: event_type, }; - keyboard::client::process_event(&event); + keyboard::client::process_event(&event, Some(lock_modes)); } // flutter only TODO new input From b0deea57f68a67ecd468f3d8e630808d34f438ef Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 10 Jan 2023 14:41:25 +0800 Subject: [PATCH 1457/2015] id for cli --- src/cli.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/cli.rs b/src/cli.rs index 57d63d397..f8527c99c 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -72,6 +72,11 @@ impl Interface for Session { } async fn handle_hash(&mut self, pass: &str, hash: Hash, peer: &mut Stream) { + log::info!( + "id={}, password={}", + crate::ipc::get_id(), + hbb_common::password_security::temporary_password() + ); handle_hash(self.lc.clone(), &pass, hash, self, peer).await; } From 1291c840b9158bc92f25fa0045f7cd2991343e1c Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 10 Jan 2023 14:47:23 +0800 Subject: [PATCH 1458/2015] 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 2d2aada23..481b6f55c 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -362,6 +362,7 @@ pub fn event_to_key_event(event: &Event, lock_modes: Option) -> Option Date: Tue, 10 Jan 2023 14:48:27 +0800 Subject: [PATCH 1459/2015] trivial change Signed-off-by: fufesou --- src/keyboard.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/keyboard.rs b/src/keyboard.rs index 481b6f55c..183154ca0 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -361,11 +361,10 @@ pub fn event_to_key_event(event: &Event, lock_modes: Option) -> Option Date: Tue, 10 Jan 2023 15:01:46 +0800 Subject: [PATCH 1460/2015] fix cli --- src/cli.rs | 3 +-- src/main.rs | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index f8527c99c..117486ee4 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -73,8 +73,7 @@ impl Interface for Session { async fn handle_hash(&mut self, pass: &str, hash: Hash, peer: &mut Stream) { log::info!( - "id={}, password={}", - crate::ipc::get_id(), + "password={}", hbb_common::password_security::temporary_password() ); handle_hash(self.lc.clone(), &pass, hash, self, peer).await; diff --git a/src/main.rs b/src/main.rs index 67ddb875f..6500a8e4a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -91,6 +91,7 @@ fn main() { let token = LocalConfig::get_option("access_token"); cli::connect_test(p, key, token); } else if let Some(p) = matches.value_of("server") { + log::info!("id={}", hbb_common::config::Config::get_id()); crate::start_server(true); } common::global_clean(); From 4c6145dccf3af06d407fb11c7cdf5f2f5854899c Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 10 Jan 2023 15:12:36 +0800 Subject: [PATCH 1461/2015] remove unwrap() && fix input source group Signed-off-by: fufesou --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 86707fd62..8dfde0335 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5387,7 +5387,7 @@ checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" [[package]] name = "tfc" version = "0.6.1" -source = "git+https://github.com/fufesou/The-Fat-Controller#48303c5dacded6ea1873bc5d69bdde3175cf336a" +source = "git+https://github.com/fufesou/The-Fat-Controller#a5f13e6ef80327eb8d860aeb26b0af93eb5aee2b" dependencies = [ "core-graphics 0.22.3", "unicode-segmentation", From 5d6cb259da89b089e87a28ded917f38b43901577 Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 10 Jan 2023 16:07:48 +0800 Subject: [PATCH 1462/2015] do not show adjust window when scale adaptive Signed-off-by: fufesou --- .../lib/desktop/widgets/remote_menubar.dart | 134 +++++++++--------- 1 file changed, 67 insertions(+), 67 deletions(-) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index b69c73091..db95d33ca 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -956,77 +956,77 @@ class _RemoteMenubarState extends State { dismissOnClicked: true, )); displayMenu.insert(3, MenuEntryDivider()); - } - if (_isWindowCanBeAdjusted(remoteCount)) { - displayMenu.insert( - 0, - MenuEntryDivider(), - ); - displayMenu.insert( - 0, - MenuEntryButton( - childBuilder: (TextStyle? style) => Container( - child: Text( - translate('Adjust Window'), - style: style, - )), - proc: () { - () async { - await _updateScreen(); - if (_screen != null) { - _setFullscreen(false); - double scale = _screen!.scaleFactor; - final wndRect = - await WindowController.fromWindowId(windowId).getFrame(); - final mediaSize = MediaQueryData.fromWindow(ui.window).size; - // On windows, wndRect is equal to GetWindowRect and mediaSize is equal to GetClientRect. - // https://stackoverflow.com/a/7561083 - double magicWidth = - wndRect.right - wndRect.left - mediaSize.width * scale; - double magicHeight = - wndRect.bottom - wndRect.top - mediaSize.height * scale; + if (_isWindowCanBeAdjusted(remoteCount)) { + displayMenu.insert( + 0, + MenuEntryDivider(), + ); + displayMenu.insert( + 0, + MenuEntryButton( + childBuilder: (TextStyle? style) => Container( + child: Text( + translate('Adjust Window'), + style: style, + )), + proc: () { + () async { + await _updateScreen(); + if (_screen != null) { + _setFullscreen(false); + double scale = _screen!.scaleFactor; + final wndRect = + await WindowController.fromWindowId(windowId).getFrame(); + final mediaSize = MediaQueryData.fromWindow(ui.window).size; + // On windows, wndRect is equal to GetWindowRect and mediaSize is equal to GetClientRect. + // https://stackoverflow.com/a/7561083 + double magicWidth = + wndRect.right - wndRect.left - mediaSize.width * scale; + double magicHeight = + wndRect.bottom - wndRect.top - mediaSize.height * scale; - final canvasModel = widget.ffi.canvasModel; - final width = - (canvasModel.getDisplayWidth() * canvasModel.scale + - canvasModel.windowBorderWidth * 2) * - scale + - magicWidth; - final height = - (canvasModel.getDisplayHeight() * canvasModel.scale + - canvasModel.tabBarHeight + - canvasModel.windowBorderWidth * 2) * - scale + - magicHeight; - double left = wndRect.left + (wndRect.width - width) / 2; - double top = wndRect.top + (wndRect.height - height) / 2; + final canvasModel = widget.ffi.canvasModel; + final width = + (canvasModel.getDisplayWidth() * canvasModel.scale + + canvasModel.windowBorderWidth * 2) * + scale + + magicWidth; + final height = + (canvasModel.getDisplayHeight() * canvasModel.scale + + canvasModel.tabBarHeight + + canvasModel.windowBorderWidth * 2) * + scale + + magicHeight; + double left = wndRect.left + (wndRect.width - width) / 2; + double top = wndRect.top + (wndRect.height - height) / 2; - Rect frameRect = _screen!.frame; - if (!isFullscreen) { - frameRect = _screen!.visibleFrame; + Rect frameRect = _screen!.frame; + if (!isFullscreen) { + frameRect = _screen!.visibleFrame; + } + if (left < frameRect.left) { + left = frameRect.left; + } + if (top < frameRect.top) { + top = frameRect.top; + } + if ((left + width) > frameRect.right) { + left = frameRect.right - width; + } + if ((top + height) > frameRect.bottom) { + top = frameRect.bottom - height; + } + await WindowController.fromWindowId(windowId) + .setFrame(Rect.fromLTWH(left, top, width, height)); } - if (left < frameRect.left) { - left = frameRect.left; - } - if (top < frameRect.top) { - top = frameRect.top; - } - if ((left + width) > frameRect.right) { - left = frameRect.right - width; - } - if ((top + height) > frameRect.bottom) { - top = frameRect.bottom - height; - } - await WindowController.fromWindowId(windowId) - .setFrame(Rect.fromLTWH(left, top, width, height)); - } - }(); - }, - padding: padding, - dismissOnClicked: true, - ), - ); + }(); + }, + padding: padding, + dismissOnClicked: true, + ), + ); + } } /// Show Codec Preference From a3643f53bf1b80cc2a715f101ce199da2d42c1c1 Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 10 Jan 2023 17:13:40 +0800 Subject: [PATCH 1463/2015] set image center when remote resolution is changed Signed-off-by: fufesou --- flutter/lib/common.dart | 6 +++--- flutter/lib/consts.dart | 5 +++++ flutter/lib/desktop/pages/remote_tab_page.dart | 2 +- .../desktop/widgets/kb_layout_type_chooser.dart | 7 ++++--- flutter/lib/desktop/widgets/remote_menubar.dart | 12 ++++++------ flutter/lib/mobile/pages/remote_page.dart | 14 +++++++------- flutter/lib/models/file_model.dart | 3 ++- flutter/lib/models/model.dart | 7 ++++++- 8 files changed, 34 insertions(+), 22 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index ed78a8e09..93cbe135d 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -977,10 +977,10 @@ Future matchPeer(String searchText, Peer peer) async { /// Get the image for the current [platform]. Widget getPlatformImage(String platform, {double size = 50}) { - platform = platform.toLowerCase(); - if (platform == 'mac os') { + if (platform == kPeerPlatformMacOS) { platform = 'mac'; - } else if (platform != 'linux' && platform != 'android') { + } else if (platform != kPeerPlatformLinux && + platform != kPeerPlatformAndroid) { platform = 'win'; } return SvgPicture.asset('assets/$platform.svg', height: size, width: size); diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 7aa200ae9..e4081d9a5 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -5,6 +5,11 @@ import 'package:flutter_hbb/common.dart'; const double kDesktopRemoteTabBarHeight = 28.0; +const String kPeerPlatformWindows = "Windows"; +const String kPeerPlatformLinux = "Linux"; +const String kPeerPlatformMacOS = "Mac OS"; +const String kPeerPlatformAndroid = "Android"; + /// [kAppTypeMain] used by 'Desktop Main Page' , 'Mobile (Client and Server)' , 'Desktop CM Page', "Install Page" const String kAppTypeMain = "main"; const String kAppTypeDesktopRemote = "remote"; diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index 604787290..198b2aea7 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -308,7 +308,7 @@ class _ConnectionTabPageState extends State { dismissOnClicked: true, )); - if (pi.platform == 'Linux' || pi.sasEnabled) { + if (pi.platform == kPeerPlatformLinux || pi.sasEnabled) { menu.add(MenuEntryButton( childBuilder: (TextStyle? style) => Text( '${translate("Insert")} Ctrl + Alt + Del', diff --git a/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart b/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart index 58a8f7109..384b0f3bd 100644 --- a/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart +++ b/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:flutter_hbb/consts.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_hbb/models/platform_model.dart'; @@ -170,14 +171,14 @@ RxString KBLayoutType = ''.obs; String getLocalPlatformForKBLayoutType(String peerPlatform) { String localPlatform = ''; - if (peerPlatform != 'Mac OS') { + if (peerPlatform != kPeerPlatformMacOS) { return localPlatform; } if (Platform.isWindows) { - localPlatform = 'Windows'; + localPlatform = kPeerPlatformWindows; } else if (Platform.isLinux) { - localPlatform = 'Linux'; + localPlatform = kPeerPlatformLinux; } // to-do: web desktop support ? return localPlatform; diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index db95d33ca..fbeb2d469 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -589,7 +589,7 @@ class _RemoteMenubarState extends State { } displayMenu.add(MenuEntryDivider()); if (perms['keyboard'] != false) { - if (pi.platform == 'Linux' || pi.sasEnabled) { + if (pi.platform == kPeerPlatformLinux || pi.sasEnabled) { displayMenu.add(MenuEntryButton( childBuilder: (TextStyle? style) => Text( '${translate("Insert")} Ctrl + Alt + Del', @@ -604,9 +604,9 @@ class _RemoteMenubarState extends State { } } if (perms['restart'] != false && - (pi.platform == 'Linux' || - pi.platform == 'Windows' || - pi.platform == 'Mac OS')) { + (pi.platform == kPeerPlatformLinux || + pi.platform == kPeerPlatformWindows || + pi.platform == kPeerPlatformMacOS)) { displayMenu.add(MenuEntryButton( childBuilder: (TextStyle? style) => Text( translate('Restart Remote Device'), @@ -633,7 +633,7 @@ class _RemoteMenubarState extends State { dismissOnClicked: true, )); - if (pi.platform == 'Windows') { + if (pi.platform == kPeerPlatformWindows) { displayMenu.add(MenuEntryButton( childBuilder: (TextStyle? style) => Obx(() => Text( translate( @@ -1157,7 +1157,7 @@ class _RemoteMenubarState extends State { } if (Platform.isWindows && - pi.platform == 'Windows' && + pi.platform == kPeerPlatformWindows && perms['file'] != false) { displayMenu.add(_createSwitchMenuEntry( 'Allow file copy and paste', 'enable-file-transfer', padding, true)); diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 97ce6268d..f0c49e9a9 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -574,14 +574,14 @@ class _RemotePageState extends State { more.add(PopupMenuItem( child: Text(translate('Physical Keyboard Input Mode')), value: 'input-mode')); - if (pi.platform == 'Linux' || pi.sasEnabled) { + if (pi.platform == kPeerPlatformLinux || pi.sasEnabled) { more.add(PopupMenuItem( child: Text('${translate('Insert')} Ctrl + Alt + Del'), value: 'cad')); } more.add(PopupMenuItem( child: Text(translate('Insert Lock')), value: 'lock')); - if (pi.platform == 'Windows' && + if (pi.platform == kPeerPlatformWindows && await bind.sessionGetToggleOption(id: id, arg: 'privacy-mode') != true) { more.add(PopupMenuItem( @@ -591,9 +591,9 @@ class _RemotePageState extends State { } } if (perms["restart"] != false && - (pi.platform == "Linux" || - pi.platform == "Windows" || - pi.platform == "Mac OS")) { + (pi.platform == kPeerPlatformLinux || + pi.platform == kPeerPlatformWindows || + pi.platform == kPeerPlatformMacOS)) { more.add(PopupMenuItem( child: Text(translate('Restart Remote Device')), value: 'restart')); } @@ -740,7 +740,7 @@ class _RemotePageState extends State { } final pi = gFFI.ffiModel.pi; - final isMac = pi.platform == "Mac OS"; + final isMac = pi.platform == kPeerPlatformMacOS; final modifiers = [ wrap('Ctrl ', () { setState(() => inputModel.ctrl = !inputModel.ctrl); @@ -995,7 +995,7 @@ void showOptions( } more.add(getToggle( id, setState, 'lock-after-session-end', 'Lock after session end')); - if (pi.platform == 'Windows') { + if (pi.platform == kPeerPlatformWindows) { more.add(getToggle(id, setState, 'privacy-mode', 'Privacy mode')); } } diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index c08d2e623..b730e6074 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/consts.dart'; import 'package:get/get.dart'; import 'package:path/path.dart' as path; @@ -347,7 +348,7 @@ class FileModel extends ChangeNotifier { id: parent.target?.id ?? "", name: "remote_show_hidden")) .isNotEmpty; _remoteOption.isWindows = - parent.target?.ffiModel.pi.platform.toLowerCase() == "windows"; + parent.target?.ffiModel.pi.platform == kPeerPlatformWindows; await Future.delayed(Duration(milliseconds: 100)); diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 1cdecbd03..3a383d9a1 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -61,7 +61,7 @@ class FfiModel with ChangeNotifier { bool get touchMode => _touchMode; - bool get isPeerAndroid => _pi.platform == 'Android'; + bool get isPeerAndroid => _pi.platform == kPeerPlatformAndroid; set inputBlocked(v) { _inputBlocked = v; @@ -238,6 +238,11 @@ class FfiModel with ChangeNotifier { if ((_display.width > _display.height) != oldOrientation) { gFFI.canvasModel.updateViewStyle(); } + if (_pi.platform == kPeerPlatformLinux || + _pi.platform == kPeerPlatformWindows || + _pi.platform == kPeerPlatformMacOS) { + parent.target?.canvasModel.updateViewStyle(); + } parent.target?.recordingModel.onSwitchDisplay(); notifyListeners(); } From 8369026c5106821d4432cfa8e7f2ff3d1ff29dca Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 10 Jan 2023 17:30:21 +0800 Subject: [PATCH 1464/2015] remove mac untested --- .github/workflows/flutter-nightly.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index e1dfdbe74..967c85385 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -254,7 +254,7 @@ jobs: - name: Rename rustdesk run: | for name in rustdesk*??.dmg; do - mv "$name" "${name%%.dmg}-untested-${{ matrix.job.target }}.dmg" + mv "$name" "${name%%.dmg}-${{ matrix.job.target }}.dmg" done - name: Publish DMG package From aafccf423d07a84c60adc89197ec605395312182 Mon Sep 17 00:00:00 2001 From: ston <2424284164@qq.com> Date: Tue, 10 Jan 2023 18:14:41 +0800 Subject: [PATCH 1465/2015] fix session type when starting wayland via tty --- libs/hbb_common/src/platform/linux.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/hbb_common/src/platform/linux.rs b/libs/hbb_common/src/platform/linux.rs index a6ae2a9e7..e82416309 100644 --- a/libs/hbb_common/src/platform/linux.rs +++ b/libs/hbb_common/src/platform/linux.rs @@ -74,7 +74,7 @@ fn get_display_server_of_session(session: &str) -> String { } else { "".to_owned() }; - if display_server.is_empty() { + if display_server.is_empty() || display_server == "tty" { // loginctl has not given the expected output. try something else. if let Ok(sestype) = std::env::var("XDG_SESSION_TYPE") { display_server = sestype; From 5728c69e18ca8b5ed5ef64174f2954de2c984198 Mon Sep 17 00:00:00 2001 From: ilGigioVr88 Date: Tue, 10 Jan 2023 11:37:44 +0100 Subject: [PATCH 1466/2015] 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 ac3ea46fa..05ee237bd 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -411,7 +411,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Local keyboard type", "Tipo di tastiera locale"), ("Select local keyboard type", "Seleziona il tipo di tastiera locale"), ("software_render_tip", ""), - ("Always use software rendering", ""), + ("Always use software rendering", "Usa sempre il render Software"), ("config_input", ""), ].iter().cloned().collect(); } From 8ab2eddf1748af194f00801719a279f06d7c6c0a Mon Sep 17 00:00:00 2001 From: 21pages Date: Wed, 11 Jan 2023 10:40:26 +0800 Subject: [PATCH 1467/2015] opt is_recent_session Signed-off-by: 21pages --- src/server/connection.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/server/connection.rs b/src/server/connection.rs index c29faa724..610276f85 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1041,18 +1041,21 @@ impl Connection { false } - fn is_of_recent_session(&mut self) -> bool { + fn is_recent_session(&mut self) -> bool { let session = SESSIONS .lock() .unwrap() .get(&self.lr.my_id) .map(|s| s.to_owned()); + SESSIONS + .lock() + .unwrap() + .retain(|_, s| s.last_recv_time.lock().unwrap().elapsed() < SESSION_TIMEOUT); if let Some(session) = session { if session.name == self.lr.my_name && session.session_id == self.lr.session_id && !self.lr.password.is_empty() && self.validate_one_password(session.random_password.clone()) - && session.last_recv_time.lock().unwrap().elapsed() < SESSION_TIMEOUT { SESSIONS.lock().unwrap().insert( self.lr.my_id.clone(), @@ -1178,7 +1181,7 @@ impl Connection { { self.send_login_error("Connection not allowed").await; return false; - } else if self.is_of_recent_session() { + } else if self.is_recent_session() { self.try_start_cm(lr.my_id, lr.my_name, true); self.send_logon_response().await; if self.port_forward_socket.is_some() { From 70997acc7f784fcb36967c291ca633f6a3de1f57 Mon Sep 17 00:00:00 2001 From: solokot Date: Wed, 11 Jan 2023 09:28:51 +0300 Subject: [PATCH 1468/2015] update ru.rs --- src/lang/ru.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 6a9d2f297..43dd1cb08 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -210,11 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Всегда подключаться через ретрансляционный сервер"), ("whitelist_tip", "Только IP-адреса из белого списка могут получить доступ ко мне"), ("Login", "Войти"), - ("Verify", ""), - ("Remember me", ""), - ("Trust this device", ""), - ("Verification code", ""), - ("verification_tip", ""), + ("Verify", "Проверить"), + ("Remember me", "Запомнить"), + ("Trust this device", "Доверенное устройство"), + ("Verification code", "Проверочный код"), + ("verification_tip", "Обнаружено новое устройство, на зарегистрированный адрес электронной почты отправлен проверочный код. Введите его, чтобы продолжить вход в систему."), ("Logout", "Выйти"), ("Tags", "Метки"), ("Search ID", "Поиск по ID"), From 3cbcd2e46a654a6c055a30ad2a254eb567661203 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 11 Jan 2023 15:35:35 +0800 Subject: [PATCH 1469/2015] mac tray icon opt --- Cargo.toml | 2 +- .../macos/Runner.xcodeproj/project.pbxproj | 8 +++++++ flutter/pubspec.lock | 4 ++-- res/mac-tray-dark-x2.png | Bin 0 -> 809 bytes res/mac-tray-dark.png | Bin 809 -> 275 bytes res/mac-tray-light-x2.png | Bin 0 -> 810 bytes res/mac-tray-light.png | Bin 810 -> 270 bytes src/platform/macos.mm | 6 +++++ src/platform/macos.rs | 1 - src/tray.rs | 21 +++++++++++++----- 10 files changed, 33 insertions(+), 9 deletions(-) create mode 100644 res/mac-tray-dark-x2.png create mode 100644 res/mac-tray-light-x2.png diff --git a/Cargo.toml b/Cargo.toml index 2713df11d..427fcd4e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -155,7 +155,7 @@ 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"] +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 a8b5306be..8f11a09ed 100644 --- a/flutter/macos/Runner.xcodeproj/project.pbxproj +++ b/flutter/macos/Runner.xcodeproj/project.pbxproj @@ -28,6 +28,8 @@ 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,6 +80,8 @@ 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 = ""; }; @@ -131,6 +135,8 @@ 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 */, @@ -259,10 +265,12 @@ 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/flutter/pubspec.lock b/flutter/pubspec.lock index 807f932bb..228817422 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -264,8 +264,8 @@ packages: dependency: "direct main" description: path: "." - ref: "82f9eab81cb2c7bfb938def7a1b399a6279bbc75" - resolved-ref: "82f9eab81cb2c7bfb938def7a1b399a6279bbc75" + ref: "057e6eb1bc7dcbcf9dafd1384274a611e4fe7124" + resolved-ref: "057e6eb1bc7dcbcf9dafd1384274a611e4fe7124" url: "https://github.com/Kingtous/rustdesk_desktop_multi_window" source: git version: "0.1.0" diff --git a/res/mac-tray-dark-x2.png b/res/mac-tray-dark-x2.png new file mode 100644 index 0000000000000000000000000000000000000000..860f9fcf5f763cec08b68b4d7fc1eb4cf00e2c40 GIT binary patch literal 809 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sivoN?T!Hi;2MYvMt$SQP;xz=dFx){*RWnv{mVKkiQ?V zdVcyc-^*+j$Ib_rS82OFyzp9feZq3v{D;kfhVv2+>fJLr{ag7;#nI{AvrgVvIbFS2 zC$N>J^Ii4ZFM>w=?Cg?T*RVxunlQXQ@TF!#&B9JCAKAoPszr4oCEU?1O)uML`iiUw zPONeEJoW1!3-6u3k+bz@cB>^9Nhm%(>h{7{M)5M2W3iK3^{Hde7EF-HoPBuniapDs zdU9&*Q$5yZ`%ke=vu9-5xmqh*#cARtIgQceD8YNW`njxgN@xNA D=o26P literal 0 HcmV?d00001 diff --git a/res/mac-tray-dark.png b/res/mac-tray-dark.png index 860f9fcf5f763cec08b68b4d7fc1eb4cf00e2c40..ba8ed8c12cf19a82e8109f4d7b46e10b9afcff99 100644 GIT binary patch literal 275 zcmV+u0qp*XP)^LjVwr$(YZkfw&xwp8a*|yW9xU@X!&-i>hkKYa7Jw~wHWq(XD+h5mh z)W85|71G;cd)RG*1iihAVBP{t!VA6_2xKB~&2%u+g@{~&@v`Z2QavZ4WFe=LJk~`c z_fq^cpOWm-%-*;|XOgj+nVjM+U3CQo&E!FsuD*jQni&ticl8Dw)68M;nhgr+9Z}Em zh^~f5NpmvBfN{>qr`RoEm~RSEG}ppN7JM~KFxuM)Zgn8+x#1w_=D276n`64)?%1ab ZPy``YXG<|C3jzQD002ovPDHLkV1m$=e8m6& literal 809 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sivoN?T!Hi;2MYvMt$SQP;xz=dFx){*RWnv{mVKkiQ?V zdVcyc-^*+j$Ib_rS82OFyzp9feZq3v{D;kfhVv2+>fJLr{ag7;#nI{AvrgVvIbFS2 zC$N>J^Ii4ZFM>w=?Cg?T*RVxunlQXQ@TF!#&B9JCAKAoPszr4oCEU?1O)uML`iiUw zPONeEJoW1!3-6u3k+bz@cB>^9Nhm%(>h{7{M)5M2W3iK3^{Hde7EF-HoPBuniapDs zdU9&*Q$5yZ`%ke=vu9-5xmqh*#cARtIgQceD8YNW`njxgN@xNA D=o26P diff --git a/res/mac-tray-light-x2.png b/res/mac-tray-light-x2.png new file mode 100644 index 0000000000000000000000000000000000000000..f723d980e594a0042a9a94aac6aef9127ec3bf8c GIT binary patch literal 810 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sivoN?T!Hle!3|jS%9?<#G%X48 z3ua*X{r>rPkH?m}pC4S`E^%>RVX83C>l269ubw}>Eh{EOljH8WiLK=cY6?qh?G5?4 zdUO1`oXWiCi7_xRF?qT;hIkzBopyWLDg&N2XGg)0I$B*y71t!^9==!kKU~_H$8+(8 zr{$rW{+Z;?TEo!qlKIB(e3j^zz3(2fl+F6gc2DxfS02;bfpPlzyc-T&j@J|OY4h7! zV{=?+sj5fGJ?-uM6L(r$oLKR+c*0aCW(}Dcc`TFkO#*LS5nJ{);$44J-?mnb8Fe#m zx-?p4J(l)Ly7pZyN}xgF7Twf^~iz^mV3Ir zv{xNHaqYWv+m&6PTyG|SlRhwgdsSMmi$c(?Rw+G|n|Jq%l`P=NdSU8ymhHg882ts) zSiJo{%)h4Vy?DZl6RZ!KS+!dIXMFC|DX}#CTU5!wJwN7n$#W4Mo{78W8f|0aR@QPj zFkPna$pJw(ec4Q=xSci3+^2&?i>5tGP<&R^8((EHKR&x(^?FsU!=mQEyZ>{-A2jU_ zoOLI9uH7e(#}cVc-YXK@)6#!QVmsiLL6M`kmw-+SEK%^$z{rpQTs1w?%yb@bF(QoD4o*fHCjbk>Dey>_sNgO* z9o~aoQN>>HF*Ja&QN?8NKQw_cQN=`16Mlf5QN>R1DVzh>qlzma88!fpL>b2ct0Tfg z&>@j4F-}q^xEUFS`3dMS*Fv3u&xS>g(OvjN#{d8T literal 810 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sivoN?T!Hle!3|jS%9?<#G%X48 z3ua*X{r>rPkH?m}pC4S`E^%>RVX83C>l269ubw}>Eh{EOljH8WiLK=cY6?qh?G5?4 zdUO1`oXWiCi7_xRF?qT;hIkzBopyWLDg&N2XGg)0I$B*y71t!^9==!kKU~_H$8+(8 zr{$rW{+Z;?TEo!qlKIB(e3j^zz3(2fl+F6gc2DxfS02;bfpPlzyc-T&j@J|OY4h7! zV{=?+sj5fGJ?-uM6L(r$oLKR+c*0aCW(}Dcc`TFkO#*LS5nJ{);$44J-?mnb8Fe#m zx-?p4J(l)Ly7pZyN}xgF7Twf^~iz^mV3Ir zv{xNHaqYWv+m&6PTyG|SlRhwgdsSMmi$c(?Rw+G|n|Jq%l`P=NdSU8ymhHg882ts) zSiJo{%)h4Vy?DZl6RZ!KS+!dIXMFC|DX}#CTU5!wJwN7n$#W4Mo{78W8f|0aR@QPj zFkPna$pJw(ec4Q=xSci3+^2&?i>5tGP<&R^8((EHKR&x(^?FsU!=mQEyZ>{-A2jU_ zoOLI9uH7e(#}cVc-YXK@)6# bool { } pub fn quit_gui() { - use cocoa::appkit::NSApp; unsafe { let () = msg_send!(NSApp(), terminate: nil); }; diff --git a/src/tray.rs b/src/tray.rs index 98a4127a3..2ee423a9f 100644 --- a/src/tray.rs +++ b/src/tray.rs @@ -206,17 +206,28 @@ 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 { + let icon_path = match mode { dark_light::Mode::Dark => { - icon_path = "mac-tray-light.png"; + if f > 1. { + "mac-tray-light_x2.png"; + } else { + "mac-tray-light.png"; + } } dark_light::Mode::Light => { - icon_path = "mac-tray-dark.png"; + if f > 1. { + "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!( "{} {}", From 2ca65a4cf82edd0cf14a8cd2172b76dfeaae06bc Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 11 Jan 2023 15:50:20 +0800 Subject: [PATCH 1470/2015] fix ci --- src/tray.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/tray.rs b/src/tray.rs index 2ee423a9f..0c593c557 100644 --- a/src/tray.rs +++ b/src/tray.rs @@ -215,16 +215,16 @@ pub fn make_tray() { let icon_path = match mode { dark_light::Mode::Dark => { if f > 1. { - "mac-tray-light_x2.png"; + "mac-tray-light_x2.png" } else { - "mac-tray-light.png"; + "mac-tray-light.png" } } dark_light::Mode::Light => { if f > 1. { - "mac-tray-dark_x2.png"; + "mac-tray-dark_x2.png" } else { - "mac-tray-dark.png"; + "mac-tray-dark.png" } } }; From 037120fe02ebabe1026da8bc0e42cb3c89ebf2b0 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 11 Jan 2023 15:51:27 +0800 Subject: [PATCH 1471/2015] typo --- src/tray.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tray.rs b/src/tray.rs index 0c593c557..76dcf3c21 100644 --- a/src/tray.rs +++ b/src/tray.rs @@ -215,14 +215,14 @@ pub fn make_tray() { let icon_path = match mode { dark_light::Mode::Dark => { if f > 1. { - "mac-tray-light_x2.png" + "mac-tray-light-x2.png" } else { "mac-tray-light.png" } } dark_light::Mode::Light => { if f > 1. { - "mac-tray-dark_x2.png" + "mac-tray-dark-x2.png" } else { "mac-tray-dark.png" } From 9e9d6fa002c9547b92687de8facad041c63a77a3 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 11 Jan 2023 18:41:45 +0800 Subject: [PATCH 1472/2015] fix linux.svg --- flutter/lib/common.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 93cbe135d..1535c7ad8 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -982,6 +982,8 @@ Widget getPlatformImage(String platform, {double size = 50}) { } else if (platform != kPeerPlatformLinux && platform != kPeerPlatformAndroid) { platform = 'win'; + } else { + platform = 'linux'; } return SvgPicture.asset('assets/$platform.svg', height: size, width: size); } From 878111f32de47062d9b0ae7031f00292b1e71d6b Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 11 Jan 2023 18:49:06 +0800 Subject: [PATCH 1473/2015] fix a3643f53bf1b80cc2a715f101ce199da2d42c1c1 --- 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 1535c7ad8..9faa06d36 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -983,7 +983,7 @@ Widget getPlatformImage(String platform, {double size = 50}) { platform != kPeerPlatformAndroid) { platform = 'win'; } else { - platform = 'linux'; + platform = platform.toLowerCase(); } return SvgPicture.asset('assets/$platform.svg', height: size, width: size); } From 3102a241667c4cf4345f2896b5cb0654550d6b30 Mon Sep 17 00:00:00 2001 From: Asur4s Date: Wed, 11 Jan 2023 23:38:05 +0800 Subject: [PATCH 1474/2015] fix default keyboard mode when changing version --- src/client.rs | 10 +++++++++- src/common.rs | 7 +++++++ src/ui_session_interface.rs | 7 ++----- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/client.rs b/src/client.rs index 1bb2ff861..fc92c3674 100644 --- a/src/client.rs +++ b/src/client.rs @@ -7,6 +7,7 @@ use cpal::{ use magnum_opus::{Channels::*, Decoder as AudioDecoder}; use sha2::{Digest, Sha256}; use std::{ + str::FromStr, collections::HashMap, net::SocketAddr, ops::{Deref, Not}, @@ -48,7 +49,7 @@ pub mod file_trait; pub mod helper; pub mod io_loop; use crate::{ - common::is_keyboard_mode_supported, + 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); @@ -1419,6 +1420,13 @@ impl LoginConfigHandler { } else { config.keyboard_mode = KeyboardMode::Legacy.to_string(); } + } else { + let keyboard_modes = + common::get_supported_keyboard_modes(get_version_number(&pi.version)); + let current_mode = &KeyboardMode::from_str(&config.keyboard_mode).unwrap_or_default(); + if !keyboard_modes.contains(current_mode) { + config.keyboard_mode = KeyboardMode::Legacy.to_string(); + } } self.conn_id = pi.conn_id; // no matter if change, for update file time diff --git a/src/common.rs b/src/common.rs index 02b4a0c10..906ea2240 100644 --- a/src/common.rs +++ b/src/common.rs @@ -698,6 +698,13 @@ pub fn is_keyboard_mode_supported(keyboard_mode: &KeyboardMode, version_number: } } +pub fn get_supported_keyboard_modes(version: i64) -> Vec { + KeyboardMode::iter() + .filter(|&mode| is_keyboard_mode_supported(mode, version)) + .map(|&mode| mode) + .collect::>() +} + #[cfg(not(target_os = "linux"))] lazy_static::lazy_static! { pub static ref IS_X11: Mutex = Mutex::new(false); diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index e7ac620ee..33193bd9e 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -4,7 +4,7 @@ use crate::client::{ load_config, send_mouse, start_video_audio_threads, FileManager, Key, LoginConfigHandler, QualityStatus, KEY_MAP, }; -use crate::common::{is_keyboard_mode_supported, GrabState}; +use crate::common::{self, is_keyboard_mode_supported, GrabState}; use crate::keyboard; use crate::{client::Data, client::Interface}; use async_trait::async_trait; @@ -204,10 +204,7 @@ impl Session { pub fn get_supported_keyboard_modes(&self) -> Vec { let version = self.get_peer_version(); - KeyboardMode::iter() - .filter(|&mode| is_keyboard_mode_supported(mode, version)) - .map(|&mode| mode) - .collect::>() + common::get_supported_keyboard_modes(version) } pub fn remove_port_forward(&self, port: i32) { From 6ad249fa41460ccb5fc0a74d3e1c8a7c284c8261 Mon Sep 17 00:00:00 2001 From: Mr-Update <37781396+Mr-Update@users.noreply.github.com> Date: Wed, 11 Jan 2023 20:27:11 +0100 Subject: [PATCH 1475/2015] Update de.rs --- src/lang/de.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/lang/de.rs b/src/lang/de.rs index a91f167a2..a195fcdba 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -210,11 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Immer über Relay-Server verbinden"), ("whitelist_tip", "Nur IPs auf der Whitelist können zugreifen."), ("Login", "Anmelden"), - ("Verify", ""), - ("Remember me", ""), - ("Trust this device", ""), - ("Verification code", ""), - ("verification_tip", ""), + ("Verify", "Überprüfen"), + ("Remember me", "Login speichern"), + ("Trust this device", "Diesem Gerät vertrauen"), + ("Verification code", "Verifizierungscode"), + ("verification_tip", "Es wurde ein neues Gerät erkannt und ein Verifizierungscode an die registrierte E-Mail-Adresse gesendet. Geben Sie den Verifizierungscode ein, um sich weiter anzumelden."), ("Logout", "Abmelden"), ("Tags", "Schlagworte"), ("Search ID", "Suche ID"), From 5e2ef998a30ec781831a6105cced70c894d50237 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Wed, 11 Jan 2023 17:19:58 -0700 Subject: [PATCH 1476/2015] use RS_PUB_KEY env var If RS_PUB_KEY is set as an env variable use the env variable --- libs/hbb_common/src/config.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 1d427a2e9..970740045 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -82,7 +82,13 @@ pub const RENDEZVOUS_SERVERS: &'static [&'static str] = &[ "rs-sg.rustdesk.com", "rs-cn.rustdesk.com", ]; -pub const RS_PUB_KEY: &'static str = "OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw="; + +//check for env variable RS_PUB_KEY if not use default +pub const RS_PUB_KEY: &'static str = match option_env!("RS_PUB_KEY") { + Some(key) => key, + None => "OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw=", +}; + pub const RENDEZVOUS_PORT: i32 = 21116; pub const RELAY_PORT: i32 = 21117; @@ -107,7 +113,7 @@ macro_rules! serde_field_string { } #[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum NetworkType { +If RS_PUB_KEY is set as an env variable use the env variablepub enum NetworkType { Direct, ProxySocks, } From 860ccd6b3a8cffd9bce8fee153c0c4f273783a37 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Wed, 11 Jan 2023 17:35:47 -0700 Subject: [PATCH 1477/2015] add env variables for RENDEZVOUS_SERVERS check for env variable RENDEZVOUS_SERVER1-3 if not use the default --- libs/hbb_common/src/config.rs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 970740045..95b7944fc 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -77,10 +77,20 @@ const CHARS: &'static [char] = &[ 'm', 'n', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', ]; +//check for env variable RENDEZVOUS_SERVER1-3 if not use the default pub const RENDEZVOUS_SERVERS: &'static [&'static str] = &[ - "rs-ny.rustdesk.com", - "rs-sg.rustdesk.com", - "rs-cn.rustdesk.com", + match option_env!("RENDEZVOUS_SERVER1") { + Some(key) => key, + None => "rs-ny.rustdesk.com", + }, + match option_env!("RENDEZVOUS_SERVER2") { + Some(key) => key, + None => "rs-sg.rustdesk.com", + }, + match option_env!("RENDEZVOUS_SERVER3") { + Some(key) => key, + None => "rs-cn.rustdesk.com", + }, ]; //check for env variable RS_PUB_KEY if not use default From 4c8a3b7adc8a492ad5bf67985986d1975094d0cf Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Wed, 11 Jan 2023 17:41:15 -0700 Subject: [PATCH 1478/2015] Update flutter-nightly.yml --- .github/workflows/flutter-nightly.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 967c85385..775bbd29b 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -15,6 +15,10 @@ env: # for multiarch gcc compatibility VCPKG_COMMIT_ID: "14e7bb4ae24616ec54ff6b2f6ef4e8659434ea44" VERSION: "1.2.0" + RS_PUB_KEY: '${{ secrets.RS_PUB_KEY }}' + RENDEZVOUS_SERVER1: '${{ secrets.RENDEZVOUS_SERVER1 }}' + RENDEZVOUS_SERVER2: '${{ secrets.RENDEZVOUS_SERVER2 }}' + RENDEZVOUS_SERVER3: '${{ secrets.RENDEZVOUS_SERVER3 }}' jobs: build-for-windows: From dfc37a0a0b1f579dd25675089bbcedf8b095c203 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Wed, 11 Jan 2023 17:49:17 -0700 Subject: [PATCH 1479/2015] disable keys on osx if NO_OSX_KEYS is set as a secret do not sign the osx build --- .github/workflows/flutter-nightly.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 775bbd29b..3bf5ebce3 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -19,6 +19,7 @@ env: RENDEZVOUS_SERVER1: '${{ secrets.RENDEZVOUS_SERVER1 }}' RENDEZVOUS_SERVER2: '${{ secrets.RENDEZVOUS_SERVER2 }}' RENDEZVOUS_SERVER3: '${{ secrets.RENDEZVOUS_SERVER3 }}' + NO_OSX_KEYS: ${{ secrets.NO_OSX_KEYS == 'False' }} jobs: build-for-windows: @@ -154,6 +155,7 @@ jobs: uses: actions/checkout@v3 - name: Import the codesign cert + if: ${{ env.NO_OSX_KEYS!= 'true' }} uses: apple-actions/import-codesign-certs@v1 with: p12-file-base64: ${{ secrets.MACOS_P12_BASE64 }} @@ -161,11 +163,13 @@ jobs: keychain: rustdesk - name: Check sign and import sign key + if: ${{ env.NO_OSX_KEYS!= 'true' }} run: | security default-keychain -s rustdesk.keychain security find-identity -v - name: Import notarize key + if: ${{ env.NO_OSX_KEYS!= 'true' }} uses: timheuer/base64-to-file@v1.2 with: # https://gregoryszorc.com/docs/apple-codesign/stable/apple_codesign_rcodesign.html#notarizing-and-stapling @@ -174,6 +178,7 @@ jobs: encodedString: ${{ secrets.MACOS_NOTARIZE_JSON }} - name: Install rcodesign tool + if: ${{ env.NO_OSX_KEYS!= 'true' }} shell: bash run: | pushd /tmp @@ -244,6 +249,7 @@ jobs: ./build.py --flutter ${{ matrix.job.extra-build-args }} - name: Codesign app and create signed dmg + if: ${{ env.NO_OSX_KEYS!= 'true' }} run: | security default-keychain -s rustdesk.keychain security unlock-keychain -p ${{ secrets.MACOS_P12_PASSWORD }} rustdesk.keychain From 61b4ea7b85384d88a80d5370201780907ad865dd Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Wed, 11 Jan 2023 17:53:17 -0700 Subject: [PATCH 1480/2015] flip boolean --- .github/workflows/flutter-nightly.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 3bf5ebce3..556396193 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -19,7 +19,7 @@ env: RENDEZVOUS_SERVER1: '${{ secrets.RENDEZVOUS_SERVER1 }}' RENDEZVOUS_SERVER2: '${{ secrets.RENDEZVOUS_SERVER2 }}' RENDEZVOUS_SERVER3: '${{ secrets.RENDEZVOUS_SERVER3 }}' - NO_OSX_KEYS: ${{ secrets.NO_OSX_KEYS == 'False' }} + NO_OSX_KEYS: ${{ secrets.NO_OSX_KEYS == 'true' }} jobs: build-for-windows: From f21bc352d5138af41f0276d5a8ac857ec8e89d54 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Wed, 11 Jan 2023 17:56:01 -0700 Subject: [PATCH 1481/2015] test workflow if check --- .github/workflows/flutter-nightly.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 556396193..e692497f9 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -155,7 +155,7 @@ jobs: uses: actions/checkout@v3 - name: Import the codesign cert - if: ${{ env.NO_OSX_KEYS!= 'true' }} + if: ${{ env.NO_OSX_KEYS== 'true' }} uses: apple-actions/import-codesign-certs@v1 with: p12-file-base64: ${{ secrets.MACOS_P12_BASE64 }} From dc9c3ca0082f4278f5ef664a922aaccbd563b529 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Wed, 11 Jan 2023 17:58:20 -0700 Subject: [PATCH 1482/2015] update all if booleans --- .github/workflows/flutter-nightly.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index e692497f9..370b7e5a5 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -163,13 +163,13 @@ jobs: keychain: rustdesk - name: Check sign and import sign key - if: ${{ env.NO_OSX_KEYS!= 'true' }} + if: ${{ env.NO_OSX_KEYS== 'true' }} run: | security default-keychain -s rustdesk.keychain security find-identity -v - name: Import notarize key - if: ${{ env.NO_OSX_KEYS!= 'true' }} + if: ${{ env.NO_OSX_KEYS== 'true' }} uses: timheuer/base64-to-file@v1.2 with: # https://gregoryszorc.com/docs/apple-codesign/stable/apple_codesign_rcodesign.html#notarizing-and-stapling @@ -178,7 +178,7 @@ jobs: encodedString: ${{ secrets.MACOS_NOTARIZE_JSON }} - name: Install rcodesign tool - if: ${{ env.NO_OSX_KEYS!= 'true' }} + if: ${{ env.NO_OSX_KEYS== 'true' }} shell: bash run: | pushd /tmp @@ -249,7 +249,7 @@ jobs: ./build.py --flutter ${{ matrix.job.extra-build-args }} - name: Codesign app and create signed dmg - if: ${{ env.NO_OSX_KEYS!= 'true' }} + if: ${{ env.NO_OSX_KEYS== 'true' }} run: | security default-keychain -s rustdesk.keychain security unlock-keychain -p ${{ secrets.MACOS_P12_PASSWORD }} rustdesk.keychain From ada75b602cc57abc45abd43b123f1134f74a12bd Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Wed, 11 Jan 2023 18:29:09 -0700 Subject: [PATCH 1483/2015] update to RS_PUB_KEY_VAL --- libs/hbb_common/src/config.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 95b7944fc..b22ed5e75 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -94,7 +94,7 @@ pub const RENDEZVOUS_SERVERS: &'static [&'static str] = &[ ]; //check for env variable RS_PUB_KEY if not use default -pub const RS_PUB_KEY: &'static str = match option_env!("RS_PUB_KEY") { +pub const RS_PUB_KEY: &'static str = match option_env!("RS_PUB_KEY_VAL") { Some(key) => key, None => "OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw=", }; From 8dd138c7e2048803e3c43202f790c6d03087e070 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Wed, 11 Jan 2023 18:30:08 -0700 Subject: [PATCH 1484/2015] RS_PUB_KEY_VAL update --- .github/workflows/flutter-nightly.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 370b7e5a5..b5ebb1175 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -15,7 +15,7 @@ env: # for multiarch gcc compatibility VCPKG_COMMIT_ID: "14e7bb4ae24616ec54ff6b2f6ef4e8659434ea44" VERSION: "1.2.0" - RS_PUB_KEY: '${{ secrets.RS_PUB_KEY }}' + RS_PUB_KEY_VAL: '${{ secrets.RS_PUB_KEY_VAL }}' RENDEZVOUS_SERVER1: '${{ secrets.RENDEZVOUS_SERVER1 }}' RENDEZVOUS_SERVER2: '${{ secrets.RENDEZVOUS_SERVER2 }}' RENDEZVOUS_SERVER3: '${{ secrets.RENDEZVOUS_SERVER3 }}' From 94057d0df5524244f02d5f6464bb2cc73793addd Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Wed, 11 Jan 2023 18:59:21 -0700 Subject: [PATCH 1485/2015] Update config.rs --- libs/hbb_common/src/config.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index b22ed5e75..e53bda578 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -123,7 +123,7 @@ macro_rules! serde_field_string { } #[derive(Clone, Copy, PartialEq, Eq, Debug)] -If RS_PUB_KEY is set as an env variable use the env variablepub enum NetworkType { +pub enum NetworkType { Direct, ProxySocks, } From a34781f4bee53d5991c42580e1fdf2d6c6e5f307 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Wed, 11 Jan 2023 23:11:52 -0700 Subject: [PATCH 1486/2015] add NO_APP_KEYS --- .github/workflows/flutter-nightly.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index b5ebb1175..f420cce62 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -20,6 +20,7 @@ env: RENDEZVOUS_SERVER2: '${{ secrets.RENDEZVOUS_SERVER2 }}' RENDEZVOUS_SERVER3: '${{ secrets.RENDEZVOUS_SERVER3 }}' NO_OSX_KEYS: ${{ secrets.NO_OSX_KEYS == 'true' }} + NO_APP_KEYS: ${{ secrets.NO_APP_KEYS == 'true' }} jobs: build-for-windows: @@ -561,6 +562,7 @@ jobs: - uses: r0adkll/sign-android-release@v1 name: Sign app APK + if: ${{ env.NO_APP_KEYS== 'true' }} id: sign-rustdesk with: releaseDirectory: ./signed-apk From 829fd97e6f13cdbb09821f8c895b3d504f0cb6b6 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Wed, 11 Jan 2023 23:32:45 -0700 Subject: [PATCH 1487/2015] change server check check for custom server by pub_key not for just the option --- src/ui_interface.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 9984198b8..f45216d0c 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -243,7 +243,11 @@ pub fn set_peer_option(id: String, name: String, value: String) { #[inline] pub fn using_public_server() -> bool { - crate::get_custom_rendezvous_server(get_option_("custom-rendezvous-server")).is_empty() + if hbb_common::config::RS_PUB_KEY == "OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw=" { + return true + } else { + return false + } } #[inline] From 00dbec3eeee42456175d4f3128f07277a5b62ee8 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Wed, 11 Jan 2023 23:58:58 -0700 Subject: [PATCH 1488/2015] Create flutter-custom-build.yml --- .github/workflows/flutter-custom-build.yml | 1506 ++++++++++++++++++++ 1 file changed, 1506 insertions(+) create mode 100644 .github/workflows/flutter-custom-build.yml diff --git a/.github/workflows/flutter-custom-build.yml b/.github/workflows/flutter-custom-build.yml new file mode 100644 index 000000000..f445ef073 --- /dev/null +++ b/.github/workflows/flutter-custom-build.yml @@ -0,0 +1,1506 @@ +name: Flutter Custom Build + +on: + workflow_dispatch: + +env: + LLVM_VERSION: "10.0" + # Note: currently 3.0.5 does not support arm64 officially, we use latest stable version first. + FLUTTER_VERSION: "3.0.5" + TAG_NAME: "nightly" + # vcpkg version: 2022.05.10 + # for multiarch gcc compatibility + VCPKG_COMMIT_ID: "14e7bb4ae24616ec54ff6b2f6ef4e8659434ea44" + VERSION: "1.2.0" + #To make a custom build with your own servers set the below secret values + RS_PUB_KEY_VAL: '${{ secrets.RS_PUB_KEY_VAL }}' + RENDEZVOUS_SERVER1: '${{ secrets.RENDEZVOUS_SERVER1 }}' + RENDEZVOUS_SERVER2: '${{ secrets.RENDEZVOUS_SERVER2 }}' + RENDEZVOUS_SERVER3: '${{ secrets.RENDEZVOUS_SERVER3 }}' + #ignore signing with key files if values below are set + NO_OSX_KEYS: ${{ secrets.NO_OSX_KEYS == 'true' }} + NO_APP_KEYS: ${{ secrets.NO_APP_KEYS == 'true' }} + +jobs: + build-for-windows: + name: ${{ matrix.job.target }} (${{ matrix.job.os }}) + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: false + matrix: + job: + # - { target: i686-pc-windows-msvc , os: windows-2019 } + # - { target: x86_64-pc-windows-gnu , os: windows-2019 } + - { target: x86_64-pc-windows-msvc, os: windows-2019 } + steps: + - name: Checkout source code + uses: actions/checkout@v3 + + - name: Install LLVM and Clang + uses: KyleMayes/install-llvm-action@v1 + with: + version: ${{ env.LLVM_VERSION }} + + - name: Install flutter + uses: subosito/flutter-action@v2 + with: + channel: "stable" + flutter-version: ${{ env.FLUTTER_VERSION }} + cache: true + + - name: Replace engine with rustdesk custom flutter engine + run: | + flutter doctor -v + flutter precache --windows + Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.0.5-rustdesk.2/windows-x64-flutter-release.zip -OutFile windows-x64-flutter-release.zip + Expand-Archive windows-x64-flutter-release.zip -DestinationPath engine + mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-3.0.5-x64/bin/cache/artifacts/engine/windows-x64-release/ + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: "1.62" + target: ${{ matrix.job.target }} + override: true + components: rustfmt + profile: minimal # minimal component installation (ie, no documentation) + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: ${{ matrix.job.os }} + + - name: Install flutter rust bridge deps + run: | + dart pub global activate ffigen --version 5.0.1 + $exists = Test-Path ~/.cargo/bin/flutter_rust_bridge_codegen.exe + Push-Location .. + git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1 + Push-Location flutter_rust_bridge/frb_codegen ; cargo install --path . ; Pop-Location + Pop-Location + Push-Location flutter ; flutter pub get ; Pop-Location + ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart + + - name: Restore from cache and install vcpkg + uses: lukka/run-vcpkg@v7 + with: + setupOnly: true + vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} + + - name: Install vcpkg dependencies + run: | + $VCPKG_ROOT/vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static + shell: bash + + - name: Build rustdesk + run: python3 .\build.py --portable --hwcodec --flutter + + - name: Sign rustdesk files + uses: GermanBluefox/code-sign-action@v7 + with: + certificate: '${{ secrets.WINDOWS_PFX_BASE64 }}' + password: '${{ secrets.WINDOWS_PFX_PASSWORD }}' + certificatesha1: '${{ secrets.WINDOWS_PFX_SHA1_THUMBPRINT }}' + # certificatename: '${{ secrets.CERTNAME }}' + folder: './flutter/build/windows/runner/Release/' + recursive: true + + - name: Build self-extracted executable + shell: bash + run: | + pushd ./libs/portable + python3 ./generate.py -f ../../flutter/build/windows/runner/Release/ -o . -e ../../flutter/build/windows/runner/Release/rustdesk.exe + popd + mkdir -p ./SignOutput + mv ./target/release/rustdesk-portable-packer.exe ./SignOutput/rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}.exe + + # - name: Rename rustdesk + # shell: bash + # run: | + # for name in rustdesk*??-install.exe; do + # mv "$name" ./SignOutput/"${name%%-install.exe}-${{ matrix.job.target }}.exe" + # done + + - name: Sign rustdesk self-extracted file + uses: GermanBluefox/code-sign-action@v7 + with: + certificate: '${{ secrets.WINDOWS_PFX_BASE64 }}' + password: '${{ secrets.WINDOWS_PFX_PASSWORD }}' + certificatesha1: '${{ secrets.WINDOWS_PFX_SHA1_THUMBPRINT }}' + # certificatename: '${{ secrets.WINDOWS_PFX_NAME }}' + folder: './SignOutput' + recursive: false + + - name: Publish Release + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + ./SignOutput/rustdesk-*.exe + + build-for-macOS: + name: ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-args }}] + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: false + matrix: + job: + - { + target: x86_64-apple-darwin, + os: macos-latest, + extra-build-args: "", + } + steps: + - name: Checkout source code + uses: actions/checkout@v3 + + - name: Import the codesign cert + if: ${{ env.NO_OSX_KEYS== 'true' }} + uses: apple-actions/import-codesign-certs@v1 + with: + p12-file-base64: ${{ secrets.MACOS_P12_BASE64 }} + p12-password: ${{ secrets.MACOS_P12_PASSWORD }} + keychain: rustdesk + + - name: Check sign and import sign key + if: ${{ env.NO_OSX_KEYS== 'true' }} + run: | + security default-keychain -s rustdesk.keychain + security find-identity -v + + - name: Import notarize key + if: ${{ env.NO_OSX_KEYS== 'true' }} + uses: timheuer/base64-to-file@v1.2 + with: + # https://gregoryszorc.com/docs/apple-codesign/stable/apple_codesign_rcodesign.html#notarizing-and-stapling + fileName: rustdesk.json + fileDir: ${{ github.workspace }} + encodedString: ${{ secrets.MACOS_NOTARIZE_JSON }} + + - name: Install rcodesign tool + if: ${{ env.NO_OSX_KEYS== 'true' }} + shell: bash + run: | + pushd /tmp + wget https://github.com/indygreg/apple-platform-rs/releases/download/apple-codesign%2F0.22.0/apple-codesign-0.22.0-macos-universal.tar.gz + tar -zxvf apple-codesign-0.22.0-macos-universal.tar.gz + mv apple-codesign-0.22.0-macos-universal/rcodesign /usr/local/bin + popd + + - name: Install build runtime + run: | + brew install llvm create-dmg nasm yasm cmake gcc wget ninja + + - name: Install flutter + uses: subosito/flutter-action@v2 + with: + channel: "stable" + flutter-version: ${{ env.FLUTTER_VERSION }} + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: ${{ matrix.job.target }} + override: true + profile: minimal # minimal component installation (ie, no documentation) + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: ${{ matrix.job.os }} + + - name: Install flutter rust bridge deps + shell: bash + run: | + dart pub global activate ffigen --version 5.0.1 + # flutter_rust_bridge + pushd /tmp + wget https://github.com/Kingtous/flutter_rust_bridge/releases/download/1.32.0-rustdesk/flutter_rust_bridge_codegen-x86_64-darwin.tgz + tar -zxvf flutter_rust_bridge_codegen-x86_64-darwin.tgz + mkdir -p ~/.cargo/bin + mv flutter_rust_bridge_codegen ~/.cargo/bin; chmod +x ~/.cargo/bin/flutter_rust_bridge_codegen + popd + pushd flutter && flutter pub get && popd + ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart + + - name: Restore from cache and install vcpkg + uses: lukka/run-vcpkg@v7 + with: + setupOnly: true + vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} + + - name: Install vcpkg dependencies + run: | + $VCPKG_ROOT/vcpkg install libvpx libyuv opus + + - name: Show version information (Rust, cargo, Clang) + shell: bash + run: | + clang --version || true + rustup -V + rustup toolchain list + rustup default + cargo -V + rustc -V + + - name: Build rustdesk + run: | + # --hwcodec not supported on macos yet + ./build.py --flutter ${{ matrix.job.extra-build-args }} + + - name: Codesign app and create signed dmg + if: ${{ env.NO_OSX_KEYS== 'true' }} + run: | + security default-keychain -s rustdesk.keychain + 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 + 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 + # notarize the rustdesk-${{ env.VERSION }}.dmg + rcodesign notary-submit --api-key-path ${{ github.workspace }}/rustdesk.json --staple rustdesk-${{ env.VERSION }}.dmg + + - name: Rename rustdesk + run: | + for name in rustdesk*??.dmg; do + mv "$name" "${name%%.dmg}-${{ matrix.job.target }}.dmg" + done + + - name: Publish DMG package + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + rustdesk*-${{ matrix.job.target }}.dmg + + build-vcpkg-deps-linux: + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: false + matrix: + job: + # - { arch: armv7, os: ubuntu-20.04 } + - { arch: x86_64, os: ubuntu-20.04 } + - { arch: aarch64, os: ubuntu-20.04 } + steps: + - name: Create vcpkg artifacts folder + run: mkdir -p /opt/artifacts + + - name: Cache Vcpkg + id: cache-vcpkg + uses: actions/cache@v3 + with: + path: /opt/artifacts + key: vcpkg-${{ matrix.job.arch }} + + - uses: Kingtous/run-on-arch-action@amd64-support + name: Run vcpkg install on ${{ matrix.job.arch }} + id: vcpkg + with: + arch: ${{ matrix.job.arch }} + distro: ubuntu18.04 + githubToken: ${{ github.token }} + setup: | + ls -l "/opt/artifacts" + dockerRunArgs: | + --volume "/opt/artifacts:/artifacts" + shell: /bin/bash + install: | + apt update -y + case "${{ matrix.job.arch }}" in + x86_64) + # CMake 3.15+ + apt install -y gpg wget ca-certificates + echo 'deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ bionic main' | tee /etc/apt/sources.list.d/kitware.list >/dev/null + wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/null | gpg --dearmor - | tee /usr/share/keyrings/kitware-archive-keyring.gpg >/dev/null + apt update -y + apt install -y curl zip unzip tar git cmake g++ gcc build-essential pkg-config wget nasm yasm ninja-build libjpeg8-dev + ;; + aarch64|armv7) + apt install -y curl zip unzip tar git cmake g++ gcc build-essential pkg-config wget nasm yasm ninja-build libjpeg8-dev automake libtool + esac + cmake --version + gcc -v + run: | + # disable git safe.directory + git config --global --add safe.directory "*" + case "${{ matrix.job.arch }}" in + x86_64) + export VCPKG_FORCE_SYSTEM_BINARIES=1 + pushd /artifacts + git clone https://github.com/microsoft/vcpkg.git || true + pushd vcpkg + git reset --hard ${{ env.VCPKG_COMMIT_ID }} + ./bootstrap-vcpkg.sh + ./vcpkg install libvpx libyuv opus + ;; + aarch64|armv7) + pushd /artifacts + # libyuv + git clone https://chromium.googlesource.com/libyuv/libyuv || true + pushd libyuv + git pull + mkdir -p build + pushd build + mkdir -p /artifacts/vcpkg/installed + cmake .. -DCMAKE_INSTALL_PREFIX=/artifacts/vcpkg/installed + make -j4 && make install + popd + popd + # libopus, ubuntu 18.04 prebuilt is not be compiled with -fPIC + wget -O opus.tar.gz http://archive.ubuntu.com/ubuntu/pool/main/o/opus/opus_1.1.2.orig.tar.gz + tar -zxvf opus.tar.gz; ls -l + pushd opus-1.1.2 + ./autogen.sh; ./configure --prefix=/artifacts/vcpkg/installed + make -j4; make install + ;; + esac + - name: Upload artifacts + uses: actions/upload-artifact@master + with: + name: vcpkg-artifact-${{ matrix.job.arch }} + path: | + /opt/artifacts/vcpkg/installed + + generate-bridge-linux: + name: generate bridge + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: false + matrix: + job: + - { + target: x86_64-unknown-linux-gnu, + os: ubuntu-18.04, + extra-build-args: "", + } + steps: + - name: Checkout source code + uses: actions/checkout@v3 + + - name: Install prerequisites + run: | + sudo apt update -y + sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang cmake libclang-dev ninja-build llvm-dev libclang-10-dev llvm-10-dev pkg-config + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: ${{ matrix.job.target }} + override: true + profile: minimal # minimal component installation (ie, no documentation) + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: bridge-${{ matrix.job.os }} + workspace: "/tmp/flutter_rust_bridge/frb_codegen" + + - name: Cache Bridge + id: cache-bridge + uses: actions/cache@v3 + with: + path: /tmp/flutter_rust_bridge + key: vcpkg-${{ matrix.job.arch }} + + - name: Install flutter + uses: subosito/flutter-action@v2 + with: + channel: "stable" + flutter-version: ${{ env.FLUTTER_VERSION }} + cache: true + + - name: Install ffigen + run: | + dart pub global activate ffigen --version 5.0.1 + + - name: Install flutter rust bridge deps + shell: bash + run: | + pushd /tmp && git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1 || true && popd + pushd /tmp/flutter_rust_bridge/frb_codegen && cargo install --path . && popd + pushd flutter && flutter pub get && popd + + - name: Run flutter rust bridge + run: | + ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart + + - name: Upload Artifact + uses: actions/upload-artifact@master + with: + name: bridge-artifact + path: | + ./src/bridge_generated.rs + ./flutter/lib/generated_bridge.dart + ./flutter/lib/generated_bridge.freezed.dart + + build-rustdesk-android-arm64: + needs: [generate-bridge-linux] + name: build rustdesk android apk ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: false + matrix: + job: + - { + arch: x86_64, + target: aarch64-linux-android, + os: ubuntu-18.04, + extra-build-features: "", + } + # - { + # arch: x86_64, + # target: armv7-linux-androideabi, + # os: ubuntu-18.04, + # extra-build-features: "", + # } + steps: + - name: Install dependencies + run: | + sudo apt update + sudo apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libvdpau-dev libva-dev libclang-dev llvm-dev libclang-10-dev llvm-10-dev pkg-config tree g++ libc6-dev gcc-multilib g++-multilib openjdk-11-jdk-headless + - name: Checkout source code + uses: actions/checkout@v3 + - name: Install flutter + uses: subosito/flutter-action@v2 + with: + channel: "stable" + flutter-version: ${{ env.FLUTTER_VERSION }} + - uses: nttld/setup-ndk@v1 + id: setup-ndk + with: + ndk-version: r22b + add-to-path: true + + - name: Download deps + shell: bash + run: | + pushd /opt + wget https://github.com/rustdesk/doc.rustdesk.com/releases/download/console/dep.tar.gz + tar xzf dep.tar.gz + + - name: Restore bridge files + uses: actions/download-artifact@master + with: + name: bridge-artifact + path: ./ + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + override: true + profile: minimal # minimal component installation (ie, no documentation) + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: rustdesk-lib-cache + key: ${{ matrix.job.target }}-${{ matrix.job.extra-build-features }} + + - name: Disable rust bridge build + run: | + sed -i "s/gen_flutter_rust_bridge();/\/\//g" build.rs + + - name: Build rustdesk lib + env: + ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} + ANDROID_NDK_ROOT: ${{ steps.setup-ndk.outputs.ndk-path }} + VCPKG_ROOT: /opt/vcpkg + run: | + rustup target add ${{ matrix.job.target }} + cargo install cargo-ndk + case ${{ matrix.job.target }} in + aarch64-linux-android) + ./flutter/ndk_arm64.sh + mkdir -p ./flutter/android/app/src/main/jniLibs/arm64-v8a + cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so + ;; + armv7-linux-androideabi) + ./flutter/ndk_arm.sh + mkdir -p ./flutter/android/app/src/main/jniLibs/armeabi-v7a + cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/librustdesk.so + ;; + esac + + - name: Build rustdesk + shell: bash + env: + JAVA_HOME: /usr/lib/jvm/java-11-openjdk-amd64 + run: | + export PATH=/usr/lib/jvm/java-11-openjdk-amd64/bin:$PATH + # download so + pushd flutter + wget -O so.tar.gz https://github.com/rustdesk/doc.rustdesk.com/releases/download/console/so.tar.gz + tar xzvf so.tar.gz + popd + # temporary use debug sign config + sed -i "s/signingConfigs.release/signingConfigs.debug/g" ./flutter/android/app/build.gradle + case ${{ matrix.job.target }} in + aarch64-linux-android) + mkdir -p ./flutter/android/app/src/main/jniLibs/arm64-v8a + cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so + # build flutter + pushd flutter + flutter build apk --release --target-platform android-arm64 --split-per-abi + mv build/app/outputs/flutter-apk/app-arm64-v8a-release.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk + ;; + armv7-linux-androideabi) + mkdir -p ./flutter/android/app/src/main/jniLibs/armeabi-v7a + cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/librustdesk.so + # build flutter + pushd flutter + flutter build apk --release --target-platform android-arm --split-per-abi + mv build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk + ;; + esac + popd + mkdir -p signed-apk; pushd signed-apk + mv ../rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk . + + - uses: r0adkll/sign-android-release@v1 + name: Sign app APK + if: ${{ env.NO_APP_KEYS== 'true' }} + id: sign-rustdesk + with: + releaseDirectory: ./signed-apk + signingKeyBase64: ${{ secrets.ANDROID_SIGNING_KEY }} + alias: ${{ secrets.ANDROID_ALIAS }} + keyStorePassword: ${{ secrets.ANDROID_KEY_STORE_PASSWORD }} + keyPassword: ${{ secrets.ANDROID_KEY_PASSWORD }} + env: + # override default build-tools version (29.0.3) -- optional + BUILD_TOOLS_VERSION: "30.0.2" + + - name: Upload Artifacts + uses: actions/upload-artifact@master + with: + name: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release-signed.apk + path: ${{steps.sign-rustdesk.outputs.signedReleaseFile}} + + - name: Publish apk package + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + ${{steps.sign-rustdesk.outputs.signedReleaseFile}} + + build-rustdesk-lib-linux-amd64: + needs: [generate-bridge-linux, build-vcpkg-deps-linux] + name: build-rust-lib ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: false + matrix: + # use a high level qemu-user-static + job: + # - { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } + # - { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } + - { + arch: x86_64, + target: x86_64-unknown-linux-gnu, + os: ubuntu-20.04, + extra-build-features: "", + } + - { + arch: x86_64, + target: x86_64-unknown-linux-gnu, + os: ubuntu-20.04, + extra-build-features: "flatpak", + } + - { + arch: x86_64, + target: x86_64-unknown-linux-gnu, + os: ubuntu-20.04, + extra-build-features: "appimage", + } + # - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } + steps: + - name: Maximize build space + run: | + sudo rm -rf /opt/ghc + sudo rm -rf /usr/local/lib/android + sudo rm -rf /usr/share/dotnet + sudo apt update -y + sudo apt install qemu-user-static + + - name: Checkout source code + uses: actions/checkout@v3 + + - name: Set Swap Space + uses: pierotofy/set-swap-space@master + with: + swap-size-gb: 12 + + - name: Free Space + run: | + df + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: ${{ matrix.job.target }} + override: true + profile: minimal # minimal component installation (ie, no documentation) + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: rustdesk-lib-cache + key: ${{ matrix.job.target }}-${{ matrix.job.extra-build-features }} + cache-directories: "/opt/rust-registry" + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: ${{ matrix.job.target }} + override: true + profile: minimal # minimal component installation (ie, no documentation) + + - name: Install local registry + run: | + mkdir -p /opt/rust-registry + cargo install cargo-local-registry + + - name: Build local registry + uses: nick-fields/retry@v2 + id: build-local-registry + continue-on-error: true + with: + max_attempts: 3 + timeout_minutes: 15 + retry_on: error + command: cargo local-registry --sync ./Cargo.lock /opt/rust-registry + + - name: Disable rust bridge build + run: | + sed -i "s/gen_flutter_rust_bridge();/\/\//g" build.rs + # only build cdylib + sed -i "s/\[\"cdylib\", \"staticlib\", \"rlib\"\]/\[\"cdylib\"\]/g" Cargo.toml + + - name: Restore bridge files + uses: actions/download-artifact@master + with: + name: bridge-artifact + path: ./ + + - name: Restore vcpkg files + uses: actions/download-artifact@master + with: + name: vcpkg-artifact-${{ matrix.job.arch }} + path: /opt/artifacts/vcpkg/installed + + - uses: Kingtous/run-on-arch-action@amd64-support + name: Build rustdesk library for ${{ matrix.job.arch }} + id: vcpkg + with: + arch: ${{ matrix.job.arch }} + distro: ubuntu18.04 + # not ready yet + # distro: ubuntu18.04-rustdesk + githubToken: ${{ github.token }} + setup: | + ls -l "${PWD}" + ls -l /opt/artifacts/vcpkg/installed + dockerRunArgs: | + --volume "${PWD}:/workspace" + --volume "/opt/artifacts:/opt/artifacts" + --volume "/opt/rust-registry:/opt/rust-registry" + shell: /bin/bash + install: | + apt update -y + echo -e "installing deps" + apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libvdpau-dev libva-dev libclang-dev llvm-dev libclang-10-dev llvm-10-dev pkg-config tree g++ gcc libvpx-dev tree > /dev/null + # we have libopus compiled by us. + apt remove -y libopus-dev || true + # output devs + ls -l ./ + tree -L 3 /opt/artifacts/vcpkg/installed + run: | + # disable git safe.directory + git config --global --add safe.directory "*" + # rust + pushd /opt + wget -O rust.tar.gz https://static.rust-lang.org/dist/rust-1.64.0-${{ matrix.job.target }}.tar.gz + tar -zxvf rust.tar.gz > /dev/null && rm rust.tar.gz + cd rust-1.64.0-${{ matrix.job.target }} && ./install.sh + rm -rf rust-1.64.0-${{ matrix.job.target }} + # edit config + mkdir -p ~/.cargo/ + echo """ + [source.crates-io] + registry = 'https://github.com/rust-lang/crates.io-index' + replace-with = 'local-registry' + + [source.local-registry] + local-registry = '/opt/rust-registry/' + """ > ~/.cargo/config + cat ~/.cargo/config + # start build + pushd /workspace + # mock + case "${{ matrix.job.arch }}" in + x86_64) + # no need mock on x86_64 + export VCPKG_ROOT=/opt/artifacts/vcpkg + cargo build --lib --features hwcodec,flutter,${{ matrix.job.extra-build-features }} --release + ;; + esac + + - name: Upload Artifacts + uses: actions/upload-artifact@master + with: + name: librustdesk-${{ matrix.job.arch }}-${{ matrix.job.extra-build-features }}.so + path: target/release/liblibrustdesk.so + + build-rustdesk-lib-linux-arm: + needs: [generate-bridge-linux, build-vcpkg-deps-linux] + name: build-rust-lib ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: false + matrix: + # use a high level qemu-user-static + job: + - { + arch: aarch64, + target: aarch64-unknown-linux-gnu, + os: ubuntu-20.04, + use-cross: true, + extra-build-features: "", + } + - { + arch: aarch64, + target: aarch64-unknown-linux-gnu, + os: ubuntu-18.04, # just for naming package, not running host + use-cross: true, + extra-build-features: "appimage", + } + # - { arch: aarch64, target: aarch64-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true, extra-build-features: "flatpak" } + # - { + # arch: armv7, + # target: arm-unknown-linux-gnueabihf, + # os: ubuntu-20.04, + # use-cross: true, + # extra-build-features: "", + # } + # - { arch: armv7, target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true, extra-build-features: "flatpak" } + # - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true } + steps: + - name: Maximize build space + run: | + sudo rm -rf /opt/ghc + sudo rm -rf /usr/local/lib/android + sudo rm -rf /usr/share/dotnet + sudo apt update -y + sudo apt install qemu-user-static + + - name: Checkout source code + uses: actions/checkout@v3 + + - name: Set Swap Space + uses: pierotofy/set-swap-space@master + with: + swap-size-gb: 12 + + - name: Free Space + run: | + df + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: ${{ matrix.job.target }} + override: true + profile: minimal # minimal component installation (ie, no documentation) + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: ${{ matrix.job.target }} + override: true + profile: minimal # minimal component installation (ie, no documentation) + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: rustdesk-lib-cache + key: ${{ matrix.job.target }}-${{ matrix.job.extra-build-features }} + cache-directories: "/opt/rust-registry" + + - name: Install local registry + run: | + mkdir -p /opt/rust-registry + cargo install cargo-local-registry + + - name: Build local registry + uses: nick-fields/retry@v2 + id: build-local-registry + continue-on-error: true + with: + max_attempts: 3 + timeout_minutes: 15 + retry_on: error + command: cargo local-registry --sync ./Cargo.lock /opt/rust-registry + + - name: Disable rust bridge build + run: | + sed -i "s/gen_flutter_rust_bridge();/\/\//g" build.rs + # only build cdylib + sed -i "s/\[\"cdylib\", \"staticlib\", \"rlib\"\]/\[\"cdylib\"\]/g" Cargo.toml + + - name: Restore bridge files + uses: actions/download-artifact@master + with: + name: bridge-artifact + path: ./ + + - name: Restore vcpkg files + uses: actions/download-artifact@master + with: + name: vcpkg-artifact-${{ matrix.job.arch }} + path: /opt/artifacts/vcpkg/installed + + - uses: Kingtous/run-on-arch-action@amd64-support + name: Build rustdesk library for ${{ matrix.job.arch }} + id: vcpkg + with: + arch: ${{ matrix.job.arch }} + distro: ubuntu18.04-rustdesk + githubToken: ${{ github.token }} + setup: | + ls -l "${PWD}" + ls -l /opt/artifacts/vcpkg/installed + dockerRunArgs: | + --volume "${PWD}:/workspace" + --volume "/opt/artifacts:/opt/artifacts" + --volume "/opt/rust-registry:/opt/rust-registry" + shell: /bin/bash + install: | + apt update -y + echo -e "installing deps" + apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libvdpau-dev libva-dev libclang-dev llvm-dev libclang-10-dev llvm-10-dev pkg-config tree g++ gcc libvpx-dev tree > /dev/null + # we have libopus compiled by us. + apt remove -y libopus-dev || true + # output devs + ls -l ./ + tree -L 3 /opt/artifacts/vcpkg/installed + run: | + # disable git safe.directory + git config --global --add safe.directory "*" + # rust + pushd /opt + wget -O rust.tar.gz https://static.rust-lang.org/dist/rust-1.64.0-${{ matrix.job.target }}.tar.gz + tar -zxvf rust.tar.gz > /dev/null && rm rust.tar.gz + cd rust-1.64.0-${{ matrix.job.target }} && ./install.sh + rm -rf rust-1.64.0-${{ matrix.job.target }} + # edit config + mkdir -p ~/.cargo/ + echo """ + [source.crates-io] + registry = 'https://github.com/rust-lang/crates.io-index' + replace-with = 'local-registry' + + [source.local-registry] + local-registry = '/opt/rust-registry/' + """ > ~/.cargo/config + cat ~/.cargo/config + # start build + pushd /workspace + # mock + case "${{ matrix.job.arch }}" in + aarch64) + cp -r /opt/artifacts/vcpkg/installed/lib/* /usr/lib/aarch64-linux-gnu/ + cp -r /opt/artifacts/vcpkg/installed/include/* /usr/include/ + ls -l /opt/artifacts/vcpkg/installed/lib/ + mkdir -p /vcpkg/installed/arm64-linux + ln -s /usr/lib/aarch64-linux-gnu /vcpkg/installed/arm64-linux/lib + ln -s /usr/include /vcpkg/installed/arm64-linux/include + export VCPKG_ROOT=/vcpkg + # disable hwcodec for compilation + cargo build --lib --features flutter,${{ matrix.job.extra-build-features }} --release + ;; + armv7) + cp -r /opt/artifacts/vcpkg/installed/lib/* /usr/lib/arm-linux-gnueabihf/ + cp -r /opt/artifacts/vcpkg/installed/include/* /usr/include/ + mkdir -p /vcpkg/installed/arm-linux + ln -s /usr/lib/arm-linux-gnueabihf /vcpkg/installed/arm-linux/lib + ln -s /usr/include /vcpkg/installed/arm-linux/include + export VCPKG_ROOT=/vcpkg + # disable hwcodec for compilation + cargo build --lib --features flutter,${{ matrix.job.extra-build-features }} --release + ;; + esac + + - name: Upload Artifacts + uses: actions/upload-artifact@master + with: + name: librustdesk-${{ matrix.job.arch }}-${{ matrix.job.extra-build-features }}.so + path: target/release/liblibrustdesk.so + + build-rustdesk-linux-arm: + needs: [build-rustdesk-lib-linux-arm] + name: build-rustdesk ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] + runs-on: ubuntu-20.04 # 20.04 has more performance on arm build + strategy: + fail-fast: false + matrix: + job: + - { + arch: aarch64, + target: aarch64-unknown-linux-gnu, + os: ubuntu-18.04, # just for naming package, not running host + use-cross: true, + extra-build-features: "", + } + - { + arch: aarch64, + target: aarch64-unknown-linux-gnu, + os: ubuntu-18.04, # just for naming package, not running host + use-cross: true, + extra-build-features: "appimage", + } + # - { + # arch: aarch64, + # target: aarch64-unknown-linux-gnu, + # os: ubuntu-18.04, # just for naming package, not running host + # use-cross: true, + # extra-build-features: "flatpak", + # } + # - { arch: armv7, target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true, extra-build-features: "" } + # - { arch: armv7, target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true, extra-build-features: "flatpak" } + # - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true } + steps: + - name: Checkout source code + uses: actions/checkout@v3 + + - name: Restore bridge files + uses: actions/download-artifact@master + with: + name: bridge-artifact + path: ./ + + - name: Prepare env + run: | + sudo apt update -y + sudo apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev libarchive-tools + mkdir -p ./target/release/ + + - name: Restore the rustdesk lib file + uses: actions/download-artifact@master + with: + name: librustdesk-${{ matrix.job.arch }}-${{ matrix.job.extra-build-features }}.so + path: ./target/release/ + + - name: Download Flutter + shell: bash + run: | + # disable git safe.directory + git config --global --add safe.directory "*" + pushd /opt + # clone repo and reset to flutter 3.0.5 + git clone https://github.com/sony/flutter-elinux.git || true + pushd flutter-elinux + # reset to flutter 3.0.5 + git fetch + git reset --hard b09a90eee643859ce4e676839227edd9fd3feba8 + popd + + - uses: Kingtous/run-on-arch-action@amd64-support + name: Build rustdesk binary for ${{ matrix.job.arch }} + id: vcpkg + with: + arch: ${{ matrix.job.arch }} + distro: ubuntu18.04-rustdesk + githubToken: ${{ github.token }} + setup: | + ls -l "${PWD}" + dockerRunArgs: | + --volume "${PWD}:/workspace" + --volume "/opt/artifacts:/opt/artifacts" + --volume "/opt/flutter-elinux:/opt/flutter-elinux" + shell: /bin/bash + install: | + apt update -y + apt-get -qq install -y git cmake g++ gcc build-essential nasm yasm curl unzip xz-utils python3 wget pkg-config ninja-build pkg-config libgtk-3-dev liblzma-dev clang libappindicator3-dev rpm + run: | + # disable git safe.directory + git config --global --add safe.directory "*" + pushd /workspace + # we use flutter-elinux to build our rustdesk + sed -i "s/flutter build linux --release/flutter-elinux build linux/g" ./build.py + # Setup flutter-elinux + export PATH=/opt/flutter-elinux/bin:$PATH + flutter-elinux doctor -v + # edit to corresponding arch + case ${{ matrix.job.arch }} in + aarch64) + sed -i "s/Architecture: amd64/Architecture: arm64/g" ./build.py + sed -i "s/x64\/release/arm64\/release/g" ./build.py + ;; + armv7) + sed -i "s/Architecture: amd64/Architecture: arm/g" ./build.py + sed -i "s/x64\/release/arm\/release/g" ./build.py + ;; + esac + python3 ./build.py --flutter --hwcodec --skip-cargo + # rpm package + echo -e "start packaging" + pushd /workspace + case ${{ matrix.job.arch }} in + armv7) + sed -i "s/64bit/32bit/g" ./res/rpm-flutter.spec + sed -i "s/linux\/x64/linux\/arm/g" ./res/rpm-flutter.spec + ;; + aarch64) + sed -i "s/linux\/x64/linux\/arm64/g" ./res/rpm-flutter.spec + ;; + esac + HBB=`pwd` rpmbuild ./res/rpm-flutter.spec -bb + pushd ~/rpmbuild/RPMS/${{ matrix.job.arch }} + mkdir -p /opt/artifacts/rpm + for name in rustdesk*??.rpm; do + mv "$name" "/opt/artifacts/rpm/${name%%.rpm}-fedora28-centos8.rpm" + done + + - name: Rename rustdesk + shell: bash + run: | + for name in rustdesk*??.deb; do + cp "$name" "${name%%.deb}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb" + done + + - name: Publish debian package + if: ${{ matrix.job.extra-build-features == '' }} + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb + + - name: Build appimage package + if: ${{ matrix.job.extra-build-features == 'appimage' }} + shell: bash + run: | + # set-up appimage-builder + pushd /tmp + wget -O appimage-builder-x86_64.AppImage https://github.com/AppImageCrafters/appimage-builder/releases/download/v1.1.0/appimage-builder-1.1.0-x86_64.AppImage + chmod +x appimage-builder-x86_64.AppImage + sudo mv appimage-builder-x86_64.AppImage /usr/local/bin/appimage-builder + popd + # run appimage-builder + pushd appimage + sudo appimage-builder --skip-tests --recipe ./AppImageBuilder-${{ matrix.job.arch }}.yml + + - name: Publish appimage package + if: ${{ matrix.job.extra-build-features == 'appimage' }} + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + ./appimage/rustdesk-${{ env.VERSION }}-*.AppImage + + - name: Upload Artifact + uses: actions/upload-artifact@master + if: ${{ contains(matrix.job.extra-build-features, 'flatpak') }} + with: + name: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb + path: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb + + - name: Patch archlinux PKGBUILD + if: ${{ matrix.job.extra-build-features == '' }} + run: | + sed -i "s/arch=('x86_64')/arch=('${{ matrix.job.arch }}')/g" res/PKGBUILD + case ${{ matrix.job.arch }} in + armv7) + sed -i "s/linux\/x64/linux\/arm/g" ./res/PKGBUILD + ;; + aarch64) + sed -i "s/linux\/x64/linux\/arm64/g" ./res/PKGBUILD + ;; + esac + + # Temporary disable for there is no many archlinux arm hosts + # - name: Build archlinux package + # if: ${{ matrix.job.extra-build-features == '' }} + # uses: vufa/arch-makepkg-action@master + # with: + # packages: > + # llvm + # clang + # libva + # libvdpau + # rust + # gstreamer + # unzip + # git + # cmake + # gcc + # curl + # wget + # yasm + # nasm + # zip + # make + # pkg-config + # clang + # gtk3 + # xdotool + # libxcb + # libxfixes + # alsa-lib + # pipewire + # python + # ttf-arphic-uming + # libappindicator-gtk3 + # scripts: | + # cd res && HBB=`pwd`/.. FLUTTER=1 makepkg -f + + # - name: Publish archlinux package + # if: ${{ matrix.job.extra-build-features == '' }} + # uses: softprops/action-gh-release@v1 + # with: + # prerelease: true + # tag_name: ${{ env.TAG_NAME }} + # files: | + # res/rustdesk*.zst + + - name: Publish fedora28/centos8 package + if: ${{ matrix.job.extra-build-features == '' }} + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + /opt/artifacts/rpm/*.rpm + + build-rustdesk-linux-amd64: + needs: [build-rustdesk-lib-linux-amd64] + name: build-rustdesk ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] + runs-on: ubuntu-20.04 + strategy: + fail-fast: false + matrix: + job: + # - { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } + # - { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } + - { + arch: x86_64, + target: x86_64-unknown-linux-gnu, + os: ubuntu-18.04, + extra-build-features: "", + } + - { + arch: x86_64, + target: x86_64-unknown-linux-gnu, + os: ubuntu-18.04, + extra-build-features: "flatpak", + } + - { + arch: x86_64, + target: x86_64-unknown-linux-gnu, + os: ubuntu-18.04, + extra-build-features: "appimage", + } + # - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } + steps: + - name: Checkout source code + uses: actions/checkout@v3 + + - name: Restore bridge files + uses: actions/download-artifact@master + with: + name: bridge-artifact + path: ./ + + - name: Prepare env + run: | + sudo apt update -y + sudo apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev libarchive-tools + mkdir -p ./target/release/ + + - name: Restore the rustdesk lib file + uses: actions/download-artifact@master + with: + name: librustdesk-${{ matrix.job.arch }}-${{ matrix.job.extra-build-features }}.so + path: ./target/release/ + + - uses: Kingtous/run-on-arch-action@amd64-support + name: Build rustdesk binary for ${{ matrix.job.arch }} + id: vcpkg + with: + arch: ${{ matrix.job.arch }} + distro: ubuntu18.04 + githubToken: ${{ github.token }} + setup: | + ls -l "${PWD}" + dockerRunArgs: | + --volume "${PWD}:/workspace" + --volume "/opt/artifacts:/opt/artifacts" + shell: /bin/bash + install: | + apt update -y + apt-get -qq install -y git cmake g++ gcc build-essential nasm yasm curl unzip xz-utils python3 wget pkg-config ninja-build pkg-config libgtk-3-dev liblzma-dev clang libappindicator3-dev rpm + run: | + # disable git safe.directory + git config --global --add safe.directory "*" + # Setup Flutter + pushd /opt + wget https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_${{ env.FLUTTER_VERSION }}-stable.tar.xz + tar xf flutter_linux_${{ env.FLUTTER_VERSION }}-stable.tar.xz + ls -l . + export PATH=/opt/flutter/bin:$PATH + flutter doctor -v + pushd /workspace + python3 ./build.py --flutter --hwcodec --skip-cargo + # rpm package + pushd /workspace + case ${{ matrix.job.arch }} in + armv7) + sed -i "s/64bit/32bit/g" ./res/rpm-flutter.spec + ;; + esac + HBB=`pwd` rpmbuild ./res/rpm-flutter.spec -bb + pushd ~/rpmbuild/RPMS/${{ matrix.job.arch }} + mkdir -p /opt/artifacts/rpm + for name in rustdesk*??.rpm; do + mv "$name" "/opt/artifacts/rpm/${name%%.rpm}-fedora28-centos8.rpm" + done + + - name: Rename rustdesk + shell: bash + run: | + for name in rustdesk*??.deb; do + # use cp to duplicate deb files to fit other packages. + cp "$name" "${name%%.deb}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb" + done + + - name: Publish debian package + if: ${{ matrix.job.extra-build-features == '' }} + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb + + - name: Upload Artifact + uses: actions/upload-artifact@master + if: ${{ contains(matrix.job.extra-build-features, 'flatpak') }} + with: + name: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb + path: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb + + - name: Patch archlinux PKGBUILD + if: ${{ matrix.job.extra-build-features == '' }} + run: | + sed -i "s/arch=('x86_64')/arch=('${{ matrix.job.arch }}')/g" res/PKGBUILD + + - name: Build archlinux package + if: ${{ matrix.job.extra-build-features == '' }} + uses: vufa/arch-makepkg-action@master + with: + packages: > + llvm + clang + libva + libvdpau + rust + gstreamer + unzip + git + cmake + gcc + curl + wget + yasm + nasm + zip + make + pkg-config + clang + gtk3 + xdotool + libxcb + libxfixes + alsa-lib + pipewire + python + ttf-arphic-uming + libappindicator-gtk3 + scripts: | + cd res && HBB=`pwd`/.. FLUTTER=1 makepkg -f + + - name: Publish archlinux package + if: ${{ matrix.job.extra-build-features == '' }} + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + res/rustdesk*.zst + + - name: Build appimage package + if: ${{ matrix.job.extra-build-features == 'appimage' }} + shell: bash + run: | + # set-up appimage-builder + pushd /tmp + wget -O appimage-builder-x86_64.AppImage https://github.com/AppImageCrafters/appimage-builder/releases/download/v1.1.0/appimage-builder-1.1.0-x86_64.AppImage + chmod +x appimage-builder-x86_64.AppImage + sudo mv appimage-builder-x86_64.AppImage /usr/local/bin/appimage-builder + popd + # run appimage-builder + pushd appimage + sudo appimage-builder --skip-tests --recipe ./AppImageBuilder-x86_64.yml + + - name: Publish appimage package + if: ${{ matrix.job.extra-build-features == 'appimage' }} + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + ./appimage/rustdesk-${{ env.VERSION }}-*.AppImage + + - name: Publish fedora28/centos8 package + if: ${{ matrix.job.extra-build-features == '' }} + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + /opt/artifacts/rpm/*.rpm + + # Temporary disable flatpak arm build + # + # build-flatpak-arm: + # name: Build Flatpak + # needs: [build-rustdesk-linux-arm] + # runs-on: ${{ matrix.job.os }} + # strategy: + # fail-fast: false + # matrix: + # job: + # # - { target: aarch64-unknown-linux-gnu , os: ubuntu-18.04, arch: arm64 } + # - { target: aarch64-unknown-linux-gnu, os: ubuntu-20.04, arch: arm64 } + # steps: + # - name: Checkout source code + # uses: actions/checkout@v3 + + # - name: Download Binary + # uses: actions/download-artifact@master + # with: + # name: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb + # path: . + + # - name: Rename Binary + # run: | + # mv rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb rustdesk-${{ env.VERSION }}.deb + + # - uses: Kingtous/run-on-arch-action@amd64-support + # name: Build rustdesk flatpak package for ${{ matrix.job.arch }} + # id: rpm + # with: + # arch: ${{ matrix.job.arch }} + # distro: ubuntu18.04 + # githubToken: ${{ github.token }} + # setup: | + # ls -l "${PWD}" + # dockerRunArgs: | + # --volume "${PWD}:/workspace" + # shell: /bin/bash + # install: | + # apt update -y + # apt install -y rpm + # run: | + # pushd /workspace + # # install + # apt update -y + # apt install -y flatpak flatpak-builder cmake g++ gcc git curl wget nasm yasm libgtk-3-dev git + # # flatpak deps + # flatpak --user remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo + # flatpak --user install -y flathub org.freedesktop.Platform/${{ matrix.job.arch }}/21.08 + # flatpak --user install -y flathub org.freedesktop.Sdk/${{ matrix.job.arch }}/21.08 + # # package + # pushd flatpak + # git clone https://github.com/flathub/shared-modules.git --depth=1 + # flatpak-builder --user --force-clean --repo=repo ./build ./rustdesk.json + # flatpak build-bundle ./repo rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}.flatpak org.rustdesk.rustdesk + + # - name: Publish flatpak package + # uses: softprops/action-gh-release@v1 + # with: + # prerelease: true + # tag_name: ${{ env.TAG_NAME }} + # files: | + # flatpak/rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}.flatpak + + build-flatpak-amd64: + name: Build Flatpak + needs: [build-rustdesk-linux-amd64] + runs-on: ubuntu-20.04 + strategy: + fail-fast: false + matrix: + job: + - { target: x86_64-unknown-linux-gnu, os: ubuntu-18.04, arch: x86_64 } + steps: + - name: Checkout source code + uses: actions/checkout@v3 + + - name: Download Binary + uses: actions/download-artifact@master + with: + name: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb + path: . + + - name: Rename Binary + run: | + mv rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb rustdesk-${{ env.VERSION }}.deb + + - uses: Kingtous/run-on-arch-action@amd64-support + name: Build rustdesk flatpak package for ${{ matrix.job.arch }} + id: rpm + with: + arch: ${{ matrix.job.arch }} + distro: ubuntu18.04 + githubToken: ${{ github.token }} + setup: | + ls -l "${PWD}" + dockerRunArgs: | + --volume "${PWD}:/workspace" + shell: /bin/bash + install: | + apt update -y + apt install -y rpm git wget curl + run: | + # disable git safe.directory + git config --global --add safe.directory "*" + pushd /workspace + # install + apt update -y + apt install -y flatpak flatpak-builder cmake g++ gcc git curl wget nasm yasm libgtk-3-dev git + # flatpak deps + flatpak --user remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo + flatpak --user install -y flathub org.freedesktop.Platform/${{ matrix.job.arch }}/21.08 + flatpak --user install -y flathub org.freedesktop.Sdk/${{ matrix.job.arch }}/21.08 + # package + pushd flatpak + git clone https://github.com/flathub/shared-modules.git --depth=1 + flatpak-builder --user --force-clean --repo=repo ./build ./rustdesk.json + flatpak build-bundle ./repo rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}.flatpak org.rustdesk.rustdesk + + - name: Publish flatpak package + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + flatpak/rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}.flatpak From ad44cf0568aebafc5d626afa0d462f694e3a77bf Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Wed, 11 Jan 2023 23:59:54 -0700 Subject: [PATCH 1489/2015] Update flutter-nightly.yml --- .github/workflows/flutter-nightly.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index f420cce62..2c1f65cff 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -15,10 +15,12 @@ env: # for multiarch gcc compatibility VCPKG_COMMIT_ID: "14e7bb4ae24616ec54ff6b2f6ef4e8659434ea44" VERSION: "1.2.0" + #To make a custom build with your own servers set the below secret values RS_PUB_KEY_VAL: '${{ secrets.RS_PUB_KEY_VAL }}' RENDEZVOUS_SERVER1: '${{ secrets.RENDEZVOUS_SERVER1 }}' RENDEZVOUS_SERVER2: '${{ secrets.RENDEZVOUS_SERVER2 }}' RENDEZVOUS_SERVER3: '${{ secrets.RENDEZVOUS_SERVER3 }}' + #ignore signing with key files if values below are set NO_OSX_KEYS: ${{ secrets.NO_OSX_KEYS == 'true' }} NO_APP_KEYS: ${{ secrets.NO_APP_KEYS == 'true' }} From fee27c5d184c914d1f5ed5892b14c1f84c8206b7 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Thu, 12 Jan 2023 00:09:41 -0700 Subject: [PATCH 1490/2015] set custom-build --- .github/workflows/flutter-custom-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flutter-custom-build.yml b/.github/workflows/flutter-custom-build.yml index f445ef073..8d9518303 100644 --- a/.github/workflows/flutter-custom-build.yml +++ b/.github/workflows/flutter-custom-build.yml @@ -7,7 +7,7 @@ env: LLVM_VERSION: "10.0" # Note: currently 3.0.5 does not support arm64 officially, we use latest stable version first. FLUTTER_VERSION: "3.0.5" - TAG_NAME: "nightly" + TAG_NAME: "custom-build" # vcpkg version: 2022.05.10 # for multiarch gcc compatibility VCPKG_COMMIT_ID: "14e7bb4ae24616ec54ff6b2f6ef4e8659434ea44" From 0af3dc2ebc8af4bef3af73e8063650f86a3fa66d Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Thu, 12 Jan 2023 00:15:06 -0700 Subject: [PATCH 1491/2015] upload apk if unsigned --- .github/workflows/flutter-custom-build.yml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/flutter-custom-build.yml b/.github/workflows/flutter-custom-build.yml index 8d9518303..7354b6c77 100644 --- a/.github/workflows/flutter-custom-build.yml +++ b/.github/workflows/flutter-custom-build.yml @@ -574,12 +574,14 @@ jobs: BUILD_TOOLS_VERSION: "30.0.2" - name: Upload Artifacts + if: ${{ env.NO_APP_KEYS== 'true' }} uses: actions/upload-artifact@master with: name: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release-signed.apk path: ${{steps.sign-rustdesk.outputs.signedReleaseFile}} - - name: Publish apk package + - name: Publish signed apk package + if: ${{ env.NO_APP_KEYS== 'true' }} uses: softprops/action-gh-release@v1 with: prerelease: true @@ -587,6 +589,15 @@ jobs: files: | ${{steps.sign-rustdesk.outputs.signedReleaseFile}} + - name: Publish unsigned apk package + if: ${{ env.NO_APP_KEYS!= 'true' }} + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + ../rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk + build-rustdesk-lib-linux-amd64: needs: [generate-bridge-linux, build-vcpkg-deps-linux] name: build-rust-lib ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] From 01b41f01f65cc33f1440298b75a3e3866fedf10b Mon Sep 17 00:00:00 2001 From: ston Date: Fri, 13 Jan 2023 02:22:57 +0800 Subject: [PATCH 1492/2015] cn.rs: update wayland_experiment_tip Signed-off-by: ston --- src/lang/cn.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/cn.rs b/src/lang/cn.rs index a486128b7..4c460a3bd 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -401,7 +401,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request access to your device", "请求访问你的设备"), ("Hide connection management window", "隐藏连接管理窗口"), ("hide_cm_tip", "在只允许密码连接并且只用固定密码的情况下才允许隐藏"), - ("wayland_experiment_tip", ""), + ("wayland_experiment_tip", "Wayland支持处于实验阶段,如果你需要使用无人值守访问,请使用X11。"), ("Right click to select tabs", "右键选择选项卡"), ("Skipped", "已跳过"), ("Add to Address Book", "添加到地址簿"), From 7861fab9b8bb1aa61f1bd7da1fffc3f430934705 Mon Sep 17 00:00:00 2001 From: Jimmy GALLAND Date: Thu, 12 Jan 2023 21:09:57 +0100 Subject: [PATCH 1493/2015] update-fr --- src/lang/fr.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 499be7c54..7c4d55ea3 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -210,11 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Forcer la connexion relais"), ("whitelist_tip", "Seul l'IP dans la liste blanche peut accéder à mon appareil"), ("Login", "Connexion"), - ("Verify", ""), - ("Remember me", ""), - ("Trust this device", ""), - ("Verification code", ""), - ("verification_tip", ""), + ("Verify", "Vérifier"), + ("Remember me", "Se souvenir de moi"), + ("Trust this device", "Faire confiance à cet appareil"), + ("Verification code", "Code de vérification"), + ("verification_tip", "Un nouvel appareil a été détecté et un code de vérification a été envoyé à l'adresse e-mail enregistrée, entrez le code de vérification pour continuer la connexion."), ("Logout", "Déconnexion"), ("Tags", "Étiqueter"), ("Search ID", "Rechercher un ID"), @@ -410,8 +410,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the web console", "Fermé manuellement par la console Web"), ("Local keyboard type", "Disposition du clavier local"), ("Select local keyboard type", "Selectionner la disposition du clavier local"), - ("software_render_tip", ""), - ("Always use software rendering", ""), - ("config_input", ""), + ("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\"."), ].iter().cloned().collect(); } From b4f7fcabadfad3da8c76a8cb354709125190b757 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 13 Jan 2023 00:06:38 -0800 Subject: [PATCH 1494/2015] opt: make duplicated action panel offstage on macos --- flutter/lib/desktop/widgets/tabbar_widget.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index d428bcb9b..8e2238f11 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -374,7 +374,7 @@ class DesktopTab extends StatelessWidget { width: 78, )), Offstage( - offstage: kUseCompatibleUiMode, + offstage: kUseCompatibleUiMode || Platform.isMacOS, child: Row(children: [ Offstage( offstage: !showLogo, @@ -555,7 +555,7 @@ class WindowActionPanelState extends State child: Row( children: [ Offstage( - offstage: !widget.showMinimize, + offstage: !widget.showMinimize || Platform.isMacOS, child: ActionIcon( message: 'Minimize', icon: IconFont.min, @@ -569,7 +569,7 @@ class WindowActionPanelState extends State isClose: false, )), Offstage( - offstage: !widget.showMaximize, + offstage: !widget.showMaximize || Platform.isMacOS, child: Obx(() => ActionIcon( message: widget.isMaximized.value ? "Restore" : "Maximize", @@ -580,7 +580,7 @@ class WindowActionPanelState extends State isClose: false, ))), Offstage( - offstage: !widget.showClose, + offstage: !widget.showClose || Platform.isMacOS, child: ActionIcon( message: 'Close', icon: IconFont.close, From a454aa55cbb66c39ac49787d55e5697db9fd2e52 Mon Sep 17 00:00:00 2001 From: "Miguel F. G" <116861809+flusheDData@users.noreply.github.com> Date: Fri, 13 Jan 2023 15:14:17 +0100 Subject: [PATCH 1495/2015] Update es.rs --- src/lang/es.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/es.rs b/src/lang/es.rs index e0e410711..a956ebca5 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -403,7 +403,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", "Permitir ocultar solo si se aceptan sesiones a través de contraseña y usando contraseña permanente"), ("wayland_experiment_tip", "El soporte para Wayland está en fase experimental, por favor, use X11 si necesita acceso desatendido."), ("Right click to select tabs", "Clic derecho para seleccionar pestañas"), - ("Skipped", ""), + ("Skipped", "Omitido"), ("Add to Address Book", "Añadir a la libreta de direcciones"), ("Group", "Grupo"), ("Search", "Búsqueda"), From d22e8f4ab8a91753b5424529a8a83994ebe96c8c Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Fri, 13 Jan 2023 21:09:17 -0700 Subject: [PATCH 1496/2015] update apk unsigned --- .github/workflows/flutter-nightly.yml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 2c1f65cff..44804c6ae 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -577,12 +577,14 @@ jobs: BUILD_TOOLS_VERSION: "30.0.2" - name: Upload Artifacts + if: ${{ env.NO_APP_KEYS== 'true' }} uses: actions/upload-artifact@master with: name: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release-signed.apk path: ${{steps.sign-rustdesk.outputs.signedReleaseFile}} - - name: Publish apk package + - name: Publish signed apk package + if: ${{ env.NO_APP_KEYS== 'true' }} uses: softprops/action-gh-release@v1 with: prerelease: true @@ -590,6 +592,15 @@ jobs: files: | ${{steps.sign-rustdesk.outputs.signedReleaseFile}} + - name: Publish unsigned apk package + if: ${{ env.NO_APP_KEYS!= 'true' }} + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + ../rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk + build-rustdesk-lib-linux-amd64: needs: [generate-bridge-linux, build-vcpkg-deps-linux] name: build-rust-lib ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] From 95c662f3bcdffc892baaf56ae470a785710eec5f Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sat, 14 Jan 2023 14:43:33 +0800 Subject: [PATCH 1497/2015] fix issue #2819 --- .github/workflows/flutter-nightly.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 967c85385..845ba339b 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -245,8 +245,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 - 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 + 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 # notarize the rustdesk-${{ env.VERSION }}.dmg rcodesign notary-submit --api-key-path ${{ github.workspace }}/rustdesk.json --staple rustdesk-${{ env.VERSION }}.dmg From d3b490ac4834ebe01decefbe7b2c3be9a7e864c8 Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 12 Jan 2023 21:03:05 +0800 Subject: [PATCH 1498/2015] elevation request Signed-off-by: 21pages --- flutter/lib/common.dart | 6 +- flutter/lib/mobile/widgets/dialog.dart | 242 +++++++++++++++++++++++++ flutter/lib/models/model.dart | 6 + libs/hbb_common/protos/message.proto | 15 ++ src/client.rs | 2 + src/client/io_loop.rs | 123 ++++++++++--- src/core_main.rs | 3 +- src/flutter_ffi.rs | 12 ++ src/lang/ca.rs | 10 + src/lang/cn.rs | 10 + src/lang/cs.rs | 10 + src/lang/da.rs | 10 + src/lang/de.rs | 10 + src/lang/en.rs | 3 + src/lang/eo.rs | 10 + src/lang/es.rs | 10 + src/lang/fa.rs | 10 + src/lang/fr.rs | 10 + src/lang/gr.rs | 10 + src/lang/hu.rs | 10 + src/lang/id.rs | 10 + src/lang/it.rs | 10 + src/lang/ja.rs | 10 + src/lang/ko.rs | 10 + src/lang/kz.rs | 10 + src/lang/pl.rs | 10 + src/lang/pt_PT.rs | 10 + src/lang/ptbr.rs | 10 + src/lang/ru.rs | 10 + src/lang/sk.rs | 10 + src/lang/sl.rs | 10 + src/lang/sq.rs | 10 + src/lang/sr.rs | 10 + src/lang/sv.rs | 10 + src/lang/template.rs | 10 + src/lang/th.rs | 10 + src/lang/tr.rs | 10 + src/lang/tw.rs | 10 + src/lang/ua.rs | 10 + src/lang/vn.rs | 10 + src/platform/windows.rs | 41 ++++- src/server/connection.rs | 66 ++++++- src/server/portable_service.rs | 100 ++++++++-- src/server/video_service.rs | 16 +- src/ui_cm_interface.rs | 6 +- src/ui_session_interface.rs | 8 + 46 files changed, 900 insertions(+), 59 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 9faa06d36..c3a8baba9 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -549,6 +549,10 @@ class OverlayDialogManager { hideMobileActionsOverlay(); } } + + bool existing(String tag) { + return _dialogs.keys.contains(tag); + } } void showToast(String text, {Duration timeout = const Duration(seconds: 2)}) { @@ -983,7 +987,7 @@ Widget getPlatformImage(String platform, {double size = 50}) { platform != kPeerPlatformAndroid) { platform = 'win'; } else { - platform = platform.toLowerCase(); + platform = platform.toLowerCase(); } return SvgPicture.asset('assets/$platform.svg', height: size, width: size); } diff --git a/flutter/lib/mobile/widgets/dialog.dart b/flutter/lib/mobile/widgets/dialog.dart index 2df80d9fd..3b5af1d81 100644 --- a/flutter/lib/mobile/widgets/dialog.dart +++ b/flutter/lib/mobile/widgets/dialog.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:convert'; import 'package:flutter/material.dart'; +import 'package:get/get.dart'; import '../../common.dart'; import '../../models/model.dart'; @@ -367,6 +368,247 @@ void showServerSettingsWithValue( }); } +void showWaitUacDialog(String id, OverlayDialogManager dialogManager) { + dialogManager.dismissAll(); + dialogManager.show( + tag: '$id-wait-uac', + (setState, close) => CustomAlertDialog( + title: Text(translate('Wait')), + content: Text(translate('wait_accept_uac_tip')).marginAll(10), + )); +} + +void _showRequestElevationDialog( + String id, OverlayDialogManager dialogManager) { + RxString groupValue = ''.obs; + RxString errUser = ''.obs; + RxString errPwd = ''.obs; + TextEditingController userController = TextEditingController(); + TextEditingController pwdController = TextEditingController(); + + void onRadioChanged(String? value) { + if (value != null) { + groupValue.value = value; + } + } + + const minTextStyle = TextStyle(fontSize: 14); + + var content = Obx(() => Column(children: [ + Row( + children: [ + Radio( + value: '', + groupValue: groupValue.value, + onChanged: onRadioChanged), + Expanded( + child: + Text(translate('Ask the remote user for authentication'))), + ], + ), + Align( + alignment: Alignment.centerLeft, + child: Text( + translate( + 'Choose this if the remote account is administrator'), + style: TextStyle(fontSize: 13)) + .marginOnly(left: 40), + ).marginOnly(bottom: 15), + Row( + children: [ + Radio( + value: 'logon', + groupValue: groupValue.value, + onChanged: onRadioChanged), + Expanded( + child: Text(translate( + 'Transmit the username and password of administrator')), + ) + ], + ), + Row( + children: [ + Expanded( + flex: 1, + child: Text( + '${translate('Username')}:', + style: minTextStyle, + ).marginOnly(right: 10)), + Expanded( + flex: 3, + child: TextField( + controller: userController, + style: minTextStyle, + decoration: InputDecoration( + isDense: true, + contentPadding: EdgeInsets.symmetric(vertical: 15), + hintText: 'eg: admin', + errorText: errUser.isEmpty ? null : errUser.value), + onChanged: (s) { + if (s.isNotEmpty) { + errUser.value = ''; + } + }, + ), + ) + ], + ).marginOnly(left: 40), + Row( + children: [ + Expanded( + flex: 1, + child: Text( + '${translate('Password')}:', + style: minTextStyle, + ).marginOnly(right: 10)), + Expanded( + flex: 3, + child: TextField( + controller: pwdController, + obscureText: true, + style: minTextStyle, + decoration: InputDecoration( + isDense: true, + contentPadding: EdgeInsets.symmetric(vertical: 15), + errorText: errPwd.isEmpty ? null : errPwd.value), + onChanged: (s) { + if (s.isNotEmpty) { + errPwd.value = ''; + } + }, + ), + ), + ], + ).marginOnly(left: 40), + Align( + alignment: Alignment.centerLeft, + child: Text(translate('still_click_uac_tip'), + style: TextStyle(fontSize: 13, fontWeight: FontWeight.bold)) + .marginOnly(top: 20)), + ])); + + dialogManager.dismissAll(); + dialogManager.show(tag: '$id-request-elevation', (setState, close) { + void submit() { + if (groupValue.value == 'logon') { + if (userController.text.isEmpty) { + errUser.value = translate('Empty Username'); + return; + } + if (pwdController.text.isEmpty) { + errPwd.value = translate('Empty Password'); + return; + } + bind.sessionElevateWithLogon( + id: id, + username: userController.text, + password: pwdController.text); + } else { + bind.sessionElevateDirect(id: id); + } + } + + return CustomAlertDialog( + title: Text(translate('Request Elevation')), + content: content, + actions: [ + ElevatedButton( + style: ElevatedButton.styleFrom(elevation: 0), + onPressed: submit, + child: Text(translate('OK')), + ), + OutlinedButton( + onPressed: () { + close(); + }, + child: Text(translate('Cancel')), + ), + ], + onSubmit: submit, + onCancel: close, + ); + }); +} + +void showOnBlockDialog( + String id, + String type, + String title, + String text, + OverlayDialogManager dialogManager, +) { + if (dialogManager.existing('$id-wait-uac') || + dialogManager.existing('$id-request-elevation')) { + return; + } + var content = Column(children: [ + Align( + alignment: Alignment.centerLeft, + child: Text( + "${translate(text)}${type.contains('uac') ? '\n' : '\n\n'}${translate('request_elevation_tip')}", + textAlign: TextAlign.left, + style: TextStyle(fontWeight: FontWeight.w400), + ).marginSymmetric(vertical: 15), + ), + ]); + dialogManager.show(tag: '$id-$type', (setState, close) { + void submit() { + close(); + _showRequestElevationDialog(id, dialogManager); + } + + return CustomAlertDialog( + title: Text(translate(title)), + content: content, + actions: [ + ElevatedButton( + style: ElevatedButton.styleFrom(elevation: 0), + onPressed: submit, + child: Text(translate('Request Elevation')), + ), + OutlinedButton( + onPressed: () { + close(); + }, + child: Text(translate('Wait')), + ), + ], + onSubmit: submit, + onCancel: close, + ); + }); +} + +void showElevationError(String id, String type, String title, String text, + OverlayDialogManager dialogManager) { + dialogManager.show(tag: '$id-$type', (setState, close) { + void submit() { + close(); + _showRequestElevationDialog(id, dialogManager); + } + + return CustomAlertDialog( + title: Text(translate(title)), + content: Text(translate(text)), + actions: [ + ElevatedButton( + style: ElevatedButton.styleFrom(elevation: 0), + onPressed: submit, + child: Text(translate('Retry')), + ), + OutlinedButton( + onPressed: () { + close(); + }, + child: Text(translate('Cancel')), + ), + ], + onSubmit: submit, + onCancel: close, + ); + }); +} + Future validateAsync(String value) async { value = value.trim(); if (value.isEmpty) { diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 3a383d9a1..83678ccb7 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -272,6 +272,12 @@ class FfiModel with ChangeNotifier { } else if (type == 'wait-remote-accept-nook') { msgBoxCommon(dialogManager, title, Text(translate(text)), [msgBoxButton("Cancel", closeConnection)]); + } else if (type == 'on-uac' || type == 'on-foreground-elevated') { + showOnBlockDialog(id, type, title, text, dialogManager); + } else if (type == 'wait-uac') { + showWaitUacDialog(id, dialogManager); + } else if (type == 'elevation-error') { + showElevationError(id, type, title, text, dialogManager); } else { var hasRetry = evt['hasRetry'] == 'true'; showMsgBox(id, type, title, text, link, hasRetry, dialogManager); diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index e39bc7c6a..f5910d963 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -552,6 +552,18 @@ message BackNotification { } } +message ElevationRequestWithLogon { + string username = 1; + string password = 2; +} + +message ElevationRequest { + oneof union { + bool direct = 1; + ElevationRequestWithLogon logon = 2; + } +} + message Misc { oneof union { ChatMessage chat_message = 4; @@ -567,6 +579,9 @@ message Misc { bool uac = 15; bool foreground_window_elevated = 16; bool stop_service = 17; + ElevationRequest elevation_request = 18; + string elevation_response = 19; + bool portable_service_running = 20; } } diff --git a/src/client.rs b/src/client.rs index 43ee5bf07..8b2edbcda 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1890,6 +1890,8 @@ pub enum Data { AddJob((i32, String, String, i32, bool, bool)), ResumeJob((i32, bool)), RecordScreen(bool, i32, i32, String), + ElevateDirect, + ElevateWithLogon(String, String), } /// Keycode for key events. diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 71d353c88..b15949041 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -632,6 +632,28 @@ impl Remote { .video_sender .send(MediaData::RecordScreen(start, w, h, id)); } + Data::ElevateDirect => { + let mut request = ElevationRequest::new(); + request.set_direct(true); + let mut misc = Misc::new(); + misc.set_elevation_request(request); + let mut msg = Message::new(); + msg.set_misc(misc); + allow_err!(peer.send(&msg).await); + } + Data::ElevateWithLogon(username, password) => { + let mut request = ElevationRequest::new(); + request.set_logon(ElevationRequestWithLogon { + username, + password, + ..Default::default() + }); + let mut misc = Misc::new(); + misc.set_elevation_request(request); + let mut msg = Message::new(); + msg.set_misc(misc); + allow_err!(peer.send(&msg).await); + } _ => {} } true @@ -989,8 +1011,13 @@ impl Remote { self.handler.ui_handler.switch_display(&s); self.video_sender.send(MediaData::Reset).ok(); if s.width > 0 && s.height > 0 { - self.handler - .set_display(s.x, s.y, s.width, s.height, s.cursor_embedded); + self.handler.set_display( + s.x, + s.y, + s.width, + s.height, + s.cursor_embedded, + ); } } Some(misc::Union::CloseReason(c)) => { @@ -1003,31 +1030,85 @@ impl Remote { } } Some(misc::Union::Uac(uac)) => { - let msgtype = "custom-uac-nocancel"; - let title = "Prompt"; - let text = "Please wait for confirmation of UAC..."; - let link = ""; - if uac { - self.handler.msgbox(msgtype, title, text, link); - } else { - self.handler - .cancel_msgbox( - &format!("{}-{}-{}-{}", msgtype, title, text, link,), + #[cfg(feature = "flutter")] + { + if uac { + self.handler.msgbox( + "on-uac", + "Prompt", + "Please wait for confirmation of UAC...", + "", ); + } else { + self.handler.cancel_msgbox("on-uac"); + self.handler.cancel_msgbox("wait-uac"); + self.handler.cancel_msgbox("elevation-error"); + } + } + #[cfg(not(feature = "flutter"))] + { + let msgtype = "custom-uac-nocancel"; + let title = "Prompt"; + let text = "Please wait for confirmation of UAC..."; + let link = ""; + if uac { + self.handler.msgbox(msgtype, title, text, link); + } else { + self.handler.cancel_msgbox(&format!( + "{}-{}-{}-{}", + msgtype, title, text, link, + )); + } } } Some(misc::Union::ForegroundWindowElevated(elevated)) => { - let msgtype = "custom-elevated-foreground-nocancel"; - let title = "Prompt"; - let text = "elevated_foreground_window_tip"; - let link = ""; - if elevated { - self.handler.msgbox(msgtype, title, text, link); + #[cfg(feature = "flutter")] + { + if elevated { + self.handler.msgbox( + "on-foreground-elevated", + "Prompt", + "elevated_foreground_window_tip", + "", + ); + } else { + self.handler.cancel_msgbox("on-foreground-elevated"); + self.handler.cancel_msgbox("wait-uac"); + self.handler.cancel_msgbox("elevation-error"); + } + } + #[cfg(not(feature = "flutter"))] + { + let msgtype = "custom-elevated-foreground-nocancel"; + let title = "Prompt"; + let text = "elevated_foreground_window_tip"; + let link = ""; + if elevated { + self.handler.msgbox(msgtype, title, text, link); + } else { + self.handler.cancel_msgbox(&format!( + "{}-{}-{}-{}", + msgtype, title, text, link, + )); + } + } + } + Some(misc::Union::ElevationResponse(err)) => { + if err.is_empty() { + self.handler.msgbox("wait-uac", "", "", ""); } else { self.handler - .cancel_msgbox( - &format!("{}-{}-{}-{}", msgtype, title, text, link,), - ); + .msgbox("elevation-error", "Elevation Error", &err, ""); + } + } + Some(misc::Union::PortableServiceRunning(b)) => { + if b { + self.handler.msgbox( + "custom-nocancel", + "Successful", + "Elevate successfully", + "", + ); } } _ => {} diff --git a/src/core_main.rs b/src/core_main.rs index 720c01da8..9083efe0e 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -106,7 +106,8 @@ pub fn core_main() -> Option> { && !_is_elevate && !_is_run_as_system { - if let Err(e) = crate::portable_service::client::start_portable_service() { + use crate::portable_service::client; + if let Err(e) = client::start_portable_service(client::StartPara::Direct) { log::error!("Failed to start portable service:{:?}", e); } } diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 594fe7767..a4b6d1395 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -492,6 +492,18 @@ pub fn session_resume_job(id: String, act_id: i32, is_remote: bool) { } } +pub fn session_elevate_direct(id: String) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.elevate_direct(); + } +} + +pub fn session_elevate_with_logon(id: String, username: String, password: String) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.elevate_with_logon(username, password); + } +} + pub fn main_get_sound_inputs() -> Vec { #[cfg(not(any(target_os = "android", target_os = "ios")))] return get_sound_inputs(); diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 9224d231a..d54b588c6 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -413,5 +413,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("request_elevation_tip", ""), + ("Wait",""), + ("Elevation Error", ""), + ("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", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 4c460a3bd..61da5d331 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -413,5 +413,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", "如果你使用英伟达显卡, 并且远程窗口在会话建立后会立刻关闭, 那么安装nouveau驱动并且选择使用软件渲染可能会有帮助。重启软件后生效。"), ("Always use software rendering", "使用软件渲染"), ("config_input", "为了能够通过键盘控制远程桌面, 请给予 RustDesk \"输入监控\" 权限。"), + ("request_elevation_tip", "如果对面有人, 也可以请求提升权限。"), + ("Wait","等待"), + ("Elevation Error", "提权失败"), + ("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窗口點擊確認。"), + ("Request Elevation", "请求提权"), + ("wait_accept_uac_tip", "请等待远端用户确认UAC对话框。"), + ("Elevate successfully", "提权成功"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 3622aef8a..d43a534ee 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -413,5 +413,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("request_elevation_tip", ""), + ("Wait",""), + ("Elevation Error", ""), + ("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", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index f07d9914e..0f7823b72 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -413,5 +413,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("request_elevation_tip", ""), + ("Wait",""), + ("Elevation Error", ""), + ("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", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index a195fcdba..8060deb49 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -413,5 +413,15 @@ 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."), + ("request_elevation_tip", ""), + ("Wait",""), + ("Elevation Error", ""), + ("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", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), ].iter().cloned().collect(); } diff --git a/src/lang/en.rs b/src/lang/en.rs index 14d221ef3..6eed43a77 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -39,5 +39,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("verification_tip", "A new device has been detected, and a verification code has been sent to the registered email address, enter the verification code to continue logging in."), ("software_render_tip", "If you have an Nvidia graphics card and the remote window closes immediately after connecting, installing the nouveau driver and choosing to use software rendering may help. A software restart is required."), ("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.") ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 2a41fdcf9..8503d153a 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -413,5 +413,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("request_elevation_tip", ""), + ("Wait",""), + ("Elevation Error", ""), + ("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", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index e0e410711..b695a6802 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -413,5 +413,15 @@ 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\"."), + ("request_elevation_tip", ""), + ("Wait",""), + ("Elevation Error", ""), + ("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", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 790d01682..6bdd05061 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -413,5 +413,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("request_elevation_tip", ""), + ("Wait",""), + ("Elevation Error", ""), + ("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", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 7c4d55ea3..bbc50e4a6 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -413,5 +413,15 @@ 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\"."), + ("request_elevation_tip", ""), + ("Wait",""), + ("Elevation Error", ""), + ("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", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), ].iter().cloned().collect(); } diff --git a/src/lang/gr.rs b/src/lang/gr.rs index 53369a4b3..7a0e0b35d 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -413,5 +413,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("request_elevation_tip", ""), + ("Wait",""), + ("Elevation Error", ""), + ("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", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 32d920994..342a29bc1 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -413,5 +413,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("request_elevation_tip", ""), + ("Wait",""), + ("Elevation Error", ""), + ("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", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index c33cccb66..671c2a8fc 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -413,5 +413,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("request_elevation_tip", ""), + ("Wait",""), + ("Elevation Error", ""), + ("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", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 05ee237bd..cd132dc45 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -413,5 +413,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", "Usa sempre il render Software"), ("config_input", ""), + ("request_elevation_tip", ""), + ("Wait",""), + ("Elevation Error", ""), + ("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", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 7dd1640f6..4343d5cec 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -413,5 +413,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("request_elevation_tip", ""), + ("Wait",""), + ("Elevation Error", ""), + ("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", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 66ff3ca95..c9874b2d6 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -413,5 +413,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("request_elevation_tip", ""), + ("Wait",""), + ("Elevation Error", ""), + ("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", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index ac688eb9f..049d490a1 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -413,5 +413,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("request_elevation_tip", ""), + ("Wait",""), + ("Elevation Error", ""), + ("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", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index afd6b4b03..5d0d575b9 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -413,5 +413,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("request_elevation_tip", ""), + ("Wait",""), + ("Elevation Error", ""), + ("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", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index bf7954b46..0749678d8 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -413,5 +413,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("request_elevation_tip", ""), + ("Wait",""), + ("Elevation Error", ""), + ("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", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 207be548f..f6c43aab0 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -413,5 +413,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("request_elevation_tip", ""), + ("Wait",""), + ("Elevation Error", ""), + ("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", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 43dd1cb08..77f64e75d 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -413,5 +413,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", "Если у вас видеокарта Nvidia и удалённое окно закрывается сразу после подключения, может помочь установка драйвера Nouveau и выбор использования программной визуализации. Потребуется перезапуск."), ("Always use software rendering", "Использовать программную визуализацию"), ("config_input", "Чтобы управлять удалённым рабочим столом с помощью клавиатуры, необходимо предоставить RustDesk разрешения \"Мониторинг ввода\"."), + ("request_elevation_tip", ""), + ("Wait",""), + ("Elevation Error", ""), + ("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", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 40f19c625..954e3ae92 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -413,5 +413,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("request_elevation_tip", ""), + ("Wait",""), + ("Elevation Error", ""), + ("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", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 5e8efc17d..3b4ee16da 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -413,5 +413,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("request_elevation_tip", ""), + ("Wait",""), + ("Elevation Error", ""), + ("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", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 0725d02e5..804a88798 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -413,5 +413,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("request_elevation_tip", ""), + ("Wait",""), + ("Elevation Error", ""), + ("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", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 3b7201bb8..9ecdd1843 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -413,5 +413,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("request_elevation_tip", ""), + ("Wait",""), + ("Elevation Error", ""), + ("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", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index eeeec80cc..ccf1eed8a 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -413,5 +413,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("request_elevation_tip", ""), + ("Wait",""), + ("Elevation Error", ""), + ("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", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index d3be7ba17..f8a7a3bc7 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -413,5 +413,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("request_elevation_tip", ""), + ("Wait",""), + ("Elevation Error", ""), + ("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", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index a4d0a033d..5bd969bdc 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -413,5 +413,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("request_elevation_tip", ""), + ("Wait",""), + ("Elevation Error", ""), + ("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", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 2d0fc8c59..ee661d9b6 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -413,5 +413,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("request_elevation_tip", ""), + ("Wait",""), + ("Elevation Error", ""), + ("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", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index a58665a70..aebf8917e 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -413,5 +413,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", "如果你使用英偉達顯卡, 並且遠程窗口在會話建立後會立刻關閉, 那麼安裝nouveau驅動並且選擇使用軟件渲染可能會有幫助。重啟軟件後生效。"), ("Always use software rendering", "使用軟件渲染"), ("config_input", ""), + ("request_elevation_tip", "如果對面有人, 也可以請求提升權限。"), + ("Wait","等待"), + ("Elevation Error", "提權失敗"), + ("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", "依然需要被控端用戶在UAC窗口點擊確認。"), + ("Request Elevation", "請求提權"), + ("wait_accept_uac_tip", "請等待遠端用戶確認UAC對話框。"), + ("Elevate successfully", "提權成功"), ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index fad7a3880..784592ff0 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -413,5 +413,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("request_elevation_tip", ""), + ("Wait",""), + ("Elevation Error", ""), + ("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", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 187572c83..ac62631c9 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -413,5 +413,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("request_elevation_tip", ""), + ("Wait",""), + ("Elevation Error", ""), + ("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", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), ].iter().cloned().collect(); } diff --git a/src/platform/windows.rs b/src/platform/windows.rs index a2a99800f..89861a418 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -24,7 +24,7 @@ use winapi::{ minwinbase::STILL_ACTIVE, processthreadsapi::{ GetCurrentProcess, GetCurrentProcessId, GetExitCodeProcess, OpenProcess, - OpenProcessToken, + OpenProcessToken, PROCESS_INFORMATION, STARTUPINFOW, }, securitybaseapi::GetTokenInformation, shellapi::ShellExecuteW, @@ -1714,3 +1714,42 @@ pub fn send_message_to_hnwd( } return true; } + +pub fn create_process_with_logon(user: &str, pwd: &str, exe: &str, arg: &str) -> ResultType<()> { + unsafe { + let wuser = wide_string(user); + let wpc = wide_string(""); + let wpwd = wide_string(pwd); + let cmd = if arg.is_empty() { + format!("\"{}\"", exe) + } else { + format!("\"{}\" {}", exe, arg) + }; + let mut wcmd = wide_string(&cmd); + let mut si: STARTUPINFOW = mem::zeroed(); + si.wShowWindow = SW_HIDE as _; + si.lpDesktop = NULL as _; + si.cb = std::mem::size_of::() as _; + si.dwFlags = STARTF_USESHOWWINDOW; + let mut pi: PROCESS_INFORMATION = mem::zeroed(); + let wexe = wide_string(exe); + if FALSE + == CreateProcessWithLogonW( + wuser.as_ptr(), + wpc.as_ptr(), + wpwd.as_ptr(), + LOGON_WITH_PROFILE, + wexe.as_ptr(), + wcmd.as_mut_ptr(), + CREATE_UNICODE_ENVIRONMENT, + NULL, + NULL as _, + &mut si as *mut STARTUPINFOW, + &mut pi as *mut PROCESS_INFORMATION, + ) + { + bail!("CreateProcessWithLogonW failed, errno={}", GetLastError()); + } + } + return Ok(()); +} diff --git a/src/server/connection.rs b/src/server/connection.rs index 93e90395e..a7526c8b4 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -101,6 +101,7 @@ pub struct Connection { last_recv_time: Arc>, chat_unanswered: bool, close_manually: bool, + elevation_requested: bool, } impl Subscriber for ConnInner { @@ -196,6 +197,7 @@ impl Connection { last_recv_time: Arc::new(Mutex::new(Instant::now())), chat_unanswered: false, close_manually: false, + elevation_requested: false, }; #[cfg(not(any(target_os = "android", target_os = "ios")))] tokio::spawn(async move { @@ -247,6 +249,8 @@ impl Connection { #[cfg(windows)] let mut last_foreground_window_elevated = false; #[cfg(windows)] + let mut last_portable_service_running = false; + #[cfg(windows)] let is_installed = crate::platform::is_installed(); loop { @@ -353,7 +357,8 @@ impl Connection { } #[cfg(windows)] ipc::Data::DataPortableService(ipc::DataPortableService::RequestStart) => { - if let Err(e) = crate::portable_service::client::start_portable_service() { + use crate::portable_service::client; + if let Err(e) = client::start_portable_service(client::StartPara::Direct) { log::error!("Failed to start portable service from cm:{:?}", e); } } @@ -440,8 +445,18 @@ impl Connection { _ = second_timer.tick() => { #[cfg(windows)] { - if !is_installed { - let portable_service_running = crate::portable_service::client::PORTABLE_SERVICE_RUNNING.lock().unwrap().clone(); + if !is_installed && conn.file_transfer.is_none() && conn.port_forward_socket.is_none(){ + let portable_service_running = crate::portable_service::client::running(); + if portable_service_running != last_portable_service_running { + last_portable_service_running = portable_service_running; + if portable_service_running && conn.elevation_requested { + let mut misc = Misc::new(); + misc.set_portable_service_running(portable_service_running); + let mut msg = Message::new(); + msg.set_misc(misc); + conn.inner.send(msg.into()); + } + } let uac = crate::video_service::IS_UAC_RUNNING.lock().unwrap().clone(); if last_uac != uac { last_uac = uac; @@ -1476,6 +1491,51 @@ impl Connection { } } } + Some(misc::Union::ElevationRequest(r)) => match r.union { + Some(elevation_request::Union::Direct(_)) => { + #[cfg(windows)] + { + let mut err = "No need to elevate".to_string(); + if !crate::platform::is_installed() + && !crate::portable_service::client::running() + { + use crate::portable_service::client; + err = client::start_portable_service(client::StartPara::Direct) + .err() + .map_or("".to_string(), |e| e.to_string()); + } + self.elevation_requested = err.is_empty(); + let mut misc = Misc::new(); + misc.set_elevation_response(err); + let mut msg = Message::new(); + msg.set_misc(misc); + self.send(msg).await; + } + } + Some(elevation_request::Union::Logon(r)) => { + #[cfg(windows)] + { + let mut err = "No need to elevate".to_string(); + if !crate::platform::is_installed() + && !crate::portable_service::client::running() + { + use crate::portable_service::client; + err = client::start_portable_service(client::StartPara::Logon( + r.username, r.password, + )) + .err() + .map_or("".to_string(), |e| e.to_string()); + } + self.elevation_requested = err.is_empty(); + let mut misc = Misc::new(); + misc.set_elevation_response(err); + let mut msg = Message::new(); + msg.set_misc(misc); + self.send(msg).await; + } + } + _ => {} + }, _ => {} }, _ => {} diff --git a/src/server/portable_service.rs b/src/server/portable_service.rs index 6b87da21b..0651fd4ce 100644 --- a/src/server/portable_service.rs +++ b/src/server/portable_service.rs @@ -451,18 +451,24 @@ pub mod server { // functions called in main process. pub mod client { use hbb_common::anyhow::Context; + use std::path::PathBuf; use super::*; lazy_static::lazy_static! { - pub static ref PORTABLE_SERVICE_RUNNING: Arc> = Default::default(); + static ref RUNNING: Arc> = Default::default(); static ref SHMEM: Arc>> = Default::default(); static ref SENDER : Mutex> = Mutex::new(client::start_ipc_server()); } - pub(crate) fn start_portable_service() -> ResultType<()> { + pub enum StartPara { + Direct, + Logon(String, String), + } + + pub(crate) fn start_portable_service(para: StartPara) -> ResultType<()> { log::info!("start portable service"); - if PORTABLE_SERVICE_RUNNING.lock().unwrap().clone() { + if RUNNING.lock().unwrap().clone() { bail!("already running"); } if SHMEM.lock().unwrap().is_none() { @@ -491,14 +497,60 @@ pub mod client { unsafe { libc::memset(shmem.as_ptr() as _, 0, shmem.len() as _); } - if crate::platform::run_background( - &std::env::current_exe()?.to_string_lossy().to_string(), - "--portable-service", - ) - .is_err() - { - *SHMEM.lock().unwrap() = None; - bail!("Failed to run portable service process"); + drop(option); + match para { + StartPara::Direct => { + if let Err(e) = crate::platform::run_background( + &std::env::current_exe()?.to_string_lossy().to_string(), + "--portable-service", + ) { + *SHMEM.lock().unwrap() = None; + bail!("Failed to run portable service process:{}", e); + } + } + StartPara::Logon(username, password) => { + #[allow(unused_mut)] + let mut exe = std::env::current_exe()?.to_string_lossy().to_string(); + #[cfg(feature = "flutter")] + { + if let Some(dir) = PathBuf::from(&exe).parent() { + if !set_dir_permission(&PathBuf::from(dir)) { + *SHMEM.lock().unwrap() = None; + bail!("Failed to set permission of {:?}", dir); + } + } + } + #[cfg(not(feature = "flutter"))] + match hbb_common::directories_next::UserDirs::new() { + Some(user_dir) => { + let dir = user_dir + .home_dir() + .join("AppData") + .join("Local") + .join("rustdesk-sciter"); + if std::fs::create_dir_all(&dir).is_ok() { + let dst = dir.join("rustdesk.exe"); + if std::fs::copy(&exe, &dst).is_ok() { + if dst.exists() { + if set_dir_permission(&dir) { + exe = dst.to_string_lossy().to_string(); + } + } + } + } + } + None => {} + } + if let Err(e) = crate::platform::windows::create_process_with_logon( + username.as_str(), + password.as_str(), + &exe, + "--portable-service", + ) { + *SHMEM.lock().unwrap() = None; + bail!("Failed to run portable service process:{}", e); + } + } } let _sender = SENDER.lock().unwrap(); Ok(()) @@ -509,6 +561,16 @@ pub mod client { *SHMEM.lock().unwrap() = None; } + fn set_dir_permission(dir: &PathBuf) -> bool { + // // give Everyone RX permission + std::process::Command::new("icacls") + .arg(dir.as_os_str()) + .arg("/grant") + .arg("Everyone:(OI)(CI)RX") + .arg("/T") + .spawn() + .is_ok() + } pub struct CapturerPortable; impl CapturerPortable { @@ -668,7 +730,7 @@ pub mod client { } Pong => { nack = 0; - *PORTABLE_SERVICE_RUNNING.lock().unwrap() = true; + *RUNNING.lock().unwrap() = true; }, ConnCount(None) => { if !quick_support { @@ -699,7 +761,7 @@ pub mod client { } } } - *PORTABLE_SERVICE_RUNNING.lock().unwrap() = false; + *RUNNING.lock().unwrap() = false; }); } Err(err) => { @@ -752,7 +814,7 @@ pub mod client { use_yuv: bool, portable_service_running: bool, ) -> ResultType> { - if portable_service_running != PORTABLE_SERVICE_RUNNING.lock().unwrap().clone() { + if portable_service_running != RUNNING.lock().unwrap().clone() { log::info!("portable service status mismatch"); } if portable_service_running { @@ -767,7 +829,7 @@ pub mod client { } pub fn get_cursor_info(pci: PCURSORINFO) -> BOOL { - if PORTABLE_SERVICE_RUNNING.lock().unwrap().clone() { + if RUNNING.lock().unwrap().clone() { get_cursor_info_(&mut SHMEM.lock().unwrap().as_mut().unwrap(), pci) } else { unsafe { winuser::GetCursorInfo(pci) } @@ -775,7 +837,7 @@ pub mod client { } pub fn handle_mouse(evt: &MouseEvent) { - if PORTABLE_SERVICE_RUNNING.lock().unwrap().clone() { + if RUNNING.lock().unwrap().clone() { handle_mouse_(evt).ok(); } else { crate::input_service::handle_mouse_(evt); @@ -783,12 +845,16 @@ pub mod client { } pub fn handle_key(evt: &KeyEvent) { - if PORTABLE_SERVICE_RUNNING.lock().unwrap().clone() { + if RUNNING.lock().unwrap().clone() { handle_key_(evt).ok(); } else { crate::input_service::handle_key_(evt); } } + + pub fn running() -> bool { + RUNNING.lock().unwrap().clone() + } } #[repr(C)] diff --git a/src/server/video_service.rs b/src/server/video_service.rs index 618b003e9..599dfbd54 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -20,14 +20,10 @@ use super::{video_qos::VideoQoS, *}; #[cfg(windows)] -use crate::portable_service::client::PORTABLE_SERVICE_RUNNING; -#[cfg(windows)] use hbb_common::get_version_number; -use hbb_common::{ - tokio::sync::{ - mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}, - Mutex as TokioMutex, - }, +use hbb_common::tokio::sync::{ + mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}, + Mutex as TokioMutex, }; #[cfg(not(windows))] use scrap::Capturer; @@ -419,7 +415,7 @@ fn run(sp: GenericService) -> ResultType<()> { #[cfg(target_os = "linux")] super::wayland::ensure_inited()?; #[cfg(windows)] - let last_portable_service_running = PORTABLE_SERVICE_RUNNING.lock().unwrap().clone(); + let last_portable_service_running = crate::portable_service::client::running(); #[cfg(not(windows))] let last_portable_service_running = false; @@ -518,14 +514,14 @@ fn run(sp: GenericService) -> ResultType<()> { bail!("SWITCH"); } #[cfg(windows)] - if last_portable_service_running != PORTABLE_SERVICE_RUNNING.lock().unwrap().clone() { + if last_portable_service_running != crate::portable_service::client::running() { bail!("SWITCH"); } check_privacy_mode_changed(&sp, c.privacy_mode_id)?; #[cfg(windows)] { if crate::platform::windows::desktop_changed() - && !PORTABLE_SERVICE_RUNNING.lock().unwrap().clone() + && !crate::portable_service::client::running() { bail!("Desktop changed"); } diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index a32662d07..551352ff7 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -780,11 +780,7 @@ fn cm_inner_send(id: i32, data: Data) { pub fn can_elevate() -> bool { #[cfg(windows)] { - return !crate::platform::is_installed() - && !crate::portable_service::client::PORTABLE_SERVICE_RUNNING - .lock() - .unwrap() - .clone(); + return !crate::platform::is_installed() && !crate::portable_service::client::running(); } #[cfg(not(windows))] return false; diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 46bf39b78..00f1f90cf 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -608,6 +608,14 @@ impl Session { } self.update_transfer_list(); } + + pub fn elevate_direct(&self) { + self.send(Data::ElevateDirect); + } + + pub fn elevate_with_logon(&self, username: String, password: String) { + self.send(Data::ElevateWithLogon(username, password)); + } } pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { From 62791613a76ac05e3c045e6797f63fd0e2f33b73 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sun, 15 Jan 2023 19:46:16 +0800 Subject: [PATCH 1499/2015] opt dialog button style Signed-off-by: 21pages --- flutter/lib/common.dart | 36 ++++++- flutter/lib/common/widgets/address_book.dart | 8 +- flutter/lib/common/widgets/dialog.dart | 96 +++++++++--------- flutter/lib/common/widgets/login.dart | 6 +- flutter/lib/common/widgets/peer_card.dart | 12 +-- .../lib/desktop/pages/desktop_home_page.dart | 4 +- .../desktop/pages/desktop_setting_page.dart | 4 +- .../lib/desktop/pages/file_manager_page.dart | 11 +-- .../widgets/kb_layout_type_chooser.dart | 2 +- .../lib/desktop/widgets/remote_menubar.dart | 14 +-- .../lib/desktop/widgets/tabbar_widget.dart | 4 +- .../lib/mobile/pages/file_manager_page.dart | 27 +++-- flutter/lib/mobile/pages/remote_page.dart | 13 +-- flutter/lib/mobile/pages/settings_page.dart | 13 ++- flutter/lib/mobile/widgets/dialog.dart | 99 ++++++------------- flutter/lib/models/file_model.dart | 25 +---- flutter/lib/models/model.dart | 2 +- flutter/lib/models/server_model.dart | 13 ++- 18 files changed, 166 insertions(+), 223 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index c3a8baba9..6ffa5ccb2 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -668,24 +668,25 @@ void msgBox(String id, String type, String title, String text, String link, if (type != "connecting" && type != "success" && !type.contains("nook")) { hasOk = true; - buttons.insert(0, msgBoxButton(translate('OK'), submit)); + buttons.insert(0, dialogButton('OK', onPressed: submit)); } hasCancel ??= !type.contains("error") && !type.contains("nocancel") && type != "restarting"; if (hasCancel) { - buttons.insert(0, msgBoxButton(translate('Cancel'), cancel)); + buttons.insert( + 0, dialogButton('Cancel', onPressed: cancel, isOutline: true)); } // TODO: test this button if (type.contains("hasclose")) { buttons.insert( 0, - msgBoxButton(translate('Close'), () { + dialogButton('Close', onPressed: () { dialogManager.dismissAll(); })); } if (link.isNotEmpty) { - buttons.insert(0, msgBoxButton(translate('JumpLink'), jumplink)); + buttons.insert(0, dialogButton('JumpLink', onPressed: jumplink)); } dialogManager.show( (setState, close) => CustomAlertDialog( @@ -1566,3 +1567,30 @@ class ServerConfig { apiServer = options['api-server'] ?? "", key = options['key'] ?? ""; } + +Widget dialogButton(String text, + {required VoidCallback? onPressed, + bool isOutline = false, + TextStyle? style}) { + if (isDesktop) { + if (isOutline) { + return OutlinedButton( + onPressed: onPressed, + child: Text(translate(text), style: style), + ); + } else { + return ElevatedButton( + style: ElevatedButton.styleFrom(elevation: 0), + onPressed: onPressed, + child: Text(translate(text), style: style), + ); + } + } else { + return TextButton( + onPressed: onPressed, + child: Text( + translate(text), + style: style, + )); + } +} diff --git a/flutter/lib/common/widgets/address_book.dart b/flutter/lib/common/widgets/address_book.dart index 34d5af485..5c1e1218c 100644 --- a/flutter/lib/common/widgets/address_book.dart +++ b/flutter/lib/common/widgets/address_book.dart @@ -335,8 +335,8 @@ class _AddressBookState extends State { ], ), actions: [ - TextButton(onPressed: close, child: Text(translate("Cancel"))), - TextButton(onPressed: submit, child: Text(translate("OK"))), + dialogButton("Cancel", onPressed: close, isOutline: true), + dialogButton("OK", onPressed: submit), ], onSubmit: submit, onCancel: close, @@ -402,8 +402,8 @@ class _AddressBookState extends State { ], ), actions: [ - TextButton(onPressed: close, child: Text(translate("Cancel"))), - TextButton(onPressed: submit, child: Text(translate("OK"))), + dialogButton("Cancel", onPressed: close, isOutline: true), + dialogButton("OK", onPressed: submit), ], onSubmit: submit, onCancel: close, diff --git a/flutter/lib/common/widgets/dialog.dart b/flutter/lib/common/widgets/dialog.dart index a6de0384f..837a197dc 100644 --- a/flutter/lib/common/widgets/dialog.dart +++ b/flutter/lib/common/widgets/dialog.dart @@ -64,8 +64,8 @@ void changeIdDialog() { ], ), actions: [ - TextButton(onPressed: close, child: Text(translate("Cancel"))), - TextButton(onPressed: submit, child: Text(translate("OK"))), + dialogButton("Cancel", onPressed: close, isOutline: true), + dialogButton("OK", onPressed: submit), ], onSubmit: submit, onCancel: close, @@ -111,48 +111,46 @@ void changeWhiteList({Function()? callback}) async { ], ), actions: [ - TextButton(onPressed: close, child: Text(translate("Cancel"))), - TextButton( - onPressed: () async { - await bind.mainSetOption(key: 'whitelist', value: ''); - callback?.call(); - close(); - }, - child: Text(translate("Clear"))), - TextButton( - onPressed: () async { - setState(() { - msg = ""; - isInProgress = true; - }); - newWhiteListField = controller.text.trim(); - var newWhiteList = ""; - if (newWhiteListField.isEmpty) { - // pass - } else { - final ips = - newWhiteListField.trim().split(RegExp(r"[\s,;\n]+")); - // test ip - final ipMatch = RegExp( - r"^(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)(\/([1-9]|[1-2][0-9]|3[0-2])){0,1}$"); - final ipv6Match = RegExp( - r"^(((?:[0-9A-Fa-f]{1,4}))*((?::[0-9A-Fa-f]{1,4}))*::((?:[0-9A-Fa-f]{1,4}))*((?::[0-9A-Fa-f]{1,4}))*|((?:[0-9A-Fa-f]{1,4}))((?::[0-9A-Fa-f]{1,4})){7})(\/([1-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])){0,1}$"); - for (final ip in ips) { - if (!ipMatch.hasMatch(ip) && !ipv6Match.hasMatch(ip)) { - msg = "${translate("Invalid IP")} $ip"; - setState(() { - isInProgress = false; - }); - return; - } + dialogButton("Cancel", onPressed: close, isOutline: true), + dialogButton("Clear", onPressed: () async { + await bind.mainSetOption(key: 'whitelist', value: ''); + callback?.call(); + close(); + }, isOutline: true), + dialogButton( + "OK", + onPressed: () async { + setState(() { + msg = ""; + isInProgress = true; + }); + newWhiteListField = controller.text.trim(); + var newWhiteList = ""; + if (newWhiteListField.isEmpty) { + // pass + } else { + final ips = newWhiteListField.trim().split(RegExp(r"[\s,;\n]+")); + // test ip + final ipMatch = RegExp( + r"^(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)(\/([1-9]|[1-2][0-9]|3[0-2])){0,1}$"); + final ipv6Match = RegExp( + r"^(((?:[0-9A-Fa-f]{1,4}))*((?::[0-9A-Fa-f]{1,4}))*::((?:[0-9A-Fa-f]{1,4}))*((?::[0-9A-Fa-f]{1,4}))*|((?:[0-9A-Fa-f]{1,4}))((?::[0-9A-Fa-f]{1,4})){7})(\/([1-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])){0,1}$"); + for (final ip in ips) { + if (!ipMatch.hasMatch(ip) && !ipv6Match.hasMatch(ip)) { + msg = "${translate("Invalid IP")} $ip"; + setState(() { + isInProgress = false; + }); + return; } - newWhiteList = ips.join(','); } - await bind.mainSetOption(key: 'whitelist', value: newWhiteList); - callback?.call(); - close(); - }, - child: Text(translate("OK"))), + newWhiteList = ips.join(','); + } + await bind.mainSetOption(key: 'whitelist', value: newWhiteList); + callback?.call(); + close(); + }, + ), ], onCancel: close, ); @@ -195,14 +193,12 @@ Future changeDirectAccessPort( ], ), actions: [ - TextButton(onPressed: close, child: Text(translate("Cancel"))), - TextButton( - onPressed: () async { - await bind.mainSetOption( - key: 'direct-access-port', value: controller.text); - close(); - }, - child: Text(translate("OK"))), + dialogButton("Cancel", onPressed: close, isOutline: true), + dialogButton("OK", onPressed: () async { + await bind.mainSetOption( + key: 'direct-access-port', value: controller.text); + close(); + }), ], onCancel: close, ); diff --git a/flutter/lib/common/widgets/login.dart b/flutter/lib/common/widgets/login.dart index 15105ae61..2f10ac005 100644 --- a/flutter/lib/common/widgets/login.dart +++ b/flutter/lib/common/widgets/login.dart @@ -550,7 +550,7 @@ Future loginDialog() async { ), ], ), - actions: [msgBoxButton(translate('Close'), onDialogCancel)], + actions: [dialogButton('Close', onPressed: onDialogCancel)], onCancel: onDialogCancel, ); }); @@ -667,8 +667,8 @@ Future verificationCodeDialog(UserPayload? user) async { ], ), actions: [ - TextButton(onPressed: close, child: Text(translate("Cancel"))), - TextButton(onPressed: onVerify, child: Text(translate("Verify"))), + dialogButton("Cancel", onPressed: close, isOutline: true), + dialogButton("Verify", onPressed: onVerify), ]); }); diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index a98739606..c07b458bc 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -662,8 +662,8 @@ abstract class BasePeerCard extends StatelessWidget { ], ), actions: [ - TextButton(onPressed: close, child: Text(translate("Cancel"))), - TextButton(onPressed: submit, child: Text(translate("OK"))), + dialogButton("Cancel", onPressed: close, isOutline: true), + dialogButton("OK", onPressed: submit), ], onSubmit: submit, onCancel: close, @@ -931,8 +931,8 @@ class AddressBookPeerCard extends BasePeerCard { ], ), actions: [ - TextButton(onPressed: close, child: Text(translate("Cancel"))), - TextButton(onPressed: submit, child: Text(translate("OK"))), + dialogButton("Cancel", onPressed: close, isOutline: true), + dialogButton("OK", onPressed: submit), ], onSubmit: submit, onCancel: close, @@ -1095,8 +1095,8 @@ void _rdpDialog(String id, CardType card) async { ), ), actions: [ - TextButton(onPressed: close, child: Text(translate("Cancel"))), - TextButton(onPressed: submit, child: Text(translate("OK"))), + dialogButton("Cancel", onPressed: close, isOutline: true), + dialogButton("OK", onPressed: submit), ], onSubmit: submit, onCancel: close, diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index fd9814cc2..471a84b1d 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -627,8 +627,8 @@ void setPasswordDialog() async { ), ), actions: [ - TextButton(onPressed: close, child: Text(translate("Cancel"))), - TextButton(onPressed: submit, child: Text(translate("OK"))), + dialogButton("Cancel", onPressed: close, isOutline: true), + dialogButton("OK", onPressed: submit), ], onSubmit: submit, onCancel: close, diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 9f2dc988e..df87a0ead 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -1671,8 +1671,8 @@ void changeSocks5Proxy() async { ), ), actions: [ - TextButton(onPressed: close, child: Text(translate('Cancel'))), - TextButton(onPressed: submit, child: Text(translate('OK'))), + dialogButton('Cancel', onPressed: close, isOutline: true), + dialogButton('OK', onPressed: submit), ], onSubmit: submit, onCancel: close, diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 60b22a516..b6a9e5fed 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -802,14 +802,9 @@ class _FileManagerPageState extends State ], ), actions: [ - TextButton( - style: flatButtonStyle, - onPressed: cancel, - child: Text(translate("Cancel"))), - ElevatedButton( - style: flatButtonStyle, - onPressed: submit, - child: Text(translate("OK"))) + dialogButton("Cancel", + onPressed: cancel, isOutline: true), + dialogButton("OK", onPressed: submit) ], onSubmit: submit, onCancel: cancel, diff --git a/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart b/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart index 384b0f3bd..90e72cd40 100644 --- a/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart +++ b/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart @@ -218,7 +218,7 @@ showKBLayoutTypeChooser( KBLayoutType.value = bind.getLocalKbLayoutType(); return v == KBLayoutType.value; }), - actions: [msgBoxButton(translate('Close'), close)], + actions: [dialogButton('Close', onPressed: close)], onCancel: close, ); }); diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 6545f8556..6a0fa9104 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -809,7 +809,7 @@ class _RemoteMenubarState extends State { } if (newValue == kRemoteImageQualityCustom) { - final btnClose = msgBoxButton(translate('Close'), () async { + final btnClose = dialogButton('Close', onPressed: () async { await setCustomValues(); widget.ffi.dialogManager.dismissAll(); }); @@ -1326,16 +1326,8 @@ void showSetOSPassword( ), ]), actions: [ - TextButton( - style: flatButtonStyle, - onPressed: close, - child: Text(translate('Cancel')), - ), - TextButton( - style: flatButtonStyle, - onPressed: submit, - child: Text(translate('OK')), - ), + dialogButton('Cancel', onPressed: close, isOutline: true), + dialogButton('OK', onPressed: submit), ], onSubmit: submit, onCancel: close, diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 8e2238f11..ec494cf22 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -687,8 +687,8 @@ Future closeConfirmDialog() async { ]), // confirm checkbox actions: [ - TextButton(onPressed: close, child: Text(translate("Cancel"))), - ElevatedButton(onPressed: submit, child: Text(translate("OK"))), + dialogButton("Cancel", onPressed: close, isOutline: true), + dialogButton("OK", onPressed: submit), ], onSubmit: submit, onCancel: close, diff --git a/flutter/lib/mobile/pages/file_manager_page.dart b/flutter/lib/mobile/pages/file_manager_page.dart index 549a44b78..7aa9a0005 100644 --- a/flutter/lib/mobile/pages/file_manager_page.dart +++ b/flutter/lib/mobile/pages/file_manager_page.dart @@ -174,23 +174,18 @@ class _FileManagerPageState extends State { ], ), actions: [ - TextButton( - style: flatButtonStyle, + dialogButton("Cancel", onPressed: () => close(false), - child: Text(translate("Cancel"))), - ElevatedButton( - style: flatButtonStyle, - onPressed: () { - if (name.value.text.isNotEmpty) { - model.createDir(PathUtil.join( - model.currentDir.path, - name.value.text, - model - .getCurrentIsWindows())); - close(); - } - }, - child: Text(translate("OK"))) + isOutline: true), + dialogButton("OK", onPressed: () { + if (name.value.text.isNotEmpty) { + model.createDir(PathUtil.join( + model.currentDir.path, + name.value.text, + model.getCurrentIsWindows())); + close(); + } + }) ])); } else if (v == "hidden") { model.toggleShowHidden(); diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index f0c49e9a9..0a10d8011 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -1098,15 +1098,9 @@ void showSetOSPassword( ), ]), actions: [ - TextButton( - style: flatButtonStyle, - onPressed: () { - close(); - }, - child: Text(translate('Cancel')), - ), - TextButton( - style: flatButtonStyle, + dialogButton('Cancel', onPressed: close, isOutline: true), + dialogButton( + 'OK', onPressed: () { var text = controller.text.trim(); bind.sessionPeerOption(id: id, name: "os-password", value: text); @@ -1117,7 +1111,6 @@ void showSetOSPassword( } close(); }, - child: Text(translate('OK')), ), ]); }); diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index b14f3ee65..c5f3b6935 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -273,13 +273,12 @@ class _SettingsState extends State with WidgetsBindingObserver { content: Text(translate( "android_open_battery_optimizations_tip")), actions: [ - TextButton( - onPressed: () => close(), - child: Text(translate("Cancel"))), - ElevatedButton( - onPressed: () => close(true), - child: - Text(translate("Open System Setting"))), + dialogButton("Cancel", + onPressed: () => close(), isOutline: true), + dialogButton( + "Open System Setting", + onPressed: () => close(true), + ), ], )); if (res == true) { diff --git a/flutter/lib/mobile/widgets/dialog.dart b/flutter/lib/mobile/widgets/dialog.dart index 3b5af1d81..0eb403833 100644 --- a/flutter/lib/mobile/widgets/dialog.dart +++ b/flutter/lib/mobile/widgets/dialog.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:convert'; import 'package:flutter/material.dart'; +import 'package:flutter_hbb/desktop/widgets/button.dart'; import 'package:get/get.dart'; import '../../common.dart'; @@ -33,10 +34,8 @@ void showRestartRemoteDevice( content: Text( "${translate('Are you sure you want to restart')} \n${pi.username}@${pi.hostname}($id) ?"), actions: [ - TextButton( - onPressed: () => close(), child: Text(translate("Cancel"))), - ElevatedButton( - onPressed: () => close(true), child: Text(translate("OK"))), + dialogButton("Cancel", onPressed: () => close(), isOutline: true), + dialogButton("OK", onPressed: () => close(true)), ], )); if (res == true) bind.sessionRestartRemoteDevice(id: id); @@ -96,15 +95,15 @@ void setPermanentPasswordDialog(OverlayDialogManager dialogManager) async { ), ])), actions: [ - TextButton( - style: flatButtonStyle, + dialogButton( + 'Cancel', onPressed: () { close(); }, - child: Text(translate('Cancel')), + isOutline: true, ), - TextButton( - style: flatButtonStyle, + dialogButton( + 'OK', onPressed: (validateLength && validateSame) ? () async { close(); @@ -118,7 +117,6 @@ void setPermanentPasswordDialog(OverlayDialogManager dialogManager) async { } } : null, - child: Text(translate('OK')), ), ], ); @@ -198,16 +196,8 @@ void enterPasswordDialog(String id, OverlayDialogManager dialogManager) async { ), ]), actions: [ - TextButton( - style: flatButtonStyle, - onPressed: cancel, - child: Text(translate('Cancel')), - ), - TextButton( - style: flatButtonStyle, - onPressed: submit, - child: Text(translate('OK')), - ), + dialogButton('Cancel', onPressed: cancel, isOutline: true), + dialogButton('OK', onPressed: submit), ], onSubmit: submit, onCancel: cancel, @@ -220,20 +210,19 @@ void wrongPasswordDialog(String id, OverlayDialogManager dialogManager) { title: Text(translate('Wrong Password')), content: Text(translate('Do you want to enter again?')), actions: [ - TextButton( - style: flatButtonStyle, + dialogButton( + 'Cancel', onPressed: () { close(); closeConnection(); }, - child: Text(translate('Cancel')), + isOutline: true, ), - TextButton( - style: flatButtonStyle, + dialogButton( + 'Retry', onPressed: () { enterPasswordDialog(id, dialogManager); }, - child: Text(translate('Retry')), ), ])); } @@ -321,15 +310,11 @@ void showServerSettingsWithValue( child: LinearProgressIndicator()) ])), actions: [ - TextButton( - style: flatButtonStyle, - onPressed: () { - close(); - }, - child: Text(translate('Cancel')), - ), - TextButton( - style: flatButtonStyle, + dialogButton('Cancel', onPressed: () { + close(); + }, isOutline: true), + dialogButton( + 'OK', onPressed: () async { setState(() { idServerMsg = null; @@ -361,7 +346,6 @@ void showServerSettingsWithValue( isInProgress = false; }); }, - child: Text(translate('OK')), ), ], ); @@ -512,17 +496,8 @@ void _showRequestElevationDialog( title: Text(translate('Request Elevation')), content: content, actions: [ - ElevatedButton( - style: ElevatedButton.styleFrom(elevation: 0), - onPressed: submit, - child: Text(translate('OK')), - ), - OutlinedButton( - onPressed: () { - close(); - }, - child: Text(translate('Cancel')), - ), + dialogButton('Cancel', onPressed: close, isOutline: true), + dialogButton('OK', onPressed: submit), ], onSubmit: submit, onCancel: close, @@ -561,17 +536,10 @@ void showOnBlockDialog( title: Text(translate(title)), content: content, actions: [ - ElevatedButton( - style: ElevatedButton.styleFrom(elevation: 0), - onPressed: submit, - child: Text(translate('Request Elevation')), - ), - OutlinedButton( - onPressed: () { - close(); - }, - child: Text(translate('Wait')), - ), + dialogButton('Wait', onPressed: () { + close(); + }, isOutline: true), + dialogButton('Request Elevation', onPressed: submit), ], onSubmit: submit, onCancel: close, @@ -591,17 +559,10 @@ void showElevationError(String id, String type, String title, String text, title: Text(translate(title)), content: Text(translate(text)), actions: [ - ElevatedButton( - style: ElevatedButton.styleFrom(elevation: 0), - onPressed: submit, - child: Text(translate('Retry')), - ), - OutlinedButton( - onPressed: () { - close(); - }, - child: Text(translate('Cancel')), - ), + dialogButton('Cancel', onPressed: () { + close(); + }, isOutline: true), + dialogButton('Retry', onPressed: submit), ], onSubmit: submit, onCancel: close, diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index b730e6074..18d42d143 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -665,14 +665,8 @@ class FileModel extends ChangeNotifier { : const SizedBox.shrink() ]), actions: [ - TextButton( - style: flatButtonStyle, - onPressed: cancel, - child: Text(translate("Cancel"))), - TextButton( - style: flatButtonStyle, - onPressed: submit, - child: Text(translate("OK"))), + dialogButton("Cancel", onPressed: cancel, isOutline: true), + dialogButton("OK", onPressed: submit), ], onSubmit: submit, onCancel: cancel, @@ -724,18 +718,9 @@ class FileModel extends ChangeNotifier { : const SizedBox.shrink() ]), actions: [ - TextButton( - style: flatButtonStyle, - onPressed: cancel, - child: Text(translate("Cancel"))), - TextButton( - style: flatButtonStyle, - onPressed: () => close(null), - child: Text(translate("Skip"))), - TextButton( - style: flatButtonStyle, - onPressed: submit, - child: Text(translate("OK"))), + dialogButton("Cancel", onPressed: cancel, isOutline: true), + dialogButton("Skip", onPressed: () => close(null), isOutline: true), + dialogButton("OK", onPressed: submit), ], onSubmit: submit, onCancel: cancel, diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 83678ccb7..641165e67 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -271,7 +271,7 @@ class FfiModel with ChangeNotifier { hasCancel: false); } else if (type == 'wait-remote-accept-nook') { msgBoxCommon(dialogManager, title, Text(translate(text)), - [msgBoxButton("Cancel", closeConnection)]); + [dialogButton("Cancel", onPressed: closeConnection)]); } else if (type == 'on-uac' || type == 'on-foreground-elevated') { showOnBlockDialog(id, type, title, text, dialogManager); } else if (type == 'wait-uac') { diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index 338da4ee3..c36a54db6 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -304,8 +304,8 @@ class ServerModel with ChangeNotifier { ]), content: Text(translate("android_service_will_start_tip")), actions: [ - TextButton(onPressed: close, child: Text(translate("Cancel"))), - ElevatedButton(onPressed: submit, child: Text(translate("OK"))), + dialogButton("Cancel", onPressed: close, isOutline: true), + dialogButton("OK", onPressed: submit), ], onSubmit: submit, onCancel: close, @@ -501,8 +501,8 @@ class ServerModel with ChangeNotifier { ], ), actions: [ - TextButton(onPressed: cancel, child: Text(translate("Dismiss"))), - ElevatedButton(onPressed: submit, child: Text(translate("Accept"))), + dialogButton("Dismiss", onPressed: cancel, isOutline: true), + dialogButton("Accept", onPressed: submit), ], onSubmit: submit, onCancel: cancel, @@ -674,9 +674,8 @@ showInputWarnAlert(FFI ffi) { ], ), actions: [ - TextButton(onPressed: close, child: Text(translate("Cancel"))), - ElevatedButton( - onPressed: submit, child: Text(translate("Open System Setting"))), + dialogButton("Cancel", onPressed: close, isOutline: true), + dialogButton("Open System Setting", onPressed: submit), ], onSubmit: submit, onCancel: close, From 6a46783ebff24ad6603cec2c826ce218275571f3 Mon Sep 17 00:00:00 2001 From: NicKoehler <53040044+NicKoehler@users.noreply.github.com> Date: Sun, 15 Jan 2023 15:30:44 +0100 Subject: [PATCH 1500/2015] Update it.rs --- src/lang/it.rs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/lang/it.rs b/src/lang/it.rs index cd132dc45..858edbd84 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -410,18 +410,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the web console", "Chiudi manualmente dalla console Web"), ("Local keyboard type", "Tipo di tastiera locale"), ("Select local keyboard type", "Seleziona il tipo di tastiera locale"), - ("software_render_tip", ""), + ("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", ""), - ("request_elevation_tip", ""), - ("Wait",""), - ("Elevation Error", ""), - ("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", ""), - ("Request Elevation", ""), - ("wait_accept_uac_tip", ""), - ("Elevate successfully", ""), + ("config_input", "Per controllare il desktop remoto con la tastiera, è necessario concedere le autorizzazioni a RustDesk \"Monitoraggio dell'input\"."), + ("request_elevation_tip", "È possibile richiedere l'elevazione se c'è qualcuno sul lato remoto."), + ("Wait", "Attendi"), + ("Elevation Error", "Errore durante l'elevazione dei diritti"), + ("Ask the remote user for authentication", "Chiedere l'autenticazione all'utente remoto"), + ("Choose this if the remote account is administrator", "Scegliere questa opzione se l'account remoto è amministratore"), + ("Transmit the username and password of administrator", "Trasmettere il nome utente e la password dell'amministratore"), + ("still_click_uac_tip", "Richiede ancora che l'utente remoto faccia clic su OK nella finestra UAC dell'esecuzione di RustDesk."), + ("Request Elevation", "Richiedi elevazione dei diritti"), + ("wait_accept_uac_tip", "Attendere che l'utente remoto accetti la finestra di dialogo UAC."), + ("Elevate successfully", "Elevazione dei diritti effettuata con successo"), ].iter().cloned().collect(); } From ea3e0fd906c25c6068150bf14c042d2f065a12bc Mon Sep 17 00:00:00 2001 From: Mr-Update <37781396+Mr-Update@users.noreply.github.com> Date: Sun, 15 Jan 2023 18:12:47 +0100 Subject: [PATCH 1501/2015] Update de.rs --- src/lang/de.rs | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/lang/de.rs b/src/lang/de.rs index 8060deb49..74c674b56 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -26,7 +26,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable Keyboard/Mouse", "Tastatur und Maus aktivieren"), ("Enable Clipboard", "Zwischenablage aktivieren"), ("Enable File Transfer", "Dateiübertragung aktivieren"), - ("Enable TCP Tunneling", "TCP-Tunnel aktivieren"), + ("Enable TCP Tunneling", "TCP-Tunnelung aktivieren"), ("IP Whitelisting", "IP-Whitelist"), ("ID/Relay Server", "ID/Vermittlungsserver"), ("Import Server Config", "Serverkonfiguration importieren"), @@ -39,7 +39,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "ID ändern"), ("Website", "Webseite"), ("About", "Über"), - ("Slogan_tip", "Mit Herzblut programmiert - in einer Welt, die im Chaos versinkt"), + ("Slogan_tip", "Mit Herzblut programmiert - in einer Welt, die im Chaos versinkt!"), ("Privacy Statement", "Datenschutz"), ("Mute", "Stummschalten"), ("Audio Input", "Audioeingang"), @@ -224,7 +224,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add Tag", "Stichwort hinzufügen"), ("Unselect all tags", "Alle Stichworte abwählen"), ("Network error", "Netzwerkfehler"), - ("Username missed", "Benutzername vergessen"), + ("Username missed", "Benutzernamen vergessen"), ("Password missed", "Passwort vergessen"), ("Wrong credentials", "Falsche Anmeldedaten"), ("Edit Tag", "Schlagwort bearbeiten"), @@ -413,15 +413,15 @@ 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."), - ("request_elevation_tip", ""), - ("Wait",""), - ("Elevation Error", ""), - ("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", ""), - ("Request Elevation", ""), - ("wait_accept_uac_tip", ""), - ("Elevate successfully", ""), + ("request_elevation_tip", "Sie können auch erhöhte Rechte anfordern, wenn sich jemand auf der Gegenseite befindet."), + ("Wait","Warten"), + ("Elevation Error", "Berechtigungsfehler"), + ("Ask the remote user for authentication", "Den entfernten Benutzer zur Authentifizierung auffordern"), + ("Choose this if the remote account is administrator", "Wählen Sie dies, wenn das entfernte Konto Administrator ist"), + ("Transmit the username and password of administrator", "Übermitteln Sie den Benutzernamen und das Passwort des Administrators"), + ("still_click_uac_tip", "Der entfernte Benutzer muss immer noch im UAC-Fenster von RustDesk auf OK klicken."), + ("Request Elevation", "Erhöhte Rechte anfordern"), + ("wait_accept_uac_tip", "Bitte warten Sie, bis der entfernte Benutzer den UAC-Dialog akzeptiert hat."), + ("Elevate successfully", "Erhöhung der Rechte erfolgreich"), ].iter().cloned().collect(); } From 485479c31b42798ff32f8dda0cb2d771827714a0 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sat, 14 Jan 2023 15:09:25 +0800 Subject: [PATCH 1502/2015] sync: depend on web Signed-off-by: 21pages --- src/hbbs_http/sync.rs | 32 ++++++++------------------------ 1 file changed, 8 insertions(+), 24 deletions(-) diff --git a/src/hbbs_http/sync.rs b/src/hbbs_http/sync.rs index 9497cc449..a060d6a20 100644 --- a/src/hbbs_http/sync.rs +++ b/src/hbbs_http/sync.rs @@ -54,6 +54,7 @@ async fn start_hbbs_sync_async() { last_send = Instant::now(); let mut v = Value::default(); v["id"] = json!(Config::get_id()); + v["ver"] = json!(hbb_common::get_version_number(crate::VERSION)); if !conns.is_empty() { v["conns"] = json!(conns); } @@ -100,33 +101,16 @@ fn heartbeat_url() -> String { } fn handle_config_options(config_options: HashMap) { - let map = HashMap::from([ - ("enable-keyboard", ""), - ("enable-clipboard", ""), - ("enable-file-transfer", ""), - ("enable-audio", ""), - ("enable-tunnel", ""), - ("enable-remote-restart", ""), - ("enable-record-session", ""), - ("allow-remote-config-modification", ""), - ("approve-mode", ""), - ("verification-method", "use-both-passwords"), - ("enable-rdp", ""), - ("enable-lan-discovery", ""), - ("direct-server", ""), - ("direct-access-port", ""), - ]); let mut options = Config::get_options(); - for (k, v) in map { - if let Some(v2) = config_options.get(k) { - if v == v2 { + config_options + .iter() + .map(|(k, v)| { + if v.is_empty() { options.remove(k); } else { - options.insert(k.to_string(), v2.to_string()); + options.insert(k.to_string(), v.to_string()); } - } else { - options.remove(k); - } - } + }) + .count(); Config::set_options(options); } From 9aecd287028e92f73d861002b5abcb88ca6be85e Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 16 Jan 2023 19:47:58 +0800 Subject: [PATCH 1503/2015] complex pernament password lowercase, uppercase, digit, length>=8 Signed-off-by: 21pages --- .../lib/common/widgets/custom_password.dart | 121 ++++++++++++++++++ .../lib/desktop/pages/desktop_home_page.dart | 103 ++++++++++----- flutter/pubspec.yaml | 1 + src/lang/ca.rs | 10 +- src/lang/cn.rs | 10 +- src/lang/cs.rs | 10 +- src/lang/da.rs | 10 +- src/lang/de.rs | 10 +- src/lang/eo.rs | 10 +- src/lang/es.rs | 10 +- src/lang/fa.rs | 10 +- src/lang/fr.rs | 10 +- src/lang/gr.rs | 10 +- src/lang/hu.rs | 10 +- src/lang/id.rs | 10 +- src/lang/it.rs | 8 ++ src/lang/ja.rs | 10 +- src/lang/ko.rs | 10 +- src/lang/kz.rs | 10 +- src/lang/pl.rs | 10 +- src/lang/pt_PT.rs | 10 +- src/lang/ptbr.rs | 10 +- src/lang/ru.rs | 10 +- src/lang/sk.rs | 10 +- src/lang/sl.rs | 10 +- src/lang/sq.rs | 10 +- src/lang/sr.rs | 10 +- src/lang/sv.rs | 10 +- src/lang/template.rs | 10 +- src/lang/th.rs | 10 +- src/lang/tr.rs | 10 +- src/lang/tw.rs | 10 +- src/lang/ua.rs | 10 +- src/lang/vn.rs | 10 +- 34 files changed, 468 insertions(+), 65 deletions(-) create mode 100644 flutter/lib/common/widgets/custom_password.dart diff --git a/flutter/lib/common/widgets/custom_password.dart b/flutter/lib/common/widgets/custom_password.dart new file mode 100644 index 000000000..99ece2434 --- /dev/null +++ b/flutter/lib/common/widgets/custom_password.dart @@ -0,0 +1,121 @@ +// https://github.com/rodrigobastosv/fancy_password_field +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:get/get.dart'; +import 'package:password_strength/password_strength.dart'; + +abstract class ValidationRule { + String get name; + bool validate(String value); +} + +class UppercaseValidationRule extends ValidationRule { + @override + String get name => translate('uppercase'); + @override + bool validate(String value) { + return value.contains(RegExp(r'[A-Z]')); + } +} + +class LowercaseValidationRule extends ValidationRule { + @override + String get name => translate('lowercase'); + + @override + bool validate(String value) { + return value.contains(RegExp(r'[a-z]')); + } +} + +class DigitValidationRule extends ValidationRule { + @override + String get name => translate('digit'); + + @override + bool validate(String value) { + return value.contains(RegExp(r'[0-9]')); + } +} + +class SpecialCharacterValidationRule extends ValidationRule { + @override + String get name => translate('special character'); + + @override + bool validate(String value) { + return value.contains(RegExp(r'[!@#$%^&*(),.?":{}|<>]')); + } +} + +class MinCharactersValidationRule extends ValidationRule { + final int _numberOfCharacters; + MinCharactersValidationRule(this._numberOfCharacters); + + @override + String get name => translate('length>=$_numberOfCharacters'); + + @override + bool validate(String value) { + return value.length >= _numberOfCharacters; + } +} + +class PasswordStrengthIndicator extends StatelessWidget { + final RxString password; + final double weakMedium = 0.33; + final double mediumStrong = 0.67; + const PasswordStrengthIndicator({Key? key, required this.password}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return Obx(() { + var strength = estimatePasswordStrength(password.value); + return Row( + children: [ + Expanded( + child: _indicator( + password.isEmpty ? Colors.grey : _getColor(strength))), + Expanded( + child: _indicator(password.isEmpty || strength < weakMedium + ? Colors.grey + : _getColor(strength))), + Expanded( + child: _indicator(password.isEmpty || strength < mediumStrong + ? Colors.grey + : _getColor(strength))), + Text(password.isEmpty ? '' : translate(_getLabel(strength))) + .marginOnly(left: password.isEmpty ? 0 : 8), + ], + ); + }); + } + + Widget _indicator(Color color) { + return Container( + height: 8, + color: color, + ); + } + + String _getLabel(double strength) { + if (strength < weakMedium) { + return 'Weak'; + } else if (strength < mediumStrong) { + return 'Medium'; + } else { + return 'Strong'; + } + } + + Color _getColor(double strength) { + if (strength < weakMedium) { + return Colors.yellow; + } else if (strength < mediumStrong) { + return Colors.blue; + } else { + return Colors.green; + } + } +} diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 471a84b1d..65c38e06b 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -6,6 +6,7 @@ import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart' hide MenuItem; import 'package:flutter/services.dart'; import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/common/widgets/custom_password.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/desktop/pages/connection_page.dart'; import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart'; @@ -543,6 +544,14 @@ void setPasswordDialog() async { final p1 = TextEditingController(text: pw); var errMsg0 = ""; var errMsg1 = ""; + final RxString rxPass = p0.text.obs; + final rules = [ + DigitValidationRule(), + UppercaseValidationRule(), + LowercaseValidationRule(), + // SpecialCharacterValidationRule(), + MinCharactersValidationRule(8), + ]; gFFI.dialogManager.show((setState, close) { submit() { @@ -551,15 +560,20 @@ void setPasswordDialog() async { errMsg1 = ""; }); final pass = p0.text.trim(); - if (pass.length < 6 && pass.isNotEmpty) { - setState(() { - errMsg0 = translate("Too short, at least 6 characters."); - }); - return; + if (pass.isNotEmpty) { + for (var r in rules) { + if (!r.validate(pass)) { + setState(() { + errMsg0 = '${translate('Prompt')}: ${r.name}'; + }); + return; + } + } } if (p1.text.trim() != pass) { setState(() { - errMsg1 = translate("The confirmation is not identical."); + errMsg1 = + '${translate('Prompt')}: ${translate("The confirmation is not identical.")}'; }); return; } @@ -579,23 +593,44 @@ void setPasswordDialog() async { ), Row( children: [ - ConstrainedBox( - constraints: const BoxConstraints(minWidth: 100), - child: Text( - "${translate('Password')}:", - textAlign: TextAlign.start, - ).marginOnly(bottom: 16.0)), - const SizedBox( - width: 24.0, - ), Expanded( child: TextField( obscureText: true, decoration: InputDecoration( + isDense: true, + contentPadding: EdgeInsets.all(15), + labelText: translate('Password'), border: const OutlineInputBorder(), errorText: errMsg0.isNotEmpty ? errMsg0 : null), controller: p0, focusNode: FocusNode()..requestFocus(), + onChanged: (value) { + rxPass.value = value; + }, + ), + ), + ], + ), + Row( + children: [ + Expanded(child: PasswordStrengthIndicator(password: rxPass)), + ], + ).marginSymmetric(vertical: 8), + const SizedBox( + height: 8.0, + ), + Row( + children: [ + Expanded( + child: TextField( + obscureText: true, + decoration: InputDecoration( + isDense: true, + contentPadding: EdgeInsets.all(15), + border: const OutlineInputBorder(), + labelText: translate('Confirmation'), + errorText: errMsg1.isNotEmpty ? errMsg1 : null), + controller: p1, ), ), ], @@ -603,26 +638,24 @@ void setPasswordDialog() async { const SizedBox( height: 8.0, ), - Row( - children: [ - ConstrainedBox( - constraints: const BoxConstraints(minWidth: 100), - child: Text("${translate('Confirmation')}:") - .marginOnly(bottom: 16.0)), - const SizedBox( - width: 24.0, - ), - Expanded( - child: TextField( - obscureText: true, - decoration: InputDecoration( - border: const OutlineInputBorder(), - errorText: errMsg1.isNotEmpty ? errMsg1 : null), - controller: p1, - ), - ), - ], - ), + Obx(() => Wrap( + runSpacing: 8, + spacing: 4, + children: rules.map((e) { + var checked = e.validate(rxPass.value.trim()); + 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(), + )) ], ), ), diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 705f4650c..f096218b0 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -93,6 +93,7 @@ dependencies: auto_size_text: ^3.0.0 bot_toast: ^4.0.3 win32: any + password_strength: ^0.2.0 dev_dependencies: diff --git a/src/lang/ca.rs b/src/lang/ca.rs index d54b588c6..bbcea1347 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -414,7 +414,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always use software rendering", ""), ("config_input", ""), ("request_elevation_tip", ""), - ("Wait",""), + ("Wait", ""), ("Elevation Error", ""), ("Ask the remote user for authentication", ""), ("Choose this if the remote account is administrator", ""), @@ -423,5 +423,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request Elevation", ""), ("wait_accept_uac_tip", ""), ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 61da5d331..2f56b6da0 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -414,7 +414,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always use software rendering", "使用软件渲染"), ("config_input", "为了能够通过键盘控制远程桌面, 请给予 RustDesk \"输入监控\" 权限。"), ("request_elevation_tip", "如果对面有人, 也可以请求提升权限。"), - ("Wait","等待"), + ("Wait", "等待"), ("Elevation Error", "提权失败"), ("Ask the remote user for authentication", "请求远端用户授权"), ("Choose this if the remote account is administrator", "当对面电脑是管理员账号时选择该选项"), @@ -423,5 +423,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request Elevation", "请求提权"), ("wait_accept_uac_tip", "请等待远端用户确认UAC对话框。"), ("Elevate successfully", "提权成功"), + ("uppercase", "大写字母"), + ("lowercase", "小写字母"), + ("digit", "数字"), + ("special character", "特殊字符"), + ("length>=8", "长度不小于8"), + ("Weak", "弱"), + ("Medium", "中"), + ("Strong", "强"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index d43a534ee..8852d602c 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -414,7 +414,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always use software rendering", ""), ("config_input", ""), ("request_elevation_tip", ""), - ("Wait",""), + ("Wait", ""), ("Elevation Error", ""), ("Ask the remote user for authentication", ""), ("Choose this if the remote account is administrator", ""), @@ -423,5 +423,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request Elevation", ""), ("wait_accept_uac_tip", ""), ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 0f7823b72..53ae46bd4 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -414,7 +414,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always use software rendering", ""), ("config_input", ""), ("request_elevation_tip", ""), - ("Wait",""), + ("Wait", ""), ("Elevation Error", ""), ("Ask the remote user for authentication", ""), ("Choose this if the remote account is administrator", ""), @@ -423,5 +423,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request Elevation", ""), ("wait_accept_uac_tip", ""), ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 74c674b56..292b2ed28 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -414,7 +414,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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."), ("request_elevation_tip", "Sie können auch erhöhte Rechte anfordern, wenn sich jemand auf der Gegenseite befindet."), - ("Wait","Warten"), + ("Wait", "Warten"), ("Elevation Error", "Berechtigungsfehler"), ("Ask the remote user for authentication", "Den entfernten Benutzer zur Authentifizierung auffordern"), ("Choose this if the remote account is administrator", "Wählen Sie dies, wenn das entfernte Konto Administrator ist"), @@ -423,5 +423,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request Elevation", "Erhöhte Rechte anfordern"), ("wait_accept_uac_tip", "Bitte warten Sie, bis der entfernte Benutzer den UAC-Dialog akzeptiert hat."), ("Elevate successfully", "Erhöhung der Rechte erfolgreich"), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 8503d153a..955a3287d 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -414,7 +414,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always use software rendering", ""), ("config_input", ""), ("request_elevation_tip", ""), - ("Wait",""), + ("Wait", ""), ("Elevation Error", ""), ("Ask the remote user for authentication", ""), ("Choose this if the remote account is administrator", ""), @@ -423,5 +423,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request Elevation", ""), ("wait_accept_uac_tip", ""), ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index c06d3f775..bae1b5cbf 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -414,7 +414,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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\"."), ("request_elevation_tip", ""), - ("Wait",""), + ("Wait", ""), ("Elevation Error", ""), ("Ask the remote user for authentication", ""), ("Choose this if the remote account is administrator", ""), @@ -423,5 +423,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request Elevation", ""), ("wait_accept_uac_tip", ""), ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 6bdd05061..a257425f1 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -414,7 +414,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always use software rendering", ""), ("config_input", ""), ("request_elevation_tip", ""), - ("Wait",""), + ("Wait", ""), ("Elevation Error", ""), ("Ask the remote user for authentication", ""), ("Choose this if the remote account is administrator", ""), @@ -423,5 +423,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request Elevation", ""), ("wait_accept_uac_tip", ""), ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index bbc50e4a6..6edec8477 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -414,7 +414,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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\"."), ("request_elevation_tip", ""), - ("Wait",""), + ("Wait", ""), ("Elevation Error", ""), ("Ask the remote user for authentication", ""), ("Choose this if the remote account is administrator", ""), @@ -423,5 +423,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request Elevation", ""), ("wait_accept_uac_tip", ""), ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), ].iter().cloned().collect(); } diff --git a/src/lang/gr.rs b/src/lang/gr.rs index 7a0e0b35d..81a50bcd8 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -414,7 +414,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always use software rendering", ""), ("config_input", ""), ("request_elevation_tip", ""), - ("Wait",""), + ("Wait", ""), ("Elevation Error", ""), ("Ask the remote user for authentication", ""), ("Choose this if the remote account is administrator", ""), @@ -423,5 +423,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request Elevation", ""), ("wait_accept_uac_tip", ""), ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 342a29bc1..9e1a4d982 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -414,7 +414,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always use software rendering", ""), ("config_input", ""), ("request_elevation_tip", ""), - ("Wait",""), + ("Wait", ""), ("Elevation Error", ""), ("Ask the remote user for authentication", ""), ("Choose this if the remote account is administrator", ""), @@ -423,5 +423,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request Elevation", ""), ("wait_accept_uac_tip", ""), ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 671c2a8fc..65c30ec6d 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -414,7 +414,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always use software rendering", ""), ("config_input", ""), ("request_elevation_tip", ""), - ("Wait",""), + ("Wait", ""), ("Elevation Error", ""), ("Ask the remote user for authentication", ""), ("Choose this if the remote account is administrator", ""), @@ -423,5 +423,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request Elevation", ""), ("wait_accept_uac_tip", ""), ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 858edbd84..f94669d34 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -423,5 +423,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request Elevation", "Richiedi elevazione dei diritti"), ("wait_accept_uac_tip", "Attendere che l'utente remoto accetti la finestra di dialogo UAC."), ("Elevate successfully", "Elevazione dei diritti effettuata con successo"), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 4343d5cec..6ebb11ef5 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -414,7 +414,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always use software rendering", ""), ("config_input", ""), ("request_elevation_tip", ""), - ("Wait",""), + ("Wait", ""), ("Elevation Error", ""), ("Ask the remote user for authentication", ""), ("Choose this if the remote account is administrator", ""), @@ -423,5 +423,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request Elevation", ""), ("wait_accept_uac_tip", ""), ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index c9874b2d6..a6825b523 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -414,7 +414,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always use software rendering", ""), ("config_input", ""), ("request_elevation_tip", ""), - ("Wait",""), + ("Wait", ""), ("Elevation Error", ""), ("Ask the remote user for authentication", ""), ("Choose this if the remote account is administrator", ""), @@ -423,5 +423,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request Elevation", ""), ("wait_accept_uac_tip", ""), ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 049d490a1..816eb370d 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -414,7 +414,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always use software rendering", ""), ("config_input", ""), ("request_elevation_tip", ""), - ("Wait",""), + ("Wait", ""), ("Elevation Error", ""), ("Ask the remote user for authentication", ""), ("Choose this if the remote account is administrator", ""), @@ -423,5 +423,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request Elevation", ""), ("wait_accept_uac_tip", ""), ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 5d0d575b9..df985cccf 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -414,7 +414,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always use software rendering", ""), ("config_input", ""), ("request_elevation_tip", ""), - ("Wait",""), + ("Wait", ""), ("Elevation Error", ""), ("Ask the remote user for authentication", ""), ("Choose this if the remote account is administrator", ""), @@ -423,5 +423,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request Elevation", ""), ("wait_accept_uac_tip", ""), ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 0749678d8..dba37b5da 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -414,7 +414,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always use software rendering", ""), ("config_input", ""), ("request_elevation_tip", ""), - ("Wait",""), + ("Wait", ""), ("Elevation Error", ""), ("Ask the remote user for authentication", ""), ("Choose this if the remote account is administrator", ""), @@ -423,5 +423,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request Elevation", ""), ("wait_accept_uac_tip", ""), ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index f6c43aab0..31c9153f2 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -414,7 +414,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always use software rendering", ""), ("config_input", ""), ("request_elevation_tip", ""), - ("Wait",""), + ("Wait", ""), ("Elevation Error", ""), ("Ask the remote user for authentication", ""), ("Choose this if the remote account is administrator", ""), @@ -423,5 +423,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request Elevation", ""), ("wait_accept_uac_tip", ""), ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 77f64e75d..b22d49cc2 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -414,7 +414,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always use software rendering", "Использовать программную визуализацию"), ("config_input", "Чтобы управлять удалённым рабочим столом с помощью клавиатуры, необходимо предоставить RustDesk разрешения \"Мониторинг ввода\"."), ("request_elevation_tip", ""), - ("Wait",""), + ("Wait", ""), ("Elevation Error", ""), ("Ask the remote user for authentication", ""), ("Choose this if the remote account is administrator", ""), @@ -423,5 +423,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request Elevation", ""), ("wait_accept_uac_tip", ""), ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 954e3ae92..56d14652d 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -414,7 +414,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always use software rendering", ""), ("config_input", ""), ("request_elevation_tip", ""), - ("Wait",""), + ("Wait", ""), ("Elevation Error", ""), ("Ask the remote user for authentication", ""), ("Choose this if the remote account is administrator", ""), @@ -423,5 +423,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request Elevation", ""), ("wait_accept_uac_tip", ""), ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 3b4ee16da..3d2ad3be8 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -414,7 +414,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always use software rendering", ""), ("config_input", ""), ("request_elevation_tip", ""), - ("Wait",""), + ("Wait", ""), ("Elevation Error", ""), ("Ask the remote user for authentication", ""), ("Choose this if the remote account is administrator", ""), @@ -423,5 +423,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request Elevation", ""), ("wait_accept_uac_tip", ""), ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 804a88798..165597e7e 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -414,7 +414,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always use software rendering", ""), ("config_input", ""), ("request_elevation_tip", ""), - ("Wait",""), + ("Wait", ""), ("Elevation Error", ""), ("Ask the remote user for authentication", ""), ("Choose this if the remote account is administrator", ""), @@ -423,5 +423,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request Elevation", ""), ("wait_accept_uac_tip", ""), ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 9ecdd1843..739d53570 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -414,7 +414,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always use software rendering", ""), ("config_input", ""), ("request_elevation_tip", ""), - ("Wait",""), + ("Wait", ""), ("Elevation Error", ""), ("Ask the remote user for authentication", ""), ("Choose this if the remote account is administrator", ""), @@ -423,5 +423,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request Elevation", ""), ("wait_accept_uac_tip", ""), ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index ccf1eed8a..498131d0c 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -414,7 +414,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always use software rendering", ""), ("config_input", ""), ("request_elevation_tip", ""), - ("Wait",""), + ("Wait", ""), ("Elevation Error", ""), ("Ask the remote user for authentication", ""), ("Choose this if the remote account is administrator", ""), @@ -423,5 +423,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request Elevation", ""), ("wait_accept_uac_tip", ""), ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index f8a7a3bc7..adb05c943 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -414,7 +414,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always use software rendering", ""), ("config_input", ""), ("request_elevation_tip", ""), - ("Wait",""), + ("Wait", ""), ("Elevation Error", ""), ("Ask the remote user for authentication", ""), ("Choose this if the remote account is administrator", ""), @@ -423,5 +423,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request Elevation", ""), ("wait_accept_uac_tip", ""), ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index 5bd969bdc..2b062c3f7 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -414,7 +414,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always use software rendering", ""), ("config_input", ""), ("request_elevation_tip", ""), - ("Wait",""), + ("Wait", ""), ("Elevation Error", ""), ("Ask the remote user for authentication", ""), ("Choose this if the remote account is administrator", ""), @@ -423,5 +423,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request Elevation", ""), ("wait_accept_uac_tip", ""), ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index ee661d9b6..a4a179c86 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -414,7 +414,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always use software rendering", ""), ("config_input", ""), ("request_elevation_tip", ""), - ("Wait",""), + ("Wait", ""), ("Elevation Error", ""), ("Ask the remote user for authentication", ""), ("Choose this if the remote account is administrator", ""), @@ -423,5 +423,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request Elevation", ""), ("wait_accept_uac_tip", ""), ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index aebf8917e..cd9f270ec 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -414,7 +414,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always use software rendering", "使用軟件渲染"), ("config_input", ""), ("request_elevation_tip", "如果對面有人, 也可以請求提升權限。"), - ("Wait","等待"), + ("Wait", "等待"), ("Elevation Error", "提權失敗"), ("Ask the remote user for authentication", "請求遠端用戶授權"), ("Choose this if the remote account is administrator", "當對面電腦是管理員賬號時選擇該選項"), @@ -423,5 +423,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request Elevation", "請求提權"), ("wait_accept_uac_tip", "請等待遠端用戶確認UAC對話框。"), ("Elevate successfully", "提權成功"), + ("uppercase", "大寫字母"), + ("lowercase", "小寫字母"), + ("digit", "數字"), + ("special character", "特殊字符"), + ("length>=8", "長度不小於8"), + ("Weak", "弱"), + ("Medium", "中"), + ("Strong", "強"), ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 784592ff0..ff24baab7 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -414,7 +414,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always use software rendering", ""), ("config_input", ""), ("request_elevation_tip", ""), - ("Wait",""), + ("Wait", ""), ("Elevation Error", ""), ("Ask the remote user for authentication", ""), ("Choose this if the remote account is administrator", ""), @@ -423,5 +423,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request Elevation", ""), ("wait_accept_uac_tip", ""), ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index ac62631c9..6988efba7 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -414,7 +414,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always use software rendering", ""), ("config_input", ""), ("request_elevation_tip", ""), - ("Wait",""), + ("Wait", ""), ("Elevation Error", ""), ("Ask the remote user for authentication", ""), ("Choose this if the remote account is administrator", ""), @@ -423,5 +423,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request Elevation", ""), ("wait_accept_uac_tip", ""), ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), ].iter().cloned().collect(); } From cc0f4509a7685ff1e03f0c2b33e049616dacfaa6 Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 16 Jan 2023 20:24:21 +0800 Subject: [PATCH 1504/2015] common dialog InputDecoration Signed-off-by: 21pages --- flutter/lib/common.dart | 10 ++++++++-- flutter/lib/desktop/pages/desktop_home_page.dart | 4 ---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 6ffa5ccb2..04d7b85d0 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -634,8 +634,14 @@ class CustomAlertDialog extends StatelessWidget { title: title, contentPadding: EdgeInsets.symmetric( horizontal: contentPadding ?? 25, vertical: 10), - content: - ConstrainedBox(constraints: contentBoxConstraints, child: content), + content: ConstrainedBox( + constraints: contentBoxConstraints, + child: Theme( + data: ThemeData( + inputDecorationTheme: InputDecorationTheme( + isDense: true, contentPadding: EdgeInsets.all(15)), + ), + child: content)), actions: actions, ), ); diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 65c38e06b..2773a3049 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -597,8 +597,6 @@ void setPasswordDialog() async { child: TextField( obscureText: true, decoration: InputDecoration( - isDense: true, - contentPadding: EdgeInsets.all(15), labelText: translate('Password'), border: const OutlineInputBorder(), errorText: errMsg0.isNotEmpty ? errMsg0 : null), @@ -625,8 +623,6 @@ void setPasswordDialog() async { child: TextField( obscureText: true, decoration: InputDecoration( - isDense: true, - contentPadding: EdgeInsets.all(15), border: const OutlineInputBorder(), labelText: translate('Confirmation'), errorText: errMsg1.isNotEmpty ? errMsg1 : null), From d793fa64a3eb0c6bbf95ca08ffa169522cf39920 Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 16 Jan 2023 20:58:42 +0800 Subject: [PATCH 1505/2015] dialog tab order Signed-off-by: 21pages --- flutter/lib/common.dart | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 04d7b85d0..23aa9535d 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -613,8 +613,9 @@ class CustomAlertDialog extends StatelessWidget { Future.delayed(Duration.zero, () { if (!focusNode.hasFocus) focusNode.requestFocus(); }); - return Focus( - focusNode: focusNode, + FocusScopeNode scopeNode = FocusScopeNode(); + return FocusScope( + node: scopeNode, autofocus: true, onKey: (node, key) { if (key.logicalKey == LogicalKeyboardKey.escape) { @@ -626,6 +627,11 @@ class CustomAlertDialog extends StatelessWidget { 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(); + } + return KeyEventResult.handled; } return KeyEventResult.ignored; }, From 260b8f0b12727536e323da5ec405f2e70ed2e668 Mon Sep 17 00:00:00 2001 From: Mr-Update <37781396+Mr-Update@users.noreply.github.com> Date: Mon, 16 Jan 2023 20:51:30 +0100 Subject: [PATCH 1506/2015] Update de.rs --- src/lang/de.rs | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/lang/de.rs b/src/lang/de.rs index 292b2ed28..dd05dcdd5 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -13,7 +13,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Service is running", "Vermittlungsdienst aktiv"), ("Service is not running", "Vermittlungsdienst deaktiviert"), ("not_ready_status", "Nicht bereit. Bitte überprüfen Sie Ihre Netzwerkverbindung."), - ("Control Remote Desktop", "Entfernten PC steuern"), + ("Control Remote Desktop", "Entfernten Desktop steuern"), ("Transfer File", "Datei übertragen"), ("Connect", "Verbinden"), ("Recent Sessions", "Letzte Sitzungen"), @@ -28,7 +28,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable File Transfer", "Dateiübertragung aktivieren"), ("Enable TCP Tunneling", "TCP-Tunnelung aktivieren"), ("IP Whitelisting", "IP-Whitelist"), - ("ID/Relay Server", "ID/Vermittlungsserver"), + ("ID/Relay Server", "ID/Relay-Server"), ("Import Server Config", "Serverkonfiguration importieren"), ("Export Server Config", "Serverkonfiguration exportieren"), ("Import server configuration successfully", "Serverkonfiguration erfolgreich importiert"), @@ -47,7 +47,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Hardware Codec", "Hardware-Codec"), ("Adaptive Bitrate", "Bitrate automatisch anpassen"), ("ID Server", "ID-Server"), - ("Relay Server", "Vermittlungsserver"), + ("Relay Server", "Relay-Server"), ("API Server", "API-Server"), ("invalid_http", "Muss mit http:// oder https:// beginnen"), ("Invalid IP", "Ungültige IP-Adresse"), @@ -127,15 +127,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Insert Lock", "Win+L (Sperren) senden"), ("Refresh", "Aktualisieren"), ("ID does not exist", "Diese ID existiert nicht."), - ("Failed to connect to rendezvous server", "Verbindung zum Vermittlungsserver fehlgeschlagen"), + ("Failed to connect to rendezvous server", "Verbindung zum Rendezvous-Server fehlgeschlagen"), ("Please try later", "Bitte versuchen Sie es später erneut."), - ("Remote desktop is offline", "Entfernter PC ist offline."), + ("Remote desktop is offline", "Entfernter Desktop ist offline."), ("Key mismatch", "Schlüssel stimmen nicht überein."), ("Timeout", "Zeitüberschreitung"), - ("Failed to connect to relay server", "Verbindung zum Vermittlungsserver fehlgeschlagen"), - ("Failed to connect via rendezvous server", "Verbindung über Vermittlungsserver ist fehlgeschlagen"), + ("Failed to connect to relay server", "Verbindung zum Relay-Server ist fehlgeschlagen"), + ("Failed to connect via rendezvous server", "Verbindung über Rendezvous-Server ist fehlgeschlagen"), ("Failed to connect via relay server", "Verbindung über Relay-Server ist fehlgeschlagen"), - ("Failed to make direct connection to remote desktop", "Direkte Verbindung zum entfernten PC fehlgeschlagen"), + ("Failed to make direct connection to remote desktop", "Direkte Verbindung zum entfernten Desktop ist fehlgeschlagen"), ("Set Password", "Passwort festlegen"), ("OS Password", "Betriebssystem-Passwort"), ("install_tip", "Aufgrund der Benutzerkontensteuerung (UAC) kann RustDesk in manchen Fällen nicht ordnungsgemäß funktionieren. Um die Benutzerkontensteuerung zu umgehen, klicken Sie bitte auf die Schaltfläche unten und installieren RustDesk auf dem System."), @@ -423,13 +423,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request Elevation", "Erhöhte Rechte anfordern"), ("wait_accept_uac_tip", "Bitte warten Sie, bis der entfernte Benutzer den UAC-Dialog akzeptiert hat."), ("Elevate successfully", "Erhöhung der Rechte erfolgreich"), - ("uppercase", ""), - ("lowercase", ""), - ("digit", ""), - ("special character", ""), - ("length>=8", ""), - ("Weak", ""), - ("Medium", ""), - ("Strong", ""), + ("uppercase", "Großbuchstaben"), + ("lowercase", "Kleinbuchstaben"), + ("digit", "Ziffern"), + ("special character", "Sonderzeichen"), + ("length>=8", "Länge ≥ 8"), + ("Weak", "Schwach"), + ("Medium", "Mittel"), + ("Strong", "Stark"), ].iter().cloned().collect(); } From edb6e307ec017ada94cf0a820d593f64783ad7d8 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Mon, 16 Jan 2023 13:26:42 -0700 Subject: [PATCH 1507/2015] add spaces --- .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 b7cb1bb32..d27b151b3 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -15,12 +15,12 @@ env: # for multiarch gcc compatibility VCPKG_COMMIT_ID: "14e7bb4ae24616ec54ff6b2f6ef4e8659434ea44" VERSION: "1.2.0" - #To make a custom build with your own servers set the below secret values + # To make a custom build with your own servers set the below secret values RS_PUB_KEY_VAL: '${{ secrets.RS_PUB_KEY_VAL }}' RENDEZVOUS_SERVER1: '${{ secrets.RENDEZVOUS_SERVER1 }}' RENDEZVOUS_SERVER2: '${{ secrets.RENDEZVOUS_SERVER2 }}' RENDEZVOUS_SERVER3: '${{ secrets.RENDEZVOUS_SERVER3 }}' - #ignore signing with key files if values below are set + # Ignore signing with key files if values below are set NO_OSX_KEYS: ${{ secrets.NO_OSX_KEYS == 'true' }} NO_APP_KEYS: ${{ secrets.NO_APP_KEYS == 'true' }} From 0173f79ecf05e9d6cb8e0cdcdd7a64ff9f384d4e Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Mon, 16 Jan 2023 13:30:54 -0700 Subject: [PATCH 1508/2015] Test original check --- src/ui_interface.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/ui_interface.rs b/src/ui_interface.rs index f45216d0c..9984198b8 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -243,11 +243,7 @@ pub fn set_peer_option(id: String, name: String, value: String) { #[inline] pub fn using_public_server() -> bool { - if hbb_common::config::RS_PUB_KEY == "OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw=" { - return true - } else { - return false - } + crate::get_custom_rendezvous_server(get_option_("custom-rendezvous-server")).is_empty() } #[inline] From c157a5b1304c87c9d33f2f0894cdd7c3549cc96e Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Mon, 16 Jan 2023 13:45:43 -0700 Subject: [PATCH 1509/2015] Change from NO_XYZ_KEYS to SKIP_ --- .github/workflows/flutter-nightly.yml | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index d27b151b3..bf092e5a7 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -21,8 +21,8 @@ env: RENDEZVOUS_SERVER2: '${{ secrets.RENDEZVOUS_SERVER2 }}' RENDEZVOUS_SERVER3: '${{ secrets.RENDEZVOUS_SERVER3 }}' # Ignore signing with key files if values below are set - NO_OSX_KEYS: ${{ secrets.NO_OSX_KEYS == 'true' }} - NO_APP_KEYS: ${{ secrets.NO_APP_KEYS == 'true' }} + SKIP_OSX_KEYS: ${{ secrets.SKIP_OSX_KEYS == 'true' }} + SKIP_APP_KEYS: ${{ secrets.SKIP_APP_KEYS == 'true' }} jobs: build-for-windows: @@ -158,7 +158,7 @@ jobs: uses: actions/checkout@v3 - name: Import the codesign cert - if: ${{ env.NO_OSX_KEYS== 'true' }} + if: ${{ env.SKIP_OSX_KEYS== 'true' }} uses: apple-actions/import-codesign-certs@v1 with: p12-file-base64: ${{ secrets.MACOS_P12_BASE64 }} @@ -166,13 +166,13 @@ jobs: keychain: rustdesk - name: Check sign and import sign key - if: ${{ env.NO_OSX_KEYS== 'true' }} + if: ${{ env.SKIP_OSX_KEYS== 'true' }} run: | security default-keychain -s rustdesk.keychain security find-identity -v - name: Import notarize key - if: ${{ env.NO_OSX_KEYS== 'true' }} + if: ${{ env.SKIP_OSX_KEYS== 'true' }} uses: timheuer/base64-to-file@v1.2 with: # https://gregoryszorc.com/docs/apple-codesign/stable/apple_codesign_rcodesign.html#notarizing-and-stapling @@ -181,7 +181,7 @@ jobs: encodedString: ${{ secrets.MACOS_NOTARIZE_JSON }} - name: Install rcodesign tool - if: ${{ env.NO_OSX_KEYS== 'true' }} + if: ${{ env.SKIP_OSX_KEYS== 'true' }} shell: bash run: | pushd /tmp @@ -252,7 +252,7 @@ jobs: ./build.py --flutter ${{ matrix.job.extra-build-args }} - name: Codesign app and create signed dmg - if: ${{ env.NO_OSX_KEYS== 'true' }} + if: ${{ env.SKIP_OSX_KEYS== 'true' }} run: | security default-keychain -s rustdesk.keychain security unlock-keychain -p ${{ secrets.MACOS_P12_PASSWORD }} rustdesk.keychain @@ -565,7 +565,7 @@ jobs: - uses: r0adkll/sign-android-release@v1 name: Sign app APK - if: ${{ env.NO_APP_KEYS== 'true' }} + if: ${{ env.SKIP_APP_KEYS== 'true' }} id: sign-rustdesk with: releaseDirectory: ./signed-apk @@ -578,14 +578,14 @@ jobs: BUILD_TOOLS_VERSION: "30.0.2" - name: Upload Artifacts - if: ${{ env.NO_APP_KEYS== 'true' }} + if: ${{ env.SKIP_APP_KEYS== 'true' }} uses: actions/upload-artifact@master with: name: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release-signed.apk path: ${{steps.sign-rustdesk.outputs.signedReleaseFile}} - name: Publish signed apk package - if: ${{ env.NO_APP_KEYS== 'true' }} + if: ${{ env.SKIP_APP_KEYS== 'true' }} uses: softprops/action-gh-release@v1 with: prerelease: true @@ -594,7 +594,7 @@ jobs: ${{steps.sign-rustdesk.outputs.signedReleaseFile}} - name: Publish unsigned apk package - if: ${{ env.NO_APP_KEYS!= 'true' }} + if: ${{ env.SKIP_APP_KEYS!= 'true' }} uses: softprops/action-gh-release@v1 with: prerelease: true From 233305ee0181ee843d3ca54ae4ac9a08cd26785e Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Mon, 16 Jan 2023 13:52:33 -0700 Subject: [PATCH 1510/2015] used the key files to check --- .github/workflows/flutter-nightly.yml | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index bf092e5a7..112cb53ad 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -20,9 +20,6 @@ env: RENDEZVOUS_SERVER1: '${{ secrets.RENDEZVOUS_SERVER1 }}' RENDEZVOUS_SERVER2: '${{ secrets.RENDEZVOUS_SERVER2 }}' RENDEZVOUS_SERVER3: '${{ secrets.RENDEZVOUS_SERVER3 }}' - # Ignore signing with key files if values below are set - SKIP_OSX_KEYS: ${{ secrets.SKIP_OSX_KEYS == 'true' }} - SKIP_APP_KEYS: ${{ secrets.SKIP_APP_KEYS == 'true' }} jobs: build-for-windows: @@ -158,7 +155,7 @@ jobs: uses: actions/checkout@v3 - name: Import the codesign cert - if: ${{ env.SKIP_OSX_KEYS== 'true' }} + if: ${{ env.MACOS_P12_BASE64== 'true' }} uses: apple-actions/import-codesign-certs@v1 with: p12-file-base64: ${{ secrets.MACOS_P12_BASE64 }} @@ -166,13 +163,13 @@ jobs: keychain: rustdesk - name: Check sign and import sign key - if: ${{ env.SKIP_OSX_KEYS== 'true' }} + if: ${{ env.MACOS_P12_BASE64== 'true' }} run: | security default-keychain -s rustdesk.keychain security find-identity -v - name: Import notarize key - if: ${{ env.SKIP_OSX_KEYS== 'true' }} + if: ${{ env.MACOS_P12_BASE64== 'true' }} uses: timheuer/base64-to-file@v1.2 with: # https://gregoryszorc.com/docs/apple-codesign/stable/apple_codesign_rcodesign.html#notarizing-and-stapling @@ -181,7 +178,7 @@ jobs: encodedString: ${{ secrets.MACOS_NOTARIZE_JSON }} - name: Install rcodesign tool - if: ${{ env.SKIP_OSX_KEYS== 'true' }} + if: ${{ env.MACOS_P12_BASE64== 'true' }} shell: bash run: | pushd /tmp @@ -252,7 +249,7 @@ jobs: ./build.py --flutter ${{ matrix.job.extra-build-args }} - name: Codesign app and create signed dmg - if: ${{ env.SKIP_OSX_KEYS== 'true' }} + if: ${{ env.MACOS_P12_BASE64== 'true' }} run: | security default-keychain -s rustdesk.keychain security unlock-keychain -p ${{ secrets.MACOS_P12_PASSWORD }} rustdesk.keychain @@ -565,7 +562,7 @@ jobs: - uses: r0adkll/sign-android-release@v1 name: Sign app APK - if: ${{ env.SKIP_APP_KEYS== 'true' }} + if: ${{ env.ANDROID_SIGNING_KEY== 'true' }} id: sign-rustdesk with: releaseDirectory: ./signed-apk @@ -578,14 +575,14 @@ jobs: BUILD_TOOLS_VERSION: "30.0.2" - name: Upload Artifacts - if: ${{ env.SKIP_APP_KEYS== 'true' }} + if: ${{ env.ANDROID_SIGNING_KEY== 'true' }} uses: actions/upload-artifact@master with: name: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release-signed.apk path: ${{steps.sign-rustdesk.outputs.signedReleaseFile}} - name: Publish signed apk package - if: ${{ env.SKIP_APP_KEYS== 'true' }} + if: ${{ env.ANDROID_SIGNING_KEY== 'true' }} uses: softprops/action-gh-release@v1 with: prerelease: true @@ -594,7 +591,7 @@ jobs: ${{steps.sign-rustdesk.outputs.signedReleaseFile}} - name: Publish unsigned apk package - if: ${{ env.SKIP_APP_KEYS!= 'true' }} + if: ${{ env.ANDROID_SIGNING_KEY!= 'true' }} uses: softprops/action-gh-release@v1 with: prerelease: true From 9cf81d20320139cb418f0c46c5fa010cf9fb0b15 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Mon, 16 Jan 2023 14:10:04 -0700 Subject: [PATCH 1511/2015] updated ui to check key value --- src/ui_interface.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 9984198b8..5b5d3c218 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -243,7 +243,12 @@ pub fn set_peer_option(id: String, name: String, value: String) { #[inline] pub fn using_public_server() -> bool { + if hbb_common::config::RS_PUB_KEY == "OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw=" { crate::get_custom_rendezvous_server(get_option_("custom-rendezvous-server")).is_empty() + return true + } else { + return false + } } #[inline] From 3cd1d42aa25157f2b8d65066607432c1a62c0c0e Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Mon, 16 Jan 2023 14:16:35 -0700 Subject: [PATCH 1512/2015] remove old line --- src/ui_interface.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 5b5d3c218..f45216d0c 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -244,7 +244,6 @@ pub fn set_peer_option(id: String, name: String, value: String) { #[inline] pub fn using_public_server() -> bool { if hbb_common::config::RS_PUB_KEY == "OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw=" { - crate::get_custom_rendezvous_server(get_option_("custom-rendezvous-server")).is_empty() return true } else { return false From 42d5ca2419ac441c74c68a9ed890964c9fd34532 Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Tue, 17 Jan 2023 13:56:43 +0800 Subject: [PATCH 1513/2015] Delete flutter-custom-build.yml I dislike duplication. --- .github/workflows/flutter-custom-build.yml | 1517 -------------------- 1 file changed, 1517 deletions(-) delete mode 100644 .github/workflows/flutter-custom-build.yml diff --git a/.github/workflows/flutter-custom-build.yml b/.github/workflows/flutter-custom-build.yml deleted file mode 100644 index 7354b6c77..000000000 --- a/.github/workflows/flutter-custom-build.yml +++ /dev/null @@ -1,1517 +0,0 @@ -name: Flutter Custom Build - -on: - workflow_dispatch: - -env: - LLVM_VERSION: "10.0" - # Note: currently 3.0.5 does not support arm64 officially, we use latest stable version first. - FLUTTER_VERSION: "3.0.5" - TAG_NAME: "custom-build" - # vcpkg version: 2022.05.10 - # for multiarch gcc compatibility - VCPKG_COMMIT_ID: "14e7bb4ae24616ec54ff6b2f6ef4e8659434ea44" - VERSION: "1.2.0" - #To make a custom build with your own servers set the below secret values - RS_PUB_KEY_VAL: '${{ secrets.RS_PUB_KEY_VAL }}' - RENDEZVOUS_SERVER1: '${{ secrets.RENDEZVOUS_SERVER1 }}' - RENDEZVOUS_SERVER2: '${{ secrets.RENDEZVOUS_SERVER2 }}' - RENDEZVOUS_SERVER3: '${{ secrets.RENDEZVOUS_SERVER3 }}' - #ignore signing with key files if values below are set - NO_OSX_KEYS: ${{ secrets.NO_OSX_KEYS == 'true' }} - NO_APP_KEYS: ${{ secrets.NO_APP_KEYS == 'true' }} - -jobs: - build-for-windows: - name: ${{ matrix.job.target }} (${{ matrix.job.os }}) - runs-on: ${{ matrix.job.os }} - strategy: - fail-fast: false - matrix: - job: - # - { target: i686-pc-windows-msvc , os: windows-2019 } - # - { target: x86_64-pc-windows-gnu , os: windows-2019 } - - { target: x86_64-pc-windows-msvc, os: windows-2019 } - steps: - - name: Checkout source code - uses: actions/checkout@v3 - - - name: Install LLVM and Clang - uses: KyleMayes/install-llvm-action@v1 - with: - version: ${{ env.LLVM_VERSION }} - - - name: Install flutter - uses: subosito/flutter-action@v2 - with: - channel: "stable" - flutter-version: ${{ env.FLUTTER_VERSION }} - cache: true - - - name: Replace engine with rustdesk custom flutter engine - run: | - flutter doctor -v - flutter precache --windows - Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.0.5-rustdesk.2/windows-x64-flutter-release.zip -OutFile windows-x64-flutter-release.zip - Expand-Archive windows-x64-flutter-release.zip -DestinationPath engine - mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-3.0.5-x64/bin/cache/artifacts/engine/windows-x64-release/ - - - name: Install Rust toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: "1.62" - target: ${{ matrix.job.target }} - override: true - components: rustfmt - profile: minimal # minimal component installation (ie, no documentation) - - - uses: Swatinem/rust-cache@v2 - with: - prefix-key: ${{ matrix.job.os }} - - - name: Install flutter rust bridge deps - run: | - dart pub global activate ffigen --version 5.0.1 - $exists = Test-Path ~/.cargo/bin/flutter_rust_bridge_codegen.exe - Push-Location .. - git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1 - Push-Location flutter_rust_bridge/frb_codegen ; cargo install --path . ; Pop-Location - Pop-Location - Push-Location flutter ; flutter pub get ; Pop-Location - ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart - - - name: Restore from cache and install vcpkg - uses: lukka/run-vcpkg@v7 - with: - setupOnly: true - vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} - - - name: Install vcpkg dependencies - run: | - $VCPKG_ROOT/vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static - shell: bash - - - name: Build rustdesk - run: python3 .\build.py --portable --hwcodec --flutter - - - name: Sign rustdesk files - uses: GermanBluefox/code-sign-action@v7 - with: - certificate: '${{ secrets.WINDOWS_PFX_BASE64 }}' - password: '${{ secrets.WINDOWS_PFX_PASSWORD }}' - certificatesha1: '${{ secrets.WINDOWS_PFX_SHA1_THUMBPRINT }}' - # certificatename: '${{ secrets.CERTNAME }}' - folder: './flutter/build/windows/runner/Release/' - recursive: true - - - name: Build self-extracted executable - shell: bash - run: | - pushd ./libs/portable - python3 ./generate.py -f ../../flutter/build/windows/runner/Release/ -o . -e ../../flutter/build/windows/runner/Release/rustdesk.exe - popd - mkdir -p ./SignOutput - mv ./target/release/rustdesk-portable-packer.exe ./SignOutput/rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}.exe - - # - name: Rename rustdesk - # shell: bash - # run: | - # for name in rustdesk*??-install.exe; do - # mv "$name" ./SignOutput/"${name%%-install.exe}-${{ matrix.job.target }}.exe" - # done - - - name: Sign rustdesk self-extracted file - uses: GermanBluefox/code-sign-action@v7 - with: - certificate: '${{ secrets.WINDOWS_PFX_BASE64 }}' - password: '${{ secrets.WINDOWS_PFX_PASSWORD }}' - certificatesha1: '${{ secrets.WINDOWS_PFX_SHA1_THUMBPRINT }}' - # certificatename: '${{ secrets.WINDOWS_PFX_NAME }}' - folder: './SignOutput' - recursive: false - - - name: Publish Release - uses: softprops/action-gh-release@v1 - with: - prerelease: true - tag_name: ${{ env.TAG_NAME }} - files: | - ./SignOutput/rustdesk-*.exe - - build-for-macOS: - name: ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-args }}] - runs-on: ${{ matrix.job.os }} - strategy: - fail-fast: false - matrix: - job: - - { - target: x86_64-apple-darwin, - os: macos-latest, - extra-build-args: "", - } - steps: - - name: Checkout source code - uses: actions/checkout@v3 - - - name: Import the codesign cert - if: ${{ env.NO_OSX_KEYS== 'true' }} - uses: apple-actions/import-codesign-certs@v1 - with: - p12-file-base64: ${{ secrets.MACOS_P12_BASE64 }} - p12-password: ${{ secrets.MACOS_P12_PASSWORD }} - keychain: rustdesk - - - name: Check sign and import sign key - if: ${{ env.NO_OSX_KEYS== 'true' }} - run: | - security default-keychain -s rustdesk.keychain - security find-identity -v - - - name: Import notarize key - if: ${{ env.NO_OSX_KEYS== 'true' }} - uses: timheuer/base64-to-file@v1.2 - with: - # https://gregoryszorc.com/docs/apple-codesign/stable/apple_codesign_rcodesign.html#notarizing-and-stapling - fileName: rustdesk.json - fileDir: ${{ github.workspace }} - encodedString: ${{ secrets.MACOS_NOTARIZE_JSON }} - - - name: Install rcodesign tool - if: ${{ env.NO_OSX_KEYS== 'true' }} - shell: bash - run: | - pushd /tmp - wget https://github.com/indygreg/apple-platform-rs/releases/download/apple-codesign%2F0.22.0/apple-codesign-0.22.0-macos-universal.tar.gz - tar -zxvf apple-codesign-0.22.0-macos-universal.tar.gz - mv apple-codesign-0.22.0-macos-universal/rcodesign /usr/local/bin - popd - - - name: Install build runtime - run: | - brew install llvm create-dmg nasm yasm cmake gcc wget ninja - - - name: Install flutter - uses: subosito/flutter-action@v2 - with: - channel: "stable" - flutter-version: ${{ env.FLUTTER_VERSION }} - - - name: Install Rust toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - target: ${{ matrix.job.target }} - override: true - profile: minimal # minimal component installation (ie, no documentation) - - - uses: Swatinem/rust-cache@v2 - with: - prefix-key: ${{ matrix.job.os }} - - - name: Install flutter rust bridge deps - shell: bash - run: | - dart pub global activate ffigen --version 5.0.1 - # flutter_rust_bridge - pushd /tmp - wget https://github.com/Kingtous/flutter_rust_bridge/releases/download/1.32.0-rustdesk/flutter_rust_bridge_codegen-x86_64-darwin.tgz - tar -zxvf flutter_rust_bridge_codegen-x86_64-darwin.tgz - mkdir -p ~/.cargo/bin - mv flutter_rust_bridge_codegen ~/.cargo/bin; chmod +x ~/.cargo/bin/flutter_rust_bridge_codegen - popd - pushd flutter && flutter pub get && popd - ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart - - - name: Restore from cache and install vcpkg - uses: lukka/run-vcpkg@v7 - with: - setupOnly: true - vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} - - - name: Install vcpkg dependencies - run: | - $VCPKG_ROOT/vcpkg install libvpx libyuv opus - - - name: Show version information (Rust, cargo, Clang) - shell: bash - run: | - clang --version || true - rustup -V - rustup toolchain list - rustup default - cargo -V - rustc -V - - - name: Build rustdesk - run: | - # --hwcodec not supported on macos yet - ./build.py --flutter ${{ matrix.job.extra-build-args }} - - - name: Codesign app and create signed dmg - if: ${{ env.NO_OSX_KEYS== 'true' }} - run: | - security default-keychain -s rustdesk.keychain - 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 - 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 - # notarize the rustdesk-${{ env.VERSION }}.dmg - rcodesign notary-submit --api-key-path ${{ github.workspace }}/rustdesk.json --staple rustdesk-${{ env.VERSION }}.dmg - - - name: Rename rustdesk - run: | - for name in rustdesk*??.dmg; do - mv "$name" "${name%%.dmg}-${{ matrix.job.target }}.dmg" - done - - - name: Publish DMG package - uses: softprops/action-gh-release@v1 - with: - prerelease: true - tag_name: ${{ env.TAG_NAME }} - files: | - rustdesk*-${{ matrix.job.target }}.dmg - - build-vcpkg-deps-linux: - runs-on: ${{ matrix.job.os }} - strategy: - fail-fast: false - matrix: - job: - # - { arch: armv7, os: ubuntu-20.04 } - - { arch: x86_64, os: ubuntu-20.04 } - - { arch: aarch64, os: ubuntu-20.04 } - steps: - - name: Create vcpkg artifacts folder - run: mkdir -p /opt/artifacts - - - name: Cache Vcpkg - id: cache-vcpkg - uses: actions/cache@v3 - with: - path: /opt/artifacts - key: vcpkg-${{ matrix.job.arch }} - - - uses: Kingtous/run-on-arch-action@amd64-support - name: Run vcpkg install on ${{ matrix.job.arch }} - id: vcpkg - with: - arch: ${{ matrix.job.arch }} - distro: ubuntu18.04 - githubToken: ${{ github.token }} - setup: | - ls -l "/opt/artifacts" - dockerRunArgs: | - --volume "/opt/artifacts:/artifacts" - shell: /bin/bash - install: | - apt update -y - case "${{ matrix.job.arch }}" in - x86_64) - # CMake 3.15+ - apt install -y gpg wget ca-certificates - echo 'deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ bionic main' | tee /etc/apt/sources.list.d/kitware.list >/dev/null - wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/null | gpg --dearmor - | tee /usr/share/keyrings/kitware-archive-keyring.gpg >/dev/null - apt update -y - apt install -y curl zip unzip tar git cmake g++ gcc build-essential pkg-config wget nasm yasm ninja-build libjpeg8-dev - ;; - aarch64|armv7) - apt install -y curl zip unzip tar git cmake g++ gcc build-essential pkg-config wget nasm yasm ninja-build libjpeg8-dev automake libtool - esac - cmake --version - gcc -v - run: | - # disable git safe.directory - git config --global --add safe.directory "*" - case "${{ matrix.job.arch }}" in - x86_64) - export VCPKG_FORCE_SYSTEM_BINARIES=1 - pushd /artifacts - git clone https://github.com/microsoft/vcpkg.git || true - pushd vcpkg - git reset --hard ${{ env.VCPKG_COMMIT_ID }} - ./bootstrap-vcpkg.sh - ./vcpkg install libvpx libyuv opus - ;; - aarch64|armv7) - pushd /artifacts - # libyuv - git clone https://chromium.googlesource.com/libyuv/libyuv || true - pushd libyuv - git pull - mkdir -p build - pushd build - mkdir -p /artifacts/vcpkg/installed - cmake .. -DCMAKE_INSTALL_PREFIX=/artifacts/vcpkg/installed - make -j4 && make install - popd - popd - # libopus, ubuntu 18.04 prebuilt is not be compiled with -fPIC - wget -O opus.tar.gz http://archive.ubuntu.com/ubuntu/pool/main/o/opus/opus_1.1.2.orig.tar.gz - tar -zxvf opus.tar.gz; ls -l - pushd opus-1.1.2 - ./autogen.sh; ./configure --prefix=/artifacts/vcpkg/installed - make -j4; make install - ;; - esac - - name: Upload artifacts - uses: actions/upload-artifact@master - with: - name: vcpkg-artifact-${{ matrix.job.arch }} - path: | - /opt/artifacts/vcpkg/installed - - generate-bridge-linux: - name: generate bridge - runs-on: ${{ matrix.job.os }} - strategy: - fail-fast: false - matrix: - job: - - { - target: x86_64-unknown-linux-gnu, - os: ubuntu-18.04, - extra-build-args: "", - } - steps: - - name: Checkout source code - uses: actions/checkout@v3 - - - name: Install prerequisites - run: | - sudo apt update -y - sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang cmake libclang-dev ninja-build llvm-dev libclang-10-dev llvm-10-dev pkg-config - - - name: Install Rust toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - target: ${{ matrix.job.target }} - override: true - profile: minimal # minimal component installation (ie, no documentation) - - - uses: Swatinem/rust-cache@v2 - with: - prefix-key: bridge-${{ matrix.job.os }} - workspace: "/tmp/flutter_rust_bridge/frb_codegen" - - - name: Cache Bridge - id: cache-bridge - uses: actions/cache@v3 - with: - path: /tmp/flutter_rust_bridge - key: vcpkg-${{ matrix.job.arch }} - - - name: Install flutter - uses: subosito/flutter-action@v2 - with: - channel: "stable" - flutter-version: ${{ env.FLUTTER_VERSION }} - cache: true - - - name: Install ffigen - run: | - dart pub global activate ffigen --version 5.0.1 - - - name: Install flutter rust bridge deps - shell: bash - run: | - pushd /tmp && git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1 || true && popd - pushd /tmp/flutter_rust_bridge/frb_codegen && cargo install --path . && popd - pushd flutter && flutter pub get && popd - - - name: Run flutter rust bridge - run: | - ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart - - - name: Upload Artifact - uses: actions/upload-artifact@master - with: - name: bridge-artifact - path: | - ./src/bridge_generated.rs - ./flutter/lib/generated_bridge.dart - ./flutter/lib/generated_bridge.freezed.dart - - build-rustdesk-android-arm64: - needs: [generate-bridge-linux] - name: build rustdesk android apk ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] - runs-on: ${{ matrix.job.os }} - strategy: - fail-fast: false - matrix: - job: - - { - arch: x86_64, - target: aarch64-linux-android, - os: ubuntu-18.04, - extra-build-features: "", - } - # - { - # arch: x86_64, - # target: armv7-linux-androideabi, - # os: ubuntu-18.04, - # extra-build-features: "", - # } - steps: - - name: Install dependencies - run: | - sudo apt update - sudo apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libvdpau-dev libva-dev libclang-dev llvm-dev libclang-10-dev llvm-10-dev pkg-config tree g++ libc6-dev gcc-multilib g++-multilib openjdk-11-jdk-headless - - name: Checkout source code - uses: actions/checkout@v3 - - name: Install flutter - uses: subosito/flutter-action@v2 - with: - channel: "stable" - flutter-version: ${{ env.FLUTTER_VERSION }} - - uses: nttld/setup-ndk@v1 - id: setup-ndk - with: - ndk-version: r22b - add-to-path: true - - - name: Download deps - shell: bash - run: | - pushd /opt - wget https://github.com/rustdesk/doc.rustdesk.com/releases/download/console/dep.tar.gz - tar xzf dep.tar.gz - - - name: Restore bridge files - uses: actions/download-artifact@master - with: - name: bridge-artifact - path: ./ - - - name: Install Rust toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - override: true - profile: minimal # minimal component installation (ie, no documentation) - - - uses: Swatinem/rust-cache@v2 - with: - prefix-key: rustdesk-lib-cache - key: ${{ matrix.job.target }}-${{ matrix.job.extra-build-features }} - - - name: Disable rust bridge build - run: | - sed -i "s/gen_flutter_rust_bridge();/\/\//g" build.rs - - - name: Build rustdesk lib - env: - ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} - ANDROID_NDK_ROOT: ${{ steps.setup-ndk.outputs.ndk-path }} - VCPKG_ROOT: /opt/vcpkg - run: | - rustup target add ${{ matrix.job.target }} - cargo install cargo-ndk - case ${{ matrix.job.target }} in - aarch64-linux-android) - ./flutter/ndk_arm64.sh - mkdir -p ./flutter/android/app/src/main/jniLibs/arm64-v8a - cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so - ;; - armv7-linux-androideabi) - ./flutter/ndk_arm.sh - mkdir -p ./flutter/android/app/src/main/jniLibs/armeabi-v7a - cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/librustdesk.so - ;; - esac - - - name: Build rustdesk - shell: bash - env: - JAVA_HOME: /usr/lib/jvm/java-11-openjdk-amd64 - run: | - export PATH=/usr/lib/jvm/java-11-openjdk-amd64/bin:$PATH - # download so - pushd flutter - wget -O so.tar.gz https://github.com/rustdesk/doc.rustdesk.com/releases/download/console/so.tar.gz - tar xzvf so.tar.gz - popd - # temporary use debug sign config - sed -i "s/signingConfigs.release/signingConfigs.debug/g" ./flutter/android/app/build.gradle - case ${{ matrix.job.target }} in - aarch64-linux-android) - mkdir -p ./flutter/android/app/src/main/jniLibs/arm64-v8a - cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so - # build flutter - pushd flutter - flutter build apk --release --target-platform android-arm64 --split-per-abi - mv build/app/outputs/flutter-apk/app-arm64-v8a-release.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk - ;; - armv7-linux-androideabi) - mkdir -p ./flutter/android/app/src/main/jniLibs/armeabi-v7a - cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/librustdesk.so - # build flutter - pushd flutter - flutter build apk --release --target-platform android-arm --split-per-abi - mv build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk - ;; - esac - popd - mkdir -p signed-apk; pushd signed-apk - mv ../rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk . - - - uses: r0adkll/sign-android-release@v1 - name: Sign app APK - if: ${{ env.NO_APP_KEYS== 'true' }} - id: sign-rustdesk - with: - releaseDirectory: ./signed-apk - signingKeyBase64: ${{ secrets.ANDROID_SIGNING_KEY }} - alias: ${{ secrets.ANDROID_ALIAS }} - keyStorePassword: ${{ secrets.ANDROID_KEY_STORE_PASSWORD }} - keyPassword: ${{ secrets.ANDROID_KEY_PASSWORD }} - env: - # override default build-tools version (29.0.3) -- optional - BUILD_TOOLS_VERSION: "30.0.2" - - - name: Upload Artifacts - if: ${{ env.NO_APP_KEYS== 'true' }} - uses: actions/upload-artifact@master - with: - name: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release-signed.apk - path: ${{steps.sign-rustdesk.outputs.signedReleaseFile}} - - - name: Publish signed apk package - if: ${{ env.NO_APP_KEYS== 'true' }} - uses: softprops/action-gh-release@v1 - with: - prerelease: true - tag_name: ${{ env.TAG_NAME }} - files: | - ${{steps.sign-rustdesk.outputs.signedReleaseFile}} - - - name: Publish unsigned apk package - if: ${{ env.NO_APP_KEYS!= 'true' }} - uses: softprops/action-gh-release@v1 - with: - prerelease: true - tag_name: ${{ env.TAG_NAME }} - files: | - ../rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk - - build-rustdesk-lib-linux-amd64: - needs: [generate-bridge-linux, build-vcpkg-deps-linux] - name: build-rust-lib ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] - runs-on: ${{ matrix.job.os }} - strategy: - fail-fast: false - matrix: - # use a high level qemu-user-static - job: - # - { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } - # - { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } - - { - arch: x86_64, - target: x86_64-unknown-linux-gnu, - os: ubuntu-20.04, - extra-build-features: "", - } - - { - arch: x86_64, - target: x86_64-unknown-linux-gnu, - os: ubuntu-20.04, - extra-build-features: "flatpak", - } - - { - arch: x86_64, - target: x86_64-unknown-linux-gnu, - os: ubuntu-20.04, - extra-build-features: "appimage", - } - # - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } - steps: - - name: Maximize build space - run: | - sudo rm -rf /opt/ghc - sudo rm -rf /usr/local/lib/android - sudo rm -rf /usr/share/dotnet - sudo apt update -y - sudo apt install qemu-user-static - - - name: Checkout source code - uses: actions/checkout@v3 - - - name: Set Swap Space - uses: pierotofy/set-swap-space@master - with: - swap-size-gb: 12 - - - name: Free Space - run: | - df - - - name: Install Rust toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - target: ${{ matrix.job.target }} - override: true - profile: minimal # minimal component installation (ie, no documentation) - - - uses: Swatinem/rust-cache@v2 - with: - prefix-key: rustdesk-lib-cache - key: ${{ matrix.job.target }}-${{ matrix.job.extra-build-features }} - cache-directories: "/opt/rust-registry" - - - name: Install Rust toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - target: ${{ matrix.job.target }} - override: true - profile: minimal # minimal component installation (ie, no documentation) - - - name: Install local registry - run: | - mkdir -p /opt/rust-registry - cargo install cargo-local-registry - - - name: Build local registry - uses: nick-fields/retry@v2 - id: build-local-registry - continue-on-error: true - with: - max_attempts: 3 - timeout_minutes: 15 - retry_on: error - command: cargo local-registry --sync ./Cargo.lock /opt/rust-registry - - - name: Disable rust bridge build - run: | - sed -i "s/gen_flutter_rust_bridge();/\/\//g" build.rs - # only build cdylib - sed -i "s/\[\"cdylib\", \"staticlib\", \"rlib\"\]/\[\"cdylib\"\]/g" Cargo.toml - - - name: Restore bridge files - uses: actions/download-artifact@master - with: - name: bridge-artifact - path: ./ - - - name: Restore vcpkg files - uses: actions/download-artifact@master - with: - name: vcpkg-artifact-${{ matrix.job.arch }} - path: /opt/artifacts/vcpkg/installed - - - uses: Kingtous/run-on-arch-action@amd64-support - name: Build rustdesk library for ${{ matrix.job.arch }} - id: vcpkg - with: - arch: ${{ matrix.job.arch }} - distro: ubuntu18.04 - # not ready yet - # distro: ubuntu18.04-rustdesk - githubToken: ${{ github.token }} - setup: | - ls -l "${PWD}" - ls -l /opt/artifacts/vcpkg/installed - dockerRunArgs: | - --volume "${PWD}:/workspace" - --volume "/opt/artifacts:/opt/artifacts" - --volume "/opt/rust-registry:/opt/rust-registry" - shell: /bin/bash - install: | - apt update -y - echo -e "installing deps" - apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libvdpau-dev libva-dev libclang-dev llvm-dev libclang-10-dev llvm-10-dev pkg-config tree g++ gcc libvpx-dev tree > /dev/null - # we have libopus compiled by us. - apt remove -y libopus-dev || true - # output devs - ls -l ./ - tree -L 3 /opt/artifacts/vcpkg/installed - run: | - # disable git safe.directory - git config --global --add safe.directory "*" - # rust - pushd /opt - wget -O rust.tar.gz https://static.rust-lang.org/dist/rust-1.64.0-${{ matrix.job.target }}.tar.gz - tar -zxvf rust.tar.gz > /dev/null && rm rust.tar.gz - cd rust-1.64.0-${{ matrix.job.target }} && ./install.sh - rm -rf rust-1.64.0-${{ matrix.job.target }} - # edit config - mkdir -p ~/.cargo/ - echo """ - [source.crates-io] - registry = 'https://github.com/rust-lang/crates.io-index' - replace-with = 'local-registry' - - [source.local-registry] - local-registry = '/opt/rust-registry/' - """ > ~/.cargo/config - cat ~/.cargo/config - # start build - pushd /workspace - # mock - case "${{ matrix.job.arch }}" in - x86_64) - # no need mock on x86_64 - export VCPKG_ROOT=/opt/artifacts/vcpkg - cargo build --lib --features hwcodec,flutter,${{ matrix.job.extra-build-features }} --release - ;; - esac - - - name: Upload Artifacts - uses: actions/upload-artifact@master - with: - name: librustdesk-${{ matrix.job.arch }}-${{ matrix.job.extra-build-features }}.so - path: target/release/liblibrustdesk.so - - build-rustdesk-lib-linux-arm: - needs: [generate-bridge-linux, build-vcpkg-deps-linux] - name: build-rust-lib ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] - runs-on: ${{ matrix.job.os }} - strategy: - fail-fast: false - matrix: - # use a high level qemu-user-static - job: - - { - arch: aarch64, - target: aarch64-unknown-linux-gnu, - os: ubuntu-20.04, - use-cross: true, - extra-build-features: "", - } - - { - arch: aarch64, - target: aarch64-unknown-linux-gnu, - os: ubuntu-18.04, # just for naming package, not running host - use-cross: true, - extra-build-features: "appimage", - } - # - { arch: aarch64, target: aarch64-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true, extra-build-features: "flatpak" } - # - { - # arch: armv7, - # target: arm-unknown-linux-gnueabihf, - # os: ubuntu-20.04, - # use-cross: true, - # extra-build-features: "", - # } - # - { arch: armv7, target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true, extra-build-features: "flatpak" } - # - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true } - steps: - - name: Maximize build space - run: | - sudo rm -rf /opt/ghc - sudo rm -rf /usr/local/lib/android - sudo rm -rf /usr/share/dotnet - sudo apt update -y - sudo apt install qemu-user-static - - - name: Checkout source code - uses: actions/checkout@v3 - - - name: Set Swap Space - uses: pierotofy/set-swap-space@master - with: - swap-size-gb: 12 - - - name: Free Space - run: | - df - - - name: Install Rust toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - target: ${{ matrix.job.target }} - override: true - profile: minimal # minimal component installation (ie, no documentation) - - - name: Install Rust toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - target: ${{ matrix.job.target }} - override: true - profile: minimal # minimal component installation (ie, no documentation) - - - uses: Swatinem/rust-cache@v2 - with: - prefix-key: rustdesk-lib-cache - key: ${{ matrix.job.target }}-${{ matrix.job.extra-build-features }} - cache-directories: "/opt/rust-registry" - - - name: Install local registry - run: | - mkdir -p /opt/rust-registry - cargo install cargo-local-registry - - - name: Build local registry - uses: nick-fields/retry@v2 - id: build-local-registry - continue-on-error: true - with: - max_attempts: 3 - timeout_minutes: 15 - retry_on: error - command: cargo local-registry --sync ./Cargo.lock /opt/rust-registry - - - name: Disable rust bridge build - run: | - sed -i "s/gen_flutter_rust_bridge();/\/\//g" build.rs - # only build cdylib - sed -i "s/\[\"cdylib\", \"staticlib\", \"rlib\"\]/\[\"cdylib\"\]/g" Cargo.toml - - - name: Restore bridge files - uses: actions/download-artifact@master - with: - name: bridge-artifact - path: ./ - - - name: Restore vcpkg files - uses: actions/download-artifact@master - with: - name: vcpkg-artifact-${{ matrix.job.arch }} - path: /opt/artifacts/vcpkg/installed - - - uses: Kingtous/run-on-arch-action@amd64-support - name: Build rustdesk library for ${{ matrix.job.arch }} - id: vcpkg - with: - arch: ${{ matrix.job.arch }} - distro: ubuntu18.04-rustdesk - githubToken: ${{ github.token }} - setup: | - ls -l "${PWD}" - ls -l /opt/artifacts/vcpkg/installed - dockerRunArgs: | - --volume "${PWD}:/workspace" - --volume "/opt/artifacts:/opt/artifacts" - --volume "/opt/rust-registry:/opt/rust-registry" - shell: /bin/bash - install: | - apt update -y - echo -e "installing deps" - apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libvdpau-dev libva-dev libclang-dev llvm-dev libclang-10-dev llvm-10-dev pkg-config tree g++ gcc libvpx-dev tree > /dev/null - # we have libopus compiled by us. - apt remove -y libopus-dev || true - # output devs - ls -l ./ - tree -L 3 /opt/artifacts/vcpkg/installed - run: | - # disable git safe.directory - git config --global --add safe.directory "*" - # rust - pushd /opt - wget -O rust.tar.gz https://static.rust-lang.org/dist/rust-1.64.0-${{ matrix.job.target }}.tar.gz - tar -zxvf rust.tar.gz > /dev/null && rm rust.tar.gz - cd rust-1.64.0-${{ matrix.job.target }} && ./install.sh - rm -rf rust-1.64.0-${{ matrix.job.target }} - # edit config - mkdir -p ~/.cargo/ - echo """ - [source.crates-io] - registry = 'https://github.com/rust-lang/crates.io-index' - replace-with = 'local-registry' - - [source.local-registry] - local-registry = '/opt/rust-registry/' - """ > ~/.cargo/config - cat ~/.cargo/config - # start build - pushd /workspace - # mock - case "${{ matrix.job.arch }}" in - aarch64) - cp -r /opt/artifacts/vcpkg/installed/lib/* /usr/lib/aarch64-linux-gnu/ - cp -r /opt/artifacts/vcpkg/installed/include/* /usr/include/ - ls -l /opt/artifacts/vcpkg/installed/lib/ - mkdir -p /vcpkg/installed/arm64-linux - ln -s /usr/lib/aarch64-linux-gnu /vcpkg/installed/arm64-linux/lib - ln -s /usr/include /vcpkg/installed/arm64-linux/include - export VCPKG_ROOT=/vcpkg - # disable hwcodec for compilation - cargo build --lib --features flutter,${{ matrix.job.extra-build-features }} --release - ;; - armv7) - cp -r /opt/artifacts/vcpkg/installed/lib/* /usr/lib/arm-linux-gnueabihf/ - cp -r /opt/artifacts/vcpkg/installed/include/* /usr/include/ - mkdir -p /vcpkg/installed/arm-linux - ln -s /usr/lib/arm-linux-gnueabihf /vcpkg/installed/arm-linux/lib - ln -s /usr/include /vcpkg/installed/arm-linux/include - export VCPKG_ROOT=/vcpkg - # disable hwcodec for compilation - cargo build --lib --features flutter,${{ matrix.job.extra-build-features }} --release - ;; - esac - - - name: Upload Artifacts - uses: actions/upload-artifact@master - with: - name: librustdesk-${{ matrix.job.arch }}-${{ matrix.job.extra-build-features }}.so - path: target/release/liblibrustdesk.so - - build-rustdesk-linux-arm: - needs: [build-rustdesk-lib-linux-arm] - name: build-rustdesk ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] - runs-on: ubuntu-20.04 # 20.04 has more performance on arm build - strategy: - fail-fast: false - matrix: - job: - - { - arch: aarch64, - target: aarch64-unknown-linux-gnu, - os: ubuntu-18.04, # just for naming package, not running host - use-cross: true, - extra-build-features: "", - } - - { - arch: aarch64, - target: aarch64-unknown-linux-gnu, - os: ubuntu-18.04, # just for naming package, not running host - use-cross: true, - extra-build-features: "appimage", - } - # - { - # arch: aarch64, - # target: aarch64-unknown-linux-gnu, - # os: ubuntu-18.04, # just for naming package, not running host - # use-cross: true, - # extra-build-features: "flatpak", - # } - # - { arch: armv7, target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true, extra-build-features: "" } - # - { arch: armv7, target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true, extra-build-features: "flatpak" } - # - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true } - steps: - - name: Checkout source code - uses: actions/checkout@v3 - - - name: Restore bridge files - uses: actions/download-artifact@master - with: - name: bridge-artifact - path: ./ - - - name: Prepare env - run: | - sudo apt update -y - sudo apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev libarchive-tools - mkdir -p ./target/release/ - - - name: Restore the rustdesk lib file - uses: actions/download-artifact@master - with: - name: librustdesk-${{ matrix.job.arch }}-${{ matrix.job.extra-build-features }}.so - path: ./target/release/ - - - name: Download Flutter - shell: bash - run: | - # disable git safe.directory - git config --global --add safe.directory "*" - pushd /opt - # clone repo and reset to flutter 3.0.5 - git clone https://github.com/sony/flutter-elinux.git || true - pushd flutter-elinux - # reset to flutter 3.0.5 - git fetch - git reset --hard b09a90eee643859ce4e676839227edd9fd3feba8 - popd - - - uses: Kingtous/run-on-arch-action@amd64-support - name: Build rustdesk binary for ${{ matrix.job.arch }} - id: vcpkg - with: - arch: ${{ matrix.job.arch }} - distro: ubuntu18.04-rustdesk - githubToken: ${{ github.token }} - setup: | - ls -l "${PWD}" - dockerRunArgs: | - --volume "${PWD}:/workspace" - --volume "/opt/artifacts:/opt/artifacts" - --volume "/opt/flutter-elinux:/opt/flutter-elinux" - shell: /bin/bash - install: | - apt update -y - apt-get -qq install -y git cmake g++ gcc build-essential nasm yasm curl unzip xz-utils python3 wget pkg-config ninja-build pkg-config libgtk-3-dev liblzma-dev clang libappindicator3-dev rpm - run: | - # disable git safe.directory - git config --global --add safe.directory "*" - pushd /workspace - # we use flutter-elinux to build our rustdesk - sed -i "s/flutter build linux --release/flutter-elinux build linux/g" ./build.py - # Setup flutter-elinux - export PATH=/opt/flutter-elinux/bin:$PATH - flutter-elinux doctor -v - # edit to corresponding arch - case ${{ matrix.job.arch }} in - aarch64) - sed -i "s/Architecture: amd64/Architecture: arm64/g" ./build.py - sed -i "s/x64\/release/arm64\/release/g" ./build.py - ;; - armv7) - sed -i "s/Architecture: amd64/Architecture: arm/g" ./build.py - sed -i "s/x64\/release/arm\/release/g" ./build.py - ;; - esac - python3 ./build.py --flutter --hwcodec --skip-cargo - # rpm package - echo -e "start packaging" - pushd /workspace - case ${{ matrix.job.arch }} in - armv7) - sed -i "s/64bit/32bit/g" ./res/rpm-flutter.spec - sed -i "s/linux\/x64/linux\/arm/g" ./res/rpm-flutter.spec - ;; - aarch64) - sed -i "s/linux\/x64/linux\/arm64/g" ./res/rpm-flutter.spec - ;; - esac - HBB=`pwd` rpmbuild ./res/rpm-flutter.spec -bb - pushd ~/rpmbuild/RPMS/${{ matrix.job.arch }} - mkdir -p /opt/artifacts/rpm - for name in rustdesk*??.rpm; do - mv "$name" "/opt/artifacts/rpm/${name%%.rpm}-fedora28-centos8.rpm" - done - - - name: Rename rustdesk - shell: bash - run: | - for name in rustdesk*??.deb; do - cp "$name" "${name%%.deb}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb" - done - - - name: Publish debian package - if: ${{ matrix.job.extra-build-features == '' }} - uses: softprops/action-gh-release@v1 - with: - prerelease: true - tag_name: ${{ env.TAG_NAME }} - files: | - rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb - - - name: Build appimage package - if: ${{ matrix.job.extra-build-features == 'appimage' }} - shell: bash - run: | - # set-up appimage-builder - pushd /tmp - wget -O appimage-builder-x86_64.AppImage https://github.com/AppImageCrafters/appimage-builder/releases/download/v1.1.0/appimage-builder-1.1.0-x86_64.AppImage - chmod +x appimage-builder-x86_64.AppImage - sudo mv appimage-builder-x86_64.AppImage /usr/local/bin/appimage-builder - popd - # run appimage-builder - pushd appimage - sudo appimage-builder --skip-tests --recipe ./AppImageBuilder-${{ matrix.job.arch }}.yml - - - name: Publish appimage package - if: ${{ matrix.job.extra-build-features == 'appimage' }} - uses: softprops/action-gh-release@v1 - with: - prerelease: true - tag_name: ${{ env.TAG_NAME }} - files: | - ./appimage/rustdesk-${{ env.VERSION }}-*.AppImage - - - name: Upload Artifact - uses: actions/upload-artifact@master - if: ${{ contains(matrix.job.extra-build-features, 'flatpak') }} - with: - name: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb - path: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb - - - name: Patch archlinux PKGBUILD - if: ${{ matrix.job.extra-build-features == '' }} - run: | - sed -i "s/arch=('x86_64')/arch=('${{ matrix.job.arch }}')/g" res/PKGBUILD - case ${{ matrix.job.arch }} in - armv7) - sed -i "s/linux\/x64/linux\/arm/g" ./res/PKGBUILD - ;; - aarch64) - sed -i "s/linux\/x64/linux\/arm64/g" ./res/PKGBUILD - ;; - esac - - # Temporary disable for there is no many archlinux arm hosts - # - name: Build archlinux package - # if: ${{ matrix.job.extra-build-features == '' }} - # uses: vufa/arch-makepkg-action@master - # with: - # packages: > - # llvm - # clang - # libva - # libvdpau - # rust - # gstreamer - # unzip - # git - # cmake - # gcc - # curl - # wget - # yasm - # nasm - # zip - # make - # pkg-config - # clang - # gtk3 - # xdotool - # libxcb - # libxfixes - # alsa-lib - # pipewire - # python - # ttf-arphic-uming - # libappindicator-gtk3 - # scripts: | - # cd res && HBB=`pwd`/.. FLUTTER=1 makepkg -f - - # - name: Publish archlinux package - # if: ${{ matrix.job.extra-build-features == '' }} - # uses: softprops/action-gh-release@v1 - # with: - # prerelease: true - # tag_name: ${{ env.TAG_NAME }} - # files: | - # res/rustdesk*.zst - - - name: Publish fedora28/centos8 package - if: ${{ matrix.job.extra-build-features == '' }} - uses: softprops/action-gh-release@v1 - with: - prerelease: true - tag_name: ${{ env.TAG_NAME }} - files: | - /opt/artifacts/rpm/*.rpm - - build-rustdesk-linux-amd64: - needs: [build-rustdesk-lib-linux-amd64] - name: build-rustdesk ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] - runs-on: ubuntu-20.04 - strategy: - fail-fast: false - matrix: - job: - # - { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } - # - { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } - - { - arch: x86_64, - target: x86_64-unknown-linux-gnu, - os: ubuntu-18.04, - extra-build-features: "", - } - - { - arch: x86_64, - target: x86_64-unknown-linux-gnu, - os: ubuntu-18.04, - extra-build-features: "flatpak", - } - - { - arch: x86_64, - target: x86_64-unknown-linux-gnu, - os: ubuntu-18.04, - extra-build-features: "appimage", - } - # - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } - steps: - - name: Checkout source code - uses: actions/checkout@v3 - - - name: Restore bridge files - uses: actions/download-artifact@master - with: - name: bridge-artifact - path: ./ - - - name: Prepare env - run: | - sudo apt update -y - sudo apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev libarchive-tools - mkdir -p ./target/release/ - - - name: Restore the rustdesk lib file - uses: actions/download-artifact@master - with: - name: librustdesk-${{ matrix.job.arch }}-${{ matrix.job.extra-build-features }}.so - path: ./target/release/ - - - uses: Kingtous/run-on-arch-action@amd64-support - name: Build rustdesk binary for ${{ matrix.job.arch }} - id: vcpkg - with: - arch: ${{ matrix.job.arch }} - distro: ubuntu18.04 - githubToken: ${{ github.token }} - setup: | - ls -l "${PWD}" - dockerRunArgs: | - --volume "${PWD}:/workspace" - --volume "/opt/artifacts:/opt/artifacts" - shell: /bin/bash - install: | - apt update -y - apt-get -qq install -y git cmake g++ gcc build-essential nasm yasm curl unzip xz-utils python3 wget pkg-config ninja-build pkg-config libgtk-3-dev liblzma-dev clang libappindicator3-dev rpm - run: | - # disable git safe.directory - git config --global --add safe.directory "*" - # Setup Flutter - pushd /opt - wget https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_${{ env.FLUTTER_VERSION }}-stable.tar.xz - tar xf flutter_linux_${{ env.FLUTTER_VERSION }}-stable.tar.xz - ls -l . - export PATH=/opt/flutter/bin:$PATH - flutter doctor -v - pushd /workspace - python3 ./build.py --flutter --hwcodec --skip-cargo - # rpm package - pushd /workspace - case ${{ matrix.job.arch }} in - armv7) - sed -i "s/64bit/32bit/g" ./res/rpm-flutter.spec - ;; - esac - HBB=`pwd` rpmbuild ./res/rpm-flutter.spec -bb - pushd ~/rpmbuild/RPMS/${{ matrix.job.arch }} - mkdir -p /opt/artifacts/rpm - for name in rustdesk*??.rpm; do - mv "$name" "/opt/artifacts/rpm/${name%%.rpm}-fedora28-centos8.rpm" - done - - - name: Rename rustdesk - shell: bash - run: | - for name in rustdesk*??.deb; do - # use cp to duplicate deb files to fit other packages. - cp "$name" "${name%%.deb}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb" - done - - - name: Publish debian package - if: ${{ matrix.job.extra-build-features == '' }} - uses: softprops/action-gh-release@v1 - with: - prerelease: true - tag_name: ${{ env.TAG_NAME }} - files: | - rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb - - - name: Upload Artifact - uses: actions/upload-artifact@master - if: ${{ contains(matrix.job.extra-build-features, 'flatpak') }} - with: - name: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb - path: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb - - - name: Patch archlinux PKGBUILD - if: ${{ matrix.job.extra-build-features == '' }} - run: | - sed -i "s/arch=('x86_64')/arch=('${{ matrix.job.arch }}')/g" res/PKGBUILD - - - name: Build archlinux package - if: ${{ matrix.job.extra-build-features == '' }} - uses: vufa/arch-makepkg-action@master - with: - packages: > - llvm - clang - libva - libvdpau - rust - gstreamer - unzip - git - cmake - gcc - curl - wget - yasm - nasm - zip - make - pkg-config - clang - gtk3 - xdotool - libxcb - libxfixes - alsa-lib - pipewire - python - ttf-arphic-uming - libappindicator-gtk3 - scripts: | - cd res && HBB=`pwd`/.. FLUTTER=1 makepkg -f - - - name: Publish archlinux package - if: ${{ matrix.job.extra-build-features == '' }} - uses: softprops/action-gh-release@v1 - with: - prerelease: true - tag_name: ${{ env.TAG_NAME }} - files: | - res/rustdesk*.zst - - - name: Build appimage package - if: ${{ matrix.job.extra-build-features == 'appimage' }} - shell: bash - run: | - # set-up appimage-builder - pushd /tmp - wget -O appimage-builder-x86_64.AppImage https://github.com/AppImageCrafters/appimage-builder/releases/download/v1.1.0/appimage-builder-1.1.0-x86_64.AppImage - chmod +x appimage-builder-x86_64.AppImage - sudo mv appimage-builder-x86_64.AppImage /usr/local/bin/appimage-builder - popd - # run appimage-builder - pushd appimage - sudo appimage-builder --skip-tests --recipe ./AppImageBuilder-x86_64.yml - - - name: Publish appimage package - if: ${{ matrix.job.extra-build-features == 'appimage' }} - uses: softprops/action-gh-release@v1 - with: - prerelease: true - tag_name: ${{ env.TAG_NAME }} - files: | - ./appimage/rustdesk-${{ env.VERSION }}-*.AppImage - - - name: Publish fedora28/centos8 package - if: ${{ matrix.job.extra-build-features == '' }} - uses: softprops/action-gh-release@v1 - with: - prerelease: true - tag_name: ${{ env.TAG_NAME }} - files: | - /opt/artifacts/rpm/*.rpm - - # Temporary disable flatpak arm build - # - # build-flatpak-arm: - # name: Build Flatpak - # needs: [build-rustdesk-linux-arm] - # runs-on: ${{ matrix.job.os }} - # strategy: - # fail-fast: false - # matrix: - # job: - # # - { target: aarch64-unknown-linux-gnu , os: ubuntu-18.04, arch: arm64 } - # - { target: aarch64-unknown-linux-gnu, os: ubuntu-20.04, arch: arm64 } - # steps: - # - name: Checkout source code - # uses: actions/checkout@v3 - - # - name: Download Binary - # uses: actions/download-artifact@master - # with: - # name: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb - # path: . - - # - name: Rename Binary - # run: | - # mv rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb rustdesk-${{ env.VERSION }}.deb - - # - uses: Kingtous/run-on-arch-action@amd64-support - # name: Build rustdesk flatpak package for ${{ matrix.job.arch }} - # id: rpm - # with: - # arch: ${{ matrix.job.arch }} - # distro: ubuntu18.04 - # githubToken: ${{ github.token }} - # setup: | - # ls -l "${PWD}" - # dockerRunArgs: | - # --volume "${PWD}:/workspace" - # shell: /bin/bash - # install: | - # apt update -y - # apt install -y rpm - # run: | - # pushd /workspace - # # install - # apt update -y - # apt install -y flatpak flatpak-builder cmake g++ gcc git curl wget nasm yasm libgtk-3-dev git - # # flatpak deps - # flatpak --user remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo - # flatpak --user install -y flathub org.freedesktop.Platform/${{ matrix.job.arch }}/21.08 - # flatpak --user install -y flathub org.freedesktop.Sdk/${{ matrix.job.arch }}/21.08 - # # package - # pushd flatpak - # git clone https://github.com/flathub/shared-modules.git --depth=1 - # flatpak-builder --user --force-clean --repo=repo ./build ./rustdesk.json - # flatpak build-bundle ./repo rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}.flatpak org.rustdesk.rustdesk - - # - name: Publish flatpak package - # uses: softprops/action-gh-release@v1 - # with: - # prerelease: true - # tag_name: ${{ env.TAG_NAME }} - # files: | - # flatpak/rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}.flatpak - - build-flatpak-amd64: - name: Build Flatpak - needs: [build-rustdesk-linux-amd64] - runs-on: ubuntu-20.04 - strategy: - fail-fast: false - matrix: - job: - - { target: x86_64-unknown-linux-gnu, os: ubuntu-18.04, arch: x86_64 } - steps: - - name: Checkout source code - uses: actions/checkout@v3 - - - name: Download Binary - uses: actions/download-artifact@master - with: - name: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb - path: . - - - name: Rename Binary - run: | - mv rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb rustdesk-${{ env.VERSION }}.deb - - - uses: Kingtous/run-on-arch-action@amd64-support - name: Build rustdesk flatpak package for ${{ matrix.job.arch }} - id: rpm - with: - arch: ${{ matrix.job.arch }} - distro: ubuntu18.04 - githubToken: ${{ github.token }} - setup: | - ls -l "${PWD}" - dockerRunArgs: | - --volume "${PWD}:/workspace" - shell: /bin/bash - install: | - apt update -y - apt install -y rpm git wget curl - run: | - # disable git safe.directory - git config --global --add safe.directory "*" - pushd /workspace - # install - apt update -y - apt install -y flatpak flatpak-builder cmake g++ gcc git curl wget nasm yasm libgtk-3-dev git - # flatpak deps - flatpak --user remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo - flatpak --user install -y flathub org.freedesktop.Platform/${{ matrix.job.arch }}/21.08 - flatpak --user install -y flathub org.freedesktop.Sdk/${{ matrix.job.arch }}/21.08 - # package - pushd flatpak - git clone https://github.com/flathub/shared-modules.git --depth=1 - flatpak-builder --user --force-clean --repo=repo ./build ./rustdesk.json - flatpak build-bundle ./repo rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}.flatpak org.rustdesk.rustdesk - - - name: Publish flatpak package - uses: softprops/action-gh-release@v1 - with: - prerelease: true - tag_name: ${{ env.TAG_NAME }} - files: | - flatpak/rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}.flatpak From 7374a924737ec1e86c8249706634b85b00c51479 Mon Sep 17 00:00:00 2001 From: Manos G <87467035+7th-fret@users.noreply.github.com> Date: Tue, 17 Jan 2023 10:49:35 +0200 Subject: [PATCH 1514/2015] Update gr.rs --- src/lang/gr.rs | 50 +++++++++++++++++++++++++------------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/src/lang/gr.rs b/src/lang/gr.rs index 81a50bcd8..a907de84b 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -403,33 +403,33 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", "Να επιτρέπεται η απόκρυψη, μόνο εάν αποδέχεστε συνδέσεις μέσω κωδικού πρόσβασης και χρησιμοποιείτε μόνιμο κωδικό πρόσβασης"), ("wayland_experiment_tip", "Η υποστήριξη Wayland βρίσκεται σε πειραματικό στάδιο, χρησιμοποιήστε το X11 εάν χρειάζεστε πρόσβαση χωρίς επίβλεψη."), ("Right click to select tabs", "Κάντε δεξί κλικ για να επιλέξετε καρτέλες"), - ("Skipped", ""), + ("Skipped", "Παράλειψη"), ("Add to Address Book", "Προσθήκη στο Βιβλίο Διευθύνσεων"), ("Group", "Ομάδα"), ("Search", "Αναζήτηση"), - ("Closed manually by the web console", ""), - ("Local keyboard type", ""), - ("Select local keyboard type", ""), - ("software_render_tip", ""), - ("Always use software rendering", ""), - ("config_input", ""), - ("request_elevation_tip", ""), - ("Wait", ""), - ("Elevation Error", ""), - ("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", ""), - ("Request Elevation", ""), - ("wait_accept_uac_tip", ""), - ("Elevate successfully", ""), - ("uppercase", ""), - ("lowercase", ""), - ("digit", ""), - ("special character", ""), - ("length>=8", ""), - ("Weak", ""), - ("Medium", ""), - ("Strong", ""), + ("Closed manually by the web console", "Κλειστό χειροκίνητα από την κονσόλα web"), + ("Local keyboard type", "Τύπος τοπικού πληκτρολογίου"), + ("Select local keyboard type", "Επιλογή τύπου τοπικού πληκτρολογίου"), + ("software_render_tip", "Εάν έχετε κάρτα γραφικών Nvidia και το παράθυρο σύνδεσης κλείνει αμέσως μετά τη σύνδεση, η εγκατάσταση του προγράμματος οδήγησης nouveau και η επιλογή χρήσης της επιτάχυνσης λογισμικού μπορεί να βοηθήσει. Απαιτείται επανεκκίνηση."), + ("Always use software rendering", "Να χρησιμοποιείται πάντα επιτάχυνση λογισμικού"), + ("config_input", "Για να ελέγξετε την απομακρυσμένη επιφάνεια εργασίας με πληκτρολόγιο, πρέπει να εκχωρήσετε δικαιώματα στο RustDesk"), + ("request_elevation_tip", "αίτημα ανύψωσης δικαιωμάτων χρήστη"), + ("Wait", "Περιμένετε"), + ("Elevation Error", "Σφάλμα ανύψωσης δικαιωμάτων χρήστη"), + ("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", "Εξακολουθεί να απαιτεί από τον απομακρυσμένο χρήστη να κάνει κλικ στο OK στο παράθυρο UAC όπου εκτελείται το RustDesk."), + ("Request Elevation", "Αίτημα ανύψωσης δικαιωμάτων χρήστη"), + ("wait_accept_uac_tip", "Περιμένετε να αποδεχτεί ο απομακρυσμένος χρήστης το παράθυρο διαλόγου UAC."), + ("Elevate successfully", "Επιτυχής ανύψωση δικαιωμάτων χρήστη"), + ("uppercase", "κεφαλαία γράμματα"), + ("lowercase", "πεζά γράμματα"), + ("digit", "ψηφίο"), + ("special character", "ειδικός χαρακτήρας"), + ("length>=8", "μήκος>=8"), + ("Weak", "Αδύναμο"), + ("Medium", "Μέτριο"), + ("Strong", "Δυνατό"), ].iter().cloned().collect(); } From 8d1f4a5f78d28085c150f06445f10e6bace9aee2 Mon Sep 17 00:00:00 2001 From: Manos G <87467035+7th-fret@users.noreply.github.com> Date: Tue, 17 Jan 2023 13:29:22 +0200 Subject: [PATCH 1515/2015] Update gr.rs --- src/lang/gr.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/gr.rs b/src/lang/gr.rs index a907de84b..91607877c 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -410,8 +410,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the web console", "Κλειστό χειροκίνητα από την κονσόλα web"), ("Local keyboard type", "Τύπος τοπικού πληκτρολογίου"), ("Select local keyboard type", "Επιλογή τύπου τοπικού πληκτρολογίου"), - ("software_render_tip", "Εάν έχετε κάρτα γραφικών Nvidia και το παράθυρο σύνδεσης κλείνει αμέσως μετά τη σύνδεση, η εγκατάσταση του προγράμματος οδήγησης nouveau και η επιλογή χρήσης της επιτάχυνσης λογισμικού μπορεί να βοηθήσει. Απαιτείται επανεκκίνηση."), - ("Always use software rendering", "Να χρησιμοποιείται πάντα επιτάχυνση λογισμικού"), + ("software_render_tip", "Εάν έχετε κάρτα γραφικών Nvidia και το παράθυρο σύνδεσης κλείνει αμέσως μετά τη σύνδεση, η εγκατάσταση του προγράμματος οδήγησης nouveau και η επιλογή χρήσης της επιτάχυνσης γραφικών μέσω λογισμικού μπορεί να βοηθήσει. Απαιτείται επανεκκίνηση."), + ("Always use software rendering", "Επιτάχυνση γραφικών μέσω λογισμικού"), ("config_input", "Για να ελέγξετε την απομακρυσμένη επιφάνεια εργασίας με πληκτρολόγιο, πρέπει να εκχωρήσετε δικαιώματα στο RustDesk"), ("request_elevation_tip", "αίτημα ανύψωσης δικαιωμάτων χρήστη"), ("Wait", "Περιμένετε"), From 8e6863ff611be2767352650b1b4d90c90569bc1d Mon Sep 17 00:00:00 2001 From: Manos G <87467035+7th-fret@users.noreply.github.com> Date: Tue, 17 Jan 2023 13:38:27 +0200 Subject: [PATCH 1516/2015] Update gr.rs --- src/lang/gr.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/gr.rs b/src/lang/gr.rs index 91607877c..4b54ba8ad 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -425,7 +425,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Elevate successfully", "Επιτυχής ανύψωση δικαιωμάτων χρήστη"), ("uppercase", "κεφαλαία γράμματα"), ("lowercase", "πεζά γράμματα"), - ("digit", "ψηφίο"), + ("digit", "Αριθμός"), ("special character", "ειδικός χαρακτήρας"), ("length>=8", "μήκος>=8"), ("Weak", "Αδύναμο"), From 67db6bfeb77dd8e7507dc04876242a95cbee582c Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Tue, 17 Jan 2023 05:56:07 -0700 Subject: [PATCH 1517/2015] check for env variable and option for message --- src/ui_interface.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/ui_interface.rs b/src/ui_interface.rs index f45216d0c..5d9dfe083 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -243,10 +243,11 @@ pub fn set_peer_option(id: String, name: String, value: String) { #[inline] pub fn using_public_server() -> bool { - if hbb_common::config::RS_PUB_KEY == "OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw=" { - return true + let key_check: Option<&'static str> = option_env!("RS_PUB_KEY_VAL"); + if key_check != "None" and crate::get_custom_rendezvous_server(get_option_("custom-rendezvous-server")).is_empty() { + return False; } else { - return false + return True; } } From 12af9e636963d1adfcc33f23f42cc9d560c43f9a Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Tue, 17 Jan 2023 06:56:09 -0700 Subject: [PATCH 1518/2015] update RENDEZVOUS_SERVER env check --- libs/hbb_common/src/config.rs | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index e53bda578..213da08f1 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -77,21 +77,13 @@ const CHARS: &'static [char] = &[ 'm', 'n', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', ]; -//check for env variable RENDEZVOUS_SERVER1-3 if not use the default -pub const RENDEZVOUS_SERVERS: &'static [&'static str] = &[ - match option_env!("RENDEZVOUS_SERVER1") { - Some(key) => key, - None => "rs-ny.rustdesk.com", - }, - match option_env!("RENDEZVOUS_SERVER2") { - Some(key) => key, - None => "rs-sg.rustdesk.com", - }, - match option_env!("RENDEZVOUS_SERVER3") { - Some(key) => key, - None => "rs-cn.rustdesk.com", - }, -]; +//check for env variable RENDEZVOUS_SERVER if not use the default +pub const RENDEZVOUS_SERVERS: [&'static str;3] = + match option_env!("RENDEZVOUS_SERVER") { + Some(key) => [key,key,key], + None => ["rs-ny.rustdesk.com","rs-sg.rustdesk.com","rs-cn.rustdesk.com"], + }; + //check for env variable RS_PUB_KEY if not use default pub const RS_PUB_KEY: &'static str = match option_env!("RS_PUB_KEY_VAL") { From f6de021d37aaeba522abceba827aa7d4559e57cc Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Tue, 17 Jan 2023 07:04:02 -0700 Subject: [PATCH 1519/2015] replace and with && --- src/ui_interface.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 5d9dfe083..8744780bc 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -244,7 +244,7 @@ pub fn set_peer_option(id: String, name: String, value: String) { #[inline] pub fn using_public_server() -> bool { let key_check: Option<&'static str> = option_env!("RS_PUB_KEY_VAL"); - if key_check != "None" and crate::get_custom_rendezvous_server(get_option_("custom-rendezvous-server")).is_empty() { + if key_check != "None" && crate::get_custom_rendezvous_server(get_option_("custom-rendezvous-server")).is_empty() { return False; } else { return True; From ada2d2b539a687c8846babc1e1b9e696b4fd7c9d Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Tue, 17 Jan 2023 07:21:01 -0700 Subject: [PATCH 1520/2015] Update ui_interface.rs --- src/ui_interface.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 8744780bc..7ea97ced1 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -244,7 +244,7 @@ pub fn set_peer_option(id: String, name: String, value: String) { #[inline] pub fn using_public_server() -> bool { let key_check: Option<&'static str> = option_env!("RS_PUB_KEY_VAL"); - if key_check != "None" && crate::get_custom_rendezvous_server(get_option_("custom-rendezvous-server")).is_empty() { + if key_check != None && crate::get_custom_rendezvous_server(get_option_("custom-rendezvous-server")).is_empty() { return False; } else { return True; From f87dff262e48254008f270e5770f20cf5fdcc85c Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Tue, 17 Jan 2023 07:39:16 -0700 Subject: [PATCH 1521/2015] Update ui_interface.rs --- 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 7ea97ced1..ff66ccb27 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -245,9 +245,9 @@ pub fn set_peer_option(id: String, name: String, value: String) { pub fn using_public_server() -> bool { let key_check: Option<&'static str> = option_env!("RS_PUB_KEY_VAL"); if key_check != None && crate::get_custom_rendezvous_server(get_option_("custom-rendezvous-server")).is_empty() { - return False; + return false; } else { - return True; + return true; } } From d601a82b5a46b961b6e6cfa08d8ac8091fc92fcf Mon Sep 17 00:00:00 2001 From: qiushiyang Date: Tue, 17 Jan 2023 22:46:11 +0800 Subject: [PATCH 1522/2015] Allow direct connect to {hostname}:{port} --- libs/hbb_common/src/lib.rs | 22 ++++++++++++++++++++++ src/client.rs | 11 +++++++++++ 2 files changed, 33 insertions(+) diff --git a/libs/hbb_common/src/lib.rs b/libs/hbb_common/src/lib.rs index 49be934fb..1069cb8cf 100644 --- a/libs/hbb_common/src/lib.rs +++ b/libs/hbb_common/src/lib.rs @@ -321,6 +321,13 @@ pub fn is_ip_str(id: &str) -> bool { is_ipv4_str(id) || is_ipv6_str(id) } +#[inline] +pub fn is_hostname_port_str(id: &str) -> bool { + regex::Regex::new(r"^[\-.0-9a-zA-Z]+:\d{1,5}$") + .unwrap() + .is_match(id) +} + #[cfg(test)] mod test_lib { use super::*; @@ -340,4 +347,19 @@ mod test_lib { assert_eq!(is_ipv6_str("[1:2::0]:"), false); assert_eq!(is_ipv6_str("1:2::0]:1"), false); } + #[test] + fn test_hostname_port() { + assert_eq!(is_ipv6_str("a:12"), true); + assert_eq!(is_ipv6_str("a.b.c:12"), true); + assert_eq!(is_ipv6_str("test.com:12"), true); + assert_eq!(is_ipv6_str("1.2.3:12"), true); + assert_eq!(is_ipv6_str("a.b.c:123456"), false); + // todo: should we also check for these edge case? + // out-of-range port + assert_eq!(is_ipv6_str("test.com:0"), true); + assert_eq!(is_ipv6_str("test.com:98989"), true); + // invalid hostname + assert_eq!(is_ipv6_str("---:12"), true); + assert_eq!(is_ipv6_str(".:12"), true); + } } diff --git a/src/client.rs b/src/client.rs index 8b2edbcda..f3cca9b36 100644 --- a/src/client.rs +++ b/src/client.rs @@ -181,6 +181,17 @@ impl Client { true, )); } + // Allow connect to {hostname}:{port} + if hbb_common.is_hostname_port_str(peer) { + return Ok(( + socket_client::connect_tcp( + peer, + RENDEZVOUS_TIMEOUT, + ) + .await?, + true, + )); + } let (mut rendezvous_server, servers, contained) = crate::get_rendezvous_server(1_000).await; let mut socket = socket_client::connect_tcp(&*rendezvous_server, RENDEZVOUS_TIMEOUT).await; debug_assert!(!servers.contains(&rendezvous_server)); From 9980246b90cafc745765590a9834ee40a4832ecf Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Tue, 17 Jan 2023 07:54:11 -0700 Subject: [PATCH 1523/2015] add RS_DEF_PUB_KEY --- libs/hbb_common/src/config.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 213da08f1..0d5b81b26 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -80,15 +80,17 @@ const CHARS: &'static [char] = &[ //check for env variable RENDEZVOUS_SERVER if not use the default pub const RENDEZVOUS_SERVERS: [&'static str;3] = match option_env!("RENDEZVOUS_SERVER") { - Some(key) => [key,key,key], + Some(key) => [key], None => ["rs-ny.rustdesk.com","rs-sg.rustdesk.com","rs-cn.rustdesk.com"], }; + +pub const RS_DEF_PUB_KEY: &'static str = "OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw="; //check for env variable RS_PUB_KEY if not use default pub const RS_PUB_KEY: &'static str = match option_env!("RS_PUB_KEY_VAL") { Some(key) => key, - None => "OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw=", + None => RS_DEF_PUB_KEY, }; pub const RENDEZVOUS_PORT: i32 = 21116; From 8aea21e9f55e3ae66a9edae5c1c168b92700f645 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Tue, 17 Jan 2023 07:54:28 -0700 Subject: [PATCH 1524/2015] check for server with RS_DEF_PUB_KEY --- src/ui_interface.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/ui_interface.rs b/src/ui_interface.rs index ff66ccb27..ef26534cf 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -243,11 +243,10 @@ pub fn set_peer_option(id: String, name: String, value: String) { #[inline] pub fn using_public_server() -> bool { - let key_check: Option<&'static str> = option_env!("RS_PUB_KEY_VAL"); - if key_check != None && crate::get_custom_rendezvous_server(get_option_("custom-rendezvous-server")).is_empty() { - return false; + if hbb_common::config::RS_PUB_KEY == hbb_common::config::RS_DEF_PUB_KEY { + return true } else { - return true; + return false } } From b59ae9bd42b515f9288fa1d4896cf648c7bb66f9 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Tue, 17 Jan 2023 08:03:24 -0700 Subject: [PATCH 1525/2015] requires 3 elements in array --- libs/hbb_common/src/config.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 0d5b81b26..d819c816d 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -80,12 +80,10 @@ const CHARS: &'static [char] = &[ //check for env variable RENDEZVOUS_SERVER if not use the default pub const RENDEZVOUS_SERVERS: [&'static str;3] = match option_env!("RENDEZVOUS_SERVER") { - Some(key) => [key], + Some(key) => [key, key, key], None => ["rs-ny.rustdesk.com","rs-sg.rustdesk.com","rs-cn.rustdesk.com"], }; - - pub const RS_DEF_PUB_KEY: &'static str = "OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw="; //check for env variable RS_PUB_KEY if not use default pub const RS_PUB_KEY: &'static str = match option_env!("RS_PUB_KEY_VAL") { From f10f969c2c2626251011ab99df1d09c4fa972228 Mon Sep 17 00:00:00 2001 From: qiushiyang Date: Wed, 18 Jan 2023 02:08:44 +0000 Subject: [PATCH 1526/2015] fix syntax error --- src/client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.rs b/src/client.rs index f3cca9b36..304314326 100644 --- a/src/client.rs +++ b/src/client.rs @@ -182,7 +182,7 @@ impl Client { )); } // Allow connect to {hostname}:{port} - if hbb_common.is_hostname_port_str(peer) { + if hbb_common::is_hostname_port_str(peer) { return Ok(( socket_client::connect_tcp( peer, From 12d446b217ed5987c43979609440c12aee24c4ce Mon Sep 17 00:00:00 2001 From: qiushiyang Date: Wed, 18 Jan 2023 03:35:13 +0000 Subject: [PATCH 1527/2015] fix test for is_hostname_port_str --- libs/hbb_common/src/lib.rs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/libs/hbb_common/src/lib.rs b/libs/hbb_common/src/lib.rs index 1069cb8cf..29b066c66 100644 --- a/libs/hbb_common/src/lib.rs +++ b/libs/hbb_common/src/lib.rs @@ -347,19 +347,20 @@ mod test_lib { assert_eq!(is_ipv6_str("[1:2::0]:"), false); assert_eq!(is_ipv6_str("1:2::0]:1"), false); } + #[test] fn test_hostname_port() { - assert_eq!(is_ipv6_str("a:12"), true); - assert_eq!(is_ipv6_str("a.b.c:12"), true); - assert_eq!(is_ipv6_str("test.com:12"), true); - assert_eq!(is_ipv6_str("1.2.3:12"), true); - assert_eq!(is_ipv6_str("a.b.c:123456"), false); + assert_eq!(is_hostname_port_str("a:12"), true); + assert_eq!(is_hostname_port_str("a.b.c:12"), true); + assert_eq!(is_hostname_port_str("test.com:12"), true); + assert_eq!(is_hostname_port_str("1.2.3:12"), true); + assert_eq!(is_hostname_port_str("a.b.c:123456"), false); // todo: should we also check for these edge case? // out-of-range port - assert_eq!(is_ipv6_str("test.com:0"), true); - assert_eq!(is_ipv6_str("test.com:98989"), true); + assert_eq!(is_hostname_port_str("test.com:0"), true); + assert_eq!(is_hostname_port_str("test.com:98989"), true); // invalid hostname - assert_eq!(is_ipv6_str("---:12"), true); - assert_eq!(is_ipv6_str(".:12"), true); + assert_eq!(is_hostname_port_str("---:12"), true); + assert_eq!(is_hostname_port_str(".:12"), true); } } From 7aced73393258fe29563de8785e27f83dc38bd8a Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Wed, 18 Jan 2023 11:48:10 +0800 Subject: [PATCH 1528/2015] Revert "Allow setting custom server and key with env variables" --- .github/workflows/flutter-nightly.yml | 24 +----------------------- libs/hbb_common/src/config.rs | 20 ++++++-------------- src/ui_interface.rs | 6 +----- 3 files changed, 8 insertions(+), 42 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 112cb53ad..845ba339b 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -15,11 +15,6 @@ env: # for multiarch gcc compatibility VCPKG_COMMIT_ID: "14e7bb4ae24616ec54ff6b2f6ef4e8659434ea44" VERSION: "1.2.0" - # To make a custom build with your own servers set the below secret values - RS_PUB_KEY_VAL: '${{ secrets.RS_PUB_KEY_VAL }}' - RENDEZVOUS_SERVER1: '${{ secrets.RENDEZVOUS_SERVER1 }}' - RENDEZVOUS_SERVER2: '${{ secrets.RENDEZVOUS_SERVER2 }}' - RENDEZVOUS_SERVER3: '${{ secrets.RENDEZVOUS_SERVER3 }}' jobs: build-for-windows: @@ -155,7 +150,6 @@ jobs: uses: actions/checkout@v3 - name: Import the codesign cert - if: ${{ env.MACOS_P12_BASE64== 'true' }} uses: apple-actions/import-codesign-certs@v1 with: p12-file-base64: ${{ secrets.MACOS_P12_BASE64 }} @@ -163,13 +157,11 @@ jobs: keychain: rustdesk - name: Check sign and import sign key - if: ${{ env.MACOS_P12_BASE64== 'true' }} run: | security default-keychain -s rustdesk.keychain security find-identity -v - name: Import notarize key - if: ${{ env.MACOS_P12_BASE64== 'true' }} uses: timheuer/base64-to-file@v1.2 with: # https://gregoryszorc.com/docs/apple-codesign/stable/apple_codesign_rcodesign.html#notarizing-and-stapling @@ -178,7 +170,6 @@ jobs: encodedString: ${{ secrets.MACOS_NOTARIZE_JSON }} - name: Install rcodesign tool - if: ${{ env.MACOS_P12_BASE64== 'true' }} shell: bash run: | pushd /tmp @@ -249,7 +240,6 @@ jobs: ./build.py --flutter ${{ matrix.job.extra-build-args }} - name: Codesign app and create signed dmg - if: ${{ env.MACOS_P12_BASE64== 'true' }} run: | security default-keychain -s rustdesk.keychain security unlock-keychain -p ${{ secrets.MACOS_P12_PASSWORD }} rustdesk.keychain @@ -562,7 +552,6 @@ jobs: - uses: r0adkll/sign-android-release@v1 name: Sign app APK - if: ${{ env.ANDROID_SIGNING_KEY== 'true' }} id: sign-rustdesk with: releaseDirectory: ./signed-apk @@ -575,14 +564,12 @@ jobs: BUILD_TOOLS_VERSION: "30.0.2" - name: Upload Artifacts - if: ${{ env.ANDROID_SIGNING_KEY== 'true' }} uses: actions/upload-artifact@master with: name: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release-signed.apk path: ${{steps.sign-rustdesk.outputs.signedReleaseFile}} - - name: Publish signed apk package - if: ${{ env.ANDROID_SIGNING_KEY== 'true' }} + - name: Publish apk package uses: softprops/action-gh-release@v1 with: prerelease: true @@ -590,15 +577,6 @@ jobs: files: | ${{steps.sign-rustdesk.outputs.signedReleaseFile}} - - name: Publish unsigned apk package - if: ${{ env.ANDROID_SIGNING_KEY!= 'true' }} - uses: softprops/action-gh-release@v1 - with: - prerelease: true - tag_name: ${{ env.TAG_NAME }} - files: | - ../rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk - build-rustdesk-lib-linux-amd64: needs: [generate-bridge-linux, build-vcpkg-deps-linux] name: build-rust-lib ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index d819c816d..1d427a2e9 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -77,20 +77,12 @@ const CHARS: &'static [char] = &[ 'm', 'n', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', ]; -//check for env variable RENDEZVOUS_SERVER if not use the default -pub const RENDEZVOUS_SERVERS: [&'static str;3] = - match option_env!("RENDEZVOUS_SERVER") { - Some(key) => [key, key, key], - None => ["rs-ny.rustdesk.com","rs-sg.rustdesk.com","rs-cn.rustdesk.com"], - }; - -pub const RS_DEF_PUB_KEY: &'static str = "OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw="; -//check for env variable RS_PUB_KEY if not use default -pub const RS_PUB_KEY: &'static str = match option_env!("RS_PUB_KEY_VAL") { - Some(key) => key, - None => RS_DEF_PUB_KEY, -}; - +pub const RENDEZVOUS_SERVERS: &'static [&'static str] = &[ + "rs-ny.rustdesk.com", + "rs-sg.rustdesk.com", + "rs-cn.rustdesk.com", +]; +pub const RS_PUB_KEY: &'static str = "OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw="; pub const RENDEZVOUS_PORT: i32 = 21116; pub const RELAY_PORT: i32 = 21117; diff --git a/src/ui_interface.rs b/src/ui_interface.rs index ef26534cf..9984198b8 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -243,11 +243,7 @@ pub fn set_peer_option(id: String, name: String, value: String) { #[inline] pub fn using_public_server() -> bool { - if hbb_common::config::RS_PUB_KEY == hbb_common::config::RS_DEF_PUB_KEY { - return true - } else { - return false - } + crate::get_custom_rendezvous_server(get_option_("custom-rendezvous-server")).is_empty() } #[inline] From aa2cd37fb35c791bb9906d80b69206d311d78a11 Mon Sep 17 00:00:00 2001 From: qiushiyang Date: Wed, 18 Jan 2023 06:08:46 +0000 Subject: [PATCH 1529/2015] use more accurate regex for {domain}:{port} --- libs/hbb_common/src/lib.rs | 40 ++++++++++++++++++++++++-------------- src/client.rs | 12 ++++-------- 2 files changed, 29 insertions(+), 23 deletions(-) diff --git a/libs/hbb_common/src/lib.rs b/libs/hbb_common/src/lib.rs index 29b066c66..e57994f34 100644 --- a/libs/hbb_common/src/lib.rs +++ b/libs/hbb_common/src/lib.rs @@ -322,10 +322,15 @@ pub fn is_ip_str(id: &str) -> bool { } #[inline] -pub fn is_hostname_port_str(id: &str) -> bool { - regex::Regex::new(r"^[\-.0-9a-zA-Z]+:\d{1,5}$") - .unwrap() - .is_match(id) +pub fn is_domain_port_str(id: &str) -> bool { + // modified regex for RFC1123 hostname. check https://stackoverflow.com/a/106223 for original version for hostname. + // according to [TLD List](https://data.iana.org/TLD/tlds-alpha-by-domain.txt) version 2023011700, + // there is no digits in TLD, and length is 2~63. + regex::Regex::new( + r"(?i)^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z][a-z-]{0,61}[a-z]:\d{1,5}$", + ) + .unwrap() + .is_match(id) } #[cfg(test)] @@ -350,17 +355,22 @@ mod test_lib { #[test] fn test_hostname_port() { - assert_eq!(is_hostname_port_str("a:12"), true); - assert_eq!(is_hostname_port_str("a.b.c:12"), true); - assert_eq!(is_hostname_port_str("test.com:12"), true); - assert_eq!(is_hostname_port_str("1.2.3:12"), true); - assert_eq!(is_hostname_port_str("a.b.c:123456"), false); - // todo: should we also check for these edge case? + 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); + // todo: should we also check for these edge cases? // out-of-range port - assert_eq!(is_hostname_port_str("test.com:0"), true); - assert_eq!(is_hostname_port_str("test.com:98989"), true); - // invalid hostname - assert_eq!(is_hostname_port_str("---:12"), true); - assert_eq!(is_hostname_port_str(".:12"), true); + assert_eq!(is_domain_port_str("test.com:0"), true); + assert_eq!(is_domain_port_str("test.com:98989"), true); } } diff --git a/src/client.rs b/src/client.rs index 304314326..493448c3b 100644 --- a/src/client.rs +++ b/src/client.rs @@ -7,10 +7,10 @@ use cpal::{ use magnum_opus::{Channels::*, Decoder as AudioDecoder}; use sha2::{Digest, Sha256}; use std::{ - str::FromStr, collections::HashMap, net::SocketAddr, ops::{Deref, Not}, + str::FromStr, sync::{atomic::AtomicBool, mpsc, Arc, Mutex, RwLock}, }; use uuid::Uuid; @@ -181,14 +181,10 @@ impl Client { true, )); } - // Allow connect to {hostname}:{port} - if hbb_common::is_hostname_port_str(peer) { + // Allow connect to {domain}:{port} + if hbb_common::is_domain_port_str(peer) { return Ok(( - socket_client::connect_tcp( - peer, - RENDEZVOUS_TIMEOUT, - ) - .await?, + socket_client::connect_tcp(peer, RENDEZVOUS_TIMEOUT).await?, true, )); } From 8fb3c452bea8dfb9c1de14db8b06f158d31147dd Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 18 Jan 2023 14:22:41 +0800 Subject: [PATCH 1530/2015] Allow setting custom server and key with env variables #2810 --- .github/workflows/flutter-nightly.yml | 2 ++ libs/hbb_common/src/config.rs | 14 +++++++++++--- src/platform/windows.rs | 16 ---------------- src/ui.rs | 5 ----- src/ui/index.tis | 1 - src/ui_interface.rs | 10 ++-------- 6 files changed, 15 insertions(+), 33 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 845ba339b..d5782eabf 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -15,6 +15,8 @@ env: # for multiarch gcc compatibility VCPKG_COMMIT_ID: "14e7bb4ae24616ec54ff6b2f6ef4e8659434ea44" VERSION: "1.2.0" + RS_PUB_KEY: '${{ secrets.RS_PUB_KEY }}' + RENDEZVOUS_SERVER: '${{ secrets.RENDEZVOUS_SERVER }}' jobs: build-for-windows: diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 1d427a2e9..abec5b231 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -49,7 +49,10 @@ lazy_static::lazy_static! { static ref CONFIG2: Arc> = Arc::new(RwLock::new(Config2::load())); static ref LOCAL_CONFIG: Arc> = Arc::new(RwLock::new(LocalConfig::load())); pub static ref ONLINE: Arc>> = Default::default(); - pub static ref PROD_RENDEZVOUS_SERVER: Arc> = Default::default(); + pub static ref PROD_RENDEZVOUS_SERVER: Arc> = Arc::new(RwLock::new(match option_env!("RENDEZVOUS_SERVER") { + Some(key) => key, + _ => "", + }.to_owned())); pub static ref APP_NAME: Arc> = Arc::new(RwLock::new("RustDesk".to_owned())); static ref KEY_PAIR: Arc, Vec)>>> = Default::default(); static ref HW_CODEC_CONFIG: Arc> = Arc::new(RwLock::new(HwCodecConfig::load())); @@ -77,12 +80,17 @@ const CHARS: &'static [char] = &[ 'm', 'n', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', ]; -pub const RENDEZVOUS_SERVERS: &'static [&'static str] = &[ +const RENDEZVOUS_SERVERS: &'static [&'static str] = &[ "rs-ny.rustdesk.com", "rs-sg.rustdesk.com", "rs-cn.rustdesk.com", ]; -pub const RS_PUB_KEY: &'static str = "OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw="; + +pub const RS_PUB_KEY: &'static str = match option_env!("RS_PUB_KEY") { + Some(key) => key, + None => "OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw=", +}; + pub const RENDEZVOUS_PORT: i32 = 21116; pub const RELAY_PORT: i32 = 21117; diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 89861a418..190834eb8 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -1368,22 +1368,6 @@ pub fn get_license() -> Option { pub fn bootstrap() { if let Some(lic) = get_license() { *config::PROD_RENDEZVOUS_SERVER.write().unwrap() = lic.host.clone(); - #[cfg(feature = "hbbs")] - { - if !is_win_server() { - return; - } - crate::hbbs::bootstrap(&lic.key, &lic.host); - std::thread::spawn(move || loop { - let tmp = Config::get_option("stop-rendezvous-service"); - if tmp.is_empty() { - crate::hbbs::start(); - } else { - crate::hbbs::stop(); - } - std::thread::sleep(std::time::Duration::from_millis(100)); - }); - } } } diff --git a/src/ui.rs b/src/ui.rs index d45a64298..4cd9ce3f7 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -208,10 +208,6 @@ impl UI { show_run_without_install() } - fn has_rendezvous_service(&self) -> bool { - has_rendezvous_service() - } - fn get_license(&self) -> String { get_license() } @@ -599,7 +595,6 @@ impl sciter::EventHandler for UI { fn peer_has_password(String); fn forget_password(String); fn set_peer_option(String, String, String); - fn has_rendezvous_service(); fn get_license(); fn test_if_valid_server(String); fn get_sound_inputs(); diff --git a/src/ui/index.tis b/src/ui/index.tis index c141d0efe..774b6184a 100644 --- a/src/ui/index.tis +++ b/src/ui/index.tis @@ -310,7 +310,6 @@ class MyIdMenu: Reactor.Component { {handler.is_rdp_service_open() ? : ""} {false && handler.using_public_server() &&
  • {svg_checkmark}{translate('Always connected via relay')}
  • } - {handler.has_rendezvous_service() ?
  • {translate(rendezvous_service_stopped ? "Start ID/relay service" : "Stop ID/relay service")}
  • : ""} {handler.is_ok_change_id() ?
    : ""} {username ?
  • {translate('Logout')} ({username})
  • : diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 9984198b8..2e6ef561c 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -134,13 +134,6 @@ pub fn show_run_without_install() -> bool { false } -#[inline] -pub fn has_rendezvous_service() -> bool { - #[cfg(all(windows, feature = "hbbs"))] - return crate::platform::is_win_server() && crate::platform::windows::get_license().is_some(); - return false; -} - #[inline] pub fn get_license() -> String { #[cfg(windows)] @@ -243,7 +236,8 @@ pub fn set_peer_option(id: String, name: String, value: String) { #[inline] pub fn using_public_server() -> bool { - crate::get_custom_rendezvous_server(get_option_("custom-rendezvous-server")).is_empty() + option_env!("RENDEZVOUS_SERVER").is_none() + && crate::get_custom_rendezvous_server(get_option_("custom-rendezvous-server")).is_empty() } #[inline] From b4a88c7780acd1942457ef662dd3b68efe4e3022 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 18 Jan 2023 14:42:08 +0800 Subject: [PATCH 1531/2015] fix CI --- src/flutter_ffi.rs | 4 ---- src/ui/index.tis | 8 -------- 2 files changed, 12 deletions(-) diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index a4b6d1395..ca6823aa5 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -619,10 +619,6 @@ pub fn main_discover() { discover(); } -pub fn main_has_rendezvous_service() -> bool { - has_rendezvous_service() -} - pub fn main_get_api_server() -> String { get_api_server() } diff --git a/src/ui/index.tis b/src/ui/index.tis index 774b6184a..2d77b1eec 100644 --- a/src/ui/index.tis +++ b/src/ui/index.tis @@ -9,7 +9,6 @@ var app; var tmp = handler.get_connect_status(); var connect_status = tmp[0]; var service_stopped = handler.get_option("stop-service") == "Y"; -var rendezvous_service_stopped = false; var using_public_server = handler.using_public_server(); var software_update_url = ""; var key_confirmed = tmp[1]; @@ -467,8 +466,6 @@ class MyIdMenu: Reactor.Component { }, 240); } else if (me.id == "stop-service") { handler.set_option("stop-service", service_stopped ? "" : "Y"); - } else if (me.id == "stop-rendezvous-service") { - handler.set_option("stop-rendezvous-service", rendezvous_service_stopped ? "" : "Y"); } else if (me.id == "change-id") { msgbox("custom-id", translate("Change ID"), "
    \
    " + translate('id_change_tip') + "
    \ @@ -1120,11 +1117,6 @@ function checkConnectStatus() { service_stopped = tmp; app.update(); } - tmp = !!handler.get_option("stop-rendezvous-service"); - if (tmp != rendezvous_service_stopped) { - rendezvous_service_stopped = tmp; - myIdMenu.update(); - } tmp = handler.using_public_server(); if (tmp != using_public_server) { using_public_server = tmp; From 134be63d116804c9bac0969cb1b3b2ab09f32fbe Mon Sep 17 00:00:00 2001 From: solokot Date: Wed, 18 Jan 2023 10:12:32 +0300 Subject: [PATCH 1532/2015] update ru.rs --- src/lang/ru.rs | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/lang/ru.rs b/src/lang/ru.rs index b22d49cc2..abe642d9c 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -51,7 +51,7 @@ 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"), + ("id_change_tip", "Допускаются только символы a-z, A-Z, 0-9 и _ (подчёркивание). Первой должна быть буква a-z, A-Z. Длина от 6 до 16."), ("Invalid format", "Неправильный формат"), ("server_not_support", "Пока не поддерживается сервером"), ("Not available", "Недоступно"), @@ -413,23 +413,23 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", "Если у вас видеокарта Nvidia и удалённое окно закрывается сразу после подключения, может помочь установка драйвера Nouveau и выбор использования программной визуализации. Потребуется перезапуск."), ("Always use software rendering", "Использовать программную визуализацию"), ("config_input", "Чтобы управлять удалённым рабочим столом с помощью клавиатуры, необходимо предоставить RustDesk разрешения \"Мониторинг ввода\"."), - ("request_elevation_tip", ""), - ("Wait", ""), - ("Elevation Error", ""), - ("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", ""), - ("Request Elevation", ""), - ("wait_accept_uac_tip", ""), - ("Elevate successfully", ""), - ("uppercase", ""), - ("lowercase", ""), - ("digit", ""), - ("special character", ""), - ("length>=8", ""), - ("Weak", ""), - ("Medium", ""), - ("Strong", ""), + ("request_elevation_tip", "Также можно запросить повышение прав, если кто-то есть на удалённой стороне."), + ("Wait", "Ждите"), + ("Elevation Error", "Ошибка повышения прав"), + ("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", "По-прежнему требуется, чтобы удалённый пользователь нажал \"OK\" в окне UAC при запуске RustDesk."), + ("Request Elevation", "Запросить повышение"), + ("wait_accept_uac_tip", "Подождите, пока удалённый пользователь подтвердит запрос UAC."), + ("Elevate successfully", "Права повышены"), + ("uppercase", "заглавные"), + ("lowercase", "строчные"), + ("digit", "цифры"), + ("special character", "спецсимволы"), + ("length>=8", "8+ символов"), + ("Weak", "Слабый"), + ("Medium", "Средний"), + ("Strong", "Стойкий"), ].iter().cloned().collect(); } From 9fa76d23490ac3a6dd955fdcdab950a5d05e115a Mon Sep 17 00:00:00 2001 From: NicKoehler <53040044+NicKoehler@users.noreply.github.com> Date: Wed, 18 Jan 2023 09:38:02 +0100 Subject: [PATCH 1533/2015] Update it.rs --- src/lang/it.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/lang/it.rs b/src/lang/it.rs index f94669d34..cdc44d640 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -423,13 +423,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request Elevation", "Richiedi elevazione dei diritti"), ("wait_accept_uac_tip", "Attendere che l'utente remoto accetti la finestra di dialogo UAC."), ("Elevate successfully", "Elevazione dei diritti effettuata con successo"), - ("uppercase", ""), - ("lowercase", ""), - ("digit", ""), - ("special character", ""), - ("length>=8", ""), - ("Weak", ""), - ("Medium", ""), - ("Strong", ""), + ("uppercase", "maiuscolo"), + ("lowercase", "minuscolo"), + ("digit", "numero"), + ("special character", "carattere speciale"), + ("length>=8", "lunghezza >= 8"), + ("Weak", "Debole"), + ("Medium", "Media"), + ("Strong", "Forte"), ].iter().cloned().collect(); } From 6aafe386b2953c09e4decb3b195cc92babd7d6c6 Mon Sep 17 00:00:00 2001 From: NicKoehler <53040044+NicKoehler@users.noreply.github.com> Date: Wed, 18 Jan 2023 09:45:53 +0100 Subject: [PATCH 1534/2015] 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 cdc44d640..e37985d8b 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -39,7 +39,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Cambia ID"), ("Website", "Sito web"), ("About", "Informazioni"), - ("Slogan_tip", ""), + ("Slogan_tip", "Fatta con il cuore in questo mondo caotico!"), ("Privacy Statement", ""), ("Mute", "Silenzia"), ("Audio Input", "Input audio"), From 845b2a943ed23621957232fb1ccac0152252b34e Mon Sep 17 00:00:00 2001 From: NicKoehler <53040044+NicKoehler@users.noreply.github.com> Date: Wed, 18 Jan 2023 10:01:19 +0100 Subject: [PATCH 1535/2015] Update it.rs --- src/lang/it.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/lang/it.rs b/src/lang/it.rs index e37985d8b..460b26aa0 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -40,7 +40,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Website", "Sito web"), ("About", "Informazioni"), ("Slogan_tip", "Fatta con il cuore in questo mondo caotico!"), - ("Privacy Statement", ""), + ("Privacy Statement", "Informativa sulla privacy"), ("Mute", "Silenzia"), ("Audio Input", "Input audio"), ("Enhancements", "Miglioramenti"), @@ -185,7 +185,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enter your password", "Inserisci la tua password"), ("Logging in...", "Autenticazione..."), ("Enable RDP session sharing", "Abilita la condivisione della sessione RDP"), - ("Auto Login", "Login automatico"), + ("Auto Login", "Accesso automatico"), ("Enable Direct IP Access", "Abilita l'accesso diretto tramite IP"), ("Rename", "Rinomina"), ("Space", "Spazio"), @@ -195,7 +195,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please enter the folder name", "Inserisci il nome della cartella"), ("Fix it", "Risolvi"), ("Warning", "Avviso"), - ("Login screen using Wayland is not supported", "La schermata di login non è supportata utilizzando Wayland"), + ("Login screen using Wayland is not supported", "La schermata di accesso non è supportata utilizzando Wayland"), ("Reboot required", "Riavvio necessario"), ("Unsupported display server ", "Display server non supportato"), ("x11 expected", "x11 necessario"), @@ -210,11 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Connetti sempre tramite relay"), ("whitelist_tip", "Solo gli indirizzi IP autorizzati possono connettersi a questo desktop"), ("Login", "Accedi"), - ("Verify", ""), - ("Remember me", ""), - ("Trust this device", ""), - ("Verification code", ""), - ("verification_tip", ""), + ("Verify", "Verifica"), + ("Remember me", "Ricordami"), + ("Trust this device", "Registra questo dispositivo come attendibile"), + ("Verification code", "Codice di verifica"), + ("verification_tip", "È stato rilevato un nuovo dispositivo e un codice di verifica è stato inviato all'indirizzo e-mail registrato; inserire il codice di verifica per continuare l'accesso."), ("Logout", "Esci"), ("Tags", "Tag"), ("Search ID", "Cerca ID"), From 8f31378155e763f4eef641364ce6ed6dad73f5c2 Mon Sep 17 00:00:00 2001 From: NicKoehler <53040044+NicKoehler@users.noreply.github.com> Date: Wed, 18 Jan 2023 10:09:10 +0100 Subject: [PATCH 1536/2015] Update it.rs --- src/lang/it.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/lang/it.rs b/src/lang/it.rs index 460b26aa0..d2358a336 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -202,12 +202,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Port", "Porta"), ("Settings", "Impostazioni"), ("Username", " Nome utente"), - ("Invalid port", "Porta non valida"), + ("Invalid port", "Numero di porta non valido"), ("Closed manually by the peer", "Chiuso manualmente dal peer"), ("Enable remote configuration modification", "Abilita la modifica remota della configurazione"), - ("Run without install", "Avvia senza installare"), + ("Run without install", "Esegui senza installare"), ("Always connected via relay", "Connesso sempre tramite relay"), - ("Always connect via relay", "Connetti sempre tramite relay"), + ("Always connect via relay", "Collegati sempre tramite relay"), ("whitelist_tip", "Solo gli indirizzi IP autorizzati possono connettersi a questo desktop"), ("Login", "Accedi"), ("Verify", "Verifica"), @@ -224,8 +224,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add Tag", "Aggiungi tag"), ("Unselect all tags", "Deseleziona tutti i tag"), ("Network error", "Errore di rete"), - ("Username missed", "Nome utente dimenticato"), - ("Password missed", "Password dimenticata"), + ("Username missed", "Nome utente mancante"), + ("Password missed", "Password mancante"), ("Wrong credentials", "Credenziali errate"), ("Edit Tag", "Modifica tag"), ("Unremember Password", "Dimentica password"), From 22412acfccaf708e7e89a29f8eac12e55b0fafab Mon Sep 17 00:00:00 2001 From: NicKoehler <53040044+NicKoehler@users.noreply.github.com> Date: Wed, 18 Jan 2023 10:18:06 +0100 Subject: [PATCH 1537/2015] Update it.rs --- src/lang/it.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/lang/it.rs b/src/lang/it.rs index d2358a336..e3c1a1f0f 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -332,9 +332,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show Menubar", "Mostra la barra dei menu"), ("Hide Menubar", "nascondi la barra dei menu"), ("Direct Connection", "Connessione diretta"), - ("Relay Connection", "Collegamento a relè"), + ("Relay Connection", "Connessione relay"), ("Secure Connection", "Connessione sicura"), - ("Insecure Connection", "Connessione insicura"), + ("Insecure Connection", "Connessione non sicura"), ("Scale original", "Scala originale"), ("Scale adaptive", "Scala adattiva"), ("General", "Generale"), @@ -348,9 +348,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Unlock Security Settings", "Sblocca impostazioni di sicurezza"), ("Enable Audio", "Abilita audio"), ("Unlock Network Settings", "Sblocca impostazioni di rete"), - ("Server", ""), + ("Server", "Server"), ("Direct IP Access", "Accesso IP diretto"), - ("Proxy", ""), + ("Proxy", "Proxy"), ("Apply", "Applica"), ("Disconnect all devices?", "Disconnettere tutti i dispositivi?"), ("Clear", "Ripulisci"), @@ -372,7 +372,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable LAN Discovery", "Abilita il rilevamento della LAN"), ("Deny LAN Discovery", "Nega il rilevamento della LAN"), ("Write a message", "Scrivi un messaggio"), - ("Prompt", ""), + ("Prompt", "Prompt"), ("Please wait for confirmation of UAC...", "Attendi la conferma dell'UAC..."), ("elevated_foreground_window_tip", "La finestra corrente del desktop remoto richiede privilegi più elevati per funzionare, quindi non è in grado di utilizzare temporaneamente il mouse e la tastiera. È possibile chiedere all'utente remoto di ridurre a icona la finestra corrente o di fare clic sul pulsante di elevazione nella finestra di gestione della connessione. Per evitare questo problema, si consiglia di installare il software sul dispositivo remoto."), ("Disconnected", "Disconnesso"), From fca833fd00574f40e7d149941564b9a1fee49f73 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Wed, 18 Jan 2023 02:34:53 -0700 Subject: [PATCH 1538/2015] fix key check in nightly yaml --- .github/workflows/flutter-nightly.yml | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index d5782eabf..99bb20d48 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -15,8 +15,8 @@ env: # for multiarch gcc compatibility VCPKG_COMMIT_ID: "14e7bb4ae24616ec54ff6b2f6ef4e8659434ea44" VERSION: "1.2.0" - RS_PUB_KEY: '${{ secrets.RS_PUB_KEY }}' - RENDEZVOUS_SERVER: '${{ secrets.RENDEZVOUS_SERVER }}' + # To make a custom build with your own servers set the below secret values + RS_PUB_KEY: '${{ secrets.RS_PUB_KEY_VAL }}' jobs: build-for-windows: @@ -152,6 +152,7 @@ jobs: uses: actions/checkout@v3 - name: Import the codesign cert + if: ${{ env.MACOS_P12_BASE64== 'true' }} uses: apple-actions/import-codesign-certs@v1 with: p12-file-base64: ${{ secrets.MACOS_P12_BASE64 }} @@ -159,11 +160,13 @@ jobs: keychain: rustdesk - name: Check sign and import sign key + if: ${{ env.MACOS_P12_BASE64== 'true' }} run: | security default-keychain -s rustdesk.keychain security find-identity -v - name: Import notarize key + if: ${{ env.MACOS_P12_BASE64== 'true' }} uses: timheuer/base64-to-file@v1.2 with: # https://gregoryszorc.com/docs/apple-codesign/stable/apple_codesign_rcodesign.html#notarizing-and-stapling @@ -172,6 +175,7 @@ jobs: encodedString: ${{ secrets.MACOS_NOTARIZE_JSON }} - name: Install rcodesign tool + if: ${{ env.MACOS_P12_BASE64== 'true' }} shell: bash run: | pushd /tmp @@ -242,6 +246,7 @@ jobs: ./build.py --flutter ${{ matrix.job.extra-build-args }} - name: Codesign app and create signed dmg + if: ${{ env.MACOS_P12_BASE64== 'true' }} run: | security default-keychain -s rustdesk.keychain security unlock-keychain -p ${{ secrets.MACOS_P12_PASSWORD }} rustdesk.keychain @@ -554,6 +559,7 @@ jobs: - uses: r0adkll/sign-android-release@v1 name: Sign app APK + if: ${{ env.ANDROID_SIGNING_KEY== 'true' }} id: sign-rustdesk with: releaseDirectory: ./signed-apk @@ -566,12 +572,14 @@ jobs: BUILD_TOOLS_VERSION: "30.0.2" - name: Upload Artifacts + if: ${{ env.ANDROID_SIGNING_KEY== 'true' }} uses: actions/upload-artifact@master with: name: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release-signed.apk path: ${{steps.sign-rustdesk.outputs.signedReleaseFile}} - - name: Publish apk package + - name: Publish signed apk package + if: ${{ env.ANDROID_SIGNING_KEY== 'true' }} uses: softprops/action-gh-release@v1 with: prerelease: true @@ -579,6 +587,15 @@ jobs: files: | ${{steps.sign-rustdesk.outputs.signedReleaseFile}} + - name: Publish unsigned apk package + if: ${{ env.ANDROID_SIGNING_KEY!= 'true' }} + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + ../rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk + build-rustdesk-lib-linux-amd64: needs: [generate-bridge-linux, build-vcpkg-deps-linux] name: build-rust-lib ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] From ac6797e4806bff8144886ccb168879f54d90eea1 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Wed, 18 Jan 2023 02:35:35 -0700 Subject: [PATCH 1539/2015] RS_PUB_KEY --- .github/workflows/flutter-nightly.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 99bb20d48..cb32ac9d4 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -16,7 +16,7 @@ env: VCPKG_COMMIT_ID: "14e7bb4ae24616ec54ff6b2f6ef4e8659434ea44" VERSION: "1.2.0" # To make a custom build with your own servers set the below secret values - RS_PUB_KEY: '${{ secrets.RS_PUB_KEY_VAL }}' + RS_PUB_KEY: '${{ secrets.RS_PUB_KEY }}' jobs: build-for-windows: From c8d1480e4e08d21e9018cc81576f1c06715fbc9f Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Wed, 18 Jan 2023 02:36:05 -0700 Subject: [PATCH 1540/2015] add RENDEZVOUS_SERVER --- .github/workflows/flutter-nightly.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index cb32ac9d4..0d1571d9d 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -17,6 +17,7 @@ env: VERSION: "1.2.0" # To make a custom build with your own servers set the below secret values RS_PUB_KEY: '${{ secrets.RS_PUB_KEY }}' + RENDEZVOUS_SERVER: '${{ secrets.RENDEZVOUS_SERVER }}' jobs: build-for-windows: From 7f42404385e68b15fb36936a996f314222565cf2 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Wed, 18 Jan 2023 11:31:32 +0800 Subject: [PATCH 1541/2015] feat: add suse nightly build --- .github/workflows/flutter-nightly.yml | 33 +++++++++- res/rpm-flutter-suse.spec | 87 +++++++++++++++++++++++++++ 2 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 res/rpm-flutter-suse.spec diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index d5782eabf..f03fcce54 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -1041,7 +1041,7 @@ jobs: esac python3 ./build.py --flutter --hwcodec --skip-cargo # rpm package - echo -e "start packaging" + echo -e "start packaging fedora package" pushd /workspace case ${{ matrix.job.arch }} in armv7) @@ -1058,6 +1058,24 @@ jobs: for name in rustdesk*??.rpm; do mv "$name" "/opt/artifacts/rpm/${name%%.rpm}-fedora28-centos8.rpm" done + # rpm suse package + echo -e "start packaging suse package" + pushd /workspace + case ${{ matrix.job.arch }} in + armv7) + sed -i "s/64bit/32bit/g" ./res/rpm-flutter-suse.spec + sed -i "s/linux\/x64/linux\/arm/g" ./res/rpm-flutter-suse.spec + ;; + aarch64) + sed -i "s/linux\/x64/linux\/arm64/g" ./res/rpm-flutter-suse.spec + ;; + esac + HBB=`pwd` rpmbuild ./res/rpm-flutter.spec -bb + pushd ~/rpmbuild/RPMS/${{ matrix.job.arch }} + mkdir -p /opt/artifacts/rpm + for name in rustdesk*??.rpm; do + mv "$name" "/opt/artifacts/rpm/${name%%.rpm}-suse.rpm" + done - name: Rename rustdesk shell: bash @@ -1264,6 +1282,19 @@ jobs: for name in rustdesk*??.rpm; do mv "$name" "/opt/artifacts/rpm/${name%%.rpm}-fedora28-centos8.rpm" done + # rpm suse package + pushd /workspace + case ${{ matrix.job.arch }} in + armv7) + sed -i "s/64bit/32bit/g" ./res/rpm-flutter-suse.spec + ;; + esac + HBB=`pwd` rpmbuild ./res/rpm-flutter-suse.spec -bb + pushd ~/rpmbuild/RPMS/${{ matrix.job.arch }} + mkdir -p /opt/artifacts/rpm + for name in rustdesk*??.rpm; do + mv "$name" "/opt/artifacts/rpm/${name%%.rpm}-suse.rpm" + done - name: Rename rustdesk shell: bash diff --git a/res/rpm-flutter-suse.spec b/res/rpm-flutter-suse.spec new file mode 100644 index 000000000..6c7055b4a --- /dev/null +++ b/res/rpm-flutter-suse.spec @@ -0,0 +1,87 @@ +Name: rustdesk +Version: 1.2.0 +Release: 0 +Summary: RPM package +License: GPL-3.0 +Requires: gtk3 libxcb1 xdotool libXfixes3 alsa-utils curl libXtst6 libappindicator-gtk3 libvdpau1 libva2 +Provides: libdesktop_drop_plugin.so()(64bit), libdesktop_multi_window_plugin.so()(64bit), libflutter_custom_cursor_plugin.so()(64bit), libflutter_linux_gtk.so()(64bit), libscreen_retriever_plugin.so()(64bit), libtray_manager_plugin.so()(64bit), liburl_launcher_linux_plugin.so()(64bit), libwindow_manager_plugin.so()(64bit), libwindow_size_plugin.so()(64bit) + +%description +The best open-source remote desktop client software, written in Rust. + +%prep +# we have no source, so nothing here + +%build +# we have no source, so nothing here + +# %global __python %{__python3} + +%install + +mkdir -p "%{buildroot}/usr/lib/rustdesk" && cp -r ${HBB}/flutter/build/linux/x64/release/bundle/* -t "%{buildroot}/usr/lib/rustdesk" +mkdir -p "%{buildroot}/usr/bin" +install -Dm 644 $HBB/res/rustdesk.service -t "%{buildroot}/usr/share/rustdesk/files" +install -Dm 644 $HBB/res/rustdesk.desktop -t "%{buildroot}/usr/share/rustdesk/files" +install -Dm 644 $HBB/res/rustdesk-link.desktop -t "%{buildroot}/usr/share/rustdesk/files" +install -Dm 644 $HBB/res/128x128@2x.png "%{buildroot}/usr/share/rustdesk/files/rustdesk.png" + +%files +/usr/lib/rustdesk/* +/usr/share/rustdesk/files/rustdesk.service +/usr/share/rustdesk/files/rustdesk.png +/usr/share/rustdesk/files/rustdesk.desktop +/usr/share/rustdesk/files/rustdesk-link.desktop + +%changelog +# let's skip this for now + +# https://www.cnblogs.com/xingmuxin/p/8990255.html +%pre +# can do something for centos7 +case "$1" in + 1) + # for install + ;; + 2) + # for upgrade + systemctl stop rustdesk || true + ;; +esac + +%post +cp /usr/share/rustdesk/files/rustdesk.service /etc/systemd/system/rustdesk.service +cp /usr/share/rustdesk/files/rustdesk.desktop /usr/share/applications/ +cp /usr/share/rustdesk/files/rustdesk-link.desktop /usr/share/applications/ +ln -s /usr/lib/rustdesk/rustdesk /usr/bin/rustdesk +systemctl daemon-reload +systemctl enable rustdesk +systemctl start rustdesk +update-desktop-database + +%preun +case "$1" in + 0) + # for uninstall + systemctl stop rustdesk || true + systemctl disable rustdesk || true + rm /etc/systemd/system/rustdesk.service || true + ;; + 1) + # for upgrade + ;; +esac + +%postun +case "$1" in + 0) + # for uninstall + rm /usr/share/applications/rustdesk.desktop || true + rm /usr/share/applications/rustdesk-link.desktop || true + rm /usr/bin/rustdesk || true + update-desktop-database + ;; + 1) + # for upgrade + ;; +esac From 6044f884eba40f98b12c6071456fba38bb067798 Mon Sep 17 00:00:00 2001 From: skycommand <17097175+skycommand@users.noreply.github.com> Date: Wed, 18 Jan 2023 20:55:36 +0330 Subject: [PATCH 1542/2015] Significantly improved the translation --- docs/README-FA.md | 72 +++++++++++++++++++++++------------------------ 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/docs/README-FA.md b/docs/README-FA.md index d86c82836..02b156dbb 100644 --- a/docs/README-FA.md +++ b/docs/README-FA.md @@ -1,60 +1,60 @@ -

    +

    RustDesk - Your remote desktop
    - اسنپ شات • -
    ساختار • - داکر • - ساخت • - سرور
    - [English] | [Українська] | [česky] | [中文] | [Magyar] | [Español] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
    - ‫برای ترجمه این RustDesk UI ،README و Doc به زبان مادری شما به کمکتون نیاز داریم + تصاویر محیط نرم‌افزار • + ساختار • + داکر • + ساخت • + سرور

    +

    [English] | [Українська] | [česky] | [中文] | [Magyar] | [Español] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]

    +

    برای ترجمه این سند (README)، رابط کاربری RustDesk، و مستندات آن به زبان مادری شما به کمکتان نیازمندیم.

    با ما گپ بزنید: [Reddit](https://www.reddit.com/r/rustdesk) | [Twitter](https://twitter.com/rustdesk) | [Discord](https://discord.gg/nDceKgxnkV) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) -یک نرم افزار دیگر کنترل دسکتاپ از راه دور، که با Rust نوشته شده است. راه اندازی سریع وبدون نیاز به تنظیمات. شما کنترل کاملی بر داده های خود دارید، بدون هیچ گونه نگرانی امنیتی. +راست‌دسک (RustDesk) نرم‌افزاری برای گارکردن با رایانه‌ی رومیزی از راه دور است و با زبان برنامه‌نویسی Rust نوشته شده است. نیاز به تنظیمات چندانی ندارد و شما را قادر می سازد تا بدون نگرانی از امنیت اطلاعات خود بر آن‌ها کنترل کامل داشته باشید. + می‌توانید از سرور rendezvous/relay ما استفاده کنید، [سرور خودتان را راه‌اندازی کنید](https://rustdesk.com/server) یا [ سرورrendezvous/relay خود را بنویسید](https://github.com/rustdesk/rustdesk). -‫راست دسک (RustDesk) از مشارکت همه استقبال می کند. برای راهنمایی جهت مشارکت به [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) مراجعه کنید. +ما از مشارکت همه استقبال می کنیم. برای راهنمایی جهت مشارکت به[`docs/CONTRIBUTING.md`](CONTRIBUTING.md) مراجعه کنید. -[راست دسک چطور کار می کند؟](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F) +[راست‌دسک چطور کار می کند؟](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F) -[دانلود باینری](https://github.com/rustdesk/rustdesk/releases) +[دریافت نرم‌افزار](https://github.com/rustdesk/rustdesk/releases) ## سرورهای عمومی رایگان -سرورهایی زیر را به صورت رایگان میتوانید استفاده می کنید. این لیست ممکن است در طول زمان تغییر کند. اگر به این سرورها نزدیک نیستید، ممکن است سرویس شما کند شود. +شما مي‌توانید از سرورهای زیر به رایگان استفاده کنید. این لیست ممکن است به مرور زمان تغییر می‌کند. اگر به این سرورها نزدیک نیستید، ممکن است اتصال شما کند باشد. | موقعیت | سرویس دهنده | مشخصات | | --------- | ------------- | ------------------ | -| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM | -| Germany | Hetzner | 2 vCPU / 4GB RAM | -| Germany | Codext | 4 vCPU / 8GB RAM | -| Finland (Helsinki) | 0x101 Cyber Security | 4 vCPU / 8GB RAM | -| USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8GB RAM | +| کره‌ی جنوبی، سئول | AWS lightsail | 1 vCPU / 0.5GB RAM | +| آلمان | Hetzner | 2 vCPU / 4GB RAM | +| آلمان | Codext | 4 vCPU / 8GB RAM | +| فنلاند، هلسینکی | 0x101 Cyber Security | 4 vCPU / 8GB RAM | +| ایالات متحده، اَشبرن | 0x101 Cyber Security | 4 vCPU / 8GB RAM | ## وابستگی ها -نسخه‌های دسکتاپ از [sciter](https://sciter.com/) برای رابط کاربری گرافیکی استفاده می‌کنند، لطفا کتابخانه پویا sciter را خودتان دانلود کنید. +نسخه‌های رومیزی از [sciter](https://sciter.com/) برای رابط کاربری گرافیکی استفاده می‌کنند. خواهشمندیم کتابخانه‌ی پویای sciter را خودتان دانلود کنید از این منابع دریافت کنید. -[ویندوز](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) | -[لینوکس](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) | -[مک](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib) +- [ویندوز](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) +- [لینوکس](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) +- [مک](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib) -نسخه های موبایل از Flutter استفاده می کنند. بعداً نسخه دسکتاپ را از Sciter به Flutter منتقل خواهیم کرد. +نسخه های همراه از Flutter استفاده می کنند. نسخه‌ی رومیزی را هم از Sciter به Flutter منتقل خواهیم کرد. -## مراحل بنیادین برای ساخت +## نیازمندی‌های ساخت -‫- محیط توسعه نرم افزار Rust و محیط ساخت ++C خود را آماده کنید +- محیط توسعه نرم افزار Rust و محیط ساخت ++C خود را آماده کنید -‫- نرم افزار [vcpkg](https://github.com/microsoft/vcpkg) را نصب کنید و متغیر `VCPKG_ROOT` را به درستی تنظیم کنید: - - - Windows: `vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static` - - Linux/MacOS: `vcpkg install libvpx libyuv opus` - -- run `cargo run` +- نرم افزار [vcpkg](https://github.com/microsoft/vcpkg) را نصب کنید و متغیر `VCPKG_ROOT` را به درستی تنظیم کنید. +- بسته‌های vcpkg مورد نیاز را نصب کنید: + - ویندوز: `vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static` + - مک و لینوکس: `vcpkg install libvpx libyuv opus` +- این دستور را اجرا کنید: `cargo run` ## [ساخت](https://rustdesk.com/docs/en/dev/build/) @@ -118,11 +118,11 @@ VCPKG_ROOT=$HOME/vcpkg cargo run ### تغییر Wayland به (X11 (Xorg -راست دسک از Wayland پشتیبانی نمی کند. برای جایگزنی Xorg به عنوان پیش‌فرض GNOM، [اینجا](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) را کلیک کنید. +راست‌دسک از Wayland پشتیبانی نمی کند. برای جایگزنی Xorg به عنوان پیش‌فرض GNOM، [اینجا](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) را کلیک کنید. ## نحوه ساخت با داکر -این مخزن گیت را کلون کنید و کانتینر را به روش زیر بسازید +این مخزن Git را دریافت کنید و کانتینر را به روش زیر بسازید ```sh git clone https://github.com/rustdesk/rustdesk @@ -130,13 +130,13 @@ cd rustdesk docker build -t "rustdesk-builder" . ``` -سپس، هر بار که نیاز به ساخت اپلیکیشن داشتید، دستور زیر را اجرا کنید: +سپس، هر بار که نیاز به ساخت ترم‌افزار داشتید، دستور زیر را اجرا کنید: ```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 ``` -توجه داشته باشید که ساخت اول ممکن است قبل از کش شدن وابستگی ها بیشتر طول بکشد، دفعات بعدی سریعتر خواهند بود. علاوه بر این، اگر نیاز به تعیین آرگومان های مختلف برای دستور ساخت دارید، می توانید این کار را در انتهای دستور ساخت و از طریق `` انجام دهید. به عنوان مثال، اگر می خواهید یک نسخه نهایی بهینه سازی شده ایجاد کنید، دستور بالا را تایپ کنید و در انتها `release--` را اضافه کنید. فایل اجرایی به دست آمده در پوشه مقصد در سیستم شما در دسترس خواهد بود و می تواند با دستور: +توجه داشته باشید که نخستین ساخت ممکن است به دلیل محلی نبودن وابستگی‌ها بیشتر طول بکشد. اما دفعات بعدی سریعتر خواهند بود. علاوه بر این، اگر نیاز به تعیین آرگومان های مختلف برای دستور ساخت دارید، می توانید این کار را در انتهای دستور ساخت و از طریق `` انجام دهید. به عنوان مثال، اگر می خواهید یک نسخه نهایی بهینه سازی شده ایجاد کنید، دستور بالا را تایپ کنید و در انتها `release--` را اضافه کنید. فایل اجرایی به دست آمده در پوشه مقصد در سیستم شما در دسترس خواهد بود و می تواند با دستور: ```sh target/debug/rustdesk @@ -163,7 +163,7 @@ target/release/rustdesk - **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter code for mobile - **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Javascript for Flutter web client -## اسکرین شات ها +## تصاویر محیط نرم‌افزار ![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) From 92d009b93d8c94685b3822ea19f8e7b60fd3732b Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Wed, 18 Jan 2023 19:18:02 -0700 Subject: [PATCH 1543/2015] replace env with secrets for consistency. --- .github/workflows/flutter-nightly.yml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 0d1571d9d..2b0e492c3 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -153,7 +153,7 @@ jobs: uses: actions/checkout@v3 - name: Import the codesign cert - if: ${{ env.MACOS_P12_BASE64== 'true' }} + if: ${{ secrets.MACOS_P12_BASE64== 'true' }} uses: apple-actions/import-codesign-certs@v1 with: p12-file-base64: ${{ secrets.MACOS_P12_BASE64 }} @@ -161,13 +161,13 @@ jobs: keychain: rustdesk - name: Check sign and import sign key - if: ${{ env.MACOS_P12_BASE64== 'true' }} + if: ${{ secrets.MACOS_P12_BASE64== 'true' }} run: | security default-keychain -s rustdesk.keychain security find-identity -v - name: Import notarize key - if: ${{ env.MACOS_P12_BASE64== 'true' }} + if: ${{ secrets.MACOS_P12_BASE64== 'true' }} uses: timheuer/base64-to-file@v1.2 with: # https://gregoryszorc.com/docs/apple-codesign/stable/apple_codesign_rcodesign.html#notarizing-and-stapling @@ -176,7 +176,7 @@ jobs: encodedString: ${{ secrets.MACOS_NOTARIZE_JSON }} - name: Install rcodesign tool - if: ${{ env.MACOS_P12_BASE64== 'true' }} + if: ${{ secrets.MACOS_P12_BASE64== 'true' }} shell: bash run: | pushd /tmp @@ -247,7 +247,7 @@ jobs: ./build.py --flutter ${{ matrix.job.extra-build-args }} - name: Codesign app and create signed dmg - if: ${{ env.MACOS_P12_BASE64== 'true' }} + if: ${{ secrets.MACOS_P12_BASE64== 'true' }} run: | security default-keychain -s rustdesk.keychain security unlock-keychain -p ${{ secrets.MACOS_P12_PASSWORD }} rustdesk.keychain @@ -560,7 +560,7 @@ jobs: - uses: r0adkll/sign-android-release@v1 name: Sign app APK - if: ${{ env.ANDROID_SIGNING_KEY== 'true' }} + if: ${{ secrets.ANDROID_SIGNING_KEY== 'true' }} id: sign-rustdesk with: releaseDirectory: ./signed-apk @@ -573,14 +573,14 @@ jobs: BUILD_TOOLS_VERSION: "30.0.2" - name: Upload Artifacts - if: ${{ env.ANDROID_SIGNING_KEY== 'true' }} + if: ${{ secrets.ANDROID_SIGNING_KEY== 'true' }} uses: actions/upload-artifact@master with: name: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release-signed.apk path: ${{steps.sign-rustdesk.outputs.signedReleaseFile}} - name: Publish signed apk package - if: ${{ env.ANDROID_SIGNING_KEY== 'true' }} + if: ${{ secrets.ANDROID_SIGNING_KEY== 'true' }} uses: softprops/action-gh-release@v1 with: prerelease: true @@ -589,7 +589,7 @@ jobs: ${{steps.sign-rustdesk.outputs.signedReleaseFile}} - name: Publish unsigned apk package - if: ${{ env.ANDROID_SIGNING_KEY!= 'true' }} + if: ${{ secrets.ANDROID_SIGNING_KEY!= 'true' }} uses: softprops/action-gh-release@v1 with: prerelease: true From 4f5b359cfce149b84b3b09c1ecc8708177443e00 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Wed, 18 Jan 2023 19:25:43 -0700 Subject: [PATCH 1544/2015] env not secret MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit You can only use the env context in the value of the with and name keys, or in a step’s if conditional, the secret value is not defined yet as its before the with. --- .github/workflows/flutter-nightly.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 2b0e492c3..65bb6f6c8 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -153,7 +153,7 @@ jobs: uses: actions/checkout@v3 - name: Import the codesign cert - if: ${{ secrets.MACOS_P12_BASE64== 'true' }} + if: ${{ env.MACOS_P12_BASE64== 'true' }} uses: apple-actions/import-codesign-certs@v1 with: p12-file-base64: ${{ secrets.MACOS_P12_BASE64 }} From fd346edebd406d22e02255d08a92988780cadc27 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Wed, 18 Jan 2023 19:28:57 -0700 Subject: [PATCH 1545/2015] env not secret must use env. not secret in if's --- .github/workflows/flutter-nightly.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 65bb6f6c8..0d1571d9d 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -161,13 +161,13 @@ jobs: keychain: rustdesk - name: Check sign and import sign key - if: ${{ secrets.MACOS_P12_BASE64== 'true' }} + if: ${{ env.MACOS_P12_BASE64== 'true' }} run: | security default-keychain -s rustdesk.keychain security find-identity -v - name: Import notarize key - if: ${{ secrets.MACOS_P12_BASE64== 'true' }} + if: ${{ env.MACOS_P12_BASE64== 'true' }} uses: timheuer/base64-to-file@v1.2 with: # https://gregoryszorc.com/docs/apple-codesign/stable/apple_codesign_rcodesign.html#notarizing-and-stapling @@ -176,7 +176,7 @@ jobs: encodedString: ${{ secrets.MACOS_NOTARIZE_JSON }} - name: Install rcodesign tool - if: ${{ secrets.MACOS_P12_BASE64== 'true' }} + if: ${{ env.MACOS_P12_BASE64== 'true' }} shell: bash run: | pushd /tmp @@ -247,7 +247,7 @@ jobs: ./build.py --flutter ${{ matrix.job.extra-build-args }} - name: Codesign app and create signed dmg - if: ${{ secrets.MACOS_P12_BASE64== 'true' }} + if: ${{ env.MACOS_P12_BASE64== 'true' }} run: | security default-keychain -s rustdesk.keychain security unlock-keychain -p ${{ secrets.MACOS_P12_PASSWORD }} rustdesk.keychain @@ -560,7 +560,7 @@ jobs: - uses: r0adkll/sign-android-release@v1 name: Sign app APK - if: ${{ secrets.ANDROID_SIGNING_KEY== 'true' }} + if: ${{ env.ANDROID_SIGNING_KEY== 'true' }} id: sign-rustdesk with: releaseDirectory: ./signed-apk @@ -573,14 +573,14 @@ jobs: BUILD_TOOLS_VERSION: "30.0.2" - name: Upload Artifacts - if: ${{ secrets.ANDROID_SIGNING_KEY== 'true' }} + if: ${{ env.ANDROID_SIGNING_KEY== 'true' }} uses: actions/upload-artifact@master with: name: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release-signed.apk path: ${{steps.sign-rustdesk.outputs.signedReleaseFile}} - name: Publish signed apk package - if: ${{ secrets.ANDROID_SIGNING_KEY== 'true' }} + if: ${{ env.ANDROID_SIGNING_KEY== 'true' }} uses: softprops/action-gh-release@v1 with: prerelease: true @@ -589,7 +589,7 @@ jobs: ${{steps.sign-rustdesk.outputs.signedReleaseFile}} - name: Publish unsigned apk package - if: ${{ secrets.ANDROID_SIGNING_KEY!= 'true' }} + if: ${{ env.ANDROID_SIGNING_KEY!= 'true' }} uses: softprops/action-gh-release@v1 with: prerelease: true From 5a214d91852c7da9593f357d30d9db9d522a4dd1 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Wed, 18 Jan 2023 19:38:41 -0700 Subject: [PATCH 1546/2015] set env values for if's --- .github/workflows/flutter-nightly.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 0d1571d9d..466ad3d59 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -15,6 +15,9 @@ env: # for multiarch gcc compatibility VCPKG_COMMIT_ID: "14e7bb4ae24616ec54ff6b2f6ef4e8659434ea44" VERSION: "1.2.0" + #signing keys + ANDROID_SIGNING_KEY: '${{ secrets.ANDROID_SIGNING_KEY }}' + MACOS_P12_BASE64: '${{ secrets.MACOS_P12_BASE64 }}' # To make a custom build with your own servers set the below secret values RS_PUB_KEY: '${{ secrets.RS_PUB_KEY }}' RENDEZVOUS_SERVER: '${{ secrets.RENDEZVOUS_SERVER }}' From 74a7523662d58bfd5cc0b036fc17fe22d21f7df5 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Wed, 18 Jan 2023 19:45:17 -0700 Subject: [PATCH 1547/2015] fix env.MACOS_P12_BASE64 --- .github/workflows/flutter-nightly.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 466ad3d59..e1bd90593 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -156,7 +156,7 @@ jobs: uses: actions/checkout@v3 - name: Import the codesign cert - if: ${{ env.MACOS_P12_BASE64== 'true' }} + if: env.MACOS_P12_BASE64 != null uses: apple-actions/import-codesign-certs@v1 with: p12-file-base64: ${{ secrets.MACOS_P12_BASE64 }} @@ -164,13 +164,13 @@ jobs: keychain: rustdesk - name: Check sign and import sign key - if: ${{ env.MACOS_P12_BASE64== 'true' }} + if: env.MACOS_P12_BASE64 != null run: | security default-keychain -s rustdesk.keychain security find-identity -v - name: Import notarize key - if: ${{ env.MACOS_P12_BASE64== 'true' }} + if: env.MACOS_P12_BASE64 != null uses: timheuer/base64-to-file@v1.2 with: # https://gregoryszorc.com/docs/apple-codesign/stable/apple_codesign_rcodesign.html#notarizing-and-stapling @@ -179,7 +179,7 @@ jobs: encodedString: ${{ secrets.MACOS_NOTARIZE_JSON }} - name: Install rcodesign tool - if: ${{ env.MACOS_P12_BASE64== 'true' }} + if: env.MACOS_P12_BASE64 != null shell: bash run: | pushd /tmp @@ -250,7 +250,7 @@ jobs: ./build.py --flutter ${{ matrix.job.extra-build-args }} - name: Codesign app and create signed dmg - if: ${{ env.MACOS_P12_BASE64== 'true' }} + if: env.MACOS_P12_BASE64 != null run: | security default-keychain -s rustdesk.keychain security unlock-keychain -p ${{ secrets.MACOS_P12_PASSWORD }} rustdesk.keychain From 9e43a071764896071661e4f3711f4f02aec6402e Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Wed, 18 Jan 2023 19:50:32 -0700 Subject: [PATCH 1548/2015] update ANDROID_SIGNING_KEY --- .github/workflows/flutter-nightly.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index e1bd90593..393df44f7 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -563,7 +563,7 @@ jobs: - uses: r0adkll/sign-android-release@v1 name: Sign app APK - if: ${{ env.ANDROID_SIGNING_KEY== 'true' }} + if: env.ANDROID_SIGNING_KEY != null id: sign-rustdesk with: releaseDirectory: ./signed-apk @@ -576,14 +576,14 @@ jobs: BUILD_TOOLS_VERSION: "30.0.2" - name: Upload Artifacts - if: ${{ env.ANDROID_SIGNING_KEY== 'true' }} + if: env.ANDROID_SIGNING_KEY != null uses: actions/upload-artifact@master with: name: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release-signed.apk path: ${{steps.sign-rustdesk.outputs.signedReleaseFile}} - name: Publish signed apk package - if: ${{ env.ANDROID_SIGNING_KEY== 'true' }} + if: env.ANDROID_SIGNING_KEY != null uses: softprops/action-gh-release@v1 with: prerelease: true @@ -592,7 +592,7 @@ jobs: ${{steps.sign-rustdesk.outputs.signedReleaseFile}} - name: Publish unsigned apk package - if: ${{ env.ANDROID_SIGNING_KEY!= 'true' }} + if: env.ANDROID_SIGNING_KEY != null uses: softprops/action-gh-release@v1 with: prerelease: true From 5e9b9d52087e5a0adc64a3da48acae4f16bb21a4 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Wed, 18 Jan 2023 19:50:57 -0700 Subject: [PATCH 1549/2015] Update flutter-nightly.yml --- .github/workflows/flutter-nightly.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 393df44f7..a782de229 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -15,7 +15,7 @@ env: # for multiarch gcc compatibility VCPKG_COMMIT_ID: "14e7bb4ae24616ec54ff6b2f6ef4e8659434ea44" VERSION: "1.2.0" - #signing keys + #signing keys env variable checks ANDROID_SIGNING_KEY: '${{ secrets.ANDROID_SIGNING_KEY }}' MACOS_P12_BASE64: '${{ secrets.MACOS_P12_BASE64 }}' # To make a custom build with your own servers set the below secret values From d58b834c4c7bcbb7e6f16604358d08a6a820e471 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Wed, 18 Jan 2023 19:58:00 -0700 Subject: [PATCH 1550/2015] verify .secrets --- .github/workflows/flutter-nightly.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index a782de229..84b0c13c7 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -156,7 +156,7 @@ jobs: uses: actions/checkout@v3 - name: Import the codesign cert - if: env.MACOS_P12_BASE64 != null + if: secrets.MACOS_P12_BASE64 != null uses: apple-actions/import-codesign-certs@v1 with: p12-file-base64: ${{ secrets.MACOS_P12_BASE64 }} From 86885eb5b4d8ee2e6b2bcadb3a5ddeb3cb296490 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Wed, 18 Jan 2023 19:59:07 -0700 Subject: [PATCH 1551/2015] .secrets doesnt work in if --- .github/workflows/flutter-nightly.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 84b0c13c7..a782de229 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -156,7 +156,7 @@ jobs: uses: actions/checkout@v3 - name: Import the codesign cert - if: secrets.MACOS_P12_BASE64 != null + if: env.MACOS_P12_BASE64 != null uses: apple-actions/import-codesign-certs@v1 with: p12-file-base64: ${{ secrets.MACOS_P12_BASE64 }} From fac375c017d1b79b014572c111e4579b7859bcb5 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Wed, 18 Jan 2023 20:29:51 -0700 Subject: [PATCH 1552/2015] fix unsigned app publish --- .github/workflows/flutter-nightly.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index a782de229..08b1af79b 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -592,7 +592,7 @@ jobs: ${{steps.sign-rustdesk.outputs.signedReleaseFile}} - name: Publish unsigned apk package - if: env.ANDROID_SIGNING_KEY != null + if: env.ANDROID_SIGNING_KEY == null uses: softprops/action-gh-release@v1 with: prerelease: true From 75c43a01fab192eae8d7aa9176d73e0d2073dd3f Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 19 Jan 2023 18:23:04 +0800 Subject: [PATCH 1553/2015] mac try x2 png not work, revert --- src/tray.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/tray.rs b/src/tray.rs index 76dcf3c21..e4203fec6 100644 --- a/src/tray.rs +++ b/src/tray.rs @@ -214,14 +214,15 @@ pub fn make_tray() { let mode = dark_light::detect(); let icon_path = match mode { dark_light::Mode::Dark => { - if f > 1. { + // still show big overflow icon in my test, so still use x1 png. + if f > 2. { "mac-tray-light-x2.png" } else { "mac-tray-light.png" } } dark_light::Mode::Light => { - if f > 1. { + if f > 2. { "mac-tray-dark-x2.png" } else { "mac-tray-dark.png" From 34f167b5fce278485fce80ee9e3dd38157c4d2fe Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 19 Jan 2023 18:25:13 +0800 Subject: [PATCH 1554/2015] let's do it with objc with svg support later. --- src/tray.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/tray.rs b/src/tray.rs index e4203fec6..6e84076c4 100644 --- a/src/tray.rs +++ b/src/tray.rs @@ -215,6 +215,7 @@ pub fn make_tray() { 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. if f > 2. { "mac-tray-light-x2.png" } else { From 7c834a6001011f401595a9d49c006344c4bcff40 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 19 Jan 2023 18:26:59 +0800 Subject: [PATCH 1555/2015] more comment --- src/tray.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/tray.rs b/src/tray.rs index 6e84076c4..e41a616de 100644 --- a/src/tray.rs +++ b/src/tray.rs @@ -216,6 +216,7 @@ pub fn make_tray() { 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 { From 941a567d30ace75c94cb3ef6c38e94fe40f77024 Mon Sep 17 00:00:00 2001 From: "Miguel F. G" <116861809+flusheDData@users.noreply.github.com> Date: Thu, 19 Jan 2023 16:17:30 +0100 Subject: [PATCH 1556/2015] Update es.rs A lot of new terms and corrections. --- src/lang/es.rs | 58 +++++++++++++++++++++++++------------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/src/lang/es.rs b/src/lang/es.rs index bae1b5cbf..5ab59b946 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -40,7 +40,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Website", "Sitio web"), ("About", "Acerca de"), ("Slogan_tip", "Hecho con corazón en este mundo caótico!"), - ("Privacy Statement", ""), + ("Privacy Statement", "Declaración de privacidad"), ("Mute", "Silenciar"), ("Audio Input", "Entrada de audio"), ("Enhancements", "Mejoras"), @@ -210,11 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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"), - ("Verify", ""), - ("Remember me", ""), - ("Trust this device", ""), - ("Verification code", ""), - ("verification_tip", ""), + ("Verify", "Verificar"), + ("Remember me", "Recordarme"), + ("Trust this device", "Confiar en este dispositivo"), + ("Verification code", "Código de verificación"), + ("verification_tip", "Se ha detectado un nuevo dispositivo y se ha enviado un código de verificación a la dirección de correo registrada. Introduzca el código de verificación para continuar con el inicio de sesión."), ("Logout", "Salir"), ("Tags", "Tags"), ("Search ID", "Buscar ID"), @@ -305,7 +305,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Language", "Idioma"), ("Keep RustDesk background service", "Dejar RustDesk como Servicio en 2do plano"), ("Ignore Battery Optimizations", "Ignorar optimizacioens de bateria"), - ("android_open_battery_optimizations_tip", ""), + ("android_open_battery_optimizations_tip", "Si deseas deshabilitar esta característica, por favor, ve a la página siguiente de ajustes, busca y entra en [Batería] y desmarca [Sin restricción]"), ("Connection not allowed", "Conexión no disponible"), ("Legacy mode", "Modo heredado"), ("Map mode", "Modo mapa"), @@ -318,8 +318,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Restart Remote Device", "Reiniciar dispositivo"), ("Are you sure you want to restart", "Esta Seguro que desea reiniciar?"), ("Restarting Remote Device", "Reiniciando dispositivo remoto"), - ("remote_restarting_tip", "Dispositivo remoto reiniciando, favor de cerrar este mensaje y reconectarse con la contraseña permamente despues de un momento."), - ("Copied", ""), + ("remote_restarting_tip", "El dispositivo remoto se está reiniciando. Por favor cierre este mensaje y vuelva a conectarse con la contraseña peremanente en unos momentos."), + ("Copied", "Copiado"), ("Exit Fullscreen", "Salir de pantalla completa"), ("Fullscreen", "Pantalla completa"), ("Mobile Actions", "Acciones móviles"), @@ -373,8 +373,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Deny LAN Discovery", "Denegar descubrimiento de LAN"), ("Write a message", "Escribir un mensaje"), ("Prompt", ""), - ("Please wait for confirmation of UAC...", ""), - ("elevated_foreground_window_tip", ""), + ("Please wait for confirmation of UAC...", "Por favor, espera confirmación de UAC"), + ("elevated_foreground_window_tip", "La ventana actual del escritorio remoto necesita privilegios elevados para funcionar, así que no puedes usar ratón y teclado temporalmente. Puedes solicitar al usuario remoto que minimize la ventana actual o hacer clic en el botón de elevación de la ventana de gestión de conexión. Para evitar este problema, se recomienda instalar el programa en el dispositivo remto."), ("Disconnected", "Desconectado"), ("Other", "Otro"), ("Confirm before closing multiple tabs", "Confirmar antes de cerrar múltiples pestañas"), @@ -413,23 +413,23 @@ 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\"."), - ("request_elevation_tip", ""), - ("Wait", ""), - ("Elevation Error", ""), - ("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", ""), - ("Request Elevation", ""), - ("wait_accept_uac_tip", ""), - ("Elevate successfully", ""), - ("uppercase", ""), - ("lowercase", ""), - ("digit", ""), - ("special character", ""), - ("length>=8", ""), - ("Weak", ""), - ("Medium", ""), - ("Strong", ""), + ("request_elevation_tip", "También puedes solicitar elevación si hay alguien en el lado remoto."), + ("Wait", "Esperar"), + ("Elevation Error", "Error de elevación"), + ("Ask the remote user for authentication", "Pida autenticación al usuario remoto"), + ("Choose this if the remote account is administrator", "Elegir si la cuenta remota es de administrador"), + ("Transmit the username and password of administrator", "Transmitir usuario y contraseña del administrador"), + ("still_click_uac_tip", "Aún se necesita que el usuario remoto haga click en OK en la ventana UAC del RusDesk en ejecución."), + ("Request Elevation", "Solicitar Elevación"), + ("wait_accept_uac_tip", "Por favor espere a que el usuario remoto acepte el diálogo UAC."), + ("Elevate successfully", "Elevar con éxito"), + ("uppercase", "mayúsculas"), + ("lowercase", "minúsculas"), + ("digit", "dígito"), + ("special character", "carácter especial"), + ("length>=8", "longitud>=8"), + ("Weak", "Débil"), + ("Medium", "Media"), + ("Strong", "Fuerte"), ].iter().cloned().collect(); } From 14d7621425819bbebad1ebbd59cd2f14362c802c Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 20 Jan 2023 00:09:58 +0800 Subject: [PATCH 1557/2015] change product name from rustdesk to RustDesk --- flutter/macos/Runner.xcodeproj/project.pbxproj | 6 +++--- flutter/macos/Runner/Configs/AppInfo.xcconfig | 2 +- flutter/macos/rustdesk.xcodeproj/project.pbxproj | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/flutter/macos/Runner.xcodeproj/project.pbxproj b/flutter/macos/Runner.xcodeproj/project.pbxproj index 8f11a09ed..fbf52403c 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 */ diff --git a/flutter/macos/Runner/Configs/AppInfo.xcconfig b/flutter/macos/Runner/Configs/AppInfo.xcconfig index 389ae0a70..66dbee50c 100644 --- a/flutter/macos/Runner/Configs/AppInfo.xcconfig +++ b/flutter/macos/Runner/Configs/AppInfo.xcconfig @@ -5,7 +5,7 @@ // 'flutter create' template. // The application's name. By default this is also the title of the Flutter window. -PRODUCT_NAME = rustdesk +PRODUCT_NAME = RustDesk // The application's bundle identifier PRODUCT_BUNDLE_IDENTIFIER = com.carriez.flutterHbb diff --git a/flutter/macos/rustdesk.xcodeproj/project.pbxproj b/flutter/macos/rustdesk.xcodeproj/project.pbxproj index 664f88618..6c58fef3d 100644 --- a/flutter/macos/rustdesk.xcodeproj/project.pbxproj +++ b/flutter/macos/rustdesk.xcodeproj/project.pbxproj @@ -84,7 +84,7 @@ "CARGO_XCODE_TARGET_OS[sdk=iphonesimulator*]" = "ios-sim"; "CARGO_XCODE_TARGET_OS[sdk=macosx*]" = darwin; ONLY_ACTIVE_ARCH = YES; - PRODUCT_NAME = rustdesk; + PRODUCT_NAME = RustDesk; SDKROOT = macosx; SUPPORTS_MACCATALYST = YES; }; @@ -105,7 +105,7 @@ "CARGO_XCODE_TARGET_OS[sdk=iphoneos*]" = ios; "CARGO_XCODE_TARGET_OS[sdk=iphonesimulator*]" = "ios-sim"; "CARGO_XCODE_TARGET_OS[sdk=macosx*]" = darwin; - PRODUCT_NAME = rustdesk; + PRODUCT_NAME = RustDesk; SDKROOT = macosx; SUPPORTS_MACCATALYST = YES; OTHER_LDFLAGS = ( From aac12c2b21be369354a5df3e5074cfe96284863f Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 20 Jan 2023 01:25:15 +0800 Subject: [PATCH 1558/2015] applicationShouldOpenUntitledFile --- .../xcshareddata/xcschemes/Runner.xcscheme | 8 +++---- flutter/macos/Runner/AppDelegate.swift | 5 ++++ flutter/pubspec.lock | 7 ++++++ src/flutter.rs | 9 +++++++ src/platform/macos.rs | 20 ++++++++++++++++ src/ui/macos.rs | 24 ++----------------- 6 files changed, 47 insertions(+), 26 deletions(-) diff --git a/flutter/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/flutter/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 898fbe4e7..9c428a004 100644 --- a/flutter/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/flutter/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -15,7 +15,7 @@ @@ -31,7 +31,7 @@ @@ -54,7 +54,7 @@ @@ -71,7 +71,7 @@ diff --git a/flutter/macos/Runner/AppDelegate.swift b/flutter/macos/Runner/AppDelegate.swift index 156e0c79b..46622746d 100644 --- a/flutter/macos/Runner/AppDelegate.swift +++ b/flutter/macos/Runner/AppDelegate.swift @@ -7,4 +7,9 @@ class AppDelegate: FlutterAppDelegate { dummy_method_to_enforce_bundling() return true } + + override func applicationShouldOpenUntitledFile(_ sender: NSApplication) -> Bool { + handle_applicationShouldOpenUntitledFile(); + return true + } } diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 228817422..ef57f375c 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -699,6 +699,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.0" + password_strength: + dependency: "direct main" + description: + name: password_strength + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0" path: dependency: "direct main" description: diff --git a/src/flutter.rs b/src/flutter.rs index 3036ca9b3..1369b5646 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -39,6 +39,15 @@ pub extern "C" fn rustdesk_core_main() -> bool { false } +#[cfg(target_os = "macos")] +#[no_mangle] +pub extern "C" fn handle_applicationShouldOpenUntitledFile() { + hbb_common::log::debug!("icon clicked on finder"); + if std::env::args().nth(1) == Some("--server".to_owned()) { + crate::platform::macos::check_main_window(); + } +} + #[cfg(windows)] #[no_mangle] pub extern "C" fn rustdesk_core_main_args(args_len: *mut c_int) -> *mut *mut c_char { diff --git a/src/platform/macos.rs b/src/platform/macos.rs index 165470cac..c7dbd9b73 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -556,3 +556,23 @@ pub fn hide_dock() { NSApp().setActivationPolicy_(NSApplicationActivationPolicyAccessory); } } + +pub fn check_main_window() { + use sysinfo::{ProcessExt, System, SystemExt}; + let mut sys = System::new(); + sys.refresh_processes(); + let app = format!("/Applications/{}.app", crate::get_app_name()); + let my_uid = sys + .process((std::process::id() as i32).into()) + .map(|x| x.user_id()) + .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; + } + } + std::process::Command::new("open") + .args(["-n", &app]) + .status() + .ok(); +} diff --git a/src/ui/macos.rs b/src/ui/macos.rs index 835fd87b0..7daef8eab 100644 --- a/src/ui/macos.rs +++ b/src/ui/macos.rs @@ -127,7 +127,7 @@ extern "C" fn application_should_handle_open_untitled_file( } hbb_common::log::debug!("icon clicked on finder"); if std::env::args().nth(1) == Some("--server".to_owned()) { - check_main_window(); + crate::platform::macos::check_main_window(); } let inner: *mut c_void = *this.get_ivar(APP_HANDLER_IVAR); let inner = &mut *(inner as *mut DelegateState); @@ -233,24 +233,4 @@ pub fn make_tray() { set_delegate(None); } crate::tray::make_tray(); -} - -pub fn check_main_window() { - use sysinfo::{ProcessExt, System, SystemExt}; - let mut sys = System::new(); - sys.refresh_processes(); - let app = format!("/Applications/{}.app", crate::get_app_name()); - let my_uid = sys - .process((std::process::id() as i32).into()) - .map(|x| x.user_id()) - .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; - } - } - std::process::Command::new("open") - .args(["-n", &app]) - .status() - .ok(); -} +} \ No newline at end of file From 45c0e10102925c16cc6882b6c9542a396d50bf73 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 20 Jan 2023 10:26:27 +0800 Subject: [PATCH 1559/2015] applicationDidFinishLaunching --- flutter/macos/Runner/AppDelegate.swift | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/flutter/macos/Runner/AppDelegate.swift b/flutter/macos/Runner/AppDelegate.swift index 46622746d..5708e35cb 100644 --- a/flutter/macos/Runner/AppDelegate.swift +++ b/flutter/macos/Runner/AppDelegate.swift @@ -3,13 +3,21 @@ import FlutterMacOS @NSApplicationMain class AppDelegate: FlutterAppDelegate { + var lauched = false; override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { dummy_method_to_enforce_bundling() return true } override func applicationShouldOpenUntitledFile(_ sender: NSApplication) -> Bool { - handle_applicationShouldOpenUntitledFile(); + if (lauched) { + handle_applicationShouldOpenUntitledFile(); + } return true } + + override func applicationDidFinishLaunching(_ aNotification: Notification) { + lauched = true; + NSApplication.shared.activate(ignoringOtherApps: true); + } } From 1da141e6a7d643d401ae45cc090a267ac8fd161d Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 20 Jan 2023 12:03:03 +0800 Subject: [PATCH 1560/2015] opt: prevent duplicate window instance on windows --- flutter/lib/common.dart | 2 +- flutter/lib/utils/multi_window_manager.dart | 2 +- flutter/windows/runner/main.cpp | 16 +++++++++------- src/core_main.rs | 2 +- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 23aa9535d..fde039017 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1310,7 +1310,7 @@ bool callUniLinksUriHandler(Uri uri) { Future.delayed(Duration.zero, () { rustDeskWinManager.newRemoteDesktop(peerId); }); - return true; + return false; } return false; } diff --git a/flutter/lib/utils/multi_window_manager.dart b/flutter/lib/utils/multi_window_manager.dart index cf6d78cd2..13e9d36bf 100644 --- a/flutter/lib/utils/multi_window_manager.dart +++ b/flutter/lib/utils/multi_window_manager.dart @@ -208,7 +208,7 @@ class RustDeskMultiWindowManager { } /// Remove active window which has [`windowId`] - /// + /// /// [Availability] /// This function should only be called from main window. /// For other windows, please post a unregister(hide) event to main window handler: diff --git a/flutter/windows/runner/main.cpp b/flutter/windows/runner/main.cpp index 9b75aa086..d76b8c040 100644 --- a/flutter/windows/runner/main.cpp +++ b/flutter/windows/runner/main.cpp @@ -52,18 +52,20 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, free_c_args(c_args, args_len); // uni links dispatch - // only do uni links when dispatch a rustdesk links - auto prefix = std::string(uniLinksPrefix); - if (!command_line_arguments.empty() && command_line_arguments.front().compare(0, prefix.size(), prefix.c_str()) == 0) { - HWND hwnd = ::FindWindow(_T("FLUTTER_RUNNER_WIN32_WINDOW"), _T("RustDesk")); - if (hwnd != NULL) { + HWND hwnd = ::FindWindow(_T("FLUTTER_RUNNER_WIN32_WINDOW"), _T("RustDesk")); + if (hwnd != NULL) { + if (!command_line_arguments.empty()) { + // Dispatch command line arguments DispatchToUniLinksDesktop(hwnd); - + } else { + // Not called with arguments, or just open the app shortcut on desktop. + // So we just show the main window instead. ::ShowWindow(hwnd, SW_NORMAL); ::SetForegroundWindow(hwnd); - return EXIT_FAILURE; } + return EXIT_FAILURE; } + // Attach to console when present (e.g., 'flutter run') or create a // new console when running with a debugger. if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) diff --git a/src/core_main.rs b/src/core_main.rs index 9083efe0e..7707a41c8 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -322,7 +322,7 @@ fn core_main_invoke_new_connection(mut args: std::env::Args) -> Option Date: Fri, 20 Jan 2023 06:26:18 +0100 Subject: [PATCH 1561/2015] Update es.rs Skip = Changed from Saltar to Omitir --- src/lang/es.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/es.rs b/src/lang/es.rs index 5ab59b946..6d94f8816 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -57,7 +57,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Not available", "No disponible"), ("Too frequent", "Demasiado frecuente"), ("Cancel", "Cancelar"), - ("Skip", "Saltar"), + ("Skip", "Omitir"), ("Close", "Cerrar"), ("Retry", "Reintentar"), ("OK", ""), From ba8fb027f62ff5b5daad97007ab49969553c0c4e Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Thu, 19 Jan 2023 22:44:10 -0700 Subject: [PATCH 1562/2015] fix apk location --- .github/workflows/flutter-nightly.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index efa085ea8..95d84ba40 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -598,7 +598,7 @@ jobs: prerelease: true tag_name: ${{ env.TAG_NAME }} files: | - ../rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk + rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release-signed.apk build-rustdesk-lib-linux-amd64: needs: [generate-bridge-linux, build-vcpkg-deps-linux] From dbbbddee495cd07e2311ca4092834585c3f0ec11 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Thu, 19 Jan 2023 23:17:07 -0700 Subject: [PATCH 1563/2015] Update flutter-nightly.yml --- .github/workflows/flutter-nightly.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 95d84ba40..f3a4bb524 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -598,7 +598,7 @@ jobs: prerelease: true tag_name: ${{ env.TAG_NAME }} files: | - rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release-signed.apk + rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk build-rustdesk-lib-linux-amd64: needs: [generate-bridge-linux, build-vcpkg-deps-linux] From d4789e141b081d0ba6a040d66fa3b3a0e781afc4 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Thu, 19 Jan 2023 23:41:07 -0700 Subject: [PATCH 1564/2015] Update flutter-nightly.yml --- .github/workflows/flutter-nightly.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index f3a4bb524..efa085ea8 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -598,7 +598,7 @@ jobs: prerelease: true tag_name: ${{ env.TAG_NAME }} files: | - rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk + ../rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk build-rustdesk-lib-linux-amd64: needs: [generate-bridge-linux, build-vcpkg-deps-linux] From 333092f983c3f9c5009dd924ea9de1fecf3caa8b Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 17 Jan 2023 13:28:33 +0800 Subject: [PATCH 1565/2015] switch sides Signed-off-by: 21pages --- flutter/lib/common.dart | 13 ++++ flutter/lib/desktop/pages/remote_page.dart | 7 +- .../lib/desktop/pages/remote_tab_page.dart | 3 + flutter/lib/desktop/pages/server_page.dart | 13 ++++ .../lib/desktop/widgets/remote_menubar.dart | 55 +++++++++++---- flutter/lib/models/model.dart | 20 +++++- flutter/lib/models/server_model.dart | 3 + flutter/lib/utils/multi_window_manager.dart | 12 ++-- libs/hbb_common/protos/message.proto | 14 ++++ src/client.rs | 33 ++++++++- src/client/io_loop.rs | 4 ++ src/flutter.rs | 66 +++++++++++++---- src/flutter_ffi.rs | 17 +++-- src/ipc.rs | 2 + 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/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/server/connection.rs | 70 ++++++++++++++----- src/ui/remote.rs | 4 +- src/ui_cm_interface.rs | 17 ++++- src/ui_session_interface.rs | 19 +++++ 49 files changed, 373 insertions(+), 61 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 23aa9535d..accbdf8df 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1606,3 +1606,16 @@ Widget dialogButton(String text, )); } } + +int get_version_num(String version) { + final list = version.split('.'); + var n = 0; + for (var i = 0; i < list.length; i++) { + n = n * 1000 + (int.tryParse(list[i]) ?? 0); + } + return n; +} + +int version_cmp(String v1, String v2) { + return get_version_num(v1) - get_version_num(v2); +} diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 55a5bbaef..fb67154bc 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -33,10 +33,12 @@ class RemotePage extends StatefulWidget { Key? key, required this.id, required this.menubarState, + this.switchUuid, }) : super(key: key); final String id; final MenubarState menubarState; + final String? switchUuid; final SimpleWrapper?> _lastState = SimpleWrapper(null); FFI get ffi => (_lastState.value! as _RemotePageState)._ffi; @@ -100,7 +102,10 @@ class _RemotePageState extends State showKBLayoutTypeChooserIfNeeded( _ffi.ffiModel.pi.platform, _ffi.dialogManager); }); - _ffi.start(widget.id); + _ffi.start( + widget.id, + switchUuid: widget.switchUuid, + ); WidgetsBinding.instance.addPostFrameCallback((_) { SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); _ffi.dialogManager diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index 198b2aea7..a3532d49a 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -64,6 +64,7 @@ class _ConnectionTabPageState extends State { key: ValueKey(peerId), id: peerId, menubarState: _menubarState, + switchUuid: params['switch_uuid'], ), )); _update_remote_count(); @@ -84,6 +85,7 @@ class _ConnectionTabPageState extends State { if (call.method == "new_remote_desktop") { final args = jsonDecode(call.arguments); final id = args['id']; + final switchUuid = args['switch_uuid']; window_on_top(windowId()); ConnectionTypeState.init(id); tabController.add(TabInfo( @@ -96,6 +98,7 @@ class _ConnectionTabPageState extends State { key: ValueKey(id), id: id, menubarState: _menubarState, + switchUuid: switchUuid, ), )); } else if (call.method == "onDestroy") { diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index fa367f488..8c8679e96 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -516,6 +516,15 @@ class _CmControlPanel extends StatelessWidget { return Column( mainAxisAlignment: MainAxisAlignment.end, children: [ + Offstage( + offstage: !client.fromSwitch, + child: buildButton(context, + color: Colors.purple, + onClick: () => handleSwitchBack(context), + icon: Icon(Icons.reply, color: Colors.white), + text: "Switch Sides", + textColor: Colors.white), + ), Offstage( offstage: !showElevation, child: buildButton(context, color: Colors.green[700], onClick: () { @@ -674,6 +683,10 @@ class _CmControlPanel extends StatelessWidget { windowManager.close(); } } + + void handleSwitchBack(BuildContext context) { + bind.cmSwitchBack(connId: 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 6a0fa9104..6ea372b1f 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -509,6 +509,7 @@ class _RemoteMenubarState extends State { List> _getControlMenu(BuildContext context) { final pi = widget.ffi.ffiModel.pi; final perms = widget.ffi.ffiModel.permissions; + final peer_version = widget.ffi.ffiModel.pi.version; const EdgeInsets padding = EdgeInsets.only(left: 14.0, right: 5.0); final List> displayMenu = []; displayMenu.addAll([ @@ -651,6 +652,18 @@ class _RemoteMenubarState extends State { dismissOnClicked: true, )); } + if (version_cmp(peer_version, '1.2.0') >= 0) { + displayMenu.add(MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Switch Sides'), + style: style, + ), + proc: () => + showConfirmSwitchSidesDialog(widget.id, widget.ffi.dialogManager), + padding: padding, + dismissOnClicked: true, + )); + } } if (pi.version.isNotEmpty) { @@ -721,6 +734,7 @@ class _RemoteMenubarState extends State { List> _getDisplayMenu( dynamic futureData, int remoteCount) { const EdgeInsets padding = EdgeInsets.only(left: 18.0, right: 8.0); + final peer_version = widget.ffi.ffiModel.pi.version; final displayMenu = [ MenuEntryRadios( text: translate('Ratio'), @@ -880,9 +894,7 @@ class _RemoteMenubarState extends State { final fpsSlider = Offstage( offstage: (await bind.mainIsUsingPublicServer() && direct != true) || - (await bind.versionToNumber( - v: widget.ffi.ffiModel.pi.version) < - await bind.versionToNumber(v: '1.2.0')), + version_cmp(peer_version, '1.2.0') < 0, child: Row( children: [ Obx((() => Slider( @@ -1391,16 +1403,33 @@ void showAuditDialog(String id, dialogManager) async { focusNode: focusNode, )), actions: [ - TextButton( - style: flatButtonStyle, - onPressed: close, - child: Text(translate('Cancel')), - ), - TextButton( - style: flatButtonStyle, - onPressed: submit, - child: Text(translate('OK')), - ), + dialogButton('Cancel', onPressed: close, isOutline: true), + dialogButton('OK', onPressed: submit) + ], + onSubmit: submit, + onCancel: close, + ); + }); +} + +void showConfirmSwitchSidesDialog( + String id, OverlayDialogManager dialogManager) async { + dialogManager.show((setState, close) { + submit() async { + await bind.sessionSwitchSides(id: id); + closeConnection(id: id); + } + + return CustomAlertDialog( + title: Text(translate('Switch Sides')), + content: Column( + children: [ + Text(translate('Please confirm if you want to share your desktop?')), + ], + ), + actions: [ + dialogButton('Cancel', onPressed: close, isOutline: true), + dialogButton('OK', onPressed: submit), ], onSubmit: submit, onCancel: close, diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 641165e67..061c3293f 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -199,6 +199,16 @@ class FfiModel with ChangeNotifier { parent.target?.serverModel.setShowElevation(show); } else if (name == 'cancel_msgbox') { cancelMsgBox(evt, peerId); + } else if (name == 'switch_sides') { + final peer_id = evt['peer_id'].toString(); + final uuid = evt['uuid'].toString(); + Future.delayed(Duration.zero, () { + rustDeskWinManager.newRemoteDesktop(peer_id, switch_uuid: uuid); + }); + } else if (name == 'switch_back') { + final peer_id = evt['peer_id'].toString(); + await bind.sessionSwitchSides(id: peer_id); + closeConnection(id: peer_id); } }; } @@ -1289,7 +1299,9 @@ class FFI { /// Start with the given [id]. Only transfer file if [isFileTransfer], only port forward if [isPortForward]. void start(String id, - {bool isFileTransfer = false, bool isPortForward = false}) { + {bool isFileTransfer = false, + bool isPortForward = false, + String? switchUuid}) { assert(!(isFileTransfer && isPortForward), 'more than one connect type'); if (isFileTransfer) { connType = ConnType.fileTransfer; @@ -1305,7 +1317,11 @@ class FFI { } // ignore: unused_local_variable final addRes = bind.sessionAddSync( - id: id, isFileTransfer: isFileTransfer, isPortForward: isPortForward); + id: id, + isFileTransfer: isFileTransfer, + isPortForward: isPortForward, + switchUuid: switchUuid ?? "", + ); final stream = bind.sessionStart(id: id); final cb = ffiModel.startEventListener(id); () async { diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index c36a54db6..176b1ba2d 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -601,6 +601,7 @@ class Client { bool restart = false; bool recording = false; bool disconnected = false; + bool fromSwitch = false; RxBool hasUnreadChatMessage = false.obs; @@ -621,6 +622,7 @@ class Client { restart = json['restart']; recording = json['recording']; disconnected = json['disconnected']; + fromSwitch = json['from_switch']; } Map toJson() { @@ -638,6 +640,7 @@ class Client { data['restart'] = restart; data['recording'] = recording; data['disconnected'] = disconnected; + data['from_switch'] = fromSwitch; return data; } diff --git a/flutter/lib/utils/multi_window_manager.dart b/flutter/lib/utils/multi_window_manager.dart index cf6d78cd2..5087538c5 100644 --- a/flutter/lib/utils/multi_window_manager.dart +++ b/flutter/lib/utils/multi_window_manager.dart @@ -40,9 +40,13 @@ class RustDeskMultiWindowManager { int? _fileTransferWindowId; int? _portForwardWindowId; - Future newRemoteDesktop(String remoteId) async { - final msg = - jsonEncode({"type": WindowType.RemoteDesktop.index, "id": remoteId}); + Future newRemoteDesktop(String remoteId, + {String? switch_uuid}) async { + final msg = jsonEncode({ + "type": WindowType.RemoteDesktop.index, + "id": remoteId, + "switch_uuid": switch_uuid ?? "" + }); try { final ids = await DesktopMultiWindow.getAllSubWindowIds(); @@ -208,7 +212,7 @@ class RustDeskMultiWindowManager { } /// Remove active window which has [`windowId`] - /// + /// /// [Availability] /// This function should only be called from main window. /// For other windows, please post a unregister(hide) event to main window handler: diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index f5910d963..12d698045 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -564,6 +564,17 @@ message ElevationRequest { } } +message SwitchSidesRequest { + bytes uuid = 1; +} + +message SwitchSidesResponse { + bytes uuid = 1; + LoginRequest lr = 2; +} + +message SwitchBack {} + message Misc { oneof union { ChatMessage chat_message = 4; @@ -582,6 +593,8 @@ message Misc { ElevationRequest elevation_request = 18; string elevation_response = 19; bool portable_service_running = 20; + SwitchSidesRequest switch_sides_request = 21; + SwitchBack switch_back = 22; } } @@ -606,5 +619,6 @@ message Message { Misc misc = 19; Cliprdr cliprdr = 20; MessageBox message_box = 21; + SwitchSidesResponse switch_sides_response = 22; } } diff --git a/src/client.rs b/src/client.rs index 493448c3b..e9b8edf39 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,4 +1,5 @@ pub use async_trait::async_trait; +use bytes::Bytes; #[cfg(not(any(target_os = "android", target_os = "linux")))] use cpal::{ traits::{DeviceTrait, HostTrait, StreamTrait}, @@ -909,6 +910,7 @@ pub struct LoginConfigHandler { pub force_relay: bool, pub direct: Option, pub received: bool, + switch_uuid: Option, } impl Deref for LoginConfigHandler { @@ -936,7 +938,7 @@ impl LoginConfigHandler { /// /// * `id` - id of peer /// * `conn_type` - Connection type enum. - pub fn initialize(&mut self, id: String, conn_type: ConnType) { + pub fn initialize(&mut self, id: String, conn_type: ConnType, switch_uuid: Option) { self.id = id; self.conn_type = conn_type; let config = self.load_config(); @@ -948,6 +950,7 @@ impl LoginConfigHandler { self.force_relay = !self.get_option("force-always-relay").is_empty(); self.direct = None; self.received = false; + self.switch_uuid = switch_uuid; } /// Check if the client should auto login. @@ -1784,6 +1787,14 @@ pub async fn handle_hash( interface: &impl Interface, peer: &mut Stream, ) { + lc.write().unwrap().hash = hash.clone(); + let uuid = lc.read().unwrap().switch_uuid.clone(); + if let Some(uuid) = uuid { + if let Ok(uuid) = uuid::Uuid::from_str(&uuid) { + send_switch_login_request(lc.clone(), peer, uuid).await; + return; + } + } let mut password = lc.read().unwrap().password.clone(); if password.is_empty() { if !password_preset.is_empty() { @@ -1848,6 +1859,26 @@ pub async fn handle_login_from_ui( send_login(lc.clone(), hasher2.finalize()[..].into(), peer).await; } +async fn send_switch_login_request( + lc: Arc>, + peer: &mut Stream, + uuid: Uuid, +) { + let mut msg_out = Message::new(); + msg_out.set_switch_sides_response(SwitchSidesResponse { + uuid: Bytes::from(uuid.as_bytes().to_vec()), + lr: hbb_common::protobuf::MessageField::some( + lc.read() + .unwrap() + .create_login_msg(vec![]) + .login_request() + .to_owned(), + ), + ..Default::default() + }); + allow_err!(peer.send(&msg_out).await); +} + /// Interface for client to send data and commands. #[async_trait] pub trait Interface: Send + Clone + 'static + Sized { diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index b15949041..ff6d6c004 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -1111,6 +1111,10 @@ impl Remote { ); } } + Some(misc::Union::SwitchBack(_)) => { + #[cfg(feature = "flutter")] + self.handler.switch_back(&self.handler.id); + } _ => {} }, Some(message::Union::TestDelay(t)) => { diff --git a/src/flutter.rs b/src/flutter.rs index 1369b5646..0b8cef704 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -1,3 +1,12 @@ +use crate::ui_session_interface::{io_loop, InvokeUiSession, Session}; +use crate::{client::*, flutter_ffi::EventToUI}; +use bytes::Bytes; +use flutter_rust_bridge::{StreamSink, ZeroCopyBuffer}; +use hbb_common::{ + bail, config::LocalConfig, get_version_number, message_proto::*, rendezvous_proto::ConnType, + ResultType, +}; +use serde_json::json; use std::{ collections::HashMap, ffi::CString, @@ -5,18 +14,6 @@ use std::{ sync::{Arc, RwLock}, }; -use flutter_rust_bridge::{StreamSink, ZeroCopyBuffer}; - -use hbb_common::{ - bail, config::LocalConfig, get_version_number, message_proto::*, rendezvous_proto::ConnType, - ResultType, -}; -use serde_json::json; - -use crate::ui_session_interface::{io_loop, InvokeUiSession, Session}; - -use crate::{client::*, flutter_ffi::EventToUI}; - pub(super) const APP_TYPE_MAIN: &str = "main"; pub(super) const APP_TYPE_DESKTOP_REMOTE: &str = "remote"; pub(super) const APP_TYPE_DESKTOP_FILE_TRANSFER: &str = "file transfer"; @@ -366,7 +363,17 @@ impl InvokeUiSession for FlutterHandler { ("y", &display.y.to_string()), ("width", &display.width.to_string()), ("height", &display.height.to_string()), - ("cursor_embedded", &{if display.cursor_embedded {1} else {0}}.to_string()), + ( + "cursor_embedded", + &{ + if display.cursor_embedded { + 1 + } else { + 0 + } + } + .to_string(), + ), ], ); } @@ -382,6 +389,10 @@ impl InvokeUiSession for FlutterHandler { fn clipboard(&self, content: String) { self.push_event("clipboard", vec![("content", &content)]); } + + fn switch_back(&self, peer_id: &str) { + self.push_event("switch_back", [("peer_id", peer_id)].into()); + } } /// Create a new remote session with the given id. @@ -391,7 +402,12 @@ impl InvokeUiSession for FlutterHandler { /// * `id` - The identifier of the remote session with prefix. Regex: [\w]*[\_]*[\d]+ /// * `is_file_transfer` - If the session is used for file transfer. /// * `is_port_forward` - If the session is used for port forward. -pub fn session_add(id: &str, is_file_transfer: bool, is_port_forward: bool) -> ResultType<()> { +pub fn session_add( + id: &str, + is_file_transfer: bool, + is_port_forward: bool, + switch_uuid: &str, +) -> ResultType<()> { let session_id = get_session_id(id.to_owned()); LocalConfig::set_remote_id(&session_id); @@ -409,11 +425,17 @@ pub fn session_add(id: &str, is_file_transfer: bool, is_port_forward: bool) -> R ConnType::DEFAULT_CONN }; + let switch_uuid = if switch_uuid.is_empty() { + None + } else { + Some(switch_uuid.to_string()) + }; + session .lc .write() .unwrap() - .initialize(session_id, conn_type); + .initialize(session_id, conn_type, switch_uuid); if let Some(same_id_session) = SESSIONS.write().unwrap().insert(id.to_owned(), session) { same_id_session.close(); @@ -590,3 +612,17 @@ pub fn set_cur_session_id(id: String) { *CUR_SESSION_ID.write().unwrap() = id; } } + +pub fn switch_sides(peer_id: &str, uuid: &Bytes) { + if let Some(stream) = GLOBAL_EVENT_STREAM.read().unwrap().get(APP_TYPE_MAIN) { + if let Ok(uuid) = uuid::Uuid::from_slice(uuid.to_vec().as_ref()) { + let uuid = uuid.to_string(); + let data = HashMap::from([ + ("name", "switch_sides"), + ("peer_id", peer_id), + ("uuid", &uuid), + ]); + stream.add(serde_json::ser::to_string(&data).unwrap_or("".into())); + } + } +} diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index ca6823aa5..874cb4d4d 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -84,8 +84,9 @@ pub fn session_add_sync( id: String, is_file_transfer: bool, is_port_forward: bool, + switch_uuid: String, ) -> SyncReturn { - if let Err(e) = session_add(&id, is_file_transfer, is_port_forward) { + if let Err(e) = session_add(&id, is_file_transfer, is_port_forward, &switch_uuid) { SyncReturn(format!("Failed to add session with id {}, {}", &id, e)) } else { SyncReturn("".to_owned()) @@ -504,6 +505,12 @@ pub fn session_elevate_with_logon(id: String, username: String, password: String } } +pub fn session_switch_sides(id: String) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.switch_sides(); + } +} + pub fn main_get_sound_inputs() -> Vec { #[cfg(not(any(target_os = "android", target_os = "ios")))] return get_sound_inputs(); @@ -1066,6 +1073,10 @@ pub fn cm_elevate_portable(conn_id: i32) { crate::ui_cm_interface::elevate_portable(conn_id); } +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(); @@ -1108,10 +1119,6 @@ pub fn query_onlines(ids: Vec) { crate::rendezvous_mediator::query_online_states(ids, handle_query_onlines) } -pub fn version_to_number(v: String) -> i64 { - hbb_common::get_version_number(&v) -} - pub fn option_synced() -> bool { crate::ui_interface::option_synced() } diff --git a/src/ipc.rs b/src/ipc.rs index 9048db766..d74842d64 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -166,6 +166,7 @@ pub enum Data { file_transfer_enabled: bool, restart: bool, recording: bool, + from_switch: bool, }, ChatMessage { text: String, @@ -207,6 +208,7 @@ pub enum Data { Empty, Disconnected, DataPortableService(DataPortableService), + SwitchBack, } #[tokio::main(flavor = "current_thread")] diff --git a/src/lang/ca.rs b/src/lang/ca.rs index bbcea1347..72f55b44b 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -431,5 +431,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", ""), ("Medium", ""), ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 2f56b6da0..14e8a463d 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -431,5 +431,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", "弱"), ("Medium", "中"), ("Strong", "强"), + ("Switch Sides", "反转访问方向"), + ("Please confirm if you want to share your desktop?", "请确认要让对方访问你的桌面?"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 8852d602c..e2935770c 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -431,5 +431,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", ""), ("Medium", ""), ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 53ae46bd4..937990ea8 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -431,5 +431,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", ""), ("Medium", ""), ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index dd05dcdd5..7394a4628 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -431,5 +431,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", "Schwach"), ("Medium", "Mittel"), ("Strong", "Stark"), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 955a3287d..839c69bbb 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -431,5 +431,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", ""), ("Medium", ""), ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 5ab59b946..88b0ba8e9 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -431,5 +431,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", "Débil"), ("Medium", "Media"), ("Strong", "Fuerte"), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index a257425f1..dfd76405e 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -431,5 +431,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", ""), ("Medium", ""), ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 6edec8477..9c9860fb2 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -431,5 +431,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", ""), ("Medium", ""), ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/gr.rs b/src/lang/gr.rs index 4b54ba8ad..6ec1152cd 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -431,5 +431,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", "Αδύναμο"), ("Medium", "Μέτριο"), ("Strong", "Δυνατό"), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 9e1a4d982..295104a67 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -431,5 +431,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", ""), ("Medium", ""), ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 65c30ec6d..5604a0c52 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -431,5 +431,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", ""), ("Medium", ""), ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index e3c1a1f0f..2e313e101 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -431,5 +431,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", "Debole"), ("Medium", "Media"), ("Strong", "Forte"), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 6ebb11ef5..a280940c7 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -431,5 +431,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", ""), ("Medium", ""), ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index a6825b523..1cdf529ce 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -431,5 +431,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", ""), ("Medium", ""), ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 816eb370d..59d26135f 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -431,5 +431,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", ""), ("Medium", ""), ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index df985cccf..ee4b45334 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -431,5 +431,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", ""), ("Medium", ""), ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index dba37b5da..66373a5e9 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -431,5 +431,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", ""), ("Medium", ""), ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 31c9153f2..5a137f391 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -431,5 +431,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", ""), ("Medium", ""), ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index abe642d9c..a7e42e0e4 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -431,5 +431,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", "Слабый"), ("Medium", "Средний"), ("Strong", "Стойкий"), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 56d14652d..c735cb28c 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -431,5 +431,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", ""), ("Medium", ""), ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 3d2ad3be8..6a17cc906 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -431,5 +431,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", ""), ("Medium", ""), ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 165597e7e..ebb43f6b7 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -431,5 +431,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", ""), ("Medium", ""), ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 739d53570..d9463318d 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -431,5 +431,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", ""), ("Medium", ""), ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 498131d0c..146e60f9a 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -431,5 +431,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", ""), ("Medium", ""), ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index adb05c943..729932973 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -431,5 +431,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", ""), ("Medium", ""), ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index 2b062c3f7..a78509e59 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -431,5 +431,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", ""), ("Medium", ""), ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index a4a179c86..483ee67e3 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -431,5 +431,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", ""), ("Medium", ""), ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index cd9f270ec..459c517ff 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -431,5 +431,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", "弱"), ("Medium", "中"), ("Strong", "強"), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index ff24baab7..ca99be12e 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -431,5 +431,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", ""), ("Medium", ""), ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 6988efba7..53de4e67c 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -431,5 +431,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", ""), ("Medium", ""), ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), ].iter().cloned().collect(); } diff --git a/src/server/connection.rs b/src/server/connection.rs index a7526c8b4..83ec5db55 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -40,6 +40,7 @@ lazy_static::lazy_static! { static ref LOGIN_FAILURES: Arc::>> = Default::default(); static ref SESSIONS: Arc::>> = Default::default(); static ref ALIVE_CONNS: Arc::>> = Default::default(); + pub static ref SWITCH_SIDES_UUID: Arc::>> = Default::default(); } pub static CLICK_TIME: AtomicI64 = AtomicI64::new(0); pub static MOUSE_MOVE_TIME: AtomicI64 = AtomicI64::new(0); @@ -102,6 +103,7 @@ pub struct Connection { chat_unanswered: bool, close_manually: bool, elevation_requested: bool, + from_switch: bool, } impl Subscriber for ConnInner { @@ -134,6 +136,7 @@ const MILLI1: Duration = Duration::from_millis(1); const SEND_TIMEOUT_VIDEO: u64 = 12_000; const SEND_TIMEOUT_OTHER: u64 = SEND_TIMEOUT_VIDEO * 10; const SESSION_TIMEOUT: Duration = Duration::from_secs(30); +const SWITCH_SIDES_TIMEOUT: Duration = Duration::from_secs(30); impl Connection { pub async fn start( @@ -198,6 +201,7 @@ impl Connection { chat_unanswered: false, close_manually: false, elevation_requested: false, + from_switch: false, }; #[cfg(not(any(target_os = "android", target_os = "ios")))] tokio::spawn(async move { @@ -362,6 +366,13 @@ impl Connection { log::error!("Failed to start portable service from cm:{:?}", e); } } + ipc::Data::SwitchBack => { + let mut misc = Misc::new(); + misc.set_switch_back(SwitchBack::default()); + let mut msg = Message::new(); + msg.set_misc(misc); + conn.send(msg).await; + } _ => {} } }, @@ -954,6 +965,7 @@ impl Connection { file_transfer_enabled: self.file_transfer_enabled(), restart: self.restart, recording: self.recording, + from_switch: self.from_switch, }); } @@ -1078,29 +1090,33 @@ impl Connection { return Config::get_option(enable_prefix_option).is_empty(); } - async fn on_message(&mut self, msg: Message) -> bool { - if let Some(message::Union::LoginRequest(lr)) = msg.union { - self.lr = lr.clone(); - if let Some(o) = lr.option.as_ref() { - self.update_option(o).await; - if let Some(q) = o.video_codec_state.clone().take() { - scrap::codec::Encoder::update_video_encoder( - self.inner.id(), - scrap::codec::EncoderUpdate::State(q), - ); - } else { - scrap::codec::Encoder::update_video_encoder( - self.inner.id(), - scrap::codec::EncoderUpdate::DisableHwIfNotExist, - ); - } + 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; + if let Some(q) = o.video_codec_state.clone().take() { + scrap::codec::Encoder::update_video_encoder( + self.inner.id(), + scrap::codec::EncoderUpdate::State(q), + ); } else { scrap::codec::Encoder::update_video_encoder( self.inner.id(), scrap::codec::EncoderUpdate::DisableHwIfNotExist, ); } - self.video_ack_required = lr.video_ack_required; + } else { + scrap::codec::Encoder::update_video_encoder( + self.inner.id(), + scrap::codec::EncoderUpdate::DisableHwIfNotExist, + ); + } + self.video_ack_required = lr.video_ack_required; + } + + async fn on_message(&mut self, msg: Message) -> bool { + if let Some(message::Union::LoginRequest(lr)) = msg.union { + self.handle_login_request_without_validation(&lr).await; if self.authorized { return true; } @@ -1247,6 +1263,21 @@ impl Connection { .unwrap() .update_network_delay(new_delay); } + } else if let Some(message::Union::SwitchSidesResponse(_s)) = msg.union { + #[cfg(feature = "flutter")] + if let Some(lr) = _s.lr.clone().take() { + self.handle_login_request_without_validation(&lr).await; + let uuid_old = SWITCH_SIDES_UUID.lock().unwrap().remove(&lr.my_id); + if let Ok(uuid) = uuid::Uuid::from_slice(_s.uuid.to_vec().as_ref()) { + if let Some((instant, uuid_old)) = uuid_old { + if instant.elapsed() < SWITCH_SIDES_TIMEOUT && uuid == uuid_old { + self.from_switch = true; + self.try_start_cm(lr.my_id.clone(), lr.my_name.clone(), true); + self.send_logon_response().await; + } + } + } + } } else if self.authorized { match msg.union { Some(message::Union::MouseEvent(me)) => { @@ -1536,6 +1567,11 @@ impl Connection { } _ => {} }, + #[cfg(feature = "flutter")] + Some(misc::Union::SwitchSidesRequest(s)) => { + crate::flutter::switch_sides(&self.lr.my_id, &s.uuid); + return false; + } _ => {} }, _ => {} diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 1f3d5f7ec..21504d20d 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -264,6 +264,8 @@ impl InvokeUiSession for SciterHandler { fn update_block_input_state(&self, on: bool) { self.call("updateBlockInputState", &make_args!(on)); } + + fn switch_back(&self, _id: &str) {} } pub struct SciterSession(Session); @@ -440,7 +442,7 @@ impl SciterSession { ConnType::DEFAULT_CONN }; - session.lc.write().unwrap().initialize(id, conn_type); + session.lc.write().unwrap().initialize(id, conn_type, None); Self(session) } diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index 551352ff7..dd0ce2b24 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -48,6 +48,7 @@ pub struct Client { pub file: bool, pub restart: bool, pub recording: bool, + pub from_switch: bool, #[serde(skip)] tx: UnboundedSender, } @@ -118,6 +119,7 @@ impl ConnectionManager { file: bool, restart: bool, recording: bool, + from_switch: bool, tx: mpsc::UnboundedSender, ) { let client = Client { @@ -134,6 +136,7 @@ impl ConnectionManager { file, restart, recording, + from_switch, tx, }; CLIENTS @@ -241,6 +244,14 @@ pub fn get_clients_length() -> usize { clients.len() } +#[inline] +#[cfg(feature = "flutter")] +pub fn switch_back(id: i32) { + if let Some(client) = CLIENTS.read().unwrap().get(&id) { + allow_err!(client.tx.send(Data::SwitchBack)); + }; +} + impl IpcTaskRunner { #[cfg(windows)] async fn enable_cliprdr_file_context(&mut self, conn_id: i32, enabled: bool) { @@ -308,9 +319,9 @@ impl IpcTaskRunner { } Ok(Some(data)) => { match data { - Data::Login{id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, file_transfer_enabled: _file_transfer_enabled, restart, recording} => { + Data::Login{id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, file_transfer_enabled: _file_transfer_enabled, restart, recording, from_switch} => { log::debug!("conn_id: {}", id); - self.cm.add_connection(id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart, recording, self.tx.clone()); + self.cm.add_connection(id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart, recording, from_switch,self.tx.clone()); self.authorized = authorized; self.conn_id = id; #[cfg(windows)] @@ -498,6 +509,7 @@ pub async fn start_listen( file, restart, recording, + from_switch, .. }) => { current_id = id; @@ -514,6 +526,7 @@ pub async fn start_listen( file, restart, recording, + from_switch, tx.clone(), ); } diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 00f1f90cf..800ca35c6 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -8,6 +8,7 @@ use crate::common::{self, is_keyboard_mode_supported, 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}; use hbb_common::rendezvous_proto::ConnType; use hbb_common::tokio::{self, sync::mpsc}; @@ -18,6 +19,7 @@ use std::collections::HashMap; use std::ops::{Deref, DerefMut}; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::{Arc, Mutex, RwLock}; +use uuid::Uuid; pub static IS_IN: AtomicBool = AtomicBool::new(false); #[derive(Clone, Default)] @@ -616,6 +618,22 @@ impl Session { pub fn elevate_with_logon(&self, username: String, password: String) { self.send(Data::ElevateWithLogon(username, password)); } + + pub fn switch_sides(&self) { + let uuid = Uuid::new_v4(); + crate::server::SWITCH_SIDES_UUID + .lock() + .unwrap() + .insert(self.id.clone(), (tokio::time::Instant::now(), uuid.clone())); + let mut misc = Misc::new(); + misc.set_switch_sides_request(SwitchSidesRequest { + uuid: Bytes::from(uuid.as_bytes().to_vec()), + ..Default::default() + }); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + self.send(Data::Message(msg_out)); + } } pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { @@ -655,6 +673,7 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { #[cfg(any(target_os = "android", target_os = "ios"))] fn clipboard(&self, content: String); fn cancel_msgbox(&self, tag: &str); + fn switch_back(&self, id: &str); } impl Deref for Session { From 81a60725f4969ea1fb376b06c20aa2e32576d38f Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 17 Jan 2023 16:13:44 +0800 Subject: [PATCH 1566/2015] switch sides: remove outdate uuid Signed-off-by: 21pages --- .../lib/desktop/widgets/remote_menubar.dart | 3 ++- src/server/connection.rs | 18 +++++++++++++++--- src/ui_session_interface.rs | 5 +---- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 6ea372b1f..62289d5f0 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -652,7 +652,8 @@ class _RemoteMenubarState extends State { dismissOnClicked: true, )); } - if (version_cmp(peer_version, '1.2.0') >= 0) { + if (pi.platform != kPeerPlatformAndroid && + version_cmp(peer_version, '1.2.0') >= 0) { displayMenu.add(MenuEntryButton( childBuilder: (TextStyle? style) => Text( translate('Switch Sides'), diff --git a/src/server/connection.rs b/src/server/connection.rs index 83ec5db55..e60d4652c 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -40,7 +40,7 @@ lazy_static::lazy_static! { static ref LOGIN_FAILURES: Arc::>> = Default::default(); static ref SESSIONS: Arc::>> = Default::default(); static ref ALIVE_CONNS: Arc::>> = Default::default(); - pub static ref SWITCH_SIDES_UUID: Arc::>> = Default::default(); + static ref SWITCH_SIDES_UUID: Arc::>> = Default::default(); } pub static CLICK_TIME: AtomicI64 = AtomicI64::new(0); pub static MOUSE_MOVE_TIME: AtomicI64 = AtomicI64::new(0); @@ -136,7 +136,7 @@ const MILLI1: Duration = Duration::from_millis(1); const SEND_TIMEOUT_VIDEO: u64 = 12_000; const SEND_TIMEOUT_OTHER: u64 = SEND_TIMEOUT_VIDEO * 10; const SESSION_TIMEOUT: Duration = Duration::from_secs(30); -const SWITCH_SIDES_TIMEOUT: Duration = Duration::from_secs(30); +const SWITCH_SIDES_TIMEOUT: Duration = Duration::from_secs(10); impl Connection { pub async fn start( @@ -1267,10 +1267,14 @@ impl Connection { #[cfg(feature = "flutter")] if let Some(lr) = _s.lr.clone().take() { self.handle_login_request_without_validation(&lr).await; + SWITCH_SIDES_UUID + .lock() + .unwrap() + .retain(|_, v| v.0.elapsed() < SWITCH_SIDES_TIMEOUT); let uuid_old = SWITCH_SIDES_UUID.lock().unwrap().remove(&lr.my_id); if let Ok(uuid) = uuid::Uuid::from_slice(_s.uuid.to_vec().as_ref()) { if let Some((instant, uuid_old)) = uuid_old { - if instant.elapsed() < SWITCH_SIDES_TIMEOUT && uuid == uuid_old { + if uuid == uuid_old { self.from_switch = true; self.try_start_cm(lr.my_id.clone(), lr.my_name.clone(), true); self.send_logon_response().await; @@ -1792,6 +1796,14 @@ impl Connection { } } +#[cfg(feature = "flutter")] +pub fn insert_switch_sides_uuid(id: String, uuid: uuid::Uuid) { + SWITCH_SIDES_UUID + .lock() + .unwrap() + .insert(id, (tokio::time::Instant::now(), uuid)); +} + #[cfg(not(any(target_os = "android", target_os = "ios")))] async fn start_ipc( mut rx_to_cm: mpsc::UnboundedReceiver, diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 800ca35c6..a5f55a05d 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -621,10 +621,7 @@ impl Session { pub fn switch_sides(&self) { let uuid = Uuid::new_v4(); - crate::server::SWITCH_SIDES_UUID - .lock() - .unwrap() - .insert(self.id.clone(), (tokio::time::Instant::now(), uuid.clone())); + crate::server::insert_switch_sides_uuid(self.id.clone(), uuid.clone()); let mut misc = Misc::new(); misc.set_switch_sides_request(SwitchSidesRequest { uuid: Bytes::from(uuid.as_bytes().to_vec()), From e57949d47204ac638ee5bc75679405e6b53c4fc9 Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 17 Jan 2023 20:16:36 +0800 Subject: [PATCH 1567/2015] switch sides: use ipc to pass msg from ui to server Signed-off-by: 21pages --- src/ipc.rs | 12 +++++++++- src/server/connection.rs | 3 +-- src/ui_cm_interface.rs | 2 +- src/ui_session_interface.rs | 44 +++++++++++++++++++++++++++---------- 4 files changed, 46 insertions(+), 15 deletions(-) diff --git a/src/ipc.rs b/src/ipc.rs index d74842d64..d4d803aec 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -208,7 +208,8 @@ pub enum Data { Empty, Disconnected, DataPortableService(DataPortableService), - SwitchBack, + SwitchSidesRequest(String), + SwitchSidesBack, } #[tokio::main(flavor = "current_thread")] @@ -429,6 +430,15 @@ async fn handle(data: Data, stream: &mut Connection) { Data::TestRendezvousServer => { crate::test_rendezvous_server(); } + Data::SwitchSidesRequest(id) => { + let uuid = uuid::Uuid::new_v4(); + crate::server::insert_switch_sides_uuid(id, uuid.clone()); + allow_err!( + stream + .send(&Data::SwitchSidesRequest(uuid.to_string())) + .await + ); + } _ => {} } } diff --git a/src/server/connection.rs b/src/server/connection.rs index e60d4652c..e4ab1b22e 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -366,7 +366,7 @@ impl Connection { log::error!("Failed to start portable service from cm:{:?}", e); } } - ipc::Data::SwitchBack => { + ipc::Data::SwitchSidesBack => { let mut misc = Misc::new(); misc.set_switch_back(SwitchBack::default()); let mut msg = Message::new(); @@ -1796,7 +1796,6 @@ impl Connection { } } -#[cfg(feature = "flutter")] pub fn insert_switch_sides_uuid(id: String, uuid: uuid::Uuid) { SWITCH_SIDES_UUID .lock() diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index dd0ce2b24..ea3553c8a 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -248,7 +248,7 @@ pub fn get_clients_length() -> usize { #[cfg(feature = "flutter")] pub fn switch_back(id: i32) { if let Some(client) = CLIENTS.read().unwrap().get(&id) { - allow_err!(client.tx.send(Data::SwitchBack)); + allow_err!(client.tx.send(Data::SwitchSidesBack)); }; } diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index a5f55a05d..a16327d75 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -17,6 +17,7 @@ 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 uuid::Uuid; @@ -619,17 +620,38 @@ impl Session { self.send(Data::ElevateWithLogon(username, password)); } - pub fn switch_sides(&self) { - let uuid = Uuid::new_v4(); - crate::server::insert_switch_sides_uuid(self.id.clone(), uuid.clone()); - let mut misc = Misc::new(); - misc.set_switch_sides_request(SwitchSidesRequest { - uuid: Bytes::from(uuid.as_bytes().to_vec()), - ..Default::default() - }); - let mut msg_out = Message::new(); - msg_out.set_misc(misc); - self.send(Data::Message(msg_out)); + #[tokio::main(flavor = "current_thread")] + pub async fn switch_sides(&self) { + match crate::ipc::connect(1000, "").await { + Ok(mut conn) => { + if conn + .send(&crate::ipc::Data::SwitchSidesRequest(self.id.to_string())) + .await + .is_ok() + { + if let Ok(Some(data)) = conn.next_timeout(1000).await { + match data { + crate::ipc::Data::SwitchSidesRequest(str_uuid) => { + if let Ok(uuid) = Uuid::from_str(&str_uuid) { + let mut misc = Misc::new(); + misc.set_switch_sides_request(SwitchSidesRequest { + uuid: Bytes::from(uuid.as_bytes().to_vec()), + ..Default::default() + }); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + self.send(Data::Message(msg_out)); + } + } + _ => {} + } + } + } + } + Err(err) => { + log::info!("server not started (will try to start): {}", err); + } + } } } From c25796e44d1b880b56eea6c3e20f5becbd573512 Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 17 Jan 2023 21:43:39 +0800 Subject: [PATCH 1568/2015] switch sides: windows Signed-off-by: 21pages --- flutter/lib/common.dart | 4 +++- flutter/lib/models/model.dart | 6 ------ src/core_main.rs | 15 ++++++++++++++- src/flutter.rs | 14 -------------- src/server/connection.rs | 12 ++++++++++-- 5 files changed, 27 insertions(+), 24 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index accbdf8df..13d0f2e60 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1307,8 +1307,10 @@ bool callUniLinksUriHandler(Uri uri) { // new connection if (uri.authority == "connection" && uri.path.startsWith("/new/")) { final peerId = uri.path.substring("/new/".length); + var param = uri.queryParameters; + String? switch_uuid = param["switch_uuid"]; Future.delayed(Duration.zero, () { - rustDeskWinManager.newRemoteDesktop(peerId); + rustDeskWinManager.newRemoteDesktop(peerId, switch_uuid: switch_uuid); }); return true; } diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 061c3293f..95ecde6e9 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -199,12 +199,6 @@ class FfiModel with ChangeNotifier { parent.target?.serverModel.setShowElevation(show); } else if (name == 'cancel_msgbox') { cancelMsgBox(evt, peerId); - } else if (name == 'switch_sides') { - final peer_id = evt['peer_id'].toString(); - final uuid = evt['uuid'].toString(); - Future.delayed(Duration.zero, () { - rustDeskWinManager.newRemoteDesktop(peer_id, switch_uuid: uuid); - }); } else if (name == 'switch_back') { final peer_id = evt['peer_id'].toString(); await bind.sessionSwitchSides(id: peer_id); diff --git a/src/core_main.rs b/src/core_main.rs index 9083efe0e..76795576e 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -298,6 +298,13 @@ fn core_main_invoke_new_connection(mut args: std::env::Args) -> Option Option { - crate::flutter::switch_sides(&self.lr.my_id, &s.uuid); - return false; + if let Ok(uuid) = uuid::Uuid::from_slice(&s.uuid.to_vec()[..]) { + crate::run_me(vec![ + "--connect", + &self.lr.my_id, + "--switch_uuid", + uuid.to_string().as_ref(), + ]) + .ok(); + return false; + } } _ => {} }, From b7844d11752f72b58a33e03e4e05419b32456ef9 Mon Sep 17 00:00:00 2001 From: 21pages Date: Wed, 18 Jan 2023 13:54:56 +0800 Subject: [PATCH 1569/2015] switch sides: linux dbus use uri as para like uni_links Signed-off-by: 21pages --- flutter/lib/common.dart | 15 ++++++++++----- .../lib/desktop/pages/desktop_home_page.dart | 4 ++-- flutter/lib/models/model.dart | 10 +++------- src/core_main.rs | 18 ++++++++++-------- src/flutter.rs | 1 - src/platform/linux.rs | 14 ++++++++------ src/server/connection.rs | 6 ++++-- src/server/dbus.rs | 15 ++++++++------- 8 files changed, 45 insertions(+), 38 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 13d0f2e60..60e2246f5 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1261,23 +1261,28 @@ StreamSubscription? listenUniLinks() { /// Returns true if we successfully handle the startup arguments. bool checkArguments() { + // bootArgs:[--connect, 362587269, --switch_uuid, e3d531cc-5dce-41e0-bd06-5d4a2b1eec05] // check connect args final connectIndex = bootArgs.indexOf("--connect"); if (connectIndex == -1) { return false; } - String? arg = + String? id = bootArgs.length < connectIndex + 1 ? null : bootArgs[connectIndex + 1]; - if (arg != null) { - if (arg.startsWith(kUniLinksPrefix)) { - return parseRustdeskUri(arg); + final switchUuidIndex = bootArgs.indexOf("--switch_uuid"); + String? switchUuid = bootArgs.length < switchUuidIndex + 1 + ? null + : bootArgs[switchUuidIndex + 1]; + if (id != null) { + if (id.startsWith(kUniLinksPrefix)) { + return parseRustdeskUri(id); } else { // remove "--connect xxx" in the `bootArgs` array bootArgs.removeAt(connectIndex); bootArgs.removeAt(connectIndex); // fallback to peer id Future.delayed(Duration.zero, () { - rustDeskWinManager.newRemoteDesktop(arg); + rustDeskWinManager.newRemoteDesktop(id, switch_uuid: switchUuid); }); return true; } diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 2773a3049..0501c298a 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -544,7 +544,7 @@ void setPasswordDialog() async { final p1 = TextEditingController(text: pw); var errMsg0 = ""; var errMsg1 = ""; - final RxString rxPass = p0.text.obs; + final RxString rxPass = pw.trim().obs; final rules = [ DigitValidationRule(), UppercaseValidationRule(), @@ -603,7 +603,7 @@ void setPasswordDialog() async { controller: p0, focusNode: FocusNode()..requestFocus(), onChanged: (value) { - rxPass.value = value; + rxPass.value = value.trim(); }, ), ), diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 95ecde6e9..cf7a88312 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -184,13 +184,9 @@ class FfiModel with ChangeNotifier { } else if (name == 'update_privacy_mode') { updatePrivacyMode(evt, peerId); } else if (name == 'new_connection') { - var arg = evt['peer_id'].toString(); - if (arg.startsWith(kUniLinksPrefix)) { - parseRustdeskUri(arg); - } else { - Future.delayed(Duration.zero, () { - rustDeskWinManager.newRemoteDesktop(arg); - }); + var uni_links = evt['uni_links'].toString(); + if (uni_links.startsWith(kUniLinksPrefix)) { + parseRustdeskUri(uni_links); } } else if (name == 'alias') { handleAliasChanged(evt); diff --git a/src/core_main.rs b/src/core_main.rs index 76795576e..75b5951da 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -305,11 +305,20 @@ fn core_main_invoke_new_connection(mut args: std::env::Args) -> Option { return None; } @@ -322,14 +331,7 @@ fn core_main_invoke_new_connection(mut args: std::env::Args) -> Option>, chat_unanswered: bool, close_manually: bool, + #[allow(unused)] elevation_requested: bool, from_switch: bool, } @@ -1547,7 +1548,7 @@ impl Connection { self.send(msg).await; } } - Some(elevation_request::Union::Logon(r)) => { + Some(elevation_request::Union::Logon(_r)) => { #[cfg(windows)] { let mut err = "No need to elevate".to_string(); @@ -1556,7 +1557,8 @@ impl Connection { { use crate::portable_service::client; err = client::start_portable_service(client::StartPara::Logon( - r.username, r.password, + _r.username, + _r.password, )) .err() .map_or("".to_string(), |e| e.to_string()); diff --git a/src/server/dbus.rs b/src/server/dbus.rs index 5a38fe7cb..081db3e8f 100644 --- a/src/server/dbus.rs +++ b/src/server/dbus.rs @@ -5,10 +5,10 @@ /// [Flutter]: handle uni links for linux use dbus::blocking::Connection; use dbus_crossroads::{Crossroads, IfaceBuilder}; -use hbb_common::{log}; -use std::{error::Error, fmt, time::Duration}; +use hbb_common::log; #[cfg(feature = "flutter")] use std::collections::HashMap; +use std::{error::Error, fmt, time::Duration}; const DBUS_NAME: &str = "org.rustdesk.rustdesk"; const DBUS_PREFIX: &str = "/dbus"; @@ -30,15 +30,16 @@ impl fmt::Display for DbusError { impl Error for DbusError {} /// invoke new connection from dbus -/// +/// /// [Tips]: /// How to test by CLI: /// - use dbus-send command: /// `dbus-send --session --print-reply --dest=org.rustdesk.rustdesk /dbus org.rustdesk.rustdesk.NewConnection string:'PEER_ID'` -pub fn invoke_new_connection(peer_id: String) -> Result<(), Box> { +pub fn invoke_new_connection(uni_links: String) -> Result<(), Box> { let conn = Connection::new_session()?; let proxy = conn.with_proxy(DBUS_NAME, DBUS_PREFIX, DBUS_TIMEOUT); - let (ret,): (String,) = proxy.method_call(DBUS_NAME, DBUS_METHOD_NEW_CONNECTION, (peer_id,))?; + let (ret,): (String,) = + proxy.method_call(DBUS_NAME, DBUS_METHOD_NEW_CONNECTION, (uni_links,))?; if ret != DBUS_METHOD_RETURN_SUCCESS { log::error!("error on call new connection to dbus server"); return Err(Box::new(DbusError("not success".to_string()))); @@ -67,7 +68,7 @@ fn handle_client_message(builder: &mut IfaceBuilder<()>) { DBUS_METHOD_NEW_CONNECTION, (DBUS_METHOD_NEW_CONNECTION_ID,), (DBUS_METHOD_RETURN,), - move |_, _, (_peer_id,): (String,)| { + move |_, _, (_uni_links,): (String,)| { #[cfg(feature = "flutter")] { use crate::flutter::{self, APP_TYPE_MAIN}; @@ -79,7 +80,7 @@ fn handle_client_message(builder: &mut IfaceBuilder<()>) { { let data = HashMap::from([ ("name", "new_connection"), - ("peer_id", _peer_id.as_str()) + ("uni_links", _uni_links.as_str()), ]); if !stream.add(serde_json::ser::to_string(&data).unwrap_or("".to_string())) { log::error!("failed to add dbus message to flutter global dbus stream."); From ee0e84be37bf3a2b0d8f0ff45e5620b4b7512b79 Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 19 Jan 2023 21:21:28 +0800 Subject: [PATCH 1570/2015] update flutter_rust_bridge to latest Signed-off-by: 21pages --- .github/workflows/flutter-ci.yml | 24 +---- .github/workflows/flutter-nightly.yml | 24 +---- .gitignore | 1 + Cargo.lock | 121 ++++++++++++++++++-------- Cargo.toml | 6 +- build.rs | 23 +++-- flutter/lib/common.dart | 11 +-- flutter/lib/models/model.dart | 4 +- flutter/pubspec.yaml | 7 +- src/flutter_ffi.rs | 4 + 10 files changed, 122 insertions(+), 103 deletions(-) diff --git a/.github/workflows/flutter-ci.yml b/.github/workflows/flutter-ci.yml index 738b57c4d..4e98f311d 100644 --- a/.github/workflows/flutter-ci.yml +++ b/.github/workflows/flutter-ci.yml @@ -70,12 +70,7 @@ jobs: - name: Install flutter rust bridge deps run: | - dart pub global activate ffigen --version 5.0.1 - $exists = Test-Path ~/.cargo/bin/flutter_rust_bridge_codegen.exe - Push-Location .. - git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1 - Push-Location flutter_rust_bridge/frb_codegen ; cargo install --path . ; Pop-Location - Pop-Location + cargo install flutter_rust_bridge_codegen Push-Location flutter ; flutter pub get ; Pop-Location ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart @@ -134,14 +129,7 @@ jobs: - name: Install flutter rust bridge deps shell: bash run: | - dart pub global activate ffigen --version 5.0.1 - # flutter_rust_bridge - pushd /tmp - wget https://github.com/Kingtous/flutter_rust_bridge/releases/download/1.32.0-rustdesk/flutter_rust_bridge_codegen-x86_64-darwin.tgz - tar -zxvf flutter_rust_bridge_codegen-x86_64-darwin.tgz - mkdir -p ~/.cargo/bin - mv flutter_rust_bridge_codegen ~/.cargo/bin; chmod +x ~/.cargo/bin/flutter_rust_bridge_codegen - popd + cargo install flutter_rust_bridge_codegen pushd flutter && flutter pub get && popd ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart @@ -307,15 +295,10 @@ jobs: flutter-version: ${{ env.FLUTTER_VERSION }} cache: true - - name: Install ffigen - run: | - dart pub global activate ffigen --version 5.0.1 - - name: Install flutter rust bridge deps shell: bash run: | - pushd /tmp && git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1 || true && popd - pushd /tmp/flutter_rust_bridge/frb_codegen && cargo install --path . && popd + cargo install flutter_rust_bridge_codegen pushd flutter && flutter pub get && popd - name: Run flutter rust bridge @@ -328,6 +311,7 @@ jobs: name: bridge-artifact path: | ./src/bridge_generated.rs + ./src/bridge_generated.io.rs ./flutter/lib/generated_bridge.dart ./flutter/lib/generated_bridge.freezed.dart diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index efa085ea8..145eee55d 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -72,12 +72,7 @@ jobs: - name: Install flutter rust bridge deps run: | - dart pub global activate ffigen --version 5.0.1 - $exists = Test-Path ~/.cargo/bin/flutter_rust_bridge_codegen.exe - Push-Location .. - git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1 - Push-Location flutter_rust_bridge/frb_codegen ; cargo install --path . ; Pop-Location - Pop-Location + cargo install flutter_rust_bridge_codegen Push-Location flutter ; flutter pub get ; Pop-Location ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart @@ -213,14 +208,7 @@ jobs: - name: Install flutter rust bridge deps shell: bash run: | - dart pub global activate ffigen --version 5.0.1 - # flutter_rust_bridge - pushd /tmp - wget https://github.com/Kingtous/flutter_rust_bridge/releases/download/1.32.0-rustdesk/flutter_rust_bridge_codegen-x86_64-darwin.tgz - tar -zxvf flutter_rust_bridge_codegen-x86_64-darwin.tgz - mkdir -p ~/.cargo/bin - mv flutter_rust_bridge_codegen ~/.cargo/bin; chmod +x ~/.cargo/bin/flutter_rust_bridge_codegen - popd + cargo install flutter_rust_bridge_codegen pushd flutter && flutter pub get && popd ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart @@ -414,15 +402,10 @@ jobs: flutter-version: ${{ env.FLUTTER_VERSION }} cache: true - - name: Install ffigen - run: | - dart pub global activate ffigen --version 5.0.1 - - name: Install flutter rust bridge deps shell: bash run: | - pushd /tmp && git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1 || true && popd - pushd /tmp/flutter_rust_bridge/frb_codegen && cargo install --path . && popd + cargo install flutter_rust_bridge_codegen pushd flutter && flutter pub get && popd - name: Run flutter rust bridge @@ -435,6 +418,7 @@ jobs: name: bridge-artifact path: | ./src/bridge_generated.rs + ./src/bridge_generated.io.rs ./flutter/lib/generated_bridge.dart ./flutter/lib/generated_bridge.freezed.dart diff --git a/.gitignore b/.gitignore index 1ecea7af8..fd5b5955e 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ cert.pfx sciter.dll **pdb src/bridge_generated.rs +src/bridge_generated.io.rs *deb rustdesk *.cache diff --git a/Cargo.lock b/Cargo.lock index 8dfde0335..693ae7d4c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -45,11 +45,13 @@ dependencies = [ [[package]] name = "allo-isolate" -version = "0.1.13" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccb993621e6bf1b67591005b0adad126159a0ab31af379743906158aed5330d0" +checksum = "8ed55848be9f41d44c79df6045b680a74a78bc579e0813f7f196cd7928e22fb1" dependencies = [ + "anyhow", "atomic", + "chrono", ] [[package]] @@ -471,6 +473,12 @@ dependencies = [ "alloc-stdlib", ] +[[package]] +name = "build-target" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "832133bbabbbaa9fbdba793456a2827627a7d2b8fb96032fa1e7666d7895832b" + [[package]] name = "bumpalo" version = "3.11.1" @@ -565,9 +573,9 @@ dependencies = [ [[package]] name = "cbindgen" -version = "0.23.0" +version = "0.24.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b6d248e3ca02f3fbfabcb9284464c596baec223a26d91bbf44a5a62ddb0d900" +checksum = "a6358dedf60f4d9b8db43ad187391afe959746101346fe51bb978126bec61dfb" dependencies = [ "clap 3.2.23", "heck 0.4.0", @@ -838,6 +846,16 @@ dependencies = [ "toml", ] +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if 1.0.0", + "wasm-bindgen", +] + [[package]] name = "convert_case" version = "0.5.0" @@ -1335,6 +1353,17 @@ dependencies = [ "byteorder", ] +[[package]] +name = "delegate" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "082a24a9967533dc5d743c602157637116fc1b52806d694a5a45e6f32567fcdd" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "derivative" version = "2.2.0" @@ -1672,6 +1701,18 @@ version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" +[[package]] +name = "extend" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5216e387a76eebaaf11f6d871ec8a4aae0b25f05456ee21f228e024b1b3610" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "failure" version = "0.1.8" @@ -1744,28 +1785,44 @@ dependencies = [ [[package]] name = "flutter_rust_bridge" -version = "1.32.0" -source = "git+https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge#e5adce55eea0b74d3680e66a2c5252edf17b07e1" +version = "1.61.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8079119bbe8fb63d7ebb731fa2aa68c6c8375f4ac95ca26d5868e64c0f4b9244" dependencies = [ "allo-isolate", "anyhow", + "build-target", + "bytemuck", + "cc", + "chrono", + "console_error_panic_hook", "flutter_rust_bridge_macros", + "js-sys", "lazy_static", + "libc", + "log", "parking_lot 0.12.1", "threadpool", + "wasm-bindgen", + "web-sys", ] [[package]] name = "flutter_rust_bridge_codegen" -version = "1.32.0" -source = "git+https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge#e5adce55eea0b74d3680e66a2c5252edf17b07e1" +version = "1.61.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efd7396bc479eae8aa24243e4c0e3d3dbda1909134f8de6bde4f080d262c9a0d" dependencies = [ "anyhow", "cargo_metadata", "cbindgen", + "clap 3.2.23", "convert_case", + "delegate", "enum_dispatch", "env_logger 0.9.3", + "extend", + "itertools 0.10.5", "lazy_static", "log", "pathdiff", @@ -1773,17 +1830,18 @@ dependencies = [ "regex", "serde 1.0.149", "serde_yaml", - "structopt", "syn", "tempfile", "thiserror", "toml", + "topological-sort", ] [[package]] name = "flutter_rust_bridge_macros" -version = "1.32.0" -source = "git+https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge#e5adce55eea0b74d3680e66a2c5252edf17b07e1" +version = "1.61.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d5cd827645690ef378be57a890d0581e17c28d07b712872af7d744f454fd27d" [[package]] name = "fnv" @@ -2164,7 +2222,7 @@ checksum = "41486a26d1366a8032b160b59065a59fb528530a46a49f627e7048fb8c064039" dependencies = [ "anyhow", "heck 0.3.3", - "itertools", + "itertools 0.9.0", "proc-macro-crate 0.1.5", "proc-macro-error", "proc-macro2", @@ -2798,6 +2856,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "0.3.4" @@ -5150,30 +5217,6 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" -[[package]] -name = "structopt" -version = "0.3.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10" -dependencies = [ - "clap 2.34.0", - "lazy_static", - "structopt-derive", -] - -[[package]] -name = "structopt-derive" -version = "0.4.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" -dependencies = [ - "heck 0.3.3", - "proc-macro-error", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "strum" version = "0.18.0" @@ -5564,6 +5607,12 @@ dependencies = [ "serde 1.0.149", ] +[[package]] +name = "topological-sort" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea68304e134ecd095ac6c3574494fc62b909f416c4fca77e440530221e549d3d" + [[package]] name = "tower-service" version = "0.3.2" diff --git a/Cargo.toml b/Cargo.toml index 427fcd4e7..1e9af30e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,7 +61,7 @@ num_cpus = "1.13" bytes = { version = "1.2", features = ["serde"] } default-net = { git = "https://github.com/Kingtous/default-net" } wol-rs = "0.9.1" -flutter_rust_bridge = { git = "https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge", optional = true } +flutter_rust_bridge = { version = "1.61.1", optional = true } errno = "0.2.8" rdev = { git = "https://github.com/fufesou/rdev" } url = { version = "2.1", features = ["serde"] } @@ -126,7 +126,7 @@ android_logger = "0.11" jni = "0.19" [target.'cfg(any(target_os = "android", target_os = "ios"))'.dependencies] -flutter_rust_bridge = { git = "https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge" } +flutter_rust_bridge = "1.61.1" [workspace] members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "libs/virtual_display", "libs/virtual_display/dylib", "libs/simple_rc", "libs/portable"] @@ -144,7 +144,7 @@ winapi = { version = "0.3", features = [ "winnt" ] } cc = "1.0" hbb_common = { path = "libs/hbb_common" } simple_rc = { path = "libs/simple_rc", optional = true } -flutter_rust_bridge_codegen = { git = "https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge" } +flutter_rust_bridge_codegen = "1.61.1" [dev-dependencies] hound = "3.5" diff --git a/build.rs b/build.rs index ade63f0bc..d15f27424 100644 --- a/build.rs +++ b/build.rs @@ -85,26 +85,35 @@ fn install_oboe() { #[cfg(feature = "flutter")] fn gen_flutter_rust_bridge() { + use lib_flutter_rust_bridge_codegen::{ + config_parse, frb_codegen, get_symbols_if_no_duplicates, RawOpts, + }; let llvm_path = match std::env::var("LLVM_HOME") { Ok(path) => Some(vec![path]), Err(_) => None, }; // Tell Cargo that if the given file changes, to rerun this build script. println!("cargo:rerun-if-changed=src/flutter_ffi.rs"); - // settings for fbr_codegen - let opts = lib_flutter_rust_bridge_codegen::Opts { + // Options for frb_codegen + let raw_opts = RawOpts { // Path of input Rust code - rust_input: "src/flutter_ffi.rs".to_string(), + rust_input: vec!["src/flutter_ffi.rs".to_string()], // Path of output generated Dart code - dart_output: "flutter/lib/generated_bridge.dart".to_string(), + dart_output: vec!["flutter/lib/generated_bridge.dart".to_string()], // Path of output generated C header c_output: Some(vec!["flutter/macos/Runner/bridge_generated.h".to_string()]), - // for other options lets use default + /// Path to the installed LLVM llvm_path, + // for other options use defaults ..Default::default() }; - // run fbr_codegen - lib_flutter_rust_bridge_codegen::frb_codegen(opts).unwrap(); + // get opts from raw opts + let configs = config_parse(raw_opts); + // generation of rust api for ffi + let all_symbols = get_symbols_if_no_duplicates(&configs).unwrap(); + for config in configs.iter() { + frb_codegen(config, &all_symbols).unwrap(); + } } fn main() { diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 60e2246f5..b7bf2b30e 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1614,15 +1614,6 @@ Widget dialogButton(String text, } } -int get_version_num(String version) { - final list = version.split('.'); - var n = 0; - for (var i = 0; i < list.length; i++) { - n = n * 1000 + (int.tryParse(list[i]) ?? 0); - } - return n; -} - int version_cmp(String v1, String v2) { - return get_version_num(v1) - get_version_num(v2); + return bind.versionToNumber(v: v1) - bind.versionToNumber(v: v2); } diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index cf7a88312..986d93fe8 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -1316,14 +1316,14 @@ class FFI { final cb = ffiModel.startEventListener(id); () async { await for (final message in stream) { - if (message is Event) { + if (message is EventToUI_Event) { try { Map event = json.decode(message.field0); await cb(event); } catch (e) { debugPrint('json.decode fail1(): $e, ${message.field0}'); } - } else if (message is Rgba) { + } else if (message is EventToUI_Rgba) { imageModel.onRgba(message.field0); } } diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index f096218b0..a63f49ba8 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -51,11 +51,7 @@ dependencies: image_picker: ^0.8.5 image: ^3.1.3 back_button_interceptor: ^6.0.1 - flutter_rust_bridge: - git: - url: https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge - ref: master - path: frb_dart + flutter_rust_bridge: ^1.61.1 window_manager: git: url: https://github.com/Kingtous/rustdesk_window_manager @@ -103,6 +99,7 @@ dev_dependencies: build_runner: ^2.1.11 freezed: ^2.0.3 flutter_lints: ^2.0.0 + ffigen: ^7.2.4 # rerun: flutter pub run flutter_launcher_icons:main icons_launcher: diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 874cb4d4d..5468f580c 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1119,6 +1119,10 @@ pub fn query_onlines(ids: Vec) { crate::rendezvous_mediator::query_online_states(ids, handle_query_onlines) } +pub fn version_to_number(v: String) -> SyncReturn { + SyncReturn(hbb_common::get_version_number(&v)) +} + pub fn option_synced() -> bool { crate::ui_interface::option_synced() } From 79461178eacc4fd31a198f9f1b178fb128b6b0eb Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Fri, 20 Jan 2023 00:08:18 -0700 Subject: [PATCH 1571/2015] Update flutter-nightly.yml --- .github/workflows/flutter-nightly.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index efa085ea8..c7855c281 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -560,6 +560,8 @@ jobs: popd mkdir -p signed-apk; pushd signed-apk mv ../rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk . + pwd + ls - uses: r0adkll/sign-android-release@v1 name: Sign app APK @@ -599,6 +601,9 @@ jobs: tag_name: ${{ env.TAG_NAME }} files: | ../rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk + run: | + pwd + ls build-rustdesk-lib-linux-amd64: needs: [generate-bridge-linux, build-vcpkg-deps-linux] From bbb853de9fa75ddac2215bd3b2ad24e156811783 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Fri, 20 Jan 2023 00:09:29 -0700 Subject: [PATCH 1572/2015] Update flutter-nightly.yml --- .github/workflows/flutter-nightly.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index c7855c281..b7ccb08bc 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -600,10 +600,7 @@ jobs: prerelease: true tag_name: ${{ env.TAG_NAME }} files: | - ../rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk - run: | - pwd - ls + ./rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk build-rustdesk-lib-linux-amd64: needs: [generate-bridge-linux, build-vcpkg-deps-linux] From 05f9a2ccf84135e9f7170fae24f248b75d504f54 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Fri, 20 Jan 2023 00:35:04 -0700 Subject: [PATCH 1573/2015] /signed-apk dir --- .github/workflows/flutter-nightly.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index b7ccb08bc..571f1eb34 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -600,7 +600,7 @@ jobs: prerelease: true tag_name: ${{ env.TAG_NAME }} files: | - ./rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk + signed-apk/rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk build-rustdesk-lib-linux-amd64: needs: [generate-bridge-linux, build-vcpkg-deps-linux] From 9c369e5f498b138d7ce4b5953d49a978b5511f88 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Fri, 20 Jan 2023 00:59:27 -0700 Subject: [PATCH 1574/2015] Update flutter-nightly.yml --- .github/workflows/flutter-nightly.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 571f1eb34..57c04e8ff 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -560,8 +560,6 @@ jobs: popd mkdir -p signed-apk; pushd signed-apk mv ../rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk . - pwd - ls - uses: r0adkll/sign-android-release@v1 name: Sign app APK From 3dcada128b600a0c364e8e0ae5266f38c3cfa226 Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 20 Jan 2023 21:03:30 +0800 Subject: [PATCH 1575/2015] fix cur session Signed-off-by: fufesou --- src/flutter.rs | 2 +- src/flutter_ffi.rs | 1 - src/keyboard.rs | 87 ++++++++++++++++++++++--------------- src/ui.rs | 18 +++++--- src/ui_interface.rs | 1 + src/ui_session_interface.rs | 6 +-- 6 files changed, 68 insertions(+), 47 deletions(-) diff --git a/src/flutter.rs b/src/flutter.rs index 1369b5646..f4e2b8363 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -23,7 +23,7 @@ pub(super) const APP_TYPE_DESKTOP_FILE_TRANSFER: &str = "file transfer"; pub(super) const APP_TYPE_DESKTOP_PORT_FORWARD: &str = "port forward"; lazy_static::lazy_static! { - static ref CUR_SESSION_ID: RwLock = Default::default(); + pub static ref CUR_SESSION_ID: RwLock = Default::default(); pub static ref SESSIONS: RwLock>> = Default::default(); pub static ref GLOBAL_EVENT_STREAM: RwLock>> = Default::default(); // rust to dart event channel } diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index ca6823aa5..d92cfba23 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -319,7 +319,6 @@ pub fn session_enter_or_leave(id: String, enter: bool) { #[cfg(not(any(target_os = "android", target_os = "ios")))] if let Some(session) = SESSIONS.read().unwrap().get(&id) { if enter { - crate::keyboard::set_cur_session(session.clone()); session.enter(); } else { session.leave(); diff --git a/src/keyboard.rs b/src/keyboard.rs index 183154ca0..de1abd231 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -2,10 +2,9 @@ use crate::client::get_key_state; use crate::common::GrabState; #[cfg(feature = "flutter")] -use crate::flutter::FlutterHandler; +use crate::flutter::{CUR_SESSION_ID, SESSIONS}; #[cfg(not(any(feature = "flutter", feature = "cli")))] -use crate::ui::remote::SciterHandler; -use crate::ui_session_interface::Session; +use crate::ui::CUR_SESSION; use hbb_common::{log, message_proto::*}; use rdev::{Event, EventType, Key}; #[cfg(any(target_os = "windows", target_os = "macos"))] @@ -22,16 +21,6 @@ static mut IS_ALT_GR: bool = false; #[cfg(any(target_os = "windows", target_os = "macos"))] static KEYBOARD_HOOKED: AtomicBool = AtomicBool::new(false); -#[cfg(feature = "flutter")] -lazy_static::lazy_static! { - static ref CUR_SESSION: Arc>>> = Default::default(); -} - -#[cfg(not(any(feature = "flutter", feature = "cli")))] -lazy_static::lazy_static! { - static ref CUR_SESSION: Arc>>> = Default::default(); -} - lazy_static::lazy_static! { static ref TO_RELEASE: Arc>> = Arc::new(Mutex::new(HashSet::::new())); static ref MODIFIERS_STATE: Mutex> = { @@ -48,23 +37,21 @@ lazy_static::lazy_static! { }; } -#[cfg(feature = "flutter")] -pub fn set_cur_session(session: Session) { - *CUR_SESSION.lock().unwrap() = Some(session); -} - -#[cfg(not(any(feature = "flutter", feature = "cli")))] -pub fn set_cur_session(session: Session) { - *CUR_SESSION.lock().unwrap() = Some(session); -} - pub mod client { use super::*; pub fn get_keyboard_mode() -> String { - #[cfg(not(feature = "cli"))] - if let Some(handler) = CUR_SESSION.lock().unwrap().as_ref() { - return handler.get_keyboard_mode(); + #[cfg(not(any(feature = "flutter", feature = "cli")))] + if let Some(session) = CUR_SESSION.lock().unwrap().as_ref() { + return session.get_keyboard_mode(); + } + #[cfg(feature = "flutter")] + if let Some(session) = SESSIONS + .read() + .unwrap() + .get(&*CUR_SESSION_ID.read().unwrap()) + { + return session.get_keyboard_mode(); } "legacy".to_string() } @@ -164,15 +151,20 @@ pub mod client { } } - pub fn lock_screen() { + pub fn event_lock_screen() -> KeyEvent { let mut key_event = KeyEvent::new(); key_event.set_control_key(ControlKey::LockScreen); key_event.down = true; key_event.mode = KeyboardMode::Legacy.into(); - send_key_event(&key_event); + key_event } - pub fn ctrl_alt_del() { + #[inline] + pub fn lock_screen() { + send_key_event(&event_lock_screen()); + } + + pub fn event_ctrl_alt_del() -> KeyEvent { let mut key_event = KeyEvent::new(); if get_peer_platform() == "Windows" { key_event.set_control_key(ControlKey::CtrlAltDel); @@ -183,7 +175,12 @@ pub mod client { key_event.press = true; } key_event.mode = KeyboardMode::Legacy.into(); - send_key_event(&key_event); + key_event + } + + #[inline] + pub fn ctrl_alt_del() { + send_key_event(&event_ctrl_alt_del()); } } @@ -254,6 +251,8 @@ pub fn release_remote_keys() { for key in to_release { let event_type = EventType::KeyRelease(key); let event = event_type_to_event(event_type); + // to-do: BUG + // Release events should be sent to the corresponding sessions, instead of current session. client::process_event(&event, None); } } @@ -382,16 +381,32 @@ pub fn event_type_to_event(event_type: EventType) -> Event { } pub fn send_key_event(key_event: &KeyEvent) { - #[cfg(not(feature = "cli"))] - if let Some(handler) = CUR_SESSION.lock().unwrap().as_ref() { - handler.send_key_event(key_event); + #[cfg(not(any(feature = "flutter", feature = "cli")))] + if let Some(session) = CUR_SESSION.lock().unwrap().as_ref() { + session.send_key_event(key_event); + } + #[cfg(feature = "flutter")] + if let Some(session) = SESSIONS + .read() + .unwrap() + .get(&*CUR_SESSION_ID.read().unwrap()) + { + session.send_key_event(key_event); } } pub fn get_peer_platform() -> String { - #[cfg(not(feature = "cli"))] - if let Some(handler) = CUR_SESSION.lock().unwrap().as_ref() { - return handler.peer_platform(); + #[cfg(not(any(feature = "flutter", feature = "cli")))] + if let Some(session) = CUR_SESSION.lock().unwrap().as_ref() { + return session.peer_platform(); + } + #[cfg(feature = "flutter")] + if let Some(session) = SESSIONS + .read() + .unwrap() + .get(&*CUR_SESSION_ID.read().unwrap()) + { + return session.peer_platform(); } "Windows".to_string() } diff --git a/src/ui.rs b/src/ui.rs index 4cd9ce3f7..b8473072d 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -13,9 +13,9 @@ use hbb_common::{ log, }; -use crate::common::get_app_name; -use crate::ipc; -use crate::ui_interface::*; +#[cfg(not(any(feature = "flutter", feature = "cli")))] +use crate::ui_session_interface::Session; +use crate::{common::get_app_name, ipc, ui_interface::*}; mod cm; #[cfg(feature = "inline")] @@ -35,6 +35,11 @@ lazy_static::lazy_static! { static ref STUPID_VALUES: Mutex>>> = Default::default(); } +#[cfg(not(any(feature = "flutter", feature = "cli")))] +lazy_static::lazy_static! { + pub static ref CUR_SESSION: Arc>>> = Default::default(); +} + struct UIHostHandler; pub fn start(args: &mut [String]) { @@ -119,9 +124,10 @@ pub fn start(args: &mut [String]) { frame.register_behavior("native-remote", move || { let handler = remote::SciterSession::new(cmd.clone(), id.clone(), pass.clone(), args.clone()); - #[cfg(not(feature = "flutter"))] - crate::keyboard::set_cur_session(handler.inner()); - + #[cfg(not(any(feature = "flutter", feature = "cli")))] + { + *CUR_SESSION.lock().unwrap() = Some(handler.inner()); + } Box::new(handler) }); page = "remote.html"; diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 2e6ef561c..ebaf8c317 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -577,6 +577,7 @@ pub fn is_installed_daemon(_prompt: bool) -> bool { } #[inline] +#[cfg(feature = "flutter")] pub fn is_can_input_monitoring(_prompt: bool) -> bool { #[cfg(target_os = "macos")] return crate::platform::macos::is_can_input_monitoring(_prompt); diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 00f1f90cf..00d046882 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -4,7 +4,7 @@ use crate::client::{ input_os_password, load_config, send_mouse, start_video_audio_threads, FileManager, Key, LoginConfigHandler, QualityStatus, KEY_MAP, }; -use crate::common::{self, is_keyboard_mode_supported, GrabState}; +use crate::common::{self, GrabState}; use crate::keyboard; use crate::{client::Data, client::Interface}; use async_trait::async_trait; @@ -780,10 +780,10 @@ impl Interface for Session { impl Session { pub fn lock_screen(&self) { - crate::keyboard::client::lock_screen(); + self.send_key_event(&crate::keyboard::client::event_lock_screen()); } pub fn ctrl_alt_del(&self) { - crate::keyboard::client::ctrl_alt_del(); + self.send_key_event(&crate::keyboard::client::event_ctrl_alt_del()); } } From efe9ba18cae424568f67e7004ad986453f47b422 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sat, 21 Jan 2023 13:02:46 +0800 Subject: [PATCH 1576/2015] fix: --install cannot be invoke caused by singleton --- flutter/windows/runner/main.cpp | 49 ++++++++++++++++++++++----------- 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/flutter/windows/runner/main.cpp b/flutter/windows/runner/main.cpp index d76b8c040..7437b0344 100644 --- a/flutter/windows/runner/main.cpp +++ b/flutter/windows/runner/main.cpp @@ -1,20 +1,21 @@ #include #include -#include #include +#include +#include + +#include #include #include "flutter_window.h" #include "utils.h" -// #include - -#include typedef char** (*FUNC_RUSTDESK_CORE_MAIN)(int*); typedef void (*FUNC_RUSTDESK_FREE_ARGS)( char**, int); const char* uniLinksPrefix = "rustdesk://"; +/// Note: `--server`, `--service` are already handled in [core_main.rs]. +const std::vector parameters_white_list = {"--install"}; -// auto bdw = bitsdojo_window_configure(BDW_CUSTOM_FRAME | BDW_HIDE_ON_STARTUP); int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, _In_ wchar_t *command_line, _In_ int show_command) { @@ -40,6 +41,10 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, } std::vector command_line_arguments = GetCommandLineArguments(); + // Remove possible trailing whitespace from command line arguments + for (auto& argument : command_line_arguments) { + argument.erase(argument.find_last_not_of(" \n\r\t")); + } int args_len = 0; char** c_args = rustdesk_core_main(&args_len); @@ -51,21 +56,33 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, std::vector rust_args(c_args, c_args + args_len); free_c_args(c_args, args_len); - // uni links dispatch + // Uri links dispatch HWND hwnd = ::FindWindow(_T("FLUTTER_RUNNER_WIN32_WINDOW"), _T("RustDesk")); if (hwnd != NULL) { - if (!command_line_arguments.empty()) { - // Dispatch command line arguments - DispatchToUniLinksDesktop(hwnd); - } else { - // Not called with arguments, or just open the app shortcut on desktop. - // So we just show the main window instead. - ::ShowWindow(hwnd, SW_NORMAL); - ::SetForegroundWindow(hwnd); + // Allow multiple flutter instances when being executed by parameters + // contained in whitelists. + bool allow_multiple_instances = false; + for (auto& whitelist_param : parameters_white_list) { + allow_multiple_instances = + allow_multiple_instances || + std::find(command_line_arguments.begin(), + command_line_arguments.end(), + whitelist_param) != command_line_arguments.end(); + } + if (!allow_multiple_instances) { + if (!command_line_arguments.empty()) { + // Dispatch command line arguments + DispatchToUniLinksDesktop(hwnd); + } else { + // Not called with arguments, or just open the app shortcut on desktop. + // So we just show the main window instead. + ::ShowWindow(hwnd, SW_NORMAL); + ::SetForegroundWindow(hwnd); + } + return EXIT_FAILURE; } - return EXIT_FAILURE; } - + // Attach to console when present (e.g., 'flutter run') or create a // new console when running with a debugger. if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) From d493a6c27a026807fb1c674dde883a1fc33a12b3 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sat, 21 Jan 2023 13:16:07 +0800 Subject: [PATCH 1577/2015] opt: add --cm --- flutter/windows/runner/main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/windows/runner/main.cpp b/flutter/windows/runner/main.cpp index 7437b0344..b7fa64dc0 100644 --- a/flutter/windows/runner/main.cpp +++ b/flutter/windows/runner/main.cpp @@ -14,7 +14,7 @@ typedef char** (*FUNC_RUSTDESK_CORE_MAIN)(int*); typedef void (*FUNC_RUSTDESK_FREE_ARGS)( char**, int); const char* uniLinksPrefix = "rustdesk://"; /// Note: `--server`, `--service` are already handled in [core_main.rs]. -const std::vector parameters_white_list = {"--install"}; +const std::vector parameters_white_list = {"--install", "--cm"}; int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, _In_ wchar_t *command_line, _In_ int show_command) From 84a45ac48fe69b5c67f698e07ad54e3f99e5fe46 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sat, 21 Jan 2023 18:02:04 +0800 Subject: [PATCH 1578/2015] hide switch sides menu until #2893 fixed Signed-off-by: 21pages --- flutter/lib/desktop/widgets/remote_menubar.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 62289d5f0..227002645 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -652,7 +652,8 @@ class _RemoteMenubarState extends State { dismissOnClicked: true, )); } - if (pi.platform != kPeerPlatformAndroid && + if (false && + pi.platform != kPeerPlatformAndroid && version_cmp(peer_version, '1.2.0') >= 0) { displayMenu.add(MenuEntryButton( childBuilder: (TextStyle? style) => Text( From 1a3f1d38fb2d049d33bb748b1f6b69291997d6a7 Mon Sep 17 00:00:00 2001 From: NicKoehler <53040044+NicKoehler@users.noreply.github.com> Date: Sat, 21 Jan 2023 13:51:33 +0100 Subject: [PATCH 1579/2015] Update it.rs --- src/lang/it.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/lang/it.rs b/src/lang/it.rs index 2e313e101..7b979aff0 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -13,7 +13,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Service is running", "Il servizio è in esecuzione"), ("Service is not running", "Il servizio non è in esecuzione"), ("not_ready_status", "Non pronto. Verifica la tua connessione"), - ("Control Remote Desktop", "Controlla una scrivania remota"), + ("Control Remote Desktop", "Controlla un desktop remoto"), ("Transfer File", "Trasferisci file"), ("Connect", "Connetti"), ("Recent Sessions", "Sessioni recenti"), @@ -372,7 +372,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable LAN Discovery", "Abilita il rilevamento della LAN"), ("Deny LAN Discovery", "Nega il rilevamento della LAN"), ("Write a message", "Scrivi un messaggio"), - ("Prompt", "Prompt"), + ("Prompt", "Richiede"), ("Please wait for confirmation of UAC...", "Attendi la conferma dell'UAC..."), ("elevated_foreground_window_tip", "La finestra corrente del desktop remoto richiede privilegi più elevati per funzionare, quindi non è in grado di utilizzare temporaneamente il mouse e la tastiera. È possibile chiedere all'utente remoto di ridurre a icona la finestra corrente o di fare clic sul pulsante di elevazione nella finestra di gestione della connessione. Per evitare questo problema, si consiglia di installare il software sul dispositivo remoto."), ("Disconnected", "Disconnesso"), @@ -423,15 +423,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request Elevation", "Richiedi elevazione dei diritti"), ("wait_accept_uac_tip", "Attendere che l'utente remoto accetti la finestra di dialogo UAC."), ("Elevate successfully", "Elevazione dei diritti effettuata con successo"), - ("uppercase", "maiuscolo"), - ("lowercase", "minuscolo"), - ("digit", "numero"), - ("special character", "carattere speciale"), - ("length>=8", "lunghezza >= 8"), + ("uppercase", "Maiuscola"), + ("lowercase", "Minuscola"), + ("digit", "Numero"), + ("special character", "Carattere speciale"), + ("length>=8", "Lunghezza >= 8"), ("Weak", "Debole"), ("Medium", "Media"), ("Strong", "Forte"), - ("Switch Sides", ""), - ("Please confirm if you want to share your desktop?", ""), + ("Switch Sides", "Cambia lato"), + ("Please confirm if you want to share your desktop?", "Vuoi condividere il tuo desktop?"), ].iter().cloned().collect(); } From 761838495a819d7515375e6d15e51ccf8bf38d1d Mon Sep 17 00:00:00 2001 From: Mr-Update <37781396+Mr-Update@users.noreply.github.com> Date: Sat, 21 Jan 2023 14:42:36 +0100 Subject: [PATCH 1580/2015] Update de.rs --- src/lang/de.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lang/de.rs b/src/lang/de.rs index 7394a4628..a567877a2 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -145,7 +145,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Configure", "Konfigurieren"), ("config_acc", "Um Ihren PC aus der Ferne zu steuern, müssen Sie RustDesk Zugriffsrechte erteilen."), ("config_screen", "Um aus der Ferne auf Ihren PC zugreifen zu können, müssen Sie RustDesk die Berechtigung \"Bildschirmaufnahme\" erteilen."), - ("Installing ...", "Installiere..."), + ("Installing ...", "Installieren..."), ("Install", "Installieren"), ("Installation", "Installation"), ("Installation Path", "Installationspfad"), @@ -201,7 +201,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("x11 expected", "X11 erwartet"), ("Port", "Port"), ("Settings", "Einstellungen"), - ("Username", " Benutzername"), + ("Username", "Benutzername"), ("Invalid port", "Ungültiger Port"), ("Closed manually by the peer", "Von der Gegenstelle manuell geschlossen"), ("Enable remote configuration modification", "Änderung der Konfiguration aus der Ferne zulassen"), @@ -431,7 +431,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", "Schwach"), ("Medium", "Mittel"), ("Strong", "Stark"), - ("Switch Sides", ""), - ("Please confirm if you want to share your desktop?", ""), + ("Switch Sides", "Seiten wechseln"), + ("Please confirm if you want to share your desktop?", "Bitte bestätigen Sie, ob Sie Ihren Desktop freigeben möchten."), ].iter().cloned().collect(); } From 3ee76fcf4f47ff2582788ea36a4fb6a061b43bee Mon Sep 17 00:00:00 2001 From: solokot Date: Sat, 21 Jan 2023 17:32:58 +0300 Subject: [PATCH 1581/2015] Update ru.rs --- src/lang/ru.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/ru.rs b/src/lang/ru.rs index a7e42e0e4..8103ae3a3 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -431,7 +431,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", "Слабый"), ("Medium", "Средний"), ("Strong", "Стойкий"), - ("Switch Sides", ""), - ("Please confirm if you want to share your desktop?", ""), + ("Switch Sides", "Переключить стороны"), + ("Please confirm if you want to share your desktop?", "Подтвердите, что хотите поделиться своим рабочим столом?"), ].iter().cloned().collect(); } From 490b4cbbb9ed711cce9179b6a1d5f55746fa3726 Mon Sep 17 00:00:00 2001 From: "Miguel F. G" <116861809+flusheDData@users.noreply.github.com> Date: Sat, 21 Jan 2023 18:51:01 +0100 Subject: [PATCH 1582/2015] 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 5161f9846..2b109c18f 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -431,7 +431,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", "Débil"), ("Medium", "Media"), ("Strong", "Fuerte"), - ("Switch Sides", ""), - ("Please confirm if you want to share your desktop?", ""), + ("Switch Sides", "Intercambiar lados"), + ("Please confirm if you want to share your desktop?", "Por favor, confirma si quieres compartir tu escritorio"), ].iter().cloned().collect(); } From 03b2546008688f6c259eef7b9161c46278db7384 Mon Sep 17 00:00:00 2001 From: Jimmy GALLAND Date: Sat, 21 Jan 2023 22:26:33 +0100 Subject: [PATCH 1583/2015] update FR --- src/lang/fr.rs | 48 ++++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 9c9860fb2..ea2dbfede 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -14,8 +14,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Service is not running", "Le service ne fonctionne pas"), ("not_ready_status", "Pas prêt, veuillez vérifier la connexion réseau"), ("Control Remote Desktop", "Contrôler le bureau à distance"), - ("Transfer File", "Transférer le fichier"), - ("Connect", "Connecter"), + ("Transfer File", "Transfert de fichiers"), + ("Connect", "Se connecter"), ("Recent Sessions", "Sessions récentes"), ("Address Book", "Carnet d'adresses"), ("Confirmation", "Confirmation"), @@ -303,7 +303,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("In privacy mode", "en mode privé"), ("Out privacy mode", "hors mode de confidentialité"), ("Language", "Langue"), - ("Keep RustDesk background service", "Gardez le service Rustdesk service arrière plan"), + ("Keep RustDesk background service", "Gardez le service RustDesk service arrière plan"), ("Ignore Battery Optimizations", "Ignorer les optimisations batterie"), ("android_open_battery_optimizations_tip", "Conseil android d'optimisation de batterie"), ("Connection not allowed", "Connexion non autorisée"), @@ -412,26 +412,26 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Select local keyboard type", "Selectionner la disposition du clavier local"), ("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\"."), - ("request_elevation_tip", ""), - ("Wait", ""), - ("Elevation Error", ""), - ("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", ""), - ("Request Elevation", ""), - ("wait_accept_uac_tip", ""), - ("Elevate successfully", ""), - ("uppercase", ""), - ("lowercase", ""), - ("digit", ""), - ("special character", ""), - ("length>=8", ""), - ("Weak", ""), - ("Medium", ""), - ("Strong", ""), - ("Switch Sides", ""), - ("Please confirm if you want to share your desktop?", ""), + ("config_input", "Afin de contrôler le bureau à distance avec le clavier, vous devez accorder à RustDesk l'autorisation \"Surveillance de l’entrée\"."), + ("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"), + ("Ask the remote user for authentication", "Demander à l'utilisateur distant de s'authentifier"), + ("Choose this if the remote account is administrator", "Choisissez ceci si le compte distant est le compte d'administrateur"), + ("Transmit the username and password of administrator", "Transmettre le nom d'utilisateur et le mot de passe de l'administrateur"), + ("still_click_uac_tip", "Nécessite toujours que l'utilisateur distant confirme par la fenêtre UAC de RustDesk en cours d'éxécution."), + ("Request Elevation", "Demande d'augmentation des privilèges"), + ("wait_accept_uac_tip", "Veuillez attendre que l'utilisateur distant accepte la boîte de dialogue UAC."), + ("Elevate successfully", "Augmentation des privilèges avec succès"), + ("uppercase", "majuscule"), + ("lowercase", "minuscule"), + ("digit", "chiffre"), + ("special character", "caractère spécial"), + ("length>=8", "longueur>=8"), + ("Weak", "Faible"), + ("Medium", "Moyen"), + ("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 ?"), ].iter().cloned().collect(); } From b556b5d7f6935c4a027d9127dafc8a07fa61d1cc Mon Sep 17 00:00:00 2001 From: Jimmy GALLAND Date: Sat, 21 Jan 2023 23:07:48 +0100 Subject: [PATCH 1584/2015] add templates/translations for Tab Home label, and two other label translations in About tab --- 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 | 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/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 +++ 31 files changed, 93 insertions(+) diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 72f55b44b..64b9bb35f 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -41,6 +41,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "Sobre"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Mute", "Silenciar"), ("Audio Input", "Entrada d'àudio"), ("Enhancements", "Millores"), diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 14e8a463d..b95f79972 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -42,6 +42,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Slogan_tip", ""), ("Privacy Statement", ""), ("Mute", "静音"), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Audio Input", "音频输入"), ("Enhancements", "增强功能"), ("Hardware Codec", "硬件编解码"), diff --git a/src/lang/cs.rs b/src/lang/cs.rs index e2935770c..b40f79405 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -42,6 +42,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Slogan_tip", ""), ("Privacy Statement", ""), ("Mute", "Ztlumit"), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Audio Input", "Vstup zvuku"), ("Enhancements", ""), ("Hardware Codec", ""), diff --git a/src/lang/da.rs b/src/lang/da.rs index 937990ea8..8bd3e9a7b 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -42,6 +42,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Slogan_tip", ""), ("Privacy Statement", ""), ("Mute", "Sluk for mikrofonen"), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Audio Input", "Lydindgang"), ("Enhancements", ""), ("Hardware Codec", ""), diff --git a/src/lang/de.rs b/src/lang/de.rs index 7394a4628..e1adc224b 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -42,6 +42,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Slogan_tip", "Mit Herzblut programmiert - in einer Welt, die im Chaos versinkt!"), ("Privacy Statement", "Datenschutz"), ("Mute", "Stummschalten"), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Audio Input", "Audioeingang"), ("Enhancements", "Verbesserungen"), ("Hardware Codec", "Hardware-Codec"), diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 839c69bbb..cbaa013d5 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -41,6 +41,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "Pri"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Mute", "Muta"), ("Audio Input", "Aŭdia enigo"), ("Enhancements", ""), diff --git a/src/lang/es.rs b/src/lang/es.rs index 5161f9846..613497476 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -41,6 +41,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "Acerca de"), ("Slogan_tip", "Hecho con corazón en este mundo caótico!"), ("Privacy Statement", "Declaración de privacidad"), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Mute", "Silenciar"), ("Audio Input", "Entrada de audio"), ("Enhancements", "Mejoras"), diff --git a/src/lang/fa.rs b/src/lang/fa.rs index dfd76405e..a7a4df073 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -41,6 +41,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "درباره"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Mute", "بستن صدا"), ("Audio Input", "ورودی صدا"), ("Enhancements", "بهبودها"), diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 9c9860fb2..273760aac 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -41,6 +41,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "À propos de"), ("Slogan_tip", "Fait avec cœur dans ce monde chaotique!"), ("Privacy Statement", "Déclaration de confidentialité"), + ("Build Date", "Date de compilation"), + ("Version", "Version"), + ("Home", "Accueil"), ("Mute", "Muet"), ("Audio Input", "Entrée audio"), ("Enhancements", "Améliorations"), diff --git a/src/lang/gr.rs b/src/lang/gr.rs index 6ec1152cd..b50f9fbf5 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -41,6 +41,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "Πληροφορίες"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Mute", "Σίγαση"), ("Audio Input", "Είσοδος ήχου"), ("Enhancements", "Βελτιώσεις"), diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 295104a67..c4e791da8 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -42,6 +42,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Slogan_tip", ""), ("Privacy Statement", ""), ("Mute", "Némítás"), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Audio Input", "Hangátvitel"), ("Enhancements", "Fejlesztések"), ("Hardware Codec", "Hardware kodek"), diff --git a/src/lang/id.rs b/src/lang/id.rs index 5604a0c52..9b8fb9f33 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -41,6 +41,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "Tentang"), ("Slogan_tip", ""), ("Privacy Statement", "Pernyataan Privasi"), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Mute", "Bisukan"), ("Audio Input", "Masukkan Audio"), ("Enhancements", "Peningkatan"), diff --git a/src/lang/it.rs b/src/lang/it.rs index 7b979aff0..e56893249 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -41,6 +41,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "Informazioni"), ("Slogan_tip", "Fatta con il cuore in questo mondo caotico!"), ("Privacy Statement", "Informativa sulla privacy"), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Mute", "Silenzia"), ("Audio Input", "Input audio"), ("Enhancements", "Miglioramenti"), diff --git a/src/lang/ja.rs b/src/lang/ja.rs index a280940c7..04b199950 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -41,6 +41,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "情報"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Mute", "ミュート"), ("Audio Input", "音声入力デバイス"), ("Enhancements", "追加機能"), diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 1cdf529ce..a2d55bd1c 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -41,6 +41,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "정보"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Mute", "음소거"), ("Audio Input", "오디오 입력"), ("Enhancements", ""), diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 59d26135f..328f4c29b 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -41,6 +41,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "Туралы"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Mute", "Дыбыссыздандыру"), ("Audio Input", "Аудио Еңгізу"), ("Enhancements", "Жақсартулар"), diff --git a/src/lang/pl.rs b/src/lang/pl.rs index ee4b45334..061b97f99 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -41,6 +41,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "O"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Mute", "Wycisz"), ("Audio Input", "Wejście audio"), ("Enhancements", "Ulepszenia"), diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 66373a5e9..8ae67062c 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -41,6 +41,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "Sobre"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Mute", "Silenciar"), ("Audio Input", "Entrada de Áudio"), ("Enhancements", "Melhorias"), diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 5a137f391..fbe2c1cf8 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -41,6 +41,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "Sobre"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Mute", "Desativar som"), ("Audio Input", "Entrada de Áudio"), ("Enhancements", "Melhorias"), diff --git a/src/lang/ru.rs b/src/lang/ru.rs index a7e42e0e4..4f222291e 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -41,6 +41,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "О программе"), ("Slogan_tip", "Сделано с душой в этом безумном мире!"), ("Privacy Statement", "Заявление о конфиденциальности"), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Mute", "Отключить звук"), ("Audio Input", "Аудиовход"), ("Enhancements", "Улучшения"), diff --git a/src/lang/sk.rs b/src/lang/sk.rs index c735cb28c..609523f05 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -41,6 +41,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "O RustDesk"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Mute", "Stíšiť"), ("Audio Input", "Zvukový vstup"), ("Enhancements", ""), diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 6a17cc906..c779ca33c 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -41,6 +41,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "O programu"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Mute", "Izklopi zvok"), ("Audio Input", "Avdio vhod"), ("Enhancements", "Izboljšave"), diff --git a/src/lang/sq.rs b/src/lang/sq.rs index ebb43f6b7..1939f6275 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -41,6 +41,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "Rreth"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Mute", "Pa zë"), ("Audio Input", "Inputi zërit"), ("Enhancements", "Përmirësimet"), diff --git a/src/lang/sr.rs b/src/lang/sr.rs index d9463318d..91c8f31b3 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -41,6 +41,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "O programu"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Mute", "Utišaj"), ("Audio Input", "Audio ulaz"), ("Enhancements", "Proširenja"), diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 146e60f9a..65bfc5122 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -41,6 +41,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "Om"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Mute", "Tyst"), ("Audio Input", "Ljud input"), ("Enhancements", "Förbättringar"), diff --git a/src/lang/template.rs b/src/lang/template.rs index 729932973..bd2d44d80 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -41,6 +41,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", ""), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Mute", ""), ("Audio Input", ""), ("Enhancements", ""), diff --git a/src/lang/th.rs b/src/lang/th.rs index a78509e59..7e1d8c45a 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -41,6 +41,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "เกี่ยวกับ"), ("Slogan_tip", "ทำด้วยใจ ในโลกใบนี้ที่ยุ่งเหยิง!"), ("Privacy Statement", "คำแถลงเกี่ยวกับความเป็นส่วนตัว"), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Mute", "ปิดเสียง"), ("Audio Input", "ออดิโออินพุท"), ("Enhancements", "การปรับปรุง"), diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 483ee67e3..47e59e551 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -41,6 +41,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "Hakkında"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Mute", "Sustur"), ("Audio Input", "Ses Girişi"), ("Enhancements", "Geliştirmeler"), diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 459c517ff..9404c1192 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -41,6 +41,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "關於"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Mute", "靜音"), ("Audio Input", "音訊輸入"), ("Enhancements", "增強功能"), diff --git a/src/lang/ua.rs b/src/lang/ua.rs index ca99be12e..eadd7ed84 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -41,6 +41,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "Про RustDesk"), ("Slogan_tip", "Створено з душею в цьому хаотичному світі!"), ("Privacy Statement", "Декларація про конфіденційність"), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Mute", "Вимкнути звук"), ("Audio Input", "Аудіовхід"), ("Enhancements", "Покращення"), diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 53de4e67c..c5d44ebc7 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -41,6 +41,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "About"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Mute", "Tắt tiếng"), ("Audio Input", "Đầu vào âm thanh"), ("Enhancements", "Các tiện itchs"), From 87a2705ba505223a7869f886911da6e09ca47298 Mon Sep 17 00:00:00 2001 From: fufesou Date: Sun, 22 Jan 2023 20:23:11 +0800 Subject: [PATCH 1585/2015] refactor, remove peer type Signed-off-by: fufesou --- flutter/lib/common/widgets/peer_card.dart | 135 ++++++++---------- .../lib/desktop/widgets/tabbar_widget.dart | 2 +- 2 files changed, 57 insertions(+), 80 deletions(-) diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index c07b458bc..3feaef51d 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -316,21 +316,11 @@ class _PeerCardState extends State<_PeerCard> bool get wantKeepAlive => true; } -enum CardType { - recent, - fav, - lan, - ab, - grp, -} - abstract class BasePeerCard extends StatelessWidget { final Peer peer; final EdgeInsets? menuPadding; - final CardType cardType; - BasePeerCard( - {required this.peer, required this.cardType, this.menuPadding, Key? key}) + BasePeerCard({required this.peer, this.menuPadding, Key? key}) : super(key: key); @override @@ -435,7 +425,7 @@ abstract class BasePeerCard extends StatelessWidget { if (Navigator.canPop(context)) { Navigator.pop(context); } - _rdpDialog(id, cardType); + _rdpDialog(id); }, )), )) @@ -480,6 +470,12 @@ abstract class BasePeerCard extends StatelessWidget { ); } + @protected + Future _isForceAlwaysRelay(String id) async { + return (await bind.mainGetPeerOption(id: id, key: 'force-always-relay')) + .isNotEmpty; + } + @protected Future> _forceAlwaysRelayAction(String id) async { const option = 'force-always-relay'; @@ -487,16 +483,12 @@ abstract class BasePeerCard extends StatelessWidget { switchType: SwitchType.scheckbox, text: translate('Always connect via relay'), getter: () async { - if (cardType == CardType.ab) { - return gFFI.abModel.find(id)?.forceAlwaysRelay ?? false; - } else { - return (await bind.mainGetPeerOption(id: id, key: option)).isNotEmpty; - } + return await _isForceAlwaysRelay(id); }, setter: (bool v) async { gFFI.abModel.setPeerForceAlwaysRelay(id, v); await bind.mainSetPeerOption( - id: id, key: option, value: bool2option('force-always-relay', v)); + id: id, key: option, value: bool2option(option, v)); }, padding: menuPadding, dismissOnClicked: true, @@ -621,14 +613,13 @@ abstract class BasePeerCard extends StatelessWidget { ); } + @protected + Future _getAlias(String id) async => + await bind.mainGetPeerOption(id: id, key: 'alias'); + void _rename(String id) async { RxBool isInProgress = false.obs; - String name; - if (cardType == CardType.ab) { - name = gFFI.abModel.find(id)?.alias ?? ""; - } else { - name = await bind.mainGetPeerOption(id: id, key: 'alias'); - } + String name = await _getAlias(id); var controller = TextEditingController(text: name); gFFI.dialogManager.show((setState, close) { submit() async { @@ -636,7 +627,7 @@ abstract class BasePeerCard extends StatelessWidget { String name = controller.text.trim(); await bind.mainSetPeerAlias(id: id, alias: name); gFFI.abModel.setPeerAlias(id, name); - update(); + _update(); close(); isInProgress.value = false; } @@ -671,34 +662,13 @@ abstract class BasePeerCard extends StatelessWidget { }); } - void update() { - switch (cardType) { - case CardType.recent: - bind.mainLoadRecentPeers(); - break; - case CardType.fav: - bind.mainLoadFavPeers(); - break; - case CardType.lan: - bind.mainLoadLanPeers(); - break; - case CardType.ab: - gFFI.abModel.pullAb(); - break; - case CardType.grp: - gFFI.groupModel.pull(); - break; - } - } + @protected + void _update(); } class RecentPeerCard extends BasePeerCard { RecentPeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key}) - : super( - peer: peer, - cardType: CardType.recent, - menuPadding: menuPadding, - key: key); + : super(peer: peer, menuPadding: menuPadding, key: key); @override Future>> _buildMenuItems( @@ -732,15 +702,15 @@ class RecentPeerCard extends BasePeerCard { } return menuItems; } + + @protected + @override + void _update() => bind.mainLoadRecentPeers(); } class FavoritePeerCard extends BasePeerCard { FavoritePeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key}) - : super( - peer: peer, - cardType: CardType.fav, - menuPadding: menuPadding, - key: key); + : super(peer: peer, menuPadding: menuPadding, key: key); @override Future>> _buildMenuItems( @@ -776,15 +746,15 @@ class FavoritePeerCard extends BasePeerCard { } return menuItems; } + + @protected + @override + void _update() => bind.mainLoadFavPeers(); } class DiscoveredPeerCard extends BasePeerCard { DiscoveredPeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key}) - : super( - peer: peer, - cardType: CardType.lan, - menuPadding: menuPadding, - key: key); + : super(peer: peer, menuPadding: menuPadding, key: key); @override Future>> _buildMenuItems( @@ -811,15 +781,15 @@ class DiscoveredPeerCard extends BasePeerCard { } return menuItems; } + + @protected + @override + void _update() => bind.mainLoadLanPeers(); } class AddressBookPeerCard extends BasePeerCard { AddressBookPeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key}) - : super( - peer: peer, - cardType: CardType.ab, - menuPadding: menuPadding, - key: key); + : super(peer: peer, menuPadding: menuPadding, key: key); @override Future>> _buildMenuItems( @@ -851,6 +821,20 @@ class AddressBookPeerCard extends BasePeerCard { return menuItems; } + @protected + @override + Future _isForceAlwaysRelay(String id) async => + gFFI.abModel.find(id)?.forceAlwaysRelay ?? false; + + @protected + @override + Future _getAlias(String id) async => + gFFI.abModel.find(id)?.alias ?? ''; + + @protected + @override + void _update() => gFFI.abModel.pullAb(); + @protected @override MenuEntryBase _removeAction( @@ -943,11 +927,7 @@ class AddressBookPeerCard extends BasePeerCard { class MyGroupPeerCard extends BasePeerCard { MyGroupPeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key}) - : super( - peer: peer, - cardType: CardType.grp, - menuPadding: menuPadding, - key: key); + : super(peer: peer, menuPadding: menuPadding, key: key); @override Future>> _buildMenuItems( @@ -974,18 +954,15 @@ class MyGroupPeerCard extends BasePeerCard { } return menuItems; } + + @protected + @override + void _update() => gFFI.groupModel.pull(); } -void _rdpDialog(String id, CardType card) async { - String port, username; - if (card == CardType.ab) { - port = gFFI.abModel.find(id)?.rdpPort ?? ''; - username = gFFI.abModel.find(id)?.rdpUsername ?? ''; - } else { - port = await bind.mainGetPeerOption(id: id, key: 'rdp_port'); - username = await bind.mainGetPeerOption(id: id, key: 'rdp_username'); - } - +void _rdpDialog(String id) async { + final port = await bind.mainGetPeerOption(id: id, key: 'rdp_port'); + final username = await bind.mainGetPeerOption(id: id, key: 'rdp_username'); final portController = TextEditingController(text: port); final userController = TextEditingController(text: username); final passwordController = TextEditingController( diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index ec494cf22..d4fcd16e9 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -831,7 +831,7 @@ class _TabState extends State<_Tab> with RestorationMixin { return ConstrainedBox( constraints: BoxConstraints(maxWidth: widget.maxLabelWidth ?? 200), child: Text( - translate(widget.label.value), + widget.label.value, textAlign: TextAlign.center, style: TextStyle( color: isSelected From 24660628a5c9ca3571978a249230106b7faf8305 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Sun, 22 Jan 2023 20:31:27 -0700 Subject: [PATCH 1586/2015] enable i686 --- .github/workflows/flutter-nightly.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index b33a6dba0..8344e562c 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -30,7 +30,7 @@ jobs: fail-fast: false matrix: job: - # - { target: i686-pc-windows-msvc , os: windows-2019 } + - { target: i686-pc-windows-msvc , os: windows-2019 } # - { target: x86_64-pc-windows-gnu , os: windows-2019 } - { target: x86_64-pc-windows-msvc, os: windows-2019 } steps: From 5aa4c420a4368c96fdb65eaee191a258ba8845aa Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Sun, 22 Jan 2023 20:38:18 -0700 Subject: [PATCH 1587/2015] update flutter version to 3.3.10 --- .github/workflows/flutter-nightly.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 8344e562c..600dd47a2 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -9,7 +9,7 @@ on: env: LLVM_VERSION: "10.0" # Note: currently 3.0.5 does not support arm64 officially, we use latest stable version first. - FLUTTER_VERSION: "3.0.5" + FLUTTER_VERSION: "3.3.10" TAG_NAME: "nightly" # vcpkg version: 2022.05.10 # for multiarch gcc compatibility From 9793b35ad6c5c01d967bb8b9f36d00bdbb623e66 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Sun, 22 Jan 2023 20:40:55 -0700 Subject: [PATCH 1588/2015] Update flutter-ci.yml --- .github/workflows/flutter-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flutter-ci.yml b/.github/workflows/flutter-ci.yml index 4e98f311d..9dc2bac7d 100644 --- a/.github/workflows/flutter-ci.yml +++ b/.github/workflows/flutter-ci.yml @@ -14,7 +14,7 @@ on: env: LLVM_VERSION: "10.0" # Note: currently 3.0.5 does not support arm64 officially, we use latest stable version first. - FLUTTER_VERSION: "3.0.5" + FLUTTER_VERSION: "3.3.10" # vcpkg version: 2022.05.10 # for multiarch gcc compatibility VCPKG_COMMIT_ID: "14e7bb4ae24616ec54ff6b2f6ef4e8659434ea44" From 0838a77908c07e6443a80465077eb7f0403cc0a0 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Sun, 22 Jan 2023 20:41:48 -0700 Subject: [PATCH 1589/2015] update llvm version --- .github/workflows/flutter-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flutter-ci.yml b/.github/workflows/flutter-ci.yml index 9dc2bac7d..67c485fa7 100644 --- a/.github/workflows/flutter-ci.yml +++ b/.github/workflows/flutter-ci.yml @@ -12,7 +12,7 @@ on: - ".github/**" env: - LLVM_VERSION: "10.0" + LLVM_VERSION: "15.0.6" # Note: currently 3.0.5 does not support arm64 officially, we use latest stable version first. FLUTTER_VERSION: "3.3.10" # vcpkg version: 2022.05.10 From fb641900755f15e61952a487d0d47f996fc150a1 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Sun, 22 Jan 2023 20:42:09 -0700 Subject: [PATCH 1590/2015] update llvm version --- .github/workflows/flutter-nightly.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 600dd47a2..cc6aa2eb8 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -7,7 +7,7 @@ on: workflow_dispatch: env: - LLVM_VERSION: "10.0" + LLVM_VERSION: "15.0.6" # Note: currently 3.0.5 does not support arm64 officially, we use latest stable version first. FLUTTER_VERSION: "3.3.10" TAG_NAME: "nightly" From 3e0ae64d61fdb4b6bb1f1ea5b5d7b713e1487f0c Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Sun, 22 Jan 2023 20:48:11 -0700 Subject: [PATCH 1591/2015] remove custom flutter from windows --- .github/workflows/flutter-ci.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/flutter-ci.yml b/.github/workflows/flutter-ci.yml index 67c485fa7..e7ddba331 100644 --- a/.github/workflows/flutter-ci.yml +++ b/.github/workflows/flutter-ci.yml @@ -47,13 +47,13 @@ jobs: flutter-version: ${{ env.FLUTTER_VERSION }} cache: true - - name: Replace engine with rustdesk custom flutter engine - run: | - flutter doctor -v - flutter precache --windows - Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.0.5-rustdesk.2/windows-x64-flutter-release.zip -OutFile windows-x64-flutter-release.zip - Expand-Archive windows-x64-flutter-release.zip -DestinationPath engine - mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-3.0.5-x64/bin/cache/artifacts/engine/windows-x64-release/ + #- name: Replace engine with rustdesk custom flutter engine + # run: | + # flutter doctor -v + # flutter precache --windows + # Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.0.5-rustdesk.2/windows-x64-flutter-release.zip -OutFile windows-x64-flutter-release.zip + # Expand-Archive windows-x64-flutter-release.zip -DestinationPath engine + # mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-3.0.5-x64/bin/cache/artifacts/engine/windows-x64-release/ - name: Install Rust toolchain uses: actions-rs/toolchain@v1 From 5ee350d58d9d6fd7a3386d0dd706ca0e641ddda8 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Sun, 22 Jan 2023 20:53:39 -0700 Subject: [PATCH 1592/2015] Update flutter-nightly.yml --- .github/workflows/flutter-nightly.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index cc6aa2eb8..362161fae 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -49,13 +49,13 @@ jobs: flutter-version: ${{ env.FLUTTER_VERSION }} cache: true - - name: Replace engine with rustdesk custom flutter engine - run: | - flutter doctor -v - flutter precache --windows - Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.0.5-rustdesk.2/windows-x64-flutter-release.zip -OutFile windows-x64-flutter-release.zip - Expand-Archive windows-x64-flutter-release.zip -DestinationPath engine - mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-3.0.5-x64/bin/cache/artifacts/engine/windows-x64-release/ + #- name: Replace engine with rustdesk custom flutter engine + # run: | + # flutter doctor -v + # flutter precache --windows + # Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.0.5-rustdesk.2/windows-x64-flutter-release.zip -OutFile windows-x64-flutter-release.zip + # Expand-Archive windows-x64-flutter-release.zip -DestinationPath engine + # mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-3.0.5-x64/bin/cache/artifacts/engine/windows-x64-release/ - name: Install Rust toolchain uses: actions-rs/toolchain@v1 From 7f13f28d2921e4cc7a6499ba2ae127b82000663a Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Sun, 22 Jan 2023 21:13:25 -0700 Subject: [PATCH 1593/2015] dont need to install rust toolchain twice --- .github/workflows/flutter-ci.yml | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/.github/workflows/flutter-ci.yml b/.github/workflows/flutter-ci.yml index e7ddba331..feaefc115 100644 --- a/.github/workflows/flutter-ci.yml +++ b/.github/workflows/flutter-ci.yml @@ -503,14 +503,6 @@ jobs: key: ${{ matrix.job.target }}-${{ matrix.job.extra-build-features }} cache-directories: "/opt/rust-registry" - - name: Install Rust toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - target: ${{ matrix.job.target }} - override: true - profile: minimal # minimal component installation (ie, no documentation) - - name: Install local registry run: | mkdir -p /opt/rust-registry @@ -669,14 +661,6 @@ jobs: override: true profile: minimal # minimal component installation (ie, no documentation) - - name: Install Rust toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - target: ${{ matrix.job.target }} - override: true - profile: minimal # minimal component installation (ie, no documentation) - - uses: Swatinem/rust-cache@v2 with: prefix-key: rustdesk-lib-cache From 35a7e4f8b730b553fbad6fc80cc9aec2450e92fc Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Sun, 22 Jan 2023 21:16:46 -0700 Subject: [PATCH 1594/2015] dont need rust toolchain twice --- .github/workflows/flutter-nightly.yml | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 362161fae..b26b6bc38 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -649,14 +649,6 @@ jobs: key: ${{ matrix.job.target }}-${{ matrix.job.extra-build-features }} cache-directories: "/opt/rust-registry" - - name: Install Rust toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - target: ${{ matrix.job.target }} - override: true - profile: minimal # minimal component installation (ie, no documentation) - - name: Install local registry run: | mkdir -p /opt/rust-registry @@ -815,14 +807,6 @@ jobs: override: true profile: minimal # minimal component installation (ie, no documentation) - - name: Install Rust toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - target: ${{ matrix.job.target }} - override: true - profile: minimal # minimal component installation (ie, no documentation) - - uses: Swatinem/rust-cache@v2 with: prefix-key: rustdesk-lib-cache From 5f0aff55009c601bdf7d352c969f04af58732a2a Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Sun, 22 Jan 2023 21:21:37 -0700 Subject: [PATCH 1595/2015] enable i686 --- .github/workflows/flutter-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flutter-ci.yml b/.github/workflows/flutter-ci.yml index feaefc115..efcd02170 100644 --- a/.github/workflows/flutter-ci.yml +++ b/.github/workflows/flutter-ci.yml @@ -28,7 +28,7 @@ jobs: fail-fast: true matrix: job: - # - { target: i686-pc-windows-msvc , os: windows-2019 } + - { target: i686-pc-windows-msvc , os: windows-2019 } # - { target: x86_64-pc-windows-gnu , os: windows-2019 } - { target: x86_64-pc-windows-msvc, os: windows-2019 } steps: From f1236f42e1824d74f1ea8a734dfad0e34016cbc1 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Sun, 22 Jan 2023 21:29:00 -0700 Subject: [PATCH 1596/2015] go back to 3.0.5 --- .github/workflows/flutter-nightly.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index b26b6bc38..6000552c7 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -9,7 +9,7 @@ on: env: LLVM_VERSION: "15.0.6" # Note: currently 3.0.5 does not support arm64 officially, we use latest stable version first. - FLUTTER_VERSION: "3.3.10" + FLUTTER_VERSION: "3.0.5" TAG_NAME: "nightly" # vcpkg version: 2022.05.10 # for multiarch gcc compatibility @@ -49,13 +49,13 @@ jobs: flutter-version: ${{ env.FLUTTER_VERSION }} cache: true - #- name: Replace engine with rustdesk custom flutter engine - # run: | - # flutter doctor -v - # flutter precache --windows - # Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.0.5-rustdesk.2/windows-x64-flutter-release.zip -OutFile windows-x64-flutter-release.zip - # Expand-Archive windows-x64-flutter-release.zip -DestinationPath engine - # mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-3.0.5-x64/bin/cache/artifacts/engine/windows-x64-release/ + - name: Replace engine with rustdesk custom flutter engine + run: | + flutter doctor -v + flutter precache --windows + Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.0.5-rustdesk.2/windows-x64-flutter-release.zip -OutFile windows-x64-flutter-release.zip + Expand-Archive windows-x64-flutter-release.zip -DestinationPath engine + mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-3.0.5-x64/bin/cache/artifacts/engine/windows-x64-release/ - name: Install Rust toolchain uses: actions-rs/toolchain@v1 From a8dd49d85f1fc0c31856b2c4d705feab27c373c8 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Sun, 22 Jan 2023 22:04:39 -0700 Subject: [PATCH 1597/2015] revert 3.0.5 --- .github/workflows/flutter-ci.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/flutter-ci.yml b/.github/workflows/flutter-ci.yml index efcd02170..34730984f 100644 --- a/.github/workflows/flutter-ci.yml +++ b/.github/workflows/flutter-ci.yml @@ -14,7 +14,7 @@ on: env: LLVM_VERSION: "15.0.6" # Note: currently 3.0.5 does not support arm64 officially, we use latest stable version first. - FLUTTER_VERSION: "3.3.10" + FLUTTER_VERSION: "3.0.5" # vcpkg version: 2022.05.10 # for multiarch gcc compatibility VCPKG_COMMIT_ID: "14e7bb4ae24616ec54ff6b2f6ef4e8659434ea44" @@ -47,13 +47,13 @@ jobs: flutter-version: ${{ env.FLUTTER_VERSION }} cache: true - #- name: Replace engine with rustdesk custom flutter engine - # run: | - # flutter doctor -v - # flutter precache --windows - # Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.0.5-rustdesk.2/windows-x64-flutter-release.zip -OutFile windows-x64-flutter-release.zip - # Expand-Archive windows-x64-flutter-release.zip -DestinationPath engine - # mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-3.0.5-x64/bin/cache/artifacts/engine/windows-x64-release/ + - name: Replace engine with rustdesk custom flutter engine + run: | + flutter doctor -v + flutter precache --windows + Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.0.5-rustdesk.2/windows-x64-flutter-release.zip -OutFile windows-x64-flutter-release.zip + Expand-Archive windows-x64-flutter-release.zip -DestinationPath engine + mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-3.0.5-x64/bin/cache/artifacts/engine/windows-x64-release/ - name: Install Rust toolchain uses: actions-rs/toolchain@v1 From bb6501c3f5b5697827136499a7ca819502edf807 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 23 Jan 2023 18:25:52 +0800 Subject: [PATCH 1598/2015] fix: rename cm individual process window https://github.com/rustdesk/rustdesk/issues/2904 --- flutter/windows/runner/main.cpp | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/flutter/windows/runner/main.cpp b/flutter/windows/runner/main.cpp index b7fa64dc0..f1ea6e579 100644 --- a/flutter/windows/runner/main.cpp +++ b/flutter/windows/runner/main.cpp @@ -96,10 +96,10 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, flutter::DartProject project(L"data"); // connection manager hide icon from taskbar - bool showOnTaskBar = true; + bool is_cm_page = false; auto cmParam = std::string("--cm"); if (!command_line_arguments.empty() && command_line_arguments.front().compare(0, cmParam.size(), cmParam.c_str()) == 0) { - showOnTaskBar = false; + is_cm_page = true; } command_line_arguments.insert(command_line_arguments.end(), rust_args.begin(), rust_args.end()); project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); @@ -107,9 +107,10 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, FlutterWindow window(project); Win32Window::Point origin(10, 10); Win32Window::Size size(800, 600); - if (!window.CreateAndShow(L"RustDesk", origin, size, showOnTaskBar)) - { - return EXIT_FAILURE; + if (!window.CreateAndShow( + is_cm_page ? L"RustDesk - Connection Manager" : L"RustDesk", origin, + size, !is_cm_page)) { + return EXIT_FAILURE; } window.SetQuitOnClose(true); From 9acec720a3966f430b6ef0ff65c194a49c3609c5 Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Mon, 23 Jan 2023 14:57:04 +0100 Subject: [PATCH 1599/2015] updated mac icon - #2722 --- flutter/pubspec.yaml | 1 + res/mac-icon.png | Bin 0 -> 90116 bytes 2 files changed, 1 insertion(+) create mode 100644 res/mac-icon.png diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index a63f49ba8..a5535c8b7 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -113,6 +113,7 @@ icons_launcher: enable: true macos: enable: true + image_path: "../res/mac-icon.png" linux: enable: true diff --git a/res/mac-icon.png b/res/mac-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..a9813152ef248d07076f956a3642575c396c0811 GIT binary patch literal 90116 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 z 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 av)BBzn526< Date: Mon, 23 Jan 2023 22:07:50 +0800 Subject: [PATCH 1600/2015] opt: update remote alias/id on taskbar in remote window https://github.com/rustdesk/rustdesk/discussions/2815#discussioncomment-4752398 --- flutter/lib/common.dart | 34 +++++++++++--- .../desktop/pages/file_manager_tab_page.dart | 4 ++ .../desktop/pages/port_forward_tab_page.dart | 4 ++ .../lib/desktop/pages/remote_tab_page.dart | 9 +++- flutter/lib/desktop/pages/server_page.dart | 7 ++- .../lib/desktop/widgets/refresh_wrapper.dart | 2 +- .../lib/desktop/widgets/tabbar_widget.dart | 14 +++--- flutter/lib/main.dart | 47 +++++++++---------- flutter/lib/utils/multi_window_manager.dart | 9 ++-- 9 files changed, 85 insertions(+), 45 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 1aead0fd4..f4e0c2d75 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1263,23 +1263,23 @@ StreamSubscription? listenUniLinks() { bool checkArguments() { // bootArgs:[--connect, 362587269, --switch_uuid, e3d531cc-5dce-41e0-bd06-5d4a2b1eec05] // check connect args - final connectIndex = bootArgs.indexOf("--connect"); + final connectIndex = kBootArgs.indexOf("--connect"); if (connectIndex == -1) { return false; } String? id = - bootArgs.length < connectIndex + 1 ? null : bootArgs[connectIndex + 1]; - final switchUuidIndex = bootArgs.indexOf("--switch_uuid"); - String? switchUuid = bootArgs.length < switchUuidIndex + 1 + kBootArgs.length < connectIndex + 1 ? null : kBootArgs[connectIndex + 1]; + final switchUuidIndex = kBootArgs.indexOf("--switch_uuid"); + String? switchUuid = kBootArgs.length < switchUuidIndex + 1 ? null - : bootArgs[switchUuidIndex + 1]; + : kBootArgs[switchUuidIndex + 1]; if (id != null) { if (id.startsWith(kUniLinksPrefix)) { return parseRustdeskUri(id); } else { // remove "--connect xxx" in the `bootArgs` array - bootArgs.removeAt(connectIndex); - bootArgs.removeAt(connectIndex); + kBootArgs.removeAt(connectIndex); + kBootArgs.removeAt(connectIndex); // fallback to peer id Future.delayed(Duration.zero, () { rustDeskWinManager.newRemoteDesktop(id, switch_uuid: switchUuid); @@ -1617,3 +1617,23 @@ Widget dialogButton(String text, int version_cmp(String v1, String v2) { return bind.versionToNumber(v: v1) - bind.versionToNumber(v: v2); } + +String getWindowName({WindowType? overrideType}) { + switch (overrideType ?? kWindowType) { + case WindowType.Main: + return "RustDesk"; + case WindowType.FileTransfer: + return "File Transfer - RustDesk"; + case WindowType.PortForward: + return "Port Forward - RustDesk"; + case WindowType.RemoteDesktop: + return "Remote Desktop - RustDesk"; + default: + break; + } + return "RustDesk"; +} + +String getWindowNameWithId(String id, {WindowType? overrideType}) { + return "${DesktopTab.labelGetterAlias(id).value} - ${getWindowName(overrideType: overrideType)}"; +} diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index 7e07eaa9a..b2566e267 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -31,6 +31,10 @@ class _FileManagerTabPageState extends State { _FileManagerTabPageState(Map params) { Get.put(DesktopTabController(tabType: DesktopTabType.fileTransfer)); + tabController.onSelected = (_, id) { + WindowController.fromWindowId(windowId()) + .setTitle(getWindowNameWithId(id)); + }; tabController.add(TabInfo( key: params['id'], label: params['id'], diff --git a/flutter/lib/desktop/pages/port_forward_tab_page.dart b/flutter/lib/desktop/pages/port_forward_tab_page.dart index d4c0a86f8..ca354f297 100644 --- a/flutter/lib/desktop/pages/port_forward_tab_page.dart +++ b/flutter/lib/desktop/pages/port_forward_tab_page.dart @@ -31,6 +31,10 @@ class _PortForwardTabPageState extends State { isRDP = params['isRDP']; tabController = Get.put(DesktopTabController(tabType: DesktopTabType.portForward)); + tabController.onSelected = (_, id) { + WindowController.fromWindowId(windowId()) + .setTitle(getWindowNameWithId(id)); + }; tabController.add(TabInfo( key: params['id'], label: params['id'], diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index a3532d49a..83928c3fe 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -39,8 +39,7 @@ class ConnectionTabPage extends StatefulWidget { class _ConnectionTabPageState extends State { final tabController = Get.put(DesktopTabController( - tabType: DesktopTabType.remoteScreen, - onSelected: (_, id) => bind.setCurSessionId(id: id))); + tabType: DesktopTabType.remoteScreen)); static const IconData selectedIcon = Icons.desktop_windows_sharp; static const IconData unselectedIcon = Icons.desktop_windows_outlined; @@ -54,6 +53,11 @@ class _ConnectionTabPageState extends State { final peerId = params['id']; if (peerId != null) { ConnectionTypeState.init(peerId); + tabController.onSelected = (_, id) { + bind.setCurSessionId(id: id); + WindowController.fromWindowId(windowId()) + .setTitle(getWindowNameWithId(id)); + }; tabController.add(TabInfo( key: peerId, label: peerId, @@ -76,6 +80,7 @@ class _ConnectionTabPageState extends State { super.initState(); tabController.onRemoved = (_, id) => onRemoveId(id); + rustDeskWinManager.setMethodHandler((call, fromWindowId) async { print( diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index 8c8679e96..521413647 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -30,7 +30,12 @@ class _DesktopServerPageState extends State void initState() { gFFI.ffiModel.updateEventListener(""); windowManager.addListener(this); - tabController.onRemoved = (_, id) => onRemoveId(id); + tabController.onRemoved = (_, id) { + onRemoveId(id); + }; + tabController.onSelected = (_, id) { + windowManager.setTitle(getWindowNameWithId(id)); + }; super.initState(); } diff --git a/flutter/lib/desktop/widgets/refresh_wrapper.dart b/flutter/lib/desktop/widgets/refresh_wrapper.dart index 4f2795d71..60e816044 100644 --- a/flutter/lib/desktop/widgets/refresh_wrapper.dart +++ b/flutter/lib/desktop/widgets/refresh_wrapper.dart @@ -26,7 +26,7 @@ class RefreshWrapperState extends State { } rebuild() { - debugPrint("=====Global State Rebuild (win-${windowId ?? 'main'})====="); + debugPrint("=====Global State Rebuild (win-${kWindowId ?? 'main'})====="); if (Get.context != null) { (context as Element).visitChildren(_rebuildElement); } diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index d4fcd16e9..ddc0e7729 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -486,7 +486,7 @@ class WindowActionPanelState extends State } }); } else { - final wc = WindowController.fromWindowId(windowId!); + final wc = WindowController.fromWindowId(kWindowId!); wc.isMaximized().then((maximized) { debugPrint("isMaximized $maximized"); if (widget.isMaximized.value != maximized) { @@ -534,10 +534,10 @@ class WindowActionPanelState extends State await windowManager.hide(); } else { // it's safe to hide the subwindow - await WindowController.fromWindowId(windowId!).hide(); + await WindowController.fromWindowId(kWindowId!).hide(); await Future.wait([ rustDeskWinManager - .call(WindowType.Main, kWindowEventHide, {"id": windowId!}), + .call(WindowType.Main, kWindowEventHide, {"id": kWindowId!}), widget.onClose?.call() ?? Future.microtask(() => null) ]); } @@ -563,7 +563,7 @@ class WindowActionPanelState extends State if (widget.isMainWindow) { windowManager.minimize(); } else { - WindowController.fromWindowId(windowId!).minimize(); + WindowController.fromWindowId(kWindowId!).minimize(); } }, isClose: false, @@ -593,7 +593,7 @@ class WindowActionPanelState extends State if (widget.isMainWindow) { await windowManager.close(); } else { - await WindowController.fromWindowId(windowId!) + await WindowController.fromWindowId(kWindowId!) .close(); } }); @@ -622,7 +622,7 @@ void startDragging(bool isMainWindow) { if (isMainWindow) { windowManager.startDragging(); } else { - WindowController.fromWindowId(windowId!).startDragging(); + WindowController.fromWindowId(kWindowId!).startDragging(); } } @@ -638,7 +638,7 @@ Future toggleMaximize(bool isMainWindow) async { return true; } } else { - final wc = WindowController.fromWindowId(windowId!); + final wc = WindowController.fromWindowId(kWindowId!); if (await wc.isMaximized()) { wc.unmaximize(); return false; diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 6593f1804..1ec963f22 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -26,13 +26,15 @@ import 'mobile/pages/home_page.dart'; import 'mobile/pages/server_page.dart'; import 'models/platform_model.dart'; -int? windowId; -late List bootArgs; +/// Basic window and launch properties. +int? kWindowId; +WindowType? kWindowType; +late List kBootArgs; Future main(List args) async { WidgetsFlutterBinding.ensureInitialized(); debugPrint("launch args: $args"); - bootArgs = List.from(args); + kBootArgs = List.from(args); if (!isDesktop) { runMobileApp(); @@ -40,10 +42,10 @@ Future main(List args) async { } // main window if (args.isNotEmpty && args.first == 'multi_window') { - windowId = int.parse(args[1]); - stateGlobal.setWindowId(windowId!); + kWindowId = int.parse(args[1]); + stateGlobal.setWindowId(kWindowId!); if (!Platform.isMacOS) { - WindowController.fromWindowId(windowId!).showTitleBar(false); + WindowController.fromWindowId(kWindowId!).showTitleBar(false); } final argument = args[2].isEmpty ? {} @@ -51,35 +53,32 @@ Future main(List args) async { int type = argument['type'] ?? -1; // to-do: No need to parse window id ? // Because stateGlobal.windowId is a global value. - argument['windowId'] = windowId; - WindowType wType = type.windowType; - switch (wType) { + argument['windowId'] = kWindowId; + kWindowType = type.windowType; + final windowName = getWindowName(); + switch (kWindowType) { case WindowType.RemoteDesktop: desktopType = DesktopType.remote; runMultiWindow( argument, kAppTypeDesktopRemote, - 'RustDesk - Remote Desktop', + windowName, ); - WindowController.fromWindowId(windowId!) - .setTitle('RustDesk - Remote Desktop'); break; case WindowType.FileTransfer: desktopType = DesktopType.fileTransfer; runMultiWindow( argument, kAppTypeDesktopFileTransfer, - 'RustDesk - File Transfer', + windowName, ); - WindowController.fromWindowId(windowId!) - .setTitle('RustDesk - File Transfer'); break; case WindowType.PortForward: desktopType = DesktopType.portForward; runMultiWindow( argument, kAppTypeDesktopPortForward, - 'RustDesk - Port Forward', + windowName, ); break; default: @@ -139,7 +138,7 @@ void runMainApp(bool startService) async { windowManager.waitUntilReadyToShow(windowOptions, () async { windowManager.setOpacity(1); }); - windowManager.setTitle("RustDesk"); + windowManager.setTitle(getWindowName()); } void runMobileApp() async { @@ -155,7 +154,7 @@ void runMultiWindow( ) async { await initEnv(appType); // set prevent close to true, we handle close event manually - WindowController.fromWindowId(windowId!).setPreventClose(true); + WindowController.fromWindowId(kWindowId!).setPreventClose(true); late Widget widget; switch (appType) { case kAppTypeDesktopRemote: @@ -184,26 +183,26 @@ void runMultiWindow( ); // we do not hide titlebar on win7 because of the frame overflow. if (kUseCompatibleUiMode) { - WindowController.fromWindowId(windowId!).showTitleBar(true); + WindowController.fromWindowId(kWindowId!).showTitleBar(true); } switch (appType) { case kAppTypeDesktopRemote: await restoreWindowPosition(WindowType.RemoteDesktop, - windowId: windowId!); + windowId: kWindowId!); break; case kAppTypeDesktopFileTransfer: - await restoreWindowPosition(WindowType.FileTransfer, windowId: windowId!); + await restoreWindowPosition(WindowType.FileTransfer, + windowId: kWindowId!); break; case kAppTypeDesktopPortForward: - await restoreWindowPosition(WindowType.PortForward, windowId: windowId!); + await restoreWindowPosition(WindowType.PortForward, windowId: kWindowId!); break; default: // no such appType exit(0); } // show window from hidden status - WindowController.fromWindowId(windowId!).show(); - WindowController.fromWindowId(windowId!).setTitle(title); + WindowController.fromWindowId(kWindowId!).show(); } void runConnectionManagerScreen(bool hide) async { diff --git a/flutter/lib/utils/multi_window_manager.dart b/flutter/lib/utils/multi_window_manager.dart index 5087538c5..ee19ac485 100644 --- a/flutter/lib/utils/multi_window_manager.dart +++ b/flutter/lib/utils/multi_window_manager.dart @@ -62,7 +62,8 @@ class RustDeskMultiWindowManager { remoteDesktopController ..setFrame(const Offset(0, 0) & const Size(1280, 720)) ..center() - ..setTitle("rustdesk - remote desktop") + ..setTitle(getWindowNameWithId(remoteId, + overrideType: WindowType.RemoteDesktop)) ..show(); registerActiveWindow(remoteDesktopController.windowId); _remoteDesktopWindowId = remoteDesktopController.windowId; @@ -88,7 +89,8 @@ class RustDeskMultiWindowManager { fileTransferController ..setFrame(const Offset(0, 0) & const Size(1280, 720)) ..center() - ..setTitle("rustdesk - file transfer") + ..setTitle(getWindowNameWithId(remoteId, + overrideType: WindowType.FileTransfer)) ..show(); registerActiveWindow(fileTransferController.windowId); _fileTransferWindowId = fileTransferController.windowId; @@ -114,7 +116,8 @@ class RustDeskMultiWindowManager { portForwardController ..setFrame(const Offset(0, 0) & const Size(1280, 720)) ..center() - ..setTitle("rustdesk - port forward") + ..setTitle( + getWindowNameWithId(remoteId, overrideType: WindowType.PortForward)) ..show(); registerActiveWindow(portForwardController.windowId); _portForwardWindowId = portForwardController.windowId; From d4851ebb4009e89f3de5fe81edd27409f158d26d Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 24 Jan 2023 01:24:53 +0800 Subject: [PATCH 1601/2015] revert 8fb3c452bea8dfb9c1de14db8b06f158d31147dd --- flutter/pubspec.lock | 66 +++++++++++++++++++++++++++++++---- libs/hbb_common/src/config.rs | 10 ++---- 2 files changed, 62 insertions(+), 14 deletions(-) diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index ef57f375c..15a1a23ac 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -78,6 +78,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.3.1" + build_cli_annotations: + dependency: transitive + description: + name: build_cli_annotations + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" build_config: dependency: transitive description: @@ -169,6 +176,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.1" + cli_util: + dependency: transitive + description: + name: cli_util + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.5" clock: dependency: transitive description: @@ -190,6 +204,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.16.0" + colorize: + dependency: transitive + description: + name: colorize + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0" contextmenu: dependency: "direct main" description: @@ -339,6 +360,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.1" + ffigen: + dependency: "direct dev" + description: + name: ffigen + url: "https://pub.dartlang.org" + source: hosted + version: "7.2.4" file: dependency: transitive description: @@ -429,12 +457,10 @@ packages: flutter_rust_bridge: dependency: "direct main" description: - path: frb_dart - ref: master - resolved-ref: e5adce55eea0b74d3680e66a2c5252edf17b07e1 - url: "https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge" - source: git - version: "1.32.0" + name: flutter_rust_bridge + url: "https://pub.dartlang.org" + source: hosted + version: "1.61.1" flutter_svg: dependency: "direct main" description: @@ -846,6 +872,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.2.1" + puppeteer: + dependency: transitive + description: + name: puppeteer + url: "https://pub.dartlang.org" + source: hosted + version: "2.12.0" qr_code_scanner: dependency: "direct main" description: @@ -853,6 +886,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.1" + quiver: + dependency: transitive + description: + name: quiver + url: "https://pub.dartlang.org" + source: hosted + version: "3.2.1" rxdart: dependency: transitive description: @@ -890,6 +930,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.4.0" + shelf_static: + dependency: transitive + description: + name: shelf_static + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.1" shelf_web_socket: dependency: transitive description: @@ -1256,6 +1303,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.1.1" + yaml_edit: + dependency: transitive + description: + name: yaml_edit + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.3" zxing2: dependency: "direct main" description: diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index abec5b231..1e3bde9eb 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -49,10 +49,7 @@ lazy_static::lazy_static! { static ref CONFIG2: Arc> = Arc::new(RwLock::new(Config2::load())); static ref LOCAL_CONFIG: Arc> = Arc::new(RwLock::new(LocalConfig::load())); pub static ref ONLINE: Arc>> = Default::default(); - pub static ref PROD_RENDEZVOUS_SERVER: Arc> = Arc::new(RwLock::new(match option_env!("RENDEZVOUS_SERVER") { - Some(key) => key, - _ => "", - }.to_owned())); + pub static ref PROD_RENDEZVOUS_SERVER: Arc> = Default::default(); pub static ref APP_NAME: Arc> = Arc::new(RwLock::new("RustDesk".to_owned())); static ref KEY_PAIR: Arc, Vec)>>> = Default::default(); static ref HW_CODEC_CONFIG: Arc> = Arc::new(RwLock::new(HwCodecConfig::load())); @@ -86,10 +83,7 @@ const RENDEZVOUS_SERVERS: &'static [&'static str] = &[ "rs-cn.rustdesk.com", ]; -pub const RS_PUB_KEY: &'static str = match option_env!("RS_PUB_KEY") { - Some(key) => key, - None => "OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw=", -}; +pub const RS_PUB_KEY: &'static str = "OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw="; pub const RENDEZVOUS_PORT: i32 = 21116; pub const RELAY_PORT: i32 = 21117; From efa7b52f49dcd0c39d6b7971785ede8558a0495c Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 24 Jan 2023 01:32:56 +0800 Subject: [PATCH 1602/2015] fix nightly build RS_PUB_KEY issue --- libs/hbb_common/src/config.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 1e3bde9eb..20334ed12 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -49,7 +49,10 @@ lazy_static::lazy_static! { static ref CONFIG2: Arc> = Arc::new(RwLock::new(Config2::load())); static ref LOCAL_CONFIG: Arc> = Arc::new(RwLock::new(LocalConfig::load())); pub static ref ONLINE: Arc>> = Default::default(); - pub static ref PROD_RENDEZVOUS_SERVER: Arc> = Default::default(); + pub static ref PROD_RENDEZVOUS_SERVER: Arc> = Arc::new(RwLock::new(match option_env!("RENDEZVOUS_SERVER") { + Some(key) if !key.is_empty() => key, + _ => "", + }.to_owned())); pub static ref APP_NAME: Arc> = Arc::new(RwLock::new("RustDesk".to_owned())); static ref KEY_PAIR: Arc, Vec)>>> = Default::default(); static ref HW_CODEC_CONFIG: Arc> = Arc::new(RwLock::new(HwCodecConfig::load())); @@ -83,7 +86,10 @@ const RENDEZVOUS_SERVERS: &'static [&'static str] = &[ "rs-cn.rustdesk.com", ]; -pub const RS_PUB_KEY: &'static str = "OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw="; +pub const RS_PUB_KEY: &'static str = match option_env!("RS_PUB_KEY") { + Some(key) if !key.is_empty() => key, + _ => "OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw=", +}; pub const RENDEZVOUS_PORT: i32 = 21116; pub const RELAY_PORT: i32 = 21117; From e66ecae5f4a2b9b97c6aff228def9652488145c6 Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Mon, 23 Jan 2023 18:57:16 +0100 Subject: [PATCH 1603/2015] generated new mac icons --- .../AppIcon.appiconset/app_icon_1024.png | Bin 13838 -> 100419 bytes .../AppIcon.appiconset/app_icon_128.png | Bin 1517 -> 5792 bytes .../AppIcon.appiconset/app_icon_16.png | Bin 349 -> 569 bytes .../AppIcon.appiconset/app_icon_256.png | Bin 3103 -> 14429 bytes .../AppIcon.appiconset/app_icon_32.png | Bin 562 -> 1256 bytes .../AppIcon.appiconset/app_icon_512.png | Bin 6186 -> 37270 bytes .../AppIcon.appiconset/app_icon_64.png | Bin 901 -> 2618 bytes 7 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 f51492c53e43124bfb81705e26f7e59004cf9c88..4b6ea50696ab62a80cf4eaa9ce6d344f8c478917 100644 GIT binary patch literal 100419 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%0DUB){U4Ess|GV&(7(tHoy(1AJxTUO5(rOw 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 BYk&X% diff --git a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png index 97555e4a23b225b6366cfc624c50b4ba2da6117b..c561ee3b4ca9f9e4eb4dd731c2091a723e886fdf 100644 GIT binary patch literal 5792 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 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 c695fd3a37f48b7ec7dc4f7a5111800044b4e3b3..3388501fc5df22fd183e482b7bfe514356a2fe15 100644 GIT binary patch literal 14429 zcmcJW^;29y)9;tX-6aqtIKc_-5Hvs_fgr)%Ef8E5cZc9kg1h^|qQN1!Ev^fTJ6xXU z-nxIlTlM}jXMX6@HC5f`%zVDx6QTB59vg!k0{{SED=Nr*1pp9Urw9Nvl-Gyej}i+2 zK#*QhMoPm2;W!)JM`L#J#eZ)+BRC!%3C%W?&kloRQBO+BnYG2Faqj&IOt-PItFcM5 z{Z?~*&Scf*p)K#HY-Pz$L}EQ1A_9IwJVkA3g1S-q^kKW3oEM>1cy*%!PrS=X=ZC%J z2_FE(`CA?rzZ=hSPZ9wl4B5HOCNBhxkY3?r05g$JGyx!lHtGv7PtA<`6M!g^O%5F- zozTyWpvGKB#sI*OB`HLb%pWrO-!TSjNlsdt7+$ycknx)0Jv5SaNm5_0V=SH`9;Zwg zc*0SCeBqr!1xh^Br*<|}KepvozqEcpGn8U7+%o3WdL30E7Vc7utZT#b`^PEA+qQ`h zT^y7qsJ9o-SM>N*k@>&NvY)zQI%$uIP5d1!symNwelF;026g68FvsAxmA%3ZXMnEZ5lHSuo=4DC3En zcN@lZ?!=goT2b@p0y>Tl#7)+SuFWI}A$wycs%MH^xg{9LgCk~kSSS-@5I_Hut{1s< z_ZoDOR8C0KPPOUY2@`yJKuBv#Us=*g&poCsA)F z_&heamy0lnwD#_dsTQ_#qcdcr-3dOSH$}?JGmH=sy}ZA`jO{#@H>TqnJG^4%2?g%H z(_1@>oZ-Z<`L#mocXKU2^>xkSs<#bs5K~g|AmU^)je-LcH|^oW?NH6=v}H&oJOO_r z|Kg-S(6hU5cgLT(ROeRmDwH9X1UQHRS~m<_hz{A)z|D3ceSn~q&le88kK}S2I4vI> zZ?WzjEn6O;&N;)|oQ?Il6jTFXaBYl+Kt_~yMFEayrJB`p3A02g?SVI>j~)-i^0S<+ z7I$<46~MAe1NHBMp8y#5Ig-HQP9J^G>fZK6YL`lbIO!hr!J)n#u{}S%`x(L)(2Zkj z<&RlN_Ky4liaeU6loUc_oZ&)w-F_AmBt%TL*-H$g-(R}nug1n0GLx7q4=t%dc+5qY z(X`Q3fp9+pkPAU@?|0#Q#3w`GkHFUjTsLh`}jsuDrTpVMs=YrFIvn)!KNUE?aO9zRj@^ zH5IL?!wr?niknlSACD?$b*t-A&=x)c?vo9|^piMpC?a>oDsmnpm1#A<{!p?3M-MD+ zopxCsNchRaeta~dKwwT6ruN*b7V&av*ndNO!OD}cnv>w(yq#4CD@hOfRpEU~tLp)5 z!UIjCyEyk{tzY`S(>Mn%KMHA9%je?-&n?{y$&UFh$EIfqTh3(Q8q|4-&0zvm&-0V~ z_YJ1iZUsBm2wu}e4J{--N90m*0xSIyV*1I=&bGXYquEvH7bX}zbwys29`%vtfL(a( zu#uUFhfD97ZY?glo^*x2CpePNCy+H!BcdGuL5Pj!PW$bRz09-aVztnbgYu zV(aBWAQOb4)YURL8I-w0>C5Nx)U+c-id{dBKq-nxbN%-oq70hoiE3m3v4YRS|8CFE zzXJ(Vj;e=jXz#Wh-Y>VE)cOTYEa*31SRY)o;{#4>YQG1G@BcVF$F1+}k~XFY*PJvE zefm}K>{~Us?tTzg3LM!^mfhbHg5rKJBr^gE z2Mpyk8&Ai$Bjra^%+>Rqh{RBG{#8);#yrlr1F4ltNnVXgr+wJ(&Bu9LM4$fS=`Hd* zaXSC)gOJujD(h9f#OVG$YLrj-phR@0g3@s~*&TyTe3-N|-enHtbLrZ}-=v;tq2a&h zVIj*&w1AJHS@LspeD~1_A1#Jziq!;(mrN&}>7||`vLusVzBGuj0O97-lO=hqnz1JCE_EZ4^XrCaMFa4 z_7711+Jd~-QQbn@_8zE(0JqO(G}>sq&4?*i;EZhWA*Ce!lo&tJOW-bcdQ)xNeSKhJ zqDIU2V<8Y6`+)OJOp`)T=58_-!;)rG{3)VEDzG>j_?a9%`uO7X<%R8fy;_9ic9;N~ z4{1^PVGD*817Caz9-a`|co}~}KQJPl+9CgPKoDQTDS9hJH*Y}J+D}M0W=`dA^O+5+ zb`Q{tY;+4MHB&0;u_emk(rltgL+}u`&dk_Cpc2JpHt|0dP?~Ap zGhg=~1{#g;J3fFWrZmq5+oL|iKanDkOEVD-;?D=WR<5xMH)PMFUP zRZ}G90OUv-2FmnBWfde8*qf945dN5du44h0{zmn~a37#_Sy1;%ZRMWX_>+#h{)kE& zkEnOaelbgcXrPBH+C_dF?}-x1?gbS_@~_^|Q1u*f?+>?9e_g$8lGcaEN^y^iZ;%NN zo;)TvWD7J>417ArJ@v-F#&5g0yhMgh7vfHENJK$<`wb0w0k}RxeMT_{49vn(F33>U z!{wZF{@W+ywxI+QY2>`TJS5NlCw&;Kh&p`kV^O6th`Q-%Tjm1Yi&^y{$akGeJg+iJ z{mA?N`9g2QiR=r~qcs(#>}NzOQ>vbqu6bY7@75Oz2to}cV<8=b{hs#61%rW5_ffHKGlGkIBwXpOd>Jid zs1qc19J)4kQ=FDFUN}nE^2DB3^{ql>aO0YkPDj7o^ed3~f2C0bm8l9MpG~frq4^h}4cQ;C-G`am@3}2M-=a~GuXv)=7`?~D zHQ;i=sG~d_KE+mXC%saMZ&Sg!0=NwNm=zvO4m&kxJi( z+yX3uF~GN5#qrp>nl{HXxj*T;A1(noFFALY&4?Tnc89JT1)f_GU~VbsqpZ#^5$O3I zK?^Ug&-fJp<%W02OL}2hVOlWohd3cBP>G{?Zs$cfdlqLQy_5mGM2)y7isJ+SJzJg- zyn9a0yR+2Pm;-maFBk|#CkDa1H-iaCfK~=ku_@b-B+r~SvY{lO#I-jEM7#8M`wpbs zaW)5hV7{oaoQ>6R{1Z{?aXf=eAxVbua13Mes1V7lIo__h_f=WrS!TP|`gcBmA_30$**3^iB+)p4f_or3 zA;18A2$x4umz5M0ZI8SgJT_5ke{er9{M#%wRmp`%9Vt8tMlpSI9$&}ftQqVu+Oe6F zhkZq>qK)DHkX>^pv^-tXW*Z)^EAI}G1vx&K=cG;W7i5hI}yfg-}? zmZgl~-pZ(J($sGarfv4pUdmWTUBKmQDi%?6Ik#e4L#H#PRA|(9dpVvLz zvKKobig4Bidu)r;O=U@!R;V-g4g0nv0yK(0^bG4yr)b(R8ZJlz->^&10NdXkyobp4 zLi^P1zMKhm{+JFGDoG`DU{#AjsvRFa}2l>Qiyheocbur-2#Y9 ziR!@I)M=A|*~{LwIe@M!rz(a$ucEf-)~zVO59+?@RIW88w1Vt+t`0RO!JMbE$gJdN zq@903yoGr0N>Q8=>1~4MI66qSCM*%P*qgiV;tCcKZE~_31L8YG0n=z|G7j~FP1`SL>(HN?k&35|2M{;#v>e_ zqy-XLNAMF`R!*!N$=~Rp8+FTM{m+^9v4DBA|Mnric~p!P|B=NIE~R#$@2hvKee z>TlmhLKg8#W)LRBG5Ty*Uo_QwZJz0XEFYJ7)>Zej z8%KF(n2!5*^ez1?!jzCyB4Z(sLSD@6i$*1M$KH>7cTyKYoyG|9LKGOQal>v;(|yWj z8XIo=Z0;-se@+DxlBVnXRc?33B2wK8c4nZJ8-JnZ6Aiw7>hHBfTh8Cvk;0KFlt-+Pm$L*%*OiO;Sl?of-lhckGZQgo=A2=&i!H@pY$2v z8l58fGKJ_`!2Ec`a)gih5eogi9q-Z#4;%bVjU!H9*gmM~F_K#SP5qnU640$-85A8J=jwhjutVKk{g~`PNxj$J;U>2)bGfX zN{(>Y86ezV*3&`OyH;l8Z#X7@%y81H_54Uf=tt+&uK{%**8_3LCCoNBM)gvxg2p-xWz+e5fCe z3iuj|`EB;wYpe-DWopEahz0b!AYQ;TZ@lN}grgj%_nh+euv^Owm18SusfYHi;u~AgjwR zu$g)*A5^hRE4mFe22Zp9IR1^vCrLX@c81xZOqhzXf@=kLvTi)RzOe#(%Kxn&6uIeW zFvt-rt~IaLsiCWu&?H?)1kAGMN7oGksVEK zIA(HTy0fs=j8J%=wiX7~k2~vf zTH*G>maNg^aZfCnQ1CsXl<~S@9^iHgvXc+1(UE}p4vmU>!|6PmctQr}h6n+E#CGz+ zUV+7~Uyu7X@&Y7@`3GUk(-ymELTnx+HX zXLzurS=}P*-_^WRz!O73!V7(p0a?Bbe|=)e1o7`R$kbq0I$wC5R5<@QR-rQatXW4~ zRM;pkH5xDv%2xxli2y~ek=S6iI0?xCN%HDCV@J6ogdn-t@Pg&ln`CdocC+(GL{|b*F46mceK+CohS9tmCES zud6w67>eV@7-q=5Vj3&BQ~E(FznIy^Cb5_8HQT?F0B_}aQ1iz8-htQ}4IM5N#XlRF zH!hB8@zOn{d7`>`8P&qm2e1)4QT7*D_G#480DUIr;RA#4K$KYD%>BrZIqWi59#{Y|Pt8c*?uQ2wF5!LOH z_C^P|D?TC%p~VB#rMVe{(IvAb!l|e}l^Am(L#uMImXa)B-J9_E^a7DdIa8>Rs7GwD zsg(_>7B#yvz^Cz@jGWA!6QM2(AoqbOh;qzLCD4szR^{C8>|KFdVDmH-Q9A5uDF!>x z4ow*u=Uvz~&1bqz<+s3(7&{AJR9{Z=8E}G^cd+memOpmHxaJYj@BAn%`}-OdLZ;XO z#x?61i&Vf9LrmFta8Hg~Kq}e@9>^du5z>ayoibz&RGZlb_Z_=dE=g5s#Zu1RR6kfH zUDh{b0`MStLgJ|(K7v|{L*Wi4XlRY8Xt(rNFREsD;J6d|_lf|s@zDL(LtFit+#jN- z5APhf6jG>{xJcR?7LSO)oChkgM?a3u5S??57L!u!d})Uf*0`^0j%%K zpy`4`Zk_;iMIcT2h5|p$(czp!QpVlfSw89?DhV7`1L6zZBC@XvoHBihycw9JGKan}_IPYayHg~MUjh|Ns z$m5_=Qbrs5ehT8S?NB1Mj!)Li*!xgEa=lP7+b{zyY&ZMj2)U0bN{}3lP?55ILg;gK z=}5@V3H%APtn|<3eo6+s<)Dk&UMy=|^_ERX&FjH+#-KSNkMAU3YWI%W*jqtR;9bTm5*0m7~q7(GIVa%%S!BK@U5#S#VC+nQY410oMZEuAos4@y9` zc=5xTw^zPbOiO|+a1R&^oj9XgvglsZ&Yd5|2aF$o0T-60EGQzNnE2Fm!rS{ z1ca`<#g%IhAx6X)MPx=14~j~RHEO3jBr$()|JQA#zF!PrFgnfQ(55c8IR}~<9I9`2 zOBL1C4+*0mmcalnBZOZ9G-=&?Wy8Pb=o{ybP26cfJFco{AeNt7e7lr$#*~eA(ef0& zrW!nZe5X76F!hbM_t);mo;AWhA$DRxk_?m}@>i;C{k15M#6Lf5%Qx0)mPi0{ZG|&? zHr*oRRSXm!fR_W4S}^m|T5e^Gbm4fK{n%+i0mt=sLMXb^Ol&0?AHK(mIasQJEli8; zupGxddt^MX#aB2cG=<^Wh3zaR2+sY0&By=~gq8~|zk7Mu0@Z0}W@($J#9V1rZp1vr z^1Hp+dUJk!cI$sY;(0wC*gW&$k)8}La!LX|tq%G8$>T5O13rZ!*u0!k=g2L+pSo_h z=PaU6OBMA*=iOkAdaFlb$6d@@KRWt)Nj7%ofC(jQXK;WE0x*E)yp)2wkgof$o1&(V zRJ8-JDvM)cmz@bQt^(N0ZzpPu>Txk)IFVp`k)rYg_8a~G;ZR-~Kgv8;rD;omzNc}g z9s+d!yM!sN(}={*qAP)f_?I>K+|%R7e+#P)ARbci(QKLk`^lv?)DFxOpBPZ%qW-E( zFHbU_cBQ+}J;X~ieLq?8X5wlfbvj~eXSiGhj%?wgXTnDSNb zKc2HZFf3ZYy=movq7Dw%53jC-y=p67lcCNDvhYRx7lHNT0{SE-@f2H5eGe+oe`w|( zIt`L4>Rjj{O`rMEqU57Bn>s-7gB(D@jQ{jOtxCWB$BZ&#I02@=APV72ca&}OIy;C8h{8jAYLGBpx_GyDgxNDee|Ev;+V*ZFsAkyL(SR%M+sIe1;*nw z@15IvMf~#HT{WYJ(EG1`8sFGSEk0U(j5jMH4s}r59ig|&z?tm%NR1Gu$2`x-iRZQP z)#$IZXLckEfXE?#YXjrJf+YA7T$kvMlpzcXDfMZ4532lG7D5r-;{35E0=U`1hCmT zYyBCu!ORQ;JZw~LG!FjFl)d@cC9s&q;?YBx?#?wt;RC;jT_eF!sdV@;KI2c_2j7{N zkzOHA;X)XizffE0X2OQq)PEDH3F<)&d%m}a2Dnt}LXN*0U15WT;uh;w_1jt&bp1!z zc6GA<`Z+pl-i}oZ?H^3MOWx(W5&9#-MF^kOqT~AGzGB9ODyfRr>3l5rd>^6H#_B>l zHowt~XzPIF;MLzz_2aM=98L?J!VK67)9ECD7Q(=oK<}dn(AQ72^q;l-Ho?P^0h@RP zIYjmij_i{N8wWiSKIk4d--RF_yU<@X(HFh?592>Mm+I}W1bXKEE|lxxEQ2VWc`?4H zr8B|Fc;cjLbn3--XLGtH|DhFY!k}AuU7XP#$#FD<>-tAbqbYLegL;Jlx>K)H-XkN_ zk#!7_O$7JNhR{<-&P%NNSU$7c#&g`ZTi^*t%V`#mUP)OMfyHamvvHP7uf?y?FKr+Q zDto<=@a(sH!zd!OF9~0`%P@VFxzl=GPf@SW(QaYL>$i`k5_7$i2a-OEXpbxTJM;e^ zf+~niKd!I?fZ_X8yMETOLP(GARlYzE`@xCGui$U!v1;)Ge7=V()n~x^uXevzf&_&1 z!7>MjrlZT4?LGQ0tT`^O6#V2WR`f+ea4aM8D1FlTrVBTt@#Uun=&H$WKJ*(wW~gh* zR%m)P>I7fr_pY`#9uW~e4n0W=x5Nj{g9sN)8Cu>MF%uphjlcCN{M^ucMd-b)m5oMt zjDadVOVRN^S!O$_G83uxGywjqWyF-iu%yk zJ#n$}W+AUr;K}c=&ge`1e1$>(h7@+Z0*~AyP-G33XyW{6v_z7CVuU_p48agbY5V zN~FGFpZ~wm1((WH>RZ4|%T`A&Q*qiv1|z&`cRJwnnRLzSq9gdVY1}AkS>{Y!$b@*W zXJ+ojviYu#f}Et(k2FlR+LeHQNy>H6d{`z<)*E~y0cJ|-*YM>|epke<{r>9RQ zO=(x*!BLl#fdm#6pi|2KGME#O8kcr74+J`_O!Uvmhj(Bj%!c%dQu_?iC~=#_Bn(m{ zwc8dj0%j=}6&p{t^6R7ahr4r!%8!$~kgf7J}Dq zq8i!0Z6+Yjbqiaa$7|^Q@6y=NrCOx_G9EjtZtSCJoIK(z^Q})IR%jTaI%$S|6%%f) z7a!^X&deT#R=VxgMW%vD7v=np#qMU$_|S?`Cf@36LP%&KvIaV$3(7p%*ueW z@)0Rp?>cDrR2Eh;GW-sBk^3smBj4`Wc@vM_PJ0x?u3dMS9#h5bH7zZ$d5HxUPKFPt z^GXN=`ejN`m)Cjruzxs$aC>SH59rOBesUWtpgisk$)@Bs znh}?JWLf>&m-3#HbOzx4n^1eLzmR=Xh?)IIqY-BjvV2W!u#BbrNC*GdZ?W1nOJ=sVeBe);`G^*C89CCi6Hm`7^_8tslHvn3s(O|wd24ws}8fLg)I<^hlJ*0 zuJ|M_l88_WbhIlLQ@Sl|7G+d=kA1PGc4&5^oHLO@%MUG|0+&tv!mOvgq!Y?hD^~H= zrb)6QUYO}ygU3T}P60~%87ZDKL30&_VlhG6@v;aM5%8;;5s zPU78{k2L@yBHX;9s$Nh+gEObDkdMKjbil*&tWjY}nBK#|N_w+44Zu+gD9~Z|oJ9ms zYzomq%cJHKl9bS99glsjYpitS^zQI0l!+zVTLMr6y%9S)Z%i2Xo5z8Et{sc}>L~5g z@z5$wY&X=7pYxTDfR`1vi9d7-a+!CE3RvDD4wU)S-94Sl`)ghwHN-=b-D!y+^#e?h@#<;(hvS#NXJBQ{) zU%ycNtZjZRbE2Lu52E_LD$PziyflX30M72Yl)3+i9zB)k^5D`T(~s<(EfJ3p{?m@4cd?fqb9%|_wxilk`Q`L6uz$zV(0Bg=U;S9$uy$;8bE1ie*-a~J`r3bO2VL7J=NbgWj7yJ1~8 zbAxruxJ?*3W4=fN^Vh(S}lMA0MLZ@uez9ttVK`C#;UiiloiG)sVlB^C`p zS+j%m$q=C{qcOV7Q8OKvNvMK%I@-%d=gaTA&8=Qk5&#p9Kv;@0EB9cUcb8_D(&D<# zu->h=W*X%WT)w&I(|M2>I~#90;-#o0TyxoENv9D)l%cvUc%CR!uI8V*^(;w7t8iS1|0{hkipm)d0UrHNj!B>^V zuUgA{?b;JWvu_u`pDdAEh_%K*llX$q0ciD%QxEbHNiX1y`~}M`Sa05qXew0`{56@H z%Y7qJVBLy-vo@d23HX^}zW?E{O+&OxtgQKDMY1(&>s+|#tU>x6zBo2>hdtj7;GSfg zNq|kFue8dVCZC8QdRtTVFQ0<#{MIm+w6131&8^NR>^%`qQ2Wd9-1pv&onK`WNQa09Q8{K0M@#=xB1sdRa98? zfC{VCi}fBjTb*X)wW~+BB!T|wXw9fQl=)IYU$EdK{OQH72wie@DRm!T>xJj~Zcq2) zr0G?O;SCAOTbBT~2aFJV>#|3rv&qk-QZ6;8dDmH#O3~Pf%1X!Ty0IG#?q$>Zvo=N1 za>x43$=B%~7^1B>X_e(_XuS&kim<;7!Q?T?^tve5%!(D-7m2(VUn#zwE`E@)dgE#JMGxDEmawZ^?aWBe6@u@v!aB5m7X#7&nTqMd^gwEk*90ELI32e6Xm}J zZ>-+d8ce!q>-v^829TAav|X$ga~%fe23V2Aj;S%T8O=xTS@X2X7dEVI@ho}q`n`j} z9VMm(R)4X_(Ri#74$Ey_>}q_fW=IetZg7COfEETpYv?Q@6c*cL%>Jp_4y7Pf?qWqRyQRBeiTNRX!KZmoJU(O|{N%_b6W` zPGGAE?%QW7^z0D?*$V~H3w3Xo`LK#>HuzZr>D6Xv&@ynxn)4)+00@Bqsq=SIldm5f z((`0*B_a?U7X zkfcU2{V7UOHon~8Y?|SZ*^T=<-3l3c>w`a?DX=jyFuHHj?WwvIV(8ykY0)V5&+jyZ zddR!`Tp32oK`J^Q=N!wt>)%W**VhY2yq^f=qsaYzA@6h*WBG1{Ey-s9X=JtY?)mWY zLc&IOf4u+(%v%G=Je!|^5!XxJio4K`v`_~a;eQ=TEg}L!a-TZ7-cH(S{~D(>w%&P+ z@kEiJL>^hGwQgXW=-z?cz4${1nQC#60D`VooN~WHUM7RQk4;l2-f;L30Nq^FQ2Cmw)o%=`b`SHQ8ft#%e%HZ^m`m|iImKia`Q-gXlOe~X&( z=367z{Ilt(ZmZ@H)(Q#)wS^Dski6U4%$A2L1}(PXHYWxwr-f9Zaj|9vTKN1v)toB9 zbcNx{C;Wq9OC3I!Sad@n{|s(!!lOe@oZW(PCCA5xRT2Y88v&qyb7g%?sXh0HzpXt- zzN@To*ND_POLoBo$K#&F@RWQPNTr>6zMHE^$32;&BQCwv7uqOVlxmt-Oc%;Ws-pdS z2NE+77Se6o971ipiViY)c|P4t81&z;Xtrk`(;su{@lD+d5&k%Z z7%j4W>w~|N$%K#=BVW-=Iro&(&IMC3cs6clf6|_-1Tbf<7$lSHbUh58YKSxEULRGR zaz$Xs0>7czl7-x?C-;9CREZpE8TyB+<~;puuY8iwnFr=Er!YDn&wij4KfAc^Bkc6^y06$m>!$6o zx0aDhC*m7@IpNzbUQhUV^Nb!Uet}j|Ttm&3e=LVSPl*OC1~{cm3yU1tyYwd$vkQrT zp;T)-eq1+Fvu7R=pL+x2eUgHOz*rwbru%K23MccF0u2a))?kDR4xQ=}APpDwlJqVf zcl42ii<;^zbAw9d`p!0V{le^>-3m?;(}xH{Ao9`?3=H;Nhx0M z5*!B5Ha*#=SHsRi8O2Wr<-D^N??UR1+M>A3B=(*GX^Yfo0YvC**^TB0kIszv31`m; z^m0x4-X55nL39mvfp1ir+Ep|7X&+I@m5~3D?tNjMOw2UDlKy4krz-7{%)g)aeqT6u zn&IPguXch?g($zU#?c5ml)3Hy>zrXNoadO zelMB; zyts2>rDg7#H*rus?oGPpVW`J;Tx@dm^8ICN0Uo5eO8+?fA&DntY0G!l<>u~cTSiQs zb;%)x)JFW&B3_ZBg#biv%EQvrra-j0UBj(lp;&FxQ%<6tlFplfB^RaP?<0c>+>m~m zd*ma)l$bPU>|xe_ znhe^Oa(w0thhqA=9hA=NxsP%Fzsr~$Z^a#W`8xm&H85Yn2LD`{_d60DTiokpFLg=P zjHwsbCR1_gU*g)G5yNND4hVYVW+F~8WNG9)59N%LKet3;(I%rz1!T-wZFD)9Z% z43X+YlBP-FFTrlI*#EnW-xL44^J~+O$@k87D)ntCQP6blPAu59&@f6W!M|#p zMrh_n6X%8XYkGQ3zk9Oj*?&{rRbYXHn%NrG_a0?8n{Pz!*rx1iUUoBJ1z4-7>y(ew znSZaTe?vJbu{43ba~VIDPURGYXEIMbhzHsG5Vm~E<#$m%%DUrx{0w~ zK8BEHGw%CyDn)Etuvv`Q%uMB(@OWVe}*=zD%KOzcI6F zLBpFY{--J(1r$gwL94K7WhhbpNO09#oJ%f|%XhOjE#|@E74HpG0xVWr96n)bZ{4xB zXZq3Uj=ix_st~?jI@KNdb2b0)7}92Md{dR!o@p1%MCf@J@u&Y?vj3qUS#_B$>E$yM7);g=cG;I zn{KA>mX|Xs`i6$K)^4i?4>ipK#zw{aTCB;gWJbvp`J(L#ZCo+a4&6t6r?`r^tnlTa ztACdfC-&Hez96n}Sq+uyB52NdDwBVaV!a&57!!(|q4}+Zhr!7lj~0kd9u^&2%$!`U z*Pn=yeh5r1+adBw8{@(F7G}*n{{fNnqC^c^l+W3e^ zf!1o3m{OGAsLI&#n`rS7b2zOe$?&8)3VmXPdm*gv)bF&&kkEG$G;)N8TWP$buP<{M zf0!2*&6u&c7k_q1k=K>OYv#5l$KUX*s;NlhVi%5(%es+Is?$V>8;Ue)Osp6!HouYl z#Fp)cdMc#lBqH*7wXz0uLj2Lnvb~n}wVd}d=MMwJ*HY)9F7dukzEEdWu_uuPp7IJk z>X2FLo^gH1@#XkmBjnBjfoYy&3+EWx{*Qn9d`=t93pl!gnFkvn{j5W4v2sbe&(hWsb`(8J!qwbWA z`>qm6&MTllF>a$h%!x-ItbO@8xnAe;CMLj#q!UtHo4g#L{YYT91_tq=H2Vo0e@7=y zAKY!h4#Un*^)5*u_7(fG|$)UPIKs)kT+LzFuQBow{;~VQ<@ep-WVEkI_D4{@Fr;7M8>j$CVr9 zH=>IOL>y;$8*9b!&vW`ot1P~Bxjjr?eoCP>=2uRITV%@m29f_DSAca+_Ct;jGlDW@+z@);n3!C`6VlJg{m2 zSn*OulK^7=1Wf_+rd+wc19&rVUGd)%alUBlH>;Q5V)c+bA{ZEbW45<@>J87yo S*<5GE$Dw92UwkikwCg>5d%lInQBE5wb+L7)eNSY?fq{ zlAMx6x?{2sGrl37}Q=A1VI=pOY<`j1lt*5 z5Rz{PS880GcEGc@b|UT&3I_k54g%OHa4Z|Rl><@*;135{nZTh0*c1Wjc)%A5_9ue0 zHqbQzey)S9ZLm{iy+9=u6ut&?T(HRlqjR9)1BhV&t2+SoH@NZ$)Vu>`*6e%Y;0H7`h|ltKq}y?8Lk zR#1Kym(pq(1h4n>^N1jF)!L-zV_5Tu#hS6EvcenhJ;}}E#k>0&3XuF|=l%Jw25JV& z*xb9nufwu8J;fDOpoYdcDU7DyI_WT$gewupls@Mne?d$J5{dSV~TuONqsK= z@_tnpzm`sbw|q)rJRGqa{TDW<#ea6CiNvkCQ{}^Xk~FXzd>;K!T26q> z-yxCoSK_Llr5Q6Cn==g>Mz)W2itLK6bDiOn+UZ&!P!RoG(9!wB3)un9hW|!A-bJs5 zQMT!Z!?SJDbE&@IR0l8mIkcs9cusEDCC>Xk?yF zm`KsqWd+MMMGoqa2UlvpSfuko>zZFhv~etQr-J)~+R_@>U~4)5}7 z{5{;c$%Z(Vs2#J-wKNPyS^r)5;2Bbsw5-TI_Whna467n-=k_b!R(2(m(F*GkgVsNN zl{T55nHD}iUomcR9!`&Eb8DFH&|cwVGzBO?IEW^E%~O*o4fm?k*>r|$W}m`B;dgnb zVY9|-rDP~GRzegBGnH87VaCrOW5jm{a^YqhQOK zUGZkBM%)P_-v|V$kZkszn!PkZ3i@_eAqK~qEj&^rh2m$C%>>-ylPffQ3tmXj(3B^3 z(u?&QKgl*~d4(Z<5Wj&h8Iu;8O7n3iJc7~=_h>G<-hmz-<6I1{4ChZ)R#IQ|@jo#9 zGxk*E50g@8R!V(g)1c_*)-134OJwr{Aq8*p+ddtSz)e-?)b+~gjgY2##kT&c`F&Zlm%SpS)&VR-|hMSla^*KA;NDg(+{Gozn)B)OUZDmT&u(0nr$rCfn^>@C?_gj1(?kOg1Iaf3|J%C-0MqjHiYgS)5X; z;T|+(|10s@eQizh(jkH&q@%C8W`r4bGu}o2EPl#{%Uk2$d$#nY^M=?Uw&$7fnm%?R z*->CifaN&QnUa+>=c4-9m%N95VgSFC>T~&_)`|+fORk@c@`YU_%f|$5?xEZ+;HMm= zi6>@gP6@;as3J?7QX7&2nk5P) z$(m@IQBD=gn?r=+`7)eG#d6Bb=%Qqi!4q7VmuQiT*z7m987qNEQC>iGH@sdNiz~peE6- z4nLWj+44Op6h=NU&-M2wLHP@-!Hb?O#R?gxKZECI`ogi(emDzNjt8i`_bLb4Xed?5468g-dWaA1GLq1$hK9SrIOIM+KMb#-w0$qqIj1x5dkG5 zhQfw{yW9uv++xOd3Qfech%dQ(u<2Q^TDq0v2D`ooh;^=+PENWK@|BVo(1XRSeI97N zW*jK_>Y7pT*+m(=4 zgZMa-2mj<~Db18v3K_XnU9>;b&jZ~pzKLs&%~`zZ!?ySc&kv2Wyee1j6NluSV>P{I zTv}&yDtfF8_T&ZiOC~m}e1v!2&@1Nelx{ybbBu2wa-xC?lVaXLNF_H1386U}#{Yc*<;ozZKF^cWwj zr@sh7M`M>lB>rZU=Uh+zi8ziVf7gF{@KJlg_S02)f+&I??Se^gW8+v!P*Jj0V8?f3 zN`0y%>;>~7j=pJtFD8qyvMXy&CR|fV=XsSK&K&B_hgPq@;ayB==Nppk95P<=kRk@j z*-*7l6K(apO-Q$Tvmy2B&Pvxvfx0y6P>L?>eY1VpA@w3^1ht_rT9l42saN~@UVGA2 z&*DB$z$K6xx;fnetw^%X^~qQ-Pr^Ozt_R!k9!H;1x6!Kh1|?E_sw8u>;3;PEE0eI{ z+bxFMkGf)@4H1g3P1x}V!G5{hwXfXZOhdYn^5|5>It$+Shh_=txz$(#$_H z7YQEtZy$zv`9o29*Nbhw<@;*o;Khh`RdRaV>1c(egxKsn z5msYO&oMqKY2of{s8x1fFT(QAw^iiIW6OoxZb~@>DBiCOuorPGTQ=ST`=E|G4?HTh z9g0K9w@ZX(EZ$REa&UVd5v%bnEY#B0gMCGjQFoO$@LTGwsWYI-K4<$ zwbIS;xDv|9i~H4MnD3_>#3qJfR-+{egn(duna@inTu`cK%y~0ucL#!N#@4LzFC@iJKLvM* zX-d%Au=+C#kg*o*n*ppF1~_tpPwt9r{`z~R)eymB>-Ox0Twf5q-46*>&$x1apLqLw zUQa^0_hV!12|1|eNc-jsS$N#`6<=|v!?AWE=1n~I)8L(pjawlH5%dM_e8~iE(y#r@ zNC)X1xcCogYe5KhVvAGKT_r5D+f)@vPkNUzJw^xvw>*UrmrwjUxDnifc`(=!B7-p! z3j9hC4hBuK9E&bk}ihdE7&cD_c1>x zR;wuZF1L)BWpd2E&R$DBNF0)Ny60l!y@^;~l(W~jr{{=Ys>+>unUNk=s!8;zrYY=7 z*Z0=(kwY!JB#DP{#pGSH&i23kH>P>qL*?9=Z~pzMc+!l_vn#0%E_Si2kN4vpHo3F;xt-1u5Y)Mnw~2O9Mh| z>!B66+NMBlv0U4`-P>JfZtl9fw{vZY82zoa}#7?xl57@7uUbQB}OAbO9s;QXN@q|ZPK90a7>=Hwe| zS`TZ*iy!%yi({Czs0O}gHc2(xPvVOwH^fFS4yPp`y0Zn*A81zcs~)~7&Mk-)EqqY+ z2U=ZCH4kaZb$^{pKweZ|8~G2MC!yr~R=TU}{aRz|u0Hv4aSK zDB;;9z|Z|a`z2Eh=^)%qeDDYeQa03FZ{<{jSKI}>yAG%+W9{T_?oH6{xeUB^960hz z`Z?5`IhCT62R(M(PC$}#po(r!x`8{$#_U-|bRwF48GmH7Zdr0=G0^fHutGa7clpB)=#(OrR8L$L}uGs zd9w*FNtU6G!5#sPq%HlJYF z2EGb%qJLlsjb+7x(~{tgCrEZAtgeKC2VBtcCvyc{;)X_{q9(?@~YBr}0xaFj=iBf>-gY)b45 z*(csm4SYI{4yb4|EYj}ibJ~bJ5sHHDAkcFKI6D9=C}HilR! z0~;5ceZE;hbqT8zsm?zl$8>#j8t1eS^MC3}9Im z{5jAyII{~0^`SwJY&2HHdag|8KAVk;4CBr~e&+|1*vMtI_}Q_y7O@|JmyQ6ny`Pz5fn* z|4^R)-tGSve*cTV|NQ;`q|N{1@Bi-h{|k8k!`}aDtp7!r|9{Wq|Ge7&%HscdwEy}1 z|0;?9>h%9}vH$1s|45ntQ=tF$`v24B|DMVJIFSFe)&C=f|9-ds#Omx200008bW%=J z00|Iku)u4jUPDLJ0003rNklfRS8 z9SMInlg;HLk$)>>{`sPcjiM{EDj~8Oc1b?%2U4ll zs5hEWw_6eg((VAAZV%`Wx)KIr4uO$lwCjCfFqR;Y#^h75VklL%htQvf!4z=5#YAR9 zn0F-r#tT?>b?0RT>vjOhW(zx=e}C=43WCF$_2Ca%OMe)s066wJnJ!S{2~<_MhW^n7 z(m2CKMf%|U!Lws!9S#`4#ZAQi|3CbHh{41&IqoZNIxVLT zfzF^(Gxj6JZ4{Ssv)IZ;SzNFzU5W`TcJAsFh|+qv#3w%)e?7XX*!TY+`&qhc zmjrmUmTQ`E-s@jDopKF#Vz5-TyZ_a{ZJm_teRGfd;FGjIGK-Jv)gM`@*%d;^ z*P?*Q|r#AGir*_J+S``{GNXYY8%qb8&B$IG{xaek@Gy<=}jM(Oy2uwGyTwrxQ72xwij> zBppi*nq)a{WwHTkqiCfj|7C`W78?Zaj>R&MVaumN}4}h#j=qB zlJ*;sVYxC^0vRj?LW5kj99a7g*^uBiv!w5|N#Od$l2~@Qo21Ac@djY4uZ8`NuV$$J zD4koy4G>_o_Dc%$BFYw|F{7Z(ZtnhpKW@VMa-^r{w`yi+f3>eB1y^4hA1VPdGB6*8 z+b*h`wHIleVjMEu1@?<9B{BFC{6c$X^W2pixBqj`g*ESq_uHR+z*_MSPBwYPy@T7s zyx~qNN!+{cPu8~9$Z=6(R1!SijHc1;-85@h$j~-)dIAKkeukgX!M87(d~zZm?G<=K zrhnC1`dGwz3A{2MHv#+g0SSuht^|YYdDUfIS3bFpU=G~GA1O@pYrG@TIm8k|-bhqI zC&AY4L(itP3Mi2v8m?ccW(h34QB6dSu7tN<%yf$}B%cHH{*+MS-`(f8ZVQ%B;2EL2 z8q&=F#@W^vYD4|vdl70)c6rnX`Mz|8gGL z1u#I`IuCiSSKuYjj;U*+dNjnxJx0-t4(Xw+)ysVBZ9ymFRAnNc^Wdiu{LQj0HE#^7 z~wUso3UBf=C5wz25K03kAf>d5e-{^dl5ni60|;C@q&>_eKf z7gBhjc6Fr2oeU=VZ0z`m+dunEfHK>EJ})PVje8)D0kq4HUSW?SlbM-4$Wrk09(W7O z$;of#ChA!r#?^@rEB5GrkQWj@r$vN>VN6!hJ-bvYq~Bs*()3mb_Tza*(x&3BmKny zOJSG3eXiEW(5C+F>#t0G3w^3oZgixhyTGNlv4>^(o9ND&+?juW{`A&;D4B!=u_XeO z+xp8w?p-W?b%(7w-*C7D!pe%G+!mI$u^_}J7jL-F_(jim4VG}7Xj*d!58$anfXnly zXFi(`KlL2e)t3`F@OJdA0BrKs|HIYy)41okb%7{VG0rAgb4ecHs6;-7Xkn0-?p;Tn zed_WpjOuX_7nd;nz=BvJ`n|oM$HB+Fe0g38o9HD%KLqmXr`)m2_@!8g`+ncF=Bql~ z#}@e01o(YvJ}LH-l|_=e})P)1x|vh&f!3}gWEoj7$@3% zb+(oy82WWLuiEXEY$vlhaEthDw}Xve(dcZ;h<@o?|0oJ#<2^2}pOp6g8q9E6;%c;E zEhb*Rz!5io{_;CCz6&^Wo2i17nttDZ{E$=Jl>-@aevD`}!NbKp?ijV94{E%7%FzCr zkwH8^%Q*3SU>QW+3!L%fUtMh^?yhnyXart>aV(SnAw#n<#qu}nP2=-s`e00m%M(3? zDMQlh6Rh9l;7-#SubBLJ`;&ph$y4LI*SQD0XN;3^!zbI^X*JZkN;3_?O*(sG1QKL6GgJ_8u`9H+foxv~r1Xz3a2} zV3V={pnQM15bMUOvj|6@N(6p)lzF!@(a{}RUIDo--gEM>ew+&ZF%P~K0ZO}r@P=22kApue zV|;o<7Tb@HENx1OZR4b$=Q6&3o=RDF?fH9I2~eXzxj#E?)?Go2e%O9vHhz&8>()vK zzq)y8cid;uU#KE+Dc*QUOP4Q( zd;dacFWE)@wCObU>rQr-8dHH%(T1~LiqB6Bz{@Mf)k-$W&c&X`P(~3(A>+CShmQI; zMblcR-Lrn@b{93al<0J+KRyM(PjtXx7Q#Y+8)N1)QCpxcj&10gza#(`fj^@hFdiBY zzY4BMJ|v8jyMG}SNO}#4MY!bz80O`BrgUV`$p}*@Z6Y3L{$+T8F<4^Pg@f|)utv54 zb~T)nRlu0^!psTt?^^&hEZXZa}^_mwElqWQlJ(B0Ji&G}1z8JnZ_u-Rvl zB^3(F6$<0Ii4xvOYZNPSvag^@WR;AxeZt4R%c9EsW6HOV4q5tnJ=wy(8`($xR7=z; z2%;f)5#_jfSwB~?FZJ%Pfk*RMd}L$`hN$pLqtoawltvDLZZF3QYpZrHEYu4%;EPvZ zy8c;4UIqCd9x?6n^aba6Svff*89S>*WGYyS>Ou7fR|PfXJdy;q=UPwzE-=&3(a6SY zIoq&}9G>++eFQgVa)mdrf4rh9`{&KJ-;Xbs`KzO2*V2NMC?Fb)P#RNz=OkQi#j3w*G|Fm)Ik0uDw$aY?$V-}ptYSL`hWP%H)EEaKJ zEtTyrQ5>SCPluX{9F6zaVJ~-DKSqlHq1{rCD})&~?*%_)6pW3cIVJp;S9<9BPVCKJsu0w-Wk+(-!L}a8`J}hy!(OLC5zuwBvmgR!6+}nv{ zkj@xGqYeZ(8CAVGnX=H)%Xc97H<27ut5Gja`TTKv`f6rooNa2cc*?WP$x0l}3&<73 zxSp!SJ8&#`(inMIlgRw__-D>V!qB3Jh>_U`KaWc+r$4za3`KgHGP^ux>FoYZSy@gOXb`PAC-7H9gru6r;A1R*w?JBy+|ji#Wd>k z&yr>nd^k^knh0;WA`N`mQ0ZG)9ANQotrKlwM#ne?6a&Y~QU8=+&eW2KH{#!}{(!M( zrfawVY@(_dAE+H_?5Or#mh|5D@*w&WdyX1yPmsHZ(3#Rdj=TF3qH$_v&{r80k$v)y z5x3NIX9w($c|A-FXv%*m70a<8rYkp_ygg<82ILaK#Opj$_R3QZD&83Zj3IQ+^YDp| zs%W29huS}K@_BAGZT{uluX1gIn%L}NBW0q6vU(eCo>T5xl(0{d&7?FZ4 zS?{$pB*-=_xL}{#rAqgfAMOVWAjiXH7+>yuTWjI14DR|i-ukcj{Jt7^3>0>T+n(UO zOJAWd2FGH5T8 za3#&iN;9-#WRC)n)T~ZMxIIwg;$9(ZbBTjepOr69Dn$C)D;G{{{#2F;5~u-9o*RO_ zCN5&7!zPpp4c|fqVTrbwr9d17GKBy|=}S_p+>KMWK7TtAz;}<$Y_c1c8zeMz^eBwgW0AIfy z$=3#4#jqWkp7Qm=lu2_O@qvOPkDND&$J;4L4PCL|9WRwn>o>420i5rmBhR1M5*BAH z5$it#$_{<4Cis&-OHG4(3Apdf&F?Kb_kwetS26a%e|W)7p1c~6oasoSk1d;7E#HPE z?#4@{_<~9=OC?wQa>yusSDq>G9HF+WmNdBt#rxg0+ug{9qvVDv_tFflZxF2k7SuA= zkoTruA5HC(?VYArf~kah;1b~ao!uKQ$i2{m+b#3W#ZCVnsFN=Zn3s6*Cv$I>w^d%? z_9kfIF4NyqijjL0bvLgRH%_v`8=>nlWo!mTGY@AQ+@DtW!>l9l=1u!aO|o6v>_Zxu zP#zEx^^I-d7Bb^oEONf0v=V&IXOZB#cs0AHuHAi~<3$!YyLOd21-MQVXRcr0YPa9C zJ(M@}IhS@|hWel9I|=;KgnYcz&_-671F3&Upz@u?x)b$3-Ndu>s>41n z#n%c7tgbG_!W7FUaRtQlsRUJh<}k&6ezt%b&$#@}v-9t&`e^@@DEX40f6n*k-QOTU zX%Y1P9^0X6yPnct(P&P}0LBAH<#U1VPczT_T2#*Y*k{GEUY{S|QL+O?C8QRVx| zsr@?Vy^IwD;rzMdi~;I!9gvwYQIqv&KCV-uS)-p1Vry08?j531NLkrqcQ>gpbg-?6 zBl1bmpn;seohW_jz#e0P8ZCH=|1YG_$8OzV^9Tn}#}@h^0Wc)dP6p|JeW#z1nz8_V zx=4gO(5qIlq8Iy(xlNWnnRmx;*-U{%M~eI3;ZAS zayFb@AZ9w7nTG%lY0A>>DQi7cBhZCYa)Fev>G>rZhRvJs1{6@QL1N_|W39*5s}lcffXAN$PT(n>`=;b)Ca zYWUjVkchGfjxQTsQxWPvQ@Ls*_*x^Cpp>SOY}XsUvWl#m`C8r>N&lco0{qGM2I{U% zI5QSeP}P?5jTE*u8XfaDxQ9-*$76*e=1LWM(Pun+XueKx&=n58KK{HK-Gck=&Tr?b=7qZM5)g~{1 zwi+YU{bWjduuAOJ4<*hK&ACPlR$+2?fVRBmb0~aAnt`R}Vvi3#3@$!xHPIRO69wvG z@k^IEK8|V$=Gnh{U9X73%DOre6-cb^?*A0_v+0NEjH^%Or_!-yk5-HEna7C=pMkv%OJDy~?m>Ow3P&~~ro-pul*n}IM4mT7D#3yZ!S+Xy zZ#NAd=su#W!2>p2?O#4lt?1y`-G1g1aJ}1na zNznJrEbw1VnZ5irv-$?n*}VE=gADR4Pum=$Vi0gh9jA48(qzFbm(8?u4Z*jJ+ z$+!7RaQKWHJWb~pZwrRr`!}Y@CTGYDqm`xGJURT~j6&;vO`nCob`fafM0*Br(K$2Q z>hQ*RaJp#+J+=3z&ik5AZ)H#a?d>Vumbbk4&VE`{keVHJyw=;p4c}SEtvw7Miit|mSl@*(odvGAKum4h~WPUu6#R(vno&{ zp;Z%k26PGCloL98Sd@rj#wed|?woL%DL`kArT3-c1)$wk^tQUOV+WSiqtu^bxo^Py zQ-;NijK6A!5?dyZzxnsof@snazm4zHf3G;TPpw4H+uzRqWbWC?nY~A`r**{sc!Sb` zd^agT#@)ZbdN$nw7z`grv$zc1zqKhUiuxW$8(1z+?gI9dMLXWHv;Nr90@IP&YeW8R zJCpF&Wz3_&33q!bI}0Ikvh(NxiYdBW8F?X1vMr5DtL&L4aJtA89(c;$DeJ5^%a@G_ zO!{4IRQ79Gewf2E$a|4@XbfJX-wX%h&Ga)%?h+eSDZy>N>9ta43ysT_;qGb&jN9Ig z+WIW=_VQ9MA$#JLpn=9dYeWvGOSxB$XyPOjHCp!pQJ*21KFJ=Q>=CV!reRM>6GKKP z7s|^?XC*EO^Ah{2FEnuZl*?85%8^0|q!GCtwWOF_d|FGh&))-I1bxtp1AeI4fiM@| z>nhnv2$zg>%pgfPq`+oNnoM3>Y+8!@l>T_YoDP0b-mY2&Zodr2cr+PQab>N;DK}yH(RPw4^Ec zd6cofFdp{&cKN(>88(LGv^@KHY~91dRyw}y5@ALyR#Jb>n{fu+^9mwPUqAM#DCJXn zsHh@UAy17B5()$q0Gk{VhWTQtgf#aeMfo>@gO zBh@@w(N2eKxWv;6(1Um`m24T-GCznmGWN9U1Q_|x`H<@U~!^4~_5*Fvf zhbo0nT1p9IED=tE?g!KVY(jF~7nB5d?R4zIj@*?1g?A*^yjgDv-k7Xc6fh(*UjP8aG#ea5qfUDp*c zKr2PBX$zefoYV=~92DsjmHsoG7tS-9Vv?5PAIU@Zk`G8aD~mSZh?I8to%8SbL}u&m z4uRCE73vuiX~NVHnx^F!%js73i6;J?=9P|(!GCSv_4-p}fT1An0J40G<6du)8N^H* z{aI*twRL471AZP=%9$I{H@;4Flv}xQkwn{cMK4niW%Bo4yTU*ch=u~WVb4m=@Fl+1 zyCav6gX@rZO{1r##g3cyW=5a!c6+^(?4?s3R1F`K5r-{r6?L3%=QD|~zgB&F7}OXz z{^;MA1d^g`!jbsEiqRdqQ1+7$?=k&6?nll*;B#}$WBVWRn;nWuE}C7uJhN={fJ2K$ z_B?e)fk}%7$G~#7>RJzWt!r4@e}=pX8My+h}XFV}e02(UQ zfrId*GFe>@)*^8r2EbCmOCHtO@i(Kf%(~mxcY;TrW271=3mPY-Wz{p@xo&0f>-zCz z(i}DxKIhlSB^~l+Syn9xTqQ7rrCt&xYEwDjY-bT^(1i|^dH~HYaudqf1F0)FdnKCx zo;fzdNXCD7?Ub`N&ak8}PM#kvIqS(#51)8uyeoPCVvnVk7mo*3b)BxWO^JbKD7L_6 zF+js^-@WyGz8SR&`W89Z_UE>7ymE6j1JX;QoQL?!Gc;sqfQIzly8l{J4#=^g*#t8% zc>)nbHiD@pzvDOKXKlP}$45dJWZ=Oq;F_bYFlA^A3LW^OMLE9ZZSwaAhG>Sk&Z+T7 znP$G5_`5ry4l`uq4{SIGX*uz`wzD>!Ih+kmOTpG zoL|Gx3wqFk=QT;B&zx*+4Yz|I0mS21Q8B4e_EZ_KjFcb=^*_r$xD zy&*yF$5Hc_kp8xewCsfe0m?gGuTLI!a}bGrZ^k-eQXIp1+BzHEB3t5IKVG0vi?z;r zQ9gZnw~;p^L3f@Lhyw@`++?BQT+Jr(Zrb9I+1u7hKI1C1FTU*D{4qhTr^`1>*#4p4LGezC=h-cnLF%yM05UwK@rY7WE zjZ%3C1>IQI?AKrwYA;CG?Zr(Nj_WzY+QB$j_P)tTy;?gsBL)FId_bR+BpA&=*D(D4 zK6L5oZ=@nv3i0dwBdL;YB2SHse)c1Obm{@2d!qiSws_}Kwhz$;;ZQ)8A-?OON2f_l z7L5i(OcBvto&!xs6s)tikp(*WETb$-GBET>e`zNAhuZ&$u4Y@c2+!Rg)z=l@yg^0E z31N#sO1jCHj{8!nv7CjKORBXqa@wu1@m>H^tF5hBsp3>9d()?QwoLpK1cJD*?BqbPY>{Te)ONgvLE&+0 zzoLz}O2O=iQfjq25YEN`uHj1CC7Xp8)Ve=zH-P4a1?_tdiwSbRdF;pbYDIGy%Ni;tJ3MXaSy0dh?|SST|Zv4in9E{Qj#1)H_6jgw!` z3_uO1bE)#k)@qW1@9~A$CQPRLg&jG3?KPgu)NC{OEu!3;%bdvZqP{F6pEJP8fhqXs zZc4YEeJxl@0kte2XqLu&^5}p)YhuDUUfbqT#Cv-cdINeRK4W|ut*D%m>7q(4vK7sK z*Boz>3rz@Uj%;+WW_?;Y{7#xHN@-7~; zp~e~O>SG$d?rmC=)722kO4)dMskR81;JCl4C8z|8zMM^bmxe1vjuyxmQi2wQ% z5Ucr!j^Huqd}pIuAzdpnXMD%Uv$877>f#OO7f)+#vsEJG7ToYAJUJKuu5N_8$942)qCVogAO-nxx=tcCmb>NL z8@!zcDDs9dO%~fBt=+5k(v_yO2pHF~~$r{yArI}zH=9Na#kbu%X(-FOkHX*7vSZfG=S z`ICqz3qiaX6>`NEGD+DFEgzy6uS*(8?owKoo-;rAmd5Lqs-)_=fKEC(n+3*y%s9m~ zilKb>U4@rsLWVa{h7P1%%kZn13aSL5~+q5~>+v!8Ko$c^L>vA>cp$H$L56ioi%%?QRT*k6q(~e^j_<_>YO^i;knL56Q-Q zUOOFuClv?IUMkMw9FlJvtTo<~l2=uOu}l#7UV#yl*35}4W{~kI!e5Rc>WaNE(D@^ntZ`#(N82^4r*j-lIY|pUExh8VUr_*uB_M!1?w!`8Um%ML{|x zWXVcv){gsG%|d3Y&DYGaBpU9~SJoSK_QJR`Gh% zw~Dw$P;x=WF#mb9jCiW&{lY9YDEu7tX9fV2lsds@$pYc9GwZLmiCkO!B_l)@0@bK&$M<$GJ~F^w`EPmVICHh!Sj5_g=#52?6-_fzmoYj+OT zw{=F_9V4GR?%6BfvW&{0fqM~^7^M1k8qnvjKsjw{t%e7nB*rK)^(EfacYRIzBOm82Uw1WVgLsvX%XCZxd0)jo)v}X zN<3>uf`A$1cRe1YdW3*UaC~2=@8am?uKQC9MyUY?Ho?S0>J1%64TU@|14#kV+O2*E z+Q1Kwiu`IhH-S@#-C{jn5g};{vG2n$R4`}L@b%_?hX!WndBPu2TpppSPjQu|$;ynU zSdcCiFw{8My@ayIN?rc_CR92&O+AR6wijZ*8&1u+@#LKq%=-Gvsr^IKC%=tH(gzK)e%-Lpcs&iT}|c28cspeLhrg2Kk;DtR+dh zSIF3VSt}*fAs34i(j= z2rlmE8k|L`t+adis*w^So`^Z*7ExdU>&|O=%kYdz&{hP;2h{m%zhO_yM*6;ojHTM7 zyFr~|0`%q5-wEafe2EUrcYH0dUIZc`8JcW~Ddw>OvklYH6p%s<6OC;%8lPi7AyBVu z7=~?-$De{F9`M)b$xYbpDJ-1}$=70hTH&|NDPwe6zRJvNN2&4EllYMYe3j?Cvmo!z zzbvh%T%eFm68)wa374%^H8gR+eLTW;<< z>p360x?F(13{T2gM>yah=bmLRj|THRY1U%bqUFM;m%RA^hZWW_aw*}?mls*D*4%xF znlp0@?BEs0nN~pgaPu}CNFD6tb~`dmj!=5$>X@sK?sGeSH;+cJ+I((13oEt;?Wh;1)?6V9fP>U8 zm_3L4;svVf4c0sx!D^ygbIz@1%M!wBKm!>xOs3=c1;@I1zb{m@7$w&S(Q9Fr zY@@r#BeLVEU>L zT%1Jt#C`&)(dW!^@f1lbb+pYO6jrI7qJDeZ1$--WuEne7(#Mo{>NF4xFGkXhd2`@Ue zE~%M)#*9=w$)3ks7Elw(EoEPREolgW}J-JWtLS%9}Sb1*iaRL8{lsbty9U=sJeo8T6 z@!qhm{{w9}yOaF1B_`4o9Et)@FToz0|1wq*&g3ZG05@>{m9Zy2SxQ;qht7Zko-F3s z45_<$UX$m>l5FF@k;C%y{f)R*0!q+OIXz6>z&YLN)Zfwf0ga@ojJa0iJ_i^GKC`MZ zRoFu>)iuG~)L$@+7zo?-Yl}nlZWGeBX zrtl|l@d9>pjHrxaQm(NWE83_hsy_8f-Rg;L9__KSZ!i&`J5{4I$s{~2Z_iLm&DS2d zzmI;@ct@HRvW#d&qRR(duICf9d?0-)XBky!0YxuO32JH+fh+z;j#G;y_F$z-*rsLJKhR79pbX?mKQ$;Ry%+3k4{o`bZ+NeB6c z{3eugPUu!iiRkc?3x|vkh(Aj0YqVJ1=60QcHToM0i5-K-5m>8IwW*Z)GWJ7zlr)Y12mq)JO!r z47#6{Qt2kF;Gjh@KQ+It$lI4sMX+qD1cX9e_k${9_Rm*BN%@hnbfwUUuOSF9jcuOtIn zN}o)pw*M?ZL5Pr^FtMD~Rdb;tlmkP@z{YOYtv81UE6@?jOk^X>Mjpo^BSktFCgqW2 zg!b372_E$9-@Asv`8s~CKKH5bD~z!I92m->({n&6j8xN-PUk#C*lX?UkaMG~(<-F> zZuqR7652>i70fdFgjvyH=<%ms6;hht#C~!GBExwrxjC`T11Fq<879%@L5vm>Qpq?Q znucsHRBcqA;~Tv}gE;!Jf(S%G7zGqYvCQ4Uln<+pp^ivbikO{~v`lgonkA$86y&PT z$8&U@?-I!oID(c9DgJa|S05o1OIZPm7Ee*|N+TX@Ram?!t3$O{Rje>QGL&|-id0$z zEHJK*C`%;omu}Wfu^VOm?E&Q?x*Q#e2q*4gRi?X1O({isCtT>%WhEiY5QK)K%pT5` zJW69hjvO_SrHv&hEJST$AOD3b&=2D@AXB7;{VSAxounOSOwBnRZY5)^SyOaJ;bg)$ zkdVk=N|KuUL*skjXql}`KMY@eq`e|gf-rEXK63d3Yc&G3mN#XgJ~x%g=}wFiD6GTV ztnJKW&21d6|9nsD6q`4xvy`b{%p&ZAKyJx&7qr~;4`BZ=Zhbga)A(LxC3D2hviFmt zG{>g^)}sBG*CeDK@K~FEk}I0J78a$w3(WrNT9Ge6OJW^4A)RXXvJ?A+g@H+m9#G5s z!KHYE*j_a$fjC&+DN^J9W{PA;G}la7Ifk~F-)Z`lo_9Iu3wgL2Rlz%==C{jJ$fYL0 zXuihYZhT;45*)*kKsL7i9UYdHV_XVub=4u`fDV|?F zD4BjTTxdx1(GJ~kwT$OG-y8wW9dR=VLZU*5$3%Vp;@Jh`&rPhgSjcJ=*3eX4O84p>O|Gz#6{6IKq!eBHSXSuR zC%BL3A%kC_wwsZ@`?PQYs2bf%r<K#22LxM zDwFMJNBMfVU=(Xq4NWCB48`1yEX~>wS z#f41^*N4^m`PD_DQHgn~+(~~>>yb4y34D4oQXSZTrw%5l=EdUnlX7AI8xa7Ei=j0E zUxx!yjm%qM8EHb-li_qRoq))MHF^UD&xc8$51(9B;US5njwYx&E&I7Pbj5dVhMQHuJb7X5Dp5m6qVN`1=$GA5l9(T3zcs| z3FX~TYdsNux%Rm6V|`yN8HQ%a=OvU3yZF+%nT1{?%LDbR9*KRa!tGA16$)g#J0;b% zd8SnsXc(G`?$@8Fvir{B8(LCz@UzLFcoQEGHk(7Z2ahG+If1{#oD(#R6rB(2o`2=f z(4uVeB6#n<#Sk$rXE*uPAT0ecH_aY^4=-nN2n7#s5kFRB(~K-CH;b z*ddxb7oup~133M4O^!$qJ8clSmv+8}=u;Xp;D~I$U2}T*JPDQq4Js>W)!u)7y&2Ke zI9-n*@5jU5V!~c3Nd|{nr4F#T^@#n2x#R_Y%s(XsPa(B3$x3zXud%HIYMz&$6o4I? z*sqsUw>IUsYGm|){T~+%L2INy8v>2U{i0TlHlL~b{0#VV1p_;+>2S*Q9bbrnQTS_9 z41w~FbpE(e@%s7yezTmoRaXPTq(vhBoj94|dD}g`)&$h|&XVjW{fA%16w$6}2! z5T(>s86t2q+pB^K{}-a4%+vZ(##}vObg|lgQw|S&msd^LlD>fnz; z1QV$=iIf7!|L#aIIFV-_?O?o&nZd=l;O&sIW5QfUa{GTbB>O_YIN2aBN7tHuWWVG= zMm8=t6oPMXj4LB6T6N#{3_q>zapnpq5aPB>JH0HZx~qQOy=JEPc96kKxem6t+FV^#0NzX_ecuQ zyE9+O7Tl_kt;Q6E{PR@K!hJ1`z5Xr!1Gd#|f0{jFt^q+z=lZrm95(NQFHxT-U6_%} zp#qsGXiU;T6hv_I*ZVEH)^uN`?7!2z0h}}nN}3U;$+5!;dzk^;}4=+sD@uXi3m<>&Q6v% zS*AZq@Dk~NNF^j5K<@NnHz_Zbj6ZR!$h}H50Y3X;)lv?4zU)qb@Il$&%^$8<+)W&P z$MY0flKwbYi9f|h5g-(g0;qw9(61*x@Bv5Bbkkz?CSc+W7GO@oB*(|McO=BvoIup} zQuY$g4n#3)x9&aCh|zc`xi>H?x?cCRpFQ=__a1lt0zfqR)U6*LRlr<50ya&hXO366 z!Akuu&-8p+iI#n*C_a2pX8B8e)-UKnTgxFspmHy1GkA=gFjuuwWZSHpTgQG^|1Se( z4z{B_)q`2JpAl(_#|Tpshg;#~3&&bHiu8K^>ybWM1hghz#7I9PIfMB{!xZN0X59%A zYwQQ45`>#$_X;_CW_Ok8X0Vjfp-o;kqCy;G0hz?gY2IN=bIW2r-vN%Bs$xW{IBW|| z63azyq*NSw9zX-$7V+j(teBx^54&X`jf`QNLld}Y)?(+EI5d<$v|>CSVMqe}%(z*} z9lQu!;fp}&rBk!P@l-`=?HVlo=egG1pJf9PBC%P(#~IM$_-QkIcZaW+d1xqY)#Os* zh!kYbqB;gc9Y9R>bJl$~-SnSNz&eytzAv?TgtSrwphR+gxz(Me0Cs(5?Nre#5**E| zBjSZgR@^}ZfXfivTASiH5|U9l60)HWeqi{n76X&8YUiV#`NN)&Oh#eNEGP7i#vWR3 zdSWPV)pQVGzP4%%pZtOwol8&hmFOLSod}E^bc|}on6VR1jo%2Ay(jx{&Co}u{T?x>0Alq0YbM0Gal z0*)v)Vb`2ehK0ckE3TFQQPSm0m;2SxiSY=^2+LT{;QuCwT#bRtzK)+rSqlI`S}=<_ z*Kr!SXWMLk7aqriv+pB_w;RiFBDdel7nqGps^e`Ntw)IGh8l}A7ne-TR!3?oIp>i- z4aUaa2YJ4fYAXW*h}`OhINTLpz%RuMxccmKJ$8Z9i%u65>2ud{l6z?#E<*{BPqzJFWVP)Xrue3-`!U0S|{S!n)BMn>3{nx-t2l8C-<+D z0mWL1)>kfu<*=}&Y|n~LE(RnTPN}hMa8}mix#)@lnB0q0!-FMwGleo`LUI9>$N?ad zaWnGAglaTl{J!mg?*pcKB22%oY)Gz0#JB3M5f5(XSh6A}cb8CxzBn)G8UHDTI)EehcDz)Po2FgI#o*F52Nxe|CwC~dw$M$ z6c<=80}WUmbwS=dm#f;0F%d%+kk*_C`d%1OsWpNUI3v{CaDA83jM+5*NIzC9;2KA zDat1^W}9R`I$_DO$G}Tr_zssS3gK~V7Y^lst#HAT)(f-Wj|q-Q&vX&C3`Kj}FEFr( zOoq!|NMcG|$ZEIZ-Sn37gLI#DjX=cFThCL@0H-j%?>u|(KYo`xe-TAd)O-p^RPLz#3OLBxF-)$EGZiS+W&!-i&|VZDP=@DV61P|7@yIXK4I01r7#16`lRNh+F-o@ph_hq&3kaZB&o)Tt=i(AO zd0FbvnIR3ofC$}$Kb{GlknCIR8PDxieDeQ+W*Bi_o|W@n;I02)jVQ=iND}@1jh+wR z{h{`GJ?lY-LEg=8hpaa&@7JWbS}Hg;^aEil>!V+Er&^;4M(h_F*XS{YbZrb;+ITVw zP+emRl+z$jPho1%giF3~}8Qc)ckY7tW*))la6mRjE;NR{pOM3{f5S>M9+V{^X0{-bo{Yz%=Y7nM2 zk%e@Olmiz+#=fVNJaj7gqH`-65eq57K++oI_mHReo)>Bw3s!0PMVa5ypy7y6*d7<% z?{Q8YSH~@YgbOIAz_BLmK{yS}RrI+Je_C=Xp~R^^F#=Du_}eJyO?k4Z z_`ezrH)flA@#lEYsvA2;N|KJ)WsYv;jjCsJ1{7v7qsuM*Hd@{i`$!{A1US8NjbZFN ztnGV;dJOnPCUKpw1jBSOm|{y1J?Cf((oLTS;MRTI_c{VC%;B+!zM_fwx7y3zyg9+# zoI(N)Tm9#&>`SQ4hrk8c;xw9wK%mmg>(t13pcIYa_t53BIvjnUDe%vOSe-h%ZUAHtWPly?>fU>mcbFxgCM=qaljZ+F;Rz&+1(_IOMoh@3>}7cQ^QFVu^h`Q_FBEZBDi}n z33PBhgB^|H9t_{TMj|HsubWSkhwy&Ru4}d^0{@RZylxd<&t~;AGG#fE90vFud%MBe zu_p(Nb@#AEXbCHJgD^Nln@hxY=q%cfLBiXr)8z7BBWb<( zf1-G=23$T9$j9yd5!4~FrymAno74^jzllNt(c5*3CNh4qUZr`$IExH7VYeKe_XO3- zqyL2L964ASoP}>?@;>72E#=T-E_}2qyXxL}H7gvOnX0o_XOfZ$4|x7~ut)Kuf)ttl z6!6tAy19f$Ftf?;8;{VZ-xW;D}8Y{j}-E{0}(WW4<&rXuZH;WM3DD{1RL!|m0-#AsjX zxt0$bbhSMB8QBjiF}~K>Y5qT_9RzG@270!-fZxoc-dkp2^07cN*eiR@_n)>WF;3!6 z!*T)@nzYqks!B{XDyvB%w4MTAK55UDyn*G`wq$$G^`(7#9q7PR4D6>YxiiXmOCHKJ4ksn z>h>RgoJHcAY=7 zaQQqjmyU4om-5~2#{2s6_HF}+xNMaoCg3Za{9>B0Ln-utwrfSJp3SGjgv^Nu4~Q!^ z&fIIwplS@Va(BNk)MEDbd_oC;;66ddf^|fXe;s5EiJ%z+e@TP>?)@x4ot-?Z3O^+g-C6ddUapJEj~pTi zBDV_csSW()k+x!Hs1ZMqgUQE-;>D7OGjwxv&F|W3!tP1@6mrAQ1LxqqC%W2P$cthY z#q{!?p&i3uRhwMz!gF_tuepL~`m2aW%C`1sV~7387T~&l{10(F$yq52(?%`gux^4)=n^;t|~ z)G=JtQG-W2mxxHB#saWHS7km$l?XSaW8}WxkwnWkUz@DO3O{vIp8Gm)sC6AH=0qkN9O@wndK8oRAjvao-Hfgo~|J60)guTR}O%GO!oE-K4 zFV4_ruv`o0+EDqt=q{9?E-XoW8zH!azp+m-*n)%1X5kPC2t7@nK(XL#(F#v9kMsrp z$*%DBulncAn%9Bz^zsLS6uF)ZK+ugjc*BcHos(GzJ_M(VCKi8|fITZG|3|Ki)RFT7CLC;A-zI#a_{_=w{?&k@ModP|ghzs0PkmitBmXLa&p z!c_zV0*lde@@Ec5*=Ot^eTalbj?^0hc@VdWk(7JR6&HK}L@|{Rf{`@mZnN+Q=C9d% z&pkIzMiXPGXeeu^PqyP+*{{n^>R!`*pO&GE!ru6`%x8|BXA?hNrQ9G zr9k&zms=Ai@vwS3J5$LCoZ5*<|6C`Cly8-&-WO(2N#l|>L|yRsNf(B|5B_Oi&g~=b zlAogW;NQVpPpaMDh#1BMEuG!^#SJ&we-YGbVaD%eR zs>_HafFg?tTH9;S#N@jba#PsVXH6AdM=5>%{L=}A&aRh&fkale8UUsZZwyFdoE)W zD7AhzLrvx`TjBHmJ7vMw%0Z&g=B7L+@Wo$He& z#FjHkQFSs}b}QeEJK6s;qwg|LwwrXEbIc`B)5ODUyXvS z-4YB}&+?9H5E4Pl?9h>xju}gj`Be0d8P`X8y$PHsV3Y&9U_t}28De(D7$^yJ&*4UGU z?FB5qxN94!V|6;_5w(;|uz6BHxsv8w&mOTaRI99*+r)+4=CH_f?ScQMUG}(@m(dC$7K&LQj0{^_-YxJY!Ugy8UO<+-eGE4wONcI-+ zb#!=BtW|B+O4?s;i)OKTnmel`<6jWwtB1B8y4a>3+0XwS0YE9Mt%cQQM0kM(((og3iS~cah%062 zWZ2&R%mTQ&0TX+im|fe7V+1*V zy;CQA0Yc(VFZoqB}LKm|I)PWOFpsI0J`Pd!Zn=D=v@)lH8s=I1VuZEQa z^a1L77kOCZN%n9;Roo$!U|NK36w_c-^ao9_NdSVXjD#hXH0_9dJF5p`s|eJE@~n~b z`1s`OREE56Z@|_#koeKe#dj;6GXMhwSzVNlwfFqxxBll;cVz_HG^p+N)0bT)ewg%@+PvebFqw}ih9^;glj@hM+lVO6_Mob)CBj~GR=ma+%99|iBSO)fiyUIdIuC~58o#Z{-yml6`4GD6taE>E+blb?wU9r}f8vErO|B08oYrg+2P-stba9-&dzdTzZu1Uv z^GqkN+q?xKx!y%X$JPTTN$#V_!}3^XvNez*`0{lU}J^SwP@29|aV|ZyGaY zVJi_L?X20_klp(L5412O!P?uWAck2ah5@~vrnuOSc=^`%KwJh05s{Y0m+DJ^=rimk zN~Rnsz<6ttbNghiwP8z(`k%g7@itLjW7)NW_a}HKAG+gubU8a1vBp}2mv~!D^U%gc zT6t}aXUy>PmRzgtP=v^!q zr(i#*b7kX5GGXMFzBg))FO^I*5-XW%hkWFU2E=rDiUEUB1LO3HD~i3$^7Wr!<{Cn; zKV((mZq3HlSN*|KaxUOkT zpYHg_@_?^3j>{O|b84@#Q|Tsab;i3E8NY0P8I?tc?!PoZr-he*6vH4K_11cAmv+D*cBmV$EEHpsOd%rLIxuo;BRc!cHr4Inh78&DI5&fRcQij|4@6s zXNN@Yl`B|)5qCdC={KWUE3(9Scu;?iidu6<{WYWe*-*f-3UoX) zDL`*`@_P&M)j|xY$NO?#fQe&$fTgZfft%8Ebths95a_E)qsGH?+ocQqf zy9wFQcAz38HbU6vvU`aBCvA%{ni2Z~7xZq7Xpz!O*!fVbXps`SM#Av~%IyI)vt5-Sk5KtxVnp z8Rqzr6mnaA$-^Gkl6Hd@!yAs%`rp9VRO6 z*u1}pJ|kD*%8}4SaRrH8(sAwpY}&Ez75BEo8SAk(CS3Si@C+jwsgn(BC6F)-D_y9jC^bIXS-5H4Py z^-ZZh0hRCqSdCHN<{Gl)rK~&gYD1^no9z+1h8c{Av-j3ZtJh2bBL3nQfnqFhLH%;K zA|5Q~P%a7F@Z^AiD@!+DhFbihla4TZ;302b3dhEZ6LIScie4ja7y%UJAQWXr1NieP zV|1`C5^WO?FKp)5Oi3YMmi6pz;M_SpNCz3ZhW8tsSW|Dw2j$XWzi?v`HcF?MTX`7r&(duR$BSKAYxQ4O4zbfoB9B7fpD|Ey+TU% z=_=-&K3eh-wRgnJ!G`&jvwq;51a{NYbvL=dEncO);(Zq$Y%MbE!N*w|x3+K@f7d1} z@)ny%*4WzqrwMvqB2z7F{@A|3-PR`}7owuix34q6N7x3t4WI0R*wSUAnOgh#(gs8c z%arPbk`xqZCx%wvTv3Xr%7E>9xZ@+5tM&AQ06)(k|E`xno)l~ATZg7?X^GkAO5pI} z^Ce6B*@apiYIhFHVS-(G3kl%0kVm_!4J+&olM$?(BVgnZ;oI|qZV}%}aXm9mP-8Ja z7yT+s&G|_2sm1Z*9sPC%T`$%gF$033@6PC(*Y-71WZJT?%hUc7>W|fGtfeSiy;i&DiOCD67(a-45s^_KxBfz@!l44 z$IeZE)T_fw2Ohj14t%@9wWZHJ5x(IUyh=a)ylw+)TKdlwT^#c^@~0a77qu**wVKh8 z5O=+aXt<+|HR({%50b0TKqZ2J?GQIiQRl7uH(r`pUJy^Y!$x%c$|0Q*vAwcW`nmB% zcaS0hAkPos>LVp3P|9UVv3F79Hjv_x^!;S*Y-wBnb(pQdzGXchT=_Z9DuBS}X+NAF|R`}yekhMnQ!5#o}s5-?z3F0)&=+*)T_FP^J2NJ2y|_iS+32v?Lm zt_YxIeCndUOU1zen2f-(V=!U?$B#!>(hNUxvxibS^gUC=Va-;xgDt z5|GyApSv_0m1v|gV|xq7nZ_+`F#P1#Kjdy+isI5-;)p7L6@qs0r&*+elr zZ?eJ>Im(i3MZDYQIBi~k>?VPYXT!)^wGcoOR0s|~By+#)Zj$j7*QjkauQ<+vFp)l~ z)&H76(`ZABTiV)OTNZ1*;?D;^$#C%BW1g#gBLhG1Hp<)azSZUN1V;e5ylWpGI0(V3 zRnF@N%M;mXL~G9998+GMz9i{0$u-&q?P{s1`w&Nuk%v_N~=n*O211K#r`*Ch+@v+?VzagPphT zmJhHS+4DN;78#3s*(V*RL#RAu*R=D~VFLi6MnB=McD?(#?1iEu27qB7iV`7{1&QpZ z_z@f26|Nl9d<3uQ>KZ?wR(=@G^|9Kn^Wh{Ls`GthDEZ~Ofus`N_Ytf0@D-vv@Z++J zDibn2@B7xLmoF_cTdc&T_Jn)a#m! zA+gvJc6r5F3+bQB*jUx{699rt7y$2_*G+Wd?zu9KROaTQoh;j-U&?ag@9zHlb~Fa?6RHb9!SGRFP%6`_R;EWw!IItqAl{u?T=Nqo}TWWtAm~z5v%! zMtqp6f2Qec-glqbea<05Mb25MXp%*7>Pb11)AH($cAH~GHgIsOuXH>g6g^YkRKf0# znnYw1PC@Kd9BQ3@`lf#`87{Dp+J(>pfr_y@jLZx+DH?6?eC1dW=Yq3DTiGs{Gf0C1 zu&&;Fp+OMmqahishcgI@R9|`4m`(fdi*2*v8KES)p|Sq9xb<~IzX-5}O>$uozG0}K zz=BBI1Gr5qY&spxvGrw>pwIX!8+Sf)eM91!OK}g2%r0;S+;Tb3tt~`yuEq#YB{bzD?pLuF9%eq$8c|uq*5S_XaLDj= z(uYRDirdP!yDhrE?-lWx?etI1Ltu7Y6j*@iwcoVknqn%}7czws)O9|^)#8WMTT%do z#bk*$2LxeKwDK|v<*k~p6}-IThTkr-8ZAc)iz|U&KVbpVSx3$KWP4;t?0eJ$Z2c(2 zV1p2gPEGn}V%8h7L)q=EVmZ?Vi%8P{WM+;Gh)6jIm6Y9EPbAZYX} z^8{teO)=TyO%sp=6eXWZkwjL+*en)kzTT z6oecgK{66iKY~bgz%o=Kt~~I9F4Z8z{d8mn3Sp#2GmOzV%h(^MpT!>v>H?``mWBZPT%VYz{ zCC0s!!JPn`j`y(cPhu1wKzaATS6@7C~I z8%;14Vf#k{2XDodma&|?QAaLX@xT2y|GvRx3HF6*Wb*_4eT3W(c8JF>_5q3TityqT z?-4}{xdtsvTes90rPWk+(DQU2BEA&Lm%t2ND2ScCmu;|6&>2-iYo+v0ew-I`uFc?M z$@@?BhqdXOIFjt0BRAf4`foZu9La$Dd(x+eo85mAgis_Id%?OOmSW+wJSh8 zR0=45sP;9?%5tO%e<@5nF)1Kl`gga_D8rrAQb5VRHCQDE9zb$%!MHtdF%@sJd}Vm% z9Rx%sl-pT|R3bR{dk9dv1V;StP-aVRJ(%?668ZG8zfySa zi{?AAT-doIwahT<+}4_ERnsHbQ=b=rijHo2jkc8H*Q{w{$%N|A+0X#~k($Na=N?I> z`jPAt4#YTL6CP{W#CA{1Suj|)p)VAr!^BvmhY=5?U{ zf;tm}W{`aXSQg~eD+q}GAyh$q=DdDJTDFuEZ#Z2SU&e8wOK{4X+TK|rg; zDVb38)&xd5J2q;k6!)$pz=64m+3IzHLe5Z$(&zHsnox)=Oi zc!BDrZ`H0$myGdl=j#A6!`)MIRX|U|C_Kb z^Uc5W7{j&#Nj~j2Z_v^r-o?5|aoxQH_tQ~Xa&`T2sTcs@aoHVI{W5ZYcd00=4Kgy5 zE$zS`&`Kk3BAGp>W+Sj2-}NlK{3ojVKQSw789w4Fkm#a_fBtHOhizryuHD?UdkK!Y0xT#T zYW?OuN9164DaOMj+~dIHG0bG4ngMX^!8KxfN+T+HIQ)`$oRjO`1Q9$s6&EM3ush1& zeKg_aBfH^YPc-d(=+#*5?F0;*F!9q{Tf(XIZQN3Am(1oyty};AKxT0aTlj6#g?bDe@3E}iwU#8-c#c}UvDpwU)mPfp&jLP- z1>X#vFb4=Ns`A?{J~s-hZ8c?aN%#|amYOR%h4`;CAI%&OCS*Z3@s-$>dTS`R+ zOm1&;hF1RMBx@qESXk7S#a*rm8nu&j1Z9L_5}kR`C&d3buy}cENUqL+cxQ37dv#p$ zC#HN!7|pO5EAfioRLmM9l-wIJ#v(5%b~+c)=}s@VKBW-SHw3|2QLT z`ty`5M?)7)_wQvz#nQ!PQ72@0fDKmRhSZ;?v~N?fb@^Iu8x&UIk@eHmFsfEz=zh#^ zCk*@_V^e*HAAtX|6NB^(YC9k=N?|({-)$MExU~|BRxEBGaMQ#6?XCJ4w}v>=wIR?Z z8sb%W)G(`k7AKgShD&!B^l?K2P_ytftYbNentY;++fT+$UM?g&t8R zY5eXEhTg}}YjIx0#0N*(JE`xlGOKr2e=`F|%@L_msX@l)1&iE?^{J!p?XReFWHW3KTGSBtl@I~I!EQnhTLXvI zLl}(vHffvb_ChD0@gMc?TAo#dL%spjjMeQBiEpd&&__kr>1eB3o^p4zFGNomcK!Qt zlInNS!;TJkK-i{<5m80yaY-rFev~qJYriX6j!DaXSDfOLQIxv@Pz^Mjh+5u)2F^6u z7253}hW=W9VaQ<8amXMOfydQOA4-I?m;SCSPpp1%wN^=tNRZh+qwqKq>@O*FUv2)Z_)=7;4NTHaIJs8i|m zxJ-ycXAaiNJ^M`|6X*Stl}Mb9Y_wvDJj~`Hx5)k2JR~?1JLA2R2p!OqHg#C*umbwn zRPHr8)rG%;MsgIQ3gfXXrgQ}3Uz811!3vE~fgsT%=(-R5lOiSe8HMY!ycYctb}`|C zKIE1EuMt~ByM;FOh;}`K8yKM{Fg^kZoI3n@>CWQXvxX1j{YVK9jvcm7Z$agmO4yRF z578nv^lH!xdpS8UQA_R5&Sh(=Dgwul9&`n85prq#f_~LB-(MCMh`J!%KUuQWBY zGxR59AB|5HF$uGbsA|<5pA+7?$k73>E}n3#Z4btRyKaX+3byVBfT6mTsVtZ>NHqRP zs)(;J@QLr*UcHlL6a?3LNeB0Vs1i3z$&ky89j%rPrd#!dMxA;LncRl5;bay1r!GXELbZL2amBT=aR5%=-?uojH#od= zOGmBM712RLl+X%WqbL!tbgjWnIC{~58H_ao1kh62@N7xtS4JU&?-R!4^TRTUoaC{% z=5N(<^+i2AJmk=^u+cSDcxKlK?q?Cz;-&UFvkTlqdWzIVwN@)t=9Yh%UoS}7tpbsR ztwY3;V#(=B6g7TuT9peHRRZ76fKzeY3 z-Xhz74(&X<4tQ$9DMN6+MG(e2_?ux!s>9^`;tWZ(%=mr)2P5l^&uax7=K^>N!8hAz zw<>L^M3J4$qi`bIlp|t?v66%=6p6)`(pX`n932E4B@VpgfivS@io!DeUwpkyATf@A@$xz@k% z+VD!%M(B**i-)1MKe;g7w4bJDnb%QZIO!8#Y-&m)2+h(m@z$XMD5-=kdUD`5y@)vq z4fPc5GXf}Ndmg!zvOV=@^I#xlrGnuG4X@}6cWMjncC#soCPDxFS&fx^=~plT-PCO~ zFKudx?ES>Vh0E7*CS;P9)xN+J zd3V?cDGxLZ*Fw${-Zf&K^khAXTev?kDLp59kb&F29*Y=3$sKzPyQ2zn?LQsoW|-Vw zwJ!<;Jhnx{5y0=gP^O}7!%)xC?~PaEvX%O4)~gLOHN;gss6;6nVG_kTJ+{9lOp-*O)Nobw=`?}vOo*mqI%?IH6b|B1`%JkU zXTW?-u~9;x#y+~JL70@k$}*Ob*TbmwaZ82AL7?$$p7e!PI_0E-m}h z6RAC3NaT1bl5^+>b9G$g2K!sj!{q0o>eKd1`}5mY&kG^>Nj2L0?KI0T&BfHo7W(y- z2l3JUptMFYOn#)`;$nSV#@T>c(E-L)Xj;)SX#>hhf`U4F3jO?-ZNVxnp7;VHhVS+b z$(eeVjM0S`S9Gr7lIS)E-ID^oHPuaz0-oWGG5Are3hk$Sot=+AhedoZ-5&M9O^+gn ztbMD$L>V#&0GZDtd1oNWXSOk~o*-rKoEoYYTl1DIwEVU5g!_jJyFJywm(hRR$0Coe zzCZV#KaH7?;Q<1b$0#V9^oR57TE656-})2chQ$$oWy<&oNWtm@a7>ZbVVd46NbP=& zeY>)kq7SPY&)Cj&22)K6-BF(VpE!%}lH)-~7y8vB#)DsJG}ly952T0DhFLef|Az z^A6-;k7nIp$;myJ(my8l)t7$c1RmKYe>vr<=P|WfL*Rm;Cgr2I18C$<7t3Y#SKVIz zt9sllx_(D6-74@V6uq+bpRX-sq_AJU{xe3hHB3^b>Z)Y|7@eD!a4v^3_dW6fIiZmD zfSM6q=J20&lIPpivF{)0r1l;JZz~$Y7cIT@s81jbK zbM6dhs&|4$^+XsJSNeIGvf-tJ<&gg$7|IbMNg)n6M5~-0F(P<6;zYwu*=qaFxx1gEH{F#Am_m$*wz z=mz|Rv;2O9Q+rs)P|#iG-J)2|7=5G3^m6KQdnVuMeV}||Y+{q=xbkd3$;f{-C9pk8 z`_E>0OVZ=+%^!zUt|f37)7|@(ZQ0VLCwH`3s5X5|0bKgJ;Y>PX{~9P$4tOG>>VkSJ zERs!FoqG*Ty2<|Ti88t3gb*$M^Dt57U2dq0^&8DLe9#O(77u0}l^YCZ5iv?=Dx7#M zubcaO)zbBgfph=z8Czz+5z^}YF#~1px@XJyFUGOR-DnX9V}2OEEfFLVg7lR-gXD

    72(C71Sw3~UF7&R`t>H@17tOC{8+r@!OUAEuALGN__E3SA zMf`s|vi?$s29ORL*%;M;3SlN=fsGc3>M%&B>xn0br9pYLGrfbCZ6ZfP*TSFz9J;ma?M>eJSuQoPvIvto+*)`PYdJgxyzi$c9^As;uQo zU0jG3gTwX_ds;k@n%PWt9#LAocd!$!5T+ncAQ8}K63%add(@cAPWwlcESFnbo_ju| z?~ltzcgr{KwY}YRYur)evp#~ESwH?B@^(CHCI}tl2RQ$THxZDSHf~*`qhNwKtmTmg zSg~sSuWgGvG*1gA&*mS0MMbR2PT&0;Z*|Clz>(bJvTw7aqjk3}7U6Ip8e;Cq?-AAL zmM3ola2!Z{-2fh=_^6TJXu^ihNasB$^6P}%ICxtK zlCL*bHS-y1Vx=Cu9|dI}S*A_W9>=GRy5K_$3HY>yahg!8LHB1c46UF5- zhttF;5EzKQ_wELVs2q3ii54^nFsF9F=rk!xoYcuN0M|q(h{_hbE6v!6r9-})} zPSc$lIx)+#&TxC&#VOzAhDb0Mp3@e;;PUVvr<#1;BN((m8j2H?|Dir+9=cS(>36g& zddBFd5)t+=UYPf=#%=21P{x?Yv-jfVx}s`yc=Pe#L7J74))97d!^+ySLw268>XCTu ziXt5dm_$#Ej&)d+&VKBD@X>Pb$aM7Z&P}2Jt8B39Iuev{@J@jKPdqj#^qG|VN)*}R z

    72(C71Sw3~UF7&R`t>H@17tOC{8+r@!OUAEuALGN__E3SA zMf`s|vi?$s29ORL*%;M;3SlN=fsGc3>M%&B>xn0br9pYLGrfbCZ6ZfP*TSFz9J;ma?M>eJSuQoPvIvto+*)`PYdJgxyzi$c9^As;uQo zU0jG3gTwX_ds;k@n%PWt9#LAocd!$!5T+ncAQ8}K63%add(@cAPWwlcESFnbo_ju| z?~ltzcgr{KwY}YRYur)evp#~ESwH?B@^(CHCI}tl2RQ$THxZDSHf~*`qhNwKtmTmg zSg~sSuWgGvG*1gA&*mS0MMbR2PT&0;Z*|Clz>(bJvTw7aqjk3}7U6Ip8e;Cq?-AAL zmM3ola2!Z{-2fh=_^6TJXu^ihNasB$^6P}%ICxtK zlCL*bHS-y1Vx=Cu9|dI}S*A_W9>=GRy5K_$3HY>yahg!8LHB1c46UF5- zhttF;5EzKQ_wELVs2q3ii54^nFsF9F=rk!xoYcuN0M|q(h{_hbE6v!6r9-})} zPSc$lIx)+#&TxC&#VOzAhDb0Mp3@e;;PUVvr<#1;BN((m8j2H?|Dir+9=cS(>36g& zddBFd5)t+=UYPf=#%=21P{x?Yv-jfVx}s`yc=Pe#L7J74))97d!^+ySLw268>XCTu ziXt5dm_$#Ej&)d+&VKBD@X>Pb$aM7Z&P}2Jt8B39Iuev{@J@jKPdqj#^qG|VN)*}R z

    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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 daacb85ae376f30551702e9ba0ef7bf56fb981a7..76d846c4a66adbeedbed97062f890cbc5832570e 100644 GIT binary patch delta 1213 zcmV;u1Va0|6vhdV8Gi!+005o0f$RVP0eVnOR7C&)003m+0A=I}K$QSgzY0N^0At_) zRKEaY;R+To0B`&Oas2^f;{a5@093yKRl){ClmKe+0BZ06Wa9;F?*L@s0At<(P_zLt zZwo(+4l{HKENKriZV)bJ3Kuj0Y3>7T^8j4Y0AbnzVAud()PDdya7q2 z0!ya?H+}?4q!BrQ5;}GVBvlJLga8&X0CE5S|Ns5|{|9yd?DhYd#{VLO{{(aYSLp{|$Nn_xk_n^8eQ8|G3!yn8yE=#Q%uB|9Q3lZm$1g zr~g-?{~Lh+5_|vA<^Rp&|HIz@z})|-&;NnB|9!XrQJ()un*TtQ|K9EY+Uozu;QzJO z|E$sfkiq|rzyDjL|4p3#DvAH__W#-H|IXw8yV?J;)PMh<%KvAo|2L2SFN^;<7fqu8 z002XDQchC<%HA`+GsC&Y2Kx2aySA)E?dsgu$-sqVFBTIJ3k2oe#JaVup_-J5dva+@ zH7zVC9@5dSuA`BEd3J7XXGuacDJVOE^`ig)0=P*;K~z}7?UiR!+dvRNMI1UV^ll&t zEr9^(y??Ecd+)trY)lCR(v$xlTX(+Eog|+%lg#7;Z^li#`*gRKR>C#^A0K8mRv%j| z{r&xB^GyY2bMuhJ@@REqW=FqzaApL$VEomfcKr@6=QHt6tN!gUbj|P^b^U#+o@>}v z?SBFcvCQ`29vI{%LQ>a;7N36cwxOiy~P;N^J9u z+QtAS=u?LLA|VTq5eW%pFDEI%fFg24&L6u707TAX#BiZV>!AcaKq$8JPxAPL?tHx6 zhkr&Ff+S8>_RO8etBSk10vG-Rd_a-KhCEkrjuNYMJ-HkzJb~LG=Q)b3AQD4eC~yP~ zbfQC0QA3d&M{tPF<5CM;NRZ?R&g85wpn*pY9dHCEa@Kb(!GRn)<_J&^Lt285sJ;JG zKsK7t--4GTD5EU>CI&fzswkuXH-@wYlYh)C$N+(fWElZF4}RkbsG6aY7e%f(0*I5F zktU6fmw+e8;?YrOraZo#f9eSOR7;KKl=T^0e=KnYP$wg_L8fG|J<=|LFMzPv##@sB z0%)~mO-^)ktC=-BN0z4<}m bn!n8-#h{tR4FTb;00000NkvXXu0mjfuMswj_X zr$rndg4F>AS}RCFNhAu05K2KvLf)HAb~pRFx98+0BqZnTp1UM$net_JcIQ0*^WFda z=l}n6W5WMAOtBUq*0@7JtObZQ?hp`b0b-5l2}mC?5Se4fB4t2-I5IO~j!%TyVI?(k z^QQ%JyKkxa!-CfQp-|g>xBONS^!N~HsE7MnIh@7kQF`ncDn2{Y+EpCJ2r$Q+mEnu# zhlV}97)iYb8=kEz;kr^RpWd_f!W$(i@+UKMiu%}Lqy@Y&J^$g|!#oN|1~Belnt z?TgetA^|C*p3JQ!i`Q<0#kW9(6z9x)X;(MZIidQK)dk_&h9w}b`(U&8zSsYHJ1xEi znxxoWqw}{VeEo55eOW;xuU%LIY~RU$qDD^q-`w*82$&rG%%fK<#NO#Ec_##yfaL5E zQfbL1kh_cU3)Gvm;* z#*8Ch1_+St@Huh@-t5HG-TUE4;1`pC-0U0co*E0h)O<+I&jUvE)PE1j)ayzJIcp9B z>y82*VYh}2uRVL9Y)*k1sO?b4B*2`IfuPBfbu*I=CA~M#1xEK`*Qd2PSGqeo*7ryP zUYi9ho&Y?v8+i9r^s=ZXTb9M13ZEze7K=j)O6J69C!9YTSpP6NMa=v*);2SnWPUqJ z#IAY3TgP=G==L!RO|d1)7CTkQ4o3l#06K>znXSgB0)G7fuxtuZe_puL3}2A_n*}=7 zlivdR5L@y6&16dij7lY26lQllg(MyGjUBCvd_SlhGZt-niLVtu~bpJ>D5 zz^qLM4WtBx{4pO1lK@#$DD8$K#c6|p-_3~J?pi&tjf5#1kF}PqJwj}PLpPY|5q!?6 z#5BJ)6L|hTBho=~*c3D{DwvD(Ho9paVFJ3v0b3s>#D&?*#g$}x><4y!-tpQ4bB%x6t5B{#eg`8+iXq;KK`|5)#|tXYVuSB2BVYGLl=!pAKXs@ax|sPkYI` zKx2fxPKnt;2--s|{t7bY(ur(aB9pRpv&qr=I7 z`3HfQBL<~6B0z|E@qM6__yVgRWRpNOWX^El?UQ0-DjbW(BtTUoJgPQWJf8RsoWXY# z8DD%`tU01PM)U+=;puVW1n|HlAkq^(KN3r%}pvo;cP#Dzv^s!b<>sUu^!DJF!Y-M`+H$vl_ zn{3M?)0WyaqUU^qQ^u+Q8*xQ=p2ZV$)fJS90jPkTD z$RT&*Xq($7IDlqg3s|joh-h(ny&kW($Dj;Bp$wp38oRu#UaY@`I4!JQ#Eu4OP$RGf zXn-@aXUFoKWZgx+SS==8B=k;Y6HRem(BK9*&MlLGfWIks5Rrm{E5O_l?7Bxu`74LS zgo}hna`|;9u855xDF%)~vxz=`&cWFR2ha2C77#yu)koC$hzZDpjY#;+_%Je@6C1~2 zHINSosbW%pUJ86w4h&@Md8)E6KLGsd12F-4_{|ic7t7KTd_m?vE4T#=Al=KN@4^c&oscX%v ze{WrY-K?k6X>K7$z_;H*=6dgpuy{={ln>CfrtGGnH;xwU)FZPoy~DSo!OJr8gd6<8 z%75x*OC|s>-Y+)K(fQW^5wI(o<~bs6D8o)Pf;`p5yV$ zWRABYCs#W{Q-ekvA+NJSgBmh?g4YmVbK(ZzZDRPQ4+`Jb-bN)w0veiMoVPv7i&9l1 zuAR!%XpSF1+J^irzab)Mj(;Z6gy)D_|8^>MK-lft>Q_UUfTqSxPMN>y`#xGos?Y87 zHi#3T1>Kqy@>Cgr?XbB&>G*r%1-?_H@9lZ_0`cvLC~cF-WX1Y4pSbZ~x}B)K+5AGl zU1gR$%m9zQ1?sAx1Y8#lZ?$%ycl`85_m{*QoH-bn+#eX;1E?Un_2)M4ZvBxgbV6V1 zfa#9Ye&XI9_^NY`ISScVd(q=^8qt6{D9%eKf;~D%0IK3NvALa-!>32%__dug7YkO; zwE|CLIkS$N{*_A$iqLm<32Z5yL_>_=cU5Xvj&m&tmjK0irdFyvxl_xTw4`e!yETw1 zigwG+Q{2o1mjLgz6QP8v!Z%!L!{@-3dQ-2<#0;F>co0ij?cLdE-v zO>x=FgZ<{LA-<%gYozQV5I;t~yf5Egdg!cIWSXEsECvmc*;)sFC3_-t2rs` z#2R`L%2Rb(BcBReQuaUM5_6A8$vxsj=JW)~8W(4>*{wlURiMNZ2IS_;6^Z!#At)pm zm*pUWA*Jb^f)Hs&*C5gSyQCqc`C(RU#P9}*nUNrC5~sfmi|nOcLt4=5mxEqUpsu9e z;wx`xtT^R&y004MGpc`@YOE^->PiE+0(uABDfhslK^{w~BP%)|cm8cI)&j&DcL<2J c0I|k@0jWP;aJP^TNB{r;07*qoM6N<$f*9osHUIzs From 1d00e321288d05f672ec480323960c5fc5480f12 Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Tue, 24 Jan 2023 03:04:45 -0700 Subject: [PATCH 1607/2015] i686 doesnt work --- .github/workflows/flutter-nightly.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 6000552c7..1f4fcf2ef 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -30,7 +30,7 @@ jobs: fail-fast: false matrix: job: - - { target: i686-pc-windows-msvc , os: windows-2019 } + # - { target: i686-pc-windows-msvc , os: windows-2019 } # - { target: x86_64-pc-windows-gnu , os: windows-2019 } - { target: x86_64-pc-windows-msvc, os: windows-2019 } steps: From 4ee0891030b4f73b94df9bac76e8de0aad53b8b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C4=83lina-Ioana=20Popa?= Date: Tue, 24 Jan 2023 16:59:31 +0100 Subject: [PATCH 1608/2015] add romanian language --- src/lang.rs | 3 + src/lang/ro.rs | 401 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 404 insertions(+) create mode 100644 src/lang/ro.rs diff --git a/src/lang.rs b/src/lang.rs index 6254e988a..9e6ea1b5b 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -16,6 +16,7 @@ mod ja; mod ko; mod pl; mod ptbr; +mod ro; mod ru; mod sk; mod tr; @@ -57,6 +58,7 @@ lazy_static::lazy_static! { ("ca", "Català"), ("gr", "Ελληνικά"), ("sv", "Svenska"), + ("ro", "Română"), ]); } @@ -111,6 +113,7 @@ pub fn translate_locale(name: String, locale: &str) -> String { "ca" => ca::T.deref(), "gr" => gr::T.deref(), "sv" => sv::T.deref(), + "ro" => ro::T.deref(), _ => en::T.deref(), }; if let Some(v) = m.get(&name as &str) { diff --git a/src/lang/ro.rs b/src/lang/ro.rs new file mode 100644 index 000000000..c5a9b529c --- /dev/null +++ b/src/lang/ro.rs @@ -0,0 +1,401 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "Stare"), + ("Your Desktop", "Desktopul tău"), + ("desk_tip", "Desktopul tău poate fi accesat folosind ID-ul și parola de mai jos."), + ("Password", "Parola"), + ("Ready", "Pregătit"), + ("Established", "Stabilit"), + ("connecting_status", "În curs de conectare la rețeaua RustDesk..."), + ("Enable Service", "Activează serviciu"), + ("Start Service", "Pornește serviciu"), + ("Service is running", "Serviciul este în curs de executare..."), + ("Service is not running", "Serviciul nu funcționează"), + ("not_ready_status", "Nepregătit. Verifică conexiunea la rețea."), + ("Control Remote Desktop", "Controlează desktop-ul la distanță"), + ("Transfer File", "Transferă fișier"), + ("Connect", "Conectează-te"), + ("Recent Sessions", "Sesiuni recente"), + ("Address Book", "Agendă"), + ("Confirmation", "Confirmare"), + ("TCP Tunneling", "Tunel TCP"), + ("Remove", "Elimină"), + ("Refresh random password", "Actualizează parolă aleatorie"), + ("Set your own password", "Setează propria parolă"), + ("Enable Keyboard/Mouse", "Activează control tastatură/mouse"), + ("Enable Clipboard", "Activează clipboard"), + ("Enable File Transfer", "Activează transfer fișiere"), + ("Enable TCP Tunneling", "Activează tunel TCP"), + ("IP Whitelisting", "Listă de IP-uri autorizate"), + ("ID/Relay Server", "Server de ID/retransmisie"), + ("Import Server Config", "Importă configurație server"), + ("Export Server Config", "Exportă configurație server"), + ("Import server configuration successfully", "Configurație server importată cu succes"), + ("Export server configuration successfully", "Configurație server exportată cu succes"), + ("Invalid server configuration", "Configurație server nevalidă"), + ("Clipboard is empty", "Clipboard gol"), + ("Stop service", "Oprește serviciu"), + ("Change ID", "Schimbă ID"), + ("Website", "Site web"), + ("About", "Despre"), + ("Mute", "Fără sunet"), + ("Audio Input", "Intrare audio"), + ("Enhancements", "Îmbunătățiri"), + ("Hardware Codec", "Codec hardware"), + ("Adaptive Bitrate", "Rată de biți adaptabilă"), + ("ID Server", "Server de ID"), + ("Relay Server", "Server de retransmisie"), + ("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"), + ("Too frequent", "Modificat prea frecvent"), + ("Cancel", "Anulează"), + ("Skip", "Omite"), + ("Close", "Închide"), + ("Retry", "Reîncearcă"), + ("OK", "OK"), + ("Password Required", "Parolă necesară"), + ("Please enter your password", "Introdu parola"), + ("Remember password", "Memorează parola"), + ("Wrong Password", "Parolă incorectă"), + ("Do you want to enter again?", "Vrei să intri din nou?"), + ("Connection Error", "Eroare de conexiune"), + ("Error", "Eroare"), + ("Reset by the peer", "Conexiunea a fost închisă de dispozitivul pereche"), + ("Connecting...", "Conectare..."), + ("Connection in progress. Please wait.", "Conectare în curs. Te rugăm așteaptă."), + ("Please try 1 minute later", "Reîncearcă într-un minut"), + ("Login Error", "Eroare de autentificare"), + ("Successful", "Succes"), + ("Connected, waiting for image...", "Conectat, se așteaptă transmiterea imaginii..."), + ("Name", "Denumire"), + ("Type", "Tip"), + ("Modified", "Modificat"), + ("Size", "Dimensiune"), + ("Show Hidden Files", "Afișează fișiere ascunse"), + ("Receive", "Acceptă"), + ("Send", "Trimite"), + ("Refresh File", "Actualizează fișier"), + ("Local", "Local"), + ("Remote", "La distanță"), + ("Remote Computer", "Computer la distanță"), + ("Local Computer", "Computer local"), + ("Confirm Delete", "Confirmă ștergerea"), + ("Delete", "Șterge"), + ("Properties", "Caracteristici"), + ("Multi Select", "Alegere multiplă"), + ("Select All", "Selectează tot"), + ("Unselect All", "Deselectează tot"), + ("Empty Directory", "Director gol"), + ("Not an empty directory", "Directorul nu este gol"), + ("Are you sure you want to delete this file?", "Sigur vrei să ștergi acest fișier?"), + ("Are you sure you want to delete this empty directory?", "Sigur vrei să ștergi acest director gol?"), + ("Are you sure you want to delete the file of this directory?", "Sigur vrei să ștergi fișierul din acest director?"), + ("Do this for all conflicts", "Aplică pentru toate conflictele"), + ("This is irreversible!", "Această acțiune este ireversibilă!"), + ("Deleting", "În curs de ștergere..."), + ("files", "fișier"), + ("Waiting", "În așteptare..."), + ("Finished", "Finalizat"), + ("Speed", "Viteză"), + ("Custom Image Quality", "Setează calitatea imaginii"), + ("Privacy mode", "Mod privat"), + ("Block user input", "Blochează utilizator"), + ("Unblock user input", "Deblochează utilizator"), + ("Adjust Window", "Ajustează fereastra"), + ("Original", "Dimensiune originală"), + ("Shrink", "Micșorează"), + ("Stretch", "Extinde"), + ("Scrollbar", "Bară de derulare"), + ("ScrollAuto", "Derulare automată"), + ("Good image quality", "Calitate bună a imaginii"), + ("Balanced", "Calitate normală a imaginii"), + ("Optimize reaction time", "Optimizează timpul de reacție"), + ("Custom", "Personalizare"), + ("Show remote cursor", "Afișează cursor la distanță"), + ("Show quality monitor", "Afișează indicator de calitate"), + ("Disable clipboard", "Dezactivează clipboard"), + ("Lock after session end", "Blochează după deconectare"), + ("Insert", "Introdu"), + ("Insert Lock", "Blochează computer"), + ("Refresh", "Reîmprospătează"), + ("ID does not exist", "ID neexistent"), + ("Failed to connect to rendezvous server", "Conectare la server rendezvous eșuată"), + ("Please try later", "Încearcă mai târziu"), + ("Remote desktop is offline", "Desktopul la distanță este offline"), + ("Key mismatch", "Nepotrivire chei"), + ("Timeout", "Conexiune expirată"), + ("Failed to connect to relay server", "Conectare la server de retransmisie eșuată"), + ("Failed to connect via rendezvous server", "Conectare prin intermediul serverului rendezvous eșuată"), + ("Failed to connect via relay server", "Conectare prin intermediul serverului de retransmisie eșuată"), + ("Failed to make direct connection to remote desktop", "Imposibil de stabilit o conexiune directă cu desktopul la distanță"), + ("Set Password", "Setează parola"), + ("OS Password", "Parolă OS"), + ("install_tip", "Din cauza restricțiilor UAC, e posibil ca RustDesk să nu funcționeze corespunzător. Pentru a evita acest lucru, fă clic pe butonul de mai jos pentru a instala RustDesk."), + ("Click to upgrade", "Fă clic pentru a face upgrade"), + ("Click to download", "Fă clic pentru a descărca"), + ("Click to update", "Fă clic pentru a actualiza"), + ("Configure", "Configurează"), + ("config_acc", "Pentru a controla desktopul la distanță, trebuie să permiți RustDesk acces la setările de Accesibilitate."), + ("config_screen", "Pentru a controla desktopul la distanță, trebuie să permiți RustDesk acces la setările de Înregistrare ecran."), + ("Installing ...", "Instalare în curs..."), + ("Install", "Instalează"), + ("Installation", "Instalare"), + ("Installation Path", "Cale de instalare"), + ("Create start menu shortcuts", "Creează comenzi rapide în meniul Start"), + ("Create desktop icon", "Creează pictogramă pe desktop"), + ("agreement_tip", "Începerea procesului de instalare înseamnă acceptarea acordului de licență."), + ("Accept and Install", "Acceptă și instalează"), + ("End-user license agreement", "Acord de licență pentru utilizatorul final"), + ("Generating ...", "Se generează..."), + ("Your installation is lower version.", "Versiunea instalată este una inferioară."), + ("not_close_tcp_tip", "Nu închide această fereastră în timp ce folosești tunelul"), + ("Listening ...", "În așteptarea conexiunii tunel..."), + ("Remote Host", "Gazdă la distanță"), + ("Remote Port", "Port la distanță"), + ("Action", "Acțiune"), + ("Add", "Adaugă"), + ("Local Port", "Port local"), + ("Local Address", "Adresă locală"), + ("Change Local Port", "Schimbă port local"), + ("setup_server_tip", "Pentru o conexiune mai rapidă, îți poți configura propriul server."), + ("Too short, at least 6 characters.", "Prea scurt; trebuie cel puțin 6 caractere."), + ("The confirmation is not identical.", "Cele două intrări nu corespund."), + ("Permissions", "Permisiuni"), + ("Accept", "Acceptă"), + ("Dismiss", "Respinge"), + ("Disconnect", "Deconectează-te"), + ("Allow using keyboard and mouse", "Permite utilizarea tastaturii și mouse-ului"), + ("Allow using clipboard", "Permite utilizarea clipboardului"), + ("Allow hearing sound", "Permite auzirea sunetului"), + ("Allow file copy and paste", "Permite copierea/lipirea fișierelor"), + ("Connected", "Conectat"), + ("Direct and encrypted connection", "Conexiune directă criptată"), + ("Relayed and encrypted connection", "Conexiune retransmisă criptată"), + ("Direct and unencrypted connection", "Conexiune directă necriptată"), + ("Relayed and unencrypted connection", "Conexiune retransmisă necriptată"), + ("Enter Remote ID", "Introdu ID-ul dispozitivului la distanță"), + ("Enter your password", "Introdu parola"), + ("Logging in...", "Se conectează..."), + ("Enable RDP session sharing", "Activează partajarea sesiunii RDP"), + ("Auto Login", "Conectare automată (valid doar dacă funcția de Blocare după deconectare este activă)"), + ("Enable Direct IP Access", "Activează accesul direct cu IP"), + ("Rename", "Redenumește"), + ("Space", "Spațiu"), + ("Create Desktop Shortcut", "Creează comandă rapidă de desktop"), + ("Change Path", "Schimbă calea"), + ("Create Folder", "Creează folder"), + ("Please enter the folder name", "Introdu numele folderului"), + ("Fix it", "Repară"), + ("Warning", "Avertisment"), + ("Login screen using Wayland is not supported", "Ecranele de conectare care folosesc Wayland nu sunt acceptate"), + ("Reboot required", "Repornire necesară"), + ("Unsupported display server ", "Tipul de server de afișaj nu este acceptat"), + ("x11 expected", "E necesar X11"), + ("Port", "Port"), + ("Settings", "Setări"), + ("Username", " Nume de utilizator"), + ("Invalid port", "Port nevalid"), + ("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"), + ("Always connect via relay", "Se conectează mereu prin retransmisie"), + ("whitelist_tip", "Doar adresele IP autorizate pot accesa acest dispozitiv"), + ("Login", "Conectare"), + ("Logout", "Deconectare"), + ("Tags", "Etichetare"), + ("Search ID", "Caută după ID"), + ("Current Wayland display server is not supported", "Serverul de afișaj Wayland nu este acceptat"), + ("whitelist_sep", "Poți folosi ca separator virgula, punctul și virgula, spațiul sau linia nouă"), + ("Add ID", "Adaugă ID"), + ("Add Tag", "Adaugă etichetă"), + ("Unselect all tags", "Deselectează toate etichetele"), + ("Network error", "Eroare de rețea"), + ("Username missed", "Lipsește numele de utilizator"), + ("Password missed", "Lipsește parola"), + ("Wrong credentials", "Nume sau parolă greșită"), + ("Edit Tag", "Modifică etichetă"), + ("Unremember Password", "Uită parola"), + ("Favorites", "Favorite"), + ("Add to Favorites", "Adaugă la Favorite"), + ("Remove from Favorites", "Șterge din Favorite"), + ("Empty", "Gol"), + ("Invalid folder name", "Denumire folder nevalidă"), + ("Socks5 Proxy", "Proxy Socks5"), + ("Hostname", "Nume gazdă"), + ("Discovered", "Descoperite"), + ("install_daemon_tip", "Pentru executare la pornirea sistemului, instalează serviciul de sistem."), + ("Remote ID", "ID dispozitiv la distanță"), + ("Paste", "Lipește"), + ("Paste here?", "Lipește aici?"), + ("Are you sure to close the connection?", "Sigur vrei să închizi conexiunea?"), + ("Download new version", "Descarcă noua versiune"), + ("Touch mode", "Mod tactil"), + ("Mouse mode", "Mod mouse"), + ("One-Finger Tap", "Apasă cu un deget"), + ("Left Mouse", "Clic stânga"), + ("One-Long Tap", "Apasă lung"), + ("Two-Finger Tap", "Apasă cu două degete"), + ("Right Mouse", "Clic dreapta"), + ("One-Finger Move", "Mișcă cu un deget"), + ("Double Tap & Move", "Apasă dublu și mișcă"), + ("Mouse Drag", "Tragere mouse"), + ("Three-Finger vertically", "Trei degete vertical"), + ("Mouse Wheel", "Rotiță mouse"), + ("Two-Finger Move", "Mișcă cu două degete"), + ("Canvas Move", "Mută ecran"), + ("Pinch to Zoom", "Apropie degetele pentru zoom"), + ("Canvas Zoom", "Zoom ecran"), + ("Reset canvas", "Reinițializează ecranul"), + ("No permission of file transfer", "Nicio permisiune pentru transferul de fișiere"), + ("Note", "Reține"), + ("Connection", "Conexiune"), + ("Share Screen", "Partajează ecran"), + ("CLOSE", "ÎNCHIDE"), + ("OPEN", "DESCHIDE"), + ("Chat", "Discută"), + ("Total", "Total"), + ("items", "elemente"), + ("Selected", "Selectat"), + ("Screen Capture", "Captură ecran"), + ("Input Control", "Control intrări"), + ("Audio Capture", "Captură audio"), + ("File Connection", "Conexiune fișier"), + ("Screen Connection", "Conexiune ecran"), + ("Do you accept?", "Accepți?"), + ("Open System Setting", "Deschide setări sistem"), + ("How to get Android input permission?", "Cum autorizez dispozitive de intrare pe Android?"), + ("android_input_permission_tip1", "Pentru ca un dispozitiv la distanță să poată controla un dispozitiv Android folosind mouse-ul sau suportul tactil, trebuie să permiți RustDesk să utilize serviciul Accesibilitate."), + ("android_input_permission_tip2", "Accesează următoarea pagină din Setări, caută și deschide [Aplicații instalate] și activează serviciul [RustDesk Input]."), + ("android_new_connection_tip", "Ai primit o nouă solicitare de control pentru dispozitivul actual."), + ("android_service_will_start_tip", "Activarea setării Captură ecran va porni automat serviciul, permițând altor dispozitive să solicite conectarea la dispozitivul tău."), + ("android_stop_service_tip", "Închiderea serviciului va închide automat toate conexiunile stabilite."), + ("android_version_audio_tip", "Versiunea actuală de Android nu suportă captura audio. Fă upgrade la Android 10 sau la o versiune superioară."), + ("android_start_service_tip", "Apasă [Pornește serviciu] sau DESCHIDE [Captură ecran] pentru a porni serviciul de partajare a ecranului."), + ("Account", "Cont"), + ("Overwrite", "Suprascrie"), + ("This file exists, skip or overwrite this file?", "Fișier deja existent. Omite sau suprascrie?"), + ("Quit", "Ieși"), + ("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"), + ("Help", "Ajutor"), + ("Failed", "Nereușit"), + ("Succeeded", "Reușit"), + ("Someone turns on privacy mode, exit", "Cineva activează modul privat, ieși din"), + ("Unsupported", "Neacceptat"), + ("Peer denied", "Dispozitiv pereche refuzat"), + ("Please install plugins", "Instalează pluginuri"), + ("Peer exit", "Ieșire dispozitiv pereche"), + ("Failed to turn off", "Dezactivare nereușită"), + ("Turned off", "Închis"), + ("In privacy mode", "În modul privat"), + ("Out privacy mode", "Ieșit din modul privat"), + ("Language", "Limbă"), + ("Keep RustDesk background service", "Rulează serviciul RustDesk în fundal"), + ("Ignore Battery Optimizations", "Ignoră optimizările de baterie"), + ("android_open_battery_optimizations_tip", "Pentru dezactivarea acestei funcții, accesează setările aplicației RustDesk, deschide secțiunea [Baterie] și deselectează [Fără restricții]."), + ("Connection not allowed", "Conexiune neautoriztă"), + ("Legacy mode", "Mod legacy"), + ("Map mode", "Mod hartă"), + ("Translate mode", "Mod traducere"), + ("Use permanent password", "Folosește parola permanentă"), + ("Use both passwords", "Folosește parola unică și cea permanentă"), + ("Set permanent password", "Setează parola permanentă"), + ("Enable Remote Restart", "Activează repornirea la distanță"), + ("Allow remote restart", "Permite repornirea la distanță"), + ("Restart Remote Device", "Repornește dispozivul la distanță"), + ("Are you sure you want to restart", "Sigur vrei să repornești dispozitivul?"), + ("Restarting Remote Device", "Se repornește dispozitivul la distanță"), + ("remote_restarting_tip", "Dispozitivul este în curs de repornire. Închide acest mesaj și reconectează-te cu parola permanentă după un timp."), + ("Copied", "Copiat"), + ("Exit Fullscreen", "Ieși din modul ecran complet"), + ("Fullscreen", "Ecran complet"), + ("Mobile Actions", "Acțiuni mobile"), + ("Select Monitor", "Selectează monitor"), + ("Control Actions", "Acțiuni de control"), + ("Display Settings", "Setări afișaj"), + ("Ratio", "Raport"), + ("Image Quality", "Calitate imagine"), + ("Scroll Style", "Stil de derulare"), + ("Show Menubar", "Arată bara de meniu"), + ("Hide Menubar", "Ascunde bara de meniu"), + ("Direct Connection", "Conexiune directă"), + ("Relay Connection", "Conexiune prin retransmisie"), + ("Secure Connection", "Conexiune securizată"), + ("Insecure Connection", "Conexiune nesecurizată"), + ("Scale original", "Scală originală"), + ("Scale adaptive", "Scală adaptivă"), + ("General", "General"), + ("Security", "Securitate"), + ("Account", "Cont"), + ("Theme", "Temă"), + ("Dark Theme", "Temă întunecată"), + ("Dark", "Întunecat"), + ("Light", "Luminos"), + ("Follow System", "Urmărește sistem"), + ("Enable hardware codec", "Activează codec hardware"), + ("Unlock Security Settings", "Deblochează setări de securitate"), + ("Enable Audio", "Activează audio"), + ("Unlock Network Settings", "Deblochează setări de rețea"), + ("Server", "Server"), + ("Direct IP Access", "Acces direct IP"), + ("Proxy", "Proxy"), + ("Port", "Port"), + ("Apply", "Aplică"), + ("Disconnect all devices?", "Vrei să deconectezi toate dispozitivele?"), + ("Clear", "Golește"), + ("Audio Input Device", "Dispozitiv de intrare audio"), + ("Deny remote access", "Interzice acces la distanță"), + ("Use IP Whitelisting", "Folosește lista de IP-uri autorizate"), + ("Network", "Rețea"), + ("Enable RDP", "Activează RDP"), + ("Pin menubar", "Fixează bara de meniu"), + ("Unpin menubar", "Detașează bara de meniu"), + ("Recording", "Înregistrare"), + ("Directory", "Director"), + ("Automatically record incoming sessions", "Înregistrează automat sesiunile viitoare"), + ("Change", "Modifică"), + ("Start session recording", "Începe înregistrare"), + ("Stop session recording", "Oprește înregistrare"), + ("Enable Recording Session", "Activează înregistrarea sesiunii"), + ("Allow recording session", "Permite înregistrarea sesiunii"), + ("Enable LAN Discovery", "Activează descoperire LAN"), + ("Deny LAN Discovery", "Interzice descoperire LAN"), + ("Write a message", "Scrie un mesaj"), + ("Prompt", "Solicită"), + ("Please wait for confirmation of UAC...", "Așteaptă confirmarea UAC..."), + ("elevated_foreground_window_tip", "Fereastra actuală a dispozitivului la distanță necesită privilegii sporite pentru a funcționa, astfel că mouse-ul și tastatura nu pot fi folosite. Poți cere utilizatorului la distanță să minimizeze fereastra actuală sau să facă clic pe butonul de sporire a privilegiilor din fereastra de gestionare a conexiunilor. Pentru a evita această problemă, recomandăm instalarea software-ului pe dispozitivul la distanță."), + ("Disconnected", "Deconectat"), + ("Other", "Altele"), + ("Confirm before closing multiple tabs", "Confirmă înainte de a închide mai multe file"), + ("Keyboard Settings", "Configurare tastatură"), + ("Custom", "Personalizat"), + ("Full Access", "Acces total"), + ("Screen Share", "Partajare ecran"), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland necesită Ubuntu 21.04 sau o versiune superioară."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland necesită o versiune superioară a distribuției Linux. Încearcă desktopul X11 sau schimbă sistemul de operare."), + ("JumpLink", "Afișează"), + ("Please Select the screen to be shared(Operate on the peer side).", "Partajează ecranul care urmează să fie partajat (operează din partea dispozitivului pereche)."), + ("Show RustDesk", "Afișează RustDesk"), + ("This PC", "Acest PC"), + ("or", "sau"), + ("Continue with", "Continuă cu"), + ("Elevate", "Sporește"), + ("Zoom cursor", "Cursor lupă"), + ("Accept sessions via password", "Acceptă sesiunile folosind parola"), + ("Accept sessions via click", "Acceptă sesiunile cu un clic de confirmare"), + ("Accept sessions via both", "Acceptă sesiunile folosind ambele moduri"), + ("Please wait for the remote side to accept your session request...", "Așteaptă ca solicitarea ta de conectare la distanță să fie acceptată..."), + ("One-time Password", "Parolă unică"), + ("Use one-time password", "Folosește parola unică"), + ("One-time password length", "Lungimea parolei unice"), + ("Request access to your device", "Solicită acces la dispozitivul tău"), + ("Hide connection management window", "Ascunde fereastra de gestionare a conexiunilor"), + ("hide_cm_tip", "Permite ascunderea ferestrei de gestionare doar dacă accepți începerea sesiunilor folosind parola permanentă"), + ].iter().cloned().collect(); +} From 917b3d22135c456a7a1f96dfdb0ab3a5f3ce5b2d Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 26 Jan 2023 11:05:23 +0800 Subject: [PATCH 1609/2015] fix issue #2937 --- 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 176b1ba2d..7703182cd 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -323,10 +323,10 @@ class ServerModel with ChangeNotifier { notifyListeners(); parent.target?.ffiModel.updateEventListener(""); await parent.target?.invokeMethod("init_service"); + // ugly is here, because for desktop, below is useless await bind.mainStartService(); updateClientState(); - if (!Platform.isLinux) { - // current linux is not supported + if (Platform.isAndroid) { Wakelock.enable(); } } From cb5855a2730e41ae3c5c899a0182bc2059248253 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 26 Jan 2023 11:25:05 +0800 Subject: [PATCH 1610/2015] fix issue #2921 --- src/ui_interface.rs | 2 +- src/ui_session_interface.rs | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/ui_interface.rs b/src/ui_interface.rs index ebaf8c317..403951eaa 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -236,7 +236,7 @@ pub fn set_peer_option(id: String, name: String, value: String) { #[inline] pub fn using_public_server() -> bool { - option_env!("RENDEZVOUS_SERVER").is_none() + option_env!("RENDEZVOUS_SERVER").unwrap_or("").is_empty() && crate::get_custom_rendezvous_server(get_option_("custom-rendezvous-server")).is_empty() } diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 3fb3f2621..48f6c1090 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -6,10 +6,11 @@ use crate::client::{ }; use crate::common::{self, GrabState}; use crate::keyboard; +use crate::ui_interface::using_public_server; use crate::{client::Data, client::Interface}; use async_trait::async_trait; use bytes::Bytes; -use hbb_common::config::{Config, LocalConfig, PeerConfig}; +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::*}; @@ -835,6 +836,9 @@ pub async fn io_loop(handler: Session) { if key.is_empty() { key = crate::platform::get_license_key(); } + if key.is_empty() && !option_env!("RENDEZVOUS_SERVER").unwrap_or("").is_empty() { + key = RS_PUB_KEY.to_owned(); + } #[cfg(not(any(target_os = "android", target_os = "ios")))] if handler.is_port_forward() { if handler.is_rdp() { From a957edf93a6391cb570e129430b71d1ab3f69a15 Mon Sep 17 00:00:00 2001 From: mehdi-song Date: Thu, 26 Jan 2023 11:47:06 +0330 Subject: [PATCH 1611/2015] Update fa.rs ;-) --- src/lang/fa.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/fa.rs b/src/lang/fa.rs index b107bb91a..da354579b 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -186,7 +186,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logging in...", "...در حال ورود"), ("Enable RDP session sharing", "اشتراک گذاری جلسه RDP را فعال کنید"), ("Auto Login", "ورود خودکار"), - ("Enable Direct IP Access", "دسترسی مستقیم IP را فعال کنید"), + ("Enable Direct IP Access", "را فعال کنید IP دسترسی مستقیم"), ("Rename", "تغییر نام"), ("Space", "فضا"), ("Create Desktop Shortcut", "ساخت میانبر روی دسکتاپ"), @@ -349,7 +349,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable Audio", "فعال شدن صدا"), ("Unlock Network Settings", "آنلاک شدن تنظیمات شبکه"), ("Server", "سرور"), - ("Direct IP Access", "IP دسترسی مستقیم "), + ("Direct IP Access", "IP دسترسی مستقیم به"), ("Proxy", "پروکسی"), ("Apply", "ثبت"), ("Disconnect all devices?", "همه دستگاه ها قطع شوند؟"), From 06a0aeb03be3c610e06ab1010d84c8f71ce21643 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Thu, 26 Jan 2023 22:57:49 +0800 Subject: [PATCH 1612/2015] opt: upgrade flutter to 3.7.0 --- .github/workflows/flutter-ci.yml | 14 ++++++-------- .github/workflows/flutter-nightly.yml | 10 +++++----- flutter/macos/Runner/MainFlutterWindow.swift | 2 +- flutter/pubspec.yaml | 2 +- 4 files changed, 13 insertions(+), 15 deletions(-) diff --git a/.github/workflows/flutter-ci.yml b/.github/workflows/flutter-ci.yml index 4e98f311d..bcdad4a29 100644 --- a/.github/workflows/flutter-ci.yml +++ b/.github/workflows/flutter-ci.yml @@ -13,8 +13,7 @@ on: env: LLVM_VERSION: "10.0" - # Note: currently 3.0.5 does not support arm64 officially, we use latest stable version first. - FLUTTER_VERSION: "3.0.5" + FLUTTER_VERSION: "3.7.0" # vcpkg version: 2022.05.10 # for multiarch gcc compatibility VCPKG_COMMIT_ID: "14e7bb4ae24616ec54ff6b2f6ef4e8659434ea44" @@ -51,9 +50,9 @@ jobs: run: | flutter doctor -v flutter precache --windows - Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.0.5-rustdesk.2/windows-x64-flutter-release.zip -OutFile windows-x64-flutter-release.zip + Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.7.0-rustdesk/windows-x64-release-flutter.zip -OutFile windows-x64-flutter-release.zip Expand-Archive windows-x64-flutter-release.zip -DestinationPath engine - mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-3.0.5-x64/bin/cache/artifacts/engine/windows-x64-release/ + mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-${{ env.FLUTTER_VERSION }}-x64/bin/cache/artifacts/engine/windows-x64-release/ - name: Install Rust toolchain uses: actions-rs/toolchain@v1 @@ -853,12 +852,12 @@ jobs: # disable git safe.directory git config --global --add safe.directory "*" pushd /opt - # clone repo and reset to flutter 3.0.5 + # clone repo and reset to flutter 3.7.0 git clone https://github.com/sony/flutter-elinux.git || true pushd flutter-elinux - # reset to flutter 3.0.5 + # reset to flutter 3.7.0 git fetch - git reset --hard b09a90eee643859ce4e676839227edd9fd3feba8 + git reset --hard 51a1d685901f79fbac51665a967c3a1a789ecee5 popd - uses: Kingtous/run-on-arch-action@amd64-support @@ -887,7 +886,6 @@ jobs: # Setup flutter-elinux export PATH=/opt/flutter-elinux/bin:$PATH flutter-elinux doctor -v - # edit to corresponding arch case ${{ matrix.job.arch }} in aarch64) sed -i "s/Architecture: amd64/Architecture: arm64/g" ./build.py diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index b33a6dba0..4cb547aa4 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -9,7 +9,7 @@ on: env: LLVM_VERSION: "10.0" # Note: currently 3.0.5 does not support arm64 officially, we use latest stable version first. - FLUTTER_VERSION: "3.0.5" + FLUTTER_VERSION: "3.7.0" TAG_NAME: "nightly" # vcpkg version: 2022.05.10 # for multiarch gcc compatibility @@ -53,9 +53,9 @@ jobs: run: | flutter doctor -v flutter precache --windows - Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.0.5-rustdesk.2/windows-x64-flutter-release.zip -OutFile windows-x64-flutter-release.zip + Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.7.0-rustdesk/windows-x64-release-flutter.zip -OutFile windows-x64-flutter-release.zip Expand-Archive windows-x64-flutter-release.zip -DestinationPath engine - mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-3.0.5-x64/bin/cache/artifacts/engine/windows-x64-release/ + mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-${{ env.FLUTTER_VERSION }}-x64/bin/cache/artifacts/engine/windows-x64-release/ - name: Install Rust toolchain uses: actions-rs/toolchain@v1 @@ -1002,9 +1002,9 @@ jobs: # clone repo and reset to flutter 3.0.5 git clone https://github.com/sony/flutter-elinux.git || true pushd flutter-elinux - # reset to flutter 3.0.5 + # reset to flutter 3.7.0 git fetch - git reset --hard b09a90eee643859ce4e676839227edd9fd3feba8 + git reset --hard 51a1d685901f79fbac51665a967c3a1a789ecee5 popd - uses: Kingtous/run-on-arch-action@amd64-support diff --git a/flutter/macos/Runner/MainFlutterWindow.swift b/flutter/macos/Runner/MainFlutterWindow.swift index 540cd9ab9..108f5a5f8 100644 --- a/flutter/macos/Runner/MainFlutterWindow.swift +++ b/flutter/macos/Runner/MainFlutterWindow.swift @@ -7,7 +7,7 @@ import desktop_drop import device_info_plus_macos import flutter_custom_cursor import package_info_plus_macos -import path_provider_macos +import path_provider_foundation import screen_retriever import sqflite // import tray_manager diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index a5535c8b7..c2a5f1c02 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -31,7 +31,7 @@ dependencies: # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.3 ffi: ^2.0.1 - path_provider: ^2.0.2 + path_provider: ^2.0.12 external_path: ^1.0.1 provider: ^6.0.3 tuple: ^2.0.0 From db9afbcb01e98d73b77a1bdb6637fdffadb4198f Mon Sep 17 00:00:00 2001 From: Kingtous Date: Thu, 26 Jan 2023 23:37:28 +0800 Subject: [PATCH 1613/2015] opt: do not show window when preparing --- flutter/lib/utils/multi_window_manager.dart | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/flutter/lib/utils/multi_window_manager.dart b/flutter/lib/utils/multi_window_manager.dart index ee19ac485..7914a4c0a 100644 --- a/flutter/lib/utils/multi_window_manager.dart +++ b/flutter/lib/utils/multi_window_manager.dart @@ -63,8 +63,7 @@ class RustDeskMultiWindowManager { ..setFrame(const Offset(0, 0) & const Size(1280, 720)) ..center() ..setTitle(getWindowNameWithId(remoteId, - overrideType: WindowType.RemoteDesktop)) - ..show(); + overrideType: WindowType.RemoteDesktop)); registerActiveWindow(remoteDesktopController.windowId); _remoteDesktopWindowId = remoteDesktopController.windowId; } else { @@ -90,8 +89,7 @@ class RustDeskMultiWindowManager { ..setFrame(const Offset(0, 0) & const Size(1280, 720)) ..center() ..setTitle(getWindowNameWithId(remoteId, - overrideType: WindowType.FileTransfer)) - ..show(); + overrideType: WindowType.FileTransfer)); registerActiveWindow(fileTransferController.windowId); _fileTransferWindowId = fileTransferController.windowId; } else { @@ -116,9 +114,8 @@ class RustDeskMultiWindowManager { portForwardController ..setFrame(const Offset(0, 0) & const Size(1280, 720)) ..center() - ..setTitle( - getWindowNameWithId(remoteId, overrideType: WindowType.PortForward)) - ..show(); + ..setTitle(getWindowNameWithId(remoteId, + overrideType: WindowType.PortForward)); registerActiveWindow(portForwardController.windowId); _portForwardWindowId = portForwardController.windowId; } else { From c9715c4e8748a9a9ac31a6516aea8eb268a4bdfe Mon Sep 17 00:00:00 2001 From: Kingtous Date: Thu, 26 Jan 2023 23:37:47 +0800 Subject: [PATCH 1614/2015] opt: remove unnecessary doc --- .github/workflows/flutter-nightly.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 4cb547aa4..151ff1213 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -8,7 +8,6 @@ on: env: LLVM_VERSION: "10.0" - # Note: currently 3.0.5 does not support arm64 officially, we use latest stable version first. FLUTTER_VERSION: "3.7.0" TAG_NAME: "nightly" # vcpkg version: 2022.05.10 @@ -999,7 +998,7 @@ jobs: # disable git safe.directory git config --global --add safe.directory "*" pushd /opt - # clone repo and reset to flutter 3.0.5 + # clone repo and reset to flutter 3.7.0 git clone https://github.com/sony/flutter-elinux.git || true pushd flutter-elinux # reset to flutter 3.7.0 From eac83fca28fb90cd3e4108854b2fa1bbb96c20d4 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 27 Jan 2023 02:00:38 +0800 Subject: [PATCH 1615/2015] fix: update scrolling to fit flutter 3.3+ --- flutter/pubspec.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index c2a5f1c02..0189ad9e4 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -75,14 +75,14 @@ dependencies: debounce_throttle: ^2.0.0 file_picker: ^5.1.0 flutter_svg: ^1.1.5 - flutter_improved_scrolling: ^0.0.3 + flutter_improved_scrolling: # currently, we use flutter 3.0.5 for windows build, latest for other builds. # # for flutter 3.0.5, please use official version(just comment code below). # if build rustdesk by flutter >=3.3, please use our custom pub below (uncomment code below). - # git: - # url: https://github.com/Kingtous/flutter_improved_scrolling - # ref: 62f09545149f320616467c306c8c5f71714a18e6 + git: + url: https://github.com/Kingtous/flutter_improved_scrolling + ref: 62f09545149f320616467c306c8c5f71714a18e6 uni_links: ^0.5.1 uni_links_desktop: ^0.1.4 path: ^1.8.1 From b144e28a60e7d7ba17434af1f7dcd436bda87b9a Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 27 Jan 2023 02:34:28 +0800 Subject: [PATCH 1616/2015] fix: arm64 build on flutter 3.7.0 https://github.com/flutter/flutter/issues/116703#issuecomment-1403956612 --- .github/workflows/flutter-ci.yml | 11 +++++++++-- .github/workflows/flutter-nightly.yml | 11 +++++++++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/.github/workflows/flutter-ci.yml b/.github/workflows/flutter-ci.yml index bcdad4a29..a2c67551d 100644 --- a/.github/workflows/flutter-ci.yml +++ b/.github/workflows/flutter-ci.yml @@ -882,10 +882,17 @@ jobs: git config --global --add safe.directory "*" pushd /workspace # we use flutter-elinux to build our rustdesk - sed -i "s/flutter build linux --release/flutter-elinux build linux/g" ./build.py - # Setup flutter-elinux export PATH=/opt/flutter-elinux/bin:$PATH + sed -i "s/flutter build linux --release/flutter-elinux build linux/g" ./build.py + # Setup flutter-elinux. Run doctor to check if issues here. flutter-elinux doctor -v + # Patch arm64 engine for flutter 3.6.0+ + flutter-elinux precache --linux + pushd /tmp + curl -O https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.7.0-stable.tar.xz + tar -xvf flutter_linux_3.7.0-stable.tar.xz flutter/bin/cache/artifacts/engine/linux-x64/shader_lib + cp -R flutter/bin/cache/artifacts/engine/linux-x64/shader_lib /opt/flutter-elinux/flutter/bin/cache/artifacts/engine/linux-arm64 + popd case ${{ matrix.job.arch }} in aarch64) sed -i "s/Architecture: amd64/Architecture: arm64/g" ./build.py diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 151ff1213..896afd005 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -1028,10 +1028,17 @@ jobs: git config --global --add safe.directory "*" pushd /workspace # we use flutter-elinux to build our rustdesk - sed -i "s/flutter build linux --release/flutter-elinux build linux/g" ./build.py - # Setup flutter-elinux export PATH=/opt/flutter-elinux/bin:$PATH + sed -i "s/flutter build linux --release/flutter-elinux build linux/g" ./build.py + # Setup flutter-elinux. Run doctor to check if issues here. flutter-elinux doctor -v + # Patch arm64 engine for flutter 3.6.0+ + flutter-elinux precache --linux + pushd /tmp + curl -O https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.7.0-stable.tar.xz + tar -xvf flutter_linux_3.7.0-stable.tar.xz flutter/bin/cache/artifacts/engine/linux-x64/shader_lib + cp -R flutter/bin/cache/artifacts/engine/linux-x64/shader_lib /opt/flutter-elinux/flutter/bin/cache/artifacts/engine/linux-arm64 + popd # edit to corresponding arch case ${{ matrix.job.arch }} in aarch64) From a0cc71c86dafb6b2ea113c3841ae4365dbf9e639 Mon Sep 17 00:00:00 2001 From: jimmyGALLAND <64364019+jimmyGALLAND@users.noreply.github.com> Date: Thu, 26 Jan 2023 22:08:33 +0100 Subject: [PATCH 1617/2015] Update fr.rs Some small fixes and improvements --- src/lang/fr.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/lang/fr.rs b/src/lang/fr.rs index ea2dbfede..9f1f23205 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -77,10 +77,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Connected, waiting for image...", "Connecté, en attente de transmission d'image..."), ("Name", "Nom"), ("Type", "Type"), - ("Modified", "Modifié"), + ("Modified", "Modifié le"), ("Size", "Taille"), ("Show Hidden Files", "Afficher les fichiers cachés"), - ("Receive", "Accepter"), + ("Receive", "Recevoir"), ("Send", "Envoyer"), ("Refresh File", "Actualiser le fichier"), ("Local", "Local"), @@ -90,7 +90,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Confirm Delete", "Confirmer la suppression"), ("Delete", "Supprimer"), ("Properties", "Propriétés"), - ("Multi Select", "Choix multiple"), + ("Multi Select", "Sélection multiple"), ("Select All", "Tout sélectionner"), ("Unselect All", "Tout déselectionner"), ("Empty Directory", "Répertoire vide"), @@ -208,7 +208,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Run without install", "Exécuter sans installer"), ("Always connected via relay", "Forcer la connexion relais"), ("Always connect via relay", "Forcer la connexion relais"), - ("whitelist_tip", "Seul l'IP dans la liste blanche peut accéder à mon appareil"), + ("whitelist_tip", "Seule une IP de la liste blanche peut accéder à mon appareil"), ("Login", "Connexion"), ("Verify", "Vérifier"), ("Remember me", "Se souvenir de moi"), @@ -269,7 +269,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Chat", "Discuter"), ("Total", "Total"), ("items", "éléments"), - ("Selected", "Choisi"), + ("Selected", "Sélectionné"), ("Screen Capture", "Capture d'écran"), ("Input Control", "Contrôle de saisie"), ("Audio Capture", "Capture audio"), @@ -303,7 +303,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("In privacy mode", "en mode privé"), ("Out privacy mode", "hors mode de confidentialité"), ("Language", "Langue"), - ("Keep RustDesk background service", "Gardez le service RustDesk service arrière plan"), + ("Keep RustDesk background service", "Gardez le service RustDesk en arrière plan"), ("Ignore Battery Optimizations", "Ignorer les optimisations batterie"), ("android_open_battery_optimizations_tip", "Conseil android d'optimisation de batterie"), ("Connection not allowed", "Connexion non autorisée"), @@ -356,14 +356,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clear", "Effacer"), ("Audio Input Device", "Périphérique source audio"), ("Deny remote access", "Interdir l'accès distant"), - ("Use IP Whitelisting", "Utiliser liste blanche d'IP"), + ("Use IP Whitelisting", "Utiliser une liste blanche d'IP"), ("Network", "Réseau"), ("Enable RDP", "Activer RDP"), ("Pin menubar", "Épingler la barre de menus"), ("Unpin menubar", "Détacher la barre de menu"), ("Recording", "Enregistrement"), ("Directory", "Répertoire"), - ("Automatically record incoming sessions", "Enregistrement automatique des session entrantes"), + ("Automatically record incoming sessions", "Enregistrement automatique des sessions entrantes"), ("Change", "Modifier"), ("Start session recording", "Commencer l'enregistrement"), ("Stop session recording", "Stopper l'enregistrement"), From a8c3499d7b932560492b1e0796abb1fb8ae86be7 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 27 Jan 2023 11:42:08 +0800 Subject: [PATCH 1618/2015] sync with rustdesk-server hbb_common --- libs/hbb_common/build.rs | 5 +- libs/hbb_common/protos/message.proto | 1248 +++++++++++----------- libs/hbb_common/src/bytes_codec.rs | 8 +- libs/hbb_common/src/compress.rs | 7 +- libs/hbb_common/src/config.rs | 128 +-- libs/hbb_common/src/fs.rs | 183 ++-- libs/hbb_common/src/lib.rs | 144 +-- libs/hbb_common/src/password_security.rs | 6 +- libs/hbb_common/src/platform/linux.rs | 8 +- libs/hbb_common/src/socket_client.rs | 13 +- libs/hbb_common/src/tcp.rs | 16 +- libs/hbb_common/src/udp.rs | 70 +- 12 files changed, 913 insertions(+), 923 deletions(-) diff --git a/libs/hbb_common/build.rs b/libs/hbb_common/build.rs index bff0cfafc..fe0d31076 100644 --- a/libs/hbb_common/build.rs +++ b/libs/hbb_common/build.rs @@ -8,10 +8,7 @@ fn main() { .out_dir(out_dir) .inputs(&["protos/rendezvous.proto", "protos/message.proto"]) .include("protos") - .customize( - protobuf_codegen::Customize::default() - .tokio_bytes(true) - ) + .customize(protobuf_codegen::Customize::default().tokio_bytes(true)) .run() .expect("Codegen failed."); } diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index 12d698045..b7965f237 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -1,624 +1,624 @@ -syntax = "proto3"; -package hbb; - -message EncodedVideoFrame { - bytes data = 1; - bool key = 2; - int64 pts = 3; -} - -message EncodedVideoFrames { repeated EncodedVideoFrame frames = 1; } - -message RGB { bool compress = 1; } - -// planes data send directly in binary for better use arraybuffer on web -message YUV { - bool compress = 1; - int32 stride = 2; -} - -message VideoFrame { - oneof union { - EncodedVideoFrames vp9s = 6; - RGB rgb = 7; - YUV yuv = 8; - EncodedVideoFrames h264s = 10; - EncodedVideoFrames h265s = 11; - } - int64 timestamp = 9; -} - -message IdPk { - string id = 1; - bytes pk = 2; -} - -message DisplayInfo { - sint32 x = 1; - sint32 y = 2; - int32 width = 3; - int32 height = 4; - string name = 5; - bool online = 6; - bool cursor_embedded = 7; -} - -message PortForward { - string host = 1; - int32 port = 2; -} - -message FileTransfer { - string dir = 1; - bool show_hidden = 2; -} - -message LoginRequest { - string username = 1; - bytes password = 2; - string my_id = 4; - string my_name = 5; - OptionMessage option = 6; - oneof union { - FileTransfer file_transfer = 7; - PortForward port_forward = 8; - } - bool video_ack_required = 9; - uint64 session_id = 10; - string version = 11; -} - -message ChatMessage { string text = 1; } - -message Features { - bool privacy_mode = 1; -} - -message SupportedEncoding { - bool h264 = 1; - bool h265 = 2; -} - -message PeerInfo { - string username = 1; - string hostname = 2; - string platform = 3; - repeated DisplayInfo displays = 4; - int32 current_display = 5; - bool sas_enabled = 6; - string version = 7; - int32 conn_id = 8; - Features features = 9; - SupportedEncoding encoding = 10; -} - -message LoginResponse { - oneof union { - string error = 1; - PeerInfo peer_info = 2; - } -} - -message MouseEvent { - int32 mask = 1; - sint32 x = 2; - sint32 y = 3; - repeated ControlKey modifiers = 4; -} - -enum KeyboardMode{ - Legacy = 0; - Map = 1; - Translate = 2; - Auto = 3; -} - -enum ControlKey { - Unknown = 0; - Alt = 1; - Backspace = 2; - CapsLock = 3; - Control = 4; - Delete = 5; - DownArrow = 6; - End = 7; - Escape = 8; - F1 = 9; - F10 = 10; - F11 = 11; - F12 = 12; - F2 = 13; - F3 = 14; - F4 = 15; - F5 = 16; - F6 = 17; - F7 = 18; - F8 = 19; - F9 = 20; - Home = 21; - LeftArrow = 22; - /// meta key (also known as "windows"; "super"; and "command") - Meta = 23; - /// option key on macOS (alt key on Linux and Windows) - Option = 24; // deprecated, use Alt instead - PageDown = 25; - PageUp = 26; - Return = 27; - RightArrow = 28; - Shift = 29; - Space = 30; - Tab = 31; - UpArrow = 32; - Numpad0 = 33; - Numpad1 = 34; - Numpad2 = 35; - Numpad3 = 36; - Numpad4 = 37; - Numpad5 = 38; - Numpad6 = 39; - Numpad7 = 40; - Numpad8 = 41; - Numpad9 = 42; - Cancel = 43; - Clear = 44; - Menu = 45; // deprecated, use Alt instead - Pause = 46; - Kana = 47; - Hangul = 48; - Junja = 49; - Final = 50; - Hanja = 51; - Kanji = 52; - Convert = 53; - Select = 54; - Print = 55; - Execute = 56; - Snapshot = 57; - Insert = 58; - Help = 59; - Sleep = 60; - Separator = 61; - Scroll = 62; - NumLock = 63; - RWin = 64; - Apps = 65; - Multiply = 66; - Add = 67; - Subtract = 68; - Decimal = 69; - Divide = 70; - Equals = 71; - NumpadEnter = 72; - RShift = 73; - RControl = 74; - RAlt = 75; - CtrlAltDel = 100; - LockScreen = 101; -} - -message KeyEvent { - bool down = 1; - bool press = 2; - oneof union { - ControlKey control_key = 3; - uint32 chr = 4; - uint32 unicode = 5; - string seq = 6; - } - repeated ControlKey modifiers = 8; - KeyboardMode mode = 9; -} - -message CursorData { - uint64 id = 1; - sint32 hotx = 2; - sint32 hoty = 3; - int32 width = 4; - int32 height = 5; - bytes colors = 6; -} - -message CursorPosition { - sint32 x = 1; - sint32 y = 2; -} - -message Hash { - string salt = 1; - string challenge = 2; -} - -message Clipboard { - bool compress = 1; - bytes content = 2; -} - -enum FileType { - Dir = 0; - DirLink = 2; - DirDrive = 3; - File = 4; - FileLink = 5; -} - -message FileEntry { - FileType entry_type = 1; - string name = 2; - bool is_hidden = 3; - uint64 size = 4; - uint64 modified_time = 5; -} - -message FileDirectory { - int32 id = 1; - string path = 2; - repeated FileEntry entries = 3; -} - -message ReadDir { - string path = 1; - bool include_hidden = 2; -} - -message ReadAllFiles { - int32 id = 1; - string path = 2; - bool include_hidden = 3; -} - -message FileAction { - oneof union { - ReadDir read_dir = 1; - FileTransferSendRequest send = 2; - FileTransferReceiveRequest receive = 3; - FileDirCreate create = 4; - FileRemoveDir remove_dir = 5; - FileRemoveFile remove_file = 6; - ReadAllFiles all_files = 7; - FileTransferCancel cancel = 8; - FileTransferSendConfirmRequest send_confirm = 9; - } -} - -message FileTransferCancel { int32 id = 1; } - -message FileResponse { - oneof union { - FileDirectory dir = 1; - FileTransferBlock block = 2; - FileTransferError error = 3; - FileTransferDone done = 4; - FileTransferDigest digest = 5; - } -} - -message FileTransferDigest { - int32 id = 1; - sint32 file_num = 2; - uint64 last_modified = 3; - uint64 file_size = 4; - bool is_upload = 5; -} - -message FileTransferBlock { - int32 id = 1; - sint32 file_num = 2; - bytes data = 3; - bool compressed = 4; - uint32 blk_id = 5; -} - -message FileTransferError { - int32 id = 1; - string error = 2; - sint32 file_num = 3; -} - -message FileTransferSendRequest { - int32 id = 1; - string path = 2; - bool include_hidden = 3; - int32 file_num = 4; -} - -message FileTransferSendConfirmRequest { - int32 id = 1; - sint32 file_num = 2; - oneof union { - bool skip = 3; - uint32 offset_blk = 4; - } -} - -message FileTransferDone { - int32 id = 1; - sint32 file_num = 2; -} - -message FileTransferReceiveRequest { - int32 id = 1; - string path = 2; // path written to - repeated FileEntry files = 3; - int32 file_num = 4; -} - -message FileRemoveDir { - int32 id = 1; - string path = 2; - bool recursive = 3; -} - -message FileRemoveFile { - int32 id = 1; - string path = 2; - sint32 file_num = 3; -} - -message FileDirCreate { - int32 id = 1; - string path = 2; -} - -// main logic from freeRDP -message CliprdrMonitorReady { -} - -message CliprdrFormat { - int32 id = 2; - string format = 3; -} - -message CliprdrServerFormatList { - repeated CliprdrFormat formats = 2; -} - -message CliprdrServerFormatListResponse { - int32 msg_flags = 2; -} - -message CliprdrServerFormatDataRequest { - int32 requested_format_id = 2; -} - -message CliprdrServerFormatDataResponse { - int32 msg_flags = 2; - bytes format_data = 3; -} - -message CliprdrFileContentsRequest { - int32 stream_id = 2; - int32 list_index = 3; - int32 dw_flags = 4; - int32 n_position_low = 5; - int32 n_position_high = 6; - int32 cb_requested = 7; - bool have_clip_data_id = 8; - int32 clip_data_id = 9; -} - -message CliprdrFileContentsResponse { - int32 msg_flags = 3; - int32 stream_id = 4; - bytes requested_data = 5; -} - -message Cliprdr { - oneof union { - CliprdrMonitorReady ready = 1; - CliprdrServerFormatList format_list = 2; - CliprdrServerFormatListResponse format_list_response = 3; - CliprdrServerFormatDataRequest format_data_request = 4; - CliprdrServerFormatDataResponse format_data_response = 5; - CliprdrFileContentsRequest file_contents_request = 6; - CliprdrFileContentsResponse file_contents_response = 7; - } -} - -message SwitchDisplay { - int32 display = 1; - sint32 x = 2; - sint32 y = 3; - int32 width = 4; - int32 height = 5; - bool cursor_embedded = 6; -} - -message PermissionInfo { - enum Permission { - Keyboard = 0; - Clipboard = 2; - Audio = 3; - File = 4; - Restart = 5; - Recording = 6; - } - - Permission permission = 1; - bool enabled = 2; -} - -enum ImageQuality { - NotSet = 0; - Low = 2; - Balanced = 3; - Best = 4; -} - -message VideoCodecState { - enum PreferCodec { - Auto = 0; - VPX = 1; - H264 = 2; - H265 = 3; - } - - int32 score_vpx = 1; - int32 score_h264 = 2; - int32 score_h265 = 3; - PreferCodec prefer = 4; -} - -message OptionMessage { - enum BoolOption { - NotSet = 0; - No = 1; - Yes = 2; - } - ImageQuality image_quality = 1; - BoolOption lock_after_session_end = 2; - BoolOption show_remote_cursor = 3; - BoolOption privacy_mode = 4; - BoolOption block_input = 5; - int32 custom_image_quality = 6; - BoolOption disable_audio = 7; - BoolOption disable_clipboard = 8; - BoolOption enable_file_transfer = 9; - VideoCodecState video_codec_state = 10; - int32 custom_fps = 11; -} - -message TestDelay { - int64 time = 1; - bool from_client = 2; - uint32 last_delay = 3; - uint32 target_bitrate = 4; -} - -message PublicKey { - bytes asymmetric_value = 1; - bytes symmetric_value = 2; -} - -message SignedId { bytes id = 1; } - -message AudioFormat { - uint32 sample_rate = 1; - uint32 channels = 2; -} - -message AudioFrame { - bytes data = 1; - int64 timestamp = 2; -} - -// Notify peer to show message box. -message MessageBox { - // Message type. Refer to flutter/lib/common.dart/msgBox(). - string msgtype = 1; - string title = 2; - // English - string text = 3; - // If not empty, msgbox provides a button to following the link. - // The link here can't be directly http url. - // It must be the key of http url configed in peer side or "rustdesk://*" (jump in app). - string link = 4; -} - -message BackNotification { - // no need to consider block input by someone else - enum BlockInputState { - BlkStateUnknown = 0; - BlkOnSucceeded = 2; - BlkOnFailed = 3; - BlkOffSucceeded = 4; - BlkOffFailed = 5; - } - enum PrivacyModeState { - PrvStateUnknown = 0; - // Privacy mode on by someone else - PrvOnByOther = 2; - // Privacy mode is not supported on the remote side - PrvNotSupported = 3; - // Privacy mode on by self - PrvOnSucceeded = 4; - // Privacy mode on by self, but denied - PrvOnFailedDenied = 5; - // Some plugins are not found - PrvOnFailedPlugin = 6; - // Privacy mode on by self, but failed - PrvOnFailed = 7; - // Privacy mode off by self - PrvOffSucceeded = 8; - // Ctrl + P - PrvOffByPeer = 9; - // Privacy mode off by self, but failed - PrvOffFailed = 10; - PrvOffUnknown = 11; - } - - oneof union { - PrivacyModeState privacy_mode_state = 1; - BlockInputState block_input_state = 2; - } -} - -message ElevationRequestWithLogon { - string username = 1; - string password = 2; -} - -message ElevationRequest { - oneof union { - bool direct = 1; - ElevationRequestWithLogon logon = 2; - } -} - -message SwitchSidesRequest { - bytes uuid = 1; -} - -message SwitchSidesResponse { - bytes uuid = 1; - LoginRequest lr = 2; -} - -message SwitchBack {} - -message Misc { - oneof union { - ChatMessage chat_message = 4; - SwitchDisplay switch_display = 5; - PermissionInfo permission_info = 6; - OptionMessage option = 7; - AudioFormat audio_format = 8; - string close_reason = 9; - bool refresh_video = 10; - bool video_received = 12; - BackNotification back_notification = 13; - bool restart_remote_device = 14; - bool uac = 15; - bool foreground_window_elevated = 16; - bool stop_service = 17; - ElevationRequest elevation_request = 18; - string elevation_response = 19; - bool portable_service_running = 20; - SwitchSidesRequest switch_sides_request = 21; - SwitchBack switch_back = 22; - } -} - -message Message { - oneof union { - SignedId signed_id = 3; - PublicKey public_key = 4; - TestDelay test_delay = 5; - VideoFrame video_frame = 6; - LoginRequest login_request = 7; - LoginResponse login_response = 8; - Hash hash = 9; - MouseEvent mouse_event = 10; - AudioFrame audio_frame = 11; - CursorData cursor_data = 12; - CursorPosition cursor_position = 13; - uint64 cursor_id = 14; - KeyEvent key_event = 15; - Clipboard clipboard = 16; - FileAction file_action = 17; - FileResponse file_response = 18; - Misc misc = 19; - Cliprdr cliprdr = 20; - MessageBox message_box = 21; - SwitchSidesResponse switch_sides_response = 22; - } -} +syntax = "proto3"; +package hbb; + +message EncodedVideoFrame { + bytes data = 1; + bool key = 2; + int64 pts = 3; +} + +message EncodedVideoFrames { repeated EncodedVideoFrame frames = 1; } + +message RGB { bool compress = 1; } + +// planes data send directly in binary for better use arraybuffer on web +message YUV { + bool compress = 1; + int32 stride = 2; +} + +message VideoFrame { + oneof union { + EncodedVideoFrames vp9s = 6; + RGB rgb = 7; + YUV yuv = 8; + EncodedVideoFrames h264s = 10; + EncodedVideoFrames h265s = 11; + } + int64 timestamp = 9; +} + +message IdPk { + string id = 1; + bytes pk = 2; +} + +message DisplayInfo { + sint32 x = 1; + sint32 y = 2; + int32 width = 3; + int32 height = 4; + string name = 5; + bool online = 6; + bool cursor_embedded = 7; +} + +message PortForward { + string host = 1; + int32 port = 2; +} + +message FileTransfer { + string dir = 1; + bool show_hidden = 2; +} + +message LoginRequest { + string username = 1; + bytes password = 2; + string my_id = 4; + string my_name = 5; + OptionMessage option = 6; + oneof union { + FileTransfer file_transfer = 7; + PortForward port_forward = 8; + } + bool video_ack_required = 9; + uint64 session_id = 10; + string version = 11; +} + +message ChatMessage { string text = 1; } + +message Features { + bool privacy_mode = 1; +} + +message SupportedEncoding { + bool h264 = 1; + bool h265 = 2; +} + +message PeerInfo { + string username = 1; + string hostname = 2; + string platform = 3; + repeated DisplayInfo displays = 4; + int32 current_display = 5; + bool sas_enabled = 6; + string version = 7; + int32 conn_id = 8; + Features features = 9; + SupportedEncoding encoding = 10; +} + +message LoginResponse { + oneof union { + string error = 1; + PeerInfo peer_info = 2; + } +} + +message MouseEvent { + int32 mask = 1; + sint32 x = 2; + sint32 y = 3; + repeated ControlKey modifiers = 4; +} + +enum KeyboardMode{ + Legacy = 0; + Map = 1; + Translate = 2; + Auto = 3; +} + +enum ControlKey { + Unknown = 0; + Alt = 1; + Backspace = 2; + CapsLock = 3; + Control = 4; + Delete = 5; + DownArrow = 6; + End = 7; + Escape = 8; + F1 = 9; + F10 = 10; + F11 = 11; + F12 = 12; + F2 = 13; + F3 = 14; + F4 = 15; + F5 = 16; + F6 = 17; + F7 = 18; + F8 = 19; + F9 = 20; + Home = 21; + LeftArrow = 22; + /// meta key (also known as "windows"; "super"; and "command") + Meta = 23; + /// option key on macOS (alt key on Linux and Windows) + Option = 24; // deprecated, use Alt instead + PageDown = 25; + PageUp = 26; + Return = 27; + RightArrow = 28; + Shift = 29; + Space = 30; + Tab = 31; + UpArrow = 32; + Numpad0 = 33; + Numpad1 = 34; + Numpad2 = 35; + Numpad3 = 36; + Numpad4 = 37; + Numpad5 = 38; + Numpad6 = 39; + Numpad7 = 40; + Numpad8 = 41; + Numpad9 = 42; + Cancel = 43; + Clear = 44; + Menu = 45; // deprecated, use Alt instead + Pause = 46; + Kana = 47; + Hangul = 48; + Junja = 49; + Final = 50; + Hanja = 51; + Kanji = 52; + Convert = 53; + Select = 54; + Print = 55; + Execute = 56; + Snapshot = 57; + Insert = 58; + Help = 59; + Sleep = 60; + Separator = 61; + Scroll = 62; + NumLock = 63; + RWin = 64; + Apps = 65; + Multiply = 66; + Add = 67; + Subtract = 68; + Decimal = 69; + Divide = 70; + Equals = 71; + NumpadEnter = 72; + RShift = 73; + RControl = 74; + RAlt = 75; + CtrlAltDel = 100; + LockScreen = 101; +} + +message KeyEvent { + bool down = 1; + bool press = 2; + oneof union { + ControlKey control_key = 3; + uint32 chr = 4; + uint32 unicode = 5; + string seq = 6; + } + repeated ControlKey modifiers = 8; + KeyboardMode mode = 9; +} + +message CursorData { + uint64 id = 1; + sint32 hotx = 2; + sint32 hoty = 3; + int32 width = 4; + int32 height = 5; + bytes colors = 6; +} + +message CursorPosition { + sint32 x = 1; + sint32 y = 2; +} + +message Hash { + string salt = 1; + string challenge = 2; +} + +message Clipboard { + bool compress = 1; + bytes content = 2; +} + +enum FileType { + Dir = 0; + DirLink = 2; + DirDrive = 3; + File = 4; + FileLink = 5; +} + +message FileEntry { + FileType entry_type = 1; + string name = 2; + bool is_hidden = 3; + uint64 size = 4; + uint64 modified_time = 5; +} + +message FileDirectory { + int32 id = 1; + string path = 2; + repeated FileEntry entries = 3; +} + +message ReadDir { + string path = 1; + bool include_hidden = 2; +} + +message ReadAllFiles { + int32 id = 1; + string path = 2; + bool include_hidden = 3; +} + +message FileAction { + oneof union { + ReadDir read_dir = 1; + FileTransferSendRequest send = 2; + FileTransferReceiveRequest receive = 3; + FileDirCreate create = 4; + FileRemoveDir remove_dir = 5; + FileRemoveFile remove_file = 6; + ReadAllFiles all_files = 7; + FileTransferCancel cancel = 8; + FileTransferSendConfirmRequest send_confirm = 9; + } +} + +message FileTransferCancel { int32 id = 1; } + +message FileResponse { + oneof union { + FileDirectory dir = 1; + FileTransferBlock block = 2; + FileTransferError error = 3; + FileTransferDone done = 4; + FileTransferDigest digest = 5; + } +} + +message FileTransferDigest { + int32 id = 1; + sint32 file_num = 2; + uint64 last_modified = 3; + uint64 file_size = 4; + bool is_upload = 5; +} + +message FileTransferBlock { + int32 id = 1; + sint32 file_num = 2; + bytes data = 3; + bool compressed = 4; + uint32 blk_id = 5; +} + +message FileTransferError { + int32 id = 1; + string error = 2; + sint32 file_num = 3; +} + +message FileTransferSendRequest { + int32 id = 1; + string path = 2; + bool include_hidden = 3; + int32 file_num = 4; +} + +message FileTransferSendConfirmRequest { + int32 id = 1; + sint32 file_num = 2; + oneof union { + bool skip = 3; + uint32 offset_blk = 4; + } +} + +message FileTransferDone { + int32 id = 1; + sint32 file_num = 2; +} + +message FileTransferReceiveRequest { + int32 id = 1; + string path = 2; // path written to + repeated FileEntry files = 3; + int32 file_num = 4; +} + +message FileRemoveDir { + int32 id = 1; + string path = 2; + bool recursive = 3; +} + +message FileRemoveFile { + int32 id = 1; + string path = 2; + sint32 file_num = 3; +} + +message FileDirCreate { + int32 id = 1; + string path = 2; +} + +// main logic from freeRDP +message CliprdrMonitorReady { +} + +message CliprdrFormat { + int32 id = 2; + string format = 3; +} + +message CliprdrServerFormatList { + repeated CliprdrFormat formats = 2; +} + +message CliprdrServerFormatListResponse { + int32 msg_flags = 2; +} + +message CliprdrServerFormatDataRequest { + int32 requested_format_id = 2; +} + +message CliprdrServerFormatDataResponse { + int32 msg_flags = 2; + bytes format_data = 3; +} + +message CliprdrFileContentsRequest { + int32 stream_id = 2; + int32 list_index = 3; + int32 dw_flags = 4; + int32 n_position_low = 5; + int32 n_position_high = 6; + int32 cb_requested = 7; + bool have_clip_data_id = 8; + int32 clip_data_id = 9; +} + +message CliprdrFileContentsResponse { + int32 msg_flags = 3; + int32 stream_id = 4; + bytes requested_data = 5; +} + +message Cliprdr { + oneof union { + CliprdrMonitorReady ready = 1; + CliprdrServerFormatList format_list = 2; + CliprdrServerFormatListResponse format_list_response = 3; + CliprdrServerFormatDataRequest format_data_request = 4; + CliprdrServerFormatDataResponse format_data_response = 5; + CliprdrFileContentsRequest file_contents_request = 6; + CliprdrFileContentsResponse file_contents_response = 7; + } +} + +message SwitchDisplay { + int32 display = 1; + sint32 x = 2; + sint32 y = 3; + int32 width = 4; + int32 height = 5; + bool cursor_embedded = 6; +} + +message PermissionInfo { + enum Permission { + Keyboard = 0; + Clipboard = 2; + Audio = 3; + File = 4; + Restart = 5; + Recording = 6; + } + + Permission permission = 1; + bool enabled = 2; +} + +enum ImageQuality { + NotSet = 0; + Low = 2; + Balanced = 3; + Best = 4; +} + +message VideoCodecState { + enum PreferCodec { + Auto = 0; + VPX = 1; + H264 = 2; + H265 = 3; + } + + int32 score_vpx = 1; + int32 score_h264 = 2; + int32 score_h265 = 3; + PreferCodec prefer = 4; +} + +message OptionMessage { + enum BoolOption { + NotSet = 0; + No = 1; + Yes = 2; + } + ImageQuality image_quality = 1; + BoolOption lock_after_session_end = 2; + BoolOption show_remote_cursor = 3; + BoolOption privacy_mode = 4; + BoolOption block_input = 5; + int32 custom_image_quality = 6; + BoolOption disable_audio = 7; + BoolOption disable_clipboard = 8; + BoolOption enable_file_transfer = 9; + VideoCodecState video_codec_state = 10; + int32 custom_fps = 11; +} + +message TestDelay { + int64 time = 1; + bool from_client = 2; + uint32 last_delay = 3; + uint32 target_bitrate = 4; +} + +message PublicKey { + bytes asymmetric_value = 1; + bytes symmetric_value = 2; +} + +message SignedId { bytes id = 1; } + +message AudioFormat { + uint32 sample_rate = 1; + uint32 channels = 2; +} + +message AudioFrame { + bytes data = 1; + int64 timestamp = 2; +} + +// Notify peer to show message box. +message MessageBox { + // Message type. Refer to flutter/lib/common.dart/msgBox(). + string msgtype = 1; + string title = 2; + // English + string text = 3; + // If not empty, msgbox provides a button to following the link. + // The link here can't be directly http url. + // It must be the key of http url configed in peer side or "rustdesk://*" (jump in app). + string link = 4; +} + +message BackNotification { + // no need to consider block input by someone else + enum BlockInputState { + BlkStateUnknown = 0; + BlkOnSucceeded = 2; + BlkOnFailed = 3; + BlkOffSucceeded = 4; + BlkOffFailed = 5; + } + enum PrivacyModeState { + PrvStateUnknown = 0; + // Privacy mode on by someone else + PrvOnByOther = 2; + // Privacy mode is not supported on the remote side + PrvNotSupported = 3; + // Privacy mode on by self + PrvOnSucceeded = 4; + // Privacy mode on by self, but denied + PrvOnFailedDenied = 5; + // Some plugins are not found + PrvOnFailedPlugin = 6; + // Privacy mode on by self, but failed + PrvOnFailed = 7; + // Privacy mode off by self + PrvOffSucceeded = 8; + // Ctrl + P + PrvOffByPeer = 9; + // Privacy mode off by self, but failed + PrvOffFailed = 10; + PrvOffUnknown = 11; + } + + oneof union { + PrivacyModeState privacy_mode_state = 1; + BlockInputState block_input_state = 2; + } +} + +message ElevationRequestWithLogon { + string username = 1; + string password = 2; +} + +message ElevationRequest { + oneof union { + bool direct = 1; + ElevationRequestWithLogon logon = 2; + } +} + +message SwitchSidesRequest { + bytes uuid = 1; +} + +message SwitchSidesResponse { + bytes uuid = 1; + LoginRequest lr = 2; +} + +message SwitchBack {} + +message Misc { + oneof union { + ChatMessage chat_message = 4; + SwitchDisplay switch_display = 5; + PermissionInfo permission_info = 6; + OptionMessage option = 7; + AudioFormat audio_format = 8; + string close_reason = 9; + bool refresh_video = 10; + bool video_received = 12; + BackNotification back_notification = 13; + bool restart_remote_device = 14; + bool uac = 15; + bool foreground_window_elevated = 16; + bool stop_service = 17; + ElevationRequest elevation_request = 18; + string elevation_response = 19; + bool portable_service_running = 20; + SwitchSidesRequest switch_sides_request = 21; + SwitchBack switch_back = 22; + } +} + +message Message { + oneof union { + SignedId signed_id = 3; + PublicKey public_key = 4; + TestDelay test_delay = 5; + VideoFrame video_frame = 6; + LoginRequest login_request = 7; + LoginResponse login_response = 8; + Hash hash = 9; + MouseEvent mouse_event = 10; + AudioFrame audio_frame = 11; + CursorData cursor_data = 12; + CursorPosition cursor_position = 13; + uint64 cursor_id = 14; + KeyEvent key_event = 15; + Clipboard clipboard = 16; + FileAction file_action = 17; + FileResponse file_response = 18; + Misc misc = 19; + Cliprdr cliprdr = 20; + MessageBox message_box = 21; + SwitchSidesResponse switch_sides_response = 22; + } +} diff --git a/libs/hbb_common/src/bytes_codec.rs b/libs/hbb_common/src/bytes_codec.rs index e029f1cc0..699aa9bff 100644 --- a/libs/hbb_common/src/bytes_codec.rs +++ b/libs/hbb_common/src/bytes_codec.rs @@ -15,6 +15,12 @@ enum DecodeState { Data(usize), } +impl Default for BytesCodec { + fn default() -> Self { + Self::new() + } +} + impl BytesCodec { pub fn new() -> Self { Self { @@ -56,7 +62,7 @@ impl BytesCodec { } src.advance(head_len); src.reserve(n); - return Ok(Some(n)); + Ok(Some(n)) } fn decode_data(&self, n: usize, src: &mut BytesMut) -> io::Result> { diff --git a/libs/hbb_common/src/compress.rs b/libs/hbb_common/src/compress.rs index a969ccf86..e7668a949 100644 --- a/libs/hbb_common/src/compress.rs +++ b/libs/hbb_common/src/compress.rs @@ -32,12 +32,7 @@ pub fn decompress(data: &[u8]) -> Vec { const MAX: usize = 1024 * 1024 * 64; const MIN: usize = 1024 * 1024; let mut n = 30 * data.len(); - if n > MAX { - n = MAX; - } - if n < MIN { - n = MIN; - } + n = n.clamp(MIN, MAX); match d.decompress(data, n) { Ok(res) => out = res, Err(err) => { diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 20334ed12..8bea99106 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -29,7 +29,7 @@ pub const READ_TIMEOUT: u64 = 30_000; pub const REG_INTERVAL: i64 = 12_000; pub const COMPRESS_LEVEL: i32 = 3; const SERIAL: i32 = 3; -const PASSWORD_ENC_VERSION: &'static str = "00"; +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= @@ -43,6 +43,7 @@ lazy_static::lazy_static! { } type Size = (i32, i32, i32, i32); +type KeyPair = (Vec, Vec); lazy_static::lazy_static! { static ref CONFIG: Arc> = Arc::new(RwLock::new(Config::load())); @@ -54,7 +55,7 @@ lazy_static::lazy_static! { _ => "", }.to_owned())); pub static ref APP_NAME: Arc> = Arc::new(RwLock::new("RustDesk".to_owned())); - static ref KEY_PAIR: Arc, Vec)>>> = Default::default(); + static ref KEY_PAIR: Arc>> = Default::default(); static ref HW_CODEC_CONFIG: Arc> = Arc::new(RwLock::new(HwCodecConfig::load())); } @@ -75,18 +76,18 @@ lazy_static::lazy_static! { ]); } -const CHARS: &'static [char] = &[ +const CHARS: &[char] = &[ '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'm', 'n', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', ]; -const RENDEZVOUS_SERVERS: &'static [&'static str] = &[ +pub const RENDEZVOUS_SERVERS: &[&str] = &[ "rs-ny.rustdesk.com", "rs-sg.rustdesk.com", "rs-cn.rustdesk.com", ]; -pub const RS_PUB_KEY: &'static str = match option_env!("RS_PUB_KEY") { +pub const RS_PUB_KEY: &str = match option_env!("RS_PUB_KEY") { Some(key) if !key.is_empty() => key, _ => "OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw=", }; @@ -131,7 +132,7 @@ pub struct Config { #[serde(default)] salt: String, #[serde(default)] - key_pair: (Vec, Vec), // sk, pk + key_pair: KeyPair, // sk, pk #[serde(default)] key_confirmed: bool, #[serde(default)] @@ -319,7 +320,7 @@ impl Config2 { pub fn load_path( file: PathBuf, ) -> T { - let cfg = match confy::load_path(&file) { + let cfg = match confy::load_path(file) { Ok(config) => config, Err(err) => { log::error!("Failed to load config: {}", err); @@ -366,20 +367,16 @@ impl Config { config.id = id; id_valid = true; store |= store2; - } else { - if crate::get_modified_time(&Self::file_("")) - .checked_sub(std::time::Duration::from_secs(30)) // allow modification during installation - .unwrap_or(crate::get_exe_time()) - < crate::get_exe_time() - { - if !config.id.is_empty() - && config.enc_id.is_empty() - && !decrypt_str_or_original(&config.id, PASSWORD_ENC_VERSION).1 - { - id_valid = true; - store = true; - } - } + } else if crate::get_modified_time(&Self::file_("")) + .checked_sub(std::time::Duration::from_secs(30)) // allow modification during installation + .unwrap_or_else(crate::get_exe_time) + < crate::get_exe_time() + && !config.id.is_empty() + && config.enc_id.is_empty() + && !decrypt_str_or_original(&config.id, PASSWORD_ENC_VERSION).1 + { + id_valid = true; + store = true; } if !id_valid { for _ in 0..3 { @@ -444,18 +441,18 @@ impl Config { #[cfg(not(any(target_os = "android", target_os = "ios")))] { #[cfg(not(target_os = "macos"))] - let org = ""; + let org = "".to_owned(); #[cfg(target_os = "macos")] let org = ORG.read().unwrap().clone(); // /var/root for root if let Some(project) = - directories_next::ProjectDirs::from("", &org, &*APP_NAME.read().unwrap()) + directories_next::ProjectDirs::from("", &org, &APP_NAME.read().unwrap()) { let mut path = patch(project.config_dir().to_path_buf()); path.push(p); return path; } - return "".into(); + "".into() } } @@ -539,9 +536,9 @@ impl Config { rendezvous_server = Self::get_rendezvous_servers() .drain(..) .next() - .unwrap_or("".to_owned()); + .unwrap_or_default(); } - if !rendezvous_server.contains(":") { + if !rendezvous_server.contains(':') { rendezvous_server = format!("{}:{}", rendezvous_server, RENDEZVOUS_PORT); } rendezvous_server @@ -559,8 +556,8 @@ impl Config { let serial_obsolute = CONFIG2.read().unwrap().serial > SERIAL; if serial_obsolute { let ss: Vec = Self::get_option("rendezvous-servers") - .split(",") - .filter(|x| x.contains(".")) + .split(',') + .filter(|x| x.contains('.')) .map(|x| x.to_owned()) .collect(); if !ss.is_empty() { @@ -580,7 +577,7 @@ impl Config { let mut delay = i64::MAX; for (tmp_host, tmp_delay) in ONLINE.lock().unwrap().iter() { if tmp_delay > &0 && tmp_delay < &delay { - delay = tmp_delay.clone(); + delay = *tmp_delay; host = tmp_host.to_string(); } } @@ -647,7 +644,7 @@ impl Config { for x in &ma.bytes()[2..] { id = (id << 8) | (*x as u32); } - id = id & 0x1FFFFFFF; + id &= 0x1FFFFFFF; Some(id.to_string()) } else { None @@ -679,11 +676,7 @@ impl Config { } pub fn get_host_key_confirmed(host: &str) -> bool { - if let Some(true) = CONFIG.read().unwrap().keys_confirmed.get(host) { - true - } else { - false - } + matches!(CONFIG.read().unwrap().keys_confirmed.get(host), Some(true)) } pub fn set_host_key_confirmed(host: &str, v: bool) { @@ -695,7 +688,7 @@ impl Config { config.store(); } - pub fn get_key_pair() -> (Vec, Vec) { + pub fn get_key_pair() -> KeyPair { // lock here to make sure no gen_keypair more than once // no use of CONFIG directly here to ensure no recursive calling in Config::load because of password dec which calling this function let mut lock = KEY_PAIR.lock().unwrap(); @@ -714,7 +707,7 @@ impl Config { }); } *lock = Some(config.key_pair.clone()); - return config.key_pair; + config.key_pair } pub fn get_id() -> String { @@ -849,7 +842,7 @@ impl Config { let ext = path.extension(); if let Some(ext) = ext { let ext = format!("{}.toml", ext.to_string_lossy()); - path.with_extension(&ext) + path.with_extension(ext) } else { path.with_extension("toml") } @@ -861,7 +854,7 @@ const PEERS: &str = "peers"; impl PeerConfig { pub fn load(id: &str) -> PeerConfig { let _lock = CONFIG.read().unwrap(); - match confy::load_path(&Self::path(id)) { + match confy::load_path(Self::path(id)) { Ok(config) => { let mut config: PeerConfig = config; let mut store = false; @@ -869,16 +862,16 @@ impl PeerConfig { decrypt_vec_or_original(&config.password, PASSWORD_ENC_VERSION); config.password = password; store = store || store2; - config.options.get_mut("rdp_password").map(|v| { + if let Some(v) = config.options.get_mut("rdp_password") { let (password, _, store2) = decrypt_str_or_original(v, PASSWORD_ENC_VERSION); *v = password; store = store || store2; - }); - config.options.get_mut("os-password").map(|v| { + } + if let Some(v) = config.options.get_mut("os-password") { let (password, _, store2) = decrypt_str_or_original(v, PASSWORD_ENC_VERSION); *v = password; store = store || store2; - }); + } if store { config.store(id); } @@ -895,34 +888,29 @@ impl PeerConfig { let _lock = CONFIG.read().unwrap(); let mut config = self.clone(); config.password = encrypt_vec_or_original(&config.password, PASSWORD_ENC_VERSION); - config - .options - .get_mut("rdp_password") - .map(|v| *v = encrypt_str_or_original(v, PASSWORD_ENC_VERSION)); - config - .options - .get_mut("os-password") - .map(|v| *v = encrypt_str_or_original(v, PASSWORD_ENC_VERSION)); + if let Some(v) = config.options.get_mut("rdp_password") { + *v = encrypt_str_or_original(v, PASSWORD_ENC_VERSION) + } + if let Some(v) = config.options.get_mut("os-password") { + *v = encrypt_str_or_original(v, PASSWORD_ENC_VERSION) + }; if let Err(err) = store_path(Self::path(id), config) { log::error!("Failed to store config: {}", err); } } pub fn remove(id: &str) { - fs::remove_file(&Self::path(id)).ok(); + fs::remove_file(Self::path(id)).ok(); } fn path(id: &str) -> PathBuf { - let id_encoded: String; - //If the id contains invalid chars, encode it let forbidden_paths = Regex::new(r".*[<>:/\\|\?\*].*").unwrap(); - if forbidden_paths.is_match(id) { - id_encoded = - "base64_".to_string() + base64::encode(id, base64::Variant::Original).as_str(); + let id_encoded = if forbidden_paths.is_match(id) { + "base64_".to_string() + base64::encode(id, base64::Variant::Original).as_str() } else { - id_encoded = id.to_string(); - } + id.to_string() + }; let path: PathBuf = [PEERS, id_encoded.as_str()].iter().collect(); Config::with_extension(Config::path(path)) } @@ -940,26 +928,24 @@ impl PeerConfig { && p.extension().map(|p| p.to_str().unwrap_or("")) == Some("toml") }) .map(|p| { - let t = crate::get_modified_time(&p); + let t = crate::get_modified_time(p); let id = p .file_stem() .map(|p| p.to_str().unwrap_or("")) .unwrap_or("") .to_owned(); - let id_decoded_string: String; - if id.starts_with("base64_") && id.len() != 7 { + let id_decoded_string = if id.starts_with("base64_") && id.len() != 7 { let id_decoded = base64::decode(&id[7..], base64::Variant::Original) - .unwrap_or(Vec::new()); - id_decoded_string = - String::from_utf8_lossy(&id_decoded).as_ref().to_owned(); + .unwrap_or_default(); + String::from_utf8_lossy(&id_decoded).as_ref().to_owned() } else { - id_decoded_string = id; - } + id + }; let c = PeerConfig::load(&id_decoded_string); if c.info.platform.is_empty() { - fs::remove_file(&p).ok(); + fs::remove_file(p).ok(); } (id_decoded_string, t, c) }) @@ -1149,7 +1135,7 @@ pub struct LanPeers { impl LanPeers { pub fn load() -> LanPeers { let _lock = CONFIG.read().unwrap(); - match confy::load_path(&Config::file_("_lan_peers")) { + match confy::load_path(Config::file_("_lan_peers")) { Ok(peers) => peers, Err(err) => { log::error!("Failed to load lan peers: {}", err); @@ -1158,9 +1144,9 @@ impl LanPeers { } } - pub fn store(peers: &Vec) { + pub fn store(peers: &[DiscoveryPeer]) { let f = LanPeers { - peers: peers.clone(), + peers: peers.to_owned(), }; if let Err(err) = store_path(Config::file_("_lan_peers"), f) { log::error!("Failed to store lan peers: {}", err); diff --git a/libs/hbb_common/src/fs.rs b/libs/hbb_common/src/fs.rs index fec8b8670..ea54e113a 100644 --- a/libs/hbb_common/src/fs.rs +++ b/libs/hbb_common/src/fs.rs @@ -13,13 +13,13 @@ use crate::{ config::{Config, COMPRESS_LEVEL}, }; -pub fn read_dir(path: &PathBuf, include_hidden: bool) -> ResultType { +pub fn read_dir(path: &Path, include_hidden: bool) -> ResultType { let mut dir = FileDirectory { - path: get_string(&path), + path: get_string(path), ..Default::default() }; #[cfg(windows)] - if "/" == &get_string(&path) { + if "/" == &get_string(path) { let drives = unsafe { winapi::um::fileapi::GetLogicalDrives() }; for i in 0..32 { if drives & (1 << i) != 0 { @@ -36,74 +36,70 @@ pub fn read_dir(path: &PathBuf, include_hidden: bool) -> ResultType String { +pub fn get_file_name(p: &Path) -> String { p.file_name() .map(|p| p.to_str().unwrap_or("")) .unwrap_or("") @@ -111,7 +107,7 @@ pub fn get_file_name(p: &PathBuf) -> String { } #[inline] -pub fn get_string(path: &PathBuf) -> String { +pub fn get_string(path: &Path) -> String { path.to_str().unwrap_or("").to_owned() } @@ -127,14 +123,14 @@ pub fn get_home_as_string() -> String { fn read_dir_recursive( path: &PathBuf, - prefix: &PathBuf, + prefix: &Path, include_hidden: bool, ) -> ResultType> { let mut files = Vec::new(); if path.is_dir() { // to-do: symbol link handling, cp the link rather than the content // to-do: file mode, for unix - let fd = read_dir(&path, include_hidden)?; + let fd = read_dir(path, include_hidden)?; for entry in fd.entries.iter() { match entry.entry_type.enum_value() { Ok(FileType::File) => { @@ -158,7 +154,7 @@ fn read_dir_recursive( } Ok(files) } else if path.is_file() { - let (size, modified_time) = if let Ok(meta) = std::fs::metadata(&path) { + let (size, modified_time) = if let Ok(meta) = std::fs::metadata(path) { ( meta.len(), meta.modified() @@ -167,7 +163,7 @@ fn read_dir_recursive( .map(|x| x.as_secs()) .unwrap_or(0) }) - .unwrap_or(0) as u64, + .unwrap_or(0), ) } else { (0, 0) @@ -249,7 +245,7 @@ pub struct RemoveJobMeta { #[inline] fn get_ext(name: &str) -> &str { - if let Some(i) = name.rfind(".") { + if let Some(i) = name.rfind('.') { return &name[i + 1..]; } "" @@ -270,6 +266,7 @@ fn is_compressed_file(name: &str) -> bool { } impl TransferJob { + #[allow(clippy::too_many_arguments)] pub fn new_write( id: i32, remote: String, @@ -281,7 +278,7 @@ impl TransferJob { enable_overwrite_detection: bool, ) -> Self { log::info!("new write {}", path); - let total_size = files.iter().map(|x| x.size as u64).sum(); + let total_size = files.iter().map(|x| x.size).sum(); Self { id, remote, @@ -307,7 +304,7 @@ impl TransferJob { ) -> ResultType { log::info!("new read {}", path); let files = get_recursive_files(&path, show_hidden)?; - let total_size = files.iter().map(|x| x.size as u64).sum(); + let total_size = files.iter().map(|x| x.size).sum(); Ok(Self { id, remote, @@ -363,7 +360,7 @@ impl TransferJob { let entry = &self.files[file_num]; let path = self.join(&entry.name); let download_path = format!("{}.download", get_string(&path)); - std::fs::rename(&download_path, &path).ok(); + std::fs::rename(download_path, &path).ok(); filetime::set_file_mtime( &path, filetime::FileTime::from_unix_time(entry.modified_time as _, 0), @@ -378,7 +375,7 @@ impl TransferJob { let entry = &self.files[file_num]; let path = self.join(&entry.name); let download_path = format!("{}.download", get_string(&path)); - std::fs::remove_file(&download_path).ok(); + std::fs::remove_file(download_path).ok(); } } @@ -433,7 +430,7 @@ impl TransferJob { } let name = &self.files[file_num].name; if self.file.is_none() { - match File::open(self.join(&name)).await { + match File::open(self.join(name)).await { Ok(file) => { self.file = Some(file); self.file_confirmed = false; @@ -447,20 +444,15 @@ impl TransferJob { } } } - if self.enable_overwrite_detection { - if !self.file_confirmed() { - if !self.file_is_waiting() { - self.send_current_digest(stream).await?; - self.set_file_is_waiting(true); - } - return Ok(None); + if self.enable_overwrite_detection && !self.file_confirmed() { + if !self.file_is_waiting() { + self.send_current_digest(stream).await?; + self.set_file_is_waiting(true); } + return Ok(None); } const BUF_SIZE: usize = 128 * 1024; - let mut buf: Vec = Vec::with_capacity(BUF_SIZE); - unsafe { - buf.set_len(BUF_SIZE); - } + let mut buf: Vec = vec![0; BUF_SIZE]; let mut compressed = false; let mut offset: usize = 0; loop { @@ -582,10 +574,7 @@ impl TransferJob { #[inline] pub fn job_completed(&self) -> bool { // has no error, Condition 2 - if !self.enable_overwrite_detection || (!self.file_confirmed && !self.file_is_waiting) { - return true; - } - return false; + !self.enable_overwrite_detection || (!self.file_confirmed && !self.file_is_waiting) } /// Get job error message, useful for getting status when job had finished @@ -660,7 +649,7 @@ pub fn new_dir(id: i32, path: String, files: Vec) -> Message { resp.set_dir(FileDirectory { id, path, - entries: files.into(), + entries: files, ..Default::default() }); let mut msg_out = Message::new(); @@ -692,7 +681,7 @@ pub fn new_receive(id: i32, path: String, file_num: i32, files: Vec) action.set_receive(FileTransferReceiveRequest { id, path, - files: files.into(), + files, file_num, ..Default::default() }); @@ -736,8 +725,8 @@ pub fn remove_job(id: i32, jobs: &mut Vec) { } #[inline] -pub fn get_job(id: i32, jobs: &mut Vec) -> Option<&mut TransferJob> { - jobs.iter_mut().filter(|x| x.id() == id).next() +pub fn get_job(id: i32, jobs: &mut [TransferJob]) -> Option<&mut TransferJob> { + jobs.iter_mut().find(|x| x.id() == id) } pub async fn handle_read_jobs( @@ -789,7 +778,7 @@ pub fn remove_all_empty_dir(path: &PathBuf) -> ResultType<()> { remove_all_empty_dir(&path.join(&entry.name)).ok(); } Ok(FileType::DirLink) | Ok(FileType::FileLink) => { - std::fs::remove_file(&path.join(&entry.name)).ok(); + std::fs::remove_file(path.join(&entry.name)).ok(); } _ => {} } @@ -813,7 +802,7 @@ pub fn create_dir(dir: &str) -> ResultType<()> { #[inline] pub fn transform_windows_path(entries: &mut Vec) { for entry in entries { - entry.name = entry.name.replace("\\", "/"); + entry.name = entry.name.replace('\\', "/"); } } diff --git a/libs/hbb_common/src/lib.rs b/libs/hbb_common/src/lib.rs index e57994f34..9e004376c 100644 --- a/libs/hbb_common/src/lib.rs +++ b/libs/hbb_common/src/lib.rs @@ -96,8 +96,24 @@ pub type ResultType = anyhow::Result; pub struct AddrMangle(); +#[inline] +pub fn try_into_v4(addr: SocketAddr) -> SocketAddr { + match addr { + SocketAddr::V6(v6) if !addr.ip().is_loopback() => { + if let Some(v4) = v6.ip().to_ipv4() { + SocketAddr::new(IpAddr::V4(v4), addr.port()) + } else { + addr + } + } + _ => addr, + } +} + impl AddrMangle { pub fn encode(addr: SocketAddr) -> Vec { + // not work with [:1]: + let addr = try_into_v4(addr); match addr { SocketAddr::V4(addr_v4) => { let tm = (SystemTime::now() @@ -129,22 +145,20 @@ impl AddrMangle { } pub fn decode(bytes: &[u8]) -> SocketAddr { + use std::convert::TryInto; + if bytes.len() > 16 { if bytes.len() != 18 { return Config::get_any_listen_addr(false); } - #[allow(invalid_value)] - let mut tmp: [u8; 2] = unsafe { std::mem::MaybeUninit::uninit().assume_init() }; - tmp.copy_from_slice(&bytes[16..]); + let tmp: [u8; 2] = bytes[16..].try_into().unwrap(); let port = u16::from_le_bytes(tmp); - #[allow(invalid_value)] - let mut tmp: [u8; 16] = unsafe { std::mem::MaybeUninit::uninit().assume_init() }; - tmp.copy_from_slice(&bytes[..16]); + let tmp: [u8; 16] = bytes[..16].try_into().unwrap(); let ip = std::net::Ipv6Addr::from(tmp); return SocketAddr::new(IpAddr::V6(ip), port); } let mut padded = [0u8; 16]; - padded[..bytes.len()].copy_from_slice(&bytes); + padded[..bytes.len()].copy_from_slice(bytes); let number = u128::from_le_bytes(padded); let tm = (number >> 17) & (u32::max_value() as u128); let ip = (((number >> 49) - tm) as u32).to_le_bytes(); @@ -158,21 +172,9 @@ impl AddrMangle { pub fn get_version_from_url(url: &str) -> String { let n = url.chars().count(); - let a = url - .chars() - .rev() - .enumerate() - .filter(|(_, x)| x == &'-') - .next() - .map(|(i, _)| i); + let a = url.chars().rev().position(|x| x == '-'); if let Some(a) = a { - let b = url - .chars() - .rev() - .enumerate() - .filter(|(_, x)| x == &'.') - .next() - .map(|(i, _)| i); + let b = url.chars().rev().position(|x| x == '.'); if let Some(b) = b { if a > b { if url @@ -195,22 +197,30 @@ pub fn get_version_from_url(url: &str) -> String { } pub fn gen_version() { + if Ok("release".to_owned()) != std::env::var("PROFILE") { + return; + } + println!("cargo:rerun-if-changed=Cargo.toml"); use std::io::prelude::*; let mut file = File::create("./src/version.rs").unwrap(); - for line in read_lines("Cargo.toml").unwrap() { - if let Ok(line) = line { - let ab: Vec<&str> = line.split("=").map(|x| x.trim()).collect(); - if ab.len() == 2 && ab[0] == "version" { - file.write_all(format!("pub const VERSION: &str = {};\n", ab[1]).as_bytes()) - .ok(); - break; - } + for line in read_lines("Cargo.toml").unwrap().flatten() { + let ab: Vec<&str> = line.split('=').map(|x| x.trim()).collect(); + if ab.len() == 2 && ab[0] == "version" { + file.write_all(format!("pub const VERSION: &str = {};\n", ab[1]).as_bytes()) + .ok(); + break; } } // generate build date let build_date = format!("{}", chrono::Local::now().format("%Y-%m-%d %H:%M")); - file.write_all(format!("pub const BUILD_DATE: &str = \"{}\";", build_date).as_bytes()) - .ok(); + file.write_all( + format!( + "#[allow(dead_code)]\npub const BUILD_DATE: &str = \"{}\";", + build_date + ) + .as_bytes(), + ) + .ok(); file.sync_all().ok(); } @@ -230,20 +240,20 @@ pub fn is_valid_custom_id(id: &str) -> bool { pub fn get_version_number(v: &str) -> i64 { let mut n = 0; - for x in v.split(".") { + for x in v.split('.') { n = n * 1000 + x.parse::().unwrap_or(0); } n } pub fn get_modified_time(path: &std::path::Path) -> SystemTime { - std::fs::metadata(&path) + std::fs::metadata(path) .map(|m| m.modified().unwrap_or(UNIX_EPOCH)) .unwrap_or(UNIX_EPOCH) } pub fn get_created_time(path: &std::path::Path) -> SystemTime { - std::fs::metadata(&path) + std::fs::metadata(path) .map(|m| m.created().unwrap_or(UNIX_EPOCH)) .unwrap_or(UNIX_EPOCH) } @@ -276,32 +286,6 @@ pub fn get_time() -> i64 { .unwrap_or(0) as _ } -#[cfg(test)] -mod tests { - use super::*; - #[test] - fn test_mangle() { - let addr = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(192, 168, 16, 32), 21116)); - assert_eq!(addr, AddrMangle::decode(&AddrMangle::encode(addr))); - - let addr = "[2001:db8::1]:8080".parse::().unwrap(); - assert_eq!(addr, AddrMangle::decode(&AddrMangle::encode(addr))); - - let addr = "[2001:db8:ff::1111]:80".parse::().unwrap(); - assert_eq!(addr, AddrMangle::decode(&AddrMangle::encode(addr))); - } - - #[test] - fn test_allow_err() { - allow_err!(Err("test err") as Result<(), &str>); - allow_err!( - Err("test err with msg") as Result<(), &str>, - "prompt {}", - "failed" - ); - } -} - #[inline] pub fn is_ipv4_str(id: &str) -> bool { regex::Regex::new(r"^\d+\.\d+\.\d+\.\d+(:\d+)?$") @@ -334,9 +318,31 @@ pub fn is_domain_port_str(id: &str) -> bool { } #[cfg(test)] -mod test_lib { +mod test { use super::*; + #[test] + fn test_mangle() { + let addr = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(192, 168, 16, 32), 21116)); + assert_eq!(addr, AddrMangle::decode(&AddrMangle::encode(addr))); + + let addr = "[2001:db8::1]:8080".parse::().unwrap(); + assert_eq!(addr, AddrMangle::decode(&AddrMangle::encode(addr))); + + let addr = "[2001:db8:ff::1111]:80".parse::().unwrap(); + assert_eq!(addr, AddrMangle::decode(&AddrMangle::encode(addr))); + } + + #[test] + fn test_allow_err() { + allow_err!(Err("test err") as Result<(), &str>); + allow_err!( + Err("test err with msg") as Result<(), &str>, + "prompt {}", + "failed" + ); + } + #[test] fn test_ipv6() { assert_eq!(is_ipv6_str("1:2:3"), true); @@ -373,4 +379,20 @@ mod test_lib { assert_eq!(is_domain_port_str("test.com:0"), true); assert_eq!(is_domain_port_str("test.com:98989"), true); } + + #[test] + fn test_mangle2() { + let addr = "[::ffff:127.0.0.1]:8080".parse().unwrap(); + let addr_v4 = "127.0.0.1:8080".parse().unwrap(); + assert_eq!(AddrMangle::decode(&AddrMangle::encode(addr)), addr_v4); + assert_eq!( + AddrMangle::decode(&AddrMangle::encode("[::127.0.0.1]:8080".parse().unwrap())), + addr_v4 + ); + assert_eq!(AddrMangle::decode(&AddrMangle::encode(addr_v4)), addr_v4); + let addr_v6 = "[ef::fe]:8080".parse().unwrap(); + assert_eq!(AddrMangle::decode(&AddrMangle::encode(addr_v6)), addr_v6); + let addr_v6 = "[::1]:8080".parse().unwrap(); + assert_eq!(AddrMangle::decode(&AddrMangle::encode(addr_v6)), addr_v6); + } } diff --git a/libs/hbb_common/src/password_security.rs b/libs/hbb_common/src/password_security.rs index 602906990..0b66107fc 100644 --- a/libs/hbb_common/src/password_security.rs +++ b/libs/hbb_common/src/password_security.rs @@ -104,7 +104,7 @@ pub fn decrypt_str_or_original(s: &str, current_version: &str) -> (String, bool, if s.len() > VERSION_LEN { let version = &s[..VERSION_LEN]; if version == "00" { - if let Ok(v) = decrypt(&s[VERSION_LEN..].as_bytes()) { + if let Ok(v) = decrypt(s[VERSION_LEN..].as_bytes()) { return ( String::from_utf8_lossy(&v).to_string(), true, @@ -149,7 +149,7 @@ pub fn decrypt_vec_or_original(v: &[u8], current_version: &str) -> (Vec, boo } fn encrypt(v: &[u8]) -> Result { - if v.len() > 0 { + if !v.is_empty() { symmetric_crypt(v, true).map(|v| base64::encode(v, base64::Variant::Original)) } else { Err(()) @@ -157,7 +157,7 @@ fn encrypt(v: &[u8]) -> Result { } fn decrypt(v: &[u8]) -> Result, ()> { - if v.len() > 0 { + if !v.is_empty() { base64::decode(v, base64::Variant::Original).and_then(|v| symmetric_crypt(&v, false)) } else { Err(()) diff --git a/libs/hbb_common/src/platform/linux.rs b/libs/hbb_common/src/platform/linux.rs index e82416309..716025dc7 100644 --- a/libs/hbb_common/src/platform/linux.rs +++ b/libs/hbb_common/src/platform/linux.rs @@ -32,7 +32,7 @@ pub fn get_display_server() -> String { // loginctl has not given the expected output. try something else. if let Ok(sid) = std::env::var("XDG_SESSION_ID") { // could also execute "cat /proc/self/sessionid" - session = sid.to_owned(); + session = sid; } if session.is_empty() { session = run_cmds("cat /proc/self/sessionid".to_owned()).unwrap_or_default(); @@ -63,7 +63,7 @@ fn get_display_server_of_session(session: &str) -> String { if let Ok(xorg_results) = run_cmds(format!("ps -e | grep \"{}.\\\\+Xorg\"", tty)) // And check if Xorg is running on that tty { - if xorg_results.trim_end().to_string() != "" { + if xorg_results.trim_end() != "" { // If it is, manually return "x11", otherwise return tty return "x11".to_owned(); } @@ -88,7 +88,7 @@ pub fn get_values_of_seat0(indices: Vec) -> Vec { if let Ok(output) = run_loginctl(None) { for line in String::from_utf8_lossy(&output.stdout).lines() { if line.contains("seat0") { - if let Some(sid) = line.split_whitespace().nth(0) { + if let Some(sid) = line.split_whitespace().next() { if is_active(sid) { return indices .into_iter() @@ -103,7 +103,7 @@ pub fn get_values_of_seat0(indices: Vec) -> Vec { // some case, there is no seat0 https://github.com/rustdesk/rustdesk/issues/73 if let Ok(output) = run_loginctl(None) { for line in String::from_utf8_lossy(&output.stdout).lines() { - if let Some(sid) = line.split_whitespace().nth(0) { + if let Some(sid) = line.split_whitespace().next() { let d = get_display_server_of_session(sid); if is_active(sid) && d != "tty" { return indices diff --git a/libs/hbb_common/src/socket_client.rs b/libs/hbb_common/src/socket_client.rs index 6f62163d1..a034b4e12 100644 --- a/libs/hbb_common/src/socket_client.rs +++ b/libs/hbb_common/src/socket_client.rs @@ -71,7 +71,7 @@ pub trait IsResolvedSocketAddr { impl IsResolvedSocketAddr for SocketAddr { fn resolve(&self) -> Option<&SocketAddr> { - Some(&self) + Some(self) } } @@ -120,12 +120,12 @@ pub async fn connect_tcp_local< if let Some(target) = target.resolve() { if let Some(local) = local { if local.is_ipv6() && target.is_ipv4() { - let target = query_nip_io(&target).await?; - return Ok(FramedStream::new(target, Some(local), ms_timeout).await?); + let target = query_nip_io(target).await?; + return FramedStream::new(target, Some(local), ms_timeout).await; } } } - Ok(FramedStream::new(target, local, ms_timeout).await?) + FramedStream::new(target, local, ms_timeout).await } #[inline] @@ -140,15 +140,14 @@ pub fn is_ipv4(target: &TargetAddr<'_>) -> bool { pub async fn query_nip_io(addr: &SocketAddr) -> ResultType { tokio::net::lookup_host(format!("{}.nip.io:{}", addr.ip(), addr.port())) .await? - .filter(|x| x.is_ipv6()) - .next() + .find(|x| x.is_ipv6()) .context("Failed to get ipv6 from nip.io") } #[inline] pub fn ipv4_to_ipv6(addr: String, ipv4: bool) -> String { if !ipv4 && crate::is_ipv4_str(&addr) { - if let Some(ip) = addr.split(":").next() { + if let Some(ip) = addr.split(':').next() { return addr.replace(ip, &format!("{}.nip.io", ip)); } } diff --git a/libs/hbb_common/src/tcp.rs b/libs/hbb_common/src/tcp.rs index a1322fc15..a7ac4eb3a 100644 --- a/libs/hbb_common/src/tcp.rs +++ b/libs/hbb_common/src/tcp.rs @@ -1,4 +1,5 @@ use crate::{bail, bytes_codec::BytesCodec, ResultType}; +use anyhow::Context as AnyhowCtx; use bytes::{BufMut, Bytes, BytesMut}; use futures::{SinkExt, StreamExt}; use protobuf::Message; @@ -209,7 +210,7 @@ impl FramedStream { if let Some(Ok(bytes)) = res.as_mut() { key.2 += 1; let nonce = Self::get_nonce(key.2); - match secretbox::open(&bytes, &nonce, &key.0) { + match secretbox::open(bytes, &nonce, &key.0) { Ok(res) => { bytes.clear(); bytes.put_slice(&res); @@ -245,16 +246,17 @@ impl FramedStream { const DEFAULT_BACKLOG: u32 = 128; -#[allow(clippy::never_loop)] pub async fn new_listener(addr: T, reuse: bool) -> ResultType { if !reuse { Ok(TcpListener::bind(addr).await?) } else { - for addr in lookup_host(&addr).await? { - let socket = new_socket(addr, true)?; - return Ok(socket.listen(DEFAULT_BACKLOG)?); - } - bail!("could not resolve to any address"); + let addr = lookup_host(&addr) + .await? + .next() + .context("could not resolve to any address")?; + new_socket(addr, true)? + .listen(DEFAULT_BACKLOG) + .map_err(anyhow::Error::msg) } } diff --git a/libs/hbb_common/src/udp.rs b/libs/hbb_common/src/udp.rs index 38121a4e1..bb0d071a2 100644 --- a/libs/hbb_common/src/udp.rs +++ b/libs/hbb_common/src/udp.rs @@ -1,11 +1,11 @@ -use crate::{bail, ResultType}; -use anyhow::anyhow; +use crate::ResultType; +use anyhow::{anyhow, Context}; use bytes::{Bytes, BytesMut}; use futures::{SinkExt, StreamExt}; use protobuf::Message; use socket2::{Domain, Socket, Type}; use std::net::SocketAddr; -use tokio::net::{ToSocketAddrs, UdpSocket}; +use tokio::net::{lookup_host, ToSocketAddrs, UdpSocket}; use tokio_socks::{udp::Socks5UdpFramed, IntoTargetAddr, TargetAddr, ToProxyAddrs}; use tokio_util::{codec::BytesCodec, udp::UdpFramed}; @@ -37,39 +37,31 @@ fn new_socket(addr: SocketAddr, reuse: bool, buf_size: usize) -> Result 0 { + socket.set_only_v6(false).ok(); + } socket.bind(&addr.into())?; Ok(socket) } impl FramedSocket { pub async fn new(addr: T) -> ResultType { - let socket = UdpSocket::bind(addr).await?; - Ok(Self::Direct(UdpFramed::new(socket, BytesCodec::new()))) + Self::new_reuse(addr, false, 0).await } - #[allow(clippy::never_loop)] - pub async fn new_reuse(addr: T) -> ResultType { - for addr in addr.to_socket_addrs()? { - let socket = new_socket(addr, true, 0)?.into_udp_socket(); - return Ok(Self::Direct(UdpFramed::new( - UdpSocket::from_std(socket)?, - BytesCodec::new(), - ))); - } - bail!("could not resolve to any address"); - } - - pub async fn new_with_buf_size( + pub async fn new_reuse( addr: T, + reuse: bool, buf_size: usize, ) -> ResultType { - for addr in addr.to_socket_addrs()? { - return Ok(Self::Direct(UdpFramed::new( - UdpSocket::from_std(new_socket(addr, false, buf_size)?.into_udp_socket())?, - BytesCodec::new(), - ))); - } - bail!("could not resolve to any address"); + let addr = lookup_host(&addr) + .await? + .next() + .context("could not resolve to any address")?; + Ok(Self::Direct(UdpFramed::new( + UdpSocket::from_std(new_socket(addr, reuse, buf_size)?.into_udp_socket())?, + BytesCodec::new(), + ))) } pub async fn new_proxy<'a, 't, P: ToProxyAddrs, T: ToSocketAddrs>( @@ -104,11 +96,12 @@ impl FramedSocket { ) -> ResultType<()> { let addr = addr.into_target_addr()?.to_owned(); let send_data = Bytes::from(msg.write_to_bytes()?); - let _ = match self { - Self::Direct(f) => match addr { - TargetAddr::Ip(addr) => f.send((send_data, addr)).await?, - _ => {} - }, + match self { + Self::Direct(f) => { + if let TargetAddr::Ip(addr) = addr { + f.send((send_data, addr)).await? + } + } Self::ProxySocks(f) => f.send((send_data, addr)).await?, }; Ok(()) @@ -123,11 +116,12 @@ impl FramedSocket { ) -> ResultType<()> { let addr = addr.into_target_addr()?.to_owned(); - let _ = match self { - Self::Direct(f) => match addr { - TargetAddr::Ip(addr) => f.send((Bytes::from(msg), addr)).await?, - _ => {} - }, + match self { + Self::Direct(f) => { + if let TargetAddr::Ip(addr) = addr { + f.send((Bytes::from(msg), addr)).await? + } + } Self::ProxySocks(f) => f.send((Bytes::from(msg), addr)).await?, }; Ok(()) @@ -165,12 +159,12 @@ impl FramedSocket { } } - pub fn is_ipv4(&self) -> bool { + pub fn local_addr(&self) -> Option { if let FramedSocket::Direct(x) = self { if let Ok(v) = x.get_ref().local_addr() { - return v.is_ipv4(); + return Some(v); } } - true + None } } From ab026d5055ec248c7b215cfa610b266b874dad57 Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 27 Jan 2023 13:03:35 +0800 Subject: [PATCH 1619/2015] fix unneccesary portable prompt window Signed-off-by: fufesou --- src/platform/windows.rs | 5 +++++ src/server/connection.rs | 2 +- src/ui_cm_interface.rs | 13 +++++-------- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 190834eb8..a77b92e07 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -839,6 +839,11 @@ pub fn check_update_broker_process() -> ResultType<()> { let cur_dir = exe_file.parent().unwrap(); let cur_exe = cur_dir.join(process_exe); + if !std::path::Path::new(&cur_exe).exists() { + std::fs::copy(origin_process_exe, cur_exe)?; + return Ok(()); + } + let ori_modified = fs::metadata(origin_process_exe)?.modified()?; if let Ok(metadata) = fs::metadata(&cur_exe) { if let Ok(cur_modified) = metadata.modified() { diff --git a/src/server/connection.rs b/src/server/connection.rs index cd5bd8cfa..c8814a3b1 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1274,7 +1274,7 @@ impl Connection { .retain(|_, v| v.0.elapsed() < SWITCH_SIDES_TIMEOUT); let uuid_old = SWITCH_SIDES_UUID.lock().unwrap().remove(&lr.my_id); if let Ok(uuid) = uuid::Uuid::from_slice(_s.uuid.to_vec().as_ref()) { - if let Some((instant, uuid_old)) = uuid_old { + if let Some((_instant, uuid_old)) = uuid_old { if uuid == uuid_old { self.from_switch = true; self.try_start_cm(lr.my_id.clone(), lr.my_name.clone(), true); diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index ea3553c8a..d620bcbc9 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -449,14 +449,11 @@ pub async fn start_ipc(cm: ConnectionManager) { #[cfg(windows)] std::thread::spawn(move || { log::info!("try create privacy mode window"); - #[cfg(windows)] - { - if let Err(e) = crate::platform::windows::check_update_broker_process() { - log::warn!( - "Failed to check update broker process. Privacy mode may not work properly. {}", - e - ); - } + if let Err(e) = crate::platform::windows::check_update_broker_process() { + log::warn!( + "Failed to check update broker process. Privacy mode may not work properly. {}", + e + ); } allow_err!(crate::ui::win_privacy::start()); }); From 6f9b3ae466dbb77dfcc140b7313336118f84d53e Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Fri, 27 Jan 2023 21:11:46 +0800 Subject: [PATCH 1620/2015] Revert "opt: upgrade flutter ci/nightly to 3.7.0 stable" --- .github/workflows/flutter-ci.yml | 25 ++++++++------------ .github/workflows/flutter-nightly.yml | 24 +++++++------------ flutter/lib/utils/multi_window_manager.dart | 11 +++++---- flutter/macos/Runner/MainFlutterWindow.swift | 2 +- flutter/pubspec.yaml | 10 ++++---- 5 files changed, 32 insertions(+), 40 deletions(-) diff --git a/.github/workflows/flutter-ci.yml b/.github/workflows/flutter-ci.yml index a2c67551d..4e98f311d 100644 --- a/.github/workflows/flutter-ci.yml +++ b/.github/workflows/flutter-ci.yml @@ -13,7 +13,8 @@ on: env: LLVM_VERSION: "10.0" - FLUTTER_VERSION: "3.7.0" + # Note: currently 3.0.5 does not support arm64 officially, we use latest stable version first. + FLUTTER_VERSION: "3.0.5" # vcpkg version: 2022.05.10 # for multiarch gcc compatibility VCPKG_COMMIT_ID: "14e7bb4ae24616ec54ff6b2f6ef4e8659434ea44" @@ -50,9 +51,9 @@ jobs: run: | flutter doctor -v flutter precache --windows - Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.7.0-rustdesk/windows-x64-release-flutter.zip -OutFile windows-x64-flutter-release.zip + Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.0.5-rustdesk.2/windows-x64-flutter-release.zip -OutFile windows-x64-flutter-release.zip Expand-Archive windows-x64-flutter-release.zip -DestinationPath engine - mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-${{ env.FLUTTER_VERSION }}-x64/bin/cache/artifacts/engine/windows-x64-release/ + mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-3.0.5-x64/bin/cache/artifacts/engine/windows-x64-release/ - name: Install Rust toolchain uses: actions-rs/toolchain@v1 @@ -852,12 +853,12 @@ jobs: # disable git safe.directory git config --global --add safe.directory "*" pushd /opt - # clone repo and reset to flutter 3.7.0 + # clone repo and reset to flutter 3.0.5 git clone https://github.com/sony/flutter-elinux.git || true pushd flutter-elinux - # reset to flutter 3.7.0 + # reset to flutter 3.0.5 git fetch - git reset --hard 51a1d685901f79fbac51665a967c3a1a789ecee5 + git reset --hard b09a90eee643859ce4e676839227edd9fd3feba8 popd - uses: Kingtous/run-on-arch-action@amd64-support @@ -882,17 +883,11 @@ jobs: git config --global --add safe.directory "*" pushd /workspace # we use flutter-elinux to build our rustdesk - export PATH=/opt/flutter-elinux/bin:$PATH sed -i "s/flutter build linux --release/flutter-elinux build linux/g" ./build.py - # Setup flutter-elinux. Run doctor to check if issues here. + # Setup flutter-elinux + export PATH=/opt/flutter-elinux/bin:$PATH flutter-elinux doctor -v - # Patch arm64 engine for flutter 3.6.0+ - flutter-elinux precache --linux - pushd /tmp - curl -O https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.7.0-stable.tar.xz - tar -xvf flutter_linux_3.7.0-stable.tar.xz flutter/bin/cache/artifacts/engine/linux-x64/shader_lib - cp -R flutter/bin/cache/artifacts/engine/linux-x64/shader_lib /opt/flutter-elinux/flutter/bin/cache/artifacts/engine/linux-arm64 - popd + # edit to corresponding arch case ${{ matrix.job.arch }} in aarch64) sed -i "s/Architecture: amd64/Architecture: arm64/g" ./build.py diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 896afd005..b33a6dba0 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -8,7 +8,8 @@ on: env: LLVM_VERSION: "10.0" - FLUTTER_VERSION: "3.7.0" + # Note: currently 3.0.5 does not support arm64 officially, we use latest stable version first. + FLUTTER_VERSION: "3.0.5" TAG_NAME: "nightly" # vcpkg version: 2022.05.10 # for multiarch gcc compatibility @@ -52,9 +53,9 @@ jobs: run: | flutter doctor -v flutter precache --windows - Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.7.0-rustdesk/windows-x64-release-flutter.zip -OutFile windows-x64-flutter-release.zip + Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.0.5-rustdesk.2/windows-x64-flutter-release.zip -OutFile windows-x64-flutter-release.zip Expand-Archive windows-x64-flutter-release.zip -DestinationPath engine - mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-${{ env.FLUTTER_VERSION }}-x64/bin/cache/artifacts/engine/windows-x64-release/ + mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-3.0.5-x64/bin/cache/artifacts/engine/windows-x64-release/ - name: Install Rust toolchain uses: actions-rs/toolchain@v1 @@ -998,12 +999,12 @@ jobs: # disable git safe.directory git config --global --add safe.directory "*" pushd /opt - # clone repo and reset to flutter 3.7.0 + # clone repo and reset to flutter 3.0.5 git clone https://github.com/sony/flutter-elinux.git || true pushd flutter-elinux - # reset to flutter 3.7.0 + # reset to flutter 3.0.5 git fetch - git reset --hard 51a1d685901f79fbac51665a967c3a1a789ecee5 + git reset --hard b09a90eee643859ce4e676839227edd9fd3feba8 popd - uses: Kingtous/run-on-arch-action@amd64-support @@ -1028,17 +1029,10 @@ jobs: git config --global --add safe.directory "*" pushd /workspace # we use flutter-elinux to build our rustdesk - export PATH=/opt/flutter-elinux/bin:$PATH sed -i "s/flutter build linux --release/flutter-elinux build linux/g" ./build.py - # Setup flutter-elinux. Run doctor to check if issues here. + # Setup flutter-elinux + export PATH=/opt/flutter-elinux/bin:$PATH flutter-elinux doctor -v - # Patch arm64 engine for flutter 3.6.0+ - flutter-elinux precache --linux - pushd /tmp - curl -O https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.7.0-stable.tar.xz - tar -xvf flutter_linux_3.7.0-stable.tar.xz flutter/bin/cache/artifacts/engine/linux-x64/shader_lib - cp -R flutter/bin/cache/artifacts/engine/linux-x64/shader_lib /opt/flutter-elinux/flutter/bin/cache/artifacts/engine/linux-arm64 - popd # edit to corresponding arch case ${{ matrix.job.arch }} in aarch64) diff --git a/flutter/lib/utils/multi_window_manager.dart b/flutter/lib/utils/multi_window_manager.dart index 7914a4c0a..ee19ac485 100644 --- a/flutter/lib/utils/multi_window_manager.dart +++ b/flutter/lib/utils/multi_window_manager.dart @@ -63,7 +63,8 @@ class RustDeskMultiWindowManager { ..setFrame(const Offset(0, 0) & const Size(1280, 720)) ..center() ..setTitle(getWindowNameWithId(remoteId, - overrideType: WindowType.RemoteDesktop)); + overrideType: WindowType.RemoteDesktop)) + ..show(); registerActiveWindow(remoteDesktopController.windowId); _remoteDesktopWindowId = remoteDesktopController.windowId; } else { @@ -89,7 +90,8 @@ class RustDeskMultiWindowManager { ..setFrame(const Offset(0, 0) & const Size(1280, 720)) ..center() ..setTitle(getWindowNameWithId(remoteId, - overrideType: WindowType.FileTransfer)); + overrideType: WindowType.FileTransfer)) + ..show(); registerActiveWindow(fileTransferController.windowId); _fileTransferWindowId = fileTransferController.windowId; } else { @@ -114,8 +116,9 @@ class RustDeskMultiWindowManager { portForwardController ..setFrame(const Offset(0, 0) & const Size(1280, 720)) ..center() - ..setTitle(getWindowNameWithId(remoteId, - overrideType: WindowType.PortForward)); + ..setTitle( + getWindowNameWithId(remoteId, overrideType: WindowType.PortForward)) + ..show(); registerActiveWindow(portForwardController.windowId); _portForwardWindowId = portForwardController.windowId; } else { diff --git a/flutter/macos/Runner/MainFlutterWindow.swift b/flutter/macos/Runner/MainFlutterWindow.swift index 108f5a5f8..540cd9ab9 100644 --- a/flutter/macos/Runner/MainFlutterWindow.swift +++ b/flutter/macos/Runner/MainFlutterWindow.swift @@ -7,7 +7,7 @@ import desktop_drop import device_info_plus_macos import flutter_custom_cursor import package_info_plus_macos -import path_provider_foundation +import path_provider_macos import screen_retriever import sqflite // import tray_manager diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 0189ad9e4..a5535c8b7 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -31,7 +31,7 @@ dependencies: # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.3 ffi: ^2.0.1 - path_provider: ^2.0.12 + path_provider: ^2.0.2 external_path: ^1.0.1 provider: ^6.0.3 tuple: ^2.0.0 @@ -75,14 +75,14 @@ dependencies: debounce_throttle: ^2.0.0 file_picker: ^5.1.0 flutter_svg: ^1.1.5 - flutter_improved_scrolling: + flutter_improved_scrolling: ^0.0.3 # currently, we use flutter 3.0.5 for windows build, latest for other builds. # # for flutter 3.0.5, please use official version(just comment code below). # if build rustdesk by flutter >=3.3, please use our custom pub below (uncomment code below). - git: - url: https://github.com/Kingtous/flutter_improved_scrolling - ref: 62f09545149f320616467c306c8c5f71714a18e6 + # git: + # url: https://github.com/Kingtous/flutter_improved_scrolling + # ref: 62f09545149f320616467c306c8c5f71714a18e6 uni_links: ^0.5.1 uni_links_desktop: ^0.1.4 path: ^1.8.1 From a529b14f2dc994d4e615c5333cb9f01d100ed1e1 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 27 Jan 2023 23:11:03 +0800 Subject: [PATCH 1621/2015] peer card ActionMore from MouseReigon to InkWell to show hand pointer --- flutter/lib/common/widgets/peer_card.dart | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index 3feaef51d..c9af6328c 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -1092,21 +1092,21 @@ Widget getOnline(double rightPadding, bool online) { } class ActionMore extends StatelessWidget { - final RxBool _iconMoreHover = false.obs; + final RxBool _hover = false.obs; @override Widget build(BuildContext context) { - return MouseRegion( - onEnter: (_) => _iconMoreHover.value = true, - onExit: (_) => _iconMoreHover.value = false, + return InkWell( + onTap: () {}, + onHover: (value) => _hover.value = value, child: Obx(() => CircleAvatar( radius: 14, - backgroundColor: _iconMoreHover.value + backgroundColor: _hover.value ? Theme.of(context).scaffoldBackgroundColor : Theme.of(context).backgroundColor, child: Icon(Icons.more_vert, size: 18, - color: _iconMoreHover.value + color: _hover.value ? Theme.of(context).textTheme.titleLarge?.color : Theme.of(context) .textTheme From bf04a03cd120ecfa692dd94c6ade220f46cc5c40 Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 27 Jan 2023 23:45:07 +0800 Subject: [PATCH 1622/2015] fix win, local detect some dead code Signed-off-by: fufesou --- Cargo.lock | 2 +- src/flutter_ffi.rs | 8 +++++++- src/keyboard.rs | 11 +++++++++++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 693ae7d4c..5c4af56e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4371,7 +4371,7 @@ dependencies = [ [[package]] name = "rdev" version = "0.5.0-2" -source = "git+https://github.com/fufesou/rdev#1be26c7e8ed0d43cebdd8331d467bb61130a2e6e" +source = "git+https://github.com/fufesou/rdev#238c9778da40056e2efda1e4264355bc89fb6358" dependencies = [ "cocoa", "core-foundation 0.9.3", diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 992fff853..bcfafe9c2 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -245,8 +245,13 @@ pub fn session_get_keyboard_mode(id: String) -> Option { } pub fn session_set_keyboard_mode(id: String, value: String) { + let mut mode_updated = false; if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) { session.save_keyboard_mode(value); + mode_updated = true; + } + if mode_updated { + crate::keyboard::update_grab_get_key_name(); } } @@ -1182,7 +1187,8 @@ pub fn main_update_me() -> SyncReturn { } pub fn set_cur_session_id(id: String) { - super::flutter::set_cur_session_id(id) + super::flutter::set_cur_session_id(id); + crate::keyboard::update_grab_get_key_name(); } pub fn install_show_run_without_install() -> SyncReturn { diff --git a/src/keyboard.rs b/src/keyboard.rs index de1abd231..054a39580 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -64,6 +64,8 @@ pub mod client { match state { GrabState::Ready => {} GrabState::Run => { + #[cfg(windows)] + update_grab_get_key_name(); #[cfg(any(target_os = "windows", target_os = "macos"))] KEYBOARD_HOOKED.swap(true, Ordering::SeqCst); @@ -184,6 +186,15 @@ 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), + _ => {} + }; +} + pub fn start_grab_loop() { #[cfg(any(target_os = "windows", target_os = "macos"))] std::thread::spawn(move || { From 7c865a80e943f7b82db25dcd61e2be689446ba9f Mon Sep 17 00:00:00 2001 From: fufesou Date: Sat, 28 Jan 2023 09:36:36 +0800 Subject: [PATCH 1623/2015] fix build Signed-off-by: fufesou --- src/flutter_ffi.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index bcfafe9c2..c30c6c847 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -245,12 +245,13 @@ pub fn session_get_keyboard_mode(id: String) -> Option { } pub fn session_set_keyboard_mode(id: String, value: String) { - let mut mode_updated = false; + let mut _mode_updated = false; if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) { session.save_keyboard_mode(value); - mode_updated = true; + _mode_updated = true; } - if mode_updated { + #[cfg(windows)] + if _mode_updated { crate::keyboard::update_grab_get_key_name(); } } @@ -1188,6 +1189,7 @@ pub fn main_update_me() -> SyncReturn { pub fn set_cur_session_id(id: String) { super::flutter::set_cur_session_id(id); + #[cfg(windows)] crate::keyboard::update_grab_get_key_name(); } From a17f14b92abca34e01115dc6016909c5534735fd Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sat, 28 Jan 2023 09:41:05 +0800 Subject: [PATCH 1624/2015] opt: upgrade flutter ci/nightly to 3.7.0 stable This reverts commit 6f9b3ae466dbb77dfcc140b7313336118f84d53e. --- .github/workflows/flutter-ci.yml | 25 ++++++++++++-------- .github/workflows/flutter-nightly.yml | 24 ++++++++++++------- flutter/lib/utils/multi_window_manager.dart | 11 ++++----- flutter/macos/Runner/MainFlutterWindow.swift | 2 +- flutter/pubspec.yaml | 10 ++++---- 5 files changed, 40 insertions(+), 32 deletions(-) diff --git a/.github/workflows/flutter-ci.yml b/.github/workflows/flutter-ci.yml index 4e98f311d..a2c67551d 100644 --- a/.github/workflows/flutter-ci.yml +++ b/.github/workflows/flutter-ci.yml @@ -13,8 +13,7 @@ on: env: LLVM_VERSION: "10.0" - # Note: currently 3.0.5 does not support arm64 officially, we use latest stable version first. - FLUTTER_VERSION: "3.0.5" + FLUTTER_VERSION: "3.7.0" # vcpkg version: 2022.05.10 # for multiarch gcc compatibility VCPKG_COMMIT_ID: "14e7bb4ae24616ec54ff6b2f6ef4e8659434ea44" @@ -51,9 +50,9 @@ jobs: run: | flutter doctor -v flutter precache --windows - Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.0.5-rustdesk.2/windows-x64-flutter-release.zip -OutFile windows-x64-flutter-release.zip + Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.7.0-rustdesk/windows-x64-release-flutter.zip -OutFile windows-x64-flutter-release.zip Expand-Archive windows-x64-flutter-release.zip -DestinationPath engine - mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-3.0.5-x64/bin/cache/artifacts/engine/windows-x64-release/ + mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-${{ env.FLUTTER_VERSION }}-x64/bin/cache/artifacts/engine/windows-x64-release/ - name: Install Rust toolchain uses: actions-rs/toolchain@v1 @@ -853,12 +852,12 @@ jobs: # disable git safe.directory git config --global --add safe.directory "*" pushd /opt - # clone repo and reset to flutter 3.0.5 + # clone repo and reset to flutter 3.7.0 git clone https://github.com/sony/flutter-elinux.git || true pushd flutter-elinux - # reset to flutter 3.0.5 + # reset to flutter 3.7.0 git fetch - git reset --hard b09a90eee643859ce4e676839227edd9fd3feba8 + git reset --hard 51a1d685901f79fbac51665a967c3a1a789ecee5 popd - uses: Kingtous/run-on-arch-action@amd64-support @@ -883,11 +882,17 @@ jobs: git config --global --add safe.directory "*" pushd /workspace # we use flutter-elinux to build our rustdesk - sed -i "s/flutter build linux --release/flutter-elinux build linux/g" ./build.py - # Setup flutter-elinux export PATH=/opt/flutter-elinux/bin:$PATH + sed -i "s/flutter build linux --release/flutter-elinux build linux/g" ./build.py + # Setup flutter-elinux. Run doctor to check if issues here. flutter-elinux doctor -v - # edit to corresponding arch + # Patch arm64 engine for flutter 3.6.0+ + flutter-elinux precache --linux + pushd /tmp + curl -O https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.7.0-stable.tar.xz + tar -xvf flutter_linux_3.7.0-stable.tar.xz flutter/bin/cache/artifacts/engine/linux-x64/shader_lib + cp -R flutter/bin/cache/artifacts/engine/linux-x64/shader_lib /opt/flutter-elinux/flutter/bin/cache/artifacts/engine/linux-arm64 + popd case ${{ matrix.job.arch }} in aarch64) sed -i "s/Architecture: amd64/Architecture: arm64/g" ./build.py diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index b33a6dba0..896afd005 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -8,8 +8,7 @@ on: env: LLVM_VERSION: "10.0" - # Note: currently 3.0.5 does not support arm64 officially, we use latest stable version first. - FLUTTER_VERSION: "3.0.5" + FLUTTER_VERSION: "3.7.0" TAG_NAME: "nightly" # vcpkg version: 2022.05.10 # for multiarch gcc compatibility @@ -53,9 +52,9 @@ jobs: run: | flutter doctor -v flutter precache --windows - Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.0.5-rustdesk.2/windows-x64-flutter-release.zip -OutFile windows-x64-flutter-release.zip + Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.7.0-rustdesk/windows-x64-release-flutter.zip -OutFile windows-x64-flutter-release.zip Expand-Archive windows-x64-flutter-release.zip -DestinationPath engine - mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-3.0.5-x64/bin/cache/artifacts/engine/windows-x64-release/ + mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-${{ env.FLUTTER_VERSION }}-x64/bin/cache/artifacts/engine/windows-x64-release/ - name: Install Rust toolchain uses: actions-rs/toolchain@v1 @@ -999,12 +998,12 @@ jobs: # disable git safe.directory git config --global --add safe.directory "*" pushd /opt - # clone repo and reset to flutter 3.0.5 + # clone repo and reset to flutter 3.7.0 git clone https://github.com/sony/flutter-elinux.git || true pushd flutter-elinux - # reset to flutter 3.0.5 + # reset to flutter 3.7.0 git fetch - git reset --hard b09a90eee643859ce4e676839227edd9fd3feba8 + git reset --hard 51a1d685901f79fbac51665a967c3a1a789ecee5 popd - uses: Kingtous/run-on-arch-action@amd64-support @@ -1029,10 +1028,17 @@ jobs: git config --global --add safe.directory "*" pushd /workspace # we use flutter-elinux to build our rustdesk - sed -i "s/flutter build linux --release/flutter-elinux build linux/g" ./build.py - # Setup flutter-elinux export PATH=/opt/flutter-elinux/bin:$PATH + sed -i "s/flutter build linux --release/flutter-elinux build linux/g" ./build.py + # Setup flutter-elinux. Run doctor to check if issues here. flutter-elinux doctor -v + # Patch arm64 engine for flutter 3.6.0+ + flutter-elinux precache --linux + pushd /tmp + curl -O https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.7.0-stable.tar.xz + tar -xvf flutter_linux_3.7.0-stable.tar.xz flutter/bin/cache/artifacts/engine/linux-x64/shader_lib + cp -R flutter/bin/cache/artifacts/engine/linux-x64/shader_lib /opt/flutter-elinux/flutter/bin/cache/artifacts/engine/linux-arm64 + popd # edit to corresponding arch case ${{ matrix.job.arch }} in aarch64) diff --git a/flutter/lib/utils/multi_window_manager.dart b/flutter/lib/utils/multi_window_manager.dart index ee19ac485..7914a4c0a 100644 --- a/flutter/lib/utils/multi_window_manager.dart +++ b/flutter/lib/utils/multi_window_manager.dart @@ -63,8 +63,7 @@ class RustDeskMultiWindowManager { ..setFrame(const Offset(0, 0) & const Size(1280, 720)) ..center() ..setTitle(getWindowNameWithId(remoteId, - overrideType: WindowType.RemoteDesktop)) - ..show(); + overrideType: WindowType.RemoteDesktop)); registerActiveWindow(remoteDesktopController.windowId); _remoteDesktopWindowId = remoteDesktopController.windowId; } else { @@ -90,8 +89,7 @@ class RustDeskMultiWindowManager { ..setFrame(const Offset(0, 0) & const Size(1280, 720)) ..center() ..setTitle(getWindowNameWithId(remoteId, - overrideType: WindowType.FileTransfer)) - ..show(); + overrideType: WindowType.FileTransfer)); registerActiveWindow(fileTransferController.windowId); _fileTransferWindowId = fileTransferController.windowId; } else { @@ -116,9 +114,8 @@ class RustDeskMultiWindowManager { portForwardController ..setFrame(const Offset(0, 0) & const Size(1280, 720)) ..center() - ..setTitle( - getWindowNameWithId(remoteId, overrideType: WindowType.PortForward)) - ..show(); + ..setTitle(getWindowNameWithId(remoteId, + overrideType: WindowType.PortForward)); registerActiveWindow(portForwardController.windowId); _portForwardWindowId = portForwardController.windowId; } else { diff --git a/flutter/macos/Runner/MainFlutterWindow.swift b/flutter/macos/Runner/MainFlutterWindow.swift index 540cd9ab9..108f5a5f8 100644 --- a/flutter/macos/Runner/MainFlutterWindow.swift +++ b/flutter/macos/Runner/MainFlutterWindow.swift @@ -7,7 +7,7 @@ import desktop_drop import device_info_plus_macos import flutter_custom_cursor import package_info_plus_macos -import path_provider_macos +import path_provider_foundation import screen_retriever import sqflite // import tray_manager diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index a5535c8b7..0189ad9e4 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -31,7 +31,7 @@ dependencies: # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.3 ffi: ^2.0.1 - path_provider: ^2.0.2 + path_provider: ^2.0.12 external_path: ^1.0.1 provider: ^6.0.3 tuple: ^2.0.0 @@ -75,14 +75,14 @@ dependencies: debounce_throttle: ^2.0.0 file_picker: ^5.1.0 flutter_svg: ^1.1.5 - flutter_improved_scrolling: ^0.0.3 + flutter_improved_scrolling: # currently, we use flutter 3.0.5 for windows build, latest for other builds. # # for flutter 3.0.5, please use official version(just comment code below). # if build rustdesk by flutter >=3.3, please use our custom pub below (uncomment code below). - # git: - # url: https://github.com/Kingtous/flutter_improved_scrolling - # ref: 62f09545149f320616467c306c8c5f71714a18e6 + git: + url: https://github.com/Kingtous/flutter_improved_scrolling + ref: 62f09545149f320616467c306c8c5f71714a18e6 uni_links: ^0.5.1 uni_links_desktop: ^0.1.4 path: ^1.8.1 From ef614d69c05ad213f239fc1811bbc4f8bd210d6c Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sat, 28 Jan 2023 09:33:57 +0800 Subject: [PATCH 1625/2015] fix: macos subwindow wont open --- flutter/lib/utils/multi_window_manager.dart | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/flutter/lib/utils/multi_window_manager.dart b/flutter/lib/utils/multi_window_manager.dart index 7914a4c0a..224052bff 100644 --- a/flutter/lib/utils/multi_window_manager.dart +++ b/flutter/lib/utils/multi_window_manager.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:io'; import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/foundation.dart'; @@ -64,6 +65,9 @@ class RustDeskMultiWindowManager { ..center() ..setTitle(getWindowNameWithId(remoteId, overrideType: WindowType.RemoteDesktop)); + if (Platform.isMacOS) { + Future.microtask(() => remoteDesktopController.show()); + } registerActiveWindow(remoteDesktopController.windowId); _remoteDesktopWindowId = remoteDesktopController.windowId; } else { @@ -90,6 +94,9 @@ class RustDeskMultiWindowManager { ..center() ..setTitle(getWindowNameWithId(remoteId, overrideType: WindowType.FileTransfer)); + if (Platform.isMacOS) { + Future.microtask(() => fileTransferController.show()); + } registerActiveWindow(fileTransferController.windowId); _fileTransferWindowId = fileTransferController.windowId; } else { @@ -116,6 +123,9 @@ class RustDeskMultiWindowManager { ..center() ..setTitle(getWindowNameWithId(remoteId, overrideType: WindowType.PortForward)); + if (Platform.isMacOS) { + Future.microtask(() => portForwardController.show()); + } registerActiveWindow(portForwardController.windowId); _portForwardWindowId = portForwardController.windowId; } else { From 435e7749641369230adeaf79d708478c1756e599 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sat, 28 Jan 2023 10:39:13 +0800 Subject: [PATCH 1626/2015] fix switch sides delay #2893 Signed-off-by: 21pages --- flutter/lib/desktop/widgets/remote_menubar.dart | 3 +-- src/server/connection.rs | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 227002645..62289d5f0 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -652,8 +652,7 @@ class _RemoteMenubarState extends State { dismissOnClicked: true, )); } - if (false && - pi.platform != kPeerPlatformAndroid && + if (pi.platform != kPeerPlatformAndroid && version_cmp(peer_version, '1.2.0') >= 0) { displayMenu.add(MenuEntryButton( childBuilder: (TextStyle? style) => Text( diff --git a/src/server/connection.rs b/src/server/connection.rs index c8814a3b1..d3f7ac149 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1583,6 +1583,7 @@ impl Connection { uuid.to_string().as_ref(), ]) .ok(); + self.on_close_manually("switch sides", "peer"); return false; } } From d0d926bfb00caa189462c74f055588aff5c7c73b Mon Sep 17 00:00:00 2001 From: 21pages Date: Sat, 28 Jan 2023 12:02:14 +0800 Subject: [PATCH 1627/2015] opt connection close Signed-off-by: 21pages --- src/client.rs | 1 + 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 | 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 | 45 ++++++++++++++++++++++++++++++++++++---- 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/server/connection.rs | 35 +++++++++++++++++-------------- 34 files changed, 124 insertions(+), 50 deletions(-) diff --git a/src/client.rs b/src/client.rs index e9b8edf39..a6df6dbec 100644 --- a/src/client.rs +++ b/src/client.rs @@ -2083,6 +2083,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"))) } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 72f55b44b..3c8df31b4 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -407,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", ""), ("Group", ""), ("Search", ""), - ("Closed manually by the web console", ""), + ("Closed manually by web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), ("software_render_tip", ""), @@ -433,5 +433,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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 14e8a463d..537313e97 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -407,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", "添加到地址簿"), ("Group", "小组"), ("Search", "搜索"), - ("Closed manually by the web console", "被web控制台手动关闭"), + ("Closed manually by web console", "被web控制台手动关闭"), ("Local keyboard type", "本地键盘类型"), ("Select local keyboard type", "请选择本地键盘类型"), ("software_render_tip", "如果你使用英伟达显卡, 并且远程窗口在会话建立后会立刻关闭, 那么安装nouveau驱动并且选择使用软件渲染可能会有帮助。重启软件后生效。"), @@ -433,5 +433,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", "正常关闭"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index e2935770c..d5a65cdbb 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -407,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", ""), ("Group", ""), ("Search", ""), - ("Closed manually by the web console", ""), + ("Closed manually by web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), ("software_render_tip", ""), @@ -433,5 +433,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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 937990ea8..eda3b8a58 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -407,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", ""), ("Group", ""), ("Search", ""), - ("Closed manually by the web console", ""), + ("Closed manually by web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), ("software_render_tip", ""), @@ -433,5 +433,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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index a567877a2..774cac7e6 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -407,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", "Zum Adressbuch hinzufügen"), ("Group", "Gruppe"), ("Search", "Suchen"), - ("Closed manually by the web console", "Manuell über die Webkonsole beendet"), + ("Closed manually by web console", "Manuell über die Webkonsole beendet"), ("Local keyboard type", "Lokaler Tastaturtyp"), ("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."), @@ -433,5 +433,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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 839c69bbb..872cb30ec 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -407,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", ""), ("Group", ""), ("Search", ""), - ("Closed manually by the web console", ""), + ("Closed manually by web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), ("software_render_tip", ""), @@ -433,5 +433,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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 2b109c18f..b7cb52804 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -407,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", "Añadir a la libreta de direcciones"), ("Group", "Grupo"), ("Search", "Búsqueda"), - ("Closed manually by the web console", "Cerrado manualmente por la consola web"), + ("Closed manually by web console", "Cerrado manualmente por la consola web"), ("Local keyboard type", "Tipo de teclado local"), ("Select local keyboard type", "Seleccionar tipo de teclado local"), ("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."), @@ -433,5 +433,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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index da354579b..52ccf3786 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -407,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", "افزودن به دفترچه آدرس"), ("Group", "گروه"), ("Search", "جستجو"), - ("Closed manually by the web console", "به صورت دستی توسط کنسول وب بسته شد"), + ("Closed manually by web console", "به صورت دستی توسط کنسول وب بسته شد"), ("Local keyboard type", "نوع صفحه کلید محلی"), ("Select local keyboard type", "نوع صفحه کلید محلی را انتخاب کنید"), ("software_render_tip", "اگر کارت گرافیک Nvidia دارید و پنجره راه دور بلافاصله پس از اتصال بسته می شود، درایور nouveau را نصب نمایید و انتخاب گزینه استفاده از رندر نرم افزار می تواند کمک کننده باشد. راه اندازی مجدد نرم افزار مورد نیاز است."), @@ -433,5 +433,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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 9f1f23205..2feb026ff 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -407,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", "Ajouter au carnet d'adresses"), ("Group", "Groupe"), ("Search", "Rechercher"), - ("Closed manually by the web console", "Fermé manuellement par la console Web"), + ("Closed manually by web console", "Fermé manuellement par la console Web"), ("Local keyboard type", "Disposition du clavier local"), ("Select local keyboard type", "Selectionner la disposition du clavier local"), ("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."), @@ -433,5 +433,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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/gr.rs b/src/lang/gr.rs index 6ec1152cd..8398fb72a 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -407,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", "Προσθήκη στο Βιβλίο Διευθύνσεων"), ("Group", "Ομάδα"), ("Search", "Αναζήτηση"), - ("Closed manually by the web console", "Κλειστό χειροκίνητα από την κονσόλα web"), + ("Closed manually by web console", "Κλειστό χειροκίνητα από την κονσόλα web"), ("Local keyboard type", "Τύπος τοπικού πληκτρολογίου"), ("Select local keyboard type", "Επιλογή τύπου τοπικού πληκτρολογίου"), ("software_render_tip", "Εάν έχετε κάρτα γραφικών Nvidia και το παράθυρο σύνδεσης κλείνει αμέσως μετά τη σύνδεση, η εγκατάσταση του προγράμματος οδήγησης nouveau και η επιλογή χρήσης της επιτάχυνσης γραφικών μέσω λογισμικού μπορεί να βοηθήσει. Απαιτείται επανεκκίνηση."), @@ -433,5 +433,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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 295104a67..96eb63656 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -407,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", ""), ("Group", ""), ("Search", ""), - ("Closed manually by the web console", ""), + ("Closed manually by web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), ("software_render_tip", ""), @@ -433,5 +433,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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 5604a0c52..b966b7af9 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -407,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", ""), ("Group", ""), ("Search", "Pencarian"), - ("Closed manually by the web console", ""), + ("Closed manually by web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), ("software_render_tip", ""), @@ -433,5 +433,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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 7b979aff0..c144d7863 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -407,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", "Aggiungi alla rubrica"), ("Group", "Gruppo"), ("Search", "Cerca"), - ("Closed manually by the web console", "Chiudi manualmente dalla console Web"), + ("Closed manually by web console", "Chiudi manualmente dalla console Web"), ("Local keyboard type", "Tipo di tastiera locale"), ("Select local keyboard type", "Seleziona il tipo di tastiera locale"), ("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."), @@ -433,5 +433,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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index a280940c7..0466c48a9 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -407,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", ""), ("Group", ""), ("Search", ""), - ("Closed manually by the web console", ""), + ("Closed manually by web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), ("software_render_tip", ""), @@ -433,5 +433,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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 1cdf529ce..c0d0bec8a 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -407,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", ""), ("Group", ""), ("Search", ""), - ("Closed manually by the web console", ""), + ("Closed manually by web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), ("software_render_tip", ""), @@ -433,5 +433,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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 59d26135f..fd8b520f4 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -407,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", ""), ("Group", ""), ("Search", ""), - ("Closed manually by the web console", ""), + ("Closed manually by web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), ("software_render_tip", ""), @@ -433,5 +433,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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index ee4b45334..8853afe5a 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -407,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", "Dodaj do Książki Adresowej"), ("Group", "Grypy"), ("Search", "Szukaj"), - ("Closed manually by the web console", ""), + ("Closed manually by web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), ("software_render_tip", ""), @@ -433,5 +433,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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 66373a5e9..4a391c3fb 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -407,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", ""), ("Group", ""), ("Search", ""), - ("Closed manually by the web console", ""), + ("Closed manually by web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), ("software_render_tip", ""), @@ -433,5 +433,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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 5a137f391..b4a46c599 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -407,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", ""), ("Group", ""), ("Search", ""), - ("Closed manually by the web console", ""), + ("Closed manually by web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), ("software_render_tip", ""), @@ -433,5 +433,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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index c5a9b529c..148723a5b 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -39,6 +39,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Schimbă ID"), ("Website", "Site web"), ("About", "Despre"), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", "Fără sunet"), ("Audio Input", "Intrare audio"), ("Enhancements", "Îmbunătățiri"), @@ -116,7 +118,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Good image quality", "Calitate bună a imaginii"), ("Balanced", "Calitate normală a imaginii"), ("Optimize reaction time", "Optimizează timpul de reacție"), - ("Custom", "Personalizare"), + ("Custom", "Personalizat"), ("Show remote cursor", "Afișează cursor la distanță"), ("Show quality monitor", "Afișează indicator de calitate"), ("Disable clipboard", "Dezactivează clipboard"), @@ -208,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Se conectează mereu prin retransmisie"), ("whitelist_tip", "Doar adresele IP autorizate pot accesa acest dispozitiv"), ("Login", "Conectare"), + ("Verify", ""), + ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "Deconectare"), ("Tags", "Etichetare"), ("Search ID", "Caută după ID"), @@ -332,7 +339,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Scală adaptivă"), ("General", "General"), ("Security", "Securitate"), - ("Account", "Cont"), ("Theme", "Temă"), ("Dark Theme", "Temă întunecată"), ("Dark", "Întunecat"), @@ -345,7 +351,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", "Server"), ("Direct IP Access", "Acces direct IP"), ("Proxy", "Proxy"), - ("Port", "Port"), ("Apply", "Aplică"), ("Disconnect all devices?", "Vrei să deconectezi toate dispozitivele?"), ("Clear", "Golește"), @@ -374,7 +379,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", "Altele"), ("Confirm before closing multiple tabs", "Confirmă înainte de a închide mai multe file"), ("Keyboard Settings", "Configurare tastatură"), - ("Custom", "Personalizat"), ("Full Access", "Acces total"), ("Screen Share", "Partajare ecran"), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland necesită Ubuntu 21.04 sau o versiune superioară."), @@ -397,5 +401,38 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request access to your device", "Solicită acces la dispozitivul tău"), ("Hide connection management window", "Ascunde fereastra de gestionare a conexiunilor"), ("hide_cm_tip", "Permite ascunderea ferestrei de gestionare doar dacă accepți începerea sesiunilor folosind parola permanentă"), + ("wayland_experiment_tip", ""), + ("Right click to select tabs", ""), + ("Skipped", ""), + ("Add to Address Book", ""), + ("Group", ""), + ("Search", ""), + ("Closed manually by web console", ""), + ("Local keyboard type", ""), + ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), + ("request_elevation_tip", ""), + ("Wait", ""), + ("Elevation Error", ""), + ("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", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), + ("Closed as expected", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 8103ae3a3..8e4411cb1 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -407,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", "Добавить в адресную книгу"), ("Group", "Группа"), ("Search", "Поиск"), - ("Closed manually by the web console", "Закрыто вручную через веб-консоль"), + ("Closed manually by web console", "Закрыто вручную через веб-консоль"), ("Local keyboard type", "Тип локальной клавиатуры"), ("Select local keyboard type", "Выберите тип локальной клавиатуры"), ("software_render_tip", "Если у вас видеокарта Nvidia и удалённое окно закрывается сразу после подключения, может помочь установка драйвера Nouveau и выбор использования программной визуализации. Потребуется перезапуск."), @@ -433,5 +433,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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index c735cb28c..582cb58ae 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -407,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", ""), ("Group", ""), ("Search", ""), - ("Closed manually by the web console", ""), + ("Closed manually by web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), ("software_render_tip", ""), @@ -433,5 +433,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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 6a17cc906..cc35e3f3f 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -407,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", "Dodaj v adresar"), ("Group", "Skupina"), ("Search", "Iskanje"), - ("Closed manually by the web console", "Ročno zaprto iz spletne konzole"), + ("Closed manually by web console", "Ročno zaprto iz spletne konzole"), ("Local keyboard type", "Lokalna vrsta tipkovnice"), ("Select local keyboard type", "Izberite lokalno vrsto tipkovnice"), ("software_render_tip", ""), @@ -433,5 +433,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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index ebb43f6b7..3f11d72e1 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -407,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", ""), ("Group", ""), ("Search", ""), - ("Closed manually by the web console", ""), + ("Closed manually by web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), ("software_render_tip", ""), @@ -433,5 +433,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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index d9463318d..96ffa4d84 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -407,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", "Dodaj u adresar"), ("Group", "Grupa"), ("Search", "Pretraga"), - ("Closed manually by the web console", ""), + ("Closed manually by web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), ("software_render_tip", ""), @@ -433,5 +433,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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 146e60f9a..2069826ee 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -407,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", ""), ("Group", ""), ("Search", ""), - ("Closed manually by the web console", ""), + ("Closed manually by web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), ("software_render_tip", ""), @@ -433,5 +433,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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 729932973..26bc8ef51 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -407,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", ""), ("Group", ""), ("Search", ""), - ("Closed manually by the web console", ""), + ("Closed manually by web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), ("software_render_tip", ""), @@ -433,5 +433,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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index a78509e59..726bd8a9d 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -407,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", "เพิ่มไปยังสมุดรายชื่อ"), ("Group", "กลุ่ม"), ("Search", "ค้นหา"), - ("Closed manually by the web console", "ถูกปิดโดยเว็บคอนโซล"), + ("Closed manually by web console", "ถูกปิดโดยเว็บคอนโซล"), ("Local keyboard type", "ประเภทคีย์บอร์ด"), ("Select local keyboard type", "เลือกประเภทคีย์บอร์ด"), ("software_render_tip", ""), @@ -433,5 +433,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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 483ee67e3..c7eb27205 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -407,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", ""), ("Group", ""), ("Search", ""), - ("Closed manually by the web console", ""), + ("Closed manually by web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), ("software_render_tip", ""), @@ -433,5 +433,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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 459c517ff..e6d2dcb61 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -407,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", "添加到地址簿"), ("Group", "小組"), ("Search", "搜索"), - ("Closed manually by the web console", "被web控制台手動關閉"), + ("Closed manually by web console", "被web控制台手動關閉"), ("Local keyboard type", "本地鍵盤類型"), ("Select local keyboard type", "請選擇本地鍵盤類型"), ("software_render_tip", "如果你使用英偉達顯卡, 並且遠程窗口在會話建立後會立刻關閉, 那麼安裝nouveau驅動並且選擇使用軟件渲染可能會有幫助。重啟軟件後生效。"), @@ -433,5 +433,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", "正常關閉"), ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index ca99be12e..9276b184e 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -407,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", "Додати IP до Адресної книги"), ("Group", "Група"), ("Search", "Пошук"), - ("Closed manually by the web console", ""), + ("Closed manually by web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), ("software_render_tip", ""), @@ -433,5 +433,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", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 53de4e67c..6649fbaa3 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -407,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", ""), ("Group", ""), ("Search", ""), - ("Closed manually by the web console", ""), + ("Closed manually by web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), ("software_render_tip", ""), @@ -433,5 +433,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", ""), ].iter().cloned().collect(); } diff --git a/src/server/connection.rs b/src/server/connection.rs index d3f7ac149..c259d54cf 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -101,7 +101,6 @@ pub struct Connection { lr: LoginRequest, last_recv_time: Arc>, chat_unanswered: bool, - close_manually: bool, #[allow(unused)] elevation_requested: bool, from_switch: bool, @@ -200,7 +199,6 @@ impl Connection { lr: Default::default(), last_recv_time: Arc::new(Mutex::new(Instant::now())), chat_unanswered: false, - close_manually: false, elevation_requested: false, from_switch: false, }; @@ -271,7 +269,9 @@ impl Connection { } } ipc::Data::Close => { - conn.on_close_manually("connection manager", "peer").await; + conn.chat_unanswered = false; // seen + conn.send_close_reason_no_retry("").await; + conn.on_close("connection manager", true).await; break; } ipc::Data::ChatMessage{text} => { @@ -411,7 +411,8 @@ impl Connection { } Ok(conns) = hbbs_rx.recv() => { if conns.contains(&id) { - conn.on_close_manually("web console", "web console").await; + conn.send_close_reason_no_retry("Closed manually by web console").await; + conn.on_close("web console", true).await; break; } } @@ -441,7 +442,8 @@ impl Connection { Some(message::Union::Misc(m)) => { match &m.union { Some(misc::Union::StopService(_)) => { - conn.on_close_manually("stop service", "peer").await; + conn.send_close_reason_no_retry("").await; + conn.on_close("stop service", true).await; break; } _ => {}, @@ -540,6 +542,9 @@ impl Connection { "action": "close", })); ALIVE_CONNS.lock().unwrap().retain(|&c| c != id); + if let Some(s) = conn.server.upgrade() { + s.write().unwrap().remove_connection(&conn.inner); + } log::info!("#{} connection loop exited", id); } @@ -1583,7 +1588,8 @@ impl Connection { uuid.to_string().as_ref(), ]) .ok(); - self.on_close_manually("switch sides", "peer"); + self.send_close_reason_no_retry("Closed as expected"); + self.on_close("switch sides", false); return false; } } @@ -1757,16 +1763,13 @@ impl Connection { } async fn on_close(&mut self, reason: &str, lock: bool) { - if let Some(s) = self.server.upgrade() { - s.write().unwrap().remove_connection(&self.inner); - } log::info!("#{} Connection closed: {}", self.inner.id(), reason); if lock && self.lock_after_session_end && self.keyboard { #[cfg(not(any(target_os = "android", target_os = "ios")))] lock_screen().await; } #[cfg(not(any(target_os = "android", target_os = "ios")))] - let data = if self.chat_unanswered && !self.close_manually { + let data = if self.chat_unanswered { ipc::Data::Disconnected } else { ipc::Data::Close @@ -1777,15 +1780,17 @@ impl Connection { self.port_forward_socket.take(); } - async fn on_close_manually(&mut self, close_from: &str, close_by: &str) { - self.close_manually = true; + // The `reason` should be consistent with `check_if_retry` if not empty + async fn send_close_reason_no_retry(&mut self, reason: &str) { let mut misc = Misc::new(); - misc.set_close_reason(format!("Closed manually by the {}", close_by)); + if reason.is_empty() { + misc.set_close_reason("Closed manually by the peer".to_string()); + } else { + misc.set_close_reason(reason.to_string()); + } let mut msg_out = Message::new(); msg_out.set_misc(misc); self.send(msg_out).await; - self.on_close(&format!("Close requested from {}", close_from), false) - .await; SESSIONS.lock().unwrap().remove(&self.lr.my_id); } From 7c2d7df62e02d881f7ad32e34c3a94eb8e0bd132 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sat, 28 Jan 2023 16:39:24 +0800 Subject: [PATCH 1628/2015] quick support if right click && run as admin on win Signed-off-by: 21pages --- src/core_main.rs | 13 ++++++++----- src/server/portable_service.rs | 17 ++++++----------- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/src/core_main.rs b/src/core_main.rs index 8b99f6131..4a2f6164c 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -54,11 +54,6 @@ pub fn core_main() -> Option> { return core_main_invoke_new_connection(std::env::args()); } let click_setup = cfg!(windows) && args.is_empty() && crate::common::is_setup(&arg_exe); - #[cfg(not(feature = "flutter"))] - { - _is_quick_support = - cfg!(windows) && args.is_empty() && arg_exe.to_lowercase().ends_with("qs.exe"); - } if click_setup { args.push("--install".to_owned()); flutter_args.push("--install".to_string()); @@ -70,6 +65,14 @@ pub fn core_main() -> Option> { println!("{}", crate::VERSION); return None; } + #[cfg(windows)] + { + _is_quick_support |= !crate::platform::is_installed() + && args.is_empty() + && (arg_exe.to_lowercase().ends_with("qs.exe") + || (!click_setup && crate::platform::is_elevated(None).unwrap_or(false))); + crate::portable_service::client::set_quick_support(_is_quick_support); + } #[cfg(debug_assertions)] { use hbb_common::env_logger::*; diff --git a/src/server/portable_service.rs b/src/server/portable_service.rs index 0651fd4ce..748cb39e4 100644 --- a/src/server/portable_service.rs +++ b/src/server/portable_service.rs @@ -459,6 +459,7 @@ pub mod client { static ref RUNNING: Arc> = Default::default(); static ref SHMEM: Arc>> = Default::default(); static ref SENDER : Mutex> = Mutex::new(client::start_ipc_server()); + static ref QUICK_SUPPORT: Arc> = Default::default(); } pub enum StartPara { @@ -561,6 +562,10 @@ pub mod client { *SHMEM.lock().unwrap() = None; } + pub fn set_quick_support(v: bool) { + *QUICK_SUPPORT.lock().unwrap() = v; + } + fn set_dir_permission(dir: &PathBuf) -> bool { // // give Everyone RX permission std::process::Command::new("icacls") @@ -685,17 +690,7 @@ pub mod client { use DataPortableService::*; let rx = Arc::new(tokio::sync::Mutex::new(rx)); let postfix = IPC_SUFFIX; - #[cfg(feature = "flutter")] - let quick_support = { - let args: Vec<_> = std::env::args().collect(); - args.contains(&"--quick_support".to_string()) - }; - #[cfg(not(feature = "flutter"))] - let quick_support = std::env::current_exe() - .unwrap_or("".into()) - .to_string_lossy() - .to_lowercase() - .ends_with("qs.exe"); + let quick_support = QUICK_SUPPORT.lock().unwrap().clone(); match new_listener(postfix).await { Ok(mut incoming) => loop { From 3e4a8671152cdae2f33e926a3045b72e3edaf59e Mon Sep 17 00:00:00 2001 From: 21pages Date: Sat, 28 Jan 2023 16:41:47 +0800 Subject: [PATCH 1629/2015] opt elevation code Signed-off-by: 21pages --- flutter/lib/models/server_model.dart | 2 +- src/server/connection.rs | 165 +++++++++++++++------------ src/server/video_service.rs | 8 +- src/ui_cm_interface.rs | 4 +- 4 files changed, 102 insertions(+), 77 deletions(-) diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index 7703182cd..56dca4cdf 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -28,7 +28,7 @@ class ServerModel with ChangeNotifier { bool _inputOk = false; bool _audioOk = false; bool _fileOk = false; - bool _showElevation = true; + bool _showElevation = false; bool _hideCm = false; int _connectStatus = 0; // Rendezvous Server status String _verificationMethod = ""; diff --git a/src/server/connection.rs b/src/server/connection.rs index c259d54cf..c7aa7fe0c 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -3,6 +3,8 @@ use super::{input_service::*, *}; use crate::clipboard_file::*; #[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::common::update_clipboard; +#[cfg(windows)] +use crate::portable_service::client as portable_client; use crate::video_service; #[cfg(any(target_os = "android", target_os = "ios"))] use crate::{common::DEVICE_NAME, flutter::connection_manager::start_channel}; @@ -101,8 +103,8 @@ pub struct Connection { lr: LoginRequest, last_recv_time: Arc>, chat_unanswered: bool, - #[allow(unused)] - elevation_requested: bool, + #[cfg(windows)] + portable: PortableState, from_switch: bool, } @@ -199,7 +201,8 @@ impl Connection { lr: Default::default(), last_recv_time: Arc::new(Mutex::new(Instant::now())), chat_unanswered: false, - elevation_requested: false, + #[cfg(windows)] + portable: Default::default(), from_switch: false, }; #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -247,14 +250,6 @@ impl Connection { #[cfg(not(any(target_os = "android", target_os = "ios")))] std::thread::spawn(move || Self::handle_input(rx_input, tx_cloned)); let mut second_timer = time::interval(Duration::from_secs(1)); - #[cfg(windows)] - let mut last_uac = false; - #[cfg(windows)] - let mut last_foreground_window_elevated = false; - #[cfg(windows)] - let mut last_portable_service_running = false; - #[cfg(windows)] - let is_installed = crate::platform::is_installed(); loop { tokio::select! { @@ -362,8 +357,7 @@ impl Connection { } #[cfg(windows)] ipc::Data::DataPortableService(ipc::DataPortableService::RequestStart) => { - use crate::portable_service::client; - if let Err(e) = client::start_portable_service(client::StartPara::Direct) { + if let Err(e) = portable_client::start_portable_service(portable_client::StartPara::Direct) { log::error!("Failed to start portable service from cm:{:?}", e); } } @@ -458,46 +452,7 @@ impl Connection { }, _ = second_timer.tick() => { #[cfg(windows)] - { - if !is_installed && conn.file_transfer.is_none() && conn.port_forward_socket.is_none(){ - let portable_service_running = crate::portable_service::client::running(); - if portable_service_running != last_portable_service_running { - last_portable_service_running = portable_service_running; - if portable_service_running && conn.elevation_requested { - let mut misc = Misc::new(); - misc.set_portable_service_running(portable_service_running); - let mut msg = Message::new(); - msg.set_misc(misc); - conn.inner.send(msg.into()); - } - } - let uac = crate::video_service::IS_UAC_RUNNING.lock().unwrap().clone(); - if last_uac != uac { - last_uac = uac; - if !uac || !portable_service_running{ - let mut misc = Misc::new(); - misc.set_uac(uac); - let mut msg = Message::new(); - msg.set_misc(misc); - conn.inner.send(msg.into()); - } - } - let foreground_window_elevated = crate::video_service::IS_FOREGROUND_WINDOW_ELEVATED.lock().unwrap().clone(); - if last_foreground_window_elevated != foreground_window_elevated { - last_foreground_window_elevated = foreground_window_elevated; - if !foreground_window_elevated || !portable_service_running { - let mut misc = Misc::new(); - misc.set_foreground_window_elevated(foreground_window_elevated); - let mut msg = Message::new(); - msg.set_misc(misc); - conn.inner.send(msg.into()); - } - } - let show_elevation = !portable_service_running; - conn.send_to_cm(ipc::Data::DataPortableService(ipc::DataPortableService::CmShowElevation(show_elevation))); - - } - } + conn.portable_check(); } _ = test_delay_timer.tick() => { if last_recv_time.elapsed() >= SEC30 { @@ -1537,15 +1492,14 @@ impl Connection { #[cfg(windows)] { let mut err = "No need to elevate".to_string(); - if !crate::platform::is_installed() - && !crate::portable_service::client::running() - { - use crate::portable_service::client; - err = client::start_portable_service(client::StartPara::Direct) - .err() - .map_or("".to_string(), |e| e.to_string()); + if !crate::platform::is_installed() && !portable_client::running() { + err = portable_client::start_portable_service( + portable_client::StartPara::Direct, + ) + .err() + .map_or("".to_string(), |e| e.to_string()); } - self.elevation_requested = err.is_empty(); + self.portable.elevation_requested = err.is_empty(); let mut misc = Misc::new(); misc.set_elevation_response(err); let mut msg = Message::new(); @@ -1557,18 +1511,14 @@ impl Connection { #[cfg(windows)] { let mut err = "No need to elevate".to_string(); - if !crate::platform::is_installed() - && !crate::portable_service::client::running() - { - use crate::portable_service::client; - err = client::start_portable_service(client::StartPara::Logon( - _r.username, - _r.password, - )) + if !crate::platform::is_installed() && !portable_client::running() { + err = portable_client::start_portable_service( + portable_client::StartPara::Logon(_r.username, _r.password), + ) .err() .map_or("".to_string(), |e| e.to_string()); } - self.elevation_requested = err.is_empty(); + self.portable.elevation_requested = err.is_empty(); let mut misc = Misc::new(); misc.set_elevation_response(err); let mut msg = Message::new(); @@ -1810,6 +1760,59 @@ impl Connection { pub fn alive_conns() -> Vec { ALIVE_CONNS.lock().unwrap().clone() } + + #[cfg(windows)] + fn portable_check(&mut self) { + if self.portable.is_installed + || self.file_transfer.is_some() + || self.port_forward_socket.is_some() + { + return; + } + let running = portable_client::running(); + let show_elevation = !running; + self.send_to_cm(ipc::Data::DataPortableService( + ipc::DataPortableService::CmShowElevation(show_elevation), + )); + if self.authorized { + let p = &mut self.portable; + if running != p.last_running { + p.last_running = running; + if running && p.elevation_requested { + let mut misc = Misc::new(); + misc.set_portable_service_running(running); + let mut msg = Message::new(); + msg.set_misc(misc); + self.inner.send(msg.into()); + } + } + let uac = crate::video_service::IS_UAC_RUNNING.lock().unwrap().clone(); + if p.last_uac != uac { + p.last_uac = uac; + if !uac || !running { + let mut misc = Misc::new(); + misc.set_uac(uac); + let mut msg = Message::new(); + msg.set_misc(misc); + self.inner.send(msg.into()); + } + } + let foreground_window_elevated = crate::video_service::IS_FOREGROUND_WINDOW_ELEVATED + .lock() + .unwrap() + .clone(); + if p.last_foreground_window_elevated != foreground_window_elevated { + p.last_foreground_window_elevated = foreground_window_elevated; + if !foreground_window_elevated || !running { + let mut misc = Misc::new(); + misc.set_foreground_window_elevated(foreground_window_elevated); + let mut msg = Message::new(); + msg.set_misc(misc); + self.inner.send(msg.into()); + } + } + } + } } pub fn insert_switch_sides_uuid(id: String, uuid: uuid::Uuid) { @@ -1984,3 +1987,25 @@ pub enum FileAuditType { RemoteSend = 0, RemoteReceive = 1, } + +#[cfg(windows)] +pub struct PortableState { + pub last_uac: bool, + pub last_foreground_window_elevated: bool, + pub last_running: bool, + pub is_installed: bool, + pub elevation_requested: bool, +} + +#[cfg(windows)] +impl Default for PortableState { + fn default() -> Self { + Self { + is_installed: crate::platform::is_installed(), + last_uac: Default::default(), + last_foreground_window_elevated: Default::default(), + last_running: Default::default(), + elevation_requested: Default::default(), + } + } +} diff --git a/src/server/video_service.rs b/src/server/video_service.rs index 599dfbd54..d041a433c 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -956,15 +956,17 @@ fn start_uac_elevation_check() { START.call_once(|| { if !crate::platform::is_installed() && !crate::platform::is_root() - && !crate::platform::is_elevated(None).map_or(false, |b| b) + && !crate::portable_service::client::running() { std::thread::spawn(|| loop { std::thread::sleep(std::time::Duration::from_secs(1)); if let Ok(uac) = crate::ui::win_privacy::is_process_consent_running() { *IS_UAC_RUNNING.lock().unwrap() = uac; } - if let Ok(elevated) = crate::platform::is_foreground_window_elevated() { - *IS_FOREGROUND_WINDOW_ELEVATED.lock().unwrap() = elevated; + if !crate::platform::is_elevated(None).unwrap_or(false) { + if let Ok(elevated) = crate::platform::is_foreground_window_elevated() { + *IS_FOREGROUND_WINDOW_ELEVATED.lock().unwrap() = elevated; + } } }); } diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index d620bcbc9..5d451e4d4 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -789,9 +789,7 @@ fn cm_inner_send(id: i32, data: Data) { pub fn can_elevate() -> bool { #[cfg(windows)] - { - return !crate::platform::is_installed() && !crate::portable_service::client::running(); - } + return !crate::platform::is_installed(); #[cfg(not(windows))] return false; } From 19f04f29c0c70d450237d26365fe178b97758d37 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sat, 28 Jan 2023 17:10:40 +0800 Subject: [PATCH 1630/2015] fix desktop dialog request focus Signed-off-by: 21pages --- flutter/lib/common.dart | 9 ++++----- flutter/lib/common/widgets/login.dart | 2 ++ flutter/lib/desktop/widgets/remote_menubar.dart | 1 + 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index f4e0c2d75..6ee57ef50 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -608,12 +608,11 @@ class CustomAlertDialog extends StatelessWidget { @override Widget build(BuildContext context) { - FocusNode focusNode = FocusNode(); - // request focus if there is no focused FocusNode in the dialog - Future.delayed(Duration.zero, () { - if (!focusNode.hasFocus) focusNode.requestFocus(); - }); + // request focus FocusScopeNode scopeNode = FocusScopeNode(); + Future.delayed(Duration.zero, () { + if (!scopeNode.hasFocus) scopeNode.requestFocus(); + }); return FocusScope( node: scopeNode, autofocus: true, diff --git a/flutter/lib/common/widgets/login.dart b/flutter/lib/common/widgets/login.dart index 2f10ac005..05fc1fc5c 100644 --- a/flutter/lib/common/widgets/login.dart +++ b/flutter/lib/common/widgets/login.dart @@ -666,6 +666,8 @@ Future verificationCodeDialog(UserPayload? user) async { child: const LinearProgressIndicator()), ], ), + onCancel: close, + onSubmit: onVerify, actions: [ dialogButton("Cancel", onPressed: close, isOutline: true), dialogButton("Verify", onPressed: onVerify), diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 62289d5f0..b9d793744 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -653,6 +653,7 @@ class _RemoteMenubarState extends State { )); } if (pi.platform != kPeerPlatformAndroid && + pi.platform != kPeerPlatformMacOS && // unsupport yet version_cmp(peer_version, '1.2.0') >= 0) { displayMenu.add(MenuEntryButton( childBuilder: (TextStyle? style) => Text( From 8a88640b18f13dcb608ba0708a1cf599584f7221 Mon Sep 17 00:00:00 2001 From: NicKoehler <53040044+NicKoehler@users.noreply.github.com> Date: Sat, 28 Jan 2023 11:49:09 +0100 Subject: [PATCH 1631/2015] Update it.rs --- src/lang/it.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lang/it.rs b/src/lang/it.rs index d7340b27f..322c324ce 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -41,9 +41,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "Informazioni"), ("Slogan_tip", "Fatta con il cuore in questo mondo caotico!"), ("Privacy Statement", "Informativa sulla privacy"), - ("Build Date", ""), - ("Version", ""), - ("Home", ""), + ("Build Date", "Data della build"), + ("Version", "Versione"), + ("Home", "Home"), ("Mute", "Silenzia"), ("Audio Input", "Input audio"), ("Enhancements", "Miglioramenti"), @@ -436,6 +436,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", ""), + ("Closed as expected", "Chiuso come previsto"), ].iter().cloned().collect(); } From 733a43df07d6710f24999df6eb376aeea2736532 Mon Sep 17 00:00:00 2001 From: csf Date: Sat, 28 Jan 2023 21:24:49 +0900 Subject: [PATCH 1632/2015] 1. fix get_api_server. 2. add device info in LoginRequest --- flutter/lib/common/hbbs/hbbs.dart | 32 ++++++++++++++++--------------- src/common.rs | 13 +++++++++++-- src/flutter_ffi.rs | 4 ++++ 3 files changed, 32 insertions(+), 17 deletions(-) diff --git a/flutter/lib/common/hbbs/hbbs.dart b/flutter/lib/common/hbbs/hbbs.dart index 27238db67..4717143fd 100644 --- a/flutter/lib/common/hbbs/hbbs.dart +++ b/flutter/lib/common/hbbs/hbbs.dart @@ -1,5 +1,9 @@ +import 'dart:io'; + import 'package:flutter_hbb/models/peer_model.dart'; +import '../../models/platform_model.dart'; + class HttpType { static const kAuthReqTypeAccount = "account"; static const kAuthReqTypeMobile = "mobile"; @@ -48,6 +52,16 @@ class PeerPayload { } } +class DeviceInfo { + static Map toJson() { + final Map data = {}; + data['os'] = Platform.operatingSystem; + data['type'] = "client"; + data['name'] = bind.mainGetHostname(); + return data; + } +} + class LoginRequest { String? username; String? password; @@ -56,7 +70,7 @@ class LoginRequest { bool? autoLogin; String? type; String? verificationCode; - String? deviceInfo; + Map deviceInfo = DeviceInfo.toJson(); LoginRequest( {this.username, @@ -65,19 +79,7 @@ class LoginRequest { this.uuid, this.autoLogin, this.type, - this.verificationCode, - this.deviceInfo}); - - LoginRequest.fromJson(Map json) { - username = json['username']; - password = json['password']; - id = json['id']; - uuid = json['uuid']; - autoLogin = json['autoLogin']; - type = json['type']; - verificationCode = json['verificationCode']; - deviceInfo = json['deviceInfo']; - } + this.verificationCode}); Map toJson() { final Map data = {}; @@ -88,7 +90,7 @@ class LoginRequest { data['autoLogin'] = autoLogin ?? ''; data['type'] = type ?? ''; data['verificationCode'] = verificationCode ?? ''; - data['deviceInfo'] = deviceInfo ?? ''; + data['deviceInfo'] = deviceInfo; return data; } } diff --git a/src/common.rs b/src/common.rs index cdf57ae3d..2bf287feb 100644 --- a/src/common.rs +++ b/src/common.rs @@ -451,6 +451,7 @@ pub fn run_me>(args: Vec) -> std::io::Result String { // fix bug of whoami #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -459,6 +460,14 @@ pub fn username() -> String { return DEVICE_NAME.lock().unwrap().clone(); } +#[inline] +pub fn hostname() -> String { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + return whoami::hostname(); + #[cfg(any(target_os = "android", target_os = "ios"))] + return DEVICE_NAME.lock().unwrap().clone(); +} + #[inline] pub fn check_port(host: T, port: i32) -> String { hbb_common::socket_client::check_port(host, port) @@ -581,9 +590,9 @@ pub fn get_api_server(api: String, custom: String) -> String { if !s0.is_empty() { let s = crate::increase_port(&s0, -2); if s == s0 { - format!("http://{}:{}", s, config::RENDEZVOUS_PORT - 2); + return format!("http://{}:{}", s, config::RENDEZVOUS_PORT - 2); } else { - format!("http://{}", s); + return format!("http://{}", s); } } "https://admin.rustdesk.com".to_owned() diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index c30c6c847..ebaa160f1 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -523,6 +523,10 @@ pub fn main_get_sound_inputs() -> Vec { vec![String::from("")] } +pub fn main_get_hostname() -> SyncReturn { + SyncReturn(crate::common::hostname()) +} + pub fn main_change_id(new_id: String) { change_id(new_id) } From 813b9ea79def023dc39982ba16befbf89c2a2f08 Mon Sep 17 00:00:00 2001 From: csf Date: Sat, 28 Jan 2023 22:02:42 +0900 Subject: [PATCH 1633/2015] fix logout failed --- flutter/lib/models/user_model.dart | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/flutter/lib/models/user_model.dart b/flutter/lib/models/user_model.dart index b0eebee53..6694d8c5c 100644 --- a/flutter/lib/models/user_model.dart +++ b/flutter/lib/models/user_model.dart @@ -80,13 +80,15 @@ class UserModel { final tag = gFFI.dialogManager.showLoading(translate('Waiting')); try { final url = await bind.mainGetApiServer(); + final authHeaders = getHttpHeaders(); + authHeaders['Content-Type'] = "application/json"; await http .post(Uri.parse('$url/api/logout'), - body: { + body: jsonEncode({ 'id': await bind.mainGetMyId(), 'uuid': await bind.mainGetUuid(), - }, - headers: getHttpHeaders()) + }), + headers: authHeaders) .timeout(Duration(seconds: 2)); } catch (e) { print("request /api/logout failed: err=$e"); From d04f047d14a543b36fe372481b924922348898ad Mon Sep 17 00:00:00 2001 From: fufesou Date: Sat, 28 Jan 2023 21:11:03 +0800 Subject: [PATCH 1634/2015] feat mouse click and move through monitor widget Signed-off-by: fufesou --- flutter/lib/common/widgets/overlay.dart | 8 ++- flutter/lib/desktop/pages/remote_page.dart | 60 ++++++++++++++-------- flutter/lib/mobile/pages/remote_page.dart | 6 ++- 3 files changed, 46 insertions(+), 28 deletions(-) diff --git a/flutter/lib/common/widgets/overlay.dart b/flutter/lib/common/widgets/overlay.dart index 81797962e..d9684bace 100644 --- a/flutter/lib/common/widgets/overlay.dart +++ b/flutter/lib/common/widgets/overlay.dart @@ -324,10 +324,8 @@ class QualityMonitor extends StatelessWidget { Widget build(BuildContext context) => ChangeNotifierProvider.value( value: qualityMonitorModel, child: Consumer( - builder: (context, qualityMonitorModel, child) => Positioned( - top: 10, - right: 10, - child: qualityMonitorModel.show + builder: (context, qualityMonitorModel, child) => + qualityMonitorModel.show ? Container( padding: const EdgeInsets.all(8), color: MyTheme.canvasColor.withAlpha(120), @@ -357,5 +355,5 @@ class QualityMonitor extends StatelessWidget { ], ), ) - : const SizedBox.shrink()))); + : const SizedBox.shrink())); } diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index fb67154bc..2e4668159 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -279,6 +279,34 @@ class _RemotePageState extends State } } + Widget _buildRawPointerMouseRegion( + Widget child, + PointerEnterEventListener? onEnter, + PointerExitEventListener? onExit, + ) { + return RawPointerMouseRegion( + onEnter: enterView, + onExit: leaveView, + onPointerDown: (event) { + // A double check for blur status. + // Note: If there's an `onPointerDown` event is triggered, `_isWindowBlur` is expected being false. + // Sometimes the system does not send the necessary focus event to flutter. We should manually + // handle this inconsistent status by setting `_isWindowBlur` to false. So we can + // ensure the grab-key thread is running when our users are clicking the remote canvas. + if (_isWindowBlur) { + debugPrint( + "Unexpected status: onPointerDown is triggered while the remote window is in blur status"); + _isWindowBlur = false; + } + if (!_rawKeyFocusNode.hasFocus) { + _rawKeyFocusNode.requestFocus(); + } + }, + inputModel: _ffi.inputModel, + child: child, + ); + } + Widget getBodyForDesktop(BuildContext context) { var paints = [ MouseRegion(onEnter: (evt) { @@ -295,27 +323,8 @@ class _RemotePageState extends State cursorOverImage: _cursorOverImage, keyboardEnabled: _keyboardEnabled, remoteCursorMoved: _remoteCursorMoved, - listenerBuilder: (child) => RawPointerMouseRegion( - onEnter: enterView, - onExit: leaveView, - onPointerDown: (event) { - // A double check for blur status. - // Note: If there's an `onPointerDown` event is triggered, `_isWindowBlur` is expected being false. - // Sometimes the system does not send the necessary focus event to flutter. We should manually - // handle this inconsistent status by setting `_isWindowBlur` to false. So we can - // ensure the grab-key thread is running when our users are clicking the remote canvas. - if (_isWindowBlur) { - debugPrint( - "Unexpected status: onPointerDown is triggered while the remote window is in blur status"); - _isWindowBlur = false; - } - if (!_rawKeyFocusNode.hasFocus) { - _rawKeyFocusNode.requestFocus(); - } - }, - inputModel: _ffi.inputModel, - child: child, - ), + listenerBuilder: (child) => + _buildRawPointerMouseRegion(child, enterView, leaveView), ); })) ]; @@ -328,7 +337,14 @@ class _RemotePageState extends State zoomCursor: _zoomCursor, )))); } - paints.add(QualityMonitor(_ffi.qualityMonitorModel)); + paints.add( + Positioned( + top: 10, + right: 10, + child: _buildRawPointerMouseRegion( + QualityMonitor(_ffi.qualityMonitorModel), null, null), + ), + ); paints.add(RemoteMenubar( id: widget.id, ffi: _ffi, diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 0a10d8011..c4b07b375 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -497,7 +497,11 @@ class _RemotePageState extends State { child: Stack(children: () { final paints = [ ImagePaint(), - QualityMonitor(gFFI.qualityMonitorModel), + Positioned( + top: 10, + right: 10, + child: QualityMonitor(gFFI.qualityMonitorModel), + ), getHelpTools(), SizedBox( width: 0, From eb831a912a8250e2f735bbffb419378c71527319 Mon Sep 17 00:00:00 2001 From: Mr-Update <37781396+Mr-Update@users.noreply.github.com> Date: Sat, 28 Jan 2023 21:52:19 +0100 Subject: [PATCH 1635/2015] Update de.rs --- src/lang/de.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/lang/de.rs b/src/lang/de.rs index 15e98e529..11ce96f6b 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -42,9 +42,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Slogan_tip", "Mit Herzblut programmiert - in einer Welt, die im Chaos versinkt!"), ("Privacy Statement", "Datenschutz"), ("Mute", "Stummschalten"), - ("Build Date", ""), - ("Version", ""), - ("Home", ""), + ("Build Date", "Erstelldatum"), + ("Version", "Version"), + ("Home", "Startseite"), ("Audio Input", "Audioeingang"), ("Enhancements", "Verbesserungen"), ("Hardware Codec", "Hardware-Codec"), @@ -244,7 +244,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Remote ID", "Entfernte ID"), ("Paste", "Einfügen"), ("Paste here?", "Hier einfügen?"), - ("Are you sure to close the connection?", "Möchten Sie diese Verbindung wirklich trennen?"), + ("Are you sure to close the connection?", "Möchten Sie diese Verbindung wirklich schließen?"), ("Download new version", "Neue Version herunterladen"), ("Touch mode", "Touch-Modus"), ("Mouse mode", "Mausmodus"), @@ -267,8 +267,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Note", "Hinweis"), ("Connection", "Verbindung"), ("Share Screen", "Bildschirm freigeben"), - ("CLOSE", "DEAKTIV."), - ("OPEN", "AKTIVIER."), + ("CLOSE", "SCHLIEẞEN"), + ("OPEN", "ÖFFNEN"), ("Chat", "Chat"), ("Total", "Gesamt"), ("items", "Einträge"), @@ -387,7 +387,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 Bildschirm aus, der freigegeben werden soll (auf der Peer-Seite arbeiten)."), + ("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)."), ("Show RustDesk", "RustDesk anzeigen"), ("This PC", "Dieser PC"), ("or", "oder"), @@ -410,7 +410,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", "Zum Adressbuch hinzufügen"), ("Group", "Gruppe"), ("Search", "Suchen"), - ("Closed manually by web console", "Manuell über die Webkonsole beendet"), + ("Closed manually by web console", "Manuell über die Webkonsole geschlossen"), ("Local keyboard type", "Lokaler Tastaturtyp"), ("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."), @@ -436,6 +436,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", ""), + ("Closed as expected", "Wie erwartet geschlossen"), ].iter().cloned().collect(); } From 7e0c9e17df28710387249e7daf34fee107ce8f7a Mon Sep 17 00:00:00 2001 From: fufesou Date: Sat, 28 Jan 2023 20:32:46 +0800 Subject: [PATCH 1636/2015] set cursor mode according to availible modes Signed-off-by: fufesou --- libs/scrap/src/wayland/pipewire.rs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/libs/scrap/src/wayland/pipewire.rs b/libs/scrap/src/wayland/pipewire.rs index c1c84f98e..d1a8d9f85 100644 --- a/libs/scrap/src/wayland/pipewire.rs +++ b/libs/scrap/src/wayland/pipewire.rs @@ -473,7 +473,17 @@ fn request_screen_cast( args.insert("multiple".into(), Variant(Box::new(true))); args.insert("types".into(), Variant(Box::new(1u32))); //| 2u32))); - let cursor_mode = if capture_cursor { 2u32 } else { 1u32 }; + let mut cursor_mode = 0u32; + let mut available_cursor_modes = 0u32; + if let Ok(modes) = portal.available_cursor_modes() { + available_cursor_modes = modes; + } + if capture_cursor { + cursor_mode = 2u32 & available_cursor_modes; + } + if cursor_mode == 0 { + cursor_mode = 1u32 & available_cursor_modes; + } let plasma = std::env::var("DESKTOP_SESSION").map_or(false, |s| s.contains("plasma")); if plasma && capture_cursor { // Warn the user if capturing the cursor is tried on kde as this can crash @@ -483,7 +493,9 @@ fn request_screen_cast( desktop, see https://bugs.kde.org/show_bug.cgi?id=435042 for details! \ You have been warned."); } - args.insert("cursor_mode".into(), Variant(Box::new(cursor_mode))); + if cursor_mode > 0 { + args.insert("cursor_mode".into(), Variant(Box::new(cursor_mode))); + } let session: dbus::Path = r .results .get("session_handle") From b84f3ba1ee7708d8150e53c6a674bbcf1ffc1bc4 Mon Sep 17 00:00:00 2001 From: fufesou Date: Sat, 28 Jan 2023 22:19:28 +0800 Subject: [PATCH 1637/2015] init wayland to update var 'cursor embeded' Signed-off-by: fufesou --- libs/scrap/src/common/mod.rs | 4 ++-- libs/scrap/src/common/wayland.rs | 18 ++++++++++++++++-- libs/scrap/src/wayland/pipewire.rs | 6 ++++++ src/common.rs | 2 +- src/server/wayland.rs | 5 +++-- 5 files changed, 28 insertions(+), 7 deletions(-) diff --git a/libs/scrap/src/common/mod.rs b/libs/scrap/src/common/mod.rs index 1de2f89d6..1df96f511 100644 --- a/libs/scrap/src/common/mod.rs +++ b/libs/scrap/src/common/mod.rs @@ -12,7 +12,7 @@ cfg_if! { mod x11; pub use self::linux::*; pub use self::x11::Frame; - pub use self::wayland::set_map_err; + pub use self::wayland::{set_map_err, detect_cursor_embeded}; } else { mod x11; pub use self::x11::*; @@ -76,7 +76,7 @@ pub fn is_cursor_embedded() -> bool { if is_x11() { x11::IS_CURSOR_EMBEDDED } else { - wayland::IS_CURSOR_EMBEDDED + unsafe { wayland::IS_CURSOR_EMBEDDED } } } diff --git a/libs/scrap/src/common/wayland.rs b/libs/scrap/src/common/wayland.rs index e625fca7e..c807479f1 100644 --- a/libs/scrap/src/common/wayland.rs +++ b/libs/scrap/src/common/wayland.rs @@ -4,12 +4,26 @@ use std::{io, sync::RwLock, time::Duration}; pub struct Capturer(Display, Box, bool, Vec); -pub const IS_CURSOR_EMBEDDED: bool = true; +pub static mut IS_CURSOR_EMBEDDED: bool = true; lazy_static::lazy_static! { static ref MAP_ERR: RwLock io::Error>> = Default::default(); } +pub fn detect_cursor_embeded() { + if unsafe { IS_CURSOR_EMBEDDED } { + use crate::common::wayland::pipewire::get_available_cursor_modes; + match get_available_cursor_modes() { + Ok(modes) => unsafe { + IS_CURSOR_EMBEDDED = (modes & 0x02) > 0; + }, + Err(..) => unsafe { + IS_CURSOR_EMBEDDED = false; + }, + } + } +} + pub fn set_map_err(f: fn(err: String) -> io::Error) { *MAP_ERR.write().unwrap() = Some(f); } @@ -74,7 +88,7 @@ impl Display { } pub fn all() -> io::Result> { - Ok(pipewire::get_capturables(true) + Ok(pipewire::get_capturables(unsafe { IS_CURSOR_EMBEDDED }) .map_err(map_err)? .drain(..) .map(|x| Display(x)) diff --git a/libs/scrap/src/wayland/pipewire.rs b/libs/scrap/src/wayland/pipewire.rs index d1a8d9f85..fefab9b77 100644 --- a/libs/scrap/src/wayland/pipewire.rs +++ b/libs/scrap/src/wayland/pipewire.rs @@ -415,6 +415,12 @@ static mut INIT: bool = false; const RESTORE_TOKEN: &str = "restore_token"; const RESTORE_TOKEN_CONF_KEY: &str = "wayland-restore-token"; +pub fn get_available_cursor_modes() -> Result { + let conn = SyncConnection::new_session()?; + let portal = get_portal(&conn); + portal.available_cursor_modes() +} + // mostly inspired by https://gitlab.gnome.org/snippets/19 fn request_screen_cast( capture_cursor: bool, diff --git a/src/common.rs b/src/common.rs index 2bf287feb..c2d5a81f0 100644 --- a/src/common.rs +++ b/src/common.rs @@ -52,7 +52,7 @@ pub fn global_init() -> bool { #[cfg(target_os = "linux")] { if !*IS_X11 { - crate::server::wayland::set_wayland_scrap_map_err(); + crate::server::wayland::init(); } } true diff --git a/src/server/wayland.rs b/src/server/wayland.rs index 68b9c37cf..96fc2fff6 100644 --- a/src/server/wayland.rs +++ b/src/server/wayland.rs @@ -1,6 +1,6 @@ use super::*; use hbb_common::{allow_err, platform::linux::DISTRO}; -use scrap::{set_map_err, Capturer, Display, Frame, TraitCapturer}; +use scrap::{detect_cursor_embeded, set_map_err, Capturer, Display, Frame, TraitCapturer}; use std::io; use super::video_service::{ @@ -12,7 +12,8 @@ lazy_static::lazy_static! { static ref LOG_SCRAP_COUNT: Mutex = Mutex::new(0); } -pub fn set_wayland_scrap_map_err() { +pub fn init() { + detect_cursor_embeded(); set_map_err(map_err_scrap); } From c0adc142159bea13e72db9b986a2f54b7ebeb9a5 Mon Sep 17 00:00:00 2001 From: fufesou Date: Sun, 29 Jan 2023 09:26:55 +0800 Subject: [PATCH 1638/2015] misspelling Signed-off-by: fufesou --- libs/scrap/src/common/mod.rs | 2 +- libs/scrap/src/common/wayland.rs | 2 +- src/server/wayland.rs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/libs/scrap/src/common/mod.rs b/libs/scrap/src/common/mod.rs index 1df96f511..af1bc4d5e 100644 --- a/libs/scrap/src/common/mod.rs +++ b/libs/scrap/src/common/mod.rs @@ -12,7 +12,7 @@ cfg_if! { mod x11; pub use self::linux::*; pub use self::x11::Frame; - pub use self::wayland::{set_map_err, detect_cursor_embeded}; + pub use self::wayland::{set_map_err, detect_cursor_embedded}; } else { mod x11; pub use self::x11::*; diff --git a/libs/scrap/src/common/wayland.rs b/libs/scrap/src/common/wayland.rs index c807479f1..9d62b87d2 100644 --- a/libs/scrap/src/common/wayland.rs +++ b/libs/scrap/src/common/wayland.rs @@ -10,7 +10,7 @@ lazy_static::lazy_static! { static ref MAP_ERR: RwLock io::Error>> = Default::default(); } -pub fn detect_cursor_embeded() { +pub fn detect_cursor_embedded() { if unsafe { IS_CURSOR_EMBEDDED } { use crate::common::wayland::pipewire::get_available_cursor_modes; match get_available_cursor_modes() { diff --git a/src/server/wayland.rs b/src/server/wayland.rs index 96fc2fff6..eada6971a 100644 --- a/src/server/wayland.rs +++ b/src/server/wayland.rs @@ -1,6 +1,6 @@ use super::*; use hbb_common::{allow_err, platform::linux::DISTRO}; -use scrap::{detect_cursor_embeded, set_map_err, Capturer, Display, Frame, TraitCapturer}; +use scrap::{detect_cursor_embedded, set_map_err, Capturer, Display, Frame, TraitCapturer}; use std::io; use super::video_service::{ @@ -13,7 +13,7 @@ lazy_static::lazy_static! { } pub fn init() { - detect_cursor_embeded(); + detect_cursor_embedded(); set_map_err(map_err_scrap); } From 340897ab1805a5ccc2d9b2c7c8af224643e4017f Mon Sep 17 00:00:00 2001 From: fufesou Date: Sun, 29 Jan 2023 09:57:05 +0800 Subject: [PATCH 1639/2015] set cursor embedded Signed-off-by: fufesou --- src/server/wayland.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/server/wayland.rs b/src/server/wayland.rs index eada6971a..817b8adb7 100644 --- a/src/server/wayland.rs +++ b/src/server/wayland.rs @@ -1,6 +1,9 @@ use super::*; use hbb_common::{allow_err, platform::linux::DISTRO}; -use scrap::{detect_cursor_embedded, set_map_err, Capturer, Display, Frame, TraitCapturer}; +use scrap::{ + detect_cursor_embedded, is_cursor_embedded, set_map_err, Capturer, Display, Frame, + TraitCapturer, +}; use std::io; use super::video_service::{ @@ -130,7 +133,7 @@ pub(super) async fn check_init() -> ResultType<()> { let num = all.len(); let (primary, mut displays) = super::video_service::get_displays_2(&all); for display in displays.iter_mut() { - display.cursor_embedded = true; + display.cursor_embedded = is_cursor_embedded(); } let mut rects: Vec<((i32, i32), usize, usize)> = Vec::new(); From d1090fc62c7d62d0bcf26d45b4675360118ba756 Mon Sep 17 00:00:00 2001 From: fufesou Date: Sun, 29 Jan 2023 10:58:04 +0800 Subject: [PATCH 1640/2015] ensure init cursor embedded Signed-off-by: fufesou --- libs/scrap/src/common/mod.rs | 4 ++-- libs/scrap/src/common/wayland.rs | 31 +++++++++++++++++++------------ src/server/wayland.rs | 6 +----- 3 files changed, 22 insertions(+), 19 deletions(-) diff --git a/libs/scrap/src/common/mod.rs b/libs/scrap/src/common/mod.rs index af1bc4d5e..45aafe7c5 100644 --- a/libs/scrap/src/common/mod.rs +++ b/libs/scrap/src/common/mod.rs @@ -12,7 +12,7 @@ cfg_if! { mod x11; pub use self::linux::*; pub use self::x11::Frame; - pub use self::wayland::{set_map_err, detect_cursor_embedded}; + pub use self::wayland::set_map_err; } else { mod x11; pub use self::x11::*; @@ -76,7 +76,7 @@ pub fn is_cursor_embedded() -> bool { if is_x11() { x11::IS_CURSOR_EMBEDDED } else { - unsafe { wayland::IS_CURSOR_EMBEDDED } + wayland::is_cursor_embedded() } } diff --git a/libs/scrap/src/common/wayland.rs b/libs/scrap/src/common/wayland.rs index 9d62b87d2..86afd5d82 100644 --- a/libs/scrap/src/common/wayland.rs +++ b/libs/scrap/src/common/wayland.rs @@ -4,22 +4,29 @@ use std::{io, sync::RwLock, time::Duration}; pub struct Capturer(Display, Box, bool, Vec); -pub static mut IS_CURSOR_EMBEDDED: bool = true; +static mut IS_CURSOR_EMBEDDED: Option = None; lazy_static::lazy_static! { static ref MAP_ERR: RwLock io::Error>> = Default::default(); } -pub fn detect_cursor_embedded() { - if unsafe { IS_CURSOR_EMBEDDED } { - use crate::common::wayland::pipewire::get_available_cursor_modes; - match get_available_cursor_modes() { - Ok(modes) => unsafe { - IS_CURSOR_EMBEDDED = (modes & 0x02) > 0; - }, - Err(..) => unsafe { - IS_CURSOR_EMBEDDED = false; - }, +pub fn is_cursor_embedded() -> bool { + unsafe { + if IS_CURSOR_EMBEDDED.is_none() { + init_cursor_embedded(); + } + IS_CURSOR_EMBEDDED.unwrap_or(false) + } +} + +unsafe fn init_cursor_embedded() { + use crate::common::wayland::pipewire::get_available_cursor_modes; + match get_available_cursor_modes() { + Ok(modes) => { + IS_CURSOR_EMBEDDED = Some((modes & 0x02) > 0); + } + Err(..) => { + IS_CURSOR_EMBEDDED = Some(false); } } } @@ -88,7 +95,7 @@ impl Display { } pub fn all() -> io::Result> { - Ok(pipewire::get_capturables(unsafe { IS_CURSOR_EMBEDDED }) + Ok(pipewire::get_capturables(is_cursor_embedded()) .map_err(map_err)? .drain(..) .map(|x| Display(x)) diff --git a/src/server/wayland.rs b/src/server/wayland.rs index 817b8adb7..954f1ed1d 100644 --- a/src/server/wayland.rs +++ b/src/server/wayland.rs @@ -1,9 +1,6 @@ use super::*; use hbb_common::{allow_err, platform::linux::DISTRO}; -use scrap::{ - detect_cursor_embedded, is_cursor_embedded, set_map_err, Capturer, Display, Frame, - TraitCapturer, -}; +use scrap::{is_cursor_embedded, set_map_err, Capturer, Display, Frame, TraitCapturer}; use std::io; use super::video_service::{ @@ -16,7 +13,6 @@ lazy_static::lazy_static! { } pub fn init() { - detect_cursor_embedded(); set_map_err(map_err_scrap); } From 176847c51eda697839468b23f1ac679c2b73e618 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sun, 29 Jan 2023 14:27:57 +0800 Subject: [PATCH 1641/2015] fix warning Signed-off-by: 21pages --- libs/scrap/src/dxgi/mod.rs | 7 +++++++ src/platform/windows.rs | 3 +++ src/server/connection.rs | 7 +++---- src/ui_session_interface.rs | 1 - 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/libs/scrap/src/dxgi/mod.rs b/libs/scrap/src/dxgi/mod.rs index 152f502a3..4a0a53402 100644 --- a/libs/scrap/src/dxgi/mod.rs +++ b/libs/scrap/src/dxgi/mod.rs @@ -58,6 +58,7 @@ impl Capturer { let mut device = ptr::null_mut(); let mut context = ptr::null_mut(); let mut duplication = ptr::null_mut(); + #[allow(invalid_value)] let mut desc = unsafe { mem::MaybeUninit::uninit().assume_init() }; let mut gdi_capturer = None; @@ -176,6 +177,7 @@ impl Capturer { unsafe fn load_frame(&mut self, timeout: UINT) -> io::Result<(*const u8, i32)> { let mut frame = ptr::null_mut(); + #[allow(invalid_value)] let mut info = mem::MaybeUninit::uninit().assume_init(); wrap_hresult((*self.duplication.0).AcquireNextFrame(timeout, &mut info, &mut frame))?; @@ -185,6 +187,7 @@ impl Capturer { return Err(std::io::ErrorKind::WouldBlock.into()); } + #[allow(invalid_value)] let mut rect = mem::MaybeUninit::uninit().assume_init(); if self.fastlane { wrap_hresult((*self.duplication.0).MapDesktopSurface(&mut rect))?; @@ -204,6 +207,7 @@ impl Capturer { ); let texture = ComPtr(texture); + #[allow(invalid_value)] let mut texture_desc = mem::MaybeUninit::uninit().assume_init(); (*texture.0).GetDesc(&mut texture_desc); @@ -362,6 +366,7 @@ impl Displays { let mut all = Vec::new(); let mut i: DWORD = 0; loop { + #[allow(invalid_value)] let mut d: DISPLAY_DEVICEW = unsafe { std::mem::MaybeUninit::uninit().assume_init() }; d.cb = std::mem::size_of::() as _; let ok = unsafe { EnumDisplayDevicesW(std::ptr::null(), i, &mut d as _, 0) }; @@ -382,6 +387,7 @@ impl Displays { gdi: true, }; disp.desc.DeviceName = d.DeviceName; + #[allow(invalid_value)] let mut m: DEVMODEW = unsafe { std::mem::MaybeUninit::uninit().assume_init() }; m.dmSize = std::mem::size_of::() as _; m.dmDriverExtra = 0; @@ -441,6 +447,7 @@ impl Displays { // We get the display's details. let desc = unsafe { + #[allow(invalid_value)] let mut desc = mem::MaybeUninit::uninit().assume_init(); (*output.0).GetDesc(&mut desc); desc diff --git a/src/platform/windows.rs b/src/platform/windows.rs index a77b92e07..b778283a5 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -49,6 +49,7 @@ use winreg::RegKey; pub fn get_cursor_pos() -> Option<(i32, i32)> { unsafe { + #[allow(invalid_value)] let mut out = mem::MaybeUninit::uninit().assume_init(); if GetCursorPos(&mut out) == FALSE { return None; @@ -61,6 +62,7 @@ pub fn reset_input_cache() {} pub fn get_cursor() -> ResultType> { unsafe { + #[allow(invalid_value)] let mut ci: CURSORINFO = mem::MaybeUninit::uninit().assume_init(); ci.cbSize = std::mem::size_of::() as _; if crate::portable_service::client::get_cursor_info(&mut ci) == FALSE { @@ -79,6 +81,7 @@ struct IconInfo(ICONINFO); impl IconInfo { fn new(icon: HICON) -> ResultType { unsafe { + #[allow(invalid_value)] let mut ii = mem::MaybeUninit::uninit().assume_init(); if GetIconInfo(icon, &mut ii) == FALSE { Err(io::Error::last_os_error().into()) diff --git a/src/server/connection.rs b/src/server/connection.rs index c7aa7fe0c..e4b667d54 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -138,7 +138,6 @@ const MILLI1: Duration = Duration::from_millis(1); const SEND_TIMEOUT_VIDEO: u64 = 12_000; const SEND_TIMEOUT_OTHER: u64 = SEND_TIMEOUT_VIDEO * 10; const SESSION_TIMEOUT: Duration = Duration::from_secs(30); -const SWITCH_SIDES_TIMEOUT: Duration = Duration::from_secs(10); impl Connection { pub async fn start( @@ -1231,7 +1230,7 @@ impl Connection { SWITCH_SIDES_UUID .lock() .unwrap() - .retain(|_, v| v.0.elapsed() < SWITCH_SIDES_TIMEOUT); + .retain(|_, v| v.0.elapsed() < Duration::from_secs(10)); let uuid_old = SWITCH_SIDES_UUID.lock().unwrap().remove(&lr.my_id); if let Ok(uuid) = uuid::Uuid::from_slice(_s.uuid.to_vec().as_ref()) { if let Some((_instant, uuid_old)) = uuid_old { @@ -1538,8 +1537,8 @@ impl Connection { uuid.to_string().as_ref(), ]) .ok(); - self.send_close_reason_no_retry("Closed as expected"); - self.on_close("switch sides", false); + self.send_close_reason_no_retry("Closed as expected").await; + self.on_close("switch sides", false).await; return false; } } diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 48f6c1090..4fc5db743 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -6,7 +6,6 @@ use crate::client::{ }; use crate::common::{self, GrabState}; use crate::keyboard; -use crate::ui_interface::using_public_server; use crate::{client::Data, client::Interface}; use async_trait::async_trait; use bytes::Bytes; From d1070b88bb3dbdaee6dac4a7f3950e027e9595fd Mon Sep 17 00:00:00 2001 From: fufesou Date: Sun, 29 Jan 2023 14:36:20 +0800 Subject: [PATCH 1642/2015] dismiss menu after switching monitor Signed-off-by: fufesou --- flutter/lib/desktop/widgets/remote_menubar.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index b9d793744..07944649c 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -362,6 +362,9 @@ class _RemoteMenubarState extends State { ), )), onPressed: () { + if (Navigator.canPop(context)) { + Navigator.pop(context); + } RxInt display = CurrentDisplayState.find(widget.id); if (display.value != i) { bind.sessionSwitchDisplay(id: widget.id, value: i); From fc15209d08b980a5e367e2bb8c530a0f91c94aeb Mon Sep 17 00:00:00 2001 From: mehdi-song Date: Sun, 29 Jan 2023 14:02:06 +0330 Subject: [PATCH 1643/2015] Update fa.rs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "Closed as expected"-> "طبق انتظار بسته شد" --- src/lang/fa.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 3b1bcfaf5..15ef1b843 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -436,6 +436,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", ""), + ("Closed as expected", "طبق انتظار بسته شد"), ].iter().cloned().collect(); } From 92748f7ef4d49112f1512b57879bb735034e870d Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sun, 29 Jan 2023 23:30:49 +0800 Subject: [PATCH 1644/2015] adjust tab colors to fix issue #2957 --- .../lib/desktop/widgets/tabbar_widget.dart | 35 +++++++++++++------ 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index ddc0e7729..598b2cc4c 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -765,7 +765,7 @@ class _ListView extends StatelessWidget { tabBuilder: tabBuilder, tabMenuBuilder: tabMenuBuilder, maxLabelWidth: maxLabelWidth, - selectedTabBackgroundColor: selectedTabBackgroundColor, + selectedTabBackgroundColor: selectedTabBackgroundColor ?? MyTheme.tabbar(context).selectedTabBackgroundColor, unSelectedTabBackgroundColor: unSelectedTabBackgroundColor, ); }).toList())); @@ -910,7 +910,7 @@ class _TabState extends State<_Tab> with RestorationMixin { tabSelected: isSelected, onClose: () => widget.onClose(), ))) - ])).paddingSymmetric(horizontal: 10), + ])).paddingOnly(left: 10, right: 5), Offstage( offstage: !showDivider, child: VerticalDivider( @@ -956,6 +956,7 @@ class _CloseButton extends StatelessWidget { child: Offstage( offstage: !visible, child: InkWell( + hoverColor: MyTheme.tabbar(context).closeHoverColor, customBorder: const RoundedRectangleBorder(), onTap: () => onClose(), child: Icon( @@ -966,7 +967,7 @@ class _CloseButton extends StatelessWidget { : MyTheme.tabbar(context).unSelectedIconColor, ), ), - )).paddingOnly(left: 5); + )).paddingOnly(left: 10); } } @@ -1055,6 +1056,8 @@ class TabbarTheme extends ThemeExtension { final Color? unSelectedIconColor; final Color? dividerColor; final Color? hoverColor; + final Color? closeHoverColor; + final Color? selectedTabBackgroundColor; const TabbarTheme( {required this.selectedTabIconColor, @@ -1064,27 +1067,33 @@ class TabbarTheme extends ThemeExtension { required this.selectedIconColor, required this.unSelectedIconColor, required this.dividerColor, - required this.hoverColor}); + required this.hoverColor, + required this.closeHoverColor, + required this.selectedTabBackgroundColor}); static const light = TabbarTheme( selectedTabIconColor: MyTheme.accent, unSelectedTabIconColor: Color.fromARGB(255, 162, 203, 241), - selectedTextColor: Color.fromARGB(255, 26, 26, 26), - unSelectedTextColor: Color.fromARGB(255, 96, 96, 96), + selectedTextColor: Colors.black, + unSelectedTextColor: Color.fromARGB(255, 112, 112, 112), selectedIconColor: Color.fromARGB(255, 26, 26, 26), unSelectedIconColor: Color.fromARGB(255, 96, 96, 96), dividerColor: Color.fromARGB(255, 238, 238, 238), - hoverColor: Color.fromARGB(51, 158, 158, 158)); + hoverColor: Color.fromARGB(51, 158, 158, 158), + closeHoverColor: Colors.black, + selectedTabBackgroundColor: Color.fromARGB(255, 240, 240, 240)); static const dark = TabbarTheme( selectedTabIconColor: MyTheme.accent, unSelectedTabIconColor: Color.fromARGB(255, 30, 65, 98), selectedTextColor: Color.fromARGB(255, 255, 255, 255), - unSelectedTextColor: Color.fromARGB(255, 207, 207, 207), - selectedIconColor: Color.fromARGB(255, 215, 215, 215), + unSelectedTextColor: Color.fromARGB(255, 192, 192, 192), + selectedIconColor: Color.fromARGB(255, 192, 192, 192), unSelectedIconColor: Color.fromARGB(255, 255, 255, 255), dividerColor: Color.fromARGB(255, 64, 64, 64), - hoverColor: Colors.black26); + hoverColor: Colors.black26, + closeHoverColor: Colors.black, + selectedTabBackgroundColor: Colors.black26); @override ThemeExtension copyWith({ @@ -1096,6 +1105,8 @@ class TabbarTheme extends ThemeExtension { Color? unSelectedIconColor, Color? dividerColor, Color? hoverColor, + Color? closeHoverColor, + Color? selectedTabBackgroundColor, }) { return TabbarTheme( selectedTabIconColor: selectedTabIconColor ?? this.selectedTabIconColor, @@ -1107,6 +1118,8 @@ class TabbarTheme extends ThemeExtension { unSelectedIconColor: unSelectedIconColor ?? this.unSelectedIconColor, dividerColor: dividerColor ?? this.dividerColor, hoverColor: hoverColor ?? this.hoverColor, + closeHoverColor: closeHoverColor ?? this.closeHoverColor, + selectedTabBackgroundColor: selectedTabBackgroundColor ?? this.selectedTabBackgroundColor, ); } @@ -1131,6 +1144,8 @@ class TabbarTheme extends ThemeExtension { Color.lerp(unSelectedIconColor, other.unSelectedIconColor, t), 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), ); } From f12de3fec0034a944857503c9a8e3cb8bdb364d3 Mon Sep 17 00:00:00 2001 From: Mateusz Prais Date: Sun, 29 Jan 2023 22:40:17 +0100 Subject: [PATCH 1645/2015] Update pl.rs --- src/lang/pl.rs | 118 ++++++++++++++++++++++++------------------------- 1 file changed, 59 insertions(+), 59 deletions(-) diff --git a/src/lang/pl.rs b/src/lang/pl.rs index f953c5c0b..467d918b6 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -3,7 +3,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = [ ("Status", "Status"), ("Your Desktop", "Twój pulpit"), - ("desk_tip", "W celu zestawienia połączenia z tym urządzeniem należy poniższego ID i hasła."), + ("desk_tip", "W celu połączenia się z tym urządzeniem należy użyć poniższego ID i hasła"), ("Password", "Hasło"), ("Ready", "Gotowe"), ("Established", "Nawiązano"), @@ -38,12 +38,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop service", "Zatrzymaj usługę"), ("Change ID", "Zmień ID"), ("Website", "Strona internetowa"), - ("About", "O"), - ("Slogan_tip", ""), - ("Privacy Statement", ""), - ("Build Date", ""), - ("Version", ""), - ("Home", ""), + ("About", "O aplikacji"), + ("Slogan_tip", "Tworzone z miłością w tym pełnym chaosu świecie!"), + ("Privacy Statement", "Oświadczenie o ochronie prywatności"), + ("Build Date", "Zbudowano"), + ("Version", "Wersja"), + ("Home", "Pulpit"), ("Mute", "Wycisz"), ("Audio Input", "Wejście audio"), ("Enhancements", "Ulepszenia"), @@ -99,7 +99,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty Directory", "Pusty katalog"), ("Not an empty directory", "Katalog nie jest pusty"), ("Are you sure you want to delete this file?", "Czy na pewno chcesz usunąć ten plik?"), - ("Are you sure you want to delete this empty directory?", "Czy na pewno chcesz usunać ten pusty katalog?"), + ("Are you sure you want to delete this empty directory?", "Czy na pewno chcesz usunąć ten pusty katalog?"), ("Are you sure you want to delete the file of this directory?", "Czy na pewno chcesz usunąć pliki z tego katalogu?"), ("Do this for all conflicts", "wykonaj dla wszystkich konfliktów"), ("This is irreversible!", "To jest nieodwracalne!"), @@ -121,7 +121,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Good image quality", "Dobra jakość obrazu"), ("Balanced", "Zrównoważony"), ("Optimize reaction time", "Zoptymalizuj czas reakcji"), - ("Custom", "Własne"), + ("Custom", "Niestandardowe"), ("Show remote cursor", "Pokazuj zdalny kursor"), ("Show quality monitor", "Parametry połączenia"), ("Disable clipboard", "Wyłącz schowek"), @@ -141,10 +141,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to make direct connection to remote desktop", "Nie udało się nawiązać bezpośredniego połączenia z pulpitem zdalnym"), ("Set Password", "Ustaw hasło"), ("OS Password", "Hasło systemu operacyjnego"), - ("install_tip", "RustDesk może nie działać poprawnie na maszynie zdalnej z przyczyn związanych z UAC. W celu uniknięcią problemów z UAC, kliknij poniższy przycisk by zainstalować RustDesk w swoim systemie."), + ("install_tip", "RustDesk może nie działać poprawnie na maszynie zdalnej z przyczyn związanych z UAC. W celu uniknięcia problemów z UAC, kliknij poniższy przycisk by zainstalować RustDesk w swoim systemie."), ("Click to upgrade", "Zaktualizuj"), ("Click to download", "Pobierz"), - ("Click to update", "Uaktualinij"), + ("Click to update", "Uaktualnij"), ("Configure", "Konfiguruj"), ("config_acc", "Konfiguracja konta"), ("config_screen", "Konfiguracja ekranu"), @@ -211,13 +211,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Run without install", "Uruchom bez instalacji"), ("Always connected via relay", "Zawsze połączony pośrednio"), ("Always connect via relay", "Zawsze łącz pośrednio"), - ("whitelist_tip", "Zezwlaj na łączenie z tym komputerem tylko z adresów IP znajdujących się na białej liście"), + ("whitelist_tip", "Zezwalaj na łączenie z tym komputerem tylko z adresów IP znajdujących się na białej liście"), ("Login", "Zaloguj"), - ("Verify", ""), - ("Remember me", ""), - ("Trust this device", ""), - ("Verification code", ""), - ("verification_tip", ""), + ("Verify", "Zweryfikuj"), + ("Remember me", "Zapamiętaj mnie"), + ("Trust this device", "Dodaj to urządzenie do zaufanych"), + ("Verification code", "Kod weryfikacyjny"), + ("verification_tip", "Nastąpiło logowanie z nowego urządzenia, kod weryfikacyjny został wysłany na podany adres email, wprowadź kod by kontynuować proces logowania"), ("Logout", "Wyloguj"), ("Tags", "Tagi"), ("Search ID", "Szukaj ID"), @@ -235,7 +235,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Favorites", "Ulubione"), ("Add to Favorites", "Dodaj do ulubionych"), ("Remove from Favorites", "Usuń z ulubionych"), - ("Empty", "Pusty"), + ("Empty", "Pusto"), ("Invalid folder name", "Nieprawidłowa nazwa folderu"), ("Socks5 Proxy", "Socks5 Proxy"), ("Hostname", "Nazwa hosta"), @@ -334,7 +334,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scroll Style", "Styl przewijania"), ("Show Menubar", "Pokaż pasek menu"), ("Hide Menubar", "Ukryj pasek menu"), - ("Direct Connection", "Połącznie bezpośrednie"), + ("Direct Connection", "Połączenie bezpośrednie"), ("Relay Connection", "Połączenie przez bramkę"), ("Secure Connection", "Połączenie szyfrowane"), ("Insecure Connection", "Połączenie nieszyfrowane"), @@ -347,12 +347,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Dark", "Ciemny"), ("Light", "Jasny"), ("Follow System", "Zgodne z systemem"), - ("Enable hardware codec", "Włącz wsparcie sprzętowe dla kodeków"), - ("Unlock Security Settings", "Odblokuj Ustawienia Zabezpieczeń"), - ("Enable Audio", "Włącz Dźwięk"), + ("Enable hardware codec", "Włącz akcelerację sprzętową kodeków"), + ("Unlock Security Settings", "Odblokuj ustawienia zabezpieczeń"), + ("Enable Audio", "Włącz dźwięk"), ("Unlock Network Settings", "Odblokuj ustawienia Sieciowe"), ("Server", "Serwer"), - ("Direct IP Access", "Bezpośredni Adres IP"), + ("Direct IP Access", "Bezpośredni adres IP"), ("Proxy", "Proxy"), ("Apply", "Zastosuj"), ("Disconnect all devices?", "Czy rozłączyć wszystkie urządzenia?"), @@ -364,20 +364,20 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable RDP", "Włącz RDP"), ("Pin menubar", "Przypnij pasek menu"), ("Unpin menubar", "Odepnij pasek menu"), - ("Recording", "Trwa nagrywanie"), + ("Recording", "Nagrywanie"), ("Directory", "Katalog"), ("Automatically record incoming sessions", "Automatycznie nagrywaj sesje przychodzące"), ("Change", "Zmień"), ("Start session recording", "Zacznij nagrywać sesję"), ("Stop session recording", "Zatrzymaj nagrywanie sesji"), - ("Enable Recording Session", "Włącz Nagrywanie Sesji"), + ("Enable Recording Session", "Włącz nagrywanie Sesji"), ("Allow recording session", "Zezwól na nagrywanie sesji"), ("Enable LAN Discovery", "Włącz wykrywanie urządzenia w sieci LAN"), ("Deny LAN Discovery", "Zablokuj wykrywanie urządzenia w sieci LAN"), ("Write a message", "Napisz wiadomość"), ("Prompt", "Monit"), - ("Please wait for confirmation of UAC...", "Oczekuje potwierdzenia ustawień UAC"), - ("elevated_foreground_window_tip", ""), + ("Please wait for confirmation of UAC...", "Poczekaj na potwierdzenie uprawnień UAC"), + ("elevated_foreground_window_tip", "Aktualne okno zdalnego urządzenia wymaga wyższych uprawnień by poprawnie działać, chwilowo niemożliwym jest korzystanie z myszy i klawiatury. Możesz poprosić zdalnego użytkownika o minimalizację okna, lub nacisnąć przycisk podniesienia uprawnień w oknie zarządzania połączeniami. By uniknąć tego problemu, rekomendujemy instalację oprogramowania na urządzeniu zdalnym."), ("Disconnected", "Rozłączone"), ("Other", "Inne"), ("Confirm before closing multiple tabs", "Potwierdź przed zamknięciem wielu kart"), @@ -385,7 +385,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Full Access", "Pełny dostęp"), ("Screen Share", "Udostępnianie ekranu"), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland wymaga Ubuntu 21.04 lub nowszego."), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland wymaga wyższej wersji dystrybucji Linuksa. Wypróbuj pulpit X11 lub zmień system operacyjny."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland wymaga nowszej dystrybucji Linuksa. Wypróbuj pulpit X11 lub zmień system operacyjny."), ("JumpLink", "View"), ("Please Select the screen to be shared(Operate on the peer side).", "Wybierz ekran do udostępnienia (działaj po stronie równorzędnej)."), ("Show RustDesk", "Pokaż RustDesk"), @@ -403,39 +403,39 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("One-time password length", "Długość hasła jednorazowego"), ("Request access to your device", "Żądanie dostępu do Twojego urządzenia"), ("Hide connection management window", "Ukryj okno zarządzania połączeniem"), - ("hide_cm_tip", ""), - ("wayland_experiment_tip", ""), - ("Right click to select tabs", ""), - ("Skipped", ""), + ("hide_cm_tip", "Pozwalaj na ukrycie tylko gdy akceptujesz sesje za pośrednictwem hasła i używasz hasła permanentnego"), + ("wayland_experiment_tip", "Wsparcie dla Wayland jest niekompletne, użyj X11 jeżeli chcesz korzystać z dostępu nienadzorowanego"), + ("Right click to select tabs", "Kliknij prawym przyciskiem myszy by wybrać zakładkę"), + ("Skipped", "Pominięte"), ("Add to Address Book", "Dodaj do Książki Adresowej"), ("Group", "Grypy"), ("Search", "Szukaj"), - ("Closed manually by web console", ""), - ("Local keyboard type", ""), - ("Select local keyboard type", ""), - ("software_render_tip", ""), - ("Always use software rendering", ""), - ("config_input", ""), - ("request_elevation_tip", ""), - ("Wait", ""), - ("Elevation Error", ""), - ("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", ""), - ("Request Elevation", ""), - ("wait_accept_uac_tip", ""), - ("Elevate successfully", ""), - ("uppercase", ""), - ("lowercase", ""), - ("digit", ""), - ("special character", ""), - ("length>=8", ""), - ("Weak", ""), - ("Medium", ""), - ("Strong", ""), - ("Switch Sides", ""), - ("Please confirm if you want to share your desktop?", ""), - ("Closed as expected", ""), + ("Closed manually by web console", "Zakończone manualnie z konsoli Web"), + ("Local keyboard type", "Lokalny typ klawiatury"), + ("Select local keyboard type", "Wybierz lokalny typ klawiatury"), + ("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\"."), + ("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ń"), + ("Ask the remote user for authentication", "Poproś użytkownika zdalnego o uwierzytelnienie"), + ("Choose this if the remote account is administrator", "Wybierz to jeżeli zdalne konto jest administratorem"), + ("Transmit the username and password of administrator", "Prześlij nazwę użytkownika i hasło administratora"), + ("still_click_uac_tip", "Nadal wymaga od zdalnego użytkownika potwierdzenia uprawnień UAC."), + ("Request Elevation", "Poproś o podniesienie uprawnień"), + ("wait_accept_uac_tip", "Prosimy czekać aż zdalny użytkownik potwierdzi uprawnienia UAC."), + ("Elevate successfully", "Pomyślnie podniesiono uprawnienia"), + ("uppercase", "wielkie litery"), + ("lowercase", "małe litery"), + ("digit", "cyfra"), + ("special character", "znak specjalny"), + ("length>=8", "długość>=8"), + ("Weak", "Słabe"), + ("Medium", "Średnie"), + ("Strong", "Mocne"), + ("Switch Sides", "Zmień 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"), ].iter().cloned().collect(); } From 6db94983a181a942474554c78b9fbb554df21f24 Mon Sep 17 00:00:00 2001 From: Simon Spannagel Date: Mon, 30 Jan 2023 08:06:48 +0100 Subject: [PATCH 1646/2015] Remove wayland fix for good Signed-off-by: simonspa --- src/platform/linux.rs | 93 ------------------------------------------- src/ui.rs | 10 ----- src/ui/index.tis | 13 ------ src/ui_interface.rs | 14 ------- 4 files changed, 130 deletions(-) diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 34276426d..ac3b32a49 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -426,104 +426,11 @@ pub fn is_login_wayland() -> bool { } } -pub fn fix_login_wayland() { - let mut file = "/etc/gdm3/custom.conf".to_owned(); - if !std::path::Path::new(&file).exists() { - file = "/etc/gdm/custom.conf".to_owned(); - } - match std::process::Command::new("pkexec") - .args(vec![ - "sed", - "-i", - "s/#WaylandEnable=false/WaylandEnable=false/g", - &file, - ]) - .output() - { - Ok(x) => { - let x = String::from_utf8_lossy(&x.stderr); - if !x.is_empty() { - log::error!("fix_login_wayland failed: {}", x); - } - } - Err(err) => { - log::error!("fix_login_wayland failed: {}", err); - } - } -} - pub fn current_is_wayland() -> bool { let dtype = get_display_server(); return "wayland" == dtype && unsafe { UNMODIFIED }; } -pub fn modify_default_login() -> String { - let dsession = std::env::var("DESKTOP_SESSION").unwrap(); - let user_name = std::env::var("USERNAME").unwrap(); - if let Ok(x) = run_cmds("ls /usr/share/* | grep ${DESKTOP_SESSION}-xorg.desktop".to_owned()) { - if x.trim_end().to_string() != "" { - match std::process::Command::new("pkexec") - .args(vec![ - "sed", - "-i", - &format!("s/={0}$/={0}-xorg/g", &dsession), - &format!("/var/lib/AccountsService/users/{}", &user_name), - ]) - .output() - { - Ok(x) => { - let x = String::from_utf8_lossy(&x.stderr); - if !x.is_empty() { - log::error!("modify_default_login failed: {}", x); - return "Fix failed! Please re-login with X server manually".to_owned(); - } else { - unsafe { - UNMODIFIED = false; - } - return "".to_owned(); - } - } - Err(err) => { - log::error!("modify_default_login failed: {}", err); - return "Fix failed! Please re-login with X server manually".to_owned(); - } - } - } else if let Ok(z) = - run_cmds("ls /usr/share/* | grep ${DESKTOP_SESSION:0:-8}.desktop".to_owned()) - { - if z.trim_end().to_string() != "" { - match std::process::Command::new("pkexec") - .args(vec![ - "sed", - "-i", - &format!("s/={}$/={}/g", &dsession, &dsession[..dsession.len() - 8]), - &format!("/var/lib/AccountsService/users/{}", &user_name), - ]) - .output() - { - Ok(x) => { - let x = String::from_utf8_lossy(&x.stderr); - if !x.is_empty() { - log::error!("modify_default_login failed: {}", x); - return "Fix failed! Please re-login with X server manually".to_owned(); - } else { - unsafe { - UNMODIFIED = false; - } - return "".to_owned(); - } - } - Err(err) => { - log::error!("modify_default_login failed: {}", err); - return "Fix failed! Please re-login with X server manually".to_owned(); - } - } - } - } - } - return "Fix failed! Please re-login with X server manually".to_owned(); -} - // to-do: test the other display manager fn _get_display_manager() -> String { if let Ok(x) = std::fs::read_to_string("/etc/X11/default-display-manager") { diff --git a/src/ui.rs b/src/ui.rs index b8473072d..637fc66be 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -434,18 +434,10 @@ impl UI { is_login_wayland() } - fn fix_login_wayland(&mut self) { - fix_login_wayland() - } - fn current_is_wayland(&mut self) -> bool { current_is_wayland() } - fn modify_default_login(&mut self) -> String { - modify_default_login() - } - fn get_software_update_url(&self) -> String { get_software_update_url() } @@ -590,9 +582,7 @@ impl sciter::EventHandler for UI { fn is_installed_daemon(bool); fn get_error(); fn is_login_wayland(); - fn fix_login_wayland(); fn current_is_wayland(); - fn modify_default_login(); fn get_options(); fn get_option(String); fn get_local_option(String); diff --git a/src/ui/index.tis b/src/ui/index.tis index 2d77b1eec..e718e4380 100644 --- a/src/ui/index.tis +++ b/src/ui/index.tis @@ -755,11 +755,6 @@ class FixWayland: Reactor.Component {

    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 literal 6186 zcmcIoc|4R~)W6TnFvi%&7GZ=zhO}6cWk}(Nnv%&VS+XR1ma@cS%bNTWq0%U!no2X- z$}%ZSmT0jQAuY%fLSud2`MrO?|Gww*d_K>;_ndprx#ygF@Auv`>LDv(K{-JHfUu3V z?75o~047NVJooW75w)FXM#NOToqk%uTJAtI58Oe|tmfMhlzZC{b;C4@(le~`jf zgq((`T|&lwBkP;U++U<&5OFO<{2wE|6UfpkGBl0gB9Id$i2Y6E)iAtLsR7EfP|JnB*WmKad~uNar_XM<#OQHu5;{eIL&`%3((j zIRNYyMH}Cc>X%}>T=EzL-xiTd$D*#a8Lili^uwR8lJ1N~8)pC@U1Vcve(VA?&8Za! zL;;{{*0Z4=fr--DauNE^(f=2*Ur+eX5dQruRkLOOQ>Cb-y}7Ia=j$hp?Vd9~cKkK_ zq3HflxgoAWt5ZWvBV%me5@)3+;-Z~Uqh>SruJG`?jRFtZvc`19_j_o=Lq-=}4MM!R z(Y6jFRp`J8=}U1{VTpIw=mz_CugkE%^u1_qw7P_7%ud}JGZRDNWUjn8#tBG8Btk;N z(61c*%sL~8GwJ#}Pm;om_Us;i9elsl*ZR>|fviqm5ufG~O9(ZcaZJ!812)aJaCcX( zIF@xSB*0_u9IX%h_2xd~C&ujoD|ewbCE+uB@n>w3vkmdq&^Xdvg=wGd4b|^y)L=KA zJLraQZ#UknbwRFu((a~INzShAq_;w)C<1J!D=aJFgY{+D-SnbzjM`7%*dm#4(C%?r zX#4BjbB40l4Cm0C9h9NBjFv1V5DPWw^B>Uf1iE-Jy-uO@PITHXg~aW*lxPt8gbkwC!Uqy0RGdFXwYNRDneudaSouJ*?1{#ivf=fYEV8X7 zXk+=q4Z_52fRl)ILjjF{?e6XVG~_Uhx`>~)czW#6@$}xp{i`av&;*L}*r4!{K0+U_ z#rO30_$|9^=9tIj$G%bb7Z%S4Vd&BCR<-j+1_-Ne2)Lc$Z7dnF; zsEiDK_cM-dr%t(TVKVb)WJ4w4XPvj%K{N?)e${_3wQV3B$6iF$lU=K0x}@;50eM{r z(3C5kU&M5@Kwpw(vVua_9(wHP6wU{f!HbW7FQ^Zt+Z%lHZTPAr+*u{r&Tj``2m34N zO^Y(Y=g=FGI8E6yc@a01z{vH+8u>W`rtxzKyD1$PLLShClCW-Sw%_2!d$%wy<1^eB zeLo&x^MM64CwD_6?u1~N)LDnk2J;9S)($j^SWmOjoADAZDIefCk6PIPh`>!Epe)zI(Rjy-Gfa6&@X0=LX3~g-IP;4}Fp==Z4}c%q_oCLk4QIP8dMjS{id; z9H8m#c85O5O+46@u~%sd#*H>mR>t-5wPEJ{16tDGtGq?*z~7{(d)*2Akv)2SS;^YF z9py>zzEkTb$o_Obmv7om7W-TPPYAS+$4JX`nja_5w;VlX#p}1K9CDJK=@w@KuNM5k ze993fz^53V`fi%D9ZR6d5`>D5}?H`zm^3iGJTjh6JSC z?is~g0e#HAS{@4=3T^r;z|i+<*4d$N2d+{_YGQ6Q4RU6*9_nAqXNXCs0&TY1QE}1myJlS`~F$Fhz zgDTI`4-^lRk~5uJoaSKWw99>*Zogf71O#RJG&ShDPz?H}Pv4pGAwgc`ob9u!aHI0f z{T#W=6oY|o=J9Az0;fOS)NjvlLLg_zr@mA@A2h%%$$HME6oW{uK4xzsZq#9-h+gI+ z8b%NdVt(4r%Sm-=_ri|hOqE8r`k|{{C~i`&_htUBCk=4}rC4Szf+BDNnsI&vp>gR6 ze1)7=J&u{93k|^^3d85m@4)pKa2^J(GT&8*5IBwlI!;&Ktf-e6b9A<*1oV*t3czDl zJM)#>yPN&EMKqklB=a*<%y?D^cE->GBLis)D9g7B#tP1+Q@XQFm^q~$1@_rny$y!| z4$3}L;_5;R`oZ%<&?wVM`~1oF6Tk+P(}skdD&0I$T+vq8QPBCWwt1Jk=M539s<$fb zkN~BjTaYi~4oiCo(i%C*cHV*|fdCm$?Pv!|M9QhoQ*{FQ;HWLloB0)BsnwD0$)zs& zU{mDFaoRnHUr>qS^m>a=xPB3C^mm=G7OQh&@Tq)X%mi!urXmS$Y)a`qYv&+ht@QDV zAMKu_l_XjWUQNh#`Z)}}$1N6}U~Q91_XMb-q1I1#We%NfKg%3(M{!l^4!>gCR&$G% zP(|7VjuO3_$tj^@zhEU&pocw=6YM~F75!A>nfO`0y)PaO%8bL75NI|p$H`Q=luw*Vnio?ILUf8s&PV&T_g@n&&qe0HWDTj6y9qM;LG zTvo&o_fv69lk=UDOstvg{lXY8#B^#bF(_m;c)e8f0WLoP_hEu-Je_??yk zR|BN`pV=Iftv2wxJthNkorXN{f-)c9lD~?Tf$u5s48MlI?u|Cd8Db#Rd7VrSaG0Q+ zz5Dg0W+2qoi7N7A%$YAVjfw2UgUaL5Qj+Gy$F91{O5O&lv;*@vL#1iSq50p;YGf@CkI6Ccr3c*rK9Nl@kbfk)A)N5JyISjh2pEGpq* zCW^D1%ja^HG1o-E+d5>T#9W0|{T(#kjC|3NNVNyoXmcM`&xS&`{tjSZ%M89pWI;XE zedM-_X@^16;RN+iYK@Iu@HI_+DAV(Cj_m91{$PMLA(`+=3T17srbwLs^*bRw{9g>zcI zVS1d>4=N^tI%Rx8A4SySLjJ36$rX?^4xLvt5h`o;V4Ye-w^iRY7A4pI+R%<~!_J9U zWJ^xCGnzbBD<`mXl3%S}zMpFPywZlHc+DU9Ac`)FpZN2*4Lj;ZZwtfUX&d3MVV7fJ zOWRnJ;H2Yx3MxvR2!*9Iw}O$qItNVQm=hP$RYm^Bd!KRxuh1*^o@v$snrV||?x=qB! zHKsy-2Am_mwl|@H%aqBP$B|n*b6W-7l=7Fr+3KT#rZk{A-({hqNJ(q{?jJ=gub>mk zmk)c7zeAfBHj|ChF94M#*UK6TTXQOTv%dt%Uio*}OJ&yh>%INdiDStzeKYdJ{-;xI==Y)s}k5;1`rSqI~!P0~`NZJ|ptWK~qpd?-BUdRip{u>Myya&_SiyT`^TD zOK}Bw>ty(#?pYI4&`S3Tw6 zsMJ>yenDQ>2Wz%IsBHbriQ4FMNHlJJKbopg!6+tV0NpVSS9GvjT3VJ9hPp)mC@$WB zsyXml1)kCBK&&rG$r0uwvo!dLx-uFKp4&0gyE2UWPfGw1GSlCoyx73&$le=wBwA{W zn=87;(_w-*;uDhSP=_K$zmhOgr?zoAo+c#|*r39dmBd5~%1B-(pBEKlgDRgqv8|dX z=S;nDGoC{z;#pQ^xT55?U>aFv>c|j^YP?Y0`Y|N;7a>OU3*`tA2-{M6(g_X9$_+26QxD;2PqsI##sY4Tkng#6&aXlNbToALV=28t7cDUi#78cUofh0QyTQO>| zl!(Gy*|-753(9$^3W--3JD^d=l1MzNP@=^Al;1mNMj?TwcV2pz9OX-7dcJU()^G31 zVu-r(s!WH^s1os;@2)vcj*H&|uZ!P#I}Uq`Sj#wF_UBl<4uW_~fbL84E(mZUDo|Fs z;9g$pKH|wmc!%@X=3tKse+)R~RQz%)4^nA$l9<%!-eAzI6zALZ&pqwceFV7L{oeYt zzb#P@Xcf$|oyX30*j1bS=S@7}y_(uSEktow&v8KLi(?GGxRw|-4R z?7s1FFcexrZ|GWqM}MLl2Cn}yzREQB!xK1-O;ycCHP@$i{stDeH~$#ADSQsi#*Mbf zSILD%9Ojef&X+ZMnhYh{lVJJZ1AgyEI!$8Y2KqG``I+`49&WwtqQ5x5TWQ1=EL|_| zmaA__v;zjG*!{k&e|Oz@7FTm8MMclMKj+EtzxRS z^!f7uW-z{Pk6+aa6Bx?#Kr*@j1X`GM6zXX|$K8jQoNrS|@L|SUOO)_WuE*bhF7xl^ zndwlzOn-7Kafisp2k#Zn9NZ{Azml%im_GRXzV8ox9`Z3VIIn?d{>W>HIC#N zF6ne1uN6`EfmmV2HYWk!KRX{Th20+H^raosg@|S!#o*aAqQQqm97WjQ0Ma^Et4L9C z3+KOwX~QrCZkAPTf^Z{@F67>{0`VKLr9>YgJKl~ zlJQ#erKd;E5kUL{{G;Fqx%`2NLF3{($3F`{Aq&2`6Z z_9~>>0XdKyc4FfWl@&_vbeT30D*&?*3T@m6BY5I=cKN&pw2i>o;H=2<-?>dw2y@J7 zwiDHZ?ZA0H)l}IX+eOrb+@7^89cZm?b zFKO#a%FuiP;xK?>e)u=PvbwWqq(}To%JKzfH93s2SdLkR8C>t|Y>JBqg zVA2Q06`XGR&@}%0tQw`ixM@T(^M;KmaTk0f5^YR&yt&^bsf7qKG1I#P2eDF zJEAaLb+eTl{v9kFn0UD!udvo0wKb_2#NZ-4hA6FH?yrq}UmTwlUx=Hn#!wIQg>g0b zk1Y0t{7M%3b(s1dx89HqZ8>`*Ws}>?XH@S!f7)mh?|s_IAoAl4l!#fS=?p zmB-X-e8E#R86t!B#})@qLU}+Mgb$MWd+NSohfx>MR6W%aCl5QSg6}ia zAro5p$goH^EThNl#B=4*{F4_OUehO@bj_c8tRT|4;(qfzd9n59p38~n4I3(?qx)M` z7jGP$eo^m)HqFl2(azecwVM2t+|hJ<&V27GUrZoY#QMLtWz0t0mn+o&O8*y)YLSL- UOVY_Hz5j7-4ji(4Y~hyhKatt@b^rhX 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 64558031098587ed42fa4c3edad58b95d7fe67bd..daacb85ae376f30551702e9ba0ef7bf56fb981a7 100644 GIT binary patch literal 2618 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 delta 889 zcmV-<1BU#%6om(n8Gi!+005o0f$RVP0Jl&~R7C&)0043S0CE2SasL2u{{V6S0CE2S zasL2u{{V6S0CE2SasL2u|NsC00B`>kegE+H{|l;|MmL+v(^8N!2j6l z|2&ibDTx2&@&D51|4f_zHjn?{?*Go@|166CB!&O!^Z%^T|9_&)|9`muWvc%gfd9nb z|EbUag}eVkmj5w~|CPl5h`s-Fvj1?f|81`SQl9@Fg8%&f|LpYtzT5w}*#Dcz|6iv6 zS)%`UwEqO5;q?Fj01R|ePE!Du$&iUDALU{)uGU0&z)1K~#9!?Ux0b>mU?H z#})p#*WKN0|9|V6X(8pc$w$a@J0sP5aJe`C`OSEIdAL>J;r%f+{OSIgDDr&&l#GA9 zBRRNxO_a~$l?~T*xFdASfgkR$;31JVQ!7lPZAa4jerAn43X zTAJckDu5Mu702uo817Q9c!7;P0+67m(@!b}!aDs#Z+{F3lm*aQD|(`!VW`s+x~D-= zfSOA=J`*t3RVWD%c3A+cL3Jfwm7pNNLd4UCg^n*?R;tJp!06GKa`PBabn7%`qajl; zUB>M{)U4yzVGqX<<+QSDfUry%y%0E$YH33m^%62_;8DiNz{SAE4cp3VfEuqrgU839 z!@D*LA%7K2dH3<|7GS{V!%Ql$d0Sf`*I3PXWF-RBB2mvPP&C=#bRZG%FS!;;fCZaT zdPyeWUC;+fVz22DvnUgIqN9@VPT0u=Blewks$eTVlndlFkPF6=KCsv>vD_b395BARQnV`i=OBj5`RHF*3`9;m`AJy)RQl!1N)Yl97sMz zoT6M1MtnI27Y2~FBU+%eq4-}OB(Ntsui6e7TUyQMhI+ro6o4HxOv-SL3<1I35u5Vd zCIp?ft5Z%?Lgu+(jp-S7xQf@#4hjd0!50lbT!*@dpQs&e#>o8QXba9w(q~q-fcJS_ z9CEHDJ(d;Kk9QKd4r{{saE-hL)C9Z|#b((&9j;o1Ql~H;5|D&IcFBTOo3~ma2noE& zAnlU$kaf+&5%HMd<(n^>-+I;k=F9fCU-y#<$6Jns Date: Mon, 23 Jan 2023 18:50:25 +0100 Subject: [PATCH 1604/2015] Use OUT_DIR in build.rs --- libs/hbb_common/.gitignore | 1 - libs/hbb_common/build.rs | 7 +++++-- libs/hbb_common/src/protos/mod.rs | 1 + 3 files changed, 6 insertions(+), 3 deletions(-) create mode 100644 libs/hbb_common/src/protos/mod.rs diff --git a/libs/hbb_common/.gitignore b/libs/hbb_common/.gitignore index b1cf151e3..693699042 100644 --- a/libs/hbb_common/.gitignore +++ b/libs/hbb_common/.gitignore @@ -1,4 +1,3 @@ /target **/*.rs.bk Cargo.lock -src/protos/ diff --git a/libs/hbb_common/build.rs b/libs/hbb_common/build.rs index 225ec34c7..bff0cfafc 100644 --- a/libs/hbb_common/build.rs +++ b/libs/hbb_common/build.rs @@ -1,8 +1,11 @@ fn main() { - std::fs::create_dir_all("src/protos").unwrap(); + 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("src/protos") + .out_dir(out_dir) .inputs(&["protos/rendezvous.proto", "protos/message.proto"]) .include("protos") .customize( diff --git a/libs/hbb_common/src/protos/mod.rs b/libs/hbb_common/src/protos/mod.rs new file mode 100644 index 000000000..c001c58fb --- /dev/null +++ b/libs/hbb_common/src/protos/mod.rs @@ -0,0 +1 @@ +include!(concat!(env!("OUT_DIR"), "/protos/mod.rs")); \ No newline at end of file From c8058cd125e843f4a551e0c8179c1e4647807cc8 Mon Sep 17 00:00:00 2001 From: mehdi-song Date: Tue, 24 Jan 2023 08:08:53 +0330 Subject: [PATCH 1605/2015] Update fa.rs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ("Group", "گروه"), ("Search", "جستجو"), ("Closed manually by the web console", "به صورت دستی توسط کنسول وب بسته شد"), ("Local keyboard type", "نوع صفحه کلید محلی"), ("Select local keyboard type", "نوع صفحه کلید محلی را انتخاب کنید"), ("software_render_tip", "اگر کارت گرافیک Nvidia دارید و پنجره راه دور بلافاصله پس از اتصال بسته می شود، درایور nouveau را نصب نمایید و انتخاب گزینه استفاده از رندر نرم افزار می تواند کمک کننده باشد. راه اندازی مجدد نرم افزار مورد نیاز است."), ("Always use software rendering", "همیشه از رندر نرم افزاری استفاده کنید"), ("config_input", "برای کنترل دسکتاپ از راه دور با صفحه کلید، باید مجوز RustDesk \"Input Monitoring\" را بدهید."), ("request_elevation_tip", "همچنین می توانید در صورت وجود شخصی در سمت راه دور درخواست ارتفاع دهید."), ("Wait", "صبر کنید"), ("Elevation Error", "خطای ارتفاع"), ("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", "همچنان کاربر از راه دور نیاز دارد که روی OK در پنجره UAC اجرای RustDesk کلیک کند."), ("Request Elevation", "درخواست ارتفاع"), ("wait_accept_uac_tip", "لطفاً منتظر بمانید تا کاربر راه دور درخواست پنجره UAC را بپذیرد."), ("Elevate successfully", "با موفقیت بالا ببرید"), ("uppercase", "حروف بزرگ"), ("lowercase", "حروف کوچک"), ("digit", "عدد"), ("special character", "کاراکتر خاص"), ("length>=8", "حداقل طول 8 کاراکتر"), ("Weak", "ضعیف"), ("Medium", "متوسط"), ("Strong", "قوی"), ("Switch Sides", "طرفین را عوض کنید"), ("Please confirm if you want to share your desktop?", "لطفاً تأیید کنید که آیا می خواهید دسکتاپ خود را به اشتراک بگذارید؟"), --- src/lang/fa.rs | 70 +++++++++++++++++++++++++------------------------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/src/lang/fa.rs b/src/lang/fa.rs index dfd76405e..b107bb91a 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -210,11 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "برای اتصال استفاده شود Relay از"), ("whitelist_tip", "های مجاز می توانند به این دسکتاپ متصل شوند IP فقط"), ("Login", "ورود"), - ("Verify", ""), - ("Remember me", ""), - ("Trust this device", ""), - ("Verification code", ""), - ("verification_tip", ""), + ("Verify", "تأیید کنید"), + ("Remember me", "مرا به یاد داشته باش"), + ("Trust this device", "به این دستگاه اعتماد کنید"), + ("Verification code", "کد تایید"), + ("verification_tip", "یک دستگاه جدید شناسایی شده است و یک کد تأیید به آدرس ایمیل ثبت شده ارسال شده است، برای ادامه ورود، کد تأیید را وارد کنید."), ("Logout", "خروج"), ("Tags", "برچسب ها"), ("Search ID", "جستجوی شناسه"), @@ -383,7 +383,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Share", "اشتراک گذاری صفحه"), ("Wayland requires Ubuntu 21.04 or higher version.", "نیازمند اوبونتو نسخه 21.04 یا بالاتر است Wayland"), ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "استفاده کنید و یا سیستم عامل خود را تغییر دهید X11 نیازمند نسخه بالاتری از توزیع لینوکس است. لطفا از دسکتاپ با سیستم"), - ("JumpLink", ""), + ("JumpLink", "چشم انداز"), ("Please Select the screen to be shared(Operate on the peer side).", "لطفاً صفحه‌ای را برای اشتراک‌گذاری انتخاب کنید (در سمت همتا به همتا کار کنید)."), ("Show RustDesk", "RustDesk نمایش"), ("This PC", "This PC"), @@ -403,35 +403,35 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", "فقط در صورت پذیرفتن جلسات از طریق رمز عبور و استفاده از رمز عبور دائمی، مخفی شدن مجاز است"), ("wayland_experiment_tip", "پشتیبانی Wayland در مرحله آزمایشی است، لطفاً در صورت نیاز به دسترسی بدون مراقبت از X11 استفاده کنید."), ("Right click to select tabs", "برای انتخاب تب ها راست کلیک کنید"), - ("Skipped", ""), + ("Skipped", "رد شد"), ("Add to Address Book", "افزودن به دفترچه آدرس"), - ("Group", ""), - ("Search", ""), - ("Closed manually by the web console", ""), - ("Local keyboard type", ""), - ("Select local keyboard type", ""), - ("software_render_tip", ""), - ("Always use software rendering", ""), - ("config_input", ""), - ("request_elevation_tip", ""), - ("Wait", ""), - ("Elevation Error", ""), - ("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", ""), - ("Request Elevation", ""), - ("wait_accept_uac_tip", ""), - ("Elevate successfully", ""), - ("uppercase", ""), - ("lowercase", ""), - ("digit", ""), - ("special character", ""), - ("length>=8", ""), - ("Weak", ""), - ("Medium", ""), - ("Strong", ""), - ("Switch Sides", ""), - ("Please confirm if you want to share your desktop?", ""), + ("Group", "گروه"), + ("Search", "جستجو"), + ("Closed manually by the web console", "به صورت دستی توسط کنسول وب بسته شد"), + ("Local keyboard type", "نوع صفحه کلید محلی"), + ("Select local keyboard type", "نوع صفحه کلید محلی را انتخاب کنید"), + ("software_render_tip", "اگر کارت گرافیک Nvidia دارید و پنجره راه دور بلافاصله پس از اتصال بسته می شود، درایور nouveau را نصب نمایید و انتخاب گزینه استفاده از رندر نرم افزار می تواند کمک کننده باشد. راه اندازی مجدد نرم افزار مورد نیاز است."), + ("Always use software rendering", "همیشه از رندر نرم افزاری استفاده کنید"), + ("config_input", "برای کنترل دسکتاپ از راه دور با صفحه کلید، باید مجوز RustDesk \"Input Monitoring\" را بدهید."), + ("request_elevation_tip", "همچنین می توانید در صورت وجود شخصی در سمت راه دور درخواست ارتفاع دهید."), + ("Wait", "صبر کنید"), + ("Elevation Error", "خطای ارتفاع"), + ("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", "همچنان کاربر از راه دور نیاز دارد که روی OK در پنجره UAC اجرای RustDesk کلیک کند."), + ("Request Elevation", "درخواست ارتفاع"), + ("wait_accept_uac_tip", "لطفاً منتظر بمانید تا کاربر راه دور درخواست پنجره UAC را بپذیرد."), + ("Elevate successfully", "با موفقیت بالا ببرید"), + ("uppercase", "حروف بزرگ"), + ("lowercase", "حروف کوچک"), + ("digit", "عدد"), + ("special character", "کاراکتر خاص"), + ("length>=8", "حداقل طول 8 کاراکتر"), + ("Weak", "ضعیف"), + ("Medium", "متوسط"), + ("Strong", "قوی"), + ("Switch Sides", "طرفین را عوض کنید"), + ("Please confirm if you want to share your desktop?", "لطفاً تأیید کنید که آیا می خواهید دسکتاپ خود را به اشتراک بگذارید؟"), ].iter().cloned().collect(); } From 374167b7827a78c9f738c5663746c3af550e3da0 Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Tue, 24 Jan 2023 10:30:29 +0100 Subject: [PATCH 1606/2015] compressed --- .../AppIcon.appiconset/app_icon_1024.png | Bin 100419 -> 23562 bytes .../AppIcon.appiconset/app_icon_128.png | Bin 5792 -> 2409 bytes .../AppIcon.appiconset/app_icon_16.png | Bin 569 -> 338 bytes .../AppIcon.appiconset/app_icon_256.png | Bin 14429 -> 4616 bytes .../AppIcon.appiconset/app_icon_32.png | Bin 1256 -> 644 bytes .../AppIcon.appiconset/app_icon_512.png | Bin 37270 -> 9733 bytes .../AppIcon.appiconset/app_icon_64.png | Bin 2618 -> 1222 bytes 7 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 4b6ea50696ab62a80cf4eaa9ce6d344f8c478917..1c6cf008ad8720ee4680a32de29836e8d31ed7fa 100644 GIT binary patch literal 23562 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 literal 100419 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%0DUB){U4Ess|GV&(7(tHoy(1AJxTUO5(rOw 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 emy;JjFXvw)9L=}O@|l|e0000eAnfB_�tgZU zTFe4Nm;f(%812Jv`F>U~H|NsC026X>*vj3#b|2~xe8i4-}c>nSE z|Ig(A%HjWcwEy?||MdC)f4Bc=tN$~O|1gaI=<@&4=KsXr|9{5B^W*>k01$LiPE!DL zgl%j+1+;%}Xho!=Q&N{R^t}K80D?(GK~xyiZH`wKgdhw>$H86$V#TieU!w`4M~}Xr zOP))>&HTTXpQ3+h2NzTe UgN~k=m;e9(07*qoM6N<$f=%R#eUBBYS`}Y4!Rckss>SojZC|3Zicxp>?@`5NVnlBW76t$K@pSz-WpB3*N zao^n-uL7u!M4~?VND|KMf?HE!_jPOg0Su0XF{)#2f&jpXw6_J01Djwt4HF+=3(VX`k}7p_jixgH%8gBPEWz0kBq+mh2T_)F&EYd+1ofkD;Ee}SKR+I!c- z<#BMl5O~iznAN^~ApkP!0i?h$M<(rWAa@v!-H}$$r+;8WCmh%IZzrHq*Mf3O!83yo zp-opD*ainT3+2ReIC-yWjTOhK0eCZS-_-Yd>ONf3Ewo4F?a(#(m>OpCO>%_6boP;1 zcz)Z9ZR`mGz86JXiCLJqtKQ!n3jxZro{`y+wpR70)Z*3MSW#Q1VhE?;P-XvTdZk{E tW_6BR10<-nQT*yKIh4`YOzXO>8WC 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 b*WWN4QtqF=Dsb+fjK7(Yff<&phmZOnXjx(N literal 14429 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 S*^ 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 literal 37270 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

    ; } - event click $(#fix-wayland) { - handler.fix_login_wayland(); - app.update(); - } - event click $(#help-me) { handler.open_url(translate("doc_fix_wayland")); } @@ -774,14 +769,6 @@ class ModifyDefaultLogin: Reactor.Component {
    ; } - event click $(#modify-default-login) { - if (var r = handler.modify_default_login()) { - // without handler, will fail, fucking stupid sciter - handler.msgbox("custom-error", "Error", r); - } - app.update(); - } - event click $(#help-me) { handler.open_url(translate("doc_fix_wayland")); } diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 403951eaa..4e0fd7744 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -614,12 +614,6 @@ pub fn is_login_wayland() -> bool { return false; } -#[inline] -pub fn fix_login_wayland() { - #[cfg(target_os = "linux")] - crate::platform::linux::fix_login_wayland(); -} - #[inline] pub fn current_is_wayland() -> bool { #[cfg(target_os = "linux")] @@ -628,14 +622,6 @@ pub fn current_is_wayland() -> bool { return false; } -#[inline] -pub fn modify_default_login() -> String { - #[cfg(target_os = "linux")] - return crate::platform::linux::modify_default_login(); - #[cfg(not(target_os = "linux"))] - return "".to_owned(); -} - #[inline] pub fn get_software_update_url() -> String { SOFTWARE_UPDATE_URL.lock().unwrap().clone() From baa30a49b9ace2e45831b6162baa5b511bfd4954 Mon Sep 17 00:00:00 2001 From: Simon Spannagel Date: Mon, 30 Jan 2023 08:39:54 +0100 Subject: [PATCH 1647/2015] Remove remnant documentation for wayland fix --- src/lang/en.rs | 1 - src/ui/index.tis | 30 ------------------------------ 2 files changed, 31 deletions(-) diff --git a/src/lang/en.rs b/src/lang/en.rs index 6eed43a77..bacef699c 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -25,7 +25,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_version_audio_tip", "The current Android version does not support audio capture, please upgrade to Android 10 or higher."), ("android_start_service_tip", "Tap [Start Service] or OPEN [Screen Capture] permission to start the screen sharing service."), ("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"), - ("doc_fix_wayland", "https://rustdesk.com/docs/en/manual/linux/#x11-required"), ("server_not_support", "Not yet supported by the server"), ("android_open_battery_optimizations_tip", "If you want to disable this feature, please go to the next RustDesk application settings page, find and enter [Battery], Uncheck [Unrestricted]"), ("remote_restarting_tip", "Remote device is restarting, please close this message box and reconnect with permanent password after a while"), diff --git a/src/ui/index.tis b/src/ui/index.tis index e718e4380..68787c86f 100644 --- a/src/ui/index.tis +++ b/src/ui/index.tis @@ -558,8 +558,6 @@ class App: Reactor.Component {is_can_screen_recording && !handler.is_process_trusted(false) ? : ""} {!service_stopped && is_can_screen_recording && handler.is_process_trusted(false) && handler.is_installed() && !handler.is_installed_daemon(false) ? : ""} {system_error ? : ""} - {!system_error && handler.is_login_wayland() && !handler.current_is_wayland() ? : ""} - {!system_error && handler.current_is_wayland() ? : ""}
    @@ -746,34 +744,6 @@ class InstallDaemon: Reactor.Component { } } -class FixWayland: Reactor.Component { - function render() { - return
    -
    {translate('Warning')}
    -
    {translate('Login screen using Wayland is not supported')}
    -
    {translate('Help')}
    -
    ; - } - - event click $(#help-me) { - handler.open_url(translate("doc_fix_wayland")); - } -} - -class ModifyDefaultLogin: Reactor.Component { - function render() { - return
    -
    {translate('Warning')}
    -
    {translate('Current Wayland display server is not supported')}
    -
    {translate('Help')}
    -
    ; - } - - event click $(#help-me) { - handler.open_url(translate("doc_fix_wayland")); - } -} - function watch_trust() { // not use TrustMe::update, because it is buggy var trusted = handler.is_process_trusted(false); From 91244ea610d94a328ebdd3fcaac420c6da7426a7 Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Mon, 30 Jan 2023 15:40:03 +0800 Subject: [PATCH 1648/2015] Update cn.rs --- src/lang/cn.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/lang/cn.rs b/src/lang/cn.rs index c028ed36c..8126e0081 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -281,12 +281,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Do you accept?", "是否接受?"), ("Open System Setting", "打开系统设置"), ("How to get Android input permission?", "如何获取安卓的输入权限?"), - ("android_input_permission_tip1", "为了让远程设备通过鼠标或触屏控制您的安卓设备,你需要允許RustDesk使用\"无障碍\"服务。"), + ("android_input_permission_tip1", "为了让远程设备通过鼠标或触屏控制您的安卓设备,你需要允許 RustDesk 使用\"无障碍\"服务。"), ("android_input_permission_tip2", "请在接下来的系统设置页面里,找到并进入 [已安装的服务] 页面,将 [RustDesk Input] 服务开启。"), ("android_new_connection_tip", "收到新的连接控制请求,对方想要控制你当前的设备。"), ("android_service_will_start_tip", "开启录屏权限将自动开启服务,允许其他设备向此设备请求建立连接。"), ("android_stop_service_tip", "关闭服务将自动关闭所有已建立的连接。"), - ("android_version_audio_tip", "当前安卓版本不支持音频录制,请升级至安卓10或更高。"), + ("android_version_audio_tip", "当前安卓版本不支持音频录制,请升级至安卓 10 或更高。"), ("android_start_service_tip", "点击 [启动服务] 或打开 [屏幕录制] 权限开启手机屏幕共享服务。"), ("Account", "账户"), ("Overwrite", "覆盖"), @@ -376,7 +376,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Deny LAN Discovery", "拒绝局域网发现"), ("Write a message", "输入聊天消息"), ("Prompt", "提示"), - ("Please wait for confirmation of UAC...", "请等待对方确认UAC..."), + ("Please wait for confirmation of UAC...", "请等待对方确认 UAC ..."), ("elevated_foreground_window_tip", "远端桌面的当前窗口需要更高的权限才能操作, 暂时无法使用鼠标键盘, 可以请求对方最小化当前窗口, 或者在连接管理窗口点击提升。为避免这个问题,建议在远端设备上安装本软件。"), ("Disconnected", "会话已结束"), ("Other", "其他"), @@ -404,16 +404,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request access to your device", "请求访问你的设备"), ("Hide connection management window", "隐藏连接管理窗口"), ("hide_cm_tip", "在只允许密码连接并且只用固定密码的情况下才允许隐藏"), - ("wayland_experiment_tip", "Wayland支持处于实验阶段,如果你需要使用无人值守访问,请使用X11。"), + ("wayland_experiment_tip", "Wayland 支持处于实验阶段,如果你需要使用无人值守访问,请使用X11。"), ("Right click to select tabs", "右键选择选项卡"), ("Skipped", "已跳过"), ("Add to Address Book", "添加到地址簿"), ("Group", "小组"), ("Search", "搜索"), - ("Closed manually by web console", "被web控制台手动关闭"), + ("Closed manually by web console", "被 web 控制台手动关闭"), ("Local keyboard type", "本地键盘类型"), ("Select local keyboard type", "请选择本地键盘类型"), - ("software_render_tip", "如果你使用英伟达显卡, 并且远程窗口在会话建立后会立刻关闭, 那么安装nouveau驱动并且选择使用软件渲染可能会有帮助。重启软件后生效。"), + ("software_render_tip", "如果你使用英伟达显卡, 并且远程窗口在会话建立后会立刻关闭, 那么安装 nouveau 驱动并且选择使用软件渲染可能会有帮助。重启软件后生效。"), ("Always use software rendering", "使用软件渲染"), ("config_input", "为了能够通过键盘控制远程桌面, 请给予 RustDesk \"输入监控\" 权限。"), ("request_elevation_tip", "如果对面有人, 也可以请求提升权限。"), @@ -422,9 +422,9 @@ 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对话框。"), + ("wait_accept_uac_tip", "请等待远端用户确认 UAC 对话框。"), ("Elevate successfully", "提权成功"), ("uppercase", "大写字母"), ("lowercase", "小写字母"), From 39515f3ed3e91d02732b5786fc34811a735b52f6 Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Mon, 30 Jan 2023 15:48:54 +0800 Subject: [PATCH 1649/2015] Revert "Remove remnant documentation for wayland fix" --- src/lang/en.rs | 1 + src/ui/index.tis | 30 ++++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/src/lang/en.rs b/src/lang/en.rs index bacef699c..6eed43a77 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -25,6 +25,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_version_audio_tip", "The current Android version does not support audio capture, please upgrade to Android 10 or higher."), ("android_start_service_tip", "Tap [Start Service] or OPEN [Screen Capture] permission to start the screen sharing service."), ("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"), + ("doc_fix_wayland", "https://rustdesk.com/docs/en/manual/linux/#x11-required"), ("server_not_support", "Not yet supported by the server"), ("android_open_battery_optimizations_tip", "If you want to disable this feature, please go to the next RustDesk application settings page, find and enter [Battery], Uncheck [Unrestricted]"), ("remote_restarting_tip", "Remote device is restarting, please close this message box and reconnect with permanent password after a while"), diff --git a/src/ui/index.tis b/src/ui/index.tis index 68787c86f..e718e4380 100644 --- a/src/ui/index.tis +++ b/src/ui/index.tis @@ -558,6 +558,8 @@ class App: Reactor.Component {is_can_screen_recording && !handler.is_process_trusted(false) ? : ""} {!service_stopped && is_can_screen_recording && handler.is_process_trusted(false) && handler.is_installed() && !handler.is_installed_daemon(false) ? : ""} {system_error ? : ""} + {!system_error && handler.is_login_wayland() && !handler.current_is_wayland() ? : ""} + {!system_error && handler.current_is_wayland() ? : ""}
    @@ -744,6 +746,34 @@ class InstallDaemon: Reactor.Component { } } +class FixWayland: Reactor.Component { + function render() { + return
    +
    {translate('Warning')}
    +
    {translate('Login screen using Wayland is not supported')}
    +
    {translate('Help')}
    +
    ; + } + + event click $(#help-me) { + handler.open_url(translate("doc_fix_wayland")); + } +} + +class ModifyDefaultLogin: Reactor.Component { + function render() { + return
    +
    {translate('Warning')}
    +
    {translate('Current Wayland display server is not supported')}
    +
    {translate('Help')}
    +
    ; + } + + event click $(#help-me) { + handler.open_url(translate("doc_fix_wayland")); + } +} + function watch_trust() { // not use TrustMe::update, because it is buggy var trusted = handler.is_process_trusted(false); From f4d030524231c7150044bef2cea45928fd55ac35 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Mon, 30 Jan 2023 15:57:27 +0800 Subject: [PATCH 1650/2015] remove unused tip --- 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/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 - src/ui/index.tis | 2 +- 33 files changed, 1 insertion(+), 33 deletions(-) diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 9d2938b2d..cd8fba24d 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -221,7 +221,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logout", "Sortir"), ("Tags", ""), ("Search ID", "Cerca ID"), - ("Current Wayland display server is not supported", "El servidor de visualització actual de Wayland no és compatible"), ("whitelist_sep", ""), ("Add ID", "Afegir ID"), ("Add Tag", "Afegir tag"), diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 8126e0081..41fa7fc26 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -221,7 +221,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logout", "登出"), ("Tags", "标签"), ("Search ID", "查找ID"), - ("Current Wayland display server is not supported", "不支持 Wayland 显示服务器"), ("whitelist_sep", "可以使用逗号,分号,空格或者换行符作为分隔符"), ("Add ID", "增加ID"), ("Add Tag", "增加标签"), diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 842c47762..5e59a86f1 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -221,7 +221,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logout", "Odhlásit se"), ("Tags", "Štítky"), ("Search ID", "Hledat identifikátor"), - ("Current Wayland display server is not supported", "Zobrazovací server Wayland zatím není podporován"), ("whitelist_sep", "Odělováno čárkou, středníkem, mezerou nebo koncem řádku"), ("Add ID", "Přidat identifikátor"), ("Add Tag", "Přidat štítek"), diff --git a/src/lang/da.rs b/src/lang/da.rs index 8e6d622a1..8eddaf0b9 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -221,7 +221,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logout", "logger af"), ("Tags", "Nøgleord"), ("Search ID", "Søg ID"), - ("Current Wayland display server is not supported", "Den aktuelle Wayland-Anzege-server understøttes ikke"), ("whitelist_sep", "Adskilt af komma, semikolon, rum eller linjepaus"), ("Add ID", "Tilføj ID"), ("Add Tag", "Tilføj nøgleord"), diff --git a/src/lang/de.rs b/src/lang/de.rs index 11ce96f6b..3418ea9f5 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -221,7 +221,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logout", "Abmelden"), ("Tags", "Schlagworte"), ("Search ID", "Suche ID"), - ("Current Wayland display server is not supported", "Der aktuelle Wayland-Anzeigeserver wird nicht unterstützt."), ("whitelist_sep", "Getrennt durch Komma, Semikolon, Leerzeichen oder Zeilenumbruch"), ("Add ID", "ID hinzufügen"), ("Add Tag", "Stichwort hinzufügen"), diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 9086c809a..b034c0394 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -221,7 +221,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logout", "Malkonekti"), ("Tags", "Etikedi"), ("Search ID", "Serĉi ID"), - ("Current Wayland display server is not supported", "La aktuala bilda servilo Wayland ne estas subtenita"), ("whitelist_sep", "Vi povas uzi komon, punktokomon, spacon aŭ linsalton kiel apartigilo"), ("Add ID", "Aldoni identigilo"), ("Add Tag", "Aldoni etikedo"), diff --git a/src/lang/es.rs b/src/lang/es.rs index e7bf83b25..8f4275d5d 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -221,7 +221,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logout", "Salir"), ("Tags", "Tags"), ("Search ID", "Buscar ID"), - ("Current Wayland display server is not supported", "El servidor de visualización actual de Wayland no es compatible"), ("whitelist_sep", "Separados por coma, punto y coma, espacio o nueva línea"), ("Add ID", "Agregar ID"), ("Add Tag", "Agregar tag"), diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 15ef1b843..316885082 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -221,7 +221,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logout", "خروج"), ("Tags", "برچسب ها"), ("Search ID", "جستجوی شناسه"), - ("Current Wayland display server is not supported", "پشتیبانی نمی شود Wayland سرور نمایش فعلی"), ("whitelist_sep", "با کاما، نقطه ویرگول، فاصله یا خط جدید از هم جدا می شوند"), ("Add ID", "افزودن شناسه"), ("Add Tag", "افزودن برچسب"), diff --git a/src/lang/fr.rs b/src/lang/fr.rs index aa752f54e..097091e75 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -221,7 +221,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logout", "Déconnexion"), ("Tags", "Étiqueter"), ("Search ID", "Rechercher un ID"), - ("Current Wayland display server is not supported", "Le serveur d'affichage Wayland n'est pas pris en charge"), ("whitelist_sep", "Vous pouvez utiliser une virgule, un point-virgule, un espace ou une nouvelle ligne comme séparateur"), ("Add ID", "Ajouter un ID"), ("Add Tag", "Ajouter une balise"), diff --git a/src/lang/gr.rs b/src/lang/gr.rs index 8e73542e5..53f9dca08 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -221,7 +221,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logout", "Αποσύνδεση"), ("Tags", "Ετικέτες"), ("Search ID", "Αναζήτηση ID"), - ("Current Wayland display server is not supported", "Ο τρέχων διακομιστής εμφάνισης Wayland δεν υποστηρίζεται"), ("whitelist_sep", "Διαχωρίζονται με κόμμα, ερωτηματικό, διάστημα ή νέα γραμμή"), ("Add ID", "Προσθήκη αναγνωριστικού ID"), ("Add Tag", "Προσθήκη ετικέτας"), diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 5ae8e0dca..f86e83012 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -221,7 +221,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logout", "Kilépés"), ("Tags", "Tagok"), ("Search ID", "Azonosító keresése..."), - ("Current Wayland display server is not supported", "A Wayland display szerver nem támogatott"), ("whitelist_sep", "A címeket veszővel, pontosvesszővel, szóközzel, vagy új sorral válassza el"), ("Add ID", "Azonosító hozzáadása"), ("Add Tag", "Címke hozzáadása"), diff --git a/src/lang/id.rs b/src/lang/id.rs index f4555fa32..6ae39f108 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -221,7 +221,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logout", "Keluar"), ("Tags", "Tag"), ("Search ID", "Cari ID"), - ("Current Wayland display server is not supported", "Server tampilan Wayland saat ini tidak didukung"), ("whitelist_sep", "Dipisahkan dengan koma, titik koma, spasi, atau baris baru"), ("Add ID", "Tambah ID"), ("Add Tag", "Tambah Tag"), diff --git a/src/lang/it.rs b/src/lang/it.rs index 322c324ce..0ec6c52b9 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -221,7 +221,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logout", "Esci"), ("Tags", "Tag"), ("Search ID", "Cerca ID"), - ("Current Wayland display server is not supported", "Questo display server Wayland non è supportato"), ("whitelist_sep", "Separati da virgola, punto e virgola, spazio o a capo"), ("Add ID", "Aggiungi ID"), ("Add Tag", "Aggiungi tag"), diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 65368bfba..8e8a5ed95 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -221,7 +221,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logout", "ログアウト"), ("Tags", "タグ"), ("Search ID", "IDを検索"), - ("Current Wayland display server is not supported", "現在のWaylandディスプレイサーバーはサポートされていません"), ("whitelist_sep", "カンマやセミコロン、空白、改行で区切ってください"), ("Add ID", "IDを追加"), ("Add Tag", "タグを追加"), diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 01b30adc0..7b56202a0 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -221,7 +221,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logout", "로그아웃"), ("Tags", "태그"), ("Search ID", "ID 검색"), - ("Current Wayland display server is not supported", "현재 Wayland 디스플레이 서버가 지원되지 않습니다"), ("whitelist_sep", "다음 글자로 구분합니다. ',(콤마) ;(세미콜론) 띄어쓰기 혹은 줄바꿈'"), ("Add ID", "ID 추가"), ("Add Tag", "태그 추가"), diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 48d94c266..dcf62ff10 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -221,7 +221,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logout", "Шығу"), ("Tags", "Тақтар"), ("Search ID", "ID Іздеу"), - ("Current Wayland display server is not supported", "Ағымдағы Wayland дисплей серберіне қолдау көрсетілмейді"), ("whitelist_sep", "Үтір, нүктелі үтір, бос орын және жаңа жолал арқылы бөлінеді"), ("Add ID", "ID Қосу"), ("Add Tag", "Тақ Қосу"), diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 467d918b6..085e74d3a 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -221,7 +221,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logout", "Wyloguj"), ("Tags", "Tagi"), ("Search ID", "Szukaj ID"), - ("Current Wayland display server is not supported", "Obecny serwer wyświetlania Wayland nie jest obsługiwany"), ("whitelist_sep", "Oddzielone przecinkiem, średnikiem, spacją lub w nowej linii"), ("Add ID", "Dodaj ID"), ("Add Tag", "Dodaj Tag"), diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 3b6f02854..aea9acd2a 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -221,7 +221,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logout", "Sair"), ("Tags", "Tags"), ("Search ID", "Procurar ID"), - ("Current Wayland display server is not supported", "Servidor de display Wayland atual não é suportado"), ("whitelist_sep", "Separado por vírcula, ponto-e-vírgula, espaços ou nova linha"), ("Add ID", "Adicionar ID"), ("Add Tag", "Adicionar Tag"), diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 9a69d1547..28683c8d5 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -221,7 +221,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logout", "Sair"), ("Tags", "Tags"), ("Search ID", "Pesquisar ID"), - ("Current Wayland display server is not supported", "Servidor de display Wayland atual não é suportado"), ("whitelist_sep", "Separado por vírcula, ponto-e-vírgula, espaços ou nova linha"), ("Add ID", "Adicionar ID"), ("Add Tag", "Adicionar Tag"), diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 148723a5b..3009e9b06 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -218,7 +218,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logout", "Deconectare"), ("Tags", "Etichetare"), ("Search ID", "Caută după ID"), - ("Current Wayland display server is not supported", "Serverul de afișaj Wayland nu este acceptat"), ("whitelist_sep", "Poți folosi ca separator virgula, punctul și virgula, spațiul sau linia nouă"), ("Add ID", "Adaugă ID"), ("Add Tag", "Adaugă etichetă"), diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 5ab0e6af4..7a7445534 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -221,7 +221,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logout", "Выйти"), ("Tags", "Метки"), ("Search ID", "Поиск по ID"), - ("Current Wayland display server is not supported", "Текущий сервер отображения Wayland не поддерживается"), ("whitelist_sep", "Раздельно запятой, точкой с запятой, пробелом или новой строкой"), ("Add ID", "Добавить ID"), ("Add Tag", "Добавить ключевое слово"), diff --git a/src/lang/sk.rs b/src/lang/sk.rs index b996295f2..2062b57a5 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -221,7 +221,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logout", "Odhlásenie"), ("Tags", "Štítky"), ("Search ID", "Hľadať ID"), - ("Current Wayland display server is not supported", "Zobrazovací (display) server Wayland nie je podporovaný"), ("whitelist_sep", "Oddelené čiarkou, bodkočiarkou, medzerou alebo koncom riadku"), ("Add ID", "Pridať ID"), ("Add Tag", "Pridať štítok"), diff --git a/src/lang/sl.rs b/src/lang/sl.rs index cca53c830..1ff78818c 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -221,7 +221,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logout", "Odjavi"), ("Tags", "Oznake"), ("Search ID", "Išči ID"), - ("Current Wayland display server is not supported", "Trenutni Wayland zaslonski strežnik ni podprt"), ("whitelist_sep", "Naslovi ločeni z vejico, podpičjem, presledkom ali novo vrstico"), ("Add ID", "Dodaj ID"), ("Add Tag", "Dodaj oznako"), diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 3bec54dd8..225652056 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -221,7 +221,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logout", "Dalje"), ("Tags", "Tage"), ("Search ID", "Kerko ID"), - ("Current Wayland display server is not supported", "Serveri aktual i ekranit Wayland nuk mbështetet"), ("whitelist_sep", "Të ndara me presje, pikëpresje, hapësira ose rresht të ri"), ("Add ID", "Shto ID"), ("Add Tag", "Shto Tag"), diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 413d3e165..57c528fdb 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -221,7 +221,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logout", "Odjava"), ("Tags", "Oznake"), ("Search ID", "Traži ID"), - ("Current Wayland display server is not supported", "Tekući Wazland server za prikaz nije podržan"), ("whitelist_sep", "Odvojeno zarezima, tačka zarezima, praznim mestima ili novim redovima"), ("Add ID", "Dodaj ID"), ("Add Tag", "Dodaj oznaku"), diff --git a/src/lang/sv.rs b/src/lang/sv.rs index baf3c3725..f98d7f005 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -221,7 +221,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logout", "Logga ut"), ("Tags", "Taggar"), ("Search ID", "Sök ID"), - ("Current Wayland display server is not supported", "Nuvarande Wayland displayserver stöds inte"), ("whitelist_sep", "Separerat av ett comma, semikolon, mellanslag eller ny linje"), ("Add ID", "Lägg till ID"), ("Add Tag", "Lägg till Tagg"), diff --git a/src/lang/template.rs b/src/lang/template.rs index 1c5305976..358444986 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -221,7 +221,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logout", ""), ("Tags", ""), ("Search ID", ""), - ("Current Wayland display server is not supported", ""), ("whitelist_sep", ""), ("Add ID", ""), ("Add Tag", ""), diff --git a/src/lang/th.rs b/src/lang/th.rs index 6fc94ca2a..d35cbdfe7 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -221,7 +221,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logout", "ออกจากระบบ"), ("Tags", "แท็ก"), ("Search ID", "ค้นหา ID"), - ("Current Wayland display server is not supported", "เซิร์ฟเวอร์การแสดงผล Wayland ปัจจุบันไม่รองรับ"), ("whitelist_sep", "คั่นโดยเครื่องหมาย comma semicolon เว้นวรรค หรือ ขึ้นบรรทัดใหม่"), ("Add ID", "เพิ่ม ID"), ("Add Tag", "เพิ่มแท็ก"), diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 4ed9b2213..1e2068fb6 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -221,7 +221,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logout", "Çıkış yap"), ("Tags", "Etiketler"), ("Search ID", "ID Arama"), - ("Current Wayland display server is not supported", "Mevcut Wayland görüntüleme sunucusu desteklenmiyor"), ("whitelist_sep", "Virgül, noktalı virgül, boşluk veya yeni satır ile ayrılmış"), ("Add ID", "ID Ekle"), ("Add Tag", "Etiket Ekle"), diff --git a/src/lang/tw.rs b/src/lang/tw.rs index cb68254bb..370c9fbed 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -221,7 +221,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logout", "登出"), ("Tags", "標籤"), ("Search ID", "搜尋 ID"), - ("Current Wayland display server is not supported", "目前不支援 Wayland 顯示伺服器"), ("whitelist_sep", "使用逗號、分號、空白,或是換行來分隔"), ("Add ID", "新增 ID"), ("Add Tag", "新增標籤"), diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 78b611ea4..bdba09b5b 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -221,7 +221,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logout", "Вийти"), ("Tags", "Ключові слова"), ("Search ID", "Пошук за ID"), - ("Current Wayland display server is not supported", "Поточний графічний сервер Wayland не підтримується"), ("whitelist_sep", "Розділені комою, крапкою з комою, пробілом або новим рядком"), ("Add ID", "Додати ID"), ("Add Tag", "Додати ключове слово"), diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 044e2e9e6..840739765 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -221,7 +221,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logout", "Đăng xuất"), ("Tags", "Tags"), ("Search ID", "Tìm ID"), - ("Current Wayland display server is not supported", "Máy chủ hình ảnh Wayland hiện không đuợc hỗ trợ"), ("whitelist_sep", "Đuợc cách nhau bởi dấu phẩy, dấu chấm phẩy, dấu cách hay dòng mới"), ("Add ID", "Thêm ID"), ("Add Tag", "Thêm Tag"), diff --git a/src/ui/index.tis b/src/ui/index.tis index e718e4380..ec2e0a748 100644 --- a/src/ui/index.tis +++ b/src/ui/index.tis @@ -764,7 +764,7 @@ class ModifyDefaultLogin: Reactor.Component { function render() { return
    {translate('Warning')}
    -
    {translate('Current Wayland display server is not supported')}
    +
    {translate('wayland_experiment_tip')}
    {translate('Help')}
    ; } From dec1820694ad2f55e7df6178d5cacc6425ee9a1e Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 30 Jan 2023 17:56:35 +0800 Subject: [PATCH 1651/2015] opt dialog style Signed-off-by: 21pages --- flutter/lib/common.dart | 92 +++++++++--- .../lib/desktop/widgets/remote_menubar.dart | 8 +- flutter/lib/mobile/widgets/dialog.dart | 138 ++++++++++-------- flutter/lib/models/model.dart | 7 +- src/client/io_loop.rs | 2 +- 5 files changed, 155 insertions(+), 92 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 6ee57ef50..ab7728af1 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -613,6 +613,7 @@ class CustomAlertDialog extends StatelessWidget { Future.delayed(Duration.zero, () { if (!scopeNode.hasFocus) scopeNode.requestFocus(); }); + const double padding = 16; return FocusScope( node: scopeNode, autofocus: true, @@ -637,8 +638,8 @@ class CustomAlertDialog extends StatelessWidget { child: AlertDialog( scrollable: true, title: title, - contentPadding: EdgeInsets.symmetric( - horizontal: contentPadding ?? 25, vertical: 10), + contentPadding: EdgeInsets.fromLTRB( + contentPadding ?? padding, 25, contentPadding ?? padding, 10), content: ConstrainedBox( constraints: contentBoxConstraints, child: Theme( @@ -648,6 +649,7 @@ class CustomAlertDialog extends StatelessWidget { ), child: content)), actions: actions, + actionsPadding: EdgeInsets.fromLTRB(0, 0, padding, padding), ), ); } @@ -701,9 +703,8 @@ void msgBox(String id, String type, String title, String text, String link, } dialogManager.show( (setState, close) => CustomAlertDialog( - title: _msgBoxTitle(title), - content: - SelectableText(translate(text), style: const TextStyle(fontSize: 15)), + title: null, + content: msgboxContent(type, title, text), actions: buttons, onSubmit: hasOk ? submit : null, onCancel: hasCancel == true ? cancel : null, @@ -712,30 +713,74 @@ void msgBox(String id, String type, String title, String text, String link, ); } -Widget msgBoxButton(String text, void Function() onPressed) { - return ButtonTheme( - padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - //limits the touch area to the button area - minWidth: 0, - //wraps child's width - height: 0, - child: TextButton( - style: flatButtonStyle, - onPressed: onPressed, - child: - Text(translate(text), style: TextStyle(color: MyTheme.accent)))); +Color? _msgboxColor(String type) { + if (type == "input-password" || type == "custom-os-password") { + return Color(0xFFAD448E); + } + if (type.contains("success")) { + return Color(0xFF32bea6); + } + if (type.contains("error") || type == "re-input-password") { + return Color(0xFFE04F5F); + } + return Color(0xFF2C8CFF); } -Widget _msgBoxTitle(String title) => - Text(translate(title), style: TextStyle(fontSize: 21)); +Widget msgboxIcon(String type) { + IconData? iconData; + if (type.contains("error") || type == "re-input-password") { + iconData = Icons.cancel; + } + if (type.contains("success")) { + iconData = Icons.check_circle; + } + if (type == "wait-uac" || type == "wait-remote-accept-nook") { + iconData = Icons.hourglass_top; + } + if (type == 'on-uac' || type == 'on-foreground-elevated') { + iconData = Icons.admin_panel_settings; + } + if (type == "info") { + iconData = Icons.info; + } + if (iconData != null) { + return Icon(iconData, size: 50, color: _msgboxColor(type)) + .marginOnly(right: 16); + } + + return Offstage(); +} + +// title should be null +Widget msgboxContent(String type, String title, String text) { + return Row( + children: [ + msgboxIcon(type), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + translate(title), + style: TextStyle(fontSize: 21), + ).marginOnly(bottom: 10), + Text(translate(text), style: const TextStyle(fontSize: 15)), + ], + ), + ), + ], + ); +} void msgBoxCommon(OverlayDialogManager dialogManager, String title, Widget content, List buttons, {bool hasCancel = true}) { dialogManager.dismissAll(); dialogManager.show((setState, close) => CustomAlertDialog( - title: _msgBoxTitle(title), + title: Text( + translate(title), + style: TextStyle(fontSize: 21), + ), content: content, actions: buttons, onCancel: hasCancel ? close : null, @@ -1589,7 +1634,8 @@ class ServerConfig { Widget dialogButton(String text, {required VoidCallback? onPressed, bool isOutline = false, - TextStyle? style}) { + TextStyle? style, + ButtonStyle? buttonStyle}) { if (isDesktop) { if (isOutline) { return OutlinedButton( @@ -1598,7 +1644,7 @@ Widget dialogButton(String text, ); } else { return ElevatedButton( - style: ElevatedButton.styleFrom(elevation: 0), + style: ElevatedButton.styleFrom(elevation: 0).merge(buttonStyle), onPressed: onPressed, child: Text(translate(text), style: style), ); diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 07944649c..3598b2fb0 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -1426,12 +1426,8 @@ void showConfirmSwitchSidesDialog( } return CustomAlertDialog( - title: Text(translate('Switch Sides')), - content: Column( - children: [ - Text(translate('Please confirm if you want to share your desktop?')), - ], - ), + content: msgboxContent('info', 'Switch Sides', + 'Please confirm if you want to share your desktop?'), actions: [ dialogButton('Cancel', onPressed: close, isOutline: true), dialogButton('OK', onPressed: submit), diff --git a/flutter/lib/mobile/widgets/dialog.dart b/flutter/lib/mobile/widgets/dialog.dart index 0eb403833..bded6d069 100644 --- a/flutter/lib/mobile/widgets/dialog.dart +++ b/flutter/lib/mobile/widgets/dialog.dart @@ -9,7 +9,7 @@ import '../../models/model.dart'; import '../../models/platform_model.dart'; void clientClose(String id, OverlayDialogManager dialogManager) { - msgBox(id, '', 'Close', 'Are you sure to close the connection?', '', + msgBox(id, 'info', 'Close', 'Are you sure to close the connection?', '', dialogManager); } @@ -33,8 +33,10 @@ void showRestartRemoteDevice( ]), content: Text( "${translate('Are you sure you want to restart')} \n${pi.username}@${pi.hostname}($id) ?"), + onCancel: close, + onSubmit: () => close(true), actions: [ - dialogButton("Cancel", onPressed: () => close(), isOutline: true), + dialogButton("Cancel", onPressed: close, isOutline: true), dialogButton("OK", onPressed: () => close(true)), ], )); @@ -48,6 +50,18 @@ void setPermanentPasswordDialog(OverlayDialogManager dialogManager) async { var validateLength = false; var validateSame = false; dialogManager.show((setState, close) { + submit() async { + close(); + dialogManager.showLoading(translate("Waiting")); + if (await gFFI.serverModel.setPermanentPassword(p0.text)) { + dialogManager.dismissAll(); + showSuccess(); + } else { + dialogManager.dismissAll(); + showError(); + } + } + return CustomAlertDialog( title: Text(translate('Set your own password')), content: Form( @@ -94,29 +108,17 @@ void setPermanentPasswordDialog(OverlayDialogManager dialogManager) async { }, ), ])), + onCancel: close, + onSubmit: (validateLength && validateSame) ? submit : null, actions: [ dialogButton( 'Cancel', - onPressed: () { - close(); - }, + onPressed: close, isOutline: true, ), dialogButton( 'OK', - onPressed: (validateLength && validateSame) - ? () async { - close(); - dialogManager.showLoading(translate("Waiting")); - if (await gFFI.serverModel.setPermanentPassword(p0.text)) { - dialogManager.dismissAll(); - showSuccess(); - } else { - dialogManager.dismissAll(); - showError(); - } - } - : null, + onPressed: (validateLength && validateSame) ? submit : null, ), ], ); @@ -205,26 +207,36 @@ void enterPasswordDialog(String id, OverlayDialogManager dialogManager) async { }); } -void wrongPasswordDialog(String id, OverlayDialogManager dialogManager) { - dialogManager.show((setState, close) => CustomAlertDialog( - title: Text(translate('Wrong Password')), - content: Text(translate('Do you want to enter again?')), - actions: [ - dialogButton( - 'Cancel', - onPressed: () { - close(); - closeConnection(); - }, - isOutline: true, - ), - dialogButton( - 'Retry', - onPressed: () { - enterPasswordDialog(id, dialogManager); - }, - ), - ])); +void wrongPasswordDialog( + String id, OverlayDialogManager dialogManager, type, title, text) { + dialogManager.dismissAll(); + dialogManager.show((setState, close) { + cancel() { + close(); + closeConnection(); + } + + submit() { + enterPasswordDialog(id, dialogManager); + } + + return CustomAlertDialog( + title: null, + content: msgboxContent(type, title, text), + onSubmit: submit, + onCancel: cancel, + actions: [ + dialogButton( + 'Cancel', + onPressed: cancel, + isOutline: true, + ), + dialogButton( + 'Retry', + onPressed: submit, + ), + ]); + }); } void showServerSettingsWithValue( @@ -352,13 +364,15 @@ void showServerSettingsWithValue( }); } -void showWaitUacDialog(String id, OverlayDialogManager dialogManager) { +void showWaitUacDialog( + String id, OverlayDialogManager dialogManager, String type) { dialogManager.dismissAll(); dialogManager.show( tag: '$id-wait-uac', (setState, close) => CustomAlertDialog( - title: Text(translate('Wait')), - content: Text(translate('wait_accept_uac_tip')).marginAll(10), + title: null, + content: msgboxContent(type, 'Wait', 'wait_accept_uac_tip') + .marginOnly(bottom: 10), )); } @@ -516,16 +530,6 @@ void showOnBlockDialog( dialogManager.existing('$id-request-elevation')) { return; } - var content = Column(children: [ - Align( - alignment: Alignment.centerLeft, - child: Text( - "${translate(text)}${type.contains('uac') ? '\n' : '\n\n'}${translate('request_elevation_tip')}", - textAlign: TextAlign.left, - style: TextStyle(fontWeight: FontWeight.w400), - ).marginSymmetric(vertical: 15), - ), - ]); dialogManager.show(tag: '$id-$type', (setState, close) { void submit() { close(); @@ -533,12 +537,11 @@ void showOnBlockDialog( } return CustomAlertDialog( - title: Text(translate(title)), - content: content, + title: null, + content: msgboxContent(type, title, + "${translate(text)}${type.contains('uac') ? '\n' : '\n\n'}${translate('request_elevation_tip')}"), actions: [ - dialogButton('Wait', onPressed: () { - close(); - }, isOutline: true), + dialogButton('Wait', onPressed: close, isOutline: true), dialogButton('Request Elevation', onPressed: submit), ], onSubmit: submit, @@ -556,8 +559,8 @@ void showElevationError(String id, String type, String title, String text, } return CustomAlertDialog( - title: Text(translate(title)), - content: Text(translate(text)), + title: null, + content: msgboxContent(type, title, text), actions: [ dialogButton('Cancel', onPressed: () { close(); @@ -570,6 +573,25 @@ void showElevationError(String id, String type, String title, String text, }); } +void showWaitAcceptDialog(String id, String type, String title, String text, + OverlayDialogManager dialogManager) { + dialogManager.dismissAll(); + dialogManager.show((setState, close) { + onCancel() { + closeConnection(); + } + + return CustomAlertDialog( + title: null, + content: msgboxContent(type, title, text), + actions: [ + dialogButton('Cancel', onPressed: onCancel, isOutline: true), + ], + onCancel: onCancel, + ); + }); +} + Future validateAsync(String value) async { value = value.trim(); if (value.isEmpty) { diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 986d93fe8..def9c82bc 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -263,19 +263,18 @@ class FfiModel with ChangeNotifier { final text = evt['text']; final link = evt['link']; if (type == 're-input-password') { - wrongPasswordDialog(id, dialogManager); + wrongPasswordDialog(id, dialogManager, type, title, text); } else if (type == 'input-password') { enterPasswordDialog(id, dialogManager); } else if (type == 'restarting') { showMsgBox(id, type, title, text, link, false, dialogManager, hasCancel: false); } else if (type == 'wait-remote-accept-nook') { - msgBoxCommon(dialogManager, title, Text(translate(text)), - [dialogButton("Cancel", onPressed: closeConnection)]); + showWaitAcceptDialog(id, type, title, text, dialogManager); } else if (type == 'on-uac' || type == 'on-foreground-elevated') { showOnBlockDialog(id, type, title, text, dialogManager); } else if (type == 'wait-uac') { - showWaitUacDialog(id, dialogManager); + showWaitUacDialog(id, dialogManager, type); } else if (type == 'elevation-error') { showElevationError(id, type, title, text, dialogManager); } else { diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index ff6d6c004..f4ecbded5 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -1104,7 +1104,7 @@ impl Remote { Some(misc::Union::PortableServiceRunning(b)) => { if b { self.handler.msgbox( - "custom-nocancel", + "custom-nocancel-success", "Successful", "Elevate successfully", "", From 87de9eb726418d208f77dfc5bb5dcc96c0993655 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Mon, 30 Jan 2023 18:30:38 +0800 Subject: [PATCH 1652/2015] a workaround of issue #2886, following the behavior address input of chrome --- flutter/lib/common/widgets/peer_tab_page.dart | 5 ++++- flutter/lib/desktop/pages/connection_page.dart | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/flutter/lib/common/widgets/peer_tab_page.dart b/flutter/lib/common/widgets/peer_tab_page.dart index 0c24fe7ea..278f5861c 100644 --- a/flutter/lib/common/widgets/peer_tab_page.dart +++ b/flutter/lib/common/widgets/peer_tab_page.dart @@ -419,7 +419,10 @@ class _PeerSearchBarState extends State { Widget _buildSearchBar() { RxBool focused = false.obs; FocusNode focusNode = FocusNode(); - focusNode.addListener(() => focused.value = focusNode.hasFocus); + focusNode.addListener(() { + focused.value = focusNode.hasFocus; + peerSearchTextController.selection = TextSelection(baseOffset: 0, extentOffset: peerSearchTextController.value.text.length); + }); return Container( width: 120, decoration: BoxDecoration( diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 2dae03250..699cc4495 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -64,6 +64,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); }); windowManager.addListener(this); } From 00a3b04aab8659ef175d9c16715ad7c1be19647f Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 30 Jan 2023 19:38:50 +0800 Subject: [PATCH 1653/2015] fix theme Signed-off-by: 21pages --- flutter/lib/common.dart | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index ab7728af1..cf7de0fa2 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -205,6 +205,9 @@ class MyTheme { splashColor: Colors.transparent, highlightColor: Colors.transparent, splashFactory: isDesktop ? NoSplash.splashFactory : null, + outlinedButtonTheme: OutlinedButtonThemeData( + style: + OutlinedButton.styleFrom(side: BorderSide(color: Colors.white38))), textButtonTheme: isDesktop ? TextButtonThemeData( style: ButtonStyle(splashFactory: NoSplash.splashFactory), @@ -641,13 +644,13 @@ class CustomAlertDialog extends StatelessWidget { contentPadding: EdgeInsets.fromLTRB( contentPadding ?? padding, 25, contentPadding ?? padding, 10), content: ConstrainedBox( - constraints: contentBoxConstraints, - child: Theme( - data: ThemeData( + constraints: contentBoxConstraints, + child: Theme( + data: Theme.of(context).copyWith( inputDecorationTheme: InputDecorationTheme( - isDense: true, contentPadding: EdgeInsets.all(15)), - ), - child: content)), + isDense: true, contentPadding: EdgeInsets.all(15))), + child: content), + ), actions: actions, actionsPadding: EdgeInsets.fromLTRB(0, 0, padding, padding), ), From d99b0bed0a4f9202e53c472f6dff86e188d92627 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 30 Jan 2023 21:42:58 +0800 Subject: [PATCH 1654/2015] fix: set edge size to zero when in fullscreen mode --- flutter/lib/desktop/pages/connection_page.dart | 13 +++++++++++++ flutter/lib/desktop/pages/remote_tab_page.dart | 17 +++++++++-------- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 699cc4495..eee4c6a20 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -8,6 +8,7 @@ import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart'; +import 'package:flutter_hbb/models/state_model.dart'; import 'package:get/get.dart'; import 'package:url_launcher/url_launcher_string.dart'; import 'package:window_manager/window_manager.dart'; @@ -92,6 +93,18 @@ class _ConnectionPageState extends State } } + @override + void onWindowEnterFullScreen() { + // Remove edge border by setting the value to zero. + stateGlobal.resizeEdgeSize.value = 0; + } + + @override + void onWindowLeaveFullScreen() { + // Restore edge border to default edge size. + stateGlobal.resizeEdgeSize.value = kWindowEdgeSize; + } + @override void onWindowClose() { super.onWindowClose(); diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index 83928c3fe..7ceacd539 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -38,8 +38,9 @@ class ConnectionTabPage extends StatefulWidget { } class _ConnectionTabPageState extends State { - final tabController = Get.put(DesktopTabController( - tabType: DesktopTabType.remoteScreen)); + final tabController = + Get.put(DesktopTabController(tabType: DesktopTabType.remoteScreen)); + final contentKey = UniqueKey(); static const IconData selectedIcon = Icons.desktop_windows_sharp; static const IconData unselectedIcon = Icons.desktop_windows_outlined; @@ -80,7 +81,6 @@ class _ConnectionTabPageState extends State { super.initState(); tabController.onRemoved = (_, id) => onRemoveId(id); - rustDeskWinManager.setMethodHandler((call, fromWindowId) async { print( @@ -197,11 +197,12 @@ class _ConnectionTabPageState extends State { ); return Platform.isMacOS ? tabWidget - : SubWindowDragToResizeArea( - child: tabWidget, - resizeEdgeSize: stateGlobal.resizeEdgeSize.value, - windowId: stateGlobal.windowId, - ); + : Obx(() => SubWindowDragToResizeArea( + key: contentKey, + child: tabWidget, + resizeEdgeSize: stateGlobal.resizeEdgeSize.value, + windowId: stateGlobal.windowId, + )); } // Note: Some dup code to ../widgets/remote_menubar From 0765f7057f3adf7c196f50c630f415878c771096 Mon Sep 17 00:00:00 2001 From: fufesou Date: Mon, 30 Jan 2023 22:12:36 +0800 Subject: [PATCH 1655/2015] try fix https://github.com/rustdesk/rustdesk/issues/2923 Signed-off-by: fufesou --- flutter/lib/desktop/widgets/remote_menubar.dart | 12 +++++++++--- flutter/lib/models/input_model.dart | 7 +++++++ flutter/lib/models/model.dart | 1 + 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 07944649c..7695bc51c 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -379,9 +379,15 @@ class _RemoteMenubarState extends State { mod_menu.PopupMenuItem( height: _MenubarTheme.height, padding: EdgeInsets.zero, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: rowChildren), + child: Listener( + onPointerHover: (PointerHoverEvent e) => + widget.ffi.inputModel.lastMousePos = e.position, + child: MouseRegion( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: rowChildren), + ), + ), ) ]; }, diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index 7356c6ec8..49115cb3f 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -408,6 +408,13 @@ class InputModel { } } + void refreshMousePos() => handleMouse({ + 'x': lastMousePos.dx, + 'y': lastMousePos.dy, + 'buttons': 0, + 'type': _kMouseEventMove, + }); + void handleMouse(Map evt) { double x = evt['x']; double y = max(0.0, evt['y']); diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 986d93fe8..1f4fbb8f0 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -244,6 +244,7 @@ class FfiModel with ChangeNotifier { parent.target?.canvasModel.updateViewStyle(); } parent.target?.recordingModel.onSwitchDisplay(); + parent.target?.inputModel.refreshMousePos(); notifyListeners(); } From 55318c2393a44d539d9d92445e05876bf27272ce Mon Sep 17 00:00:00 2001 From: jimmyGALLAND <64364019+jimmyGALLAND@users.noreply.github.com> Date: Mon, 30 Jan 2023 17:10:05 +0100 Subject: [PATCH 1656/2015] Update desktop_tab_page.dart --- flutter/lib/desktop/pages/desktop_tab_page.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_tab_page.dart b/flutter/lib/desktop/pages/desktop_tab_page.dart index 57c7fe4b8..c1965921c 100644 --- a/flutter/lib/desktop/pages/desktop_tab_page.dart +++ b/flutter/lib/desktop/pages/desktop_tab_page.dart @@ -23,7 +23,7 @@ class DesktopTabPage extends StatefulWidget { DesktopTabController tabController = Get.find(); tabController.add(TabInfo( key: kTabLabelSettingPage, - label: kTabLabelSettingPage, + label: translate(kTabLabelSettingPage), selectedIcon: Icons.build_sharp, unselectedIcon: Icons.build_outlined, page: DesktopSettingPage( @@ -46,7 +46,7 @@ class _DesktopTabPageState extends State { RemoteCountState.init(); tabController.add(TabInfo( key: kTabLabelHomePage, - label: kTabLabelHomePage, + label: translate(kTabLabelHomePage), selectedIcon: Icons.home_sharp, unselectedIcon: Icons.home_outlined, closable: false, From 61389bc11fd9b00a8fb742d4239376f8b74ac629 Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 30 Jan 2023 21:40:13 +0800 Subject: [PATCH 1657/2015] adjust quality monitor ui Signed-off-by: 21pages --- flutter/lib/common/widgets/overlay.dart | 72 +++++++++++++------------ src/ui/remote.css | 2 +- 2 files changed, 40 insertions(+), 34 deletions(-) diff --git a/flutter/lib/common/widgets/overlay.dart b/flutter/lib/common/widgets/overlay.dart index d9684bace..aaf52fb07 100644 --- a/flutter/lib/common/widgets/overlay.dart +++ b/flutter/lib/common/widgets/overlay.dart @@ -1,3 +1,4 @@ +import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; import 'package:provider/provider.dart'; @@ -316,44 +317,49 @@ class _DraggableState extends State { } class QualityMonitor extends StatelessWidget { - static const textStyle = TextStyle(color: MyTheme.grayBg); final QualityMonitorModel qualityMonitorModel; QualityMonitor(this.qualityMonitorModel); + Widget _row(String info, String? value) { + return Row( + children: [ + Expanded( + flex: 8, + child: AutoSizeText(info, + style: TextStyle(color: MyTheme.grayBg), + textAlign: TextAlign.right, + maxLines: 1)), + Spacer(flex: 1), + Expanded( + flex: 8, + child: AutoSizeText(value ?? '', + style: TextStyle(color: MyTheme.grayBg), maxLines: 1)), + ], + ); + } + @override Widget build(BuildContext context) => ChangeNotifierProvider.value( value: qualityMonitorModel, child: Consumer( - builder: (context, qualityMonitorModel, child) => - qualityMonitorModel.show - ? Container( - padding: const EdgeInsets.all(8), - color: MyTheme.canvasColor.withAlpha(120), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Speed: ${qualityMonitorModel.data.speed ?? ''}", - style: textStyle, - ), - Text( - "FPS: ${qualityMonitorModel.data.fps ?? ''}", - style: textStyle, - ), - Text( - "Delay: ${qualityMonitorModel.data.delay ?? ''} ms", - style: textStyle, - ), - Text( - "Target Bitrate: ${qualityMonitorModel.data.targetBitrate ?? ''}kb", - style: textStyle, - ), - Text( - "Codec: ${qualityMonitorModel.data.codecFormat ?? ''}", - style: textStyle, - ), - ], - ), - ) - : const SizedBox.shrink())); + builder: (context, qualityMonitorModel, child) => qualityMonitorModel + .show + ? Container( + constraints: BoxConstraints(maxWidth: 200), + padding: const EdgeInsets.all(8), + color: MyTheme.canvasColor.withAlpha(120), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _row("Speed", qualityMonitorModel.data.speed ?? ''), + _row("FPS", qualityMonitorModel.data.fps ?? ''), + _row( + "Delay", "${qualityMonitorModel.data.delay ?? ''}ms"), + _row("Target Bitrate", + "${qualityMonitorModel.data.targetBitrate ?? ''}kb"), + _row("Codec", qualityMonitorModel.data.codecFormat ?? ''), + ], + ), + ) + : const SizedBox.shrink())); } diff --git a/src/ui/remote.css b/src/ui/remote.css index 66c5ce80f..71b2c1682 100644 --- a/src/ui/remote.css +++ b/src/ui/remote.css @@ -16,7 +16,7 @@ div#quality-monitor { padding: 5px; min-width: 150px; color: azure; - border: solid azure; + border: 0.5px solid azure; } video#handler { From fb81f206b7e471f659a07ed0b7a2842e18ec89ad Mon Sep 17 00:00:00 2001 From: 21pages Date: Sun, 29 Jan 2023 17:36:37 +0800 Subject: [PATCH 1658/2015] opt flink creation Signed-off-by: 21pages --- src/platform/windows.rs | 10 +++++ src/server/portable_service.rs | 68 ++++++++++++++-------------------- 2 files changed, 38 insertions(+), 40 deletions(-) diff --git a/src/platform/windows.rs b/src/platform/windows.rs index b778283a5..2e0d56eab 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -1745,3 +1745,13 @@ pub fn create_process_with_logon(user: &str, pwd: &str, exe: &str, arg: &str) -> } return Ok(()); } + +pub fn set_path_permission(dir: &PathBuf, permission: &str) -> ResultType<()> { + std::process::Command::new("icacls") + .arg(dir.as_os_str()) + .arg("/grant") + .arg(format!("Everyone:(OI)(CI){}", permission)) + .arg("/T") + .spawn()?; + Ok(()) +} diff --git a/src/server/portable_service.rs b/src/server/portable_service.rs index 748cb39e4..a2f6fb829 100644 --- a/src/server/portable_service.rs +++ b/src/server/portable_service.rs @@ -2,9 +2,7 @@ use core::slice; use hbb_common::{ allow_err, anyhow::anyhow, - bail, - config::Config, - log, + bail, log, message_proto::{KeyEvent, MouseEvent}, protobuf::Message, tokio::{self, sync::mpsc}, @@ -15,6 +13,7 @@ use shared_memory::*; use std::{ mem::size_of, ops::{Deref, DerefMut}, + path::PathBuf, sync::{Arc, Mutex}, time::Duration, }; @@ -25,6 +24,7 @@ use winapi::{ use crate::{ ipc::{self, new_listener, Connection, Data, DataPortableService}, + platform::set_path_permission, video_service::get_current_display, }; @@ -72,7 +72,7 @@ impl DerefMut for SharedMemory { impl SharedMemory { pub fn create(name: &str, size: usize) -> ResultType { - let flink = Self::flink(name.to_string()); + let flink = Self::flink(name.to_string())?; let shmem = match ShmemConf::new() .size(size) .flink(&flink) @@ -91,12 +91,12 @@ impl SharedMemory { } }; log::info!("Create shared memory, size:{}, flink:{}", size, flink); - Self::set_all_perm(&flink); + set_path_permission(&PathBuf::from(flink), "F").ok(); Ok(SharedMemory { inner: shmem }) } pub fn open_existing(name: &str) -> ResultType { - let flink = Self::flink(name.to_string()); + let flink = Self::flink(name.to_string())?; let shmem = match ShmemConf::new().flink(&flink).allow_raw(true).open() { Ok(m) => m, Err(e) => { @@ -116,30 +116,29 @@ impl SharedMemory { } } - fn flink(name: String) -> String { - let mut shmem_flink = format!("shared_memory{}", name); - if cfg!(windows) { - let df = "C:\\ProgramData"; - let df = if std::path::Path::new(df).exists() { - df.to_owned() - } else { - std::env::var("TEMP").unwrap_or("C:\\Windows\\TEMP".to_owned()) - }; - let df = format!("{}\\{}", df, *hbb_common::config::APP_NAME.read().unwrap()); - std::fs::create_dir(&df).ok(); - shmem_flink = format!("{}\\{}", df, shmem_flink); + fn flink(name: String) -> ResultType { + let disk = std::env::var("SystemDrive").unwrap_or("C:".to_string()); + let mut dir = PathBuf::from(disk); + let dir1 = dir.join("ProgramData"); + let dir2 = std::env::var("TEMP") + .map(|d| PathBuf::from(d)) + .unwrap_or(dir.join("Windows").join("Temp")); + if dir1.exists() { + dir = dir1; + } else if dir2.exists() { + dir = dir2; } else { - shmem_flink = Config::ipc_path("").replace("ipc", "") + &shmem_flink; + bail!("no vaild flink directory"); } - return shmem_flink; - } - - fn set_all_perm(_p: &str) { - #[cfg(not(windows))] - { - use std::os::unix::fs::PermissionsExt; - std::fs::set_permissions(_p, std::fs::Permissions::from_mode(0o0777)).ok(); + dir = dir.join(hbb_common::config::APP_NAME.read().unwrap().clone()); + if !dir.exists() { + std::fs::create_dir(&dir)?; + set_path_permission(&dir, "F").ok(); } + Ok(dir + .join(format!("shared_memory{}", name)) + .to_string_lossy() + .to_string()) } } @@ -451,7 +450,6 @@ pub mod server { // functions called in main process. pub mod client { use hbb_common::anyhow::Context; - use std::path::PathBuf; use super::*; @@ -515,7 +513,7 @@ pub mod client { #[cfg(feature = "flutter")] { if let Some(dir) = PathBuf::from(&exe).parent() { - if !set_dir_permission(&PathBuf::from(dir)) { + if set_path_permission(&PathBuf::from(dir), "RX").is_err() { *SHMEM.lock().unwrap() = None; bail!("Failed to set permission of {:?}", dir); } @@ -533,7 +531,7 @@ pub mod client { let dst = dir.join("rustdesk.exe"); if std::fs::copy(&exe, &dst).is_ok() { if dst.exists() { - if set_dir_permission(&dir) { + if set_path_permission(&dir, "RX").is_ok() { exe = dst.to_string_lossy().to_string(); } } @@ -566,16 +564,6 @@ pub mod client { *QUICK_SUPPORT.lock().unwrap() = v; } - fn set_dir_permission(dir: &PathBuf) -> bool { - // // give Everyone RX permission - std::process::Command::new("icacls") - .arg(dir.as_os_str()) - .arg("/grant") - .arg("Everyone:(OI)(CI)RX") - .arg("/T") - .spawn() - .is_ok() - } pub struct CapturerPortable; impl CapturerPortable { From 74a73b7ffd6008be1d49c67a0642fc1938e4b790 Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 31 Jan 2023 17:51:20 +0800 Subject: [PATCH 1659/2015] add default position for portal streams Signed-off-by: fufesou --- libs/scrap/src/wayland/pipewire.rs | 31 +++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/libs/scrap/src/wayland/pipewire.rs b/libs/scrap/src/wayland/pipewire.rs index fefab9b77..9c0ad9774 100644 --- a/libs/scrap/src/wayland/pipewire.rs +++ b/libs/scrap/src/wayland/pipewire.rs @@ -386,21 +386,22 @@ fn streams_from_response(response: OrgFreedesktopPortalRequestResponse) -> Vec

    >(), - ) - }) - .next(); - if let Some(v) = v { - if v.len() == 2 { - info.position.0 = v[0] as _; - info.position.1 = v[1] as _; + if let Some(pos) = attributes.get("position") { + let v = pos + .as_iter()? + .filter_map(|v| { + Some( + v.as_iter()? + .map(|x| x.as_i64().unwrap_or(0)) + .collect::>(), + ) + }) + .next(); + if let Some(v) = v { + if v.len() == 2 { + info.position.0 = v[0] as _; + info.position.1 = v[1] as _; + } } } Some(info) From c1ae4a6028c8b0b99d0fc0840da0cdad26e5ebd6 Mon Sep 17 00:00:00 2001 From: sjpark Date: Wed, 1 Feb 2023 08:10:57 +0900 Subject: [PATCH 1660/2015] tray bug fix --- src/core_main.rs | 2 +- src/ui.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core_main.rs b/src/core_main.rs index 4a2f6164c..8658b736c 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -198,7 +198,7 @@ pub fn core_main() -> Option> { { std::thread::spawn(move || crate::start_server(true)); crate::platform::macos::hide_dock(); - crate::tray::make_tray(); + crate::ui::macos::make_tray(); return None; } #[cfg(target_os = "linux")] diff --git a/src/ui.rs b/src/ui.rs index b8473072d..db1cac074 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -21,7 +21,7 @@ mod cm; #[cfg(feature = "inline")] pub mod inline; #[cfg(target_os = "macos")] -mod macos; +pub mod macos; pub mod remote; #[cfg(target_os = "windows")] pub mod win_privacy; From ec1da900ec6bfa35b7f1302c34871f58604d5e5e Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 1 Feb 2023 10:42:02 +0800 Subject: [PATCH 1661/2015] fix issue #2963: run gen_version no matter debug or release --- libs/hbb_common/src/lib.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/libs/hbb_common/src/lib.rs b/libs/hbb_common/src/lib.rs index 9e004376c..c9f9e90d7 100644 --- a/libs/hbb_common/src/lib.rs +++ b/libs/hbb_common/src/lib.rs @@ -197,9 +197,6 @@ pub fn get_version_from_url(url: &str) -> String { } pub fn gen_version() { - if Ok("release".to_owned()) != std::env::var("PROFILE") { - return; - } println!("cargo:rerun-if-changed=Cargo.toml"); use std::io::prelude::*; let mut file = File::create("./src/version.rs").unwrap(); From 2f26b2a355f896e54f24abba44689ce77ef050b9 Mon Sep 17 00:00:00 2001 From: solokot Date: Wed, 1 Feb 2023 09:08:54 +0300 Subject: [PATCH 1662/2015] Update ru.rs --- src/lang/ru.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 7a7445534..22f938ec5 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -41,9 +41,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "О программе"), ("Slogan_tip", "Сделано с душой в этом безумном мире!"), ("Privacy Statement", "Заявление о конфиденциальности"), - ("Build Date", ""), - ("Version", ""), - ("Home", ""), + ("Build Date", "Дата сборки"), + ("Version", "Версия"), + ("Home", "Главная"), ("Mute", "Отключить звук"), ("Audio Input", "Аудиовход"), ("Enhancements", "Улучшения"), @@ -434,7 +434,7 @@ 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", "Закрыто по ожиданию"), ].iter().cloned().collect(); } From 60ff4982ca6337a96cf80630d02aa9a7c76ab120 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Wed, 1 Feb 2023 14:03:55 +0800 Subject: [PATCH 1663/2015] fix: macos location restore incorrectly --- .../desktop/pages/file_manager_tab_page.dart | 3 --- .../desktop/pages/port_forward_tab_page.dart | 3 --- flutter/lib/main.dart | 24 +++++++++---------- flutter/pubspec.yaml | 2 +- 4 files changed, 13 insertions(+), 19 deletions(-) diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index b2566e267..95bf0b182 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -71,9 +71,6 @@ class _FileManagerTabPageState extends State { reloadCurrentWindow(); } }); - Future.delayed(Duration.zero, () { - restoreWindowPosition(WindowType.FileTransfer, windowId: windowId()); - }); } @override diff --git a/flutter/lib/desktop/pages/port_forward_tab_page.dart b/flutter/lib/desktop/pages/port_forward_tab_page.dart index ca354f297..c29ad64b0 100644 --- a/flutter/lib/desktop/pages/port_forward_tab_page.dart +++ b/flutter/lib/desktop/pages/port_forward_tab_page.dart @@ -79,9 +79,6 @@ class _PortForwardTabPageState extends State { reloadCurrentWindow(); } }); - Future.delayed(Duration.zero, () { - restoreWindowPosition(WindowType.PortForward, windowId: windowId()); - }); } @override diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 1ec963f22..4579ef223 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -122,20 +122,20 @@ void runMainApp(bool startService) async { } gFFI.userModel.refreshCurrentUser(); runApp(App()); - // restore the location of the main window before window hide or show - await restoreWindowPosition(WindowType.Main); - // check the startup argument, if we successfully handle the argument, we keep the main window hidden. - if (checkArguments()) { - windowManager.hide(); - } else { - windowManager.show(); - windowManager.focus(); - // move registration of active main window here to prevent async visible check. - rustDeskWinManager.registerActiveWindow(kWindowMainId); - } - // set window option + // Set window option. WindowOptions windowOptions = getHiddenTitleBarWindowOptions(); windowManager.waitUntilReadyToShow(windowOptions, () async { + // Restore the location of the main window before window hide or show. + await restoreWindowPosition(WindowType.Main); + // Check the startup argument, if we successfully handle the argument, we keep the main window hidden. + if (checkArguments()) { + windowManager.hide(); + } else { + windowManager.show(); + windowManager.focus(); + // Move registration of active main window here to prevent from async visible check. + rustDeskWinManager.registerActiveWindow(kWindowMainId); + } windowManager.setOpacity(1); }); windowManager.setTitle(getWindowName()); diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 0189ad9e4..3d08033bb 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -59,7 +59,7 @@ dependencies: desktop_multi_window: git: url: https://github.com/Kingtous/rustdesk_desktop_multi_window - ref: 057e6eb1bc7dcbcf9dafd1384274a611e4fe7124 + ref: bc8604a88e52b2b6e64d2661ae49a71450a47af8 freezed_annotation: ^2.0.3 flutter_custom_cursor: ^0.0.2 window_size: From bf71e38426159e8c85e2d5dbfbaba99675dfa25c Mon Sep 17 00:00:00 2001 From: Kingtous Date: Wed, 1 Feb 2023 14:13:53 +0800 Subject: [PATCH 1664/2015] fix: linux sub-window pos for double-check pos --- flutter/lib/desktop/pages/file_manager_tab_page.dart | 3 +++ flutter/lib/desktop/pages/port_forward_tab_page.dart | 3 +++ flutter/lib/desktop/pages/remote_tab_page.dart | 3 +++ 3 files changed, 9 insertions(+) diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index 95bf0b182..b2566e267 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -71,6 +71,9 @@ class _FileManagerTabPageState extends State { reloadCurrentWindow(); } }); + Future.delayed(Duration.zero, () { + restoreWindowPosition(WindowType.FileTransfer, windowId: windowId()); + }); } @override diff --git a/flutter/lib/desktop/pages/port_forward_tab_page.dart b/flutter/lib/desktop/pages/port_forward_tab_page.dart index c29ad64b0..ca354f297 100644 --- a/flutter/lib/desktop/pages/port_forward_tab_page.dart +++ b/flutter/lib/desktop/pages/port_forward_tab_page.dart @@ -79,6 +79,9 @@ class _PortForwardTabPageState extends State { reloadCurrentWindow(); } }); + Future.delayed(Duration.zero, () { + restoreWindowPosition(WindowType.PortForward, windowId: windowId()); + }); } @override diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index 7ceacd539..55124fbcc 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -113,6 +113,9 @@ class _ConnectionTabPageState extends State { } _update_remote_count(); }); + Future.delayed(Duration.zero, () { + restoreWindowPosition(WindowType.RemoteDesktop, windowId: windowId()); + }); } @override From cbf0da61956f19478ef87984845aaf73649a5c7a Mon Sep 17 00:00:00 2001 From: Kingtous Date: Wed, 1 Feb 2023 16:29:13 +0800 Subject: [PATCH 1665/2015] feat: add trackpad listener support based on flutter 3.3 --- flutter/lib/common/widgets/remote_input.dart | 2 -- flutter/lib/models/input_model.dart | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/flutter/lib/common/widgets/remote_input.dart b/flutter/lib/common/widgets/remote_input.dart index 2d0dcacdf..2fb409970 100644 --- a/flutter/lib/common/widgets/remote_input.dart +++ b/flutter/lib/common/widgets/remote_input.dart @@ -64,11 +64,9 @@ class RawPointerMouseRegion extends StatelessWidget { }, onPointerMove: inputModel.onPointMoveImage, onPointerSignal: inputModel.onPointerSignalImage, - /* onPointerPanZoomStart: inputModel.onPointerPanZoomStart, onPointerPanZoomUpdate: inputModel.onPointerPanZoomUpdate, onPointerPanZoomEnd: inputModel.onPointerPanZoomEnd, - */ child: MouseRegion( cursor: cursor ?? MouseCursor.defer, onEnter: onEnter, diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index 49115cb3f..d2f671cdc 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -310,7 +310,7 @@ class InputModel { } } -/* + int _signOrZero(num x) { if (x == 0) { return 0; @@ -361,7 +361,7 @@ class InputModel { trackpadScrollDistance = Offset.zero; } -*/ + void onPointDownImage(PointerDownEvent e) { debugPrint("onPointDownImage"); From 5149b90e539a05756b38e14672848bcbcd94ec7c Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 1 Feb 2023 17:11:24 +0800 Subject: [PATCH 1666/2015] fix hide docker (can not call too early) --- flutter/lib/main.dart | 1 + flutter/macos/Podfile.lock | 15 +- .../macos/Runner.xcodeproj/project.pbxproj | 5 +- flutter/pubspec.lock | 671 +++++++++++------- src/core_main.rs | 2 - src/flutter_ffi.rs | 6 + 6 files changed, 440 insertions(+), 260 deletions(-) diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 4579ef223..53ae2f5dd 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -88,6 +88,7 @@ Future main(List args) async { debugPrint("--cm started"); desktopType = DesktopType.cm; await windowManager.ensureInitialized(); + bind.mainHideDocker(); runConnectionManagerScreen(args.contains('--hide')); } else if (args.contains('--install')) { runInstallPage(); diff --git a/flutter/macos/Podfile.lock b/flutter/macos/Podfile.lock index 8d41945c8..3187c6349 100644 --- a/flutter/macos/Podfile.lock +++ b/flutter/macos/Podfile.lock @@ -13,7 +13,8 @@ PODS: - FMDB/standard (2.7.5) - package_info_plus_macos (0.0.1): - FlutterMacOS - - path_provider_macos (0.0.1): + - path_provider_foundation (0.0.1): + - Flutter - FlutterMacOS - screen_retriever (0.0.1): - FlutterMacOS @@ -38,7 +39,7 @@ DEPENDENCIES: - flutter_custom_cursor (from `Flutter/ephemeral/.symlinks/plugins/flutter_custom_cursor/macos`) - FlutterMacOS (from `Flutter/ephemeral`) - package_info_plus_macos (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus_macos/macos`) - - path_provider_macos (from `Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos`) + - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/macos`) - screen_retriever (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos`) - sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/macos`) - uni_links_desktop (from `Flutter/ephemeral/.symlinks/plugins/uni_links_desktop/macos`) @@ -64,8 +65,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral package_info_plus_macos: :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus_macos/macos - path_provider_macos: - :path: Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos + path_provider_foundation: + :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/macos screen_retriever: :path: Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos sqflite: @@ -86,14 +87,14 @@ SPEC CHECKSUMS: desktop_multi_window: 566489c048b501134f9d7fb6a2354c60a9126486 device_info_plus_macos: 1ad388a1ef433505c4038e7dd9605aadd1e2e9c7 flutter_custom_cursor: 629957115075c672287bd0fa979d863ccf6024f7 - FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424 + FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a package_info_plus_macos: f010621b07802a241d96d01876d6705f15e77c1c - path_provider_macos: 3c0c3b4b0d4a76d2bf989a913c2de869c5641a19 + path_provider_foundation: 37748e03f12783f9de2cb2c4eadfaa25fe6d4852 screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38 sqflite: a5789cceda41d54d23f31d6de539d65bb14100ea uni_links_desktop: 45900fb319df48fcdea2df0756e9c2626696b026 - url_launcher_macos: 597e05b8e514239626bcf4a850fcf9ef5c856ec3 + url_launcher_macos: c04e4fa86382d4f94f6b38f14625708be3ae52e2 wakelock_macos: bc3f2a9bd8d2e6c89fee1e1822e7ddac3bd004a9 window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 window_size: 339dafa0b27a95a62a843042038fa6c3c48de195 diff --git a/flutter/macos/Runner.xcodeproj/project.pbxproj b/flutter/macos/Runner.xcodeproj/project.pbxproj index fbf52403c..7a17c3de1 100644 --- a/flutter/macos/Runner.xcodeproj/project.pbxproj +++ b/flutter/macos/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 54; objects = { /* Begin PBXAggregateTarget section */ @@ -279,6 +279,7 @@ /* Begin PBXShellScriptBuildPhase section */ 3399D490228B24CF009A79C7 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -429,7 +430,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 15a1a23ac..c193c0651 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -5,288 +5,328 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - url: "https://pub.dartlang.org" + sha256: "0c80aeab9bc807ab10022cd3b2f4cf2ecdf231949dc1ddd9442406a003f19201" + url: "https://pub.dev" source: hosted - version: "50.0.0" + version: "52.0.0" after_layout: dependency: transitive description: name: after_layout - url: "https://pub.dartlang.org" + sha256: "95a1cb2ca1464f44f14769329fbf15987d20ab6c88f8fc5d359bd362be625f29" + url: "https://pub.dev" source: hosted version: "1.2.0" analyzer: dependency: transitive description: name: analyzer - url: "https://pub.dartlang.org" + sha256: cd8ee83568a77f3ae6b913a36093a1c9b1264e7cb7f834d9ddd2311dade9c1f4 + url: "https://pub.dev" source: hosted - version: "5.2.0" + version: "5.4.0" animations: dependency: transitive description: name: animations - url: "https://pub.dartlang.org" + sha256: fe8a6bdca435f718bb1dc8a11661b2c22504c6da40ef934cee8327ed77934164 + url: "https://pub.dev" source: hosted version: "2.0.7" archive: dependency: transitive description: name: archive - url: "https://pub.dartlang.org" + sha256: d6347d54a2d8028e0437e3c099f66fdb8ae02c4720c1e7534c9f24c10351f85d + url: "https://pub.dev" source: hosted - version: "3.3.5" + version: "3.3.6" args: dependency: transitive description: name: args - url: "https://pub.dartlang.org" + sha256: "139d809800a412ebb26a3892da228b2d0ba36f0ef5d9a82166e5e52ec8d61611" + url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" async: dependency: transitive description: name: async - url: "https://pub.dartlang.org" + sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 + url: "https://pub.dev" source: hosted - version: "2.9.0" + version: "2.10.0" auto_size_text: dependency: "direct main" description: name: auto_size_text - url: "https://pub.dartlang.org" + sha256: "3f5261cd3fb5f2a9ab4e2fc3fba84fd9fcaac8821f20a1d4e71f557521b22599" + url: "https://pub.dev" source: hosted version: "3.0.0" back_button_interceptor: dependency: "direct main" description: name: back_button_interceptor - url: "https://pub.dartlang.org" + sha256: e47660f2178a4392eb72001f9594d3fdcb5efde93e59d2819d61fda499e781c8 + url: "https://pub.dev" source: hosted version: "6.0.2" bot_toast: dependency: "direct main" description: name: bot_toast - url: "https://pub.dartlang.org" + sha256: "19306147033316a7873c5d261b874fca3f341c05e4e1c12be56153ad11187edd" + url: "https://pub.dev" source: hosted version: "4.0.3" build: dependency: transitive description: name: build - url: "https://pub.dartlang.org" + sha256: "3fbda25365741f8251b39f3917fb3c8e286a96fd068a5a242e11c2012d495777" + url: "https://pub.dev" source: hosted version: "2.3.1" build_cli_annotations: dependency: transitive description: name: build_cli_annotations - url: "https://pub.dartlang.org" + sha256: b59d2769769efd6c9ff6d4c4cede0be115a566afc591705c2040b707534b1172 + url: "https://pub.dev" source: hosted version: "2.1.0" build_config: dependency: transitive description: name: build_config - url: "https://pub.dartlang.org" + sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + url: "https://pub.dev" source: hosted version: "1.1.1" build_daemon: dependency: transitive description: name: build_daemon - url: "https://pub.dartlang.org" + sha256: "6bc5544ea6ce4428266e7ea680e945c68806c4aae2da0eb5e9ccf38df8d6acbf" + url: "https://pub.dev" source: hosted version: "3.1.0" build_resolvers: dependency: transitive description: name: build_resolvers - url: "https://pub.dartlang.org" + sha256: "7c35a3a7868626257d8aee47b51c26b9dba11eaddf3431117ed2744951416aab" + url: "https://pub.dev" source: hosted - version: "2.0.10" + version: "2.1.0" build_runner: dependency: "direct dev" description: name: build_runner - url: "https://pub.dartlang.org" + sha256: b0a8a7b8a76c493e85f1b84bffa0588859a06197863dba8c9036b15581fd9727 + url: "https://pub.dev" source: hosted version: "2.3.3" build_runner_core: dependency: transitive description: name: build_runner_core - url: "https://pub.dartlang.org" + sha256: "14febe0f5bac5ae474117a36099b4de6f1dbc52df6c5e55534b3da9591bf4292" + url: "https://pub.dev" source: hosted version: "7.2.7" built_collection: dependency: transitive description: name: built_collection - url: "https://pub.dartlang.org" + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" source: hosted version: "5.1.1" built_value: dependency: transitive description: name: built_value - url: "https://pub.dartlang.org" + sha256: "169565c8ad06adb760c3645bf71f00bff161b00002cace266cad42c5d22a7725" + url: "https://pub.dev" source: hosted - version: "8.4.2" + version: "8.4.3" cached_network_image: dependency: transitive description: name: cached_network_image - url: "https://pub.dartlang.org" + sha256: fd3d0dc1d451f9a252b32d95d3f0c3c487bc41a75eba2e6097cb0b9c71491b15 + url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.2.3" cached_network_image_platform_interface: dependency: transitive description: name: cached_network_image_platform_interface - url: "https://pub.dartlang.org" + sha256: bb2b8403b4ccdc60ef5f25c70dead1f3d32d24b9d6117cfc087f496b178594a7 + url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "2.0.0" cached_network_image_web: dependency: transitive description: name: cached_network_image_web - url: "https://pub.dartlang.org" + sha256: b8eb814ebfcb4dea049680f8c1ffb2df399e4d03bf7a352c775e26fa06e02fa0 + url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.2" characters: dependency: transitive description: name: characters - url: "https://pub.dartlang.org" + sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c + url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.1" charcode: dependency: transitive description: name: charcode - url: "https://pub.dartlang.org" + sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 + url: "https://pub.dev" source: hosted version: "1.3.1" checked_yaml: dependency: transitive description: name: checked_yaml - url: "https://pub.dartlang.org" + sha256: "3d1505d91afa809d177efd4eed5bb0eb65805097a1463abdd2add076effae311" + url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.0.2" cli_util: dependency: transitive description: name: cli_util - url: "https://pub.dartlang.org" + sha256: "66f86e916d285c1a93d3b79587d94bd71984a66aac4ff74e524cfa7877f1395c" + url: "https://pub.dev" source: hosted version: "0.3.5" clock: dependency: transitive description: name: clock - url: "https://pub.dartlang.org" + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" code_builder: dependency: transitive description: name: code_builder - url: "https://pub.dartlang.org" + sha256: "0d43dd1288fd145de1ecc9a3948ad4a6d5a82f0a14c4fdd0892260787d975cbe" + url: "https://pub.dev" source: hosted version: "4.4.0" collection: dependency: transitive description: name: collection - url: "https://pub.dartlang.org" + sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" colorize: dependency: transitive description: name: colorize - url: "https://pub.dartlang.org" + sha256: "584746cd6ba1cba0633b6720f494fe6f9601c4170f0666c1579d2aa2a61071ba" + url: "https://pub.dev" source: hosted version: "3.0.0" contextmenu: dependency: "direct main" description: name: contextmenu - url: "https://pub.dartlang.org" + sha256: e0c7d60e2fc9f316f5b03f5fe2c0f977d65125345d1a1f77eea02be612e32d0c + url: "https://pub.dev" source: hosted version: "3.0.0" convert: dependency: transitive description: name: convert - url: "https://pub.dartlang.org" + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.1" cross_file: dependency: transitive description: name: cross_file - url: "https://pub.dartlang.org" + sha256: f71079978789bc2fe78d79227f1f8cfe195b31bbd8db2399b0d15a4b96fb843b + url: "https://pub.dev" source: hosted version: "0.3.3+2" crypto: dependency: transitive description: name: crypto - url: "https://pub.dartlang.org" + sha256: aa274aa7774f8964e4f4f38cc994db7b6158dd36e9187aaceaddc994b35c6c67 + url: "https://pub.dev" source: hosted version: "3.0.2" csslib: dependency: transitive description: name: csslib - url: "https://pub.dartlang.org" + sha256: b36c7f7e24c0bdf1bf9a3da461c837d1de64b9f8beb190c9011d8c72a3dfd745 + url: "https://pub.dev" source: hosted version: "0.17.2" cupertino_icons: dependency: "direct main" description: name: cupertino_icons - url: "https://pub.dartlang.org" + sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be + url: "https://pub.dev" source: hosted version: "1.0.5" dart_style: dependency: transitive description: name: dart_style - url: "https://pub.dartlang.org" + sha256: "7a03456c3490394c8e7665890333e91ae8a49be43542b616e414449ac358acd4" + url: "https://pub.dev" source: hosted version: "2.2.4" dash_chat_2: dependency: "direct main" description: name: dash_chat_2 - url: "https://pub.dartlang.org" + sha256: "7ffdeb023fb2c9e194e2147ef8e967d36e4481493178051ceb36d98c62396ddd" + url: "https://pub.dev" source: hosted version: "0.0.15" debounce_throttle: dependency: "direct main" description: name: debounce_throttle - url: "https://pub.dartlang.org" + sha256: c95cf47afda975fc507794a52040a16756fb2f31ad3027d4e691c41862ff5692 + url: "https://pub.dev" source: hosted version: "2.0.0" desktop_drop: dependency: "direct main" description: name: desktop_drop - url: "https://pub.dartlang.org" + sha256: "0cd056191b701a2b5ba040f2306349e461fafdaa5df4569b2228cdf87b58eced" + url: "https://pub.dev" source: hosted version: "0.3.3" desktop_multi_window: dependency: "direct main" description: path: "." - ref: "057e6eb1bc7dcbcf9dafd1384274a611e4fe7124" - resolved-ref: "057e6eb1bc7dcbcf9dafd1384274a611e4fe7124" + ref: bc8604a88e52b2b6e64d2661ae49a71450a47af8 + resolved-ref: bc8604a88e52b2b6e64d2661ae49a71450a47af8 url: "https://github.com/Kingtous/rustdesk_desktop_multi_window" source: git version: "0.1.0" @@ -294,98 +334,112 @@ packages: dependency: "direct main" description: name: device_info_plus - url: "https://pub.dartlang.org" + sha256: b809c4ed5f7fcdb325ccc70b80ad934677dc4e2aa414bf46859a42bfdfafcbb6 + url: "https://pub.dev" source: hosted version: "4.1.3" device_info_plus_linux: dependency: transitive description: name: device_info_plus_linux - url: "https://pub.dartlang.org" + sha256: "77a8b3c4af06bc46507f89304d9f49dfc64b4ae004b994532ed23b34adeae4b3" + url: "https://pub.dev" source: hosted version: "3.0.0" device_info_plus_macos: dependency: transitive description: name: device_info_plus_macos - url: "https://pub.dartlang.org" + sha256: "37961762fbd46d3620c7b69ca606671014db55fc1b7a11e696fd90ed2e8fe03d" + url: "https://pub.dev" source: hosted version: "3.0.0" device_info_plus_platform_interface: dependency: transitive description: name: device_info_plus_platform_interface - url: "https://pub.dartlang.org" + sha256: "83fdba24fcf6846d3b10f10dfdc8b6c6d7ada5f8ed21d62ea2909c2dfa043773" + url: "https://pub.dev" source: hosted version: "3.0.0" device_info_plus_web: dependency: transitive description: name: device_info_plus_web - url: "https://pub.dartlang.org" + sha256: "5890f6094df108181c7a29720bc23d0fd6159f17d82787fac093d1fefcaf6325" + url: "https://pub.dev" source: hosted version: "3.0.0" device_info_plus_windows: dependency: transitive description: name: device_info_plus_windows - url: "https://pub.dartlang.org" + sha256: "23a2874af0e23ee6e3a2a0ebcecec3a9da13241f2cb93a93a44c8764df123dd7" + url: "https://pub.dev" source: hosted version: "4.1.0" draggable_float_widget: dependency: "direct main" description: name: draggable_float_widget - url: "https://pub.dartlang.org" + sha256: f3b291b335b7f7c7b721a6f42aeb6209fdfb055ea87980bff68c551b250795ea + url: "https://pub.dev" source: hosted version: "0.0.2" event_bus: dependency: transitive description: name: event_bus - url: "https://pub.dartlang.org" + sha256: "44baa799834f4c803921873e7446a2add0f3efa45e101a054b1f0ab9b95f8edc" + url: "https://pub.dev" source: hosted version: "2.0.0" external_path: dependency: "direct main" description: name: external_path - url: "https://pub.dartlang.org" + sha256: "2095c626fbbefe70d5a4afc9b1137172a68ee2c276e51c3c1283394485bea8f4" + url: "https://pub.dev" source: hosted version: "1.0.3" ffi: dependency: "direct main" description: name: ffi - url: "https://pub.dartlang.org" + sha256: a38574032c5f1dd06c4aee541789906c12ccaab8ba01446e800d9c5b79c4a978 + url: "https://pub.dev" source: hosted version: "2.0.1" ffigen: dependency: "direct dev" description: name: ffigen - url: "https://pub.dartlang.org" + sha256: "42bbfddebacef09c9a4eb2d9ef4049fa6a39edb8622b72ca69200cb6f1e3a6c0" + url: "https://pub.dev" source: hosted version: "7.2.4" file: dependency: transitive description: name: file - url: "https://pub.dartlang.org" + sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + url: "https://pub.dev" source: hosted version: "6.1.4" file_picker: dependency: "direct main" description: name: file_picker - url: "https://pub.dartlang.org" + sha256: d090ae03df98b0247b82e5928f44d1b959867049d18d73635e2e0bc3f49542b9 + url: "https://pub.dev" source: hosted - version: "5.2.4" + version: "5.2.5" fixnum: dependency: transitive description: name: fixnum - url: "https://pub.dartlang.org" + sha256: "04be3e934c52e082558cc9ee21f42f5c1cd7a1262f4c63cd0357c08d5bba81ec" + url: "https://pub.dev" source: hosted version: "1.0.1" flutter: @@ -397,42 +451,49 @@ packages: dependency: transitive description: name: flutter_blurhash - url: "https://pub.dartlang.org" + sha256: "05001537bd3fac7644fa6558b09ec8c0a3f2eba78c0765f88912882b1331a5c6" + url: "https://pub.dev" source: hosted version: "0.7.0" flutter_breadcrumb: dependency: "direct main" description: name: flutter_breadcrumb - url: "https://pub.dartlang.org" + sha256: "1531680034def621878562ad763079933dabe9f9f5d5add5a094190edc33259b" + url: "https://pub.dev" source: hosted version: "1.0.1" flutter_cache_manager: dependency: transitive description: name: flutter_cache_manager - url: "https://pub.dartlang.org" + sha256: "32cd900555219333326a2d0653aaaf8671264c29befa65bbd9856d204a4c9fb3" + url: "https://pub.dev" source: hosted version: "3.3.0" flutter_custom_cursor: dependency: "direct main" description: name: flutter_custom_cursor - url: "https://pub.dartlang.org" + sha256: "6c5204cf6a16650355b8aa47a8402e79922c07641390a32021a1069b561909ec" + url: "https://pub.dev" source: hosted - version: "0.0.2" + version: "0.0.3" flutter_improved_scrolling: dependency: "direct main" description: - name: flutter_improved_scrolling - url: "https://pub.dartlang.org" - source: hosted + path: "." + ref: "62f09545149f320616467c306c8c5f71714a18e6" + resolved-ref: "62f09545149f320616467c306c8c5f71714a18e6" + url: "https://github.com/Kingtous/flutter_improved_scrolling" + source: git version: "0.0.3" flutter_lints: dependency: "direct dev" description: name: flutter_lints - url: "https://pub.dartlang.org" + sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c + url: "https://pub.dev" source: hosted version: "2.0.1" flutter_localizations: @@ -444,28 +505,32 @@ packages: dependency: transitive description: name: flutter_parsed_text - url: "https://pub.dartlang.org" + sha256: "529cf5793b7acdf16ee0f97b158d0d4ba0bf06e7121ef180abe1a5b59e32c1e2" + url: "https://pub.dev" source: hosted version: "2.2.1" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle - url: "https://pub.dartlang.org" + sha256: "60fc7b78455b94e6de2333d2f95196d32cf5c22f4b0b0520a628804cb463503b" + url: "https://pub.dev" source: hosted version: "2.0.7" flutter_rust_bridge: dependency: "direct main" description: name: flutter_rust_bridge - url: "https://pub.dartlang.org" + sha256: "5aea0f3980dcd314f1890ef0d2392263817899cc15e543734b5d4dbe66b761eb" + url: "https://pub.dev" source: hosted - version: "1.61.1" + version: "1.62.0" flutter_svg: dependency: "direct main" description: name: flutter_svg - url: "https://pub.dartlang.org" + sha256: "6ff9fa12892ae074092de2fa6a9938fb21dbabfdaa2ff57dc697ff912fc8d4b2" + url: "https://pub.dev" source: hosted version: "1.1.6" flutter_web_plugins: @@ -477,427 +542,480 @@ packages: dependency: "direct dev" description: name: freezed - url: "https://pub.dartlang.org" + sha256: e819441678f1679b719008ff2ff0ef045d66eed9f9ec81166ca0d9b02a187454 + url: "https://pub.dev" source: hosted version: "2.3.2" freezed_annotation: dependency: "direct main" description: name: freezed_annotation - url: "https://pub.dartlang.org" + sha256: aeac15850ef1b38ee368d4c53ba9a847e900bb2c53a4db3f6881cbb3cb684338 + url: "https://pub.dev" source: hosted version: "2.2.0" frontend_server_client: dependency: transitive description: name: frontend_server_client - url: "https://pub.dartlang.org" + sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + url: "https://pub.dev" source: hosted version: "3.2.0" get: dependency: "direct main" description: name: get - url: "https://pub.dartlang.org" + sha256: "2ba20a47c8f1f233bed775ba2dd0d3ac97b4cf32fc17731b3dfc672b06b0e92a" + url: "https://pub.dev" source: hosted version: "4.6.5" glob: dependency: transitive description: name: glob - url: "https://pub.dartlang.org" + sha256: "4515b5b6ddb505ebdd242a5f2cc5d22d3d6a80013789debfbda7777f47ea308c" + url: "https://pub.dev" source: hosted version: "2.1.1" graphs: dependency: transitive description: name: graphs - url: "https://pub.dartlang.org" + sha256: f9e130f3259f52d26f0cfc0e964513796dafed572fa52e45d2f8d6ca14db39b2 + url: "https://pub.dev" source: hosted version: "2.2.0" html: dependency: transitive description: name: html - url: "https://pub.dartlang.org" + sha256: d9793e10dbe0e6c364f4c59bf3e01fb33a9b2a674bc7a1081693dba0614b6269 + url: "https://pub.dev" source: hosted version: "0.15.1" http: dependency: "direct main" description: name: http - url: "https://pub.dartlang.org" + sha256: "6aa2946395183537c8b880962d935877325d6a09a2867c3970c05c0fed6ac482" + url: "https://pub.dev" source: hosted version: "0.13.5" http_multi_server: dependency: transitive description: name: http_multi_server - url: "https://pub.dartlang.org" + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + url: "https://pub.dev" source: hosted version: "3.2.1" http_parser: dependency: transitive description: name: http_parser - url: "https://pub.dartlang.org" + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" source: hosted version: "4.0.2" icons_launcher: dependency: "direct dev" description: name: icons_launcher - url: "https://pub.dartlang.org" + sha256: c8e3ae1263822feafaec8a3c666ec84c2143470e1612f5481f1c875024c5f37e + url: "https://pub.dev" source: hosted - version: "2.0.4" + version: "2.0.6" image: dependency: "direct main" description: name: image - url: "https://pub.dartlang.org" + sha256: "8e9d133755c3e84c73288363e6343157c383a0c6c56fc51afcc5d4d7180306d6" + url: "https://pub.dev" source: hosted - version: "3.2.2" + version: "3.3.0" image_picker: dependency: "direct main" description: name: image_picker - url: "https://pub.dartlang.org" + sha256: f98d76672d309c8b7030c323b3394669e122d52b307d2bbd8d06bd70f5b2aabe + url: "https://pub.dev" source: hosted - version: "0.8.6" + version: "0.8.6+1" image_picker_android: dependency: transitive description: name: image_picker_android - url: "https://pub.dartlang.org" + sha256: b1cbfec0f5aef427a18eb573f5445af8c9c568626bf3388553e40c263d3f7368 + url: "https://pub.dev" source: hosted - version: "0.8.5+4" + version: "0.8.5+5" image_picker_for_web: dependency: transitive description: name: image_picker_for_web - url: "https://pub.dartlang.org" + sha256: "7d319fb74955ca46d9bf7011497860e3923bb67feebcf068f489311065863899" + url: "https://pub.dev" source: hosted version: "2.1.10" image_picker_ios: dependency: transitive description: name: image_picker_ios - url: "https://pub.dartlang.org" + sha256: "39c013200046d14c58b71dc4fa3d00e425fc9c699d589136cd3ca018727c0493" + url: "https://pub.dev" source: hosted - version: "0.8.6+3" + version: "0.8.6+6" image_picker_platform_interface: dependency: transitive description: name: image_picker_platform_interface - url: "https://pub.dartlang.org" + sha256: "7cef2f28f4f2fef99180f636c3d446b4ccbafd6ba0fad2adc9a80c4040f656b8" + url: "https://pub.dev" source: hosted version: "2.6.2" intl: dependency: transitive description: name: intl - url: "https://pub.dartlang.org" + sha256: "910f85bce16fb5c6f614e117efa303e85a1731bb0081edf3604a2ae6e9a3cc91" + url: "https://pub.dev" source: hosted version: "0.17.0" io: dependency: transitive description: name: io - url: "https://pub.dartlang.org" + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.4" js: dependency: transitive description: name: js - url: "https://pub.dartlang.org" + sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + url: "https://pub.dev" source: hosted - version: "0.6.4" + version: "0.6.5" json_annotation: dependency: transitive description: name: json_annotation - url: "https://pub.dartlang.org" + sha256: c33da08e136c3df0190bd5bbe51ae1df4a7d96e7954d1d7249fea2968a72d317 + url: "https://pub.dev" source: hosted - version: "4.7.0" + version: "4.8.0" lints: dependency: transitive description: name: lints - url: "https://pub.dartlang.org" + sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" + url: "https://pub.dev" source: hosted version: "2.0.1" logging: dependency: transitive description: name: logging - url: "https://pub.dartlang.org" + sha256: "04094f2eb032cbb06c6f6e8d3607edcfcb0455e2bb6cbc010cb01171dcb64e6d" + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" matcher: dependency: transitive description: name: matcher - url: "https://pub.dartlang.org" + sha256: c94db23593b89766cda57aab9ac311e3616cf87c6fa4e9749df032f66f30dcb8 + url: "https://pub.dev" source: hosted - version: "0.12.12" + version: "0.12.14" material_color_utilities: dependency: transitive description: name: material_color_utilities - url: "https://pub.dartlang.org" + sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + url: "https://pub.dev" source: hosted - version: "0.1.4" + version: "0.2.0" meta: dependency: transitive description: name: meta - url: "https://pub.dartlang.org" + sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + url: "https://pub.dev" source: hosted - version: "1.7.0" + version: "1.8.0" mime: dependency: transitive description: name: mime - url: "https://pub.dartlang.org" + sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.4" nested: dependency: transitive description: name: nested - url: "https://pub.dartlang.org" + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" source: hosted version: "1.0.0" octo_image: dependency: transitive description: name: octo_image - url: "https://pub.dartlang.org" + sha256: "107f3ed1330006a3bea63615e81cf637433f5135a52466c7caa0e7152bca9143" + url: "https://pub.dev" source: hosted version: "1.0.2" package_config: dependency: transitive description: name: package_config - url: "https://pub.dartlang.org" + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" source: hosted version: "2.1.0" package_info_plus: dependency: "direct main" description: name: package_info_plus - url: "https://pub.dartlang.org" + sha256: f62d7253edc197fe3c88d7c2ddab82d68f555e778d55390ccc3537eca8e8d637 + url: "https://pub.dev" source: hosted version: "1.4.3+1" package_info_plus_linux: dependency: transitive description: name: package_info_plus_linux - url: "https://pub.dartlang.org" + sha256: "04b575f44233d30edbb80a94e57cad9107aada334fc02aabb42b6becd13c43fc" + url: "https://pub.dev" source: hosted version: "1.0.5" package_info_plus_macos: dependency: transitive description: name: package_info_plus_macos - url: "https://pub.dartlang.org" + sha256: a2ad8b4acf4cd479d4a0afa5a74ea3f5b1c7563b77e52cc32b3ee6956d5482a6 + url: "https://pub.dev" source: hosted version: "1.3.0" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface - url: "https://pub.dartlang.org" + sha256: f7a0c8f1e7e981bc65f8b64137a53fd3c195b18d429fba960babc59a5a1c7ae8 + url: "https://pub.dev" source: hosted version: "1.0.2" package_info_plus_web: dependency: transitive description: name: package_info_plus_web - url: "https://pub.dartlang.org" + sha256: f0829327eb534789e0a16ccac8936a80beed4e2401c4d3a74f3f39094a822d3b + url: "https://pub.dev" source: hosted version: "1.0.6" package_info_plus_windows: dependency: transitive description: name: package_info_plus_windows - url: "https://pub.dartlang.org" + sha256: "79524f11c42dd9078b96d797b3cf79c0a2883a50c4920dc43da8562c115089bc" + url: "https://pub.dev" source: hosted version: "2.1.0" password_strength: dependency: "direct main" description: name: password_strength - url: "https://pub.dartlang.org" + sha256: "0e51e3d864e37873a1347e658147f88b66e141ee36c58e19828dc5637961e1ce" + url: "https://pub.dev" source: hosted version: "0.2.0" path: dependency: "direct main" description: name: path - url: "https://pub.dartlang.org" + sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b + url: "https://pub.dev" source: hosted - version: "1.8.1" + version: "1.8.2" path_drawing: dependency: transitive description: name: path_drawing - url: "https://pub.dartlang.org" + sha256: bbb1934c0cbb03091af082a6389ca2080345291ef07a5fa6d6e078ba8682f977 + url: "https://pub.dev" source: hosted version: "1.0.1" path_parsing: dependency: transitive description: name: path_parsing - url: "https://pub.dartlang.org" + sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf + url: "https://pub.dev" source: hosted version: "1.0.1" path_provider: dependency: "direct main" description: name: path_provider - url: "https://pub.dartlang.org" + sha256: dcea5feb97d8abf90cab9e9030b497fb7c3cbf26b7a1fe9e3ef7dcb0a1ddec95 + url: "https://pub.dev" source: hosted - version: "2.0.11" + version: "2.0.12" path_provider_android: dependency: transitive description: name: path_provider_android - url: "https://pub.dartlang.org" + sha256: a776c088d671b27f6e3aa8881d64b87b3e80201c64e8869b811325de7a76c15e + url: "https://pub.dev" source: hosted version: "2.0.22" - path_provider_ios: + path_provider_foundation: dependency: transitive description: - name: path_provider_ios - url: "https://pub.dartlang.org" + name: path_provider_foundation + sha256: "62a68e7e1c6c459f9289859e2fae58290c981ce21d1697faf54910fe1faa4c74" + url: "https://pub.dev" source: hosted - version: "2.0.11" + version: "2.1.1" path_provider_linux: dependency: transitive description: name: path_provider_linux - url: "https://pub.dartlang.org" + sha256: ab0987bf95bc591da42dffb38c77398fc43309f0b9b894dcc5d6f40c4b26c379 + url: "https://pub.dev" source: hosted version: "2.1.7" - path_provider_macos: - dependency: transitive - description: - name: path_provider_macos - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.6" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - url: "https://pub.dartlang.org" + sha256: f0abc8ebd7253741f05488b4813d936b4d07c6bae3e86148a09e342ee4b08e76 + url: "https://pub.dev" source: hosted version: "2.0.5" path_provider_windows: dependency: transitive description: name: path_provider_windows - url: "https://pub.dartlang.org" + sha256: bcabbe399d4042b8ee687e17548d5d3f527255253b4a639f5f8d2094a9c2b45c + url: "https://pub.dev" source: hosted version: "2.1.3" pedantic: dependency: transitive description: name: pedantic - url: "https://pub.dartlang.org" + sha256: "67fc27ed9639506c856c840ccce7594d0bdcd91bc8d53d6e52359449a1d50602" + url: "https://pub.dev" source: hosted version: "1.11.1" petitparser: dependency: transitive description: name: petitparser - url: "https://pub.dartlang.org" + sha256: "49392a45ced973e8d94a85fdb21293fbb40ba805fc49f2965101ae748a3683b4" + url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "5.1.0" platform: dependency: transitive description: name: platform - url: "https://pub.dartlang.org" + sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76" + url: "https://pub.dev" source: hosted version: "3.1.0" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - url: "https://pub.dartlang.org" + sha256: dbf0f707c78beedc9200146ad3cb0ab4d5da13c246336987be6940f026500d3a + url: "https://pub.dev" source: hosted version: "2.1.3" pointycastle: dependency: transitive description: name: pointycastle - url: "https://pub.dartlang.org" + sha256: db7306cf0249f838d1a24af52b5a5887c5bf7f31d8bb4e827d071dc0939ad346 + url: "https://pub.dev" source: hosted version: "3.6.2" pool: dependency: transitive description: name: pool - url: "https://pub.dartlang.org" + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" source: hosted version: "1.5.1" process: dependency: transitive description: name: process - url: "https://pub.dartlang.org" + sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" + url: "https://pub.dev" source: hosted version: "4.2.4" provider: dependency: "direct main" description: name: provider - url: "https://pub.dartlang.org" + sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f + url: "https://pub.dev" source: hosted version: "6.0.5" pub_semver: dependency: transitive description: name: pub_semver - url: "https://pub.dartlang.org" + sha256: "307de764d305289ff24ad257ad5c5793ce56d04947599ad68b3baa124105fc17" + url: "https://pub.dev" source: hosted version: "2.1.3" pubspec_parse: dependency: transitive description: name: pubspec_parse - url: "https://pub.dartlang.org" + sha256: "75f6614d6dde2dc68948dffbaa4fe5dae32cd700eb9fb763fe11dfb45a3c4d0a" + url: "https://pub.dev" source: hosted version: "1.2.1" puppeteer: dependency: transitive description: name: puppeteer - url: "https://pub.dartlang.org" + sha256: "4e235aaf9a338a45c9eb1ee38956e0ba369867bf144d7a27fdaf245409b2b87b" + url: "https://pub.dev" source: hosted - version: "2.12.0" + version: "2.21.0" qr_code_scanner: dependency: "direct main" description: name: qr_code_scanner - url: "https://pub.dartlang.org" + sha256: f23b68d893505a424f0bd2e324ebea71ed88465d572d26bb8d2e78a4749591fd + url: "https://pub.dev" source: hosted version: "1.0.1" quiver: dependency: transitive description: name: quiver - url: "https://pub.dartlang.org" + sha256: b1c1ac5ce6688d77f65f3375a9abb9319b3cb32486bdc7a1e0fdf004d7ba4e47 + url: "https://pub.dev" source: hosted version: "3.2.1" rxdart: dependency: transitive description: name: rxdart - url: "https://pub.dartlang.org" + sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" + url: "https://pub.dev" source: hosted version: "0.27.7" screen_retriever: @@ -913,42 +1031,48 @@ packages: dependency: "direct main" description: name: scroll_pos - url: "https://pub.dartlang.org" + sha256: cfca311b6b8d51538ff90e206fbe6ce3b36e7125ea6da4a40eb626c7f9f083b1 + url: "https://pub.dev" source: hosted version: "0.3.0" settings_ui: dependency: "direct main" description: name: settings_ui - url: "https://pub.dartlang.org" + sha256: d9838037cb554b24b4218b2d07666fbada3478882edefae375ee892b6c820ef3 + url: "https://pub.dev" source: hosted version: "2.0.2" shelf: dependency: transitive description: name: shelf - url: "https://pub.dartlang.org" + sha256: c24a96135a2ccd62c64b69315a14adc5c3419df63b4d7c05832a346fdb73682c + url: "https://pub.dev" source: hosted version: "1.4.0" shelf_static: dependency: transitive description: name: shelf_static - url: "https://pub.dartlang.org" + sha256: e792b76b96a36d4a41b819da593aff4bdd413576b3ba6150df5d8d9996d2e74c + url: "https://pub.dev" source: hosted version: "1.1.1" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - url: "https://pub.dartlang.org" + sha256: a988c0e8d8ffbdb8a28aa7ec8e449c260f3deb808781fe1284d22c5bba7156e8 + url: "https://pub.dev" source: hosted version: "1.0.3" simple_observable: dependency: transitive description: name: simple_observable - url: "https://pub.dartlang.org" + sha256: b392795c48f8b5f301b4c8f73e15f56e38fe70f42278c649d8325e859a783301 + url: "https://pub.dev" source: hosted version: "2.0.0" sky_engine: @@ -960,308 +1084,352 @@ packages: dependency: transitive description: name: source_gen - url: "https://pub.dartlang.org" + sha256: "2d79738b6bbf38a43920e2b8d189e9a3ce6cc201f4b8fc76be5e4fe377b1c38d" + url: "https://pub.dev" source: hosted version: "1.2.6" source_span: dependency: transitive description: name: source_span - url: "https://pub.dartlang.org" + sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + url: "https://pub.dev" source: hosted version: "1.9.1" sqflite: dependency: transitive description: name: sqflite - url: "https://pub.dartlang.org" + sha256: "78324387dc81df14f78df06019175a86a2ee0437624166c382e145d0a7fd9a4f" + url: "https://pub.dev" source: hosted - version: "2.0.3+1" + version: "2.2.4+1" sqflite_common: dependency: transitive description: name: sqflite_common - url: "https://pub.dartlang.org" + sha256: bfd6973aaeeb93475bc0d875ac9aefddf7965ef22ce09790eb963992ffc5183f + url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.2+2" stack_trace: dependency: transitive description: name: stack_trace - url: "https://pub.dartlang.org" + sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.11.0" stream_channel: dependency: transitive description: name: stream_channel - url: "https://pub.dartlang.org" + sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + url: "https://pub.dev" source: hosted version: "2.1.1" stream_transform: dependency: transitive description: name: stream_transform - url: "https://pub.dartlang.org" + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + url: "https://pub.dev" source: hosted version: "2.1.0" string_scanner: dependency: transitive description: name: string_scanner - url: "https://pub.dartlang.org" + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.2.0" synchronized: dependency: transitive description: name: synchronized - url: "https://pub.dartlang.org" + sha256: "33b31b6beb98100bf9add464a36a8dd03eb10c7a8cf15aeec535e9b054aaf04b" + url: "https://pub.dev" source: hosted - version: "3.0.0+3" + version: "3.0.1" term_glyph: dependency: transitive description: name: term_glyph - url: "https://pub.dartlang.org" + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" source: hosted version: "1.2.1" timing: dependency: transitive description: name: timing - url: "https://pub.dartlang.org" + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.0.1" toggle_switch: dependency: "direct main" description: name: toggle_switch - url: "https://pub.dartlang.org" + sha256: "3814548f25ee11f88d3b1905e2e7c8e47e4a406752f553ed287f6d86a2dcf91d" + url: "https://pub.dev" source: hosted version: "1.4.0" tuple: dependency: "direct main" description: name: tuple - url: "https://pub.dartlang.org" + sha256: "0ea99cd2f9352b2586583ab2ce6489d1f95a5f6de6fb9492faaf97ae2060f0aa" + url: "https://pub.dev" source: hosted version: "2.0.1" typed_data: dependency: transitive description: name: typed_data - url: "https://pub.dartlang.org" + sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5" + url: "https://pub.dev" source: hosted version: "1.3.1" uni_links: dependency: "direct main" description: name: uni_links - url: "https://pub.dartlang.org" + sha256: "051098acfc9e26a9fde03b487bef5d3d228ca8f67693480c6f33fd4fbb8e2b6e" + url: "https://pub.dev" source: hosted version: "0.5.1" uni_links_desktop: dependency: "direct main" description: name: uni_links_desktop - url: "https://pub.dartlang.org" + sha256: "205484c01890259b56d9271bcf299adf9889e881616c976f13061e29e94bb9f0" + url: "https://pub.dev" source: hosted version: "0.1.4" uni_links_platform_interface: dependency: transitive description: name: uni_links_platform_interface - url: "https://pub.dartlang.org" + sha256: "929cf1a71b59e3b7c2d8a2605a9cf7e0b125b13bc858e55083d88c62722d4507" + url: "https://pub.dev" source: hosted version: "1.0.0" uni_links_web: dependency: transitive description: name: uni_links_web - url: "https://pub.dartlang.org" + sha256: "7539db908e25f67de2438e33cc1020b30ab94e66720b5677ba6763b25f6394df" + url: "https://pub.dev" source: hosted version: "0.1.0" universal_io: dependency: transitive description: name: universal_io - url: "https://pub.dartlang.org" + sha256: "79f78ddad839ee3aae3ec7c01eb4575faf0d5c860f8e5223bc9f9c17f7f03cef" + url: "https://pub.dev" source: hosted version: "2.0.4" url_launcher: dependency: "direct main" description: name: url_launcher - url: "https://pub.dartlang.org" + sha256: "698fa0b4392effdc73e9e184403b627362eb5fbf904483ac9defbb1c2191d809" + url: "https://pub.dev" source: hosted - version: "6.1.7" + version: "6.1.8" url_launcher_android: dependency: transitive description: name: url_launcher_android - url: "https://pub.dartlang.org" + sha256: "3e2f6dfd2c7d9cd123296cab8ef66cfc2c1a13f5845f42c7a0f365690a8a7dd1" + url: "https://pub.dev" source: hosted - version: "6.0.22" + version: "6.0.23" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - url: "https://pub.dartlang.org" + sha256: bb328b24d3bccc20bdf1024a0990ac4f869d57663660de9c936fb8c043edefe3 + url: "https://pub.dev" source: hosted - version: "6.0.17" + version: "6.0.18" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - url: "https://pub.dartlang.org" + sha256: "318c42cba924e18180c029be69caf0a1a710191b9ec49bb42b5998fdcccee3cc" + url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - url: "https://pub.dartlang.org" + sha256: "41988b55570df53b3dd2a7fc90c76756a963de6a8c5f8e113330cb35992e2094" + url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - url: "https://pub.dartlang.org" + sha256: "4eae912628763eb48fc214522e58e942fd16ce195407dbf45638239523c759a6" + url: "https://pub.dev" source: hosted version: "2.1.1" url_launcher_web: dependency: transitive description: name: url_launcher_web - url: "https://pub.dartlang.org" + sha256: "44d79408ce9f07052095ef1f9a693c258d6373dc3944249374e30eff7219ccb0" + url: "https://pub.dev" source: hosted - version: "2.0.13" + version: "2.0.14" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - url: "https://pub.dartlang.org" + sha256: b6217370f8eb1fd85c8890c539f5a639a01ab209a36db82c921ebeacefc7a615 + url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.3" uuid: dependency: transitive description: name: uuid - url: "https://pub.dartlang.org" + sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" + url: "https://pub.dev" source: hosted version: "3.0.7" vector_math: dependency: transitive description: name: vector_math - url: "https://pub.dartlang.org" + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" video_player: dependency: transitive description: name: video_player - url: "https://pub.dartlang.org" + sha256: "59f7f31c919c59cbedd37c617317045f5f650dc0eeb568b0b0de9a36472bdb28" + url: "https://pub.dev" source: hosted - version: "2.4.10" + version: "2.5.1" video_player_android: dependency: transitive description: name: video_player_android - url: "https://pub.dartlang.org" + sha256: "984388511230bac63feb53b2911a70e829fe0976b6b2213f5c579c4e0a882db3" + url: "https://pub.dev" source: hosted version: "2.3.10" video_player_avfoundation: dependency: transitive description: name: video_player_avfoundation - url: "https://pub.dartlang.org" + sha256: d9f7a46d6a77680adb03ec05a381025d6e890ebe636637c6c3014cc3926b97e9 + url: "https://pub.dev" source: hosted version: "2.3.8" video_player_platform_interface: dependency: transitive description: name: video_player_platform_interface - url: "https://pub.dartlang.org" + sha256: "42bb75de5e9b79e1f20f1d95f688fac0f95beac4d89c6eb2cd421724d4432dae" + url: "https://pub.dev" source: hosted version: "6.0.1" video_player_web: dependency: transitive description: name: video_player_web - url: "https://pub.dartlang.org" + sha256: b649b07b8f8f553bee4a97a0a53d0fe78a70b115eafaf0105b612b32b05ddb99 + url: "https://pub.dev" source: hosted version: "2.0.13" visibility_detector: dependency: "direct main" description: name: visibility_detector - url: "https://pub.dartlang.org" + sha256: "15c54a459ec2c17b4705450483f3d5a2858e733aee893dcee9d75fd04814940d" + url: "https://pub.dev" source: hosted version: "0.3.3" wakelock: dependency: "direct main" description: name: wakelock - url: "https://pub.dartlang.org" + sha256: "769ecf42eb2d07128407b50cb93d7c10bd2ee48f0276ef0119db1d25cc2f87db" + url: "https://pub.dev" source: hosted version: "0.6.2" wakelock_macos: dependency: transitive description: name: wakelock_macos - url: "https://pub.dartlang.org" + sha256: "047c6be2f88cb6b76d02553bca5a3a3b95323b15d30867eca53a19a0a319d4cd" + url: "https://pub.dev" source: hosted version: "0.4.0" wakelock_platform_interface: dependency: transitive description: name: wakelock_platform_interface - url: "https://pub.dartlang.org" + sha256: "1f4aeb81fb592b863da83d2d0f7b8196067451e4df91046c26b54a403f9de621" + url: "https://pub.dev" source: hosted version: "0.3.0" wakelock_web: dependency: transitive description: name: wakelock_web - url: "https://pub.dartlang.org" + sha256: "1b256b811ee3f0834888efddfe03da8d18d0819317f20f6193e2922b41a501b5" + url: "https://pub.dev" source: hosted version: "0.4.0" wakelock_windows: dependency: transitive description: name: wakelock_windows - url: "https://pub.dartlang.org" + sha256: "857f77b3fe6ae82dd045455baa626bc4b93cb9bb6c86bf3f27c182167c3a5567" + url: "https://pub.dev" source: hosted version: "0.2.1" watcher: dependency: transitive description: name: watcher - url: "https://pub.dartlang.org" + sha256: "6a7f46926b01ce81bfc339da6a7f20afbe7733eff9846f6d6a5466aa4c6667c0" + url: "https://pub.dev" source: hosted version: "1.0.2" web_socket_channel: dependency: transitive description: name: web_socket_channel - url: "https://pub.dartlang.org" + sha256: ca49c0bc209c687b887f30527fb6a9d80040b072cc2990f34b9bec3e7663101b + url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.0" win32: dependency: "direct main" description: name: win32 - url: "https://pub.dartlang.org" + sha256: c9ebe7ee4ab0c2194e65d3a07d8c54c5d00bb001b76081c4a04cdb8448b59e46 + url: "https://pub.dev" source: hosted version: "3.1.3" win32_registry: dependency: transitive description: name: win32_registry - url: "https://pub.dartlang.org" + sha256: "66e78552f17501aced68fe77425b13156998f1bd3d58f1cd8cd0af2dbe4520e3" + url: "https://pub.dev" source: hosted version: "1.0.2" window_manager: @@ -1286,37 +1454,42 @@ packages: dependency: transitive description: name: xdg_directories - url: "https://pub.dartlang.org" + sha256: bd512f03919aac5f1313eb8249f223bacf4927031bf60b02601f81f687689e86 + url: "https://pub.dev" source: hosted - version: "0.2.0+2" + version: "0.2.0+3" xml: dependency: transitive description: name: xml - url: "https://pub.dartlang.org" + sha256: "979ee37d622dec6365e2efa4d906c37470995871fe9ae080d967e192d88286b5" + url: "https://pub.dev" source: hosted - version: "6.1.0" + version: "6.2.2" yaml: dependency: transitive description: name: yaml - url: "https://pub.dartlang.org" + sha256: "23812a9b125b48d4007117254bca50abb6c712352927eece9e155207b1db2370" + url: "https://pub.dev" source: hosted version: "3.1.1" yaml_edit: dependency: transitive description: name: yaml_edit - url: "https://pub.dartlang.org" + sha256: "4240d1b19841b8af5786121e4e357735cc2a8ffb19176bff5769d73c34e2a8a5" + url: "https://pub.dev" source: hosted version: "2.0.3" zxing2: dependency: "direct main" description: name: zxing2 - url: "https://pub.dartlang.org" + sha256: "1913c33844c68b62573741134ef5f987f1e15e331c95ac7dc327afbb9896e9ec" + url: "https://pub.dev" source: hosted - version: "0.1.0" + version: "0.1.1" sdks: - dart: ">=2.17.1 <3.0.0" - flutter: ">=3.0.0" + dart: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" diff --git a/src/core_main.rs b/src/core_main.rs index 8658b736c..d5b56bc1f 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -247,8 +247,6 @@ pub fn core_main() -> Option> { #[cfg(feature = "flutter")] crate::flutter::connection_manager::start_listen_ipc_thread(); crate::ui_interface::start_option_status_sync(); - #[cfg(target_os = "macos")] - crate::platform::macos::hide_dock(); } } //_async_logger_holder.map(|x| x.flush()); diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index ebaa160f1..c2ae2b6b0 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1241,6 +1241,12 @@ pub fn main_is_login_wayland() -> SyncReturn { SyncReturn(is_login_wayland()) } +pub fn main_hide_docker() -> SyncReturn { + #[cfg(target_os = "macos")] + crate::platform::macos::hide_dock(); + SyncReturn(true) +} + #[cfg(target_os = "android")] pub mod server_side { use jni::{ From 20003841d080de149ca96a798d99d8719e5d0458 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 1 Feb 2023 17:36:05 +0800 Subject: [PATCH 1667/2015] bind.mainHideDocker must be put in windowManager.waitUntilReadyToShow --- flutter/lib/main.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 53ae2f5dd..ff7a72124 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -88,7 +88,6 @@ Future main(List args) async { debugPrint("--cm started"); desktopType = DesktopType.cm; await windowManager.ensureInitialized(); - bind.mainHideDocker(); runConnectionManagerScreen(args.contains('--hide')); } else if (args.contains('--install')) { runInstallPage(); @@ -224,6 +223,7 @@ void showCmWindow() { WindowOptions windowOptions = getHiddenTitleBarWindowOptions(size: kConnectionManagerWindowSize); windowManager.waitUntilReadyToShow(windowOptions, () async { + bind.mainHideDocker(); await windowManager.show(); await Future.wait([windowManager.focus(), windowManager.setOpacity(1)]); // ensure initial window size to be changed @@ -237,6 +237,7 @@ void hideCmWindow() { getHiddenTitleBarWindowOptions(size: kConnectionManagerWindowSize); windowManager.setOpacity(0); windowManager.waitUntilReadyToShow(windowOptions, () async { + bind.mainHideDocker(); await windowManager.hide(); }); } From 2e53580caaf069e4e044b03d45331ab6ddfe2ce6 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 1 Feb 2023 19:36:36 +0800 Subject: [PATCH 1668/2015] beautify quality monitor --- flutter/lib/common/widgets/overlay.dart | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/flutter/lib/common/widgets/overlay.dart b/flutter/lib/common/widgets/overlay.dart index aaf52fb07..4b4172ffd 100644 --- a/flutter/lib/common/widgets/overlay.dart +++ b/flutter/lib/common/widgets/overlay.dart @@ -320,20 +320,21 @@ class QualityMonitor extends StatelessWidget { final QualityMonitorModel qualityMonitorModel; QualityMonitor(this.qualityMonitorModel); - Widget _row(String info, String? value) { + Widget _row(String info, String? value, {Color? rightColor}) { return Row( children: [ Expanded( flex: 8, child: AutoSizeText(info, - style: TextStyle(color: MyTheme.grayBg), + style: TextStyle(color: MyTheme.darkGray), textAlign: TextAlign.right, maxLines: 1)), Spacer(flex: 1), Expanded( flex: 8, child: AutoSizeText(value ?? '', - style: TextStyle(color: MyTheme.grayBg), maxLines: 1)), + style: TextStyle(color: rightColor ?? Colors.white), + maxLines: 1)), ], ); } @@ -351,13 +352,15 @@ class QualityMonitor extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _row("Speed", qualityMonitorModel.data.speed ?? ''), - _row("FPS", qualityMonitorModel.data.fps ?? ''), + _row("Speed", qualityMonitorModel.data.speed ?? '-'), + _row("FPS", qualityMonitorModel.data.fps ?? '-'), _row( - "Delay", "${qualityMonitorModel.data.delay ?? ''}ms"), + "Delay", "${qualityMonitorModel.data.delay ?? '-'}ms", + rightColor: Colors.green), _row("Target Bitrate", - "${qualityMonitorModel.data.targetBitrate ?? ''}kb"), - _row("Codec", qualityMonitorModel.data.codecFormat ?? ''), + "${qualityMonitorModel.data.targetBitrate ?? '-'}kb"), + _row( + "Codec", qualityMonitorModel.data.codecFormat ?? '-'), ], ), ) From 6f95c38f854ed95536843aeeef84791ef29ae216 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 1 Feb 2023 20:36:14 +0800 Subject: [PATCH 1669/2015] chore: remove useless code --- src/core_main.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/core_main.rs b/src/core_main.rs index d5b56bc1f..b34047f86 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -290,10 +290,6 @@ fn import_config(path: &str) { /// If it returns [`Some`], then the process will continue, and flutter gui will be started. #[cfg(feature = "flutter")] fn core_main_invoke_new_connection(mut args: std::env::Args) -> Option> { - args.position(|element| { - return element == "--connect"; - }) - .unwrap(); let peer_id = args.next().unwrap_or("".to_string()); if peer_id.is_empty() { eprintln!("please provide a valid peer id"); From fdfda2a982c68d5d7e889a1d29b6b12f78235f9a Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 1 Feb 2023 20:40:09 +0800 Subject: [PATCH 1670/2015] chore: revert last commit and change unwrap to ? --- src/core_main.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/core_main.rs b/src/core_main.rs index b34047f86..714502e85 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -290,6 +290,9 @@ fn import_config(path: &str) { /// If it returns [`Some`], then the process will continue, and flutter gui will be started. #[cfg(feature = "flutter")] fn core_main_invoke_new_connection(mut args: std::env::Args) -> Option> { + args.position(|element| { + return element == "--connect"; + })?; let peer_id = args.next().unwrap_or("".to_string()); if peer_id.is_empty() { eprintln!("please provide a valid peer id"); From 68cc667f475ee66c445fe66380c98c4ef9f9d934 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 1 Feb 2023 21:28:26 +0800 Subject: [PATCH 1671/2015] partially fix issue #2747: text selectable, more top margin of buttons on dialog --- flutter/lib/common.dart | 3 ++- flutter/lib/common/widgets/chat_page.dart | 3 ++- .../lib/desktop/pages/desktop_setting_page.dart | 15 +++++++++------ 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index cf7de0fa2..ee7353c12 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -707,7 +707,8 @@ void msgBox(String id, String type, String title, String text, String link, dialogManager.show( (setState, close) => CustomAlertDialog( title: null, - content: msgboxContent(type, title, text), + content: SelectionArea( + child: msgboxContent(type, title, text).paddingOnly(bottom: 10)), actions: buttons, onSubmit: hasOk ? submit : null, onCancel: hasCancel == true ? cancel : null, diff --git a/flutter/lib/common/widgets/chat_page.dart b/flutter/lib/common/widgets/chat_page.dart index 510ce1f73..d1d96199a 100644 --- a/flutter/lib/common/widgets/chat_page.dart +++ b/flutter/lib/common/widgets/chat_page.dart @@ -51,7 +51,7 @@ class ChatPage extends StatelessWidget implements PageShape { return Stack( children: [ LayoutBuilder(builder: (context, constraints) { - return DashChat( + final chat = DashChat( onSend: (chatMsg) { chatModel.send(chatMsg); chatModel.inputNode.requestFocus(); @@ -108,6 +108,7 @@ class ChatPage extends StatelessWidget implements PageShape { borderBottomLeft: 8, )), ); + return SelectionArea(child: chat); }), desktopType == DesktopType.cm || chatModel.currentID == ChatModel.clientModeID diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index df87a0ead..06300cda4 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -1113,10 +1113,12 @@ class _AboutState extends State<_About> { const SizedBox( height: 8.0, ), - Text('${translate('Version')}: $version') - .marginSymmetric(vertical: 4.0), - Text('${translate('Build Date')}: $buildDate') - .marginSymmetric(vertical: 4.0), + SelectionArea( + child: Text('${translate('Version')}: $version') + .marginSymmetric(vertical: 4.0)), + SelectionArea( + child: Text('${translate('Build Date')}: $buildDate') + .marginSymmetric(vertical: 4.0)), InkWell( onTap: () { launchUrlString('https://rustdesk.com/privacy'); @@ -1137,7 +1139,8 @@ class _AboutState extends State<_About> { decoration: const BoxDecoration(color: Color(0xFF2c8cff)), padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 8), - child: Row( + child: SelectionArea( + child: Row( children: [ Expanded( child: Column( @@ -1157,7 +1160,7 @@ class _AboutState extends State<_About> { ), ), ], - ), + )), ).marginSymmetric(vertical: 4.0) ], ).marginOnly(left: _kContentHMargin) From a9f2144638db5b1c9736fbc63928b9bec55b7b84 Mon Sep 17 00:00:00 2001 From: "Miguel F. G" <116861809+flusheDData@users.noreply.github.com> Date: Wed, 1 Feb 2023 15:42:28 +0100 Subject: [PATCH 1672/2015] Update es.rs new term added --- src/lang/es.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/es.rs b/src/lang/es.rs index 8f4275d5d..3848d1925 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -435,6 +435,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", ""), + ("Closed as expected", "Cerrado como se esperaba"), ].iter().cloned().collect(); } From 8d60bcd51a503a06e3c9e95b716d8be5dbd06a28 Mon Sep 17 00:00:00 2001 From: 21pages Date: Wed, 1 Feb 2023 19:49:41 +0800 Subject: [PATCH 1673/2015] remove useless empty --switch_uuid Signed-off-by: 21pages --- flutter/lib/utils/multi_window_manager.dart | 9 ++++++--- src/core_main.rs | 8 ++++++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/flutter/lib/utils/multi_window_manager.dart b/flutter/lib/utils/multi_window_manager.dart index 224052bff..550e9ab08 100644 --- a/flutter/lib/utils/multi_window_manager.dart +++ b/flutter/lib/utils/multi_window_manager.dart @@ -43,11 +43,14 @@ class RustDeskMultiWindowManager { Future newRemoteDesktop(String remoteId, {String? switch_uuid}) async { - final msg = jsonEncode({ + var params = { "type": WindowType.RemoteDesktop.index, "id": remoteId, - "switch_uuid": switch_uuid ?? "" - }); + }; + if (switch_uuid != null) { + params['switch_uuid'] = switch_uuid; + } + final msg = jsonEncode(params); try { final ids = await DesktopMultiWindow.getAllSubWindowIds(); diff --git a/src/core_main.rs b/src/core_main.rs index 714502e85..89a962f1d 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -304,9 +304,13 @@ fn core_main_invoke_new_connection(mut args: std::env::Args) -> Option Date: Wed, 1 Feb 2023 19:56:57 +0800 Subject: [PATCH 1674/2015] default display settings Signed-off-by: 21pages --- .../desktop/pages/desktop_setting_page.dart | 215 +++++++++++++++++- .../lib/desktop/widgets/remote_menubar.dart | 44 ++-- libs/hbb_common/src/config.rs | 107 ++++++++- src/flutter_ffi.rs | 8 + src/lang/ca.rs | 10 +- src/lang/cn.rs | 8 + src/lang/cs.rs | 8 + src/lang/da.rs | 8 + src/lang/de.rs | 8 + src/lang/eo.rs | 10 +- src/lang/es.rs | 10 +- src/lang/fa.rs | 10 +- src/lang/fr.rs | 10 +- src/lang/gr.rs | 10 +- src/lang/hu.rs | 8 + src/lang/id.rs | 10 +- src/lang/it.rs | 10 +- src/lang/ja.rs | 10 +- src/lang/ko.rs | 10 +- src/lang/kz.rs | 10 +- src/lang/pl.rs | 10 +- src/lang/pt_PT.rs | 10 +- src/lang/ptbr.rs | 10 +- src/lang/ro.rs | 11 + src/lang/ru.rs | 10 +- src/lang/sk.rs | 10 +- src/lang/sl.rs | 10 +- src/lang/sq.rs | 10 +- src/lang/sr.rs | 10 +- src/lang/sv.rs | 10 +- src/lang/template.rs | 10 +- src/lang/th.rs | 10 +- src/lang/tr.rs | 10 +- src/lang/tw.rs | 10 +- src/lang/ua.rs | 10 +- src/lang/vn.rs | 10 +- src/ui_interface.rs | 12 + 37 files changed, 643 insertions(+), 54 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 06300cda4..e4a7e1a25 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -33,6 +33,7 @@ const double _kContentFontSize = 15; const Color _accentColor = MyTheme.accent; const String _kSettingPageControllerTag = 'settingPageController'; const String _kSettingPageIndexTag = 'settingPageIndex'; +const int _kPageCount = 6; class _TabInfo { late final String label; @@ -51,7 +52,7 @@ class DesktopSettingPage extends StatefulWidget { State createState() => _DesktopSettingPageState(); static void switch2page(int page) { - if (page >= 5) return; + if (page >= _kPageCount) return; try { if (Get.isRegistered(tag: _kSettingPageControllerTag)) { DesktopTabPage.onAddSetting(initialPage: page); @@ -75,6 +76,7 @@ class _DesktopSettingPageState extends State _TabInfo('Security', Icons.enhanced_encryption_outlined, Icons.enhanced_encryption), _TabInfo('Network', Icons.link_outlined, Icons.link), + _TabInfo('Display', Icons.desktop_windows_outlined, Icons.desktop_windows), _TabInfo('Account', Icons.person_outline, Icons.person), _TabInfo('About', Icons.info_outline, Icons.info) ]; @@ -88,7 +90,8 @@ class _DesktopSettingPageState extends State @override void initState() { super.initState(); - selectedIndex = (widget.initialPage < 5 ? widget.initialPage : 0).obs; + selectedIndex = + (widget.initialPage < _kPageCount ? widget.initialPage : 0).obs; Get.put(selectedIndex, tag: _kSettingPageIndexTag); controller = PageController(initialPage: widget.initialPage); Get.put(controller, tag: _kSettingPageControllerTag); @@ -130,6 +133,7 @@ class _DesktopSettingPageState extends State _General(), _Safety(), _Network(), + _Display(), _Account(), _About(), ], @@ -1047,6 +1051,213 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin { } } +class _Display extends StatefulWidget { + const _Display({Key? key}) : super(key: key); + + @override + State<_Display> createState() => _DisplayState(); +} + +class _DisplayState extends State<_Display> { + @override + Widget build(BuildContext context) { + final scrollController = ScrollController(); + return DesktopScrollWrapper( + scrollController: scrollController, + child: ListView( + controller: scrollController, + physics: NeverScrollableScrollPhysics(), + children: [ + viewStyle(context), + scrollStyle(context), + imageQuality(context), + codec(context), + ]).marginOnly(bottom: _kListViewBottomMargin)); + } + + Widget viewStyle(BuildContext context) { + final key = 'view_style'; + onChanged(String value) async { + await bind.mainSetUserDefaultOption(key: key, value: value); + setState(() {}); + } + + final groupValue = bind.mainGetUserDefaultOption(key: key); + return _Card(title: 'Default View Style', children: [ + _Radio(context, + value: kRemoteViewStyleOriginal, + groupValue: groupValue, + label: 'Scale original', + onChanged: onChanged), + _Radio(context, + value: kRemoteViewStyleAdaptive, + groupValue: groupValue, + label: 'Scale adaptive', + onChanged: onChanged), + ]); + } + + Widget scrollStyle(BuildContext context) { + final key = 'scroll_style'; + onChanged(String value) async { + await bind.mainSetUserDefaultOption(key: key, value: value); + setState(() {}); + } + + final groupValue = bind.mainGetUserDefaultOption(key: key); + return _Card(title: 'Default Scroll Style', children: [ + _Radio(context, + value: kRemoteScrollStyleAuto, + groupValue: groupValue, + label: 'ScrollAuto', + onChanged: onChanged), + _Radio(context, + value: kRemoteScrollStyleBar, + groupValue: groupValue, + label: 'Scrollbar', + onChanged: onChanged), + ]); + } + + Widget imageQuality(BuildContext context) { + final key = 'image_quality'; + onChanged(String value) async { + await bind.mainSetUserDefaultOption(key: key, value: value); + setState(() {}); + } + + final groupValue = bind.mainGetUserDefaultOption(key: key); + final qualityKey = 'custom_image_quality'; + final qualityValue = + (double.tryParse(bind.mainGetUserDefaultOption(key: qualityKey)) ?? + 50.0) + .obs; + final fpsKey = 'custom-fps'; + final fpsValue = + (double.tryParse(bind.mainGetUserDefaultOption(key: fpsKey)) ?? 30.0) + .obs; + return _Card(title: 'Default Image Quality', children: [ + _Radio(context, + value: kRemoteImageQualityBest, + groupValue: groupValue, + label: 'Good image quality', + onChanged: onChanged), + _Radio(context, + value: kRemoteImageQualityBalanced, + groupValue: groupValue, + label: 'Balanced', + onChanged: onChanged), + _Radio(context, + value: kRemoteImageQualityLow, + groupValue: groupValue, + label: 'Optimize reaction time', + onChanged: onChanged), + _Radio(context, + value: kRemoteImageQualityCustom, + groupValue: groupValue, + label: 'Custom', + onChanged: onChanged), + Offstage( + offstage: groupValue != kRemoteImageQualityCustom, + child: Column( + children: [ + Obx(() => Row( + children: [ + Slider( + value: qualityValue.value, + min: 10.0, + max: 100.0, + divisions: 18, + onChanged: (double value) async { + qualityValue.value = value; + await bind.mainSetUserDefaultOption( + key: qualityKey, value: value.toString()); + }, + ), + SizedBox( + width: 40, + child: Text( + '${qualityValue.value.round()}%', + style: const TextStyle(fontSize: 15), + )), + SizedBox( + width: 50, + child: Text( + translate('Bitrate'), + style: const TextStyle(fontSize: 15), + )) + ], + )), + Obx(() => Row( + children: [ + Slider( + value: fpsValue.value, + min: 10.0, + max: 120.0, + divisions: 22, + onChanged: (double value) async { + fpsValue.value = value; + await bind.mainSetUserDefaultOption( + key: fpsKey, value: value.toString()); + }, + ), + SizedBox( + width: 40, + child: Text( + '${fpsValue.value.round()}', + style: const TextStyle(fontSize: 15), + )), + SizedBox( + width: 50, + child: Text( + translate('FPS'), + style: const TextStyle(fontSize: 15), + )) + ], + )), + ], + ), + ) + ]); + } + + Widget codec(BuildContext context) { + if (!bind.mainHasHwcodec()) { + return Offstage(); + } + final key = 'codec-preference'; + onChanged(String value) async { + await bind.mainSetUserDefaultOption(key: key, value: value); + setState(() {}); + } + + final groupValue = bind.mainGetUserDefaultOption(key: key); + + return _Card(title: 'Default Codec', children: [ + _Radio(context, + value: 'auto', + groupValue: groupValue, + label: 'Auto', + onChanged: onChanged), + _Radio(context, + value: 'vp9', + groupValue: groupValue, + label: 'VP9', + onChanged: onChanged), + _Radio(context, + value: 'h264', + groupValue: groupValue, + label: 'H264', + onChanged: onChanged), + _Radio(context, + value: 'h265', + groupValue: groupValue, + label: 'H265', + onChanged: onChanged), + ]); + } +} + class _Account extends StatefulWidget { const _Account({Key? key}) : super(key: key); diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 517dc9750..64d289fcc 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -867,18 +867,24 @@ class _RemoteMenubarState extends State { value: qualitySliderValue.value, min: qualityMinValue, max: qualityMaxValue, - divisions: 90, + divisions: 18, onChanged: (double value) { qualitySliderValue.value = value; debouncerQuality.value = value; }, ), SizedBox( - width: 90, - child: Obx(() => Text( - '${qualitySliderValue.value.round()}% Bitrate', - style: const TextStyle(fontSize: 15), - ))) + width: 40, + child: Text( + '${qualitySliderValue.value.round()}%', + style: const TextStyle(fontSize: 15), + )), + SizedBox( + width: 50, + child: Text( + translate('Bitrate'), + style: const TextStyle(fontSize: 15), + )) ], )); // fps @@ -919,20 +925,17 @@ class _RemoteMenubarState extends State { }, ))), SizedBox( - width: 90, - child: Obx(() { - final fps = fpsSliderValue.value.round(); - String text; - if (fps < 100) { - text = '$fps FPS'; - } else { - text = '$fps FPS'; - } - return Text( - text, - style: const TextStyle(fontSize: 15), - ); - })) + width: 40, + child: Obx(() => Text( + '${fpsSliderValue.value.round()}', + style: const TextStyle(fontSize: 15), + ))), + SizedBox( + width: 50, + child: Text( + translate('FPS'), + style: const TextStyle(fontSize: 15), + )) ], ), ); @@ -1111,6 +1114,7 @@ class _RemoteMenubarState extends State { )); } } + displayMenu.add(MenuEntryDivider()); /// Show remote cursor if (!widget.ffi.canvasModel.cursorEmbedded) { diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 8bea99106..ce4be6119 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -192,7 +192,10 @@ pub struct PeerConfig { deserialize_with = "PeerConfig::deserialize_image_quality" )] pub image_quality: String, - #[serde(default)] + #[serde( + default = "PeerConfig::default_custom_image_quality", + deserialize_with = "PeerConfig::deserialize_custom_image_quality" + )] pub custom_image_quality: Vec, #[serde(default)] pub show_remote_cursor: bool, @@ -961,26 +964,51 @@ impl PeerConfig { serde_field_string!( default_view_style, deserialize_view_style, - "original".to_owned() + UserDefaultConfig::load().get("view_style") ); serde_field_string!( default_scroll_style, deserialize_scroll_style, - "scrollauto".to_owned() + UserDefaultConfig::load().get("scroll_style") ); serde_field_string!( default_image_quality, deserialize_image_quality, - "balanced".to_owned() + UserDefaultConfig::load().get("image_quality") ); + fn default_custom_image_quality() -> Vec { + let f: f64 = UserDefaultConfig::load() + .get("custom_image_quality") + .parse() + .unwrap_or(50.0); + vec![f as _] + } + + fn deserialize_custom_image_quality<'de, D>(deserializer: D) -> Result, D::Error> + where + D: de::Deserializer<'de>, + { + let v: Vec = de::Deserialize::deserialize(deserializer)?; + if v.len() == 1 && v[0] >= 10 && v[0] <= 100 { + Ok(v) + } else { + Ok(Self::default_custom_image_quality()) + } + } + fn deserialize_options<'de, D>(deserializer: D) -> Result, D::Error> where D: de::Deserializer<'de>, { let mut mp: HashMap = de::Deserialize::deserialize(deserializer)?; - if !mp.contains_key("codec-preference") { - mp.insert("codec-preference".to_owned(), "auto".to_owned()); + let mut key = "codec-preference"; + if !mp.contains_key(key) { + mp.insert(key.to_owned(), UserDefaultConfig::load().get(key)); + } + key = "custom-fps"; + if !mp.contains_key(key) { + mp.insert(key.to_owned(), UserDefaultConfig::load().get(key)); } Ok(mp) } @@ -1192,6 +1220,73 @@ impl HwCodecConfig { } } +#[derive(Debug, Default, Serialize, Deserialize, Clone)] +pub struct UserDefaultConfig { + #[serde(default)] + options: HashMap, +} + +impl UserDefaultConfig { + pub fn load() -> UserDefaultConfig { + Config::load_::("_default") + } + + #[inline] + fn store(&self) { + Config::store_(self, "_default"); + } + + pub fn get(&self, key: &str) -> String { + match key { + "view_style" => self.get_string(key, "original", vec!["adaptive"]), + "scroll_style" => self.get_string(key, "scrollauto", vec!["scrollbar"]), + "image_quality" => self.get_string(key, "balanced", vec!["best", "low", "custom"]), + "codec-preference" => self.get_string(key, "auto", vec!["vp9", "h264", "h265"]), + "custom_image_quality" => self.get_double_string(key, 50.0, 10.0, 100.0), + "custom-fps" => self.get_double_string(key, 30.0, 10.0, 120.0), + _ => self + .options + .get(key) + .map(|v| v.to_string()) + .unwrap_or_default(), + } + } + + pub fn set(&mut self, key: String, value: String) { + self.options.insert(key, value); + self.store(); + } + + #[inline] + fn get_string(&self, key: &str, default: &str, others: Vec<&str>) -> String { + match self.options.get(key) { + Some(option) => { + if others.contains(&option.as_str()) { + option.to_owned() + } else { + default.to_owned() + } + } + None => default.to_owned(), + } + } + + #[inline] + fn get_double_string(&self, key: &str, default: f64, min: f64, max: f64) -> String { + match self.options.get(key) { + Some(option) => { + let v: f64 = option.parse().unwrap_or(default); + if v >= min && v <= max { + v.to_string() + } else { + default.to_string() + } + } + None => default.to_string(), + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index c2ae2b6b0..d40c66d19 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -791,6 +791,14 @@ pub fn main_default_video_save_directory() -> String { default_video_save_directory() } +pub fn main_set_user_default_option(key: String, value: String) { + set_user_default_option(key, value); +} + +pub fn main_get_user_default_option(key: String) -> SyncReturn { + SyncReturn(get_user_default_option(key)) +} + pub fn session_add_port_forward( id: String, local_port: i32, diff --git a/src/lang/ca.rs b/src/lang/ca.rs index cd8fba24d..ac3dba290 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -41,10 +41,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "Sobre"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Mute", "Silenciar"), ("Build Date", ""), ("Version", ""), ("Home", ""), - ("Mute", "Silenciar"), ("Audio Input", "Entrada d'àudio"), ("Enhancements", "Millores"), ("Hardware Codec", "Còdec de hardware"), @@ -436,5 +436,13 @@ 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", ""), + ("Display", ""), + ("Default View Style", ""), + ("Default Scroll Style", ""), + ("Default Image Quality", ""), + ("Default Codec", ""), + ("Bitrate", ""), + ("FPS", ""), + ("Auto", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 41fa7fc26..5f03ba759 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -436,5 +436,13 @@ 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", "正常关闭"), + ("Display", "显示"), + ("Default View Style", "默认显示方式"), + ("Default Scroll Style", "默认滚动方式"), + ("Default Image Quality", "默认图像质量"), + ("Default Codec", "默认编解码"), + ("Bitrate", "波特率"), + ("FPS", "帧率"), + ("Auto", "自动"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 5e59a86f1..43f3b423a 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -436,5 +436,13 @@ 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", ""), + ("Display", ""), + ("Default View Style", ""), + ("Default Scroll Style", ""), + ("Default Image Quality", ""), + ("Default Codec", ""), + ("Bitrate", ""), + ("FPS", ""), + ("Auto", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 8eddaf0b9..5f9e49265 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -436,5 +436,13 @@ 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", ""), + ("Display", ""), + ("Default View Style", ""), + ("Default Scroll Style", ""), + ("Default Image Quality", ""), + ("Default Codec", ""), + ("Bitrate", ""), + ("FPS", ""), + ("Auto", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 3418ea9f5..a683ae44d 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -436,5 +436,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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", ""), + ("Default View Style", ""), + ("Default Scroll Style", ""), + ("Default Image Quality", ""), + ("Default Codec", ""), + ("Bitrate", ""), + ("FPS", ""), + ("Auto", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index b034c0394..7f92a9b19 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -41,10 +41,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "Pri"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Mute", "Muta"), ("Build Date", ""), ("Version", ""), ("Home", ""), - ("Mute", "Muta"), ("Audio Input", "Aŭdia enigo"), ("Enhancements", ""), ("Hardware Codec", ""), @@ -436,5 +436,13 @@ 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", ""), + ("Display", ""), + ("Default View Style", ""), + ("Default Scroll Style", ""), + ("Default Image Quality", ""), + ("Default Codec", ""), + ("Bitrate", ""), + ("FPS", ""), + ("Auto", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 3848d1925..505149759 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -41,10 +41,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "Acerca de"), ("Slogan_tip", "Hecho con corazón en este mundo caótico!"), ("Privacy Statement", "Declaración de privacidad"), + ("Mute", "Silenciar"), ("Build Date", ""), ("Version", ""), ("Home", ""), - ("Mute", "Silenciar"), ("Audio Input", "Entrada de audio"), ("Enhancements", "Mejoras"), ("Hardware Codec", "Códec de hardware"), @@ -436,5 +436,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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", ""), + ("Default View Style", ""), + ("Default Scroll Style", ""), + ("Default Image Quality", ""), + ("Default Codec", ""), + ("Bitrate", ""), + ("FPS", ""), + ("Auto", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 316885082..7e1264939 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -41,10 +41,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "درباره"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Mute", "بستن صدا"), ("Build Date", ""), ("Version", ""), ("Home", ""), - ("Mute", "بستن صدا"), ("Audio Input", "ورودی صدا"), ("Enhancements", "بهبودها"), ("Hardware Codec", "کدک سخت افزاری"), @@ -436,5 +436,13 @@ 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", "طبق انتظار بسته شد"), + ("Display", ""), + ("Default View Style", ""), + ("Default Scroll Style", ""), + ("Default Image Quality", ""), + ("Default Codec", ""), + ("Bitrate", ""), + ("FPS", ""), + ("Auto", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 097091e75..9b50c8db7 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -41,10 +41,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "À propos de"), ("Slogan_tip", "Fait avec cœur dans ce monde chaotique!"), ("Privacy Statement", "Déclaration de confidentialité"), + ("Mute", "Muet"), ("Build Date", "Date de compilation"), ("Version", "Version"), ("Home", "Accueil"), - ("Mute", "Muet"), ("Audio Input", "Entrée audio"), ("Enhancements", "Améliorations"), ("Hardware Codec", "Transcodage matériel"), @@ -436,5 +436,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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", ""), + ("Display", ""), + ("Default View Style", ""), + ("Default Scroll Style", ""), + ("Default Image Quality", ""), + ("Default Codec", ""), + ("Bitrate", ""), + ("FPS", ""), + ("Auto", ""), ].iter().cloned().collect(); } diff --git a/src/lang/gr.rs b/src/lang/gr.rs index 53f9dca08..82e90a117 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -41,10 +41,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "Πληροφορίες"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Mute", "Σίγαση"), ("Build Date", ""), ("Version", ""), ("Home", ""), - ("Mute", "Σίγαση"), ("Audio Input", "Είσοδος ήχου"), ("Enhancements", "Βελτιώσεις"), ("Hardware Codec", "Κωδικοποιητής υλικού"), @@ -436,5 +436,13 @@ 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", ""), + ("Display", ""), + ("Default View Style", ""), + ("Default Scroll Style", ""), + ("Default Image Quality", ""), + ("Default Codec", ""), + ("Bitrate", ""), + ("FPS", ""), + ("Auto", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index f86e83012..f1b231d3c 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -436,5 +436,13 @@ 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", ""), + ("Display", ""), + ("Default View Style", ""), + ("Default Scroll Style", ""), + ("Default Image Quality", ""), + ("Default Codec", ""), + ("Bitrate", ""), + ("FPS", ""), + ("Auto", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 6ae39f108..e7b3c2cce 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -41,10 +41,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "Tentang"), ("Slogan_tip", ""), ("Privacy Statement", "Pernyataan Privasi"), + ("Mute", "Bisukan"), ("Build Date", ""), ("Version", ""), ("Home", ""), - ("Mute", "Bisukan"), ("Audio Input", "Masukkan Audio"), ("Enhancements", "Peningkatan"), ("Hardware Codec", "Codec Perangkat Keras"), @@ -436,5 +436,13 @@ 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", ""), + ("Display", ""), + ("Default View Style", ""), + ("Default Scroll Style", ""), + ("Default Image Quality", ""), + ("Default Codec", ""), + ("Bitrate", ""), + ("FPS", ""), + ("Auto", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 0ec6c52b9..ec7e07312 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -41,10 +41,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "Informazioni"), ("Slogan_tip", "Fatta con il cuore in questo mondo caotico!"), ("Privacy Statement", "Informativa sulla privacy"), + ("Mute", "Silenzia"), ("Build Date", "Data della build"), ("Version", "Versione"), ("Home", "Home"), - ("Mute", "Silenzia"), ("Audio Input", "Input audio"), ("Enhancements", "Miglioramenti"), ("Hardware Codec", "Codifica Hardware"), @@ -436,5 +436,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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", ""), + ("Default View Style", ""), + ("Default Scroll Style", ""), + ("Default Image Quality", ""), + ("Default Codec", ""), + ("Bitrate", ""), + ("FPS", ""), + ("Auto", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 8e8a5ed95..a65f3d56a 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -41,10 +41,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "情報"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Mute", "ミュート"), ("Build Date", ""), ("Version", ""), ("Home", ""), - ("Mute", "ミュート"), ("Audio Input", "音声入力デバイス"), ("Enhancements", "追加機能"), ("Hardware Codec", "ハードウェア コーデック"), @@ -436,5 +436,13 @@ 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", ""), + ("Display", ""), + ("Default View Style", ""), + ("Default Scroll Style", ""), + ("Default Image Quality", ""), + ("Default Codec", ""), + ("Bitrate", ""), + ("FPS", ""), + ("Auto", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 7b56202a0..8f7167df4 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -41,10 +41,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "정보"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Mute", "음소거"), ("Build Date", ""), ("Version", ""), ("Home", ""), - ("Mute", "음소거"), ("Audio Input", "오디오 입력"), ("Enhancements", ""), ("Hardware Codec", "하드웨어 코덱"), @@ -436,5 +436,13 @@ 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", ""), + ("Display", ""), + ("Default View Style", ""), + ("Default Scroll Style", ""), + ("Default Image Quality", ""), + ("Default Codec", ""), + ("Bitrate", ""), + ("FPS", ""), + ("Auto", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index dcf62ff10..1651beb92 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -41,10 +41,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "Туралы"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Mute", "Дыбыссыздандыру"), ("Build Date", ""), ("Version", ""), ("Home", ""), - ("Mute", "Дыбыссыздандыру"), ("Audio Input", "Аудио Еңгізу"), ("Enhancements", "Жақсартулар"), ("Hardware Codec", "Hardware Codec"), @@ -436,5 +436,13 @@ 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", ""), + ("Display", ""), + ("Default View Style", ""), + ("Default Scroll Style", ""), + ("Default Image Quality", ""), + ("Default Codec", ""), + ("Bitrate", ""), + ("FPS", ""), + ("Auto", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 085e74d3a..0b0c454c0 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -41,10 +41,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "O aplikacji"), ("Slogan_tip", "Tworzone z miłością w tym pełnym chaosu świecie!"), ("Privacy Statement", "Oświadczenie o ochronie prywatności"), + ("Mute", "Wycisz"), ("Build Date", "Zbudowano"), ("Version", "Wersja"), ("Home", "Pulpit"), - ("Mute", "Wycisz"), ("Audio Input", "Wejście audio"), ("Enhancements", "Ulepszenia"), ("Hardware Codec", "Kodek sprzętowy"), @@ -436,5 +436,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Switch Sides", "Zmień 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", ""), + ("Default View Style", ""), + ("Default Scroll Style", ""), + ("Default Image Quality", ""), + ("Default Codec", ""), + ("Bitrate", ""), + ("FPS", ""), + ("Auto", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index aea9acd2a..d327011fe 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -41,10 +41,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "Sobre"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Mute", "Silenciar"), ("Build Date", ""), ("Version", ""), ("Home", ""), - ("Mute", "Silenciar"), ("Audio Input", "Entrada de Áudio"), ("Enhancements", "Melhorias"), ("Hardware Codec", ""), @@ -436,5 +436,13 @@ 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", ""), + ("Display", ""), + ("Default View Style", ""), + ("Default Scroll Style", ""), + ("Default Image Quality", ""), + ("Default Codec", ""), + ("Bitrate", ""), + ("FPS", ""), + ("Auto", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 28683c8d5..a442b5858 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -41,10 +41,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "Sobre"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Mute", "Desativar som"), ("Build Date", ""), ("Version", ""), ("Home", ""), - ("Mute", "Desativar som"), ("Audio Input", "Entrada de Áudio"), ("Enhancements", "Melhorias"), ("Hardware Codec", "Codec de hardware"), @@ -436,5 +436,13 @@ 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", ""), + ("Display", ""), + ("Default View Style", ""), + ("Default Scroll Style", ""), + ("Default Image Quality", ""), + ("Default Codec", ""), + ("Bitrate", ""), + ("FPS", ""), + ("Auto", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 3009e9b06..b90a21cea 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -42,6 +42,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Slogan_tip", ""), ("Privacy Statement", ""), ("Mute", "Fără sunet"), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Audio Input", "Intrare audio"), ("Enhancements", "Îmbunătățiri"), ("Hardware Codec", "Codec hardware"), @@ -433,5 +436,13 @@ 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", ""), + ("Display", ""), + ("Default View Style", ""), + ("Default Scroll Style", ""), + ("Default Image Quality", ""), + ("Default Codec", ""), + ("Bitrate", ""), + ("FPS", ""), + ("Auto", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 22f938ec5..f92815137 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -41,10 +41,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "О программе"), ("Slogan_tip", "Сделано с душой в этом безумном мире!"), ("Privacy Statement", "Заявление о конфиденциальности"), + ("Mute", "Отключить звук"), ("Build Date", "Дата сборки"), ("Version", "Версия"), ("Home", "Главная"), - ("Mute", "Отключить звук"), ("Audio Input", "Аудиовход"), ("Enhancements", "Улучшения"), ("Hardware Codec", "Аппаратный кодек"), @@ -436,5 +436,13 @@ 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", "Закрыто по ожиданию"), + ("Display", ""), + ("Default View Style", ""), + ("Default Scroll Style", ""), + ("Default Image Quality", ""), + ("Default Codec", ""), + ("Bitrate", ""), + ("FPS", ""), + ("Auto", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 2062b57a5..a6b5b7b4f 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -41,10 +41,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "O RustDesk"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Mute", "Stíšiť"), ("Build Date", ""), ("Version", ""), ("Home", ""), - ("Mute", "Stíšiť"), ("Audio Input", "Zvukový vstup"), ("Enhancements", ""), ("Hardware Codec", ""), @@ -436,5 +436,13 @@ 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", ""), + ("Display", ""), + ("Default View Style", ""), + ("Default Scroll Style", ""), + ("Default Image Quality", ""), + ("Default Codec", ""), + ("Bitrate", ""), + ("FPS", ""), + ("Auto", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 1ff78818c..1cabf9bbc 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -41,10 +41,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "O programu"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Mute", "Izklopi zvok"), ("Build Date", ""), ("Version", ""), ("Home", ""), - ("Mute", "Izklopi zvok"), ("Audio Input", "Avdio vhod"), ("Enhancements", "Izboljšave"), ("Hardware Codec", "Strojni kodek"), @@ -436,5 +436,13 @@ 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", ""), + ("Display", ""), + ("Default View Style", ""), + ("Default Scroll Style", ""), + ("Default Image Quality", ""), + ("Default Codec", ""), + ("Bitrate", ""), + ("FPS", ""), + ("Auto", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 225652056..6bfdc8230 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -41,10 +41,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "Rreth"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Mute", "Pa zë"), ("Build Date", ""), ("Version", ""), ("Home", ""), - ("Mute", "Pa zë"), ("Audio Input", "Inputi zërit"), ("Enhancements", "Përmirësimet"), ("Hardware Codec", "Kodeku Harduerik"), @@ -436,5 +436,13 @@ 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", ""), + ("Display", ""), + ("Default View Style", ""), + ("Default Scroll Style", ""), + ("Default Image Quality", ""), + ("Default Codec", ""), + ("Bitrate", ""), + ("FPS", ""), + ("Auto", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 57c528fdb..cfdb3712b 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -41,10 +41,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "O programu"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Mute", "Utišaj"), ("Build Date", ""), ("Version", ""), ("Home", ""), - ("Mute", "Utišaj"), ("Audio Input", "Audio ulaz"), ("Enhancements", "Proširenja"), ("Hardware Codec", "Hardverski kodek"), @@ -436,5 +436,13 @@ 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", ""), + ("Display", ""), + ("Default View Style", ""), + ("Default Scroll Style", ""), + ("Default Image Quality", ""), + ("Default Codec", ""), + ("Bitrate", ""), + ("FPS", ""), + ("Auto", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index f98d7f005..5d25b6a14 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -41,10 +41,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "Om"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Mute", "Tyst"), ("Build Date", ""), ("Version", ""), ("Home", ""), - ("Mute", "Tyst"), ("Audio Input", "Ljud input"), ("Enhancements", "Förbättringar"), ("Hardware Codec", "Hårdvarucodec"), @@ -436,5 +436,13 @@ 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", ""), + ("Display", ""), + ("Default View Style", ""), + ("Default Scroll Style", ""), + ("Default Image Quality", ""), + ("Default Codec", ""), + ("Bitrate", ""), + ("FPS", ""), + ("Auto", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 358444986..0e77eca0d 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -41,10 +41,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", ""), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Mute", ""), ("Build Date", ""), ("Version", ""), ("Home", ""), - ("Mute", ""), ("Audio Input", ""), ("Enhancements", ""), ("Hardware Codec", ""), @@ -436,5 +436,13 @@ 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", ""), + ("Display", ""), + ("Default View Style", ""), + ("Default Scroll Style", ""), + ("Default Image Quality", ""), + ("Default Codec", ""), + ("Bitrate", ""), + ("FPS", ""), + ("Auto", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index d35cbdfe7..da4b7fba5 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -41,10 +41,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "เกี่ยวกับ"), ("Slogan_tip", "ทำด้วยใจ ในโลกใบนี้ที่ยุ่งเหยิง!"), ("Privacy Statement", "คำแถลงเกี่ยวกับความเป็นส่วนตัว"), + ("Mute", "ปิดเสียง"), ("Build Date", ""), ("Version", ""), ("Home", ""), - ("Mute", "ปิดเสียง"), ("Audio Input", "ออดิโออินพุท"), ("Enhancements", "การปรับปรุง"), ("Hardware Codec", "ฮาร์ดแวร์ codec"), @@ -436,5 +436,13 @@ 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", ""), + ("Display", ""), + ("Default View Style", ""), + ("Default Scroll Style", ""), + ("Default Image Quality", ""), + ("Default Codec", ""), + ("Bitrate", ""), + ("FPS", ""), + ("Auto", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 1e2068fb6..717072bfb 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -41,10 +41,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "Hakkında"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Mute", "Sustur"), ("Build Date", ""), ("Version", ""), ("Home", ""), - ("Mute", "Sustur"), ("Audio Input", "Ses Girişi"), ("Enhancements", "Geliştirmeler"), ("Hardware Codec", "Donanımsal Codec"), @@ -436,5 +436,13 @@ 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", ""), + ("Display", ""), + ("Default View Style", ""), + ("Default Scroll Style", ""), + ("Default Image Quality", ""), + ("Default Codec", ""), + ("Bitrate", ""), + ("FPS", ""), + ("Auto", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 370c9fbed..0076a7a81 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -41,10 +41,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "關於"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Mute", "靜音"), ("Build Date", ""), ("Version", ""), ("Home", ""), - ("Mute", "靜音"), ("Audio Input", "音訊輸入"), ("Enhancements", "增強功能"), ("Hardware Codec", "硬件編解碼"), @@ -436,5 +436,13 @@ 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", "正常關閉"), + ("Display", "顯示"), + ("Default View Style", "默認顯示方式"), + ("Default Scroll Style", "默認滾動方式"), + ("Default Image Quality", "默認圖像質量"), + ("Default Codec", "默認編解碼"), + ("Bitrate", "波特率"), + ("FPS", "幀率"), + ("Auto", "自動"), ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index bdba09b5b..980febc97 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -41,10 +41,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "Про RustDesk"), ("Slogan_tip", "Створено з душею в цьому хаотичному світі!"), ("Privacy Statement", "Декларація про конфіденційність"), + ("Mute", "Вимкнути звук"), ("Build Date", ""), ("Version", ""), ("Home", ""), - ("Mute", "Вимкнути звук"), ("Audio Input", "Аудіовхід"), ("Enhancements", "Покращення"), ("Hardware Codec", "Апаратний кодек"), @@ -436,5 +436,13 @@ 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", ""), + ("Display", ""), + ("Default View Style", ""), + ("Default Scroll Style", ""), + ("Default Image Quality", ""), + ("Default Codec", ""), + ("Bitrate", ""), + ("FPS", ""), + ("Auto", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 840739765..8785acfc3 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -41,10 +41,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("About", "About"), ("Slogan_tip", ""), ("Privacy Statement", ""), + ("Mute", "Tắt tiếng"), ("Build Date", ""), ("Version", ""), ("Home", ""), - ("Mute", "Tắt tiếng"), ("Audio Input", "Đầu vào âm thanh"), ("Enhancements", "Các tiện itchs"), ("Hardware Codec", "Codec phần cứng"), @@ -436,5 +436,13 @@ 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", ""), + ("Display", ""), + ("Default View Style", ""), + ("Default Scroll Style", ""), + ("Default Image Quality", ""), + ("Default Codec", ""), + ("Bitrate", ""), + ("FPS", ""), + ("Auto", ""), ].iter().cloned().collect(); } diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 4e0fd7744..d357c9cef 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -917,6 +917,18 @@ pub fn account_auth_result() -> String { serde_json::to_string(&account::OidcSession::get_result()).unwrap_or_default() } +#[cfg(feature = "flutter")] +pub fn set_user_default_option(key: String, value: String) { + use hbb_common::config::UserDefaultConfig; + UserDefaultConfig::load().set(key, value); +} + +#[cfg(feature = "flutter")] +pub fn get_user_default_option(key: String) -> String { + use hbb_common::config::UserDefaultConfig; + UserDefaultConfig::load().get(&key) +} + // notice: avoiding create ipc connection repeatedly, // because windows named pipe has serious memory leak issue. #[tokio::main(flavor = "current_thread")] From 92145eeb717f1eba07c7298c4bca13e37aad6857 Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 2 Feb 2023 09:39:14 +0800 Subject: [PATCH 1675/2015] other bool default display settings Signed-off-by: 21pages --- .../desktop/pages/desktop_setting_page.dart | 34 ++++++++ libs/hbb_common/src/config.rs | 80 +++++++++++++++---- src/client.rs | 40 +++++----- src/client/io_loop.rs | 14 ++-- 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/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 + 36 files changed, 159 insertions(+), 41 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index e4a7e1a25..4b6cf2a62 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -1072,6 +1072,7 @@ class _DisplayState extends State<_Display> { scrollStyle(context), imageQuality(context), codec(context), + other(context), ]).marginOnly(bottom: _kListViewBottomMargin)); } @@ -1256,6 +1257,39 @@ class _DisplayState extends State<_Display> { onChanged: onChanged), ]); } + + Widget otherRow(String label, String key) { + final value = bind.mainGetUserDefaultOption(key: key) == 'Y'; + onChanged(bool b) async { + await bind.mainSetUserDefaultOption(key: key, value: b ? 'Y' : ''); + setState(() {}); + } + + return GestureDetector( + child: Row( + children: [ + Checkbox(value: value, onChanged: (_) => onChanged(!value)) + .marginOnly(right: 5), + Expanded( + child: Text(translate(label)), + ) + ], + ).marginOnly(left: _kCheckBoxLeftMargin), + onTap: () => onChanged(!value)); + } + + Widget other(BuildContext context) { + return _Card(title: 'Other Default Options', children: [ + otherRow('Show remote cursor', 'show_remote_cursor'), + otherRow('Zoom cursor', 'zoom-cursor'), + otherRow('Show quality monitor', 'show_quality_monitor'), + otherRow('Mute', 'disable_audio'), + otherRow('Allow file copy and paste', 'enable_file_transfer'), + otherRow('Disable clipboard', 'disable_clipboard'), + otherRow('Lock after session end', 'lock_after_session_end'), + otherRow('Privacy mode', 'privacy_mode'), + ]); + } } class _Account extends StatefulWidget { diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index ce4be6119..71dd9a5c6 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -115,6 +115,26 @@ macro_rules! serde_field_string { }; } +macro_rules! serde_field_bool { + ($struct_name: ident, $field_name: literal, $func: ident) => { + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct $struct_name { + #[serde(rename = $field_name)] + pub v: bool, + } + impl Default for $struct_name { + fn default() -> Self { + Self { v: Self::$func() } + } + } + impl $struct_name { + pub fn $func() -> bool { + UserDefaultConfig::load().get($field_name) == "Y" + } + } + }; +} + #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum NetworkType { Direct, @@ -197,24 +217,24 @@ pub struct PeerConfig { deserialize_with = "PeerConfig::deserialize_custom_image_quality" )] pub custom_image_quality: Vec, - #[serde(default)] - pub show_remote_cursor: bool, - #[serde(default)] - pub lock_after_session_end: bool, - #[serde(default)] - pub privacy_mode: bool, + #[serde(flatten)] + pub show_remote_cursor: ShowRemoteCursor, + #[serde(flatten)] + pub lock_after_session_end: LockAfterSessionEnd, + #[serde(flatten)] + pub privacy_mode: PrivacyMode, #[serde(default)] pub port_forwards: Vec<(i32, String, i32)>, #[serde(default)] pub direct_failures: i32, - #[serde(default)] - pub disable_audio: bool, - #[serde(default)] - pub disable_clipboard: bool, - #[serde(default)] - pub enable_file_transfer: bool, - #[serde(default)] - pub show_quality_monitor: bool, + #[serde(flatten)] + pub disable_audio: DisableAudio, + #[serde(flatten)] + pub disable_clipboard: DisableClipboard, + #[serde(flatten)] + pub enable_file_transfer: EnableFileTransfer, + #[serde(flatten)] + pub show_quality_monitor: ShowQualityMonitor, #[serde(default)] pub keyboard_mode: String, @@ -1010,10 +1030,42 @@ impl PeerConfig { if !mp.contains_key(key) { mp.insert(key.to_owned(), UserDefaultConfig::load().get(key)); } + key = "zoom-cursor"; + if !mp.contains_key(key) { + mp.insert(key.to_owned(), UserDefaultConfig::load().get(key)); + } Ok(mp) } } +serde_field_bool!( + ShowRemoteCursor, + "show_remote_cursor", + default_show_remote_cursor +); +serde_field_bool!( + ShowQualityMonitor, + "show_quality_monitor", + default_show_quality_monitor +); +serde_field_bool!(DisableAudio, "disable_audio", default_disable_audio); +serde_field_bool!( + EnableFileTransfer, + "enable_file_transfer", + default_enable_file_transfer +); +serde_field_bool!( + DisableClipboard, + "disable_clipboard", + default_disable_clipboard +); +serde_field_bool!( + LockAfterSessionEnd, + "lock_after_session_end", + default_lock_after_session_end +); +serde_field_bool!(PrivacyMode, "privacy_mode", default_privacy_mode); + #[derive(Debug, Default, Serialize, Deserialize, Clone)] pub struct LocalConfig { #[serde(default)] diff --git a/src/client.rs b/src/client.rs index a6df6dbec..fb42ce840 100644 --- a/src/client.rs +++ b/src/client.rs @@ -956,7 +956,7 @@ impl LoginConfigHandler { /// Check if the client should auto login. /// Return password if the client should auto login, otherwise return empty string. pub fn should_auto_login(&self) -> String { - let l = self.lock_after_session_end; + let l = self.lock_after_session_end.v; let a = !self.get_option("auto-login").is_empty(); let p = self.get_option("os-password"); if !p.is_empty() && l && a { @@ -1063,32 +1063,32 @@ impl LoginConfigHandler { let mut option = OptionMessage::default(); let mut config = self.load_config(); if name == "show-remote-cursor" { - config.show_remote_cursor = !config.show_remote_cursor; - option.show_remote_cursor = (if config.show_remote_cursor { + config.show_remote_cursor.v = !config.show_remote_cursor.v; + option.show_remote_cursor = (if config.show_remote_cursor.v { BoolOption::Yes } else { BoolOption::No }) .into(); } else if name == "disable-audio" { - config.disable_audio = !config.disable_audio; - option.disable_audio = (if config.disable_audio { + config.disable_audio.v = !config.disable_audio.v; + option.disable_audio = (if config.disable_audio.v { BoolOption::Yes } else { BoolOption::No }) .into(); } else if name == "disable-clipboard" { - config.disable_clipboard = !config.disable_clipboard; - option.disable_clipboard = (if config.disable_clipboard { + config.disable_clipboard.v = !config.disable_clipboard.v; + option.disable_clipboard = (if config.disable_clipboard.v { BoolOption::Yes } else { BoolOption::No }) .into(); } else if name == "lock-after-session-end" { - config.lock_after_session_end = !config.lock_after_session_end; - option.lock_after_session_end = (if config.lock_after_session_end { + config.lock_after_session_end.v = !config.lock_after_session_end.v; + option.lock_after_session_end = (if config.lock_after_session_end.v { BoolOption::Yes } else { BoolOption::No @@ -1096,15 +1096,15 @@ impl LoginConfigHandler { .into(); } else if name == "privacy-mode" { // try toggle privacy mode - option.privacy_mode = (if config.privacy_mode { + option.privacy_mode = (if config.privacy_mode.v { BoolOption::No } else { BoolOption::Yes }) .into(); } else if name == "enable-file-transfer" { - config.enable_file_transfer = !config.enable_file_transfer; - option.enable_file_transfer = (if config.enable_file_transfer { + config.enable_file_transfer.v = !config.enable_file_transfer.v; + option.enable_file_transfer = (if config.enable_file_transfer.v { BoolOption::Yes } else { BoolOption::No @@ -1115,7 +1115,7 @@ impl LoginConfigHandler { } else if name == "unblock-input" { option.block_input = BoolOption::No.into(); } else if name == "show-quality-monitor" { - config.show_quality_monitor = !config.show_quality_monitor; + config.show_quality_monitor.v = !config.show_quality_monitor.v; } else { let v = self.options.get(&name).is_some(); if v { @@ -1252,19 +1252,19 @@ impl LoginConfigHandler { /// * `name` - The name of the toggle option. pub fn get_toggle_option(&self, name: &str) -> bool { if name == "show-remote-cursor" { - self.config.show_remote_cursor + self.config.show_remote_cursor.v } else if name == "lock-after-session-end" { - self.config.lock_after_session_end + self.config.lock_after_session_end.v } else if name == "privacy-mode" { - self.config.privacy_mode + self.config.privacy_mode.v } else if name == "enable-file-transfer" { - self.config.enable_file_transfer + self.config.enable_file_transfer.v } else if name == "disable-audio" { - self.config.disable_audio + self.config.disable_audio.v } else if name == "disable-clipboard" { - self.config.disable_clipboard + self.config.disable_clipboard.v } else if name == "show-quality-monitor" { - self.config.show_quality_monitor + self.config.show_quality_monitor.v } else { !self.get_option(name).is_empty() } diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index f4ecbded5..0178fe9e8 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -277,7 +277,7 @@ impl Remote { } if !SERVER_CLIPBOARD_ENABLED.load(Ordering::SeqCst) || !SERVER_KEYBOARD_ENABLED.load(Ordering::SeqCst) - || lc.read().unwrap().disable_clipboard + || lc.read().unwrap().disable_clipboard.v { continue; } @@ -778,7 +778,7 @@ impl Remote { || self.handler.is_port_forward() || !SERVER_CLIPBOARD_ENABLED.load(Ordering::SeqCst) || !SERVER_KEYBOARD_ENABLED.load(Ordering::SeqCst) - || self.handler.lc.read().unwrap().disable_clipboard) + || self.handler.lc.read().unwrap().disable_clipboard.v) { let txt = self.old_clipboard.lock().unwrap().clone(); if !txt.is_empty() { @@ -808,7 +808,7 @@ impl Remote { self.handler.set_cursor_position(cp); } Some(message::Union::Clipboard(cb)) => { - if !self.handler.lc.read().unwrap().disable_clipboard { + if !self.handler.lc.read().unwrap().disable_clipboard.v { #[cfg(not(any(target_os = "android", target_os = "ios")))] update_clipboard(cb, Some(&self.old_clipboard)); #[cfg(any(target_os = "android", target_os = "ios"))] @@ -1121,7 +1121,7 @@ impl Remote { self.handler.handle_test_delay(t, peer).await; } Some(message::Union::AudioFrame(frame)) => { - if !self.handler.lc.read().unwrap().disable_audio { + if !self.handler.lc.read().unwrap().disable_audio.v { self.audio_sender.send(MediaData::AudioFrame(frame)).ok(); } } @@ -1204,7 +1204,7 @@ impl Remote { #[inline(always)] fn update_privacy_mode(&mut self, on: bool) { let mut config = self.handler.load_config(); - config.privacy_mode = on; + config.privacy_mode.v = on; self.handler.save_config(config); self.handler.update_privacy_mode(); @@ -1278,14 +1278,14 @@ impl Remote { #[cfg(windows)] { let enabled = SERVER_FILE_TRANSFER_ENABLED.load(Ordering::SeqCst) - && self.handler.lc.read().unwrap().enable_file_transfer; + && self.handler.lc.read().unwrap().enable_file_transfer.v; ContextSend::enable(enabled); } } #[cfg(windows)] fn handle_cliprdr_msg(&self, clip: hbb_common::message_proto::Cliprdr) { - if !self.handler.lc.read().unwrap().disable_clipboard { + if !self.handler.lc.read().unwrap().disable_clipboard.v { #[cfg(feature = "flutter")] if let Some(hbb_common::message_proto::cliprdr::Union::FormatList(_)) = &clip.union { if self.client_conn_id diff --git a/src/lang/ca.rs b/src/lang/ca.rs index ac3dba290..f2210f971 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -444,5 +444,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Bitrate", ""), ("FPS", ""), ("Auto", ""), + ("Other Default Options", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 5f03ba759..00d62946f 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -444,5 +444,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Bitrate", "波特率"), ("FPS", "帧率"), ("Auto", "自动"), + ("Other Default Options", "其它默认选项"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 43f3b423a..453ecefb3 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -444,5 +444,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Bitrate", ""), ("FPS", ""), ("Auto", ""), + ("Other Default Options", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 5f9e49265..dcaeb3eaa 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -444,5 +444,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Bitrate", ""), ("FPS", ""), ("Auto", ""), + ("Other Default Options", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index a683ae44d..5b68c0e7a 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -444,5 +444,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Bitrate", ""), ("FPS", ""), ("Auto", ""), + ("Other Default Options", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 7f92a9b19..0c7f13d7e 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -444,5 +444,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Bitrate", ""), ("FPS", ""), ("Auto", ""), + ("Other Default Options", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 505149759..6f866845c 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -444,5 +444,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Bitrate", ""), ("FPS", ""), ("Auto", ""), + ("Other Default Options", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 7e1264939..72cde49f9 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -444,5 +444,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Bitrate", ""), ("FPS", ""), ("Auto", ""), + ("Other Default Options", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 9b50c8db7..19b932d2f 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -444,5 +444,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Bitrate", ""), ("FPS", ""), ("Auto", ""), + ("Other Default Options", ""), ].iter().cloned().collect(); } diff --git a/src/lang/gr.rs b/src/lang/gr.rs index 82e90a117..bc25ab6c6 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -444,5 +444,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Bitrate", ""), ("FPS", ""), ("Auto", ""), + ("Other Default Options", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index f1b231d3c..49ce8f140 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -444,5 +444,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Bitrate", ""), ("FPS", ""), ("Auto", ""), + ("Other Default Options", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index e7b3c2cce..0fa6e0293 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -444,5 +444,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Bitrate", ""), ("FPS", ""), ("Auto", ""), + ("Other Default Options", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index ec7e07312..6edd4a461 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -444,5 +444,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Bitrate", ""), ("FPS", ""), ("Auto", ""), + ("Other Default Options", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index a65f3d56a..35e20d7fd 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -444,5 +444,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Bitrate", ""), ("FPS", ""), ("Auto", ""), + ("Other Default Options", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 8f7167df4..d03b07992 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -444,5 +444,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Bitrate", ""), ("FPS", ""), ("Auto", ""), + ("Other Default Options", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 1651beb92..2006c67d1 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -444,5 +444,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Bitrate", ""), ("FPS", ""), ("Auto", ""), + ("Other Default Options", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 0b0c454c0..daf4a7846 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -444,5 +444,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Bitrate", ""), ("FPS", ""), ("Auto", ""), + ("Other Default Options", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index d327011fe..64e5e9315 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -444,5 +444,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Bitrate", ""), ("FPS", ""), ("Auto", ""), + ("Other Default Options", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index a442b5858..0f64ae67f 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -444,5 +444,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Bitrate", ""), ("FPS", ""), ("Auto", ""), + ("Other Default Options", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index b90a21cea..7e209dff8 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -444,5 +444,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Bitrate", ""), ("FPS", ""), ("Auto", ""), + ("Other Default Options", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index f92815137..7ec6c1554 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -444,5 +444,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Bitrate", ""), ("FPS", ""), ("Auto", ""), + ("Other Default Options", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index a6b5b7b4f..a703c0799 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -444,5 +444,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Bitrate", ""), ("FPS", ""), ("Auto", ""), + ("Other Default Options", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 1cabf9bbc..16c948ceb 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -444,5 +444,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Bitrate", ""), ("FPS", ""), ("Auto", ""), + ("Other Default Options", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 6bfdc8230..285a51732 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -444,5 +444,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Bitrate", ""), ("FPS", ""), ("Auto", ""), + ("Other Default Options", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index cfdb3712b..dd943e0e6 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -444,5 +444,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Bitrate", ""), ("FPS", ""), ("Auto", ""), + ("Other Default Options", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 5d25b6a14..3050ff635 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -444,5 +444,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Bitrate", ""), ("FPS", ""), ("Auto", ""), + ("Other Default Options", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 0e77eca0d..7572da9de 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -444,5 +444,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Bitrate", ""), ("FPS", ""), ("Auto", ""), + ("Other Default Options", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index da4b7fba5..535e4e772 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -444,5 +444,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Bitrate", ""), ("FPS", ""), ("Auto", ""), + ("Other Default Options", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 717072bfb..80b384c6c 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -444,5 +444,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Bitrate", ""), ("FPS", ""), ("Auto", ""), + ("Other Default Options", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 0076a7a81..f5d9539d8 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -444,5 +444,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Bitrate", "波特率"), ("FPS", "幀率"), ("Auto", "自動"), + ("Other Default Options", "其它默認選項"), ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 980febc97..37a7d6bcd 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -444,5 +444,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Bitrate", ""), ("FPS", ""), ("Auto", ""), + ("Other Default Options", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 8785acfc3..d78f5aa7b 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -444,5 +444,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Bitrate", ""), ("FPS", ""), ("Auto", ""), + ("Other Default Options", ""), ].iter().cloned().collect(); } From 6119e040067530a32b3dc70bac6b5eb9ae4941f6 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Thu, 2 Feb 2023 13:57:20 +0800 Subject: [PATCH 1676/2015] fix: synchronize macOS window theme on flutter theme changed. --- flutter/lib/common.dart | 20 +++++++++++ .../lib/desktop/widgets/refresh_wrapper.dart | 4 +++ flutter/lib/main.dart | 4 +++ flutter/lib/utils/platform_channel.dart | 34 +++++++++++++++++++ flutter/macos/Runner/MainFlutterWindow.swift | 31 +++++++++++++++++ flutter/pubspec.yaml | 2 +- 6 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 flutter/lib/utils/platform_channel.dart diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index ee7353c12..2a4441d36 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -9,6 +9,7 @@ import 'package:back_button_interceptor/back_button_interceptor.dart'; import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:ffi/ffi.dart'; import 'package:flutter/foundation.dart'; +import 'package:flutter_hbb/utils/platform_channel.dart'; import 'package:win32/win32.dart' as win32; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; @@ -235,6 +236,12 @@ class MyTheme { } bind.mainChangeTheme(dark: mode.toShortString()); } + // Synchronize the window theme of the system. + if (Platform.isMacOS) { + final isDark = mode == ThemeMode.dark; + RdPlatformChannel.instance.changeSystemWindowTheme( + isDark ? SystemWindowTheme.dark : SystemWindowTheme.light); + } } static ThemeMode currentThemeMode() { @@ -1686,3 +1693,16 @@ String getWindowName({WindowType? overrideType}) { String getWindowNameWithId(String id, {WindowType? overrideType}) { return "${DesktopTab.labelGetterAlias(id).value} - ${getWindowName(overrideType: overrideType)}"; } + +void updateSystemWindowTheme() { + // Set system window theme for macOS + final userPreference = MyTheme.getThemeModePreference(); + if (userPreference != ThemeMode.system) { + if (Platform.isMacOS) { + RdPlatformChannel.instance.changeSystemWindowTheme( + userPreference == ThemeMode.light + ? SystemWindowTheme.light + : SystemWindowTheme.dark); + } + } +} \ No newline at end of file diff --git a/flutter/lib/desktop/widgets/refresh_wrapper.dart b/flutter/lib/desktop/widgets/refresh_wrapper.dart index 60e816044..b4ea14d01 100644 --- a/flutter/lib/desktop/widgets/refresh_wrapper.dart +++ b/flutter/lib/desktop/widgets/refresh_wrapper.dart @@ -1,9 +1,11 @@ import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/main.dart'; import 'package:get/get.dart'; class RefreshWrapper extends StatefulWidget { final Widget Function(BuildContext context) builder; + const RefreshWrapper({super.key, required this.builder}); @override @@ -30,6 +32,8 @@ class RefreshWrapperState extends State { if (Get.context != null) { (context as Element).visitChildren(_rebuildElement); } + // Synchronize the window theme of the system. + updateSystemWindowTheme(); setState(() {}); } diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index ff7a72124..5b1e0c37c 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -108,6 +108,8 @@ Future initEnv(String appType) async { await initGlobalFFI(); // await Firebase.initializeApp(); _registerEventHandler(); + // Update the system theme. + updateSystemWindowTheme(); } void runMainApp(bool startService) async { @@ -327,6 +329,8 @@ class _AppState extends State { to = ThemeMode.light; } Get.changeThemeMode(to); + // Synchronize the window theme of the system. + updateSystemWindowTheme(); if (desktopType == DesktopType.main) { bind.mainChangeTheme(dark: to.toShortString()); } diff --git a/flutter/lib/utils/platform_channel.dart b/flutter/lib/utils/platform_channel.dart new file mode 100644 index 000000000..21f08f53f --- /dev/null +++ b/flutter/lib/utils/platform_channel.dart @@ -0,0 +1,34 @@ +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hbb/main.dart'; + +enum SystemWindowTheme { light, dark } + +/// The platform channel for RustDesk +class RdPlatformChannel { + RdPlatformChannel._(); + + static final RdPlatformChannel _windowUtil = RdPlatformChannel._(); + + static RdPlatformChannel get instance => _windowUtil; + + final MethodChannel _osxMethodChannel = + MethodChannel("org.rustdesk.rustdesk/macos"); + final MethodChannel _winMethodChannel = + MethodChannel("org.rustdesk.rustdesk/windows"); + final MethodChannel _linuxMethodChannel = + MethodChannel("org.rustdesk.rustdesk/linux"); + + /// Change the theme of the system window + Future changeSystemWindowTheme(SystemWindowTheme theme) { + assert(Platform.isMacOS); + if (kDebugMode) { + print( + "[Window ${kWindowId ?? 'Main'}] change system window theme to ${theme.name}"); + } + return _osxMethodChannel + .invokeMethod("setWindowTheme", {"themeName": theme.name}); + } +} diff --git a/flutter/macos/Runner/MainFlutterWindow.swift b/flutter/macos/Runner/MainFlutterWindow.swift index 108f5a5f8..cea1e94bb 100644 --- a/flutter/macos/Runner/MainFlutterWindow.swift +++ b/flutter/macos/Runner/MainFlutterWindow.swift @@ -27,12 +27,16 @@ class MainFlutterWindow: NSWindow { let windowFrame = self.frame self.contentViewController = flutterViewController self.setFrame(windowFrame, display: true) + // register self method handler + let registrar = flutterViewController.registrar(forPlugin: "RustDeskPlugin") + setMethodHandler(registrar: registrar) RegisterGeneratedPlugins(registry: flutterViewController) FlutterMultiWindowPlugin.setOnWindowCreatedCallback { controller in // Register the plugin which you want access from other isolate. // DesktopLifecyclePlugin.register(with: controller.registrar(forPlugin: "DesktopLifecyclePlugin")) + self.setMethodHandler(registrar: controller.registrar(forPlugin: "RustDeskPlugin")) DesktopDropPlugin.register(with: controller.registrar(forPlugin: "DesktopDropPlugin")) DeviceInfoPlusMacosPlugin.register(with: controller.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) FlutterCustomCursorPlugin.register(with: controller.registrar(forPlugin: "FlutterCustomCursorPlugin")) @@ -53,4 +57,31 @@ class MainFlutterWindow: NSWindow { super.order(place, relativeTo: otherWin) hiddenWindowAtLaunch() } + + /// Override window theme. + public func setWindowInterfaceMode(window: NSWindow, themeName: String) { + window.appearance = NSAppearance(named: themeName == "light" ? .aqua : .darkAqua) + } + + public func setMethodHandler(registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: "org.rustdesk.rustdesk/macos", binaryMessenger: registrar.messenger) + channel.setMethodCallHandler({ + (call, result) -> Void in + switch call.method { + case "setWindowTheme": + let arg = call.arguments as! [String: Any] + let themeName = arg["themeName"] as? String + guard let window = registrar.view?.window else { + result(nil) + return + } + self.setWindowInterfaceMode(window: window,themeName: themeName ?? "light") + result(nil) + break; + default: + result(FlutterMethodNotImplemented) + } + }) + } } + diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 3d08033bb..95449e611 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -61,7 +61,7 @@ dependencies: url: https://github.com/Kingtous/rustdesk_desktop_multi_window ref: bc8604a88e52b2b6e64d2661ae49a71450a47af8 freezed_annotation: ^2.0.3 - flutter_custom_cursor: ^0.0.2 + flutter_custom_cursor: ^0.0.4 window_size: git: url: https://github.com/google/flutter-desktop-embedding.git From 205f37cd56a715b07c2379a32171f32349b21fdf Mon Sep 17 00:00:00 2001 From: Kingtous Date: Thu, 2 Feb 2023 14:03:50 +0800 Subject: [PATCH 1677/2015] opt: shrink unnecessary theme code --- flutter/lib/common.dart | 22 +++++++++------------- flutter/lib/utils/platform_channel.dart | 2 +- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 2a4441d36..a2623ff15 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -225,22 +225,18 @@ class MyTheme { return themeModeFromString(bind.mainGetLocalOption(key: kCommConfKeyTheme)); } - static void changeDarkMode(ThemeMode mode) { + static void changeDarkMode(ThemeMode mode) async { Get.changeThemeMode(mode); if (desktopType == DesktopType.main) { if (mode == ThemeMode.system) { - bind.mainSetLocalOption(key: kCommConfKeyTheme, value: ''); + await bind.mainSetLocalOption(key: kCommConfKeyTheme, value: ''); } else { - bind.mainSetLocalOption( + await bind.mainSetLocalOption( key: kCommConfKeyTheme, value: mode.toShortString()); } - bind.mainChangeTheme(dark: mode.toShortString()); - } - // Synchronize the window theme of the system. - if (Platform.isMacOS) { - final isDark = mode == ThemeMode.dark; - RdPlatformChannel.instance.changeSystemWindowTheme( - isDark ? SystemWindowTheme.dark : SystemWindowTheme.light); + await bind.mainChangeTheme(dark: mode.toShortString()); + // Synchronize the window theme of the system. + updateSystemWindowTheme(); } } @@ -1694,12 +1690,12 @@ String getWindowNameWithId(String id, {WindowType? overrideType}) { return "${DesktopTab.labelGetterAlias(id).value} - ${getWindowName(overrideType: overrideType)}"; } -void updateSystemWindowTheme() { - // Set system window theme for macOS +Future updateSystemWindowTheme() async { + // Set system window theme for macOS. final userPreference = MyTheme.getThemeModePreference(); if (userPreference != ThemeMode.system) { if (Platform.isMacOS) { - RdPlatformChannel.instance.changeSystemWindowTheme( + await RdPlatformChannel.instance.changeSystemWindowTheme( userPreference == ThemeMode.light ? SystemWindowTheme.light : SystemWindowTheme.dark); diff --git a/flutter/lib/utils/platform_channel.dart b/flutter/lib/utils/platform_channel.dart index 21f08f53f..1a36fb7a5 100644 --- a/flutter/lib/utils/platform_channel.dart +++ b/flutter/lib/utils/platform_channel.dart @@ -6,7 +6,7 @@ import 'package:flutter_hbb/main.dart'; enum SystemWindowTheme { light, dark } -/// The platform channel for RustDesk +/// The platform channel for RustDesk. class RdPlatformChannel { RdPlatformChannel._(); From 8dba3942052b322b2772fb929821940e8437f239 Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 1 Feb 2023 20:58:21 +0800 Subject: [PATCH 1678/2015] scale system cursor image Signed-off-by: fufesou --- flutter/lib/desktop/pages/remote_page.dart | 1 - .../lib/desktop/widgets/remote_menubar.dart | 37 +++++++++---------- flutter/lib/models/model.dart | 34 ++++++++--------- 3 files changed, 32 insertions(+), 40 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 2e4668159..1687f348e 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -3,7 +3,6 @@ import 'dart:io'; import 'dart:ui' as ui; import 'package:desktop_multi_window/desktop_multi_window.dart'; -import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_custom_cursor/cursor_manager.dart' diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 517dc9750..4f16f8227 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -1133,26 +1133,23 @@ class _RemoteMenubarState extends State { }()); } - /// Show remote cursor scaling with image - if (widget.state.viewStyle.value != kRemoteViewStyleOriginal) { - displayMenu.add(() { - final opt = 'zoom-cursor'; - final state = PeerBoolOption.find(widget.id, opt); - return MenuEntrySwitch2( - switchType: SwitchType.scheckbox, - text: translate('Zoom cursor'), - getter: () { - return state; - }, - setter: (bool v) async { - state.value = v; - await bind.sessionToggleOption(id: widget.id, value: opt); - }, - padding: padding, - dismissOnClicked: true, - ); - }()); - } + displayMenu.add(() { + final opt = 'zoom-cursor'; + final state = PeerBoolOption.find(widget.id, opt); + return MenuEntrySwitch2( + switchType: SwitchType.scheckbox, + text: translate('Zoom cursor'), + getter: () { + return state; + }, + setter: (bool v) async { + state.value = v; + await bind.sessionToggleOption(id: widget.id, value: opt); + }, + padding: padding, + dismissOnClicked: true, + ); + }()); /// Show quality monitor displayMenu.add(MenuEntrySwitch( diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 1eac1be39..78e6ce6af 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -778,28 +778,24 @@ class CursorData { scale = 1.0; } else { // Update data if scale changed. - if (Platform.isWindows) { - final tgtWidth = (width * scale).toInt(); - final tgtHeight = (width * scale).toInt(); - if (tgtWidth < kMinCursorSize || tgtHeight < kMinCursorSize) { - double sw = kMinCursorSize.toDouble() / width; - double sh = kMinCursorSize.toDouble() / height; - scale = sw < sh ? sh : sw; - } + final tgtWidth = (width * scale).toInt(); + final tgtHeight = (width * scale).toInt(); + if (tgtWidth < kMinCursorSize || tgtHeight < kMinCursorSize) { + double sw = kMinCursorSize.toDouble() / width; + double sh = kMinCursorSize.toDouble() / height; + scale = sw < sh ? sh : sw; } } - if (Platform.isWindows) { - if (_doubleToInt(oldScale) != _doubleToInt(scale)) { - data = img2 - .copyResize( - image!, - width: (width * scale).toInt(), - height: (height * scale).toInt(), - interpolation: img2.Interpolation.average, - ) - .getBytes(format: img2.Format.bgra); - } + if (_doubleToInt(oldScale) != _doubleToInt(scale)) { + data = img2 + .copyResize( + image!, + width: (width * scale).toInt(), + height: (height * scale).toInt(), + interpolation: img2.Interpolation.average, + ) + .getBytes(format: img2.Format.bgra); } this.scale = scale; From 8881462f748068a6118196212c9f98aebe4e3a31 Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 1 Feb 2023 22:12:28 +0800 Subject: [PATCH 1679/2015] debug macos Signed-off-by: fufesou --- flutter/lib/models/model.dart | 37 +++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 78e6ce6af..b2df5faac 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -747,7 +747,7 @@ class CanvasModel with ChangeNotifier { class CursorData { final String peerId; final int id; - final img2.Image? image; + final img2.Image image; double scale; Uint8List? data; final double hotxOrigin; @@ -788,14 +788,27 @@ class CursorData { } if (_doubleToInt(oldScale) != _doubleToInt(scale)) { - data = img2 - .copyResize( - image!, - width: (width * scale).toInt(), - height: (height * scale).toInt(), - interpolation: img2.Interpolation.average, - ) - .getBytes(format: img2.Format.bgra); + if (Platform.isWindows) { + data = img2 + .copyResize( + image, + width: (width * scale).toInt(), + height: (height * scale).toInt(), + interpolation: img2.Interpolation.average, + ) + .getBytes(format: img2.Format.bgra); + } else { + data = Uint8List.fromList( + img2.encodePng( + img2.copyResize( + image, + width: (width * scale).toInt(), + height: (height * scale).toInt(), + interpolation: img2.Interpolation.average, + ), + ), + ); + } } this.scale = scale; @@ -863,7 +876,7 @@ class PredefinedCursor { _cache = CursorData( peerId: '', id: id, - image: _image2?.clone(), + image: _image2!.clone(), scale: scale, data: data, hotxOrigin: @@ -1063,9 +1076,9 @@ class CursorModel with ChangeNotifier { Future _updateCache( Uint8List rgba, ui.Image image, int id, int w, int h) async { Uint8List? data; - img2.Image? imgOrigin; + img2.Image imgOrigin = + img2.Image.fromBytes(w, h, rgba, format: img2.Format.rgba); if (Platform.isWindows) { - imgOrigin = img2.Image.fromBytes(w, h, rgba, format: img2.Format.rgba); data = imgOrigin.getBytes(format: img2.Format.bgra); } else { ByteData? imgBytes = From b5fbc23cb9b9fc7ba915e1024085d8ac7ad1bf18 Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 2 Feb 2023 12:39:39 +0800 Subject: [PATCH 1680/2015] zoom system cursor when view scale is adaptive Signed-off-by: fufesou --- flutter/lib/desktop/pages/remote_page.dart | 21 ++++++----- .../lib/desktop/widgets/remote_menubar.dart | 37 ++++++++++--------- 2 files changed, 32 insertions(+), 26 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 1687f348e..1ce9dec4a 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -362,10 +362,10 @@ class _RemotePageState extends State class ImagePaint extends StatefulWidget { final String id; - final Rx zoomCursor; - final Rx cursorOverImage; - final Rx keyboardEnabled; - final Rx remoteCursorMoved; + final RxBool zoomCursor; + final RxBool cursorOverImage; + final RxBool keyboardEnabled; + final RxBool remoteCursorMoved; final Widget Function(Widget)? listenerBuilder; ImagePaint( @@ -388,10 +388,10 @@ class _ImagePaintState extends State { final ScrollController _vertical = ScrollController(); String get id => widget.id; - Rx get zoomCursor => widget.zoomCursor; - Rx get cursorOverImage => widget.cursorOverImage; - Rx get keyboardEnabled => widget.keyboardEnabled; - Rx get remoteCursorMoved => widget.remoteCursorMoved; + RxBool get zoomCursor => widget.zoomCursor; + RxBool get cursorOverImage => widget.cursorOverImage; + RxBool get keyboardEnabled => widget.keyboardEnabled; + RxBool get remoteCursorMoved => widget.remoteCursorMoved; Widget Function(Widget)? get listenerBuilder => widget.listenerBuilder; @override @@ -466,7 +466,10 @@ class _ImagePaintState extends State { if (cache == null) { return MouseCursor.defer; } else { - final key = cache.updateGetKey(scale, zoomCursor.value); + final isViewAdaptive = + Provider.of(context, listen: false).viewStyle.style == + kRemoteViewStyleAdaptive; + final key = cache.updateGetKey(scale, zoomCursor.value && isViewAdaptive); if (!cursor.cachedKeys.contains(key)) { debugPrint("Register custom cursor with key $key"); // [Safety] diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 4f16f8227..517dc9750 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -1133,23 +1133,26 @@ class _RemoteMenubarState extends State { }()); } - displayMenu.add(() { - final opt = 'zoom-cursor'; - final state = PeerBoolOption.find(widget.id, opt); - return MenuEntrySwitch2( - switchType: SwitchType.scheckbox, - text: translate('Zoom cursor'), - getter: () { - return state; - }, - setter: (bool v) async { - state.value = v; - await bind.sessionToggleOption(id: widget.id, value: opt); - }, - padding: padding, - dismissOnClicked: true, - ); - }()); + /// Show remote cursor scaling with image + if (widget.state.viewStyle.value != kRemoteViewStyleOriginal) { + displayMenu.add(() { + final opt = 'zoom-cursor'; + final state = PeerBoolOption.find(widget.id, opt); + return MenuEntrySwitch2( + switchType: SwitchType.scheckbox, + text: translate('Zoom cursor'), + getter: () { + return state; + }, + setter: (bool v) async { + state.value = v; + await bind.sessionToggleOption(id: widget.id, value: opt); + }, + padding: padding, + dismissOnClicked: true, + ); + }()); + } /// Show quality monitor displayMenu.add(MenuEntrySwitch( From 77ee60c8dc730af4b6658119b452cd1d417bba66 Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 2 Feb 2023 12:47:39 +0800 Subject: [PATCH 1681/2015] scale remote cursor when view style is adaptive Signed-off-by: fufesou --- flutter/lib/desktop/pages/remote_page.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 1ce9dec4a..858157853 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -635,7 +635,8 @@ class CursorPaint extends StatelessWidget { double x = (m.x - hotx) * c.scale + cx; double y = (m.y - hoty) * c.scale + cy; double scale = 1.0; - if (zoomCursor.isTrue) { + final isViewAdaptive = c.viewStyle.style == kRemoteViewStyleAdaptive; + if (zoomCursor.isTrue && isViewAdaptive) { x = m.x - hotx + cx / c.scale; y = m.y - hoty + cy / c.scale; scale = c.scale; From aafc2e0a8e4d0525b19ad011936354d6802bfb84 Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 2 Feb 2023 13:08:41 +0800 Subject: [PATCH 1682/2015] zoom cursor on different OSs Signed-off-by: fufesou --- flutter/lib/desktop/pages/remote_page.dart | 28 +++++++++++++++++----- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 858157853..dd71797f3 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -466,10 +466,19 @@ class _ImagePaintState extends State { if (cache == null) { return MouseCursor.defer; } else { - final isViewAdaptive = - Provider.of(context, listen: false).viewStyle.style == - kRemoteViewStyleAdaptive; - final key = cache.updateGetKey(scale, zoomCursor.value && isViewAdaptive); + bool shouldScale = false; + if (Platform.isWindows) { + final isViewAdaptive = + Provider.of(context, listen: false).viewStyle.style == + kRemoteViewStyleAdaptive; + shouldScale = zoomCursor.value && isViewAdaptive; + } else { + final isViewOriginal = + Provider.of(context, listen: false).viewStyle.style == + kRemoteViewStyleOriginal; + shouldScale = zoomCursor.value || isViewOriginal; + } + final key = cache.updateGetKey(scale, shouldScale); if (!cursor.cachedKeys.contains(key)) { debugPrint("Register custom cursor with key $key"); // [Safety] @@ -635,8 +644,15 @@ class CursorPaint extends StatelessWidget { double x = (m.x - hotx) * c.scale + cx; double y = (m.y - hoty) * c.scale + cy; double scale = 1.0; - final isViewAdaptive = c.viewStyle.style == kRemoteViewStyleAdaptive; - if (zoomCursor.isTrue && isViewAdaptive) { + bool shouldScale = false; + if (Platform.isWindows) { + final isViewAdaptive = c.viewStyle.style == kRemoteViewStyleAdaptive; + shouldScale = zoomCursor.value && isViewAdaptive; + } else { + final isViewOriginal = c.viewStyle.style == kRemoteViewStyleOriginal; + shouldScale = zoomCursor.value || isViewOriginal; + } + if (shouldScale) { x = m.x - hotx + cx / c.scale; y = m.y - hoty + cy / c.scale; scale = c.scale; From d511d1e27a024847a50d31ec85392478907dccd6 Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 2 Feb 2023 13:40:13 +0800 Subject: [PATCH 1683/2015] zoom remote cursor when view style is original Signed-off-by: fufesou --- flutter/lib/desktop/pages/remote_page.dart | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index dd71797f3..f38cdfb6e 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -644,15 +644,8 @@ class CursorPaint extends StatelessWidget { double x = (m.x - hotx) * c.scale + cx; double y = (m.y - hoty) * c.scale + cy; double scale = 1.0; - bool shouldScale = false; - if (Platform.isWindows) { - final isViewAdaptive = c.viewStyle.style == kRemoteViewStyleAdaptive; - shouldScale = zoomCursor.value && isViewAdaptive; - } else { - final isViewOriginal = c.viewStyle.style == kRemoteViewStyleOriginal; - shouldScale = zoomCursor.value || isViewOriginal; - } - if (shouldScale) { + final isViewOriginal = c.viewStyle.style == kRemoteViewStyleOriginal; + if (zoomCursor.value || isViewOriginal) { x = m.x - hotx + cx / c.scale; y = m.y - hoty + cy / c.scale; scale = c.scale; From f9e3a3f074ffc3cfc43e398abb712d631497e930 Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 2 Feb 2023 14:39:58 +0800 Subject: [PATCH 1684/2015] zoom cursor with dpi Signed-off-by: fufesou --- flutter/lib/desktop/pages/remote_page.dart | 32 ++++++++++++---------- flutter/lib/models/model.dart | 15 +++++----- 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index f38cdfb6e..f7889d008 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -399,6 +399,20 @@ class _ImagePaintState extends State { final m = Provider.of(context); var c = Provider.of(context); final s = c.scale; + var cursorScale = 1.0; + + if (Platform.isWindows) { + // debug win10 + final isViewAdaptive = c.viewStyle.style == kRemoteViewStyleAdaptive; + if (zoomCursor.value && isViewAdaptive) { + cursorScale = s * c.devicePixelRatio; + } + } else { + final isViewOriginal = c.viewStyle.style == kRemoteViewStyleOriginal; + if (zoomCursor.value || isViewOriginal) { + cursorScale = s; + } + } mouseRegion({child}) => Obx(() => MouseRegion( cursor: cursorOverImage.isTrue @@ -414,10 +428,10 @@ class _ImagePaintState extends State { _lastRemoteCursorMoved = false; _firstEnterImage.value = true; } - return _buildCustomCursor(context, s); + return _buildCustomCursor(context, cursorScale); } }()) - : _buildDisabledCursor(context, s) + : _buildDisabledCursor(context, cursorScale) : MouseCursor.defer, onHover: (evt) {}, child: child)); @@ -466,19 +480,7 @@ class _ImagePaintState extends State { if (cache == null) { return MouseCursor.defer; } else { - bool shouldScale = false; - if (Platform.isWindows) { - final isViewAdaptive = - Provider.of(context, listen: false).viewStyle.style == - kRemoteViewStyleAdaptive; - shouldScale = zoomCursor.value && isViewAdaptive; - } else { - final isViewOriginal = - Provider.of(context, listen: false).viewStyle.style == - kRemoteViewStyleOriginal; - shouldScale = zoomCursor.value || isViewOriginal; - } - final key = cache.updateGetKey(scale, shouldScale); + final key = cache.updateGetKey(scale); if (!cursor.cachedKeys.contains(key)) { debugPrint("Register custom cursor with key $key"); // [Safety] diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index b2df5faac..f49bb270c 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -540,6 +540,7 @@ class CanvasModel with ChangeNotifier { double _y = 0; // image scale double _scale = 1.0; + double _devicePixelRatio = 1.0; Size _size = Size.zero; // the tabbar over the image // double tabBarHeight = 0.0; @@ -563,6 +564,7 @@ class CanvasModel with ChangeNotifier { double get x => _x; double get y => _y; double get scale => _scale; + double get devicePixelRatio => _devicePixelRatio; Size get size => _size; ScrollStyle get scrollStyle => _scrollStyle; ViewStyle get viewStyle => _lastViewStyle; @@ -611,8 +613,9 @@ class CanvasModel with ChangeNotifier { _lastViewStyle = viewStyle; _scale = viewStyle.scale; + _devicePixelRatio = ui.window.devicePixelRatio; if (kIgnoreDpi && style == kRemoteViewStyleOriginal) { - _scale = 1.0 / ui.window.devicePixelRatio; + _scale = 1.0 / _devicePixelRatio; } _x = (size.width - displayWidth * _scale) / 2; _y = (size.height - displayHeight * _scale) / 2; @@ -772,11 +775,9 @@ class CursorData { int _doubleToInt(double v) => (v * 10e6).round().toInt(); - double _checkUpdateScale(double scale, bool shouldScale) { + double _checkUpdateScale(double scale) { double oldScale = this.scale; - if (!shouldScale) { - scale = 1.0; - } else { + if (scale != 1.0) { // Update data if scale changed. final tgtWidth = (width * scale).toInt(); final tgtHeight = (width * scale).toInt(); @@ -817,8 +818,8 @@ class CursorData { return scale; } - String updateGetKey(double scale, bool shouldScale) { - scale = _checkUpdateScale(scale, shouldScale); + String updateGetKey(double scale) { + scale = _checkUpdateScale(scale); return '${peerId}_${id}_${_doubleToInt(width * scale)}_${_doubleToInt(height * scale)}'; } } From 40a75e3dfab6707f3a87a36aee1a9f57460d8df1 Mon Sep 17 00:00:00 2001 From: "Miguel F. G" <116861809+flusheDData@users.noreply.github.com> Date: Thu, 2 Feb 2023 08:49:12 +0100 Subject: [PATCH 1685/2015] Update es.rs New 'Default' terms added. --- src/lang/es.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/lang/es.rs b/src/lang/es.rs index 6f866845c..5fdb7ee2c 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -436,14 +436,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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", ""), - ("Default View Style", ""), - ("Default Scroll Style", ""), - ("Default Image Quality", ""), - ("Default Codec", ""), - ("Bitrate", ""), + ("Display", "Pantalla"), + ("Default View Style", "Estilo de vista predeterminado"), + ("Default Scroll Style", "Estilo de desplazamiento predeterminado"), + ("Default Image Quality", "Calidad de imagen predeterminada"), + ("Default Codec", "Códec predeterminado"), + ("Bitrate", "Tasa de bits"), ("FPS", ""), ("Auto", ""), - ("Other Default Options", ""), + ("Other Default Options", "Otras opciones predeterminadas"), ].iter().cloned().collect(); } From 1e9625045b222fd9d63013b37ceb88944ac5b6d9 Mon Sep 17 00:00:00 2001 From: csf Date: Thu, 2 Feb 2023 20:05:57 +0900 Subject: [PATCH 1686/2015] fix chat text selectable --- flutter/lib/common/widgets/chat_page.dart | 25 +++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/flutter/lib/common/widgets/chat_page.dart b/flutter/lib/common/widgets/chat_page.dart index d1d96199a..62f81b797 100644 --- a/flutter/lib/common/widgets/chat_page.dart +++ b/flutter/lib/common/widgets/chat_page.dart @@ -95,10 +95,31 @@ class ChatPage extends StatelessWidget implements PageShape { color: Theme.of(context).colorScheme.primary)), messageOptions: MessageOptions( showOtherUsersAvatar: false, - showTime: true, - currentUserTextColor: Colors.white, textColor: Colors.white, maxWidth: constraints.maxWidth * 0.7, + messageTextBuilder: (message, _, __) { + final isOwnMessage = + message.user.id == currentUser.id; + return Column( + crossAxisAlignment: isOwnMessage + ? CrossAxisAlignment.end + : CrossAxisAlignment.start, + children: [ + Text(message.text, + style: TextStyle(color: Colors.white)), + Padding( + padding: const EdgeInsets.only(top: 5), + child: Text( + "${message.createdAt.hour}:${message.createdAt.minute}", + style: TextStyle( + color: Colors.white, + fontSize: 10, + ), + ), + ), + ], + ); + }, messageDecorationBuilder: (_, __, ___) => defaultMessageDecoration( color: MyTheme.accent80, From c6269b54af37e60fb4a03e6f06623065ea998a0e Mon Sep 17 00:00:00 2001 From: csf Date: Thu, 2 Feb 2023 21:39:25 +0900 Subject: [PATCH 1687/2015] add requestChatInputFocus() --- flutter/lib/models/chat_model.dart | 12 ++++++++++++ flutter/test/cm_test.dart | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/flutter/lib/models/chat_model.dart b/flutter/lib/models/chat_model.dart index 18a0be279..bab88a9dd 100644 --- a/flutter/lib/models/chat_model.dart +++ b/flutter/lib/models/chat_model.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:dash_chat_2/dash_chat_2.dart'; import 'package:draggable_float_widget/draggable_float_widget.dart'; import 'package:flutter/material.dart'; @@ -139,6 +141,7 @@ class ChatModel with ChangeNotifier { }); overlayState.insert(overlay); chatWindowOverlayEntry = overlay; + requestChatInputFocus(); } hideChatWindowOverlay() { @@ -188,6 +191,7 @@ class ChatModel with ChangeNotifier { await windowManager.setSizeAlignment( kConnectionManagerWindowSize, Alignment.topRight); } else { + requestChatInputFocus(); await windowManager.show(); await windowManager.setSizeAlignment(Size(600, 400), Alignment.topRight); _isShowCMChatPage = !_isShowCMChatPage; @@ -292,4 +296,12 @@ class ChatModel with ChangeNotifier { resetClientMode() { _messages[clientModeID]?.clear(); } + + void requestChatInputFocus() { + Timer(Duration(milliseconds: 100), () { + if (inputNode.hasListeners && inputNode.canRequestFocus) { + inputNode.requestFocus(); + } + }); + } } diff --git a/flutter/test/cm_test.dart b/flutter/test/cm_test.dart index 592a28fcf..2c037c7b0 100644 --- a/flutter/test/cm_test.dart +++ b/flutter/test/cm_test.dart @@ -16,7 +16,7 @@ final testClients = [ Client(3, false, false, "UserDDDDDDDDDDDd", "441123123", true, false, false) ]; -/// -t lib/cm_main.dart to test cm +/// flutter run -d {platform} -t lib/cm_test.dart to test cm void main(List args) async { isTest = true; WidgetsFlutterBinding.ensureInitialized(); From 0d9d506dac843986685c69ca72fdfd553d217f78 Mon Sep 17 00:00:00 2001 From: NicKoehler <53040044+NicKoehler@users.noreply.github.com> Date: Thu, 2 Feb 2023 14:48:22 +0100 Subject: [PATCH 1688/2015] Update it.rs --- src/lang/it.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/lang/it.rs b/src/lang/it.rs index 6edd4a461..d84b56a8a 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -436,14 +436,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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", ""), - ("Default View Style", ""), - ("Default Scroll Style", ""), - ("Default Image Quality", ""), - ("Default Codec", ""), - ("Bitrate", ""), - ("FPS", ""), - ("Auto", ""), - ("Other Default Options", ""), + ("Display", "Visualizzazione"), + ("Default View Style", "Stile Visualizzazione Predefinito"), + ("Default Scroll Style", "Stile Scorrimento Predefinito"), + ("Default Image Quality", "Qualità Immagine Predefinita"), + ("Default Codec", "Codec Predefinito"), + ("Bitrate", "Bitrate"), + ("FPS", "FPS"), + ("Auto", "Auto"), + ("Other Default Options", "Altre Opzioni Predefinite"), ].iter().cloned().collect(); } From 1a1bd1b5d8c6be911451e288b577092795d967f4 Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 2 Feb 2023 16:45:29 +0800 Subject: [PATCH 1689/2015] recover reordered peer tab, because flutter 3.7.0 fix ReorderableListView crash Signed-off-by: 21pages --- flutter/lib/common/widgets/peer_tab_page.dart | 235 +++++++++++++----- 1 file changed, 168 insertions(+), 67 deletions(-) diff --git a/flutter/lib/common/widgets/peer_tab_page.dart b/flutter/lib/common/widgets/peer_tab_page.dart index 278f5861c..fff5e2ffd 100644 --- a/flutter/lib/common/widgets/peer_tab_page.dart +++ b/flutter/lib/common/widgets/peer_tab_page.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'dart:ui' as ui; import 'package:bot_toast/bot_toast.dart'; @@ -20,7 +21,9 @@ const int groupTabIndex = 4; const String defaultGroupTabname = 'Group'; class StatePeerTab { - final RxInt currentTab = 0.obs; + final RxInt currentTab = 0.obs; // index in tabNames + final RxList visibleOrderedTabs = RxList.empty(growable: true); + List tabOrder = List.from([0, 1, 2, 3, 4]); // constant length final RxInt tabHiddenFlag = 0.obs; final RxList tabNames = [ 'Recent Sessions', @@ -31,53 +34,80 @@ class StatePeerTab { ].obs; StatePeerTab._() { + // init tabHiddenFlag tabHiddenFlag.value = (int.tryParse( bind.getLocalFlutterConfig(k: 'hidden-peer-card'), radix: 2) ?? 0); var tabs = _notHiddenTabs(); + // remove dynamic tabs + tabs.remove(groupTabIndex); + // init tabOrder + try { + final conf = bind.getLocalFlutterConfig(k: 'peer-tab-order'); + if (conf.isNotEmpty) { + final json = jsonDecode(conf); + if (json is List) { + final List list = + json.map((e) => int.tryParse(e.toString()) ?? -1).toList(); + if (list.length == tabOrder.length && + tabOrder.every((e) => list.contains(e))) { + tabOrder = list; + } + } + } + } catch (e) { + debugPrintStack(label: '$e'); + } + // init visibleOrderedTabs + var tempList = tabOrder.toList(); + tempList.removeWhere((e) => !tabs.contains(e)); + visibleOrderedTabs.value = tempList; + // init currentTab currentTab.value = int.tryParse(bind.getLocalFlutterConfig(k: 'peer-tab-index')) ?? 0; if (!tabs.contains(currentTab.value)) { - currentTab.value = 0; + if (tabs.isNotEmpty) { + currentTab.value = tabs[0]; + } else { + currentTab.value = 0; + } } } static final StatePeerTab instance = StatePeerTab._(); + // check dynamic tabs check() { - var tabs = _notHiddenTabs(); - if (filterGroupCard()) { - if (currentTab.value == groupTabIndex) { - currentTab.value = - tabs.firstWhereOrNull((e) => e != groupTabIndex) ?? 0; - bind.setLocalFlutterConfig( - k: 'peer-tab-index', v: currentTab.value.toString()); - } + tabOrder2visibleOrderedTabs(); + if (visibleOrderedTabs.contains(groupTabIndex) && + int.tryParse(bind.getLocalFlutterConfig(k: 'peer-tab-index')) == + groupTabIndex) { + currentTab.value = groupTabIndex; + } + if (gFFI.userModel.isAdmin.isFalse && gFFI.userModel.groupName.isNotEmpty) { + tabNames[groupTabIndex] = gFFI.userModel.groupName.value; } else { - if (gFFI.userModel.isAdmin.isFalse && - gFFI.userModel.groupName.isNotEmpty) { - tabNames[groupTabIndex] = gFFI.userModel.groupName.value; - } else { - tabNames[groupTabIndex] = defaultGroupTabname; - } - if (tabs.contains(groupTabIndex) && - int.tryParse(bind.getLocalFlutterConfig(k: 'peer-tab-index')) == - groupTabIndex) { - currentTab.value = groupTabIndex; - } + tabNames[groupTabIndex] = defaultGroupTabname; } } - List currentTabs() { - var v = List.empty(growable: true); - for (int i = 0; i < tabNames.length; i++) { - if (!_isTabHidden(i) && !_isTabFilter(i)) { - v.add(i); - } + visibleOrderedTabs2TabOrder() { + var tmpTabOrder = visibleOrderedTabs.toList(); + var left = tabOrder.where((e) => !tmpTabOrder.contains(e)).toList(); + for (var t in left) { + _addTabInOrder(tmpTabOrder, t); } - return v; + statePeerTab.tabOrder = tmpTabOrder; + bind.setLocalFlutterConfig(k: 'peer-tab-order', v: jsonEncode(tmpTabOrder)); } + tabOrder2visibleOrderedTabs() { + var visible = statePeerTab.visibleTabs(); + statePeerTab.visibleOrderedTabs.value = + statePeerTab.tabOrder.where((e) => visible.contains(e)).toList(); + } + + // return true if hide group card bool filterGroupCard() { if (gFFI.groupModel.users.isEmpty || (gFFI.userModel.isAdmin.isFalse && gFFI.userModel.groupName.isEmpty)) { @@ -87,6 +117,17 @@ class StatePeerTab { } } + // return index array of tabNames + List visibleTabs() { + var v = List.empty(growable: true); + for (int i = 0; i < tabNames.length; i++) { + if (!_isTabHidden(i) && !_isTabFilter(i)) { + v.add(i); + } + } + return v; + } + bool _isTabHidden(int tabindex) { return tabHiddenFlag & (1 << tabindex) != 0; } @@ -107,6 +148,41 @@ class StatePeerTab { } return v; } + + // add tabIndex to list + _addTabInOrder(List list, int tabIndex) { + if (!tabOrder.contains(tabIndex) || list.contains(tabIndex)) { + return; + } + bool sameOrder = true; + int lastIndex = -1; + for (int i = 0; i < list.length; i++) { + var index = tabOrder.lastIndexOf(list[i]); + if (index > lastIndex) { + lastIndex = index; + continue; + } else { + sameOrder = false; + break; + } + } + if (sameOrder) { + var indexInTabOrder = tabOrder.indexOf(tabIndex); + var left = List.empty(growable: true); + for (int i = 0; i < indexInTabOrder; i++) { + left.add(tabOrder[i]); + } + int insertIndex = list.lastIndexWhere((e) => left.contains(e)); + if (insertIndex < 0) { + insertIndex = 0; + } else { + insertIndex += 1; + } + list.insert(insertIndex, tabIndex); + } else { + list.add(tabIndex); + } + } } final statePeerTab = StatePeerTab.instance; @@ -177,11 +253,6 @@ class _PeerTabPageState extends State } } - @override - void dispose() { - super.dispose(); - } - @override Widget build(BuildContext context) { return Column( @@ -215,40 +286,57 @@ class _PeerTabPageState extends State Widget _createSwitchBar(BuildContext context) { final textColor = Theme.of(context).textTheme.titleLarge?.color; return Obx(() { - var tabs = statePeerTab.currentTabs(); - return ListView( + var tabs = statePeerTab.visibleOrderedTabs; + int indexCounter = -1; + return ReorderableListView( + buildDefaultDragHandles: false, + onReorder: (oldIndex, newIndex) { + if (oldIndex < newIndex) { + newIndex -= 1; + } + var list = tabs.toList(); + final int item = list.removeAt(oldIndex); + list.insert(newIndex, item); + tabs.value = list; + statePeerTab.visibleOrderedTabs2TabOrder(); + }, scrollDirection: Axis.horizontal, physics: NeverScrollableScrollPhysics(), - controller: ScrollController(), + scrollController: ScrollController(), children: tabs.map((t) { - return InkWell( - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 8), - decoration: BoxDecoration( - color: statePeerTab.currentTab.value == t - ? Theme.of(context).backgroundColor - : null, - borderRadius: BorderRadius.circular(isDesktop ? 2 : 6), - ), - child: Align( - alignment: Alignment.center, - child: Text( - translatedTabname(t), - textAlign: TextAlign.center, - style: TextStyle( - height: 1, - fontSize: 14, - color: statePeerTab.currentTab.value == t - ? textColor - : textColor - ?..withOpacity(0.5)), + indexCounter++; + return ReorderableDragStartListener( + key: ValueKey(t), + index: indexCounter, + child: InkWell( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8), + decoration: BoxDecoration( + color: statePeerTab.currentTab.value == t + ? Theme.of(context).backgroundColor + : null, + borderRadius: BorderRadius.circular(isDesktop ? 2 : 6), ), - )), - onTap: () async { - await handleTabSelection(t); - await bind.setLocalFlutterConfig( - k: 'peer-tab-index', v: t.toString()); - }, + child: Align( + alignment: Alignment.center, + child: Text( + translatedTabname(t), + textAlign: TextAlign.center, + style: TextStyle( + height: 1, + fontSize: 14, + color: statePeerTab.currentTab.value == t + ? textColor + : textColor + ?..withOpacity(0.5)), + ), + )), + onTap: () async { + await handleTabSelection(t); + await bind.setLocalFlutterConfig( + k: 'peer-tab-index', v: t.toString()); + }, + ), ); }).toList()); }); @@ -275,7 +363,7 @@ class _PeerTabPageState extends State final verticalMargin = isDesktop ? 12.0 : 6.0; return Expanded( child: Obx(() { - var tabs = statePeerTab.currentTabs(); + var tabs = statePeerTab.visibleOrderedTabs; if (tabs.isEmpty) { return visibleContextMenuListener(Center( child: Text(translate('Right click to select tabs')), @@ -322,7 +410,7 @@ class _PeerTabPageState extends State } adjustTab() { - var tabs = statePeerTab.currentTabs(); + var tabs = statePeerTab.visibleOrderedTabs; if (tabs.isNotEmpty && !tabs.contains(statePeerTab.currentTab.value)) { statePeerTab.currentTab.value = tabs[0]; } @@ -349,11 +437,13 @@ class _PeerTabPageState extends State Widget visibleContextMenu(CancelFunc cancelFunc) { return Obx(() { final List menu = List.empty(growable: true); + final List menuIndex = List.empty(growable: true); for (int i = 0; i < statePeerTab.tabNames.length; i++) { if (i == groupTabIndex && statePeerTab.filterGroupCard()) { continue; } int bitMask = 1 << i; + menuIndex.add(i); menu.add(MenuEntrySwitch( switchType: SwitchType.scheckbox, text: translatedTabname(i), @@ -369,12 +459,21 @@ class _PeerTabPageState extends State await bind.setLocalFlutterConfig( k: 'hidden-peer-card', v: statePeerTab.tabHiddenFlag.value.toRadixString(2)); + statePeerTab.tabOrder2visibleOrderedTabs(); cancelFunc(); adjustTab(); })); } + // show in tabOrder + List menu2 = List.empty(growable: true); + statePeerTab.tabOrder.map((e) { + final index = menuIndex.indexOf(e); + if (index >= 0) { + menu2.add(menu[index]); + } + }).toList(); return mod_menu.PopupMenu( - items: menu + items: menu2 .map((entry) => entry.build( context, const MenuConfig( @@ -421,7 +520,9 @@ class _PeerSearchBarState extends State { FocusNode focusNode = FocusNode(); focusNode.addListener(() { focused.value = focusNode.hasFocus; - peerSearchTextController.selection = TextSelection(baseOffset: 0, extentOffset: peerSearchTextController.value.text.length); + peerSearchTextController.selection = TextSelection( + baseOffset: 0, + extentOffset: peerSearchTextController.value.text.length); }); return Container( width: 120, From 50c8855d2816897527fb65c4cd01c8e1f7f16c6a Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 3 Feb 2023 16:18:08 +0800 Subject: [PATCH 1690/2015] unify peer tab text color with tab bar text color --- flutter/lib/common/widgets/peer_tab_page.dart | 5 ++--- flutter/pubspec.lock | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/flutter/lib/common/widgets/peer_tab_page.dart b/flutter/lib/common/widgets/peer_tab_page.dart index fff5e2ffd..150121c59 100644 --- a/flutter/lib/common/widgets/peer_tab_page.dart +++ b/flutter/lib/common/widgets/peer_tab_page.dart @@ -284,7 +284,6 @@ class _PeerTabPageState extends State } Widget _createSwitchBar(BuildContext context) { - final textColor = Theme.of(context).textTheme.titleLarge?.color; return Obx(() { var tabs = statePeerTab.visibleOrderedTabs; int indexCounter = -1; @@ -326,8 +325,8 @@ class _PeerTabPageState extends State height: 1, fontSize: 14, color: statePeerTab.currentTab.value == t - ? textColor - : textColor + ? MyTheme.tabbar(context).selectedTextColor + : MyTheme.tabbar(context).unSelectedTextColor ?..withOpacity(0.5)), ), )), diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index c193c0651..ebb105178 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -475,10 +475,10 @@ packages: dependency: "direct main" description: name: flutter_custom_cursor - sha256: "6c5204cf6a16650355b8aa47a8402e79922c07641390a32021a1069b561909ec" + sha256: "3850a32ac6de351ccc5e4286b6d94ff70c10abecd44479ea6c5aaea17264285d" url: "https://pub.dev" source: hosted - version: "0.0.3" + version: "0.0.4" flutter_improved_scrolling: dependency: "direct main" description: From e05b95743c2f67bfaee077f4338007c9c6e16238 Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 3 Feb 2023 17:02:28 +0800 Subject: [PATCH 1691/2015] cursor position and size update Signed-off-by: fufesou --- flutter/lib/desktop/pages/remote_page.dart | 77 +++++++++++-------- .../widgets/material_mod_popup_menu.dart | 16 +++- .../lib/desktop/widgets/remote_menubar.dart | 22 ++++-- flutter/lib/models/input_model.dart | 2 - flutter/lib/models/model.dart | 2 +- 5 files changed, 74 insertions(+), 45 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index f7889d008..0e0127312 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -399,42 +399,51 @@ class _ImagePaintState extends State { final m = Provider.of(context); var c = Provider.of(context); final s = c.scale; - var cursorScale = 1.0; - if (Platform.isWindows) { - // debug win10 - final isViewAdaptive = c.viewStyle.style == kRemoteViewStyleAdaptive; - if (zoomCursor.value && isViewAdaptive) { - cursorScale = s * c.devicePixelRatio; - } - } else { - final isViewOriginal = c.viewStyle.style == kRemoteViewStyleOriginal; - if (zoomCursor.value || isViewOriginal) { - cursorScale = s; - } - } + mouseRegion({child}) => Obx(() { + double getCursorScale() { + var c = Provider.of(context); + var cursorScale = 1.0; + if (Platform.isWindows) { + // debug win10 + final isViewAdaptive = + c.viewStyle.style == kRemoteViewStyleAdaptive; + if (zoomCursor.value && isViewAdaptive) { + cursorScale = s * c.devicePixelRatio; + } + } else { + final isViewOriginal = + c.viewStyle.style == kRemoteViewStyleOriginal; + if (zoomCursor.value || isViewOriginal) { + cursorScale = s; + } + } + return cursorScale; + } - mouseRegion({child}) => Obx(() => MouseRegion( - cursor: cursorOverImage.isTrue - ? c.cursorEmbedded - ? SystemMouseCursors.none - : keyboardEnabled.isTrue - ? (() { - if (remoteCursorMoved.isTrue) { - _lastRemoteCursorMoved = true; - return SystemMouseCursors.none; - } else { - if (_lastRemoteCursorMoved) { - _lastRemoteCursorMoved = false; - _firstEnterImage.value = true; - } - return _buildCustomCursor(context, cursorScale); - } - }()) - : _buildDisabledCursor(context, cursorScale) - : MouseCursor.defer, - onHover: (evt) {}, - child: child)); + return MouseRegion( + cursor: cursorOverImage.isTrue + ? c.cursorEmbedded + ? SystemMouseCursors.none + : keyboardEnabled.isTrue + ? (() { + if (remoteCursorMoved.isTrue) { + _lastRemoteCursorMoved = true; + return SystemMouseCursors.none; + } else { + if (_lastRemoteCursorMoved) { + _lastRemoteCursorMoved = false; + _firstEnterImage.value = true; + } + return _buildCustomCursor( + context, getCursorScale()); + } + }()) + : _buildDisabledCursor(context, getCursorScale()) + : MouseCursor.defer, + onHover: (evt) {}, + child: child); + }); if (c.imageOverflow.isTrue && c.scrollStyle == ScrollStyle.scrollbar) { final imageWidth = c.getDisplayWidth() * s; diff --git a/flutter/lib/desktop/widgets/material_mod_popup_menu.dart b/flutter/lib/desktop/widgets/material_mod_popup_menu.dart index a371e8f52..666c9a6e2 100644 --- a/flutter/lib/desktop/widgets/material_mod_popup_menu.dart +++ b/flutter/lib/desktop/widgets/material_mod_popup_menu.dart @@ -790,6 +790,7 @@ class _PopupMenuRoute extends PopupRoute { _PopupMenuRoute({ required this.position, required this.items, + this.menuWrapper, this.initialValue, this.elevation, required this.barrierLabel, @@ -802,6 +803,7 @@ class _PopupMenuRoute extends PopupRoute { final RelativeRect position; final List> items; + final MenuWrapper? menuWrapper; final List itemSizes; final T? initialValue; final double? elevation; @@ -844,11 +846,14 @@ class _PopupMenuRoute extends PopupRoute { } } - final Widget menu = _PopupMenu( + Widget menu = _PopupMenu( route: this, semanticLabel: semanticLabel, constraints: constraints, ); + if (this.menuWrapper != null) { + menu = this.menuWrapper!(menu); + } final MediaQueryData mediaQuery = MediaQuery.of(context); return MediaQuery.removePadding( context: context, @@ -1035,6 +1040,7 @@ Future showMenu({ required BuildContext context, required RelativeRect position, required List> items, + MenuWrapper? menuWrapper, T? initialValue, double? elevation, String? semanticLabel, @@ -1062,6 +1068,7 @@ Future showMenu({ return navigator.push(_PopupMenuRoute( position: position, items: items, + menuWrapper: menuWrapper, initialValue: initialValue, elevation: elevation, semanticLabel: semanticLabel, @@ -1094,6 +1101,8 @@ typedef PopupMenuCanceled = void Function(); typedef PopupMenuItemBuilder = List> Function( BuildContext context); +typedef MenuWrapper = Widget Function(Widget child); + /// Displays a menu when pressed and calls [onSelected] when the menu is dismissed /// because an item was selected. The value passed to [onSelected] is the value of /// the selected menu item. @@ -1124,6 +1133,7 @@ class PopupMenuButton extends StatefulWidget { const PopupMenuButton({ Key? key, required this.itemBuilder, + this.menuWrapper, this.initialValue, this.onHover, this.onSelected, @@ -1151,6 +1161,9 @@ class PopupMenuButton extends StatefulWidget { /// Called when the button is pressed to create the items to show in the menu. final PopupMenuItemBuilder itemBuilder; + /// Menu wrapper. + final MenuWrapper? menuWrapper; + /// The value of the menu item, if any, that should be highlighted when the menu opens. final T? initialValue; @@ -1333,6 +1346,7 @@ class PopupMenuButtonState extends State> { context: context, elevation: widget.elevation ?? popupMenuTheme.elevation, items: items, + menuWrapper: widget.menuWrapper, initialValue: widget.initialValue, position: position, shape: widget.shape ?? popupMenuTheme.shape, diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 64d289fcc..db1721d99 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -221,6 +221,16 @@ class _RemoteMenubarState extends State { } } + Widget _buildPointerTrackWidget(Widget child) { + return Listener( + onPointerHover: (PointerHoverEvent e) => + widget.ffi.inputModel.lastMousePos = e.position, + child: MouseRegion( + child: child, + ), + ); + } + Widget _buildMenubar(BuildContext context) { final List menubarItems = []; if (!isWebDesktop) { @@ -379,13 +389,10 @@ class _RemoteMenubarState extends State { mod_menu.PopupMenuItem( height: _MenubarTheme.height, padding: EdgeInsets.zero, - child: Listener( - onPointerHover: (PointerHoverEvent e) => - widget.ffi.inputModel.lastMousePos = e.position, - child: MouseRegion( - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: rowChildren), + child: _buildPointerTrackWidget( + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: rowChildren, ), ), ) @@ -435,6 +442,7 @@ class _RemoteMenubarState extends State { ), tooltip: translate('Display Settings'), position: mod_menu.PopupMenuPosition.under, + menuWrapper: _buildPointerTrackWidget, itemBuilder: (BuildContext context) => _getDisplayMenu(snapshot.data!, remoteCount) .map((entry) => entry.build( diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index d2f671cdc..8c37f50bd 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -310,7 +310,6 @@ class InputModel { } } - int _signOrZero(num x) { if (x == 0) { return 0; @@ -362,7 +361,6 @@ class InputModel { trackpadScrollDistance = Offset.zero; } - void onPointDownImage(PointerDownEvent e) { debugPrint("onPointDownImage"); if (e.kind != ui.PointerDeviceKind.mouse) { diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index f49bb270c..da711bf13 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -244,7 +244,6 @@ class FfiModel with ChangeNotifier { parent.target?.canvasModel.updateViewStyle(); } parent.target?.recordingModel.onSwitchDisplay(); - parent.target?.inputModel.refreshMousePos(); notifyListeners(); } @@ -621,6 +620,7 @@ class CanvasModel with ChangeNotifier { _y = (size.height - displayHeight * _scale) / 2; _imageOverflow.value = _x < 0 || y < 0; notifyListeners(); + parent.target?.inputModel.refreshMousePos(); } updateScrollStyle() async { From 17aac13247a94bbf35a8a169a4b50f8a4b14a9f4 Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 3 Feb 2023 18:28:47 +0800 Subject: [PATCH 1692/2015] ignore first update cursor postion Signed-off-by: fufesou --- flutter/lib/desktop/widgets/remote_menubar.dart | 3 ++- flutter/lib/models/model.dart | 7 ++++++- src/client.rs | 8 ++++++-- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index db1721d99..2a84dcf14 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -1157,8 +1157,9 @@ class _RemoteMenubarState extends State { return state; }, setter: (bool v) async { - state.value = v; await bind.sessionToggleOption(id: widget.id, value: opt); + state.value = + bind.sessionGetToggleOptionSync(id: widget.id, arg: opt); }, padding: padding, dismissOnClicked: true, diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index da711bf13..8a7a1005d 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -904,6 +904,7 @@ class CursorModel with ChangeNotifier { double _hoty = 0; double _displayOriginX = 0; double _displayOriginY = 0; + bool _firstUpdateMousePos = false; bool gotMouseControl = true; DateTime _lastPeerMouse = DateTime.now() .subtract(Duration(milliseconds: 2 * kMouseControlTimeoutMSec)); @@ -1121,7 +1122,11 @@ class CursorModel with ChangeNotifier { /// Update the cursor position. updateCursorPosition(Map evt, String id) async { - gotMouseControl = false; + if (!_firstUpdateMousePos) { + _firstUpdateMousePos = true; + } else { + gotMouseControl = false; + } _lastPeerMouse = DateTime.now(); _x = double.parse(evt['x']); _y = double.parse(evt['y']); diff --git a/src/client.rs b/src/client.rs index fb42ce840..e0ac68c5d 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1117,8 +1117,12 @@ impl LoginConfigHandler { } else if name == "show-quality-monitor" { config.show_quality_monitor.v = !config.show_quality_monitor.v; } else { - let v = self.options.get(&name).is_some(); - if v { + let is_set = self + .options + .get(&name) + .map(|o| !o.is_empty()) + .unwrap_or(false); + if is_set { self.config.options.remove(&name); } else { self.config.options.insert(name, "Y".to_owned()); From 66851efaa3ca2b9c5274ed80b7e43c155d4ff789 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 3 Feb 2023 17:08:40 +0800 Subject: [PATCH 1693/2015] fix: --connect command on macOS & window closing issues --- flutter/lib/common.dart | 22 ++++++++++-------- flutter/lib/consts.dart | 3 ++- .../lib/desktop/widgets/tabbar_widget.dart | 12 ++++++---- flutter/lib/main.dart | 8 +++---- flutter/lib/utils/multi_window_manager.dart | 23 ++++++++++++++++++- flutter/lib/utils/platform_channel.dart | 6 +++++ flutter/macos/Runner/AppDelegate.swift | 9 ++++---- flutter/macos/Runner/Info.plist | 10 ++++---- flutter/macos/Runner/MainFlutterWindow.swift | 3 +++ 9 files changed, 68 insertions(+), 28 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index a2623ff15..c058ec434 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -3,14 +3,11 @@ import 'dart:convert'; import 'dart:ffi' hide Size; import 'dart:io'; import 'dart:math'; -import 'dart:typed_data'; import 'package:back_button_interceptor/back_button_interceptor.dart'; import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:ffi/ffi.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter_hbb/utils/platform_channel.dart'; -import 'package:win32/win32.dart' as win32; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -19,14 +16,17 @@ import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; import 'package:flutter_hbb/main.dart'; import 'package:flutter_hbb/models/peer_model.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; +import 'package:flutter_hbb/utils/platform_channel.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:uni_links/uni_links.dart'; import 'package:uni_links_desktop/uni_links_desktop.dart'; -import 'package:window_manager/window_manager.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:window_size/window_size.dart' as window_size; import 'package:url_launcher/url_launcher.dart'; +import 'package:win32/win32.dart' as win32; +import 'package:window_manager/window_manager.dart'; +import 'package:window_size/window_size.dart' as window_size; +import '../consts.dart'; import 'common/widgets/overlay.dart'; import 'mobile/pages/file_manager_page.dart'; import 'mobile/pages/remote_page.dart'; @@ -34,8 +34,6 @@ import 'models/input_model.dart'; import 'models/model.dart'; import 'models/platform_model.dart'; -import '../consts.dart'; - final globalKey = GlobalKey(); final navigationBarKey = GlobalKey(); @@ -1275,9 +1273,11 @@ Future restoreWindowPosition(WindowType type, {int? windowId}) async { /// initUniLinks should only be used on macos/windows. /// we use dbus for linux currently. Future initUniLinks() async { - if (!Platform.isWindows && !Platform.isMacOS) { + if (Platform.isLinux) { return; } + // Register uni links for Windows. The required info of url scheme is already + // declared in `Info.plist` for macOS. if (Platform.isWindows) { registerProtocol('rustdesk'); } @@ -1508,8 +1508,12 @@ Future onActiveWindowChanged() async { } catch (err) { debugPrintStack(label: "$err"); } finally { + debugPrint("Start closing RustDesk..."); await windowManager.setPreventClose(false); await windowManager.close(); + if (Platform.isMacOS) { + RdPlatformChannel.instance.terminate(); + } } } } diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index e4081d9a5..f48b612a8 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -1,9 +1,10 @@ -import 'package:flutter/material.dart'; import 'dart:io'; +import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; const double kDesktopRemoteTabBarHeight = 28.0; +const int kMainWindowId = 0; const String kPeerPlatformWindows = "Windows"; const String kPeerPlatformLinux = "Linux"; diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 598b2cc4c..223076951 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -1,23 +1,23 @@ -import 'dart:io'; import 'dart:async'; +import 'dart:io'; import 'dart:math'; import 'dart:ui' as ui; +import 'package:bot_toast/bot_toast.dart'; import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart' hide TabBarTheme; import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/common/shared_state.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/main.dart'; -import 'package:flutter_hbb/common/shared_state.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/state_model.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:get/get_rx/src/rx_workers/utils/debouncer.dart'; import 'package:scroll_pos/scroll_pos.dart'; import 'package:window_manager/window_manager.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:bot_toast/bot_toast.dart'; import '../../utils/multi_window_manager.dart'; @@ -527,7 +527,9 @@ class WindowActionPanelState extends State void onWindowClose() async { // hide window on close if (widget.isMainWindow) { - await rustDeskWinManager.unregisterActiveWindow(0); + if (rustDeskWinManager.getActiveWindows().contains(kMainWindowId)) { + await rustDeskWinManager.unregisterActiveWindow(kMainWindowId); + } // `hide` must be placed after unregisterActiveWindow, because once all windows are hidden, // flutter closes the application on macOS. We should ensure the post-run logic has ran successfully. // e.g.: saving window position. diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 5b1e0c37c..b41cc17df 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -1,22 +1,22 @@ import 'dart:convert'; import 'dart:io'; +import 'package:bot_toast/bot_toast.dart'; import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart'; -import 'package:flutter_hbb/desktop/pages/server_page.dart'; import 'package:flutter_hbb/desktop/pages/install_page.dart'; +import 'package:flutter_hbb/desktop/pages/server_page.dart'; import 'package:flutter_hbb/desktop/screen/desktop_file_transfer_screen.dart'; import 'package:flutter_hbb/desktop/screen/desktop_port_forward_screen.dart'; import 'package:flutter_hbb/desktop/screen/desktop_remote_screen.dart'; import 'package:flutter_hbb/desktop/widgets/refresh_wrapper.dart'; +import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:window_manager/window_manager.dart'; -import 'package:bot_toast/bot_toast.dart'; // import 'package:window_manager/window_manager.dart'; @@ -139,8 +139,8 @@ void runMainApp(bool startService) async { rustDeskWinManager.registerActiveWindow(kWindowMainId); } windowManager.setOpacity(1); + windowManager.setTitle(getWindowName()); }); - windowManager.setTitle(getWindowName()); } void runMobileApp() async { diff --git a/flutter/lib/utils/multi_window_manager.dart b/flutter/lib/utils/multi_window_manager.dart index 550e9ab08..3af189ef6 100644 --- a/flutter/lib/utils/multi_window_manager.dart +++ b/flutter/lib/utils/multi_window_manager.dart @@ -160,6 +160,24 @@ class RustDeskMultiWindowManager { return null; } + void clearWindowType(WindowType type) { + switch (type) { + case WindowType.Main: + return; + case WindowType.RemoteDesktop: + _remoteDesktopWindowId = null; + break; + case WindowType.FileTransfer: + _fileTransferWindowId = null; + break; + case WindowType.PortForward: + _portForwardWindowId = null; + break; + case WindowType.Unknown: + break; + } + } + void setMethodHandler( Future Function(MethodCall call, int fromWindowId)? handler) { DesktopMultiWindow.setMethodHandler(handler); @@ -186,8 +204,11 @@ class RustDeskMultiWindowManager { } await WindowController.fromWindowId(wId).setPreventClose(false); await WindowController.fromWindowId(wId).close(); - } on Error { + } catch (e) { + debugPrint("$e"); return; + } finally { + clearWindowType(type); } } } diff --git a/flutter/lib/utils/platform_channel.dart b/flutter/lib/utils/platform_channel.dart index 1a36fb7a5..7b60ef63c 100644 --- a/flutter/lib/utils/platform_channel.dart +++ b/flutter/lib/utils/platform_channel.dart @@ -31,4 +31,10 @@ class RdPlatformChannel { return _osxMethodChannel .invokeMethod("setWindowTheme", {"themeName": theme.name}); } + + /// Terminate .app manually. + Future terminate() { + assert(Platform.isMacOS); + return _osxMethodChannel.invokeMethod("terminate"); + } } diff --git a/flutter/macos/Runner/AppDelegate.swift b/flutter/macos/Runner/AppDelegate.swift index 5708e35cb..3498decd3 100644 --- a/flutter/macos/Runner/AppDelegate.swift +++ b/flutter/macos/Runner/AppDelegate.swift @@ -3,21 +3,22 @@ import FlutterMacOS @NSApplicationMain class AppDelegate: FlutterAppDelegate { - var lauched = false; + var launched = false; override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { dummy_method_to_enforce_bundling() - return true + // https://github.com/leanflutter/window_manager/issues/214 + return false } override func applicationShouldOpenUntitledFile(_ sender: NSApplication) -> Bool { - if (lauched) { + if (launched) { handle_applicationShouldOpenUntitledFile(); } return true } override func applicationDidFinishLaunching(_ aNotification: Notification) { - lauched = true; + launched = true; NSApplication.shared.activate(ignoringOtherApps: true); } } diff --git a/flutter/macos/Runner/Info.plist b/flutter/macos/Runner/Info.plist index d1077e0e4..c926019ab 100644 --- a/flutter/macos/Runner/Info.plist +++ b/flutter/macos/Runner/Info.plist @@ -23,8 +23,10 @@ CFBundleTypeRole Editor - CFBundleURLName + CFBundleURLIconFile + CFBundleURLName + com.carriez.rustdesk CFBundleURLSchemes rustdesk @@ -35,13 +37,13 @@ $(FLUTTER_BUILD_NUMBER) LSMinimumSystemVersion $(MACOSX_DEPLOYMENT_TARGET) + LSUIElement + 1 NSHumanReadableCopyright $(PRODUCT_COPYRIGHT) NSMainNibFile MainMenu NSPrincipalClass - NSApplication - LSUIElement - 1 + NSApplication diff --git a/flutter/macos/Runner/MainFlutterWindow.swift b/flutter/macos/Runner/MainFlutterWindow.swift index cea1e94bb..042840569 100644 --- a/flutter/macos/Runner/MainFlutterWindow.swift +++ b/flutter/macos/Runner/MainFlutterWindow.swift @@ -78,6 +78,9 @@ class MainFlutterWindow: NSWindow { self.setWindowInterfaceMode(window: window,themeName: themeName ?? "light") result(nil) break; + case "terminate": + NSApplication.shared.terminate(self) + result(nil) default: result(FlutterMethodNotImplemented) } From c13c89c0d6f09a14daea21b4a2e4cf5dd4bd4dff Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 3 Feb 2023 18:52:22 +0800 Subject: [PATCH 1694/2015] fix: uni links cause main window show --- flutter/lib/common.dart | 17 ++++++++++------- flutter/lib/main.dart | 7 +++++-- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index c058ec434..7e22e0848 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1272,9 +1272,9 @@ Future restoreWindowPosition(WindowType type, {int? windowId}) async { /// [Availability] /// initUniLinks should only be used on macos/windows. /// we use dbus for linux currently. -Future initUniLinks() async { +Future initUniLinks() async { if (Platform.isLinux) { - return; + return false; } // Register uni links for Windows. The required info of url scheme is already // declared in `Info.plist` for macOS. @@ -1285,11 +1285,12 @@ Future initUniLinks() async { try { final initialLink = await getInitialLink(); if (initialLink == null) { - return; + return false; } - parseRustdeskUri(initialLink); + return parseRustdeskUri(initialLink); } catch (err) { debugPrintStack(label: "$err"); + return false; } } @@ -1310,11 +1311,13 @@ StreamSubscription? listenUniLinks() { return sub; } -/// Returns true if we successfully handle the startup arguments. +/// Handle command line arguments +/// +/// * Returns true if we successfully handle the startup arguments. bool checkArguments() { // bootArgs:[--connect, 362587269, --switch_uuid, e3d531cc-5dce-41e0-bd06-5d4a2b1eec05] // check connect args - final connectIndex = kBootArgs.indexOf("--connect"); + var connectIndex = kBootArgs.indexOf("--connect"); if (connectIndex == -1) { return false; } @@ -1368,7 +1371,7 @@ bool callUniLinksUriHandler(Uri uri) { Future.delayed(Duration.zero, () { rustDeskWinManager.newRemoteDesktop(peerId, switch_uuid: switch_uuid); }); - return false; + return true; } return false; } diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index b41cc17df..67a243eff 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -114,7 +114,6 @@ Future initEnv(String appType) async { void runMainApp(bool startService) async { // register uni links - initUniLinks(); await initEnv(kAppTypeMain); // trigger connection status updater await bind.mainCheckConnectStatus(); @@ -130,7 +129,11 @@ void runMainApp(bool startService) async { // Restore the location of the main window before window hide or show. await restoreWindowPosition(WindowType.Main); // Check the startup argument, if we successfully handle the argument, we keep the main window hidden. - if (checkArguments()) { + final handledByUniLinks = await initUniLinks(); + final handledByCli = checkArguments(); + debugPrint( + "handled by uni links: $handledByUniLinks, handled by cli: $handledByCli"); + if (handledByUniLinks || handledByCli) { windowManager.hide(); } else { windowManager.show(); From ca97826b80e09979f29f2c96993e6125d42e0e36 Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 3 Feb 2023 19:17:59 +0800 Subject: [PATCH 1695/2015] update cursor position when menu is dismissed Signed-off-by: fufesou --- flutter/lib/desktop/widgets/popup_menu.dart | 55 ++++++++++++++++--- .../lib/desktop/widgets/remote_menubar.dart | 45 ++++++++++++++- 2 files changed, 90 insertions(+), 10 deletions(-) diff --git a/flutter/lib/desktop/widgets/popup_menu.dart b/flutter/lib/desktop/widgets/popup_menu.dart index 0cbdad929..9833dcbca 100644 --- a/flutter/lib/desktop/widgets/popup_menu.dart +++ b/flutter/lib/desktop/widgets/popup_menu.dart @@ -109,13 +109,17 @@ class MenuConfig { this.boxWidth}); } +typedef DismissCallback = Function(); + abstract class MenuEntryBase { bool dismissOnClicked; + DismissCallback? dismissCallback; RxBool? enabled; MenuEntryBase({ this.dismissOnClicked = false, this.enabled, + this.dismissCallback, }); List> build(BuildContext context, MenuConfig conf); @@ -146,12 +150,14 @@ class MenuEntryRadioOption { String value; bool dismissOnClicked; RxBool? enabled; + DismissCallback? dismissCallback; MenuEntryRadioOption({ required this.text, required this.value, this.dismissOnClicked = false, this.enabled, + this.dismissCallback, }); } @@ -177,8 +183,13 @@ class MenuEntryRadios extends MenuEntryBase { required this.optionSetter, this.padding, dismissOnClicked = false, + dismissCallback, RxBool? enabled, - }) : super(dismissOnClicked: dismissOnClicked, enabled: enabled) { + }) : super( + dismissOnClicked: dismissOnClicked, + enabled: enabled, + dismissCallback: dismissCallback, + ) { () async { _curOption.value = await curOptionGetter(); }(); @@ -249,6 +260,9 @@ class MenuEntryRadios extends MenuEntryBase { onPressed() { if (opt.dismissOnClicked && Navigator.canPop(context)) { Navigator.pop(context); + if (opt.dismissCallback != null) { + opt.dismissCallback!(); + } } setOption(opt.value); } @@ -360,6 +374,9 @@ class MenuEntrySubRadios extends MenuEntryBase { onPressed: () { if (opt.dismissOnClicked && Navigator.canPop(context)) { Navigator.pop(context); + if (opt.dismissCallback != null) { + opt.dismissCallback!(); + } } setOption(opt.value); }, @@ -421,7 +438,12 @@ abstract class MenuEntrySwitchBase extends MenuEntryBase { this.textStyle, this.padding, RxBool? enabled, - }) : super(dismissOnClicked: dismissOnClicked, enabled: enabled); + dismissCallback, + }) : super( + dismissOnClicked: dismissOnClicked, + enabled: enabled, + dismissCallback: dismissCallback, + ); RxBool get curOption; Future setOption(bool? option); @@ -463,6 +485,9 @@ abstract class MenuEntrySwitchBase extends MenuEntryBase { if (super.dismissOnClicked && Navigator.canPop(context)) { Navigator.pop(context); + if (super.dismissCallback != null) { + super.dismissCallback!(); + } } setOption(v); }, @@ -474,6 +499,9 @@ abstract class MenuEntrySwitchBase extends MenuEntryBase { if (super.dismissOnClicked && Navigator.canPop(context)) { Navigator.pop(context); + if (super.dismissCallback != null) { + super.dismissCallback!(); + } } setOption(v); }, @@ -485,6 +513,9 @@ abstract class MenuEntrySwitchBase extends MenuEntryBase { onPressed: () { if (super.dismissOnClicked && Navigator.canPop(context)) { Navigator.pop(context); + if (super.dismissCallback != null) { + super.dismissCallback!(); + } } setOption(!curOption.value); }, @@ -508,6 +539,7 @@ class MenuEntrySwitch extends MenuEntrySwitchBase { EdgeInsets? padding, dismissOnClicked = false, RxBool? enabled, + dismissCallback, }) : super( switchType: switchType, text: text, @@ -515,6 +547,7 @@ class MenuEntrySwitch extends MenuEntrySwitchBase { padding: padding, dismissOnClicked: dismissOnClicked, enabled: enabled, + dismissCallback: dismissCallback, ) { () async { _curOption.value = await getter(); @@ -551,12 +584,15 @@ class MenuEntrySwitch2 extends MenuEntrySwitchBase { EdgeInsets? padding, dismissOnClicked = false, RxBool? enabled, + dismissCallback, }) : super( - switchType: switchType, - text: text, - textStyle: textStyle, - padding: padding, - dismissOnClicked: dismissOnClicked); + switchType: switchType, + text: text, + textStyle: textStyle, + padding: padding, + dismissOnClicked: dismissOnClicked, + dismissCallback: dismissCallback, + ); @override RxBool get curOption => getter(); @@ -627,9 +663,11 @@ class MenuEntryButton extends MenuEntryBase { this.padding, dismissOnClicked = false, RxBool? enabled, + dismissCallback, }) : super( dismissOnClicked: dismissOnClicked, enabled: enabled, + dismissCallback: dismissCallback, ); Widget _buildChild(BuildContext context, MenuConfig conf) { @@ -641,6 +679,9 @@ class MenuEntryButton extends MenuEntryBase { ? () { if (super.dismissOnClicked && Navigator.canPop(context)) { Navigator.pop(context); + if (super.dismissCallback != null) { + super.dismissCallback!(); + } } proc(); } diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 2a84dcf14..5b418bcc4 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -231,6 +231,8 @@ class _RemoteMenubarState extends State { ); } + _menuDismissCallback() => widget.ffi.inputModel.refreshMousePos(); + Widget _buildMenubar(BuildContext context) { final List menubarItems = []; if (!isWebDesktop) { @@ -374,6 +376,7 @@ class _RemoteMenubarState extends State { onPressed: () { if (Navigator.canPop(context)) { Navigator.pop(context); + _menuDismissCallback(); } RxInt display = CurrentDisplayState.find(widget.id); if (display.value != i) { @@ -551,6 +554,7 @@ class _RemoteMenubarState extends State { onPressed: () { if (Navigator.canPop(context)) { Navigator.pop(context); + _menuDismissCallback(); } showSetOSPassword( widget.id, false, widget.ffi.dialogManager); @@ -563,6 +567,7 @@ class _RemoteMenubarState extends State { }, padding: padding, dismissOnClicked: true, + dismissCallback: _menuDismissCallback, ), MenuEntryButton( childBuilder: (TextStyle? style) => Text( @@ -574,6 +579,7 @@ class _RemoteMenubarState extends State { }, padding: padding, dismissOnClicked: true, + dismissCallback: _menuDismissCallback, ), MenuEntryButton( childBuilder: (TextStyle? style) => Text( @@ -585,6 +591,7 @@ class _RemoteMenubarState extends State { connect(context, widget.id, isTcpTunneling: true); }, dismissOnClicked: true, + dismissCallback: _menuDismissCallback, ), ]); // {handler.get_audit_server() &&

  • {translate('Note')}
  • } @@ -602,6 +609,7 @@ class _RemoteMenubarState extends State { }, padding: padding, dismissOnClicked: true, + dismissCallback: _menuDismissCallback, ), ); } @@ -618,6 +626,7 @@ class _RemoteMenubarState extends State { }, padding: padding, dismissOnClicked: true, + dismissCallback: _menuDismissCallback, )); } } @@ -635,6 +644,7 @@ class _RemoteMenubarState extends State { }, padding: padding, dismissOnClicked: true, + dismissCallback: _menuDismissCallback, )); } @@ -649,6 +659,7 @@ class _RemoteMenubarState extends State { }, padding: padding, dismissOnClicked: true, + dismissCallback: _menuDismissCallback, )); if (pi.platform == kPeerPlatformWindows) { @@ -667,6 +678,7 @@ class _RemoteMenubarState extends State { }, padding: padding, dismissOnClicked: true, + dismissCallback: _menuDismissCallback, )); } if (pi.platform != kPeerPlatformAndroid && @@ -681,6 +693,7 @@ class _RemoteMenubarState extends State { showConfirmSwitchSidesDialog(widget.id, widget.ffi.dialogManager), padding: padding, dismissOnClicked: true, + dismissCallback: _menuDismissCallback, )); } } @@ -696,6 +709,7 @@ class _RemoteMenubarState extends State { }, padding: padding, dismissOnClicked: true, + dismissCallback: _menuDismissCallback, )); } @@ -717,6 +731,7 @@ class _RemoteMenubarState extends State { // }, // padding: padding, // dismissOnClicked: true, + // dismissCallback: _menuDismissCallback, // )); // } } @@ -762,11 +777,13 @@ class _RemoteMenubarState extends State { text: translate('Scale original'), value: kRemoteViewStyleOriginal, dismissOnClicked: true, + dismissCallback: _menuDismissCallback, ), MenuEntryRadioOption( text: translate('Scale adaptive'), value: kRemoteViewStyleAdaptive, dismissOnClicked: true, + dismissCallback: _menuDismissCallback, ), ], curOptionGetter: () async { @@ -782,6 +799,7 @@ class _RemoteMenubarState extends State { }, padding: padding, dismissOnClicked: true, + dismissCallback: _menuDismissCallback, ), MenuEntryDivider(), MenuEntryRadios( @@ -791,21 +809,26 @@ class _RemoteMenubarState extends State { text: translate('Good image quality'), value: kRemoteImageQualityBest, dismissOnClicked: true, + dismissCallback: _menuDismissCallback, ), MenuEntryRadioOption( text: translate('Balanced'), value: kRemoteImageQualityBalanced, dismissOnClicked: true, + dismissCallback: _menuDismissCallback, ), MenuEntryRadioOption( text: translate('Optimize reaction time'), value: kRemoteImageQualityLow, dismissOnClicked: true, + dismissCallback: _menuDismissCallback, ), MenuEntryRadioOption( - text: translate('Custom'), - value: kRemoteImageQualityCustom, - dismissOnClicked: true), + text: translate('Custom'), + value: kRemoteImageQualityCustom, + dismissOnClicked: true, + dismissCallback: _menuDismissCallback, + ), ], curOptionGetter: () async => // null means peer id is not found, which there's no need to care about @@ -970,12 +993,14 @@ class _RemoteMenubarState extends State { text: translate('ScrollAuto'), value: kRemoteScrollStyleAuto, dismissOnClicked: true, + dismissCallback: _menuDismissCallback, enabled: widget.ffi.canvasModel.imageOverflow, ), MenuEntryRadioOption( text: translate('Scrollbar'), value: kRemoteScrollStyleBar, dismissOnClicked: true, + dismissCallback: _menuDismissCallback, enabled: widget.ffi.canvasModel.imageOverflow, ), ], @@ -988,6 +1013,7 @@ class _RemoteMenubarState extends State { }, padding: padding, dismissOnClicked: true, + dismissCallback: _menuDismissCallback, )); displayMenu.insert(3, MenuEntryDivider()); @@ -1058,6 +1084,7 @@ class _RemoteMenubarState extends State { }, padding: padding, dismissOnClicked: true, + dismissCallback: _menuDismissCallback, ), ); } @@ -1084,11 +1111,13 @@ class _RemoteMenubarState extends State { text: translate('Auto'), value: 'auto', dismissOnClicked: true, + dismissCallback: _menuDismissCallback, ), MenuEntryRadioOption( text: 'VP9', value: 'vp9', dismissOnClicked: true, + dismissCallback: _menuDismissCallback, ), ]; if (codecs[0]) { @@ -1096,6 +1125,7 @@ class _RemoteMenubarState extends State { text: 'H264', value: 'h264', dismissOnClicked: true, + dismissCallback: _menuDismissCallback, )); } if (codecs[1]) { @@ -1103,6 +1133,7 @@ class _RemoteMenubarState extends State { text: 'H265', value: 'h265', dismissOnClicked: true, + dismissCallback: _menuDismissCallback, )); } return list; @@ -1119,6 +1150,7 @@ class _RemoteMenubarState extends State { }, padding: padding, dismissOnClicked: true, + dismissCallback: _menuDismissCallback, )); } } @@ -1141,6 +1173,7 @@ class _RemoteMenubarState extends State { }, padding: padding, dismissOnClicked: true, + dismissCallback: _menuDismissCallback, ); }()); } @@ -1163,6 +1196,7 @@ class _RemoteMenubarState extends State { }, padding: padding, dismissOnClicked: true, + dismissCallback: _menuDismissCallback, ); }()); } @@ -1182,6 +1216,7 @@ class _RemoteMenubarState extends State { }, padding: padding, dismissOnClicked: true, + dismissCallback: _menuDismissCallback, )); final perms = widget.ffi.ffiModel.permissions; @@ -1219,6 +1254,7 @@ class _RemoteMenubarState extends State { }, padding: padding, dismissOnClicked: true, + dismissCallback: _menuDismissCallback, )); } } @@ -1290,6 +1326,7 @@ class _RemoteMenubarState extends State { onPressed: () { if (Navigator.canPop(context)) { Navigator.pop(context); + _menuDismissCallback(); } showKBLayoutTypeChooser( localPlatform, widget.ffi.dialogManager); @@ -1302,6 +1339,7 @@ class _RemoteMenubarState extends State { proc: () {}, padding: EdgeInsets.zero, dismissOnClicked: false, + dismissCallback: _menuDismissCallback, ), ); } @@ -1321,6 +1359,7 @@ class _RemoteMenubarState extends State { }, padding: padding, dismissOnClicked: dismissOnClicked, + dismissCallback: _menuDismissCallback, ); } } From 0940c93a481b90652af890e320ace5c4e00dde3e Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 3 Feb 2023 20:27:05 +0800 Subject: [PATCH 1696/2015] show cursor on conn is established Signed-off-by: fufesou --- flutter/lib/models/model.dart | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 8a7a1005d..d032719e9 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -904,10 +904,10 @@ class CursorModel with ChangeNotifier { double _hoty = 0; double _displayOriginX = 0; double _displayOriginY = 0; - bool _firstUpdateMousePos = false; + DateTime? _firstUpdateMouseTime; bool gotMouseControl = true; DateTime _lastPeerMouse = DateTime.now() - .subtract(Duration(milliseconds: 2 * kMouseControlTimeoutMSec)); + .subtract(Duration(milliseconds: 3000 * kMouseControlTimeoutMSec)); String id = ''; WeakReference parent; @@ -926,6 +926,15 @@ class CursorModel with ChangeNotifier { DateTime.now().difference(_lastPeerMouse).inMilliseconds < kMouseControlTimeoutMSec; + bool isConnIn2Secs() { + if (_firstUpdateMouseTime == null) { + _firstUpdateMouseTime = DateTime.now(); + return true; + } else { + return DateTime.now().difference(_firstUpdateMouseTime!).inSeconds < 2; + } + } + CursorModel(this.parent); Set get cachedKeys => _cacheKeys; @@ -1122,12 +1131,10 @@ class CursorModel with ChangeNotifier { /// Update the cursor position. updateCursorPosition(Map evt, String id) async { - if (!_firstUpdateMousePos) { - _firstUpdateMousePos = true; - } else { + if (!isConnIn2Secs()) { gotMouseControl = false; + _lastPeerMouse = DateTime.now(); } - _lastPeerMouse = DateTime.now(); _x = double.parse(evt['x']); _y = double.parse(evt['y']); try { From 0d36166ea88847510426433dee115e17c693fe25 Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 3 Feb 2023 23:07:47 +0800 Subject: [PATCH 1697/2015] sync option after toggle Signed-off-by: fufesou --- flutter/lib/desktop/pages/remote_tab_page.dart | 6 +++--- flutter/lib/desktop/widgets/remote_menubar.dart | 7 ++++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index 55124fbcc..d832db0c6 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -273,6 +273,7 @@ class _ConnectionTabPageState extends State { menu.add(MenuEntryDivider()); menu.add(() { final state = ShowRemoteCursorState.find(key); + final optKey = 'show-remote-cursor'; return MenuEntrySwitch2( switchType: SwitchType.scheckbox, text: translate('Show remote cursor'), @@ -280,9 +281,8 @@ class _ConnectionTabPageState extends State { return state; }, setter: (bool v) async { - state.value = v; - await bind.sessionToggleOption( - id: key, value: 'show-remote-cursor'); + await bind.sessionToggleOption(id: key, value: optKey); + state.value = bind.sessionGetToggleOptionSync(id: key, arg: optKey); cancelFunc(); }, padding: padding, diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 5b418bcc4..d6b1cec72 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -1160,6 +1160,7 @@ class _RemoteMenubarState extends State { if (!widget.ffi.canvasModel.cursorEmbedded) { displayMenu.add(() { final state = ShowRemoteCursorState.find(widget.id); + final optKey = 'show-remote-cursor'; return MenuEntrySwitch2( switchType: SwitchType.scheckbox, text: translate('Show remote cursor'), @@ -1167,9 +1168,9 @@ class _RemoteMenubarState extends State { return state; }, setter: (bool v) async { - state.value = v; - await bind.sessionToggleOption( - id: widget.id, value: 'show-remote-cursor'); + await bind.sessionToggleOption(id: widget.id, value: optKey); + state.value = + bind.sessionGetToggleOptionSync(id: widget.id, arg: optKey); }, padding: padding, dismissOnClicked: true, From 96a7182ff85ce35f47d46a4c5ff8c9a3258bad15 Mon Sep 17 00:00:00 2001 From: solokot Date: Fri, 3 Feb 2023 20:05:48 +0300 Subject: [PATCH 1698/2015] update ru.rs --- src/lang/ru.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 7ec6c1554..54b064c18 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -436,14 +436,14 @@ 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", "Закрыто по ожиданию"), - ("Display", ""), - ("Default View Style", ""), - ("Default Scroll Style", ""), - ("Default Image Quality", ""), - ("Default Codec", ""), - ("Bitrate", ""), - ("FPS", ""), - ("Auto", ""), - ("Other Default Options", ""), + ("Display", "Отображение"), + ("Default View Style", "Стиль отображения по умолчанию"), + ("Default Scroll Style", "Стиль прокрутки по умолчанию"), + ("Default Image Quality", "Качество изображения по умолчанию"), + ("Default Codec", "Кодек по умолчанию"), + ("Bitrate", "Битрейт"), + ("FPS", "FPS"), + ("Auto", "Авто"), + ("Other Default Options", "Другие параметры по умолчанию"), ].iter().cloned().collect(); } From f9d106ea745017b1c7c2e8c69b2ea1ba6dc670c4 Mon Sep 17 00:00:00 2001 From: Mr-Update <37781396+Mr-Update@users.noreply.github.com> Date: Fri, 3 Feb 2023 22:36:50 +0100 Subject: [PATCH 1699/2015] Update de.rs --- src/lang/de.rs | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/lang/de.rs b/src/lang/de.rs index 5b68c0e7a..2d6d3d069 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -200,7 +200,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Warning", "Warnung"), ("Login screen using Wayland is not supported", "Anmeldebildschirm mit Wayland wird nicht unterstützt."), ("Reboot required", "Neustart erforderlich"), - ("Unsupported display server ", "Nicht unterstützter Display-Server"), + ("Unsupported display server ", "Nicht unterstützter Anzeigeserver"), ("x11 expected", "X11 erwartet"), ("Port", "Port"), ("Settings", "Einstellungen"), @@ -327,7 +327,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Mobile Actions", "Mobile Aktionen"), ("Select Monitor", "Bildschirm auswählen"), ("Control Actions", "Aktionen"), - ("Display Settings", "Bildschirmeinstellungen"), + ("Display Settings", "Anzeigeeinstellungen"), ("Ratio", "Verhältnis"), ("Image Quality", "Bildqualität"), ("Scroll Style", "Scroll-Stil"), @@ -338,7 +338,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Secure Connection", "Sichere Verbindung"), ("Insecure Connection", "Unsichere Verbindung"), ("Scale original", "Keine Skalierung"), - ("Scale adaptive", "Automatische Skalierung"), + ("Scale adaptive", "Anpassbare Skalierung"), ("General", "Allgemein"), ("Security", "Sicherheit"), ("Theme", "Farbgebung"), @@ -358,7 +358,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clear", "Zurücksetzen"), ("Audio Input Device", "Audioeingabegerät"), ("Deny remote access", "Fernzugriff verbieten"), - ("Use IP Whitelisting", "IP-Whitelist benutzen"), + ("Use IP Whitelisting", "IP-Whitelist verwenden"), ("Network", "Netzwerk"), ("Enable RDP", "RDP aktivieren"), ("Pin menubar", "Menüleiste anpinnen"), @@ -436,14 +436,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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", ""), - ("Default View Style", ""), - ("Default Scroll Style", ""), - ("Default Image Quality", ""), - ("Default Codec", ""), - ("Bitrate", ""), - ("FPS", ""), - ("Auto", ""), - ("Other Default Options", ""), + ("Display", "Anzeige"), + ("Default View Style", "Standard-Ansichtsstil"), + ("Default Scroll Style", "Standard-Scroll-Stil"), + ("Default Image Quality", "Standard-Bildqualität"), + ("Default Codec", "Standard-Codec"), + ("Bitrate", "Bitrate"), + ("FPS", "fps"), + ("Auto", "Automatisch"), + ("Other Default Options", "Weitere Standardoptionen"), ].iter().cloned().collect(); } From 3a1b9781124e0a59f64c5ea4cbc30ebdf1fe742d Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sat, 4 Feb 2023 01:10:32 +0800 Subject: [PATCH 1700/2015] feat: add event handler on rust macos --- Cargo.lock | 41 +++++++++++++++++-- Cargo.toml | 2 + .../macos/Runner.xcodeproj/project.pbxproj | 5 ++- flutter/macos/Runner/MainFlutterWindow.swift | 1 - src/ui/macos.rs | 27 ++++++++++-- 5 files changed, 68 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5c4af56e9..e15641363 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1137,7 +1137,7 @@ checksum = "413487ef345ab5cdfbf23e66070741217a701bce70f2f397a54221b4f2b6056a" dependencies = [ "dconf_rs", "detect-desktop-environment", - "dirs", + "dirs 4.0.0", "objc", "rust-ini", "web-sys", @@ -1401,6 +1401,16 @@ dependencies = [ "dirs-sys-next", ] +[[package]] +name = "dirs" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13aea89a5c93364a98e9b37b2fa237effbb694d5cfe01c5b70941f7eb087d5e3" +dependencies = [ + "cfg-if 0.1.10", + "dirs-sys", +] + [[package]] name = "dirs" version = "4.0.0" @@ -1873,6 +1883,19 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fruitbasket" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "898289b8e0528c84fb9b88f15ac9d5109bcaf23e0e49bb6f64deee0d86b6a351" +dependencies = [ + "dirs 2.0.2", + "objc", + "objc-foundation", + "objc_id", + "time 0.1.45", +] + [[package]] name = "fuchsia-cprng" version = "0.1.1" @@ -3657,6 +3680,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" dependencies = [ "malloc_buf", + "objc_exception", ] [[package]] @@ -3670,6 +3694,15 @@ dependencies = [ "objc_id", ] +[[package]] +name = "objc_exception" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" +dependencies = [ + "cc", +] + [[package]] name = "objc_id" version = "0.1.1" @@ -4655,6 +4688,7 @@ dependencies = [ "flexi_logger", "flutter_rust_bridge", "flutter_rust_bridge_codegen", + "fruitbasket", "glib 0.16.5", "gtk", "hbb_common", @@ -4673,6 +4707,7 @@ dependencies = [ "mouce", "num_cpus", "objc", + "objc_id", "parity-tokio-ipc", "rdev", "repng", @@ -4713,7 +4748,7 @@ name = "rustdesk-portable-packer" version = "0.1.0" dependencies = [ "brotli", - "dirs", + "dirs 4.0.0", "embed-resource", "md5", ] @@ -6591,7 +6626,7 @@ dependencies = [ "async-trait", "byteorder", "derivative", - "dirs", + "dirs 4.0.0", "enumflags2", "event-listener", "futures-core", diff --git a/Cargo.toml b/Cargo.toml index 1e9af30e5..936b9e349 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -106,6 +106,8 @@ core-graphics = "0.22" include_dir = "0.7.2" tray-item = "0.7" # looks better than trayicon dark-light = "0.2" +fruitbasket = "0.10.0" +objc_id = "0.1.1" [target.'cfg(target_os = "linux")'.dependencies] psimple = { package = "libpulse-simple-binding", version = "2.25" } diff --git a/flutter/macos/Runner.xcodeproj/project.pbxproj b/flutter/macos/Runner.xcodeproj/project.pbxproj index 7a17c3de1..066560203 100644 --- a/flutter/macos/Runner.xcodeproj/project.pbxproj +++ b/flutter/macos/Runner.xcodeproj/project.pbxproj @@ -227,7 +227,7 @@ TargetAttributes = { 33CC10EC2044A3C60003C045 = { CreatedOnToolsVersion = 9.2; - LastSwiftMigration = 1100; + LastSwiftMigration = 1420; ProvisioningStyle = Automatic; SystemCapabilities = { com.apple.Sandbox = { @@ -463,6 +463,7 @@ MACOSX_DEPLOYMENT_TARGET = 10.14; PRODUCT_BUNDLE_IDENTIFIER = com.carriez.rustdesk; PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; }; name = Profile; @@ -607,6 +608,7 @@ MACOSX_DEPLOYMENT_TARGET = 10.14; PRODUCT_BUNDLE_IDENTIFIER = com.carriez.rustdesk; PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; "SWIFT_OBJC_BRIDGING_HEADER[arch=*]" = Runner/bridge_generated.h; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -643,6 +645,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.carriez.rustdesk; PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; "SWIFT_OBJC_BRIDGING_HEADER[arch=*]" = Runner/bridge_generated.h; SWIFT_VERSION = 5.0; }; diff --git a/flutter/macos/Runner/MainFlutterWindow.swift b/flutter/macos/Runner/MainFlutterWindow.swift index 042840569..97b46bb84 100644 --- a/flutter/macos/Runner/MainFlutterWindow.swift +++ b/flutter/macos/Runner/MainFlutterWindow.swift @@ -87,4 +87,3 @@ class MainFlutterWindow: NSWindow { }) } } - diff --git a/src/ui/macos.rs b/src/ui/macos.rs index 7daef8eab..39812cf90 100644 --- a/src/ui/macos.rs +++ b/src/ui/macos.rs @@ -1,3 +1,5 @@ +use std::{ffi::c_void, rc::Rc}; + #[cfg(target_os = "macos")] use cocoa::{ appkit::{NSApp, NSApplication, NSApplicationActivationPolicy::*, NSMenu, NSMenuItem}, @@ -8,11 +10,14 @@ use objc::{ class, declare::ClassDecl, msg_send, - runtime::{Object, Sel, BOOL}, + runtime::{BOOL, Object, Sel}, sel, sel_impl, }; -use sciter::{make_args, Host}; -use std::{ffi::c_void, rc::Rc}; +use objc::runtime::Class; +use objc_id::WeakId; +use sciter::{Host, make_args}; + +use hbb_common::log; static APP_HANDLER_IVAR: &str = "GoDeskAppHandler"; @@ -98,12 +103,21 @@ 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)); let decl = decl.register(); let delegate: id = msg_send![decl, alloc]; let () = msg_send![delegate, init]; let state = DelegateState { handler }; let handler_ptr = Box::into_raw(Box::new(state)); (*delegate).set_ivar(APP_HANDLER_IVAR, handler_ptr as *mut c_void); + // Set the url scheme handler + let cls = Class::get("NSAppleEventManager").unwrap(); + let manager: *mut Object = msg_send![cls, sharedAppleEventManager]; + let _: () = msg_send![manager, + setEventHandler: delegate + andSelector: sel!(handleEvent:withReplyEvent:) + forEventClass: fruitbasket::kInternetEventClass + andEventID: fruitbasket::kAEGetURL]; let () = msg_send![NSApp(), setDelegate: delegate]; } @@ -167,6 +181,13 @@ extern "C" fn handle_menu_item(this: &mut Object, _: Sel, item: id) { } } +extern 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!("event found {}", url); + let _ = crate::run_me(vec![url]); +} + unsafe fn make_menu_item(title: &str, key: &str, tag: u32) -> *mut Object { let title = NSString::alloc(nil).init_str(title); let action = sel!(handleMenuItem:); From 7e69cbde26a1a62f3e319fdfebd12637a2dd2956 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sat, 4 Feb 2023 01:22:40 +0800 Subject: [PATCH 1701/2015] opt: support binary + uri links startup --- flutter/lib/common.dart | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 7e22e0848..9f3e2c740 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1315,6 +1315,12 @@ StreamSubscription? listenUniLinks() { /// /// * Returns true if we successfully handle the startup arguments. bool checkArguments() { + if (kBootArgs.isNotEmpty) { + final ret = parseRustdeskUri(kBootArgs.first); + if (ret) { + return true; + } + } // bootArgs:[--connect, 362587269, --switch_uuid, e3d531cc-5dce-41e0-bd06-5d4a2b1eec05] // check connect args var connectIndex = kBootArgs.indexOf("--connect"); @@ -1352,7 +1358,7 @@ bool checkArguments() { bool parseRustdeskUri(String uriPath) { final uri = Uri.tryParse(uriPath); if (uri == null) { - print("uri is not valid: $uriPath"); + debugPrint("uri is not valid: $uriPath"); return false; } return callUniLinksUriHandler(uri); From a9fc63c34f6f0bb18701e9597d1a5d8568c35ccc Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sat, 4 Feb 2023 01:31:56 +0800 Subject: [PATCH 1702/2015] opt: add default url scheme handler for macos --- src/flutter_ffi.rs | 31 ++++++++++++++++++------------- src/ui/macos.rs | 9 +++++++-- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index d40c66d19..d001dd388 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -3,28 +3,29 @@ use std::{ ffi::{CStr, CString}, os::raw::c_char, }; +use std::str::FromStr; use flutter_rust_bridge::{StreamSink, SyncReturn, ZeroCopyBuffer}; use serde_json::json; -use crate::common::is_keyboard_mode_supported; -use hbb_common::message_proto::KeyboardMode; -use hbb_common::ResultType; use hbb_common::{ - config::{self, LocalConfig, PeerConfig, ONLINE}, + config::{self, LocalConfig, ONLINE, PeerConfig}, fs, log, }; -use std::str::FromStr; +use hbb_common::message_proto::KeyboardMode; +use hbb_common::ResultType; -// use crate::hbbs_http::account::AuthResult; - -use crate::flutter::{self, SESSIONS}; -use crate::ui_interface::{self, *}; 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, *}; + +// use crate::hbbs_http::account::AuthResult; + fn initialize(app_dir: &str) { *config::APP_DIR.write().unwrap() = app_dir.to_owned(); #[cfg(target_os = "android")] @@ -910,6 +911,11 @@ pub fn main_start_dbus_server() { } } +pub fn osx_handle_uni_links(url: String) { + #![cfg(target_os = "macos")] + crate::ui::macos::handle_url_scheme(url); +} + pub fn session_send_mouse(id: String, msg: String) { if let Ok(m) = serde_json::from_str::>(&msg) { let alt = m.get("alt").is_some(); @@ -1257,13 +1263,12 @@ pub fn main_hide_docker() -> SyncReturn { #[cfg(target_os = "android")] pub mod server_side { + use hbb_common::log; use jni::{ + JNIEnv, objects::{JClass, JString}, sys::jstring, - JNIEnv, - }; - - use hbb_common::log; + }; use crate::start_server; diff --git a/src/ui/macos.rs b/src/ui/macos.rs index 39812cf90..94e75959c 100644 --- a/src/ui/macos.rs +++ b/src/ui/macos.rs @@ -181,11 +181,16 @@ extern "C" fn handle_menu_item(this: &mut Object, _: Sel, item: id) { } } -extern fn handle_apple_event(this: &Object, _cmd: Sel, event: u64, _reply: u64) { +/// The function to handle the url scheme sent by system. +pub fn handle_url_scheme(url: String) { + unimplemented!(); +} + +extern 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!("event found {}", url); - let _ = crate::run_me(vec![url]); + handle_url_scheme(url); } unsafe fn make_menu_item(title: &str, key: &str, tag: u32) -> *mut Object { From 4dfae8da1075450346ae72927faac8fc659027d5 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sat, 4 Feb 2023 11:23:36 +0800 Subject: [PATCH 1703/2015] feat: add url scheme handler for macos --- flutter/lib/consts.dart | 3 +- flutter/lib/main.dart | 2 +- flutter/lib/models/model.dart | 3 ++ flutter/lib/models/native_model.dart | 10 +++- src/flutter_ffi.rs | 25 +++++++--- src/ipc.rs | 11 +++- src/server.rs | 75 ++++++++++++++++++++++------ src/ui/macos.rs | 18 +++++-- 8 files changed, 116 insertions(+), 31 deletions(-) diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index f48b612a8..1fc97f410 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -11,8 +11,9 @@ const String kPeerPlatformLinux = "Linux"; const String kPeerPlatformMacOS = "Mac OS"; const String kPeerPlatformAndroid = "Android"; -/// [kAppTypeMain] used by 'Desktop Main Page' , 'Mobile (Client and Server)' , 'Desktop CM Page', "Install Page" +/// [kAppTypeMain] used by 'Desktop Main Page' , 'Mobile (Client and Server)', "Install Page" const String kAppTypeMain = "main"; +const String kAppTypeConnectionManager = "cm"; const String kAppTypeDesktopRemote = "remote"; const String kAppTypeDesktopFileTransfer = "file transfer"; const String kAppTypeDesktopPortForward = "port forward"; diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 67a243eff..86cc9d89b 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -211,7 +211,7 @@ void runMultiWindow( } void runConnectionManagerScreen(bool hide) async { - await initEnv(kAppTypeMain); + await initEnv(kAppTypeConnectionManager); _runApp( '', const DesktopServerPage(), diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index d032719e9..aae4c6a07 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -199,6 +199,9 @@ class FfiModel with ChangeNotifier { final peer_id = evt['peer_id'].toString(); await bind.sessionSwitchSides(id: peer_id); closeConnection(id: peer_id); + } else if (name == "on_url_scheme_received") { + final url = evt['url'].toString(); + parseRustdeskUri(url); } }; } diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index cf2de4219..d6885bfb0 100644 --- a/flutter/lib/models/native_model.dart +++ b/flutter/lib/models/native_model.dart @@ -8,6 +8,7 @@ import 'package:external_path/external_path.dart'; import 'package:ffi/ffi.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_hbb/consts.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:path_provider/path_provider.dart'; import 'package:win32/win32.dart' as win32; @@ -46,6 +47,8 @@ class PlatformFFI { static get localeName => Platform.localeName; + static get isMain => instance._appType == kAppTypeMain; + static Future getVersion() async { PackageInfo packageInfo = await PackageInfo.fromPlatform(); return packageInfo.version; @@ -112,8 +115,11 @@ class PlatformFFI { } _ffiBind = RustdeskImpl(dylib); if (Platform.isLinux) { - // start dbus service, no need to await - await _ffiBind.mainStartDbusServer(); + // Start a dbus service, no need to await + _ffiBind.mainStartDbusServer(); + } else if (Platform.isMacOS) { + // Start an ipc server for handling url schemes. + _ffiBind.mainStartIpcUrlServer(); } _startListenEvent(_ffiBind); // global event try { diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index d001dd388..5dccd9050 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1,8 +1,4 @@ -use std::{ - collections::HashMap, - ffi::{CStr, CString}, - os::raw::c_char, -}; +use std::{collections::HashMap, ffi::{CStr, CString}, os::raw::c_char, thread}; use std::str::FromStr; use flutter_rust_bridge::{StreamSink, SyncReturn, ZeroCopyBuffer}; @@ -1261,6 +1257,23 @@ pub fn main_hide_docker() -> SyncReturn { SyncReturn(true) } +/// Start an ipc server for receiving the url scheme. +/// +/// * Should only be called in the main flutter window. +/// * macOS only +pub fn main_start_ipc_url_server() { + #[cfg(target_os = "macos")] + thread::spawn(move || crate::server::start_ipc_url_server()); +} + +/// Send a url scheme throught the ipc. +/// +/// * macOS only +pub fn send_url_scheme(url: String) { + #[cfg(target_os = "macos")] + thread::spawn(move || crate::ui::macos::handle_url_scheme(url)); +} + #[cfg(target_os = "android")] pub mod server_side { use hbb_common::log; @@ -1268,7 +1281,7 @@ pub mod server_side { JNIEnv, objects::{JClass, JString}, sys::jstring, - }; + }; use crate::start_server; diff --git a/src/ipc.rs b/src/ipc.rs index d4d803aec..d610fb84d 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, timeout, tokio, + log, password_security as password, ResultType, timeout, + tokio, tokio::io::{AsyncRead, AsyncWrite}, tokio_util::codec::Framed, - ResultType, }; use crate::rendezvous_mediator::RendezvousMediator; @@ -210,6 +210,7 @@ pub enum Data { DataPortableService(DataPortableService), SwitchSidesRequest(String), SwitchSidesBack, + UrlLink(String) } #[tokio::main(flavor = "current_thread")] @@ -832,3 +833,9 @@ pub async fn test_rendezvous_server() -> ResultType<()> { c.send(&Data::TestRendezvousServer).await?; Ok(()) } + +#[tokio::main(flavor = "current_thread")] +pub async fn send_url_scheme(url: String) -> ResultType<()> { + connect(1_000, "_url").await?.send(&Data::UrlLink(url)).await?; + Ok(()) +} diff --git a/src/server.rs b/src/server.rs index 381e3df90..de213ae5a 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,8 +1,13 @@ -use crate::ipc::Data; +use std::{ + collections::HashMap, + net::SocketAddr, + sync::{Arc, Mutex, RwLock, Weak}, + time::Duration, +}; + use bytes::Bytes; + pub use connection::*; -#[cfg(not(any(target_os = "android", target_os = "ios")))] -use hbb_common::config::Config2; use hbb_common::{ allow_err, anyhow::{anyhow, Context}, @@ -12,19 +17,19 @@ use hbb_common::{ message_proto::*, protobuf::{Enum, Message as _}, rendezvous_proto::*, + ResultType, socket_client, - sodiumoxide::crypto::{box_, secretbox, sign}, - timeout, tokio, ResultType, Stream, + sodiumoxide::crypto::{box_, secretbox, sign}, Stream, timeout, tokio, }; #[cfg(not(any(target_os = "android", target_os = "ios")))] -use service::ServiceTmpl; +use hbb_common::config::Config2; +use hbb_common::tcp::new_listener; use service::{GenericService, Service, Subscriber}; -use std::{ - collections::HashMap, - net::SocketAddr, - sync::{Arc, Mutex, RwLock, Weak}, - time::Duration, -}; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use service::ServiceTmpl; + +use crate::ipc::{connect, Data}; +use crate::ui_interface::SENDER; pub mod audio_service; cfg_if::cfg_if! { @@ -55,8 +60,6 @@ mod service; mod video_qos; pub mod video_service; -use hbb_common::tcp::new_listener; - pub type Childs = Arc>>; type ConnMap = HashMap; @@ -425,6 +428,50 @@ pub async fn start_server(is_server: bool) { } } +#[cfg(target_os = "macos")] +#[tokio::main(flavor = "current_thread")] +pub async fn start_ipc_url_server() { + log::debug!("Start an ipc server for listening to url schemes"); + match crate::ipc::new_listener("_url").await { + Ok(mut incoming) => { + 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.") + } + } + } + Err(err) => { + log::error!("{}", err); + } + _ => {} + } + } + } + Err(err) => { + log::error!("{}", err); + } + } +} + #[cfg(target_os = "macos")] async fn sync_and_watch_config_dir() { if crate::platform::is_root() { diff --git a/src/ui/macos.rs b/src/ui/macos.rs index 94e75959c..98e355dc1 100644 --- a/src/ui/macos.rs +++ b/src/ui/macos.rs @@ -17,7 +17,9 @@ use objc::runtime::Class; use objc_id::WeakId; use sciter::{Host, make_args}; -use hbb_common::log; +use hbb_common::{log, tokio}; + +use crate::ui_cm_interface::start_ipc; static APP_HANDLER_IVAR: &str = "GoDeskAppHandler"; @@ -181,16 +183,22 @@ extern "C" fn handle_menu_item(this: &mut Object, _: Sel, item: id) { } } -/// The function to handle the url scheme sent by system. +/// 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) { - unimplemented!(); + 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 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!("event found {}", url); - handle_url_scheme(url); + log::debug!("an event was received: {}", url); + std::thread::spawn(move || handle_url_scheme(url)); } unsafe fn make_menu_item(title: &str, key: &str, tag: u32) -> *mut Object { From a349be6428cca3675d777821b71554e4e49b0952 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sat, 4 Feb 2023 11:33:08 +0800 Subject: [PATCH 1704/2015] opt: remove unnecessary ffi func --- src/flutter_ffi.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 5dccd9050..ca9314c43 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -907,11 +907,6 @@ pub fn main_start_dbus_server() { } } -pub fn osx_handle_uni_links(url: String) { - #![cfg(target_os = "macos")] - crate::ui::macos::handle_url_scheme(url); -} - pub fn session_send_mouse(id: String, msg: String) { if let Ok(m) = serde_json::from_str::>(&msg) { let alt = m.get("alt").is_some(); From 151b115fc900ad15fd2fc79319e166b48c9b6661 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sat, 4 Feb 2023 13:37:48 +0800 Subject: [PATCH 1705/2015] fix: android build --- src/server.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/server.rs b/src/server.rs index de213ae5a..109fc1e9a 100644 --- a/src/server.rs +++ b/src/server.rs @@ -29,7 +29,6 @@ use service::{GenericService, Service, Subscriber}; use service::ServiceTmpl; use crate::ipc::{connect, Data}; -use crate::ui_interface::SENDER; pub mod audio_service; cfg_if::cfg_if! { From dd00ea5abd24be98addc5444294f52908cbc729f Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sat, 4 Feb 2023 16:18:54 +0800 Subject: [PATCH 1706/2015] opt: reuse current main window when using url scheme --- src/core_main.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/core_main.rs b/src/core_main.rs index 89a962f1d..99d0e888e 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -1,4 +1,6 @@ -use hbb_common::log; +use std::future::Future; + +use hbb_common::{log, ResultType}; /// shared by flutter and sciter main function /// @@ -346,5 +348,11 @@ fn core_main_invoke_new_connection(mut args: std::env::Args) -> Option Date: Sun, 5 Feb 2023 07:59:29 +0330 Subject: [PATCH 1707/2015] Update fa.rs --- src/lang/fa.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 72cde49f9..dd1c75bac 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -436,14 +436,14 @@ 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", "طبق انتظار بسته شد"), - ("Display", ""), - ("Default View Style", ""), - ("Default Scroll Style", ""), - ("Default Image Quality", ""), - ("Default Codec", ""), - ("Bitrate", ""), - ("FPS", ""), - ("Auto", ""), - ("Other Default Options", ""), + ("Display", "نمایش دادن"), + ("Default View Style", "سبک نمایش پیش فرض"), + ("Default Scroll Style", "سبک پیش‌فرض اسکرول"), + ("Default Image Quality", "کیفیت تصویر پیش فرض"), + ("Default Codec", "کدک پیش فرض"), + ("Bitrate", "میزان بیت صفحه نمایش"), + ("FPS", "FPS"), + ("Auto", "خودکار"), + ("Other Default Options", "سایر گزینه های پیش فرض"), ].iter().cloned().collect(); } From afb76c63261ac5d2f1602ec3d7627a1168ee11c6 Mon Sep 17 00:00:00 2001 From: mehdi-song Date: Sun, 5 Feb 2023 10:20:05 +0330 Subject: [PATCH 1708/2015] Update README-FA.md --- docs/README-FA.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/README-FA.md b/docs/README-FA.md index 02b156dbb..496e81849 100644 --- a/docs/README-FA.md +++ b/docs/README-FA.md @@ -1,6 +1,6 @@

    RustDesk - Your remote desktop
    -
    تصاویر محیط نرم‌افزار • + تصاویر محیط نرم‌افزارساختارداکرساخت • @@ -9,12 +9,12 @@

    [English] | [Українська] | [česky] | [中文] | [Magyar] | [Español] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]

    برای ترجمه این سند (README)، رابط کاربری RustDesk، و مستندات آن به زبان مادری شما به کمکتان نیازمندیم.

    -با ما گپ بزنید: [Reddit](https://www.reddit.com/r/rustdesk) | [Twitter](https://twitter.com/rustdesk) | [Discord](https://discord.gg/nDceKgxnkV) +با ما گفتگو کنید: [Reddit](https://www.reddit.com/r/rustdesk) | [Twitter](https://twitter.com/rustdesk) | [Discord](https://discord.gg/nDceKgxnkV) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) -راست‌دسک (RustDesk) نرم‌افزاری برای گارکردن با رایانه‌ی رومیزی از راه دور است و با زبان برنامه‌نویسی Rust نوشته شده است. نیاز به تنظیمات چندانی ندارد و شما را قادر می سازد تا بدون نگرانی از امنیت اطلاعات خود بر آن‌ها کنترل کامل داشته باشید. +راست‌دسک (RustDesk) نرم‌افزاری برای کارکردن با رایانه‌ی رومیزی از راه دور است و با زبان برنامه‌نویسی Rust نوشته شده است. نیاز به تنظیمات چندانی ندارد و شما را قادر می سازد تا بدون نگرانی از امنیت اطلاعات خود بر آن‌ها کنترل کامل داشته باشید. می‌توانید از سرور rendezvous/relay ما استفاده کنید، [سرور خودتان را راه‌اندازی کنید](https://rustdesk.com/server) یا [ سرورrendezvous/relay خود را بنویسید](https://github.com/rustdesk/rustdesk). @@ -130,7 +130,7 @@ cd rustdesk docker build -t "rustdesk-builder" . ``` -سپس، هر بار که نیاز به ساخت ترم‌افزار داشتید، دستور زیر را اجرا کنید: +سپس، هر بار که نیاز به ساخت نرم‌افزار داشتید، دستور زیر را اجرا کنید: ```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 From 3462756a11a8b69bed40246cd4a6362b291f7bfd Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sun, 5 Feb 2023 16:56:13 +0800 Subject: [PATCH 1709/2015] optimize dialog margin, fix password eye icon color --- flutter/lib/common.dart | 8 +++----- flutter/lib/consts.dart | 1 - flutter/lib/mobile/widgets/dialog.dart | 5 +---- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 9f3e2c740..8236597ff 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -694,7 +694,6 @@ void msgBox(String id, String type, String title, String text, String link, buttons.insert( 0, dialogButton('Cancel', onPressed: cancel, isOutline: true)); } - // TODO: test this button if (type.contains("hasclose")) { buttons.insert( 0, @@ -708,8 +707,7 @@ void msgBox(String id, String type, String title, String text, String link, dialogManager.show( (setState, close) => CustomAlertDialog( title: null, - content: SelectionArea( - child: msgboxContent(type, title, text).paddingOnly(bottom: 10)), + content: SelectionArea(child: msgboxContent(type, title, text)), actions: buttons, onSubmit: hasOk ? submit : null, onCancel: hasCancel == true ? cancel : null, @@ -774,7 +772,7 @@ Widget msgboxContent(String type, String title, String text) { ), ), ], - ); + ).marginOnly(bottom: 12); } void msgBoxCommon(OverlayDialogManager dialogManager, String title, @@ -1714,4 +1712,4 @@ Future updateSystemWindowTheme() async { : SystemWindowTheme.dark); } } -} \ No newline at end of file +} diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 1fc97f410..c95c62fcc 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -26,7 +26,6 @@ const String kWindowEventShow = "show"; const String kWindowConnect = "connect"; const String kUniLinksPrefix = "rustdesk://"; -const String kActionNewConnection = "connection/new/"; const String kTabLabelHomePage = "Home"; const String kTabLabelSettingPage = "Settings"; diff --git a/flutter/lib/mobile/widgets/dialog.dart b/flutter/lib/mobile/widgets/dialog.dart index bded6d069..2fbe40091 100644 --- a/flutter/lib/mobile/widgets/dialog.dart +++ b/flutter/lib/mobile/widgets/dialog.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'dart:convert'; import 'package:flutter/material.dart'; -import 'package:flutter_hbb/desktop/widgets/button.dart'; import 'package:get/get.dart'; import '../../common.dart'; @@ -371,8 +370,7 @@ void showWaitUacDialog( tag: '$id-wait-uac', (setState, close) => CustomAlertDialog( title: null, - content: msgboxContent(type, 'Wait', 'wait_accept_uac_tip') - .marginOnly(bottom: 10), + content: msgboxContent(type, 'Wait', 'wait_accept_uac_tip'), )); } @@ -647,7 +645,6 @@ class _PasswordWidgetState extends State { icon: Icon( // Based on passwordVisible state choose the icon _passwordVisible ? Icons.visibility : Icons.visibility_off, - color: Theme.of(context).primaryColorDark, ), onPressed: () { // Update the state i.e. toggle the state of passwordVisible variable From 255c58ef7b725ea64012073ae8d8cb48720d7b98 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sun, 5 Feb 2023 17:29:54 +0800 Subject: [PATCH 1710/2015] opt: close button color and corner on tab --- flutter/lib/desktop/widgets/tabbar_widget.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 223076951..cfbddbafb 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -959,7 +959,7 @@ class _CloseButton extends StatelessWidget { offstage: !visible, child: InkWell( hoverColor: MyTheme.tabbar(context).closeHoverColor, - customBorder: const RoundedRectangleBorder(), + customBorder: const CircleBorder(), onTap: () => onClose(), child: Icon( Icons.close, @@ -1082,7 +1082,7 @@ class TabbarTheme extends ThemeExtension { unSelectedIconColor: Color.fromARGB(255, 96, 96, 96), dividerColor: Color.fromARGB(255, 238, 238, 238), hoverColor: Color.fromARGB(51, 158, 158, 158), - closeHoverColor: Colors.black, + closeHoverColor: Color.fromARGB(255, 224, 224, 224), selectedTabBackgroundColor: Color.fromARGB(255, 240, 240, 240)); static const dark = TabbarTheme( From 133fba573bea02d9a29b64e879d522f13d331069 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sun, 5 Feb 2023 18:20:22 +0800 Subject: [PATCH 1711/2015] confirmed issue #2935 is false report, set_bitrate was called, and bandwidth has obvious change if you watch car game video --- src/server/video_service.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/video_service.rs b/src/server/video_service.rs index d041a433c..55920e320 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -498,7 +498,7 @@ fn run(sp: GenericService) -> ResultType<()> { video_qos.target_bitrate, video_qos.fps ); - encoder.set_bitrate(video_qos.target_bitrate).unwrap(); + allow_err!(encoder.set_bitrate(video_qos.target_bitrate)); spf = video_qos.spf(); } drop(video_qos); From bb0f481df31fc6121a2c5782c158d0d4a3d3f66c Mon Sep 17 00:00:00 2001 From: botanicvelious Date: Sun, 5 Feb 2023 17:00:22 -0700 Subject: [PATCH 1712/2015] update rust build action to use the same on all --- .github/workflows/flutter-nightly.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 83ad7629e..5ca284cee 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -59,10 +59,9 @@ jobs: - name: Install Rust toolchain uses: actions-rs/toolchain@v1 with: - toolchain: "1.62" + toolchain: stable target: ${{ matrix.job.target }} override: true - components: rustfmt profile: minimal # minimal component installation (ie, no documentation) - uses: Swatinem/rust-cache@v2 From c306ec3ba76217f24d66384f31c1d8fecb388291 Mon Sep 17 00:00:00 2001 From: csf Date: Mon, 6 Feb 2023 09:54:21 +0900 Subject: [PATCH 1713/2015] opt chat window on its overlay, make window focusable as a desktop app --- flutter/lib/common/widgets/overlay.dart | 64 ++++++++++++++----------- flutter/lib/models/chat_model.dart | 31 ++++++++++-- 2 files changed, 61 insertions(+), 34 deletions(-) diff --git a/flutter/lib/common/widgets/overlay.dart b/flutter/lib/common/widgets/overlay.dart index 4b4172ffd..d84789d9c 100644 --- a/flutter/lib/common/widgets/overlay.dart +++ b/flutter/lib/common/widgets/overlay.dart @@ -1,6 +1,7 @@ import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; +import 'package:get/get_state_manager/src/rx_flutter/rx_obx_widget.dart'; import 'package:provider/provider.dart'; import '../../consts.dart'; @@ -91,28 +92,31 @@ class DraggableChatWindow extends StatelessWidget { bottom: BorderSide( color: Theme.of(context).hintColor.withOpacity(0.4)))), height: 38, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 8), - child: Row(children: [ - Icon(Icons.chat_bubble_outline, - size: 20, color: Theme.of(context).colorScheme.primary), - SizedBox(width: 6), - Text(translate("Chat")) - ])), - Padding( - padding: EdgeInsets.all(2), - child: ActionIcon( - message: 'Close', - icon: IconFont.close, - onTap: chatModel.hideChatWindowOverlay, - isClose: true, - boxSize: 32, - )) - ], - ), + child: Obx(() => Opacity( + opacity: chatModel.isWindowFocus.value ? 1.0 : 0.4, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 15, vertical: 8), + child: Row(children: [ + Icon(Icons.chat_bubble_outline, + size: 20, color: Theme.of(context).colorScheme.primary), + SizedBox(width: 6), + Text(translate("Chat")) + ])), + Padding( + padding: EdgeInsets.all(2), + child: ActionIcon( + message: 'Close', + icon: IconFont.close, + onTap: chatModel.hideChatWindowOverlay, + isClose: true, + boxSize: 32, + )) + ], + ))), ); } } @@ -304,15 +308,17 @@ class _DraggableState extends State { if (widget.checkKeyboard) { checkKeyboard(); } - if (widget.checkKeyboard) { + if (widget.checkScreenSize) { checkScreenSize(); } - return Positioned( - top: _position.dy, - left: _position.dx, - width: widget.width, - height: widget.height, - child: widget.builder(context, onPanUpdate)); + return Stack(children: [ + Positioned( + top: _position.dy, + left: _position.dx, + width: widget.width, + height: widget.height, + child: widget.builder(context, onPanUpdate)) + ]); } } diff --git a/flutter/lib/models/chat_model.dart b/flutter/lib/models/chat_model.dart index bab88a9dd..dd35bd22f 100644 --- a/flutter/lib/models/chat_model.dart +++ b/flutter/lib/models/chat_model.dart @@ -4,6 +4,8 @@ import 'package:dash_chat_2/dash_chat_2.dart'; import 'package:draggable_float_widget/draggable_float_widget.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/models/platform_model.dart'; +import 'package:get/get_rx/src/rx_types/rx_types.dart'; +import 'package:get/get_state_manager/src/rx_flutter/rx_obx_widget.dart'; import 'package:window_manager/window_manager.dart'; import '../consts.dart'; @@ -37,6 +39,8 @@ class ChatModel with ChangeNotifier { OverlayEntry? chatWindowOverlayEntry; bool isConnManager = false; + RxBool isWindowFocus = true.obs; + final ChatUser me = ChatUser( id: "", firstName: "Me", @@ -133,11 +137,28 @@ class ChatModel with ChangeNotifier { final overlayState = _getOverlayState(); if (overlayState == null) return; final overlay = OverlayEntry(builder: (context) { - return DraggableChatWindow( - position: const Offset(20, 80), - width: 250, - height: 350, - chatModel: this); + bool innerClicked = false; + return Listener( + onPointerDown: (_) { + if (!innerClicked) { + isWindowFocus.value = false; + } + innerClicked = false; + }, + child: Obx(() => Container( + color: isWindowFocus.value ? Colors.red.withOpacity(0.3) : null, + child: Listener( + onPointerDown: (_) { + innerClicked = true; + if (!isWindowFocus.value) { + isWindowFocus.value = true; + } + }, + child: DraggableChatWindow( + position: const Offset(20, 80), + width: 250, + height: 350, + chatModel: this))))); }); overlayState.insert(overlay); chatWindowOverlayEntry = overlay; From 74dc2b253832e9cd6cab2f5c9e08d19d8a479b4b Mon Sep 17 00:00:00 2001 From: fufesou Date: Mon, 6 Feb 2023 11:27:20 +0800 Subject: [PATCH 1714/2015] refactor remote menu Signed-off-by: fufesou --- .../lib/desktop/pages/remote_tab_page.dart | 93 +----- .../lib/desktop/widgets/remote_menubar.dart | 278 ++++++++++++------ 2 files changed, 207 insertions(+), 164 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index d832db0c6..9b00b481f 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -243,96 +243,35 @@ class _ConnectionTabPageState extends State { padding: padding, ), MenuEntryDivider(), - MenuEntryRadios( - text: translate('Ratio'), - optionsGetter: () => [ - MenuEntryRadioOption( - text: translate('Scale original'), - value: kRemoteViewStyleOriginal, - dismissOnClicked: true, - ), - MenuEntryRadioOption( - text: translate('Scale adaptive'), - value: kRemoteViewStyleAdaptive, - dismissOnClicked: true, - ), - ], - curOptionGetter: () async => - // null means peer id is not found, which there's no need to care about - await bind.sessionGetViewStyle(id: key) ?? '', - optionSetter: (String oldValue, String newValue) async { - await bind.sessionSetViewStyle(id: key, value: newValue); - ffi.canvasModel.updateViewStyle(); - cancelFunc(); - }, - padding: padding, + RemoteMenuEntry.viewStyle( + key, + ffi, + padding, + dismissFunc: cancelFunc, ), ]); if (!ffi.canvasModel.cursorEmbedded) { menu.add(MenuEntryDivider()); - menu.add(() { - final state = ShowRemoteCursorState.find(key); - final optKey = 'show-remote-cursor'; - return MenuEntrySwitch2( - switchType: SwitchType.scheckbox, - text: translate('Show remote cursor'), - getter: () { - return state; - }, - setter: (bool v) async { - await bind.sessionToggleOption(id: key, value: optKey); - state.value = bind.sessionGetToggleOptionSync(id: key, arg: optKey); - cancelFunc(); - }, - padding: padding, - ); - }()); + menu.add(RemoteMenuEntry.showRemoteCursor( + key, + padding, + dismissFunc: cancelFunc, + )); } if (perms['keyboard'] != false) { if (perms['clipboard'] != false) { - menu.add(MenuEntrySwitch( - switchType: SwitchType.scheckbox, - text: translate('Disable clipboard'), - getter: () async { - return bind.sessionGetToggleOptionSync( - id: key, arg: 'disable-clipboard'); - }, - setter: (bool v) async { - await bind.sessionToggleOption(id: key, value: 'disable-clipboard'); - cancelFunc(); - }, - padding: padding, - )); + menu.add(RemoteMenuEntry.disableClipboard(key, padding, + dismissFunc: cancelFunc)); } - menu.add(MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - translate('Insert Lock'), - style: style, - ), - proc: () { - bind.sessionLockScreen(id: key); - cancelFunc(); - }, - padding: padding, - dismissOnClicked: true, - )); + menu.add( + RemoteMenuEntry.insertLock(key, padding, dismissFunc: cancelFunc)); if (pi.platform == kPeerPlatformLinux || pi.sasEnabled) { - menu.add(MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - '${translate("Insert")} Ctrl + Alt + Del', - style: style, - ), - proc: () { - bind.sessionCtrlAltDel(id: key); - cancelFunc(); - }, - padding: padding, - dismissOnClicked: true, - )); + menu.add(RemoteMenuEntry.insertCtrlAltDel(key, padding, + dismissFunc: cancelFunc)); } } diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index d6b1cec72..36b9504c0 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -99,6 +99,175 @@ class _MenubarTheme { static const double dividerHeight = 12.0; } +typedef DismissFunc = void Function(); + +class RemoteMenuEntry { + static MenuEntryRadios viewStyle( + String remoteId, + FFI ffi, + EdgeInsets padding, { + DismissFunc? dismissFunc, + DismissCallback? dismissCallback, + RxString? rxViewStyle, + }) { + return MenuEntryRadios( + text: translate('Ratio'), + optionsGetter: () => [ + MenuEntryRadioOption( + text: translate('Scale original'), + value: kRemoteViewStyleOriginal, + dismissOnClicked: true, + dismissCallback: dismissCallback, + ), + MenuEntryRadioOption( + text: translate('Scale adaptive'), + value: kRemoteViewStyleAdaptive, + dismissOnClicked: true, + dismissCallback: dismissCallback, + ), + ], + curOptionGetter: () async { + // null means peer id is not found, which there's no need to care about + final viewStyle = await bind.sessionGetViewStyle(id: remoteId) ?? ''; + if (rxViewStyle != null) { + rxViewStyle.value = viewStyle; + } + return viewStyle; + }, + optionSetter: (String oldValue, String newValue) async { + await bind.sessionSetViewStyle(id: remoteId, value: newValue); + if (rxViewStyle != null) { + rxViewStyle.value = newValue; + } + ffi.canvasModel.updateViewStyle(); + if (dismissFunc != null) { + dismissFunc(); + } + }, + padding: padding, + dismissOnClicked: true, + dismissCallback: dismissCallback, + ); + } + + static MenuEntrySwitch2 showRemoteCursor( + String remoteId, + EdgeInsets padding, { + DismissFunc? dismissFunc, + DismissCallback? dismissCallback, + }) { + final state = ShowRemoteCursorState.find(remoteId); + final optKey = 'show-remote-cursor'; + return MenuEntrySwitch2( + switchType: SwitchType.scheckbox, + text: translate('Show remote cursor'), + getter: () { + return state; + }, + setter: (bool v) async { + await bind.sessionToggleOption(id: remoteId, value: optKey); + state.value = + bind.sessionGetToggleOptionSync(id: remoteId, arg: optKey); + if (dismissFunc != null) { + dismissFunc(); + } + }, + padding: padding, + dismissOnClicked: true, + dismissCallback: dismissCallback, + ); + } + + static MenuEntrySwitch disableClipboard( + String remoteId, + EdgeInsets? padding, { + DismissFunc? dismissFunc, + DismissCallback? dismissCallback, + }) { + return createSwitchMenuEntry( + remoteId, + 'Disable clipboard', + 'disable-clipboard', + padding, + true, + dismissCallback: dismissCallback, + ); + } + + static MenuEntrySwitch createSwitchMenuEntry( + String remoteId, + String text, + String option, + EdgeInsets? padding, + bool dismissOnClicked, { + DismissFunc? dismissFunc, + DismissCallback? dismissCallback, + }) { + return MenuEntrySwitch( + switchType: SwitchType.scheckbox, + text: translate(text), + getter: () async { + return bind.sessionGetToggleOptionSync(id: remoteId, arg: option); + }, + setter: (bool v) async { + await bind.sessionToggleOption(id: remoteId, value: option); + if (dismissFunc != null) { + dismissFunc(); + } + }, + padding: padding, + dismissOnClicked: dismissOnClicked, + dismissCallback: dismissCallback, + ); + } + + static MenuEntryButton insertLock( + String remoteId, + EdgeInsets? padding, { + DismissFunc? dismissFunc, + DismissCallback? dismissCallback, + }) { + return MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Insert Lock'), + style: style, + ), + proc: () { + bind.sessionLockScreen(id: remoteId); + if (dismissFunc != null) { + dismissFunc(); + } + }, + padding: padding, + dismissOnClicked: true, + dismissCallback: dismissCallback, + ); + } + + static insertCtrlAltDel( + String remoteId, + EdgeInsets? padding, { + DismissFunc? dismissFunc, + DismissCallback? dismissCallback, + }) { + return MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + '${translate("Insert")} Ctrl + Alt + Del', + style: style, + ), + proc: () { + bind.sessionCtrlAltDel(id: remoteId); + if (dismissFunc != null) { + dismissFunc(); + } + }, + padding: padding, + dismissOnClicked: true, + dismissCallback: dismissCallback, + ); + } +} + class RemoteMenubar extends StatefulWidget { final String id; final FFI ffi; @@ -616,18 +785,8 @@ class _RemoteMenubarState extends State { displayMenu.add(MenuEntryDivider()); if (perms['keyboard'] != false) { if (pi.platform == kPeerPlatformLinux || pi.sasEnabled) { - displayMenu.add(MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - '${translate("Insert")} Ctrl + Alt + Del', - style: style, - ), - proc: () { - bind.sessionCtrlAltDel(id: widget.id); - }, - padding: padding, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - )); + displayMenu.add(RemoteMenuEntry.insertCtrlAltDel(widget.id, padding, + dismissCallback: _menuDismissCallback)); } } if (perms['restart'] != false && @@ -649,18 +808,8 @@ class _RemoteMenubarState extends State { } if (perms['keyboard'] != false) { - displayMenu.add(MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - translate('Insert Lock'), - style: style, - ), - proc: () { - bind.sessionLockScreen(id: widget.id); - }, - padding: padding, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - )); + displayMenu.add(RemoteMenuEntry.insertLock(widget.id, padding, + dismissCallback: _menuDismissCallback)); if (pi.platform == kPeerPlatformWindows) { displayMenu.add(MenuEntryButton( @@ -770,36 +919,12 @@ class _RemoteMenubarState extends State { const EdgeInsets padding = EdgeInsets.only(left: 18.0, right: 8.0); final peer_version = widget.ffi.ffiModel.pi.version; final displayMenu = [ - MenuEntryRadios( - text: translate('Ratio'), - optionsGetter: () => [ - MenuEntryRadioOption( - text: translate('Scale original'), - value: kRemoteViewStyleOriginal, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - ), - MenuEntryRadioOption( - text: translate('Scale adaptive'), - value: kRemoteViewStyleAdaptive, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - ), - ], - curOptionGetter: () async { - // null means peer id is not found, which there's no need to care about - final viewStyle = await bind.sessionGetViewStyle(id: widget.id) ?? ''; - widget.state.viewStyle.value = viewStyle; - return viewStyle; - }, - optionSetter: (String oldValue, String newValue) async { - await bind.sessionSetViewStyle(id: widget.id, value: newValue); - widget.state.viewStyle.value = newValue; - widget.ffi.canvasModel.updateViewStyle(); - }, - padding: padding, - dismissOnClicked: true, + RemoteMenuEntry.viewStyle( + widget.id, + widget.ffi, + padding, dismissCallback: _menuDismissCallback, + rxViewStyle: widget.state.viewStyle, ), MenuEntryDivider(), MenuEntryRadios( @@ -1158,25 +1283,11 @@ class _RemoteMenubarState extends State { /// Show remote cursor if (!widget.ffi.canvasModel.cursorEmbedded) { - displayMenu.add(() { - final state = ShowRemoteCursorState.find(widget.id); - final optKey = 'show-remote-cursor'; - return MenuEntrySwitch2( - switchType: SwitchType.scheckbox, - text: translate('Show remote cursor'), - getter: () { - return state; - }, - setter: (bool v) async { - await bind.sessionToggleOption(id: widget.id, value: optKey); - state.value = - bind.sessionGetToggleOptionSync(id: widget.id, arg: optKey); - }, - padding: padding, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - ); - }()); + displayMenu.add(RemoteMenuEntry.showRemoteCursor( + widget.id, + padding, + dismissCallback: _menuDismissCallback, + )); } /// Show remote cursor scaling with image @@ -1237,8 +1348,11 @@ class _RemoteMenubarState extends State { if (perms['keyboard'] != false) { if (perms['clipboard'] != false) { - displayMenu.add(_createSwitchMenuEntry( - 'Disable clipboard', 'disable-clipboard', padding, true)); + displayMenu.add(RemoteMenuEntry.disableClipboard( + widget.id, + padding, + dismissCallback: _menuDismissCallback, + )); } displayMenu.add(_createSwitchMenuEntry( 'Lock after session end', 'lock-after-session-end', padding, true)); @@ -1349,19 +1463,9 @@ class _RemoteMenubarState extends State { MenuEntrySwitch _createSwitchMenuEntry( String text, String option, EdgeInsets? padding, bool dismissOnClicked) { - return MenuEntrySwitch( - switchType: SwitchType.scheckbox, - text: translate(text), - getter: () async { - return bind.sessionGetToggleOptionSync(id: widget.id, arg: option); - }, - setter: (bool v) async { - await bind.sessionToggleOption(id: widget.id, value: option); - }, - padding: padding, - dismissOnClicked: dismissOnClicked, - dismissCallback: _menuDismissCallback, - ); + return RemoteMenuEntry.createSwitchMenuEntry( + widget.id, text, option, padding, dismissOnClicked, + dismissCallback: _menuDismissCallback); } } From 40d0ea016bae6dca3b51f503f25003312f129299 Mon Sep 17 00:00:00 2001 From: 21pages Date: Fri, 3 Feb 2023 15:07:45 +0800 Subject: [PATCH 1715/2015] refactor peer tab with model, make it scrollable Signed-off-by: 21pages --- flutter/lib/common/widgets/peer_tab_page.dart | 449 ++++++------------ flutter/lib/main.dart | 1 + flutter/lib/mobile/widgets/dialog.dart | 6 +- flutter/lib/models/group_model.dart | 4 +- flutter/lib/models/model.dart | 5 +- flutter/lib/models/peer_tab_model.dart | 275 +++++++++++ flutter/lib/models/user_model.dart | 2 +- 7 files changed, 423 insertions(+), 319 deletions(-) create mode 100644 flutter/lib/models/peer_tab_model.dart diff --git a/flutter/lib/common/widgets/peer_tab_page.dart b/flutter/lib/common/widgets/peer_tab_page.dart index 150121c59..4080f9c11 100644 --- a/flutter/lib/common/widgets/peer_tab_page.dart +++ b/flutter/lib/common/widgets/peer_tab_page.dart @@ -1,7 +1,7 @@ -import 'dart:convert'; import 'dart:ui' as ui; import 'package:bot_toast/bot_toast.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common/widgets/address_book.dart'; import 'package:flutter_hbb/common/widgets/my_group.dart'; @@ -12,181 +12,15 @@ import 'package:flutter_hbb/desktop/widgets/popup_menu.dart'; import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; import 'package:flutter_hbb/desktop/widgets/material_mod_popup_menu.dart' as mod_menu; +import 'package:flutter_hbb/models/peer_tab_model.dart'; import 'package:get/get.dart'; +import 'package:get/get_rx/src/rx_workers/utils/debouncer.dart'; +import 'package:provider/provider.dart'; +import 'package:visibility_detector/visibility_detector.dart'; import '../../common.dart'; import '../../models/platform_model.dart'; -const int groupTabIndex = 4; -const String defaultGroupTabname = 'Group'; - -class StatePeerTab { - final RxInt currentTab = 0.obs; // index in tabNames - final RxList visibleOrderedTabs = RxList.empty(growable: true); - List tabOrder = List.from([0, 1, 2, 3, 4]); // constant length - final RxInt tabHiddenFlag = 0.obs; - final RxList tabNames = [ - 'Recent Sessions', - 'Favorites', - 'Discovered', - 'Address Book', - defaultGroupTabname, - ].obs; - - StatePeerTab._() { - // init tabHiddenFlag - tabHiddenFlag.value = (int.tryParse( - bind.getLocalFlutterConfig(k: 'hidden-peer-card'), - radix: 2) ?? - 0); - var tabs = _notHiddenTabs(); - // remove dynamic tabs - tabs.remove(groupTabIndex); - // init tabOrder - try { - final conf = bind.getLocalFlutterConfig(k: 'peer-tab-order'); - if (conf.isNotEmpty) { - final json = jsonDecode(conf); - if (json is List) { - final List list = - json.map((e) => int.tryParse(e.toString()) ?? -1).toList(); - if (list.length == tabOrder.length && - tabOrder.every((e) => list.contains(e))) { - tabOrder = list; - } - } - } - } catch (e) { - debugPrintStack(label: '$e'); - } - // init visibleOrderedTabs - var tempList = tabOrder.toList(); - tempList.removeWhere((e) => !tabs.contains(e)); - visibleOrderedTabs.value = tempList; - // init currentTab - currentTab.value = - int.tryParse(bind.getLocalFlutterConfig(k: 'peer-tab-index')) ?? 0; - if (!tabs.contains(currentTab.value)) { - if (tabs.isNotEmpty) { - currentTab.value = tabs[0]; - } else { - currentTab.value = 0; - } - } - } - static final StatePeerTab instance = StatePeerTab._(); - - // check dynamic tabs - check() { - tabOrder2visibleOrderedTabs(); - if (visibleOrderedTabs.contains(groupTabIndex) && - int.tryParse(bind.getLocalFlutterConfig(k: 'peer-tab-index')) == - groupTabIndex) { - currentTab.value = groupTabIndex; - } - if (gFFI.userModel.isAdmin.isFalse && gFFI.userModel.groupName.isNotEmpty) { - tabNames[groupTabIndex] = gFFI.userModel.groupName.value; - } else { - tabNames[groupTabIndex] = defaultGroupTabname; - } - } - - visibleOrderedTabs2TabOrder() { - var tmpTabOrder = visibleOrderedTabs.toList(); - var left = tabOrder.where((e) => !tmpTabOrder.contains(e)).toList(); - for (var t in left) { - _addTabInOrder(tmpTabOrder, t); - } - statePeerTab.tabOrder = tmpTabOrder; - bind.setLocalFlutterConfig(k: 'peer-tab-order', v: jsonEncode(tmpTabOrder)); - } - - tabOrder2visibleOrderedTabs() { - var visible = statePeerTab.visibleTabs(); - statePeerTab.visibleOrderedTabs.value = - statePeerTab.tabOrder.where((e) => visible.contains(e)).toList(); - } - - // return true if hide group card - bool filterGroupCard() { - if (gFFI.groupModel.users.isEmpty || - (gFFI.userModel.isAdmin.isFalse && gFFI.userModel.groupName.isEmpty)) { - return true; - } else { - return false; - } - } - - // return index array of tabNames - List visibleTabs() { - var v = List.empty(growable: true); - for (int i = 0; i < tabNames.length; i++) { - if (!_isTabHidden(i) && !_isTabFilter(i)) { - v.add(i); - } - } - return v; - } - - bool _isTabHidden(int tabindex) { - return tabHiddenFlag & (1 << tabindex) != 0; - } - - bool _isTabFilter(int tabIndex) { - if (tabIndex == groupTabIndex) { - return filterGroupCard(); - } - return false; - } - - List _notHiddenTabs() { - var v = List.empty(growable: true); - for (int i = 0; i < tabNames.length; i++) { - if (!_isTabHidden(i)) { - v.add(i); - } - } - return v; - } - - // add tabIndex to list - _addTabInOrder(List list, int tabIndex) { - if (!tabOrder.contains(tabIndex) || list.contains(tabIndex)) { - return; - } - bool sameOrder = true; - int lastIndex = -1; - for (int i = 0; i < list.length; i++) { - var index = tabOrder.lastIndexOf(list[i]); - if (index > lastIndex) { - lastIndex = index; - continue; - } else { - sameOrder = false; - break; - } - } - if (sameOrder) { - var indexInTabOrder = tabOrder.indexOf(tabIndex); - var left = List.empty(growable: true); - for (int i = 0; i < indexInTabOrder; i++) { - left.add(tabOrder[i]); - } - int insertIndex = list.lastIndexWhere((e) => left.contains(e)); - if (insertIndex < 0) { - insertIndex = 0; - } else { - insertIndex += 1; - } - list.insert(insertIndex, tabIndex); - } else { - list.add(tabIndex); - } - } -} - -final statePeerTab = StatePeerTab.instance; - class PeerTabPage extends StatefulWidget { const PeerTabPage({Key? key}) : super(key: key); @override @@ -232,11 +66,10 @@ class _PeerTabPageState extends State ), () => {}), ]; + final _scrollDebounce = Debouncer(delay: Duration(milliseconds: 50)); @override void initState() { - adjustTab(); - final uiType = bind.getLocalFlutterConfig(k: 'peer-card-ui-type'); if (uiType != '') { peerCardUiType.value = int.parse(uiType) == PeerUiType.list.index @@ -248,7 +81,7 @@ class _PeerTabPageState extends State Future handleTabSelection(int tabIndex) async { if (tabIndex < entries.length) { - statePeerTab.currentTab.value = tabIndex; + gFFI.peerTabModel.setCurrentTab(tabIndex); entries[tabIndex].load(); } } @@ -270,6 +103,7 @@ class _PeerTabPageState extends State Expanded( child: visibleContextMenuListener( _createSwitchBar(context))), + buildScrollJumper(), const PeerSearchBar(), Offstage( offstage: !isDesktop, @@ -284,98 +118,115 @@ class _PeerTabPageState extends State } Widget _createSwitchBar(BuildContext context) { - return Obx(() { - var tabs = statePeerTab.visibleOrderedTabs; - int indexCounter = -1; - return ReorderableListView( - buildDefaultDragHandles: false, - onReorder: (oldIndex, newIndex) { - if (oldIndex < newIndex) { - newIndex -= 1; - } - var list = tabs.toList(); - final int item = list.removeAt(oldIndex); - list.insert(newIndex, item); - tabs.value = list; - statePeerTab.visibleOrderedTabs2TabOrder(); - }, - scrollDirection: Axis.horizontal, - physics: NeverScrollableScrollPhysics(), - scrollController: ScrollController(), - children: tabs.map((t) { - indexCounter++; - return ReorderableDragStartListener( + final model = Provider.of(context); + int indexCounter = -1; + return ReorderableListView( + buildDefaultDragHandles: false, + onReorder: (oldIndex, newIndex) { + model.onReorder(oldIndex, newIndex); + }, + scrollDirection: Axis.horizontal, + physics: NeverScrollableScrollPhysics(), + scrollController: model.sc, + children: model.visibleOrderedTabs.map((t) { + indexCounter++; + return ReorderableDragStartListener( + key: ValueKey(t), + index: indexCounter, + child: VisibilityDetector( key: ValueKey(t), - index: indexCounter, - child: InkWell( - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 8), - decoration: BoxDecoration( - color: statePeerTab.currentTab.value == t - ? Theme.of(context).backgroundColor - : null, - borderRadius: BorderRadius.circular(isDesktop ? 2 : 6), - ), - child: Align( - alignment: Alignment.center, - child: Text( - translatedTabname(t), - textAlign: TextAlign.center, - style: TextStyle( - height: 1, - fontSize: 14, - color: statePeerTab.currentTab.value == t - ? MyTheme.tabbar(context).selectedTextColor - : MyTheme.tabbar(context).unSelectedTextColor - ?..withOpacity(0.5)), - ), - )), - onTap: () async { - await handleTabSelection(t); - await bind.setLocalFlutterConfig( - k: 'peer-tab-index', v: t.toString()); + onVisibilityChanged: (info) { + final id = (info.key as ValueKey).value; + model.setTabFullyVisible(id, info.visibleFraction > 0.99); + }, + child: Listener( + // handle mouse wheel + onPointerSignal: (e) { + if (e is PointerScrollEvent) { + if (!model.sc.canScroll) return; + _scrollDebounce.call(() { + model.sc.animateTo(model.sc.offset + e.scrollDelta.dy, + duration: Duration(milliseconds: 200), + curve: Curves.ease); + }); + } }, + child: InkWell( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8), + decoration: BoxDecoration( + color: model.currentTab == t + ? Theme.of(context).backgroundColor + : null, + borderRadius: BorderRadius.circular(isDesktop ? 2 : 6), + ), + child: Align( + alignment: Alignment.center, + child: Text( + model.translatedTabname(t), + textAlign: TextAlign.center, + style: TextStyle( + height: 1, + fontSize: 14, + color: model.currentTab == t + ? MyTheme.tabbar(context).selectedTextColor + : MyTheme.tabbar(context).unSelectedTextColor + ?..withOpacity(0.5)), + ), + )), + onTap: () async { + await handleTabSelection(t); + await bind.setLocalFlutterConfig( + k: 'peer-tab-index', v: t.toString()); + }, + ), ), - ); - }).toList()); - }); + ), + ); + }).toList()); } - translatedTabname(int index) { - if (index < statePeerTab.tabNames.length) { - final name = statePeerTab.tabNames[index]; - if (index == groupTabIndex) { - if (name == defaultGroupTabname) { - return translate(name); - } else { - return name; - } - } else { - return translate(name); - } - } - assert(false); - return index.toString(); + Widget buildScrollJumper() { + final model = Provider.of(context); + return Offstage( + offstage: !model.showScrollBtn, + child: Row( + children: [ + GestureDetector( + child: Icon(Icons.arrow_left, + size: 22, + color: model.leftFullyVisible + ? Theme.of(context).disabledColor + : null), + onTap: model.sc.backward), + GestureDetector( + child: Icon(Icons.arrow_right, + size: 22, + color: model.rightFullyVisible + ? Theme.of(context).disabledColor + : null), + onTap: model.sc.forward) + ], + )); } Widget _createPeersView() { - final verticalMargin = isDesktop ? 12.0 : 6.0; - return Expanded( - child: Obx(() { - var tabs = statePeerTab.visibleOrderedTabs; - if (tabs.isEmpty) { - return visibleContextMenuListener(Center( - child: Text(translate('Right click to select tabs')), - )); + final model = Provider.of(context); + Widget child; + if (model.visibleOrderedTabs.isEmpty) { + child = visibleContextMenuListener(Center( + child: Text(translate('Right click to select tabs')), + )); + } else { + if (model.visibleOrderedTabs.contains(model.currentTab)) { + child = entries[model.currentTab].widget; } else { - if (tabs.contains(statePeerTab.currentTab.value)) { - return entries[statePeerTab.currentTab.value].widget; - } else { - statePeerTab.currentTab.value = tabs[0]; - return entries[statePeerTab.currentTab.value].widget; - } + model.setCurrentTab(model.visibleOrderedTabs[0]); + child = entries[0].widget; } - }).marginSymmetric(vertical: verticalMargin)); + } + return Expanded( + child: child.marginSymmetric(vertical: isDesktop ? 12.0 : 6.0)); } Widget _createPeerViewTypeSwitch(BuildContext context) { @@ -408,13 +259,6 @@ class _PeerTabPageState extends State ); } - adjustTab() { - var tabs = statePeerTab.visibleOrderedTabs; - if (tabs.isNotEmpty && !tabs.contains(statePeerTab.currentTab.value)) { - statePeerTab.currentTab.value = tabs[0]; - } - } - Widget visibleContextMenuListener(Widget child) { return Listener( onPointerDown: (e) { @@ -434,55 +278,36 @@ class _PeerTabPageState extends State } Widget visibleContextMenu(CancelFunc cancelFunc) { - return Obx(() { - final List menu = List.empty(growable: true); - final List menuIndex = List.empty(growable: true); - for (int i = 0; i < statePeerTab.tabNames.length; i++) { - if (i == groupTabIndex && statePeerTab.filterGroupCard()) { - continue; - } - int bitMask = 1 << i; - menuIndex.add(i); - menu.add(MenuEntrySwitch( - switchType: SwitchType.scheckbox, - text: translatedTabname(i), - getter: () async { - return statePeerTab.tabHiddenFlag & bitMask == 0; - }, - setter: (show) async { - if (show) { - statePeerTab.tabHiddenFlag.value &= ~bitMask; - } else { - statePeerTab.tabHiddenFlag.value |= bitMask; - } - await bind.setLocalFlutterConfig( - k: 'hidden-peer-card', - v: statePeerTab.tabHiddenFlag.value.toRadixString(2)); - statePeerTab.tabOrder2visibleOrderedTabs(); - cancelFunc(); - adjustTab(); - })); - } - // show in tabOrder - List menu2 = List.empty(growable: true); - statePeerTab.tabOrder.map((e) { - final index = menuIndex.indexOf(e); - if (index >= 0) { - menu2.add(menu[index]); - } - }).toList(); - return mod_menu.PopupMenu( - items: menu2 - .map((entry) => entry.build( - context, - const MenuConfig( - commonColor: MyTheme.accent, - height: 20.0, - dividerHeight: 12.0, - ))) - .expand((i) => i) - .toList()); - }); + final model = Provider.of(context); + final List menu = List.empty(growable: true); + final List menuIndex = List.empty(growable: true); + var list = model.orderedNotFilteredTabs(); + for (int i = 0; i < list.length; i++) { + int tabIndex = list[i]; + int bitMask = 1 << tabIndex; + menuIndex.add(tabIndex); + menu.add(MenuEntrySwitch( + switchType: SwitchType.scheckbox, + text: model.translatedTabname(tabIndex), + getter: () async { + return model.tabHiddenFlag & bitMask == 0; + }, + setter: (show) async { + model.onHideShow(tabIndex, show); + cancelFunc(); + })); + } + return mod_menu.PopupMenu( + items: menu + .map((entry) => entry.build( + context, + const MenuConfig( + commonColor: MyTheme.accent, + height: 20.0, + dividerHeight: 12.0, + ))) + .expand((i) => i) + .toList()); } } diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 86cc9d89b..a2ae959c0 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -353,6 +353,7 @@ class _AppState extends State { ChangeNotifierProvider.value(value: gFFI.imageModel), ChangeNotifierProvider.value(value: gFFI.cursorModel), ChangeNotifierProvider.value(value: gFFI.canvasModel), + ChangeNotifierProvider.value(value: gFFI.peerTabModel), ], child: GetMaterialApp( navigatorKey: globalKey, diff --git a/flutter/lib/mobile/widgets/dialog.dart b/flutter/lib/mobile/widgets/dialog.dart index 2fbe40091..7e9a9879c 100644 --- a/flutter/lib/mobile/widgets/dialog.dart +++ b/flutter/lib/mobile/widgets/dialog.dart @@ -643,9 +643,9 @@ class _PasswordWidgetState extends State { // Here is key idea suffixIcon: IconButton( icon: Icon( - // Based on passwordVisible state choose the icon - _passwordVisible ? Icons.visibility : Icons.visibility_off, - ), + // Based on passwordVisible state choose the icon + _passwordVisible ? Icons.visibility : Icons.visibility_off, + color: MyTheme.lightTheme.primaryColor), onPressed: () { // Update the state i.e. toggle the state of passwordVisible variable setState(() { diff --git a/flutter/lib/models/group_model.dart b/flutter/lib/models/group_model.dart index 4d9fab0e4..5e2b85f90 100644 --- a/flutter/lib/models/group_model.dart +++ b/flutter/lib/models/group_model.dart @@ -35,7 +35,7 @@ class GroupModel { await reset(); if (gFFI.userModel.userName.isEmpty || (gFFI.userModel.isAdmin.isFalse && gFFI.userModel.groupName.isEmpty)) { - statePeerTab.check(); + gFFI.peerTabModel.check_dynamic_tabs(); return; } userLoading.value = true; @@ -82,7 +82,7 @@ class GroupModel { userLoadError.value = err.toString(); } finally { userLoading.value = false; - statePeerTab.check(); + gFFI.peerTabModel.check_dynamic_tabs(); } } diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index aae4c6a07..daf7bfe34 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -13,6 +13,7 @@ import 'package:flutter_hbb/models/ab_model.dart'; import 'package:flutter_hbb/models/chat_model.dart'; import 'package:flutter_hbb/models/file_model.dart'; import 'package:flutter_hbb/models/group_model.dart'; +import 'package:flutter_hbb/models/peer_tab_model.dart'; import 'package:flutter_hbb/models/server_model.dart'; import 'package:flutter_hbb/models/user_model.dart'; import 'package:flutter_hbb/models/state_model.dart'; @@ -1292,8 +1293,9 @@ class FFI { late final AbModel abModel; // global late final GroupModel groupModel; // global late final UserModel userModel; // global + late final PeerTabModel peerTabModel; // global late final QualityMonitorModel qualityMonitorModel; // session - late final RecordingModel recordingModel; // recording + late final RecordingModel recordingModel; // session late final InputModel inputModel; // session FFI() { @@ -1305,6 +1307,7 @@ class FFI { chatModel = ChatModel(WeakReference(this)); fileModel = FileModel(WeakReference(this)); userModel = UserModel(WeakReference(this)); + peerTabModel = PeerTabModel(WeakReference(this)); abModel = AbModel(WeakReference(this)); groupModel = GroupModel(WeakReference(this)); qualityMonitorModel = QualityMonitorModel(WeakReference(this)); diff --git a/flutter/lib/models/peer_tab_model.dart b/flutter/lib/models/peer_tab_model.dart new file mode 100644 index 000000000..7c6211682 --- /dev/null +++ b/flutter/lib/models/peer_tab_model.dart @@ -0,0 +1,275 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; +import 'package:get/get.dart'; +import 'package:scroll_pos/scroll_pos.dart'; + +import '../common.dart'; +import 'model.dart'; + +const int groupTabIndex = 4; +const String defaultGroupTabname = 'Group'; + +class PeerTabModel with ChangeNotifier { + WeakReference parent; + int get currentTab => _currentTab; + int _currentTab = 0; // index in tabNames + List get visibleOrderedTabs => _visibleOrderedTabs; + List _visibleOrderedTabs = List.empty(growable: true); + List get tabOrder => _tabOrder; + List _tabOrder = List.from([0, 1, 2, 3, 4]); // constant length + int get tabHiddenFlag => _tabHiddenFlag; + int _tabHiddenFlag = 0; + bool get showScrollBtn => _showScrollBtn; + bool _showScrollBtn = false; + final List _fullyVisible = List.filled(5, false); + bool get leftFullyVisible => _leftFullyVisible; + bool _leftFullyVisible = false; + bool get rightFullyVisible => _rightFullyVisible; + bool _rightFullyVisible = false; + ScrollPosController sc = ScrollPosController(); + List tabNames = [ + 'Recent Sessions', + 'Favorites', + 'Discovered', + 'Address Book', + defaultGroupTabname, + ]; + + PeerTabModel(this.parent) { + // init tabHiddenFlag + _tabHiddenFlag = int.tryParse( + bind.getLocalFlutterConfig(k: 'hidden-peer-card'), + radix: 2) ?? + 0; + var tabs = _notHiddenTabs(); + // remove dynamic tabs + tabs.remove(groupTabIndex); + // init tabOrder + try { + final conf = bind.getLocalFlutterConfig(k: 'peer-tab-order'); + if (conf.isNotEmpty) { + final json = jsonDecode(conf); + if (json is List) { + final List list = + json.map((e) => int.tryParse(e.toString()) ?? -1).toList(); + if (list.length == _tabOrder.length && + _tabOrder.every((e) => list.contains(e))) { + _tabOrder = list; + } + } + } + } catch (e) { + debugPrintStack(label: '$e'); + } + // init visibleOrderedTabs + var tempList = _tabOrder.toList(); + tempList.removeWhere((e) => !tabs.contains(e)); + _visibleOrderedTabs = tempList; + // init currentTab + _currentTab = + int.tryParse(bind.getLocalFlutterConfig(k: 'peer-tab-index')) ?? 0; + if (!tabs.contains(_currentTab)) { + if (tabs.isNotEmpty) { + _currentTab = tabs[0]; + } else { + _currentTab = 0; + } + } + sc.itemCount = _visibleOrderedTabs.length; + } + + check_dynamic_tabs() { + var visible = visibleTabs(); + _visibleOrderedTabs = _tabOrder.where((e) => visible.contains(e)).toList(); + if (_visibleOrderedTabs.contains(groupTabIndex) && + int.tryParse(bind.getLocalFlutterConfig(k: 'peer-tab-index')) == + groupTabIndex) { + _currentTab = groupTabIndex; + } + if (gFFI.userModel.isAdmin.isFalse && gFFI.userModel.groupName.isNotEmpty) { + tabNames[groupTabIndex] = gFFI.userModel.groupName.value; + } else { + tabNames[groupTabIndex] = defaultGroupTabname; + } + sc.itemCount = _visibleOrderedTabs.length; + notifyListeners(); + } + + setCurrentTab(int index) { + if (_currentTab != index) { + _currentTab = index; + notifyListeners(); + } + } + + setTabFullyVisible(int index, bool visible) { + if (index >= 0 && index < _fullyVisible.length) { + if (visible != _fullyVisible[index]) { + _fullyVisible[index] = visible; + bool changed = false; + bool show = _visibleOrderedTabs.any((e) => !_fullyVisible[e]); + if (show != _showScrollBtn) { + _showScrollBtn = show; + changed = true; + } + if (_visibleOrderedTabs.isNotEmpty && _visibleOrderedTabs[0] == index) { + if (_leftFullyVisible != visible) { + _leftFullyVisible = visible; + changed = true; + } + } + if (_visibleOrderedTabs.isNotEmpty && + _visibleOrderedTabs.last == index) { + if (_rightFullyVisible != visible) { + _rightFullyVisible = visible; + changed = true; + } + } + if (changed) { + notifyListeners(); + } + } + } + } + + onReorder(oldIndex, newIndex) { + if (oldIndex < newIndex) { + newIndex -= 1; + } + var list = _visibleOrderedTabs.toList(); + final int item = list.removeAt(oldIndex); + list.insert(newIndex, item); + _visibleOrderedTabs = list; + + var tmpTabOrder = _visibleOrderedTabs.toList(); + var left = _tabOrder.where((e) => !tmpTabOrder.contains(e)).toList(); + for (var t in left) { + _addTabInOrder(tmpTabOrder, t); + } + _tabOrder = tmpTabOrder; + bind.setLocalFlutterConfig(k: 'peer-tab-order', v: jsonEncode(tmpTabOrder)); + notifyListeners(); + } + + onHideShow(int index, bool show) async { + int bitMask = 1 << index; + if (show) { + _tabHiddenFlag &= ~bitMask; + } else { + _tabHiddenFlag |= bitMask; + } + await bind.setLocalFlutterConfig( + k: 'hidden-peer-card', v: _tabHiddenFlag.toRadixString(2)); + var visible = visibleTabs(); + _visibleOrderedTabs = _tabOrder.where((e) => visible.contains(e)).toList(); + if (_visibleOrderedTabs.isNotEmpty && + !_visibleOrderedTabs.contains(_currentTab)) { + _currentTab = _visibleOrderedTabs[0]; + } + notifyListeners(); + } + + List orderedNotFilteredTabs() { + var list = tabOrder.toList(); + if (_filterGroupCard()) { + list.remove(groupTabIndex); + } + return list; + } + + // return index array of tabNames + List visibleTabs() { + var v = List.empty(growable: true); + for (int i = 0; i < tabNames.length; i++) { + if (!_isTabHidden(i) && !_isTabFilter(i)) { + v.add(i); + } + } + return v; + } + + String translatedTabname(int index) { + if (index >= 0 && index < tabNames.length) { + final name = tabNames[index]; + if (index == groupTabIndex) { + if (name == defaultGroupTabname) { + return translate(name); + } else { + return name; + } + } else { + return translate(name); + } + } + assert(false); + return index.toString(); + } + + bool _isTabHidden(int tabindex) { + return _tabHiddenFlag & (1 << tabindex) != 0; + } + + bool _isTabFilter(int tabIndex) { + if (tabIndex == groupTabIndex) { + return _filterGroupCard(); + } + return false; + } + + // return true if hide group card + bool _filterGroupCard() { + if (gFFI.groupModel.users.isEmpty || + (gFFI.userModel.isAdmin.isFalse && gFFI.userModel.groupName.isEmpty)) { + return true; + } else { + return false; + } + } + + List _notHiddenTabs() { + var v = List.empty(growable: true); + for (int i = 0; i < tabNames.length; i++) { + if (!_isTabHidden(i)) { + v.add(i); + } + } + return v; + } + + // add tabIndex to list + _addTabInOrder(List list, int tabIndex) { + if (!_tabOrder.contains(tabIndex) || list.contains(tabIndex)) { + return; + } + bool sameOrder = true; + int lastIndex = -1; + for (int i = 0; i < list.length; i++) { + var index = _tabOrder.lastIndexOf(list[i]); + if (index > lastIndex) { + lastIndex = index; + continue; + } else { + sameOrder = false; + break; + } + } + if (sameOrder) { + var indexInTabOrder = _tabOrder.indexOf(tabIndex); + var left = List.empty(growable: true); + for (int i = 0; i < indexInTabOrder; i++) { + left.add(_tabOrder[i]); + } + int insertIndex = list.lastIndexWhere((e) => left.contains(e)); + if (insertIndex < 0) { + insertIndex = 0; + } else { + insertIndex += 1; + } + list.insert(insertIndex, tabIndex); + } else { + list.add(tabIndex); + } + } +} diff --git a/flutter/lib/models/user_model.dart b/flutter/lib/models/user_model.dart index 6694d8c5c..7f40b3333 100644 --- a/flutter/lib/models/user_model.dart +++ b/flutter/lib/models/user_model.dart @@ -62,7 +62,7 @@ class UserModel { await gFFI.groupModel.reset(); userName.value = ''; groupName.value = ''; - statePeerTab.check(); + gFFI.peerTabModel.check_dynamic_tabs(); } Future _parseAndUpdateUser(UserPayload user) async { From 893f18cdec1b4fedf72af67bbfb7fe03e047db11 Mon Sep 17 00:00:00 2001 From: csf Date: Tue, 7 Feb 2023 00:11:48 +0900 Subject: [PATCH 1716/2015] add PenetrableOverlayState, opt chat page over remote_page --- flutter/lib/common/widgets/overlay.dart | 106 ++++++++++++++---- flutter/lib/desktop/pages/remote_page.dart | 89 ++++++++------- .../lib/desktop/widgets/remote_menubar.dart | 13 ++- flutter/lib/models/chat_model.dart | 75 +++++-------- 4 files changed, 177 insertions(+), 106 deletions(-) diff --git a/flutter/lib/common/widgets/overlay.dart b/flutter/lib/common/widgets/overlay.dart index d84789d9c..3e248700f 100644 --- a/flutter/lib/common/widgets/overlay.dart +++ b/flutter/lib/common/widgets/overlay.dart @@ -1,7 +1,7 @@ import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; -import 'package:get/get_state_manager/src/rx_flutter/rx_obx_widget.dart'; +import 'package:get/get.dart'; import 'package:provider/provider.dart'; import '../../consts.dart'; @@ -92,31 +92,30 @@ class DraggableChatWindow extends StatelessWidget { bottom: BorderSide( color: Theme.of(context).hintColor.withOpacity(0.4)))), height: 38, - child: Obx(() => Opacity( - opacity: chatModel.isWindowFocus.value ? 1.0 : 0.4, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: - const EdgeInsets.symmetric(horizontal: 15, vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 8), + child: Obx(() => Opacity( + opacity: chatModel.isWindowFocus.value ? 1.0 : 0.4, child: Row(children: [ Icon(Icons.chat_bubble_outline, size: 20, color: Theme.of(context).colorScheme.primary), SizedBox(width: 6), Text(translate("Chat")) - ])), - Padding( - padding: EdgeInsets.all(2), - child: ActionIcon( - message: 'Close', - icon: IconFont.close, - onTap: chatModel.hideChatWindowOverlay, - isClose: true, - boxSize: 32, - )) - ], - ))), + ])))), + Padding( + padding: EdgeInsets.all(2), + child: ActionIcon( + message: 'Close', + icon: IconFont.close, + onTap: chatModel.hideChatWindowOverlay, + isClose: true, + boxSize: 32, + )) + ], + ), ); } } @@ -372,3 +371,68 @@ class QualityMonitor extends StatelessWidget { ) : const SizedBox.shrink())); } + +class PenetrableOverlayState { + final _middleBlocked = false.obs; + final _overlayKey = GlobalKey(); + + VoidCallback? onMiddleBlockedClick; // to-do use listener + + RxBool get middleBlocked => _middleBlocked; + GlobalKey get overlayKey => _overlayKey; + OverlayState? get overlayState => _overlayKey.currentState; + + OverlayState? getOverlayStateOrGlobal() { + if (overlayState == null) { + if (globalKey.currentState == null || + globalKey.currentState!.overlay == null) return null; + return globalKey.currentState!.overlay; + } else { + return overlayState; + } + } + + void addMiddleBlockedListener(void Function(bool) cb) { + _middleBlocked.listen(cb); + } + + void setMiddleBlocked(bool blocked) { + if (blocked != _middleBlocked.value) { + _middleBlocked.value = blocked; + } + } +} + +class PenetrableOverlay extends StatelessWidget { + final Widget underlying; + final List? upperLayer; + + final PenetrableOverlayState state; + + PenetrableOverlay( + {required this.underlying, required this.state, this.upperLayer}); + + @override + Widget build(BuildContext context) { + final initialEntries = [ + OverlayEntry(builder: (_) => underlying), + + /// middle layer + OverlayEntry( + builder: (context) => Obx(() => Listener( + onPointerDown: (_) { + state.onMiddleBlockedClick?.call(); + }, + child: Container( + color: state.middleBlocked.value + ? Colors.red.withOpacity(0.3) + : null)))), + ]; + + if (upperLayer != null) { + initialEntries.addAll(upperLayer!); + } + + return Overlay(key: state.overlayKey, initialEntries: initialEntries); + } +} diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 2e4668159..4bda68c2d 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -62,6 +62,8 @@ class _RemotePageState extends State late RxBool _remoteCursorMoved; late RxBool _keyboardEnabled; + final overlayState = PenetrableOverlayState(); + final FocusNode _rawKeyFocusNode = FocusNode(debugLabel: "rawkeyFocusNode"); Function(bool)? _onEnterOrLeaveImage4Menubar; @@ -133,6 +135,12 @@ class _RemotePageState extends State // }); // _isCustomCursorInited = true; // } + + _ffi.chatModel.setPenetrableOverlayState(overlayState); + // make remote page penetrable automatically, effective for chat over remote + overlayState.onMiddleBlockedClick = () { + overlayState.setMiddleBlocked(false); + }; } @override @@ -192,39 +200,47 @@ class _RemotePageState extends State Widget buildBody(BuildContext context) { return Scaffold( - backgroundColor: Theme.of(context).backgroundColor, - body: Overlay( - initialEntries: [ - OverlayEntry(builder: (context) { - _ffi.chatModel.setOverlayState(Overlay.of(context)); - _ffi.dialogManager.setOverlayState(Overlay.of(context)); - return Container( - color: Colors.black, - child: RawKeyFocusScope( - focusNode: _rawKeyFocusNode, - onFocusChange: (bool imageFocused) { - debugPrint( - "onFocusChange(window active:${!_isWindowBlur}) $imageFocused"); - // See [onWindowBlur]. - if (Platform.isWindows) { - if (_isWindowBlur) { - imageFocused = false; - Future.delayed(Duration.zero, () { - _rawKeyFocusNode.unfocus(); - }); - } - if (imageFocused) { - _ffi.inputModel.enterOrLeave(true); - } else { - _ffi.inputModel.enterOrLeave(false); - } - } - }, - inputModel: _ffi.inputModel, - child: getBodyForDesktop(context))); - }) - ], - )); + backgroundColor: Theme.of(context).backgroundColor, + body: PenetrableOverlay( + state: overlayState, + underlying: Container( + color: Colors.black, + child: RawKeyFocusScope( + focusNode: _rawKeyFocusNode, + onFocusChange: (bool imageFocused) { + debugPrint( + "onFocusChange(window active:${!_isWindowBlur}) $imageFocused"); + // See [onWindowBlur]. + if (Platform.isWindows) { + if (_isWindowBlur) { + imageFocused = false; + Future.delayed(Duration.zero, () { + _rawKeyFocusNode.unfocus(); + }); + } + if (imageFocused) { + _ffi.inputModel.enterOrLeave(true); + } else { + _ffi.inputModel.enterOrLeave(false); + } + } + }, + inputModel: _ffi.inputModel, + child: getBodyForDesktop(context))), + upperLayer: [ + OverlayEntry( + builder: (context) => RemoteMenubar( + id: widget.id, + ffi: _ffi, + state: widget.menubarState, + onEnterOrLeaveImageSetter: (func) => + _onEnterOrLeaveImage4Menubar = func, + onEnterOrLeaveImageCleaner: () => + _onEnterOrLeaveImage4Menubar = null, + )) + ], + ), + ); } @override @@ -345,13 +361,6 @@ class _RemotePageState extends State QualityMonitor(_ffi.qualityMonitorModel), null, null), ), ); - paints.add(RemoteMenubar( - id: widget.id, - ffi: _ffi, - state: widget.menubarState, - onEnterOrLeaveImageSetter: (func) => _onEnterOrLeaveImage4Menubar = func, - onEnterOrLeaveImageCleaner: () => _onEnterOrLeaveImage4Menubar = null, - )); return Stack( children: paints, ); diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 64d289fcc..6ad030464 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -297,12 +297,23 @@ class _RemoteMenubarState extends State { ); } + final _chatButtonKey = GlobalKey(); Widget _buildChat(BuildContext context) { return IconButton( + key: _chatButtonKey, tooltip: translate('Chat'), onPressed: () { + RenderBox? renderBox = + _chatButtonKey.currentContext?.findRenderObject() as RenderBox?; + + Offset? initPos; + if (renderBox != null) { + final pos = renderBox.localToGlobal(Offset.zero); + initPos = Offset(pos.dx, pos.dy + _MenubarTheme.dividerHeight); + } + widget.ffi.chatModel.changeCurrentID(ChatModel.clientModeID); - widget.ffi.chatModel.toggleChatOverlay(); + widget.ffi.chatModel.toggleChatOverlay(chatInitPos: initPos); }, icon: const Icon( Icons.message, diff --git a/flutter/lib/models/chat_model.dart b/flutter/lib/models/chat_model.dart index dd35bd22f..b61ce79a7 100644 --- a/flutter/lib/models/chat_model.dart +++ b/flutter/lib/models/chat_model.dart @@ -5,7 +5,6 @@ import 'package:draggable_float_widget/draggable_float_widget.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:get/get_rx/src/rx_types/rx_types.dart'; -import 'package:get/get_state_manager/src/rx_flutter/rx_obx_widget.dart'; import 'package:window_manager/window_manager.dart'; import '../consts.dart'; @@ -30,16 +29,12 @@ class MessageBody { class ChatModel with ChangeNotifier { static final clientModeID = -1; - /// _overlayState: - /// Desktop: store session overlay by using [setOverlayState]. - /// Mobile: always null, use global overlay. - /// see [_getOverlayState] in [showChatIconOverlay] or [showChatWindowOverlay] - OverlayState? _overlayState; OverlayEntry? chatIconOverlayEntry; OverlayEntry? chatWindowOverlayEntry; bool isConnManager = false; RxBool isWindowFocus = true.obs; + PenetrableOverlayState? pOverlayState; final ChatUser me = ChatUser( id: "", @@ -58,6 +53,19 @@ class ChatModel with ChangeNotifier { bool get isShowCMChatPage => _isShowCMChatPage; + void setPenetrableOverlayState(PenetrableOverlayState state) { + pOverlayState = state; + + pOverlayState!.addMiddleBlockedListener((v) { + if (!v) { + isWindowFocus.value = false; + if (isWindowFocus.value) { + isWindowFocus.toggle(); + } + } + }); + } + final WeakReference parent; ChatModel(this.parent); @@ -74,20 +82,6 @@ class ChatModel with ChangeNotifier { } } - setOverlayState(OverlayState? os) { - _overlayState = os; - } - - OverlayState? _getOverlayState() { - if (_overlayState == null) { - if (globalKey.currentState == null || - globalKey.currentState!.overlay == null) return null; - return globalKey.currentState!.overlay; - } else { - return _overlayState; - } - } - showChatIconOverlay({Offset offset = const Offset(200, 50)}) { if (chatIconOverlayEntry != null) { chatIconOverlayEntry!.remove(); @@ -100,7 +94,7 @@ class ChatModel with ChangeNotifier { } } - final overlayState = _getOverlayState(); + final overlayState = pOverlayState?.getOverlayStateOrGlobal(); if (overlayState == null) return; final overlay = OverlayEntry(builder: (context) { @@ -132,33 +126,26 @@ class ChatModel with ChangeNotifier { } } - showChatWindowOverlay() { + showChatWindowOverlay({Offset? chatInitPos}) { if (chatWindowOverlayEntry != null) return; - final overlayState = _getOverlayState(); + isWindowFocus.value = true; + pOverlayState?.setMiddleBlocked(true); + + final overlayState = pOverlayState?.getOverlayStateOrGlobal(); if (overlayState == null) return; final overlay = OverlayEntry(builder: (context) { - bool innerClicked = false; return Listener( onPointerDown: (_) { - if (!innerClicked) { - isWindowFocus.value = false; + if (!isWindowFocus.value) { + isWindowFocus.value = true; + pOverlayState?.setMiddleBlocked(true); } - innerClicked = false; }, - child: Obx(() => Container( - color: isWindowFocus.value ? Colors.red.withOpacity(0.3) : null, - child: Listener( - onPointerDown: (_) { - innerClicked = true; - if (!isWindowFocus.value) { - isWindowFocus.value = true; - } - }, - child: DraggableChatWindow( - position: const Offset(20, 80), - width: 250, - height: 350, - chatModel: this))))); + child: DraggableChatWindow( + position: chatInitPos ?? Offset(20, 80), + width: 250, + height: 350, + chatModel: this)); }); overlayState.insert(overlay); chatWindowOverlayEntry = overlay; @@ -167,6 +154,7 @@ class ChatModel with ChangeNotifier { hideChatWindowOverlay() { if (chatWindowOverlayEntry != null) { + pOverlayState?.setMiddleBlocked(false); chatWindowOverlayEntry!.remove(); chatWindowOverlayEntry = null; return; @@ -176,13 +164,13 @@ class ChatModel with ChangeNotifier { _isChatOverlayHide() => ((!isDesktop && chatIconOverlayEntry == null) || chatWindowOverlayEntry == null); - toggleChatOverlay() { + toggleChatOverlay({Offset? chatInitPos}) { if (_isChatOverlayHide()) { gFFI.invokeMethod("enable_soft_keyboard", true); if (!isDesktop) { showChatIconOverlay(); } - showChatWindowOverlay(); + showChatWindowOverlay(chatInitPos: chatInitPos); } else { hideChatIconOverlay(); hideChatWindowOverlay(); @@ -310,7 +298,6 @@ class ChatModel with ChangeNotifier { close() { hideChatIconOverlay(); hideChatWindowOverlay(); - _overlayState = null; notifyListeners(); } From b2afde4b27e82944250143304529210e6d6ad5aa Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 7 Feb 2023 00:18:25 +0800 Subject: [PATCH 1717/2015] tmp workaround of '-cm' not exit cause rustdesk not launchable from finder --- src/flutter.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/flutter.rs b/src/flutter.rs index d8f83c6b2..761f8a612 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -39,7 +39,8 @@ pub extern "C" fn rustdesk_core_main() -> bool { #[no_mangle] pub extern "C" fn handle_applicationShouldOpenUntitledFile() { hbb_common::log::debug!("icon clicked on finder"); - if std::env::args().nth(1) == Some("--server".to_owned()) { + let x = std::env::args().nth(1).unwrap_or_default(); + if x == "--server" || x == "--cm" { crate::platform::macos::check_main_window(); } } From 1426771ec9cb208ec5d5e06fe643ea2bf0852f1d Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 7 Feb 2023 01:31:11 +0800 Subject: [PATCH 1718/2015] fix: uni links failed to be invoked with --cm running on macOS --- flutter/lib/common.dart | 16 +++++++++++++--- flutter/lib/main.dart | 8 +++++++- flutter/lib/models/native_model.dart | 2 +- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 8236597ff..41043069a 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1292,14 +1292,24 @@ Future initUniLinks() async { } } -StreamSubscription? listenUniLinks() { - if (!(Platform.isWindows || Platform.isMacOS)) { +/// Listen for uni links. +/// +/// * handleByFlutter: Should uni links being handled by Flutter. +/// +/// Returns a [StreamSubscription] which can listen the uni links. +StreamSubscription? listenUniLinks({handleByFlutter = true}) { + if (Platform.isLinux) { return null; } final sub = uriLinkStream.listen((Uri? uri) { + debugPrint("A uri was received: $uri."); if (uri != null) { - callUniLinksUriHandler(uri); + if (handleByFlutter) { + callUniLinksUriHandler(uri); + } else { + bind.sendUrlScheme(url: uri.toString()); + } } else { print("uni listen error: uri is empty."); } diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index a2ae959c0..cc40d962f 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:convert'; import 'dart:io'; @@ -31,6 +32,9 @@ int? kWindowId; WindowType? kWindowType; late List kBootArgs; +/// Uni links. +StreamSubscription? _uniLinkSubscription; + Future main(List args) async { WidgetsFlutterBinding.ensureInitialized(); debugPrint("launch args: $args"); @@ -203,7 +207,7 @@ void runMultiWindow( await restoreWindowPosition(WindowType.PortForward, windowId: kWindowId!); break; default: - // no such appType + // no such appType exit(0); } // show window from hidden status @@ -222,6 +226,8 @@ void runConnectionManagerScreen(bool hide) async { } else { showCmWindow(); } + // Start the uni links handler and redirect links to Native, not for Flutter. + _uniLinkSubscription = listenUniLinks(handleByFlutter: false); } void showCmWindow() { diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index d6885bfb0..628bf502d 100644 --- a/flutter/lib/models/native_model.dart +++ b/flutter/lib/models/native_model.dart @@ -117,7 +117,7 @@ class PlatformFFI { if (Platform.isLinux) { // Start a dbus service, no need to await _ffiBind.mainStartDbusServer(); - } else if (Platform.isMacOS) { + } else if (Platform.isMacOS && isMain) { // Start an ipc server for handling url schemes. _ffiBind.mainStartIpcUrlServer(); } From 9d391d3801b802b5885ed1ab35af9bb01670e07c Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 7 Feb 2023 01:35:38 +0800 Subject: [PATCH 1719/2015] opt: format and name --- flutter/lib/common.dart | 2 +- flutter/lib/main.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 41043069a..30d38b8db 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1294,7 +1294,7 @@ Future initUniLinks() async { /// Listen for uni links. /// -/// * handleByFlutter: Should uni links being handled by Flutter. +/// * handleByFlutter: Should uni links be handled by Flutter. /// /// Returns a [StreamSubscription] which can listen the uni links. StreamSubscription? listenUniLinks({handleByFlutter = true}) { diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index cc40d962f..c19adf753 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -207,7 +207,7 @@ void runMultiWindow( await restoreWindowPosition(WindowType.PortForward, windowId: kWindowId!); break; default: - // no such appType + // no such appType exit(0); } // show window from hidden status From 564b35d4c2a0bd9266c8b76f3eb6f6d6293e2a41 Mon Sep 17 00:00:00 2001 From: Andrzej Rudnik Date: Mon, 6 Feb 2023 21:29:36 +0100 Subject: [PATCH 1720/2015] Update pl.rs --- src/lang/pl.rs | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/lang/pl.rs b/src/lang/pl.rs index daf4a7846..b7ccbdbb1 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -270,8 +270,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OPEN", "Otwórz"), ("Chat", "Czat"), ("Total", "Łącznie"), - ("items", "elementy"), - ("Selected", "Zaznaczone"), + ("items", "elementów"), + ("Selected", "Zaznaczonych"), ("Screen Capture", "Przechwytywanie ekranu"), ("Input Control", "Kontrola wejścia"), ("Audio Capture", "Przechwytywanie dźwięku"), @@ -345,7 +345,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Dark Theme", "Ciemny motyw"), ("Dark", "Ciemny"), ("Light", "Jasny"), - ("Follow System", "Zgodne z systemem"), + ("Follow System", "Zgodny z systemem"), ("Enable hardware codec", "Włącz akcelerację sprzętową kodeków"), ("Unlock Security Settings", "Odblokuj ustawienia zabezpieczeń"), ("Enable Audio", "Włącz dźwięk"), @@ -392,7 +392,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("or", "albo"), ("Continue with", "Kontynuuj z"), ("Elevate", "Uzyskaj uprawnienia"), - ("Zoom cursor", "Zoom kursora"), + ("Zoom cursor", "Powiększenie kursora"), ("Accept sessions via password", "Uwierzytelnij sesję używając hasła"), ("Accept sessions via click", "Uwierzytelnij sesję poprzez kliknięcie"), ("Accept sessions via both", "Uwierzytelnij sesję za pomocą obu sposobów"), @@ -407,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Right click to select tabs", "Kliknij prawym przyciskiem myszy by wybrać zakładkę"), ("Skipped", "Pominięte"), ("Add to Address Book", "Dodaj do Książki Adresowej"), - ("Group", "Grypy"), + ("Group", "Grupy"), ("Search", "Szukaj"), ("Closed manually by web console", "Zakończone manualnie z konsoli Web"), ("Local keyboard type", "Lokalny typ klawiatury"), @@ -433,17 +433,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", "Słabe"), ("Medium", "Średnie"), ("Strong", "Mocne"), - ("Switch Sides", "Zmień Strony"), + ("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", ""), - ("Default View Style", ""), - ("Default Scroll Style", ""), - ("Default Image Quality", ""), - ("Default Codec", ""), - ("Bitrate", ""), - ("FPS", ""), - ("Auto", ""), - ("Other Default Options", ""), + ("Display", "Wyświetlanie"), + ("Default View Style", "Domyślny styl wyświetlania"), + ("Default Scroll Style", "Domyślny styl przewijania"), + ("Default Image Quality", "Domyślna jakość obrazu"), + ("Default Codec", "Dokyślny kodek"), + ("Bitrate", "Bitrate"), + ("FPS", "FPS"), + ("Auto", "Auto"), + ("Other Default Options", "Inne opcje domyślne"), ].iter().cloned().collect(); } From bcbc1573aa9f13114987dba9015a411f08fb8d59 Mon Sep 17 00:00:00 2001 From: jimmyGALLAND <64364019+jimmyGALLAND@users.noreply.github.com> Date: Mon, 6 Feb 2023 22:45:27 +0100 Subject: [PATCH 1721/2015] Update fr.rs --- src/lang/fr.rs | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 19b932d2f..3b7f23ab9 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -63,7 +63,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Skip", "Ignorer"), ("Close", "Fermer"), ("Retry", "Réessayer"), - ("OK", "Confirmer"), + ("OK", "Valider"), ("Password Required", "Mot de passe requis"), ("Please enter your password", "Veuillez saisir votre mot de passe"), ("Remember password", "Mémoriser le mot de passe"), @@ -126,7 +126,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Afficher le moniteur de qualité"), ("Disable clipboard", "Désactiver le presse-papier"), ("Lock after session end", "Verrouiller l'ordinateur distant après la déconnexion"), - ("Insert", "Insérer"), + ("Insert", "Envoyer"), ("Insert Lock", "Verrouiller l'ordinateur distant"), ("Refresh", "Rafraîchir l'écran"), ("ID does not exist", "L'ID n'existe pas"), @@ -291,7 +291,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Overwrite", "Écraser"), ("This file exists, skip or overwrite this file?", "Ce fichier existe, ignorer ou écraser ce fichier ?"), ("Quit", "Quitter"), - ("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"), + ("doc_mac_permission", "https://rustdesk.com/docs/fr/manual/mac/#enable-permissions"), ("Help", "Aider"), ("Failed", "échouer"), ("Succeeded", "Succès"), @@ -435,15 +435,15 @@ 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", ""), - ("Display", ""), - ("Default View Style", ""), - ("Default Scroll Style", ""), - ("Default Image Quality", ""), - ("Default Codec", ""), - ("Bitrate", ""), - ("FPS", ""), - ("Auto", ""), - ("Other Default Options", ""), + ("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"), + ("Default Image Quality", "Qualité d'image par défaut"), + ("Default Codec", "Codec par défaut"), + ("Bitrate", "Débit"), + ("FPS", "FPS"), + ("Auto", "Auto"), + ("Other Default Options", "Autres options par défaut"), ].iter().cloned().collect(); } From e1a9cfcf7f841e57715709f8adcd6407a2e1062b Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 7 Feb 2023 12:47:07 +0800 Subject: [PATCH 1722/2015] fix flink Signed-off-by: 21pages --- src/server/portable_service.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/server/portable_service.rs b/src/server/portable_service.rs index a2f6fb829..c783fef52 100644 --- a/src/server/portable_service.rs +++ b/src/server/portable_service.rs @@ -118,11 +118,9 @@ impl SharedMemory { fn flink(name: String) -> ResultType { let disk = std::env::var("SystemDrive").unwrap_or("C:".to_string()); - let mut dir = PathBuf::from(disk); - let dir1 = dir.join("ProgramData"); - let dir2 = std::env::var("TEMP") - .map(|d| PathBuf::from(d)) - .unwrap_or(dir.join("Windows").join("Temp")); + let dir1 = PathBuf::from(format!("{}\\ProgramData", disk)); + let dir2 = PathBuf::from(format!("{}\\Windows\\Temp", disk)); + let mut dir; if dir1.exists() { dir = dir1; } else if dir2.exists() { From cf3ddb2a183bcfdd506942fde57f69c36058756b Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 7 Feb 2023 15:16:49 +0800 Subject: [PATCH 1723/2015] filter foreground window to avoid frequent prompts Signed-off-by: 21pages --- src/platform/windows.rs | 31 ++++++++++++++++++++++++++++++- src/server/video_service.rs | 5 +---- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 2e0d56eab..17f275c2a 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -11,6 +11,7 @@ use std::io::prelude::*; use std::{ ffi::OsString, fs, io, mem, + os::windows::process::CommandExt, path::PathBuf, sync::{Arc, Mutex}, time::{Duration, Instant}, @@ -1644,6 +1645,29 @@ pub fn is_elevated(process_id: Option) -> ResultType { } } +#[inline] +fn filter_foreground_window(process_id: DWORD) -> ResultType { + if let Ok(output) = std::process::Command::new("tasklist") + .args(vec![ + "/SVC", + "/NH", + "/FI", + &format!("PID eq {}", process_id), + ]) + .creation_flags(CREATE_NO_WINDOW) + .output() + { + let s = String::from_utf8_lossy(&output.stdout) + .to_string() + .to_lowercase(); + Ok(["Taskmgr", "mmc", "regedit"] + .iter() + .any(|name| s.contains(&name.to_string().to_lowercase()))) + } else { + bail!("run tasklist failed"); + } +} + pub fn is_foreground_window_elevated() -> ResultType { unsafe { let mut process_id: DWORD = 0; @@ -1651,7 +1675,12 @@ pub fn is_foreground_window_elevated() -> ResultType { if process_id == 0 { bail!("Failed to get processId, errno {}", GetLastError()) } - is_elevated(Some(process_id)) + let elevated = is_elevated(Some(process_id))?; + if elevated { + filter_foreground_window(process_id) + } else { + Ok(false) + } } } diff --git a/src/server/video_service.rs b/src/server/video_service.rs index 55920e320..57fdf2c22 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -954,10 +954,7 @@ pub fn get_current_display() -> ResultType<(usize, usize, Display)> { fn start_uac_elevation_check() { static START: Once = Once::new(); START.call_once(|| { - if !crate::platform::is_installed() - && !crate::platform::is_root() - && !crate::portable_service::client::running() - { + if !crate::platform::is_installed() && !crate::platform::is_root() { std::thread::spawn(|| loop { std::thread::sleep(std::time::Duration::from_secs(1)); if let Ok(uac) = crate::ui::win_privacy::is_process_consent_running() { From 8aba51c1202e45067d1fc2a541cc096fccf5d4d4 Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 7 Feb 2023 15:39:46 +0800 Subject: [PATCH 1724/2015] fix cm push_event Signed-off-by: 21pages --- src/flutter.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/flutter.rs b/src/flutter.rs index 761f8a612..b4f1f6bc6 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -14,6 +14,7 @@ use std::{ }; pub(super) const APP_TYPE_MAIN: &str = "main"; +pub(super) const APP_TYPE_CM: &str = "cm"; pub(super) const APP_TYPE_DESKTOP_REMOTE: &str = "remote"; pub(super) const APP_TYPE_DESKTOP_FILE_TRANSFER: &str = "file transfer"; pub(super) const APP_TYPE_DESKTOP_PORT_FORWARD: &str = "port forward"; @@ -528,11 +529,7 @@ pub mod connection_manager { assert!(h.get("name").is_none()); h.insert("name", name); - if let Some(s) = GLOBAL_EVENT_STREAM - .read() - .unwrap() - .get(super::APP_TYPE_MAIN) - { + 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())); }; } From e0f73ccc28e2f85cb999d03281f29d2fdaf6280d Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 7 Feb 2023 16:17:00 +0800 Subject: [PATCH 1725/2015] remove docs/SECURITY.md to disable security report which is not the report we imagine --- docs/SECURITY.md | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 docs/SECURITY.md diff --git a/docs/SECURITY.md b/docs/SECURITY.md deleted file mode 100644 index f1114f913..000000000 --- a/docs/SECURITY.md +++ /dev/null @@ -1,13 +0,0 @@ -# Security Policy - -## Supported Versions - -| Version | Supported | -| --------- | ------------------ | -| 1.1.x | :white_check_mark: | -| 1.x | :white_check_mark: | -| Below 1.0 | :x: | - -## Reporting a Vulnerability - -Here we should write what to do in case of a security vulnerability From 28ad271693c4ba550d41b82a68a7a90d392118dc Mon Sep 17 00:00:00 2001 From: Kingtous Date: Thu, 3 Nov 2022 21:09:37 +0800 Subject: [PATCH 1726/2015] wip: dual audio transmission server --- src/client/io_loop.rs | 54 +++++++++++++++++++++++++++++++++++++++- src/server.rs | 21 ++++++++++++++++ src/server/connection.rs | 6 +++++ 3 files changed, 80 insertions(+), 1 deletion(-) diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 0178fe9e8..bcbea994b 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -2,13 +2,16 @@ use crate::client::{ Client, CodecFormat, MediaData, MediaSender, QualityStatus, MILLI1, SEC30, SERVER_CLIPBOARD_ENABLED, SERVER_FILE_TRANSFER_ENABLED, SERVER_KEYBOARD_ENABLED, }; -use crate::common; #[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::common::{check_clipboard, update_clipboard, ClipboardContext, CLIPBOARD_INTERVAL}; +use crate::{audio_service, common, ConnInner, CLIENT_SERVER}; #[cfg(windows)] use clipboard::{cliprdr::CliprdrClientContext, ContextSend}; +use hbb_common::futures::channel::mpsc::unbounded; +use hbb_common::tokio::sync::mpsc::error::TryRecvError; +use crate::server::Service; use crate::ui_session_interface::{InvokeUiSession, Session}; use crate::{client::Data, client::Interface}; @@ -253,6 +256,55 @@ impl Remote { } } + // Start a local audio recorder, records audio and send to remote + fn start_client_audio( + &mut self, + audio_sender: MediaSender, + ) -> Option> { + if self.handler.is_file_transfer() || self.handler.is_port_forward() { + return None; + } + // Create a channel to receive error or closed message + let (tx, rx) = std::sync::mpsc::channel(); + let (tx_audio_data, mut rx_audio_data) = hbb_common::tokio::sync::mpsc::unbounded_channel(); + // Create a stand-alone inner, add subscribe to audio service + let client_conn_inner = ConnInner::new( + CLIENT_SERVER.write().unwrap().get_new_id(), + Some(tx_audio_data), + None, + ); + CLIENT_SERVER + .write() + .unwrap() + .subscribe(audio_service::NAME, client_conn_inner, true); + std::thread::spawn(move || { + loop { + // check if client is closed + match rx.try_recv() { + Ok(_) | Err(std::sync::mpsc::TryRecvError::Disconnected) => { + log::debug!("Exit local audio service of client"); + break; + } + _ => {} + } + match rx_audio_data.try_recv() { + Ok((instant, msg)) => match msg.union { + Some(_) => todo!(), + None => todo!(), + }, + Err(err) => { + if err == TryRecvError::Empty { + // ignore + } else { + log::debug!("Failed to record local audio channel: {}", err); + } + } + } + } + }); + Some(tx) + } + fn start_clipboard(&mut self) -> Option> { if self.handler.is_file_transfer() || self.handler.is_port_forward() { return None; diff --git a/src/server.rs b/src/server.rs index 109fc1e9a..bef49f132 100644 --- a/src/server.rs +++ b/src/server.rs @@ -29,6 +29,13 @@ use service::{GenericService, Service, Subscriber}; use service::ServiceTmpl; use crate::ipc::{connect, Data}; +pub use service::{GenericService, Service, ServiceTmpl, Subscriber}; +use std::{ + collections::HashMap, + net::SocketAddr, + sync::{Arc, Mutex, RwLock, Weak}, + time::Duration, +}; pub mod audio_service; cfg_if::cfg_if! { @@ -65,6 +72,13 @@ type ConnMap = HashMap; lazy_static::lazy_static! { pub static ref CHILD_PROCESS: Childs = Default::default(); pub static ref CONN_COUNT: Arc> = Default::default(); + // A client server used to provide local services(audio, video, clipboard, etc.) + // for all initiative connections. + // + // [Note] + // Now we use this [`CLIENT_SERVER`] to do following operations: + // - record local audio, and send to remote + pub static ref CLIENT_SERVER: ServerPtr = new(); } pub struct Server { @@ -316,6 +330,13 @@ impl Server { } } } + + // get a new unique id + pub fn get_new_id(&mut self) -> i32 { + let new_id = self.id_count; + self.id_count += 1; + new_id + } } impl Drop for Server { diff --git a/src/server/connection.rs b/src/server/connection.rs index e4b667d54..d340021ad 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -108,6 +108,12 @@ pub struct Connection { from_switch: bool, } +impl ConnInner { + pub fn new(id: i32, tx: Option, tx_video: Option) -> Self { + Self { id, tx, tx_video } + } +} + impl Subscriber for ConnInner { #[inline] fn id(&self) -> i32 { From 1f40963b5d23fd4cc6c7be75aa55077b977ed5f0 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 4 Nov 2022 12:02:17 +0800 Subject: [PATCH 1727/2015] wip: connection --- src/client.rs | 16 ++++++++++--- src/client/io_loop.rs | 51 ++++++++++++++++++++++++++++------------ src/flutter_ffi.rs | 3 +++ src/server/connection.rs | 8 +++++++ 4 files changed, 60 insertions(+), 18 deletions(-) diff --git a/src/client.rs b/src/client.rs index e0ac68c5d..08a8de747 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1543,7 +1543,6 @@ where F: 'static + FnMut(&[u8]) + Send, { let (video_sender, video_receiver) = mpsc::channel::(); - let (audio_sender, audio_receiver) = mpsc::channel::(); let mut video_callback = video_callback; let latency_controller = LatencyController::new(); @@ -1573,8 +1572,19 @@ where } log::info!("Video decoder loop exits"); }); + let audio_sender = start_audio_thread(Some(latency_controller_cl)); + return (video_sender, audio_sender); +} + +/// Start an audio thread +/// Return a audio [`MediaSender`] +pub fn start_audio_thread( + latency_controller: Option>>, +) -> MediaSender { + let latency_controller = latency_controller.unwrap_or(LatencyController::new()); + let (audio_sender, audio_receiver) = mpsc::channel::(); std::thread::spawn(move || { - let mut audio_handler = AudioHandler::new(latency_controller_cl); + let mut audio_handler = AudioHandler::new(latency_controller); loop { if let Ok(data) = audio_receiver.recv() { match data { @@ -1592,7 +1602,7 @@ where } log::info!("Audio decoder loop exits"); }); - return (video_sender, audio_sender); + audio_sender } /// Handle latency test. diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index bcbea994b..857f94891 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -32,6 +32,7 @@ use hbb_common::tokio::{ }; use hbb_common::{allow_err, message_proto::*, sleep}; use hbb_common::{fs, log, Stream}; +use std::borrow::Borrow; use std::collections::HashMap; use std::sync::atomic::{AtomicUsize, Ordering}; @@ -89,6 +90,7 @@ impl Remote { pub async fn io_loop(&mut self, key: &str, token: &str) { let stop_clipboard = self.start_clipboard(); + let stop_client_audio = self.start_client_audio(); let mut last_recv_time = Instant::now(); let mut received = false; let conn_type = if self.handler.is_file_transfer() { @@ -96,6 +98,7 @@ impl Remote { } else { ConnType::default() }; + match Client::start( &self.handler.id, key, @@ -224,6 +227,9 @@ impl Remote { if let Some(stop) = stop_clipboard { stop.send(()).ok(); } + if let Some(stop) = stop_client_audio { + stop.send(()).ok(); + } SERVER_KEYBOARD_ENABLED.store(false, Ordering::SeqCst); SERVER_CLIPBOARD_ENABLED.store(false, Ordering::SeqCst); SERVER_FILE_TRANSFER_ENABLED.store(false, Ordering::SeqCst); @@ -257,10 +263,7 @@ impl Remote { } // Start a local audio recorder, records audio and send to remote - fn start_client_audio( - &mut self, - audio_sender: MediaSender, - ) -> Option> { + fn start_client_audio(&mut self) -> Option> { if self.handler.is_file_transfer() || self.handler.is_port_forward() { return None; } @@ -268,29 +271,47 @@ impl Remote { let (tx, rx) = std::sync::mpsc::channel(); let (tx_audio_data, mut rx_audio_data) = hbb_common::tokio::sync::mpsc::unbounded_channel(); // Create a stand-alone inner, add subscribe to audio service - let client_conn_inner = ConnInner::new( - CLIENT_SERVER.write().unwrap().get_new_id(), - Some(tx_audio_data), - None, + let conn_id = CLIENT_SERVER.write().unwrap().get_new_id(); + let client_conn_inner = ConnInner::new(conn_id.clone(), Some(tx_audio_data), None); + // now we subscribe + CLIENT_SERVER.write().unwrap().subscribe( + audio_service::NAME, + client_conn_inner.clone(), + true, ); - CLIENT_SERVER - .write() - .unwrap() - .subscribe(audio_service::NAME, client_conn_inner, true); + let tx_audio = self.sender.clone(); std::thread::spawn(move || { loop { // check if client is closed match rx.try_recv() { Ok(_) | Err(std::sync::mpsc::TryRecvError::Disconnected) => { log::debug!("Exit local audio service of client"); + // unsubscribe + CLIENT_SERVER.write().unwrap().subscribe( + audio_service::NAME, + client_conn_inner, + false, + ); break; } _ => {} } match rx_audio_data.try_recv() { - Ok((instant, msg)) => match msg.union { - Some(_) => todo!(), - None => todo!(), + Ok((instant, msg)) => match &msg.union { + Some(message::Union::AudioFrame(frame)) => { + let mut msg = Message::new(); + msg.set_audio_frame(frame.clone()); + tx_audio.send(Data::Message(msg)).ok(); + log::debug!("send audio frame {}", frame.timestamp); + } + Some(message::Union::Misc(misc)) => { + let mut msg = Message::new(); + msg.set_misc(misc.clone()); + tx_audio.send(Data::Message(msg)).ok(); + log::debug!("send audio misc {:?}", misc.audio_format()); + } + _ => {} + None => {} }, Err(err) => { if err == TryRecvError::Empty { diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index ca9314c43..4b671ff1b 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1244,6 +1244,9 @@ pub fn main_current_is_wayland() -> SyncReturn { pub fn main_is_login_wayland() -> SyncReturn { SyncReturn(is_login_wayland()) +pub fn main_start_pa() { + #[cfg(target_os = "linux")] + std::thread::spawn(crate::ipc::start_pa); } pub fn main_hide_docker() -> SyncReturn { diff --git a/src/server/connection.rs b/src/server/connection.rs index d340021ad..34adeb59b 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1533,6 +1533,10 @@ impl Connection { } _ => {} }, + Some(misc::Union::AudioFormat(format)) => { + // TODO: implement audio format handler + println!("recv audio format"); + } #[cfg(feature = "flutter")] Some(misc::Union::SwitchSidesRequest(s)) => { if let Ok(uuid) = uuid::Uuid::from_slice(&s.uuid.to_vec()[..]) { @@ -1550,6 +1554,10 @@ impl Connection { } _ => {} }, + Some(message::Union::AudioFrame(audio_frame)) => { + // TODO: implement audio frame handler + println!("recv audio frame"); + } _ => {} } } From 65ab43aa4a9ebc7563a1eceb822c57f309d24eb3 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sat, 17 Dec 2022 10:39:07 +0800 Subject: [PATCH 1728/2015] opt: compile --- src/flutter_ffi.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 4b671ff1b..d9f67e566 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1244,6 +1244,8 @@ pub fn main_current_is_wayland() -> SyncReturn { pub fn main_is_login_wayland() -> SyncReturn { SyncReturn(is_login_wayland()) +} + pub fn main_start_pa() { #[cfg(target_os = "linux")] std::thread::spawn(crate::ipc::start_pa); From 8e2d6945d0e0b3fd16de4d7b8883867c3236a4c8 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sun, 29 Jan 2023 11:55:37 +0800 Subject: [PATCH 1729/2015] feat: add audio thread in server being controlled --- src/client/io_loop.rs | 3 +-- src/server/connection.rs | 19 +++++++++++++------ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 857f94891..bac1e5d23 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -297,7 +297,7 @@ impl Remote { _ => {} } match rx_audio_data.try_recv() { - Ok((instant, msg)) => match &msg.union { + Ok((_instant, msg)) => match &msg.union { Some(message::Union::AudioFrame(frame)) => { let mut msg = Message::new(); msg.set_audio_frame(frame.clone()); @@ -311,7 +311,6 @@ impl Remote { log::debug!("send audio misc {:?}", misc.audio_format()); } _ => {} - None => {} }, Err(err) => { if err == TryRecvError::Empty { diff --git a/src/server/connection.rs b/src/server/connection.rs index 34adeb59b..2ee3bc8ed 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -5,7 +5,7 @@ use crate::clipboard_file::*; use crate::common::update_clipboard; #[cfg(windows)] use crate::portable_service::client as portable_client; -use crate::video_service; +use crate::{video_service, client::{MediaSender, start_audio_thread, LatencyController, MediaData}}; #[cfg(any(target_os = "android", target_os = "ios"))] use crate::{common::DEVICE_NAME, flutter::connection_manager::start_channel}; use crate::{ipc, VERSION}; @@ -95,6 +95,7 @@ pub struct Connection { disable_clipboard: bool, // by peer disable_audio: bool, // by peer enable_file_transfer: bool, // by peer + audio_sender: MediaSender, // audio by the remote peer/client tx_input: std_mpsc::Sender, // handle input messages video_ack_required: bool, peer_info: (String, String), @@ -168,6 +169,9 @@ impl Connection { let mut hbbs_rx = crate::hbbs_http::sync::signal_receiver(); let tx_cloned = tx.clone(); + // Start a audio thread to play the audio sent by peer. + let latency_controller = LatencyController::new(); + let audio_sender = start_audio_thread(Some(latency_controller)); let mut conn = Self { inner: ConnInner { id, @@ -209,6 +213,7 @@ impl Connection { #[cfg(windows)] portable: Default::default(), from_switch: false, + audio_sender, }; #[cfg(not(any(target_os = "android", target_os = "ios")))] tokio::spawn(async move { @@ -1534,8 +1539,9 @@ impl Connection { _ => {} }, Some(misc::Union::AudioFormat(format)) => { - // TODO: implement audio format handler - println!("recv audio format"); + if !self.disable_audio { + allow_err!(self.audio_sender.send(MediaData::AudioFormat(format))); + } } #[cfg(feature = "flutter")] Some(misc::Union::SwitchSidesRequest(s)) => { @@ -1554,9 +1560,10 @@ impl Connection { } _ => {} }, - Some(message::Union::AudioFrame(audio_frame)) => { - // TODO: implement audio frame handler - println!("recv audio frame"); + Some(message::Union::AudioFrame(frame)) => { + if !self.disable_audio { + allow_err!(self.audio_sender.send(MediaData::AudioFrame(frame))); + } } _ => {} } From 45a6fc361883a6fd1ff76a4fe3a7ca9bd54b09da Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sun, 29 Jan 2023 14:10:06 +0800 Subject: [PATCH 1730/2015] opt: remove latency detector on single audio --- src/client.rs | 5 +++++ src/client/helper.rs | 11 +++++++++++ src/server/connection.rs | 4 +++- 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/client.rs b/src/client.rs index 08a8de747..b2cd0f2f7 100644 --- a/src/client.rs +++ b/src/client.rs @@ -714,6 +714,7 @@ impl AudioHandler { .check_audio(frame.timestamp) .not() { + log::debug!("audio frame {} is ignored", frame.timestamp); return; } } @@ -724,6 +725,7 @@ impl AudioHandler { } #[cfg(target_os = "linux")] if self.simple.is_none() { + log::debug!("PulseAudio simple binding does not exists"); return; } #[cfg(target_os = "android")] @@ -768,6 +770,7 @@ impl AudioHandler { unsafe { std::slice::from_raw_parts::(buffer.as_ptr() as _, n * 4) }; self.simple.as_mut().map(|x| x.write(data_u8)); } + log::debug!("write Audio frame {} to system.", frame.timestamp); } }); } @@ -1589,9 +1592,11 @@ pub fn start_audio_thread( if let Ok(data) = audio_receiver.recv() { match data { MediaData::AudioFrame(af) => { + log::debug!("recved audio frame={}", af.timestamp); audio_handler.handle_frame(af); } MediaData::AudioFormat(f) => { + log::debug!("recved audio format, sample rate={}", f.sample_rate); audio_handler.handle_format(f); } _ => {} diff --git a/src/client/helper.rs b/src/client/helper.rs index e4736c0e8..005b2df72 100644 --- a/src/client/helper.rs +++ b/src/client/helper.rs @@ -18,6 +18,7 @@ pub struct LatencyController { last_video_remote_ts: i64, // generated on remote device update_time: Instant, allow_audio: bool, + enabled: bool } impl Default for LatencyController { @@ -26,6 +27,7 @@ impl Default for LatencyController { last_video_remote_ts: Default::default(), update_time: Instant::now(), allow_audio: Default::default(), + enabled: true } } } @@ -36,6 +38,11 @@ impl LatencyController { Arc::new(Mutex::new(LatencyController::default())) } + /// Set whether this [LatencyController] should be enabled. + pub fn set_enabled(&mut self, enable: bool) { + self.enabled = enable; + } + /// Update the latency controller with the latest video timestamp. pub fn update_video(&mut self, timestamp: i64) { self.last_video_remote_ts = timestamp; @@ -44,6 +51,10 @@ impl LatencyController { /// Check if the audio should be played based on the current latency. pub fn check_audio(&mut self, timestamp: i64) -> bool { + if !self.enabled { + self.allow_audio = true; + return self.allow_audio; + } // Compute audio latency. let expected = self.update_time.elapsed().as_millis() as i64 + self.last_video_remote_ts; let latency = expected - timestamp; diff --git a/src/server/connection.rs b/src/server/connection.rs index 2ee3bc8ed..1924cfca0 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -171,6 +171,8 @@ impl Connection { let tx_cloned = tx.clone(); // Start a audio thread to play the audio sent by peer. let latency_controller = LatencyController::new(); + // No video frame will be sent here, so we need to disable latency controller, or audio check may fail. + latency_controller.lock().unwrap().set_enabled(false); let audio_sender = start_audio_thread(Some(latency_controller)); let mut conn = Self { inner: ConnInner { @@ -1561,7 +1563,7 @@ impl Connection { _ => {} }, Some(message::Union::AudioFrame(frame)) => { - if !self.disable_audio { + if !self.disable_audio { allow_err!(self.audio_sender.send(MediaData::AudioFrame(frame))); } } From e7e8e1a18b6e3bcdf190931869527489704296a4 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sun, 29 Jan 2023 22:23:18 +0800 Subject: [PATCH 1731/2015] opt: send audio frame when connected --- src/client/io_loop.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index bac1e5d23..f16c9af72 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -90,7 +90,6 @@ impl Remote { pub async fn io_loop(&mut self, key: &str, token: &str) { let stop_clipboard = self.start_clipboard(); - let stop_client_audio = self.start_client_audio(); let mut last_recv_time = Instant::now(); let mut received = false; let conn_type = if self.handler.is_file_transfer() { @@ -114,6 +113,8 @@ impl Remote { SERVER_FILE_TRANSFER_ENABLED.store(true, Ordering::SeqCst); self.handler.set_connection_type(peer.is_secured(), direct); // flutter -> connection_ready self.handler.set_connection_info(direct, false); + // Start client audio when connection is established. + let stop_client_audio = self.start_client_audio(); // just build for now #[cfg(not(windows))] @@ -218,6 +219,10 @@ impl Remote { } } log::debug!("Exit io_loop of id={}", self.handler.id); + // Stop client audio server. + if let Some(stop) = stop_client_audio { + stop.send(()).ok(); + } } Err(err) => { self.handler @@ -227,9 +232,6 @@ impl Remote { if let Some(stop) = stop_clipboard { stop.send(()).ok(); } - if let Some(stop) = stop_client_audio { - stop.send(()).ok(); - } SERVER_KEYBOARD_ENABLED.store(false, Ordering::SeqCst); SERVER_CLIPBOARD_ENABLED.store(false, Ordering::SeqCst); SERVER_FILE_TRANSFER_ENABLED.store(false, Ordering::SeqCst); From 4f3c5b42ae158a52a1d276964b38034241e0b187 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 30 Jan 2023 01:39:42 +0800 Subject: [PATCH 1732/2015] opt: send audio format and data after login successfully. --- src/client/io_loop.rs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index f16c9af72..d568feb4e 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -44,6 +44,8 @@ pub struct Remote { audio_sender: MediaSender, receiver: mpsc::UnboundedReceiver, sender: mpsc::UnboundedSender, + // Stop sending local audio to remote client. + stop_local_audio_sender: Option>, old_clipboard: Arc>, read_jobs: Vec, write_jobs: Vec, @@ -85,6 +87,7 @@ impl Remote { data_count: Arc::new(AtomicUsize::new(0)), frame_count, video_format: CodecFormat::Unknown, + stop_local_audio_sender: None, } } @@ -113,8 +116,6 @@ impl Remote { SERVER_FILE_TRANSFER_ENABLED.store(true, Ordering::SeqCst); self.handler.set_connection_type(peer.is_secured(), direct); // flutter -> connection_ready self.handler.set_connection_info(direct, false); - // Start client audio when connection is established. - let stop_client_audio = self.start_client_audio(); // just build for now #[cfg(not(windows))] @@ -220,8 +221,8 @@ impl Remote { } log::debug!("Exit io_loop of id={}", self.handler.id); // Stop client audio server. - if let Some(stop) = stop_client_audio { - stop.send(()).ok(); + if let Some(s) = self.stop_local_audio_sender.take() { + s.send(()).ok(); } } Err(err) => { @@ -865,6 +866,15 @@ impl Remote { }); } } + // Start audio thread for playback + if !self.handler.is_file_transfer() && !self.handler.is_port_forward() { + // Cancel previous local audio session. + if let Some(sender) = self.stop_local_audio_sender.take() { + allow_err!(sender.send(())); + } + // Start client audio when connection is established. + self.stop_local_audio_sender = self.start_client_audio(); + } if self.handler.is_file_transfer() { self.handler.load_last_jobs(); From 3b34e2ea453fe6a7667e980fcd05b5d009cc065c Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 30 Jan 2023 11:15:47 +0800 Subject: [PATCH 1733/2015] feat: run local audio server at start --- flutter/lib/models/native_model.dart | 8 ++++++-- src/ui.rs | 3 +++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index 628bf502d..34a673953 100644 --- a/flutter/lib/models/native_model.dart +++ b/flutter/lib/models/native_model.dart @@ -118,8 +118,12 @@ class PlatformFFI { // Start a dbus service, no need to await _ffiBind.mainStartDbusServer(); } else if (Platform.isMacOS && isMain) { - // Start an ipc server for handling url schemes. - _ffiBind.mainStartIpcUrlServer(); + Future.wait([ + // Start dbus service. + _ffiBind.mainStartDbusServer(), + // Start local audio pulseaudio server. + _ffiBind.mainStartPa() + ]); } _startListenEvent(_ffiBind); // global event try { diff --git a/src/ui.rs b/src/ui.rs index 8763194fe..7973a0ba4 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -95,6 +95,9 @@ pub fn start(args: &mut [String]) { frame.event_handler(UI {}); frame.sciter_handler(UIHostHandler {}); page = "index.html"; + // Start pulse audio local server. + #[cfg(target_os = "linux")] + std::thread::spawn(crate::ipc::start_pa); } else if args[0] == "--install" { frame.event_handler(UI {}); frame.sciter_handler(UIHostHandler {}); From 9134c2826e0205aaff80cffe299c0f5c6a71ecc0 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 30 Jan 2023 11:32:46 +0800 Subject: [PATCH 1734/2015] feat: set audio only mode --- src/client/helper.rs | 19 ++++++++++--------- src/server/connection.rs | 2 +- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/client/helper.rs b/src/client/helper.rs index 005b2df72..248cf5928 100644 --- a/src/client/helper.rs +++ b/src/client/helper.rs @@ -18,7 +18,7 @@ pub struct LatencyController { last_video_remote_ts: i64, // generated on remote device update_time: Instant, allow_audio: bool, - enabled: bool + audio_only: bool } impl Default for LatencyController { @@ -27,7 +27,7 @@ impl Default for LatencyController { last_video_remote_ts: Default::default(), update_time: Instant::now(), allow_audio: Default::default(), - enabled: true + audio_only: true } } } @@ -38,9 +38,9 @@ impl LatencyController { Arc::new(Mutex::new(LatencyController::default())) } - /// Set whether this [LatencyController] should be enabled. - pub fn set_enabled(&mut self, enable: bool) { - self.enabled = enable; + /// Set whether this [LatencyController] should be working in audio only mode. + pub fn set_audio_only(&mut self, only: bool) { + self.audio_only = only; } /// Update the latency controller with the latest video timestamp. @@ -51,10 +51,6 @@ impl LatencyController { /// Check if the audio should be played based on the current latency. pub fn check_audio(&mut self, timestamp: i64) -> bool { - if !self.enabled { - self.allow_audio = true; - return self.allow_audio; - } // Compute audio latency. let expected = self.update_time.elapsed().as_millis() as i64 + self.last_video_remote_ts; let latency = expected - timestamp; @@ -70,6 +66,11 @@ impl LatencyController { self.allow_audio = true; } } + // No video frame here, which means the update time is not triggered. + // We manually update the time here. + if self.audio_only { + self.update_time = Instant::now(); + } self.allow_audio } } diff --git a/src/server/connection.rs b/src/server/connection.rs index 1924cfca0..d5c2103b1 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -172,7 +172,7 @@ impl Connection { // Start a audio thread to play the audio sent by peer. let latency_controller = LatencyController::new(); // No video frame will be sent here, so we need to disable latency controller, or audio check may fail. - latency_controller.lock().unwrap().set_enabled(false); + latency_controller.lock().unwrap().set_audio_only(true); let audio_sender = start_audio_thread(Some(latency_controller)); let mut conn = Self { inner: ConnInner { From 95d06e160b21df29a62926fefd46c9f318c2cae1 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 30 Jan 2023 11:51:03 +0800 Subject: [PATCH 1735/2015] fix: latency --- src/client/helper.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/client/helper.rs b/src/client/helper.rs index 248cf5928..e3acf3a44 100644 --- a/src/client/helper.rs +++ b/src/client/helper.rs @@ -27,7 +27,7 @@ impl Default for LatencyController { last_video_remote_ts: Default::default(), update_time: Instant::now(), allow_audio: Default::default(), - audio_only: true + audio_only: false } } } @@ -53,7 +53,11 @@ impl LatencyController { pub fn check_audio(&mut self, timestamp: i64) -> bool { // Compute audio latency. let expected = self.update_time.elapsed().as_millis() as i64 + self.last_video_remote_ts; - let latency = expected - timestamp; + let latency = if self.audio_only { + expected + } else { + expected - timestamp + }; // Set MAX and MIN, avoid fixing too frequently. if self.allow_audio { if latency.abs() > MAX_LATENCY { @@ -66,11 +70,9 @@ impl LatencyController { self.allow_audio = true; } } - // No video frame here, which means the update time is not triggered. + // No video frame here, which means the update time is not up to date. // We manually update the time here. - if self.audio_only { - self.update_time = Instant::now(); - } + self.update_time = Instant::now(); self.allow_audio } } From cb228bef2b7093115909686a31d304d47eaa6e1e Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 30 Jan 2023 20:30:35 +0800 Subject: [PATCH 1736/2015] feat: add audio switch ui --- flutter/lib/consts.dart | 6 +++ .../lib/desktop/widgets/remote_menubar.dart | 26 +++++++++++++ libs/hbb_common/protos/message.proto | 6 +++ libs/hbb_common/src/config.rs | 10 +++++ src/client.rs | 39 +++++++++++++++++++ src/flutter_ffi.rs | 14 +++++++ src/lang/ca.rs | 3 ++ src/lang/cn.rs | 3 ++ src/lang/cs.rs | 3 ++ src/lang/da.rs | 3 ++ src/lang/de.rs | 3 ++ src/lang/eo.rs | 3 ++ src/lang/es.rs | 4 ++ src/lang/fa.rs | 3 ++ src/lang/fr.rs | 3 ++ src/lang/gr.rs | 3 ++ src/lang/hu.rs | 3 ++ src/lang/id.rs | 3 ++ src/lang/it.rs | 3 ++ src/lang/ja.rs | 3 ++ src/lang/ko.rs | 3 ++ src/lang/kz.rs | 3 ++ src/lang/pl.rs | 3 ++ src/lang/pt_PT.rs | 3 ++ src/lang/ptbr.rs | 3 ++ src/lang/ro.rs | 3 ++ src/lang/ru.rs | 5 +++ src/lang/sk.rs | 3 ++ src/lang/sl.rs | 3 ++ src/lang/sq.rs | 3 ++ src/lang/sr.rs | 3 ++ src/lang/sv.rs | 3 ++ src/lang/template.rs | 3 ++ src/lang/th.rs | 3 ++ src/lang/tr.rs | 3 ++ src/lang/tw.rs | 3 ++ src/lang/ua.rs | 3 ++ src/lang/vn.rs | 3 ++ src/ui_session_interface.rs | 19 +++++++++ 39 files changed, 219 insertions(+) diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index c95c62fcc..99130f892 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -106,6 +106,12 @@ const kRemoteImageQualityLow = 'low'; /// [kRemoteImageQualityCustom] Custom image quality. const kRemoteImageQualityCustom = 'custom'; +/// [kRemoteAudioGuestToHost] Guest to host audio mode(default). +const kRemoteAudioGuestToHost = 'guest-to-host'; + +/// [kRemoteAudioTwoWay] two-way audio mode(default). +const kRemoteAudioTwoWay = 'two-way'; + const kIgnoreDpi = true; /// flutter/packages/flutter/lib/src/services/keyboard_key.dart -> _keyLabels diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 36b9504c0..1e5723b61 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -1106,6 +1106,30 @@ class _RemoteMenubarState extends State { padding: padding, ), MenuEntryDivider(), + MenuEntryRadios( + text: translate('Audio Transmission Mode'), + optionsGetter: () => [ + MenuEntryRadioOption( + text: translate('Guest to Host'), + value: kRemoteAudioGuestToHost, + dismissOnClicked: true, + ), + MenuEntryRadioOption( + text: translate('Two way'), + value: kRemoteAudioTwoWay, + dismissOnClicked: true, + ), + ], + curOptionGetter: () async => + // null means peer id is not found, which there's no need to care about + await bind.sessionGetAudioMode(id: widget.id) ?? '', + optionSetter: (String oldValue, String newValue) async { + if (oldValue != newValue) { + await bind.sessionSetAudioMode(id: widget.id, value: newValue); + } + }, + padding: padding, + ), ]; if (widget.state.viewStyle.value == kRemoteViewStyleOriginal) { @@ -1337,6 +1361,8 @@ class _RemoteMenubarState extends State { if (perms['audio'] != false) { displayMenu .add(_createSwitchMenuEntry('Mute', 'disable-audio', padding, true)); + displayMenu + .add(_createSwitchMenuEntry('Mute', 'disable-audio', padding, true)); } if (Platform.isWindows && diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index b7965f237..da4865069 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -444,6 +444,11 @@ enum ImageQuality { Best = 4; } +enum AudioMode { + GuestToHost = 0; + TwoWay = 1; +} + message VideoCodecState { enum PreferCodec { Auto = 0; @@ -475,6 +480,7 @@ message OptionMessage { BoolOption enable_file_transfer = 9; VideoCodecState video_codec_state = 10; int32 custom_fps = 11; + AudioMode audio_mode = 12; } message TestDelay { diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 71dd9a5c6..6032ae9c7 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -212,6 +212,11 @@ pub struct PeerConfig { deserialize_with = "PeerConfig::deserialize_image_quality" )] pub image_quality: String, + #[serde( + default = "PeerConfig::default_audio_mode", + deserialize_with = "PeerConfig::deserialize_audio_mode" + )] + pub audio_mode: String, #[serde( default = "PeerConfig::default_custom_image_quality", deserialize_with = "PeerConfig::deserialize_custom_image_quality" @@ -996,6 +1001,11 @@ impl PeerConfig { deserialize_image_quality, UserDefaultConfig::load().get("image_quality") ); + serde_field_string!( + default_audio_mode, + deserialize_audio_mode, + "guest-to-host".to_owned() + ); fn default_custom_image_quality() -> Vec { let f: f64 = UserDefaultConfig::load() diff --git a/src/client.rs b/src/client.rs index b2cd0f2f7..54796a935 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1252,6 +1252,27 @@ impl LoginConfigHandler { } } + /// Parse the audio mode option. + /// Return [`AudioMode`] if the option is valid, otherwise return `None`. + /// + /// # Arguments + /// + /// * `q` - The audio mode option. + /// * `ignore_default` - Ignore the default value. + fn get_audio_mode_enum(&self, q: &str, ignore_default: bool) -> Option { + if q == "guest-to-host" { + Some(AudioMode::GuestToHost) + } else if q == "two-way" { + Some(AudioMode::TwoWay) + } else { + if ignore_default { + None + } else { + Some(AudioMode::GuestToHost) + } + } + } + /// Get the status of a toggle option. /// /// # Arguments @@ -1338,6 +1359,24 @@ impl LoginConfigHandler { res } + pub fn save_audio_mode(&mut self, value: String) -> Option { + let mut res = None; + if let Some(q) = self.get_audio_mode_enum(&value, false) { + let mut misc = Misc::new(); + misc.set_option(OptionMessage { + audio_mode: q.into(), + ..Default::default() + }); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + res = Some(msg_out); + } + let mut config = self.load_config(); + config.audio_mode = value; + self.save_config(config); + res + } + /// Create a [`Message`] for saving custom fps. /// /// # Arguments diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index d9f67e566..10fd67fdd 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -233,6 +233,20 @@ pub fn session_set_image_quality(id: String, value: String) { } } +pub fn session_get_audio_mode(id: String) -> Option { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + Some(session.get_audio_mode()) + } else { + None + } +} + +pub fn session_set_audio_mode(id: String, value: String) { + if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) { + session.save_audio_mode(value); + } +} + pub fn session_get_keyboard_mode(id: String) -> Option { if let Some(session) = SESSIONS.read().unwrap().get(&id) { Some(session.get_keyboard_mode()) diff --git a/src/lang/ca.rs b/src/lang/ca.rs index f2210f971..197435158 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 00d62946f..c74f352ce 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", "帧率"), ("Auto", "自动"), ("Other Default Options", "其它默认选项"), + ("Guest to Host", "被控到主机"), + ("Two way", "双向"), + ("Audio Transmission Mode", "音频传输模式"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 453ecefb3..d956ddf53 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index dcaeb3eaa..9e771567a 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -436,6 +436,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Switch Sides", ""), ("Please confirm if you want to share your desktop?", ""), ("Closed as expected", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ("Display", ""), ("Default View Style", ""), ("Default Scroll Style", ""), diff --git a/src/lang/de.rs b/src/lang/de.rs index 2d6d3d069..a112385a6 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", "fps"), ("Auto", "Automatisch"), ("Other Default Options", "Weitere Standardoptionen"), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 0c7f13d7e..342eac51c 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 5fdb7ee2c..74acd8c69 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -445,5 +445,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", "Otras opciones predeterminadas"), + ("Closed as expected", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index dd1c75bac..50e883227 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", "FPS"), ("Auto", "خودکار"), ("Other Default Options", "سایر گزینه های پیش فرض"), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 3b7f23ab9..9bfdb6b1e 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", "FPS"), ("Auto", "Auto"), ("Other Default Options", "Autres options par défaut"), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/gr.rs b/src/lang/gr.rs index bc25ab6c6..a569b750f 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 49ce8f140..e28294de0 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 0fa6e0293..ece6c9233 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index d84b56a8a..e252219c1 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", "FPS"), ("Auto", "Auto"), ("Other Default Options", "Altre Opzioni Predefinite"), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 35e20d7fd..036bc8ec7 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index d03b07992..6da983849 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 2006c67d1..459139f58 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index b7ccbdbb1..483879d49 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", "FPS"), ("Auto", "Auto"), ("Other Default Options", "Inne opcje domyślne"), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 64e5e9315..cff00333a 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 0f64ae67f..9fe5eab8c 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 7e209dff8..36e2a99de 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 54b064c18..31f24a5e8 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -445,5 +445,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", "FPS"), ("Auto", "Авто"), ("Other Default Options", "Другие параметры по умолчанию"), + ("Please confirm if you want to share your desktop?", "Подтвердите, что хотите поделиться своим рабочим столом?"), + ("Closed as expected", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index a703c0799..8cf858df0 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 16c948ceb..0e2208c3c 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 285a51732..44159fb4a 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index dd943e0e6..892b3664e 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 3050ff635..619a68508 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 7572da9de..f0458b115 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index 535e4e772..f61ba325a 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 80b384c6c..cade148a7 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index f5d9539d8..46cc90c1e 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", "幀率"), ("Auto", "自動"), ("Other Default Options", "其它默認選項"), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 37a7d6bcd..7c355edd5 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index d78f5aa7b..f7640ae50 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -445,5 +445,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Guest to Host", ""), + ("Two way", ""), + ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 4fc5db743..234c9a4d7 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -89,6 +89,18 @@ impl Session { self.lc.write().unwrap().save_keyboard_mode(value); } + pub fn get_audio_mode(&self) -> String { + self.lc.read().unwrap().audio_mode.clone() + } + + pub fn save_audio_mode(&self, value: String) { + let msg = self.lc.write().unwrap().save_audio_mode(value); + // Notify remote guest that the audio mode has been changed. + if let Some(msg) = msg { + self.send(Data::Message(msg)); + } + } + pub fn save_view_style(&mut self, value: String) { self.lc.write().unwrap().save_view_style(value); } @@ -653,6 +665,13 @@ impl Session { } } } + + fn get_audio_transmission_mode(&self, id: &str) { + + } + fn set_audio_transmission_mode(&self, id: &str, mode: String) { + + } } pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { From 393e0e9afbc69df74f95ee98306fc322c5ef456f Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 30 Jan 2023 21:53:26 +0800 Subject: [PATCH 1737/2015] add: divider --- flutter/lib/desktop/widgets/remote_menubar.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 1e5723b61..bb2079930 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -1130,6 +1130,7 @@ class _RemoteMenubarState extends State { }, padding: padding, ), + MenuEntryDivider(), ]; if (widget.state.viewStyle.value == kRemoteViewStyleOriginal) { From 8ab49d11d149de458d6ea95d1543b9c384568632 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 30 Jan 2023 22:06:52 +0800 Subject: [PATCH 1738/2015] feat: add audio mode config --- src/client.rs | 5 ++-- src/client/io_loop.rs | 46 +++++++++++++++++++++++++++++-------- src/ui_session_interface.rs | 4 ++++ 3 files changed, 43 insertions(+), 12 deletions(-) diff --git a/src/client.rs b/src/client.rs index 54796a935..d76f930c8 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1259,7 +1259,7 @@ impl LoginConfigHandler { /// /// * `q` - The audio mode option. /// * `ignore_default` - Ignore the default value. - fn get_audio_mode_enum(&self, q: &str, ignore_default: bool) -> Option { + pub fn get_audio_mode_enum(q: &str, ignore_default: bool) -> Option { if q == "guest-to-host" { Some(AudioMode::GuestToHost) } else if q == "two-way" { @@ -1361,7 +1361,7 @@ impl LoginConfigHandler { pub fn save_audio_mode(&mut self, value: String) -> Option { let mut res = None; - if let Some(q) = self.get_audio_mode_enum(&value, false) { + if let Some(q) = LoginConfigHandler::get_audio_mode_enum(&value, false) { let mut misc = Misc::new(); misc.set_option(OptionMessage { audio_mode: q.into(), @@ -1981,6 +1981,7 @@ pub enum Data { RemovePortForward(i32), AddPortForward((i32, String, i32)), ToggleClipboardFile, + ChangeAudioMode(AudioMode), NewRDP, SetConfirmOverrideFile((i32, i32, bool, bool, bool)), AddJob((i32, String, String, i32, bool, bool)), diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index d568feb4e..af8c1048b 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -1,5 +1,5 @@ use crate::client::{ - Client, CodecFormat, MediaData, MediaSender, QualityStatus, MILLI1, SEC30, + Client, CodecFormat, LoginConfigHandler, MediaData, MediaSender, QualityStatus, MILLI1, SEC30, SERVER_CLIPBOARD_ENABLED, SERVER_FILE_TRANSFER_ENABLED, SERVER_KEYBOARD_ENABLED, }; #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -386,6 +386,24 @@ impl Remote { Data::ToggleClipboardFile => { self.check_clipboard_file_context(); } + Data::ChangeAudioMode(audio_mode) => { + match audio_mode { + AudioMode::GuestToHost => { + if let Some(sender) = self.stop_local_audio_sender.take() { + allow_err!(sender.send(())); + } + } + AudioMode::TwoWay => { + // Start audio thread for playback. + // Cancel previous local audio session. + if let Some(sender) = self.stop_local_audio_sender.take() { + allow_err!(sender.send(())); + } + // Start client audio when connection is established. + self.stop_local_audio_sender = self.start_client_audio(); + } + } + } Data::Message(msg) => { allow_err!(peer.send(&msg).await); } @@ -866,19 +884,27 @@ impl Remote { }); } } - // Start audio thread for playback - if !self.handler.is_file_transfer() && !self.handler.is_port_forward() { - // Cancel previous local audio session. - if let Some(sender) = self.stop_local_audio_sender.take() { - allow_err!(sender.send(())); - } - // Start client audio when connection is established. - self.stop_local_audio_sender = self.start_client_audio(); - } if self.handler.is_file_transfer() { self.handler.load_last_jobs(); } + + // Start audio thread for playback if current audio mode is two-way transmission. + if !self.handler.is_file_transfer() && !self.handler.is_port_forward() { + let audio_mode = LoginConfigHandler::get_audio_mode_enum( + self.handler.load_config().audio_mode.as_str(), + false, + ) + .unwrap_or(AudioMode::GuestToHost); + if audio_mode == AudioMode::TwoWay { + // Cancel previous local audio session. + if let Some(sender) = self.stop_local_audio_sender.take() { + allow_err!(sender.send(())); + } + // Start client audio when connection is established. + self.stop_local_audio_sender = self.start_client_audio(); + } + } } _ => {} }, diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 234c9a4d7..73414e405 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -94,6 +94,10 @@ impl Session { } pub fn save_audio_mode(&self, value: String) { + let mode = LoginConfigHandler::get_audio_mode_enum(value.as_str(), false); + if let Some(mode)= mode { + self.send(Data::ChangeAudioMode(mode)); + } let msg = self.lc.write().unwrap().save_audio_mode(value); // Notify remote guest that the audio mode has been changed. if let Some(msg) = msg { From 05822991bfaa48fbf86ff31b734d77a431a75cde Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 30 Jan 2023 22:57:20 +0800 Subject: [PATCH 1739/2015] opt: rename to dual-way --- flutter/lib/consts.dart | 4 ++-- flutter/lib/desktop/widgets/remote_menubar.dart | 4 ++-- libs/hbb_common/protos/message.proto | 2 +- src/client.rs | 4 ++-- src/client/io_loop.rs | 7 ++++--- src/lang/ca.rs | 2 +- src/lang/cn.rs | 2 +- src/lang/cs.rs | 2 +- src/lang/da.rs | 2 +- src/lang/de.rs | 2 +- src/lang/eo.rs | 2 +- src/lang/es.rs | 2 +- src/lang/fa.rs | 2 +- src/lang/fr.rs | 2 +- src/lang/gr.rs | 2 +- src/lang/hu.rs | 2 +- src/lang/id.rs | 2 +- src/lang/it.rs | 2 +- src/lang/ja.rs | 2 +- src/lang/ko.rs | 2 +- src/lang/kz.rs | 2 +- src/lang/pl.rs | 2 +- src/lang/pt_PT.rs | 2 +- src/lang/ptbr.rs | 2 +- src/lang/ro.rs | 2 +- src/lang/ru.rs | 2 +- src/lang/sk.rs | 2 +- src/lang/sl.rs | 2 +- src/lang/sq.rs | 2 +- src/lang/sr.rs | 2 +- src/lang/sv.rs | 2 +- src/lang/template.rs | 2 +- src/lang/th.rs | 2 +- src/lang/tr.rs | 2 +- src/lang/tw.rs | 2 +- src/lang/ua.rs | 2 +- src/lang/vn.rs | 2 +- src/ui_session_interface.rs | 7 ------- 38 files changed, 43 insertions(+), 49 deletions(-) diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 99130f892..26e25a209 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -109,8 +109,8 @@ const kRemoteImageQualityCustom = 'custom'; /// [kRemoteAudioGuestToHost] Guest to host audio mode(default). const kRemoteAudioGuestToHost = 'guest-to-host'; -/// [kRemoteAudioTwoWay] two-way audio mode(default). -const kRemoteAudioTwoWay = 'two-way'; +/// [kRemoteAudioDualWay] dual-way audio mode(default). +const kRemoteAudioDualWay = 'dual-way'; const kIgnoreDpi = true; diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index bb2079930..9864947c6 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -1115,8 +1115,8 @@ class _RemoteMenubarState extends State { dismissOnClicked: true, ), MenuEntryRadioOption( - text: translate('Two way'), - value: kRemoteAudioTwoWay, + text: translate('Dual way'), + value: kRemoteAudioDualWay, dismissOnClicked: true, ), ], diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index da4865069..48b999438 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -446,7 +446,7 @@ enum ImageQuality { enum AudioMode { GuestToHost = 0; - TwoWay = 1; + DualWay = 1; } message VideoCodecState { diff --git a/src/client.rs b/src/client.rs index d76f930c8..649b180bb 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1262,8 +1262,8 @@ impl LoginConfigHandler { pub fn get_audio_mode_enum(q: &str, ignore_default: bool) -> Option { if q == "guest-to-host" { Some(AudioMode::GuestToHost) - } else if q == "two-way" { - Some(AudioMode::TwoWay) + } else if q == "dual-way" { + Some(AudioMode::DualWay) } else { if ignore_default { None diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index af8c1048b..a284fdade 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -393,7 +393,7 @@ impl Remote { allow_err!(sender.send(())); } } - AudioMode::TwoWay => { + AudioMode::DualWay => { // Start audio thread for playback. // Cancel previous local audio session. if let Some(sender) = self.stop_local_audio_sender.take() { @@ -889,14 +889,15 @@ impl Remote { self.handler.load_last_jobs(); } - // Start audio thread for playback if current audio mode is two-way transmission. + // Start audio thread for playback if current audio mode is dual-way transmission. if !self.handler.is_file_transfer() && !self.handler.is_port_forward() { let audio_mode = LoginConfigHandler::get_audio_mode_enum( self.handler.load_config().audio_mode.as_str(), false, ) .unwrap_or(AudioMode::GuestToHost); - if audio_mode == AudioMode::TwoWay { + log::debug!("current audio mode: {:?}", audio_mode); + if audio_mode == AudioMode::DualWay { // Cancel previous local audio session. if let Some(sender) = self.stop_local_audio_sender.take() { allow_err!(sender.send(())); diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 197435158..e45dc5fb5 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", ""), ("Other Default Options", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index c74f352ce..84bfcb384 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", "自动"), ("Other Default Options", "其它默认选项"), ("Guest to Host", "被控到主机"), - ("Two way", "双向"), + ("Dual way", "双向"), ("Audio Transmission Mode", "音频传输模式"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index d956ddf53..ef9cd7bf8 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", ""), ("Other Default Options", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 9e771567a..32aa1f0af 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -437,7 +437,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please confirm if you want to share your desktop?", ""), ("Closed as expected", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ("Display", ""), ("Default View Style", ""), diff --git a/src/lang/de.rs b/src/lang/de.rs index a112385a6..f8fac0737 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", "Automatisch"), ("Other Default Options", "Weitere Standardoptionen"), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 342eac51c..4aa2be8db 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", ""), ("Other Default Options", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 74acd8c69..932936da3 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -447,7 +447,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", "Otras opciones predeterminadas"), ("Closed as expected", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 50e883227..b8c45fbee 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", "خودکار"), ("Other Default Options", "سایر گزینه های پیش فرض"), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 9bfdb6b1e..64a8b4e40 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", "Auto"), ("Other Default Options", "Autres options par défaut"), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/gr.rs b/src/lang/gr.rs index a569b750f..3918db55b 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", ""), ("Other Default Options", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index e28294de0..edad7ecd0 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", ""), ("Other Default Options", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index ece6c9233..1b2dc4ad5 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", ""), ("Other Default Options", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index e252219c1..27432303d 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", "Auto"), ("Other Default Options", "Altre Opzioni Predefinite"), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 036bc8ec7..ae375b8ee 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", ""), ("Other Default Options", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 6da983849..417f88fe6 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", ""), ("Other Default Options", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 459139f58..e852278dd 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", ""), ("Other Default Options", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 483879d49..4cce52e08 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", "Auto"), ("Other Default Options", "Inne opcje domyślne"), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index cff00333a..29252926f 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", ""), ("Other Default Options", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 9fe5eab8c..8ec40cf13 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", ""), ("Other Default Options", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 36e2a99de..c4f798abf 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", ""), ("Other Default Options", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 31f24a5e8..949eba641 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -448,7 +448,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please confirm if you want to share your desktop?", "Подтвердите, что хотите поделиться своим рабочим столом?"), ("Closed as expected", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 8cf858df0..7de4d10ce 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", ""), ("Other Default Options", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 0e2208c3c..bf30f96d8 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", ""), ("Other Default Options", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 44159fb4a..db560166a 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", ""), ("Other Default Options", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 892b3664e..599cd651b 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", ""), ("Other Default Options", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 619a68508..c0616300c 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", ""), ("Other Default Options", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index f0458b115..282b564d7 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", ""), ("Other Default Options", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index f61ba325a..b2bee959a 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", ""), ("Other Default Options", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index cade148a7..b6efeaf0a 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", ""), ("Other Default Options", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 46cc90c1e..eea71e6bf 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", "自動"), ("Other Default Options", "其它默認選項"), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 7c355edd5..f0d85a551 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", ""), ("Other Default Options", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index f7640ae50..5e4009570 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -446,7 +446,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", ""), ("Other Default Options", ""), ("Guest to Host", ""), - ("Two way", ""), + ("Dual way", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 73414e405..1e7848505 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -669,13 +669,6 @@ impl Session { } } } - - fn get_audio_transmission_mode(&self, id: &str) { - - } - fn set_audio_transmission_mode(&self, id: &str, mode: String) { - - } } pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { From cab1fc719aed30a7f0afac289d55e1b03375ac91 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 30 Jan 2023 23:42:38 +0800 Subject: [PATCH 1740/2015] feat: add audio mode in sciter --- src/ui/header.tis | 10 +++++++++- src/ui/remote.rs | 2 ++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/ui/header.tis b/src/ui/header.tis index dd0b35541..e3f0c70a1 100644 --- a/src/ui/header.tis +++ b/src/ui/header.tis @@ -183,6 +183,9 @@ class Header: Reactor.Component {
  • {svg_checkmark}{translate('Balanced')}
  • {svg_checkmark}{translate('Optimize reaction time')}
  • {svg_checkmark}{translate('Custom')}
  • +
    +
  • {svg_checkmark}{translate('Guest to Host')}
  • +
  • {svg_checkmark}{translate('Dual way')}
  • {show_codec ?
  • {svg_checkmark}Auto
  • @@ -378,7 +381,7 @@ class Header: Reactor.Component { togglePrivacyMode(me.id); } else if (me.id == "show-quality-monitor") { toggleQualityMonitor(me.id); - }else if (me.attributes.hasClass("toggle-option")) { + } else if (me.attributes.hasClass("toggle-option")) { handler.toggle_option(me.id); toggleMenuState(); } else if (!me.attributes.hasClass("selected")) { @@ -391,6 +394,8 @@ class Header: Reactor.Component { } else if (type == "codec-preference") { handler.set_option("codec-preference", me.id); handler.change_prefer_codec(); + } else if (type == "audio-mode") { + handler.save_audio_mode(me.id); } toggleMenuState(); } @@ -434,6 +439,9 @@ function toggleMenuState() { var c = handler.get_option("codec-preference"); if (!c) c = "auto"; values.push(c); + var a = handler.get_audio_mode(); + if (!a) a = "guest-to-host"; + values.push(a); for (var el in $$(menu#display-options li)) { el.attributes.toggleClass("selected", values.indexOf(el.id) >= 0); } diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 21504d20d..541d3a141 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -420,6 +420,8 @@ impl sciter::EventHandler for SciterSession { fn supported_hwcodec(); fn change_prefer_codec(); fn restart_remote_device(); + fn save_audio_mode(String); + fn get_audio_mode(); } } From 7e5c5b50e5a6dd6b9c1f265cfb1520db4319e739 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 31 Jan 2023 10:01:31 +0800 Subject: [PATCH 1741/2015] feat: set to default input device when in dual-way --- src/client/io_loop.rs | 10 +++++++-- src/common.rs | 44 +++++++++++++++++++++++++++++++++++++++- src/flutter_ffi.rs | 10 +++++++++ src/platform/linux.rs | 18 ++++++++++++++++ src/server/connection.rs | 7 ++++++- 5 files changed, 85 insertions(+), 4 deletions(-) diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index a284fdade..9117c8c5f 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -2,13 +2,14 @@ use crate::client::{ Client, CodecFormat, LoginConfigHandler, MediaData, MediaSender, QualityStatus, MILLI1, SEC30, SERVER_CLIPBOARD_ENABLED, SERVER_FILE_TRANSFER_ENABLED, SERVER_KEYBOARD_ENABLED, }; +use crate::common::{get_default_sound_input, set_sound_input}; #[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::common::{check_clipboard, update_clipboard, ClipboardContext, CLIPBOARD_INTERVAL}; use crate::{audio_service, common, ConnInner, CLIENT_SERVER}; #[cfg(windows)] use clipboard::{cliprdr::CliprdrClientContext, ContextSend}; -use hbb_common::futures::channel::mpsc::unbounded; + use hbb_common::tokio::sync::mpsc::error::TryRecvError; use crate::server::Service; @@ -32,7 +33,7 @@ use hbb_common::tokio::{ }; use hbb_common::{allow_err, message_proto::*, sleep}; use hbb_common::{fs, log, Stream}; -use std::borrow::Borrow; + use std::collections::HashMap; use std::sync::atomic::{AtomicUsize, Ordering}; @@ -270,6 +271,11 @@ impl Remote { if self.handler.is_file_transfer() || self.handler.is_port_forward() { return None; } + // Switch to default input device + let default_sound_device = get_default_sound_input(); + if let Some(device) = default_sound_device { + set_sound_input(device); + } // Create a channel to receive error or closed message let (tx, rx) = std::sync::mpsc::channel(); let (tx_audio_data, mut rx_audio_data) = hbb_common::tokio::sync::mpsc::unbounded_channel(); diff --git a/src/common.rs b/src/common.rs index c2d5a81f0..9cbc9b150 100644 --- a/src/common.rs +++ b/src/common.rs @@ -30,6 +30,8 @@ use hbb_common::{ // #[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))] use hbb_common::{config::RENDEZVOUS_PORT, futures::future::join_all}; +use crate::ui_interface::{set_option, get_option}; + pub type NotifyMessageBox = fn(String, String, String, String) -> dyn Future; pub const CLIPBOARD_NAME: &'static str = "clipboard"; @@ -105,6 +107,46 @@ pub fn check_clipboard( None } +/// Set sound input device. +pub fn set_sound_input(device: String) { + let prior_device = get_option("audio-input".to_owned()); + if prior_device != device { + log::info!("switch to audio input device {}", device); + set_option("audio-input".to_owned(), device); + } else { + log::info!("audio input is already set to {}", device); + } +} + +/// Get system's default sound input device name. +#[inline] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn get_default_sound_input() -> Option { + #[cfg(not(target_os = "linux"))] + { + use cpal::traits::{DeviceTrait, HostTrait}; + let host = cpal::default_host(); + let dev = host.default_input_device(); + return if let Some(dev) = dev { + match dev.name() { + Ok(name) => Some(name), + Err(_) => None, + } + } else { + None + }; + } + #[cfg(target_os = "linux")] + { + let input = crate::platform::linux::get_default_pa_source(); + return if let Some(input) = input { + Some(input.1) + } else { + None + }; + } +} + #[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn update_clipboard(clipboard: Clipboard, old: Option<&Arc>>) { let content = if clipboard.compress { @@ -715,5 +757,5 @@ pub fn make_fd_to_json(id: i32, path: String, entries: &Vec) -> Strin #[cfg(test)] mod test_common { - use super::*; + } diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 10fd67fdd..31cb07c07 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -4,6 +4,9 @@ use std::str::FromStr; use flutter_rust_bridge::{StreamSink, SyncReturn, ZeroCopyBuffer}; use serde_json::json; +use crate::common::{is_keyboard_mode_supported, get_default_sound_input}; +use hbb_common::message_proto::KeyboardMode; +use hbb_common::ResultType; use hbb_common::{ config::{self, LocalConfig, ONLINE, PeerConfig}, fs, log, @@ -534,6 +537,13 @@ pub fn main_get_sound_inputs() -> Vec { vec![String::from("")] } +pub fn main_get_default_sound_input() -> Option { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + return get_default_sound_input(); + #[cfg(any(target_os = "android", target_os = "ios"))] + String::from("") +} + pub fn main_get_hostname() -> SyncReturn { SyncReturn(crate::common::hostname()) } diff --git a/src/platform/linux.rs b/src/platform/linux.rs index ac3b32a49..8fa95ac90 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -534,6 +534,24 @@ pub fn get_pa_sources() -> Vec<(String, String)> { out } +pub fn get_default_pa_source() -> Option<(String, String)> { + use pulsectl::controllers::*; + match SourceController::create() { + Ok(mut handler) => { + if let Ok(dev) = handler.get_default_device() { + return Some(( + dev.name.unwrap_or("".to_owned()), + dev.description.unwrap_or("".to_owned()), + )); + } + } + Err(err) => { + log::error!("Failed to get_pa_source: {:?}", err); + } + } + None +} + pub fn lock_screen() { std::process::Command::new("xdg-screensaver") .arg("lock") diff --git a/src/server/connection.rs b/src/server/connection.rs index d5c2103b1..20cbe0f86 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -5,7 +5,7 @@ use crate::clipboard_file::*; use crate::common::update_clipboard; #[cfg(windows)] use crate::portable_service::client as portable_client; -use crate::{video_service, client::{MediaSender, start_audio_thread, LatencyController, MediaData}}; +use crate::{video_service, client::{MediaSender, start_audio_thread, LatencyController, MediaData}, common::{get_default_sound_input, set_sound_input}}; #[cfg(any(target_os = "android", target_os = "ios"))] use crate::{common::DEVICE_NAME, flutter::connection_manager::start_channel}; use crate::{ipc, VERSION}; @@ -1542,6 +1542,11 @@ impl Connection { }, Some(misc::Union::AudioFormat(format)) => { if !self.disable_audio { + // Switch to default input device + let default_sound_device = get_default_sound_input(); + if let Some(device) = default_sound_device { + set_sound_input(device); + } allow_err!(self.audio_sender.send(MediaData::AudioFormat(format))); } } From 60925057f0c66ded4d9dc66b52d85b86059ffc1c Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 31 Jan 2023 10:23:58 +0800 Subject: [PATCH 1742/2015] fix: poison error on setting sound input --- src/common.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/common.rs b/src/common.rs index 9cbc9b150..70d50619e 100644 --- a/src/common.rs +++ b/src/common.rs @@ -112,7 +112,9 @@ pub fn set_sound_input(device: String) { let prior_device = get_option("audio-input".to_owned()); if prior_device != device { log::info!("switch to audio input device {}", device); - set_option("audio-input".to_owned(), device); + std::thread::spawn(move || { + set_option("audio-input".to_owned(), device); + }); } else { log::info!("audio input is already set to {}", device); } From 038d660e6063c6a8222cd7f8c2753ee07492a6e8 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 31 Jan 2023 11:10:14 +0800 Subject: [PATCH 1743/2015] fix: android build --- src/common.rs | 6 ++++++ src/flutter_ffi.rs | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/common.rs b/src/common.rs index 70d50619e..3e6409c53 100644 --- a/src/common.rs +++ b/src/common.rs @@ -149,6 +149,12 @@ pub fn get_default_sound_input() -> Option { } } +#[inline] +#[cfg(any(target_os = "android", target_os = "ios"))] +pub fn get_default_sound_input() -> Option { + None +} + #[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn update_clipboard(clipboard: Clipboard, old: Option<&Arc>>) { let content = if clipboard.compress { diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 31cb07c07..0fe6818de 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -541,7 +541,7 @@ pub fn main_get_default_sound_input() -> Option { #[cfg(not(any(target_os = "android", target_os = "ios")))] return get_default_sound_input(); #[cfg(any(target_os = "android", target_os = "ios"))] - String::from("") + None } pub fn main_get_hostname() -> SyncReturn { From ebec8811c2ec49da7bd3f59db98d38ad0ead84a6 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 31 Jan 2023 13:32:10 +0800 Subject: [PATCH 1744/2015] opt: add microphone permission tip --- flutter/lib/common.dart | 27 ++++++++++++++ .../lib/desktop/pages/desktop_home_page.dart | 35 +++++++++++++++++-- flutter/macos/Runner/Info.plist | 2 ++ flutter/macos/Runner/MainFlutterWindow.swift | 18 ++++++++++ src/lang/ca.rs | 1 + src/lang/cn.rs | 1 + src/lang/cs.rs | 1 + src/lang/da.rs | 1 + src/lang/de.rs | 1 + src/lang/en.rs | 3 +- src/lang/eo.rs | 1 + src/lang/es.rs | 1 + src/lang/fa.rs | 1 + src/lang/fr.rs | 1 + src/lang/gr.rs | 1 + src/lang/hu.rs | 1 + src/lang/id.rs | 1 + src/lang/it.rs | 1 + src/lang/ja.rs | 1 + src/lang/ko.rs | 1 + src/lang/kz.rs | 1 + src/lang/pl.rs | 1 + src/lang/pt_PT.rs | 1 + src/lang/ptbr.rs | 1 + src/lang/ro.rs | 1 + src/lang/ru.rs | 1 + src/lang/sk.rs | 1 + src/lang/sl.rs | 1 + src/lang/sq.rs | 1 + src/lang/sr.rs | 1 + src/lang/sv.rs | 1 + src/lang/template.rs | 1 + src/lang/th.rs | 1 + src/lang/tr.rs | 1 + src/lang/tw.rs | 1 + src/lang/ua.rs | 1 + src/lang/vn.rs | 1 + 37 files changed, 114 insertions(+), 3 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 30d38b8db..df2a75f56 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1723,3 +1723,30 @@ Future updateSystemWindowTheme() async { } } } +/// macOS only +/// +/// Note: not found a general solution for rust based AVFoundation bingding. +/// [AVFoundation] crate has compile error. +const kMacOSPermChannel = MethodChannel("org.rustdesk.rustdesk/macos"); + +enum PermissionAuthorizeType { + undetermined, + authorized, + denied, // and restricted +} + +Future osxCanRecordAudio() async { + int res = await kMacOSPermChannel.invokeMethod("canRecordAudio"); + print(res); + if (res > 0) { + return PermissionAuthorizeType.authorized; + } else if (res == 0) { + return PermissionAuthorizeType.undetermined; + } else { + return PermissionAuthorizeType.denied; + } +} + +Future osxRequestAudio() async { + return await kMacOSPermChannel.invokeMethod("requestRecordAudio"); +} diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 0501c298a..71dd2c96e 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -44,6 +44,7 @@ class _DesktopHomePageState extends State var watchIsCanScreenRecording = false; var watchIsProcessTrust = false; var watchIsInputMonitoring = false; + var watchIsCanRecordAudio = false; Timer? _updateTimer; @override @@ -79,7 +80,16 @@ class _DesktopHomePageState extends State buildTip(context), buildIDBoard(context), buildPasswordBoard(context), - buildHelpCards(), + FutureBuilder( + future: buildHelpCards(), + builder: (_, data) { + if (data.hasData) { + return data.data!; + } else { + return const Offstage(); + } + }, + ), ], ), ), @@ -302,7 +312,7 @@ class _DesktopHomePageState extends State ); } - Widget buildHelpCards() { + Future buildHelpCards() async { if (updateUrl.isNotEmpty) { return buildInstallCard( "Status", @@ -348,6 +358,13 @@ class _DesktopHomePageState extends State return buildInstallCard("", "install_daemon_tip", "Install", () async { bind.mainIsInstalledDaemon(prompt: true); }); + } else if ((await osxCanRecordAudio() != + PermissionAuthorizeType.authorized)) { + return buildInstallCard("Permissions", "config_microphone", "Configure", + () async { + osxRequestAudio(); + watchIsCanRecordAudio = true; + }); } } else if (Platform.isLinux) { if (bind.mainCurrentIsWayland()) { @@ -481,6 +498,20 @@ class _DesktopHomePageState extends State setState(() {}); } } + if (watchIsCanRecordAudio) { + if (Platform.isMacOS) { + Future.microtask(() async { + if ((await osxCanRecordAudio() == + PermissionAuthorizeType.authorized)) { + watchIsCanRecordAudio = false; + setState(() {}); + } + }); + } else { + watchIsCanRecordAudio = false; + setState(() {}); + } + } }); Get.put(svcStopped, tag: 'stop-service'); rustDeskWinManager.registerActiveWindowListener(onActiveWindowChanged); diff --git a/flutter/macos/Runner/Info.plist b/flutter/macos/Runner/Info.plist index c926019ab..96616e8c4 100644 --- a/flutter/macos/Runner/Info.plist +++ b/flutter/macos/Runner/Info.plist @@ -43,6 +43,8 @@ $(PRODUCT_COPYRIGHT) NSMainNibFile MainMenu + NSMicrophoneUsageDescription + Record the sound from microphone for the purpose of the remote desktop. NSPrincipalClass NSApplication diff --git a/flutter/macos/Runner/MainFlutterWindow.swift b/flutter/macos/Runner/MainFlutterWindow.swift index 97b46bb84..21e870320 100644 --- a/flutter/macos/Runner/MainFlutterWindow.swift +++ b/flutter/macos/Runner/MainFlutterWindow.swift @@ -1,4 +1,5 @@ import Cocoa +import AVFoundation import FlutterMacOS import desktop_multi_window // import bitsdojo_window_macos @@ -81,6 +82,23 @@ class MainFlutterWindow: NSWindow { case "terminate": NSApplication.shared.terminate(self) result(nil) + case "canRecordAudio": + switch AVCaptureDevice.authorizationStatus(for: .audio) { + case .authorized: + result(1) + break + case .notDetermined: + result(0) + break + default: + result(-1) + break + } + case "requestRecordAudio": + AVCaptureDevice.requestAccess(for: .audio, completionHandler: { granted in + result(granted) + }) + break default: result(FlutterMethodNotImplemented) } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index e45dc5fb5..e65927876 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 84bfcb384..bcb2c3daf 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", "如果你使用英伟达显卡, 并且远程窗口在会话建立后会立刻关闭, 那么安装 nouveau 驱动并且选择使用软件渲染可能会有帮助。重启软件后生效。"), ("Always use software rendering", "使用软件渲染"), ("config_input", "为了能够通过键盘控制远程桌面, 请给予 RustDesk \"输入监控\" 权限。"), + ("config_microphone", "为了支持通过麦克风进行音频传输,请给予 RustDesk \"录音\"权限。"), ("request_elevation_tip", "如果对面有人, 也可以请求提升权限。"), ("Wait", "等待"), ("Elevation Error", "提权失败"), diff --git a/src/lang/cs.rs b/src/lang/cs.rs index ef9cd7bf8..d16e3abef 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), diff --git a/src/lang/da.rs b/src/lang/da.rs index 32aa1f0af..23884b995 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), diff --git a/src/lang/de.rs b/src/lang/de.rs index f8fac0737..1839edb87 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", "Wenn Sie eine Nvidia-Grafikkarte haben und sich das entfernte Fenster sofort nach dem Herstellen der Verbindung schließt, kann es helfen, den Nouveau-Treiber zu installieren und Software-Rendering zu verwenden. Ein Neustart der Software ist erforderlich."), ("Always use software rendering", "Software-Rendering immer verwenden"), ("config_input", "Um den entfernten Desktop mit der Tastatur steuern zu können, müssen Sie RustDesk \"Input Monitoring\"-Rechte erteilen."), + ("config_microphone", ""), ("request_elevation_tip", "Sie können auch erhöhte Rechte anfordern, wenn sich jemand auf der Gegenseite befindet."), ("Wait", "Warten"), ("Elevation Error", "Berechtigungsfehler"), diff --git a/src/lang/en.rs b/src/lang/en.rs index 6eed43a77..37c08a974 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -41,6 +41,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("config_input", "In order to control remote desktop with keyboard, you need to grant RustDesk \"Input Monitoring\" permissions."), ("request_elevation_tip","You can also request elevation if there is someone on the remote side."), ("wait_accept_uac_tip","Please wait for the remote user to accept the UAC dialog."), - ("still_click_uac_tip", "Still requires the remote user to click OK on the UAC window of running RustDesk.") + ("still_click_uac_tip", "Still requires the remote user to click OK on the UAC window of running RustDesk."), + ("config_microphone", "In order to speak remotely, you need to grant RustDesk \"Record Audio\" permissions.") ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 4aa2be8db..aa8829874 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), diff --git a/src/lang/es.rs b/src/lang/es.rs index 932936da3..da13843f0 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", "Si tienes una gráfica Nvidia y la ventana remota se cierra inmediatamente, instalar el driver nouveau y elegir renderizado por software podría ayudar. Se requiere reiniciar la aplicación."), ("Always use software rendering", "Usar siempre renderizado por software"), ("config_input", "Para controlar el escritorio remoto con el teclado necesitas dar a RustDesk permisos de \"Monitorización de entrada\"."), + ("config_microphone", ""), ("request_elevation_tip", "También puedes solicitar elevación si hay alguien en el lado remoto."), ("Wait", "Esperar"), ("Elevation Error", "Error de elevación"), diff --git a/src/lang/fa.rs b/src/lang/fa.rs index b8c45fbee..7664af99e 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", "اگر کارت گرافیک Nvidia دارید و پنجره راه دور بلافاصله پس از اتصال بسته می شود، درایور nouveau را نصب نمایید و انتخاب گزینه استفاده از رندر نرم افزار می تواند کمک کننده باشد. راه اندازی مجدد نرم افزار مورد نیاز است."), ("Always use software rendering", "همیشه از رندر نرم افزاری استفاده کنید"), ("config_input", "برای کنترل دسکتاپ از راه دور با صفحه کلید، باید مجوز RustDesk \"Input Monitoring\" را بدهید."), + ("config_microphone", ""), ("request_elevation_tip", "همچنین می توانید در صورت وجود شخصی در سمت راه دور درخواست ارتفاع دهید."), ("Wait", "صبر کنید"), ("Elevation Error", "خطای ارتفاع"), diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 64a8b4e40..db49b5a78 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", "Si vous avez une carte graphique NVIDIA et que la fenêtre distante se ferme immédiatement après la connexion, l'installation du pilote Nouveau et le choix d'utiliser le rendu du logiciel peuvent aider. Un redémarrage du logiciel est requis."), ("Always use software rendering", "Utiliser toujours le rendu logiciel"), ("config_input", "Afin de contrôler le bureau à distance avec le clavier, vous devez accorder à RustDesk l'autorisation \"Surveillance de l’entrée\"."), + ("config_microphone", ""), ("request_elevation_tip", "Vous pouvez également demander une augmentation des privilèges s'il y a quelqu'un du côté distant."), ("Wait", "En cours"), ("Elevation Error", "Erreur d'augmentation des privilèges"), diff --git a/src/lang/gr.rs b/src/lang/gr.rs index 3918db55b..5312e6381 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", "Εάν έχετε κάρτα γραφικών Nvidia και το παράθυρο σύνδεσης κλείνει αμέσως μετά τη σύνδεση, η εγκατάσταση του προγράμματος οδήγησης nouveau και η επιλογή χρήσης της επιτάχυνσης γραφικών μέσω λογισμικού μπορεί να βοηθήσει. Απαιτείται επανεκκίνηση."), ("Always use software rendering", "Επιτάχυνση γραφικών μέσω λογισμικού"), ("config_input", "Για να ελέγξετε την απομακρυσμένη επιφάνεια εργασίας με πληκτρολόγιο, πρέπει να εκχωρήσετε δικαιώματα στο RustDesk"), + ("config_microphone", ""), ("request_elevation_tip", "αίτημα ανύψωσης δικαιωμάτων χρήστη"), ("Wait", "Περιμένετε"), ("Elevation Error", "Σφάλμα ανύψωσης δικαιωμάτων χρήστη"), diff --git a/src/lang/hu.rs b/src/lang/hu.rs index edad7ecd0..2f6c490ad 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), diff --git a/src/lang/id.rs b/src/lang/id.rs index 1b2dc4ad5..7b9325076 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), diff --git a/src/lang/it.rs b/src/lang/it.rs index 27432303d..31864b220 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", "Se si dispone di una scheda grafica Nvidia e la finestra remota si chiude immediatamente dopo la connessione, l'installazione del driver nouveau e la scelta di utilizzare il rendering software possono aiutare. È necessario un riavvio del software."), ("Always use software rendering", "Usa sempre il render Software"), ("config_input", "Per controllare il desktop remoto con la tastiera, è necessario concedere le autorizzazioni a RustDesk \"Monitoraggio dell'input\"."), + ("config_microphone", ""), ("request_elevation_tip", "È possibile richiedere l'elevazione se c'è qualcuno sul lato remoto."), ("Wait", "Attendi"), ("Elevation Error", "Errore durante l'elevazione dei diritti"), diff --git a/src/lang/ja.rs b/src/lang/ja.rs index ae375b8ee..5f2b68c46 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 417f88fe6..59cc9fdff 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), diff --git a/src/lang/kz.rs b/src/lang/kz.rs index e852278dd..8a939764b 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 4cce52e08..788aa8b62 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", "Jeżeli posiadasz kartę graficzną Nvidia i okno zamyka się natychmiast po nawiązaniu połączenia, instalacja sterownika nouveau i wybór renderowania programowego mogą pomóc. Restart aplikacji jest wymagany."), ("Always use software rendering", "Zawsze używaj renderowania programowego"), ("config_input", "By kontrolować zdalne urządzenie przy pomocy klawiatury, musisz udzielić aplikacji RustDesk uprawnień do \"Urządzeń Wejściowych\"."), + ("config_microphone", ""), ("request_elevation_tip", "Możesz poprosić o podniesienie uprawnień jeżeli ktoś posiada dostęp do zdalnego urządzenia."), ("Wait", "Czekaj"), ("Elevation Error", "Błąd przy podnoszeniu uprawnień"), diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 29252926f..c6899ee54 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 8ec40cf13..cdac5f686 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), diff --git a/src/lang/ro.rs b/src/lang/ro.rs index c4f798abf..5865d0206 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 949eba641..fe1de2e91 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", "Если у вас видеокарта Nvidia и удалённое окно закрывается сразу после подключения, может помочь установка драйвера Nouveau и выбор использования программной визуализации. Потребуется перезапуск."), ("Always use software rendering", "Использовать программную визуализацию"), ("config_input", "Чтобы управлять удалённым рабочим столом с помощью клавиатуры, необходимо предоставить RustDesk разрешения \"Мониторинг ввода\"."), + ("config_microphone", ""), ("request_elevation_tip", "Также можно запросить повышение прав, если кто-то есть на удалённой стороне."), ("Wait", "Ждите"), ("Elevation Error", "Ошибка повышения прав"), diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 7de4d10ce..88f09313f 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), diff --git a/src/lang/sl.rs b/src/lang/sl.rs index bf30f96d8..f78a6e9e3 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), diff --git a/src/lang/sq.rs b/src/lang/sq.rs index db560166a..63e834c25 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 599cd651b..33355fd38 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), diff --git a/src/lang/sv.rs b/src/lang/sv.rs index c0616300c..8af2ccb8a 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), diff --git a/src/lang/template.rs b/src/lang/template.rs index 282b564d7..1abc20b36 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), diff --git a/src/lang/th.rs b/src/lang/th.rs index b2bee959a..173143821 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), diff --git a/src/lang/tr.rs b/src/lang/tr.rs index b6efeaf0a..072275334 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), diff --git a/src/lang/tw.rs b/src/lang/tw.rs index eea71e6bf..8c0968901 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", "如果你使用英偉達顯卡, 並且遠程窗口在會話建立後會立刻關閉, 那麼安裝nouveau驅動並且選擇使用軟件渲染可能會有幫助。重啟軟件後生效。"), ("Always use software rendering", "使用軟件渲染"), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", "如果對面有人, 也可以請求提升權限。"), ("Wait", "等待"), ("Elevation Error", "提權失敗"), diff --git a/src/lang/ua.rs b/src/lang/ua.rs index f0d85a551..1934a8eb4 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 5e4009570..24c0d9009 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -415,6 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", ""), ("Always use software rendering", ""), ("config_input", ""), + ("config_microphone", ""), ("request_elevation_tip", ""), ("Wait", ""), ("Elevation Error", ""), From 2452a58eaa5e0d14fd5a16e135a40a9acaf547ea Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 31 Jan 2023 14:07:14 +0800 Subject: [PATCH 1745/2015] opt: rename and move audio transmission mode --- .../lib/desktop/widgets/remote_menubar.dart | 53 ++++++++++--------- src/lang/ca.rs | 2 + src/lang/cn.rs | 2 + src/lang/cs.rs | 2 + src/lang/da.rs | 4 +- src/lang/de.rs | 2 + src/lang/eo.rs | 2 + src/lang/es.rs | 4 +- src/lang/fa.rs | 2 + src/lang/fr.rs | 2 + src/lang/gr.rs | 2 + src/lang/hu.rs | 2 + src/lang/id.rs | 2 + src/lang/it.rs | 2 + src/lang/ja.rs | 2 + src/lang/ko.rs | 2 + src/lang/kz.rs | 2 + src/lang/pl.rs | 2 + src/lang/pt_PT.rs | 2 + src/lang/ptbr.rs | 2 + src/lang/ro.rs | 2 + src/lang/ru.rs | 4 +- src/lang/sk.rs | 2 + src/lang/sl.rs | 4 +- src/lang/sq.rs | 2 + src/lang/sr.rs | 2 + src/lang/sv.rs | 2 + src/lang/template.rs | 2 + src/lang/th.rs | 2 + src/lang/tr.rs | 2 + src/lang/tw.rs | 2 + src/lang/ua.rs | 2 + src/lang/vn.rs | 2 + 33 files changed, 91 insertions(+), 34 deletions(-) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 9864947c6..0df962cba 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -884,7 +884,33 @@ class _RemoteMenubarState extends State { // )); // } } - + displayMenu.addAll([ + MenuEntryDivider(), + MenuEntryRadios( + text: translate('Audio Transmission Mode'), + optionsGetter: () => [ + MenuEntryRadioOption( + text: translate('Guest to host audio transmission'), + value: kRemoteAudioGuestToHost, + dismissOnClicked: true, + ), + MenuEntryRadioOption( + text: translate('Dual-way audio transmission'), + value: kRemoteAudioDualWay, + dismissOnClicked: true, + ), + ], + curOptionGetter: () async => + // null means peer id is not found, which there's no need to care about + await bind.sessionGetAudioMode(id: widget.id) ?? '', + optionSetter: (String oldValue, String newValue) async { + if (oldValue != newValue) { + await bind.sessionSetAudioMode(id: widget.id, value: newValue); + } + }, + padding: padding, + ), + ]); return displayMenu; } @@ -1106,31 +1132,6 @@ class _RemoteMenubarState extends State { padding: padding, ), MenuEntryDivider(), - MenuEntryRadios( - text: translate('Audio Transmission Mode'), - optionsGetter: () => [ - MenuEntryRadioOption( - text: translate('Guest to Host'), - value: kRemoteAudioGuestToHost, - dismissOnClicked: true, - ), - MenuEntryRadioOption( - text: translate('Dual way'), - value: kRemoteAudioDualWay, - dismissOnClicked: true, - ), - ], - curOptionGetter: () async => - // null means peer id is not found, which there's no need to care about - await bind.sessionGetAudioMode(id: widget.id) ?? '', - optionSetter: (String oldValue, String newValue) async { - if (oldValue != newValue) { - await bind.sessionSetAudioMode(id: widget.id, value: newValue); - } - }, - padding: padding, - ), - MenuEntryDivider(), ]; if (widget.state.viewStyle.value == kRemoteViewStyleOriginal) { diff --git a/src/lang/ca.rs b/src/lang/ca.rs index e65927876..4404e178d 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", ""), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index bcb2c3daf..08f6824c7 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", "其它默认选项"), ("Guest to Host", "被控到主机"), ("Dual way", "双向"), + ("Guest to host audio transmission", "被控到主机音频传输"), + ("Dual-way audio transmission", "双向音频传输"), ("Audio Transmission Mode", "音频传输模式"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index d16e3abef..a2a19a37a 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", ""), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 23884b995..905f4814e 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -437,8 +437,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Switch Sides", ""), ("Please confirm if you want to share your desktop?", ""), ("Closed as expected", ""), - ("Guest to Host", ""), - ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ("Display", ""), ("Default View Style", ""), diff --git a/src/lang/de.rs b/src/lang/de.rs index 1839edb87..4028e3337 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", "Weitere Standardoptionen"), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index aa8829874..fe3830b99 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", ""), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index da13843f0..b9b31f109 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -447,8 +447,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", ""), ("Other Default Options", "Otras opciones predeterminadas"), ("Closed as expected", ""), - ("Guest to Host", ""), - ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 7664af99e..0b92c6658 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", "سایر گزینه های پیش فرض"), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index db49b5a78..4965f6dab 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", "Autres options par défaut"), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/gr.rs b/src/lang/gr.rs index 5312e6381..e40151ccf 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", ""), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 2f6c490ad..0e1887e48 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", ""), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 7b9325076..689ae98cf 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", ""), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 31864b220..65f91ecec 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", "Altre Opzioni Predefinite"), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 5f2b68c46..33fb2da05 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", ""), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 59cc9fdff..c874dd695 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", ""), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 8a939764b..01014bab0 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", ""), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 788aa8b62..9dd005bdd 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", "Inne opcje domyślne"), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index c6899ee54..716d3df82 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", ""), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index cdac5f686..c7d0cd6ec 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", ""), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 5865d0206..2d48b91b4 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", ""), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index fe1de2e91..8224cd5eb 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -448,8 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", "Другие параметры по умолчанию"), ("Please confirm if you want to share your desktop?", "Подтвердите, что хотите поделиться своим рабочим столом?"), ("Closed as expected", ""), - ("Guest to Host", ""), - ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 88f09313f..5e0330954 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", ""), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index f78a6e9e3..a75da46bd 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -446,8 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), - ("Guest to Host", ""), - ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 63e834c25..d3964a2e9 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", ""), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 33355fd38..78059645d 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", ""), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 8af2ccb8a..ca2257756 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", ""), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 1abc20b36..4355d643a 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", ""), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index 173143821..57dfe6e43 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", ""), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 072275334..49a42af4a 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", ""), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 8c0968901..50e684258 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", "其它默認選項"), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 1934a8eb4..f37ed341e 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", ""), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 24c0d9009..5788a7f3d 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -448,6 +448,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other Default Options", ""), ("Guest to Host", ""), ("Dual way", ""), + ("Guest to host audio transmission", ""), + ("Dual way audio transmission", ""), ("Audio Transmission Mode", ""), ].iter().cloned().collect(); } From efa4530c97f6eee9c8c8dcd36188218ada8e52f1 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 31 Jan 2023 22:49:17 +0800 Subject: [PATCH 1746/2015] feat: add chat svg --- flutter/assets/chat.svg | 1 + .../lib/desktop/widgets/remote_menubar.dart | 96 +++++++++++-------- src/lang/cn.rs | 2 + 3 files changed, 58 insertions(+), 41 deletions(-) create mode 100644 flutter/assets/chat.svg diff --git a/flutter/assets/chat.svg b/flutter/assets/chat.svg new file mode 100644 index 000000000..03491be6e --- /dev/null +++ b/flutter/assets/chat.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 0df962cba..0004c65fe 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -9,6 +9,7 @@ import 'package:flutter_hbb/models/chat_model.dart'; import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:debounce_throttle/debounce_throttle.dart'; @@ -478,20 +479,6 @@ class _RemoteMenubarState extends State { ); } - Widget _buildChat(BuildContext context) { - return IconButton( - tooltip: translate('Chat'), - onPressed: () { - widget.ffi.chatModel.changeCurrentID(ChatModel.clientModeID); - widget.ffi.chatModel.toggleChatOverlay(); - }, - icon: const Icon( - Icons.message, - color: _MenubarTheme.commonColor, - ), - ); - } - Widget _buildMonitor(BuildContext context) { final pi = widget.ffi.ffiModel.pi; return mod_menu.PopupMenuButton( @@ -695,6 +682,60 @@ class _RemoteMenubarState extends State { ); } + Widget _buildChat(BuildContext context) { + FfiModel ffiModel = Provider.of(context); + return mod_menu.PopupMenuButton( + padding: EdgeInsets.zero, + icon: SvgPicture.asset( + "assets/chat.svg", + color: _MenubarTheme.commonColor, + width: Theme.of(context).iconTheme.size ?? 24.0, + height: Theme.of(context).iconTheme.size ?? 24.0, + ), + tooltip: translate('Chat'), + position: mod_menu.PopupMenuPosition.under, + itemBuilder: (BuildContext context) => _getChatMenu(context) + .map((entry) => entry.build( + context, + const MenuConfig( + commonColor: _MenubarTheme.commonColor, + height: _MenubarTheme.height, + dividerHeight: _MenubarTheme.dividerHeight, + ))) + .expand((i) => i) + .toList(), + ); + } + + List> _getChatMenu(BuildContext context) { + final List> chatMenu = []; + const EdgeInsets padding = EdgeInsets.only(left: 14.0, right: 5.0); + chatMenu.addAll([ + MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Text chat'), + style: style, + ), + proc: () { + widget.ffi.chatModel.changeCurrentID(ChatModel.clientModeID); + widget.ffi.chatModel.toggleChatOverlay(); + }, + padding: padding, + dismissOnClicked: true, + ), + MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Voice call'), + style: style, + ), + proc: () {}, + padding: padding, + dismissOnClicked: true, + ), + ]); + return chatMenu; + } + List> _getControlMenu(BuildContext context) { final pi = widget.ffi.ffiModel.pi; final perms = widget.ffi.ffiModel.permissions; @@ -884,33 +925,6 @@ class _RemoteMenubarState extends State { // )); // } } - displayMenu.addAll([ - MenuEntryDivider(), - MenuEntryRadios( - text: translate('Audio Transmission Mode'), - optionsGetter: () => [ - MenuEntryRadioOption( - text: translate('Guest to host audio transmission'), - value: kRemoteAudioGuestToHost, - dismissOnClicked: true, - ), - MenuEntryRadioOption( - text: translate('Dual-way audio transmission'), - value: kRemoteAudioDualWay, - dismissOnClicked: true, - ), - ], - curOptionGetter: () async => - // null means peer id is not found, which there's no need to care about - await bind.sessionGetAudioMode(id: widget.id) ?? '', - optionSetter: (String oldValue, String newValue) async { - if (oldValue != newValue) { - await bind.sessionSetAudioMode(id: widget.id, value: newValue); - } - }, - padding: padding, - ), - ]); return displayMenu; } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 08f6824c7..65039f0fe 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -450,6 +450,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Dual way", "双向"), ("Guest to host audio transmission", "被控到主机音频传输"), ("Dual-way audio transmission", "双向音频传输"), + ("Voice call", "语音通话"), + ("Text chat", "文字聊天"), ("Audio Transmission Mode", "音频传输模式"), ].iter().cloned().collect(); } From b335d2c82840dd3ef09efc9ebdd7d417ca3a9a25 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sun, 5 Feb 2023 15:36:31 +0800 Subject: [PATCH 1747/2015] fix: import --- src/client/io_loop.rs | 1 - src/flutter_ffi.rs | 3 --- src/server.rs | 7 ------- 3 files changed, 11 deletions(-) diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 9117c8c5f..dcfa7b74e 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -12,7 +12,6 @@ use clipboard::{cliprdr::CliprdrClientContext, ContextSend}; use hbb_common::tokio::sync::mpsc::error::TryRecvError; -use crate::server::Service; use crate::ui_session_interface::{InvokeUiSession, Session}; use crate::{client::Data, client::Interface}; diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 0fe6818de..1ecbb0646 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -11,15 +11,12 @@ use hbb_common::{ config::{self, LocalConfig, ONLINE, PeerConfig}, fs, log, }; -use hbb_common::message_proto::KeyboardMode; -use hbb_common::ResultType; use crate::{ client::file_trait::FileManager, common::make_fd_to_json, flutter::{session_add, session_start_}, }; -use crate::common::is_keyboard_mode_supported; use crate::flutter::{self, SESSIONS}; use crate::ui_interface::{self, *}; diff --git a/src/server.rs b/src/server.rs index bef49f132..616d92375 100644 --- a/src/server.rs +++ b/src/server.rs @@ -29,13 +29,6 @@ use service::{GenericService, Service, Subscriber}; use service::ServiceTmpl; use crate::ipc::{connect, Data}; -pub use service::{GenericService, Service, ServiceTmpl, Subscriber}; -use std::{ - collections::HashMap, - net::SocketAddr, - sync::{Arc, Mutex, RwLock, Weak}, - time::Duration, -}; pub mod audio_service; cfg_if::cfg_if! { From 45b93100d6d0837d53d12dca30605cc0b10b1ea4 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sun, 5 Feb 2023 23:47:06 +0800 Subject: [PATCH 1748/2015] feat: add voice call proto --- libs/hbb_common/protos/message.proto | 14 ++++++ src/client.rs | 49 +++++++++++---------- src/client/io_loop.rs | 64 ++++++++++++++++++---------- src/flutter_ffi.rs | 18 ++++++-- src/server/connection.rs | 6 +++ src/ui/remote.rs | 8 ++-- src/ui_session_interface.rs | 48 +++++++++++++-------- 7 files changed, 138 insertions(+), 69 deletions(-) diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index 48b999438..323b464fa 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -604,6 +604,18 @@ message Misc { } } +message VoiceCallRequest { + int64 req_timestamp = 1; + // Indicates whether the request is a connect action or a disconnect action. + bool is_connect = 2; +} + +message VoiceCallResponse { + bool accepted = 1; + int64 req_timestamp = 2; // Should copy from [VoiceCallRequest::req_timestamp]. + int64 ack_timestamp = 3; +} + message Message { oneof union { SignedId signed_id = 3; @@ -626,5 +638,7 @@ message Message { Cliprdr cliprdr = 20; MessageBox message_box = 21; SwitchSidesResponse switch_sides_response = 22; + VoiceCallRequest voice_call_request = 23; + VoiceCallResponse voice_call_response = 24; } } diff --git a/src/client.rs b/src/client.rs index 649b180bb..5911c40ed 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,58 +1,61 @@ -pub use async_trait::async_trait; -use bytes::Bytes; -#[cfg(not(any(target_os = "android", target_os = "linux")))] -use cpal::{ - traits::{DeviceTrait, HostTrait, StreamTrait}, - Device, Host, StreamConfig, -}; -use magnum_opus::{Channels::*, Decoder as AudioDecoder}; -use sha2::{Digest, Sha256}; use std::{ collections::HashMap, net::SocketAddr, ops::{Deref, Not}, str::FromStr, - sync::{atomic::AtomicBool, mpsc, Arc, Mutex, RwLock}, + sync::{Arc, atomic::AtomicBool, mpsc, Mutex, RwLock}, }; + +pub use async_trait::async_trait; +use bytes::Bytes; +#[cfg(not(any(target_os = "android", target_os = "linux")))] +use cpal::{ + Device, + Host, StreamConfig, traits::{DeviceTrait, HostTrait, StreamTrait}, +}; +use magnum_opus::{Channels::*, Decoder as AudioDecoder}; +use sha2::{Digest, Sha256}; use uuid::Uuid; pub use file_trait::FileManager; use hbb_common::{ + AddrMangle, allow_err, anyhow::{anyhow, Context}, bail, config::{ - Config, PeerConfig, PeerInfoSerde, CONNECT_TIMEOUT, READ_TIMEOUT, RELAY_PORT, + Config, CONNECT_TIMEOUT, PeerConfig, PeerInfoSerde, READ_TIMEOUT, RELAY_PORT, RENDEZVOUS_TIMEOUT, - }, - get_version_number, log, - message_proto::{option_message::BoolOption, *}, + }, get_version_number, + log, + message_proto::{*, option_message::BoolOption}, protobuf::Message as _, rand, rendezvous_proto::*, + ResultType, socket_client, sodiumoxide::crypto::{box_, secretbox, sign}, - timeout, - tokio::time::Duration, - AddrMangle, ResultType, Stream, + Stream, timeout, tokio::time::Duration, }; -pub use helper::LatencyController; pub use helper::*; +pub use helper::LatencyController; use scrap::{ codec::{Decoder, DecoderCfg}, record::{Recorder, RecorderContext}, VpxDecoderConfig, VpxVideoCodecId, }; +use crate::{ + common::{self, is_keyboard_mode_supported}, + server::video_service::{SCRAP_X11_REF_URL, SCRAP_X11_REQUIRED}, +}; + pub use super::lang::*; pub mod file_trait; pub mod helper; pub mod io_loop; -use crate::{ - common::{self, is_keyboard_mode_supported}, - server::video_service::{SCRAP_X11_REF_URL, SCRAP_X11_REQUIRED}, -}; + pub static SERVER_KEYBOARD_ENABLED: AtomicBool = AtomicBool::new(true); pub static SERVER_FILE_TRANSFER_ENABLED: AtomicBool = AtomicBool::new(true); pub static SERVER_CLIPBOARD_ENABLED: AtomicBool = AtomicBool::new(true); @@ -1989,6 +1992,8 @@ pub enum Data { RecordScreen(bool, i32, i32, String), ElevateDirect, ElevateWithLogon(String, String), + NewVoiceCall, + CloseVoiceCall, } /// Keycode for key events. diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index dcfa7b74e..67946f546 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -1,42 +1,38 @@ -use crate::client::{ - Client, CodecFormat, LoginConfigHandler, MediaData, MediaSender, QualityStatus, MILLI1, SEC30, - SERVER_CLIPBOARD_ENABLED, SERVER_FILE_TRANSFER_ENABLED, SERVER_KEYBOARD_ENABLED, -}; -use crate::common::{get_default_sound_input, set_sound_input}; -#[cfg(not(any(target_os = "android", target_os = "ios")))] -use crate::common::{check_clipboard, update_clipboard, ClipboardContext, CLIPBOARD_INTERVAL}; -use crate::{audio_service, common, ConnInner, CLIENT_SERVER}; +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; +use std::sync::atomic::{AtomicUsize, Ordering}; #[cfg(windows)] use clipboard::{cliprdr::CliprdrClientContext, ContextSend}; - -use hbb_common::tokio::sync::mpsc::error::TryRecvError; - -use crate::ui_session_interface::{InvokeUiSession, Session}; -use crate::{client::Data, client::Interface}; - +use hbb_common::{allow_err, message_proto::*, sleep, get_time}; +use hbb_common::{fs, log, Stream}; use hbb_common::config::{PeerConfig, TransferSerde}; use hbb_common::fs::{ - can_enable_overwrite_detection, get_job, get_string, new_send_confirm, DigestCheckResult, + can_enable_overwrite_detection, DigestCheckResult, get_job, get_string, new_send_confirm, RemoveJobMeta, }; use hbb_common::message_proto::permission_info::Permission; use hbb_common::protobuf::Message as _; use hbb_common::rendezvous_proto::ConnType; -#[cfg(windows)] -use hbb_common::tokio::sync::Mutex as TokioMutex; use hbb_common::tokio::{ self, sync::mpsc, time::{self, Duration, Instant, Interval}, }; -use hbb_common::{allow_err, message_proto::*, sleep}; -use hbb_common::{fs, log, Stream}; +use hbb_common::tokio::sync::mpsc::error::TryRecvError; +#[cfg(windows)] +use hbb_common::tokio::sync::Mutex as TokioMutex; -use std::collections::HashMap; - -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::{Arc, Mutex}; +use crate::{audio_service, CLIENT_SERVER, common, ConnInner}; +use crate::{client::Data, client::Interface}; +use crate::client::{ + Client, CodecFormat, LoginConfigHandler, MediaData, MediaSender, MILLI1, QualityStatus, SEC30, + SERVER_CLIPBOARD_ENABLED, SERVER_FILE_TRANSFER_ENABLED, SERVER_KEYBOARD_ENABLED, +}; +use crate::common::{get_default_sound_input, set_sound_input}; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use crate::common::{check_clipboard, CLIPBOARD_INTERVAL, ClipboardContext, update_clipboard}; +use crate::ui_session_interface::{InvokeUiSession, Session}; pub struct Remote { handler: Session, @@ -752,6 +748,22 @@ impl Remote { msg.set_misc(misc); allow_err!(peer.send(&msg).await); } + Data::NewVoiceCall => { + let mut request = VoiceCallRequest::new(); + request.is_connect = true; + request.req_timestamp = get_time(); + let mut msg = Message::new(); + msg.set_voice_call_request(request); + allow_err!(peer.send(&msg).await); + } + Data::CloseVoiceCall => { + let mut request = VoiceCallRequest::new(); + request.is_connect = false; + request.req_timestamp = get_time(); + let mut msg = Message::new(); + msg.set_voice_call_request(request); + allow_err!(peer.send(&msg).await); + } _ => {} } true @@ -1262,6 +1274,12 @@ impl Remote { self.handler .msgbox(&msgbox.msgtype, &msgbox.title, &msgbox.text, &link); } + Some(message::Union::VoiceCallRequest(request)) => { + // TODO + } + Some(message::Union::VoiceCallResponse(response)) => { + // TODO + } _ => {} } } diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 1ecbb0646..15bfe90d4 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -4,19 +4,19 @@ use std::str::FromStr; use flutter_rust_bridge::{StreamSink, SyncReturn, ZeroCopyBuffer}; use serde_json::json; -use crate::common::{is_keyboard_mode_supported, get_default_sound_input}; -use hbb_common::message_proto::KeyboardMode; -use hbb_common::ResultType; use hbb_common::{ config::{self, LocalConfig, ONLINE, PeerConfig}, fs, log, }; +use hbb_common::message_proto::KeyboardMode; +use hbb_common::ResultType; use crate::{ client::file_trait::FileManager, common::make_fd_to_json, flutter::{session_add, session_start_}, }; +use crate::common::{get_default_sound_input, is_keyboard_mode_supported}; use crate::flutter::{self, SESSIONS}; use crate::ui_interface::{self, *}; @@ -840,6 +840,18 @@ pub fn session_new_rdp(id: String) { } } +pub fn session_request_voice_call(id: String) { + if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) { + session.request_voice_call(); + } +} + +pub fn session_close_voice_call(id: String) { + if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) { + session.close_voice_call(); + } +} + pub fn main_get_last_remote_id() -> String { LocalConfig::get_remote_id() } diff --git a/src/server/connection.rs b/src/server/connection.rs index 20cbe0f86..c3acae9cc 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1572,6 +1572,12 @@ impl Connection { allow_err!(self.audio_sender.send(MediaData::AudioFrame(frame))); } } + Some(message::Union::VoiceCallRequest(request)) => { + // TODO + } + Some(message::Union::VoiceCallResponse(response)) => { + // TODO + } _ => {} } } diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 541d3a141..1b0d172b9 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -6,12 +6,12 @@ use std::{ use sciter::{ dom::{ - event::{EventReason, BEHAVIOR_EVENTS, EVENT_GROUPS, PHASE_MASK}, - Element, HELEMENT, + Element, + event::{BEHAVIOR_EVENTS, EVENT_GROUPS, EventReason, PHASE_MASK}, HELEMENT, }, make_args, - video::{video_destination, AssetPtr, COLOR_SPACE}, Value, + video::{AssetPtr, COLOR_SPACE, video_destination}, }; use hbb_common::{ @@ -422,6 +422,8 @@ impl sciter::EventHandler for SciterSession { fn restart_remote_device(); fn save_audio_mode(String); fn get_audio_mode(); + fn request_voice_call(); + fn close_voice_call(); } } diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 1e7848505..147cd9149 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -1,26 +1,30 @@ -use crate::client::io_loop::Remote; -use crate::client::{ - check_if_retry, handle_hash, handle_login_error, handle_login_from_ui, handle_test_delay, - input_os_password, load_config, send_mouse, start_video_audio_threads, FileManager, Key, - LoginConfigHandler, QualityStatus, KEY_MAP, -}; -use crate::common::{self, GrabState}; -use crate::keyboard; -use crate::{client::Data, client::Interface}; -use async_trait::async_trait; -use bytes::Bytes; -use hbb_common::config::{Config, LocalConfig, PeerConfig, RS_PUB_KEY}; -use hbb_common::rendezvous_proto::ConnType; -use hbb_common::tokio::{self, sync::mpsc}; -use hbb_common::{allow_err, message_proto::*}; -use hbb_common::{fs, get_version_number, log, Stream}; -use rdev::{Event, EventType::*}; use std::collections::HashMap; use std::ops::{Deref, DerefMut}; use std::str::FromStr; -use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::{Arc, Mutex, RwLock}; +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; + +use async_trait::async_trait; +use bytes::Bytes; +use rdev::{Event, EventType::*}; use uuid::Uuid; + +use hbb_common::{allow_err, message_proto::*}; +use hbb_common::{fs, get_version_number, log, Stream}; +use hbb_common::config::{Config, LocalConfig, PeerConfig, RS_PUB_KEY}; +use hbb_common::rendezvous_proto::ConnType; +use hbb_common::tokio::{self, sync::mpsc}; + +use crate::{client::Data, client::Interface}; +use crate::client::{ + check_if_retry, FileManager, handle_hash, handle_login_error, handle_login_from_ui, + handle_test_delay, input_os_password, Key, KEY_MAP, load_config, LoginConfigHandler, + QualityStatus, send_mouse, start_video_audio_threads, +}; +use crate::client::io_loop::Remote; +use crate::common::{self, GrabState}; +use crate::keyboard; + pub static IS_IN: AtomicBool = AtomicBool::new(false); #[derive(Clone, Default)] @@ -669,6 +673,14 @@ impl Session { } } } + + pub fn request_voice_call(&self) { + self.send(Data::NewVoiceCall); + } + + pub fn close_voice_call(&self) { + self.send(Data::CloseVoiceCall); + } } pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { From a04980fa1325a2da1a2625983b1aa016a3153187 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 6 Feb 2023 09:37:52 +0800 Subject: [PATCH 1749/2015] refactor: remove audio mode --- libs/hbb_common/protos/message.proto | 6 ----- src/client.rs | 40 ---------------------------- src/client/io_loop.rs | 36 ------------------------- src/flutter_ffi.rs | 14 ---------- src/ui/header.tis | 5 ---- src/ui/remote.rs | 2 -- src/ui_session_interface.rs | 16 ----------- 7 files changed, 119 deletions(-) diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index 323b464fa..ed2706382 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -444,11 +444,6 @@ enum ImageQuality { Best = 4; } -enum AudioMode { - GuestToHost = 0; - DualWay = 1; -} - message VideoCodecState { enum PreferCodec { Auto = 0; @@ -480,7 +475,6 @@ message OptionMessage { BoolOption enable_file_transfer = 9; VideoCodecState video_codec_state = 10; int32 custom_fps = 11; - AudioMode audio_mode = 12; } message TestDelay { diff --git a/src/client.rs b/src/client.rs index 5911c40ed..2ea33b655 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1255,27 +1255,6 @@ impl LoginConfigHandler { } } - /// Parse the audio mode option. - /// Return [`AudioMode`] if the option is valid, otherwise return `None`. - /// - /// # Arguments - /// - /// * `q` - The audio mode option. - /// * `ignore_default` - Ignore the default value. - pub fn get_audio_mode_enum(q: &str, ignore_default: bool) -> Option { - if q == "guest-to-host" { - Some(AudioMode::GuestToHost) - } else if q == "dual-way" { - Some(AudioMode::DualWay) - } else { - if ignore_default { - None - } else { - Some(AudioMode::GuestToHost) - } - } - } - /// Get the status of a toggle option. /// /// # Arguments @@ -1362,24 +1341,6 @@ impl LoginConfigHandler { res } - pub fn save_audio_mode(&mut self, value: String) -> Option { - let mut res = None; - if let Some(q) = LoginConfigHandler::get_audio_mode_enum(&value, false) { - let mut misc = Misc::new(); - misc.set_option(OptionMessage { - audio_mode: q.into(), - ..Default::default() - }); - let mut msg_out = Message::new(); - msg_out.set_misc(misc); - res = Some(msg_out); - } - let mut config = self.load_config(); - config.audio_mode = value; - self.save_config(config); - res - } - /// Create a [`Message`] for saving custom fps. /// /// # Arguments @@ -1984,7 +1945,6 @@ pub enum Data { RemovePortForward(i32), AddPortForward((i32, String, i32)), ToggleClipboardFile, - ChangeAudioMode(AudioMode), NewRDP, SetConfirmOverrideFile((i32, i32, bool, bool, bool)), AddJob((i32, String, String, i32, bool, bool)), diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 67946f546..d0e72a7e6 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -387,24 +387,6 @@ impl Remote { Data::ToggleClipboardFile => { self.check_clipboard_file_context(); } - Data::ChangeAudioMode(audio_mode) => { - match audio_mode { - AudioMode::GuestToHost => { - if let Some(sender) = self.stop_local_audio_sender.take() { - allow_err!(sender.send(())); - } - } - AudioMode::DualWay => { - // Start audio thread for playback. - // Cancel previous local audio session. - if let Some(sender) = self.stop_local_audio_sender.take() { - allow_err!(sender.send(())); - } - // Start client audio when connection is established. - self.stop_local_audio_sender = self.start_client_audio(); - } - } - } Data::Message(msg) => { allow_err!(peer.send(&msg).await); } @@ -905,24 +887,6 @@ impl Remote { if self.handler.is_file_transfer() { self.handler.load_last_jobs(); } - - // Start audio thread for playback if current audio mode is dual-way transmission. - if !self.handler.is_file_transfer() && !self.handler.is_port_forward() { - let audio_mode = LoginConfigHandler::get_audio_mode_enum( - self.handler.load_config().audio_mode.as_str(), - false, - ) - .unwrap_or(AudioMode::GuestToHost); - log::debug!("current audio mode: {:?}", audio_mode); - if audio_mode == AudioMode::DualWay { - // Cancel previous local audio session. - if let Some(sender) = self.stop_local_audio_sender.take() { - allow_err!(sender.send(())); - } - // Start client audio when connection is established. - self.stop_local_audio_sender = self.start_client_audio(); - } - } } _ => {} }, diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 15bfe90d4..e28332943 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -233,20 +233,6 @@ pub fn session_set_image_quality(id: String, value: String) { } } -pub fn session_get_audio_mode(id: String) -> Option { - if let Some(session) = SESSIONS.read().unwrap().get(&id) { - Some(session.get_audio_mode()) - } else { - None - } -} - -pub fn session_set_audio_mode(id: String, value: String) { - if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) { - session.save_audio_mode(value); - } -} - pub fn session_get_keyboard_mode(id: String) -> Option { if let Some(session) = SESSIONS.read().unwrap().get(&id) { Some(session.get_keyboard_mode()) diff --git a/src/ui/header.tis b/src/ui/header.tis index e3f0c70a1..009995f4f 100644 --- a/src/ui/header.tis +++ b/src/ui/header.tis @@ -183,9 +183,6 @@ class Header: Reactor.Component {
  • {svg_checkmark}{translate('Balanced')}
  • {svg_checkmark}{translate('Optimize reaction time')}
  • {svg_checkmark}{translate('Custom')}
  • -
    -
  • {svg_checkmark}{translate('Guest to Host')}
  • -
  • {svg_checkmark}{translate('Dual way')}
  • {show_codec ?
  • {svg_checkmark}Auto
  • @@ -394,8 +391,6 @@ class Header: Reactor.Component { } else if (type == "codec-preference") { handler.set_option("codec-preference", me.id); handler.change_prefer_codec(); - } else if (type == "audio-mode") { - handler.save_audio_mode(me.id); } toggleMenuState(); } diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 1b0d172b9..5d6692c3b 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -420,8 +420,6 @@ impl sciter::EventHandler for SciterSession { fn supported_hwcodec(); fn change_prefer_codec(); fn restart_remote_device(); - fn save_audio_mode(String); - fn get_audio_mode(); fn request_voice_call(); fn close_voice_call(); } diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 147cd9149..2f6827523 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -93,22 +93,6 @@ impl Session { self.lc.write().unwrap().save_keyboard_mode(value); } - pub fn get_audio_mode(&self) -> String { - self.lc.read().unwrap().audio_mode.clone() - } - - pub fn save_audio_mode(&self, value: String) { - let mode = LoginConfigHandler::get_audio_mode_enum(value.as_str(), false); - if let Some(mode)= mode { - self.send(Data::ChangeAudioMode(mode)); - } - let msg = self.lc.write().unwrap().save_audio_mode(value); - // Notify remote guest that the audio mode has been changed. - if let Some(msg) = msg { - self.send(Data::Message(msg)); - } - } - pub fn save_view_style(&mut self, value: String) { self.lc.write().unwrap().save_view_style(value); } From b412a7122b837dd3d9d31c29f04ffc237356d97c Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 6 Feb 2023 11:42:25 +0800 Subject: [PATCH 1750/2015] feat: rust connection implementation --- src/client/helper.rs | 23 +++++- src/client/io_loop.rs | 85 ++++++++++++-------- src/flutter.rs | 16 ++++ src/ipc.rs | 5 +- src/lang/cn.rs | 1 + src/server/connection.rs | 151 ++++++++++++++++++++++++------------ src/ui/remote.rs | 16 ++++ src/ui_session_interface.rs | 4 + 8 files changed, 220 insertions(+), 81 deletions(-) diff --git a/src/client/helper.rs b/src/client/helper.rs index e3acf3a44..20acd811a 100644 --- a/src/client/helper.rs +++ b/src/client/helper.rs @@ -5,7 +5,7 @@ use std::{ use hbb_common::{ log, - message_proto::{video_frame, VideoFrame}, + message_proto::{video_frame, VideoFrame, Message, VoiceCallRequest, VoiceCallResponse}, get_time, }; const MAX_LATENCY: i64 = 500; @@ -115,3 +115,24 @@ pub struct QualityStatus { pub target_bitrate: Option, pub codec_format: Option, } + +#[inline] +pub fn new_voice_call_request(is_connect: bool) -> Message { + let mut req = VoiceCallRequest::new(); + req.is_connect = is_connect; + req.req_timestamp = get_time(); + let mut msg = Message::new(); + msg.set_voice_call_request(req); + msg +} + +#[inline] +pub fn new_voice_call_response(request_timestamp: i64, accepted: bool) -> Message { + let mut resp = VoiceCallResponse::new(); + resp.accepted = accepted; + resp.req_timestamp = request_timestamp; + resp.ack_timestamp = get_time(); + let mut msg = Message::new(); + msg.set_voice_call_response(resp); + msg +} \ No newline at end of file diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index d0e72a7e6..8f2b45321 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -1,38 +1,40 @@ use std::collections::HashMap; -use std::sync::{Arc, Mutex}; +use std::num::NonZeroI64; use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::{Arc, Mutex}; #[cfg(windows)] use clipboard::{cliprdr::CliprdrClientContext, ContextSend}; -use hbb_common::{allow_err, message_proto::*, sleep, get_time}; -use hbb_common::{fs, log, Stream}; use hbb_common::config::{PeerConfig, TransferSerde}; use hbb_common::fs::{ - can_enable_overwrite_detection, DigestCheckResult, get_job, get_string, new_send_confirm, + can_enable_overwrite_detection, get_job, get_string, new_send_confirm, DigestCheckResult, RemoveJobMeta, }; use hbb_common::message_proto::permission_info::Permission; use hbb_common::protobuf::Message as _; use hbb_common::rendezvous_proto::ConnType; +use hbb_common::tokio::sync::mpsc::error::TryRecvError; +#[cfg(windows)] +use hbb_common::tokio::sync::Mutex as TokioMutex; use hbb_common::tokio::{ self, sync::mpsc, time::{self, Duration, Instant, Interval}, }; -use hbb_common::tokio::sync::mpsc::error::TryRecvError; -#[cfg(windows)] -use hbb_common::tokio::sync::Mutex as TokioMutex; +use hbb_common::{allow_err, get_time, message_proto::*, sleep}; +use hbb_common::{fs, log, Stream}; -use crate::{audio_service, CLIENT_SERVER, common, ConnInner}; -use crate::{client::Data, client::Interface}; use crate::client::{ - Client, CodecFormat, LoginConfigHandler, MediaData, MediaSender, MILLI1, QualityStatus, SEC30, - SERVER_CLIPBOARD_ENABLED, SERVER_FILE_TRANSFER_ENABLED, SERVER_KEYBOARD_ENABLED, + new_voice_call_request, Client, CodecFormat, LoginConfigHandler, MediaData, MediaSender, + QualityStatus, MILLI1, SEC30, SERVER_CLIPBOARD_ENABLED, SERVER_FILE_TRANSFER_ENABLED, + SERVER_KEYBOARD_ENABLED, }; -use crate::common::{get_default_sound_input, set_sound_input}; #[cfg(not(any(target_os = "android", target_os = "ios")))] -use crate::common::{check_clipboard, CLIPBOARD_INTERVAL, ClipboardContext, update_clipboard}; +use crate::common::{check_clipboard, update_clipboard, ClipboardContext, CLIPBOARD_INTERVAL}; +use crate::common::{get_default_sound_input, set_sound_input}; use crate::ui_session_interface::{InvokeUiSession, Session}; +use crate::{audio_service, common, ConnInner, CLIENT_SERVER}; +use crate::{client::Data, client::Interface}; pub struct Remote { handler: Session, @@ -41,7 +43,8 @@ pub struct Remote { receiver: mpsc::UnboundedReceiver, sender: mpsc::UnboundedSender, // Stop sending local audio to remote client. - stop_local_audio_sender: Option>, + stop_voice_call_sender: Option>, + voice_call_request_timestamp: Option, old_clipboard: Arc>, read_jobs: Vec, write_jobs: Vec, @@ -83,7 +86,8 @@ impl Remote { data_count: Arc::new(AtomicUsize::new(0)), frame_count, video_format: CodecFormat::Unknown, - stop_local_audio_sender: None, + stop_voice_call_sender: None, + voice_call_request_timestamp: None, } } @@ -217,7 +221,7 @@ impl Remote { } log::debug!("Exit io_loop of id={}", self.handler.id); // Stop client audio server. - if let Some(s) = self.stop_local_audio_sender.take() { + if let Some(s) = self.stop_voice_call_sender.take() { s.send(()).ok(); } } @@ -261,8 +265,15 @@ impl Remote { } } - // Start a local audio recorder, records audio and send to remote - fn start_client_audio(&mut self) -> Option> { + fn stop_voice_call(&mut self) { + let voice_call_sender = std::mem::replace(&mut self.stop_voice_call_sender, None); + if let Some(stopper) = voice_call_sender { + let _ = stopper.send(()); + } + } + + // Start a voice call recorder, records audio and send to remote + fn start_voice_call(&mut self) -> Option> { if self.handler.is_file_transfer() || self.handler.is_port_forward() { return None; } @@ -731,19 +742,17 @@ impl Remote { allow_err!(peer.send(&msg).await); } Data::NewVoiceCall => { - let mut request = VoiceCallRequest::new(); - request.is_connect = true; - request.req_timestamp = get_time(); - let mut msg = Message::new(); - msg.set_voice_call_request(request); + let msg = new_voice_call_request(true); + // Save the voice call request timestamp for the further validation. + self.voice_call_request_timestamp = Some( + NonZeroI64::new(msg.voice_call_request().req_timestamp) + .unwrap_or(NonZeroI64::new(get_time()).unwrap()), + ); allow_err!(peer.send(&msg).await); } Data::CloseVoiceCall => { - let mut request = VoiceCallRequest::new(); - request.is_connect = false; - request.req_timestamp = get_time(); - let mut msg = Message::new(); - msg.set_voice_call_request(request); + self.stop_voice_call(); + let msg = new_voice_call_request(false); allow_err!(peer.send(&msg).await); } _ => {} @@ -1238,11 +1247,25 @@ impl Remote { self.handler .msgbox(&msgbox.msgtype, &msgbox.title, &msgbox.text, &link); } - Some(message::Union::VoiceCallRequest(request)) => { - // TODO + Some(message::Union::VoiceCallRequest(_request)) => { + // TODO: maybe we will do voice call from the peer. } Some(message::Union::VoiceCallResponse(response)) => { - // TODO + let ts = std::mem::replace(&mut self.voice_call_request_timestamp, None); + if let Some(ts) = ts { + if response.req_timestamp != ts.get() { + log::debug!("Possible encountering a voice call attack."); + } else { + if response.accepted { + // The peer accepts the voice call. + self.handler.on_voice_call_start(); + self.stop_voice_call_sender = self.start_voice_call(); + } else { + // The peer refused the voice call. + self.handler.on_voice_call_stop("Refused"); + } + } + } } _ => {} } diff --git a/src/flutter.rs b/src/flutter.rs index b4f1f6bc6..7062d85df 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -394,6 +394,22 @@ impl InvokeUiSession for FlutterHandler { fn switch_back(&self, peer_id: &str) { self.push_event("switch_back", [("peer_id", peer_id)].into()); } + + fn on_voice_call_start(&self) { + self.push_event("on_voice_call_start", [].into()); + } + + fn on_voice_call_stop(&self, reason: &str) { + self.push_event("on_voice_call_stop", [("reason", reason)].into()) + } + + fn on_voice_call_waiting(&self) { + self.push_event("on_voice_call_waiting", [].into()); + } + + fn on_voice_call_incoming(&self) { + self.push_event("on_voice_call_incoming", [].into()); + } } /// Create a new remote session with the given id. diff --git a/src/ipc.rs b/src/ipc.rs index d610fb84d..18f618847 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -210,7 +210,10 @@ pub enum Data { DataPortableService(DataPortableService), SwitchSidesRequest(String), SwitchSidesBack, - UrlLink(String) + UrlLink(String), + VoiceCallIncoming, + VoiceCallResponse(bool), + CloseVoiceCall(String), } #[tokio::main(flavor = "current_thread")] diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 65039f0fe..5a9abba9c 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -453,5 +453,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", "语音通话"), ("Text chat", "文字聊天"), ("Audio Transmission Mode", "音频传输模式"), + ("Refused", "已拒绝") ].iter().cloned().collect(); } diff --git a/src/server/connection.rs b/src/server/connection.rs index c3acae9cc..1007c71ca 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -5,7 +5,11 @@ use crate::clipboard_file::*; use crate::common::update_clipboard; #[cfg(windows)] use crate::portable_service::client as portable_client; -use crate::{video_service, client::{MediaSender, start_audio_thread, LatencyController, MediaData}, common::{get_default_sound_input, set_sound_input}}; +use crate::{ + client::{start_audio_thread, LatencyController, MediaData, MediaSender, new_voice_call_request, new_voice_call_response}, + common::{get_default_sound_input, set_sound_input}, + video_service, +}; #[cfg(any(target_os = "android", target_os = "ios"))] use crate::{common::DEVICE_NAME, flutter::connection_manager::start_channel}; use crate::{ipc, VERSION}; @@ -32,7 +36,10 @@ use serde_json::{json, value::Value}; use sha2::{Digest, Sha256}; #[cfg(not(any(target_os = "android", target_os = "ios")))] use std::sync::atomic::Ordering; -use std::sync::{atomic::AtomicI64, mpsc as std_mpsc}; +use std::{ + num::NonZeroI64, + sync::{atomic::AtomicI64, mpsc as std_mpsc}, +}; #[cfg(not(any(target_os = "android", target_os = "ios")))] use system_shutdown; @@ -90,13 +97,19 @@ pub struct Connection { recording: bool, last_test_delay: i64, lock_after_session_end: bool, - show_remote_cursor: bool, // by peer + show_remote_cursor: bool, + // by peer ip: String, - disable_clipboard: bool, // by peer - disable_audio: bool, // by peer - enable_file_transfer: bool, // by peer - audio_sender: MediaSender, // audio by the remote peer/client - tx_input: std_mpsc::Sender, // handle input messages + disable_clipboard: bool, + // by peer + disable_audio: bool, + // by peer + enable_file_transfer: bool, + // by peer + audio_sender: MediaSender, + // audio by the remote peer/client + tx_input: std_mpsc::Sender, + // handle input messages video_ack_required: bool, peer_info: (String, String), server_audit_conn: String, @@ -107,6 +120,8 @@ pub struct Connection { #[cfg(windows)] portable: PortableState, from_switch: bool, + voice_call_request_timestamp: Option, + audio_input_device_before_voice_call: Option, } impl ConnInner { @@ -216,6 +231,8 @@ impl Connection { portable: Default::default(), from_switch: false, audio_sender, + voice_call_request_timestamp: None, + audio_input_device_before_voice_call: None, }; #[cfg(not(any(target_os = "android", target_os = "ios")))] tokio::spawn(async move { @@ -380,6 +397,12 @@ impl Connection { msg.set_misc(misc); conn.send(msg).await; } + ipc::Data::VoiceCallResponse(accepted) => { + conn.start_voice_call().await; + } + ipc::Data::CloseVoiceCall(_reason) => { + conn.close_voice_call().await; + } _ => {} } }, @@ -650,15 +673,15 @@ impl Connection { .collect(); if !whitelist.is_empty() && whitelist - .iter() - .filter(|x| x == &"0.0.0.0") - .next() - .is_none() + .iter() + .filter(|x| x == &"0.0.0.0") + .next() + .is_none() && whitelist - .iter() - .filter(|x| IpCidr::from_str(x).map_or(false, |y| y.contains(addr.ip()))) - .next() - .is_none() + .iter() + .filter(|x| IpCidr::from_str(x).map_or(false, |y| y.contains(addr.ip()))) + .next() + .is_none() { self.send_login_error("Your ip is blocked by the peer") .await; @@ -784,7 +807,7 @@ impl Connection { }; self.post_conn_audit(json!({"peer": self.peer_info, "type": conn_type})); #[allow(unused_mut)] - let mut username = crate::platform::get_active_username(); + let mut username = crate::platform::get_active_username(); let mut res = LoginResponse::new(); let mut pi = PeerInfo { username: username.clone(), @@ -811,7 +834,7 @@ impl Connection { h265, ..Default::default() }) - .into(); + .into(); } if self.port_forward_socket.is_some() { @@ -855,7 +878,7 @@ impl Connection { privacy_mode: video_service::is_privacy_mode_supported(), ..Default::default() }) - .into(); + .into(); let mut sub_service = false; if self.file_transfer.is_some() { @@ -1138,7 +1161,7 @@ impl Connection { "Failed to access remote {}, please make sure if it is open", addr )) - .await; + .await; return false; } } @@ -1302,12 +1325,12 @@ impl Connection { } } Some(message::Union::Clipboard(cb)) => - { - #[cfg(not(any(target_os = "android", target_os = "ios")))] - if self.clipboard { - update_clipboard(cb, None); + { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if self.clipboard { + update_clipboard(cb, None); + } } - } Some(message::Union::Cliprdr(_clip)) => { if self.file_transfer_enabled() { #[cfg(windows)] @@ -1490,15 +1513,15 @@ impl Connection { } Some(misc::Union::RestartRemoteDevice(_)) => - { - #[cfg(not(any(target_os = "android", target_os = "ios")))] - if self.restart { - match system_shutdown::reboot() { - Ok(_) => log::info!("Restart by the peer"), - Err(e) => log::error!("Failed to restart:{}", e), + { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if self.restart { + match system_shutdown::reboot() { + Ok(_) => log::info!("Restart by the peer"), + Err(e) => log::error!("Failed to restart:{}", e), + } } } - } Some(misc::Union::ElevationRequest(r)) => match r.union { Some(elevation_request::Union::Direct(_)) => { #[cfg(windows)] @@ -1508,8 +1531,8 @@ impl Connection { err = portable_client::start_portable_service( portable_client::StartPara::Direct, ) - .err() - .map_or("".to_string(), |e| e.to_string()); + .err() + .map_or("".to_string(), |e| e.to_string()); } self.portable.elevation_requested = err.is_empty(); let mut misc = Misc::new(); @@ -1527,8 +1550,8 @@ impl Connection { err = portable_client::start_portable_service( portable_client::StartPara::Logon(_r.username, _r.password), ) - .err() - .map_or("".to_string(), |e| e.to_string()); + .err() + .map_or("".to_string(), |e| e.to_string()); } self.portable.elevation_requested = err.is_empty(); let mut misc = Misc::new(); @@ -1541,12 +1564,7 @@ impl Connection { _ => {} }, Some(misc::Union::AudioFormat(format)) => { - if !self.disable_audio { - // Switch to default input device - let default_sound_device = get_default_sound_input(); - if let Some(device) = default_sound_device { - set_sound_input(device); - } + if !self.disable_audio { allow_err!(self.audio_sender.send(MediaData::AudioFormat(format))); } } @@ -1559,7 +1577,7 @@ impl Connection { "--switch_uuid", uuid.to_string().as_ref(), ]) - .ok(); + .ok(); self.send_close_reason_no_retry("Closed as expected").await; self.on_close("switch sides", false).await; return false; @@ -1573,10 +1591,19 @@ impl Connection { } } Some(message::Union::VoiceCallRequest(request)) => { - // TODO + if request.is_connect { + self.voice_call_request_timestamp = Some( + NonZeroI64::new(request.req_timestamp) + .unwrap_or(NonZeroI64::new(get_time()).unwrap()), + ); + // Call cm. + self.send_to_cm(Data::VoiceCallIncoming); + } else { + self.close_voice_call().await; + } } - Some(message::Union::VoiceCallResponse(response)) => { - // TODO + Some(message::Union::VoiceCallResponse(_response)) => { + // TODO: Maybe we can do a voice call from cm directly. } _ => {} } @@ -1584,6 +1611,34 @@ impl Connection { true } + pub async fn start_voice_call(&self) { + if let Some(ts) = conn.voice_call_request_timestamp.take() { + let msg = new_voice_call_response(ts.get(), accepted); + conn.send(msg).await; + if accepted { + // Backup the default input device. + let audio_input_device = Config::get_option("audio-input"); + conn.audio_input_device_before_voice_call = Some(audio_input_device); + // Switch to default input device + let default_sound_device = get_default_sound_input(); + if let Some(device) = default_sound_device { + set_sound_input(device); + } + } + } else { + log::warn!("Possible a voice call attack."); + } + } + + pub async fn close_voice_call(&mut self) { + // Restore to the prior audio device. + if let Some(sound_input) = std::mem::replace(&mut self.audio_input_device_before_voice_call, None) { + set_sound_input(sound_input); + // Notify the connection manager. + self.send_to_cm(Data::CloseVoiceCall("Closed manually by the peer".to_owned())); + } + } + async fn update_option(&mut self, o: &OptionMessage) { log::info!("Option update: {:?}", o); if let Ok(q) = o.image_quality.enum_value() { @@ -1752,13 +1807,13 @@ impl Connection { lock_screen().await; } #[cfg(not(any(target_os = "android", target_os = "ios")))] - let data = if self.chat_unanswered { + let data = if self.chat_unanswered { ipc::Data::Disconnected } else { ipc::Data::Close }; #[cfg(any(target_os = "android", target_os = "ios"))] - let data = ipc::Data::Close; + let data = ipc::Data::Close; self.tx_to_cm.send(data).ok(); self.port_forward_socket.take(); } diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 5d6692c3b..eb83890d4 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -266,6 +266,22 @@ impl InvokeUiSession for SciterHandler { } fn switch_back(&self, _id: &str) {} + + fn on_voice_call_start(&self) { + self.call("onVoiceCallStart", &make_args!()); + } + + fn on_voice_call_stop(&self, reason: &str) { + self.call("onVoiceCallStop", &make_args!(reason)); + } + + fn on_voice_call_waiting(&self) { + self.call("onVoiceCallWaiting", &make_args!()); + } + + fn on_voice_call_incoming(&self) { + self.call("onVoiceCallIncoming", &make_args!()); + } } pub struct SciterSession(Session); diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 2f6827523..a740b373e 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -705,6 +705,10 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { fn clipboard(&self, content: String); fn cancel_msgbox(&self, tag: &str); fn switch_back(&self, id: &str); + fn on_voice_call_start(&self); + fn on_voice_call_stop(&self, reason: &str); + fn on_voice_call_waiting(&self); + fn on_voice_call_incoming(&self); } impl Deref for Session { From 11c60088111ba9d9312fd974896afee688a3a722 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 6 Feb 2023 11:53:37 +0800 Subject: [PATCH 1751/2015] fix: rust conn build --- src/server/connection.rs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/server/connection.rs b/src/server/connection.rs index 1007c71ca..87b3f74ea 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -398,7 +398,9 @@ impl Connection { conn.send(msg).await; } ipc::Data::VoiceCallResponse(accepted) => { - conn.start_voice_call().await; + if accepted { + conn.start_voice_call().await; + } } ipc::Data::CloseVoiceCall(_reason) => { conn.close_voice_call().await; @@ -1611,19 +1613,17 @@ impl Connection { true } - pub async fn start_voice_call(&self) { - if let Some(ts) = conn.voice_call_request_timestamp.take() { + pub async fn start_voice_call(&mut self) { + if let Some(ts) = self.voice_call_request_timestamp.take() { let msg = new_voice_call_response(ts.get(), accepted); conn.send(msg).await; - if accepted { - // Backup the default input device. - let audio_input_device = Config::get_option("audio-input"); - conn.audio_input_device_before_voice_call = Some(audio_input_device); - // Switch to default input device - let default_sound_device = get_default_sound_input(); - if let Some(device) = default_sound_device { - set_sound_input(device); - } + // Backup the default input device. + let audio_input_device = Config::get_option("audio-input"); + self.audio_input_device_before_voice_call = Some(audio_input_device); + // Switch to default input device + let default_sound_device = get_default_sound_input(); + if let Some(device) = default_sound_device { + set_sound_input(device); } } else { log::warn!("Possible a voice call attack."); From a601e3b241eddc3f5a104fee89a8518be79ca34a Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 6 Feb 2023 12:10:15 +0800 Subject: [PATCH 1752/2015] fix: compile --- src/flutter_ffi.rs | 1 + src/server/connection.rs | 24 ++++++++++++------------ 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index e28332943..588733c37 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1288,6 +1288,7 @@ pub fn main_start_ipc_url_server() { /// Send a url scheme throught the ipc. /// /// * macOS only +#[allow(unused_variables)] pub fn send_url_scheme(url: String) { #[cfg(target_os = "macos")] thread::spawn(move || crate::ui::macos::handle_url_scheme(url)); diff --git a/src/server/connection.rs b/src/server/connection.rs index 87b3f74ea..c4c9ec168 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -398,9 +398,7 @@ impl Connection { conn.send(msg).await; } ipc::Data::VoiceCallResponse(accepted) => { - if accepted { - conn.start_voice_call().await; - } + conn.handle_voice_call(accepted).await; } ipc::Data::CloseVoiceCall(_reason) => { conn.close_voice_call().await; @@ -1613,17 +1611,19 @@ impl Connection { true } - pub async fn start_voice_call(&mut self) { + pub async fn handle_voice_call(&mut self, accepted: bool) { if let Some(ts) = self.voice_call_request_timestamp.take() { let msg = new_voice_call_response(ts.get(), accepted); - conn.send(msg).await; - // Backup the default input device. - let audio_input_device = Config::get_option("audio-input"); - self.audio_input_device_before_voice_call = Some(audio_input_device); - // Switch to default input device - let default_sound_device = get_default_sound_input(); - if let Some(device) = default_sound_device { - set_sound_input(device); + self.send(msg).await; + if accepted { + // Backup the default input device. + let audio_input_device = Config::get_option("audio-input"); + self.audio_input_device_before_voice_call = Some(audio_input_device); + // Switch to default input device + let default_sound_device = get_default_sound_input(); + if let Some(device) = default_sound_device { + set_sound_input(device); + } } } else { log::warn!("Possible a voice call attack."); From 850c4bcbbf5bfbf152ccda3e876330e5f7286f7e Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 6 Feb 2023 12:14:20 +0800 Subject: [PATCH 1753/2015] opt: uniform name --- src/client/io_loop.rs | 3 ++- src/flutter.rs | 4 ++-- src/ui/remote.rs | 4 ++-- src/ui_session_interface.rs | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 8f2b45321..e34df30b4 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -749,6 +749,7 @@ impl Remote { .unwrap_or(NonZeroI64::new(get_time()).unwrap()), ); allow_err!(peer.send(&msg).await); + self.handler.on_voice_call_waiting(); } Data::CloseVoiceCall => { self.stop_voice_call(); @@ -1262,7 +1263,7 @@ impl Remote { self.stop_voice_call_sender = self.start_voice_call(); } else { // The peer refused the voice call. - self.handler.on_voice_call_stop("Refused"); + self.handler.on_voice_call_closed("Refused"); } } } diff --git a/src/flutter.rs b/src/flutter.rs index 7062d85df..f8d8569ba 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -399,8 +399,8 @@ impl InvokeUiSession for FlutterHandler { self.push_event("on_voice_call_start", [].into()); } - fn on_voice_call_stop(&self, reason: &str) { - self.push_event("on_voice_call_stop", [("reason", reason)].into()) + fn on_voice_call_closed(&self, reason: &str) { + self.push_event("on_voice_call_closed", [("reason", reason)].into()) } fn on_voice_call_waiting(&self) { diff --git a/src/ui/remote.rs b/src/ui/remote.rs index eb83890d4..9888e5831 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -271,8 +271,8 @@ impl InvokeUiSession for SciterHandler { self.call("onVoiceCallStart", &make_args!()); } - fn on_voice_call_stop(&self, reason: &str) { - self.call("onVoiceCallStop", &make_args!(reason)); + fn on_voice_call_closed(&self, reason: &str) { + self.call("onVoiceCallClosed", &make_args!(reason)); } fn on_voice_call_waiting(&self) { diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index a740b373e..4b47608f9 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -706,7 +706,7 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { fn cancel_msgbox(&self, tag: &str); fn switch_back(&self, id: &str); fn on_voice_call_start(&self); - fn on_voice_call_stop(&self, reason: &str); + fn on_voice_call_closed(&self, reason: &str); fn on_voice_call_waiting(&self); fn on_voice_call_incoming(&self); } From 040396b3f8421075adce6762010bd74b964d407f Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 6 Feb 2023 12:53:57 +0800 Subject: [PATCH 1754/2015] feat: cm interface --- src/flutter.rs | 12 ++++++++++++ src/ipc.rs | 1 + src/server/connection.rs | 1 + src/ui/cm.rs | 12 ++++++++++++ src/ui_cm_interface.rs | 27 +++++++++++++++++++++++++++ 5 files changed, 53 insertions(+) diff --git a/src/flutter.rs b/src/flutter.rs index f8d8569ba..e83beb03b 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -537,6 +537,18 @@ pub mod connection_manager { fn show_elevation(&self, show: bool) { self.push_event("show_elevation", vec![("show", &show.to_string())]); } + + fn voice_call_started(&self, id: i32) { + self.push_event("voice_call_started", vec![("show", &id.to_string())]); + } + + fn voice_call_incoming(&self, id: i32) { + self.push_event("voice_call_incoming", vec![("id", &id.to_string())]); + } + + fn voice_call_closed(&self, id: i32, reason: &str) { + self.push_event("voice_call_closed", vec![("id", &id.to_string()), ("reason", &reason.to_string())]); + } } impl FlutterHandler { diff --git a/src/ipc.rs b/src/ipc.rs index 18f618847..0ede560fc 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -212,6 +212,7 @@ pub enum Data { SwitchSidesBack, UrlLink(String), VoiceCallIncoming, + StartVoiceCall, VoiceCallResponse(bool), CloseVoiceCall(String), } diff --git a/src/server/connection.rs b/src/server/connection.rs index c4c9ec168..da0126213 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1624,6 +1624,7 @@ impl Connection { if let Some(device) = default_sound_device { set_sound_input(device); } + self.send_to_cm(Data::StartVoiceCall); } } else { log::warn!("Possible a voice call attack."); diff --git a/src/ui/cm.rs b/src/ui/cm.rs index 2bd8824db..dc941c3d0 100644 --- a/src/ui/cm.rs +++ b/src/ui/cm.rs @@ -55,6 +55,18 @@ impl InvokeUiCM for SciterHandler { fn show_elevation(&self, show: bool) { self.call("showElevation", &make_args!(show)); } + + fn voice_call_started(&self, id: i32) { + self.call("voice_call_started", &make_args!(id)); + } + + fn voice_call_incoming(&self, id: i32) { + self.call("voice_call_incoming", &make_args!(id)); + } + + fn voice_call_closed(&self, id: i32, reason: &str) { + self.call("voice_call_incoming", &make_args!(id, reason)); + } } impl SciterHandler { diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index 5d451e4d4..1120a1731 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -88,6 +88,12 @@ pub trait InvokeUiCM: Send + Clone + 'static + Sized { fn change_language(&self); fn show_elevation(&self, show: bool); + + fn voice_call_started(&self, id: i32); + + fn voice_call_incoming(&self, id: i32); + + fn voice_call_closed(&self, id: i32, reason: &str); } impl Deref for ConnectionManager { @@ -180,6 +186,18 @@ impl ConnectionManager { fn show_elevation(&self, show: bool) { self.ui_handler.show_elevation(show); } + + fn voice_call_started(&self, id: i32) { + self.ui_handler.voice_call_started(id); + } + + fn voice_call_incoming(&self, id: i32) { + self.ui_handler.voice_call_incoming(id); + } + + fn voice_call_closed(&self, id: i32, reason: &str) { + self.ui_handler.voice_call_closed(id, reason); + } } #[inline] @@ -389,6 +407,15 @@ impl IpcTaskRunner { Data::DataPortableService(ipc::DataPortableService::CmShowElevation(show)) => { self.cm.show_elevation(show); } + Data::StartVoiceCall => { + self.cm.voice_call_started(self.conn_id); + } + Data::VoiceCallIncoming => { + self.cm.voice_call_incoming(self.conn_id); + } + Data::CloseVoiceCall(reason) => { + self.cm.voice_call_closed(self.conn_id, reason.as_str()); + } _ => { } From ea391542fcf607619631c63505df28fd84ec7c67 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 6 Feb 2023 15:36:36 +0800 Subject: [PATCH 1755/2015] opt: rename to on_voice_call_started --- src/client/io_loop.rs | 2 +- src/flutter.rs | 4 ++-- src/ui/remote.rs | 2 +- src/ui_session_interface.rs | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index e34df30b4..d49227864 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -1259,7 +1259,7 @@ impl Remote { } else { if response.accepted { // The peer accepts the voice call. - self.handler.on_voice_call_start(); + self.handler.on_voice_call_started(); self.stop_voice_call_sender = self.start_voice_call(); } else { // The peer refused the voice call. diff --git a/src/flutter.rs b/src/flutter.rs index e83beb03b..4249e4d94 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -395,8 +395,8 @@ impl InvokeUiSession for FlutterHandler { self.push_event("switch_back", [("peer_id", peer_id)].into()); } - fn on_voice_call_start(&self) { - self.push_event("on_voice_call_start", [].into()); + fn on_voice_call_started(&self) { + self.push_event("on_voice_call_started", [].into()); } fn on_voice_call_closed(&self, reason: &str) { diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 9888e5831..999b409e0 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -267,7 +267,7 @@ impl InvokeUiSession for SciterHandler { fn switch_back(&self, _id: &str) {} - fn on_voice_call_start(&self) { + fn on_voice_call_started(&self) { self.call("onVoiceCallStart", &make_args!()); } diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 4b47608f9..f63bbd081 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -705,7 +705,7 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { fn clipboard(&self, content: String); fn cancel_msgbox(&self, tag: &str); fn switch_back(&self, id: &str); - fn on_voice_call_start(&self); + fn on_voice_call_started(&self); fn on_voice_call_closed(&self, reason: &str); fn on_voice_call_waiting(&self); fn on_voice_call_incoming(&self); From 5e21a81a5cc6aca17ba9a4726a626b14b06a67cc Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 6 Feb 2023 20:10:39 +0800 Subject: [PATCH 1756/2015] wip: implement flutter ui --- Cargo.lock | 5 +-- Cargo.toml | 2 +- flutter/assets/voice_call.svg | 1 + flutter/assets/voice_call_waiting.svg | 1 + .../lib/desktop/widgets/remote_menubar.dart | 32 ++++++++++++++++++- flutter/lib/models/chat_model.dart | 32 +++++++++++++++++++ flutter/lib/models/model.dart | 15 +++++++++ 7 files changed, 84 insertions(+), 4 deletions(-) create mode 100644 flutter/assets/voice_call.svg create mode 100644 flutter/assets/voice_call_waiting.svg diff --git a/Cargo.lock b/Cargo.lock index e15641363..52fcc76cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1334,8 +1334,9 @@ checksum = "f578e8e2c440e7297e008bb5486a3a8a194775224bbc23729b0dbdfaeebf162e" [[package]] name = "default-net" -version = "0.11.0" -source = "git+https://github.com/Kingtous/default-net#bdaad8dd5b08efcba303e71729d3d0b1d5ccdb25" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14e349ed1e06fb344a7dd8b5a676375cf671b31e8900075dd2be816efc063a63" dependencies = [ "libc", "memalloc", diff --git a/Cargo.toml b/Cargo.toml index 936b9e349..b315024e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,7 +59,7 @@ base64 = "0.13" sysinfo = "0.24" num_cpus = "1.13" bytes = { version = "1.2", features = ["serde"] } -default-net = { git = "https://github.com/Kingtous/default-net" } +default-net = "0.12.0" wol-rs = "0.9.1" flutter_rust_bridge = { version = "1.61.1", optional = true } errno = "0.2.8" diff --git a/flutter/assets/voice_call.svg b/flutter/assets/voice_call.svg new file mode 100644 index 000000000..0637b58d9 --- /dev/null +++ b/flutter/assets/voice_call.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/flutter/assets/voice_call_waiting.svg b/flutter/assets/voice_call_waiting.svg new file mode 100644 index 000000000..fd8334f92 --- /dev/null +++ b/flutter/assets/voice_call_waiting.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 0004c65fe..d06be52fa 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -426,6 +426,7 @@ class _RemoteMenubarState extends State { menubarItems.add(_buildKeyboard(context)); if (!isWeb) { menubarItems.add(_buildChat(context)); + menubarItems.add(_buildVoiceCall(context)); } menubarItems.add(_buildRecording(context)); menubarItems.add(_buildClose(context)); @@ -707,6 +708,32 @@ class _RemoteMenubarState extends State { ); } + Widget _buildVoiceCall(BuildContext context) { + return Obx( + () { + switch (widget.ffi.chatModel.voiceCallStatus.value) { + case VoiceCallStatus.waitingForResponse: + return SvgPicture.asset( + "assets/voice_call_waiting.svg", + color: _MenubarTheme.commonColor, + width: Theme.of(context).iconTheme.size ?? 24.0, + height: Theme.of(context).iconTheme.size ?? 24.0, + ); + break; + case VoiceCallStatus.connected: + return SvgPicture.asset( + "assets/voice_call.svg", + color: Colors.red, + width: Theme.of(context).iconTheme.size ?? 24.0, + height: Theme.of(context).iconTheme.size ?? 24.0, + ); + default: + return const Offstage(); + } + }, + ); + } + List> _getChatMenu(BuildContext context) { final List> chatMenu = []; const EdgeInsets padding = EdgeInsets.only(left: 14.0, right: 5.0); @@ -728,7 +755,10 @@ class _RemoteMenubarState extends State { translate('Voice call'), style: style, ), - proc: () {}, + proc: () { + // Request a voice call. + bind.sessionRequestVoiceCall(id: widget.id); + }, padding: padding, dismissOnClicked: true, ), diff --git a/flutter/lib/models/chat_model.dart b/flutter/lib/models/chat_model.dart index 18a0be279..61602c5b4 100644 --- a/flutter/lib/models/chat_model.dart +++ b/flutter/lib/models/chat_model.dart @@ -2,6 +2,7 @@ import 'package:dash_chat_2/dash_chat_2.dart'; import 'package:draggable_float_widget/draggable_float_widget.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/models/platform_model.dart'; +import 'package:get/get.dart'; import 'package:window_manager/window_manager.dart'; import '../consts.dart'; @@ -33,8 +34,13 @@ class ChatModel with ChangeNotifier { OverlayState? _overlayState; OverlayEntry? chatIconOverlayEntry; OverlayEntry? chatWindowOverlayEntry; + bool isConnManager = false; + final Rx _voiceCallStatus = Rx(VoiceCallStatus.notStarted); + + Rx get voiceCallStatus => _voiceCallStatus; + final ChatUser me = ChatUser( id: "", firstName: "Me", @@ -292,4 +298,30 @@ class ChatModel with ChangeNotifier { resetClientMode() { _messages[clientModeID]?.clear(); } + + void onVoiceCallWaiting() { + _voiceCallStatus.value = VoiceCallStatus.waitingForResponse; + } + + void onVoiceCallStarted() { + _voiceCallStatus.value = VoiceCallStatus.connected; + } + + void onVoiceCallClosed(String reason) { + _voiceCallStatus.value = VoiceCallStatus.notStarted; + } + + void onVoiceCallIncoming() { + if (isConnManager) { + _voiceCallStatus.value = VoiceCallStatus.incoming; + } + } } + +enum VoiceCallStatus { + notStarted, + waitingForResponse, + connected, + // Connection manager only. + incoming +} \ No newline at end of file diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index daf7bfe34..2a4c68839 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -203,6 +203,21 @@ class FfiModel with ChangeNotifier { } else if (name == "on_url_scheme_received") { final url = evt['url'].toString(); parseRustdeskUri(url); + } else if (name == "on_voice_call_waiting") { + // Waiting for the response from the peer. + parent.target?.chatModel.onVoiceCallWaiting(); + } else if (name == "on_voice_call_started") { + // Voice call is connected. + parent.target?.chatModel.onVoiceCallStarted(); + } else if (name == "on_voice_call_closed") { + // Voice call is closed with reason. + final reason = evt['reason'].toString(); + parent.target?.chatModel.onVoiceCallClosed(reason); + } else if (name == "on_voice_call_incoming") { + // Voice call is requested by the peer. + parent.target?.chatModel.onVoiceCallIncoming(); + } else { + debugPrint("Unknown event name: $name"); } }; } From 2943d2d0ccaad9ffe580b98979af95cf44100fb5 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 7 Feb 2023 16:11:55 +0800 Subject: [PATCH 1757/2015] feat: cm interface --- flutter/lib/desktop/pages/server_page.dart | 42 +++++++++++++++++- .../lib/desktop/widgets/remote_menubar.dart | 32 +++++++++----- flutter/lib/models/chat_model.dart | 4 ++ flutter/lib/models/model.dart | 2 + flutter/lib/models/server_model.dart | 18 ++++++++ src/client/io_loop.rs | 1 + src/flutter.rs | 13 ++---- src/flutter_ffi.rs | 8 ++++ src/server/connection.rs | 2 +- src/ui/cm.rs | 19 ++++---- src/ui_cm_interface.rs | 44 +++++++++++++++---- src/ui_session_interface.rs | 2 +- 12 files changed, 143 insertions(+), 44 deletions(-) diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index 521413647..b2f70cdd5 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -521,6 +521,38 @@ class _CmControlPanel extends StatelessWidget { return Column( mainAxisAlignment: MainAxisAlignment.end, children: [ + Offstage( + offstage: !client.inVoiceCall, + child: buildButton(context, + color: Colors.purple, + onClick: () => closeVoiceCall(), + icon: Icon(Icons.reply, color: Colors.white), + text: "Stop voice call", + textColor: Colors.white), + ), + Offstage( + offstage: !client.incomingVoiceCall, + child: Row( + children: [ + Expanded( + child: buildButton(context, + color: MyTheme.accent, + onClick: () => handleVoiceCall(true), + icon: Icon(Icons.phone, color: Colors.white), + text: "Accept", + textColor: Colors.white), + ), + Expanded( + child: buildButton(context, + color: Colors.red, + onClick: () => handleVoiceCall(false), + icon: Icon(Icons.phone, color: Colors.white), + text: "Deny", + textColor: Colors.white), + ) + ], + ), + ), Offstage( offstage: !client.fromSwitch, child: buildButton(context, @@ -626,7 +658,7 @@ class _CmControlPanel extends StatelessWidget { .marginSymmetric(horizontal: showElevation ? 0 : bigMargin); } - buildButton( + Widget buildButton( BuildContext context, { required Color? color, required Function() onClick, @@ -692,6 +724,14 @@ class _CmControlPanel extends StatelessWidget { void handleSwitchBack(BuildContext context) { bind.cmSwitchBack(connId: client.id); } + + void handleVoiceCall(bool accept) { + bind.cmHandleIncomingVoiceCall(id: client.id, accept: accept); + } + + void closeVoiceCall() { + bind.cmCloseVoiceCall(id: client.id); + } } void checkClickTime(int id, Function() callback) async { diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index d06be52fa..653ff37b1 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -713,19 +713,27 @@ class _RemoteMenubarState extends State { () { switch (widget.ffi.chatModel.voiceCallStatus.value) { case VoiceCallStatus.waitingForResponse: - return SvgPicture.asset( - "assets/voice_call_waiting.svg", - color: _MenubarTheme.commonColor, - width: Theme.of(context).iconTheme.size ?? 24.0, - height: Theme.of(context).iconTheme.size ?? 24.0, - ); - break; + return IconButton( + onPressed: () { + widget.ffi.chatModel.closeVoiceCall(widget.id); + }, + icon: SvgPicture.asset( + "assets/voice_call_waiting.svg", + color: Colors.red, + width: Theme.of(context).iconTheme.size ?? 24.0, + height: Theme.of(context).iconTheme.size ?? 24.0, + )); case VoiceCallStatus.connected: - return SvgPicture.asset( - "assets/voice_call.svg", - color: Colors.red, - width: Theme.of(context).iconTheme.size ?? 24.0, - height: Theme.of(context).iconTheme.size ?? 24.0, + return IconButton( + onPressed: () { + widget.ffi.chatModel.closeVoiceCall(widget.id); + }, + icon: SvgPicture.asset( + "assets/voice_call.svg", + color: Colors.red, + width: Theme.of(context).iconTheme.size ?? 24.0, + height: Theme.of(context).iconTheme.size ?? 24.0, + ), ); default: return const Offstage(); diff --git a/flutter/lib/models/chat_model.dart b/flutter/lib/models/chat_model.dart index 61602c5b4..14af96570 100644 --- a/flutter/lib/models/chat_model.dart +++ b/flutter/lib/models/chat_model.dart @@ -316,6 +316,10 @@ class ChatModel with ChangeNotifier { _voiceCallStatus.value = VoiceCallStatus.incoming; } } + + void closeVoiceCall(String id) { + bind.sessionCloseVoiceCall(id: id); + } } enum VoiceCallStatus { diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 2a4c68839..a2fe205af 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -216,6 +216,8 @@ class FfiModel with ChangeNotifier { } else if (name == "on_voice_call_incoming") { // Voice call is requested by the peer. parent.target?.chatModel.onVoiceCallIncoming(); + } else if (name == "update_voice_call_state") { + parent.target?.serverModel.updateVoiceCallState(evt); } else { debugPrint("Unknown event name: $name"); } diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index 56dca4cdf..6cd905c37 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -579,6 +579,20 @@ class ServerModel with ChangeNotifier { notifyListeners(); } } + + void updateVoiceCallState(Map evt) { + try { + final client = Client.fromJson(jsonDecode(evt["client"])); + final index = _clients.indexWhere((element) => element.id == client.id); + if (index != -1) { + _clients[index].inVoiceCall = evt['in_voice_call']; + _clients[index].incomingVoiceCall = evt['incoming_voice_call']; + notifyListeners(); + } + } catch (e) { + debugPrint("updateVoiceCallState failed: $e"); + } + } } enum ClientType { @@ -602,6 +616,8 @@ class Client { bool recording = false; bool disconnected = false; bool fromSwitch = false; + bool inVoiceCall = false; + bool incomingVoiceCall = false; RxBool hasUnreadChatMessage = false.obs; @@ -623,6 +639,8 @@ class Client { recording = json['recording']; disconnected = json['disconnected']; fromSwitch = json['from_switch']; + inVoiceCall = json['in_voice_call']; + incomingVoiceCall = json['incoming_voice_call']; } Map toJson() { diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index d49227864..aa51df378 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -754,6 +754,7 @@ impl Remote { Data::CloseVoiceCall => { self.stop_voice_call(); let msg = new_voice_call_request(false); + self.handler.on_voice_call_closed("Closed manually by the peer"); allow_err!(peer.send(&msg).await); } _ => {} diff --git a/src/flutter.rs b/src/flutter.rs index 4249e4d94..a27a9d4e1 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -538,16 +538,9 @@ pub mod connection_manager { self.push_event("show_elevation", vec![("show", &show.to_string())]); } - fn voice_call_started(&self, id: i32) { - self.push_event("voice_call_started", vec![("show", &id.to_string())]); - } - - fn voice_call_incoming(&self, id: i32) { - self.push_event("voice_call_incoming", vec![("id", &id.to_string())]); - } - - fn voice_call_closed(&self, id: i32, reason: &str) { - self.push_event("voice_call_closed", vec![("id", &id.to_string()), ("reason", &reason.to_string())]); + fn update_voice_call_state(&self, client: &crate::ui_cm_interface::Client) { + let client_json = serde_json::to_string(&client).unwrap_or("".into()); + self.push_event("update_voice_call_state", vec![("client", &client_json)]); } } diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 588733c37..cfca0e082 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -838,6 +838,14 @@ pub fn session_close_voice_call(id: String) { } } +pub fn cm_handle_incoming_voice_call(id: i32, accept: bool) { + crate::ui_cm_interface::handle_incoming_voice_call(id, accept); +} + +pub fn cm_close_voice_call(id: i32) { + crate::ui_cm_interface::close_voice_call(id); +} + pub fn main_get_last_remote_id() -> String { LocalConfig::get_remote_id() } diff --git a/src/server/connection.rs b/src/server/connection.rs index da0126213..1e88b9b05 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1636,7 +1636,7 @@ impl Connection { if let Some(sound_input) = std::mem::replace(&mut self.audio_input_device_before_voice_call, None) { set_sound_input(sound_input); // Notify the connection manager. - self.send_to_cm(Data::CloseVoiceCall("Closed manually by the peer".to_owned())); + self.send_to_cm(Data::CloseVoiceCall("".to_owned())); } } diff --git a/src/ui/cm.rs b/src/ui/cm.rs index dc941c3d0..cce553154 100644 --- a/src/ui/cm.rs +++ b/src/ui/cm.rs @@ -56,16 +56,15 @@ impl InvokeUiCM for SciterHandler { self.call("showElevation", &make_args!(show)); } - fn voice_call_started(&self, id: i32) { - self.call("voice_call_started", &make_args!(id)); - } - - fn voice_call_incoming(&self, id: i32) { - self.call("voice_call_incoming", &make_args!(id)); - } - - fn voice_call_closed(&self, id: i32, reason: &str) { - self.call("voice_call_incoming", &make_args!(id, reason)); + fn update_voice_call_state(&self, client: &crate::ui_cm_interface::Client) { + self.call( + "updateVoiceCallState", + &make_args!( + client.id, + client.in_voice_call, + client.incoming_voice_call + ), + ); } } diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index 1120a1731..ccddab0ee 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -49,6 +49,8 @@ pub struct Client { pub restart: bool, pub recording: bool, pub from_switch: bool, + pub in_voice_call: bool, + pub incoming_voice_call: bool, #[serde(skip)] tx: UnboundedSender, } @@ -89,11 +91,7 @@ pub trait InvokeUiCM: Send + Clone + 'static + Sized { fn show_elevation(&self, show: bool); - fn voice_call_started(&self, id: i32); - - fn voice_call_incoming(&self, id: i32); - - fn voice_call_closed(&self, id: i32, reason: &str); + fn update_voice_call_state(&self, client: &Client); } impl Deref for ConnectionManager { @@ -144,6 +142,8 @@ impl ConnectionManager { recording, from_switch, tx, + in_voice_call: false, + incoming_voice_call: false }; CLIENTS .write() @@ -188,15 +188,27 @@ impl ConnectionManager { } fn voice_call_started(&self, id: i32) { - self.ui_handler.voice_call_started(id); + if let Some(client) = CLIENTS.write().unwrap().get_mut(&id) { + client.incoming_voice_call = false; + client.in_voice_call = true; + self.ui_handler.update_voice_call_state(client); + } } fn voice_call_incoming(&self, id: i32) { - self.ui_handler.voice_call_incoming(id); + if let Some(client) = CLIENTS.write().unwrap().get_mut(&id) { + client.incoming_voice_call = true; + client.in_voice_call = false; + self.ui_handler.update_voice_call_state(client); + } } - fn voice_call_closed(&self, id: i32, reason: &str) { - self.ui_handler.voice_call_closed(id, reason); + fn voice_call_closed(&self, id: i32, _reason: &str) { + if let Some(client) = CLIENTS.write().unwrap().get_mut(&id) { + client.incoming_voice_call = false; + client.in_voice_call = false; + self.ui_handler.update_voice_call_state(client); + } } } @@ -832,3 +844,17 @@ pub fn elevate_portable(_id: i32) { } } } + +#[inline] +pub fn handle_incoming_voice_call(id: i32, accept: bool) { + if let Some(client) = CLIENTS.write().unwrap().get_mut(&id) { + allow_err!(client.tx.send(Data::VoiceCallResponse(accept))); + }; +} + +#[inline] +pub fn close_voice_call(id: i32) { + if let Some(client) = CLIENTS.write().unwrap().get_mut(&id) { + allow_err!(client.tx.send(Data::CloseVoiceCall("".to_owned()))); + }; +} \ No newline at end of file diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index f63bbd081..cd0bdcde2 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -661,7 +661,7 @@ impl Session { pub fn request_voice_call(&self) { self.send(Data::NewVoiceCall); } - + pub fn close_voice_call(&self) { self.send(Data::CloseVoiceCall); } From bd07f60a1109aff1ef0aa87b8621b2d80ee326b6 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 7 Feb 2023 16:41:33 +0800 Subject: [PATCH 1758/2015] disable blank issue, use better format --- .github/ISSUE_TEMPLATE/bug_report.md | 32 -------------- .github/ISSUE_TEMPLATE/bug_report.yaml | 49 +++++++++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 1 - .github/ISSUE_TEMPLATE/feature_request.md | 10 ----- .github/ISSUE_TEMPLATE/feature_request.yaml | 25 +++++++++++ 5 files changed, 74 insertions(+), 43 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yaml delete mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yaml diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 5ba29c8b6..000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -name: Bug Report -about: Report a bug (English only, Please). -title: "" -labels: bug -assignees: '' - ---- - - - -**Describe the bug you encountered:** - -... - -**What did you expect to happen instead?** - -... - - -**How did you install `RustDesk`?** - - - ---- - -**RustDesk version and environment** - - diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml new file mode 100644 index 000000000..87fc6a5f5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -0,0 +1,49 @@ +name: Bug Report +description: Create a bug report to help us improve +title: "[Bug] " +body: + - type: textarea + id: desc + attributes: + label: Bug Description + description: A clear and concise description of what the bug is + validations: + required: true + - type: textarea + id: reproduce + attributes: + label: How to Reproduce + description: What steps can we take to reproduce this behavior? + validations: + required: true + - type: textarea + id: expected + attributes: + label: Expected Behavior + description: A clear and concise description of what you expected to happen + validations: + required: true + - type: input + id: os + attributes: + label: Operating System + description: What OS are you seeing this bug on? local side / remote side. + validations: + required: true + - type: input + id: version + attributes: + label: RustDesk Version(s) + description: What version(s) of RustDesk do you see this bug on? local side / remote side. + validations: + required: true + - type: textarea + id: screenshots + attributes: + label: Screenshots + description: If applicable, please add screenshots to help explain your problem + - type: textarea + id: context + attributes: + label: Additional Context + description: Add any additonal context about the problem here diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 01de3b330..7b43e397b 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,4 +1,3 @@ -blank_issues_enabled: true contact_links: - name: Ask a question url: https://github.com/rustdesk/rustdesk/discussions/category_choices diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 0d21f017d..000000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -name: Feature Request -about: Suggest an idea for this project ((English only, Please). -title: '' -labels: enhancement -assignees: '' - ---- - - diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml new file mode 100644 index 000000000..01f6c6aca --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -0,0 +1,25 @@ +name: Feature Request +description: Suggest an idea for RustDesk +title: "[FR] " +body: + - type: textarea + id: desc + attributes: + label: Description + description: Describe your suggested feature and the main use cases + validations: + required: true + + - type: textarea + id: users + attributes: + label: Impact + description: What types of users can benefit from using the suggested feature? + validations: + required: true + + - type: textarea + id: context + attributes: + label: Additional Context + description: Add any additonal context about the feature here From fc933ad7b4c8e88f035aea44694ff53721895a33 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 7 Feb 2023 16:47:19 +0800 Subject: [PATCH 1759/2015] fix: voice call 1 --- flutter/lib/models/server_model.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index 6cd905c37..eec424bfe 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -585,8 +585,8 @@ class ServerModel with ChangeNotifier { final client = Client.fromJson(jsonDecode(evt["client"])); final index = _clients.indexWhere((element) => element.id == client.id); if (index != -1) { - _clients[index].inVoiceCall = evt['in_voice_call']; - _clients[index].incomingVoiceCall = evt['incoming_voice_call']; + _clients[index].inVoiceCall = client.inVoiceCall; + _clients[index].incomingVoiceCall = client.incomingVoiceCall; notifyListeners(); } } catch (e) { From cd6cdbff8f9c9fb38d7ad9631634ba2b9bea328d Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 7 Feb 2023 16:53:46 +0800 Subject: [PATCH 1760/2015] fix: close notify --- src/client/io_loop.rs | 2 +- src/server/connection.rs | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index aa51df378..234f4f842 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -1264,7 +1264,7 @@ impl Remote { self.stop_voice_call_sender = self.start_voice_call(); } else { // The peer refused the voice call. - self.handler.on_voice_call_closed("Refused"); + self.handler.on_voice_call_closed(""); } } } diff --git a/src/server/connection.rs b/src/server/connection.rs index 1e88b9b05..7a16df811 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1614,7 +1614,6 @@ impl Connection { pub async fn handle_voice_call(&mut self, accepted: bool) { if let Some(ts) = self.voice_call_request_timestamp.take() { let msg = new_voice_call_response(ts.get(), accepted); - self.send(msg).await; if accepted { // Backup the default input device. let audio_input_device = Config::get_option("audio-input"); @@ -1625,7 +1624,10 @@ impl Connection { set_sound_input(device); } self.send_to_cm(Data::StartVoiceCall); + } else { + self.send_to_cm(Data::CloseVoiceCall("".to_owned())); } + self.send(msg).await; } else { log::warn!("Possible a voice call attack."); } From b82df0913731e60e87be25149c238ba5bb0c3e67 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 7 Feb 2023 17:00:36 +0800 Subject: [PATCH 1761/2015] new SECURITY.md --- docs/SECURITY.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 docs/SECURITY.md diff --git a/docs/SECURITY.md b/docs/SECURITY.md new file mode 100644 index 000000000..c595885f2 --- /dev/null +++ b/docs/SECURITY.md @@ -0,0 +1,9 @@ +# Security Policy + +## Reporting a Vulnerability + +We value security for the project very highly. We encourage all users to report any vulnerabilities they discover to us. +If you find a security vulnerability in the RustDesk project, please report it responsibly by sending an email to info@rustdesk.com. + +At this juncture, we don't have a bug bounty program. We are a small team trying to solve a big problem. We urge you to report any vulnerabilities responsibly +so that we can continue building a secure application for the entire community. From 66aaf243cf7654c40628187a0249ac77b9452c7a Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 7 Feb 2023 17:09:36 +0800 Subject: [PATCH 1762/2015] opt: notify cm --- flutter/lib/desktop/pages/server_page.dart | 7 ++++--- flutter/lib/models/server_model.dart | 6 ++++++ src/client/io_loop.rs | 2 +- src/server/connection.rs | 7 ++++--- 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index b2f70cdd5..a253b9aa2 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -524,7 +524,7 @@ class _CmControlPanel extends StatelessWidget { Offstage( offstage: !client.inVoiceCall, child: buildButton(context, - color: Colors.purple, + color: Colors.red, onClick: () => closeVoiceCall(), icon: Icon(Icons.reply, color: Colors.white), text: "Stop voice call", @@ -538,7 +538,7 @@ class _CmControlPanel extends StatelessWidget { child: buildButton(context, color: MyTheme.accent, onClick: () => handleVoiceCall(true), - icon: Icon(Icons.phone, color: Colors.white), + icon: Icon(Icons.phone_enabled, color: Colors.white), text: "Accept", textColor: Colors.white), ), @@ -546,7 +546,8 @@ class _CmControlPanel extends StatelessWidget { child: buildButton(context, color: Colors.red, onClick: () => handleVoiceCall(false), - icon: Icon(Icons.phone, color: Colors.white), + icon: + Icon(Icons.phone_disabled_rounded, color: Colors.white), text: "Deny", textColor: Colors.white), ) diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index eec424bfe..aab12ab5d 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -587,6 +587,12 @@ class ServerModel with ChangeNotifier { if (index != -1) { _clients[index].inVoiceCall = client.inVoiceCall; _clients[index].incomingVoiceCall = client.incomingVoiceCall; + if (client.incomingVoiceCall) { + // Has incoming phone call, let's set the window on top. + Future.delayed(Duration.zero, () { + window_on_top(null); + }); + } notifyListeners(); } } catch (e) { diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 234f4f842..05eab692a 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -1259,7 +1259,7 @@ impl Remote { log::debug!("Possible encountering a voice call attack."); } else { if response.accepted { - // The peer accepts the voice call. + // The peer accepted the voice call. self.handler.on_voice_call_started(); self.stop_voice_call_sender = self.start_voice_call(); } else { diff --git a/src/server/connection.rs b/src/server/connection.rs index 7a16df811..86d837619 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1596,9 +1596,11 @@ impl Connection { NonZeroI64::new(request.req_timestamp) .unwrap_or(NonZeroI64::new(get_time()).unwrap()), ); - // Call cm. + // Notify the connection manager. self.send_to_cm(Data::VoiceCallIncoming); } else { + // Notify the connection manager. + self.send_to_cm(Data::CloseVoiceCall("".to_owned())); self.close_voice_call().await; } } @@ -1617,6 +1619,7 @@ impl Connection { if accepted { // Backup the default input device. let audio_input_device = Config::get_option("audio-input"); + log::debug!("Backup the sound input device {}", audio_input_device); self.audio_input_device_before_voice_call = Some(audio_input_device); // Switch to default input device let default_sound_device = get_default_sound_input(); @@ -1637,8 +1640,6 @@ impl Connection { // Restore to the prior audio device. if let Some(sound_input) = std::mem::replace(&mut self.audio_input_device_before_voice_call, None) { set_sound_input(sound_input); - // Notify the connection manager. - self.send_to_cm(Data::CloseVoiceCall("".to_owned())); } } From bdbb9ac2887e7af7785c3718f79e86a792058056 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 7 Feb 2023 17:15:01 +0800 Subject: [PATCH 1763/2015] opt: issues template --- .github/ISSUE_TEMPLATE/bug_report.yaml | 22 ++++++++++++---------- .github/ISSUE_TEMPLATE/task.yaml | 20 ++++++++++++++++++++ 2 files changed, 32 insertions(+), 10 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/task.yaml diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 87fc6a5f5..d3036ba24 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -1,7 +1,14 @@ name: Bug Report -description: Create a bug report to help us improve +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: @@ -23,18 +30,13 @@ body: description: A clear and concise description of what you expected to happen validations: required: true - - type: input - id: os - attributes: - label: Operating System - description: What OS are you seeing this bug on? local side / remote side. - validations: - required: true - type: input id: version attributes: - label: RustDesk Version(s) - description: What version(s) of RustDesk do you see this bug on? local side / remote side. + 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. + placeholder: | + Windows 10, 1.1.9 / osx 13.1, 1.1.8 validations: required: true - type: textarea diff --git a/.github/ISSUE_TEMPLATE/task.yaml b/.github/ISSUE_TEMPLATE/task.yaml new file mode 100644 index 000000000..a1ff080c5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/task.yaml @@ -0,0 +1,20 @@ +name: 📝 Task +description: Create a task for the team to work on +title: "[Task]: " +labels: [Task] +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 + attributes: + label: SubTasks + placeholder: | + - Sub Task 1 + - Sub Task 2 + validations: + required: false From 29b1d106aa8385b03a40ecfa7e125831a3920caf Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 7 Feb 2023 17:16:06 +0800 Subject: [PATCH 1764/2015] opt: ui and message --- flutter/lib/desktop/pages/server_page.dart | 4 ++-- src/lang/ca.rs | 8 +++----- src/lang/cn.rs | 7 +------ src/lang/cs.rs | 8 +++----- src/lang/da.rs | 6 +++--- src/lang/de.rs | 8 +++----- src/lang/eo.rs | 8 +++----- src/lang/es.rs | 9 ++++----- src/lang/fa.rs | 8 +++----- src/lang/fr.rs | 8 +++----- src/lang/gr.rs | 8 +++----- src/lang/hu.rs | 8 +++----- src/lang/id.rs | 8 +++----- src/lang/it.rs | 8 +++----- src/lang/ja.rs | 8 +++----- src/lang/ko.rs | 8 +++----- src/lang/kz.rs | 8 +++----- src/lang/pl.rs | 8 +++----- src/lang/pt_PT.rs | 8 +++----- src/lang/ptbr.rs | 8 +++----- src/lang/ro.rs | 8 +++----- src/lang/ru.rs | 12 +++++------- src/lang/sk.rs | 8 +++----- src/lang/sl.rs | 6 +++--- src/lang/sq.rs | 8 +++----- src/lang/sr.rs | 8 +++----- src/lang/sv.rs | 8 +++----- src/lang/template.rs | 8 +++----- src/lang/th.rs | 8 +++----- src/lang/tr.rs | 8 +++----- src/lang/tw.rs | 8 +++----- src/lang/ua.rs | 8 +++----- src/lang/vn.rs | 8 +++----- 33 files changed, 99 insertions(+), 161 deletions(-) diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index a253b9aa2..66a043fef 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -526,7 +526,7 @@ class _CmControlPanel extends StatelessWidget { child: buildButton(context, color: Colors.red, onClick: () => closeVoiceCall(), - icon: Icon(Icons.reply, color: Colors.white), + icon: Icon(Icons.phone_disabled_rounded, color: Colors.white), text: "Stop voice call", textColor: Colors.white), ), @@ -548,7 +548,7 @@ class _CmControlPanel extends StatelessWidget { onClick: () => handleVoiceCall(false), icon: Icon(Icons.phone_disabled_rounded, color: Colors.white), - text: "Deny", + text: "Dismiss", textColor: Colors.white), ) ], diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 4404e178d..e98c6636a 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 5a9abba9c..64c37709a 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -446,13 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", "帧率"), ("Auto", "自动"), ("Other Default Options", "其它默认选项"), - ("Guest to Host", "被控到主机"), - ("Dual way", "双向"), - ("Guest to host audio transmission", "被控到主机音频传输"), - ("Dual-way audio transmission", "双向音频传输"), ("Voice call", "语音通话"), ("Text chat", "文字聊天"), - ("Audio Transmission Mode", "音频传输模式"), - ("Refused", "已拒绝") + ("Stop voice call", "停止语音聊天"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index a2a19a37a..70a3eb6c7 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 905f4814e..ae943e1e8 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -437,9 +437,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Switch Sides", ""), ("Please confirm if you want to share your desktop?", ""), ("Closed as expected", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), ("Display", ""), ("Default View Style", ""), ("Default Scroll Style", ""), @@ -449,5 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 4028e3337..44bbafdac 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", "fps"), ("Auto", "Automatisch"), ("Other Default Options", "Weitere Standardoptionen"), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index fe3830b99..f457833f8 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index b9b31f109..220447454 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -436,7 +436,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Strong", "Fuerte"), ("Switch Sides", "Intercambiar lados"), ("Please confirm if you want to share your desktop?", "Por favor, confirma si quieres compartir tu escritorio"), - ("Closed as expected", "Cerrado como se esperaba"), + ("Closed as expected", ""), ("Display", "Pantalla"), ("Default View Style", "Estilo de vista predeterminado"), ("Default Scroll Style", "Estilo de desplazamiento predeterminado"), @@ -446,9 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", "Otras opciones predeterminadas"), - ("Closed as expected", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 0b92c6658..c206f91ff 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", "FPS"), ("Auto", "خودکار"), ("Other Default Options", "سایر گزینه های پیش فرض"), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 4965f6dab..39ee3bc7f 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", "FPS"), ("Auto", "Auto"), ("Other Default Options", "Autres options par défaut"), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/gr.rs b/src/lang/gr.rs index e40151ccf..7cb678ecc 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 0e1887e48..25562f556 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 689ae98cf..68a80e540 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 65f91ecec..9730bbc2d 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", "FPS"), ("Auto", "Auto"), ("Other Default Options", "Altre Opzioni Predefinite"), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 33fb2da05..7069c0daf 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index c874dd695..43eb552d3 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 01014bab0..49c7b9916 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 9dd005bdd..41239961a 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", "FPS"), ("Auto", "Auto"), ("Other Default Options", "Inne opcje domyślne"), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 716d3df82..e69a140c9 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index c7d0cd6ec..0887a5915 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 2d48b91b4..304353d42 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 8224cd5eb..1e6c6962a 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -435,8 +435,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Medium", "Средний"), ("Strong", "Стойкий"), ("Switch Sides", "Переключить стороны"), - ("Please confirm if you want to share your desktop?", "Подтверждаете, что хотите поделиться своим рабочим столом?"), - ("Closed as expected", "Закрыто по ожиданию"), + ("Please confirm if you want to share your desktop?", "Подтвердите, что хотите поделиться своим рабочим столом?"), + ("Closed as expected", ""), ("Display", "Отображение"), ("Default View Style", "Стиль отображения по умолчанию"), ("Default Scroll Style", "Стиль прокрутки по умолчанию"), @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", "FPS"), ("Auto", "Авто"), ("Other Default Options", "Другие параметры по умолчанию"), - ("Please confirm if you want to share your desktop?", "Подтвердите, что хотите поделиться своим рабочим столом?"), - ("Closed as expected", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 5e0330954..6f6f7a18e 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index a75da46bd..2fb74fa5d 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -446,8 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index d3964a2e9..5d4a6e1ad 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 78059645d..31a3ade8f 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index ca2257756..e30c09e44 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 4355d643a..b88618074 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index 57dfe6e43..1c75aaae7 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 49a42af4a..a9e2c1715 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 50e684258..7c49a29a2 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", "幀率"), ("Auto", "自動"), ("Other Default Options", "其它默認選項"), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index f37ed341e..92c99d90c 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 5788a7f3d..8bb1d45e9 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -446,10 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", ""), - ("Guest to Host", ""), - ("Dual way", ""), - ("Guest to host audio transmission", ""), - ("Dual way audio transmission", ""), - ("Audio Transmission Mode", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), ].iter().cloned().collect(); } From 926afc908fb00fc626a9a3012777bd73a3a5c1b2 Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Tue, 7 Feb 2023 17:20:53 +0800 Subject: [PATCH 1765/2015] Update bug_report.yaml --- .github/ISSUE_TEMPLATE/bug_report.yaml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index d3036ba24..9bf1f6153 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -2,13 +2,13 @@ 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: 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 9d0e4bdad0d81d2827d1dd0f506df2285e566791 Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Tue, 7 Feb 2023 17:22:23 +0800 Subject: [PATCH 1766/2015] Update feature_request.yaml --- .github/ISSUE_TEMPLATE/feature_request.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml index 01f6c6aca..ab4e9ae39 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yaml +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -1,4 +1,4 @@ -name: Feature Request +name: 🛠️ Feature request description: Suggest an idea for RustDesk title: "[FR] " body: From f9864c1d0f77a3a9824d53934715eeca6bb4fb47 Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Tue, 7 Feb 2023 17:23:09 +0800 Subject: [PATCH 1767/2015] Update bug_report.yaml --- .github/ISSUE_TEMPLATE/bug_report.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 9bf1f6153..16509a3be 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -1,4 +1,4 @@ -name: Bug Report +name: 🐞 Bug report description: Thanks for taking the time to fill out this bug report! Please fill the form in **English** title: "[Bug] " body: From 79ca1aa116e2d53c24bfa67f402c18e1cb4ea827 Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Tue, 7 Feb 2023 17:30:53 +0800 Subject: [PATCH 1768/2015] Update feature_request.yaml --- .github/ISSUE_TEMPLATE/feature_request.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml index ab4e9ae39..50cd6d0cf 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yaml +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -2,6 +2,14 @@ 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 4ea41b52d3066031f8ea8ac32942c7e67f36eada Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 7 Feb 2023 18:01:54 +0800 Subject: [PATCH 1769/2015] fix: execution order of listening ipc thread --- flutter/lib/main.dart | 3 +++ src/client.rs | 2 -- src/client/io_loop.rs | 2 +- src/core_main.rs | 2 -- src/flutter_ffi.rs | 4 ++++ 5 files changed, 8 insertions(+), 5 deletions(-) diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index c19adf753..b923a31e1 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -114,6 +114,9 @@ Future initEnv(String appType) async { _registerEventHandler(); // Update the system theme. updateSystemWindowTheme(); + if (appType == kAppTypeConnectionManager) { + await bind.cmStartListenIpcThread(); + } } void runMainApp(bool startService) async { diff --git a/src/client.rs b/src/client.rs index 2ea33b655..020bea1f0 100644 --- a/src/client.rs +++ b/src/client.rs @@ -773,7 +773,6 @@ impl AudioHandler { unsafe { std::slice::from_raw_parts::(buffer.as_ptr() as _, n * 4) }; self.simple.as_mut().map(|x| x.write(data_u8)); } - log::debug!("write Audio frame {} to system.", frame.timestamp); } }); } @@ -1595,7 +1594,6 @@ pub fn start_audio_thread( if let Ok(data) = audio_receiver.recv() { match data { MediaData::AudioFrame(af) => { - log::debug!("recved audio frame={}", af.timestamp); audio_handler.handle_frame(af); } MediaData::AudioFormat(f) => { diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 05eab692a..c8a0f2ca3 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -300,7 +300,7 @@ impl Remote { // check if client is closed match rx.try_recv() { Ok(_) | Err(std::sync::mpsc::TryRecvError::Disconnected) => { - log::debug!("Exit local audio service of client"); + log::debug!("Exit voice call audio service of client"); // unsubscribe CLIENT_SERVER.write().unwrap().subscribe( audio_service::NAME, diff --git a/src/core_main.rs b/src/core_main.rs index 99d0e888e..03d057eff 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -246,8 +246,6 @@ pub fn core_main() -> Option> { } else if args[0] == "--cm" { // call connection manager to establish connections // meanwhile, return true to call flutter window to show control panel - #[cfg(feature = "flutter")] - crate::flutter::connection_manager::start_listen_ipc_thread(); crate::ui_interface::start_option_status_sync(); } } diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index cfca0e082..84407cd96 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1284,6 +1284,10 @@ pub fn main_hide_docker() -> SyncReturn { SyncReturn(true) } +pub fn cm_start_listen_ipc_thread() { + crate::flutter::connection_manager::start_listen_ipc_thread(); +} + /// Start an ipc server for receiving the url scheme. /// /// * Should only be called in the main flutter window. From 795b0068d0deefa1eeb99a52c8b6cef1fd1e30d5 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 7 Feb 2023 18:17:31 +0800 Subject: [PATCH 1770/2015] opt: close voice call msg --- src/server/connection.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/server/connection.rs b/src/server/connection.rs index 86d837619..1bacad124 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1599,8 +1599,6 @@ impl Connection { // Notify the connection manager. self.send_to_cm(Data::VoiceCallIncoming); } else { - // Notify the connection manager. - self.send_to_cm(Data::CloseVoiceCall("".to_owned())); self.close_voice_call().await; } } @@ -1641,6 +1639,7 @@ impl Connection { if let Some(sound_input) = std::mem::replace(&mut self.audio_input_device_before_voice_call, None) { set_sound_input(sound_input); } + self.send_to_cm(Data::CloseVoiceCall("".to_owned())); } async fn update_option(&mut self, o: &OptionMessage) { From db8b6d618f0d6b93b69f97dcfc42bca26063b2cf Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 7 Feb 2023 19:09:22 +0800 Subject: [PATCH 1771/2015] fix: audio close status sync --- src/client/io_loop.rs | 11 +++++++++-- src/server/connection.rs | 4 ++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index c8a0f2ca3..96ddd51f0 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -1249,8 +1249,15 @@ impl Remote { self.handler .msgbox(&msgbox.msgtype, &msgbox.title, &msgbox.text, &link); } - Some(message::Union::VoiceCallRequest(_request)) => { - // TODO: maybe we will do voice call from the peer. + Some(message::Union::VoiceCallRequest(request)) => { + if request.is_connect { + // TODO: maybe we will do voice call from the peer in the future. + } else { + if let Some(sender) = self.stop_voice_call_sender.take() { + allow_err!(sender.send(())); + self.handler.on_voice_call_closed(""); + } + } } Some(message::Union::VoiceCallResponse(response)) => { let ts = std::mem::replace(&mut self.voice_call_request_timestamp, None); diff --git a/src/server/connection.rs b/src/server/connection.rs index 1bacad124..17417cf61 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -402,6 +402,9 @@ impl Connection { } ipc::Data::CloseVoiceCall(_reason) => { conn.close_voice_call().await; + // Notify the peer that we closed the voice call. + let req = new_voice_call_request(false); + conn.send(req).await; } _ => {} } @@ -1639,6 +1642,7 @@ impl Connection { if let Some(sound_input) = std::mem::replace(&mut self.audio_input_device_before_voice_call, None) { set_sound_input(sound_input); } + // Notify the connection manager that the voice call has been closed. self.send_to_cm(Data::CloseVoiceCall("".to_owned())); } From c4b1c51e9e745f32037e04c3ae17fd4a6f0799a5 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 7 Feb 2023 19:33:58 +0800 Subject: [PATCH 1772/2015] opt: more debug info --- flutter/lib/main.dart | 4 +--- src/flutter.rs | 4 +++- src/server/connection.rs | 5 +++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index b923a31e1..c61287d4f 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -114,9 +114,6 @@ Future initEnv(String appType) async { _registerEventHandler(); // Update the system theme. updateSystemWindowTheme(); - if (appType == kAppTypeConnectionManager) { - await bind.cmStartListenIpcThread(); - } } void runMainApp(bool startService) async { @@ -219,6 +216,7 @@ void runMultiWindow( void runConnectionManagerScreen(bool hide) async { await initEnv(kAppTypeConnectionManager); + await bind.cmStartListenIpcThread(); _runApp( '', const DesktopServerPage(), diff --git a/src/flutter.rs b/src/flutter.rs index a27a9d4e1..2d7d3fb86 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -549,9 +549,11 @@ pub mod connection_manager { let mut h: HashMap<&str, &str> = event.iter().cloned().collect(); assert!(h.get("name").is_none()); h.insert("name", name); - + if let Some(s) = GLOBAL_EVENT_STREAM.read().unwrap().get(super::APP_TYPE_CM) { s.add(serde_json::ser::to_string(&h).unwrap_or("".to_owned())); + } else { + println!("Push event {} failed. No {} event stream found.", name, super::APP_TYPE_CM); }; } } diff --git a/src/server/connection.rs b/src/server/connection.rs index 17417cf61..a8849b4e6 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -401,10 +401,11 @@ impl Connection { conn.handle_voice_call(accepted).await; } ipc::Data::CloseVoiceCall(_reason) => { + log::debug!("Close the voice call from the ipc."); conn.close_voice_call().await; // Notify the peer that we closed the voice call. - let req = new_voice_call_request(false); - conn.send(req).await; + let msg = new_voice_call_request(false); + conn.send(msg).await; } _ => {} } From 86b88c2927a0251dcd8cdbd90e799ced45bb5d04 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 7 Feb 2023 19:40:50 +0800 Subject: [PATCH 1773/2015] opt: open audio when needed --- src/client/io_loop.rs | 3 ++- src/server/connection.rs | 18 ++++++++++-------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 96ddd51f0..f5792bce3 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -1251,8 +1251,9 @@ impl Remote { } Some(message::Union::VoiceCallRequest(request)) => { if request.is_connect { - // TODO: maybe we will do voice call from the peer in the future. + // TODO: maybe we will do a voice call from the peer in the future. } else { + log::debug!("The remote has requested to close the voice call"); if let Some(sender) = self.stop_voice_call_sender.take() { allow_err!(sender.send(())); self.handler.on_voice_call_closed(""); diff --git a/src/server/connection.rs b/src/server/connection.rs index a8849b4e6..02888d1ea 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -106,7 +106,7 @@ pub struct Connection { // by peer enable_file_transfer: bool, // by peer - audio_sender: MediaSender, + audio_sender: Option, // audio by the remote peer/client tx_input: std_mpsc::Sender, // handle input messages @@ -184,11 +184,6 @@ impl Connection { let mut hbbs_rx = crate::hbbs_http::sync::signal_receiver(); let tx_cloned = tx.clone(); - // Start a audio thread to play the audio sent by peer. - let latency_controller = LatencyController::new(); - // No video frame will be sent here, so we need to disable latency controller, or audio check may fail. - latency_controller.lock().unwrap().set_audio_only(true); - let audio_sender = start_audio_thread(Some(latency_controller)); let mut conn = Self { inner: ConnInner { id, @@ -230,7 +225,7 @@ impl Connection { #[cfg(windows)] portable: Default::default(), from_switch: false, - audio_sender, + audio_sender: None, voice_call_request_timestamp: None, audio_input_device_before_voice_call: None, }; @@ -1569,7 +1564,14 @@ impl Connection { }, Some(misc::Union::AudioFormat(format)) => { if !self.disable_audio { - allow_err!(self.audio_sender.send(MediaData::AudioFormat(format))); + // Drop the audio sender previously. + std::mem::replace(&mut self.audio_sender, None); + // Start a audio thread to play the audio sent by peer. + let latency_controller = LatencyController::new(); + // No video frame will be sent here, so we need to disable latency controller, or audio check may fail. + latency_controller.lock().unwrap().set_audio_only(true); + self.audio_sender = Some(start_audio_thread(Some(latency_controller))); + allow_err!(self.audio_sender.unwrap().send(MediaData::AudioFormat(format))); } } #[cfg(feature = "flutter")] From 404915c97512cbb9a60d58f70ae9eb83c60c2733 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 7 Feb 2023 19:49:42 +0800 Subject: [PATCH 1774/2015] fix: compile --- src/server/connection.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/server/connection.rs b/src/server/connection.rs index 02888d1ea..9ce53c960 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1565,13 +1565,13 @@ impl Connection { Some(misc::Union::AudioFormat(format)) => { if !self.disable_audio { // Drop the audio sender previously. - std::mem::replace(&mut self.audio_sender, None); + drop(std::mem::replace(&mut self.audio_sender, None)); // Start a audio thread to play the audio sent by peer. let latency_controller = LatencyController::new(); // No video frame will be sent here, so we need to disable latency controller, or audio check may fail. latency_controller.lock().unwrap().set_audio_only(true); self.audio_sender = Some(start_audio_thread(Some(latency_controller))); - allow_err!(self.audio_sender.unwrap().send(MediaData::AudioFormat(format))); + allow_err!(self.audio_sender.as_ref().unwrap().send(MediaData::AudioFormat(format))); } } #[cfg(feature = "flutter")] @@ -1593,7 +1593,11 @@ impl Connection { }, Some(message::Union::AudioFrame(frame)) => { if !self.disable_audio { - allow_err!(self.audio_sender.send(MediaData::AudioFrame(frame))); + if let Some(sender) = &self.audio_sender { + allow_err!(sender.send(MediaData::AudioFrame(frame))); + } else { + log::warn!("Processing audio frame without the voice call audio sender."); + } } } Some(message::Union::VoiceCallRequest(request)) => { From 344d927ff8bbd090b02967ba0e1217cbdb1776f2 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 7 Feb 2023 20:38:27 +0800 Subject: [PATCH 1775/2015] opt: optimize icon --- flutter/assets/record_screen.svg | 24 +++++ flutter/assets/voice_call.svg | 2 +- .../lib/desktop/pages/desktop_home_page.dart | 16 ++-- .../lib/desktop/widgets/remote_menubar.dart | 93 ++++++++++++------- flutter/lib/models/chat_model.dart | 2 +- 5 files changed, 95 insertions(+), 42 deletions(-) create mode 100644 flutter/assets/record_screen.svg diff --git a/flutter/assets/record_screen.svg b/flutter/assets/record_screen.svg new file mode 100644 index 000000000..e1b962124 --- /dev/null +++ b/flutter/assets/record_screen.svg @@ -0,0 +1,24 @@ + + + + + + + + + \ No newline at end of file diff --git a/flutter/assets/voice_call.svg b/flutter/assets/voice_call.svg index 0637b58d9..5654befc7 100644 --- a/flutter/assets/voice_call.svg +++ b/flutter/assets/voice_call.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 71dd2c96e..2986adc7a 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -358,14 +358,16 @@ class _DesktopHomePageState extends State return buildInstallCard("", "install_daemon_tip", "Install", () async { bind.mainIsInstalledDaemon(prompt: true); }); - } else if ((await osxCanRecordAudio() != - PermissionAuthorizeType.authorized)) { - return buildInstallCard("Permissions", "config_microphone", "Configure", - () async { - osxRequestAudio(); - watchIsCanRecordAudio = true; - }); } + //// Disable microphone configuration for macOS. We will request the permission when needed. + // else if ((await osxCanRecordAudio() != + // PermissionAuthorizeType.authorized)) { + // return buildInstallCard("Permissions", "config_microphone", "Configure", + // () async { + // osxRequestAudio(); + // watchIsCanRecordAudio = true; + // }); + // } } else if (Platform.isLinux) { if (bind.mainCurrentIsWayland()) { return buildInstallCard( diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 653ff37b1..dcc531408 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -657,12 +657,17 @@ class _RemoteMenubarState extends State { ? translate('Stop session recording') : translate('Start session recording'), onPressed: () => value.toggle(), - icon: Icon( - value.start - ? Icons.pause_circle_filled - : Icons.videocam_outlined, - color: _MenubarTheme.commonColor, - ), + icon: value.start + ? Icon( + Icons.pause_circle_filled, + color: _MenubarTheme.commonColor, + ) + : SvgPicture.asset( + "assets/record_screen.svg", + color: _MenubarTheme.commonColor, + width: Theme.of(context).iconTheme.size ?? 22.0, + height: Theme.of(context).iconTheme.size ?? 22.0, + ), )); } else { return Offstage(); @@ -708,36 +713,58 @@ class _RemoteMenubarState extends State { ); } + Widget _getVoiceCallIcon() { + switch (widget.ffi.chatModel.voiceCallStatus.value) { + case VoiceCallStatus.waitingForResponse: + return IconButton( + onPressed: () { + widget.ffi.chatModel.closeVoiceCall(widget.id); + }, + icon: SvgPicture.asset( + "assets/voice_call_waiting.svg", + color: Colors.red, + width: Theme.of(context).iconTheme.size ?? 20.0, + height: Theme.of(context).iconTheme.size ?? 20.0, + )); + case VoiceCallStatus.connected: + return IconButton( + onPressed: () { + widget.ffi.chatModel.closeVoiceCall(widget.id); + }, + icon: Icon( + Icons.phone_disabled_rounded, + color: Colors.red, + size: Theme.of(context).iconTheme.size ?? 22.0, + ), + ); + default: + return const Offstage(); + } + } + + String? _getVoiceCallTooltip() { + switch (widget.ffi.chatModel.voiceCallStatus.value) { + case VoiceCallStatus.waitingForResponse: + return "Waiting"; + case VoiceCallStatus.connected: + return "Disconnect"; + default: + return null; + } + } + Widget _buildVoiceCall(BuildContext context) { return Obx( () { - switch (widget.ffi.chatModel.voiceCallStatus.value) { - case VoiceCallStatus.waitingForResponse: - return IconButton( - onPressed: () { - widget.ffi.chatModel.closeVoiceCall(widget.id); - }, - icon: SvgPicture.asset( - "assets/voice_call_waiting.svg", - color: Colors.red, - width: Theme.of(context).iconTheme.size ?? 24.0, - height: Theme.of(context).iconTheme.size ?? 24.0, - )); - case VoiceCallStatus.connected: - return IconButton( - onPressed: () { - widget.ffi.chatModel.closeVoiceCall(widget.id); - }, - icon: SvgPicture.asset( - "assets/voice_call.svg", - color: Colors.red, - width: Theme.of(context).iconTheme.size ?? 24.0, - height: Theme.of(context).iconTheme.size ?? 24.0, - ), - ); - default: - return const Offstage(); - } + final tooltipText = _getVoiceCallTooltip(); + return tooltipText == null + ? const Offstage() + : IconButton( + padding: EdgeInsets.zero, + icon: _getVoiceCallIcon(), + tooltip: translate(tooltipText), + onPressed: () => bind.sessionRequestVoiceCall(id: widget.id), + ); }, ); } diff --git a/flutter/lib/models/chat_model.dart b/flutter/lib/models/chat_model.dart index 14af96570..bf7f8773d 100644 --- a/flutter/lib/models/chat_model.dart +++ b/flutter/lib/models/chat_model.dart @@ -328,4 +328,4 @@ enum VoiceCallStatus { connected, // Connection manager only. incoming -} \ No newline at end of file +} From c3b273a5add1f208a50c062f69906f45fc680156 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 7 Feb 2023 20:48:09 +0800 Subject: [PATCH 1776/2015] fix: android compile --- src/flutter_ffi.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 84407cd96..2e6c450c1 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1285,6 +1285,7 @@ pub fn main_hide_docker() -> SyncReturn { } pub fn cm_start_listen_ipc_thread() { + #[cfg(not(any(target_os = "android", target_os = "ios")))] crate::flutter::connection_manager::start_listen_ipc_thread(); } From e944b776bc6ce9afe31ac3ce1b7e6f4520cd8f18 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 7 Feb 2023 20:59:13 +0800 Subject: [PATCH 1777/2015] opt: remove unnecessary config field --- libs/hbb_common/src/config.rs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 6032ae9c7..71dd9a5c6 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -212,11 +212,6 @@ pub struct PeerConfig { deserialize_with = "PeerConfig::deserialize_image_quality" )] pub image_quality: String, - #[serde( - default = "PeerConfig::default_audio_mode", - deserialize_with = "PeerConfig::deserialize_audio_mode" - )] - pub audio_mode: String, #[serde( default = "PeerConfig::default_custom_image_quality", deserialize_with = "PeerConfig::deserialize_custom_image_quality" @@ -1001,11 +996,6 @@ impl PeerConfig { deserialize_image_quality, UserDefaultConfig::load().get("image_quality") ); - serde_field_string!( - default_audio_mode, - deserialize_audio_mode, - "guest-to-host".to_owned() - ); fn default_custom_image_quality() -> Vec { let f: f64 = UserDefaultConfig::load() From 3ca72e82a0b26d8a1aa38bbb731f9c7119b66953 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 7 Feb 2023 21:04:50 +0800 Subject: [PATCH 1778/2015] new logo design --- .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 1605 -> 3114 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 1087 -> 1939 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 2097 -> 4087 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 3013 -> 6636 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 3799 -> 8908 bytes .../Icon-App-1024x1024@1x.png | Bin 10508 -> 49903 bytes .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin 360 -> 669 bytes .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin 564 -> 1344 bytes .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin 779 -> 2049 bytes .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin 455 -> 969 bytes .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin 781 -> 1948 bytes .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin 1072 -> 3139 bytes .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin 564 -> 1344 bytes .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin 978 -> 2846 bytes .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin 1368 -> 4240 bytes .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin 1368 -> 4240 bytes .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin 1962 -> 6893 bytes .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin 926 -> 2594 bytes .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin 1691 -> 5794 bytes .../Icon-App-83.5x83.5@2x.png | Bin 1839 -> 6468 bytes .../macos/Runner.xcodeproj/project.pbxproj | 9 +- .../AppIcon.appiconset/Contents.json | 130 +++++++++--------- .../AppIcon.appiconset/app_icon_1024.png | Bin 23562 -> 53345 bytes .../AppIcon.appiconset/app_icon_128.png | Bin 2409 -> 5475 bytes .../AppIcon.appiconset/app_icon_16.png | Bin 338 -> 978 bytes .../AppIcon.appiconset/app_icon_256.png | Bin 4616 -> 10828 bytes .../AppIcon.appiconset/app_icon_32.png | Bin 644 -> 1555 bytes .../AppIcon.appiconset/app_icon_512.png | Bin 9733 -> 23370 bytes .../AppIcon.appiconset/app_icon_64.png | Bin 1222 -> 2851 bytes flutter/pubspec.lock | 8 ++ flutter/pubspec.yaml | 29 ++-- flutter/web/icons/Icon-192.png | Bin 4103 -> 8908 bytes flutter/web/icons/Icon-512.png | Bin 12570 -> 25973 bytes flutter/web/icons/Icon-maskable-192.png | Bin 4106 -> 8908 bytes flutter/web/icons/Icon-maskable-512.png | Bin 12626 -> 25973 bytes flutter/web/manifest.json | 2 +- flutter/windows/runner/resources/app_icon.ico | Bin 21592 -> 1961 bytes res/128x128.png | Bin 1575 -> 75 bytes res/128x128@2x.png | Bin 2760 -> 10623 bytes res/32x32.png | Bin 493 -> 74 bytes res/64x64.png | Bin 2264 -> 74 bytes res/icon-margin.png | Bin 12179 -> 0 bytes res/icon.ico | Bin 34072 -> 48 bytes res/icon.png | Bin 12963 -> 60426 bytes res/logo-header.svg | 2 +- res/mac-icon.png | Bin 90116 -> 51695 bytes res/mac-tray-dark-x2.png | Bin 809 -> 1585 bytes res/mac-tray-dark.png | Bin 275 -> 535 bytes res/mac-tray-light-x2.png | Bin 810 -> 1193 bytes res/mac-tray-light.png | Bin 270 -> 415 bytes res/tray-icon.ico | Bin 4286 -> 4286 bytes 51 files changed, 96 insertions(+), 84 deletions(-) mode change 100644 => 120000 res/128x128.png mode change 100644 => 120000 res/32x32.png mode change 100644 => 120000 res/64x64.png delete mode 100644 res/icon-margin.png mode change 100644 => 120000 res/icon.ico 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 d5d2c49c89429f17812a634e49dc5407b4782bb1..eac2fe7241381b7d162fb15323837ea7101e4a84 100644 GIT binary patch delta 3110 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 A(EtDd delta 1589 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 diff --git a/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png index 41ccba607c6c0a5c84d367341ca562d2e8b5e264..d32c8f8e80bec54065a4cfdbac465fb35af8124d 100644 GIT binary patch literal 4087 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 delta 2085 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 qM%IAf*eABYz8wNklBs*w#M07ZwsQ7(k@#I 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 N002ovPDHLkV1m#B3G)B| diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png index c35862a8cad79e203ff3a6a98b58612aa478d041..16cef3177fb50c285857dd57f2fb57647a41df88 100644 GIT binary patch literal 49903 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 literal 10508 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 delta 344 zcmV-e0jK_*1?U2h8Gi!+006pI?LPnj0De$RR7C)B|NsC05PJXm{QsuS|8%ndEsFnl zv;Q)U|L*qx%i{l@$^U(~|8TJXV5k2+l>ZWY{|9yd1atrO`Tz0v|J>~V$Ke0L-T#KW z|0jq4A%p)LfdA?9|LF4n;_v^p)&H>5|E|*imc;*$!2f2d|9@Mg|4^R)N16XQk^dQf z|4o3B(f|MerAb6VR2Ufrz=e^+FaQL=J;@Zd9fW6s{u|7weE|SUTlD|{v%wqOP5@1^1=A_{1*UzLfL-zd49NvB*PMY} z(gPf_1iEAe7*Uf~U|wWVfK9Rnd?xP`><+0n!1$6J`?+gsvIfvSCmEU~D*)V^nr@Zj q7Epll=f49KEdT%;wJiWZ+0+N!F$M@7z2^)70000iq_u zlkrGE_C^p1QjQLmh`dGM{x7hMao6oghdbCk5&mmV!Vm)v0|=DA99U`Mx8&+yEXb~Q`Z9ZRYQiA9XA%BNA>>#y;6n1-Mc1s0C4I!+D zBv!23S+IogtDT`xB5r z)J8JGAAj~r07E8(HWPR81!F}Hd(b4-KIl6<=)0SkAG>_@HWRNoGzIQ-A*UzmSWWr} z0_b=yJ?iPPz~LJ{0M$qh`=w4#ur4nvG5Ci8q#ifU-uswPQ<{C?iI{g)HW9vZYucU1F7oTG z`rfIq@=u(7Yfkn=P_B++!6VZ^sa@q6)z!5RrRU_kR&5n}Jz-T_ln4S>K+4tWS+F;V z6@8!;s@S}w9=FhJ2A&Nwgx;_^W@%!qd@0DNBg7ekl}Qe(TXe;r>@L;Se^@qJXBZc8^vQjTx3bl z;WuaHE0dBUAQ~aE8@YVFS|C&%KT`&WCVv&H;+#Rd zI{mT~?W!7fv6*|`Mkyc#7C3ltE7?-h9N!jJ<4s zt=|B?y$Ub7aQhCfc?3-yz#(SRNinCeg4|~zktKnnUBXTT8v>1@i)>tVruYa&$ za_Fu`5SD>~o)O!+<<<)_`TEV>Qnejc+n~*&)XUf`cHEJFpOE8YAv?G595}JjB=V_7 zAE3w;OKt5!ZQb)jF3EGpD0a9}e7MK+-F}gY^V9X>=0e>(X_f0TyF3RS4@PFAR!+|x1#db~sHt9i43A{YQ-WBE-7$m&ZROWEU zw~L$Y$z&%5n2gRnuj4QK;3qx!=F|c0@?k*3bX`}8Gi!+006rnNM8T|0F6*gR7C)B|NsC02X+7a{r^0Y|4N$wAcOy9 zs{dc6|N8v@M3?{Y_y6nj|J&>Tz1sh!&Hq1?|2B{RE{p#ffBz17|MU3&jgS!8IxBqak|9?=P{|a{h#NPk2)c=jY z|BS!?RG|Mlk^k4||DekMqd?oG0004ENkl7qjx+eexTz(c?%J*R;cr; zZScx;n5!UVZg<98%aMB5qa!{U#7+O6`m)?)0LAE3cTq7wbkyz< o1Dh_O-XEAC?es_Z$YwS250E_%Xl{s52><{907*qoM6N<$f=It2u>b%7 diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png index ab315a4c6b0e580ee9e2cc9567ac156af384c918..18ebaab699c080d107c6faffc1c9ddf0819dec5e 100644 GIT binary patch delta 2046 zcmVU2gpf8K~#7F?V4+B6jc<*@15EAwq4q8X-k(r zSJC*wR3HLsHxWpve$o#{e7`lm1&tDouS89#e9**z8iTJd`oUNPiqdL8u|<@qhyvS| zwm{pZ-M;oQJCEa?-EDVg_D*+anPQBdY^K|py?cLi=bUrT|9{>!0RZ~9ndzSf$jAPN zK3DdihbA!{h)F$RaX2Q0L=xcOV6of6)Ywf`Rt95iR7VCtADa|fPjl^OVqHN#z)J`N zB6x`-lX#C@W_*Rq+*D$1F0(BzFrVcOA_3|9|+R0@FQJ&U-2g=9!su2OX2(oukp${s<3oQj#?Zil!+k+%qTtni4h~ z9e<@~;_>Rj^$QCNO}f!_L+>5p@B1;bCYJU%H@8@N&#n}*1@{kKRi*yHN zPS9fE5K;kW5H}%iZ*ujcPmz7rQALH!M?fpW_&wMKEX60VyHr?*EuB|fHI9b*A3`ND=!?8~X zqHV(*ncKe0Wpk?~8l0vE4JYmi3%;KMC!%7!7Qp_nu(^M7W35XEbclyH zx_AT$m!NaoSoGN{$8|OOCNrw_hJ~iCGksAhnV{l+xT$aQVNaopHp$bWKRhf3LMZJk z)PLNpOl&uV_+&)9wsWK}BBs?aa;IZFynib6BrS{6q2E0u#Gik92r9T=U1~Lim=j^u zkE8x6ak`9Abh`EA)DsH}GV53h^q(WrFN2_*s}9H9h-413V(N1 zA>%2`Q`l))sfDSvvpo?Z{vU;#T*?A1r|0x!r{wm!3?@z(@oX}jst}yv9wtguX=>=H z*lcBfsckNpku1w4L`~J?WJpvmk!C~{AdS=^pTzp5Q7#5@>8f3StC=&Pon}>g1$Qtq z7tAOagOpEZa62+eLN#=m6KAwfaesFQGw#|5^G}M3=vQ*PE$Tp0L)Xkx3TY)b$t{5} zlsSxalG71sAOwnPNy(ilBGvy_7NTMs4n{R(m4tWurFEAWnW}f3jLA{Rr2Ap2Ukig9 zGNh0inv_&aJ-F={(vnr5{BAJxHa`~+eLDAL#U=}rLzUN5kK2w?WL1K@?SHuB4?;t+ zF&?siSlBx_Q{_>(eQruQNrArpLiFqog{(^FmL%L`5Hu<7o;na#hM=1Jx(aJf&=-~C zODo{lK|1&OWoTu!F~j`kZ`}SNF(U+1+#UzB+)a@h^pfq~W%!XV)H0S>Lka%+Hr8e` z0D4zH|LlRN{KXVkPM*BGj(=Xb)1mLU0=>8!kAxN6^(DCDE_R{I0OFl}{0(1+1raHi zffU?!Gk(x(r#7cU7g+JrP4LWDDOV<}-`vTZJ+TPTawzs(Cr5y%YXUkqAuhM+T=#f% zC3>$D9XlhpuXf>%dvRR}g_sjyUq95iu$OjKW&2Kv>%Wh7Cj2<1lz%DQZX2`lV)`)8 z25mFrjtAkbeex50bOl;K8{>g-SpAW3WCSn+s~eb`FJ{)%Gvx*9PxehnJA1`#NBI4N ziEX6JxmL}+xzSl-)hvT&(@s?`{CczI)$UPPwN2~}%3}tSoWI-rGLy%JYv-YPc3F%B zIp`jj2E%DPUE1qaGk@H-G?>@b&!+bC6nw`=!1um5FcI%jS)Y*MkDrH`{Wz@^0xEZ7 z3A1InbM`&Ee4vk?0pEL4l31f2ls~6hyv%{Su83(}UEs);QguW3je&0q3{0kpUoCF! zGKk7uU&QWdc2+vFD>~iKE#IMmF{Ip#Gl09%&9<&}R629gpntldCABn4&6n}ZRBog~ zRB_*Ofq9G1menirhkoK}^yOjVB-*a5XXmBNjyGRpy?K#I*KWCP==xH8cmr(u7H!^( zC%F8%?Pm1QV)MpjW}-RtGUSsz$Rz*b8uZvz@NOsG+6Dd*ecTI+n7c1!A6RTEcjj3X zt<807*qoM6N<$f*ObL!2kdN delta 766 zcmVjjH|7olLTBH9vlK(M`{~m(>@%R70-2c7W z|Fzcte765?uK#AL|4*I&NSXiI>i^d0|Hjm&N~!zW+s+|KsoffVlr$rT?ta|EJFXIa2;}0006ZNklT6oujcIAvyTA2Z|rmo|!A`=v^pY5j>6ofxhOrP}MR;b^8dyif{B$s;G* zwru!(N`zipLVwAWD7{aiMJwJ;i!3Pkm^nhr|EB&6}9TXz0`ai zosjQcHI@1T&Xh_94US^&o3h@3nXK6ZN2p)O=>QcuFH`eCtA9g)ajWEmoPn%!0w&t4mn`im#qQlAA+Ih!=<@lRm(54ZP~@ZZ=kKLo&8_yj z6zFQNG9m9u*-aI$v|9P4zmoG9s)dt0aiD)Er%k|{yt^qLqZ@f+0{YMLs*6pZRCkbx z>Ybp;OpY7+y|U(puw7Ld1E8gh+XiJGv*|ZL8F%MB%I#+II)~v!Bl=lUU2%+cQt&cZ zb~#ReeeTbjOG#>WCa1gsyAQvYgFaAO%-^^DP_#z0Izyseb3wEP+7jtWsMRA%<5@`U wsC45}Lel3hqR-EgQ0j76GaSv-uNO-F1G>T-l-XG?PXGV_07*qoM6N<$f|yI5N&o-= diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png index 6d69c01e18397d3da443238836eb42b69cacc901..a8ee14a31e4b0fe52fed14f5e6cd05e0b625d7f8 100644 GIT binary patch delta 958 zcmV;v13~=91IY)F8Gix*007zX@K^u<1CU8XK~#7FwU*mY6G0Tl&+Kk%cgv+M2?bi@ zVnli8Z|*pL}R=>h(2k2@`4gIMj=5XRI~^v zrGVXDU}wf@yKQ&NN->6$KJ0d8e>>-#Z@yW9QVR1B!hAsI%6}b=k^ZnEV=4)#QskWr z`2XU5npCdzM>~e2U&0FT6u88XhYBm_Ew(hdtSj=&|Hci*$jNu%HeZyWBnxQ<>41z| z2jmO>(2+8!ZK(qxG?QS!?Fr(BC!w!l3><>LX3W}+ZVZLbEVdU2bKrgq;7yO@z!cH1 z(I6PDz1C46aDT?5+MBT`^*)LOv z!+_fxAlE-)(7e-IVYfcm$l%^kT+2TdZMQ2$LB}o5L)A9aKd#R24?w4%)|cntJ{eL5CNr*7 zYT?R7PRC{8UCNz(Ns`w*M|0T&L=@WcI;<^%w|~TvPBFu6^1%7$h=BzjZtmhe?%7($ zp!=VOyGM2Q)Ld;zUuU?rPQ=hx-@wgAZufF^cGn=8CQ$E2NaGKWzu}e_(VmHfk)~R` zr_x;KFd1-JT-=FHZwJw&BJOsR{r43n&Z;6<;{v+`V=>w}jK9kyaeQR#$tiN@R$H|w z9eFgEy1Z7X*2lN@l3CA$I}f+={usOBlgjh}?yc gcBQ$*e0@j#0g9ATCw(}=4FCWD07*qoM6N<$f@q4~ga7~l delta 440 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 delta 1062 zcmV+>1ljw;7_bPC8Gi!+002f7DP8~o0Hsh&R7C)B|NsC0{r>*~a{mo@|M2(!26X@X z{r~j&{~vi^H={|a~i>h%BN?*Cq;|52X*5PJXR@&Cr(|CPl5fw})0fB*RV z|LyhvrOp3|z5jHw|75BEOPl{UkN@55|JCUK%i{mM+W)Q5|9_y$|Cq)9dbR&mp#MFS z|1pgJoyq?ziT@>r|F_rwk-`6Num5VS|Bk@_S)%_%m;bNR|ESLYOLh_D0009`$|4TcIH9aK20HI}$e9v!qaSeFkP2MXKH>q^)ZC+fu z&6$NVG+ujpcYjw!S*-i8D`qHDdvxi}P_FiOcZaeaNU&K&*%~W?GnB7Qcoxbho0#A? z9Pp0c&_M;ip#q6Nj64rEwK=}cYMcJ4i`y*^V+LLjI5OaF=N4`u+jU;l5b$|nzYTG< z^O_+KffhrfX#-cO?gcdt0NB*D7P!Jkj}ct}@Te1$E`N$Tx=igVs|OU*XPl+>gT`K> z0J&$JD*>R#o5Fi0tP!a5mhgZ{M+BakG=7Acj$oWUkO(xUHWStjm>7;3CAKFur*nD| zjOoL~Hdfu;A?jfgD~mJo%^A`9K+>Jhh_!<}R+GA27eA3xLEQ{_j)r^gMNl)gkj-gQ zGm%I&6Mxja6xqAJV|=Nvk!?f8t{{uEre=vO4}!`ovMs2Y8ehR{i5xzaIhpm2??e z!NiIoA(6(SQiq8;6$@#qR5mc-Q>hwt>oBn~>VH~PS};+g(m)z17#c|!uo-fZu37N> z?Fxo^hKIL>SNq}x$>EBIoxH=M3k@bsOTJ^}8K=TT5OBVE@m^ma6*GHRG!Ki&}8 zsy3A^vb<9BRpg!i6%a*2FhtzllVHR%@>wfJjlMb#6lW2ya%I85oyR!) z{t@5q_G8AmOBiVjCU_qRELIiI^)Q2g#V}gx<&4h`BF=xU!Aptl{*h>^K z6^v>8)D;61G+7DfiI)DEWS7;3E`2IzYN^>r8o#C5Wzg6u0rgyY( zz3TDr(wF{-5SzJ!8!eC9G2N#M0$rc%eZD**Uj_N9mYTc@6BTsu8y2|7Z+M2?K>1_~ zd`0?GUHUIzs diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png index 5fc34ce9a95f150c792d5f88b422a1b32ba40456..fd3b01b6d288c9a0d2eae79824afb420cba33e5a 100644 GIT binary patch delta 1336 zcmV-81;_fd1i%WA8Gix*000A=FFF7K1qVq)K~#7F)tFyQQ&$|v&$;)uz4Q-sz+f#f zLBP$i87EPQy3FZxEbd{MW_$3B4JAu-2}`y~WCHGg+}`%EnCW8NGUGO07ZDK`C#;L^ z4;mRzD}xRlltL-}ckeknx24{D4yC0Hi}CkB4$Zyi)ARfD{eS&V1&lGc7be^j>iq_u zlkrGE_C^p1QjQLmh`dGM{x7hMao6oghdbCk5&mmV!Vm)v0|=DA99U`Mx8&+yEXb~Q`Z9ZRYQiA9XA%BNA>>#y;6n1-Mc1s0C4I!+D zBv!23S+IogtDT`xB5r z)J8JGAAj~r07E8(HWPR81!F}Hd(b4-KIl6<=)0SkAG>_@HWRNoGzIQ-A*UzmSWWr} z0_b=yJ?iPPz~LJ{0M$qh`=w4#ur4nvG5Ci8q#ifU-uswPQ<{C?iI{g)HW9vZYucU1F7oTG z`rfIq@=u(7Yfkn=P_B++!6VZ^sa@q6)z!5RrRU_kR&5n}Jz-T_ln4S>K+4tWS+F;V z6@8!;s@S}w9=FhJ2A&Nwgx;_^W@%!qd@0DNBg7ekl}Qe(TXe;r>@L;Se^@qJXBZc8^vQjTx3bl z;WuaHE0dBUAQ~aE8@YVFS|C&%KT`&WCVv&H;+#Rd zI{mT~?W!7fv6*|`Mkyc#7C3ltE7?-h9N!jJ<4s zt=|B?y$Ub7aQhCfc?3-yz#(SRNinCeg4|~zktKnnUBXTT8v>1@i)>tVruYa&$ za_Fu`5SD>~o)O!+<<<)_`TEV>Qnejc+n~*&)XUf`cHEJFpOE8YAv?G595}JjB=V_7 zAE3w;OKt5!ZQb)jF3EGpD0a9}e7MK+-F}gY^V9X>=0e>(X_f0TyF3RS4@PFAR!+|x1#db~sHt9i43A{YQ-WBE-7$m&ZROWEU zw~L$Y$z&%5n2gRnuj4QK;3qx!=F|c0@?k*3bX`}8Gi!+006rnNM8T|0F6*gR7C)B|NsC02X+7a{r^0Y|4N$wAcOy9 zs{dc6|N8v@M3?{Y_y6nj|J&>Tz1sh!&Hq1?|2B{RE{p#ffBz17|MU3&jgS!8IxBqak|9?=P{|a{h#NPk2)c=jY z|BS!?RG|Mlk^k4||DekMqd?oG0004ENkl7qjx+eexTz(c?%J*R;cr; zZScx;n5!UVZg<98%aMB5qa!{U#7+O6`m)?)0LAE3cTq7wbkyz< o1Dh_O-XEAC?es_Z$YwS250E_%Xl{s52><{907*qoM6N<$f=It2u>b%7 diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png index 6928a4e6d8c50182ef8a030468ff770cfed2c937..aee7e4321be4c7803a4d8c12683eb2e5f0062d0c 100644 GIT binary patch literal 2846 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 ws|rC?VLFh+W%U05c-&AcOx6 zc>nYG|L^wyoXG$6`v1}8|GL@#cC!Cfp#L_H|1XRG7k>Zi^#9@R|I6b49fAMV=>NXk z|CPl5j==wdx&LRX|5>B|PMrTkmj4lZ|J>~V#^3*ky#IZ-|9?A@|FqTru+#sm(Ep;# z|8TJXDv19jhX3X9|H9q>r_TS`>i>zo|6Zm4g>sOY0008?Nklm zhOojryaOvCEPwHCBhrE$X zsR95)W^`uwNYG_gUj_<82j;eI3gQb#%o~I-HBXth0)K!t3%aI>jfe$zS<6`i4<>>( zi|PQdC5!eB^t=iSIurb--e=JhdWFhNlP!ei^U54|4xKr(+@ysZuNE7ksrWJYI; zBJ`@v+MryG8Mg)gz!|%gq87@8ZAw+u?@X96MwydYyB$+%dnnPT5ci zXgI#?@Q%JthO>jaX%R+><7b;Y76p9T~tTt5ojLP3^u46U2N7seDq=sYM~ z44!F1!$~PDpfI5D0*pIz1eN52t~a4jfy(KFYJc8QI2w(wbChcNCPT3kQ`n=_hDQB< zPr+@X#3H8ffZCK&)pWfwhME^sszGT+$?u`WlagCET0q`WrWH~06^XN$y0Vtm%y4V? zowISN{Xi+~Fl*32!M4k+D1gS?WzK5Sf)Gvz%sWAA%mR1Uz<-*Dlm$EJIXg<(g>MkC z=zp<*fv``*ZN)W!kzR)d139>5w3{&0>ocz_7hF>33}fM*Ih8*Iu3Gp+&1dQM)*n0g zOtVt_B6CZOX%lv4&$?Vmf@6Ir|$?UQGUETTSlqKoAQL@0l#=hH6UJGah=4VBo z2dw~gST(~DRt=FZV3F4b*RbeBFtvH88$c6e*MlX_fCR~d2P>R}??B!HSkN2D%Y|qY p;!EBsrVJ$GWcJPV$86Z<+uw$~DCT@XL}vg1002ovPDHLkV1g^I^QZs- diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png index a13129e156dcc8e44937d4492ef206d7725d8548..2d0da17b1782ad67e7de6f7c5426199cbcfa8df7 100644 GIT binary patch literal 4240 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 YfLlQ8cmVrfj76gLL|3gs)h6u!0N77q;Q#;t delta 1959 zcmV;Y2Uz&+HL4Gg8Gi!+000UT_5c6?0Hsh&R7C)B|NsC026X@F^ZyWf|NH&_A%p)a ziT@RR|M&X;3U~kD?*G!}|Ha?`s?YyqssB)(|M2(!kHP<4rT;Jgd|FhNqX{-N6nE&|v|MU6(q|E=H%Kvk*|9>2S|G?b;o5%l!yZ_|y z|JLaL$l?E%#s5H+|IFk6uG0TAjsNZS|Ge4%Zm$0(hW{CV|Ig(AT?7ty000KSNklAkY0rSIPwrVdOf{DCKufRs~KW$fF!+QFcuE9w8H|;9mSJ`uoEBwI}I|hbAeB=V`9e9(SMOszWV;7h7luKoh7LW`6zkq z>-5{x8wPGK>5s3E(2#0eKB&~vBlJ8{&vRWt4qC0!z>|WOrPI(ZRj|0vYn#SOv{IT- zi#seT@=4$pt%3yfAje`hJ_#jg1tb>SVG(x^mn3$B);-O*Of(#va(hK{KG5=LmKx%~ zOpoTR(0_7ikDt}u<#y{l69wSCWePJhbIR@g-`NK*6jsmJN-}bi=l6kG7R?A&iXqZ8 zn#Kl$Pafk`>qt$wVywMw(Bxo=bTpqC@4-H3dL5*pJf%X=sdf>wCQk@@%VY-ww{im} zE8n5lxbaw zI8b)hy*M*toCPiy1^R{3hB2x>E;gs^!Qea_x1TzbrD(f!GV||TP94>be&wb!S8pi^ z+O9H#IW9A%sOD^$t>$o%5+ysMpY7@_Fn_r$C1YIZJxfUuml#np!^K*Z1WR0?K!NAu zV!lVo2DT}fpizUroV?Np|`E(M|Pl($G_p3dkl zF0C|HL>k?lni3Sf@Xw|=ONQ=KQ-ApmTps5MQYZqA_c}!l{Uuzf4E3{=cz+p(2ezKk zxrCs9l-P3hZ;vUOM7T(2LQ#Z^?38a^ee!n{t#Fy1tIMoIhnFDVCzOqxbZAz6jS^q~ z>eYfVT5c~uPLB@m5@wgU*pTW`ADq8;X);c>j#K64jN|z!ynbpKoTS-jtXF98pBd{R z#0g#l3RII`yfR*O1Y6A_6Mxj9XZ_KM#c>cMp(vG+I74~~=N;_T#s_L{=c)|~hDJ^B2P}ZZZ zpdzG33bUPV!XTxVndw#E*qS13HROdU?Kjx(eOl_tIihG_;6$dtlYd|6=db=kf4QZV zIX(p`J_!~?-ih5aM=OOT9D3{1!T|sBY2z^mr~C&dTb&kk39k7ACMurNJ%X-*bEZ3C zf<69an8Z6G72~OO(8yAyR#OlaZMz3Nt*)VQ0m}HOuZndrZHLg^bp~i+AIDR#h!muH$JYd)9U|z;wFy7 z$*^H?6P~)OfSHG;stkS4f?n@d9Z;y6A_%@&h!XYecYDBVq36kYI z`_P2K8G4#b|DT{)!rkcd-=I0hod~mEL3<(Yf+_GTXy)H>2h56pf#dT7C#O~5KL$Q- zoH_7&(09>8%!K6rJ%p_N+7*cap4 diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png index 229bdf563dba4a57d01b59bcc19522eb674596e3..76abd423b27687314d56951abb83d5f22006ffdf 100644 GIT binary patch literal 2594 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 diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png index caffb26a36f60caa3ab4f40d826505730fca1ef8..e08138333486262d3a505ef532a2827709005d41 100644 GIT binary patch literal 5794 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 YEi`7===YERv1c?@MNI{SoK@KW0OTwX%K!iX delta 1686 zcmV;H25I@CEt?IH8Gi!+001u>&=UXv0HRP#R7C)B|NsC0`TYL{bN}`F|J&>T4tf9l z{r}A4{|9ydu+#q|h5r?O|4*I&E{p#ui2v{Q|Hj|{I+6byfdAs}|I+3Em&N~IrvF=` z|38%f=kot`vj1+Fynp4#$YG^idA68A_8^6P)48{3|ko(P#DY&NR^s5T_Wl{V2|$7hi*IdA;+-N6k5C8dw;#sT7UJ}gK+lEEA8|>Y-F^j zQ-lyn4O}{OgSDYThq`f3SZms)V;R#Aw5MYy zQw@PGX7Yav$Z3}pZacJb6Q!qvy=(|do!V|huFm6LzwZ$}ByjwGsH-xT^ z&s1Y!xsW|TQ|F%P?(uHK4B-HJsy$|y<6VPU!Y62PubE|!cL6gwBj|7!%+$t*duD3Z zp<=~MeSA82%+#$y!4tD=C-~;B#Z1pGtaX}9=YMa0Tt=2&_L#IbXPVbl9fDx+95PKf zf~8xg=nZ0+boNcA=mxNI7t+XoLaJc1rP&Rvv}h9UU1aGbGzuH#%*gHm=XmsJlAyS_ zOM`h8=V|n4@?w>ulWlOecu9k9jDq5^UAf*FOE%DMup6blHbODMD@AX}$xSF5p^&NC zY=64Cr)Y^ndX$W3dh4`zg#zvOwBO)04(rw0lau+Rx9ctlPK1vULcXCL{vTmH0- z^{)>tOn@>IrckAkMFy}a@XIo+uD<${u zg>@VX?va<Z>tRO<<|+FrAaU;6&%P#|$kOwx-O~y2CfKzJJ3k-45(6 znaO#;r|La3Ic+GoikNMbryd+Y#c|9`efdHv+dxVEm09L^H_A~7dv{G{h@`86YzIXn zhv|GFktIu@>SfDRUGwR(NRF$xC~ia&B)Pyv*^?yf1AN-n{3;j^xEK}Zd%>FRiz!!^{{zcZ zn6|aUlY(&<;Lo{x>5po=BmA*OM5$opqK-dwd~;|h7&#vX_!C#PrNi&Q6VCH(t>NM~ gRcg11#}oesfRbFCP7h8}!TXrT_o{ diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png index 751104548a96b25993043b95b8ab079875a21077..46de51af67e1c4ab5b129af9ac5fb03cad3e972a 100644 GIT binary patch literal 6468 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 diff --git a/flutter/macos/Runner.xcodeproj/project.pbxproj b/flutter/macos/Runner.xcodeproj/project.pbxproj index 066560203..18166c8ff 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,6 +462,7 @@ ); 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; @@ -607,6 +608,7 @@ ); 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; @@ -644,6 +646,7 @@ /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; diff --git a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json index 7b4d860d6..682280dc5 100644 --- a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,68 +1,68 @@ { - "images": [ - { - "filename": "app_icon_16.png", - "idiom": "mac", - "scale": "1x", - "size": "16x16" + "info": { + "author": "icons_launcher", + "version": 1 }, - { - "filename": "app_icon_32.png", - "idiom": "mac", - "scale": "2x", - "size": "16x16" - }, - { - "filename": "app_icon_32.png", - "idiom": "mac", - "scale": "1x", - "size": "32x32" - }, - { - "filename": "app_icon_64.png", - "idiom": "mac", - "scale": "2x", - "size": "32x32" - }, - { - "filename": "app_icon_128.png", - "idiom": "mac", - "scale": "1x", - "size": "128x128" - }, - { - "filename": "app_icon_256.png", - "idiom": "mac", - "scale": "2x", - "size": "128x128" - }, - { - "filename": "app_icon_256.png", - "idiom": "mac", - "scale": "1x", - "size": "256x256" - }, - { - "filename": "app_icon_512.png", - "idiom": "mac", - "scale": "2x", - "size": "256x256" - }, - { - "filename": "app_icon_512.png", - "idiom": "mac", - "scale": "1x", - "size": "512x512" - }, - { - "filename": "app_icon_1024.png", - "idiom": "mac", - "scale": "2x", - "size": "512x512" - } - ], - "info": { - "author": "icons_launcher", - "version": 1 - } + "images": [ + { + "size": "16x16", + "idiom": "mac", + "filename": "app_icon_16.png", + "scale": "1x" + }, + { + "size": "16x16", + "idiom": "mac", + "filename": "app_icon_32.png", + "scale": "2x" + }, + { + "size": "32x32", + "idiom": "mac", + "filename": "app_icon_32.png", + "scale": "1x" + }, + { + "size": "32x32", + "idiom": "mac", + "filename": "app_icon_64.png", + "scale": "2x" + }, + { + "size": "128x128", + "idiom": "mac", + "filename": "app_icon_128.png", + "scale": "1x" + }, + { + "size": "128x128", + "idiom": "mac", + "filename": "app_icon_256.png", + "scale": "2x" + }, + { + "size": "256x256", + "idiom": "mac", + "filename": "app_icon_256.png", + "scale": "1x" + }, + { + "size": "256x256", + "idiom": "mac", + "filename": "app_icon_512.png", + "scale": "2x" + }, + { + "size": "512x512", + "idiom": "mac", + "filename": "app_icon_512.png", + "scale": "1x" + }, + { + "size": "512x512", + "idiom": "mac", + "filename": "app_icon_1024.png", + "scale": "2x" + } + ] } \ No newline at end of file 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 1c6cf008ad8720ee4680a32de29836e8d31ed7fa..9af6f2121eb5a5394d671f8e0ab600cef240b3cb 100644 GIT binary patch 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_128.png b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png index 8c2837a94be001dc0272ce64cfad36886903508f..493ec70768563bacad4356d5b0de3562d835663b 100644 GIT binary patch literal 5475 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 literal 2409 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 delta 322 zcmV-I0logx2hswN8Gi!+001a04^sdD0B=xCR7C&)00TUT0!5wxF>eAnfB_�tgZU zTFe4Nm;f(%812Jv`F>U~H|NsC026X>*vj3#b|2~xe8i4-}c>nSE z|Ig(A%HjWcwEy?||MdC)f4Bc=tN$~O|1gaI=<@&4=KsXr|9{5B^W*>k01$LiPE!DL zgl%j+1+;%}Xho!=Q&N{R^t}K80D?(GK~xyiZH`wKgdhw>$H86$V#TieU!w`4M~}Xr zOP))>&HTTXpQ3+h2NzTe UgN~k=m;e9(07*qoM6N<$f^iv?cK`qY 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 ac1b107662f7d9065b4bac6dfe07e3159121e3e7..22893b8ea78c8c2d4dc1835a5e19b3c056e11d28 100644 GIT binary patch literal 10828 zcmch7^*f5GX0iY5@R1^d%60gM~hsysNN9 z-@JE_ktSk;`RobY-i#aWS?B+H3t)Jei<^*Qvvcx5kWveI-<8bbCm{F=vDm;Ck z7&3dV{7-987sxmOa{LWx4@OB{{WY!<7208rAcY>qt_Q|}3QY^9-E)qt1{`t$J$5T^ zkFK0lVHY3I(*du_YhF zCWT?#hcE-(Kksv)5L`o+pCgNpo-6{^QA#TOmrI^wontd&-iIoguk1E+wgSdpT`qc( zB6;JSl#+6Rkrw}YKrl**-v6s+T@0v_EzHd-B^9D#X12uUKXQ5<7`{_ae_x61V)%_& zPFU4Zo~n4H!=GC+DyX zrq9OR^{HF&<~{RSih@rb<;Hj)@cnM;Y`TWNg{r3JcrlR#3OR&6lJ-Za5GSC)(NR^C zZ}#ILvH4}W2q6`pV9J5DLX2UR&rX&LsxPhxUOyCm`}R%R=f|3~A@KK8^J2ya0v#OX z?H$L3Wk5_=xb;JPB%xh6{T)uy!xc{eatBJKPrIW zS%k97g$D^6!xcA$o*R9cpq6b~EQ)yrqY;_D{cP{HWk4e_ZLSF|p^+RX)e zK6zp>{L-=t_km)&Qp!>nICFj$nR=)R#7;nee}9AQOsWwE{m)+-3L0+!!|B}Zy0c`e zTkcdpntlU!@r#5YMwR&FTyXTXB#Dgi(XWWp<6yqAdu(j1TMlhj87b2wa8L7dGpQo@ zQP}3WAkg4D}g4JK&;kV ztmMYHP{CV9gHjJjHP(3}puBG*{4y!`w}=JqE~ z=H&s_10Vd5i5_Cfe25PN@ie|w`yDvW3;>V3eD!laSLrG5&!_Y+4eC}r=KPXhhWedu z#>c9hSWwPDb|o6YOXgjHe)S`Pie#ag3p_O?pfQv{L>3eW`<@|n`h$k_Em^!6$4$9hX4?I*l*&UX1(Hk+BO@bL zPEsvQ4IP~`{JaHvdT(i3;Lel#+1S^xfkc$~iwr$3zbTsX&_f2MrhkJG% zFZ+@{uJx~c13xK0s6GS7jVK~B<7Cemq$DLc@>zyQuD6v8sBxNx}rOsAg=SS_KGm|?%5#h!5F zFW$3c1~Fzt%+B=R80@JDzi$rs#&M>02J<<3W-`r5M)8q2MS?3lJu1SbkGU_Ff+zUL z?l*1YrZIozw6-i_AjZe+!nQU+vL z11`$vY2!&u#_1F05(0`4DGrF-mKNKQ5HXR2_pdeSjS1R0joE>sYEjH~{6UiA+wl>2Q)-XY`3hEP`(^mp0e^DHH3k>Z4{LPx-*Q!)QGf-+)Bb>jx z23j!p%5C)Map733!^rHz5z$az=ai75$CU{vCT)+L%V6a>Fsl7rEGR8sog@6J_Rk%c>hmdclbW>RtQ5Vl(#x!2c4#_sn`>u^669(ay8v?4l_0hlStisS&ASj&m& zowC!OQ&iXXhz}H1i5pVjxTYhWSnsY*>y1p7#c9=;Nd$KPLssSLQ2zdXuWDJH-g2L* zbv4J689BRyw>V%C<&{UmoOj; ze_suLB3ATpqiT?qP@KOx;^aw;mUe&Jys<}A`tX2peBuOEv;O>1RWmwPcDjSY2-vT! zg;#E>`Se9&%lhRyFOg2Yjr<9_^OLg-!?szZ23ML zV}g%yfs6v@Tu>ay2~M&>3u+z`t)^r+)g8l`uEb5z%4~x#IRi>pRvX%TnXR*OL&-oI zwx2S>g&0Pz6bK|Mqb>#Mm635BrgH+AJaLDsWf5|!-4=xSkniL3#4m3G2z+Gqtzz2w z8_=M;kk-k5HPFIwg;_R`KnAJ{iq0*Od{hxTIjV>PcUz==Vur~s*=jdDjve9$BX-PZ z=UBDGp)Sw?GM*VrWxv6ap%IcOR=3p^`s{-rE6reie28M&<&SA^gAmy#SDy^g)|&oY z*q3~MXS?nQyh9!McYI^zZ=N}WTH$)JxUIRqd49LKZUmHQ?F2qHR2E)iDud#gej#HDxWT%lLcW5m9g5<}DXC}V&Zwi2Rkgr8`r(KxULMOjj{{3R5kA>@zA8l71a16>fFT@VwN8zA^P;3S8eT z96BlTSwIP-f3Dg?Kj4WPMhbyu4aobxUmDSzgc4;K1-JXr5fz>@fQ)cFR%m8k#iJB;AwS3w_NT`{lW0k1H`x*q&rQ%g03vZkho z!bO|yKsAF(ePkFZyfL7q?SP$94P}cQ9=?vh5~4zP^F3{lB{nDIXPI;Omufn1Qxn4b z&ypwMkJeT=w|f|hCCe`Ej$!LFl=jh_VE0RLf>}M+OYb|ehb4=@5?4R!o&|FA;I>lz z;fWK1OvhfgR<3@D{MU9;N6&e2t%P^JdMK+9+}w(xg*p?>PhBC>FpD9aSxF9`zS`EY z63t#~F3o#tYg@ijBmJqSmA-Ht6SX=Hp}VT6--(vHcNJiQK_(KgH;yU+$kh2VWt&3U@ty5dm=7;@9J=Nl#!E*I3iHZcxRPZd@gO$xgok#6X%6( zH6*v#ex1MZfbvmL!=j`7LyIeXojdw{97ODU(CVW4I?VFf@LG>ZF%!fe<%!L@T3)Qp z`?2adr0iYSSAGj;b%MS~S5uS=ZZhgm zOXH4VKgtMPV% zgJ*)yoQNJOItjQLM-62~pB7$|0{GDj?;*Ewd{0u$?B!$UDbvegHR-mQPnY!W)d2FY z$wX!!Lr*twD4cyy%XzuHFCJlh zYX^g(!`*(r<*1f`AdIkNxUMv)!IQLp|GSQtx{s}ZHr6fM70(TX>O$7miS{~oUQ$^5=|>k&qcPmn7{+PTcDf1lMfHnw$In{TJAbAqPhfB7rd+zM}Ocztr!$zOsx-TUICd`H7&)?nXtvnb_Lr{+**VZTy1Hep3W|$TW`gZ}-tN zIw{tL{aP!kgvtnU^+?o2LcvDyW$yKzikN%H4KrbIF)sFF`?I^?^%^bbszv?hl|5r0 ztP9ms1YFe)GqbK%A^}AdrMU=IrldFBXHq=M10_od*FRl)4ztQumXDfkbFrm_c;>M79m;g=X*GI2T6b52uB;Y;Syu)#gjPy^H)B8}?Y_`=ZBfE!1Ur zhF)AJK7Z3Dl0rw5R$*$;t_fR{tgEFe9z-OS!HURNSNkC(lOy16jqM{J_88jz#p!j? z?T?Ih1HPS7EAN)B#G4r$(KmoZ3SStiwzK;a8^Y-W%L?oIwN8HWeieSv%E7zmF<9O2 zl}yy1SK9(-5!?fBuD0z<+@)%f3%A+0B$Qc|t3ShZ^8Q&tRwGPBa{3C#;x3Fi5hCh; zil^wIt}*irB|eAI5(pP5@P!E{p5L+ zLFf=0H^4&>?rJ=&9a0wo4i=Q8+Pc)e*=l4~Pn~MgUaHdpaiSrIwe^!mE27+sAZV)~ zL1sFcRX7D{_*ZCAAo=!Qp7GDgt#2#kflzSu=E!|Q&;zRw(7Dn9nfb(f*vJ9capl0< z1#&xb^H5wVbYNFp-Mc0z?_dBw?ud)c$$)s$rt1OLkfF^|bt`os`a#177T6}#CPdHp z<7WKbHdexPpYb723V@tLf?aVg>&C+mCxFw4T6Jx-~42d_fXj=>MIEDqF!+wO~3@Pjd-vop}!y9G!CDv^HLyi4CfwJ%mbTA znT>(!Qsg=AnlBplGwV!1`b5e|LrJrVVP}6PKh!yb9q#33YGNfM{}x8j2~V9RY|rYEBDbHM?}ny;47#PP zH4(llpyB|3-cqT{`!ANxdM{YK)%&nnXv{>H+r7}nuWJC0->)+h(+Q)1*E4)6;GImL#USjRx>;sFr;B* zMp@{F_bmUWPOWMs3#JWFm(MdWTKS?3GfKb?n!LVp6AIGq$V|zN3ds=haagJGTc*U_ zufQ!3rhJoS*-Wgh2FOm>9p_EeSjkTd&Ac8MTphko$p*v1ZI;8%u8pfOGIL7*RKhkK zk1nj*{(+>lK^a7wJyM1y^Q^d1^VEKp7`V=o(}N)1Y}$?SH%UB*4iy5K>&0$Cc{`Q@ zg`&t4dGRS+!Q~(WhE!7IuL=M-g;|3)De-~-DXCl8nd0|l!Wmz*S?3{>=VA zrvqH-q1In-r2KYY|-#_^NQ07{?`V%hD(0ZfBia@npLKH!`W_cO|qG z>|V$5)hZlBpHq7LSxOiN`}_f?br;(^@_ya8kngdvoC@FlSFY%DYU7F#x!;ei$ud22g(h6PhsjTfV8LtPn~o$ zj9cN$dlEFL9PnZ$pD7FoWQDFr9N{i_waz>d`Mw`MAV zQ?d$6=s{%@jj%M{0@zaLVqsw?qpRf?{k)tTTjvHQ3I&>hoA{+oBgH05Py z<`vuNhbK?|T`?t<8qh=8f!06r0y4M|j6#c`I{Xj2M(ye)&vC7pD}>}s*nwAC-&(L8 z;y6%2x?F&E4Xw#PK`*{!geDpTHpfN$ALVEiVPi_eq|3BB%;^F` zpsgf>g~i>LcED*;WF6fbKvV;-g`$n$5e!EpL96QLmf+GW@&=MxYuqQ=_RIxrLmO}d zpOR3_6lT;PNjjX=Xv(_}&!BzrJl|Eg=rt}?Az2ahu zT0*7>AqC4#tL&-Ky^(+q^?(sOmE}KpdI{h@A-38hf>o?TEX7rNZ0dP-0fUza%};!n zY#Ly+sy3b?)*xpFME3y=bsR2;&i5Y;L8O3MAo{(h*}r$BsT(+kW4O5hoKN*1 z?}I{QPVt44keG8^fZ-O1NZU-Sbe6znd=*!%h6v2OG(p;Mk)SS&)n`hiw$pChU~DT$ z5+C`d?^EcAHLdQdUO^yk)J9Lb$REQdTGrg{19ZI z0BUS1Cj#oZD15+nki7L*Sc)aoPgf8IAFo#~p&Iex{HkaFhRQT1gX(&`+>d!16-ZJ# z9n;%WK*S@@YmxzHgyb#PX>yA#XFH<|fli=A%*+S_&&Muq?@^vvm{`lzo$?J6)fvGT z1w)|8JG~LoY{iv`11^AoU&_rxyLw~Cg?Sg)_QDou@C%hk-SyjeV8AAs!Wn`%uxax! zPkM#nVLsU3U#}1{`vw#~Awl`E)S0r96VQ|Mlv+}9-l%6N8Je`dEST>6xp%y6kci8p zx_o2L6N_omczXX4Kb0J*TO3;IR)mk~s%AsArrrEv_S4!4fNqgR?V3`=edeZn=Wc6k zr}o0sGL0PB_nbiHtM~rX$uRWVRsQfk9lhI)#fseEgHryZhFc20=v`EE(*u!Sw>}DyM01+>W=G}a# zV#LH!{D>TTd1eV20}G}$CxB70Yl9{97}$q#DM;1Uaq;Pj*b@-39mdPS>0=)y2~0Y3 zn^xQR*iyMe3L%WIv}nHTKk80;k?LXG?%N&unt~VwIw_Y;4>r4@en1@8R}@TuKEoB$ zfFpf*NB9LbPe+Uh=E3{S!+_DOtWByA;T9UN;TboYtj~*}f-kE$z!BQ2RqT)%O*4FD zX7^m22dB=VON@i}YTZZNZqsWAjP*|UbIKsgjoR#!DZ2Z_H5_1xmhyh~N5t)nyjdL- zK_VY;7y`zK0t&p^v2T4qdM|%PV94IQ69r@T5u{kWOnu^pP zfTPZY=m>+uulwbt4ws*`{#q_Uw8NPT#(!P@QWIKPn8!eu9R|u*ZvtsKg^>jyxBE_W znqrGr*xjplf#LL*!IXv@<$s~+oQPp;Cad?~wL^^8a@^kB*^8k@ew3XE$bdu=CN;bo zKVaWIVUq2>Ct5yVZ(bW*UWKN!BHFuFa*DgrK|kf3ey>om_nnQvlW)P30d7Mxb#dX# z8h!SUVIq5|yPDaB00!G-Gm^|dtWPN}^d6_oxe`c1VXG=D{oJZf7c|TWzjX;X$MZYA zKn!2^3YPa(=2g7`3dZdX>ibrLnMASaaV+I=F7J*1llJOD|+30f*Kz~ zR&UQ+x?2h>BUpHul19@)DQ~-Zq+T!8x+$Z7%X;Rh4|6`R&p3_S>@7C)OIBCpQ9P|G5h_Gz(vvdaB*usMJPa%`-UjNhKd+Fq z6aX3jLH2F+#Jv`{9{^ij4pbP=s1+*qJ7kFt&#m`srIXB;uvW&`qU|3#PCurdi|HK@ zTK2QX8#M4l*;910k7*czn)B1*crHAa9JbIeb_nhcIRfTPz>jA|0KyO09O6qN zf4$!fd~Xh0b+P6l$RN6e{UIj$H9Ve_1zYlcR{w3?8mEeV_OX>UDufnCIN#AlVDLqV zjv`2=-Ov3Iyrp{MKrn-YbbTKlVhE9#r6`Lno?1xQ`G)E7C@#$ufQY^qQYmu@G$)e3 zn5u{lRQl$I!i^)Kl6+@sq(|cdSi0=Ydzi|H5V0mD78MhyaD1nOlTb$pD z@teGh6(J7=t6@J@4&s|q7ToObsgmaR0oHErUnqu`OSSo8@v>>l22E7_ z>))3@e!3Q9iK+>^IkfQ>Bh>v+ml(E852fi@y6&iafuK)R^N^dA!ox>eN*1nkZxi(> zHb|XVQYcg!hwu;^&-E)?LXPd@>8`QILnbXpR^#q#i1I4A5coGtuyN6?yi+#<^3aID zcU&ZQtF|E$WK>O}Mq)~!*V=-2^$?6`Li%$zTzupUw9Qnb!Rev2OpwF<`++d8Cn~rgYZ5N?iL(o@d)SgmhOG|ga&3%0xK!xS`oqD8E+(q-E9qwQXQsmd$H-)`+ zw~g5NNLe-Pkd`?&TRAM{0{rW2J_s;T#pB1#gJ}dNj3hs`AKc9>?Rc@JoCM{<3TeKh zGrO~iJT1QorsD#TUKxNNNg19yG|r8YY-=q&xpVF1e+{?M6Z@L7NP7q31pod@NgI#z zu#>naYD9~RH5@**Kc;UC!Vr*vuPak+XP5Owb-Vh2^l+eyJ3UmFnm5mz=t~(xM6bpX zLxcaRT!V)^dJ#Bv0uU|k~GBZRObp5&$zHuU`dvnLlE+P(`#MCe`31mVS#jEFb28;M`BpzJ< zDYc_zVU1whP5ANmQiq|0i`ZFoS{*H`si|q{L(PA^U64T9xDB(h#Wi3?WGgVOtvay3 zHDCPyiEz-YwuT1Ghks($m;dYJDms_Us|_+sOjuS^`jKn(aqQ}2Ow|9X)6)7ny)S5; zLM&b}LXwo#Y*tDA0C(VcIpp!lu$+z4a>wr9D2I2nS{w?<`-A10%f3`Aey;^JLi8m}VT zy!QyRa-()Y71h8qU3|$!`(F+)uB%mg912sMa+APX1pXW=!oMM|9xcs-FO+pcVC;Rh~z0}D(ViG zzgm##i_vv3WDng^0(3*QKZ#|TQ6Z0N$5FXFyH`8INagW{RxX5E=jB9?*`kQh)9X&! zzl&~4is)X^hAaTbiB#e$bcO9@J%{RCl=gd`U)aL?GafwW+hz}7E(Br6<(df(xoJ{V zC58#vm4$4aMs=!DT3)GkSxig}o#{9GWG67O&37olV+YkN97<&Vip>bzqv%`@&N3q( z{mvQb=P^?PR`IY{Q7EMJ>l-@bB12*jNz{F=t8>p7+oG|xY4foNOdyvut}8bgIC{X~ zx&>5_C`+~&fT*I8@r<2pIZ2@n4YbSR&Kc+tbDuGWH1Mk`V`>;0vfqew=ShV;SfMMW zB9UjmQAp>NjEo)l1LI1)FM^1uMHyrj;Lsu@1r&)xz2JOxllV72ir^`uUYWzSF6k7e zfjsDn?|`sL%%wd!uOx!?X|&7UQ*@N%2tQ<`#qJa16~@sjmI{Gl@n(R`o*$py!jTps zSOM(9G1Y2Q?|`Z4>B18fof%DJdgU&5^5@g2DjuLR&m^iRn3l(N`gn#Lc}SWZQcwZL z@}1!gXuVX39RJswK5G_x<~Kx7)xjPn!}qJK4Y-aYcN$ zOH-_U(VF%6Tx3=Lu0bmbfuNdh3ewmDqfzgE> zR1lo-68vA4KcQNpA`~a5p|Y0i<*SDaOlpucY|?^uPg$s%XT1K{eNs1S)jbsgJQn1^ z&zxEg_uSbImQKK4uVnuC5B13(i#Cg`nD3h_}V zU{q2mzO~=Z``$xG%70qOPxX_!Y!lHmhGj|i zb5(=Py*e`M(?i)FDaRfn5_Yf7{B*EnZ1^o!U?mj2`Q5h(b^f3v61s1W zrA+#ST?t}CS6hoW#|Wm#i1&8I9yS^D7(pT*+laQJbYtNoe_gNS{!;Kk1V|@tm#6O7 z%UqGZgYOfEf8qBUVA7r)KJzyBku1T7*1>whSWiw*StoqZOb*_-IgRE^56vx`Eg2X- z5T}C79s8cw&$Z1U%7dPnnYlK$?WH)qrZZzGn!JO|dF5GS+U|4Um_H_Bi0174u;<|5 zkRL`88N?eXwYY**=Z6?f#42Fr3)| zh^rp7-L(bjNSQNu_Wm8Oirv%VdrRb)TF^uKtR4V#+#63La(H1goF47aON3y>=W|@) z;o!JA48z2CmTK<*1B!Yh_3z^p$raRuZ{ze@e&`ob?>EfzOR1M!Tz6TtEZ1IOW-z3H z-iuTTM)JuUSEvfFP-BTopSa}aTHLX6$rMdH;S?93m8*o1oR*cV=LgSp_?>CgG?aJ|Ka#IyvyLzAW`qRPl!loi1;)?3jY%$8KO5njCh z@_)|nj*ChF;5BS>G^wCriusC^?0XoR+psA2g7T%fdE99s^w{K6mfi-VzY>bvjwfcJa{dn>Q?KG#pwtGvzfGYhXA^Z(YV!3BH zjW~KSE@(q;uyr$Mv|KI!_q}T`78x&2sQE;W^d*C$Y|u6H zTZe!YpMEToH-ynJL2pv|ajP0iXvskES$WiMV<%&?EsCXaql2=853lQnS_kY$r((nB{ WX8Oy%I)Y}20+i&`WNY5Q!u}tVQGC(> literal 4616 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 b*WWN4QtqF=Dsb+fjK7(Yff<&phmZOnXjx(N 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 9e593fcbd46695dca489df197dcde05b64211cce..583a485712bcc8691458c7abb38b37906ae6649a 100644 GIT binary patch delta 1549 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 literal 9733 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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 76d846c4a66adbeedbed97062f890cbc5832570e..f98ccf1f3eca7f764f5a066ae22f6b88fcf6653f 100644 GIT binary patch literal 2851 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 bn!n8-#h{tR4FTb;00000NkvXXu0mjfO=(GY diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index ebb105178..cd618dfc4 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -488,6 +488,14 @@ packages: url: "https://github.com/Kingtous/flutter_improved_scrolling" source: git version: "0.0.3" + flutter_launcher_icons: + dependency: "direct main" + description: + name: flutter_launcher_icons + sha256: ce0e501cfc258907842238e4ca605e74b7fd1cdf04b3b43e86c43f3e40a1592c + url: "https://pub.dev" + source: hosted + version: "0.11.0" flutter_lints: dependency: "direct dev" description: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 95449e611..8701d9f5b 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -90,6 +90,7 @@ dependencies: bot_toast: ^4.0.3 win32: any password_strength: ^0.2.0 + flutter_launcher_icons: ^0.11.0 dev_dependencies: @@ -101,21 +102,21 @@ dev_dependencies: flutter_lints: ^2.0.0 ffigen: ^7.2.4 -# rerun: flutter pub run flutter_launcher_icons:main -icons_launcher: +# rerun: flutter pub run flutter_launcher_icons +flutter_icons: image_path: "../res/icon.png" - platforms: - android: - enable: true - ios: - enable: true - windows: - enable: true - macos: - enable: true - image_path: "../res/mac-icon.png" - linux: - enable: true + remove_alpha_ios: true + android: true + ios: true + windows: + generate: true + macos: + image_path: "../res/mac-icon.png" + generate: true + linux: true + web: + generate: true + # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/flutter/web/icons/Icon-192.png b/flutter/web/icons/Icon-192.png index 5d4566850a8c402c3ca0565f20295e9466361d9e..e8c754f4ad2127a599473544ca40d6169fbe35f0 100644 GIT binary patch literal 8908 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%0D7IE-;TOZw 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 bECTt^{N{VZG)-Gz(;!_fgDbU{>?8jN?wrh+ literal 12570 zcmX9^2{_cx8=qafVTF~eq^$csqC#$wGbCru%26SAk!!7MD>-wE6q1BU$h9JoTT1R# zjwEd4KK4I;e~*XnGxN^-zVpsI@65dK=e~)N4kLyO0{~#u)73Hs00}>lfbJ0d_b*^@ z8vvOpJuMBhfRR7rG$sFT9R2vW@?E!=$LQ3^JC%^ScOIkgm*YoOK}Kb->%zpPQL0|( z%~;pM0F`&`Pya0GnXeK)H{AJd)A1r^;qgv$O(R$1VYPo7K{DX!^H65^%HZVO{Z-%hn1weV=E}CoJd3~cx zzfoi`0s%zu7iz4}^h(J_D-Qe=-|9meFVO(9<&3JXQjl_DzwVOuw&&P&05H<4N+B^7 z9ds3y!WJtTf`6810a?vimA{nr{^8UHm8PUgrbFOq>%{Wt7sZ}1<`az@>(CrxzwDiJOjGo87g>|l;7UIIpi0kBbflwx3EQ~?fMYFrM9e?Qt)**) z767W~#>7napR`~j$mnRE+)ksMLE zG&KlLi*&T^MQ`0(;`qKing-xc)-@=!Aaw4z&orj;FamsG&3h~>O_uT~4**W8&qp2h zMUMh-+HzxwH6f5ukji-&fXvd;u7x``onhgrp34jXgfg!B8+3+!-j6v3Y2S{0I!C(f z7u*&$Iy%DTqaodIGW;S(2WyFB-R_vWl%LvGD+W+&yW- z{C9#|T~O)1s~N_ZZA@29?W!iAD(kdXg(rk}8;-+q@7qR4hJRL7EKvIn2%UX+`ij%2 z5_WY5I&Ye1xSCdeS<~4?0(_^Fe`B<+P-QW&oagaeY=cHoijT zJTTse_v+Ul(Rn)-Zw z{*=qX?zwQF(v$b=YyVo@_B>gk-uhyz94$MbMiz>C{H*|fZVLSONz0`i(uNX&VWMED z&pziiU@p<%k*90nB z6uVO}{eQCQP;32tzW>NvO;_X(q{ucVC52(2h?Kt(k9T$cqo4d5X6y(}`+3ut4FT|& zXV7cRdcX5Uz7=47BrJLM)!X4`13jI(N)m#v&&Hf?aS{tD+{$h6#V1`KZSt~%acyDv zorl~zp;Y@nL2{=&;h#4iU53huE=k_Y)N`-)?*)#_jjPP;H&!(|7u+&--gtSr$@kHo zC-qlL>RQkF)U|fWRR6UZ4uGPc8$t1@4qYl%Grv+}BUWZ+D|US#`^3P;M2b`?`;)Mp z?nedDU-G+S3akvh6}_%xzA?Y0=CHjaGhaIEbSTjh5PAQN92$_1*4<0h-x5xdbz|0D z{&#reOili~fAM+)!$%4}_BHEtoV8V@&NJ708g8qadn#1%JmdfA`(xOKU0zRidM2Zt$h6264QL! zg{fMpCLZx8;wcO1ZnrJ08kJ4n7oQB&@_PetKc0L1xx4jnL!UimC00@=pO&Wa@NEmR zZRr&0^bddcUFPMle^q6nYU_m0gjw`J}fKla353j==4_~&~vrU-df++B)^%6&xFwkD_T zw7?P-m*lgP6Xu%k=R6+Up~Qy%wvUs>!r< zZp-}i$`-0y-8!D{{??CYfBoj;T=F{qlQ)rS`T{+WaZNkv!;!ETsLUxPXZ8oDT^!nr zss@gapHB>g>dPi=G?@2Odpjt8LDbS!gC{AMx+Xcg;8!uHeY5tY|g?=5Q7W<60i|m){YS%^d&KZHVY~Xu&}PIxGlX zoGgwl{E4KNhQ)L15yn{Wu+(Mq;g<{$>MpBU##;jI_KTAtGFd;#R2heiU9TGP%UqCc zdy)O>tIB#)WR~^lZXFN99r_>TTjh#E@7snrq4(TiYI>=5W@0Y5{8nz*hV$xG4fd{a z-i86e*KQA?(n1>8VS`Yu6w1a#T8`D31uebbD|xsR9Gp7c0#f*QiUE zylZiKpT+XmS6Oo=@v!gor?>0Y*LcP;9o{dQeHY4##C0QflU<@g*c|NLt-kKu%~Fky zBCyrw?9-Bys`@CglWy)6ZTxS2Uo|xU1qr=kq@VkCS@@bC6m4B>U+F-Warif- zMKa!CsZmfO8P?Zl8Xr1^fr9@~q{Y~4NJF$ip=#ZyTFK4BNS-}QB6SJcvuioGUXRQ- z`Sjjm~>0m__s{WlPablAy0Mf zg=}}Pc&O^YPSCD12G4vwSJ4?uV-FGpsn(;YlPg)Lawc6X|6+qjaqZ%W!&d*f)LZIf z()E3Xp0pt)GQWXONIJOxHy_~kZ%a(ByP6_4-5tVvxy6$j`zBvB1;UO@#o6W8N1Ut> zn`S&3FhmbWR7sW2cppkY;_H_kYHY}IVU#?OJ|BVQ7IJs=!rn6j*xUt?nH($%KN-Ka zzaLhThI1@Y!i|4w1o`0Us1m7{jR9!^6c_+gPNMz%j}Izv@vc1))!1p|36=krCasEl z%9g6@aALZz;R3Ynu+yMN9uB|j9=Q=1@VpL3GBeYsX8$1_boxXt7gZ7PD-CxWdRr?` zpm?7yADK@YxjA+ns%}4u?+9&f%bl5^twixQbGP|BkFzaF^Kl}U-8rIHccv;s8hYC# zFQRy_?HgU5O|GeioBMPw>*6^fI_3TBV`18M8tv`F{NsJ)4|bsQl42@0+tdu}zYoV} z%WNygmP~ja3iw!?3r7W)NYZXcJ_@?BnViBphvIdzLD14qp-lk=2IBB91e2-BLuK{J zgoJArn+EemLm-y#KlStd85^*~iw2%!(Eqr0pkiZi_^Bne8suMpmkm} zonR8MUd1LvrN3_znFZ2H$JC6bTz7_b( z;6!Ri;-x}ap%7QAqW^>}GgVU4dCzex^V?k+rMfp%>tnC>r0IFyVV?1276qI4+4m?2aXLd z33L?!Wq$s9 zPdLSAvt%$d^et_v@^H_-{7FeRt;i&w`KsHE)AW$Tev3k&Jyx@ed#}-;tF621T?n2dVz}^npe@}<6+8{p~Ly$M7+zFtee%Gh@L#vdi``Xwi$`ycBQ)?^!*mtDAFzSl0X!~bg?3i$ zIS=E;_T$sL?hZHC?`Qx;JBjhh2eIeG(4fpyWKm6!4lvx&%UzheD@2Jhnw>USZ$T~p zay(mqK(Qmive`94v-^ohi|2+QqkWHtmd1~?=j{djP|4RPLvvPWcZ~F!j*HuUFb#Ve zBGb7q)bPxdEcfYQ{%B@W2bbQ#zNlE3?2Y-ZCOrCOq5WEokwA8ZHCZkqE&1a?_gP=` z$;+2S{r61X=Ux52+TzmuS_h@t)3W)isU5n>mtDb@L&9oz)|*Do=qvsxa<6>g``(I7 zlg0cZ_#3oT-=AmQ!~7JYZ-HsU%isY{^W_^tjCS*%T;KLNa%~yt!JQ;vLuTPwO3k^E zMqjJB%C0k^7yfDPs&bcK*K|H0`!nXBcST8Ew)!xqpRv91^)CG@guSdkL#=^62Hd%V zqmZ-qKaINh^iYx;3dUHG@tcMYvX%ufmKCcUr;bfzjPb`9?@*{h?r~SdDfl(of`_ce zbZ`UojPrW^t=lhS;=8_?yYHQCW-H~Dk?UiHMFZB8>oihV8;feH1&_59_i#A=N*$ff zeO&6awm4Y;o?3H_dYO=s5vc}gtCF%pwHlPYZY1YR`IPLvEV!;8b;B8!cWk=&Y|?)p zW=cL@-FO6ld9J`FGeHg>-`z>+)?75SqGBnHry||2ZiiCsc8=fi%DK&;-4@FRN|dz8 zxE*6X={rJ}R=$(Jq}bUo+=Xv757RyaZr8qrA zl5U2Pv9C5(Z?lZ|KmPnE2E}A*!K;#fgGOGz60 zRU+qCwGbqWN;FfsV;bB@E0IfXKJUD$NzD<2CHGWs#b%Q(uSC~GE`99S9unWgEvU=7Ix?NXXv%C$h;R1vYC#38%NVbXOvpt3OI;F#gJ5?RPf;UU0{8 z{Uhh$d9Tc5)$5r=JSUXRF60!kgWz{Goe}fp0+#RVnYNm}9vz+t(o7I)a;%cR1-G3v zd%kzc@(EsU)(4xnEYG3~J1gpmGvP=QIrqa64NQ8>-0|@ars$!`c=r5`%ZvO;G|mb$Nk*p1mt8;T32KVAjdr?2;7j7tn>*F(6;Nr=tf;~>o6q3Xvp%;L`5k+Gv?~- z49(w9!vq0EyRL9YUNP`pPA37CyOf-?6<=K`?CPQa zF}K5zl<&$ENUwn)-Mxn?&XW~V8A>d{^^A(@66%wk1W{<96oxB`PIPOhzjkXUik!hT zEl5U#4Q@^|4ef$|PD(z68>f3FA$gb~pEv@#gsH2>nZs9%jIdPWporsUy|J&I1W9OM z)6HR~aWm?l35TLaeV=n6;noY}%CHxn0o*<(P()BQXs5fT+;qka`>huNk93pNr~(qFYcHApsO_K7Wc)op>0FvzU>* znE%5^gZ;6&?$DsHI6@2r-1G>tKhx^k9V$ymj~#x6zB)pyjv&Ra9WiS*hRL>Pa=k~| zGtV0#%|Q8@tp<`QXG%c2)ojd1%LL7{B*YI{)ZEI&Av}hw9{wV`sEbIPa=4g~jAsL9 zm(1Ew-dF!fUn`y%GJWt7Wlk{*@;QQs=P-|iE@?%1CH&W}llta7o$QfcNYZ>5<&*a` z&8ZU$hAd_p8&OUI01Ygs8IE-AH5JfrsT+Qxtqfo^VSl)h#FEPZGQ-)N*$E1R%TxzbXfaTUNH=Ber;cN?_I!=B_ltzD+)` zypxg!_q9{=OS987{q$k-776%grj`1d7lK8=X0p2mx5acaqt*);KAvKhZfYDfe}^}4 zri(5Kj9S9oD!jTR;d{xF^BkCr#`rHSXqMV)B+B|UHQ z>8j5$w;wb&$r)l}JQ)R#6K~(E*Tu&C`Rdg29Erc^iupm&6oyO+;blCuhbIq^gI=M$ zYf2Hr>iIJMR6W+c=88<^_7os}_*?U!%Q$t?&-3`;7+KV2)7qIaj895C<>H z*1YD^P=SvaCugXmG_wGY6erkSyP?4ydM48ycW==f4 z*jn@Y{VJv&bJ}3jY|5%|OgaxG00rOT3Rmutr8SxD@A@N6F(5Yds zKr|?=9Yao906;c-gZ8e;bgjH@0+N`HnYBs0Dj!4wW)X=uq^~@#UN6yl(TDZLAXDNq zS%x28Yt9@2c0dG}nBGRvAGrbQ@k7o*!m@+Gv3i-K>%7Efqy6p!s}o@Hch6X zZ4n7}9X4`pjjjOY^Be+4br0Ed0(%6>`LrjXWawLy{Si+A$=N*?11C}b;phV!5yv6ncsSdP~x ze<;K74LbCI9cfSA8jwdZAp05EtVF7}lbf?70fj76p=6C9c8b~~YU}{HRS`M>?qGD+ zXbs_oZ$fDHCAcD#K1mL+`EHl_g($eajKH%>$O&>A`HWU^fSPs{d8>q*2ll}LSU(`Y z8I_=$5dUx0DDYxOGCx`O71?9~xp>vL6Hv;5b!gC70!UMp913k}g1g4cx1@AQk&G~+ zT)caNwRLJbuwtsnHgeP6UZ*$~rjQnnZmIiqcK+5q2zVuGy8rD@N-U1)O}bymc^E~b zF0ldP~maA}x z9U~j^hJZQ-D2G*}ffNOU#_?_mOHv8}DO?Vce%<+xR0Plu_hQs#W@cYT@EHCF^KmPJ*AUXc<@eqy9`DWn|U*07!$E(pqF6v0?;seoMTY?-w;op_TCo%AlC~V;f zs6*muQt54Z=pu=;p7cjnf4a;5_=^GV;^pvETPs2XCW_vEYO#}?IA|V0=_2rD%GVM2 zS7;!1tv+sTiXBW$>YJcbPR;kYOrzcuzKG{Y!5k@{p*!|>N(NV7lxk+hS441#;}a&H z`Fp(qWiE5?#Ie6unSri^iuotYoOj^qq2QFJJjX`r96MNd5Gz7fYWir~`4G}emEtkQ z%K}BwF*eDpxo|rbhj5s++_f(UIL5~P612J3yVi;o zqbuZIaZsOsR>|+xPro;Q<|-A0^kO$=x?;yACfOgW;=ndZyeNA4FuySI<)*s0CR9 zBOJZ`!UYTLtCh@cm_OGzv+IYlrZ-18;;{AA4@z(r1&V4k#dF~nPJ%f2zBnyblrw-a zqSGq*JOl48--8I*s1o2fp{H@=ZMwDv_KVsUb9AYII6@pKI7Q*;F_;2zMI7B+c) zGLl=Ru~ste`{OAGu&s1R5SW|^2E1G_o#B}f_4}q`Rz2!1a9p(}2n+|On**)uz1$i} z^c) zC*}tOH87{sQ>~9_?5>+=U<^l&c5s^jg%eneL4J^pi;-#qD|P+Jk$e)oWnF|<&(NEA zoc4v%j#zixopfp4@E<<&+tJfP0?@oY>-?^0(UtoX{XOU(KRpVPeEz#L-`2DMe?I(Kk)(MH@y^b{%W>{CHkIGfg=lM50D-g)@AA7XxV`39?aY6Cfh%3$42`KY{FDZVf=mpaQrz>;G)PG z9d3=yq(SB`9#51o0hD)zhScKu7vW`oygEmj!c?x9(A+ucyz0=30dcncMTkJ*0-LM^ z{=@s;ZhSV;B@&6BJSOCYA6ZToJRCRsb#S%?NvpaSRO7GZtBD{zbknnm zgQOBdd)EX4HK}Lj2Fpf-z3_={d^w!(&2{T72?3My(o8txU%8fFlU2tgy@*3~+RAcl zpuRrtAdQT8pkE;WK3VkNgxha})jJsOX4}rg9ZYCIJ*7v+!c57WQljWTqe_F2zxLq~ z^0yR0Vn7L;SWHcLnv&_5l18Ncfb%w2ReevWd@XwOs2xB@7>tSF|-S9n9* zM20n8Kkej*awr%&8{R9ue#jL*@)Psw;u&nMEZk2$49=6wT}(BnvvnLCIo!($Lr%7& z*L_5%VF%y!bR)&PJGu^q`zrlLPLGfzKaxK!O6tvxGR3%z?5isiUo@{IiF+mHiEDp@ zDTmz^_3(#(6vR{}QnRnFg}#qBGZFE+o696er5eNPj_IIBPpYn3hmh;hNdvdhoeN@O zXsIuWtSZuvktFLM?q^pB8y0#)XK|jJVXBXvo5YYCq9U4XWiH@&}57@2>Yr!d>2FJabOX zy4*EigHz4S#3+esar_TOVeShS$hL&hsr8B&As6Kx2r`skS(2@<1`Vr4fC_@7!7r+dduzKxOFMdcnR5zS6+%4}td-c)sR6hc zbo0yA^|=V?Q|5iE{SAwAqjjCU0omd+6iFRuOyU^=sFZcsZc&)cuB(01%QmHVJYrvw zXZQM%Kz}m~m>9Df3Dd_C9vQQqO}9inMhFL&ydJ!BvaI=oX3xpgeLGmf;aUFZU2yeT zzHMfs`S#Zv^Ql79^46Z7x5^5q|4fOemrL%cjkHzS!ZQXwYBY5N3r>5!P+^;aC;6r( z%*yYDmegK&JXXFkbiZ^CB?^KU>S4BcWf=KzeT2Cy>$vN!)Qc>#QAFy$=cMV2!6z3o z*g%}Dik$?p{Y|S&#N3N*r#6~@(b4PDd)X?NhP!BkjCJMDtVo=PS=;3%apKYA%zA2E zC%fgoO#YE#bnx&@iu`4Ag|}gL&wB)tGW??7*a3 zS&8`T327q5J$&f=_QK~shjQO*W3Nt3{t434#(>NHah&vg`q~A9K6h&D8Fqcx4EFjR z3NY>24Eez8mgC^vOD$t3W_LCn=lXWJ{$%jC;~%|}zDsYo1oPOB3K{r%U;r+iZSHoQ zV@4e#@wEM|hmn&U8gg+&gs3oz$yY)u=(Yzpfc_V~NhDR8L?r2N(I^9o#dV^`vD>Dp zt~NxLPYc#5^9>^EIALvhQ%h;SRZ*k$rTiI%jD>15toKjnbUMy=O~J zr->$v0`*@tBhX`1LV4y{c@$n*G^a^;~**sGacbbE4GQ4q^uUvG)Mr24J1 z$$c**3HR)DJfUa)#{iCheKGIqFz=U{1rvSnoG@$j3^Yc<7t z6;E)Q5}nDV9@wq_x*jd9d&C2F<=onla&!N$9;@BcAm*jx0@#Txk-ZzgAP zz=E2~0?U2sZWN4zwrcai8MsJp=@nr)J2ke*$6t2}My=L$p8tPyw2@>?uS^-+AClyZ zOo*y0SWx`N31c9h=Q%1esdFJ9H|5O&n)mv)MdRwHTlv5QX~fB(*(!OlzPID8BmsNs z-V;A-e{B}XtoKgBdhU_PNZKox0PfUsX_A!lJ2fG3G~oGs?OTw7OjP$yu;H)-9b$3L zx0t7pmlM(LS6)y{z^}U8#eiLghR+3i;AsP z%0GU*)~^@pxA_qfEOhj=>cV-1qqd0EVXHRR&|ey`?ppd>CE_HZd2S~tH(b>Kg?ReR zbbC~bc^MXRvE7sm<~#IA5+}^FoZzGjo^7`dvj{VR73t0eih&GW@J;;2M&qzyczoAb zTe#{`n0wlMo%e@qZ$D=!)PekEV{cYu_TjOm2n~n}h_6mL%gwe)vZM_RYYJ1d zAfIwk-q5*sh_cKBdo91E+?%FTo^?}Y!}GW@DDSr}$GITF4#DvBcaFcnw*XcG+OWOL zZ$wX!VWMT$pJ8?Plosrq`exd5GMqN}OAL}SuZm1NRW@k9bmT)**g=(;U-!6ib=XJs zw;0mGI7;f$72Y!$&5k^=(CZJ;zE;8!`*2TP&%r|%9#RE8EZaM<=KF))f3dPdTnx>$ zdWy#=H0Wsx31Xub=T=|w@Y@3HD6iWxjK4yG|{X|(q; zSd`4xdxzp0rXwL#gm4M?rY3BVHrP|{3ZPwix{?Y#*#{G1Dt2qbvU6QXejF z!-cvbG(3~bmfvw^ZLii7W=l<-oE!gH#8Vrh|HF51>3YoReVie%iAeli&hZe(3Uq{j zftAl=zar=h0B&N`^mo&sy-?Ke;gGSWpejCxgjV@I6(7Uq8PDTTH$CP|>$jFBj66&0 z%O|@hMT>r@gstC_W0CtmMgm@?Z_ZuPKyE|k6NaOF#8n>ZFaLqd!QW!eq(R+!RH;ic zH-o~-$bIGJrXAVP2-Mj(>8Je}F7Nj7ivnF1JOm*uht^l?BX^1>EEV3;LYWhX^I(C@ zp7H2y&)<_FznUI9?CySwSB3Vk&wtmz2dfXqI^OVwm4&t3{Hy>kZjA;3pW?$0d{&<` zNbh}pk~FAa8D{!?w5#tqM`gZECQ6nHZK36>ZwDbVavvWkH*jo)HOTW=K;ebR3}Zm~2dyVE_^#FG!qym)0WnFYU>7z%!B z_m4#0aRp4tweP~sOpZQU$f(rW?mjvn9er$XgJ5+PLkg5aUFZ=1tv?xLnOSwQxX7%? zI+ZhfVSDI4MzStWuiN$18|J^%z#caI7hAg6;@Q<&wb*2iKN#0zn~UjW+k*^)+q(Ga zY-Q;+8?lBj$t{7{p8*4G_~*8CU8rSum!?kp8YS)+EOae9d*OgG>z*`p5GNJyJTP=f z&+?n#D$zWkl?`tzg>uQfd!~Al#`rt7OPW58L%-Mm@NCu2ex0cVNA}%q7xQN`?v)ww z4%NgJkRgD1VBMdmJF z;XJ0ZxWL+$u9dnvk-ARzBfK-;13huuZ0Doy{121e2WN)bvxX1_?CXjBuU!msm-y|> z<74^Gk^L%>G6aTjtF;RsJed;TOFp)kRZ@$RagE>m`k|nzWov%sLGYYISo+q^qZbCb zzxnO#au4&pCkLgjG5b$x;)Ke!(A!t!gJMFLKd!UYRtoChZxGJEv-+ccjyZ3(VW&D~ z#NkTowU<;^HP@KE?U|UC0-ezWX)WWD0;5@W(qzq1;u)$jCGp$3e)FWT-U-g{muK5b zt53Wgn@O#J%~MTGM&*v70wbLoRY%q?WBOAuN(44!2OM z-xcV(jS^4Cexv(TeEB*Hz{mY-MxHQn-FJh}A*pC@6AE*ys~82vs^jDfYNs@#y$+0KJ?s z{C3MgRqCLz3(J%D)vVherW;AFSKKmO&35zwz?V_wzkYr(CdJ3cSFHOg)T3;@AF?BV zHE$BO^0$0IBoaU8`d59cWtq_EXgUCn4s3fgyKUb~2@Zq`iM%$bnwU{l?^tt$h|F{T zjB6SrPm_2KfV>61)n{v_f3hjX& z&ZD34IZEO#&utt4T-Tmr>{iC0+I@tp zYLcq5?^#bK-9{m(M2YrN7Dw=+!+oK^FI7^!kil%g zW~7}dRR6ibQ5%sBT|p`r97VtPtgGd$Q!x+Y+LC^b<+E-G^&f0uWfR+(d2Iy%UX_aa^ G68;CvQ0u<{ diff --git a/flutter/web/icons/Icon-maskable-192.png b/flutter/web/icons/Icon-maskable-192.png index 30147e96ef31e6bf72170d69837eec2f42d4b8d4..e8c754f4ad2127a599473544ca40d6169fbe35f0 100644 GIT binary patch literal 8908 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 IM6N<$f~u99dH?_b diff --git a/flutter/web/icons/Icon-maskable-512.png b/flutter/web/icons/Icon-maskable-512.png index e84ca5bc7a212951765d3993264a50517b8fe82f..2f8929e267b44bfe9325ba479ff131d16e2586de 100644 GIT binary patch literal 25973 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%0D7IE-;TOZw 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 bECTt^{N{VZG)-Gz(;!_fgDbU{>?8jN?wrh+ literal 12626 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 literal 21592 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 diff --git a/res/128x128.png b/res/128x128.png new file mode 120000 index 000000000..f69b60eb2 --- /dev/null +++ b/res/128x128.png @@ -0,0 +1 @@ +../flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png \ No newline at end of file diff --git a/res/128x128@2x.png b/res/128x128@2x.png index d6f8d20fa045eee8541c3912b49428b68af98324..9bccf65bbc430a5661e689143c553bd2e223dee6 100644 GIT binary patch literal 10623 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 C;{ApI diff --git a/res/32x32.png b/res/32x32.png deleted file mode 100644 index 33dc805376406f8e5ee596dc1484350b1eeffe8b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 493 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dy69RlfT!A!0;eT{g@L#6%|Kzp* zFW>op=)(VwrT-s1`@iet|AgNEO^g1&`S2eIzW(^nko$k{ng4(O{y%>Cze&~q<$L}I zwf&#I;s1|c|37~H|MJ~`*T(;wkNlS}`~Udm|2dog*UbArW5a))ivJ6@{eSxU|GNGE zBRc;V&G_%z^8f6O|8k}O`&a!}yk`9n=p^=%AirP+l~wQmzg^ir;ie)310$=ai(^Oy zGMa`n|T{tWZ_4Bqe#( zj91f_ykC6Pi_=9iGSJl1+-J3!tinyc9rO4dC3+hz7O*@}ah|}GvTwo@^Pf z=Uf+t*$+fFOtw0}w$8MI<-?nA52rZFh3d|5urlI5fBpV<69)5FjqThq_tX{N`8Rw{ z(EY-YpnjCW#y#xwT*jHp8C%v1D(ILheD~3~7xZ)0_NPm3Ulcp}Q}65cJJ$=;qYj;v hTh_$uerzE>1B1vdse~7o*t$XS=IQF^vd$@?2>_Jq`C|Y8 diff --git a/res/32x32.png b/res/32x32.png new file mode 120000 index 000000000..7c1136a73 --- /dev/null +++ b/res/32x32.png @@ -0,0 +1 @@ +../flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png \ No newline at end of file diff --git a/res/64x64.png b/res/64x64.png deleted file mode 100644 index d93638e6eb1d82b24807d7b3d0a6ec5c72e0bd86..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2264 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 GA^#74rn9U7 diff --git a/res/icon.ico b/res/icon.ico new file mode 120000 index 000000000..75324b38c --- /dev/null +++ b/res/icon.ico @@ -0,0 +1 @@ +../flutter/windows/runner/resources/app_icon.ico \ No newline at end of file diff --git a/res/icon.png b/res/icon.png index 823967c49a8f3440fbb26e2bd05c96c78b939141..2575d80e7bb285054ef964780d77b4a9a90d1491 100644 GIT binary patch literal 60426 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 diff --git a/res/logo-header.svg b/res/logo-header.svg index 40c19c43c..9712636bf 100644 --- a/res/logo-header.svg +++ b/res/logo-header.svg @@ -1 +1 @@ -RUSTDESKYour remote desktop \ No newline at end of file + \ No newline at end of file diff --git a/res/mac-icon.png b/res/mac-icon.png index a9813152ef248d07076f956a3642575c396c0811..b6e08923fd8fde70dd059b76976ddc32c35749db 100644 GIT binary patch literal 51695 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%0D}t}!A64# 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 Te#RJt2VrvD;u!IW+pYft1QtW! literal 90116 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 z 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 literal 809 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sivoN?T!Hi;2MYvMt$SQP;xz=dFx){*RWnv{mVKkiQ?V zdVcyc-^*+j$Ib_rS82OFyzp9feZq3v{D;kfhVv2+>fJLr{ag7;#nI{AvrgVvIbFS2 zC$N>J^Ii4ZFM>w=?Cg?T*RVxunlQXQ@TF!#&B9JCAKAoPszr4oCEU?1O)uML`iiUw zPONeEJoW1!3-6u3k+bz@cB>^9Nhm%(>h{7{M)5M2W3iK3^{Hde7EF-HoPBuniapDs zdU9&*Q$5yZ`%ke=vu9-5xmqh*#cARtIgQceD8YNW`njxgN@xNA D=o26P diff --git a/res/mac-tray-dark.png b/res/mac-tray-dark.png index ba8ed8c12cf19a82e8109f4d7b46e10b9afcff99..a98fe63b0930e9e9358059dd6661e1eaadfd73fe 100644 GIT binary patch delta 521 zcmV+k0`~or0+$4k8Gi-<001BJ|6u?C010qNS#tmY2^9bU2^9epf*r&F000?uMObuG zZ)S9NVRB^vcXxL#X>MzCV_|S*E^l&Yo9;Xs0004`Nkl*YzRL{|`7)pTLb9a9uA|C+U{jP{?nc1_d zs;V?igR0&v%YX8nWLefTv#(Lx$7c3;JRYAdYaxWNWoAb}$KAVXW;+plz6u6tn&tq= z09Dl;1@1-p*Q?-}zD4K0a`9En?)qsBjH-T){F@3$l=pv;MY;QeU%40(07T^Wx&pwR z$p1ru_faxmMTQV^;Azy&K+S9)FyL%9!b_E9*>y8}3x8ahg-lgniMx}g>4PLmPK%=W z8A8aLrgx?$?0dB2u{fplO1xTM*()12p#&C!%B_r;BAbp-{@684td9^#&Z%%wh1F4GQTUQP1&+u7*cRb27$&an8u6*ezg~ zZwgT~*TP5^d^Jok+S>?jbs+4y;UMVdxM%*GW4hn&*ry9n1R+;vOED-50ssI207*qo IM6N<$f_evhlK=n! diff --git a/res/mac-tray-light-x2.png b/res/mac-tray-light-x2.png index f723d980e594a0042a9a94aac6aef9127ec3bf8c..253450ecbc102a345187896f2b65ab1900d8fcac 100644 GIT binary patch delta 1184 zcmV;R1Yi5A2B`^<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!3pV0000rPkH?m}pC4S`E^%>RVX83C>l269ubw}>Eh{EOljH8WiLK=cY6?qh?G5?4 zdUO1`oXWiCi7_xRF?qT;hIkzBopyWLDg&N2XGg)0I$B*y71t!^9==!kKU~_H$8+(8 zr{$rW{+Z;?TEo!qlKIB(e3j^zz3(2fl+F6gc2DxfS02;bfpPlzyc-T&j@J|OY4h7! zV{=?+sj5fGJ?-uM6L(r$oLKR+c*0aCW(}Dcc`TFkO#*LS5nJ{);$44J-?mnb8Fe#m zx-?p4J(l)Ly7pZyN}xgF7Twf^~iz^mV3Ir zv{xNHaqYWv+m&6PTyG|SlRhwgdsSMmi$c(?Rw+G|n|Jq%l`P=NdSU8ymhHg882ts) zSiJo{%)h4Vy?DZl6RZ!KS+!dIXMFC|DX}#CTU5!wJwN7n$#W4Mo{78W8f|0aR@QPj zFkPna$pJw(ec4Q=xSci3+^2&?i>5tGP<&R^8((EHKR&x(^?FsU!=mQEyZ>{-A2jU_ zoOLI9uH7e(#}cVc-YXK@)6#MzCV_|S*E^l&Yo9;Xs0003jNkl#UYIJ7F_P{lv99e)&;`Z@GmJYH&Ve-KDQ z@=J1Zf=Wi_)m`;Q{ZPNuJ9SU3W#eOGJOuUpAK&WQAcCTLG9)p@qdFNA6zF3csPl_1 za0HwOdl@JLTfi$Y1=N7!bmqA2Yqd1sZBi+#&mm`)Fn5agoMhCqD87*ZD^cr*z$3$P zAL;c3C`FEooPT71%SbOJz*yvP1cE>D3DAigiz5&$Mfy_$oJNjHPI3j|bip%rV7MRU2wc{ZzX_-%;nX@js(a3259AKP&(M00{s|MNUMnLSTaLAEsOY delta 253 zcmV+_A(1OFPEseh85xH83Ft7_ zLY;umhDDCiUIDf`0C;A2l<4NTr~a8^x?gVFS2&vj^r{wZl8^AF00000NkvXXu0mjf D1{!dZ diff --git a/res/tray-icon.ico b/res/tray-icon.ico index fd2e61628a2ae248de18c5492c90badad1025aa2..df8bdaccbd9df0c34ca00bb736ba2361f94e1705 100644 GIT binary patch literal 4286 zcmchb`)^c56vwA6G0_-bAi-*46MljCqpxUu(4e3ajs6uz{lUZ-P;4yKVnMVKCB)D+ z_z08`H3$_bExX$^fFRN;E%>CSw7c7FugmV;*L{9HbGLUpclXY27t_w>v@>(gna`a$ zvvcMyV>R??Xkh#=J8&Cg%Nb*sW_A>H6DdC7M$2Edi)sU9FvNfxT`u}_k0 zAaS|;e;dhSmsSFD_6v&*7I=Iefo=k_t+`ed)C3)$Y8$3U~DF7V$hozv=)Zk`wZEw44F2P?KQ|9SOY71 z&P(!dCH%XE-pB~-jdk;6Go*Jgq}mzOowdk@UIR=`80 zcx#>a!{~n##`(`kc0xP(Ix=6ZhAPb4Wyd=h{_Uc5IP;2UJZa;fGZgP7uv+VZ*k!L- zMxp)vr?*dN8RI9A+P4bDY-KNl@>MO2=^q7a|Ek@wer~U|t$4 za)=>v>{h9xzV+sk^W6OPfAIg>4Uru_%n&|ylbqjvcTp~(95eWi)V`8Jy|~F)b3Sj* zp6sC*gjP!3^!@9FJeEIGGy5Nw+E-F2!G|ESCyp{qpMP5FW(>SLn?t?lxfwkx^^qye z^jHb|YA@xav28LJ*wdqwzrAdp$I=HMp!|`Q`uQm=O+obdoz9x`d2{yFK8k1RlAn2@ zFWdciK7SYGl63D&u&aCqJu!rE-_im*HvCM1Bdb{9)0D?*3i-Q`OZG4%zNfqwljjgC zGXdN1UTO5?5GF&*FdnFbmXQ03&yx+j?A#qhHqW8%d4BEagGdd(jqKlD$Xw}!I?@Cs z^oSGhsT1`u)KPIbSb7Xbx{7OQURusc{$q^z79MVDUyXMeC!|zf9G|$A;#*q O@Sg}4xo~EP*#80bVj{T! literal 4286 zcmc&&T}V_x6rQcZM%El76HzThvO6B<#UW z`%x3pD2fc%wZup(BTQ06=^-+~ptZtE{hG7G=$$+FuCvOzv&@}4=g#+i=bV|HyR(dW z_$MKO(Uq0WVQc|oYz42}#1eTWd${mtn>?Zl!^jn0mc(m-*FIhmqe|R0NMYMXlo{hT zUyStJ<49o_$Oojd?Gvso^UL$VwQJ+1PjeB?d}k~K%J&0xdBCOBBDbgJiGGzA@GW=G zw~SeBe;CD{wG?=J4;UK6aCviEXjL2U)>zL4@EoFd|TN+zXqe=xGDeXPI*}m8f{?U0!2`FG$p}<@?OAGM3B)z8-kqt>x(%GY25XS zp}pN&zPvZX+4UO=tt>!iy&2E_11hdR1f)$j!&-fyd|Qj$udQR8?&;lOFSHjrjdin~ zo(~NEQ(2y!Gw1mBNyXCKrA;%(*X65t>gBID$3Hl%;~Q_8?SI8P^>@M77ISUNDaG2` zrTftO1h}}&nd`(%pgL2}JFygQb;h{OAK9;MaOR@@i1yd>iv?%;Y=GWzzrXALl7;dX zNhi{?(!F{vrAkw=;Jv#?TJ*N98aO^rVD0uDPS2>dQ+wp44+|ZnV zs8ru6&1FKr;-FL%ZEq(~lsvwerLMirUMbdCR|9Zkv(TX5rX4{(&wg&(!hdf~idH+8 z=dICv3GuZV9P|3L=O_ApfS(1E Date: Tue, 7 Feb 2023 21:14:01 +0800 Subject: [PATCH 1779/2015] add design.svg --- res/design.svg | 374 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 374 insertions(+) create mode 100644 res/design.svg diff --git a/res/design.svg b/res/design.svg new file mode 100644 index 000000000..62e568242 --- /dev/null +++ b/res/design.svg @@ -0,0 +1,374 @@ + +rustdesk From 7820c890c569214fffbebc9382fd7adf35389cf1 Mon Sep 17 00:00:00 2001 From: Colin Delahunty <72827203+colin99d@users.noreply.github.com> Date: Tue, 7 Feb 2023 13:32:42 -0500 Subject: [PATCH 1780/2015] Small fix to README wording --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bc9bacf19..62950846f 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ RustDesk welcomes contribution from everyone. See [`docs/CONTRIBUTING.md`](docs/ ## Free Public Servers -Below are the servers you are using for free, it may change along the time. If you are not close to one of these, your network may be slow. +Below are the servers you are using for free, it may change over time. If you are not close to one of these, your network may be slow. | Location | Vendor | Specification | | --------- | ------------- | ------------------ | | Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM | From 9527d3b41d9408ea377084b1c5e3a018f0289f65 Mon Sep 17 00:00:00 2001 From: Colin Delahunty <72827203+colin99d@users.noreply.github.com> Date: Tue, 7 Feb 2023 13:33:38 -0500 Subject: [PATCH 1781/2015] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 62950846f..866063726 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ RustDesk welcomes contribution from everyone. See [`docs/CONTRIBUTING.md`](docs/ ## Free Public Servers -Below are the servers you are using for free, it may change over time. If you are not close to one of these, your network may be slow. +Below are the servers you are using for free, they may change over time. If you are not close to one of these, your network may be slow. | Location | Vendor | Specification | | --------- | ------------- | ------------------ | | Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM | From cf121bdf47550df9725b604e340e1fa4491cafd4 Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 2 Feb 2023 22:27:11 +0800 Subject: [PATCH 1782/2015] win, translate mode, not debug yet Signed-off-by: fufesou --- Cargo.lock | 26 ++++++++- Cargo.toml | 2 +- src/keyboard.rs | 105 ++++++++++++++++++++++++++++-------- src/server/input_service.rs | 17 +++++- src/ui_session_interface.rs | 21 ++++++++ 5 files changed, 144 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e15641363..4ac2720be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1556,7 +1556,7 @@ dependencies = [ "log", "objc", "pkg-config", - "rdev", + "rdev 0.5.0-2 (git+https://github.com/fufesou/rdev)", "serde 1.0.149", "serde_derive", "tfc", @@ -4401,6 +4401,28 @@ dependencies = [ "num_cpus", ] +[[package]] +name = "rdev" +version = "0.5.0-2" +dependencies = [ + "cocoa", + "core-foundation 0.9.3", + "core-foundation-sys 0.8.3", + "core-graphics 0.22.3", + "enum-map", + "epoll", + "inotify", + "lazy_static", + "libc", + "log", + "mio 0.8.5", + "strum 0.24.1", + "strum_macros 0.24.3", + "widestring 1.0.2", + "winapi 0.3.9", + "x11 2.20.1", +] + [[package]] name = "rdev" version = "0.5.0-2" @@ -4709,7 +4731,7 @@ dependencies = [ "objc", "objc_id", "parity-tokio-ipc", - "rdev", + "rdev 0.5.0-2", "repng", "reqwest", "rpassword 7.2.0", diff --git a/Cargo.toml b/Cargo.toml index 936b9e349..5d75b7a23 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,7 +63,7 @@ default-net = { git = "https://github.com/Kingtous/default-net" } wol-rs = "0.9.1" flutter_rust_bridge = { version = "1.61.1", optional = true } errno = "0.2.8" -rdev = { git = "https://github.com/fufesou/rdev" } +rdev = { path = "../rdev" } url = { version = "2.1", features = ["serde"] } reqwest = { version = "0.11", features = ["blocking", "json", "rustls-tls"], default-features=false } diff --git a/src/keyboard.rs b/src/keyboard.rs index 054a39580..bcb0650ac 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -92,7 +92,8 @@ pub mod client { if is_long_press(&event) { return; } - if let Some(key_event) = event_to_key_event(&event, lock_modes) { + + for key_event in event_to_key_events(&event, lock_modes) { send_key_event(&key_event); } } @@ -341,7 +342,7 @@ fn update_modifiers_state(event: &Event) { }; } -pub fn event_to_key_event(event: &Event, lock_modes: Option) -> Option { +pub fn event_to_key_events(event: &Event, lock_modes: Option) -> Vec { let mut key_event = KeyEvent::new(); update_modifiers_state(event); @@ -357,28 +358,38 @@ pub fn event_to_key_event(event: &Event, lock_modes: Option) -> Option map_keyboard_mode(event, key_event)?, - KeyboardMode::Translate => translate_keyboard_mode(event, key_event)?, + let mut key_events = match keyboard_mode { + KeyboardMode::Map => match map_keyboard_mode(event, key_event) { + Some(event) => [event].to_vec(), + None => Vec::new(), + }, + KeyboardMode::Translate => translate_keyboard_mode(event, key_event), _ => { #[cfg(not(any(target_os = "android", target_os = "ios")))] { - legacy_keyboard_mode(event, key_event)? + legacy_keyboard_mode(event, key_event) } #[cfg(any(target_os = "android", target_os = "ios"))] { - None? + Vec::new() } } }; - #[cfg(not(any(target_os = "android", target_os = "ios")))] - if let Some(lock_modes) = lock_modes { - add_numlock_capslock_with_lock_modes(&mut key_event, lock_modes); - } else { - add_numlock_capslock_status(&mut key_event); + + if keyboard_mode != KeyboardMode::Translate { + for key_event in &mut key_events { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if let Some(lock_modes) = lock_modes { + add_numlock_capslock_with_lock_modes(key_event, lock_modes); + } else { + add_numlock_capslock_status(key_event); + } + } } - return Some(key_event); + println!("REMOVE ME ========================= key_events {:?}", &key_events); + + key_events } pub fn event_type_to_event(event_type: EventType) -> Event { @@ -386,6 +397,7 @@ pub fn event_type_to_event(event_type: EventType) -> Event { event_type, time: SystemTime::now(), name: None, + unicode: Vec::new(), code: 0, scan_code: 0, } @@ -423,13 +435,14 @@ pub fn get_peer_platform() -> String { } #[cfg(not(any(target_os = "android", target_os = "ios")))] -pub fn legacy_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option { +pub fn legacy_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Vec { + let mut events = Vec::new(); // legacy mode(0): Generate characters locally, look for keycode on other side. let (mut key, down_or_up) = match event.event_type { EventType::KeyPress(key) => (key, true), EventType::KeyRelease(key) => (key, false), _ => { - return None; + return events; } }; @@ -475,7 +488,7 @@ pub fn legacy_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option Option { if is_win && ctrl && alt { client::ctrl_alt_del(); - return None; + return events; } Some(ControlKey::Delete) } @@ -545,7 +558,7 @@ pub fn legacy_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option Some(ControlKey::Subtract), Key::KpPlus => Some(ControlKey::Add), Key::CapsLock | Key::NumLock | Key::ScrollLock => { - return None; + return events; } Key::Home => Some(ControlKey::Home), Key::End => Some(ControlKey::End), @@ -628,12 +641,12 @@ pub fn legacy_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option Option Option { @@ -703,6 +717,51 @@ pub fn map_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option Option { - None +#[cfg(target_os = "windows")] +fn is_modifier_code(scan_code: u32) -> bool { + match scan_code { + // Alt | AltGr | ControlLeft | ControlRight | ShiftLeft | ShiftRight | MetaLeft | MetaRight + 0x38 | 0xE038 | 0x1D | 0xE01D | 0x2A | 0x36 | 0xE05B | 0xE05C => true, + _ => false, + } +} + +#[cfg(target_os = "linux")] +fn is_modifier_code(key_code: u32) -> bool { + match scan_code { + 64 | 108 | 37 | 105 | 50 | 62 | 133 | 134 => true, + _ => false, + } +} + +#[cfg(target_os = "macos")] +fn is_modifier_code(key_code: u32) -> bool { + match scan_code { + 0x3A | 0x3D | 0x3B | 0x3E | 0x38 | 0x3C | 0x37 | 0x36 => true, + _ => false, + } +} + +pub fn translate_keyboard_mode(event: &Event, key_event: KeyEvent) -> Vec { + #[cfg(target_os = "windows")] + let is_modifier = is_modifier_code(event.scan_code); + #[cfg(target_os = "linux")] + let is_modifier = is_modifier_code(event.key_code); + #[cfg(target_os = "macos")] + let is_modifier = is_modifier_code(event.key_code); + + let mut events: Vec = Vec::new(); + if is_modifier { + if let Some(evt) = map_keyboard_mode(event, key_event) { + events.push(evt); + } + return events; + } + + for unicode in &event.unicode { + let mut evt = key_event.clone(); + evt.set_unicode(*unicode as _); + events.push(evt); + } + events } diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 2715a2643..072ef53fb 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -1067,6 +1067,21 @@ fn legacy_keyboard_mode(evt: &KeyEvent) { release_keys(&mut en, &to_release); } +fn translate_keyboard_mode(evt: &KeyEvent) { + match evt.union { + Some(key_event::Union::Unicode(unicode)) => { + println!("REMOVE ME ========================= simulate_unicode {}", unicode); + allow_err!(rdev::simulate_unicode(unicode as _)); + }, + Some(key_event::Union::Chr(..)) => { + map_keyboard_mode(evt) + } + _ => { + log::debug!("Unreachable. Unexpected key event {:?}", &evt); + } + } +} + pub fn handle_key_(evt: &KeyEvent) { if EXITING.load(Ordering::SeqCst) { return; @@ -1080,7 +1095,7 @@ pub fn handle_key_(evt: &KeyEvent) { map_keyboard_mode(evt); } KeyboardMode::Translate => { - legacy_keyboard_mode(evt); + translate_keyboard_mode(evt); } _ => { legacy_keyboard_mode(evt); diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 4fc5db743..95b8cdbd0 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -361,11 +361,31 @@ impl Session { } pub fn enter(&self) { + #[cfg(target_os = "windows")] + { + match &self.lc.read().unwrap().keyboard_mode as _ { + "legacy" => { + println!("REMOVE ME =========================== enter legacy "); + rdev::set_get_key_name(true); + } + "translate" => { + println!("REMOVE ME =========================== enter translate "); + rdev::set_get_key_name(true); + } + _ => {} + } + } + IS_IN.store(true, Ordering::SeqCst); keyboard::client::change_grab_status(GrabState::Run); } pub fn leave(&self) { + #[cfg(target_os = "windows")] + { + println!("REMOVE ME =========================== leave "); + rdev::set_get_key_name(false); + } IS_IN.store(false, Ordering::SeqCst); keyboard::client::change_grab_status(GrabState::Wait); } @@ -429,6 +449,7 @@ impl Session { let event = Event { time: std::time::SystemTime::now(), name: Option::Some(name.to_owned()), + unicode: Vec::new(), code: keycode as _, scan_code: scancode as _, event_type: event_type, From 6e54cd2e6b7a7e207b13e31f2668db4df98f13ee Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 3 Feb 2023 10:41:47 +0800 Subject: [PATCH 1783/2015] win, translate mode, check dead code Signed-off-by: fufesou --- .../lib/desktop/widgets/remote_menubar.dart | 29 +++++---- src/common.rs | 4 +- src/keyboard.rs | 63 ++++++------------- src/ui_session_interface.rs | 5 +- 4 files changed, 41 insertions(+), 60 deletions(-) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 36b9504c0..4fd702ad8 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -1382,25 +1382,23 @@ class _RemoteMenubarState extends State { text: translate('Ratio'), optionsGetter: () { List list = []; - List modes = ["legacy"]; + List modes = [ + KeyboardModeMenu(key: 'legacy', menu: 'Legacy mode'), + KeyboardModeMenu(key: 'map', menu: 'Map mode'), + KeyboardModeMenu(key: 'translate', menu: 'Translate mode'), + ]; - if (bind.sessionIsKeyboardModeSupported(id: widget.id, mode: "map")) { - modes.add("map"); - } - - for (String mode in modes) { - if (mode == "legacy") { + for (KeyboardModeMenu mode in modes) { + if (bind.sessionIsKeyboardModeSupported( + id: widget.id, mode: mode.key)) { list.add(MenuEntryRadioOption( - text: translate('Legacy mode'), value: 'legacy')); - } else if (mode == "map") { - list.add(MenuEntryRadioOption( - text: translate('Map mode'), value: 'map')); + text: translate(mode.menu), value: mode.key)); } } return list; }, curOptionGetter: () async { - return await bind.sessionGetKeyboardMode(id: widget.id) ?? "legacy"; + return await bind.sessionGetKeyboardMode(id: widget.id) ?? 'legacy'; }, optionSetter: (String oldValue, String newValue) async { await bind.sessionSetKeyboardMode(id: widget.id, value: newValue); @@ -1689,3 +1687,10 @@ class _DraggableShowHideState extends State<_DraggableShowHide> { ); } } + +class KeyboardModeMenu { + final String key; + final String menu; + + KeyboardModeMenu({required this.key, required this.menu}); +} diff --git a/src/common.rs b/src/common.rs index c2d5a81f0..8f8ce8dec 100644 --- a/src/common.rs +++ b/src/common.rs @@ -671,8 +671,8 @@ pub fn is_keyboard_mode_supported(keyboard_mode: &KeyboardMode, version_number: match keyboard_mode { KeyboardMode::Legacy => true, KeyboardMode::Map => version_number >= hbb_common::get_version_number("1.2.0"), - KeyboardMode::Translate => false, - KeyboardMode::Auto => false, + KeyboardMode::Translate => version_number >= hbb_common::get_version_number("1.2.0"), + KeyboardMode::Auto => version_number >= hbb_common::get_version_number("1.2.0"), } } diff --git a/src/keyboard.rs b/src/keyboard.rs index bcb0650ac..7d5f36af2 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -387,7 +387,10 @@ pub fn event_to_key_events(event: &Event, lock_modes: Option) -> Vec Event { Event { event_type, time: SystemTime::now(), - name: None, - unicode: Vec::new(), + unicode: None, code: 0, scan_code: 0, } @@ -571,7 +573,8 @@ pub fn legacy_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Vec { if s.len() <= 2 { // exclude chinese characters @@ -717,51 +720,25 @@ pub fn map_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option bool { - match scan_code { - // Alt | AltGr | ControlLeft | ControlRight | ShiftLeft | ShiftRight | MetaLeft | MetaRight - 0x38 | 0xE038 | 0x1D | 0xE01D | 0x2A | 0x36 | 0xE05B | 0xE05C => true, - _ => false, - } -} - -#[cfg(target_os = "linux")] -fn is_modifier_code(key_code: u32) -> bool { - match scan_code { - 64 | 108 | 37 | 105 | 50 | 62 | 133 | 134 => true, - _ => false, - } -} - -#[cfg(target_os = "macos")] -fn is_modifier_code(key_code: u32) -> bool { - match scan_code { - 0x3A | 0x3D | 0x3B | 0x3E | 0x38 | 0x3C | 0x37 | 0x36 => true, - _ => false, - } -} - pub fn translate_keyboard_mode(event: &Event, key_event: KeyEvent) -> Vec { - #[cfg(target_os = "windows")] - let is_modifier = is_modifier_code(event.scan_code); - #[cfg(target_os = "linux")] - let is_modifier = is_modifier_code(event.key_code); - #[cfg(target_os = "macos")] - let is_modifier = is_modifier_code(event.key_code); - let mut events: Vec = Vec::new(); - if is_modifier { + match &event.unicode { + Some(unicode_info) => { + if !unicode_info.is_dead { + for code in &unicode_info.unicode { + let mut evt = key_event.clone(); + evt.set_unicode(*code as _); + events.push(evt); + } + } + } + None => {} + } + if events.is_empty() { if let Some(evt) = map_keyboard_mode(event, key_event) { events.push(evt); } return events; } - - for unicode in &event.unicode { - let mut evt = key_event.clone(); - evt.set_unicode(*unicode as _); - events.push(evt); - } events } diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 95b8cdbd0..3801eda67 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -423,7 +423,7 @@ impl Session { pub fn handle_flutter_key_event( &self, - name: &str, + _name: &str, keycode: i32, scancode: i32, lock_modes: i32, @@ -448,8 +448,7 @@ impl Session { }; let event = Event { time: std::time::SystemTime::now(), - name: Option::Some(name.to_owned()), - unicode: Vec::new(), + unicode: None, code: keycode as _, scan_code: scancode as _, event_type: event_type, From 6eec0041bd7038a062a250b7f6dbd8bb3b4569d1 Mon Sep 17 00:00:00 2001 From: fufesou Date: Mon, 6 Feb 2023 16:26:27 +0800 Subject: [PATCH 1784/2015] win, tranlsate mode, handle shift Signed-off-by: fufesou --- src/keyboard.rs | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/keyboard.rs b/src/keyboard.rs index 7d5f36af2..08ab23b1b 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -88,14 +88,17 @@ pub mod client { } } - pub fn process_event(event: &Event, lock_modes: Option) { + pub fn process_event(event: &Event, lock_modes: Option) -> KeyboardMode { + let keyboard_mode = get_keyboard_mode_enum(); + if is_long_press(&event) { - return; + return keyboard_mode; } - for key_event in event_to_key_events(&event, lock_modes) { + for key_event in event_to_key_events(&event, keyboard_mode, lock_modes) { send_key_event(&key_event); } + keyboard_mode } pub fn get_modifiers_state( @@ -205,7 +208,14 @@ pub fn start_grab_loop() { return Some(event); } if KEYBOARD_HOOKED.load(Ordering::SeqCst) { - client::process_event(&event, None); + let keyboard_mode = client::process_event(&event, None); + if keyboard_mode == KeyboardMode::Translate { + // shift + if event.scan_code == 0x2A { + return Some(event); + } + } + if is_press { return None; } else { @@ -342,7 +352,7 @@ fn update_modifiers_state(event: &Event) { }; } -pub fn event_to_key_events(event: &Event, lock_modes: Option) -> Vec { +pub fn event_to_key_events(event: &Event, keyboard_mode: KeyboardMode, lock_modes: Option) -> Vec { let mut key_event = KeyEvent::new(); update_modifiers_state(event); @@ -356,7 +366,6 @@ pub fn event_to_key_events(event: &Event, lock_modes: Option) -> Vec {} } - let keyboard_mode = get_keyboard_mode_enum(); key_event.mode = keyboard_mode.into(); let mut key_events = match keyboard_mode { KeyboardMode::Map => match map_keyboard_mode(event, key_event) { From ddc9792d15420a2a3e56cc6b37cc89a064c5013b Mon Sep 17 00:00:00 2001 From: fufesou Date: Mon, 6 Feb 2023 18:13:17 +0800 Subject: [PATCH 1785/2015] win, translate mode, debug almost done Signed-off-by: fufesou --- Cargo.lock | 28 ++----------------- Cargo.toml | 2 +- .../lib/desktop/widgets/remote_menubar.dart | 6 ++++ src/keyboard.rs | 9 ++---- src/server/input_service.rs | 1 - src/ui_session_interface.rs | 11 ++------ 6 files changed, 14 insertions(+), 43 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4ac2720be..988363019 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1556,7 +1556,7 @@ dependencies = [ "log", "objc", "pkg-config", - "rdev 0.5.0-2 (git+https://github.com/fufesou/rdev)", + "rdev", "serde 1.0.149", "serde_derive", "tfc", @@ -4404,29 +4404,7 @@ dependencies = [ [[package]] name = "rdev" version = "0.5.0-2" -dependencies = [ - "cocoa", - "core-foundation 0.9.3", - "core-foundation-sys 0.8.3", - "core-graphics 0.22.3", - "enum-map", - "epoll", - "inotify", - "lazy_static", - "libc", - "log", - "mio 0.8.5", - "strum 0.24.1", - "strum_macros 0.24.3", - "widestring 1.0.2", - "winapi 0.3.9", - "x11 2.20.1", -] - -[[package]] -name = "rdev" -version = "0.5.0-2" -source = "git+https://github.com/fufesou/rdev#238c9778da40056e2efda1e4264355bc89fb6358" +source = "git+https://github.com/fufesou/rdev#77b45e9e43f713851874c7fbb8e7149ab4f2e6a1" dependencies = [ "cocoa", "core-foundation 0.9.3", @@ -4731,7 +4709,7 @@ dependencies = [ "objc", "objc_id", "parity-tokio-ipc", - "rdev 0.5.0-2", + "rdev", "repng", "reqwest", "rpassword 7.2.0", diff --git a/Cargo.toml b/Cargo.toml index 5d75b7a23..936b9e349 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,7 +63,7 @@ default-net = { git = "https://github.com/Kingtous/default-net" } wol-rs = "0.9.1" flutter_rust_bridge = { version = "1.61.1", optional = true } errno = "0.2.8" -rdev = { path = "../rdev" } +rdev = { git = "https://github.com/fufesou/rdev" } url = { version = "2.1", features = ["serde"] } reqwest = { version = "0.11", features = ["blocking", "json", "rustls-tls"], default-features=false } diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 4fd702ad8..c3c8ce3fe 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -1391,6 +1391,12 @@ class _RemoteMenubarState extends State { for (KeyboardModeMenu mode in modes) { if (bind.sessionIsKeyboardModeSupported( id: widget.id, mode: mode.key)) { + if (mode.key == 'translate') { + if (!Platform.isWindows || + widget.ffi.ffiModel.pi.platform != kPeerPlatformWindows) { + continue; + } + } list.add(MenuEntryRadioOption( text: translate(mode.menu), value: mode.key)); } diff --git a/src/keyboard.rs b/src/keyboard.rs index 08ab23b1b..fd9514427 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -210,8 +210,8 @@ pub fn start_grab_loop() { if KEYBOARD_HOOKED.load(Ordering::SeqCst) { let keyboard_mode = client::process_event(&event, None); if keyboard_mode == KeyboardMode::Translate { - // shift - if event.scan_code == 0x2A { + // SHIFT(0x2A) RSHIFT(0x36) + if event.scan_code == 0x2A || event.scan_code == 0x36 { return Some(event); } } @@ -396,11 +396,6 @@ pub fn event_to_key_events(event: &Event, keyboard_mode: KeyboardMode, lock_mode } } - println!( - "REMOVE ME ========================= key_events {:?}", - &key_events - ); - key_events } diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 072ef53fb..1d7d4773d 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -1070,7 +1070,6 @@ fn legacy_keyboard_mode(evt: &KeyEvent) { fn translate_keyboard_mode(evt: &KeyEvent) { match evt.union { Some(key_event::Union::Unicode(unicode)) => { - println!("REMOVE ME ========================= simulate_unicode {}", unicode); allow_err!(rdev::simulate_unicode(unicode as _)); }, Some(key_event::Union::Chr(..)) => { diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 3801eda67..12412d7cd 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -364,14 +364,8 @@ impl Session { #[cfg(target_os = "windows")] { match &self.lc.read().unwrap().keyboard_mode as _ { - "legacy" => { - println!("REMOVE ME =========================== enter legacy "); - rdev::set_get_key_name(true); - } - "translate" => { - println!("REMOVE ME =========================== enter translate "); - rdev::set_get_key_name(true); - } + "legacy" => rdev::set_get_key_name(true), + "translate" => rdev::set_get_key_name(true), _ => {} } } @@ -383,7 +377,6 @@ impl Session { pub fn leave(&self) { #[cfg(target_os = "windows")] { - println!("REMOVE ME =========================== leave "); rdev::set_get_key_name(false); } IS_IN.store(false, Ordering::SeqCst); From 347add18744358b9d07247bee458f2a4ca17c0f8 Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 8 Feb 2023 09:48:04 +0800 Subject: [PATCH 1786/2015] win, translate mode, to debug Signed-off-by: fufesou --- Cargo.lock | 26 +++++++++- Cargo.toml | 2 +- src/keyboard.rs | 100 ++++++++++++++++++++++++++++++++---- src/server/input_service.rs | 13 ++++- 4 files changed, 128 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 988363019..f5ffa7f9d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1556,7 +1556,7 @@ dependencies = [ "log", "objc", "pkg-config", - "rdev", + "rdev 0.5.0-2 (git+https://github.com/fufesou/rdev)", "serde 1.0.149", "serde_derive", "tfc", @@ -4401,6 +4401,28 @@ dependencies = [ "num_cpus", ] +[[package]] +name = "rdev" +version = "0.5.0-2" +dependencies = [ + "cocoa", + "core-foundation 0.9.3", + "core-foundation-sys 0.8.3", + "core-graphics 0.22.3", + "enum-map", + "epoll", + "inotify", + "lazy_static", + "libc", + "log", + "mio 0.8.5", + "strum 0.24.1", + "strum_macros 0.24.3", + "widestring 1.0.2", + "winapi 0.3.9", + "x11 2.20.1", +] + [[package]] name = "rdev" version = "0.5.0-2" @@ -4709,7 +4731,7 @@ dependencies = [ "objc", "objc_id", "parity-tokio-ipc", - "rdev", + "rdev 0.5.0-2", "repng", "reqwest", "rpassword 7.2.0", diff --git a/Cargo.toml b/Cargo.toml index 936b9e349..5d75b7a23 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,7 +63,7 @@ default-net = { git = "https://github.com/Kingtous/default-net" } wol-rs = "0.9.1" flutter_rust_bridge = { version = "1.61.1", optional = true } errno = "0.2.8" -rdev = { git = "https://github.com/fufesou/rdev" } +rdev = { path = "../rdev" } url = { version = "2.1", features = ["serde"] } reqwest = { version = "0.11", features = ["blocking", "json", "rustls-tls"], default-features=false } diff --git a/src/keyboard.rs b/src/keyboard.rs index fd9514427..492314ab6 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -199,6 +199,9 @@ pub fn update_grab_get_key_name() { }; } +#[cfg(target_os = "windows")] +static mut IS_LAST_0X021D: bool = false; + pub fn start_grab_loop() { #[cfg(any(target_os = "windows", target_os = "macos"))] std::thread::spawn(move || { @@ -210,12 +213,22 @@ pub fn start_grab_loop() { if KEYBOARD_HOOKED.load(Ordering::SeqCst) { let keyboard_mode = client::process_event(&event, None); if keyboard_mode == KeyboardMode::Translate { - // SHIFT(0x2A) RSHIFT(0x36) - if event.scan_code == 0x2A || event.scan_code == 0x36 { - return Some(event); + #[cfg(target_os = "windows")] + match event.scan_code { + 0x2A => rdev::set_modifier(Key::ShiftLeft, is_press), + 0x36 => rdev::set_modifier(Key::ShiftRight, is_press), + 0x38 => rdev::set_modifier(Key::Alt, is_press), + 0xE038 => rdev::set_modifier(Key::AltGr, is_press), + 0xE05B => rdev::set_modifier(Key::MetaLeft, is_press), + 0xE05C => rdev::set_modifier(Key::MetaRight, is_press), + _ => {} + } + #[cfg(target_os = "windows")] + unsafe { + IS_LAST_0X021D = event.scan_code == 0x021D; } } - + if is_press { return None; } else { @@ -352,7 +365,11 @@ fn update_modifiers_state(event: &Event) { }; } -pub fn event_to_key_events(event: &Event, keyboard_mode: KeyboardMode, lock_modes: Option) -> Vec { +pub fn event_to_key_events( + event: &Event, + keyboard_mode: KeyboardMode, + lock_modes: Option, +) -> Vec { let mut key_event = KeyEvent::new(); update_modifiers_state(event); @@ -577,7 +594,10 @@ pub fn legacy_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Vec { if s.len() <= 2 { @@ -724,8 +744,7 @@ pub fn map_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option Vec { - let mut events: Vec = Vec::new(); +fn try_fill_unicode(event: &Event, key_event: &KeyEvent, events: &mut Vec) { match &event.unicode { Some(unicode_info) => { if !unicode_info.is_dead { @@ -738,8 +757,71 @@ pub fn translate_keyboard_mode(event: &Event, key_event: KeyEvent) -> Vec {} } +} + +#[cfg(target_os = "windows")] +fn is_hot_key_modifiers_down() -> bool { + if rdev::get_modifier(Key::ControlLeft) || rdev::get_modifier(Key::ControlRight) { + return true; + } + if rdev::get_modifier(Key::Alt) || rdev::get_modifier(Key::AltGr) { + return true; + } + if rdev::get_modifier(Key::MetaLeft) || rdev::get_modifier(Key::MetaRight) { + return true; + } + return false; +} + +pub fn translate_virtual_keycode(event: &Event, mut key_event: KeyEvent) -> Option { + match event.event_type { + EventType::KeyPress(..) => { + key_event.down = true; + } + EventType::KeyRelease(..) => { + key_event.down = false; + } + _ => return None, + }; + + let mut peer = get_peer_platform().to_lowercase(); + peer.retain(|c| !c.is_whitespace()); + + // #[cfg(target_os = "windows")] + // let keycode = match peer.as_str() { + // "windows" => event.code, + // "macos" => { + // if hbb_common::config::LocalConfig::get_kb_layout_type() == "ISO" { + // rdev::win_scancode_to_macos_iso_code(event.scan_code)? + // } else { + // rdev::win_scancode_to_macos_code(event.scan_code)? + // } + // } + // _ => rdev::win_scancode_to_linux_code(event.scan_code)?, + // }; + + key_event.set_chr(event.code as _); + Some(key_event) +} + +pub fn translate_keyboard_mode(event: &Event, key_event: KeyEvent) -> Vec { + let mut events: Vec = Vec::new(); + #[cfg(target_os = "windows")] + unsafe { + if IS_LAST_0X021D { + if event.scan_code == 0xE038 { + return events; + } + } + } + + #[cfg(target_os = "windows")] + if !is_hot_key_modifiers_down() { + try_fill_unicode(event, &key_event, &mut events); + } + if events.is_empty() { - if let Some(evt) = map_keyboard_mode(event, key_event) { + if let Some(evt) = translate_virtual_keycode(event, key_event) { events.push(evt); } return events; diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 1d7d4773d..133b9a830 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -1067,13 +1067,24 @@ fn legacy_keyboard_mode(evt: &KeyEvent) { release_keys(&mut en, &to_release); } +#[cfg(target_os = "windows")] +fn translate_process_virtual_keycode(vk: u32, down: bool) { + let scancode = rdev::vk_to_scancode(vk); + // map mode(1): Send keycode according to the peer platform. + record_pressed_key(scancode as u64 + KEY_CHAR_START, down); + + crate::platform::windows::try_change_desktop(); + sim_rdev_rawkey(scancode, down); +} + fn translate_keyboard_mode(evt: &KeyEvent) { match evt.union { Some(key_event::Union::Unicode(unicode)) => { allow_err!(rdev::simulate_unicode(unicode as _)); }, Some(key_event::Union::Chr(..)) => { - map_keyboard_mode(evt) + #[cfg(target_os = "windows")] + translate_process_virtual_keycode(evt.chr(), evt.down) } _ => { log::debug!("Unreachable. Unexpected key event {:?}", &evt); From 1294103ba778016a118ba51cbf9a3d61f1db5212 Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 8 Feb 2023 13:31:49 +0800 Subject: [PATCH 1787/2015] win, translate mode, debug Signed-off-by: fufesou --- src/keyboard.rs | 62 +++++++++++++++++++------------- src/server/input_service.rs | 71 ++++++++++++++++++++++--------------- 2 files changed, 80 insertions(+), 53 deletions(-) diff --git a/src/keyboard.rs b/src/keyboard.rs index 492314ab6..bdf1c5c1b 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -200,7 +200,7 @@ pub fn update_grab_get_key_name() { } #[cfg(target_os = "windows")] -static mut IS_LAST_0X021D: bool = false; +static mut IS_0X021D_DOWN: bool = false; pub fn start_grab_loop() { #[cfg(any(target_os = "windows", target_os = "macos"))] @@ -210,33 +210,43 @@ pub fn start_grab_loop() { if key == Key::CapsLock || key == Key::NumLock { return Some(event); } - if KEYBOARD_HOOKED.load(Ordering::SeqCst) { - let keyboard_mode = client::process_event(&event, None); - if keyboard_mode == KeyboardMode::Translate { - #[cfg(target_os = "windows")] - match event.scan_code { - 0x2A => rdev::set_modifier(Key::ShiftLeft, is_press), - 0x36 => rdev::set_modifier(Key::ShiftRight, is_press), - 0x38 => rdev::set_modifier(Key::Alt, is_press), - 0xE038 => rdev::set_modifier(Key::AltGr, is_press), - 0xE05B => rdev::set_modifier(Key::MetaLeft, is_press), - 0xE05C => rdev::set_modifier(Key::MetaRight, is_press), - _ => {} - } - #[cfg(target_os = "windows")] - unsafe { - IS_LAST_0X021D = event.scan_code == 0x021D; - } - } + let mut _keyboard_mode = KeyboardMode::Map; + let scan_code = event.scan_code; + let res = if KEYBOARD_HOOKED.load(Ordering::SeqCst) { + _keyboard_mode = client::process_event(&event, None); if is_press { - return None; + None } else { - return Some(event); + Some(event) } } else { - return Some(event); + Some(event) + }; + + #[cfg(target_os = "windows")] + match scan_code { + 0x1D | 0x021D => rdev::set_modifier(Key::ControlLeft, is_press), + 0xE01D => rdev::set_modifier(Key::ControlRight, is_press), + 0x2A => rdev::set_modifier(Key::ShiftLeft, is_press), + 0x36 => rdev::set_modifier(Key::ShiftRight, is_press), + 0x38 => rdev::set_modifier(Key::Alt, is_press), + // Right Alt + 0xE038 => rdev::set_modifier(Key::AltGr, is_press), + 0xE05B => rdev::set_modifier(Key::MetaLeft, is_press), + 0xE05C => rdev::set_modifier(Key::MetaRight, is_press), + _ => {} } + + #[cfg(target_os = "windows")] + unsafe { + // AltGr + if scan_code == 0x021D { + IS_0X021D_DOWN = is_press; + } + } + + return res; }; let func = move |event: Event| match event.event_type { EventType::KeyPress(key) => try_handle_keyboard(event, key, true), @@ -808,7 +818,11 @@ pub fn translate_keyboard_mode(event: &Event, key_event: KeyEvent) -> Vec = Vec::new(); #[cfg(target_os = "windows")] unsafe { - if IS_LAST_0X021D { + if event.scan_code == 0x021D { + return events; + } + + if IS_0X021D_DOWN { if event.scan_code == 0xE038 { return events; } @@ -816,7 +830,7 @@ pub fn translate_keyboard_mode(event: &Event, key_event: KeyEvent) -> Vec ResultType<()> Ok(()) } +#[derive(Copy, Clone, PartialEq, Eq, Hash)] +enum KeysDown { + RdevKey(RawKey), + EnigoKey(u64) +} + lazy_static::lazy_static! { static ref ENIGO: Arc> = { Arc::new(Mutex::new(Enigo::new())) }; - static ref KEYS_DOWN: Arc>> = Default::default(); + static ref KEYS_DOWN: Arc>> = Default::default(); static ref LATEST_PEER_INPUT_CURSOR: Arc> = Default::default(); static ref LATEST_SYS_CURSOR_POS: Arc> = Arc::new(Mutex::new((Instant::now().sub(MOUSE_MOVE_PROTECTION_TIMEOUT), (0, 0)))); } @@ -375,12 +380,7 @@ fn record_key_is_control_key(record_key: u64) -> bool { #[inline] fn record_key_is_chr(record_key: u64) -> bool { - KEY_RDEV_START <= record_key && record_key < KEY_CHAR_START -} - -#[inline] -fn record_key_is_rdev_layout(record_key: u64) -> bool { - KEY_CHAR_START <= record_key + record_key < KEY_CHAR_START } #[inline] @@ -396,15 +396,18 @@ fn record_key_to_key(record_key: u64) -> Option { } #[inline] -fn release_record_key(record_key: u64) { +fn release_record_key(record_key: KeysDown) { let func = move || { - if record_key_is_rdev_layout(record_key) { - simulate_(&EventType::KeyRelease(RdevKey::Unknown( - (record_key - KEY_RDEV_START) as _, - ))); - } else if let Some(key) = record_key_to_key(record_key) { - ENIGO.lock().unwrap().key_up(key); - log::debug!("Fixed {:?} timeout", key); + match record_key { + KeysDown::RdevKey(raw_key) => { + simulate_(&EventType::KeyRelease(RdevKey::RawKey(raw_key))); + } + KeysDown::EnigoKey(key) => { + if let Some(key) = record_key_to_key(key) { + ENIGO.lock().unwrap().key_up(key); + log::debug!("Fixed {:?} timeout", key); + } + } } }; @@ -733,7 +736,7 @@ pub fn reset_input_ondisconn() { } } -fn sim_rdev_rawkey(code: u32, keydown: bool) { +fn sim_rdev_rawkey_position(code: u32, keydown: bool) { #[cfg(target_os = "windows")] let rawkey = RawKey::ScanCode(code); #[cfg(target_os = "linux")] @@ -744,6 +747,23 @@ fn sim_rdev_rawkey(code: u32, keydown: bool) { #[cfg(target_os = "macos")] let rawkey = RawKey::MacVirtualKeycode(code); + // map mode(1): Send keycode according to the peer platform. + record_pressed_key(KeysDown::RdevKey(rawkey), keydown); + + let event_type = if keydown { + EventType::KeyPress(RdevKey::RawKey(rawkey)) + } else { + EventType::KeyRelease(RdevKey::RawKey(rawkey)) + }; + simulate_(&event_type); +} + +fn sim_rdev_rawkey_virtual(code: u32, keydown: bool) { + #[cfg(target_os = "windows")] + let rawkey = RawKey::WinVirtualKeycode(code); + + record_pressed_key(KeysDown::RdevKey(rawkey), keydown); + let event_type = if keydown { EventType::KeyPress(RdevKey::RawKey(rawkey)) } else { @@ -874,9 +894,6 @@ fn sync_numlock_capslock_status(key_event: &KeyEvent) { } fn map_keyboard_mode(evt: &KeyEvent) { - // map mode(1): Send keycode according to the peer platform. - record_pressed_key(evt.chr() as u64 + KEY_CHAR_START, evt.down); - #[cfg(windows)] crate::platform::windows::try_change_desktop(); @@ -894,7 +911,7 @@ fn map_keyboard_mode(evt: &KeyEvent) { return; } - sim_rdev_rawkey(evt.chr(), evt.down); + sim_rdev_rawkey_position(evt.chr(), evt.down); } #[cfg(target_os = "macos")] @@ -1011,7 +1028,7 @@ fn release_keys(en: &mut Enigo, to_release: &Vec) { } } -fn record_pressed_key(record_key: u64, down: bool) { +fn record_pressed_key(record_key: KeysDown, down: bool) { let mut key_down = KEYS_DOWN.lock().unwrap(); if down { key_down.insert(record_key, Instant::now()); @@ -1050,12 +1067,12 @@ fn legacy_keyboard_mode(evt: &KeyEvent) { return; } let record_key = ck.value() as u64; - record_pressed_key(record_key, down); + record_pressed_key(KeysDown::EnigoKey(record_key), down); process_control_key(&mut en, &ck, down) } Some(key_event::Union::Chr(chr)) => { let record_key = chr as u64 + KEY_CHAR_START; - record_pressed_key(record_key, down); + record_pressed_key(KeysDown::EnigoKey(record_key), down); process_chr(&mut en, chr, down) } Some(key_event::Union::Unicode(chr)) => process_unicode(&mut en, chr), @@ -1069,12 +1086,8 @@ fn legacy_keyboard_mode(evt: &KeyEvent) { #[cfg(target_os = "windows")] fn translate_process_virtual_keycode(vk: u32, down: bool) { - let scancode = rdev::vk_to_scancode(vk); - // map mode(1): Send keycode according to the peer platform. - record_pressed_key(scancode as u64 + KEY_CHAR_START, down); - crate::platform::windows::try_change_desktop(); - sim_rdev_rawkey(scancode, down); + sim_rdev_rawkey_virtual(vk, down); } fn translate_keyboard_mode(evt: &KeyEvent) { From 5c7f2678fa870427975bdc98c29c7126bb35ba58 Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 8 Feb 2023 14:23:05 +0800 Subject: [PATCH 1788/2015] update rdev Signed-off-by: fufesou --- Cargo.lock | 26 ++------------------------ Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f5ffa7f9d..988363019 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1556,7 +1556,7 @@ dependencies = [ "log", "objc", "pkg-config", - "rdev 0.5.0-2 (git+https://github.com/fufesou/rdev)", + "rdev", "serde 1.0.149", "serde_derive", "tfc", @@ -4401,28 +4401,6 @@ dependencies = [ "num_cpus", ] -[[package]] -name = "rdev" -version = "0.5.0-2" -dependencies = [ - "cocoa", - "core-foundation 0.9.3", - "core-foundation-sys 0.8.3", - "core-graphics 0.22.3", - "enum-map", - "epoll", - "inotify", - "lazy_static", - "libc", - "log", - "mio 0.8.5", - "strum 0.24.1", - "strum_macros 0.24.3", - "widestring 1.0.2", - "winapi 0.3.9", - "x11 2.20.1", -] - [[package]] name = "rdev" version = "0.5.0-2" @@ -4731,7 +4709,7 @@ dependencies = [ "objc", "objc_id", "parity-tokio-ipc", - "rdev 0.5.0-2", + "rdev", "repng", "reqwest", "rpassword 7.2.0", diff --git a/Cargo.toml b/Cargo.toml index 5d75b7a23..936b9e349 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,7 +63,7 @@ default-net = { git = "https://github.com/Kingtous/default-net" } wol-rs = "0.9.1" flutter_rust_bridge = { version = "1.61.1", optional = true } errno = "0.2.8" -rdev = { path = "../rdev" } +rdev = { git = "https://github.com/fufesou/rdev" } url = { version = "2.1", features = ["serde"] } reqwest = { version = "0.11", features = ["blocking", "json", "rustls-tls"], default-features=false } From 01f762ffdb57d7dee4a17110d293252b1621c49a Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 8 Feb 2023 14:14:13 +0800 Subject: [PATCH 1789/2015] build linux Signed-off-by: fufesou --- src/keyboard.rs | 5 ++++- src/server/input_service.rs | 33 ++++++++++++++++----------------- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/src/keyboard.rs b/src/keyboard.rs index bdf1c5c1b..91480ba30 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -830,10 +830,13 @@ pub fn translate_keyboard_mode(event: &Event, key_event: KeyEvent) -> Vec ResultType<()> #[derive(Copy, Clone, PartialEq, Eq, Hash)] enum KeysDown { RdevKey(RawKey), - EnigoKey(u64) + EnigoKey(u64), } lazy_static::lazy_static! { @@ -397,16 +397,14 @@ fn record_key_to_key(record_key: u64) -> Option { #[inline] fn release_record_key(record_key: KeysDown) { - let func = move || { - match record_key { - KeysDown::RdevKey(raw_key) => { - simulate_(&EventType::KeyRelease(RdevKey::RawKey(raw_key))); - } - KeysDown::EnigoKey(key) => { - if let Some(key) = record_key_to_key(key) { - ENIGO.lock().unwrap().key_up(key); - log::debug!("Fixed {:?} timeout", key); - } + let func = move || match record_key { + KeysDown::RdevKey(raw_key) => { + simulate_(&EventType::KeyRelease(RdevKey::RawKey(raw_key))); + } + KeysDown::EnigoKey(key) => { + if let Some(key) = record_key_to_key(key) { + ENIGO.lock().unwrap().key_up(key); + log::debug!("Fixed {:?} timeout", key); } } }; @@ -758,12 +756,10 @@ fn sim_rdev_rawkey_position(code: u32, keydown: bool) { simulate_(&event_type); } +#[cfg(target_os = "windows")] fn sim_rdev_rawkey_virtual(code: u32, keydown: bool) { - #[cfg(target_os = "windows")] let rawkey = RawKey::WinVirtualKeycode(code); - record_pressed_key(KeysDown::RdevKey(rawkey), keydown); - let event_type = if keydown { EventType::KeyPress(RdevKey::RawKey(rawkey)) } else { @@ -941,10 +937,11 @@ fn release_unpressed_modifiers(en: &mut Enigo, key_event: &KeyEvent) { #[cfg(target_os = "linux")] fn is_altgr_pressed() -> bool { + let altgr_rawkey = RawKey::LinuxXorgKeycode(ControlKey::RAlt.value() as _); KEYS_DOWN .lock() .unwrap() - .get(&(ControlKey::RAlt.value() as _)) + .get(&KeysDown::RdevKey(altgr_rawkey)) .is_some() } @@ -1093,9 +1090,11 @@ fn translate_process_virtual_keycode(vk: u32, down: bool) { fn translate_keyboard_mode(evt: &KeyEvent) { match evt.union { Some(key_event::Union::Unicode(unicode)) => { + #[cfg(target_os = "windows")] allow_err!(rdev::simulate_unicode(unicode as _)); - }, - Some(key_event::Union::Chr(..)) => { + } + Some(key_event::Union::Chr(..)) => + { #[cfg(target_os = "windows")] translate_process_virtual_keycode(evt.chr(), evt.down) } From 586f0a272663222c6d013aca3129c4be0dfd0fae Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 8 Feb 2023 14:17:37 +0800 Subject: [PATCH 1790/2015] compile macos Signed-off-by: fufesou --- src/server/input_service.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 7b6130ad1..edf0ef497 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -1089,9 +1089,9 @@ fn translate_process_virtual_keycode(vk: u32, down: bool) { fn translate_keyboard_mode(evt: &KeyEvent) { match evt.union { - Some(key_event::Union::Unicode(unicode)) => { + Some(key_event::Union::Unicode(_unicode)) => { #[cfg(target_os = "windows")] - allow_err!(rdev::simulate_unicode(unicode as _)); + allow_err!(rdev::simulate_unicode(_unicode as _)); } Some(key_event::Union::Chr(..)) => { From d263d1892bf6fc1258c5b922b8cbd3b0922deebe Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 8 Feb 2023 14:34:52 +0800 Subject: [PATCH 1791/2015] update rdev Signed-off-by: fufesou --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 988363019..93b40ca3a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4404,7 +4404,7 @@ dependencies = [ [[package]] name = "rdev" version = "0.5.0-2" -source = "git+https://github.com/fufesou/rdev#77b45e9e43f713851874c7fbb8e7149ab4f2e6a1" +source = "git+https://github.com/fufesou/rdev#4d8231f05e14c5a04cd7d2c1288e87ad52d39e4c" dependencies = [ "cocoa", "core-foundation 0.9.3", From 948f9f28dbbd2846e8026f595021d7fb2a7c0b73 Mon Sep 17 00:00:00 2001 From: NicKoehler <53040044+NicKoehler@users.noreply.github.com> Date: Wed, 8 Feb 2023 10:15:08 +0100 Subject: [PATCH 1792/2015] Update it.rs --- src/lang/it.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lang/it.rs b/src/lang/it.rs index 9730bbc2d..a4ea58304 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -446,8 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", "FPS"), ("Auto", "Auto"), ("Other Default Options", "Altre Opzioni Predefinite"), - ("Voice call", ""), - ("Text chat", ""), - ("Stop voice call", ""), + ("Voice call", "Chiamata vocale"), + ("Text chat", "Chat testuale"), + ("Stop voice call", "Interrompi la chiamata vocale"), ].iter().cloned().collect(); } From 7c13be587638c61ffee6fe09a82c2e114ffd2531 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 8 Feb 2023 17:26:44 +0800 Subject: [PATCH 1793/2015] update issue template and clippy for hbb_common --- .github/ISSUE_TEMPLATE/bug_report.yaml | 15 ++++-- libs/hbb_common/build.rs | 4 +- libs/hbb_common/src/bytes_codec.rs | 40 ++++++++-------- libs/hbb_common/src/config.rs | 6 +-- libs/hbb_common/src/lib.rs | 60 +++++++++++------------- libs/hbb_common/src/password_security.rs | 48 +++++++++---------- libs/hbb_common/src/platform/linux.rs | 2 +- libs/hbb_common/src/protos/mod.rs | 2 +- libs/hbb_common/src/socket_client.rs | 22 ++++----- libs/hbb_common/src/tcp.rs | 2 +- 10 files changed, 103 insertions(+), 98 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 16509a3be..c2d92097c 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -30,13 +30,22 @@ body: description: A clear and concise description of what you expected to happen validations: required: true + - type: input + id: os + attributes: + label: Operating system(s) on local side and remote side + description: What operating system(s) do you see this bug on? local side -> remote side. + placeholder: | + Windows 10 -> osx + validations: + required: true - type: input id: version attributes: - label: Operating System(s) and RustDesk Version(s) on local side and remote side - description: What Operatiing System(s) and version(s) of RustDesk do you see this bug on? local side / remote side. + label: RustDesk Version(s) on local side and remote side + description: What RustDesk version(s) do you see this bug on? local side -> remote side. placeholder: | - Windows 10, 1.1.9 / osx 13.1, 1.1.8 + 1.1.9 -> 1.1.8 validations: required: true - type: textarea diff --git a/libs/hbb_common/build.rs b/libs/hbb_common/build.rs index fe0d31076..5ebc3a287 100644 --- a/libs/hbb_common/build.rs +++ b/libs/hbb_common/build.rs @@ -2,11 +2,11 @@ fn main() { let out_dir = format!("{}/protos", std::env::var("OUT_DIR").unwrap()); std::fs::create_dir_all(&out_dir).unwrap(); - + protobuf_codegen::Codegen::new() .pure() .out_dir(out_dir) - .inputs(&["protos/rendezvous.proto", "protos/message.proto"]) + .inputs(["protos/rendezvous.proto", "protos/message.proto"]) .include("protos") .customize(protobuf_codegen::Customize::default().tokio_bytes(true)) .run() diff --git a/libs/hbb_common/src/bytes_codec.rs b/libs/hbb_common/src/bytes_codec.rs index 699aa9bff..bfc798715 100644 --- a/libs/hbb_common/src/bytes_codec.rs +++ b/libs/hbb_common/src/bytes_codec.rs @@ -143,32 +143,32 @@ mod tests { let mut buf = BytesMut::new(); let mut bytes: Vec = Vec::new(); bytes.resize(0x3F, 1); - assert!(!codec.encode(bytes.into(), &mut buf).is_err()); + assert!(codec.encode(bytes.into(), &mut buf).is_ok()); let buf_saved = buf.clone(); assert_eq!(buf.len(), 0x3F + 1); if let Ok(Some(res)) = codec.decode(&mut buf) { assert_eq!(res.len(), 0x3F); assert_eq!(res[0], 1); } else { - assert!(false); + panic!(); } let mut codec2 = BytesCodec::new(); let mut buf2 = BytesMut::new(); if let Ok(None) = codec2.decode(&mut buf2) { } else { - assert!(false); + panic!(); } buf2.extend(&buf_saved[0..1]); if let Ok(None) = codec2.decode(&mut buf2) { } else { - assert!(false); + panic!(); } buf2.extend(&buf_saved[1..]); if let Ok(Some(res)) = codec2.decode(&mut buf2) { assert_eq!(res.len(), 0x3F); assert_eq!(res[0], 1); } else { - assert!(false); + panic!(); } } @@ -177,21 +177,21 @@ mod tests { let mut codec = BytesCodec::new(); let mut buf = BytesMut::new(); let mut bytes: Vec = Vec::new(); - assert!(!codec.encode("".into(), &mut buf).is_err()); + assert!(codec.encode("".into(), &mut buf).is_ok()); assert_eq!(buf.len(), 1); bytes.resize(0x3F + 1, 2); - assert!(!codec.encode(bytes.into(), &mut buf).is_err()); + assert!(codec.encode(bytes.into(), &mut buf).is_ok()); assert_eq!(buf.len(), 0x3F + 2 + 2); if let Ok(Some(res)) = codec.decode(&mut buf) { assert_eq!(res.len(), 0); } else { - assert!(false); + panic!(); } if let Ok(Some(res)) = codec.decode(&mut buf) { assert_eq!(res.len(), 0x3F + 1); assert_eq!(res[0], 2); } else { - assert!(false); + panic!(); } } @@ -201,13 +201,13 @@ mod tests { let mut buf = BytesMut::new(); let mut bytes: Vec = Vec::new(); bytes.resize(0x3F - 1, 3); - assert!(!codec.encode(bytes.into(), &mut buf).is_err()); + assert!(codec.encode(bytes.into(), &mut buf).is_ok()); assert_eq!(buf.len(), 0x3F + 1 - 1); if let Ok(Some(res)) = codec.decode(&mut buf) { assert_eq!(res.len(), 0x3F - 1); assert_eq!(res[0], 3); } else { - assert!(false); + panic!(); } } #[test] @@ -216,13 +216,13 @@ mod tests { let mut buf = BytesMut::new(); let mut bytes: Vec = Vec::new(); bytes.resize(0x3FFF, 4); - assert!(!codec.encode(bytes.into(), &mut buf).is_err()); + assert!(codec.encode(bytes.into(), &mut buf).is_ok()); assert_eq!(buf.len(), 0x3FFF + 2); if let Ok(Some(res)) = codec.decode(&mut buf) { assert_eq!(res.len(), 0x3FFF); assert_eq!(res[0], 4); } else { - assert!(false); + panic!(); } } @@ -232,13 +232,13 @@ mod tests { let mut buf = BytesMut::new(); let mut bytes: Vec = Vec::new(); bytes.resize(0x3FFFFF, 5); - assert!(!codec.encode(bytes.into(), &mut buf).is_err()); + assert!(codec.encode(bytes.into(), &mut buf).is_ok()); assert_eq!(buf.len(), 0x3FFFFF + 3); if let Ok(Some(res)) = codec.decode(&mut buf) { assert_eq!(res.len(), 0x3FFFFF); assert_eq!(res[0], 5); } else { - assert!(false); + panic!(); } } @@ -248,33 +248,33 @@ mod tests { let mut buf = BytesMut::new(); let mut bytes: Vec = Vec::new(); bytes.resize(0x3FFFFF + 1, 6); - assert!(!codec.encode(bytes.into(), &mut buf).is_err()); + assert!(codec.encode(bytes.into(), &mut buf).is_ok()); let buf_saved = buf.clone(); assert_eq!(buf.len(), 0x3FFFFF + 4 + 1); if let Ok(Some(res)) = codec.decode(&mut buf) { assert_eq!(res.len(), 0x3FFFFF + 1); assert_eq!(res[0], 6); } else { - assert!(false); + panic!(); } let mut codec2 = BytesCodec::new(); let mut buf2 = BytesMut::new(); buf2.extend(&buf_saved[0..1]); if let Ok(None) = codec2.decode(&mut buf2) { } else { - assert!(false); + panic!(); } buf2.extend(&buf_saved[1..6]); if let Ok(None) = codec2.decode(&mut buf2) { } else { - assert!(false); + panic!(); } buf2.extend(&buf_saved[6..]); if let Ok(Some(res)) = codec2.decode(&mut buf2) { assert_eq!(res.len(), 0x3FFFFF + 1); assert_eq!(res[0], 6); } else { - assert!(false); + panic!(); } } } diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 71dd9a5c6..1e4d80c9f 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -288,7 +288,7 @@ fn patch(path: PathBuf) -> PathBuf { .trim() .to_owned(); if user != "root" { - return format!("/home/{}", user).into(); + return format!("/home/{user}").into(); } } } @@ -525,7 +525,7 @@ impl Config { let mut path: PathBuf = format!("/tmp/{}", *APP_NAME.read().unwrap()).into(); fs::create_dir(&path).ok(); fs::set_permissions(&path, fs::Permissions::from_mode(0o0777)).ok(); - path.push(format!("ipc{}", postfix)); + path.push(format!("ipc{postfix}")); path.to_str().unwrap_or("").to_owned() } } @@ -562,7 +562,7 @@ impl Config { .unwrap_or_default(); } if !rendezvous_server.contains(':') { - rendezvous_server = format!("{}:{}", rendezvous_server, RENDEZVOUS_PORT); + rendezvous_server = format!("{rendezvous_server}:{RENDEZVOUS_PORT}"); } rendezvous_server } diff --git a/libs/hbb_common/src/lib.rs b/libs/hbb_common/src/lib.rs index c9f9e90d7..1c49adfb7 100644 --- a/libs/hbb_common/src/lib.rs +++ b/libs/hbb_common/src/lib.rs @@ -211,11 +211,7 @@ pub fn gen_version() { // generate build date let build_date = format!("{}", chrono::Local::now().format("%Y-%m-%d %H:%M")); file.write_all( - format!( - "#[allow(dead_code)]\npub const BUILD_DATE: &str = \"{}\";", - build_date - ) - .as_bytes(), + format!("#[allow(dead_code)]\npub const BUILD_DATE: &str = \"{build_date}\";\n").as_bytes(), ) .ok(); file.sync_all().ok(); @@ -342,39 +338,39 @@ mod test { #[test] fn test_ipv6() { - assert_eq!(is_ipv6_str("1:2:3"), true); - assert_eq!(is_ipv6_str("[ab:2:3]:12"), true); - assert_eq!(is_ipv6_str("[ABEF:2a:3]:12"), true); - assert_eq!(is_ipv6_str("[ABEG:2a:3]:12"), false); - assert_eq!(is_ipv6_str("1[ab:2:3]:12"), false); - assert_eq!(is_ipv6_str("1.1.1.1"), false); - assert_eq!(is_ip_str("1.1.1.1"), true); - assert_eq!(is_ipv6_str("1:2:"), false); - assert_eq!(is_ipv6_str("1:2::0"), true); - assert_eq!(is_ipv6_str("[1:2::0]:1"), true); - assert_eq!(is_ipv6_str("[1:2::0]:"), false); - assert_eq!(is_ipv6_str("1:2::0]:1"), false); + assert!(is_ipv6_str("1:2:3")); + assert!(is_ipv6_str("[ab:2:3]:12")); + assert!(is_ipv6_str("[ABEF:2a:3]:12")); + assert!(!is_ipv6_str("[ABEG:2a:3]:12")); + assert!(!is_ipv6_str("1[ab:2:3]:12")); + assert!(!is_ipv6_str("1.1.1.1")); + assert!(is_ip_str("1.1.1.1")); + assert!(!is_ipv6_str("1:2:")); + assert!(is_ipv6_str("1:2::0")); + assert!(is_ipv6_str("[1:2::0]:1")); + assert!(!is_ipv6_str("[1:2::0]:")); + assert!(!is_ipv6_str("1:2::0]:1")); } #[test] fn test_hostname_port() { - assert_eq!(is_domain_port_str("a:12"), false); - assert_eq!(is_domain_port_str("a.b.c:12"), false); - assert_eq!(is_domain_port_str("test.com:12"), true); - assert_eq!(is_domain_port_str("test-UPPER.com:12"), true); - assert_eq!(is_domain_port_str("some-other.domain.com:12"), true); - assert_eq!(is_domain_port_str("under_score:12"), false); - assert_eq!(is_domain_port_str("a@bc:12"), false); - assert_eq!(is_domain_port_str("1.1.1.1:12"), false); - assert_eq!(is_domain_port_str("1.2.3:12"), false); - assert_eq!(is_domain_port_str("1.2.3.45:12"), false); - assert_eq!(is_domain_port_str("a.b.c:123456"), false); - assert_eq!(is_domain_port_str("---:12"), false); - assert_eq!(is_domain_port_str(".:12"), false); + assert!(!is_domain_port_str("a:12")); + assert!(!is_domain_port_str("a.b.c:12")); + assert!(is_domain_port_str("test.com:12")); + assert!(is_domain_port_str("test-UPPER.com:12")); + assert!(is_domain_port_str("some-other.domain.com:12")); + assert!(!is_domain_port_str("under_score:12")); + assert!(!is_domain_port_str("a@bc:12")); + assert!(!is_domain_port_str("1.1.1.1:12")); + assert!(!is_domain_port_str("1.2.3:12")); + assert!(!is_domain_port_str("1.2.3.45:12")); + assert!(!is_domain_port_str("a.b.c:123456")); + assert!(!is_domain_port_str("---:12")); + assert!(!is_domain_port_str(".:12")); // todo: should we also check for these edge cases? // out-of-range port - assert_eq!(is_domain_port_str("test.com:0"), true); - assert_eq!(is_domain_port_str("test.com:98989"), true); + assert!(is_domain_port_str("test.com:0")); + assert!(is_domain_port_str("test.com:98989")); } #[test] diff --git a/libs/hbb_common/src/password_security.rs b/libs/hbb_common/src/password_security.rs index 0b66107fc..ddfe28baa 100644 --- a/libs/hbb_common/src/password_security.rs +++ b/libs/hbb_common/src/password_security.rs @@ -192,51 +192,51 @@ mod test { let data = "Hello World"; let encrypted = encrypt_str_or_original(data, version); let (decrypted, succ, store) = decrypt_str_or_original(&encrypted, version); - println!("data: {}", data); - println!("encrypted: {}", encrypted); - println!("decrypted: {}", decrypted); + println!("data: {data}"); + println!("encrypted: {encrypted}"); + println!("decrypted: {decrypted}"); assert_eq!(data, decrypted); assert_eq!(version, &encrypted[..2]); - assert_eq!(succ, true); - assert_eq!(store, false); + assert!(succ); + assert!(!store); let (_, _, store) = decrypt_str_or_original(&encrypted, "99"); - assert_eq!(store, true); - assert_eq!(decrypt_str_or_original(&decrypted, version).1, false); + assert!(store); + assert!(!decrypt_str_or_original(&decrypted, version).1); assert_eq!(encrypt_str_or_original(&encrypted, version), encrypted); println!("test vec"); let data: Vec = vec![1, 2, 3, 4, 5, 6]; let encrypted = encrypt_vec_or_original(&data, version); let (decrypted, succ, store) = decrypt_vec_or_original(&encrypted, version); - println!("data: {:?}", data); - println!("encrypted: {:?}", encrypted); - println!("decrypted: {:?}", decrypted); + println!("data: {data:?}"); + println!("encrypted: {encrypted:?}"); + println!("decrypted: {decrypted:?}"); assert_eq!(data, decrypted); assert_eq!(version.as_bytes(), &encrypted[..2]); - assert_eq!(store, false); - assert_eq!(succ, true); + assert!(!store); + assert!(succ); let (_, _, store) = decrypt_vec_or_original(&encrypted, "99"); - assert_eq!(store, true); - assert_eq!(decrypt_vec_or_original(&decrypted, version).1, false); + assert!(store); + assert!(!decrypt_vec_or_original(&decrypted, version).1); assert_eq!(encrypt_vec_or_original(&encrypted, version), encrypted); println!("test original"); let data = version.to_string() + "Hello World"; let (decrypted, succ, store) = decrypt_str_or_original(&data, version); assert_eq!(data, decrypted); - assert_eq!(store, true); - assert_eq!(succ, false); + assert!(store); + assert!(!succ); let verbytes = version.as_bytes(); - let data: Vec = vec![verbytes[0] as u8, verbytes[1] as u8, 1, 2, 3, 4, 5, 6]; + let data: Vec = vec![verbytes[0], verbytes[1], 1, 2, 3, 4, 5, 6]; let (decrypted, succ, store) = decrypt_vec_or_original(&data, version); assert_eq!(data, decrypted); - assert_eq!(store, true); - assert_eq!(succ, false); + assert!(store); + assert!(!succ); let (_, succ, store) = decrypt_str_or_original("", version); - assert_eq!(store, false); - assert_eq!(succ, false); - let (_, succ, store) = decrypt_vec_or_original(&vec![], version); - assert_eq!(store, false); - assert_eq!(succ, false); + assert!(!store); + assert!(!succ); + let (_, succ, store) = decrypt_vec_or_original(&[], version); + assert!(!store); + assert!(!succ); } } diff --git a/libs/hbb_common/src/platform/linux.rs b/libs/hbb_common/src/platform/linux.rs index 716025dc7..7c107d11c 100644 --- a/libs/hbb_common/src/platform/linux.rs +++ b/libs/hbb_common/src/platform/linux.rs @@ -60,7 +60,7 @@ fn get_display_server_of_session(session: &str) -> String { .replace("TTY=", "") .trim_end() .into(); - if let Ok(xorg_results) = run_cmds(format!("ps -e | grep \"{}.\\\\+Xorg\"", tty)) + if let Ok(xorg_results) = run_cmds(format!("ps -e | grep \"{tty}.\\\\+Xorg\"")) // And check if Xorg is running on that tty { if xorg_results.trim_end() != "" { diff --git a/libs/hbb_common/src/protos/mod.rs b/libs/hbb_common/src/protos/mod.rs index c001c58fb..57d9b68fe 100644 --- a/libs/hbb_common/src/protos/mod.rs +++ b/libs/hbb_common/src/protos/mod.rs @@ -1 +1 @@ -include!(concat!(env!("OUT_DIR"), "/protos/mod.rs")); \ No newline at end of file +include!(concat!(env!("OUT_DIR"), "/protos/mod.rs")); diff --git a/libs/hbb_common/src/socket_client.rs b/libs/hbb_common/src/socket_client.rs index a034b4e12..2d9b5a984 100644 --- a/libs/hbb_common/src/socket_client.rs +++ b/libs/hbb_common/src/socket_client.rs @@ -13,22 +13,22 @@ use tokio_socks::{IntoTargetAddr, TargetAddr}; pub fn check_port(host: T, port: i32) -> String { let host = host.to_string(); if crate::is_ipv6_str(&host) { - if host.starts_with("[") { + if host.starts_with('[') { return host; } - return format!("[{}]:{}", host, port); + return format!("[{host}]:{port}"); } - if !host.contains(":") { - return format!("{}:{}", host, port); + if !host.contains(':') { + return format!("{host}:{port}"); } - return host; + host } #[inline] pub fn increase_port(host: T, offset: i32) -> String { let host = host.to_string(); if crate::is_ipv6_str(&host) { - if host.starts_with("[") { + if host.starts_with('[') { let tmp: Vec<&str> = host.split("]:").collect(); if tmp.len() == 2 { let port: i32 = tmp[1].parse().unwrap_or(0); @@ -37,8 +37,8 @@ pub fn increase_port(host: T, offset: i32) -> String { } } } - } else if host.contains(":") { - let tmp: Vec<&str> = host.split(":").collect(); + } else if host.contains(':') { + let tmp: Vec<&str> = host.split(':').collect(); if tmp.len() == 2 { let port: i32 = tmp[1].parse().unwrap_or(0); if port > 0 { @@ -46,7 +46,7 @@ pub fn increase_port(host: T, offset: i32) -> String { } } } - return host; + host } pub fn test_if_valid_server(host: &str) -> String { @@ -148,7 +148,7 @@ pub async fn query_nip_io(addr: &SocketAddr) -> ResultType { pub fn ipv4_to_ipv6(addr: String, ipv4: bool) -> String { if !ipv4 && crate::is_ipv4_str(&addr) { if let Some(ip) = addr.split(':').next() { - return addr.replace(ip, &format!("{}.nip.io", ip)); + return addr.replace(ip, &format!("{ip}.nip.io")); } } addr @@ -163,7 +163,7 @@ async fn test_target(target: &str) -> ResultType { tokio::net::lookup_host(target) .await? .next() - .context(format!("Failed to look up host for {}", target)) + .context(format!("Failed to look up host for {target}")) } #[inline] diff --git a/libs/hbb_common/src/tcp.rs b/libs/hbb_common/src/tcp.rs index a7ac4eb3a..f574e8309 100644 --- a/libs/hbb_common/src/tcp.rs +++ b/libs/hbb_common/src/tcp.rs @@ -100,7 +100,7 @@ impl FramedStream { } } } - bail!(format!("Failed to connect to {}", remote_addr)); + bail!(format!("Failed to connect to {remote_addr}")); } pub async fn connect<'a, 't, P, T>( From 4134b77680126f408e5ce88f7eb4c3ad5711b749 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 8 Feb 2023 19:17:59 +0800 Subject: [PATCH 1794/2015] improve ffi enum data size, fix compile warning on mac --- src/client/io_loop.rs | 2 +- src/common.rs | 7 +---- src/core_main.rs | 4 +-- src/ipc.rs | 28 +++++++++++++++---- src/keyboard.rs | 6 ++-- src/server.rs | 62 +++++++++++++++++++++--------------------- src/ui/macos.rs | 9 ++---- src/ui_cm_interface.rs | 4 ++- 8 files changed, 65 insertions(+), 57 deletions(-) diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index f5792bce3..5186aff4d 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -25,7 +25,7 @@ use hbb_common::{allow_err, get_time, message_proto::*, sleep}; use hbb_common::{fs, log, Stream}; use crate::client::{ - new_voice_call_request, Client, CodecFormat, LoginConfigHandler, MediaData, MediaSender, + new_voice_call_request, Client, CodecFormat, MediaData, MediaSender, QualityStatus, MILLI1, SEC30, SERVER_CLIPBOARD_ENABLED, SERVER_FILE_TRANSFER_ENABLED, SERVER_KEYBOARD_ENABLED, }; diff --git a/src/common.rs b/src/common.rs index 2142d973d..79a4664db 100644 --- a/src/common.rs +++ b/src/common.rs @@ -30,7 +30,7 @@ use hbb_common::{ // #[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))] use hbb_common::{config::RENDEZVOUS_PORT, futures::future::join_all}; -use crate::ui_interface::{set_option, get_option}; +use crate::ui_interface::{get_option, set_option}; pub type NotifyMessageBox = fn(String, String, String, String) -> dyn Future; @@ -762,8 +762,3 @@ pub fn make_fd_to_json(id: i32, path: String, entries: &Vec) -> Strin fd_json.insert("entries".into(), json!(entries_out)); serde_json::to_string(&fd_json).unwrap_or("".into()) } - -#[cfg(test)] -mod test_common { - -} diff --git a/src/core_main.rs b/src/core_main.rs index 03d057eff..0af7026e9 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -1,6 +1,4 @@ -use std::future::Future; - -use hbb_common::{log, ResultType}; +use hbb_common::log; /// shared by flutter and sciter main function /// diff --git a/src/ipc.rs b/src/ipc.rs index 0ede560fc..699b0bcd7 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -16,10 +16,10 @@ use hbb_common::{ config::{self, Config, Config2}, futures::StreamExt as _, futures_util::sink::SinkExt, - log, password_security as password, ResultType, timeout, - tokio, + log, password_security as password, timeout, tokio, tokio::io::{AsyncRead, AsyncWrite}, tokio_util::codec::Framed, + ResultType, }; use crate::rendezvous_mediator::RendezvousMediator; @@ -190,7 +190,7 @@ pub enum Data { Socks(Option), FS(FS), Test, - SyncConfig(Option<(Config, Config2)>), + SyncConfig(Option>), #[cfg(not(any(target_os = "android", target_os = "ios")))] ClipboardFile(ClipboardFile), ClipboardFileEnabled(bool), @@ -419,7 +419,8 @@ async fn handle(data: Data, stream: &mut Connection) { let t = Config::get_nat_type(); allow_err!(stream.send(&Data::NatType(Some(t))).await); } - Data::SyncConfig(Some((config, config2))) => { + Data::SyncConfig(Some(configs)) => { + let (config, config2) = *configs; let _chk = CheckIfRestart::new(); Config::set(config); Config2::set(config2); @@ -428,7 +429,9 @@ async fn handle(data: Data, stream: &mut Connection) { Data::SyncConfig(None) => { allow_err!( stream - .send(&Data::SyncConfig(Some((Config::get(), Config2::get())))) + .send(&Data::SyncConfig(Some( + (Config::get(), Config2::get()).into() + ))) .await ); } @@ -840,6 +843,19 @@ pub async fn test_rendezvous_server() -> ResultType<()> { #[tokio::main(flavor = "current_thread")] pub async fn send_url_scheme(url: String) -> ResultType<()> { - connect(1_000, "_url").await?.send(&Data::UrlLink(url)).await?; + connect(1_000, "_url") + .await? + .send(&Data::UrlLink(url)) + .await?; Ok(()) } + +#[cfg(test)] +mod test { + use super::*; + #[test] + fn verify_ffi_enum_data_size() { + println!("{}", std::mem::size_of::()); + assert!(std::mem::size_of::() < 96); + } +} diff --git a/src/keyboard.rs b/src/keyboard.rs index 91480ba30..17c52abf7 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -212,7 +212,7 @@ pub fn start_grab_loop() { } let mut _keyboard_mode = KeyboardMode::Map; - let scan_code = event.scan_code; + let _scan_code = event.scan_code; let res = if KEYBOARD_HOOKED.load(Ordering::SeqCst) { _keyboard_mode = client::process_event(&event, None); if is_press { @@ -225,7 +225,7 @@ pub fn start_grab_loop() { }; #[cfg(target_os = "windows")] - match scan_code { + match _scan_code { 0x1D | 0x021D => rdev::set_modifier(Key::ControlLeft, is_press), 0xE01D => rdev::set_modifier(Key::ControlRight, is_press), 0x2A => rdev::set_modifier(Key::ShiftLeft, is_press), @@ -241,7 +241,7 @@ pub fn start_grab_loop() { #[cfg(target_os = "windows")] unsafe { // AltGr - if scan_code == 0x021D { + if _scan_code == 0x021D { IS_0X021D_DOWN = is_press; } } diff --git a/src/server.rs b/src/server.rs index 616d92375..7807c4fac 100644 --- a/src/server.rs +++ b/src/server.rs @@ -8,6 +8,9 @@ use std::{ use bytes::Bytes; pub use connection::*; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use hbb_common::config::Config2; +use hbb_common::tcp::new_listener; use hbb_common::{ allow_err, anyhow::{anyhow, Context}, @@ -17,18 +20,15 @@ use hbb_common::{ message_proto::*, protobuf::{Enum, Message as _}, rendezvous_proto::*, - ResultType, socket_client, - sodiumoxide::crypto::{box_, secretbox, sign}, Stream, timeout, tokio, + sodiumoxide::crypto::{box_, secretbox, sign}, + timeout, tokio, ResultType, Stream, }; #[cfg(not(any(target_os = "android", target_os = "ios")))] -use hbb_common::config::Config2; -use hbb_common::tcp::new_listener; -use service::{GenericService, Service, Subscriber}; -#[cfg(not(any(target_os = "android", target_os = "ios")))] use service::ServiceTmpl; +use service::{GenericService, Service, Subscriber}; -use crate::ipc::{connect, Data}; +use crate::ipc::Data; pub mod audio_service; cfg_if::cfg_if! { @@ -65,7 +65,7 @@ type ConnMap = HashMap; lazy_static::lazy_static! { pub static ref CHILD_PROCESS: Childs = Default::default(); pub static ref CONN_COUNT: Arc> = Default::default(); - // A client server used to provide local services(audio, video, clipboard, etc.) + // A client server used to provide local services(audio, video, clipboard, etc.) // for all initiative connections. // // [Note] @@ -420,7 +420,8 @@ pub async fn start_server(is_server: bool) { if conn.send(&Data::SyncConfig(None)).await.is_ok() { if let Ok(Some(data)) = conn.next_timeout(1000).await { match data { - Data::SyncConfig(Some((config, config2))) => { + Data::SyncConfig(Some(configs)) => { + let (config, config2) = *configs; if Config::set(config) { log::info!("config synced"); } @@ -450,28 +451,26 @@ pub async fn start_ipc_url_server() { while let Some(Ok(conn)) = incoming.next().await { let mut conn = crate::ipc::Connection::new(conn); match conn.next_timeout(1000).await { - Ok(Some(data)) => { - match data { - Data::UrlLink(url) => { - #[cfg(feature = "flutter")] - { - if let Some(stream) = crate::flutter::GLOBAL_EVENT_STREAM.read().unwrap().get( - crate::flutter::APP_TYPE_MAIN - ) { - let mut m = HashMap::new(); - m.insert("name", "on_url_scheme_received"); - m.insert("url", url.as_str()); - stream.add(serde_json::to_string(&m).unwrap()); - } else { - log::warn!("No main window app found!"); - } - } - } - _ => { - log::warn!("An unexpected data was sent to the ipc url server.") + Ok(Some(data)) => match data { + #[cfg(feature = "flutter")] + Data::UrlLink(url) => { + if let Some(stream) = crate::flutter::GLOBAL_EVENT_STREAM + .read() + .unwrap() + .get(crate::flutter::APP_TYPE_MAIN) + { + let mut m = HashMap::new(); + m.insert("name", "on_url_scheme_received"); + m.insert("url", url.as_str()); + stream.add(serde_json::to_string(&m).unwrap()); + } else { + log::warn!("No main window app found!"); } } - } + _ => { + log::warn!("An unexpected data was sent to the ipc url server.") + } + }, Err(err) => { log::error!("{}", err); } @@ -509,7 +508,8 @@ async fn sync_and_watch_config_dir() { if conn.send(&Data::SyncConfig(None)).await.is_ok() { if let Ok(Some(data)) = conn.next_timeout(1000).await { match data { - Data::SyncConfig(Some((config, config2))) => { + Data::SyncConfig(Some(configs)) => { + let (config, config2) = *configs; let _chk = crate::ipc::CheckIfRestart::new(); if cfg0.0 != config { cfg0.0 = config.clone(); @@ -534,7 +534,7 @@ async fn sync_and_watch_config_dir() { let cfg = (Config::get(), Config2::get()); if cfg != cfg0 { log::info!("config updated, sync to root"); - match conn.send(&Data::SyncConfig(Some(cfg.clone()))).await { + match conn.send(&Data::SyncConfig(Some(cfg.clone().into()))).await { Err(e) => { log::error!("sync config to root failed: {}", e); break; diff --git a/src/ui/macos.rs b/src/ui/macos.rs index 98e355dc1..f34b7c2c1 100644 --- a/src/ui/macos.rs +++ b/src/ui/macos.rs @@ -14,12 +14,9 @@ use objc::{ sel, sel_impl, }; use objc::runtime::Class; -use objc_id::WeakId; use sciter::{Host, make_args}; -use hbb_common::{log, tokio}; - -use crate::ui_cm_interface::start_ipc; +use hbb_common::log; static APP_HANDLER_IVAR: &str = "GoDeskAppHandler"; @@ -141,7 +138,7 @@ extern "C" fn application_should_handle_open_untitled_file( if !LAUNCHED { return YES; } - hbb_common::log::debug!("icon clicked on finder"); + log::debug!("icon clicked on finder"); if std::env::args().nth(1) == Some("--server".to_owned()) { crate::platform::macos::check_main_window(); } @@ -267,4 +264,4 @@ pub fn make_tray() { set_delegate(None); } crate::tray::make_tray(); -} \ No newline at end of file +} diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index ccddab0ee..de33b0169 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -845,6 +845,7 @@ pub fn elevate_portable(_id: i32) { } } +#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] #[inline] pub fn handle_incoming_voice_call(id: i32, accept: bool) { if let Some(client) = CLIENTS.write().unwrap().get_mut(&id) { @@ -852,9 +853,10 @@ pub fn handle_incoming_voice_call(id: i32, accept: bool) { }; } +#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] #[inline] pub fn close_voice_call(id: i32) { if let Some(client) = CLIENTS.write().unwrap().get_mut(&id) { allow_err!(client.tx.send(Data::CloseVoiceCall("".to_owned()))); }; -} \ No newline at end of file +} From 1588e44d61b255ed3eb99529e90dd7e18e4779d9 Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 8 Feb 2023 19:17:43 +0800 Subject: [PATCH 1795/2015] win, translate mode, fix dead key Signed-off-by: fufesou --- Cargo.lock | 2 +- src/keyboard.rs | 21 +++++++++++++-------- src/ui_session_interface.rs | 6 +++--- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c53c573f2..83f623ca7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4405,7 +4405,7 @@ dependencies = [ [[package]] name = "rdev" version = "0.5.0-2" -source = "git+https://github.com/fufesou/rdev#4d8231f05e14c5a04cd7d2c1288e87ad52d39e4c" +source = "git+https://github.com/fufesou/rdev#cedc4e62744566775026af4b434ef799804c1130" dependencies = [ "cocoa", "core-foundation 0.9.3", diff --git a/src/keyboard.rs b/src/keyboard.rs index 17c52abf7..5b9920714 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -193,8 +193,8 @@ pub mod client { #[cfg(windows)] pub fn update_grab_get_key_name() { match get_keyboard_mode_enum() { - KeyboardMode::Map => rdev::set_get_key_name(false), - KeyboardMode::Translate => rdev::set_get_key_name(true), + KeyboardMode::Map => rdev::set_get_key_unicode(false), + KeyboardMode::Translate => rdev::set_get_key_unicode(true), _ => {} }; } @@ -256,6 +256,7 @@ pub fn start_grab_loop() { if let Err(error) = rdev::grab(func) { log::error!("rdev Error: {:?}", error) } + rdev::set_event_popup(false); }); #[cfg(target_os = "linux")] @@ -757,12 +758,10 @@ pub fn map_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option) { match &event.unicode { Some(unicode_info) => { - if !unicode_info.is_dead { - for code in &unicode_info.unicode { - let mut evt = key_event.clone(); - evt.set_unicode(*code as _); - events.push(evt); - } + for code in &unicode_info.unicode { + let mut evt = key_event.clone(); + evt.set_unicode(*code as _); + events.push(evt); } } None => {} @@ -816,6 +815,12 @@ pub fn translate_virtual_keycode(event: &Event, mut key_event: KeyEvent) -> Opti pub fn translate_keyboard_mode(event: &Event, key_event: KeyEvent) -> Vec { let mut events: Vec = Vec::new(); + if let Some(unicode_info) = &event.unicode { + if unicode_info.is_dead { + return events; + } + } + #[cfg(target_os = "windows")] unsafe { if event.scan_code == 0x021D { diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index dc0e365ab..87ea8e9eb 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -368,8 +368,8 @@ impl Session { #[cfg(target_os = "windows")] { match &self.lc.read().unwrap().keyboard_mode as _ { - "legacy" => rdev::set_get_key_name(true), - "translate" => rdev::set_get_key_name(true), + "legacy" => rdev::set_get_key_unicode(true), + "translate" => rdev::set_get_key_unicode(true), _ => {} } } @@ -381,7 +381,7 @@ impl Session { pub fn leave(&self) { #[cfg(target_os = "windows")] { - rdev::set_get_key_name(false); + rdev::set_get_key_unicode(false); } IS_IN.store(false, Ordering::SeqCst); keyboard::client::change_grab_status(GrabState::Wait); From c049e728fd3f49db3b99338c377131294ce90473 Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 8 Feb 2023 19:25:25 +0800 Subject: [PATCH 1796/2015] suppress warns Signed-off-by: fufesou --- src/flutter_ffi.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 2e6c450c1..bb1b8b8b9 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -928,7 +928,7 @@ pub fn main_start_dbus_server() { { use crate::dbus::start_dbus_server; // spawn new thread to start dbus server - std::thread::spawn(|| { + thread::spawn(|| { let _ = start_dbus_server(); }); } @@ -1275,7 +1275,7 @@ pub fn main_is_login_wayland() -> SyncReturn { pub fn main_start_pa() { #[cfg(target_os = "linux")] - std::thread::spawn(crate::ipc::start_pa); + thread::spawn(crate::ipc::start_pa); } pub fn main_hide_docker() -> SyncReturn { @@ -1302,9 +1302,9 @@ pub fn main_start_ipc_url_server() { /// /// * macOS only #[allow(unused_variables)] -pub fn send_url_scheme(url: String) { +pub fn send_url_scheme(_url: String) { #[cfg(target_os = "macos")] - thread::spawn(move || crate::ui::macos::handle_url_scheme(url)); + thread::spawn(move || crate::ui::macos::handle_url_scheme(_url)); } #[cfg(target_os = "android")] @@ -1324,7 +1324,7 @@ pub mod server_side { _class: JClass, ) { log::debug!("startServer from java"); - std::thread::spawn(move || start_server(true)); + thread::spawn(move || start_server(true)); } #[no_mangle] From 3a0137a3f71bef19fa3a0e440f3025c860cfcaf3 Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 8 Feb 2023 19:45:15 +0800 Subject: [PATCH 1797/2015] suppress warns Signed-off-by: fufesou --- src/flutter_ffi.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index bb1b8b8b9..ec4a90973 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1,6 +1,9 @@ -use std::{collections::HashMap, ffi::{CStr, CString}, os::raw::c_char, thread}; +use std::{collections::HashMap, ffi::{CStr, CString}, os::raw::c_char}; use std::str::FromStr; +#[cfg(any(target_os = "linux", target_os = "macos"))] +use std::thread; + use flutter_rust_bridge::{StreamSink, SyncReturn, ZeroCopyBuffer}; use serde_json::json; From 2feed1cdaf26c0f1a0f4f07819bfcb86b0e3934e Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 8 Feb 2023 20:00:16 +0800 Subject: [PATCH 1798/2015] though this change exe name to rustdesk, but it also change the name used in the other place --- flutter/macos/Runner.xcodeproj/project.pbxproj | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/flutter/macos/Runner.xcodeproj/project.pbxproj b/flutter/macos/Runner.xcodeproj/project.pbxproj index 18166c8ff..066560203 100644 --- a/flutter/macos/Runner.xcodeproj/project.pbxproj +++ b/flutter/macos/Runner.xcodeproj/project.pbxproj @@ -64,7 +64,7 @@ 295AD07E63F13855C270A0E0 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* rustdesk.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = rustdesk.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10ED2044A3C60003C045 /* RustDesk.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = RustDesk.app; sourceTree = BUILT_PRODUCTS_DIR; }; 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; @@ -127,7 +127,7 @@ 33CC10EE2044A3C60003C045 /* Products */ = { isa = PBXGroup; children = ( - 33CC10ED2044A3C60003C045 /* rustdesk.app */, + 33CC10ED2044A3C60003C045 /* RustDesk.app */, ); name = Products; sourceTree = ""; @@ -212,7 +212,7 @@ ); name = Runner; productName = Runner; - productReference = 33CC10ED2044A3C60003C045 /* rustdesk.app */; + productReference = 33CC10ED2044A3C60003C045 /* RustDesk.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ @@ -462,7 +462,6 @@ ); MACOSX_DEPLOYMENT_TARGET = 10.14; PRODUCT_BUNDLE_IDENTIFIER = com.carriez.rustdesk; - PRODUCT_NAME = rustdesk; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -608,7 +607,6 @@ ); MACOSX_DEPLOYMENT_TARGET = 10.14; PRODUCT_BUNDLE_IDENTIFIER = com.carriez.rustdesk; - PRODUCT_NAME = rustdesk; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; "SWIFT_OBJC_BRIDGING_HEADER[arch=*]" = Runner/bridge_generated.h; @@ -646,7 +644,6 @@ /dev/null, ); PRODUCT_BUNDLE_IDENTIFIER = com.carriez.rustdesk; - PRODUCT_NAME = rustdesk; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; "SWIFT_OBJC_BRIDGING_HEADER[arch=*]" = Runner/bridge_generated.h; From 80da209be8226a0af25b64511e6c35f36cfb8829 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 8 Feb 2023 20:09:07 +0800 Subject: [PATCH 1799/2015] change executable name from RustDesk to rustdesk in mac deployment --- .github/workflows/flutter-nightly.yml | 1 - build.py | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 5ca284cee..f03cd0be8 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -242,7 +242,6 @@ jobs: security unlock-keychain -p ${{ secrets.MACOS_P12_PASSWORD }} rustdesk.keychain # start sign the rustdesk.app and dmg rm rustdesk-${{ env.VERSION }}.dmg || true - mv ./flutter/build/macos/Build/Products/Release/rustdesk.app ./flutter/build/macos/Build/Products/Release/RustDesk.app codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep ./flutter/build/macos/Build/Products/Release/RustDesk.app -v create-dmg --icon "RustDesk.app" 200 190 --hide-extension "RustDesk.app" --window-size 800 400 --app-drop-link 600 185 rustdesk-${{ env.VERSION }}.dmg ./flutter/build/macos/Build/Products/Release/RustDesk.app codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep rustdesk-${{ env.VERSION }}.dmg -v diff --git a/build.py b/build.py index 6b107ff4b..dce434720 100755 --- a/build.py +++ b/build.py @@ -322,8 +322,9 @@ def build_flutter_dmg(version, features): os.system('sed -i "" "s/char \*\*rustdesk_core_main(int \*args_len);//" flutter/macos/Runner/bridge_generated.h') os.chdir('flutter') os.system('flutter build macos --release') + os.system('mv ./build/macos/Build/Products/Release/RustDesk.app/Contents/MacOS/RustDesk ./build/macos/Build/Products/Release/RustDesk.app/Contents/MacOS/rustdesk') os.system( - "create-dmg rustdesk.dmg ./build/macos/Build/Products/Release/rustdesk.app") + "create-dmg rustdesk.dmg ./build/macos/Build/Products/Release/RustDesk.app") os.rename("rustdesk.dmg", f"../rustdesk-{version}.dmg") os.chdir("..") From 3ae53a5d577b944baffb33da4ef9c959fefa1c72 Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 8 Feb 2023 20:41:19 +0800 Subject: [PATCH 1800/2015] fix build Signed-off-by: fufesou --- src/keyboard.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/keyboard.rs b/src/keyboard.rs index 5b9920714..28e151580 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -256,6 +256,7 @@ pub fn start_grab_loop() { if let Err(error) = rdev::grab(func) { log::error!("rdev Error: {:?}", error) } + #[cfg(target_os = "windows")] rdev::set_event_popup(false); }); From 0dba0130893b54951fe3df3b9ce4997037a0a7ca Mon Sep 17 00:00:00 2001 From: csf Date: Wed, 8 Feb 2023 21:54:48 +0900 Subject: [PATCH 1801/2015] remove unused Overlay in desktop_tab_page.dart and server_page.dart --- .../lib/desktop/pages/desktop_tab_page.dart | 28 +++++++--------- flutter/lib/desktop/pages/server_page.dart | 33 ++++++++----------- 2 files changed, 24 insertions(+), 37 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_tab_page.dart b/flutter/lib/desktop/pages/desktop_tab_page.dart index c1965921c..35d5a61ef 100644 --- a/flutter/lib/desktop/pages/desktop_tab_page.dart +++ b/flutter/lib/desktop/pages/desktop_tab_page.dart @@ -64,23 +64,17 @@ class _DesktopTabPageState extends State { @override Widget build(BuildContext context) { final tabWidget = Container( - child: Overlay(initialEntries: [ - OverlayEntry(builder: (context) { - gFFI.dialogManager.setOverlayState(Overlay.of(context)); - return Scaffold( - backgroundColor: Theme.of(context).backgroundColor, - body: DesktopTab( - controller: tabController, - tail: ActionIcon( - message: 'Settings', - icon: IconFont.menu, - onTap: DesktopTabPage.onAddSetting, - isClose: false, - ), - )); - }) - ]), - ); + child: Scaffold( + backgroundColor: Theme.of(context).backgroundColor, + body: DesktopTab( + controller: tabController, + tail: ActionIcon( + message: 'Settings', + icon: IconFont.menu, + onTap: DesktopTabPage.onAddSetting, + isClose: false, + ), + ))); return Platform.isMacOS ? tabWidget : Obx( diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index 521413647..b4d7f4fac 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -68,26 +68,19 @@ class _DesktopServerPageState extends State ], child: Consumer( builder: (context, serverModel, child) => Container( - decoration: BoxDecoration( - border: - Border.all(color: MyTheme.color(context).border!)), - child: Overlay(initialEntries: [ - OverlayEntry(builder: (context) { - gFFI.dialogManager.setOverlayState(Overlay.of(context)); - return Scaffold( - backgroundColor: Theme.of(context).backgroundColor, - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Expanded(child: ConnectionManager()), - ], - ), - ), - ); - }) - ]), - ))); + decoration: BoxDecoration( + border: Border.all(color: MyTheme.color(context).border!)), + child: Scaffold( + backgroundColor: Theme.of(context).backgroundColor, + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Expanded(child: ConnectionManager()), + ], + ), + ), + )))); } @override From 3d5aca18d690235ec1fb361b8526a25af7d46672 Mon Sep 17 00:00:00 2001 From: csf Date: Wed, 8 Feb 2023 22:01:15 +0900 Subject: [PATCH 1802/2015] refactor OverlayKeyState for OverlayDialogManager and ChatModel --- flutter/lib/common.dart | 30 +++++++++++-------- flutter/lib/common/widgets/overlay.dart | 24 ++++----------- .../lib/desktop/pages/file_manager_page.dart | 7 +++-- flutter/lib/desktop/pages/remote_page.dart | 16 ++++++---- flutter/lib/models/chat_model.dart | 18 +++++------ 5 files changed, 47 insertions(+), 48 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index a2623ff15..04e29eaa0 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -369,20 +369,25 @@ class Dialog { } } +class OverlayKeyState { + final _overlayKey = GlobalKey(); + + /// use global overlay by default + OverlayState? get state => + _overlayKey.currentState ?? globalKey.currentState?.overlay; + + GlobalKey? get key => _overlayKey; +} + class OverlayDialogManager { - OverlayState? _overlayState; final Map _dialogs = {}; + var _overlayKeyState = OverlayKeyState(); int _tagCount = 0; OverlayEntry? _mobileActionsOverlayEntry; - /// By default OverlayDialogManager use global overlay - OverlayDialogManager() { - _overlayState = globalKey.currentState?.overlay; - } - - void setOverlayState(OverlayState? overlayState) { - _overlayState = overlayState; + void setOverlayState(OverlayKeyState overlayKeyState) { + _overlayKeyState = overlayKeyState; } void dismissAll() { @@ -406,7 +411,7 @@ class OverlayDialogManager { bool useAnimation = true, bool forceGlobal = false}) { final overlayState = - forceGlobal ? globalKey.currentState?.overlay : _overlayState; + forceGlobal ? globalKey.currentState?.overlay : _overlayKeyState.state; if (overlayState == null) { return Future.error( @@ -510,7 +515,8 @@ class OverlayDialogManager { void showMobileActionsOverlay({FFI? ffi}) { if (_mobileActionsOverlayEntry != null) return; - if (_overlayState == null) return; + final overlayState = _overlayKeyState.state; + if (overlayState == null) return; // compute overlay position final screenW = MediaQuery.of(globalKey.currentContext!).size.width; @@ -536,7 +542,7 @@ class OverlayDialogManager { onHidePressed: () => hideMobileActionsOverlay(), ); }); - _overlayState!.insert(overlay); + overlayState.insert(overlay); _mobileActionsOverlayEntry = overlay; } @@ -1701,4 +1707,4 @@ Future updateSystemWindowTheme() async { : SystemWindowTheme.dark); } } -} \ No newline at end of file +} diff --git a/flutter/lib/common/widgets/overlay.dart b/flutter/lib/common/widgets/overlay.dart index 3e248700f..32dced02a 100644 --- a/flutter/lib/common/widgets/overlay.dart +++ b/flutter/lib/common/widgets/overlay.dart @@ -372,25 +372,12 @@ class QualityMonitor extends StatelessWidget { : const SizedBox.shrink())); } -class PenetrableOverlayState { +class BlockableOverlayState extends OverlayKeyState { final _middleBlocked = false.obs; - final _overlayKey = GlobalKey(); VoidCallback? onMiddleBlockedClick; // to-do use listener RxBool get middleBlocked => _middleBlocked; - GlobalKey get overlayKey => _overlayKey; - OverlayState? get overlayState => _overlayKey.currentState; - - OverlayState? getOverlayStateOrGlobal() { - if (overlayState == null) { - if (globalKey.currentState == null || - globalKey.currentState!.overlay == null) return null; - return globalKey.currentState!.overlay; - } else { - return overlayState; - } - } void addMiddleBlockedListener(void Function(bool) cb) { _middleBlocked.listen(cb); @@ -403,13 +390,13 @@ class PenetrableOverlayState { } } -class PenetrableOverlay extends StatelessWidget { +class BlockableOverlay extends StatelessWidget { final Widget underlying; final List? upperLayer; - final PenetrableOverlayState state; + final BlockableOverlayState state; - PenetrableOverlay( + BlockableOverlay( {required this.underlying, required this.state, this.upperLayer}); @override @@ -433,6 +420,7 @@ class PenetrableOverlay extends StatelessWidget { initialEntries.addAll(upperLayer!); } - return Overlay(key: state.overlayKey, initialEntries: initialEntries); + /// set key + return Overlay(key: state.key, initialEntries: initialEntries); } } diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index b6a9e5fed..9955c2768 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -80,6 +80,7 @@ class _FileManagerPageState extends State Entry? _lastClickEntry; final _dropMaskVisible = false.obs; // TODO impl drop mask + final _overlayKeyState = OverlayKeyState(); ScrollController getBreadCrumbScrollController(bool isLocal) { return isLocal ? _breadCrumbScrollerLocal : _breadCrumbScrollerRemote; @@ -115,6 +116,7 @@ class _FileManagerPageState extends State // register location listener _locationNodeLocal.addListener(onLocalLocationFocusChanged); _locationNodeRemote.addListener(onRemoteLocationFocusChanged); + _ffi.dialogManager.setOverlayState(_overlayKeyState); } @override @@ -137,9 +139,8 @@ class _FileManagerPageState extends State @override Widget build(BuildContext context) { super.build(context); - return Overlay(initialEntries: [ - OverlayEntry(builder: (context) { - _ffi.dialogManager.setOverlayState(Overlay.of(context)); + return Overlay(key: _overlayKeyState.key, initialEntries: [ + OverlayEntry(builder: (_) { return ChangeNotifierProvider.value( value: _ffi.fileModel, child: Consumer(builder: (context, model, child) { diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 4bda68c2d..c444d1f5f 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -62,7 +62,7 @@ class _RemotePageState extends State late RxBool _remoteCursorMoved; late RxBool _keyboardEnabled; - final overlayState = PenetrableOverlayState(); + final _blockableOverlayState = BlockableOverlayState(); final FocusNode _rawKeyFocusNode = FocusNode(debugLabel: "rawkeyFocusNode"); @@ -136,10 +136,11 @@ class _RemotePageState extends State // _isCustomCursorInited = true; // } - _ffi.chatModel.setPenetrableOverlayState(overlayState); + _ffi.dialogManager.setOverlayState(_blockableOverlayState); + _ffi.chatModel.setOverlayState(_blockableOverlayState); // make remote page penetrable automatically, effective for chat over remote - overlayState.onMiddleBlockedClick = () { - overlayState.setMiddleBlocked(false); + _blockableOverlayState.onMiddleBlockedClick = () { + _blockableOverlayState.setMiddleBlocked(false); }; } @@ -201,8 +202,11 @@ class _RemotePageState extends State Widget buildBody(BuildContext context) { return Scaffold( backgroundColor: Theme.of(context).backgroundColor, - body: PenetrableOverlay( - state: overlayState, + + /// the Overlay key will be set with _blockableOverlayState in BlockableOverlay + /// see override build() in [BlockableOverlay] + body: BlockableOverlay( + state: _blockableOverlayState, underlying: Container( color: Colors.black, child: RawKeyFocusScope( diff --git a/flutter/lib/models/chat_model.dart b/flutter/lib/models/chat_model.dart index b61ce79a7..8320d08dd 100644 --- a/flutter/lib/models/chat_model.dart +++ b/flutter/lib/models/chat_model.dart @@ -34,7 +34,7 @@ class ChatModel with ChangeNotifier { bool isConnManager = false; RxBool isWindowFocus = true.obs; - PenetrableOverlayState? pOverlayState; + BlockableOverlayState? _blockableOverlayState; final ChatUser me = ChatUser( id: "", @@ -53,10 +53,10 @@ class ChatModel with ChangeNotifier { bool get isShowCMChatPage => _isShowCMChatPage; - void setPenetrableOverlayState(PenetrableOverlayState state) { - pOverlayState = state; + void setOverlayState(BlockableOverlayState blockableOverlayState) { + _blockableOverlayState = blockableOverlayState; - pOverlayState!.addMiddleBlockedListener((v) { + _blockableOverlayState!.addMiddleBlockedListener((v) { if (!v) { isWindowFocus.value = false; if (isWindowFocus.value) { @@ -94,7 +94,7 @@ class ChatModel with ChangeNotifier { } } - final overlayState = pOverlayState?.getOverlayStateOrGlobal(); + final overlayState = _blockableOverlayState?.state; if (overlayState == null) return; final overlay = OverlayEntry(builder: (context) { @@ -129,16 +129,16 @@ class ChatModel with ChangeNotifier { showChatWindowOverlay({Offset? chatInitPos}) { if (chatWindowOverlayEntry != null) return; isWindowFocus.value = true; - pOverlayState?.setMiddleBlocked(true); + _blockableOverlayState?.setMiddleBlocked(true); - final overlayState = pOverlayState?.getOverlayStateOrGlobal(); + final overlayState = _blockableOverlayState?.state; if (overlayState == null) return; final overlay = OverlayEntry(builder: (context) { return Listener( onPointerDown: (_) { if (!isWindowFocus.value) { isWindowFocus.value = true; - pOverlayState?.setMiddleBlocked(true); + _blockableOverlayState?.setMiddleBlocked(true); } }, child: DraggableChatWindow( @@ -154,7 +154,7 @@ class ChatModel with ChangeNotifier { hideChatWindowOverlay() { if (chatWindowOverlayEntry != null) { - pOverlayState?.setMiddleBlocked(false); + _blockableOverlayState?.setMiddleBlocked(false); chatWindowOverlayEntry!.remove(); chatWindowOverlayEntry = null; return; From ac1ae9fc3bbfb7c7cd343222f59618957637093c Mon Sep 17 00:00:00 2001 From: csf Date: Wed, 8 Feb 2023 10:11:53 +0900 Subject: [PATCH 1803/2015] workaround: PageView reload --- .../lib/desktop/widgets/tabbar_widget.dart | 33 +++++++++++++++---- flutter/lib/models/model.dart | 4 +-- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 598b2cc4c..ddc51eddb 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -327,14 +327,32 @@ class DesktopTab extends StatelessWidget { )); } + List _tabWidgets = []; Widget _buildPageView() { return _buildBlock( child: Obx(() => PageView( controller: state.value.pageController, physics: NeverScrollableScrollPhysics(), - children: state.value.tabs - .map((tab) => tab.page) - .toList(growable: false)))); + children: () { + /// to-do refactor, separate connection state and UI state for remote session. + /// [workaround] PageView children need an immutable list, after it has been passed into PageView + final tabLen = state.value.tabs.length; + if (tabLen == _tabWidgets.length) { + return _tabWidgets; + } else if (_tabWidgets.isNotEmpty && + tabLen == _tabWidgets.length + 1) { + /// On add. Use the previous list(pointer) to prevent item's state init twice. + /// *[_tabWidgets.isNotEmpty] means TabsWindow(remote_tab_page or file_manager_tab_page) opened before, but was hidden. In this case, we have to reload, otherwise the child can't be built. + _tabWidgets.add(state.value.tabs.last.page); + return _tabWidgets; + } else { + /// On remove or change. Use new list(pointer) to reload list children so that items loading order is normal. + /// the Widgets in list must enable [AutomaticKeepAliveClientMixin] + final newList = state.value.tabs.map((v) => v.page).toList(); + _tabWidgets = newList; + return newList; + } + }()))); } /// Check whether to show ListView @@ -765,7 +783,8 @@ class _ListView extends StatelessWidget { tabBuilder: tabBuilder, tabMenuBuilder: tabMenuBuilder, maxLabelWidth: maxLabelWidth, - selectedTabBackgroundColor: selectedTabBackgroundColor ?? MyTheme.tabbar(context).selectedTabBackgroundColor, + selectedTabBackgroundColor: selectedTabBackgroundColor ?? + MyTheme.tabbar(context).selectedTabBackgroundColor, unSelectedTabBackgroundColor: unSelectedTabBackgroundColor, ); }).toList())); @@ -1119,7 +1138,8 @@ class TabbarTheme extends ThemeExtension { dividerColor: dividerColor ?? this.dividerColor, hoverColor: hoverColor ?? this.hoverColor, closeHoverColor: closeHoverColor ?? this.closeHoverColor, - selectedTabBackgroundColor: selectedTabBackgroundColor ?? this.selectedTabBackgroundColor, + selectedTabBackgroundColor: + selectedTabBackgroundColor ?? this.selectedTabBackgroundColor, ); } @@ -1145,7 +1165,8 @@ class TabbarTheme extends ThemeExtension { dividerColor: Color.lerp(dividerColor, other.dividerColor, t), hoverColor: Color.lerp(hoverColor, other.hoverColor, t), closeHoverColor: Color.lerp(closeHoverColor, other.closeHoverColor, t), - selectedTabBackgroundColor: Color.lerp(selectedTabBackgroundColor, other.selectedTabBackgroundColor, t), + selectedTabBackgroundColor: Color.lerp( + selectedTabBackgroundColor, other.selectedTabBackgroundColor, t), ); } diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 1eac1be39..5e4693ccc 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -17,7 +17,6 @@ import 'package:flutter_hbb/models/server_model.dart'; import 'package:flutter_hbb/models/user_model.dart'; import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/common/shared_state.dart'; -import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:tuple/tuple.dart'; import 'package:image/image.dart' as img2; import 'package:flutter_custom_cursor/cursor_manager.dart'; @@ -25,7 +24,6 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import '../common.dart'; -import '../common/shared_state.dart'; import '../utils/image.dart' as img; import '../mobile/widgets/dialog.dart'; import 'input_model.dart'; @@ -1348,13 +1346,13 @@ class FFI { canvasModel.y, canvasModel.scale, ffiModel.pi.currentDisplay); } bind.sessionClose(id: id); - id = ''; imageModel.update(null); cursorModel.clear(); ffiModel.clear(); canvasModel.clear(); inputModel.resetModifiers(); debugPrint('model $id closed'); + id = ''; } void setMethodCallHandler(FMethod callback) { From 552e45b320a6e1361580764332f8401801e7c160 Mon Sep 17 00:00:00 2001 From: csf Date: Wed, 8 Feb 2023 22:05:11 +0900 Subject: [PATCH 1804/2015] BlockableOverlay blocked layer transparent color --- flutter/lib/common/widgets/overlay.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/flutter/lib/common/widgets/overlay.dart b/flutter/lib/common/widgets/overlay.dart index 32dced02a..ba7b8a059 100644 --- a/flutter/lib/common/widgets/overlay.dart +++ b/flutter/lib/common/widgets/overlay.dart @@ -411,9 +411,8 @@ class BlockableOverlay extends StatelessWidget { state.onMiddleBlockedClick?.call(); }, child: Container( - color: state.middleBlocked.value - ? Colors.red.withOpacity(0.3) - : null)))), + color: + state.middleBlocked.value ? Colors.transparent : null)))), ]; if (upperLayer != null) { From 38d26ec47b2d44e351661b71451afcc6ebfaa275 Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 8 Feb 2023 21:50:18 +0800 Subject: [PATCH 1805/2015] fix altgr Signed-off-by: fufesou --- src/keyboard.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/keyboard.rs b/src/keyboard.rs index 28e151580..105b84400 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -840,6 +840,13 @@ pub fn translate_keyboard_mode(event: &Event, key_event: KeyEvent) -> Vec Vec Date: Wed, 8 Feb 2023 22:06:18 +0800 Subject: [PATCH 1806/2015] fix CI --- src/flutter_ffi.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index ec4a90973..ad0d119d7 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1,7 +1,7 @@ use std::{collections::HashMap, ffi::{CStr, CString}, os::raw::c_char}; use std::str::FromStr; -#[cfg(any(target_os = "linux", target_os = "macos"))] +#[cfg(any(target_os = "linux", target_os = "macos", target_os = "android"))] use std::thread; use flutter_rust_bridge::{StreamSink, SyncReturn, ZeroCopyBuffer}; From 45c66060e5f6a0fe812ad2fb13b1b4889df82b3d Mon Sep 17 00:00:00 2001 From: enforcer007 Date: Wed, 8 Feb 2023 22:20:48 +0530 Subject: [PATCH 1807/2015] devcontainer configuration --- .devcontainer/Dockerfile | 19 +++++++++++++++++++ .devcontainer/devcontainer.json | 29 +++++++++++++++++++++++++++++ .gitignore | 5 +++++ 3 files changed, 53 insertions(+) create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 000000000..0381ff966 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,19 @@ +FROM debian + +WORKDIR / +RUN apt update -y && 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 unzip zip sudo libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev + +RUN git clone https://github.com/microsoft/vcpkg && cd vcpkg && git checkout 134505003bb46e20fbace51ccfb69243fbbc5f82 +RUN /vcpkg/bootstrap-vcpkg.sh -disableMetrics +RUN /vcpkg/vcpkg --disable-metrics install libvpx libyuv opus + +RUN groupadd -r user && useradd -r -g user user --home /home/user && mkdir -p /home/user && chown user /home/user && echo "user ALL=(ALL) NOPASSWD:ALL" | sudo tee /etc/sudoers.d/user +WORKDIR /home/user +RUN wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so +USER user +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > rustup.sh +RUN chmod +x rustup.sh +RUN ./rustup.sh -y + +USER root +ENV HOME=/home/user diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000..24ba9a915 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,29 @@ +{ + "name": "rustdesk", + "build": { + "dockerfile": "Dockerfile", + "args": { + "BUILDKIT_INLINE_CACHE": "0" + } + }, + "workspaceMount": "source=${localWorkspaceFolder},target=/home/user/rustdesk,type=bind,consistency=cache", + "workspaceFolder": "/home/user/rustdesk", + "postStartCommand": "./entrypoint", + "remoteUser": "user", + "customizations": { + "vscode": { + "extensions": [ + "vadimcn.vscode-lldb", + "mutantdino.resourcemonitor", + "rust-lang.rust-analyzer", + "tamasfe.even-better-toml", + "serayuzgur.crates" + ], + "settings": { + "files.watcherExclude": { + "**/target/**": true + } + } + } + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index fd5b5955e..a71c71a4e 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,8 @@ flatpak/.flatpak-builder/debian-binary flatpak/build/** # bridge file lib/generated_bridge.dart +# vscode devcontainer +.gitconfig +.vscode-server/ +.ssh +.devcontainer/.* 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 1808/2015] Update de.rs --- src/lang/de.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/lang/de.rs b/src/lang/de.rs index 44bbafdac..1743505cc 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -392,7 +392,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("or", "oder"), ("Continue with", "Fortfahren mit"), ("Elevate", "Erheben"), - ("Zoom cursor", "Cursor zoomen"), + ("Zoom cursor", "Cursor vergrößern"), ("Accept sessions via password", "Sitzung mit Passwort bestätigen"), ("Accept sessions via click", "Sitzung mit einem Klick bestätigen"), ("Accept sessions via both", "Sitzung mit Klick und Passwort bestätigen"), @@ -414,8 +414,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Select local keyboard type", "Lokalen Tastaturtyp auswählen"), ("software_render_tip", "Wenn Sie eine Nvidia-Grafikkarte haben und sich das entfernte Fenster sofort nach dem Herstellen der Verbindung schließt, kann es helfen, den Nouveau-Treiber zu installieren und Software-Rendering zu verwenden. Ein Neustart der Software ist erforderlich."), ("Always use software rendering", "Software-Rendering immer verwenden"), - ("config_input", "Um den entfernten Desktop mit der Tastatur steuern zu können, müssen Sie RustDesk \"Input Monitoring\"-Rechte erteilen."), - ("config_microphone", ""), + ("config_input", "Um den entfernten Desktop mit der Tastatur steuern zu können, müssen Sie RustDesk die Berechtigung \"Input Monitoring\" erteilen."), + ("config_microphone", "Um aus der Ferne sprechen zu können, müssen Sie RustDesk die Berechtigung \"Audio aufzeichnen\" erteilen."), ("request_elevation_tip", "Sie können auch erhöhte Rechte anfordern, wenn sich jemand auf der Gegenseite befindet."), ("Wait", "Warten"), ("Elevation Error", "Berechtigungsfehler"), @@ -445,9 +445,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Bitrate", "Bitrate"), ("FPS", "fps"), ("Auto", "Automatisch"), - ("Other Default Options", "Weitere Standardoptionen"), - ("Voice call", ""), - ("Text chat", ""), - ("Stop voice call", ""), + ("Other Default Options", "Weitere Standardeinstellungen"), + ("Voice call", "Sprachanruf"), + ("Text chat", "Text-Chat"), + ("Stop voice call", "Sprachanruf beenden"), ].iter().cloned().collect(); } From 244cfa25f14f9ee86ac8fc371f5ff1f0823c35eb Mon Sep 17 00:00:00 2001 From: csf Date: Thu, 9 Feb 2023 10:29:35 +0900 Subject: [PATCH 1809/2015] opt dark theme in gesture_help.dart --- flutter/lib/mobile/widgets/gesture_help.dart | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/flutter/lib/mobile/widgets/gesture_help.dart b/flutter/lib/mobile/widgets/gesture_help.dart index 37cc77c8f..bc31ae2c4 100644 --- a/flutter/lib/mobile/widgets/gesture_help.dart +++ b/flutter/lib/mobile/widgets/gesture_help.dart @@ -2,8 +2,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; import 'package:toggle_switch/toggle_switch.dart'; -import '../../models/model.dart'; - class GestureIcons { static const String _family = 'gestureicons'; @@ -79,7 +77,10 @@ class _GestureHelpState extends State { children: [ ToggleSwitch( initialLabelIndex: _selectedIndex, - inactiveBgColor: MyTheme.darkGray, + activeFgColor: Colors.white, + inactiveFgColor: Colors.white60, + activeBgColor: [MyTheme.accent], + inactiveBgColor: Theme.of(context).hintColor, totalSwitches: 2, minWidth: 150, fontSize: 15, @@ -188,7 +189,7 @@ class GestureInfo extends StatelessWidget { @override Widget build(BuildContext context) { return Container( - width: this.width, + width: width, child: Column( children: [ Icon( @@ -199,11 +200,14 @@ class GestureInfo extends StatelessWidget { SizedBox(height: 6), Text(fromText, textAlign: TextAlign.center, - style: TextStyle(fontSize: 9, color: Colors.grey)), + style: + TextStyle(fontSize: 9, color: Theme.of(context).hintColor)), SizedBox(height: 3), Text(toText, textAlign: TextAlign.center, - style: TextStyle(fontSize: 12, color: Colors.black)) + style: TextStyle( + fontSize: 12, + color: Theme.of(context).textTheme.bodySmall?.color)) ], )); } From 4f25b03a10a41adf3220a35944b99cfc6ff6ea61 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 9 Feb 2023 16:54:26 +0800 Subject: [PATCH 1810/2015] fix CI --- src/flutter.rs | 15 +++++++++++---- src/flutter_ffi.rs | 48 ++++++++++++++++++++++------------------------ 2 files changed, 34 insertions(+), 29 deletions(-) diff --git a/src/flutter.rs b/src/flutter.rs index 2d7d3fb86..bf5746c13 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -1,5 +1,8 @@ -use crate::ui_session_interface::{io_loop, InvokeUiSession, Session}; -use crate::{client::*, flutter_ffi::EventToUI}; +use crate::{ + client::*, + flutter_ffi::EventToUI, + ui_session_interface::{io_loop, InvokeUiSession, Session}, +}; use flutter_rust_bridge::{StreamSink, ZeroCopyBuffer}; use hbb_common::{ bail, config::LocalConfig, get_version_number, message_proto::*, rendezvous_proto::ConnType, @@ -549,11 +552,15 @@ pub mod connection_manager { let mut h: HashMap<&str, &str> = event.iter().cloned().collect(); assert!(h.get("name").is_none()); h.insert("name", name); - + if let Some(s) = GLOBAL_EVENT_STREAM.read().unwrap().get(super::APP_TYPE_CM) { s.add(serde_json::ser::to_string(&h).unwrap_or("".to_owned())); } else { - println!("Push event {} failed. No {} event stream found.", name, super::APP_TYPE_CM); + println!( + "Push event {} failed. No {} event stream found.", + name, + super::APP_TYPE_CM + ); }; } } diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index ad0d119d7..a7e32d0b2 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1,27 +1,25 @@ -use std::{collections::HashMap, ffi::{CStr, CString}, os::raw::c_char}; -use std::str::FromStr; - -#[cfg(any(target_os = "linux", target_os = "macos", target_os = "android"))] -use std::thread; - -use flutter_rust_bridge::{StreamSink, SyncReturn, ZeroCopyBuffer}; -use serde_json::json; - -use hbb_common::{ - config::{self, LocalConfig, ONLINE, PeerConfig}, - fs, log, -}; -use hbb_common::message_proto::KeyboardMode; -use hbb_common::ResultType; - use crate::{ client::file_trait::FileManager, common::make_fd_to_json, + common::{get_default_sound_input, is_keyboard_mode_supported}, + flutter::{self, SESSIONS}, flutter::{session_add, session_start_}, + ui_interface::{self, *}, +}; +use flutter_rust_bridge::{StreamSink, SyncReturn, ZeroCopyBuffer}; +use hbb_common::{ + config::{self, LocalConfig, PeerConfig, ONLINE}, + fs, log, + message_proto::KeyboardMode, + ResultType, +}; +use serde_json::json; +use std::{ + collections::HashMap, + ffi::{CStr, CString}, + os::raw::c_char, + str::FromStr, }; -use crate::common::{get_default_sound_input, is_keyboard_mode_supported}; -use crate::flutter::{self, SESSIONS}; -use crate::ui_interface::{self, *}; // use crate::hbbs_http::account::AuthResult; @@ -931,7 +929,7 @@ pub fn main_start_dbus_server() { { use crate::dbus::start_dbus_server; // spawn new thread to start dbus server - thread::spawn(|| { + std::thread::spawn(|| { let _ = start_dbus_server(); }); } @@ -1278,7 +1276,7 @@ pub fn main_is_login_wayland() -> SyncReturn { pub fn main_start_pa() { #[cfg(target_os = "linux")] - thread::spawn(crate::ipc::start_pa); + std::thread::spawn(crate::ipc::start_pa); } pub fn main_hide_docker() -> SyncReturn { @@ -1298,7 +1296,7 @@ pub fn cm_start_listen_ipc_thread() { /// * macOS only pub fn main_start_ipc_url_server() { #[cfg(target_os = "macos")] - thread::spawn(move || crate::server::start_ipc_url_server()); + std::thread::spawn(move || crate::server::start_ipc_url_server()); } /// Send a url scheme throught the ipc. @@ -1307,16 +1305,16 @@ pub fn main_start_ipc_url_server() { #[allow(unused_variables)] pub fn send_url_scheme(_url: String) { #[cfg(target_os = "macos")] - thread::spawn(move || crate::ui::macos::handle_url_scheme(_url)); + std::thread::spawn(move || crate::ui::macos::handle_url_scheme(_url)); } #[cfg(target_os = "android")] pub mod server_side { use hbb_common::log; use jni::{ - JNIEnv, objects::{JClass, JString}, sys::jstring, + JNIEnv, }; use crate::start_server; @@ -1327,7 +1325,7 @@ pub mod server_side { _class: JClass, ) { log::debug!("startServer from java"); - thread::spawn(move || start_server(true)); + std::thread::spawn(move || start_server(true)); } #[no_mangle] From fcd1f9b4a3758112098108e563f429a7897576d8 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 9 Feb 2023 18:11:32 +0800 Subject: [PATCH 1811/2015] refactor handle_applicationShouldOpenUntitledFile --- src/flutter.rs | 6 +----- src/platform/macos.rs | 15 +++++++++++++-- src/ui/macos.rs | 18 +++++++++--------- 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/src/flutter.rs b/src/flutter.rs index bf5746c13..f60d9b30e 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -42,11 +42,7 @@ pub extern "C" fn rustdesk_core_main() -> bool { #[cfg(target_os = "macos")] #[no_mangle] pub extern "C" fn handle_applicationShouldOpenUntitledFile() { - hbb_common::log::debug!("icon clicked on finder"); - let x = std::env::args().nth(1).unwrap_or_default(); - if x == "--server" || x == "--cm" { - crate::platform::macos::check_main_window(); - } + crate::platform::macos::handle_applicationShouldOpenUntitledFile(); } #[cfg(windows)] diff --git a/src/platform/macos.rs b/src/platform/macos.rs index c7dbd9b73..b61f51732 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -557,7 +557,7 @@ pub fn hide_dock() { } } -pub fn check_main_window() { +fn check_main_window() -> bool { use sysinfo::{ProcessExt, System, SystemExt}; let mut sys = System::new(); sys.refresh_processes(); @@ -568,11 +568,22 @@ pub fn check_main_window() { .unwrap_or_default(); for (_, p) in sys.processes().iter() { if p.cmd().len() == 1 && p.user_id() == my_uid && p.cmd()[0].contains(&app) { - return; + return true; } } std::process::Command::new("open") .args(["-n", &app]) .status() .ok(); + false +} + +pub fn handle_applicationShouldOpenUntitledFile() { + hbb_common::log::debug!("icon clicked on finder"); + let x = std::env::args().nth(1).unwrap_or_default(); + if x == "--server" || x == "--cm" { + if crate::platform::macos::check_main_window() { + crate::ipc::send_url_scheme("rustdesk:".into()); + } + } } diff --git a/src/ui/macos.rs b/src/ui/macos.rs index f34b7c2c1..c6600608b 100644 --- a/src/ui/macos.rs +++ b/src/ui/macos.rs @@ -6,15 +6,15 @@ use cocoa::{ base::{id, nil, YES}, foundation::{NSAutoreleasePool, NSString}, }; +use objc::runtime::Class; use objc::{ class, declare::ClassDecl, msg_send, - runtime::{BOOL, Object, Sel}, + runtime::{Object, Sel, BOOL}, sel, sel_impl, }; -use objc::runtime::Class; -use sciter::{Host, make_args}; +use sciter::{make_args, Host}; use hbb_common::log; @@ -102,7 +102,10 @@ unsafe fn set_delegate(handler: Option>) { sel!(handleMenuItem:), handle_menu_item as extern "C" fn(&mut Object, Sel, id), ); - decl.add_method(sel!(handleEvent:withReplyEvent:), handle_apple_event as extern fn(&Object, Sel, u64, u64)); + decl.add_method( + sel!(handleEvent:withReplyEvent:), + handle_apple_event as extern "C" fn(&Object, Sel, u64, u64), + ); let decl = decl.register(); let delegate: id = msg_send![decl, alloc]; let () = msg_send![delegate, init]; @@ -138,10 +141,7 @@ extern "C" fn application_should_handle_open_untitled_file( if !LAUNCHED { return YES; } - log::debug!("icon clicked on finder"); - if std::env::args().nth(1) == Some("--server".to_owned()) { - crate::platform::macos::check_main_window(); - } + crate::platform::macos::handle_applicationShouldOpenUntitledFile(); let inner: *mut c_void = *this.get_ivar(APP_HANDLER_IVAR); let inner = &mut *(inner as *mut DelegateState); (*inner).command(AWAKE); @@ -191,7 +191,7 @@ pub fn handle_url_scheme(url: String) { } } -extern fn handle_apple_event(_this: &Object, _cmd: Sel, event: u64, _reply: u64) { +extern "C" fn handle_apple_event(_this: &Object, _cmd: Sel, event: u64, _reply: u64) { let event = event as *mut Object; let url = fruitbasket::parse_url_event(event); log::debug!("an event was received: {}", url); From c03adf53347ac519e2c4bb4327bbcca6599d06ab Mon Sep 17 00:00:00 2001 From: mehdi-song Date: Thu, 9 Feb 2023 15:30:41 +0330 Subject: [PATCH 1812/2015] Update fa.rs --- src/lang/fa.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lang/fa.rs b/src/lang/fa.rs index c206f91ff..8413673a1 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -446,8 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", "FPS"), ("Auto", "خودکار"), ("Other Default Options", "سایر گزینه های پیش فرض"), - ("Voice call", ""), - ("Text chat", ""), - ("Stop voice call", ""), + ("Voice call", "تماس صوتی"), + ("Text chat", "گفتگو متنی (چت متنی)"), + ("Stop voice call", "توقف تماس صوتی"), ].iter().cloned().collect(); } From 15a8460fcd36a690915fa52f984377a9e9570e65 Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Thu, 9 Feb 2023 13:36:48 +0100 Subject: [PATCH 1813/2015] removed SizedBox --- .../lib/desktop/pages/port_forward_page.dart | 57 +++++++++---------- 1 file changed, 27 insertions(+), 30 deletions(-) diff --git a/flutter/lib/desktop/pages/port_forward_page.dart b/flutter/lib/desktop/pages/port_forward_page.dart index f513a1c6a..2385813eb 100644 --- a/flutter/lib/desktop/pages/port_forward_page.dart +++ b/flutter/lib/desktop/pages/port_forward_page.dart @@ -179,36 +179,33 @@ class _PortForwardPageState extends State buildTunnelInputCell(context, controller: remotePortController, inputFormatters: portInputFormatter), - SizedBox( - width: _kColumn4Width, - child: ElevatedButton( - style: ElevatedButton.styleFrom( - elevation: 0, side: const BorderSide(color: MyTheme.border)), - onPressed: () async { - int? localPort = int.tryParse(localPortController.text); - int? remotePort = int.tryParse(remotePortController.text); - if (localPort != null && - remotePort != null && - (remoteHostController.text.isEmpty || - remoteHostController.text.trim().isNotEmpty)) { - await bind.sessionAddPortForward( - id: 'pf_${widget.id}', - localPort: localPort, - remoteHost: remoteHostController.text.trim().isEmpty - ? 'localhost' - : remoteHostController.text.trim(), - remotePort: remotePort); - localPortController.clear(); - remoteHostController.clear(); - remotePortController.clear(); - refreshTunnelConfig(); - } - }, - child: Text( - translate('Add'), - ), - ).marginAll(10), - ), + ElevatedButton( + style: ElevatedButton.styleFrom( + elevation: 0, side: const BorderSide(color: MyTheme.border)), + onPressed: () async { + int? localPort = int.tryParse(localPortController.text); + int? remotePort = int.tryParse(remotePortController.text); + if (localPort != null && + remotePort != null && + (remoteHostController.text.isEmpty || + remoteHostController.text.trim().isNotEmpty)) { + await bind.sessionAddPortForward( + id: 'pf_${widget.id}', + localPort: localPort, + remoteHost: remoteHostController.text.trim().isEmpty + ? 'localhost' + : remoteHostController.text.trim(), + remotePort: remotePort); + localPortController.clear(); + remoteHostController.clear(); + remotePortController.clear(); + refreshTunnelConfig(); + } + }, + child: Text( + translate('Add'), + ), + ).marginAll(10), ]), ); } From f7643077d339accf11896d9be4e1d876e0c88f99 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 9 Feb 2023 21:28:42 +0800 Subject: [PATCH 1814/2015] new tray --- Cargo.lock | 670 +++++++++++++----- Cargo.toml | 13 +- .../macos/Runner.xcodeproj/project.pbxproj | 16 - res/mac-tray-dark-x2.png | Bin 1585 -> 703 bytes res/mac-tray-dark.png | Bin 535 -> 0 bytes res/mac-tray-light-x2.png | Bin 1193 -> 728 bytes res/mac-tray-light.png | Bin 415 -> 0 bytes src/core_main.rs | 25 +- src/flutter.rs | 2 +- src/platform/macos.rs | 8 +- src/tray.rs | 224 ++---- src/ui/macos.rs | 9 +- 12 files changed, 569 insertions(+), 398 deletions(-) delete mode 100644 res/mac-tray-dark.png delete mode 100644 res/mac-tray-light.png diff --git a/Cargo.lock b/Cargo.lock index 83f623ca7..f0f66e287 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -153,7 +153,7 @@ checksum = "dc120354d1b5ec6d7aaf4876b602def75595937b5e15d356eb554ab5177e08bb" dependencies = [ "clipboard-win", "core-graphics 0.22.3", - "image", + "image 0.23.14", "log", "objc", "objc-foundation", @@ -278,24 +278,24 @@ dependencies = [ [[package]] name = "atk" -version = "0.15.1" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c3d816ce6f0e2909a96830d6911c2aff044370b1ef92d7f267b43bae5addedd" +checksum = "39991bc421ddf72f70159011b323ff49b0f783cc676a7287c59453da2e2531cf" dependencies = [ "atk-sys", "bitflags", - "glib 0.15.12", + "glib 0.16.5", "libc", ] [[package]] name = "atk-sys" -version = "0.15.1" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58aeb089fb698e06db8089971c7ee317ab9644bade33383f63631437b03aafb6" +checksum = "11ad703eb64dc058024f0e57ccfa069e15a413b98dbd50a1a950e743b7f11148" dependencies = [ - "glib-sys 0.15.10", - "gobject-sys 0.15.10", + "glib-sys 0.16.3", + "gobject-sys 0.16.3", "libc", "system-deps 6.0.3", ] @@ -405,6 +405,12 @@ dependencies = [ "syn", ] +[[package]] +name = "bit_field" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcb6dd1c2376d2e096796e234a70e17e94cc2d5d54ff8ce42b28cef1d0d359a4" + [[package]] name = "bitflags" version = "1.3.2" @@ -508,24 +514,25 @@ dependencies = [ [[package]] name = "cairo-rs" -version = "0.15.12" +version = "0.16.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c76ee391b03d35510d9fa917357c7f1855bd9a6659c95a1b392e33f49b3369bc" +checksum = "f3125b15ec28b84c238f6f476c6034016a5f6cc0221cb514ca46c532139fc97d" dependencies = [ "bitflags", "cairo-sys-rs", - "glib 0.15.12", + "glib 0.16.5", "libc", + "once_cell", "thiserror", ] [[package]] name = "cairo-sys-rs" -version = "0.15.1" +version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c55d429bef56ac9172d25fecb85dc8068307d17acd74b377866b7a1ef25d3c8" +checksum = "7c48f4af05fabdcfa9658178e1326efa061853f040ce7d72e33af6885196f421" dependencies = [ - "glib-sys 0.15.10", + "glib-sys 0.16.3", "libc", "system-deps 6.0.3", ] @@ -972,7 +979,7 @@ dependencies = [ "alsa", "core-foundation-sys 0.8.3", "coreaudio-rs", - "jni", + "jni 0.19.0", "js-sys", "lazy_static", "libc", @@ -1059,6 +1066,12 @@ dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "crypto-common" version = "0.1.6" @@ -1131,9 +1144,9 @@ dependencies = [ [[package]] name = "dark-light" -version = "0.2.3" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413487ef345ab5cdfbf23e66070741217a701bce70f2f397a54221b4f2b6056a" +checksum = "a62007a65515b3cd88c733dd3464431f05d2ad066999a824259d8edc3cf6f645" dependencies = [ "dconf_rs", "detect-desktop-environment", @@ -1712,6 +1725,22 @@ version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" +[[package]] +name = "exr" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8af5ef47e2ed89d23d0ecbc1b681b30390069de70260937877514377fc24feb" +dependencies = [ + "bit_field", + "flume", + "half", + "lebe", + "miniz_oxide 0.6.2", + "smallvec", + "threadpool", + "zune-inflate", +] + [[package]] name = "extend" version = "1.1.2" @@ -1794,6 +1823,19 @@ dependencies = [ "time 0.3.9", ] +[[package]] +name = "flume" +version = "0.10.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577" +dependencies = [ + "futures-core", + "futures-sink", + "nanorand", + "pin-project", + "spin 0.9.5", +] + [[package]] name = "flutter_rust_bridge" version = "1.61.1" @@ -2040,63 +2082,90 @@ dependencies = [ [[package]] name = "gdk" -version = "0.15.4" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6e05c1f572ab0e1f15be94217f0dc29088c248b14f792a5ff0af0d84bcda9e8" +checksum = "aa9cb33da481c6c040404a11f8212d193889e9b435db2c14fd86987f630d3ce1" dependencies = [ "bitflags", "cairo-rs", "gdk-pixbuf", "gdk-sys", "gio", - "glib 0.15.12", + "glib 0.16.5", "libc", "pango", ] [[package]] name = "gdk-pixbuf" -version = "0.15.11" +version = "0.16.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad38dd9cc8b099cceecdf41375bb6d481b1b5a7cd5cd603e10a69a9383f8619a" +checksum = "c3578c60dee9d029ad86593ed88cb40f35c1b83360e12498d055022385dd9a05" dependencies = [ "bitflags", "gdk-pixbuf-sys", "gio", - "glib 0.15.12", + "glib 0.16.5", "libc", ] [[package]] name = "gdk-pixbuf-sys" -version = "0.15.10" +version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "140b2f5378256527150350a8346dbdb08fadc13453a7a2d73aecd5fab3c402a7" +checksum = "3092cf797a5f1210479ea38070d9ae8a5b8e9f8f1be9f32f4643c529c7d70016" dependencies = [ - "gio-sys 0.15.10", - "glib-sys 0.15.10", - "gobject-sys 0.15.10", + "gio-sys", + "glib-sys 0.16.3", + "gobject-sys 0.16.3", "libc", "system-deps 6.0.3", ] [[package]] name = "gdk-sys" -version = "0.15.1" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e7a08c1e8f06f4177fb7e51a777b8c1689f743a7bc11ea91d44d2226073a88" +checksum = "d76354f97a913e55b984759a997b693aa7dc71068c9e98bcce51aa167a0a5c5a" dependencies = [ "cairo-sys-rs", "gdk-pixbuf-sys", - "gio-sys 0.15.10", - "glib-sys 0.15.10", - "gobject-sys 0.15.10", + "gio-sys", + "glib-sys 0.16.3", + "gobject-sys 0.16.3", "libc", "pango-sys", "pkg-config", "system-deps 6.0.3", ] +[[package]] +name = "gdkwayland-sys" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4511710212ed3020b61a8622a37aa6f0dd2a84516575da92e9b96928dcbe83ba" +dependencies = [ + "gdk-sys", + "glib-sys 0.16.3", + "gobject-sys 0.16.3", + "libc", + "pkg-config", + "system-deps 6.0.3", +] + +[[package]] +name = "gdkx11-sys" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa2bf8b5b8c414bc5d05e48b271896d0fd3ddb57464a3108438082da61de6af" +dependencies = [ + "gdk-sys", + "glib-sys 0.16.3", + "libc", + "system-deps 6.0.3", + "x11 2.20.1", +] + [[package]] name = "generic-array" version = "0.14.6" @@ -2124,8 +2193,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" dependencies = [ "cfg-if 1.0.0", + "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "gif" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3edd93c6756b4dfaf2709eafcc345ba2636565295c198a9cfbf75fa5e3e00b06" +dependencies = [ + "color_quant", + "weezl", ] [[package]] @@ -2136,34 +2217,24 @@ checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d" [[package]] name = "gio" -version = "0.15.12" +version = "0.16.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68fdbc90312d462781a395f7a16d96a2b379bb6ef8cd6310a2df272771c4283b" +checksum = "2a1c84b4534a290a29160ef5c6eff2a9c95833111472e824fc5cb78b513dd092" dependencies = [ "bitflags", "futures-channel", "futures-core", "futures-io", - "gio-sys 0.15.10", - "glib 0.15.12", + "futures-util", + "gio-sys", + "glib 0.16.5", "libc", "once_cell", + "pin-project-lite", + "smallvec", "thiserror", ] -[[package]] -name = "gio-sys" -version = "0.15.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32157a475271e2c4a023382e9cab31c4584ee30a97da41d3c4e9fdd605abcf8d" -dependencies = [ - "glib-sys 0.15.10", - "gobject-sys 0.15.10", - "libc", - "system-deps 6.0.3", - "winapi 0.3.9", -] - [[package]] name = "gio-sys" version = "0.16.3" @@ -2196,26 +2267,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "glib" -version = "0.15.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edb0306fbad0ab5428b0ca674a23893db909a98582969c9b537be4ced78c505d" -dependencies = [ - "bitflags", - "futures-channel", - "futures-core", - "futures-executor", - "futures-task", - "glib-macros 0.15.11", - "glib-sys 0.15.10", - "gobject-sys 0.15.10", - "libc", - "once_cell", - "smallvec", - "thiserror", -] - [[package]] name = "glib" version = "0.16.5" @@ -2228,7 +2279,7 @@ dependencies = [ "futures-executor", "futures-task", "futures-util", - "gio-sys 0.16.3", + "gio-sys", "glib-macros 0.16.3", "glib-sys 0.16.3", "gobject-sys 0.16.3", @@ -2254,21 +2305,6 @@ dependencies = [ "syn", ] -[[package]] -name = "glib-macros" -version = "0.15.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25a68131a662b04931e71891fb14aaf65ee4b44d08e8abc10f49e77418c86c64" -dependencies = [ - "anyhow", - "heck 0.4.0", - "proc-macro-crate 1.2.1", - "proc-macro-error", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "glib-macros" version = "0.16.3" @@ -2294,16 +2330,6 @@ dependencies = [ "system-deps 1.3.2", ] -[[package]] -name = "glib-sys" -version = "0.15.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef4b192f8e65e9cf76cbf4ea71fa8e3be4a0e18ffe3d68b8da6836974cc5bad4" -dependencies = [ - "libc", - "system-deps 6.0.3", -] - [[package]] name = "glib-sys" version = "0.16.3" @@ -2331,17 +2357,6 @@ dependencies = [ "system-deps 1.3.2", ] -[[package]] -name = "gobject-sys" -version = "0.15.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d57ce44246becd17153bd035ab4d32cfee096a657fc01f2231c9278378d1e0a" -dependencies = [ - "glib-sys 0.15.10", - "libc", - "system-deps 6.0.3", -] - [[package]] name = "gobject-sys" version = "0.16.3" @@ -2370,7 +2385,7 @@ dependencies = [ "gstreamer-sys", "libc", "muldiv", - "num-rational", + "num-rational 0.3.2", "once_cell", "paste", "pretty-hex", @@ -2488,9 +2503,9 @@ dependencies = [ [[package]] name = "gtk" -version = "0.15.5" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e3004a2d5d6d8b5057d2b57b3712c9529b62e82c77f25c1fecde1fd5c23bd0" +checksum = "e4d3507d43908c866c805f74c9dd593c0ce7ba5c38e576e41846639cdcd4bee6" dependencies = [ "atk", "bitflags", @@ -2500,7 +2515,7 @@ dependencies = [ "gdk", "gdk-pixbuf", "gio", - "glib 0.15.12", + "glib 0.16.5", "gtk-sys", "gtk3-macros", "libc", @@ -2511,17 +2526,17 @@ dependencies = [ [[package]] name = "gtk-sys" -version = "0.15.3" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5bc2f0587cba247f60246a0ca11fe25fb733eabc3de12d1965fc07efab87c84" +checksum = "89b5f8946685d5fe44497007786600c2f368ff6b1e61a16251c89f72a97520a3" dependencies = [ "atk-sys", "cairo-sys-rs", "gdk-pixbuf-sys", "gdk-sys", - "gio-sys 0.15.10", - "glib-sys 0.15.10", - "gobject-sys 0.15.10", + "gio-sys", + "glib-sys 0.16.3", + "gobject-sys 0.16.3", "libc", "pango-sys", "system-deps 6.0.3", @@ -2529,9 +2544,9 @@ dependencies = [ [[package]] name = "gtk3-macros" -version = "0.15.4" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24f518afe90c23fba585b2d7697856f9e6a7bbc62f65588035e66f6afb01a2e9" +checksum = "8cfd6557b1018b773e43c8de9d0d13581d6b36190d0501916cbec4731db5ccff" dependencies = [ "anyhow", "proc-macro-crate 1.2.1", @@ -2560,6 +2575,15 @@ dependencies = [ "tracing", ] +[[package]] +name = "half" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b4af3693f1b705df946e9fe5631932443781d0aabb423b62fcd4d73f6d2fd0" +dependencies = [ + "crunchy", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -2781,10 +2805,29 @@ dependencies = [ "byteorder", "color_quant", "num-iter", - "num-rational", + "num-rational 0.3.2", "num-traits 0.2.15", - "png", - "tiff", + "png 0.16.8", + "tiff 0.6.1", +] + +[[package]] +name = "image" +version = "0.24.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69b7ea949b537b0fd0af141fff8c77690f2ce96f4f41f042ccb6c69c6c965945" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "exr", + "gif", + "jpeg-decoder 0.3.0", + "num-rational 0.4.1", + "num-traits 0.2.15", + "png 0.17.7", + "scoped_threadpool", + "tiff 0.8.1", ] [[package]] @@ -2915,6 +2958,20 @@ dependencies = [ "walkdir", ] +[[package]] +name = "jni" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "039022cdf4d7b1cf548d31f60ae783138e5fd42013f6271049d7df7afadef96c" +dependencies = [ + "cesu8", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", +] + [[package]] name = "jni-sys" version = "0.3.0" @@ -2936,6 +2993,15 @@ version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "229d53d58899083193af11e15917b5640cd40b29ff475a1fe4ef725deb02d0f2" +[[package]] +name = "jpeg-decoder" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e" +dependencies = [ + "rayon", +] + [[package]] name = "js-sys" version = "0.3.60" @@ -2955,6 +3021,17 @@ dependencies = [ "winapi-build", ] +[[package]] +name = "keyboard-types" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7668b7cff6a51fe61cdde64cd27c8a220786f399501b57ebe36f7d8112fd68" +dependencies = [ + "bitflags", + "serde 1.0.149", + "unicode-segmentation", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -2968,12 +3045,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] -name = "libappindicator" -version = "0.7.1" +name = "lebe" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2d3cb96d092b4824cb306c9e544c856a4cb6210c1081945187f7f1924b47e8" +checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" + +[[package]] +name = "libappindicator" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e1edfdc9b0853358306c6dfb4b77c79c779174256fe93d80c0b5ebca451a2f" dependencies = [ - "glib 0.15.12", + "glib 0.16.5", "gtk", "gtk-sys", "libappindicator-sys", @@ -2982,9 +3065,9 @@ dependencies = [ [[package]] name = "libappindicator-sys" -version = "0.7.3" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1b3b6681973cea8cc3bce7391e6d7d5502720b80a581c9a95c9cbaf592826aa" +checksum = "08fcb2bea89cee9613982501ec83eaa2d09256b24540ae463c52a28906163918" dependencies = [ "gtk-sys", "libloading", @@ -3085,6 +3168,25 @@ dependencies = [ "walkdir", ] +[[package]] +name = "libxdo" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00333b8756a3d28e78def82067a377de7fa61b24909000aeaa2b446a948d14db" +dependencies = [ + "libxdo-sys", +] + +[[package]] +name = "libxdo-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db23b9e7e2b7831bbd8aac0bbeeeb7b68cbebc162b227e7052e8e55829a09212" +dependencies = [ + "libc", + "x11 2.20.1", +] + [[package]] name = "link-cplusplus" version = "1.0.7" @@ -3340,12 +3442,41 @@ dependencies = [ "glob", ] +[[package]] +name = "muda" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66365a21dc5e322c6b6ba25c735d00153c57dd2eb377926aa50e3caf547b6f6" +dependencies = [ + "cocoa", + "crossbeam-channel", + "gdk", + "gdk-pixbuf", + "gtk", + "keyboard-types", + "libxdo", + "objc", + "once_cell", + "png 0.17.7", + "thiserror", + "windows-sys 0.45.0", +] + [[package]] name = "muldiv" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0419348c027fa7be448d2ae7ea0e4e04c2334c31dc4e74ab29f00a2a7ca69204" +[[package]] +name = "nanorand" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +dependencies = [ + "getrandom", +] + [[package]] name = "ndk" version = "0.5.0" @@ -3616,6 +3747,17 @@ dependencies = [ "num-traits 0.2.15", ] +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg 1.1.0", + "num-integer", + "num-traits 0.2.15", +] + [[package]] name = "num-traits" version = "0.1.43" @@ -3728,7 +3870,7 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27f63c358b4fa0fbcfefd7c8be5cfc39c08ce2389f5325687e7762a48d30a5c1" dependencies = [ - "jni", + "jni 0.19.0", "ndk 0.6.0", "ndk-context", "num-derive", @@ -3747,9 +3889,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" +checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" [[package]] name = "openssl-probe" @@ -3783,20 +3925,15 @@ version = "6.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" -[[package]] -name = "padlock" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c10569378a1dacd9f30dbe7ae49e054d2c45dc2f8ee49899903e09c3924e8b6f" - [[package]] name = "pango" -version = "0.15.10" +version = "0.16.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e4045548659aee5313bde6c582b0d83a627b7904dd20dc2d9ef0895d414e4f" +checksum = "cdff66b271861037b89d028656184059e03b0b6ccb36003820be19f7200b1e94" dependencies = [ "bitflags", - "glib 0.15.12", + "gio", + "glib 0.16.5", "libc", "once_cell", "pango-sys", @@ -3804,12 +3941,12 @@ dependencies = [ [[package]] name = "pango-sys" -version = "0.15.10" +version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2a00081cde4661982ed91d80ef437c20eacaf6aa1a5962c0279ae194662c3aa" +checksum = "9e134909a9a293e04d2cc31928aa95679c5e4df954d0b85483159bd20d8f047f" dependencies = [ - "glib-sys 0.15.10", - "gobject-sys 0.15.10", + "glib-sys 0.16.3", + "gobject-sys 0.16.3", "libc", "system-deps 6.0.3", ] @@ -4005,6 +4142,18 @@ dependencies = [ "miniz_oxide 0.3.7", ] +[[package]] +name = "png" +version = "0.17.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d708eaf860a19b19ce538740d2b4bdeeb8337fa53f7738455e706623ad5c638" +dependencies = [ + "bitflags", + "crc32fast", + "flate2", + "miniz_oxide 0.6.2", +] + [[package]] name = "polling" version = "2.5.1" @@ -4547,7 +4696,7 @@ dependencies = [ "cc", "libc", "once_cell", - "spin", + "spin 0.5.2", "untrusted", "web-sys", "winapi 0.3.9", @@ -4690,15 +4839,13 @@ dependencies = [ "flutter_rust_bridge", "flutter_rust_bridge_codegen", "fruitbasket", - "glib 0.16.5", - "gtk", "hbb_common", "hound", + "image 0.24.5", "impersonate_system", "include_dir", - "jni", + "jni 0.19.0", "lazy_static", - "libappindicator", "libc", "libpulse-binding", "libpulse-simple-binding", @@ -4730,7 +4877,8 @@ dependencies = [ "sys-locale", "sysinfo", "system_shutdown", - "tray-item", + "tao", + "tray-icon", "trayicon", "url", "uuid", @@ -4868,6 +5016,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" +[[package]] +name = "scoped_threadpool" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8" + [[package]] name = "scopeguard" version = "1.1.0" @@ -4889,7 +5043,7 @@ dependencies = [ "gstreamer-video", "hbb_common", "hwcodec", - "jni", + "jni 0.19.0", "lazy_static", "libc", "log", @@ -5127,6 +5281,12 @@ version = "1.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" +[[package]] +name = "simd-adler32" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14a5df39617d7c8558154693a1bb8157a4aab8179209540cc0b10e5dc24e0b18" + [[package]] name = "simple_rc" version = "0.1.0" @@ -5217,6 +5377,15 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" +[[package]] +name = "spin" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dccf47db1b41fa1573ed27ccf5e08e3ca771cb994f776668c5ebda893b248fc" +dependencies = [ + "lock_api", +] + [[package]] name = "static_assertions" version = "1.1.0" @@ -5399,6 +5568,61 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "tao" +version = "0.17.0" +source = "git+https://github.com/tauri-apps/tao?branch=muda#676bd90a80286b893d8850cc4e3813a0c4a27dcf" +dependencies = [ + "bitflags", + "cairo-rs", + "cc", + "cocoa", + "core-foundation 0.9.3", + "core-graphics 0.22.3", + "crossbeam-channel", + "dispatch", + "gdk", + "gdk-pixbuf", + "gdk-sys", + "gdkwayland-sys", + "gdkx11-sys", + "gio", + "glib 0.16.5", + "glib-sys 0.16.3", + "gtk", + "image 0.24.5", + "instant", + "jni 0.20.0", + "lazy_static", + "libc", + "log", + "ndk 0.6.0", + "ndk-context", + "ndk-sys 0.3.0", + "objc", + "once_cell", + "parking_lot 0.12.1", + "png 0.17.7", + "raw-window-handle 0.5.0", + "scopeguard", + "tao-macros", + "unicode-segmentation", + "uuid", + "windows 0.44.0", + "windows-implement", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.0" +source = "git+https://github.com/tauri-apps/tao?branch=muda#676bd90a80286b893d8850cc4e3813a0c4a27dcf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tap" version = "1.0.1" @@ -5509,11 +5733,22 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a53f4706d65497df0c4349241deddf35f84cee19c87ed86ea8ca590f4464437" dependencies = [ - "jpeg-decoder", + "jpeg-decoder 0.1.22", "miniz_oxide 0.4.4", "weezl", ] +[[package]] +name = "tiff" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7449334f9ff2baf290d55d73983a7d6fa15e01198faef72af07e2a8db851e471" +dependencies = [ + "flate2", + "jpeg-decoder 0.3.0", + "weezl", +] + [[package]] name = "time" version = "0.1.45" @@ -5698,21 +5933,22 @@ dependencies = [ ] [[package]] -name = "tray-item" -version = "0.7.1" +name = "tray-icon" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0914b62e00e8f51241806cb9f9c4ea6b10c75d94cae02c89278de6f4b98c7d0f" +checksum = "d62801a4da61bb100b8d3174a5a46fed7b6ea03cc2ae93ee7340793b09a94ce3" dependencies = [ "cocoa", "core-graphics 0.22.3", - "gtk", + "crossbeam-channel", + "dirs-next", "libappindicator", - "libc", + "muda", "objc", - "objc-foundation", - "objc_id", - "padlock", - "winapi 0.3.9", + "once_cell", + "png 0.17.7", + "thiserror", + "windows-sys 0.45.0", ] [[package]] @@ -5811,9 +6047,9 @@ dependencies = [ [[package]] name = "uuid" -version = "1.2.2" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "422ee0de9031b5b948b97a8fc04e3aa35230001a722ddd27943e0be31564ce4c" +checksum = "1674845326ee10d37ca60470760d4288a6f80f304007d92e5c53bab78c9cfd79" dependencies = [ "getrandom", ] @@ -6242,6 +6478,39 @@ dependencies = [ "windows_x86_64_msvc 0.34.0", ] +[[package]] +name = "windows" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e745dab35a0c4c77aa3ce42d595e13d2003d6902d6b08c9ef5fc326d08da12b" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-targets", +] + +[[package]] +name = "windows-implement" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce87ca8e3417b02dc2a8a22769306658670ec92d78f1bd420d6310a67c245c6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "853f69a591ecd4f810d29f17e902d40e349fb05b0b11fff63b08b826bfe39c7f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-service" version = "0.4.0" @@ -6287,19 +6556,43 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" dependencies = [ "windows_aarch64_gnullvm", - "windows_aarch64_msvc 0.42.0", - "windows_i686_gnu 0.42.0", - "windows_i686_msvc 0.42.0", - "windows_x86_64_gnu 0.42.0", + "windows_aarch64_msvc 0.42.1", + "windows_i686_gnu 0.42.1", + "windows_i686_msvc 0.42.1", + "windows_x86_64_gnu 0.42.1", "windows_x86_64_gnullvm", - "windows_x86_64_msvc 0.42.0", + "windows_x86_64_msvc 0.42.1", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc 0.42.1", + "windows_i686_gnu 0.42.1", + "windows_i686_msvc 0.42.1", + "windows_x86_64_gnu 0.42.1", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc 0.42.1", ] [[package]] name = "windows_aarch64_gnullvm" -version = "0.42.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" +checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" [[package]] name = "windows_aarch64_msvc" @@ -6327,9 +6620,9 @@ checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" [[package]] name = "windows_aarch64_msvc" -version = "0.42.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" +checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" [[package]] name = "windows_i686_gnu" @@ -6357,9 +6650,9 @@ checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" [[package]] name = "windows_i686_gnu" -version = "0.42.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" +checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" [[package]] name = "windows_i686_msvc" @@ -6387,9 +6680,9 @@ checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" [[package]] name = "windows_i686_msvc" -version = "0.42.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" +checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" [[package]] name = "windows_x86_64_gnu" @@ -6417,15 +6710,15 @@ checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" [[package]] name = "windows_x86_64_gnu" -version = "0.42.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" +checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" [[package]] name = "windows_x86_64_gnullvm" -version = "0.42.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" +checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" [[package]] name = "windows_x86_64_msvc" @@ -6453,9 +6746,9 @@ checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" [[package]] name = "windows_x86_64_msvc" -version = "0.42.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" +checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" [[package]] name = "winit" @@ -6566,12 +6859,12 @@ dependencies = [ [[package]] name = "x11-dl" -version = "2.20.1" +version = "2.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1536d6965a5d4e573c7ef73a2c15ebcd0b2de3347bdf526c34c297c00ac40f0" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" dependencies = [ - "lazy_static", "libc", + "once_cell", "pkg-config", ] @@ -6703,6 +6996,15 @@ dependencies = [ "libc", ] +[[package]] +name = "zune-inflate" +version = "0.2.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c473377c11c4a3ac6a2758f944cd336678e9c977aa0abf54f6450cf77e902d6d" +dependencies = [ + "simd-adler32", +] + [[package]] name = "zvariant" version = "3.9.0" diff --git a/Cargo.toml b/Cargo.toml index b315024e9..9588d10b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -86,7 +86,6 @@ arboard = "2.0" system_shutdown = "3.0.0" [target.'cfg(target_os = "windows")'.dependencies] -#systray = { git = "https://github.com/open-trade/systray-rs" } trayicon = { git = "https://github.com/open-trade/trayicon-rs", features = ["winit"] } winit = "0.26" winapi = { version = "0.3", features = ["winuser"] } @@ -104,11 +103,15 @@ dispatch = "0.2" core-foundation = "0.9" core-graphics = "0.22" include_dir = "0.7.2" -tray-item = "0.7" # looks better than trayicon -dark-light = "0.2" +dark-light = "1.0" fruitbasket = "0.10.0" objc_id = "0.1.1" +[target.'cfg(any(target_os = "macos", target_os = "linux"))'.dependencies] +tray-icon = "0.4" +tao = { git = "https://github.com/tauri-apps/tao", branch = "muda" } +image = "0.24" + [target.'cfg(target_os = "linux")'.dependencies] psimple = { package = "libpulse-simple-binding", version = "2.25" } pulse = { package = "libpulse-binding", version = "2.26" } @@ -118,9 +121,6 @@ mouce = { git="https://github.com/fufesou/mouce.git" } evdev = { git="https://github.com/fufesou/evdev" } dbus = "0.9" dbus-crossroads = "0.5" -gtk = "0.15" -libappindicator = "0.7" -glib = "0.16.5" backtrace = "0.3" [target.'cfg(target_os = "android")'.dependencies] @@ -157,7 +157,6 @@ identifier = "com.carriez.rustdesk" icon = ["res/32x32.png", "res/128x128.png", "res/128x128@2x.png"] deb_depends = ["libgtk-3-0", "libxcb-randr0", "libxdo3", "libxfixes3", "libxcb-shape0", "libxcb-xfixes0", "libasound2", "libsystemd0", "curl", "libvdpau1", "libva2"] osx_minimum_system_version = "10.14" -resources = ["res/mac-tray-light.png","res/mac-tray-dark.png", "res/mac-tray-light-x2.png","res/mac-tray-dark-x2.png"] #https://github.com/johnthagen/min-sized-rust [profile.release] diff --git a/flutter/macos/Runner.xcodeproj/project.pbxproj b/flutter/macos/Runner.xcodeproj/project.pbxproj index 066560203..0019335ef 100644 --- a/flutter/macos/Runner.xcodeproj/project.pbxproj +++ b/flutter/macos/Runner.xcodeproj/project.pbxproj @@ -26,10 +26,6 @@ 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; - 7E4BCD762966B0EC006D24E2 /* mac-tray-light.png in Resources */ = {isa = PBXBuildFile; fileRef = 7E4BCD742966B0EC006D24E2 /* mac-tray-light.png */; }; - 7E4BCD772966B0EC006D24E2 /* mac-tray-dark.png in Resources */ = {isa = PBXBuildFile; fileRef = 7E4BCD752966B0EC006D24E2 /* mac-tray-dark.png */; }; - 7E881462296E98EE00A0C54F /* mac-tray-light-x2.png in Resources */ = {isa = PBXBuildFile; fileRef = 7E881461296E98ED00A0C54F /* mac-tray-light-x2.png */; }; - 7E881464296E991200A0C54F /* mac-tray-dark-x2.png in Resources */ = {isa = PBXBuildFile; fileRef = 7E881463296E991200A0C54F /* mac-tray-dark-x2.png */; }; 84010BA8292CF66600152837 /* liblibrustdesk.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 84010BA7292CF66600152837 /* liblibrustdesk.dylib */; settings = {ATTRIBUTES = (Weak, ); }; }; 84010BA9292CF68300152837 /* liblibrustdesk.dylib in Embed Libraries */ = {isa = PBXBuildFile; fileRef = 84010BA7292CF66600152837 /* liblibrustdesk.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; C5E54335B73C89F72DB1B606 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 26C84465887F29AE938039CB /* Pods_Runner.framework */; }; @@ -78,10 +74,6 @@ 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; 7436B85D94E8F7B5A9324869 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; - 7E4BCD742966B0EC006D24E2 /* mac-tray-light.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "mac-tray-light.png"; path = "../../res/mac-tray-light.png"; sourceTree = ""; }; - 7E4BCD752966B0EC006D24E2 /* mac-tray-dark.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "mac-tray-dark.png"; path = "../../res/mac-tray-dark.png"; sourceTree = ""; }; - 7E881461296E98ED00A0C54F /* mac-tray-light-x2.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "mac-tray-light-x2.png"; path = "../../res/mac-tray-light-x2.png"; sourceTree = ""; }; - 7E881463296E991200A0C54F /* mac-tray-dark-x2.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "mac-tray-dark-x2.png"; path = "../../res/mac-tray-dark-x2.png"; sourceTree = ""; }; 84010BA7292CF66600152837 /* liblibrustdesk.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = liblibrustdesk.dylib; path = ../../target/release/liblibrustdesk.dylib; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; C3BB669FF6190AE1B11BCAEA /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; @@ -135,10 +127,6 @@ 33CC11242044D66E0003C045 /* Resources */ = { isa = PBXGroup; children = ( - 7E881463296E991200A0C54F /* mac-tray-dark-x2.png */, - 7E881461296E98ED00A0C54F /* mac-tray-light-x2.png */, - 7E4BCD752966B0EC006D24E2 /* mac-tray-dark.png */, - 7E4BCD742966B0EC006D24E2 /* mac-tray-light.png */, 33CC10F22044A3C60003C045 /* Assets.xcassets */, 33CC10F42044A3C60003C045 /* MainMenu.xib */, 33CC10F72044A3C60003C045 /* Info.plist */, @@ -265,12 +253,8 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 7E881462296E98EE00A0C54F /* mac-tray-light-x2.png in Resources */, - 7E4BCD762966B0EC006D24E2 /* mac-tray-light.png in Resources */, - 7E4BCD772966B0EC006D24E2 /* mac-tray-dark.png in Resources */, 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, - 7E881464296E991200A0C54F /* mac-tray-dark-x2.png in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/res/mac-tray-dark-x2.png b/res/mac-tray-dark-x2.png index bdd48ad15ade67946a7c45b5ff1896ce96878ca3..595b850aef971e9e756aa55222318de018782cad 100644 GIT binary patch literal 703 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sT>(BJu0UD}24rMpfJ_Mq35WoM z3uH@5N+OFxxDYiES-2Lspt!g=L`qgx7A^>3NJ~pY7(jC%YzP_1Z50M|jc!ShUogX; zPhSLOC0_OOy}g+iuK4}l*`4VIlcIDUmnQ|Oi*B36aqQ@-1wBRP2P)&eYL+XTdN43B zZufL?4DmQVb=vi5O$GuE?H>AC3JaK$)?5GoU!AQiX~whbJLkEboF}(1XWW=Ln}sRR z=X_+|LH08R&9aXcTQ0c%j>A-~_v}yZMZs=~>+Sj`K4q~oZ?swZF20ws{#9mO7xS6p zv*M-xf7X8cr|q%C9{xh}55NDsj0jYI_dLJWc-Gc`{^~EpCq?P|-`<|ox1&a!|7`f7 z5dVh?i~dK=6|`r*x4*bzUT@8U#6LGryMOdJ5%^R4*!DTsJQy9WZr-qC)s$0`2i#6F zNleqSH+`bDAZK-ja!}j4ntRNrjlzREhrX;Y`fbcAG9%*mI0B4JkNw{^9Q zbb@w#(7L-<*v-B;1yA-gmehKveC*NN{)O{p|Gi}R{xtf`KkHn#D@h0RldLx9+-!7w z9X)+YOzNDKzc0zH%gZ+MSKFQXka5QH$Ey}6-w8I}r{h{XueFVdQ&MBb@04s+K-~a#s delta 1579 zcmV+`2Gse#1+ff}8Gi-<00374`G)`i010qNS#tmY8$kd78$kiGsWprM000?uMObuG zZ)S9NVRB^vcXxL#X>MzCV_|S*E^l&Yo9;Xs000HPNkl3Nt`OY^(%nZK-gTaul z>(3EUH#08<&EDYq5v*4^LbTO&%|Oe%kR0V(f}fnNYm8R)CMAY2S7If zs-T;TM05zizJGW;K3<6r&jEx&p`fN|pD^<#0F9NLv;eo6dB3J-kN9|bVqw<}4A=-Tx3^l@2z(SHEi+S)cV^Iia2Rh-E&oKB}- zP9~F+vJDE(0mCrf0B{(9T19i^7%P2MmvC~0K5d??YwggI*#)~ zB9Vv|>wh>Bi8M`5Pp<}W)v~Nh`N!_M1Ey(y%FH`G8lPn5-MX&t85$b;N2WnA7;Mya z{dH!ZH)qbA-e@#>y_hx>3N2Gr^#n4h2?N+A3%c#^FI*Lrg%Ia z_s~X~tohRbf_Z;mC!!^})MI89vTb_@yqW((2!HWvt;~nR;YUx^)1cw70jn z5r5Hg$^LiMYRu;`Dye(gFpPzY<2XH#+l<`OH0{gEeaVqZrH%l&B_UOas7E296_S1T z%C7T%7#|!G=G``kdlx#Dv(=&%5FXXFBAYWyPpNMHj+LjH9D#&a)%Ap&E5%y$q#`B_P6R3V}(lAXTn z!;@E5$%u&PiXyw_0$3Le25VK3n;|B_f5H_XBuBvdLs15I6v20~i<>xIskU6cG@Ng)j_bu3;G85z$*7nty$pE5*#D2#(|I25@thFv`q5LI~O9$`j#m_)!2q zGxM7snoXwD>0P-WGvjS!WMs@RjI98^MkYV{1HdXF#Bi~mOw%-7*If@_OxN|pvO}!3 zwY5`Gl&t_hg7@O_uF=uanQdpm+9)%3HZ(N+)ZgDfRl@w^%zOfOHsC1$$A18vb{wbA zah##Cv9XD)w%K*v2!LmZXcd50P+5Zbn-F3-GpCCNAVb#tDFDr7q`B%vHLBbb0G3;p zbv6G_&$WLK=7H*!=UryrAcVL&`+%q0=+S&N*(!uMTIhiE5DJAtK~+_MheXk-ia12H zB_5CODJGB}LaM5+!TmLV6Mw*lcszcrl*r2*_|u;sPh0}9ZuW#Ng!CM+EbAPAk5M}1 z@XI8C?LvqZGUfq)pBlUm|L@UqOaj=OPN#S0Kcr-+_+yX7ix8Jnz diff --git a/res/mac-tray-dark.png b/res/mac-tray-dark.png deleted file mode 100644 index a98fe63b0930e9e9358059dd6661e1eaadfd73fe..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 535 zcmV+y0_gpTP);W&wBJ!Z>At9 zUc^FsksPeMS(97nWkF(EkrOS0N(DabN?WuIt^u z;B$9>BO)IF-2JYIY?;}!s;a6qO@pf5EX(qpWLefTv#(Lx$7c3;JRYAdYaxWNWoAb} z$KAVXW;+plz6u6tn&tq=09Dl;1@1-p*Q?-}zD4K0a`9En?)qsBjH-T){F@3$l=pv; zMY;QeU%40(07T^Wx&pwR$p1ru_faxmMTQV^;Azy&K+S9)FyL%9!b_E9*>y8}3tX9n zOjTcryOXBrgCt2#i=y}$Ldct@c_|`8cmJZQ;_kPAClL&=S5?&uiI=I<>0A|&k3hQS z9gbS9*2Cd&_-kQlM5G6_SAYiW&0&bPajtf|eIrSdZJ_V&*MQ$5Qn>q|X_|w{WO5q& Z{{Yw=yte_XgkS&w002ovPDHLkV1j~<^7a4# diff --git a/res/mac-tray-light-x2.png b/res/mac-tray-light-x2.png index 253450ecbc102a345187896f2b65ab1900d8fcac..2e27118884994f5573334acc2cd962e373903619 100644 GIT binary patch literal 728 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sGXs1=T!HleK?$zPvV;H~XH*j8 z7tHYa&;1DkuOch-WyHd`et&qoYkO|8lDq`p_pdk39xKpMpWN#&eCy<@nHdS@j~C6& zJm4{9{Q_xkK2AHGgg-)V!0xPnYml1&)oC>@L8Y!wu-I= z_u`*gZa0|f#k4^q$y}Rv;>*<62@Td2S10A&2)BOWvQDx4`gf<#&MQyrhOYj%Nqwqu z&Nj`LN4C{VmHpUzr}bp-(~L}e`MA@|6so{@4lW7dl#JlIeizW zt$}C3o_|rb0={}jm6@fquWtU-sqr^nE&Jj6g}>`>{yC!d??LRx-~UwaNj7*LS^R_f ze^w3i4fE%rkHjN?vWgsO39#7s=#7BM&+iKiszp;@eEP+$HD4!p!bhuyQ=U(r_Fk9e zjo%#7l%L2kx5d2YK=0>9zGvateD7HPALMdj}EVtg> zvr;tv;7z;d!FfL?-v9bXd0tiS(&!s=O6vm`_vT+-H~Yg%fA+_2v3KX`=YLIo_mD4e k&&g9$GD7;yH~#%#KWoL|&dIu_pj5!%>FVdQ&MBb@03sKs`Tzg` delta 1184 zcmV;R1Yi5u1*r*;8Gi-<00374`G)`i010qNS#tmY8$kd78$kiGsWprM000?uMObuG zZ)S9NVRB^vcXxL#X>MzCV_|S*E^l&Yo9;Xs000CwNklZK`teW5`?A@Y>|d7 zW>hM)X;SYUCncSKi|1VDdS>o9&->nUryrcnJ^%B*|L1+*^L{+{v`L}_ZUr6$`hk@| z53m^MNbgsHzkng&81Mt|9q@az6}L4JKo{^d@FZruEuxGgz+T`(U?j?gq|j@jq}L=> zRUTuK-jcK=)PGNic&4NmOE#Y6qNL|T0`Lx@6~HIJT~XZ}&RXC{k1%m)Heh3Qy37MC z^9qZ4U@PzrFtbTRAq)bmfKiV)uLrgR?=!vTPXp_5nuo?c@Hp@}Fryq_E=~gvVLjkx zssL`oUZ9T2@*u{6VXT+ulHV9n=7CAm=qyP`Bd5SINq-+n+9Ih}Qj#fqnWT-9-j!6T zdu}Mz=S`Pg>9cFH_p-uQB8*ohy(DQ)frVXeQXg~pMzO44vn z{S`@_l7FtP16zTco$vhtU`Oz5;inro2+YsXbO4)SVja93m9IoJX$aOgIH z1+WCT+gbQ-;I}wmZq7P&_W|=0U>&~0$QZCE(o_gvq;(aM0P7Oq9%sS(fOD~~0;pg? zc<4{CGpvV$aekus#;IGG0KHD};Yc$v3^;YYZGVy~6mQ(F05<@oq%a)Z2>j#FjU>QL z4&gAn5~k)FiP3@0O7wusrlr@#{d&wK!!2d>LXERb{| z6X~F&b3%4->aR{0Te1U#a~N z`zPOO;Oo>_9<}U68CmWsX8uI|k1@G_uX!jhJwJ~9BYGlxBeWYs%$Lbd;LE9Qgnta7 zg}~4Fl1^oD6K8}c8Qimf_#n!3pV0000JM1D&kPZ zKeHVam-;#MTs&TCZ+{R-Lh?&;a)L@m=G9&GM*UE~)H`)gt!3k5V>|@){2$-y+8~0W zdNL$2#iKeI6BOuU8>sV(E^q{#2YVSP16#l=Fa^|r<8D3DAigiz5&$Mfy_$ zoJNjHPI3j|bip%rV7MRU2wc{ZzX_-%;nX@js(a3259AKP&(M002ov JPDHLkV1ggSrT_o{ diff --git a/src/core_main.rs b/src/core_main.rs index 0af7026e9..e2f3f80e0 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -164,9 +164,6 @@ pub fn core_main() -> Option> { #[cfg(feature = "with_rc")] hbb_common::allow_err!(crate::rc::extract_resources(&args[1])); return None; - } else if args[0] == "--tray" { - crate::tray::start_tray(); - return None; } else if args[0] == "--portable-service" { crate::platform::elevate_or_run_as_system( click_setup, @@ -183,34 +180,24 @@ pub fn core_main() -> Option> { std::fs::remove_file(&args[1]).ok(); return None; } + } else if args[0] == "--tray" { + crate::tray::start_tray(); + return None; } else if args[0] == "--service" { log::info!("start --service"); crate::start_os_service(); return None; } else if args[0] == "--server" { log::info!("start --server with user {}", crate::username()); - #[cfg(target_os = "windows")] + #[cfg(any(target_os = "linux", target_os = "windows"))] { crate::start_server(true); return None; } #[cfg(target_os = "macos")] - { - std::thread::spawn(move || crate::start_server(true)); - crate::platform::macos::hide_dock(); - crate::ui::macos::make_tray(); - return None; - } - #[cfg(target_os = "linux")] { let handler = std::thread::spawn(move || crate::start_server(true)); - // Show the tray in linux only when current user is a normal user - // [Note] - // As for GNOME, the tray cannot be shown in user's status bar. - // As for KDE, the tray can be shown without user's theme. - if !crate::platform::is_root() { - crate::tray::start_tray(); - } + crate::tray::start_tray(); // prevent server exit when encountering errors from tray hbb_common::allow_err!(handler.join()); } @@ -349,6 +336,6 @@ fn core_main_invoke_new_connection(mut args: std::env::Args) -> Option bool { #[cfg(target_os = "macos")] #[no_mangle] pub extern "C" fn handle_applicationShouldOpenUntitledFile() { - crate::platform::macos::handle_applicationShouldOpenUntitledFile(); + crate::platform::macos::handle_application_should_open_untitled_file(); } #[cfg(windows)] diff --git a/src/platform/macos.rs b/src/platform/macos.rs index b61f51732..0c8c51455 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -17,7 +17,7 @@ use core_graphics::{ display::{kCGNullWindowID, kCGWindowListOptionOnScreenOnly, CGWindowListCopyWindowInfo}, window::{kCGWindowName, kCGWindowOwnerPID}, }; -use hbb_common::{bail, log}; +use hbb_common::{allow_err, bail, log}; use include_dir::{include_dir, Dir}; use objc::{class, msg_send, sel, sel_impl}; use scrap::{libc::c_void, quartz::ffi::*}; @@ -578,12 +578,12 @@ fn check_main_window() -> bool { false } -pub fn handle_applicationShouldOpenUntitledFile() { +pub fn handle_application_should_open_untitled_file() { hbb_common::log::debug!("icon clicked on finder"); let x = std::env::args().nth(1).unwrap_or_default(); - if x == "--server" || x == "--cm" { + if x == "--server" || x == "--cm" || x == "--tray" { if crate::platform::macos::check_main_window() { - crate::ipc::send_url_scheme("rustdesk:".into()); + allow_err!(crate::ipc::send_url_scheme("rustdesk:".into())); } } } diff --git a/src/tray.rs b/src/tray.rs index e41a616de..b449bbbd3 100644 --- a/src/tray.rs +++ b/src/tray.rs @@ -1,11 +1,5 @@ -#[cfg(any(target_os = "linux", target_os = "windows"))] +#[cfg(any(target_os = "windows"))] use super::ui_interface::get_option_opt; -#[cfg(target_os = "linux")] -use hbb_common::log::{debug, error, info}; -#[cfg(target_os = "linux")] -use libappindicator::AppIndicator; -#[cfg(target_os = "linux")] -use std::env::temp_dir; #[cfg(target_os = "windows")] use std::sync::{Arc, Mutex}; #[cfg(target_os = "windows")] @@ -83,119 +77,10 @@ pub fn start_tray() { }); } -/// Start a tray icon in Linux -/// -/// [Block] -/// This function will block current execution, show the tray icon and handle events. -#[cfg(target_os = "linux")] -pub fn start_tray() { - use std::time::Duration; - - use glib::{clone, Continue}; - use gtk::traits::{GtkMenuItemExt, MenuShellExt, WidgetExt}; - - info!("configuring tray"); - // init gtk context - if let Err(err) = gtk::init() { - error!("Error when starting the tray: {}", err); - return; - } - if let Some(mut appindicator) = get_default_app_indicator() { - let mut menu = gtk::Menu::new(); - let stoped = is_service_stopped(); - // start/stop service - let label = if stoped { - crate::client::translate("Start Service".to_owned()) - } else { - crate::client::translate("Stop service".to_owned()) - }; - let menu_item_service = gtk::MenuItem::with_label(label.as_str()); - menu_item_service.connect_activate(move |_| { - let _lock = crate::ui_interface::SENDER.lock().unwrap(); - change_service_state(); - }); - menu.append(&menu_item_service); - // show tray item - menu.show_all(); - appindicator.set_menu(&mut menu); - // start event loop - info!("Setting tray event loop"); - // check the connection status for every second - glib::timeout_add_local( - Duration::from_secs(1), - clone!(@strong menu_item_service as item => move || { - let _lock = crate::ui_interface::SENDER.lock().unwrap(); - update_tray_service_item(&item); - // continue to trigger the next status check - Continue(true) - }), - ); - gtk::main(); - } else { - error!("Tray process exit now"); - } -} - -#[cfg(target_os = "linux")] -fn change_service_state() { - if is_service_stopped() { - debug!("Now try to start service"); - crate::ipc::set_option("stop-service", ""); - } else { - debug!("Now try to stop service"); - crate::ipc::set_option("stop-service", "Y"); - } -} - -#[cfg(target_os = "linux")] -#[inline] -fn update_tray_service_item(item: >k::MenuItem) { - use gtk::traits::GtkMenuItemExt; - - if is_service_stopped() { - item.set_label(&crate::client::translate("Start Service".to_owned())); - } else { - item.set_label(&crate::client::translate("Stop service".to_owned())); - } -} - -#[cfg(target_os = "linux")] -fn get_default_app_indicator() -> Option { - use libappindicator::AppIndicatorStatus; - use std::io::Write; - - let icon = include_bytes!("../res/icon.png"); - // appindicator does not support icon buffer, so we write it to tmp folder - let mut icon_path = temp_dir(); - icon_path.push("RustDesk"); - icon_path.push("rustdesk.png"); - match std::fs::File::create(icon_path.clone()) { - Ok(mut f) => { - f.write_all(icon).unwrap(); - // set .png icon file to be writable - // this ensures successful file rewrite when switching between x11 and wayland. - let mut perm = f.metadata().unwrap().permissions(); - if perm.readonly() { - perm.set_readonly(false); - f.set_permissions(perm).unwrap(); - } - } - Err(err) => { - error!("Error when writing icon to {:?}: {}", icon_path, err); - return None; - } - } - debug!("write temp icon complete"); - let mut appindicator = AppIndicator::new("RustDesk", icon_path.to_str().unwrap_or("rustdesk")); - appindicator.set_label("RustDesk", "A remote control software."); - appindicator.set_status(AppIndicatorStatus::Active); - Some(appindicator) -} - /// Check if service is stoped. /// Return [`true`] if service is stoped, [`false`] otherwise. #[inline] -#[cfg(any(target_os = "linux", target_os = "windows"))] +#[cfg(any(target_os = "windows"))] fn is_service_stopped() -> bool { if let Some(v) = get_option_opt("stop-service") { v == "Y" @@ -204,47 +89,68 @@ fn is_service_stopped() -> bool { } } -#[cfg(target_os = "macos")] -pub fn make_tray() { - extern "C" { - fn BackingScaleFactor() -> f32; - } - let f = unsafe { BackingScaleFactor() }; - use tray_item::TrayItem; - let mode = dark_light::detect(); - let icon_path = match mode { - dark_light::Mode::Dark => { - // still show big overflow icon in my test, so still use x1 png. - // let's do it with objc with svg support later. - // or use another tray crate, or find out in tauri (it has tray support) - if f > 2. { - "mac-tray-light-x2.png" - } else { - "mac-tray-light.png" - } - } - dark_light::Mode::Light => { - if f > 2. { - "mac-tray-dark-x2.png" - } else { - "mac-tray-dark.png" - } - } - }; - if let Ok(mut tray) = TrayItem::new(&crate::get_app_name(), icon_path) { - tray.add_label(&format!( - "{} {}", - crate::get_app_name(), - crate::lang::translate("Service is running".to_owned()) - )) - .ok(); +/// Start a tray icon in Linux +/// +/// [Block] +/// This function will block current execution, show the tray icon and handle events. +#[cfg(target_os = "linux")] +pub fn start_tray() {} - let inner = tray.inner_mut(); - inner.add_quit_item(&crate::lang::translate("Quit".to_owned())); - inner.display(); - } else { - loop { - std::thread::sleep(std::time::Duration::from_secs(3)); - } - } +#[cfg(target_os = "macos")] +pub fn start_tray() { + use hbb_common::{allow_err, log}; + allow_err!(make_tray()); +} + +#[cfg(target_os = "macos")] +pub fn make_tray() -> hbb_common::ResultType<()> { + // https://github.com/tauri-apps/tray-icon/blob/dev/examples/tao.rs + use hbb_common::anyhow::Context; + use tao::event_loop::{ControlFlow, EventLoopBuilder}; + use tray_icon::{TrayEvent, TrayIconBuilder}; + let mode = dark_light::detect(); + const LIGHT: &[u8] = include_bytes!("../res/mac-tray-light-x2.png"); + const DARK: &[u8] = include_bytes!("../res/mac-tray-dark-x2.png"); + let icon = match mode { + dark_light::Mode::Dark => DARK, + _ => LIGHT, + }; + let (icon_rgba, icon_width, icon_height) = { + let image = image::load_from_memory(icon) + .context("Failed to open icon path")? + .into_rgba8(); + let (width, height) = image.dimensions(); + let rgba = image.into_raw(); + (rgba, width, height) + }; + let icon = tray_icon::icon::Icon::from_rgba(icon_rgba, icon_width, icon_height) + .context("Failed to open icon")?; + + let event_loop = EventLoopBuilder::new().build(); + + let _tray_icon = Some( + TrayIconBuilder::new() + .with_tooltip(format!( + "{} {}", + crate::get_app_name(), + crate::lang::translate("Service is running".to_owned()) + )) + .with_icon(icon) + .build()?, + ); + + let tray_channel = TrayEvent::receiver(); + let mut docker_hiden = false; + + event_loop.run(move |_event, _, control_flow| { + if !docker_hiden { + crate::platform::macos::hide_dock(); + docker_hiden = true; + } + *control_flow = ControlFlow::Poll; + + if tray_channel.try_recv().is_ok() { + crate::platform::macos::handle_application_should_open_untitled_file(); + } + }); } diff --git a/src/ui/macos.rs b/src/ui/macos.rs index c6600608b..8a1fc990c 100644 --- a/src/ui/macos.rs +++ b/src/ui/macos.rs @@ -141,7 +141,7 @@ extern "C" fn application_should_handle_open_untitled_file( if !LAUNCHED { return YES; } - crate::platform::macos::handle_applicationShouldOpenUntitledFile(); + crate::platform::macos::handle_application_should_open_untitled_file(); let inner: *mut c_void = *this.get_ivar(APP_HANDLER_IVAR); let inner = &mut *(inner as *mut DelegateState); (*inner).command(AWAKE); @@ -258,10 +258,3 @@ pub fn show_dock() { NSApp().setActivationPolicy_(NSApplicationActivationPolicyRegular); } } - -pub fn make_tray() { - unsafe { - set_delegate(None); - } - crate::tray::make_tray(); -} From 1f5d68ef224ccdbb2ba00eed37419156ed640615 Mon Sep 17 00:00:00 2001 From: csf Date: Thu, 9 Feb 2023 22:55:56 +0900 Subject: [PATCH 1815/2015] workaround for https://github.com/rustdesk/rustdesk/issues/3131 --- flutter/lib/mobile/pages/remote_page.dart | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index c4b07b375..853f3168c 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -228,13 +228,18 @@ class _RemotePageState extends State { return false; }, child: getRawPointerAndKeyBody(Scaffold( - // resizeToAvoidBottomInset: true, + // workaround for https://github.com/rustdesk/rustdesk/issues/3131 + floatingActionButtonLocation: hideKeyboard + ? FABLocation(FloatingActionButtonLocation.endFloat, 0, -35) + : null, floatingActionButton: !showActionButton ? null : FloatingActionButton( mini: !hideKeyboard, child: Icon( - hideKeyboard ? Icons.expand_more : Icons.expand_less), + hideKeyboard ? Icons.expand_more : Icons.expand_less, + color: Colors.white, + ), backgroundColor: MyTheme.accent, onPressed: () { setState(() { @@ -1134,3 +1139,16 @@ void sendPrompt(bool isMac, String key) { gFFI.inputModel.ctrl = old; } } + +class FABLocation extends FloatingActionButtonLocation { + FloatingActionButtonLocation location; + double offsetX; + double offsetY; + FABLocation(this.location, this.offsetX, this.offsetY); + + @override + Offset getOffset(ScaffoldPrelayoutGeometry scaffoldGeometry) { + final offset = location.getOffset(scaffoldGeometry); + return Offset(offset.dx + offsetX, offset.dy + offsetY); + } +} From 2a0c9699e8bf7c2393fcd863b256c976cde8e4dc Mon Sep 17 00:00:00 2001 From: csf Date: Thu, 9 Feb 2023 23:00:34 +0900 Subject: [PATCH 1816/2015] move ImagePainter, and fix mobile drawImage quality --- flutter/lib/desktop/pages/remote_page.dart | 38 +-------------------- flutter/lib/mobile/pages/remote_page.dart | 27 +-------------- flutter/lib/utils/image.dart | 39 ++++++++++++++++++++++ 3 files changed, 41 insertions(+), 63 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index a7289335f..211d36c39 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -21,6 +21,7 @@ import '../../mobile/widgets/dialog.dart'; import '../../models/model.dart'; import '../../models/platform_model.dart'; import '../../common/shared_state.dart'; +import '../../utils/image.dart'; import '../widgets/remote_menubar.dart'; import '../widgets/kb_layout_type_chooser.dart'; @@ -685,40 +686,3 @@ class CursorPaint extends StatelessWidget { ); } } - -class ImagePainter extends CustomPainter { - ImagePainter({ - required this.image, - required this.x, - required this.y, - required this.scale, - }); - - ui.Image? image; - double x; - double y; - double scale; - - @override - void paint(Canvas canvas, Size size) { - if (image == null) return; - if (x.isNaN || y.isNaN) return; - canvas.scale(scale, scale); - // https://github.com/flutter/flutter/issues/76187#issuecomment-784628161 - // https://api.flutter-io.cn/flutter/dart-ui/FilterQuality.html - var paint = Paint(); - if ((scale - 1.0).abs() > 0.001) { - paint.filterQuality = FilterQuality.medium; - if (scale > 10.00000) { - paint.filterQuality = FilterQuality.high; - } - } - canvas.drawImage( - image!, Offset(x.toInt().toDouble(), y.toInt().toDouble()), paint); - } - - @override - bool shouldRepaint(CustomPainter oldDelegate) { - return oldDelegate != this; - } -} diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 853f3168c..956b985a7 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -17,6 +17,7 @@ import '../../common/widgets/remote_input.dart'; import '../../models/input_model.dart'; import '../../models/model.dart'; import '../../models/platform_model.dart'; +import '../../utils/image.dart'; import '../widgets/dialog.dart'; import '../widgets/gestures.dart'; @@ -898,32 +899,6 @@ class CursorPaint extends StatelessWidget { } } -class ImagePainter extends CustomPainter { - ImagePainter({ - required this.image, - required this.x, - required this.y, - required this.scale, - }); - - ui.Image? image; - double x; - double y; - double scale; - - @override - void paint(Canvas canvas, Size size) { - if (image == null) return; - canvas.scale(scale, scale); - canvas.drawImage(image!, Offset(x, y), Paint()); - } - - @override - bool shouldRepaint(CustomPainter oldDelegate) { - return oldDelegate != this; - } -} - void showOptions( BuildContext context, String id, OverlayDialogManager dialogManager) async { String quality = diff --git a/flutter/lib/utils/image.dart b/flutter/lib/utils/image.dart index 1f0d5b0cd..7a6bcbc15 100644 --- a/flutter/lib/utils/image.dart +++ b/flutter/lib/utils/image.dart @@ -1,6 +1,8 @@ import 'dart:typed_data'; import 'dart:ui' as ui; +import 'package:flutter/widgets.dart'; + Future decodeImageFromPixels( Uint8List pixels, int width, @@ -47,3 +49,40 @@ Future decodeImageFromPixels( descriptor.dispose(); return frameInfo.image; } + +class ImagePainter extends CustomPainter { + ImagePainter({ + required this.image, + required this.x, + required this.y, + required this.scale, + }); + + ui.Image? image; + double x; + double y; + double scale; + + @override + void paint(Canvas canvas, Size size) { + if (image == null) return; + if (x.isNaN || y.isNaN) return; + canvas.scale(scale, scale); + // https://github.com/flutter/flutter/issues/76187#issuecomment-784628161 + // https://api.flutter-io.cn/flutter/dart-ui/FilterQuality.html + var paint = Paint(); + if ((scale - 1.0).abs() > 0.001) { + paint.filterQuality = FilterQuality.medium; + if (scale > 10.00000) { + paint.filterQuality = FilterQuality.high; + } + } + canvas.drawImage( + image!, Offset(x.toInt().toDouble(), y.toInt().toDouble()), paint); + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) { + return oldDelegate != this; + } +} From 58f67481344524fafa2e041e7214744efe16c7b5 Mon Sep 17 00:00:00 2001 From: csf Date: Thu, 9 Feb 2023 23:14:24 +0900 Subject: [PATCH 1817/2015] fix physical keyboard on mobile does not work --- flutter/lib/common/widgets/remote_input.dart | 11 ++++- flutter/lib/mobile/pages/remote_page.dart | 52 ++++++++++---------- flutter/lib/models/input_model.dart | 14 +++--- 3 files changed, 44 insertions(+), 33 deletions(-) diff --git a/flutter/lib/common/widgets/remote_input.dart b/flutter/lib/common/widgets/remote_input.dart index 2fb409970..5833e760d 100644 --- a/flutter/lib/common/widgets/remote_input.dart +++ b/flutter/lib/common/widgets/remote_input.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hbb/models/state_model.dart'; +import '../../common.dart'; import '../../models/input_model.dart'; class RawKeyFocusScope extends StatelessWidget { @@ -19,6 +20,13 @@ class RawKeyFocusScope extends StatelessWidget { @override Widget build(BuildContext context) { + final FocusOnKeyCallback? onKey; + if (isAndroid) { + onKey = inputModel.handleRawKeyEvent; + } else { + onKey = stateGlobal.grabKeyboard ? inputModel.handleRawKeyEvent : null; + } + return FocusScope( autofocus: true, child: Focus( @@ -26,8 +34,7 @@ class RawKeyFocusScope extends StatelessWidget { canRequestFocus: true, focusNode: focusNode, onFocusChange: onFocusChange, - onKey: - stateGlobal.grabKeyboard ? inputModel.handleRawKeyEvent : null, + onKey: onKey, child: child)); } } diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 956b985a7..9ae856250 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -581,9 +581,10 @@ class _RemotePageState extends State { child: Text(translate('Reset canvas')), value: 'reset_canvas')); } if (perms['keyboard'] != false) { - more.add(PopupMenuItem( - child: Text(translate('Physical Keyboard Input Mode')), - value: 'input-mode')); + // * Currently mobile does not enable map mode + // more.add(PopupMenuItem( + // child: Text(translate('Physical Keyboard Input Mode')), + // value: 'input-mode')); if (pi.platform == kPeerPlatformLinux || pi.sasEnabled) { more.add(PopupMenuItem( child: Text('${translate('Insert')} Ctrl + Alt + Del'), @@ -638,8 +639,9 @@ class _RemotePageState extends State { ); if (value == 'cad') { bind.sessionCtrlAltDel(id: widget.id); - } else if (value == 'input-mode') { - changePhysicalKeyboardInputMode(); + // * Currently mobile does not enable map mode + // } else if (value == 'input-mode') { + // changePhysicalKeyboardInputMode(); } else if (value == 'lock') { bind.sessionLockScreen(id: widget.id); } else if (value == 'block-input') { @@ -701,26 +703,26 @@ class _RemotePageState extends State { })); } - void changePhysicalKeyboardInputMode() async { - var current = await bind.sessionGetKeyboardMode(id: widget.id) ?? "legacy"; - gFFI.dialogManager.show((setState, close) { - void setMode(String? v) async { - await bind.sessionPeerOption( - id: widget.id, name: "keyboard-mode", value: v ?? ""); - setState(() => current = v ?? ''); - Future.delayed(Duration(milliseconds: 300), close); - } - - return CustomAlertDialog( - title: Text(translate('Physical Keyboard Input Mode')), - content: Column(mainAxisSize: MainAxisSize.min, children: [ - getRadio('Legacy mode', 'legacy', current, setMode, - contentPadding: EdgeInsets.zero), - getRadio('Map mode', 'map', current, setMode, - contentPadding: EdgeInsets.zero), - ])); - }, clickMaskDismiss: true); - } + // * Currently mobile does not enable map mode + // void changePhysicalKeyboardInputMode() async { + // var current = await bind.sessionGetKeyboardMode(id: widget.id) ?? "legacy"; + // gFFI.dialogManager.show((setState, close) { + // void setMode(String? v) async { + // await bind.sessionSetKeyboardMode(id: widget.id, value: v ?? ""); + // setState(() => current = v ?? ''); + // Future.delayed(Duration(milliseconds: 300), close); + // } + // + // return CustomAlertDialog( + // title: Text(translate('Physical Keyboard Input Mode')), + // content: Column(mainAxisSize: MainAxisSize.min, children: [ + // getRadio('Legacy mode', 'legacy', current, setMode, + // contentPadding: EdgeInsets.zero), + // getRadio('Map mode', 'map', current, setMode, + // contentPadding: EdgeInsets.zero), + // ])); + // }, clickMaskDismiss: true); + // } Widget getHelpTools() { final keyboard = isKeyboardShown(); diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index 8c37f50bd..c37d01860 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -58,9 +58,12 @@ class InputModel { InputModel(this.parent); KeyEventResult handleRawKeyEvent(FocusNode data, RawKeyEvent e) { - bind.sessionGetKeyboardMode(id: id).then((result) { - keyboardMode = result.toString(); - }); + // * Currently mobile does not enable map mode + if (isDesktop) { + bind.sessionGetKeyboardMode(id: id).then((result) { + keyboardMode = result.toString(); + }); + } final key = e.logicalKey; if (e is RawKeyDownEvent) { @@ -93,10 +96,9 @@ class InputModel { } } - if (keyboardMode == 'map') { + // * Currently mobile does not enable map mode + if (isDesktop && keyboardMode == 'map') { mapKeyboardMode(e); - } else if (keyboardMode == 'translate') { - legacyKeyboardMode(e); } else { legacyKeyboardMode(e); } From 628fa513f7402550c23ac96e63bd17958fc1f6d5 Mon Sep 17 00:00:00 2001 From: csf Date: Thu, 9 Feb 2023 23:36:24 +0900 Subject: [PATCH 1818/2015] mobile remote_page.dart HelpTools add 'Insert' --- flutter/lib/mobile/pages/remote_page.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 9ae856250..54b6f1d47 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -814,6 +814,9 @@ class _RemotePageState extends State { wrap('End', () { inputModel.inputKey('VK_END'); }), + wrap('Ins', () { + inputModel.inputKey('VK_INSERT'); + }), wrap('Del', () { inputModel.inputKey('VK_DELETE'); }), From 73a2f41794a81603f8b603ac3a3c92e3f39fbe57 Mon Sep 17 00:00:00 2001 From: "Miguel F. G" <116861809+flusheDData@users.noreply.github.com> Date: Thu, 9 Feb 2023 16:18:36 +0100 Subject: [PATCH 1819/2015] Update es.rs New terms added --- src/lang/es.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lang/es.rs b/src/lang/es.rs index 220447454..939a4831f 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -446,8 +446,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", ""), ("Auto", ""), ("Other Default Options", "Otras opciones predeterminadas"), - ("Voice call", ""), - ("Text chat", ""), - ("Stop voice call", ""), + ("Voice call", "Llamada de voz"), + ("Text chat", "Chat de texto"), + ("Stop voice call", "Detener llamada de voz"), ].iter().cloned().collect(); } From 37a3185c1c92c7fc69c016ada8d24f5dda8eea10 Mon Sep 17 00:00:00 2001 From: solokot Date: Thu, 9 Feb 2023 20:17:34 +0300 Subject: [PATCH 1820/2015] Update ru.rs --- src/lang/ru.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 1e6c6962a..1792eccce 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -415,7 +415,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("software_render_tip", "Если у вас видеокарта Nvidia и удалённое окно закрывается сразу после подключения, может помочь установка драйвера Nouveau и выбор использования программной визуализации. Потребуется перезапуск."), ("Always use software rendering", "Использовать программную визуализацию"), ("config_input", "Чтобы управлять удалённым рабочим столом с помощью клавиатуры, необходимо предоставить RustDesk разрешения \"Мониторинг ввода\"."), - ("config_microphone", ""), + ("config_microphone", "Чтобы разговаривать с удалённой стороной, необходимо предоставить RustDesk разрешение \"Запись аудио\"."), ("request_elevation_tip", "Также можно запросить повышение прав, если кто-то есть на удалённой стороне."), ("Wait", "Ждите"), ("Elevation Error", "Ошибка повышения прав"), @@ -435,19 +435,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Medium", "Средний"), ("Strong", "Стойкий"), ("Switch Sides", "Переключить стороны"), - ("Please confirm if you want to share your desktop?", "Подтвердите, что хотите поделиться своим рабочим столом?"), - ("Closed as expected", ""), + ("Please confirm if you want to share your desktop?", "Подтверждаете, что хотите поделиться своим рабочим столом?"), + ("Closed as expected", "Закрыто по ожиданию"), ("Display", "Отображение"), ("Default View Style", "Стиль отображения по умолчанию"), ("Default Scroll Style", "Стиль прокрутки по умолчанию"), ("Default Image Quality", "Качество изображения по умолчанию"), ("Default Codec", "Кодек по умолчанию"), ("Bitrate", "Битрейт"), - ("FPS", "FPS"), + ("FPS", "Частота кадров"), ("Auto", "Авто"), ("Other Default Options", "Другие параметры по умолчанию"), - ("Voice call", ""), - ("Text chat", ""), - ("Stop voice call", ""), + ("Voice call", "Голосовой вызов"), + ("Text chat", "Текстовый чат"), + ("Stop voice call", "Завершить голосовой вызов"), ].iter().cloned().collect(); } From 9d88a06cdfffde6d28612799420479e2177a8bfa Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 10 Feb 2023 15:05:35 +0800 Subject: [PATCH 1821/2015] 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 0001d0762..965218c95 100644 --- a/flutter/assets/logo.svg +++ b/flutter/assets/logo.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 5c37900f2..9ba7a6315 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -234,7 +234,7 @@ class DesktopTab extends StatelessWidget { Key? key, required this.controller, this.showLogo = true, - this.showTitle = true, + this.showTitle = false, this.showMinimize = true, this.showMaximize = true, this.showClose = true, diff --git a/flutter/lib/desktop/widgets/titlebar_widget.dart b/flutter/lib/desktop/widgets/titlebar_widget.dart index 475b4cb86..38e4d917b 100644 --- a/flutter/lib/desktop/widgets/titlebar_widget.dart +++ b/flutter/lib/desktop/widgets/titlebar_widget.dart @@ -24,47 +24,8 @@ class DesktopTitleBar extends StatelessWidget { Expanded( child: child ?? Offstage(), ) - // const WindowButtons() ], ), ); } -} - -// final buttonColors = WindowButtonColors( -// iconNormal: const Color(0xFF805306), -// mouseOver: const Color(0xFFF6A00C), -// mouseDown: const Color(0xFF805306), -// iconMouseOver: const Color(0xFF805306), -// iconMouseDown: const Color(0xFFFFD500)); -// -// final closeButtonColors = WindowButtonColors( -// mouseOver: const Color(0xFFD32F2F), -// mouseDown: const Color(0xFFB71C1C), -// iconNormal: const Color(0xFF805306), -// iconMouseOver: Colors.white); -// -// class WindowButtons extends StatelessWidget { -// const WindowButtons({Key? key}) : super(key: key); -// -// @override -// Widget build(BuildContext context) { -// return Row( -// children: [ -// MinimizeWindowButton(colors: buttonColors, onPressed: () { -// windowManager.minimize(); -// },), -// MaximizeWindowButton(colors: buttonColors, onPressed: () async { -// if (await windowManager.isMaximized()) { -// windowManager.restore(); -// } else { -// windowManager.maximize(); -// } -// },), -// CloseWindowButton(colors: closeButtonColors, onPressed: () { -// windowManager.close(); -// },), -// ], -// ); -// } -// } +} \ No newline at end of file diff --git a/res/logo.svg b/res/logo.svg index 0001d0762..965218c95 100644 --- a/res/logo.svg +++ b/res/logo.svg @@ -1 +1 @@ - + \ No newline at end of file From 3c9e70d3a42b6038abf39050e4db2feefbe8ac5f Mon Sep 17 00:00:00 2001 From: grummbeer Date: Fri, 10 Feb 2023 09:31:43 +0100 Subject: [PATCH 1822/2015] fix autofocus --- flutter/lib/desktop/pages/desktop_setting_page.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 4b6cf2a62..366fb2ed7 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -1861,7 +1861,7 @@ void changeSocks5Proxy() async { border: const OutlineInputBorder(), errorText: proxyMsg.isNotEmpty ? proxyMsg : null), controller: proxyController, - focusNode: FocusNode()..requestFocus(), + autofocus: true, ), ), ], From be09728bf584030c1e79457bfd0e311b45548bee Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 10 Feb 2023 17:09:31 +0800 Subject: [PATCH 1823/2015] exclude ui module (sciter) for flutter --- src/cli.rs | 2 +- src/client/file_trait.rs | 4 +- src/common.rs | 11 +++ src/flutter_ffi.rs | 9 +-- src/lib.rs | 2 +- src/main.rs | 13 +++- src/ui.rs | 109 ++++++++++++++++++++++---- src/ui/macos.rs | 13 +--- src/ui_interface.rs | 161 +-------------------------------------- 9 files changed, 123 insertions(+), 201 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 117486ee4..40ab21188 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -36,7 +36,7 @@ impl Session { .lc .write() .unwrap() - .initialize(id.to_owned(), ConnType::PORT_FORWARD); + .initialize(id.to_owned(), ConnType::PORT_FORWARD, None); session } } diff --git a/src/client/file_trait.rs b/src/client/file_trait.rs index 2ecfca837..49e3f2358 100644 --- a/src/client/file_trait.rs +++ b/src/client/file_trait.rs @@ -7,7 +7,7 @@ pub trait FileManager: Interface { fs::get_home_as_string() } - #[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] + #[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli", feature = "flutter")))] fn read_dir(&self, path: String, include_hidden: bool) -> sciter::Value { match fs::read_dir(&fs::get_path(&path), include_hidden) { Err(_) => sciter::Value::null(), @@ -20,7 +20,7 @@ pub trait FileManager: Interface { } } - #[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))] + #[cfg(any(target_os = "android", target_os = "ios", feature = "cli", feature = "flutter"))] fn read_dir(&self, path: &str, include_hidden: bool) -> String { use crate::common::make_fd_to_json; match fs::read_dir(&fs::get_path(path), include_hidden) { diff --git a/src/common.rs b/src/common.rs index 79a4664db..b66261ebe 100644 --- a/src/common.rs +++ b/src/common.rs @@ -762,3 +762,14 @@ pub fn make_fd_to_json(id: i32, path: String, entries: &Vec) -> Strin fd_json.insert("entries".into(), json!(entries_out)); serde_json::to_string(&fd_json).unwrap_or("".into()) } + +/// The function to handle the url scheme sent by the system. +/// +/// 1. Try to send the url scheme from ipc. +/// 2. If failed to send the url scheme, we open a new main window to handle this url scheme. +pub fn handle_url_scheme(url: String) { + if let Err(err) = crate::ipc::send_url_scheme(url.clone()) { + log::debug!("Send the url to the existing flutter process failed, {}. Let's open a new program to handle this.", err); + let _ = crate::run_me(vec![url]); + } +} \ No newline at end of file diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index a7e32d0b2..a79ef2de8 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1119,13 +1119,6 @@ pub fn cm_switch_back(conn_id: i32) { crate::ui_cm_interface::switch_back(conn_id); } -pub fn main_get_icon() -> String { - #[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] - return ui_interface::get_icon(); - #[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))] - return String::new(); -} - pub fn main_get_build_date() -> String { crate::BUILD_DATE.to_string() } @@ -1305,7 +1298,7 @@ pub fn main_start_ipc_url_server() { #[allow(unused_variables)] pub fn send_url_scheme(_url: String) { #[cfg(target_os = "macos")] - std::thread::spawn(move || crate::ui::macos::handle_url_scheme(_url)); + std::thread::spawn(move || crate::handle_url_scheme(_url)); } #[cfg(target_os = "android")] diff --git a/src/lib.rs b/src/lib.rs index 7b94c8a2c..748d375b4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,7 +20,7 @@ pub use self::rendezvous_mediator::*; pub mod common; #[cfg(not(any(target_os = "ios")))] pub mod ipc; -#[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] +#[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli", feature = "flutter")))] pub mod ui; mod version; pub use version::*; diff --git a/src/main.rs b/src/main.rs index 6500a8e4a..8bc375841 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ // Requires Rust 1.18. //#![windows_subsystem = "windows"] +#[cfg(not(feature = "flutter"))] use librustdesk::*; #[cfg(any(target_os = "android", target_os = "ios"))] @@ -16,7 +17,12 @@ fn main() { common::global_clean(); } -#[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] +#[cfg(not(any( + target_os = "android", + target_os = "ios", + feature = "cli", + feature = "flutter" +)))] fn main() { if !common::global_init() { return; @@ -27,6 +33,11 @@ fn main() { common::global_clean(); } +#[cfg(feature = "flutter")] +fn main() { + hbb_common::log::info!("Hello world!"); +} + #[cfg(feature = "cli")] fn main() { if !common::global_init() { diff --git a/src/ui.rs b/src/ui.rs index 7973a0ba4..aede5fe7a 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -9,7 +9,7 @@ use sciter::Value; use hbb_common::{ allow_err, - config::{self, PeerConfig}, + config::{self, LocalConfig, PeerConfig}, log, }; @@ -38,6 +38,7 @@ lazy_static::lazy_static! { #[cfg(not(any(feature = "flutter", feature = "cli")))] lazy_static::lazy_static! { pub static ref CUR_SESSION: Arc>>> = Default::default(); + static ref CHILDREN : Children = Default::default(); } struct UIHostHandler; @@ -190,11 +191,11 @@ impl UI { } fn get_remote_id(&mut self) -> String { - get_remote_id() + LocalConfig::get_remote_id() } fn set_remote_id(&mut self, id: String) { - set_remote_id(id); + LocalConfig::set_remote_id(&id); } fn goto_install(&mut self) { @@ -309,7 +310,10 @@ impl UI { } fn is_release(&self) -> bool { - is_release() + #[cfg(not(debug_assertions))] + return true; + #[cfg(debug_assertions)] + return false; } fn is_rdp_service_open(&self) -> bool { @@ -329,11 +333,18 @@ impl UI { } fn closing(&mut self, x: i32, y: i32, w: i32, h: i32) { - closing(x, y, w, h) + crate::server::input_service::fix_key_down_timeout_at_exit(); + LocalConfig::set_size(x, y, w, h); } fn get_size(&mut self) -> Value { - Value::from_iter(get_size()) + let s = LocalConfig::get_size(); + let mut v = Vec::new(); + v.push(s.0); + v.push(s.1); + v.push(s.2); + v.push(s.3); + Value::from_iter(v) } fn get_mouse_time(&self) -> f64 { @@ -388,7 +399,7 @@ impl UI { fn get_recent_sessions(&mut self) -> Value { // to-do: limit number of recent sessions, and remove old peer file - let peers: Vec = get_recent_sessions() + let peers: Vec = PeerConfig::peers() .drain(..) .map(|p| Self::get_peer_value(p.0, p.2)) .collect(); @@ -396,11 +407,11 @@ impl UI { } fn get_icon(&mut self) -> String { - get_icon() + crate::get_icon() } fn remove_peer(&mut self, id: String) { - remove_peer(id) + PeerConfig::remove(&id); } fn remove_discovered(&mut self, id: String) { @@ -442,7 +453,7 @@ impl UI { } fn get_software_update_url(&self) -> String { - get_software_update_url() + crate::SOFTWARE_UPDATE_URL.lock().unwrap().clone() } fn get_new_version(&self) -> String { @@ -458,14 +469,30 @@ impl UI { } fn get_software_ext(&self) -> String { - get_software_ext() + #[cfg(windows)] + let p = "exe"; + #[cfg(target_os = "macos")] + let p = "dmg"; + #[cfg(target_os = "linux")] + let p = "deb"; + p.to_owned() } fn get_software_store_path(&self) -> String { - get_software_store_path() + let mut p = std::env::temp_dir(); + let name = crate::SOFTWARE_UPDATE_URL + .lock() + .unwrap() + .split("/") + .last() + .map(|x| x.to_owned()) + .unwrap_or(crate::get_app_name()); + p.push(name); + format!("{}.{}", p.to_string_lossy(), self.get_software_ext()) } fn create_shortcut(&self, _id: String) { + #[cfg(windows)] create_shortcut(_id) } @@ -495,7 +522,17 @@ impl UI { } fn open_url(&self, url: String) { - open_url(url) + #[cfg(windows)] + let p = "explorer"; + #[cfg(target_os = "macos")] + let p = "open"; + #[cfg(target_os = "linux")] + let p = if std::path::Path::new("/usr/bin/firefox").exists() { + "firefox" + } else { + "xdg-open" + }; + allow_err!(std::process::Command::new(p).arg(url).spawn()); } fn change_id(&self, id: String) { @@ -508,7 +545,7 @@ impl UI { } fn is_ok_change_id(&self) -> bool { - is_ok_change_id() + machine_uid::get().is_ok() } fn get_async_job_status(&self) -> String { @@ -516,11 +553,11 @@ impl UI { } fn t(&self, name: String) -> String { - t(name) + crate::client::translate(name) } fn is_xfce(&self) -> bool { - is_xfce() + crate::platform::is_xfce() } fn get_api_server(&self) -> String { @@ -683,3 +720,43 @@ pub fn value_crash_workaround(values: &[Value]) -> Arc> { STUPID_VALUES.lock().unwrap().push(persist.clone()); persist } + +#[inline] +pub fn new_remote(id: String, remote_type: String) { + let mut lock = CHILDREN.lock().unwrap(); + let args = vec![format!("--{}", remote_type), id.clone()]; + let key = (id.clone(), remote_type.clone()); + if let Some(c) = lock.1.get_mut(&key) { + if let Ok(Some(_)) = c.try_wait() { + lock.1.remove(&key); + } else { + if remote_type == "rdp" { + allow_err!(c.kill()); + std::thread::sleep(std::time::Duration::from_millis(30)); + c.try_wait().ok(); + lock.1.remove(&key); + } else { + return; + } + } + } + match crate::run_me(args) { + Ok(child) => { + lock.1.insert(key, child); + } + Err(err) => { + log::error!("Failed to spawn remote: {}", err); + } + } +} + +#[inline] +pub fn recent_sessions_updated() -> bool { + let mut children = CHILDREN.lock().unwrap(); + if children.0 { + children.0 = false; + true + } else { + false + } +} diff --git a/src/ui/macos.rs b/src/ui/macos.rs index 8a1fc990c..cd0e5871b 100644 --- a/src/ui/macos.rs +++ b/src/ui/macos.rs @@ -180,22 +180,11 @@ extern "C" fn handle_menu_item(this: &mut Object, _: Sel, item: id) { } } -/// The function to handle the url scheme sent by the system. -/// -/// 1. Try to send the url scheme from ipc. -/// 2. If failed to send the url scheme, we open a new main window to handle this url scheme. -pub fn handle_url_scheme(url: String) { - if let Err(err) = crate::ipc::send_url_scheme(url.clone()) { - log::debug!("Send the url to the existing flutter process failed, {}. Let's open a new program to handle this.", err); - let _ = crate::run_me(vec![url]); - } -} - extern "C" fn handle_apple_event(_this: &Object, _cmd: Sel, event: u64, _reply: u64) { let event = event as *mut Object; let url = fruitbasket::parse_url_event(event); log::debug!("an event was received: {}", url); - std::thread::spawn(move || handle_url_scheme(url)); + std::thread::spawn(move || crate::handle_url_scheme(url)); } unsafe fn make_menu_item(title: &str, key: &str, tag: u32) -> *mut Object { diff --git a/src/ui_interface.rs b/src/ui_interface.rs index d357c9cef..6576c340c 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -2,7 +2,6 @@ use std::{ collections::HashMap, process::Child, sync::{Arc, Mutex}, - time::SystemTime, }; #[cfg(any(target_os = "android", target_os = "ios"))] @@ -31,7 +30,6 @@ pub type Children = Arc)>>; type Status = (i32, bool, i64, String); // (status_num, key_confirmed, mouse_time, id) lazy_static::lazy_static! { - static ref CHILDREN : Children = Default::default(); static ref UI_STATUS : Arc> = Arc::new(Mutex::new((0, false, 0, "".to_owned()))); static ref OPTIONS : Arc>> = Arc::new(Mutex::new(Config::get_options())); static ref ASYNC_JOB_STATUS : Arc> = Default::default(); @@ -44,17 +42,6 @@ lazy_static::lazy_static! { pub static ref SENDER : Mutex> = Mutex::new(check_connect_status(true)); } -#[inline] -pub fn recent_sessions_updated() -> bool { - let mut children = CHILDREN.lock().unwrap(); - if children.0 { - children.0 = false; - true - } else { - false - } -} - #[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] #[inline] pub fn get_id() -> String { @@ -64,16 +51,6 @@ pub fn get_id() -> String { return ipc::get_id(); } -#[inline] -pub fn get_remote_id() -> String { - LocalConfig::get_remote_id() -} - -#[inline] -pub fn set_remote_id(id: String) { - LocalConfig::set_remote_id(&id); -} - #[inline] pub fn goto_install() { allow_err!(crate::run_me(vec!["--install"])); @@ -419,24 +396,6 @@ pub fn is_installed_lower_version() -> bool { } } -#[inline] -pub fn closing(x: i32, y: i32, w: i32, h: i32) { - #[cfg(not(any(target_os = "android", target_os = "ios")))] - crate::server::input_service::fix_key_down_timeout_at_exit(); - LocalConfig::set_size(x, y, w, h); -} - -#[inline] -pub fn get_size() -> Vec { - let s = LocalConfig::get_size(); - let mut v = Vec::new(); - v.push(s.0); - v.push(s.1); - v.push(s.2); - v.push(s.3); - v -} - #[inline] pub fn get_mouse_time() -> f64 { let ui_status = UI_STATUS.lock().unwrap(); @@ -507,51 +466,6 @@ pub fn store_fav(fav: Vec) { LocalConfig::set_fav(fav); } -#[inline] -pub fn get_recent_sessions() -> Vec<(String, SystemTime, PeerConfig)> { - PeerConfig::peers() -} - -#[inline] -#[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] -pub fn get_icon() -> String { - crate::get_icon() -} - -#[inline] -pub fn remove_peer(id: String) { - PeerConfig::remove(&id); -} - -#[inline] -pub fn new_remote(id: String, remote_type: String) { - let mut lock = CHILDREN.lock().unwrap(); - let args = vec![format!("--{}", remote_type), id.clone()]; - let key = (id.clone(), remote_type.clone()); - if let Some(c) = lock.1.get_mut(&key) { - if let Ok(Some(_)) = c.try_wait() { - lock.1.remove(&key); - } else { - if remote_type == "rdp" { - allow_err!(c.kill()); - std::thread::sleep(std::time::Duration::from_millis(30)); - c.try_wait().ok(); - lock.1.remove(&key); - } else { - return; - } - } - } - match crate::run_me(args) { - Ok(child) => { - lock.1.insert(key, child); - } - Err(err) => { - log::error!("Failed to spawn remote: {}", err); - } - } -} - #[inline] pub fn is_process_trusted(_prompt: bool) -> bool { #[cfg(target_os = "macos")] @@ -622,11 +536,6 @@ pub fn current_is_wayland() -> bool { return false; } -#[inline] -pub fn get_software_update_url() -> String { - SOFTWARE_UPDATE_URL.lock().unwrap().clone() -} - #[inline] pub fn get_new_version() -> String { hbb_common::get_version_from_url(&*SOFTWARE_UPDATE_URL.lock().unwrap()) @@ -643,36 +552,9 @@ pub fn get_app_name() -> String { crate::get_app_name() } -#[inline] -#[cfg(not(any(target_os = "android", target_os = "ios")))] -pub fn get_software_ext() -> String { - #[cfg(windows)] - let p = "exe"; - #[cfg(target_os = "macos")] - let p = "dmg"; - #[cfg(target_os = "linux")] - let p = "deb"; - p.to_owned() -} - -#[inline] -#[cfg(not(any(target_os = "android", target_os = "ios")))] -pub fn get_software_store_path() -> String { - let mut p = std::env::temp_dir(); - let name = SOFTWARE_UPDATE_URL - .lock() - .unwrap() - .split("/") - .last() - .map(|x| x.to_owned()) - .unwrap_or(crate::get_app_name()); - p.push(name); - format!("{}.{}", p.to_string_lossy(), get_software_ext()) -} - +#[cfg(windows)] #[inline] pub fn create_shortcut(_id: String) { - #[cfg(windows)] crate::platform::windows::create_shortcut(&_id).ok(); } @@ -719,22 +601,6 @@ pub fn get_uuid() -> String { base64::encode(hbb_common::get_uuid()) } -#[inline] -#[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] -pub fn open_url(url: String) { - #[cfg(windows)] - let p = "explorer"; - #[cfg(target_os = "macos")] - let p = "open"; - #[cfg(target_os = "linux")] - let p = if std::path::Path::new("/usr/bin/firefox").exists() { - "firefox" - } else { - "xdg-open" - }; - allow_err!(std::process::Command::new(p).arg(url).spawn()); -} - #[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] #[inline] pub fn change_id(id: String) { @@ -756,23 +622,11 @@ pub fn post_request(url: String, body: String, header: String) { }); } -#[inline] -#[cfg(not(any(target_os = "android", target_os = "ios")))] -pub fn is_ok_change_id() -> bool { - machine_uid::get().is_ok() -} - #[inline] pub fn get_async_job_status() -> String { ASYNC_JOB_STATUS.lock().unwrap().clone() } -#[inline] -#[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] -pub fn t(name: String) -> String { - crate::client::translate(name) -} - #[inline] pub fn get_langs() -> String { crate::lang::LANGS.to_string() @@ -813,11 +667,6 @@ pub fn default_video_save_directory() -> String { "".to_owned() } -#[inline] -pub fn is_xfce() -> bool { - crate::platform::is_xfce() -} - #[inline] pub fn get_api_server() -> String { crate::get_api_server( @@ -834,14 +683,6 @@ pub fn has_hwcodec() -> bool { return true; } -#[inline] -pub fn is_release() -> bool { - #[cfg(not(debug_assertions))] - return true; - #[cfg(debug_assertions)] - return false; -} - #[cfg(not(any(target_os = "android", target_os = "ios")))] #[inline] pub fn is_root() -> bool { From 930faecb13fbf3761f66aeeea7371903b5e741f3 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 10 Feb 2023 17:38:08 +0800 Subject: [PATCH 1824/2015] fix ci --- src/ui_interface.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 6576c340c..26038218e 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -511,9 +511,9 @@ pub fn get_error() -> String { if dtype != "x11" { return format!( "{} {}, {}", - t("Unsupported display server ".to_owned()), + crate::client::translate("Unsupported display server ".to_owned()), dtype, - t("x11 expected".to_owned()), + crate::client::translate("x11 expected".to_owned()), ); } } From 7edb3e6e92a90ba520edc52d8b66354c0f9a0378 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 10 Feb 2023 17:48:53 +0800 Subject: [PATCH 1825/2015] CI --- src/lib.rs | 2 ++ src/main.rs | 8 +------- src/platform/windows.rs | 12 ++++++------ src/server/connection.rs | 4 ++-- src/server/video_service.rs | 10 +++++----- src/ui.rs | 2 -- src/ui_cm_interface.rs | 2 +- src/{ui => }/win_privacy.rs | 0 8 files changed, 17 insertions(+), 23 deletions(-) rename src/{ui => }/win_privacy.rs (100%) diff --git a/src/lib.rs b/src/lib.rs index 748d375b4..5dcd6389c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -56,3 +56,5 @@ pub mod clipboard_file; #[cfg(all(windows, feature = "with_rc"))] pub mod rc; +#[cfg(target_os = "windows")] +pub mod win_privacy; diff --git a/src/main.rs b/src/main.rs index 8bc375841..169515425 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,10 +2,9 @@ // Requires Rust 1.18. //#![windows_subsystem = "windows"] -#[cfg(not(feature = "flutter"))] use librustdesk::*; -#[cfg(any(target_os = "android", target_os = "ios"))] +#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] fn main() { if !common::global_init() { return; @@ -33,11 +32,6 @@ fn main() { common::global_clean(); } -#[cfg(feature = "flutter")] -fn main() { - hbb_common::log::info!("Hello world!"); -} - #[cfg(feature = "cli")] fn main() { if !common::global_init() { diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 17f275c2a..bd6a1fc4c 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -833,8 +833,8 @@ fn get_default_install_path() -> String { pub fn check_update_broker_process() -> ResultType<()> { // let (_, path, _, _) = get_install_info(); - let process_exe = crate::ui::win_privacy::INJECTED_PROCESS_EXE; - let origin_process_exe = crate::ui::win_privacy::ORIGIN_PROCESS_EXE; + let process_exe = crate::win_privacy::INJECTED_PROCESS_EXE; + let origin_process_exe = crate::win_privacy::ORIGIN_PROCESS_EXE; let exe_file = std::env::current_exe()?; if exe_file.parent().is_none() { @@ -919,8 +919,8 @@ pub fn copy_exe_cmd(src_exe: &str, _exe: &str, path: &str) -> String { ", main_exe = main_exe, path = path, - ORIGIN_PROCESS_EXE = crate::ui::win_privacy::ORIGIN_PROCESS_EXE, - broker_exe = crate::ui::win_privacy::INJECTED_PROCESS_EXE, + ORIGIN_PROCESS_EXE = crate::win_privacy::ORIGIN_PROCESS_EXE, + broker_exe = crate::win_privacy::INJECTED_PROCESS_EXE, ); } @@ -938,7 +938,7 @@ pub fn update_me() -> ResultType<()> { {lic} ", copy_exe = copy_exe_cmd(&src_exe, &exe, &path), - broker_exe = crate::ui::win_privacy::INJECTED_PROCESS_EXE, + broker_exe = crate::win_privacy::INJECTED_PROCESS_EXE, app_name = crate::get_app_name(), lic = register_licence(), cur_pid = get_current_pid(), @@ -1203,7 +1203,7 @@ fn get_before_uninstall() -> String { netsh advfirewall firewall delete rule name=\"{app_name} Service\" ", app_name = app_name, - broker_exe = crate::ui::win_privacy::INJECTED_PROCESS_EXE, + broker_exe = crate::win_privacy::INJECTED_PROCESS_EXE, ext = ext, cur_pid = get_current_pid(), ) diff --git a/src/server/connection.rs b/src/server/connection.rs index 9ce53c960..53ccd7008 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -2045,7 +2045,7 @@ mod privacy_mode { pub(super) fn turn_off_privacy(_conn_id: i32) -> Message { #[cfg(windows)] { - use crate::ui::win_privacy::*; + use crate::win_privacy::*; let res = turn_off_privacy(_conn_id, None); match res { @@ -2069,7 +2069,7 @@ mod privacy_mode { pub(super) fn turn_on_privacy(_conn_id: i32) -> ResultType { #[cfg(windows)] { - let plugin_exist = crate::ui::win_privacy::turn_on_privacy(_conn_id)?; + let plugin_exist = crate::win_privacy::turn_on_privacy(_conn_id)?; Ok(plugin_exist) } #[cfg(not(windows))] diff --git a/src/server/video_service.rs b/src/server/video_service.rs index 57fdf2c22..bc9c5ff6f 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -207,7 +207,7 @@ fn create_capturer( if privacy_mode_id > 0 { #[cfg(windows)] { - use crate::ui::win_privacy::*; + use crate::win_privacy::*; match scrap::CapturerMag::new( display.origin(), @@ -308,11 +308,11 @@ pub fn test_create_capturer(privacy_mode_id: i32, timeout_millis: u64) -> bool { fn check_uac_switch(privacy_mode_id: i32, capturer_privacy_mode_id: i32) -> ResultType<()> { if capturer_privacy_mode_id != 0 { if privacy_mode_id != capturer_privacy_mode_id { - if !crate::ui::win_privacy::is_process_consent_running()? { + if !crate::win_privacy::is_process_consent_running()? { bail!("consent.exe is running"); } } - if crate::ui::win_privacy::is_process_consent_running()? { + if crate::win_privacy::is_process_consent_running()? { bail!("consent.exe is running"); } } @@ -372,7 +372,7 @@ fn get_capturer(use_yuv: bool, portable_service_running: bool) -> ResultType)>>; #[allow(dead_code)] diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index de33b0169..f5c575d43 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -494,7 +494,7 @@ pub async fn start_ipc(cm: ConnectionManager) { e ); } - allow_err!(crate::ui::win_privacy::start()); + allow_err!(crate::win_privacy::start()); }); match ipc::new_listener("_cm").await { diff --git a/src/ui/win_privacy.rs b/src/win_privacy.rs similarity index 100% rename from src/ui/win_privacy.rs rename to src/win_privacy.rs From 23f133b83674347f8bd7f9e61f6c764e0dda23cc Mon Sep 17 00:00:00 2001 From: grummbeer Date: Fri, 10 Feb 2023 10:50:48 +0100 Subject: [PATCH 1826/2015] unify padding of dialogs --- flutter/lib/common.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index a731f0b08..4ad4a9927 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -648,8 +648,6 @@ class CustomAlertDialog extends StatelessWidget { child: AlertDialog( scrollable: true, title: title, - contentPadding: EdgeInsets.fromLTRB( - contentPadding ?? padding, 25, contentPadding ?? padding, 10), content: ConstrainedBox( constraints: contentBoxConstraints, child: Theme( From 07b86bee8e521872048e159bdd213f09335b22a2 Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 10 Feb 2023 18:26:23 +0800 Subject: [PATCH 1827/2015] try fix memory issue when decoding is too slow Signed-off-by: fufesou --- flutter/lib/models/model.dart | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index ca99a5bd1..feab5bdc8 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -415,6 +415,8 @@ class ImageModel with ChangeNotifier { String id = ''; + int decodeCount = 0; + WeakReference parent; final List _callbacksOnFirstImage = []; @@ -434,7 +436,13 @@ class ImageModel with ChangeNotifier { } } } + + if (decodeCount >= 1) { + return; + } + final pid = parent.target?.id; + decodeCount += 1; ui.decodeImageFromPixels( rgba, parent.target?.ffiModel.display.width ?? 0, @@ -442,6 +450,7 @@ class ImageModel with ChangeNotifier { isWeb ? ui.PixelFormat.rgba8888 : ui.PixelFormat.bgra8888, (image) { if (parent.target?.id != pid) return; try { + decodeCount -= 1; // my throw exception, because the listener maybe already dispose update(image); } catch (e) { From 7ccee565095647c3553f1fb6e79c5f0ecf854cf7 Mon Sep 17 00:00:00 2001 From: enforcer007 Date: Fri, 10 Feb 2023 10:34:19 +0000 Subject: [PATCH 1828/2015] need not required for docker >23.0.1 --- .devcontainer/devcontainer.json | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 24ba9a915..cc348f38f 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,10 +1,7 @@ { "name": "rustdesk", "build": { - "dockerfile": "Dockerfile", - "args": { - "BUILDKIT_INLINE_CACHE": "0" - } + "dockerfile": "Dockerfile" }, "workspaceMount": "source=${localWorkspaceFolder},target=/home/user/rustdesk,type=bind,consistency=cache", "workspaceFolder": "/home/user/rustdesk", From a73514c35b9b7403b743628c1e5e3cb111217bee Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 10 Feb 2023 18:35:02 +0800 Subject: [PATCH 1829/2015] fix counter logic Signed-off-by: fufesou --- flutter/lib/models/model.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index feab5bdc8..add1289e2 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -448,9 +448,9 @@ class ImageModel with ChangeNotifier { parent.target?.ffiModel.display.width ?? 0, parent.target?.ffiModel.display.height ?? 0, isWeb ? ui.PixelFormat.rgba8888 : ui.PixelFormat.bgra8888, (image) { + decodeCount -= 1; if (parent.target?.id != pid) return; try { - decodeCount -= 1; // my throw exception, because the listener maybe already dispose update(image); } catch (e) { From 5b36555faa97a48d26cfda2bc95c58e71ef91294 Mon Sep 17 00:00:00 2001 From: 21pages Date: Fri, 10 Feb 2023 18:42:08 +0800 Subject: [PATCH 1830/2015] flutter option enable share rdp Signed-off-by: 21pages --- .../desktop/pages/desktop_setting_page.dart | 28 +++++++++++++++++++ src/flutter_ffi.rs | 4 +++ 2 files changed, 32 insertions(+) diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 4b6cf2a62..5d524523a 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -701,6 +701,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { child: _OptionCheckBox(context, 'Enable RDP', 'enable-rdp', enabled: enabled), ), + shareRdp(context, enabled), _OptionCheckBox(context, 'Deny LAN Discovery', 'enable-lan-discovery', reverse: true, enabled: enabled), ...directIp(context), @@ -708,6 +709,33 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { ]); } + shareRdp(BuildContext context, bool enabled) { + onChanged(bool b) async { + await bind.mainSetShareRdp(enable: b); + setState(() {}); + } + + bool value = bind.mainIsShareRdp(); + return Offstage( + offstage: !(Platform.isWindows && bind.mainIsRdpServiceOpen()), + child: GestureDetector( + child: Row( + children: [ + Checkbox( + value: value, + onChanged: enabled ? (_) => onChanged(!value) : null) + .marginOnly(right: 5), + Expanded( + child: Text(translate('Enable RDP session sharing'), + style: + TextStyle(color: _disabledTextColor(context, enabled))), + ) + ], + ).marginOnly(left: _kCheckBoxLeftMargin), + onTap: enabled ? () => onChanged(!value) : null), + ); + } + List directIp(BuildContext context) { TextEditingController controller = TextEditingController(); update() => setState(() {}); diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index a7e32d0b2..3611b5dbf 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1210,6 +1210,10 @@ pub fn main_is_rdp_service_open() -> SyncReturn { SyncReturn(is_rdp_service_open()) } +pub fn main_set_share_rdp(enable: bool) { + set_share_rdp(enable) +} + pub fn main_goto_install() -> SyncReturn { goto_install(); SyncReturn(true) From b4357e1e000f4914953385dd23982aceb776a863 Mon Sep 17 00:00:00 2001 From: grummbeer Date: Fri, 10 Feb 2023 12:51:49 +0100 Subject: [PATCH 1831/2015] 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 1832/2015] Addressbook login. Button instead of text --- flutter/lib/common/widgets/address_book.dart | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/flutter/lib/common/widgets/address_book.dart b/flutter/lib/common/widgets/address_book.dart index 5c1e1218c..5cd2af2be 100644 --- a/flutter/lib/common/widgets/address_book.dart +++ b/flutter/lib/common/widgets/address_book.dart @@ -43,13 +43,10 @@ class _AddressBookState extends State { return Obx(() { if (gFFI.userModel.userName.value.isEmpty) { return Center( - child: InkWell( - onTap: loginDialog, - child: Text( - translate("Login"), - style: const TextStyle(decoration: TextDecoration.underline), - ), - ), + child: ElevatedButton( + onPressed: loginDialog, + child: Text(translate("Login")) + ) ); } else { if (gFFI.abModel.abLoading.value) { From 19c7cd99d57f91b4697eed912961ac53f9410250 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 10 Feb 2023 21:18:55 +0800 Subject: [PATCH 1833/2015] fix: --cm cannot exit on macOS --- flutter/lib/common.dart | 5 +++++ flutter/lib/desktop/pages/server_page.dart | 15 +++++++++++++-- flutter/lib/models/model.dart | 2 +- flutter/lib/models/server_model.dart | 6 ++---- 4 files changed, 21 insertions(+), 7 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 4ad4a9927..d86960a0d 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -49,6 +49,11 @@ int androidVersion = 0; int windowsBuildNumber = 0; DesktopType? desktopType; +/// Check if the app is running with single view mode. +bool isSingleViewApp() { + return desktopType == DesktopType.cm; +} + /// * debug or test only, DO NOT enable in release build bool isTest = false; diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index b66a08e74..252e1cd12 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -1,11 +1,13 @@ // original cm window in Sciter version. import 'dart:async'; +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; import 'package:flutter_hbb/models/chat_model.dart'; +import 'package:flutter_hbb/utils/platform_channel.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:window_manager/window_manager.dart'; @@ -47,8 +49,17 @@ class _DesktopServerPageState extends State @override void onWindowClose() { - gFFI.serverModel.closeAll(); - gFFI.close(); + Future.wait([ + gFFI.serverModel.closeAll(), + gFFI.close() + ]).then((_) { + if (Platform.isMacOS) { + RdPlatformChannel.instance.terminate(); + } else { + windowManager.setPreventClose(false); + windowManager.close(); + } + }); super.onWindowClose(); } diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index add1289e2..eb837ba70 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -1399,12 +1399,12 @@ class FFI { await setCanvasConfig(id, cursorModel.x, cursorModel.y, canvasModel.x, canvasModel.y, canvasModel.scale, ffiModel.pi.currentDisplay); } - bind.sessionClose(id: id); imageModel.update(null); cursorModel.clear(); ffiModel.clear(); canvasModel.clear(); inputModel.resetModifiers(); + await bind.sessionClose(id: id); debugPrint('model $id closed'); id = ''; } diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index aab12ab5d..b2043f3c2 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -560,10 +560,8 @@ class ServerModel with ChangeNotifier { } } - closeAll() { - for (var client in _clients) { - bind.cmCloseConnection(connId: client.id); - } + Future closeAll() async { + await Future.wait(_clients.map((client) => bind.cmCloseConnection(connId: client.id))); _clients.clear(); tabController.state.value.tabs.clear(); } From cfc6f4b88a5c362226e029df5f0c8cc9a78b638b Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 10 Feb 2023 21:32:51 +0800 Subject: [PATCH 1834/2015] mouse do not control in black blank area Signed-off-by: fufesou --- flutter/lib/models/input_model.dart | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index c37d01860..b1491d526 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -485,10 +485,19 @@ class InputModel { y /= canvasModel.scale; x += d.x; y += d.y; + + if (x < d.x || y < d.y || x > (d.x + d.width) || y > (d.y + d.height)) { + // If left mouse up, no early return. + if (evt['buttons'] != kPrimaryMouseButton || type != 'up') { + return; + } + } + if (type != '') { x = 0; y = 0; } + evt['x'] = '${x.round()}'; evt['y'] = '${y.round()}'; var buttons = ''; From 3e17fd372b21a6cbfa7188a03d0a5ffd030c6e80 Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Fri, 10 Feb 2023 23:33:52 +0800 Subject: [PATCH 1835/2015] Revert "unify padding of dialogs" --- flutter/lib/common.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index d86960a0d..a295ad4f8 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -653,6 +653,8 @@ class CustomAlertDialog extends StatelessWidget { child: AlertDialog( scrollable: true, title: title, + contentPadding: EdgeInsets.fromLTRB( + contentPadding ?? padding, 25, contentPadding ?? padding, 10), content: ConstrainedBox( constraints: contentBoxConstraints, child: Theme( From d416d7d9658abfd5cd3ab954c9cb34d1a3e41b99 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sat, 11 Feb 2023 00:21:19 +0800 Subject: [PATCH 1836/2015] base64 icon only for sciter --- libs/hbb_common/src/config.rs | 8 +------- src/common.rs | 7 +------ src/ui.rs | 15 ++++++++++++++- src/ui/cm.rs | 2 +- src/ui/remote.rs | 2 +- 5 files changed, 18 insertions(+), 16 deletions(-) diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 1e4d80c9f..3bfc885c5 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -30,13 +30,7 @@ pub const REG_INTERVAL: i64 = 12_000; pub const COMPRESS_LEVEL: i32 = 3; const SERIAL: i32 = 3; const PASSWORD_ENC_VERSION: &str = "00"; -// 128x128 -#[cfg(target_os = "macos")] // 128x128 on 160x160 canvas, then shrink to 128, mac looks better with padding -pub const ICON: &str = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAMAAAD04JH5AAAAyVBMVEUAAAAAcf8Acf8Acf8Acv8Acf8Acf8Acf8Acf8AcP8Acf8Ab/8AcP8Acf////8AaP/z+f/o8v/k7v/5/v/T5f8AYP/u9v/X6f+hx/+Kuv95pP8Aef/B1/+TwP9xoP8BdP/g6P+Irv9ZmP8Bgf/E3f98q/9sn/+01f+Es/9nm/9Jif8hhv8off/M4P+syP+avP86iP/c7f+xy/9yqf9Om/9hk/9Rjv+60P99tv9fpf88lv8yjf8Tgf8deP+kvP8BiP8NeP8hkP80gP8oj2VLAAAADXRSTlMA7o7qLvnaxZ1FOxYPjH9HWgAABHJJREFUeNrtm+tW4jAQgBfwuu7MtIUWsOUiCCioIIgLiqvr+z/UHq/LJKVkmwTcc/r9E2nzlU4mSTP9lpGRkZGR8VX5cZjfL+yCEXYL+/nDH//U/Pd8DgyTy39Xbv7oIAcWyB0cqbW/sweW2NtRaj8H1sgpGOwUIAH7Bkd7YJW9dXFwAJY5WNP/cmCZQnJvzIN18on5LwfWySXlxEPYAIcad8D6PdiHDbCfIFCADVBIENiFDbCbIACKPPXrZ+cP8E6/0znvP4EymgIEravIRcTxu8HxNSJ60a8W0AYECKrlAN+YwAthCd9wm1Ug6wKzIn5SgRduXfwkqDasCjx0XFzi9PV6zwNcIuhcWBOg+ikySq8C9UD4dEKWBCoOcspvAuLHTo9sCDQiFPHotRM48j8G5gVur1FdAN2uaYEuiz7xFsgEJ2RUoMUakXuBTHHoGxQYOBhHjeUBAefEnMAowFhaLBOKuOemBBbxLRQrH2PBCgMvNCPQGMeevTb9zLrPxz2Mo+QbEaijzPUcOOHMQZkKGRAIPem39+bypREMPTkQW/oCfk866zAkiIFG4yIKRE/aAnfiSd0WrORY6pFdXQEqi9mvAQm0RIOSnoCcZ8vJoz3diCnjRk+g8VP4/fuQDJ2Lxr6WwG0gXs9aTpDzW0vgDBlVUpixR8gYk44AD8FrUKHr8JQJGgIDnoDqoALxmWPQSi9AVVzm8gKUuEPGr/QCvptwJkbSYT/TC4S8C96DGjTj86aHtAI0x2WaBIq0eSYYpRa4EsdWVVwWu9O0Aj6f6dyBMnwEraeOgSYu0wZlauzA47QCbT7DgAQSE+hZWoEBF/BBmWOewNMK3BsSqKUW4MGcWqCSVmDkbvkXGKQOwg6PAUO9oL3xXhA20yaiCjuwYygRVQlUOTWTCf2SuNJTxeFjgaHByGuAIvd8ItdPLTDhS7IuqEE1YSKVOgbayLhSFQhMzYh8hwfBs1r7c505YVIQYEdNoKwxK06MJiyrpUFHiF0NAfCQUVHoiRclIXJIR6C2fqG37pBHvcWpgwzvAtYwkR5UGV2e42UISdBJETl3mg8ouo54Rcnti1/vaT+iuUQBt500Cgo4U10BeHSkk57FB0JjWkKRMWgLUA0lLodtImAQdaMiiri3+gIAPZQoutHNsgKF1aaDMhMyIdBf8Th+Bh8MTjGWCpl5Wv43tDmnF+IUVMrcZgRoiAxhtrloYizNkZaAnF5leglbNhj0wYCAbCDvGb0mP4nib7O7ZlcYQ2m1gPtIZgVgGNNMeaVAaWR+57TrqgtUnm3sHQ+kYeE6fufUubG1ez50FXbPnWgBlgSABmN3TTcsRl2yWkHRrwbiunvk/W2+Mg1hPZplPDeXRbZzStFH15s1QIVd3UImP5z/bHpeeQLvRJ7XLFUffQIlCvqlXETQbgN9/rlYABGosv+Vi9m2Xs639YLGrZd0br+odetlvdsvbN56abfd4vbCzv9Q3v/ygoOV21A4OPpfXvH4Ai+5ZGRkZGRkbJA/t/I0QMzoMiEAAAAASUVORK5CYII= -"; -#[cfg(not(target_os = "macos"))] // 128x128 no padding -pub const ICON: &str = "data:image/png;base64,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 -"; + #[cfg(target_os = "macos")] lazy_static::lazy_static! { pub static ref ORG: Arc> = Arc::new(RwLock::new("com.carriez".to_owned())); diff --git a/src/common.rs b/src/common.rs index b66261ebe..ee44cf4f2 100644 --- a/src/common.rs +++ b/src/common.rs @@ -588,11 +588,6 @@ async fn check_software_update_() -> hbb_common::ResultType<()> { Ok(()) } -#[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] -pub fn get_icon() -> String { - hbb_common::config::ICON.to_owned() -} - pub fn get_app_name() -> String { hbb_common::config::APP_NAME.read().unwrap().clone() } @@ -772,4 +767,4 @@ pub fn handle_url_scheme(url: String) { log::debug!("Send the url to the existing flutter process failed, {}. Let's open a new program to handle this.", err); let _ = crate::run_me(vec![url]); } -} \ No newline at end of file +} diff --git a/src/ui.rs b/src/ui.rs index ce97745fb..1b6838e46 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -405,7 +405,7 @@ impl UI { } fn get_icon(&mut self) -> String { - crate::get_icon() + get_icon() } fn remove_peer(&mut self, id: String) { @@ -758,3 +758,16 @@ pub fn recent_sessions_updated() -> bool { false } } + +pub fn get_icon() -> String { + // 128x128 + #[cfg(target_os = "macos")] + // 128x128 on 160x160 canvas, then shrink to 128, mac looks better with padding + { + "data:image/png;base64,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".into() + } + #[cfg(not(target_os = "macos"))] // 128x128 no padding + { + "data:image/png;base64,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".into() + } +} diff --git a/src/ui/cm.rs b/src/ui/cm.rs index cce553154..a574b5e88 100644 --- a/src/ui/cm.rs +++ b/src/ui/cm.rs @@ -100,7 +100,7 @@ impl SciterConnectionManager { } fn get_icon(&mut self) -> String { - crate::get_icon() + super::get_icon() } fn check_click_time(&mut self, id: i32) { diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 999b409e0..fdb6b2df8 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -486,7 +486,7 @@ impl SciterSession { } pub fn get_icon(&self) -> String { - crate::get_icon() + super::get_icon() } fn supported_hwcodec(&self) -> Value { From 7514a067d378f74a75b206fe86e0f1ed76f61a5b Mon Sep 17 00:00:00 2001 From: Carsten Date: Fri, 10 Feb 2023 21:32:21 +0100 Subject: [PATCH 1837/2015] Update README-DE.md fix grammar and improve readability --- docs/README-DE.md | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/docs/README-DE.md b/docs/README-DE.md index 0b51d8fdd..e537d41f3 100644 --- a/docs/README-DE.md +++ b/docs/README-DE.md @@ -6,24 +6,24 @@ DateistrukturScreenshots
    [English] | [Українська] | [česky] | [中文] | [Magyar] | [Español] | [فارسی] | [Français] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
    - Wir brauchen deine Hilfe um diese README Datei zu verbessern und aktualisieren + Wir brauchen deine Hilfe, um diese README Datei zu verbessern und zu aktualisieren

    -Rede mit uns: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) +Rede mit uns auf: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) -Das hier ist ein Programm was, man nutzen kann, um einen Computer fernzusteuern, es wurde in Rust geschrieben. Es funktioniert ohne Konfiguration oder ähnliches, man kann es einfach direkt nutzen. Du hast volle Kontrolle über deine Daten und brauchst dir daher auch keine Sorgen um die Sicherheit dieser Daten zu machen. Du kannst unseren Rendezvous/Relay Server nutzen, [einen eigenen Server eröffnen](https://rustdesk.com/server) oder [einen neuen eigenen Server programmieren](https://github.com/rustdesk/rustdesk-server-demo). +RustDesk ist eine in Rust geschriebene Remote-Desktop-Software, die out-of-the-box ohne besondere Konfiguration funktioniert. Du hast die volle Kontrolle über deine Daten und musst dir keine Sorgen um die Sicherheit machen. Du kannst unseren Rendezvous/Relay Server nutzen, [einen eigenen Server aufsetzen](https://rustdesk.com/server) oder [einen eigenen Server programmieren](https://github.com/rustdesk/rustdesk-server-demo). -RustDesk heißt jegliche Mitarbeit willkommen. Schau dir [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) an, wenn du Hilfe brauchst für den Start. +RustDesk heißt jegliche Mitarbeit willkommen. Schau dir [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) an, wenn du Unterstützung beim Start brauchst. [**PROGRAMM DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases) ## Kostenlose öffentliche Server -Hier sind die Server, die du kostenlos nutzen kannst, es kann sein das sich diese Liste immer mal wieder ändert. Falls du nicht in der Nähe einer dieser Server bist, kann es sein, dass deine Verbindung langsam sein wird. +Nachfolgend sind die Server gelistet, die du kostenlos nutzen kannst. Es kann sein, dass sich diese Liste immer mal wieder ändert. Falls du nicht in der Nähe einer dieser Server bist, kann es sein, dass deine Verbindung langsam sein wird. -| Standort | Serverart | Spezifikationen | Kommentare | +| Standort | Anbieter | Spezifikationen | Kommentar | | --------- | ------------- | ------------------ | ---------- | | Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM | | | Germany | Codext | 2 vCPU / 4GB RAM | @@ -33,7 +33,7 @@ Hier sind die Server, die du kostenlos nutzen kannst, es kann sein das sich dies ## Abhängigkeiten -Die Desktop-Versionen nutzen [Sciter](https://sciter.com/) für die Oberfläche, bitte lade die dynamische Sciter Bibliothek selbst herunter. +Die Desktop-Versionen nutzen [Sciter](https://sciter.com/) oder Flutter für die GUI. Bitte lade die dynamische Sciter Bibliothek selbst herunter. [Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) | [Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) | @@ -41,7 +41,7 @@ Die Desktop-Versionen nutzen [Sciter](https://sciter.com/) für die Oberfläche, ## Die groben Schritte zum Kompilieren -- Bereite deine Rust Entwicklungsumgebung und C++ Entwicklungsumgebung vor +- Bereite deine Rust Entwicklungsumgebung und C++ Build-Umgebung vor - Installiere [vcpkg](https://github.com/microsoft/vcpkg) und füge die `VCPKG_ROOT` Systemumgebungsvariable hinzu @@ -110,11 +110,11 @@ cargo run ### Ändere Wayland zu X11 (Xorg) -RustDesk unterstützt "Wayland" nicht. Siehe [hier](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) um Xorg als Standard GNOME Session zu nutzen. +RustDesk unterstützt "Wayland" nicht. Siehe [hier](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/), um Xorg als Standard GNOME Session zu nutzen. -## Auf Docker Kompilieren +## Auf Docker kompilieren -Beginne damit das Repository zu klonen und den Docker Container zu bauen: +Beginne damit, das Repository zu klonen und den Docker Container zu bauen: ```sh git clone https://github.com/rustdesk/rustdesk @@ -122,13 +122,13 @@ cd rustdesk docker build -t "rustdesk-builder" . ``` -Jedes Mal, wenn du das Programm Kompilieren musst, nutze diesen Befehl: +Jedes Mal, wenn du das Programm kompilieren musst, nutze diesen Befehl: ```sh docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder ``` -Bedenke, dass das erste Mal Kompilieren länger dauern kann, da die Abhängigkeiten erst kompiliert werden müssen bevor sie zwischengespeichert werden können. Darauf folgende Kompiliervorgänge werden schneller sein. Falls du zusätzliche oder andere Argumente für den Kompilierbefehl angeben musst, kannst du diese am Ende des Befehls an der `` Position machen. Wenn du zum Beispiel eine optimierte Releaseversion kompilieren willst, kannst du das tun, indem du `--release` am Ende des Befehls anhängst. Das daraus entstehende Programm kannst du im “target” Ordner auf deinem System finden. Du kannst es mit folgenden Befehlen ausführen: +Bedenke, dass das erste Mal Kompilieren länger dauern kann, da die Abhängigkeiten erst kompiliert werden müssen bevor sie zwischengespeichert werden können. Nachfolgende Kompiliervorgänge werden schneller sein. Falls du zusätzliche oder andere Argumente für den Kompilierbefehl angeben musst, kannst du diese am Ende des Befehls an der `` Position machen. Wenn du zum Beispiel eine optimierte Releaseversion kompilieren willst, kannst du das tun, indem du `--release` am Ende des Befehls anhängst. Das daraus entstehende Programm kannst du im “target” Ordner auf deinem System finden. Du kannst es mit folgenden Befehlen ausführen: ```sh target/debug/rustdesk @@ -140,13 +140,13 @@ Oder, wenn du eine Releaseversion benutzt: target/release/rustdesk ``` -Bitte gehe sicher, dass du diese Befehle vom Stammverzeichnis vom RustDesk Repository nutzt, sonst kann es passieren, dass das Programm die Ressourcen nicht finden kann. Bitte bedenke auch, dass Unterbefehle von Cargo, wie z. B. `install` oder `run` aktuell noch nicht unterstützt werden, da sie das Programm innerhalb des Containers starten oder installieren würden, anstatt auf deinem eigentlichen System. +Bitte stelle sicher, dass du diese Befehle vom Stammverzeichnis vom RustDesk Repository nutzt. Ansonsten kann es passieren, dass das Programm die Ressourcen nicht finden kann. Bitte bedenke auch, dass Unterbefehle von Cargo, wie z. B. `install` oder `run` aktuell noch nicht unterstützt werden, da sie das Programm innerhalb des Containers starten oder installieren würden, anstatt auf deinem eigentlichen System. ## Dateistruktur -- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: Video Codec, Konfiguration, TCP/UDP Wrapper, Protokoll Puffer, fs Funktionen für Dateitransfer, und ein paar andere nützliche Funktionen +- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: Video Codec, Konfiguration, TCP/UDP Wrapper, Protokoll Puffer, fs Funktionen für Dateitransfer und ein paar andere nützliche Funktionen - **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: Bildschirmaufnahme -- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: Plattformspezifische Maus und Tastatur Steuerung +- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: Plattformspezifische Maus- und Tastatur-Steuerung - **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI - **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: Audio/Zwischenablage/Eingabe/Videodienste und Netzwerk Verbindungen - **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: Starten einer Peer-Verbindung From 491932cda104b517ef236b27e026a603831f1400 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sat, 11 Feb 2023 09:57:27 +0800 Subject: [PATCH 1838/2015] opt: fetch rgba positively for sessions on flutter --- flutter/lib/models/model.dart | 7 ++++++- src/flutter.rs | 8 +++++++- src/flutter_ffi.rs | 13 ++++++++++++- src/ui/remote.rs | 5 +++++ src/ui_session_interface.rs | 1 + 5 files changed, 31 insertions(+), 3 deletions(-) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index eb837ba70..f30209a60 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -1376,7 +1376,12 @@ class FFI { debugPrint('json.decode fail1(): $e, ${message.field0}'); } } else if (message is EventToUI_Rgba) { - imageModel.onRgba(message.field0); + // Fetch the image buffer from rust codes. + bind.sessionGetRgba(id: id).then((rgba) { + if (rgba != null) { + imageModel.onRgba(rgba); + } + }); } } }(); diff --git a/src/flutter.rs b/src/flutter.rs index 7533244eb..8ef451397 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -110,6 +110,7 @@ pub unsafe extern "C" fn free_c_args(ptr: *mut *mut c_char, len: c_int) { #[derive(Default, Clone)] pub struct FlutterHandler { pub event_stream: Arc>>>, + pub rgba: Arc>>> } impl FlutterHandler { @@ -290,7 +291,8 @@ impl InvokeUiSession for FlutterHandler { fn on_rgba(&self, data: &[u8]) { if let Some(stream) = &*self.event_stream.read().unwrap() { - stream.add(EventToUI::Rgba(ZeroCopyBuffer(data.to_owned()))); + drop(self.rgba.write().unwrap().replace(data.to_owned())); + stream.add(EventToUI::Rgba); } } @@ -409,6 +411,10 @@ impl InvokeUiSession for FlutterHandler { fn on_voice_call_incoming(&self) { self.push_event("on_voice_call_incoming", [].into()); } + + fn get_rgba(&self) -> Option> { + self.rgba.write().unwrap().take() + } } /// Create a new remote session with the given id. diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index a12d5acab..3a0fcc5fa 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -20,6 +20,7 @@ use std::{ os::raw::c_char, str::FromStr, }; +use crate::ui_session_interface::InvokeUiSession; // use crate::hbbs_http::account::AuthResult; @@ -47,7 +48,7 @@ fn initialize(app_dir: &str) { pub enum EventToUI { Event(String), - Rgba(ZeroCopyBuffer>), + Rgba, } pub fn start_global_event_stream(s: StreamSink, app_type: String) -> ResultType<()> { @@ -103,6 +104,16 @@ pub fn session_get_remember(id: String) -> Option { } } +pub fn session_get_rgba(id: String) -> Option>> { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + return match session.get_rgba() { + Some(buf) => Some(ZeroCopyBuffer(buf)), + _ => None + }; + } + None +} + pub fn session_get_toggle_option(id: String, arg: String) -> Option { if let Some(session) = SESSIONS.read().unwrap().get(&id) { Some(session.get_toggle_option(arg)) diff --git a/src/ui/remote.rs b/src/ui/remote.rs index fdb6b2df8..06af70eae 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -282,6 +282,11 @@ impl InvokeUiSession for SciterHandler { fn on_voice_call_incoming(&self) { self.call("onVoiceCallIncoming", &make_args!()); } + + /// RGBA is directly rendered by [on_rgba]. No need to store the rgba for the sciter ui. + fn get_rgba(&self) -> Option> { + None + } } pub struct SciterSession(Session); diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 87ea8e9eb..2944a76d1 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -722,6 +722,7 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { fn on_voice_call_closed(&self, reason: &str); fn on_voice_call_waiting(&self); fn on_voice_call_incoming(&self); + fn get_rgba(&self) -> Option>; } impl Deref for Session { From f8c78a6bf2ca029d7b4fdc3523bb0b9ad4e3fbde Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sat, 11 Feb 2023 10:14:09 +0800 Subject: [PATCH 1839/2015] opt: remove unnecessary rgba events to decrease memory usage --- src/flutter.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/flutter.rs b/src/flutter.rs index 8ef451397..a2dcbdbcf 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -291,8 +291,12 @@ impl InvokeUiSession for FlutterHandler { fn on_rgba(&self, data: &[u8]) { if let Some(stream) = &*self.event_stream.read().unwrap() { - drop(self.rgba.write().unwrap().replace(data.to_owned())); - stream.add(EventToUI::Rgba); + let former_rgba = self.rgba.write().unwrap().replace(data.to_owned()); + if former_rgba.is_none() { + // The [former_rgba] is none, which means the latest rgba had taken from flutter. + // We need to send a signal to flutter for notifying there's a new rgba buffer here. + stream.add(EventToUI::Rgba); + } } } From f521b1665a81f0e7dc11356fae993d7f26d3e4fb Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sat, 11 Feb 2023 12:25:13 +0800 Subject: [PATCH 1840/2015] opt: no copy during transmitting the decoded frame --- src/client.rs | 4 ++-- src/flutter.rs | 6 +++--- src/ui/remote.rs | 4 ++-- src/ui_session_interface.rs | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/client.rs b/src/client.rs index 020bea1f0..ecfc59749 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1545,7 +1545,7 @@ pub type MediaSender = mpsc::Sender; /// * `video_callback` - The callback for video frame. Being called when a video frame is ready. pub fn start_video_audio_threads(video_callback: F) -> (MediaSender, MediaSender) where - F: 'static + FnMut(&[u8]) + Send, + F: 'static + FnMut(Vec) + Send, { let (video_sender, video_receiver) = mpsc::channel::(); let mut video_callback = video_callback; @@ -1560,7 +1560,7 @@ where match data { MediaData::VideoFrame(vf) => { if let Ok(true) = video_handler.handle_frame(vf) { - video_callback(&video_handler.rgb); + video_callback(std::mem::replace(&mut video_handler.rgb, vec![])); } } MediaData::Reset => { diff --git a/src/flutter.rs b/src/flutter.rs index a2dcbdbcf..bee4dd7a5 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -3,7 +3,7 @@ use crate::{ flutter_ffi::EventToUI, ui_session_interface::{io_loop, InvokeUiSession, Session}, }; -use flutter_rust_bridge::{StreamSink, ZeroCopyBuffer}; +use flutter_rust_bridge::{StreamSink}; use hbb_common::{ bail, config::LocalConfig, get_version_number, message_proto::*, rendezvous_proto::ConnType, ResultType, @@ -289,9 +289,9 @@ impl InvokeUiSession for FlutterHandler { // unused in flutter fn adapt_size(&self) {} - fn on_rgba(&self, data: &[u8]) { + fn on_rgba(&self, data: Vec) { if let Some(stream) = &*self.event_stream.read().unwrap() { - let former_rgba = self.rgba.write().unwrap().replace(data.to_owned()); + let former_rgba = self.rgba.write().unwrap().replace(data); if former_rgba.is_none() { // The [former_rgba] is none, which means the latest rgba had taken from flutter. // We need to send a signal to flutter for notifying there's a new rgba buffer here. diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 06af70eae..b6663ad7e 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -201,12 +201,12 @@ impl InvokeUiSession for SciterHandler { self.call("adaptSize", &make_args!()); } - fn on_rgba(&self, data: &[u8]) { + fn on_rgba(&self, data: Vec) { VIDEO .lock() .unwrap() .as_mut() - .map(|v| v.render_frame(data).ok()); + .map(|v| v.render_frame(&data).ok()); } fn set_peer_info(&self, pi: &PeerInfo) { diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 2944a76d1..cbf6d0171 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -712,7 +712,7 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { fn update_block_input_state(&self, on: bool); fn job_progress(&self, id: i32, file_num: i32, speed: f64, finished_size: f64); fn adapt_size(&self); - fn on_rgba(&self, data: &[u8]); + fn on_rgba(&self, data: Vec); fn msgbox(&self, msgtype: &str, title: &str, text: &str, link: &str, retry: bool); #[cfg(any(target_os = "android", target_os = "ios"))] fn clipboard(&self, content: String); @@ -957,7 +957,7 @@ pub async fn io_loop(handler: Session) { let frame_count = Arc::new(AtomicUsize::new(0)); let frame_count_cl = frame_count.clone(); let ui_handler = handler.ui_handler.clone(); - let (video_sender, audio_sender) = start_video_audio_threads(move |data: &[u8]| { + let (video_sender, audio_sender) = start_video_audio_threads(move |data: Vec| { frame_count_cl.fetch_add(1, Ordering::Relaxed); ui_handler.on_rgba(data); }); From bf38fb7118321986b0cf502ab0809f742d74c3fb Mon Sep 17 00:00:00 2001 From: grummbeer Date: Sat, 11 Feb 2023 12:32:30 +0100 Subject: [PATCH 1841/2015] Dialog. Unify padding. --- flutter/lib/common.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index a295ad4f8..6c1245a7d 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -653,6 +653,7 @@ class CustomAlertDialog extends StatelessWidget { child: AlertDialog( scrollable: true, title: title, + titlePadding: EdgeInsets.fromLTRB(padding, 24, padding, 0), contentPadding: EdgeInsets.fromLTRB( contentPadding ?? padding, 25, contentPadding ?? padding, 10), content: ConstrainedBox( From 01d30bce9e4509b6129843bf2d460d0351c28638 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sun, 12 Feb 2023 01:52:11 +0800 Subject: [PATCH 1842/2015] opt: reduce copy and malloc times for both of flutter and rust --- flutter/lib/models/model.dart | 30 +++++++++++++--- flutter/lib/models/native_model.dart | 26 +++++++++++++- src/client.rs | 8 ++--- src/flutter.rs | 53 ++++++++++++++++++++++------ src/flutter_ffi.rs | 10 ------ src/ui/remote.rs | 10 +++--- src/ui_session_interface.rs | 6 ++-- 7 files changed, 105 insertions(+), 38 deletions(-) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index f30209a60..e09a99875 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -1,10 +1,12 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:ffi' hide Size; import 'dart:io'; import 'dart:math'; import 'dart:typed_data'; import 'dart:ui' as ui; +import 'package:ffi/ffi.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hbb/consts.dart'; @@ -1367,6 +1369,9 @@ class FFI { final stream = bind.sessionStart(id: id); final cb = ffiModel.startEventListener(id); () async { + // Preserved for the rgba data. + Pointer? buffer; + int? bufferSize; await for (final message in stream) { if (message is EventToUI_Event) { try { @@ -1377,13 +1382,30 @@ class FFI { } } else if (message is EventToUI_Rgba) { // Fetch the image buffer from rust codes. - bind.sessionGetRgba(id: id).then((rgba) { - if (rgba != null) { - imageModel.onRgba(rgba); + final sz = platformFFI.getRgbaSize(id); + if (sz == null) { + return; + } + // The buffer does not exists or the bufferSize is not + // equal to the required size. + if (buffer == null || bufferSize != sz) { + // reallocate buffer + if (buffer != null) { + malloc.free(buffer); } - }); + buffer = malloc.allocate(sz); + bufferSize = sz; + } + final rgba = platformFFI.getRgba(id, buffer, bufferSize!); + if (rgba != null) { + imageModel.onRgba(rgba); + } } } + // Free the buffer allocated on the heap. + if (buffer != null) { + malloc.free(buffer); + } }(); // every instance will bind a stream this.id = id; diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index 34a673953..588c3646f 100644 --- a/flutter/lib/models/native_model.dart +++ b/flutter/lib/models/native_model.dart @@ -23,7 +23,10 @@ class RgbaFrame extends Struct { } typedef F2 = Pointer Function(Pointer, Pointer); -typedef F3 = void Function(Pointer, Pointer); +typedef F3 = Void Function(Pointer, Pointer); +typedef F3Dart = void Function(Pointer, Pointer); +typedef F4 = Uint64 Function(Pointer); +typedef F4Dart = int Function(Pointer); typedef HandleEvent = Future Function(Map evt); /// FFI wrapper around the native Rust core. @@ -44,6 +47,8 @@ class PlatformFFI { final _toAndroidChannel = const MethodChannel('mChannel'); RustdeskImpl get ffiBind => _ffiBind; + F3Dart? _session_get_rgba; + F4Dart? _session_get_rgba_size; static get localeName => Platform.localeName; @@ -92,6 +97,23 @@ class PlatformFFI { return res; } + Uint8List? getRgba(String id, Pointer buffer, int bufSize) { + if (_session_get_rgba == null) return null; + var a = id.toNativeUtf8(); + _session_get_rgba!(a, buffer); + final data = buffer.asTypedList(bufSize); + malloc.free(a); + return data; + } + + int? getRgbaSize(String id) { + if (_session_get_rgba_size == null) return null; + var a = id.toNativeUtf8(); + final bufferSize = _session_get_rgba_size!(a); + malloc.free(a); + return bufferSize; + } + /// Init the FFI class, loads the native Rust core library. Future init(String appType) async { _appType = appType; @@ -107,6 +129,8 @@ class PlatformFFI { debugPrint('initializing FFI $_appType'); try { _translate = dylib.lookupFunction('translate'); + _session_get_rgba = dylib.lookupFunction("session_get_rgba"); + _session_get_rgba_size = dylib.lookupFunction("session_get_rgba_size"); try { // SYSTEM user failed _dir = (await getApplicationDocumentsDirectory()).path; diff --git a/src/client.rs b/src/client.rs index ecfc59749..c6e0a759f 100644 --- a/src/client.rs +++ b/src/client.rs @@ -817,7 +817,7 @@ impl AudioHandler { pub struct VideoHandler { decoder: Decoder, latency_controller: Arc>, - pub rgb: Vec, + pub rgb: Arc>>, recorder: Arc>>, record: bool, } @@ -850,7 +850,7 @@ impl VideoHandler { } match &vf.union { Some(frame) => { - let res = self.decoder.handle_video_frame(frame, &mut self.rgb); + let res = self.decoder.handle_video_frame(frame, &mut self.rgb.write().unwrap()); if self.record { self.recorder .lock() @@ -1545,7 +1545,7 @@ pub type MediaSender = mpsc::Sender; /// * `video_callback` - The callback for video frame. Being called when a video frame is ready. pub fn start_video_audio_threads(video_callback: F) -> (MediaSender, MediaSender) where - F: 'static + FnMut(Vec) + Send, + F: 'static + FnMut(Arc>>) + Send, { let (video_sender, video_receiver) = mpsc::channel::(); let mut video_callback = video_callback; @@ -1560,7 +1560,7 @@ where match data { MediaData::VideoFrame(vf) => { if let Ok(true) = video_handler.handle_frame(vf) { - video_callback(std::mem::replace(&mut video_handler.rgb, vec![])); + video_callback(video_handler.rgb.clone()); } } MediaData::Reset => { diff --git a/src/flutter.rs b/src/flutter.rs index bee4dd7a5..bb6f85bb9 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -15,6 +15,7 @@ use std::{ os::raw::{c_char, c_int}, sync::{Arc, RwLock}, }; +use libc::memcpy; pub(super) const APP_TYPE_MAIN: &str = "main"; pub(super) const APP_TYPE_CM: &str = "cm"; @@ -110,7 +111,8 @@ pub unsafe extern "C" fn free_c_args(ptr: *mut *mut c_char, len: c_int) { #[derive(Default, Clone)] pub struct FlutterHandler { pub event_stream: Arc>>>, - pub rgba: Arc>>> + pub rgba: Arc>>, + pub rgba_valid: Arc> } impl FlutterHandler { @@ -289,15 +291,18 @@ impl InvokeUiSession for FlutterHandler { // unused in flutter fn adapt_size(&self) {} - fn on_rgba(&self, data: Vec) { - if let Some(stream) = &*self.event_stream.read().unwrap() { - let former_rgba = self.rgba.write().unwrap().replace(data); - if former_rgba.is_none() { - // The [former_rgba] is none, which means the latest rgba had taken from flutter. - // We need to send a signal to flutter for notifying there's a new rgba buffer here. - stream.add(EventToUI::Rgba); - } + fn on_rgba(&self, data: Arc>>) { + // If the current rgba is not fetched by flutter, i.e., is valid. + // We give up sending a new event to flutter. + if *self.rgba_valid.read().unwrap() { + return; } + // Return the rgba buffer to the video handler for reusing allocated rgba buffer. + std::mem::swap::>(data.write().unwrap().as_mut(), self.rgba.write().unwrap().as_mut()); + if let Some(stream) = &*self.event_stream.read().unwrap() { + stream.add(EventToUI::Rgba); + } + let _ = std::mem::replace(&mut *self.rgba_valid.write().unwrap(), true); } fn set_peer_info(&self, pi: &PeerInfo) { @@ -416,8 +421,13 @@ impl InvokeUiSession for FlutterHandler { self.push_event("on_voice_call_incoming", [].into()); } - fn get_rgba(&self) -> Option> { - self.rgba.write().unwrap().take() + fn get_rgba(&mut self, buffer: *mut u8) { + // [Safety] + // * It must be ensures the buffer has enough space to place the whole rgba. + let max_len = self.rgba.read().unwrap().len(); + unsafe { std::ptr::copy_nonoverlapping(self.rgba.read().unwrap().as_ptr(), buffer, max_len)}; + // mark the rgba has been taken from flutter. + let _ = std::mem::replace(&mut *self.rgba_valid.write().unwrap(), false); } } @@ -645,3 +655,24 @@ pub fn set_cur_session_id(id: String) { *CUR_SESSION_ID.write().unwrap() = id; } } + +#[no_mangle] +pub fn session_get_rgba_size(id: *const char) -> usize { + let id = unsafe { std::ffi::CStr::from_ptr(id as _) }; + if let Ok(id) = id.to_str() { + if let Some(session) = SESSIONS.write().unwrap().get_mut(id) { + return session.rgba.read().unwrap().len(); + } + } + 0 +} + +#[no_mangle] +pub fn session_get_rgba(id: *const char, buffer: *mut u8) { + let id = unsafe { std::ffi::CStr::from_ptr(id as _) }; + if let Ok(id) = id.to_str() { + if let Some(session) = SESSIONS.write().unwrap().get_mut(id) { + return session.get_rgba(buffer); + } + } +} \ No newline at end of file diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 3a0fcc5fa..b4e79b361 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -104,16 +104,6 @@ pub fn session_get_remember(id: String) -> Option { } } -pub fn session_get_rgba(id: String) -> Option>> { - if let Some(session) = SESSIONS.read().unwrap().get(&id) { - return match session.get_rgba() { - Some(buf) => Some(ZeroCopyBuffer(buf)), - _ => None - }; - } - None -} - pub fn session_get_toggle_option(id: String, arg: String) -> Option { if let Some(session) = SESSIONS.read().unwrap().get(&id) { Some(session.get_toggle_option(arg)) diff --git a/src/ui/remote.rs b/src/ui/remote.rs index b6663ad7e..ecf96ab32 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -3,6 +3,7 @@ use std::{ ops::{Deref, DerefMut}, sync::{Arc, Mutex}, }; +use std::sync::RwLock; use sciter::{ dom::{ @@ -17,6 +18,7 @@ use sciter::{ use hbb_common::{ allow_err, fs::TransferJobMeta, log, message_proto::*, rendezvous_proto::ConnType, }; +use hbb_common::tokio::io::AsyncReadExt; use crate::{ client::*, @@ -201,12 +203,12 @@ impl InvokeUiSession for SciterHandler { self.call("adaptSize", &make_args!()); } - fn on_rgba(&self, data: Vec) { + fn on_rgba(&self, data: Arc>>) { VIDEO .lock() .unwrap() .as_mut() - .map(|v| v.render_frame(&data).ok()); + .map(|v| v.render_frame(data.read().unwrap().as_ref()).ok()); } fn set_peer_info(&self, pi: &PeerInfo) { @@ -284,9 +286,7 @@ impl InvokeUiSession for SciterHandler { } /// RGBA is directly rendered by [on_rgba]. No need to store the rgba for the sciter ui. - fn get_rgba(&self) -> Option> { - None - } + fn get_rgba(&mut self, _buffer: *mut u8) {} } pub struct SciterSession(Session); diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index cbf6d0171..85deb68c2 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -712,7 +712,7 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { fn update_block_input_state(&self, on: bool); fn job_progress(&self, id: i32, file_num: i32, speed: f64, finished_size: f64); fn adapt_size(&self); - fn on_rgba(&self, data: Vec); + fn on_rgba(&self, data: Arc>>); fn msgbox(&self, msgtype: &str, title: &str, text: &str, link: &str, retry: bool); #[cfg(any(target_os = "android", target_os = "ios"))] fn clipboard(&self, content: String); @@ -722,7 +722,7 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { fn on_voice_call_closed(&self, reason: &str); fn on_voice_call_waiting(&self); fn on_voice_call_incoming(&self); - fn get_rgba(&self) -> Option>; + fn get_rgba(&mut self, buffer: *mut u8); } impl Deref for Session { @@ -957,7 +957,7 @@ pub async fn io_loop(handler: Session) { let frame_count = Arc::new(AtomicUsize::new(0)); let frame_count_cl = frame_count.clone(); let ui_handler = handler.ui_handler.clone(); - let (video_sender, audio_sender) = start_video_audio_threads(move |data: Vec| { + let (video_sender, audio_sender) = start_video_audio_threads(move |data: Arc>> | { frame_count_cl.fetch_add(1, Ordering::Relaxed); ui_handler.on_rgba(data); }); From e0007788b1bec4af91bc286d46f3725c25614a65 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sun, 12 Feb 2023 08:25:48 +0800 Subject: [PATCH 1843/2015] no blank issue, and make logo.svg compatible with flutter without inline style --- .github/ISSUE_TEMPLATE/config.yml | 1 + flutter/assets/logo.svg | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 7b43e397b..2da6bbaf1 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,3 +1,4 @@ +blank_issues_enabled: false contact_links: - name: Ask a question url: https://github.com/rustdesk/rustdesk/discussions/category_choices diff --git a/flutter/assets/logo.svg b/flutter/assets/logo.svg index 965218c95..d3a3f7b37 100644 --- a/flutter/assets/logo.svg +++ b/flutter/assets/logo.svg @@ -1 +1 @@ - \ No newline at end of file + From fbbb2cd4ff9ac856e5511b3b6de796197caafdfe Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sun, 12 Feb 2023 08:49:09 +0800 Subject: [PATCH 1844/2015] fix another svg compatibility, move def back, to make href can find --- flutter/assets/logo.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/assets/logo.svg b/flutter/assets/logo.svg index d3a3f7b37..13eb73f22 100644 --- a/flutter/assets/logo.svg +++ b/flutter/assets/logo.svg @@ -1 +1 @@ - + From 3d40569dee56c903b481f3ab27108f524bb74e6c Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sun, 12 Feb 2023 09:03:13 +0800 Subject: [PATCH 1845/2015] change all ocusNode: FocusNode()..requestFocus(), to autofocus: true`` --- flutter/lib/common/widgets/address_book.dart | 2 +- flutter/lib/common/widgets/dialog.dart | 6 +++--- flutter/lib/common/widgets/peer_card.dart | 4 ++-- flutter/lib/desktop/pages/desktop_home_page.dart | 2 +- flutter/lib/desktop/pages/file_manager_page.dart | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/flutter/lib/common/widgets/address_book.dart b/flutter/lib/common/widgets/address_book.dart index 5cd2af2be..bd2a01296 100644 --- a/flutter/lib/common/widgets/address_book.dart +++ b/flutter/lib/common/widgets/address_book.dart @@ -386,7 +386,7 @@ class _AddressBookState extends State { errorText: msg.isEmpty ? null : translate(msg), ), controller: controller, - focusNode: FocusNode()..requestFocus(), + autofocus: true, ), ), ], diff --git a/flutter/lib/common/widgets/dialog.dart b/flutter/lib/common/widgets/dialog.dart index 837a197dc..e96a2b406 100644 --- a/flutter/lib/common/widgets/dialog.dart +++ b/flutter/lib/common/widgets/dialog.dart @@ -54,7 +54,7 @@ void changeIdDialog() { ], maxLength: 16, controller: controller, - focusNode: FocusNode()..requestFocus(), + autofocus: true, ), const SizedBox( height: 4.0, @@ -99,7 +99,7 @@ void changeWhiteList({Function()? callback}) async { errorText: msg.isEmpty ? null : translate(msg), ), controller: controller, - focusNode: FocusNode()..requestFocus()), + autofocus: true), ), ], ), @@ -186,7 +186,7 @@ Future changeDirectAccessPort( r'^([0-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$')), ], controller: controller, - focusNode: FocusNode()..requestFocus()), + autofocus: true), ), ], ), diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index c9af6328c..3c9a438a0 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -641,7 +641,7 @@ abstract class BasePeerCard extends StatelessWidget { child: Form( child: TextFormField( controller: controller, - focusNode: FocusNode()..requestFocus(), + autofocus: true, decoration: const InputDecoration(border: OutlineInputBorder()), ), @@ -1013,7 +1013,7 @@ void _rdpDialog(String id) async { decoration: const InputDecoration( border: OutlineInputBorder(), hintText: '3389'), controller: portController, - focusNode: FocusNode()..requestFocus(), + autofocus: true, ), ), ], diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 2986adc7a..cde1e6d74 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -634,7 +634,7 @@ void setPasswordDialog() async { border: const OutlineInputBorder(), errorText: errMsg0.isNotEmpty ? errMsg0 : null), controller: p0, - focusNode: FocusNode()..requestFocus(), + autofocus: true, onChanged: (value) { rxPass.value = value.trim(); }, diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 9955c2768..27bb0377d 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -798,7 +798,7 @@ class _FileManagerPageState extends State "Please enter the folder name"), ), controller: name, - focusNode: FocusNode()..requestFocus(), + autofocus: true, ), ], ), From d2e24173d0d87e840e41e22dc1a74b588322979e Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sun, 12 Feb 2023 10:28:04 +0800 Subject: [PATCH 1846/2015] opt: read uint8list directly from rust codes --- flutter/lib/models/model.dart | 30 +++--------------- flutter/lib/models/native_model.dart | 39 +++++++++++++++++------ src/client.rs | 8 ++--- src/flutter.rs | 47 +++++++++++++++++++--------- src/ui/remote.rs | 8 +++-- src/ui_session_interface.rs | 7 +++-- 6 files changed, 78 insertions(+), 61 deletions(-) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index e09a99875..8cf90eba9 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -417,8 +417,6 @@ class ImageModel with ChangeNotifier { String id = ''; - int decodeCount = 0; - WeakReference parent; final List _callbacksOnFirstImage = []; @@ -439,20 +437,16 @@ class ImageModel with ChangeNotifier { } } - if (decodeCount >= 1) { - return; - } - final pid = parent.target?.id; - decodeCount += 1; ui.decodeImageFromPixels( rgba, parent.target?.ffiModel.display.width ?? 0, parent.target?.ffiModel.display.height ?? 0, isWeb ? ui.PixelFormat.rgba8888 : ui.PixelFormat.bgra8888, (image) { - decodeCount -= 1; if (parent.target?.id != pid) return; try { + // Unlock the rgba memory from rust codes. + platformFFI.nextRgba(id); // my throw exception, because the listener maybe already dispose update(image); } catch (e) { @@ -1370,8 +1364,6 @@ class FFI { final cb = ffiModel.startEventListener(id); () async { // Preserved for the rgba data. - Pointer? buffer; - int? bufferSize; await for (final message in stream) { if (message is EventToUI_Event) { try { @@ -1383,29 +1375,15 @@ class FFI { } else if (message is EventToUI_Rgba) { // Fetch the image buffer from rust codes. final sz = platformFFI.getRgbaSize(id); - if (sz == null) { + if (sz == null || sz == 0) { return; } - // The buffer does not exists or the bufferSize is not - // equal to the required size. - if (buffer == null || bufferSize != sz) { - // reallocate buffer - if (buffer != null) { - malloc.free(buffer); - } - buffer = malloc.allocate(sz); - bufferSize = sz; - } - final rgba = platformFFI.getRgba(id, buffer, bufferSize!); + final rgba = platformFFI.getRgba(id, sz); if (rgba != null) { imageModel.onRgba(rgba); } } } - // Free the buffer allocated on the heap. - if (buffer != null) { - malloc.free(buffer); - } }(); // every instance will bind a stream this.id = id; diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index 588c3646f..ba62b775e 100644 --- a/flutter/lib/models/native_model.dart +++ b/flutter/lib/models/native_model.dart @@ -9,6 +9,7 @@ import 'package:ffi/ffi.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hbb/consts.dart'; +import 'package:get/get.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:path_provider/path_provider.dart'; import 'package:win32/win32.dart' as win32; @@ -23,10 +24,11 @@ class RgbaFrame extends Struct { } typedef F2 = Pointer Function(Pointer, Pointer); -typedef F3 = Void Function(Pointer, Pointer); -typedef F3Dart = void Function(Pointer, Pointer); +typedef F3 = Pointer Function(Pointer); typedef F4 = Uint64 Function(Pointer); typedef F4Dart = int Function(Pointer); +typedef F5 = Void Function(Pointer); +typedef F5Dart = void Function(Pointer); typedef HandleEvent = Future Function(Map evt); /// FFI wrapper around the native Rust core. @@ -47,8 +49,9 @@ class PlatformFFI { final _toAndroidChannel = const MethodChannel('mChannel'); RustdeskImpl get ffiBind => _ffiBind; - F3Dart? _session_get_rgba; + F3? _session_get_rgba; F4Dart? _session_get_rgba_size; + F5Dart? _session_next_rgba; static get localeName => Platform.localeName; @@ -97,13 +100,19 @@ class PlatformFFI { return res; } - Uint8List? getRgba(String id, Pointer buffer, int bufSize) { + Uint8List? getRgba(String id, int bufSize) { if (_session_get_rgba == null) return null; var a = id.toNativeUtf8(); - _session_get_rgba!(a, buffer); - final data = buffer.asTypedList(bufSize); - malloc.free(a); - return data; + try { + final buffer = _session_get_rgba!(a); + if (buffer == nullptr) { + return null; + } + final data = buffer.asTypedList(bufSize); + return data; + } finally { + malloc.free(a); + } } int? getRgbaSize(String id) { @@ -114,6 +123,13 @@ class PlatformFFI { return bufferSize; } + void nextRgba(String id) { + if (_session_next_rgba == null) return; + final a = id.toNativeUtf8(); + _session_next_rgba!(a); + malloc.free(a); + } + /// Init the FFI class, loads the native Rust core library. Future init(String appType) async { _appType = appType; @@ -129,8 +145,11 @@ class PlatformFFI { debugPrint('initializing FFI $_appType'); try { _translate = dylib.lookupFunction('translate'); - _session_get_rgba = dylib.lookupFunction("session_get_rgba"); - _session_get_rgba_size = dylib.lookupFunction("session_get_rgba_size"); + _session_get_rgba = dylib.lookupFunction("session_get_rgba"); + _session_get_rgba_size = + dylib.lookupFunction("session_get_rgba_size"); + _session_next_rgba = + dylib.lookupFunction("session_next_rgba"); try { // SYSTEM user failed _dir = (await getApplicationDocumentsDirectory()).path; diff --git a/src/client.rs b/src/client.rs index c6e0a759f..a21592578 100644 --- a/src/client.rs +++ b/src/client.rs @@ -817,7 +817,7 @@ impl AudioHandler { pub struct VideoHandler { decoder: Decoder, latency_controller: Arc>, - pub rgb: Arc>>, + pub rgb: Vec, recorder: Arc>>, record: bool, } @@ -850,7 +850,7 @@ impl VideoHandler { } match &vf.union { Some(frame) => { - let res = self.decoder.handle_video_frame(frame, &mut self.rgb.write().unwrap()); + let res = self.decoder.handle_video_frame(frame, &mut self.rgb); if self.record { self.recorder .lock() @@ -1545,7 +1545,7 @@ pub type MediaSender = mpsc::Sender; /// * `video_callback` - The callback for video frame. Being called when a video frame is ready. pub fn start_video_audio_threads(video_callback: F) -> (MediaSender, MediaSender) where - F: 'static + FnMut(Arc>>) + Send, + F: 'static + FnMut(&mut Vec) + Send, { let (video_sender, video_receiver) = mpsc::channel::(); let mut video_callback = video_callback; @@ -1560,7 +1560,7 @@ where match data { MediaData::VideoFrame(vf) => { if let Ok(true) = video_handler.handle_frame(vf) { - video_callback(video_handler.rgb.clone()); + video_callback(&mut video_handler.rgb); } } MediaData::Reset => { diff --git a/src/flutter.rs b/src/flutter.rs index bb6f85bb9..a60e379f9 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -15,7 +15,7 @@ use std::{ os::raw::{c_char, c_int}, sync::{Arc, RwLock}, }; -use libc::memcpy; +use std::sync::atomic::{AtomicBool, Ordering}; pub(super) const APP_TYPE_MAIN: &str = "main"; pub(super) const APP_TYPE_CM: &str = "cm"; @@ -111,8 +111,10 @@ pub unsafe extern "C" fn free_c_args(ptr: *mut *mut c_char, len: c_int) { #[derive(Default, Clone)] pub struct FlutterHandler { pub event_stream: Arc>>>, + // SAFETY: [rgba] is guarded by [rgba_valid], and it's safe to reach [rgba] with `rgba_valid == true`. + // We must check the `rgba_valid` before reading [rgba]. pub rgba: Arc>>, - pub rgba_valid: Arc> + pub rgba_valid: Arc } impl FlutterHandler { @@ -291,18 +293,18 @@ impl InvokeUiSession for FlutterHandler { // unused in flutter fn adapt_size(&self) {} - fn on_rgba(&self, data: Arc>>) { + fn on_rgba(&self, data: &mut Vec) { // If the current rgba is not fetched by flutter, i.e., is valid. // We give up sending a new event to flutter. - if *self.rgba_valid.read().unwrap() { + if self.rgba_valid.load(Ordering::Relaxed) { return; } + self.rgba_valid.store(true, Ordering::Relaxed); // Return the rgba buffer to the video handler for reusing allocated rgba buffer. - std::mem::swap::>(data.write().unwrap().as_mut(), self.rgba.write().unwrap().as_mut()); + std::mem::swap::>(data, &mut *self.rgba.write().unwrap()); if let Some(stream) = &*self.event_stream.read().unwrap() { stream.add(EventToUI::Rgba); } - let _ = std::mem::replace(&mut *self.rgba_valid.write().unwrap(), true); } fn set_peer_info(&self, pi: &PeerInfo) { @@ -421,13 +423,17 @@ impl InvokeUiSession for FlutterHandler { self.push_event("on_voice_call_incoming", [].into()); } - fn get_rgba(&mut self, buffer: *mut u8) { - // [Safety] - // * It must be ensures the buffer has enough space to place the whole rgba. - let max_len = self.rgba.read().unwrap().len(); - unsafe { std::ptr::copy_nonoverlapping(self.rgba.read().unwrap().as_ptr(), buffer, max_len)}; - // mark the rgba has been taken from flutter. - let _ = std::mem::replace(&mut *self.rgba_valid.write().unwrap(), false); + #[inline] + fn get_rgba(&self) -> *const u8 { + if self.rgba_valid.load(Ordering::Relaxed) { + return self.rgba.read().unwrap().as_ptr(); + } + std::ptr::null_mut() + } + + #[inline] + fn next_rgba(&mut self) { + self.rgba_valid.store(false, Ordering::Relaxed); } } @@ -668,11 +674,22 @@ pub fn session_get_rgba_size(id: *const char) -> usize { } #[no_mangle] -pub fn session_get_rgba(id: *const char, buffer: *mut u8) { +pub fn session_get_rgba(id: *const char) -> *const u8 { let id = unsafe { std::ffi::CStr::from_ptr(id as _) }; if let Ok(id) = id.to_str() { if let Some(session) = SESSIONS.write().unwrap().get_mut(id) { - return session.get_rgba(buffer); + return session.get_rgba(); + } + } + std::ptr::null() +} + +#[no_mangle] +pub fn session_next_rgba(id: *const char) { + let id = unsafe { std::ffi::CStr::from_ptr(id as _) }; + if let Ok(id) = id.to_str() { + if let Some(session) = SESSIONS.write().unwrap().get_mut(id) { + return session.next_rgba(); } } } \ No newline at end of file diff --git a/src/ui/remote.rs b/src/ui/remote.rs index ecf96ab32..e44e31401 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -203,12 +203,12 @@ impl InvokeUiSession for SciterHandler { self.call("adaptSize", &make_args!()); } - fn on_rgba(&self, data: Arc>>) { + fn on_rgba(&self, data: &mut Vec) { VIDEO .lock() .unwrap() .as_mut() - .map(|v| v.render_frame(data.read().unwrap().as_ref()).ok()); + .map(|v| v.render_frame(data).ok()); } fn set_peer_info(&self, pi: &PeerInfo) { @@ -286,7 +286,9 @@ impl InvokeUiSession for SciterHandler { } /// RGBA is directly rendered by [on_rgba]. No need to store the rgba for the sciter ui. - fn get_rgba(&mut self, _buffer: *mut u8) {} + fn get_rgba(&self) -> *const u8 { std::ptr::null() } + + fn next_rgba(&mut self) {} } pub struct SciterSession(Session); diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 85deb68c2..25c15f52f 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -712,7 +712,7 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { fn update_block_input_state(&self, on: bool); fn job_progress(&self, id: i32, file_num: i32, speed: f64, finished_size: f64); fn adapt_size(&self); - fn on_rgba(&self, data: Arc>>); + fn on_rgba(&self, data: &mut Vec); fn msgbox(&self, msgtype: &str, title: &str, text: &str, link: &str, retry: bool); #[cfg(any(target_os = "android", target_os = "ios"))] fn clipboard(&self, content: String); @@ -722,7 +722,8 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { fn on_voice_call_closed(&self, reason: &str); fn on_voice_call_waiting(&self); fn on_voice_call_incoming(&self); - fn get_rgba(&mut self, buffer: *mut u8); + fn get_rgba(&self) -> *const u8; + fn next_rgba(&mut self); } impl Deref for Session { @@ -957,7 +958,7 @@ pub async fn io_loop(handler: Session) { let frame_count = Arc::new(AtomicUsize::new(0)); let frame_count_cl = frame_count.clone(); let ui_handler = handler.ui_handler.clone(); - let (video_sender, audio_sender) = start_video_audio_threads(move |data: Arc>> | { + let (video_sender, audio_sender) = start_video_audio_threads(move |data: &mut Vec | { frame_count_cl.fetch_add(1, Ordering::Relaxed); ui_handler.on_rgba(data); }); From 9fb5b2cb5f9511c2b4754fa1a18cacd1cce1922d Mon Sep 17 00:00:00 2001 From: csf Date: Sun, 12 Feb 2023 21:26:04 +0900 Subject: [PATCH 1847/2015] use flutter_keyboard_visibility --- flutter/lib/mobile/pages/remote_page.dart | 71 +++++++++-------------- flutter/pubspec.lock | 48 +++++++++++++++ flutter/pubspec.yaml | 1 + 3 files changed, 75 insertions(+), 45 deletions(-) diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 54b6f1d47..d1faa5494 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -7,6 +7,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/mobile/widgets/gesture_help.dart'; import 'package:flutter_hbb/models/chat_model.dart'; +import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart'; import 'package:get/get_state_manager/src/rx_flutter/rx_obx_widget.dart'; import 'package:provider/provider.dart'; import 'package:wakelock/wakelock.dart'; @@ -33,10 +34,8 @@ class RemotePage extends StatefulWidget { } class _RemotePageState extends State { - Timer? _interval; Timer? _timer; bool _showBar = !isWebDesktop; - double _bottom = 0; String _value = ''; double _scale = 1; double _mouseScrollIntegral = 0; // mouse scroll speed controller @@ -44,6 +43,8 @@ class _RemotePageState extends State { var _more = true; var _fn = false; + late final keyboardVisibilityController = KeyboardVisibilityController(); + late final StreamSubscription keyboardSubscription; final FocusNode _mobileFocusNode = FocusNode(); final FocusNode _physicalFocusNode = FocusNode(); var _showEdit = false; // use soft keyboard @@ -58,14 +59,14 @@ class _RemotePageState extends State { SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); gFFI.dialogManager .showLoading(translate('Connecting...'), onCancel: closeConnection); - _interval = - Timer.periodic(Duration(milliseconds: 30), (timer) => interval()); }); Wakelock.enable(); _physicalFocusNode.requestFocus(); gFFI.ffiModel.updateEventListener(widget.id); gFFI.inputModel.listenToMouse(true); gFFI.qualityMonitorModel.checkShowQualityMonitor(widget.id); + keyboardSubscription = + keyboardVisibilityController.onChange.listen(onSoftKeyboardChanged); } @override @@ -76,49 +77,27 @@ class _RemotePageState extends State { _mobileFocusNode.dispose(); _physicalFocusNode.dispose(); gFFI.close(); - _interval?.cancel(); _timer?.cancel(); gFFI.dialogManager.dismissAll(); SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: SystemUiOverlay.values); Wakelock.disable(); + keyboardSubscription.cancel(); super.dispose(); } - void resetTool() { + void onSoftKeyboardChanged(bool visible) { inputModel.resetModifiers(); - } - - bool isKeyboardShown() { - return _bottom >= 100; - } - - // crash on web before widget initiated. - void intervalUnsafe() { - var v = MediaQuery.of(context).viewInsets.bottom; - if (v != _bottom) { - resetTool(); - setState(() { - _bottom = v; - if (v < 100) { - SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, - overlays: []); - // [pi.version.isNotEmpty] -> check ready or not, avoid login without soft-keyboard - if (gFFI.chatModel.chatWindowOverlayEntry == null && - gFFI.ffiModel.pi.version.isNotEmpty) { - gFFI.invokeMethod("enable_soft_keyboard", false); - } - } - }); + if (!visible) { + SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); + // [pi.version.isNotEmpty] -> check ready or not, avoid login without soft-keyboard + if (gFFI.chatModel.chatWindowOverlayEntry == null && + gFFI.ffiModel.pi.version.isNotEmpty) { + gFFI.invokeMethod("enable_soft_keyboard", false); + } } } - void interval() { - try { - intervalUnsafe(); - } catch (e) {} - } - // handle mobile virtual keyboard void handleSoftKeyboardInput(String newValue) { var oldValue = _value; @@ -219,8 +198,9 @@ class _RemotePageState extends State { @override Widget build(BuildContext context) { final pi = Provider.of(context).pi; - final hideKeyboard = isKeyboardShown() && _showEdit; - final showActionButton = !_showBar || hideKeyboard; + final isHideKeyboardFAB = + keyboardVisibilityController.isVisible && _showEdit; + final showActionButton = !_showBar || isHideKeyboardFAB; final keyboard = gFFI.ffiModel.permissions['keyboard'] != false; return WillPopScope( @@ -230,21 +210,21 @@ class _RemotePageState extends State { }, child: getRawPointerAndKeyBody(Scaffold( // workaround for https://github.com/rustdesk/rustdesk/issues/3131 - floatingActionButtonLocation: hideKeyboard + floatingActionButtonLocation: isHideKeyboardFAB ? FABLocation(FloatingActionButtonLocation.endFloat, 0, -35) : null, floatingActionButton: !showActionButton ? null : FloatingActionButton( - mini: !hideKeyboard, + mini: !isHideKeyboardFAB, child: Icon( - hideKeyboard ? Icons.expand_more : Icons.expand_less, + isHideKeyboardFAB ? Icons.expand_more : Icons.expand_less, color: Colors.white, ), backgroundColor: MyTheme.accent, onPressed: () { setState(() { - if (hideKeyboard) { + if (isHideKeyboardFAB) { _showEdit = false; gFFI.invokeMethod("enable_soft_keyboard", false); _mobileFocusNode.unfocus(); @@ -725,7 +705,7 @@ class _RemotePageState extends State { // } Widget getHelpTools() { - final keyboard = isKeyboardShown(); + final keyboard = keyboardVisibilityController.isVisible; if (!keyboard) { return SizedBox(); } @@ -858,9 +838,10 @@ class _RemotePageState extends State { spacing: space, runSpacing: space, children: [SizedBox(width: 9999)] + - (keyboard - ? modifiers + keys + (_fn ? fn : []) + (_more ? more : []) - : modifiers), + modifiers + + keys + + (_fn ? fn : []) + + (_more ? more : []), )); } } diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index cd618dfc4..91a061fb9 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -488,6 +488,54 @@ packages: url: "https://github.com/Kingtous/flutter_improved_scrolling" source: git version: "0.0.3" + flutter_keyboard_visibility: + dependency: "direct main" + description: + name: flutter_keyboard_visibility + sha256: "86b71bbaffa38e885f5c21b1182408b9be6951fd125432cf6652c636254cef2d" + url: "https://pub.dev" + source: hosted + version: "5.4.0" + flutter_keyboard_visibility_linux: + dependency: transitive + description: + name: flutter_keyboard_visibility_linux + sha256: "6fba7cd9bb033b6ddd8c2beb4c99ad02d728f1e6e6d9b9446667398b2ac39f08" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + flutter_keyboard_visibility_macos: + dependency: transitive + description: + name: flutter_keyboard_visibility_macos + sha256: c5c49b16fff453dfdafdc16f26bdd8fb8d55812a1d50b0ce25fc8d9f2e53d086 + url: "https://pub.dev" + source: hosted + version: "1.0.0" + flutter_keyboard_visibility_platform_interface: + dependency: transitive + description: + name: flutter_keyboard_visibility_platform_interface + sha256: e43a89845873f7be10cb3884345ceb9aebf00a659f479d1c8f4293fcb37022a4 + url: "https://pub.dev" + source: hosted + version: "2.0.0" + flutter_keyboard_visibility_web: + dependency: transitive + description: + name: flutter_keyboard_visibility_web + sha256: d3771a2e752880c79203f8d80658401d0c998e4183edca05a149f5098ce6e3d1 + url: "https://pub.dev" + source: hosted + version: "2.0.0" + flutter_keyboard_visibility_windows: + dependency: transitive + description: + name: flutter_keyboard_visibility_windows + sha256: fc4b0f0b6be9b93ae527f3d527fb56ee2d918cd88bbca438c478af7bcfd0ef73 + url: "https://pub.dev" + source: hosted + version: "1.0.0" flutter_launcher_icons: dependency: "direct main" description: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 8701d9f5b..df29252c9 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -91,6 +91,7 @@ dependencies: win32: any password_strength: ^0.2.0 flutter_launcher_icons: ^0.11.0 + flutter_keyboard_visibility: ^5.4.0 dev_dependencies: From 6e4e463f5f28e5c819e46570e12bb2e2a867ccc1 Mon Sep 17 00:00:00 2001 From: csf Date: Sun, 12 Feb 2023 22:03:43 +0900 Subject: [PATCH 1848/2015] update HelpTools, use StatefulWidget --- flutter/lib/mobile/pages/remote_page.dart | 74 ++++++++++++++--------- 1 file changed, 46 insertions(+), 28 deletions(-) diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index d1faa5494..1ec57b46e 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -41,8 +41,6 @@ class _RemotePageState extends State { double _mouseScrollIntegral = 0; // mouse scroll speed controller Orientation? _currentOrientation; - var _more = true; - var _fn = false; late final keyboardVisibilityController = KeyboardVisibilityController(); late final StreamSubscription keyboardSubscription; final FocusNode _mobileFocusNode = FocusNode(); @@ -96,6 +94,8 @@ class _RemotePageState extends State { gFFI.invokeMethod("enable_soft_keyboard", false); } } + // update for Scaffold + setState(() {}); } // handle mobile virtual keyboard @@ -478,6 +478,7 @@ class _RemotePageState extends State { } Widget getBodyForMobile() { + final keyboardIsVisible = keyboardVisibilityController.isVisible; return Container( color: MyTheme.canvasColor, child: Stack(children: () { @@ -488,7 +489,7 @@ class _RemotePageState extends State { right: 10, child: QualityMonitor(gFFI.qualityMonitorModel), ), - getHelpTools(), + KeyHelpTools(requestShow: keyboardIsVisible), SizedBox( width: 0, height: 0, @@ -703,33 +704,51 @@ class _RemotePageState extends State { // ])); // }, clickMaskDismiss: true); // } +} - Widget getHelpTools() { - final keyboard = keyboardVisibilityController.isVisible; - if (!keyboard) { +class KeyHelpTools extends StatefulWidget { + /// need to show by external request, etc [keyboardIsVisible] or [changeTouchMode] + final bool requestShow; + + KeyHelpTools({required this.requestShow}); + + @override + State createState() => _KeyHelpToolsState(); +} + +class _KeyHelpToolsState extends State { + var _more = true; + var _fn = false; + + InputModel get inputModel => gFFI.inputModel; + + Widget wrap(String text, void Function() onPressed, + [bool? active, IconData? icon]) { + return TextButton( + style: TextButton.styleFrom( + minimumSize: Size(0, 0), + padding: EdgeInsets.symmetric(vertical: 10, horizontal: 9.75), + //adds padding inside the button + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + //limits the touch area to the button area + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5.0), + ), + backgroundColor: active == true ? MyTheme.accent80 : null, + ), + child: icon != null + ? Icon(icon, size: 17, color: Colors.white) + : Text(translate(text), + style: TextStyle(color: Colors.white, fontSize: 11)), + onPressed: onPressed); + } + + @override + Widget build(BuildContext context) { + if (!widget.requestShow) { return SizedBox(); } final size = MediaQuery.of(context).size; - wrap(String text, void Function() onPressed, - [bool? active, IconData? icon]) { - return TextButton( - style: TextButton.styleFrom( - minimumSize: Size(0, 0), - padding: EdgeInsets.symmetric(vertical: 10, horizontal: 9.75), - //adds padding inside the button - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - //limits the touch area to the button area - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(5.0), - ), - backgroundColor: active == true ? MyTheme.accent80 : null, - ), - child: icon != null - ? Icon(icon, size: 17, color: Colors.white) - : Text(translate(text), - style: TextStyle(color: Colors.white, fontSize: 11)), - onPressed: onPressed); - } final pi = gFFI.ffiModel.pi; final isMac = pi.platform == kPeerPlatformMacOS; @@ -832,8 +851,7 @@ class _RemotePageState extends State { final space = size.width > 320 ? 4.0 : 2.0; return Container( color: Color(0xAA000000), - padding: EdgeInsets.only( - top: keyboard ? 24 : 4, left: 0, right: 0, bottom: 8), + padding: EdgeInsets.only(top: widget.requestShow ? 24 : 4, bottom: 8), child: Wrap( spacing: space, runSpacing: space, From 4b52431dbf295b1d71361335ddcb6838a48c2c2e Mon Sep 17 00:00:00 2001 From: csf Date: Sun, 12 Feb 2023 22:20:51 +0900 Subject: [PATCH 1849/2015] KeyHelpTools add pin , and keep enable when hasModifierOn --- flutter/lib/mobile/pages/remote_page.dart | 42 +++++++++++++++-------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 1ec57b46e..63a289c95 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -85,7 +85,6 @@ class _RemotePageState extends State { } void onSoftKeyboardChanged(bool visible) { - inputModel.resetModifiers(); if (!visible) { SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); // [pi.version.isNotEmpty] -> check ready or not, avoid login without soft-keyboard @@ -719,11 +718,12 @@ class KeyHelpTools extends StatefulWidget { class _KeyHelpToolsState extends State { var _more = true; var _fn = false; + var _pin = false; InputModel get inputModel => gFFI.inputModel; Widget wrap(String text, void Function() onPressed, - [bool? active, IconData? icon]) { + {bool? active, IconData? icon}) { return TextButton( style: TextButton.styleFrom( minimumSize: Size(0, 0), @@ -737,7 +737,7 @@ class _KeyHelpToolsState extends State { backgroundColor: active == true ? MyTheme.accent80 : null, ), child: icon != null - ? Icon(icon, size: 17, color: Colors.white) + ? Icon(icon, size: 14, color: Colors.white) : Text(translate(text), style: TextStyle(color: Colors.white, fontSize: 11)), onPressed: onPressed); @@ -745,8 +745,13 @@ class _KeyHelpToolsState extends State { @override Widget build(BuildContext context) { - if (!widget.requestShow) { - return SizedBox(); + final hasModifierOn = inputModel.ctrl || + inputModel.alt || + inputModel.shift || + inputModel.command; + + if (!_pin && !hasModifierOn && !widget.requestShow) { + return Offstage(); } final size = MediaQuery.of(context).size; @@ -755,16 +760,16 @@ class _KeyHelpToolsState extends State { final modifiers = [ wrap('Ctrl ', () { setState(() => inputModel.ctrl = !inputModel.ctrl); - }, inputModel.ctrl), + }, active: inputModel.ctrl), wrap(' Alt ', () { setState(() => inputModel.alt = !inputModel.alt); - }, inputModel.alt), + }, active: inputModel.alt), wrap('Shift', () { setState(() => inputModel.shift = !inputModel.shift); - }, inputModel.shift), + }, active: inputModel.shift), wrap(isMac ? ' Cmd ' : ' Win ', () { setState(() => inputModel.command = !inputModel.command); - }, inputModel.command), + }, active: inputModel.command), ]; final keys = [ wrap( @@ -777,7 +782,14 @@ class _KeyHelpToolsState extends State { } }, ), - _fn), + active: _fn), + wrap( + '', + () => setState( + () => _pin = !_pin, + ), + active: _pin, + icon: Icons.push_pin), wrap( ' ... ', () => setState( @@ -788,7 +800,7 @@ class _KeyHelpToolsState extends State { } }, ), - _more), + active: _more), ]; final fn = [ SizedBox(width: 9999), @@ -828,16 +840,16 @@ class _KeyHelpToolsState extends State { SizedBox(width: 9999), wrap('', () { inputModel.inputKey('VK_LEFT'); - }, false, Icons.keyboard_arrow_left), + }, icon: Icons.keyboard_arrow_left), wrap('', () { inputModel.inputKey('VK_UP'); - }, false, Icons.keyboard_arrow_up), + }, icon: Icons.keyboard_arrow_up), wrap('', () { inputModel.inputKey('VK_DOWN'); - }, false, Icons.keyboard_arrow_down), + }, icon: Icons.keyboard_arrow_down), wrap('', () { inputModel.inputKey('VK_RIGHT'); - }, false, Icons.keyboard_arrow_right), + }, icon: Icons.keyboard_arrow_right), wrap(isMac ? 'Cmd+C' : 'Ctrl+C', () { sendPrompt(isMac, 'VK_C'); }), From 14a187f47105ae2d60ec6b91ae36a65894732be8 Mon Sep 17 00:00:00 2001 From: csf Date: Sun, 12 Feb 2023 22:44:53 +0900 Subject: [PATCH 1850/2015] change GestureHelp from ModalBottomSheet to bottomNavigationBar, add show KeyTools when GestureHelp showed --- flutter/lib/mobile/pages/remote_page.dart | 73 ++++++++++++----------- 1 file changed, 37 insertions(+), 36 deletions(-) diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 63a289c95..951d63faf 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -36,12 +36,13 @@ class RemotePage extends StatefulWidget { class _RemotePageState extends State { Timer? _timer; bool _showBar = !isWebDesktop; + bool _showGestureHelp = false; String _value = ''; double _scale = 1; double _mouseScrollIntegral = 0; // mouse scroll speed controller Orientation? _currentOrientation; - late final keyboardVisibilityController = KeyboardVisibilityController(); + final keyboardVisibilityController = KeyboardVisibilityController(); late final StreamSubscription keyboardSubscription; final FocusNode _mobileFocusNode = FocusNode(); final FocusNode _physicalFocusNode = FocusNode(); @@ -197,9 +198,9 @@ class _RemotePageState extends State { @override Widget build(BuildContext context) { final pi = Provider.of(context).pi; - final isHideKeyboardFAB = + final keyboardIsVisible = keyboardVisibilityController.isVisible && _showEdit; - final showActionButton = !_showBar || isHideKeyboardFAB; + final showActionButton = !_showBar || keyboardIsVisible || _showGestureHelp; final keyboard = gFFI.ffiModel.permissions['keyboard'] != false; return WillPopScope( @@ -209,33 +210,39 @@ class _RemotePageState extends State { }, child: getRawPointerAndKeyBody(Scaffold( // workaround for https://github.com/rustdesk/rustdesk/issues/3131 - floatingActionButtonLocation: isHideKeyboardFAB + floatingActionButtonLocation: keyboardIsVisible ? FABLocation(FloatingActionButtonLocation.endFloat, 0, -35) : null, floatingActionButton: !showActionButton ? null : FloatingActionButton( - mini: !isHideKeyboardFAB, + mini: !keyboardIsVisible, child: Icon( - isHideKeyboardFAB ? Icons.expand_more : Icons.expand_less, + (keyboardIsVisible || _showGestureHelp) + ? Icons.expand_more + : Icons.expand_less, color: Colors.white, ), backgroundColor: MyTheme.accent, onPressed: () { setState(() { - if (isHideKeyboardFAB) { + if (keyboardIsVisible) { _showEdit = false; gFFI.invokeMethod("enable_soft_keyboard", false); _mobileFocusNode.unfocus(); _physicalFocusNode.requestFocus(); + } else if (_showGestureHelp) { + _showGestureHelp = false; } else { _showBar = !_showBar; } }); }), - bottomNavigationBar: _showBar && pi.displays.isNotEmpty - ? getBottomAppBar(keyboard) - : null, + bottomNavigationBar: _showGestureHelp + ? getGestureHelp() + : (_showBar && pi.displays.isNotEmpty + ? getBottomAppBar(keyboard) + : null), body: Overlay( initialEntries: [ OverlayEntry(builder: (context) { @@ -325,7 +332,8 @@ class _RemotePageState extends State { icon: Icon(gFFI.ffiModel.touchMode ? Icons.touch_app : Icons.mouse), - onPressed: changeTouchMode, + onPressed: () => setState( + () => _showGestureHelp = !_showGestureHelp), ), ]) + (isWeb @@ -488,7 +496,7 @@ class _RemotePageState extends State { right: 10, child: QualityMonitor(gFFI.qualityMonitorModel), ), - KeyHelpTools(requestShow: keyboardIsVisible), + KeyHelpTools(requestShow: (keyboardIsVisible || _showGestureHelp)), SizedBox( width: 0, height: 0, @@ -658,29 +666,20 @@ class _RemotePageState extends State { }(); } - void changeTouchMode() { - setState(() => _showEdit = false); - showModalBottomSheet( - // backgroundColor: MyTheme.grayBg, - isScrollControlled: true, - context: context, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(5))), - builder: (context) => DraggableScrollableSheet( - expand: false, - builder: (context, scrollController) { - return SingleChildScrollView( - controller: ScrollController(), - padding: EdgeInsets.symmetric(vertical: 10), - child: GestureHelp( - touchMode: gFFI.ffiModel.touchMode, - onTouchModeChange: (t) { - gFFI.ffiModel.toggleTouchMode(); - final v = gFFI.ffiModel.touchMode ? 'Y' : ''; - bind.sessionPeerOption( - id: widget.id, name: "touch", value: v); - })); - })); + /// aka changeTouchMode + BottomAppBar getGestureHelp() { + return BottomAppBar( + child: SingleChildScrollView( + controller: ScrollController(), + padding: EdgeInsets.symmetric(vertical: 10), + child: GestureHelp( + touchMode: gFFI.ffiModel.touchMode, + onTouchModeChange: (t) { + gFFI.ffiModel.toggleTouchMode(); + final v = gFFI.ffiModel.touchMode ? 'Y' : ''; + bind.sessionPeerOption( + id: widget.id, name: "touch", value: v); + }))); } // * Currently mobile does not enable map mode @@ -719,6 +718,7 @@ class _KeyHelpToolsState extends State { var _more = true; var _fn = false; var _pin = false; + final _keyboardVisibilityController = KeyboardVisibilityController(); InputModel get inputModel => gFFI.inputModel; @@ -863,7 +863,8 @@ class _KeyHelpToolsState extends State { final space = size.width > 320 ? 4.0 : 2.0; return Container( color: Color(0xAA000000), - padding: EdgeInsets.only(top: widget.requestShow ? 24 : 4, bottom: 8), + padding: EdgeInsets.only( + top: _keyboardVisibilityController.isVisible ? 24 : 4, bottom: 8), child: Wrap( spacing: space, runSpacing: space, From 0ecc35dcb3e4e0e89f8d0405ef8dc230180cace8 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 13 Feb 2023 15:12:05 +0800 Subject: [PATCH 1851/2015] opt: fix codesign with strict and verbose mode --- .github/workflows/flutter-nightly.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index f03cd0be8..1ab21dbff 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -242,9 +242,9 @@ jobs: security unlock-keychain -p ${{ secrets.MACOS_P12_PASSWORD }} rustdesk.keychain # start sign the rustdesk.app and dmg rm rustdesk-${{ env.VERSION }}.dmg || true - codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep ./flutter/build/macos/Build/Products/Release/RustDesk.app -v + codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep --strict ./flutter/build/macos/Build/Products/Release/RustDesk.app -vvv create-dmg --icon "RustDesk.app" 200 190 --hide-extension "RustDesk.app" --window-size 800 400 --app-drop-link 600 185 rustdesk-${{ env.VERSION }}.dmg ./flutter/build/macos/Build/Products/Release/RustDesk.app - codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep rustdesk-${{ env.VERSION }}.dmg -v + codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep --strict rustdesk-${{ env.VERSION }}.dmg -vvv # notarize the rustdesk-${{ env.VERSION }}.dmg rcodesign notary-submit --api-key-path ${{ github.workspace }}/rustdesk.json --staple rustdesk-${{ env.VERSION }}.dmg From d45224dfd8ee487263532e33c0cc707361306f45 Mon Sep 17 00:00:00 2001 From: fufesou Date: Mon, 13 Feb 2023 16:04:47 +0800 Subject: [PATCH 1852/2015] refactor login error message Signed-off-by: fufesou --- flutter/lib/common/widgets/login.dart | 33 ++++++++++++++------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/flutter/lib/common/widgets/login.dart b/flutter/lib/common/widgets/login.dart index 05fc1fc5c..14a2c38bc 100644 --- a/flutter/lib/common/widgets/login.dart +++ b/flutter/lib/common/widgets/login.dart @@ -197,24 +197,25 @@ class _WidgetOPState extends State { _failedMsg = ''; } return Offstage( - offstage: - _failedMsg.isEmpty && widget.curOP.value != widget.config.op, - child: Row( - children: [ - Text( - _stateMsg, - style: TextStyle(fontSize: 12), - ), - SizedBox(width: 8), - Text( - _failedMsg, - style: TextStyle( - fontSize: 14, - color: Colors.red, - ), + offstage: + _failedMsg.isEmpty && widget.curOP.value != widget.config.op, + child: RichText( + text: TextSpan( + text: '$_stateMsg ', + style: + DefaultTextStyle.of(context).style.copyWith(fontSize: 12), + children: [ + TextSpan( + text: _failedMsg, + style: DefaultTextStyle.of(context).style.copyWith( + fontSize: 14, + color: Colors.red, + ), ), ], - )); + ), + ), + ); }), Obx( () => Offstage( From 9492f401f4e147b0c28f46392e075c78d1da7644 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Mon, 13 Feb 2023 16:18:46 +0800 Subject: [PATCH 1853/2015] fix: allowing idle scroll events --- flutter/lib/common.dart | 25 +++++++++++++++++++ .../lib/desktop/pages/connection_page.dart | 2 +- .../lib/desktop/pages/desktop_home_page.dart | 1 + .../desktop/pages/desktop_setting_page.dart | 16 ++++++------ 4 files changed, 35 insertions(+), 9 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 6c1245a7d..ba7e3d762 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1735,6 +1735,7 @@ Future updateSystemWindowTheme() async { } } } + /// macOS only /// /// Note: not found a general solution for rust based AVFoundation bingding. @@ -1762,3 +1763,27 @@ Future osxCanRecordAudio() async { Future osxRequestAudio() async { return await kMacOSPermChannel.invokeMethod("requestRecordAudio"); } + +class DraggableNeverScrollableScrollPhysics extends ScrollPhysics { + /// Creates scroll physics that does not let the user scroll. + const DraggableNeverScrollableScrollPhysics({super.parent}); + + @override + DraggableNeverScrollableScrollPhysics applyTo(ScrollPhysics? ancestor) { + return DraggableNeverScrollableScrollPhysics(parent: buildParent(ancestor)); + } + + @override + bool shouldAcceptUserOffset(ScrollMetrics position) { + // TODO: find a better solution to check if the offset change is caused by the scrollbar. + // Workaround: when dragging with the scrollbar, it always triggers an [IdleScrollActivity]. + if (position is ScrollPositionWithSingleContext) { + // ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member + return position.activity is IdleScrollActivity; + } + return false; + } + + @override + bool get allowImplicitScrolling => false; +} diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index eee4c6a20..f352c313e 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -120,7 +120,7 @@ class _ConnectionPageState extends State scrollController: _scrollController, child: CustomScrollView( controller: _scrollController, - physics: NeverScrollableScrollPhysics(), + physics: DraggableNeverScrollableScrollPhysics(), slivers: [ SliverList( delegate: SliverChildListDelegate([ diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index cde1e6d74..af7f14815 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -75,6 +75,7 @@ class _DesktopHomePageState extends State scrollController: _leftPaneScrollController, child: SingleChildScrollView( controller: _leftPaneScrollController, + physics: DraggableNeverScrollableScrollPhysics(), child: Column( children: [ buildTip(context), diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 80dcd80b1..378ddbd1b 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -128,7 +128,7 @@ class _DesktopSettingPageState extends State scrollController: controller, child: PageView( controller: controller, - physics: NeverScrollableScrollPhysics(), + physics: DraggableNeverScrollableScrollPhysics(), children: const [ _General(), _Safety(), @@ -170,7 +170,7 @@ class _DesktopSettingPageState extends State return DesktopScrollWrapper( scrollController: scrollController, child: ListView( - physics: NeverScrollableScrollPhysics(), + physics: DraggableNeverScrollableScrollPhysics(), controller: scrollController, children: tabs .asMap() @@ -234,7 +234,7 @@ class _GeneralState extends State<_General> { return DesktopScrollWrapper( scrollController: scrollController, child: ListView( - physics: NeverScrollableScrollPhysics(), + physics: DraggableNeverScrollableScrollPhysics(), controller: scrollController, children: [ theme(), @@ -456,7 +456,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { return DesktopScrollWrapper( scrollController: scrollController, child: SingleChildScrollView( - physics: NeverScrollableScrollPhysics(), + physics: DraggableNeverScrollableScrollPhysics(), controller: scrollController, child: Column( children: [ @@ -908,7 +908,7 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin { scrollController: scrollController, child: ListView( controller: scrollController, - physics: NeverScrollableScrollPhysics(), + physics: DraggableNeverScrollableScrollPhysics(), children: [ _lock(locked, 'Unlock Network Settings', () { locked = false; @@ -1094,7 +1094,7 @@ class _DisplayState extends State<_Display> { scrollController: scrollController, child: ListView( controller: scrollController, - physics: NeverScrollableScrollPhysics(), + physics: DraggableNeverScrollableScrollPhysics(), children: [ viewStyle(context), scrollStyle(context), @@ -1334,7 +1334,7 @@ class _AccountState extends State<_Account> { return DesktopScrollWrapper( scrollController: scrollController, child: ListView( - physics: NeverScrollableScrollPhysics(), + physics: DraggableNeverScrollableScrollPhysics(), controller: scrollController, children: [ _Card(title: 'Account', children: [accountAction()]), @@ -1378,7 +1378,7 @@ class _AboutState extends State<_About> { scrollController: scrollController, child: SingleChildScrollView( controller: scrollController, - physics: NeverScrollableScrollPhysics(), + physics: DraggableNeverScrollableScrollPhysics(), child: _Card(title: '${translate('About')} RustDesk', children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, From 6f106251f923d215f4b76e93143b1bf50838b141 Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 13 Feb 2023 16:40:24 +0800 Subject: [PATCH 1854/2015] force relay when id is suffixed with "/r" Signed-off-by: 21pages --- flutter/lib/common.dart | 25 ++++++++------- .../lib/desktop/pages/connection_page.dart | 10 ++++-- .../lib/desktop/pages/desktop_home_page.dart | 1 + .../lib/desktop/pages/file_manager_page.dart | 6 ++-- .../desktop/pages/file_manager_tab_page.dart | 12 +++++-- .../lib/desktop/pages/port_forward_page.dart | 6 ++-- .../desktop/pages/port_forward_tab_page.dart | 8 ++++- flutter/lib/desktop/pages/remote_page.dart | 3 ++ .../lib/desktop/pages/remote_tab_page.dart | 2 ++ flutter/lib/models/model.dart | 13 ++++---- flutter/lib/utils/multi_window_manager.dart | 28 +++++++++++----- src/client.rs | 32 +++++++++++-------- src/flutter.rs | 13 ++++---- src/flutter_ffi.rs | 11 +++++-- src/ui/remote.rs | 20 ++++++++---- 15 files changed, 127 insertions(+), 63 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 6c1245a7d..ca34eace4 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1405,13 +1405,14 @@ bool callUniLinksUriHandler(Uri uri) { connectMainDesktop(String id, {required bool isFileTransfer, required bool isTcpTunneling, - required bool isRDP}) async { + required bool isRDP, + bool? forceRelay}) async { if (isFileTransfer) { - await rustDeskWinManager.newFileTransfer(id); + await rustDeskWinManager.newFileTransfer(id, forceRelay: forceRelay); } else if (isTcpTunneling || isRDP) { - await rustDeskWinManager.newPortForward(id, isRDP); + await rustDeskWinManager.newPortForward(id, isRDP, forceRelay: forceRelay); } else { - await rustDeskWinManager.newRemoteDesktop(id); + await rustDeskWinManager.newRemoteDesktop(id, forceRelay: forceRelay); } } @@ -1422,7 +1423,8 @@ connectMainDesktop(String id, connect(BuildContext context, String id, {bool isFileTransfer = false, bool isTcpTunneling = false, - bool isRDP = false}) async { + bool isRDP = false, + bool forceRelay = false}) async { if (id == '') return; id = id.replaceAll(' ', ''); assert(!(isFileTransfer && isTcpTunneling && isRDP), @@ -1430,18 +1432,18 @@ connect(BuildContext context, String id, if (isDesktop) { if (desktopType == DesktopType.main) { - await connectMainDesktop( - id, - isFileTransfer: isFileTransfer, - isTcpTunneling: isTcpTunneling, - isRDP: isRDP, - ); + await connectMainDesktop(id, + isFileTransfer: isFileTransfer, + isTcpTunneling: isTcpTunneling, + isRDP: isRDP, + forceRelay: forceRelay); } else { await rustDeskWinManager.call(WindowType.Main, kWindowConnect, { 'id': id, 'isFileTransfer': isFileTransfer, 'isTcpTunneling': isTcpTunneling, 'isRDP': isRDP, + "forceRelay": forceRelay, }); } } else { @@ -1735,6 +1737,7 @@ Future updateSystemWindowTheme() async { } } } + /// macOS only /// /// Note: not found a general solution for rust based AVFoundation bingding. diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index eee4c6a20..71660cfa7 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -66,7 +66,8 @@ class _ConnectionPageState extends State _idFocusNode.addListener(() { _idInputFocused.value = _idFocusNode.hasFocus; // select all to faciliate removing text, just following the behavior of address input of chrome - _idController.selection = TextSelection(baseOffset: 0, extentOffset: _idController.value.text.length); + _idController.selection = TextSelection( + baseOffset: 0, extentOffset: _idController.value.text.length); }); windowManager.addListener(this); } @@ -149,8 +150,11 @@ class _ConnectionPageState extends State /// Callback for the connect button. /// Connects to the selected peer. void onConnect({bool isFileTransfer = false}) { - final id = _idController.id; - connect(context, id, isFileTransfer: isFileTransfer); + var id = _idController.id; + var forceRelay = id.endsWith(r'/r'); + if (forceRelay) id = id.substring(0, id.length - 2); + connect(context, id, + isFileTransfer: isFileTransfer, forceRelay: forceRelay); } /// UI for the remote ID TextField. diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index cde1e6d74..ced8e33eb 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -556,6 +556,7 @@ class _DesktopHomePageState extends State isFileTransfer: call.arguments['isFileTransfer'], isTcpTunneling: call.arguments['isTcpTunneling'], isRDP: call.arguments['isRDP'], + forceRelay: call.arguments['forceRelay'], ); } }); diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 27bb0377d..988baca57 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -46,8 +46,10 @@ enum MouseFocusScope { } class FileManagerPage extends StatefulWidget { - const FileManagerPage({Key? key, required this.id}) : super(key: key); + const FileManagerPage({Key? key, required this.id, this.forceRelay}) + : super(key: key); final String id; + final bool? forceRelay; @override State createState() => _FileManagerPageState(); @@ -102,7 +104,7 @@ class _FileManagerPageState extends State void initState() { super.initState(); _ffi = FFI(); - _ffi.start(widget.id, isFileTransfer: true); + _ffi.start(widget.id, isFileTransfer: true, forceRelay: widget.forceRelay); WidgetsBinding.instance.addPostFrameCallback((_) { _ffi.dialogManager .showLoading(translate('Connecting...'), onCancel: closeConnection); diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index b2566e267..7540f7662 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -41,7 +41,11 @@ class _FileManagerTabPageState extends State { selectedIcon: selectedIcon, unselectedIcon: unselectedIcon, onTabCloseButton: () => () => tabController.closeBy(params['id']), - page: FileManagerPage(key: ValueKey(params['id']), id: params['id']))); + page: FileManagerPage( + key: ValueKey(params['id']), + id: params['id'], + forceRelay: params['forceRelay'], + ))); } @override @@ -64,7 +68,11 @@ class _FileManagerTabPageState extends State { selectedIcon: selectedIcon, unselectedIcon: unselectedIcon, onTabCloseButton: () => tabController.closeBy(id), - page: FileManagerPage(key: ValueKey(id), id: id))); + page: FileManagerPage( + key: ValueKey(id), + id: id, + forceRelay: args['forceRelay'], + ))); } else if (call.method == "onDestroy") { tabController.clear(); } else if (call.method == kWindowActionRebuild) { diff --git a/flutter/lib/desktop/pages/port_forward_page.dart b/flutter/lib/desktop/pages/port_forward_page.dart index 2385813eb..2ac6bf23a 100644 --- a/flutter/lib/desktop/pages/port_forward_page.dart +++ b/flutter/lib/desktop/pages/port_forward_page.dart @@ -26,10 +26,12 @@ class _PortForward { } class PortForwardPage extends StatefulWidget { - const PortForwardPage({Key? key, required this.id, required this.isRDP}) + const PortForwardPage( + {Key? key, required this.id, required this.isRDP, this.forceRelay}) : super(key: key); final String id; final bool isRDP; + final bool? forceRelay; @override State createState() => _PortForwardPageState(); @@ -47,7 +49,7 @@ class _PortForwardPageState extends State void initState() { super.initState(); _ffi = FFI(); - _ffi.start(widget.id, isPortForward: true); + _ffi.start(widget.id, isPortForward: true, forceRelay: widget.forceRelay); Get.put(_ffi, tag: 'pf_${widget.id}'); if (!Platform.isLinux) { Wakelock.enable(); diff --git a/flutter/lib/desktop/pages/port_forward_tab_page.dart b/flutter/lib/desktop/pages/port_forward_tab_page.dart index ca354f297..ee5dd9b53 100644 --- a/flutter/lib/desktop/pages/port_forward_tab_page.dart +++ b/flutter/lib/desktop/pages/port_forward_tab_page.dart @@ -44,6 +44,7 @@ class _PortForwardTabPageState extends State { key: ValueKey(params['id']), id: params['id'], isRDP: isRDP, + forceRelay: params['forceRelay'], ))); } @@ -72,7 +73,12 @@ class _PortForwardTabPageState extends State { label: id, selectedIcon: selectedIcon, unselectedIcon: unselectedIcon, - page: PortForwardPage(id: id, isRDP: isRDP))); + page: PortForwardPage( + key: ValueKey(args['id']), + id: id, + isRDP: isRDP, + forceRelay: args['forceRelay'], + ))); } else if (call.method == "onDestroy") { tabController.clear(); } else if (call.method == kWindowActionRebuild) { diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 211d36c39..f9db985d9 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -34,11 +34,13 @@ class RemotePage extends StatefulWidget { required this.id, required this.menubarState, this.switchUuid, + this.forceRelay, }) : super(key: key); final String id; final MenubarState menubarState; final String? switchUuid; + final bool? forceRelay; final SimpleWrapper?> _lastState = SimpleWrapper(null); FFI get ffi => (_lastState.value! as _RemotePageState)._ffi; @@ -107,6 +109,7 @@ class _RemotePageState extends State _ffi.start( widget.id, switchUuid: widget.switchUuid, + forceRelay: widget.forceRelay, ); WidgetsBinding.instance.addPostFrameCallback((_) { SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index 9b00b481f..c251aadc1 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -70,6 +70,7 @@ class _ConnectionTabPageState extends State { id: peerId, menubarState: _menubarState, switchUuid: params['switch_uuid'], + forceRelay: params['forceRelay'], ), )); _update_remote_count(); @@ -104,6 +105,7 @@ class _ConnectionTabPageState extends State { id: id, menubarState: _menubarState, switchUuid: switchUuid, + forceRelay: args['forceRelay'], ), )); } else if (call.method == "onDestroy") { diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 8cf90eba9..d0a2ea601 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -1339,7 +1339,8 @@ class FFI { void start(String id, {bool isFileTransfer = false, bool isPortForward = false, - String? switchUuid}) { + String? switchUuid, + bool? forceRelay}) { assert(!(isFileTransfer && isPortForward), 'more than one connect type'); if (isFileTransfer) { connType = ConnType.fileTransfer; @@ -1355,11 +1356,11 @@ class FFI { } // ignore: unused_local_variable final addRes = bind.sessionAddSync( - id: id, - isFileTransfer: isFileTransfer, - isPortForward: isPortForward, - switchUuid: switchUuid ?? "", - ); + id: id, + isFileTransfer: isFileTransfer, + isPortForward: isPortForward, + switchUuid: switchUuid ?? "", + forceRelay: forceRelay ?? false); final stream = bind.sessionStart(id: id); final cb = ffiModel.startEventListener(id); () async { diff --git a/flutter/lib/utils/multi_window_manager.dart b/flutter/lib/utils/multi_window_manager.dart index 3af189ef6..864659a66 100644 --- a/flutter/lib/utils/multi_window_manager.dart +++ b/flutter/lib/utils/multi_window_manager.dart @@ -41,11 +41,15 @@ class RustDeskMultiWindowManager { int? _fileTransferWindowId; int? _portForwardWindowId; - Future newRemoteDesktop(String remoteId, - {String? switch_uuid}) async { + Future newRemoteDesktop( + String remoteId, { + String? switch_uuid, + bool? forceRelay, + }) async { var params = { "type": WindowType.RemoteDesktop.index, "id": remoteId, + "forceRelay": forceRelay }; if (switch_uuid != null) { params['switch_uuid'] = switch_uuid; @@ -78,9 +82,12 @@ class RustDeskMultiWindowManager { } } - Future newFileTransfer(String remoteId) async { - final msg = - jsonEncode({"type": WindowType.FileTransfer.index, "id": remoteId}); + Future newFileTransfer(String remoteId, {bool? forceRelay}) async { + var msg = jsonEncode({ + "type": WindowType.FileTransfer.index, + "id": remoteId, + "forceRelay": forceRelay, + }); try { final ids = await DesktopMultiWindow.getAllSubWindowIds(); @@ -107,9 +114,14 @@ class RustDeskMultiWindowManager { } } - Future newPortForward(String remoteId, bool isRDP) async { - final msg = jsonEncode( - {"type": WindowType.PortForward.index, "id": remoteId, "isRDP": isRDP}); + Future newPortForward(String remoteId, bool isRDP, + {bool? forceRelay}) async { + final msg = jsonEncode({ + "type": WindowType.PortForward.index, + "id": remoteId, + "isRDP": isRDP, + "forceRelay": forceRelay, + }); try { final ids = await DesktopMultiWindow.getAllSubWindowIds(); diff --git a/src/client.rs b/src/client.rs index a21592578..05b34d781 100644 --- a/src/client.rs +++ b/src/client.rs @@ -3,15 +3,15 @@ use std::{ net::SocketAddr, ops::{Deref, Not}, str::FromStr, - sync::{Arc, atomic::AtomicBool, mpsc, Mutex, RwLock}, + sync::{atomic::AtomicBool, mpsc, Arc, Mutex, RwLock}, }; pub use async_trait::async_trait; use bytes::Bytes; #[cfg(not(any(target_os = "android", target_os = "linux")))] use cpal::{ - Device, - Host, StreamConfig, traits::{DeviceTrait, HostTrait, StreamTrait}, + traits::{DeviceTrait, HostTrait, StreamTrait}, + Device, Host, StreamConfig, }; use magnum_opus::{Channels::*, Decoder as AudioDecoder}; use sha2::{Digest, Sha256}; @@ -19,26 +19,26 @@ use uuid::Uuid; pub use file_trait::FileManager; use hbb_common::{ - AddrMangle, allow_err, anyhow::{anyhow, Context}, bail, config::{ - Config, CONNECT_TIMEOUT, PeerConfig, PeerInfoSerde, READ_TIMEOUT, RELAY_PORT, + Config, PeerConfig, PeerInfoSerde, CONNECT_TIMEOUT, READ_TIMEOUT, RELAY_PORT, RENDEZVOUS_TIMEOUT, - }, get_version_number, - log, - message_proto::{*, option_message::BoolOption}, + }, + get_version_number, log, + message_proto::{option_message::BoolOption, *}, protobuf::Message as _, rand, rendezvous_proto::*, - ResultType, socket_client, sodiumoxide::crypto::{box_, secretbox, sign}, - Stream, timeout, tokio::time::Duration, + timeout, + tokio::time::Duration, + AddrMangle, ResultType, Stream, }; -pub use helper::*; pub use helper::LatencyController; +pub use helper::*; use scrap::{ codec::{Decoder, DecoderCfg}, record::{Recorder, RecorderContext}, @@ -943,7 +943,13 @@ impl LoginConfigHandler { /// /// * `id` - id of peer /// * `conn_type` - Connection type enum. - pub fn initialize(&mut self, id: String, conn_type: ConnType, switch_uuid: Option) { + pub fn initialize( + &mut self, + id: String, + conn_type: ConnType, + switch_uuid: Option, + force_relay: bool, + ) { self.id = id; self.conn_type = conn_type; let config = self.load_config(); @@ -952,7 +958,7 @@ impl LoginConfigHandler { self.session_id = rand::random(); self.supported_encoding = None; self.restarting_remote_device = false; - self.force_relay = !self.get_option("force-always-relay").is_empty(); + self.force_relay = !self.get_option("force-always-relay").is_empty() || force_relay; self.direct = None; self.received = false; self.switch_uuid = switch_uuid; diff --git a/src/flutter.rs b/src/flutter.rs index a60e379f9..0161e644a 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -3,19 +3,19 @@ use crate::{ flutter_ffi::EventToUI, ui_session_interface::{io_loop, InvokeUiSession, Session}, }; -use flutter_rust_bridge::{StreamSink}; +use flutter_rust_bridge::StreamSink; use hbb_common::{ bail, config::LocalConfig, get_version_number, message_proto::*, rendezvous_proto::ConnType, ResultType, }; use serde_json::json; +use std::sync::atomic::{AtomicBool, Ordering}; use std::{ collections::HashMap, ffi::CString, os::raw::{c_char, c_int}, sync::{Arc, RwLock}, }; -use std::sync::atomic::{AtomicBool, Ordering}; pub(super) const APP_TYPE_MAIN: &str = "main"; pub(super) const APP_TYPE_CM: &str = "cm"; @@ -114,7 +114,7 @@ pub struct FlutterHandler { // SAFETY: [rgba] is guarded by [rgba_valid], and it's safe to reach [rgba] with `rgba_valid == true`. // We must check the `rgba_valid` before reading [rgba]. pub rgba: Arc>>, - pub rgba_valid: Arc + pub rgba_valid: Arc, } impl FlutterHandler { @@ -449,6 +449,7 @@ pub fn session_add( is_file_transfer: bool, is_port_forward: bool, switch_uuid: &str, + force_relay: bool, ) -> ResultType<()> { let session_id = get_session_id(id.to_owned()); LocalConfig::set_remote_id(&session_id); @@ -477,7 +478,7 @@ pub fn session_add( .lc .write() .unwrap() - .initialize(session_id, conn_type, switch_uuid); + .initialize(session_id, conn_type, switch_uuid, force_relay); if let Some(same_id_session) = SESSIONS.write().unwrap().insert(id.to_owned(), session) { same_id_session.close(); @@ -667,7 +668,7 @@ pub fn session_get_rgba_size(id: *const char) -> usize { let id = unsafe { std::ffi::CStr::from_ptr(id as _) }; if let Ok(id) = id.to_str() { if let Some(session) = SESSIONS.write().unwrap().get_mut(id) { - return session.rgba.read().unwrap().len(); + return session.rgba.read().unwrap().len(); } } 0 @@ -692,4 +693,4 @@ pub fn session_next_rgba(id: *const char) { return session.next_rgba(); } } -} \ No newline at end of file +} diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index b4e79b361..3025d722c 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1,3 +1,4 @@ +use crate::ui_session_interface::InvokeUiSession; use crate::{ client::file_trait::FileManager, common::make_fd_to_json, @@ -20,7 +21,6 @@ use std::{ os::raw::c_char, str::FromStr, }; -use crate::ui_session_interface::InvokeUiSession; // use crate::hbbs_http::account::AuthResult; @@ -84,8 +84,15 @@ pub fn session_add_sync( is_file_transfer: bool, is_port_forward: bool, switch_uuid: String, + force_relay: bool, ) -> SyncReturn { - if let Err(e) = session_add(&id, is_file_transfer, is_port_forward, &switch_uuid) { + if let Err(e) = session_add( + &id, + is_file_transfer, + is_port_forward, + &switch_uuid, + force_relay, + ) { SyncReturn(format!("Failed to add session with id {}, {}", &id, e)) } else { SyncReturn("".to_owned()) diff --git a/src/ui/remote.rs b/src/ui/remote.rs index e44e31401..447c2e31d 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -1,24 +1,24 @@ +use std::sync::RwLock; use std::{ collections::HashMap, ops::{Deref, DerefMut}, sync::{Arc, Mutex}, }; -use std::sync::RwLock; use sciter::{ dom::{ - Element, - event::{BEHAVIOR_EVENTS, EVENT_GROUPS, EventReason, PHASE_MASK}, HELEMENT, + event::{EventReason, BEHAVIOR_EVENTS, EVENT_GROUPS, PHASE_MASK}, + Element, HELEMENT, }, make_args, + video::{video_destination, AssetPtr, COLOR_SPACE}, Value, - video::{AssetPtr, COLOR_SPACE, video_destination}, }; +use hbb_common::tokio::io::AsyncReadExt; use hbb_common::{ allow_err, fs::TransferJobMeta, log, message_proto::*, rendezvous_proto::ConnType, }; -use hbb_common::tokio::io::AsyncReadExt; use crate::{ client::*, @@ -286,7 +286,9 @@ impl InvokeUiSession for SciterHandler { } /// RGBA is directly rendered by [on_rgba]. No need to store the rgba for the sciter ui. - fn get_rgba(&self) -> *const u8 { std::ptr::null() } + fn get_rgba(&self) -> *const u8 { + std::ptr::null() + } fn next_rgba(&mut self) {} } @@ -467,7 +469,11 @@ impl SciterSession { ConnType::DEFAULT_CONN }; - session.lc.write().unwrap().initialize(id, conn_type, None); + session + .lc + .write() + .unwrap() + .initialize(id, conn_type, None, false); Self(session) } From 201646da4c0248bdc64dffac22c243489abfaa51 Mon Sep 17 00:00:00 2001 From: Ikko Eltociear Ashimine Date: Mon, 13 Feb 2023 18:20:40 +0900 Subject: [PATCH 1855/2015] add translate ja readme --- docs/README-JP.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/README-JP.md b/docs/README-JP.md index 6d3b6d380..36c74dfed 100644 --- a/docs/README-JP.md +++ b/docs/README-JP.md @@ -14,7 +14,7 @@ Chat with us: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitt [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) -Rustで書かれた、設定不要ですぐに使えるリモートデスクトップソフトウェアです。自分のデータを完全にコントロールでき、セキュリティの心配もありません。私たちのランデブー/リレーサーバを使うことも、[自分で設定する](https://rustdesk.com/server) ことも、 [自分でランデブー/リレーサーバを書くこともできます。](https://github.com/rustdesk/rustdesk-server-demo). +Rustで書かれた、設定不要ですぐに使えるリモートデスクトップソフトウェアです。自分のデータを完全にコントロールでき、セキュリティの心配もありません。私たちのランデブー/リレーサーバを使うことも、[自分で設定する](https://rustdesk.com/server) ことも、 [自分でランデブー/リレーサーバを書くこともできます](https://github.com/rustdesk/rustdesk-server-demo)。 ![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) @@ -58,7 +58,7 @@ RustDeskは誰からの貢献も歓迎します。 貢献するには [`docs/CON -## [Build](https://rustdesk.com/docs/en/dev/build/) +## [ビルド](https://rustdesk.com/docs/en/dev/build/) ## Linuxでのビルド手順 @@ -105,7 +105,7 @@ cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/ cd ``` -### Build +### ビルド ```sh curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh @@ -154,7 +154,7 @@ target/release/rustdesk これらのコマンドをRustDeskリポジトリのルートから実行していることを確認してください。そうしないと、アプリケーションが必要なリソースを見つけられない可能性があります。また、 `install` や `run` などの他の cargo サブコマンドは、ホストではなくコンテナ内にプログラムをインストールまたは実行するため、現在この方法ではサポートされていないことに注意してください。 -## File Structure +## ファイル構造 - **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: ビデオコーデック、コンフィグ、tcp/udpラッパー、protobuf、ファイル転送用のfs関数、その他のユーティリティ関数 - **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: スクリーンキャプチャ @@ -165,7 +165,7 @@ target/release/rustdesk - **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: [rustdesk-server](https://github.com/rustdesk/rustdesk-server), と通信し、リモートダイレクト (TCP hole punching) または中継接続を待つ。 - **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: プラットフォーム固有のコード -## Snapshot +## スナップショット ![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) From 65e1b7d74e653319fc453f24836c14b88e824a60 Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Mon, 13 Feb 2023 10:53:05 +0100 Subject: [PATCH 1856/2015] 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 zFzULhKo)QowST`E zDuYNLi<_rOn~EI-v4-);BFCsU<(#&ahG!6NR6xRJPI9BsFLHeN;z00G1c*mFdlGsgz);J|qkGsDc-&8PK ztQx;K$>F3Kd+p}DqtgTXXFi^>2P#p=cFii+6wXMOaGy=}F^fF2`HIYyY_PTBUMo5O ztEU@BSWP@b8lQ&4M!_rJPy3k4lE_+W*@ucp_G3mK+T%JzwB4?i&Qq0M=#%`PuO zor&kh7OAGkFJHEKCN6qcHEG4??YW`kiS0X&WOy!Qi3-q3Y7}3Ji4k4+-tGRFO^f&# zf2{cW^=IGh@*{i`^Tsd}HiiD5b>?!wbg@L*FS^?FQx>R4lee3Ci3FRx#auL zh6nok2^*Fe$MX)+QmYB-Un61jR4_>W0gFs>SR2fJ4%DhBRRBt*n}M`Gn>WrE*o8A5 z^=PDp4m}P9CAS%qZK*GQ4zsi9o=?IO?p0?c9-KF!M?@V3v&gp>sN$&TG@l4GIsGk; z{Wu7-6w_^DU?`03%<5sj$PeALrP1l$&;fz5On)% zW%Ck^8n08rWJ+?|C;AvrmC85t!|KX!%J7({Et{u?Wa`N-*(-!(v1d{_$LxrtM zd6G1GX`aA*<=(IMf~lU^^Y+o5e4}Meez#z0IZn|q z&}MgPD>>U+^m6`02Y3sq!%iVkNH#grg>(bap*AmmZgYaU4QYy9h<2K0q<8vrMpXd1 za1?8V=S?+vZ(*=NdeKpd->9hXIc^`$cO=db6uVCxbhE`+vPegU&1u2d55hJNuHIB2 zC4FIncWJ#abTh3nyppGyyZ+}a6x`&fwSaCL9>J;wv=xBX%UG<#t7avpsUQzH)Bsd< zWpgbdcwozo8;=%|$N2fz-2EYyH5>?ehvr2|$#(G5stm90f;Rm^U=)aC18i-!w#=1U z>|n zqK{c_s)DOPFUzMydUP#S0}#I*Y@-_|Jm>MMMdqU52Q7F{4wj{Erx zvYOLBEzYTzS_4X%7bH~|&_mnT{!X*gUz+UnTh+|G==l`S0h(dpi|2oQivyjn`olfD zjPT&M=y|bo=^UUm_=g@grTrOQ?>#-9Bwabi32k4Pzf9)1TJEr;t-rIdb*FEv)|c=2 zMJ`ALY$mkOPe6eErABH6h?pRS!*0uCq`&DjQ8kLT42@P$hCiyWN?t4#?=Q~6`9F3T z+{}}8_!AS`&$O6gDQ{gznMAGIrPpc6wmE{3Aq4m)<-QfiM}5^V@LVPF&rQ}wBTznN zku}PEMK14iLjNAnzd^QdCv6jgjyr~D9ZAyoY6=YivLv(dJ4k7Au4;{w8`BQze++Fm z8_GcmFBo(#C~VTLP?k{vZq*eo9_@pyBn?7rl2cYCs;g)AC^h@@2+Ol-Xv7(aLu52P%&&?Sct7 zL4;x3c(p%Es`oCj-)rng>*I|M4|&LxQtP^_KVX9+P0-ga{biuEX6Yl->ol^N^P+1U z+RL&T;FVPRV+yL&P*}U@9m%nNQVC0UJmqxG?dZv$VgP)!;3o?BjTT55XD-+8Jpaq; zKipk#-S+1kt(lN1xgI@$wo4DrnV=$rJx#~BwUiSahy%IyGBMR1L#NUG%#lodT`U(7 zn9r0gc~}Jrxno7w2g$p874>ey_D$Xw+;!+#a7BNZRCiHfQiV;rxMPK89+AFQYU6y2ZPc2W3`Tr}xGu=g9D2fdk6@2uU!wJ0O-0s$ z=4A16o~h`PQSWP(cCUHS(jV>~N&V8Z4a1}+eIXo#oUR!YB+6~#+qFq<0I8=z1v$7* z%_{4R*gy^N2G>A)bb7?vhN6^TN@Gcv{-U_{Hm77fC@XA73GF7k@6uM!9R;)ky}sLa z3~Dc5Y5Hwu8w9#AXhCq!2ni4pZ+ESvU;~`QUh6h1T)D7(>_a4*1$F_{@P@PQX#Ykl z5G=^ojq@nPR@R4X8{^(|{UeYccwZk<_%vP`RkFH%_v^kI&>PR8&Xdj!WEr8o;UsJ) zx32KX<#^CR<98eFu!DOZSN$V8O7AsZ;o3p)TSaMzmnBW~&W1xfq)>`9O2p26x{jGN zJ2&=H^Q6U1X9Y?y_1I|ZpLJRpY5R><0zu9iaEcz=A^X^>)f9CQzd&?<^=K_oK(F`r z+RNyDd;%}?8CVB*iwp()=jfGEYY*2%x$w%tvkqktaUT979> z9x>T^sqmFEw>r4uNkhqc2}sP-20$|Hd?(qWe{ZpH!YSM`HA}3;Sjf%;s#9YD=ANri zL)s4AG(>?5@a?3K5~1>fxriXQlbT=aP%e~Xc(_^IOfiUN-K3L;Qi+ z-S-yPA}ji3q4ilQM|$UaSqkPwS}RwcA7iJyoRE?EHtxxjhaid{njh7T7)m~*fEfOP z-4;K$vE>~Iwc;yry9_Cma*W?&V^3^Znu^1{zGy5JcO7DDTDH7!N(sl=?T(ArWD7dM z`~tl8S6Bb)h{0|#e=_#^2fh#foYyH=_Tc3tdYH|*8@nz(PfuOi1L{cii17uiii2TH z);ft0jx-I}SxGf;H2k3ie#Qa=oZ=(gKvYIeTdXgZI10(MWmSuxo7)ML!7fFb_`a75 z=}U>Vhvx5mZa|Z1k?`R6*dga>LH2{5wFQbtI1LQGVo=NPBdHC~{LKC1Rqwfm#k5<6 zQd1DWIGF9R#<{-T!=b}xHD_!DJUArYCOD%@ENxaH9ly7|hUVHkopkw7NpU3D2?~NL z)<>nR^?LB+S>I5mvz)XY7gG1;X_p2)oIOqAt5(;kC=#-1Pwf$wo%SZ0fY%i$tpXtn zxOpf%-D%XvcsXtEkw(Ykf{_A9{sE)%&zKr)%6IGpH5Cq}CMGg7g>~xGM6& zB);dYtVy?!fyroTa@F2-;{T)Sz2mX|zyI;)#Z^?c5)q-StjNj?O+^SWT4M42#uhjS+Nb*I+zfP%gq{d zu@E{1*wD*~*#FI}u1ZH)1IRgb_`$u%FRFD>;(#(vO z56wR6=>g^W^KUxZb-woV&}RnHZyn)Q!EJpa*x72`817%{b6WmT1AM1w=nR*V9CFN< z)4O(P9{u<~SqFHfwdz~e`|Be zH_E;}37092%-b(Ri7x>Ev0iLxR_TVGxy$sQC$%-GC?JG*U*B)`7*|D4pB2)0agY2S z0zzJlo88n*Hsqv!rnM}`xKqkFV863UbJy z51J1@Td0%dnk;z1bBwl*lrO$2i2G%B_cH9nB&<%k&EKTS3)vfry zY0z{G$ZbwP_3U~XH;x6UZLOkf9j`uDaWPTcdaC2ll4Uo2xlIdt9)T31UmIS(?orxu z>coFEQq;oEprQoi;C@1^5*Z9wQe-P$oC?KGwUnz$9~BBq)`r&Y*48*hS})g+B8Y1W z(P^|yAr_@SO)Xvx%?R zbPmC~l0@vkkdIQ%TrZPf=Ek#Lg&cl#fzOApIgh+Ri@}RoBpB~G zh&D@^!m`0|=Lh9)-wx_IBK(MqXkkIx$h_K}5oNv$Ta-le>?@r|0yPKrylW1dF9wJJ zV~+nO6YFpMM6G7Dkzs*}vM{vN>t=}+UsSzZ@gX-=ChA@O$gk7G^Hr1zjHmXRin;v! zx%6eJ`mSlm#kuQlrUneA`xti&R|GSL9}N%pHyPHhlau>1Jv23*l(e|$((&C?$4&9# zK+8rmU)w^?YoS$5<(7qs6P`>Gf(^347QBecU|(J;`iF4Ql!2Kd-FhIi!j(BjvZkYc zR6F#x28S*7p7yglYN*_P- z!+3^9LZMDg`|ruGqjr5==MbU_NWjAO#M8)8w~=X*4z+H(5;aa!>a@Du39&z`1Zjj= zyy3cXHm|cshMUMh@w8>s_kHOFl5;!tE=gJ56zG?9xso8KC$7#w5uH!3N{p=dAS)y( zx5R~*tgQC;w<^P!s>IfX;zY$=9`m|k3Evw+JQ#s=L7gez!536^etX1+vW_m7m7JMg zvr-Uu6MF2K5LRlOKSRo*_jG6EZRrxiNx-8(Nu4jce$p;#R4t z)v*abGQ>;gN`t(!o{HennH7A%pdyU+Jqykn`ME#2#dbWjasHD}-n$3!i#+bI*0XM9 zy=eW(9k0W>V2GRL72H)X{)BsP9oOEXLN2MEOKj=OeSLwY;KPZ|U4!(n zfQ04gXoZ3!Gm8+`GQkXPqsCUJU%vb}hQi@cy1ab2T&94DqBDKv67_5VOgw)KUgCsl zH=9mGwHmMf!f~sBDQI^qJ6}`pmx^!Og;RN%ZSrs?KR1S<>U6oADlz8F`gL0?lbbTR z9IEfW4Kd1AnqT<+<0N^!NpZW)mA?0>p(sCs$THUx(w4eA&r@5Z^~-Zt9W0d}Nq$Y| z_Dwbg4h8)AGs$(7IngB>m9VMm)D|OK&J#E0WbHPokhfo|ci8_nV>jM~v-#o0ix(HA zJcOuxgEm&Wq%v-9Q^z14@^I(b`WYf~#C~~HBf7Lhsi%^1$G7L{Go?m+O~2IeWZGXd zlyQ((7NjarGlbf3;{{+>pNb`S9~z36UE@ODUcHui7O|M!o@PY>w~EjfN-BD-q2xFz zP8CAoiD~ZQ%MQ{?rrYe_L*!xSwd`pdYjuWm35c#pTRbllZNLkDqwB)|_YtBa7o-q) z$eAgI>Zi`hb&8zN=6-KD^T8*>^AtBx#2=9}SIjzjK-QT7dHagHd{z0Xd!j;5zhPOG zwWYO`D)JgGnF`y^#Q6A}V>31X-NaO71xsM1x0zJlGXEZOee$Qg?ZDf9O6GkpH^eh? ze5l0eiTB1nGcr%Bo`3Xe)uTsHV&f3Rz%uQrh@_egrKGac*?f~T?^h&BKB3Sk> zVQ%Cnl{|j6hD}efj7Cfssdo(A#BQIbKMncutFaf>7P*i+Q>#x-6sL;_t;|g47T)_e=dgC= zcR9vNQgOAoY83ioc^zGOaeacD-H!?znM8*Zm6eVxCUGIUUdf4FHp*m4W1>5@PpVpNp6~Vq9+UwMu;Q^T7-ay&n~pb zBdAiw&yP6VIyXYhQA{%1bo;VV$&J_TtY9PfBYSH6M_S)_uhS!D7r0o8?L4ErT}OTm zl3y-BtA8nVbw!2m(yQ@Wg}zbVQy;5qQAP>@n}J{wch%BsY1t%69P)Ok`?i+Tr^YkG z7Uug{zL3q?#Ckx$>lPu*MCy6{$B&37HaMFVLUWQj<(-e@`a|SGK)^_rq(>9;60U*yFiou{{71tm;m ziIC&{MCVF@q#qfIAkHMW3ZUyw0eD}~CxKF*AsVDA2Q)+DsN3sdi^3n9rzMsrcHw?Z zf6A;_tlTPjKudRk3UU7!@JhNtro;6t&lm91x@H?kLl^6xyX!yfnBzt^F)du|$l6LK zb*vtcuv*eIz~1-$Tj}FzeOdHbH7yZQk%@}uajGv$?|t96x;GZvB#(_a$#e9}4G+>R z>0EdJy$2tGzIbrba{TbJNKJL&y1C?sPIYjYkGhj6k=chCp&`H&-dS@Ag1{;E72F|& z$OS$7VxR0&!a>i6ljT%=Lg!bmUP|sG9Nwz+8g*JwM!x-w=TpC-?sK+#!)b^Ob2jgn zi|LU~xQbKpeoFUz0jJRXO&#jNl>b~Lef^OOyiPWIl2J-x_O*y`lVgH>%6&g#R)nyZ zx0aI?I*-mR4op0y!#wSvyv82ot(G(Y`+$HcdT$8x87@{PJH=%YadGSzwMNhK#1yj) zo)c->H3+EhY~l@L5l-~~USqwYoGp0R{%P}96j5=r3cDRX_LZ@#GeTf>7dCA9atPUc zv94^1l{4?C5gLDlZd_4ZPp>5G5sKN z$jR)FkDM;@qy4{q;o%Rav#LN4lDOApbr!musF8s4T6CW%%s4_4L)TGR%`Psna&DBAIAoYegbo6v?f)cQ?S@mS8!fT7^5Ba9T$^VJbbR3W(jGti7s zyuZZUT}p28lqzzncIaN<?Z-6HjOwNh|pg)NJH05WJ8s}o#b`A3(A8A^F(vJIh9`ob*gA1lIob#G_#r;X%jI=kJXO;U0onYQwt?*F zM~&x1mgo*pV8&e$QI1QguL1`+>#iWK_r`)NR%=Oq~j?Hgp#~Cxn9VKRY4l8&>{c? zF=hM{%{e%v3q z(_Q#>rAqzFA8D|FG2r0tjSZIEh0wr;;pOqS!uW4E8=Ab5&U3j>`uk<}ft1%*7VrOi z>soyKt+UN>04pOW9p{Q}t)raMV%Ka$xI4Ltj%EZu(WVgw(1&8klK#=-ERkz{@y<+K2>R8-+QcAy4WCp{PDs5;>^9k z^MSUa3DuA|->-2$P!%T4;&DKQ8#e4d8%5?su>397Sy2#6bvRO`LLD9ucs|!w^r_Pq_=1Dx5bsn^`U=EB_x1_P_GH?dNfd1U{f z;>TiW+5G&EC;f<$+;>+1NFkyHoD>87VR$exb)6 zoS00&g{x7A^A|D4c%<=4+-B_n3G1@SP;gXrj_a4BYQ(#sX-X;xGL_Xc>Ocvb@i8Gi zK{0S*1vyj~Y8zY^{pyj;wB~hl3!A%d`UkkHN=yj2<3OC7x-pF#Y*2yY@Rd)-`nZmP zcJf(FsBZ0mW0W1ShsDAG1QbX|~a(_Zd_W-e$ zAA+~|aVqAp1KsU@Li@+I_192d&!~C{MTj5%l!P}yN069+^9k)=vE@{%5|Q{? zJiL&{5+dNpTcsMDCHiv$>aiCONQSm*pxPb`C!Rv+go!hjE>g zR};oW>luI~;E+B>M8)_`v?D$7j@aXWHq@+s<0?BS(xAZ5X<`&RZ(jZM-9GXV>@dW+ z6ADO`F$|LBW6IO#qyOce1faZH8<&=1bPWDDL!~*Ts_e;$4am7M(JHDD`^tBXX}o-) z^!dnxZ_LmzH($|hi*S*ufkPFM0#9ZrttaOw>?eg-yes}RG2Bw>$z~jrZ-2Uyp{mGa z_-E1!4(1?P7?T|v({`-`Wu_?VtuQK2Vvea5IY(O;_J2+ zgcO*G>^M~`c&k11)7s4Md>Y&xqZaiqc3ZuASP;#6M;EcMu(=4qwNEpC@HFuoG->}J zwaQ3@CmZB_n#$pPa~-z|9UdYeRk~SA+;KlxpG=v01f8h4^k6Ff&F?rwyLnu=^X3^D zD}=0W?d@t0>|3RGX6jSpgwi`KQze2h8qaenrNR zhigP~YV$Eob=3^5@pXF!sz_okVY z4`HKb20otCIoP5I53+P$^XPDvEI5%4DY`{3p5YrK2;3#5F=WSaa;`LYC59j5M856A9)DMx3eopzA8|gRANWEI*cWC7ju5t!MrZ^~GYi(>KcE+hp=a<|^d zaQCwx=a@<}P~;}|OobhS33OT<#e(8H`Z?10K|Jyoa)&G9=N8c~)_xC<)&fd#a|2|D zSOiQ#bK}|EpT6aU7W`jGtZtE?{91{x`tQk08@x?+<4~liyd`^>b}YxNbWbS~mgV^q zYSXfm+Z*r6yhF_0_M5}VQ)*oAEy86#KU)O3CoRkrxuvbtMf!=$mKl*lbB5Fw8=8z&mf+@ICIMvm@!yPe z&ux6ChTs;NWTOsYGv0SF3?Oz<95EYvuwPw-<@2_Xb8J$XMoj4ti?)=er@0ru&#giy z28{?rMMpE_`ODN;WO$)FL49buqHfAATrA8jU zDCFL!{{-^EhXOXl^w%{@m}lzHclrP6RkR`{1lgGD_tY{dqJq1Z*^z2H$qxZs@lW~o za>-6X;oUDMzIh!C;3kl1zkOXu6a?u#@ycbY?FtzwfIQ}i6$w;iHNC8vXPK4juf zPlUq_rdP>8^x9TaebXXTf_P8n_V(%FMM!0`BJM;wqsw<4vdjSVV0>ve?zh4YZy^P3>4JNF|Ew|$V*lX z+&t-*dlB=4p8m+vuU2KsvS84xw@1C(j^G*igud_#1@4uBg?F||4(A=KxkPq<%J@N= zZ54bg=4$1pid;>Va7nFZgDouGnT!kn)cH0WvLMJuXu)f1fqT|aYewq0u`l|qBo6Ws z5hUL8VXwJB!1mz%z^a*gk#`v~Zr!X<z?iC zx4mHU2J5@sO3a(Tp< zD5CXd(7=XiZK@?eVLkIpgL5H7Q-P2^qIUgPk~8U&tCDkk((ht$YXNOXfv!hw$m|`cZBb>w^#~0POTlf0+~E!xjRU z!CoFs^4V*l;pb#Kw)rsW`=<%iKLLl_*W6x4fd`#(-QdDkwry=l2Qsu z`H=X4To9?opvXMUKaEz5C&2kt9pVmC`JBaHC*ueH6 z=BOb8=pHYpSc-uY?>dS#pnu4-)X3U@58p@v3qX$4_-v^0ENLw zrm`v`3EPXs$T8)3P%tRRJI2$!lF*^N_15j1C{ooC9AP=~@fvxPvAOs6K#dF{$L-K~ zPy?A~gnp+qF`DS#k*#xbw>kWzkweyP5&j=mu825ouVJQ-tQzF#SY3GIcJ|zxX z+cy@N+$B~SWD%<<=~fwYSFK)FuhvGrC^bv&JB5oV`E7oMwX3m%~Lb^x4m^S$Y48`<#4I9|}JLw{Xe zj@-~V^D*u!8Is@qLkTbYDgvAtII8xWp`xtR3i7w2J1aA$;<@p49;_;Y`1 z7-PdcSsW>@3&m_&!pSKzp5h;#C{m_ShtVB4vNCgL<0dt(i185QKG+$wErdIg$?;Ob z@2Q{5fODVfycBV(WdF)u_D_nkw-_j3((ZPK`jKN(?*8-$-q2isxR{a!3xF_j+#7Eh zy(#_P`+oo5kKe8w7M8GmQTI&o$ZiE`cy)NHC(B1RQ@QhCWbE>b>%Q#ZvhY9mOgWAP z_n3U}++JS!T5r*<%MUw_5y;80;*bPU%B#Utmx%&@48Pj=?S$M9RStS8O#jYo>>coRVNCS3tv*J}%9pC$lx_lxWTDpo9skgH?6(-T`9 zcarh1sjd@b>+=T^d1>g2>s$Atn>gz4dPC=^mdT*Tj}>K(XI21|Pa&49aaciTd$7Oq z$S?}w2Mj6LJ2gHCZ0;c{Vv2{m;mo)Fcd_c*?)12puh11>7Uy~FgUsC*XMJQh&T{oo zY^}zCy*T(#{lWQ4sJ?To5vC*kwHpWikYeBobnC-x^b;?}GH8&O9${*(;%d5A4F%oBSfV16qWUE^D zv7^uQM||mU(g!u`pSdK*4J|S;f#-YjazEO)D;Xmy?C6baaa>p4yq`p4E_*eh&c}MA za&9o?B=w+6Jiw1D_GcRsz9%$O0e-l$%+rDHPusw3GG%_P*G3x+fabbY`&l#v+$~o+ z;U(Q>>2Wopm(1ajsMiuH0CDw}e=Q;n)D#J&qU6`|)_NU9gyRS47q72Y-`@o)|1Cvt zduamc1aA%f6K&2PUwKkNSpNZj?Nn8oLwhld63&nh=IzJE*eHZqo3G)+uu>k>-SK82_Bl{&8d)2^5GafB3VsG&xEAJmliA~la$5j!2P%S8r8lqr+$|b zbAx&F9xPWp2`}zubN0!Mu*6e$i*F7&NeopnKEpo;3c-+(`xb`~afnKxu(8(r`SRuN z)TVtHJz$m=)ObW*V-u2FGJv&`t<$g~>f&w#o@sL8Tz!<+9&KifOM!ST6z|ANAkhPF zfM`!s%xJ+4K*;#kK8nn~mLUXJHA5;ZH+-zL6~>@KjmsQ$hDVj3%Y2e~+)s@aS^eS>h8^KLoU3e}*b=rNVGf6JD54$Rryg4eK+Qfg;*U1~VM8gBPaC z1&aw*$sUXt#UVPWMP!%;An9_pien|`uD3cw08X_R;2Zsd3XL@zFZTHAXO~ z3|jc6))yE{aA@?eY(6;=IJ<@b7>!iqS$LnF_UV%M+sREdo{F~}(uS0piU?BPyrJW! z&zXN>s}dH>K*7 z#frIjEe|(T%*%!c@9=WV$=yQ9?YF}djoJO4$GvYXKb`!FtlWLfzmxVZfMm=R(eJ~| zvk${uE8R!V>OYwhl-)|k!ms^m#JuL3^geGviqSTyPo{{cB zj~PJ4Ak|9hPKuV=(3h|}9*aYW)4m1I<(*wS7|8MNf)mk07b@=CH{J6~$<8z)Iw#3GVR_U7MGzei3#(+Cv9r(iW8o=RW+Sed6^g+q z2z(~N)8_1WwP4`>&<%eCps%!!I3%9_)%CF_FB0UBn2a^!5UKgfiPEWkSP0IQAcb2FTs5bN z|IsqqeeG&oS5wHnU&`=_mq#wB&QC7uQ^Jm7)o5qAs+mjt!yEfM&M{k&a70(Tu5Y}G za|SV3nG-h0HqBM3@cQ8>C?`X)rM`&1iXOeG57cR>411c;Ii#N%F3ezZTB3-A4n=`; zmq^MJC1Jh*m9M&QLL#%JOfGePxSp4IN|ZdJyGyG6yRTX;N8mJ5k))se8}*Zl`HVRv z1J!e@l6YA_2Y*4#Jp1wu?q^KBgAJ#j(5WjQ}ws+!YRlU+}TSpyQ;hj0ipLqu%I;6Af%}Pi_ZO3Ka%y{ zaJXRKuC>)`(_kNFuIXY&3hgEZ-JuZeY?;Bw4JuZIJG6;a+V0LDj<`CuGQ&&9xkD2o zVcoy4tUy{-6C^iN9X>qkWAZCIxvQ9x5QGb&A$zG89X6w1sq6bPF3?dOz&0*^?vMJ& zOGQBsmkavM$p>8DF(WGZpWEyQlm%QDDslb@#8l-R6j%YuQLn~(n;wxbd1~5B zMhnH7Qa2xcGk2*Ydurqofq!ct(VttHpXW8ZC+UBZe!tfHz_u4*AVGb_JYGh7xRa5` zxMu_Oz(H4sI^5iKXn@&if!(7KkBAjpI_G5C=+O9nfB)a$xiZNa8nKBoPoN51W5ZJ6 z_OU5^b2F|?#zz&Q0&gT&>3xjbTwaE!mS$8KXy=dZvjrTdDy<-vA#1gzVzRuCOZkU( z$>vW3FnzE6%}i@+k~_e%Wz(SuwRq%|@1mwEX-cZhmV&syx3F9%scXNO>0lQ|XEEKl z)C1hVA`;ihDoq&tduF(QoBeWq^@`W_@DI6^qrwSF@3pVQaUm*SeJoGK-i`R*;1@iV zYL`&#?(=H@)0+`HnAMX8^e~gA11C(k^k{SQfxq|sk4Ju|afqFljbXB|&rfK0J!}oJW@0g zX6!i)#Li}vOndA(vU zQ~e4;pr0%JNLs!#!B7sbjv;1=_g1IIrv-gHGK)PDv){=T2j&A4s zC2Bt|!%vsyue=(b5bG>j_-`Qb2Zz|jmO~fJl z)*9wTyV|nGg_YiW)?BI#Kx%#ySX)pvpb#I@4MqkZ2IxPgk{|H66j51R7Ku@WH z5=Cxhv%Vo4-kM7g?{HC+dGLgxYS6ggkgoQ#!xFa9bw@XVzga(rw@Pijy?pi@LbO)e zS`i&;6owgc4BaFS7+(pw7aaH)O6dkxsgb{T1Eevn zW4u0GG360>6wuG*EX+w;Y5D~RK0J6F`3o%o?=Ac>K`aj8bzKxl$?57vCh%hr5}X?= zaj)eANW|QWdK;HE_rv^{UZcDiZB*7uYad&WaNg@QbO)LE08sni(-@4$QR;ZHFjLv! zcP;^d(eA?E$qa|~N@Sy>AbyNj%+JK)F;o`byXn$@2)f+EcNyKVhvFHG`ATf_@* zTaQ8y#_dV+;$=P56E*D~%gP?A;(}4n+RL3B{_}c|*D(ZD1L_y3&f#^loai~{zi0^ zU7|qf&cNd&?yAhIjB)Y_HyG{*CWTdbF3j;%dgXqm2YTVbFl9cWg&tLj$MFaSaYAWh zMcX4k1Fa!w>wJLP(~pXMTdKsyuwQKi>~|3<8jBb$F8G{3p^KY{TUyd(9=NPJTa%ADkmlOF(JmlE!*Ds#O~>+|j7DQxn^jv=#M{ z$Iu238RQ#!NZ`8Ce$3@M=A3g@IU~g{iZ)RZ| z;K`eK_k{>dewcWCx=E=((`OuFvN*V`v{pY}T|2**Jp4fCc>mnx0K`x`R1V0*nDhHk z@~%#P|6_0aH**KcXZ`q6T>5c!btMN-IZC2&a4E^W{6kwaz`29zX34ofw}u3_?3HZ| z$~iii9N$q^5nvAPH8+p}<7eyw)Z4&Ot@_B;o=+0KtQZr^SZTdbyX(8r?;q$j?2MZ@ zR`6CX={LH(q6xBxAp4#hoY`d05aR__N3Jd=rL!iJ&;=v!I|yP|Y^#>DdD9X#MK6ZJ zuhg*`|NLP8MV?#5biJo=ZdDmRA#UQKf40KGo`ir1FbsW0+d#?Viu$Y2LEvLLd@2Zi z;r%`I-(a<{xe%R8WsyaE;Y z+h|Afr`ITW5_kRKzOcJ>_mYId{+q+gWo`t7WCS&#?iZILoL>hjl;qCx|3i}6)kkV` z!pG0+r)y;uv%lwjE$+-Z2K(wMs|K_*<1KoF$yGQz>DR<@ON6l zM(etVWS7wYG~MLAxUhZPCqX}$f>+^EeWBSTOWO)uuzgTF6q1|Y&0V#xR{iC%>Koof zaOi*CzIaE3p(kHt=m5ER7r4Cs&UfB3O#OVgDq3UtNT;8ci5{^%+oee*ckZef$mwV- zZthuRPvT@3jpv__-~W03w8SZZ?oK3iYZ^Q}bIOk3rG7;gX!t@mO;*tCGbe7C|GJBk zW9STeZEEp1g2JPrS#Iwb2kk)+Ng4YChGoPS>7h->j{A~&fiDNxX1uKun+`UP-G?=Z zLA&-wv#HAIfqF=|fe!^d6cec1L6{ED)vQP1hxx)#7Bm@*V2?5CIda75>>&oHaQ0puYf98^vI&5B|Y%dN~ zh{1ut^J?nD1&zG(H?jLe+3s)=CEK^m8Gd;i`+jIv`;Q06{!shyxKHlEV%R4H9KK$Q z$WeS+ZsrJ>Bo5UwIM6opkl=2WXrd6MLjp;9m0XzrLnZ%$clx>>^n<_@4ghde+)oF+ zrwT&2cP@)3C;8}nZR~De2tljBht+sSD3_ONo5?4BfeQxD#RTV<1LUu_{d(cSjg1kU z)jZKhi{m@~aD8dwr*q9YjzA? z-l}b%pXl3zQ&LFN8!t|!XT6WT8#$!T7sk-lO3rt7EGDM22BD=nnc$C*!PNLmD|K2K zo+hkR|16dOuqSE3uXmGP01BT1JWMQWvt!)FB&FDFW8vbET^PPQqGj-mJue7dc$K$G zg%m23DT;P%WgXoJIXVO+TXKO7IIHn{W6p&DL3b}?o6!zaV>~hnhg(g@X-)$W26et* z9ny@aOb{4lTLa~;;@{6ho0d>YGTLrsa~E}QpvNgmM1dUjsSzSmjdW0%&%HX4mS=kz zDfZ5i)L(X8-gNE;3EuHKqi^PBAMjy?#e<6vtA=b?X2hxz**@{iKP%WtMMa7I7_G(c zD64?sCsramm0KA(^y*S6} zdRJSdu>wW8WI@S5#l8t3PDZ6kNH+kb6hLy~jv*xkbt$aN{v-`toZUZsQzaCHoCdrm zuGi-Z!*@h#jHmdY+^wIHUcOYZ3wKpZw zavAZ=+lM`JS?um5LP=|hUnom2Vq<_kqAm0)D5l-|yky_}OrzJl83fY>^kE@cW^_2x zZM!t6ID7<-K&SZIbHhGC>b{%)ej!jiGq6u>J)j{*8mNU`>0K`0r#PQK>iKvRy6AT) zDM%F1WvzgV@p4!1myL)=ao8nvX`k_g@ZLrB^O~Ga$M?dnECK6lD+s+{1Y48gyQ-hF zImFz@x6dDGSO&yT)fG)nZ2O#sBJq&v$KMQLES_|akT-nX9eBRpHUYTK{mH;;2;reO zI~Z|_Ic^6+j3>Ei<(L#^Hl9AhP0WMaL3^b{S$=&d5Gg?fl*vjmhuyMWtgoqI?140B zTEzbz?7?trW!!A7u2uBhMfWIa4RR$<62H1S`SdeS@D|Y&d(}#ZShJn_@;`r_uf;IYi=qAjt*6PE@4ZmiqOuB)4MD#4czpeC!70EMxAAXP7OO7(gxbByDsykuyhR)eW$Ug^;z_C#2D=crfk23*c9Kd-+eUQ45vt|VrGTr4a zAQ;=<#%)c{7eIoLu5YN`W+*H7VlBg9Q|^+}cYjYRDjfM!_vjNv=d;e7D%h8_!(uQA zd=%GKp_S4aijdvsfE)jC$cpQAVC4q>`%FmgMJ?bYO#34;3D7H5(wHbHQ45 zeoi?(;Vgc`iMUHd(Bp9VYO>@LY@!%%0}6ZKvR%kUd2m?no+HZ`x0s zhb#U2)(3C%40ylzYy>Y`JtuG0?2Gh61T1MLlDf)0EJ`ir95O=pt)o(v`Q3lUC5Ly0fpiCOW(a(=-Kk{Ck z6D=MYsYnwODmfSbex3LbiL))@sIuFrxr>e#%#A7Q0v)Jo#}l|bR3@z_IPvtYq+Z-{ zli~05r?0o)qUxbQ&UZT<7PK$U(MalvV%?!UUj4fthI zmF%8Djr|XX)hge!)@^?aw5WDbjAVr!Ylgx-G2ut2XQE9LFIfVJj2&wSLo>3uqE^(1 z9dOOBt@fP_uD8mbX)tm{jAbqC43u~C1s1#2UuIhBGgEN%JTu!gXOCb@im_2W-!r!{j-Ug#TWLVS6Xy<<}cQ`~)$)3fOJN*9eY=KUK^*mug{ zJG8hW{;H}*L(R~J>Ih*&L}reCN6}=f91plUqxHvMX}2ZZum|@lv`Lf{qKrdEBw03V zstMnCNl?Fy{ue*3Pgd}bDOY&&B$FuZc%k#c>YoWUr}i-Y$UJ#hFS6Rm#b;`=N0k2| zG*Ab?4+ygkKPcbjvEpZ%H%|PL2)$ey0DHpXcAvA^Nt`i5#=LaWJ1u~8ol?bi;gqQI z$>HUFbkg(Weae$DI>xd^4{n?I_;8zjp|X3fp%Qw%aPhE>ojSGdX((ltEC+Sa!;K-@ zJpf+oLTIf!Y-~?cZI$YfLP|#1@fjnTy?}NO z^*McxVc(L=`nMk7jaWXbo*yd)2TbVz!JY2kp#2YrM|EBgtHl0Q#H?Yb&ZV0n^-AM2 zPcH>*>_DzhPHNUuag9Auwh!Geb_j!qfkj}gJTahsC-T*a*HL>BSJ?F3A$JG$-`dPre)&0eZsJ?8hpmGY2?}qu z`xT|eQ;s3p@TJE-lbR7+VteWz`Y1h3kIwz-XsJ41mwz=5Q$Y@|H5GlO&C7)f6=^?c zZ&2etU)M%=|aG%EBuTkveDA0-oGiCtZBeuBiSQ6l??7lwmo+5TJyZw zTEhiJXnnHbHMl_PkYoo?SlpkU6-o*Q$j%_wXzcROq+T~&PJsy)>y0qY>V!THsA_6)B z7ms*L=pF02X=c1W0QMYa@7T$S;5#!C1&c}boNHaYW|jLMZt;jX&teBj1S zjn5xXBMlJb7}WPV@^C4?M{|k3ap`-zZZ=6R~*JmSND z^R=P9Of4?W@3)+;kEzi}zpugUGB4GZjbcvF{O`L+CaRN(pU zO~)=BQqM!V9y;Xyt*`f9Y!0<@7TY^-{te}7eP-h-lTC?_9?BqZy)4B?>(C~qkiL~|0F+Z`RpJE74&c=O;1*% z8)A#+4SQ3`cPf2q+Z)E0sBxsLu9{DRZwOKtuvgwyp>5(kIzJM0s=`5sl(DX5z@oj^ zW7bcYQ2J}_H%)z)LjUu@DV9*?;_Ejr_sh!jyw)C8&|M9f z6GYLnAN&LEB{B={l{BwS6R_H>Pt7H_u-a0%qOW~Bl99G-lrmohL4G`P6s*dcVUb&) z;yLR22mQRk923QVLwSws%Wvg(t_RQ6i-;W_u^P*f^0L(y-?MI_AUNNoJ#>`Y-27QM zbGPdJ^C(`YrTdRQ92bzYQ7Kj{7hJ1uwmy#S0VCE5lBHpTa#mTAj^(*FoqqtsG(k(s}E>b8u#EVa_ z0XM`77xOm%9?xdS-vOdZdhYEsb<+H?h&^^pT}5Ije}8;>!m(4s)7I4zdEZZN%sxmw zqdz}#2I?6L=g#eQrH$Q4aNPB=5*6Vd@DbvkIl;}7ET_n`&_hizs3m_we?B>`biG!x zW25Ky^Br-|z*+s{0fi{JZEc>8>tb?ripNZqS`=<$RRG>5C>)!QeV(LgsoA;Tv6 zdXX8LbC%_PuFn;RUim6}p5X3KPGEJBtmNLiC^tmoXI>T_Zrp0V|0{K)Rjm4Oep@wM z&rDxf%s5x<=#jH&XIj?u{%Tq7p$l*lk<D_zlXQFQl0=$5TE#S|0`Z4`tt)v*Ck|DdVD`$ zn|o1ze^LPZZSj2@tR`qbo1UD#!fOT!Gf--GO#wv<-*Juz=)G*>_PUNzjDA!7t(4xb zfK&a>d37tIdmPINRWDl64Z_si3-6^l_4S$lL=Lybz|EO?^*`2gpJB!I5t@g-ecoge zJtj)4jg2)a7tm=*#+LMXu5xNlW8i3p>>-$2z+kg2uE^Yvy89 zt*a4N;gVeU)-c5Ir;b7J;4k*rhu43pzdl3LccgVs0zFBPTiHCRvUQF$ygGL9&7z?R zY3^f@iBfHm!Xe>NO*-fL-3@tTy86BqLZZqVVw;5=qJIfWM&bM5Cg{kDoK)?FL?ySO z-#6LVhiKWz0}qKR>kVfuoFbK-9_|TytWfZAEAV`#XlYnj5@V#~>r38s-*nAYhZWrp z4;PB<+jx2*tbXn}XGC&&c%8SF9c{grHg)4g{?pf+Z}BCGckGL8dc7%Axi|Jq0pH=c z`4#N9qgWODM|+hy^L?hXE*uzc7^7|Orb+>pZwhWGkJ!=j)i-OvI$<9}r2M59!(&+m zuQbk#(014KZMHn)n}1mRylIS-CMz%YF8B*t>YH3=vSR+sENV!E>cV`;jliZ@$H1zK z3%pKi3GXy*xM@_LIEKY+$nX!NW}%JA(tN{pDQ8yK+)myE>QhSNMuJ!Eh11FJpNp_O zNls`UrkFhdn5v{??IUlJRE^=;We!G3TCcA0gPx!gwHnRRh(ka+QSN(7-jZ9(WyS5b#{XQzZx^}!4DkA&s;gzXcf zww`sLXJt4Z=wj}1UBmS|8bG^C?ptEVYZr59IbCFsGrD0ivP8FGlKta4SIzGMxbf(| zr;VUuX&G=Mr%R?HA!Jdd|F`l=ePs)OZH-pn6I-pD6-o;!b*X{e9utmTl9ZuC61ai! z;2?zrv&DgYP&-YrbvL&YV`>LP>P(dRRFQ)Nqe2;OGn&ZI2LjZ&kbVGt;5S%>Nds%8 z{=E@sJ)w%Pwq^Qk{d@T#r!C9KVx6j&lAoOB7AI-(>}&_~Zz{ZryWW?GFG-r`eb6jtl`iXki`|-4$_8%@2yejABbhbH( zPxu2DX!&c-bJjqq!xzUY;yFMP=Ra7@mjkKOy zqI1QkK&v|B&>?%G{^7z0xSC0F_xNcH?o8c_R){g1RL++a)Ok^9)*BV= zo}0}cuH^P1iCm<I>`K_D@`@-jPvjGP^F0f*WQ=ML%p{De+HqGCC3RJLKG>X z#TEt?PS&E5tYs_9h{n|z?tXTW?^^9iG%+2!p1SiB1kdOj7Qj;vHz z!qw#E$O+^gv>s@535`0}fSoL@w#orQ_a+eXi2=SW>3NEA3qw4v@D*!_^6^Opx^~gI z{&?W+L&%wkMRM%pYd7Og4Px8^B+j)wq$Oj0ELt}?6>LJKtESZ*OrBk}?joE1bb=4y z);DMiQDMjnukL+M4x6Rh8ClxWUs$y zcjdHt7-PEcF{5fxH8|*xPpXkFUP*yO$?14^(e+O$i|aMd_bK!RXdEn*Xjh`e84!H= z(cGxc#_{cR`kqZYa}Z+plGRMC_T@4;E2RMorHLf-R`!hh@%PW?LpYSaT*RO{)^VDQua*-R>}0PSv20K?bH0D7 z)7S3l^jVBo3HQnGUum%VGA!$ttwqoEjpamJ^nU2yUvi>mbVT=7C4AR$Rd57tJMRAl z)w$4NVCJ`U0=hJy!fp1ch_HO`v#$|a%@;igKN z&9W@^i4G`Y3Rw98v{ezCOvN($#GPRC3OrXv+)f5PV$lbml%Lw@Kc~GU$S%r;ZYbi{ zS>ql_=aiG2-%{^l(X2sX(2L~q-G1_R&Y+QiDmW_gWaRmX;%erG;Eg!Igp;nGiiTY^A8%uM5RUhg`d$EYe6 z6mdDoRSF0Jg=LR)HwQPyDHAcft_uk8Mx?MwHw2_l5{#RNl*wkZsS+s{9NTuJu{Rz- zB43|9KTuimG7yuYt*A)QM5msW;CT*JgHxWY=bQ>%UA<3y5Y#zUl z-MktJieBtTcy^VHt^#g>g0sbNjO~}p@*_I=Ep?sH^nLKS_B;73u+^qh;mJ&sI0DbQ zs@&`+{;ogLMrT)v6Pw8Zsf!3q#y5#)BY=$GKMK$|^+0C^XjH$#h%q$YI>dZVI-z;F z7qlVhV9|`@qj%UWZ2kmUP$6-zI-_d*u3K7H!w;ziP-PD-h&eB!8w;QK^`^_{y7<{W z_-O`g{*!HE&*AQ2Yu&hviPB<4euyxRmw>bYXZx=R#Wca`F9Q2Ba zx~6Yhkog(Z{<6=RKv05MB^%3jl$PRjnmslUCACuVFGjre##F7O0yw`kH<2gvr zZ4qr7vdHp#d+tz@vxv)K+O^9Tm{-KN-vBdE1^p<*a8%UR=9XKu2EPRC1}G+jJg{0y zOC(3R$>B0al7(E8L6Ni-UJw_uDSxi%k@ z)}b>68=`Ve2f+&BpI>^{FPQ6z&?-DKqMgh zUGw|zBN;wstqh8U`I|He(Ym{)L)+x8Xztm5yW%s`)#}Hjk-M7a4K8*k7qE4*@~0@v zr|M|UI=*@UEo2&R;-vo?0s6qKXl`b5tW^AZcpjNsW-wt*R$m0$O8{Z#`9F=Z9)0X) z+9Xz7B>w%0lmYzUL%6j$ec8}(f+#AOGnNNA}&g7WBeC)IwxsI@h^y) zH_?{JE6lKSxY$Qv8#j?1(EPGD1`X1kYsn})I(OAH-X`&;eKl-zQF7j*-UhV!w zR!oL#Yf;fXP8hqz5|ka44_xA|<0(JD^8;*X{QLR#AL5kLe%2w21gF~#bue&!G64K} zY1}%?)p#!0<~vu~Kg5l!K(%>KBO8BE>SYKO+2=YXi6^CM&sU#-BHFk`Se~2iZinTY z|H-H}sx9ISrmavgm$lUxY_{~N1w>A~+pwxNut&-GR31DiSkRX7+q*336PikQcBEB; zbV)aP8mWZ$so+O-uCrdOIHJSclBzuo?}4NnFNnjTThUJ+6 zWd7nHYPrX7W5G^@1+}wn<}OoF*5q~lDNCKd5o3oK@yi&cRVZgkBoU8Bow^~WcU}nU zagbp&9=!^S>vPxwa{2aQkBG~UzjUi&@WZ599R-O=mwGC09<6y}_nWt8^97}+c_EKN zb;j2#lzY&5$tKKJ?xwr1oj`Ur>d4>fvdzD{-QRU+d;7i*2Zhm(Efo%3X#aU|=p7EZE8J&pRTNwT~$2cN>mxbQwgztp-`LAw? zx29MB$}YG0<)7qQ&L1MnZQL11F6<4>XeP=vFj=V|hVTxCwa5*c_BMzmX_ToItP^ z_~Bbvz}0@ryHyx;=;+g|DErV*<2lCx-J8q(6W>;yzxepgI{X)W#8I1r?&h(r_>=~A zGevC@e49sfPZUT5(N~5PDkl3pEPmUb_J50JeyxIH@u;=e!gWz_q|chd$$JWXemn(Q zc%zH`ZDZKdOsQfP?PS0FS^2w3}u#E+a05EwG>mDCOTSt{o^D5saA3L%}n1&76xFVVn%Az3XAfHg|caX)IowF zEFY_q!V_!H4U?RE6IyIRC_RAx_)lB*NgP$MU(t`U!)%GRm)#Pmw7x$VoDK9yaO9|U zp0iU}>Z<%`t(q=T6%Nh_NaXzwP-gVgDqgR|ZqZ`WC%&|pc6<+I&mb^uoA$&H_nXNq zJCCLO&upe&*Z*|a^rvrH(PUfF*J>z)i{gPm`~Cy&@d+B=c6W*VBwnZB9!wil^%;=n zW>|)6Z0Z~C#_mXa^dAPr%%473T>sH;o{}-Ke&HIA7F7D_uetxmw~#2Mggdd=eZy~d z^H{O8v;OPrf0=u>6$9HL>OL-4=0eX{$#Fp(_scA{0j(f5DwKc~wW-K!7QBsNldRZ$ z<)4Q64Haqf$zYQ%eV0>jv1aPMZGS@T9RCn*^oRC zy6~>!e>?PTqvNqTIN42^OeyqE7E9TZ4HC%hh4XlzcIz8Kqx>1&j?3WxdWoxYXTiCT zmw){4)W=n!=91*qngctd&ZP=6g-kO%P>?hzh&DS7_Gx>@#m@@(4;<=2{qOV#V@|+l zS#vhWHX63LBYC*jTaN&=?S_IHUtg7icHdsBv74E&eD%NceFKF6O<$SwFAakkzr7yf ztv36|yB0pa@9)g9As4-4@2=xNo$_b$w&5eA5Z94^Fjp$&Y)(Gg#e`k!7~jbS4Hvy7 zfgN`HsX3N@#=qFEpockf2cG*U@KdvE6%U+c{MINLE|xfOcs$J0Dj%G=gB*&t-|H*C zC&N>@N!Yoq*=&%t|8%^6eUt)Ud-&GhZ@^RBHHb3?ElvCftu7h6r2Y^t{Wqy`w8jqS zmmmh&_9$a0rR4AplbBaalPjVoXs4*ZXgz)CxBevKocKRkr?8|Z4zAC21v1Lq-v8z{ zgeu_L%zOg;|K2Shp8~-aWu>T=$rC^J&2jPGr>&&t){uC>Q8Q(m=gg~iz|q{Dc6!^y z+VSrJrhY~NKV7F8Z6v(CsAzDb(hx!8NiKg!;$e4jo)5YJvLETw)2bWSy4dDr8)-j% z?RH!sGLWfNY#ttZ?p3L(-xpn{u|^==5?-j<*#Pj5_uUkC%-w9Wczazr5f{bgZ2^Jk z5%^paujLoxl*veP!=LckqNCD5S13VTu!z9KM=;edzvC|a_BXqJ1_iH(6@%f!YH>4r zS1dZ?TIf@^YIL21VG%|EQtbN_Wb@QNi3HBEBas?0gDVxZN9>Vhfk@;23Du(SPoa z9>{T1W(!yKrDH(HpY0@9;HnsB=JKe<&@JluUK{D9(8V`Ju$;QNUdAkFwq7IkQ6>Ot3FmHG=WoY8$^ZV!@RgiW#cCo<{26`f{;^p%nz7EOCwztouY334;=8SOS^6Ph1c^(ee{ryoas~E`XeI~}=fFx2T zey1RKx<>YylE+x(q}qv-p^k02g$|5K;--dZKqQp+Fp>qpf%+8-BpBnm-%n0c3@#H$ypgr?Kw zh&vtMqZ?x24Ep^2q;dWK9;6W%yq66ok{Ww_1_LiB`c>)jeT=hl??v3zJphRvH|thZ z&Iwa@U&{w<_`JNVKL0$|aBH@8&&bSNOYv@}^8@pZaP`EW6Usp$n&RZfMkFf^N&e7B ztNh_U^=)dZ-)te03#xxxa}k`l*<(6cF)A)4zPKV-xHUBQT!N^m%q2ER3oRz{yKJ*F zjt=i-P6rEK5%bZyb@-W*-N196D<(=TsnZ-0-)~x|SS#f#8&yLZ znja`jrz7gT38M5ty>hm22j9;jskuy6Nb}2}Z#4L7WUve*#vWm%3~O5=LPBs=zCnr9 zMrH{uX5)?QLc6854t&~}2wH911>?pu9A=T6ec?p^S>$*rF@L>>mCs|^IP^N~hv4#` zH7l&-ezRNF03$soJA6zO+$U!B%R=uI^v}!*Nd;YP=w^<0>h2{ZjaMA~Z`$hN_>M1f z!{~*V8eT@Z)ha~uj$SwydQtO{YY$zURk zq;Y>+2VRx;)V1pv@#bydiI<@e6#fhino!KxPA$!j2af>kS|9rcp_D04u-=~P9QJCO zLW|$s4eWwZ4ZY}aGA4eiWpPvazX|#XSAXm%RSc%H46`b3k-==9F(9;_!UqUr(;=rn zRL!iuq#$X1Z1xODCnRIEWX95@qSt5bnVE5&Exu?vxTyj`ulYviE@5O*zL6gWV+Nd; zqk~53Vy=&PYk!w|SSvbEY)X~8q6(|MMF(}QPKOlJ9%;<=389f;4OQk<%NCKWi69jQ zDB1o`xz(2wy;H{-Df2pe3PTnyn(n377v0)m?FM|pt3mINwqZ_+aZssHleXsdzH%XS zXjsGiv%;_t%t4{6{ziX!Hdu{$y7Ou=r~mYXv~6~qEyY0 z{tA;BnxRfqhX^f+MI)ieLG>|Z=5i--hgd<QzQ3wF(+Tw1gwyUDoO-P>LG_~hW;l}BPOX}G5b3m4L z-e5TT#eiGenwLslyE+V4AEDcYMzh-1(jQsRpV$-gyzXPsW??^nk(&Ufem==&o)-_R z?YfE4VBBTMWEHzNP(J*&IM*$UsH$N^!}_*WhHV%L_z}-w`d}DiO|o>zrFu=s%9qt4 zN4oCnlvRdTlCrMaNiqfZIi|m^z$W{7bi%O#%C+yjD>cTZ z^?{HS1JQ52Tu>)s2+mugP~FFD&-+cV$3M zCre?qpTDD(#X`)ox|lN#LKb~GW2xOT!$Fq91Y3k2X^m~C{+?8C0u_(Hth4%I*Vr2{ zuPpJ6b}7R@=qoH@5901H-ytKt?vvKZmgX&eMm=*oQZx8-WgL7LOvM%+DyuKn`J}ub ziog;E<}2c1X}hyW2IkAEQ3zBydO;v*k#z6%b>h(>vY_nrL_oV`)y@!MC^nvU7*y%U zrc%}7H9#&wcFlV%2XoZdnBK3)S;F)tkWX>DfXkMA2)#?Rin1Z5Qif@RE|s7nFw%EZ zAyy@YmB4G%|M@fg!g8u&;Kw@NeouN-A?uQG6GM9J5=MD3sjQ`$znO&G$9a6e-e0n` zdp2_2SGaJ*=bE<9+JoZSNTy0FLI4cK2dj-rt>GVS$V6EJ;QIH%#YfKx-WanYpWPaY zKB!db5r5?O#QcBE>FZHl)xC=;p^CBU6Qk&H15bL40N;yoB=&gkiQf+c3ZDsrEigj| zdSYIEih)D`OKv#$YC{I$>evr)a?wjzUr2$o9=OACjTU;;2V)z&(UMtSxExv^F1f(t zJaw;3+-Z&rdNsMrhR|GTL^X_9#Rpw$I3g+OJ8>p`W_LEqvz8yfa-}80OQ-jkv@pj? zD81$xo@h43LKzBdu@?95awePEQv{u-Qf%ab2de-xN_>420X*0tcUEzLvL4F|fYmHg zZShN_VKp`RIOcrquHoQqiie1rv@OxUQd1hV1`%rqbdEen6);FI zw9L$P`&f4zW_ZSDX;?HH^+u4&_4L;pt_qWXW}t@}Bw&mL5L;eY}ptikcuM@Qg&EPdO(>t}ev_BY89-uxUc9lo0b5)0Mx`qg(O z$*q7_;|Jv+b8;Z>y-Y2eXM5*ied+%iW(2rd!gBQ5^Vr_WT6CWXH?wQMBvbt^5Wvhj zwymVRV2D>GXl?N1n7svJOf|AyTXq!Xe%1~jma~J+ujh;6i92B-z{O2B(FUY+nZrqo zS262nL>Z?j4Zx1kxFFd#H`8rR6y^m5Iq#eIZ6bt>_K_NNC4OBTJ|Q!_RQ9X&kP7__ zXUVFayEeBArM!B#lL}Cxt?4n{t5T`=pmb1>ex(J6VMN;ErOjrWG*zV(-!>Fe!+ z?peaO+~{f!V8AQxrQ9wIs?lG=uCG<>Q}-Ba3{{Sm4RG9TB2v&u;Ec@^oCG<$vJ9so zUdV937MGPYxv4c>^dLznQrR;vx?3qWXt~!obA4PpA45rf?a5h^Md4#bnDTbbhu??t zz6|%W#%e?wPR!$_MKro0w^E+iKoXcGK;Jd9?yWzQmmXy5lmgRxOT($DJbj&cP^c-> zVT|>BpYOQcToMoABp5R0~l?`96f{_xy6@98^%Vn0vS;I~6vQXGV;*^Pu!^T2XicV>? z1xp23WaG?`@4sK3-k#`ba9o0It2Q=<$h1KZnJj|W@vLCh4qyD+vUT+crA_mA9rOGl zE(%Z=$@tFA8&tW7?_Y-rq3e>~s*B@ovhbDX#~IvrMhJ$tTdTF9u)9*1ZsCOV3 z(1q2C-AJDzS+?p<&Jud(c8Kj0SA0MIE@FP`@Ypw>r|X6D23v@YS&o0slE9=7zKo8_ zojbLc=vXc-&|jC!s67>miq~Y@9Et^`0`Iql9Mb#={c+GM_jxLg`93;Od;eGxu+#%- z2S4~;Yp*sgGGW6AboGE3!$9J_-WX3^Dtb5`;gv|p$+R@u zPGOQ?lV03k1HJNWx!Az!ugZ7Z%!g4uxdx7KDphZGn#3=%f%I?rR8Wx_-dzn+^QZQr z%LiO$35 zYr+IGnG3gpy&(#*=OkYn8onDWJdHGWx#A|G2b z5I_qzVYHAjwJo|winmBlMI=^XHWokGif&p8MW_l{^asmi`4U>b1)sPV?&QEYqg4(p zI^!TDc4i|c^Rma>Y=XB+6jH?ZM!~I^1KB=X{hA;$2426e%l?0 z>YV5#xB3WTmi1Wwq-XJq{d*HTQ2Rj77N?=|k>RVz=9+$emW%d&9;R(owbUC*IhOEt zi0j^dNX*KtJB*OLRH#X>21ptz3(8T+Mmaf&bC=?GW-h$?HN{aq5;V{!T?fT1qveFx zY+zQ=$J{opnDTBq9!)JXqkK+(NMtxiOM#fHZZ{N*?{(2R>NyT3XliDtjp&s-(xj(( zV{g*FF~*rIxYQ5Equ7}F(vL-8 zy%lCZ;KYji`g8HtTe@YxR&g}t;Kpm7 zt;ksoA;JETmjuxih>w(j8@8(2$!!9uOo1r7B6%rLlb!}R+x|5^!$4xg$n0`MK3?7T z6YFf)#@4SR@3R}yMDc>6nd1)!1ypLOYsZjZCq%tK;t z=~LxWr8puG!J(vX9PQl@Z>_X<#mTXARjM{rYqP;D&Ke9uZY(qs>_v&*s`O>SyA0gj_5txtB|zC{z=ZRYmohUM z=A@7P45&RNT7pHb4z@MZ_D;|;jI4NMs_ zLOzdIpDE$`uMw9*Oyc=FfyrGqfx677I4CRK9|IJTFC)iFh(3gC@gQv03y}{J;xy)$ zjx)OIb`@l$m0R!LnrMw0KoGhg-iJJk5c_M@Owzv{<)LYL|5|J%K(oH6HSKskTd+v2 zlejqo!XoBQ=oE+y#Z455#J;4LM4xJIG4}yP59v|;)V@d}?dyGSu`UON1>N4WcAHhd ziaLt>F8atSwQM*}t^~GOml^D@eKe`weBz1sJgGs=1euNL4q(DF3@)FrU+Zts*@RGE zB!d&Ynz+iuud%;%82z1a)i~HXXp*Nk#M6-`9k zJ@g>jSAJ}5ruP*N%sU1m zqcae3?-!q?ts>etGTbm$w@~5pQ+*o&6}E0Z7uKmuX#;!uO{Q{1u+uLZKE5-~^|b9NDb~1SFta zVuBeE!tm#c&x-h(1W+Boao zjTPqG`$WI88xvje@o7VD345s5E$oJFxhr>sIFa=1b^geJ`Zbfc%d#cyn`V}g=8ax_gt>9Xii3*@PfINN{{0Vts z9zM0w?thnqvU9Z)?d*($uXkoWDJum+fNJOMQ~Yma;aD#NWck3UF60xQ}KWdhZPv~YGk%}KuJEXVaNhfT1`!|-SRx3WT6 zl>xG`YA}eBue1B9rDPf|_fva442;iQzeJK<&ZIx-k5k#?0$`C$4A&IrD7EJBvSRa8 zj5DRFeU$YQ{8{~EBa5V&d7jI=*Eo&f>}6*zd}l4N%Hd1K8( z?t~|0-qdgia^fmm+thuhg9mdQH-Pj;Chz>Hwrs6O(*DHm^H}2q7j#dE9s_lV8^COA z@j_xz+D?qpWIw&mtW019x4-dT#f_UK3as$r0dQNvQtfJtW$PVH`XqM9lRdzhS)*gi zwS*WTw)I~jmGi++6*hUPD3fuT1b;dJsJyl^gf${pEI$RGsNM;Qz1L2(SEUDlTaBr< z+f>Try6$^MZU>>u{Y81m$zcLUO}A{V)1>>@5Z|yl&39+Kgn5$q)CX)p3`s*y!xML? zuPs|oAZhY8h1-I>xYCJDnb2IKmWjX6;)2@qv_YSW>Vo7i@WBb4Yf@ZLwk@J9#5YTn zP!%A1JE2bW(gbXtGbfq0FIS4#|7KfSph#vjNn4a@M>U+5gF#;>?#!Fu3U_KB^Q3Px z0{KTD=U-XMJoCEK<4w+``T-aOJD#X?yr+~7DRKBBPYB< zrFxLt%toy|#*)dJ^ZY=?VbGZus55%zS+>R-0pF)+XJ0xs^EzTi{T;T%*h6SX) znSd=Za+2q#W^+sCF0k2kg@-C$C_qFGobXWRI07ur?Ipd9=a#L*U|oX+TXvv@IO(H$ zNmdrQGzHkMIaAO*wUu*fPfl{v#Owtd^ft1lU%1rhP@V=Wg?dumoGx)eeEEHxWD{cr z$M%^>unRH(%4M)$_Sg_RmV1G9;_w7&H9dJ}#RRZy&mC5}|BY>Q9dv973CO}ZSc0UB z0aW`(Gv|*<@E!8(atTw@v%d8x4C?Zx+F~72{4ZwZ4Lekbrh>k9va*6$D(Vu|mczVl z#t0{QrQ#q2RnAmn-KREY*1$Hr=NYKBxrLJ&oRf@lmh(jglgO1YV>JI@Yih%aSR`=z>9ITBsmE4qC`g8qEfQm=XyY72T z&Tt2e+|-9^zcq6%YvPF98Zs1b8E_69 zT^|z4YA#ZaJ+IkvPQg(2t+M6oK0iBW+oVA{uXv|4on_QuO-vDmH ze)TIhrBv`Sz*@UJ40@i#vnkcsz6=xSn4y69kUgj6@pf$Q)W#@SNpYz1Lep671x#v^ zu)7pEZj0r)gxCp1)UAW1IB7m9Sm26>I%gSpsJf45$J}#VWMjM3L!Qg*XP6qwZt&@i z=2^7CiM*waw(2b{QElk!EaxnfYg8ShE_VpGS zeCU_SOEZ~FUy=!qRxq#!__vyENc1n#h6dWF40Q4pqBxa6IN~O0U zmnwm$%C3!c-x zk0SuvF9y}Fzt^D1+RV2h3u6wiamZRg5GoO!sMOMuVWMC(xiXqsaduClA$=`@OB(1 zIVb|wqHAZ}Wa`I|%1Q3<3%SAzO!ws*x*D3YZrlu38rhssFen(S14zHObuZx3pb)p$ z)9&0)ree4B)^)g`%5f6b%Gs@prGQzf-#We!5v-PhAmrv-wmwM>cGC;>uXV-+yy+8% zYN1jxe585Pjaqi!%siZl0QdC};M!6GZ+SoOd{JB3~)seu?L$m?gB9 zTc=OIyDUnb7;HTBECOif!#|k5)zb1t_)Eq%>D`g;R9)LpcklDXpu6y;3I$3#w%eUT z2{uRCgU7)k(b~jxMt#N7R~U7kQj!{ssCBRr+=b@@=q;@AVG(d(w<14Z+o$3!!lc#? z$n5Nd6p7|niF5P3sxbLMbK@0u2uEuU(Ie3={mun_`dnBNu3}4eVFYF+Wjf*yI;5-qZbvE0h>S<#^n?LwEJop6BO)UX7(=r5g8fGNj6_1ZVFp%0LmvPS9KHtlTJT z#tJdW5;Kx{-W&S;2w<3OLZg`nZA7szl{%IqO%Btz+krW}eU^ERaBOb<3>tU~Gh~SY zX~_CQ2Jy1^uU^NijOltG8*$6jHTe<|x1;;o(cpPYx@op;34&M|Kntagtn67EbUZ<4 z94QIMrB`YbISbO7vhM5$cJOtF!x;HE2t>rJKqw0AC&IhMS^x9RcklZ==RN2B{eJIz&U2n~p69*o>U?me%32iwz{*4Rb|sd`TxyTsbME_b(B0mk7l?Ldjxi zAD6_OC%l?9{ya@+V1t?|!iP!lcD#!XRsDqWx}eZn2*gM`xstpelF$aRYq^--BO*N& zQ9GBZUj-m#iBQNV)$w4D76=vdxH1kX<`_5365hi{1+fxBnwV59GR_rI>V>3IA%P)OPhSpg6;OXH*;X#mhxqgnUg$TT>&FFB0~b~> z2VPFPJ`?La5M6jL)X5US?=J0n%BPRiaif z*H2*7P}Z75rtC(e#er|y2X(qyYc3LX#wx>{?9_3e-ami*fKH7h^}NZ;ON&2u+}B9! z^QVIN7%x{RbCY7@V~W3oK6i+0>mJ$1$-I08Z@cQ_-al~gq8F|MgM%ATAqK1ZM(2)} zMPh>Ptl9Q4zqs!0KNbs*_;YV&`}v57lV_pU_-;gY`lZ-`h zP;BVmk$T&UKszKne6Q_|!^F;GtTW5sGLMuR9AwX~KOV4kE;OJ*?L}wBpK1p|G2H@J z+~Bage@zATUBPiXE!ic4(N?zzBjE?M%5HFf-%0ntThRWK9_I&_5*PtPkGO$pc{hbS z5`B`xL2zxNzb70%!S84ALx5)>T1vphp`9jk@(BFX;+Y>lhkpd+PD0~ z+laMR=44%04_jSE-HIdkUHThQ-+nx44M{M1>03F9-DjXZh5yd=UzxqJ_f<&$h-dZ< zvM~~W6A!<*BN17>ih7l+nfUfst$sWu*3Dz}*{prqPE-xw!Pm7>yd1q<%+TAEOn0Sx z^js;)G0!x74Wqi&%qWfNu@x~9d~WOPZC%()<*>{>(ssK+iP79(7LIO6$GS_H+sSTb z=_u;6l4!Yg%paI$-(Q`GYiVNYT*_h)e0+a!Z$$XqU5sZv|3+)@@zvXHGV5oAh2aNQ zg2Hv6KmO!jku=9etaZK5iu6PJ)1pF>6E)nvc+(DF#pT_oS7{xOsoI36#BP9r43`T` zJYVB^p6`m-zxDO?D}Tkbe^uUx#zF>GyPNLXS@{~Ei>$q`F7SypKTUNd20`zx=l*31g- z(4Efs+#L8X^9U9)$vRRyVM-SoeG{sep-!+0>*?NUdBY^`etBYVENeK8>3?POfU>e032(Ye zlc+b<>~-1#K2S7#O*bv z!op}Jo=0})gKb4?9IMXQFp!9|M0GMb$%BeWto+YWB?GE`SLLbAL@Hu)LNmq^PDh8X z9tNB48+mDw{L%w0Y2TGoEty-5LDXMo;Dn}fB4arJzAafRQv0M19-9hYVb%5qji%X> zHM(q^_b?NcVdMlq+L?|XE%lX%Uqy`k!>Tr_a8f=4tg+n;HXbl~;FcYVL@?H^y{d*c zHTi`_)RkrXpFqIdFdwz)*7iwp!cK3)_6Lt^mzGIX)(u@48{xWAQ}wkW(WjpZz9N+U zKpLzF74dm}P)_==DGm&dU~XCykm~?!cx_~x z;6((MSm#pO)PP6VE~Ua@qFZ_!tqb0CKlCTy)&~e|W*EOyu>yi=Q+0 z*Tc8_tASB*3mQPLR!v%_`cstQly*(@r$i4RoDc25fANpb_dD8x!fhDaOj*ut^kx*7 zyW@UYt?TKy=acdraBVn&74N>6qJg#+tl&ZtvJHCmbNbo&{*(_7`%VM$?Opp;2kLo+ zc!&O!ZSXhCDSo!wG=U1pGrvC#9DkY^u)A@ajk!j-A@pTUP1NX?1~pFyvyeqVX9V`@G}N_4JiKKD3$l*qRMhVsDi49yI_aN(KTpE-?mC*F4t(>r+}JT}bj zquQ;K5(ATu>}LF7ueKktIs`D7o83|Kx^{+qDb*2W!Ktb|Hw9gqc>UtKuB{ePtN2L# zDvPQAz6~>n#?KW68V??-ibT->Bk7w=S=C>cpk23o&}So83)kcW4Nn>N|T7B%jB;|cF>ybFQfV=^+NWHzqr3kKbdj@TS4neT0Vhs_waU( z{v7b71Vbq!51|9cuw36fb<4uemyTL{U~P3McVx05`KFz@c)0Bv*$c7z>9f3S`!%O4 zW-G28&a_QiWnB&rG7Hgut{YSS*y_sNx7hb-&K(EErCB+J%|`luUmvmh)s7E!qy+Ay zWIdHkPMkH-#FEQ=YXzR8?k8^_R7Ifdl`ew&Xb0WOQoob;cDj+G#_a+y#$o0r{;jlk zL^%!R>}3n!Rk66IMzyD2`8IEBdDPz7R*ggY80s1sG;T>0b_bwKGWPVyB}#CUur`z5 zFWoyH)eAB#52)(y-{|;K`s{l$B#M&UUGL83q5PXY^ZcPQADIkcwXl-E+O<{JWrJFb2X}4EVlpv0F{Qiir&A7`p?Kn{`oFB>p=hj z81AgBtgE4{%;o9j;o$tr9suyq4oZ|&?^0lXw^*bJd%(WSwUUr8V@i7)F=@o76rn3n zw`1kC&H2b#UGF6u@l4oT`4~e|CPz5?OJc=C((b7qjUO=0D`Eb+$fFOd9JotdTFU_- z-_$;HN*w6a?B<|HE|z9_Te*^!Rw>;<#UWKpM8g^Xp%Mwk2QzJ&-uQ%k0M=WHadFX) zigndRNk%$8c6)m)he%GbPr+yVFBDSH%X`6(@NwS#_Mwid!>^zkZ1)+p;N`Ey1hQC; z9wD@+UJo#Nag=2@O1VzSxGpBOi@qE)PHlSb3g#EIA*Wa2D=r9o@T%K+Mx#0*|2RB8 zWcE~RO%H1byw?YE8RKdRK9tA*GB1;o*uEbDQ8i5<3RS5whUshX@c_lM+qD8@W5WcauFCEHgT zh17r)G?1D)w3^<&^0Kw1vc-ciU3Ym#w4LVV>HVcJ3IKp)YN#k0_yhOg6t4}Ynr=qy zKKAoKxWXODXw^~Rw|RM1bkUJ!_c4GUDfUgoWj3YUj8PE*X)&cUYQv`&-@g@`bbu&c zj@@U`WeBx!((o^(V&+mnDWDR-opv1kL)k5aeo9JOY<8bq(nN;Gx1^!$pO@LSgoAk` z;qpyvBv-jLoh+VErKy(8)#z4438xk$M0Bg#0GJ zI~q=$$HI5Ss#+Y24^mZC)xpKZ#WJSnV|Vbw+OE9Q<8U$#-`Q_Qu5N~gQ&BZFH9`09 z-+$8JHs8=xS9cPbO7l4iFg`xs%)r1fgdH6v#3v^w-`!2l>j|0{Q{Ot$#vchnPOSU3 zT3cH)cC)eI7Y$zP1NIb5!Y>2yhs!KDnodJNEPfvbgNZmeI?B60Vi|sVaOwdHeTZFX z@?Ny-3mcUuTtWU4458uTt?pPD0GiZaWrYbd-CeeH==NJe#EYV&B_+@9q`nF6dH6$X z=+nWIoaga#k}u$Zw9*{1RAEn`uNrCw`m_kNXKM}exK;(7b(59VXP?wjc{&>CDn3c)2;^smdf-d*9S^Wt$ zc=A*(%-rkzwjHvaqA+=|Jqjn!L*cRD;t08^#J$-*>vjUJCfSHpYYBSI{5BbygxzzYc1}@bqh(6s)d@B-3Y1PP_Q-AaTkkd`iYsTc4D1f| zqn`7e-67}e{Q*tXR9T`qut9fScvF{G`sDGJ2?tLMhJDo+++t(T)C>J?mEAg|8S1UK zIzX?WsEDal{?T~g8G}DoB*iUw*p6iTH~I6bbL%ycT`iYo5KhMvNv@9Yu&49$VV!d9 zWBtW@c~AsK>P=l$)uB*7VtqkWUY?NVwbD;4?Nb&nxf~(;*x=#jz8AbAT>VrlnA1JbO89qvAtBiZ_dnSI$Ly$`L_g^`zyq*RKcBccIZ+Gl z+>K!tf+@%g+m5W%aU6ZGtaXt1qOc$e4CKE5(GD2PEVklXf++l)NM~`V!|ko>r(49Px5y3?{>|qGP(oK+wnN@d%A6lBJiQy+*cA9~U0wV4@>JUlB=+iz#7A#J z1*7y((hHpDBBIuRhl%4V8cVtaZZr^bXF3ZT9LEu>I>Ancz|iyLXZ zK_$sBVjyjI&fhAlq6NdX0aVYpy{3P9j_j8mU;k>xRbD_aLvjebg5w%Qi5svp7xcP- zXx-=LuK7z>fA%OV=-@f$!sH9T|4$3p`X>_o4f)19I90Bn*GU+qyeZ4KFvk!$lEM!X z*ZR38C+1&VNe&56>7o~kuIebuV7YTOF)L=I zkEfm}#HUX5#ERwAxjm+TbQ^F#U_+Jn)!GT1o3=DMFUzze8{_a)Y%F{PdQQctpNt~^ zrHxzNyGw3jXy2FnVeT+#D((c&_cV?1{dU`M4GA)vgDyUKYQ;ms*D_P3)n|o!;YcuA^J5o3hDa*wM>; z#-7dH8N9rQI9=?vlajmf`WZQ6CV9u4pv_H#17}9UCKkSV_M}J z2wS`Sp=lOB3peo}kN#OMGX}~eL(N^mp1^@kmQ7?PC9yPpy(`s^`$wqv!M!0oaYsUYW0c*FYUlrjCyx^{JQZ zQC6=<`C1>>T+f_`r_q>IR~Km{E~QnuD9nJQ4rr?@I=yoi1cyZ(rW-818t?S_?(ad0 zyM7mFnjb#j^HHg4=%p^3C!$buOeh0HUO{K#qED8n#0k^Pg?^KL!d`WP)zf#9H zX9?$-60FC~$G@3Z1Oa}urvdZtw(p26otw{vv>lM1vQa-5wIRUL>Nea2d@uAWL5IQ% zPj|GUl<1y@xIS7j*vGBMnO?6RW*zijrp)#s4DlWrfMU(%`i}4{z`$sf)J!CRhB@J0 zn1AXKXFGr9jICE!9j@9xf0tYMLXszeA7q9!vG%i8VB=)QBUN zIgK0d^P0qxXls1v8DI77XNMcVUkpZTgn(v|b7?01Zr1`-$A_rDvK`*{w6~o*$8G;# z&iq)cC1Ni8eHP$v4wW{aT@+U;LLMC-Oe~5vr;M%3MKQl>{!yAUP%i8~ihTxn?-KOW z(zh}fHIHPrdc0GKKYLpmYRw)uYp4SrV8Z09=8&U&$BNNpe0u~=9Gr9ShIKjh`41tG zt|;En-Vf;~wOXxln+cfhR;ZYVf;@yJWE6$~DQlSRFO<@{CfG_dp)>*MlaWO*No znxh8h+8NLuq}Pmu;FHI!6`EyF=_Vz9s{@R_^6?{P#8r)JZ-^5ZRt0((qVvAUD+YPe zDI|s9zADjqx?H&2a_O(LAXbNjh8XMDV@hswfqCZ6;P1#zatZtyom81!m>3kzbvAhE zmx_MGmzNC=TO39q##Np6PGSKp6zYdG3;YaFjWvmuyGIssbXi$z&gHBg?j zIe$4-$W`mfNN&erOWCOiDyJT8p+KxPkz`A?P}GTFrBFYhgaVxRZpx_i1N7{kX>I^u z+qJTupMS)4F^RZY3!8(tkFKEZh!!s+KVTMh%*Q7opf2+)R7f^0!njn9xN5)fV|_xJ z{930j@w9#PMfo|GV1QZ z%^FI5svQ6m%I8T!4S|E5H=zK{(eT$+=N9}fXF078u5$ghwE%{39D!bAjU2kmX4saN z%oFns)KL1 z2iNY?9#&9ezuL5Wwh(y`w&uw`3w?acpxEaG)G2bUbuEU1BID@}TM*nEj0M919)F#P zZu+#)j5<)x)8}*(?X|0ew!`D44P3sk+$3vN-jR`9&dfase$r{_+O4Dd_?i_nM8NC7$}+{L8nJ7jssdX z{w@vmL5zlQHlJgH;Ik@8iyg^fjar(rMyQW@ih!S;-5O~z?PxvX#x3mD#;Myy`5(Ch zMHU6(rQU0Gb2fZGTIPuN6Y0E(>JV>a6}5F@ptqe8Te~r%!JVVYwFcq1b8?rH!+L1g z4|u_$Q)WY!mGc8Y4dyq5cx{n;CF`w_GXeGvRnGAzvPL2%y{ze(k8G8zt8 zq2X&0bjnG@zI-htiMh24anIDcjbuHMNs*&}-yHr7jz8PPz(dW`$4KtqN%+Xc#ZQg% zyG6@sw9z*;3-h3BZ#U{55$DK$by|Od7Gc8~{JHpsz@?cw#U-F0X$rqOLFcb?aGpC& zjGg!`n)*)CmZWLLXmOlND|Xoxa|P-4yX=`hQ@1K%h4jCY0F?mf-YoDF!KsBjkssLBc$GKnkxQvxXAY*@N zjvlV`gWM#OT9OkjLyg*k+M-Zq9AyV_T|`0O%Yz!V;*$%9zIiGjP^L3~cM8+1ytENY%4Cs)q}sx=p`#q^ zc>a_f-;wMq>`fFz{rU6fS{vY}8OO_{+TNINOSg6+ZnG`xu(3x``tyqft)$+nI%7u# zyv28IeSJ8YMMv0+geb-jMO32Kr4{e$Fm?4?J_b{(Tp*D#udAn5qx^3^Xtu_F5L(BR z7!u9wRSVYUylb;9yrl{DgR&kTg_{_yxlUWrH~wSJ8ZJEjAijUUzL}xnY2`n{ob{zQ zg{bCeB@hT9iB4~%meP1q)o4q15-Gk^|D-@HsOrT8PIe_6q0cYzNzm{0`asIy#z@Yk znlxe^wJYWJ9mTJ#(%MMn?@FQT=xWKmWe3`b6ooN!L1`>^3TB?69urqm!Q@&8erd!vOpM)9OUrJrF$qpe|{Rb?)5VgB8{*o zUpidw+Ki5gVOrnV82JdX^#edEDppc{cFPmnSK#4^~ncX}#fU5>RzAa*Khhe$80YaMQM z39ar#aRu2{1a9?`9IFxENwiHe<-p2MJa|pj9kGC~A?{%CD=IGD`ts&@qwU@qi%VE) zzU)KFUE0=#l$l6AY3e{rp#Y{dqsd!Cp$yA^8!amu??NtT%81+$wjIxYfe&~-Cbog? zeCd=P+ad4KlK$5;`htf^(Bx1~fY__Hrlyy_>BAh?r-ygmWc+Gf6wdjIh^RI4-=wy9 z-SEm;%bVVr4r(Y+*9LF&4{uecVYIimH;*K`KcmY7_&TxOKRAfAV|v?TvwWflIaTO} z)-L1(!oGZ=si}N7&P6!onpyFE6w~S%GGKB0@@BNu_)7q4CBEN4IZ5LXy2BZPum9x2p zjOSoO(xMU`W=w}lRkrjyzmhk5o}|)&2AgFsc1e{^GssU++C-3azY9v&Tr_`~n-52mL3RHi6`bR^hBqZuZyjhY=L|IH+2wA`qq6Y(ekKV@@&8DQK z9SHFA|I}DLf3#4s{?-XCY<0H(D`gi?afxU8cEFCQ9~?g_2d+{{R$Wf-e97 diff --git a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png index 4bed6f3fa9928528fa6e0046af8bc011861b76f3..3bd2b7ede030927dcf652ee5db6131bc94df7c69 100644 GIT binary patch delta 433 zcmV;i0Z#tX2fzc68Gi!+001a04^sdD0J>02R7C&)000000RaI300960|NsC0`T6649sARW5($XWmZ0HjGoK~xyi zZO%mk!cYuF(SITpoYdW2acBSAB27EwjdIVQeCE&CIgFdL)Od-Bs%9P@TLgTUpGIqT z9}!?VVHoVPel?X@07?(iP01y|D!w9tlPcIK?=Bz)G(go3a15^0l3^qS z+QkM7JFg!e0cc?BVWcRf3_Wlt=Rl9sJ2r=~27trK^(q|;j}3l(#;;)9f}b(H?1d>! b*kS(wqNfsT9EuRY00000NkvXXu0mjf_j}yV delta 967 zcmV;&133J^1JVbO8Gi-<001BJ|6u?C0fcEoLr_UWLm+T+Z)Rz1WdHzpoPCi!NW(xF zhTo=2MJgTaAmWgrI$01Eanx2QLWNK(wCZ4T=^r#{NK#xJ1=oUuKZ{id7iV1^Tm?b! z2gKFINzp}0{4Oc9i1Ci&9^U)jm%Hx(p;={`)iVKTx@~4s34bw{Ull{I5D-F`W<+L| zF)vAJ_>Ql81o(az=UM)Be~tmQU@;&d63;Qiw23!}XEtqv^FDEum1ULqoOs-%3lcwa zUGeyhbIE0aXGYCzCP^G67E4{MbTKQN8u2u7Ox1MC7jhn}oVPe@)jDh6lfN)r&{vkZ zPHO}yEMf@~M1Lr#qk<}I#A(+_v5=wjgpYsN^-JVZ$W;L&#{z25AiI9>Klt5St2j03 zC500}?~CJni~^xupw)1k?_D?aC4=ki2wis2}wjjRCocUlh11tK@`WoGy9{dra_X{Lam@_E@_D%cnOLuJ=PvA zIf{7l*rWafDn0aWDHHqU&M1E|H;@*PI0A zCM~`BxfvjzH1PQAAr687EBSPs6Jvk0B`TjJU~sJ6PGJ6Wx8gJbw`d+uzO-PHoM$;P z_!qYJB|4`ZsU$FWr8}@_%@AZn8aN_3dbs@nNb1PrVE#aExUC+aQ{yrW`T^I*DF`?Y zkAFyWo!TDz6YzS^NAsAG1OtVX1-BoNKF4tXI>*EXhWx0KBrtQ4K~l9>yB$1u*9HVf z>8eGZ-~%1#rk>wbgJpR1M&Rj(0Li3)GzD59KiCYpQ47mA&iASc0m`1e?S5}CEvMO} zz@|e(WVR_2%Z`n)L|8q_(E#ObWzWcst3V43OLsUn_rswT#u+lh-Rn`UR^O|f7@$0@ pynWVXa=Wj8zf2KCc^m(@egUUy%9+Dj8;bw{002ovPDHLkV1fvh%1i(N diff --git a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png index 22893b8ea78c8c2d4dc1835a5e19b3c056e11d28..88f2eee49a9a8162eeedf5df764e8842ac2d0912 100644 GIT binary patch literal 6198 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 literal 10828 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 WX8Oy%I)Y}20+i&`WNY5Q!u}tVQGC(> diff --git a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png index 583a485712bcc8691458c7abb38b37906ae6649a..18151e82b11bfd52df745f84cdc3fb0fddc45f30 100644 GIT binary patch delta 863 zcmV-l1EBnq4C@Av8Gi!+002a!ipBr{0b)>0R7C&)000002L}fN0s;U400000_xJbf z>+Amh{{R2~;o;%_{{HXp@B91vM@L5j0s;a80ssI21Ox;C00960|NHy<|NsB7v9Y3} zqR!6F{QUg?|NpPAuk`fv|NsC0|Nj7g`v8*j0J7%*cm4o+{eJ*``~ZUb0I%o(wC3;k z|KRZd0EPJghxh=D_5g|Y0G9CpneYIh?f|9i0I2H#s_Fo(=>WIo0GsfDz5d|!`~Z&h z5Tx(`p6>vo?Etyt0KDVB-2d%LqNkiOPc@y0hvieK~y-)t&w$e!axwkX_{cgp)P18DOLqaixhV$ zQg?U#{=K~r?#T4dzM0J2?fXq`_pRceG^bJ9Wm0RV`?hV&{-;K4JwQWhxkxcd5u!gO z-OH`)qJL{?bN@3VRc0}u7ccp@)wT5vAY$>a-f#5av!nQ2i>~YtBGc=XMF5A7r@_#6 zpE!7y-e3^Gz(HsSfQYv4Zo666BdVNXU`PV4oX$WndSS#~dh^WxO6C{=+O%@Q*pn(sRX_lJe*o_FD_}7s7D+MInZcmu_DZH059rckLd@)M? z67S`Oj#~7>j(W9JYCgbTLecyn06yvi62p55*2p%MaKe2MqJ55QN&q;W2=Ir5xDmvf zj#xneJ0rc3I4Jp!6AW(6rEEwPn6hTX6;1pPld0eZMyGreZ30lPB ptY+h|Oh%K1Q4q1rkW36KI=}E|c%z@Oh>`#R002ovPDHLkV1jA>&Itej delta 1549 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 diff --git a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png index f98ccf1f3eca7f764f5a066ae22f6b88fcf6653f..d2bd35cd1da49a6401a7cac00f58241c4cccfe1b 100644 GIT binary patch delta 1577 zcmV+^2G;qb7OxDD8Gi!+005o0f$RVP0rgN!R7C&)00II65)u*u0s;X60RjR71_lNR z2?_T0_Wu6<3JMAe3JUS@@c{t=`1tq%0RaL60@T#h0|NsJ3JU)I{{R2~{QUg?|NoAT zjsO4v0RaI3000LE2Lb{D`uh6({QUX(`QYH-?(XjT`ueuEwtxEi`u+X=udlBJ1O&#$ z#^mJW-rnB-|Nj7k`2c|W0IcZ%ujl}_Hx9l0D1iYl=A?7`v92n z0G#gty5s1H0Jr3Ty#7F*{lDz_0J-Jw_y1t9{1J}$VAJma zp6@`%>|m?@0C@f&wd|kE|B%H0zu^AQ?ET>P{GZhPpxF9Aruv}W_;APfK(FOn@nFvIaNF-7uJ2IH?jW-6 zK+Wm^wdSwX|8TJXA%^~Nx%_~|`mf~qFrE1zm-vv__pj>qfYS95p7If)@nFsHF}m*& zr|&_Jh)@5W(c_%RYJl001m>QchC<7X=hZG!hBu;SLbnMM6I>t|kWX(6W$o zIx8O>DD?8w$Gww>gJE4drJ<8s6XK8n00X^AL_t(o!|jxJQxicL#$!(JQcXytgT1>z zYzX3kQNW-m3PK>kh|-Hx5u`~Iq=UV8{hVIeO@H3I+hxdqj^i`T5Xd~gdG>9l|N85+ zGdU^Ix(hM2%bJ*UXeZllMO%{fFx{Vwwyb2DKM}>DM5F%zf{H)GVPsGsIK8|UiA3s` zf&pZZzLZwI`)yBej?b5S>eR`Tw=Z1ScxtGmt^qpxr`|JX&*tR#DhZ*yZ*kKIhY-~0 z>VN6eXJi7&P%9D2`&tmx0qFN8A)wBpo3{WV7iTD6(EyUX03hs*eNom`Py`8;(G1!; z>=gjOW{vr~uM`yAfdo>?C0!M50AP>J|NQy$5~0XQc%a`49sqbmG?7;U3G3f_?x}Ua zejqBK&7X$}cZ$9*j0V(!`s&Li{<)PF8h?O8SW|_VaHU|T3d{v@)l1zZW61%)HvGTV ztFwNU@TCn=R4;dPkpRp_z~r^7g-V9c%~ZeA&ENR1Z83lu16oT;Fky`5e}*UOBLW zfZ;4mcuWTb$#%~%0HKSRP(}v?q|K_7ce9=WsL56dlZc^v5<%YXHQWUwKq8cu7Pqqj zDj~v3ke{z)c*g|b-Hb32j9gL)<19e0y4{sO7=XK%FXd;;1V$07$bDF5JfZO%CN$9j z9pu7HGXMkUR6^EkI$(j^s37ABb$^c)44|B}YEcv$sQxj0X(hr3_C-Q<4@Hsqo2x+my7DRr zVHF`-4Y2yF@8eIQ?2DnHGR586kV{)3U;9oJ1Al-T?15%D z)y3q>;SAk~Xb&tRXVLlPZsOh45V~U%V&Ndbq4783c`#&{2NULi0Div)@KPiMn8EMw zL_qs_0I(m|0Q#kFHW&y~+FN_V4gj_zNUQs=q8YMOLimktL5|n}K=4?Q9x*6JD_I(D zAJXra#UlWKIU@xbR5#Fgw|}OlW+XJ+MA^=R8D0|*?@mB*DZ$N|AwWn!>WIhhIGQe) zG_q&9;>eKT^4LsTnYlFALCeKPUrSb<#f7I-RU;$*Gb?qJZbJ7o64Q$$>0PNOJ2cd=Xp+%{30v*8UOIS-6nWV b|Cs&&CV3;(y9-hE00000NkvXXu0mjfVM!{+ literal 2851 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 a2it#tsAIaS9p3lv(b>V(zQoQq_WuBm=|rLc literal 51695 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%0D}t}!A64# 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 Te#RJt2VrvD;u!IW+pYft1QtW! From c8bee2792926b461cea0cc6c435e5ef230bcd053 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Mon, 13 Feb 2023 18:39:14 +0800 Subject: [PATCH 1857/2015] remove RustDesk renamed to rustdesk which is for ps convience but cause code sign failure --- build.py | 1 - 1 file changed, 1 deletion(-) diff --git a/build.py b/build.py index dce434720..9e490166f 100755 --- a/build.py +++ b/build.py @@ -322,7 +322,6 @@ def build_flutter_dmg(version, features): os.system('sed -i "" "s/char \*\*rustdesk_core_main(int \*args_len);//" flutter/macos/Runner/bridge_generated.h') os.chdir('flutter') os.system('flutter build macos --release') - os.system('mv ./build/macos/Build/Products/Release/RustDesk.app/Contents/MacOS/RustDesk ./build/macos/Build/Products/Release/RustDesk.app/Contents/MacOS/rustdesk') os.system( "create-dmg rustdesk.dmg ./build/macos/Build/Products/Release/RustDesk.app") os.rename("rustdesk.dmg", f"../rustdesk-{version}.dmg") From 8a68974f4f1fa17073a82c331075ed5dd2ca4a0a Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 14 Feb 2023 14:30:42 +0800 Subject: [PATCH 1858/2015] Try out change CFBundleExecutable to rustdesk from EXECUTABLE_NAME, so that it is not "RustDesk" --- flutter/macos/Runner/Info.plist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/macos/Runner/Info.plist b/flutter/macos/Runner/Info.plist index 96616e8c4..0438f9d85 100644 --- a/flutter/macos/Runner/Info.plist +++ b/flutter/macos/Runner/Info.plist @@ -5,7 +5,7 @@ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable - $(EXECUTABLE_NAME) + rustdesk CFBundleIconFile CFBundleIdentifier From b65f940a25ebb3414e493908ee456742ecf230eb Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Tue, 14 Feb 2023 14:45:31 +0800 Subject: [PATCH 1859/2015] fix: issue #3204 --- src/lang.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang.rs b/src/lang.rs index f24d015e2..3dc81c8aa 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -81,7 +81,7 @@ pub fn translate_locale(name: String, locale: &str) -> String { if lang.is_empty() { // zh_CN on Linux, zh-Hans-CN on mac, zh_CN_#Hans on Android if locale.starts_with("zh") { - lang = (if locale.contains("TW") { "tw" } else { "cn" }).to_owned(); + lang = (if locale.contains("tw") { "tw" } else { "cn" }).to_owned(); } } if lang.is_empty() { From 60fa453495152f21be622de154efdd28d79efd39 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 14 Feb 2023 14:50:01 +0800 Subject: [PATCH 1860/2015] revert back, https://stackoverflow.com/questions/3654931/application-failed-codesign-verification, codesign fail after change executable_name --- flutter/macos/Runner/Info.plist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/macos/Runner/Info.plist b/flutter/macos/Runner/Info.plist index 0438f9d85..96616e8c4 100644 --- a/flutter/macos/Runner/Info.plist +++ b/flutter/macos/Runner/Info.plist @@ -5,7 +5,7 @@ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable - rustdesk + $(EXECUTABLE_NAME) CFBundleIconFile CFBundleIdentifier From 1adfc2c7b00cea74ce299676c9b1c5828291fb7d Mon Sep 17 00:00:00 2001 From: FastAct <93490087+FastAct@users.noreply.github.com> Date: Tue, 14 Feb 2023 10:05:47 +0100 Subject: [PATCH 1861/2015] changed language files added dutch translatoinn --- src/lang.rs | 3 + src/lang/nl.rs | 453 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 456 insertions(+) create mode 100644 src/lang/nl.rs diff --git a/src/lang.rs b/src/lang.rs index f24d015e2..a50d2b5b9 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -14,6 +14,7 @@ mod id; mod it; mod ja; mod ko; +mod nl; mod pl; mod ptbr; mod ro; @@ -40,6 +41,7 @@ lazy_static::lazy_static! { ("it", "Italiano"), ("fr", "Français"), ("de", "Deutsch"), + ("nl", "Nederlands"), ("cn", "简体中文"), ("tw", "繁體中文"), ("pt", "Português"), @@ -99,6 +101,7 @@ pub fn translate_locale(name: String, locale: &str) -> String { "it" => it::T.deref(), "tw" => tw::T.deref(), "de" => de::T.deref(), + "nl" => nl::T.deref(), "es" => es::T.deref(), "hu" => hu::T.deref(), "ru" => ru::T.deref(), diff --git a/src/lang/nl.rs b/src/lang/nl.rs new file mode 100644 index 000000000..3b01492d3 --- /dev/null +++ b/src/lang/nl.rs @@ -0,0 +1,453 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "Status"), + ("Your Desktop", "Uw Bureaublad"), + ("desk_tip", "Uw bureaublad is toegankelijk via de ID en het wachtwoord hieronder."), + ("Password", "Wachtwoord"), + ("Ready", "Klaar"), + ("Established", "Opgezet"), + ("connecting_status", "Verbinding maken met het RustDesk netwerk..."), + ("Enable Service", "Service Inschakelen"), + ("Start Service", "Start Service"), + ("Service is running", "De service loopt."), + ("Service is not running", "De service loopt niet"), + ("not_ready_status", "Niet klaar, controleer de netwerkverbinding"), + ("Control Remote Desktop", "Beheer Extern Bureaublad"), + ("Transfer File", "Bestand Overzetten"), + ("Connect", "Verbinden"), + ("Recent Sessions", "Recente Behandelingen"), + ("Address Book", "Adresboek"), + ("Confirmation", "Bevestiging"), + ("TCP Tunneling", "TCP Tunneling"), + ("Remove", "Verwijder"), + ("Refresh random password", "Vernieuw willekeurig wachtwoord"), + ("Set your own password", "Stel je eigen wachtwoord in"), + ("Enable Keyboard/Mouse", "Toetsenbord/Muis Inschakelen"), + ("Enable Clipboard", "Klembord Inschakelen"), + ("Enable File Transfer", "Bestandsoverdracht Inschakelen"), + ("Enable TCP Tunneling", "TCP Tunneling Inschakelen"), + ("IP Whitelisting", "IP Witte Lijst"), + ("ID/Relay Server", "ID/Relay Server"), + ("Import Server Config", "Importeer Serverconfiguratie"), + ("Export Server Config", "Exporteer Serverconfiguratie"), + ("Import server configuration successfully", "Importeren serverconfiguratie succesvol"), + ("Export server configuration successfully", "Exporteren serverconfiguratie succesvol"), + ("Invalid server configuration", "Ongeldige Serverconfiguratie"), + ("Clipboard is empty", "Klembord is leeg"), + ("Stop service", "Stop service"), + ("Change ID", "Wijzig ID"), + ("Website", "Website"), + ("About", "Over"), + ("Slogan_tip", "Gedaan met het hart in deze chaotische wereld!"), + ("Privacy Statement", "Privacyverklaring"), + ("Mute", "Geluid uit"), + ("Build Date", "Versie datum"), + ("Version", "Versie"), + ("Home", "Startpagina"), + ("Audio Input", "Audio Ingang"), + ("Enhancements", "Verbeteringen"), + ("Hardware Codec", "Hardware Codec"), + ("Adaptive Bitrate", "Aangepaste Bitsnelheid"), + ("ID Server", "Server ID"), + ("Relay Server", "Relay Server"), + ("API Server", "API Server"), + ("invalid_http", "Moet beginnen met http:// of https://"), + ("Invalid IP", "Ongeldig IP"), + ("id_change_tip", "Alleen de letters a-z, A-Z, 0-9, _ (underscore) kunnen worden gebruikt. De eerste letter moet a-z, A-Z zijn. De lengte moet tussen 6 en 16 liggen."), + ("Invalid format", "Ongeldig formaat"), + ("server_not_support", "Nog niet ondersteund door de server"), + ("Not available", "Niet beschikbaar"), + ("Too frequent", "Te vaak"), + ("Cancel", "Annuleer"), + ("Skip", "Overslaan"), + ("Close", "Sluit"), + ("Retry", "Probeer opnieuw"), + ("OK", "OK"), + ("Password Required", "Wachtwoord vereist"), + ("Please enter your password", "Geef uw wachtwoord in"), + ("Remember password", "Wachtwoord onthouden"), + ("Wrong Password", "Verkeerd wachtwoord"), + ("Do you want to enter again?", "Wil je opnieuw ingeven?"), + ("Connection Error", "Fout bij verbinding"), + ("Error", "Fout"), + ("Reset by the peer", "Reset door de peer"), + ("Connecting...", "Verbinding maken..."), + ("Connection in progress. Please wait.", "Verbinding in uitvoering. Even geduld a.u.b."), + ("Please try 1 minute later", "Probeer 1 minuut later"), + ("Login Error", "Login Fout"), + ("Successful", "Succesvol"), + ("Connected, waiting for image...", "Verbonden, wacht op beeld..."), + ("Name", "Naam"), + ("Type", "Type"), + ("Modified", "Gewijzigd"), + ("Size", "Grootte"), + ("Show Hidden Files", "Toon verborgen bestanden"), + ("Receive", "Ontvangen"), + ("Send", "Verzenden"), + ("Refresh File", "Bestand Verversen"), + ("Local", "Lokaal"), + ("Remote", "Op afstand"), + ("Remote Computer", "Externe Computer"), + ("Local Computer", "Lokale Computer"), + ("Confirm Delete", "Bevestig Verwijderen"), + ("Delete", "Verwijder"), + ("Properties", "Eigenschappen"), + ("Multi Select", "Meervoudig selecteren"), + ("Select All", "Selecteer Alle"), + ("Unselect All", "Deselecteer alles"), + ("Empty Directory", "Lege Map"), + ("Not an empty directory", "Geen Lege Map"), + ("Are you sure you want to delete this file?", "Weet je zeker dat je dit bestand wilt verwijderen?"), + ("Are you sure you want to delete this empty directory?", "Weet je zeker dat je deze lege map wilt verwijderen?"), + ("Are you sure you want to delete the file of this directory?", "Weet je zeker dat je het bestand uit deze map wilt verwijderen?"), + ("Do this for all conflicts", "Doe dit voor alle conflicten"), + ("This is irreversible!", "Dit is onomkeerbaar!"), + ("Deleting", "Verwijderen"), + ("files", "bestanden"), + ("Waiting", "Wachten"), + ("Finished", "Voltooid"), + ("Speed", "Snelheid"), + ("Custom Image Quality", "Aangepaste beeldkwaliteit"), + ("Privacy mode", "Privacymodus"), + ("Block user input", "Gebruikersinvoer blokkeren"), + ("Unblock user input", "Gebruikersinvoer opheffen"), + ("Adjust Window", "Venster Aanpassen"), + ("Original", "Origineel"), + ("Shrink", "Verkleinen"), + ("Stretch", "Uitrekken"), + ("Scrollbar", "Schuifbalk"), + ("ScrollAuto", "Auto Schuiven"), + ("Good image quality", "Goede beeldkwaliteit"), + ("Balanced", "Gebalanceerd"), + ("Optimize reaction time", "Optimaliseer reactietijd"), + ("Custom", "Aangepast"), + ("Show remote cursor", "Toon cursor van extern bureaublad"), + ("Show quality monitor", "Kwaliteitsmonitor tonen"), + ("Disable clipboard", "Klembord uitschakelen"), + ("Lock after session end", "Vergrendelen na einde sessie"), + ("Insert", "Invoegen"), + ("Insert Lock", "Vergrendeling Invoegen"), + ("Refresh", "Vernieuwen"), + ("ID does not exist", "ID bestaat niet"), + ("Failed to connect to rendezvous server", "Verbinding met rendez-vous-server mislukt"), + ("Please try later", "Probeer later opnieuw"), + ("Remote desktop is offline", "Extern bureaublad is offline"), + ("Key mismatch", "Code onjuist"), + ("Timeout", "Time-out"), + ("Failed to connect to relay server", "Verbinding met relayserver mislukt"), + ("Failed to connect via rendezvous server", "Verbinding via rendez-vous-server mislukt"), + ("Failed to connect via relay server", "Verbinding via relaisserver mislukt"), + ("Failed to make direct connection to remote desktop", "Onmogelijk direct verbinding te maken met extern bureaublad"), + ("Set Password", "Wachtwoord Instellen"), + ("OS Password", "OS Wachtwoord"), + ("install_tip", "Je gebruikt een niet geinstalleerde versie. Als gevolg van UAC-beperkingen is het in sommige gevallen niet mogelijk om als controleterminal de muis en het toetsenbord te bedienen of het scherm over te nemen. Klik op de knop hieronder om RustDesk op het systeem te installeren om het bovenstaande probleem te voorkomen."), + ("Click to upgrade", "Klik voor upgrade"), + ("Click to download", "Klik om te downloaden"), + ("Click to update", "Klik om bij te werken"), + ("Configure", "Configureren"), + ("config_acc", "Om je bureaublad op afstand te kunnen bedienen, moet je RustDesk \"toegankelijkheid\" toestemming geven."), + ("config_screen", "Om toegang te krijgen tot het externe bureaublad, moet je RustDesk de toestemming \"schermregistratie\" geven."), + ("Installing ...", "Installeren ..."), + ("Install", "Installeer"), + ("Installation", "Installatie"), + ("Installation Path", "Installatie Pad"), + ("Create start menu shortcuts", "Startmenu snelkoppelingen maken"), + ("Create desktop icon", "Bureaubladpictogram maken"), + ("agreement_tip", "Het starten van de installatie betekent het accepteren van de licentieovereenkomst."), + ("Accept and Install", "Accepteren en installeren"), + ("End-user license agreement", "Licentieovereenkomst eindgebruiker"), + ("Generating ...", "Genereert ..."), + ("Your installation is lower version.", "Uw installatie is een lagere versie."), + ("not_close_tcp_tip", "Gelieve dit venster niet te sluiten wanneer u de tunnel gebruikt"), + ("Listening ...", "Luisteren ..."), + ("Remote Host", "Externe Host"), + ("Remote Port", "Externe Poort"), + ("Action", "Actie"), + ("Add", "Toevoegen"), + ("Local Port", "Lokale Poort"), + ("Local Address", "Lokaal Adres"), + ("Change Local Port", "Wijzig Lokale Poort"), + ("setup_server_tip", "Als u een snellere verbindingssnelheid nodig heeft, kunt u ervoor kiezen om uw eigen server aan te maken"), + ("Too short, at least 6 characters.", "e kort, minstens 6 tekens."), + ("The confirmation is not identical.", "De bevestiging is niet identiek."), + ("Permissions", "Machtigingen"), + ("Accept", "Accepteren"), + ("Dismiss", "Afwijzen"), + ("Disconnect", "Verbinding verbreken"), + ("Allow using keyboard and mouse", "Gebruik toetsenbord en muis toestaan"), + ("Allow using clipboard", "Gebruik klembord toestaan"), + ("Allow hearing sound", "Geluidsweergave toestaan"), + ("Allow file copy and paste", "Kopieren en plakken van bestanden toestaan"), + ("Connected", "Verbonden"), + ("Direct and encrypted connection", "Directe en versleutelde verbinding"), + ("Relayed and encrypted connection", "Doorgeschakelde en versleutelde verbinding"), + ("Direct and unencrypted connection", "Directe en niet-versleutelde verbinding"), + ("Relayed and unencrypted connection", "Doorgeschakelde en niet-versleutelde verbinding"), + ("Enter Remote ID", "Voer Extern ID in"), + ("Enter your password", "Voer uw wachtwoord in"), + ("Logging in...", "Aanmelden..."), + ("Enable RDP session sharing", "Delen van RDP-sessie inschakelen"), + ("Auto Login", "Automatisch Aanmelden"), + ("Enable Direct IP Access", "Directe IP-toegang inschakelen"), + ("Rename", "Naam wijzigen"), + ("Space", "Spatie"), + ("Create Desktop Shortcut", "Snelkoppeling op bureaublad maken"), + ("Change Path", "Pad wijzigen"), + ("Create Folder", "Map Maken"), + ("Please enter the folder name", "Geef de mapnaam op"), + ("Fix it", "Repareer het"), + ("Warning", "Waarschuwing"), + ("Login screen using Wayland is not supported", "Aanmeldingsscherm via Wayland wordt niet ondersteund"), + ("Reboot required", "Opnieuw opstarten vereist"), + ("Unsupported display server ", "Niet-ondersteunde weergaveserver"), + ("x11 expected", "x11 verwacht"), + ("Port", "Poort"), + ("Settings", "Instellingen"), + ("Username", "Gebruikersnaam"), + ("Invalid port", "Ongeldige poort"), + ("Closed manually by the peer", "Handmatig gesloten door de peer"), + ("Enable remote configuration modification", "Wijziging configuratie op afstand inschakelen"), + ("Run without install", "Uitvoeren zonder installatie"), + ("Always connected via relay", "Altijd verbonden via relay"), + ("Always connect via relay", "Altijd verbinden via relay"), + ("whitelist_tip", "Alleen een IP-adres op de witte lijst krijgt toegang tot mijn toestel"), + ("Login", "Log In"), + ("Verify", "Controleer"), + ("Remember me", "Herinner mij"), + ("Trust this device", "Vertrouw dit apparaat"), + ("Verification code", "Verificatie code"), + ("verification_tip", "Er is een nieuw apparaat gedetecteerd en er is een verificatiecode naar het geregistreerde e-mailadres gestuurd, voer de verificatiecode in om de verbinding voort te zetten."), + ("Logout", "Log Uit"), + ("Tags", "Labels"), + ("Search ID", "Zoek ID"), + ("whitelist_sep", "Gescheiden door komma, puntkomma, spatie of nieuwe regel"), + ("Add ID", "ID Toevoegen"), + ("Add Tag", "Label Toevoegen"), + ("Unselect all tags", "Alle labels verwijderen"), + ("Network error", "Netwerkfout"), + ("Username missed", "Gebruikersnaam gemist"), + ("Password missed", "Wachtwoord vergeten"), + ("Wrong credentials", "Verkeerde inloggegevens"), + ("Edit Tag", "Label Bewerken"), + ("Unremember Password", "Wachtwoord vergeten"), + ("Favorites", "Favorieten"), + ("Add to Favorites", "Toevoegen aan Favorieten"), + ("Remove from Favorites", "Verwijderen uit Favorieten"), + ("Empty", "Leeg"), + ("Invalid folder name", "Ongeldige mapnaam"), + ("Socks5 Proxy", "Socks5 Proxy"), + ("Hostname", "Hostnaam"), + ("Discovered", "Ontdekt"), + ("install_daemon_tip", "Om bij het opstarten van de computer te kunnen beginnen, moet je de systeemdienst installeren."), + ("Remote ID", "Externe ID"), + ("Paste", "Plakken"), + ("Paste here?", "Hier plakken"), + ("Are you sure to close the connection?", "Weet je zeker dat je de verbinding wilt sluiten?"), + ("Download new version", "Download nieuwe versie"), + ("Touch mode", "Aanraak modus"), + ("Mouse mode", "Muismodus"), + ("One-Finger Tap", "Een-Vinger Tik"), + ("Left Mouse", "Linkermuis"), + ("One-Long Tap", "Een-Vinger-Lange-Tik"), + ("Two-Finger Tap", "Twee-Vingers-Tik"), + ("Right Mouse", "Rechter muis"), + ("One-Finger Move", "Een-Vinger-Verplaatsing"), + ("Double Tap & Move", "Dubbel Tik en Verplaatsen"), + ("Mouse Drag", "Muis Slepen"), + ("Three-Finger vertically", "Drie-Vinger verticaal"), + ("Mouse Wheel", "Muiswiel"), + ("Two-Finger Move", "Twee-Vingers Verplaatsen"), + ("Canvas Move", "Canvas Verplaatsen"), + ("Pinch to Zoom", "Knijp om te Zoomen"), + ("Canvas Zoom", "Canvas Zoom"), + ("Reset canvas", "Reset canvas"), + ("No permission of file transfer", "Geen toestemming voor bestandsoverdracht"), + ("Note", "Opmerking"), + ("Connection", "Verbinding"), + ("Share Screen", "Scherm Delen"), + ("CLOSE", "SLUITEN"), + ("OPEN", "OPEN"), + ("Chat", "Chat"), + ("Total", "Totaal"), + ("items", "items"), + ("Selected", "Geselecteerd"), + ("Screen Capture", "Schermopname"), + ("Input Control", "Invoercontrole"), + ("Audio Capture", "Audio Opnemen"), + ("File Connection", "Bestandsverbinding"), + ("Screen Connection", "Schermverbinding"), + ("Do you accept?", "Sta je toe?"), + ("Open System Setting", "Systeeminstelling Openen"), + ("How to get Android input permission?", "Hoe krijg ik Android invoer toestemming?"), + ("android_input_permission_tip1", "Om ervoor te zorgen dat een extern apparaat uw Android-apparaat kan besturen via muis of aanraking, moet u RustDesk toestaan om de \"Toegankelijkheid\" service te gebruiken."), + ("android_input_permission_tip2", "Ga naar de volgende pagina met systeeminstellingen, zoek en ga naar [Geinstalleerde Services], schakel de service [RustDesk Input] in."), + ("android_new_connection_tip", "Er is een nieuw controleverzoek binnengekomen, dat uw huidige apparaat wil controleren."), + ("android_service_will_start_tip", "Als u \"Schermopname\" inschakelt, wordt de service automatisch gestart, zodat andere apparaten een verbinding met uw apparaat kunnen aanvragen."), + ("android_stop_service_tip", "Het sluiten van de service zal automatisch alle gemaakte verbindingen sluiten."), + ("android_version_audio_tip", "De huidige versie van Android ondersteunt geen audio-opname, upgrade naar Android 10 of hoger."), + ("android_start_service_tip", "Druk op [Start Service] of op de permissie OPEN [Screenshot] om de service voor het overnemen van het scherm te starten."), + ("Account", "Account"), + ("Overwrite", "Overschrijven"), + ("This file exists, skip or overwrite this file?", "Dit bestand bestaat reeds, overslaan of overschrijven?"), + ("Quit", "Afsluiten"), + ("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"), + ("Help", "https://rustdesk.com/docs/en/manual/linux/#x11-required"), + ("Failed", "Mislukt"), + ("Succeeded", "Geslaagd"), + ("Someone turns on privacy mode, exit", "Iemand schakelt privacymodus in, afsluiten"), + ("Unsupported", "Niet Ondersteund"), + ("Peer denied", "Peer geweigerd"), + ("Please install plugins", "Installeer plugins"), + ("Peer exit", "Peer afgesloten"), + ("Failed to turn off", "Uitschakelen mislukt"), + ("Turned off", "Uitgeschakeld"), + ("In privacy mode", "In privacymodus"), + ("Out privacy mode", "Uit privacymodus"), + ("Language", "Taal"), + ("Keep RustDesk background service", "RustDesk achtergronddienst behouden"), + ("Ignore Battery Optimizations", "Negeer Batterij Optimalisaties"), + ("android_open_battery_optimizations_tip", "Ga naar de volgende pagina met instellingen"), + ("Connection not allowed", "Verbinding niet toegestaan"), + ("Legacy mode", "Verouderde modus"), + ("Map mode", "Map mode"), + ("Translate mode", "Vertaalmodus"), + ("Use permanent password", "Gebruik permanent wachtwoord"), + ("Use both passwords", "Gebruik beide wachtwoorden"), + ("Set permanent password", "Stel permanent wachtwoord in"), + ("Enable Remote Restart", "Schakel Herstart op afstand in"), + ("Allow remote restart", "Opnieuw Opstarten op afstand toestaan"), + ("Restart Remote Device", "Apparaat op afstand herstarten"), + ("Are you sure you want to restart", "Weet je zeker dat je wilt herstarten"), + ("Restarting Remote Device", "Apparaat op afstand herstarten"), + ("remote_restarting_tip", "Apparaat op afstand wordt opnieuw opgestart, sluit dit bericht en maak na een ogenblik opnieuw verbinding met het permanente wachtwoord."), + ("Copied", "Gekopieerd"), + ("Exit Fullscreen", "Volledig Scherm sluiten"), + ("Fullscreen", "Volledig Scherm"), + ("Mobile Actions", "Mobiele Acties"), + ("Select Monitor", "Selecteer Monitor"), + ("Control Actions", "Controleacties"), + ("Display Settings", "Beeldscherminstellingen"), + ("Ratio", "Verhouding"), + ("Image Quality", "Beeldkwaliteit"), + ("Scroll Style", "Scroll Stijl"), + ("Show Menubar", "Toon Menubalk"), + ("Hide Menubar", "Verberg Menubalk"), + ("Direct Connection", "Directe Verbinding"), + ("Relay Connection", "Relaisverbinding"), + ("Secure Connection", "Beveiligde Verbinding"), + ("Insecure Connection", "Onveilige Verbinding"), + ("Scale original", "Oorspronkelijke schaal"), + ("Scale adaptive", "Schaalaanpassing"), + ("General", "Algemeen"), + ("Security", "Beveiliging"), + ("Theme", "Thema"), + ("Dark Theme", "Donker Thema"), + ("Dark", "Donker"), + ("Light", "Licht"), + ("Follow System", "Volg Systeem"), + ("Enable hardware codec", "Hardware codec inschakelen"), + ("Unlock Security Settings", "Beveiligingsinstellingen vrijgeven"), + ("Enable Audio", "Audio Inschakelen"), + ("Unlock Network Settings", "Netwerkinstellingen Vrijgeven"), + ("Server", "Server"), + ("Direct IP Access", "Directe IP toegang"), + ("Proxy", "Proxy"), + ("Apply", "Toepassen"), + ("Disconnect all devices?", "Alle apparaten uitschakelen?"), + ("Clear", "Wis"), + ("Audio Input Device", "Audio-invoerapparaat"), + ("Deny remote access", "Toegang op afstand weigeren"), + ("Use IP Whitelisting", "Gebruik een witte lijst van IP-adressen"), + ("Network", "Netwerk"), + ("Enable RDP", "Zet RDP aan"), + ("Pin menubar", "Menubalk Vastzetten"), + ("Unpin menubar", "Menubalk vrijmaken"), + ("Recording", "Opnemen"), + ("Directory", "Map"), + ("Automatically record incoming sessions", "Automatisch inkomende sessies opnemen"), + ("Change", "Wissel"), + ("Start session recording", "Start de sessieopname"), + ("Stop session recording", "Stop de sessieopname"), + ("Enable Recording Session", "Opnamesessie Activeren"), + ("Allow recording session", "Opnamesessie toestaan"), + ("Enable LAN Discovery", "LAN-detectie inschakelen"), + ("Deny LAN Discovery", "LAN-detectie Weigeren"), + ("Write a message", "Schrijf een bericht"), + ("Prompt", "Verzoek"), + ("Please wait for confirmation of UAC...", "Wacht op bevestiging van UAC..."), + ("elevated_foreground_window_tip", "Het momenteel geopende venster van de op afstand bediende computer vereist hogere rechten. Daarom is het momenteel niet mogelijk de muis en het toetsenbord te gebruiken. Vraag de gebruiker wiens computer u op afstand bedient om het venster te minimaliseren of de rechten te verhogen. Om dit probleem in de toekomst te voorkomen, wordt aanbevolen de software te installeren op de op afstand bediende computer."), + ("Disconnected", "Afgesloten"), + ("Other", "Andere"), + ("Confirm before closing multiple tabs", "Bevestig voordat u meerdere tabbladen sluit"), + ("Keyboard Settings", "Toetsenbord instellingen"), + ("Full Access", "Volledige Toegang"), + ("Screen Share", "Scherm Delen"), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland vereist Ubuntu 21.04 of een hogere versie."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland vereist een hogere versie van Linux distro. Probeer X11 desktop of verander je OS."), + ("JumpLink", "JumpLink"), + ("Please Select the screen to be shared(Operate on the peer side).", "Selecteer het scherm dat moet worden gedeeld (Bediening aan de kant van de peer)."), + ("Show RustDesk", "Toon RustDesk"), + ("This PC", "Deze PC"), + ("or", "of"), + ("Continue with", "Ga verder met"), + ("Elevate", "Verhoog"), + ("Zoom cursor", "Cursor Zoomen"), + ("Accept sessions via password", "Sessies accepteren via wachtwoord"), + ("Accept sessions via click", "Sessies accepteren via klik"), + ("Accept sessions via both", "Accepteer sessies via beide"), + ("Please wait for the remote side to accept your session request...", "Wacht tot de andere kant uw sessieverzoek accepteert..."), + ("One-time Password", "Eenmalig Wachtwoord"), + ("Use one-time password", "Gebruik een eenmalig Wachtwoord"), + ("One-time password length", "Eenmalig Wachtwoord lengre"), + ("Request access to your device", "Toegang tot uw toestel aanvragen"), + ("Hide connection management window", "Verberg het venster voor verbindingsbeheer"), + ("hide_cm_tip", "Dit kan alleen als de toegang via een permanent wachtwoord verloopt."), + ("wayland_experiment_tip", "Wayland ondersteuning is slechts experimenteel. Gebruik alsjeblieft X11 als je onbeheerde toegang nodig hebt."), + ("Right click to select tabs", "Rechts klikken om tabbladen te selecteren"), + ("Skipped", "Overgeslagen"), + ("Add to Address Book", "Toevoegen aan Adresboek"), + ("Group", "Groep"), + ("Search", "Zoek"), + ("Closed manually by web console", "Handmatig gesloten door webconsole"), + ("Local keyboard type", "Lokaal toetsenbord"), + ("Select local keyboard type", "Selecteer lokaal toetsenbord"), + ("software_render_tip", "Als u een NVIDIA grafische kaart hebt en het externe venster sluit onmiddellijk na verbinding, kan het helpen om het nieuwe stuurprogramma te installeren en te kiezen voor software rendering. Een software herstart is vereist."), + ("Always use software rendering", "Gebruik altijd software rendering"), + ("config_input", "config_invoer"), + ("config_microphone", "config_microfoon"), + ("request_elevation_tip", "U kunt ook meer rechten vragen als iemand aan de andere kant aanwezig is."), + ("Wait", "Wacht"), + ("Elevation Error", "Verhogingsfout"), + ("Ask the remote user for authentication", "Vraag de gebruiker op afstand om bevestiging"), + ("Choose this if the remote account is administrator", ""), + ("Transmit the username and password of administrator", ""), + ("still_click_uac_tip", "De gebruiker op afstand moet altijd bevestigen via het UAC-venster van de werkende RustDesk."), + ("Request Elevation", "Verzoek om meer rechten"), + ("wait_accept_uac_tip", "Wacht tot de gebruiker op afstand het UAC-dialoogvenster accepteert."), + ("Elevate successfully", "Succesvolle verhoging van privileges"), + ("uppercase", "Hoofdletter"), + ("lowercase", "kleine letter"), + ("digit", "cijfer"), + ("special character", "speciaal teken"), + ("length>=8", "lengte>=8"), + ("Weak", "Zwak"), + ("Medium", "Midelmatig"), + ("Strong", "Sterk"), + ("Switch Sides", "Wissel van kant"), + ("Please confirm if you want to share your desktop?", "bevestig als je je bureaublad wilt delen?"), + ("Closed as expected", "Gesloten zoals verwacht"), + ("Display", "Weergave"), + ("Default View Style", "Standaard Weergave Stijl"), + ("Default Scroll Style", "Standaard Scroll Stijl"), + ("Default Image Quality", "Standaard Beeldkwaliteit"), + ("Default Codec", "tandaard Codec"), + ("Bitrate", "Bitrate"), + ("FPS", "FPS"), + ("Auto", "Auto"), + ("Other Default Options", "Andere Standaardopties"), + ("Voice call", "Spraakoproep"), + ("Text chat", "Tekst chat"), + ("Stop voice call", "Stop spraakoproep"), + ].iter().cloned().collect(); +} From cea123c79f5f0e39ed394df0f60f0d404949ee27 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 14 Feb 2023 19:20:22 +0800 Subject: [PATCH 1862/2015] more lang in setup.nsi --- res/setup.nsi | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/res/setup.nsi b/res/setup.nsi index 5410e0ff5..635851d0a 100644 --- a/res/setup.nsi +++ b/res/setup.nsi @@ -56,8 +56,74 @@ InstallDir "$PROGRAMFILES64\${PRODUCT_NAME}" #################################################################### # Language -!insertmacro MUI_LANGUAGE "English" +!insertmacro MUI_LANGUAGE "English" ; The first language is the default language +!insertmacro MUI_LANGUAGE "French" +!insertmacro MUI_LANGUAGE "German" +!insertmacro MUI_LANGUAGE "Spanish" +!insertmacro MUI_LANGUAGE "SpanishInternational" !insertmacro MUI_LANGUAGE "SimpChinese" +!insertmacro MUI_LANGUAGE "TradChinese" +!insertmacro MUI_LANGUAGE "Japanese" +!insertmacro MUI_LANGUAGE "Korean" +!insertmacro MUI_LANGUAGE "Italian" +!insertmacro MUI_LANGUAGE "Dutch" +!insertmacro MUI_LANGUAGE "Danish" +!insertmacro MUI_LANGUAGE "Swedish" +!insertmacro MUI_LANGUAGE "Norwegian" +!insertmacro MUI_LANGUAGE "NorwegianNynorsk" +!insertmacro MUI_LANGUAGE "Finnish" +!insertmacro MUI_LANGUAGE "Greek" +!insertmacro MUI_LANGUAGE "Russian" +!insertmacro MUI_LANGUAGE "Portuguese" +!insertmacro MUI_LANGUAGE "PortugueseBR" +!insertmacro MUI_LANGUAGE "Polish" +!insertmacro MUI_LANGUAGE "Ukrainian" +!insertmacro MUI_LANGUAGE "Czech" +!insertmacro MUI_LANGUAGE "Slovak" +!insertmacro MUI_LANGUAGE "Croatian" +!insertmacro MUI_LANGUAGE "Bulgarian" +!insertmacro MUI_LANGUAGE "Hungarian" +!insertmacro MUI_LANGUAGE "Thai" +!insertmacro MUI_LANGUAGE "Romanian" +!insertmacro MUI_LANGUAGE "Latvian" +!insertmacro MUI_LANGUAGE "Macedonian" +!insertmacro MUI_LANGUAGE "Estonian" +!insertmacro MUI_LANGUAGE "Turkish" +!insertmacro MUI_LANGUAGE "Lithuanian" +!insertmacro MUI_LANGUAGE "Slovenian" +!insertmacro MUI_LANGUAGE "Serbian" +!insertmacro MUI_LANGUAGE "SerbianLatin" +!insertmacro MUI_LANGUAGE "Arabic" +!insertmacro MUI_LANGUAGE "Farsi" +!insertmacro MUI_LANGUAGE "Hebrew" +!insertmacro MUI_LANGUAGE "Indonesian" +!insertmacro MUI_LANGUAGE "Mongolian" +!insertmacro MUI_LANGUAGE "Luxembourgish" +!insertmacro MUI_LANGUAGE "Albanian" +!insertmacro MUI_LANGUAGE "Breton" +!insertmacro MUI_LANGUAGE "Belarusian" +!insertmacro MUI_LANGUAGE "Icelandic" +!insertmacro MUI_LANGUAGE "Malay" +!insertmacro MUI_LANGUAGE "Bosnian" +!insertmacro MUI_LANGUAGE "Kurdish" +!insertmacro MUI_LANGUAGE "Irish" +!insertmacro MUI_LANGUAGE "Uzbek" +!insertmacro MUI_LANGUAGE "Galician" +!insertmacro MUI_LANGUAGE "Afrikaans" +!insertmacro MUI_LANGUAGE "Catalan" +!insertmacro MUI_LANGUAGE "Esperanto" +!insertmacro MUI_LANGUAGE "Asturian" +!insertmacro MUI_LANGUAGE "Basque" +!insertmacro MUI_LANGUAGE "Pashto" +!insertmacro MUI_LANGUAGE "ScotsGaelic" +!insertmacro MUI_LANGUAGE "Georgian" +!insertmacro MUI_LANGUAGE "Vietnamese" +!insertmacro MUI_LANGUAGE "Welsh" +!insertmacro MUI_LANGUAGE "Armenian" +!insertmacro MUI_LANGUAGE "Corsican" +!insertmacro MUI_LANGUAGE "Tatar" +!insertmacro MUI_LANGUAGE "Hindi" + #################################################################### # Sections From d2e0cb396f90cc24ef126da7c0d3766b26ee07f1 Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 14 Feb 2023 19:44:14 +0800 Subject: [PATCH 1863/2015] relay hint msgbox Signed-off-by: 21pages --- flutter/lib/models/model.dart | 42 +++++++++++++++++++++++- src/client.rs | 4 +++ src/client/io_loop.rs | 18 +++++++--- src/flutter_ffi.rs | 7 ++-- src/lang/ca.rs | 3 +- src/lang/cn.rs | 3 +- src/lang/cs.rs | 3 +- src/lang/da.rs | 3 +- src/lang/de.rs | 3 +- src/lang/en.rs | 3 +- src/lang/eo.rs | 3 +- src/lang/es.rs | 3 +- src/lang/fa.rs | 3 +- src/lang/fr.rs | 3 +- src/lang/gr.rs | 3 +- src/lang/hu.rs | 3 +- src/lang/id.rs | 3 +- src/lang/it.rs | 3 +- src/lang/ja.rs | 3 +- src/lang/ko.rs | 3 +- src/lang/kz.rs | 3 +- src/lang/pl.rs | 3 +- src/lang/pt_PT.rs | 3 +- src/lang/ptbr.rs | 3 +- src/lang/ro.rs | 3 +- src/lang/ru.rs | 3 +- src/lang/sk.rs | 3 +- src/lang/sl.rs | 3 +- src/lang/sq.rs | 3 +- src/lang/sr.rs | 3 +- src/lang/sv.rs | 3 +- src/lang/template.rs | 3 +- src/lang/th.rs | 3 +- src/lang/tr.rs | 3 +- src/lang/tw.rs | 3 +- src/lang/ua.rs | 3 +- src/lang/vn.rs | 3 +- src/ui/remote.rs | 6 ++-- src/ui_session_interface.rs | 62 ++++++++++++++++++++++++++++------- 39 files changed, 179 insertions(+), 59 deletions(-) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index d0a2ea601..0bd6934a8 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -298,6 +298,8 @@ class FfiModel with ChangeNotifier { showWaitUacDialog(id, dialogManager, type); } else if (type == 'elevation-error') { showElevationError(id, type, title, text, dialogManager); + } else if (type == "relay-hint") { + showRelayHintDialog(id, type, title, text, dialogManager); } else { var hasRetry = evt['hasRetry'] == 'true'; showMsgBox(id, type, title, text, link, hasRetry, dialogManager); @@ -312,7 +314,7 @@ class FfiModel with ChangeNotifier { _timer?.cancel(); if (hasRetry) { _timer = Timer(Duration(seconds: _reconnects), () { - bind.sessionReconnect(id: id); + bind.sessionReconnect(id: id, forceRelay: false); clearPermissions(); dialogManager.showLoading(translate('Connecting...'), onCancel: closeConnection); @@ -323,6 +325,44 @@ class FfiModel with ChangeNotifier { } } + void showRelayHintDialog(String id, String type, String title, String text, + OverlayDialogManager dialogManager) { + dialogManager.show(tag: '$id-$type', (setState, close) { + onClose() { + closeConnection(); + close(); + } + + reconnect(bool forceRelay) { + bind.sessionReconnect(id: id, forceRelay: forceRelay); + clearPermissions(); + dialogManager.showLoading(translate('Connecting...'), + onCancel: closeConnection); + } + + final style = + ElevatedButton.styleFrom(backgroundColor: Colors.green[700]); + return CustomAlertDialog( + title: null, + content: msgboxContent(type, title, + "${translate(text)}\n\n${translate('relay_hint_tip')}"), + actions: [ + dialogButton('Close', onPressed: onClose, isOutline: true), + dialogButton('Retry', onPressed: () => reconnect(false)), + dialogButton('Connect via relay', + onPressed: () => reconnect(true), buttonStyle: style), + dialogButton('Always connect via relay', onPressed: () { + const option = 'force-always-relay'; + bind.sessionPeerOption( + id: id, name: option, value: bool2option(option, true)); + reconnect(true); + }, buttonStyle: style), + ], + onCancel: onClose, + ); + }); + } + /// Handle the peer info event based on [evt]. handlePeerInfo(Map evt, String peerId) async { // recent peer updated by handle_peer_info(ui_session_interface.rs) --> handle_peer_info(client.rs) --> save_config(client.rs) diff --git a/src/client.rs b/src/client.rs index 05b34d781..77221bdb2 100644 --- a/src/client.rs +++ b/src/client.rs @@ -916,6 +916,8 @@ pub struct LoginConfigHandler { pub direct: Option, pub received: bool, switch_uuid: Option, + pub success_time: Option, + pub direct_error_counter: usize, } impl Deref for LoginConfigHandler { @@ -962,6 +964,8 @@ impl LoginConfigHandler { self.direct = None; self.received = false; self.switch_uuid = switch_uuid; + self.success_time = None; + self.direct_error_counter = 0; } /// Check if the client should auto login. diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 5186aff4d..de91b091d 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -25,9 +25,8 @@ use hbb_common::{allow_err, get_time, message_proto::*, sleep}; use hbb_common::{fs, log, Stream}; use crate::client::{ - new_voice_call_request, Client, CodecFormat, MediaData, MediaSender, - QualityStatus, MILLI1, SEC30, SERVER_CLIPBOARD_ENABLED, SERVER_FILE_TRANSFER_ENABLED, - SERVER_KEYBOARD_ENABLED, + new_voice_call_request, Client, CodecFormat, MediaData, MediaSender, QualityStatus, MILLI1, + SEC30, SERVER_CLIPBOARD_ENABLED, SERVER_FILE_TRANSFER_ENABLED, SERVER_KEYBOARD_ENABLED, }; #[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::common::{check_clipboard, update_clipboard, ClipboardContext, CLIPBOARD_INTERVAL}; @@ -148,7 +147,15 @@ impl Remote { Err(err) => { log::error!("Connection closed: {}", err); self.handler.set_force_relay(direct, received); - self.handler.msgbox("error", "Connection Error", &err.to_string(), ""); + let msgtype = "error"; + let title = "Connection Error"; + let text = err.to_string(); + let show_relay_hint = self.handler.show_relay_hint(last_recv_time, msgtype, title, &text); + if show_relay_hint{ + self.handler.msgbox("relay-hint", title, &text, ""); + } else { + self.handler.msgbox(msgtype, title, &text, ""); + } break; } Ok(ref bytes) => { @@ -754,7 +761,8 @@ impl Remote { Data::CloseVoiceCall => { self.stop_voice_call(); let msg = new_voice_call_request(false); - self.handler.on_voice_call_closed("Closed manually by the peer"); + self.handler + .on_voice_call_closed("Closed manually by the peer"); allow_err!(peer.send(&msg).await); } _ => {} diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 3025d722c..f8ee512d8 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1,4 +1,3 @@ -use crate::ui_session_interface::InvokeUiSession; use crate::{ client::file_trait::FileManager, common::make_fd_to_json, @@ -7,7 +6,7 @@ use crate::{ flutter::{session_add, session_start_}, ui_interface::{self, *}, }; -use flutter_rust_bridge::{StreamSink, SyncReturn, ZeroCopyBuffer}; +use flutter_rust_bridge::{StreamSink, SyncReturn}; use hbb_common::{ config::{self, LocalConfig, PeerConfig, ONLINE}, fs, log, @@ -157,9 +156,9 @@ pub fn session_record_screen(id: String, start: bool, width: usize, height: usiz } } -pub fn session_reconnect(id: String) { +pub fn session_reconnect(id: String, force_relay: bool) { if let Some(session) = SESSIONS.read().unwrap().get(&id) { - session.reconnect(); + session.reconnect(force_relay); } } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index e98c6636a..d483a185d 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Tancat manualment pel peer"), ("Enable remote configuration modification", "Habilitar modificació remota de configuració"), ("Run without install", "Executar sense instal·lar"), - ("Always connected via relay", "Connectat sempre a través de relay"), + ("Connect via relay", ""), ("Always connect via relay", "Connecta sempre a través de relay"), ("whitelist_tip", ""), ("Login", "Inicia sessió"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 64c37709a..7dea516ba 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "被对方手动关闭"), ("Enable remote configuration modification", "允许远程修改配置"), ("Run without install", "无安装运行"), - ("Always connected via relay", "强制走中继连接"), + ("Connect via relay", "中继连接"), ("Always connect via relay", "强制走中继连接"), ("whitelist_tip", "只有白名单里的ip才能访问我"), ("Login", "登录"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", "语音通话"), ("Text chat", "文字聊天"), ("Stop voice call", "停止语音聊天"), + ("relay_hint_tip", "可能无法直连,可以尝试中继连接。\n另外,如果想直接使用中继连接,可以在ID后面添加/r,或者在卡片选项里选择强制走中继连接。"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 70a3eb6c7..97a3ebc48 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Ručně ukončeno protějškem"), ("Enable remote configuration modification", "Umožnit upravování nastavení vzdáleného"), ("Run without install", "Spustit bez instalování"), - ("Always connected via relay", "Vždy spojováno prostřednictvím brány pro předávání (relay)"), + ("Connect via relay", ""), ("Always connect via relay", "Vždy se spojovat prostřednictvím brány pro předávání (relay)"), ("whitelist_tip", "Přístup je umožněn pouze z IP adres, nacházejících se na seznamu povolených"), ("Login", "Přihlásit se"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index ae943e1e8..bab81914e 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Manuelt lukket af peer"), ("Enable remote configuration modification", "Tillad at ændre afstandskonfigurationen"), ("Run without install", "Kør uden installation"), - ("Always connected via relay", "Tilslut altid via relæ-server"), + ("Connect via relay", ""), ("Always connect via relay", "Forbindelse via relæ-server"), ("whitelist_tip", "Kun IP'er på udgivelseslisten kan få adgang til mig"), ("Login", "Login"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 1743505cc..05d02dd58 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Von der Gegenstelle manuell geschlossen"), ("Enable remote configuration modification", "Änderung der Konfiguration aus der Ferne zulassen"), ("Run without install", "Ohne Installation ausführen"), - ("Always connected via relay", "Immer über Relay-Server verbunden"), + ("Connect via relay", ""), ("Always connect via relay", "Immer über Relay-Server verbinden"), ("whitelist_tip", "Nur IPs auf der Whitelist können zugreifen."), ("Login", "Anmelden"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", "Sprachanruf"), ("Text chat", "Text-Chat"), ("Stop voice call", "Sprachanruf beenden"), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/en.rs b/src/lang/en.rs index 37c08a974..4bfa86349 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -42,6 +42,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("request_elevation_tip","You can also request elevation if there is someone on the remote side."), ("wait_accept_uac_tip","Please wait for the remote user to accept the UAC dialog."), ("still_click_uac_tip", "Still requires the remote user to click OK on the UAC window of running RustDesk."), - ("config_microphone", "In order to speak remotely, you need to grant RustDesk \"Record Audio\" permissions.") + ("config_microphone", "In order to speak remotely, you need to grant RustDesk \"Record Audio\" permissions."), + ("relay_hint_tip", "It may not be possible to connect directly, you can try to connect via relay. \nIn addition, if you want to use relay on your first try, you can add the \"/r\" suffix to the ID, or select the option \"Always connect via relay\" in the peer card."), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index f457833f8..47eeb3367 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Manuale fermita de la samtavolano"), ("Enable remote configuration modification", "Permesi foran redaktadon de la konfiguracio"), ("Run without install", "Plenumi sen instali"), - ("Always connected via relay", "Ĉiam konektata per relajso"), + ("Connect via relay", ""), ("Always connect via relay", "Ĉiam konekti per relajso"), ("whitelist_tip", "Nur la IP en la blanka listo povas kontroli mian komputilon"), ("Login", "Konekti"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 939a4831f..4634cea81 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Cerrado manualmente por el par"), ("Enable remote configuration modification", "Habilitar modificación remota de configuración"), ("Run without install", "Ejecutar sin instalar"), - ("Always connected via relay", "Siempre conectado a través de relay"), + ("Connect via relay", ""), ("Always connect via relay", "Conéctese siempre a través de relay"), ("whitelist_tip", "Solo las direcciones IP autorizadas pueden conectarse a este escritorio"), ("Login", "Iniciar sesión"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", "Llamada de voz"), ("Text chat", "Chat de texto"), ("Stop voice call", "Detener llamada de voz"), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 8413673a1..2d0f29a5b 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "به صورت دستی توسط میزبان بسته شد"), ("Enable remote configuration modification", "فعال بودن اعمال تغییرات پیکربندی از راه دور"), ("Run without install", "بدون نصب اجرا شود"), - ("Always connected via relay", "متصل است Relay همیشه با"), + ("Connect via relay", ""), ("Always connect via relay", "برای اتصال استفاده شود Relay از"), ("whitelist_tip", "های مجاز می توانند به این دسکتاپ متصل شوند IP فقط"), ("Login", "ورود"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", "تماس صوتی"), ("Text chat", "گفتگو متنی (چت متنی)"), ("Stop voice call", "توقف تماس صوتی"), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 39ee3bc7f..4e0e79aa0 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Fermé manuellement par le pair"), ("Enable remote configuration modification", "Autoriser la modification de la configuration à distance"), ("Run without install", "Exécuter sans installer"), - ("Always connected via relay", "Forcer la connexion relais"), + ("Connect via relay", ""), ("Always connect via relay", "Forcer la connexion relais"), ("whitelist_tip", "Seule une IP de la liste blanche peut accéder à mon appareil"), ("Login", "Connexion"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/gr.rs b/src/lang/gr.rs index 7cb678ecc..09284738a 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Έκλεισε από τον απομακρυσμένο σταθμό"), ("Enable remote configuration modification", "Ενεργοποίηση απομακρυσμένης τροποποίησης ρυθμίσεων"), ("Run without install", "Εκτέλεση χωρίς εγκατάσταση"), - ("Always connected via relay", "Πάντα συνδεδεμένο μέσω αναμετάδοσης"), + ("Connect via relay", ""), ("Always connect via relay", "Σύνδεση πάντα μέσω αναμετάδοσης"), ("whitelist_tip", "Μόνο οι IP της λίστας επιτρεπόμενων έχουν πρόσβαση"), ("Login", "Σύνδεση"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 25562f556..16c99d207 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "A kapcsolatot a másik fél manuálisan bezárta"), ("Enable remote configuration modification", "Távoli konfiguráció módosítás engedélyezése"), ("Run without install", "Futtatás feltelepítés nélkül"), - ("Always connected via relay", "Mindig közvetítőn keresztül csatlakozik"), + ("Connect via relay", ""), ("Always connect via relay", "Mindig közvetítőn keresztüli csatlakozás"), ("whitelist_tip", "Csak az engedélyezési listán szereplő címek csatlakozhatnak"), ("Login", "Belépés"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 68a80e540..f4be0396f 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Ditutup secara manual oleh peer"), ("Enable remote configuration modification", "Aktifkan modifikasi konfigurasi jarak jauh"), ("Run without install", "Jalankan tanpa menginstal"), - ("Always connected via relay", "Selalu terhubung melalui relai"), + ("Connect via relay", ""), ("Always connect via relay", "Selalu terhubung melalui relai"), ("whitelist_tip", "Hanya whitelisted IP yang dapat mengakses saya"), ("Login", "Masuk"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index a4ea58304..15f7b977f 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Chiuso manualmente dal peer"), ("Enable remote configuration modification", "Abilita la modifica remota della configurazione"), ("Run without install", "Esegui senza installare"), - ("Always connected via relay", "Connesso sempre tramite relay"), + ("Connect via relay", ""), ("Always connect via relay", "Collegati sempre tramite relay"), ("whitelist_tip", "Solo gli indirizzi IP autorizzati possono connettersi a questo desktop"), ("Login", "Accedi"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", "Chiamata vocale"), ("Text chat", "Chat testuale"), ("Stop voice call", "Interrompi la chiamata vocale"), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 7069c0daf..acf1c9b96 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "相手が手動で切断しました"), ("Enable remote configuration modification", "リモート設定変更を有効化"), ("Run without install", "インストールせずに実行"), - ("Always connected via relay", "常に中継サーバー経由で接続"), + ("Connect via relay", ""), ("Always connect via relay", "常に中継サーバー経由で接続"), ("whitelist_tip", "ホワイトリストに登録されたIPからのみ接続を許可します"), ("Login", "ログイン"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 43eb552d3..e1bc43182 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "다른 사용자에 의해 종료됨"), ("Enable remote configuration modification", "원격 구성 변경 활성화"), ("Run without install", "설치 없이 실행"), - ("Always connected via relay", "항상 relay를 통해 접속됨"), + ("Connect via relay", ""), ("Always connect via relay", "항상 relay를 통해 접속하기"), ("whitelist_tip", "화이트리스트에 있는 IP만 현 데스크탑에 접속 가능합니다"), ("Login", "로그인"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 49c7b9916..488290537 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Пир қолымен жабылған"), ("Enable remote configuration modification", "Қашықтан қалыптарды өзгертуді іске қосу"), ("Run without install", "Орнатпай-ақ Іске қосу"), - ("Always connected via relay", "Әрқашан да релай сербері арқылы қосулы"), + ("Connect via relay", ""), ("Always connect via relay", "Әрқашан да релай сербері арқылы қосылу"), ("whitelist_tip", "Маған тек ақ-тізімделген IP қол жеткізе алады"), ("Login", "Кіру"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 41239961a..e6ba5b171 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Połączenie zakończone ręcznie przez peer"), ("Enable remote configuration modification", "Włącz zdalną modyfikację konfiguracji"), ("Run without install", "Uruchom bez instalacji"), - ("Always connected via relay", "Zawsze połączony pośrednio"), + ("Connect via relay", ""), ("Always connect via relay", "Zawsze łącz pośrednio"), ("whitelist_tip", "Zezwalaj na łączenie z tym komputerem tylko z adresów IP znajdujących się na białej liście"), ("Login", "Zaloguj"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index e69a140c9..a1ad932b1 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Fechada manualmente pelo destino"), ("Enable remote configuration modification", "Habilitar modificações de configuração remotas"), ("Run without install", "Executar sem instalar"), - ("Always connected via relay", "Sempre conectado via relay"), + ("Connect via relay", ""), ("Always connect via relay", "Sempre conectar via relay"), ("whitelist_tip", "Somente IPs na whitelist podem me acessar"), ("Login", "Login"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 0887a5915..5ece46006 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Fechada manualmente pelo parceiro"), ("Enable remote configuration modification", "Habilitar modificações de configuração remotas"), ("Run without install", "Executar sem instalar"), - ("Always connected via relay", "Sempre conectado via relay"), + ("Connect via relay", ""), ("Always connect via relay", "Sempre conectar via relay"), ("whitelist_tip", "Somente IPs confiáveis podem me acessar"), ("Login", "Login"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 304353d42..e9b83e298 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Închis manual de dispozitivul pereche"), ("Enable remote configuration modification", "Activează modificarea configurației de la distanță"), ("Run without install", "Rulează fără instalare"), - ("Always connected via relay", "Se conectează mereu prin retransmisie"), + ("Connect via relay", ""), ("Always connect via relay", "Se conectează mereu prin retransmisie"), ("whitelist_tip", "Doar adresele IP autorizate pot accesa acest dispozitiv"), ("Login", "Conectare"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 1792eccce..a8ef18d8a 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Закрыто удалённым узлом вручную"), ("Enable remote configuration modification", "Разрешить удалённое изменение конфигурации"), ("Run without install", "Запустить без установки"), - ("Always connected via relay", "Всегда подключается через ретрансляционный сервер"), + ("Connect via relay", ""), ("Always connect via relay", "Всегда подключаться через ретрансляционный сервер"), ("whitelist_tip", "Только IP-адреса из белого списка могут получить доступ ко мне"), ("Login", "Войти"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", "Голосовой вызов"), ("Text chat", "Текстовый чат"), ("Stop voice call", "Завершить голосовой вызов"), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 6f6f7a18e..47a795342 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Manuálne ukončené opačnou stranou pripojenia"), ("Enable remote configuration modification", "Povoliť zmeny konfigurácie zo vzdialeného PC"), ("Run without install", "Spustiť bez inštalácie"), - ("Always connected via relay", "Vždy pripojené cez prepájací server"), + ("Connect via relay", ""), ("Always connect via relay", "Vždy pripájať cez prepájací server"), ("whitelist_tip", "Len vymenované IP adresy majú oprávnenie sa pripojiť k vzdialenej správe"), ("Login", "Prihlásenie"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 2fb74fa5d..1eb33b970 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Povezavo ročno prekinil odjemalec"), ("Enable remote configuration modification", "Omogoči oddaljeno spreminjanje nastavitev"), ("Run without install", "Zaženi brez namestitve"), - ("Always connected via relay", "Vedno povezan preko posrednika"), + ("Connect via relay", ""), ("Always connect via relay", "Vedno poveži preko posrednika"), ("whitelist_tip", "Dostop je možen samo iz dovoljenih IPjev"), ("Login", "Prijavi"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 5d4a6e1ad..1ade9757a 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "E mbyllur manualisht nga peer"), ("Enable remote configuration modification", "Aktivizoni modifikimin e konfigurimit në distancë"), ("Run without install", "Ekzekuto pa instaluar"), - ("Always connected via relay", "Gjithmonë i ldihur me transmetues"), + ("Connect via relay", ""), ("Always connect via relay", "Gjithmonë lidheni me transmetues"), ("whitelist_tip", "Vetëm IP e listës së bardhë mund të më aksesoj."), ("Login", "Hyrje"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 31a3ade8f..e5704093d 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Klijent ručno raskinuo konekciju"), ("Enable remote configuration modification", "Dozvoli modifikaciju udaljene konfiguracije"), ("Run without install", "Pokreni bez instalacije"), - ("Always connected via relay", "Uvek spojne preko posrednika"), + ("Connect via relay", ""), ("Always connect via relay", "Uvek se spoj preko posrednika"), ("whitelist_tip", "Samo dozvoljene IP mi mogu pristupiti"), ("Login", "Prijava"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index e30c09e44..063892074 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Stängd manuellt av klienten"), ("Enable remote configuration modification", "Tillåt fjärrkonfigurering"), ("Run without install", "Kör utan installation"), - ("Always connected via relay", "Anslut alltid via relay"), + ("Connect via relay", ""), ("Always connect via relay", "Anslut alltid via relay"), ("whitelist_tip", "Bara vitlistade IPs kan koppla upp till mig"), ("Login", "Logga in"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index b88618074..4190ba399 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", ""), ("Enable remote configuration modification", ""), ("Run without install", ""), - ("Always connected via relay", ""), + ("Connect via relay", ""), ("Always connect via relay", ""), ("whitelist_tip", ""), ("Login", ""), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index 1c75aaae7..629c5ac77 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "ถูกปิดโดยอีกฝั่งการการเชื่อมต่อ"), ("Enable remote configuration modification", "เปิดการใช้งานการแก้ไขการตั้งค่าปลายทาง"), ("Run without install", "ใช้งานโดยไม่ต้องติดตั้ง"), - ("Always connected via relay", "เชื่อมต่อผ่านรีเลย์เสมอ"), + ("Connect via relay", ""), ("Always connect via relay", "เชื่อมต่อผ่านรีเลย์เสมอ"), ("whitelist_tip", "อนุญาตเฉพาะการเชื่อมต่อจาก IP ที่ไวท์ลิสต์"), ("Login", "เข้าสู่ระบบ"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index a9e2c1715..b683fb78a 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Eş tarafından manuel olarak kapatıldı"), ("Enable remote configuration modification", "Uzaktan yapılandırma değişikliğini etkinleştir"), ("Run without install", "Yüklemeden çalıştır"), - ("Always connected via relay", "Her zaman röle ile bağlı"), + ("Connect via relay", ""), ("Always connect via relay", "Always connect via relay"), ("whitelist_tip", "Bu masaüstüne yalnızca yetkili IP adresleri bağlanabilir"), ("Login", "Giriş yap"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 7c49a29a2..e4957e3d7 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "由對方手動關閉"), ("Enable remote configuration modification", "啟用遠端更改設定"), ("Run without install", "跳過安裝直接執行"), - ("Always connected via relay", "一律透過轉送連線"), + ("Connect via relay", ""), ("Always connect via relay", "一律透過轉送連線"), ("whitelist_tip", "只有白名單中的 IP 可以存取"), ("Login", "登入"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 92c99d90c..3c1d7776a 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Закрито вузлом вручну"), ("Enable remote configuration modification", "Дозволити віддалену зміну конфігурації"), ("Run without install", "Запустити без установки"), - ("Always connected via relay", "Завжди підключений через ретрансляційний сервер"), + ("Connect via relay", ""), ("Always connect via relay", "Завжди підключатися через ретрансляційний сервер"), ("whitelist_tip", "Тільки IP-адреси з білого списку можуть отримати доступ до мене"), ("Login", "Увійти"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 8bb1d45e9..76f611429 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Đóng thủ công bởi peer"), ("Enable remote configuration modification", "Cho phép thay đổi cấu hình bên từ xa"), ("Run without install", "Chạy mà không cần cài"), - ("Always connected via relay", "Luôn đuợc kết nối qua relay"), + ("Connect via relay", ""), ("Always connect via relay", "Luôn kết nối qua relay"), ("whitelist_tip", "Chỉ có những IP đựoc cho phép mới có thể truy cập"), ("Login", "Đăng nhập"), @@ -449,5 +449,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", ""), ("Text chat", ""), ("Stop voice call", ""), + ("relay_hint_tip", ""), ].iter().cloned().collect(); } diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 447c2e31d..1725a8f41 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -1,4 +1,3 @@ -use std::sync::RwLock; use std::{ collections::HashMap, ops::{Deref, DerefMut}, @@ -15,7 +14,6 @@ use sciter::{ Value, }; -use hbb_common::tokio::io::AsyncReadExt; use hbb_common::{ allow_err, fs::TransferJobMeta, log, message_proto::*, rendezvous_proto::ConnType, }; @@ -348,7 +346,7 @@ impl sciter::EventHandler for SciterSession { let site = AssetPtr::adopt(ptr as *mut video_destination); log::debug!("[video] start video"); *VIDEO.lock().unwrap() = Some(site); - self.reconnect(); + self.reconnect(false); } } BEHAVIOR_EVENTS::VIDEO_INITIALIZED => { @@ -397,7 +395,7 @@ impl sciter::EventHandler for SciterSession { fn transfer_file(); fn tunnel(); fn lock_screen(); - fn reconnect(); + fn reconnect(bool); fn get_chatbox(); fn get_icon(); fn get_home_dir(); diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 25c15f52f..97db904d4 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -1,29 +1,30 @@ use std::collections::HashMap; use std::ops::{Deref, DerefMut}; use std::str::FromStr; -use std::sync::{Arc, Mutex, RwLock}; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; +use std::sync::{Arc, Mutex, RwLock}; +use std::time::Duration; use async_trait::async_trait; use bytes::Bytes; use rdev::{Event, EventType::*}; use uuid::Uuid; -use hbb_common::{allow_err, message_proto::*}; -use hbb_common::{fs, get_version_number, log, Stream}; use hbb_common::config::{Config, LocalConfig, PeerConfig, RS_PUB_KEY}; use hbb_common::rendezvous_proto::ConnType; use hbb_common::tokio::{self, sync::mpsc}; +use hbb_common::{allow_err, message_proto::*}; +use hbb_common::{fs, get_version_number, log, Stream}; -use crate::{client::Data, client::Interface}; -use crate::client::{ - check_if_retry, FileManager, handle_hash, handle_login_error, handle_login_from_ui, - handle_test_delay, input_os_password, Key, KEY_MAP, load_config, LoginConfigHandler, - QualityStatus, send_mouse, start_video_audio_threads, -}; use crate::client::io_loop::Remote; +use crate::client::{ + check_if_retry, handle_hash, handle_login_error, handle_login_from_ui, handle_test_delay, + input_os_password, load_config, send_mouse, start_video_audio_threads, FileManager, Key, + LoginConfigHandler, QualityStatus, KEY_MAP, +}; use crate::common::{self, GrabState}; use crate::keyboard; +use crate::{client::Data, client::Interface}; pub static IS_IN: AtomicBool = AtomicBool::new(false); @@ -531,9 +532,13 @@ impl Session { } } - pub fn reconnect(&self) { + pub fn reconnect(&self, force_relay: bool) { self.send(Data::Close); let cloned = self.clone(); + // override only if true + if true == force_relay { + cloned.lc.write().unwrap().force_relay = true; + } let mut lock = self.thread.lock().unwrap(); lock.take().map(|t| t.join()); *lock = Some(std::thread::spawn(move || { @@ -674,10 +679,42 @@ impl Session { pub fn request_voice_call(&self) { self.send(Data::NewVoiceCall); } - + pub fn close_voice_call(&self) { self.send(Data::CloseVoiceCall); } + + pub fn show_relay_hint( + &mut self, + last_recv_time: tokio::time::Instant, + msgtype: &str, + title: &str, + text: &str, + ) -> bool { + let duration = Duration::from_secs(3); + let counter_interval = 3; + let lock = self.lc.read().unwrap(); + let success_time = lock.success_time; + let direct = lock.direct.unwrap_or(false); + let received = lock.received; + drop(lock); + if let Some(success_time) = success_time { + if direct && last_recv_time.duration_since(success_time) < duration { + let retry_for_relay = direct && !received; + let retry = check_if_retry(msgtype, title, text, retry_for_relay); + if retry && !retry_for_relay { + self.lc.write().unwrap().direct_error_counter += 1; + if self.lc.read().unwrap().direct_error_counter % counter_interval == 0 { + #[cfg(feature = "flutter")] + return true; + } + } + } else { + self.lc.write().unwrap().direct_error_counter = 0; + } + } + false + } } pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { @@ -813,6 +850,7 @@ impl Interface for Session { "Connected, waiting for image...", "", ); + self.lc.write().unwrap().success_time = Some(tokio::time::Instant::now()); } self.on_connected(self.lc.read().unwrap().conn_type); #[cfg(windows)] @@ -958,7 +996,7 @@ pub async fn io_loop(handler: Session) { let frame_count = Arc::new(AtomicUsize::new(0)); let frame_count_cl = frame_count.clone(); let ui_handler = handler.ui_handler.clone(); - let (video_sender, audio_sender) = start_video_audio_threads(move |data: &mut Vec | { + let (video_sender, audio_sender) = start_video_audio_threads(move |data: &mut Vec| { frame_count_cl.fetch_add(1, Ordering::Relaxed); ui_handler.on_rgba(data); }); From 491317bd6fe5cd931e61215a0c40e101a705054b Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Tue, 14 Feb 2023 13:57:33 +0100 Subject: [PATCH 1864/2015] modernized menu bar --- flutter/assets/actions.svg | 3 + flutter/assets/chat.svg | 3 +- flutter/assets/close.svg | 2 + flutter/assets/display.svg | 2 + flutter/assets/fullscreen.svg | 2 + flutter/assets/fullscreen_exit.svg | 2 + flutter/assets/keyboard.svg | 2 + flutter/assets/pinned.svg | 2 + flutter/assets/rec.svg | 2 + flutter/assets/unpinned.svg | 2 + .../lib/desktop/widgets/remote_menubar.dart | 227 +++++++++--------- 11 files changed, 138 insertions(+), 111 deletions(-) create mode 100644 flutter/assets/actions.svg create mode 100644 flutter/assets/close.svg create mode 100644 flutter/assets/display.svg create mode 100644 flutter/assets/fullscreen.svg create mode 100644 flutter/assets/fullscreen_exit.svg create mode 100644 flutter/assets/keyboard.svg create mode 100644 flutter/assets/pinned.svg create mode 100644 flutter/assets/rec.svg create mode 100644 flutter/assets/unpinned.svg diff --git a/flutter/assets/actions.svg b/flutter/assets/actions.svg new file mode 100644 index 000000000..feaf416cd --- /dev/null +++ b/flutter/assets/actions.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/flutter/assets/chat.svg b/flutter/assets/chat.svg index 03491be6e..830ef0d33 100644 --- a/flutter/assets/chat.svg +++ b/flutter/assets/chat.svg @@ -1 +1,2 @@ - \ No newline at end of file + + \ No newline at end of file diff --git a/flutter/assets/close.svg b/flutter/assets/close.svg new file mode 100644 index 000000000..1e9a30711 --- /dev/null +++ b/flutter/assets/close.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/flutter/assets/display.svg b/flutter/assets/display.svg new file mode 100644 index 000000000..8a87116ff --- /dev/null +++ b/flutter/assets/display.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/flutter/assets/fullscreen.svg b/flutter/assets/fullscreen.svg new file mode 100644 index 000000000..73d79cf0e --- /dev/null +++ b/flutter/assets/fullscreen.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/flutter/assets/fullscreen_exit.svg b/flutter/assets/fullscreen_exit.svg new file mode 100644 index 000000000..f2b3ae27b --- /dev/null +++ b/flutter/assets/fullscreen_exit.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/flutter/assets/keyboard.svg b/flutter/assets/keyboard.svg new file mode 100644 index 000000000..569c68727 --- /dev/null +++ b/flutter/assets/keyboard.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/flutter/assets/pinned.svg b/flutter/assets/pinned.svg new file mode 100644 index 000000000..2563015f7 --- /dev/null +++ b/flutter/assets/pinned.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/flutter/assets/rec.svg b/flutter/assets/rec.svg new file mode 100644 index 000000000..14546b971 --- /dev/null +++ b/flutter/assets/rec.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/flutter/assets/unpinned.svg b/flutter/assets/unpinned.svg new file mode 100644 index 000000000..ba4ab5328 --- /dev/null +++ b/flutter/assets/unpinned.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 6bb49000b..77d687d93 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -405,9 +405,10 @@ class _RemoteMenubarState extends State { Widget _buildMenubar(BuildContext context) { final List menubarItems = []; + final double iconSize = Theme.of(context).iconTheme.size ?? 30.0; if (!isWebDesktop) { - menubarItems.add(_buildPinMenubar(context)); - menubarItems.add(_buildFullscreen(context)); + menubarItems.add(_buildPinMenubar(context, iconSize)); + menubarItems.add(_buildFullscreen(context, iconSize)); if (widget.ffi.ffiModel.isPeerAndroid) { menubarItems.add(IconButton( tooltip: translate('Mobile Actions'), @@ -420,77 +421,84 @@ class _RemoteMenubarState extends State { )); } } - menubarItems.add(_buildMonitor(context)); - menubarItems.add(_buildControl(context)); - menubarItems.add(_buildDisplay(context)); - menubarItems.add(_buildKeyboard(context)); + menubarItems.add(_buildMonitor(context, iconSize)); + menubarItems.add(_buildControl(context, iconSize)); + menubarItems.add(_buildDisplay(context, iconSize)); + menubarItems.add(_buildKeyboard(context, iconSize)); if (!isWeb) { - menubarItems.add(_buildChat(context)); - menubarItems.add(_buildVoiceCall(context)); + menubarItems.add(_buildChat(context, iconSize)); + menubarItems.add(_buildVoiceCall(context, iconSize)); } - menubarItems.add(_buildRecording(context)); - menubarItems.add(_buildClose(context)); + menubarItems.add(_buildRecording(context, iconSize)); + menubarItems.add(_buildClose(context, iconSize)); return PopupMenuTheme( - data: const PopupMenuThemeData( - textStyle: TextStyle(color: _MenubarTheme.commonColor)), - child: Column(mainAxisSize: MainAxisSize.min, children: [ + data: const PopupMenuThemeData( + textStyle: TextStyle(color: _MenubarTheme.commonColor)), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ Container( - decoration: BoxDecoration( - color: Colors.white, - border: Border.all(color: MyTheme.border), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical( + bottom: Radius.circular(10), ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: menubarItems, - )), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: menubarItems, + ), + ), _buildDraggableShowHide(context), - ])); + ], + ), + ); } - Widget _buildPinMenubar(BuildContext context) { - return Obx(() => IconButton( - tooltip: translate(pin ? 'Unpin menubar' : 'Pin menubar'), - onPressed: () { - widget.state.switchPin(); - }, - icon: Obx(() => Transform.rotate( - angle: pin ? math.pi / 4 : 0, - child: Icon( - Icons.push_pin, - color: pin ? _MenubarTheme.commonColor : Colors.grey, - ))), - )); + Widget _buildPinMenubar(BuildContext context, double iconSize) { + return Obx( + () => IconButton( + padding: EdgeInsets.zero, + iconSize: iconSize, + tooltip: translate(pin ? 'Unpin menubar' : 'Pin menubar'), + onPressed: () { + widget.state.switchPin(); + }, + icon: SvgPicture.asset( + pin ? "assets/pinned.svg" : "assets/unpinned.svg", + color: pin ? _MenubarTheme.commonColor : Colors.grey[800], + ), + ), + ); } - Widget _buildFullscreen(BuildContext context) { + Widget _buildFullscreen(BuildContext context, double iconSize) { return IconButton( + padding: EdgeInsets.zero, + iconSize: iconSize, tooltip: translate(isFullscreen ? 'Exit Fullscreen' : 'Fullscreen'), onPressed: () { _setFullscreen(!isFullscreen); }, - icon: isFullscreen - ? const Icon( - Icons.fullscreen_exit, - color: _MenubarTheme.commonColor, - ) - : const Icon( - Icons.fullscreen, - color: _MenubarTheme.commonColor, - ), + icon: SvgPicture.asset( + isFullscreen ? "assets/fullscreen_exit.svg" : "assets/fullscreen.svg", + color: _MenubarTheme.commonColor, + ), ); } - Widget _buildMonitor(BuildContext context) { + Widget _buildMonitor(BuildContext context, double iconSize) { final pi = widget.ffi.ffiModel.pi; return mod_menu.PopupMenuButton( + iconSize: iconSize, tooltip: translate('Select Monitor'), padding: EdgeInsets.zero, position: mod_menu.PopupMenuPosition.under, icon: Stack( alignment: Alignment.center, children: [ - const Icon( - Icons.personal_video, + SvgPicture.asset( + "assets/display.svg", color: _MenubarTheme.commonColor, ), Padding( @@ -499,8 +507,7 @@ class _RemoteMenubarState extends State { RxInt display = CurrentDisplayState.find(widget.id); return Text( '${display.value + 1}/${pi.displays.length}', - style: const TextStyle( - color: _MenubarTheme.commonColor, fontSize: 8), + style: const TextStyle(color: Colors.white, fontSize: 8), ); }), ) @@ -513,23 +520,22 @@ class _RemoteMenubarState extends State { Stack( alignment: Alignment.center, children: [ - const Icon( - Icons.personal_video, - color: _MenubarTheme.commonColor, - ), + SvgPicture.asset("assets/display.svg"), TextButton( child: Container( - alignment: AlignmentDirectional.center, - constraints: - const BoxConstraints(minHeight: _MenubarTheme.height), - child: Padding( - padding: const EdgeInsets.only(bottom: 2.5), - child: Text( - (i + 1).toString(), - style: - const TextStyle(color: _MenubarTheme.commonColor), + alignment: AlignmentDirectional.center, + constraints: + const BoxConstraints(minHeight: _MenubarTheme.height), + child: Padding( + padding: const EdgeInsets.only(bottom: 2.5), + child: Text( + (i + 1).toString(), + style: TextStyle( + color: Theme.of(context).scaffoldBackgroundColor, ), - )), + ), + ), + ), onPressed: () { if (Navigator.canPop(context)) { Navigator.pop(context); @@ -561,11 +567,12 @@ class _RemoteMenubarState extends State { ); } - Widget _buildControl(BuildContext context) { + Widget _buildControl(BuildContext context, double iconSize) { return mod_menu.PopupMenuButton( + iconSize: iconSize, padding: EdgeInsets.zero, - icon: const Icon( - Icons.bolt, + icon: SvgPicture.asset( + "assets/actions.svg", color: _MenubarTheme.commonColor, ), tooltip: translate('Control Actions'), @@ -583,7 +590,7 @@ class _RemoteMenubarState extends State { ); } - Widget _buildDisplay(BuildContext context) { + Widget _buildDisplay(BuildContext context, double iconSize) { return FutureBuilder(future: () async { widget.state.viewStyle.value = await bind.sessionGetViewStyle(id: widget.id) ?? ''; @@ -595,9 +602,10 @@ class _RemoteMenubarState extends State { return Obx(() { final remoteCount = RemoteCountState.find().value; return mod_menu.PopupMenuButton( + iconSize: iconSize, padding: EdgeInsets.zero, - icon: const Icon( - Icons.tv, + icon: SvgPicture.asset( + "assets/display.svg", color: _MenubarTheme.commonColor, ), tooltip: translate('Display Settings'), @@ -622,15 +630,16 @@ class _RemoteMenubarState extends State { }); } - Widget _buildKeyboard(BuildContext context) { + Widget _buildKeyboard(BuildContext context, double iconSize) { FfiModel ffiModel = Provider.of(context); if (ffiModel.permissions['keyboard'] == false) { return Offstage(); } return mod_menu.PopupMenuButton( + iconSize: iconSize, padding: EdgeInsets.zero, - icon: const Icon( - Icons.keyboard, + icon: SvgPicture.asset( + "assets/keyboard.svg", color: _MenubarTheme.commonColor, ), tooltip: translate('Keyboard Settings'), @@ -648,57 +657,54 @@ class _RemoteMenubarState extends State { ); } - Widget _buildRecording(BuildContext context) { + Widget _buildRecording(BuildContext context, double iconSize) { return Consumer(builder: ((context, value, child) { if (value.permissions['recording'] != false) { return Consumer( - builder: (context, value, child) => IconButton( - tooltip: value.start - ? translate('Stop session recording') - : translate('Start session recording'), - onPressed: () => value.toggle(), - icon: value.start - ? Icon( - Icons.pause_circle_filled, - color: _MenubarTheme.commonColor, - ) - : SvgPicture.asset( - "assets/record_screen.svg", - color: _MenubarTheme.commonColor, - width: Theme.of(context).iconTheme.size ?? 22.0, - height: Theme.of(context).iconTheme.size ?? 22.0, - ), - )); + builder: (context, value, child) => IconButton( + padding: EdgeInsets.zero, + iconSize: iconSize, + tooltip: value.start + ? translate('Stop session recording') + : translate('Start session recording'), + onPressed: () => value.toggle(), + icon: SvgPicture.asset( + "assets/rec.svg", + color: value.start ? Colors.red : _MenubarTheme.commonColor, + ), + ), + ); } else { return Offstage(); } })); } - Widget _buildClose(BuildContext context) { + Widget _buildClose(BuildContext context, double iconSize) { return IconButton( + iconSize: iconSize, + padding: EdgeInsets.zero, tooltip: translate('Close'), onPressed: () { clientClose(widget.id, widget.ffi.dialogManager); }, - icon: const Icon( - Icons.close, - color: _MenubarTheme.commonColor, + icon: SvgPicture.asset( + "assets/close.svg", + color: Colors.red, ), ); } final _chatButtonKey = GlobalKey(); - Widget _buildChat(BuildContext context) { + Widget _buildChat(BuildContext context, double iconSize) { FfiModel ffiModel = Provider.of(context); return mod_menu.PopupMenuButton( + iconSize: iconSize, key: _chatButtonKey, padding: EdgeInsets.zero, icon: SvgPicture.asset( "assets/chat.svg", color: _MenubarTheme.commonColor, - width: Theme.of(context).iconTheme.size ?? 24.0, - height: Theme.of(context).iconTheme.size ?? 24.0, ), tooltip: translate('Chat'), position: mod_menu.PopupMenuPosition.under, @@ -719,15 +725,14 @@ class _RemoteMenubarState extends State { switch (widget.ffi.chatModel.voiceCallStatus.value) { case VoiceCallStatus.waitingForResponse: return IconButton( - onPressed: () { - widget.ffi.chatModel.closeVoiceCall(widget.id); - }, - icon: SvgPicture.asset( - "assets/voice_call_waiting.svg", - color: Colors.red, - width: Theme.of(context).iconTheme.size ?? 20.0, - height: Theme.of(context).iconTheme.size ?? 20.0, - )); + onPressed: () { + widget.ffi.chatModel.closeVoiceCall(widget.id); + }, + icon: SvgPicture.asset( + "assets/voice_call_waiting.svg", + color: Colors.red, + ), + ); case VoiceCallStatus.connected: return IconButton( onPressed: () { @@ -736,7 +741,6 @@ class _RemoteMenubarState extends State { icon: Icon( Icons.phone_disabled_rounded, color: Colors.red, - size: Theme.of(context).iconTheme.size ?? 22.0, ), ); default: @@ -755,13 +759,14 @@ class _RemoteMenubarState extends State { } } - Widget _buildVoiceCall(BuildContext context) { + Widget _buildVoiceCall(BuildContext context, double iconSize) { return Obx( () { final tooltipText = _getVoiceCallTooltip(); return tooltipText == null ? const Offstage() : IconButton( + iconSize: iconSize, padding: EdgeInsets.zero, icon: _getVoiceCallIcon(), tooltip: translate(tooltipText), @@ -1748,7 +1753,7 @@ class _DraggableShowHideState extends State<_DraggableShowHide> { child: Icon( Icons.drag_indicator, size: 20, - color: Colors.grey, + color: Colors.grey[800], ), feedback: widget, onDragStarted: (() { @@ -1801,7 +1806,9 @@ class _DraggableShowHideState extends State<_DraggableShowHide> { child: Container( decoration: BoxDecoration( color: Colors.white, - border: Border.all(color: MyTheme.border), + borderRadius: BorderRadius.vertical( + bottom: Radius.circular(5), + ), ), child: SizedBox( height: 20, From 50f751c21521fd63985a9123f05bb706c048ba37 Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 10 Feb 2023 09:03:19 +0800 Subject: [PATCH 1865/2015] temp commit Signed-off-by: fufesou --- src/keyboard.rs | 10 ++++++---- src/server/input_service.rs | 3 +++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/keyboard.rs b/src/keyboard.rs index 105b84400..9ca5a16f0 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -759,10 +759,12 @@ pub fn map_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option) { match &event.unicode { Some(unicode_info) => { - for code in &unicode_info.unicode { - let mut evt = key_event.clone(); - evt.set_unicode(*code as _); - events.push(evt); + if let Some(name) = unicode_info.name { + if name.len() > 0 { + let mut evt = key_event.clone(); + evt.set_seq(name); + events.push(evt); + } } } None => {} diff --git a/src/server/input_service.rs b/src/server/input_service.rs index edf0ef497..2b19bbaff 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -1093,6 +1093,9 @@ fn translate_keyboard_mode(evt: &KeyEvent) { #[cfg(target_os = "windows")] allow_err!(rdev::simulate_unicode(_unicode as _)); } + Some(key_event::Union::Seq(seq)) => { + ENIGO.lock().unwrap().key_sequence(&seq); + } Some(key_event::Union::Chr(..)) => { #[cfg(target_os = "windows")] From e24f5e7eed10b321500fb6fdfe64d7e8bb766d87 Mon Sep 17 00:00:00 2001 From: fufesou Date: Mon, 13 Feb 2023 14:55:57 +0800 Subject: [PATCH 1866/2015] mid commit Signed-off-by: fufesou --- libs/hbb_common/protos/message.proto | 2 ++ src/keyboard.rs | 33 ++++------------------------ src/server/input_service.rs | 7 +++--- 3 files changed, 9 insertions(+), 33 deletions(-) diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index ed2706382..7e3d0b0a4 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -201,6 +201,8 @@ message KeyEvent { bool press = 2; oneof union { ControlKey control_key = 3; + // high word, sym key code. win: virtual-key code, linux: keysym ?, macos: + // low word, position key code. win: scancode, linux: key code, macos: key code uint32 chr = 4; uint32 unicode = 5; string seq = 6; diff --git a/src/keyboard.rs b/src/keyboard.rs index 9ca5a16f0..02f34132f 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -785,34 +785,9 @@ fn is_hot_key_modifiers_down() -> bool { return false; } -pub fn translate_virtual_keycode(event: &Event, mut key_event: KeyEvent) -> Option { - match event.event_type { - EventType::KeyPress(..) => { - key_event.down = true; - } - EventType::KeyRelease(..) => { - key_event.down = false; - } - _ => return None, - }; - - let mut peer = get_peer_platform().to_lowercase(); - peer.retain(|c| !c.is_whitespace()); - - // #[cfg(target_os = "windows")] - // let keycode = match peer.as_str() { - // "windows" => event.code, - // "macos" => { - // if hbb_common::config::LocalConfig::get_kb_layout_type() == "ISO" { - // rdev::win_scancode_to_macos_iso_code(event.scan_code)? - // } else { - // rdev::win_scancode_to_macos_code(event.scan_code)? - // } - // } - // _ => rdev::win_scancode_to_linux_code(event.scan_code)?, - // }; - - key_event.set_chr(event.code as _); +pub fn translate_vk_scan_code(event: &Event, mut key_event: KeyEvent) -> Option { + let mut key_event = map_keyboard_mode(event, key_event)?; + key_event.set_chr((key_event.chr() & 0x0000FFFF) | ((event.code as u32) << 16)); Some(key_event) } @@ -853,7 +828,7 @@ pub fn translate_keyboard_mode(event: &Event, key_event: KeyEvent) -> Vec { - #[cfg(target_os = "windows")] - allow_err!(rdev::simulate_unicode(_unicode as _)); - } Some(key_event::Union::Seq(seq)) => { ENIGO.lock().unwrap().key_sequence(&seq); } @@ -1101,6 +1097,9 @@ fn translate_keyboard_mode(evt: &KeyEvent) { #[cfg(target_os = "windows")] translate_process_virtual_keycode(evt.chr(), evt.down) } + Some(key_event::Union::Unicode(..)) => { + // Do not handle unicode for now. + } _ => { log::debug!("Unreachable. Unexpected key event {:?}", &evt); } From 50ce57024c74ed9faab2099ed5c544be4e51e3a4 Mon Sep 17 00:00:00 2001 From: fufesou Date: Mon, 13 Feb 2023 16:26:14 +0800 Subject: [PATCH 1867/2015] macos, win, translate mode, Signed-off-by: fufesou --- src/keyboard.rs | 1 + src/server/input_service.rs | 13 ++++++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/keyboard.rs b/src/keyboard.rs index 02f34132f..8aa5f72d2 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -787,6 +787,7 @@ fn is_hot_key_modifiers_down() -> bool { pub fn translate_vk_scan_code(event: &Event, mut key_event: KeyEvent) -> Option { let mut key_event = map_keyboard_mode(event, key_event)?; + #[cfg(target_os = "windows")] key_event.set_chr((key_event.chr() & 0x0000FFFF) | ((event.code as u32) << 16)); Some(key_event) } diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 0f40cb7d6..18ff433a3 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -1082,9 +1082,14 @@ fn legacy_keyboard_mode(evt: &KeyEvent) { } #[cfg(target_os = "windows")] -fn translate_process_virtual_keycode(vk: u32, down: bool) { +fn translate_process_code(code: u32, down: bool) { crate::platform::windows::try_change_desktop(); - sim_rdev_rawkey_virtual(vk, down); + let vk_code = + + match code >> 16 { + 0 => sim_rdev_rawkey_position(code, down), + vk_code => sim_rdev_rawkey_virtual(vk_code, down), + }; } fn translate_keyboard_mode(evt: &KeyEvent) { @@ -1095,7 +1100,9 @@ fn translate_keyboard_mode(evt: &KeyEvent) { Some(key_event::Union::Chr(..)) => { #[cfg(target_os = "windows")] - translate_process_virtual_keycode(evt.chr(), evt.down) + translate_process_code(evt.chr(), evt.down); + #[cfg(not(target_os = "windows"))] + sim_rdev_rawkey_position(code, down); } Some(key_event::Union::Unicode(..)) => { // Do not handle unicode for now. From 7dfcc401e5d59b53e6243211639ef990cc4a2384 Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 14 Feb 2023 15:42:02 +0800 Subject: [PATCH 1868/2015] translate mode, mac --> win, init debug Signed-off-by: fufesou --- Cargo.lock | 3 +- .../lib/desktop/widgets/remote_menubar.dart | 4 +- src/flutter_ffi.rs | 1 - src/keyboard.rs | 95 ++++++++++++++----- src/server/input_service.rs | 6 +- 5 files changed, 77 insertions(+), 32 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f0f66e287..2fcdef290 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4554,12 +4554,13 @@ dependencies = [ [[package]] name = "rdev" version = "0.5.0-2" -source = "git+https://github.com/fufesou/rdev#cedc4e62744566775026af4b434ef799804c1130" +source = "git+https://github.com/fufesou/rdev#593f0ba37139ed6f4f88a4120e972612ec4b1c6f" dependencies = [ "cocoa", "core-foundation 0.9.3", "core-foundation-sys 0.8.3", "core-graphics 0.22.3", + "dispatch", "enum-map", "epoll", "inotify", diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 6bb49000b..9f8265fec 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -1510,8 +1510,8 @@ class _RemoteMenubarState extends State { if (bind.sessionIsKeyboardModeSupported( id: widget.id, mode: mode.key)) { if (mode.key == 'translate') { - if (!Platform.isWindows || - widget.ffi.ffiModel.pi.platform != kPeerPlatformWindows) { + if (Platform.isLinux || + widget.ffi.ffiModel.pi.platform == kPeerPlatformLinux) { continue; } } diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index b4e79b361..0e307abe3 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -20,7 +20,6 @@ use std::{ os::raw::c_char, str::FromStr, }; -use crate::ui_session_interface::InvokeUiSession; // use crate::hbbs_http::account::AuthResult; diff --git a/src/keyboard.rs b/src/keyboard.rs index 8aa5f72d2..7e4ba2b39 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -18,6 +18,13 @@ use std::{ #[cfg(windows)] static mut IS_ALT_GR: bool = false; +#[allow(dead_code)] +const OS_LOWER_WINDOWS: &str = "windows"; +#[allow(dead_code)] +const OS_LOWER_LINUX: &str = "linux"; +#[allow(dead_code)] +const OS_LOWER_MACOS: &str = "macos"; + #[cfg(any(target_os = "windows", target_os = "macos"))] static KEYBOARD_HOOKED: AtomicBool = AtomicBool::new(false); @@ -202,6 +209,9 @@ pub fn update_grab_get_key_name() { #[cfg(target_os = "windows")] static mut IS_0X021D_DOWN: bool = false; +#[cfg(target_os = "macos")] +static mut IS_LEFT_OPTION_DOWN: bool = false; + pub fn start_grab_loop() { #[cfg(any(target_os = "windows", target_os = "macos"))] std::thread::spawn(move || { @@ -213,6 +223,7 @@ pub fn start_grab_loop() { let mut _keyboard_mode = KeyboardMode::Map; let _scan_code = event.scan_code; + let _code = event.code; let res = if KEYBOARD_HOOKED.load(Ordering::SeqCst) { _keyboard_mode = client::process_event(&event, None); if is_press { @@ -246,6 +257,13 @@ pub fn start_grab_loop() { } } + #[cfg(target_os = "macos")] + unsafe { + if _code as u32 == rdev::kVK_Option { + IS_LEFT_OPTION_DOWN = is_press; + } + } + return res; }; let func = move |event: Event| match event.event_type { @@ -253,11 +271,13 @@ pub fn start_grab_loop() { EventType::KeyRelease(key) => try_handle_keyboard(event, key, false), _ => Some(event), }; + #[cfg(target_os = "macos")] + rdev::set_is_main_thread(false); + #[cfg(target_os = "windows")] + rdev::set_event_popup(false); if let Err(error) = rdev::grab(func) { log::error!("rdev Error: {:?}", error) } - #[cfg(target_os = "windows")] - rdev::set_event_popup(false); }); #[cfg(target_os = "linux")] @@ -395,13 +415,16 @@ pub fn event_to_key_events( _ => {} } + let mut peer = get_peer_platform().to_lowercase(); + peer.retain(|c| !c.is_whitespace()); + key_event.mode = keyboard_mode.into(); let mut key_events = match keyboard_mode { - KeyboardMode::Map => match map_keyboard_mode(event, key_event) { + KeyboardMode::Map => match map_keyboard_mode(peer.as_str(), event, key_event) { Some(event) => [event].to_vec(), None => Vec::new(), }, - KeyboardMode::Translate => translate_keyboard_mode(event, key_event), + KeyboardMode::Translate => translate_keyboard_mode(peer.as_str(), event, key_event), _ => { #[cfg(not(any(target_os = "android", target_os = "ios")))] { @@ -424,7 +447,6 @@ pub fn event_to_key_events( } } } - key_events } @@ -698,7 +720,7 @@ pub fn legacy_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Vec Option { +pub fn map_keyboard_mode(peer: &str, event: &Event, mut key_event: KeyEvent) -> Option { match event.event_type { EventType::KeyPress(..) => { key_event.down = true; @@ -709,12 +731,9 @@ pub fn map_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option return None, }; - let mut peer = get_peer_platform().to_lowercase(); - peer.retain(|c| !c.is_whitespace()); - #[cfg(target_os = "windows")] - let keycode = match peer.as_str() { - "windows" => { + let keycode = match peer { + OS_LOWER_WINDOWS => { // https://github.com/rustdesk/rustdesk/issues/1371 // Filter scancodes that are greater than 255 and the hight word is not 0xE0. if event.scan_code > 255 && (event.scan_code >> 8) != 0xE0 { @@ -722,7 +741,7 @@ pub fn map_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option { + OS_LOWER_MACOS => { if hbb_common::config::LocalConfig::get_kb_layout_type() == "ISO" { rdev::win_scancode_to_macos_iso_code(event.scan_code)? } else { @@ -732,15 +751,15 @@ pub fn map_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option rdev::win_scancode_to_linux_code(event.scan_code)?, }; #[cfg(target_os = "macos")] - let keycode = match peer.as_str() { - "windows" => rdev::macos_code_to_win_scancode(event.code as _)?, - "macos" => event.code as _, + let keycode = match peer { + OS_LOWER_WINDOWS => rdev::macos_code_to_win_scancode(event.code as _)?, + OS_LOWER_MACOS => event.code as _, _ => rdev::macos_code_to_linux_code(event.code as _)?, }; #[cfg(target_os = "linux")] - let keycode = match peer.as_str() { - "windows" => rdev::linux_code_to_win_scancode(event.code as _)?, - "macos" => { + let keycode = match peer { + OS_LOWER_WINDOWS => rdev::linux_code_to_win_scancode(event.code as _)?, + OS_LOWER_MACOS => { if hbb_common::config::LocalConfig::get_kb_layout_type() == "ISO" { rdev::linux_code_to_macos_iso_code(event.code as _)? } else { @@ -759,10 +778,10 @@ pub fn map_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option) { match &event.unicode { Some(unicode_info) => { - if let Some(name) = unicode_info.name { + if let Some(name) = &unicode_info.name { if name.len() > 0 { let mut evt = key_event.clone(); - evt.set_seq(name); + evt.set_seq(name.to_string()); events.push(evt); } } @@ -785,21 +804,42 @@ fn is_hot_key_modifiers_down() -> bool { return false; } -pub fn translate_vk_scan_code(event: &Event, mut key_event: KeyEvent) -> Option { +#[inline] +#[cfg(target_os = "windows")] +pub fn translate_key_code(event: &Event, mut key_event: KeyEvent) -> Option { let mut key_event = map_keyboard_mode(event, key_event)?; - #[cfg(target_os = "windows")] key_event.set_chr((key_event.chr() & 0x0000FFFF) | ((event.code as u32) << 16)); Some(key_event) } -pub fn translate_keyboard_mode(event: &Event, key_event: KeyEvent) -> Vec { +#[inline] +#[cfg(not(target_os = "windows"))] +pub fn translate_key_code(peer: &str, event: &Event, key_event: KeyEvent) -> Option { + map_keyboard_mode(peer, event, key_event) +} + +pub fn translate_keyboard_mode(peer: &str, event: &Event, key_event: KeyEvent) -> Vec { let mut events: Vec = Vec::new(); if let Some(unicode_info) = &event.unicode { if unicode_info.is_dead { + #[cfg(target_os = "macos")] + if peer != OS_LOWER_MACOS && unsafe { IS_LEFT_OPTION_DOWN } { + // try clear dead key state + // rdev::clear_dead_key_state(); + } else { + return events; + } + #[cfg(not(target_os = "macos"))] return events; } } + #[cfg(target_os = "macos")] + // ignore right option key + if event.code as u32 == rdev::kVK_RightOption { + return events; + } + #[cfg(target_os = "windows")] unsafe { if event.scan_code == 0x021D { @@ -825,11 +865,16 @@ pub fn translate_keyboard_mode(event: &Event, key_event: KeyEvent) -> Vec { - ENIGO.lock().unwrap().key_sequence(&seq); + ENIGO.lock().unwrap().key_sequence(seq); } Some(key_event::Union::Chr(..)) => { #[cfg(target_os = "windows")] translate_process_code(evt.chr(), evt.down); #[cfg(not(target_os = "windows"))] - sim_rdev_rawkey_position(code, down); + sim_rdev_rawkey_position(evt.chr(), evt.down); } Some(key_event::Union::Unicode(..)) => { // Do not handle unicode for now. From b2d13647be0a84be2047194f7786346bbbd049f2 Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 14 Feb 2023 15:58:36 +0800 Subject: [PATCH 1869/2015] translate mode, mac --> win, debug 2 Signed-off-by: fufesou --- libs/enigo/src/win/win_impl.rs | 42 ++++++++++++++++------------------ src/keyboard.rs | 4 ++-- src/server/input_service.rs | 2 -- 3 files changed, 22 insertions(+), 26 deletions(-) diff --git a/libs/enigo/src/win/win_impl.rs b/libs/enigo/src/win/win_impl.rs index 2e1108b9e..115cb9789 100644 --- a/libs/enigo/src/win/win_impl.rs +++ b/libs/enigo/src/win/win_impl.rs @@ -39,7 +39,7 @@ fn mouse_event(flags: u32, data: u32, dx: i32, dy: i32) -> DWORD { unsafe { SendInput(1, &mut input as LPINPUT, size_of::() as c_int) } } -fn keybd_event(flags: u32, vk: u16, scan: u16) -> DWORD { +fn keybd_event(mut flags: u32, vk: u16, scan: u16) -> DWORD { let mut scan = scan; unsafe { // https://github.com/rustdesk/rustdesk/issues/366 @@ -52,35 +52,33 @@ fn keybd_event(flags: u32, vk: u16, scan: u16) -> DWORD { scan = MapVirtualKeyExW(vk as _, 0, LAYOUT) as _; } } - let mut input: INPUT = unsafe { std::mem::MaybeUninit::zeroed().assume_init() }; - input.type_ = INPUT_KEYBOARD; + + if flags & KEYEVENTF_UNICODE == 0 { + if scan >> 8 == 0xE0 || scan >> 8 == 0xE1 { + flags |= winapi::um::winuser::KEYEVENTF_EXTENDEDKEY; + } + } + let mut union: INPUT_u = unsafe { std::mem::zeroed() }; unsafe { - let dst_ptr = (&mut input.u as *mut _) as *mut u8; - let flags = match vk as _ { - winapi::um::winuser::VK_HOME | - winapi::um::winuser::VK_UP | - winapi::um::winuser::VK_PRIOR | - winapi::um::winuser::VK_LEFT | - winapi::um::winuser::VK_RIGHT | - winapi::um::winuser::VK_END | - winapi::um::winuser::VK_DOWN | - winapi::um::winuser::VK_NEXT | - winapi::um::winuser::VK_INSERT | - winapi::um::winuser::VK_DELETE => flags | winapi::um::winuser::KEYEVENTF_EXTENDEDKEY, - _ => flags, - }; - - let k = KEYBDINPUT { + *union.ki_mut() = KEYBDINPUT { wVk: vk, wScan: scan, dwFlags: flags, time: 0, dwExtraInfo: ENIGO_INPUT_EXTRA_VALUE, }; - let src_ptr = (&k as *const _) as *const u8; - std::ptr::copy_nonoverlapping(src_ptr, dst_ptr, size_of::()); } - unsafe { SendInput(1, &mut input as LPINPUT, size_of::() as c_int) } + let mut inputs = [INPUT { + type_: INPUT_KEYBOARD, + u: union, + }; 1]; + unsafe { + SendInput( + inputs.len() as UINT, + inputs.as_mut_ptr(), + size_of::() as c_int, + ) + } } fn get_error() -> String { diff --git a/src/keyboard.rs b/src/keyboard.rs index 7e4ba2b39..4dcbe5c97 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -806,8 +806,8 @@ fn is_hot_key_modifiers_down() -> bool { #[inline] #[cfg(target_os = "windows")] -pub fn translate_key_code(event: &Event, mut key_event: KeyEvent) -> Option { - let mut key_event = map_keyboard_mode(event, key_event)?; +pub fn translate_key_code(peer: &str, event: &Event, key_event: KeyEvent) -> Option { + let mut key_event = map_keyboard_mode(peer, event, key_event)?; key_event.set_chr((key_event.chr() & 0x0000FFFF) | ((event.code as u32) << 16)); Some(key_event) } diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 59f503a14..67267bd94 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -1084,8 +1084,6 @@ fn legacy_keyboard_mode(evt: &KeyEvent) { #[cfg(target_os = "windows")] fn translate_process_code(code: u32, down: bool) { crate::platform::windows::try_change_desktop(); - let vk_code = - match code >> 16 { 0 => sim_rdev_rawkey_position(code, down), vk_code => sim_rdev_rawkey_virtual(vk_code, down), From a20f6b7d5e442f305d4058818d80243093c9a2eb Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 14 Feb 2023 17:11:27 +0800 Subject: [PATCH 1870/2015] translate mode, fix win dead key Signed-off-by: fufesou --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 2fcdef290..b308de149 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4554,7 +4554,7 @@ dependencies = [ [[package]] name = "rdev" version = "0.5.0-2" -source = "git+https://github.com/fufesou/rdev#593f0ba37139ed6f4f88a4120e972612ec4b1c6f" +source = "git+https://github.com/fufesou/rdev#5b9fb5e42117f44e0ce0fe7cf2bddf270c75f1dc" dependencies = [ "cocoa", "core-foundation 0.9.3", From e24f72040e5577d6ed44c73f4b45635b712a19f7 Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 14 Feb 2023 22:09:25 +0800 Subject: [PATCH 1871/2015] translate mode, trivial changes Signed-off-by: fufesou --- flutter/lib/desktop/widgets/remote_menubar.dart | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 9f8265fec..1a1a558fa 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -1515,8 +1515,11 @@ class _RemoteMenubarState extends State { continue; } } - list.add(MenuEntryRadioOption( - text: translate(mode.menu), value: mode.key)); + var text = translate(mode.menu); + if (mode.key == 'translate') { + text = '$text beta legacy 2'; + } + list.add(MenuEntryRadioOption(text: text, value: mode.key)); } } return list; From 16dd1f3c797c7a6015d9cfadef4ea33e1f8d6d67 Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 14 Feb 2023 22:20:12 +0800 Subject: [PATCH 1872/2015] translate mode, trivial changes Signed-off-by: fufesou --- flutter/lib/desktop/widgets/remote_menubar.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 1a1a558fa..66a13f606 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -1517,7 +1517,7 @@ class _RemoteMenubarState extends State { } var text = translate(mode.menu); if (mode.key == 'translate') { - text = '$text beta legacy 2'; + text = '$text beta'; } list.add(MenuEntryRadioOption(text: text, value: mode.key)); } From 20be9e10b11e02b6e463ce3390abe0ab2670cfc7 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 14 Feb 2023 11:50:04 +0800 Subject: [PATCH 1873/2015] opt: scrollable on menubar, avoid overflow --- flutter/lib/desktop/widgets/remote_menubar.dart | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 66a13f606..c68b394e4 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -439,9 +439,12 @@ class _RemoteMenubarState extends State { color: Colors.white, border: Border.all(color: MyTheme.border), ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: menubarItems, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + mainAxisSize: MainAxisSize.min, + children: menubarItems, + ), )), _buildDraggableShowHide(context), ])); From 8df357c9411faa4a23908a3aeb6fd72414634f60 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Wed, 15 Feb 2023 15:03:19 +0800 Subject: [PATCH 1874/2015] refactor: use listview for file lists --- flutter/lib/consts.dart | 12 + .../lib/desktop/pages/file_manager_page.dart | 298 ++++++++++-------- flutter/lib/models/file_model.dart | 4 + 3 files changed, 186 insertions(+), 128 deletions(-) diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 26e25a209..2b4bc7f32 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -50,6 +50,18 @@ const int kMobileMaxDisplayHeight = 1280; const int kDesktopMaxDisplayWidth = 1920; const int kDesktopMaxDisplayHeight = 1080; +const double kDesktopFileTransferNameColWidth = 200; +const double kDesktopFileTransferModifiedColWidth = 120; +const double kDesktopFileTransferRowHeight = 25.0; +const double kDesktopFileTransferHeaderHeight = 25.0; + +// https://en.wikipedia.org/wiki/Non-breaking_space +const int $nbsp = 0x00A0; + +extension StringExtension on String { + String get nonBreaking => replaceAll(' ', String.fromCharCode($nbsp)); +} + const Size kConnectionManagerWindowSize = Size(300, 400); // Tabbar transition duration, now we remove the duration const Duration kTabTransitionDuration = Duration.zero; diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 27bb0377d..fef0dd3d3 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -236,10 +236,7 @@ class _FileManagerPageState extends State crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( - child: SingleChildScrollView( - controller: scrollController, - child: _buildDataTable(context, isLocal, scrollController), - ), + child: _buildFileList(context, isLocal, scrollController), ) ], )), @@ -248,25 +245,11 @@ class _FileManagerPageState extends State ); } - Widget _buildDataTable( + Widget _buildFileList( BuildContext context, bool isLocal, ScrollController scrollController) { - const rowHeight = 25.0; final fd = model.getCurrentDir(isLocal); final entries = fd.entries; - final sortIndex = (SortBy style) { - switch (style) { - case SortBy.name: - return 0; - case SortBy.type: - return 0; - case SortBy.modified: - return 1; - case SortBy.size: - return 2; - } - }(model.getSortStyle(isLocal)); - final sortAscending = - isLocal ? model.localSortAscending : model.remoteSortAscending; + final selectedEntries = getSelectedItems(isLocal); return MouseRegion( onEnter: (evt) { @@ -287,7 +270,6 @@ class _FileManagerPageState extends State onNext: (buffer) { debugPrint("searching next for $buffer"); assert(buffer.length == 1); - final selectedEntries = getSelectedItems(isLocal); assert(selectedEntries.length <= 1); var skipCount = 0; if (selectedEntries.items.isNotEmpty) { @@ -312,7 +294,8 @@ class _FileManagerPageState extends State return; } _jumpToEntry( - isLocal, searchResult.first, scrollController, rowHeight, buffer); + isLocal, searchResult.first, scrollController, + kDesktopFileTransferRowHeight, buffer); }, onSearch: (buffer) { debugPrint("searching for $buffer"); @@ -327,7 +310,8 @@ class _FileManagerPageState extends State return; } _jumpToEntry( - isLocal, searchResult.first, scrollController, rowHeight, buffer); + isLocal, searchResult.first, scrollController, + kDesktopFileTransferRowHeight, buffer); }, child: ObxValue( (searchText) { @@ -336,118 +320,120 @@ class _FileManagerPageState extends State return element.name.contains(searchText.value); }).toList(growable: false) : entries; - return DataTable( - key: ValueKey(isLocal ? 0 : 1), - showCheckboxColumn: false, - dataRowHeight: rowHeight, - headingRowHeight: 30, - horizontalMargin: 8, - columnSpacing: 8, - showBottomBorder: true, - sortColumnIndex: sortIndex, - sortAscending: sortAscending, - columns: [ - DataColumn( - label: Text( - translate("Name"), - ).marginSymmetric(horizontal: 4), - onSort: (columnIndex, ascending) { - model.changeSortStyle(SortBy.name, - isLocal: isLocal, ascending: ascending); - }), - DataColumn( - label: Text( - translate("Modified"), - ), - onSort: (columnIndex, ascending) { - model.changeSortStyle(SortBy.modified, - isLocal: isLocal, ascending: ascending); - }), - DataColumn( - label: Text(translate("Size")), - onSort: (columnIndex, ascending) { - model.changeSortStyle(SortBy.size, - isLocal: isLocal, ascending: ascending); - }), - ], - rows: filteredEntries.map((entry) { + final rows = filteredEntries.map((entry) { final sizeStr = entry.isFile ? readableFileSize(entry.size.toDouble()) : ""; final lastModifiedStr = entry.isDrive ? " " : "${entry.lastModified().toString().replaceAll(".000", "")} "; - return DataRow( - key: ValueKey(entry.name), - onSelectChanged: (s) { - _onSelectedChanged(getSelectedItems(isLocal), - filteredEntries, entry, isLocal); - }, - selected: getSelectedItems(isLocal).contains(entry), - cells: [ - DataCell( - Container( - width: 200, - child: Tooltip( - waitDuration: Duration(milliseconds: 500), - message: entry.name, - child: Row(children: [ - entry.isDrive - ? Image( - image: iconHardDrive, - fit: BoxFit.scaleDown, - color: Theme.of(context) - .iconTheme - .color - ?.withOpacity(0.7)) - .paddingAll(4) - : Icon( - entry.isFile - ? Icons.feed_outlined - : Icons.folder, - size: 20, - color: Theme.of(context) - .iconTheme - .color - ?.withOpacity(0.7), - ).marginSymmetric(horizontal: 2), - Expanded( - child: Text(entry.name, - overflow: TextOverflow.ellipsis)) - ]), + final isSelected = selectedEntries.contains(entry); + return SizedBox( + key: ValueKey(entry.name), + height: kDesktopFileTransferRowHeight, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + const Divider( + height: 1, + ), + Expanded( + child: Ink( + decoration: isSelected + ? BoxDecoration(color: Theme.of(context).hoverColor) + : null, + child: InkWell( + child: Row(children: [ + GestureDetector( + child: Container( + width: kDesktopFileTransferNameColWidth, + child: Tooltip( + waitDuration: Duration(milliseconds: 500), + message: entry.name, + child: Row(children: [ + entry.isDrive + ? Image( + image: iconHardDrive, + fit: BoxFit.scaleDown, + color: Theme.of(context) + .iconTheme + .color + ?.withOpacity(0.7)) + .paddingAll(4) + : Icon( + entry.isFile + ? Icons.feed_outlined + : Icons.folder, + size: 20, + color: Theme.of(context) + .iconTheme + .color + ?.withOpacity(0.7), + ).marginSymmetric(horizontal: 2), + Expanded( + child: Text(entry.name.nonBreaking, + overflow: TextOverflow.ellipsis)) + ]), + )), + onTap: () { + final items = getSelectedItems(isLocal); + // handle double click + if (_checkDoubleClick(entry)) { + openDirectory(entry.path, isLocal: isLocal); + items.clear(); + return; + } + _onSelectedChanged( + items, filteredEntries, entry, isLocal); + }, + ), + GestureDetector( + child: SizedBox( + width: kDesktopFileTransferModifiedColWidth, + child: Tooltip( + waitDuration: Duration(milliseconds: 500), + message: lastModifiedStr, + child: Text( + lastModifiedStr, + style: TextStyle( + fontSize: 12, color: MyTheme.darkGray), + )), )), - onTap: () { - final items = getSelectedItems(isLocal); - - // handle double click - if (_checkDoubleClick(entry)) { - openDirectory(entry.path, isLocal: isLocal); - items.clear(); - return; - } - _onSelectedChanged( - items, filteredEntries, entry, isLocal); - }, + GestureDetector( + child: Tooltip( + waitDuration: Duration(milliseconds: 500), + message: sizeStr, + child: Text( + sizeStr, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 10, + color: MyTheme.darkGray), + ))), + ]), + ), ), - DataCell(FittedBox( - child: Tooltip( - waitDuration: Duration(milliseconds: 500), - message: lastModifiedStr, - child: Text( - lastModifiedStr, - style: TextStyle( - fontSize: 12, color: MyTheme.darkGray), - )))), - DataCell(Tooltip( - waitDuration: Duration(milliseconds: 500), - message: sizeStr, - child: Text( - sizeStr, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 10, color: MyTheme.darkGray), - ))), - ]); - }).toList(growable: false), + ), + ], + ), + ); + }).toList(growable: false); + + return Column( + children: [ + // Header + _buildFileBrowserHeader(context, isLocal), + // Body + Expanded( + child: ListView.builder( + controller: scrollController, + itemExtent: kDesktopFileTransferRowHeight, + itemBuilder: (context, index) { + return rows[index]; + }, + itemCount: rows.length, + ), + ), + ], ); }, isLocal ? _searchTextLocal : _searchTextRemote, @@ -1133,4 +1119,60 @@ class _FileManagerPageState extends State } }); } + + Widget headerItemFunc( + double? width, SortBy sortBy, String name, bool isLocal) { + final headerTextStyle = + Theme.of(context).dataTableTheme.headingTextStyle ?? TextStyle(); + return ObxValue>( + (ascending) => InkWell( + onTap: () { + if (ascending.value == null) { + ascending.value = true; + } else { + ascending.value = !ascending.value!; + } + model.changeSortStyle(sortBy, + isLocal: isLocal, ascending: ascending.value!); + }, + child: SizedBox( + width: width, + height: kDesktopFileTransferHeaderHeight, + child: Row( + children: [ + Text( + name, + style: headerTextStyle, + ).marginSymmetric( + horizontal: sortBy == SortBy.name ? 4 : 0.0), + ascending.value != null + ? Icon(ascending.value! + ? Icons.arrow_upward + : Icons.arrow_downward) + : const Offstage() + ], + ), + ), + ), () { + if (model.getSortStyle(isLocal) == sortBy) { + return model.getSortAscending(isLocal).obs; + } else { + return Rx(null); + } + }()); + } + + Widget _buildFileBrowserHeader(BuildContext context, bool isLocal) { + return Row( + children: [ + headerItemFunc(kDesktopFileTransferNameColWidth, SortBy.name, + translate("Name"), isLocal), + headerItemFunc(kDesktopFileTransferModifiedColWidth, SortBy.modified, + translate("Modified"), isLocal), + Expanded( + child: + headerItemFunc(null, SortBy.size, translate("Size"), isLocal)) + ], + ); + } } diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index 18d42d143..5817e54fe 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -75,6 +75,10 @@ class FileModel extends ChangeNotifier { return isLocal ? _localSortStyle : _remoteSortStyle; } + bool getSortAscending(bool isLocal) { + return isLocal ? _localSortAscending : _remoteSortAscending; + } + FileDirectory _currentLocalDir = FileDirectory(); FileDirectory get currentLocalDir => _currentLocalDir; From 66378f63d9bb329bfa659380fc6b6de17f17b37d Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 15 Feb 2023 15:25:28 +0800 Subject: [PATCH 1875/2015] fix macos command-tab Signed-off-by: fufesou --- src/server/input_service.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 67267bd94..917a815bb 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -719,7 +719,7 @@ fn reset_input() { let _lock = VIRTUAL_INPUT_MTX.lock(); VIRTUAL_INPUT = VirtualInput::new( CGEventSourceStateID::Private, - CGEventTapLocation::AnnotatedSession, + CGEventTapLocation::Session, ) .ok(); } From 2047fd822b97659f291d02c6f573503a03eb2b8e Mon Sep 17 00:00:00 2001 From: Kingtous Date: Wed, 15 Feb 2023 16:44:40 +0800 Subject: [PATCH 1876/2015] opt: early unlock frame --- flutter/lib/models/model.dart | 10 ++++++---- flutter/lib/utils/image.dart | 2 ++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 8cf90eba9..a1d9ff0df 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -438,15 +438,17 @@ class ImageModel with ChangeNotifier { } final pid = parent.target?.id; - ui.decodeImageFromPixels( + img.decodeImageFromPixels( rgba, parent.target?.ffiModel.display.width ?? 0, parent.target?.ffiModel.display.height ?? 0, - isWeb ? ui.PixelFormat.rgba8888 : ui.PixelFormat.bgra8888, (image) { + isWeb ? ui.PixelFormat.rgba8888 : ui.PixelFormat.bgra8888, + onPixelsCopied: () { + // Unlock the rgba memory from rust codes. + platformFFI.nextRgba(id); + }).then((image) { if (parent.target?.id != pid) return; try { - // Unlock the rgba memory from rust codes. - platformFFI.nextRgba(id); // my throw exception, because the listener maybe already dispose update(image); } catch (e) { diff --git a/flutter/lib/utils/image.dart b/flutter/lib/utils/image.dart index 7a6bcbc15..a153dbc63 100644 --- a/flutter/lib/utils/image.dart +++ b/flutter/lib/utils/image.dart @@ -11,6 +11,7 @@ Future decodeImageFromPixels( int? rowBytes, int? targetWidth, int? targetHeight, + VoidCallback? onPixelsCopied, bool allowUpscaling = true, }) async { if (targetWidth != null) { @@ -22,6 +23,7 @@ Future decodeImageFromPixels( final ui.ImmutableBuffer buffer = await ui.ImmutableBuffer.fromUint8List(pixels); + onPixelsCopied?.call(); final ui.ImageDescriptor descriptor = ui.ImageDescriptor.raw( buffer, width: width, From c5d39b0c105cf95f987be60bd6d573a7ba89aa03 Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Wed, 15 Feb 2023 11:40:17 +0100 Subject: [PATCH 1877/2015] reworked --- flutter/assets/actions.svg | 3 +- flutter/assets/chat.svg | 2 +- flutter/assets/close.svg | 2 +- flutter/assets/display.svg | 2 +- flutter/assets/fullscreen.svg | 2 +- flutter/assets/fullscreen_exit.svg | 2 +- flutter/assets/keyboard.svg | 2 +- flutter/assets/pinned.svg | 2 +- flutter/assets/rec.svg | 2 +- flutter/assets/unpinned.svg | 2 +- flutter/lib/desktop/pages/remote_page.dart | 2 +- .../lib/desktop/pages/remote_tab_page.dart | 9 ++- .../widgets/material_mod_popup_menu.dart | 9 +-- flutter/lib/desktop/widgets/menu_button.dart | 63 +++++++++++++++++ .../lib/desktop/widgets/remote_menubar.dart | 67 +++++++++++-------- 15 files changed, 125 insertions(+), 46 deletions(-) create mode 100644 flutter/lib/desktop/widgets/menu_button.dart diff --git a/flutter/assets/actions.svg b/flutter/assets/actions.svg index feaf416cd..5403853db 100644 --- a/flutter/assets/actions.svg +++ b/flutter/assets/actions.svg @@ -1,3 +1,2 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/chat.svg b/flutter/assets/chat.svg index 830ef0d33..7088107b0 100644 --- a/flutter/assets/chat.svg +++ b/flutter/assets/chat.svg @@ -1,2 +1,2 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/close.svg b/flutter/assets/close.svg index 1e9a30711..7488acc9f 100644 --- a/flutter/assets/close.svg +++ b/flutter/assets/close.svg @@ -1,2 +1,2 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/display.svg b/flutter/assets/display.svg index 8a87116ff..b5a88106e 100644 --- a/flutter/assets/display.svg +++ b/flutter/assets/display.svg @@ -1,2 +1,2 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/fullscreen.svg b/flutter/assets/fullscreen.svg index 73d79cf0e..cd01f93f9 100644 --- a/flutter/assets/fullscreen.svg +++ b/flutter/assets/fullscreen.svg @@ -1,2 +1,2 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/fullscreen_exit.svg b/flutter/assets/fullscreen_exit.svg index f2b3ae27b..8d4414897 100644 --- a/flutter/assets/fullscreen_exit.svg +++ b/flutter/assets/fullscreen_exit.svg @@ -1,2 +1,2 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/keyboard.svg b/flutter/assets/keyboard.svg index 569c68727..d5481d7a1 100644 --- a/flutter/assets/keyboard.svg +++ b/flutter/assets/keyboard.svg @@ -1,2 +1,2 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/pinned.svg b/flutter/assets/pinned.svg index 2563015f7..dd718b96a 100644 --- a/flutter/assets/pinned.svg +++ b/flutter/assets/pinned.svg @@ -1,2 +1,2 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/rec.svg b/flutter/assets/rec.svg index 14546b971..33a57e9d0 100644 --- a/flutter/assets/rec.svg +++ b/flutter/assets/rec.svg @@ -1,2 +1,2 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/unpinned.svg b/flutter/assets/unpinned.svg index ba4ab5328..9e9e3de8b 100644 --- a/flutter/assets/unpinned.svg +++ b/flutter/assets/unpinned.svg @@ -1,2 +1,2 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 211d36c39..dac62032f 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -201,7 +201,7 @@ class _RemotePageState extends State Widget buildBody(BuildContext context) { return Scaffold( - backgroundColor: Theme.of(context).backgroundColor, + backgroundColor: Theme.of(context).colorScheme.background, /// the Overlay key will be set with _blockableOverlayState in BlockableOverlay /// see override build() in [BlockableOverlay] diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index 9b00b481f..610a7d1a5 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -22,7 +22,10 @@ import 'package:bot_toast/bot_toast.dart'; import '../../models/platform_model.dart'; class _MenuTheme { - static const Color commonColor = MyTheme.accent; + static const Color blueColor = MyTheme.button; + static const Color hoverBlueColor = MyTheme.accent; + static const Color redColor = Colors.redAccent; + static const Color hoverRedColor = Colors.red; // kMinInteractiveDimension static const double height = 20.0; static const double dividerHeight = 12.0; @@ -134,7 +137,7 @@ class _ConnectionTabPageState extends State { width: stateGlobal.windowBorderWidth.value), ), child: Scaffold( - backgroundColor: Theme.of(context).backgroundColor, + backgroundColor: Theme.of(context).colorScheme.background, body: DesktopTab( controller: tabController, onWindowCloseButton: handleWindowCloseButton, @@ -280,7 +283,7 @@ class _ConnectionTabPageState extends State { .map((entry) => entry.build( context, const MenuConfig( - commonColor: _MenuTheme.commonColor, + commonColor: _MenuTheme.blueColor, height: _MenuTheme.height, dividerHeight: _MenuTheme.dividerHeight, ))) diff --git a/flutter/lib/desktop/widgets/material_mod_popup_menu.dart b/flutter/lib/desktop/widgets/material_mod_popup_menu.dart index 666c9a6e2..05c3059d4 100644 --- a/flutter/lib/desktop/widgets/material_mod_popup_menu.dart +++ b/flutter/lib/desktop/widgets/material_mod_popup_menu.dart @@ -5,6 +5,8 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/desktop/widgets/menu_button.dart'; // Examples can assume: // enum Commands { heroAndScholar, hurricaneCame } @@ -1391,22 +1393,21 @@ class PopupMenuButtonState extends State> { onTap: widget.enabled ? showButtonMenu : null, onHover: widget.onHover, canRequestFocus: _canRequestFocus, - radius: widget.splashRadius, enableFeedback: enableFeedback, child: widget.child, ), ); } - return IconButton( + return MenuButton( icon: widget.icon ?? Icon(Icons.adaptive.more), - padding: widget.padding, - splashRadius: widget.splashRadius, iconSize: widget.iconSize ?? iconTheme.size ?? _kDefaultIconSize, tooltip: widget.tooltip ?? MaterialLocalizations.of(context).showMenuTooltip, onPressed: widget.enabled ? showButtonMenu : null, enableFeedback: enableFeedback, + color: MyTheme.button, + hoverColor: MyTheme.accent, ); } } diff --git a/flutter/lib/desktop/widgets/menu_button.dart b/flutter/lib/desktop/widgets/menu_button.dart new file mode 100644 index 000000000..ce63dcab1 --- /dev/null +++ b/flutter/lib/desktop/widgets/menu_button.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; + +class MenuButton extends StatefulWidget { + final GestureTapCallback? onPressed; + final Color color; + final Color hoverColor; + final Color? splashColor; + final Widget icon; + final double iconSize; + final String tooltip; + final EdgeInsetsGeometry padding; + final bool enableFeedback; + const MenuButton({ + super.key, + required this.onPressed, + required this.color, + required this.hoverColor, + required this.icon, + required this.iconSize, + required this.tooltip, + this.splashColor, + this.padding = const EdgeInsets.all(5), + this.enableFeedback = true, + }); + + @override + State createState() => _MenuButtonState(); +} + +class _MenuButtonState extends State { + bool _isHover = false; + + @override + Widget build(BuildContext context) { + return Padding( + padding: widget.padding, + child: Tooltip( + message: widget.tooltip, + child: Material( + type: MaterialType.transparency, + child: Ink( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5), + color: _isHover ? widget.hoverColor : widget.color, + ), + child: InkWell( + onHover: (val) { + setState(() { + _isHover = val; + }); + }, + borderRadius: BorderRadius.circular(5), + splashColor: widget.splashColor, + enableFeedback: widget.enableFeedback, + onTap: widget.onPressed, + child: widget.icon, + ), + ), + ), + ), + ); + } +} diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 77d687d93..ff586a1f1 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -5,6 +5,7 @@ import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_hbb/desktop/widgets/menu_button.dart'; import 'package:flutter_hbb/models/chat_model.dart'; import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/consts.dart'; @@ -94,7 +95,10 @@ class MenubarState { } class _MenubarTheme { - static const Color commonColor = MyTheme.accent; + static const Color blueColor = MyTheme.button; + static const Color hoverBlueColor = MyTheme.accent; + static const Color redColor = Colors.redAccent; + static const Color hoverRedColor = Colors.red; // kMinInteractiveDimension static const double height = 20.0; static const double dividerHeight = 12.0; @@ -412,7 +416,7 @@ class _RemoteMenubarState extends State { if (widget.ffi.ffiModel.isPeerAndroid) { menubarItems.add(IconButton( tooltip: translate('Mobile Actions'), - color: _MenubarTheme.commonColor, + color: _MenubarTheme.blueColor, icon: const Icon(Icons.build), onPressed: () { widget.ffi.dialogManager @@ -433,7 +437,7 @@ class _RemoteMenubarState extends State { menubarItems.add(_buildClose(context, iconSize)); return PopupMenuTheme( data: const PopupMenuThemeData( - textStyle: TextStyle(color: _MenubarTheme.commonColor)), + textStyle: TextStyle(color: _MenubarTheme.blueColor)), child: Column( mainAxisSize: MainAxisSize.min, children: [ @@ -457,8 +461,7 @@ class _RemoteMenubarState extends State { Widget _buildPinMenubar(BuildContext context, double iconSize) { return Obx( - () => IconButton( - padding: EdgeInsets.zero, + () => MenuButton( iconSize: iconSize, tooltip: translate(pin ? 'Unpin menubar' : 'Pin menubar'), onPressed: () { @@ -466,15 +469,16 @@ class _RemoteMenubarState extends State { }, icon: SvgPicture.asset( pin ? "assets/pinned.svg" : "assets/unpinned.svg", - color: pin ? _MenubarTheme.commonColor : Colors.grey[800], + color: Colors.white, ), + color: pin ? _MenubarTheme.blueColor : Colors.grey[800]!, + hoverColor: pin ? _MenubarTheme.hoverBlueColor : Colors.grey[850]!, ), ); } Widget _buildFullscreen(BuildContext context, double iconSize) { - return IconButton( - padding: EdgeInsets.zero, + return MenuButton( iconSize: iconSize, tooltip: translate(isFullscreen ? 'Exit Fullscreen' : 'Fullscreen'), onPressed: () { @@ -482,8 +486,10 @@ class _RemoteMenubarState extends State { }, icon: SvgPicture.asset( isFullscreen ? "assets/fullscreen_exit.svg" : "assets/fullscreen.svg", - color: _MenubarTheme.commonColor, + color: Colors.white, ), + color: _MenubarTheme.blueColor, + hoverColor: _MenubarTheme.hoverBlueColor, ); } @@ -492,14 +498,13 @@ class _RemoteMenubarState extends State { return mod_menu.PopupMenuButton( iconSize: iconSize, tooltip: translate('Select Monitor'), - padding: EdgeInsets.zero, position: mod_menu.PopupMenuPosition.under, icon: Stack( alignment: Alignment.center, children: [ SvgPicture.asset( "assets/display.svg", - color: _MenubarTheme.commonColor, + color: Colors.white, ), Padding( padding: const EdgeInsets.only(bottom: 3.9), @@ -520,7 +525,10 @@ class _RemoteMenubarState extends State { Stack( alignment: Alignment.center, children: [ - SvgPicture.asset("assets/display.svg"), + SvgPicture.asset( + "assets/display.svg", + color: Colors.white, + ), TextButton( child: Container( alignment: AlignmentDirectional.center, @@ -531,7 +539,7 @@ class _RemoteMenubarState extends State { child: Text( (i + 1).toString(), style: TextStyle( - color: Theme.of(context).scaffoldBackgroundColor, + color: Colors.white, ), ), ), @@ -573,7 +581,7 @@ class _RemoteMenubarState extends State { padding: EdgeInsets.zero, icon: SvgPicture.asset( "assets/actions.svg", - color: _MenubarTheme.commonColor, + color: Colors.white, ), tooltip: translate('Control Actions'), position: mod_menu.PopupMenuPosition.under, @@ -581,7 +589,7 @@ class _RemoteMenubarState extends State { .map((entry) => entry.build( context, const MenuConfig( - commonColor: _MenubarTheme.commonColor, + commonColor: _MenubarTheme.blueColor, height: _MenubarTheme.height, dividerHeight: _MenubarTheme.dividerHeight, ))) @@ -606,7 +614,7 @@ class _RemoteMenubarState extends State { padding: EdgeInsets.zero, icon: SvgPicture.asset( "assets/display.svg", - color: _MenubarTheme.commonColor, + color: Colors.white, ), tooltip: translate('Display Settings'), position: mod_menu.PopupMenuPosition.under, @@ -616,7 +624,7 @@ class _RemoteMenubarState extends State { .map((entry) => entry.build( context, const MenuConfig( - commonColor: _MenubarTheme.commonColor, + commonColor: _MenubarTheme.blueColor, height: _MenubarTheme.height, dividerHeight: _MenubarTheme.dividerHeight, ))) @@ -640,7 +648,7 @@ class _RemoteMenubarState extends State { padding: EdgeInsets.zero, icon: SvgPicture.asset( "assets/keyboard.svg", - color: _MenubarTheme.commonColor, + color: Colors.white, ), tooltip: translate('Keyboard Settings'), position: mod_menu.PopupMenuPosition.under, @@ -648,7 +656,7 @@ class _RemoteMenubarState extends State { .map((entry) => entry.build( context, const MenuConfig( - commonColor: _MenubarTheme.commonColor, + commonColor: _MenubarTheme.blueColor, height: _MenubarTheme.height, dividerHeight: _MenubarTheme.dividerHeight, ))) @@ -661,8 +669,7 @@ class _RemoteMenubarState extends State { return Consumer(builder: ((context, value, child) { if (value.permissions['recording'] != false) { return Consumer( - builder: (context, value, child) => IconButton( - padding: EdgeInsets.zero, + builder: (context, value, child) => MenuButton( iconSize: iconSize, tooltip: value.start ? translate('Stop session recording') @@ -670,8 +677,13 @@ class _RemoteMenubarState extends State { onPressed: () => value.toggle(), icon: SvgPicture.asset( "assets/rec.svg", - color: value.start ? Colors.red : _MenubarTheme.commonColor, + color: Colors.white, ), + color: + value.start ? _MenubarTheme.redColor : _MenubarTheme.blueColor, + hoverColor: value.start + ? _MenubarTheme.hoverRedColor + : _MenubarTheme.hoverBlueColor, ), ); } else { @@ -681,17 +693,18 @@ class _RemoteMenubarState extends State { } Widget _buildClose(BuildContext context, double iconSize) { - return IconButton( + return MenuButton( iconSize: iconSize, - padding: EdgeInsets.zero, tooltip: translate('Close'), onPressed: () { clientClose(widget.id, widget.ffi.dialogManager); }, icon: SvgPicture.asset( "assets/close.svg", - color: Colors.red, + color: Colors.white, ), + color: _MenubarTheme.redColor, + hoverColor: _MenubarTheme.hoverRedColor, ); } @@ -704,7 +717,7 @@ class _RemoteMenubarState extends State { padding: EdgeInsets.zero, icon: SvgPicture.asset( "assets/chat.svg", - color: _MenubarTheme.commonColor, + color: Colors.white, ), tooltip: translate('Chat'), position: mod_menu.PopupMenuPosition.under, @@ -712,7 +725,7 @@ class _RemoteMenubarState extends State { .map((entry) => entry.build( context, const MenuConfig( - commonColor: _MenubarTheme.commonColor, + commonColor: _MenubarTheme.blueColor, height: _MenubarTheme.height, dividerHeight: _MenubarTheme.dividerHeight, ))) From 952596080279c8778cd3cd7edd8af6da80fa9089 Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Wed, 15 Feb 2023 13:19:15 +0100 Subject: [PATCH 1878/2015] added new call end/wait icons --- flutter/assets/call_end.svg | 2 + flutter/assets/call_wait.svg | 2 + flutter/lib/desktop/pages/remote_page.dart | 2 +- .../lib/desktop/pages/remote_tab_page.dart | 2 +- .../widgets/material_mod_popup_menu.dart | 1 - flutter/lib/desktop/widgets/menu_button.dart | 6 +- .../lib/desktop/widgets/remote_menubar.dart | 79 +++++++------------ 7 files changed, 38 insertions(+), 56 deletions(-) create mode 100644 flutter/assets/call_end.svg create mode 100644 flutter/assets/call_wait.svg diff --git a/flutter/assets/call_end.svg b/flutter/assets/call_end.svg new file mode 100644 index 000000000..39367c3c5 --- /dev/null +++ b/flutter/assets/call_end.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/flutter/assets/call_wait.svg b/flutter/assets/call_wait.svg new file mode 100644 index 000000000..42a11fe56 --- /dev/null +++ b/flutter/assets/call_wait.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index dac62032f..211d36c39 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -201,7 +201,7 @@ class _RemotePageState extends State Widget buildBody(BuildContext context) { return Scaffold( - backgroundColor: Theme.of(context).colorScheme.background, + backgroundColor: Theme.of(context).backgroundColor, /// the Overlay key will be set with _blockableOverlayState in BlockableOverlay /// see override build() in [BlockableOverlay] diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index 610a7d1a5..7bd2a4126 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -137,7 +137,7 @@ class _ConnectionTabPageState extends State { width: stateGlobal.windowBorderWidth.value), ), child: Scaffold( - backgroundColor: Theme.of(context).colorScheme.background, + backgroundColor: Theme.of(context).backgroundColor, body: DesktopTab( controller: tabController, onWindowCloseButton: handleWindowCloseButton, diff --git a/flutter/lib/desktop/widgets/material_mod_popup_menu.dart b/flutter/lib/desktop/widgets/material_mod_popup_menu.dart index 05c3059d4..47de1be20 100644 --- a/flutter/lib/desktop/widgets/material_mod_popup_menu.dart +++ b/flutter/lib/desktop/widgets/material_mod_popup_menu.dart @@ -1401,7 +1401,6 @@ class PopupMenuButtonState extends State> { return MenuButton( icon: widget.icon ?? Icon(Icons.adaptive.more), - iconSize: widget.iconSize ?? iconTheme.size ?? _kDefaultIconSize, tooltip: widget.tooltip ?? MaterialLocalizations.of(context).showMenuTooltip, onPressed: widget.enabled ? showButtonMenu : null, diff --git a/flutter/lib/desktop/widgets/menu_button.dart b/flutter/lib/desktop/widgets/menu_button.dart index ce63dcab1..b2871e0cd 100644 --- a/flutter/lib/desktop/widgets/menu_button.dart +++ b/flutter/lib/desktop/widgets/menu_button.dart @@ -6,8 +6,7 @@ class MenuButton extends StatefulWidget { final Color hoverColor; final Color? splashColor; final Widget icon; - final double iconSize; - final String tooltip; + final String? tooltip; final EdgeInsetsGeometry padding; final bool enableFeedback; const MenuButton({ @@ -16,9 +15,8 @@ class MenuButton extends StatefulWidget { required this.color, required this.hoverColor, required this.icon, - required this.iconSize, - required this.tooltip, this.splashColor, + this.tooltip = "", this.padding = const EdgeInsets.all(5), this.enableFeedback = true, }); diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index ff586a1f1..5029560b0 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -409,10 +409,9 @@ class _RemoteMenubarState extends State { Widget _buildMenubar(BuildContext context) { final List menubarItems = []; - final double iconSize = Theme.of(context).iconTheme.size ?? 30.0; if (!isWebDesktop) { - menubarItems.add(_buildPinMenubar(context, iconSize)); - menubarItems.add(_buildFullscreen(context, iconSize)); + menubarItems.add(_buildPinMenubar(context)); + menubarItems.add(_buildFullscreen(context)); if (widget.ffi.ffiModel.isPeerAndroid) { menubarItems.add(IconButton( tooltip: translate('Mobile Actions'), @@ -425,16 +424,16 @@ class _RemoteMenubarState extends State { )); } } - menubarItems.add(_buildMonitor(context, iconSize)); - menubarItems.add(_buildControl(context, iconSize)); - menubarItems.add(_buildDisplay(context, iconSize)); - menubarItems.add(_buildKeyboard(context, iconSize)); + menubarItems.add(_buildMonitor(context)); + menubarItems.add(_buildControl(context)); + menubarItems.add(_buildDisplay(context)); + menubarItems.add(_buildKeyboard(context)); if (!isWeb) { - menubarItems.add(_buildChat(context, iconSize)); - menubarItems.add(_buildVoiceCall(context, iconSize)); + menubarItems.add(_buildChat(context)); + menubarItems.add(_buildVoiceCall(context)); } - menubarItems.add(_buildRecording(context, iconSize)); - menubarItems.add(_buildClose(context, iconSize)); + menubarItems.add(_buildRecording(context)); + menubarItems.add(_buildClose(context)); return PopupMenuTheme( data: const PopupMenuThemeData( textStyle: TextStyle(color: _MenubarTheme.blueColor)), @@ -459,10 +458,9 @@ class _RemoteMenubarState extends State { ); } - Widget _buildPinMenubar(BuildContext context, double iconSize) { + Widget _buildPinMenubar(BuildContext context) { return Obx( () => MenuButton( - iconSize: iconSize, tooltip: translate(pin ? 'Unpin menubar' : 'Pin menubar'), onPressed: () { widget.state.switchPin(); @@ -477,9 +475,8 @@ class _RemoteMenubarState extends State { ); } - Widget _buildFullscreen(BuildContext context, double iconSize) { + Widget _buildFullscreen(BuildContext context) { return MenuButton( - iconSize: iconSize, tooltip: translate(isFullscreen ? 'Exit Fullscreen' : 'Fullscreen'), onPressed: () { _setFullscreen(!isFullscreen); @@ -493,10 +490,9 @@ class _RemoteMenubarState extends State { ); } - Widget _buildMonitor(BuildContext context, double iconSize) { + Widget _buildMonitor(BuildContext context) { final pi = widget.ffi.ffiModel.pi; return mod_menu.PopupMenuButton( - iconSize: iconSize, tooltip: translate('Select Monitor'), position: mod_menu.PopupMenuPosition.under, icon: Stack( @@ -575,9 +571,8 @@ class _RemoteMenubarState extends State { ); } - Widget _buildControl(BuildContext context, double iconSize) { + Widget _buildControl(BuildContext context) { return mod_menu.PopupMenuButton( - iconSize: iconSize, padding: EdgeInsets.zero, icon: SvgPicture.asset( "assets/actions.svg", @@ -598,7 +593,7 @@ class _RemoteMenubarState extends State { ); } - Widget _buildDisplay(BuildContext context, double iconSize) { + Widget _buildDisplay(BuildContext context) { return FutureBuilder(future: () async { widget.state.viewStyle.value = await bind.sessionGetViewStyle(id: widget.id) ?? ''; @@ -610,7 +605,6 @@ class _RemoteMenubarState extends State { return Obx(() { final remoteCount = RemoteCountState.find().value; return mod_menu.PopupMenuButton( - iconSize: iconSize, padding: EdgeInsets.zero, icon: SvgPicture.asset( "assets/display.svg", @@ -638,13 +632,12 @@ class _RemoteMenubarState extends State { }); } - Widget _buildKeyboard(BuildContext context, double iconSize) { + Widget _buildKeyboard(BuildContext context) { FfiModel ffiModel = Provider.of(context); if (ffiModel.permissions['keyboard'] == false) { return Offstage(); } return mod_menu.PopupMenuButton( - iconSize: iconSize, padding: EdgeInsets.zero, icon: SvgPicture.asset( "assets/keyboard.svg", @@ -665,12 +658,11 @@ class _RemoteMenubarState extends State { ); } - Widget _buildRecording(BuildContext context, double iconSize) { + Widget _buildRecording(BuildContext context) { return Consumer(builder: ((context, value, child) { if (value.permissions['recording'] != false) { return Consumer( builder: (context, value, child) => MenuButton( - iconSize: iconSize, tooltip: value.start ? translate('Stop session recording') : translate('Start session recording'), @@ -692,9 +684,8 @@ class _RemoteMenubarState extends State { })); } - Widget _buildClose(BuildContext context, double iconSize) { + Widget _buildClose(BuildContext context) { return MenuButton( - iconSize: iconSize, tooltip: translate('Close'), onPressed: () { clientClose(widget.id, widget.ffi.dialogManager); @@ -709,10 +700,9 @@ class _RemoteMenubarState extends State { } final _chatButtonKey = GlobalKey(); - Widget _buildChat(BuildContext context, double iconSize) { + Widget _buildChat(BuildContext context) { FfiModel ffiModel = Provider.of(context); return mod_menu.PopupMenuButton( - iconSize: iconSize, key: _chatButtonKey, padding: EdgeInsets.zero, icon: SvgPicture.asset( @@ -737,24 +727,15 @@ class _RemoteMenubarState extends State { Widget _getVoiceCallIcon() { switch (widget.ffi.chatModel.voiceCallStatus.value) { case VoiceCallStatus.waitingForResponse: - return IconButton( - onPressed: () { - widget.ffi.chatModel.closeVoiceCall(widget.id); - }, - icon: SvgPicture.asset( - "assets/voice_call_waiting.svg", - color: Colors.red, - ), + return SvgPicture.asset( + "assets/call_wait.svg", + color: Colors.white, ); + case VoiceCallStatus.connected: - return IconButton( - onPressed: () { - widget.ffi.chatModel.closeVoiceCall(widget.id); - }, - icon: Icon( - Icons.phone_disabled_rounded, - color: Colors.red, - ), + return SvgPicture.asset( + "assets/call_end.svg", + color: Colors.white, ); default: return const Offstage(); @@ -772,18 +753,18 @@ class _RemoteMenubarState extends State { } } - Widget _buildVoiceCall(BuildContext context, double iconSize) { + Widget _buildVoiceCall(BuildContext context) { return Obx( () { final tooltipText = _getVoiceCallTooltip(); return tooltipText == null ? const Offstage() - : IconButton( - iconSize: iconSize, - padding: EdgeInsets.zero, + : MenuButton( icon: _getVoiceCallIcon(), tooltip: translate(tooltipText), onPressed: () => bind.sessionRequestVoiceCall(id: widget.id), + color: _MenubarTheme.redColor, + hoverColor: _MenubarTheme.hoverRedColor, ); }, ); From 957bb65b9f624d6b00377787033e545cf6423562 Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Wed, 15 Feb 2023 13:27:21 +0100 Subject: [PATCH 1879/2015] adjusted spacing --- flutter/lib/desktop/widgets/menu_button.dart | 2 +- flutter/lib/desktop/widgets/remote_menubar.dart | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/flutter/lib/desktop/widgets/menu_button.dart b/flutter/lib/desktop/widgets/menu_button.dart index b2871e0cd..904195f71 100644 --- a/flutter/lib/desktop/widgets/menu_button.dart +++ b/flutter/lib/desktop/widgets/menu_button.dart @@ -17,7 +17,7 @@ class MenuButton extends StatefulWidget { required this.icon, this.splashColor, this.tooltip = "", - this.padding = const EdgeInsets.all(5), + this.padding = const EdgeInsets.symmetric(horizontal: 2.5, vertical: 5), this.enableFeedback = true, }); diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 5029560b0..afc5b2d9f 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -449,7 +449,11 @@ class _RemoteMenubarState extends State { ), child: Row( mainAxisSize: MainAxisSize.min, - children: menubarItems, + children: [ + SizedBox(width: 2.5), + ...menubarItems, + SizedBox(width: 2.5) + ], ), ), _buildDraggableShowHide(context), From d5502f58ef5c1c95554ad7917f9aa1eeab21d004 Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 15 Feb 2023 20:39:30 +0800 Subject: [PATCH 1880/2015] release session stream after close Signed-off-by: fufesou --- flutter/lib/models/model.dart | 3 +++ src/flutter_ffi.rs | 3 +++ 2 files changed, 6 insertions(+) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index a1d9ff0df..865a8bea6 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -1368,6 +1368,9 @@ class FFI { // Preserved for the rgba data. await for (final message in stream) { if (message is EventToUI_Event) { + if (message.field0 == "close") { + break; + } try { Map event = json.decode(message.field0); await cb(event); diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 0e307abe3..3f9940854 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -132,6 +132,9 @@ pub fn session_login(id: String, password: String, remember: bool) { pub fn session_close(id: String) { if let Some(session) = SESSIONS.read().unwrap().get(&id) { + if let Some(stream) = &*session.event_stream.read().unwrap() { + stream.add(EventToUI::Event("close".to_owned())); + } session.close(); } let _ = SESSIONS.write().unwrap().remove(&id); From eac6dae3a7aed17b81916b9369c2ed94914f054f Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Wed, 15 Feb 2023 14:14:21 +0100 Subject: [PATCH 1881/2015] increased margin --- flutter/lib/desktop/widgets/menu_button.dart | 2 +- flutter/lib/desktop/widgets/remote_menubar.dart | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/flutter/lib/desktop/widgets/menu_button.dart b/flutter/lib/desktop/widgets/menu_button.dart index 904195f71..7c9fe67eb 100644 --- a/flutter/lib/desktop/widgets/menu_button.dart +++ b/flutter/lib/desktop/widgets/menu_button.dart @@ -17,7 +17,7 @@ class MenuButton extends StatefulWidget { required this.icon, this.splashColor, this.tooltip = "", - this.padding = const EdgeInsets.symmetric(horizontal: 2.5, vertical: 5), + this.padding = const EdgeInsets.symmetric(horizontal: 3, vertical: 6), this.enableFeedback = true, }); diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 189f58f4b..933850c99 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -452,9 +452,9 @@ class _RemoteMenubarState extends State { child: Row( mainAxisSize: MainAxisSize.min, children: [ - SizedBox(width: 2.5), + SizedBox(width: 3), ...menubarItems, - SizedBox(width: 2.5) + SizedBox(width: 3) ], ), ), From d8fe75860465a09fc5b80143069a7cd719cafb2f Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 15 Feb 2023 21:27:50 +0800 Subject: [PATCH 1882/2015] set event stream to None in rust side Signed-off-by: fufesou --- flutter/lib/models/model.dart | 1 + src/flutter.rs | 8 ++++++++ src/flutter_ffi.rs | 9 +++------ 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 865a8bea6..39b1cdd03 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -1389,6 +1389,7 @@ class FFI { } } } + debugPrint('Exit session event loop'); }(); // every instance will bind a stream this.id = id; diff --git a/src/flutter.rs b/src/flutter.rs index a60e379f9..d0f397d3f 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -134,6 +134,14 @@ impl FlutterHandler { stream.add(EventToUI::Event(out)); } } + + pub fn close_event_stream(&mut self) { + let mut stream_lock = self.event_stream.write().unwrap(); + if let Some(stream) = &*stream_lock { + stream.add(EventToUI::Event("close".to_owned())); + } + *stream_lock = None; + } } impl InvokeUiSession for FlutterHandler { diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 3f9940854..53ddb724a 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -6,7 +6,7 @@ use crate::{ flutter::{session_add, session_start_}, ui_interface::{self, *}, }; -use flutter_rust_bridge::{StreamSink, SyncReturn, ZeroCopyBuffer}; +use flutter_rust_bridge::{StreamSink, SyncReturn}; use hbb_common::{ config::{self, LocalConfig, PeerConfig, ONLINE}, fs, log, @@ -131,13 +131,10 @@ pub fn session_login(id: String, password: String, remember: bool) { } pub fn session_close(id: String) { - if let Some(session) = SESSIONS.read().unwrap().get(&id) { - if let Some(stream) = &*session.event_stream.read().unwrap() { - stream.add(EventToUI::Event("close".to_owned())); - } + if let Some(mut session) = SESSIONS.write().unwrap().remove(&id) { + session.close_event_stream(); session.close(); } - let _ = SESSIONS.write().unwrap().remove(&id); } pub fn session_refresh(id: String) { From 432f0b7e3e3924c8f90704f76f07ba4b38f7bd4a Mon Sep 17 00:00:00 2001 From: grummbeer Date: Wed, 15 Feb 2023 15:20:09 +0100 Subject: [PATCH 1883/2015] CustomDialog. Add left padding to actions --- flutter/lib/common.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index ba7e3d762..bdef5f638 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -665,7 +665,7 @@ class CustomAlertDialog extends StatelessWidget { child: content), ), actions: actions, - actionsPadding: EdgeInsets.fromLTRB(0, 0, padding, padding), + actionsPadding: EdgeInsets.fromLTRB(padding, 0, padding, padding), ), ); } From 8f64940147214b266cdae0f82dcb16660bcc5f08 Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Wed, 15 Feb 2023 20:17:36 +0100 Subject: [PATCH 1884/2015] changed linux icon --- flutter/assets/linux.svg | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/flutter/assets/linux.svg b/flutter/assets/linux.svg index 74248b5f0..5427305ba 100644 --- a/flutter/assets/linux.svg +++ b/flutter/assets/linux.svg @@ -1,6 +1,2 @@ - - - - - - + + \ No newline at end of file From 97ad7a42bdeb7ba6460aa24e562ae4bbe2dbbd4d Mon Sep 17 00:00:00 2001 From: Kingtous Date: Thu, 16 Feb 2023 10:58:27 +0800 Subject: [PATCH 1885/2015] fix: window manager called on Android & bugfix etc. --- flutter/lib/common.dart | 3 +++ flutter/lib/desktop/widgets/remote_menubar.dart | 2 +- src/ui/header.tis | 3 --- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index ba7e3d762..8a33f214c 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -336,6 +336,9 @@ closeConnection({String? id}) { } void window_on_top(int? id) { + if (!isDesktop) { + return; + } if (id == null) { // main window windowManager.restore(); diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 933850c99..0fa12cd6f 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -769,7 +769,7 @@ class _RemoteMenubarState extends State { : MenuButton( icon: _getVoiceCallIcon(), tooltip: translate(tooltipText), - onPressed: () => bind.sessionRequestVoiceCall(id: widget.id), + onPressed: () => bind.sessionCloseVoiceCall(id: widget.id), color: _MenubarTheme.redColor, hoverColor: _MenubarTheme.hoverRedColor, ); diff --git a/src/ui/header.tis b/src/ui/header.tis index 009995f4f..1fb694397 100644 --- a/src/ui/header.tis +++ b/src/ui/header.tis @@ -434,9 +434,6 @@ function toggleMenuState() { var c = handler.get_option("codec-preference"); if (!c) c = "auto"; values.push(c); - var a = handler.get_audio_mode(); - if (!a) a = "guest-to-host"; - values.push(a); for (var el in $$(menu#display-options li)) { el.attributes.toggleClass("selected", values.indexOf(el.id) >= 0); } From ed441242bf290b4df7fba366ce29d692baa84994 Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 16 Feb 2023 14:54:13 +0800 Subject: [PATCH 1886/2015] add reconnect button on Connection Error Signed-off-by: 21pages --- flutter/lib/common.dart | 9 ++++++++- flutter/lib/models/model.dart | 32 +++++++++++++++++--------------- src/lang/ca.rs | 3 ++- src/lang/cn.rs | 5 +++-- src/lang/cs.rs | 3 ++- src/lang/da.rs | 3 ++- src/lang/de.rs | 3 ++- src/lang/eo.rs | 3 ++- src/lang/es.rs | 3 ++- src/lang/fa.rs | 3 ++- src/lang/fr.rs | 3 ++- src/lang/gr.rs | 3 ++- src/lang/hu.rs | 3 ++- src/lang/id.rs | 3 ++- src/lang/it.rs | 3 ++- src/lang/ja.rs | 3 ++- src/lang/ko.rs | 3 ++- src/lang/kz.rs | 3 ++- src/lang/nl.rs | 6 ++++-- src/lang/pl.rs | 3 ++- src/lang/pt_PT.rs | 3 ++- src/lang/ptbr.rs | 3 ++- src/lang/ro.rs | 3 ++- src/lang/ru.rs | 3 ++- src/lang/sk.rs | 3 ++- src/lang/sl.rs | 3 ++- src/lang/sq.rs | 3 ++- src/lang/sr.rs | 3 ++- src/lang/sv.rs | 3 ++- src/lang/template.rs | 3 ++- src/lang/th.rs | 3 ++- src/lang/tr.rs | 3 ++- src/lang/tw.rs | 11 ++++++----- src/lang/ua.rs | 3 ++- src/lang/vn.rs | 3 ++- 35 files changed, 98 insertions(+), 55 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 9f375860d..c01fe8910 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -676,7 +676,7 @@ class CustomAlertDialog extends StatelessWidget { void msgBox(String id, String type, String title, String text, String link, OverlayDialogManager dialogManager, - {bool? hasCancel}) { + {bool? hasCancel, ReconnectHandle? reconnect}) { dialogManager.dismissAll(); List buttons = []; bool hasOk = false; @@ -716,6 +716,13 @@ void msgBox(String id, String type, String title, String text, String link, dialogManager.dismissAll(); })); } + if (reconnect != null && title == "Connection Error") { + buttons.insert( + 0, + dialogButton('Reconnect', isOutline: true, onPressed: () { + reconnect(dialogManager, id, false); + })); + } if (link.isNotEmpty) { buttons.insert(0, dialogButton('JumpLink', onPressed: jumplink)); } diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 28d3ae622..458ca29f4 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -33,6 +33,7 @@ import 'input_model.dart'; import 'platform_model.dart'; typedef HandleMsgBox = Function(Map evt, String id); +typedef ReconnectHandle = Function(OverlayDialogManager, String, bool); final _waitForImage = {}; class FfiModel with ChangeNotifier { @@ -310,14 +311,12 @@ class FfiModel with ChangeNotifier { showMsgBox(String id, String type, String title, String text, String link, bool hasRetry, OverlayDialogManager dialogManager, {bool? hasCancel}) { - msgBox(id, type, title, text, link, dialogManager, hasCancel: hasCancel); + msgBox(id, type, title, text, link, dialogManager, + hasCancel: hasCancel, reconnect: reconnect); _timer?.cancel(); if (hasRetry) { _timer = Timer(Duration(seconds: _reconnects), () { - bind.sessionReconnect(id: id, forceRelay: false); - clearPermissions(); - dialogManager.showLoading(translate('Connecting...'), - onCancel: closeConnection); + reconnect(dialogManager, id, false); }); _reconnects *= 2; } else { @@ -325,6 +324,14 @@ class FfiModel with ChangeNotifier { } } + void reconnect( + OverlayDialogManager dialogManager, String id, bool forceRelay) { + bind.sessionReconnect(id: id, forceRelay: forceRelay); + clearPermissions(); + dialogManager.showLoading(translate('Connecting...'), + onCancel: closeConnection); + } + void showRelayHintDialog(String id, String type, String title, String text, OverlayDialogManager dialogManager) { dialogManager.show(tag: '$id-$type', (setState, close) { @@ -333,13 +340,6 @@ class FfiModel with ChangeNotifier { close(); } - reconnect(bool forceRelay) { - bind.sessionReconnect(id: id, forceRelay: forceRelay); - clearPermissions(); - dialogManager.showLoading(translate('Connecting...'), - onCancel: closeConnection); - } - final style = ElevatedButton.styleFrom(backgroundColor: Colors.green[700]); return CustomAlertDialog( @@ -348,14 +348,16 @@ class FfiModel with ChangeNotifier { "${translate(text)}\n\n${translate('relay_hint_tip')}"), actions: [ dialogButton('Close', onPressed: onClose, isOutline: true), - dialogButton('Retry', onPressed: () => reconnect(false)), + dialogButton('Retry', + onPressed: () => reconnect(dialogManager, id, false)), dialogButton('Connect via relay', - onPressed: () => reconnect(true), buttonStyle: style), + onPressed: () => reconnect(dialogManager, id, true), + buttonStyle: style), dialogButton('Always connect via relay', onPressed: () { const option = 'force-always-relay'; bind.sessionPeerOption( id: id, name: option, value: bool2option(option, true)); - reconnect(true); + reconnect(dialogManager, id, true); }, buttonStyle: style), ], onCancel: onClose, diff --git a/src/lang/ca.rs b/src/lang/ca.rs index d483a185d..3220c824a 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 7dea516ba..d0fdcb3fd 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -422,7 +422,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Ask the remote user for authentication", "请求远端用户授权"), ("Choose this if the remote account is administrator", "当对面电脑是管理员账号时选择该选项"), ("Transmit the username and password of administrator", "发送管理员账号的用户名密码"), - ("still_click_uac_tip", "依然需要被控端用戶在運行 RustDesk 的 UAC 窗口點擊確認。"), + ("still_click_uac_tip", "依然需要被控端用户在运行 RustDesk 的 UAC 窗口点击确认。"), ("Request Elevation", "请求提权"), ("wait_accept_uac_tip", "请等待远端用户确认 UAC 对话框。"), ("Elevate successfully", "提权成功"), @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", "文字聊天"), ("Stop voice call", "停止语音聊天"), ("relay_hint_tip", "可能无法直连,可以尝试中继连接。\n另外,如果想直接使用中继连接,可以在ID后面添加/r,或者在卡片选项里选择强制走中继连接。"), - ].iter().cloned().collect(); + ("Reconnect", "重连"), + ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 97a3ebc48..aca4778e6 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index bab81914e..7b959a778 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 05d02dd58..1672af2b9 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", "Text-Chat"), ("Stop voice call", "Sprachanruf beenden"), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 47eeb3367..9c9097f6e 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 4634cea81..dd1322873 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", "Chat de texto"), ("Stop voice call", "Detener llamada de voz"), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 2d0f29a5b..db565fe28 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", "گفتگو متنی (چت متنی)"), ("Stop voice call", "توقف تماس صوتی"), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 4e0e79aa0..fd46b4cf2 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/gr.rs b/src/lang/gr.rs index 09284738a..90c8e105a 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 16c99d207..78648a034 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index f4be0396f..d06cc649a 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 15f7b977f..57215e2e5 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", "Chat testuale"), ("Stop voice call", "Interrompi la chiamata vocale"), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index acf1c9b96..6e72d4b04 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index e1bc43182..b7b59ed9c 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 488290537..9fdc29260 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 3b01492d3..2502cb34c 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Handmatig gesloten door de peer"), ("Enable remote configuration modification", "Wijziging configuratie op afstand inschakelen"), ("Run without install", "Uitvoeren zonder installatie"), - ("Always connected via relay", "Altijd verbonden via relay"), + ("Connect via relay", ""), ("Always connect via relay", "Altijd verbinden via relay"), ("whitelist_tip", "Alleen een IP-adres op de witte lijst krijgt toegang tot mijn toestel"), ("Login", "Log In"), @@ -449,5 +449,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", "Spraakoproep"), ("Text chat", "Tekst chat"), ("Stop voice call", "Stop spraakoproep"), - ].iter().cloned().collect(); + ("relay_hint_tip", ""), + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index e6ba5b171..24563d21f 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index a1ad932b1..078bf3761 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 5ece46006..e08700d44 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index e9b83e298..5be2a914a 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index a8ef18d8a..4af362953 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", "Текстовый чат"), ("Stop voice call", "Завершить голосовой вызов"), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 47a795342..bf4b85b1b 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 1eb33b970..f464cb8fc 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 1ade9757a..a6b83d9f3 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index e5704093d..09c34b4fc 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 063892074..2154b2729 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 4190ba399..f46a301f6 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index 629c5ac77..93e984be3 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index b683fb78a..214ee83df 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index e4957e3d7..db26e5387 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -446,9 +446,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", "幀率"), ("Auto", "自動"), ("Other Default Options", "其它默認選項"), - ("Voice call", ""), - ("Text chat", ""), - ("Stop voice call", ""), - ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Voice call", "語音通話"), + ("Text chat", "文字聊天"), + ("Stop voice call", "停止語音聊天"), + ("relay_hint_tip", "可能無法直連,可以嘗試中繼連接。 \n另外,如果想直接使用中繼連接,可以在ID後面添加/r,或者在卡片選項裡選擇強制走中繼連接。"), + ("Reconnect", "重連"), + ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 3c1d7776a..c3894726a 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 76f611429..45c2cc519 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -450,5 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", ""), ("Stop voice call", ""), ("relay_hint_tip", ""), - ].iter().cloned().collect(); + ("Reconnect", ""), + ].iter().cloned().collect(); } From 24473ebd7bb8353c21b32d76b95ed8e11ee13050 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 16 Feb 2023 15:01:15 +0800 Subject: [PATCH 1887/2015] fix: issue #3231 --- flutter/lib/desktop/widgets/remote_menubar.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 0fa12cd6f..2b7f8c00a 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -1459,8 +1459,6 @@ class _RemoteMenubarState extends State { if (perms['audio'] != false) { displayMenu .add(_createSwitchMenuEntry('Mute', 'disable-audio', padding, true)); - displayMenu - .add(_createSwitchMenuEntry('Mute', 'disable-audio', padding, true)); } if (Platform.isWindows && From 9d4f899dfd6df25f6bef117d74593a90f796ba53 Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 16 Feb 2023 15:16:54 +0800 Subject: [PATCH 1888/2015] fix using default onSubmit after tab tapped Signed-off-by: 21pages --- flutter/lib/common.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index c01fe8910..0880fdb91 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -632,6 +632,7 @@ class CustomAlertDialog extends StatelessWidget { if (!scopeNode.hasFocus) scopeNode.requestFocus(); }); const double padding = 16; + bool tabTapped = false; return FocusScope( node: scopeNode, autofocus: true, @@ -641,13 +642,15 @@ class CustomAlertDialog extends StatelessWidget { onCancel?.call(); } return KeyEventResult.handled; // avoid TextField exception on escape - } else if (onSubmit != null && + } else if (!tabTapped && + onSubmit != null && key.logicalKey == LogicalKeyboardKey.enter) { if (key is RawKeyDownEvent) onSubmit?.call(); return KeyEventResult.handled; } else if (key.logicalKey == LogicalKeyboardKey.tab) { if (key is RawKeyDownEvent) { scopeNode.nextFocus(); + tabTapped = true; } return KeyEventResult.handled; } From 4cddaa4f0c97906593ce7301f725efb6fd0d86ce Mon Sep 17 00:00:00 2001 From: grummbeer Date: Wed, 15 Feb 2023 16:41:34 +0100 Subject: [PATCH 1889/2015] Unify button style for desktop --- flutter/lib/common.dart | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 0880fdb91..c2f8f9a34 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -500,12 +500,14 @@ class OverlayDialogManager { Offstage( offstage: !showCancel, child: Center( - child: TextButton( - style: flatButtonStyle, - onPressed: cancel, - child: Text(translate('Cancel'), - style: - const TextStyle(color: MyTheme.accent))))) + child: isDesktop + ? dialogButton('Cancel', onPressed: cancel) + : TextButton( + style: flatButtonStyle, + onPressed: cancel, + child: Text(translate('Cancel'), + style: const TextStyle( + color: MyTheme.accent))))) ])), onCancel: showCancel ? cancel : null, ); From b62a05e15f7e3ad3fc2803dcc60db91cc08467b4 Mon Sep 17 00:00:00 2001 From: grummbeer Date: Thu, 16 Feb 2023 07:45:31 +0100 Subject: [PATCH 1890/2015] CustomDialog. Set padding bottom to default if no actions set --- flutter/lib/common.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index c2f8f9a34..85aae4c80 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -662,8 +662,8 @@ class CustomAlertDialog extends StatelessWidget { scrollable: true, title: title, titlePadding: EdgeInsets.fromLTRB(padding, 24, padding, 0), - contentPadding: EdgeInsets.fromLTRB( - contentPadding ?? padding, 25, contentPadding ?? padding, 10), + contentPadding: EdgeInsets.fromLTRB(contentPadding ?? padding, 25, + contentPadding ?? padding, actions is List ? 10 : padding), content: ConstrainedBox( constraints: contentBoxConstraints, child: Theme( From 891121c64d179db48e117b8c010a0a301f6462aa Mon Sep 17 00:00:00 2001 From: grummbeer Date: Thu, 16 Feb 2023 11:05:07 +0100 Subject: [PATCH 1891/2015] Unify input labels. Remove colon from login labels --- flutter/lib/common/widgets/login.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flutter/lib/common/widgets/login.dart b/flutter/lib/common/widgets/login.dart index 14a2c38bc..43dc3a658 100644 --- a/flutter/lib/common/widgets/login.dart +++ b/flutter/lib/common/widgets/login.dart @@ -324,13 +324,13 @@ class LoginWidgetUserPass extends StatelessWidget { children: [ const SizedBox(height: 8.0), DialogTextField( - title: '${translate("Username")}:', + title: translate("Username"), controller: username, focusNode: userFocusNode, prefixIcon: Icon(Icons.account_circle_outlined), errorText: usernameMsg), DialogTextField( - title: '${translate("Password")}:', + title: translate("Password"), obscureText: true, controller: pass, prefixIcon: Icon(Icons.lock_outline), From 10305ab54809e720eb07a580b03a452346ad1bea Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 16 Feb 2023 20:01:06 +0800 Subject: [PATCH 1892/2015] refact text clipboard Signed-off-by: fufesou --- src/client.rs | 105 +++++++++++++++++++++++++++++++++-- src/client/io_loop.rs | 106 ++++++++++++------------------------ src/flutter.rs | 28 ++++++++++ src/ui/remote.rs | 5 +- src/ui_session_interface.rs | 45 +++++++++++++-- 5 files changed, 207 insertions(+), 82 deletions(-) diff --git a/src/client.rs b/src/client.rs index 77221bdb2..97012e516 100644 --- a/src/client.rs +++ b/src/client.rs @@ -3,7 +3,7 @@ use std::{ net::SocketAddr, ops::{Deref, Not}, str::FromStr, - sync::{atomic::AtomicBool, mpsc, Arc, Mutex, RwLock}, + sync::{mpsc, Arc, Mutex, RwLock}, }; pub use async_trait::async_trait; @@ -34,7 +34,7 @@ use hbb_common::{ socket_client, sodiumoxide::crypto::{box_, secretbox, sign}, timeout, - tokio::time::Duration, + tokio::{sync::mpsc::UnboundedSender, time::Duration}, AddrMangle, ResultType, Stream, }; pub use helper::LatencyController; @@ -50,21 +50,30 @@ use crate::{ server::video_service::{SCRAP_X11_REF_URL, SCRAP_X11_REQUIRED}, }; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use crate::{ + common::{check_clipboard, ClipboardContext, CLIPBOARD_INTERVAL}, + ui_session_interface::SessionPermissionConfig, +}; + pub use super::lang::*; pub mod file_trait; pub mod helper; pub mod io_loop; -pub static SERVER_KEYBOARD_ENABLED: AtomicBool = AtomicBool::new(true); -pub static SERVER_FILE_TRANSFER_ENABLED: AtomicBool = AtomicBool::new(true); -pub static SERVER_CLIPBOARD_ENABLED: AtomicBool = AtomicBool::new(true); pub const MILLI1: Duration = Duration::from_millis(1); pub const SEC30: Duration = Duration::from_secs(30); /// Client of the remote desktop. pub struct Client; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +struct TextClipboardState { + is_required: bool, + running: bool, +} + #[cfg(not(any(target_os = "android", target_os = "linux")))] lazy_static::lazy_static! { static ref AUDIO_HOST: Host = cpal::default_host(); @@ -73,6 +82,8 @@ lazy_static::lazy_static! { #[cfg(not(any(target_os = "android", target_os = "ios")))] lazy_static::lazy_static! { static ref ENIGO: Arc> = Arc::new(Mutex::new(enigo::Enigo::new())); + static ref OLD_CLIPBOARD_TEXT: Arc> = Default::default(); + static ref TEXT_CLIPBOARD_STATE: Arc> = Arc::new(Mutex::new(TextClipboardState::new())); } #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -598,6 +609,86 @@ impl Client { conn.send(&msg_out).await?; Ok(conn) } + + #[inline] + #[cfg(feature = "flutter")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + pub fn set_is_text_clipboard_required(b: bool) { + TEXT_CLIPBOARD_STATE.lock().unwrap().is_required = b; + } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + fn try_stop_clipboard(_self_id: &str) { + #[cfg(feature = "flutter")] + if crate::flutter::other_sessions_running(_self_id) { + return; + } + TEXT_CLIPBOARD_STATE.lock().unwrap().running = false; + } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + fn try_start_clipboard(_conf_tx: Option<(SessionPermissionConfig, UnboundedSender)>) { + let mut clipboard_lock = TEXT_CLIPBOARD_STATE.lock().unwrap(); + if clipboard_lock.running { + return; + } + + match ClipboardContext::new() { + Ok(mut ctx) => { + clipboard_lock.running = true; + // ignore clipboard update before service start + check_clipboard(&mut ctx, Some(&OLD_CLIPBOARD_TEXT)); + std::thread::spawn(move || { + log::info!("Start text clipboard loop"); + loop { + std::thread::sleep(Duration::from_millis(CLIPBOARD_INTERVAL)); + if !TEXT_CLIPBOARD_STATE.lock().unwrap().running { + break; + } + + if !TEXT_CLIPBOARD_STATE.lock().unwrap().is_required { + continue; + } + + if let Some(msg) = check_clipboard(&mut ctx, Some(&OLD_CLIPBOARD_TEXT)) { + #[cfg(feature = "flutter")] + crate::flutter::send_text_clipboard_msg(msg); + #[cfg(not(feature = "flutter"))] + if let Some((cfg, tx)) = &_conf_tx { + if cfg.is_text_clipboard_required() { + let _ = tx.send(Data::Message(msg)); + } + } + } + } + log::info!("Stop text clipboard loop"); + }); + } + Err(err) => { + log::error!("Failed to start clipboard service of client: {}", err); + } + } + } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + fn get_current_text_clipboard_msg() -> Option { + let txt = &*OLD_CLIPBOARD_TEXT.lock().unwrap(); + if txt.is_empty() { + None + } else { + Some(crate::create_clipboard_msg(txt.clone())) + } + } +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +impl TextClipboardState { + fn new() -> Self { + Self { + is_required: true, + running: false, + } + } } /// Audio handler for the [`Client`]. @@ -1148,6 +1239,10 @@ impl LoginConfigHandler { if !name.contains("block-input") { self.save_config(config); } + #[cfg(feature = "flutter")] + if name == "disable-clipboard" { + crate::flutter::update_text_clipboard_required(); + } let mut misc = Misc::new(); misc.set_option(option); let mut msg_out = Message::new(); diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index de91b091d..427d0a72a 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -26,10 +26,10 @@ use hbb_common::{fs, log, Stream}; use crate::client::{ new_voice_call_request, Client, CodecFormat, MediaData, MediaSender, QualityStatus, MILLI1, - SEC30, SERVER_CLIPBOARD_ENABLED, SERVER_FILE_TRANSFER_ENABLED, SERVER_KEYBOARD_ENABLED, + SEC30, }; #[cfg(not(any(target_os = "android", target_os = "ios")))] -use crate::common::{check_clipboard, update_clipboard, ClipboardContext, CLIPBOARD_INTERVAL}; +use crate::common::update_clipboard; use crate::common::{get_default_sound_input, set_sound_input}; use crate::ui_session_interface::{InvokeUiSession, Session}; use crate::{audio_service, common, ConnInner, CLIENT_SERVER}; @@ -91,7 +91,6 @@ impl Remote { } pub async fn io_loop(&mut self, key: &str, token: &str) { - let stop_clipboard = self.start_clipboard(); let mut last_recv_time = Instant::now(); let mut received = false; let conn_type = if self.handler.is_file_transfer() { @@ -110,9 +109,6 @@ impl Remote { .await { Ok((mut peer, direct)) => { - SERVER_KEYBOARD_ENABLED.store(true, Ordering::SeqCst); - SERVER_CLIPBOARD_ENABLED.store(true, Ordering::SeqCst); - SERVER_FILE_TRANSFER_ENABLED.store(true, Ordering::SeqCst); self.handler.set_connection_type(peer.is_secured(), direct); // flutter -> connection_ready self.handler.set_connection_info(direct, false); @@ -237,12 +233,7 @@ impl Remote { .msgbox("error", "Connection Error", &err.to_string(), ""); } } - if let Some(stop) = stop_clipboard { - stop.send(()).ok(); - } - SERVER_KEYBOARD_ENABLED.store(false, Ordering::SeqCst); - SERVER_CLIPBOARD_ENABLED.store(false, Ordering::SeqCst); - SERVER_FILE_TRANSFER_ENABLED.store(false, Ordering::SeqCst); + Client::try_stop_clipboard(&self.handler.id); } fn handle_job_status(&mut self, id: i32, file_num: i32, err: Option) { @@ -347,46 +338,6 @@ impl Remote { Some(tx) } - fn start_clipboard(&mut self) -> Option> { - if self.handler.is_file_transfer() || self.handler.is_port_forward() { - return None; - } - let (tx, rx) = std::sync::mpsc::channel(); - let old_clipboard = self.old_clipboard.clone(); - let tx_protobuf = self.sender.clone(); - let lc = self.handler.lc.clone(); - #[cfg(not(any(target_os = "android", target_os = "ios")))] - match ClipboardContext::new() { - Ok(mut ctx) => { - // ignore clipboard update before service start - check_clipboard(&mut ctx, Some(&old_clipboard)); - std::thread::spawn(move || loop { - std::thread::sleep(Duration::from_millis(CLIPBOARD_INTERVAL)); - match rx.try_recv() { - Ok(_) | Err(std::sync::mpsc::TryRecvError::Disconnected) => { - log::debug!("Exit clipboard service of client"); - break; - } - _ => {} - } - if !SERVER_CLIPBOARD_ENABLED.load(Ordering::SeqCst) - || !SERVER_KEYBOARD_ENABLED.load(Ordering::SeqCst) - || lc.read().unwrap().disable_clipboard.v - { - continue; - } - if let Some(msg) = check_clipboard(&mut ctx, Some(&old_clipboard)) { - tx_protobuf.send(Data::Message(msg)).ok(); - } - }); - } - Err(err) => { - log::error!("Failed to start clipboard service of client: {}", err); - } - } - Some(tx) - } - async fn handle_msg_from_ui(&mut self, data: Data, peer: &mut Stream) -> bool { match data { Data::Close => { @@ -885,22 +836,28 @@ impl Remote { Some(login_response::Union::PeerInfo(pi)) => { self.handler.handle_peer_info(pi); self.check_clipboard_file_context(); - if !(self.handler.is_file_transfer() - || self.handler.is_port_forward() - || !SERVER_CLIPBOARD_ENABLED.load(Ordering::SeqCst) - || !SERVER_KEYBOARD_ENABLED.load(Ordering::SeqCst) - || self.handler.lc.read().unwrap().disable_clipboard.v) - { - let txt = self.old_clipboard.lock().unwrap().clone(); - if !txt.is_empty() { - let msg_out = crate::create_clipboard_msg(txt); - let sender = self.sender.clone(); - tokio::spawn(async move { - // due to clipboard service interval time - sleep(common::CLIPBOARD_INTERVAL as f32 / 1_000.).await; - sender.send(Data::Message(msg_out)).ok(); - }); - } + if !(self.handler.is_file_transfer() || self.handler.is_port_forward()) { + let sender = self.sender.clone(); + let permission_config = self.handler.get_permission_config(); + + #[cfg(feature = "flutter")] + Client::try_start_clipboard(None); + #[cfg(not(feature = "flutter"))] + Client::try_start_clipboard(Some(( + permission_config.clone(), + sender.clone(), + ))); + + tokio::spawn(async move { + // due to clipboard service interval time + sleep(common::CLIPBOARD_INTERVAL as f32 / 1_000.).await; + if permission_config.is_text_clipboard_required() { + if let Some(msg_out) = Client::get_current_text_clipboard_msg() + { + sender.send(Data::Message(msg_out)).ok(); + } + } + }); } if self.handler.is_file_transfer() { @@ -1092,18 +1049,23 @@ impl Remote { log::info!("Change permission {:?} -> {}", p.permission, p.enabled); match p.permission.enum_value_or_default() { Permission::Keyboard => { - SERVER_KEYBOARD_ENABLED.store(p.enabled, Ordering::SeqCst); + #[cfg(feature = "flutter")] + crate::flutter::update_text_clipboard_required(); + *self.handler.server_keyboard_enabled.write().unwrap() = p.enabled; self.handler.set_permission("keyboard", p.enabled); } Permission::Clipboard => { - SERVER_CLIPBOARD_ENABLED.store(p.enabled, Ordering::SeqCst); + #[cfg(feature = "flutter")] + crate::flutter::update_text_clipboard_required(); + *self.handler.server_clipboard_enabled.write().unwrap() = p.enabled; self.handler.set_permission("clipboard", p.enabled); } Permission::Audio => { self.handler.set_permission("audio", p.enabled); } Permission::File => { - SERVER_FILE_TRANSFER_ENABLED.store(p.enabled, Ordering::SeqCst); + *self.handler.server_file_transfer_enabled.write().unwrap() = + p.enabled; if !p.enabled && self.handler.is_file_transfer() { return true; } @@ -1416,7 +1378,7 @@ impl Remote { fn check_clipboard_file_context(&self) { #[cfg(windows)] { - let enabled = SERVER_FILE_TRANSFER_ENABLED.load(Ordering::SeqCst) + let enabled = *self.handler.server_file_transfer_enabled.read().unwrap() && self.handler.lc.read().unwrap().enable_file_transfer.v; ContextSend::enable(enabled); } diff --git a/src/flutter.rs b/src/flutter.rs index bd1f4f1af..c8f875da5 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -464,6 +464,9 @@ pub fn session_add( let session: Session = Session { id: session_id.clone(), + server_keyboard_enabled: Arc::new(RwLock::new(true)), + server_file_transfer_enabled: Arc::new(RwLock::new(true)), + server_clipboard_enabled: Arc::new(RwLock::new(true)), ..Default::default() }; @@ -514,6 +517,31 @@ pub fn session_start_(id: &str, event_stream: StreamSink) -> ResultTy } } +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn update_text_clipboard_required() { + let is_required = SESSIONS + .read() + .unwrap() + .iter() + .any(|(_id, session)| session.is_text_clipboard_required()); + Client::set_is_text_clipboard_required(is_required); +} + +#[inline] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn other_sessions_running(id: &str) -> bool { + SESSIONS.read().unwrap().keys().filter(|k| *k != id).count() != 0 +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn send_text_clipboard_msg(msg: Message) { + for (_id, session) in SESSIONS.read().unwrap().iter() { + if session.is_text_clipboard_required() { + session.send(Data::Message(msg.clone())); + } + } +} + // Server Side #[cfg(not(any(target_os = "ios")))] pub mod connection_manager { diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 1725a8f41..a86f07d0f 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -1,7 +1,7 @@ use std::{ collections::HashMap, ops::{Deref, DerefMut}, - sync::{Arc, Mutex}, + sync::{Arc, Mutex, RwLock}, }; use sciter::{ @@ -454,6 +454,9 @@ impl SciterSession { id: id.clone(), password: password.clone(), args, + server_keyboard_enabled: Arc::new(RwLock::new(true)), + server_file_transfer_enabled: Arc::new(RwLock::new(true)), + server_clipboard_enabled: Arc::new(RwLock::new(true)), ..Default::default() }; diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 97db904d4..947f8fb6f 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -1,9 +1,11 @@ use std::collections::HashMap; use std::ops::{Deref, DerefMut}; use std::str::FromStr; -use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; -use std::sync::{Arc, Mutex, RwLock}; -use std::time::Duration; +use std::sync::{ + atomic::{AtomicBool, AtomicUsize, Ordering}, + Arc, Mutex, RwLock, +}; +use std::time::{Duration, SystemTime}; use async_trait::async_trait; use bytes::Bytes; @@ -37,9 +39,38 @@ pub struct Session { pub sender: Arc>>>, pub thread: Arc>>>, pub ui_handler: T, + pub server_keyboard_enabled: Arc>, + pub server_file_transfer_enabled: Arc>, + pub server_clipboard_enabled: Arc>, +} + +#[derive(Clone)] +pub struct SessionPermissionConfig { + pub lc: Arc>, + pub server_keyboard_enabled: Arc>, + pub server_file_transfer_enabled: Arc>, + pub server_clipboard_enabled: Arc>, +} + +impl SessionPermissionConfig { + pub fn is_text_clipboard_required(&self) -> bool { + println!("REMOVE ME ==================== is_text_clipboard_required {} -{}-{}", *self.server_clipboard_enabled.read().unwrap(), *self.server_keyboard_enabled.read().unwrap(), !self.lc.read().unwrap().disable_clipboard.v); + *self.server_clipboard_enabled.read().unwrap() + && *self.server_keyboard_enabled.read().unwrap() + && !self.lc.read().unwrap().disable_clipboard.v + } } impl Session { + pub fn get_permission_config(&self) -> SessionPermissionConfig { + SessionPermissionConfig { + lc: self.lc.clone(), + server_keyboard_enabled: self.server_keyboard_enabled.clone(), + server_file_transfer_enabled: self.server_file_transfer_enabled.clone(), + server_clipboard_enabled: self.server_clipboard_enabled.clone(), + } + } + pub fn is_file_transfer(&self) -> bool { self.lc .read() @@ -128,6 +159,12 @@ impl Session { self.lc.read().unwrap().is_privacy_mode_supported() } + pub fn is_text_clipboard_required(&self) -> bool { + *self.server_clipboard_enabled.read().unwrap() + && *self.server_keyboard_enabled.read().unwrap() + && !self.lc.read().unwrap().disable_clipboard.v + } + pub fn refresh_video(&self) { self.send(Data::Message(LoginConfigHandler::refresh())); } @@ -445,7 +482,7 @@ impl Session { KeyRelease(key) }; let event = Event { - time: std::time::SystemTime::now(), + time: SystemTime::now(), unicode: None, code: keycode as _, scan_code: scancode as _, From 241925dc83c7b656e92171977eb1676c2c0e1908 Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 16 Feb 2023 20:28:06 +0800 Subject: [PATCH 1893/2015] remove debug print Signed-off-by: fufesou --- src/ui_session_interface.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 947f8fb6f..2344f84a1 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -54,7 +54,6 @@ pub struct SessionPermissionConfig { impl SessionPermissionConfig { pub fn is_text_clipboard_required(&self) -> bool { - println!("REMOVE ME ==================== is_text_clipboard_required {} -{}-{}", *self.server_clipboard_enabled.read().unwrap(), *self.server_keyboard_enabled.read().unwrap(), !self.lc.read().unwrap().disable_clipboard.v); *self.server_clipboard_enabled.read().unwrap() && *self.server_keyboard_enabled.read().unwrap() && !self.lc.read().unwrap().disable_clipboard.v From 0d2113cd293446317ec1f64a263347615070ae0c Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 16 Feb 2023 20:48:42 +0800 Subject: [PATCH 1894/2015] build android Signed-off-by: fufesou --- src/client.rs | 5 ++++- src/client/io_loop.rs | 6 ++++++ src/flutter_ffi.rs | 4 +++- src/keyboard.rs | 4 +++- src/ui_session_interface.rs | 1 + 5 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/client.rs b/src/client.rs index 97012e516..51e7f9a29 100644 --- a/src/client.rs +++ b/src/client.rs @@ -18,6 +18,8 @@ use sha2::{Digest, Sha256}; use uuid::Uuid; pub use file_trait::FileManager; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use hbb_common::tokio::sync::mpsc::UnboundedSender; use hbb_common::{ allow_err, anyhow::{anyhow, Context}, @@ -34,7 +36,7 @@ use hbb_common::{ socket_client, sodiumoxide::crypto::{box_, secretbox, sign}, timeout, - tokio::{sync::mpsc::UnboundedSender, time::Duration}, + tokio::time::Duration, AddrMangle, ResultType, Stream, }; pub use helper::LatencyController; @@ -1240,6 +1242,7 @@ impl LoginConfigHandler { self.save_config(config); } #[cfg(feature = "flutter")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] if name == "disable-clipboard" { crate::flutter::update_text_clipboard_required(); } diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 427d0a72a..c673531ec 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -233,6 +233,7 @@ impl Remote { .msgbox("error", "Connection Error", &err.to_string(), ""); } } + #[cfg(not(any(target_os = "android", target_os = "ios")))] Client::try_stop_clipboard(&self.handler.id); } @@ -841,13 +842,16 @@ impl Remote { let permission_config = self.handler.get_permission_config(); #[cfg(feature = "flutter")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] Client::try_start_clipboard(None); #[cfg(not(feature = "flutter"))] + #[cfg(not(any(target_os = "android", target_os = "ios")))] Client::try_start_clipboard(Some(( permission_config.clone(), sender.clone(), ))); + #[cfg(not(any(target_os = "android", target_os = "ios")))] tokio::spawn(async move { // due to clipboard service interval time sleep(common::CLIPBOARD_INTERVAL as f32 / 1_000.).await; @@ -1050,12 +1054,14 @@ impl Remote { match p.permission.enum_value_or_default() { Permission::Keyboard => { #[cfg(feature = "flutter")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] crate::flutter::update_text_clipboard_required(); *self.handler.server_keyboard_enabled.write().unwrap() = p.enabled; self.handler.set_permission("keyboard", p.enabled); } Permission::Clipboard => { #[cfg(feature = "flutter")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] crate::flutter::update_text_clipboard_required(); *self.handler.server_clipboard_enabled.write().unwrap() = p.enabled; self.handler.set_permission("clipboard", p.enabled); diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 0aa7de07f..f3bc45856 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1,11 +1,13 @@ use crate::{ client::file_trait::FileManager, common::make_fd_to_json, - common::{get_default_sound_input, is_keyboard_mode_supported}, + common::is_keyboard_mode_supported, flutter::{self, SESSIONS}, flutter::{session_add, session_start_}, ui_interface::{self, *}, }; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use crate::common::get_default_sound_input; use flutter_rust_bridge::{StreamSink, SyncReturn}; use hbb_common::{ config::{self, LocalConfig, PeerConfig, ONLINE}, diff --git a/src/keyboard.rs b/src/keyboard.rs index 4dcbe5c97..3f7ed6779 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -5,7 +5,9 @@ use crate::common::GrabState; use crate::flutter::{CUR_SESSION_ID, SESSIONS}; #[cfg(not(any(feature = "flutter", feature = "cli")))] use crate::ui::CUR_SESSION; -use hbb_common::{log, message_proto::*}; +use hbb_common::message_proto::*; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use hbb_common::log; use rdev::{Event, EventType, Key}; #[cfg(any(target_os = "windows", target_os = "macos"))] use std::sync::atomic::{AtomicBool, Ordering}; diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 2344f84a1..b225151ff 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -1,3 +1,4 @@ +#[cfg(not(any(target_os = "android", target_os = "ios")))] use std::collections::HashMap; use std::ops::{Deref, DerefMut}; use std::str::FromStr; From 4cd36e9bd0b3405313f20d67e7da8d71366cc370 Mon Sep 17 00:00:00 2001 From: grummbeer Date: Thu, 16 Feb 2023 16:23:46 +0100 Subject: [PATCH 1895/2015] Unify password field behavior --- .../desktop/pages/desktop_setting_page.dart | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 378ddbd1b..25c485a2a 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -1832,6 +1832,7 @@ void changeSocks5Proxy() async { var proxyController = TextEditingController(text: proxy); var userController = TextEditingController(text: username); var pwdController = TextEditingController(text: password); + RxBool obscure = true.obs; var isInProgress = false; gFFI.dialogManager.show((setState, close) { @@ -1929,12 +1930,17 @@ void changeSocks5Proxy() async { width: 24.0, ), Expanded( - child: TextField( - decoration: const InputDecoration( - border: OutlineInputBorder(), - ), - controller: pwdController, - ), + child: Obx(() => TextField( + obscureText: obscure.value, + decoration: InputDecoration( + border: const OutlineInputBorder(), + suffixIcon: IconButton( + onPressed: () => obscure.value = !obscure.value, + icon: Icon(obscure.value + ? Icons.visibility_off + : Icons.visibility))), + controller: pwdController, + )), ), ], ), From 6432183bb4ff58776ad8682c9e8e55200609d1cf Mon Sep 17 00:00:00 2001 From: "Miguel F. G" <116861809+flusheDData@users.noreply.github.com> Date: Thu, 16 Feb 2023 16:44:39 +0100 Subject: [PATCH 1896/2015] Update es.rs New terms added --- src/lang/es.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/es.rs b/src/lang/es.rs index dd1322873..63c1d26fc 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -449,7 +449,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", "Llamada de voz"), ("Text chat", "Chat de texto"), ("Stop voice call", "Detener llamada de voz"), - ("relay_hint_tip", ""), - ("Reconnect", ""), + ("relay_hint_tip", "Puede que no sea posible conectar directamente. Puedes tratar de conectar a través de relay. \nAdicionalmente, si quieres usar relay en el primer intento, puedes añadir el sufijo \"/r\" a la ID o seleccionar la opción \"Conectar siempre a través de relay\" en la tarjeta del par."), + ("Reconnect", "Reconectar"), ].iter().cloned().collect(); } From a0caf8f257d43bc83df8edb6c17d5d2a3ec3ae1d Mon Sep 17 00:00:00 2001 From: ilGigioVr88 Date: Thu, 16 Feb 2023 17:15:37 +0100 Subject: [PATCH 1897/2015] Update it.rs --- src/lang/it.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/it.rs b/src/lang/it.rs index 57215e2e5..ab0c8064c 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -450,6 +450,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Text chat", "Chat testuale"), ("Stop voice call", "Interrompi la chiamata vocale"), ("relay_hint_tip", ""), - ("Reconnect", ""), + ("Reconnect", "Riconnetti"), ].iter().cloned().collect(); } From 897f694ad4a76d35cac57891eddb5204eebcd1ce Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Thu, 16 Feb 2023 18:17:42 +0100 Subject: [PATCH 1898/2015] fix for #3240 --- flutter/assets/actions_mobile.svg | 2 ++ flutter/lib/desktop/widgets/remote_menubar.dart | 10 +++++++--- 2 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 flutter/assets/actions_mobile.svg diff --git a/flutter/assets/actions_mobile.svg b/flutter/assets/actions_mobile.svg new file mode 100644 index 000000000..6aed6053e --- /dev/null +++ b/flutter/assets/actions_mobile.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 2b7f8c00a..3bec6862a 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -413,14 +413,18 @@ class _RemoteMenubarState extends State { menubarItems.add(_buildPinMenubar(context)); menubarItems.add(_buildFullscreen(context)); if (widget.ffi.ffiModel.isPeerAndroid) { - menubarItems.add(IconButton( + menubarItems.add(MenuButton( tooltip: translate('Mobile Actions'), - color: _MenubarTheme.blueColor, - icon: const Icon(Icons.build), + icon: SvgPicture.asset( + "assets/actions_mobile.svg", + color: Colors.white, + ), onPressed: () { widget.ffi.dialogManager .toggleMobileActionsOverlay(ffi: widget.ffi); }, + color: _MenubarTheme.blueColor, + hoverColor: _MenubarTheme.hoverBlueColor, )); } } From 285b5033165f48b802852d1eba16f97c7b0fd377 Mon Sep 17 00:00:00 2001 From: grummbeer Date: Thu, 16 Feb 2023 19:20:26 +0100 Subject: [PATCH 1899/2015] improve input of permanent password --- .../lib/desktop/pages/desktop_home_page.dart | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index d9afbea55..b5cadbcdf 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -596,13 +596,13 @@ void setPasswordDialog() async { }); final pass = p0.text.trim(); if (pass.isNotEmpty) { - for (var r in rules) { - if (!r.validate(pass)) { - setState(() { - errMsg0 = '${translate('Prompt')}: ${r.name}'; - }); - return; - } + final Iterable violations = rules.where((r) => !r.validate(pass)); + if (violations.isNotEmpty) { + setState(() { + errMsg0 = + '${translate('Prompt')}: ${violations.map((r) => r.name).join(', ')}'; + }); + return; } } if (p1.text.trim() != pass) { @@ -639,6 +639,9 @@ void setPasswordDialog() async { autofocus: true, onChanged: (value) { rxPass.value = value.trim(); + setState(() { + errMsg0 = ''; + }); }, ), ), @@ -662,6 +665,11 @@ void setPasswordDialog() async { labelText: translate('Confirmation'), errorText: errMsg1.isNotEmpty ? errMsg1 : null), controller: p1, + onChanged: (value) { + setState(() { + errMsg1 = ''; + }); + }, ), ), ], From 512563f7967182918f67fc1d8c435c7e2959f987 Mon Sep 17 00:00:00 2001 From: solokot Date: Fri, 17 Feb 2023 02:08:02 +0300 Subject: [PATCH 1900/2015] update ru.rs --- src/lang/ru.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 4af362953..c389d6821 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -209,8 +209,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Закрыто удалённым узлом вручную"), ("Enable remote configuration modification", "Разрешить удалённое изменение конфигурации"), ("Run without install", "Запустить без установки"), - ("Connect via relay", ""), - ("Always connect via relay", "Всегда подключаться через ретрансляционный сервер"), + ("Connect via relay", "Подключится через ретранслятор"), + ("Always connect via relay", "Всегда подключаться через ретранслятор"), ("whitelist_tip", "Только IP-адреса из белого списка могут получить доступ ко мне"), ("Login", "Войти"), ("Verify", "Проверить"), @@ -449,7 +449,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", "Голосовой вызов"), ("Text chat", "Текстовый чат"), ("Stop voice call", "Завершить голосовой вызов"), - ("relay_hint_tip", ""), - ("Reconnect", ""), + ("relay_hint_tip", "Прямое подключение может оказаться невозможным. В этом случае можно попытаться подключиться через сервер ретрансляции. \nКроме того, если вы хотите сразу использовать сервер ретрансляции, можно добавить к ID суффикс \"/r\" или включить \"Всегда подключаться через ретранслятор\" в настройках удалённого узла."), + ("Reconnect", "Переподключить"), ].iter().cloned().collect(); } From 000799d1814e7d854f53ce5c51aad0829c7aebbf Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 17 Feb 2023 11:59:03 +0800 Subject: [PATCH 1901/2015] fix CI --- .github/workflows/flutter-ci.yml | 2 +- .github/workflows/flutter-nightly.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/flutter-ci.yml b/.github/workflows/flutter-ci.yml index 5d4cf39c9..78c60df37 100644 --- a/.github/workflows/flutter-ci.yml +++ b/.github/workflows/flutter-ci.yml @@ -105,7 +105,7 @@ jobs: - name: Install build runtime run: | - brew install llvm create-dmg nasm yasm cmake gcc wget ninja + brew install llvm create-dmg nasm yasm cmake gcc wget ninja pkg-config - name: Install flutter uses: subosito/flutter-action@v2 diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 1ab21dbff..ffcadd18b 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -183,7 +183,7 @@ jobs: - name: Install build runtime run: | - brew install llvm create-dmg nasm yasm cmake gcc wget ninja + brew install llvm create-dmg nasm yasm cmake gcc wget ninja pkg-config - name: Install flutter uses: subosito/flutter-action@v2 From 302499d1e01babe5d7eb147b07407e1bbfd3d4d5 Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 17 Feb 2023 13:32:17 +0800 Subject: [PATCH 1902/2015] fix sync displays info && select monitor menu Signed-off-by: fufesou --- .../widgets/material_mod_popup_menu.dart | 2 +- flutter/lib/desktop/widgets/menu_button.dart | 6 +- .../lib/desktop/widgets/remote_menubar.dart | 86 ++++++++++--------- flutter/lib/models/model.dart | 24 ++++++ flutter/lib/models/state_model.dart | 1 + libs/hbb_common/protos/message.proto | 1 + src/client/io_loop.rs | 8 ++ src/common.rs | 2 + src/flutter.rs | 33 ++++--- src/server/connection.rs | 84 ++++++++++-------- src/server/video_service.rs | 52 +++++++++-- src/ui/header.tis | 8 ++ src/ui/remote.rs | 34 +++++--- src/ui_session_interface.rs | 1 + 14 files changed, 234 insertions(+), 108 deletions(-) diff --git a/flutter/lib/desktop/widgets/material_mod_popup_menu.dart b/flutter/lib/desktop/widgets/material_mod_popup_menu.dart index 47de1be20..3e85cb296 100644 --- a/flutter/lib/desktop/widgets/material_mod_popup_menu.dart +++ b/flutter/lib/desktop/widgets/material_mod_popup_menu.dart @@ -1400,7 +1400,7 @@ class PopupMenuButtonState extends State> { } return MenuButton( - icon: widget.icon ?? Icon(Icons.adaptive.more), + child: widget.icon ?? Icon(Icons.adaptive.more), tooltip: widget.tooltip ?? MaterialLocalizations.of(context).showMenuTooltip, onPressed: widget.enabled ? showButtonMenu : null, diff --git a/flutter/lib/desktop/widgets/menu_button.dart b/flutter/lib/desktop/widgets/menu_button.dart index 7c9fe67eb..96cc9fa9b 100644 --- a/flutter/lib/desktop/widgets/menu_button.dart +++ b/flutter/lib/desktop/widgets/menu_button.dart @@ -5,7 +5,7 @@ class MenuButton extends StatefulWidget { final Color color; final Color hoverColor; final Color? splashColor; - final Widget icon; + final Widget child; final String? tooltip; final EdgeInsetsGeometry padding; final bool enableFeedback; @@ -14,7 +14,7 @@ class MenuButton extends StatefulWidget { required this.onPressed, required this.color, required this.hoverColor, - required this.icon, + required this.child, this.splashColor, this.tooltip = "", this.padding = const EdgeInsets.symmetric(horizontal: 3, vertical: 6), @@ -51,7 +51,7 @@ class _MenuButtonState extends State { splashColor: widget.splashColor, enableFeedback: widget.enableFeedback, onTap: widget.onPressed, - child: widget.icon, + child: widget.child, ), ), ), diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 2b7f8c00a..c97ef9d32 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -472,7 +472,7 @@ class _RemoteMenubarState extends State { onPressed: () { widget.state.switchPin(); }, - icon: SvgPicture.asset( + child: SvgPicture.asset( pin ? "assets/pinned.svg" : "assets/unpinned.svg", color: Colors.white, ), @@ -488,7 +488,7 @@ class _RemoteMenubarState extends State { onPressed: () { _setFullscreen(!isFullscreen); }, - icon: SvgPicture.asset( + child: SvgPicture.asset( isFullscreen ? "assets/fullscreen_exit.svg" : "assets/fullscreen.svg", color: Colors.white, ), @@ -499,7 +499,7 @@ class _RemoteMenubarState extends State { Widget _buildMonitor(BuildContext context) { final pi = widget.ffi.ffiModel.pi; - return mod_menu.PopupMenuButton( + final monitor = mod_menu.PopupMenuButton( tooltip: translate('Select Monitor'), position: mod_menu.PopupMenuPosition.under, icon: Stack( @@ -524,43 +524,44 @@ class _RemoteMenubarState extends State { itemBuilder: (BuildContext context) { final List rowChildren = []; for (int i = 0; i < pi.displays.length; i++) { - rowChildren.add( - Stack( - alignment: Alignment.center, - children: [ - SvgPicture.asset( - "assets/display.svg", - color: Colors.white, - ), - TextButton( - child: Container( - alignment: AlignmentDirectional.center, - constraints: - const BoxConstraints(minHeight: _MenubarTheme.height), - child: Padding( - padding: const EdgeInsets.only(bottom: 2.5), - child: Text( - (i + 1).toString(), - style: TextStyle( - color: Colors.white, - ), + rowChildren.add(MenuButton( + color: _MenubarTheme.blueColor, + hoverColor: _MenubarTheme.hoverBlueColor, + child: Container( + alignment: AlignmentDirectional.center, + constraints: + const BoxConstraints(minHeight: _MenubarTheme.height), + child: Stack( + alignment: Alignment.center, + children: [ + SvgPicture.asset( + "assets/display.svg", + color: Colors.white, + ), + Padding( + padding: const EdgeInsets.only(bottom: 2.5), + child: Text( + (i + 1).toString(), + style: TextStyle( + color: Colors.white, + fontSize: 12, ), ), - ), - onPressed: () { - if (Navigator.canPop(context)) { - Navigator.pop(context); - _menuDismissCallback(); - } - RxInt display = CurrentDisplayState.find(widget.id); - if (display.value != i) { - bind.sessionSwitchDisplay(id: widget.id, value: i); - } - }, - ) - ], + ) + ], + ), ), - ); + onPressed: () { + if (Navigator.canPop(context)) { + Navigator.pop(context); + _menuDismissCallback(); + } + RxInt display = CurrentDisplayState.find(widget.id); + if (display.value != i) { + bind.sessionSwitchDisplay(id: widget.id, value: i); + } + }, + )); } return >[ mod_menu.PopupMenuItem( @@ -576,6 +577,11 @@ class _RemoteMenubarState extends State { ]; }, ); + + return Obx(() => Offstage( + offstage: stateGlobal.displaysCount.value < 2, + child: monitor, + )); } Widget _buildControl(BuildContext context) { @@ -674,7 +680,7 @@ class _RemoteMenubarState extends State { ? translate('Stop session recording') : translate('Start session recording'), onPressed: () => value.toggle(), - icon: SvgPicture.asset( + child: SvgPicture.asset( "assets/rec.svg", color: Colors.white, ), @@ -697,7 +703,7 @@ class _RemoteMenubarState extends State { onPressed: () { clientClose(widget.id, widget.ffi.dialogManager); }, - icon: SvgPicture.asset( + child: SvgPicture.asset( "assets/close.svg", color: Colors.white, ), @@ -767,7 +773,7 @@ class _RemoteMenubarState extends State { return tooltipText == null ? const Offstage() : MenuButton( - icon: _getVoiceCallIcon(), + child: _getVoiceCallIcon(), tooltip: translate(tooltipText), onPressed: () => bind.sessionCloseVoiceCall(id: widget.id), color: _MenubarTheme.redColor, diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 458ca29f4..1afb5b147 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -140,6 +140,8 @@ class FfiModel with ChangeNotifier { handleMsgBox(evt, peerId); } else if (name == 'peer_info') { handlePeerInfo(evt, peerId); + } else if (name == 'sync_peer_info') { + handleSyncPeerInfo(evt, peerId); } else if (name == 'connection_ready') { setConnectionType( peerId, evt['secure'] == 'true', evt['direct'] == 'true'); @@ -415,6 +417,7 @@ class FfiModel with ChangeNotifier { d.cursorEmbedded = d0['cursor_embedded'] == 1; _pi.displays.add(d); } + stateGlobal.displaysCount.value = _pi.displays.length; if (_pi.currentDisplay < _pi.displays.length) { _display = _pi.displays[_pi.currentDisplay]; } @@ -431,6 +434,27 @@ class FfiModel with ChangeNotifier { notifyListeners(); } + /// Handle the peer info synchronization event based on [evt]. + handleSyncPeerInfo(Map evt, String peerId) async { + if (evt['displays'] != null) { + List displays = json.decode(evt['displays']); + List newDisplays = []; + for (int i = 0; i < displays.length; ++i) { + Map d0 = displays[i]; + var d = Display(); + d.x = d0['x'].toDouble(); + d.y = d0['y'].toDouble(); + d.width = d0['width']; + d.height = d0['height']; + d.cursorEmbedded = d0['cursor_embedded'] == 1; + newDisplays.add(d); + } + _pi.displays = newDisplays; + stateGlobal.displaysCount.value = _pi.displays.length; + } + notifyListeners(); + } + updateBlockInputState(Map evt, String peerId) { _inputBlocked = evt['input_state'] == 'on'; notifyListeners(); diff --git a/flutter/lib/models/state_model.dart b/flutter/lib/models/state_model.dart index e4c9fa03f..761c95ded 100644 --- a/flutter/lib/models/state_model.dart +++ b/flutter/lib/models/state_model.dart @@ -14,6 +14,7 @@ class StateGlobal { final RxDouble _resizeEdgeSize = RxDouble(kWindowEdgeSize); final RxDouble _windowBorderWidth = RxDouble(kWindowBorderWidth); final RxBool showRemoteMenuBar = false.obs; + final RxInt displaysCount = 0.obs; int get windowId => _windowId; bool get fullscreen => _fullscreen; diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index 7e3d0b0a4..2a3fd05b4 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -636,5 +636,6 @@ message Message { SwitchSidesResponse switch_sides_response = 22; VoiceCallRequest voice_call_request = 23; VoiceCallResponse voice_call_response = 24; + PeerInfo peer_info = 25; } } diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index c673531ec..b51c481a5 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -1253,6 +1253,14 @@ impl Remote { } } } + Some(message::Union::PeerInfo(pi)) => { + match pi.conn_id { + crate::SYNC_PEER_INFO_DISPLAYS => { + self.handler.set_displays(&pi.displays); + } + _ => {} + } + } _ => {} } } diff --git a/src/common.rs b/src/common.rs index ee44cf4f2..02d367b5e 100644 --- a/src/common.rs +++ b/src/common.rs @@ -37,6 +37,8 @@ pub type NotifyMessageBox = fn(String, String, String, String) -> dyn Future) -> String { + let mut msg_vec = Vec::new(); + for ref d in displays.iter() { + let mut h: HashMap<&str, i32> = Default::default(); + h.insert("x", d.x); + h.insert("y", d.y); + h.insert("width", d.width); + h.insert("height", d.height); + h.insert("cursor_embedded", if d.cursor_embedded { 1 } else { 0 }); + msg_vec.push(h); + } + serde_json::ser::to_string(&msg_vec).unwrap_or("".to_owned()) + } } impl InvokeUiSession for FlutterHandler { @@ -316,17 +330,7 @@ impl InvokeUiSession for FlutterHandler { } fn set_peer_info(&self, pi: &PeerInfo) { - let mut displays = Vec::new(); - for ref d in pi.displays.iter() { - let mut h: HashMap<&str, i32> = Default::default(); - h.insert("x", d.x); - h.insert("y", d.y); - h.insert("width", d.width); - h.insert("height", d.height); - h.insert("cursor_embedded", if d.cursor_embedded { 1 } else { 0 }); - displays.push(h); - } - let displays = serde_json::ser::to_string(&displays).unwrap_or("".to_owned()); + let displays = Self::make_displays_msg(&pi.displays); let mut features: HashMap<&str, i32> = Default::default(); for ref f in pi.features.iter() { features.insert("privacy_mode", if f.privacy_mode { 1 } else { 0 }); @@ -351,6 +355,13 @@ impl InvokeUiSession for FlutterHandler { ); } + fn set_displays(&self, displays: &Vec) { + self.push_event( + "sync_peer_info", + vec![("displays", &Self::make_displays_msg(displays))], + ); + } + fn on_connected(&self, _conn_type: ConnType) {} fn msgbox(&self, msgtype: &str, title: &str, text: &str, link: &str, retry: bool) { diff --git a/src/server/connection.rs b/src/server/connection.rs index 53ccd7008..1a974c51d 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -6,7 +6,10 @@ use crate::common::update_clipboard; #[cfg(windows)] use crate::portable_service::client as portable_client; use crate::{ - client::{start_audio_thread, LatencyController, MediaData, MediaSender, new_voice_call_request, new_voice_call_response}, + client::{ + new_voice_call_request, new_voice_call_response, start_audio_thread, LatencyController, + MediaData, MediaSender, + }, common::{get_default_sound_input, set_sound_input}, video_service, }; @@ -672,15 +675,15 @@ impl Connection { .collect(); if !whitelist.is_empty() && whitelist - .iter() - .filter(|x| x == &"0.0.0.0") - .next() - .is_none() + .iter() + .filter(|x| x == &"0.0.0.0") + .next() + .is_none() && whitelist - .iter() - .filter(|x| IpCidr::from_str(x).map_or(false, |y| y.contains(addr.ip()))) - .next() - .is_none() + .iter() + .filter(|x| IpCidr::from_str(x).map_or(false, |y| y.contains(addr.ip()))) + .next() + .is_none() { self.send_login_error("Your ip is blocked by the peer") .await; @@ -806,7 +809,7 @@ impl Connection { }; self.post_conn_audit(json!({"peer": self.peer_info, "type": conn_type})); #[allow(unused_mut)] - let mut username = crate::platform::get_active_username(); + let mut username = crate::platform::get_active_username(); let mut res = LoginResponse::new(); let mut pi = PeerInfo { username: username.clone(), @@ -833,7 +836,7 @@ impl Connection { h265, ..Default::default() }) - .into(); + .into(); } if self.port_forward_socket.is_some() { @@ -877,7 +880,7 @@ impl Connection { privacy_mode: video_service::is_privacy_mode_supported(), ..Default::default() }) - .into(); + .into(); let mut sub_service = false; if self.file_transfer.is_some() { @@ -893,10 +896,11 @@ impl Connection { res.set_error(format!("{}", err)); } Ok((current, displays)) => { - pi.displays = displays.into(); + pi.displays = displays.clone(); pi.current_display = current as _; res.set_peer_info(pi); sub_service = true; + *super::video_service::LAST_SYNC_DISPLAYS.write().unwrap() = displays; } } } @@ -1160,7 +1164,7 @@ impl Connection { "Failed to access remote {}, please make sure if it is open", addr )) - .await; + .await; return false; } } @@ -1324,12 +1328,12 @@ impl Connection { } } Some(message::Union::Clipboard(cb)) => - { - #[cfg(not(any(target_os = "android", target_os = "ios")))] - if self.clipboard { - update_clipboard(cb, None); - } + { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if self.clipboard { + update_clipboard(cb, None); } + } Some(message::Union::Cliprdr(_clip)) => { if self.file_transfer_enabled() { #[cfg(windows)] @@ -1512,15 +1516,15 @@ impl Connection { } Some(misc::Union::RestartRemoteDevice(_)) => - { - #[cfg(not(any(target_os = "android", target_os = "ios")))] - if self.restart { - match system_shutdown::reboot() { - Ok(_) => log::info!("Restart by the peer"), - Err(e) => log::error!("Failed to restart:{}", e), - } + { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if self.restart { + match system_shutdown::reboot() { + Ok(_) => log::info!("Restart by the peer"), + Err(e) => log::error!("Failed to restart:{}", e), } } + } Some(misc::Union::ElevationRequest(r)) => match r.union { Some(elevation_request::Union::Direct(_)) => { #[cfg(windows)] @@ -1530,8 +1534,8 @@ impl Connection { err = portable_client::start_portable_service( portable_client::StartPara::Direct, ) - .err() - .map_or("".to_string(), |e| e.to_string()); + .err() + .map_or("".to_string(), |e| e.to_string()); } self.portable.elevation_requested = err.is_empty(); let mut misc = Misc::new(); @@ -1549,8 +1553,8 @@ impl Connection { err = portable_client::start_portable_service( portable_client::StartPara::Logon(_r.username, _r.password), ) - .err() - .map_or("".to_string(), |e| e.to_string()); + .err() + .map_or("".to_string(), |e| e.to_string()); } self.portable.elevation_requested = err.is_empty(); let mut misc = Misc::new(); @@ -1571,7 +1575,11 @@ impl Connection { // No video frame will be sent here, so we need to disable latency controller, or audio check may fail. latency_controller.lock().unwrap().set_audio_only(true); self.audio_sender = Some(start_audio_thread(Some(latency_controller))); - allow_err!(self.audio_sender.as_ref().unwrap().send(MediaData::AudioFormat(format))); + allow_err!(self + .audio_sender + .as_ref() + .unwrap() + .send(MediaData::AudioFormat(format))); } } #[cfg(feature = "flutter")] @@ -1583,7 +1591,7 @@ impl Connection { "--switch_uuid", uuid.to_string().as_ref(), ]) - .ok(); + .ok(); self.send_close_reason_no_retry("Closed as expected").await; self.on_close("switch sides", false).await; return false; @@ -1596,7 +1604,9 @@ impl Connection { if let Some(sender) = &self.audio_sender { allow_err!(sender.send(MediaData::AudioFrame(frame))); } else { - log::warn!("Processing audio frame without the voice call audio sender."); + log::warn!( + "Processing audio frame without the voice call audio sender." + ); } } } @@ -1646,7 +1656,9 @@ impl Connection { pub async fn close_voice_call(&mut self) { // Restore to the prior audio device. - if let Some(sound_input) = std::mem::replace(&mut self.audio_input_device_before_voice_call, None) { + if let Some(sound_input) = + std::mem::replace(&mut self.audio_input_device_before_voice_call, None) + { set_sound_input(sound_input); } // Notify the connection manager that the voice call has been closed. @@ -1821,13 +1833,13 @@ impl Connection { lock_screen().await; } #[cfg(not(any(target_os = "android", target_os = "ios")))] - let data = if self.chat_unanswered { + let data = if self.chat_unanswered { ipc::Data::Disconnected } else { ipc::Data::Close }; #[cfg(any(target_os = "android", target_os = "ios"))] - let data = ipc::Data::Close; + let data = ipc::Data::Close; self.tx_to_cm.send(data).ok(); self.port_forward_socket.take(); } diff --git a/src/server/video_service.rs b/src/server/video_service.rs index bc9c5ff6f..52b1717c4 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -65,6 +65,7 @@ lazy_static::lazy_static! { pub static ref VIDEO_QOS: Arc> = Default::default(); pub static ref IS_UAC_RUNNING: Arc> = Default::default(); pub static ref IS_FOREGROUND_WINDOW_ELEVATED: Arc> = Default::default(); + pub static ref LAST_SYNC_DISPLAYS: Arc>> = Default::default(); } fn is_capturer_mag_supported() -> bool { @@ -407,6 +408,43 @@ fn get_capturer(use_yuv: bool, portable_service_running: bool) -> ResultType Option> { + let displays = try_get_displays().ok()?; + let last_sync_displays = &*LAST_SYNC_DISPLAYS.read().unwrap(); + + if displays.len() != last_sync_displays.len() { + Some(displays) + } else { + for i in 0..displays.len() { + if displays[i].height() != (last_sync_displays[i].height as usize) { + return Some(displays); + } + if displays[i].width() != (last_sync_displays[i].width as usize) { + return Some(displays); + } + if displays[i].origin() != (last_sync_displays[i].x, last_sync_displays[i].y) { + return Some(displays); + } + } + None + } +} + +fn check_displays_changed() -> Option { + let displays = check_displays_new()?; + let (current, displays) = get_displays_2(&displays); + let mut pi = PeerInfo { + conn_id: crate::SYNC_PEER_INFO_DISPLAYS, + ..Default::default() + }; + pi.displays = displays.clone(); + pi.current_display = current as _; + let mut msg_out = Message::new(); + msg_out.set_peer_info(pi); + *LAST_SYNC_DISPLAYS.write().unwrap() = displays; + Some(msg_out) +} + fn run(sp: GenericService) -> ResultType<()> { #[cfg(windows)] ensure_close_virtual_device()?; @@ -529,6 +567,11 @@ fn run(sp: GenericService) -> ResultType<()> { let now = time::Instant::now(); if last_check_displays.elapsed().as_millis() > 1000 { last_check_displays = now; + + if let Some(msg_out) = check_displays_changed() { + sp.send(msg_out); + } + if c.ndisplay != get_display_num() { log::info!("Displays changed"); *SWITCH.lock().unwrap() = true; @@ -798,11 +841,7 @@ fn get_display_num() -> usize { } } - if let Ok(d) = try_get_displays() { - d.len() - } else { - 0 - } + LAST_SYNC_DISPLAYS.read().unwrap().len() } pub(super) fn get_displays_2(all: &Vec) -> (usize, Vec) { @@ -861,6 +900,7 @@ pub async fn switch_display(i: i32) { } } +#[inline] pub fn refresh() { #[cfg(target_os = "android")] Display::refresh_size(); @@ -888,10 +928,12 @@ fn get_primary() -> usize { 0 } +#[inline] pub async fn switch_to_primary() { switch_display(get_primary() as _).await; } +#[inline] #[cfg(not(windows))] fn try_get_displays() -> ResultType> { Ok(Display::all()?) diff --git a/src/ui/header.tis b/src/ui/header.tis index 1fb694397..e25c0d544 100644 --- a/src/ui/header.tis +++ b/src/ui/header.tis @@ -480,6 +480,14 @@ handler.updatePi = function(v) { } } +handler.updateDisplays = function(v) { + pi.displays = v; + header.update(); + if (is_port_forward) { + view.windowState = View.WINDOW_MINIMIZED; + } +} + function updatePrivacyMode() { var el = $(li#privacy-mode); if (el) { diff --git a/src/ui/remote.rs b/src/ui/remote.rs index a86f07d0f..4794efb65 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -53,6 +53,20 @@ impl SciterHandler { allow_err!(e.call_method(func, &super::value_crash_workaround(args)[..])); } } + + fn make_displays_array(displays: &Vec) -> Value { + let mut displays_value = Value::array(0); + for d in displays.iter() { + let mut display = Value::map(); + display.set_item("x", d.x); + display.set_item("y", d.y); + display.set_item("width", d.width); + display.set_item("height", d.height); + display.set_item("cursor_embedded", d.cursor_embedded); + displays_value.push(display); + } + displays_value + } } impl InvokeUiSession for SciterHandler { @@ -215,22 +229,18 @@ impl InvokeUiSession for SciterHandler { pi_sciter.set_item("hostname", pi.hostname.clone()); pi_sciter.set_item("platform", pi.platform.clone()); pi_sciter.set_item("sas_enabled", pi.sas_enabled); - - let mut displays = Value::array(0); - for ref d in pi.displays.iter() { - let mut display = Value::map(); - display.set_item("x", d.x); - display.set_item("y", d.y); - display.set_item("width", d.width); - display.set_item("height", d.height); - display.set_item("cursor_embedded", d.cursor_embedded); - displays.push(display); - } - pi_sciter.set_item("displays", displays); + pi_sciter.set_item("displays", Self::make_displays_array(&pi.displays)); pi_sciter.set_item("current_display", pi.current_display); self.call("updatePi", &make_args!(pi_sciter)); } + fn set_displays(&self, displays: &Vec) { + self.call( + "updateDisplays", + &make_args!(Self::make_displays_array(displays)), + ); + } + fn on_connected(&self, conn_type: ConnType) { match conn_type { ConnType::RDP => {} diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index b225151ff..5a83ee572 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -761,6 +761,7 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { fn set_display(&self, x: i32, y: i32, w: i32, h: i32, cursor_embedded: bool); fn switch_display(&self, display: &SwitchDisplay); fn set_peer_info(&self, peer_info: &PeerInfo); // flutter + fn set_displays(&self, displays: &Vec); fn on_connected(&self, conn_type: ConnType); fn update_privacy_mode(&self); fn set_permission(&self, name: &str, value: bool); From d95a03924ee2421205390b45574ab2be7e5a7f08 Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 17 Feb 2023 13:47:09 +0800 Subject: [PATCH 1903/2015] fix build Signed-off-by: fufesou --- flutter/lib/desktop/widgets/remote_menubar.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index d7d944cb2..e82e9d26e 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -415,7 +415,7 @@ class _RemoteMenubarState extends State { if (widget.ffi.ffiModel.isPeerAndroid) { menubarItems.add(MenuButton( tooltip: translate('Mobile Actions'), - icon: SvgPicture.asset( + child: SvgPicture.asset( "assets/actions_mobile.svg", color: Colors.white, ), From 4bff430fdb196d8211d30d8ab9de7b8c923d9b43 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 17 Feb 2023 13:58:16 +0800 Subject: [PATCH 1904/2015] fix svg warning --- flutter/assets/linux.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/assets/linux.svg b/flutter/assets/linux.svg index 5427305ba..1738a02ee 100644 --- a/flutter/assets/linux.svg +++ b/flutter/assets/linux.svg @@ -1,2 +1,2 @@ - \ No newline at end of file + From cdf9867b5c368370c4c9a79c2b5c99bd13a912b6 Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 17 Feb 2023 14:33:01 +0800 Subject: [PATCH 1905/2015] fix update options without auth Signed-off-by: fufesou --- src/server/connection.rs | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/src/server/connection.rs b/src/server/connection.rs index 1a974c51d..2e2bce3e6 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1092,7 +1092,8 @@ impl Connection { async fn handle_login_request_without_validation(&mut self, lr: &LoginRequest) { self.lr = lr.clone(); if let Some(o) = lr.option.as_ref() { - self.update_option(o).await; + // It may not be a good practice to update all options here. + self.update_options(o).await; if let Some(q) = o.video_codec_state.clone().take() { scrap::codec::Encoder::update_video_encoder( self.inner.id(), @@ -1496,7 +1497,7 @@ impl Connection { self.chat_unanswered = true; } Some(misc::Union::Option(o)) => { - self.update_option(&o).await; + self.update_options(&o).await; } Some(misc::Union::RefreshVideo(r)) => { if r { @@ -1665,8 +1666,7 @@ impl Connection { self.send_to_cm(Data::CloseVoiceCall("".to_owned())); } - async fn update_option(&mut self, o: &OptionMessage) { - log::info!("Option update: {:?}", o); + async fn update_options_without_auth(&mut self, o: &OptionMessage) { if let Ok(q) = o.image_quality.enum_value() { let image_quality; if let ImageQuality::NotSet = q { @@ -1691,7 +1691,18 @@ impl Connection { .unwrap() .update_user_fps(o.custom_fps as _); } + if let Some(q) = o.video_codec_state.clone().take() { + scrap::codec::Encoder::update_video_encoder( + self.inner.id(), + scrap::codec::EncoderUpdate::State(q), + ); + } + } + async fn update_options_with_auth(&mut self, o: &OptionMessage) { + if !self.authorized { + return; + } if let Ok(q) = o.lock_after_session_end.enum_value() { if q != BoolOption::NotSet { self.lock_after_session_end = q == BoolOption::Yes; @@ -1818,12 +1829,12 @@ impl Connection { } } } - if let Some(q) = o.video_codec_state.clone().take() { - scrap::codec::Encoder::update_video_encoder( - self.inner.id(), - scrap::codec::EncoderUpdate::State(q), - ); - } + } + + async fn update_options(&mut self, o: &OptionMessage) { + log::info!("Option update: {:?}", o); + self.update_options_without_auth(o); + self.update_options_with_auth(o); } async fn on_close(&mut self, reason: &str, lock: bool) { From 6def4ccdbdf1ea70fae0183a61d52809d17ddb08 Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 17 Feb 2023 14:47:42 +0800 Subject: [PATCH 1906/2015] await Signed-off-by: fufesou --- src/server/connection.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/server/connection.rs b/src/server/connection.rs index 2e2bce3e6..9cdbf974c 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1833,8 +1833,8 @@ impl Connection { async fn update_options(&mut self, o: &OptionMessage) { log::info!("Option update: {:?}", o); - self.update_options_without_auth(o); - self.update_options_with_auth(o); + self.update_options_without_auth(o).await; + self.update_options_with_auth(o).await; } async fn on_close(&mut self, reason: &str, lock: bool) { From 591314617557b283155ddbf124999f2beab9829a Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Fri, 17 Feb 2023 10:44:43 +0100 Subject: [PATCH 1907/2015] Android adaptive icons and monochromatic icons --- .../android/app/src/main/AndroidManifest.xml | 2 ++ .../com/carriez/flutter_hbb/MainService.kt | 2 +- .../res/mipmap-anydpi-v26/ic_launcher.xml | 6 ++++++ .../mipmap-anydpi-v26/ic_launcher_round.xml | 6 ++++++ .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 3114 -> 3990 bytes .../mipmap-hdpi/ic_launcher_foreground.png | Bin 0 -> 7492 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 6161 bytes .../src/main/res/mipmap-hdpi/ic_stat_logo.png | Bin 0 -> 1028 bytes .../src/main/res/mipmap-ldpi/ic_launcher.png | Bin 0 -> 1667 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 1939 -> 2207 bytes .../mipmap-mdpi/ic_launcher_foreground.png | Bin 0 -> 4348 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 3525 bytes .../src/main/res/mipmap-mdpi/ic_stat_logo.png | Bin 0 -> 715 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 4087 -> 4827 bytes .../mipmap-xhdpi/ic_launcher_foreground.png | Bin 0 -> 9515 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 7604 bytes .../main/res/mipmap-xhdpi/ic_stat_logo.png | Bin 0 -> 1524 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 6636 -> 9171 bytes .../mipmap-xxhdpi/ic_launcher_foreground.png | Bin 0 -> 33762 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 13879 bytes .../main/res/mipmap-xxhdpi/ic_stat_logo.png | Bin 0 -> 2091 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 8908 -> 9893 bytes .../mipmap-xxxhdpi/ic_launcher_foreground.png | Bin 0 -> 41583 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 16113 bytes .../main/res/mipmap-xxxhdpi/ic_stat_logo.png | Bin 0 -> 3162 bytes .../res/values/ic_launcher_background.xml | 4 ++++ 26 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 flutter/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 flutter/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png create mode 100644 flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 flutter/android/app/src/main/res/mipmap-hdpi/ic_stat_logo.png create mode 100644 flutter/android/app/src/main/res/mipmap-ldpi/ic_launcher.png create mode 100644 flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png create mode 100644 flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 flutter/android/app/src/main/res/mipmap-mdpi/ic_stat_logo.png create mode 100644 flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png create mode 100644 flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 flutter/android/app/src/main/res/mipmap-xhdpi/ic_stat_logo.png create mode 100644 flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png create mode 100644 flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 flutter/android/app/src/main/res/mipmap-xxhdpi/ic_stat_logo.png create mode 100644 flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png create mode 100644 flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_stat_logo.png create mode 100644 flutter/android/app/src/main/res/values/ic_launcher_background.xml diff --git a/flutter/android/app/src/main/AndroidManifest.xml b/flutter/android/app/src/main/AndroidManifest.xml index 04b2ccc9a..9b25f4973 100644 --- a/flutter/android/app/src/main/AndroidManifest.xml +++ b/flutter/android/app/src/main/AndroidManifest.xml @@ -16,6 +16,8 @@ + + + + + \ No newline at end of file diff --git a/flutter/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/flutter/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 000000000..65291b96e --- /dev/null +++ b/flutter/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png index eac2fe7241381b7d162fb15323837ea7101e4a84..d05404d3af59e68e46ab3009c3684052323da15a 100644 GIT binary patch literal 3990 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 AwEzGB diff --git a/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..3742f241f4dd449bbf2a5a9fa9289d7e1da2ecb3 GIT binary patch literal 7492 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 O0000yxZ-s@cgAfqT86=9BZr&zDg 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 N002ovPDHLkV1k>~Gu8kA literal 0 HcmV?d00001 diff --git a/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png index 8c01e98de01749763dc3f6e0557ac66303ceace4..f16b3d61d9817e58e916df8748a8b833981e6b23 100644 GIT binary patch delta 2196 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 literal 0 HcmV?d00001 diff --git a/flutter/android/app/src/main/res/mipmap-mdpi/ic_stat_logo.png b/flutter/android/app/src/main/res/mipmap-mdpi/ic_stat_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..c179bf053492ab2f17f09a22cbbc47088a36566f GIT binary patch literal 715 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 BRjdF2 literal 4087 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 diff --git a/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..f8ced45f132b86c5080b55773415e84325b8130b GIT binary patch literal 9515 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 a0{;Sf*2F-`L=oly0000 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 literal 6636 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 qM%IAf*eA5}h(mYB5E4T(^hh^D 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 P6AX}(Qj)9@Hwpb8pbK#N literal 0 HcmV?d00001 diff --git a/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_stat_logo.png b/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_stat_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..2cbe6eaf1e94d22a655131ef5ddae7ab5917fc55 GIT binary patch literal 2091 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 SCmh2&11QO<%T`L82mcS4z>B&7 literal 8908 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 ATmS$7 literal 0 HcmV?d00001 diff --git a/flutter/android/app/src/main/res/values/ic_launcher_background.xml b/flutter/android/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 000000000..ab9832824 --- /dev/null +++ b/flutter/android/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #ffffff + \ No newline at end of file From 5dc0c5be5e2dfad0b99cf7c65927e8812242e403 Mon Sep 17 00:00:00 2001 From: grummbeer Date: Mon, 13 Feb 2023 14:58:52 +0100 Subject: [PATCH 1908/2015] invert color of checkmark in darkmode --- flutter/lib/common.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 85aae4c80..e1dd1a1f8 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -217,6 +217,9 @@ class MyTheme { style: ButtonStyle(splashFactory: NoSplash.splashFactory), ) : null, + checkboxTheme: const CheckboxThemeData( + checkColor: MaterialStatePropertyAll(dark) + ), ).copyWith( extensions: >[ ColorThemeExtension.dark, From 7dfe20417ed75264e86c12660a85d23507cd603b Mon Sep 17 00:00:00 2001 From: Mr-Update <37781396+Mr-Update@users.noreply.github.com> Date: Fri, 17 Feb 2023 22:34:46 +0100 Subject: [PATCH 1909/2015] Update de.rs --- src/lang/de.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/lang/de.rs b/src/lang/de.rs index 1672af2b9..38f4fddab 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -209,7 +209,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Von der Gegenstelle manuell geschlossen"), ("Enable remote configuration modification", "Änderung der Konfiguration aus der Ferne zulassen"), ("Run without install", "Ohne Installation ausführen"), - ("Connect via relay", ""), + ("Connect via relay", "Verbindung über Relay-Server"), ("Always connect via relay", "Immer über Relay-Server verbinden"), ("whitelist_tip", "Nur IPs auf der Whitelist können zugreifen."), ("Login", "Anmelden"), @@ -272,21 +272,21 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Total", "Gesamt"), ("items", "Einträge"), ("Selected", "Ausgewählt"), - ("Screen Capture", "Bildschirmzugr."), - ("Input Control", "Eingabezugriff"), - ("Audio Capture", "Audiozugriff"), - ("File Connection", "Dateizugriff"), + ("Screen Capture", "Bildschirmaufnahme"), + ("Input Control", "Eingabesteuerung"), + ("Audio Capture", "Audioaufnahme"), + ("File Connection", "Dateiverbindung"), ("Screen Connection", "Bildschirmanschluss"), ("Do you accept?", "Verbindung zulassen?"), ("Open System Setting", "Systemeinstellung öffnen"), ("How to get Android input permission?", "Wie erhalte ich eine Android-Eingabeberechtigung?"), ("android_input_permission_tip1", "Damit ein entferntes Gerät Ihr Android-Gerät steuern kann, müssen Sie RustDesk erlauben, den Dienst \"Barrierefreiheit\" zu verwenden."), - ("android_input_permission_tip2", "Bitte gehen Sie zur nächsten Systemeinstellungsseite, suchen Sie [Installierte Dienste] und schalten Sie den Dienst [RustDesk Input] ein."), + ("android_input_permission_tip2", "Bitte gehen Sie zur nächsten Systemeinstellungsseite, suchen Sie \"Installierte Dienste\" und schalten Sie den Dienst \"RustDesk Input\" ein."), ("android_new_connection_tip", "möchte ihr Gerät steuern."), ("android_service_will_start_tip", "Durch das Aktivieren der Bildschirmfreigabe wird der Dienst automatisch gestartet, sodass andere Geräte dieses Android-Gerät steuern können."), ("android_stop_service_tip", "Durch das Deaktivieren des Dienstes werden automatisch alle hergestellten Verbindungen getrennt."), ("android_version_audio_tip", "Ihre Android-Version unterstützt keine Audioaufnahme, bitte aktualisieren Sie auf Android 10 oder höher, falls möglich."), - ("android_start_service_tip", "Tippen Sie auf [Dienst aktivieren] oder aktivieren Sie die Berechtigung [Bildschirmzugr.], um den Bildschirmfreigabedienst zu starten."), + ("android_start_service_tip", "Tippen Sie auf \"Dienst aktivieren\" oder aktivieren Sie die Berechtigung \"Bildschirmaufnahme\", um den Bildschirmfreigabedienst zu starten."), ("Account", "Konto"), ("Overwrite", "Überschreiben"), ("This file exists, skip or overwrite this file?", "Diese Datei existiert; überspringen oder überschreiben?"), @@ -386,7 +386,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland erfordert Ubuntu 21.04 oder eine höhere Version."), ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland erfordert eine höhere Version der Linux-Distribution. Bitte versuchen Sie den X11-Desktop oder ändern Sie Ihr Betriebssystem."), ("JumpLink", "View"), - ("Please Select the screen to be shared(Operate on the peer side).", "Bitte wählen Sie den freizugebenden Bildschirm aus (Bedienung auf der Peer-Seite)."), + ("Please Select the screen to be shared(Operate on the peer side).", "Bitte wählen Sie den freizugebenden Bildschirm aus (Bedienung auf der Gegenseite)."), ("Show RustDesk", "RustDesk anzeigen"), ("This PC", "Dieser PC"), ("or", "oder"), @@ -449,7 +449,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", "Sprachanruf"), ("Text chat", "Text-Chat"), ("Stop voice call", "Sprachanruf beenden"), - ("relay_hint_tip", ""), - ("Reconnect", ""), + ("relay_hint_tip", "Wenn eine direkte Verbindung nicht möglich ist, können Sie versuchen, eine Verbindung über einen Relay-Server herzustellen. \nWenn Sie eine Relay-Verbindung beim ersten Versuch herstellen möchten, können Sie das Suffix \"/r\" an die ID anhängen oder die Option \"Immer über Relay-Server verbinden\" auf der Gegenstelle auswählen."), + ("Reconnect", "Erneut verbinden"), ].iter().cloned().collect(); } From 116649eaf2dede3874a50ba8a27568249da94e3a Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Sat, 18 Feb 2023 09:42:40 +0800 Subject: [PATCH 1910/2015] Update bug_report.yaml --- .github/ISSUE_TEMPLATE/bug_report.yaml | 9 --------- 1 file changed, 9 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index c2d92097c..a955c2a2e 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -1,14 +1,5 @@ name: 🐞 Bug report description: Thanks for taking the time to fill out this bug report! Please fill the form in **English** -title: "[Bug] " -body: - - type: checkboxes - attributes: - label: Is there an existing issue for this? - description: Please search to see if an issue related to this already exists. - options: - - label: I have searched the existing issues - required: true - type: textarea id: desc attributes: From 38cb44a89c17f605d714c3b5f70e708f64812546 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sat, 18 Feb 2023 09:44:45 +0800 Subject: [PATCH 1911/2015] remove title and checkbox in issue template because title cause guy empty title and no body care about the checkbox`` --- .github/ISSUE_TEMPLATE/bug_report.yaml | 1 + .github/ISSUE_TEMPLATE/feature_request.yaml | 9 --------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index a955c2a2e..ec23aa7a9 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -1,5 +1,6 @@ name: 🐞 Bug report description: Thanks for taking the time to fill out this bug report! Please fill the form in **English** +body: - type: textarea id: desc attributes: diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml index 50cd6d0cf..29b0d0e0f 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yaml +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -1,15 +1,6 @@ name: 🛠️ Feature request description: Suggest an idea for RustDesk -title: "[FR] " body: - - type: checkboxes - attributes: - label: Is there an existing issue for this? - description: Please search to see if an issue related to this already exists. - options: - - label: I have searched the existing issues - required: true - - type: textarea id: desc attributes: From 3fca9c166187abcfa89ad68d96fb77880ce467e1 Mon Sep 17 00:00:00 2001 From: enforcer007 Date: Sat, 18 Feb 2023 19:43:51 +0530 Subject: [PATCH 1912/2015] docker file --- .devcontainer/Dockerfile | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 0381ff966..a96c782d7 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,5 +1,4 @@ FROM debian - WORKDIR / RUN apt update -y && 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 unzip zip sudo libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev @@ -15,5 +14,10 @@ RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > rustup.sh RUN chmod +x rustup.sh RUN ./rustup.sh -y +# Install Flutter +RUN wget https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.7.3-stable.tar.xz +RUN tar xf flutter_linux_3.7.3-stable.tar.xz +RUN export PATH="$PATH:/home/user/flutter/bin" + USER root ENV HOME=/home/user From df8c7b1c3096eff65da615cdad08d69d00df11a5 Mon Sep 17 00:00:00 2001 From: grummbeer Date: Sun, 12 Feb 2023 12:59:51 +0100 Subject: [PATCH 1913/2015] remove boxed layout of nested option --- .../desktop/pages/desktop_setting_page.dart | 94 +++++++------------ 1 file changed, 36 insertions(+), 58 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 25c485a2a..34398dd0d 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -650,7 +650,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { context, onChanged != null)), ), ], - ).paddingSymmetric(horizontal: 10), + ).paddingOnly(right: 10), onTap: () => onChanged?.call(value), )) .toList(); @@ -675,6 +675,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { if (usePassword) radios[0], if (usePassword) _SubLabeledWidget( + context, 'One-time password length', Row( children: [ @@ -756,9 +757,10 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { controller.text = data['port'].toString(); return Offstage( offstage: !enabled, - child: Row(children: [ - _SubLabeledWidget( - 'Port', + child: _SubLabeledWidget( + context, + 'Port', + Row(children: [ SizedBox( width: 80, child: TextField( @@ -772,28 +774,29 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { textAlign: TextAlign.end, decoration: const InputDecoration( hintText: '21118', - border: InputBorder.none, - contentPadding: EdgeInsets.only(right: 5), + border: OutlineInputBorder(), + contentPadding: + EdgeInsets.only(bottom: 10, top: 10, right: 10), isCollapsed: true, ), - ), + ).marginOnly(right: 15), ), - enabled: enabled && !locked, - ).marginOnly(left: 5), - Obx(() => ElevatedButton( - onPressed: applyEnabled.value && enabled && !locked - ? () async { - applyEnabled.value = false; - await bind.mainSetOption( - key: 'direct-access-port', - value: controller.text); - } - : null, - child: Text( - translate('Apply'), - ), - ).marginOnly(left: 20)) - ]), + Obx(() => ElevatedButton( + onPressed: applyEnabled.value && enabled && !locked + ? () async { + applyEnabled.value = false; + await bind.mainSetOption( + key: 'direct-access-port', + value: controller.text); + } + : null, + child: Text( + translate('Apply'), + ), + )) + ]), + enabled: enabled && !locked, + ), ); }, ), @@ -1614,43 +1617,18 @@ Widget _SubButton(String label, Function() onPressed, [bool enabled = true]) { } // ignore: non_constant_identifier_names -Widget _SubLabeledWidget(String label, Widget child, {bool enabled = true}) { - RxBool hover = false.obs; +Widget _SubLabeledWidget(BuildContext context, String label, Widget child, + {bool enabled = true}) { return Row( children: [ - MouseRegion( - onEnter: (_) => hover.value = true, - onExit: (_) => hover.value = false, - child: Obx( - () { - return Container( - height: 32, - decoration: BoxDecoration( - border: Border.all( - color: hover.value && enabled - ? const Color(0xFFD7D7D7) - : const Color(0xFFCBCBCB), - width: hover.value && enabled ? 2 : 1)), - child: Row( - children: [ - Container( - height: 28, - color: (hover.value && enabled) - ? const Color(0xFFD7D7D7) - : const Color(0xFFCBCBCB), - alignment: Alignment.center, - padding: const EdgeInsets.symmetric( - horizontal: 5, vertical: 2), - child: Text( - '${translate(label)}: ', - style: const TextStyle(fontWeight: FontWeight.w300), - ), - ).paddingAll(2), - child, - ], - )); - }, - )), + Text( + '${translate(label)}: ', + style: TextStyle(color: _disabledTextColor(context, enabled)), + ), + SizedBox( + width: 10, + ), + child, ], ).marginOnly(left: _kContentHSubMargin); } From 6ba2515b560a3c84384fb3478bcbf1b2a0d1250d Mon Sep 17 00:00:00 2001 From: enforcer007 Date: Sat, 18 Feb 2023 20:47:11 +0530 Subject: [PATCH 1914/2015] updated --- .devcontainer/Dockerfile | 2 +- .devcontainer/devcontainer.json | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index a96c782d7..86c11ccf6 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,4 +1,4 @@ -FROM debian +FROM mcr.microsoft.com/devcontainers/base:ubuntu WORKDIR / RUN apt update -y && 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 unzip zip sudo libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index cc348f38f..426127fd9 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,7 +1,8 @@ { "name": "rustdesk", "build": { - "dockerfile": "Dockerfile" + "dockerfile": "./Dockerfile", + "context": "." }, "workspaceMount": "source=${localWorkspaceFolder},target=/home/user/rustdesk,type=bind,consistency=cache", "workspaceFolder": "/home/user/rustdesk", From 7dc0cefeee2eb5e6ee3899b0ed63c200dc82ba85 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sat, 18 Feb 2023 23:34:28 +0800 Subject: [PATCH 1915/2015] fix #3257 and opt svg --- flutter/assets/GitHub.svg | 2 +- flutter/assets/Google.svg | 2 +- flutter/assets/Okta.svg | 31 +-------------------------- flutter/assets/actions.svg | 3 +-- flutter/assets/actions_mobile.svg | 3 +-- flutter/assets/android.svg | 2 +- flutter/assets/call_end.svg | 3 +-- flutter/assets/call_wait.svg | 3 +-- flutter/assets/chat.svg | 3 +-- flutter/assets/close.svg | 3 +-- flutter/assets/display.svg | 3 +-- flutter/assets/fullscreen.svg | 3 +-- flutter/assets/fullscreen_exit.svg | 3 +-- flutter/assets/insecure.svg | 2 +- flutter/assets/insecure_relay.svg | 2 +- flutter/assets/kb_layout_iso.svg | 2 +- flutter/assets/kb_layout_not_iso.svg | 2 +- flutter/assets/keyboard.svg | 3 +-- flutter/assets/linux.svg | 3 +-- flutter/assets/logo.svg | 2 +- flutter/assets/mac.svg | 2 +- flutter/assets/pinned.svg | 3 +-- flutter/assets/rec.svg | 3 +-- flutter/assets/record_screen.svg | 25 +-------------------- flutter/assets/secure.svg | 4 +--- flutter/assets/secure_relay.svg | 2 +- flutter/assets/unpinned.svg | 3 +-- flutter/assets/voice_call.svg | 2 +- flutter/assets/voice_call_waiting.svg | 2 +- flutter/assets/win.svg | 2 +- 30 files changed, 30 insertions(+), 98 deletions(-) diff --git a/flutter/assets/GitHub.svg b/flutter/assets/GitHub.svg index a5bd1de81..ef0bb12a7 100644 --- a/flutter/assets/GitHub.svg +++ b/flutter/assets/GitHub.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/Google.svg b/flutter/assets/Google.svg index b7bb2f42f..df394a84f 100644 --- a/flutter/assets/Google.svg +++ b/flutter/assets/Google.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/Okta.svg b/flutter/assets/Okta.svg index 0fa45b93d..931e72844 100644 --- a/flutter/assets/Okta.svg +++ b/flutter/assets/Okta.svg @@ -1,30 +1 @@ - - - - - - - - - - - + \ No newline at end of file diff --git a/flutter/assets/actions.svg b/flutter/assets/actions.svg index 5403853db..3049f3b89 100644 --- a/flutter/assets/actions.svg +++ b/flutter/assets/actions.svg @@ -1,2 +1 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/actions_mobile.svg b/flutter/assets/actions_mobile.svg index 6aed6053e..4185945e1 100644 --- a/flutter/assets/actions_mobile.svg +++ b/flutter/assets/actions_mobile.svg @@ -1,2 +1 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/android.svg b/flutter/assets/android.svg index e46dab11e..6fd89c9ab 100644 --- a/flutter/assets/android.svg +++ b/flutter/assets/android.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/flutter/assets/call_end.svg b/flutter/assets/call_end.svg index 39367c3c5..7c07ee25d 100644 --- a/flutter/assets/call_end.svg +++ b/flutter/assets/call_end.svg @@ -1,2 +1 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/call_wait.svg b/flutter/assets/call_wait.svg index 42a11fe56..530f12a97 100644 --- a/flutter/assets/call_wait.svg +++ b/flutter/assets/call_wait.svg @@ -1,2 +1 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/chat.svg b/flutter/assets/chat.svg index 7088107b0..c4ab3c92d 100644 --- a/flutter/assets/chat.svg +++ b/flutter/assets/chat.svg @@ -1,2 +1 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/close.svg b/flutter/assets/close.svg index 7488acc9f..fb18eabd2 100644 --- a/flutter/assets/close.svg +++ b/flutter/assets/close.svg @@ -1,2 +1 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/display.svg b/flutter/assets/display.svg index b5a88106e..9d107d699 100644 --- a/flutter/assets/display.svg +++ b/flutter/assets/display.svg @@ -1,2 +1 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/fullscreen.svg b/flutter/assets/fullscreen.svg index cd01f93f9..93f27bf7b 100644 --- a/flutter/assets/fullscreen.svg +++ b/flutter/assets/fullscreen.svg @@ -1,2 +1 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/fullscreen_exit.svg b/flutter/assets/fullscreen_exit.svg index 8d4414897..f244631fe 100644 --- a/flutter/assets/fullscreen_exit.svg +++ b/flutter/assets/fullscreen_exit.svg @@ -1,2 +1 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/insecure.svg b/flutter/assets/insecure.svg index 37bb196e3..5a344dd04 100644 --- a/flutter/assets/insecure.svg +++ b/flutter/assets/insecure.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/flutter/assets/insecure_relay.svg b/flutter/assets/insecure_relay.svg index f08bee6a6..17b474e6e 100644 --- a/flutter/assets/insecure_relay.svg +++ b/flutter/assets/insecure_relay.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/flutter/assets/kb_layout_iso.svg b/flutter/assets/kb_layout_iso.svg index 69f0c96cb..163e045e1 100644 --- a/flutter/assets/kb_layout_iso.svg +++ b/flutter/assets/kb_layout_iso.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/kb_layout_not_iso.svg b/flutter/assets/kb_layout_not_iso.svg index 09a055be3..cfbb046ca 100644 --- a/flutter/assets/kb_layout_not_iso.svg +++ b/flutter/assets/kb_layout_not_iso.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/keyboard.svg b/flutter/assets/keyboard.svg index d5481d7a1..d72033f6d 100644 --- a/flutter/assets/keyboard.svg +++ b/flutter/assets/keyboard.svg @@ -1,2 +1 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/linux.svg b/flutter/assets/linux.svg index 1738a02ee..2c3697be9 100644 --- a/flutter/assets/linux.svg +++ b/flutter/assets/linux.svg @@ -1,2 +1 @@ - - + \ No newline at end of file diff --git a/flutter/assets/logo.svg b/flutter/assets/logo.svg index 13eb73f22..4d43f8bcd 100644 --- a/flutter/assets/logo.svg +++ b/flutter/assets/logo.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/flutter/assets/mac.svg b/flutter/assets/mac.svg index 8092b3af3..ccf9c7aab 100644 --- a/flutter/assets/mac.svg +++ b/flutter/assets/mac.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/flutter/assets/pinned.svg b/flutter/assets/pinned.svg index dd718b96a..a8715011b 100644 --- a/flutter/assets/pinned.svg +++ b/flutter/assets/pinned.svg @@ -1,2 +1 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/rec.svg b/flutter/assets/rec.svg index 33a57e9d0..09aa55e2a 100644 --- a/flutter/assets/rec.svg +++ b/flutter/assets/rec.svg @@ -1,2 +1 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/record_screen.svg b/flutter/assets/record_screen.svg index e1b962124..bbd948c73 100644 --- a/flutter/assets/record_screen.svg +++ b/flutter/assets/record_screen.svg @@ -1,24 +1 @@ - - - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/secure.svg b/flutter/assets/secure.svg index 29e1d3c4f..fcd99f2f5 100644 --- a/flutter/assets/secure.svg +++ b/flutter/assets/secure.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/flutter/assets/secure_relay.svg b/flutter/assets/secure_relay.svg index 8ecbdb47b..af54808a8 100644 --- a/flutter/assets/secure_relay.svg +++ b/flutter/assets/secure_relay.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/flutter/assets/unpinned.svg b/flutter/assets/unpinned.svg index 9e9e3de8b..7e93a7a35 100644 --- a/flutter/assets/unpinned.svg +++ b/flutter/assets/unpinned.svg @@ -1,2 +1 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/voice_call.svg b/flutter/assets/voice_call.svg index 5654befc7..bf90ec958 100644 --- a/flutter/assets/voice_call.svg +++ b/flutter/assets/voice_call.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/voice_call_waiting.svg b/flutter/assets/voice_call_waiting.svg index fd8334f92..f1771c3fd 100644 --- a/flutter/assets/voice_call_waiting.svg +++ b/flutter/assets/voice_call_waiting.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/win.svg b/flutter/assets/win.svg index 326f7829d..a0f7e3def 100644 --- a/flutter/assets/win.svg +++ b/flutter/assets/win.svg @@ -1 +1 @@ - + \ No newline at end of file From 11d5cdb4f119f0fc523cce61bf6ce67cd2013777 Mon Sep 17 00:00:00 2001 From: Mr-Update <37781396+Mr-Update@users.noreply.github.com> Date: Sat, 18 Feb 2023 23:24:29 +0100 Subject: [PATCH 1916/2015] Update README-DE.md - Translation improved - Missing parts from the english readme added --- docs/README-DE.md | 115 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 81 insertions(+), 34 deletions(-) diff --git a/docs/README-DE.md b/docs/README-DE.md index e537d41f3..8ee4a51fa 100644 --- a/docs/README-DE.md +++ b/docs/README-DE.md @@ -1,63 +1,84 @@

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

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

    - RustDesk - Dit fjernskrivebord
    + RustDesk - Your remote desktop
    ServersBuildDocker • From d08fa1fb11bbe3e180ceb1a15e38ee254d72a201 Mon Sep 17 00:00:00 2001 From: enforcer007 Date: Mon, 20 Feb 2023 15:30:36 +0000 Subject: [PATCH 1939/2015] setup --- .devcontainer/Dockerfile | 2 +- .devcontainer/build.sh | 73 +++++++++++++++++++++++++++++++++ .devcontainer/devcontainer.json | 6 ++- .devcontainer/setup.sh | 19 +++++++++ flutter/build_android.sh | 8 ++-- 5 files changed, 102 insertions(+), 6 deletions(-) create mode 100755 .devcontainer/build.sh create mode 100644 .devcontainer/setup.sh diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 6d00302f7..32a440b28 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -28,7 +28,7 @@ RUN $HOME/.cargo/bin/cargo install cargo-ndk # Install Flutter RUN wget https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.7.3-stable.tar.xz -RUN tar xf flutter_linux_3.7.3-stable.tar.xz +RUN tar xf flutter_linux_3.7.3-stable.tar.xz && rm flutter_linux_3.7.3-stable.tar.xz ENV PATH="$PATH:$HOME/flutter/bin" RUN dart pub global activate ffigen 5.0.1 diff --git a/.devcontainer/build.sh b/.devcontainer/build.sh new file mode 100755 index 000000000..a41d4dc38 --- /dev/null +++ b/.devcontainer/build.sh @@ -0,0 +1,73 @@ +#!/bin/bash + +set -e + +MODE=${1:---debug} +TYPE=${2:-linux} +MODE=${MODE/*-/} + + +build(){ + pwd + $WORKDIR/entrypoint $1 +} + +build_arm64(){ + CWD=$(pwd) + cd $WORKDIR + $WORKDIR/flutter/ndk_arm64.sh + cp $WORKDIR/target/aarch64-linux-android/release/liblibrustdesk.so $WORKDIR/flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so + cd $CWD +} + +build_apk(){ + cd $WORKDIR/flutter + MODE=$1 $WORKDIR/flutter/build_android.sh + cd $WORKDIR +} + +key_gen(){ + if [ ! -f $WORKDIR/flutter/android/key.properties ] + then + if [ ! -f $HOME/upload-keystore.jks ] + then + echo "Remember the password you enter in keytool!" + keytool -genkey -v -keystore $HOME/upload-keystore.jks -keyalg RSA -keysize 2048 -validity 10000 -alias upload + else + read -r -p "enter the password used to generate $HOME/upload-keystore.jks" password + echo -e "storePassword=${password}\nkeyPassword=${password}\nkeyAlias=upload\nstoreFile=$HOME/upload-keystore.jks" > $WORKDIR/flutter/android/key.properties + fi + fi +} + +android_build(){ + if [ ! -d $WORKDIR/flutter/android/app/src/main/jniLibs/arm64-v8a ] + then + $WORKDIR/.devcontainer/setup.sh android + fi + build_arm64 + case $1 in + debug) + build_apk debug + ;; + release) + key_gen + build_apk release + ;; + esac +} + +case "$MODE:$TYPE" in + "debug:linux") + build + ;; + "release:linux") + build --release + ;; + "debug:android") + android_build debug + ;; + "release:android") + android_build release + ;; +esac diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 432d05136..a5c5c8c19 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -6,7 +6,7 @@ }, "workspaceMount": "source=${localWorkspaceFolder},target=/home/vscode/rustdesk,type=bind,consistency=cache", "workspaceFolder": "/home/vscode/rustdesk", - "postStartCommand": "./entrypoint", + "postStartCommand": ".devcontainer/build", "features": { "ghcr.io/devcontainers/features/java:1": {}, "ghcr.io/akhildevelops/devcontainer-features/android-cli:latest": { @@ -20,7 +20,9 @@ "mutantdino.resourcemonitor", "rust-lang.rust-analyzer", "tamasfe.even-better-toml", - "serayuzgur.crates" + "serayuzgur.crates", + "mhutchie.git-graph", + "eamodio.gitlens" ], "settings": { "files.watcherExclude": { diff --git a/.devcontainer/setup.sh b/.devcontainer/setup.sh new file mode 100644 index 000000000..a206c3607 --- /dev/null +++ b/.devcontainer/setup.sh @@ -0,0 +1,19 @@ +#!/bin/bash +set -e +case $1 in + android) + # install deps + cd $WORKDIR/flutter + flutter pub get + wget https://github.com/rustdesk/doc.rustdesk.com/releases/download/console/so.tar.gz + tar xzf so.tar.gz + rm so.tar.gz + sudo chown -R $(whoami) $ANDROID_HOME + echo "Setup is Done." + ;; + linux) + echo "Linux Setup" + ;; +esac + + \ No newline at end of file diff --git a/flutter/build_android.sh b/flutter/build_android.sh index 0a2854299..b7a475d63 100755 --- a/flutter/build_android.sh +++ b/flutter/build_android.sh @@ -1,8 +1,10 @@ #!/usr/bin/env bash + +MODE=${MODE:=debug} $ANDROID_NDK_HOME/toolchains/aarch64-linux-android-4.9/prebuilt/linux-x86_64/bin/aarch64-linux-android-strip android/app/src/main/jniLibs/arm64-v8a/* -#flutter build apk --target-platform android-arm64,android-arm --release --obfuscate --split-debug-info ./split-debug-info -flutter build apk --split-per-abi --target-platform android-arm64,android-arm --release --obfuscate --split-debug-info ./split-debug-info -#flutter build appbundle --target-platform android-arm64,android-arm --release --obfuscate --split-debug-info ./split-debug-info +flutter build apk --target-platform android-arm64,android-arm --${MODE} --obfuscate --split-debug-info ./split-debug-info +flutter build apk --split-per-abi --target-platform android-arm64,android-arm --${MODE} --obfuscate --split-debug-info ./split-debug-info +flutter build appbundle --target-platform android-arm64,android-arm --${MODE} --obfuscate --split-debug-info ./split-debug-info # build in linux # $ANDROID_NDK/toolchains/aarch64-linux-android-4.9/prebuilt/linux-x86_64/bin/aarch64-linux-android-strip android/app/src/main/jniLibs/arm64-v8a/* From 4d554044e889f27924d056a7fbadfea683d0db88 Mon Sep 17 00:00:00 2001 From: enforcer007 Date: Mon, 20 Feb 2023 15:39:46 +0000 Subject: [PATCH 1940/2015] fix key gen --- .devcontainer/build.sh | 9 +++++---- .devcontainer/setup.sh | 0 2 files changed, 5 insertions(+), 4 deletions(-) mode change 100644 => 100755 .devcontainer/setup.sh diff --git a/.devcontainer/build.sh b/.devcontainer/build.sh index a41d4dc38..7a85b6da6 100755 --- a/.devcontainer/build.sh +++ b/.devcontainer/build.sh @@ -31,12 +31,13 @@ key_gen(){ then if [ ! -f $HOME/upload-keystore.jks ] then - echo "Remember the password you enter in keytool!" + echo -e "\n$HOME/upload-keystore.jks is not created.\nLet's create it.\nRemember the password you enter in keytool!" keytool -genkey -v -keystore $HOME/upload-keystore.jks -keyalg RSA -keysize 2048 -validity 10000 -alias upload - else - read -r -p "enter the password used to generate $HOME/upload-keystore.jks" password - echo -e "storePassword=${password}\nkeyPassword=${password}\nkeyAlias=upload\nstoreFile=$HOME/upload-keystore.jks" > $WORKDIR/flutter/android/key.properties fi + read -r -p "enter the password used to generate $HOME/upload-keystore.jks\n" password + echo -e "storePassword=${password}\nkeyPassword=${password}\nkeyAlias=upload\nstoreFile=$HOME/upload-keystore.jks" > $WORKDIR/flutter/android/key.properties + else + echo "Believing storeFile is created in $WORKDIR/flutter/android/key.properties" fi } diff --git a/.devcontainer/setup.sh b/.devcontainer/setup.sh old mode 100644 new mode 100755 From ededf09a67903da1cab746c384bb76d8e9a9c1d9 Mon Sep 17 00:00:00 2001 From: enforcer007 Date: Mon, 20 Feb 2023 16:31:27 +0000 Subject: [PATCH 1941/2015] build sh --- .devcontainer/devcontainer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index a5c5c8c19..cd82c75e3 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -6,7 +6,7 @@ }, "workspaceMount": "source=${localWorkspaceFolder},target=/home/vscode/rustdesk,type=bind,consistency=cache", "workspaceFolder": "/home/vscode/rustdesk", - "postStartCommand": ".devcontainer/build", + "postStartCommand": ".devcontainer/build.sh", "features": { "ghcr.io/devcontainers/features/java:1": {}, "ghcr.io/akhildevelops/devcontainer-features/android-cli:latest": { From 8f35f5c65b80b796d8878784e815c208e1fc7efd Mon Sep 17 00:00:00 2001 From: enforcer007 Date: Mon, 20 Feb 2023 18:05:16 +0000 Subject: [PATCH 1942/2015] setup key --- .devcontainer/build.sh | 7 ++++--- .devcontainer/setup.sh | 4 ++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.devcontainer/build.sh b/.devcontainer/build.sh index 7a85b6da6..df87aace7 100755 --- a/.devcontainer/build.sh +++ b/.devcontainer/build.sh @@ -14,6 +14,8 @@ build(){ build_arm64(){ CWD=$(pwd) + cd $WORKDIR/flutter + flutter pub get cd $WORKDIR $WORKDIR/flutter/ndk_arm64.sh cp $WORKDIR/target/aarch64-linux-android/release/liblibrustdesk.so $WORKDIR/flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so @@ -31,13 +33,12 @@ key_gen(){ then if [ ! -f $HOME/upload-keystore.jks ] then - echo -e "\n$HOME/upload-keystore.jks is not created.\nLet's create it.\nRemember the password you enter in keytool!" - keytool -genkey -v -keystore $HOME/upload-keystore.jks -keyalg RSA -keysize 2048 -validity 10000 -alias upload + $WORKDIR/.devcontainer/setup.sh key fi read -r -p "enter the password used to generate $HOME/upload-keystore.jks\n" password echo -e "storePassword=${password}\nkeyPassword=${password}\nkeyAlias=upload\nstoreFile=$HOME/upload-keystore.jks" > $WORKDIR/flutter/android/key.properties else - echo "Believing storeFile is created in $WORKDIR/flutter/android/key.properties" + echo "Believing storeFile is created ref: $WORKDIR/flutter/android/key.properties" fi } diff --git a/.devcontainer/setup.sh b/.devcontainer/setup.sh index a206c3607..c972f47b2 100755 --- a/.devcontainer/setup.sh +++ b/.devcontainer/setup.sh @@ -14,6 +14,10 @@ case $1 in linux) echo "Linux Setup" ;; + key) + echo -e "\n$HOME/upload-keystore.jks is not created.\nLet's create it.\nRemember the password you enter in keytool!" + keytool -genkey -v -keystore $HOME/upload-keystore.jks -keyalg RSA -keysize 2048 -validity 10000 -alias upload + ;; esac \ No newline at end of file From cb744463d490d57e26c365dea01b218de43e0dc2 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 21 Feb 2023 10:42:03 +0800 Subject: [PATCH 1943/2015] screenshot required --- .github/ISSUE_TEMPLATE/bug_report.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index ec23aa7a9..fea1a3672 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -44,7 +44,9 @@ body: id: screenshots attributes: label: Screenshots - description: If applicable, please add screenshots to help explain your problem + description: Please add screenshots to help explain your problem, if applicable, please upload video. + validations: + required: true - type: textarea id: context attributes: From 95a0d90891944ca209692cd34259729843117c3e Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 21 Feb 2023 11:40:21 +0800 Subject: [PATCH 1944/2015] add FAQ --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index df0ca8328..5e4c5e70d 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Yet another remote desktop software, written in Rust. Works out of the box, no c RustDesk welcomes contribution from everyone. See [`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md) for help getting started. -[**How does RustDesk work?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F) +[**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ) [**BINARY DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases) From 2bef19c1a46c99bc6497b4c9d3bc06f8312dc2c1 Mon Sep 17 00:00:00 2001 From: Seff <46768740+seffs@users.noreply.github.com> Date: Tue, 21 Feb 2023 07:35:09 +0100 Subject: [PATCH 1945/2015] fix desktop entry key/values Similar to #1255 and related to #1299, running `desktop-file-validate /usr/share/applications/rustdesk.desktop` on Ubuntu 22.04 returns the following: ``` /usr/share/applications/rustdesk.desktop: error: value "1.2.0" for key "Version" in group "Desktop Entry" is not a known version /usr/share/applications/rustdesk.desktop: error: required key "Exec" in group "Desktop Action new-window" is not present ``` * "Version" refers to the Freedesktop Specification[1], not the program's one. Given that this was correctly defined in rustdesk-link.desktop, the same value should be used here too. * The new-window section is missing the `Exec` key. Ubuntu 22.04 refuses to launch from the Activities overview (apps menu) without this key. [1] https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html --- res/rustdesk.desktop | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/res/rustdesk.desktop b/res/rustdesk.desktop index c9cf1f254..ca1c9a9f7 100644 --- a/res/rustdesk.desktop +++ b/res/rustdesk.desktop @@ -1,5 +1,5 @@ [Desktop Entry] -Version=1.2.0 +Version=1.5.0 Name=RustDesk GenericName=Remote Desktop Comment=Remote Desktop @@ -16,4 +16,4 @@ X-Desktop-File-Install-Version=0.23 [Desktop Action new-window] Name=Open a New Window - +Exec=rustdesk %u From acf2dfd779749e92d3e0687fe40c8b8723dfd8a6 Mon Sep 17 00:00:00 2001 From: Seff <46768740+seffs@users.noreply.github.com> Date: Tue, 21 Feb 2023 07:40:54 +0100 Subject: [PATCH 1946/2015] fix: versioning --- res/rustdesk.desktop | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/rustdesk.desktop b/res/rustdesk.desktop index ca1c9a9f7..f31a16dec 100644 --- a/res/rustdesk.desktop +++ b/res/rustdesk.desktop @@ -1,5 +1,5 @@ [Desktop Entry] -Version=1.5.0 +Version=1.5 Name=RustDesk GenericName=Remote Desktop Comment=Remote Desktop From 1e1a544c9ec7ef93b2cd4a2041fcd245819b1357 Mon Sep 17 00:00:00 2001 From: enforcer007 Date: Tue, 21 Feb 2023 07:00:59 +0000 Subject: [PATCH 1947/2015] defaults to release --- flutter/build_android.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/build_android.sh b/flutter/build_android.sh index b7a475d63..c6b639f87 100755 --- a/flutter/build_android.sh +++ b/flutter/build_android.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -MODE=${MODE:=debug} +MODE=${MODE:=release} $ANDROID_NDK_HOME/toolchains/aarch64-linux-android-4.9/prebuilt/linux-x86_64/bin/aarch64-linux-android-strip android/app/src/main/jniLibs/arm64-v8a/* flutter build apk --target-platform android-arm64,android-arm --${MODE} --obfuscate --split-debug-info ./split-debug-info flutter build apk --split-per-abi --target-platform android-arm64,android-arm --${MODE} --obfuscate --split-debug-info ./split-debug-info From 4beacf93d71305577db319b0b0e716d80848dd0a Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 21 Feb 2023 15:07:44 +0800 Subject: [PATCH 1948/2015] kill check-hwcodec-config process Signed-off-by: 21pages --- Cargo.lock | 2 +- Cargo.toml | 1 - libs/hbb_common/Cargo.toml | 1 + libs/hbb_common/src/lib.rs | 1 + libs/scrap/src/common/hwcodec.rs | 38 ++++++++++++++++++++++---------- src/ipc.rs | 2 +- src/platform/macos.rs | 2 +- 7 files changed, 31 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 48981e169..115845b50 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2623,6 +2623,7 @@ dependencies = [ "serde_json 1.0.89", "socket2 0.3.19", "sodiumoxide", + "sysinfo", "tokio", "tokio-socks", "tokio-util", @@ -4887,7 +4888,6 @@ dependencies = [ "shutdown_hooks", "simple_rc", "sys-locale", - "sysinfo", "system_shutdown", "tao", "tray-icon", diff --git a/Cargo.toml b/Cargo.toml index 0ebe49fdf..f685e3f2e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,7 +55,6 @@ uuid = { version = "1.0", features = ["v4"] } clap = "3.0" rpassword = "7.0" base64 = "0.13" -sysinfo = "0.24" num_cpus = "1.13" bytes = { version = "1.2", features = ["serde"] } default-net = "0.12.0" diff --git a/libs/hbb_common/Cargo.toml b/libs/hbb_common/Cargo.toml index 0457bb19a..a125078d2 100644 --- a/libs/hbb_common/Cargo.toml +++ b/libs/hbb_common/Cargo.toml @@ -33,6 +33,7 @@ tokio-socks = { git = "https://github.com/open-trade/tokio-socks" } chrono = "0.4" backtrace = "0.3" libc = "0.2" +sysinfo = "0.24" [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 99cb6f408..bfb773908 100644 --- a/libs/hbb_common/src/lib.rs +++ b/libs/hbb_common/src/lib.rs @@ -42,6 +42,7 @@ pub use chrono; pub use libc; pub use directories_next; pub mod keyboard; +pub use sysinfo; #[cfg(feature = "quic")] pub type Stream = quic::Connection; diff --git a/libs/scrap/src/common/hwcodec.rs b/libs/scrap/src/common/hwcodec.rs index 9cd6077a6..27b157b79 100644 --- a/libs/scrap/src/common/hwcodec.rs +++ b/libs/scrap/src/common/hwcodec.rs @@ -317,16 +317,30 @@ pub fn check_config() { } pub fn check_config_process(force_reset: bool) { - if force_reset { - HwCodecConfig::remove(); - } - if let Ok(exe) = std::env::current_exe() { - std::thread::spawn(move || { - std::process::Command::new(exe) - .arg("--check-hwcodec-config") - .status() - .ok(); - HwCodecConfig::refresh(); - }); - }; + use hbb_common::sysinfo::{ProcessExt, System, SystemExt}; + + std::thread::spawn(move || { + if force_reset { + HwCodecConfig::remove(); + } + if let Ok(exe) = std::env::current_exe() { + if let Some(file_name) = exe.file_name().to_owned() { + let s = System::new_all(); + let arg = "--check-hwcodec-config"; + for process in s.processes_by_name(&file_name.to_string_lossy().to_string()) { + if process.cmd().iter().any(|cmd| cmd.contains(arg)) { + log::warn!("already have process {}", arg); + return; + } + } + if let Ok(mut child) = std::process::Command::new(exe).arg(arg).spawn() { + let second = 3; + std::thread::sleep(std::time::Duration::from_secs(second)); + // kill: Different platforms have different results + child.kill().ok(); + HwCodecConfig::refresh(); + } + } + }; + }); } diff --git a/src/ipc.rs b/src/ipc.rs index 699b0bcd7..b1b130340 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -549,7 +549,7 @@ async fn check_pid(postfix: &str) { file.read_to_string(&mut content).ok(); let pid = content.parse::().unwrap_or(0); if pid > 0 { - use sysinfo::{ProcessExt, System, SystemExt}; + use hbb_common::sysinfo::{ProcessExt, System, SystemExt}; let mut sys = System::new(); sys.refresh_processes(); if let Some(p) = sys.process(pid.into()) { diff --git a/src/platform/macos.rs b/src/platform/macos.rs index 0c8c51455..910c26982 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -558,7 +558,7 @@ pub fn hide_dock() { } fn check_main_window() -> bool { - use sysinfo::{ProcessExt, System, SystemExt}; + use hbb_common::sysinfo::{ProcessExt, System, SystemExt}; let mut sys = System::new(); sys.refresh_processes(); let app = format!("/Applications/{}.app", crate::get_app_name()); From a91c9ef614036aeb3806ef3905b125a19d78f167 Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 21 Feb 2023 16:29:06 +0800 Subject: [PATCH 1949/2015] fix ab ActionMore can't popup Signed-off-by: 21pages --- flutter/lib/common/widgets/address_book.dart | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/flutter/lib/common/widgets/address_book.dart b/flutter/lib/common/widgets/address_book.dart index bd2a01296..88a5aaaa3 100644 --- a/flutter/lib/common/widgets/address_book.dart +++ b/flutter/lib/common/widgets/address_book.dart @@ -43,11 +43,8 @@ class _AddressBookState extends State { return Obx(() { if (gFFI.userModel.userName.value.isEmpty) { return Center( - child: ElevatedButton( - onPressed: loginDialog, - child: Text(translate("Login")) - ) - ); + child: ElevatedButton( + onPressed: loginDialog, child: Text(translate("Login")))); } else { if (gFFI.abModel.abLoading.value) { return const Center( @@ -153,13 +150,13 @@ class _AddressBookState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(translate('Tags')), - GestureDetector( - onTapDown: (e) { - final x = e.globalPosition.dx; - final y = e.globalPosition.dy; + Listener( + onPointerDown: (e) { + final x = e.position.dx; + final y = e.position.dy; menuPos = RelativeRect.fromLTRB(x, y, x, y); }, - onTap: () => _showMenu(menuPos), + onPointerUp: (_) => _showMenu(menuPos), child: ActionMore()), ], ); From 9dbd1f88f5ec72b0c320173ff28ae7d38d2a2889 Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 21 Feb 2023 18:43:43 +0800 Subject: [PATCH 1950/2015] listen flutter key event when there's no input monitor permission Signed-off-by: fufesou --- flutter/lib/common/widgets/remote_input.dart | 11 +---------- flutter/lib/consts.dart | 1 + flutter/lib/desktop/pages/desktop_home_page.dart | 5 +++++ flutter/lib/desktop/pages/remote_tab_page.dart | 2 ++ flutter/lib/desktop/widgets/remote_menubar.dart | 6 ++++++ flutter/lib/models/input_model.dart | 4 ++++ src/flutter_ffi.rs | 9 ++++++--- 7 files changed, 25 insertions(+), 13 deletions(-) diff --git a/flutter/lib/common/widgets/remote_input.dart b/flutter/lib/common/widgets/remote_input.dart index 5833e760d..dd39cbdfd 100644 --- a/flutter/lib/common/widgets/remote_input.dart +++ b/flutter/lib/common/widgets/remote_input.dart @@ -1,8 +1,6 @@ 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 { @@ -20,13 +18,6 @@ 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( @@ -34,7 +25,7 @@ class RawKeyFocusScope extends StatelessWidget { canRequestFocus: true, focusNode: focusNode, onFocusChange: onFocusChange, - onKey: onKey, + onKey: inputModel.handleRawKeyEvent, child: child)); } } diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 2b4bc7f32..a4cb50025 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -20,6 +20,7 @@ const String kAppTypeDesktopPortForward = "port forward"; const String kWindowMainWindowOnTop = "main_window_on_top"; const String kWindowGetWindowInfo = "get_window_info"; +const String kWindowDisableGrabKeyboard = "disable_grab_keyboard"; const String kWindowActionRebuild = "rebuild"; const String kWindowEventHide = "hide"; const String kWindowEventShow = "show"; diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index b5cadbcdf..ff99c9dc8 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -14,6 +14,7 @@ import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart'; import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/server_model.dart'; +import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; @@ -498,6 +499,10 @@ class _DesktopHomePageState extends State if (watchIsInputMonitoring) { if (bind.mainIsCanInputMonitoring(prompt: false)) { watchIsInputMonitoring = false; + // Do not notify for now. + // Monitoring may not take effect until the process is restarted. + // rustDeskWinManager.call( + // WindowType.RemoteDesktop, kWindowDisableGrabKeyboard, ''); setState(() {}); } } diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index 64c78f24d..ef3a0dd04 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -111,6 +111,8 @@ class _ConnectionTabPageState extends State { forceRelay: args['forceRelay'], ), )); + } else if (call.method == kWindowDisableGrabKeyboard) { + stateGlobal.grabKeyboard = false; } else if (call.method == "onDestroy") { tabController.clear(); } else if (call.method == kWindowActionRebuild) { diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index e82e9d26e..adbf50abe 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -650,6 +650,12 @@ class _RemoteMenubarState extends State { } Widget _buildKeyboard(BuildContext context) { + // Do not support peer 1.1.9. + if (Platform.isMacOS && stateGlobal.grabKeyboard) { + bind.sessionSetKeyboardMode(id: widget.id, value: 'map'); + return Offstage(); + } + FfiModel ffiModel = Provider.of(context); if (ffiModel.permissions['keyboard'] == false) { return Offstage(); diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index b1491d526..9a5b06b14 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -58,6 +58,10 @@ class InputModel { InputModel(this.parent); KeyEventResult handleRawKeyEvent(FocusNode data, RawKeyEvent e) { + if (!stateGlobal.grabKeyboard) { + return KeyEventResult.handled; + } + // * Currently mobile does not enable map mode if (isDesktop) { bind.sessionGetKeyboardMode(id: id).then((result) { diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index f3bc45856..68ddce9b7 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1,13 +1,13 @@ +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use crate::common::get_default_sound_input; use crate::{ client::file_trait::FileManager, - common::make_fd_to_json, common::is_keyboard_mode_supported, + common::make_fd_to_json, 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}, @@ -1181,6 +1181,9 @@ pub fn main_start_grab_keyboard() -> SyncReturn { return SyncReturn(false); } crate::keyboard::client::start_grab_loop(); + if !is_can_input_monitoring(false) { + return SyncReturn(false); + } SyncReturn(true) } From ac6ea0d9fc13c1cdfbfd7c49c2b0e76c13568012 Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 21 Feb 2023 19:04:22 +0800 Subject: [PATCH 1951/2015] 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 adbf50abe..45857aa45 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -651,7 +651,7 @@ class _RemoteMenubarState extends State { Widget _buildKeyboard(BuildContext context) { // Do not support peer 1.1.9. - if (Platform.isMacOS && stateGlobal.grabKeyboard) { + if (stateGlobal.grabKeyboard) { bind.sessionSetKeyboardMode(id: widget.id, value: 'map'); return Offstage(); } From bfb0ea9d1dc36afa9e973060590ed46bd7dd85d2 Mon Sep 17 00:00:00 2001 From: Integral <71180087+Integral-Tech@users.noreply.github.com> Date: Tue, 21 Feb 2023 20:50:59 +0800 Subject: [PATCH 1952/2015] Update cn.rs --- src/lang/cn.rs | 138 ++++++++++++++++++++++++------------------------- 1 file changed, 69 insertions(+), 69 deletions(-) diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 9d0d176da..78a1f9e73 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -3,7 +3,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = [ ("Status", "状态"), ("Your Desktop", "你的桌面"), - ("desk_tip", "你的桌面可以通过下面的ID和密码访问。"), + ("desk_tip", "你的桌面可以通过下面的 ID 和密码访问。"), ("Password", "密码"), ("Ready", "就绪"), ("Established", "已建立"), @@ -11,7 +11,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable Service", "允许服务"), ("Start Service", "启动服务"), ("Service is running", "服务正在运行"), - ("Service is not running", "服务没有启动"), + ("Service is not running", "服务未运行"), ("not_ready_status", "未就绪,请检查网络连接"), ("Control Remote Desktop", "控制远程桌面"), ("Transfer File", "传输文件"), @@ -19,49 +19,49 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recent Sessions", "最近访问过"), ("Address Book", "地址簿"), ("Confirmation", "确认"), - ("TCP Tunneling", "TCP隧道"), + ("TCP Tunneling", "TCP 隧道"), ("Remove", "删除"), ("Refresh random password", "刷新随机密码"), ("Set your own password", "设置密码"), ("Enable Keyboard/Mouse", "允许控制键盘/鼠标"), ("Enable Clipboard", "允许同步剪贴板"), ("Enable File Transfer", "允许传输文件"), - ("Enable TCP Tunneling", "允许建立TCP隧道"), - ("IP Whitelisting", "IP白名单"), + ("Enable TCP Tunneling", "允许建立 TCP 隧道"), + ("IP Whitelisting", "IP 白名单"), ("ID/Relay Server", "ID/中继服务器"), ("Import Server Config", "导入服务器配置"), ("Export Server Config", "导出服务器配置"), ("Import server configuration successfully", "导入服务器配置信息成功"), ("Export server configuration successfully", "导出服务器配置信息成功"), - ("Invalid server configuration", "无效服务器配置,请修改后重新拷贝配置信息到剪贴板后点击此按钮"), - ("Clipboard is empty", "拷贝配置信息到剪贴板后点击此按钮,可以自动导入配置"), + ("Invalid server configuration", "服务器配置无效,请修改后重新复制配置信息到剪贴板,然后点击此按钮"), + ("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之间。"), + ("Change ID", "更改 ID"), + ("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", "关于"), ("Slogan_tip", ""), - ("Privacy Statement", ""), + ("Privacy Statement", "隐私声明"), ("Mute", "静音"), - ("Build Date", ""), - ("Version", ""), - ("Home", ""), + ("Build Date", "构建日期"), + ("Version", "版本"), + ("Home", "主页"), ("Audio Input", "音频输入"), ("Enhancements", "增强功能"), ("Hardware Codec", "硬件编解码"), ("Adaptive Bitrate", "自适应码率"), - ("ID Server", "ID服务器"), + ("ID Server", "ID 服务器"), ("Relay Server", "中继服务器"), - ("API Server", "API服务器"), - ("invalid_http", "必须以http://或者https://开头"), - ("Invalid IP", "无效IP"), + ("API Server", "API 服务器"), + ("invalid_http", "必须以 http:// 或者 https:// 开头"), + ("Invalid IP", "无效 IP"), ("Invalid format", "无效格式"), ("server_not_support", "服务器暂不支持"), - ("Not available", "已被占用"), + ("Not available", "不可用"), ("Too frequent", "修改太频繁,请稍后再试"), ("Cancel", "取消"), ("Skip", "跳过"), @@ -72,12 +72,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please enter your password", "请输入密码"), ("Remember password", "记住密码"), ("Wrong Password", "密码错误"), - ("Do you want to enter again?", "还想输入一次吗?"), + ("Do you want to enter again?", "是否要再次输入?"), ("Connection Error", "连接错误"), ("Error", "错误"), ("Reset by the peer", "连接被对方关闭"), ("Connecting...", "正在连接..."), - ("Connection in progress. Please wait.", "连接进行中,请稍等。"), + ("Connection in progress. Please wait.", "正在进行连接,请稍候。"), ("Please try 1 minute later", "一分钟后再试"), ("Login Error", "登录错误"), ("Successful", "成功"), @@ -102,14 +102,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Unselect All", "取消全选"), ("Empty Directory", "空文件夹"), ("Not an empty directory", "这不是一个空文件夹"), - ("Are you sure you want to delete this file?", "是否删除此文件?"), - ("Are you sure you want to delete this empty directory?", "是否删除此空文件夹?"), - ("Are you sure you want to delete the file of this directory?", "是否删除文件夹下的文件?"), + ("Are you sure you want to delete this file?", "是否删除此文件?"), + ("Are you sure you want to delete this empty directory?", "是否删除此空文件夹?"), + ("Are you sure you want to delete the file of this directory?", "是否删除此文件夹下的文件?"), ("Do this for all conflicts", "应用于其它冲突"), ("This is irreversible!", "此操作不可逆!"), ("Deleting", "正在删除"), ("files", "文件"), - ("Waiting", "等待..."), + ("Waiting", "正在等待..."), ("Finished", "完成"), ("Speed", "速度"), ("Custom Image Quality", "设置画面质量"), @@ -128,31 +128,31 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Custom", "自定义"), ("Show remote cursor", "显示远程光标"), ("Show quality monitor", "显示质量监测"), - ("Disable clipboard", "禁止剪贴板"), - ("Lock after session end", "断开后锁定远程电脑"), + ("Disable clipboard", "禁用剪贴板"), + ("Lock after session end", "会话结束后锁定远程电脑"), ("Insert", "插入"), ("Insert Lock", "锁定远程电脑"), ("Refresh", "刷新画面"), - ("ID does not exist", "ID不存在"), + ("ID does not exist", "ID 不存在"), ("Failed to connect to rendezvous server", "连接注册服务器失败"), ("Please try later", "请稍后再试"), - ("Remote desktop is offline", "远程电脑不在线"), - ("Key mismatch", "Key不匹配"), + ("Remote desktop is offline", "远程电脑处于离线状态"), + ("Key mismatch", "密钥不匹配"), ("Timeout", "连接超时"), ("Failed to connect to relay server", "无法连接到中继服务器"), ("Failed to connect via rendezvous server", "无法通过注册服务器建立连接"), ("Failed to connect via relay server", "无法通过中继服务器建立连接"), - ("Failed to make direct connection to remote desktop", "无法建立直接连接"), + ("Failed to make direct connection to remote desktop", "无法直接连接到远程桌面"), ("Set Password", "设置密码"), ("OS Password", "操作系统密码"), - ("install_tip", "你正在运行未安装版本,由于UAC限制,作为被控端,会在某些情况下无法控制鼠标键盘,或者录制屏幕,请点击下面的按钮将 RustDesk 安装到系统,从而规避上述问题。"), + ("install_tip", "你正在运行未安装版本,由于 UAC 限制,作为被控端,会在某些情况下无法控制鼠标键盘,或者录制屏幕,请点击下面的按钮将 RustDesk 安装到系统,从而规避上述问题。"), ("Click to upgrade", "点击这里升级"), ("Click to download", "点击这里下载"), ("Click to update", "点击这里更新"), ("Configure", "配置"), ("config_acc", "为了能够远程控制你的桌面, 请给予 RustDesk \"辅助功能\" 权限。"), ("config_screen", "为了能够远程访问你的桌面, 请给予 RustDesk \"屏幕录制\" 权限。"), - ("Installing ...", "安装 ..."), + ("Installing ...", "安装中..."), ("Install", "安装"), ("Installation", "安装"), ("Installation Path", "安装路径"), @@ -161,10 +161,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("agreement_tip", "开始安装即表示接受许可协议。"), ("Accept and Install", "同意并安装"), ("End-user license agreement", "用户协议"), - ("Generating ...", "正在产生 ..."), + ("Generating ...", "正在生成..."), ("Your installation is lower version.", "你安装的版本比当前运行的低。"), ("not_close_tcp_tip", "请在使用隧道的时候,不要关闭本窗口"), - ("Listening ...", "正在等待隧道连接 ..."), + ("Listening ...", "正在等待隧道连接..."), ("Remote Host", "远程主机"), ("Remote Port", "远程端口"), ("Action", "动作"), @@ -173,7 +173,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Local Address", "当前地址"), ("Change Local Port", "修改本地端口"), ("setup_server_tip", "如果需要更快连接速度,你可以选择自建服务器"), - ("Too short, at least 6 characters.", "太短了,至少6个字符"), + ("Too short, at least 6 characters.", "太短了,至少 6 个字符"), ("The confirmation is not identical.", "两次输入不匹配"), ("Permissions", "权限"), ("Accept", "接受"), @@ -183,21 +183,21 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Allow using clipboard", "允许使用剪贴板"), ("Allow hearing sound", "允许听到声音"), ("Allow file copy and paste", "允许复制粘贴文件"), - ("Connected", "已经连接"), + ("Connected", "已连接"), ("Direct and encrypted connection", "加密直连"), ("Relayed and encrypted connection", "加密中继连接"), ("Direct and unencrypted connection", "非加密直连"), ("Relayed and unencrypted connection", "非加密中继连接"), - ("Enter Remote ID", "输入对方ID"), + ("Enter Remote ID", "输入对方 ID"), ("Enter your password", "输入密码"), ("Logging in...", "正在登录..."), - ("Enable RDP session sharing", "允许RDP会话共享"), + ("Enable RDP session sharing", "允许 RDP 会话共享"), ("Auto Login", "自动登录(设置断开后锁定才有效)"), - ("Enable Direct IP Access", "允许IP直接访问"), - ("Rename", "改名"), + ("Enable Direct IP Access", "允许 IP 直接访问"), + ("Rename", "重命名"), ("Space", "空格"), ("Create Desktop Shortcut", "创建桌面快捷方式"), - ("Change Path", "改变路径"), + ("Change Path", "更改路径"), ("Create Folder", "创建文件夹"), ("Please enter the folder name", "请输入文件夹名称"), ("Fix it", "修复"), @@ -212,29 +212,29 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Invalid port", "无效端口"), ("Closed manually by the peer", "被对方手动关闭"), ("Enable remote configuration modification", "允许远程修改配置"), - ("Run without install", "无安装运行"), + ("Run without install", "不安装直接运行"), ("Connect via relay", "中继连接"), ("Always connect via relay", "强制走中继连接"), - ("whitelist_tip", "只有白名单里的ip才能访问我"), + ("whitelist_tip", "只有白名单里的 IP 才能访问我"), ("Login", "登录"), ("Verify", "验证"), ("Remember me", "记住我"), ("Trust this device", "信任此设备"), ("Verification code", "验证码"), - ("verification_tip", "检测到新设备登录,已向注册邮箱发送了登录验证码,输入验证码继续登录"), + ("verification_tip", "检测到新设备登录,已向注册邮箱发送了登录验证码,请输入验证码继续登录"), ("Logout", "登出"), ("Tags", "标签"), - ("Search ID", "查找ID"), + ("Search ID", "查找 ID"), ("whitelist_sep", "可以使用逗号,分号,空格或者换行符作为分隔符"), - ("Add ID", "增加ID"), + ("Add ID", "增加 ID"), ("Add Tag", "增加标签"), ("Unselect all tags", "取消选择所有标签"), ("Network error", "网络错误"), ("Username missed", "用户名没有填写"), ("Password missed", "密码没有填写"), - ("Wrong credentials", "提供的登入信息错误"), + ("Wrong credentials", "提供的登录信息错误"), ("Edit Tag", "修改标签"), - ("Unremember Password", "忘掉密码"), + ("Unremember Password", "忘记密码"), ("Favorites", "收藏"), ("Add to Favorites", "加入到收藏"), ("Remove from Favorites", "从收藏中删除"), @@ -244,9 +244,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Hostname", "主机名"), ("Discovered", "已发现"), ("install_daemon_tip", "为了开机启动,请安装系统服务。"), - ("Remote ID", "远程ID"), + ("Remote ID", "远程 ID"), ("Paste", "粘贴"), - ("Paste here?", "粘贴到这里?"), + ("Paste here?", "粘贴到这里?"), ("Are you sure to close the connection?", "是否确认关闭连接?"), ("Download new version", "下载新版本"), ("Touch mode", "触屏模式"), @@ -284,7 +284,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Do you accept?", "是否接受?"), ("Open System Setting", "打开系统设置"), ("How to get Android input permission?", "如何获取安卓的输入权限?"), - ("android_input_permission_tip1", "为了让远程设备通过鼠标或触屏控制您的安卓设备,你需要允許 RustDesk 使用\"无障碍\"服务。"), + ("android_input_permission_tip1", "为了让远程设备通过鼠标或触屏控制您的安卓设备,你需要允许 RustDesk 使用\"无障碍\"服务。"), ("android_input_permission_tip2", "请在接下来的系统设置页面里,找到并进入 [已安装的服务] 页面,将 [RustDesk Input] 服务开启。"), ("android_new_connection_tip", "收到新的连接控制请求,对方想要控制你当前的设备。"), ("android_service_will_start_tip", "开启录屏权限将自动开启服务,允许其他设备向此设备请求建立连接。"), @@ -293,7 +293,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_start_service_tip", "点击 [启动服务] 或打开 [屏幕录制] 权限开启手机屏幕共享服务。"), ("Account", "账户"), ("Overwrite", "覆盖"), - ("This file exists, skip or overwrite this file?", "这个文件/文件夹已存在,跳过/覆盖?"), + ("This file exists, skip or overwrite this file?", "这个文件/文件夹已存在,跳过/覆盖?"), ("Quit", "退出"), ("doc_mac_permission", "https://rustdesk.com/docs/zh-cn/manual/mac#%E5%90%AF%E7%94%A8%E6%9D%83%E9%99%90"), ("Help", "帮助"), @@ -314,7 +314,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_open_battery_optimizations_tip", "如需关闭此功能,请在接下来的 RustDesk 应用设置页面中,找到并进入 [电源] 页面,取消勾选 [不受限制]"), ("Connection not allowed", "对方不允许连接"), ("Legacy mode", "传统模式"), - ("Map mode", "1:1传输"), + ("Map mode", "1:1 传输"), ("Translate mode", "翻译模式"), ("Use permanent password", "使用固定密码"), ("Use both passwords", "同时使用两种密码"), @@ -355,16 +355,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable Audio", "允许传输音频"), ("Unlock Network Settings", "解锁网络设置"), ("Server", "服务器"), - ("Direct IP Access", "IP直接访问"), + ("Direct IP Access", "IP 直接访问"), ("Proxy", "代理"), ("Apply", "应用"), - ("Disconnect all devices?", "断开所有远程连接?"), + ("Disconnect all devices?", "断开所有远程连接?"), ("Clear", "清空"), ("Audio Input Device", "音频输入设备"), ("Deny remote access", "拒绝远程访问"), - ("Use IP Whitelisting", "只允许白名单上的IP访问"), + ("Use IP Whitelisting", "只允许白名单上的 IP 访问"), ("Network", "网络"), - ("Enable RDP", "允许RDP访问"), + ("Enable RDP", "允许 RDP 访问"), ("Pin menubar", "固定菜单栏"), ("Unpin menubar", "取消固定菜单栏"), ("Recording", "录屏"), @@ -379,7 +379,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Deny LAN Discovery", "拒绝局域网发现"), ("Write a message", "输入聊天消息"), ("Prompt", "提示"), - ("Please wait for confirmation of UAC...", "请等待对方确认 UAC ..."), + ("Please wait for confirmation of UAC...", "请等待对方确认 UAC..."), ("elevated_foreground_window_tip", "远端桌面的当前窗口需要更高的权限才能操作, 暂时无法使用鼠标键盘, 可以请求对方最小化当前窗口, 或者在连接管理窗口点击提升。为避免这个问题,建议在远端设备上安装本软件。"), ("Disconnected", "会话已结束"), ("Other", "其他"), @@ -407,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request access to your device", "请求访问你的设备"), ("Hide connection management window", "隐藏连接管理窗口"), ("hide_cm_tip", "在只允许密码连接并且只用固定密码的情况下才允许隐藏"), - ("wayland_experiment_tip", "Wayland 支持处于实验阶段,如果你需要使用无人值守访问,请使用X11。"), + ("wayland_experiment_tip", "Wayland 支持处于实验阶段,如果你需要使用无人值守访问,请使用 X11。"), ("Right click to select tabs", "右键选择选项卡"), ("Skipped", "已跳过"), ("Add to Address Book", "添加到地址簿"), @@ -417,7 +417,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Local keyboard type", "本地键盘类型"), ("Select local keyboard type", "请选择本地键盘类型"), ("software_render_tip", "如果你使用英伟达显卡, 并且远程窗口在会话建立后会立刻关闭, 那么安装 nouveau 驱动并且选择使用软件渲染可能会有帮助。重启软件后生效。"), - ("Always use software rendering", "使用软件渲染"), + ("Always use software rendering", "始终使用软件渲染"), ("config_input", "为了能够通过键盘控制远程桌面, 请给予 RustDesk \"输入监控\" 权限。"), ("config_microphone", "为了支持通过麦克风进行音频传输,请给予 RustDesk \"录音\"权限。"), ("request_elevation_tip", "如果对面有人, 也可以请求提升权限。"), @@ -434,25 +434,25 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("lowercase", "小写字母"), ("digit", "数字"), ("special character", "特殊字符"), - ("length>=8", "长度不小于8"), + ("length>=8", "长度不小于 8"), ("Weak", "弱"), ("Medium", "中"), ("Strong", "强"), ("Switch Sides", "反转访问方向"), - ("Please confirm if you want to share your desktop?", "请确认要让对方访问你的桌面?"), + ("Please confirm if you want to share your desktop?", "请确认是否要让对方访问你的桌面?"), ("Display", "显示"), ("Default View Style", "默认显示方式"), ("Default Scroll Style", "默认滚动方式"), ("Default Image Quality", "默认图像质量"), ("Default Codec", "默认编解码"), - ("Bitrate", "波特率"), + ("Bitrate", "比特率"), ("FPS", "帧率"), ("Auto", "自动"), ("Other Default Options", "其它默认选项"), ("Voice call", "语音通话"), ("Text chat", "文字聊天"), - ("Stop voice call", "停止语音聊天"), - ("relay_hint_tip", "可能无法直连,可以尝试中继连接。\n另外,如果想直接使用中继连接,可以在ID后面添加/r,或者在卡片选项里选择强制走中继连接。"), + ("Stop voice call", "停止语音通话"), + ("relay_hint_tip", "可能无法直连,可以尝试中继连接。\n另外,如果想直接使用中继连接,可以在 ID 后面添加/r,或者在卡片选项里选择强制走中继连接。"), ("Reconnect", "重连"), ].iter().cloned().collect(); } From c1066aab3a344430d86010eeae6259e6b84ce183 Mon Sep 17 00:00:00 2001 From: Integral <71180087+Integral-Tech@users.noreply.github.com> Date: Tue, 21 Feb 2023 21:42:15 +0800 Subject: [PATCH 1953/2015] Update cn.rs Some small tweaks --- src/lang/cn.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 78a1f9e73..4824ac5e9 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -122,9 +122,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stretch", "伸展"), ("Scrollbar", "滚动条"), ("ScrollAuto", "自动滚动"), - ("Good image quality", "好画质"), - ("Balanced", "一般画质"), - ("Optimize reaction time", "优化反应时间"), + ("Good image quality", "画质最优化"), + ("Balanced", "平衡"), + ("Optimize reaction time", "速度最优化"), ("Custom", "自定义"), ("Show remote cursor", "显示远程光标"), ("Show quality monitor", "显示质量监测"), @@ -215,7 +215,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Run without install", "不安装直接运行"), ("Connect via relay", "中继连接"), ("Always connect via relay", "强制走中继连接"), - ("whitelist_tip", "只有白名单里的 IP 才能访问我"), + ("whitelist_tip", "只有白名单里的 IP 才能访问本机"), ("Login", "登录"), ("Verify", "验证"), ("Remember me", "记住我"), @@ -396,7 +396,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("or", "或"), ("Continue with", "使用"), ("Elevate", "提权"), - ("Zoom cursor", "缩放鼠标"), + ("Zoom cursor", "缩放光标"), ("Accept sessions via password", "只允许密码访问"), ("Accept sessions via click", "只允许点击访问"), ("Accept sessions via both", "允许密码或点击访问"), @@ -445,7 +445,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Default Scroll Style", "默认滚动方式"), ("Default Image Quality", "默认图像质量"), ("Default Codec", "默认编解码"), - ("Bitrate", "比特率"), + ("Bitrate", "码率"), ("FPS", "帧率"), ("Auto", "自动"), ("Other Default Options", "其它默认选项"), From f03c265f9c224b95c169b34447ff2bc69707458d Mon Sep 17 00:00:00 2001 From: Kingtous Date: Tue, 21 Feb 2023 21:39:32 +0800 Subject: [PATCH 1954/2015] fix: orderout not working when fullscreen on macos --- flutter/lib/desktop/widgets/tabbar_widget.dart | 15 +++++++++++---- flutter/pubspec.yaml | 2 +- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 9ba7a6315..357abab2e 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -548,13 +548,20 @@ class WindowActionPanelState extends State if (rustDeskWinManager.getActiveWindows().contains(kMainWindowId)) { await rustDeskWinManager.unregisterActiveWindow(kMainWindowId); } - // `hide` must be placed after unregisterActiveWindow, because once all windows are hidden, - // flutter closes the application on macOS. We should ensure the post-run logic has ran successfully. - // e.g.: saving window position. + // macOS specific workaround, the windows is not hiding when in fullscreen. + if (Platform.isMacOS && await windowManager.isFullScreen()) { + await windowManager.setFullScreen(false); + await Future.delayed(Duration(seconds: 1)); + } await windowManager.hide(); } else { // it's safe to hide the subwindow - await WindowController.fromWindowId(kWindowId!).hide(); + final controller = WindowController.fromWindowId(kWindowId!); + if (Platform.isMacOS && await controller.isFullScreen()) { + await controller.setFullscreen(false); + await Future.delayed(Duration(seconds: 1)); + } + await controller.hide(); await Future.wait([ rustDeskWinManager .call(WindowType.Main, kWindowEventHide, {"id": kWindowId!}), diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index df29252c9..a4584f4a1 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -59,7 +59,7 @@ dependencies: desktop_multi_window: git: url: https://github.com/Kingtous/rustdesk_desktop_multi_window - ref: bc8604a88e52b2b6e64d2661ae49a71450a47af8 + ref: 84a027ac2eed31e1b7c0ad11de47ed846501824e freezed_annotation: ^2.0.3 flutter_custom_cursor: ^0.0.4 window_size: From a46c39a67b78ab02cb2be6d049947a29112f8ea4 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Wed, 22 Feb 2023 09:04:43 +0800 Subject: [PATCH 1955/2015] add: texture renderer --- flutter/lib/desktop/widgets/tabbar_widget.dart | 2 +- flutter/pubspec.yaml | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 357abab2e..ee3aaaf2c 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -548,7 +548,7 @@ class WindowActionPanelState extends State if (rustDeskWinManager.getActiveWindows().contains(kMainWindowId)) { await rustDeskWinManager.unregisterActiveWindow(kMainWindowId); } - // macOS specific workaround, the windows is not hiding when in fullscreen. + // macOS specific workaround, the window is not hiding when in fullscreen. if (Platform.isMacOS && await windowManager.isFullScreen()) { await windowManager.setFullScreen(false); await Future.delayed(Duration(seconds: 1)); diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index a4584f4a1..e009ea890 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -59,7 +59,7 @@ dependencies: desktop_multi_window: git: url: https://github.com/Kingtous/rustdesk_desktop_multi_window - ref: 84a027ac2eed31e1b7c0ad11de47ed846501824e + ref: f37357ed98a10717576eb9ed8413e92b2ec5d13a freezed_annotation: ^2.0.3 flutter_custom_cursor: ^0.0.4 window_size: @@ -92,6 +92,7 @@ dependencies: password_strength: ^0.2.0 flutter_launcher_icons: ^0.11.0 flutter_keyboard_visibility: ^5.4.0 + texture_rgba_renderer: ^0.0.8 dev_dependencies: From ead828071fb79449de1d71aa15e4f2e6fb432021 Mon Sep 17 00:00:00 2001 From: enforcer007 Date: Wed, 22 Feb 2023 10:04:49 +0530 Subject: [PATCH 1956/2015] dev container spin up --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 5e4c5e70d..a1790107f 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,13 @@ Yet another remote desktop software, written in Rust. Works out of the box, no c RustDesk welcomes contribution from everyone. See [`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md) for help getting started. [**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ) +## Dev Container + +[![Open in Dev Containers](https://img.shields.io/static/v1?label=Dev%20Container&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/rustdesk/rustdesk) + +If you already have VS Code and Docker installed, you can click the badge above to get started. Clicking will cause VS Code to automatically install the Dev Containers extension if needed, clone the source code into a container volume, and spin up a dev container for use. + +[**How does RustDesk work?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F) [**BINARY DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases) From db5a8404fec93f9817355639b760088bc712a3d7 Mon Sep 17 00:00:00 2001 From: enforcer007 Date: Wed, 22 Feb 2023 10:30:18 +0530 Subject: [PATCH 1957/2015] devcontainer docs --- README.md | 12 +++++++----- docs/DEVCONTAINER.md | 14 ++++++++++++++ 2 files changed, 21 insertions(+), 5 deletions(-) create mode 100644 docs/DEVCONTAINER.md diff --git a/README.md b/README.md index a1790107f..c081ca9cf 100644 --- a/README.md +++ b/README.md @@ -20,11 +20,6 @@ Yet another remote desktop software, written in Rust. Works out of the box, no c RustDesk welcomes contribution from everyone. See [`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md) for help getting started. [**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ) -## Dev Container - -[![Open in Dev Containers](https://img.shields.io/static/v1?label=Dev%20Container&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/rustdesk/rustdesk) - -If you already have VS Code and Docker installed, you can click the badge above to get started. Clicking will cause VS Code to automatically install the Dev Containers extension if needed, clone the source code into a container volume, and spin up a dev container for use. [**How does RustDesk work?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F) @@ -48,6 +43,13 @@ Below are the servers you are using for free, they may change over time. If you | USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8GB RAM | | Ukraine (Kyiv) | dc.volia (2VM) | 2 vCPU / 4GB RAM | +## Dev Container + +[![Open in Dev Containers](https://img.shields.io/static/v1?label=Dev%20Container&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/rustdesk/rustdesk) + +If you already have VS Code and Docker installed, you can click the badge above to get started. Clicking will cause VS Code to automatically install the Dev Containers extension if needed, clone the source code into a container volume, and spin up a dev container for use. + +Go through [DEVCONTAINER.md](docs/DEVCONTAINER.md) for more info. ## Dependencies Desktop versions use [sciter](https://sciter.com/) or Flutter for GUI, this tutorial is for Sciter only. diff --git a/docs/DEVCONTAINER.md b/docs/DEVCONTAINER.md new file mode 100644 index 000000000..067e0ecf9 --- /dev/null +++ b/docs/DEVCONTAINER.md @@ -0,0 +1,14 @@ + +After the start of devcontainer in docker container, a linux binary in debug mode is created. + +Currently devcontainer offers linux and android builds in both debug and release mode. + +Below is the table on commands to run from root of the project for creating specific builds. + +Command|Build Type|Mode +-|-|-| +`.devcontainer/build.sh --debug linux`|Linux|debug +`.devcontainer/build.sh --release linux`|Linux|release +`.devcontainer/build.sh --debug android`|android-arm64|debug +`.devcontainer/build.sh --release android`|android-arm64|debug + From 9873a2d70032e63d46b760486190c4cad9f531d5 Mon Sep 17 00:00:00 2001 From: enforcer007 Date: Wed, 22 Feb 2023 10:54:16 +0530 Subject: [PATCH 1958/2015] Don't run github actions on ignored paths. --- .github/workflows/ci.yml | 5 +++++ .github/workflows/flutter-ci.yml | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2e1702a60..bba114315 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,6 +7,9 @@ name: CI on: workflow_dispatch: pull_request: + paths-ignore: + - "docs/**" + - "README.md" push: branches: - master @@ -14,6 +17,8 @@ on: - '*' paths-ignore: - ".github/**" + - "docs/**" + - "README.md" jobs: # ensure_cargo_fmt: diff --git a/.github/workflows/flutter-ci.yml b/.github/workflows/flutter-ci.yml index 78c60df37..2386f17dd 100644 --- a/.github/workflows/flutter-ci.yml +++ b/.github/workflows/flutter-ci.yml @@ -3,6 +3,9 @@ name: Full Flutter CI on: workflow_dispatch: pull_request: + paths-ignore: + - "docs/**" + - "README.md" push: branches: - master @@ -10,6 +13,8 @@ on: - '*' paths-ignore: - ".github/**" + - "docs/**" + - "README.md" env: LLVM_VERSION: "15.0.6" From 65374b25933adb74f19a99fdd2864788ecddbf30 Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Wed, 22 Feb 2023 13:31:09 +0800 Subject: [PATCH 1959/2015] Update README.md --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index c081ca9cf..419a91f96 100644 --- a/README.md +++ b/README.md @@ -21,8 +21,6 @@ RustDesk welcomes contribution from everyone. See [`docs/CONTRIBUTING.md`](docs/ [**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ) -[**How does RustDesk work?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F) - [**BINARY DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases) [**NIGHTLY BUILD**](https://github.com/rustdesk/rustdesk/releases/tag/nightly) From c26c3459058302b305022a58b632e7f33349ed6d Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Wed, 22 Feb 2023 13:31:52 +0800 Subject: [PATCH 1960/2015] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 419a91f96..8af79915b 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ Below are the servers you are using for free, they may change over time. If you If you already have VS Code and Docker installed, you can click the badge above to get started. Clicking will cause VS Code to automatically install the Dev Containers extension if needed, clone the source code into a container volume, and spin up a dev container for use. Go through [DEVCONTAINER.md](docs/DEVCONTAINER.md) for more info. + ## Dependencies Desktop versions use [sciter](https://sciter.com/) or Flutter for GUI, this tutorial is for Sciter only. From 848872e914af67368636c458d8abfee324fe472b Mon Sep 17 00:00:00 2001 From: mehdi-song Date: Wed, 22 Feb 2023 14:35:26 +0330 Subject: [PATCH 1961/2015] 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 00f6b70ac..70051f3e8 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -442,7 +442,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please confirm if you want to share your desktop?", "لطفاً تأیید کنید که آیا می خواهید دسکتاپ خود را به اشتراک بگذارید؟"), ("Display", "نمایش دادن"), ("Default View Style", "سبک نمایش پیش فرض"), - ("Default Scroll Style", "سبک پیش‌فرض اسکرول"), + ("Default Scroll Style", "سبک پیش‌ فرض اسکرول"), ("Default Image Quality", "کیفیت تصویر پیش فرض"), ("Default Codec", "کدک پیش فرض"), ("Bitrate", "میزان بیت صفحه نمایش"), @@ -452,7 +452,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", " را به شناسه اضافه کنید یا گزینه \"همیشه از طریق رله متصل شوید\" را در کارت همتا انتخاب کنید. همچنین، اگر می‌خواهید فوراً از سرور رله استفاده کنید، می‌توانید پسوند \"/r\".\n اتصال مستقیم ممکن است امکان پذیر نباشد. در این صورت می توانید سعی کنید از طریق سرور رله متصل شوید"), + ("Reconnect", "اتصال مجدد"), ].iter().cloned().collect(); } From 325077435c6a0e82c2109f30d7b2fecdcfb40165 Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Wed, 22 Feb 2023 22:13:21 +0100 Subject: [PATCH 1962/2015] file manager redesign implementation --- flutter/lib/common.dart | 20 +- flutter/lib/consts.dart | 2 +- .../lib/desktop/pages/file_manager_page.dart | 1024 ++++++++++------- .../desktop/pages/file_manager_tab_page.dart | 20 +- flutter/lib/desktop/widgets/menu_button.dart | 6 +- flutter/pubspec.lock | 10 +- flutter/pubspec.yaml | 1 + 7 files changed, 652 insertions(+), 431 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index e1dd1a1f8..ff8dfbb09 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -152,7 +152,7 @@ class MyTheme { static const Color canvasColor = Color(0xFF212121); static const Color border = Color(0xFFCCCCCC); static const Color idColor = Color(0xFF00B6F0); - static const Color darkGray = Color(0xFFB9BABC); + static const Color darkGray = Color.fromARGB(255, 148, 148, 148); static const Color cmIdColor = Color(0xFF21790B); static const Color dark = Colors.black87; static const Color button = Color(0xFF2C8CFF); @@ -160,8 +160,9 @@ class MyTheme { static ThemeData lightTheme = ThemeData( brightness: Brightness.light, - backgroundColor: Color(0xFFFFFFFF), - scaffoldBackgroundColor: Color(0xFFEEEEEE), + backgroundColor: Color(0xFFEEEEEE), + hoverColor: Color.fromARGB(255, 224, 224, 224), + scaffoldBackgroundColor: Color(0xFFFFFFFF), textTheme: const TextTheme( titleLarge: TextStyle(fontSize: 19, color: Colors.black87), titleSmall: TextStyle(fontSize: 14, color: Colors.black87), @@ -169,6 +170,7 @@ class MyTheme { bodyMedium: TextStyle(fontSize: 14, color: Colors.black87, height: 1.25), labelLarge: TextStyle(fontSize: 16.0, color: MyTheme.accent80)), + cardColor: Color(0xFFEEEEEE), hintColor: Color(0xFFAAAAAA), primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, @@ -191,8 +193,9 @@ class MyTheme { ); static ThemeData darkTheme = ThemeData( brightness: Brightness.dark, - backgroundColor: Color(0xFF252525), - scaffoldBackgroundColor: Color(0xFF141414), + backgroundColor: Color(0xFF24252B), + hoverColor: Color.fromARGB(255, 45, 46, 53), + scaffoldBackgroundColor: Color(0xFF18191E), textTheme: const TextTheme( titleLarge: TextStyle(fontSize: 19), titleSmall: TextStyle(fontSize: 14), @@ -200,7 +203,7 @@ class MyTheme { bodyMedium: TextStyle(fontSize: 14, height: 1.25), labelLarge: TextStyle( fontSize: 16.0, fontWeight: FontWeight.bold, color: accent80)), - cardColor: Color(0xFF252525), + cardColor: Color(0xFF24252B), primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, tabBarTheme: const TabBarTheme( @@ -217,9 +220,8 @@ class MyTheme { style: ButtonStyle(splashFactory: NoSplash.splashFactory), ) : null, - checkboxTheme: const CheckboxThemeData( - checkColor: MaterialStatePropertyAll(dark) - ), + checkboxTheme: + const CheckboxThemeData(checkColor: MaterialStatePropertyAll(dark)), ).copyWith( extensions: >[ ColorThemeExtension.dark, diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 2b4bc7f32..22ba221a9 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -52,7 +52,7 @@ const int kDesktopMaxDisplayHeight = 1080; const double kDesktopFileTransferNameColWidth = 200; const double kDesktopFileTransferModifiedColWidth = 120; -const double kDesktopFileTransferRowHeight = 25.0; +const double kDesktopFileTransferRowHeight = 30.0; const double kDesktopFileTransferHeaderHeight = 25.0; // https://en.wikipedia.org/wiki/Non-breaking_space diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 4edffb3b6..262121f3d 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -2,20 +2,23 @@ import 'dart:async'; import 'dart:io'; import 'dart:math'; +import 'package:percent_indicator/percent_indicator.dart'; import 'package:desktop_drop/desktop_drop.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_breadcrumb/flutter_breadcrumb.dart'; import 'package:flutter_hbb/desktop/widgets/list_search_action_listener.dart'; +import 'package:flutter_hbb/desktop/widgets/menu_button.dart'; import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; import 'package:flutter_hbb/models/file_model.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:wakelock/wakelock.dart'; + import '../../consts.dart'; import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu; - import '../../common.dart'; import '../../models/model.dart'; import '../../models/platform_model.dart'; @@ -147,7 +150,7 @@ class _FileManagerPageState extends State value: _ffi.fileModel, child: Consumer(builder: (context, model, child) { return Scaffold( - backgroundColor: Theme.of(context).backgroundColor, + backgroundColor: Theme.of(context).scaffoldBackgroundColor, body: Row( children: [ Flexible(flex: 3, child: body(isLocal: true)), @@ -192,35 +195,42 @@ class _FileManagerPageState extends State ]; return Listener( - onPointerDown: (e) { - final x = e.position.dx; - final y = e.position.dy; - menuPos = RelativeRect.fromLTRB(x, y, x, y); - }, - child: IconButton( - icon: const Icon(Icons.more_vert), - splashRadius: kDesktopIconButtonSplashRadius, - onPressed: () => mod_menu.showMenu( - context: context, - position: menuPos, - items: items - .map((e) => e.build( - context, - MenuConfig( - commonColor: CustomPopupMenuTheme.commonColor, - height: CustomPopupMenuTheme.height, - dividerHeight: CustomPopupMenuTheme.dividerHeight))) - .expand((i) => i) - .toList(), - elevation: 8, - ), - )); + onPointerDown: (e) { + final x = e.position.dx; + final y = e.position.dy; + menuPos = RelativeRect.fromLTRB(x, y, x, y); + }, + child: MenuButton( + onPressed: () => mod_menu.showMenu( + context: context, + position: menuPos, + items: items + .map( + (e) => e.build( + context, + MenuConfig( + commonColor: CustomPopupMenuTheme.commonColor, + height: CustomPopupMenuTheme.height, + dividerHeight: CustomPopupMenuTheme.dividerHeight), + ), + ) + .expand((i) => i) + .toList(), + elevation: 8, + ), + child: SvgPicture.asset( + "assets/dots.svg", + color: Theme.of(context).tabBarTheme.labelColor, + ), + color: Theme.of(context).cardColor, + hoverColor: Theme.of(context).hoverColor, + ), + ); } Widget body({bool isLocal = false}) { final scrollController = ScrollController(); return Container( - decoration: BoxDecoration(border: Border.all(color: Colors.black26)), margin: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(8.0), child: DropTarget( @@ -231,18 +241,22 @@ class _FileManagerPageState extends State onDragExited: (exit) { _dropMaskVisible.value = false; }, - child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - headTools(isLocal), - Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + headTools(isLocal), + Expanded( child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: _buildFileList(context, isLocal, scrollController), - ) - ], - )), - ]), + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: _buildFileList(context, isLocal, scrollController), + ) + ], + ), + ), + ], + ), ), ); } @@ -295,8 +309,7 @@ class _FileManagerPageState extends State }); return; } - _jumpToEntry( - isLocal, searchResult.first, scrollController, + _jumpToEntry(isLocal, searchResult.first, scrollController, kDesktopFileTransferRowHeight, buffer); }, onSearch: (buffer) { @@ -311,8 +324,7 @@ class _FileManagerPageState extends State }); return; } - _jumpToEntry( - isLocal, searchResult.first, scrollController, + _jumpToEntry(isLocal, searchResult.first, scrollController, kDesktopFileTransferRowHeight, buffer); }, child: ObxValue( @@ -323,100 +335,118 @@ class _FileManagerPageState extends State }).toList(growable: false) : entries; final rows = filteredEntries.map((entry) { - final sizeStr = - entry.isFile ? readableFileSize(entry.size.toDouble()) : ""; - final lastModifiedStr = entry.isDrive - ? " " - : "${entry.lastModified().toString().replaceAll(".000", "")} "; + final sizeStr = + entry.isFile ? readableFileSize(entry.size.toDouble()) : ""; + final lastModifiedStr = entry.isDrive + ? " " + : "${entry.lastModified().toString().replaceAll(".000", "")} "; 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), - )), - )), - GestureDetector( - child: Tooltip( - waitDuration: Duration(milliseconds: 500), - message: sizeStr, - child: Text( - sizeStr, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 10, - color: MyTheme.darkGray), - ))), - ]), - ), + return Padding( + padding: EdgeInsets.symmetric(vertical: 1), + child: Container( + decoration: BoxDecoration( + color: isSelected + ? Theme.of(context).hoverColor + : Theme.of(context).cardColor, + borderRadius: BorderRadius.all( + Radius.circular(5.0), ), ), - ], - ), + key: ValueKey(entry.name), + height: kDesktopFileTransferRowHeight, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Expanded( + 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) + : SvgPicture.asset( + entry.isFile + ? "assets/file.svg" + : "assets/folder.svg", + color: Theme.of(context) + .tabBarTheme + .labelColor, + ), + 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); + }, + ), + Expanded( + child: GestureDetector( + child: SizedBox( + width: + kDesktopFileTransferModifiedColWidth, + child: Tooltip( + waitDuration: + Duration(milliseconds: 500), + message: lastModifiedStr, + child: Text( + lastModifiedStr, + style: TextStyle( + fontSize: 12, + color: MyTheme.darkGray, + ), + )), + ), + ), + ), + SizedBox( + width: 100, + child: GestureDetector( + child: Tooltip( + waitDuration: Duration(milliseconds: 500), + message: sizeStr, + child: Text( + sizeStr, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 10, + color: MyTheme.darkGray), + ), + ), + ), + ), + ], + ), + ), + ), + ], + )), ); }).toList(growable: false); @@ -520,98 +550,147 @@ class _FileManagerPageState extends State Widget statusList() { return PreferredSize( preferredSize: const Size(200, double.infinity), - child: Container( - margin: const EdgeInsets.only(top: 16.0, bottom: 16.0, right: 16.0), - padding: const EdgeInsets.all(8.0), - decoration: BoxDecoration(border: Border.all(color: Colors.grey)), - child: Obx( - () => ListView.builder( - controller: ScrollController(), - itemBuilder: (BuildContext context, int index) { - final item = model.jobTable[index]; - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Transform.rotate( - angle: item.isRemote ? pi : 0, - child: const Icon(Icons.send)), - const SizedBox( - width: 16.0, - ), - Expanded( + child: model.jobTable.isEmpty + ? Center(child: Text(translate("Empty"))) + : Container( + margin: + const EdgeInsets.only(top: 16.0, bottom: 16.0, right: 16.0), + padding: const EdgeInsets.all(8.0), + child: Obx( + () => ListView.builder( + controller: ScrollController(), + itemBuilder: (BuildContext context, int index) { + final item = model.jobTable[index]; + return Padding( + padding: const EdgeInsets.only(bottom: 5), + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).cardColor, + borderRadius: BorderRadius.all( + Radius.circular(8.0), + ), + ), child: Column( mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, children: [ - Tooltip( - waitDuration: Duration(milliseconds: 500), - message: item.jobName, - child: Text( - item.jobName, - maxLines: 1, - overflow: TextOverflow.ellipsis, - )), - Wrap( + Row( + crossAxisAlignment: CrossAxisAlignment.center, children: [ - Text( - '${item.display()} ${max(0, item.fileNum)}/${item.fileCount} '), - Text( - '${translate("files")} ${readableFileSize(item.totalSize.toDouble())} '), - Offstage( - offstage: - item.state != JobState.inProgress, - child: Text( - '${"${readableFileSize(item.speed)}/s"} ')), - Offstage( - offstage: item.totalSize <= 0, - child: Text( - '${(item.finishedSize.toDouble() * 100 / item.totalSize.toDouble()).toStringAsFixed(2)}%'), + Transform.rotate( + angle: item.isRemote ? pi : 0, + child: SvgPicture.asset( + "assets/arrow.svg", + color: Theme.of(context) + .tabBarTheme + .labelColor, + ), + ), + const SizedBox( + width: 16.0, + ), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Tooltip( + waitDuration: + Duration(milliseconds: 500), + message: item.jobName, + child: Text( + item.jobName, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + Wrap( + children: [ + Text( + '${item.display()} ${max(0, item.fileNum)}/${item.fileCount} '), + Text( + '${translate("files")} ${readableFileSize(item.totalSize.toDouble())} '), + Offstage( + offstage: item.state != + JobState.inProgress, + child: Text( + '${"${readableFileSize(item.speed)}/s"} '), + ), + Offstage( + offstage: item.state != + JobState.inProgress, + child: LinearPercentIndicator( + padding: EdgeInsets.all(0), + width: MediaQuery.of(context) + .size + .width * + 0.15, + animateFromLastPercent: true, + center: Text( + '${(item.finishedSize / item.totalSize * 100).toStringAsFixed(0)}%', + ), + barRadius: Radius.circular(15), + percent: item.finishedSize / + item.totalSize, + progressColor: MyTheme.accent, + backgroundColor: + Color(0xFF4C4F62), + lineHeight: + kDesktopFileTransferRowHeight, + ).paddingSymmetric(vertical: 15), + ), + ], + ), + ], + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Offstage( + offstage: item.state != JobState.paused, + child: MenuButton( + onPressed: () { + model.resumeJob(item.id); + }, + child: SvgPicture.asset( + "assets/refresh.svg", + color: Theme.of(context) + .tabBarTheme + .labelColor, + ), + color: MyTheme.accent, + hoverColor: MyTheme.accent80, + ), + ), + MenuButton( + padding: EdgeInsets.only(right: 15), + child: SvgPicture.asset( + "assets/close.svg", + color: Theme.of(context) + .tabBarTheme + .labelColor, + ), + onPressed: () { + model.jobTable.removeAt(index); + model.cancelJob(item.id); + }, + color: MyTheme.accent, + hoverColor: MyTheme.accent80, + ), + ], ), ], ), ], - ), + ).paddingSymmetric(vertical: 10), ), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Offstage( - offstage: item.state != JobState.paused, - child: IconButton( - onPressed: () { - model.resumeJob(item.id); - }, - splashRadius: kDesktopIconButtonSplashRadius, - icon: const Icon(Icons.restart_alt_rounded)), - ), - IconButton( - icon: const Icon(Icons.close), - splashRadius: 1, - onPressed: () { - model.jobTable.removeAt(index); - model.cancelJob(item.id); - }, - ), - ], - ) - ], - ), - SizedBox( - height: 8.0, - ), - Divider( - height: 2.0, - ) - ], - ); - }, - itemCount: model.jobTable.length, - ), - ), - )); + ); + }, + itemCount: model.jobTable.length, + ), + ), + )); } Widget headTools(bool isLocal) { @@ -620,95 +699,128 @@ class _FileManagerPageState extends State final locationFocus = isLocal ? _locationNodeLocal : _locationNodeRemote; final selectedItems = getSelectedItems(isLocal); return Container( - child: Column( - children: [ - // symbols - PreferredSize( - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Container( - width: 50, - height: 50, - decoration: BoxDecoration(color: Colors.blue), - padding: EdgeInsets.all(8.0), - child: FutureBuilder( - future: bind.sessionGetPlatform( - id: _ffi.id, isRemote: !isLocal), - builder: (context, snapshot) { - if (snapshot.hasData && snapshot.data!.isNotEmpty) { - return getPlatformImage('${snapshot.data}'); - } else { - return CircularProgressIndicator( - color: Colors.white, - ); - } - })), - Text(isLocal - ? translate("Local Computer") - : translate("Remote Computer")) - .marginOnly(left: 8.0) - ], - ), - preferredSize: Size(double.infinity, 70)), - // buttons - Row( - children: [ - Row( - children: [ - IconButton( - icon: const Icon(Icons.arrow_back), - splashRadius: kDesktopIconButtonSplashRadius, - onPressed: () { - selectedItems.clear(); - model.goBack(isLocal: isLocal); - }, - ), - IconButton( - icon: const Icon(Icons.arrow_upward), - splashRadius: kDesktopIconButtonSplashRadius, - onPressed: () { - selectedItems.clear(); - model.goToParentDirectory(isLocal: isLocal); - }, - ), - ], - ), - Expanded( - child: GestureDetector( - onTap: () { - locationStatus.value = - locationStatus.value == LocationStatus.bread - ? LocationStatus.pathLocation - : LocationStatus.bread; - Future.delayed(Duration.zero, () { - if (locationStatus.value == LocationStatus.pathLocation) { - locationFocus.requestFocus(); - } - }); - }, - child: Obx(() => Container( - decoration: BoxDecoration( - border: Border.all( - color: locationStatus.value == LocationStatus.bread - ? Colors.black12 - : Theme.of(context) - .colorScheme - .primary - .withOpacity(0.5))), + child: Column( + children: [ + // symbols + PreferredSize( child: Row( + crossAxisAlignment: CrossAxisAlignment.center, children: [ - Expanded( - child: locationStatus.value == LocationStatus.bread - ? buildBread(isLocal) - : buildPathLocation(isLocal)), + Container( + width: 50, + height: 50, + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(8)), + color: MyTheme.accent, + ), + padding: EdgeInsets.all(8.0), + child: FutureBuilder( + future: bind.sessionGetPlatform( + id: _ffi.id, isRemote: !isLocal), + builder: (context, snapshot) { + if (snapshot.hasData && + snapshot.data!.isNotEmpty) { + return getPlatformImage('${snapshot.data}'); + } else { + return CircularProgressIndicator( + color: Theme.of(context) + .tabBarTheme + .labelColor, + ); + } + })), + Text(isLocal + ? translate("Local Computer") + : translate("Remote Computer")) + .marginOnly(left: 8.0) ], - ))), - )), - Obx(() { - switch (locationStatus.value) { - case LocationStatus.bread: - return IconButton( + ), + preferredSize: Size(double.infinity, 70)) + .paddingOnly(bottom: 15), + // buttons + Row( + children: [ + Row( + children: [ + MenuButton( + padding: EdgeInsets.only( + right: 3, + ), + child: SvgPicture.asset( + "assets/arrow.svg", + color: Theme.of(context).tabBarTheme.labelColor, + ), + color: Theme.of(context).cardColor, + hoverColor: Theme.of(context).hoverColor, + onPressed: () { + selectedItems.clear(); + model.goBack(isLocal: isLocal); + }, + ), + MenuButton( + child: RotatedBox( + quarterTurns: 3, + child: SvgPicture.asset( + "assets/arrow.svg", + color: Theme.of(context).tabBarTheme.labelColor, + ), + ), + color: Theme.of(context).cardColor, + hoverColor: Theme.of(context).hoverColor, + onPressed: () { + selectedItems.clear(); + model.goToParentDirectory(isLocal: isLocal); + }, + ), + ], + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 3.0), + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).cardColor, + borderRadius: BorderRadius.all( + Radius.circular(8.0), + ), + ), + child: Padding( + padding: EdgeInsets.symmetric(vertical: 2.5), + child: GestureDetector( + onTap: () { + locationStatus.value = + locationStatus.value == LocationStatus.bread + ? LocationStatus.pathLocation + : LocationStatus.bread; + Future.delayed(Duration.zero, () { + if (locationStatus.value == + LocationStatus.pathLocation) { + locationFocus.requestFocus(); + } + }); + }, + child: Obx( + () => Container( + child: Row( + children: [ + Expanded( + child: locationStatus.value == + LocationStatus.bread + ? buildBread(isLocal) + : buildPathLocation(isLocal)), + ], + ), + ), + ), + ), + ), + ), + ), + ), + Obx(() { + switch (locationStatus.value) { + case LocationStatus.bread: + return MenuButton( onPressed: () { locationStatus.value = LocationStatus.fileSearchBar; final focusNode = @@ -716,49 +828,77 @@ class _FileManagerPageState extends State Future.delayed( Duration.zero, () => focusNode.requestFocus()); }, - splashRadius: kDesktopIconButtonSplashRadius, - icon: Icon(Icons.search)); - case LocationStatus.pathLocation: - return IconButton( - color: Theme.of(context).disabledColor, + child: SvgPicture.asset( + "assets/search.svg", + color: Theme.of(context).tabBarTheme.labelColor, + ), + color: Theme.of(context).cardColor, + hoverColor: Theme.of(context).hoverColor, + ); + case LocationStatus.pathLocation: + return MenuButton( onPressed: null, - splashRadius: kDesktopIconButtonSplashRadius, - icon: Icon(Icons.close)); - case LocationStatus.fileSearchBar: - return IconButton( + child: SvgPicture.asset( + "assets/close.svg", + color: Theme.of(context).tabBarTheme.labelColor, + ), color: Theme.of(context).disabledColor, + hoverColor: Theme.of(context).hoverColor, + ); + case LocationStatus.fileSearchBar: + return MenuButton( onPressed: () { onSearchText("", isLocal); locationStatus.value = LocationStatus.bread; }, - splashRadius: 1, - icon: Icon(Icons.close)); - } - }), - IconButton( + child: SvgPicture.asset( + "assets/close.svg", + color: Theme.of(context).tabBarTheme.labelColor, + ), + color: Theme.of(context).cardColor, + hoverColor: Theme.of(context).hoverColor, + ); + } + }), + MenuButton( + padding: EdgeInsets.only( + left: 3, + ), onPressed: () { model.refresh(isLocal: isLocal); }, - splashRadius: kDesktopIconButtonSplashRadius, - icon: const Icon(Icons.refresh)), - ], - ), - Row( - textDirection: isLocal ? TextDirection.ltr : TextDirection.rtl, - children: [ - Expanded( - child: Row( - mainAxisAlignment: - isLocal ? MainAxisAlignment.start : MainAxisAlignment.end, - children: [ - IconButton( - onPressed: () { - model.goHome(isLocal: isLocal); - }, - icon: const Icon(Icons.home_outlined), - splashRadius: kDesktopIconButtonSplashRadius, - ), - IconButton( + child: SvgPicture.asset( + "assets/refresh.svg", + color: Theme.of(context).tabBarTheme.labelColor, + ), + color: Theme.of(context).cardColor, + hoverColor: Theme.of(context).hoverColor, + ), + ], + ), + Row( + textDirection: isLocal ? TextDirection.ltr : TextDirection.rtl, + children: [ + Expanded( + child: Row( + mainAxisAlignment: + isLocal ? MainAxisAlignment.start : MainAxisAlignment.end, + children: [ + MenuButton( + padding: EdgeInsets.only( + right: 3, + ), + onPressed: () { + model.goHome(isLocal: isLocal); + }, + child: SvgPicture.asset( + "assets/home.svg", + color: Theme.of(context).tabBarTheme.labelColor, + ), + color: Theme.of(context).cardColor, + hoverColor: Theme.of(context).hoverColor, + ), + MenuButton( onPressed: () { final name = TextEditingController(); _ffi.dialogManager.show((setState, close) { @@ -800,9 +940,14 @@ class _FileManagerPageState extends State ); }); }, - splashRadius: kDesktopIconButtonSplashRadius, - icon: const Icon(Icons.create_new_folder_outlined)), - IconButton( + child: SvgPicture.asset( + "assets/folder_new.svg", + color: Theme.of(context).tabBarTheme.labelColor, + ), + color: Theme.of(context).cardColor, + hoverColor: Theme.of(context).hoverColor, + ), + MenuButton( onPressed: validItems(selectedItems) ? () async { await (model.removeAction(selectedItems, @@ -810,32 +955,80 @@ class _FileManagerPageState extends State selectedItems.clear(); } : null, - splashRadius: kDesktopIconButtonSplashRadius, - icon: const Icon(Icons.delete_forever_outlined)), - menu(isLocal: isLocal), - ], + child: SvgPicture.asset( + "assets/trash.svg", + color: Theme.of(context).tabBarTheme.labelColor, + ), + color: Theme.of(context).cardColor, + hoverColor: Theme.of(context).hoverColor, + ), + menu(isLocal: isLocal), + ], + ), ), - ), - TextButton.icon( + ElevatedButton.icon( + style: ButtonStyle( + padding: MaterialStateProperty.all(isLocal + ? EdgeInsets.only(left: 10) + : EdgeInsets.only(right: 10)), + backgroundColor: MaterialStateProperty.all( + selectedItems.length == 0 + ? MyTheme.accent80 + : MyTheme.accent, + ), + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(18.0), + ), + ), + ), onPressed: validItems(selectedItems) ? () { model.sendFiles(selectedItems, isRemote: !isLocal); selectedItems.clear(); } : null, - icon: Transform.rotate( - angle: isLocal ? 0 : pi, - child: const Icon( - Icons.send, - ), - ), - label: Text( - isLocal ? translate('Send') : translate('Receive'), - )), - ], - ).marginOnly(top: 8.0) - ], - )); + icon: isLocal + ? Text( + translate('Send'), + textAlign: TextAlign.right, + style: TextStyle( + color: selectedItems.length == 0 + ? MyTheme.darkGray + : Colors.white, + ), + ) + : RotatedBox( + quarterTurns: 2, + child: SvgPicture.asset( + "assets/arrow.svg", + color: selectedItems.length == 0 + ? MyTheme.darkGray + : Colors.white, + alignment: Alignment.bottomRight, + ), + ), + label: isLocal + ? SvgPicture.asset( + "assets/arrow.svg", + color: selectedItems.length == 0 + ? MyTheme.darkGray + : Colors.white, + ) + : Text( + translate('Receive'), + style: TextStyle( + color: selectedItems.length == 0 + ? MyTheme.darkGray + : Colors.white, + ), + ), + ), + ], + ).marginOnly(top: 8.0) + ], + ), + ); } bool validItems(SelectedItems items) { @@ -890,25 +1083,27 @@ class _FileManagerPageState extends State mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( - child: Listener( - // handle mouse wheel - onPointerSignal: (e) { - if (e is PointerScrollEvent) { - final sc = getBreadCrumbScrollController(isLocal); - final scale = Platform.isWindows ? 2 : 4; - sc.jumpTo(sc.offset + e.scrollDelta.dy / scale); - } - }, - child: BreadCrumb( - items: items, - divider: Icon(Icons.chevron_right), - overflow: ScrollableOverflow( - controller: - getBreadCrumbScrollController(isLocal)), - ))), + child: Listener( + // handle mouse wheel + onPointerSignal: (e) { + if (e is PointerScrollEvent) { + final sc = getBreadCrumbScrollController(isLocal); + final scale = Platform.isWindows ? 2 : 4; + sc.jumpTo(sc.offset + e.scrollDelta.dy / scale); + } + }, + child: BreadCrumb( + items: items, + divider: const Icon(Icons.keyboard_arrow_right_rounded), + overflow: ScrollableOverflow( + controller: getBreadCrumbScrollController(isLocal), + ), + ), + ), + ), ActionIcon( message: "", - icon: Icons.arrow_drop_down, + icon: Icons.keyboard_arrow_down_rounded, onTap: () async { final renderBox = locationBarKey.currentContext ?.findRenderObject() as RenderBox; @@ -1021,13 +1216,23 @@ class _FileManagerPageState extends State .marginSymmetric(horizontal: 4))); } else { final list = PathUtil.split(path, isWindows); - breadCrumbList.addAll(list.asMap().entries.map((e) => BreadCrumbItem( - content: TextButton( + breadCrumbList.addAll( + list.asMap().entries.map( + (e) => BreadCrumbItem( + content: TextButton( child: Text(e.value), style: ButtonStyle( - minimumSize: MaterialStateProperty.all(Size(0, 0))), - onPressed: () => onPressed(list.sublist(0, e.key + 1))) - .marginSymmetric(horizontal: 4)))); + minimumSize: MaterialStateProperty.all( + Size(0, 0), + ), + ), + onPressed: () => onPressed( + list.sublist(0, e.key + 1), + ), + ).marginSymmetric(horizontal: 4), + ), + ), + ); } return breadCrumbList; } @@ -1054,29 +1259,35 @@ class _FileManagerPageState extends State : searchTextObs.value; final textController = TextEditingController(text: text) ..selection = TextSelection.collapsed(offset: text.length); - return Row(children: [ - Icon( - locationStatus.value == LocationStatus.pathLocation - ? Icons.folder - : Icons.search, - color: Theme.of(context).hintColor, - ).paddingSymmetric(horizontal: 2), - Expanded( + return Row( + children: [ + SvgPicture.asset( + locationStatus.value == LocationStatus.pathLocation + ? "assets/folder.svg" + : "assets/search.svg", + color: Theme.of(context).tabBarTheme.labelColor, + ), + Expanded( child: TextField( - focusNode: focusNode, - decoration: InputDecoration( - border: InputBorder.none, - isDense: true, - prefix: Padding(padding: EdgeInsets.only(left: 4.0))), - controller: textController, - onSubmitted: (path) { - openDirectory(path, isLocal: isLocal); - }, - onChanged: locationStatus.value == LocationStatus.fileSearchBar - ? (searchText) => onSearchText(searchText, isLocal) - : null, - )) - ]); + focusNode: focusNode, + decoration: InputDecoration( + border: InputBorder.none, + isDense: true, + prefix: Padding( + padding: EdgeInsets.only(left: 4.0), + ), + ), + controller: textController, + onSubmitted: (path) { + openDirectory(path, isLocal: isLocal); + }, + onChanged: locationStatus.value == LocationStatus.fileSearchBar + ? (searchText) => onSearchText(searchText, isLocal) + : null, + ), + ) + ], + ); } onSearchText(String searchText, bool isLocal) { @@ -1145,12 +1356,13 @@ class _FileManagerPageState extends State Text( name, style: headerTextStyle, - ).marginSymmetric( - horizontal: sortBy == SortBy.name ? 4 : 0.0), + ).marginSymmetric(horizontal: 4), ascending.value != null - ? Icon(ascending.value! - ? Icons.arrow_upward - : Icons.arrow_downward) + ? Icon( + ascending.value! + ? Icons.keyboard_arrow_up_rounded + : Icons.keyboard_arrow_down_rounded, + ) : const Offstage() ], ), diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index 7540f7662..bbe2b28be 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -86,18 +86,14 @@ class _FileManagerTabPageState extends State { @override Widget build(BuildContext context) { - final tabWidget = Container( - decoration: BoxDecoration( - border: Border.all(color: MyTheme.color(context).border!)), - child: Scaffold( - backgroundColor: Theme.of(context).backgroundColor, - body: DesktopTab( - controller: tabController, - onWindowCloseButton: handleWindowCloseButton, - tail: const AddButton().paddingOnly(left: 10), - labelGetter: DesktopTab.labelGetterAlias, - )), - ); + final tabWidget = Scaffold( + backgroundColor: Theme.of(context).cardColor, + body: DesktopTab( + controller: tabController, + onWindowCloseButton: handleWindowCloseButton, + tail: const AddButton().paddingOnly(left: 10), + labelGetter: DesktopTab.labelGetterAlias, + )); return Platform.isMacOS ? tabWidget : SubWindowDragToResizeArea( diff --git a/flutter/lib/desktop/widgets/menu_button.dart b/flutter/lib/desktop/widgets/menu_button.dart index 96cc9fa9b..df2c48ab4 100644 --- a/flutter/lib/desktop/widgets/menu_button.dart +++ b/flutter/lib/desktop/widgets/menu_button.dart @@ -27,6 +27,7 @@ class MenuButton extends StatefulWidget { class _MenuButtonState extends State { bool _isHover = false; + final double _borderRadius = 8.0; @override Widget build(BuildContext context) { @@ -38,16 +39,17 @@ class _MenuButtonState extends State { type: MaterialType.transparency, child: Ink( decoration: BoxDecoration( - borderRadius: BorderRadius.circular(5), + borderRadius: BorderRadius.circular(_borderRadius), color: _isHover ? widget.hoverColor : widget.color, ), child: InkWell( + hoverColor: widget.hoverColor, onHover: (val) { setState(() { _isHover = val; }); }, - borderRadius: BorderRadius.circular(5), + borderRadius: BorderRadius.circular(_borderRadius), splashColor: widget.splashColor, enableFeedback: widget.enableFeedback, onTap: widget.onPressed, diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 91a061fb9..64c44a555 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -970,6 +970,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.11.1" + percent_indicator: + dependency: "direct main" + description: + name: percent_indicator + sha256: cec41f67181fbd5322aa68b355621d1a4eea827426b8eeb613f6cbe195ff7b4a + url: "https://pub.dev" + source: hosted + version: "4.2.2" petitparser: dependency: transitive description: @@ -1547,5 +1555,5 @@ packages: source: hosted version: "0.1.1" sdks: - dart: ">=2.18.0 <4.0.0" + dart: ">=2.18.0 <3.0.0" flutter: ">=3.3.0" diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index df29252c9..7789f92f4 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -92,6 +92,7 @@ dependencies: password_strength: ^0.2.0 flutter_launcher_icons: ^0.11.0 flutter_keyboard_visibility: ^5.4.0 + percent_indicator: ^4.2.2 dev_dependencies: From b5ca85fb9b8566f80ce8f2d76c387b10634becdc Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Wed, 22 Feb 2023 22:44:06 +0100 Subject: [PATCH 1963/2015] fix colors in light theme --- .../lib/desktop/pages/file_manager_page.dart | 93 ++++++++++--------- 1 file changed, 51 insertions(+), 42 deletions(-) diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 262121f3d..d42a28292 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -567,7 +567,7 @@ class _FileManagerPageState extends State decoration: BoxDecoration( color: Theme.of(context).cardColor, borderRadius: BorderRadius.all( - Radius.circular(8.0), + Radius.circular(15.0), ), ), child: Column( @@ -584,7 +584,7 @@ class _FileManagerPageState extends State .tabBarTheme .labelColor, ), - ), + ).paddingOnly(left: 15), const SizedBox( width: 16.0, ), @@ -602,44 +602,57 @@ class _FileManagerPageState extends State item.jobName, maxLines: 1, overflow: TextOverflow.ellipsis, + ).paddingSymmetric(vertical: 10), + ), + Text( + '${translate("Total")} ${readableFileSize(item.totalSize.toDouble())}', + style: TextStyle( + fontSize: 12, + color: MyTheme.darkGray, ), ), - Wrap( - children: [ - Text( - '${item.display()} ${max(0, item.fileNum)}/${item.fileCount} '), - Text( - '${translate("files")} ${readableFileSize(item.totalSize.toDouble())} '), - Offstage( - offstage: item.state != - JobState.inProgress, - child: Text( - '${"${readableFileSize(item.speed)}/s"} '), + Offstage( + offstage: + item.state != JobState.inProgress, + child: Text( + '${translate("Speed")} ${readableFileSize(item.speed)}/s', + style: TextStyle( + fontSize: 12, + color: MyTheme.darkGray, ), - Offstage( - offstage: item.state != - JobState.inProgress, - child: LinearPercentIndicator( - padding: EdgeInsets.all(0), - width: MediaQuery.of(context) - .size - .width * - 0.15, - animateFromLastPercent: true, - center: Text( - '${(item.finishedSize / item.totalSize * 100).toStringAsFixed(0)}%', - ), - barRadius: Radius.circular(15), - percent: item.finishedSize / - item.totalSize, - progressColor: MyTheme.accent, - backgroundColor: - Color(0xFF4C4F62), - lineHeight: - kDesktopFileTransferRowHeight, - ).paddingSymmetric(vertical: 15), + ), + ), + Offstage( + offstage: + item.state == JobState.inProgress, + child: Text( + translate( + item.display(), ), - ], + style: TextStyle( + fontSize: 12, + color: MyTheme.darkGray, + ), + ), + ), + Offstage( + offstage: + item.state != JobState.inProgress, + child: LinearPercentIndicator( + padding: EdgeInsets.only(right: 15), + animateFromLastPercent: true, + center: Text( + '${(item.finishedSize / item.totalSize * 100).toStringAsFixed(0)}%', + ), + barRadius: Radius.circular(15), + percent: item.finishedSize / + item.totalSize, + progressColor: MyTheme.accent, + backgroundColor: + Theme.of(context).hoverColor, + lineHeight: + kDesktopFileTransferRowHeight, + ).paddingSymmetric(vertical: 15), ), ], ), @@ -655,9 +668,7 @@ class _FileManagerPageState extends State }, child: SvgPicture.asset( "assets/refresh.svg", - color: Theme.of(context) - .tabBarTheme - .labelColor, + color: Colors.white, ), color: MyTheme.accent, hoverColor: MyTheme.accent80, @@ -667,9 +678,7 @@ class _FileManagerPageState extends State padding: EdgeInsets.only(right: 15), child: SvgPicture.asset( "assets/close.svg", - color: Theme.of(context) - .tabBarTheme - .labelColor, + color: Colors.white, ), onPressed: () { model.jobTable.removeAt(index); From 85a82a6ba74d1fe2d276a7bf756783d275f229c7 Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Wed, 22 Feb 2023 22:47:09 +0100 Subject: [PATCH 1964/2015] added svgs --- flutter/assets/arrow.svg | 2 ++ flutter/assets/dots.svg | 2 ++ flutter/assets/file.svg | 2 ++ flutter/assets/folder.svg | 2 ++ flutter/assets/folder_new.svg | 2 ++ flutter/assets/home.svg | 2 ++ flutter/assets/refresh.svg | 2 ++ flutter/assets/search.svg | 2 ++ flutter/assets/trash.svg | 2 ++ 9 files changed, 18 insertions(+) create mode 100644 flutter/assets/arrow.svg create mode 100644 flutter/assets/dots.svg create mode 100644 flutter/assets/file.svg create mode 100644 flutter/assets/folder.svg create mode 100644 flutter/assets/folder_new.svg create mode 100644 flutter/assets/home.svg create mode 100644 flutter/assets/refresh.svg create mode 100644 flutter/assets/search.svg create mode 100644 flutter/assets/trash.svg diff --git a/flutter/assets/arrow.svg b/flutter/assets/arrow.svg new file mode 100644 index 000000000..d0f032bc2 --- /dev/null +++ b/flutter/assets/arrow.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/flutter/assets/dots.svg b/flutter/assets/dots.svg new file mode 100644 index 000000000..19563b849 --- /dev/null +++ b/flutter/assets/dots.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/flutter/assets/file.svg b/flutter/assets/file.svg new file mode 100644 index 000000000..21c7fb9de --- /dev/null +++ b/flutter/assets/file.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/flutter/assets/folder.svg b/flutter/assets/folder.svg new file mode 100644 index 000000000..3959f7874 --- /dev/null +++ b/flutter/assets/folder.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/flutter/assets/folder_new.svg b/flutter/assets/folder_new.svg new file mode 100644 index 000000000..22b729204 --- /dev/null +++ b/flutter/assets/folder_new.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/flutter/assets/home.svg b/flutter/assets/home.svg new file mode 100644 index 000000000..45a018f5d --- /dev/null +++ b/flutter/assets/home.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/flutter/assets/refresh.svg b/flutter/assets/refresh.svg new file mode 100644 index 000000000..f77fcfd4c --- /dev/null +++ b/flutter/assets/refresh.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/flutter/assets/search.svg b/flutter/assets/search.svg new file mode 100644 index 000000000..295136d7e --- /dev/null +++ b/flutter/assets/search.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/flutter/assets/trash.svg b/flutter/assets/trash.svg new file mode 100644 index 000000000..f9037e0e1 --- /dev/null +++ b/flutter/assets/trash.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file From 922a70adb45ae15a16376dd096a0fbda48c4fdb8 Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Wed, 22 Feb 2023 22:52:29 +0100 Subject: [PATCH 1965/2015] removed filesize expanded --- .../lib/desktop/pages/file_manager_page.dart | 31 +++++++++---------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index d42a28292..0d55552af 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -406,23 +406,20 @@ class _FileManagerPageState extends State items, filteredEntries, entry, isLocal); }, ), - Expanded( - child: GestureDetector( - child: SizedBox( - width: - kDesktopFileTransferModifiedColWidth, - child: Tooltip( - waitDuration: - Duration(milliseconds: 500), - message: lastModifiedStr, - child: Text( - lastModifiedStr, - style: TextStyle( - fontSize: 12, - color: MyTheme.darkGray, - ), - )), - ), + GestureDetector( + child: SizedBox( + width: kDesktopFileTransferModifiedColWidth, + child: Tooltip( + waitDuration: + Duration(milliseconds: 500), + message: lastModifiedStr, + child: Text( + lastModifiedStr, + style: TextStyle( + fontSize: 12, + color: MyTheme.darkGray, + ), + )), ), ), SizedBox( From 12a33cdfbb6b3161afce023d0675c3928dcded7c Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Wed, 22 Feb 2023 23:01:31 +0100 Subject: [PATCH 1966/2015] Merge remote-tracking branch 'upstream/master' into file-manager-redesign --- .devcontainer/Dockerfile | 53 +++- .devcontainer/build.sh | 75 +++++ .devcontainer/devcontainer.json | 21 +- .devcontainer/setup.sh | 23 ++ .github/ISSUE_TEMPLATE/bug_report.yaml | 4 +- .github/workflows/ci.yml | 5 + .github/workflows/flutter-ci.yml | 5 + Cargo.lock | 2 +- Cargo.toml | 1 - README.md | 10 +- docs/DEVCONTAINER.md | 14 + flutter/build_android.sh | 10 +- flutter/lib/common/widgets/address_book.dart | 17 +- flutter/lib/common/widgets/remote_input.dart | 11 +- flutter/lib/consts.dart | 1 + .../lib/desktop/pages/desktop_home_page.dart | 5 + .../lib/desktop/pages/remote_tab_page.dart | 2 + .../lib/desktop/widgets/remote_menubar.dart | 6 + .../lib/desktop/widgets/tabbar_widget.dart | 15 +- flutter/lib/models/input_model.dart | 4 + flutter/pubspec.lock | 12 +- flutter/pubspec.yaml | 265 +++++++++--------- libs/hbb_common/Cargo.toml | 1 + libs/hbb_common/src/lib.rs | 1 + libs/scrap/src/common/hwcodec.rs | 38 ++- res/rustdesk.desktop | 4 +- src/flutter_ffi.rs | 9 +- src/ipc.rs | 2 +- src/lang/cn.rs | 146 +++++----- src/lang/fa.rs | 6 +- src/platform/macos.rs | 2 +- 31 files changed, 488 insertions(+), 282 deletions(-) create mode 100755 .devcontainer/build.sh create mode 100755 .devcontainer/setup.sh create mode 100644 docs/DEVCONTAINER.md diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 0381ff966..32a440b28 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,19 +1,50 @@ -FROM debian +FROM mcr.microsoft.com/devcontainers/base:ubuntu-22.04 +ENV HOME=/home/vscode +ENV WORKDIR=$HOME/rustdesk + +WORKDIR $HOME +RUN sudo apt update -y && 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 unzip zip sudo libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev +WORKDIR / + +RUN git clone https://github.com/microsoft/vcpkg +WORKDIR vcpkg +RUN git checkout 134505003bb46e20fbace51ccfb69243fbbc5f82 +RUN /vcpkg/bootstrap-vcpkg.sh -disableMetrics +ENV VCPKG_ROOT=/vcpkg +RUN $VCPKG_ROOT/vcpkg --disable-metrics install libvpx libyuv opus WORKDIR / -RUN apt update -y && 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 unzip zip sudo libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev +RUN wget https://github.com/rustdesk/doc.rustdesk.com/releases/download/console/dep.tar.gz && tar xzf dep.tar.gz -RUN git clone https://github.com/microsoft/vcpkg && cd vcpkg && git checkout 134505003bb46e20fbace51ccfb69243fbbc5f82 -RUN /vcpkg/bootstrap-vcpkg.sh -disableMetrics -RUN /vcpkg/vcpkg --disable-metrics install libvpx libyuv opus -RUN groupadd -r user && useradd -r -g user user --home /home/user && mkdir -p /home/user && chown user /home/user && echo "user ALL=(ALL) NOPASSWD:ALL" | sudo tee /etc/sudoers.d/user -WORKDIR /home/user +USER vscode +WORKDIR $HOME RUN wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so -USER user RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > rustup.sh RUN chmod +x rustup.sh -RUN ./rustup.sh -y +RUN $HOME/rustup.sh -y +RUN $HOME/.cargo/bin/rustup target add aarch64-linux-android +RUN $HOME/.cargo/bin/cargo install cargo-ndk -USER root -ENV HOME=/home/user +# Install Flutter +RUN wget https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.7.3-stable.tar.xz +RUN tar xf flutter_linux_3.7.3-stable.tar.xz && rm flutter_linux_3.7.3-stable.tar.xz +ENV PATH="$PATH:$HOME/flutter/bin" +RUN dart pub global activate ffigen 5.0.1 + + +# Install packages +RUN sudo apt-get install -y libclang-dev +RUN sudo apt install -y gcc-multilib + +WORKDIR $WORKDIR +ENV ANDROID_NDK_HOME=/opt/android/ndk/22.1.7171670 + +# Somehow try to automate flutter pub get +# https://rustdesk.com/docs/en/dev/build/android/ +# Put below steps in entrypoint.sh +# cd flutter +# wget https://github.com/rustdesk/doc.rustdesk.com/releases/download/console/so.tar.gz +# tar xzf so.tar.gz + +# own /opt/android diff --git a/.devcontainer/build.sh b/.devcontainer/build.sh new file mode 100755 index 000000000..df87aace7 --- /dev/null +++ b/.devcontainer/build.sh @@ -0,0 +1,75 @@ +#!/bin/bash + +set -e + +MODE=${1:---debug} +TYPE=${2:-linux} +MODE=${MODE/*-/} + + +build(){ + pwd + $WORKDIR/entrypoint $1 +} + +build_arm64(){ + CWD=$(pwd) + cd $WORKDIR/flutter + flutter pub get + cd $WORKDIR + $WORKDIR/flutter/ndk_arm64.sh + cp $WORKDIR/target/aarch64-linux-android/release/liblibrustdesk.so $WORKDIR/flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so + cd $CWD +} + +build_apk(){ + cd $WORKDIR/flutter + MODE=$1 $WORKDIR/flutter/build_android.sh + cd $WORKDIR +} + +key_gen(){ + if [ ! -f $WORKDIR/flutter/android/key.properties ] + then + if [ ! -f $HOME/upload-keystore.jks ] + then + $WORKDIR/.devcontainer/setup.sh key + fi + read -r -p "enter the password used to generate $HOME/upload-keystore.jks\n" password + echo -e "storePassword=${password}\nkeyPassword=${password}\nkeyAlias=upload\nstoreFile=$HOME/upload-keystore.jks" > $WORKDIR/flutter/android/key.properties + else + echo "Believing storeFile is created ref: $WORKDIR/flutter/android/key.properties" + fi +} + +android_build(){ + if [ ! -d $WORKDIR/flutter/android/app/src/main/jniLibs/arm64-v8a ] + then + $WORKDIR/.devcontainer/setup.sh android + fi + build_arm64 + case $1 in + debug) + build_apk debug + ;; + release) + key_gen + build_apk release + ;; + esac +} + +case "$MODE:$TYPE" in + "debug:linux") + build + ;; + "release:linux") + build --release + ;; + "debug:android") + android_build debug + ;; + "release:android") + android_build release + ;; +esac diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 24ba9a915..cd82c75e3 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,15 +1,18 @@ { "name": "rustdesk", "build": { - "dockerfile": "Dockerfile", - "args": { - "BUILDKIT_INLINE_CACHE": "0" + "dockerfile": "./Dockerfile", + "context": "." + }, + "workspaceMount": "source=${localWorkspaceFolder},target=/home/vscode/rustdesk,type=bind,consistency=cache", + "workspaceFolder": "/home/vscode/rustdesk", + "postStartCommand": ".devcontainer/build.sh", + "features": { + "ghcr.io/devcontainers/features/java:1": {}, + "ghcr.io/akhildevelops/devcontainer-features/android-cli:latest": { + "PACKAGES": "platform-tools,ndk;22.1.7171670" } }, - "workspaceMount": "source=${localWorkspaceFolder},target=/home/user/rustdesk,type=bind,consistency=cache", - "workspaceFolder": "/home/user/rustdesk", - "postStartCommand": "./entrypoint", - "remoteUser": "user", "customizations": { "vscode": { "extensions": [ @@ -17,7 +20,9 @@ "mutantdino.resourcemonitor", "rust-lang.rust-analyzer", "tamasfe.even-better-toml", - "serayuzgur.crates" + "serayuzgur.crates", + "mhutchie.git-graph", + "eamodio.gitlens" ], "settings": { "files.watcherExclude": { diff --git a/.devcontainer/setup.sh b/.devcontainer/setup.sh new file mode 100755 index 000000000..c972f47b2 --- /dev/null +++ b/.devcontainer/setup.sh @@ -0,0 +1,23 @@ +#!/bin/bash +set -e +case $1 in + android) + # install deps + cd $WORKDIR/flutter + flutter pub get + wget https://github.com/rustdesk/doc.rustdesk.com/releases/download/console/so.tar.gz + tar xzf so.tar.gz + rm so.tar.gz + sudo chown -R $(whoami) $ANDROID_HOME + echo "Setup is Done." + ;; + linux) + echo "Linux Setup" + ;; + key) + echo -e "\n$HOME/upload-keystore.jks is not created.\nLet's create it.\nRemember the password you enter in keytool!" + keytool -genkey -v -keystore $HOME/upload-keystore.jks -keyalg RSA -keysize 2048 -validity 10000 -alias upload + ;; +esac + + \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index ec23aa7a9..fea1a3672 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -44,7 +44,9 @@ body: id: screenshots attributes: label: Screenshots - description: If applicable, please add screenshots to help explain your problem + description: Please add screenshots to help explain your problem, if applicable, please upload video. + validations: + required: true - type: textarea id: context attributes: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2e1702a60..bba114315 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,6 +7,9 @@ name: CI on: workflow_dispatch: pull_request: + paths-ignore: + - "docs/**" + - "README.md" push: branches: - master @@ -14,6 +17,8 @@ on: - '*' paths-ignore: - ".github/**" + - "docs/**" + - "README.md" jobs: # ensure_cargo_fmt: diff --git a/.github/workflows/flutter-ci.yml b/.github/workflows/flutter-ci.yml index 78c60df37..2386f17dd 100644 --- a/.github/workflows/flutter-ci.yml +++ b/.github/workflows/flutter-ci.yml @@ -3,6 +3,9 @@ name: Full Flutter CI on: workflow_dispatch: pull_request: + paths-ignore: + - "docs/**" + - "README.md" push: branches: - master @@ -10,6 +13,8 @@ on: - '*' paths-ignore: - ".github/**" + - "docs/**" + - "README.md" env: LLVM_VERSION: "15.0.6" diff --git a/Cargo.lock b/Cargo.lock index 48981e169..115845b50 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2623,6 +2623,7 @@ dependencies = [ "serde_json 1.0.89", "socket2 0.3.19", "sodiumoxide", + "sysinfo", "tokio", "tokio-socks", "tokio-util", @@ -4887,7 +4888,6 @@ dependencies = [ "shutdown_hooks", "simple_rc", "sys-locale", - "sysinfo", "system_shutdown", "tao", "tray-icon", diff --git a/Cargo.toml b/Cargo.toml index 0ebe49fdf..f685e3f2e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,7 +55,6 @@ uuid = { version = "1.0", features = ["v4"] } clap = "3.0" rpassword = "7.0" base64 = "0.13" -sysinfo = "0.24" num_cpus = "1.13" bytes = { version = "1.2", features = ["serde"] } default-net = "0.12.0" diff --git a/README.md b/README.md index df0ca8328..8af79915b 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Yet another remote desktop software, written in Rust. Works out of the box, no c RustDesk welcomes contribution from everyone. See [`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md) for help getting started. -[**How does RustDesk work?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F) +[**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ) [**BINARY DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases) @@ -41,6 +41,14 @@ Below are the servers you are using for free, they may change over time. If you | USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8GB RAM | | Ukraine (Kyiv) | dc.volia (2VM) | 2 vCPU / 4GB RAM | +## Dev Container + +[![Open in Dev Containers](https://img.shields.io/static/v1?label=Dev%20Container&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/rustdesk/rustdesk) + +If you already have VS Code and Docker installed, you can click the badge above to get started. Clicking will cause VS Code to automatically install the Dev Containers extension if needed, clone the source code into a container volume, and spin up a dev container for use. + +Go through [DEVCONTAINER.md](docs/DEVCONTAINER.md) for more info. + ## Dependencies Desktop versions use [sciter](https://sciter.com/) or Flutter for GUI, this tutorial is for Sciter only. diff --git a/docs/DEVCONTAINER.md b/docs/DEVCONTAINER.md new file mode 100644 index 000000000..067e0ecf9 --- /dev/null +++ b/docs/DEVCONTAINER.md @@ -0,0 +1,14 @@ + +After the start of devcontainer in docker container, a linux binary in debug mode is created. + +Currently devcontainer offers linux and android builds in both debug and release mode. + +Below is the table on commands to run from root of the project for creating specific builds. + +Command|Build Type|Mode +-|-|-| +`.devcontainer/build.sh --debug linux`|Linux|debug +`.devcontainer/build.sh --release linux`|Linux|release +`.devcontainer/build.sh --debug android`|android-arm64|debug +`.devcontainer/build.sh --release android`|android-arm64|debug + diff --git a/flutter/build_android.sh b/flutter/build_android.sh index 01ff23488..c6b639f87 100755 --- a/flutter/build_android.sh +++ b/flutter/build_android.sh @@ -1,8 +1,10 @@ #!/usr/bin/env bash -$ANDROID_NDK/toolchains/aarch64-linux-android-4.9/prebuilt/darwin-x86_64/bin/aarch64-linux-android-strip android/app/src/main/jniLibs/arm64-v8a/* -flutter build apk --target-platform android-arm64,android-arm --release --obfuscate --split-debug-info ./split-debug-info -flutter build apk ---split-per-abi --target-platform android-arm64,android-arm --release --obfuscate --split-debug-info ./split-debug-info -flutter build appbundle --target-platform android-arm64,android-arm --release --obfuscate --split-debug-info ./split-debug-info + +MODE=${MODE:=release} +$ANDROID_NDK_HOME/toolchains/aarch64-linux-android-4.9/prebuilt/linux-x86_64/bin/aarch64-linux-android-strip android/app/src/main/jniLibs/arm64-v8a/* +flutter build apk --target-platform android-arm64,android-arm --${MODE} --obfuscate --split-debug-info ./split-debug-info +flutter build apk --split-per-abi --target-platform android-arm64,android-arm --${MODE} --obfuscate --split-debug-info ./split-debug-info +flutter build appbundle --target-platform android-arm64,android-arm --${MODE} --obfuscate --split-debug-info ./split-debug-info # build in linux # $ANDROID_NDK/toolchains/aarch64-linux-android-4.9/prebuilt/linux-x86_64/bin/aarch64-linux-android-strip android/app/src/main/jniLibs/arm64-v8a/* diff --git a/flutter/lib/common/widgets/address_book.dart b/flutter/lib/common/widgets/address_book.dart index bd2a01296..88a5aaaa3 100644 --- a/flutter/lib/common/widgets/address_book.dart +++ b/flutter/lib/common/widgets/address_book.dart @@ -43,11 +43,8 @@ class _AddressBookState extends State { return Obx(() { if (gFFI.userModel.userName.value.isEmpty) { return Center( - child: ElevatedButton( - onPressed: loginDialog, - child: Text(translate("Login")) - ) - ); + child: ElevatedButton( + onPressed: loginDialog, child: Text(translate("Login")))); } else { if (gFFI.abModel.abLoading.value) { return const Center( @@ -153,13 +150,13 @@ class _AddressBookState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(translate('Tags')), - GestureDetector( - onTapDown: (e) { - final x = e.globalPosition.dx; - final y = e.globalPosition.dy; + Listener( + onPointerDown: (e) { + final x = e.position.dx; + final y = e.position.dy; menuPos = RelativeRect.fromLTRB(x, y, x, y); }, - onTap: () => _showMenu(menuPos), + onPointerUp: (_) => _showMenu(menuPos), child: ActionMore()), ], ); diff --git a/flutter/lib/common/widgets/remote_input.dart b/flutter/lib/common/widgets/remote_input.dart index 5833e760d..dd39cbdfd 100644 --- a/flutter/lib/common/widgets/remote_input.dart +++ b/flutter/lib/common/widgets/remote_input.dart @@ -1,8 +1,6 @@ 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 { @@ -20,13 +18,6 @@ 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( @@ -34,7 +25,7 @@ class RawKeyFocusScope extends StatelessWidget { canRequestFocus: true, focusNode: focusNode, onFocusChange: onFocusChange, - onKey: onKey, + onKey: inputModel.handleRawKeyEvent, child: child)); } } diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 22ba221a9..2b73182fd 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -20,6 +20,7 @@ const String kAppTypeDesktopPortForward = "port forward"; const String kWindowMainWindowOnTop = "main_window_on_top"; const String kWindowGetWindowInfo = "get_window_info"; +const String kWindowDisableGrabKeyboard = "disable_grab_keyboard"; const String kWindowActionRebuild = "rebuild"; const String kWindowEventHide = "hide"; const String kWindowEventShow = "show"; diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index b5cadbcdf..ff99c9dc8 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -14,6 +14,7 @@ import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart'; import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/server_model.dart'; +import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; @@ -498,6 +499,10 @@ class _DesktopHomePageState extends State if (watchIsInputMonitoring) { if (bind.mainIsCanInputMonitoring(prompt: false)) { watchIsInputMonitoring = false; + // Do not notify for now. + // Monitoring may not take effect until the process is restarted. + // rustDeskWinManager.call( + // WindowType.RemoteDesktop, kWindowDisableGrabKeyboard, ''); setState(() {}); } } diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index 64c78f24d..ef3a0dd04 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -111,6 +111,8 @@ class _ConnectionTabPageState extends State { forceRelay: args['forceRelay'], ), )); + } else if (call.method == kWindowDisableGrabKeyboard) { + stateGlobal.grabKeyboard = false; } else if (call.method == "onDestroy") { tabController.clear(); } else if (call.method == kWindowActionRebuild) { diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index e82e9d26e..45857aa45 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -650,6 +650,12 @@ class _RemoteMenubarState extends State { } Widget _buildKeyboard(BuildContext context) { + // Do not support peer 1.1.9. + if (stateGlobal.grabKeyboard) { + bind.sessionSetKeyboardMode(id: widget.id, value: 'map'); + return Offstage(); + } + FfiModel ffiModel = Provider.of(context); if (ffiModel.permissions['keyboard'] == false) { return Offstage(); diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 9ba7a6315..ee3aaaf2c 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -548,13 +548,20 @@ class WindowActionPanelState extends State if (rustDeskWinManager.getActiveWindows().contains(kMainWindowId)) { await rustDeskWinManager.unregisterActiveWindow(kMainWindowId); } - // `hide` must be placed after unregisterActiveWindow, because once all windows are hidden, - // flutter closes the application on macOS. We should ensure the post-run logic has ran successfully. - // e.g.: saving window position. + // macOS specific workaround, the window is not hiding when in fullscreen. + if (Platform.isMacOS && await windowManager.isFullScreen()) { + await windowManager.setFullScreen(false); + await Future.delayed(Duration(seconds: 1)); + } await windowManager.hide(); } else { // it's safe to hide the subwindow - await WindowController.fromWindowId(kWindowId!).hide(); + final controller = WindowController.fromWindowId(kWindowId!); + if (Platform.isMacOS && await controller.isFullScreen()) { + await controller.setFullscreen(false); + await Future.delayed(Duration(seconds: 1)); + } + await controller.hide(); await Future.wait([ rustDeskWinManager .call(WindowType.Main, kWindowEventHide, {"id": kWindowId!}), diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index b1491d526..9a5b06b14 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -58,6 +58,10 @@ class InputModel { InputModel(this.parent); KeyEventResult handleRawKeyEvent(FocusNode data, RawKeyEvent e) { + if (!stateGlobal.grabKeyboard) { + return KeyEventResult.handled; + } + // * Currently mobile does not enable map mode if (isDesktop) { bind.sessionGetKeyboardMode(id: id).then((result) { diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 64c44a555..a07df9c2e 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -325,8 +325,8 @@ packages: dependency: "direct main" description: path: "." - ref: bc8604a88e52b2b6e64d2661ae49a71450a47af8 - resolved-ref: bc8604a88e52b2b6e64d2661ae49a71450a47af8 + ref: f37357ed98a10717576eb9ed8413e92b2ec5d13a + resolved-ref: f37357ed98a10717576eb9ed8413e92b2ec5d13a url: "https://github.com/Kingtous/rustdesk_desktop_multi_window" source: git version: "0.1.0" @@ -1224,6 +1224,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.1" + texture_rgba_renderer: + dependency: "direct main" + description: + name: texture_rgba_renderer + sha256: fbb09b2c6b4ce71261927f9e7e4ea339af3e2f3f2b175f6fb921de1c66ec848d + url: "https://pub.dev" + source: hosted + version: "0.0.8" timing: dependency: transitive description: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 7789f92f4..667b3645e 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -19,156 +19,153 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev version: 1.2.0 environment: - sdk: ">=2.17.0" + sdk: ">=2.17.0" dependencies: - flutter: - sdk: flutter - flutter_localizations: - sdk: flutter - - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^1.0.3 - ffi: ^2.0.1 - path_provider: ^2.0.12 - external_path: ^1.0.1 - provider: ^6.0.3 - tuple: ^2.0.0 - wakelock: ^0.6.2 - device_info_plus: ^4.1.2 - #firebase_analytics: ^9.1.5 - package_info_plus: ^1.4.2 - url_launcher: ^6.0.9 - toggle_switch: ^1.4.0 - dash_chat_2: ^0.0.14 - draggable_float_widget: ^0.0.2 - settings_ui: ^2.0.2 - flutter_breadcrumb: ^1.0.1 - http: ^0.13.4 - qr_code_scanner: ^1.0.0 - zxing2: ^0.1.0 - image_picker: ^0.8.5 - image: ^3.1.3 - back_button_interceptor: ^6.0.1 - flutter_rust_bridge: ^1.61.1 - window_manager: - git: - url: https://github.com/Kingtous/rustdesk_window_manager - ref: 32b24c66151b72bba033ef8b954486aa9351d97b - desktop_multi_window: - git: - url: https://github.com/Kingtous/rustdesk_desktop_multi_window - ref: bc8604a88e52b2b6e64d2661ae49a71450a47af8 - freezed_annotation: ^2.0.3 - flutter_custom_cursor: ^0.0.4 - window_size: - git: - url: https://github.com/google/flutter-desktop-embedding.git - path: plugins/window_size - ref: a738913c8ce2c9f47515382d40827e794a334274 - get: ^4.6.5 - visibility_detector: ^0.3.3 - contextmenu: ^3.0.0 - desktop_drop: ^0.3.3 - scroll_pos: ^0.3.0 - debounce_throttle: ^2.0.0 - file_picker: ^5.1.0 - flutter_svg: ^1.1.5 - flutter_improved_scrolling: - # currently, we use flutter 3.0.5 for windows build, latest for other builds. - # - # for flutter 3.0.5, please use official version(just comment code below). - # if build rustdesk by flutter >=3.3, please use our custom pub below (uncomment code below). - git: - url: https://github.com/Kingtous/flutter_improved_scrolling - ref: 62f09545149f320616467c306c8c5f71714a18e6 - uni_links: ^0.5.1 - uni_links_desktop: ^0.1.4 - path: ^1.8.1 - auto_size_text: ^3.0.0 - bot_toast: ^4.0.3 - win32: any - password_strength: ^0.2.0 - flutter_launcher_icons: ^0.11.0 - flutter_keyboard_visibility: ^5.4.0 - percent_indicator: ^4.2.2 + flutter: + sdk: flutter + flutter_localizations: + sdk: flutter + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.3 + ffi: ^2.0.1 + path_provider: ^2.0.12 + external_path: ^1.0.1 + provider: ^6.0.3 + tuple: ^2.0.0 + wakelock: ^0.6.2 + device_info_plus: ^4.1.2 + #firebase_analytics: ^9.1.5 + package_info_plus: ^1.4.2 + url_launcher: ^6.0.9 + toggle_switch: ^1.4.0 + dash_chat_2: ^0.0.14 + draggable_float_widget: ^0.0.2 + settings_ui: ^2.0.2 + flutter_breadcrumb: ^1.0.1 + http: ^0.13.4 + qr_code_scanner: ^1.0.0 + zxing2: ^0.1.0 + image_picker: ^0.8.5 + image: ^3.1.3 + back_button_interceptor: ^6.0.1 + flutter_rust_bridge: ^1.61.1 + window_manager: + git: + url: https://github.com/Kingtous/rustdesk_window_manager + ref: 32b24c66151b72bba033ef8b954486aa9351d97b + desktop_multi_window: + git: + url: https://github.com/Kingtous/rustdesk_desktop_multi_window + ref: f37357ed98a10717576eb9ed8413e92b2ec5d13a + freezed_annotation: ^2.0.3 + flutter_custom_cursor: ^0.0.4 + window_size: + git: + url: https://github.com/google/flutter-desktop-embedding.git + path: plugins/window_size + ref: a738913c8ce2c9f47515382d40827e794a334274 + get: ^4.6.5 + visibility_detector: ^0.3.3 + contextmenu: ^3.0.0 + desktop_drop: ^0.3.3 + scroll_pos: ^0.3.0 + debounce_throttle: ^2.0.0 + file_picker: ^5.1.0 + flutter_svg: ^1.1.5 + flutter_improved_scrolling: + # currently, we use flutter 3.0.5 for windows build, latest for other builds. + # + # for flutter 3.0.5, please use official version(just comment code below). + # if build rustdesk by flutter >=3.3, please use our custom pub below (uncomment code below). + git: + url: https://github.com/Kingtous/flutter_improved_scrolling + ref: 62f09545149f320616467c306c8c5f71714a18e6 + uni_links: ^0.5.1 + uni_links_desktop: ^0.1.4 + path: ^1.8.1 + auto_size_text: ^3.0.0 + bot_toast: ^4.0.3 + win32: any + password_strength: ^0.2.0 + flutter_launcher_icons: ^0.11.0 + flutter_keyboard_visibility: ^5.4.0 + percent_indicator: ^4.2.2 + texture_rgba_renderer: ^0.0.8 dev_dependencies: - icons_launcher: ^2.0.4 - #flutter_test: - #sdk: flutter - build_runner: ^2.1.11 - freezed: ^2.0.3 - flutter_lints: ^2.0.0 - ffigen: ^7.2.4 + icons_launcher: ^2.0.4 + #flutter_test: + #sdk: flutter + build_runner: ^2.1.11 + freezed: ^2.0.3 + flutter_lints: ^2.0.0 + ffigen: ^7.2.4 # rerun: flutter pub run flutter_launcher_icons flutter_icons: - image_path: "../res/icon.png" - remove_alpha_ios: true - android: true - ios: true - windows: - generate: true - macos: - image_path: "../res/mac-icon.png" - generate: true - linux: true - web: - generate: true - + image_path: "../res/icon.png" + remove_alpha_ios: true + android: true + ios: true + windows: + generate: true + macos: + image_path: "../res/mac-icon.png" + generate: true + linux: true + web: + generate: true # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec # The following section is specific to Flutter. flutter: - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. - uses-material-design: true + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true - # To add assets to your application, add an assets section, like this: - assets: - - assets/ + # To add assets to your application, add an assets section, like this: + assets: + - assets/ - fonts: - - family: GestureIcons - fonts: - - asset: assets/gestures.ttf - - family: Tabbar - fonts: - - asset: assets/tabbar.ttf - - family: PeerSearchbar - fonts: - - asset: assets/peer_searchbar.ttf + fonts: + - family: GestureIcons + fonts: + - asset: assets/gestures.ttf + - family: Tabbar + fonts: + - asset: assets/tabbar.ttf + - family: PeerSearchbar + fonts: + - asset: assets/peer_searchbar.ttf - + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware. - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware. + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages - # For details regarding adding assets from package dependencies, see - # https://flutter.dev/assets-and-images/#from-packages - - # To add custom fonts to your application, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.dev/custom-fonts/#from-packages + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages diff --git a/libs/hbb_common/Cargo.toml b/libs/hbb_common/Cargo.toml index 0457bb19a..a125078d2 100644 --- a/libs/hbb_common/Cargo.toml +++ b/libs/hbb_common/Cargo.toml @@ -33,6 +33,7 @@ tokio-socks = { git = "https://github.com/open-trade/tokio-socks" } chrono = "0.4" backtrace = "0.3" libc = "0.2" +sysinfo = "0.24" [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 99cb6f408..bfb773908 100644 --- a/libs/hbb_common/src/lib.rs +++ b/libs/hbb_common/src/lib.rs @@ -42,6 +42,7 @@ pub use chrono; pub use libc; pub use directories_next; pub mod keyboard; +pub use sysinfo; #[cfg(feature = "quic")] pub type Stream = quic::Connection; diff --git a/libs/scrap/src/common/hwcodec.rs b/libs/scrap/src/common/hwcodec.rs index 9cd6077a6..27b157b79 100644 --- a/libs/scrap/src/common/hwcodec.rs +++ b/libs/scrap/src/common/hwcodec.rs @@ -317,16 +317,30 @@ pub fn check_config() { } pub fn check_config_process(force_reset: bool) { - if force_reset { - HwCodecConfig::remove(); - } - if let Ok(exe) = std::env::current_exe() { - std::thread::spawn(move || { - std::process::Command::new(exe) - .arg("--check-hwcodec-config") - .status() - .ok(); - HwCodecConfig::refresh(); - }); - }; + use hbb_common::sysinfo::{ProcessExt, System, SystemExt}; + + std::thread::spawn(move || { + if force_reset { + HwCodecConfig::remove(); + } + if let Ok(exe) = std::env::current_exe() { + if let Some(file_name) = exe.file_name().to_owned() { + let s = System::new_all(); + let arg = "--check-hwcodec-config"; + for process in s.processes_by_name(&file_name.to_string_lossy().to_string()) { + if process.cmd().iter().any(|cmd| cmd.contains(arg)) { + log::warn!("already have process {}", arg); + return; + } + } + if let Ok(mut child) = std::process::Command::new(exe).arg(arg).spawn() { + let second = 3; + std::thread::sleep(std::time::Duration::from_secs(second)); + // kill: Different platforms have different results + child.kill().ok(); + HwCodecConfig::refresh(); + } + } + }; + }); } diff --git a/res/rustdesk.desktop b/res/rustdesk.desktop index c9cf1f254..f31a16dec 100644 --- a/res/rustdesk.desktop +++ b/res/rustdesk.desktop @@ -1,5 +1,5 @@ [Desktop Entry] -Version=1.2.0 +Version=1.5 Name=RustDesk GenericName=Remote Desktop Comment=Remote Desktop @@ -16,4 +16,4 @@ X-Desktop-File-Install-Version=0.23 [Desktop Action new-window] Name=Open a New Window - +Exec=rustdesk %u diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index f3bc45856..68ddce9b7 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1,13 +1,13 @@ +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use crate::common::get_default_sound_input; use crate::{ client::file_trait::FileManager, - common::make_fd_to_json, common::is_keyboard_mode_supported, + common::make_fd_to_json, 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}, @@ -1181,6 +1181,9 @@ pub fn main_start_grab_keyboard() -> SyncReturn { return SyncReturn(false); } crate::keyboard::client::start_grab_loop(); + if !is_can_input_monitoring(false) { + return SyncReturn(false); + } SyncReturn(true) } diff --git a/src/ipc.rs b/src/ipc.rs index 699b0bcd7..b1b130340 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -549,7 +549,7 @@ async fn check_pid(postfix: &str) { file.read_to_string(&mut content).ok(); let pid = content.parse::().unwrap_or(0); if pid > 0 { - use sysinfo::{ProcessExt, System, SystemExt}; + use hbb_common::sysinfo::{ProcessExt, System, SystemExt}; let mut sys = System::new(); sys.refresh_processes(); if let Some(p) = sys.process(pid.into()) { diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 9d0d176da..4824ac5e9 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -3,7 +3,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = [ ("Status", "状态"), ("Your Desktop", "你的桌面"), - ("desk_tip", "你的桌面可以通过下面的ID和密码访问。"), + ("desk_tip", "你的桌面可以通过下面的 ID 和密码访问。"), ("Password", "密码"), ("Ready", "就绪"), ("Established", "已建立"), @@ -11,7 +11,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable Service", "允许服务"), ("Start Service", "启动服务"), ("Service is running", "服务正在运行"), - ("Service is not running", "服务没有启动"), + ("Service is not running", "服务未运行"), ("not_ready_status", "未就绪,请检查网络连接"), ("Control Remote Desktop", "控制远程桌面"), ("Transfer File", "传输文件"), @@ -19,49 +19,49 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recent Sessions", "最近访问过"), ("Address Book", "地址簿"), ("Confirmation", "确认"), - ("TCP Tunneling", "TCP隧道"), + ("TCP Tunneling", "TCP 隧道"), ("Remove", "删除"), ("Refresh random password", "刷新随机密码"), ("Set your own password", "设置密码"), ("Enable Keyboard/Mouse", "允许控制键盘/鼠标"), ("Enable Clipboard", "允许同步剪贴板"), ("Enable File Transfer", "允许传输文件"), - ("Enable TCP Tunneling", "允许建立TCP隧道"), - ("IP Whitelisting", "IP白名单"), + ("Enable TCP Tunneling", "允许建立 TCP 隧道"), + ("IP Whitelisting", "IP 白名单"), ("ID/Relay Server", "ID/中继服务器"), ("Import Server Config", "导入服务器配置"), ("Export Server Config", "导出服务器配置"), ("Import server configuration successfully", "导入服务器配置信息成功"), ("Export server configuration successfully", "导出服务器配置信息成功"), - ("Invalid server configuration", "无效服务器配置,请修改后重新拷贝配置信息到剪贴板后点击此按钮"), - ("Clipboard is empty", "拷贝配置信息到剪贴板后点击此按钮,可以自动导入配置"), + ("Invalid server configuration", "服务器配置无效,请修改后重新复制配置信息到剪贴板,然后点击此按钮"), + ("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之间。"), + ("Change ID", "更改 ID"), + ("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", "关于"), ("Slogan_tip", ""), - ("Privacy Statement", ""), + ("Privacy Statement", "隐私声明"), ("Mute", "静音"), - ("Build Date", ""), - ("Version", ""), - ("Home", ""), + ("Build Date", "构建日期"), + ("Version", "版本"), + ("Home", "主页"), ("Audio Input", "音频输入"), ("Enhancements", "增强功能"), ("Hardware Codec", "硬件编解码"), ("Adaptive Bitrate", "自适应码率"), - ("ID Server", "ID服务器"), + ("ID Server", "ID 服务器"), ("Relay Server", "中继服务器"), - ("API Server", "API服务器"), - ("invalid_http", "必须以http://或者https://开头"), - ("Invalid IP", "无效IP"), + ("API Server", "API 服务器"), + ("invalid_http", "必须以 http:// 或者 https:// 开头"), + ("Invalid IP", "无效 IP"), ("Invalid format", "无效格式"), ("server_not_support", "服务器暂不支持"), - ("Not available", "已被占用"), + ("Not available", "不可用"), ("Too frequent", "修改太频繁,请稍后再试"), ("Cancel", "取消"), ("Skip", "跳过"), @@ -72,12 +72,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please enter your password", "请输入密码"), ("Remember password", "记住密码"), ("Wrong Password", "密码错误"), - ("Do you want to enter again?", "还想输入一次吗?"), + ("Do you want to enter again?", "是否要再次输入?"), ("Connection Error", "连接错误"), ("Error", "错误"), ("Reset by the peer", "连接被对方关闭"), ("Connecting...", "正在连接..."), - ("Connection in progress. Please wait.", "连接进行中,请稍等。"), + ("Connection in progress. Please wait.", "正在进行连接,请稍候。"), ("Please try 1 minute later", "一分钟后再试"), ("Login Error", "登录错误"), ("Successful", "成功"), @@ -102,14 +102,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Unselect All", "取消全选"), ("Empty Directory", "空文件夹"), ("Not an empty directory", "这不是一个空文件夹"), - ("Are you sure you want to delete this file?", "是否删除此文件?"), - ("Are you sure you want to delete this empty directory?", "是否删除此空文件夹?"), - ("Are you sure you want to delete the file of this directory?", "是否删除文件夹下的文件?"), + ("Are you sure you want to delete this file?", "是否删除此文件?"), + ("Are you sure you want to delete this empty directory?", "是否删除此空文件夹?"), + ("Are you sure you want to delete the file of this directory?", "是否删除此文件夹下的文件?"), ("Do this for all conflicts", "应用于其它冲突"), ("This is irreversible!", "此操作不可逆!"), ("Deleting", "正在删除"), ("files", "文件"), - ("Waiting", "等待..."), + ("Waiting", "正在等待..."), ("Finished", "完成"), ("Speed", "速度"), ("Custom Image Quality", "设置画面质量"), @@ -122,37 +122,37 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stretch", "伸展"), ("Scrollbar", "滚动条"), ("ScrollAuto", "自动滚动"), - ("Good image quality", "好画质"), - ("Balanced", "一般画质"), - ("Optimize reaction time", "优化反应时间"), + ("Good image quality", "画质最优化"), + ("Balanced", "平衡"), + ("Optimize reaction time", "速度最优化"), ("Custom", "自定义"), ("Show remote cursor", "显示远程光标"), ("Show quality monitor", "显示质量监测"), - ("Disable clipboard", "禁止剪贴板"), - ("Lock after session end", "断开后锁定远程电脑"), + ("Disable clipboard", "禁用剪贴板"), + ("Lock after session end", "会话结束后锁定远程电脑"), ("Insert", "插入"), ("Insert Lock", "锁定远程电脑"), ("Refresh", "刷新画面"), - ("ID does not exist", "ID不存在"), + ("ID does not exist", "ID 不存在"), ("Failed to connect to rendezvous server", "连接注册服务器失败"), ("Please try later", "请稍后再试"), - ("Remote desktop is offline", "远程电脑不在线"), - ("Key mismatch", "Key不匹配"), + ("Remote desktop is offline", "远程电脑处于离线状态"), + ("Key mismatch", "密钥不匹配"), ("Timeout", "连接超时"), ("Failed to connect to relay server", "无法连接到中继服务器"), ("Failed to connect via rendezvous server", "无法通过注册服务器建立连接"), ("Failed to connect via relay server", "无法通过中继服务器建立连接"), - ("Failed to make direct connection to remote desktop", "无法建立直接连接"), + ("Failed to make direct connection to remote desktop", "无法直接连接到远程桌面"), ("Set Password", "设置密码"), ("OS Password", "操作系统密码"), - ("install_tip", "你正在运行未安装版本,由于UAC限制,作为被控端,会在某些情况下无法控制鼠标键盘,或者录制屏幕,请点击下面的按钮将 RustDesk 安装到系统,从而规避上述问题。"), + ("install_tip", "你正在运行未安装版本,由于 UAC 限制,作为被控端,会在某些情况下无法控制鼠标键盘,或者录制屏幕,请点击下面的按钮将 RustDesk 安装到系统,从而规避上述问题。"), ("Click to upgrade", "点击这里升级"), ("Click to download", "点击这里下载"), ("Click to update", "点击这里更新"), ("Configure", "配置"), ("config_acc", "为了能够远程控制你的桌面, 请给予 RustDesk \"辅助功能\" 权限。"), ("config_screen", "为了能够远程访问你的桌面, 请给予 RustDesk \"屏幕录制\" 权限。"), - ("Installing ...", "安装 ..."), + ("Installing ...", "安装中..."), ("Install", "安装"), ("Installation", "安装"), ("Installation Path", "安装路径"), @@ -161,10 +161,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("agreement_tip", "开始安装即表示接受许可协议。"), ("Accept and Install", "同意并安装"), ("End-user license agreement", "用户协议"), - ("Generating ...", "正在产生 ..."), + ("Generating ...", "正在生成..."), ("Your installation is lower version.", "你安装的版本比当前运行的低。"), ("not_close_tcp_tip", "请在使用隧道的时候,不要关闭本窗口"), - ("Listening ...", "正在等待隧道连接 ..."), + ("Listening ...", "正在等待隧道连接..."), ("Remote Host", "远程主机"), ("Remote Port", "远程端口"), ("Action", "动作"), @@ -173,7 +173,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Local Address", "当前地址"), ("Change Local Port", "修改本地端口"), ("setup_server_tip", "如果需要更快连接速度,你可以选择自建服务器"), - ("Too short, at least 6 characters.", "太短了,至少6个字符"), + ("Too short, at least 6 characters.", "太短了,至少 6 个字符"), ("The confirmation is not identical.", "两次输入不匹配"), ("Permissions", "权限"), ("Accept", "接受"), @@ -183,21 +183,21 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Allow using clipboard", "允许使用剪贴板"), ("Allow hearing sound", "允许听到声音"), ("Allow file copy and paste", "允许复制粘贴文件"), - ("Connected", "已经连接"), + ("Connected", "已连接"), ("Direct and encrypted connection", "加密直连"), ("Relayed and encrypted connection", "加密中继连接"), ("Direct and unencrypted connection", "非加密直连"), ("Relayed and unencrypted connection", "非加密中继连接"), - ("Enter Remote ID", "输入对方ID"), + ("Enter Remote ID", "输入对方 ID"), ("Enter your password", "输入密码"), ("Logging in...", "正在登录..."), - ("Enable RDP session sharing", "允许RDP会话共享"), + ("Enable RDP session sharing", "允许 RDP 会话共享"), ("Auto Login", "自动登录(设置断开后锁定才有效)"), - ("Enable Direct IP Access", "允许IP直接访问"), - ("Rename", "改名"), + ("Enable Direct IP Access", "允许 IP 直接访问"), + ("Rename", "重命名"), ("Space", "空格"), ("Create Desktop Shortcut", "创建桌面快捷方式"), - ("Change Path", "改变路径"), + ("Change Path", "更改路径"), ("Create Folder", "创建文件夹"), ("Please enter the folder name", "请输入文件夹名称"), ("Fix it", "修复"), @@ -212,29 +212,29 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Invalid port", "无效端口"), ("Closed manually by the peer", "被对方手动关闭"), ("Enable remote configuration modification", "允许远程修改配置"), - ("Run without install", "无安装运行"), + ("Run without install", "不安装直接运行"), ("Connect via relay", "中继连接"), ("Always connect via relay", "强制走中继连接"), - ("whitelist_tip", "只有白名单里的ip才能访问我"), + ("whitelist_tip", "只有白名单里的 IP 才能访问本机"), ("Login", "登录"), ("Verify", "验证"), ("Remember me", "记住我"), ("Trust this device", "信任此设备"), ("Verification code", "验证码"), - ("verification_tip", "检测到新设备登录,已向注册邮箱发送了登录验证码,输入验证码继续登录"), + ("verification_tip", "检测到新设备登录,已向注册邮箱发送了登录验证码,请输入验证码继续登录"), ("Logout", "登出"), ("Tags", "标签"), - ("Search ID", "查找ID"), + ("Search ID", "查找 ID"), ("whitelist_sep", "可以使用逗号,分号,空格或者换行符作为分隔符"), - ("Add ID", "增加ID"), + ("Add ID", "增加 ID"), ("Add Tag", "增加标签"), ("Unselect all tags", "取消选择所有标签"), ("Network error", "网络错误"), ("Username missed", "用户名没有填写"), ("Password missed", "密码没有填写"), - ("Wrong credentials", "提供的登入信息错误"), + ("Wrong credentials", "提供的登录信息错误"), ("Edit Tag", "修改标签"), - ("Unremember Password", "忘掉密码"), + ("Unremember Password", "忘记密码"), ("Favorites", "收藏"), ("Add to Favorites", "加入到收藏"), ("Remove from Favorites", "从收藏中删除"), @@ -244,9 +244,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Hostname", "主机名"), ("Discovered", "已发现"), ("install_daemon_tip", "为了开机启动,请安装系统服务。"), - ("Remote ID", "远程ID"), + ("Remote ID", "远程 ID"), ("Paste", "粘贴"), - ("Paste here?", "粘贴到这里?"), + ("Paste here?", "粘贴到这里?"), ("Are you sure to close the connection?", "是否确认关闭连接?"), ("Download new version", "下载新版本"), ("Touch mode", "触屏模式"), @@ -284,7 +284,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Do you accept?", "是否接受?"), ("Open System Setting", "打开系统设置"), ("How to get Android input permission?", "如何获取安卓的输入权限?"), - ("android_input_permission_tip1", "为了让远程设备通过鼠标或触屏控制您的安卓设备,你需要允許 RustDesk 使用\"无障碍\"服务。"), + ("android_input_permission_tip1", "为了让远程设备通过鼠标或触屏控制您的安卓设备,你需要允许 RustDesk 使用\"无障碍\"服务。"), ("android_input_permission_tip2", "请在接下来的系统设置页面里,找到并进入 [已安装的服务] 页面,将 [RustDesk Input] 服务开启。"), ("android_new_connection_tip", "收到新的连接控制请求,对方想要控制你当前的设备。"), ("android_service_will_start_tip", "开启录屏权限将自动开启服务,允许其他设备向此设备请求建立连接。"), @@ -293,7 +293,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_start_service_tip", "点击 [启动服务] 或打开 [屏幕录制] 权限开启手机屏幕共享服务。"), ("Account", "账户"), ("Overwrite", "覆盖"), - ("This file exists, skip or overwrite this file?", "这个文件/文件夹已存在,跳过/覆盖?"), + ("This file exists, skip or overwrite this file?", "这个文件/文件夹已存在,跳过/覆盖?"), ("Quit", "退出"), ("doc_mac_permission", "https://rustdesk.com/docs/zh-cn/manual/mac#%E5%90%AF%E7%94%A8%E6%9D%83%E9%99%90"), ("Help", "帮助"), @@ -314,7 +314,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_open_battery_optimizations_tip", "如需关闭此功能,请在接下来的 RustDesk 应用设置页面中,找到并进入 [电源] 页面,取消勾选 [不受限制]"), ("Connection not allowed", "对方不允许连接"), ("Legacy mode", "传统模式"), - ("Map mode", "1:1传输"), + ("Map mode", "1:1 传输"), ("Translate mode", "翻译模式"), ("Use permanent password", "使用固定密码"), ("Use both passwords", "同时使用两种密码"), @@ -355,16 +355,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable Audio", "允许传输音频"), ("Unlock Network Settings", "解锁网络设置"), ("Server", "服务器"), - ("Direct IP Access", "IP直接访问"), + ("Direct IP Access", "IP 直接访问"), ("Proxy", "代理"), ("Apply", "应用"), - ("Disconnect all devices?", "断开所有远程连接?"), + ("Disconnect all devices?", "断开所有远程连接?"), ("Clear", "清空"), ("Audio Input Device", "音频输入设备"), ("Deny remote access", "拒绝远程访问"), - ("Use IP Whitelisting", "只允许白名单上的IP访问"), + ("Use IP Whitelisting", "只允许白名单上的 IP 访问"), ("Network", "网络"), - ("Enable RDP", "允许RDP访问"), + ("Enable RDP", "允许 RDP 访问"), ("Pin menubar", "固定菜单栏"), ("Unpin menubar", "取消固定菜单栏"), ("Recording", "录屏"), @@ -379,7 +379,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Deny LAN Discovery", "拒绝局域网发现"), ("Write a message", "输入聊天消息"), ("Prompt", "提示"), - ("Please wait for confirmation of UAC...", "请等待对方确认 UAC ..."), + ("Please wait for confirmation of UAC...", "请等待对方确认 UAC..."), ("elevated_foreground_window_tip", "远端桌面的当前窗口需要更高的权限才能操作, 暂时无法使用鼠标键盘, 可以请求对方最小化当前窗口, 或者在连接管理窗口点击提升。为避免这个问题,建议在远端设备上安装本软件。"), ("Disconnected", "会话已结束"), ("Other", "其他"), @@ -396,7 +396,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("or", "或"), ("Continue with", "使用"), ("Elevate", "提权"), - ("Zoom cursor", "缩放鼠标"), + ("Zoom cursor", "缩放光标"), ("Accept sessions via password", "只允许密码访问"), ("Accept sessions via click", "只允许点击访问"), ("Accept sessions via both", "允许密码或点击访问"), @@ -407,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request access to your device", "请求访问你的设备"), ("Hide connection management window", "隐藏连接管理窗口"), ("hide_cm_tip", "在只允许密码连接并且只用固定密码的情况下才允许隐藏"), - ("wayland_experiment_tip", "Wayland 支持处于实验阶段,如果你需要使用无人值守访问,请使用X11。"), + ("wayland_experiment_tip", "Wayland 支持处于实验阶段,如果你需要使用无人值守访问,请使用 X11。"), ("Right click to select tabs", "右键选择选项卡"), ("Skipped", "已跳过"), ("Add to Address Book", "添加到地址簿"), @@ -417,7 +417,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Local keyboard type", "本地键盘类型"), ("Select local keyboard type", "请选择本地键盘类型"), ("software_render_tip", "如果你使用英伟达显卡, 并且远程窗口在会话建立后会立刻关闭, 那么安装 nouveau 驱动并且选择使用软件渲染可能会有帮助。重启软件后生效。"), - ("Always use software rendering", "使用软件渲染"), + ("Always use software rendering", "始终使用软件渲染"), ("config_input", "为了能够通过键盘控制远程桌面, 请给予 RustDesk \"输入监控\" 权限。"), ("config_microphone", "为了支持通过麦克风进行音频传输,请给予 RustDesk \"录音\"权限。"), ("request_elevation_tip", "如果对面有人, 也可以请求提升权限。"), @@ -434,25 +434,25 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("lowercase", "小写字母"), ("digit", "数字"), ("special character", "特殊字符"), - ("length>=8", "长度不小于8"), + ("length>=8", "长度不小于 8"), ("Weak", "弱"), ("Medium", "中"), ("Strong", "强"), ("Switch Sides", "反转访问方向"), - ("Please confirm if you want to share your desktop?", "请确认要让对方访问你的桌面?"), + ("Please confirm if you want to share your desktop?", "请确认是否要让对方访问你的桌面?"), ("Display", "显示"), ("Default View Style", "默认显示方式"), ("Default Scroll Style", "默认滚动方式"), ("Default Image Quality", "默认图像质量"), ("Default Codec", "默认编解码"), - ("Bitrate", "波特率"), + ("Bitrate", "码率"), ("FPS", "帧率"), ("Auto", "自动"), ("Other Default Options", "其它默认选项"), ("Voice call", "语音通话"), ("Text chat", "文字聊天"), - ("Stop voice call", "停止语音聊天"), - ("relay_hint_tip", "可能无法直连,可以尝试中继连接。\n另外,如果想直接使用中继连接,可以在ID后面添加/r,或者在卡片选项里选择强制走中继连接。"), + ("Stop voice call", "停止语音通话"), + ("relay_hint_tip", "可能无法直连,可以尝试中继连接。\n另外,如果想直接使用中继连接,可以在 ID 后面添加/r,或者在卡片选项里选择强制走中继连接。"), ("Reconnect", "重连"), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 00f6b70ac..70051f3e8 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -442,7 +442,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please confirm if you want to share your desktop?", "لطفاً تأیید کنید که آیا می خواهید دسکتاپ خود را به اشتراک بگذارید؟"), ("Display", "نمایش دادن"), ("Default View Style", "سبک نمایش پیش فرض"), - ("Default Scroll Style", "سبک پیش‌فرض اسکرول"), + ("Default Scroll Style", "سبک پیش‌ فرض اسکرول"), ("Default Image Quality", "کیفیت تصویر پیش فرض"), ("Default Codec", "کدک پیش فرض"), ("Bitrate", "میزان بیت صفحه نمایش"), @@ -452,7 +452,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", " را به شناسه اضافه کنید یا گزینه \"همیشه از طریق رله متصل شوید\" را در کارت همتا انتخاب کنید. همچنین، اگر می‌خواهید فوراً از سرور رله استفاده کنید، می‌توانید پسوند \"/r\".\n اتصال مستقیم ممکن است امکان پذیر نباشد. در این صورت می توانید سعی کنید از طریق سرور رله متصل شوید"), + ("Reconnect", "اتصال مجدد"), ].iter().cloned().collect(); } diff --git a/src/platform/macos.rs b/src/platform/macos.rs index 0c8c51455..910c26982 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -558,7 +558,7 @@ pub fn hide_dock() { } fn check_main_window() -> bool { - use sysinfo::{ProcessExt, System, SystemExt}; + use hbb_common::sysinfo::{ProcessExt, System, SystemExt}; let mut sys = System::new(); sys.refresh_processes(); let app = format!("/Applications/{}.app", crate::get_app_name()); From c26053b8040f0cb13561f2bb42ce8b49f530901d Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Wed, 22 Feb 2023 23:17:33 +0100 Subject: [PATCH 1967/2015] formatted pubspec --- flutter/pubspec.yaml | 142 +++++++++++++++++++++---------------------- 1 file changed, 71 insertions(+), 71 deletions(-) diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index cd917b5e3..572b3e20a 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -22,78 +22,78 @@ environment: sdk: ">=2.17.0" dependencies: - flutter: - sdk: flutter - flutter_localizations: - sdk: flutter + flutter: + sdk: flutter + flutter_localizations: + sdk: flutter - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^1.0.3 - ffi: ^2.0.1 - path_provider: ^2.0.12 - external_path: ^1.0.1 - provider: ^6.0.3 - tuple: ^2.0.0 - wakelock: ^0.6.2 - device_info_plus: ^4.1.2 - #firebase_analytics: ^9.1.5 - package_info_plus: ^1.4.2 - url_launcher: ^6.0.9 - toggle_switch: ^1.4.0 - dash_chat_2: ^0.0.14 - draggable_float_widget: ^0.0.2 - settings_ui: ^2.0.2 - flutter_breadcrumb: ^1.0.1 - http: ^0.13.4 - qr_code_scanner: ^1.0.0 - zxing2: ^0.1.0 - image_picker: ^0.8.5 - image: ^3.1.3 - back_button_interceptor: ^6.0.1 - flutter_rust_bridge: ^1.61.1 - window_manager: - git: - url: https://github.com/Kingtous/rustdesk_window_manager - ref: 32b24c66151b72bba033ef8b954486aa9351d97b - desktop_multi_window: - git: - url: https://github.com/Kingtous/rustdesk_desktop_multi_window - ref: f37357ed98a10717576eb9ed8413e92b2ec5d13a - freezed_annotation: ^2.0.3 - flutter_custom_cursor: ^0.0.4 - window_size: - git: - url: https://github.com/google/flutter-desktop-embedding.git - path: plugins/window_size - ref: a738913c8ce2c9f47515382d40827e794a334274 - get: ^4.6.5 - visibility_detector: ^0.3.3 - contextmenu: ^3.0.0 - desktop_drop: ^0.3.3 - scroll_pos: ^0.3.0 - debounce_throttle: ^2.0.0 - file_picker: ^5.1.0 - flutter_svg: ^1.1.5 - flutter_improved_scrolling: - # currently, we use flutter 3.0.5 for windows build, latest for other builds. - # - # for flutter 3.0.5, please use official version(just comment code below). - # if build rustdesk by flutter >=3.3, please use our custom pub below (uncomment code below). - git: - url: https://github.com/Kingtous/flutter_improved_scrolling - ref: 62f09545149f320616467c306c8c5f71714a18e6 - uni_links: ^0.5.1 - uni_links_desktop: ^0.1.4 - path: ^1.8.1 - auto_size_text: ^3.0.0 - bot_toast: ^4.0.3 - win32: any - password_strength: ^0.2.0 - flutter_launcher_icons: ^0.11.0 - flutter_keyboard_visibility: ^5.4.0 - texture_rgba_renderer: ^0.0.8 - percent_indicator: ^4.2.2 + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.3 + ffi: ^2.0.1 + path_provider: ^2.0.12 + external_path: ^1.0.1 + provider: ^6.0.3 + tuple: ^2.0.0 + wakelock: ^0.6.2 + device_info_plus: ^4.1.2 + #firebase_analytics: ^9.1.5 + package_info_plus: ^1.4.2 + url_launcher: ^6.0.9 + toggle_switch: ^1.4.0 + dash_chat_2: ^0.0.14 + draggable_float_widget: ^0.0.2 + settings_ui: ^2.0.2 + flutter_breadcrumb: ^1.0.1 + http: ^0.13.4 + qr_code_scanner: ^1.0.0 + zxing2: ^0.1.0 + image_picker: ^0.8.5 + image: ^3.1.3 + back_button_interceptor: ^6.0.1 + flutter_rust_bridge: ^1.61.1 + window_manager: + git: + url: https://github.com/Kingtous/rustdesk_window_manager + ref: 32b24c66151b72bba033ef8b954486aa9351d97b + desktop_multi_window: + git: + url: https://github.com/Kingtous/rustdesk_desktop_multi_window + ref: f37357ed98a10717576eb9ed8413e92b2ec5d13a + freezed_annotation: ^2.0.3 + flutter_custom_cursor: ^0.0.4 + window_size: + git: + url: https://github.com/google/flutter-desktop-embedding.git + path: plugins/window_size + ref: a738913c8ce2c9f47515382d40827e794a334274 + get: ^4.6.5 + visibility_detector: ^0.3.3 + contextmenu: ^3.0.0 + desktop_drop: ^0.3.3 + scroll_pos: ^0.3.0 + debounce_throttle: ^2.0.0 + file_picker: ^5.1.0 + flutter_svg: ^1.1.5 + flutter_improved_scrolling: + # currently, we use flutter 3.0.5 for windows build, latest for other builds. + # + # for flutter 3.0.5, please use official version(just comment code below). + # if build rustdesk by flutter >=3.3, please use our custom pub below (uncomment code below). + git: + url: https://github.com/Kingtous/flutter_improved_scrolling + ref: 62f09545149f320616467c306c8c5f71714a18e6 + uni_links: ^0.5.1 + uni_links_desktop: ^0.1.4 + path: ^1.8.1 + auto_size_text: ^3.0.0 + bot_toast: ^4.0.3 + win32: any + password_strength: ^0.2.0 + flutter_launcher_icons: ^0.11.0 + flutter_keyboard_visibility: ^5.4.0 + texture_rgba_renderer: ^0.0.8 + percent_indicator: ^4.2.2 dev_dependencies: icons_launcher: ^2.0.4 From 30840f9988a90d3000910da377e46b17301de03f Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 23 Feb 2023 14:43:41 +0800 Subject: [PATCH 1968/2015] fix toggle clipboard dead lock Signed-off-by: fufesou --- src/client.rs | 5 ----- src/flutter_ffi.rs | 10 ++++++++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/client.rs b/src/client.rs index f36bdae78..aa3523185 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1241,11 +1241,6 @@ impl LoginConfigHandler { if !name.contains("block-input") { 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(); - } let mut misc = Misc::new(); misc.set_option(option); let mut msg_out = Message::new(); diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 68ddce9b7..7eeb96b5c 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -165,9 +165,15 @@ pub fn session_reconnect(id: String, force_relay: bool) { } pub fn session_toggle_option(id: String, value: String) { + let mut is_found = false; if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) { - log::warn!("toggle option {}", value); - session.toggle_option(value); + is_found = true; + log::warn!("toggle option {}", &value); + session.toggle_option(value.clone()); + } + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if is_found && value == "disable-clipboard" { + crate::flutter::update_text_clipboard_required(); } } From 54bebee35fef2f2c2746a0ddfcb3f9bab5badfc5 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sat, 18 Feb 2023 11:16:07 +0800 Subject: [PATCH 1969/2015] wip: texture windows --- Cargo.lock | 1 + Cargo.toml | 3 ++- src/flutter.rs | 71 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 115845b50..eb5461a6e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4860,6 +4860,7 @@ dependencies = [ "include_dir", "jni 0.19.0", "lazy_static", + "libloading", "libpulse-binding", "libpulse-simple-binding", "mac_address", diff --git a/Cargo.toml b/Cargo.toml index f685e3f2e..0a7af0cbc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ default-run = "rustdesk" [lib] name = "librustdesk" -crate-type = ["cdylib", "staticlib", "rlib"] +crate-type = ["cdylib"] [[bin]] name = "naming" @@ -67,6 +67,7 @@ url = { version = "2.1", features = ["serde"] } reqwest = { version = "0.11", features = ["blocking", "json", "rustls-tls"], default-features=false } chrono = "0.4.23" cidr-utils = "0.5.9" +libloading = "0.7.4" [target.'cfg(not(any(target_os = "android", target_os = "linux")))'.dependencies] cpal = "0.13.5" diff --git a/src/flutter.rs b/src/flutter.rs index bad6e0008..cab7a900d 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -8,6 +8,8 @@ use hbb_common::{ bail, config::LocalConfig, get_version_number, message_proto::*, rendezvous_proto::ConnType, ResultType, }; +use libc::c_void; +use libloading::Library; use serde_json::json; use std::sync::atomic::{AtomicBool, Ordering}; use std::{ @@ -115,6 +117,58 @@ pub struct FlutterHandler { // We must check the `rgba_valid` before reading [rgba]. pub rgba: Arc>>, pub rgba_valid: Arc, + pub renderer: Arc> +} +// pub type FlutterRgbaRendererPluginOnRgba = unsafe extern "C" fn(texture_rgba: *mut c_void , buffer: *const u8 , width: c_int, height: c_int); + +extern "C" { + fn FlutterRgbaRendererPluginOnRgba(texture_rgba: *mut c_void , buffer: *const u8 , width: c_int, height: c_int); +} + +// Video Texture Renderer in Flutter +#[derive(Default, Clone)] +pub struct VideoRenderer { + // TextureRgba pointer in flutter native. + ptr: usize, + width: i32, + height: i32, + // on_rgba_func: FlutterRgbaRendererPluginOnRgba +} + +// impl Default for VideoRenderer { +// fn default() -> Self { +// unsafe { +// let lib = Library::new("texture_rgba_renderer_plugin").expect("`libtexture_rgba_renderer_plugin` not found, please add `texture_rgba_renderer` in your project"); +// let func = lib.get(b"FlutterRgbaRendererPluginOnRgba"); +// } + +// } +// } + + + +impl VideoRenderer { + pub fn new(ptr: usize) -> Self { + Self { + ptr, + ..Default::default() + } + } + + pub fn set_size(&mut self, width: i32, height: i32) { + self.width = width; + self.height = height; + } + + pub fn on_rgba(&self, rgba: *const u8) { + if self.ptr == usize::default() { + return; + } + #[cfg(target_os = "windows")] + unsafe { + FlutterRgbaRendererPluginOnRgba(self.ptr as _, rgba, self.width as _, self.height as _); + } + } } impl FlutterHandler { @@ -156,6 +210,10 @@ impl FlutterHandler { } serde_json::ser::to_string(&msg_vec).unwrap_or("".to_owned()) } + + pub fn register_texture(&self, ptr: usize) { + self.renderer.write().unwrap().ptr = ptr; + } } impl InvokeUiSession for FlutterHandler { @@ -324,9 +382,12 @@ impl InvokeUiSession for FlutterHandler { self.rgba_valid.store(true, Ordering::Relaxed); // Return the rgba buffer to the video handler for reusing allocated rgba buffer. std::mem::swap::>(data, &mut *self.rgba.write().unwrap()); + #[cfg(not(any(target_os = "windows")))] if let Some(stream) = &*self.event_stream.read().unwrap() { stream.add(EventToUI::Rgba); } + #[cfg(any(target_os = "windows"))] + self.renderer.read().unwrap().on_rgba(self.rgba.read().unwrap().as_ptr()); } fn set_peer_info(&self, pi: &PeerInfo) { @@ -741,3 +802,13 @@ pub fn session_next_rgba(id: *const char) { } } } + +#[no_mangle] +pub fn session_register_texture(id: *const char, ptr: 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.register_texture(ptr); + } + } +} \ No newline at end of file From ea07b9690e8cc3ae7e7c8e88dd01a50117e789e6 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sat, 18 Feb 2023 11:47:18 +0800 Subject: [PATCH 1970/2015] fix: rgba compile --- src/flutter.rs | 69 ++++++++++++++++++++++++++++---------------------- 1 file changed, 39 insertions(+), 30 deletions(-) diff --git a/src/flutter.rs b/src/flutter.rs index cab7a900d..2888ffe75 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -9,7 +9,7 @@ use hbb_common::{ ResultType, }; use libc::c_void; -use libloading::Library; +use libloading::{Library, Symbol}; use serde_json::json; use std::sync::atomic::{AtomicBool, Ordering}; use std::{ @@ -29,6 +29,18 @@ lazy_static::lazy_static! { pub static ref CUR_SESSION_ID: RwLock = Default::default(); pub static ref SESSIONS: RwLock>> = Default::default(); pub static ref GLOBAL_EVENT_STREAM: RwLock>> = Default::default(); // rust to dart event channel + #[cfg(not(any(target_os = "ios", target_os = "android")))] + pub static ref TEXURE_RGBA_RENDERER_PLUGIN: Library = { + unsafe { + #[cfg(target_os = "windows")] + let lib = Library::new("texture_rgba_renderer_plugin.dll"); + #[cfg(target_os = "macos")] + let lib = Library::new("texture_rgba_renderer_plugin.dylib"); + #[cfg(target_os = "linux")] + let lib = Library::new("texture_rgba_renderer_plugin.so"); + lib.expect("`libtexture_rgba_renderer_plugin` not found, please add `texture_rgba_renderer` in your flutter project") + } + }; } /// FFI for rustdesk core's main entry. @@ -117,35 +129,33 @@ pub struct FlutterHandler { // We must check the `rgba_valid` before reading [rgba]. pub rgba: Arc>>, pub rgba_valid: Arc, - pub renderer: Arc> -} -// pub type FlutterRgbaRendererPluginOnRgba = unsafe extern "C" fn(texture_rgba: *mut c_void , buffer: *const u8 , width: c_int, height: c_int); - -extern "C" { - fn FlutterRgbaRendererPluginOnRgba(texture_rgba: *mut c_void , buffer: *const u8 , width: c_int, height: c_int); + pub renderer: VideoRenderer, } +pub type FlutterRgbaRendererPluginOnRgba = + unsafe extern "C" fn(texture_rgba: *mut c_void, buffer: *const u8, width: c_int, height: c_int); // Video Texture Renderer in Flutter -#[derive(Default, Clone)] +#[derive(Clone)] pub struct VideoRenderer { // TextureRgba pointer in flutter native. ptr: usize, width: i32, height: i32, - // on_rgba_func: FlutterRgbaRendererPluginOnRgba + on_rgba_func: Symbol<'static, FlutterRgbaRendererPluginOnRgba>, } -// impl Default for VideoRenderer { -// fn default() -> Self { -// unsafe { -// let lib = Library::new("texture_rgba_renderer_plugin").expect("`libtexture_rgba_renderer_plugin` not found, please add `texture_rgba_renderer` in your project"); -// let func = lib.get(b"FlutterRgbaRendererPluginOnRgba"); -// } - -// } -// } - - +impl Default for VideoRenderer { + fn default() -> Self { + unsafe { + Self { + on_rgba_func: TEXURE_RGBA_RENDERER_PLUGIN + .get::(b"FlutterRgbaRendererPluginOnRgba") + .expect("Symbol FlutterRgbaRendererPluginOnRgba not found."), + ..Default::default() + } + } + } +} impl VideoRenderer { pub fn new(ptr: usize) -> Self { @@ -164,10 +174,8 @@ impl VideoRenderer { if self.ptr == usize::default() { return; } - #[cfg(target_os = "windows")] - unsafe { - FlutterRgbaRendererPluginOnRgba(self.ptr as _, rgba, self.width as _, self.height as _); - } + let func = self.on_rgba_func.clone(); + unsafe {func(self.ptr as _, rgba, self.width as _, self.height as _)}; } } @@ -211,8 +219,8 @@ impl FlutterHandler { serde_json::ser::to_string(&msg_vec).unwrap_or("".to_owned()) } - pub fn register_texture(&self, ptr: usize) { - self.renderer.write().unwrap().ptr = ptr; + pub fn register_texture(&mut self, ptr: usize) { + self.renderer.ptr = ptr; } } @@ -382,12 +390,13 @@ impl InvokeUiSession for FlutterHandler { self.rgba_valid.store(true, Ordering::Relaxed); // Return the rgba buffer to the video handler for reusing allocated rgba buffer. std::mem::swap::>(data, &mut *self.rgba.write().unwrap()); - #[cfg(not(any(target_os = "windows")))] + #[cfg(any(target_os = "android", target_os = "ios"))] if let Some(stream) = &*self.event_stream.read().unwrap() { stream.add(EventToUI::Rgba); } - #[cfg(any(target_os = "windows"))] - self.renderer.read().unwrap().on_rgba(self.rgba.read().unwrap().as_ptr()); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + self.renderer + .on_rgba(self.rgba.read().unwrap().as_ptr()); } fn set_peer_info(&self, pi: &PeerInfo) { @@ -811,4 +820,4 @@ pub fn session_register_texture(id: *const char, ptr: usize) { return session.register_texture(ptr); } } -} \ No newline at end of file +} From d3455f3ce2711e8af6631df25511f28548278720 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Sun, 19 Feb 2023 15:25:30 +0800 Subject: [PATCH 1971/2015] feat: adapt for the latest renderer plugin --- flutter/lib/common.dart | 5 +++ flutter/lib/desktop/pages/remote_page.dart | 42 ++++++++++++++++++---- flutter/lib/models/model.dart | 17 ++++----- flutter/lib/models/native_model.dart | 13 +++++++ src/flutter.rs | 28 ++++++++++----- src/ui/remote.rs | 2 +- src/ui_session_interface.rs | 2 +- 7 files changed, 85 insertions(+), 24 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index ff8dfbb09..6d3e4c3b7 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -19,6 +19,7 @@ import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:flutter_hbb/utils/platform_channel.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; +import 'package:texture_rgba_renderer/texture_rgba_renderer.dart'; import 'package:uni_links/uni_links.dart'; import 'package:uni_links_desktop/uni_links_desktop.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -44,6 +45,10 @@ var isWeb = false; var isWebDesktop = false; var version = ""; int androidVersion = 0; +/// Incriment count for textureId. +int _textureId = 0; +int get newTextureId => _textureId ++; +final textureRenderer = TextureRgbaRenderer(); /// only available for Windows target int windowsBuildNumber = 0; diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index f9db985d9..df9874172 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -63,6 +63,8 @@ class _RemotePageState extends State late RxBool _zoomCursor; late RxBool _remoteCursorMoved; late RxBool _keyboardEnabled; + late RxInt _textureId; + late int _textureKey; final _blockableOverlayState = BlockableOverlayState(); @@ -85,6 +87,8 @@ class _RemotePageState extends State _showRemoteCursor = ShowRemoteCursorState.find(id); _keyboardEnabled = KeyboardEnabledState.find(id); _remoteCursorMoved = RemoteCursorMovedState.find(id); + _textureKey = newTextureId; + _textureId = RxInt(-1); } void _removeStates(String id) { @@ -119,6 +123,16 @@ class _RemotePageState extends State if (!Platform.isLinux) { Wakelock.enable(); } + // Register texture. + _textureId.value = -1; + textureRenderer.createTexture(_textureKey).then((id) async { + if (id != -1) { + final ptr = await textureRenderer.getTexturePtr(_textureKey); + debugPrint("id: $id, texture_key: $_textureKey"); + platformFFI.registerTexture(widget.id, ptr); + _textureId.value = id; + } + }); _ffi.ffiModel.updateEventListener(widget.id); _ffi.qualityMonitorModel.checkShowQualityMonitor(widget.id); // Session option should be set after models.dart/FFI.start @@ -198,6 +212,7 @@ class _RemotePageState extends State Wakelock.disable(); } Get.delete(tag: widget.id); + textureRenderer.closeTexture(_textureKey); super.dispose(); _removeStates(widget.id); } @@ -346,6 +361,7 @@ class _RemotePageState extends State cursorOverImage: _cursorOverImage, keyboardEnabled: _keyboardEnabled, remoteCursorMoved: _remoteCursorMoved, + textureId: _textureId, listenerBuilder: (child) => _buildRawPointerMouseRegion(child, enterView, leaveView), ); @@ -383,6 +399,7 @@ class ImagePaint extends StatefulWidget { final RxBool cursorOverImage; final RxBool keyboardEnabled; final RxBool remoteCursorMoved; + final RxInt textureId; final Widget Function(Widget)? listenerBuilder; ImagePaint( @@ -392,6 +409,7 @@ class ImagePaint extends StatefulWidget { required this.cursorOverImage, required this.keyboardEnabled, required this.remoteCursorMoved, + required this.textureId, this.listenerBuilder}) : super(key: key); @@ -466,9 +484,15 @@ class _ImagePaintState extends State { final imageWidth = c.getDisplayWidth() * s; final imageHeight = c.getDisplayHeight() * s; final imageSize = Size(imageWidth, imageHeight); - final imageWidget = CustomPaint( - size: imageSize, - painter: ImagePainter(image: m.image, x: 0, y: 0, scale: s), + print("width: $imageWidth/$imageHeight"); + // final imageWidget = CustomPaint( + // size: imageSize, + // painter: ImagePainter(image: m.image, x: 0, y: 0, scale: s), + // ); + final imageWidget = SizedBox( + width: imageHeight, + height: imageHeight, + child: Obx(() => Texture(textureId: widget.textureId.value)), ); return NotificationListener( @@ -493,9 +517,15 @@ class _ImagePaintState extends State { context, _buildListener(imageWidget), c.size, imageSize)), )); } else { - final imageWidget = CustomPaint( - size: Size(c.size.width, c.size.height), - painter: ImagePainter(image: m.image, x: c.x / s, y: c.y / s, scale: s), + // final imageWidget = CustomPaint( + // size: Size(c.size.width, c.size.height), + // painter: ImagePainter(image: m.image, x: c.x / s, y: c.y / s, scale: s), + // ); + final imageWidget = Center( + child: AspectRatio( + aspectRatio: c.size.width / c.size.height, + child: Obx(() => Texture(textureId: widget.textureId.value)), + ), ); return mouseRegion(child: _buildListener(imageWidget)); } diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 1afb5b147..0b6f14636 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -1446,14 +1446,15 @@ class FFI { } } else if (message is EventToUI_Rgba) { // Fetch the image buffer from rust codes. - final sz = platformFFI.getRgbaSize(id); - if (sz == null || sz == 0) { - return; - } - final rgba = platformFFI.getRgba(id, sz); - if (rgba != null) { - imageModel.onRgba(rgba); - } + // final sz = platformFFI.getRgbaSize(id); + // if (sz == null || sz == 0) { + // return; + // } + // final rgba = platformFFI.getRgba(id, sz); + // if (rgba != null) { + // imageModel.onRgba(rgba); + // } + // imageModel.onRgba(rgba); } } debugPrint('Exit session event loop'); diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index ba62b775e..13f5b4587 100644 --- a/flutter/lib/models/native_model.dart +++ b/flutter/lib/models/native_model.dart @@ -30,6 +30,9 @@ typedef F4Dart = int Function(Pointer); typedef F5 = Void Function(Pointer); typedef F5Dart = void Function(Pointer); typedef HandleEvent = Future Function(Map evt); +// pub fn session_register_texture(id: *const char, ptr: usize) +typedef F6 = Void Function(Pointer, Uint64); +typedef F6Dart = void Function(Pointer, int); /// FFI wrapper around the native Rust core. /// Hides the platform differences. @@ -52,6 +55,8 @@ class PlatformFFI { F3? _session_get_rgba; F4Dart? _session_get_rgba_size; F5Dart? _session_next_rgba; + F6Dart? _session_register_texture; + static get localeName => Platform.localeName; @@ -130,6 +135,13 @@ class PlatformFFI { malloc.free(a); } + void registerTexture(String id, int ptr) { + if (_session_register_texture == null) return; + final a = id.toNativeUtf8(); + _session_register_texture!(a, ptr); + malloc.free(a); + } + /// Init the FFI class, loads the native Rust core library. Future init(String appType) async { _appType = appType; @@ -150,6 +162,7 @@ class PlatformFFI { dylib.lookupFunction("session_get_rgba_size"); _session_next_rgba = dylib.lookupFunction("session_next_rgba"); + _session_register_texture = dylib.lookupFunction("session_register_texture"); try { // SYSTEM user failed _dir = (await getApplicationDocumentsDirectory()).path; diff --git a/src/flutter.rs b/src/flutter.rs index 2888ffe75..f5d764e66 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -8,7 +8,7 @@ use hbb_common::{ bail, config::LocalConfig, get_version_number, message_proto::*, rendezvous_proto::ConnType, ResultType, }; -use libc::c_void; +use libc::{c_void}; use libloading::{Library, Symbol}; use serde_json::json; use std::sync::atomic::{AtomicBool, Ordering}; @@ -37,7 +37,7 @@ lazy_static::lazy_static! { #[cfg(target_os = "macos")] let lib = Library::new("texture_rgba_renderer_plugin.dylib"); #[cfg(target_os = "linux")] - let lib = Library::new("texture_rgba_renderer_plugin.so"); + let lib = Library::new("libtexture_rgba_renderer_plugin.so"); lib.expect("`libtexture_rgba_renderer_plugin` not found, please add `texture_rgba_renderer` in your flutter project") } }; @@ -129,7 +129,8 @@ pub struct FlutterHandler { // We must check the `rgba_valid` before reading [rgba]. pub rgba: Arc>>, pub rgba_valid: Arc, - pub renderer: VideoRenderer, + pub renderer: Arc>, + peer_info: Arc> } pub type FlutterRgbaRendererPluginOnRgba = unsafe extern "C" fn(texture_rgba: *mut c_void, buffer: *const u8, width: c_int, height: c_int); @@ -148,10 +149,12 @@ impl Default for VideoRenderer { fn default() -> Self { unsafe { Self { + ptr: 0, + width: 0, + height: 0, on_rgba_func: TEXURE_RGBA_RENDERER_PLUGIN .get::(b"FlutterRgbaRendererPluginOnRgba") .expect("Symbol FlutterRgbaRendererPluginOnRgba not found."), - ..Default::default() } } } @@ -220,7 +223,7 @@ impl FlutterHandler { } pub fn register_texture(&mut self, ptr: usize) { - self.renderer.ptr = ptr; + self.renderer.write().unwrap().ptr = ptr; } } @@ -381,6 +384,7 @@ impl InvokeUiSession for FlutterHandler { // unused in flutter fn adapt_size(&self) {} + #[inline] 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. @@ -390,13 +394,15 @@ impl InvokeUiSession for FlutterHandler { self.rgba_valid.store(true, Ordering::Relaxed); // Return the rgba buffer to the video handler for reusing allocated rgba buffer. std::mem::swap::>(data, &mut *self.rgba.write().unwrap()); - #[cfg(any(target_os = "android", target_os = "ios"))] if let Some(stream) = &*self.event_stream.read().unwrap() { stream.add(EventToUI::Rgba); } #[cfg(not(any(target_os = "android", target_os = "ios")))] - self.renderer + { + self.renderer.read().unwrap() .on_rgba(self.rgba.read().unwrap().as_ptr()); + self.next_rgba(); + } } fn set_peer_info(&self, pi: &PeerInfo) { @@ -410,6 +416,9 @@ impl InvokeUiSession for FlutterHandler { features.insert("privacy_mode", 0); } let features = serde_json::ser::to_string(&features).unwrap_or("".to_owned()); + *self.peer_info.write().unwrap() = pi.clone(); + let curr_display = &pi.displays[pi.current_display as usize]; + self.renderer.write().unwrap().set_size(curr_display.width, curr_display.height); self.push_event( "peer_info", vec![ @@ -426,6 +435,7 @@ impl InvokeUiSession for FlutterHandler { } fn set_displays(&self, displays: &Vec) { + self.peer_info.write().unwrap().displays = displays.clone(); self.push_event( "sync_peer_info", vec![("displays", &Self::make_displays_msg(displays))], @@ -457,6 +467,8 @@ impl InvokeUiSession for FlutterHandler { } fn switch_display(&self, display: &SwitchDisplay) { + let curr_display = &self.peer_info.read().unwrap().displays[display.display as usize]; + self.renderer.write().unwrap().set_size(curr_display.width, curr_display.height); self.push_event( "switch_display", vec![ @@ -521,7 +533,7 @@ impl InvokeUiSession for FlutterHandler { } #[inline] - fn next_rgba(&mut self) { + fn next_rgba(&self) { self.rgba_valid.store(false, Ordering::Relaxed); } } diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 4794efb65..7b31c84e9 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -298,7 +298,7 @@ impl InvokeUiSession for SciterHandler { std::ptr::null() } - fn next_rgba(&mut self) {} + fn next_rgba(&self) {} } pub struct SciterSession(Session); diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 5a83ee572..5fbf2f4e7 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -798,7 +798,7 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { fn on_voice_call_waiting(&self); fn on_voice_call_incoming(&self); fn get_rgba(&self) -> *const u8; - fn next_rgba(&mut self); + fn next_rgba(&self); } impl Deref for Session { From 5acedecf0c09546ec368ca22fa7367ec7b9c0ae5 Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 21 Feb 2023 21:56:46 +0800 Subject: [PATCH 1972/2015] texture paint Signed-off-by: fufesou --- flutter/lib/desktop/pages/remote_page.dart | 33 ++++++--- flutter/lib/models/model.dart | 45 +++++++---- src/flutter.rs | 86 ++++++++++++++-------- src/flutter_ffi.rs | 6 ++ 4 files changed, 115 insertions(+), 55 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index df9874172..4a2f5c0e8 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -126,9 +126,9 @@ class _RemotePageState extends State // Register texture. _textureId.value = -1; textureRenderer.createTexture(_textureKey).then((id) async { + debugPrint("id: $id, texture_key: $_textureKey"); if (id != -1) { final ptr = await textureRenderer.getTexturePtr(_textureKey); - debugPrint("id: $id, texture_key: $_textureKey"); platformFFI.registerTexture(widget.id, ptr); _textureId.value = id; } @@ -197,6 +197,8 @@ class _RemotePageState extends State @override void dispose() { debugPrint("REMOTE PAGE dispose ${widget.id}"); + platformFFI.registerTexture(widget.id, 0); + textureRenderer.closeTexture(_textureKey); // ensure we leave this session, this is a double check bind.sessionEnterOrLeave(id: widget.id, enter: false); DesktopMultiWindow.removeListener(this); @@ -212,7 +214,6 @@ class _RemotePageState extends State Wakelock.disable(); } Get.delete(tag: widget.id); - textureRenderer.closeTexture(_textureKey); super.dispose(); _removeStates(widget.id); } @@ -484,15 +485,14 @@ class _ImagePaintState extends State { final imageWidth = c.getDisplayWidth() * s; final imageHeight = c.getDisplayHeight() * s; final imageSize = Size(imageWidth, imageHeight); - print("width: $imageWidth/$imageHeight"); // final imageWidget = CustomPaint( // size: imageSize, // painter: ImagePainter(image: m.image, x: 0, y: 0, scale: s), // ); final imageWidget = SizedBox( - width: imageHeight, + width: imageWidth, height: imageHeight, - child: Obx(() => Texture(textureId: widget.textureId.value)), + child: Obx(() => Texture(textureId: widget.textureId.value)), ); return NotificationListener( @@ -521,13 +521,22 @@ class _ImagePaintState extends State { // size: Size(c.size.width, c.size.height), // painter: ImagePainter(image: m.image, x: c.x / s, y: c.y / s, scale: s), // ); - final imageWidget = Center( - child: AspectRatio( - aspectRatio: c.size.width / c.size.height, - child: Obx(() => Texture(textureId: widget.textureId.value)), - ), - ); - return mouseRegion(child: _buildListener(imageWidget)); + if (c.size.width > 0 && c.size.height > 0) { + final imageWidget = Stack( + children: [ + Positioned( + left: c.x, + top: c.y, + width: c.getDisplayWidth() * s, + height: c.getDisplayHeight() * s, + child: Texture(textureId: widget.textureId.value), + ) + ], + ); + return mouseRegion(child: _buildListener(imageWidget)); + } else { + return Container(); + } } } diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 0b6f14636..a38db2a90 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -252,6 +252,8 @@ class FfiModel with ChangeNotifier { parent.target?.cursorModel.updateDisplayOrigin(_display.x, _display.y); } + _updateSessionWidthHeight(peerId, display.width, display.height); + try { CurrentDisplayState.find(peerId).value = _pi.currentDisplay; } catch (e) { @@ -367,6 +369,10 @@ class FfiModel with ChangeNotifier { }); } + _updateSessionWidthHeight(String id, int width, int height) { + bind.sessionSetSize(id: id, width: display.width, height: display.height); + } + /// 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) @@ -420,6 +426,7 @@ class FfiModel with ChangeNotifier { stateGlobal.displaysCount.value = _pi.displays.length; if (_pi.currentDisplay < _pi.displays.length) { _display = _pi.displays[_pi.currentDisplay]; + _updateSessionWidthHeight(peerId, display.width, display.height); } if (displays.isNotEmpty) { parent.target?.dialogManager.showLoading( @@ -485,19 +492,18 @@ class ImageModel with ChangeNotifier { WeakReference parent; - final List _callbacksOnFirstImage = []; + final List callbacksOnFirstImage = []; ImageModel(this.parent); - addCallbackOnFirstImage(Function(String) cb) => - _callbacksOnFirstImage.add(cb); + addCallbackOnFirstImage(Function(String) cb) => callbacksOnFirstImage.add(cb); onRgba(Uint8List rgba) { if (_waitForImage[id]!) { _waitForImage[id] = false; parent.target?.dialogManager.dismissAll(); if (isDesktop) { - for (final cb in _callbacksOnFirstImage) { + for (final cb in callbacksOnFirstImage) { cb(id); } } @@ -1445,16 +1451,27 @@ class FFI { debugPrint('json.decode fail1(): $e, ${message.field0}'); } } else if (message is EventToUI_Rgba) { - // Fetch the image buffer from rust codes. - // final sz = platformFFI.getRgbaSize(id); - // if (sz == null || sz == 0) { - // return; - // } - // final rgba = platformFFI.getRgba(id, sz); - // if (rgba != null) { - // imageModel.onRgba(rgba); - // } - // imageModel.onRgba(rgba); + if (Platform.isAndroid || Platform.isIOS) { + // Fetch the image buffer from rust codes. + final sz = platformFFI.getRgbaSize(id); + if (sz == null || sz == 0) { + return; + } + final rgba = platformFFI.getRgba(id, sz); + if (rgba != null) { + imageModel.onRgba(rgba); + } + } else { + if (_waitForImage[id]!) { + _waitForImage[id] = false; + dialogManager.dismissAll(); + for (final cb in imageModel.callbacksOnFirstImage) { + cb(id); + } + await canvasModel.updateViewStyle(); + await canvasModel.updateScrollStyle(); + } + } } } debugPrint('Exit session event loop'); diff --git a/src/flutter.rs b/src/flutter.rs index f5d764e66..a5689bce6 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -5,12 +5,13 @@ use crate::{ }; use flutter_rust_bridge::StreamSink; use hbb_common::{ - bail, config::LocalConfig, get_version_number, message_proto::*, rendezvous_proto::ConnType, - ResultType, + bail, config::LocalConfig, get_version_number, libc::c_void, message_proto::*, + rendezvous_proto::ConnType, ResultType, }; -use libc::{c_void}; use libloading::{Library, Symbol}; use serde_json::json; + +#[cfg(any(target_os = "android", target_os = "ios"))] use std::sync::atomic::{AtomicBool, Ordering}; use std::{ collections::HashMap, @@ -30,7 +31,7 @@ lazy_static::lazy_static! { pub static ref SESSIONS: RwLock>> = Default::default(); pub static ref GLOBAL_EVENT_STREAM: RwLock>> = Default::default(); // rust to dart event channel #[cfg(not(any(target_os = "ios", target_os = "android")))] - pub static ref TEXURE_RGBA_RENDERER_PLUGIN: Library = { + pub static ref TEXTURE_RGBA_RENDERER_PLUGIN: Library = { unsafe { #[cfg(target_os = "windows")] let lib = Library::new("texture_rgba_renderer_plugin.dll"); @@ -127,21 +128,26 @@ 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]. + #[cfg(any(target_os = "android", target_os = "ios"))] pub rgba: Arc>>, + #[cfg(any(target_os = "android", target_os = "ios"))] pub rgba_valid: Arc, - pub renderer: Arc>, - peer_info: Arc> + #[cfg(not(any(target_os = "android", target_os = "ios")))] + notify_rendered: Arc>, + renderer: Arc>, + peer_info: Arc>, } pub type FlutterRgbaRendererPluginOnRgba = unsafe extern "C" fn(texture_rgba: *mut c_void, buffer: *const u8, width: c_int, height: c_int); // Video Texture Renderer in Flutter #[derive(Clone)] -pub struct VideoRenderer { +struct VideoRenderer { // TextureRgba pointer in flutter native. ptr: usize, width: i32, height: i32, + data_len: usize, on_rgba_func: Symbol<'static, FlutterRgbaRendererPluginOnRgba>, } @@ -152,7 +158,8 @@ impl Default for VideoRenderer { ptr: 0, width: 0, height: 0, - on_rgba_func: TEXURE_RGBA_RENDERER_PLUGIN + data_len: 0, + on_rgba_func: TEXTURE_RGBA_RENDERER_PLUGIN .get::(b"FlutterRgbaRendererPluginOnRgba") .expect("Symbol FlutterRgbaRendererPluginOnRgba not found."), } @@ -161,24 +168,30 @@ impl Default for VideoRenderer { } impl VideoRenderer { - pub fn new(ptr: usize) -> Self { - Self { - ptr, - ..Default::default() - } - } - + #[inline] pub fn set_size(&mut self, width: i32, height: i32) { self.width = width; self.height = height; + self.data_len = if width > 0 && height > 0 { + (width * height * 4) as usize + } else { + 0 + }; } - pub fn on_rgba(&self, rgba: *const u8) { - if self.ptr == usize::default() { + pub fn on_rgba(&self, rgba: &Vec) { + if self.ptr == usize::default() || rgba.len() != self.data_len { return; } let func = self.on_rgba_func.clone(); - unsafe {func(self.ptr as _, rgba, self.width as _, self.height as _)}; + unsafe { + func( + self.ptr as _, + rgba.as_ptr() as _, + self.width as _, + self.height as _, + ) + }; } } @@ -222,9 +235,16 @@ impl FlutterHandler { serde_json::ser::to_string(&msg_vec).unwrap_or("".to_owned()) } + #[inline] pub fn register_texture(&mut self, ptr: usize) { self.renderer.write().unwrap().ptr = ptr; } + + #[inline] + pub fn set_size(&mut self, width: i32, height: i32) { + *self.notify_rendered.write().unwrap() = false; + self.renderer.write().unwrap().set_size(width, height); + } } impl InvokeUiSession for FlutterHandler { @@ -385,6 +405,7 @@ impl InvokeUiSession for FlutterHandler { fn adapt_size(&self) {} #[inline] + #[cfg(any(target_os = "android", target_os = "ios"))] 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. @@ -397,11 +418,18 @@ impl InvokeUiSession for FlutterHandler { if let Some(stream) = &*self.event_stream.read().unwrap() { stream.add(EventToUI::Rgba); } - #[cfg(not(any(target_os = "android", target_os = "ios")))] - { - self.renderer.read().unwrap() - .on_rgba(self.rgba.read().unwrap().as_ptr()); - self.next_rgba(); + } + + #[inline] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + fn on_rgba(&self, data: &mut Vec) { + self.renderer.read().unwrap().on_rgba(data); + if *self.notify_rendered.read().unwrap() { + return; + } + if let Some(stream) = &*self.event_stream.read().unwrap() { + stream.add(EventToUI::Rgba); + *self.notify_rendered.write().unwrap() = true; } } @@ -417,8 +445,6 @@ impl InvokeUiSession for FlutterHandler { } let features = serde_json::ser::to_string(&features).unwrap_or("".to_owned()); *self.peer_info.write().unwrap() = pi.clone(); - let curr_display = &pi.displays[pi.current_display as usize]; - self.renderer.write().unwrap().set_size(curr_display.width, curr_display.height); self.push_event( "peer_info", vec![ @@ -467,8 +493,6 @@ impl InvokeUiSession for FlutterHandler { } fn switch_display(&self, display: &SwitchDisplay) { - let curr_display = &self.peer_info.read().unwrap().displays[display.display as usize]; - self.renderer.write().unwrap().set_size(curr_display.width, curr_display.height); self.push_event( "switch_display", vec![ @@ -526,6 +550,7 @@ impl InvokeUiSession for FlutterHandler { #[inline] fn get_rgba(&self) -> *const u8 { + #[cfg(any(target_os = "android", target_os = "ios"))] if self.rgba_valid.load(Ordering::Relaxed) { return self.rgba.read().unwrap().as_ptr(); } @@ -534,6 +559,7 @@ impl InvokeUiSession for FlutterHandler { #[inline] fn next_rgba(&self) { + #[cfg(any(target_os = "android", target_os = "ios"))] self.rgba_valid.store(false, Ordering::Relaxed); } } @@ -793,8 +819,10 @@ pub fn set_cur_session_id(id: String) { } #[no_mangle] -pub fn session_get_rgba_size(id: *const char) -> usize { - let id = unsafe { std::ffi::CStr::from_ptr(id as _) }; +pub fn session_get_rgba_size(_id: *const char) -> usize { + #[cfg(any(target_os = "android", target_os = "ios"))] + let id = unsafe { std::ffi::CStr::from_ptr(_id as _) }; + #[cfg(any(target_os = "android", target_os = "ios"))] if let Ok(id) = id.to_str() { if let Some(session) = SESSIONS.write().unwrap().get_mut(id) { return session.rgba.read().unwrap().len(); diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 7eeb96b5c..c55866dbe 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -529,6 +529,12 @@ pub fn session_switch_sides(id: String) { } } +pub fn session_set_size(id: String, width: i32, height: i32) { + if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) { + session.set_size(width, height); + } +} + pub fn main_get_sound_inputs() -> Vec { #[cfg(not(any(target_os = "android", target_os = "ios")))] return get_sound_inputs(); From 77c4a14845604775751359b51cc5be2b8ae90c23 Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 21 Feb 2023 23:46:13 +0800 Subject: [PATCH 1973/2015] flutter texture render, mid commit Signed-off-by: fufesou --- flutter/lib/desktop/pages/remote_page.dart | 61 ++++---- flutter/lib/models/model.dart | 23 +-- libs/scrap/src/common/codec.rs | 26 ++-- libs/scrap/src/common/convert.rs | 158 ++++++++++++++++----- libs/scrap/src/common/hwcodec.rs | 22 ++- libs/scrap/src/common/mediacodec.rs | 52 +++++-- libs/scrap/src/common/mod.rs | 7 + libs/scrap/src/common/vpxcodec.rs | 82 +++++++---- src/client.rs | 7 +- src/flutter.rs | 16 ++- src/flutter_ffi.rs | 11 ++ 11 files changed, 322 insertions(+), 143 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 4a2f5c0e8..c78ffb439 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -65,6 +65,7 @@ class _RemotePageState extends State late RxBool _keyboardEnabled; late RxInt _textureId; late int _textureKey; + final useTextureRender = bind.mainUseTextureRender(); final _blockableOverlayState = BlockableOverlayState(); @@ -363,6 +364,7 @@ class _RemotePageState extends State keyboardEnabled: _keyboardEnabled, remoteCursorMoved: _remoteCursorMoved, textureId: _textureId, + useTextureRender: useTextureRender, listenerBuilder: (child) => _buildRawPointerMouseRegion(child, enterView, leaveView), ); @@ -401,6 +403,7 @@ class ImagePaint extends StatefulWidget { final RxBool keyboardEnabled; final RxBool remoteCursorMoved; final RxInt textureId; + final bool useTextureRender; final Widget Function(Widget)? listenerBuilder; ImagePaint( @@ -411,6 +414,7 @@ class ImagePaint extends StatefulWidget { required this.keyboardEnabled, required this.remoteCursorMoved, required this.textureId, + required this.useTextureRender, this.listenerBuilder}) : super(key: key); @@ -485,15 +489,19 @@ class _ImagePaintState extends State { final imageWidth = c.getDisplayWidth() * s; final imageHeight = c.getDisplayHeight() * s; final imageSize = Size(imageWidth, imageHeight); - // final imageWidget = CustomPaint( - // size: imageSize, - // painter: ImagePainter(image: m.image, x: 0, y: 0, scale: s), - // ); - final imageWidget = SizedBox( - width: imageWidth, - height: imageHeight, - child: Obx(() => Texture(textureId: widget.textureId.value)), - ); + late final Widget imageWidget; + if (widget.useTextureRender) { + imageWidget = SizedBox( + width: imageWidth, + height: imageHeight, + child: Obx(() => Texture(textureId: widget.textureId.value)), + ); + } else { + imageWidget = CustomPaint( + size: imageSize, + painter: ImagePainter(image: m.image, x: 0, y: 0, scale: s), + ); + } return NotificationListener( onNotification: (notification) { @@ -517,22 +525,27 @@ class _ImagePaintState extends State { context, _buildListener(imageWidget), c.size, imageSize)), )); } else { - // final imageWidget = CustomPaint( - // size: Size(c.size.width, c.size.height), - // painter: ImagePainter(image: m.image, x: c.x / s, y: c.y / s, scale: s), - // ); + late final Widget imageWidget; if (c.size.width > 0 && c.size.height > 0) { - final imageWidget = Stack( - children: [ - Positioned( - left: c.x, - top: c.y, - width: c.getDisplayWidth() * s, - height: c.getDisplayHeight() * s, - child: Texture(textureId: widget.textureId.value), - ) - ], - ); + if (widget.useTextureRender) { + imageWidget = Stack( + children: [ + Positioned( + left: c.x, + top: c.y, + width: c.getDisplayWidth() * s, + height: c.getDisplayHeight() * s, + child: Texture(textureId: widget.textureId.value), + ) + ], + ); + } else { + imageWidget = CustomPaint( + size: Size(c.size.width, c.size.height), + painter: + ImagePainter(image: m.image, x: c.x / s, y: c.y / s, scale: s), + ); + } return mouseRegion(child: _buildListener(imageWidget)); } else { return Container(); diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index a38db2a90..5ef72a0af 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -1438,6 +1438,7 @@ class FFI { final stream = bind.sessionStart(id: id); final cb = ffiModel.startEventListener(id); () async { + final useTextureRender = bind.mainUseTextureRender(); // Preserved for the rgba data. await for (final message in stream) { if (message is EventToUI_Event) { @@ -1451,17 +1452,7 @@ class FFI { debugPrint('json.decode fail1(): $e, ${message.field0}'); } } else if (message is EventToUI_Rgba) { - if (Platform.isAndroid || Platform.isIOS) { - // Fetch the image buffer from rust codes. - final sz = platformFFI.getRgbaSize(id); - if (sz == null || sz == 0) { - return; - } - final rgba = platformFFI.getRgba(id, sz); - if (rgba != null) { - imageModel.onRgba(rgba); - } - } else { + if (useTextureRender) { if (_waitForImage[id]!) { _waitForImage[id] = false; dialogManager.dismissAll(); @@ -1471,6 +1462,16 @@ class FFI { await canvasModel.updateViewStyle(); await canvasModel.updateScrollStyle(); } + } else { + // Fetch the image buffer from rust codes. + final sz = platformFFI.getRgbaSize(id); + if (sz == null || sz == 0) { + return; + } + final rgba = platformFFI.getRgba(id, sz); + if (rgba != null) { + imageModel.onRgba(rgba); + } } } } diff --git a/libs/scrap/src/common/codec.rs b/libs/scrap/src/common/codec.rs index acfd4c674..3adc24a14 100644 --- a/libs/scrap/src/common/codec.rs +++ b/libs/scrap/src/common/codec.rs @@ -11,7 +11,7 @@ use crate::hwcodec::*; use crate::mediacodec::{ MediaCodecDecoder, MediaCodecDecoders, H264_DECODER_SUPPORT, H265_DECODER_SUPPORT, }; -use crate::vpxcodec::*; +use crate::{vpxcodec::*, ImageFormat}; use hbb_common::{ anyhow::anyhow, @@ -306,16 +306,17 @@ impl Decoder { pub fn handle_video_frame( &mut self, frame: &video_frame::Union, + fmt: ImageFormat, rgb: &mut Vec, ) -> ResultType { match frame { video_frame::Union::Vp9s(vp9s) => { - Decoder::handle_vp9s_video_frame(&mut self.vpx, vp9s, rgb) + Decoder::handle_vp9s_video_frame(&mut self.vpx, vp9s, fmt, rgb) } #[cfg(feature = "hwcodec")] video_frame::Union::H264s(h264s) => { if let Some(decoder) = &mut self.hw.h264 { - Decoder::handle_hw_video_frame(decoder, h264s, rgb, &mut self.i420) + Decoder::handle_hw_video_frame(decoder, h264s, fmt, rgb, &mut self.i420) } else { Err(anyhow!("don't support h264!")) } @@ -323,7 +324,7 @@ impl Decoder { #[cfg(feature = "hwcodec")] video_frame::Union::H265s(h265s) => { if let Some(decoder) = &mut self.hw.h265 { - Decoder::handle_hw_video_frame(decoder, h265s, rgb, &mut self.i420) + Decoder::handle_hw_video_frame(decoder, h265s, fmt, rgb, &mut self.i420) } else { Err(anyhow!("don't support h265!")) } @@ -331,7 +332,7 @@ impl Decoder { #[cfg(feature = "mediacodec")] video_frame::Union::H264s(h264s) => { if let Some(decoder) = &mut self.media_codec.h264 { - Decoder::handle_mediacodec_video_frame(decoder, h264s, rgb) + Decoder::handle_mediacodec_video_frame(decoder, h264s, fmt, rgb) } else { Err(anyhow!("don't support h264!")) } @@ -339,7 +340,7 @@ impl Decoder { #[cfg(feature = "mediacodec")] video_frame::Union::H265s(h265s) => { if let Some(decoder) = &mut self.media_codec.h265 { - Decoder::handle_mediacodec_video_frame(decoder, h265s, rgb) + Decoder::handle_mediacodec_video_frame(decoder, h265s, fmt, rgb) } else { Err(anyhow!("don't support h265!")) } @@ -351,6 +352,7 @@ impl Decoder { fn handle_vp9s_video_frame( decoder: &mut VpxDecoder, vp9s: &EncodedVideoFrames, + fmt: ImageFormat, rgb: &mut Vec, ) -> ResultType { let mut last_frame = Image::new(); @@ -367,7 +369,7 @@ impl Decoder { if last_frame.is_null() { Ok(false) } else { - last_frame.rgb(1, true, rgb); + last_frame.to(fmt, 1, rgb); Ok(true) } } @@ -376,14 +378,15 @@ impl Decoder { fn handle_hw_video_frame( decoder: &mut HwDecoder, frames: &EncodedVideoFrames, - rgb: &mut Vec, + fmt: ImageFormat, + raw: &mut Vec, i420: &mut Vec, ) -> ResultType { let mut ret = false; for h264 in frames.frames.iter() { for image in decoder.decode(&h264.data)? { // TODO: just process the last frame - if image.bgra(rgb, i420).is_ok() { + if image.to_fmt(fmt, raw, i420).is_ok() { ret = true; } } @@ -395,11 +398,12 @@ impl Decoder { fn handle_mediacodec_video_frame( decoder: &mut MediaCodecDecoder, frames: &EncodedVideoFrames, - rgb: &mut Vec, + fmt: ImageFormat, + raw: &mut Vec, ) -> ResultType { let mut ret = false; for h264 in frames.frames.iter() { - return decoder.decode(&h264.data, rgb); + return decoder.decode(&h264.data, fmt, raw); } return Ok(false); } diff --git a/libs/scrap/src/common/convert.rs b/libs/scrap/src/common/convert.rs index 2b0223a0a..a2177805e 100644 --- a/libs/scrap/src/common/convert.rs +++ b/libs/scrap/src/common/convert.rs @@ -103,6 +103,19 @@ extern "C" { height: c_int, ) -> c_int; + pub fn I420ToABGR( + src_y: *const u8, + src_stride_y: c_int, + src_u: *const u8, + src_stride_u: c_int, + src_v: *const u8, + src_stride_v: c_int, + dst_rgba: *mut u8, + dst_stride_rgba: c_int, + width: c_int, + height: c_int, + ) -> c_int; + pub fn NV12ToARGB( src_y: *const u8, src_stride_y: c_int, @@ -246,6 +259,7 @@ pub unsafe fn nv12_to_i420( #[cfg(feature = "hwcodec")] pub mod hw { use hbb_common::{anyhow::anyhow, ResultType}; + use crate::ImageFormat; #[cfg(target_os = "windows")] use hwcodec::{ffmpeg::ffmpeg_linesize_offset_length, AVPixelFormat}; @@ -315,7 +329,8 @@ pub mod hw { } #[cfg(target_os = "windows")] - pub fn hw_nv12_to_bgra( + pub fn hw_nv12_to( + fmt: ImageFormat, width: usize, height: usize, src_y: &[u8], @@ -355,18 +370,39 @@ pub mod hw { width as _, height as _, ); - super::I420ToARGB( - i420_offset_y, - i420_stride_y, - i420_offset_u, - i420_stride_u, - i420_offset_v, - i420_stride_v, - dst.as_mut_ptr(), - (width * 4) as _, - width as _, - height as _, - ); + match fmt { + ImageFormat::ARGB => { + super::I420ToARGB( + i420_offset_y, + i420_stride_y, + i420_offset_u, + i420_stride_u, + i420_offset_v, + i420_stride_v, + dst.as_mut_ptr(), + (width * 4) as _, + width as _, + height as _, + ); + } + ImageFormat::ABGR => { + super::I420ToABGR( + i420_offset_y, + i420_stride_y, + i420_offset_u, + i420_stride_u, + i420_offset_v, + i420_stride_v, + dst.as_mut_ptr(), + (width * 4) as _, + width as _, + height as _, + ); + } + _ => { + return Err(anyhow!("unsupported image format")); + } + } return Ok(()); }; } @@ -374,7 +410,8 @@ pub mod hw { } #[cfg(not(target_os = "windows"))] - pub fn hw_nv12_to_bgra( + pub fn hw_nv12_to( + fmt: ImageFormat, width: usize, height: usize, src_y: &[u8], @@ -387,23 +424,46 @@ pub mod hw { ) -> ResultType<()> { dst.resize(width * height * 4, 0); unsafe { - match super::NV12ToARGB( - src_y.as_ptr(), - src_stride_y as _, - src_uv.as_ptr(), - src_stride_uv as _, - dst.as_mut_ptr(), - (width * 4) as _, - width as _, - height as _, - ) { - 0 => Ok(()), - _ => Err(anyhow!("NV12ToARGB failed")), + match fmt { + ImageFormat::ARGB => { + match super::NV12ToARGB( + src_y.as_ptr(), + src_stride_y as _, + src_uv.as_ptr(), + src_stride_uv as _, + dst.as_mut_ptr(), + (width * 4) as _, + width as _, + height as _, + ) { + 0 => Ok(()), + _ => Err(anyhow!("NV12ToARGB failed")), + } + } + ImageFormat::ABGR => { + match super::NV12ToABGR( + src_y.as_ptr(), + src_stride_y as _, + src_uv.as_ptr(), + src_stride_uv as _, + dst.as_mut_ptr(), + (width * 4) as _, + width as _, + height as _, + ) { + 0 => Ok(()), + _ => Err(anyhow!("NV12ToABGR failed")), + } + } + _ => { + Err(anyhow!("unsupported image format")); + } } } } - pub fn hw_i420_to_bgra( + pub fn hw_i420_to( + fmt: ImageFormat, width: usize, height: usize, src_y: &[u8], @@ -419,18 +479,38 @@ pub mod hw { let src_v = src_v.as_ptr(); dst.resize(width * height * 4, 0); unsafe { - super::I420ToARGB( - src_y, - src_stride_y as _, - src_u, - src_stride_u as _, - src_v, - src_stride_v as _, - dst.as_mut_ptr(), - (width * 4) as _, - width as _, - height as _, - ); + match fmt { + ImageFormat::ARGB => { + super::I420ToARGB( + src_y, + src_stride_y as _, + src_u, + src_stride_u as _, + src_v, + src_stride_v as _, + dst.as_mut_ptr(), + (width * 4) as _, + width as _, + height as _, + ); + } + ImageFormat::ABGR => { + super::I420ToABGR( + src_y, + src_stride_y as _, + src_u, + src_stride_u as _, + src_v, + src_stride_v as _, + dst.as_mut_ptr(), + (width * 4) as _, + width as _, + height as _, + ); + } + _ => { + } + } }; } } diff --git a/libs/scrap/src/common/hwcodec.rs b/libs/scrap/src/common/hwcodec.rs index 27b157b79..d2b9f414f 100644 --- a/libs/scrap/src/common/hwcodec.rs +++ b/libs/scrap/src/common/hwcodec.rs @@ -1,6 +1,6 @@ use crate::{ codec::{EncoderApi, EncoderCfg}, - hw, HW_STRIDE_ALIGN, + hw, ImageFormat, HW_STRIDE_ALIGN, }; use hbb_common::{ anyhow::{anyhow, Context}, @@ -236,22 +236,24 @@ pub struct HwDecoderImage<'a> { } impl HwDecoderImage<'_> { - pub fn bgra(&self, bgra: &mut Vec, i420: &mut Vec) -> ResultType<()> { + pub fn to_fmt(&self, fmt: ImageFormat, fmt_data: &mut Vec, i420: &mut Vec) -> ResultType<()> { let frame = self.frame; match frame.pixfmt { - AVPixelFormat::AV_PIX_FMT_NV12 => hw::hw_nv12_to_bgra( + AVPixelFormat::AV_PIX_FMT_NV12 => hw::hw_nv12_to( + fmt, frame.width as _, frame.height as _, &frame.data[0], &frame.data[1], frame.linesize[0] as _, frame.linesize[1] as _, - bgra, + fmt_data, i420, HW_STRIDE_ALIGN, ), AVPixelFormat::AV_PIX_FMT_YUV420P => { - hw::hw_i420_to_bgra( + hw::hw_i420_to( + fmt, frame.width as _, frame.height as _, &frame.data[0], @@ -260,12 +262,20 @@ impl HwDecoderImage<'_> { frame.linesize[0] as _, frame.linesize[1] as _, frame.linesize[2] as _, - bgra, + fmt_data, ); return Ok(()); } } } + + pub fn bgra(&self, bgra: &mut Vec, i420: &mut Vec) -> ResultType<()> { + self.to_fmt(ImageFormat::ARGB, bgra, i420) + } + + pub fn rgba(&self, rgba: &mut Vec, i420: &mut Vec) -> ResultType<()> { + self.to_fmt(ImageFormat::ABGR, rgba, i420) + } } fn get_config(k: &str) -> ResultType { diff --git a/libs/scrap/src/common/mediacodec.rs b/libs/scrap/src/common/mediacodec.rs index 406baecb5..77c21ffcf 100644 --- a/libs/scrap/src/common/mediacodec.rs +++ b/libs/scrap/src/common/mediacodec.rs @@ -8,9 +8,10 @@ use std::{ time::Duration, }; +use crate::ImageFormat; use crate::{ codec::{EncoderApi, EncoderCfg}, - I420ToARGB, + I420ToABGR, I420ToARGB, }; /// MediaCodec mime type name @@ -50,7 +51,7 @@ impl MediaCodecDecoder { MediaCodecDecoders { h264, h265 } } - pub fn decode(&mut self, data: &[u8], rgb: &mut Vec) -> ResultType { + pub fn decode(&mut self, data: &[u8], fmt: ImageFormat, raw: &mut Vec) -> ResultType { match self.dequeue_input_buffer(Duration::from_millis(10))? { Some(mut input_buffer) => { let mut buf = input_buffer.buffer_mut(); @@ -83,23 +84,44 @@ impl MediaCodecDecoder { let bps = 4; let u = buf.len() * 2 / 3; let v = buf.len() * 5 / 6; - rgb.resize(h * w * bps, 0); + raw.resize(h * w * bps, 0); let y_ptr = buf.as_ptr(); let u_ptr = buf[u..].as_ptr(); let v_ptr = buf[v..].as_ptr(); unsafe { - I420ToARGB( - y_ptr, - stride, - u_ptr, - stride / 2, - v_ptr, - stride / 2, - rgb.as_mut_ptr(), - (w * bps) as _, - w as _, - h as _, - ); + match fmt { + ImageFormat::ARGB => { + I420ToARGB( + y_ptr, + stride, + u_ptr, + stride / 2, + v_ptr, + stride / 2, + raw.as_mut_ptr(), + (w * bps) as _, + w as _, + h as _, + ); + } + ImageFormat::ARGB => { + I420ToABGR( + y_ptr, + stride, + u_ptr, + stride / 2, + v_ptr, + stride / 2, + raw.as_mut_ptr(), + (w * bps) as _, + w as _, + h as _, + ); + } + _ => { + bail!("Unsupported image format"); + } + } } self.release_output_buffer(output_buffer, false)?; Ok(true) diff --git a/libs/scrap/src/common/mod.rs b/libs/scrap/src/common/mod.rs index 45aafe7c5..c7da57734 100644 --- a/libs/scrap/src/common/mod.rs +++ b/libs/scrap/src/common/mod.rs @@ -43,6 +43,13 @@ pub const HW_STRIDE_ALIGN: usize = 0; // recommended by av_frame_get_buffer pub mod record; mod vpx; +#[derive(Copy, Clone)] +pub enum ImageFormat { + Raw, + ABGR, + ARGB, +} + #[inline] pub fn would_block_if_equal(old: &mut Vec, b: &[u8]) -> std::io::Result<()> { // does this really help? diff --git a/libs/scrap/src/common/vpxcodec.rs b/libs/scrap/src/common/vpxcodec.rs index 5164886a1..7a65b193d 100644 --- a/libs/scrap/src/common/vpxcodec.rs +++ b/libs/scrap/src/common/vpxcodec.rs @@ -6,8 +6,8 @@ use hbb_common::anyhow::{anyhow, Context}; use hbb_common::message_proto::{EncodedVideoFrame, EncodedVideoFrames, Message, VideoFrame}; use hbb_common::{get_time, ResultType}; -use crate::codec::EncoderApi; use crate::STRIDE_ALIGN; +use crate::{codec::EncoderApi, ImageFormat}; use super::vpx::{vp8e_enc_control_id::*, vpx_codec_err_t::*, *}; use hbb_common::bytes::Bytes; @@ -417,7 +417,7 @@ impl VpxDecoder { Ok(Self { ctx }) } - pub fn decode2rgb(&mut self, data: &[u8], rgba: bool) -> Result> { + pub fn decode2rgb(&mut self, data: &[u8], fmt: ImageFormat) -> Result> { let mut img = Image::new(); for frame in self.decode(data)? { drop(img); @@ -431,7 +431,7 @@ impl VpxDecoder { Ok(Vec::new()) } else { let mut out = Default::default(); - img.rgb(1, rgba, &mut out); + img.to(fmt, 1, &mut out); Ok(out) } } @@ -539,40 +539,60 @@ impl Image { self.inner().stride[iplane] } - pub fn rgb(&self, stride_align: usize, rgba: bool, dst: &mut Vec) { + pub fn to(&self, fmt: ImageFormat, stride_align: usize, dst: &mut Vec) { let h = self.height(); let mut w = self.width(); - let bps = if rgba { 4 } else { 3 }; + let bps = match fmt { + ImageFormat::Raw => 3, + ImageFormat::ARGB | ImageFormat::ABGR => 4, + }; w = (w + stride_align - 1) & !(stride_align - 1); dst.resize(h * w * bps, 0); let img = self.inner(); unsafe { - if rgba { - super::I420ToARGB( - img.planes[0], - img.stride[0], - img.planes[1], - img.stride[1], - img.planes[2], - img.stride[2], - dst.as_mut_ptr(), - (w * bps) as _, - self.width() as _, - self.height() as _, - ); - } else { - super::I420ToRAW( - img.planes[0], - img.stride[0], - img.planes[1], - img.stride[1], - img.planes[2], - img.stride[2], - dst.as_mut_ptr(), - (w * bps) as _, - self.width() as _, - self.height() as _, - ); + match fmt { + ImageFormat::Raw => { + super::I420ToRAW( + img.planes[0], + img.stride[0], + img.planes[1], + img.stride[1], + img.planes[2], + img.stride[2], + dst.as_mut_ptr(), + (w * bps) as _, + self.width() as _, + self.height() as _, + ); + } + ImageFormat::ARGB => { + super::I420ToARGB( + img.planes[0], + img.stride[0], + img.planes[1], + img.stride[1], + img.planes[2], + img.stride[2], + dst.as_mut_ptr(), + (w * bps) as _, + self.width() as _, + self.height() as _, + ); + } + ImageFormat::ABGR => { + super::I420ToABGR( + img.planes[0], + img.stride[0], + img.planes[1], + img.stride[1], + img.planes[2], + img.stride[2], + dst.as_mut_ptr(), + (w * bps) as _, + self.width() as _, + self.height() as _, + ); + } } } } diff --git a/src/client.rs b/src/client.rs index aa3523185..9f4cef831 100644 --- a/src/client.rs +++ b/src/client.rs @@ -45,6 +45,7 @@ use scrap::{ codec::{Decoder, DecoderCfg}, record::{Recorder, RecorderContext}, VpxDecoderConfig, VpxVideoCodecId, + ImageFormat, }; use crate::{ @@ -943,7 +944,11 @@ impl VideoHandler { } match &vf.union { Some(frame) => { - let res = self.decoder.handle_video_frame(frame, &mut self.rgb); + #[cfg(feature = "flutter_texture_render")] + let fmt = ImageFormat::ARGB; + #[cfg(not(feature = "flutter_texture_render"))] + let fmt = ImageFormat::ABGR; + let res = self.decoder.handle_video_frame(frame, fmt, &mut self.rgb); if self.record { self.recorder .lock() diff --git a/src/flutter.rs b/src/flutter.rs index a5689bce6..f78e1bd92 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -128,19 +128,21 @@ 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]. - #[cfg(any(target_os = "android", target_os = "ios"))] + #[cfg(not(feature = "flutter_texture_render"))] pub rgba: Arc>>, - #[cfg(any(target_os = "android", target_os = "ios"))] + #[cfg(not(feature = "flutter_texture_render"))] pub rgba_valid: Arc, - #[cfg(not(any(target_os = "android", target_os = "ios")))] + #[cfg(feature = "flutter_texture_render")] notify_rendered: Arc>, renderer: Arc>, peer_info: Arc>, } +#[cfg(feature = "flutter_texture_render")] pub type FlutterRgbaRendererPluginOnRgba = unsafe extern "C" fn(texture_rgba: *mut c_void, buffer: *const u8, width: c_int, height: c_int); // Video Texture Renderer in Flutter +#[cfg(feature = "flutter_texture_render")] #[derive(Clone)] struct VideoRenderer { // TextureRgba pointer in flutter native. @@ -151,6 +153,7 @@ struct VideoRenderer { on_rgba_func: Symbol<'static, FlutterRgbaRendererPluginOnRgba>, } +#[cfg(feature = "flutter_texture_render")] impl Default for VideoRenderer { fn default() -> Self { unsafe { @@ -167,6 +170,7 @@ impl Default for VideoRenderer { } } +#[cfg(feature = "flutter_texture_render")] impl VideoRenderer { #[inline] pub fn set_size(&mut self, width: i32, height: i32) { @@ -236,11 +240,13 @@ impl FlutterHandler { } #[inline] + #[cfg(feature = "flutter_texture_render")] pub fn register_texture(&mut self, ptr: usize) { self.renderer.write().unwrap().ptr = ptr; } #[inline] + #[cfg(feature = "flutter_texture_render")] pub fn set_size(&mut self, width: i32, height: i32) { *self.notify_rendered.write().unwrap() = false; self.renderer.write().unwrap().set_size(width, height); @@ -405,7 +411,7 @@ impl InvokeUiSession for FlutterHandler { fn adapt_size(&self) {} #[inline] - #[cfg(any(target_os = "android", target_os = "ios"))] + #[cfg(not(feature = "flutter_texture_render"))] 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. @@ -421,7 +427,7 @@ impl InvokeUiSession for FlutterHandler { } #[inline] - #[cfg(not(any(target_os = "android", target_os = "ios")))] + #[cfg(feature = "flutter_texture_render")] fn on_rgba(&self, data: &mut Vec) { self.renderer.read().unwrap().on_rgba(data); if *self.notify_rendered.read().unwrap() { diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index c55866dbe..d8861eb0a 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1306,6 +1306,17 @@ pub fn main_hide_docker() -> SyncReturn { SyncReturn(true) } +pub fn main_use_texture_render() -> SyncReturn { + #[cfg(not(feature = "flutter_texture_render"))] + { + SyncReturn(false) + } + #[cfg(feature = "flutter_texture_render")] + { + SyncReturn(true) + } +} + 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 173e3bcd0d9d3a05ee8081cd52983906b8ad9d3f Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 22 Feb 2023 09:43:57 +0800 Subject: [PATCH 1974/2015] debug win, without hwcodec Signed-off-by: fufesou --- Cargo.toml | 1 + libs/scrap/src/common/mediacodec.rs | 3 +- src/client.rs | 4 +-- src/flutter.rs | 45 ++++++++++++++++++++--------- src/flutter_ffi.rs | 7 +++-- 5 files changed, 39 insertions(+), 21 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0a7af0cbc..050a0cd47 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ inline = [] hbbs = [] cli = [] with_rc = ["simple_rc"] +flutter_texture_render = [] appimage = [] flatpak = [] use_samplerate = ["samplerate"] diff --git a/libs/scrap/src/common/mediacodec.rs b/libs/scrap/src/common/mediacodec.rs index 77c21ffcf..7bda0b69d 100644 --- a/libs/scrap/src/common/mediacodec.rs +++ b/libs/scrap/src/common/mediacodec.rs @@ -1,5 +1,4 @@ -use hbb_common::anyhow::Error; -use hbb_common::{bail, ResultType}; +use hbb_common::{log, anyhow::Error, bail, ResultType}; use ndk::media::media_codec::{MediaCodec, MediaCodecDirection, MediaFormat}; use std::ops::Deref; use std::{ diff --git a/src/client.rs b/src/client.rs index 9f4cef831..0c2cf09cd 100644 --- a/src/client.rs +++ b/src/client.rs @@ -945,9 +945,9 @@ impl VideoHandler { match &vf.union { Some(frame) => { #[cfg(feature = "flutter_texture_render")] - let fmt = ImageFormat::ARGB; - #[cfg(not(feature = "flutter_texture_render"))] let fmt = ImageFormat::ABGR; + #[cfg(not(feature = "flutter_texture_render"))] + let fmt = ImageFormat::ARGB; let res = self.decoder.handle_video_frame(frame, fmt, &mut self.rgb); if self.record { self.recorder diff --git a/src/flutter.rs b/src/flutter.rs index f78e1bd92..c232d891d 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -4,14 +4,17 @@ use crate::{ ui_session_interface::{io_loop, InvokeUiSession, Session}, }; use flutter_rust_bridge::StreamSink; +#[cfg(feature = "flutter_texture_render")] +use hbb_common::libc::c_void; use hbb_common::{ - bail, config::LocalConfig, get_version_number, libc::c_void, message_proto::*, - rendezvous_proto::ConnType, ResultType, + bail, config::LocalConfig, get_version_number, message_proto::*, rendezvous_proto::ConnType, + ResultType, }; +#[cfg(feature = "flutter_texture_render")] use libloading::{Library, Symbol}; use serde_json::json; -#[cfg(any(target_os = "android", target_os = "ios"))] +#[cfg(not(feature = "flutter_texture_render"))] use std::sync::atomic::{AtomicBool, Ordering}; use std::{ collections::HashMap, @@ -30,7 +33,10 @@ lazy_static::lazy_static! { pub static ref CUR_SESSION_ID: RwLock = Default::default(); pub static ref SESSIONS: RwLock>> = Default::default(); pub static ref GLOBAL_EVENT_STREAM: RwLock>> = Default::default(); // rust to dart event channel - #[cfg(not(any(target_os = "ios", target_os = "android")))] +} + +#[cfg(feature = "flutter_texture_render")] +lazy_static::lazy_static! { pub static ref TEXTURE_RGBA_RENDERER_PLUGIN: Library = { unsafe { #[cfg(target_os = "windows")] @@ -134,6 +140,7 @@ pub struct FlutterHandler { pub rgba_valid: Arc, #[cfg(feature = "flutter_texture_render")] notify_rendered: Arc>, + #[cfg(feature = "flutter_texture_render")] renderer: Arc>, peer_info: Arc>, } @@ -556,7 +563,7 @@ impl InvokeUiSession for FlutterHandler { #[inline] fn get_rgba(&self) -> *const u8 { - #[cfg(any(target_os = "android", target_os = "ios"))] + #[cfg(not(feature = "flutter_texture_render"))] if self.rgba_valid.load(Ordering::Relaxed) { return self.rgba.read().unwrap().as_ptr(); } @@ -565,7 +572,7 @@ impl InvokeUiSession for FlutterHandler { #[inline] fn next_rgba(&self) { - #[cfg(any(target_os = "android", target_os = "ios"))] + #[cfg(not(feature = "flutter_texture_render"))] self.rgba_valid.store(false, Ordering::Relaxed); } } @@ -825,23 +832,28 @@ pub fn set_cur_session_id(id: String) { } #[no_mangle] -pub fn session_get_rgba_size(_id: *const char) -> usize { - #[cfg(any(target_os = "android", target_os = "ios"))] - let id = unsafe { std::ffi::CStr::from_ptr(_id as _) }; - #[cfg(any(target_os = "android", target_os = "ios"))] +#[cfg(not(feature = "flutter_texture_render"))] +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) { + if let Some(session) = SESSIONS.read().unwrap().get(id) { return session.rgba.read().unwrap().len(); } } 0 } +#[no_mangle] +#[cfg(feature = "flutter_texture_render")] +pub fn session_get_rgba_size(_id: *const char) -> usize { + 0 +} + #[no_mangle] 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) { + if let Some(session) = SESSIONS.read().unwrap().get(id) { return session.get_rgba(); } } @@ -852,18 +864,23 @@ pub fn session_get_rgba(id: *const char) -> *const u8 { 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) { + if let Some(session) = SESSIONS.read().unwrap().get(id) { return session.next_rgba(); } } } #[no_mangle] +#[cfg(feature = "flutter_texture_render")] pub fn session_register_texture(id: *const char, ptr: 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) { + if let Some(session) = SESSIONS.read().unwrap().get(id) { return session.register_texture(ptr); } } } + +#[no_mangle] +#[cfg(not(feature = "flutter_texture_render"))] +pub fn session_register_texture(_id: *const char, _ptr: usize) {} diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index d8861eb0a..14906d568 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -529,9 +529,10 @@ pub fn session_switch_sides(id: String) { } } -pub fn session_set_size(id: String, width: i32, height: i32) { - if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) { - session.set_size(width, height); +pub fn session_set_size(_id: String, _width: i32, _height: i32) { + #[cfg(feature = "flutter_texture_render")] + if let Some(session) = SESSIONS.write().unwrap().get_mut(&_id) { + session.set_size(_width, _height); } } From d70ffaa2b86b0321d65f1a419d286e1b99b00c80 Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 22 Feb 2023 09:57:51 +0800 Subject: [PATCH 1975/2015] update pubspec Signed-off-by: fufesou --- flutter/pubspec.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index a07df9c2e..5ffe805b8 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -1563,5 +1563,5 @@ packages: source: hosted version: "0.1.1" sdks: - dart: ">=2.18.0 <3.0.0" + dart: ">=2.18.0 <4.0.0" flutter: ">=3.3.0" From ed0338b038b9ed087cae015f7db169295e30beff Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 22 Feb 2023 10:25:21 +0800 Subject: [PATCH 1976/2015] fix build && default flutter_texture_render Signed-off-by: fufesou --- build.py | 1 + libs/scrap/src/common/convert.rs | 13 ++++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/build.py b/build.py index 9e490166f..727b53fe0 100755 --- a/build.py +++ b/build.py @@ -239,6 +239,7 @@ def get_features(args): features.append('hwcodec') if args.flutter: features.append('flutter') + features.append('flutter_texture_render') if args.flatpak: features.append('flatpak') if args.appimage: diff --git a/libs/scrap/src/common/convert.rs b/libs/scrap/src/common/convert.rs index a2177805e..f3ad51a21 100644 --- a/libs/scrap/src/common/convert.rs +++ b/libs/scrap/src/common/convert.rs @@ -126,6 +126,17 @@ extern "C" { width: c_int, height: c_int, ) -> c_int; + + pub fn NV12ToABGR( + src_y: *const u8, + src_stride_y: c_int, + src_uv: *const u8, + src_stride_uv: c_int, + dst_rgba: *mut u8, + dst_stride_rgba: c_int, + width: c_int, + height: c_int, + ) -> c_int; } // https://github.com/webmproject/libvpx/blob/master/vpx/src/vpx_image.c @@ -456,7 +467,7 @@ pub mod hw { } } _ => { - Err(anyhow!("unsupported image format")); + Err(anyhow!("unsupported image format")) } } } From 20021c6541b04caf1c414c7e4795ba6198349a1f Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 22 Feb 2023 11:03:40 +0800 Subject: [PATCH 1977/2015] fix build Signed-off-by: fufesou --- src/flutter.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/flutter.rs b/src/flutter.rs index c232d891d..42da3f038 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -875,7 +875,7 @@ pub fn session_next_rgba(id: *const char) { pub fn session_register_texture(id: *const char, ptr: usize) { let id = unsafe { std::ffi::CStr::from_ptr(id as _) }; if let Ok(id) = id.to_str() { - if let Some(session) = SESSIONS.read().unwrap().get(id) { + if let Some(session) = SESSIONS.write().unwrap().get_mut(id) { return session.register_texture(ptr); } } From 8b7be688c27383c83962b064d843fbaa25e22eea Mon Sep 17 00:00:00 2001 From: fufesou Date: Wed, 22 Feb 2023 22:33:17 +0800 Subject: [PATCH 1978/2015] macos, linux, r and b are reversed Signed-off-by: fufesou --- Cargo.lock | 327 +++++++++++-------- Cargo.toml | 1 + flutter/macos/Podfile.lock | 6 + flutter/macos/Runner/MainFlutterWindow.swift | 2 + src/flutter.rs | 82 +++-- 5 files changed, 256 insertions(+), 162 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index eb5461a6e..14b09a9d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -254,9 +254,9 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2cda8f4bcc10624c4e85bc66b3f452cca98cfa5ca002dc83a16aad2367641bea" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -271,9 +271,9 @@ version = "0.1.59" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31e6e93155431f3931513b243d371981bb2770112b370c82745a1d19d2f99364" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -377,8 +377,8 @@ dependencies = [ "lazycell", "log", "peeking_take_while", - "proc-macro2", - "quote", + "proc-macro2 1.0.47", + "quote 1.0.21", "regex", "rustc-hash", "shlex", @@ -397,12 +397,12 @@ dependencies = [ "lazy_static", "lazycell", "peeking_take_while", - "proc-macro2", - "quote", + "proc-macro2 1.0.47", + "quote 1.0.21", "regex", "rustc-hash", "shlex", - "syn", + "syn 1.0.105", ] [[package]] @@ -588,11 +588,11 @@ dependencies = [ "heck 0.4.0", "indexmap", "log", - "proc-macro2", - "quote", + "proc-macro2 1.0.47", + "quote 1.0.21", "serde 1.0.149", "serde_json 1.0.89", - "syn", + "syn 1.0.105", "tempfile", "toml", ] @@ -721,9 +721,9 @@ checksum = "ea0c8bce528c4be4da13ea6fead8965e95b6073585a2f05204bd8f4119f82a65" dependencies = [ "heck 0.4.0", "proc-macro-error", - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -1119,10 +1119,10 @@ dependencies = [ "cc", "codespan-reporting", "once_cell", - "proc-macro2", - "quote", + "proc-macro2 1.0.47", + "quote 1.0.21", "scratch", - "syn", + "syn 1.0.105", ] [[package]] @@ -1137,9 +1137,9 @@ version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1362b0ddcfc4eb0a1f57b68bd77dd99f0e826958a96abd0ae9bd092e114ffed6" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -1177,10 +1177,10 @@ checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" dependencies = [ "fnv", "ident_case", - "proc-macro2", - "quote", + "proc-macro2 1.0.47", + "quote 1.0.21", "strsim 0.10.0", - "syn", + "syn 1.0.105", ] [[package]] @@ -1190,8 +1190,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" dependencies = [ "darling_core", - "quote", - "syn", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -1373,9 +1373,9 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "082a24a9967533dc5d743c602157637116fc1b52806d694a5a45e6f32567fcdd" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -1384,9 +1384,9 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -1481,6 +1481,29 @@ dependencies = [ "libloading", ] +[[package]] +name = "dlopen" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e80ad39f814a9abe68583cd50a2d45c8a67561c3361ab8da240587dda80937" +dependencies = [ + "dlopen_derive", + "lazy_static", + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "dlopen_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f236d9e1b1fbd81cea0f9cbdc8dcc7e8ebcd80e6659cd7cb2ad5f6c05946c581" +dependencies = [ + "libc", + "quote 0.6.13", + "syn 0.15.44", +] + [[package]] name = "dlv-list" version = "0.3.0" @@ -1592,9 +1615,9 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9045e2676cd5af83c3b167d917b0a5c90a4d8e266e2683d6631b235c457fc27" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -1604,9 +1627,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eb359f1476bf611266ac1f5355bc14aeca37b299d0ebccc038ee7058891c9cb" dependencies = [ "once_cell", - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -1625,9 +1648,9 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f58dc3c5e468259f19f2d46304a6b28f1c3d034442e14b322d2b850e36f6d5ae" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -1670,10 +1693,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c34a887c8df3ed90498c1c437ce21f211c8e27672921a8ffa293cb8d6d4caa9e" dependencies = [ "proc-macro-error", - "proc-macro2", - "quote", + "proc-macro2 1.0.47", + "quote 1.0.21", "rustversion", - "syn", + "syn 1.0.105", "synstructure", ] @@ -1747,9 +1770,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c5216e387a76eebaaf11f6d871ec8a4aae0b25f05456ee21f228e024b1b3610" dependencies = [ "proc-macro-error", - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -1878,11 +1901,11 @@ dependencies = [ "lazy_static", "log", "pathdiff", - "quote", + "quote 1.0.21", "regex", "serde 1.0.149", "serde_yaml", - "syn", + "syn 1.0.105", "tempfile", "thiserror", "toml", @@ -2035,9 +2058,9 @@ version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bdfb8ce053d86b91919aad980c220b1fb8401a9394410e1c289ed7e66b61835d" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -2299,9 +2322,9 @@ dependencies = [ "itertools 0.9.0", "proc-macro-crate 0.1.5", "proc-macro-error", - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -2314,9 +2337,9 @@ dependencies = [ "heck 0.4.0", "proc-macro-crate 1.2.1", "proc-macro-error", - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -2550,9 +2573,9 @@ dependencies = [ "anyhow", "proc-macro-crate 1.2.1", "proc-macro-error", - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -2856,8 +2879,8 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b139284b5cf57ecfa712bcc66950bb635b31aff41c188e8a4cfc758eca374a3f" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.47", + "quote 1.0.21", ] [[package]] @@ -3564,9 +3587,9 @@ checksum = "0df7ac00c4672f9d5aece54ee3347520b7e20f158656c7db2e6de01902eb7a6c" dependencies = [ "darling", "proc-macro-crate 1.2.1", - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -3713,9 +3736,9 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -3805,9 +3828,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b0498641e53dd6ac1a4f22547548caa6864cc4933784319cd1775271c5a46ce" dependencies = [ "proc-macro-crate 1.2.1", - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -4121,9 +4144,9 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -4230,9 +4253,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" dependencies = [ "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", "version_check", ] @@ -4242,11 +4265,20 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.47", + "quote 1.0.21", "version_check", ] +[[package]] +name = "proc-macro2" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" +dependencies = [ + "unicode-xid 0.1.0", +] + [[package]] name = "proc-macro2" version = "1.0.47" @@ -4374,13 +4406,22 @@ dependencies = [ "tracing", ] +[[package]] +name = "quote" +version = "0.6.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1" +dependencies = [ + "proc-macro2 0.4.30", +] + [[package]] name = "quote" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" dependencies = [ - "proc-macro2", + "proc-macro2 1.0.47", ] [[package]] @@ -4846,6 +4887,7 @@ dependencies = [ "dbus-crossroads", "default-net", "dispatch", + "dlopen", "enigo", "errno", "evdev", @@ -5158,9 +5200,9 @@ version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4eae9b04cbffdfd550eb462ed33bc6a1b68c935127d008b27444d08380f94e4" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -5192,9 +5234,9 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fe39d9fbb0ebf5eb2c7cb7e2a47e4f462fad1379f1166b8ae49ad9eae89a7ca" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -5453,9 +5495,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87c85aa3f8ea653bfd3ddf25f7ee357ee4d204731f6aa9ad04002306f6e2774c" dependencies = [ "heck 0.3.3", - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -5465,10 +5507,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" dependencies = [ "heck 0.4.0", - "proc-macro2", - "quote", + "proc-macro2 1.0.47", + "quote 1.0.21", "rustversion", - "syn", + "syn 1.0.105", +] + +[[package]] +name = "syn" +version = "0.15.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5" +dependencies = [ + "proc-macro2 0.4.30", + "quote 0.6.13", + "unicode-xid 0.1.0", ] [[package]] @@ -5477,8 +5530,8 @@ version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b9b43d45702de4c839cb9b51d9f529c5dd26a4aff255b42b1ebc03e88ee908" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.47", + "quote 1.0.21", "unicode-ident", ] @@ -5488,10 +5541,10 @@ version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" dependencies = [ - "proc-macro2", - "quote", - "syn", - "unicode-xid", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", + "unicode-xid 0.2.4", ] [[package]] @@ -5630,9 +5683,9 @@ name = "tao-macros" version = "0.1.0" source = "git+https://github.com/tauri-apps/tao?branch=muda#676bd90a80286b893d8850cc4e3813a0c4a27dcf" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -5725,9 +5778,9 @@ version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -5831,9 +5884,9 @@ version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -5920,9 +5973,9 @@ version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -6033,6 +6086,12 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +[[package]] +name = "unicode-xid" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" + [[package]] name = "unicode-xid" version = "0.2.4" @@ -6177,9 +6236,9 @@ dependencies = [ "bumpalo", "log", "once_cell", - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", "wasm-bindgen-shared", ] @@ -6201,7 +6260,7 @@ version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" dependencies = [ - "quote", + "quote 1.0.21", "wasm-bindgen-macro-support", ] @@ -6211,9 +6270,9 @@ version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -6281,8 +6340,8 @@ version = "0.29.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f4303d8fa22ab852f789e75a967f0a2cdc430a607751c0499bada3e451cbd53" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.47", + "quote 1.0.21", "xml-rs", ] @@ -6507,9 +6566,9 @@ version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce87ca8e3417b02dc2a8a22769306658670ec92d78f1bd420d6310a67c245c6" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -6518,9 +6577,9 @@ version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "853f69a591ecd4f810d29f17e902d40e349fb05b0b11fff63b08b826bfe39c7f" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -6962,10 +7021,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45066039ebf3330820e495e854f8b312abb68f0a39e97972d092bd72e8bb3e8e" dependencies = [ "proc-macro-crate 1.2.1", - "proc-macro2", - "quote", + "proc-macro2 1.0.47", + "quote 1.0.21", "regex", - "syn", + "syn 1.0.105", ] [[package]] @@ -7038,7 +7097,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "155247a5d1ab55e335421c104ccd95d64f17cebbd02f50cdbc1c33385f9c4d81" dependencies = [ "proc-macro-crate 1.2.1", - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", ] diff --git a/Cargo.toml b/Cargo.toml index 050a0cd47..b51930f72 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -64,6 +64,7 @@ flutter_rust_bridge = { version = "1.61.1", optional = true } errno = "0.2.8" rdev = { git = "https://github.com/fufesou/rdev" } url = { version = "2.1", features = ["serde"] } +dlopen = "0.1" reqwest = { version = "0.11", features = ["blocking", "json", "rustls-tls"], default-features=false } chrono = "0.4.23" diff --git a/flutter/macos/Podfile.lock b/flutter/macos/Podfile.lock index 3187c6349..16dc0d352 100644 --- a/flutter/macos/Podfile.lock +++ b/flutter/macos/Podfile.lock @@ -21,6 +21,8 @@ PODS: - sqflite (0.0.2): - FlutterMacOS - FMDB (>= 2.7.5) + - texture_rgba_renderer (0.0.1): + - FlutterMacOS - uni_links_desktop (0.0.1): - FlutterMacOS - url_launcher_macos (0.0.1): @@ -42,6 +44,7 @@ DEPENDENCIES: - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/macos`) - screen_retriever (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos`) - sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/macos`) + - texture_rgba_renderer (from `Flutter/ephemeral/.symlinks/plugins/texture_rgba_renderer/macos`) - uni_links_desktop (from `Flutter/ephemeral/.symlinks/plugins/uni_links_desktop/macos`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) - wakelock_macos (from `Flutter/ephemeral/.symlinks/plugins/wakelock_macos/macos`) @@ -71,6 +74,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos sqflite: :path: Flutter/ephemeral/.symlinks/plugins/sqflite/macos + texture_rgba_renderer: + :path: Flutter/ephemeral/.symlinks/plugins/texture_rgba_renderer/macos uni_links_desktop: :path: Flutter/ephemeral/.symlinks/plugins/uni_links_desktop/macos url_launcher_macos: @@ -93,6 +98,7 @@ SPEC CHECKSUMS: path_provider_foundation: 37748e03f12783f9de2cb2c4eadfaa25fe6d4852 screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38 sqflite: a5789cceda41d54d23f31d6de539d65bb14100ea + texture_rgba_renderer: cbed959a3c127122194a364e14b8577bd62dc8f2 uni_links_desktop: 45900fb319df48fcdea2df0756e9c2626696b026 url_launcher_macos: c04e4fa86382d4f94f6b38f14625708be3ae52e2 wakelock_macos: bc3f2a9bd8d2e6c89fee1e1822e7ddac3bd004a9 diff --git a/flutter/macos/Runner/MainFlutterWindow.swift b/flutter/macos/Runner/MainFlutterWindow.swift index 21e870320..e9043da71 100644 --- a/flutter/macos/Runner/MainFlutterWindow.swift +++ b/flutter/macos/Runner/MainFlutterWindow.swift @@ -17,6 +17,7 @@ import url_launcher_macos import wakelock_macos import window_manager import window_size +import texture_rgba_renderer class MainFlutterWindow: NSWindow { override func awakeFromNib() { @@ -49,6 +50,7 @@ class MainFlutterWindow: NSWindow { UrlLauncherPlugin.register(with: controller.registrar(forPlugin: "UrlLauncherPlugin")) WakelockMacosPlugin.register(with: controller.registrar(forPlugin: "WakelockMacosPlugin")) WindowSizePlugin.register(with: controller.registrar(forPlugin: "WindowSizePlugin")) + TextureRgbaRendererPlugin.register(with: controller.registrar(forPlugin: "TextureRgbaRendererPlugin")) } super.awakeFromNib() diff --git a/src/flutter.rs b/src/flutter.rs index 42da3f038..51c96ddcf 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -3,15 +3,22 @@ use crate::{ flutter_ffi::EventToUI, ui_session_interface::{io_loop, InvokeUiSession, Session}, }; +#[cfg(feature = "flutter_texture_render")] +#[cfg(target_os = "macos")] +use dlopen::{ + symbor::{Library, Symbol}, + Error as LibError, +}; use flutter_rust_bridge::StreamSink; #[cfg(feature = "flutter_texture_render")] use hbb_common::libc::c_void; use hbb_common::{ - bail, config::LocalConfig, get_version_number, message_proto::*, rendezvous_proto::ConnType, - ResultType, + bail, config::LocalConfig, get_version_number, log, message_proto::*, + rendezvous_proto::ConnType, ResultType, }; #[cfg(feature = "flutter_texture_render")] -use libloading::{Library, Symbol}; +#[cfg(not(target_os = "macos"))] +use libloading::{Error as LibError, Library, Symbol}; use serde_json::json; #[cfg(not(feature = "flutter_texture_render"))] @@ -37,16 +44,16 @@ lazy_static::lazy_static! { #[cfg(feature = "flutter_texture_render")] lazy_static::lazy_static! { - pub static ref TEXTURE_RGBA_RENDERER_PLUGIN: Library = { + pub static ref TEXTURE_RGBA_RENDERER_PLUGIN: Result = { + #[cfg(not(target_os = "macos"))] unsafe { #[cfg(target_os = "windows")] - let lib = Library::new("texture_rgba_renderer_plugin.dll"); - #[cfg(target_os = "macos")] - let lib = Library::new("texture_rgba_renderer_plugin.dylib"); + Library::new("texture_rgba_renderer_plugin.dll"); #[cfg(target_os = "linux")] - let lib = Library::new("libtexture_rgba_renderer_plugin.so"); - lib.expect("`libtexture_rgba_renderer_plugin` not found, please add `texture_rgba_renderer` in your flutter project") + Library::new("libtexture_rgba_renderer_plugin.so"); } + #[cfg(target_os = "macos")] + Library::open_self() }; } @@ -157,22 +164,40 @@ struct VideoRenderer { width: i32, height: i32, data_len: usize, - on_rgba_func: Symbol<'static, FlutterRgbaRendererPluginOnRgba>, + on_rgba_func: Option>, } #[cfg(feature = "flutter_texture_render")] impl Default for VideoRenderer { fn default() -> Self { - unsafe { - Self { - ptr: 0, - width: 0, - height: 0, - data_len: 0, - on_rgba_func: TEXTURE_RGBA_RENDERER_PLUGIN - .get::(b"FlutterRgbaRendererPluginOnRgba") - .expect("Symbol FlutterRgbaRendererPluginOnRgba not found."), + let on_rgba_func = match &*TEXTURE_RGBA_RENDERER_PLUGIN { + Ok(lib) => { + #[cfg(not(target_os = "macos"))] + let find_sym_res = + lib.get::(b"FlutterRgbaRendererPluginOnRgba"); + #[cfg(target_os = "macos")] + let find_sym_res = unsafe { + lib.symbol::("FlutterRgbaRendererPluginOnRgba") + }; + match find_sym_res { + Ok(sym) => Some(sym), + Err(e) => { + log::error!("Failed to find symbol FlutterRgbaRendererPluginOnRgba, {e}"); + None + } + } } + Err(e) => { + log::error!("Failed to load texture rgba renderer plugin, {e}"); + None + } + }; + Self { + ptr: 0, + width: 0, + height: 0, + data_len: 0, + on_rgba_func, } } } @@ -194,15 +219,16 @@ impl VideoRenderer { if self.ptr == usize::default() || rgba.len() != self.data_len { return; } - let func = self.on_rgba_func.clone(); - unsafe { - func( - self.ptr as _, - rgba.as_ptr() as _, - self.width as _, - self.height as _, - ) - }; + if let Some(func) = &self.on_rgba_func { + unsafe { + func( + self.ptr as _, + rgba.as_ptr() as _, + self.width as _, + self.height as _, + ) + }; + } } } From 9559a889fbefc53912b3a049d347344fb7723897 Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 23 Feb 2023 10:02:54 +0800 Subject: [PATCH 1979/2015] register plugin && fix r&b colors Signed-off-by: fufesou --- flutter/windows/runner/flutter_window.cpp | 10 ++++++++++ src/client.rs | 5 +++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/flutter/windows/runner/flutter_window.cpp b/flutter/windows/runner/flutter_window.cpp index b43b9095e..2f1f36f73 100644 --- a/flutter/windows/runner/flutter_window.cpp +++ b/flutter/windows/runner/flutter_window.cpp @@ -2,6 +2,9 @@ #include +#include +#include + #include "flutter/generated_plugin_registrant.h" FlutterWindow::FlutterWindow(const flutter::DartProject& project) @@ -25,6 +28,13 @@ bool FlutterWindow::OnCreate() { return false; } RegisterPlugins(flutter_controller_->engine()); + DesktopMultiWindowSetWindowCreatedCallback([](void *controller) { + auto *flutter_view_controller = + reinterpret_cast(controller); + auto *registry = flutter_view_controller->engine(); + TextureRgbaRendererPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("TextureRgbaRendererPlugin")); + }); SetChildContent(flutter_controller_->view()->GetNativeWindow()); return true; } diff --git a/src/client.rs b/src/client.rs index 0c2cf09cd..ebfda7283 100644 --- a/src/client.rs +++ b/src/client.rs @@ -944,9 +944,10 @@ impl VideoHandler { } match &vf.union { Some(frame) => { - #[cfg(feature = "flutter_texture_render")] + // windows && flutter_texture_render, fmt is ImageFormat::ABGR + #[cfg(all(target_os = "windows", feature = "flutter_texture_render"))] let fmt = ImageFormat::ABGR; - #[cfg(not(feature = "flutter_texture_render"))] + #[cfg(not(all(target_os = "windows", feature = "flutter_texture_render")))] let fmt = ImageFormat::ARGB; let res = self.decoder.handle_video_frame(frame, fmt, &mut self.rgb); if self.record { From b8e381d79d30b7d47013ee9e0fd6bbefefcfb92d Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 23 Feb 2023 10:21:31 +0800 Subject: [PATCH 1980/2015] win, debug Signed-off-by: fufesou --- Cargo.lock | 1 - Cargo.toml | 1 - src/flutter.rs | 56 ++++++++++++++++++++++++-------------------------- 3 files changed, 27 insertions(+), 31 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 14b09a9d2..8483cbac1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4902,7 +4902,6 @@ dependencies = [ "include_dir", "jni 0.19.0", "lazy_static", - "libloading", "libpulse-binding", "libpulse-simple-binding", "mac_address", diff --git a/Cargo.toml b/Cargo.toml index b51930f72..b424b01d1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,7 +69,6 @@ dlopen = "0.1" reqwest = { version = "0.11", features = ["blocking", "json", "rustls-tls"], default-features=false } chrono = "0.4.23" cidr-utils = "0.5.9" -libloading = "0.7.4" [target.'cfg(not(any(target_os = "android", target_os = "linux")))'.dependencies] cpal = "0.13.5" diff --git a/src/flutter.rs b/src/flutter.rs index 51c96ddcf..f2f950ad3 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -4,21 +4,18 @@ use crate::{ ui_session_interface::{io_loop, InvokeUiSession, Session}, }; #[cfg(feature = "flutter_texture_render")] -#[cfg(target_os = "macos")] +// #[cfg(target_os = "macos")] use dlopen::{ symbor::{Library, Symbol}, Error as LibError, }; use flutter_rust_bridge::StreamSink; -#[cfg(feature = "flutter_texture_render")] -use hbb_common::libc::c_void; use hbb_common::{ - bail, config::LocalConfig, get_version_number, log, message_proto::*, - rendezvous_proto::ConnType, ResultType, + bail, config::LocalConfig, get_version_number, message_proto::*, rendezvous_proto::ConnType, + ResultType, }; #[cfg(feature = "flutter_texture_render")] -#[cfg(not(target_os = "macos"))] -use libloading::{Error as LibError, Library, Symbol}; +use hbb_common::{libc::c_void, log}; use serde_json::json; #[cfg(not(feature = "flutter_texture_render"))] @@ -42,19 +39,19 @@ lazy_static::lazy_static! { pub static ref GLOBAL_EVENT_STREAM: RwLock>> = Default::default(); // rust to dart event channel } -#[cfg(feature = "flutter_texture_render")] +#[cfg(all(target_os = "windows", feature = "flutter_texture_render"))] lazy_static::lazy_static! { - pub static ref TEXTURE_RGBA_RENDERER_PLUGIN: Result = { - #[cfg(not(target_os = "macos"))] - unsafe { - #[cfg(target_os = "windows")] - Library::new("texture_rgba_renderer_plugin.dll"); - #[cfg(target_os = "linux")] - Library::new("libtexture_rgba_renderer_plugin.so"); - } - #[cfg(target_os = "macos")] - Library::open_self() - }; + pub static ref TEXTURE_RGBA_RENDERER_PLUGIN: Result = Library::open("texture_rgba_renderer_plugin.dll"); +} + +#[cfg(all(target_os = "linux", feature = "flutter_texture_render"))] +lazy_static::lazy_static! { + pub static ref TEXTURE_RGBA_RENDERER_PLUGIN: Result = Library::open("libtexture_rgba_renderer_plugin.so"); +} + +#[cfg(all(target_os = "macos", feature = "flutter_texture_render"))] +lazy_static::lazy_static! { + pub static ref TEXTURE_RGBA_RENDERER_PLUGIN: Result = Library::open_self(); } /// FFI for rustdesk core's main entry. @@ -136,21 +133,26 @@ pub unsafe extern "C" fn free_c_args(ptr: *mut *mut c_char, len: c_int) { // Afterwards the vector will be dropped and thus freed. } +#[cfg(feature = "flutter_texture_render")] +#[derive(Default, Clone)] +pub struct FlutterHandler { + pub event_stream: Arc>>>, + notify_rendered: Arc>, + renderer: Arc>, + peer_info: Arc>, +} + +#[cfg(not(feature = "flutter_texture_render"))] #[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]. - #[cfg(not(feature = "flutter_texture_render"))] pub rgba: Arc>>, - #[cfg(not(feature = "flutter_texture_render"))] pub rgba_valid: Arc, - #[cfg(feature = "flutter_texture_render")] - notify_rendered: Arc>, - #[cfg(feature = "flutter_texture_render")] - renderer: Arc>, peer_info: Arc>, } + #[cfg(feature = "flutter_texture_render")] pub type FlutterRgbaRendererPluginOnRgba = unsafe extern "C" fn(texture_rgba: *mut c_void, buffer: *const u8, width: c_int, height: c_int); @@ -172,10 +174,6 @@ impl Default for VideoRenderer { fn default() -> Self { let on_rgba_func = match &*TEXTURE_RGBA_RENDERER_PLUGIN { Ok(lib) => { - #[cfg(not(target_os = "macos"))] - let find_sym_res = - lib.get::(b"FlutterRgbaRendererPluginOnRgba"); - #[cfg(target_os = "macos")] let find_sym_res = unsafe { lib.symbol::("FlutterRgbaRendererPluginOnRgba") }; From b84062b8f414317879c31558c743ca07f435838d Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 23 Feb 2023 13:40:08 +0800 Subject: [PATCH 1981/2015] texture render, add log info Signed-off-by: fufesou --- src/flutter.rs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/flutter.rs b/src/flutter.rs index f2f950ad3..c501bd4a5 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -4,18 +4,17 @@ use crate::{ ui_session_interface::{io_loop, InvokeUiSession, Session}, }; #[cfg(feature = "flutter_texture_render")] -// #[cfg(target_os = "macos")] use dlopen::{ symbor::{Library, Symbol}, Error as LibError, }; use flutter_rust_bridge::StreamSink; -use hbb_common::{ - bail, config::LocalConfig, get_version_number, message_proto::*, rendezvous_proto::ConnType, - ResultType, -}; #[cfg(feature = "flutter_texture_render")] -use hbb_common::{libc::c_void, log}; +use hbb_common::libc::c_void; +use hbb_common::{ + bail, config::LocalConfig, get_version_number, log, message_proto::*, + rendezvous_proto::ConnType, ResultType, +}; use serde_json::json; #[cfg(not(feature = "flutter_texture_render"))] @@ -665,6 +664,13 @@ pub fn session_start_(id: &str, event_stream: StreamSink) -> ResultTy *session.event_stream.write().unwrap() = Some(event_stream); let session = session.clone(); std::thread::spawn(move || { + #[cfg(feature = "flutter_texture_render")] + log::info!( + "Session {} start, render by flutter texture rgba plugin", + id + ); + #[cfg(not(feature = "flutter_texture_render"))] + log::info!("Session {} start, render by flutter paint widget", id); io_loop(session); }); Ok(()) From 09aa42c53344c6d100adf481fbceec0ae64babfe Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 23 Feb 2023 14:02:16 +0800 Subject: [PATCH 1982/2015] fix build Signed-off-by: fufesou --- src/flutter.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/flutter.rs b/src/flutter.rs index c501bd4a5..d366a0eda 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -661,16 +661,16 @@ pub fn session_add( /// * `events2ui` - The events channel to ui. pub fn session_start_(id: &str, event_stream: StreamSink) -> ResultType<()> { if let Some(session) = SESSIONS.write().unwrap().get_mut(id) { + #[cfg(feature = "flutter_texture_render")] + log::info!( + "Session {} start, render by flutter texture rgba plugin", + id + ); + #[cfg(not(feature = "flutter_texture_render"))] + log::info!("Session {} start, render by flutter paint widget", id); *session.event_stream.write().unwrap() = Some(event_stream); let session = session.clone(); std::thread::spawn(move || { - #[cfg(feature = "flutter_texture_render")] - log::info!( - "Session {} start, render by flutter texture rgba plugin", - id - ); - #[cfg(not(feature = "flutter_texture_render"))] - log::info!("Session {} start, render by flutter paint widget", id); io_loop(session); }); Ok(()) From 4cb6e82893565a97f80ef97cc639c852a822391c Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 23 Feb 2023 15:16:32 +0800 Subject: [PATCH 1983/2015] add feature flutter_texture_render for linux Signed-off-by: fufesou --- .github/workflows/flutter-nightly.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index ffcadd18b..b08193971 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -732,7 +732,7 @@ jobs: x86_64) # no need mock on x86_64 export VCPKG_ROOT=/opt/artifacts/vcpkg - cargo build --lib --features hwcodec,flutter,${{ matrix.job.extra-build-features }} --release + cargo build --lib --features hwcodec,flutter,flutter_texture_render,${{ matrix.job.extra-build-features }} --release ;; esac @@ -900,7 +900,7 @@ jobs: ln -s /usr/include /vcpkg/installed/arm64-linux/include export VCPKG_ROOT=/vcpkg # disable hwcodec for compilation - cargo build --lib --features flutter,${{ matrix.job.extra-build-features }} --release + cargo build --lib --features flutter,flutter_texture_render,${{ matrix.job.extra-build-features }} --release ;; armv7) cp -r /opt/artifacts/vcpkg/installed/lib/* /usr/lib/arm-linux-gnueabihf/ @@ -910,7 +910,7 @@ jobs: ln -s /usr/include /vcpkg/installed/arm-linux/include export VCPKG_ROOT=/vcpkg # disable hwcodec for compilation - cargo build --lib --features flutter,${{ matrix.job.extra-build-features }} --release + cargo build --lib --features flutter,flutter_texture_render,${{ matrix.job.extra-build-features }} --release ;; esac From 275da850ffaf5af6c57d313ce5480eee495753c2 Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 23 Feb 2023 16:30:12 +0800 Subject: [PATCH 1984/2015] do not create texture when texture render is not enabled Signed-off-by: fufesou --- flutter/lib/desktop/pages/remote_page.dart | 24 +++++++++++++--------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index c78ffb439..e52334512 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -126,14 +126,16 @@ class _RemotePageState extends State } // Register texture. _textureId.value = -1; - textureRenderer.createTexture(_textureKey).then((id) async { - debugPrint("id: $id, texture_key: $_textureKey"); - if (id != -1) { - final ptr = await textureRenderer.getTexturePtr(_textureKey); - platformFFI.registerTexture(widget.id, ptr); - _textureId.value = id; - } - }); + if (useTextureRender) { + textureRenderer.createTexture(_textureKey).then((id) async { + debugPrint("id: $id, texture_key: $_textureKey"); + if (id != -1) { + final ptr = await textureRenderer.getTexturePtr(_textureKey); + platformFFI.registerTexture(widget.id, ptr); + _textureId.value = id; + } + }); + } _ffi.ffiModel.updateEventListener(widget.id); _ffi.qualityMonitorModel.checkShowQualityMonitor(widget.id); // Session option should be set after models.dart/FFI.start @@ -198,8 +200,10 @@ class _RemotePageState extends State @override void dispose() { debugPrint("REMOTE PAGE dispose ${widget.id}"); - platformFFI.registerTexture(widget.id, 0); - textureRenderer.closeTexture(_textureKey); + if (useTextureRender) { + platformFFI.registerTexture(widget.id, 0); + textureRenderer.closeTexture(_textureKey); + } // ensure we leave this session, this is a double check bind.sessionEnterOrLeave(id: widget.id, enter: false); DesktopMultiWindow.removeListener(this); From aeed94bb96963be3018717f2b726504bdc04f74c Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 23 Feb 2023 18:03:40 +0800 Subject: [PATCH 1985/2015] update flutter-ci && restore crate-type Signed-off-by: fufesou --- .github/workflows/flutter-ci.yml | 6 +++--- Cargo.toml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/flutter-ci.yml b/.github/workflows/flutter-ci.yml index 2386f17dd..74e4efa99 100644 --- a/.github/workflows/flutter-ci.yml +++ b/.github/workflows/flutter-ci.yml @@ -593,7 +593,7 @@ jobs: x86_64) # no need mock on x86_64 export VCPKG_ROOT=/opt/artifacts/vcpkg - cargo build --lib --features hwcodec,flutter,${{ matrix.job.extra-build-features }} --release + cargo build --lib --features hwcodec,flutter,flutter_texture_render,${{ matrix.job.extra-build-features }} --release ;; esac @@ -761,7 +761,7 @@ jobs: ln -s /usr/include /vcpkg/installed/arm64-linux/include export VCPKG_ROOT=/vcpkg # disable hwcodec for compilation - cargo build --lib --features flutter,${{ matrix.job.extra-build-features }} --release + cargo build --lib --features flutter,flutter_texture_render,${{ matrix.job.extra-build-features }} --release ;; armv7) cp -r /opt/artifacts/vcpkg/installed/lib/* /usr/lib/arm-linux-gnueabihf/ @@ -771,7 +771,7 @@ jobs: ln -s /usr/include /vcpkg/installed/arm-linux/include export VCPKG_ROOT=/vcpkg # disable hwcodec for compilation - cargo build --lib --features flutter,${{ matrix.job.extra-build-features }} --release + cargo build --lib --features flutter,flutter_texture_render,${{ matrix.job.extra-build-features }} --release ;; esac diff --git a/Cargo.toml b/Cargo.toml index b424b01d1..c20366983 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ default-run = "rustdesk" [lib] name = "librustdesk" -crate-type = ["cdylib"] +crate-type = ["cdylib", "staticlib", "rlib"] [[bin]] name = "naming" From 75fb964a340b85a39e36152a56c93b1865f48f1e Mon Sep 17 00:00:00 2001 From: Kingtous Date: Thu, 23 Feb 2023 19:08:44 +0800 Subject: [PATCH 1986/2015] opt: lack of frame border in remote page --- .../desktop/pages/file_manager_tab_page.dart | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index bbe2b28be..148d928d9 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -86,14 +86,18 @@ class _FileManagerTabPageState extends State { @override Widget build(BuildContext context) { - final tabWidget = Scaffold( - backgroundColor: Theme.of(context).cardColor, - body: DesktopTab( - controller: tabController, - onWindowCloseButton: handleWindowCloseButton, - tail: const AddButton().paddingOnly(left: 10), - labelGetter: DesktopTab.labelGetterAlias, - )); + final tabWidget = Container( + decoration: BoxDecoration( + border: Border.all(color: MyTheme.color(context).border!)), + child: Scaffold( + backgroundColor: Theme.of(context).cardColor, + body: DesktopTab( + controller: tabController, + onWindowCloseButton: handleWindowCloseButton, + tail: const AddButton().paddingOnly(left: 10), + labelGetter: DesktopTab.labelGetterAlias, + )), + ); return Platform.isMacOS ? tabWidget : SubWindowDragToResizeArea( From fdc04266f6f016efc39ef4dc02a9a342520db46a Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 23 Feb 2023 19:28:30 +0800 Subject: [PATCH 1987/2015] fix #1947 --- src/tray.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/tray.rs b/src/tray.rs index 12523605d..5e1620036 100644 --- a/src/tray.rs +++ b/src/tray.rs @@ -112,8 +112,8 @@ pub fn make_tray() -> hbb_common::ResultType<()> { 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, + dark_light::Mode::Dark => LIGHT, + _ => DARK, }; let (icon_rgba, icon_width, icon_height) = { let image = image::load_from_memory(icon) @@ -147,7 +147,7 @@ pub fn make_tray() -> hbb_common::ResultType<()> { crate::platform::macos::hide_dock(); docker_hiden = true; } - *control_flow = ControlFlow::Poll; + *control_flow = ControlFlow::Wait; if tray_channel.try_recv().is_ok() { crate::platform::macos::handle_application_should_open_untitled_file(); From bb26ba3384bde03c5a45c931a14d0cfcd4d5cea4 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 23 Feb 2023 20:01:50 +0800 Subject: [PATCH 1988/2015] Exit in mac tray --- src/platform/macos.rs | 25 ++++++++++++++++--------- src/tray.rs | 24 +++++++++++++++++++++--- src/ui_interface.rs | 2 +- 3 files changed, 38 insertions(+), 13 deletions(-) diff --git a/src/platform/macos.rs b/src/platform/macos.rs index 910c26982..3e19cca28 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -171,7 +171,7 @@ pub fn is_installed_daemon(prompt: bool) -> bool { false } -pub fn uninstall() -> bool { +pub fn uninstall(show_new_window: bool) -> bool { // to-do: do together with win/linux about refactory start/stop service if !is_installed_daemon(false) { return false; @@ -206,14 +206,21 @@ pub fn uninstall() -> bool { .args(&["remove", &format!("{}_server", crate::get_full_name())]) .status() .ok(); - std::process::Command::new("sh") - .arg("-c") - .arg(&format!( - "sleep 0.5; open /Applications/{}.app", - crate::get_app_name(), - )) - .spawn() - .ok(); + if show_new_window { + std::process::Command::new("sh") + .arg("-c") + .arg(&format!( + "sleep 0.5; open /Applications/{}.app", + crate::get_app_name(), + )) + .spawn() + .ok(); + } else { + std::process::Command::new("pkill") + .arg(crate::get_app_name()) + .status() + .ok(); + } quit_gui(); } } diff --git a/src/tray.rs b/src/tray.rs index 5e1620036..617ec2c93 100644 --- a/src/tray.rs +++ b/src/tray.rs @@ -107,7 +107,10 @@ 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}; + use tray_icon::{ + menu::{Menu, MenuEvent, MenuItem}, + ClickEvent, 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"); @@ -128,8 +131,13 @@ pub fn make_tray() -> hbb_common::ResultType<()> { let event_loop = EventLoopBuilder::new().build(); + let tray_menu = Menu::new(); + let quit_i = MenuItem::new(crate::client::translate("Exit".to_owned()), true, None); + tray_menu.append_items(&[&quit_i]); + let _tray_icon = Some( TrayIconBuilder::new() + .with_menu(Box::new(tray_menu)) .with_tooltip(format!( "{} {}", crate::get_app_name(), @@ -139,6 +147,7 @@ pub fn make_tray() -> hbb_common::ResultType<()> { .build()?, ); + let menu_channel = MenuEvent::receiver(); let tray_channel = TrayEvent::receiver(); let mut docker_hiden = false; @@ -149,8 +158,17 @@ pub fn make_tray() -> hbb_common::ResultType<()> { } *control_flow = ControlFlow::Wait; - if tray_channel.try_recv().is_ok() { - crate::platform::macos::handle_application_should_open_untitled_file(); + if let Ok(event) = menu_channel.try_recv() { + if event.id == quit_i.id() { + crate::platform::macos::uninstall(false); + } + println!("{event:?}"); + } + + if let Ok(event) = tray_channel.try_recv() { + if event.event == ClickEvent::Double { + crate::platform::macos::handle_application_should_open_untitled_file(); + } } }); } diff --git a/src/ui_interface.rs b/src/ui_interface.rs index f44bb4eea..dd111f86e 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -295,7 +295,7 @@ pub fn set_option(key: String, value: String) { #[cfg(target_os = "macos")] if &key == "stop-service" { let is_stop = value == "Y"; - if is_stop && crate::platform::macos::uninstall() { + if is_stop && crate::platform::macos::uninstall(true) { return; } } From a149ba832b293a0601296da3c2fc62588db262cb Mon Sep 17 00:00:00 2001 From: grummbeer Date: Fri, 17 Feb 2023 20:07:21 +0100 Subject: [PATCH 1989/2015] PeerCard. Menu. Move "remove" to last position. --- flutter/lib/common/widgets/peer_card.dart | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index f1b94ecdf..7b24ec2e4 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -690,9 +690,6 @@ class RecentPeerCard extends BasePeerCard { } menuItems.add(MenuEntryDivider()); menuItems.add(_renameAction(peer.id)); - menuItems.add(_removeAction(peer.id, () async { - await bind.mainLoadRecentPeers(); - })); if (await bind.mainPeerHasPassword(id: peer.id)) { menuItems.add(_unrememberPasswordAction(peer.id)); } @@ -700,6 +697,9 @@ class RecentPeerCard extends BasePeerCard { if (!gFFI.abModel.idContainBy(peer.id)) { menuItems.add(_addToAb(peer)); } + menuItems.add(_removeAction(peer.id, () async { + await bind.mainLoadRecentPeers(); + })); return menuItems; } @@ -732,9 +732,6 @@ class FavoritePeerCard extends BasePeerCard { } menuItems.add(MenuEntryDivider()); menuItems.add(_renameAction(peer.id)); - menuItems.add(_removeAction(peer.id, () async { - await bind.mainLoadFavPeers(); - })); if (await bind.mainPeerHasPassword(id: peer.id)) { menuItems.add(_unrememberPasswordAction(peer.id)); } @@ -744,6 +741,9 @@ class FavoritePeerCard extends BasePeerCard { if (!gFFI.abModel.idContainBy(peer.id)) { menuItems.add(_addToAb(peer)); } + menuItems.add(_removeAction(peer.id, () async { + await bind.mainLoadFavPeers(); + })); return menuItems; } @@ -775,10 +775,10 @@ class DiscoveredPeerCard extends BasePeerCard { menuItems.add(_createShortCutAction(peer.id)); } menuItems.add(MenuEntryDivider()); - menuItems.add(_removeAction(peer.id, () async {})); if (!gFFI.abModel.idContainBy(peer.id)) { menuItems.add(_addToAb(peer)); } + menuItems.add(_removeAction(peer.id, () async {})); return menuItems; } @@ -811,13 +811,13 @@ class AddressBookPeerCard extends BasePeerCard { } menuItems.add(MenuEntryDivider()); menuItems.add(_renameAction(peer.id)); - menuItems.add(_removeAction(peer.id, () async {})); if (await bind.mainPeerHasPassword(id: peer.id)) { menuItems.add(_unrememberPasswordAction(peer.id)); } if (gFFI.abModel.tags.isNotEmpty) { menuItems.add(_editTagAction(peer.id)); } + menuItems.add(_removeAction(peer.id, () async {})); return menuItems; } From 02b5085e2b681e4e47075fd67a24d5873f479974 Mon Sep 17 00:00:00 2001 From: grummbeer Date: Thu, 23 Feb 2023 13:07:59 +0100 Subject: [PATCH 1990/2015] PeerCard. Menu. Make "remove" more visible --- flutter/lib/common/widgets/peer_card.dart | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index 7b24ec2e4..6ea6a97aa 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -515,9 +515,21 @@ abstract class BasePeerCard extends StatelessWidget { String id, Future Function() reloadFunc, {bool isLan = false}) { return MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - translate('Remove'), - style: style, + childBuilder: (TextStyle? style) => Row( + children: [ + Text( + translate('Remove'), + style: style?.copyWith(color: Colors.red), + ), + Expanded( + child: Align( + alignment: Alignment.centerRight, + child: Transform.scale( + scale: 0.8, + child: Icon(Icons.delete_forever, color: Colors.red), + ), + ).marginOnly(right: 4)), + ], ), proc: () { () async { @@ -697,6 +709,7 @@ class RecentPeerCard extends BasePeerCard { if (!gFFI.abModel.idContainBy(peer.id)) { menuItems.add(_addToAb(peer)); } + menuItems.add(MenuEntryDivider()); menuItems.add(_removeAction(peer.id, () async { await bind.mainLoadRecentPeers(); })); @@ -741,6 +754,7 @@ class FavoritePeerCard extends BasePeerCard { if (!gFFI.abModel.idContainBy(peer.id)) { menuItems.add(_addToAb(peer)); } + menuItems.add(MenuEntryDivider()); menuItems.add(_removeAction(peer.id, () async { await bind.mainLoadFavPeers(); })); @@ -778,6 +792,7 @@ class DiscoveredPeerCard extends BasePeerCard { if (!gFFI.abModel.idContainBy(peer.id)) { menuItems.add(_addToAb(peer)); } + menuItems.add(MenuEntryDivider()); menuItems.add(_removeAction(peer.id, () async {})); return menuItems; } @@ -817,6 +832,7 @@ class AddressBookPeerCard extends BasePeerCard { if (gFFI.abModel.tags.isNotEmpty) { menuItems.add(_editTagAction(peer.id)); } + menuItems.add(MenuEntryDivider()); menuItems.add(_removeAction(peer.id, () async {})); return menuItems; } From 6ae0456c45bb2f9419c35e6bb7dd4e2e8e5d0bec Mon Sep 17 00:00:00 2001 From: grummbeer Date: Thu, 23 Feb 2023 13:11:49 +0100 Subject: [PATCH 1991/2015] PeerCard. Menu. Change button text "remove" to "delete" --- flutter/lib/common/widgets/peer_card.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index 6ea6a97aa..4a376c588 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -518,7 +518,7 @@ abstract class BasePeerCard extends StatelessWidget { childBuilder: (TextStyle? style) => Row( children: [ Text( - translate('Remove'), + translate('Delete'), style: style?.copyWith(color: Colors.red), ), Expanded( From b139c90dd793e3dd65533aec12ed445f3f12ee51 Mon Sep 17 00:00:00 2001 From: grummbeer Date: Mon, 20 Feb 2023 20:25:37 +0100 Subject: [PATCH 1992/2015] PeerCard. Menu. Make "add to favorites" dynamic --- flutter/lib/common/widgets/peer_card.dart | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index 4a376c588..9d9d3d01f 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -689,6 +689,9 @@ class RecentPeerCard extends BasePeerCard { _connectAction(context, peer), _transferFileAction(context, peer.id), ]; + + final List favs = (await bind.mainGetFav()).toList(); + if (isDesktop && peer.platform != 'Android') { menuItems.add(_tcpTunnelingAction(context, peer.id)); } @@ -705,7 +708,13 @@ class RecentPeerCard extends BasePeerCard { if (await bind.mainPeerHasPassword(id: peer.id)) { menuItems.add(_unrememberPasswordAction(peer.id)); } - menuItems.add(_addFavAction(peer.id)); + + if (!favs.contains(peer.id)) { + menuItems.add(_addFavAction(peer.id)); + } else { + menuItems.add(_rmFavAction(peer.id, () async {})); + } + if (!gFFI.abModel.idContainBy(peer.id)) { menuItems.add(_addToAb(peer)); } From 819dc4e1a9643df899375240b217dcaccb4a8fd8 Mon Sep 17 00:00:00 2001 From: grummbeer Date: Fri, 17 Feb 2023 22:37:08 +0100 Subject: [PATCH 1993/2015] PeerCard. Menu. "add to favorites" visual indicator --- flutter/lib/common/widgets/peer_card.dart | 36 +++++++++++++++++++---- 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index 9d9d3d01f..fd0499305 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -565,9 +565,21 @@ abstract class BasePeerCard extends StatelessWidget { @protected MenuEntryBase _addFavAction(String id) { return MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - translate('Add to Favorites'), - style: style, + childBuilder: (TextStyle? style) => Row( + children: [ + Text( + translate('Add to Favorites'), + style: style, + ), + Expanded( + child: Align( + alignment: Alignment.centerRight, + child: Transform.scale( + scale: 0.8, + child: Icon(Icons.star_outline), + ), + ).marginOnly(right: 4)), + ], ), proc: () { () async { @@ -587,9 +599,21 @@ abstract class BasePeerCard extends StatelessWidget { MenuEntryBase _rmFavAction( String id, Future Function() reloadFunc) { return MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - translate('Remove from Favorites'), - style: style, + childBuilder: (TextStyle? style) => Row( + children: [ + Text( + translate('Remove from Favorites'), + style: style, + ), + Expanded( + child: Align( + alignment: Alignment.centerRight, + child: Transform.scale( + scale: 0.8, + child: Icon(Icons.star), + ), + ).marginOnly(right: 4)), + ], ), proc: () { () async { From b98581303e7e62f3dcdbfb04737f520345e0fc5d Mon Sep 17 00:00:00 2001 From: grummbeer Date: Thu, 23 Feb 2023 13:39:01 +0100 Subject: [PATCH 1994/2015] PeerCard. Menu. Hide "Add to Addressbook" if not logged in --- flutter/lib/common/widgets/peer_card.dart | 28 +++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index fd0499305..325dfd2ed 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -739,9 +739,15 @@ class RecentPeerCard extends BasePeerCard { menuItems.add(_rmFavAction(peer.id, () async {})); } - if (!gFFI.abModel.idContainBy(peer.id)) { + if (gFFI.userModel.userName.isNotEmpty) { + // if (!gFFI.abModel.idContainBy(peer.id)) { + // menuItems.add(_addToAb(peer)); + // } else { + // menuItems.add(_removeFromAb(peer)); + // } menuItems.add(_addToAb(peer)); } + menuItems.add(MenuEntryDivider()); menuItems.add(_removeAction(peer.id, () async { await bind.mainLoadRecentPeers(); @@ -784,9 +790,16 @@ class FavoritePeerCard extends BasePeerCard { menuItems.add(_rmFavAction(peer.id, () async { await bind.mainLoadFavPeers(); })); - if (!gFFI.abModel.idContainBy(peer.id)) { + + if (gFFI.userModel.userName.isNotEmpty) { + // if (!gFFI.abModel.idContainBy(peer.id)) { + // menuItems.add(_addToAb(peer)); + // } else { + // menuItems.add(_removeFromAb(peer)); + // } menuItems.add(_addToAb(peer)); } + menuItems.add(MenuEntryDivider()); menuItems.add(_removeAction(peer.id, () async { await bind.mainLoadFavPeers(); @@ -821,10 +834,16 @@ class DiscoveredPeerCard extends BasePeerCard { if (Platform.isWindows) { menuItems.add(_createShortCutAction(peer.id)); } - menuItems.add(MenuEntryDivider()); - if (!gFFI.abModel.idContainBy(peer.id)) { + + if (gFFI.userModel.userName.isNotEmpty) { + // if (!gFFI.abModel.idContainBy(peer.id)) { + // menuItems.add(_addToAb(peer)); + // } else { + // menuItems.add(_removeFromAb(peer)); + // } menuItems.add(_addToAb(peer)); } + menuItems.add(MenuEntryDivider()); menuItems.add(_removeAction(peer.id, () async {})); return menuItems; @@ -865,6 +884,7 @@ class AddressBookPeerCard extends BasePeerCard { if (gFFI.abModel.tags.isNotEmpty) { menuItems.add(_editTagAction(peer.id)); } + menuItems.add(MenuEntryDivider()); menuItems.add(_removeAction(peer.id, () async {})); return menuItems; From 27b8df617d2d0b4fa8ac851ea73851597aefb94c Mon Sep 17 00:00:00 2001 From: grummbeer Date: Mon, 20 Feb 2023 20:13:36 +0100 Subject: [PATCH 1995/2015] PeerCard. Menu. Remove peer also from favorites when deleted --- flutter/lib/common/widgets/peer_card.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index 325dfd2ed..657ba3ccf 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -536,6 +536,10 @@ abstract class BasePeerCard extends StatelessWidget { if (isLan) { // TODO } else { + final favs = (await bind.mainGetFav()).toList(); + if (favs.remove(id)) { + await bind.mainStoreFav(favs: favs); + } await bind.mainRemovePeer(id: id); } removePreference(id); From 0739820774c92e5d0688ec7397c559a720becf69 Mon Sep 17 00:00:00 2001 From: grummbeer Date: Tue, 21 Feb 2023 15:58:00 +0100 Subject: [PATCH 1996/2015] PeerCard. Menu. Add menu item "add to favorite" to DiscoveredPeerCard --- flutter/lib/common/widgets/peer_card.dart | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index 657ba3ccf..db2a90d9e 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -827,6 +827,9 @@ class DiscoveredPeerCard extends BasePeerCard { _connectAction(context, peer), _transferFileAction(context, peer.id), ]; + + final List favs = (await bind.mainGetFav()).toList(); + if (isDesktop && peer.platform != 'Android') { menuItems.add(_tcpTunnelingAction(context, peer.id)); } @@ -839,6 +842,12 @@ class DiscoveredPeerCard extends BasePeerCard { menuItems.add(_createShortCutAction(peer.id)); } + if (!favs.contains(peer.id)) { + menuItems.add(_addFavAction(peer.id)); + } else { + menuItems.add(_rmFavAction(peer.id, () async {})); + } + if (gFFI.userModel.userName.isNotEmpty) { // if (!gFFI.abModel.idContainBy(peer.id)) { // menuItems.add(_addToAb(peer)); From 8c3be1c8ced9eba83d682c02ac15bdfbdeaeb840 Mon Sep 17 00:00:00 2001 From: grummbeer Date: Tue, 21 Feb 2023 18:01:43 +0100 Subject: [PATCH 1997/2015] PeerCard. Menu. Add label to text input on "rename" --- flutter/lib/common/widgets/peer_card.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index db2a90d9e..8d4d58772 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -682,8 +682,9 @@ abstract class BasePeerCard extends StatelessWidget { child: TextFormField( controller: controller, autofocus: true, - decoration: - const InputDecoration(border: OutlineInputBorder()), + decoration: InputDecoration( + border: OutlineInputBorder(), + labelText: translate('Name')), ), ), ), From 37deaf67ccc62fdba61eff2ade71d14ef26d116f Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Thu, 23 Feb 2023 14:53:24 +0100 Subject: [PATCH 1998/2015] fix back icon --- flutter/lib/desktop/pages/file_manager_page.dart | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 0d55552af..39d66f568 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -752,9 +752,12 @@ class _FileManagerPageState extends State padding: EdgeInsets.only( right: 3, ), - child: SvgPicture.asset( - "assets/arrow.svg", - color: Theme.of(context).tabBarTheme.labelColor, + child: RotatedBox( + quarterTurns: 2, + child: SvgPicture.asset( + "assets/arrow.svg", + color: Theme.of(context).tabBarTheme.labelColor, + ), ), color: Theme.of(context).cardColor, hoverColor: Theme.of(context).hoverColor, From 135e0c8a99be4e9c629110bb65ee6e62cc8b45a3 Mon Sep 17 00:00:00 2001 From: fufesou Date: Thu, 23 Feb 2023 21:57:51 +0800 Subject: [PATCH 1999/2015] add mutex guard for arboard funcs Signed-off-by: fufesou --- src/common.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/common.rs b/src/common.rs index 02d367b5e..5f24fd5c3 100644 --- a/src/common.rs +++ b/src/common.rs @@ -52,6 +52,11 @@ lazy_static::lazy_static! { pub static ref DEVICE_NAME: Arc> = Default::default(); } +#[cfg(not(any(target_os = "android", target_os = "ios")))] +lazy_static::lazy_static! { + static ref ARBOARD_MTX: Arc> = Arc::new(Mutex::new(())); +} + pub fn global_init() -> bool { #[cfg(target_os = "linux")] { @@ -96,7 +101,11 @@ pub fn check_clipboard( ) -> Option { let side = if old.is_none() { "host" } else { "client" }; let old = if let Some(old) = old { old } else { &CONTENT }; - if let Ok(content) = ctx.get_text() { + let content = { + let _lock = ARBOARD_MTX.lock().unwrap(); + ctx.get_text() + }; + if let Ok(content) = content { if content.len() < 2_000_000 && !content.is_empty() { let changed = content != *old.lock().unwrap(); if changed { @@ -174,6 +183,7 @@ pub fn update_clipboard(clipboard: Clipboard, old: Option<&Arc>>) let side = if old.is_none() { "host" } else { "client" }; let old = if let Some(old) = old { old } else { &CONTENT }; *old.lock().unwrap() = content.clone(); + let _lock = ARBOARD_MTX.lock().unwrap(); allow_err!(ctx.set_text(content)); log::debug!("{} updated on {}", CLIPBOARD_NAME, side); } From ab9acc76fce569a984b7216121c41b5544c4f07b Mon Sep 17 00:00:00 2001 From: NicKoehler Date: Thu, 23 Feb 2023 16:49:31 +0100 Subject: [PATCH 2000/2015] backgroundcolor migration --- flutter/lib/common.dart | 15 ++++++++++----- flutter/lib/common/widgets/chat_page.dart | 6 ++++-- flutter/lib/common/widgets/peer_card.dart | 8 ++++---- flutter/lib/common/widgets/peer_tab_page.dart | 7 ++++--- flutter/lib/desktop/pages/connection_page.dart | 2 +- .../lib/desktop/pages/desktop_home_page.dart | 4 ++-- .../lib/desktop/pages/desktop_setting_page.dart | 2 +- flutter/lib/desktop/pages/desktop_tab_page.dart | 2 +- .../lib/desktop/pages/port_forward_page.dart | 17 +++++++++-------- .../desktop/pages/port_forward_tab_page.dart | 2 +- flutter/lib/desktop/pages/remote_page.dart | 2 +- flutter/lib/desktop/pages/remote_tab_page.dart | 2 +- flutter/lib/desktop/pages/server_page.dart | 9 +++------ 13 files changed, 42 insertions(+), 36 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 6d3e4c3b7..e1b9ac90c 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -45,9 +45,10 @@ var isWeb = false; var isWebDesktop = false; var version = ""; int androidVersion = 0; + /// Incriment count for textureId. int _textureId = 0; -int get newTextureId => _textureId ++; +int get newTextureId => _textureId++; final textureRenderer = TextureRgbaRenderer(); /// only available for Windows target @@ -165,7 +166,6 @@ class MyTheme { static ThemeData lightTheme = ThemeData( brightness: Brightness.light, - backgroundColor: Color(0xFFEEEEEE), hoverColor: Color.fromARGB(255, 224, 224, 224), scaffoldBackgroundColor: Color(0xFFFFFFFF), textTheme: const TextTheme( @@ -177,7 +177,6 @@ class MyTheme { labelLarge: TextStyle(fontSize: 16.0, color: MyTheme.accent80)), cardColor: Color(0xFFEEEEEE), hintColor: Color(0xFFAAAAAA), - primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, tabBarTheme: const TabBarTheme( labelColor: Colors.black87, @@ -190,6 +189,10 @@ class MyTheme { style: ButtonStyle(splashFactory: NoSplash.splashFactory), ) : null, + colorScheme: ColorScheme.fromSwatch(primarySwatch: Colors.blue).copyWith( + brightness: Brightness.light, + background: Color(0xFFEEEEEE), + ), ).copyWith( extensions: >[ ColorThemeExtension.light, @@ -198,7 +201,6 @@ class MyTheme { ); static ThemeData darkTheme = ThemeData( brightness: Brightness.dark, - backgroundColor: Color(0xFF24252B), hoverColor: Color.fromARGB(255, 45, 46, 53), scaffoldBackgroundColor: Color(0xFF18191E), textTheme: const TextTheme( @@ -209,7 +211,6 @@ class MyTheme { labelLarge: TextStyle( fontSize: 16.0, fontWeight: FontWeight.bold, color: accent80)), cardColor: Color(0xFF24252B), - primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, tabBarTheme: const TabBarTheme( labelColor: Colors.white70, @@ -227,6 +228,10 @@ class MyTheme { : null, checkboxTheme: const CheckboxThemeData(checkColor: MaterialStatePropertyAll(dark)), + colorScheme: ColorScheme.fromSwatch( + brightness: Brightness.dark, + primarySwatch: Colors.blue, + ).copyWith(background: Color(0xFF24252B)), ).copyWith( extensions: >[ ColorThemeExtension.dark, diff --git a/flutter/lib/common/widgets/chat_page.dart b/flutter/lib/common/widgets/chat_page.dart index 62f81b797..c1991633a 100644 --- a/flutter/lib/common/widgets/chat_page.dart +++ b/flutter/lib/common/widgets/chat_page.dart @@ -75,7 +75,8 @@ class ChatPage extends StatelessWidget implements PageShape { hintText: "${translate('Write a message')}...", filled: true, - fillColor: Theme.of(context).backgroundColor, + fillColor: + Theme.of(context).colorScheme.background, contentPadding: EdgeInsets.all(10), border: OutlineInputBorder( borderRadius: BorderRadius.circular(6), @@ -88,7 +89,8 @@ class ChatPage extends StatelessWidget implements PageShape { : defaultInputDecoration( hintText: "${translate('Write a message')}...", - fillColor: Theme.of(context).backgroundColor), + fillColor: + Theme.of(context).colorScheme.background), sendButtonBuilder: defaultSendButton( padding: EdgeInsets.symmetric( horizontal: 6, vertical: 0), diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index f1b94ecdf..0a175139f 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -170,8 +170,8 @@ class _PeerCardState extends State<_PeerCard> ), Expanded( child: Container( - decoration: - BoxDecoration(color: Theme.of(context).backgroundColor), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.background), child: Row( children: [ Expanded( @@ -266,7 +266,7 @@ class _PeerCardState extends State<_PeerCard> ), ), Container( - color: Theme.of(context).backgroundColor, + color: Theme.of(context).colorScheme.background, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -1090,7 +1090,7 @@ class ActionMore extends StatelessWidget { radius: 14, backgroundColor: _hover.value ? Theme.of(context).scaffoldBackgroundColor - : Theme.of(context).backgroundColor, + : Theme.of(context).colorScheme.background, child: Icon(Icons.more_vert, size: 18, color: _hover.value diff --git a/flutter/lib/common/widgets/peer_tab_page.dart b/flutter/lib/common/widgets/peer_tab_page.dart index 4080f9c11..da7e37e6b 100644 --- a/flutter/lib/common/widgets/peer_tab_page.dart +++ b/flutter/lib/common/widgets/peer_tab_page.dart @@ -156,7 +156,7 @@ class _PeerTabPageState extends State padding: const EdgeInsets.symmetric(horizontal: 8), decoration: BoxDecoration( color: model.currentTab == t - ? Theme.of(context).backgroundColor + ? Theme.of(context).colorScheme.background : null, borderRadius: BorderRadius.circular(isDesktop ? 2 : 6), ), @@ -231,7 +231,8 @@ class _PeerTabPageState extends State Widget _createPeerViewTypeSwitch(BuildContext context) { final textColor = Theme.of(context).textTheme.titleLarge?.color; - final activeDeco = BoxDecoration(color: Theme.of(context).backgroundColor); + final activeDeco = + BoxDecoration(color: Theme.of(context).colorScheme.background); return Row( children: [PeerUiType.grid, PeerUiType.list] .map((type) => Obx( @@ -351,7 +352,7 @@ class _PeerSearchBarState extends State { return Container( width: 120, decoration: BoxDecoration( - color: Theme.of(context).backgroundColor, + color: Theme.of(context).colorScheme.background, borderRadius: BorderRadius.circular(6), ), child: Obx(() => Row( diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 646ee2a8d..4aad66eee 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -164,7 +164,7 @@ class _ConnectionPageState extends State width: 320 + 20 * 2, padding: const EdgeInsets.fromLTRB(20, 24, 20, 22), decoration: BoxDecoration( - color: Theme.of(context).backgroundColor, + color: Theme.of(context).colorScheme.background, borderRadius: const BorderRadius.all(Radius.circular(13)), ), child: Ink( diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index ff99c9dc8..dfa5762b0 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -71,7 +71,7 @@ class _DesktopHomePageState extends State value: gFFI.serverModel, child: Container( width: 200, - color: Theme.of(context).backgroundColor, + color: Theme.of(context).colorScheme.background, child: DesktopScrollWrapper( scrollController: _leftPaneScrollController, child: SingleChildScrollView( @@ -185,7 +185,7 @@ class _DesktopHomePageState extends State radius: 15, backgroundColor: hover.value ? Theme.of(context).scaffoldBackgroundColor - : Theme.of(context).backgroundColor, + : Theme.of(context).colorScheme.background, child: Icon( Icons.more_vert_outlined, size: 20, diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 971c713ce..06a79093a 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -108,7 +108,7 @@ class _DesktopSettingPageState extends State Widget build(BuildContext context) { super.build(context); return Scaffold( - backgroundColor: Theme.of(context).backgroundColor, + backgroundColor: Theme.of(context).colorScheme.background, body: Row( children: [ SizedBox( diff --git a/flutter/lib/desktop/pages/desktop_tab_page.dart b/flutter/lib/desktop/pages/desktop_tab_page.dart index 35d5a61ef..053a2d8a2 100644 --- a/flutter/lib/desktop/pages/desktop_tab_page.dart +++ b/flutter/lib/desktop/pages/desktop_tab_page.dart @@ -65,7 +65,7 @@ class _DesktopTabPageState extends State { Widget build(BuildContext context) { final tabWidget = Container( child: Scaffold( - backgroundColor: Theme.of(context).backgroundColor, + backgroundColor: Theme.of(context).colorScheme.background, body: DesktopTab( controller: tabController, tail: ActionIcon( diff --git a/flutter/lib/desktop/pages/port_forward_page.dart b/flutter/lib/desktop/pages/port_forward_page.dart index 2ac6bf23a..ae070b47b 100644 --- a/flutter/lib/desktop/pages/port_forward_page.dart +++ b/flutter/lib/desktop/pages/port_forward_page.dart @@ -91,7 +91,7 @@ class _PortForwardPageState extends State Flexible( child: Container( decoration: BoxDecoration( - color: Theme.of(context).backgroundColor, + color: Theme.of(context).colorScheme.background, border: Border.all(width: 1, color: MyTheme.border)), child: widget.isRDP ? buildRdp(context) : buildTunnel(context), @@ -134,7 +134,7 @@ class _PortForwardPageState extends State return Theme( data: Theme.of(context) - .copyWith(backgroundColor: Theme.of(context).backgroundColor), + .copyWith(backgroundColor: Theme.of(context).colorScheme.background), child: Obx(() => ListView.builder( controller: ScrollController(), itemCount: pfs.length + 2, @@ -169,7 +169,8 @@ class _PortForwardPageState extends State return Container( height: _kRowHeight, - decoration: BoxDecoration(color: Theme.of(context).backgroundColor), + decoration: + BoxDecoration(color: Theme.of(context).colorScheme.background), child: Row(children: [ buildTunnelInputCell(context, controller: localPortController, @@ -229,7 +230,7 @@ class _PortForwardPageState extends State borderSide: BorderSide(color: MyTheme.color(context).border!)), focusedBorder: OutlineInputBorder( borderSide: BorderSide(color: MyTheme.color(context).border!)), - fillColor: Theme.of(context).backgroundColor, + fillColor: Theme.of(context).colorScheme.background, contentPadding: const EdgeInsets.all(10), hintText: hint, hintStyle: @@ -251,7 +252,7 @@ class _PortForwardPageState extends State ? MyTheme.currentThemeMode() == ThemeMode.dark ? const Color(0xFF202020) : const Color(0xFFF4F5F6) - : Theme.of(context).backgroundColor), + : Theme.of(context).colorScheme.background), child: Row(children: [ text(pf.localPort.toString()), const SizedBox(width: _kColumn1Width), @@ -293,7 +294,7 @@ class _PortForwardPageState extends State ).marginOnly(left: _kTextLeftMargin)); return Theme( data: Theme.of(context) - .copyWith(backgroundColor: Theme.of(context).backgroundColor), + .copyWith(backgroundColor: Theme.of(context).colorScheme.background), child: ListView.builder( controller: ScrollController(), itemCount: 2, @@ -312,8 +313,8 @@ class _PortForwardPageState extends State } else { return Container( height: _kRowHeight, - decoration: - BoxDecoration(color: Theme.of(context).backgroundColor), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.background), child: Row(children: [ Expanded( child: Align( diff --git a/flutter/lib/desktop/pages/port_forward_tab_page.dart b/flutter/lib/desktop/pages/port_forward_tab_page.dart index ee5dd9b53..f2d75d00f 100644 --- a/flutter/lib/desktop/pages/port_forward_tab_page.dart +++ b/flutter/lib/desktop/pages/port_forward_tab_page.dart @@ -96,7 +96,7 @@ class _PortForwardTabPageState extends State { decoration: BoxDecoration( border: Border.all(color: MyTheme.color(context).border!)), child: Scaffold( - backgroundColor: Theme.of(context).backgroundColor, + backgroundColor: Theme.of(context).colorScheme.background, body: DesktopTab( controller: tabController, onWindowCloseButton: () async { diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index e52334512..ab0daece7 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -225,7 +225,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 ef3a0dd04..0deb646c0 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -141,7 +141,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, diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index 252e1cd12..45591b79b 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -49,10 +49,7 @@ class _DesktopServerPageState extends State @override void onWindowClose() { - Future.wait([ - gFFI.serverModel.closeAll(), - gFFI.close() - ]).then((_) { + Future.wait([gFFI.serverModel.closeAll(), gFFI.close()]).then((_) { if (Platform.isMacOS) { RdPlatformChannel.instance.terminate(); } else { @@ -82,7 +79,7 @@ class _DesktopServerPageState extends State decoration: BoxDecoration( border: Border.all(color: MyTheme.color(context).border!)), child: Scaffold( - backgroundColor: Theme.of(context).backgroundColor, + backgroundColor: Theme.of(context).colorScheme.background, body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.start, @@ -189,7 +186,7 @@ class ConnectionManagerState extends State { windowManager.startDragging(); }, child: Container( - color: Theme.of(context).backgroundColor, + color: Theme.of(context).colorScheme.background, ), ), ), From 080c98769437e0a931fc9073bf309e000e19a075 Mon Sep 17 00:00:00 2001 From: jimmyGALLAND <64364019+jimmyGALLAND@users.noreply.github.com> Date: Thu, 23 Feb 2023 22:17:59 +0100 Subject: [PATCH 2001/2015] Update fr.rs --- src/lang/fr.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 1f6e9f55b..50cb29938 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -37,10 +37,10 @@ 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", ""), + ("Your new ID", "Votre nouvel ID"), + ("length %min% to %max%", "longueur de %min% à %max%"), + ("starts with a letter", "commence par une lettre"), + ("allowed characters", "caractères autorisés"), ("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"), @@ -89,7 +89,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show Hidden Files", "Afficher les fichiers cachés"), ("Receive", "Recevoir"), ("Send", "Envoyer"), - ("Refresh File", "Actualiser le fichier"), + ("Refresh File", "Rafraîchir le contenu"), ("Local", "Local"), ("Remote", "Distant"), ("Remote Computer", "Ordinateur distant"), From 91a2a5b56e283b208a3822c6fccd81c3fb8ea599 Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 9 Feb 2023 15:53:51 +0800 Subject: [PATCH 2002/2015] win resolution && api Signed-off-by: 21pages --- flutter/lib/models/model.dart | 41 +++++++++++++ libs/hbb_common/protos/message.proto | 10 ++++ src/flutter.rs | 25 ++++++++ src/flutter_ffi.rs | 8 ++- src/platform/mod.rs | 10 +++- src/platform/windows.rs | 90 +++++++++++++++++++++++++++- src/server/connection.rs | 48 +++++++++++++++ src/server/video_service.rs | 15 ++++- src/ui_session_interface.rs | 12 ++++ 9 files changed, 255 insertions(+), 4 deletions(-) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 5ef72a0af..f4efe2f08 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -270,6 +270,7 @@ class FfiModel with ChangeNotifier { parent.target?.canvasModel.updateViewStyle(); } parent.target?.recordingModel.onSwitchDisplay(); + handleResolutions(peerId, evt["resolutions"]); notifyListeners(); } @@ -437,10 +438,35 @@ class FfiModel with ChangeNotifier { } Map features = json.decode(evt['features']); _pi.features.privacyMode = features['privacy_mode'] == 1; + handleResolutions(peerId, evt["resolutions"]); } notifyListeners(); } + handleResolutions(String id, dynamic resolutions) { + try { + final List dynamicArray = jsonDecode(resolutions as String); + List arr = List.empty(growable: true); + for (int i = 0; i < dynamicArray.length; i++) { + var width = dynamicArray[i]["width"]; + var height = dynamicArray[i]["height"]; + if (width is int && width > 0 && height is int && height > 0) { + arr.add(Resolution(width, height)); + } + } + arr.sort((a, b) { + if (b.width != a.width) { + return b.width - a.width; + } else { + return b.height - a.height; + } + }); + _pi.resolutions = arr; + } catch (e) { + debugPrint("Failed to parse resolutions:$e"); + } + } + /// Handle the peer info synchronization event based on [evt]. handleSyncPeerInfo(Map evt, String peerId) async { if (evt['displays'] != null) { @@ -458,6 +484,9 @@ class FfiModel with ChangeNotifier { } _pi.displays = newDisplays; stateGlobal.displaysCount.value = _pi.displays.length; + if (_pi.currentDisplay >= 0 && _pi.currentDisplay < _pi.displays.length) { + _display = _pi.displays[_pi.currentDisplay]; + } } notifyListeners(); } @@ -1532,6 +1561,17 @@ class Display { } } +class Resolution { + int width = 0; + int height = 0; + Resolution(this.width, this.height); + + @override + String toString() { + return 'Resolution($width,$height)'; + } +} + class Features { bool privacyMode = false; } @@ -1545,6 +1585,7 @@ class PeerInfo { int currentDisplay = 0; List displays = []; Features features = Features(); + List resolutions = []; } const canvasKey = 'canvas'; diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index 2a3fd05b4..be3a1e51e 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -90,6 +90,7 @@ message PeerInfo { int32 conn_id = 8; Features features = 9; SupportedEncoding encoding = 10; + SupportedResolutions resolutions = 11; } message LoginResponse { @@ -416,6 +417,13 @@ message Cliprdr { } } +message Resolution { + int32 width = 1; + int32 height = 2; +} + +message SupportedResolutions { repeated Resolution resolutions = 1; } + message SwitchDisplay { int32 display = 1; sint32 x = 2; @@ -423,6 +431,7 @@ message SwitchDisplay { int32 width = 4; int32 height = 5; bool cursor_embedded = 6; + SupportedResolutions resolutions = 7; } message PermissionInfo { @@ -597,6 +606,7 @@ message Misc { bool portable_service_running = 20; SwitchSidesRequest switch_sides_request = 21; SwitchBack switch_back = 22; + Resolution change_resolution = 24; } } diff --git a/src/flutter.rs b/src/flutter.rs index d366a0eda..ea73eb925 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -480,6 +480,7 @@ impl InvokeUiSession for FlutterHandler { features.insert("privacy_mode", 0); } let features = serde_json::ser::to_string(&features).unwrap_or("".to_owned()); + let resolutions = serialize_resolutions(&pi.resolutions.resolutions); *self.peer_info.write().unwrap() = pi.clone(); self.push_event( "peer_info", @@ -492,6 +493,7 @@ impl InvokeUiSession for FlutterHandler { ("version", &pi.version), ("features", &features), ("current_display", &pi.current_display.to_string()), + ("resolutions", &resolutions), ], ); } @@ -529,6 +531,7 @@ impl InvokeUiSession for FlutterHandler { } fn switch_display(&self, display: &SwitchDisplay) { + let resolutions = serialize_resolutions(&display.resolutions.resolutions); self.push_event( "switch_display", vec![ @@ -548,6 +551,7 @@ impl InvokeUiSession for FlutterHandler { } .to_string(), ), + ("resolutions", &resolutions), ], ); } @@ -861,6 +865,27 @@ pub fn set_cur_session_id(id: String) { } } +#[inline] +fn serialize_resolutions(resolutions: &Vec) -> String { + #[derive(Debug, serde::Serialize)] + struct ResolutionSerde { + width: i32, + height: i32, + } + + let mut v = vec![]; + resolutions + .iter() + .map(|r| { + v.push(ResolutionSerde { + width: r.width, + height: r.height, + }) + }) + .count(); + serde_json::ser::to_string(&v).unwrap_or("".to_string()) +} + #[no_mangle] #[cfg(not(feature = "flutter_texture_render"))] pub fn session_get_rgba_size(id: *const char) -> usize { diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 14906d568..8a8bf4de4 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -529,7 +529,13 @@ pub fn session_switch_sides(id: String) { } } -pub fn session_set_size(_id: String, _width: i32, _height: i32) { +pub fn session_change_resolution(id: String, width: i32, height: i32) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.change_resolution(width, height); + } +} + +pub fn session_set_size(_id: String, _width: i32, _height: i32) { #[cfg(feature = "flutter_texture_render")] if let Some(session) = SESSIONS.write().unwrap().get_mut(&_id) { session.set_size(_width, _height); diff --git a/src/platform/mod.rs b/src/platform/mod.rs index ed5fcfaa1..ad058d4c0 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -74,5 +74,13 @@ mod tests { assert!(!get_cursor_pos().is_none()); } } -} + #[cfg(not(any(target_os = "android", target_os = "ios")))] + #[test] + fn test_resolution() { + let name = r"\\.\DISPLAY1"; + println!("current:{:?}", current_resolution(name)); + println!("change:{:?}", change_resolution(name, 2880, 1800)); + println!("resolutions:{:?}", resolutions(name)); + } +} diff --git a/src/platform/windows.rs b/src/platform/windows.rs index bd6a1fc4c..6b3f8013c 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -5,7 +5,9 @@ use crate::license::*; use hbb_common::{ allow_err, bail, config::{self, Config}, - log, sleep, timeout, tokio, + log, + message_proto::Resolution, + sleep, timeout, tokio, }; use std::io::prelude::*; use std::{ @@ -1784,3 +1786,89 @@ pub fn set_path_permission(dir: &PathBuf, permission: &str) -> ResultType<()> { .spawn()?; Ok(()) } + +pub fn resolutions(name: &str) -> Vec { + unsafe { + let mut dm: DEVMODEW = std::mem::zeroed(); + let wname = wide_string(name); + let len = if wname.len() <= dm.dmDeviceName.len() { + wname.len() + } else { + dm.dmDeviceName.len() + }; + std::ptr::copy_nonoverlapping(wname.as_ptr(), dm.dmDeviceName.as_mut_ptr(), len); + dm.dmSize = std::mem::size_of::() as _; + let mut v = vec![]; + let mut num = 0; + loop { + if EnumDisplaySettingsW(NULL as _, num, &mut dm) == 0 { + break; + } + let r = Resolution { + width: dm.dmPelsWidth as _, + height: dm.dmPelsHeight as _, + ..Default::default() + }; + if !v.contains(&r) { + v.push(r); + } + num += 1; + } + v + } +} + +pub fn current_resolution(name: &str) -> ResultType { + unsafe { + let mut dm: DEVMODEW = std::mem::zeroed(); + dm.dmSize = std::mem::size_of::() as _; + let wname = wide_string(name); + if EnumDisplaySettingsW(wname.as_ptr(), ENUM_CURRENT_SETTINGS, &mut dm) == 0 { + bail!( + "failed to get currrent resolution, errno={}", + GetLastError() + ); + } + let r = Resolution { + width: dm.dmPelsWidth as _, + height: dm.dmPelsHeight as _, + ..Default::default() + }; + Ok(r) + } +} + +pub fn change_resolution(name: &str, width: usize, height: usize) -> ResultType<()> { + unsafe { + let mut dm: DEVMODEW = std::mem::zeroed(); + if FALSE == EnumDisplaySettingsW(NULL as _, ENUM_CURRENT_SETTINGS, &mut dm) { + bail!("EnumDisplaySettingsW failed, errno={}", GetLastError()); + } + let wname = wide_string(name); + let len = if wname.len() <= dm.dmDeviceName.len() { + wname.len() + } else { + dm.dmDeviceName.len() + }; + std::ptr::copy_nonoverlapping(wname.as_ptr(), dm.dmDeviceName.as_mut_ptr(), len); + dm.dmSize = std::mem::size_of::() as _; + dm.dmPelsWidth = width as _; + dm.dmPelsHeight = height as _; + dm.dmFields = DM_PELSHEIGHT | DM_PELSWIDTH; + let res = ChangeDisplaySettingsExW( + wname.as_ptr(), + &mut dm, + NULL as _, + CDS_UPDATEREGISTRY | CDS_GLOBAL | CDS_RESET, + NULL, + ); + if res != DISP_CHANGE_SUCCESSFUL { + bail!( + "ChangeDisplaySettingsExW failed, res={}, errno={}", + res, + GetLastError() + ); + } + Ok(()) + } +} diff --git a/src/server/connection.rs b/src/server/connection.rs index d2eb21ee5..85fcb676b 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -123,6 +123,7 @@ pub struct Connection { #[cfg(windows)] portable: PortableState, from_switch: bool, + origin_resolution: HashMap, voice_call_request_timestamp: Option, audio_input_device_before_voice_call: Option, } @@ -228,6 +229,7 @@ impl Connection { #[cfg(windows)] portable: Default::default(), from_switch: false, + origin_resolution: Default::default(), audio_sender: None, voice_call_request_timestamp: None, audio_input_device_before_voice_call: None, @@ -533,6 +535,8 @@ impl Connection { conn.post_conn_audit(json!({ "action": "close", })); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + conn.reset_resolution(); ALIVE_CONNS.lock().unwrap().retain(|&c| c != id); if let Some(s) = conn.server.upgrade() { s.write().unwrap().remove_connection(&conn.inner); @@ -881,6 +885,16 @@ impl Connection { ..Default::default() }) .into(); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + pi.resolutions = Some(SupportedResolutions { + resolutions: video_service::get_current_display_name() + .map(|name| crate::platform::resolutions(&name)) + .unwrap_or(vec![]), + ..Default::default() + }) + .into(); + } let mut sub_service = false; if self.file_transfer.is_some() { @@ -1597,6 +1611,26 @@ impl Connection { return false; } } + #[cfg(not(any(target_os = "android", target_os = "ios")))] + Some(misc::Union::ChangeResolution(r)) => { + if self.keyboard { + if let Ok(name) = video_service::get_current_display_name() { + if let Ok(current) = crate::platform::current_resolution(&name) { + if let Err(e) = crate::platform::change_resolution( + &name, + r.width as _, + r.height as _, + ) { + log::error!("change resolution failed:{:?}", e); + } else { + if !self.origin_resolution.contains_key(&name) { + self.origin_resolution.insert(name, current); + } + } + } + } + } + } _ => {} }, Some(message::Union::AudioFrame(frame)) => { @@ -1937,6 +1971,20 @@ impl Connection { } } } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + fn reset_resolution(&self) { + self.origin_resolution + .iter() + .map(|(name, r)| { + if let Err(e) = + crate::platform::change_resolution(&name, r.width as _, r.height as _) + { + log::error!("change resolution failed:{:?}", e); + } + }) + .count(); + } } pub fn insert_switch_sides_uuid(id: String, uuid: uuid::Uuid) { diff --git a/src/server/video_service.rs b/src/server/video_service.rs index 52b1717c4..a9a9fd9ab 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -356,7 +356,7 @@ fn get_capturer(use_yuv: bool, portable_service_running: bool) -> ResultType ResultType ResultType<()> { width: c.width as _, height: c.height as _, cursor_embedded: capture_cursor_embedded(), + #[cfg(not(any(target_os = "android", target_os = "ios")))] + resolutions: Some(SupportedResolutions { + resolutions: get_current_display_name() + .map(|name| crate::platform::resolutions(&name)) + .unwrap_or(vec![]), + ..SupportedResolutions::default() + }) + .into(), ..Default::default() }); let mut msg_out = Message::new(); @@ -992,6 +1001,10 @@ pub fn get_current_display() -> ResultType<(usize, usize, Display)> { get_current_display_2(try_get_displays()?) } +pub fn get_current_display_name() -> ResultType { + Ok(get_current_display_2(try_get_displays()?)?.2.name()) +} + #[cfg(windows)] fn start_uac_elevation_check() { static START: Once = Once::new(); diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 5fbf2f4e7..fd5a7d9c0 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -713,6 +713,18 @@ impl Session { } } + pub fn change_resolution(&self, width: i32, height: i32) { + let mut misc = Misc::new(); + misc.set_change_resolution(Resolution { + width, + height, + ..Default::default() + }); + let mut msg = Message::new(); + msg.set_misc(misc); + self.send(Data::Message(msg)); + } + pub fn request_voice_call(&self) { self.send(Data::NewVoiceCall); } From 18a66749a1a7d0b767cb05f197bebc309c7bcbd0 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sat, 11 Feb 2023 19:04:33 +0800 Subject: [PATCH 2003/2015] linux x11 resolution Signed-off-by: 21pages --- Cargo.lock | 72 +++++++++++++++++++++++++++++++++-- Cargo.toml | 1 + libs/scrap/src/common/x11.rs | 4 +- libs/scrap/src/x11/display.rs | 7 ++++ libs/scrap/src/x11/ffi.rs | 18 +++++++++ libs/scrap/src/x11/iter.rs | 30 +++++++++++++++ src/platform/linux.rs | 55 +++++++++++++++++++++++++- 7 files changed, 180 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8483cbac1..a2cdf91a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1159,14 +1159,38 @@ dependencies = [ "zvariant", ] +[[package]] +name = "darling" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858" +dependencies = [ + "darling_core 0.10.2", + "darling_macro 0.10.2", +] + [[package]] name = "darling" version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.13.4", + "darling_macro 0.13.4", +] + +[[package]] +name = "darling_core" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2 1.0.47", + "quote 1.0.21", + "strsim 0.9.3", + "syn 1.0.105", ] [[package]] @@ -1183,13 +1207,24 @@ dependencies = [ "syn 1.0.105", ] +[[package]] +name = "darling_macro" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72" +dependencies = [ + "darling_core 0.10.2", + "quote 1.0.21", + "syn 1.0.105", +] + [[package]] name = "darling_macro" version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" dependencies = [ - "darling_core", + "darling_core 0.13.4", "quote 1.0.21", "syn 1.0.105", ] @@ -1389,6 +1424,18 @@ dependencies = [ "syn 1.0.105", ] +[[package]] +name = "derive_setters" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1cf41b4580a37cca5ef2ada2cc43cf5d6be3983f4522e83010d67ab6925e84b" +dependencies = [ + "darling 0.10.2", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", +] + [[package]] name = "detect-desktop-environment" version = "0.2.0" @@ -3585,7 +3632,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0df7ac00c4672f9d5aece54ee3347520b7e20f158656c7db2e6de01902eb7a6c" dependencies = [ - "darling", + "darling 0.13.4", "proc-macro-crate 1.2.1", "proc-macro2 1.0.47", "quote 1.0.21", @@ -4944,6 +4991,7 @@ dependencies = [ "winreg 0.10.1", "winres", "wol-rs", + "xrandr-parser", ] [[package]] @@ -5469,6 +5517,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" +[[package]] +name = "strsim" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" + [[package]] name = "strsim" version = "0.10.0" @@ -6965,6 +7019,16 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" +[[package]] +name = "xrandr-parser" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5af43ba661cee58bd86b9f81a899e45a15ac7f42fa4401340f73c0c2950030c1" +dependencies = [ + "derive_setters", + "serde 1.0.149", +] + [[package]] name = "yaml-rust" version = "0.4.5" diff --git a/Cargo.toml b/Cargo.toml index c20366983..f93f776a0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -121,6 +121,7 @@ mouce = { git="https://github.com/fufesou/mouce.git" } evdev = { git="https://github.com/fufesou/evdev" } dbus = "0.9" dbus-crossroads = "0.5" +xrandr-parser = "0.3.0" [target.'cfg(target_os = "android")'.dependencies] android_logger = "0.11" diff --git a/libs/scrap/src/common/x11.rs b/libs/scrap/src/common/x11.rs index 61112bff7..6e3fc94fb 100644 --- a/libs/scrap/src/common/x11.rs +++ b/libs/scrap/src/common/x11.rs @@ -1,4 +1,4 @@ -use crate::{x11, common::TraitCapturer}; +use crate::{common::TraitCapturer, x11}; use std::{io, ops, time::Duration}; pub struct Capturer(x11::Capturer); @@ -90,6 +90,6 @@ impl Display { } pub fn name(&self) -> String { - "".to_owned() + self.0.name() } } diff --git a/libs/scrap/src/x11/display.rs b/libs/scrap/src/x11/display.rs index 0c5ba5035..a33903caa 100644 --- a/libs/scrap/src/x11/display.rs +++ b/libs/scrap/src/x11/display.rs @@ -9,6 +9,7 @@ pub struct Display { default: bool, rect: Rect, root: xcb_window_t, + name: String, } #[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)] @@ -25,12 +26,14 @@ impl Display { default: bool, rect: Rect, root: xcb_window_t, + name: String, ) -> Display { Display { server, default, rect, root, + name, } } @@ -52,4 +55,8 @@ impl Display { pub fn root(&self) -> xcb_window_t { self.root } + + pub fn name(&self) -> String { + self.name.clone() + } } diff --git a/libs/scrap/src/x11/ffi.rs b/libs/scrap/src/x11/ffi.rs index 500f57615..b34fed416 100644 --- a/libs/scrap/src/x11/ffi.rs +++ b/libs/scrap/src/x11/ffi.rs @@ -65,6 +65,21 @@ extern "C" { ) -> xcb_randr_monitor_info_iterator_t; pub fn xcb_randr_monitor_info_next(i: *mut xcb_randr_monitor_info_iterator_t); + + pub fn xcb_get_atom_name( + c: *mut xcb_connection_t, + atom: xcb_atom_t, + ) -> xcb_get_atom_name_cookie_t; + + pub fn xcb_get_atom_name_reply( + c: *mut xcb_connection_t, + cookie: xcb_get_atom_name_cookie_t, + e: *mut *mut xcb_generic_error_t, + ) -> *const xcb_get_atom_name_reply_t; + + pub fn xcb_get_atom_name_name(reply: *const xcb_get_atom_name_request_t) -> *const u8; + + pub fn xcb_get_atom_name_name_length(reply: *const xcb_get_atom_name_reply_t) -> i32; } pub const XCB_IMAGE_FORMAT_Z_PIXMAP: u8 = 2; @@ -78,6 +93,9 @@ pub type xcb_timestamp_t = u32; pub type xcb_colormap_t = u32; pub type xcb_shm_seg_t = u32; pub type xcb_drawable_t = u32; +pub type xcb_get_atom_name_cookie_t = u32; +pub type xcb_get_atom_name_reply_t = u32; +pub type xcb_get_atom_name_request_t = xcb_get_atom_name_reply_t; #[repr(C)] pub struct xcb_setup_t { diff --git a/libs/scrap/src/x11/iter.rs b/libs/scrap/src/x11/iter.rs index 406c27352..28609376b 100644 --- a/libs/scrap/src/x11/iter.rs +++ b/libs/scrap/src/x11/iter.rs @@ -1,3 +1,4 @@ +use std::ffi::CString; use std::ptr; use std::rc::Rc; @@ -64,6 +65,7 @@ impl Iterator for DisplayIter { if inner.rem != 0 { unsafe { let data = &*inner.data; + let name = get_atom_name(self.server.raw(), data.name); let display = Display::new( self.server.clone(), @@ -75,6 +77,7 @@ impl Iterator for DisplayIter { h: data.height, }, root, + name, ); xcb_randr_monitor_info_next(inner); @@ -91,3 +94,30 @@ impl Iterator for DisplayIter { } } } + +fn get_atom_name(conn: *mut xcb_connection_t, atom: xcb_atom_t) -> String { + let empty = "".to_owned(); + if atom == 0 { + return empty; + } + unsafe { + let mut e: xcb_generic_error_t = std::mem::zeroed(); + let reply = xcb_get_atom_name_reply( + conn, + xcb_get_atom_name(conn, atom), + &mut ((&mut e) as *mut xcb_generic_error_t) as _, + ); + if reply == std::ptr::null() { + return empty; + } + let length = xcb_get_atom_name_name_length(reply); + let name = xcb_get_atom_name_name(reply); + let mut v = vec![0u8; length as _]; + std::ptr::copy_nonoverlapping(name as _, v.as_mut_ptr(), length as _); + libc::free(reply as *mut _); + if let Ok(s) = CString::new(v) { + return s.to_string_lossy().to_string(); + } + empty + } +} diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 32c32efb9..08e343d49 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -1,7 +1,7 @@ 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::{allow_err, anyhow::anyhow, bail, log, message_proto::Resolution}; use std::{ cell::RefCell, path::PathBuf, @@ -10,6 +10,7 @@ use std::{ Arc, }, }; +use xrandr_parser::Parser; type Xdo = *const c_void; @@ -641,3 +642,55 @@ pub fn get_double_click_time() -> u32 { double_click_time } } + +pub fn resolutions(name: &str) -> Vec { + let mut v = vec![]; + let mut parser = Parser::new(); + if parser.parse().is_ok() { + if let Ok(connector) = parser.get_connector(name) { + if let Ok(resolutions) = &connector.available_resolutions() { + for r in resolutions { + if let Ok(width) = r.horizontal.parse::() { + if let Ok(height) = r.vertical.parse::() { + let resolution = Resolution { + width, + height, + ..Default::default() + }; + if !v.contains(&resolution) { + v.push(resolution); + } + } + } + } + } + } + } + v +} + +pub fn current_resolution(name: &str) -> ResultType { + let mut parser = Parser::new(); + parser.parse().map_err(|e| anyhow!(e))?; + let connector = parser.get_connector(name).map_err(|e| anyhow!(e))?; + let r = connector.current_resolution(); + let width = r.horizontal.parse::()?; + let height = r.vertical.parse::()?; + Ok(Resolution { + width, + height, + ..Default::default() + }) +} + +pub fn change_resolution(name: &str, width: usize, height: usize) -> ResultType<()> { + std::process::Command::new("xrandr") + .args(vec![ + "--output", + name, + "--mode", + &format!("{}x{}", width, height), + ]) + .spawn()?; + Ok(()) +} From 5b8e51d6b981e1b0cad80edcb56ab1cc5d6a8fb3 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sun, 12 Feb 2023 16:09:41 +0800 Subject: [PATCH 2004/2015] mac resolution Signed-off-by: 21pages --- src/platform/macos.mm | 111 ++++++++++++++++++++++++++++++++++++++++++ src/platform/macos.rs | 73 ++++++++++++++++++++++++++- 2 files changed, 183 insertions(+), 1 deletion(-) diff --git a/src/platform/macos.mm b/src/platform/macos.mm index 789404cb6..443351469 100644 --- a/src/platform/macos.mm +++ b/src/platform/macos.mm @@ -40,3 +40,114 @@ extern "C" float BackingScaleFactor() { if (s) return [s backingScaleFactor]; return 1; } + +// https://github.com/jhford/screenresolution/blob/master/cg_utils.c +// https://github.com/jdoupe/screenres/blob/master/setgetscreen.m + +extern "C" bool MacGetModeNum(CGDirectDisplayID display, uint32_t *numModes) { + CFArrayRef allModes = CGDisplayCopyAllDisplayModes(display, NULL); + if (allModes == NULL) { + return false; + } + *numModes = CFArrayGetCount(allModes); + CFRelease(allModes); + return true; +} + +extern "C" bool MacGetModes(CGDirectDisplayID display, uint32_t *widths, uint32_t *heights, uint32_t max, uint32_t *numModes) { + CFArrayRef allModes = CGDisplayCopyAllDisplayModes(display, NULL); + if (allModes == NULL) { + return false; + } + *numModes = CFArrayGetCount(allModes); + for (int i = 0; i < *numModes && i < max; i++) { + CGDisplayModeRef mode = (CGDisplayModeRef)CFArrayGetValueAtIndex(allModes, i); + widths[i] = (uint32_t)CGDisplayModeGetWidth(mode); + heights[i] = (uint32_t)CGDisplayModeGetHeight(mode); + } + CFRelease(allModes); + return true; +} + +extern "C" bool MacGetMode(CGDirectDisplayID display, uint32_t *width, uint32_t *height) { + CGDisplayModeRef mode = CGDisplayCopyDisplayMode(display); + if (mode == NULL) { + return false; + } + *width = (uint32_t)CGDisplayModeGetWidth(mode); + *height = (uint32_t)CGDisplayModeGetHeight(mode); + CGDisplayModeRelease(mode); + return true; +} + +size_t bitDepth(CGDisplayModeRef mode) { + size_t depth = 0; + CFStringRef pixelEncoding = CGDisplayModeCopyPixelEncoding(mode); + // my numerical representation for kIO16BitFloatPixels and kIO32bitFloatPixels + // are made up and possibly non-sensical + if (kCFCompareEqualTo == CFStringCompare(pixelEncoding, CFSTR(kIO32BitFloatPixels), kCFCompareCaseInsensitive)) { + depth = 96; + } else if (kCFCompareEqualTo == CFStringCompare(pixelEncoding, CFSTR(kIO64BitDirectPixels), kCFCompareCaseInsensitive)) { + depth = 64; + } else if (kCFCompareEqualTo == CFStringCompare(pixelEncoding, CFSTR(kIO16BitFloatPixels), kCFCompareCaseInsensitive)) { + depth = 48; + } else if (kCFCompareEqualTo == CFStringCompare(pixelEncoding, CFSTR(IO32BitDirectPixels), kCFCompareCaseInsensitive)) { + depth = 32; + } else if (kCFCompareEqualTo == CFStringCompare(pixelEncoding, CFSTR(kIO30BitDirectPixels), kCFCompareCaseInsensitive)) { + depth = 30; + } else if (kCFCompareEqualTo == CFStringCompare(pixelEncoding, CFSTR(IO16BitDirectPixels), kCFCompareCaseInsensitive)) { + depth = 16; + } else if (kCFCompareEqualTo == CFStringCompare(pixelEncoding, CFSTR(IO8BitIndexedPixels), kCFCompareCaseInsensitive)) { + depth = 8; + } + CFRelease(pixelEncoding); + return depth; +} + +bool setDisplayToMode(CGDirectDisplayID display, CGDisplayModeRef mode) { + CGError rc; + CGDisplayConfigRef config; + rc = CGBeginDisplayConfiguration(&config); + if (rc != kCGErrorSuccess) { + return false; + } + rc = CGConfigureDisplayWithDisplayMode(config, display, mode, NULL); + if (rc != kCGErrorSuccess) { + return false; + } + rc = CGCompleteDisplayConfiguration(config, kCGConfigureForSession); + if (rc != kCGErrorSuccess) { + return false; + } + return true; +} + + +extern "C" bool MacSetMode(CGDirectDisplayID display, uint32_t width, uint32_t height) +{ + bool ret = false; + CGDisplayModeRef currentMode = CGDisplayCopyDisplayMode(display); + if (currentMode == NULL) { + return ret; + } + CFArrayRef allModes = CGDisplayCopyAllDisplayModes(display, NULL); + if (allModes == NULL) { + CGDisplayModeRelease(currentMode); + return ret; + } + int numModes = CFArrayGetCount(allModes); + CGDisplayModeRef bestMode = NULL; + for (int i = 0; i < numModes; i++) { + CGDisplayModeRef mode = (CGDisplayModeRef)CFArrayGetValueAtIndex(allModes, i); + if (width == CGDisplayModeGetWidth(mode) && + height == CGDisplayModeGetHeight(mode) && + bitDepth(currentMode) == bitDepth(mode) && + CGDisplayModeGetRefreshRate(currentMode) == CGDisplayModeGetRefreshRate(mode)) { + ret = setDisplayToMode(display, mode); + break; + } + } + CGDisplayModeRelease(currentMode); + CFRelease(allModes); + return ret; +} \ No newline at end of file diff --git a/src/platform/macos.rs b/src/platform/macos.rs index 3e19cca28..025274840 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::{allow_err, bail, log}; +use hbb_common::{allow_err, anyhow::anyhow, bail, log, message_proto::Resolution}; use include_dir::{include_dir, Dir}; use objc::{class, msg_send, sel, sel_impl}; use scrap::{libc::c_void, quartz::ffi::*}; @@ -34,6 +34,16 @@ extern "C" { static kAXTrustedCheckOptionPrompt: CFStringRef; fn AXIsProcessTrustedWithOptions(options: CFDictionaryRef) -> BOOL; fn InputMonitoringAuthStatus(_: BOOL) -> BOOL; + fn MacGetModeNum(display: u32, numModes: *mut u32) -> BOOL; + fn MacGetModes( + display: u32, + widths: *mut u32, + heights: *mut u32, + max: u32, + numModes: *mut u32, + ) -> BOOL; + fn MacGetMode(display: u32, width: *mut u32, height: *mut u32) -> BOOL; + fn MacSetMode(display: u32, width: u32, height: u32) -> BOOL; } pub fn is_process_trusted(prompt: bool) -> bool { @@ -594,3 +604,64 @@ pub fn handle_application_should_open_untitled_file() { } } } + +pub fn resolutions(name: &str) -> Vec { + let mut v = vec![]; + if let Ok(display) = name.parse::() { + let mut num = 0; + unsafe { + if YES == MacGetModeNum(display, &mut num) { + let (mut widths, mut heights) = (vec![0; num as _], vec![0; num as _]); + let mut realNum = 0; + if YES + == MacGetModes( + display, + widths.as_mut_ptr(), + heights.as_mut_ptr(), + num, + &mut realNum, + ) + { + if realNum <= num { + for i in 0..realNum { + let resolution = Resolution { + width: widths[i as usize] as _, + height: heights[i as usize] as _, + ..Default::default() + }; + if !v.contains(&resolution) { + v.push(resolution); + } + } + } + } + } + } + } + v +} + +pub fn current_resolution(name: &str) -> ResultType { + let display = name.parse::().map_err(|e| anyhow!(e))?; + unsafe { + let (mut width, mut height) = (0, 0); + if NO == MacGetMode(display, &mut width, &mut height) { + bail!("MacGetMode failed"); + } + Ok(Resolution { + width: width as _, + height: height as _, + ..Default::default() + }) + } +} + +pub fn change_resolution(name: &str, width: usize, height: usize) -> ResultType<()> { + let display = name.parse::().map_err(|e| anyhow!(e))?; + unsafe { + if NO == MacSetMode(display, width as _, height as _) { + bail!("MacSetMode failed"); + } + } + Ok(()) +} From 4338451f6f7f64a9f84c38d690c11b1b78b44e7e Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 23 Feb 2023 14:30:29 +0800 Subject: [PATCH 2005/2015] refactor remote menubar with MenuBar for submenu Signed-off-by: 21pages --- flutter/lib/common.dart | 16 + .../desktop/pages/desktop_setting_page.dart | 34 +- .../lib/desktop/widgets/remote_menubar.dart | 2791 +++++++++-------- 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/nl.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 + 36 files changed, 1582 insertions(+), 1325 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 6d3e4c3b7..ddd9ea1ac 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1814,3 +1814,19 @@ class DraggableNeverScrollableScrollPhysics extends ScrollPhysics { @override bool get allowImplicitScrolling => false; } + +Widget futureBuilder( + {required Future? future, required Widget Function(dynamic data) hasData}) { + return FutureBuilder( + future: future, + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.hasData) { + return hasData(snapshot.data!); + } else { + if (snapshot.hasError) { + debugPrint(snapshot.error.toString()); + } + return Container(); + } + }); +} diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 971c713ce..ffe707cf0 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -319,7 +319,7 @@ class _GeneralState extends State<_General> { bind.mainSetOption(key: 'audio-input', value: device); } - return _futureBuilder(future: () async { + return futureBuilder(future: () async { List devices = (await bind.mainGetSoundInputs()).toList(); if (Platform.isWindows) { devices.insert(0, 'System Sound'); @@ -346,7 +346,7 @@ class _GeneralState extends State<_General> { } Widget record(BuildContext context) { - return _futureBuilder(future: () async { + return futureBuilder(future: () async { String customDirectory = await bind.mainGetOption(key: 'video-save-directory'); String defaultDirectory = await bind.mainDefaultVideoSaveDirectory(); @@ -399,7 +399,7 @@ class _GeneralState extends State<_General> { } Widget language() { - return _futureBuilder(future: () async { + return futureBuilder(future: () async { String langs = await bind.mainGetLangs(); String lang = bind.mainGetLocalOption(key: kCommConfKeyLang); return {'langs': langs, 'lang': lang}; @@ -487,7 +487,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { Widget _permissions(context, bool stopService) { bool enabled = !locked; - return _futureBuilder(future: () async { + return futureBuilder(future: () async { return await bind.mainGetOption(key: 'access-mode'); }(), hasData: (data) { String accessMode = data! as String; @@ -744,7 +744,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { return [ _OptionCheckBox(context, 'Enable Direct IP Access', 'direct-server', update: update, enabled: !locked), - _futureBuilder( + futureBuilder( future: () async { String enabled = await bind.mainGetOption(key: 'direct-server'); String port = await bind.mainGetOption(key: 'direct-access-port'); @@ -805,7 +805,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { Widget whitelist() { bool enabled = !locked; - return _futureBuilder(future: () async { + return futureBuilder(future: () async { return await bind.mainGetOption(key: 'whitelist'); }(), hasData: (data) { RxBool hasWhitelist = (data as String).isNotEmpty.obs; @@ -931,7 +931,7 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin { } server(bool enabled) { - return _futureBuilder(future: () async { + return futureBuilder(future: () async { return await bind.mainGetOptions(); }(), hasData: (data) { // Setting page is not modal, oldOptions should only be used when getting options, never when setting. @@ -1366,7 +1366,7 @@ class _About extends StatefulWidget { class _AboutState extends State<_About> { @override Widget build(BuildContext context) { - return _futureBuilder(future: () async { + return futureBuilder(future: () async { final license = await bind.mainGetLicense(); final version = await bind.mainGetVersion(); final buildDate = await bind.mainGetBuildDate(); @@ -1500,7 +1500,7 @@ Widget _OptionCheckBox(BuildContext context, String label, String key, bool enabled = true, Icon? checkedIcon, bool? fakeValue}) { - return _futureBuilder( + return futureBuilder( future: bind.mainGetOption(key: key), hasData: (data) { bool value = option2bool(key, data.toString()); @@ -1633,22 +1633,6 @@ Widget _SubLabeledWidget(BuildContext context, String label, Widget child, ).marginOnly(left: _kContentHSubMargin); } -Widget _futureBuilder( - {required Future? future, required Widget Function(dynamic data) hasData}) { - return FutureBuilder( - future: future, - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.hasData) { - return hasData(snapshot.data!); - } else { - if (snapshot.hasError) { - debugPrint(snapshot.error.toString()); - } - return Container(); - } - }); -} - Widget _lock( bool locked, String label, diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 45857aa45..993d02683 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -1,11 +1,9 @@ import 'dart:convert'; import 'dart:io'; -import 'dart:math' as math; 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'; @@ -23,7 +21,6 @@ import '../../models/model.dart'; import '../../models/platform_model.dart'; import '../../common/shared_state.dart'; import './popup_menu.dart'; -import './material_mod_popup_menu.dart' as mod_menu; import './kb_layout_type_chooser.dart'; class MenubarState { @@ -102,6 +99,11 @@ class _MenubarTheme { // kMinInteractiveDimension static const double height = 20.0; static const double dividerHeight = 12.0; + + static const double buttonSize = 32; + static const double buttonHMargin = 3; + static const double buttonVMargin = 6; + static const double iconRadius = 8; } typedef DismissFunc = void Function(); @@ -280,7 +282,7 @@ class RemoteMenubar extends StatefulWidget { final Function(Function(bool)) onEnterOrLeaveImageSetter; final Function() onEnterOrLeaveImageCleaner; - const RemoteMenubar({ + RemoteMenubar({ Key? key, required this.id, required this.ffi, @@ -296,7 +298,6 @@ class RemoteMenubar extends StatefulWidget { class _RemoteMenubarState extends State { late Debouncer _debouncerHide; bool _isCursorOverImage = false; - window_size.Screen? _screen; final _fractionX = 0.5.obs; final _dragging = false.obs; @@ -347,7 +348,6 @@ class _RemoteMenubarState extends State { @override Widget build(BuildContext context) { // No need to use future builder here. - _updateScreen(); return Align( alignment: Alignment.topCenter, child: Obx(() => show.value @@ -375,6 +375,577 @@ class _RemoteMenubarState extends State { }); } + Widget _buildMenubar(BuildContext context) { + final List menubarItems = []; + if (!isWebDesktop) { + menubarItems.add(_PinMenu(state: widget.state)); + menubarItems.add( + _FullscreenMenu(state: widget.state, setFullscreen: _setFullscreen)); + menubarItems.add(_MobileActionMenu(ffi: widget.ffi)); + } + menubarItems.add(_MonitorMenu(id: widget.id, ffi: widget.ffi)); + menubarItems + .add(_ControlMenu(id: widget.id, ffi: widget.ffi, state: widget.state)); + menubarItems.add(_DisplayMenu( + id: widget.id, + ffi: widget.ffi, + state: widget.state, + setFullscreen: _setFullscreen, + )); + menubarItems.add(_KeyboardMenu(id: widget.id, ffi: widget.ffi)); + if (!isWeb) { + menubarItems.add(_ChatMenu(id: widget.id, ffi: widget.ffi)); + menubarItems.add(_VoiceCallMenu(id: widget.id, ffi: widget.ffi)); + } + menubarItems.add(_RecordMenu()); + menubarItems.add(_CloseMenu(id: widget.id, ffi: widget.ffi)); + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(10)), + ), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: MenuBar( + children: [ + SizedBox(width: _MenubarTheme.buttonHMargin), + ...menubarItems, + SizedBox(width: _MenubarTheme.buttonHMargin) + ], + )), + ), + _buildDraggableShowHide(context), + ], + ); + } +} + +class _PinMenu extends StatelessWidget { + final MenubarState state; + const _PinMenu({Key? key, required this.state}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Obx( + () => _IconMenuButton( + assetName: state.pin ? "assets/pinned.svg" : "assets/unpinned.svg", + tooltip: state.pin ? 'Unpin menubar' : 'Pin menubar', + onPressed: state.switchPin, + color: state.pin ? _MenubarTheme.blueColor : Colors.grey[800]!, + hoverColor: + state.pin ? _MenubarTheme.hoverBlueColor : Colors.grey[850]!, + ), + ); + } +} + +class _FullscreenMenu extends StatelessWidget { + final MenubarState state; + final Function(bool) setFullscreen; + bool get isFullscreen => stateGlobal.fullscreen; + const _FullscreenMenu( + {Key? key, required this.state, required this.setFullscreen}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return _IconMenuButton( + assetName: + isFullscreen ? "assets/fullscreen_exit.svg" : "assets/fullscreen.svg", + tooltip: isFullscreen ? 'Exit Fullscreen' : 'Fullscreen', + onPressed: () => setFullscreen(!isFullscreen), + color: _MenubarTheme.blueColor, + hoverColor: _MenubarTheme.hoverBlueColor, + ); + } +} + +class _MobileActionMenu extends StatelessWidget { + final FFI ffi; + const _MobileActionMenu({Key? key, required this.ffi}) : super(key: key); + + @override + Widget build(BuildContext context) { + if (!ffi.ffiModel.isPeerAndroid) return Offstage(); + return _IconMenuButton( + assetName: 'assets/actions_mobile.svg', + tooltip: 'Mobile Actions', + onPressed: () => ffi.dialogManager.toggleMobileActionsOverlay(ffi: ffi), + color: _MenubarTheme.blueColor, + hoverColor: _MenubarTheme.hoverBlueColor, + ); + } +} + +class _MonitorMenu extends StatelessWidget { + final String id; + final FFI ffi; + const _MonitorMenu({Key? key, required this.id, required this.ffi}) + : super(key: key); + + @override + Widget build(BuildContext context) { + if (stateGlobal.displaysCount.value < 2) return Offstage(); + return _IconSubmenuButton( + icon: icon(), + ffi: ffi, + color: _MenubarTheme.blueColor, + hoverColor: _MenubarTheme.hoverBlueColor, + menuStyle: MenuStyle( + padding: + MaterialStatePropertyAll(EdgeInsets.symmetric(horizontal: 6))), + menuChildren: [Row(children: displays(context))]); + } + + icon() { + final pi = ffi.ffiModel.pi; + return Stack( + alignment: Alignment.center, + children: [ + SvgPicture.asset( + "assets/display.svg", + color: Colors.white, + ), + Padding( + padding: const EdgeInsets.only(bottom: 3.9), + child: Obx(() { + RxInt display = CurrentDisplayState.find(id); + return Text( + '${display.value + 1}/${pi.displays.length}', + style: const TextStyle(color: Colors.white, fontSize: 8), + ); + }), + ) + ], + ); + } + + List displays(BuildContext context) { + final List rowChildren = []; + final pi = ffi.ffiModel.pi; + for (int i = 0; i < pi.displays.length; i++) { + rowChildren.add(_IconMenuButton( + color: _MenubarTheme.blueColor, + hoverColor: _MenubarTheme.hoverBlueColor, + tooltip: "", + hMargin: 6, + vMargin: 12, + icon: 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: 3.5 /*2.5*/), + child: Text( + (i + 1).toString(), + style: TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ) + ], + ), + ), + onPressed: () { + _menuDismissCallback(ffi); + RxInt display = CurrentDisplayState.find(id); + if (display.value != i) { + bind.sessionSwitchDisplay(id: id, value: i); + } + }, + )); + } + return rowChildren; + } +} + +class _ControlMenu extends StatelessWidget { + final String id; + final FFI ffi; + final MenubarState state; + _ControlMenu( + {Key? key, required this.id, required this.ffi, required this.state}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return _IconSubmenuButton( + svg: "assets/actions.svg", + color: _MenubarTheme.blueColor, + hoverColor: _MenubarTheme.hoverBlueColor, + ffi: ffi, + menuChildren: [ + osPassword(), + transferFile(context), + tcpTunneling(context), + note(), + Divider(), + ctrlAltDel(), + restart(), + blockUserInput(), + switchSides(), + refresh(), + ]); + } + + osPassword() { + return _MenuItemButton( + child: Text(translate('OS Password')), + trailingIcon: Transform.scale(scale: 0.8, child: Icon(Icons.edit)), + ffi: ffi, + onPressed: () => _showSetOSPassword(id, false, ffi.dialogManager)); + } + + _showSetOSPassword( + String id, bool login, OverlayDialogManager dialogManager) async { + final controller = TextEditingController(); + var password = + await bind.sessionGetOption(id: id, arg: 'os-password') ?? ''; + var autoLogin = + await bind.sessionGetOption(id: id, arg: 'auto-login') != ''; + controller.text = password; + dialogManager.show((setState, close) { + submit() { + var text = controller.text.trim(); + bind.sessionPeerOption(id: id, name: 'os-password', value: text); + bind.sessionPeerOption( + id: id, name: 'auto-login', value: autoLogin ? 'Y' : ''); + if (text != '' && login) { + bind.sessionInputOsPassword(id: id, value: text); + } + close(); + } + + return CustomAlertDialog( + title: Text(translate('OS Password')), + content: Column(mainAxisSize: MainAxisSize.min, children: [ + PasswordWidget(controller: controller), + CheckboxListTile( + contentPadding: const EdgeInsets.all(0), + dense: true, + controlAffinity: ListTileControlAffinity.leading, + title: Text( + translate('Auto Login'), + ), + value: autoLogin, + onChanged: (v) { + if (v == null) return; + setState(() => autoLogin = v); + }, + ), + ]), + actions: [ + dialogButton('Cancel', onPressed: close, isOutline: true), + dialogButton('OK', onPressed: submit), + ], + onSubmit: submit, + onCancel: close, + ); + }); + } + + transferFile(BuildContext context) { + return _MenuItemButton( + child: Text(translate('Transfer File')), + ffi: ffi, + onPressed: () => connect(context, id, isFileTransfer: true)); + } + + tcpTunneling(BuildContext context) { + return _MenuItemButton( + child: Text(translate('TCP Tunneling')), + ffi: ffi, + onPressed: () => connect(context, id, isTcpTunneling: true)); + } + + note() { + final auditServer = bind.sessionGetAuditServerSync(id: id, typ: "conn"); + final visible = auditServer.isNotEmpty; + if (!visible) return Offstage(); + return _MenuItemButton( + child: Text(translate('Note')), + ffi: ffi, + onPressed: () => _showAuditDialog(id, ffi.dialogManager), + ); + } + + _showAuditDialog(String id, dialogManager) async { + final controller = TextEditingController(); + dialogManager.show((setState, close) { + submit() { + var text = controller.text.trim(); + if (text != '') { + bind.sessionSendNote(id: id, note: text); + } + close(); + } + + late final focusNode = FocusNode( + onKey: (FocusNode node, RawKeyEvent evt) { + if (evt.logicalKey.keyLabel == 'Enter') { + if (evt is RawKeyDownEvent) { + int pos = controller.selection.base.offset; + controller.text = + '${controller.text.substring(0, pos)}\n${controller.text.substring(pos)}'; + controller.selection = + TextSelection.fromPosition(TextPosition(offset: pos + 1)); + } + return KeyEventResult.handled; + } + if (evt.logicalKey.keyLabel == 'Esc') { + if (evt is RawKeyDownEvent) { + close(); + } + return KeyEventResult.handled; + } else { + return KeyEventResult.ignored; + } + }, + ); + + return CustomAlertDialog( + title: Text(translate('Note')), + content: SizedBox( + width: 250, + height: 120, + child: TextField( + autofocus: true, + keyboardType: TextInputType.multiline, + textInputAction: TextInputAction.newline, + decoration: const InputDecoration.collapsed( + hintText: 'input note here', + ), + maxLines: null, + maxLength: 256, + controller: controller, + focusNode: focusNode, + )), + actions: [ + dialogButton('Cancel', onPressed: close, isOutline: true), + dialogButton('OK', onPressed: submit) + ], + onSubmit: submit, + onCancel: close, + ); + }); + } + + ctrlAltDel() { + final perms = ffi.ffiModel.permissions; + final pi = ffi.ffiModel.pi; + final visible = perms['keyboard'] != false && + (pi.platform == kPeerPlatformLinux || pi.sasEnabled); + if (!visible) return Offstage(); + return _MenuItemButton( + child: Text('${translate("Insert")} Ctrl + Alt + Del'), + ffi: ffi, + onPressed: () => bind.sessionCtrlAltDel(id: id)); + } + + restart() { + final perms = ffi.ffiModel.permissions; + final pi = ffi.ffiModel.pi; + final visible = perms['restart'] != false && + (pi.platform == kPeerPlatformLinux || + pi.platform == kPeerPlatformWindows || + pi.platform == kPeerPlatformMacOS); + if (!visible) return Offstage(); + return _MenuItemButton( + child: Text(translate('Restart Remote Device')), + ffi: ffi, + onPressed: () => showRestartRemoteDevice(pi, id, ffi.dialogManager)); + } + + blockUserInput() { + final perms = ffi.ffiModel.permissions; + final pi = ffi.ffiModel.pi; + final visible = + perms['keyboard'] != false && pi.platform == kPeerPlatformWindows; + if (!visible) return Offstage(); + return _MenuItemButton( + child: Obx(() => Text(translate( + '${BlockInputState.find(id).value ? 'Unb' : 'B'}lock user input'))), + ffi: ffi, + onPressed: () { + RxBool blockInput = BlockInputState.find(id); + bind.sessionToggleOption( + id: id, value: '${blockInput.value ? 'un' : ''}block-input'); + blockInput.value = !blockInput.value; + }); + } + + switchSides() { + final perms = ffi.ffiModel.permissions; + final pi = ffi.ffiModel.pi; + final visible = perms['keyboard'] != false && + pi.platform != kPeerPlatformAndroid && + pi.platform != kPeerPlatformMacOS && + version_cmp(pi.version, '1.2.0') >= 0; + if (!visible) return Offstage(); + return _MenuItemButton( + child: Text(translate('Switch Sides')), + ffi: ffi, + onPressed: () => _showConfirmSwitchSidesDialog(id, ffi.dialogManager)); + } + + void _showConfirmSwitchSidesDialog( + String id, OverlayDialogManager dialogManager) async { + dialogManager.show((setState, close) { + submit() async { + await bind.sessionSwitchSides(id: id); + closeConnection(id: id); + } + + return CustomAlertDialog( + content: msgboxContent('info', 'Switch Sides', + 'Please confirm if you want to share your desktop?'), + actions: [ + dialogButton('Cancel', onPressed: close, isOutline: true), + dialogButton('OK', onPressed: submit), + ], + onSubmit: submit, + onCancel: close, + ); + }); + } + + refresh() { + final pi = ffi.ffiModel.pi; + final visible = pi.version.isNotEmpty; + if (!visible) return Offstage(); + return _MenuItemButton( + child: Text(translate('Refresh')), + ffi: ffi, + onPressed: () => bind.sessionRefresh(id: id)); + } +} + +class _DisplayMenu extends StatefulWidget { + final String id; + final FFI ffi; + final MenubarState state; + final Function(bool) setFullscreen; + _DisplayMenu( + {Key? key, + required this.id, + required this.ffi, + required this.state, + required this.setFullscreen}) + : super(key: key); + + @override + State<_DisplayMenu> createState() => _DisplayMenuState(); +} + +class _DisplayMenuState extends State<_DisplayMenu> { + window_size.Screen? _screen; + + bool get isFullscreen => stateGlobal.fullscreen; + + int get windowId => stateGlobal.windowId; + + Map get perms => widget.ffi.ffiModel.permissions; + + PeerInfo get pi => widget.ffi.ffiModel.pi; + + @override + Widget build(BuildContext context) { + _updateScreen(); + return _IconSubmenuButton( + svg: "assets/display.svg", + ffi: widget.ffi, + color: _MenubarTheme.blueColor, + hoverColor: _MenubarTheme.hoverBlueColor, + menuChildren: [ + adjustWindow(), + viewStyle(), + scrollStyle(), + imageQuality(), + codec(), + resolutions(), + Divider(), + showRemoteCursor(), + zoomCursor(), + showQualityMonitor(), + mute(), + fileCopyAndPaste(), + disableClipboard(), + lockAfterSessionEnd(), + privacyMode(), + ]); + } + + adjustWindow() { + final visible = _isWindowCanBeAdjusted(); + if (!visible) return Offstage(); + return Column( + children: [ + _MenuItemButton( + child: Text(translate('Adjust Window')), + onPressed: _doAdjustWindow, + ffi: widget.ffi), + Divider(), + ], + ); + } + + _doAdjustWindow() async { + await _updateScreen(); + if (_screen != null) { + widget.setFullscreen(false); + double scale = _screen!.scaleFactor; + final wndRect = await WindowController.fromWindowId(windowId).getFrame(); + final mediaSize = MediaQueryData.fromWindow(ui.window).size; + // On windows, wndRect is equal to GetWindowRect and mediaSize is equal to GetClientRect. + // https://stackoverflow.com/a/7561083 + double magicWidth = + wndRect.right - wndRect.left - mediaSize.width * scale; + double magicHeight = + wndRect.bottom - wndRect.top - mediaSize.height * scale; + + final canvasModel = widget.ffi.canvasModel; + final width = (canvasModel.getDisplayWidth() * canvasModel.scale + + canvasModel.windowBorderWidth * 2) * + scale + + magicWidth; + final height = (canvasModel.getDisplayHeight() * canvasModel.scale + + canvasModel.tabBarHeight + + canvasModel.windowBorderWidth * 2) * + scale + + magicHeight; + double left = wndRect.left + (wndRect.width - width) / 2; + double top = wndRect.top + (wndRect.height - height) / 2; + + Rect frameRect = _screen!.frame; + if (!isFullscreen) { + frameRect = _screen!.visibleFrame; + } + if (left < frameRect.left) { + left = frameRect.left; + } + if (top < frameRect.top) { + top = frameRect.top; + } + if ((left + width) > frameRect.right) { + left = frameRect.right - width; + } + if ((top + height) > frameRect.bottom) { + top = frameRect.bottom - height; + } + await WindowController.fromWindowId(windowId) + .setFrame(Rect.fromLTWH(left, top, width, height)); + } + } + _updateScreen() async { final v = await rustDeskWinManager.call( WindowType.Main, kWindowGetWindowInfo, ''); @@ -395,638 +966,11 @@ class _RemoteMenubarState extends State { } } - Widget _buildPointerTrackWidget(Widget child) { - return Listener( - onPointerHover: (PointerHoverEvent e) => - widget.ffi.inputModel.lastMousePos = e.position, - child: MouseRegion( - child: child, - ), - ); - } - - _menuDismissCallback() => widget.ffi.inputModel.refreshMousePos(); - - Widget _buildMenubar(BuildContext context) { - final List menubarItems = []; - if (!isWebDesktop) { - menubarItems.add(_buildPinMenubar(context)); - menubarItems.add(_buildFullscreen(context)); - if (widget.ffi.ffiModel.isPeerAndroid) { - menubarItems.add(MenuButton( - tooltip: translate('Mobile Actions'), - child: SvgPicture.asset( - "assets/actions_mobile.svg", - color: Colors.white, - ), - onPressed: () { - widget.ffi.dialogManager - .toggleMobileActionsOverlay(ffi: widget.ffi); - }, - color: _MenubarTheme.blueColor, - hoverColor: _MenubarTheme.hoverBlueColor, - )); - } + _isWindowCanBeAdjusted() { + if (widget.state.viewStyle.value != kRemoteViewStyleOriginal) { + return false; } - menubarItems.add(_buildMonitor(context)); - menubarItems.add(_buildControl(context)); - menubarItems.add(_buildDisplay(context)); - menubarItems.add(_buildKeyboard(context)); - if (!isWeb) { - menubarItems.add(_buildChat(context)); - menubarItems.add(_buildVoiceCall(context)); - } - menubarItems.add(_buildRecording(context)); - menubarItems.add(_buildClose(context)); - return PopupMenuTheme( - data: const PopupMenuThemeData( - textStyle: TextStyle(color: _MenubarTheme.blueColor)), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.vertical( - bottom: Radius.circular(10), - ), - ), - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox(width: 3), - ...menubarItems, - SizedBox(width: 3) - ], - ), - ), - ), - _buildDraggableShowHide(context), - ], - ), - ); - } - - Widget _buildPinMenubar(BuildContext context) { - return Obx( - () => MenuButton( - tooltip: translate(pin ? 'Unpin menubar' : 'Pin menubar'), - onPressed: () { - widget.state.switchPin(); - }, - child: SvgPicture.asset( - pin ? "assets/pinned.svg" : "assets/unpinned.svg", - color: Colors.white, - ), - color: pin ? _MenubarTheme.blueColor : Colors.grey[800]!, - hoverColor: pin ? _MenubarTheme.hoverBlueColor : Colors.grey[850]!, - ), - ); - } - - Widget _buildFullscreen(BuildContext context) { - return MenuButton( - tooltip: translate(isFullscreen ? 'Exit Fullscreen' : 'Fullscreen'), - onPressed: () { - _setFullscreen(!isFullscreen); - }, - child: SvgPicture.asset( - isFullscreen ? "assets/fullscreen_exit.svg" : "assets/fullscreen.svg", - color: Colors.white, - ), - color: _MenubarTheme.blueColor, - hoverColor: _MenubarTheme.hoverBlueColor, - ); - } - - Widget _buildMonitor(BuildContext context) { - final pi = widget.ffi.ffiModel.pi; - final monitor = mod_menu.PopupMenuButton( - tooltip: translate('Select Monitor'), - position: mod_menu.PopupMenuPosition.under, - icon: Stack( - alignment: Alignment.center, - children: [ - SvgPicture.asset( - "assets/display.svg", - color: Colors.white, - ), - Padding( - padding: const EdgeInsets.only(bottom: 3.9), - child: Obx(() { - RxInt display = CurrentDisplayState.find(widget.id); - return Text( - '${display.value + 1}/${pi.displays.length}', - style: const TextStyle(color: Colors.white, fontSize: 8), - ); - }), - ) - ], - ), - itemBuilder: (BuildContext context) { - final List rowChildren = []; - for (int i = 0; i < pi.displays.length; i++) { - 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); - } - }, - )); - } - return >[ - mod_menu.PopupMenuItem( - height: _MenubarTheme.height, - padding: EdgeInsets.zero, - child: _buildPointerTrackWidget( - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: rowChildren, - ), - ), - ) - ]; - }, - ); - - return Obx(() => Offstage( - offstage: stateGlobal.displaysCount.value < 2, - child: monitor, - )); - } - - Widget _buildControl(BuildContext context) { - return mod_menu.PopupMenuButton( - padding: EdgeInsets.zero, - icon: SvgPicture.asset( - "assets/actions.svg", - color: Colors.white, - ), - tooltip: translate('Control Actions'), - position: mod_menu.PopupMenuPosition.under, - itemBuilder: (BuildContext context) => _getControlMenu(context) - .map((entry) => entry.build( - context, - const MenuConfig( - commonColor: _MenubarTheme.blueColor, - height: _MenubarTheme.height, - dividerHeight: _MenubarTheme.dividerHeight, - ))) - .expand((i) => i) - .toList(), - ); - } - - Widget _buildDisplay(BuildContext context) { - return FutureBuilder(future: () async { - widget.state.viewStyle.value = - await bind.sessionGetViewStyle(id: widget.id) ?? ''; - final supportedHwcodec = - await bind.sessionSupportedHwcodec(id: widget.id); - return {'supportedHwcodec': supportedHwcodec}; - }(), builder: (context, snapshot) { - if (snapshot.hasData) { - return Obx(() { - final remoteCount = RemoteCountState.find().value; - return mod_menu.PopupMenuButton( - padding: EdgeInsets.zero, - icon: SvgPicture.asset( - "assets/display.svg", - color: Colors.white, - ), - tooltip: translate('Display Settings'), - position: mod_menu.PopupMenuPosition.under, - menuWrapper: _buildPointerTrackWidget, - itemBuilder: (BuildContext context) => - _getDisplayMenu(snapshot.data!, remoteCount) - .map((entry) => entry.build( - context, - const MenuConfig( - commonColor: _MenubarTheme.blueColor, - height: _MenubarTheme.height, - dividerHeight: _MenubarTheme.dividerHeight, - ))) - .expand((i) => i) - .toList(), - ); - }); - } else { - return const Offstage(); - } - }); - } - - Widget _buildKeyboard(BuildContext context) { - // Do not support peer 1.1.9. - if (stateGlobal.grabKeyboard) { - bind.sessionSetKeyboardMode(id: widget.id, value: 'map'); - return Offstage(); - } - - FfiModel ffiModel = Provider.of(context); - if (ffiModel.permissions['keyboard'] == false) { - return Offstage(); - } - return mod_menu.PopupMenuButton( - padding: EdgeInsets.zero, - icon: SvgPicture.asset( - "assets/keyboard.svg", - color: Colors.white, - ), - tooltip: translate('Keyboard Settings'), - position: mod_menu.PopupMenuPosition.under, - itemBuilder: (BuildContext context) => _getKeyboardMenu() - .map((entry) => entry.build( - context, - const MenuConfig( - commonColor: _MenubarTheme.blueColor, - height: _MenubarTheme.height, - dividerHeight: _MenubarTheme.dividerHeight, - ))) - .expand((i) => i) - .toList(), - ); - } - - Widget _buildRecording(BuildContext context) { - return Consumer(builder: ((context, value, child) { - if (value.permissions['recording'] != false) { - return Consumer( - builder: (context, value, child) => MenuButton( - tooltip: value.start - ? translate('Stop session recording') - : translate('Start session recording'), - onPressed: () => value.toggle(), - child: SvgPicture.asset( - "assets/rec.svg", - color: Colors.white, - ), - color: - value.start ? _MenubarTheme.redColor : _MenubarTheme.blueColor, - hoverColor: value.start - ? _MenubarTheme.hoverRedColor - : _MenubarTheme.hoverBlueColor, - ), - ); - } else { - return Offstage(); - } - })); - } - - Widget _buildClose(BuildContext context) { - return MenuButton( - tooltip: translate('Close'), - onPressed: () { - clientClose(widget.id, widget.ffi.dialogManager); - }, - child: SvgPicture.asset( - "assets/close.svg", - color: Colors.white, - ), - color: _MenubarTheme.redColor, - hoverColor: _MenubarTheme.hoverRedColor, - ); - } - - final _chatButtonKey = GlobalKey(); - Widget _buildChat(BuildContext context) { - FfiModel ffiModel = Provider.of(context); - return mod_menu.PopupMenuButton( - key: _chatButtonKey, - padding: EdgeInsets.zero, - icon: SvgPicture.asset( - "assets/chat.svg", - color: Colors.white, - ), - tooltip: translate('Chat'), - position: mod_menu.PopupMenuPosition.under, - itemBuilder: (BuildContext context) => _getChatMenu(context) - .map((entry) => entry.build( - context, - const MenuConfig( - commonColor: _MenubarTheme.blueColor, - height: _MenubarTheme.height, - dividerHeight: _MenubarTheme.dividerHeight, - ))) - .expand((i) => i) - .toList(), - ); - } - - Widget _getVoiceCallIcon() { - switch (widget.ffi.chatModel.voiceCallStatus.value) { - case VoiceCallStatus.waitingForResponse: - return SvgPicture.asset( - "assets/call_wait.svg", - color: Colors.white, - ); - - case VoiceCallStatus.connected: - return SvgPicture.asset( - "assets/call_end.svg", - color: Colors.white, - ); - 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( - () { - final tooltipText = _getVoiceCallTooltip(); - return tooltipText == null - ? const Offstage() - : MenuButton( - child: _getVoiceCallIcon(), - tooltip: translate(tooltipText), - onPressed: () => bind.sessionCloseVoiceCall(id: widget.id), - color: _MenubarTheme.redColor, - hoverColor: _MenubarTheme.hoverRedColor, - ); - }, - ); - } - - 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: () { - 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(chatInitPos: initPos); - }, - padding: padding, - dismissOnClicked: true, - ), - MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - translate('Voice call'), - style: style, - ), - proc: () { - // Request a voice call. - bind.sessionRequestVoiceCall(id: widget.id); - }, - padding: padding, - dismissOnClicked: true, - ), - ]); - return chatMenu; - } - - List> _getControlMenu(BuildContext context) { - final pi = widget.ffi.ffiModel.pi; - final perms = widget.ffi.ffiModel.permissions; - final peer_version = widget.ffi.ffiModel.pi.version; - const EdgeInsets padding = EdgeInsets.only(left: 14.0, right: 5.0); - final List> displayMenu = []; - displayMenu.addAll([ - MenuEntryButton( - childBuilder: (TextStyle? style) => Container( - alignment: AlignmentDirectional.center, - height: _MenubarTheme.height, - child: Row( - children: [ - Text( - translate('OS Password'), - style: style, - ), - Expanded( - child: Align( - alignment: Alignment.centerRight, - child: Transform.scale( - scale: 0.8, - child: IconButton( - padding: EdgeInsets.zero, - icon: const Icon(Icons.edit), - onPressed: () { - if (Navigator.canPop(context)) { - Navigator.pop(context); - _menuDismissCallback(); - } - showSetOSPassword( - widget.id, false, widget.ffi.dialogManager); - })), - )) - ], - )), - proc: () { - showSetOSPassword(widget.id, false, widget.ffi.dialogManager); - }, - padding: padding, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - ), - MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - translate('Transfer File'), - style: style, - ), - proc: () { - connect(context, widget.id, isFileTransfer: true); - }, - padding: padding, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - ), - MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - translate('TCP Tunneling'), - style: style, - ), - padding: padding, - proc: () { - connect(context, widget.id, isTcpTunneling: true); - }, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - ), - ]); - // {handler.get_audit_server() &&

  • {translate('Note')}
  • } - final auditServer = - bind.sessionGetAuditServerSync(id: widget.id, typ: "conn"); - if (auditServer.isNotEmpty) { - displayMenu.add( - MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - translate('Note'), - style: style, - ), - proc: () { - showAuditDialog(widget.id, widget.ffi.dialogManager); - }, - padding: padding, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - ), - ); - } - displayMenu.add(MenuEntryDivider()); - if (perms['keyboard'] != false) { - if (pi.platform == kPeerPlatformLinux || pi.sasEnabled) { - displayMenu.add(RemoteMenuEntry.insertCtrlAltDel(widget.id, padding, - dismissCallback: _menuDismissCallback)); - } - } - if (perms['restart'] != false && - (pi.platform == kPeerPlatformLinux || - pi.platform == kPeerPlatformWindows || - pi.platform == kPeerPlatformMacOS)) { - displayMenu.add(MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - translate('Restart Remote Device'), - style: style, - ), - proc: () { - showRestartRemoteDevice(pi, widget.id, gFFI.dialogManager); - }, - padding: padding, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - )); - } - - if (perms['keyboard'] != false) { - displayMenu.add(RemoteMenuEntry.insertLock(widget.id, padding, - dismissCallback: _menuDismissCallback)); - - if (pi.platform == kPeerPlatformWindows) { - displayMenu.add(MenuEntryButton( - childBuilder: (TextStyle? style) => Obx(() => Text( - translate( - '${BlockInputState.find(widget.id).value ? 'Unb' : 'B'}lock user input'), - style: style, - )), - proc: () { - RxBool blockInput = BlockInputState.find(widget.id); - bind.sessionToggleOption( - id: widget.id, - value: '${blockInput.value ? 'un' : ''}block-input'); - blockInput.value = !blockInput.value; - }, - padding: padding, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - )); - } - if (pi.platform != kPeerPlatformAndroid && - pi.platform != kPeerPlatformMacOS && // unsupport yet - version_cmp(peer_version, '1.2.0') >= 0) { - displayMenu.add(MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - translate('Switch Sides'), - style: style, - ), - proc: () => - showConfirmSwitchSidesDialog(widget.id, widget.ffi.dialogManager), - padding: padding, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - )); - } - } - - if (pi.version.isNotEmpty) { - displayMenu.add(MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - translate('Refresh'), - style: style, - ), - proc: () { - bind.sessionRefresh(id: widget.id); - }, - padding: padding, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - )); - } - - if (!isWebDesktop) { - // if (perms['keyboard'] != false && perms['clipboard'] != false) { - // displayMenu.add(MenuEntryButton( - // childBuilder: (TextStyle? style) => Text( - // translate('Paste'), - // style: style, - // ), - // proc: () { - // () async { - // ClipboardData? data = - // await Clipboard.getData(Clipboard.kTextPlain); - // if (data != null && data.text != null) { - // bind.sessionInputString(id: widget.id, value: data.text ?? ''); - // } - // }(); - // }, - // padding: padding, - // dismissOnClicked: true, - // dismissCallback: _menuDismissCallback, - // )); - // } - } - return displayMenu; - } - - bool _isWindowCanBeAdjusted(int remoteCount) { + final remoteCount = RemoteCountState.find().value; if (remoteCount != 1) { return false; } @@ -1052,312 +996,277 @@ class _RemoteMenubarState extends State { selfHeight > (requiredHeight * scale); } - List> _getDisplayMenu( - dynamic futureData, int remoteCount) { - const EdgeInsets padding = EdgeInsets.only(left: 18.0, right: 8.0); - final peer_version = widget.ffi.ffiModel.pi.version; - final displayMenu = [ - RemoteMenuEntry.viewStyle( - widget.id, - widget.ffi, - padding, - dismissCallback: _menuDismissCallback, - rxViewStyle: widget.state.viewStyle, - ), - MenuEntryDivider(), - MenuEntryRadios( - text: translate('Image Quality'), - optionsGetter: () => [ - MenuEntryRadioOption( - text: translate('Good image quality'), + viewStyle() { + return futureBuilder(future: () async { + final viewStyle = await bind.sessionGetViewStyle(id: widget.id) ?? ''; + widget.state.viewStyle.value = viewStyle; + return viewStyle; + }(), hasData: (data) { + final groupValue = data as String; + onChanged(String? value) async { + if (value == null) return; + await bind.sessionSetViewStyle(id: widget.id, value: value); + widget.state.viewStyle.value = value; + widget.ffi.canvasModel.updateViewStyle(); + } + + return Column(children: [ + _RadioMenuButton( + child: Text(translate('Scale original')), + value: kRemoteViewStyleOriginal, + groupValue: groupValue, + onChanged: onChanged, + ffi: widget.ffi, + ), + _RadioMenuButton( + child: Text(translate('Scale adaptive')), + value: kRemoteViewStyleAdaptive, + groupValue: groupValue, + onChanged: onChanged, + ffi: widget.ffi, + ), + Divider(), + ]); + }); + } + + scrollStyle() { + final visible = widget.state.viewStyle.value == kRemoteViewStyleOriginal; + if (!visible) return Offstage(); + return futureBuilder(future: () async { + final scrollStyle = await bind.sessionGetScrollStyle(id: widget.id) ?? ''; + return scrollStyle; + }(), hasData: (data) { + final groupValue = data as String; + onChange(String? value) async { + if (value == null) return; + await bind.sessionSetScrollStyle(id: widget.id, value: value); + widget.ffi.canvasModel.updateScrollStyle(); + } + + final enabled = widget.ffi.canvasModel.imageOverflow.value; + return Column(children: [ + _RadioMenuButton( + child: Text(translate('ScrollAuto')), + value: kRemoteScrollStyleAuto, + groupValue: groupValue, + onChanged: enabled ? (value) => onChange(value) : null, + ffi: widget.ffi, + ), + _RadioMenuButton( + child: Text(translate('Scrollbar')), + value: kRemoteScrollStyleBar, + groupValue: groupValue, + onChanged: enabled ? (value) => onChange(value) : null, + ffi: widget.ffi, + ), + Divider(), + ]); + }); + } + + imageQuality() { + return futureBuilder(future: () async { + final imageQuality = + await bind.sessionGetImageQuality(id: widget.id) ?? ''; + return imageQuality; + }(), hasData: (data) { + final groupValue = data as String; + onChanged(String? value) async { + if (value == null) return; + await bind.sessionSetImageQuality(id: widget.id, value: value); + } + + return SubmenuButton( + child: Text(translate('Image Quality')), + menuChildren: [ + _RadioMenuButton( + child: Text(translate('Good image quality')), value: kRemoteImageQualityBest, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, + groupValue: groupValue, + onChanged: onChanged, + ffi: widget.ffi, ), - MenuEntryRadioOption( - text: translate('Balanced'), + _RadioMenuButton( + child: Text(translate('Balanced')), value: kRemoteImageQualityBalanced, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, + groupValue: groupValue, + onChanged: onChanged, + ffi: widget.ffi, ), - MenuEntryRadioOption( - text: translate('Optimize reaction time'), + _RadioMenuButton( + child: Text(translate('Optimize reaction time')), value: kRemoteImageQualityLow, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, + groupValue: groupValue, + onChanged: onChanged, + ffi: widget.ffi, ), - MenuEntryRadioOption( - text: translate('Custom'), + _RadioMenuButton( + child: Text(translate('Custom')), value: kRemoteImageQualityCustom, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - ), - ], - curOptionGetter: () async => - // null means peer id is not found, which there's no need to care about - await bind.sessionGetImageQuality(id: widget.id) ?? '', - optionSetter: (String oldValue, String newValue) async { - if (oldValue != newValue) { - await bind.sessionSetImageQuality(id: widget.id, value: newValue); - } - - double qualityInitValue = 50; - double fpsInitValue = 30; - bool qualitySet = false; - bool fpsSet = false; - setCustomValues({double? quality, double? fps}) async { - if (quality != null) { - qualitySet = true; - await bind.sessionSetCustomImageQuality( - id: widget.id, value: quality.toInt()); - } - if (fps != null) { - fpsSet = true; - await bind.sessionSetCustomFps(id: widget.id, fps: fps.toInt()); - } - if (!qualitySet) { - qualitySet = true; - await bind.sessionSetCustomImageQuality( - id: widget.id, value: qualityInitValue.toInt()); - } - if (!fpsSet) { - fpsSet = true; - await bind.sessionSetCustomFps( - id: widget.id, fps: fpsInitValue.toInt()); - } - } - - if (newValue == kRemoteImageQualityCustom) { - final btnClose = dialogButton('Close', onPressed: () async { - await setCustomValues(); - widget.ffi.dialogManager.dismissAll(); - }); - - // quality - final quality = - await bind.sessionGetCustomImageQuality(id: widget.id); - qualityInitValue = quality != null && quality.isNotEmpty - ? quality[0].toDouble() - : 50.0; - const qualityMinValue = 10.0; - const qualityMaxValue = 100.0; - if (qualityInitValue < qualityMinValue) { - qualityInitValue = qualityMinValue; - } - if (qualityInitValue > qualityMaxValue) { - qualityInitValue = qualityMaxValue; - } - final RxDouble qualitySliderValue = RxDouble(qualityInitValue); - final debouncerQuality = Debouncer( - Duration(milliseconds: 1000), - onChanged: (double v) { - setCustomValues(quality: v); - }, - initialValue: qualityInitValue, - ); - final qualitySlider = Obx(() => Row( - children: [ - Slider( - value: qualitySliderValue.value, - min: qualityMinValue, - max: qualityMaxValue, - divisions: 18, - onChanged: (double value) { - qualitySliderValue.value = value; - debouncerQuality.value = value; - }, - ), - SizedBox( - width: 40, - child: Text( - '${qualitySliderValue.value.round()}%', - style: const TextStyle(fontSize: 15), - )), - SizedBox( - width: 50, - child: Text( - translate('Bitrate'), - style: const TextStyle(fontSize: 15), - )) - ], - )); - // fps - final fpsOption = - await bind.sessionGetOption(id: widget.id, arg: 'custom-fps'); - fpsInitValue = - fpsOption == null ? 30 : double.tryParse(fpsOption) ?? 30; - if (fpsInitValue < 10 || fpsInitValue > 120) { - fpsInitValue = 30; - } - final RxDouble fpsSliderValue = RxDouble(fpsInitValue); - final debouncerFps = Debouncer( - Duration(milliseconds: 1000), - onChanged: (double v) { - setCustomValues(fps: v); - }, - initialValue: qualityInitValue, - ); - bool? direct; - try { - direct = ConnectionTypeState.find(widget.id).direct.value == - ConnectionType.strDirect; - } catch (_) {} - final fpsSlider = Offstage( - offstage: - (await bind.mainIsUsingPublicServer() && direct != true) || - version_cmp(peer_version, '1.2.0') < 0, - child: Row( - children: [ - Obx((() => Slider( - value: fpsSliderValue.value, - min: 10, - max: 120, - divisions: 22, - onChanged: (double value) { - fpsSliderValue.value = value; - debouncerFps.value = value; - }, - ))), - SizedBox( - width: 40, - child: Obx(() => Text( - '${fpsSliderValue.value.round()}', - style: const TextStyle(fontSize: 15), - ))), - SizedBox( - width: 50, - child: Text( - translate('FPS'), - style: const TextStyle(fontSize: 15), - )) - ], - ), - ); - - final content = Column( - children: [qualitySlider, fpsSlider], - ); - msgBoxCommon(widget.ffi.dialogManager, 'Custom Image Quality', - content, [btnClose]); - } - }, - padding: padding, - ), - MenuEntryDivider(), - ]; - - if (widget.state.viewStyle.value == kRemoteViewStyleOriginal) { - displayMenu.insert( - 2, - MenuEntryRadios( - text: translate('Scroll Style'), - optionsGetter: () => [ - MenuEntryRadioOption( - text: translate('ScrollAuto'), - value: kRemoteScrollStyleAuto, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - enabled: widget.ffi.canvasModel.imageOverflow, - ), - MenuEntryRadioOption( - text: translate('Scrollbar'), - value: kRemoteScrollStyleBar, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - enabled: widget.ffi.canvasModel.imageOverflow, - ), - ], - curOptionGetter: () async => - // null means peer id is not found, which there's no need to care about - await bind.sessionGetScrollStyle(id: widget.id) ?? '', - optionSetter: (String oldValue, String newValue) async { - await bind.sessionSetScrollStyle(id: widget.id, value: newValue); - widget.ffi.canvasModel.updateScrollStyle(); + groupValue: groupValue, + onChanged: (value) { + onChanged(value); + _customImageQualityDialog(); }, - padding: padding, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - )); - displayMenu.insert(3, MenuEntryDivider()); - - if (_isWindowCanBeAdjusted(remoteCount)) { - displayMenu.insert( - 0, - MenuEntryDivider(), - ); - displayMenu.insert( - 0, - MenuEntryButton( - childBuilder: (TextStyle? style) => Container( - child: Text( - translate('Adjust Window'), - style: style, - )), - proc: () { - () async { - await _updateScreen(); - if (_screen != null) { - _setFullscreen(false); - double scale = _screen!.scaleFactor; - final wndRect = - await WindowController.fromWindowId(windowId).getFrame(); - final mediaSize = MediaQueryData.fromWindow(ui.window).size; - // On windows, wndRect is equal to GetWindowRect and mediaSize is equal to GetClientRect. - // https://stackoverflow.com/a/7561083 - double magicWidth = - wndRect.right - wndRect.left - mediaSize.width * scale; - double magicHeight = - wndRect.bottom - wndRect.top - mediaSize.height * scale; - - final canvasModel = widget.ffi.canvasModel; - final width = - (canvasModel.getDisplayWidth() * canvasModel.scale + - canvasModel.windowBorderWidth * 2) * - scale + - magicWidth; - final height = - (canvasModel.getDisplayHeight() * canvasModel.scale + - canvasModel.tabBarHeight + - canvasModel.windowBorderWidth * 2) * - scale + - magicHeight; - double left = wndRect.left + (wndRect.width - width) / 2; - double top = wndRect.top + (wndRect.height - height) / 2; - - Rect frameRect = _screen!.frame; - if (!isFullscreen) { - frameRect = _screen!.visibleFrame; - } - if (left < frameRect.left) { - left = frameRect.left; - } - if (top < frameRect.top) { - top = frameRect.top; - } - if ((left + width) > frameRect.right) { - left = frameRect.right - width; - } - if ((top + height) > frameRect.bottom) { - top = frameRect.bottom - height; - } - await WindowController.fromWindowId(windowId) - .setFrame(Rect.fromLTWH(left, top, width, height)); - } - }(); - }, - padding: padding, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, + ffi: widget.ffi, ), - ); + ].map((e) => _buildPointerTrackWidget(e, widget.ffi)).toList(), + ); + }); + } + + _customImageQualityDialog() async { + double qualityInitValue = 50; + double fpsInitValue = 30; + bool qualitySet = false; + bool fpsSet = false; + setCustomValues({double? quality, double? fps}) async { + if (quality != null) { + qualitySet = true; + await bind.sessionSetCustomImageQuality( + id: widget.id, value: quality.toInt()); + } + if (fps != null) { + fpsSet = true; + await bind.sessionSetCustomFps(id: widget.id, fps: fps.toInt()); + } + if (!qualitySet) { + qualitySet = true; + await bind.sessionSetCustomImageQuality( + id: widget.id, value: qualityInitValue.toInt()); + } + if (!fpsSet) { + fpsSet = true; + await bind.sessionSetCustomFps( + id: widget.id, fps: fpsInitValue.toInt()); } } - /// Show Codec Preference - if (bind.mainHasHwcodec()) { + final btnClose = dialogButton('Close', onPressed: () async { + await setCustomValues(); + widget.ffi.dialogManager.dismissAll(); + }); + + // quality + final quality = await bind.sessionGetCustomImageQuality(id: widget.id); + qualityInitValue = + quality != null && quality.isNotEmpty ? quality[0].toDouble() : 50.0; + const qualityMinValue = 10.0; + const qualityMaxValue = 100.0; + if (qualityInitValue < qualityMinValue) { + qualityInitValue = qualityMinValue; + } + if (qualityInitValue > qualityMaxValue) { + qualityInitValue = qualityMaxValue; + } + final RxDouble qualitySliderValue = RxDouble(qualityInitValue); + final debouncerQuality = Debouncer( + Duration(milliseconds: 1000), + onChanged: (double v) { + setCustomValues(quality: v); + }, + initialValue: qualityInitValue, + ); + final qualitySlider = Obx(() => Row( + children: [ + Slider( + value: qualitySliderValue.value, + min: qualityMinValue, + max: qualityMaxValue, + divisions: 18, + onChanged: (double value) { + qualitySliderValue.value = value; + debouncerQuality.value = value; + }, + ), + SizedBox( + width: 40, + child: Text( + '${qualitySliderValue.value.round()}%', + style: const TextStyle(fontSize: 15), + )), + SizedBox( + width: 50, + child: Text( + translate('Bitrate'), + style: const TextStyle(fontSize: 15), + )) + ], + )); + // fps + final fpsOption = + await bind.sessionGetOption(id: widget.id, arg: 'custom-fps'); + fpsInitValue = fpsOption == null ? 30 : double.tryParse(fpsOption) ?? 30; + if (fpsInitValue < 10 || fpsInitValue > 120) { + fpsInitValue = 30; + } + final RxDouble fpsSliderValue = RxDouble(fpsInitValue); + final debouncerFps = Debouncer( + Duration(milliseconds: 1000), + onChanged: (double v) { + setCustomValues(fps: v); + }, + initialValue: qualityInitValue, + ); + bool? direct; + try { + direct = ConnectionTypeState.find(widget.id).direct.value == + ConnectionType.strDirect; + } catch (_) {} + final fpsSlider = Offstage( + offstage: (await bind.mainIsUsingPublicServer() && direct != true) || + version_cmp(pi.version, '1.2.0') < 0, + child: Row( + children: [ + Obx((() => Slider( + value: fpsSliderValue.value, + min: 10, + max: 120, + divisions: 22, + onChanged: (double value) { + fpsSliderValue.value = value; + debouncerFps.value = value; + }, + ))), + SizedBox( + width: 40, + child: Obx(() => Text( + '${fpsSliderValue.value.round()}', + style: const TextStyle(fontSize: 15), + ))), + SizedBox( + width: 50, + child: Text( + translate('FPS'), + style: const TextStyle(fontSize: 15), + )) + ], + ), + ); + + final content = Column( + children: [qualitySlider, fpsSlider], + ); + msgBoxCommon( + widget.ffi.dialogManager, 'Custom Image Quality', content, [btnClose]); + } + + codec() { + return futureBuilder(future: () async { + final supportedHwcodec = + await bind.sessionSupportedHwcodec(id: widget.id); + final codecPreference = + await bind.sessionGetOption(id: widget.id, arg: 'codec-preference') ?? + ''; + return { + 'supportedHwcodec': supportedHwcodec, + 'codecPreference': codecPreference + }; + }(), hasData: (data) { final List codecs = []; try { - final Map codecsJson = jsonDecode(futureData['supportedHwcodec']); + final Map codecsJson = jsonDecode(data['supportedHwcodec']); final h264 = codecsJson['h264'] ?? false; final h265 = codecsJson['h265'] ?? false; codecs.add(h264); @@ -1365,385 +1274,655 @@ class _RemoteMenubarState extends State { } catch (e) { debugPrint("Show Codec Preference err=$e"); } - if (codecs.length == 2 && (codecs[0] || codecs[1])) { - displayMenu.add(MenuEntryRadios( - text: translate('Codec Preference'), - optionsGetter: () { - final list = [ - MenuEntryRadioOption( - text: translate('Auto'), - value: 'auto', - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - ), - MenuEntryRadioOption( - text: 'VP9', - value: 'vp9', - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - ), - ]; - if (codecs[0]) { - list.add(MenuEntryRadioOption( - text: 'H264', - value: 'h264', - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - )); - } - if (codecs[1]) { - list.add(MenuEntryRadioOption( - text: 'H265', - value: 'h265', - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - )); - } - return list; - }, - curOptionGetter: () async => - // null means peer id is not found, which there's no need to care about - await bind.sessionGetOption( - id: widget.id, arg: 'codec-preference') ?? - '', - optionSetter: (String oldValue, String newValue) async { - await bind.sessionPeerOption( - id: widget.id, name: 'codec-preference', value: newValue); - bind.sessionChangePreferCodec(id: widget.id); - }, - padding: padding, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - )); + final visible = bind.mainHasHwcodec() && + codecs.length == 2 && + (codecs[0] || codecs[1]); + if (!visible) return Offstage(); + final groupValue = data['codecPreference'] as String; + onChanged(String? value) async { + if (value == null) return; + await bind.sessionPeerOption( + id: widget.id, name: 'codec-preference', value: value); + bind.sessionChangePreferCodec(id: widget.id); } - } - displayMenu.add(MenuEntryDivider()); - /// Show remote cursor - if (!widget.ffi.canvasModel.cursorEmbedded) { - displayMenu.add(RemoteMenuEntry.showRemoteCursor( - widget.id, - padding, - dismissCallback: _menuDismissCallback, - )); - } - - /// Show remote cursor scaling with image - if (widget.state.viewStyle.value != kRemoteViewStyleOriginal) { - displayMenu.add(() { - final opt = 'zoom-cursor'; - final state = PeerBoolOption.find(widget.id, opt); - return MenuEntrySwitch2( - switchType: SwitchType.scheckbox, - text: translate('Zoom cursor'), - getter: () { - return state; - }, - setter: (bool v) async { - await bind.sessionToggleOption(id: widget.id, value: opt); - state.value = - bind.sessionGetToggleOptionSync(id: widget.id, arg: opt); - }, - padding: padding, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - ); - }()); - } - - /// Show quality monitor - displayMenu.add(MenuEntrySwitch( - switchType: SwitchType.scheckbox, - text: translate('Show quality monitor'), - getter: () async { - return bind.sessionGetToggleOptionSync( - id: widget.id, arg: 'show-quality-monitor'); - }, - setter: (bool v) async { - await bind.sessionToggleOption( - id: widget.id, value: 'show-quality-monitor'); - widget.ffi.qualityMonitorModel.checkShowQualityMonitor(widget.id); - }, - padding: padding, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - )); - - final perms = widget.ffi.ffiModel.permissions; - final pi = widget.ffi.ffiModel.pi; - - if (perms['audio'] != false) { - displayMenu - .add(_createSwitchMenuEntry('Mute', 'disable-audio', padding, true)); - } - - if (Platform.isWindows && - pi.platform == kPeerPlatformWindows && - perms['file'] != false) { - displayMenu.add(_createSwitchMenuEntry( - 'Allow file copy and paste', 'enable-file-transfer', padding, true)); - } - - if (perms['keyboard'] != false) { - if (perms['clipboard'] != false) { - displayMenu.add(RemoteMenuEntry.disableClipboard( - widget.id, - padding, - dismissCallback: _menuDismissCallback, - )); - } - displayMenu.add(_createSwitchMenuEntry( - 'Lock after session end', 'lock-after-session-end', padding, true)); - if (pi.features.privacyMode) { - displayMenu.add(MenuEntrySwitch2( - switchType: SwitchType.scheckbox, - text: translate('Privacy mode'), - getter: () { - return PrivacyModeState.find(widget.id); - }, - setter: (bool v) async { - await bind.sessionToggleOption( - id: widget.id, value: 'privacy-mode'); - }, - padding: padding, - dismissOnClicked: true, - dismissCallback: _menuDismissCallback, - )); - } - } - return displayMenu; - } - - List> _getKeyboardMenu() { - final List> keyboardMenu = [ - MenuEntryRadios( - text: translate('Ratio'), - optionsGetter: () { - List list = []; - List modes = [ - KeyboardModeMenu(key: 'legacy', menu: 'Legacy mode'), - KeyboardModeMenu(key: 'map', menu: 'Map mode'), - KeyboardModeMenu(key: 'translate', menu: 'Translate mode'), - ]; - - for (KeyboardModeMenu mode in modes) { - if (bind.sessionIsKeyboardModeSupported( - id: widget.id, mode: mode.key)) { - if (mode.key == 'translate') { - if (Platform.isLinux || - widget.ffi.ffiModel.pi.platform == kPeerPlatformLinux) { - continue; - } - } - var text = translate(mode.menu); - if (mode.key == 'translate') { - text = '$text beta'; - } - list.add(MenuEntryRadioOption(text: text, value: mode.key)); - } - } - return list; - }, - curOptionGetter: () async { - return await bind.sessionGetKeyboardMode(id: widget.id) ?? 'legacy'; - }, - optionSetter: (String oldValue, String newValue) async { - await bind.sessionSetKeyboardMode(id: widget.id, value: newValue); - }, - ) - ]; - final localPlatform = - getLocalPlatformForKBLayoutType(widget.ffi.ffiModel.pi.platform); - if (localPlatform != '') { - keyboardMenu.add(MenuEntryDivider()); - keyboardMenu.add( - MenuEntryButton( - childBuilder: (TextStyle? style) => Container( - alignment: AlignmentDirectional.center, - height: _MenubarTheme.height, - child: Row( - children: [ - Obx(() => RichText( - text: TextSpan( - text: '${translate('Local keyboard type')}: ', - style: DefaultTextStyle.of(context).style, - children: [ - TextSpan( - text: KBLayoutType.value, - style: TextStyle(fontWeight: FontWeight.bold), - ), - ], - ), - )), - Expanded( - child: Align( - alignment: Alignment.centerRight, - child: Transform.scale( - scale: 0.8, - child: IconButton( - padding: EdgeInsets.zero, - icon: const Icon(Icons.settings), - onPressed: () { - if (Navigator.canPop(context)) { - Navigator.pop(context); - _menuDismissCallback(); - } - showKBLayoutTypeChooser( - localPlatform, widget.ffi.dialogManager); - }, - ), - ), - )) - ], - )), - proc: () {}, - padding: EdgeInsets.zero, - dismissOnClicked: false, - dismissCallback: _menuDismissCallback, - ), - ); - } - return keyboardMenu; - } - - MenuEntrySwitch _createSwitchMenuEntry( - String text, String option, EdgeInsets? padding, bool dismissOnClicked) { - return RemoteMenuEntry.createSwitchMenuEntry( - widget.id, text, option, padding, dismissOnClicked, - dismissCallback: _menuDismissCallback); - } -} - -void showSetOSPassword( - String id, bool login, OverlayDialogManager dialogManager) async { - final controller = TextEditingController(); - var password = await bind.sessionGetOption(id: id, arg: 'os-password') ?? ''; - var autoLogin = await bind.sessionGetOption(id: id, arg: 'auto-login') != ''; - controller.text = password; - dialogManager.show((setState, close) { - submit() { - var text = controller.text.trim(); - bind.sessionPeerOption(id: id, name: 'os-password', value: text); - bind.sessionPeerOption( - id: id, name: 'auto-login', value: autoLogin ? 'Y' : ''); - if (text != '' && login) { - bind.sessionInputOsPassword(id: id, value: text); - } - close(); - } - - return CustomAlertDialog( - title: Text(translate('OS Password')), - content: Column(mainAxisSize: MainAxisSize.min, children: [ - PasswordWidget(controller: controller), - CheckboxListTile( - contentPadding: const EdgeInsets.all(0), - dense: true, - controlAffinity: ListTileControlAffinity.leading, - title: Text( - translate('Auto Login'), - ), - value: autoLogin, - onChanged: (v) { - if (v == null) return; - setState(() => autoLogin = v); - }, - ), - ]), - actions: [ - dialogButton('Cancel', onPressed: close, isOutline: true), - dialogButton('OK', onPressed: submit), - ], - onSubmit: submit, - onCancel: close, - ); - }); -} - -void showAuditDialog(String id, dialogManager) async { - final controller = TextEditingController(); - dialogManager.show((setState, close) { - submit() { - var text = controller.text.trim(); - if (text != '') { - bind.sessionSendNote(id: id, note: text); - } - close(); - } - - late final focusNode = FocusNode( - onKey: (FocusNode node, RawKeyEvent evt) { - if (evt.logicalKey.keyLabel == 'Enter') { - if (evt is RawKeyDownEvent) { - int pos = controller.selection.base.offset; - controller.text = - '${controller.text.substring(0, pos)}\n${controller.text.substring(pos)}'; - controller.selection = - TextSelection.fromPosition(TextPosition(offset: pos + 1)); - } - return KeyEventResult.handled; - } - if (evt.logicalKey.keyLabel == 'Esc') { - if (evt is RawKeyDownEvent) { - close(); - } - return KeyEventResult.handled; - } else { - return KeyEventResult.ignored; - } - }, - ); - - return CustomAlertDialog( - title: Text(translate('Note')), - content: SizedBox( - width: 250, - height: 120, - child: TextField( - autofocus: true, - keyboardType: TextInputType.multiline, - textInputAction: TextInputAction.newline, - decoration: const InputDecoration.collapsed( - hintText: 'input note here', + return SubmenuButton( + child: Text(translate('Codec')), + menuChildren: [ + _RadioMenuButton( + child: Text(translate('Auto')), + value: 'auto', + groupValue: groupValue, + onChanged: onChanged, + ffi: widget.ffi, ), - // inputFormatters: [ - // LengthLimitingTextInputFormatter(16), - // // FilteringTextInputFormatter(RegExp(r'[a-zA-z][a-zA-z0-9\_]*'), allow: true) - // ], - maxLines: null, - maxLength: 256, - controller: controller, - focusNode: focusNode, - )), - actions: [ - dialogButton('Cancel', onPressed: close, isOutline: true), - dialogButton('OK', onPressed: submit) - ], - onSubmit: submit, - onCancel: close, - ); - }); -} + _RadioMenuButton( + child: Text(translate('VP9')), + value: 'vp9', + groupValue: groupValue, + onChanged: onChanged, + ffi: widget.ffi, + ), + _RadioMenuButton( + child: Text(translate('H264')), + value: 'h264', + groupValue: groupValue, + onChanged: onChanged, + ffi: widget.ffi, + ), + _RadioMenuButton( + child: Text(translate('H265')), + value: 'h265', + groupValue: groupValue, + onChanged: onChanged, + ffi: widget.ffi, + ), + ].map((e) => _buildPointerTrackWidget(e, widget.ffi)).toList()); + }); + } -void showConfirmSwitchSidesDialog( - String id, OverlayDialogManager dialogManager) async { - dialogManager.show((setState, close) { - submit() async { - await bind.sessionSwitchSides(id: id); - closeConnection(id: id); + resolutions() { + final resolutions = widget.ffi.ffiModel.pi.resolutions; + final visible = widget.ffi.ffiModel.permissions["keyboard"] != false && + resolutions.length > 1; + if (!visible) return Offstage(); + final display = widget.ffi.ffiModel.display; + final groupValue = "${display.width}x${display.height}"; + onChanged(String? value) async { + if (value == null) return; + final list = value.split('x'); + if (list.length == 2) { + final w = int.tryParse(list[0]); + final h = int.tryParse(list[1]); + if (w != null && h != null) { + await bind.sessionChangeResolution( + id: widget.id, width: w, height: h); + } + } } - return CustomAlertDialog( - content: msgboxContent('info', 'Switch Sides', - 'Please confirm if you want to share your desktop?'), - actions: [ - dialogButton('Cancel', onPressed: close, isOutline: true), - dialogButton('OK', onPressed: submit), + return SubmenuButton( + menuChildren: resolutions + .map((e) => _RadioMenuButton( + value: '${e.width}x${e.height}', + groupValue: groupValue, + onChanged: onChanged, + ffi: widget.ffi, + child: Text('${e.width}x${e.height}'))) + .toList() + .map((e) => _buildPointerTrackWidget(e, widget.ffi)) + .toList(), + child: Text(translate("Resolution"))); + } + + showRemoteCursor() { + final visible = !widget.ffi.canvasModel.cursorEmbedded; + if (!visible) return Offstage(); + final state = ShowRemoteCursorState.find(widget.id); + final option = 'show-remote-cursor'; + return _CheckboxMenuButton( + value: state.value, + onChanged: (value) async { + if (value == null) return; + await bind.sessionToggleOption(id: widget.id, value: option); + state.value = + bind.sessionGetToggleOptionSync(id: widget.id, arg: option); + }, + ffi: widget.ffi, + child: Text(translate('Show remote cursor'))); + } + + zoomCursor() { + final visible = widget.state.viewStyle.value != kRemoteViewStyleOriginal; + if (!visible) return Offstage(); + final option = 'zoom-cursor'; + final peerState = PeerBoolOption.find(widget.id, option); + return _CheckboxMenuButton( + value: peerState.value, + onChanged: (value) async { + if (value == null) return; + await bind.sessionToggleOption(id: widget.id, value: option); + peerState.value = + bind.sessionGetToggleOptionSync(id: widget.id, arg: option); + }, + ffi: widget.ffi, + child: Text(translate('Zoom cursor'))); + } + + showQualityMonitor() { + final option = 'show-quality-monitor'; + final value = bind.sessionGetToggleOptionSync(id: widget.id, arg: option); + return _CheckboxMenuButton( + value: value, + onChanged: (value) async { + if (value == null) return; + await bind.sessionToggleOption(id: widget.id, value: option); + widget.ffi.qualityMonitorModel.checkShowQualityMonitor(widget.id); + }, + ffi: widget.ffi, + child: Text(translate('Show quality monitor'))); + } + + mute() { + final visible = perms['audio'] != false; + if (!visible) return Offstage(); + final option = 'disable-audio'; + final value = bind.sessionGetToggleOptionSync(id: widget.id, arg: option); + return _CheckboxMenuButton( + value: value, + onChanged: (value) { + if (value == null) return; + bind.sessionToggleOption(id: widget.id, value: option); + }, + ffi: widget.ffi, + child: Text(translate('Mute'))); + } + + fileCopyAndPaste() { + final visible = Platform.isWindows && + pi.platform == kPeerPlatformWindows && + perms['file'] != false; + if (!visible) return Offstage(); + final option = 'enable-file-transfer'; + final value = bind.sessionGetToggleOptionSync(id: widget.id, arg: option); + return _CheckboxMenuButton( + value: value, + onChanged: (value) { + if (value == null) return; + bind.sessionToggleOption(id: widget.id, value: option); + }, + ffi: widget.ffi, + child: Text(translate('Allow file copy and paste'))); + } + + disableClipboard() { + final visible = perms['keyboard'] != false && perms['clipboard'] != false; + if (!visible) return Offstage(); + final option = 'disable-clipboard'; + final value = bind.sessionGetToggleOptionSync(id: widget.id, arg: option); + return _CheckboxMenuButton( + value: value, + onChanged: (value) { + if (value == null) return; + bind.sessionToggleOption(id: widget.id, value: option); + }, + ffi: widget.ffi, + child: Text(translate('Disable clipboard'))); + } + + lockAfterSessionEnd() { + final visible = perms['keyboard'] != false; + if (!visible) return Offstage(); + final option = 'lock-after-session-end'; + final value = bind.sessionGetToggleOptionSync(id: widget.id, arg: option); + return _CheckboxMenuButton( + value: value, + onChanged: (value) { + if (value == null) return; + bind.sessionToggleOption(id: widget.id, value: option); + }, + ffi: widget.ffi, + child: Text(translate('Lock after session end'))); + } + + privacyMode() { + bool visible = perms['keyboard'] != false && pi.features.privacyMode; + if (!visible) return Offstage(); + final option = 'privacy-mode'; + final rxValue = PrivacyModeState.find(widget.id); + return _CheckboxMenuButton( + value: rxValue.value, + onChanged: (value) { + if (value == null) return; + bind.sessionToggleOption(id: widget.id, value: option); + }, + ffi: widget.ffi, + child: Text(translate('Privacy mode'))); + } +} + +class _KeyboardMenu extends StatelessWidget { + final String id; + final FFI ffi; + _KeyboardMenu({ + Key? key, + required this.id, + required this.ffi, + }) : super(key: key); + + PeerInfo get pi => ffi.ffiModel.pi; + + @override + Widget build(BuildContext context) { + var ffiModel = Provider.of(context); + if (ffiModel.permissions['keyboard'] == false) return Offstage(); + // Do not support peer 1.1.9. + if (stateGlobal.grabKeyboard) { + bind.sessionSetKeyboardMode(id: id, value: 'map'); + return Offstage(); + } + return _IconSubmenuButton( + svg: "assets/keyboard.svg", + ffi: ffi, + color: _MenubarTheme.blueColor, + hoverColor: _MenubarTheme.hoverBlueColor, + menuChildren: [mode(), localKeyboardType()]); + } + + mode() { + return futureBuilder(future: () async { + return await bind.sessionGetKeyboardMode(id: id) ?? 'legacy'; + }(), hasData: (data) { + final groupValue = data as String; + List modes = [ + KeyboardModeMenu(key: 'legacy', menu: 'Legacy mode'), + KeyboardModeMenu(key: 'map', menu: 'Map mode'), + KeyboardModeMenu(key: 'translate', menu: 'Translate mode'), + ]; + List<_RadioMenuButton> list = []; + onChanged(String? value) async { + if (value == null) return; + await bind.sessionSetKeyboardMode(id: id, value: value); + } + + for (KeyboardModeMenu mode in modes) { + if (bind.sessionIsKeyboardModeSupported(id: id, mode: mode.key)) { + if (mode.key == 'translate') { + if (Platform.isLinux || pi.platform == kPeerPlatformLinux) { + continue; + } + } + var text = translate(mode.menu); + if (mode.key == 'translate') { + text = '$text beta'; + } + list.add(_RadioMenuButton( + child: Text(text), + value: mode.key, + groupValue: groupValue, + onChanged: onChanged, + ffi: ffi, + )); + } + } + return Column(children: list); + }); + } + + localKeyboardType() { + final localPlatform = getLocalPlatformForKBLayoutType(pi.platform); + final visible = localPlatform != ''; + if (!visible) return Offstage(); + return Column( + children: [ + Divider(), + _MenuItemButton( + child: Text( + '${translate('Local keyboard type')}: ${KBLayoutType.value}'), + trailingIcon: const Icon(Icons.settings), + ffi: ffi, + onPressed: () => + showKBLayoutTypeChooser(localPlatform, ffi.dialogManager), + ) ], - onSubmit: submit, - onCancel: close, ); - }); + } +} + +class _ChatMenu extends StatefulWidget { + final String id; + final FFI ffi; + _ChatMenu({ + Key? key, + required this.id, + required this.ffi, + }) : super(key: key); + + @override + State<_ChatMenu> createState() => _ChatMenuState(); +} + +class _ChatMenuState extends State<_ChatMenu> { + // Using in StatelessWidget got `Looking up a deactivated widget's ancestor is unsafe`. + final chatButtonKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + return _IconSubmenuButton( + key: chatButtonKey, + svg: 'assets/chat.svg', + ffi: widget.ffi, + color: _MenubarTheme.blueColor, + hoverColor: _MenubarTheme.hoverBlueColor, + menuChildren: [textChat(), voiceCall()]); + } + + textChat() { + return _MenuItemButton( + child: Text(translate('Text chat')), + ffi: widget.ffi, + 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(chatInitPos: initPos); + }); + } + + voiceCall() { + return _MenuItemButton( + child: Text(translate('Voice call')), + ffi: widget.ffi, + onPressed: () => bind.sessionRequestVoiceCall(id: widget.id), + ); + } +} + +class _VoiceCallMenu extends StatelessWidget { + final String id; + final FFI ffi; + _VoiceCallMenu({ + Key? key, + required this.id, + required this.ffi, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Obx( + () { + final String tooltip; + final String icon; + switch (ffi.chatModel.voiceCallStatus.value) { + case VoiceCallStatus.waitingForResponse: + tooltip = "Waiting"; + icon = "assets/call_wait.svg"; + break; + case VoiceCallStatus.connected: + tooltip = "Disconnect"; + icon = "assets/call_end.svg"; + break; + default: + return Offstage(); + } + return _IconMenuButton( + assetName: icon, + tooltip: tooltip, + onPressed: () => bind.sessionCloseVoiceCall(id: id), + color: _MenubarTheme.redColor, + hoverColor: _MenubarTheme.hoverRedColor); + }, + ); + } +} + +class _RecordMenu extends StatelessWidget { + const _RecordMenu({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + var ffi = Provider.of(context); + final visible = ffi.permissions['recording'] != false; + if (!visible) return Offstage(); + return Consumer( + builder: (context, value, child) => _IconMenuButton( + assetName: 'assets/rec.svg', + tooltip: + value.start ? 'Stop session recording' : 'Start session recording', + onPressed: () => value.toggle(), + color: value.start ? _MenubarTheme.redColor : _MenubarTheme.blueColor, + hoverColor: value.start + ? _MenubarTheme.hoverRedColor + : _MenubarTheme.hoverBlueColor, + ), + ); + } +} + +class _CloseMenu extends StatelessWidget { + final String id; + final FFI ffi; + const _CloseMenu({Key? key, required this.id, required this.ffi}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return _IconMenuButton( + assetName: 'assets/close.svg', + tooltip: 'Close', + onPressed: () => clientClose(id, ffi.dialogManager), + color: _MenubarTheme.redColor, + hoverColor: _MenubarTheme.hoverRedColor, + ); + } +} + +class _IconMenuButton extends StatefulWidget { + final String? assetName; + final Widget? icon; + final String tooltip; + final Color color; + final Color hoverColor; + final VoidCallback? onPressed; + final double? hMargin; + final double? vMargin; + const _IconMenuButton({ + Key? key, + this.assetName, + this.icon, + required this.tooltip, + required this.color, + required this.hoverColor, + required this.onPressed, + this.hMargin, + this.vMargin, + }) : super(key: key); + + @override + State<_IconMenuButton> createState() => _IconMenuButtonState(); +} + +class _IconMenuButtonState extends State<_IconMenuButton> { + bool hover = false; + + @override + Widget build(BuildContext context) { + assert(widget.assetName != null || widget.icon != null); + final icon = widget.icon ?? + SvgPicture.asset( + widget.assetName!, + color: Colors.white, + width: _MenubarTheme.buttonSize, + height: _MenubarTheme.buttonSize, + ); + return SizedBox( + width: _MenubarTheme.buttonSize, + height: _MenubarTheme.buttonSize, + child: MenuItemButton( + style: ButtonStyle( + padding: MaterialStatePropertyAll(EdgeInsets.zero), + overlayColor: MaterialStatePropertyAll(Colors.transparent)), + onHover: (value) => setState(() { + hover = value; + }), + onPressed: widget.onPressed, + child: Tooltip( + message: translate(widget.tooltip), + child: Material( + type: MaterialType.transparency, + child: Ink( + decoration: BoxDecoration( + borderRadius: + BorderRadius.circular(_MenubarTheme.iconRadius), + color: hover ? widget.hoverColor : widget.color, + ), + child: icon))), + ), + ).marginSymmetric( + horizontal: widget.hMargin ?? _MenubarTheme.buttonHMargin, + vertical: widget.vMargin ?? _MenubarTheme.buttonVMargin); + } +} + +class _IconSubmenuButton extends StatefulWidget { + final String? svg; + final Widget? icon; + final Color color; + final Color hoverColor; + final List menuChildren; + final MenuStyle? menuStyle; + final FFI ffi; + + _IconSubmenuButton( + {Key? key, + this.svg, + this.icon, + required this.color, + required this.hoverColor, + required this.menuChildren, + required this.ffi, + this.menuStyle}) + : super(key: key); + + @override + State<_IconSubmenuButton> createState() => _IconSubmenuButtonState(); +} + +class _IconSubmenuButtonState extends State<_IconSubmenuButton> { + bool hover = false; + + @override + Widget build(BuildContext context) { + assert(widget.svg != null || widget.icon != null); + final icon = widget.icon ?? + SvgPicture.asset( + widget.svg!, + color: Colors.white, + width: _MenubarTheme.buttonSize, + height: _MenubarTheme.buttonSize, + ); + return SizedBox( + width: _MenubarTheme.buttonSize, + height: _MenubarTheme.buttonSize, + child: SubmenuButton( + menuStyle: widget.menuStyle, + style: ButtonStyle( + padding: MaterialStatePropertyAll(EdgeInsets.zero), + overlayColor: MaterialStatePropertyAll(Colors.transparent)), + onHover: (value) => setState(() { + hover = value; + }), + child: Material( + type: MaterialType.transparency, + child: Ink( + decoration: BoxDecoration( + borderRadius: + BorderRadius.circular(_MenubarTheme.iconRadius), + color: hover ? widget.hoverColor : widget.color, + ), + child: icon)), + menuChildren: widget.menuChildren + .map((e) => _buildPointerTrackWidget(e, widget.ffi)) + .toList())) + .marginSymmetric( + horizontal: _MenubarTheme.buttonHMargin, + vertical: _MenubarTheme.buttonVMargin); + } +} + +class _MenuItemButton extends StatelessWidget { + final VoidCallback? onPressed; + final Widget? trailingIcon; + final Widget? child; + final FFI ffi; + _MenuItemButton( + {Key? key, + this.onPressed, + this.trailingIcon, + required this.child, + required this.ffi}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return MenuItemButton( + key: key, + onPressed: onPressed != null + ? () { + _menuDismissCallback(ffi); + onPressed?.call(); + } + : null, + trailingIcon: trailingIcon, + child: child); + } +} + +class _CheckboxMenuButton extends StatelessWidget { + final bool? value; + final ValueChanged? onChanged; + final Widget? child; + final FFI ffi; + const _CheckboxMenuButton( + {Key? key, + required this.value, + required this.onChanged, + required this.child, + required this.ffi}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return CheckboxMenuButton( + key: key, + value: value, + child: child, + onChanged: onChanged != null + ? (bool? value) { + _menuDismissCallback(ffi); + onChanged?.call(value); + } + : null, + ); + } +} + +class _RadioMenuButton extends StatelessWidget { + final T value; + final T? groupValue; + final ValueChanged? onChanged; + final Widget? child; + final FFI ffi; + const _RadioMenuButton( + {Key? key, + required this.value, + required this.groupValue, + required this.onChanged, + required this.child, + required this.ffi}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return RadioMenuButton( + value: value, + groupValue: groupValue, + child: child, + onChanged: onChanged != null + ? (T? value) { + _menuDismissCallback(ffi); + onChanged?.call(value); + } + : null, + ); + } } class _DraggableShowHide extends StatefulWidget { @@ -1843,3 +2022,15 @@ class KeyboardModeMenu { KeyboardModeMenu({required this.key, required this.menu}); } + +_menuDismissCallback(FFI ffi) => ffi.inputModel.refreshMousePos(); + +Widget _buildPointerTrackWidget(Widget child, FFI ffi) { + return Listener( + onPointerHover: (PointerHoverEvent e) => + ffi.inputModel.lastMousePos = e.position, + child: MouseRegion( + child: child, + ), + ); +} diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 45c552848..71aa39337 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 4824ac5e9..818e63203 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", "停止语音通话"), ("relay_hint_tip", "可能无法直连,可以尝试中继连接。\n另外,如果想直接使用中继连接,可以在 ID 后面添加/r,或者在卡片选项里选择强制走中继连接。"), ("Reconnect", "重连"), + ("Codec", "编解码"), + ("Resolution", "分辨率"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index e2761e45e..be0ffa7f4 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 2020a2b6f..150a57715 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 7cf563fc3..c9c25df2b 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", "Sprachanruf beenden"), ("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"), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index c22532440..bb2615efc 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 3ce2860f0..d7e43b6bf 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", "Detener llamada de voz"), ("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"), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 70051f3e8..d8fcff436 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", "توقف تماس صوتی"), ("relay_hint_tip", " را به شناسه اضافه کنید یا گزینه \"همیشه از طریق رله متصل شوید\" را در کارت همتا انتخاب کنید. همچنین، اگر می‌خواهید فوراً از سرور رله استفاده کنید، می‌توانید پسوند \"/r\".\n اتصال مستقیم ممکن است امکان پذیر نباشد. در این صورت می توانید سعی کنید از طریق سرور رله متصل شوید"), ("Reconnect", "اتصال مجدد"), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 1f6e9f55b..cb4d8d69f 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/gr.rs b/src/lang/gr.rs index b7ebf4577..c18e6c07b 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 21ab28214..557e3faf0 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index f48de17f6..1a34e6fea 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 4c63106da..7256b13d8 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", "Interrompi la chiamata vocale"), ("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"), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index b291a6e7a..d6354c1c9 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index d63e83187..dc57c8bf9 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index b8b9eb1df..6698b2c5f 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 1a806c803..545e1ec2e 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", "Stop spraakoproep"), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 2b29c7cb2..eea46accb 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index e91cd3909..ee1561123 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index b0fe9175d..7b16bdf34 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index d0232ba37..315eadd2a 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index fe5d708ad..6d212490b 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", "Завершить голосовой вызов"), ("relay_hint_tip", "Прямое подключение может оказаться невозможным. В этом случае можно попытаться подключиться через сервер ретрансляции. \nКроме того, если вы хотите сразу использовать сервер ретрансляции, можно добавить к ID суффикс \"/r\" или включить \"Всегда подключаться через ретранслятор\" в настройках удалённого узла."), ("Reconnect", "Переподключить"), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 458002f4c..462a78ab6 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 2abd1870f..0eb1949fe 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 6b739e8ab..2fc5dfe0d 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 90a435fd7..17882094c 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index a98ea6346..250cf3405 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 61c2b5d28..dcdcc1289 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index 236ee5e8d..a1eb34c54 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index f2a34e212..09c40a83f 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 84e74716f..ca1193eaa 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", "停止語音聊天"), ("relay_hint_tip", "可能無法直連,可以嘗試中繼連接。 \n另外,如果想直接使用中繼連接,可以在ID後面添加/r,或者在卡片選項裡選擇強制走中繼連接。"), ("Reconnect", "重連"), + ("Codec", "編解碼"), + ("Resolution", "分辨率"), ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 0c4caf4db..b48385e6e 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 19e1184d9..61d7c0b8a 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -454,5 +454,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop voice call", ""), ("relay_hint_tip", ""), ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), ].iter().cloned().collect(); } From f5edf44f0f9c28ea865647e9596b1b5bccbdae2b Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 23 Feb 2023 21:31:00 +0800 Subject: [PATCH 2006/2015] remote menubar theme Signed-off-by: 21pages --- .../lib/desktop/widgets/remote_menubar.dart | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 993d02683..b32520fa6 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -408,18 +408,32 @@ class _RemoteMenubarState extends State { ), child: SingleChildScrollView( scrollDirection: Axis.horizontal, - child: MenuBar( - children: [ - SizedBox(width: _MenubarTheme.buttonHMargin), - ...menubarItems, - SizedBox(width: _MenubarTheme.buttonHMargin) - ], + child: Theme( + data: themeData(), + child: MenuBar( + children: [ + SizedBox(width: _MenubarTheme.buttonHMargin), + ...menubarItems, + SizedBox(width: _MenubarTheme.buttonHMargin) + ], + ), )), ), _buildDraggableShowHide(context), ], ); } + + ThemeData themeData() { + return Theme.of(context).copyWith( + menuButtonTheme: MenuButtonThemeData( + style: ButtonStyle( + minimumSize: MaterialStatePropertyAll(Size(64, 36)), + textStyle: MaterialStatePropertyAll( + TextStyle(fontWeight: FontWeight.normal)))), + dividerTheme: DividerThemeData(space: 4), + ); + } } class _PinMenu extends StatelessWidget { From 69f16ccd9f086cebfa2053f9df48c6d96b42c7d3 Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 23 Feb 2023 21:57:45 +0800 Subject: [PATCH 2007/2015] delay 3s to adjust window after changing resolution Signed-off-by: 21pages --- flutter/lib/desktop/widgets/remote_menubar.dart | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index b32520fa6..4f9a227bd 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -1351,6 +1351,14 @@ class _DisplayMenuState extends State<_DisplayMenu> { if (w != null && h != null) { await bind.sessionChangeResolution( id: widget.id, width: w, height: h); + Future.delayed(Duration(seconds: 3), () async { + final display = widget.ffi.ffiModel.display; + if (w == display.width && h == display.height) { + if (_isWindowCanBeAdjusted()) { + _doAdjustWindow(); + } + } + }); } } } From 920477bbb2d9fde761fc1c1321ea19bca7d58912 Mon Sep 17 00:00:00 2001 From: 21pages Date: Fri, 24 Feb 2023 13:13:51 +0800 Subject: [PATCH 2008/2015] delete discovery from RustDesk_lan_peers.toml Signed-off-by: 21pages --- flutter/lib/common/widgets/peer_card.dart | 8 ++++++-- src/flutter_ffi.rs | 4 ++++ src/ui.rs | 4 +--- src/ui_interface.rs | 7 +++++++ 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index 8d4d58772..470b631ce 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -534,7 +534,7 @@ abstract class BasePeerCard extends StatelessWidget { proc: () { () async { if (isLan) { - // TODO + bind.mainRemoveDiscovered(id: id); } else { final favs = (await bind.mainGetFav()).toList(); if (favs.remove(id)) { @@ -859,7 +859,11 @@ class DiscoveredPeerCard extends BasePeerCard { } menuItems.add(MenuEntryDivider()); - menuItems.add(_removeAction(peer.id, () async {})); + menuItems.add( + _removeAction(peer.id, () async { + await bind.mainLoadLanPeers(); + }, isLan: true), + ); return menuItems; } diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 8a8bf4de4..23a65c2da 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -796,6 +796,10 @@ pub fn main_load_lan_peers() { }; } +pub fn main_remove_discovered(id: String) { + remove_discovered(id); +} + fn main_broadcast_message(data: &HashMap<&str, &str>) { let apps = vec![ flutter::APP_TYPE_DESKTOP_REMOTE, diff --git a/src/ui.rs b/src/ui.rs index 1b6838e46..a197cb257 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -413,9 +413,7 @@ impl UI { } fn remove_discovered(&mut self, id: String) { - let mut peers = config::LanPeers::load().peers; - peers.retain(|x| x.id != id); - config::LanPeers::store(&peers); + remove_discovered(id); } fn send_wol(&mut self, id: String) { diff --git a/src/ui_interface.rs b/src/ui_interface.rs index dd111f86e..3b2ba0897 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -596,6 +596,13 @@ pub fn get_lan_peers() -> Vec> { .collect() } +#[inline] +pub fn remove_discovered(id: String) { + let mut peers = config::LanPeers::load().peers; + peers.retain(|x| x.id != id); + config::LanPeers::store(&peers); +} + #[inline] pub fn get_uuid() -> String { base64::encode(hbb_common::get_uuid()) From 0ad6bca9ceb87bcdd17354a29460ac34aec3fef5 Mon Sep 17 00:00:00 2001 From: 21pages Date: Fri, 24 Feb 2023 13:39:28 +0800 Subject: [PATCH 2009/2015] check existence for visibility of addFavAction/addToAb Signed-off-by: 21pages --- flutter/lib/common/widgets/peer_card.dart | 38 ++++++++++------------- src/flutter_ffi.rs | 4 +++ 2 files changed, 20 insertions(+), 22 deletions(-) diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index 470b631ce..5b6120a02 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -745,12 +745,9 @@ class RecentPeerCard extends BasePeerCard { } if (gFFI.userModel.userName.isNotEmpty) { - // if (!gFFI.abModel.idContainBy(peer.id)) { - // menuItems.add(_addToAb(peer)); - // } else { - // menuItems.add(_removeFromAb(peer)); - // } - menuItems.add(_addToAb(peer)); + if (!gFFI.abModel.idContainBy(peer.id)) { + menuItems.add(_addToAb(peer)); + } } menuItems.add(MenuEntryDivider()); @@ -797,12 +794,9 @@ class FavoritePeerCard extends BasePeerCard { })); if (gFFI.userModel.userName.isNotEmpty) { - // if (!gFFI.abModel.idContainBy(peer.id)) { - // menuItems.add(_addToAb(peer)); - // } else { - // menuItems.add(_removeFromAb(peer)); - // } - menuItems.add(_addToAb(peer)); + if (!gFFI.abModel.idContainBy(peer.id)) { + menuItems.add(_addToAb(peer)); + } } menuItems.add(MenuEntryDivider()); @@ -843,19 +837,19 @@ class DiscoveredPeerCard extends BasePeerCard { menuItems.add(_createShortCutAction(peer.id)); } - if (!favs.contains(peer.id)) { - menuItems.add(_addFavAction(peer.id)); - } else { - menuItems.add(_rmFavAction(peer.id, () async {})); + final inRecent = await bind.mainIsInRecentPeers(id: peer.id); + if (inRecent) { + if (!favs.contains(peer.id)) { + menuItems.add(_addFavAction(peer.id)); + } else { + menuItems.add(_rmFavAction(peer.id, () async {})); + } } if (gFFI.userModel.userName.isNotEmpty) { - // if (!gFFI.abModel.idContainBy(peer.id)) { - // menuItems.add(_addToAb(peer)); - // } else { - // menuItems.add(_removeFromAb(peer)); - // } - menuItems.add(_addToAb(peer)); + if (!gFFI.abModel.idContainBy(peer.id)) { + menuItems.add(_addToAb(peer)); + } } menuItems.add(MenuEntryDivider()); diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 23a65c2da..0866ff739 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -726,6 +726,10 @@ pub fn main_peer_has_password(id: String) -> bool { peer_has_password(id) } +pub fn main_is_in_recent_peers(id: String) -> bool { + PeerConfig::peers().iter().any(|e| e.0 == id) +} + pub fn main_load_recent_peers() { if !config::APP_DIR.read().unwrap().is_empty() { let peers: Vec> = PeerConfig::peers() From a9598e006a61a6eeee758c0031025b9a50eca0a1 Mon Sep 17 00:00:00 2001 From: 21pages Date: Fri, 24 Feb 2023 15:51:13 +0800 Subject: [PATCH 2010/2015] request elevation menu Signed-off-by: 21pages --- .../lib/desktop/widgets/remote_menubar.dart | 10 ++++++++++ flutter/lib/mobile/widgets/dialog.dart | 7 +++---- flutter/lib/models/model.dart | 20 +++++++++++++++++++ src/client/io_loop.rs | 19 ++++++++++-------- src/flutter.rs | 7 +++++++ src/server/connection.rs | 16 +++++---------- src/ui/remote.rs | 2 ++ src/ui_session_interface.rs | 1 + 8 files changed, 59 insertions(+), 23 deletions(-) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 4f9a227bd..bdf9c1f1d 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -598,6 +598,7 @@ class _ControlMenu extends StatelessWidget { hoverColor: _MenubarTheme.hoverBlueColor, ffi: ffi, menuChildren: [ + requestElevation(), osPassword(), transferFile(context), tcpTunneling(context), @@ -611,6 +612,15 @@ class _ControlMenu extends StatelessWidget { ]); } + requestElevation() { + final visible = ffi.elevationModel.showRequestMenu; + if (!visible) return Offstage(); + return _MenuItemButton( + child: Text(translate('Request Elevation')), + ffi: ffi, + onPressed: () => showRequestElevationDialog(id, ffi.dialogManager)); + } + osPassword() { return _MenuItemButton( child: Text(translate('OS Password')), diff --git a/flutter/lib/mobile/widgets/dialog.dart b/flutter/lib/mobile/widgets/dialog.dart index 7e9a9879c..931999382 100644 --- a/flutter/lib/mobile/widgets/dialog.dart +++ b/flutter/lib/mobile/widgets/dialog.dart @@ -374,8 +374,7 @@ void showWaitUacDialog( )); } -void _showRequestElevationDialog( - String id, OverlayDialogManager dialogManager) { +void showRequestElevationDialog(String id, OverlayDialogManager dialogManager) { RxString groupValue = ''.obs; RxString errUser = ''.obs; RxString errPwd = ''.obs; @@ -531,7 +530,7 @@ void showOnBlockDialog( dialogManager.show(tag: '$id-$type', (setState, close) { void submit() { close(); - _showRequestElevationDialog(id, dialogManager); + showRequestElevationDialog(id, dialogManager); } return CustomAlertDialog( @@ -553,7 +552,7 @@ void showElevationError(String id, String type, String title, String text, dialogManager.show(tag: '$id-$type', (setState, close) { void submit() { close(); - _showRequestElevationDialog(id, dialogManager); + showRequestElevationDialog(id, dialogManager); } return CustomAlertDialog( diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index f4efe2f08..eae41679f 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -203,6 +203,8 @@ class FfiModel with ChangeNotifier { final peer_id = evt['peer_id'].toString(); await bind.sessionSwitchSides(id: peer_id); closeConnection(id: peer_id); + } else if (name == 'portable_service_running') { + parent.target?.elevationModel.onPortableServiceRunning(evt); } else if (name == "on_url_scheme_received") { final url = evt['url'].toString(); parseRustdeskUri(url); @@ -439,6 +441,7 @@ class FfiModel with ChangeNotifier { Map features = json.decode(evt['features']); _pi.features.privacyMode = features['privacy_mode'] == 1; handleResolutions(peerId, evt["resolutions"]); + parent.target?.elevationModel.onPeerInfo(_pi); } notifyListeners(); } @@ -1395,6 +1398,21 @@ class RecordingModel with ChangeNotifier { } } +class ElevationModel with ChangeNotifier { + WeakReference parent; + ElevationModel(this.parent); + bool _running = false; + bool _canElevate = false; + bool get showRequestMenu => _canElevate && !_running; + onPeerInfo(PeerInfo pi) { + _canElevate = pi.platform == kPeerPlatformWindows && pi.sasEnabled == false; + } + + onPortableServiceRunning(Map evt) { + _running = evt['running'] == 'true'; + } +} + enum ConnType { defaultConn, fileTransfer, portForward, rdp } /// Flutter state manager and data communication with the Rust core. @@ -1420,6 +1438,7 @@ class FFI { late final QualityMonitorModel qualityMonitorModel; // session late final RecordingModel recordingModel; // session late final InputModel inputModel; // session + late final ElevationModel elevationModel; // session FFI() { imageModel = ImageModel(WeakReference(this)); @@ -1436,6 +1455,7 @@ class FFI { qualityMonitorModel = QualityMonitorModel(WeakReference(this)); recordingModel = RecordingModel(WeakReference(this)); inputModel = InputModel(WeakReference(this)); + elevationModel = ElevationModel(WeakReference(this)); } /// Start with the given [id]. Only transfer file if [isFileTransfer], only port forward if [isPortForward]. diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index b51c481a5..1c7788193 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -56,6 +56,7 @@ pub struct Remote { data_count: Arc, frame_count: Arc, video_format: CodecFormat, + elevation_requested: bool, } impl Remote { @@ -87,6 +88,7 @@ impl Remote { video_format: CodecFormat::Unknown, stop_voice_call_sender: None, voice_call_request_timestamp: None, + elevation_requested: false, } } @@ -686,6 +688,7 @@ impl Remote { let mut msg = Message::new(); msg.set_misc(misc); allow_err!(peer.send(&msg).await); + self.elevation_requested = true; } Data::ElevateWithLogon(username, password) => { let mut request = ElevationRequest::new(); @@ -699,6 +702,7 @@ impl Remote { let mut msg = Message::new(); msg.set_misc(misc); allow_err!(peer.send(&msg).await); + self.elevation_requested = true; } Data::NewVoiceCall => { let msg = new_voice_call_request(true); @@ -1181,7 +1185,8 @@ impl Remote { } } Some(misc::Union::PortableServiceRunning(b)) => { - if b { + self.handler.portable_service_running(b); + if self.elevation_requested && b { self.handler.msgbox( "custom-nocancel-success", "Successful", @@ -1253,14 +1258,12 @@ impl Remote { } } } - Some(message::Union::PeerInfo(pi)) => { - match pi.conn_id { - crate::SYNC_PEER_INFO_DISPLAYS => { - self.handler.set_displays(&pi.displays); - } - _ => {} + Some(message::Union::PeerInfo(pi)) => match pi.conn_id { + crate::SYNC_PEER_INFO_DISPLAYS => { + self.handler.set_displays(&pi.displays); } - } + _ => {} + }, _ => {} } } diff --git a/src/flutter.rs b/src/flutter.rs index ea73eb925..2f660775f 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -572,6 +572,13 @@ impl InvokeUiSession for FlutterHandler { self.push_event("switch_back", [("peer_id", peer_id)].into()); } + fn portable_service_running(&self, running: bool) { + self.push_event( + "portable_service_running", + [("running", running.to_string().as_str())].into(), + ); + } + fn on_voice_call_started(&self) { self.push_event("on_voice_call_started", [].into()); } diff --git a/src/server/connection.rs b/src/server/connection.rs index 85fcb676b..b2e198681 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1552,7 +1552,6 @@ impl Connection { .err() .map_or("".to_string(), |e| e.to_string()); } - self.portable.elevation_requested = err.is_empty(); let mut misc = Misc::new(); misc.set_elevation_response(err); let mut msg = Message::new(); @@ -1571,7 +1570,6 @@ impl Connection { .err() .map_or("".to_string(), |e| e.to_string()); } - self.portable.elevation_requested = err.is_empty(); let mut misc = Misc::new(); misc.set_elevation_response(err); let mut msg = Message::new(); @@ -1936,13 +1934,11 @@ impl Connection { let p = &mut self.portable; if running != p.last_running { p.last_running = running; - if running && p.elevation_requested { - let mut misc = Misc::new(); - misc.set_portable_service_running(running); - let mut msg = Message::new(); - msg.set_misc(misc); - self.inner.send(msg.into()); - } + let mut misc = Misc::new(); + misc.set_portable_service_running(running); + let mut msg = Message::new(); + msg.set_misc(misc); + self.inner.send(msg.into()); } let uac = crate::video_service::IS_UAC_RUNNING.lock().unwrap().clone(); if p.last_uac != uac { @@ -2166,7 +2162,6 @@ pub struct PortableState { pub last_foreground_window_elevated: bool, pub last_running: bool, pub is_installed: bool, - pub elevation_requested: bool, } #[cfg(windows)] @@ -2177,7 +2172,6 @@ impl Default for PortableState { last_uac: Default::default(), last_foreground_window_elevated: Default::default(), last_running: Default::default(), - elevation_requested: Default::default(), } } } diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 7b31c84e9..c6e0229b2 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -277,6 +277,8 @@ impl InvokeUiSession for SciterHandler { fn switch_back(&self, _id: &str) {} + fn portable_service_running(&self, _running: bool) {} + 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 fd5a7d9c0..f726ed526 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -805,6 +805,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 portable_service_running(&self, running: bool); fn on_voice_call_started(&self); fn on_voice_call_closed(&self, reason: &str); fn on_voice_call_waiting(&self); From c3c4505132b3e7109361d02a1b6fa69a7377bd9b Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 24 Feb 2023 14:15:54 +0800 Subject: [PATCH 2011/2015] feat: make file manager draggable --- flutter/lib/consts.dart | 2 + .../lib/desktop/pages/file_manager_page.dart | 170 ++++++++++++------ .../lib/desktop/widgets/dragable_divider.dart | 53 ++++++ .../widgets/list_search_action_listener.dart | 1 + 4 files changed, 168 insertions(+), 58 deletions(-) create mode 100644 flutter/lib/desktop/widgets/dragable_divider.dart diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 2b73182fd..537784918 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -53,6 +53,8 @@ const int kDesktopMaxDisplayHeight = 1080; const double kDesktopFileTransferNameColWidth = 200; const double kDesktopFileTransferModifiedColWidth = 120; +const double kDesktopFileTransferMinimumWidth = 100; +const double kDesktopFileTransferMaximumWidth = 300; const double kDesktopFileTransferRowHeight = 30.0; const double kDesktopFileTransferHeaderHeight = 25.0; diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 0d55552af..68023f929 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:io'; import 'dart:math'; +import 'package:flutter_hbb/desktop/widgets/dragable_divider.dart'; import 'package:percent_indicator/percent_indicator.dart'; import 'package:desktop_drop/desktop_drop.dart'; import 'package:flutter/gestures.dart'; @@ -78,6 +79,10 @@ class _FileManagerPageState extends State final _keyboardNodeRemote = FocusNode(debugLabel: "keyboardNodeRemote"); final _listSearchBufferLocal = TimeoutStringBuffer(); final _listSearchBufferRemote = TimeoutStringBuffer(); + final _nameColWidthLocal = kDesktopFileTransferNameColWidth.obs; + final _modifiedColWidthLocal = kDesktopFileTransferModifiedColWidth.obs; + final _nameColWidthRemote = kDesktopFileTransferNameColWidth.obs; + final _modifiedColWidthRemote = kDesktopFileTransferModifiedColWidth.obs; /// [_lastClickTime], [_lastClickEntry] help to handle double click int _lastClickTime = @@ -297,11 +302,12 @@ class _FileManagerPageState extends State } var searchResult = entries .skip(skipCount) - .where((element) => element.name.startsWith(buffer)); + .where((element) => element.name.toLowerCase().startsWith(buffer)); if (searchResult.isEmpty) { // cannot find next, lets restart search from head + debugPrint("restart search from head"); searchResult = - entries.where((element) => element.name.startsWith(buffer)); + entries.where((element) => element.name.toLowerCase().startsWith(buffer)); } if (searchResult.isEmpty) { setState(() { @@ -316,7 +322,7 @@ class _FileManagerPageState extends State debugPrint("searching for $buffer"); final selectedEntries = getSelectedItems(isLocal); final searchResult = - entries.where((element) => element.name.startsWith(buffer)); + entries.where((element) => element.name.toLowerCase().startsWith(buffer)); selectedEntries.clear(); if (searchResult.isEmpty) { setState(() { @@ -362,37 +368,41 @@ class _FileManagerPageState extends State 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) - : SvgPicture.asset( - entry.isFile - ? "assets/file.svg" - : "assets/folder.svg", - color: Theme.of(context) - .tabBarTheme - .labelColor, - ), - Expanded( - child: Text( - entry.name.nonBreaking, - overflow: - TextOverflow.ellipsis)) - ]), - )), + child: Obx( + () => Container( + width: isLocal + ? _nameColWidthLocal.value + : _nameColWidthRemote.value, + 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) + : SvgPicture.asset( + entry.isFile + ? "assets/file.svg" + : "assets/folder.svg", + color: Theme.of(context) + .tabBarTheme + .labelColor, + ), + Expanded( + child: Text( + entry.name.nonBreaking, + overflow: + TextOverflow.ellipsis)) + ]), + )), + ), onTap: () { final items = getSelectedItems(isLocal); // handle double click @@ -406,24 +416,35 @@ class _FileManagerPageState extends State items, filteredEntries, entry, isLocal); }, ), + SizedBox( + width: 2.0, + ), GestureDetector( - child: SizedBox( - width: kDesktopFileTransferModifiedColWidth, - child: Tooltip( - waitDuration: - Duration(milliseconds: 500), - message: lastModifiedStr, - child: Text( - lastModifiedStr, - style: TextStyle( - fontSize: 12, - color: MyTheme.darkGray, - ), - )), + child: Obx( + () => SizedBox( + width: isLocal + ? _modifiedColWidthLocal.value + : _modifiedColWidthRemote.value, + child: Tooltip( + waitDuration: + Duration(milliseconds: 500), + message: lastModifiedStr, + child: Text( + lastModifiedStr, + style: TextStyle( + fontSize: 12, + color: MyTheme.darkGray, + ), + )), + ), ), ), + // Divider from header. SizedBox( - width: 100, + width: 2.0, + ), + Expanded( + // width: 100, child: GestureDetector( child: Tooltip( waitDuration: Duration(milliseconds: 500), @@ -1362,6 +1383,7 @@ class _FileManagerPageState extends State Text( name, style: headerTextStyle, + overflow: TextOverflow.ellipsis, ).marginSymmetric(horizontal: 4), ascending.value != null ? Icon( @@ -1383,16 +1405,48 @@ class _FileManagerPageState extends State } 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)) - ], + final nameColWidth = isLocal ? _nameColWidthLocal : _nameColWidthRemote; + final modifiedColWidth = + isLocal ? _modifiedColWidthLocal : _modifiedColWidthRemote; + final padding = EdgeInsets.all(1.0); + return SizedBox( + height: kDesktopFileTransferHeaderHeight, + child: Row( + children: [ + Obx( + () => headerItemFunc( + nameColWidth.value, SortBy.name, translate("Name"), isLocal), + ), + DraggableDivider( + axis: Axis.vertical, + onPointerMove: (dx) { + nameColWidth.value += dx; + nameColWidth.value = min( + kDesktopFileTransferMaximumWidth, + max(kDesktopFileTransferMinimumWidth, + nameColWidth.value)); + }, + padding: padding, + ), + Obx( + () => headerItemFunc(modifiedColWidth.value, SortBy.modified, + translate("Modified"), isLocal), + ), + DraggableDivider( + axis: Axis.vertical, + onPointerMove: (dx) { + modifiedColWidth.value += dx; + modifiedColWidth.value = min( + kDesktopFileTransferMaximumWidth, + max(kDesktopFileTransferMinimumWidth, + modifiedColWidth.value)); + }, + padding: padding), + Expanded( + child: + headerItemFunc(null, SortBy.size, translate("Size"), isLocal)) + ], + ), ); } } diff --git a/flutter/lib/desktop/widgets/dragable_divider.dart b/flutter/lib/desktop/widgets/dragable_divider.dart new file mode 100644 index 000000000..3821b7e0d --- /dev/null +++ b/flutter/lib/desktop/widgets/dragable_divider.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/src/widgets/framework.dart'; +import 'package:flutter/src/widgets/placeholder.dart'; + +class DraggableDivider extends StatefulWidget { + final Axis axis; + final double thickness; + final Color color; + final Function(double)? onPointerMove; + final VoidCallback? onHover; + final EdgeInsets padding; + const DraggableDivider({ + super.key, + this.axis = Axis.horizontal, + this.thickness = 1.0, + this.color = const Color.fromARGB(200, 177, 175, 175), + this.onPointerMove, + this.padding = const EdgeInsets.symmetric(horizontal: 1.0), + this.onHover, + }); + + @override + State createState() => _DraggableDividerState(); +} + +class _DraggableDividerState extends State { + @override + Widget build(BuildContext context) { + return Listener( + onPointerMove: (event) { + final dl = + widget.axis == Axis.horizontal ? event.localDelta.dy : event.localDelta.dx; + widget.onPointerMove?.call(dl); + }, + onPointerHover: (event) => widget.onHover?.call(), + child: MouseRegion( + cursor: SystemMouseCursors.resizeLeftRight, + child: Padding( + padding: widget.padding, + child: Container( + decoration: BoxDecoration(color: widget.color), + width: widget.axis == Axis.horizontal + ? double.infinity + : widget.thickness, + height: widget.axis == Axis.horizontal + ? widget.thickness + : double.infinity, + ), + ), + ), + ); + } +} diff --git a/flutter/lib/desktop/widgets/list_search_action_listener.dart b/flutter/lib/desktop/widgets/list_search_action_listener.dart index 9598c3400..36128bf26 100644 --- a/flutter/lib/desktop/widgets/list_search_action_listener.dart +++ b/flutter/lib/desktop/widgets/list_search_action_listener.dart @@ -55,6 +55,7 @@ class TimeoutStringBuffer { } ListSearchAction input(String ch) { + ch = ch.toLowerCase(); final curr = DateTime.now(); try { if (curr.difference(_duration).inMilliseconds > timeoutMilliSec) { From b10c0ffe54c30649a7e3394a7ccf1f03295cd59d Mon Sep 17 00:00:00 2001 From: Kingtous Date: Fri, 24 Feb 2023 15:56:37 +0800 Subject: [PATCH 2012/2015] opt: fs explorer resizable & search next for loop --- .../lib/desktop/pages/file_manager_page.dart | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 68023f929..569e1cb9a 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -316,7 +316,7 @@ class _FileManagerPageState extends State return; } _jumpToEntry(isLocal, searchResult.first, scrollController, - kDesktopFileTransferRowHeight, buffer); + kDesktopFileTransferRowHeight); }, onSearch: (buffer) { debugPrint("searching for $buffer"); @@ -331,7 +331,7 @@ class _FileManagerPageState extends State return; } _jumpToEntry(isLocal, searchResult.first, scrollController, - kDesktopFileTransferRowHeight, buffer); + kDesktopFileTransferRowHeight); }, child: ObxValue( (searchText) { @@ -471,7 +471,11 @@ class _FileManagerPageState extends State return Column( children: [ // Header - _buildFileBrowserHeader(context, isLocal), + Row( + children: [ + Expanded(child: _buildFileBrowserHeader(context, isLocal)), + ], + ), // Body Expanded( child: ListView.builder( @@ -493,7 +497,7 @@ class _FileManagerPageState extends State } void _jumpToEntry(bool isLocal, Entry entry, - ScrollController scrollController, double rowHeight, String buffer) { + ScrollController scrollController, double rowHeight) { final entries = model.getCurrentDir(isLocal).entries; final index = entries.indexOf(entry); if (index == -1) { @@ -501,7 +505,7 @@ class _FileManagerPageState extends State } final selectedEntries = getSelectedItems(isLocal); final searchResult = - entries.where((element) => element.name.startsWith(buffer)); + entries.where((element) => element == entry); selectedEntries.clear(); if (searchResult.isEmpty) { return; @@ -1380,18 +1384,23 @@ class _FileManagerPageState extends State height: kDesktopFileTransferHeaderHeight, child: Row( children: [ - Text( - name, - style: headerTextStyle, - overflow: TextOverflow.ellipsis, - ).marginSymmetric(horizontal: 4), - ascending.value != null + Flexible( + flex: 2, + child: Text( + name, + style: headerTextStyle, + overflow: TextOverflow.ellipsis, + ).marginSymmetric(horizontal: 4), + ), + Flexible( + flex: 1, + child: ascending.value != null ? Icon( ascending.value! ? Icons.keyboard_arrow_up_rounded : Icons.keyboard_arrow_down_rounded, ) - : const Offstage() + : const Offstage()) ], ), ), From 47a514a41670b2a4f9134219bf2854dd412c0d0f Mon Sep 17 00:00:00 2001 From: 21pages Date: Fri, 24 Feb 2023 16:20:00 +0800 Subject: [PATCH 2013/2015] optimize menubar code Signed-off-by: 21pages --- .../lib/desktop/widgets/remote_menubar.dart | 37 +++++++++++++++---- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index bdf9c1f1d..8710d27c3 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -1101,7 +1101,8 @@ class _DisplayMenuState extends State<_DisplayMenu> { await bind.sessionSetImageQuality(id: widget.id, value: value); } - return SubmenuButton( + return _SubmenuButton( + ffi: widget.ffi, child: Text(translate('Image Quality')), menuChildren: [ _RadioMenuButton( @@ -1135,7 +1136,7 @@ class _DisplayMenuState extends State<_DisplayMenu> { }, ffi: widget.ffi, ), - ].map((e) => _buildPointerTrackWidget(e, widget.ffi)).toList(), + ], ); }); } @@ -1310,7 +1311,8 @@ class _DisplayMenuState extends State<_DisplayMenu> { bind.sessionChangePreferCodec(id: widget.id); } - return SubmenuButton( + return _SubmenuButton( + ffi: widget.ffi, child: Text(translate('Codec')), menuChildren: [ _RadioMenuButton( @@ -1341,7 +1343,7 @@ class _DisplayMenuState extends State<_DisplayMenu> { onChanged: onChanged, ffi: widget.ffi, ), - ].map((e) => _buildPointerTrackWidget(e, widget.ffi)).toList()); + ]); }); } @@ -1373,7 +1375,8 @@ class _DisplayMenuState extends State<_DisplayMenu> { } } - return SubmenuButton( + return _SubmenuButton( + ffi: widget.ffi, menuChildren: resolutions .map((e) => _RadioMenuButton( value: '${e.width}x${e.height}', @@ -1381,8 +1384,6 @@ class _DisplayMenuState extends State<_DisplayMenu> { onChanged: onChanged, ffi: widget.ffi, child: Text('${e.width}x${e.height}'))) - .toList() - .map((e) => _buildPointerTrackWidget(e, widget.ffi)) .toList(), child: Text(translate("Resolution"))); } @@ -1869,6 +1870,28 @@ class _IconSubmenuButtonState extends State<_IconSubmenuButton> { } } +class _SubmenuButton extends StatelessWidget { + final List menuChildren; + final Widget? child; + final FFI ffi; + const _SubmenuButton({ + Key? key, + required this.menuChildren, + required this.child, + required this.ffi, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return SubmenuButton( + key: key, + child: child, + menuChildren: + menuChildren.map((e) => _buildPointerTrackWidget(e, ffi)).toList(), + ); + } +} + class _MenuItemButton extends StatelessWidget { final VoidCallback? onPressed; final Widget? trailingIcon; From 2a71b65a618364aaf9f30f75fc428a2fc07f8940 Mon Sep 17 00:00:00 2001 From: 21pages Date: Fri, 24 Feb 2023 19:06:37 +0800 Subject: [PATCH 2014/2015] add missing insertLock menu Signed-off-by: 21pages --- flutter/lib/desktop/widgets/remote_menubar.dart | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 8710d27c3..37bbbd66a 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -606,6 +606,7 @@ class _ControlMenu extends StatelessWidget { Divider(), ctrlAltDel(), restart(), + insertLock(), blockUserInput(), switchSides(), refresh(), @@ -789,6 +790,16 @@ class _ControlMenu extends StatelessWidget { onPressed: () => showRestartRemoteDevice(pi, id, ffi.dialogManager)); } + insertLock() { + final perms = ffi.ffiModel.permissions; + final visible = perms['keyboard'] != false; + if (!visible) return Offstage(); + return _MenuItemButton( + child: Text(translate('Insert Lock')), + ffi: ffi, + onPressed: () => bind.sessionLockScreen(id: id)); + } + blockUserInput() { final perms = ffi.ffiModel.permissions; final pi = ffi.ffiModel.pi; From c6f8df36a2018ea3a20cb692bf439ea989c6a6cc Mon Sep 17 00:00:00 2001 From: fufesou Date: Fri, 24 Feb 2023 19:55:46 +0800 Subject: [PATCH 2015/2015] update options after login Signed-off-by: fufesou --- src/server/connection.rs | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/src/server/connection.rs b/src/server/connection.rs index b2e198681..898939b62 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -126,6 +126,7 @@ pub struct Connection { origin_resolution: HashMap, voice_call_request_timestamp: Option, audio_input_device_before_voice_call: Option, + options_in_login: Option, } impl ConnInner { @@ -233,6 +234,7 @@ impl Connection { audio_sender: None, voice_call_request_timestamp: None, audio_input_device_before_voice_call: None, + options_in_login: None, }; #[cfg(not(any(target_os = "android", target_os = "ios")))] tokio::spawn(async move { @@ -921,6 +923,9 @@ impl Connection { let mut msg_out = Message::new(); msg_out.set_login_response(res); self.send(msg_out).await; + if let Some(o) = self.options_in_login.take() { + self.update_options(&o).await; + } if let Some((dir, show_hidden)) = self.file_transfer.clone() { let dir = if !dir.is_empty() && std::path::Path::new(&dir).is_dir() { &dir @@ -1106,8 +1111,7 @@ 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() { - // It may not be a good practice to update all options here. - self.update_options(o).await; + self.options_in_login = Some(o.clone()); if let Some(q) = o.video_codec_state.clone().take() { scrap::codec::Encoder::update_video_encoder( self.inner.id(), @@ -1697,7 +1701,8 @@ impl Connection { self.send_to_cm(Data::CloseVoiceCall("".to_owned())); } - async fn update_options_without_auth(&mut self, o: &OptionMessage) { + async fn update_options(&mut self, o: &OptionMessage) { + log::info!("Option update: {:?}", o); if let Ok(q) = o.image_quality.enum_value() { let image_quality; if let ImageQuality::NotSet = q { @@ -1728,12 +1733,6 @@ impl Connection { 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; @@ -1862,12 +1861,6 @@ impl Connection { } } - async fn update_options(&mut self, o: &OptionMessage) { - log::info!("Option update: {:?}", 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) { log::info!("#{} Connection closed: {}", self.inner.id(), reason); if lock && self.lock_after_session_end && self.keyboard {

    ;}!Rm*K#2_v5OK@H}M7~yz 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%0AEx@=0w+!b`I0gd+P-9i11IP9YKvNzc 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¨tz9>eCpK|o`UfvIULu3QBMjO`2y-bP!Oatu0 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 Ax&QzG 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 b3dc255d52198ca510462a80f9cb43f084f97996..d5d2c49c89429f17812a634e49dc5407b4782bb1 100644 GIT binary patch delta 1589 zcmV-52Fm%_6U7XWBYy@ANkl_h)U;t~C{JzU z=rJPqp!$sJ97BeUsCQ{Sqax6dVKeSByA`q#ZcMKaPcs^bGsymP(vAiq_tjUDW`d&i z2O6#bmYfA%e18E5FCU=II?LxK90Htq0ucWG!0k7{g400t#X#sDflN^NUckuxfV(%W z^4$vv+ST+meRsenufGC)wgcgN2XH}A`U0c&17AGg06Jn!Upw6&Ik2haZ4!6I@VG&Pjlq7gUpWwCVfJKMmDt!11n>55YZJ;_2k(KK_uuKL*GSN!GvKE04u7#1!ABo}n+ba>YJG0Ioj!h;ewHx=d!7IgKccBdBq=*C~kPd*|}5M5Gm;o%L$ zmdkw2qV+m(arUV|LG_k_i)H7vpEssySa)wY#M1`|)=iM0aR;e&I%Ir-VF|U z^g%B{{KewgNs-L1qtf7k$2%v`_rt{9B}dMlfaABjN`yfi#6KGp|!b-&v|Pg!Sg{c zK7)(vuQVLq7lNB+s|5=hv>V*?+9qBOO#xqf2`;>SspU_wAZnxY^x)px2El#wq#vV^pllPAx1(el_itwpS6+Z3)?e0mWp&;u zDB`1=JW&ezo6C@<)4>mD6sI1{9~0ee4kF; z9kj;e=MPo9_$>D_7M)>dKWQ_ri;d5d)4J6)6O$GBGvUgrWqXoZhu86I1PC9 z6^h6)QGFvhjil3#3A_tP#G6u2;Mjw}XLrsVG+g=Q9EsU`2gpA)0N)N3Yk=I(=;md= z8^ru}s1f@dH-r|LrrqsO9-==aYCoXWT43#EDiY0456kD%tptxh;y2$s%3BF4spMl`&>ow3}5iW@R)M9D>6 z8A1E!7(y(2`~1=4A~QqBYz2?Nkl7!gZjL0j-pwVU5@|3bkkJ3j zV@_r!$-H~tyvb0=cg{N}^X9+r-uZIx{nvLL@Q0Y>?&v*r3V))1`V;xoRjw9(<^O+B zBG)10XJ+E-O>ZG<_U76I9W~zVGi?z1&3X(?A_mE)20dJ<5uw)yL~GbMI_2{D>Vq} zGi|Ues~I64+U0{V#NyV)GqG+|nn6&%S+j68?KvvA!TAu{^nA`Cs=UV~ZB-9%v_QAy z(GIb5evSIsQ!~0cosLUJU_EL=M0_0gpst}F$7PHTB!4I*?!%4O0!3o*A3IaKOiz)? zKV!&+2u*5sHG+V)O@ZMz0z+beTe|~M@^2}-08C#EyzsHNY{yo>Tlb1`?Fd}F2z*%p zd~_H{-w&+M2F_imt5MhHi`j@~jeuE0g$UW{!W(*j*GPJwn*=P%1S&38-M>50AcqJG z1G+~T*MEIC0Vn0#F;X81n^5)3fE{@RzL6=MSnw$wox+MHs22{klFO`TA<{-dRrCP zM$nJi%f>p<*ee1>`9*UXPjH`lK$h~)MF}J?13t?Qd<<^bLJ&99jH#j%8(C|A=A8Fu ztFxd>8<|bw7bMOH4u5Oy8NLXZpw2R=cP3dUhpPU_Y43Zd0@!rhGy&F57E|H{L*oUC zfPV;TAX9sJikO>zw2m3x+XsAK=6!F+AwvUNMQo&O+Eaj=o|k6_xF=aAi0l5P@iK@` z*4lsijm*3+svC*Kd;bA$`3EqmA23ohV(TXQ&nHRqMgmh-SsR}lmI=Bu9=In_zuntH z%3~XV11J5I*#eGqI0a>3{Wg{{UdkUSQ*X{c*Zsi6E-;XYSBz1aNe0X)7TFn@A9 zky8jH%r`XWpc~ZMaH&1Jt-3j(0&RjG7470sv~(X&7r$w%ZV%>Q_#Pp* zG|;%`1RLMUlKJCCU{SKUEW<8Cx_<%dvi17VB8X0?3jX>*a6=HDcxk7QG)&D{Ww0)2 z1kt6SLu!U|GKH7!)Eh(~pmuv|ix`|^&1G-yqW^qlf(CY0lUpT*;Bgy#ARR2*1i^2`Y$dPN3_AjVqEaP+j{8iWD|PpQeN{0>hj6BMoBDAx7fG3;swR71X{ znps(8g6R905!60}MZyuy)PLl3s#h(B5Nea^vEG z8EfixnGNd!{IIFHTyBwmu1uMr?FZH5n7F$;0r+Q@-axE^rp434)qiANiod}V$^?Cp zZ&<5Q3UL<($?J+BXDrZ1(UG>|Cu#-E7R!%LyUkpV4k2UL43q~Gw7X{Ct|m(#al=0S zKC}p8vC_;T=JpW5Y0ngL<`Mltsp93kM0+U&@rZZ-?Y|zfHfRw zoxu3M>TTBU6)k++T7TLE(T-S>Wn?&1K&{5wq2InMGwNSPGSMpHqonU~Kti;7n+uFt z2atZUlzRTnJ7RluS5H4Pm(H5S-wiTSz^vZTL8sP6Jz`3tdVkW=4JwK~8WBWaSR#lq z;0J}I)%WRzIC{$rSU=e?7uE|&D|Q3hYEixP`&bT4HBR@1RUW$d@UUpzjr#38VY&Q; z`K8pAM$%5FV3VLNl{&zKHmjUo#aSlK!<)>?a-1CcGn6ez5^*IBW& zr6w>P?ObXk5bQv}1kuJACsTRlL?Ee`wP*MioQGQ+6&+-f^YIaD?Q4Q9Wxxz$j#_G% z{j{`o_atAPqlXj4hKrFV|K;S~ zMgbG6M1R~dCC-8+JH6i(Bk0P|-|KYAQs;YN%%^Y5F$zNWo0JgSh{}Tn7-zGnf7HMw z#FyrH5$&28DI$hIeU}%(85J|f2!x$35u>lrz9LXf9CK725yMkZ>JVH)X3L1?gKkZ= z?KqX4?ZDTYR#YAlQ;+I6{>e56vSx3sJU@p=#DCPo6BWo_w$jjIJY0z9=hUMLC-crVfa0H;z%9EFPxDe0JxpGduGed>!IV)vZ zJxS`GIc`*UHA)IfdGZnu7ecq>QRwO%#Zyq^8|E)9y!&@qf@d}yqn#tFe7k6?<=l{K a68s+yM6z@EpozEu0000u7N2n24Gwreoowie@h^Qvy>&o3Bqqg3{^4zkk%dx|{xCs%ZWBp$HWt zq-^sY08>WxeoX~Eu%;ELdwG|Ty$xW}crA2ymC^gU{(ICuJ|bWd-d-e7N1)7XVE#$q z{#$@pe$GD4#>)Wl@)NNB5>R&u5UQIV31xu*wy3iNxbqq=3D<+t$?vXs_Z1khLqSA` zK=_`rR^1VApnpsocL3aSJ|iPQ^*8NSB{1riMb-hP20Ey?=mPp1h~tNs~aZ!DMC$|m`VE&*SIQUOxWv`x zU@1D?OM$j)!P0z{o~c0nrC^zM#7lt#x8ai4pMj8?<}x9=()HL+SzUlG{NuMm%_zkD zlVFL_Pk&J$;~22iTC7)~)oQThnWQLCZ2?$vOwcP(Y&saI9Et*sSAZq$NUZ`?AuNrS zD+)AR4wkIrvFC)e*?k0crlQjdI**bqxBR%^?yjY4sgYxJ6?{&JvM`7$r(KYi%)~4 z!+I|TXhHtl0+-O=PQcJze7h&;eyK-jTvARw0*}xGPorko9{gSs&3SIVghwVF(!3_7 zu77OrEg0hS51{u}`}~7<0$+Z@5W8*w!MbP`pa|Wawo8m}kI2;;?GMS{jd1QU<@3Rf zt%QStNAF;WSp8+5l*3^NS_8)!;0s%eQTtF2;kj1h%dJHu9s;*eH#+keJtMxnJ6&Zy z@cbiKNK|eNxZh7IGR^zlX{%~xYOih90)N+Ez>Zg+?Ym79wePXy)_jmo8qSdE5Lw5R zAo(ykEpES#ezyN#is7{TET`yl+g)3{r6blYTmlmudLIyUd~aHTeB-+ddiQ{i?@d?u zHea8KaET%bg5Eu#<9ibcd=|-TWfyiND4mv$?@dwj$!p~hE}?f1==i?w<(+hh>?Ds~ lw|Q%8<_D?G%VzT*e*xoIp`bG{E5iT)002ovPDHLkV1l_J2HOAt delta 1665 zcmV-{27dX!2$2ntBYy^1Nkl>Nh>9Y0&RuVS ze7oO%_cDY0X1;;BXLrBz+dX^soU@;S$}oa}$O%KxZR%8n^?!PpehHz+AGM2r>2bl8 zn~l7*bmV-Rj?znI9-;Anv*uAPFnrzTa6}HS4J{S$15czQG9Iz>;t@Uh1$>h<8io7L zuuu?a9u}&3gPrq)-oP6}fcB26vg97Xb6*0NZvagLg@5vl7PJaabrsLY3BZCKziFrqw!f&Kpg3qgJx#a6!KX?>UKF4US%8r zQg>JVz4v_JnZ9CkcMq5MXLDPc3=~`fw&&|;rb%E)5*>OitNdnpx;Npx{1JJ zoz2^XHwB*UPp)>m^>~&9UXGK?zRM#wxYE8Kzj8y6{*1g?N>B4PFH(TBkj{y63%8^s z5Jm@2LRWLy=}X=z@4>Cxz{GT*M{7y~!>Y^KX$+14emJTgM@b+#h8hHz%ih`Idnmvp z_(9vCQ!6Y2Zegxqr+9 z8Pm2?1A#MbNylK{z?LCC^MQ94+NNGVVs;S-UaN?OgzlMrjrt5 zE@SiIMu!AdG%E%um_g;z?>{# zWHhZ`v{9G*0zChaT%m=d zL@dK5#ep6tEO&$JK;tQSnehmPDT?%zvFK<0I+0dR{8FaKVdFZ=+wf*zmMki6oGVnG z!MU`LW)1*?G*0l%$H+l!(9udjivYW_5vjoL89;PPb=!vZg^7H)O(Z-a6o2QxI@r3v z#o*N*xg039aGjN*T^Hg46_eOD9cUA*Zp+HD$uI$~O6w?p;({38ZkA`OGRI+bulc|N zGLxhpYQ^f^3D9odR*zX;4y5h@W^DwD6!Dh~writY;~%?Pl*f7VhBY>C=gHWLO*XU# zJR#H8k|#T6SBnZT0S;I-7Js=a*xPK%1t#?arpE%ED;27^tmwJS;L_sx;;KaV{wYf9 z8T;wLD)(M_)}0A(e&y9I4cjZ%Zuns@)w7MTd9D|EP(}*IEfS@8wsThgv?9{Ai6R58 z+&yqa4)t{;j$ZR5kz?+8@zgkq@(&xxOUpp)Jf8{F3i3C8Yq-BX;CkzOz5%aGD7$_R z2Ny0i+#gQxw+DQE&o|(810~3sl7P!6T*8g=hZD}Ecnr&Mx^KYmW$?^rO&Nu}HSSx8 zigVzk^ALY~z}NSPNE`!4G}n#!ChT7L+@^>&>=Cm0O>8zi^B&=U0Z`KbR^3+f00000 LNkvXXu0mjfpU^%E diff --git a/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png index 8a4e80f53f5219178e79be11ecc88c11c36cd126..41ccba607c6c0a5c84d367341ca562d2e8b5e264 100644 GIT binary patch delta 2085 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 FV1jHnFi`*i diff --git a/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index 8bb8d570f0dd7a72fdddc94020282353a6f3375f..c10349d71712b736a1bef9ef76ec05092b514272 100644 GIT binary patch delta 3008 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 N002ovPDHLkV1m#B3G)B| literal 6268 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

    );oKl6MY=aqIYxj|N9+zxgN5e}0cT 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 z%0A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